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.
119 lines
4.5 KiB
Markdown
119 lines
4.5 KiB
Markdown
# 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`.
|