mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 14:50:02 +00:00
fix(audio): renumber timestamps between apad and atrim in mixed branches (#3380)
On FFmpeg 5.x through 8.0.x the samples `apad` appends carry timestamps the following `atrim` misreads. A delayed branch then sounds at t=0 instead of its offset and, once four or more branches are mixed, the last one disappears from the output entirely. No error is raised; the render succeeds with wrong audio. Reverting to `apad=whole_dur=` is not an option: #2769 moved off that form because some builds reject the option outright ("Error applying option 'whole_dur': Option not found"). Inserting `asetpts=N/SR/TB` between the pad and the trim rebuilds the timestamps from the sample count using only filters every build ships, so it fixes the misplacement without giving up the portability that change bought. Verified on FFmpeg 4.2.7, 7.0.2, an 8.x nightly and 8.1.1: the current form is wrong on the middle two, the new form is correct on all four. audioPadTrim.ts also pads with apad+atrim but has no adelay and is correct on every version tested, so it is left alone. Closes #3344
This commit is contained in:
@@ -296,7 +296,7 @@ describe("processCompositionAudio", () => {
|
||||
|
||||
expect(filter).toContain("volume=0");
|
||||
expect(filter).toContain("[mixed]volume=1[out]");
|
||||
expect(filter).toContain("apad,atrim=0:2");
|
||||
expect(filter).toContain("apad,asetpts=N/SR/TB,atrim=0:2");
|
||||
expect(filter).not.toContain("whole_dur");
|
||||
expect(filter).not.toContain("normalize=");
|
||||
expect(filter).not.toContain("weights=");
|
||||
@@ -464,7 +464,7 @@ describe("processCompositionAudio", () => {
|
||||
// 2 s clip + the 1.9 s tail 0.6 + size * 2.6 generates.
|
||||
expect(filter).toContain("atrim=0:3.9,");
|
||||
// And still cut at the composition's end, so a tail cannot extend the video.
|
||||
expect(filter).toContain("apad,atrim=0:8");
|
||||
expect(filter).toContain("apad,asetpts=N/SR/TB,atrim=0:8");
|
||||
});
|
||||
|
||||
it("hands the volume envelope to the FX pass instead of ducking the file after it", async () => {
|
||||
@@ -1078,6 +1078,60 @@ describe("processCompositionAudio", () => {
|
||||
expect((filter?.match(/apad,/g) ?? []).length).toBe(trackCount);
|
||||
});
|
||||
|
||||
it("renumbers timestamps between apad and atrim on every mixed branch", async () => {
|
||||
// Regression: `apad` then `atrim` is the portable pad-to-length shape --
|
||||
// #2769 moved off `apad=whole_dur=` because some builds reject that option.
|
||||
// But on FFmpeg 5.x-8.0.x the padded samples carry timestamps `atrim`
|
||||
// misreads, so a delayed branch sounds at t=0 and, past three branches, the
|
||||
// last one vanishes from the mix. `asetpts=N/SR/TB` between the two rebuilds
|
||||
// the timestamps from the sample count and costs no portability, since all
|
||||
// three filters exist in every build we support.
|
||||
const baseDir = mkdtempSync(join(tmpdir(), "hf-audio-base-"));
|
||||
const workDir = mkdtempSync(join(tmpdir(), "hf-audio-work-"));
|
||||
tempDirs.push(baseDir, workDir);
|
||||
writeFileSync(join(baseDir, "a.wav"), "stub");
|
||||
writeFileSync(join(baseDir, "b.wav"), "stub");
|
||||
|
||||
// Two branches, the second delayed: the shape that misplaced audio.
|
||||
await processCompositionAudio(
|
||||
[
|
||||
{
|
||||
id: "a",
|
||||
src: "a.wav",
|
||||
start: 0,
|
||||
end: 1,
|
||||
mediaStart: 0,
|
||||
layer: 0,
|
||||
volume: 1,
|
||||
type: "audio",
|
||||
},
|
||||
{
|
||||
id: "b",
|
||||
src: "b.wav",
|
||||
start: 4,
|
||||
end: 5,
|
||||
mediaStart: 0,
|
||||
layer: 1,
|
||||
volume: 1,
|
||||
type: "audio",
|
||||
},
|
||||
],
|
||||
baseDir,
|
||||
workDir,
|
||||
join(baseDir, "out.m4a"),
|
||||
8,
|
||||
);
|
||||
|
||||
const filter = capturedFilterScripts.at(-1) ?? "";
|
||||
const branches = filter.match(/apad[^;]*/g) ?? [];
|
||||
expect(branches).toHaveLength(2);
|
||||
for (const branch of branches) {
|
||||
expect(branch).toMatch(/^apad,asetpts=N\/SR\/TB,atrim=0:/);
|
||||
}
|
||||
// The portability constraint #2769 established still holds.
|
||||
expect(filter).not.toContain("whole_dur");
|
||||
});
|
||||
|
||||
it("retries with the current file-valued filter option when a nightly removes the legacy alias", async () => {
|
||||
const baseDir = mkdtempSync(join(tmpdir(), "hf-audio-base-"));
|
||||
const workDir = mkdtempSync(join(tmpdir(), "hf-audio-work-"));
|
||||
|
||||
@@ -709,8 +709,19 @@ async function mixAudioTracks(
|
||||
// can run over what follows but never past the end of the video.
|
||||
const trimDuration = track.end - track.start + (track.tailSeconds ?? 0);
|
||||
const volumeFilter = buildVolumeExpression(track, ignoreAutomation);
|
||||
// `apad` then `atrim` is the portable pad-to-length shape: PR #2769 moved
|
||||
// off `apad=whole_dur=` because some FFmpeg builds reject that option
|
||||
// outright ("Error applying option 'whole_dur': Option not found").
|
||||
// But on FFmpeg 5.x through 8.0.x the samples `apad` appends carry
|
||||
// timestamps the following `atrim` misreads, so a delayed branch lands at
|
||||
// t=0 and, once four or more branches are mixed, the last one disappears
|
||||
// entirely. `asetpts=N/SR/TB` renumbers the padded stream from the sample
|
||||
// count before the trim reads it, which fixes the misplacement while
|
||||
// keeping the filter set every build supports. Verified correct on 4.2.7,
|
||||
// 7.0.2, an 8.x nightly and 8.1.1; the un-reset form is wrong on the
|
||||
// middle two.
|
||||
filterParts.push(
|
||||
`[${i}:a]atrim=0:${formatFilterNumber(trimDuration)},${volumeFilter},adelay=${delayMs}|${delayMs},apad,atrim=0:${formatFilterNumber(totalDuration)}[a${i}]`,
|
||||
`[${i}:a]atrim=0:${formatFilterNumber(trimDuration)},${volumeFilter},adelay=${delayMs}|${delayMs},apad,asetpts=N/SR/TB,atrim=0:${formatFilterNumber(totalDuration)}[a${i}]`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -211,8 +211,9 @@ async function mixTracks(
|
||||
const delayMs = Math.round(track.start * 1000);
|
||||
const trimDuration = track.duration > 0 ? track.duration : totalDuration;
|
||||
|
||||
// See audioMixer.ts for why asetpts sits between apad and atrim.
|
||||
filterParts.push(
|
||||
`[${i}:a]atrim=0:${trimDuration},volume=${track.volume},adelay=${delayMs}|${delayMs},apad,atrim=0:${totalDuration}[a${i}]`,
|
||||
`[${i}:a]atrim=0:${trimDuration},volume=${track.volume},adelay=${delayMs}|${delayMs},apad,asetpts=N/SR/TB,atrim=0:${totalDuration}[a${i}]`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user