use crate::{AudioBuffer, AudioError, AudioFrame, AudioResult, ChannelLayout};
use audioadapter::Adapter;
use audioadapter_buffers::direct::SequentialSliceOfVecs;
use bytes::Bytes;
use oximedia_core::SampleFormat;
use rubato::{
Async as RubatoAsync, Fft as RubatoFft, FixedAsync, FixedSync, Resampler as RubatoResampler,
ResamplerConstructionError, SincInterpolationParameters, SincInterpolationType, WindowFunction,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum ResamplerQuality {
Low,
#[default]
Medium,
High,
Best,
Draft,
Good,
}
impl ResamplerQuality {
#[must_use]
pub const fn canonical(self) -> Self {
match self {
Self::Draft => Self::Low,
Self::Good => Self::High,
other => other,
}
}
#[must_use]
fn fft_chunk_size(&self) -> usize {
match self.canonical() {
Self::Low => 256,
Self::Medium => 1024,
Self::High | Self::Best => 2048,
_ => 1024,
}
}
#[must_use]
fn fft_sub_chunks(&self) -> usize {
match self.canonical() {
Self::Low => 1,
Self::Medium => 2,
Self::High | Self::Best => 4,
_ => 2,
}
}
#[must_use]
fn sinc_params(&self) -> SincInterpolationParameters {
match self.canonical() {
Self::Low | Self::Medium => SincInterpolationParameters {
sinc_len: 64,
f_cutoff: 0.95,
interpolation: SincInterpolationType::Cubic,
oversampling_factor: 128,
window: WindowFunction::BlackmanHarris2,
},
Self::High => SincInterpolationParameters {
sinc_len: 128,
f_cutoff: 0.95,
interpolation: SincInterpolationType::Cubic,
oversampling_factor: 256,
window: WindowFunction::BlackmanHarris2,
},
Self::Best => SincInterpolationParameters {
sinc_len: 256,
f_cutoff: 0.95,
interpolation: SincInterpolationType::Linear,
oversampling_factor: 256,
window: WindowFunction::BlackmanHarris2,
},
_ => SincInterpolationParameters {
sinc_len: 128,
f_cutoff: 0.95,
interpolation: SincInterpolationType::Cubic,
oversampling_factor: 256,
window: WindowFunction::BlackmanHarris2,
},
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ResamplerStrategy {
Passthrough,
FixedInOut,
FixedIn,
FixedOut,
}
pub struct Resampler {
source_rate: u32,
target_rate: u32,
channels: usize,
quality: ResamplerQuality,
strategy: ResamplerStrategy,
ratio: f64,
engine: ResamplerEngine,
input_buffer: Vec<Vec<f32>>,
buffered_samples: usize,
}
enum ResamplerEngine {
Passthrough,
Fft(Box<RubatoFft<f32>>),
SincAsync(Box<RubatoAsync<f32>>),
}
impl Resampler {
pub fn new(
source_rate: u32,
target_rate: u32,
channels: usize,
quality: ResamplerQuality,
) -> AudioResult<Self> {
Self::with_max_buffering(source_rate, target_rate, channels, quality, 8192)
}
#[allow(clippy::too_many_arguments)]
pub fn with_max_buffering(
source_rate: u32,
target_rate: u32,
channels: usize,
quality: ResamplerQuality,
max_input_frames: usize,
) -> AudioResult<Self> {
if source_rate == 0 || target_rate == 0 {
return Err(AudioError::InvalidParameter(
"Sample rate must be non-zero".into(),
));
}
if channels == 0 || channels > 32 {
return Err(AudioError::InvalidParameter(
"Channel count must be between 1 and 32".into(),
));
}
let ratio = f64::from(target_rate) / f64::from(source_rate);
let (strategy, engine) = if source_rate == target_rate {
(ResamplerStrategy::Passthrough, ResamplerEngine::Passthrough)
} else {
Self::create_engine(
source_rate,
target_rate,
channels,
quality,
max_input_frames,
)?
};
let input_buffer = vec![Vec::new(); channels];
Ok(Self {
source_rate,
target_rate,
channels,
quality,
strategy,
ratio,
engine,
input_buffer,
buffered_samples: 0,
})
}
fn create_engine(
source_rate: u32,
target_rate: u32,
channels: usize,
quality: ResamplerQuality,
_max_input_frames: usize,
) -> AudioResult<(ResamplerStrategy, ResamplerEngine)> {
let chunk_size = quality.fft_chunk_size();
let gcd = gcd(source_rate, target_rate);
let ratio_num = target_rate / gcd;
let ratio_den = source_rate / gcd;
if matches!(
quality.canonical(),
ResamplerQuality::High | ResamplerQuality::Best
) {
let params = quality.sinc_params();
let engine = RubatoAsync::<f32>::new_sinc(
f64::from(target_rate) / f64::from(source_rate),
2.0,
¶ms,
chunk_size,
channels,
FixedAsync::Input,
)
.map_err(map_rubato_error)?;
return Ok((
ResamplerStrategy::FixedIn,
ResamplerEngine::SincAsync(Box::new(engine)),
));
}
let sub_chunks = quality.fft_sub_chunks();
if ratio_num < 100 && ratio_den < 100 {
match RubatoFft::<f32>::new(
source_rate as usize,
target_rate as usize,
chunk_size,
sub_chunks,
channels,
FixedSync::Both,
) {
Ok(engine) => {
return Ok((
ResamplerStrategy::FixedInOut,
ResamplerEngine::Fft(Box::new(engine)),
));
}
Err(_) => {
}
}
}
let engine = RubatoFft::<f32>::new(
source_rate as usize,
target_rate as usize,
chunk_size,
sub_chunks,
channels,
FixedSync::Input,
)
.map_err(map_rubato_error)?;
Ok((
ResamplerStrategy::FixedIn,
ResamplerEngine::Fft(Box::new(engine)),
))
}
pub fn resample(&mut self, input: &AudioFrame) -> AudioResult<AudioFrame> {
if self.strategy == ResamplerStrategy::Passthrough {
return Ok(input.clone());
}
if input.channels.count() != self.channels {
return Err(AudioError::InvalidParameter(format!(
"Channel count mismatch: expected {}, got {}",
self.channels,
input.channels.count()
)));
}
let input_planar = self.convert_to_f32_planar(input)?;
let output_planar = self.process_samples(&input_planar)?;
self.convert_from_f32_planar(&output_planar, input.format, &input.channels)
}
fn process_samples(&mut self, input: &[Vec<f32>]) -> AudioResult<Vec<Vec<f32>>> {
match &mut self.engine {
ResamplerEngine::Passthrough => Ok(input.to_vec()),
ResamplerEngine::Fft(engine) => Self::process_fixed_in_engine(
engine.as_mut(),
input,
&mut self.input_buffer,
&mut self.buffered_samples,
self.channels,
"FFT",
),
ResamplerEngine::SincAsync(engine) => Self::process_fixed_in_engine(
engine.as_mut(),
input,
&mut self.input_buffer,
&mut self.buffered_samples,
self.channels,
"Sinc",
),
}
}
fn process_fixed_in_engine(
engine: &mut dyn RubatoResampler<f32>,
input: &[Vec<f32>],
input_buffer: &mut [Vec<f32>],
buffered_samples: &mut usize,
channels: usize,
label: &str,
) -> AudioResult<Vec<Vec<f32>>> {
let mut output: Vec<Vec<f32>> = Vec::new();
let mut remaining = input.to_vec();
while !remaining.is_empty() && !remaining[0].is_empty() {
let chunk_size = engine.input_frames_next();
if remaining[0].len() >= chunk_size {
let chunk: Vec<Vec<f32>> = remaining
.iter()
.map(|ch| ch[..chunk_size].to_vec())
.collect();
let input_adapter = SequentialSliceOfVecs::new(&chunk, channels, chunk_size)
.map_err(|e| {
AudioError::Internal(format!(
"{label} resampling: input adapter creation failed: {e}"
))
})?;
let result = engine
.process(&input_adapter, 0, None)
.map_err(|e| AudioError::Internal(format!("{label} resampling failed: {e}")))?;
let result_planar = interleaved_owned_to_planar(&result, channels);
if output.is_empty() {
output = result_planar;
} else {
for (out_ch, res_ch) in output.iter_mut().zip(result_planar.iter()) {
out_ch.extend_from_slice(res_ch);
}
}
remaining = remaining
.iter()
.map(|ch| ch[chunk_size..].to_vec())
.collect::<Vec<_>>();
} else {
for (buf_ch, rem_ch) in input_buffer.iter_mut().zip(remaining.iter()) {
buf_ch.extend_from_slice(rem_ch);
}
*buffered_samples = remaining[0].len();
break;
}
}
Ok(output)
}
fn convert_to_f32_planar(&self, frame: &AudioFrame) -> AudioResult<Vec<Vec<f32>>> {
let sample_count = frame.sample_count();
let mut planar = vec![vec![0.0f32; sample_count]; self.channels];
match &frame.samples {
AudioBuffer::Interleaved(data) => {
self.deinterleave_to_f32(data, frame.format, &mut planar)?;
}
AudioBuffer::Planar(planes) => {
self.planes_to_f32(planes, frame.format, &mut planar)?;
}
}
Ok(planar)
}
#[allow(clippy::cast_precision_loss)]
fn deinterleave_to_f32(
&self,
data: &[u8],
format: SampleFormat,
output: &mut [Vec<f32>],
) -> AudioResult<()> {
let sample_count = output[0].len();
let bytes_per_sample = format.bytes_per_sample();
for sample_idx in 0..sample_count {
for ch in 0..self.channels {
let offset = (sample_idx * self.channels + ch) * bytes_per_sample;
let value = self.read_sample(data, offset, format)?;
output[ch][sample_idx] = value;
}
}
Ok(())
}
fn planes_to_f32(
&self,
planes: &[Bytes],
format: SampleFormat,
output: &mut [Vec<f32>],
) -> AudioResult<()> {
let sample_count = output[0].len();
let bytes_per_sample = format.bytes_per_sample();
for ch in 0..self.channels {
if ch >= planes.len() {
return Err(AudioError::InvalidData(
"Insufficient planes for channel count".into(),
));
}
for sample_idx in 0..sample_count {
let offset = sample_idx * bytes_per_sample;
let value = self.read_sample(&planes[ch], offset, format)?;
output[ch][sample_idx] = value;
}
}
Ok(())
}
#[allow(clippy::cast_precision_loss)]
fn read_sample(&self, data: &[u8], offset: usize, format: SampleFormat) -> AudioResult<f32> {
let bytes_per_sample = format.bytes_per_sample();
if offset + bytes_per_sample > data.len() {
return Err(AudioError::InvalidData(
"Sample offset out of bounds".into(),
));
}
let value = match format {
SampleFormat::U8 => (f32::from(data[offset]) - 128.0) / 128.0,
SampleFormat::S16 | SampleFormat::S16p => {
let bytes = [data[offset], data[offset + 1]];
let sample = i16::from_le_bytes(bytes);
sample as f32 / 32768.0
}
SampleFormat::S32 | SampleFormat::S32p => {
let bytes = [
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
];
let sample = i32::from_le_bytes(bytes);
sample as f32 / 2_147_483_648.0
}
SampleFormat::F32 | SampleFormat::F32p => {
let bytes = [
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
];
f32::from_le_bytes(bytes)
}
SampleFormat::F64 | SampleFormat::F64p => {
let bytes = [
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
data[offset + 4],
data[offset + 5],
data[offset + 6],
data[offset + 7],
];
#[allow(clippy::cast_possible_truncation)]
let result = f64::from_le_bytes(bytes) as f32;
result
}
_ => {
return Err(AudioError::UnsupportedFormat(format!(
"Unsupported sample format: {format}"
)))
}
};
Ok(value)
}
fn convert_from_f32_planar(
&self,
planar: &[Vec<f32>],
format: SampleFormat,
channels: &ChannelLayout,
) -> AudioResult<AudioFrame> {
if planar.is_empty() {
let mut frame = AudioFrame::new(format, self.target_rate, channels.clone());
frame.samples = AudioBuffer::Interleaved(Bytes::new());
return Ok(frame);
}
let mut frame = AudioFrame::new(format, self.target_rate, channels.clone());
if format.is_planar() {
frame.samples = self.f32_to_planar_bytes(planar, format)?;
} else {
frame.samples = self.f32_to_interleaved_bytes(planar, format)?;
}
Ok(frame)
}
fn f32_to_interleaved_bytes(
&self,
planar: &[Vec<f32>],
format: SampleFormat,
) -> AudioResult<AudioBuffer> {
let sample_count = planar[0].len();
let bytes_per_sample = format.bytes_per_sample();
let total_bytes = sample_count * self.channels * bytes_per_sample;
let mut data = vec![0u8; total_bytes];
for sample_idx in 0..sample_count {
for ch in 0..self.channels {
let offset = (sample_idx * self.channels + ch) * bytes_per_sample;
let value = planar[ch][sample_idx];
self.write_sample(&mut data, offset, value, format)?;
}
}
Ok(AudioBuffer::Interleaved(Bytes::from(data)))
}
fn f32_to_planar_bytes(
&self,
planar: &[Vec<f32>],
format: SampleFormat,
) -> AudioResult<AudioBuffer> {
let sample_count = planar[0].len();
let bytes_per_sample = format.bytes_per_sample();
let plane_size = sample_count * bytes_per_sample;
let mut planes = Vec::with_capacity(self.channels);
for ch in 0..self.channels {
let mut plane_data = vec![0u8; plane_size];
for sample_idx in 0..sample_count {
let offset = sample_idx * bytes_per_sample;
let value = planar[ch][sample_idx];
self.write_sample(&mut plane_data, offset, value, format)?;
}
planes.push(Bytes::from(plane_data));
}
Ok(AudioBuffer::Planar(planes))
}
#[allow(clippy::cast_possible_truncation)]
fn write_sample(
&self,
data: &mut [u8],
offset: usize,
value: f32,
format: SampleFormat,
) -> AudioResult<()> {
let bytes_per_sample = format.bytes_per_sample();
if offset + bytes_per_sample > data.len() {
return Err(AudioError::InvalidData(
"Sample offset out of bounds".into(),
));
}
match format {
SampleFormat::U8 => {
let sample = ((value.clamp(-1.0, 1.0) * 128.0) + 128.0) as u8;
data[offset] = sample;
}
SampleFormat::S16 | SampleFormat::S16p => {
let sample = (value.clamp(-1.0, 1.0) * 32767.0) as i16;
let bytes = sample.to_le_bytes();
data[offset..offset + 2].copy_from_slice(&bytes);
}
SampleFormat::S32 | SampleFormat::S32p => {
let sample = (value.clamp(-1.0, 1.0) * 2_147_483_647.0) as i32;
let bytes = sample.to_le_bytes();
data[offset..offset + 4].copy_from_slice(&bytes);
}
SampleFormat::F32 | SampleFormat::F32p => {
let bytes = value.to_le_bytes();
data[offset..offset + 4].copy_from_slice(&bytes);
}
SampleFormat::F64 | SampleFormat::F64p => {
let bytes = f64::from(value).to_le_bytes();
data[offset..offset + 8].copy_from_slice(&bytes);
}
_ => {
return Err(AudioError::UnsupportedFormat(format!(
"Unsupported sample format: {format}"
)))
}
}
Ok(())
}
#[must_use]
pub fn is_passthrough(&self) -> bool {
self.source_rate == self.target_rate
}
#[must_use]
pub fn ratio(&self) -> f64 {
self.ratio
}
#[must_use]
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
pub fn output_sample_count(&self, input_samples: usize) -> usize {
((input_samples as f64) * self.ratio).ceil() as usize
}
pub fn reset(&mut self) {
for ch in &mut self.input_buffer {
ch.clear();
}
self.buffered_samples = 0;
match &mut self.engine {
ResamplerEngine::Fft(engine) => {
engine.reset();
}
ResamplerEngine::SincAsync(engine) => {
engine.reset();
}
ResamplerEngine::Passthrough => {}
}
}
#[must_use]
pub const fn source_rate(&self) -> u32 {
self.source_rate
}
#[must_use]
pub const fn target_rate(&self) -> u32 {
self.target_rate
}
#[must_use]
pub const fn channels(&self) -> usize {
self.channels
}
#[must_use]
pub const fn quality(&self) -> ResamplerQuality {
self.quality
}
}
#[must_use]
const fn gcd(mut a: u32, mut b: u32) -> u32 {
while b != 0 {
let temp = b;
b = a % b;
a = temp;
}
a
}
fn map_rubato_error(err: ResamplerConstructionError) -> AudioError {
AudioError::InvalidParameter(format!("Resampler construction failed: {err}"))
}
fn interleaved_owned_to_planar(
interleaved: &dyn Adapter<'_, f32>,
channels: usize,
) -> Vec<Vec<f32>> {
let frames = interleaved.frames();
let mut planar = vec![vec![0.0f32; frames]; channels];
for ch in 0..channels {
interleaved.copy_from_channel_to_slice(ch, 0, &mut planar[ch]);
}
planar
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{AudioBuffer, ChannelLayout};
use bytes::Bytes;
use oximedia_core::SampleFormat;
fn mono_f32_frame(n: usize, value: f32, sample_rate: u32) -> AudioFrame {
let mut bytes = Vec::with_capacity(n * 4);
for _ in 0..n {
bytes.extend_from_slice(&value.to_le_bytes());
}
let mut frame = AudioFrame::new(SampleFormat::F32, sample_rate, ChannelLayout::Mono);
frame.samples = AudioBuffer::Interleaved(Bytes::from(bytes));
frame
}
#[test]
fn test_draft_is_alias_for_low() {
assert_eq!(ResamplerQuality::Draft.canonical(), ResamplerQuality::Low);
}
#[test]
fn test_good_is_alias_for_high() {
assert_eq!(ResamplerQuality::Good.canonical(), ResamplerQuality::High);
}
#[test]
fn test_best_canonical_is_best() {
assert_eq!(ResamplerQuality::Best.canonical(), ResamplerQuality::Best);
}
#[test]
fn test_medium_canonical_is_medium() {
assert_eq!(
ResamplerQuality::Medium.canonical(),
ResamplerQuality::Medium
);
}
#[test]
fn test_low_fft_chunk_size_smallest() {
let low = ResamplerQuality::Low.fft_chunk_size();
let medium = ResamplerQuality::Medium.fft_chunk_size();
let high = ResamplerQuality::High.fft_chunk_size();
assert!(low <= medium, "Low ({low}) should be <= Medium ({medium})");
assert!(
medium <= high,
"Medium ({medium}) should be <= High ({high})"
);
}
#[test]
fn test_best_sinc_len_ge_low() {
let low_params = ResamplerQuality::Low.sinc_params();
let best_params = ResamplerQuality::Best.sinc_params();
assert!(
best_params.sinc_len >= low_params.sinc_len,
"Best sinc_len ({}) should be >= Low ({})",
best_params.sinc_len,
low_params.sinc_len
);
}
#[test]
fn test_draft_fft_chunk_same_as_low() {
assert_eq!(
ResamplerQuality::Draft.fft_chunk_size(),
ResamplerQuality::Low.fft_chunk_size(),
);
}
#[test]
fn test_good_fft_chunk_same_as_high() {
assert_eq!(
ResamplerQuality::Good.fft_chunk_size(),
ResamplerQuality::High.fft_chunk_size(),
);
}
#[test]
fn test_resampler_passthrough_same_rate() {
let r = Resampler::new(48_000, 48_000, 1, ResamplerQuality::Medium)
.expect("passthrough resampler");
assert!(r.is_passthrough());
assert_eq!(r.ratio(), 1.0);
}
#[test]
fn test_resampler_upsample_ratio() {
let r =
Resampler::new(44_100, 48_000, 1, ResamplerQuality::Low).expect("upsample resampler");
assert!(!r.is_passthrough());
let expected = 48_000.0 / 44_100.0;
assert!((r.ratio() - expected).abs() < 1e-6, "ratio mismatch");
}
#[test]
fn test_resampler_construction_draft_quality() {
let r = Resampler::new(44_100, 48_000, 2, ResamplerQuality::Draft);
assert!(
r.is_ok(),
"Draft resampler construction failed: {:?}",
r.err()
);
}
#[test]
fn test_resampler_construction_good_quality() {
let r = Resampler::new(44_100, 48_000, 1, ResamplerQuality::Good);
assert!(
r.is_ok(),
"Good resampler construction failed: {:?}",
r.err()
);
}
#[test]
fn test_resampler_construction_best_quality() {
let r = Resampler::new(44_100, 48_000, 1, ResamplerQuality::Best);
assert!(
r.is_ok(),
"Best resampler construction failed: {:?}",
r.err()
);
}
#[test]
fn test_resampler_passthrough_returns_correct_sample_rate() {
let mut r =
Resampler::new(48_000, 48_000, 1, ResamplerQuality::Medium).expect("passthrough");
let frame = mono_f32_frame(512, 0.5, 48_000);
let out = r.resample(&frame).expect("passthrough resample");
assert_eq!(out.sample_rate, 48_000);
assert_eq!(out.format, SampleFormat::F32);
}
#[test]
fn test_resampler_source_target_rate_accessors() {
let r = Resampler::new(44_100, 48_000, 1, ResamplerQuality::Low).expect("resampler");
assert_eq!(r.source_rate(), 44_100);
assert_eq!(r.target_rate(), 48_000);
assert_eq!(r.channels(), 1);
}
#[test]
fn test_resampler_output_sample_count_upsample() {
let r = Resampler::new(44_100, 48_000, 1, ResamplerQuality::Low).expect("resampler");
let out_count = r.output_sample_count(441);
assert!(out_count >= 480, "expected >= 480, got {out_count}");
assert!(out_count <= 483, "expected <= 483, got {out_count}");
}
#[test]
fn test_resampler_error_on_zero_sample_rate() {
let r = Resampler::new(0, 48_000, 1, ResamplerQuality::Low);
assert!(r.is_err(), "zero source rate should fail");
}
#[test]
fn test_resampler_error_on_zero_channels() {
let r = Resampler::new(44_100, 48_000, 0, ResamplerQuality::Low);
assert!(r.is_err(), "zero channels should fail");
}
#[test]
fn test_resampler_quality_accessor() {
let r = Resampler::new(44_100, 48_000, 1, ResamplerQuality::Best).expect("resampler");
assert_eq!(r.quality(), ResamplerQuality::Best);
}
#[test]
fn test_low_sub_chunks_le_high() {
let low_sc = ResamplerQuality::Low.fft_sub_chunks();
let high_sc = ResamplerQuality::High.fft_sub_chunks();
assert!(
low_sc <= high_sc,
"Low sub_chunks ({low_sc}) <= High ({high_sc})"
);
}
}
pub mod sample_rates {
pub const RATE_8000: u32 = 8000;
pub const RATE_11025: u32 = 11025;
pub const RATE_16000: u32 = 16000;
pub const RATE_22050: u32 = 22050;
pub const RATE_32000: u32 = 32000;
pub const RATE_44100: u32 = 44100;
pub const RATE_48000: u32 = 48000;
pub const RATE_88200: u32 = 88200;
pub const RATE_96000: u32 = 96000;
pub const RATE_176400: u32 = 176400;
pub const RATE_192000: u32 = 192000;
}