Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion electron/ipc/ffmpeg/filters.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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)}`);
}

Expand Down
35 changes: 31 additions & 4 deletions electron/ipc/nativeVideoExport.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import { ATEMPO_FILTER_EPSILON } from "./ffmpeg/filters";
import {
buildEditedTrackSourceAudioFilter,
buildTrimmedSourceAudioFilter,
Expand All @@ -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 },
Expand All @@ -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(
Expand Down
27 changes: 15 additions & 12 deletions electron/ipc/nativeVideoExport.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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;
}
Expand All @@ -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}]`);
Expand Down