mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 15:25:44 +00:00
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:
@@ -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)}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}]`);
|
||||
|
||||
Reference in New Issue
Block a user