Article

网络音频 Web Audio API 入门手册

在 HTML5 <audio>元素之前,需要 Flash 或其他插件才能打破网络的寂静。尽管网络音频不再需要插件,但音频标签为实现复杂的游戏和交互式应用程序带来了明显的限制。

Web Audio API 是高级 JavaScript API,用于处理和合成 Web 应用程序中的音频。该 API 的目标是包括现代游戏音频引擎中的功能以及现代桌面音频制作应用程序中的某些混合,处理和过滤任务。接下来是对使用此强大 API 的简要介绍。

AudioContext

一个 AudioContext 是用于管理和播放所有声音。要使用 Web Audio API 产生声音,请创建一个或多个声音源,并将它们连接到 AudioContext 实例提供的声音目标。该连接不一定是直接的,可以通过任意数量的中间 AudioNode 充当音频信号的处理模块。该路由更详细的网络音频描述规范。

单个实例 AudioContext 可以支持多个声音输入和复杂的音频图,因此对于我们创建的每个音频应用程序,我们只需要其中之一即可。许多有趣的 Web Audio API 功能(例如,创建 AudioNode 和解码音频文件数据)都是方法 AudioContext。

以下代码段创建了一个 AudioContext:

var context;
window.addEventListener('load', init, false);
function init() {
  try {
    // Fix up for prefixing
    window.AudioContext = window.AudioContext||window.webkitAudioContext;
    context = new AudioContext();
  }
  catch(e) {
    alert('Web Audio API is not supported in this browser');
  }
}

对于基于 WebKit 和 Blink 的浏览器,目前需要使用 webkit 前缀,即 webkitAudioContext。

载入声音

Web Audio API 使用 AudioBuffer 来播放中短长度的声音。基本方法是使用 XMLHttpRequest 来获取声音文件。该 API 支持以多种格式加载音频文件数据,例如 WAV,MP3,AAC,OGG 等。浏览器对不同音频格式的支持各不相同。

以下代码片段演示了如何加载声音样本:

var dogBarkingBuffer = null;
// Fix up prefixing
window.AudioContext = window.AudioContext || window.webkitAudioContext;
var context = new AudioContext();

function loadDogSound(url) {
  var request = new XMLHttpRequest();
  request.open('GET', url, true);
  request.responseType = 'arraybuffer';

  // Decode asynchronously
  request.onload = function() {
    context.decodeAudioData(request.response, function(buffer) {
      dogBarkingBuffer = buffer;
    }, onError);
  }
  request.send();
}

音频文件数据是二进制文件(不是文本文件),因此我们将 responseType 请求的设置为'arraybuffer'

接收到(未编码的)音频文件数据后,可以将其保留以备以后解码,也可以使用AudioContextdecodeAudioData()方法立即对其进行解码。此方法获取 ArrayBuffer 存储在其中的音频文件数据request.response并异步解码(不阻止 JavaScript 主执行线程)。

decodeAudioData()完成时,它调用的回调函数,其提供经解码的 PCM 音频数据作为一个 AudioBuffer。

播放声音

Getting Started with Web Audio API A

一旦 AudioBuffers 加载了一个或多个,我们就可以播放声音了。假设我们刚刚加载 AudioBuffer 了狗叫声,并且加载完成。然后,我们可以使用以下代码播放此缓冲区。

// Fix up prefixing
window.AudioContext = window.AudioContext || window.webkitAudioContext;
var context = new AudioContext();

function playSound(buffer) {
  var source = context.createBufferSource(); // creates a sound source
  source.buffer = buffer;                    // tell the source which sound to play
  source.connect(context.destination);       // connect the source to the context's destination (the speakers)
  source.start(0);                           // play the source now
                                             // note: on older systems, may have to use deprecated noteOn(time);
}

playSound()每当有人按下键或用鼠标单击某些东西时,都可以调用此功能。

start(time)功能使您可以轻松地为游戏和其他时间紧迫的应用安排精确的声音播放。但是,要使此调度正常工作,请确保已预先加载声音缓冲区。(在较旧的系统上,您可能需要致电noteOn(time)而不是start(time)。)

需要注意的重要一点是,在 iOS 上,Apple 当前会将所有声音输出静音,直到在用户交互事件(例如,playSound()在触摸事件处理程序内部调用)期间首次播放声音为止。除非您避免这种情况,否则您可能会在 iOS 上的 Web Audio 上“无法正常工作”-为避免此类问题,请在早期的 UI 事件中播放声音(甚至可以通过连接到具有零增益的增益节点将其静音)。

