1use crate::audio::BeatGrid;
5use crate::cast;
6use crate::dsp::{ModulationSpectrum, SCAN_TOP_HZ, TRACK_RATE_HZ, Track, quantile, snap};
7use crate::notes::NoteList;
8use crate::units::{Bpm, Cents, Decibels, Hz, Seconds};
9
10use super::median;
11use super::track::{HOP, LevelTrack};
12
13pub const PUMP_BINS: usize = 64;
15pub const PUMP_REFERENCE: f64 = 0.9;
18pub const PUMP_BACK: Decibels = Decibels::from_const(1.5);
21pub const PUMP_MIN: Decibels = Decibels::from_const(3.0);
27pub const PUMP_BEATS_MIN: usize = 4;
29pub const PUMP_STRIP_BEATS: usize = 8;
36pub const PHASE_SLACK: usize = 3;
39pub const PHASE_DEPTH: f64 = 0.5;
52pub const PHASE_SHARE: f64 = 0.5;
55pub const PERIOD_RANGE: f64 = 0.025;
66pub const PERIOD_RUN_BEATS: usize = 64;
72pub const FREE_RESIDUAL: Cents = Cents::from_const(50.0);
75pub const FREE_REACH: Cents = Cents::from_const(60.0);
82pub const DUCK_TAIL_BINS: f64 = 3.0;
89pub const DUCK_TAIL_WINDOWS: f64 = 0.5;
97pub const FOLD_ERROR_SHARE: f64 = 0.9;
107pub const FOLD_ERROR_GAIN: Decibels = Decibels::from_const(0.15);
114pub const FREE_SWING: Decibels = Decibels::from_const(1.0);
118pub const SUSTAIN_LEAD: Seconds = Seconds::from_const(0.05);
122pub const FALL_MAX: usize = 8;
131pub const FREE_RATE_MIN: Hz = Hz::from_const(0.5);
133
134#[derive(Clone, Copy, Debug, PartialEq, Eq)]
136pub enum PumpLock {
137 Beat,
139 Note,
141 Free,
143 None,
145}
146
147#[derive(Clone, Debug, PartialEq)]
149pub struct Pump {
150 pub depth: Decibels,
152 pub back: Option<Seconds>,
155 pub tau: Option<Seconds>,
159 pub lock: PumpLock,
161 pub beats: usize,
163 pub curve: Vec<f64>,
166 grid: BeatGrid,
167 falls: Vec<Option<f64>>,
170 fold_error: f64,
174 free_line: Option<f64>,
177}
178
179impl Pump {
180 #[must_use]
210 pub fn find(
211 track: &LevelTrack,
212 grid: BeatGrid,
213 notes: &NoteList,
214 held: &[(f64, f64)],
215 ) -> Option<Self> {
216 let read = Self::read(track, grid, notes, held)?;
217 let Some(fitted) = fit(track, grid, held)
218 .and_then(|g| Self::read(track, g, notes, held))
219 .filter(|p| p.lock == PumpLock::Beat)
220 else {
221 return Some(read);
222 };
223 let replaces = match read.lock {
224 PumpLock::Beat => fitted.replaces_a_beat_lock(&read),
225 PumpLock::Free => {
226 read.free_line
227 .is_some_and(|cents| cents <= FREE_REACH.get())
228 && fitted.has_a_ducks_shape(track.window().get())
229 }
230 PumpLock::None => fitted.has_a_ducks_shape(0.0),
233 PumpLock::Note => true,
234 };
235 Some(if replaces { fitted } else { read })
236 }
237
238 fn replaces_a_beat_lock(&self, read: &Self) -> bool {
241 if self.strips() == read.strips() {
242 folds_better(self.fold_error, read.fold_error)
243 } else {
244 self.strips() && self.fold_error < read.fold_error
245 }
246 }
247
248 fn read(
250 track: &LevelTrack,
251 grid: BeatGrid,
252 notes: &NoteList,
253 held: &[(f64, f64)],
254 ) -> Option<Self> {
255 let period = grid.beat_period().get();
256 let mut beats = Vec::new();
257 let mut onset_beats = Vec::new();
258 let mut odd = Vec::new();
259 let mut longest = (0.0, 0.0);
260 let mut run: Option<(usize, f64)> = None;
261 let mut starts = Vec::new();
262 for b in covered(track, grid, held) {
263 let start = grid.beat_time(b).get();
264 let end = start + period;
265 let first = match run {
266 Some((last, first)) if b == last + 1 => first,
267 _ => start,
268 };
269 run = Some((b, first));
270 let Some(curve) = beat_curve(track, start, period, 1) else {
271 continue;
272 };
273 starts.push(start);
274 if end - first > longest.1 - longest.0 {
275 longest = (first, end);
276 }
277 onset_beats.push(starts_in(notes, start, end));
278 odd.push(b % 2 == 1);
279 beats.push(curve);
280 }
281 if beats.len() < PUMP_BEATS_MIN {
282 return None;
283 }
284 let wide = median_curve(&beats);
285 let curve = wide[PUMP_BINS..].to_vec();
286 let depth = dip(&curve);
287 let low = PUMP_BINS + dip_bin(&curve);
288 let falls: Vec<f64> = beats.iter().map(|b| fall_into(b, low)).collect();
289 let free_line = free_line(track, longest, grid);
290 let free = free_line.is_some_and(|cents| cents >= FREE_RESIDUAL.get());
291 let lock = lock(&beats, &onset_beats, &odd, &falls, &wide, depth, free);
292 let falls = if lock == PumpLock::Beat {
293 let end = track.time(track.db().len()) - grid.offset().get();
294 (0..cast::floor_to_usize(end / period).unwrap_or(0))
295 .map(|b| {
296 beat_curve(track, grid.beat_time(b).get(), period, 1)
297 .map(|c| fall_into(&c, low))
298 })
299 .collect()
300 } else {
301 Vec::new()
302 };
303 let fold_error = fold_error(track, &starts, period, &curve);
304 let back = recovery(&curve).map(|bins| bins * period / cast::usize_to_f64(PUMP_BINS));
305 let tau = back
306 .filter(|_| lock == PumpLock::Beat)
307 .map(|back| back / (depth / PUMP_BACK.get()).ln());
308 Some(Self {
309 depth: Decibels::clamped(depth),
310 back: back.map(Seconds::clamped),
311 tau: tau.map(Seconds::clamped),
312 lock,
313 beats: beats.len(),
314 curve,
315 grid,
316 falls,
317 fold_error,
318 free_line,
319 })
320 }
321
322 #[must_use]
328 pub fn carries(&self, from: f64, to: f64) -> bool {
329 let (period, offset) = (self.grid.beat_period().get(), self.grid.offset().get());
330 let first = cast::floor_to_usize((from - offset) / period).unwrap_or(0);
331 let last = cast::ceil_to_usize((to - offset) / period)
332 .unwrap_or(0)
333 .min(self.falls.len());
334 let falls: Vec<f64> = self.falls[first.min(last)..last]
335 .iter()
336 .flatten()
337 .copied()
338 .collect();
339 median(&falls).is_none_or(|fall| fall >= PHASE_DEPTH * self.depth.get())
340 }
341
342 #[must_use]
346 pub fn strips(&self) -> bool {
347 self.lock == PumpLock::Beat && self.beats >= PUMP_STRIP_BEATS
348 }
349
350 #[must_use]
353 pub fn bpm(&self) -> Bpm {
354 self.grid.bpm()
355 }
356
357 fn has_a_ducks_shape(&self, window: f64) -> bool {
361 let low = dip_bin(&self.curve);
362 let depth = self.depth.get();
363 let first = |at: &dyn Fn(usize) -> usize, level: f64| {
364 (1..PUMP_BINS)
365 .find(|&k| self.curve[at(k) % PUMP_BINS] >= -level * depth)
366 .unwrap_or(PUMP_BINS)
367 };
368 let (after, before) = (|k| low + k, |k| low + PUMP_BINS - k);
369 let rise = first(&after, 0.25);
370 let bins = cast::usize_to_f64(PUMP_BINS) * window / self.grid.beat_period().get();
371 let tail = cast::usize_to_f64(rise.saturating_sub(first(&after, 0.5)));
372 tail >= DUCK_TAIL_BINS + DUCK_TAIL_WINDOWS * bins && rise > first(&before, 0.25)
373 }
374
375 #[must_use]
378 pub fn at(&self, t: f64) -> f64 {
379 let period = self.grid.beat_period().get();
380 let phase = ((t - self.grid.offset().get()) / period).rem_euclid(1.0);
381 let bins = cast::usize_to_f64(PUMP_BINS);
382 let x = (phase * bins - 0.5).rem_euclid(bins);
383 let lo = cast::floor_to_usize(x).unwrap_or(0).min(PUMP_BINS - 1);
384 let hi = (lo + 1) % PUMP_BINS;
385 let w = x - cast::usize_to_f64(lo);
386 self.curve[lo] * (1.0 - w) + self.curve[hi] * w
387 }
388
389 pub fn strip(&self, track: &mut LevelTrack) {
392 let start = track.time(0);
393 for (i, db) in track.db_mut().iter_mut().enumerate() {
394 *db -= self.at(start + cast::usize_to_f64(i) * HOP.get());
395 }
396 }
397}
398
399fn covered(track: &LevelTrack, grid: BeatGrid, held: &[(f64, f64)]) -> Vec<usize> {
402 let (period, offset) = (grid.beat_period().get(), grid.offset().get());
403 let end = track.time(track.db().len());
404 let mut beats: Vec<usize> = held
405 .iter()
406 .flat_map(|&(on, off)| {
407 let first = cast::ceil_to_usize((on + SUSTAIN_LEAD.get() - offset) / period - 1e-9);
408 let last = cast::floor_to_usize((off.min(end) - offset) / period + 1e-9);
409 first.unwrap_or(0)..last.unwrap_or(0)
410 })
411 .collect();
412 beats.sort_unstable();
413 beats.dedup();
414 beats
415}
416
417fn starts_in(notes: &NoteList, start: f64, end: f64) -> bool {
420 let lead = SUSTAIN_LEAD.get();
421 notes
422 .notes()
423 .iter()
424 .any(|n| (start - lead..end - lead).contains(&n.onset().get()))
425}
426
427fn fit(track: &LevelTrack, grid: BeatGrid, held: &[(f64, f64)]) -> Option<BeatGrid> {
436 let beats = covered(track, grid, held);
437 let (mut longest, mut run) = ((0, 0), (0, 0));
438 for (i, &b) in beats.iter().enumerate() {
439 run = if i > 0 && b == beats[i - 1] + 1 {
440 (run.0, run.1 + 1)
441 } else {
442 (b, 1)
443 };
444 if run.1 > longest.1 {
445 longest = run;
446 }
447 }
448 if longest.1 < PUMP_BEATS_MIN {
449 return None;
450 }
451 let time = |b: usize| grid.beat_time(b).get();
452 let at = |share: f64| {
453 BeatGrid::new(
454 Bpm::clamped(grid.bpm().get() * (1.0 + share)),
455 grid.offset(),
456 )
457 };
458 let step = |(from, to): (f64, f64)| {
459 grid.beat_period().get() / (cast::usize_to_f64(PUMP_BINS) * (to - from))
460 };
461 let search = |centre: f64, half: f64, span: (f64, f64)| -> Option<f64> {
462 let step = step(span);
463 let n = cast::floor_to_usize(half / step).unwrap_or(0);
464 (0..=2 * n)
465 .map(|k| centre + (cast::usize_to_f64(k) - cast::usize_to_f64(n)) * step)
466 .map(|share| (share, folded_depth(track, at(share), held, span)))
467 .max_by(|a, b| a.1.total_cmp(&b.1))
468 .map(|(share, _)| share)
469 };
470 let run = (
471 time(longest.0),
472 time(longest.0 + longest.1.min(PERIOD_RUN_BEATS)),
473 );
474 let all = (time(beats[0]), time(beats[beats.len() - 1] + 1));
475 let coarse = search(0.0, PERIOD_RANGE, run)?;
476 let share = search(coarse, step(run), all)?;
477 (share != 0.0).then(|| at(share))
478}
479
480fn folded_depth(
483 track: &LevelTrack,
484 grid: BeatGrid,
485 held: &[(f64, f64)],
486 (from, to): (f64, f64),
487) -> f64 {
488 let period = grid.beat_period().get();
489 let mut sum = [0.0; PUMP_BINS];
490 let mut count = 0.0;
491 for b in covered(track, grid, held) {
492 let start = grid.beat_time(b).get();
493 if start < from - 1e-9 || start + period > to + 1e-9 {
494 continue;
495 }
496 if let Some(curve) = beat_curve(track, start, period, 0) {
497 for (s, c) in sum.iter_mut().zip(curve) {
498 *s += c;
499 }
500 count += 1.0;
501 }
502 }
503 if count == 0.0 {
504 return 0.0;
505 }
506 dip(&sum.map(|s| s / count))
507}
508
509fn fall_into(curve: &[f64], low: usize) -> f64 {
513 let near = low - PHASE_SLACK..(low + PHASE_SLACK + 1).min(curve.len());
514 let top = curve[near.start - FALL_MAX..near.start]
515 .iter()
516 .copied()
517 .fold(f64::NEG_INFINITY, f64::max);
518 let bottom = curve[near].iter().copied().fold(f64::INFINITY, f64::min);
519 top - bottom
520}
521
522fn beat_curve(track: &LevelTrack, start: f64, period: f64, before: usize) -> Option<Vec<f64>> {
526 let count = (before + 1) * PUMP_BINS;
527 let mut sums = vec![0.0; count];
528 let mut counts = vec![0_usize; count];
529 let bins = cast::usize_to_f64(PUMP_BINS);
530 let first = start - cast::usize_to_f64(before) * period;
531 for i in track.index(first.max(track.time(0)))..track.index(start + period) {
532 let phase = (track.time(i) - start) / period;
533 let bin = if phase >= 0.0 || before == 0 {
535 cast::floor_to_usize(phase * bins)
536 .unwrap_or(0)
537 .min(PUMP_BINS - 1)
538 + count
539 - PUMP_BINS
540 } else {
541 cast::floor_to_usize((phase + cast::usize_to_f64(before)) * bins)
542 .unwrap_or(0)
543 .min(count - PUMP_BINS - 1)
544 };
545 sums[bin] += track.db()[i];
546 counts[bin] += 1;
547 }
548 if counts.contains(&0) {
549 return None;
550 }
551 let means: Vec<f64> = sums
552 .iter()
553 .zip(counts)
554 .map(|(s, c)| s / cast::usize_to_f64(c))
555 .collect();
556 let mut sorted = means[count - PUMP_BINS..].to_vec();
557 sorted.sort_by(f64::total_cmp);
558 let reference = quantile(&sorted, PUMP_REFERENCE)?;
559 Some(means.into_iter().map(|m| m - reference).collect())
560}
561
562fn median_curve<B: AsRef<[f64]>>(beats: &[B]) -> Vec<f64> {
564 (0..beats.first().map_or(0, |b| b.as_ref().len()))
565 .map(|k| median(&beats.iter().map(|b| b.as_ref()[k]).collect::<Vec<_>>()).unwrap_or(0.0))
566 .collect()
567}
568
569fn dip(curve: &[f64]) -> f64 {
571 -curve.iter().copied().fold(0.0, f64::min)
572}
573
574fn dip_bin(curve: &[f64]) -> usize {
576 curve
577 .iter()
578 .enumerate()
579 .min_by(|a, b| a.1.total_cmp(b.1))
580 .map_or(0, |(k, _)| k)
581}
582
583fn recovery(curve: &[f64]) -> Option<f64> {
588 let low = dip_bin(curve);
589 let back = -PUMP_BACK.get();
590 let at = |k: usize| curve[(low + k) % PUMP_BINS];
591 (1..PUMP_BINS).find(|&k| at(k) >= back).map(|k| {
592 let (a, b) = (at(k - 1), at(k));
593 let w = if b > a { (back - a) / (b - a) } else { 1.0 };
594 cast::usize_to_f64(k - 1) + w.clamp(0.0, 1.0)
595 })
596}
597
598fn fall_to(curve: &[f64], low: usize) -> usize {
601 (1..=low)
602 .find(|&k| curve[low - k] >= -PUMP_BACK.get())
603 .unwrap_or(PUMP_BINS)
604}
605
606fn dips(curve: &[f64], depth: f64) -> usize {
612 let Some(start) = (0..PUMP_BINS).find(|&k| curve[k] >= -0.25 * depth) else {
614 return 1;
615 };
616 let mut inside = false;
617 let mut count = 0;
618 for k in 0..PUMP_BINS {
619 let v = curve[(start + k) % PUMP_BINS];
620 if !inside && v <= -0.5 * depth {
621 inside = true;
622 count += 1;
623 } else if inside && v >= -0.25 * depth {
624 inside = false;
625 }
626 }
627 count
628}
629
630fn lock(
651 beats: &[Vec<f64>],
652 onset_beats: &[bool],
653 odd: &[bool],
654 falls: &[f64],
655 wide: &[f64],
656 depth: f64,
657 free: bool,
658) -> PumpLock {
659 let curve = &wide[PUMP_BINS..];
660 let median_of = |flags: &[bool], value: bool| -> Option<Vec<f64>> {
661 let group: Vec<&Vec<f64>> = beats
662 .iter()
663 .zip(flags)
664 .filter(|&(_, &f)| f == value)
665 .map(|(b, _)| b)
666 .collect();
667 (group.len() >= 2).then(|| median_curve(&group))
668 };
669 let group = |with_onset: bool| median_of(onset_beats, with_onset).map(|c| dip(&c[PUMP_BINS..]));
670 if let (Some(with), Some(without)) = (group(true), group(false))
671 && with >= PUMP_MIN.get()
672 && without < 0.5 * PUMP_MIN.get()
673 {
674 return PumpLock::Note;
675 }
676 if free {
677 return PumpLock::Free;
678 }
679 let low = PUMP_BINS + dip_bin(curve);
680 let half_falls = |parity: bool| {
681 median_of(odd, parity).is_some_and(|half| {
682 fall_to(&half, low) <= FALL_MAX && half[low] <= -PHASE_DEPTH * depth
683 })
684 };
685 if depth >= PUMP_MIN.get()
686 && fall_to(wide, low) <= FALL_MAX
687 && dips(curve, depth) == 1
688 && half_falls(false)
689 && half_falls(true)
690 {
691 let near = falls.iter().filter(|&&f| f >= PHASE_DEPTH * depth).count();
692 if cast::usize_to_f64(near) >= PHASE_SHARE * cast::usize_to_f64(falls.len()) {
693 return PumpLock::Beat;
694 }
695 }
696 PumpLock::None
697}
698
699fn folds_better(error: f64, other: f64) -> bool {
702 error < FOLD_ERROR_SHARE * other && other - error >= FOLD_ERROR_GAIN.get()
703}
704
705fn fold_error(track: &LevelTrack, starts: &[f64], period: f64, curve: &[f64]) -> f64 {
709 let bins = cast::usize_to_f64(PUMP_BINS);
710 let mut squares = 0.0;
711 let mut count = 0.0;
712 for &start in starts {
713 let rests: Vec<f64> = (track.index(start)..track.index(start + period))
714 .map(|i| {
715 let phase = (track.time(i) - start) / period;
716 let bin = cast::floor_to_usize(phase * bins)
717 .unwrap_or(0)
718 .min(PUMP_BINS - 1);
719 track.db()[i] - curve[bin]
720 })
721 .collect();
722 let frames = cast::usize_to_f64(rests.len());
723 let mean = rests.iter().sum::<f64>() / frames;
724 squares += rests.iter().map(|r| (r - mean).powi(2)).sum::<f64>();
725 count += frames;
726 }
727 (squares / count).sqrt()
728}
729
730fn free_line(track: &LevelTrack, (start, end): (f64, f64), grid: BeatGrid) -> Option<f64> {
734 let (lo, hi) = (track.index(start), track.index(end));
735 let block = cast::round_to_usize(1.0 / (TRACK_RATE_HZ.get() * HOP.get())).unwrap_or(5);
736 let values: Vec<f64> = track.db()[lo..hi]
740 .chunks_exact(block.max(1))
741 .map(|c| 10f64.powf(c.iter().sum::<f64>() / cast::usize_to_f64(c.len()) / 20.0))
742 .collect();
743 let track = Track::new(values, TRACK_RATE_HZ);
744 let spectrum = ModulationSpectrum::of(&track);
745 let rate = spectrum.rate(FREE_RATE_MIN, SCAN_TOP_HZ).ok()?;
746 let &rate = rate.value()?;
747 let amplitude =
749 spectrum.magnitude_at(rate.get()) * 4.0 / (track.span().get() * TRACK_RATE_HZ.get());
750 let swing = amplitude * 20.0 / std::f64::consts::LN_10;
752 (swing >= FREE_SWING.get()).then(|| snap(rate, grid.bpm()).residual.get().abs())
753}
754
755#[cfg(test)]
756mod tests {
757 use super::*;
758 use crate::units::Bpm;
759
760 #[test]
761 fn the_constants_pass_the_unit_check() {
762 assert_eq!(PUMP_BACK, Decibels::new(1.5).unwrap());
763 assert_eq!(PUMP_MIN, Decibels::new(3.0).unwrap());
764 assert_eq!(FREE_RESIDUAL, Cents::new(50.0).unwrap());
765 assert_eq!(FREE_SWING, Decibels::new(1.0).unwrap());
766 assert_eq!(FREE_RATE_MIN, Hz::new(0.5).unwrap());
767 assert_eq!(SUSTAIN_LEAD, Seconds::new(0.05).unwrap());
768 }
769
770 fn grid() -> BeatGrid {
771 BeatGrid::new(Bpm::new(120.0).unwrap(), Seconds::clamped(0.0))
772 }
773
774 #[test]
775 fn the_counted_beats_follow_the_grid_offset() {
776 let track = LevelTrack::new(Seconds::clamped(0.0), vec![0.0; 16_000]);
777 let grid = BeatGrid::new(Bpm::new(120.0).unwrap(), Seconds::clamped(0.6));
778 assert_eq!(
781 covered(&track, grid, &[(1.0, 16.0)]),
782 (1..30).collect::<Vec<_>>()
783 );
784 }
785
786 #[test]
787 fn an_onset_up_to_the_lead_before_a_beat_belongs_to_it() {
788 let starts = |onset: f64| {
789 let note = crate::notes::Note::new(
790 Seconds::clamped(onset),
791 Seconds::clamped(onset + 5.0),
792 crate::units::MidiPitch::new(45.0).unwrap(),
793 )
794 .unwrap();
795 starts_in(&NoteList::new(vec![note]), 1.0, 1.5)
796 };
797 assert!(starts(0.97));
798 assert!(!starts(0.93), "70 ms before the beat");
799 assert!(starts(1.43), "70 ms before the next beat");
800 assert!(!starts(1.47), "30 ms before the next beat");
801 }
802
803 #[test]
804 fn the_folded_depth_is_the_mean_over_the_beats_whatever_their_count() {
805 let db: Vec<f64> = (0..4_000)
807 .map(|i| if i % 500 < 50 { -6.0 } else { 0.0 })
808 .collect();
809 let track = LevelTrack::new(Seconds::clamped(0.0), db);
810 for beats in [4.0, 7.0] {
811 let depth = folded_depth(&track, grid(), &[(-1.0, 4.0)], (0.0, 0.5 * beats));
812 assert!((depth - 6.0).abs() < 1e-9, "{beats}: {depth}");
813 }
814 }
815
816 #[test]
817 fn the_recovery_interpolates_between_bins() {
818 let mut curve = vec![0.0; PUMP_BINS];
819 curve[0] = -12.0;
820 curve[1] = -6.0;
821 curve[2] = -2.0;
822 curve[3] = -0.5;
823 assert!((recovery(&curve).unwrap() - (2.0 + 1.0 / 3.0)).abs() < 1e-12);
825 assert!(recovery(&vec![-4.0; PUMP_BINS]).is_none());
826 curve.rotate_left(1);
828 assert!((recovery(&curve).unwrap() - (2.0 + 1.0 / 3.0)).abs() < 1e-12);
829 }
830
831 fn dipped(at: usize, depth: f64) -> Vec<f64> {
833 let mut curve = vec![0.0; PUMP_BINS];
834 curve[at] = -depth;
835 curve[at + 1] = -0.5 * depth;
836 curve
837 }
838
839 const ALTERNATE: [bool; 8] = [false, true, false, true, false, true, false, true];
841
842 fn wide(at: usize, depth: f64) -> Vec<f64> {
844 [vec![0.0; PUMP_BINS], dipped(at, depth)].concat()
845 }
846
847 #[test]
848 fn the_beat_lock_needs_half_the_beats_to_fall_into_the_dip() {
849 let curve = wide(10, 8.0);
850 let beats: Vec<Vec<f64>> = (0..8).map(|_| wide(10, 8.0)).collect();
851 let mut falls = [4.0, 4.0, 4.0, 4.0, 3.9, 1.0, 1.0, 0.0];
853 assert_eq!(
854 lock(&beats, &[false; 8], &ALTERNATE, &falls, &curve, 8.0, false),
855 PumpLock::Beat
856 );
857 falls[3] = 3.9;
859 assert_eq!(
860 lock(&beats, &[false; 8], &ALTERNATE, &falls, &curve, 8.0, false),
861 PumpLock::None
862 );
863 }
864
865 #[test]
866 fn the_beat_lock_needs_the_curve_to_fall_from_the_beat_before() {
867 let ramp: Vec<f64> = (0..PUMP_BINS)
871 .map(|k| -6.0 * (1.0 - cast::usize_to_f64(k) / 63.0))
872 .collect();
873 let rising = [ramp.iter().map(|v| v - 6.0).collect(), ramp.clone()].concat();
874 let beats = vec![rising.clone(); 8];
875 assert_eq!(
876 lock(
877 &beats,
878 &[false; 8],
879 &ALTERNATE,
880 &[6.0; 8],
881 &rising,
882 6.0,
883 false,
884 ),
885 PumpLock::None
886 );
887 let ducked = [vec![0.0; PUMP_BINS], ramp].concat();
889 let beats = vec![ducked.clone(); 8];
890 assert_eq!(
891 lock(
892 &beats,
893 &[false; 8],
894 &ALTERNATE,
895 &[6.0; 8],
896 &ducked,
897 6.0,
898 false,
899 ),
900 PumpLock::Beat
901 );
902 }
903
904 #[test]
905 fn the_beat_lock_needs_the_odd_and_the_even_beats_to_dip() {
906 let beats: Vec<Vec<f64>> = (0..9)
909 .map(|i| {
910 if i % 2 == 0 {
911 wide(10, 8.0)
912 } else {
913 vec![0.0; 2 * PUMP_BINS]
914 }
915 })
916 .collect();
917 let odd: Vec<bool> = (0..9).map(|i| i % 2 == 1).collect();
918 let curve = median_curve(&beats);
919 assert!((dip(&curve[PUMP_BINS..]) - 8.0).abs() < 1e-12);
920 assert_eq!(
921 lock(&beats, &[false; 9], &odd, &[8.0; 9], &curve, 8.0, false),
922 PumpLock::None
923 );
924 let beats: Vec<Vec<f64>> = (0..9)
926 .map(|i| wide(10, if i % 2 == 0 { 8.0 } else { 3.9 }))
927 .collect();
928 assert_eq!(
929 lock(&beats, &[false; 9], &odd, &[8.0; 9], &curve, 8.0, false),
930 PumpLock::None
931 );
932 let beats = vec![wide(10, 8.0); 9];
933 assert_eq!(
934 lock(&beats, &[false; 9], &odd, &[8.0; 9], &curve, 8.0, false),
935 PumpLock::Beat
936 );
937 }
938
939 #[test]
940 fn a_beat_falls_from_the_bins_before_its_dip_to_the_lowest_near_it() {
941 let mut curve = vec![-1.0; 2 * PUMP_BINS];
944 curve[63] = 0.5;
945 curve[77] = -9.0;
946 assert!((fall_into(&curve, 74) - 9.5).abs() < 1e-12);
947 let mut curve = vec![-1.0; 2 * PUMP_BINS];
948 curve[62] = 0.5;
949 curve[78] = -9.0;
950 assert!(fall_into(&curve, 74).abs() < 1e-12);
951 let mut curve = vec![-1.0; 2 * PUMP_BINS];
953 curve[126] = -9.0;
954 assert!((fall_into(&curve, 126) - 8.0).abs() < 1e-12);
955 }
956
957 #[test]
958 fn the_note_lock_needs_the_other_beats_under_half_the_floor() {
959 let curve = wide(10, 4.0);
960 let onsets = [true, true, false, false];
961 let with = |other: f64| {
962 let beats = vec![
963 wide(10, 4.0),
964 wide(10, 4.0),
965 wide(30, other),
966 wide(30, other),
967 ];
968 lock(
969 &beats,
970 &onsets,
971 &ALTERNATE[..4],
972 &[0.0; 4],
973 &curve,
974 4.0,
975 false,
976 )
977 };
978 assert_eq!(with(1.0), PumpLock::Note);
979 assert_ne!(with(1.6), PumpLock::Note);
980 }
981
982 #[test]
983 fn a_note_takes_the_strip_by_the_beats_after_the_grid_offset() {
984 let pump = Pump {
985 depth: Decibels::clamped(8.0),
986 back: None,
987 tau: None,
988 lock: PumpLock::Beat,
989 beats: 8,
990 curve: vec![0.0; PUMP_BINS],
991 grid: BeatGrid::new(Bpm::new(120.0).unwrap(), Seconds::clamped(0.6)),
992 falls: [0.0, 0.0, 8.0, 8.0, 8.0, 8.0, 8.0, 8.0].map(Some).to_vec(),
993 fold_error: 0.0,
994 free_line: None,
995 };
996 assert!(!pump.carries(0.6, 1.6), "beats 0 and 1 do not fall");
997 assert!(pump.carries(1.6, 2.6), "beats 2 and 3 fall");
998 }
999
1000 #[test]
1001 fn a_ducks_shape_recovers_slower_than_it_falls() {
1002 let shaped = |fall: f64, rise: f64, window: f64| {
1004 let curve: Vec<f64> = (0..PUMP_BINS)
1005 .map(|bin| {
1006 let k = cast::usize_to_f64(bin) - 32.0;
1007 let width = if k < 0.0 { fall } else { rise };
1008 -8.0 * (1.0 - k.abs() / width).max(0.0)
1009 })
1010 .collect();
1011 Pump {
1012 depth: Decibels::clamped(8.0),
1013 back: None,
1014 tau: None,
1015 lock: PumpLock::Beat,
1016 beats: 8,
1017 curve,
1018 grid: grid(),
1019 falls: Vec::new(),
1020 fold_error: 0.0,
1021 free_line: None,
1022 }
1023 .has_a_ducks_shape(window)
1024 };
1025 assert!(shaped(4.0, 16.0, 0.012));
1028 assert!(!shaped(4.0, 12.0, 0.012), "3 bins under a window");
1029 assert!(shaped(4.0, 12.0, 0.0));
1030 assert!(!shaped(4.0, 8.0, 0.0), "2 bins without a window");
1031 assert!(!shaped(40.0, 24.0, 0.0), "falls slower than it recovers");
1032 assert!(!shaped(24.0, 24.0, 0.0), "falls as it recovers");
1033 }
1034
1035 #[test]
1036 fn a_better_fold_needs_both_a_share_and_a_gain() {
1037 assert!(folds_better(1.0, 1.2));
1038 assert!(!folds_better(1.0, 1.1), "0.1 dB");
1039 assert!(!folds_better(3.0, 3.2), "a fifteenth");
1040 assert!(folds_better(2.68, 3.0));
1041 assert!(!folds_better(2.72, 3.0));
1042 assert!(
1043 !folds_better(FOLD_ERROR_SHARE * 3.0, 3.0),
1044 "the share itself"
1045 );
1046 assert!(folds_better(0.0, FOLD_ERROR_GAIN.get()), "the gain itself");
1047 }
1048
1049 #[test]
1050 fn a_gained_strip_needs_a_lower_fold_error_and_a_lost_one_never_replaces() {
1051 let read = |beats: usize, fold_error: f64| Pump {
1052 depth: Decibels::clamped(8.0),
1053 back: None,
1054 tau: None,
1055 lock: PumpLock::Beat,
1056 beats,
1057 curve: vec![0.0; PUMP_BINS],
1058 grid: BeatGrid::new(Bpm::new(120.0).unwrap(), Seconds::clamped(0.0)),
1059 falls: Vec::new(),
1060 fold_error,
1061 free_line: None,
1062 };
1063 assert!(read(8, 1.99).replaces_a_beat_lock(&read(7, 2.0)));
1064 assert!(!read(8, 2.0).replaces_a_beat_lock(&read(7, 2.0)));
1065 assert!(!read(7, 1.0).replaces_a_beat_lock(&read(8, 2.0)));
1066 assert!(read(8, 1.0).replaces_a_beat_lock(&read(8, 2.0)));
1067 assert!(!read(8, 1.9).replaces_a_beat_lock(&read(8, 2.0)));
1068 }
1069
1070 #[test]
1071 fn the_curve_follows_the_grid_offset() {
1072 let mut curve = vec![0.0; PUMP_BINS];
1073 curve[0] = -8.0;
1074 let pump = Pump {
1075 depth: Decibels::clamped(8.0),
1076 back: None,
1077 tau: None,
1078 lock: PumpLock::Beat,
1079 beats: 8,
1080 curve,
1081 grid: BeatGrid::new(Bpm::new(120.0).unwrap(), Seconds::clamped(0.1)),
1082 falls: Vec::new(),
1083 fold_error: 0.0,
1084 free_line: None,
1085 };
1086 assert!((pump.at(0.1 + 0.5 / 128.0) + 8.0).abs() < 1e-9);
1087 assert!(pump.at(0.5 / 128.0).abs() < 1e-9);
1088 }
1089
1090 #[test]
1091 fn the_curve_wraps_between_the_last_and_the_first_bin() {
1092 let mut curve = vec![0.0; PUMP_BINS];
1093 curve[0] = -8.0;
1094 let pump = Pump {
1095 depth: Decibels::clamped(8.0),
1096 back: None,
1097 tau: None,
1098 lock: PumpLock::Beat,
1099 beats: 4,
1100 curve,
1101 grid: grid(),
1102 falls: Vec::new(),
1103 fold_error: 0.0,
1104 free_line: None,
1105 };
1106 assert!((pump.at(0.5 / 128.0) + 8.0).abs() < 1e-9);
1109 assert!((pump.at(0.0) + 4.0).abs() < 1e-9);
1110 assert!((pump.at(1.0) + 4.0).abs() < 1e-9);
1111 }
1112
1113 proptest::proptest! {
1114 #[test]
1115 fn the_dips_and_the_recovery_do_not_depend_on_where_the_beat_starts(
1116 curve in proptest::collection::vec(-20.0..0.0f64, PUMP_BINS),
1117 shift in 0..PUMP_BINS,
1118 ) {
1119 let mut rotated = curve.clone();
1120 rotated.rotate_left(shift);
1121 let depth = dip(&curve);
1122 proptest::prop_assert_eq!(dips(&curve, depth), dips(&rotated, depth));
1123 proptest::prop_assert_eq!(recovery(&curve), recovery(&rotated));
1124 }
1125 }
1126}