web-audio网络音频入门


备注

Web Audio API是一种W3C标准,通过高级API,可以比标准<audio> -tag更低级别地访问音频系统。

用例包括游戏,艺术,音频合成,交互式应用程序,音频制作以及需要对音频数据进行细粒度控制的任何应用程序。

API可以接受来自多个源的输入,包括加载音频文件并通过API或<audio> -elements对其进行解码。它还提供通过使用振荡器节点直接通过API生成声音的工具。

还有许多处理节点,例如增益,延迟和脚本处理器(最终将被弃用并由更高效的节点替换)。这些可以反过来用于构建更复杂的效果和音频图形。

播放音频

要使用Web Audio API播放音频,我们需要获取音频数据的ArrayBuffer并将其传递给BufferSource进行播放。

要获取要播放的声音的音频缓冲区,您需要使用AudioContext.decodeAudioData 方法,如下所示:

const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
// Fetch the MP3 file from the server
fetch("sound/track.mp3")
    // Return the data as an ArrayBuffer
    .then(response => response.arrayBuffer())
    // Decode the audio data
    .then(buffer => audioCtx.decodeAudioData(buffer))
    .then(decodedData => {
        // ...
    });
 

当最终承诺解决时,您将以AudioBuffer 的形式获得音频。然后可以将其附加到AudioBufferSourceNode - 并播放 - 如下所示:

const source = context.createBufferSource();
source.buffer = decodedData; // <==
source.start(...);
 

其中.start() 有三个参数可以偏移何时播放样本,偏移样本中的位置,并分别表示播放样本的时间。

有关如何操作缓冲区源的更多信息可以在MDN上找到。

实时更改两个音频源

此示例显示如何使用两个音频源,并根据另一个更改其中一个音频源。在这种情况下,我们创建一个音频Ducker,如果辅助音轨产生声音,它将降低主音轨的音量。

ScriptProcessorNode将定期事件发送到其audioprocess处理程序。在这个链接到辅助音频源的处理程序中,我们计算音频的“响度”,并用它来改变主音频源上的动态压缩器。然后将两者发送到用户的扬声器/耳机。结果是当在次要音频轨道中检测到声音时主要音频轨道中的音量变化非常突然。我们可以通过使用平均值来使这更平滑,并且在检测到辅助音频之前使用延迟线来改变音量,但是在该示例中该过程应该是清楚的。

//The DuckingNode will heavily compress the primary source when the secondary source produces sound
class DuckingNode {
  constructor(context) {
    let blocksize = 2048;
    let normalThreshold = -50;
    let duckThreshold = 0.15;
  
    //Creating nodes
    this.compressor = context.createDynamicsCompressor();
    this.processor = context.createScriptProcessor(blocksize, 2, 2);
    
    //Configuring nodes
    this.compressor.threshold.value = normalThreshold;
    this.compressor.knee.value = 0;
    this.compressor.ratio.value = 12;
    this.compressor.reduction.value = -20;
    this.compressor.attack.value = 0;
    this.compressor.release.value = 1.5;
    
    let self = this;
    this.processor.onaudioprocess = function(audioProcessingEvent) {
      let inputBuffer = audioProcessingEvent.inputBuffer;
      let outputBuffer = audioProcessingEvent.outputBuffer;
      let rms;
      let total = 0.0;
      let len = blocksize * outputBuffer.numberOfChannels;

      for (let channel = 0; channel < outputBuffer.numberOfChannels; channel++) {
        let inputData = inputBuffer.getChannelData(channel);
        let outputData = outputBuffer.getChannelData(channel);

        for (let sample = 0; sample < inputBuffer.length; sample++) {
          // make output equal to the same as the input
          outputData[sample] = inputData[sample];
          total += Math.abs(inputData[sample]);
        }
      }
      
      //Root mean square
      rms = Math.sqrt( total / len );

      //We set the threshold to at least 'normalThreshold'
      self.compressor.threshold.value = normalThreshold + Math.min(rms - duckThreshold, 0) * 5 * normalThreshold;
    }
  }
  
  get primaryInput () {
    return this.compressor;
  }
  
  get secondaryInput () {
    return this.processor;
  }
  
  connectPrimary(node) {
    this.compressor.connect(node);
  }
  
  connectSecondary(node) {
    this.processor.connect(node);
  }
};
 
let audioContext = new (window.AudioContext || window.webkitAudioContext)();

//Select two <audio> elements on the page. Ideally they have the autoplay attribute
let musicElement = document.getElementById("music");
let voiceoverElement = document.getElementById("voiceover");

//We create source nodes from them
let musicSourceNode = audioContext.createMediaElementSource(musicElement);
let voiceoverSourceNode = audioContext.createMediaElementSource(voiceoverElement);

let duckingNode = new DuckingNode(audioContext);

//Connect everything up
musicSourceNode.connect(duckingNode.primaryInput);
voiceoverSourceNode.connect(duckingNode.secondaryInput);
duckingNode.connectPrimary(audioContext.destination);
duckingNode.connectSecondary(audioContext.destination);
 

