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(¬e).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}