抽象 Web 音频 API

当然,最好创建一个更通用的加载系统,该系统没有硬编码来加载此特定声音。处理音频应用程序或游戏将使用的许多中短音的方法有很多,以下是如何使用 BufferLoader 类的示例。让我们创建两个 AudioBuffers,并且,一旦加载它们,就让我们同时播放它们。

window.onload = init;
var context;
var bufferLoader;

function init() {
  // Fix up prefixing
  window.AudioContext = window.AudioContext || window.webkitAudioContext;
  context = new AudioContext();

  bufferLoader = new BufferLoader(
    context,
    [
      '../sounds/hyper-reality/br-jam-loop.wav',
      '../sounds/hyper-reality/laughter.wav',
    ],
    finishedLoading
    );

  bufferLoader.load();
}

function finishedLoading(bufferList) {
  // Create two sources and play them both together.
  var source1 = context.createBufferSource();
  var source2 = context.createBufferSource();
  source1.buffer = bufferList[0];
  source2.buffer = bufferList[1];

  source1.connect(context.destination);
  source2.connect(context.destination);
  source1.start(0);
  source2.start(0);
}

以节奏播放声音

Web Audio API 使开发人员可以精确地安排播放时间。为了演示这一点,让我们建立一个简单的节奏轨道。可能最广为人知的鼓组模式如下:

Getting Started with Web Audio API B

其中,每八分音符会演奏一次踩 hat,而四分之一的时间则每四分之一交替演奏踢鼓和圈套器。

假设我们已经加载 kick,snare 和 hihat 缓冲区,代码要做到这一点很简单:

for (var bar = 0; bar < 2; bar++) {
  var time = startTime + bar * 8 * eighthNoteTime;
  // Play the bass (kick) drum on beats 1, 5
  playSound(kick, time);
  playSound(kick, time + 4 * eighthNoteTime);

  // Play the snare drum on beats 3, 7
  playSound(snare, time + 2 * eighthNoteTime);
  playSound(snare, time + 6 * eighthNoteTime);

  // Play the hi-hat every eighth note.
  for (var i = 0; i < 8; ++i) {
    playSound(hihat, time + i * eighthNoteTime);
  }
}

在这里,我们只重复一遍,而不是在乐谱中看到无限循环。该函数 playSound 是一种在指定时间播放缓冲区的方法,如下所示:

function playSound(buffer, time) {
  var source = context.createBufferSource();
  source.buffer = buffer;
  source.connect(context.destination);
  source.start(time);
}

本部分完整代码

var RhythmSample = {
};

RhythmSample.play = function() {
  function playSound(buffer, time) {
    var source = context.createBufferSource();
    source.buffer = buffer;
    source.connect(context.destination);
    if (!source.start)
      source.start = source.noteOn;
    source.start(time);
  }

  var kick = BUFFERS.kick;
  var snare = BUFFERS.snare;
  var hihat = BUFFERS.hihat;

  // We'll start playing the rhythm 100 milliseconds from "now"
  var startTime = context.currentTime + 0.100;
  var tempo = 80; // BPM (beats per minute)
  var eighthNoteTime = (60 / tempo) / 2;

  // Play 2 bars of the following:
  for (var bar = 0; bar < 2; bar++) {
    var time = startTime + bar * 8 * eighthNoteTime;
    // Play the bass (kick) drum on beats 1, 5
    playSound(kick, time);
    playSound(kick, time + 4 * eighthNoteTime);

    // Play the snare drum on beats 3, 7
    playSound(snare, time + 2 * eighthNoteTime);
    playSound(snare, time + 6 * eighthNoteTime);

    // Play the hi-hat every eighthh note.
    for (var i = 0; i < 8; ++i) {
      playSound(hihat, time + i * eighthNoteTime);
    }
  }
};

改变音量

您可能要对声音执行的最基本的操作之一就是更改音量。使用 Web Audio API,我们可以通过 GainNode 将源路由到其目的地,以便控制音量:

Getting Started with Web Audio API C

可以通过以下方式实现此连接设置:

// Create a gain node.
var gainNode = context.createGain();
// Connect the source to the gain node.
source.connect(gainNode);
// Connect the gain node to the destination.
gainNode.connect(context.destination);

设置完图表后,您可以通过gainNode.gain.value以编程方式更改音量:

// Reduce the volume.
gainNode.gain.value = 0.5;

本部分完整代码

var VolumeSample = {
};

