Web Audio API : 高效播放 PCM 流

Web Audio API : efficiently play a PCM stream

这里有个问题:

我做的是:

var samples = []; // each element of this array stores a chunk of 512 samples
var audioCtx = new AudioContext();
var source = audioCtx.createBufferSource();

source.buffer = audioCtx.createBuffer(1, 512, 88200);

// bufferSize is 512 because it is the size of chunks
var scriptNode = audioCtx.createScriptProcessor(512, 1, 1);

scriptNode.onaudioprocess = function(audioProcessingEvent) {
  // play a chunk if there is at least one.
  if (samples.length > 0) {
    audioProcessingEvent.outputBuffer.copyToChannel(samples.shift(), 0, 0);
  }
};

source.connect(scriptNode);
scriptNode.connect(audioCtx.destination);
source.start();

peerConnection.addEventListener("datachannel", function(e) {
  e.channel.onmessage = function(m) {
    var values = new Float32Array(m.data);
    samples.push(values);
  };
);

有几个问题:

提前感谢您的任何建议!

你想要一个 AudioBuffer.

您可以直接从 TypedArray 将原始 PCM 数据复制到其通道中。
您可以指定其 sampleRate,AudioContext 将负责重新采样以匹配声卡的设置。

但是要注意,每个块 2048 字节意味着每个块只代表 5ms 的音频数据 @88Khz:我们传递一个 Float32Array,所以字节大小是 4 和 2048 / 4 / 88200 = ±0.0058s.
您可能想要增加它,并实施一些缓冲策略。

这是一个小演示,作为将块数据存储到缓冲区 Float32Array 中的概念证明。

const min_sample_duration = 2; // sec
const sample_rate = 88200; // Hz
// how much data is needed to play for at least min_sample_duration
const min_sample_size = min_sample_duration * sample_rate;

const fetching_interval = 100; // ms

// you'll probably want this much bigger
let chunk_size = 2048; // bytes

const log = document.getElementById( 'log' );
const btn = document.getElementById( 'btn' );

btn.onclick = e => {

  let stopped = false;
  let is_reading = false;
  
  const ctx = new AudioContext();
  // to control output volume
  const gain = ctx.createGain();
  gain.gain.value = 0.01;
  gain.connect( ctx.destination );
  // this will get updated at every new fetch
  let fetched_data  = new Float32Array( 0 );
  // keep it accessible so we can stop() it
  let active_node;

  // let's begin
  periodicFetch();

  // UI update
  btn.textContent = "stop";
  btn.onclick = e => {
    stopped = true;
    if( active_node ) { active_node.stop(0); }
  };
  oninput = handleUIEvents;

  // our fake fetcher, calls itself every 50ms
  function periodicFetch() {

    // data from server (here just some noise)
    const noise = Float32Array.from( { length: chunk_size / 4 }, _ => (Math.random() * 1) - 0.5 );
    // we concatenate the data just fetched with what we have already buffered
    fetched_data = concatFloat32Arrays( fetched_data, noise );
    // for demo only
    log.textContent = "buffering: " +  fetched_data.length + '/ ' + min_sample_size;

    if( !stopped ) {
      // do it again
      setTimeout( periodicFetch , fetching_interval );
    }
    // if we are not actively reading and have fetched enough
    if( !is_reading && fetched_data.length > min_sample_size ) {
      readingLoop(); // start reading
    }
  
  }
  function readingLoop() {
  
    if( stopped  || fetched_data.length < min_sample_size ) {
      is_reading = false;
      return;
    }
    // let the world know we are actively reading
    is_reading = true;
    // create a new AudioBuffer
    const aud_buf = ctx.createBuffer( 1, fetched_data.length, sample_rate );
    // copy our fetched data to its first channel
    aud_buf.copyToChannel( fetched_data, 0 );

    // clear the buffered data
    fetched_data = new Float32Array( 0 );
    
    // the actual player
    active_node = ctx.createBufferSource();
    active_node.buffer = aud_buf;
    active_node.onended = readingLoop; // in case we buffered enough while playing
    active_node.connect( gain );
    active_node.start( 0 );

  }

  function handleUIEvents( evt ) {

    const type = evt.target.name;
    const value = evt.target.value;
    switch( type ) {
      case "chunk-size":
        chunk_size = +value;
        break;
      case "volume":
        gain.gain.value = +value;
        break;
    }

  }

};

// helpers
function concatFloat32Arrays( arr1, arr2 ) {
  if( !arr1 || !arr1.length ) {
    return arr2 && arr2.slice();
  }
  if( !arr2 || !arr2.length ) {
    return arr1 && arr1.slice();
  }
  const out = new Float32Array( arr1.length + arr2.length );
  out.set( arr1 );
  out.set( arr2, arr1.length );
  return out;
}
label { display: block }
<button id="btn">start</button>
<pre id="log"></pre>

<div>
<label>Output volume:<input type="range" name="volume" min="0" max="0.5" step="0.01" value="0.01"></label>
</div>
<div>
Size of each chunk fetched:
  <label><input type="radio" name="chunk-size" value="2048" checked>2048 bytes (OP's current)</label>
  <label><input type="radio" name="chunk-size" value="35280">35280 bytes (barely enough for 0.1s interval)</label>
  <label><input type="radio" name="chunk-size" value="44100">44100 bytes (enough for 0.1s interval)</label>
</div>