87 lines
2.2 KiB
TypeScript
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);
|
|
})();
|