From 578ff8cff449a0972d762ce39ab4b33cd0eee65c Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Sat, 11 Apr 2026 09:48:32 +0400 Subject: [PATCH] feat(debug): GUI toggle for DRED verbose logs + macOS mic permission MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DRED verbose logs (off by default — keeps logcat clean in normal use): - wzp-codec: DRED_VERBOSE_LOGS atomic flag with dred_verbose_logs() / set_dred_verbose_logs() helpers - opus_enc: gate "DRED enabled" + libopus version logs behind the flag - desktop/src-tauri/engine.rs: gate DredRecvState parse log, reconstruction log, classical PLC log, and DRED-counter fields in the Android recv heartbeat (non-verbose path still logs basic recv stats) - Tauri commands set_dred_verbose_logs / get_dred_verbose_logs - Settings panel gets a "DRED debug logs (verbose, dev only)" checkbox; preference persists in wzp-settings localStorage and is pushed to Rust on save and on app boot macOS mic permission: - Add desktop/src-tauri/Info.plist with NSMicrophoneUsageDescription. Without it, modern macOS silently denies CoreAudio capture for ad-hoc-signed Tauri builds — capture starts but every callback hands you zeros. Symptom: phones could not hear desktop client, desktop could still hear phones (playout has no TCC gate). The Tauri 2 bundler auto-merges this file into WarzonePhone.app's Contents/Info.plist on the next build, so first launch will pop the standard mic prompt. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/wzp-codec/src/lib.rs | 20 ++++++++ crates/wzp-codec/src/opus_enc.rs | 37 +++++++-------- desktop/index.html | 4 ++ desktop/src-tauri/Info.plist | 21 +++++++++ desktop/src-tauri/src/engine.rs | 79 +++++++++++++++++++------------- desktop/src-tauri/src/lib.rs | 17 +++++++ desktop/src/main.ts | 15 ++++++ 7 files changed, 143 insertions(+), 50 deletions(-) create mode 100644 desktop/src-tauri/Info.plist diff --git a/crates/wzp-codec/src/lib.rs b/crates/wzp-codec/src/lib.rs index 30cf1e7..f923170 100644 --- a/crates/wzp-codec/src/lib.rs +++ b/crates/wzp-codec/src/lib.rs @@ -28,6 +28,26 @@ pub use denoise::NoiseSupressor; pub use silence::{ComfortNoise, SilenceDetector}; pub use wzp_proto::{AudioDecoder, AudioEncoder, CodecId, QualityProfile}; +use std::sync::atomic::{AtomicBool, Ordering}; + +/// Global verbose-logging flag for DRED. Off by default — when enabled +/// (via the GUI debug toggle wired through Tauri), the encoder logs its +/// DRED config + libopus version, and the recv path logs every DRED +/// reconstruction, classical PLC fill, and parse heartbeat. Off in +/// "normal" mode keeps logcat clean. +static DRED_VERBOSE_LOGS: AtomicBool = AtomicBool::new(false); + +/// Returns whether DRED verbose logging is currently enabled. +#[inline] +pub fn dred_verbose_logs() -> bool { + DRED_VERBOSE_LOGS.load(Ordering::Relaxed) +} + +/// Enable/disable DRED verbose logging at runtime. +pub fn set_dred_verbose_logs(enabled: bool) { + DRED_VERBOSE_LOGS.store(enabled, Ordering::Relaxed); +} + /// Create an adaptive encoder starting at the given quality profile. /// /// The returned encoder accepts 48 kHz mono PCM regardless of the active diff --git a/crates/wzp-codec/src/opus_enc.rs b/crates/wzp-codec/src/opus_enc.rs index 43bf756..b5e156a 100644 --- a/crates/wzp-codec/src/opus_enc.rs +++ b/crates/wzp-codec/src/opus_enc.rs @@ -186,25 +186,26 @@ impl OpusEncoder { .set_packet_loss(DRED_LOSS_FLOOR_PCT) .map_err(|e| CodecError::EncodeFailed(format!("set packet loss floor: {e:?}")))?; - // Bumped from debug! to info! so the DRED config is visible in - // logcat without enabling debug-level filtering. Each call's - // first OpusEncoder construction will log this; subsequent - // profile switches log it again with the new tier. - info!( - codec = ?codec, - dred_frames, - dred_ms = dred_frames as u32 * 10, - loss_floor_pct = DRED_LOSS_FLOOR_PCT, - "opus encoder: DRED enabled" - ); + // Both of these are gated behind the GUI debug toggle so logcat + // stays clean in normal mode. Flip "DRED verbose logs" in the + // settings panel to see the per-encoder config + libopus version. + if crate::dred_verbose_logs() { + info!( + codec = ?codec, + dred_frames, + dred_ms = dred_frames as u32 * 10, + loss_floor_pct = DRED_LOSS_FLOOR_PCT, + "opus encoder: DRED enabled" + ); - // One-shot logging of the linked libopus version so we can - // confirm at a glance that opusic-c (libopus 1.5.2) is loaded. - // Pre-Phase-0 audiopus shipped libopus 1.3 which has no DRED; - // if this log says "libopus 1.3" something is very wrong. - LIBOPUS_VERSION_LOGGED.get_or_init(|| { - info!(libopus_version = %opusic_c::version(), "linked libopus version"); - }); + // One-shot logging of the linked libopus version so we can + // confirm at a glance that opusic-c (libopus 1.5.2) is loaded. + // Pre-Phase-0 audiopus shipped libopus 1.3 which has no DRED; + // if this log says "libopus 1.3" something is very wrong. + LIBOPUS_VERSION_LOGGED.get_or_init(|| { + info!(libopus_version = %opusic_c::version(), "linked libopus version"); + }); + } Ok(()) } diff --git a/desktop/index.html b/desktop/index.html index 9f31ac9..2770fc2 100644 --- a/desktop/index.html +++ b/desktop/index.html @@ -169,6 +169,10 @@ Automatic Gain Control +

