const MIN_PITCH_HZ: f64 = 65.0;
const MAX_PITCH_HZ: f64 = 400.0;
fn find_pitch_period(s: &[i16], min_p: usize, max_p: usize) -> usize {
let max_p = max_p.min(s.len() / 2);
if max_p <= min_p {
return min_p.max(1);
}
let n = max_p;
let mut best_p = min_p;
let mut best_diff = i64::MAX;
for p in min_p..=max_p {
let mut diff = 0i64;
for i in 0..n {
diff += (s[i] as i64 - s[i + p] as i64).abs();
}
if diff < best_diff {
best_diff = diff;
best_p = p;
}
}
best_p
}
fn overlap_add(out: &mut Vec<i16>, down: &[i16], up: &[i16], n: usize) {
for i in 0..n {
let t = (i as f64 + 0.5) / n as f64;
let v = down[i] as f64 * (1.0 - t) + up[i] as f64 * t;
out.push(v.round().clamp(i16::MIN as f64, i16::MAX as f64) as i16);
}
}
pub fn change_tempo(pcm: &[i16], speed: f64, sample_rate: u32) -> Vec<i16> {
let min_p = (sample_rate as f64 / MAX_PITCH_HZ) as usize;
let max_p = (sample_rate as f64 / MIN_PITCH_HZ) as usize;
if !(speed.is_finite()) || (speed - 1.0).abs() < 1e-3 || speed <= 0.0 || pcm.len() < 2 * max_p {
return pcm.to_vec();
}
let mut out: Vec<i16> = Vec::with_capacity((pcm.len() as f64 / speed) as usize + max_p);
let mut pos = 0usize;
while pos + 2 * max_p <= pcm.len() {
let period = find_pitch_period(&pcm[pos..], min_p, max_p).clamp(min_p.max(1), max_p);
let expected = pos as f64 / speed;
if speed > 1.0 && out.len() as f64 > expected {
overlap_add(&mut out, &pcm[pos..], &pcm[pos + period..], period);
pos += 2 * period;
} else if speed < 1.0 && (out.len() as f64) < expected {
out.extend_from_slice(&pcm[pos..pos + period]);
overlap_add(&mut out, &pcm[pos..], &pcm[pos + period..], period);
pos += period;
} else {
out.extend_from_slice(&pcm[pos..pos + period]);
pos += period;
}
}
out.extend_from_slice(&pcm[pos..]);
out
}
fn resample_linear(s: &[i16], target_len: usize) -> Vec<i16> {
if s.is_empty() || target_len == 0 {
return Vec::new();
}
if s.len() == target_len {
return s.to_vec();
}
let last = (s.len() - 1) as f64;
let denom = (target_len - 1).max(1) as f64;
(0..target_len)
.map(|i| {
let src = i as f64 * last / denom;
let idx = src.floor() as usize;
let frac = src - idx as f64;
let a = s[idx] as f64;
let b = s[(idx + 1).min(s.len() - 1)] as f64;
(a + (b - a) * frac).round().clamp(i16::MIN as f64, i16::MAX as f64) as i16
})
.collect()
}
pub fn change_pitch(pcm: &[i16], factor: f64, sample_rate: u32) -> Vec<i16> {
if !factor.is_finite() || (factor - 1.0).abs() < 1e-3 || factor <= 0.0 || pcm.is_empty() {
return pcm.to_vec();
}
let stretched = change_tempo(pcm, 1.0 / factor, sample_rate);
resample_linear(&stretched, pcm.len())
}
#[cfg(test)]
mod tests {
use super::*;
const SR: u32 = 22050;
fn sine(freq: f64, n: usize) -> Vec<i16> {
(0..n)
.map(|i| {
let t = i as f64 / SR as f64;
(10_000.0 * (2.0 * std::f64::consts::PI * freq * t).sin()) as i16
})
.collect()
}
fn est_freq(s: &[i16]) -> f64 {
let mut crossings = Vec::new();
for i in 1..s.len() {
if s[i - 1] < 0 && s[i] >= 0 {
crossings.push(i);
}
}
if crossings.len() < 2 {
return 0.0;
}
let span = (crossings[crossings.len() - 1] - crossings[0]) as f64;
let periods = (crossings.len() - 1) as f64;
SR as f64 * periods / span
}
#[test]
fn speed_one_is_unchanged() {
let s = sine(200.0, 8000);
assert_eq!(change_tempo(&s, 1.0, SR), s);
}
#[test]
fn faster_shortens_but_keeps_pitch() {
let s = sine(200.0, 20_000);
let fast = change_tempo(&s, 2.0, SR);
let ratio = fast.len() as f64 / s.len() as f64;
assert!((ratio - 0.5).abs() < 0.12, "expected ~0.5x length, got {ratio:.3}");
let f = est_freq(&fast);
assert!((f - 200.0).abs() < 20.0, "pitch should stay ~200 Hz, got {f:.0}");
}
#[test]
fn slower_lengthens_but_keeps_pitch() {
let s = sine(150.0, 20_000);
let slow = change_tempo(&s, 0.5, SR);
let ratio = slow.len() as f64 / s.len() as f64;
assert!((ratio - 2.0).abs() < 0.25, "expected ~2x length, got {ratio:.3}");
let f = est_freq(&slow);
assert!((f - 150.0).abs() < 15.0, "pitch should stay ~150 Hz, got {f:.0}");
}
#[test]
fn short_or_degenerate_input_is_passthrough() {
let s = sine(200.0, 100); assert_eq!(change_tempo(&s, 2.0, SR), s);
let s2 = sine(200.0, 20_000);
assert_eq!(change_tempo(&s2, 0.0, SR), s2); }
#[test]
fn raise_pitch_keeps_duration() {
let s = sine(200.0, 20_000);
let up = change_pitch(&s, 1.5, SR);
assert_eq!(up.len(), s.len(), "pitch shift must preserve length");
let f = est_freq(&up);
assert!((f - 300.0).abs() < 30.0, "expected ~300 Hz, got {f:.0}");
}
#[test]
fn lower_pitch_keeps_duration() {
let s = sine(300.0, 20_000);
let down = change_pitch(&s, 0.5, SR);
assert_eq!(down.len(), s.len());
let f = est_freq(&down);
assert!((f - 150.0).abs() < 20.0, "expected ~150 Hz, got {f:.0}");
}
#[test]
fn pitch_factor_one_is_unchanged() {
let s = sine(200.0, 8000);
assert_eq!(change_pitch(&s, 1.0, SR), s);
}
}