如何不断地重新填充缓冲区/流并播放音频,而不会在缓冲区末尾出现恼人的暂停和点击声?

问题描述 投票:0回答:2

尝试创建舒缓的氢声音发生器,但网络音频 api 太有限,或者我错过了一些东西。没有 onbufferend 或 onrequestmoredata 或类似的。唯一存在的东西是来自 AudioBufferSourceNode 的 onend。我想做的事不可能吗?

stackoverflow 抱怨我应该添加更多详细信息,因为该帖子主要包含代码,但我没有更多详细信息要添加。

document.addEventListener("DOMContentLoaded", () => {
const button = document.querySelector('button');
const buttonStop = document.querySelector('#buttonStop');

let AudioContext = window.AudioContext || window.webkitAudioContext;
let audioCtx;

// Mono
const channels = 1;

function init() {
  audioCtx = new AudioContext();
}

buttonStop.onclick = function() {
  audioCtx.close();
  audioCtx = null;
}


const clamp = (num, min, max) => Math.min(Math.max(num, min), max);

const hz_REAL_TIME_FREQUENCY = 440;
let dk;
let dPos = 0.0;
let firsttime = true;
const table = [];

function sum(t) {
  if (firsttime) {
    for (var i = 0; i < 9; ++i) {
      let n = i + 2; // todo: this should continually increase, 2 -> infinite
      table[i] = [];
      table[i][0] = ((1                    - 1/Math.pow( n   , 2)) );
      table[i][1] = ((1/4                  - 1/Math.pow((n+1), 2)) );
      table[i][2] = ((1/9                  - 1/Math.pow((n+2), 2)) );
      table[i][3] = (((1 / Math.pow(4, 2)) - 1/Math.pow((n+3), 2)) );
      table[i][4] = (((1 / Math.pow(5, 2)) - 1/Math.pow((n+4), 2)) );
      table[i][5] = (((1 / Math.pow(6, 2)) - 1/Math.pow((n+5), 2)) );
    }
    firsttime = false;
  }

  let sum_value = 0.0;
  for (let i = 0; i < 9; ++i)
  {
    sum_value += Math.sin(table[i][0] * t);
    sum_value += Math.sin(table[i][1] * t);
    sum_value += Math.sin(table[i][2] * t);
    sum_value += Math.sin(table[i][3] * t);
    sum_value += Math.sin(table[i][4] * t);
    sum_value += Math.sin(table[i][5] * t);
  }
  return sum_value;
}

button.onclick = function() {
  if(!audioCtx) {
    init();
    dk = hz_REAL_TIME_FREQUENCY * 2 * Math.PI / audioCtx.sampleRate;
  }

  // Create an empty two second stereo buffer at the
  // sample rate of the AudioContext
  let frameCount_buffersize = audioCtx.sampleRate * 2.0;

  let myArrayBuffer = audioCtx.createBuffer(channels, frameCount_buffersize, audioCtx.sampleRate);

function fillAudioBuffer() {
  // Fill the buffer with white noise;
  //just random values between -1.0 and 1.0
  for (let channel = 0; channel < channels; channel++) {
   // This gives us the actual array that contains the data
   let nowBuffering = myArrayBuffer.getChannelData(channel);
   for (let i_sampleNumber = 0; i_sampleNumber < frameCount_buffersize; i_sampleNumber++) {
     // audio needs to be in [-1.0; 1.0]
     nowBuffering[i_sampleNumber] = clamp(sum(dPos) * 0.03, -1.0, 1.0);
     dPos += dk;
   }
   //console.log(nowBuffering);
  }
}

function continueSource() {
  // Get an AudioBufferSourceNode.
  // This is the AudioNode to use when we want to play an AudioBuffer
  let source = audioCtx.createBufferSource();
  // set the buffer in the AudioBufferSourceNode
  fillAudioBuffer();
  source.buffer = myArrayBuffer;
  // connect the AudioBufferSourceNode to the
  // destination so we can hear the sound
  source.connect(audioCtx.destination);

// OR

/*let gain = audioCtx.createGain();

// Set parameters
gain.gain.value = 0.1;

// Connect graph
source.connect(gain);
gain.connect(audioCtx.destination);/**/


  // start the source playing
  source.start();

  source.onended = () => {
    source.disconnect(audioCtx.destination);
    continueSource();
  }
}
continueSource();
}
});
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="viewport" content="width=device-width">

    <title>Hydrogen sound</title>
  </head>

  <body>
    <h1>Hydrogen sound</h1>
    <button>Make hydrogen sound</button>
    <button id="buttonStop">Stop</button>
  </body>
</html>

编辑:

