feat: Add Mic Sensitivity Noise Gate slider and core audio gating
This commit is contained in:
@@ -39,6 +39,7 @@ pub enum AppMessage {
|
|||||||
PeerVolumeChanged(EndpointId, f32),
|
PeerVolumeChanged(EndpointId, f32),
|
||||||
InputDeviceSelected(AudioDevice),
|
InputDeviceSelected(AudioDevice),
|
||||||
OutputDeviceSelected(AudioDevice),
|
OutputDeviceSelected(AudioDevice),
|
||||||
|
NoiseGateChanged(f32),
|
||||||
EventOccurred(Event),
|
EventOccurred(Event),
|
||||||
NavigateToSettings,
|
NavigateToSettings,
|
||||||
NavigateBack,
|
NavigateBack,
|
||||||
@@ -89,6 +90,7 @@ impl Default for AppState {
|
|||||||
let _ = UI_RX.set(Mutex::new(Some(ui_rx)));
|
let _ = UI_RX.set(Mutex::new(Some(ui_rx)));
|
||||||
|
|
||||||
let config = AppConfig::load();
|
let config = AppConfig::load();
|
||||||
|
let _ = controller.send(CoreCommand::SetNoiseGateThreshold(config.noise_gate_threshold));
|
||||||
let all_devices = enumerate_audio_devices();
|
let all_devices = enumerate_audio_devices();
|
||||||
let input_devices: Vec<_> = all_devices.iter().filter(|d| d.is_input).cloned().collect();
|
let input_devices: Vec<_> = all_devices.iter().filter(|d| d.is_input).cloned().collect();
|
||||||
let output_devices: Vec<_> = all_devices.iter().filter(|d| !d.is_input).cloned().collect();
|
let output_devices: Vec<_> = all_devices.iter().filter(|d| !d.is_input).cloned().collect();
|
||||||
@@ -251,6 +253,11 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
|||||||
state.config.save();
|
state.config.save();
|
||||||
state.selected_output = Some(dev);
|
state.selected_output = Some(dev);
|
||||||
}
|
}
|
||||||
|
AppMessage::NoiseGateChanged(val) => {
|
||||||
|
state.config.noise_gate_threshold = val;
|
||||||
|
state.config.save();
|
||||||
|
let _ = state.controller.send(CoreCommand::SetNoiseGateThreshold(val));
|
||||||
|
}
|
||||||
AppMessage::EventOccurred(Event::Keyboard(keyboard::Event::KeyPressed { key, .. })) => {
|
AppMessage::EventOccurred(Event::Keyboard(keyboard::Event::KeyPressed { key, .. })) => {
|
||||||
if state.is_setting_hotkey {
|
if state.is_setting_hotkey {
|
||||||
state.ptt_hotkey = key.clone();
|
state.ptt_hotkey = key.clone();
|
||||||
@@ -384,6 +391,11 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
).width(iced::Length::Fixed(160.0))
|
).width(iced::Length::Fixed(160.0))
|
||||||
].spacing(4),
|
].spacing(4),
|
||||||
].spacing(10),
|
].spacing(10),
|
||||||
|
vertical_space(20.0),
|
||||||
|
column![
|
||||||
|
text(format!("Mic Sensitivity (Noise Gate): {:.1}%", state.config.noise_gate_threshold * 100.0)).size(14).color(color_subtext),
|
||||||
|
slider(0.0..=0.1, state.config.noise_gate_threshold, AppMessage::NoiseGateChanged)
|
||||||
|
].spacing(10).width(iced::Length::Fixed(320.0)),
|
||||||
vertical_space(30.0),
|
vertical_space(30.0),
|
||||||
button(
|
button(
|
||||||
text("Back")
|
text("Back")
|
||||||
|
|||||||
+12
-1
@@ -2,10 +2,21 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
pub input_device: String,
|
pub input_device: String,
|
||||||
pub output_device: String,
|
pub output_device: String,
|
||||||
|
pub noise_gate_threshold: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AppConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
input_device: "".to_string(),
|
||||||
|
output_device: "".to_string(),
|
||||||
|
noise_gate_threshold: 0.01,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppConfig {
|
impl AppConfig {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ pub enum CoreCommand {
|
|||||||
SetPttMode(bool),
|
SetPttMode(bool),
|
||||||
SetPttActive(bool),
|
SetPttActive(bool),
|
||||||
SetPeerVolume(EndpointId, f32),
|
SetPeerVolume(EndpointId, f32),
|
||||||
|
SetNoiseGateThreshold(f32),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ async fn run_core_loop(
|
|||||||
let is_deafened = Arc::new(AtomicBool::new(false));
|
let is_deafened = Arc::new(AtomicBool::new(false));
|
||||||
let ptt_mode = Arc::new(AtomicBool::new(false));
|
let ptt_mode = Arc::new(AtomicBool::new(false));
|
||||||
let ptt_active = Arc::new(AtomicBool::new(false));
|
let ptt_active = Arc::new(AtomicBool::new(false));
|
||||||
|
let noise_gate_threshold = Arc::new(std::sync::atomic::AtomicU32::new(0.01f32.to_bits()));
|
||||||
let peer_volumes = Arc::new(Mutex::new(HashMap::<EndpointId, f32>::new()));
|
let peer_volumes = Arc::new(Mutex::new(HashMap::<EndpointId, f32>::new()));
|
||||||
let mut current_name = "Anonymous".to_string();
|
let mut current_name = "Anonymous".to_string();
|
||||||
|
|
||||||
@@ -194,6 +195,7 @@ async fn run_core_loop(
|
|||||||
let is_muted_clone = is_muted.clone();
|
let is_muted_clone = is_muted.clone();
|
||||||
let ptt_mode_clone = ptt_mode.clone();
|
let ptt_mode_clone = ptt_mode.clone();
|
||||||
let ptt_active_clone = ptt_active.clone();
|
let ptt_active_clone = ptt_active.clone();
|
||||||
|
let noise_gate_threshold_clone = noise_gate_threshold.clone();
|
||||||
let transport_clone = transport.clone();
|
let transport_clone = transport.clone();
|
||||||
let room_state_clone = room_state.clone();
|
let room_state_clone = room_state.clone();
|
||||||
let tokio_handle = tokio::runtime::Handle::current();
|
let tokio_handle = tokio::runtime::Handle::current();
|
||||||
@@ -215,6 +217,21 @@ async fn run_core_loop(
|
|||||||
if ptt_mode_clone.load(Ordering::Relaxed) && !ptt_active_clone.load(Ordering::Relaxed) {
|
if ptt_mode_clone.load(Ordering::Relaxed) && !ptt_active_clone.load(Ordering::Relaxed) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let ng_bits = noise_gate_threshold_clone.load(Ordering::Relaxed);
|
||||||
|
let ng_thresh = f32::from_bits(ng_bits);
|
||||||
|
if ng_thresh > 0.0001 {
|
||||||
|
let mut sum_sq = 0.0f32;
|
||||||
|
for &sample in &pcm {
|
||||||
|
let normalized = (sample as f32) / 32768.0;
|
||||||
|
sum_sq += normalized * normalized;
|
||||||
|
}
|
||||||
|
let rms = (sum_sq / pcm.len() as f32).sqrt();
|
||||||
|
if rms < ng_thresh {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Ok(encoded) = encoder.encode(&pcm) {
|
if let Ok(encoded) = encoder.encode(&pcm) {
|
||||||
let bytes = bytes::Bytes::from(encoded);
|
let bytes = bytes::Bytes::from(encoded);
|
||||||
let active = room_state_clone.active_peers();
|
let active = room_state_clone.active_peers();
|
||||||
@@ -428,6 +445,10 @@ async fn run_core_loop(
|
|||||||
let mut guard = peer_volumes.lock().await;
|
let mut guard = peer_volumes.lock().await;
|
||||||
guard.insert(peer_id, vol);
|
guard.insert(peer_id, vol);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CoreCommand::SetNoiseGateThreshold(threshold) => {
|
||||||
|
noise_gate_threshold.store(threshold.to_bits(), Ordering::Relaxed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
with open("src/app/mod.rs", "r") as f:
|
||||||
|
text = f.read()
|
||||||
|
|
||||||
|
# 1. Add to AppMessage
|
||||||
|
text = text.replace(
|
||||||
|
" OutputDeviceSelected(AudioDevice),\n EventOccurred(Event),",
|
||||||
|
" OutputDeviceSelected(AudioDevice),\n NoiseGateChanged(f32),\n EventOccurred(Event),"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Add to AppState (already has config, so we can just read from config.noise_gate_threshold, but state needs it too if we don't want to use state.config everywhere)
|
||||||
|
# Let's just use state.config.noise_gate_threshold
|
||||||
|
|
||||||
|
# 3. Add to update() match
|
||||||
|
update_old = """ AppMessage::OutputDeviceSelected(dev) => {
|
||||||
|
state.config.output_device = dev.name.clone();
|
||||||
|
state.config.save();
|
||||||
|
state.selected_output = Some(dev);
|
||||||
|
}"""
|
||||||
|
update_new = """ AppMessage::OutputDeviceSelected(dev) => {
|
||||||
|
state.config.output_device = dev.name.clone();
|
||||||
|
state.config.save();
|
||||||
|
state.selected_output = Some(dev);
|
||||||
|
}
|
||||||
|
AppMessage::NoiseGateChanged(val) => {
|
||||||
|
state.config.noise_gate_threshold = val;
|
||||||
|
state.config.save();
|
||||||
|
let _ = state.controller.send(CoreCommand::SetNoiseGateThreshold(val));
|
||||||
|
}"""
|
||||||
|
text = text.replace(update_old, update_new)
|
||||||
|
|
||||||
|
# 4. We also need to send the initial noise gate value to the controller on Join/Create,
|
||||||
|
# but wait! CoreController receives it async. Maybe we send it right after controller is created?
|
||||||
|
# Or just in AppState::default() we can't send because it's async? No, controller.send() is async? Actually it's non-blocking channel send.
|
||||||
|
# Let's just add it to Join message or send it when RoomJoined occurs.
|
||||||
|
# Wait, let's just send it when creating the controller? No, `state.controller.send()` returns Result, it's non-blocking.
|
||||||
|
default_old = """ let controller = Arc::new(CoreController::new(ui_tx));
|
||||||
|
let _ = UI_RX.set(Mutex::new(Some(ui_rx)));
|
||||||
|
|
||||||
|
let config = AppConfig::load();"""
|
||||||
|
default_new = """ let controller = Arc::new(CoreController::new(ui_tx));
|
||||||
|
let _ = UI_RX.set(Mutex::new(Some(ui_rx)));
|
||||||
|
|
||||||
|
let config = AppConfig::load();
|
||||||
|
let _ = controller.send(CoreCommand::SetNoiseGateThreshold(config.noise_gate_threshold));"""
|
||||||
|
text = text.replace(default_old, default_new)
|
||||||
|
|
||||||
|
# 5. Add slider to view_settings
|
||||||
|
settings_old = """ ].spacing(10),
|
||||||
|
vertical_space(30.0),
|
||||||
|
button("""
|
||||||
|
|
||||||
|
settings_new = """ ].spacing(10),
|
||||||
|
vertical_space(20.0),
|
||||||
|
column![
|
||||||
|
text(&format!("Mic Sensitivity (Noise Gate): {:.1}%", state.config.noise_gate_threshold * 100.0)).size(14).color(color_subtext),
|
||||||
|
slider(0.0..=0.1, state.config.noise_gate_threshold, AppMessage::NoiseGateChanged)
|
||||||
|
].spacing(10).width(iced::Length::Fixed(320.0)),
|
||||||
|
vertical_space(30.0),
|
||||||
|
button("""
|
||||||
|
text = text.replace(settings_old, settings_new)
|
||||||
|
|
||||||
|
with open("src/app/mod.rs", "w") as f:
|
||||||
|
f.write(text)
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
with open("src/core/mod.rs", "r") as f:
|
||||||
|
text = f.read()
|
||||||
|
|
||||||
|
# 1. Add noise_gate_threshold to state
|
||||||
|
text = text.replace(
|
||||||
|
"let ptt_active = Arc::new(AtomicBool::new(false));",
|
||||||
|
"let ptt_active = Arc::new(AtomicBool::new(false));\n let noise_gate_threshold = Arc::new(std::sync::atomic::AtomicU32::new(0.01f32.to_bits()));"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Add noise_gate_threshold to capture thread closure
|
||||||
|
text = text.replace(
|
||||||
|
"let ptt_active_clone = ptt_active.clone();",
|
||||||
|
"let ptt_active_clone = ptt_active.clone();\n let noise_gate_threshold_clone = noise_gate_threshold.clone();"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. Add noise gate logic to capture loop
|
||||||
|
old_capture_loop = """ while let Ok(pcm) = capture_rx.recv() {
|
||||||
|
if is_muted_clone.load(Ordering::Relaxed) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ptt_mode_clone.load(Ordering::Relaxed) && !ptt_active_clone.load(Ordering::Relaxed) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Ok(encoded) = encoder.encode(&pcm) {"""
|
||||||
|
|
||||||
|
new_capture_loop = """ while let Ok(pcm) = capture_rx.recv() {
|
||||||
|
if is_muted_clone.load(Ordering::Relaxed) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ptt_mode_clone.load(Ordering::Relaxed) && !ptt_active_clone.load(Ordering::Relaxed) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ng_bits = noise_gate_threshold_clone.load(Ordering::Relaxed);
|
||||||
|
let ng_thresh = f32::from_bits(ng_bits);
|
||||||
|
if ng_thresh > 0.0001 {
|
||||||
|
let mut sum_sq = 0.0f32;
|
||||||
|
for &sample in &pcm {
|
||||||
|
let normalized = (sample as f32) / 32768.0;
|
||||||
|
sum_sq += normalized * normalized;
|
||||||
|
}
|
||||||
|
let rms = (sum_sq / pcm.len() as f32).sqrt();
|
||||||
|
if rms < ng_thresh {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(encoded) = encoder.encode(&pcm) {"""
|
||||||
|
text = text.replace(old_capture_loop, new_capture_loop)
|
||||||
|
|
||||||
|
# 4. Handle CoreCommand::SetNoiseGateThreshold
|
||||||
|
old_match_end = """ CoreCommand::SetPeerVolume(peer_id, vol) => {
|
||||||
|
let mut guard = peer_volumes.lock().await;
|
||||||
|
guard.insert(peer_id, vol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
new_match_end = """ CoreCommand::SetPeerVolume(peer_id, vol) => {
|
||||||
|
let mut guard = peer_volumes.lock().await;
|
||||||
|
guard.insert(peer_id, vol);
|
||||||
|
}
|
||||||
|
|
||||||
|
CoreCommand::SetNoiseGateThreshold(threshold) => {
|
||||||
|
noise_gate_threshold.store(threshold.to_bits(), Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
text = text.replace(old_match_end, new_match_end)
|
||||||
|
|
||||||
|
with open("src/core/mod.rs", "w") as f:
|
||||||
|
f.write(text)
|
||||||
Reference in New Issue
Block a user