diff --git a/electron/ipc/ffmpeg/filters.test.ts b/electron/ipc/ffmpeg/filters.test.ts index bc28a3ca..3569d729 100644 --- a/electron/ipc/ffmpeg/filters.test.ts +++ b/electron/ipc/ffmpeg/filters.test.ts @@ -69,6 +69,32 @@ describe("getAudioSyncAdjustment", () => { "[1:a]apad=pad_dur=120.000,aresample=async=1:first_pts=0,asetpts=PTS-STARTPTS[aout]", ]); }); + + it("can add a small gain boost before resampling", () => { + const filterParts: string[] = []; + appendSyncedAudioFilter( + filterParts, + "[1:a]", + "aout", + getAudioSyncAdjustment(120, 120), + 1.4, + ); + + expect(filterParts).toEqual([ + "[1:a]volume=1.400,aresample=async=1:first_pts=0,asetpts=PTS-STARTPTS[aout]", + ]); + }); + + it("can prepend mic normalization filters before sync handling", () => { + const filterParts: string[] = []; + appendSyncedAudioFilter(filterParts, "[1:a]", "aout", getAudioSyncAdjustment(120, 120), { + preFilters: ["loudnorm=I=-16:TP=-1.5:LRA=11"], + }); + + expect(filterParts).toEqual([ + "[1:a]loudnorm=I=-16:TP=-1.5:LRA=11,aresample=async=1:first_pts=0,asetpts=PTS-STARTPTS[aout]", + ]); + }); }); describe("applyRecordedAudioStartDelay", () => { diff --git a/electron/ipc/ffmpeg/filters.ts b/electron/ipc/ffmpeg/filters.ts index 735e3c66..e6fa1253 100644 --- a/electron/ipc/ffmpeg/filters.ts +++ b/electron/ipc/ffmpeg/filters.ts @@ -102,8 +102,12 @@ export function appendSyncedAudioFilter( inputLabel: string, outputLabel: string, adjustment: AudioSyncAdjustment, + options: number | { volumeMultiplier?: number; preFilters?: string[] } = 1, ) { - const filters: string[] = []; + const volumeMultiplier = + typeof options === "number" ? options : (options.volumeMultiplier ?? 1); + const preFilters = typeof options === "number" ? [] : (options.preFilters ?? []); + const filters: string[] = [...preFilters]; if (adjustment.mode === "delay" && adjustment.delayMs > 0) { filters.push(`adelay=${adjustment.delayMs}|${adjustment.delayMs}`); @@ -117,6 +121,14 @@ export function appendSyncedAudioFilter( filters.push(`apad=pad_dur=${formatFfmpegSeconds(adjustment.durationDeltaMs)}`); } + if ( + Number.isFinite(volumeMultiplier) && + volumeMultiplier > 0 && + Math.abs(volumeMultiplier - 1) > 0.0005 + ) { + filters.push(`volume=${volumeMultiplier.toFixed(3)}`); + } + filters.push("aresample=async=1:first_pts=0", "asetpts=PTS-STARTPTS"); filterParts.push(`${inputLabel}${filters.join(",")}[${outputLabel}]`); } diff --git a/electron/ipc/recording/windows.ts b/electron/ipc/recording/windows.ts index 612cc9f0..e0336413 100644 --- a/electron/ipc/recording/windows.ts +++ b/electron/ipc/recording/windows.ts @@ -33,6 +33,9 @@ import { import { emitRecordingInterrupted } from "./events"; const execFileAsync = promisify(execFile); +// Match the browser path's "usable speech level" intent with a standard +// loudness pass on native Windows mic audio instead of a fixed tiny boost. +const WINDOWS_NATIVE_MIC_PRE_FILTERS = ["loudnorm=I=-16:TP=-1.5:LRA=11"]; export async function isNativeWindowsCaptureAvailable(): Promise { if (process.platform !== "win32") return false; @@ -266,7 +269,9 @@ export async function muxNativeWindowsVideoWithAudio( const micLabel = micPauseFilter ? "[mic_trimmed]" : "[2:a]"; appendSyncedAudioFilter(filterParts, systemLabel, "s", systemAdjustment); - appendSyncedAudioFilter(filterParts, micLabel, "m", micAdjustment); + appendSyncedAudioFilter(filterParts, micLabel, "m", micAdjustment, { + preFilters: WINDOWS_NATIVE_MIC_PRE_FILTERS, + }); filterParts.push("[s][m]amix=inputs=2:duration=longest:normalize=0[aout]"); await execFileAsync( @@ -312,7 +317,13 @@ export async function muxNativeWindowsVideoWithAudio( filterParts.push(pauseFilter); } const srcLabel = pauseFilter ? "[trimmed_audio]" : "[1:a]"; - appendSyncedAudioFilter(filterParts, srcLabel, "aout", singleAdjustment); + appendSyncedAudioFilter( + filterParts, + srcLabel, + "aout", + singleAdjustment, + audioInputs[0] === "mic" ? { preFilters: WINDOWS_NATIVE_MIC_PRE_FILTERS } : 1, + ); await execFileAsync( ffmpegPath,