fix(export): use tempo filters for speed audio

Use FFmpeg atempo filters for native edited-track speed changes, including near-unity speed tolerance coverage.
This commit is contained in:
Wiii
2026-04-26 22:59:20 +07:00
committed by GitHub
parent 61bdb0979f
commit 574cefc6bd
3 changed files with 48 additions and 17 deletions
+2 -1
View File
@@ -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)}`);
}
+31 -4
View File
@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import { ATEMPO_FILTER_EPSILON } from "./ffmpeg/filters";
import {
buildEditedTrackSourceAudioFilter,
buildTrimmedSourceAudioFilter,
@@ -20,7 +21,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,23 +32,49 @@ 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]",
);
});
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("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(
+15 -12
View File
@@ -1,4 +1,8 @@
import { ATEMPO_FILTER_EPSILON, 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;
}
@@ -217,17 +225,12 @@ export function buildEditedTrackSourceAudioFilter(
"asetpts=PTS-STARTPTS",
];
if (Math.abs(speed - 1) > 0.0001) {
const adjustedSampleRate = Math.round(normalizedSourceSampleRate * speed);
if (!Number.isSafeInteger(adjustedSampleRate) || adjustedSampleRate < 1) {
hasInvalidSegment = true;
return;
}
segmentFilter.push(
`asetrate=${adjustedSampleRate}`,
`aresample=${normalizedSourceSampleRate}`,
);
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}]`);