document.addEventListener("DOMContentLoaded", () => {
const button = document.querySelector('button');
const buttonStop = document.querySelector('#buttonStop');

let AudioContext = window.AudioContext || window.webkitAudioContext;
let audioCtx;

// Mono
const channels = 1;

function init() {
  audioCtx = new AudioContext();
}

buttonStop.onclick = function() {
  audioCtx.close();
  audioCtx = null;
}


const clamp = (num, min, max) => Math.min(Math.max(num, min), max);

const hz_REAL_TIME_FREQUENCY = 440;
let dk;
let dPos = 0.0;
let firsttime = true;
const table = [];

function sum(t) {
  if (firsttime) {
    for (var i = 0; i < 9; ++i) {
      let n = i + 2; // todo: this should continually increase, 2 -> infinite
      table[i] = [];
      table[i][0] = ((1                    - 1/Math.pow( n   , 2)) );
      table[i][1] = ((1/4                  - 1/Math.pow((n+1), 2)) );
      table[i][2] = ((1/9                  - 1/Math.pow((n+2), 2)) );
      table[i][3] = (((1 / Math.pow(4, 2)) - 1/Math.pow((n+3), 2)) );
      table[i][4] = (((1 / Math.pow(5, 2)) - 1/Math.pow((n+4), 2)) );
      table[i][5] = (((1 / Math.pow(6, 2)) - 1/Math.pow((n+5), 2)) );
    }
    firsttime = false;
  }

  let sum_value = 0.0;
  for (let i = 0; i < 9; ++i)
  {
    sum_value += Math.sin(table[i][0] * t);
    sum_value += Math.sin(table[i][1] * t);
    sum_value += Math.sin(table[i][2] * t);
    sum_value += Math.sin(table[i][3] * t);
    sum_value += Math.sin(table[i][4] * t);
    sum_value += Math.sin(table[i][5] * t);
  }
  return sum_value;
}

button.onclick = function() {
  if(!audioCtx) {
    init();
    dk = hz_REAL_TIME_FREQUENCY * 2 * Math.PI / audioCtx.sampleRate;
  }

  // Create an empty two second stereo buffer at the
  // sample rate of the AudioContext
  let frameCount_buffersize = audioCtx.sampleRate * 2.0;

  let myArrayBuffer = audioCtx.createBuffer(channels, frameCount_buffersize, audioCtx.sampleRate);

  let myArrayBuffer2 = audioCtx.createBuffer(channels, frameCount_buffersize, audioCtx.sampleRate);

function fillAudioBuffer() {
  // Fill the buffer with white noise;
  //just random values between -1.0 and 1.0
  for (let channel = 0; channel < channels; channel++) {
   // This gives us the actual array that contains the data
   let nowBuffering = myArrayBuffer.getChannelData(channel);
   for (let i_sampleNumber = 0; i_sampleNumber < frameCount_buffersize; i_sampleNumber++) {
     // audio needs to be in [-1.0; 1.0]
     nowBuffering[i_sampleNumber] = clamp(sum(dPos) * 0.03, -1.0, 1.0);
     dPos += dk;
   }
   //console.log(nowBuffering);
  }
}
function fillAudioBuffer2() {
  // Fill the buffer with white noise;
  //just random values between -1.0 and 1.0
  for (let channel = 0; channel < channels; channel++) {
   // This gives us the actual array that contains the data
   let nowBuffering = myArrayBuffer2.getChannelData(channel);
   for (let i_sampleNumber = 0; i_sampleNumber < frameCount_buffersize; i_sampleNumber++) {
     // audio needs to be in [-1.0; 1.0]
     nowBuffering[i_sampleNumber] = clamp(sum(dPos) * 0.03, -1.0, 1.0);
     dPos += dk;
   }
   //console.log(nowBuffering);
  }
}
let i = 0;
fillAudioBuffer();
function continueSource() {
  // Get an AudioBufferSourceNode.
  // This is the AudioNode to use when we want to play an AudioBuffer
  let source = audioCtx.createBufferSource();
  // set the buffer in the AudioBufferSourceNode
  if (i++ & 1) {
    fillAudioBuffer();
    source.buffer = myArrayBuffer2;
  } else {
    fillAudioBuffer2();
    source.buffer = myArrayBuffer;
  }
  // connect the AudioBufferSourceNode to the
  // destination so we can hear the sound
  source.connect(audioCtx.destination);

// OR

/*let gain = audioCtx.createGain();

// Set parameters
gain.gain.value = 0.1;

// Connect graph
source.connect(gain);
gain.connect(audioCtx.destination);/**/


  // start the source playing
  source.start();

  source.onended = () => {
    source.disconnect(audioCtx.destination);
    continueSource();
  }
}
continueSource();
}
});
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="viewport" content="width=device-width">

    <title>Hydrogen sound</title>
  </head>

  <body>
    <h1>Hydrogen sound</h1>
    <button>Make hydrogen sound</button>
    <button id="buttonStop">Stop</button>
  </body>
