Spaces:
Running
Running
File size: 5,099 Bytes
b8cc2bf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | 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);
}
}
|