From 272d3a1cb7a3b207e75e070055fe0d2dae01c2af Mon Sep 17 00:00:00 2001 From: ReTeamSpeak Date: Wed, 13 May 2026 17:35:40 +0900 Subject: [PATCH] feat: add advanced Sonora voice controls --- src/iced-app/src/main.rs | 39 +++++++++++++++- src/iced-app/src/noise_cancel.rs | 76 +++++++++++++++++++++++++++++--- src/iced-app/src/types.rs | 5 +++ src/iced-app/src/view.rs | 59 +++++++++++++++++++++++++ 4 files changed, 173 insertions(+), 6 deletions(-) diff --git a/src/iced-app/src/main.rs b/src/iced-app/src/main.rs index 8079cec..4360d8c 100644 --- a/src/iced-app/src/main.rs +++ b/src/iced-app/src/main.rs @@ -74,6 +74,7 @@ struct App { vad: Arc>, noise_reducer: Arc>, nc_method: noise_cancel::NoiseCancelMethod, + sonora_settings: noise_cancel::SonoraSettings, imported_identity: Option, identity_string_input: String, identity_file_input: String, @@ -92,6 +93,7 @@ impl App { let appearance_mode = settings.appearance_mode; let talk_mode = settings.talk_mode; let nc_method = settings.noise_cancel_method; + let sonora_settings = settings.sonora_settings; let selected_output_device = settings.selected_output_device; let selected_input_device = settings.selected_input_device; @@ -130,8 +132,12 @@ impl App { continuous_active: false, talk_mode, vad: Arc::new(Mutex::new(audio::VoiceActivation::new(0.005))), - noise_reducer: Arc::new(Mutex::new(noise_cancel::NoiseReducer::new(nc_method))), + noise_reducer: Arc::new(Mutex::new(noise_cancel::NoiseReducer::new( + nc_method, + sonora_settings, + ))), nc_method, + sonora_settings, imported_identity: None, identity_string_input: String::new(), identity_file_input: String::new(), @@ -160,6 +166,7 @@ impl App { selected_input_device: self.selected_input_device.clone(), talk_mode: self.talk_mode, noise_cancel_method: self.nc_method, + sonora_settings: self.sonora_settings, }); } @@ -594,6 +601,36 @@ impl App { self.save_settings(); Task::none() } + Message::SetSonoraNoiseSuppressionLevel(level) => { + self.sonora_settings.noise_suppression_level = level; + let settings = self.sonora_settings; + let nc = self.noise_reducer.clone(); + tokio::spawn(async move { + nc.lock().await.set_sonora_settings(settings); + }); + self.save_settings(); + Task::none() + } + Message::ToggleSonoraAgc => { + self.sonora_settings.agc_enabled = !self.sonora_settings.agc_enabled; + let settings = self.sonora_settings; + let nc = self.noise_reducer.clone(); + tokio::spawn(async move { + nc.lock().await.set_sonora_settings(settings); + }); + self.save_settings(); + Task::none() + } + Message::ToggleSonoraHighPass => { + self.sonora_settings.high_pass_enabled = !self.sonora_settings.high_pass_enabled; + let settings = self.sonora_settings; + let nc = self.noise_reducer.clone(); + tokio::spawn(async move { + nc.lock().await.set_sonora_settings(settings); + }); + self.save_settings(); + Task::none() + } Message::ToggleMicMute => { self.mic_muted = !self.mic_muted; let h = self.handle.clone(); diff --git a/src/iced-app/src/noise_cancel.rs b/src/iced-app/src/noise_cancel.rs index e9e48d5..70bd7d5 100644 --- a/src/iced-app/src/noise_cancel.rs +++ b/src/iced-app/src/noise_cancel.rs @@ -1,5 +1,8 @@ use serde::{Deserialize, Serialize}; -use sonora::config::{NoiseSuppression, NoiseSuppressionLevel}; +use sonora::config::{ + AdaptiveDigital, GainController2, HighPassFilter, NoiseSuppression, + NoiseSuppressionLevel, +}; use sonora::{AudioProcessing, Config, StreamConfig}; const SONORA_FRAME_SIZE: usize = 480; @@ -25,8 +28,58 @@ impl NoiseCancelMethod { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct SonoraSettings { + pub noise_suppression_level: NoiseSuppressionLevelSetting, + pub agc_enabled: bool, + pub high_pass_enabled: bool, +} + +impl Default for SonoraSettings { + fn default() -> Self { + Self { + noise_suppression_level: NoiseSuppressionLevelSetting::Moderate, + agc_enabled: true, + high_pass_enabled: true, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum NoiseSuppressionLevelSetting { + Low, + Moderate, + High, + VeryHigh, +} + +impl NoiseSuppressionLevelSetting { + pub fn label(self) -> &'static str { + match self { + Self::Low => "Low", + Self::Moderate => "Moderate", + Self::High => "High", + Self::VeryHigh => "Very High", + } + } + + pub fn all() -> &'static [NoiseSuppressionLevelSetting] { + &[Self::Low, Self::Moderate, Self::High, Self::VeryHigh] + } + + fn to_sonora(self) -> NoiseSuppressionLevel { + match self { + Self::Low => NoiseSuppressionLevel::Low, + Self::Moderate => NoiseSuppressionLevel::Moderate, + Self::High => NoiseSuppressionLevel::High, + Self::VeryHigh => NoiseSuppressionLevel::VeryHigh, + } + } +} + pub struct NoiseReducer { method: NoiseCancelMethod, + sonora_settings: SonoraSettings, nnnoiseless: Option>>, sonora: Option, residual_buf: Vec, @@ -39,13 +92,18 @@ struct SonoraNoiseReducer { } impl SonoraNoiseReducer { - fn new() -> Self { + fn new(settings: SonoraSettings) -> Self { let stream = StreamConfig::new(48_000, 1); let config = Config { + high_pass_filter: settings.high_pass_enabled.then(HighPassFilter::default), noise_suppression: Some(NoiseSuppression { - level: NoiseSuppressionLevel::Moderate, + level: settings.noise_suppression_level.to_sonora(), ..NoiseSuppression::default() }), + gain_controller2: settings.agc_enabled.then(|| GainController2 { + adaptive_digital: Some(AdaptiveDigital::default()), + ..GainController2::default() + }), ..Config::default() }; @@ -73,9 +131,10 @@ impl SonoraNoiseReducer { } impl NoiseReducer { - pub fn new(method: NoiseCancelMethod) -> Self { + pub fn new(method: NoiseCancelMethod, sonora_settings: SonoraSettings) -> Self { let mut this = Self { method, + sonora_settings, nnnoiseless: None, sonora: None, residual_buf: Vec::new(), @@ -90,7 +149,7 @@ impl NoiseReducer { _ => None, }; self.sonora = match self.method { - NoiseCancelMethod::Sonora => Some(SonoraNoiseReducer::new()), + NoiseCancelMethod::Sonora => Some(SonoraNoiseReducer::new(self.sonora_settings)), _ => None, }; self.residual_buf.clear(); @@ -101,6 +160,13 @@ impl NoiseReducer { self.init_method(); } + pub fn set_sonora_settings(&mut self, settings: SonoraSettings) { + self.sonora_settings = settings; + if self.method == NoiseCancelMethod::Sonora { + self.init_method(); + } + } + pub fn process(&mut self, _samples: &mut [f32]) { match self.method { NoiseCancelMethod::None => {} diff --git a/src/iced-app/src/types.rs b/src/iced-app/src/types.rs index 41035a3..b83bec3 100644 --- a/src/iced-app/src/types.rs +++ b/src/iced-app/src/types.rs @@ -41,6 +41,9 @@ pub(crate) enum Message { PttReleased, ToggleTalkMode, SetNoiseCancelMethod(noise_cancel::NoiseCancelMethod), + SetSonoraNoiseSuppressionLevel(noise_cancel::NoiseSuppressionLevelSetting), + ToggleSonoraAgc, + ToggleSonoraHighPass, StartContinuous, StopContinuous, MicSamples(Vec), @@ -135,6 +138,7 @@ pub(crate) struct AppSettings { pub(crate) selected_input_device: String, pub(crate) talk_mode: audio::TalkMode, pub(crate) noise_cancel_method: noise_cancel::NoiseCancelMethod, + pub(crate) sonora_settings: noise_cancel::SonoraSettings, } impl Default for AppSettings { @@ -145,6 +149,7 @@ impl Default for AppSettings { selected_input_device: String::new(), talk_mode: audio::TalkMode::PushToTalk, noise_cancel_method: noise_cancel::NoiseCancelMethod::None, + sonora_settings: noise_cancel::SonoraSettings::default(), } } } diff --git a/src/iced-app/src/view.rs b/src/iced-app/src/view.rs index baa6755..1be74bc 100644 --- a/src/iced-app/src/view.rs +++ b/src/iced-app/src/view.rs @@ -649,6 +649,7 @@ impl App { }; let nc_label = self.nc_method.label(); + let sonora_noise_label = self.sonora_settings.noise_suppression_level.label(); let voice_col = column![ text("Voice Activation Mode").size(13), @@ -687,6 +688,64 @@ impl App { }, ); + let voice_col = if self.nc_method == noise_cancel::NoiseCancelMethod::Sonora { + let voice_col = voice_col + .push(vertical_space().height(8)) + .push(text("Sonora Noise Suppression").size(11).style(text::secondary)) + .push(text(sonora_noise_label).size(12).style(text::secondary)); + + let voice_col = noise_cancel::NoiseSuppressionLevelSetting::all() + .iter() + .copied() + .fold(voice_col, |column, level| { + column.push( + button(text(level.label()).size(11)) + .padding([4, 12]) + .style(if self.sonora_settings.noise_suppression_level == level { + theme::primary_button + } else { + theme::secondary_button + }) + .on_press(Message::SetSonoraNoiseSuppressionLevel(level)), + ) + }); + + voice_col + .push(vertical_space().height(8)) + .push( + button(text(if self.sonora_settings.agc_enabled { + "AGC2: On" + } else { + "AGC2: Off" + }) + .size(11)) + .padding([4, 12]) + .style(if self.sonora_settings.agc_enabled { + theme::primary_button + } else { + theme::secondary_button + }) + .on_press(Message::ToggleSonoraAgc), + ) + .push( + button(text(if self.sonora_settings.high_pass_enabled { + "High-Pass: On" + } else { + "High-Pass: Off" + }) + .size(11)) + .padding([4, 12]) + .style(if self.sonora_settings.high_pass_enabled { + theme::primary_button + } else { + theme::secondary_button + }) + .on_press(Message::ToggleSonoraHighPass), + ) + } else { + voice_col + }; + settings_col = settings_col.push( container(voice_col) .style(theme::card_container)