Identity

diff --git a/desktop/src-tauri/Info.plist b/desktop/src-tauri/Info.plist new file mode 100644 index 0000000..15e1cfd --- /dev/null +++ b/desktop/src-tauri/Info.plist @@ -0,0 +1,21 @@ + + + + + + NSMicrophoneUsageDescription + WarzonePhone needs microphone access to transmit your voice during calls. + + diff --git a/desktop/src-tauri/src/engine.rs b/desktop/src-tauri/src/engine.rs index 884095b..63cdd60 100644 --- a/desktop/src-tauri/src/engine.rs +++ b/desktop/src-tauri/src/engine.rs @@ -159,7 +159,7 @@ impl DredRecvState { // is steady-state without drowning the log. let should_log = self.parses_with_data == 1 || self.parses_with_data % 100 == 0; - if should_log { + if should_log && wzp_codec::dred_verbose_logs() { info!( seq, samples_available = available, @@ -225,21 +225,22 @@ impl DredRecvState { match reconstructed { Some(_n) => { self.dred_reconstructions += 1; - // Log every DRED reconstruction. These are - // rare events on a clean network — when - // they fire, we want to know exactly which - // gap was filled and how the offset math - // played out. Acceptable to be chatty here. - info!( - missing_seq, - anchor_seq = ?self.last_good_seq, - offset_samples, - offset_ms = offset_samples / 48, - samples_available = available, - gap_size = gap, - total_dred_recoveries = self.dred_reconstructions, - "DRED reconstruction fired for missing frame" - ); + // Log every DRED reconstruction (gated behind + // the GUI verbose-logs toggle). When enabled, + // we want to know exactly which gap was + // filled and how the offset math played out. + if wzp_codec::dred_verbose_logs() { + info!( + missing_seq, + anchor_seq = ?self.last_good_seq, + offset_samples, + offset_ms = offset_samples / 48, + samples_available = available, + gap_size = gap, + total_dred_recoveries = self.dred_reconstructions, + "DRED reconstruction fired for missing frame" + ); + } emit(out); } None => { @@ -251,8 +252,9 @@ impl DredRecvState { // is whichever check failed in the if // above (offset out of range, no good // state, or reconstruct error). - if self.classical_plc_invocations <= 3 - || self.classical_plc_invocations % 50 == 0 + if (self.classical_plc_invocations <= 3 + || self.classical_plc_invocations % 50 == 0) + && wzp_codec::dred_verbose_logs() { info!( missing_seq, @@ -695,20 +697,33 @@ impl CallEngine { // Heartbeat every 2s with decode+playout state if heartbeat.elapsed() >= std::time::Duration::from_secs(2) { let fr = recv_fr.load(Ordering::Relaxed); - info!( - recv_fr = fr, - decoded_frames, - last_decode_n, - last_written, - written_samples, - decode_errs, - codec = ?current_codec, - dred_recv = dred_recv.dred_reconstructions, - classical_plc = dred_recv.classical_plc_invocations, - dred_parses_with_data = dred_recv.parses_with_data, - dred_parses_total = dred_recv.parses_total, - "recv heartbeat (android)" - ); + if wzp_codec::dred_verbose_logs() { + info!( + recv_fr = fr, + decoded_frames, + last_decode_n, + last_written, + written_samples, + decode_errs, + codec = ?current_codec, + dred_recv = dred_recv.dred_reconstructions, + classical_plc = dred_recv.classical_plc_invocations, + dred_parses_with_data = dred_recv.parses_with_data, + dred_parses_total = dred_recv.parses_total, + "recv heartbeat (android)" + ); + } else { + info!( + recv_fr = fr, + decoded_frames, + last_decode_n, + last_written, + written_samples, + decode_errs, + codec = ?current_codec, + "recv heartbeat (android)" + ); + } heartbeat = std::time::Instant::now(); } } diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 44205ca..985ef5a 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -110,6 +110,22 @@ struct PingResult { server_fingerprint: String, } +/// Toggle DRED verbose logging at runtime (gates the chatty per-frame +/// reconstruction + parse logs in opus_enc and engine.rs). Wired to the +/// "DRED debug logs" checkbox in the GUI settings panel. +#[tauri::command] +fn set_dred_verbose_logs(enabled: bool) { + wzp_codec::set_dred_verbose_logs(enabled); + tracing::info!(enabled, "DRED verbose logs toggled"); +} + +/// Read the current DRED verbose logging flag (so the GUI can hydrate +/// its checkbox on startup without trusting localStorage alone). +#[tauri::command] +fn get_dred_verbose_logs() -> bool { + wzp_codec::dred_verbose_logs() +} + /// Ping a relay to check if it's online, measure RTT, and get server identity. #[tauri::command] async fn ping_relay(relay: String) -> Result { @@ -687,6 +703,7 @@ pub fn run() { deregister, set_speakerphone, is_speakerphone_on, get_call_history, get_recent_contacts, clear_call_history, + set_dred_verbose_logs, get_dred_verbose_logs, ]) .run(tauri::generate_context!()) .expect("error while running WarzonePhone"); diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 7cd6640..df31422 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -82,6 +82,7 @@ const settingsBtnCall = document.getElementById("settings-btn-call")!; const sRoom = document.getElementById("s-room") as HTMLInputElement; const sAlias = document.getElementById("s-alias") as HTMLInputElement; const sOsAec = document.getElementById("s-os-aec") as HTMLInputElement; +const sDredDebug = document.getElementById("s-dred-debug") as HTMLInputElement; const sAgc = document.getElementById("s-agc") as HTMLInputElement; const sQuality = document.getElementById("s-quality") as HTMLInputElement; const sQualityLabel = document.getElementById("s-quality-label")!; @@ -140,6 +141,10 @@ interface Settings { agc: boolean; quality: string; recentRooms: RecentRoom[]; + /// When true, the Rust side emits the chatty per-frame DRED parse + + /// reconstruction + classical-PLC logs and adds DRED counters to the + /// recv heartbeat. Off in normal mode keeps logcat clean. + dredDebugLogs: boolean; } function loadSettings(): Settings { @@ -152,6 +157,7 @@ function loadSettings(): Settings { ], selectedRelay: 0, room: "general", alias: "", osAec: true, agc: true, quality: "auto", recentRooms: [], + dredDebugLogs: false, }; try { const raw = localStorage.getItem("wzp-settings"); @@ -402,6 +408,10 @@ function renderRecentRooms(rooms: RecentRoom[]) { // ── Init ── applySettings(); setTimeout(pingAllRelays, 300); +// Hydrate the Rust DRED verbose-logs flag from saved settings on boot so +// the choice survives app restarts without needing the user to reopen +// the settings panel. +invoke("set_dred_verbose_logs", { enabled: !!loadSettings().dredDebugLogs }).catch(() => {}); // Load fingerprint + alias + git hash + render identicon interface AppInfo { git_hash: string; alias: string; fingerprint: string; data_dir: string } @@ -714,6 +724,7 @@ listen("call-event", (event: any) => { function openSettings() { const s = loadSettings(); sRoom.value = s.room; sAlias.value = s.alias; sOsAec.checked = s.osAec; + sDredDebug.checked = !!s.dredDebugLogs; const qi = qualityToIndex(s.quality || "auto"); sQuality.value = String(qi); updateQualityUI(qi); @@ -753,7 +764,11 @@ settingsSave.addEventListener("click", () => { const s = loadSettings(); s.room = sRoom.value; s.alias = sAlias.value; s.osAec = sOsAec.checked; s.quality = QUALITY_STEPS[parseInt(sQuality.value)] || "auto"; + s.dredDebugLogs = sDredDebug.checked; saveSettingsObj(s); + // Push the new flag to the Rust side immediately so the next encoded + // frame already honors it without waiting for an app restart. + invoke("set_dred_verbose_logs", { enabled: s.dredDebugLogs }).catch(() => {}); roomInput.value = s.room; aliasInput.value = s.alias; osAecCheckbox.checked = s.osAec; renderRecentRooms(s.recentRooms); closeSettings();