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.
120 lines
4.6 KiB
Markdown
120 lines
4.6 KiB
Markdown
# Audio Capture / Playback Spike
|
|
|
|
Chanora proof-of-concept. **Not product code.**
|
|
|
|
| Field | Value |
|
|
|---|---|
|
|
| PoC name | `audio-capture-playback-spike` |
|
|
| PoC plan | [`docs/architecture/proof-of-concept-plan.md`](../../docs/architecture/proof-of-concept-plan.md) §2 |
|
|
| Purpose | Prove platform audio capture + playback behaviour |
|
|
| Exit criterion (plan) | "Capture/playback works on at least one desktop and one mobile target" |
|
|
| Authority | DEC-011 (platform-native first), audio family in SysDes / SRS |
|
|
|
|
## Honest status
|
|
|
|
**Partial pass.**
|
|
|
|
- **Linux desktop**: verified. cpal opens both the default input and
|
|
the default output device, capture streams build (or report a typed
|
|
error when no hardware-backed input is available), and playback
|
|
drives a full PCM stream end-to-end through the platform audio
|
|
stack (ALSA → PipeWire on this host).
|
|
- **Mobile (Android / iOS)**: **not verified**. cpal supports Oboe
|
|
(Android) and AVAudioEngine (iOS) but this PoC has not been built
|
|
or run on either. The exit criterion's mobile half remains an open
|
|
item for a follow-up spike — see "Gaps" below.
|
|
|
|
## What it proves
|
|
|
|
- `AudioCapture::record_to_wav(path, duration)` — opens the default
|
|
input device, reads frames in any supported sample format, down-mixes
|
|
to mono, writes 16-bit-PCM WAV.
|
|
- `AudioPlayback::play_wav(path)` — opens the default output device,
|
|
picks a stream config matching the file's sample rate, blocks until
|
|
the file drains.
|
|
- `synth_sine_wav` — produces a deterministic 440 Hz sine WAV so the
|
|
playback path is exercisable on hosts without a usable input device.
|
|
- A typed `AudioError` covering the practical failure modes (no
|
|
device, config mismatch, stream-build failure, WAV I/O, unsupported
|
|
format). Production code (`chanora_audio`) will widen this when DSP
|
|
+ Opus + jitter buffer surfaces land.
|
|
- Headless-friendly behaviour: if capture cannot build a stream, the
|
|
CLI falls back to synth and the playback path is still exercised.
|
|
This is the same shape production code would use for a
|
|
"device test" UI.
|
|
|
|
## Layout
|
|
|
|
```text
|
|
audio-capture-playback-spike/
|
|
src/
|
|
lib.rs # crate root + re-exports
|
|
capture.rs # AudioCapture, AudioError, sample-format adapters
|
|
playback.rs # AudioPlayback
|
|
synth.rs # synth_sine_wav (deterministic test signal)
|
|
main.rs # audio-capture-playback-cli
|
|
tests/
|
|
audio.rs # 2 tests
|
|
Cargo.toml
|
|
```
|
|
|
|
## Reproduce (Linux desktop)
|
|
|
|
Requires Rust stable, ALSA dev headers (cpal builds against ALSA on
|
|
Linux), and a working audio stack (PipeWire / Pulse / pure ALSA).
|
|
|
|
```bash
|
|
cargo test
|
|
cargo run --bin audio-capture-playback-cli -- roundtrip
|
|
```
|
|
|
|
CLI modes:
|
|
|
|
| Mode | What it does |
|
|
|---|---|
|
|
| `capture` | Records 1 s of audio from the default input device to `${TMPDIR}/chanora_audio_spike_capture.wav`. |
|
|
| `synth` | Writes a 440 Hz / 500 ms sine wave to `${TMPDIR}/chanora_audio_spike_synth.wav`. |
|
|
| `playback` | Plays the synth WAV through the default output device. |
|
|
| `roundtrip` (default) | Tries `capture`; on any failure, falls back to `synth`; then `playback`. |
|
|
|
|
### PipeWire-only Linux hosts
|
|
|
|
On hosts where the only audio server is PipeWire (no `pulseaudio`
|
|
package), `cpal` still uses ALSA underneath and needs a
|
|
`pcm.!default` alias to the PipeWire PCM plugin. The package
|
|
`pipewire-alsa` (or your distro equivalent) plus a one-line
|
|
`~/.asoundrc` is the standard fix:
|
|
|
|
```text
|
|
pcm.!default { type pipewire }
|
|
ctl.!default { type pipewire }
|
|
```
|
|
|
|
## Gaps (the mobile half of the exit criterion)
|
|
|
|
| Target | What's missing | Notes |
|
|
|---|---|---|
|
|
| Android | Build & run cpal-oboe path on an emulator or device | Needs NDK + Gradle wiring + emulator. cpal supports it; this spike has not built it. |
|
|
| iOS | Build & run cpal-AVAudioEngine path on a device | Needs macOS + Xcode + a developer account. |
|
|
|
|
These should be closed by a follow-up spike (or by promotion into the
|
|
product `chanora_audio` crate scaffold) before the audio half of the
|
|
PoC matrix is considered complete.
|
|
|
|
## Scope boundaries
|
|
|
|
- **No DSP.** HPF / NS / AEC / AGC / mixing / Opus encode-decode /
|
|
jitter buffer all belong to `chanora_audio` and are out of scope.
|
|
- **No bit-exact loopback assertion.** This PoC verifies *that* a
|
|
capture stream can be opened and *that* a playback stream can drive
|
|
a file to completion. It does not verify capture-to-playback signal
|
|
integrity.
|
|
- **No latency measurement.**
|
|
- **No device-permission flows.** Mobile microphone consent UI is
|
|
product-level.
|
|
- **No echo-cancellation evidence.** Different PoC.
|
|
|
|
## Verification log
|
|
|
|
See `VERIFICATION.md` in this directory.
|