</html>

编辑两个。缓冲区大小增加(不需要),并且 fillAudioBuffer 函数从主线程卸载。这有效。我不需要在我的 C++ 版本上执行此操作。 JavaScript 比我想象的要慢。

document.addEventListener("DOMContentLoaded", () => {
const button = document.querySelector('button');
const buttonStop = document.querySelector('#buttonStop');

let AudioContext = window.AudioContext || window.webkitAudioContext;
let audioCtx;

// Mono
const channels = 1;

function init() {
  audioCtx = new AudioContext();
}

buttonStop.onclick = function() {
  audioCtx.close();
  audioCtx = null;
}


const clamp = (num, min, max) => Math.min(Math.max(num, min), max);

const hz_REAL_TIME_FREQUENCY = 440;
let dk;
let dPos = 0.0;
let firsttime = true;
const table = [];

function sum(t) {
  if (firsttime) {
    for (var i = 0; i < 9; ++i) {
      let n = i + 2; // todo: this should continually increase, 2 -> infinite
      table[i] = [];
      table[i][0] = ((1                    - 1/Math.pow( n   , 2)) );
      table[i][1] = ((1/4                  - 1/Math.pow((n+1), 2)) );
      table[i][2] = ((1/9                  - 1/Math.pow((n+2), 2)) );
      table[i][3] = (((1 / Math.pow(4, 2)) - 1/Math.pow((n+3), 2)) );
      table[i][4] = (((1 / Math.pow(5, 2)) - 1/Math.pow((n+4), 2)) );
      table[i][5] = (((1 / Math.pow(6, 2)) - 1/Math.pow((n+5), 2)) );
    }
    firsttime = false;
  }

  let sum_value = 0.0;
  for (let i = 0; i < 9; ++i)
  {
    sum_value += Math.sin(table[i][0] * t);
    sum_value += Math.sin(table[i][1] * t);
    sum_value += Math.sin(table[i][2] * t);
    sum_value += Math.sin(table[i][3] * t);
    sum_value += Math.sin(table[i][4] * t);
    sum_value += Math.sin(table[i][5] * t);
  }
  return sum_value;
}

button.onclick = function() {
  if(!audioCtx) {
    init();
    dk = hz_REAL_TIME_FREQUENCY * 2 * Math.PI / audioCtx.sampleRate;
  }

  // Create an empty two second stereo buffer at the
  // sample rate of the AudioContext
  let frameCount_buffersize = audioCtx.sampleRate * 20.0;

  let myArrayBuffer = audioCtx.createBuffer(channels, frameCount_buffersize, audioCtx.sampleRate);

  let myArrayBuffer2 = audioCtx.createBuffer(channels, frameCount_buffersize, audioCtx.sampleRate);

function fillAudioBuffer() {
  // Fill the buffer with white noise;
  //just random values between -1.0 and 1.0
  for (let channel = 0; channel < channels; channel++) {
   // This gives us the actual array that contains the data
   let nowBuffering = myArrayBuffer.getChannelData(channel);
   for (let i_sampleNumber = 0; i_sampleNumber < frameCount_buffersize; i_sampleNumber++) {
     // audio needs to be in [-1.0; 1.0]
     nowBuffering[i_sampleNumber] = clamp(sum(dPos) * 0.03, -1.0, 1.0);
     dPos += dk;
   }
   //console.log(nowBuffering);
  }
}
function fillAudioBuffer2() {
  // Fill the buffer with white noise;
  //just random values between -1.0 and 1.0
  for (let channel = 0; channel < channels; channel++) {
   // This gives us the actual array that contains the data
   let nowBuffering = myArrayBuffer2.getChannelData(channel);
   for (let i_sampleNumber = 0; i_sampleNumber < frameCount_buffersize; i_sampleNumber++) {
     // audio needs to be in [-1.0; 1.0]
     nowBuffering[i_sampleNumber] = clamp(sum(dPos) * 0.03, -1.0, 1.0);
     dPos += dk;
   }
   //console.log(nowBuffering);
  }
}
let i = 0;
fillAudioBuffer();
function continueSource() {
  // Get an AudioBufferSourceNode.
  // This is the AudioNode to use when we want to play an AudioBuffer
  let source = audioCtx.createBufferSource();
  // set the buffer in the AudioBufferSourceNode
  if (i++ & 1) {
    console.log('Using myArrayBuffer2', i);
    setTimeout(() => fillAudioBuffer(), 0);
    source.buffer = myArrayBuffer2;
  } else {
    console.log('Using myArrayBuffer', i);
    setTimeout(() => fillAudioBuffer2(), 0);
    source.buffer = myArrayBuffer;
  }
  // connect the AudioBufferSourceNode to the
  // destination so we can hear the sound
  source.connect(audioCtx.destination);

// OR

/*let gain = audioCtx.createGain();

// Set parameters
gain.gain.value = 0.1;

// Connect graph
source.connect(gain);
gain.connect(audioCtx.destination);/**/


  // start the source playing
  source.start();

  source.onended = () => {
    source.disconnect(audioCtx.destination);
    continueSource();
  }
}
continueSource();

}
});
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="viewport" content="width=device-width">

    <title>Hydrogen sound</title>
  </head>

  <body>
    <h1>Hydrogen sound</h1>
    <button>Make hydrogen sound</button>
    <button id="buttonStop">Stop</button>
  </body>
