@volley/recognition-client-sdk
Version:
Recognition Service TypeScript/Node.js Client SDK
170 lines (147 loc) • 4.24 kB
text/typescript
/**
* Audio Ring Buffer
* Manages circular buffer for audio data with overflow detection
*/
export interface BufferedAudio {
data: ArrayBuffer | ArrayBufferView;
timestamp: number;
}
export interface AudioRingBufferConfig {
maxBufferDurationSec: number;
chunksPerSecond: number;
logger?: (level: 'debug' | 'info' | 'warn' | 'error', message: string, data?: any) => void;
}
export class AudioRingBuffer {
private buffer: BufferedAudio[] = [];
private bufferSize: number;
private writeIndex = 0;
private readIndex = 0;
private hasWrapped = false;
private totalBufferedBytes = 0;
private overflowCount = 0;
private chunksBuffered = 0;
private logger?: (level: 'debug' | 'info' | 'warn' | 'error', message: string, data?: any) => void;
constructor(config: AudioRingBufferConfig) {
this.bufferSize = config.maxBufferDurationSec * config.chunksPerSecond;
this.buffer = new Array(this.bufferSize);
if (config.logger) {
this.logger = config.logger;
}
}
/**
* Write audio chunk to ring buffer with overflow detection
*/
write(audioData: ArrayBuffer | ArrayBufferView): void {
const bytes = ArrayBuffer.isView(audioData) ? audioData.byteLength : audioData.byteLength;
// Write to current position
this.buffer[this.writeIndex] = {
data: audioData,
timestamp: Date.now()
};
// Advance write pointer
const nextWriteIndex = (this.writeIndex + 1) % this.bufferSize;
// Detect overflow: write caught up to read
if (nextWriteIndex === this.readIndex && this.writeIndex !== this.readIndex) {
this.hasWrapped = true;
this.overflowCount++;
// Log buffer overflow event
if (this.logger) {
this.logger('debug', 'Buffer overflow detected', {
bufferSize: this.bufferSize,
totalOverflows: this.overflowCount,
droppedChunk: this.buffer[this.readIndex]?.timestamp
});
}
// Move read pointer forward to make room (drop oldest)
this.readIndex = (this.readIndex + 1) % this.bufferSize;
}
this.writeIndex = nextWriteIndex;
this.chunksBuffered++;
this.totalBufferedBytes += bytes;
}
/**
* Read next chunk from buffer
*/
read(): BufferedAudio | null {
if (this.isEmpty()) {
return null;
}
const chunk = this.buffer[this.readIndex];
this.readIndex = (this.readIndex + 1) % this.bufferSize;
return chunk || null;
}
/**
* Read all buffered chunks without removing them
*/
readAll(): BufferedAudio[] {
const chunks: BufferedAudio[] = [];
let index = this.readIndex;
while (index !== this.writeIndex) {
const chunk = this.buffer[index];
if (chunk) {
chunks.push(chunk);
}
index = (index + 1) % this.bufferSize;
}
return chunks;
}
/**
* Flush all buffered data and advance read pointer
*/
flush(): BufferedAudio[] {
const chunks = this.readAll();
this.readIndex = this.writeIndex;
return chunks;
}
/**
* Get count of buffered chunks
*/
getBufferedCount(): number {
if (this.writeIndex >= this.readIndex) {
return this.writeIndex - this.readIndex;
} else {
// Wrapped around
return this.bufferSize - this.readIndex + this.writeIndex;
}
}
/**
* Check if buffer is empty
*/
isEmpty(): boolean {
return this.readIndex === this.writeIndex;
}
/**
* Check if buffer has overflowed
*/
isOverflowing(): boolean {
return this.hasWrapped;
}
/**
* Clear the buffer and reset all counters
* Frees memory by releasing all stored audio chunks
*/
clear(): void {
this.buffer = [];
this.writeIndex = 0;
this.readIndex = 0;
this.hasWrapped = false;
this.overflowCount = 0;
this.chunksBuffered = 0;
this.totalBufferedBytes = 0;
if (this.logger) {
this.logger('debug', 'Audio buffer cleared');
}
}
/**
* Get buffer statistics
*/
getStats() {
return {
chunksBuffered: this.chunksBuffered,
currentBufferedChunks: this.getBufferedCount(),
overflowCount: this.overflowCount,
hasWrapped: this.hasWrapped,
totalBufferedBytes: this.totalBufferedBytes
};
}
}