feat(android): host→client audio — Opus → AAudio (LowLatency)
M4 Android stage 1 (audio). An audio thread pulls Opus packets from the connector (next_audio), decodes to interleaved f32 stereo, and feeds AAudio via its realtime data callback through a jitter ring ported from the Linux client (prime ~3 quanta, drop-oldest cap, re-prime on drain). All in Rust on native threads — symmetric with the video decode path. - crates/punktfunk-android: audio.rs (Opus decode + jitter ring + AAudio callback); SessionHandle gains an audio slot; nativeStartAudio/nativeStopAudio JNI; Drop stops it. Android-only deps: opus 0.3 (libopus via cmake, static) + ndk "audio" (AAudio) — pure C/NDK, no libc++_shared to bundle. - clients/android: NativeBridge start/stop audio, called in the SurfaceView lifecycle. - kit/build.gradle.kts: cargo-ndk env for the libopus cmake build (NDK root, Ninja, LIBOPUS_STATIC/NO_PKG) + --platform 31 (libaaudio is API 26+). Verified live (emulator -> gamescope host on the LAN box): AAudio opened 48k/stereo/f32; a 440 Hz tone played into the host capture sink reached the client decoded -- opus ~200/s, pcm_frames climbing in lockstep, peak=0.089 (real content, not silence), with video streaming concurrently. Some underruns under emulator jitter (verify on hardware). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,14 +56,29 @@ fun registerCargoNdk(taskName: String, release: Boolean) =
|
||||
description = "cargo-ndk build of punktfunk-android (${if (release) "release" else "debug"})"
|
||||
workingDir = repoRoot
|
||||
val sdk = androidSdkDir()
|
||||
// A GUI Android Studio launch does not source the login shell, so make cargo + the NDK
|
||||
// discoverable explicitly (works the same from a bare CLI).
|
||||
environment("PATH", cargoBin + File.pathSeparator + System.getenv("PATH"))
|
||||
// A GUI Android Studio launch does not source the login shell, so make cargo, the NDK, and
|
||||
// cmake (libopus builds via the cmake crate) discoverable explicitly — same as a bare CLI.
|
||||
val cmakeBin = "$sdk/cmake/3.22.1/bin"
|
||||
environment(
|
||||
"PATH",
|
||||
cargoBin + File.pathSeparator + cmakeBin + File.pathSeparator + System.getenv("PATH"),
|
||||
)
|
||||
environment("ANDROID_HOME", sdk)
|
||||
environment("ANDROID_NDK_HOME", "$sdk/ndk/$ndkVer")
|
||||
// CMake's built-in Android support (used by the cmake crate for libopus) finds the NDK via
|
||||
// these, and uses Ninja (bundled next to the SDK cmake) since there's no `make`.
|
||||
environment("ANDROID_NDK_ROOT", "$sdk/ndk/$ndkVer")
|
||||
environment("ANDROID_NDK", "$sdk/ndk/$ndkVer")
|
||||
environment("CMAKE_GENERATOR", "Ninja")
|
||||
// audiopus_sys picks static-vs-dynamic by HOST not target — force the bundled static libopus
|
||||
// (pure C) so the android .so links it instead of looking for the host's libopus.so.
|
||||
environment("LIBOPUS_STATIC", "1")
|
||||
environment("LIBOPUS_NO_PKG", "1")
|
||||
val cmd = mutableListOf(
|
||||
"cargo", "ndk",
|
||||
"-t", "arm64-v8a", "-t", "x86_64",
|
||||
// Link against the minSdk-31 sysroot so libaaudio (API 26+) is found.
|
||||
"--platform", "31",
|
||||
"-o", file("src/main/jniLibs").absolutePath,
|
||||
"build", "-p", "punktfunk-android",
|
||||
)
|
||||
|
||||
@@ -37,4 +37,13 @@ object NativeBridge {
|
||||
|
||||
/** Stop + join the decode thread without closing the session. No-op on `0`. */
|
||||
external fun nativeStopVideo(handle: Long)
|
||||
|
||||
/**
|
||||
* Start host→client audio: Opus decode → jitter ring → AAudio (LowLatency), all in Rust. No-op
|
||||
* if already started. Best-effort — a failure leaves video streaming.
|
||||
*/
|
||||
external fun nativeStartAudio(handle: Long)
|
||||
|
||||
/** Stop + join the audio thread and close AAudio, without closing the session. No-op on `0`. */
|
||||
external fun nativeStopAudio(handle: Long)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user