#![allow(
clippy::cast_lossless,
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_sign_loss,
clippy::needless_range_loop
)]
use crate::{ProcessCtx, Processor, StateBlob, StateError};
#[derive(Clone, Debug)]
pub struct TimeStretchProcessor {
rate: f32,
pitch: f32,
history: [Vec<f32>; 2],
write: usize,
sweep: f64,
window: usize,
bypass: bool,
}
impl TimeStretchProcessor {
#[must_use]
pub fn new(rate: f32, pitch: f32) -> Self {
Self {
rate: ratio(rate),
pitch: ratio(pitch),
history: [Vec::new(), Vec::new()],
write: 0,
sweep: 0.0,
window: 0,
bypass: false,
}
}
pub fn set_ratios(&mut self, rate: f32, pitch: f32) {
self.rate = ratio(rate);
self.pitch = ratio(pitch);
}
pub fn set_bypass(&mut self, bypass: bool) {
self.bypass = bypass;
}
fn centre(&self) -> f64 {
self.window as f64 / 2.0
}
fn read(&self, channel: usize, delay: f64) -> f32 {
let history = &self.history[channel];
let capacity = history.len();
let position = (self.write as f64 + capacity as f64 - delay).rem_euclid(capacity as f64);
let first = position.floor() as usize % capacity;
let second = (first + 1) % capacity;
let fraction = (position - position.floor()) as f32;
history[first] + (history[second] - history[first]) * fraction
}
}
fn weight(sweep: f64, window: f64) -> f32 {
(1.0 - (2.0 * sweep / window - 1.0).abs()) as f32
}
impl Processor for TimeStretchProcessor {
fn prepare(&mut self, sample_rate: u32, _max_block: usize) {
self.window = (usize::try_from(sample_rate).unwrap_or(48_000) / 50).max(32);
let capacity = self.window * 2;
self.history = [vec![0.0; capacity], vec![0.0; capacity]];
self.write = 0;
self.sweep = self.centre();
}
fn process(&mut self, _ctx: &ProcessCtx, inputs: &[&[f32]], outputs: &mut [&mut [f32]]) {
let channels = inputs.len().min(outputs.len()).min(2);
let frames = inputs
.iter()
.take(channels)
.map(|v| v.len())
.chain(outputs.iter().take(channels).map(|v| v.len()))
.min()
.unwrap_or(0);
let capacity = self.history[0].len();
if capacity == 0 {
return;
}
let window = self.window as f64;
let half = window / 2.0;
let drift = 1.0 - f64::from(self.pitch) / f64::from(self.rate);
for frame in 0..frames {
for channel in 0..channels {
let input = inputs[channel][frame];
self.history[channel][self.write] = if input.is_finite() { input } else { 0.0 };
}
let first = self.sweep;
let second = (first + half).rem_euclid(window);
let (first_weight, second_weight) = (weight(first, window), weight(second, window));
for channel in 0..channels {
outputs[channel][frame] = if self.bypass {
self.history[channel][(self.write + capacity - self.window) % capacity]
} else {
self.read(channel, half + first) * first_weight
+ self.read(channel, half + second) * second_weight
};
}
self.sweep = (self.sweep + drift).rem_euclid(window);
if self.sweep >= window {
self.sweep = 0.0;
}
self.write = (self.write + 1) % capacity;
}
}
fn reset(&mut self) {
for channel in &mut self.history {
channel.fill(0.0);
}
self.write = 0;
self.sweep = self.centre();
}
fn latency_samples(&self) -> u32 {
u32::try_from(self.window).unwrap_or(u32::MAX)
}
fn save_state(&self) -> StateBlob {
let mut bytes =
Vec::with_capacity(16 + self.history.iter().map(Vec::len).sum::<usize>() * 4);
bytes.extend_from_slice(&(u64::try_from(self.write).unwrap_or(0)).to_le_bytes());
bytes.extend_from_slice(&self.sweep.to_le_bytes());
for channel in &self.history {
for sample in channel {
bytes.extend_from_slice(&sample.to_le_bytes());
}
}
StateBlob::new(bytes)
}
fn load_state(&mut self, state: &StateBlob) -> Result<(), StateError> {
let bytes = state.as_bytes();
let expected = 16 + self.history.iter().map(Vec::len).sum::<usize>() * 4;
if bytes.len() != expected {
return Err(StateError::Mismatch);
}
let write = usize::try_from(u64::from_le_bytes(
bytes[0..8].try_into().expect("fixed range"),
))
.ok();
let sweep = f64::from_le_bytes(bytes[8..16].try_into().expect("fixed range"));
let samples = bytes[16..]
.as_chunks::<4>()
.0
.iter()
.map(|word| f32::from_le_bytes(*word));
if write.is_none_or(|value| value >= self.history[0].len())
|| !(0.0..self.window as f64).contains(&sweep)
|| samples.clone().any(|sample| !sample.is_finite())
{
return Err(StateError::Invalid);
}
self.write = write.unwrap_or(0);
self.sweep = sweep;
for (target, sample) in self.history.iter_mut().flatten().zip(samples) {
*target = sample;
}
Ok(())
}
}
fn ratio(value: f32) -> f32 {
if value.is_finite() {
value.clamp(0.25, 4.0)
} else {
1.0
}
}
#[cfg(test)]
mod tests {
use super::TimeStretchProcessor;
use crate::{ProcessCtx, Processor, TransportState};
use alpha2_time::MediaTime;
fn context(sample_rate: u32) -> ProcessCtx {
ProcessCtx {
position: MediaTime::ZERO,
sample_position: 0,
sample_rate,
transport: TransportState::Playing,
}
}
fn run(processor: &mut TimeStretchProcessor, sample_rate: u32, input: &[f32]) -> Vec<f32> {
processor.prepare(sample_rate, 512);
let mut output = vec![0.0; input.len()];
for (input, output) in input.chunks(512).zip(output.chunks_mut(512)) {
processor.process(&context(sample_rate), &[input], &mut [output]);
}
output
}
fn largest_step(samples: &[f32]) -> f32 {
samples
.windows(2)
.map(|pair| (pair[1] - pair[0]).abs())
.fold(0.0, f32::max)
}
#[test]
#[allow(clippy::cast_precision_loss)]
fn a_transposed_tone_moves_as_smoothly_as_the_tone_it_becomes() {
for (rate, pitch, frequency) in [
(1.0, 1.2, 37.0),
(1.0, 0.8, 37.0),
(1.0, 1.2, 440.0),
(0.9, 1.2, 37.0),
(1.0, 1.05, 220.0),
(2.0, 0.5, 97.0),
] {
let input = (0..96_000)
.map(|index| {
0.5 * libm::sinf(core::f32::consts::TAU * frequency * index as f32 / 48_000.0)
})
.collect::<Vec<_>>();
let mut processor = TimeStretchProcessor::new(rate, pitch);
let output = run(&mut processor, 48_000, &input);
let transposed =
0.5 * core::f32::consts::TAU * frequency * (pitch / rate).max(1.0) / 48_000.0;
let step = largest_step(&output[4 * 960..]);
assert!(
step < 2.0 * transposed + 2.0e-3,
"rate {rate} pitch {pitch} {frequency} Hz: a step of {step}, the tone's {transposed}"
);
}
}
#[test]
fn an_impulse_comes_out_as_late_as_the_reported_latency_bypassed_or_not() {
for sample_rate in [44_100, 48_000, 96_000] {
for ratio in [0.25, 0.5, 1.0, 1.5, 2.0, 4.0] {
for bypass in [false, true] {
let mut input = vec![0.0; 16_384];
input[1_000] = 1.0;
let mut processor = TimeStretchProcessor::new(ratio, ratio);
processor.set_bypass(bypass);
let output = run(&mut processor, sample_rate, &input);
let peak = output
.iter()
.enumerate()
.max_by(|a, b| a.1.abs().total_cmp(&b.1.abs()))
.map(|(index, _)| index)
.expect("samples");
assert_eq!(
peak - 1_000,
processor.latency_samples() as usize,
"{sample_rate} Hz, ratio {ratio}, bypass {bypass}"
);
assert_eq!(output[peak].to_bits(), 1.0_f32.to_bits());
}
}
}
}
#[test]
fn changing_the_ratios_keeps_the_latency() {
let mut processor = TimeStretchProcessor::new(1.0, 1.0);
processor.prepare(48_000, 512);
let latency = processor.latency_samples();
processor.set_ratios(0.5, 2.0);
assert_eq!(processor.latency_samples(), latency);
processor.set_ratios(4.0, 0.25);
assert_eq!(processor.latency_samples(), latency);
}
}