From 2573308f79414da4244caa46a28e6b33a1b7c4b0 Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Sat, 25 Apr 2026 15:16:19 +0700 Subject: [PATCH] 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) --- electron/ipc/ffmpeg/filters.test.ts | 43 +++++++++++++++++++++++++++++ electron/ipc/ffmpeg/filters.ts | 15 ++++++++++ electron/ipc/types.ts | 2 +- 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/electron/ipc/ffmpeg/filters.test.ts b/electron/ipc/ffmpeg/filters.test.ts index deab8017..d86efbc7 100644 --- a/electron/ipc/ffmpeg/filters.test.ts +++ b/electron/ipc/ffmpeg/filters.test.ts @@ -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]", + ]); + }); }); diff --git a/electron/ipc/ffmpeg/filters.ts b/electron/ipc/ffmpeg/filters.ts index ce973f16..a975b4f3 100644 --- a/electron/ipc/ffmpeg/filters.ts +++ b/electron/ipc/ffmpeg/filters.ts @@ -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}]`); } diff --git a/electron/ipc/types.ts b/electron/ipc/types.ts index 5922fb0e..7c7de9c1 100644 --- a/electron/ipc/types.ts +++ b/electron/ipc/types.ts @@ -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;