How to continually refill buffer/stream in and play audio without that annoying pause and click sound at the end of buffer?

Viewed 1047

Trying to create soothing hydrogen sound generator but web audio api is too limited or I'm missing something. No onbufferend or onrequestmoredata or similar. Only thing that exist is onended from AudioBufferSourceNode. Is what I'm trying to do not possible?

stackoverflow is whining about I should add more details because the post contains mostly code but I have no more details to add.

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>

Edit:

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>

Edit two. Buffer size increased (no needed) and fillAudioBuffer functions offloaded from main thread. This works. I didn't need to do this on my C++ version. Javascript is slower than I thought.

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>

1 Answers

Here is one possible approach to do what you want, assuming the clicking you get is because there's a gap between the end of one buffer and the start of the next.

Create two separate buffers and two corresponding AudioBufferSourceNodes. Set an onended event handler for each one. Start playing the first buffer and schedule the second to start just at the end of the first.

When you get the onended event, create a new buffer and source and schedule it to start playing at the end of the second buffer. Set a new onended event handler for this buffer that basiscally does the same.

Now when you get an onended event, there will be a buffer already scheduled to play without gaps, and you can create a new one ready to go when the currently playing one is done.

However, you may still get some clicks between buffers because the value at the end of one buffer may be very different from the value at the beginning of the next. To fix this, you may need to either ramp down (via a gain node) the end of one buffer, and ramp up the beginning of the next. Or cross-fade the two buffers to have smooth transition.

The fade-in/fade-out of the buffers could be done via AudioBufferSourceNode AudioParam automations, or you can do it when you fill the buffer.

Related