Files
chanora/poc/audio-capture-playback-spike
EdisonJwa 4c64517e45 docs(governance): promote audio PoC to PASS; close mobile-Android half
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].
2026-05-14 18:25:03 +08:00
..

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 §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

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).

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:

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.