</html>

javascript web-audio-api
2个回答
1
投票

这是一种可能的方法来完成您想要的操作,假设您获得的点击声是因为一个缓冲区的末尾与下一个缓冲区的开始之间存在间隙。

创建两个独立的缓冲区和两个相应的AudioBufferSourceNode。为每个事件设置一个 onending 事件处理程序。开始播放第一个缓冲区并安排第二个缓冲区在第一个缓冲区结束时开始。

当您收到已结束的事件时,创建一个新的缓冲区和源,并安排它在第二个缓冲区的末尾开始播放。为此缓冲区设置一个新的 onend 事件处理程序,其基本上执行相同的操作。

现在,当您收到一个已结束的事件时,将会有一个缓冲区已安排好无间隙地播放,并且您可以在当前播放的事件完成后创建一个准备就绪的新事件。

但是,您可能仍然会在缓冲区之间收到一些点击,因为一个缓冲区末尾的值可能与下一个缓冲区开头的值非常不同。要解决此问题,您可能需要(通过增益节点)降低一个缓冲区的末尾,并提高下一个缓冲区的开头。或者交叉淡入淡出两个缓冲区以实现平滑过渡。

缓冲区的淡入/淡出可以通过 AudioBufferSourceNode AudioParam 自动化完成,或者您可以在填充缓冲区时完成。


0
投票

在我的实验中,从任意源(例如 websocket)传输音频的正确方法是使用 AudioWorkletProcessor

最小代码示例。假设来自套接字的 16 位 48 kHz PCM 数据。 这会连接到 http://localhost/ws 上的 websocket

const sample_rate = 48000; // Hz

// Websocket url
const ws_url = "http://localhost/ws"

let audio_context = null;
let ws = null;

async function start() {
    if (ws != null) {
        return;
    }

    // Create an AudioContext that plays audio from the AudioWorkletNode  
    audio_context = new AudioContext();
    await audio_context.audioWorklet.addModule('audioProcessor.js');
    const audioNode = new AudioWorkletNode(audio_context, 'audio-processor');
    audioNode.connect(audio_context.destination);

    // Setup the websocket 
    ws = new WebSocket(ws_url);
    ws.binaryType = 'arraybuffer';

    // Process incoming messages
    ws.onmessage =  (event) => {
        // Convert to Float32 lpcm, which is what AudioWorkletNode expects
        const int16Array = new Int16Array(event.data);
        let float32Array = new Float32Array(int16Array.length);
        for (let i = 0; i < int16Array.length; i++) {
            float32Array[i] = int16Array[i] / 32768.; 
        }

        // Send the audio data to the AudioWorkletNode
        audioNode.port.postMessage({ message: 'audioData', audioData: float32Array });
    }
    
    ws.onopen = () => {
        console.log('WebSocket connection opened.');
    };

    ws.onclose = () => {
        console.log('WebSocket connection closed.');
    };
    
    ws.onerror = error => {
        console.error('WebSocket error:', error);
    };
}

async function stop() {
    console.log('Stopping audio');
    if (audio_context) {
        await audio_context.close();
        audio_context = null;
        ws.close();
        ws = null;
    }
}

这还需要:

音频处理器.js

class AudioProcessor extends AudioWorkletProcessor {

    constructor() {
        super();
        this.buffer = new Float32Array();

        // Receive audio data from the main thread, and add it to the buffer
        this.port.onmessage = (event) => {
            let newFetchedData = new Float32Array(this.buffer.length + event.data.audioData.length);
            newFetchedData.set(this.buffer, 0);
            newFetchedData.set(event.data.audioData, this.buffer.length); 
            this.buffer = newFetchedData;
        };
    }

    // Take a chunk from the buffer and send it to the output to be played
    process(inputs, outputs, parameters) {
        const output = outputs[0];
        const channel = output[0];
        const bufferLength = this.buffer.length;
        for (let i = 0; i < channel.length; i++) {
            channel[i] = (i < bufferLength) ? this.buffer[i] : 0;
        }
        this.buffer = this.buffer.slice(channel.length);
        return true;
    }
}

registerProcessor('audio-processor', AudioProcessor);
© www.soinside.com 2019 - 2024. All rights reserved.