From 95f90c787f7287f6e74b34f7220470c5a9eca4cb Mon Sep 17 00:00:00 2001 From: Claudius Date: Sat, 31 Oct 2026 09:21:02 +0000 Subject: [PATCH] Audio M3: build the AAudio driver; step 1 probe and result Bumps third_party/inferno to the AAudio driver branch (Scion-Project/pyre PR 64, audio/aaudio, on top of current pyre master). build-android.sh passes -Daaudio=enabled when the Inferno tree has the option, so a failed detection stops the build instead of shipping without sound. tools/aaudio_tone.c is the step 1 probe: it opens an AAudio stream with the driver's settings and plays 440 Hz. It opened and ran from a plain exec'd process both as the shell user and in the app's uid (run-as) on the API 36 emulator; docs/AUDIO.md records the numbers. No app change: passing -audiodev aaudio is issue 94. --- docs/AUDIO.md | 35 +++++++++--- native/build-android.sh | 11 +++- third_party/inferno | 2 +- tools/aaudio_tone.c | 116 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 154 insertions(+), 10 deletions(-) create mode 100644 tools/aaudio_tone.c diff --git a/docs/AUDIO.md b/docs/AUDIO.md index 68d7371..d081d79 100644 --- a/docs/AUDIO.md +++ b/docs/AUDIO.md @@ -81,7 +81,7 @@ QEMU needs somewhere to send PCM. Options considered: |---|---|---|---| | QEMU code | new audio driver, ~300 lines + QAPI/meson | new audio driver (socket writer), ~200 lines | none | | App code | audio focus only | reader thread + `AudioTrack` + reconnect | reader thread + `AudioTrack` | -| Child-process mode (default) | yes, if AAudio works from the exec'd child (same uid/SELinux domain as the app; to verify) | yes | yes | +| Child-process mode (default) | yes: AAudio opens from a plain exec'd native process (checked, see below) | yes | yes | | In-process mode (`:vm`) | yes, plainly an app process | yes | yes | | Latency | lowest: a callback straight from the audio HAL, MMAP path on Pixel | extra socket + `AudioTrack` buffer | pipe buffer + `AudioTrack` buffer; writes block the QEMU audio timer if the reader stalls | | Copies | QEMU ring → AAudio | QEMU → kernel → app → AudioTrack | same as socket | @@ -92,9 +92,30 @@ because the display was VNC on a unix socket. Since #66 the app can run QEMU in- straight into a `Surface`, with input over JNI, so the "everything over unix sockets" design is now only the child-process path. An in-QEMU AAudio driver works the same way in both modes, keeps PCM inside one process, and gives the lowest latency. The app's only job is to -request audio focus while a VM runs. The one thing to check first (milestone 3, step 1) is that -AAudio opens from the exec'd child process. If it doesn't, the fallback is the socket writer -plus `AudioTrack`, used for the child-process mode only. +request audio focus while a VM runs. The one thing to check first (milestone 3, step 1) was that +AAudio opens from the exec'd child process. If it hadn't, the fallback would have been the socket +writer plus `AudioTrack`, for the child-process mode only. It does open: + +**Step 1 result (2026-10-31).** `tools/aaudio_tone.c` opens an output stream with the same builder +settings as the driver (S16 stereo 48 kHz, low latency, `USAGE_GAME`, data callback) and plays +440 Hz for a few seconds, printing every result code and the frames the callback produced. Built +with NDK r28c for API 31 and run on the API 36 arm64 emulator (`-no-audio`, so checked by result +codes and frame counts, not by ear): + +| Run as | Domain | open / start / stop | State | Frames in 3 s | Xruns | +|---|---|---|---|---|---| +| shell (`adb shell`, uid 2000) | `shell` | all `AAUDIO_OK` | `STARTED` | 136320 | 0 | +| the app's uid (`run-as io.github.dylanneve1.iosonpixel`) | `runas_app` | all `AAUDIO_OK` | `STARTED` | 114240 | 0 | + +Both got 48000 Hz, 2 ch, I16, 960-frame bursts. The emulator granted `PERFORMANCE_MODE_NONE`, as +expected from its HAL. `run-as` isn't quite the `untrusted_app` domain the app's exec'd QEMU child +runs in, but AAudio only needs binder access to audioserver, which both domains have, and the +plainest case (the shell user) works too. The first run in the real child is the issue 94 wiring. + +``` +$NDK/toolchains/llvm/prebuilt/*/bin/aarch64-linux-android31-clang -O2 -o aaudio_tone tools/aaudio_tone.c -laaudio -lm +adb push aaudio_tone /data/local/tmp/ && adb shell /data/local/tmp/aaudio_tone 3 +``` Format: open the stream as S16 stereo 48 kHz, `AAUDIO_PERFORMANCE_MODE_LOW_LATENCY`, `AAUDIO_USAGE_GAME`, data-callback mode reading from a lock-free ring that QEMU's `write` op @@ -140,9 +161,9 @@ Each milestone is one PR (or a few small ones) with its own issue. MCA/DMA config instead of hard-coding cluster 5 / S16 stereo. Check: a system sound (lock click, keyboard click, a ringtone preview) produces non-silent samples in a `wav` capture on the Mac. *Effort: 3–6 days; this is the part with unknowns.* -3. **Inferno: AAudio audio driver** (#93). `audio/aaudio.c`, QAPI `AudiodevDriver` entry, meson - detection of `aaudio/AAudio.h` + `libaaudio`. Step 1: confirm AAudio opens from the - app's child process. Check: tone/WAV playback on the emulator and a Pixel. *Effort: 2–3 days.* +3. **Inferno: AAudio audio driver** (#93, Inferno#64). `audio/aaudio.c`, QAPI `AudiodevDriver` entry, + meson detection of `aaudio/AAudio.h` + `libaaudio`. Step 1, checking that AAudio opens from a + plain exec'd process, passed (see above). Check: tone/WAV playback on the emulator and a Pixel. *Effort: 2–3 days.* 4. **ios-on-pixel: build and app wiring** (#94). Enable the driver in `build-android.sh`, pass `audio=on,audiodev=snd0 -audiodev aaudio,id=snd0` (behind a setting, default on once stable), request audio focus, mute while paused. *Effort: 1 day.* diff --git a/native/build-android.sh b/native/build-android.sh index caa0476..91469a8 100755 --- a/native/build-android.sh +++ b/native/build-android.sh @@ -331,7 +331,7 @@ configure_qemu() { # builddir [extra meson args...] -Dvde=disabled -Dalsa=disabled -Doss=disabled -Dpa=disabled -Dpipewire=disabled \ -Djack=disabled -Dsndio=disabled -Dfuse=disabled -Dmultiprocess=disabled \ -Dvalgrind=disabled -Dlibkeyutils=disabled -Dauth_pam=disabled -Dbrlapi=disabled \ - $(lto_args) "$@" + $(lto_args) $(audio_args) "$@" qemu_config_id "$@" > "$b/.android-config" } @@ -339,11 +339,18 @@ lto_args() { [[ "$QEMU_LTO" == off ]] || echo "-Db_lto=true -Db_lto_mode=$QEMU_LTO" } +# The AAudio driver (-audiodev aaudio, docs/AUDIO.md). meson would pick it up +# on its own from the NDK headers; enabling it makes a failed detection an +# error instead of a build without sound. Older Inferno trees lack the option. +audio_args() { + grep -q "option('aaudio'" "$SRC_QEMU/meson_options.txt" && echo "-Daaudio=enabled" +} + # Everything that decides how QEMU is compiled, so a build tree configured with # other options (tuning, -O level, LTO, PGO) gets reconfigured instead of reused. qemu_config_id() { # [extra meson args...] cat "$WORK/android-arm64-qemu.cross" - echo "$(lto_args) $*" + echo "$(lto_args) $(audio_args) $*" } needs_configure() { # builddir [extra meson args...] diff --git a/third_party/inferno b/third_party/inferno index 30cdd3f..eb2d111 160000 --- a/third_party/inferno +++ b/third_party/inferno @@ -1 +1 @@ -Subproject commit 30cdd3f67fe49b044b6b92f0b61df4b0a5eeaccf +Subproject commit eb2d11158f8b178ccc963551c390561edcef2ac4 diff --git a/tools/aaudio_tone.c b/tools/aaudio_tone.c new file mode 100644 index 0000000..6f844cf --- /dev/null +++ b/tools/aaudio_tone.c @@ -0,0 +1,116 @@ +/* + * aaudio_tone: Scion audio milestone 3, step 1 probe. + * Opens an AAudio output stream the way the planned QEMU driver will + * (S16 stereo 48 kHz, low latency, USAGE_GAME, data callback), plays a + * 440 Hz tone for a few seconds and prints every API result code plus the + * number of frames the callback produced. Run it as a plain exec'd native + * process to check that AAudio works outside an app's Java process. + * + * Build: aarch64-linux-android31-clang -O2 -o aaudio_tone aaudio_tone.c -laaudio -lm + * Usage: aaudio_tone [seconds] + */ +#include +#include +#include +#include +#include +#include +#include + +static atomic_long frames_cb; +static atomic_int cb_calls; +static atomic_int err_code; +static double phase; + +static aaudio_data_callback_result_t data_cb(AAudioStream *s, void *user, + void *data, int32_t n) +{ + int16_t *out = data; + int32_t ch = AAudioStream_getChannelCount(s); + double rate = AAudioStream_getSampleRate(s); + for (int32_t i = 0; i < n; i++) { + int16_t v = (int16_t)(sin(phase) * 8000); + phase += 2 * M_PI * 440.0 / rate; + if (phase > 2 * M_PI) { + phase -= 2 * M_PI; + } + for (int32_t c = 0; c < ch; c++) { + *out++ = v; + } + } + atomic_fetch_add(&frames_cb, n); + atomic_fetch_add(&cb_calls, 1); + return AAUDIO_CALLBACK_RESULT_CONTINUE; +} + +static void error_cb(AAudioStream *s, void *user, aaudio_result_t e) +{ + atomic_store(&err_code, e); +} + +#define CHECK(what, r) \ + printf("%-40s %d (%s)\n", what, (int)(r), AAudio_convertResultToText(r)) + +int main(int argc, char **argv) +{ + int secs = argc > 1 ? atoi(argv[1]) : 3; + AAudioStreamBuilder *b = NULL; + AAudioStream *s = NULL; + aaudio_result_t r; + + printf("uid=%d pid=%d\n", (int)getuid(), (int)getpid()); + r = AAudio_createStreamBuilder(&b); + CHECK("AAudio_createStreamBuilder", r); + if (r != AAUDIO_OK) { + return 1; + } + AAudioStreamBuilder_setDirection(b, AAUDIO_DIRECTION_OUTPUT); + AAudioStreamBuilder_setFormat(b, AAUDIO_FORMAT_PCM_I16); + AAudioStreamBuilder_setChannelCount(b, 2); + AAudioStreamBuilder_setSampleRate(b, 48000); + AAudioStreamBuilder_setPerformanceMode(b, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY); + AAudioStreamBuilder_setSharingMode(b, AAUDIO_SHARING_MODE_SHARED); + AAudioStreamBuilder_setUsage(b, AAUDIO_USAGE_GAME); + AAudioStreamBuilder_setContentType(b, AAUDIO_CONTENT_TYPE_MUSIC); + AAudioStreamBuilder_setDataCallback(b, data_cb, NULL); + AAudioStreamBuilder_setErrorCallback(b, error_cb, NULL); + + r = AAudioStreamBuilder_openStream(b, &s); + CHECK("AAudioStreamBuilder_openStream", r); + AAudioStreamBuilder_delete(b); + if (r != AAUDIO_OK) { + return 2; + } + printf("stream: rate=%d ch=%d fmt=%d perf=%d share=%d burst=%d bufcap=%d\n", + AAudioStream_getSampleRate(s), AAudioStream_getChannelCount(s), + AAudioStream_getFormat(s), AAudioStream_getPerformanceMode(s), + AAudioStream_getSharingMode(s), AAudioStream_getFramesPerBurst(s), + AAudioStream_getBufferCapacityInFrames(s)); + + r = AAudioStream_requestStart(s); + CHECK("AAudioStream_requestStart", r); + aaudio_stream_state_t next = AAUDIO_STREAM_STATE_UNINITIALIZED; + r = AAudioStream_waitForStateChange(s, AAUDIO_STREAM_STATE_STARTING, + &next, 2000000000LL); + CHECK("waitForStateChange(STARTING)", r); + printf("state after start: %s\n", AAudio_convertStreamStateToText(next)); + + for (int i = 0; i < secs; i++) { + sleep(1); + printf("t=%ds frames_cb=%ld calls=%d framesWritten=%lld framesRead=%lld " + "xruns=%d state=%s err=%d\n", + i + 1, (long)atomic_load(&frames_cb), atomic_load(&cb_calls), + (long long)AAudioStream_getFramesWritten(s), + (long long)AAudioStream_getFramesRead(s), + AAudioStream_getXRunCount(s), + AAudio_convertStreamStateToText(AAudioStream_getState(s)), + atomic_load(&err_code)); + } + r = AAudioStream_requestStop(s); + CHECK("AAudioStream_requestStop", r); + r = AAudioStream_close(s); + CHECK("AAudioStream_close", r); + long f = atomic_load(&frames_cb); + printf("RESULT %s frames=%ld\n", f > 0 ? "OK" : "NO_FRAMES", f); + return f > 0 ? 0 : 3; +}