Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 55 additions & 2 deletions packages/producer/src/services/render/audioPadTrim.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
*/

import { describe, expect, it, mock } from "bun:test";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
buildPadTrimAudioArgs,
buildPadTrimAudioPlan,
Expand Down Expand Up @@ -306,7 +309,7 @@ describe("padOrTrimAudioToVideoFrameCount", () => {
expect(captured.args).toHaveLength(1);
});

it("attenuates the duration-normalized artifact from its measured AAC true peak", async () => {
it("attenuates the duration-normalized artifact with AAC correction headroom", async () => {
const calls: string[][] = [];
const { input } = harness({
video: { frameCount: 90, fpsNum: 30, fpsDen: 1 },
Expand All @@ -326,9 +329,59 @@ describe("padOrTrimAudioToVideoFrameCount", () => {
expect(result.error).toBe("synthetic correction stop");
expect(calls).toHaveLength(2);
const correctionArgs = calls[1]!;
expect(correctionArgs[correctionArgs.indexOf("-af") + 1]).toBe("volume=-2.500dB");
expect(correctionArgs[correctionArgs.indexOf("-af") + 1]).toBe("volume=-3.000dB");
expect(correctionArgs[correctionArgs.indexOf("-t") + 1]).toBe("3.000000");
});

it("uses correction headroom to converge within three AAC passes", async () => {
const dir = mkdtempSync(join(tmpdir(), "hf-aac-convergence-"));
const corrections: number[] = [];
let attenuationDb = 0;
const { input } = harness({
video: { frameCount: 90, fpsNum: 30, fpsDen: 1 },
audio: { durationSeconds: 3 },
});
input.videoPath = join(dir, "video.mp4");
input.audioPath = join(dir, "source.m4a");
input.outputPath = join(dir, "normalized.m4a");
input.probeAudioTruePeakDbfs = async () => attenuationDb * 0.4;
input.runFfmpeg = async (args) => {
const filter = args[args.indexOf("-af") + 1];
if (filter?.startsWith("volume=")) {
attenuationDb = Number(filter.slice("volume=".length, -2));
corrections.push(attenuationDb);
}
writeFileSync(args.at(-1)!, "");
return { success: true };
};

try {
const result = await padOrTrimAudioToVideoFrameCount(input);

expect(result.success, result.error).toBe(true);
expect(corrections).toEqual([-1.5, -2.4, -2.94]);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});

it("reports every measured peak and attenuation when correction is exhausted", async () => {
const peaks = [0, -0.4, -0.6, -0.8];
let probeIndex = 0;
const { input } = harness({
video: { frameCount: 90, fpsNum: 30, fpsDen: 1 },
audio: { durationSeconds: 3 },
});
input.probeAudioTruePeakDbfs = async () => peaks[probeIndex++]!;

const result = await padOrTrimAudioToVideoFrameCount(input);

expect(result.success).toBe(false);
expect(result.error).toContain("pass 0: 0.000 dBFS at 0.000 dB attenuation");
expect(result.error).toContain("pass 1: -0.400 dBFS at -1.500 dB attenuation");
expect(result.error).toContain("pass 2: -0.600 dBFS at -2.600 dB attenuation");
expect(result.error).toContain("pass 3: -0.800 dBFS at -3.500 dB attenuation");
});
});

// ── Public-path path redaction ────────────────────────────────────────────
Expand Down
12 changes: 10 additions & 2 deletions packages/producer/src/services/render/audioPadTrim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const AUDIO_DURATION_TOLERANCE_SECONDS = 0.001;

/** Delivery headroom applied after every AAC encode in this stage. */
export const AAC_DELIVERY_TRUE_PEAK_DBFS = -1;
const AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB = 0.5;
const MAX_TRUE_PEAK_CORRECTION_PASSES = 3;

export interface ProbeVideoFrameInfo {
Expand Down Expand Up @@ -431,10 +432,15 @@ async function enforceAacTruePeak(
try {
scratchDir = mkdtempSync(join(dirname(input.audioPath), ".true-peak-"));
const correctedPath = join(scratchDir, "audio.m4a");
const correctionTargetDbfs = AAC_DELIVERY_TRUE_PEAK_DBFS - AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB;
const measurements: string[] = [];
let attenuationDb = 0;
let measuredPath = input.audioPath;
for (let pass = 0; pass <= MAX_TRUE_PEAK_CORRECTION_PASSES; pass += 1) {
const truePeakDbfs = await input.probeTruePeak(measuredPath, input.signal);
measurements.push(
`pass ${pass}: ${truePeakDbfs.toFixed(3)} dBFS at ${attenuationDb.toFixed(3)} dB attenuation`,
);
if (Number.isNaN(truePeakDbfs) || truePeakDbfs === Number.POSITIVE_INFINITY) {
return { success: false, error: "audioPadTrim: FFmpeg reported an invalid true peak" };
}
Expand All @@ -444,7 +450,9 @@ async function enforceAacTruePeak(
}
if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;

attenuationDb += AAC_DELIVERY_TRUE_PEAK_DBFS - truePeakDbfs;
// Aim below the acceptance ceiling because this correction itself is
// another lossy AAC encode and can regenerate content-dependent peaks.
attenuationDb += correctionTargetDbfs - truePeakDbfs;
const result = await input.runner(
buildAacTruePeakCorrectionArgs(
input.audioPath,
Expand All @@ -467,7 +475,7 @@ async function enforceAacTruePeak(
}
return {
success: false,
error: `audioPadTrim: AAC true peak remained above ${AAC_DELIVERY_TRUE_PEAK_DBFS} dBFS after ${MAX_TRUE_PEAK_CORRECTION_PASSES} correction passes`,
error: `audioPadTrim: AAC true peak remained above ${AAC_DELIVERY_TRUE_PEAK_DBFS} dBFS after ${MAX_TRUE_PEAK_CORRECTION_PASSES} correction passes; ${measurements.join("; ")}`,
};
} catch (err) {
return {
Expand Down
Loading