// Gain node needs to be mutated by volume control.
VolumeSample.gainNode = null;

VolumeSample.play = function() {
  if (!context.createGain)
    context.createGain = context.createGainNode;
  this.gainNode = context.createGain();
  var source = context.createBufferSource();
  source.buffer = BUFFERS.techno;

  // Connect source to a gain node
  source.connect(this.gainNode);
  // Connect gain node to destination
  this.gainNode.connect(context.destination);
  // Start playback in a loop
  source.loop = true;
  if (!source.start)
    source.start = source.noteOn;
  source.start(0);
  this.source = source;
};

VolumeSample.changeVolume = function(element) {
  var volume = element.value;
  var fraction = parseInt(element.value) / parseInt(element.max);
  // Let's use an x*x curve (x-squared) since simple linear (x) does not
  // sound as good.
  this.gainNode.gain.value = fraction * fraction;
};

VolumeSample.stop = function() {
  if (!this.source.stop)
    this.source.stop = source.noteOff;
  this.source.stop(0);
};

VolumeSample.toggle = function() {
  this.playing ? this.stop() : this.play();
  this.playing = !this.playing;
};

声音之间淡入淡出

现在,假设我们有一个稍微复杂的场景,我们正在播放多种声音,但希望在它们之间进行淡入淡出。这在类似 DJ 的应用程序中很常见,在该应用程序中,我们有两个转盘,希望能够从一个声源平移到另一个声源。

可以使用以下音频图完成此操作:

Getting Started with Web Audio API D

要进行设置,我们只需创建两个 GainNodes,然后使用类似此功能的东西通过节点连接每个源:

function createSource(buffer) {
  var source = context.createBufferSource();
  // Create a gain node.
  var gainNode = context.createGain();
  source.buffer = buffer;
  // Turn on looping.
  source.loop = true;
  // Connect source to gain.
  source.connect(gainNode);
  // Connect gain to destination.
  gainNode.connect(context.destination);

  return {
    source: source,
    gainNode: gainNode
  };
}

等功率交叉淡入淡出

当您在样本之间平移时,幼稚的线性交叉淡入淡出方法会显示出体积下降。

Getting Started with Web Audio API E

为了解决这个问题,我们使用了一条相等的功率曲线,其中相应的增益曲线是非线性的,并且以更高的幅度相交。这样可以最大程度地减少音频区域之间的音量下降,从而使水平之间可能略有不同的区域之间的淡入淡出。

Getting Started with Web Audio API F

本部分完整代码

var CrossfadeSample = {playing:false};

CrossfadeSample.play = function() {
  // Create two sources.
  this.ctl1 = createSource(BUFFERS.drums);
  this.ctl2 = createSource(BUFFERS.organ);
  // Mute the second source.
  this.ctl1.gainNode.gain.value = 0;
  // Start playback in a loop
  if (!this.ctl1.source.start) {
    this.ctl1.source.noteOn(0);
    this.ctl2.source.noteOn(0);
  } else {
    this.ctl1.source.start(0);
    this.ctl2.source.start(0);
  }

  function createSource(buffer) {
    var source = context.createBufferSource();
    var gainNode = context.createGain ? context.createGain() : context.createGainNode();
    source.buffer = buffer;
    // Turn on looping
    source.loop = true;
    // Connect source to gain.
    source.connect(gainNode);
    // Connect gain to destination.
    gainNode.connect(context.destination);

    return {
      source: source,
      gainNode: gainNode
    };
  }
};

CrossfadeSample.stop = function() {
  if (!this.ctl1.source.stop) {
    this.ctl1.source.noteOff(0);
    this.ctl2.source.noteOff(0);
  } else {
    this.ctl1.source.stop(0);
    this.ctl2.source.stop(0);
  }
};

// Fades between 0 (all source 1) and 1 (all source 2)
CrossfadeSample.crossfade = function(element) {
  var x = parseInt(element.value) / parseInt(element.max);
  // Use an equal-power crossfading curve:
  var gain1 = Math.cos(x * 0.5*Math.PI);
  var gain2 = Math.cos((1.0 - x) * 0.5*Math.PI);
  this.ctl1.gainNode.gain.value = gain1;
  this.ctl2.gainNode.gain.value = gain2;
};

CrossfadeSample.toggle = function() {
  this.playing ? this.stop() : this.play();
  this.playing = !this.playing;
};

播放列表淡入淡出

