fp/services/our/scripts/analyze.ts
CJ_Clippy bdc4894e3e
Some checks are pending
ci / build (push) Waiting to run
ci / test (push) Waiting to run
add audio analysis
2025-09-01 22:49:27 -08:00

87 lines
2.2 KiB
TypeScript

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<string> {
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<AudioStats> {
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);
})();