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>>,
noise_reducer: Arc<Mutex<noise_cancel::NoiseReducer>>,
nc_method: noise_cancel::NoiseCancelMethod,
sonora_settings: noise_cancel::SonoraSettings,
imported_identity: Option<Identity>,
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();
+71 -5
View File
@@ -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<Box<nnnoiseless::DenoiseState<'static>>>,
sonora: Option<SonoraNoiseReducer>,
residual_buf: Vec<f32>,
@@ -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 => {}
+5
View File
@@ -41,6 +41,9 @@ pub(crate) enum Message {
PttReleased,
ToggleTalkMode,
SetNoiseCancelMethod(noise_cancel::NoiseCancelMethod),
SetSonoraNoiseSuppressionLevel(noise_cancel::NoiseSuppressionLevelSetting),
ToggleSonoraAgc,
ToggleSonoraHighPass,
StartContinuous,
StopContinuous,
MicSamples(Vec<f32>),
@@ -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(),
}
}
}
+59
View File
@@ -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)