Compare commits
2
Commits
v0.2.0
...
cbba4b644e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbba4b644e | ||
|
|
c5375e200a |
+115
-5
@@ -208,6 +208,7 @@ pub enum AppMessage {
|
|||||||
ClearHotkey(HotkeyAction),
|
ClearHotkey(HotkeyAction),
|
||||||
PeerVolumeChanged(EndpointId, f32),
|
PeerVolumeChanged(EndpointId, f32),
|
||||||
PeerPanChanged(EndpointId, f32),
|
PeerPanChanged(EndpointId, f32),
|
||||||
|
PeerGateChanged(EndpointId, f32),
|
||||||
PeerEqChanged(EndpointId, EqBand, f32),
|
PeerEqChanged(EndpointId, EqBand, f32),
|
||||||
/// Toggle local mute of a peer (silence them just for us).
|
/// Toggle local mute of a peer (silence them just for us).
|
||||||
TogglePeerMute(EndpointId),
|
TogglePeerMute(EndpointId),
|
||||||
@@ -349,7 +350,6 @@ pub struct AppState {
|
|||||||
/// refreshed when the background is changed/removed. `None` = no custom bg.
|
/// refreshed when the background is changed/removed. `None` = no custom bg.
|
||||||
background_image: Option<bytes::Bytes>,
|
background_image: Option<bytes::Bytes>,
|
||||||
peers: HashMap<EndpointId, PeerState>,
|
peers: HashMap<EndpointId, PeerState>,
|
||||||
peer_volumes: HashMap<EndpointId, f32>,
|
|
||||||
audio_levels: HashMap<EndpointId, f32>,
|
audio_levels: HashMap<EndpointId, f32>,
|
||||||
/// Peers we've locally muted (their audio isn't mixed into our output).
|
/// Peers we've locally muted (their audio isn't mixed into our output).
|
||||||
locally_muted: HashSet<EndpointId>,
|
locally_muted: HashSet<EndpointId>,
|
||||||
@@ -474,6 +474,16 @@ impl Default for AppState {
|
|||||||
let _ = controller.send(CoreCommand::SetPeerPan(id, *pan));
|
let _ = controller.send(CoreCommand::SetPeerPan(id, *pan));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (peer, volume) in &config.peer_volume {
|
||||||
|
if let Ok(id) = peer.parse::<EndpointId>() {
|
||||||
|
let _ = controller.send(CoreCommand::SetPeerVolume(id, *volume));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (peer, threshold) in &config.peer_gate {
|
||||||
|
if let Ok(id) = peer.parse::<EndpointId>() {
|
||||||
|
let _ = controller.send(CoreCommand::SetPeerGate(id, *threshold));
|
||||||
|
}
|
||||||
|
}
|
||||||
let pixelpass_available =
|
let pixelpass_available =
|
||||||
crate::screenshare::is_available(config.pixelpass_path.as_deref());
|
crate::screenshare::is_available(config.pixelpass_path.as_deref());
|
||||||
let all_devices = enumerate_audio_devices();
|
let all_devices = enumerate_audio_devices();
|
||||||
@@ -505,7 +515,6 @@ impl Default for AppState {
|
|||||||
config,
|
config,
|
||||||
background_image,
|
background_image,
|
||||||
peers: HashMap::new(),
|
peers: HashMap::new(),
|
||||||
peer_volumes: HashMap::new(),
|
|
||||||
audio_levels: HashMap::new(),
|
audio_levels: HashMap::new(),
|
||||||
locally_muted: HashSet::new(),
|
locally_muted: HashSet::new(),
|
||||||
call_started: None,
|
call_started: None,
|
||||||
@@ -744,6 +753,34 @@ fn set_peer_pan_config(config: &mut AppConfig, id: EndpointId, pan: f32) -> f32
|
|||||||
pan
|
pan
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Store the per-peer listener volume, clamped to the slider range. Unity gain
|
||||||
|
/// (`1.0`) is the implicit default, so an at-unity entry is removed rather than
|
||||||
|
/// stored to keep the config tidy. Returns the clamped value.
|
||||||
|
fn set_peer_volume_config(config: &mut AppConfig, id: EndpointId, volume: f32) -> f32 {
|
||||||
|
let volume = volume.clamp(0.0, 2.0);
|
||||||
|
let key = id.to_string();
|
||||||
|
if (volume - 1.0).abs() <= 0.001 {
|
||||||
|
config.peer_volume.remove(&key);
|
||||||
|
} else {
|
||||||
|
config.peer_volume.insert(key, volume);
|
||||||
|
}
|
||||||
|
volume
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store the per-peer listener noise-gate threshold, clamped to the slider
|
||||||
|
/// range. `0.0` means the gate is off, so an at-zero entry is removed rather
|
||||||
|
/// than stored. Returns the clamped value.
|
||||||
|
fn set_peer_gate_config(config: &mut AppConfig, id: EndpointId, threshold: f32) -> f32 {
|
||||||
|
let threshold = threshold.clamp(0.0, METER_MAX);
|
||||||
|
let key = id.to_string();
|
||||||
|
if threshold <= 0.0 {
|
||||||
|
config.peer_gate.remove(&key);
|
||||||
|
} else {
|
||||||
|
config.peer_gate.insert(key, threshold);
|
||||||
|
}
|
||||||
|
threshold
|
||||||
|
}
|
||||||
|
|
||||||
fn peer_eq_settings(config: &AppConfig, id: &EndpointId) -> EqSettings {
|
fn peer_eq_settings(config: &AppConfig, id: &EndpointId) -> EqSettings {
|
||||||
config
|
config
|
||||||
.peer_eq
|
.peer_eq
|
||||||
@@ -1038,13 +1075,17 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
|||||||
state.config.save();
|
state.config.save();
|
||||||
}
|
}
|
||||||
AppMessage::PeerVolumeChanged(id, vol) => {
|
AppMessage::PeerVolumeChanged(id, vol) => {
|
||||||
state.peer_volumes.insert(id, vol);
|
let vol = set_peer_volume_config(&mut state.config, id, vol);
|
||||||
let _ = state.controller.send(CoreCommand::SetPeerVolume(id, vol));
|
let _ = state.controller.send(CoreCommand::SetPeerVolume(id, vol));
|
||||||
}
|
}
|
||||||
AppMessage::PeerPanChanged(id, pan) => {
|
AppMessage::PeerPanChanged(id, pan) => {
|
||||||
let pan = set_peer_pan_config(&mut state.config, id, pan);
|
let pan = set_peer_pan_config(&mut state.config, id, pan);
|
||||||
let _ = state.controller.send(CoreCommand::SetPeerPan(id, pan));
|
let _ = state.controller.send(CoreCommand::SetPeerPan(id, pan));
|
||||||
}
|
}
|
||||||
|
AppMessage::PeerGateChanged(id, threshold) => {
|
||||||
|
let threshold = set_peer_gate_config(&mut state.config, id, threshold);
|
||||||
|
let _ = state.controller.send(CoreCommand::SetPeerGate(id, threshold));
|
||||||
|
}
|
||||||
AppMessage::PeerEqChanged(id, band, gain_db) => {
|
AppMessage::PeerEqChanged(id, band, gain_db) => {
|
||||||
let settings = set_peer_eq_config(&mut state.config, id, band, gain_db);
|
let settings = set_peer_eq_config(&mut state.config, id, band, gain_db);
|
||||||
let _ = state.controller.send(CoreCommand::SetPeerEq(id, settings));
|
let _ = state.controller.send(CoreCommand::SetPeerEq(id, settings));
|
||||||
@@ -3348,11 +3389,18 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
].spacing(8);
|
].spacing(8);
|
||||||
|
|
||||||
// Peer volume slider
|
// Peer volume slider
|
||||||
let current_vol = state.peer_volumes.get(peer_id).copied().unwrap_or(1.0);
|
let current_vol = state
|
||||||
|
.config
|
||||||
|
.peer_volume
|
||||||
|
.get(&peer_id.to_string())
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(1.0);
|
||||||
card_content = card_content.push(
|
card_content = card_content.push(
|
||||||
row![
|
row![
|
||||||
text("Vol:").size(12).color(color_subtext),
|
text("Vol:").size(12).color(color_subtext),
|
||||||
slider(0.0..=2.0, current_vol, move |v| AppMessage::PeerVolumeChanged(peer_id_clone, v))
|
slider(0.0..=2.0, current_vol, move |v| AppMessage::PeerVolumeChanged(peer_id_clone, v))
|
||||||
|
.step(0.01)
|
||||||
|
.on_release(AppMessage::PersistConfig)
|
||||||
].spacing(8).align_y(iced::alignment::Vertical::Center)
|
].spacing(8).align_y(iced::alignment::Vertical::Center)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -3371,6 +3419,28 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
.align_y(iced::alignment::Vertical::Center),
|
.align_y(iced::alignment::Vertical::Center),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Peer noise gate: suppress this peer's background noise on our end.
|
||||||
|
// Threshold is normalized RMS on the same 0..METER_MAX scale as the
|
||||||
|
// mic gate; 0 = off.
|
||||||
|
let current_gate = state.config.peer_gate.get(&peer_key).copied().unwrap_or(0.0);
|
||||||
|
let gate_label = if current_gate <= 0.0 {
|
||||||
|
"Off".to_string()
|
||||||
|
} else {
|
||||||
|
format!("{:.0}%", (current_gate / METER_MAX * 100.0).clamp(0.0, 100.0))
|
||||||
|
};
|
||||||
|
card_content = card_content.push(
|
||||||
|
row![
|
||||||
|
text("Gate:").size(12).color(color_subtext),
|
||||||
|
container(text(gate_label).size(11).color(color_subtext))
|
||||||
|
.width(iced::Length::Fixed(58.0)),
|
||||||
|
slider(0.0..=METER_MAX, current_gate, move |v| AppMessage::PeerGateChanged(peer_id_clone, v))
|
||||||
|
.step(0.001)
|
||||||
|
.on_release(AppMessage::PersistConfig),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(iced::alignment::Vertical::Center),
|
||||||
|
);
|
||||||
|
|
||||||
let eq = peer_eq_settings(&state.config, peer_id);
|
let eq = peer_eq_settings(&state.config, peer_id);
|
||||||
let eq_row = |label: &'static str, band: EqBand, value: f32| -> Element<'_, AppMessage> {
|
let eq_row = |label: &'static str, band: EqBand, value: f32| -> Element<'_, AppMessage> {
|
||||||
row![
|
row![
|
||||||
@@ -4900,8 +4970,48 @@ impl Program<AppMessage> for Icon {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
format_duration, initial_window_position, reconnect_attempt_chime, reconnected_chime,
|
format_duration, initial_window_position, reconnect_attempt_chime, reconnected_chime,
|
||||||
GateMeter, METER_MAX,
|
set_peer_gate_config, set_peer_volume_config, AppConfig, GateMeter, METER_MAX,
|
||||||
};
|
};
|
||||||
|
use iroh::SecretKey;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn peer_gate_persists_when_on_and_clears_when_off() {
|
||||||
|
let mut config = AppConfig::default();
|
||||||
|
let id = SecretKey::generate().public();
|
||||||
|
|
||||||
|
// A positive threshold is stored, clamped to the slider's METER_MAX ceiling.
|
||||||
|
let stored = set_peer_gate_config(&mut config, id, 0.05);
|
||||||
|
assert_eq!(stored, 0.05);
|
||||||
|
assert_eq!(config.peer_gate.get(&id.to_string()).copied(), Some(0.05));
|
||||||
|
assert_eq!(set_peer_gate_config(&mut config, id, 99.0), METER_MAX);
|
||||||
|
|
||||||
|
// Zero (or negative) means "gate off" — the entry is removed so the
|
||||||
|
// config doesn't carry a disabled gate.
|
||||||
|
let stored = set_peer_gate_config(&mut config, id, 0.0);
|
||||||
|
assert_eq!(stored, 0.0);
|
||||||
|
assert!(!config.peer_gate.contains_key(&id.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn peer_volume_persists_non_unity_and_clears_at_unity() {
|
||||||
|
let mut config = AppConfig::default();
|
||||||
|
let id = SecretKey::generate().public();
|
||||||
|
|
||||||
|
// A non-unity value is clamped into range and stored.
|
||||||
|
let stored = set_peer_volume_config(&mut config, id, 1.5);
|
||||||
|
assert_eq!(stored, 1.5);
|
||||||
|
assert_eq!(config.peer_volume.get(&id.to_string()).copied(), Some(1.5));
|
||||||
|
|
||||||
|
// Out-of-range values clamp to the slider bounds.
|
||||||
|
assert_eq!(set_peer_volume_config(&mut config, id, 5.0), 2.0);
|
||||||
|
assert_eq!(set_peer_volume_config(&mut config, id, -1.0), 0.0);
|
||||||
|
|
||||||
|
// Returning to unity removes the entry (unity is the implicit default),
|
||||||
|
// so the config doesn't accumulate no-op entries.
|
||||||
|
let stored = set_peer_volume_config(&mut config, id, 1.0);
|
||||||
|
assert_eq!(stored, 1.0);
|
||||||
|
assert!(!config.peer_volume.contains_key(&id.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn x11_restores_saved_window_position() {
|
fn x11_restores_saved_window_position() {
|
||||||
|
|||||||
@@ -255,6 +255,15 @@ pub struct AppConfig {
|
|||||||
/// keyed by peer node id string. Local preference only.
|
/// keyed by peer node id string. Local preference only.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub peer_pan: HashMap<String, f32>,
|
pub peer_pan: HashMap<String, f32>,
|
||||||
|
/// Per-peer listener-side volume/gain (`1.0` = unity), keyed by peer node id
|
||||||
|
/// string. Local preference only; never sent to peers. Absent entry = unity.
|
||||||
|
#[serde(default)]
|
||||||
|
pub peer_volume: HashMap<String, f32>,
|
||||||
|
/// Per-peer listener-side noise-gate threshold (normalized RMS, `0.0` = off),
|
||||||
|
/// keyed by peer node id string. Local preference only; never sent to peers.
|
||||||
|
/// Absent entry = gate disabled (pass-through).
|
||||||
|
#[serde(default)]
|
||||||
|
pub peer_gate: HashMap<String, f32>,
|
||||||
/// Focused app-local keyboard shortcuts.
|
/// Focused app-local keyboard shortcuts.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub hotkeys: crate::hotkeys::HotkeyMap,
|
pub hotkeys: crate::hotkeys::HotkeyMap,
|
||||||
@@ -317,6 +326,8 @@ impl Default for AppConfig {
|
|||||||
recents: Vec::new(),
|
recents: Vec::new(),
|
||||||
peer_eq: HashMap::new(),
|
peer_eq: HashMap::new(),
|
||||||
peer_pan: HashMap::new(),
|
peer_pan: HashMap::new(),
|
||||||
|
peer_volume: HashMap::new(),
|
||||||
|
peer_gate: HashMap::new(),
|
||||||
hotkeys: crate::hotkeys::HotkeyMap::default(),
|
hotkeys: crate::hotkeys::HotkeyMap::default(),
|
||||||
window_width: default_window_width(),
|
window_width: default_window_width(),
|
||||||
window_height: default_window_height(),
|
window_height: default_window_height(),
|
||||||
@@ -457,6 +468,8 @@ mod tests {
|
|||||||
// shortcut settings.
|
// shortcut settings.
|
||||||
assert!(deserialized.peer_eq.is_empty());
|
assert!(deserialized.peer_eq.is_empty());
|
||||||
assert!(deserialized.peer_pan.is_empty());
|
assert!(deserialized.peer_pan.is_empty());
|
||||||
|
assert!(deserialized.peer_volume.is_empty());
|
||||||
|
assert!(deserialized.peer_gate.is_empty());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
crate::hotkeys::format_binding(
|
crate::hotkeys::format_binding(
|
||||||
deserialized
|
deserialized
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ pub enum CoreCommand {
|
|||||||
SetPeerEq(EndpointId, crate::audio::eq::EqSettings),
|
SetPeerEq(EndpointId, crate::audio::eq::EqSettings),
|
||||||
/// Listener-side per-peer pan. Local only; never leaves this app instance.
|
/// Listener-side per-peer pan. Local only; never leaves this app instance.
|
||||||
SetPeerPan(EndpointId, f32),
|
SetPeerPan(EndpointId, f32),
|
||||||
|
/// Listener-side per-peer noise gate threshold (normalized RMS, `0.0` = off).
|
||||||
|
/// Applies the same smooth gate as the mic path to a peer's incoming audio,
|
||||||
|
/// to suppress their background noise on our end. Local only.
|
||||||
|
SetPeerGate(EndpointId, f32),
|
||||||
/// Locally mute/unmute a peer: when muted, their audio is decoded (so levels
|
/// Locally mute/unmute a peer: when muted, their audio is decoded (so levels
|
||||||
/// still show) but not mixed into our output.
|
/// still show) but not mixed into our output.
|
||||||
SetPeerMuted(EndpointId, bool),
|
SetPeerMuted(EndpointId, bool),
|
||||||
|
|||||||
@@ -869,6 +869,8 @@ async fn run_core_loop(
|
|||||||
let peer_volumes = Arc::new(Mutex::new(HashMap::<EndpointId, f32>::new()));
|
let peer_volumes = Arc::new(Mutex::new(HashMap::<EndpointId, f32>::new()));
|
||||||
let peer_eq = Arc::new(Mutex::new(HashMap::<EndpointId, EqSettings>::new()));
|
let peer_eq = Arc::new(Mutex::new(HashMap::<EndpointId, EqSettings>::new()));
|
||||||
let peer_pan = Arc::new(Mutex::new(HashMap::<EndpointId, f32>::new()));
|
let peer_pan = Arc::new(Mutex::new(HashMap::<EndpointId, f32>::new()));
|
||||||
|
// Listener-side per-peer noise-gate thresholds (normalized RMS, 0.0 = off).
|
||||||
|
let peer_gate = Arc::new(Mutex::new(HashMap::<EndpointId, f32>::new()));
|
||||||
// Peers locally muted by us: decoded for level metering but not mixed.
|
// Peers locally muted by us: decoded for level metering but not mixed.
|
||||||
let locally_muted = Arc::new(Mutex::new(HashSet::<EndpointId>::new()));
|
let locally_muted = Arc::new(Mutex::new(HashSet::<EndpointId>::new()));
|
||||||
let mut current_name = "Anonymous".to_string();
|
let mut current_name = "Anonymous".to_string();
|
||||||
@@ -1397,6 +1399,7 @@ async fn run_core_loop(
|
|||||||
let peer_volumes_mixer = peer_volumes.clone();
|
let peer_volumes_mixer = peer_volumes.clone();
|
||||||
let peer_eq_mixer = peer_eq.clone();
|
let peer_eq_mixer = peer_eq.clone();
|
||||||
let peer_pan_mixer = peer_pan.clone();
|
let peer_pan_mixer = peer_pan.clone();
|
||||||
|
let peer_gate_mixer = peer_gate.clone();
|
||||||
let locally_muted_mixer = locally_muted.clone();
|
let locally_muted_mixer = locally_muted.clone();
|
||||||
let output_gain_mixer = output_gain.clone();
|
let output_gain_mixer = output_gain.clone();
|
||||||
let ui_tx_mixer = ui_tx.clone();
|
let ui_tx_mixer = ui_tx.clone();
|
||||||
@@ -1413,6 +1416,11 @@ async fn run_core_loop(
|
|||||||
// Per-peer EQ filter state. Settings are live-cloned each
|
// Per-peer EQ filter state. Settings are live-cloned each
|
||||||
// cycle; state is rebuilt only when a peer's EQ changes.
|
// cycle; state is rebuilt only when a peer's EQ changes.
|
||||||
let mut peer_eqs: HashMap<EndpointId, Eq> = HashMap::new();
|
let mut peer_eqs: HashMap<EndpointId, Eq> = HashMap::new();
|
||||||
|
// Per-peer noise-gate envelope state. The threshold is passed
|
||||||
|
// per frame (live slider), so the gate is never rebuilt — only
|
||||||
|
// created once per peer and dropped when the peer leaves.
|
||||||
|
let mut peer_noise_gates: HashMap<EndpointId, crate::audio::gate::NoiseGate> =
|
||||||
|
HashMap::new();
|
||||||
// When the ring is at/above target we have nothing to do; nap
|
// When the ring is at/above target we have nothing to do; nap
|
||||||
// briefly and re-check. Short enough (relative to the ~60ms
|
// briefly and re-check. Short enough (relative to the ~60ms
|
||||||
// target and ~21ms device quantum) that we always refill well
|
// target and ~21ms device quantum) that we always refill well
|
||||||
@@ -1438,6 +1446,7 @@ async fn run_core_loop(
|
|||||||
let current_volumes = peer_volumes_mixer.lock().await.clone();
|
let current_volumes = peer_volumes_mixer.lock().await.clone();
|
||||||
let current_eq = peer_eq_mixer.lock().await.clone();
|
let current_eq = peer_eq_mixer.lock().await.clone();
|
||||||
let current_pans = peer_pan_mixer.lock().await.clone();
|
let current_pans = peer_pan_mixer.lock().await.clone();
|
||||||
|
let current_gates = peer_gate_mixer.lock().await.clone();
|
||||||
let muted_peers = locally_muted_mixer.lock().await.clone();
|
let muted_peers = locally_muted_mixer.lock().await.clone();
|
||||||
let mut peer_frames: Vec<(Vec<i16>, f32)> = Vec::new();
|
let mut peer_frames: Vec<(Vec<i16>, f32)> = Vec::new();
|
||||||
let mut peers_seen = HashSet::new();
|
let mut peers_seen = HashSet::new();
|
||||||
@@ -1463,6 +1472,26 @@ async fn run_core_loop(
|
|||||||
stems.push((peer_id, frame.clone()));
|
stems.push((peer_id, frame.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listener-side per-peer noise gate, applied to the
|
||||||
|
// raw decoded frame (after the clean stem tap, before
|
||||||
|
// volume/EQ) so the threshold tracks the peer's true
|
||||||
|
// signal level regardless of our volume setting. The
|
||||||
|
// gate's "should transmit" return is irrelevant here —
|
||||||
|
// we only attenuate. Threshold 0 = off; the gate is
|
||||||
|
// created lazily and dropped when disabled.
|
||||||
|
let gate_threshold =
|
||||||
|
current_gates.get(&peer_id).copied().unwrap_or(0.0);
|
||||||
|
if gate_threshold > 0.0 {
|
||||||
|
peer_noise_gates
|
||||||
|
.entry(peer_id)
|
||||||
|
.or_insert_with(|| {
|
||||||
|
crate::audio::gate::NoiseGate::new(48_000)
|
||||||
|
})
|
||||||
|
.process(&mut frame, gate_threshold);
|
||||||
|
} else {
|
||||||
|
peer_noise_gates.remove(&peer_id);
|
||||||
|
}
|
||||||
|
|
||||||
let vol = current_volumes.get(&peer_id).copied().unwrap_or(1.0);
|
let vol = current_volumes.get(&peer_id).copied().unwrap_or(1.0);
|
||||||
apply_volume(&mut frame, vol);
|
apply_volume(&mut frame, vol);
|
||||||
|
|
||||||
@@ -1507,6 +1536,8 @@ async fn run_core_loop(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
peer_eqs.retain(|id, _| peers_seen.contains(id) || current_eq.contains_key(id));
|
peer_eqs.retain(|id, _| peers_seen.contains(id) || current_eq.contains_key(id));
|
||||||
|
peer_noise_gates
|
||||||
|
.retain(|id, _| peers_seen.contains(id) || current_gates.contains_key(id));
|
||||||
|
|
||||||
// Lossless i32 sum, then the limiter applies the master
|
// Lossless i32 sum, then the limiter applies the master
|
||||||
// output gain (in f32, so a boost past the ceiling is
|
// output gain (in f32, so a boost past the ceiling is
|
||||||
@@ -1884,6 +1915,16 @@ async fn run_core_loop(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CoreCommand::SetPeerGate(peer_id, threshold) => {
|
||||||
|
let threshold = threshold.clamp(0.0, 1.0);
|
||||||
|
let mut guard = peer_gate.lock().await;
|
||||||
|
if threshold <= 0.0 {
|
||||||
|
guard.remove(&peer_id);
|
||||||
|
} else {
|
||||||
|
guard.insert(peer_id, threshold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CoreCommand::SetPeerMuted(peer_id, muted) => {
|
CoreCommand::SetPeerMuted(peer_id, muted) => {
|
||||||
let mut guard = locally_muted.lock().await;
|
let mut guard = locally_muted.lock().await;
|
||||||
if muted {
|
if muted {
|
||||||
|
|||||||
Reference in New Issue
Block a user