fix(client/android): place audio with the picture on Android too
The core, Linux, Windows and host halves of the audio latency overhaul landed with Android deliberately left inert: `JitterPolicy`'s sync target defaults to `None`, so this ring kept behaving exactly as it always had. What was missing was not the loop but its REFERENCE — nothing here published where a frame actually reached glass, and a controller with no reference is the mechanism you can prove is present but that cannot act. This wires both halves. The decode thread now reads the host capture `pts_ns` that every `AudioPacket` has always carried and that this client, like every other, dropped on the floor. Against the ring depth (published by the AAudio callback through the shared `AudioSyncCell`) and the video plane's end-to-end figure it computes audio_e2e = (now + buffered_ahead + clock_offset) − pts_ns av_offset = audio_e2e − video_e2e (> 0 ⇒ audio behind the picture) and asks the ring for a depth that closes it. Only ASKS: `set_sync_target` is clamped between the underrun-driven adaptive floor and the hard cap, so a link whose jitter genuinely needs more buffer than the picture is away keeps its buffer and the residual is reported instead of being taken out of the listener's stream. Continuity outranks sync, on this ring as on the others. The reference comes from `DisplayTracker`'s `OnFrameRendered` callback — the one place in the client that knows a frame truly latched — and it is computed ABOVE the HUD gate now. A sync loop that only ran while the overlay was up would be off on exactly the devices that report latency; the stats LOCK stays gated, which is what that early-return was really protecting. Both decode loops feed it, so sync works with "Low-latency mode" off as well. Two deliberate refusals: * The figure is published RAW. The HUD shaves the OS present floor off its shown display/end-to-end numbers — metrics report what Punktfunk controls — but sound has to reach the ear when the light reaches the eye, and a floor-shaved reference would place audio a whole latch period early on every device. * Below API 33 there is no render callback, so there is no confirmed present and the loop stays inert (target `None` ⇒ today's behaviour exactly). The release instant is NOT substituted for it: a release targets a FUTURE vsync and runs a whole latch period (8-21 ms measured) ahead of glass, well outside the loop's deadband — it would place audio early on every frame while looking like it was working. The plane is also no longer invisible. Ring depth and the smoothed offset ride the stats array at 33/34 and the Detailed HUD carries `audio buffer N ms · a/v ±N ms`, the same wording the desktop HUD uses — both numbers, because a deep ring on a jittery link is correct behaviour and only the offset separates that from audio simply held late. The 1 Hz logcat line gains `av_ms` beside its depth, and the depth itself now has ONE publisher: the counter copy is gone in favour of the sync cell both readers already share. The escape hatch is two levers. `PUNKTFUNK_NO_AV_SYNC=1` keeps the contract the desktop clients document, but an app launched from the launcher inherits no environment, so the one a field tester can actually reach is `adb shell setprop debug.punktfunk.no_av_sync 1` — no rebuild, exactly like `debug.punktfunk.presenter`. A loop that steers playback has to be bisectable on the device that reports the regression. Verified: `cargo ndk -t arm64-v8a check` clean; `cargo clippy -p punktfunk-client-android --all-targets -- -D warnings` clean on the host lane CI lints, and the Android target introduces no new findings (5 pre-existing lints in audio/mic/pad_audio/vsync are unchanged — the android-gated modules are never linted by the host workspace); `cargo fmt --all --check` clean; `./gradlew :app:testDebugUnitTest` green. The new HUD test was proven non-vacuous by planting the defect first — dropping the render call fails its three positive assertions and leaves the three absence assertions passing, which is the shape a test that "passes for the wrong reason" would not have. design/audio-latency-overhaul.md W4. Apple (W6) still keeps today's behaviour.
This commit is contained in:
@@ -73,6 +73,7 @@ e2e 14.2/19.8 ms (p50/p95) · host 3.1 · net 6.7 · decode 2.1 · display 2.3 m
|
||||
host: queue 0.6 · encode 1.8 · xfer 0.2 · pace 0.5 ms
|
||||
present: mailbox
|
||||
lost 3 (2.4%)
|
||||
audio buffer 28 ms · a/v +4 ms
|
||||
```
|
||||
|
||||
Android (headline and `display` both floor-shaved, like the Apple clients — the raw
|
||||
@@ -85,6 +86,7 @@ HEVC · 10-bit · HDR (BT.2020 PQ) · 4:2:0
|
||||
end-to-end 14.2 ms p50 · 19.8 p95 · capture→displayed
|
||||
= host 3.1 + network 6.7 + decode 2.1 + display 2.3 · presents 119
|
||||
os present +16.7 excluded (display pipeline minimum)
|
||||
audio buffer 28 ms · a/v +4 ms
|
||||
lost 3 (2.4%) · skipped 1 · FEC 12
|
||||
```
|
||||
|
||||
@@ -185,6 +187,16 @@ lost 3 (2.4%)
|
||||
(frames your client chose not to display because a newer one had already arrived) and
|
||||
`FEC` (packet shards the error correction recovered this second — loss you *didn't*
|
||||
feel) are reported by the **Android client only**; the other clients show `lost` alone.
|
||||
- **The audio line** — Detailed only, on Linux · Windows · Steam Deck · Android, and shown
|
||||
once sound is actually playing. `audio buffer` is how much decoded audio is queued ahead
|
||||
of your speakers; `a/v` is where that *puts* it relative to the picture — **positive means
|
||||
audio is playing behind the picture**, negative means ahead of it. The client steers the
|
||||
buffer to drive `a/v` toward zero, but never below the depth your link's jitter needs, so
|
||||
on a rough connection you may see the buffer hold and a small `a/v` remain: that is the
|
||||
client choosing an unbroken stream over perfect lip-sync, and it is the honest reading
|
||||
rather than a hidden compromise. The `a/v` term is omitted when it is zero — aligned, or
|
||||
not yet measured (it needs a frame on screen to compare against, and a few seconds to
|
||||
settle). The Apple clients do not report it yet.
|
||||
|
||||
All values refresh once per second over the last second of frames.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user