wam-community
Version:
A collection of prebuilt Web Audio Modules ready for use
279 lines (242 loc) • 8.25 kB
JavaScript
/** @typedef {import('@webaudiomodules/api').AudioWorkletGlobalScope} AudioWorkletGlobalScope */
/** @typedef {import('./types').WamSDKBaseModuleScope} WamSDKBaseModuleScope */
/** @typedef {import('./types').TypedArrayConstructor} TypedArrayConstructor */
/** @typedef {import('./types').TypedArray} TypedArray */
/** @typedef {import('./types').RingBuffer} IRingBuffer */
/** @typedef {typeof import('./types').RingBuffer} RingBufferConstructor */
/**
* @param {string} [moduleId]
* @returns {RingBufferConstructor}
*/
const getRingBuffer = (moduleId) => {
/** @type {AudioWorkletGlobalScope} */
// @ts-ignore
const audioWorkletGlobalScope = globalThis;
/**
* A Single Producer - Single Consumer thread-safe wait-free ring buffer.
* The producer and the consumer can be on separate threads, but cannot change roles,
* except with external synchronization. Adapted from https://github.com/padenot/ringbuf.js
* MPL-2.0 License (see RingBuffer_LICENSE.txt)
*
* @implements {IRingBuffer}
* @author padenot
*/
class RingBuffer {
/**
* @param {number} capacity
* @param {TypedArrayConstructor} Type
*/
static getStorageForCapacity(capacity, Type) {
if (!Type.BYTES_PER_ELEMENT) {
throw new Error('Pass in a ArrayBuffer subclass');
}
const bytes = 8 + (capacity + 1) * Type.BYTES_PER_ELEMENT;
return new SharedArrayBuffer(bytes);
}
/**
* `sab` is a SharedArrayBuffer with a capacity calculated by calling
* `getStorageForCapacity` with the desired capacity.
*
* @param {SharedArrayBuffer} sab
* @param {TypedArrayConstructor} Type
*/
constructor(sab, Type) {
// eslint-disable-next-line no-prototype-builtins
if (!Type.BYTES_PER_ELEMENT) {
throw new Error('Pass a concrete typed array class as second argument');
}
// Maximum usable size is 1<<32 - type.BYTES_PER_ELEMENT bytes in the ring
// buffer for this version, easily changeable.
// -4 for the write ptr (uint32_t offsets)
// -4 for the read ptr (uint32_t offsets)
// capacity counts the empty slot to distinguish between full and empty.
this._Type = Type;
this._capacity = (sab.byteLength - 8) / Type.BYTES_PER_ELEMENT;
this.buf = sab;
this.write_ptr = new Uint32Array(this.buf, 0, 1);
this.read_ptr = new Uint32Array(this.buf, 4, 1);
this.storage = new Type(this.buf, 8, this._capacity);
}
/**
* Returns the type of the underlying ArrayBuffer for this RingBuffer. This
* allows implementing crude type checking.
*/
get type() {
return this._Type.name;
}
/**
* Push bytes to the ring buffer. `elements` is a typed array of the same type
* as passed in the ctor, to be written to the queue.
* Returns the number of elements written to the queue.
*
* @param {TypedArray} elements
*/
push(elements) {
const rd = Atomics.load(this.read_ptr, 0);
const wr = Atomics.load(this.write_ptr, 0);
if ((wr + 1) % this._storageCapacity() === rd) {
// full
return 0;
}
const toWrite = Math.min(this._availableWrite(rd, wr), elements.length);
const firstPart = Math.min(this._storageCapacity() - wr, toWrite);
const secondPart = toWrite - firstPart;
this._copy(elements, 0, this.storage, wr, firstPart);
this._copy(elements, firstPart, this.storage, 0, secondPart);
// publish the enqueued data to the other side
Atomics.store(
this.write_ptr,
0,
(wr + toWrite) % this._storageCapacity(),
);
return toWrite;
}
/**
* Read `elements.length` elements from the ring buffer if `elements` is a typed
* array of the same type as passed in the ctor. If `elements` is an integer,
* pop and discard that many elements from the ring buffer.
* Returns the number of elements read from the queue, they are placed at the
* beginning of the array passed as parameter if `elements` is not an integer.
*
* @param {TypedArray | number} elements
*/
pop(elements) {
const rd = Atomics.load(this.read_ptr, 0);
const wr = Atomics.load(this.write_ptr, 0);
if (wr === rd) {
return 0;
}
const isArray = !Number.isInteger(elements);
// @ts-ignore
const toRead = Math.min(this._availableRead(rd, wr), isArray ? elements.length : elements);
if (isArray) {
const firstPart = Math.min(this._storageCapacity() - rd, toRead);
const secondPart = toRead - firstPart;
// @ts-ignore
this._copy(this.storage, rd, elements, 0, firstPart);
// @ts-ignore
this._copy(this.storage, 0, elements, firstPart, secondPart);
}
Atomics.store(this.read_ptr, 0, (rd + toRead) % this._storageCapacity());
return toRead;
}
/**
* True if the ring buffer is empty false otherwise. This can be late on the
* reader side: it can return true even if something has just been pushed.
*/
get empty() {
const rd = Atomics.load(this.read_ptr, 0);
const wr = Atomics.load(this.write_ptr, 0);
return wr === rd;
}
/**
* True if the ring buffer is full, false otherwise. This can be late on the
* write side: it can return true when something has just been popped.
*/
get full() {
const rd = Atomics.load(this.read_ptr, 0);
const wr = Atomics.load(this.write_ptr, 0);
return (wr + 1) % this._capacity !== rd;
}
/**
* The usable capacity for the ring buffer: the number of elements that can be
* stored.
*/
get capacity() {
return this._capacity - 1;
}
/**
* Number of elements available for reading. This can be late, and report less
* elements that is actually in the queue, when something has just been
* enqueued.
*/
get availableRead() {
const rd = Atomics.load(this.read_ptr, 0);
const wr = Atomics.load(this.write_ptr, 0);
return this._availableRead(rd, wr);
}
/**
* Number of elements available for writing. This can be late, and report less
* elements that is actually available for writing, when something has just
* been dequeued.
*/
get availableWrite() {
const rd = Atomics.load(this.read_ptr, 0);
const wr = Atomics.load(this.write_ptr, 0);
return this._availableWrite(rd, wr);
}
// private methods //
/**
* Number of elements available for reading, given a read and write pointer..
*
* @param {number} rd
* @param {number} wr
*/
_availableRead(rd, wr) {
if (wr > rd) {
return wr - rd;
}
return wr + this._storageCapacity() - rd;
}
/**
* Number of elements available from writing, given a read and write pointer.
*
* @param {number} rd
* @param {number} wr
*/
_availableWrite(rd, wr) {
let rv = rd - wr - 1;
if (wr >= rd) {
rv += this._storageCapacity();
}
return rv;
}
/**
* The size of the storage for elements not accounting the space for the index.
*/
_storageCapacity() {
return this._capacity;
}
/**
* Copy `size` elements from `input`, starting at offset `offset_input`, to
* `output`, starting at offset `offset_output`.
*
* @param {TypedArray} input
* @param {number} offsetInput
* @param {TypedArray} output
* @param {number} offsetOutput
* @param {number} size
*/
_copy(input, offsetInput, output, offsetOutput, size) {
for (let i = 0; i < size; i++) {
output[offsetOutput + i] = input[offsetInput + i];
}
}
}
if (audioWorkletGlobalScope.AudioWorkletProcessor) {
/** @type {WamSDKBaseModuleScope} */
const ModuleScope = audioWorkletGlobalScope.webAudioModules.getModuleScope(moduleId);
if (!ModuleScope.RingBuffer) ModuleScope.RingBuffer = RingBuffer;
}
return RingBuffer;
};
export default getRingBuffer;
/* Usage in main thread:
import executable from 'RingBuffer.js';
const RingBuffer = executable();
*/
/* Usage in audio thread:
// in main thread:
audioWorklet.addModule('RingBuffer.js');
// in audio thread
const { RingBuffer } = globalThis;
*/
/* Usage in audio thread with a build system:
// in main thread:
import getRingBuffer from 'RingBuffer.js';
const blob = new Blob([`(${getRingBuffer.toString()})(JSON.stringify(moduleId));`], { type: 'text/javascript' });
const url = window.URL.createObjectURL(blob);
audioWorklet.addModule(url);
// in audio thread
const { RingBuffer } = globalThis.webAudioModules.dependencies[moduleId];
*/