use std::fs::File;
use std::path::Path;
use symphonia::core::audio::SampleBuffer;
use symphonia::core::codecs::{DecoderOptions, CODEC_TYPE_NULL};
use symphonia::core::errors::Error as SymphoniaError;
use symphonia::core::formats::FormatOptions;
use symphonia::core::io::MediaSourceStream;
use symphonia::core::meta::MetadataOptions;
use symphonia::core::probe::Hint;
use crate::pcm::Resampler;
#[derive(Debug, thiserror::Error)]
pub enum DecodeError {
#[error("файл не открывается: {0}")]
Io(#[from] std::io::Error),
#[error("формат не поддерживается — подойдут mp3, m4a, wav, aiff, flac, ogg")]
Unsupported,
#[error("в файле нет звуковой дорожки")]
NoTrack,
#[error("файл повреждён: {0}")]
Broken(String),
}
pub fn decode_file(path: &Path) -> Result<Vec<i16>, DecodeError> {
let file = File::open(path)?;
let stream = 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,
stream,
&FormatOptions::default(),
&MetadataOptions::default(),
)
.map_err(|_| DecodeError::Unsupported)?;
let mut format = probed.format;
let track = format
.tracks()
.iter()
.find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
.ok_or(DecodeError::NoTrack)?;
let track_id = track.id;
let mut decoder = symphonia::default::get_codecs()
.make(&track.codec_params, &DecoderOptions::default())
.map_err(|_| DecodeError::Unsupported)?;
let mut out = Vec::new();
let mut resampler: Option<Resampler> = None;
let mut buffer: Option<SampleBuffer<f32>> = None;
loop {
let packet = match format.next_packet() {
Ok(p) => p,
Err(SymphoniaError::IoError(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
Err(SymphoniaError::ResetRequired) => break,
Err(e) => return Err(DecodeError::Broken(e.to_string())),
};
if packet.track_id() != track_id {
continue;
}
let decoded = match decoder.decode(&packet) {
Ok(d) => d,
Err(SymphoniaError::DecodeError(_)) => continue,
Err(e) => return Err(DecodeError::Broken(e.to_string())),
};
let spec = *decoded.spec();
let r = resampler.get_or_insert_with(|| Resampler::new(spec.channels.count(), spec.rate));
let b = match buffer.as_mut() {
Some(b) if b.capacity() >= decoded.capacity() * spec.channels.count() => b,
_ => buffer.insert(SampleBuffer::new(decoded.capacity() as u64, spec)),
};
b.copy_interleaved_ref(decoded);
out.extend(r.push(b.samples()));
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pcm::{encode_wav, ms_to_samples};
#[test]
fn reads_our_own_wav() {
let dir = std::env::temp_dir().join(format!("meet-decode-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("t.wav");
let samples: Vec<i16> = (0..ms_to_samples(500))
.map(|i| ((i % 64) as i16 - 32) * 300)
.collect();
std::fs::write(&path, encode_wav(&samples)).unwrap();
let back = decode_file(&path).unwrap();
assert_eq!(back.len(), samples.len());
let worst = back
.iter()
.zip(&samples)
.map(|(a, b)| (*a as i32 - *b as i32).abs())
.max()
.unwrap();
assert!(worst <= 1, "{worst}");
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn stereo_44k_wav_is_converted() {
let rate = 44_100u32;
let frames = rate as usize / 2;
let mut data = Vec::new();
for i in 0..frames {
let v = ((i as f32 / rate as f32 * std::f32::consts::TAU * 200.0).sin() * 10_000.0) as i16;
data.extend_from_slice(&v.to_le_bytes());
data.extend_from_slice(&v.to_le_bytes());
}
let mut wav = Vec::new();
wav.extend_from_slice(b"RIFF");
wav.extend_from_slice(&(36 + data.len() as u32).to_le_bytes());
wav.extend_from_slice(b"WAVEfmt ");
wav.extend_from_slice(&16u32.to_le_bytes());
wav.extend_from_slice(&1u16.to_le_bytes());
wav.extend_from_slice(&2u16.to_le_bytes());
wav.extend_from_slice(&rate.to_le_bytes());
wav.extend_from_slice(&(rate * 4).to_le_bytes());
wav.extend_from_slice(&4u16.to_le_bytes());
wav.extend_from_slice(&16u16.to_le_bytes());
wav.extend_from_slice(b"data");
wav.extend_from_slice(&(data.len() as u32).to_le_bytes());
wav.extend_from_slice(&data);
let dir = std::env::temp_dir().join(format!("meet-decode44-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("rec.wav");
std::fs::write(&path, wav).unwrap();
let out = decode_file(&path).unwrap();
assert!(
(out.len() as i64 - 8_000).abs() <= 1,
"полсекунды в 16 кГц: {}",
out.len()
);
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn garbage_is_unsupported() {
let dir = std::env::temp_dir().join(format!("meet-decode-bad-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("notes.txt");
std::fs::write(&path, "это не звук").unwrap();
assert!(matches!(decode_file(&path), Err(DecodeError::Unsupported)));
std::fs::remove_dir_all(dir).unwrap();
}
}