From 8098161f4266a3730f35ec566e3298558aa6a60e Mon Sep 17 00:00:00 2001 From: Stanislaw Kem Date: Wed, 18 Feb 2026 13:39:51 +0100 Subject: [PATCH 1/2] Harden runtime paths and silence non-actionable warnings --- src/capture.rs | 4 +-- src/config.rs | 2 +- src/modular_pipeline.rs | 2 ++ src/processors/aec.rs | 14 +++++++--- src/processors/noise_suppression.rs | 16 +++++++---- src/processors/resample.rs | 43 ++++++++++++++++++++++++----- src/stats.rs | 1 + 7 files changed, 63 insertions(+), 19 deletions(-) diff --git a/src/capture.rs b/src/capture.rs index f025c9a..ecaedf4 100644 --- a/src/capture.rs +++ b/src/capture.rs @@ -31,8 +31,8 @@ impl SCStreamOutputTrait for AudioOutputHandler { if num_buffers == 0 { return; } - // Get first buffer to check channels - let first_buffer = audio_data.get(0).unwrap(); + // Get first buffer to check channels; skip malformed empty payloads. + let Some(first_buffer) = audio_data.get(0) else { return; }; let channels_per_buffer = first_buffer.number_channels as usize; if num_buffers == 1 { diff --git a/src/config.rs b/src/config.rs index 24adda1..9e82eff 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,6 @@ use pyo3::prelude::*; -#[pyclass] +#[pyclass(skip_from_py_object)] #[derive(Clone, Debug)] pub struct AudioProcessingConfig { #[pyo3(get, set)] diff --git a/src/modular_pipeline.rs b/src/modular_pipeline.rs index 03f1f5c..d277dfe 100644 --- a/src/modular_pipeline.rs +++ b/src/modular_pipeline.rs @@ -10,6 +10,7 @@ use numpy::ToPyArray; use std::time::Instant; /// Modular pipeline that processes audio through a chain of processors +#[allow(dead_code)] pub struct ModularPipeline { rx: Receiver, stop_rx: Receiver<()>, @@ -20,6 +21,7 @@ pub struct ModularPipeline { stats: RuntimeStatsHandle, } +#[allow(dead_code)] impl ModularPipeline { fn select_total_pipeline_delay(input_timestamp: u64, output_timestamp: u64, processing_delay: u64) -> u64 { let timestamp_delay = output_timestamp.saturating_sub(input_timestamp); diff --git a/src/processors/aec.rs b/src/processors/aec.rs index 044c7b7..ce95daa 100644 --- a/src/processors/aec.rs +++ b/src/processors/aec.rs @@ -39,7 +39,7 @@ impl AecProcessor { pub fn new(config: AudioProcessingConfig, stats: RuntimeStatsHandle) -> Self { let apm = if config.enable_aec { - Some(Self::create_apm(&config)) + Self::create_apm(&config) } else { None }; @@ -265,11 +265,17 @@ impl AecProcessor { apm_config } - fn create_apm(config: &AudioProcessingConfig) -> Processor { - let apm = Processor::new(48_000).expect("Failed to create WebRTC Processor for AEC"); + fn create_apm(config: &AudioProcessingConfig) -> Option { + let apm = match Processor::new(48_000) { + Ok(apm) => apm, + Err(err) => { + eprintln!("Warning: failed to create AEC processor: {}", err); + return None; + } + }; let delay_ms = config.aec_stream_delay_ms.max(0); apm.set_config(Self::build_apm_config(delay_ms)); - apm + Some(apm) } } diff --git a/src/processors/noise_suppression.rs b/src/processors/noise_suppression.rs index f8d270f..3e8f1e1 100644 --- a/src/processors/noise_suppression.rs +++ b/src/processors/noise_suppression.rs @@ -14,7 +14,7 @@ pub struct NoiseSuppressionProcessor { impl NoiseSuppressionProcessor { pub fn new(config: AudioProcessingConfig) -> Self { let apm = if config.enable_ns { - Some(Self::create_ns_apm(&config)) + Self::create_ns_apm(&config) } else { None }; @@ -25,8 +25,14 @@ impl NoiseSuppressionProcessor { } } - fn create_ns_apm(_config: &AudioProcessingConfig) -> Processor { - let apm = Processor::new(48_000).expect("Failed to create WebRTC Processor for Noise Suppression"); + fn create_ns_apm(_config: &AudioProcessingConfig) -> Option { + let apm = match Processor::new(48_000) { + Ok(apm) => apm, + Err(err) => { + eprintln!("Warning: failed to create NS processor: {}", err); + return None; + } + }; let mut apm_config = Config::default(); @@ -44,7 +50,7 @@ impl NoiseSuppressionProcessor { apm_config.gain_controller = None; apm.set_config(apm_config); - apm + Some(apm) } } @@ -88,7 +94,7 @@ impl AudioProcessor for NoiseSuppressionProcessor { fn reset(&mut self) { // Reset NS processor state if needed if self.config.enable_ns { - self.apm = Some(Self::create_ns_apm(&self.config)); + self.apm = Self::create_ns_apm(&self.config); } else { self.apm = None; } diff --git a/src/processors/resample.rs b/src/processors/resample.rs index 1de3f60..1ed4d98 100644 --- a/src/processors/resample.rs +++ b/src/processors/resample.rs @@ -26,6 +26,7 @@ impl<'a> Adapter<'a, f32> for PlanarBuffer<'a> { struct StreamState { resampler: Option>, + source_rate: u32, input_buffer: VecDeque, output_queue: VecDeque, current_timestamp: u64, @@ -36,20 +37,30 @@ struct StreamState { impl StreamState { fn new(source_rate: u32, target_rate: u32, target_channels: u16, chunk_size: usize) -> Self { let resampler = if source_rate != target_rate { - Some(Fft::::new( + match Fft::::new( source_rate as usize, target_rate as usize, chunk_size, 1, target_channels as usize, FixedSync::Input, - ).expect("Failed to create resampler")) + ) { + Ok(resampler) => Some(resampler), + Err(err) => { + eprintln!( + "Warning: failed to create resampler {}->{}Hz: {}. Falling back to passthrough.", + source_rate, target_rate, err + ); + None + } + } } else { None }; Self { resampler, + source_rate, input_buffer: VecDeque::with_capacity(chunk_size * 4), output_queue: VecDeque::with_capacity(chunk_size * 4), current_timestamp: 0, @@ -158,12 +169,18 @@ impl ResampleProcessor { if channels == 1 { for _ in 0..chunk_size { - planar_data[0].push(state.input_buffer.pop_front().unwrap()); + let Some(sample) = state.input_buffer.pop_front() else { + return results; + }; + planar_data[0].push(sample); } } else { for _ in 0..chunk_size { for channel_buf in &mut planar_data { - channel_buf.push(state.input_buffer.pop_front().unwrap()); + let Some(sample) = state.input_buffer.pop_front() else { + return results; + }; + channel_buf.push(sample); } } } @@ -178,12 +195,20 @@ impl ResampleProcessor { let mut samples = Vec::new(); if channels == 1 { for i in 0..output.frames() { - samples.push(output.read_sample(0, i).unwrap()); + if let Some(sample) = output.read_sample(0, i) { + samples.push(sample); + } else { + return results; + } } } else { for i in 0..output.frames() { for ch in 0..channels { - samples.push(output.read_sample(ch, i).unwrap()); + if let Some(sample) = output.read_sample(ch, i) { + samples.push(sample); + } else { + return results; + } } } } @@ -214,7 +239,11 @@ impl ResampleProcessor { results.push(AudioFrame { source, samples, - sample_rate: target_rate, + sample_rate: if state.source_rate == target_rate { + target_rate + } else { + state.source_rate + }, channels: target_channels, timestamp: frame_ts, }); diff --git a/src/stats.rs b/src/stats.rs index f59827d..f3e0057 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -103,6 +103,7 @@ impl RuntimeStatsHandle { } } + #[allow(dead_code)] pub fn reset(&self) { if let Ok(mut stats) = self.inner.lock() { *stats = RuntimeStats::default(); From a71949312064fbd52950f32b787056a694463a66 Mon Sep 17 00:00:00 2001 From: Stanislaw Kem Date: Wed, 18 Feb 2026 23:06:57 +0100 Subject: [PATCH 2/2] Fix PyO3 config extraction with from_py_object --- src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.rs b/src/config.rs index 9e82eff..1db0b9a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,6 @@ use pyo3::prelude::*; -#[pyclass(skip_from_py_object)] +#[pyclass(from_py_object)] #[derive(Clone, Debug)] pub struct AudioProcessingConfig { #[pyo3(get, set)]