diff --git a/src/components/ExportSettings.tsx b/src/components/ExportSettings.tsx index 6eee9b9b..958b017a 100644 --- a/src/components/ExportSettings.tsx +++ b/src/components/ExportSettings.tsx @@ -196,10 +196,19 @@ export default function ExportSettings({ + + Noise Reduction + +<<<<<<< HEAD +======= + + +

+ Reduce grain in low-light footage +

+ +
+ {(['off', 'light', 'medium', 'heavy'] as const).map((level) => ( + + ))} +
+ + {recipe.noiseReduction !== 'off' && ( +
+ + ⚠ Note: significantly increases processing time. + +
+ )} +>>>>>>> 92b9083 (feat: add noise reduction filter with Light/Medium/Heavy presets (closes #129)) ); diff --git a/src/components/SubtitleGenerator.tsx b/src/components/SubtitleGenerator.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/lib/constants.ts b/src/lib/constants.ts index e713f779..2707fe22 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -22,6 +22,5 @@ export const DEFAULT_RECIPE: EditRecipe = { denoise: false, soundOnCompletion: false, normalizeAudio: false, - textOverlays: [], - version: RECIPE_VERSION, + noiseReduction: 'off', }; diff --git a/src/lib/ffmpeg.ts b/src/lib/ffmpeg.ts index 625387d2..7f75160a 100644 --- a/src/lib/ffmpeg.ts +++ b/src/lib/ffmpeg.ts @@ -336,7 +336,15 @@ export function buildVideoFilter(recipe: EditRecipe, targetW: number, targetH: n if (recipe.stabilization) { filters.push("deshake"); } - +// Noise reduction using hqdn3d filter + if (recipe.noiseReduction && recipe.noiseReduction !== 'off') { + const noiseMap = { + light: 'hqdn3d=2.0:1.5:3.0:2.5', + medium: 'hqdn3d=4.0:3.0:6.0:4.5', + heavy: 'hqdn3d=6.0:5.0:9.0:6.5', + } + filters.push(noiseMap[recipe.noiseReduction]) + } if (recipe.rotate === 90) { filters.push("transpose=1"); } else if (recipe.rotate === 180) { @@ -392,8 +400,7 @@ export function buildVideoFilter(recipe: EditRecipe, targetW: number, targetH: n return filters.join(","); } -export function buildAudioFilter(speed: number, normalizeAudio: boolean): string { - if (speed <= 0) return ""; + export function buildAudioFilter(speed: number, normalizeAudio: boolean = false): string { const filters: string[] = []; let remaining = speed; diff --git a/src/lib/tests/ffmpeg.test.ts b/src/lib/tests/ffmpeg.test.ts index c2e42e74..a3a7817a 100644 --- a/src/lib/tests/ffmpeg.test.ts +++ b/src/lib/tests/ffmpeg.test.ts @@ -5,36 +5,30 @@ describe("buildAudioFilter", () => { it("should return an empty string for 1.0x speed", () => { expect(buildAudioFilter(1, false)).toBe(""); }); - it("should chain two 0.5x filters for 0.25x speed", () => { expect(buildAudioFilter(0.25, false)).toBe("atempo=0.5,atempo=0.5"); }); - it("should chain two 2.0x filters for 4.0x speed", () => { expect(buildAudioFilter(4, false)).toBe("atempo=2.0,atempo=2"); }); - it("should chain multiple 0.5x filters and a remainder for 0.1x speed", () => { - // 0.1 / 0.5 = 0.2 - // 0.2 / 0.5 = 0.4 - // 0.4 / 0.5 = 0.8 - // Result should be three 0.5s and one 0.8 expect(buildAudioFilter(0.1, false)).toBe("atempo=0.5,atempo=0.5,atempo=0.5,atempo=0.8"); }); - it("should chain multiple 2.0x filters and a remainder for 3.0x speed", () => { - // 3.0 / 2.0 = 1.5 expect(buildAudioFilter(3, false)).toBe("atempo=2.0,atempo=1.5"); }); - it("should handle boundary values inside the 0.5x-2.0x range without chaining", () => { expect(buildAudioFilter(0.5, false)).toBe("atempo=0.5"); +<<<<<<< HEAD expect(buildAudioFilter(2.0, false)).toBe("atempo=2"); // Note: Number(2.0.toFixed(4)) -> 2 +======= + expect(buildAudioFilter(2.0, false)).toBe("atempo=2"); +>>>>>>> 286e9fb (fix: add noiseReduction to EditRecipe type and fix test argument counts) expect(buildAudioFilter(1.5, false)).toBe("atempo=1.5"); expect(buildAudioFilter(0.75, false)).toBe("atempo=0.75"); }); - it("should chain properly for very large speeds", () => { +<<<<<<< HEAD // 10 / 2.0 = 5 // 5 / 2.0 = 2.5 // 2.5 / 2.0 = 1.25 @@ -44,5 +38,8 @@ describe("buildAudioFilter", () => { it("should append loudnorm filter when normalizeAudio is true", () => { const result = buildAudioFilter(1, true); expect(result).toContain("loudnorm"); +======= + expect(buildAudioFilter(10, false)).toBe("atempo=2.0,atempo=2.0,atempo=2.0,atempo=1.25"); +>>>>>>> 286e9fb (fix: add noiseReduction to EditRecipe type and fix test argument counts) }); -}); +}); \ No newline at end of file diff --git a/src/lib/types.ts b/src/lib/types.ts index deb816b8..2bd9df36 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -30,6 +30,7 @@ export interface EditRecipe { format: "mp4" | "webm" | "mkv" | "gif"; stabilization: boolean; denoise: boolean; + noiseReduction: 'off' | 'light' | 'medium' | 'heavy'; brightness: number; contrast: number; saturation: number; @@ -75,6 +76,39 @@ export type ExportStatus = | "done" | "error"; +export const SPEED_STEPS = [ + 0.25, + 0.5, + 0.75, + 1, + 1.25, + 1.5, + 2, + 4, +] as const; + +export const DEFAULT_RECIPE: EditRecipe = { + preset: "vertical-9-16", + customWidth: 1920, + customHeight: 1080, + framing: "fit", + trimStart: 0, + trimEnd: null, + rotate: 0, + keepAudio: true, + normalizeAudio: false, + speed: 1, + quality: 23, + format: "mp4", + stabilization: false, + noiseReduction: 'off', + brightness: 0, + contrast: 0, + saturation: 0, + soundOnCompletion: false, +}; + +>>>>>>> 92b9083 (feat: add noise reduction filter with Light/Medium/Heavy presets (closes #129)) export const MAX_FILE_SIZE = 2 * 1024 * 1024 * 1024;