- 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>
105 lines
3.4 KiB
Rust
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))
|
|
}
|