For a music app, I need to be able to continuously and seamlessly generate raw audio samples using the web audio API. After searching, I found out about the AudioBuffer(https://developer.mozilla.org/en-US/docs/Web/API/AudioBuffer), and it seems that that's what I need. However, the audio buffer can only be played once(https://developer.mozilla.org/en-US/docs/Web/API/AudioBufferSourceNode), so it can't really play continuously. I tried this workaround:
const buffer = audioCtx.createBuffer(1, 10000, audioCtx.sampleRate);
for(let i = 0; i < buffer.length; i++)
buffer.getChannelData(0)[i] = ((i / audioCtx.sampleRate) * 440) % 1;
const source = audioCtx.createBufferSource();
source.buffer = buffer;
source.onended = function() {
console.log(this);
const newSource = audioCtx.createBufferSource();
for(let i = 0; i < buffer.length; i++)
buffer.getChannelData(0)[i] = ((i / audioCtx.sampleRate) * 440) % 1;
newSource.buffer = buffer;
newSource.connect(audioCtx.destination);
newSource.onended = (this.onended as Function).bind(newSource);
newSource.start();
}
source.connect(audioCtx.destination);
source.start();
In essence, this code creates a buffer & source node, plays the buffer, and when the buffer ends it creates a new source and buffer and continues playing. However, this method produces noticeable silences when buffer finishes playing. I assume this has something to do with the JS event loop, but I'm not sure.
Ideally, I would want something like this:
audioCtx.createSampleStream(() => {
// generate samples here.
return Math.random() * 2 - 1;
})
Hopefully I will be able to get this to work. If I don't, I will probably try writing a npm package with c++ bindings to do this.
I think the API you are looking for is the AudioWorklet
. It's a way to run your code directly on the audio thread. It allows you to fill the buffers right before they get played.
Setting it up is normally a bit complicated since your processor needs to be defined in a separate JavaScript file. But it's also possible to use a Blob
as shown below.
The example is based on your snippet which generates random samples.
const blob = new Blob(
[`
class MyProcessor extends AudioWorkletProcessor {
process(_, outputs) {
for (const output of outputs) {
for (const channelData of output) {
for (let i = 0; i < channelData.length; i += 1) {
channelData[i] = Math.random() * 2 - 1;
}
}
}
return true;
}
}
registerProcessor('my-processor', MyProcessor);
`],
{ type: 'application/javascript' }
);
const url = URL.createObjectURL(blob);
const audioContext = new AudioContext();
await audioContext.audioWorklet.addModule(url);
const myAudioWorkletNode = new AudioWorkletNode(audioContext, 'my-processor');
myAudioWorkletNode.connect(audioContext.destination);