feat(kon): add audio crate — cpal capture, VAD, rubato resample, symphonia decode
- File decode via symphonia (mp3, aac, flac, wav, ogg) with mono mixdown - Sinc interpolation resampling via rubato 0.15 (48kHz/44.1kHz → 16kHz) - WAV I/O via hound (read/write with f32→i16 conversion) - Microphone capture via cpal 0.17 (WASAPI on Windows) with mpsc channel output - Voice activity detection via Silero VAD V5 (voice_activity_detector 0.2) - Async decode_and_resample() for file transcription pipeline - 3 tests passing (resample passthrough, duration preservation, WAV roundtrip) - clippy clean, no ort version conflicts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
75
crates/audio/src/capture.rs
Normal file
75
crates/audio/src/capture.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use std::sync::mpsc;
|
||||
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
|
||||
use kon_core::error::{KonError, Result};
|
||||
|
||||
/// A chunk of captured audio from the microphone.
|
||||
pub struct AudioChunk {
|
||||
pub samples: Vec<f32>,
|
||||
pub sample_rate: u32,
|
||||
pub channels: u16,
|
||||
}
|
||||
|
||||
/// Manages microphone capture via cpal.
|
||||
/// Call `start()` to begin capturing, which returns a receiver for audio chunks.
|
||||
/// Call `stop()` to end the stream.
|
||||
pub struct MicrophoneCapture {
|
||||
stream: Option<cpal::Stream>,
|
||||
}
|
||||
|
||||
impl MicrophoneCapture {
|
||||
/// Start capturing audio from the default input device.
|
||||
/// Returns a receiver that yields AudioChunks as they arrive.
|
||||
pub fn start() -> Result<(Self, mpsc::Receiver<AudioChunk>)> {
|
||||
let host = cpal::default_host();
|
||||
let device = host.default_input_device().ok_or_else(|| {
|
||||
KonError::AudioCaptureFailed("No input device found".into())
|
||||
})?;
|
||||
|
||||
let config = device.default_input_config().map_err(|e| {
|
||||
KonError::AudioCaptureFailed(format!("No input config: {e}"))
|
||||
})?;
|
||||
|
||||
let sample_rate = config.sample_rate();
|
||||
let channels = config.channels() as u16;
|
||||
|
||||
let (tx, rx) = mpsc::channel::<AudioChunk>();
|
||||
|
||||
let stream = device
|
||||
.build_input_stream(
|
||||
&config.into(),
|
||||
move |data: &[f32], _info: &cpal::InputCallbackInfo| {
|
||||
let _ = tx.send(AudioChunk {
|
||||
samples: data.to_vec(),
|
||||
sample_rate,
|
||||
channels,
|
||||
});
|
||||
},
|
||||
|err| eprintln!("audio capture error: {err}"),
|
||||
None,
|
||||
)
|
||||
.map_err(|e| {
|
||||
KonError::AudioCaptureFailed(format!("Build stream failed: {e}"))
|
||||
})?;
|
||||
|
||||
stream.play().map_err(|e| {
|
||||
KonError::AudioCaptureFailed(format!("Stream play failed: {e}"))
|
||||
})?;
|
||||
|
||||
Ok((Self { stream: Some(stream) }, rx))
|
||||
}
|
||||
|
||||
/// Stop capturing audio.
|
||||
pub fn stop(&mut self) {
|
||||
if let Some(stream) = self.stream.take() {
|
||||
let _ = stream.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MicrophoneCapture {
|
||||
fn drop(&mut self) {
|
||||
self.stop();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user