Files
Lumotia/crates/audio/src/decode.rs
jake a30c9cc107 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>
2026-03-16 20:56:30 +00:00

105 lines
3.4 KiB
Rust

use std::fs::File;
use std::path::Path;
use symphonia::core::audio::SampleBuffer;
use symphonia::core::codecs::DecoderOptions;
use symphonia::core::formats::FormatOptions;
use symphonia::core::io::MediaSourceStream;
use symphonia::core::meta::MetadataOptions;
use symphonia::core::probe::Hint;
use kon_core::error::{KonError, Result};
use kon_core::types::AudioSamples;
/// Decode an audio file to mono f32 PCM samples.
/// Supports all formats symphonia handles: mp3, aac, flac, wav, ogg, etc.
pub fn decode_audio_file(path: &Path) -> Result<AudioSamples> {
let file = File::open(path)
.map_err(|e| KonError::AudioDecodeFailed(format!("Cannot open file: {e}")))?;
let mss = MediaSourceStream::new(Box::new(file), Default::default());
let mut hint = Hint::new();
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
hint.with_extension(ext);
}
let probed = symphonia::default::get_probe()
.format(&hint, mss, &FormatOptions::default(), &MetadataOptions::default())
.map_err(|e| KonError::AudioDecodeFailed(format!("Unsupported format: {e}")))?;
let mut format = probed.format;
let track = format
.default_track()
.ok_or_else(|| KonError::AudioDecodeFailed("No audio track found".into()))?;
let sample_rate = track
.codec_params
.sample_rate
.ok_or_else(|| KonError::AudioDecodeFailed("Unknown sample rate".into()))?;
if sample_rate == 0 {
return Err(KonError::AudioDecodeFailed("Invalid sample rate: 0".into()));
}
let track_id = track.id;
let mut decoder = symphonia::default::get_codecs()
.make(&track.codec_params, &DecoderOptions::default())
.map_err(|e| KonError::AudioDecodeFailed(format!("Codec error: {e}")))?;
let mut samples: Vec<f32> = Vec::new();
let mut decode_errors = 0u32;
loop {
let packet = match format.next_packet() {
Ok(p) => p,
Err(symphonia::core::errors::Error::IoError(ref e))
if e.kind() == std::io::ErrorKind::UnexpectedEof =>
{
break;
}
Err(symphonia::core::errors::Error::ResetRequired) => break,
Err(_) => break,
};
if packet.track_id() != track_id {
continue;
}
let decoded = match decoder.decode(&packet) {
Ok(d) => d,
Err(_) => {
decode_errors += 1;
continue;
}
};
let spec = *decoded.spec();
let channels = spec.channels.count();
let mut sample_buf =
SampleBuffer::<f32>::new(decoded.capacity() as u64, spec);
sample_buf.copy_interleaved_ref(decoded);
let buf = sample_buf.samples();
if channels == 1 {
samples.extend_from_slice(buf);
} else {
for chunk in buf.chunks(channels) {
let sum: f32 = chunk.iter().sum();
samples.push(sum / channels as f32);
}
}
}
if samples.is_empty() {
if decode_errors > 0 {
return Err(KonError::AudioDecodeFailed(format!(
"No audio decoded ({decode_errors} packets failed — file may be corrupt)"
)));
}
return Err(KonError::AudioDecodeFailed("No audio data decoded".into()));
}
Ok(AudioSamples::new(samples, sample_rate, 1))
}