Files
chanora/poc/audio-capture-playback-android-spike/VERIFICATION.md
T
EdisonJwa ec21a880d2 feat(poc/audio): add Android mobile audio spike
Closes the mobile half of the PoC plan §2 audio exit criterion
left open by poc/audio-capture-playback-spike. The desktop and
mobile halves together fully retire the audio PoC.

Stack:
  Kotlin (MainActivity) → JNI → Rust cdylib
    → cpal 0.16 → Oboe (AAudio / OpenSL ES) → Android audio HAL

Layout:
  rust crate (src/lib.rs)   — JNI_OnLoad, initContext,
                              playSine440, record1sToFile;
                              panic-catching at JNI boundary;
                              android_logger → logcat
  android/ (Gradle 8.7,     — minSdk 24, compileSdk 34, AGP 8.5.2.
   AGP 8.5.2, Kotlin 1.9.24)  cargoBuildRust task wraps cargo-ndk
                              -P 26 -t <abi> for all four ABIs;
                              wired into preBuild so AGP picks up
                              the produced .so files.

Verified on 2026-05-13 on a physical Motorola Moto G Stylus 5G
(2023), Android 14 SDK 34 arm64-v8a:
  - Playback: 500 ms 440 Hz mono sine, 22,050 frames emitted at
    44.1 kHz through cpal/Oboe/AAudio/device speaker.
  - Capture: 1 s from default input, 42,624 frames written to
    /data/data/app.chanora.poc.audio/files/chanora_poc_capture.wav.
    File pulled via 'adb exec-out run-as ... cat' and confirmed
    by file(1) as 'RIFF (little-endian) data, WAVE audio,
    Microsoft PCM, 16 bit, mono 44100 Hz'. Header bytes
    cross-checked against the reported frame count.

Notes:
  - cpal links libaaudio (introduced API 26), so cargo-ndk targets
    API 26 via -P 26 while the Android module's minSdk stays at 24
    (DEC-004). API 24/25 devices would fall back to OpenSL ES at
    runtime; not exercised here.
  - JNI panic safety: every JNI entry point wraps its body in
    std::panic::catch_unwind and a tracing panic hook routes
    panic messages to logcat under tag 'ChanoraAudioPoC'. Without
    this, cpal panicking inside an extern "system" function would
    abort the process.
  - The emulator AVD chanora-poc-api34 and its system image were
    installed during Phase 0 but emulator verification was skipped
    once the physical-device run succeeded. Real-device evidence
    is stronger.

Surfaced finding: DEC-011.1 mobile half promoted from Deferred to
Accepted for Android in the same docs commit; iOS remains
explicitly Deferred (requires macOS + Xcode hardware).

Authority: PoC plan §2, DEC-011, DEC-011.1.
Not product code; not promoted into chanora_audio.
2026-05-14 18:24:53 +08:00

