e32bd30c858eea72f7e2fe6fefdcb762a794b3ed
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b5f91d50bb |
feat(android): tier-A pad audio — the 0xD1 plane on the pad's USB endpoint (WP9)
The Android twin of `pf-client-core`'s pad_audio: drain the host's per-pad DualSense streams, Opus-decode haptics (kind 0) and speaker (kind 1), interleave into the pad's own 4-channel layout, and render on the pad itself. Every other client hands that stream to the platform's audio graph. Android cannot: AOSP's UsbAlsaManager denylists the DualSense's output by VID/PID, so the kernel enumerates the pad's playback node and the framework discards it — `hasOutput: false`, nothing for setPreferredDevice to target, /dev/snd closed by SELinux, and UsbRequest rejects non-bulk/interrupt endpoints. So this drives the pad's isochronous endpoint directly via uac-host on the descriptor Java owns. That is measured, not assumed. On a Nothing Phone (3): the claim succeeds unprivileged, the gamepad and the pad's microphone both keep working, and the underrun-free floor is 4 ms — holding under eight-core load with the SoC in severe thermal throttling. The renderer runs at 6 ms, one step of headroom, because the same measurement found transient events that are not depth-dependent. Structured to the crate's own convention: the mixer and PLC are ungated so they compile and unit-test in the host workspace (8 tests), while everything touching an Android-only dependency is cfg'd to android. Two details worth review: - The kinds arrive on different cadences (5 ms vs 10 ms), so each has its own write cursor and both shift together on overflow — a haptics-only session renders with a silent speaker pair instead of stalling on a kind that will never arrive, and the two can never skew. - An unrecognised kind is dropped rather than folded into the coil pair. A `min(1)` clamp would have rendered a future kind straight into the actuators. Lifecycle mirrors MicCapture: dropping the handle joins the thread, and nativeStopPadAudio returns only once it has, so Kotlin may close the UsbDeviceConnection as soon as it returns and not before. usbfs-iso/uac-host enter as git dependencies pinned by revision — a transport under a real-time deadline should move when we choose. They become version dependencies once published to crates.io. |
||
|
|
253e0bbe7c |
feat(android): the mic has an off switch you can reach mid-stream
Wave 1 gave the Android client a mic worth using. It gave it no way to stop talking: leaving the stream, or digging through Settings to turn the whole feature off, were the only ways to stop the room being heard. Now a tap (or Select + Y on a pad) mutes it, and the screen says so while it lasts. How muting gates the capture, and why that way. The AAudio input stream is never stopped: a stop/start would re-run the input-preset fallback ladder and re-prime the buffers on every toggle — hundreds of milliseconds, and possibly a landing on a different rung, silently losing the HAL echo canceller wave 1 went to some trouble to get. Instead the encode loop reads an AtomicBool per 10 ms frame and, while it is set, drains the frame out of its ring and drops it there — the last point before it would have become an Opus packet. Nothing is encoded, nothing is sent, and the realtime capture callback is untouched, so its allocation-free discipline and the queue policy stay exactly as wave 1 verified them. A toggle costs one atomic store and takes effect on the next 10 ms boundary. The frame counter keeps advancing across a mute, because it numbers the captured 10 ms TIMELINE rather than the datagrams. The gap the host then sees is exactly the audio that never came: its de-jitter conceals at most a few frames of it before the pump's 600 ms stale-gap flush resets the chain outright, which is the right reading of a mute. Encoding silence instead would have kept a pointless uplink and a host-side ring alive for its whole duration. Mute is per session and nothing is persisted — a new stream always starts unmuted, and no new setting exists. The flag lives on the session handle rather than on the capture, so the mic stop/start a surface recreate performs brings the user's choice back with it, with no window in which the fresh capture could send an unmuted frame. The control is offered on the evidence that a capture is actually running (nativeMicActive), not on the setting: with the mic disabled, RECORD_AUDIO denied, or every AAudio input rung refused, there is nothing on screen to lie about. On touch it is a pill in the corner the stats HUD doesn't use — the one in-stream control, so it sits above the gesture layer to take its own taps — dim while live, a red "Muted" badge while it isn't. On TV that badge is the indicator alone: Select + Y is the control there, and a focusable button would fight the game for the D-pad. Y is deliberately not one of the exit chord's buttons, so neither chord can be reached through the other. One honest consequence of keeping the stream open: the platform's recording indicator stays lit while muted, because the mic really is still open. What stops is the encode and the send. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
02f7cfbb1d |
feat(client/android): the speed test, writing where the tested host actually reads
Android had no speed test at all — the one client where "what bitrate should I use?" had no answer but guessing. It measures over the REAL data plane: a minimal 720p connect, then the host bursts filler for two seconds, so the answer is about the link this host's stream will take rather than generic throughput. Two new JNI calls (`nativeSpeedTest` / `nativeProbeResult`) front the core's probe, deliberately measure-only. The measurement is the easy half. The half that was wrong on every client for a long time is WHERE the answer goes. A measured bitrate belongs in the layer the tested host actually resolves bitrate from (design §5.3): its bound profile's override if it has one, the global if the host is unbound — and if the host is bound to a profile that INHERITS bitrate, both are defensible, so the user gets both buttons instead of us guessing. That target depends only on the host, so it is known before the result lands and the button can say where it will write: "Apply to “Travel”". Writing the global unconditionally — the old behaviour everywhere — is what made measuring the slow box downstairs quietly re-tune the desktop. Reachable from the host card's overflow and from the console's host options: a TV box on a powerline adapter is exactly the machine whose link is worth measuring, even though profile editing stays off that surface. While there: a successful write no longer renders in the error container. The connect screen's one status line was red by design — correct for a failure, a small lie for "75 Mbit/s set in “Travel”" — so confirmations got their own. Verified on the emulator against a real host: 108 Mbit/s measured on a host bound to a bitrate-setting profile, target resolved to that profile, and Apply wrote 75397 kbps into the profile's overlay with the global untouched and the profile's other overrides unmoved. |
||
|
|
c90343c22f |
feat(android): shared clipboard (text) — device↔host sync while streaming
ci / web (push) Successful in 1m4s
ci / docs-site (push) Successful in 1m13s
apple / swift (push) Successful in 1m21s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
decky / build-publish (push) Successful in 33s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
ci / bench (push) Successful in 8m18s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 6m49s
deb / build-publish (push) Successful in 10m26s
release / apple (push) Successful in 9m22s
docker / deploy-docs (push) Successful in 28s
deb / build-publish-host (push) Successful in 12m44s
arch / build-publish (push) Successful in 15m51s
windows-host / package (push) Successful in 16m14s
apple / screenshots (push) Successful in 6m37s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m6s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m20s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m18s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m39s
ci / rust (push) Successful in 24m48s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m31s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m21s
android / android (push) Successful in 11m30s
flatpak / build-publish (push) Successful in 6m16s
The desktop clients' clipboard protocol, adopted on Android (text-only v1): opt-in ClipControl at stream start when the host advertises HOST_CAP_CLIPBOARD and the new "Shared clipboard" setting (default on) allows. Device → host: local copies (primary-clip listener + a probe at start) are announced as lazy format-list offers; the text crosses only when the host actually pastes (FetchRequest → served from the live clipboard). Host → device: a host copy's offer is fetched eagerly and lands in the system clipboard (Android has no practical lazy-paste provider), with an echo guard so the resulting clip-changed callback doesn't bounce it back as a new offer. Native side: session/clipboard.rs JNI shims over NativeClient's clip_* surface; events cross to Kotlin as compact strings from a blocking nativeNextClip poll on a dedicated thread (the nativeNextRumble pattern), joined before the session handle is freed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c890de49c9 |
refactor(android): split session JNI into modules, HUD-gated stats, AAudio open retry
- native: the 756-line session.rs becomes session/{mod,connect,input,planes}.rs
around a SessionHandle (connect lifecycle + trust, input plane shims, plane
start/stop + stats drain).
- Decode-stats sampling is HUD-gated (nativeSetVideoStatsEnabled): with the
overlay hidden the decode thread skips the per-AU clock read + lock; enabling
resets the measurement window.
- audio: the AAudio open path is a per-sharing-mode try_open closure — the
realtime callback state (ring, prime, free-list) is rebuilt per attempt, so a
failed exclusive-mode try can't leak state into the shared-mode retry.
- Kotlin: ConnectScreen/StreamScreen slimmed by extracting ConnectDialogs,
StatsOverlay and TouchInput.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|