use std::path::Path;
use rubato::audioadapter_buffers::direct::InterleavedSlice;
use rubato::{Fft, FixedSync, Resampler, WindowFunction};
use crate::LoadError;
#[derive(Debug, Clone)]
pub struct IrData {
pub channels: Vec<Vec<f32>>,
pub num_channels: u16,
pub sample_rate: u32,
}
pub fn load_ir(path: impl AsRef<Path>, target_sample_rate: u32) -> Result<IrData, LoadError> {
let reader = hound::WavReader::open(path).map_err(LoadError::Hound)?;
load_ir_from_reader(reader, target_sample_rate)
}
pub fn resample(samples: &[f32], from_rate: u32, to_rate: u32) -> Result<Vec<f32>, LoadError> {
if from_rate == to_rate || samples.is_empty() {
return Ok(samples.to_vec());
}
let mut resampler = make_resampler(from_rate, to_rate)?;
let mut out = Vec::new();
resample_into(&mut resampler, samples, &mut out)?;
Ok(out)
}
fn make_resampler(from_rate: u32, to_rate: u32) -> Result<Fft<f32>, LoadError> {
const CHUNK: usize = 1024;
Fft::<f32>::new_custom(
from_rate as usize,
to_rate as usize,
CHUNK,
2, 1, WindowFunction::BlackmanHarris2,
FixedSync::Input,
)
.map_err(LoadError::Resample)
}
fn resample_into(
resampler: &mut Fft<f32>,
samples: &[f32],
out: &mut Vec<f32>,
) -> Result<(), LoadError> {
let n_frames = samples.len();
let out_buf_len = resampler.process_all_needed_output_len(n_frames);
out.clear();
out.resize(out_buf_len, 0.0);
let input = InterleavedSlice::new(samples, 1, n_frames)
.map_err(|e| LoadError::UnsupportedFormat(e.to_string()))?;
let mut output = InterleavedSlice::new_mut(out.as_mut_slice(), 1, out_buf_len)
.map_err(|e| LoadError::UnsupportedFormat(e.to_string()))?;
let (_, out_frames) = resampler
.process_all_into_buffer(&input, &mut output, n_frames, None)
.map_err(LoadError::ResampleProcess)?;
out.truncate(out_frames);
Ok(())
}
pub(crate) fn load_ir_from_reader<R: std::io::Read>(
mut reader: hound::WavReader<R>,
target_sample_rate: u32,
) -> Result<IrData, LoadError> {
let spec = reader.spec();
let file_sample_rate = spec.sample_rate;
let num_channels = spec.channels;
let num_ch = num_channels as usize;
let interleaved = decode_samples(&mut reader, &spec)?;
let mut channels: Vec<Vec<f32>> = (0..num_ch)
.map(|ch| {
interleaved
.iter()
.skip(ch)
.step_by(num_ch)
.copied()
.collect()
})
.collect();
if file_sample_rate != target_sample_rate {
let mut resampler = make_resampler(file_sample_rate, target_sample_rate)?;
let mut input_scratch: Vec<f32> = Vec::new();
for ch in channels.iter_mut() {
std::mem::swap(ch, &mut input_scratch);
resampler.reset(); resample_into(&mut resampler, &input_scratch, ch)?;
}
}
Ok(IrData {
channels,
num_channels,
sample_rate: target_sample_rate,
})
}
fn decode_samples<R: std::io::Read>(
reader: &mut hound::WavReader<R>,
spec: &hound::WavSpec,
) -> Result<Vec<f32>, LoadError> {
use hound::SampleFormat;
match (spec.sample_format, spec.bits_per_sample) {
(SampleFormat::Float, 32) => reader
.samples::<f32>()
.map(|s| s.map_err(LoadError::Hound))
.collect(),
(SampleFormat::Int, 16) => {
let scale = 1.0 / i16::MAX as f32; reader
.samples::<i16>()
.map(|s| s.map(|x| x as f32 * scale).map_err(LoadError::Hound))
.collect()
}
(SampleFormat::Int, bits @ (24 | 32)) => {
let scale = 1.0 / (1_u64 << (bits - 1)) as f32;
reader
.samples::<i32>()
.map(|s| s.map(|x| x as f32 * scale).map_err(LoadError::Hound))
.collect()
}
(fmt, bits) => Err(LoadError::UnsupportedFormat(format!("{fmt:?} {bits}-bit"))),
}
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use super::*;
fn make_wav_mono_i16(sample_rate: u32, samples: &[f32]) -> Cursor<Vec<u8>> {
let spec = hound::WavSpec {
channels: 1,
sample_rate,
bits_per_sample: 16,
sample_format: hound::SampleFormat::Int,
};
let mut buf = Cursor::new(Vec::new());
let mut w = hound::WavWriter::new(&mut buf, spec).unwrap();
for &s in samples {
w.write_sample((s * i16::MAX as f32) as i16).unwrap();
}
w.finalize().unwrap();
buf.set_position(0);
buf
}
fn make_wav_stereo_i16(sample_rate: u32, l: &[f32], r: &[f32]) -> Cursor<Vec<u8>> {
assert_eq!(l.len(), r.len());
let spec = hound::WavSpec {
channels: 2,
sample_rate,
bits_per_sample: 16,
sample_format: hound::SampleFormat::Int,
};
let mut buf = Cursor::new(Vec::new());
let mut w = hound::WavWriter::new(&mut buf, spec).unwrap();
for (&ls, &rs) in l.iter().zip(r.iter()) {
w.write_sample((ls * i16::MAX as f32) as i16).unwrap();
w.write_sample((rs * i16::MAX as f32) as i16).unwrap();
}
w.finalize().unwrap();
buf.set_position(0);
buf
}
fn make_wav_multi_i16(sample_rate: u32, channels: &[&[f32]]) -> Cursor<Vec<u8>> {
let num_ch = channels.len();
assert!(num_ch > 0);
let n = channels[0].len();
assert!(
channels.iter().all(|c| c.len() == n),
"channels differ in length"
);
let spec = hound::WavSpec {
channels: num_ch as u16,
sample_rate,
bits_per_sample: 16,
sample_format: hound::SampleFormat::Int,
};
let mut buf = Cursor::new(Vec::new());
let mut w = hound::WavWriter::new(&mut buf, spec).unwrap();
for i in 0..n {
for ch in channels {
w.write_sample((ch[i] * i16::MAX as f32) as i16).unwrap();
}
}
w.finalize().unwrap();
buf.set_position(0);
buf
}
fn make_wav_mono_f32(sample_rate: u32, samples: &[f32]) -> Cursor<Vec<u8>> {
let spec = hound::WavSpec {
channels: 1,
sample_rate,
bits_per_sample: 32,
sample_format: hound::SampleFormat::Float,
};
let mut buf = Cursor::new(Vec::new());
let mut w = hound::WavWriter::new(&mut buf, spec).unwrap();
for &s in samples {
w.write_sample(s).unwrap();
}
w.finalize().unwrap();
buf.set_position(0);
buf
}
#[test]
fn resample_same_rate_is_identity() {
let input: Vec<f32> = (0..64).map(|i| i as f32 / 64.0).collect();
let output = resample(&input, 44_100, 44_100).unwrap();
assert_eq!(output, input);
}
#[test]
fn resample_empty_returns_empty() {
let out = resample(&[], 44_100, 48_000).unwrap();
assert!(out.is_empty());
}
#[test]
fn resample_upsample_length() {
let n = 4410_usize; let input = vec![0.5_f32; n];
let output = resample(&input, 44_100, 48_000).unwrap();
let expected = (n as f64 * 48_000.0 / 44_100.0).round() as usize;
assert_eq!(
output.len(),
expected,
"upsample length mismatch: got {} expected {}",
output.len(),
expected
);
}
#[test]
fn resample_downsample_length() {
let n = 4800_usize; let input = vec![0.5_f32; n];
let output = resample(&input, 48_000, 44_100).unwrap();
let expected = (n as f64 * 44_100.0 / 48_000.0).round() as usize;
assert_eq!(
output.len(),
expected,
"downsample length mismatch: got {} expected {}",
output.len(),
expected
);
}
#[test]
fn resample_dc_signal_preserved() {
let input = vec![1.0_f32; 4096];
let output = resample(&input, 44_100, 48_000).unwrap();
let mid = &output[64..output.len().saturating_sub(64)];
assert!(!mid.is_empty());
for &s in mid {
assert!(
(s - 1.0).abs() < 0.01,
"DC deviation too large: {s} (expected ≈ 1.0)"
);
}
}
#[test]
fn load_ir_mono_i16_same_rate() {
let samples = vec![0.0_f32, 0.5, -0.5, 1.0, -1.0];
let wav = make_wav_mono_i16(48_000, &samples);
let reader = hound::WavReader::new(wav).unwrap();
let ir = load_ir_from_reader(reader, 48_000).unwrap();
assert_eq!(ir.sample_rate, 48_000);
assert_eq!(ir.num_channels, 1);
assert_eq!(ir.channels.len(), 1);
assert_eq!(ir.channels[0].len(), samples.len());
for (&got, &expected) in ir.channels[0].iter().zip(samples.iter()) {
assert!(
(got - expected).abs() < 1.0 / i16::MAX as f32 + 1e-6,
"16-bit round-trip: got {got}, expected {expected}"
);
}
}
#[test]
fn load_ir_stereo_i16_deinterleaved() {
let left = vec![1.0_f32, 0.5, 0.0];
let right = vec![0.0_f32, -0.5, -1.0];
let wav = make_wav_stereo_i16(44_100, &left, &right);
let reader = hound::WavReader::new(wav).unwrap();
let ir = load_ir_from_reader(reader, 44_100).unwrap();
assert_eq!(ir.num_channels, 2);
assert_eq!(ir.channels.len(), 2);
assert_eq!(ir.channels[0].len(), left.len());
assert_eq!(ir.channels[1].len(), right.len());
let tol = 1.0 / i16::MAX as f32 + 1e-6;
for i in 0..left.len() {
assert!(
(ir.channels[0][i] - left[i]).abs() < tol,
"L[{i}]: got {}, expected {}",
ir.channels[0][i],
left[i]
);
assert!(
(ir.channels[1][i] - right[i]).abs() < tol,
"R[{i}]: got {}, expected {}",
ir.channels[1][i],
right[i]
);
}
}
#[test]
fn load_ir_float32_exact() {
let samples = vec![0.123_456_78_f32, -0.987_654_3, 0.5];
let wav = make_wav_mono_f32(48_000, &samples);
let reader = hound::WavReader::new(wav).unwrap();
let ir = load_ir_from_reader(reader, 48_000).unwrap();
assert_eq!(ir.channels[0], samples, "float32 round-trip must be exact");
}
#[test]
fn load_ir_resamples_mono() {
let wav_sr = 44_100_u32;
let target_sr = 48_000_u32;
let n = 4410_usize;
let input: Vec<f32> = (0..n).map(|i| i as f32 / n as f32).collect();
let wav = make_wav_mono_i16(wav_sr, &input);
let reader = hound::WavReader::new(wav).unwrap();
let ir = load_ir_from_reader(reader, target_sr).unwrap();
let expected_len = (n as f64 * target_sr as f64 / wav_sr as f64).round() as usize;
assert_eq!(ir.sample_rate, target_sr);
assert_eq!(ir.num_channels, 1);
assert_eq!(
ir.channels[0].len(),
expected_len,
"resampled length mismatch"
);
}
#[test]
fn load_ir_resamples_stereo_both_channels() {
let wav_sr = 44_100_u32;
let target_sr = 48_000_u32;
let n = 2048_usize;
let left = vec![0.5_f32; n];
let right = vec![-0.5_f32; n];
let wav = make_wav_stereo_i16(wav_sr, &left, &right);
let reader = hound::WavReader::new(wav).unwrap();
let ir = load_ir_from_reader(reader, target_sr).unwrap();
let expected_len = (n as f64 * target_sr as f64 / wav_sr as f64).ceil() as usize;
assert_eq!(ir.num_channels, 2);
assert_eq!(ir.channels[0].len(), expected_len, "L channel length");
assert_eq!(ir.channels[1].len(), expected_len, "R channel length");
}
#[test]
fn load_ir_no_resample_when_rates_match() {
let samples = vec![1.0_f32, -1.0, 0.5, -0.5];
let wav = make_wav_mono_f32(48_000, &samples);
let reader = hound::WavReader::new(wav).unwrap();
let ir = load_ir_from_reader(reader, 48_000).unwrap();
assert_eq!(ir.channels[0], samples);
}
#[test]
fn resample_into_reuses_dirty_buffer() {
let input: Vec<f32> = (0..2048).map(|i| (i as f32 * 0.01).sin()).collect();
let expected = resample(&input, 44_100, 48_000).unwrap();
let mut resampler = make_resampler(44_100, 48_000).unwrap();
let mut out = Vec::new();
resample_into(&mut resampler, &input, &mut out).unwrap();
assert_eq!(out, expected, "first call must match one-shot output");
let cap_after_first = out.capacity();
out.iter_mut().for_each(|s| *s = 9.99);
resampler.reset();
resample_into(&mut resampler, &input, &mut out).unwrap();
assert_eq!(out, expected, "reused buffer must match one-shot output");
assert_eq!(
out.capacity(),
cap_after_first,
"second same-sized call must reuse the allocation"
);
}
#[test]
fn reused_resampler_matches_fresh_per_signal() {
let sig_a: Vec<f32> = (0..1500).map(|i| (i as f32 * 0.02).sin()).collect();
let sig_b: Vec<f32> = (0..1500).map(|i| (i as f32 * 0.05).cos()).collect();
let fresh_a = resample(&sig_a, 48_000, 44_100).unwrap();
let fresh_b = resample(&sig_b, 48_000, 44_100).unwrap();
let mut resampler = make_resampler(48_000, 44_100).unwrap();
let mut out = Vec::new();
resampler.reset();
resample_into(&mut resampler, &sig_a, &mut out).unwrap();
assert_eq!(out, fresh_a, "signal A after reuse must match fresh");
let reused_a = out.clone();
resampler.reset();
resample_into(&mut resampler, &sig_b, &mut out).unwrap();
assert_eq!(out, fresh_b, "signal B after reuse must match fresh");
assert_ne!(reused_a, out);
}
#[test]
fn load_ir_resamples_four_channels() {
let wav_sr = 44_100_u32;
let target_sr = 48_000_u32;
let n = 4096_usize;
let ch0: Vec<f32> = vec![0.25_f32; n];
let ch1: Vec<f32> = vec![-0.25_f32; n];
let ch2: Vec<f32> = vec![0.75_f32; n];
let ch3: Vec<f32> = vec![-0.75_f32; n];
let wav = make_wav_multi_i16(wav_sr, &[&ch0, &ch1, &ch2, &ch3]);
let reader = hound::WavReader::new(wav).unwrap();
let ir = load_ir_from_reader(reader, target_sr).unwrap();
let expected_len = (n as f64 * target_sr as f64 / wav_sr as f64).ceil() as usize;
assert_eq!(ir.num_channels, 4);
assert_eq!(ir.channels.len(), 4);
for (c, ch) in ir.channels.iter().enumerate() {
assert_eq!(ch.len(), expected_len, "channel {c} length mismatch");
}
let levels = [0.25_f32, -0.25, 0.75, -0.75];
let tol = 1.0 / i16::MAX as f32 + 0.02;
for (ch, &level) in ir.channels.iter().zip(levels.iter()) {
let mid = &ch[64..ch.len().saturating_sub(64)];
assert!(!mid.is_empty());
for &s in mid {
assert!((s - level).abs() < tol, "expected ≈ {level}, got {s}");
}
}
}
}