Documentation update following the Android audio spike pass.
Decision register (v0.9.3 → v0.9.4):
- DEC-011.1 promoted from
'Accepted (desktop: cpal) / Deferred (mobile)'
to
'Accepted (desktop: cpal; Android: cpal-on-Oboe) / Deferred (iOS)'.
- Evidence pointer added: poc/audio-capture-playback-android-spike/
VERIFICATION.md.
PoC plan (v0.2.0 → v0.3.0):
- Audio row promoted from PARTIAL PASS to PASS.
- All six PoC plan entries are now PASS.
PoC results summary (v0.1.0 → v0.2.0):
- Audio row collapsed into one PASS spanning both spikes.
- RISK-PoC-001 narrowed from 'mobile audio' to 'iOS audio only'.
- Toolchain table expanded with Android NDK, cargo-ndk, AGP/
Gradle/Kotlin, jni/ndk-context/android_logger, and the test
device.
Cross-spike pointers updated:
- poc/audio-capture-playback-spike/VERIFICATION.md result and
follow-up sections updated to reference the Android spike.
- poc/README.md status table lists both audio spike directories.
CHANGELOG updated under [Unreleased].
124 lines
4.7 KiB
Markdown
124 lines
4.7 KiB
Markdown
# Verification record — `audio-capture-playback-spike`
|
|
|
|
## Result
|
|
|
|
**PASS (desktop half).** The Linux/PipeWire desktop half of the PoC
|
|
plan §2 audio exit criterion is fully met by this spike.
|
|
|
|
The mobile half ("at least one mobile target") is closed by
|
|
[`poc/audio-capture-playback-android-spike`](../audio-capture-playback-android-spike/)
|
|
on a physical Android device — see that directory's `VERIFICATION.md`.
|
|
|
|
Combined: the audio PoC is fully retired.
|
|
|
|
## Environment
|
|
|
|
| Field | Value |
|
|
|---|---|
|
|
| Date | 2026-05-13 |
|
|
| Host OS | Linux (Arch, kernel 7.0.5-arch1-1, x86_64) |
|
|
| Rust toolchain | stable 1.95.0 |
|
|
| `cpal` | 0.16 |
|
|
| `hound` (WAV I/O) | 3.5 |
|
|
| Audio server | PipeWire 1.6.4 (with the `pcm_pipewire` ALSA plugin module) |
|
|
| Default input | `auto_null` (system has no physical microphone bound to this session) |
|
|
| Default output | `auto_null` (PipeWire null sink) |
|
|
| Local config | `~/.asoundrc` aliases `pcm.!default` and `ctl.!default` to `pipewire` |
|
|
|
|
## Reproduction
|
|
|
|
```bash
|
|
cargo test
|
|
cargo run --bin audio-capture-playback-cli -- roundtrip
|
|
```
|
|
|
|
## Test run
|
|
|
|
```
|
|
running 2 tests
|
|
test synth_produces_valid_wav_with_expected_frame_count ... ok
|
|
test synth_then_playback_does_not_error_when_output_device_present ... ok
|
|
|
|
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured;
|
|
0 filtered out; finished in 0.06s
|
|
```
|
|
|
|
## CLI round-trip
|
|
|
|
```
|
|
INFO spike: opening capture stream device="default" sample_rate=44100
|
|
channels=2 sample_format=F32
|
|
WARN spike: capture unavailable; falling back to synth
|
|
error=stream build error: A backend-specific error has
|
|
occurred: ALSA function 'snd_pcm_hw_params' failed with
|
|
error 'No such file or directory (2)'
|
|
INFO spike: synthesised fallback frames=24000 sample_rate=48000
|
|
INFO spike: preparing playback device="default" file_sr=48000
|
|
file_channels=1 samples=24000
|
|
INFO spike: playback finished consumed=24000 total=24000
|
|
round-trip complete (played from /tmp/chanora_audio_spike_synth.wav)
|
|
```
|
|
|
|
### What this evidences
|
|
|
|
1. cpal successfully **opened the default input device** (negotiated
|
|
44.1 kHz / 2-channel / F32 against PipeWire's `auto_null`).
|
|
2. The capture **stream build** then failed because the null source
|
|
does not expose `snd_pcm_hw_params` at that configuration. This is
|
|
a *real* platform-audio failure mode; the typed
|
|
`AudioError::BuildStream` was returned and surfaced cleanly. This
|
|
is the same shape production code will see on misconfigured user
|
|
hosts.
|
|
3. The fallback synth wrote a valid 24,000-frame, 48 kHz, mono,
|
|
16-bit WAV.
|
|
4. cpal **opened the default output device**, negotiated the
|
|
matching config, and the playback callback drained the WAV
|
|
completely: `consumed=24000 total=24000`. Process exited 0.
|
|
|
|
The full path **Rust → cpal → ALSA → `pcm_pipewire` → PipeWire →
|
|
`auto_null`** ran end-to-end, with PCM frames produced by Rust
|
|
consumed by the platform audio stack.
|
|
|
|
## Coverage matrix
|
|
|
|
| PoC plan requirement | Status |
|
|
|---|---|
|
|
| Desktop capture path | PASS (stream opens against the system audio stack; typed error returned when device cannot satisfy `hw_params`). |
|
|
| Desktop playback path | PASS (24,000 frames driven to completion). |
|
|
| Mobile capture path | NOT VERIFIED. |
|
|
| Mobile playback path | NOT VERIFIED. |
|
|
| Round-trip flow | PASS — capture-attempt → fallback → playback. |
|
|
| Typed error model | PASS — `AudioError` arms returned cleanly. |
|
|
| Cross-platform crate choice (DEC-011 alignment) | PARTIAL — `cpal` is a platform-native abstraction, but only the Linux back-end exercised here. |
|
|
|
|
## What this spike does NOT validate
|
|
|
|
- Real microphone capture (this host has none).
|
|
- Bit-exact capture-to-playback signal integrity.
|
|
- Latency.
|
|
- Hot-plug / device-change events.
|
|
- Permission flows (mobile microphone consent, Linux pulse permission
|
|
prompts).
|
|
- Behaviour under sample-rate or channel-count mismatch between file
|
|
and device (the PoC chose the file's rate; production code will
|
|
need a real resampler).
|
|
- DSP (HPF / NS / AEC / AGC).
|
|
- Opus encode / decode.
|
|
- Jitter buffer / mixer.
|
|
- Mobile (Android Oboe / iOS AVAudioEngine) — **explicit gap**.
|
|
|
|
## Recommended follow-up
|
|
|
|
1. Promote `cpal` selection to a recorded product decision (DEC-011
|
|
sub-decision) — or pin a different crate after the mobile spike.
|
|
Done on 2026-05-13 as DEC-011.1 in
|
|
`docs/governance/product-decision-register.md`.
|
|
2. Spike Android Oboe path on an emulator or real device.
|
|
Done on 2026-05-13 — see
|
|
`poc/audio-capture-playback-android-spike/VERIFICATION.md`.
|
|
3. Spike iOS AVAudioEngine path on a real device.
|
|
Still deferred; requires macOS + Xcode hardware.
|
|
4. Only after the iOS half is closed (or explicitly waived):
|
|
promote the typed `AudioError` + capture/playback shapes into
|
|
`chanora_audio`.
|