这个例子的一部分来自Kevin Ennis的 回答mdn的代码。

录制麦克风源的音频

要从用户的麦克风录制音频,我们必须首先获得用户访问设备的许可:

navigator.mediaDevices.getUserMedia({ audio: true })
    .then(successCallback)
    .catch(failureCallback);
 

成功之后,我们将使用MediaStream 对象调用successCallback ,我们可以使用它来访问麦克风:

var audioContext = new (window.AudioContext || window.webkitAudioContext)();
// Create a source from our MediaStream
var source = audioContext.createMediaStreamSource(mediaStream);
// Now create a Javascript processing node with the following parameters:
// 4096 = bufferSize (See notes below)
// 2 = numberOfInputChannels (i.e. Stereo input)
// 2 = numberOfOutputChannels (i.e. Stereo output)
var node = audioContext.createScriptProcessor(4096, 2, 2);
node.onaudioprocess = function(data) {
    console.log(data);
}
// Connect the microphone to the script processor
source.connect(node);
node.connect(audioContext.destination);
 

onaudioprocess 事件使我们能够从麦克风访问Raw PCM数据流。我们可以像这样访问缓冲数据:

node.onaudioprocess = function(data) {
    var leftChannel = data.inputBuffer.getChannelData(0).buffer;
    var rightChannel = data.inputBuffer.getChannelData(1).buffer;
}
 

当我们完成录制时,我们必须断开与源的连接并丢弃脚本处理器:

node.disconnect();
source.disconnect();
 

有关bufferSize 重要说明

bufferSize 确定调用onaudioprocess 回调的频率。较低的值会导致较低(较好)的延迟,较高的值会减少音频分解/毛刺。值为零将允许浏览器实现选择适当的值。如果手动传入,则必须是以下值之一:256,512,1024,2048,4096,8192,16384。

建立

我们首先创建一个音频上下文,然后创建一个振荡器,这是检查您的设置是否有效的最简单方法。 (示例小提琴)

// We can either use the standard AudioContext or the webkitAudioContext (Safari)
var audioContext = (window.AudioContext || window.webkitAudioContext);

// We create a context from which we will create all web audio nodes
var context = new audioContext();

// Create an oscillator and make some noise
var osc = context.createOscillator();

// set a frequecy, in this case 440Hz which is an A note
osc.frequency.value = 440;

// connect the oscillator to the context destination (which routes to your speakers)
osc.connect(context.destination);

// start the sound right away
osc.start(context.currentTime);

// stop the sound in a second
osc.stop(context.currentTime + 1);
 

合成音频

在此示例中,我们将展示如何生成简单的正弦波,并将其输出到用户的扬声器/耳机上。

let audioContext = new (window.AudioContext || window.webkitAudioContext)();

let sourceNode = audioContext.createOscillator();
sourceNode.type = 'sine';
sourceNode.frequency.value = 261.6;
sourceNode.detune.value = 0;

//Connect the source to the speakers
sourceNode.connect(audioContext.destination);

//Make the sound audible for 100 ms
sourceNode.start();
window.setTimeout(function() { sourceNode.stop(); }, 100);
 

startstop 的方法sourceNode 上述每个变量有一个可选的参数when ,指定多少启动或停止之前的等待。

因此,停止声音的另一种方法是:

sourceNode.start();
sourceNode.stop(0.1);
 

振荡器节点的type 参数可以设置为以下任何值:

  • 正弦(默认)
  • 广场
  • 锯齿
  • 黄金三角
  • 自定义波

自定义波是PeriodicWaves ,可以使用AudioContext.createPeriodicWave 方法创建。

使用音频效果

通过链接源节点和目标节点之间的节点,可以将效果应用于音频。在这个例子中,我们使用增益节点来静音源,并且只在特定时间发出声音。这允许我们创建摩尔斯电码。

function morse(gainNode, pattern) {
  let silenceTimeout = 300;
  let noiseTimeout;
  
  if(pattern === '') {
    //We are done here
    return;
  } else if(pattern.charAt(0) === '.') {
    noiseTimeout = 100;
  } else if(pattern.charAt(0) === '-') {
    noiseTimeout = 400;
  } else {
    console.error(pattern.charAt(0), ': Character not recognized.');
    return;
  }
  
  //Briefly let sound through this gain node
  gainNode.gain.value = 1;
  window.setTimeout(function() {
    gainNode.gain.value = 0;
    window.setTimeout(morse, silenceTimeout, gainNode, pattern.substring(1));
  }, noiseTimeout);
}
 
let audioContext = new (window.AudioContext || window.webkitAudioContext)();

let sourceNode = audioContext.createOscillator();
let gainNode = audioContext.createGain();

sourceNode.type = 'sine';
sourceNode.frequency.value = 261.6;
sourceNode.detune.value = 0;

//Mute sound going through this gain node
gainNode.gain.value = 0;

//SourceNode -> GainNode -> Speakers
sourceNode.connect(gainNode);
gainNode.connect(audioContext.destination);

//The source node starts outputting
sourceNode.start();

//Output SOS
morse(gainNode, '...---...');