Build 4c6aac6 added a stop+sleep+start Oboe restart inside the
set_speakerphone Tauri command, but calling wzp_native::audio_stop()
and audio_start() synchronously from an async fn blocks the tokio
executor thread — those FFI calls wait for AAudio to finalise the
stream teardown/bringup, which takes ~400ms each on Nothing phone
(Pixel is fast enough to hide the bug).
Reproduced on Nothing: 7 rapid Speaker button clicks across ~30
seconds, each restarting Oboe. After the 5th click the engine send
and recv tokio tasks froze for 22 seconds — decoded_frames stuck at
1159 across 9 heartbeats, send_drops growing from 148 to 1720 as
encoded frames couldn't make it past `send_t.send_media(pkt).await`.
At 08:40:48 the runtime finally caught up and processed a 911-frame
burst at once (buffered QUIC datagrams flooding through). Classic
"blocking sync call in async context" anti-pattern.
Fix: run the stop + start sequence inside tokio::task::spawn_blocking
so the Oboe teardown + reopen happens on a dedicated blocking thread,
leaving the tokio runtime free to keep driving the send and recv
tasks. AAudio's requestStop returns only after the stream is actually
in Stopped state, so the explicit sleep that bridged stop and start
is no longer needed and is dropped.
Send and recv tasks still see a ~500ms window of empty reads /
partial writes during the blocking restart, but they get SCHEDULED
through it — network packets keep being received + decoded + dropped
into the playout ring, and captured mic samples keep being encoded +
sent through quinn. No more executor starvation, no more 22-second
audio dropouts, no more send_drops burst.
Pixel still worked before this fix only because its AAudio teardown
is fast enough to not exceed the scheduler's cooperative yield
interval — same bug was latent on both devices, Nothing just made it
visible.