use rubato::audioadapter_buffers::direct::InterleavedSlice;
use rubato::{
Async, FixedAsync, Indexing, Resampler, SincInterpolationParameters, SincInterpolationType,
WindowFunction,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PcmFormat {
pub sample_rate: u32,
pub channels: u16,
}
pub(crate) struct PcmConverter {
format: PcmFormat,
channels: usize,
resampler: Option<MonoResampler>,
remainder: Vec<f32>,
mono: Vec<f32>,
}
impl PcmConverter {
pub(crate) fn new(format: PcmFormat, target_rate: u32) -> Result<Self, String> {
let channels = usize::from(format.channels.max(1));
let resampler = if format.sample_rate == target_rate {
None
} else {
Some(MonoResampler::new(format.sample_rate, target_rate)?)
};
Ok(PcmConverter {
format,
channels,
resampler,
remainder: Vec::new(),
mono: Vec::new(),
})
}
pub(crate) fn matches(&self, format: PcmFormat) -> bool {
self.format == format
}
pub(crate) fn convert(
&mut self,
interleaved: &[f32],
out: &mut Vec<f32>,
) -> Result<(), String> {
self.remainder.extend_from_slice(interleaved);
let frames = self.remainder.len() / self.channels;
let used = frames * self.channels;
self.mono.clear();
downmix_to_mono(&self.remainder[..used], self.channels, &mut self.mono);
self.remainder.drain(..used);
match &mut self.resampler {
None => out.extend_from_slice(&self.mono),
Some(rs) => rs.push(&self.mono, out)?,
}
Ok(())
}
}
fn downmix_to_mono(src: &[f32], channels: usize, out: &mut Vec<f32>) {
if channels <= 1 {
out.extend_from_slice(src);
return;
}
let inv = 1.0 / channels as f32;
for frame in src.chunks_exact(channels) {
let sum: f32 = frame.iter().sum();
out.push(sum * inv);
}
}
struct MonoResampler {
inner: Async<f32>,
chunk_in_frames: usize,
max_out_frames: usize,
in_accum: Vec<f32>,
out_scratch: Vec<f32>,
}
impl MonoResampler {
fn new(in_sr: u32, out_sr: u32) -> Result<Self, String> {
let ratio = out_sr as f64 / in_sr as f64;
let chunk_in_frames = (in_sr as usize / 50).max(64);
let params = SincInterpolationParameters {
sinc_len: 128,
f_cutoff: 0.95,
interpolation: SincInterpolationType::Linear,
oversampling_factor: 128,
window: WindowFunction::BlackmanHarris2,
};
let inner = Async::<f32>::new_sinc(
ratio,
1.0, ¶ms,
chunk_in_frames,
1, FixedAsync::Input,
)
.map_err(|e| format!("rubato sinc resampler construction failed: {e}"))?;
let max_out_frames = inner.output_frames_max();
Ok(MonoResampler {
inner,
chunk_in_frames,
max_out_frames,
in_accum: Vec::with_capacity(chunk_in_frames * 4),
out_scratch: vec![0.0; max_out_frames],
})
}
fn push(&mut self, mono: &[f32], out: &mut Vec<f32>) -> Result<(), String> {
self.in_accum.extend_from_slice(mono);
let step = self.chunk_in_frames;
while self.in_accum.len() >= step {
let in_adapter = InterleavedSlice::new(&self.in_accum[..step], 1, self.chunk_in_frames)
.map_err(|e| format!("rubato interleaved input adapter failed: {e}"))?;
let mut out_adapter =
InterleavedSlice::new_mut(&mut self.out_scratch[..], 1, self.max_out_frames)
.map_err(|e| format!("rubato interleaved output adapter failed: {e}"))?;
let indexing = Indexing {
input_offset: 0,
output_offset: 0,
partial_len: None,
active_channels_mask: None,
};
let (_in_used, out_written) = self
.inner
.process_into_buffer(&in_adapter, &mut out_adapter, Some(&indexing))
.map_err(|e| format!("rubato process_into_buffer failed: {e}"))?;
out.extend_from_slice(&self.out_scratch[..out_written]); self.in_accum.drain(..step);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f32::consts::PI;
#[test]
fn downmix_stereo_is_lr_average() {
let src = [0.5, -0.5, 0.3, 0.3, 1.0, 0.0];
let mut out = Vec::new();
downmix_to_mono(&src, 2, &mut out);
assert_eq!(out, vec![0.0, 0.3, 0.5]);
}
#[test]
fn downmix_quad_is_channel_average() {
let src = [1.0, 2.0, 3.0, 4.0]; let mut out = Vec::new();
downmix_to_mono(&src, 4, &mut out);
assert_eq!(out, vec![2.5]);
}
#[test]
fn downmix_mono_is_copy() {
let src = [0.1, -0.2, 0.3];
let mut out = Vec::new();
downmix_to_mono(&src, 1, &mut out);
assert_eq!(out, src.to_vec());
}
#[test]
fn resample_48k_to_16k_preserves_tone() {
let mut conv = PcmConverter::new(
PcmFormat {
sample_rate: 48_000,
channels: 1,
},
16_000,
)
.unwrap();
let freq = 440.0_f32;
let amp = 0.5_f32;
let mut out = Vec::new();
let total = 48_000 * 2;
let mut i = 0usize;
while i < total {
let take = 441.min(total - i);
let block: Vec<f32> = (0..take)
.map(|k| (2.0 * PI * freq * ((i + k) as f32) / 48_000.0).sin() * amp)
.collect();
conv.convert(&block, &mut out).unwrap();
i += take;
}
assert!(out.len() >= 16_000, "1 秒以上の出力が必要: {}", out.len());
let mid = &out[4_000..4_000 + 16_000];
let crossings = zero_crossings(mid);
assert!(
(876..=884).contains(&crossings),
"16k リサンプル後の周波数がずれた: crossings={crossings}"
);
let got = rms(mid);
let expect = amp / std::f32::consts::SQRT_2;
assert!(
(got - expect).abs() < 0.02,
"16k リサンプル後の RMS がずれた: got={got} expect={expect}"
);
}
#[test]
fn same_rate_stereo_only_downmixes() {
let mut conv = PcmConverter::new(
PcmFormat {
sample_rate: 16_000,
channels: 2,
},
16_000,
)
.unwrap();
assert!(conv.resampler.is_none());
let mut out = Vec::new();
conv.convert(&[0.5, -0.5, 0.2, 0.2], &mut out).unwrap();
assert_eq!(out, vec![0.0, 0.2]);
}
#[test]
fn split_across_partial_frame_matches_bulk() {
let fmt = PcmFormat {
sample_rate: 16_000,
channels: 2,
};
let interleaved: Vec<f32> = (0..2000).map(|i| (i as f32) * 1e-3).collect();
let mut bulk = Vec::new();
PcmConverter::new(fmt, 16_000)
.unwrap()
.convert(&interleaved, &mut bulk)
.unwrap();
let mut split = Vec::new();
let mut conv = PcmConverter::new(fmt, 16_000).unwrap();
for chunk in interleaved.chunks(777) {
conv.convert(chunk, &mut split).unwrap();
}
assert_eq!(bulk, split);
}
fn rms(samples: &[f32]) -> f32 {
let sum_sq: f64 = samples.iter().map(|&x| (x as f64) * (x as f64)).sum();
(sum_sq / samples.len() as f64).sqrt() as f32
}
fn zero_crossings(samples: &[f32]) -> usize {
let mut n = 0;
for w in samples.windows(2) {
if (w[0] < 0.0 && w[1] >= 0.0) || (w[0] >= 0.0 && w[1] < 0.0) {
n += 1;
}
}
n
}
}