use core::mem::MaybeUninit;
#[cfg(target_arch = "aarch64")]
use core::arch::aarch64::{
float32x4_t, float64x2_t, vdupq_n_f64, vfmaq_f32, vmulq_f64, vst1q_f32, vsubq_f32,
};
#[inline]
#[doc(hidden)]
pub fn resample_linear_scalar(out: &mut [MaybeUninit<f32>], samples: &[f32], ratio: f64) {
assert!(
!samples.is_empty(),
"resample_linear_scalar: samples must be non-empty"
);
let last_in = samples.len() - 1;
for (i, slot) in out.iter_mut().enumerate() {
let x = i as f64 * ratio;
let lo = x.floor();
let frac = x - lo;
let lo_idx = (lo as usize).min(last_in);
let hi_idx = (lo_idx + 1).min(last_in);
let a = samples[lo_idx];
let b = samples[hi_idx];
slot.write(a + (b - a) * frac as f32);
}
}
#[cfg(target_arch = "aarch64")]
#[inline]
#[target_feature(enable = "neon")]
unsafe fn resample_linear_neon(out: &mut [MaybeUninit<f32>], samples: &[f32], ratio: f64) {
assert!(
!samples.is_empty(),
"resample_linear_neon: samples must be non-empty"
);
assert!(
out.len().is_multiple_of(4),
"resample_linear_neon: out.len() ({}) must be a multiple of 4 (caller splits the tail)",
out.len(),
);
let last_in = samples.len() - 1;
let body_len = out.len();
unsafe {
let dst_base = out.as_mut_ptr().cast::<f32>();
let ratio_v = vdupq_n_f64(ratio);
let mut i = 0usize;
while i + 4 <= body_len {
let lane0_1 = {
let base = [(i as f64), (i + 1) as f64];
let v = core::arch::aarch64::vld1q_f64(base.as_ptr());
vmulq_f64(v, ratio_v)
};
let lane2_3 = {
let base = [(i + 2) as f64, (i + 3) as f64];
let v = core::arch::aarch64::vld1q_f64(base.as_ptr());
vmulq_f64(v, ratio_v)
};
let extract = |v: float64x2_t, lane: u32| -> f64 {
match lane {
0 => core::arch::aarch64::vgetq_lane_f64::<0>(v),
_ => core::arch::aarch64::vgetq_lane_f64::<1>(v),
}
};
let x_lanes: [f64; 4] = [
extract(lane0_1, 0),
extract(lane0_1, 1),
extract(lane2_3, 0),
extract(lane2_3, 1),
];
let mut lo_idx_lanes = [0usize; 4];
let mut frac_lanes_f64 = [0.0f64; 4];
for j in 0..4 {
let xj = x_lanes[j];
let lo = xj.floor();
frac_lanes_f64[j] = xj - lo;
lo_idx_lanes[j] = (lo as usize).min(last_in);
}
let a_lanes = [
samples[lo_idx_lanes[0]],
samples[lo_idx_lanes[1]],
samples[lo_idx_lanes[2]],
samples[lo_idx_lanes[3]],
];
let b_lanes = [
samples[(lo_idx_lanes[0] + 1).min(last_in)],
samples[(lo_idx_lanes[1] + 1).min(last_in)],
samples[(lo_idx_lanes[2] + 1).min(last_in)],
samples[(lo_idx_lanes[3] + 1).min(last_in)],
];
let frac_lanes_f32: [f32; 4] = [
frac_lanes_f64[0] as f32,
frac_lanes_f64[1] as f32,
frac_lanes_f64[2] as f32,
frac_lanes_f64[3] as f32,
];
let a_v = core::arch::aarch64::vld1q_f32(a_lanes.as_ptr());
let b_v = core::arch::aarch64::vld1q_f32(b_lanes.as_ptr());
let frac_v = core::arch::aarch64::vld1q_f32(frac_lanes_f32.as_ptr());
let diff = vsubq_f32(b_v, a_v);
let result: float32x4_t = vfmaq_f32(a_v, diff, frac_v);
vst1q_f32(dst_base.add(i), result);
i += 4;
}
}
}
#[cfg(target_arch = "aarch64")]
#[inline]
#[target_feature(enable = "neon")]
unsafe fn resample_linear_neon_tail(
out: &mut [MaybeUninit<f32>],
samples: &[f32],
ratio: f64,
i_base: usize,
) {
assert!(
!samples.is_empty(),
"resample_linear_neon_tail: samples must be non-empty"
);
let last_in = samples.len() - 1;
for (j, slot) in out.iter_mut().enumerate() {
let i = i_base + j;
let x = i as f64 * ratio;
let lo = x.floor();
let frac = x - lo;
let lo_idx = (lo as usize).min(last_in);
let hi_idx = (lo_idx + 1).min(last_in);
let a = samples[lo_idx];
let b = samples[hi_idx];
slot.write(a + (b - a) * frac as f32);
}
}
#[inline]
#[doc(hidden)]
pub fn resample_linear(out: &mut [MaybeUninit<f32>], samples: &[f32], ratio: f64) {
assert!(
!samples.is_empty(),
"simd::audio::resample_linear: samples must be non-empty"
);
#[cfg(target_arch = "aarch64")]
{
if crate::simd::is_neon_available() {
let n = out.len();
let body_len = n - (n % 4);
unsafe {
let (body, tail) = out.split_at_mut(body_len);
if body_len > 0 {
resample_linear_neon(body, samples, ratio);
}
if !tail.is_empty() {
resample_linear_neon_tail(tail, samples, ratio, body_len);
}
}
return;
}
}
resample_linear_scalar(out, samples, ratio);
}
#[cfg(test)]
mod tests {
use super::{resample_linear, resample_linear_scalar};
use crate::simd::diff::assert_close_slice_over_lane_sweep;
const RESAMPLE_TOL_ABS: f64 = 1e-6;
const RESAMPLE_TOL_REL: f64 = 1e-6;
fn pair_2x(sweep_len: usize) -> (Vec<f32>, Vec<f32>) {
if sweep_len == 0 {
return (Vec::new(), Vec::new());
}
let mut samples: Vec<f32> = Vec::with_capacity(sweep_len);
for k in 0..sweep_len {
let v = ((k as f32) * 0.1).sin();
samples.push(v);
}
let out_len = sweep_len * 2;
let ratio = 0.5_f64;
let mut s_out: Vec<f32> = Vec::with_capacity(out_len);
let spare_s = s_out.spare_capacity_mut();
resample_linear_scalar(&mut spare_s[..out_len], &samples, ratio);
unsafe { s_out.set_len(out_len) };
let mut d_out: Vec<f32> = Vec::with_capacity(out_len);
let spare_d = d_out.spare_capacity_mut();
resample_linear(&mut spare_d[..out_len], &samples, ratio);
unsafe { d_out.set_len(out_len) };
(s_out, d_out)
}
#[test]
fn resample_linear_scalar_matches_dispatcher_tolerance() {
let s = |xs: &[i32]| {
let n = xs.len();
if n == 0 {
return Vec::new();
}
let (so, _) = pair_2x(n);
so.into_iter().map(|x| x as f64).collect()
};
let d = |xs: &[i32]| {
let n = xs.len();
if n == 0 {
return Vec::new();
}
let (_, dout) = pair_2x(n);
dout.into_iter().map(|x| x as f64).collect()
};
assert_close_slice_over_lane_sweep(
4,
s,
d,
|n| vec![0_i32; n],
RESAMPLE_TOL_ABS,
RESAMPLE_TOL_REL,
);
}
#[test]
fn resample_linear_constant_signal_is_constant() {
let samples = vec![0.5_f32; 32];
let ratio = 0.7_f64;
let out_len = 50;
let mut out: Vec<f32> = Vec::with_capacity(out_len);
let spare = out.spare_capacity_mut();
resample_linear(&mut spare[..out_len], &samples, ratio);
unsafe { out.set_len(out_len) };
for (i, v) in out.iter().enumerate() {
assert!(
(*v - 0.5).abs() < 1e-6,
"constant interpolation at i={i} should be 0.5 (got {v})"
);
}
}
#[test]
fn resample_linear_unit_ratio_copies_samples() {
let samples: Vec<f32> = (0..16).map(|i| i as f32 * 0.1).collect();
let ratio = 1.0_f64;
let mut out: Vec<f32> = Vec::with_capacity(samples.len());
let spare = out.spare_capacity_mut();
resample_linear(&mut spare[..samples.len()], &samples, ratio);
unsafe { out.set_len(samples.len()) };
for (i, (s, d)) in samples.iter().zip(out.iter()).enumerate() {
assert!(
(s - d).abs() < 1e-6,
"unit-ratio resample should copy: i={i} src={s} out={d}"
);
}
}
#[test]
fn resample_linear_single_input_replicates() {
let samples = vec![0.42_f32];
let ratio = 0.3_f64;
let out_len = 17;
let mut out: Vec<f32> = Vec::with_capacity(out_len);
let spare = out.spare_capacity_mut();
resample_linear(&mut spare[..out_len], &samples, ratio);
unsafe { out.set_len(out_len) };
for (i, v) in out.iter().enumerate() {
assert!(
(*v - 0.42).abs() < 1e-6,
"single-sample resample at i={i} should be 0.42 (got {v})"
);
}
}
#[test]
fn resample_linear_first_output_is_first_sample() {
let samples: Vec<f32> = vec![0.1, 0.2, 0.3, 0.4];
let out_len = 4;
let ratio = 0.5_f64;
let mut out: Vec<f32> = Vec::with_capacity(out_len);
let spare = out.spare_capacity_mut();
resample_linear(&mut spare[..out_len], &samples, ratio);
unsafe { out.set_len(out_len) };
assert!((out[0] - 0.1).abs() < 1e-6, "out[0] should be samples[0]");
}
#[test]
#[should_panic(expected = "simd::audio::resample_linear: samples must be non-empty")]
fn resample_linear_panics_on_empty_samples() {
let samples: Vec<f32> = Vec::new();
let out_len = 4;
let mut out: Vec<f32> = Vec::with_capacity(out_len);
let spare = out.spare_capacity_mut();
resample_linear(&mut spare[..out_len], &samples, 0.5);
}
}