Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,4 @@ Cargo.lock

*.env

examples/audio-worklet-to-worker.wav
74 changes: 74 additions & 0 deletions examples/audio-worklet-to-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// -----------------------------------------------------------------------------
// Adapted from padenot's ringbuf.js example
// https://github.com/padenot/ringbuf.js/tree/main/public/example/audioworklet-to-worker
// -----------------------------------------------------------------------------
import path from 'node:path';
import fs from 'node:fs';
import { Worker } from 'node:worker_threads';

import { AudioContext, OscillatorNode, GainNode, StereoPannerNode, AudioWorkletNode } from '#node-web-audio-api';
import { RingBuffer } from 'ringbuf.js';
import { sleep } from '@ircam/sc-utils';

const audioContext = new AudioContext();
await audioContext.audioWorklet.addModule(path.join('worklets', 'audio-worklet-to-worker', 'recorder-worklet.js'));
// One second of stereo Float32 PCM ought to be plentiful.
const sharedArrayBuffer = RingBuffer.getStorageForCapacity(audioContext.sampleRate * 2, Float32Array);

// Setup the wav writer worker
const recorderWorker = new Worker('./examples/worklets/audio-worklet-to-worker/wav-writer.js', {
workerData: {
sharedArrayBuffer,
channelCount: 2,
sampleRate: audioContext.sampleRate,
},
});

// Setup web audio
audioContext.resume();

// Generate a tone that goes left and right and up and down. Route it to an
// AudioWorkletProcessor that does the recording, as well as to the output.
const osc = new OscillatorNode(audioContext);
const fm = new OscillatorNode(audioContext, { frequency: 1.0 });
const gain = new GainNode(audioContext, { gain: 110 });
const panner = new StereoPannerNode(audioContext);
const panModulation = new OscillatorNode(audioContext, { frequency: 2.0 });
const recorderWorklet = new AudioWorkletNode(audioContext, 'recorder-worklet', {
processorOptions: { sharedArrayBuffer },
});

// setup graph
panModulation.connect(panner.pan);
fm.connect(gain).connect(osc.frequency);
osc.connect(panner).connect(audioContext.destination);
panner.connect(recorderWorklet);

osc.start(0);
fm.start(0);
panModulation.start(0);

// Starve the main thread
const mainThreadLoadIntervalId = setInterval(function() {
var start = Date.now();
// eslint-disable-next-line no-empty
while (Date.now() - start < 90) {}
}, 100);

await sleep(2);

recorderWorker.on('message', async arrayBuffer => {
console.log('> main thread: stop rendering');

clearInterval(mainThreadLoadIntervalId);
await audioContext.close();
recorderWorker.terminate();

// Replay the wav file in audio buffer source node
const pathname = path.join(import.meta.dirname, 'audio-worklet-to-worker.wav');
console.log('> main thread: write file to disk: ', pathname);

fs.writeFileSync(pathname, Buffer.from(arrayBuffer));
});

recorderWorker.postMessage({ command: 'stop' });
61 changes: 61 additions & 0 deletions examples/main-thread-to-audio-worklet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// -----------------------------------------------------------------------------
// Adapted from padenot's ringbuf.js example
// https://github.com/padenot/ringbuf.js/tree/main/public/example/main-thread-to-audioworklet
// -----------------------------------------------------------------------------
import path from 'node:path';

import { AudioContext, AudioWorkletNode } from '#node-web-audio-api';
import { RingBuffer, AudioWriter, ParameterWriter } from 'ringbuf.js';

const audioContext = new AudioContext();
await audioContext.audioWorklet.addModule(path.join('worklets', 'main-thread-to-audio-worklet', 'processor.js'));

let frequency = 440;
let phase = 0.0;
const sine = new Float32Array(128);

// 50ms of buffer, increase in case of glitches
const sharedArrayBuffer = RingBuffer.getStorageForCapacity(
audioContext.sampleRate / 20,
Float32Array,
);
const ringBuffer = new RingBuffer(sharedArrayBuffer, Float32Array);
const audioWriter = new AudioWriter(ringBuffer);