155 lines
5.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Verification record — `audio-capture-playback-android-spike`
## Result
PASS. The mobile half of the PoC plan §2 audio exit criterion
("at least one mobile target") is met on a physical Android device.
This combined with the desktop half (closed by
`poc/audio-capture-playback-spike` on Linux + PipeWire on 2026-05-13)
fully retires the audio PoC.
## Environment
| Field | Value |
|---|---|
| Date | 2026-05-13 |
| Build host OS | Linux (Arch, kernel 7.0.5-arch1-1, x86_64) |
| Rust toolchain | stable 1.95.0 (59807616e 2026-04-14) |
| Android SDK | platform 34, build-tools 34.0.0, platform-tools |
| Android NDK | r26.3.11579264 |
| `cargo-ndk` | 4.1.2 |
| AGP | 8.5.2 |
| Gradle | 8.7 |
| Kotlin | 1.9.24 |
| `cpal` | 0.16 |
| `hound` | 3.5 |
| `jni` | 0.21 |
| `ndk-context` | 0.1.1 |
| `android_logger` | 0.14 |
| Test device | Motorola Moto G Stylus 5G (2023), Android 14 (SDK 34), arm64-v8a (transport_id ZD222DQHFY) |
The Android x86_64 emulator AVD `chanora-poc-api34` was created and
the system image installed, but emulator verification was **skipped
intentionally** once the physical-device run succeeded; the physical
device is stronger evidence than emulator audio HAL.
## Reproduction
```bash
cd poc/audio-capture-playback-android-spike/android
./gradlew :app:assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk
adb shell pm grant app.chanora.poc.audio android.permission.RECORD_AUDIO
adb shell am start -n app.chanora.poc.audio/.MainActivity
adb shell input tap 540 520 # Play
adb shell input tap 540 640 # Record
```
## Observed result
### Playback (PLAY 500 MS SINE)
logcat:
```
ChanoraPoCActivity: playing…
ChanoraAudioPoC: audio_capture_playback_android_spike: playback device='default'
sample_rate=44100 channels=2 fmt=F32
ChanoraAudioPoC: audio_capture_playback_android_spike: playSine440 ok n=22050
ChanoraPoCActivity: PLAY OK frames=22050
```
UI status text after tap: `"PLAY OK frames=22050"`.
22,050 frames at 44.1 kHz mono equals exactly 500 ms — the requested
duration. The full Rust → cpal → Oboe → AAudio → speaker path
completed without error.
### Capture (RECORD 1 S TO FILE)
logcat:
```
ChanoraPoCActivity: recording 1 s…
ChanoraAudioPoC: audio_capture_playback_android_spike: capture device='default'
sample_rate=44100 channels=2 fmt=F32
ChanoraAudioPoC: audio_capture_playback_android_spike: record1sToFile ok n=42624
ChanoraPoCActivity: RECORD OK frames=42624 bytes=85292
path=/data/user/0/app.chanora.poc.audio/files/chanora_poc_capture.wav
```
UI status text after tap: `"RECORD OK frames=42624 bytes=85292
path=/data/user/0/app.chanora.poc.audio/files/chanora_poc_capture.wav"`.
42,624 frames at 44.1 kHz is approximately 966 ms — slightly under
the requested 1 s because `std::thread::sleep` and the cpal callback
boundary do not align perfectly. The full Rust → cpal → Oboe → AAudio
→ microphone path completed without error.
### File integrity
The WAV file was pulled with:
```bash
adb exec-out run-as app.chanora.poc.audio cat files/chanora_poc_capture.wav \
> /tmp/chanora_capture.wav
```
`file(1)` output:
```
/tmp/chanora_capture.wav: RIFF (little-endian) data, WAVE audio,
Microsoft PCM, 16 bit, mono 44100 Hz
```
Header bytes confirm:
```
00000000: 5249 4646 244d 0100 5741 5645 666d 7420 RIFF$M..WAVEfmt
00000010: 1000 0000 0100 0100 44ac 0000 8858 0100 ........D....X..
00000020: 0200 1000 6461 7461 004d 0100 0000 0000 ....data.M......
```
- `RIFF` magic, size 85,284 + 8.
- `WAVE` format, fmt chunk: PCM (0x0001), mono (0x0001), 44100 Hz
(0x0000AC44), 88200 bytes/s, block-align 2, 16 bits/sample.
- `data` chunk size 0x00014D00 = 85,248 bytes of PCM ≈ 42,624 i16
samples — matches the reported frame count.
## Coverage matrix
| PoC plan exit criterion | Status | Evidence |
|---|---|---|
| Capture/playback works on at least one **desktop** target | PASS | `poc/audio-capture-playback-spike/VERIFICATION.md` |
| Capture/playback works on at least one **mobile** target | **PASS** | This document. |
## What this spike does NOT validate
- iOS (deferred per DEC-011.1; requires macOS + Xcode hardware).
- Android emulator audio path (skipped — physical device is
stronger evidence).
- API 2425 devices (cpal's OpenSL ES fallback path is not exercised
here; only AAudio via API 26+).
- Bit-exact loopback signal integrity from capture to playback.
- Audio latency under load.
- Streaming reconfiguration on device change or audio focus loss.
- Background-service / foreground-service audio (required for a real
voice client; out of scope here).
- DSP, Opus encode/decode, jitter buffer, mixer — owned by
`chanora_audio`.
- Privacy-sensitive UX (microphone consent flows beyond a single
runtime grant).
## Decisions closed by this spike
- **DEC-011.1** sub-decision is promoted from
`Accepted (desktop) / Deferred (mobile)` to
`Accepted (desktop: cpal; Android: cpal-on-Oboe) / Deferred (iOS)`.
See `docs/governance/product-decision-register.md` v0.9.4.
- The audio row in
`docs/architecture/proof-of-concept-plan.md` §2 is promoted from
PARTIAL PASS to PASS.
- RISK-PoC-001 in `docs/governance/poc-results-summary.md` is closed
for Android; iOS portion remains open.