Web Audio之getChannelData

2,577 阅读2分钟

在Web的api中,无论是图形canvas的getImageData还是音频的getChannelData,只要是"get..Data"的api都是很强大的api,它们能够让你拿到相对原始且完整的数据,前者能够拿到每一个像素的rgba,后者能够拿到每一个通道的PCM数据。

白噪声

getChannelData是针对AudioBuffer来说的,我们来看一下一段MDN上用ES5写的生成白噪声的代码,它将会生成一段2s的白噪声并播放。

// 两个通道,也就是立体声
var channels = 2;

// 总共2s,乘以sampleRate采样率就是总的PCM数据长度
var frameCount = audioCtx.sampleRate * 2.0;
// 创建一个buffer,三个参数分别是通道数,存贮在缓冲区中的PCM数据的长度,采样率
var myArrayBuffer = audioCtx.createBuffer(channels, frameCount, audioCtx.sampleRate);

button.onclick = function() {
  // 对每一个频道都填充白噪音
  for (var channel = 0; channel < channels; channel++) {
    // 当前的频道,总共填充两个频道
    var nowBuffering = myArrayBuffer.getChannelData(channel);
    for (var i = 0; i < frameCount; i++) {
      // 对当前频道填充-1到1的随机数
      nowBuffering[i] = Math.random() * 2 - 1;
    }
  }
  
  // 创建一个AudioBuffer的容器,也就是AudioBufferSourceNode
  var source = audioCtx.createBufferSource();
  source.buffer = myArrayBuffer;

  // 链接到总输出
  source.connect(audioCtx.destination);

  // 播放
  source.start();
}

*很好奇如果填充Perlin Noise会咋样,还没试过。

手动升调

对于AudioBufferSourceNode来说有自带的detuneplaybackRate来达成升降调,但其实这两个方法并没有本质的区别,都是通过压缩或拉长来改变音调,只不过前者的单位是cents,是一种音乐中非常小的单位,1200cents等于一个octave,也就人们常说的八度。

比如有一段正弦波,我们将它升高一个八度,简单画了个图示:

用代码来表示就是

// buffer代表改变前,buffer_pitch代表升调后
  const buffer_pitch = ctx.createBuffer(2, buffer0.length * 1/2, ctx.sampleRate);
  node.buffer = buffer_pitch;

  for (let i = 0; i < buffer_pitch.length; i += 1) {
    buffer_pitch.getChannelData(0)[i] = buffer.getChannelData(0)[i * 2];
    buffer_pitch.getChannelData(1)[i] = buffer.getChannelData(1)[i * 2];
  }

如果想要变调不变速,对于这种简单周期波形则只需要将缺少的时间填充相同的变调后的波即可。

频道替换

如果一首曲子有两个版本,可以将一个版本的一个channel替换成另一个版本的channel,一边一个版本,双倍的快乐。

startBtn.addEventListener("click", e => {
  const ctx = new AudioContext();
  const node = ctx.createBufferSource();

  node.connect(ctx.destination);
  
  Promise.all([get("./0.mp3"), get("./1.mp3")]).then(res => {
    Promise.all([ctx.decodeAudioData(res[0]), ctx.decodeAudioData(res[1])]).then(buffers => {
      const buffer0 = buffers[0];
      const buffer1 = buffers[1];
      
      node.buffer = buffer0;
      
      for (let i = 0; i < buffer0.length; i += 1) {
        buffer0.getChannelData(0)[i] = buffer1.getChannelData(0)[i];
      }

      node.start();
    });
  });
});

但是遍历一整个channelData是比较耗时的操作,务必将其放到web worker中来进行。

最后

以上只是getChannelData用法的冰山一角,专业的音频工作者可以用PCM数据完成非常多的操作,有兴趣的可以去多了解一下,音频还是一个非常有趣的领域的。