const sharedArrayBuffer2 = RingBuffer.getStorageForCapacity(31, Uint8Array);
const ringBuffer2 = new RingBuffer(sharedArrayBuffer2, Uint8Array);
const paramWriter = new ParameterWriter(ringBuffer2);

const processor = new AudioWorkletNode(audioContext, 'processor', {
processorOptions: {
audioQueue: sharedArrayBuffer,
paramQueue: sharedArrayBuffer2,
},
});

processor.connect(audioContext.destination);

// change freq and amp every second
setInterval(() => {
frequency = Math.random() * 900 + 100;

const gain = Math.random();
paramWriter.enqueue_change(0, gain);

console.log(`Frequency: ${frequency}, Gain: ${gain}`);
}, 1000);

setInterval(() => {
// Synthetize a simple sine wave so it's easy to hear glitches, continuously
// if there is room in the ring buffer.
while (audioWriter.available_write() > 128) {
for (let i = 0; i < 128; i++) {
sine[i] = Math.sin(phase);
phase += (2 * Math.PI * frequency) / audioContext.sampleRate;
if (phase > 2 * Math.PI) {
phase -= 2 * Math.PI;
}
}
audioWriter.enqueue(sine);
}
}, 10);
31 changes: 31 additions & 0 deletions examples/worklets/audio-worklet-to-worker/recorder-worklet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

// -----------------------------------------------------------------------------
// Adapted from padenot's ringbuf.js example
// https://github.com/padenot/ringbuf.js/tree/main/public/example/audioworklet-to-worker
// -----------------------------------------------------------------------------

import { AudioWriter, RingBuffer, interleave } from 'ringbuf.js';

class RecorderWorklet extends AudioWorkletProcessor {
constructor(options) {
super();
// Staging buffer to interleave the audio data.
this.interleaved = new Float32Array(128 * 2); // stereo
const { sharedArrayBuffer } = options.processorOptions;
this.audioWriter = new AudioWriter(new RingBuffer(sharedArrayBuffer, Float32Array));
}

process(inputs, _outputs, _parameters) {
// interleave and store in the queue
if (inputs[0]) {
interleave(inputs[0], this.interleaved);

if (this.audioWriter.enqueue(this.interleaved) !== 256) {
console.log(`underrun: the worker doesn't dequeue fast enough!`);
}
}
return true;
}
}

registerProcessor("recorder-worklet", RecorderWorklet);
118 changes: 118 additions & 0 deletions examples/worklets/audio-worklet-to-worker/wav-writer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@

// -----------------------------------------------------------------------------
// Adapted from padenot's ringbuf.js example
// https://github.com/padenot/ringbuf.js/tree/main/public/example/audioworklet-to-worker
// -----------------------------------------------------------------------------

import { AudioReader, RingBuffer } from 'ringbuf.js';
import {
parentPort,
workerData,
} from 'node:worker_threads';

const {
sharedArrayBuffer,
// The number of channels of the audio stream read from the queue.
channelCount,
// The sample-rate of the audio stream read from the queue.
sampleRate,
} = workerData;

const audioReader = new AudioReader(
new RingBuffer(sharedArrayBuffer, Float32Array),
);

// Store the audio data, segment by segments, as array of int16 samples.
const pcm = [];
// A smaller staging array to copy the audio samples from, before conversion
// to uint16. It's size is 4 times less than the 1 second worth of data
// that the ring buffer can hold, so it's 250ms, allowing to not make
// deadlines:
// staging buffer size = ring buffer size / sizeof(float32) / stereo / 4
const staging = new Float32Array(sharedArrayBuffer.byteLength / 4 / 4 / 2);
// Attempt to dequeue every 100ms. Making this deadline isn't critical:
// there's 1 second worth of space in the queue, and we'll be dequeing
const readQueueIntervalId = setInterval(readFromQueue, 100);

