1use std::f32::consts::PI;
14
15pub const STRETCH_SEGMENT_GRAINS: i64 = 32;
17
18pub const STRETCH_WARMUP_GRAINS: i64 = 8;
20
21#[derive(Clone, Copy, Debug)]
23pub struct StretchSignal<'a> {
24 pub start: i64,
26 pub samples: &'a [f32],
28}
29
30impl StretchSignal<'_> {
31 fn at(&self, index: i64) -> f32 {
32 let value = index
33 .checked_sub(self.start)
34 .and_then(|at| usize::try_from(at).ok())
35 .and_then(|at| self.samples.get(at).copied())
36 .unwrap_or(0.0);
37 if value.is_finite() { value } else { 0.0 }
38 }
39}
40
41#[derive(Clone, Debug)]
43pub struct Stretch {
44 hop: i64,
45 tolerance: i64,
46 window: Vec<f32>,
47}
48
49impl Stretch {
50 #[must_use]
52 pub fn new(sample_rate: u32) -> Self {
53 let rate = f64::from(sample_rate.max(1));
54 #[allow(clippy::cast_possible_truncation)]
55 let hop = ((rate * 0.020).round() as i64).max(1);
56 #[allow(clippy::cast_possible_truncation)]
57 let tolerance = (rate * 0.010).round() as i64;
58 let length = 2 * hop;
59 #[allow(clippy::cast_precision_loss)]
60 let window = (0..length)
61 .map(|index| {
62 let value = libm::sinf(PI * index as f32 / length as f32);
63 value * value
64 })
65 .collect();
66 Self {
67 hop,
68 tolerance,
69 window,
70 }
71 }
72
73 #[must_use]
75 pub const fn hop(&self) -> i64 {
76 self.hop
77 }
78
79 #[must_use]
81 pub const fn tolerance(&self) -> i64 {
82 self.tolerance
83 }
84
85 #[must_use]
87 pub const fn window(&self) -> i64 {
88 2 * self.hop
89 }
90
91 #[must_use]
93 pub fn grains(&self, first: i64, end: i64) -> (i64, i64) {
94 (
95 first.div_euclid(self.hop) - 1,
96 (end - 1).div_euclid(self.hop),
97 )
98 }
99
100 #[must_use]
102 pub const fn segment(grain: i64) -> i64 {
103 grain.div_euclid(STRETCH_SEGMENT_GRAINS)
104 }
105
106 #[must_use]
108 pub const fn chain(segment: i64) -> (i64, i64) {
109 let first = segment * STRETCH_SEGMENT_GRAINS;
110 (
111 first - STRETCH_WARMUP_GRAINS,
112 first + STRETCH_SEGMENT_GRAINS - 1,
113 )
114 }
115
116 pub fn reach(&self, first: i64, last: i64, position: impl Fn(i64) -> i64) -> (i64, i64) {
119 let span = self.window() + self.tolerance;
120 let mut from = i64::MAX;
121 let mut to = i64::MIN;
122 let mut previous = None;
123 for grain in first..=last {
124 let at = position(grain);
125 from = from.min(at - self.tolerance);
126 to = to.max(at + span);
127 if let Some(previous) = previous {
128 to = to.max(previous + self.hop + span);
129 }
130 previous = Some(at);
131 }
132 (from, to)
133 }
134
135 pub fn reach_output(&self, first: i64, end: i64, position: impl Fn(i64) -> i64) -> (i64, i64) {
138 let (low, high) = self.grains(first, end);
139 let mut reach = (i64::MAX, i64::MIN);
140 for segment in Self::segment(low)..=Self::segment(high) {
141 let (from, to) = Self::chain(segment);
142 let (from, to) = self.reach(from, to, &position);
143 reach = (reach.0.min(from), reach.1.max(to));
144 }
145 reach
146 }
147
148 pub fn offsets(
150 &self,
151 segment: i64,
152 position: impl Fn(i64) -> i64,
153 mono: StretchSignal<'_>,
154 ) -> Vec<i64> {
155 let (first, last) = Self::chain(segment);
156 let own = segment * STRETCH_SEGMENT_GRAINS;
157 let (from, to) = self.reach(first, last, &position);
158 let boxes: Vec<f32> = (from..to)
160 .map(|index| {
161 ((mono.at(index) + mono.at(index + 1)) + mono.at(index + 2)) + mono.at(index + 3)
162 })
163 .collect();
164 let boxes = StretchSignal {
165 start: from,
166 samples: &boxes,
167 };
168 let mut offsets = Vec::with_capacity(usize::try_from(STRETCH_SEGMENT_GRAINS).unwrap_or(0));
169 let mut read = position(first);
170 for grain in first + 1..=last {
171 let at = position(grain);
172 let offset = self.search(read + self.hop, at, mono, boxes);
173 read = at + offset;
174 if grain >= own {
175 offsets.push(offset);
176 }
177 }
178 offsets
179 }
180
181 fn search(
184 &self,
185 reference: i64,
186 at: i64,
187 mono: StretchSignal<'_>,
188 boxes: StretchSignal<'_>,
189 ) -> i64 {
190 let length = self.window();
191 let coarse = self.tolerance / 4;
192 let mut best = (0, f64::NEG_INFINITY);
193 for step in nearest_first(-coarse, coarse) {
194 let score = correlation(boxes, reference, at + 4 * step, length / 4, 4);
195 if score > best.1 {
196 best = (step, score);
197 }
198 }
199 let centre = 4 * best.0;
200 let mut best = (0, f64::NEG_INFINITY);
201 let low = (centre - 4).max(-self.tolerance);
202 let high = (centre + 4).min(self.tolerance);
203 for offset in nearest_first(low, high) {
204 let score = correlation(mono, reference, at + offset, length, 1);
205 if score > best.1 {
206 best = (offset, score);
207 }
208 }
209 best.0
210 }
211
212 pub fn overlap_add(
215 &self,
216 first: i64,
217 output: &mut [f32],
218 grains: impl IntoIterator<Item = (i64, i64)>,
219 signal: StretchSignal<'_>,
220 ) {
221 let end = first + i64::try_from(output.len()).unwrap_or(i64::MAX);
222 for (grain, read) in grains {
223 let start = grain * self.hop;
224 let from = start.max(first);
225 let to = (start + self.window()).min(end);
226 for sample in from..to {
227 let index = usize::try_from(sample - start).unwrap_or(0);
228 let out = usize::try_from(sample - first).unwrap_or(0);
229 output[out] += self.window[index] * signal.at(read + (sample - start));
230 }
231 }
232 }
233}
234
235#[derive(Clone, Debug)]
237pub struct StretchReader {
238 stretch: Stretch,
239 segments: Vec<(i64, Vec<i64>)>,
240}
241
242impl StretchReader {
243 #[must_use]
245 pub fn new(sample_rate: u32) -> Self {
246 Self {
247 stretch: Stretch::new(sample_rate),
248 segments: Vec::new(),
249 }
250 }
251
252 #[must_use]
254 pub const fn stretch(&self) -> &Stretch {
255 &self.stretch
256 }
257
258 pub fn render(
261 &mut self,
262 first: i64,
263 outputs: &mut [&mut [f32]],
264 position: impl Fn(i64) -> i64,
265 channels: &[StretchSignal<'_>],
266 mono: StretchSignal<'_>,
267 ) {
268 let Some(length) = outputs.first().map(|output| output.len()) else {
269 return;
270 };
271 if length == 0 {
272 return;
273 }
274 let end = first + i64::try_from(length).unwrap_or(i64::MAX);
275 let (low, high) = self.stretch.grains(first, end);
276 let segments = Stretch::segment(low)..=Stretch::segment(high);
277 self.segments
278 .retain(|(segment, _)| segments.contains(segment));
279 for segment in segments.clone() {
280 if !self.segments.iter().any(|(kept, _)| *kept == segment) {
281 let offsets = self.stretch.offsets(segment, &position, mono);
282 self.segments.push((segment, offsets));
283 }
284 }
285 let offset = |grain: i64| {
286 let segment = Stretch::segment(grain);
287 let index = grain - segment * STRETCH_SEGMENT_GRAINS;
288 self.segments
289 .iter()
290 .find(|(kept, _)| *kept == segment)
291 .and_then(|(_, offsets)| offsets.get(usize::try_from(index).ok()?).copied())
292 .unwrap_or(0)
293 };
294 let reads: Vec<(i64, i64)> = (low..=high)
295 .map(|grain| (grain, position(grain) + offset(grain)))
296 .collect();
297 for (output, signal) in outputs.iter_mut().zip(channels) {
298 self.stretch
299 .overlap_add(first, output, reads.iter().copied(), *signal);
300 }
301 }
302}
303
304fn nearest_first(low: i64, high: i64) -> impl Iterator<Item = i64> {
306 let mut values: Vec<i64> = (low..=high).collect();
307 values.sort_by_key(|&value| (value.abs(), value));
308 values.into_iter()
309}
310
311fn correlation(signal: StretchSignal<'_>, a: i64, b: i64, count: i64, stride: i64) -> f64 {
314 let mut dot = 0.0_f64;
315 let mut energy = 0.0_f64;
316 for index in 0..count {
317 let x = f64::from(signal.at(a + index * stride));
318 let y = f64::from(signal.at(b + index * stride));
319 dot += x * y;
320 energy += y * y;
321 }
322 if energy > 0.0 {
323 dot / energy.sqrt()
324 } else {
325 0.0
326 }
327}
328
329#[cfg(test)]
330#[path = "stretch_tests.rs"]
331mod tests;