// Ring buffer with audio samples. enum STATE { READ_POS = 0, // The current read position WRITE_POS = 1, // The current write position LENGTH = 2, // Clever way of saving the total number of enums values. } interface FrameCopyToOptions { frameCount?: number frameOffset?: number planeIndex: number } // This is implemented by AudioData in WebCodecs, but we don't import it because it's a DOM type. interface Frame { numberOfFrames: number numberOfChannels: number copyTo(dst: Float32Array, options: FrameCopyToOptions): void } // No prototype to make this easier to send via postMessage export class RingShared { state: SharedArrayBuffer channels: SharedArrayBuffer[] capacity: number constructor(channels: number, capacity: number) { // Store the current state in a separate ring buffer. this.state = new SharedArrayBuffer(STATE.LENGTH * Int32Array.BYTES_PER_ELEMENT) // Create a buffer for each audio channel this.channels = [] for (let i = 0; i < channels; i += 1) { const buffer = new SharedArrayBuffer(capacity * Float32Array.BYTES_PER_ELEMENT) this.channels.push(buffer) } this.capacity = capacity } } export class Ring { state: Int32Array channels: Float32Array[] capacity: number constructor(shared: RingShared) { this.state = new Int32Array(shared.state) this.channels = [] for (const channel of shared.channels) { this.channels.push(new Float32Array(channel)) } this.capacity = shared.capacity } // Write samples for single audio frame, returning the total number written. write(frame: Frame): number { const readPos = Atomics.load(this.state, STATE.READ_POS) const writePos = Atomics.load(this.state, STATE.WRITE_POS) const available = this.capacity - (writePos - readPos) if (available <= 0) return 0 const toWrite = Math.min(frame.numberOfFrames, available) const startPos = writePos const endPos = writePos + toWrite const startIndex = startPos % this.capacity const endIndex = endPos % this.capacity // Loop over each channel for (let i = 0; i < this.channels.length; i += 1) { const channel = this.channels[i] // If the AudioData doesn't have enough channels, duplicate it. const planeIndex = Math.min(i, frame.numberOfChannels - 1) if (startIndex < endIndex) { // One continuous range to copy. const full = channel.subarray(startIndex, endIndex) frame.copyTo(full, { planeIndex, frameCount: endIndex - startIndex, }) } else { const first = channel.subarray(startIndex) const second = channel.subarray(0, endIndex) frame.copyTo(first, { planeIndex, frameCount: first.length, }) // We need this conditional when startIndex == 0 and endIndex == 0 // When capacity=4410 and frameCount=1024, this was happening 52s into the audio. if (second.length) { frame.copyTo(second, { planeIndex, frameOffset: first.length, frameCount: second.length, }) } } } Atomics.store(this.state, STATE.WRITE_POS, endPos) return toWrite } read(dst: Float32Array[]): number { const readPos = Atomics.load(this.state, STATE.READ_POS) const writePos = Atomics.load(this.state, STATE.WRITE_POS) const startPos = readPos let endPos = startPos + dst[0].length if (endPos > writePos) { endPos = writePos if (endPos <= startPos) { // Nothing to read return 0 } } const startIndex = startPos % this.capacity const endIndex = endPos % this.capacity // Loop over each channel for (let i = 0; i < dst.length; i += 1) { if (i >= this.channels.length) { // ignore excess channels } const input = this.channels[i] const output = dst[i] if (startIndex < endIndex) { const full = input.subarray(startIndex, endIndex) output.set(full) } else { const first = input.subarray(startIndex) const second = input.subarray(0, endIndex) output.set(first) output.set(second, first.length) } } Atomics.store(this.state, STATE.READ_POS, endPos) return endPos - startPos } clear() { const pos = Atomics.load(this.state, STATE.WRITE_POS) Atomics.store(this.state, STATE.READ_POS, pos) } size() { // TODO is this thread safe? const readPos = Atomics.load(this.state, STATE.READ_POS) const writePos = Atomics.load(this.state, STATE.WRITE_POS) return writePos - readPos } }