feat: echo cancellation via PipeWire module-echo-cancel
Add an opt-in Echo Cancellation toggle (Settings) that routes the call through PipeWire's module-echo-cancel (WebRTC AEC + noise suppression + AGC) instead of running an in-process canceller. PipeWire already sees both the mic and the speaker monitor, so it handles the echo-reference alignment for free and we avoid a C++ DSP dependency. src/audio/echo_cancel.rs (new): - enable(real_source, real_sink) loads the module via pactl (aec_method=webrtc), bound to the chosen devices with source_master/sink_master (defaults if unset), waits for the virtual nodes to appear, and returns an RAII guard that unloads the module on drop. Best-effort pre-clean of a stale instance from a crashed run. - EC_SOURCE / EC_SINK are the virtual cleaned-mic source and reference sink. core: when echo_cancellation is set on Join, load the module and point capture at EC_SOURCE / playback at EC_SINK; stash the guard in ActiveSession so it unloads on shutdown (after the audio streams release the nodes). Any failure logs + warns the UI and falls back to the direct devices — never blocks the call. config: new echo_cancellation_enabled (serde default false). app: Settings checkbox under Mic Sensitivity, applied on next room join. An ignored live smoke test covers the real load/unload path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -46,6 +46,7 @@ pub enum AppMessage {
|
||||
NavigateToSettings,
|
||||
NavigateBack,
|
||||
ToggleNotifications(bool),
|
||||
ToggleEchoCancellation(bool),
|
||||
CustomSoundPathChanged(Sound, String),
|
||||
}
|
||||
|
||||
@@ -196,6 +197,7 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
||||
ticket: state.ticket_input.clone(),
|
||||
input_device,
|
||||
output_device,
|
||||
echo_cancellation: state.config.echo_cancellation_enabled,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -208,6 +210,7 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
||||
ticket: "create".to_string(),
|
||||
input_device,
|
||||
output_device,
|
||||
echo_cancellation: state.config.echo_cancellation_enabled,
|
||||
});
|
||||
}
|
||||
AppMessage::LeavePressed => {
|
||||
@@ -331,6 +334,11 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
||||
state.config.save();
|
||||
notify::set_enabled(enabled);
|
||||
}
|
||||
AppMessage::ToggleEchoCancellation(enabled) => {
|
||||
state.config.echo_cancellation_enabled = enabled;
|
||||
state.config.save();
|
||||
// Applied on the next join, since the audio graph is rebuilt then.
|
||||
}
|
||||
AppMessage::CustomSoundPathChanged(sound, path) => {
|
||||
let path_opt = if path.trim().is_empty() { None } else { Some(path) };
|
||||
match sound {
|
||||
@@ -515,6 +523,11 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
||||
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).step(0.001),
|
||||
text("Smoothly fades out audio below this level. 0% disables the gate.").size(11).color(color_subtext),
|
||||
vertical_space(4.0),
|
||||
checkbox(state.config.echo_cancellation_enabled)
|
||||
.label("Echo cancellation")
|
||||
.on_toggle(AppMessage::ToggleEchoCancellation),
|
||||
text("Cancels speaker echo + suppresses noise (PipeWire). Takes effect on your next room join.").size(11).color(color_subtext),
|
||||
].spacing(8).width(iced::Length::Fill),
|
||||
column![
|
||||
text("Network Privacy").size(14).color(color_subtext),
|
||||
|
||||
Reference in New Issue
Block a user