1use std::fmt;
5
6use crate::cast;
7use crate::confidence::{ConfidenceError, Estimate, Level, Reason, ReasonCode, Signal};
8use crate::dsp::descriptors::too_few_frames;
9use crate::dsp::{ActiveFrames, MIN_ACTIVE_FRAMES, Stft, StftParams, Summary, Window};
10use crate::notes::NoteList;
11use crate::units::{Bpm, Decibels, Hz, Normalized, Seconds};
12
13use super::stereo::{StereoScreen, stereo_screen};
14use super::{Audio, SampleRate};
15
16pub const LONG_FRAME_BELOW_HZ: Hz = Hz::from_const(100.0);
23pub const BAND_EDGE_STEEP_DROP_DB: Decibels = Decibels::from_const(25.0);
27pub const BAND_EDGE_PLATEAU_DB: Decibels = Decibels::from_const(20.0);
33pub const BAND_EDGE_MARGIN_DB: Decibels = Decibels::from_const(30.0);
40pub const CEILING_TOLERANCE: Normalized = Normalized::from_const(1e-5);
42pub const FLAT_TOP_MIN_RUN_S: Seconds = Seconds::from_const(0.001);
44pub const CLIP_APPROACH: Normalized = Normalized::from_const(0.5);
50pub const FLAT_TOP_MIN_SHARE: Normalized = Normalized::from_const(0.001);
52
53pub const BAND_EDGE_CONVENTION: &str = "long-term power spectrum of the mid over the active \
55frames that lie wholly inside the stem, median-smoothed over one semitone on each side; where \
56the smoothed level falls at least 25 dB over one semitone above 2 kHz and the median level of \
57the octave below lies within 20 dB of the level where the fall starts, the edge is the \
58half-depth point of the steepest such fall; otherwise the noise floor is the lowest level above \
591 kHz and the edge is the highest bin at least 30 dB above it, at low confidence, or the Nyquist \
60frequency when no bin above 1 kHz is";
61pub const HARD_LIMITED_CONVENTION: &str = "true when runs of at least 1 ms of samples \
63within 1e-5 of the channel peak, each with its neighbours on both sides at least half the \
64peak on the same side, cover at least 0.1 % of one of the first two channels";
65
66#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
68pub enum NoteValue {
69 FourBars,
71 TwoBars,
73 Whole,
75 Half,
77 Quarter,
79 Eighth,
81 Sixteenth,
83 ThirtySecond,
85}
86
87impl NoteValue {
88 fn beats(self) -> f64 {
89 match self {
90 Self::FourBars => 16.0,
91 Self::TwoBars => 8.0,
92 Self::Whole => 4.0,
93 Self::Half => 2.0,
94 Self::Quarter => 1.0,
95 Self::Eighth => 0.5,
96 Self::Sixteenth => 0.25,
97 Self::ThirtySecond => 0.125,
98 }
99 }
100
101 fn label(self) -> &'static str {
102 match self {
103 Self::FourBars => "4 bars",
104 Self::TwoBars => "2 bars",
105 Self::Whole => "1/1",
106 Self::Half => "1/2",
107 Self::Quarter => "1/4",
108 Self::Eighth => "1/8",
109 Self::Sixteenth => "1/16",
110 Self::ThirtySecond => "1/32",
111 }
112 }
113}
114
115#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
117pub enum Feel {
118 Straight,
120 Triplet,
122 Dotted,
124}
125
126impl Feel {
127 fn stretch(self) -> (f64, f64) {
131 match self {
132 Self::Straight => (1.0, 1.0),
133 Self::Triplet => (2.0, 3.0),
134 Self::Dotted => (3.0, 2.0),
135 }
136 }
137}
138
139#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
141pub struct Division {
142 pub value: NoteValue,
144 pub feel: Feel,
146}
147
148impl Division {
149 pub const ALL: [Self; 20] = {
152 use Feel::{Dotted, Straight, Triplet};
153 use NoteValue::{Eighth, FourBars, Half, Quarter, Sixteenth, ThirtySecond, TwoBars, Whole};
154 [
155 Self::of(FourBars, Straight),
156 Self::of(TwoBars, Straight),
157 Self::of(Whole, Dotted),
158 Self::of(Whole, Straight),
159 Self::of(Half, Dotted),
160 Self::of(Whole, Triplet),
161 Self::of(Half, Straight),
162 Self::of(Quarter, Dotted),
163 Self::of(Half, Triplet),
164 Self::of(Quarter, Straight),
165 Self::of(Eighth, Dotted),
166 Self::of(Quarter, Triplet),
167 Self::of(Eighth, Straight),
168 Self::of(Sixteenth, Dotted),
169 Self::of(Eighth, Triplet),
170 Self::of(Sixteenth, Straight),
171 Self::of(ThirtySecond, Dotted),
172 Self::of(Sixteenth, Triplet),
173 Self::of(ThirtySecond, Straight),
174 Self::of(ThirtySecond, Triplet),
175 ]
176 };
177
178 const fn of(value: NoteValue, feel: Feel) -> Self {
179 Self { value, feel }
180 }
181
182 #[must_use]
185 pub fn beats_per_cycle(self) -> f64 {
186 let (num, den) = self.feel.stretch();
187 self.value.beats() * num / den
188 }
189
190 #[must_use]
193 pub fn rate(self, bpm: Bpm) -> Hz {
194 let (num, den) = self.feel.stretch();
197 Hz::clamped(bpm.get() / 60.0 / self.value.beats() * den / num)
198 }
199}
200
201impl fmt::Display for Division {
202 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203 let suffix = match self.feel {
204 Feel::Straight => "",
205 Feel::Triplet => "T",
206 Feel::Dotted => "D",
207 };
208 write!(f, "{}{suffix}", self.value.label())
209 }
210}
211
212#[derive(Clone, Copy, Debug, PartialEq)]
216pub struct BeatGrid {
217 bpm: Bpm,
218 offset: Seconds,
219}
220
221impl BeatGrid {
222 #[must_use]
224 pub fn new(bpm: Bpm, offset: Seconds) -> Self {
225 Self { bpm, offset }
226 }
227
228 #[must_use]
230 pub fn bpm(self) -> Bpm {
231 self.bpm
232 }
233
234 #[must_use]
236 pub fn offset(self) -> Seconds {
237 self.offset
238 }
239
240 #[must_use]
242 pub fn beat_period(self) -> Seconds {
243 Seconds::clamped(60.0 / self.bpm.get())
244 }
245
246 #[must_use]
248 pub fn beat_time(self, beat: usize) -> Seconds {
249 Seconds::clamped(self.offset.get() + cast::usize_to_f64(beat) * 60.0 / self.bpm.get())
250 }
251
252 #[must_use]
254 pub fn rate(self, division: Division) -> Hz {
255 division.rate(self.bpm)
256 }
257}
258
259#[derive(Clone, Debug, PartialEq)]
262pub struct Stem {
263 audio: Audio,
264 mid: Vec<f64>,
265 grid: BeatGrid,
266 median_f0: Option<Hz>,
267 frame_len: usize,
268 band_edge: Estimate<Hz, Hz>,
269 hard_limited: bool,
270 stereo: StereoScreen,
271}
272
273impl Stem {
274 pub fn new(
282 audio: Audio,
283 grid: BeatGrid,
284 notes: Option<&NoteList>,
285 ) -> Result<Self, ConfidenceError> {
286 let mid = audio.mono();
287 let median_f0 = notes.and_then(median_f0);
288 let rate = audio.sample_rate();
289 let frame_len = frame_len_for(median_f0, rate);
290 let band_edge = band_edge(&mid, rate, StftParams::fixed(frame_len))?;
291 let hard_limited = audio
293 .channels()
294 .iter()
295 .take(2)
296 .any(|channel| is_flat_topped(channel, rate));
297 let stereo = match audio.channels() {
298 [left, right, ..] => stereo_screen(left, right, rate, median_f0)?,
299 _ => StereoScreen::one_channel()?,
300 };
301 Ok(Self {
302 audio,
303 mid,
304 grid,
305 median_f0,
306 frame_len,
307 band_edge,
308 hard_limited,
309 stereo,
310 })
311 }
312
313 #[must_use]
315 pub fn audio(&self) -> &Audio {
316 &self.audio
317 }
318
319 #[must_use]
321 pub fn sample_rate(&self) -> SampleRate {
322 self.audio.sample_rate()
323 }
324
325 #[must_use]
327 pub fn duration(&self) -> Seconds {
328 self.audio.duration()
329 }
330
331 #[must_use]
336 pub fn mid(&self) -> &[f64] {
337 &self.mid
338 }
339
340 #[must_use]
343 pub fn side(&self) -> Option<Vec<f64>> {
344 match self.audio.channels() {
345 [left, right, ..] => Some(left.iter().zip(right).map(|(l, r)| (l - r) / 2.0).collect()),
346 _ => None,
347 }
348 }
349
350 #[must_use]
352 pub fn grid(&self) -> BeatGrid {
353 self.grid
354 }
355
356 #[must_use]
358 pub fn median_f0(&self) -> Option<Hz> {
359 self.median_f0
360 }
361
362 #[must_use]
369 pub fn frame_len(&self) -> usize {
370 self.frame_len
371 }
372
373 #[must_use]
377 pub fn band_edge(&self) -> &Estimate<Hz, Hz> {
378 &self.band_edge
379 }
380
381 #[must_use]
384 pub fn hard_limited(&self) -> bool {
385 self.hard_limited
386 }
387
388 #[must_use]
390 pub fn stereo(&self) -> &StereoScreen {
391 &self.stereo
392 }
393}
394
395fn median_f0(notes: &NoteList) -> Option<Hz> {
396 let frequencies: Vec<f64> = notes
397 .notes()
398 .iter()
399 .map(|note| Hz::from(note.pitch()).get())
400 .collect();
401 Summary::of(&frequencies).map(|summary| Hz::clamped(summary.median()))
402}
403
404const FRAME_BASE_RATE_HZ: f64 = 44_100.0;
406
407fn frame_len_for(median_f0: Option<Hz>, rate: SampleRate) -> usize {
408 let base = match median_f0 {
409 Some(f0) if f0 < LONG_FRAME_BELOW_HZ => 8192.0,
410 _ => 4096.0,
411 };
412 let wanted = base * rate.hz().get() / FRAME_BASE_RATE_HZ;
413 let up = cast::ceil_to_usize(wanted)
414 .unwrap_or(1)
415 .max(1)
416 .next_power_of_two();
417 if cast::usize_to_f64(up) > wanted * std::f64::consts::SQRT_2 {
420 (up / 2).max(1)
421 } else {
422 up
423 }
424}
425
426pub fn band_edge(
432 samples: &[f64],
433 rate: SampleRate,
434 params: StftParams,
435) -> Result<Estimate<Hz, Hz>, ConfidenceError> {
436 let params = params.with_window(Window::Hann);
437 let active = ActiveFrames::of(samples, params);
438 let frames = Reason::new(
439 ReasonCode::ActiveFrames,
440 Signal::Count(u64::try_from(active.count()).unwrap_or(u64::MAX)),
441 )?;
442 if active.count() == 0 {
443 return Estimate::none(vec![
444 Reason::new(ReasonCode::SilentStem, Signal::Flag(true))?,
445 frames,
446 ]);
447 }
448 if active.count() < MIN_ACTIVE_FRAMES {
449 return Estimate::none(vec![frames, too_few_frames()?]);
450 }
451 let inside: Vec<usize> = active
455 .indices()
456 .filter(|&frame| {
457 let start = params.frame_start(frame);
458 start >= 0
459 && usize::try_from(start)
460 .is_ok_and(|start| start + params.frame_len() <= samples.len())
461 })
462 .collect();
463 let frames_used = if inside.is_empty() {
464 active.indices().collect()
465 } else {
466 inside
467 };
468 let (smoothed, bin_hz) = smoothed_spectrum(samples, rate, params, &frames_used);
469 let steepest = steepest_edge(&smoothed, bin_hz);
470 let depth = steepest.map_or(0.0, |(_, depth)| depth);
471 let mut reasons = vec![
472 frames,
473 Reason::new(ReasonCode::BandEdgeDrop, Signal::Number(depth))?,
474 ];
475 let (edge, level) = match steepest {
479 Some((edge, depth)) if depth >= BAND_EDGE_STEEP_DROP_DB.get() => (edge, Level::High),
480 _ => {
481 if let Some(edge) = margin_edge(&smoothed, bin_hz) {
482 (edge, Level::Low)
483 } else {
484 reasons.push(Reason::new(ReasonCode::NoBandEdge, Signal::Flag(true))?);
485 (smoothed.len().saturating_sub(1), Level::Low)
486 }
487 }
488 };
489 Estimate::new(
490 Hz::clamped(cast::usize_to_f64(edge) * bin_hz),
491 level,
492 Some(Hz::clamped(bin_hz)),
493 reasons,
494 )
495}
496
497fn smoothed_spectrum(
500 samples: &[f64],
501 rate: SampleRate,
502 params: StftParams,
503 frames: &[usize],
504) -> (Vec<f64>, f64) {
505 let mut stft = Stft::new(params);
506 let mut power = vec![0.0; params.bins()];
507 for &frame in frames {
508 for (sum, value) in power.iter_mut().zip(stft.frame(samples, frame)) {
509 *sum += value.norm_sqr();
510 }
511 }
512 let peak = power.iter().copied().fold(0.0, f64::max);
513 let levels: Vec<f64> = power
515 .iter()
516 .map(|p| 10.0 * (p + peak * 1e-30).log10())
517 .collect();
518 (semitone_median(&levels), params.bin_width(rate).get())
519}
520
521fn margin_edge(smoothed: &[f64], bin_hz: f64) -> Option<usize> {
525 let first_above_1k = cast::ceil_to_usize(1000.0 / bin_hz).unwrap_or(1).max(1);
526 let floor = smoothed
527 .get(first_above_1k..)
528 .and_then(|band| band.iter().copied().reduce(f64::min))
529 .unwrap_or(f64::NEG_INFINITY);
530 smoothed
531 .iter()
532 .rposition(|&level| level >= floor + BAND_EDGE_MARGIN_DB.get())
533 .filter(|&edge| edge >= first_above_1k)
534}
535
536fn steepest_edge(smoothed: &[f64], bin_hz: f64) -> Option<(usize, f64)> {
539 let semitone = 2.0_f64.powf(1.0 / 12.0);
540 let first = cast::ceil_to_usize(2000.0 / bin_hz).unwrap_or(1).max(1);
541 let (depth, start, stop) = (first..smoothed.len())
542 .filter_map(|bin| {
543 let stop = cast::round_to_usize(cast::usize_to_f64(bin) * semitone)?;
544 let below = *smoothed.get(stop)?;
545 Some((smoothed[bin] - below, bin, stop))
546 })
547 .fold(None, |best: Option<(f64, usize, usize)>, item| match best {
548 Some(b) if b.0 >= item.0 => Some(b),
549 _ => Some(item),
550 })?;
551 let half = smoothed[start] - depth / 2.0;
552 let edge = (start..=stop).rev().find(|&bin| smoothed[bin] >= half)?;
553 let octave = Summary::of(&smoothed[start / 2..start])?.median();
556 let supported = octave >= smoothed[start] - BAND_EDGE_PLATEAU_DB.get();
557 Some((edge, if supported { depth } else { 0.0 }))
558}
559
560fn semitone_median(levels: &[f64]) -> Vec<f64> {
563 let semitone = 2.0_f64.powf(1.0 / 12.0) - 1.0;
564 (0..levels.len())
565 .map(|bin| {
566 let half = cast::round_to_usize(cast::usize_to_f64(bin) * semitone)
567 .unwrap_or(2)
568 .max(2);
569 let lo = bin.saturating_sub(half);
570 let hi = (bin + half + 1).min(levels.len());
571 Summary::of(&levels[lo..hi]).map_or(levels[bin], Summary::median)
572 })
573 .collect()
574}
575
576#[must_use]
578pub fn is_flat_topped(channel: &[f64], rate: SampleRate) -> bool {
579 let peak = channel.iter().fold(0.0_f64, |max, x| max.max(x.abs()));
580 if peak <= 0.0 || channel.is_empty() {
581 return false;
582 }
583 let ceiling = peak * (1.0 - CEILING_TOLERANCE.get());
584 let min_run = cast::ceil_to_usize(FLAT_TOP_MIN_RUN_S.get() * rate.hz().get())
585 .unwrap_or(1)
586 .max(2);
587 let approaches = |index: Option<usize>, sign: f64| {
590 index
591 .and_then(|i| channel.get(i))
592 .is_some_and(|&x| x * sign >= CLIP_APPROACH.get() * ceiling)
593 };
594 let mut flat = 0_usize;
595 let mut start = 0;
596 while start < channel.len() {
597 let sign = channel[start].signum();
598 if channel[start].abs() < ceiling {
599 start += 1;
600 continue;
601 }
602 let end = channel[start..]
603 .iter()
604 .position(|&x| x.abs() < ceiling || x.is_sign_positive() != sign.is_sign_positive())
605 .map_or(channel.len(), |offset| start + offset);
606 let run = end - start;
607 if run >= min_run && approaches(start.checked_sub(1), sign) && approaches(Some(end), sign) {
608 flat += run;
609 }
610 start = end;
611 }
612 cast::usize_to_f64(flat) >= FLAT_TOP_MIN_SHARE.get() * cast::usize_to_f64(channel.len())
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618
619 #[test]
620 fn the_constants_pass_the_unit_check() {
621 assert_eq!(LONG_FRAME_BELOW_HZ, Hz::new(100.0).unwrap());
622 assert_eq!(BAND_EDGE_MARGIN_DB, Decibels::new(30.0).unwrap());
623 assert_eq!(BAND_EDGE_STEEP_DROP_DB, Decibels::new(25.0).unwrap());
624 assert_eq!(BAND_EDGE_PLATEAU_DB, Decibels::new(20.0).unwrap());
625 assert_eq!(CEILING_TOLERANCE, Normalized::new(1e-5).unwrap());
626 assert_eq!(FLAT_TOP_MIN_RUN_S, Seconds::new(0.001).unwrap());
627 assert_eq!(FLAT_TOP_MIN_SHARE, Normalized::new(0.001).unwrap());
628 assert_eq!(CLIP_APPROACH, Normalized::new(0.5).unwrap());
629 }
630
631 fn bpm(value: f64) -> Bpm {
632 Bpm::new(value).unwrap()
633 }
634
635 fn rate() -> SampleRate {
636 SampleRate::new(44_100).unwrap()
637 }
638
639 #[test]
640 fn at_120_bpm_beats_fall_every_half_second_and_divisions_have_their_rates() {
641 let grid = BeatGrid::new(bpm(120.0), Seconds::new(0.0).unwrap());
642 assert_eq!(grid.beat_period().get(), 0.5);
643 assert_eq!(grid.beat_time(3).get(), 1.5);
644 let straight = |value| Division {
645 value,
646 feel: Feel::Straight,
647 };
648 assert_eq!(grid.rate(straight(NoteValue::Half)).get(), 1.0);
649 assert_eq!(grid.rate(straight(NoteValue::Quarter)).get(), 2.0);
650 assert_eq!(grid.rate(straight(NoteValue::Eighth)).get(), 4.0);
651 assert_eq!(grid.rate(straight(NoteValue::Sixteenth)).get(), 8.0);
652 assert_eq!(grid.rate(straight(NoteValue::Whole)).get(), 0.5);
653 let quarter = |feel| Division {
654 value: NoteValue::Quarter,
655 feel,
656 };
657 assert_eq!(grid.rate(quarter(Feel::Triplet)).get(), 3.0);
658 assert!((grid.rate(quarter(Feel::Dotted)).get() - 4.0 / 3.0).abs() < 1e-15);
659 }
660
661 #[test]
662 fn rate_and_beats_per_cycle_read_one_ratio() {
663 for tempo in [60.0, 97.3, 120.0, 128.0, 174.25] {
664 let beat_hz = tempo / 60.0;
665 for division in Division::ALL {
666 let rate = division.rate(bpm(tempo)).get();
667 let product = rate * division.beats_per_cycle();
668 assert!(
669 (product / beat_hz - 1.0).abs() < 1e-12,
670 "{division} at {tempo}"
671 );
672 let base = beat_hz / division.value.beats();
674 let earlier = match division.feel {
675 Feel::Straight => base,
676 Feel::Triplet => base * 1.5,
677 Feel::Dotted => base * 2.0 / 3.0,
678 };
679 assert_eq!(rate.to_bits(), earlier.to_bits(), "{division} at {tempo}");
680 }
681 }
682 }
683
684 #[test]
685 fn divisions_are_sorted_slowest_first_and_named() {
686 let rates: Vec<f64> = Division::ALL
687 .iter()
688 .map(|d| d.rate(bpm(120.0)).get())
689 .collect();
690 assert!(rates.windows(2).all(|pair| pair[0] < pair[1]), "{rates:?}");
691 assert_eq!(Division::ALL[10].to_string(), "1/8D");
692 assert_eq!(Division::ALL[0].to_string(), "4 bars");
693 }
694
695 #[test]
696 fn a_one_channel_stem_has_no_side_and_no_stereo_values() {
697 let samples: Vec<f64> = (0..44_100)
698 .map(|n| (cast::usize_to_f64(n) * 0.05).sin())
699 .collect();
700 let audio = Audio::new(rate(), vec![samples.clone()]).unwrap();
701 let stem = Stem::new(
702 audio,
703 BeatGrid::new(bpm(120.0), Seconds::new(0.0).unwrap()),
704 None,
705 )
706 .unwrap();
707 assert_eq!(stem.mid(), samples.as_slice());
708 assert_eq!(stem.side(), None);
709 assert!(!stem.stereo().confidence().is_estimate());
710 assert_eq!(stem.frame_len(), 4096);
711 }
712
713 #[test]
714 fn equal_channels_have_zero_side_and_opposite_ones_zero_mid() {
715 let x: Vec<f64> = (0..8820)
716 .map(|n| (cast::usize_to_f64(n) * 0.05).sin())
717 .collect();
718 let negated: Vec<f64> = x.iter().map(|v| -v).collect();
719 let grid = BeatGrid::new(bpm(120.0), Seconds::new(0.0).unwrap());
720 let same = Stem::new(
721 Audio::new(rate(), vec![x.clone(), x.clone()]).unwrap(),
722 grid,
723 None,
724 )
725 .unwrap();
726 assert!(same.side().unwrap().iter().all(|&s| s == 0.0));
727 let opposite =
728 Stem::new(Audio::new(rate(), vec![x, negated]).unwrap(), grid, None).unwrap();
729 assert!(opposite.mid().iter().all(|&m| m == 0.0));
730 }
731
732 #[test]
733 fn the_harmonic_frame_keeps_its_duration_at_every_rate() {
734 let low = Some(Hz::new(55.0).unwrap());
735 let high = Some(Hz::new(220.0).unwrap());
736 let lens = |hz| {
737 let rate = SampleRate::new(hz).unwrap();
738 (
739 frame_len_for(high, rate),
740 frame_len_for(low, rate),
741 frame_len_for(None, rate),
742 )
743 };
744 assert_eq!(lens(44_100), (4096, 8192, 4096));
745 assert_eq!(lens(48_000), (4096, 8192, 4096));
746 assert_eq!(lens(32_000), (4096, 8192, 4096));
747 assert_eq!(lens(88_200), (8192, 16_384, 8192));
748 assert_eq!(lens(96_000), (8192, 16_384, 8192));
749 assert_eq!(lens(192_000), (16_384, 32_768, 16_384));
750 assert_eq!(lens(22_050), (2048, 4096, 2048));
751 assert_eq!(lens(8_000), (1024, 2048, 1024));
752 for hz in (8_000..=192_000).step_by(50) {
755 let rate = SampleRate::new(hz).unwrap();
756 let seconds = |len| cast::usize_to_f64(len) / f64::from(hz);
757 let ratio = seconds(frame_len_for(low, rate)) / (8192.0 / 44_100.0);
758 assert!(
759 (1.0 / std::f64::consts::SQRT_2..=std::f64::consts::SQRT_2).contains(&ratio),
760 "{hz}: {ratio}"
761 );
762 }
763 }
764
765 #[test]
766 fn a_stem_at_96_khz_keeps_the_frame_duration_of_44_1_khz() {
767 let at = |hz: u32| {
768 let rate = SampleRate::new(hz).unwrap();
769 let samples: Vec<f64> = (0..usize::try_from(hz).unwrap())
770 .map(|n| (cast::usize_to_f64(n) * 0.05).sin())
771 .collect();
772 let stem = Stem::new(
773 Audio::new(rate, vec![samples]).unwrap(),
774 BeatGrid::new(bpm(120.0), Seconds::new(0.0).unwrap()),
775 None,
776 )
777 .unwrap();
778 rate.seconds(stem.frame_len()).get()
779 };
780 assert!((at(96_000) - 8192.0 / 96_000.0).abs() < 1e-12);
782 assert!((at(44_100) - 4096.0 / 44_100.0).abs() < 1e-12);
783 }
784
785 #[test]
786 fn a_clipped_sine_is_flat_topped_and_clean_or_saturated_ones_are_not() {
787 let sine: Vec<f64> = (0..44_100)
788 .map(|n| (std::f64::consts::TAU * 110.0 * cast::usize_to_f64(n) / 44_100.0).sin())
789 .collect();
790 let clipped: Vec<f64> = sine.iter().map(|v| v.clamp(-0.5, 0.5)).collect();
791 let saturated: Vec<f64> = sine.iter().map(|v| (4.0 * v).tanh()).collect();
792 assert!(is_flat_topped(&clipped, rate()));
793 assert!(!is_flat_topped(&sine, rate()));
794 assert!(!is_flat_topped(&saturated, rate()));
795 assert!(!is_flat_topped(&[0.0; 100], rate()));
796 }
797}