keet-streaming / src /lib /audio /RingBuffer.ts
ysdede's picture
feat(space): migrate Hugging Face Space to keet SolidJS app
b8cc2bf
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);
}
}