feat: add advanced Sonora voice controls

This commit is contained in:
ReTeamSpeak
2026-05-13 17:35:40 +09:00
parent a852374bf9
commit 272d3a1cb7
4 changed files with 173 additions and 6 deletions
+38 -1
View File
@@ -74,6 +74,7 @@ struct App {
vad: Arc<Mutex<audio::VoiceActivation>>, vad: Arc<Mutex<audio::VoiceActivation>>,
noise_reducer: Arc<Mutex<noise_cancel::NoiseReducer>>, noise_reducer: Arc<Mutex<noise_cancel::NoiseReducer>>,
nc_method: noise_cancel::NoiseCancelMethod, nc_method: noise_cancel::NoiseCancelMethod,
sonora_settings: noise_cancel::SonoraSettings,
imported_identity: Option<Identity>, imported_identity: Option<Identity>,
identity_string_input: String, identity_string_input: String,
identity_file_input: String, identity_file_input: String,
@@ -92,6 +93,7 @@ impl App {
let appearance_mode = settings.appearance_mode; let appearance_mode = settings.appearance_mode;
let talk_mode = settings.talk_mode; let talk_mode = settings.talk_mode;
let nc_method = settings.noise_cancel_method; let nc_method = settings.noise_cancel_method;
let sonora_settings = settings.sonora_settings;
let selected_output_device = settings.selected_output_device; let selected_output_device = settings.selected_output_device;
let selected_input_device = settings.selected_input_device; let selected_input_device = settings.selected_input_device;
@@ -130,8 +132,12 @@ impl App {
continuous_active: false, continuous_active: false,
talk_mode, talk_mode,
vad: Arc::new(Mutex::new(audio::VoiceActivation::new(0.005))), 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, nc_method,
sonora_settings,
imported_identity: None, imported_identity: None,
identity_string_input: String::new(), identity_string_input: String::new(),
identity_file_input: String::new(), identity_file_input: String::new(),
@@ -160,6 +166,7 @@ impl App {
selected_input_device: self.selected_input_device.clone(), selected_input_device: self.selected_input_device.clone(),
talk_mode: self.talk_mode, talk_mode: self.talk_mode,
noise_cancel_method: self.nc_method, noise_cancel_method: self.nc_method,
sonora_settings: self.sonora_settings,
}); });
} }
@@ -594,6 +601,36 @@ impl App {
self.save_settings(); self.save_settings();
Task::none() 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 => { Message::ToggleMicMute => {
self.mic_muted = !self.mic_muted; self.mic_muted = !self.mic_muted;
let h = self.handle.clone(); let h = self.handle.clone();
+71 -5
View File
@@ -1,5 +1,8 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sonora::config::{NoiseSuppression, NoiseSuppressionLevel}; use sonora::config::{
AdaptiveDigital, GainController2, HighPassFilter, NoiseSuppression,
NoiseSuppressionLevel,
};
use sonora::{AudioProcessing, Config, StreamConfig}; use sonora::{AudioProcessing, Config, StreamConfig};
const SONORA_FRAME_SIZE: usize = 480; 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 { pub struct NoiseReducer {
method: NoiseCancelMethod, method: NoiseCancelMethod,
sonora_settings: SonoraSettings,
nnnoiseless: Option<Box<nnnoiseless::DenoiseState<'static>>>, nnnoiseless: Option<Box<nnnoiseless::DenoiseState<'static>>>,
sonora: Option<SonoraNoiseReducer>, sonora: Option<SonoraNoiseReducer>,
residual_buf: Vec<f32>, residual_buf: Vec<f32>,
@@ -39,13 +92,18 @@ struct SonoraNoiseReducer {
} }
impl SonoraNoiseReducer { impl SonoraNoiseReducer {
fn new() -> Self { fn new(settings: SonoraSettings) -> Self {
let stream = StreamConfig::new(48_000, 1); let stream = StreamConfig::new(48_000, 1);
let config = Config { let config = Config {
high_pass_filter: settings.high_pass_enabled.then(HighPassFilter::default),
noise_suppression: Some(NoiseSuppression { noise_suppression: Some(NoiseSuppression {
level: NoiseSuppressionLevel::Moderate, level: settings.noise_suppression_level.to_sonora(),
..NoiseSuppression::default() ..NoiseSuppression::default()
}), }),
gain_controller2: settings.agc_enabled.then(|| GainController2 {
adaptive_digital: Some(AdaptiveDigital::default()),
..GainController2::default()
}),
..Config::default() ..Config::default()
}; };
@@ -73,9 +131,10 @@ impl SonoraNoiseReducer {
} }
impl NoiseReducer { impl NoiseReducer {
pub fn new(method: NoiseCancelMethod) -> Self { pub fn new(method: NoiseCancelMethod, sonora_settings: SonoraSettings) -> Self {
let mut this = Self { let mut this = Self {
method, method,
sonora_settings,
nnnoiseless: None, nnnoiseless: None,
sonora: None, sonora: None,
residual_buf: Vec::new(), residual_buf: Vec::new(),
@@ -90,7 +149,7 @@ impl NoiseReducer {
_ => None, _ => None,
}; };
self.sonora = match self.method { self.sonora = match self.method {
NoiseCancelMethod::Sonora => Some(SonoraNoiseReducer::new()), NoiseCancelMethod::Sonora => Some(SonoraNoiseReducer::new(self.sonora_settings)),
_ => None, _ => None,
}; };
self.residual_buf.clear(); self.residual_buf.clear();
@@ -101,6 +160,13 @@ impl NoiseReducer {
self.init_method(); 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]) { pub fn process(&mut self, _samples: &mut [f32]) {
match self.method { match self.method {
NoiseCancelMethod::None => {} NoiseCancelMethod::None => {}
+5
View File
@@ -41,6 +41,9 @@ pub(crate) enum Message {
PttReleased, PttReleased,
ToggleTalkMode, ToggleTalkMode,
SetNoiseCancelMethod(noise_cancel::NoiseCancelMethod), SetNoiseCancelMethod(noise_cancel::NoiseCancelMethod),
SetSonoraNoiseSuppressionLevel(noise_cancel::NoiseSuppressionLevelSetting),
ToggleSonoraAgc,
ToggleSonoraHighPass,
StartContinuous, StartContinuous,
StopContinuous, StopContinuous,
MicSamples(Vec<f32>), MicSamples(Vec<f32>),
@@ -135,6 +138,7 @@ pub(crate) struct AppSettings {
pub(crate) selected_input_device: String, pub(crate) selected_input_device: String,
pub(crate) talk_mode: audio::TalkMode, pub(crate) talk_mode: audio::TalkMode,
pub(crate) noise_cancel_method: noise_cancel::NoiseCancelMethod, pub(crate) noise_cancel_method: noise_cancel::NoiseCancelMethod,
pub(crate) sonora_settings: noise_cancel::SonoraSettings,
} }
impl Default for AppSettings { impl Default for AppSettings {
@@ -145,6 +149,7 @@ impl Default for AppSettings {
selected_input_device: String::new(), selected_input_device: String::new(),
talk_mode: audio::TalkMode::PushToTalk, talk_mode: audio::TalkMode::PushToTalk,
noise_cancel_method: noise_cancel::NoiseCancelMethod::None, noise_cancel_method: noise_cancel::NoiseCancelMethod::None,
sonora_settings: noise_cancel::SonoraSettings::default(),
} }
} }
} }
+59
View File
@@ -649,6 +649,7 @@ impl App {
}; };
let nc_label = self.nc_method.label(); let nc_label = self.nc_method.label();
let sonora_noise_label = self.sonora_settings.noise_suppression_level.label();
let voice_col = column![ let voice_col = column![
text("Voice Activation Mode").size(13), 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( settings_col = settings_col.push(
container(voice_col) container(voice_col)
.style(theme::card_container) .style(theme::card_container)