// Read some float32 pcm from the queue, convert to int16 pcm, and push it to our global queue
function readFromQueue() {
const samplesRead = audioReader.dequeue(staging);
if (!samplesRead) {
return 0;
}

const segment = new Int16Array(samplesRead);

for (let i = 0; i < samplesRead; i++) {
segment[i] = Math.min(Math.max(staging[i], -1.0), 1.0) * (2 << 14 - 1);
}

pcm.push(segment);

return samplesRead;
}

parentPort.on('message', e => {
switch (e.command) {
case "stop": {
clearInterval(readQueueIntervalId);
// Drain the ring buffer
while (readFromQueue()) {
/* empty */
}

// Structure of a wav file, with a byte offset for the values to modify:
// sample-rate, channel count, block align.
const CHANNEL_OFFSET = 22;
const SAMPLE_RATE_OFFSET = 24;
const BLOCK_ALIGN_OFFSET = 32;
const header = [
// RIFF header
0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x57, 0x41, 0x56, 0x45,
// fmt chunk. We always write 16-bit samples.
0x66, 0x6d, 0x74, 0x20, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x10, 0x00,
// data chunk
0x64, 0x61, 0x74, 0x61, 0xfe, 0xff, 0xff, 0x7f,
];
// Find final size: size of the header + number of samples * channel count
// * 2 because pcm16
let size = header.length;
for (let i = 0; i < pcm.length; i++) {
size += pcm[i].length * 2;
}
const wav = new Uint8Array(size);
const view = new DataView(wav.buffer);

// Copy the header, and modify the values: note that RIFF
// is little-endian, we need to pass `true` as the last param.
for (let i = 0; i < wav.length; i++) {
wav[i] = header[i];
}

console.log(
`> wav writer thread: sending back wav file: ${sampleRate}Hz, ${channelCount} channels, int16`
);

view.setUint16(CHANNEL_OFFSET, channelCount, true);
view.setUint32(SAMPLE_RATE_OFFSET, sampleRate, true);
view.setUint16(BLOCK_ALIGN_OFFSET, channelCount * 2, true);

// Finally, copy each segment in order as int16, and transfer the array
// back to the main thread for download.
let writeIndex = header.length;

for (let segment = 0; segment < pcm.length; segment++) {
for (let sample = 0; sample < pcm[segment].length; sample++) {
view.setInt16(writeIndex, pcm[segment][sample], true);
writeIndex += 2;
}
}
parentPort.postMessage(wav.buffer, [wav.buffer]);
break;
}
default: {
throw Error("Case not handled");
}
}
});
45 changes: 45 additions & 0 deletions examples/worklets/main-thread-to-audio-worklet/processor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// -----------------------------------------------------------------------------
// Adapted from padenot's ringbuf.js example
// https://github.com/padenot/ringbuf.js/tree/main/public/example/main-thread-to-audioworklet
// -----------------------------------------------------------------------------
import { AudioReader, ParameterReader, RingBuffer } from 'ringbuf.js';

class Processor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [];
}

constructor(options) {
super(options);
this.interleaved = new Float32Array(128);
this.amp = 1.0;
this.o = { index: 0, value: 0 };

const { audioQueue, paramQueue } = options.processorOptions;

this.audioReader = new AudioReader(
new RingBuffer(audioQueue, Float32Array)
);
this.paramReader = new ParameterReader(
new RingBuffer(paramQueue, Uint8Array)
);
}

process(inputs, outputs, parameters) {
// get any param changes
if (this.paramReader.dequeue_change(this.o)) {
this.amp = this.o.value;
}

// read 128 frames from the queue, [deinterleave,] and write to output buffers.
this.audioReader.dequeue(this.interleaved);

for (let i = 0; i < 128; i++) {
outputs[0][0][i] = this.amp * this.interleaved[i];
}

return true;
}
}

registerProcessor("processor", Processor);
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"js-beautify": "^1.15.1",
"mocha": "^11.0.1",
"octokit": "^5.0.5",
"ringbuf.js": "^0.4.0",
"template-literal": "^1.0.4",
"webidl2": "^24.2.0",
"wpt-runner": "^7.0.0"
Expand Down
Loading