1#![allow(
2 clippy::cast_lossless,
3 clippy::cast_possible_truncation,
4 clippy::cast_precision_loss,
5 clippy::cast_sign_loss,
6 clippy::needless_range_loop
7)]
8
9use crate::{ProcessCtx, Processor, StateBlob, StateError};
10
11#[derive(Clone, Debug)]
26pub struct TimeStretchProcessor {
27 rate: f32,
28 pitch: f32,
29 history: [Vec<f32>; 2],
30 write: usize,
31 sweep: f64,
34 window: usize,
35 bypass: bool,
36}
37
38impl TimeStretchProcessor {
39 #[must_use]
42 pub fn new(rate: f32, pitch: f32) -> Self {
43 Self {
44 rate: ratio(rate),
45 pitch: ratio(pitch),
46 history: [Vec::new(), Vec::new()],
47 write: 0,
48 sweep: 0.0,
49 window: 0,
50 bypass: false,
51 }
52 }
53 pub fn set_ratios(&mut self, rate: f32, pitch: f32) {
56 self.rate = ratio(rate);
57 self.pitch = ratio(pitch);
58 }
59 pub fn set_bypass(&mut self, bypass: bool) {
62 self.bypass = bypass;
63 }
64 fn centre(&self) -> f64 {
67 self.window as f64 / 2.0
68 }
69 fn read(&self, channel: usize, delay: f64) -> f32 {
72 let history = &self.history[channel];
73 let capacity = history.len();
74 let position = (self.write as f64 + capacity as f64 - delay).rem_euclid(capacity as f64);
75 let first = position.floor() as usize % capacity;
76 let second = (first + 1) % capacity;
77 let fraction = (position - position.floor()) as f32;
78 history[first] + (history[second] - history[first]) * fraction
79 }
80}
81
82fn weight(sweep: f64, window: f64) -> f32 {
85 (1.0 - (2.0 * sweep / window - 1.0).abs()) as f32
86}
87
88impl Processor for TimeStretchProcessor {
89 fn prepare(&mut self, sample_rate: u32, _max_block: usize) {
90 self.window = (usize::try_from(sample_rate).unwrap_or(48_000) / 50).max(32);
91 let capacity = self.window * 2;
93 self.history = [vec![0.0; capacity], vec![0.0; capacity]];
94 self.write = 0;
95 self.sweep = self.centre();
96 }
97 fn process(&mut self, _ctx: &ProcessCtx, inputs: &[&[f32]], outputs: &mut [&mut [f32]]) {
98 let channels = inputs.len().min(outputs.len()).min(2);
99 let frames = inputs
100 .iter()
101 .take(channels)
102 .map(|v| v.len())
103 .chain(outputs.iter().take(channels).map(|v| v.len()))
104 .min()
105 .unwrap_or(0);
106 let capacity = self.history[0].len();
107 if capacity == 0 {
108 return;
109 }
110 let window = self.window as f64;
111 let half = window / 2.0;
112 let drift = 1.0 - f64::from(self.pitch) / f64::from(self.rate);
115 for frame in 0..frames {
116 for channel in 0..channels {
117 let input = inputs[channel][frame];
118 self.history[channel][self.write] = if input.is_finite() { input } else { 0.0 };
121 }
122 let first = self.sweep;
123 let second = (first + half).rem_euclid(window);
124 let (first_weight, second_weight) = (weight(first, window), weight(second, window));
125 for channel in 0..channels {
126 outputs[channel][frame] = if self.bypass {
127 self.history[channel][(self.write + capacity - self.window) % capacity]
128 } else {
129 self.read(channel, half + first) * first_weight
130 + self.read(channel, half + second) * second_weight
131 };
132 }
133 self.sweep = (self.sweep + drift).rem_euclid(window);
134 if self.sweep >= window {
136 self.sweep = 0.0;
137 }
138 self.write = (self.write + 1) % capacity;
139 }
140 }
141 fn reset(&mut self) {
142 for channel in &mut self.history {
143 channel.fill(0.0);
144 }
145 self.write = 0;
146 self.sweep = self.centre();
147 }
148 fn latency_samples(&self) -> u32 {
151 u32::try_from(self.window).unwrap_or(u32::MAX)
152 }
153 fn save_state(&self) -> StateBlob {
154 let mut bytes =
155 Vec::with_capacity(16 + self.history.iter().map(Vec::len).sum::<usize>() * 4);
156 bytes.extend_from_slice(&(u64::try_from(self.write).unwrap_or(0)).to_le_bytes());
157 bytes.extend_from_slice(&self.sweep.to_le_bytes());
158 for channel in &self.history {
159 for sample in channel {
160 bytes.extend_from_slice(&sample.to_le_bytes());
161 }
162 }
163 StateBlob::new(bytes)
164 }
165 fn load_state(&mut self, state: &StateBlob) -> Result<(), StateError> {
166 let bytes = state.as_bytes();
167 let expected = 16 + self.history.iter().map(Vec::len).sum::<usize>() * 4;
168 if bytes.len() != expected {
169 return Err(StateError::Mismatch);
170 }
171 let write = usize::try_from(u64::from_le_bytes(
172 bytes[0..8].try_into().expect("fixed range"),
173 ))
174 .ok();
175 let sweep = f64::from_le_bytes(bytes[8..16].try_into().expect("fixed range"));
176 let samples = bytes[16..]
177 .as_chunks::<4>()
178 .0
179 .iter()
180 .map(|word| f32::from_le_bytes(*word));
181 if write.is_none_or(|value| value >= self.history[0].len())
182 || !(0.0..self.window as f64).contains(&sweep)
183 || samples.clone().any(|sample| !sample.is_finite())
184 {
185 return Err(StateError::Invalid);
186 }
187 self.write = write.unwrap_or(0);
188 self.sweep = sweep;
189 for (target, sample) in self.history.iter_mut().flatten().zip(samples) {
190 *target = sample;
191 }
192 Ok(())
193 }
194}
195
196fn ratio(value: f32) -> f32 {
197 if value.is_finite() {
198 value.clamp(0.25, 4.0)
199 } else {
200 1.0
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::TimeStretchProcessor;
207 use crate::{ProcessCtx, Processor, TransportState};
208 use alpha2_time::MediaTime;
209
210 fn context(sample_rate: u32) -> ProcessCtx {
211 ProcessCtx {
212 position: MediaTime::ZERO,
213 sample_position: 0,
214 sample_rate,
215 transport: TransportState::Playing,
216 }
217 }
218
219 fn run(processor: &mut TimeStretchProcessor, sample_rate: u32, input: &[f32]) -> Vec<f32> {
221 processor.prepare(sample_rate, 512);
222 let mut output = vec![0.0; input.len()];
223 for (input, output) in input.chunks(512).zip(output.chunks_mut(512)) {
224 processor.process(&context(sample_rate), &[input], &mut [output]);
225 }
226 output
227 }
228
229 fn largest_step(samples: &[f32]) -> f32 {
230 samples
231 .windows(2)
232 .map(|pair| (pair[1] - pair[0]).abs())
233 .fold(0.0, f32::max)
234 }
235
236 #[test]
237 #[allow(clippy::cast_precision_loss)]
238 fn a_transposed_tone_moves_as_smoothly_as_the_tone_it_becomes() {
239 for (rate, pitch, frequency) in [
242 (1.0, 1.2, 37.0),
243 (1.0, 0.8, 37.0),
244 (1.0, 1.2, 440.0),
245 (0.9, 1.2, 37.0),
246 (1.0, 1.05, 220.0),
247 (2.0, 0.5, 97.0),
248 ] {
249 let input = (0..96_000)
250 .map(|index| {
251 0.5 * libm::sinf(core::f32::consts::TAU * frequency * index as f32 / 48_000.0)
252 })
253 .collect::<Vec<_>>();
254 let mut processor = TimeStretchProcessor::new(rate, pitch);
255 let output = run(&mut processor, 48_000, &input);
256 let transposed =
259 0.5 * core::f32::consts::TAU * frequency * (pitch / rate).max(1.0) / 48_000.0;
260 let step = largest_step(&output[4 * 960..]);
261 assert!(
262 step < 2.0 * transposed + 2.0e-3,
263 "rate {rate} pitch {pitch} {frequency} Hz: a step of {step}, the tone's {transposed}"
264 );
265 }
266 }
267
268 #[test]
269 fn an_impulse_comes_out_as_late_as_the_reported_latency_bypassed_or_not() {
270 for sample_rate in [44_100, 48_000, 96_000] {
271 for ratio in [0.25, 0.5, 1.0, 1.5, 2.0, 4.0] {
272 for bypass in [false, true] {
273 let mut input = vec![0.0; 16_384];
274 input[1_000] = 1.0;
275 let mut processor = TimeStretchProcessor::new(ratio, ratio);
276 processor.set_bypass(bypass);
277 let output = run(&mut processor, sample_rate, &input);
278 let peak = output
279 .iter()
280 .enumerate()
281 .max_by(|a, b| a.1.abs().total_cmp(&b.1.abs()))
282 .map(|(index, _)| index)
283 .expect("samples");
284 assert_eq!(
285 peak - 1_000,
286 processor.latency_samples() as usize,
287 "{sample_rate} Hz, ratio {ratio}, bypass {bypass}"
288 );
289 assert_eq!(output[peak].to_bits(), 1.0_f32.to_bits());
290 }
291 }
292 }
293 }
294
295 #[test]
296 fn changing_the_ratios_keeps_the_latency() {
297 let mut processor = TimeStretchProcessor::new(1.0, 1.0);
298 processor.prepare(48_000, 512);
299 let latency = processor.latency_samples();
300 processor.set_ratios(0.5, 2.0);
301 assert_eq!(processor.latency_samples(), latency);
302 processor.set_ratios(4.0, 0.25);
303 assert_eq!(processor.latency_samples(), latency);
304 }
305}