另一个常见的交叉渐变器应用程序是音乐播放器应用程序。当歌曲改变时,我们希望淡出当前曲目,并淡入新曲目,以免产生刺耳的过渡。为此,请在将来安排交叉淡入淡出。尽管我们可以setTimeout用来执行此调度,但这并不精确。借助 Web Audio API,我们可以使用 AudioParam 接口来安排参数的未来值,例如的增益值 GainNode。

因此,给定一个播放列表,我们可以通过安排当前播放曲目的增益减小和下一首曲目的增益增大来在曲目之间进行转换,两者都在当前曲目结束播放之前:

function playHelper(bufferNow, bufferLater) {
  var playNow = createSource(bufferNow);
  var source = playNow.source;
  var gainNode = playNow.gainNode;
  var duration = bufferNow.duration;
  var currTime = context.currentTime;
  // Fade the playNow track in.
  gainNode.gain.linearRampToValueAtTime(0, currTime);
  gainNode.gain.linearRampToValueAtTime(1, currTime + ctx.FADE_TIME);
  // Play the playNow track.
  source.start(0);
  // At the end of the track, fade it out.
  gainNode.gain.linearRampToValueAtTime(1, currTime + duration-ctx.FADE_TIME);
  gainNode.gain.linearRampToValueAtTime(0, currTime + duration);
  // Schedule a recursive track change with the tracks swapped.
  var recurse = arguments.callee;
  ctx.timer = setTimeout(function() {
    recurse(bufferLater, bufferNow);
  }, (duration - ctx.FADE_TIME) * 1000);
}

Web Audio API 提供了一组方便的 RampToValue 方法来逐渐更改参数的值,例如 linearRampToValueAtTime 和 exponentialRampToValueAtTime。

虽然可以从内置的线性和指数函数中选择过渡定时函数(如上所述),但是您也可以使用 setValueCurveAtTime 函数通过一组值来指定自己的值曲线。

本部分完整代码

var CrossfadePlaylistSample = {
  FADE_TIME: 1, // Seconds
  playing: false
};

CrossfadePlaylistSample.play = function() {
  var ctx = this;
  playHelper(BUFFERS.jam, BUFFERS.crowd);

  function createSource(buffer) {
    var source = context.createBufferSource();
    var gainNode = context.createGain ? context.createGain() : context.createGainNode();
    source.buffer = buffer;
    // Connect source to gain.
    source.connect(gainNode);
    // Connect gain to destination.
    gainNode.connect(context.destination);

    return {
      source: source,
      gainNode: gainNode
    };
  }

  function playHelper(bufferNow, bufferLater) {
    var playNow = createSource(bufferNow);
    var source = playNow.source;
    ctx.source = source;
    var gainNode = playNow.gainNode;
    var duration = bufferNow.duration;
    var currTime = context.currentTime;
    // Fade the playNow track in.
    gainNode.gain.linearRampToValueAtTime(0, currTime);
    gainNode.gain.linearRampToValueAtTime(1, currTime + ctx.FADE_TIME);
    // Play the playNow track.
    source.start ? source.start(0) : source.noteOn(0);
    // At the end of the track, fade it out.
    gainNode.gain.linearRampToValueAtTime(1, currTime + duration-ctx.FADE_TIME);
    gainNode.gain.linearRampToValueAtTime(0, currTime + duration);
    // Schedule a recursive track change with the tracks swapped.
    var recurse = arguments.callee;
    ctx.timer = setTimeout(function() {
      recurse(bufferLater, bufferNow);
    }, (duration - ctx.FADE_TIME) * 1000);
  }

};

CrossfadePlaylistSample.stop = function() {
  clearTimeout(this.timer);
  this.source.stop ? this.source.stop(0) : this.source.noteOff(0);
};

CrossfadePlaylistSample.toggle = function() {
  this.playing ? this.stop() : this.play();
  this.playing = !this.playing;
};

对声音应用简单的滤镜效果

Getting Started with Web Audio API G

Web Audio API 使您可以将声音从一个音频节点传输到另一个音频节点,从而创建可能复杂的处理器链,以向您的声音形式添加复杂的效果。

一种实现方法是将 BiquadFilterNode 放置在声音源和目标之间。这种类型的音频节点可以执行各种低阶滤波器,这些低阶滤波器可用于构建图形均衡器甚至更复杂的效果,这主要与选择声音的频谱的哪些部分要强调和哪些要服从有关。

支持的过滤器类型包括:

  • 低通滤波器
  • 高通滤波器
  • 带通滤波器
  • 低架过滤器
  • 高架过滤器
  • 峰值过滤器
  • 陷波滤波器
  • 全通滤波器

