use std::collections::VecDeque;
use std::f32::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum Error {
#[error("sample rate must be greater than zero")]
InvalidSampleRate,
#[error("channel count must be greater than zero")]
InvalidChannels,
#[error("interleaved sample count {len} is not a multiple of {channels} channels")]
UnalignedInput {
len: usize,
channels: u16,
},
}
pub type Result<T> = core::result::Result<T, Error>;
pub const MIN_TEMPO: f32 = 0.25;
pub const MAX_TEMPO: f32 = 4.0;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Config {
pub hop_ms: f32,
pub search_ms: f32,
}
impl Default for Config {
fn default() -> Self {
Self {
hop_ms: 15.0,
search_ms: 12.0,
}
}
}
#[derive(Debug, Clone)]
pub struct TimeStretch {
sample_rate: u32,
channels: usize,
tempo: f32,
hop: usize, frame: usize, search: usize, window: Vec<f32>,
input: Vec<f32>,
origin: usize,
primed: bool,
draining: bool,
ideal: f64, last_src: usize, accum: Vec<f32>,
output: VecDeque<f32>,
}
impl TimeStretch {
pub fn new(sample_rate: u32, channels: u16) -> Result<Self> {
Self::with_config(sample_rate, channels, Config::default())
}
pub fn with_config(sample_rate: u32, channels: u16, config: Config) -> Result<Self> {
if sample_rate == 0 {
return Err(Error::InvalidSampleRate);
}
if channels == 0 {
return Err(Error::InvalidChannels);
}
let hop = ((sample_rate as f32 * config.hop_ms / 1000.0).round() as usize).max(1);
let frame = hop * 2;
let search = ((sample_rate as f32 * config.search_ms / 1000.0).round() as usize).max(1);
let window = hann(frame);
let channels = channels as usize;
Ok(Self {
sample_rate,
channels,
tempo: 1.0,
hop,
frame,
search,
window,
input: Vec::new(),
origin: 0,
primed: false,
draining: false,
ideal: 0.0,
last_src: 0,
accum: vec![0.0; hop * channels],
output: VecDeque::new(),
})
}
pub fn set_tempo(&mut self, tempo: f32) {
if tempo.is_finite() && tempo > 0.0 {
self.tempo = tempo.clamp(MIN_TEMPO, MAX_TEMPO);
}
}
pub fn tempo(&self) -> f32 {
self.tempo
}
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
pub fn channels(&self) -> u16 {
self.channels as u16
}
pub fn push(&mut self, samples: &[f32]) {
self.input.extend_from_slice(samples);
}
pub fn available(&self) -> usize {
self.output.len()
}
pub fn buffered(&self) -> usize {
self.input.len() / self.channels
}
pub fn pull(&mut self, max: usize) -> Vec<f32> {
while self.output.len() < max {
if !self.step() {
break;
}
}
let n = max.min(self.output.len()) / self.channels * self.channels;
self.output.drain(0..n).collect()
}
pub fn flush(&mut self) -> Vec<f32> {
self.draining = true;
while self.step() {}
if self.primed {
for i in 0..self.hop {
for c in 0..self.channels {
self.output.push_back(self.accum[i * self.channels + c]);
}
}
self.primed = false;
for x in &mut self.accum {
*x = 0.0;
}
}
self.draining = false;
self.output.drain(..).collect()
}
pub fn reset(&mut self) {
self.input.clear();
self.output.clear();
self.origin = 0;
self.primed = false;
self.draining = false;
self.ideal = 0.0;
self.last_src = 0;
for x in &mut self.accum {
*x = 0.0;
}
}
#[inline]
fn at(&self, pos: usize, channel: usize) -> f32 {
self.input[(pos - self.origin) * self.channels + channel]
}
#[inline]
fn avail_end(&self) -> usize {
self.origin + self.input.len() / self.channels
}
#[inline]
fn analysis_hop(&self) -> f64 {
self.hop as f64 * self.tempo as f64
}
fn step(&mut self) -> bool {
let ch = self.channels;
let ss = self.hop;
let frame = self.frame;
let avail_end = self.avail_end();
if !self.primed {
if self.origin + frame > avail_end {
return false;
}
let src = self.origin;
for i in 0..ss {
for c in 0..ch {
self.output.push_back(self.at(src + i, c) * self.window[i]);
}
}
for i in 0..ss {
for c in 0..ch {
self.accum[i * ch + c] = self.at(src + ss + i, c) * self.window[ss + i];
}
}
self.last_src = src;
self.ideal = src as f64 + self.analysis_hop();
self.primed = true;
self.compact();
return true;
}
let base = self.ideal.round() as i64;
let cmin = self.origin as i64;
let cmax = avail_end as i64 - frame as i64;
let target = self.last_src + ss;
let needed_end = (base + self.search as i64 + frame as i64).max((target + ss) as i64);
if !self.draining && needed_end > avail_end as i64 {
return false;
}
if cmax < cmin || target + ss > avail_end {
return false;
}
let lo = (base - self.search as i64).max(cmin);
let hi = (base + self.search as i64).min(cmax);
if hi < lo {
return false;
}
let mut best = lo as usize;
let mut best_score = f32::NEG_INFINITY;
for cand in lo..=hi {
let cand = cand as usize;
let mut score = 0.0f32;
for i in 0..ss {
for c in 0..ch {
score += self.at(cand + i, c) * self.at(target + i, c);
}
}
if score > best_score {
best_score = score;
best = cand;
}
}
let src = best;
for i in 0..ss {
for c in 0..ch {
let v = self.accum[i * ch + c] + self.at(src + i, c) * self.window[i];
self.output.push_back(v);
}
}
for i in 0..ss {
for c in 0..ch {
self.accum[i * ch + c] = self.at(src + ss + i, c) * self.window[ss + i];
}
}
self.last_src = src;
self.ideal += self.analysis_hop();
self.compact();
true
}
fn compact(&mut self) {
let next_cand_lo = (self.ideal.round() as i64 - self.search as i64).max(0) as usize;
let keep_from = self.last_src.min(next_cand_lo);
if keep_from > self.origin + (1 << 16) {
let drop = (keep_from - self.origin) * self.channels;
self.input.drain(0..drop);
self.origin = keep_from;
}
}
}
fn hann(n: usize) -> Vec<f32> {
(0..n)
.map(|i| 0.5 - 0.5 * (2.0 * PI * i as f32 / n as f32).cos())
.collect()
}
pub fn stretch(samples: &[f32], sample_rate: u32, channels: u16, tempo: f32) -> Result<Vec<f32>> {
if channels == 0 {
return Err(Error::InvalidChannels);
}
if !samples.len().is_multiple_of(channels as usize) {
return Err(Error::UnalignedInput {
len: samples.len(),
channels,
});
}
let mut ts = TimeStretch::new(sample_rate, channels)?;
ts.set_tempo(tempo);
ts.push(samples);
let mut out = ts.pull(usize::MAX);
out.extend(ts.flush());
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_bad_config() {
assert_eq!(
TimeStretch::new(0, 2).unwrap_err(),
Error::InvalidSampleRate
);
assert_eq!(
TimeStretch::new(44_100, 0).unwrap_err(),
Error::InvalidChannels
);
assert_eq!(
stretch(&[0.0; 3], 8000, 2, 1.0).unwrap_err(),
Error::UnalignedInput {
len: 3,
channels: 2
}
);
}
#[test]
fn set_tempo_clamps_and_ignores_bad() {
let mut ts = TimeStretch::new(44_100, 1).unwrap();
assert_eq!(ts.tempo(), 1.0);
ts.set_tempo(1.5);
assert_eq!(ts.tempo(), 1.5);
ts.set_tempo(100.0);
assert_eq!(ts.tempo(), MAX_TEMPO);
ts.set_tempo(0.01);
assert_eq!(ts.tempo(), MIN_TEMPO);
ts.set_tempo(-1.0);
assert_eq!(ts.tempo(), MIN_TEMPO); ts.set_tempo(f32::NAN);
assert_eq!(ts.tempo(), MIN_TEMPO); }
#[test]
fn empty_and_tiny_inputs_do_not_panic() {
assert!(stretch(&[], 44_100, 1, 1.5).unwrap().is_empty());
let out = stretch(&[0.1, -0.1, 0.2], 44_100, 1, 2.0).unwrap();
assert!(out.iter().all(|s| s.is_finite()));
}
}