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:
@@ -28,6 +28,26 @@ pub use denoise::NoiseSupressor;
|
|||||||
pub use silence::{ComfortNoise, SilenceDetector};
|
pub use silence::{ComfortNoise, SilenceDetector};
|
||||||
pub use wzp_proto::{AudioDecoder, AudioEncoder, CodecId, QualityProfile};
|
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.
|
/// Create an adaptive encoder starting at the given quality profile.
|
||||||
///
|
///
|
||||||
/// The returned encoder accepts 48 kHz mono PCM regardless of the active
|
/// The returned encoder accepts 48 kHz mono PCM regardless of the active
|
||||||
|
|||||||
@@ -186,25 +186,26 @@ impl OpusEncoder {
|
|||||||
.set_packet_loss(DRED_LOSS_FLOOR_PCT)
|
.set_packet_loss(DRED_LOSS_FLOOR_PCT)
|
||||||
.map_err(|e| CodecError::EncodeFailed(format!("set packet loss floor: {e:?}")))?;
|
.map_err(|e| CodecError::EncodeFailed(format!("set packet loss floor: {e:?}")))?;
|
||||||
|
|
||||||
// Bumped from debug! to info! so the DRED config is visible in
|
// Both of these are gated behind the GUI debug toggle so logcat
|
||||||
// logcat without enabling debug-level filtering. Each call's
|
// stays clean in normal mode. Flip "DRED verbose logs" in the
|
||||||
// first OpusEncoder construction will log this; subsequent
|
// settings panel to see the per-encoder config + libopus version.
|
||||||
// profile switches log it again with the new tier.
|
if crate::dred_verbose_logs() {
|
||||||
info!(
|
info!(
|
||||||
codec = ?codec,
|
codec = ?codec,
|
||||||
dred_frames,
|
dred_frames,
|
||||||
dred_ms = dred_frames as u32 * 10,
|
dred_ms = dred_frames as u32 * 10,
|
||||||
loss_floor_pct = DRED_LOSS_FLOOR_PCT,
|
loss_floor_pct = DRED_LOSS_FLOOR_PCT,
|
||||||
"opus encoder: DRED enabled"
|
"opus encoder: DRED enabled"
|
||||||
);
|
);
|
||||||
|
|
||||||
// One-shot logging of the linked libopus version so we can
|
// One-shot logging of the linked libopus version so we can
|
||||||
// confirm at a glance that opusic-c (libopus 1.5.2) is loaded.
|
// 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;
|
// Pre-Phase-0 audiopus shipped libopus 1.3 which has no DRED;
|
||||||
// if this log says "libopus 1.3" something is very wrong.
|
// if this log says "libopus 1.3" something is very wrong.
|
||||||
LIBOPUS_VERSION_LOGGED.get_or_init(|| {
|
LIBOPUS_VERSION_LOGGED.get_or_init(|| {
|
||||||
info!(libopus_version = %opusic_c::version(), "linked libopus version");
|
info!(libopus_version = %opusic_c::version(), "linked libopus version");
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,6 +169,10 @@
|
|||||||
<input id="s-agc" type="checkbox" checked />
|
<input id="s-agc" type="checkbox" checked />
|
||||||
Automatic Gain Control
|
Automatic Gain Control
|
||||||
</label>
|
</label>
|
||||||
|
<label class="checkbox">
|
||||||
|
<input id="s-dred-debug" type="checkbox" />
|
||||||
|
DRED debug logs (verbose, dev only)
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3>Identity</h3>
|
<h3>Identity</h3>
|
||||||
|
|||||||
21
desktop/src-tauri/Info.plist
Normal file
21
desktop/src-tauri/Info.plist
Normal 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>
|
||||||
@@ -159,7 +159,7 @@ impl DredRecvState {
|
|||||||
// is steady-state without drowning the log.
|
// is steady-state without drowning the log.
|
||||||
let should_log = self.parses_with_data == 1
|
let should_log = self.parses_with_data == 1
|
||||||
|| self.parses_with_data % 100 == 0;
|
|| self.parses_with_data % 100 == 0;
|
||||||
if should_log {
|
if should_log && wzp_codec::dred_verbose_logs() {
|
||||||
info!(
|
info!(
|
||||||
seq,
|
seq,
|
||||||
samples_available = available,
|
samples_available = available,
|
||||||
@@ -225,21 +225,22 @@ impl DredRecvState {
|
|||||||
match reconstructed {
|
match reconstructed {
|
||||||
Some(_n) => {
|
Some(_n) => {
|
||||||
self.dred_reconstructions += 1;
|
self.dred_reconstructions += 1;
|
||||||
// Log every DRED reconstruction. These are
|
// Log every DRED reconstruction (gated behind
|
||||||
// rare events on a clean network — when
|
// the GUI verbose-logs toggle). When enabled,
|
||||||
// they fire, we want to know exactly which
|
// we want to know exactly which gap was
|
||||||
// gap was filled and how the offset math
|
// filled and how the offset math played out.
|
||||||
// played out. Acceptable to be chatty here.
|
if wzp_codec::dred_verbose_logs() {
|
||||||
info!(
|
info!(
|
||||||
missing_seq,
|
missing_seq,
|
||||||
anchor_seq = ?self.last_good_seq,
|
anchor_seq = ?self.last_good_seq,
|
||||||
offset_samples,
|
offset_samples,
|
||||||
offset_ms = offset_samples / 48,
|
offset_ms = offset_samples / 48,
|
||||||
samples_available = available,
|
samples_available = available,
|
||||||
gap_size = gap,
|
gap_size = gap,
|
||||||
total_dred_recoveries = self.dred_reconstructions,
|
total_dred_recoveries = self.dred_reconstructions,
|
||||||
"DRED reconstruction fired for missing frame"
|
"DRED reconstruction fired for missing frame"
|
||||||
);
|
);
|
||||||
|
}
|
||||||
emit(out);
|
emit(out);
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
@@ -251,8 +252,9 @@ impl DredRecvState {
|
|||||||
// is whichever check failed in the if
|
// is whichever check failed in the if
|
||||||
// above (offset out of range, no good
|
// above (offset out of range, no good
|
||||||
// state, or reconstruct error).
|
// state, or reconstruct error).
|
||||||
if self.classical_plc_invocations <= 3
|
if (self.classical_plc_invocations <= 3
|
||||||
|| self.classical_plc_invocations % 50 == 0
|
|| self.classical_plc_invocations % 50 == 0)
|
||||||
|
&& wzp_codec::dred_verbose_logs()
|
||||||
{
|
{
|
||||||
info!(
|
info!(
|
||||||
missing_seq,
|
missing_seq,
|
||||||
@@ -695,20 +697,33 @@ impl CallEngine {
|
|||||||
// Heartbeat every 2s with decode+playout state
|
// Heartbeat every 2s with decode+playout state
|
||||||
if heartbeat.elapsed() >= std::time::Duration::from_secs(2) {
|
if heartbeat.elapsed() >= std::time::Duration::from_secs(2) {
|
||||||
let fr = recv_fr.load(Ordering::Relaxed);
|
let fr = recv_fr.load(Ordering::Relaxed);
|
||||||
info!(
|
if wzp_codec::dred_verbose_logs() {
|
||||||
recv_fr = fr,
|
info!(
|
||||||
decoded_frames,
|
recv_fr = fr,
|
||||||
last_decode_n,
|
decoded_frames,
|
||||||
last_written,
|
last_decode_n,
|
||||||
written_samples,
|
last_written,
|
||||||
decode_errs,
|
written_samples,
|
||||||
codec = ?current_codec,
|
decode_errs,
|
||||||
dred_recv = dred_recv.dred_reconstructions,
|
codec = ?current_codec,
|
||||||
classical_plc = dred_recv.classical_plc_invocations,
|
dred_recv = dred_recv.dred_reconstructions,
|
||||||
dred_parses_with_data = dred_recv.parses_with_data,
|
classical_plc = dred_recv.classical_plc_invocations,
|
||||||
dred_parses_total = dred_recv.parses_total,
|
dred_parses_with_data = dred_recv.parses_with_data,
|
||||||
"recv heartbeat (android)"
|
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();
|
heartbeat = std::time::Instant::now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,6 +110,22 @@ struct PingResult {
|
|||||||
server_fingerprint: String,
|
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.
|
/// Ping a relay to check if it's online, measure RTT, and get server identity.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn ping_relay(relay: String) -> Result<PingResult, String> {
|
async fn ping_relay(relay: String) -> Result<PingResult, String> {
|
||||||
@@ -687,6 +703,7 @@ pub fn run() {
|
|||||||
deregister,
|
deregister,
|
||||||
set_speakerphone, is_speakerphone_on,
|
set_speakerphone, is_speakerphone_on,
|
||||||
get_call_history, get_recent_contacts, clear_call_history,
|
get_call_history, get_recent_contacts, clear_call_history,
|
||||||
|
set_dred_verbose_logs, get_dred_verbose_logs,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running WarzonePhone");
|
.expect("error while running WarzonePhone");
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ const settingsBtnCall = document.getElementById("settings-btn-call")!;
|
|||||||
const sRoom = document.getElementById("s-room") as HTMLInputElement;
|
const sRoom = document.getElementById("s-room") as HTMLInputElement;
|
||||||
const sAlias = document.getElementById("s-alias") as HTMLInputElement;
|
const sAlias = document.getElementById("s-alias") as HTMLInputElement;
|
||||||
const sOsAec = document.getElementById("s-os-aec") 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 sAgc = document.getElementById("s-agc") as HTMLInputElement;
|
||||||
const sQuality = document.getElementById("s-quality") as HTMLInputElement;
|
const sQuality = document.getElementById("s-quality") as HTMLInputElement;
|
||||||
const sQualityLabel = document.getElementById("s-quality-label")!;
|
const sQualityLabel = document.getElementById("s-quality-label")!;
|
||||||
@@ -140,6 +141,10 @@ interface Settings {
|
|||||||
agc: boolean;
|
agc: boolean;
|
||||||
quality: string;
|
quality: string;
|
||||||
recentRooms: RecentRoom[];
|
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 {
|
function loadSettings(): Settings {
|
||||||
@@ -152,6 +157,7 @@ function loadSettings(): Settings {
|
|||||||
],
|
],
|
||||||
selectedRelay: 0, room: "general", alias: "",
|
selectedRelay: 0, room: "general", alias: "",
|
||||||
osAec: true, agc: true, quality: "auto", recentRooms: [],
|
osAec: true, agc: true, quality: "auto", recentRooms: [],
|
||||||
|
dredDebugLogs: false,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem("wzp-settings");
|
const raw = localStorage.getItem("wzp-settings");
|
||||||
@@ -402,6 +408,10 @@ function renderRecentRooms(rooms: RecentRoom[]) {
|
|||||||
// ── Init ──
|
// ── Init ──
|
||||||
applySettings();
|
applySettings();
|
||||||
setTimeout(pingAllRelays, 300);
|
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
|
// Load fingerprint + alias + git hash + render identicon
|
||||||
interface AppInfo { git_hash: string; alias: string; fingerprint: string; data_dir: string }
|
interface AppInfo { git_hash: string; alias: string; fingerprint: string; data_dir: string }
|
||||||
@@ -714,6 +724,7 @@ listen("call-event", (event: any) => {
|
|||||||
function openSettings() {
|
function openSettings() {
|
||||||
const s = loadSettings();
|
const s = loadSettings();
|
||||||
sRoom.value = s.room; sAlias.value = s.alias; sOsAec.checked = s.osAec;
|
sRoom.value = s.room; sAlias.value = s.alias; sOsAec.checked = s.osAec;
|
||||||
|
sDredDebug.checked = !!s.dredDebugLogs;
|
||||||
const qi = qualityToIndex(s.quality || "auto");
|
const qi = qualityToIndex(s.quality || "auto");
|
||||||
sQuality.value = String(qi);
|
sQuality.value = String(qi);
|
||||||
updateQualityUI(qi);
|
updateQualityUI(qi);
|
||||||
@@ -753,7 +764,11 @@ settingsSave.addEventListener("click", () => {
|
|||||||
const s = loadSettings();
|
const s = loadSettings();
|
||||||
s.room = sRoom.value; s.alias = sAlias.value; s.osAec = sOsAec.checked;
|
s.room = sRoom.value; s.alias = sAlias.value; s.osAec = sOsAec.checked;
|
||||||
s.quality = QUALITY_STEPS[parseInt(sQuality.value)] || "auto";
|
s.quality = QUALITY_STEPS[parseInt(sQuality.value)] || "auto";
|
||||||
|
s.dredDebugLogs = sDredDebug.checked;
|
||||||
saveSettingsObj(s);
|
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;
|
roomInput.value = s.room; aliasInput.value = s.alias; osAecCheckbox.checked = s.osAec;
|
||||||
renderRecentRooms(s.recentRooms);
|
renderRecentRooms(s.recentRooms);
|
||||||
closeSettings();
|
closeSettings();
|
||||||
|
|||||||
Reference in New Issue
Block a user