import { spawn } from "child_process"; export interface AudioStats { peak: number; // True peak in dBTP lufsIntegrated: number; // LUFS-I lra: number; // Loudness range } async function runFFmpeg(args: string[]): Promise { return new Promise((resolve, reject) => { console.log("Running ffmpeg:", args.join(" ")); const ff = spawn("ffmpeg", args); let stdout = ""; let stderr = ""; ff.stdout.on("data", (data) => { const str = data.toString(); stdout += str; process.stdout.write(str); // debug passthrough }); ff.stderr.on("data", (data) => { const str = data.toString(); stderr += str; process.stdout.write(str); // debug passthrough }); ff.on("close", (code) => { console.log("ffmpeg exited with code:", code); if (code === 0) resolve(stdout + "\n" + stderr); else reject(new Error(stderr)); }); }); } function parseEbur128(output: string): AudioStats { const lines = output.split("\n").map(l => l.trim()); let lufsIntegrated = NaN; let lra = NaN; let peak = NaN; let inTruePeak = false; for (const line of lines) { // Integrated loudness (LUFS-I) const iMatch = line.match(/^I:\s*(-?\d+(\.\d+)?) LUFS/); if (iMatch) lufsIntegrated = parseFloat(iMatch[1]); // Loudness range (LRA) const lraMatch = line.match(/^LRA:\s*(-?\d+(\.\d+)?)/); if (lraMatch) lra = parseFloat(lraMatch[1]); // True peak (dBTP) if (line === "True peak:") inTruePeak = true; if (inTruePeak) { const pMatch = line.match(/Peak:\s*(-?\d+(\.\d+)?) dBFS/); if (pMatch) { peak = parseFloat(pMatch[1]); inTruePeak = false; } } } return { lufsIntegrated, lra, peak }; } export async function analyzeAudio(inputFile: string): Promise { const args = [ "-hide_banner", "-i", inputFile, "-filter_complex", "ebur128=peak=true:metadata=1:framelog=verbose", "-f", "null", "-", ]; const output = await runFFmpeg(args); return parseEbur128(output); } // Test (async () => { const stats = await analyzeAudio("/home/cj/Videos/28years/voiceover-compressed2.wav"); console.log("FINAL STATS:", stats); })();