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.
47 lines
1.7 KiB
Rust
47 lines
1.7 KiB
Rust
//! Verification tests. These run against the host audio stack and
|
||
//! therefore live behind the `audio-hardware` opt-in feature for CI
|
||
//! environments without a usable audio device. Without the feature
|
||
//! they fall back to a pure-software synth+WAV round-trip that does
|
||
//! not touch cpal.
|
||
|
||
use std::time::Duration;
|
||
|
||
use audio_capture_playback_spike::synth_sine_wav;
|
||
|
||
#[test]
|
||
fn synth_produces_valid_wav_with_expected_frame_count() {
|
||
let tmp = tempfile::tempdir().unwrap();
|
||
let path = tmp.path().join("sine.wav");
|
||
let (frames, sr) =
|
||
synth_sine_wav(&path, 440.0, Duration::from_millis(500), 48_000).unwrap();
|
||
assert_eq!(sr, 48_000);
|
||
// 0.5 s × 48 kHz = 24 000 frames (mono).
|
||
assert!(frames >= 23_900 && frames <= 24_100, "got {frames}");
|
||
|
||
let reader = hound::WavReader::open(&path).unwrap();
|
||
let spec = reader.spec();
|
||
assert_eq!(spec.channels, 1);
|
||
assert_eq!(spec.sample_rate, 48_000);
|
||
assert_eq!(spec.bits_per_sample, 16);
|
||
assert_eq!(reader.len(), frames as u32);
|
||
}
|
||
|
||
#[test]
|
||
fn synth_then_playback_does_not_error_when_output_device_present() {
|
||
use audio_capture_playback_spike::AudioPlayback;
|
||
use cpal::traits::HostTrait;
|
||
|
||
// Skip on hosts with no output device. The PoC's exit criterion
|
||
// is satisfied by the verification CLI run captured in
|
||
// VERIFICATION.md; this test is a CI sanity check.
|
||
if cpal::default_host().default_output_device().is_none() {
|
||
eprintln!("SKIP: no default output device");
|
||
return;
|
||
}
|
||
|
||
let tmp = tempfile::tempdir().unwrap();
|
||
let path = tmp.path().join("sine.wav");
|
||
synth_sine_wav(&path, 440.0, Duration::from_millis(120), 48_000).unwrap();
|
||
AudioPlayback::play_wav(&path).expect("playback round-trip should not error");
|
||
}
|