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.
39 lines
1.2 KiB
Rust
39 lines
1.2 KiB
Rust
//! Synthesised waveform for environments with no real input device.
|
|
//!
|
|
//! Generates a 16-bit-PCM mono WAV containing a sine wave at the
|
|
//! requested frequency and duration. This lets the playback path be
|
|
//! exercised against the platform output stack even when capture is
|
|
//! unavailable (typical headless CI host).
|
|
|
|
use std::path::Path;
|
|
use std::time::Duration;
|
|
|
|
use hound::{SampleFormat, WavSpec, WavWriter};
|
|
|
|
use crate::capture::AudioError;
|
|
|
|
/// Write a sine-wave WAV file. Returns (frames_written, sample_rate).
|
|
pub fn synth_sine_wav(
|
|
path: &Path,
|
|
freq_hz: f32,
|
|
duration: Duration,
|
|
sample_rate: u32,
|
|
) -> Result<(u64, u32), AudioError> {
|
|
let spec = WavSpec {
|
|
channels: 1,
|
|
sample_rate,
|
|
bits_per_sample: 16,
|
|
sample_format: SampleFormat::Int,
|
|
};
|
|
let mut w = WavWriter::create(path, spec)?;
|
|
let total_frames = ((duration.as_secs_f64() * sample_rate as f64) as u64).max(1);
|
|
let amp = f32::from(i16::MAX) * 0.5;
|
|
for n in 0..total_frames {
|
|
let t = n as f32 / sample_rate as f32;
|
|
let s = (2.0 * std::f32::consts::PI * freq_hz * t).sin() * amp;
|
|
w.write_sample(s as i16)?;
|
|
}
|
|
w.finalize()?;
|
|
Ok((total_frames, sample_rate))
|
|
}
|