Files
chanora/poc/audio-capture-playback-spike/tests/audio.rs
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

47 lines
1.7 KiB
Rust
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 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");
}