Files
chanora/poc/audio-capture-playback-spike/VERIFICATION.md
T
EdisonJwa d5b53996bc feat(poc/audio): add audio capture/playback spike (partial — desktop only)
Proof-of-concept addressing the audio exit criterion from
docs/architecture/proof-of-concept-plan.md §2:
  "Capture/playback works on at least one desktop and one mobile
   target."

PARTIAL PASS. The desktop half is verified on Linux; the mobile
half is NOT verified by this PoC and remains a documented open gap.

Implements via cpal (matching DEC-011 'platform-native first'):
  - AudioCapture::record_to_wav opens the default input device,
    handles f32/i16/u16 sample formats, down-mixes to mono, writes
    16-bit PCM WAV via hound.
  - AudioPlayback::play_wav opens the default output device, picks
    a stream config matching the WAV, blocks until drained.
  - synth_sine_wav produces a deterministic 440 Hz test signal for
    headless verification of the playback path when no microphone
    is available.
  - Typed AudioError DTO with NoInputDevice, NoOutputDevice,
    DefaultConfig, BuildStream, PlayStream, Wav, Io,
    UnsupportedFormat arms.

Verified on 2026-05-13 (Linux + cpal + PipeWire). Capture stream
opened against the system default input; build failed against the
auto_null source (typed AudioError::BuildStream returned cleanly,
demonstrating the production error path); fallback to synth fired;
playback drove 24,000 frames to completion through
Rust → cpal → ALSA → pcm_pipewire → PipeWire → auto_null.
Both audio.rs tests pass.

Mobile gap (explicit, NOT closed):
  - Android Oboe path not built or run.
  - iOS AVAudioEngine path not built or run.

Surfaced finding for the decision register: DEC-011 does not pin an
audio crate. The PoC uses cpal; production code needs an owner
ruling, ideally after the mobile spike closes the gap.

Out of scope: DSP (HPF/NS/AEC/AGC), Opus encode/decode, jitter
buffer, mixer, latency measurement, bit-exact loopback, device
permission flows. These belong to chanora_audio.

Authority: PoC plan §2, DEC-011, SysDes audio subsystem.
Not product code; not promoted into chanora_audio.
2026-05-14 12:27:04 +08:00

119 lines
4.5 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-spike`
## Result
**PARTIAL PASS.** Desktop half met on Linux; **mobile half not
verified**.
The PoC plan §2 exit criterion is "Capture/playback works on at least
one desktop and one mobile target." This spike has empirically
verified the **desktop** path (Linux × cpal × PipeWire). The
**mobile** path (Android Oboe or iOS AVAudioEngine) has not been
built or run in this session and remains a documented gap. See the
`Gaps` section in `README.md`.
## 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.
2. Spike Android Oboe path on an emulator or real device.
3. Spike iOS AVAudioEngine path on a real device.
4. Only after the mobile half is closed: promote the typed
`AudioError` + capture/playback shapes into `chanora_audio`.