use std::fs::File;
use std::io::BufReader;
use std::path::{Path, PathBuf};
use audio_core_bsd::AudioFrame;
use hound::{SampleFormat, WavReader, WavSpec};
use crate::decoder::{ContainerDecoder, FormatKind, StreamInfo};
use crate::error::{CodecError, Result};
const FRAMES_PER_CHUNK: usize = 1024;
pub struct WavDecoder {
path: PathBuf,
reader: WavReader<BufReader<File>>,
spec: WavSpec,
}
impl WavDecoder {
pub fn open(path: &Path) -> Result<Self> {
let reader = WavReader::open(path).map_err(|e| CodecError::Io(e.to_string()))?;
let spec = reader.spec();
Ok(Self {
path: path.to_path_buf(),
reader,
spec,
})
}
fn reopen(&mut self, path: &Path) -> Result<()> {
let reader = WavReader::open(path).map_err(|e| CodecError::Io(e.to_string()))?;
self.spec = reader.spec();
self.reader = reader;
self.path = path.to_path_buf();
Ok(())
}
fn stream_info(&self) -> Result<StreamInfo> {
if self.spec.channels == 0 {
return Err(CodecError::InvalidChannelCount(0));
}
if self.spec.sample_rate == 0 {
return Err(CodecError::InvalidSampleRate(0));
}
let total_frames = u64::from(self.reader.duration());
Ok(StreamInfo {
format: FormatKind::Wav,
sample_rate: self.spec.sample_rate,
channels: self.spec.channels,
bits_per_sample: u32::from(self.spec.bits_per_sample),
total_frames: Some(total_frames),
})
}
#[allow(clippy::cast_precision_loss)]
fn norm_int(sample: i32, bits: u16) -> f32 {
let denom = (1u64 << (bits - 1)) as f32;
let v = sample as f32 / denom;
if v.is_finite() {
v
} else {
0.0
}
}
fn norm_float(sample: f32) -> f32 {
if sample.is_finite() {
sample
} else {
0.0
}
}
fn read_chunk(&mut self) -> Result<Option<AudioFrame>> {
let channels = usize::from(self.spec.channels);
let bits = self.spec.bits_per_sample;
let want = FRAMES_PER_CHUNK.checked_mul(channels).unwrap_or(0);
let mut inter: Vec<f32> = Vec::with_capacity(want);
match self.spec.sample_format {
SampleFormat::Int => {
let mut it = self.reader.samples::<i32>();
for _ in 0..want {
match it.next() {
Some(Ok(s)) => inter.push(Self::norm_int(s, bits)),
Some(Err(e)) => return Err(CodecError::Decode(e.to_string())),
None => break,
}
}
}
SampleFormat::Float => {
let mut it = self.reader.samples::<f32>();
for _ in 0..want {
match it.next() {
Some(Ok(s)) => inter.push(Self::norm_float(s)),
Some(Err(e)) => return Err(CodecError::Decode(e.to_string())),
None => break,
}
}
}
}
if inter.is_empty() {
return Ok(None);
}
if channels == 0 {
return Err(CodecError::InvalidChannelCount(0));
}
let frames = inter.len() / channels;
let mut planar = vec![0.0_f32; channels * frames];
for i in 0..frames {
for c in 0..channels {
planar[c * frames + i] = inter[i * channels + c];
}
}
Ok(Some(AudioFrame::from_planar(
self.spec.channels,
self.spec.sample_rate,
planar,
)))
}
}
impl ContainerDecoder for WavDecoder {
fn open(&mut self, path: &Path) -> Result<StreamInfo> {
if path != self.path {
self.reopen(path)?;
}
self.stream_info()
}
fn next_frame(&mut self) -> Result<Option<AudioFrame>> {
self.read_chunk()
}
}
#[cfg(test)]
mod tests {
use super::*;
use hound::{SampleFormat, WavSpec, WavWriter};
use std::path::PathBuf;
fn approx_eq(a: f32, b: f32) -> bool {
(a - b).abs() < 1e-4
}
const SCALE_16BIT: f32 = 1.0 / 32_768.0;
fn write_wav_16bit(
channels: u16,
sample_rate: u32,
interleaved: &[i16],
suffix: &str,
) -> PathBuf {
let spec = WavSpec {
channels,
sample_rate,
bits_per_sample: 16,
sample_format: SampleFormat::Int,
};
let mut path = std::env::temp_dir();
path.push(format!(
"audio_codec_bsd_wav_test_{}_{}.wav",
std::process::id(),
suffix
));
let mut writer = WavWriter::create(&path, spec).expect("create wav");
for &v in interleaved {
writer.write_sample(v).expect("write sample");
}
writer.finalize().expect("finalize wav");
path
}
#[test]
fn stereo_deinterleave_recovers_independent_channels() {
const FRAMES: usize = 8;
let want_ch0: [i16; FRAMES] = [-3500, -2500, -1500, -500, 500, 1500, 2500, 3500];
let want_ch1: [i16; FRAMES] = [16_000; FRAMES];
let mut interleaved: Vec<i16> = Vec::with_capacity(FRAMES * 2);
for i in 0..FRAMES {
interleaved.push(want_ch0[i]);
interleaved.push(want_ch1[i]);
}
let path = write_wav_16bit(2, 48_000, &interleaved, "stereo_deinterleave");
let mut dec = WavDecoder::open(&path).expect("open");
let info = dec.open(&path).expect("trait open");
assert_eq!(info.format, FormatKind::Wav);
assert_eq!(info.channels, 2);
assert_eq!(info.sample_rate, 48_000);
assert_eq!(info.bits_per_sample, 16);
let frame = dec.next_frame().expect("decode frame").expect("some frame");
assert_eq!(frame.channels, 2);
assert_eq!(frame.samples.len(), FRAMES * 2);
let ch0 = frame.channel_slice(0);
let ch1 = frame.channel_slice(1);
assert_ne!(ch0, ch1);
for (i, &want) in want_ch0.iter().enumerate() {
let expected = f32::from(want) * SCALE_16BIT;
assert!(
approx_eq(ch0[i], expected),
"ch0[{i}] = {}, expected {expected}",
ch0[i]
);
}
for (i, &want) in want_ch1.iter().enumerate() {
let expected = f32::from(want) * SCALE_16BIT;
assert!(
approx_eq(ch1[i], expected),
"ch1[{i}] = {}, expected {expected}",
ch1[i]
);
}
let _ = std::fs::remove_file(&path);
}
#[test]
fn mono_round_trip_values_within_tolerance() {
let interleaved: [i16; 6] = [0, 4_096, 8_192, 16_384, -8_192, -16_384];
let path = write_wav_16bit(1, 44_100, &interleaved, "mono_roundtrip");
let mut dec = WavDecoder::open(&path).expect("open");
let _info = dec.open(&path).expect("trait open");
let frame = dec.next_frame().expect("decode").expect("some");
assert_eq!(frame.channels, 1);
let ch0 = frame.channel_slice(0);
assert_eq!(ch0.len(), interleaved.len());
for (i, &want) in interleaved.iter().enumerate() {
let expected = f32::from(want) * SCALE_16BIT;
assert!(
approx_eq(ch0[i], expected),
"mono[{i}] = {}, expected {expected}",
ch0[i]
);
}
let _ = std::fs::remove_file(&path);
}
#[test]
fn eof_returns_none_after_drain() {
let interleaved: [i16; 3] = [100, 200, 300];
let path = write_wav_16bit(1, 48_000, &interleaved, "eof");
let mut dec = WavDecoder::open(&path).expect("open");
let _info = dec.open(&path).expect("trait open");
let first = dec.next_frame().expect("first").expect("some data");
assert!(!first.samples.is_empty());
let next = dec.next_frame().expect("second call is ok");
assert!(next.is_none(), "expected EOF, got {next:?}");
let _ = std::fs::remove_file(&path);
}
}