From 991225cc403d9c883f9bd9d506e5d82445f8f9a5 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Mon, 27 Oct 2025 12:19:19 -0700 Subject: [PATCH] Add native 48kHz support and input resampling to audio mixer --- crates/recording/src/feeds/microphone.rs | 12 ++++ crates/recording/src/sources/audio_mixer.rs | 76 +++++++++++++-------- 2 files changed, 61 insertions(+), 27 deletions(-) diff --git a/crates/recording/src/feeds/microphone.rs b/crates/recording/src/feeds/microphone.rs index 719b03e083..270e9c0e7b 100644 --- a/crates/recording/src/feeds/microphone.rs +++ b/crates/recording/src/feeds/microphone.rs @@ -129,6 +129,8 @@ impl MicrophoneFeed { fn get_usable_device(device: Device) -> Option<(String, Device, SupportedStreamConfig)> { let device_name_for_logging = device.name().ok(); + let preferred_rate = cpal::SampleRate(48_000); + let result = device .supported_input_configs() .map_err(|error| { @@ -149,6 +151,16 @@ fn get_usable_device(device: Device) -> Option<(String, Device, SupportedStreamC .then(b.max_sample_rate().cmp(&a.max_sample_rate())) }); + // First try to find a config that natively supports 48 kHz so we + // don't have to rely on resampling later. + if let Some(config) = configs.iter().find(|config| { + ffmpeg_sample_format_for(config.sample_format()).is_some() + && config.min_sample_rate().0 <= preferred_rate.0 + && config.max_sample_rate().0 >= preferred_rate.0 + }) { + return Some(config.clone().with_sample_rate(preferred_rate)); + } + configs.into_iter().find_map(|config| { ffmpeg_sample_format_for(config.sample_format()) .map(|_| config.with_sample_rate(select_sample_rate(&config))) diff --git a/crates/recording/src/sources/audio_mixer.rs b/crates/recording/src/sources/audio_mixer.rs index 9bf143b675..5e451f4162 100644 --- a/crates/recording/src/sources/audio_mixer.rs +++ b/crates/recording/src/sources/audio_mixer.rs @@ -68,29 +68,46 @@ impl AudioMixerBuilder { pub fn build(self, output: mpsc::Sender) -> Result { let mut filter_graph = ffmpeg::filter::Graph::new(); - let mut abuffers = self - .sources - .iter() - .enumerate() - .map(|(i, source)| { - let info = &source.info; - let args = format!( - "time_base={}:sample_rate={}:sample_fmt={}:channel_layout=0x{:x}", - info.time_base, - info.rate(), - info.sample_format.name(), - info.channel_layout().bits() - ); - - debug!("audio mixer input {i}: {args}"); - - filter_graph.add( - &ffmpeg::filter::find("abuffer").expect("Failed to find abuffer filter"), - &format!("src{i}"), - &args, - ) - }) - .collect::, _>>()?; + let mut abuffers = Vec::new(); + let mut resamplers = Vec::new(); + + let target_info = AudioMixer::INFO; + let target_rate = target_info.rate(); + let target_sample_fmt = target_info.sample_format.name(); + let target_channel_layout_bits = target_info.channel_layout().bits(); + + for (i, source) in self.sources.iter().enumerate() { + let info = &source.info; + let args = format!( + "time_base={}:sample_rate={}:sample_fmt={}:channel_layout=0x{:x}", + info.time_base, + info.rate(), + info.sample_format.name(), + info.channel_layout().bits() + ); + + debug!("audio mixer input {i}: {args}"); + + let mut abuffer = filter_graph.add( + &ffmpeg::filter::find("abuffer").expect("Failed to find abuffer filter"), + &format!("src{i}"), + &args, + )?; + + let mut resample = filter_graph.add( + &ffmpeg::filter::find("aresample").expect("Failed to find aresample filter"), + &format!("resample{i}"), + &format!( + "out_sample_rate={}:out_sample_fmt={}:out_chlayout=0x{:x}", + target_rate, target_sample_fmt, target_channel_layout_bits + ), + )?; + + abuffer.link(0, &mut resample, 0); + + abuffers.push(abuffer); + resamplers.push(resample); + } let mut amix = filter_graph.add( &ffmpeg::filter::find("amix").expect("Failed to find amix filter"), @@ -101,12 +118,15 @@ impl AudioMixerBuilder { ), )?; - let aformat_args = "sample_fmts=flt:sample_rates=48000:channel_layouts=stereo"; + let aformat_args = format!( + "sample_fmts={}:sample_rates={}:channel_layouts=0x{:x}", + target_sample_fmt, target_rate, target_channel_layout_bits + ); let mut aformat = filter_graph.add( &ffmpeg::filter::find("aformat").expect("Failed to find aformat filter"), "aformat", - aformat_args, + &aformat_args, )?; let mut abuffersink = filter_graph.add( @@ -115,8 +135,8 @@ impl AudioMixerBuilder { "", )?; - for (i, abuffer) in abuffers.iter_mut().enumerate() { - abuffer.link(0, &mut amix, i as u32); + for (i, resample) in resamplers.iter_mut().enumerate() { + resample.link(0, &mut amix, i as u32); } amix.link(0, &mut aformat, 0); @@ -136,6 +156,7 @@ impl AudioMixerBuilder { samples_out: 0, last_tick: None, abuffers, + resamplers, abuffersink, output, _filter_graph: filter_graph, @@ -210,6 +231,7 @@ pub struct AudioMixer { last_tick: Option, // sample_timestamps: VecDeque<(usize, Timestamp)>, abuffers: Vec, + resamplers: Vec, abuffersink: ffmpeg::filter::Context, _filter_graph: ffmpeg::filter::Graph, _amix: ffmpeg::filter::Context,