mod error;
mod normalize;
mod resample;
#[cfg(test)]
mod tests;
pub use error::DecodeError;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::ops::RangeInclusive;
use std::path::Path;
use symphonia::core::audio::SampleBuffer;
use symphonia::core::codecs::{CODEC_TYPE_NULL, CodecParameters, DecoderOptions};
use symphonia::core::errors::Error as SymphoniaError;
use symphonia::core::formats::{FormatOptions, FormatReader, Packet};
use symphonia::core::io::{MediaSource, MediaSourceStream};
use symphonia::core::meta::MetadataOptions;
use symphonia::core::probe::Hint;
#[derive(Debug, Clone)]
pub struct DecodedAudio {
pub samples: Vec<f32>,
pub sample_rate: u32,
pub source_sample_rate: u32,
pub source_channels: u16,
pub applied_gain_db: f32,
}
impl DecodedAudio {
pub fn from_samples(samples: Vec<f32>, sample_rate: u32) -> Self {
Self {
samples,
sample_rate,
source_sample_rate: sample_rate,
source_channels: 1,
applied_gain_db: 0.0,
}
}
pub fn duration_s(&self) -> f32 {
self.samples.len() as f32 / self.sample_rate as f32
}
}
#[derive(Debug, Clone)]
pub struct DecodeConfig {
pub target_sample_rate: u32,
pub target_dbfs: f32,
pub max_gain_db: f32,
pub normalize: bool,
pub max_duration_s: Option<f32>,
}
impl Default for DecodeConfig {
fn default() -> Self {
Self {
target_sample_rate: 16_000,
target_dbfs: -20.0,
max_gain_db: 3.0,
normalize: true,
max_duration_s: None,
}
}
}
pub fn decode_file(path: &Path, config: &DecodeConfig) -> Result<DecodedAudio, DecodeError> {
let file = File::open(path).map_err(|source| DecodeError::Open {
path: path.to_path_buf(),
source,
})?;
let extension = path.extension().and_then(|ext| ext.to_str());
decode_source(Box::new(file), extension, config)
}
pub fn decode_reader<R>(
reader: R,
extension_hint: Option<&str>,
config: &DecodeConfig,
) -> Result<DecodedAudio, DecodeError>
where
R: Read + Seek + Send + Sync + 'static,
{
decode_source(Box::new(ReadSeekSource(reader)), extension_hint, config)
}
struct ReadSeekSource<R>(R);
impl<R: Read> Read for ReadSeekSource<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.0.read(buf)
}
}
impl<R: Seek> Seek for ReadSeekSource<R> {
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
self.0.seek(pos)
}
}
impl<R: Read + Seek + Send + Sync> MediaSource for ReadSeekSource<R> {
fn is_seekable(&self) -> bool {
true
}
fn byte_len(&self) -> Option<u64> {
None
}
}
const SOURCE_SAMPLE_RATES: RangeInclusive<u32> = 1_000..=768_000;
const MAX_PREALLOCATED_FRAMES: u64 = 48_000 * 600;
struct Track {
id: u32,
codec: CodecParameters,
sample_rate: u32,
}
struct MonoTrack {
samples: Vec<f32>,
source_channels: u16,
}
fn decode_source(
source: Box<dyn MediaSource>,
extension_hint: Option<&str>,
config: &DecodeConfig,
) -> Result<DecodedAudio, DecodeError> {
if config.target_sample_rate == 0 {
return Err(DecodeError::InvalidConfig(
"target_sample_rate must be non-zero",
));
}
let mut format = probe(source, extension_hint)?;
let track = select_track(format.as_ref())?;
let mono = decode_to_mono(format.as_mut(), &track, config.max_duration_s)?;
let mut samples =
resample::resample_mono(mono.samples, track.sample_rate, config.target_sample_rate)?;
let applied_gain_db = if config.normalize {
normalize::normalize_in_place(&mut samples, config.target_dbfs, config.max_gain_db)
} else {
0.0
};
Ok(DecodedAudio {
samples,
sample_rate: config.target_sample_rate,
source_sample_rate: track.sample_rate,
source_channels: mono.source_channels,
applied_gain_db,
})
}
fn probe(
source: Box<dyn MediaSource>,
extension_hint: Option<&str>,
) -> Result<Box<dyn FormatReader>, DecodeError> {
let mut hint = Hint::new();
if let Some(extension) = extension_hint {
hint.with_extension(extension);
}
let format_options = FormatOptions {
enable_gapless: true,
..Default::default()
};
symphonia::default::get_probe()
.format(
&hint,
MediaSourceStream::new(source, Default::default()),
&format_options,
&MetadataOptions::default(),
)
.map(|probed| probed.format)
.map_err(|error| match error {
SymphoniaError::IoError(source) => DecodeError::Read(source),
other => DecodeError::UnsupportedFormat(other.to_string()),
})
}
fn select_track(format: &dyn FormatReader) -> Result<Track, DecodeError> {
let track = format
.tracks()
.iter()
.find(|track| track.codec_params.codec != CODEC_TYPE_NULL)
.ok_or(DecodeError::NoAudioTrack)?;
let sample_rate = track
.codec_params
.sample_rate
.ok_or(DecodeError::MissingSampleRate)?;
if !SOURCE_SAMPLE_RATES.contains(&sample_rate) {
return Err(DecodeError::UnsupportedSampleRate(sample_rate));
}
Ok(Track {
id: track.id,
codec: track.codec_params.clone(),
sample_rate,
})
}
fn decode_to_mono(
format: &mut dyn FormatReader,
track: &Track,
max_duration_s: Option<f32>,
) -> Result<MonoTrack, DecodeError> {
let max_frames =
max_duration_s.map(|limit_s| (f64::from(limit_s) * f64::from(track.sample_rate)) as usize);
let mut decoder = symphonia::default::get_codecs()
.make(&track.codec, &DecoderOptions::default())
.map_err(|error| DecodeError::UnsupportedFormat(error.to_string()))?;
let declared_frames = track.codec.n_frames.unwrap_or(0);
let mut samples = Vec::with_capacity(declared_frames.min(MAX_PREALLOCATED_FRAMES) as usize);
let mut source_channels = track.codec.channels.map_or(0, |channels| channels.count());
let mut interleaved: Option<SampleBuffer<f32>> = None;
while let Some(packet) = next_packet(format, track.id)? {
let decoded = match decoder.decode(&packet) {
Ok(decoded) => decoded,
Err(SymphoniaError::DecodeError(_)) => continue,
Err(SymphoniaError::ResetRequired) => {
decoder.reset();
continue;
}
Err(error) if is_end_of_stream(&error) => break,
Err(error) => return Err(DecodeError::Decode(error.to_string())),
};
if decoded.frames() == 0 {
continue;
}
let spec = *decoded.spec();
let channels = spec.channels.count().max(1);
if source_channels == 0 {
source_channels = channels;
}
let buffer = match &mut interleaved {
Some(buffer) if buffer.capacity() >= decoded.frames() * channels => buffer,
slot => slot.insert(SampleBuffer::new(decoded.capacity() as u64, spec)),
};
buffer.copy_interleaved_ref(decoded);
samples.extend(
buffer
.samples()
.chunks_exact(channels)
.map(|frame| frame.iter().sum::<f32>() / channels as f32),
);
if let (Some(max_frames), Some(limit_s)) = (max_frames, max_duration_s)
&& samples.len() > max_frames
{
return Err(DecodeError::TooLong { limit_s });
}
}
Ok(MonoTrack {
samples,
source_channels: source_channels.max(1) as u16,
})
}
fn next_packet(
format: &mut dyn FormatReader,
track_id: u32,
) -> Result<Option<Packet>, DecodeError> {
loop {
match format.next_packet() {
Ok(packet) if packet.track_id() == track_id => return Ok(Some(packet)),
Ok(_) => continue,
Err(SymphoniaError::ResetRequired) => return Ok(None),
Err(error) if is_end_of_stream(&error) => return Ok(None),
Err(SymphoniaError::IoError(source)) => return Err(DecodeError::Read(source)),
Err(error) => return Err(DecodeError::Decode(error.to_string())),
}
}
}
fn is_end_of_stream(error: &SymphoniaError) -> bool {
matches!(error, SymphoniaError::IoError(io) if io.kind() == std::io::ErrorKind::UnexpectedEof)
}