fix(audio-sync): cap leading-silence delay at 15s, anchor and pad-end above

When a recorded audio file is significantly shorter than the captured
video, getAudioSyncAdjustment previously returned mode "delay" for any
delta above 3% relative AND 1500ms absolute, prepending leading silence
equal to the full duration delta via `adelay=N|N`. For long recordings
where the duration delta can grow to tens of seconds or minutes,
issue #252 reports up to 2 minutes of pure silence at the start of a
10-min Windows recording before audio cuts in mid-clip.

Cap the delay branch at 15s. Above the cap, return a new "pad" mode
that anchors the audio at the start and emits `apad=pad_dur=...` so the
audio plays from t=0 and trailing silence fills the rest of the video
duration. Users perceive this as "audio recording cut out before video
ended" rather than "audio is shifted late vs video", which is the
clearer failure mode when the underlying capture produced a short
audio file.

Owner's existing assertion that 10s short audio gets delay (test #3 in
filters.test.ts: getAudioSyncAdjustment(120, 110) -> delay 10000) is
preserved unchanged because 10000 <= 15000.

Refs: #252

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
wiiiii123
2026-04-25 15:16:19 +07:00
co-authored by Claude Opus 4.7
parent f629cbf992
commit 2573308f79
3 changed files with 59 additions and 1 deletions
+43
View File
@@ -47,4 +47,47 @@ describe("getAudioSyncAdjustment", () => {
"[1:a]atempo=0.975000,aresample=async=1:first_pts=0,asetpts=PTS-STARTPTS[aout]",
]);
});
it("anchors and pads at end when the duration delta exceeds the delay cap (long recording case)", () => {
// Reporter case in issue #252: 10-min video with audio probed as ~8 min.
// Previously this returned mode "delay" with delayMs 120000, prepending
// 2 minutes of leading silence via `adelay`. That is jarring on long
// recordings. Above the cap we anchor at the start instead.
expect(getAudioSyncAdjustment(600, 480)).toEqual({
mode: "pad",
delayMs: 0,
tempoRatio: 1,
durationDeltaMs: 120000,
});
});
it("anchors and pads at end when delta is just over the delay cap", () => {
// 35-second short on a 10-min recording — the earlier symptom on issue
// #252 before the regression to ~2 min. Above the 15s cap, anchor.
expect(getAudioSyncAdjustment(600, 565)).toEqual({
mode: "pad",
delayMs: 0,
tempoRatio: 1,
durationDeltaMs: 35000,
});
});
it("keeps the existing delay branch for moderately short audio at the cap boundary", () => {
// Boundary: 15-second short stays in delay mode (cap is strictly >).
expect(getAudioSyncAdjustment(120, 105)).toEqual({
mode: "delay",
delayMs: 15000,
tempoRatio: 1,
durationDeltaMs: 15000,
});
});
it("emits apad with trailing silence for the pad mode instead of leading adelay", () => {
const filterParts: string[] = [];
appendSyncedAudioFilter(filterParts, "[1:a]", "aout", getAudioSyncAdjustment(600, 480));
expect(filterParts).toEqual([
"[1:a]apad=pad_dur=120.000,aresample=async=1:first_pts=0,asetpts=PTS-STARTPTS[aout]",
]);
});
});
+15
View File
@@ -1,5 +1,12 @@
import type { AudioSyncAdjustment, PauseSegment } from "../types";
// When the audio file is significantly shorter than the video, prepending
// leading silence equal to the full duration delta (the `delay` branch) is
// reasonable for small deltas but becomes very jarring at tens of seconds:
// the user hears a long silence before audio cuts in mid-clip. Above this
// cap, anchor audio at the start and pad trailing silence instead.
const MAX_AUDIO_SYNC_DELAY_MS = 15000;
export function buildAtempoFilters(tempoRatio: number): string[] {
if (!Number.isFinite(tempoRatio) || tempoRatio <= 0) {
return [];
@@ -58,6 +65,10 @@ export function getAudioSyncAdjustment(
return { mode: "tempo", delayMs: 0, tempoRatio, durationDeltaMs };
}
if (durationDeltaMs > MAX_AUDIO_SYNC_DELAY_MS) {
return { mode: "pad", delayMs: 0, tempoRatio: 1, durationDeltaMs };
}
return { mode: "delay", delayMs: durationDeltaMs, tempoRatio: 1, durationDeltaMs };
}
@@ -77,6 +88,10 @@ export function appendSyncedAudioFilter(
filters.push(...buildAtempoFilters(adjustment.tempoRatio));
}
if (adjustment.mode === "pad" && adjustment.durationDeltaMs > 0) {
filters.push(`apad=pad_dur=${(adjustment.durationDeltaMs / 1000).toFixed(3)}`);
}
filters.push("aresample=async=1:first_pts=0", "asetpts=PTS-STARTPTS");
filterParts.push(`${inputLabel}${filters.join(",")}[${outputLabel}]`);
}
+1 -1
View File
@@ -156,7 +156,7 @@ export type UiohookModuleNamespace = {
};
export type AudioSyncAdjustment = {
mode: "none" | "tempo" | "delay";
mode: "none" | "tempo" | "delay" | "pad";
delayMs: number;
tempoRatio: number;
durationDeltaMs: number;