Spaces:
Running
Running
| import { IRingBuffer } from './types'; | |
| /** | |
| * Fixed-size circular buffer for PCM audio samples. | |
| * Uses global frame offsets for absolute addressing. | |
| */ | |
| export class RingBuffer implements IRingBuffer { | |
| readonly sampleRate: number; | |
| readonly maxFrames: number; | |
| private buffer: Float32Array; | |
| private currentFrame: number = 0; // The next frame to be written (global) | |
| constructor(sampleRate: number, durationSeconds: number) { | |
| this.sampleRate = sampleRate; | |
| this.maxFrames = Math.floor(sampleRate * durationSeconds); | |
| this.buffer = new Float32Array(this.maxFrames); | |
| } | |
| /** | |
| * Append PCM frames to the buffer. | |
| */ | |
| write(chunk: Float32Array): void { | |
| let chunkLength = chunk.length; | |
| let dataToWrite = chunk; | |
| // If chunk is larger than buffer (unlikely but handle it), only take the end | |
| if (chunkLength > this.maxFrames) { | |
| const start = chunkLength - this.maxFrames; | |
| dataToWrite = chunk.subarray(start); | |
| // Advance frame counter for the skipped part | |
| this.currentFrame += start; | |
| // Now we only write maxFrames | |
| chunkLength = this.maxFrames; | |
| } | |
| const writePos = this.currentFrame % this.maxFrames; | |
| const remainingSpace = this.maxFrames - writePos; | |
| if (chunkLength <= remainingSpace) { | |
| // Single operation | |
| this.buffer.set(dataToWrite, writePos); | |
| } else { | |
| // Wrap around | |
| this.buffer.set(dataToWrite.subarray(0, remainingSpace), writePos); | |
| this.buffer.set(dataToWrite.subarray(remainingSpace), 0); | |
| } | |
| this.currentFrame += chunkLength; | |
| } | |
| /** | |
| * Read samples from [startFrame, endFrame). | |
| * @throws RangeError if data has been overwritten by circular buffer. | |
| */ | |
| read(startFrame: number, endFrame: number): Float32Array { | |
| if (startFrame < 0) throw new RangeError('startFrame must be non-negative'); | |
| if (endFrame <= startFrame) return new Float32Array(0); | |
| const baseFrame = this.getBaseFrameOffset(); | |
| if (startFrame < baseFrame) { | |
| throw new RangeError( | |
| `Requested frame ${startFrame} has been overwritten. Oldest available: ${baseFrame}` | |
| ); | |
| } | |
| if (endFrame > this.currentFrame) { | |
| throw new RangeError( | |
| `Requested frame ${endFrame} is in the future. Latest available: ${this.currentFrame}` | |
| ); | |
| } | |
| const length = endFrame - startFrame; | |
| const result = new Float32Array(length); | |
| const readPos = startFrame % this.maxFrames; | |
| const remainingAtEnd = this.maxFrames - readPos; | |
| if (length <= remainingAtEnd) { | |
| result.set(this.buffer.subarray(readPos, readPos + length)); | |
| } else { | |
| result.set(this.buffer.subarray(readPos, this.maxFrames)); | |
| result.set(this.buffer.subarray(0, length - remainingAtEnd), remainingAtEnd); | |
| } | |
| return result; | |
| } | |
| /** | |
| * Read samples from [startFrame, endFrame) into a caller-supplied buffer. | |
| * Zero-allocation: writes into `dest` starting at offset 0. | |
| * Returns the number of samples actually written (may be less than | |
| * dest.length if the requested range is shorter). | |
| * @throws RangeError if data has been overwritten or is in the future. | |
| */ | |
| readInto(startFrame: number, endFrame: number, dest: Float32Array): number { | |
| if (startFrame < 0) throw new RangeError('startFrame must be non-negative'); | |
| if (endFrame <= startFrame) return 0; | |
| const baseFrame = this.getBaseFrameOffset(); | |
| if (startFrame < baseFrame) { | |
| throw new RangeError( | |
| `Requested frame ${startFrame} has been overwritten. Oldest available: ${baseFrame}` | |
| ); | |
| } | |
| if (endFrame > this.currentFrame) { | |
| throw new RangeError( | |
| `Requested frame ${endFrame} is in the future. Latest available: ${this.currentFrame}` | |
| ); | |
| } | |
| const length = endFrame - startFrame; | |
| const readPos = startFrame % this.maxFrames; | |
| const remainingAtEnd = this.maxFrames - readPos; | |
| if (length <= remainingAtEnd) { | |
| dest.set(this.buffer.subarray(readPos, readPos + length)); | |
| } else { | |
| dest.set(this.buffer.subarray(readPos, this.maxFrames)); | |
| dest.set(this.buffer.subarray(0, length - remainingAtEnd), remainingAtEnd); | |
| } | |
| return length; | |
| } | |
| getCurrentFrame(): number { | |
| return this.currentFrame; | |
| } | |
| getFillCount(): number { | |
| return Math.min(this.currentFrame, this.maxFrames); | |
| } | |
| getSize(): number { | |
| return this.maxFrames; | |
| } | |
| getCurrentTime(): number { | |
| return this.currentFrame / this.sampleRate; | |
| } | |
| getBaseFrameOffset(): number { | |
| return Math.max(0, this.currentFrame - this.maxFrames); | |
| } | |
| reset(): void { | |
| this.currentFrame = 0; | |
| this.buffer.fill(0); | |
| } | |
| } | |