chore: continously write to audio file to avoid memory consumption + audio loss on crash

This commit is contained in:
Naomi Chopra 2025-07-08 06:19:20 +05:30 committed by haritabh-z01
parent c82f86143a
commit b32064c53c
4 changed files with 171 additions and 44 deletions

View file

@ -0,0 +1,147 @@
import * as fs from "node:fs";
import { logger } from "../main/logger";
/**
* StreamingWavWriter allows incremental writing of audio data to a WAV file.
* It writes a placeholder header initially and updates it when finalized.
*/
export class StreamingWavWriter {
private fileStream: fs.WriteStream;
private dataSize = 0;
private sampleRate: number;
private channels: number;
private bitDepth: number;
private isFinalized = false;
constructor(
filePath: string,
sampleRate = 16000,
channels = 1,
bitDepth = 16,
) {
this.sampleRate = sampleRate;
this.channels = channels;
this.bitDepth = bitDepth;
// Create write stream
this.fileStream = fs.createWriteStream(filePath);
// Write initial WAV header with placeholder sizes
this.writeHeader();
}
/**
* Write WAV header with current or placeholder sizes
*/
private writeHeader(): void {
const header = Buffer.alloc(44);
// RIFF chunk
header.write("RIFF", 0);
header.writeUInt32LE(this.dataSize + 36, 4); // File size - 8
header.write("WAVE", 8);
// fmt sub-chunk
header.write("fmt ", 12);
header.writeUInt32LE(16, 16); // Sub-chunk size
header.writeUInt16LE(1, 20); // Audio format (1 = PCM)
header.writeUInt16LE(this.channels, 22);
header.writeUInt32LE(this.sampleRate, 24);
header.writeUInt32LE(
(this.sampleRate * this.channels * this.bitDepth) / 8,
28,
); // Byte rate
header.writeUInt16LE((this.channels * this.bitDepth) / 8, 32); // Block align
header.writeUInt16LE(this.bitDepth, 34);
// data sub-chunk
header.write("data", 36);
header.writeUInt32LE(this.dataSize, 40);
this.fileStream.write(header);
}
/**
* Append audio data to the WAV file
* @param audioData Float32Array of audio samples
*/
async appendAudio(audioData: Float32Array): Promise<void> {
if (this.isFinalized) {
throw new Error("Cannot append to finalized WAV file");
}
// Convert Float32Array to Int16 buffer
const buffer = Buffer.alloc(audioData.length * 2);
for (let i = 0; i < audioData.length; i++) {
const sample = Math.max(-1, Math.min(1, audioData[i]));
buffer.writeInt16LE(Math.floor(sample * 32767), i * 2);
}
// Write to file
await new Promise<void>((resolve, reject) => {
this.fileStream.write(buffer, (err) => {
if (err) reject(err);
else resolve();
});
});
this.dataSize += buffer.length;
logger.transcription.debug("Appended audio to WAV file", {
samplesWritten: audioData.length,
bytesWritten: buffer.length,
totalDataSize: this.dataSize,
});
}
/**
* Finalize the WAV file by updating the header with correct sizes
*/
async finalize(): Promise<void> {
if (this.isFinalized) return;
this.isFinalized = true;
// Close the stream
await new Promise<void>((resolve) => {
this.fileStream.end(() => resolve());
});
// Reopen file to update header with correct sizes
const fd = await fs.promises.open(this.fileStream.path as string, "r+");
try {
// Update file size in RIFF header
const fileSizeBuffer = Buffer.alloc(4);
fileSizeBuffer.writeUInt32LE(this.dataSize + 36, 0);
await fd.write(fileSizeBuffer, 0, 4, 4);
// Update data size in data sub-chunk
const dataSizeBuffer = Buffer.alloc(4);
dataSizeBuffer.writeUInt32LE(this.dataSize, 0);
await fd.write(dataSizeBuffer, 0, 4, 40);
logger.transcription.info("Finalized WAV file", {
path: this.fileStream.path,
dataSize: this.dataSize,
duration: this.dataSize / 2 / this.sampleRate, // seconds
});
} finally {
await fd.close();
}
}
/**
* Get the current size of audio data written
*/
getDataSize(): number {
return this.dataSize;
}
/**
* Get the file path
*/
getFilePath(): string {
return this.fileStream.path as string;
}
}