fix(android-audio): Fix D+C — stop+prime cycle on every call start
Addresses the first-join no-audio regression (tasks #35-37) where the Oboe playout callback fires once (cb#0) and then stops draining the ring on the Nothing Phone, causing written_samples to freeze at 7679 (ring capacity minus one burst). Second call (rejoin) always works because audio_stop tears down the streams and audio_start rebuilds them fresh. Two combined fixes: **Fix D (task #37)**: always call audio_stop() before audio_start() at the top of CallEngine::start. On a cold launch this is a no-op (streams not yet started). On subsequent calls it guarantees a clean teardown before rebuild — the same thing rejoin does. Added a 50ms pause between stop and start to let the Android HAL release the audio session. **Fix C (task #36)**: after audio_start(), immediately write 960 samples (20ms) of silence into the playout ring. This ensures the Oboe playout callback has data to drain on its first invocation. On devices where an empty-ring first callback causes the stream to self-pause (Nothing Phone's Qualcomm HAL), the priming data keeps the callback loop alive until real decoded audio arrives from the recv task. Together these cover the two most likely root causes: 1. Stale Oboe state from a previous audio_start that didn't clean up properly → Fix D forces a clean rebuild 2. Playout callback self-pausing on an empty ring → Fix C ensures the ring is non-empty at callback time Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -417,20 +417,47 @@ impl CallEngine {
|
|||||||
"wzp-native not loaded — dlopen failed at startup"
|
"wzp-native not loaded — dlopen failed at startup"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fix D (task #37): explicit stop+start cycle on EVERY call
|
||||||
|
// start — not just rejoin. Empirically, the first call after
|
||||||
|
// app launch on Nothing Phone has the Oboe playout callback
|
||||||
|
// fire once (cb#0) and then stop draining the ring, causing
|
||||||
|
// written_samples to freeze at 7679 (ring capacity minus
|
||||||
|
// one burst). Rejoin (second call) always works because
|
||||||
|
// audio_stop tears down the streams and audio_start rebuilds
|
||||||
|
// them in a state that the audio driver accepts. By always
|
||||||
|
// running stop first (no-op on cold start when not yet
|
||||||
|
// started), we get the same "fresh rebuild" behavior on
|
||||||
|
// every call.
|
||||||
|
crate::wzp_native::audio_stop();
|
||||||
|
// Brief pause to let Android's audio routing + AudioManager
|
||||||
|
// settle after the stop. 50ms is enough for the driver to
|
||||||
|
// release the audio session; shorter risks the new start
|
||||||
|
// hitting a "device busy" on some HALs.
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||||
|
|
||||||
let t_pre_audio = call_t0.elapsed().as_millis();
|
let t_pre_audio = call_t0.elapsed().as_millis();
|
||||||
if let Err(code) = crate::wzp_native::audio_start() {
|
if let Err(code) = crate::wzp_native::audio_start() {
|
||||||
return Err(anyhow::anyhow!("wzp_native_audio_start failed: code {code}"));
|
return Err(anyhow::anyhow!("wzp_native_audio_start failed: code {code}"));
|
||||||
}
|
}
|
||||||
// Diagnostic: how long did audio_start() take, and at what
|
|
||||||
// wall-clock offset from CallEngine::start did it complete?
|
// Fix C (task #36): prime the playout ring with 20ms of
|
||||||
// Compare to the C++ "playout cb#0" log timestamp in logcat to
|
// silence immediately after audio_start so the Oboe playout
|
||||||
// see whether the Oboe playout callback fires before or after
|
// callback has data to drain on its FIRST invocation. On
|
||||||
// the recv task starts pushing decoded frames.
|
// devices where the callback only fires when the ring is
|
||||||
|
// non-empty (or where an empty-ring callback causes the
|
||||||
|
// stream to self-pause), this ensures the callback keeps
|
||||||
|
// running until real decoded audio arrives.
|
||||||
|
{
|
||||||
|
let silence = vec![0i16; 960]; // 20ms @ 48kHz mono
|
||||||
|
let _ = crate::wzp_native::audio_write_playout(&silence);
|
||||||
|
}
|
||||||
|
|
||||||
let t_audio_start_done = call_t0.elapsed().as_millis();
|
let t_audio_start_done = call_t0.elapsed().as_millis();
|
||||||
info!(
|
info!(
|
||||||
t_ms = t_audio_start_done,
|
t_ms = t_audio_start_done,
|
||||||
audio_start_ms = t_audio_start_done.saturating_sub(t_pre_audio),
|
audio_start_ms = t_audio_start_done.saturating_sub(t_pre_audio),
|
||||||
"first-join diag: wzp-native audio started"
|
"first-join diag: wzp-native audio started (with stop+prime cycle)"
|
||||||
);
|
);
|
||||||
|
|
||||||
let running = Arc::new(AtomicBool::new(true));
|
let running = Arc::new(AtomicBool::new(true));
|
||||||
|
|||||||
Reference in New Issue
Block a user