Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions docs/AUDIO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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
Expand Down Expand Up @@ -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.*
Expand Down
11 changes: 9 additions & 2 deletions native/build-android.sh
Original file line number Diff line number Diff line change
Expand Up @@ -331,19 +331,26 @@ 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"
}

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...]
Expand Down
116 changes: 116 additions & 0 deletions tools/aaudio_tone.c
Original file line number Diff line number Diff line change
@@ -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 <aaudio/AAudio.h>
#include <math.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>

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;
}
Loading