From 9a3ca785097ad86e0b7832cf8428b308e2a01ff9 Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Sun, 26 Apr 2026 21:59:46 +0700 Subject: [PATCH 1/3] fix(export): use tempo filters for speed audio --- electron/ipc/nativeVideoExport.test.ts | 8 ++++---- electron/ipc/nativeVideoExport.ts | 19 ++++++++++++------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/electron/ipc/nativeVideoExport.test.ts b/electron/ipc/nativeVideoExport.test.ts index 855e3c07f..c19d6c826 100644 --- a/electron/ipc/nativeVideoExport.test.ts +++ b/electron/ipc/nativeVideoExport.test.ts @@ -20,7 +20,7 @@ describe("buildTrimmedSourceAudioFilter", () => { }); describe("buildEditedTrackSourceAudioFilter", () => { - it("builds a concat filtergraph that pitch-shifts via asetrate for speed changes", () => { + it("builds a concat filtergraph that applies tempo filters for speed changes", () => { const filter = buildEditedTrackSourceAudioFilter( [ { startMs: 0, endMs: 2_000, speed: 1 }, @@ -31,19 +31,19 @@ describe("buildEditedTrackSourceAudioFilter", () => { expect(filter).toBe( "[1:a]atrim=start=0.000:end=2.000,asetpts=PTS-STARTPTS[edited_audio_0];" + - "[1:a]atrim=start=2.000:end=6.000,asetpts=PTS-STARTPTS,asetrate=66150,aresample=44100[edited_audio_1];" + + "[1:a]atrim=start=2.000:end=6.000,asetpts=PTS-STARTPTS,atempo=1.500000[edited_audio_1];" + "[edited_audio_0][edited_audio_1]concat=n=2:v=0:a=1[aout]", ); }); - it("builds a filtergraph for slowdown segments by lowering then resampling the source rate", () => { + it("builds a filtergraph for slowdown segments with a tempo filter", () => { const filter = buildEditedTrackSourceAudioFilter( [{ startMs: 0, endMs: 2_000, speed: 0.5 }], 44_100, ); expect(filter).toBe( - "[1:a]atrim=start=0.000:end=2.000,asetpts=PTS-STARTPTS,asetrate=22050,aresample=44100[edited_audio_0];" + + "[1:a]atrim=start=0.000:end=2.000,asetpts=PTS-STARTPTS,atempo=0.500000[edited_audio_0];" + "[edited_audio_0]anull[aout]", ); }); diff --git a/electron/ipc/nativeVideoExport.ts b/electron/ipc/nativeVideoExport.ts index 5ca8fa081..f0fa6162f 100644 --- a/electron/ipc/nativeVideoExport.ts +++ b/electron/ipc/nativeVideoExport.ts @@ -1,4 +1,8 @@ +import { buildAtempoFilters } from "./ffmpeg/filters"; + const NATIVE_EXPORT_INPUT_BYTES_PER_PIXEL = 4; +const MIN_EDITED_TRACK_TEMPO_SPEED = 0.5; +const MAX_EDITED_TRACK_TEMPO_SPEED = 2; export type NativeExportEncodingMode = "fast" | "balanced" | "quality"; @@ -207,7 +211,11 @@ export function buildEditedTrackSourceAudioFilter( const label = `edited_audio_${index}`; const speed = segment.speed; - if (!Number.isFinite(speed) || speed <= 0) { + if ( + !Number.isFinite(speed) || + speed < MIN_EDITED_TRACK_TEMPO_SPEED || + speed > MAX_EDITED_TRACK_TEMPO_SPEED + ) { hasInvalidSegment = true; return; } @@ -218,16 +226,13 @@ export function buildEditedTrackSourceAudioFilter( ]; if (Math.abs(speed - 1) > 0.0001) { - const adjustedSampleRate = Math.round(normalizedSourceSampleRate * speed); - if (!Number.isSafeInteger(adjustedSampleRate) || adjustedSampleRate < 1) { + const tempoFilters = buildAtempoFilters(speed); + if (tempoFilters.length === 0) { hasInvalidSegment = true; return; } - segmentFilter.push( - `asetrate=${adjustedSampleRate}`, - `aresample=${normalizedSourceSampleRate}`, - ); + segmentFilter.push(...tempoFilters); } filterParts.push(`${segmentFilter.join(",")}[${label}]`); From aa245efba7259f604af04e0880e9efe90c0ffc32 Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Sun, 26 Apr 2026 22:51:09 +0700 Subject: [PATCH 2/3] fix(export): tolerate near-unity audio speeds --- electron/ipc/ffmpeg/filters.ts | 3 ++- electron/ipc/nativeVideoExport.test.ts | 12 ++++++++++++ electron/ipc/nativeVideoExport.ts | 14 ++++++-------- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/electron/ipc/ffmpeg/filters.ts b/electron/ipc/ffmpeg/filters.ts index e6fa12533..dfc59fbbe 100644 --- a/electron/ipc/ffmpeg/filters.ts +++ b/electron/ipc/ffmpeg/filters.ts @@ -1,6 +1,7 @@ import type { AudioSyncAdjustment, PauseSegment } from "../types"; const MAX_AUDIO_SYNC_DELAY_MS = 15000; +export const ATEMPO_FILTER_EPSILON = 0.0005; export function buildAtempoFilters(tempoRatio: number): string[] { if (!Number.isFinite(tempoRatio) || tempoRatio <= 0) { @@ -20,7 +21,7 @@ export function buildAtempoFilters(tempoRatio: number): string[] { remaining /= 2.0; } - if (Math.abs(remaining - 1) > 0.0005) { + if (Math.abs(remaining - 1) > ATEMPO_FILTER_EPSILON) { filters.push(`atempo=${remaining.toFixed(6)}`); } diff --git a/electron/ipc/nativeVideoExport.test.ts b/electron/ipc/nativeVideoExport.test.ts index c19d6c826..7327a9a43 100644 --- a/electron/ipc/nativeVideoExport.test.ts +++ b/electron/ipc/nativeVideoExport.test.ts @@ -48,6 +48,18 @@ describe("buildEditedTrackSourceAudioFilter", () => { ); }); + it("treats near-unity speed changes as unchanged audio", () => { + const filter = buildEditedTrackSourceAudioFilter( + [{ startMs: 0, endMs: 2_000, speed: 1.0002 }], + 44_100, + ); + + expect(filter).toBe( + "[1:a]atrim=start=0.000:end=2.000,asetpts=PTS-STARTPTS[edited_audio_0];" + + "[edited_audio_0]anull[aout]", + ); + }); + it("returns null when the edited-track filtergraph inputs are incomplete", () => { expect(buildEditedTrackSourceAudioFilter([], 44_100)).toBeNull(); expect( diff --git a/electron/ipc/nativeVideoExport.ts b/electron/ipc/nativeVideoExport.ts index f0fa6162f..3a8d812bf 100644 --- a/electron/ipc/nativeVideoExport.ts +++ b/electron/ipc/nativeVideoExport.ts @@ -1,4 +1,4 @@ -import { buildAtempoFilters } from "./ffmpeg/filters"; +import { ATEMPO_FILTER_EPSILON, buildAtempoFilters } from "./ffmpeg/filters"; const NATIVE_EXPORT_INPUT_BYTES_PER_PIXEL = 4; const MIN_EDITED_TRACK_TEMPO_SPEED = 0.5; @@ -225,14 +225,12 @@ export function buildEditedTrackSourceAudioFilter( "asetpts=PTS-STARTPTS", ]; - if (Math.abs(speed - 1) > 0.0001) { - const tempoFilters = buildAtempoFilters(speed); - if (tempoFilters.length === 0) { - hasInvalidSegment = true; - return; - } - + const tempoFilters = buildAtempoFilters(speed); + if (tempoFilters.length > 0) { segmentFilter.push(...tempoFilters); + } else if (Math.abs(speed - 1) > ATEMPO_FILTER_EPSILON) { + hasInvalidSegment = true; + return; } filterParts.push(`${segmentFilter.join(",")}[${label}]`); From c713fc2eec6dc90824dc4c24aa409036e220c606 Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Sun, 26 Apr 2026 22:57:08 +0700 Subject: [PATCH 3/3] test(export): cover tempo epsilon boundary --- electron/ipc/nativeVideoExport.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/electron/ipc/nativeVideoExport.test.ts b/electron/ipc/nativeVideoExport.test.ts index 7327a9a43..6f7fbb4a7 100644 --- a/electron/ipc/nativeVideoExport.test.ts +++ b/electron/ipc/nativeVideoExport.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest"; +import { ATEMPO_FILTER_EPSILON } from "./ffmpeg/filters"; import { buildEditedTrackSourceAudioFilter, buildTrimmedSourceAudioFilter, @@ -60,6 +61,20 @@ describe("buildEditedTrackSourceAudioFilter", () => { ); }); + it("treats exact epsilon speed changes as unchanged audio", () => { + for (const speed of [1 - ATEMPO_FILTER_EPSILON, 1 + ATEMPO_FILTER_EPSILON]) { + const filter = buildEditedTrackSourceAudioFilter( + [{ startMs: 0, endMs: 2_000, speed }], + 44_100, + ); + + expect(filter).toBe( + "[1:a]atrim=start=0.000:end=2.000,asetpts=PTS-STARTPTS[edited_audio_0];" + + "[edited_audio_0]anull[aout]", + ); + } + }); + it("returns null when the edited-track filtergraph inputs are incomplete", () => { expect(buildEditedTrackSourceAudioFilter([], 44_100)).toBeNull(); expect(