Skip to main content

rich_ext/
dsp.rs

1//! Signal processing for live views of audio and other sampled data: a
2//! fast Fourier transform, a Hann window and magnitude spectra; test
3//! signals (tones, harmonic partials, noise and a mix of them); trigger
4//! detection, as an oscilloscope's; a spectrum's peak and log-spaced
5//! bands, with levels that fall under gravity ([`Falloff`]), as a bar
6//! spectrum's (cava's); and the frequencies of musical notes, in equal
7//! temperament or scope-tui's table ([`NoteTable`]), with the note nearest
8//! a frequency, and a [`Tuner`] reading a spectrum's note and cents,
9//! smoothed.
10//!
11//! Not a port of anything upstream: an rs-rich addition, written for
12//! intuiTUIve's spectroscope (the `scope` example, after scope-tui), with
13//! no dependencies. [`fft`] takes any length: radix 2 for a power of two,
14//! and Bluestein's chirp transform (over a power-of-two FFT) otherwise, so
15//! a buffer of 1000 or 4410 samples is transformed at its own length.
16//!
17//! ```
18//! use rich_ext::dsp;
19//!
20//! // A 1 kHz tone, sampled at 8 kHz.
21//! let rate = 8000.0;
22//! let tone: Vec<f64> = (0..1024)
23//!     .map(|i| (2.0 * std::f64::consts::PI * 1000.0 * i as f64 / rate).sin())
24//!     .collect();
25//! let spectrum = dsp::spectrum(&dsp::hann(&tone), rate);
26//! let (peak, _) = spectrum
27//!     .iter()
28//!     .copied()
29//!     .max_by(|a, b| a.1.total_cmp(&b.1))
30//!     .unwrap();
31//! assert!((peak - 1000.0).abs() < rate / 1024.0);
32//! ```
33
34use std::f64::consts::PI;
35
36/// A complex number, as (real, imaginary).
37pub type Complex = (f64, f64);
38
39fn mul((a, b): Complex, (c, d): Complex) -> Complex {
40    (a * c - b * d, a * d + b * c)
41}
42
43/// The magnitude of `c`: `√(re² + im²)`.
44pub fn magnitude((re, im): Complex) -> f64 {
45    re.hypot(im)
46}
47
48/// `samples` under a Hann window, `0.5 (1 − cos(2πi / n))`, which tapers
49/// both ends to zero so a buffer that does not hold whole periods leaks
50/// less into neighbouring bins. The periodic form, as scope-tui's.
51pub fn hann(samples: &[f64]) -> Vec<f64> {
52    let n = samples.len() as f64;
53    samples
54        .iter()
55        .enumerate()
56        .map(|(i, s)| s * 0.5 * (1.0 - (2.0 * PI * i as f64 / n).cos()))
57        .collect()
58}
59
60/// An in-place radix-2 FFT; `data`'s length is a power of two.
61fn radix2(data: &mut [Complex]) {
62    let n = data.len();
63    if n < 2 {
64        return;
65    }
66    let bits = n.trailing_zeros();
67    for i in 0..n {
68        let j = i.reverse_bits() >> (usize::BITS - bits);
69        if i < j {
70            data.swap(i, j);
71        }
72    }
73    let mut len = 2;
74    while len <= n {
75        let angle = -2.0 * PI / len as f64;
76        for start in (0..n).step_by(len) {
77            for k in 0..len / 2 {
78                let w = ((angle * k as f64).cos(), (angle * k as f64).sin());
79                let (a, b) = (data[start + k], mul(data[start + k + len / 2], w));
80                data[start + k] = (a.0 + b.0, a.1 + b.1);
81                data[start + k + len / 2] = (a.0 - b.0, a.1 - b.1);
82            }
83        }
84        len *= 2;
85    }
86}
87
88/// The discrete Fourier transform of real `samples`, of their own length:
89/// `X[k] = Σ x[j] e^(−2πijk/n)`, unnormalised. Radix 2 for a power of two,
90/// Bluestein's chirp transform otherwise; an empty input gives an empty
91/// output.
92///
93/// ```
94/// use rich_ext::dsp::fft;
95///
96/// let bins = fft(&[1.0, 0.0, 0.0]);
97/// assert_eq!(bins.len(), 3);
98/// assert!(bins.iter().all(|(re, im)| (re - 1.0).abs() < 1e-12 && im.abs() < 1e-12));
99/// ```
100pub fn fft(samples: &[f64]) -> Vec<Complex> {
101    let n = samples.len();
102    if n == 0 {
103        return Vec::new();
104    }
105    if n.is_power_of_two() {
106        let mut data: Vec<Complex> = samples.iter().map(|s| (*s, 0.0)).collect();
107        radix2(&mut data);
108        return data;
109    }
110    // X[k] = w[k] Σ x[j] w[j] conj(w[k - j]), with w[j] = e^(-iπj²/n): a
111    // convolution, worked with a power-of-two FFT.
112    let m = (2 * n).saturating_sub(1).next_power_of_two();
113    let chirp: Vec<Complex> = (0..n)
114        .map(|j| {
115            // j² mod 2n keeps the angle small, so precise.
116            let angle = -PI * ((j * j) % (2 * n)) as f64 / n as f64;
117            (angle.cos(), angle.sin())
118        })
119        .collect();
120    let mut a = vec![(0.0, 0.0); m];
121    for (j, s) in samples.iter().enumerate() {
122        a[j] = (s * chirp[j].0, s * chirp[j].1);
123    }
124    let mut b = vec![(0.0, 0.0); m];
125    for (j, w) in chirp.iter().enumerate() {
126        b[j] = (w.0, -w.1);
127        if j > 0 {
128            b[m - j] = (w.0, -w.1);
129        }
130    }
131    radix2(&mut a);
132    radix2(&mut b);
133    // The inverse FFT: the conjugate of the forward FFT of the conjugate.
134    let mut c: Vec<Complex> = a
135        .iter()
136        .zip(&b)
137        .map(|(x, y)| {
138            let p = mul(*x, *y);
139            (p.0, -p.1)
140        })
141        .collect();
142    radix2(&mut c);
143    let scale = m as f64;
144    (0..n)
145        .map(|k| mul((c[k].0 / scale, -c[k].1 / scale), chirp[k]))
146        .collect()
147}
148
149/// The magnitude of every bin of `samples`' [`fft`], as `(frequency in Hz,
150/// magnitude)`: all `n` of them, `rate / n` Hz apart, the mirrored half
151/// above the Nyquist frequency too (up to `rate − rate / n`), as
152/// scope-tui plots them. [`spectrum`] stops at the Nyquist frequency.
153///
154/// ```
155/// use rich_ext::dsp::spectrum_full;
156///
157/// let bins = spectrum_full(&[0.0; 100], 1000.0);
158/// assert_eq!(bins.len(), 100);
159/// assert_eq!(bins[99].0, 990.0);
160/// ```
161pub fn spectrum_full(samples: &[f64], rate: f64) -> Vec<(f64, f64)> {
162    let n = samples.len();
163    if n == 0 {
164        return Vec::new();
165    }
166    let resolution = rate / n as f64;
167    fft(samples)
168        .into_iter()
169        .enumerate()
170        .map(|(i, bin)| (i as f64 * resolution, magnitude(bin)))
171        .collect()
172}
173
174/// The magnitude spectrum of `samples` taken at `rate` samples a second:
175/// `(frequency in Hz, magnitude)` for each bin from 0 Hz up to the Nyquist
176/// frequency (`rate / 2`), `n / 2 + 1` of them, `rate / n` Hz apart. The
177/// magnitudes are the [`fft`]'s, unnormalised: a full-scale sine of `n`
178/// samples peaks near `n / 2` (`n / 4` under a [`hann`] window). Window the
179/// samples first if they do not hold whole periods.
180///
181/// ```
182/// use rich_ext::dsp::spectrum;
183///
184/// let bins = spectrum(&[0.0; 100], 1000.0);
185/// assert_eq!(bins.len(), 51);
186/// assert_eq!(bins[1].0, 10.0);
187/// assert_eq!(bins[50].0, 500.0);
188/// ```
189pub fn spectrum(samples: &[f64], rate: f64) -> Vec<(f64, f64)> {
190    let n = samples.len();
191    if n == 0 {
192        return Vec::new();
193    }
194    let resolution = rate / n as f64;
195    fft(samples)
196        .into_iter()
197        .take(n / 2 + 1)
198        .enumerate()
199        .map(|(i, bin)| (i as f64 * resolution, magnitude(bin)))
200        .collect()
201}
202
203/// A sine wave of `hz` at `rate` samples a second, from -1 to 1 (or
204/// ±[`amplitude`](Tone::amplitude)), as an endless iterator of samples. Its
205/// phase carries on from one buffer to the next, so `tone.by_ref().take(n)`
206/// in a loop is one continuous wave.
207///
208/// ```
209/// use rich_ext::dsp::tone;
210///
211/// // 1 kHz at 4 kHz: a quarter period a sample.
212/// let wave: Vec<f64> = tone(1000.0, 4000.0).take(5).collect();
213/// let rounded: Vec<f64> = wave.iter().map(|s| s.round()).collect();
214/// assert_eq!(rounded, [0.0, 1.0, 0.0, -1.0, 0.0]);
215/// ```
216pub fn tone(hz: f64, rate: f64) -> Tone {
217    Tone {
218        step: if rate > 0.0 { hz / rate } else { 0.0 },
219        phase: 0.0,
220        amplitude: 1.0,
221    }
222}
223
224/// A sine wave: see [`tone`].
225#[derive(Clone, Debug, PartialEq)]
226pub struct Tone {
227    /// Cycles a sample.
228    step: f64,
229    /// Where in its cycle the next sample is, from 0 to 1.
230    phase: f64,
231    amplitude: f64,
232}
233
234impl Tone {
235    /// Its peak (default 1).
236    #[must_use]
237    pub fn amplitude(mut self, amplitude: f64) -> Self {
238        self.amplitude = amplitude;
239        self
240    }
241
242    /// Where in its cycle it starts, in radians (default 0).
243    #[must_use]
244    pub fn phase(mut self, radians: f64) -> Self {
245        self.phase = (radians / (2.0 * PI)).rem_euclid(1.0);
246        self
247    }
248}
249
250impl Iterator for Tone {
251    type Item = f64;
252
253    fn next(&mut self) -> Option<f64> {
254        let sample = self.amplitude * (2.0 * PI * self.phase).sin();
255        self.phase = (self.phase + self.step).rem_euclid(1.0);
256        Some(sample)
257    }
258}
259
260/// A note with overtones: a [`tone`] at `hz` and one at each whole multiple
261/// of it, the `k`th (from 1, the note itself) at `amplitudes[k - 1]`.
262/// Partials at or above the Nyquist frequency (`rate / 2`) are left out, so
263/// nothing folds back as a lower pitch.
264///
265/// ```
266/// use rich_ext::dsp::{partials, spectrum, tone};
267///
268/// // A note and its octave, half as loud.
269/// let samples: Vec<f64> = partials(100.0, 1000.0, &[1.0, 0.5]).take(1000).collect();
270/// let bins = spectrum(&samples, 1000.0);
271/// assert!((bins[100].1 - 500.0).abs() < 1e-6);
272/// assert!((bins[200].1 - 250.0).abs() < 1e-6);
273/// ```
274pub fn partials(hz: f64, rate: f64, amplitudes: &[f64]) -> Mix {
275    let mut mix = Mix::new();
276    for (k, amplitude) in amplitudes.iter().enumerate() {
277        let partial = hz * (k + 1) as f64;
278        if partial < rate / 2.0 {
279            mix = mix.add(*amplitude, tone(partial, rate));
280        }
281    }
282    mix
283}
284
285/// White noise from -1 to 1 (xorshift), the same for the same `seed`, as
286/// an endless iterator. [`Noise::low_pass`] smooths it into a wander that
287/// falls away with frequency, as music's spectrum does.
288///
289/// ```
290/// use rich_ext::dsp::noise;
291///
292/// let a: Vec<f64> = noise(7).take(64).collect();
293/// assert_eq!(a, noise(7).take(64).collect::<Vec<_>>());
294/// assert!(a.iter().all(|s| (-1.0..1.0).contains(s)));
295/// ```
296pub fn noise(seed: u64) -> Noise {
297    Noise {
298        // xorshift never leaves 0, so the seed is mixed and made odd.
299        state: seed.wrapping_mul(0x9e37_79b9_7f4a_7c15) | 1,
300        smoothing: None,
301        low: 0.0,
302    }
303}
304
305/// White or low-passed noise: see [`noise`].
306#[derive(Clone, Debug, PartialEq)]
307pub struct Noise {
308    state: u64,
309    /// The one-pole low-pass's coefficient, when filtered.
310    smoothing: Option<f64>,
311    /// The filter's last output.
312    low: f64,
313}
314
315impl Noise {
316    /// Through a one-pole low-pass at `hz` (taken at `rate` samples a
317    /// second): fast changes are smoothed away, so the noise wanders.
318    /// Filtering takes energy out: the result is quieter than -1 to 1.
319    #[must_use]
320    pub fn low_pass(mut self, hz: f64, rate: f64) -> Self {
321        let coefficient = if rate > 0.0 {
322            1.0 - (-2.0 * PI * hz / rate).exp()
323        } else {
324            1.0
325        };
326        self.smoothing = Some(coefficient.clamp(0.0, 1.0));
327        self
328    }
329
330    fn white(&mut self) -> f64 {
331        self.state ^= self.state << 13;
332        self.state ^= self.state >> 7;
333        self.state ^= self.state << 17;
334        (self.state >> 11) as f64 / (1u64 << 52) as f64 - 1.0
335    }
336}
337
338impl Iterator for Noise {
339    type Item = f64;
340
341    fn next(&mut self) -> Option<f64> {
342        let white = self.white();
343        Some(match self.smoothing {
344            Some(a) => {
345                self.low += a * (white - self.low);
346                self.low
347            }
348            None => white,
349        })
350    }
351}
352
353/// Several sources added together, each scaled by its gain: tones, noise,
354/// [`partials`], another mix, or any iterator of samples. It ends when
355/// every source has ended (never, with a tone in it); a source that ends
356/// early adds nothing after.
357///
358/// ```
359/// use rich_ext::dsp::{mix, noise, tone};
360///
361/// let rate = 48_000.0;
362/// let signal = mix()
363///     .add(0.6, tone(440.0, rate))
364///     .add(0.2, tone(880.0, rate))
365///     .add(0.05, noise(1).low_pass(300.0, rate));
366/// let buffer: Vec<f64> = signal.take(1024).collect();
367/// assert!(buffer.iter().all(|s| s.abs() <= 0.85));
368/// ```
369pub fn mix() -> Mix {
370    Mix::new()
371}
372
373/// A sum of sources: see [`mix`].
374#[derive(Default)]
375pub struct Mix {
376    parts: Vec<(f64, Box<dyn Iterator<Item = f64> + Send>)>,
377}
378
379impl std::fmt::Debug for Mix {
380    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
381        let gains: Vec<f64> = self.parts.iter().map(|(gain, _)| *gain).collect();
382        f.debug_struct("Mix").field("gains", &gains).finish()
383    }
384}
385
386impl Mix {
387    /// An empty mix, which ends at once.
388    pub fn new() -> Self {
389        Mix::default()
390    }
391
392    /// Add `source`, scaled by `gain`.
393    #[must_use]
394    pub fn add(mut self, gain: f64, source: impl Iterator<Item = f64> + Send + 'static) -> Self {
395        self.parts.push((gain, Box::new(source)));
396        self
397    }
398}
399
400impl Iterator for Mix {
401    type Item = f64;
402
403    fn next(&mut self) -> Option<f64> {
404        let mut sum = 0.0;
405        let mut any = false;
406        for (gain, source) in &mut self.parts {
407            if let Some(sample) = source.next() {
408                sum += *gain * sample;
409                any = true;
410            }
411        }
412        any.then_some(sum)
413    }
414}
415
416/// Which way a signal crosses a [`trigger`] level.
417#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
418pub enum Edge {
419    /// From at or below the level to above it.
420    #[default]
421    Rising,
422    /// From at or above the level to below it.
423    Falling,
424}
425
426/// Where `samples` first cross `level` on `edge` and stay past it for
427/// `depth` samples (at least 1): the index of the last sample before the
428/// crossing, as an oscilloscope's trigger finds where to start its trace
429/// so a periodic wave stands still. `None` when it never does. A greater
430/// depth skips noise that crosses and comes straight back.
431///
432/// ```
433/// use rich_ext::dsp::{trigger, Edge};
434///
435/// let wave = [0.5, -0.2, 0.1, 0.3, 0.4, -0.1];
436/// assert_eq!(trigger(&wave, 0.0, 1, Edge::Rising), Some(1));
437/// assert_eq!(trigger(&wave, 0.0, 1, Edge::Falling), Some(0));
438/// // Past the level for three samples: 0.1, 0.3 and 0.4.
439/// assert_eq!(trigger(&wave, 0.0, 3, Edge::Rising), Some(1));
440/// assert_eq!(trigger(&wave, 0.0, 4, Edge::Rising), None);
441/// ```
442pub fn trigger(samples: &[f64], level: f64, depth: usize, edge: Edge) -> Option<usize> {
443    let depth = depth.max(1);
444    let last = samples.len().checked_sub(depth + 1)?;
445    (0..=last).find(|&i| {
446        let after = &samples[i + 1..=i + depth];
447        match edge {
448            Edge::Rising => samples[i] <= level && after.iter().all(|s| *s > level),
449            Edge::Falling => samples[i] >= level && after.iter().all(|s| *s < level),
450        }
451    })
452}
453
454/// The semitones of the natural notes above C.
455const NATURALS: [(char, i32); 7] = [
456    ('C', 0),
457    ('D', 2),
458    ('E', 4),
459    ('F', 5),
460    ('G', 7),
461    ('A', 9),
462    ('B', 11),
463];
464
465/// The names [`nearest_note`] gives, from C, with sharps.
466const NAMES: [&str; 12] = [
467    "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
468];
469
470/// The frequency in Hz of a note in scientific pitch notation, in equal
471/// temperament with A4 at 440 Hz: a letter (either case), any number of
472/// sharps (`#`, `♯`) or flats (`b`, `♭`), and an octave (`-1` to `9`;
473/// 4, the octave of middle C, when left out). `None` when it is not a
474/// note. [`NoteTable::ScopeTui`] reads notes as scope-tui does instead.
475///
476/// ```
477/// use rich_ext::dsp::note_hz;
478///
479/// assert_eq!(note_hz("A4"), Some(440.0));
480/// assert_eq!(note_hz("a"), Some(440.0));
481/// assert_eq!(note_hz("A5"), Some(880.0));
482/// assert!((note_hz("C4").unwrap() - 261.626).abs() < 0.001);
483/// assert_eq!(note_hz("C#3"), note_hz("Db3"));
484/// assert!((note_hz("C0").unwrap() - 16.352).abs() < 0.001);
485/// assert_eq!(note_hz("H2"), None);
486/// ```
487pub fn note_hz(note: &str) -> Option<f64> {
488    NoteTable::Equal.hz(note)
489}
490
491/// Which pitches [`NoteTable::hz`] gives a note.
492///
493/// ```
494/// use rich_ext::dsp::NoteTable;
495///
496/// // E2: 82.41 Hz in equal temperament; scope-tui's octave-0 E, 20.60 Hz,
497/// // doubled twice: 82.4 Hz (as an f32).
498/// assert!((NoteTable::Equal.hz("E2").unwrap() - 82.407).abs() < 0.001);
499/// assert_eq!(NoteTable::ScopeTui.hz("E2"), Some(f64::from(82.4f32)));
500/// // Without an octave: 4 in equal temperament, 0 in scope-tui's table.
501/// assert_eq!(NoteTable::Equal.hz("A"), Some(440.0));
502/// assert_eq!(NoteTable::ScopeTui.hz("A"), Some(27.5));
503/// ```
504#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
505pub enum NoteTable {
506    /// Equal temperament, A4 at 440 Hz, octave 4 by default: exact
507    /// ([`note_hz`]).
508    #[default]
509    Equal,
510    /// scope-tui's table (`music.rs`, 0.3.5): the twelve pitches of octave
511    /// 0 rounded to the hundredth of a hertz (C0 16.35, …, B0 30.87),
512    /// doubled once an octave in `f32`, octave 0 by default and no octave
513    /// below it. A clone that divides a sample rate by these pitches picks
514    /// the buffer sizes scope-tui's `--tune` does, sample for sample
515    /// (about one note in ten is a sample off equal temperament's).
516    ScopeTui,
517}
518
519/// scope-tui's octave-0 pitches, from C, as it keeps them.
520const SCOPE_TUI_OCTAVE_0: [f32; 12] = [
521    16.35, 17.32, 18.35, 19.45, 20.60, 21.83, 23.12, 24.50, 25.96, 27.50, 29.14, 30.87,
522];
523
524impl NoteTable {
525    /// The octave a note without one is in: 4 ([`Equal`](Self::Equal)) or
526    /// 0 ([`ScopeTui`](Self::ScopeTui)).
527    pub fn default_octave(self) -> i32 {
528        match self {
529            NoteTable::Equal => 4,
530            NoteTable::ScopeTui => 0,
531        }
532    }
533
534    /// The frequency in Hz of `note`, written as [`note_hz`] reads it, in
535    /// this table. `None` when it is not a note (or, in
536    /// [`ScopeTui`](Self::ScopeTui), is below octave 0).
537    pub fn hz(self, note: &str) -> Option<f64> {
538        // Semitones from C0.
539        let from_c0 = parse_note(note, self.default_octave())?;
540        match self {
541            NoteTable::Equal => Some(440.0 * 2f64.powf(f64::from(from_c0 - 57) / 12.0)),
542            NoteTable::ScopeTui => {
543                let octave = u32::try_from(from_c0.div_euclid(12)).ok()?;
544                let mut hz = SCOPE_TUI_OCTAVE_0[from_c0.rem_euclid(12) as usize];
545                for _ in 0..octave {
546                    hz *= 2.0;
547                }
548                Some(f64::from(hz))
549            }
550        }
551    }
552}
553
554/// `note`'s semitones from C0, its octave `default_octave` when it names
555/// none.
556fn parse_note(note: &str, default_octave: i32) -> Option<i32> {
557    let note = note.trim();
558    let mut chars = note.chars();
559    let letter = chars.next()?.to_ascii_uppercase();
560    let (_, mut semitone) = *NATURALS.iter().find(|(name, _)| *name == letter)?;
561    let rest = chars.as_str();
562    let octave_at = rest
563        .find(|c: char| c.is_ascii_digit() || c == '-')
564        .unwrap_or(rest.len());
565    for accidental in rest[..octave_at].chars() {
566        semitone += match accidental {
567            '#' | '♯' => 1,
568            'b' | '♭' => -1,
569            _ => return None,
570        };
571    }
572    let octave: i32 = match &rest[octave_at..] {
573        "" => default_octave,
574        digits => digits.parse().ok().filter(|o| (-1..=9).contains(o))?,
575    };
576    Some(octave * 12 + semitone)
577}
578
579/// The note nearest `hz` in equal temperament (A4 at 440 Hz): its name,
580/// with sharps (`"A"`, `"C#"`), its octave, and how far `hz` is from it in
581/// cents (hundredths of a semitone, from -50 to 50): a tuner's reading.
582/// `None` for a frequency that is not positive and finite, or that falls
583/// outside octaves -1 to 9.
584///
585/// ```
586/// use rich_ext::dsp::nearest_note;
587///
588/// assert_eq!(nearest_note(440.0), Some(("A", 4, 0.0)));
589/// let (name, octave, cents) = nearest_note(445.0).unwrap();
590/// assert_eq!((name, octave), ("A", 4));
591/// assert!((cents - 19.56).abs() < 0.01);
592/// assert_eq!(nearest_note(130.0).map(|(n, o, _)| (n, o)), Some(("C", 3)));
593/// ```
594pub fn nearest_note(hz: f64) -> Option<(&'static str, i32, f64)> {
595    if !(hz.is_finite() && hz > 0.0) {
596        return None;
597    }
598    let from_a4 = 12.0 * (hz / 440.0).log2();
599    let nearest = from_a4.round();
600    let cents = ((from_a4 - nearest) * 100.0 * 1e9).round() / 1e9;
601    // Semitones from C-1.
602    let from_c = nearest as i64 + 9 + 5 * 12;
603    if !(0..11 * 12).contains(&from_c) {
604        return None;
605    }
606    let name = NAMES[(from_c % 12) as usize];
607    Some((name, (from_c / 12 - 1) as i32, cents))
608}
609
610/// The frequency of the loudest bin of `spectrum` (`(Hz, magnitude)`
611/// pairs, lowest first, as [`spectrum`] gives them) above 0 Hz, refined
612/// between bins by the parabola through its and its two neighbours'
613/// logarithms (a [`hann`]-windowed peak's shape), so a
614/// tuner reads finer than the bins are apart. `None` when there is no
615/// bin above 0 Hz louder than zero (silence).
616///
617/// ```
618/// use rich_ext::dsp::{hann, nearest_note, peak_hz, spectrum, tone};
619///
620/// // A4, 2048 samples at 48 kHz: bins 23.4 Hz apart.
621/// let samples: Vec<f64> = tone(440.0, 48_000.0).take(2048).collect();
622/// let hz = peak_hz(&spectrum(&hann(&samples), 48_000.0)).unwrap();
623/// assert!((hz - 440.0).abs() < 1.0, "{hz}");
624/// let (name, octave, cents) = nearest_note(hz).unwrap();
625/// assert_eq!((name, octave), ("A", 4));
626/// assert!(cents.abs() < 5.0);
627/// assert_eq!(peak_hz(&spectrum(&[0.0; 64], 48_000.0)), None);
628/// ```
629pub fn peak_hz(spectrum: &[(f64, f64)]) -> Option<f64> {
630    let (at, &(hz, level)) = spectrum
631        .iter()
632        .enumerate()
633        .filter(|(_, (hz, level))| *hz > 0.0 && level.is_finite())
634        .max_by(|a, b| a.1.1.total_cmp(&b.1.1))?;
635    if level <= 0.0 {
636        return None;
637    }
638    let (Some(&(before_hz, a)), Some(&(after_hz, c))) = (
639        at.checked_sub(1).and_then(|i| spectrum.get(i)),
640        spectrum.get(at + 1),
641    ) else {
642        return Some(hz);
643    };
644    if before_hz <= 0.0 || a <= 0.0 || c <= 0.0 {
645        return Some(hz);
646    }
647    // On the logarithms of the levels, which a windowed peak's top fits
648    // closely.
649    let (a, b, c) = (a.ln(), level.ln(), c.ln());
650    let curve = a - 2.0 * b + c;
651    if !curve.is_finite() || curve >= 0.0 {
652        return Some(hz);
653    }
654    // The vertex, from -½ to ½ a bin from `hz`.
655    let shift = (0.5 * (a - c) / curve).clamp(-0.5, 0.5);
656    let step = if shift < 0.0 {
657        hz - before_hz
658    } else {
659        after_hz - hz
660    };
661    Some(hz + shift * step)
662}
663
664/// `spectrum` (`(Hz, magnitude)` pairs, lowest first) gathered into `n`
665/// bands spaced by the logarithm of frequency from `lo_hz` to `hi_hz`, so
666/// each octave gets as many bands as any other, as cava's bars and a
667/// graphic equaliser's sliders: each band the loudest bin in it, or, for a
668/// band narrower than the bins are apart (the low end), the level between
669/// the bins either side of its middle. Empty for no bands; zeros when the
670/// range is not positive and increasing, or the spectrum is empty.
671///
672/// ```
673/// use rich_ext::dsp::bands;
674///
675/// // Bins 10 Hz apart; one loud at 100 Hz, one at 1 kHz.
676/// let spectrum: Vec<(f64, f64)> = (0..=200)
677///     .map(|i| (i as f64 * 10.0, if i == 10 || i == 100 { 1.0 } else { 0.0 }))
678///     .collect();
679/// // Three bands a decade each: 10-100 Hz, 100-1000, 1000-10000.
680/// assert_eq!(bands(&spectrum, 3, 10.0, 10_000.0), [0.0, 1.0, 1.0]);
681/// ```
682pub fn bands(spectrum: &[(f64, f64)], n: usize, lo_hz: f64, hi_hz: f64) -> Vec<f64> {
683    let mut out = vec![0.0; n];
684    let valid = lo_hz.is_finite() && hi_hz.is_finite() && 0.0 < lo_hz && lo_hz < hi_hz;
685    if n == 0 || spectrum.is_empty() || !valid {
686        return out;
687    }
688    let edge = |k: usize| lo_hz * (hi_hz / lo_hz).powf(k as f64 / n as f64);
689    for (k, band) in out.iter_mut().enumerate() {
690        let (from, to) = (edge(k), edge(k + 1));
691        let last = k + 1 == n;
692        let inside = spectrum
693            .iter()
694            .filter(|(hz, _)| from <= *hz && (*hz < to || (last && *hz <= to)))
695            .map(|(_, level)| *level)
696            .filter(|level| !level.is_nan())
697            .fold(None, |a: Option<f64>, v| Some(a.map_or(v, |a| a.max(v))));
698        *band = inside.unwrap_or_else(|| level_at(spectrum, (from * to).sqrt()));
699    }
700    out
701}
702
703/// `spectrum`'s level at `hz`, a straight line between the bins either
704/// side; the end bin's beyond the ends.
705fn level_at(spectrum: &[(f64, f64)], hz: f64) -> f64 {
706    let after = spectrum.partition_point(|(f, _)| *f < hz);
707    match (
708        after.checked_sub(1).map(|i| spectrum[i]),
709        spectrum.get(after),
710    ) {
711        (Some((f0, a)), Some(&(f1, b))) if f1 > f0 => a + (b - a) * (hz - f0) / (f1 - f0),
712        (_, Some(&(_, b))) => b,
713        (Some((_, a)), None) => a,
714        (None, None) => 0.0,
715    }
716}
717
718/// Levels that rise at once and fall under gravity, as cava's bars do:
719/// a level that drops is not followed down at once, but falls faster and
720/// faster, by `g` more each [`update`](Self::update), until it meets the
721/// new level. Rises are followed at once. Keep one per view, and update it
722/// once a frame with the new levels ([`bands`]).
723///
724/// ```
725/// use rich_ext::dsp::Falloff;
726///
727/// let mut bars = Falloff::gravity(0.1);
728/// assert_eq!(bars.update(&[1.0]), [1.0]);
729/// // Silence: down by 0.1, then 0.2, then 0.3 more, then at the floor.
730/// assert_eq!(bars.update(&[0.0]), [0.9]);
731/// assert!((bars.update(&[0.0])[0] - 0.7).abs() < 1e-12);
732/// assert!((bars.update(&[0.0])[0] - 0.4).abs() < 1e-12);
733/// assert_eq!(bars.update(&[0.0]), [0.0]);
734/// // A rise is shown at once.
735/// assert_eq!(bars.update(&[0.8]), [0.8]);
736/// ```
737#[derive(Clone, Debug, Default, PartialEq)]
738pub struct Falloff {
739    gravity: f64,
740    levels: Vec<f64>,
741    speeds: Vec<f64>,
742}
743
744impl Falloff {
745    /// Levels falling `g` faster each update (in the levels' units per
746    /// update, per update); 0 or less follows every drop at once.
747    pub fn gravity(g: f64) -> Self {
748        Falloff {
749            gravity: if g.is_finite() { g.max(0.0) } else { 0.0 },
750            levels: Vec::new(),
751            speeds: Vec::new(),
752        }
753    }
754
755    /// Take the newest levels and return those to show. A level the last
756    /// update did not have starts where it is; a NaN level is taken as
757    /// silence, so the shown level falls.
758    pub fn update(&mut self, levels: &[f64]) -> &[f64] {
759        self.levels.truncate(levels.len());
760        self.speeds.truncate(levels.len());
761        for (i, &level) in levels.iter().enumerate() {
762            let level = if level.is_nan() {
763                f64::NEG_INFINITY
764            } else {
765                level
766            };
767            if i == self.levels.len() {
768                self.levels.push(level);
769                self.speeds.push(0.0);
770                continue;
771            }
772            if level >= self.levels[i] || self.gravity == 0.0 {
773                self.levels[i] = level;
774                self.speeds[i] = 0.0;
775            } else {
776                self.speeds[i] += self.gravity;
777                self.levels[i] = (self.levels[i] - self.speeds[i]).max(level);
778            }
779        }
780        &self.levels
781    }
782
783    /// The levels the last update returned.
784    pub fn levels(&self) -> &[f64] {
785        &self.levels
786    }
787
788    /// Forget the levels: the next update starts afresh.
789    pub fn reset(&mut self) {
790        self.levels.clear();
791        self.speeds.clear();
792    }
793}
794
795/// What a [`Tuner`] reads: the frequency heard, the note nearest it (in
796/// equal temperament, A4 at 440 Hz) and how far off it is.
797#[derive(Clone, Copy, Debug, PartialEq)]
798pub struct Reading {
799    /// The frequency, smoothed, in Hz.
800    pub hz: f64,
801    /// The nearest note's name (`"A"`, `"C#"`).
802    pub note: &'static str,
803    /// Its octave (A4 is octave 4).
804    pub octave: i32,
805    /// How far the frequency is from the note, in cents (-50 to 50): sharp
806    /// above 0, flat below.
807    pub cents: f64,
808}
809
810impl Reading {
811    /// The note with its octave: `A4`, `C#3`.
812    pub fn name(&self) -> String {
813        format!("{}{}", self.note, self.octave)
814    }
815
816    /// Whether it is within `cents` of the note: in tune.
817    pub fn in_tune(&self, cents: f64) -> bool {
818        self.cents.abs() <= cents
819    }
820}
821
822/// A tuner's reading of a spectrum, as a guitar tuner shows it: the
823/// loudest peak ([`peak_hz`]), the nearest note to it ([`nearest_note`])
824/// and the cents between, smoothed from one spectrum to the next so the
825/// needle settles rather than jitters. A peak more than half a semitone
826/// from the smoothed one is a new note, taken at once; quieter than the
827/// [`floor`](Self::floor), the tuner hears nothing, and after
828/// [`hold`](Self::hold) spectra of nothing it lets the reading go.
829///
830/// ```
831/// use rich_ext::dsp::{hann, spectrum, tone, Tuner};
832///
833/// let mut tuner = Tuner::new();
834/// // A string a little sharp of A4.
835/// let samples: Vec<f64> = tone(442.0, 48_000.0).take(4096).collect();
836/// let reading = tuner.listen(&samples, 48_000.0).unwrap();
837/// assert_eq!(reading.name(), "A4");
838/// assert!((reading.cents - 7.85).abs() < 1.0, "{reading:?}");
839/// // Silence: the last reading is held for a while, then let go.
840/// let quiet = spectrum(&hann(&[0.0; 4096]), 48_000.0);
841/// assert!(tuner.update(&quiet).is_some());
842/// for _ in 0..10 {
843///     tuner.update(&quiet);
844/// }
845/// assert_eq!(tuner.reading(), None);
846/// ```
847#[derive(Clone, Debug, PartialEq)]
848pub struct Tuner {
849    smoothing: f64,
850    floor: f64,
851    hold: usize,
852    hz: Option<f64>,
853    quiet: usize,
854}
855
856impl Default for Tuner {
857    fn default() -> Self {
858        Tuner::new()
859    }
860}
861
862impl Tuner {
863    /// A tuner smoothing by half, hearing any peak above 0.001, holding a
864    /// reading through four quiet spectra.
865    pub fn new() -> Self {
866        Tuner {
867            smoothing: 0.5,
868            floor: 0.001,
869            hold: 4,
870            hz: None,
871            quiet: 0,
872        }
873    }
874
875    /// How much of the last reading each new one keeps, from 0 (none: each
876    /// spectrum read alone) to just under 1 (barely moving); default a
877    /// half.
878    #[must_use]
879    pub fn smoothing(mut self, keep: f64) -> Self {
880        if keep.is_finite() {
881            self.smoothing = keep.clamp(0.0, 0.99);
882        }
883        self
884    }
885
886    /// The quietest peak heard (a magnitude, as [`spectrum`] gives them);
887    /// default 0.001.
888    #[must_use]
889    pub fn floor(mut self, level: f64) -> Self {
890        if level.is_finite() {
891            self.floor = level.max(0.0);
892        }
893        self
894    }
895
896    /// How many spectra with nothing heard keep the last reading (default
897    /// 4), so a note that dips does not blink out.
898    #[must_use]
899    pub fn hold(mut self, spectra: usize) -> Self {
900        self.hold = spectra;
901        self
902    }
903
904    /// Read `spectrum` (`(Hz, magnitude)` pairs, lowest first, as
905    /// [`spectrum`] gives them): the reading now, smoothed.
906    pub fn update(&mut self, spectrum: &[(f64, f64)]) -> Option<Reading> {
907        let loudest = spectrum
908            .iter()
909            .filter(|(hz, _)| *hz > 0.0)
910            .map(|(_, level)| *level)
911            .fold(0.0f64, f64::max);
912        let heard = (loudest > self.floor)
913            .then(|| peak_hz(spectrum))
914            .flatten()
915            .filter(|hz| nearest_note(*hz).is_some());
916        match heard {
917            Some(hz) => {
918                self.quiet = 0;
919                self.hz = Some(match self.hz {
920                    // Within half a semitone: the same note, smoothed in
921                    // pitch (on the log of the frequency).
922                    Some(old) if (1200.0 * (hz / old).log2()).abs() < 50.0 => {
923                        (self.smoothing * old.log2() + (1.0 - self.smoothing) * hz.log2()).exp2()
924                    }
925                    _ => hz,
926                });
927            }
928            None => {
929                self.quiet += 1;
930                if self.quiet > self.hold {
931                    self.hz = None;
932                }
933            }
934        }
935        self.reading()
936    }
937
938    /// Read `samples` taken at `rate` samples a second: windowed
939    /// ([`hann`]) and transformed ([`spectrum`]), then
940    /// [`update`](Self::update).
941    pub fn listen(&mut self, samples: &[f64], rate: f64) -> Option<Reading> {
942        self.update(&spectrum(&hann(samples), rate))
943    }
944
945    /// The reading the last update gave.
946    pub fn reading(&self) -> Option<Reading> {
947        let hz = self.hz?;
948        let (note, octave, cents) = nearest_note(hz)?;
949        Some(Reading {
950            hz,
951            note,
952            octave,
953            cents,
954        })
955    }
956
957    /// Forget the reading: the next peak is taken as it is.
958    pub fn reset(&mut self) {
959        self.hz = None;
960        self.quiet = 0;
961    }
962}
963
964#[cfg(test)]
965mod tests {
966    use super::*;
967
968    #[test]
969    fn a_tuner_smooths_within_a_note_and_jumps_to_another() {
970        let rate = 48_000.0;
971        let read = |hz: f64| -> Vec<(f64, f64)> {
972            let samples: Vec<f64> = tone(hz, rate).take(8192).collect();
973            spectrum(&hann(&samples), rate)
974        };
975        let mut tuner = Tuner::new().smoothing(0.5);
976        let first = tuner.update(&read(440.0)).unwrap();
977        assert_eq!(first.name(), "A4");
978        assert!(first.in_tune(1.0), "{first:?}");
979        // Ten cents sharp: half way there after one more spectrum.
980        let sharp = 440.0 * 2f64.powf(10.0 / 1200.0);
981        let next = tuner.update(&read(sharp)).unwrap();
982        assert!((next.cents - 5.0).abs() < 1.0, "{next:?}");
983        // E2, a new note: taken at once.
984        let low = tuner.update(&read(82.41)).unwrap();
985        assert_eq!(low.name(), "E2");
986        assert!(low.cents.abs() < 5.0, "{low:?}");
987        tuner.reset();
988        assert_eq!(tuner.reading(), None);
989    }
990
991    /// The DFT by its definition, to check the fast paths against.
992    fn naive(samples: &[f64]) -> Vec<Complex> {
993        let n = samples.len();
994        (0..n)
995            .map(|k| {
996                samples.iter().enumerate().fold((0.0, 0.0), |acc, (j, s)| {
997                    let angle = -2.0 * PI * (j * k % n) as f64 / n as f64;
998                    (acc.0 + s * angle.cos(), acc.1 + s * angle.sin())
999                })
1000            })
1001            .collect()
1002    }
1003
1004    fn close(a: &[Complex], b: &[Complex]) -> bool {
1005        a.len() == b.len()
1006            && a.iter()
1007                .zip(b)
1008                .all(|(x, y)| (x.0 - y.0).abs() < 1e-7 && (x.1 - y.1).abs() < 1e-7)
1009    }
1010
1011    fn random(n: usize) -> Vec<f64> {
1012        // A small LCG: deterministic, no dependency.
1013        let mut state = 0x2545_f491_u64;
1014        (0..n)
1015            .map(|_| {
1016                state = state
1017                    .wrapping_mul(6_364_136_223_846_793_005)
1018                    .wrapping_add(1);
1019                (state >> 33) as f64 / (1u64 << 31) as f64 - 1.0
1020            })
1021            .collect()
1022    }
1023
1024    #[test]
1025    fn matches_the_definition_at_every_length() {
1026        for n in [1, 2, 3, 4, 5, 7, 8, 12, 16, 100, 128, 441, 1000] {
1027            let samples = random(n);
1028            assert!(close(&fft(&samples), &naive(&samples)), "length {n}");
1029        }
1030        assert!(fft(&[]).is_empty());
1031    }
1032
1033    #[test]
1034    fn a_tone_peaks_at_its_frequency() {
1035        // 4410 samples at 44.1 kHz: 10 Hz bins, a length Bluestein handles.
1036        let rate = 44_100.0;
1037        for hz in [440.0, 1000.0, 12_340.0] {
1038            let tone: Vec<f64> = (0..4410)
1039                .map(|i| (2.0 * PI * hz * i as f64 / rate).sin())
1040                .collect();
1041            let bins = spectrum(&hann(&tone), rate);
1042            assert_eq!(bins.len(), 2206);
1043            let (peak, level) = bins
1044                .iter()
1045                .copied()
1046                .max_by(|a, b| a.1.total_cmp(&b.1))
1047                .unwrap();
1048            assert!((peak - hz).abs() <= 10.0, "{hz}: peak at {peak}");
1049            // A Hann window halves a sine's peak: n / 4.
1050            assert!((level - 4410.0 / 4.0).abs() < 4410.0 * 0.05, "{level}");
1051        }
1052    }
1053
1054    #[test]
1055    fn the_window_tapers_to_zero() {
1056        let window = hann(&[1.0; 8]);
1057        assert_eq!(window[0], 0.0);
1058        assert!((window[4] - 1.0).abs() < 1e-12);
1059        assert!((window[2] - 0.5).abs() < 1e-12);
1060        assert!(hann(&[]).is_empty());
1061    }
1062
1063    #[test]
1064    fn a_constant_is_all_in_the_first_bin() {
1065        let bins = spectrum(&[0.5; 64], 64.0);
1066        assert_eq!(bins.len(), 33);
1067        assert!((bins[0].1 - 32.0).abs() < 1e-9);
1068        assert!(bins[1..].iter().all(|(_, m)| *m < 1e-9));
1069        assert!(spectrum(&[], 48_000.0).is_empty());
1070    }
1071    #[test]
1072    fn the_full_spectrum_mirrors_the_half() {
1073        let samples = noise(9).take(16).collect::<Vec<_>>();
1074        let full = spectrum_full(&samples, 16.0);
1075        let half = spectrum(&samples, 16.0);
1076        assert_eq!(full.len(), 16);
1077        assert_eq!(&full[..9], &half[..]);
1078        // A real signal's bins above the Nyquist frequency mirror those below.
1079        for k in 1..8 {
1080            assert!((full[k].1 - full[16 - k].1).abs() < 1e-9);
1081        }
1082        assert!(spectrum_full(&[], 1.0).is_empty());
1083    }
1084
1085    #[test]
1086    fn a_tone_keeps_its_phase_across_buffers() {
1087        let mut wave = tone(440.0, 48_000.0).amplitude(0.5);
1088        let first: Vec<f64> = wave.by_ref().take(100).collect();
1089        let second: Vec<f64> = wave.take(100).collect();
1090        let whole: Vec<f64> = tone(440.0, 48_000.0).amplitude(0.5).take(200).collect();
1091        assert_eq!([first, second].concat(), whole);
1092        let shifted: Vec<f64> = tone(1.0, 4.0).phase(PI / 2.0).take(2).collect();
1093        assert!((shifted[0] - 1.0).abs() < 1e-12 && shifted[1].abs() < 1e-12);
1094    }
1095
1096    #[test]
1097    fn partials_above_nyquist_are_left_out() {
1098        // 300 Hz at 1 kHz: the second partial (600 Hz) would alias.
1099        let mix = partials(300.0, 1000.0, &[1.0, 1.0, 1.0]);
1100        assert_eq!(mix.parts.len(), 1);
1101    }
1102
1103    #[test]
1104    fn filtered_noise_is_smoother() {
1105        let white: Vec<f64> = noise(3).take(4096).collect();
1106        let low: Vec<f64> = noise(3).low_pass(100.0, 48_000.0).take(4096).collect();
1107        let roughness = |s: &[f64]| s.windows(2).map(|w| (w[1] - w[0]).abs()).sum::<f64>();
1108        assert!(roughness(&low) * 50.0 < roughness(&white));
1109        assert_ne!(noise(1).next(), noise(2).next());
1110    }
1111
1112    #[test]
1113    fn a_mix_ends_when_every_source_has() {
1114        let a = [1.0, 2.0, 3.0].into_iter();
1115        let b = [10.0].into_iter();
1116        let out: Vec<f64> = mix().add(1.0, a).add(0.5, b).collect();
1117        assert_eq!(out, [6.0, 2.0, 3.0]);
1118        assert_eq!(Mix::new().next(), None);
1119    }
1120
1121    #[test]
1122    fn triggers_respect_the_depth_and_the_end() {
1123        let wave = [0.0, 1.0, -1.0, 1.0, 1.0];
1124        assert_eq!(trigger(&wave, 0.5, 1, Edge::Rising), Some(0));
1125        assert_eq!(trigger(&wave, 0.5, 2, Edge::Rising), Some(2));
1126        assert_eq!(trigger(&wave, 0.5, 3, Edge::Rising), None);
1127        assert_eq!(trigger(&wave, 0.0, 1, Edge::Falling), Some(1));
1128        // Too short for the depth.
1129        assert_eq!(trigger(&[0.0], 0.5, 1, Edge::Rising), None);
1130        assert_eq!(trigger(&[], 0.5, 0, Edge::Rising), None);
1131    }
1132
1133    #[test]
1134    fn notes_round_trip() {
1135        for name in ["C-1", "A0", "C4", "F#5", "B9", "G#2"] {
1136            let hz = note_hz(name).unwrap();
1137            let (letter, octave, cents) = nearest_note(hz).unwrap();
1138            assert_eq!(format!("{letter}{octave}"), name);
1139            assert_eq!(cents, 0.0, "{name}");
1140        }
1141        assert_eq!(note_hz("Bb3"), note_hz("A#3"));
1142        assert_eq!(note_hz("B#3"), note_hz("C4"));
1143        assert_eq!(note_hz("E♭4"), note_hz("D#4"));
1144        for bad in ["", "X4", "A10", "A-2", "Ax4", "A4.5"] {
1145            assert_eq!(note_hz(bad), None, "{bad}");
1146        }
1147        assert_eq!(nearest_note(0.0), None);
1148        assert_eq!(nearest_note(f64::NAN), None);
1149        assert_eq!(nearest_note(1.0), None);
1150    }
1151
1152    #[test]
1153    fn scope_tuis_table_is_its_octave_0_doubled() {
1154        let t = NoteTable::ScopeTui;
1155        assert_eq!(t.hz("C"), Some(f64::from(16.35f32)));
1156        assert_eq!(t.hz("B0"), Some(f64::from(30.87f32)));
1157        assert_eq!(t.hz("Eb3"), t.hz("D#3"));
1158        assert_eq!(t.hz("A4"), Some(440.0));
1159        // B# is the next octave's C, as in equal temperament.
1160        assert_eq!(t.hz("B#3"), t.hz("C4"));
1161        // No octave below 0; Cb0 would be B-1.
1162        assert_eq!(t.hz("C-1"), None);
1163        assert_eq!(t.hz("Cb0"), None);
1164        assert_eq!(t.hz("H"), None);
1165    }
1166
1167    #[test]
1168    fn scope_tuis_buffer_sizes_come_out_sample_for_sample() {
1169        // scope-tui's `--tune`: `(rate as f32 * (1.0 / freq)).round()`.
1170        // Dividing in f64 by the table's pitch gives the same size for
1171        // every note it can read at the usual rates.
1172        for rate in [44_100u32, 48_000, 96_000] {
1173            for octave in 0..=8 {
1174                for name in NAMES {
1175                    let note = format!("{name}{octave}");
1176                    let hz = NoteTable::ScopeTui.hz(&note).unwrap();
1177                    let ours = (f64::from(rate) / hz).round() as u32;
1178                    let theirs = (rate as f32 * (1.0 / hz as f32)).round() as u32;
1179                    assert_eq!(ours, theirs, "{note} at {rate}");
1180                }
1181            }
1182        }
1183        // E2 at 48 kHz: 583 samples in scope-tui's table, 582 in equal
1184        // temperament.
1185        let size = |table: NoteTable| (48_000.0 / table.hz("E2").unwrap()).round();
1186        assert_eq!(size(NoteTable::ScopeTui), 583.0);
1187        assert_eq!(size(NoteTable::Equal), 582.0);
1188    }
1189
1190    #[test]
1191    fn peaks_are_refined_between_bins() {
1192        // 1003 Hz with 10 Hz bins: between the 1000 and 1010 Hz bins.
1193        let rate = 10_240.0;
1194        let samples: Vec<f64> = tone(1003.0, rate).take(1024).collect();
1195        let bins = spectrum(&hann(&samples), rate);
1196        let hz = peak_hz(&bins).unwrap();
1197        assert!((hz - 1003.0).abs() < 1.0, "{hz}");
1198        // The 0 Hz bin never wins, however loud.
1199        assert_eq!(peak_hz(&[(0.0, 9.0), (10.0, 1.0)]), Some(10.0));
1200        assert_eq!(peak_hz(&[]), None);
1201        assert_eq!(peak_hz(&[(0.0, 9.0)]), None);
1202    }
1203
1204    #[test]
1205    fn narrow_bands_take_the_level_between_bins() {
1206        // Bins 100 Hz apart rising 1 a bin: a band around 150 Hz holds no
1207        // bin, so it reads between 100 and 200 Hz.
1208        let spectrum: Vec<(f64, f64)> = (0..10).map(|i| (i as f64 * 100.0, i as f64)).collect();
1209        let b = bands(&spectrum, 1, 140.0, 160.0);
1210        assert!((b[0] - 1.497).abs() < 0.01, "{b:?}");
1211        assert!(bands(&spectrum, 0, 1.0, 2.0).is_empty());
1212        assert_eq!(bands(&spectrum, 2, 0.0, 100.0), [0.0, 0.0]);
1213        assert_eq!(bands(&[], 2, 1.0, 100.0), [0.0, 0.0]);
1214        // The top edge is in the last band.
1215        assert_eq!(bands(&spectrum, 2, 100.0, 900.0)[1], 9.0);
1216    }
1217
1218    #[test]
1219    fn a_falloff_follows_more_bands_and_nan() {
1220        let mut f = Falloff::gravity(0.5);
1221        assert_eq!(f.update(&[1.0]), [1.0]);
1222        // A new band starts where it is; NaN falls.
1223        assert_eq!(f.update(&[f64::NAN, 2.0]), [0.5, 2.0]);
1224        assert_eq!(f.update(&[0.0]), [0.0]);
1225        f.reset();
1226        assert!(f.levels().is_empty());
1227        let mut instant = Falloff::gravity(0.0);
1228        instant.update(&[1.0]);
1229        assert_eq!(instant.update(&[0.25]), [0.25]);
1230    }
1231}