feat(debug): GUI toggle for DRED verbose logs + macOS mic permission

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) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-04-11 09:48:32 +04:00
parent 16890576fb
commit 578ff8cff4
7 changed files with 143 additions and 50 deletions

View File

@@ -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

View File

@@ -186,10 +186,10 @@ 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.
// 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,
@@ -205,6 +205,7 @@ impl OpusEncoder {
LIBOPUS_VERSION_LOGGED.get_or_init(|| {
info!(libopus_version = %opusic_c::version(), "linked libopus version");
});
}
Ok(())
}

View File

@@ -169,6 +169,10 @@
<input id="s-agc" type="checkbox" checked />
Automatic Gain Control
</label>
<label class="checkbox">
<input id="s-dred-debug" type="checkbox" />
DRED debug logs (verbose, dev only)
</label>
</div>
<div class="settings-section">
<h3>Identity</h3>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!--
Custom Info.plist keys merged into the bundled WarzonePhone.app by
tauri-bundler. The base Info.plist (CFBundleIdentifier, version,
etc.) is generated from tauri.conf.json — only put *additional*
keys here.
NSMicrophoneUsageDescription is required by macOS TCC for any
app that opens an audio input unit. Without this string the OS
silently denies CoreAudio capture (input callbacks return zeros)
and the app never appears in System Settings → Privacy &
Security → Microphone. This was the root cause of the desktop
mic regression where phones could not hear the desktop client.
-->
<key>NSMicrophoneUsageDescription</key>
<string>WarzonePhone needs microphone access to transmit your voice during calls.</string>
</dict>
</plist>

View File

@@ -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,11 +225,11 @@ 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.
// 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,
@@ -240,6 +240,7 @@ impl DredRecvState {
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,6 +697,7 @@ 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);
if wzp_codec::dred_verbose_logs() {
info!(
recv_fr = fr,
decoded_frames,
@@ -709,6 +712,18 @@ impl CallEngine {
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();
}
}

View File

@@ -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<PingResult, String> {
@@ -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");

View File

@@ -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();