所有的滤波器都包含用于指定一定数量的 增益,应用滤波器的频率以及品质因数的参数。低通滤波器保持较低的频率范围,但会丢弃高频。折断点由频率值确定,Q 因子为无单位,并确定曲线图的形状。增益仅影响某些滤波器,例如低架滤波器和峰值滤波器,而不影响该低通滤波器。

让我们设置一个简单的低通滤波器,以仅从声音样本中提取基数:

// Create the filter
var filter = context.createBiquadFilter();
// Create the audio graph.
source.connect(filter);
filter.connect(context.destination);
// Create and specify parameters for the low-pass filter.
filter.type = 'lowpass'; // Low-pass filter. See BiquadFilterNode docs
filter.frequency.value = 440; // Set cutoff to 440 HZ
// Playback the sound.
source.start(0);

通常,需要调整频率控制以达到对数刻度,因为人类的听觉本身按照相同的原理工作(即 A4 为 440hz,A5 为 880hz)。有关更多详细信息,请参见 FilterSample.changeFrequency 上面的源代码链接中的函数。

本部分完整代码

var FilterSample = {
  FREQ_MUL: 7000,
  QUAL_MUL: 30,
  playing: false
};

FilterSample.play = function() {
  // Create the source.
  var source = context.createBufferSource();
  source.buffer = BUFFERS.techno;
  // Create the filter.
  var filter = context.createBiquadFilter();
  //filter.type is defined as string type in the latest API. But this is defined as number type in old API.
  filter.type = (typeof filter.type === 'string') ? 'lowpass' : 0; // LOWPASS
  filter.frequency.value = 5000;
  // Connect source to filter, filter to destination.
  source.connect(filter);
  filter.connect(context.destination);
  // Play!
  if (!source.start)
    source.start = source.noteOn;
  source.start(0);
  source.loop = true;
  // Save source and filterNode for later access.
  this.source = source;
  this.filter = filter;
};

FilterSample.stop = function() {
  if (!this.source.stop)
    this.source.stop = source.noteOff;
  this.source.stop(0);
  this.source.noteOff(0);
};

FilterSample.toggle = function() {
  this.playing ? this.stop() : this.play();
  this.playing = !this.playing;
};

FilterSample.changeFrequency = function(element) {
  // Clamp the frequency between the minimum value (40 Hz) and half of the
  // sampling rate.
  var minValue = 40;
  var maxValue = context.sampleRate / 2;
  // Logarithm (base 2) to compute how many octaves fall in the range.
  var numberOfOctaves = Math.log(maxValue / minValue) / Math.LN2;
  // Compute a multiplier from 0 to 1 based on an exponential scale.
  var multiplier = Math.pow(2, numberOfOctaves * (element.value - 1.0));
  // Get back to the frequency value between min and max.
  this.filter.frequency.value = maxValue * multiplier;
};

FilterSample.changeQuality = function(element) {
  this.filter.Q.value = element.value * this.QUAL_MUL;
};

FilterSample.toggleFilter = function(element) {
  this.source.disconnect(0);
  this.filter.disconnect(0);
  // Check if we want to enable the filter.
  if (element.checked) {
    // Connect through the filter.
    this.source.connect(this.filter);
    this.filter.connect(context.destination);
  } else {
    // Otherwise, connect directly.
    this.source.connect(context.destination);
  }
};

最后,请注意,示例代码使您可以连接和断开过滤器,从而动态更改 AudioContext 图。我们可以通过调用断开 AudioNode 与图的连接 node.disconnect(outputNumber)。例如,要将图形从通过过滤器重新路由到直接连接,我们可以执行以下操作:

// Disconnect the source and filter.
source.disconnect(0);
filter.disconnect(0);
// Connect the source directly.
source.connect(context.destination);

进一步实践

我们已经介绍了 API 的基础知识,包括加载和播放音频样本。我们构建了带有增益节点和滤波器的音频图,并安排了声音和音频参数调整以启用一些常见的声音效果。此时,您就可以开始构建一些不错的 Web 音频应用程序了!

如果您正在寻找灵感,许多开发人员已经 使用 Web Audio API 进行了出色的工作。我最喜欢的一些东西包括:

AudioJedit,一个浏览器中的声音拼接工具,使用 SoundCloud 永久链接。

ToneCraft,一种音序器,通过堆叠 3D 块创建声音。

Plink,一款使用 Web Audio 和 Web Sockets 的协作音乐制作游戏。