use std::f64::consts::PI;
pub type Complex = (f64, f64);
fn mul((a, b): Complex, (c, d): Complex) -> Complex {
(a * c - b * d, a * d + b * c)
}
pub fn magnitude((re, im): Complex) -> f64 {
re.hypot(im)
}
pub fn hann(samples: &[f64]) -> Vec<f64> {
let n = samples.len() as f64;
samples
.iter()
.enumerate()
.map(|(i, s)| s * 0.5 * (1.0 - (2.0 * PI * i as f64 / n).cos()))
.collect()
}
fn radix2(data: &mut [Complex]) {
let n = data.len();
if n < 2 {
return;
}
let bits = n.trailing_zeros();
for i in 0..n {
let j = i.reverse_bits() >> (usize::BITS - bits);
if i < j {
data.swap(i, j);
}
}
let mut len = 2;
while len <= n {
let angle = -2.0 * PI / len as f64;
for start in (0..n).step_by(len) {
for k in 0..len / 2 {
let w = ((angle * k as f64).cos(), (angle * k as f64).sin());
let (a, b) = (data[start + k], mul(data[start + k + len / 2], w));
data[start + k] = (a.0 + b.0, a.1 + b.1);
data[start + k + len / 2] = (a.0 - b.0, a.1 - b.1);
}
}
len *= 2;
}
}
pub fn fft(samples: &[f64]) -> Vec<Complex> {
let n = samples.len();
if n == 0 {
return Vec::new();
}
if n.is_power_of_two() {
let mut data: Vec<Complex> = samples.iter().map(|s| (*s, 0.0)).collect();
radix2(&mut data);
return data;
}
let m = (2 * n).saturating_sub(1).next_power_of_two();
let chirp: Vec<Complex> = (0..n)
.map(|j| {
let angle = -PI * ((j * j) % (2 * n)) as f64 / n as f64;
(angle.cos(), angle.sin())
})
.collect();
let mut a = vec![(0.0, 0.0); m];
for (j, s) in samples.iter().enumerate() {
a[j] = (s * chirp[j].0, s * chirp[j].1);
}
let mut b = vec![(0.0, 0.0); m];
for (j, w) in chirp.iter().enumerate() {
b[j] = (w.0, -w.1);
if j > 0 {
b[m - j] = (w.0, -w.1);
}
}
radix2(&mut a);
radix2(&mut b);
let mut c: Vec<Complex> = a
.iter()
.zip(&b)
.map(|(x, y)| {
let p = mul(*x, *y);
(p.0, -p.1)
})
.collect();
radix2(&mut c);
let scale = m as f64;
(0..n)
.map(|k| mul((c[k].0 / scale, -c[k].1 / scale), chirp[k]))
.collect()
}
pub fn spectrum_full(samples: &[f64], rate: f64) -> Vec<(f64, f64)> {
let n = samples.len();
if n == 0 {
return Vec::new();
}
let resolution = rate / n as f64;
fft(samples)
.into_iter()
.enumerate()
.map(|(i, bin)| (i as f64 * resolution, magnitude(bin)))
.collect()
}
pub fn spectrum(samples: &[f64], rate: f64) -> Vec<(f64, f64)> {
let n = samples.len();
if n == 0 {
return Vec::new();
}
let resolution = rate / n as f64;
fft(samples)
.into_iter()
.take(n / 2 + 1)
.enumerate()
.map(|(i, bin)| (i as f64 * resolution, magnitude(bin)))
.collect()
}
pub fn tone(hz: f64, rate: f64) -> Tone {
Tone {
step: if rate > 0.0 { hz / rate } else { 0.0 },
phase: 0.0,
amplitude: 1.0,
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Tone {
step: f64,
phase: f64,
amplitude: f64,
}
impl Tone {
#[must_use]
pub fn amplitude(mut self, amplitude: f64) -> Self {
self.amplitude = amplitude;
self
}
#[must_use]
pub fn phase(mut self, radians: f64) -> Self {
self.phase = (radians / (2.0 * PI)).rem_euclid(1.0);
self
}
}
impl Iterator for Tone {
type Item = f64;
fn next(&mut self) -> Option<f64> {
let sample = self.amplitude * (2.0 * PI * self.phase).sin();
self.phase = (self.phase + self.step).rem_euclid(1.0);
Some(sample)
}
}
pub fn partials(hz: f64, rate: f64, amplitudes: &[f64]) -> Mix {
let mut mix = Mix::new();
for (k, amplitude) in amplitudes.iter().enumerate() {
let partial = hz * (k + 1) as f64;
if partial < rate / 2.0 {
mix = mix.add(*amplitude, tone(partial, rate));
}
}
mix
}
pub fn noise(seed: u64) -> Noise {
Noise {
state: seed.wrapping_mul(0x9e37_79b9_7f4a_7c15) | 1,
smoothing: None,
low: 0.0,
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Noise {
state: u64,
smoothing: Option<f64>,
low: f64,
}
impl Noise {
#[must_use]
pub fn low_pass(mut self, hz: f64, rate: f64) -> Self {
let coefficient = if rate > 0.0 {
1.0 - (-2.0 * PI * hz / rate).exp()
} else {
1.0
};
self.smoothing = Some(coefficient.clamp(0.0, 1.0));
self
}
fn white(&mut self) -> f64 {
self.state ^= self.state << 13;
self.state ^= self.state >> 7;
self.state ^= self.state << 17;
(self.state >> 11) as f64 / (1u64 << 52) as f64 - 1.0
}
}
impl Iterator for Noise {
type Item = f64;
fn next(&mut self) -> Option<f64> {
let white = self.white();
Some(match self.smoothing {
Some(a) => {
self.low += a * (white - self.low);
self.low
}
None => white,
})
}
}
pub fn mix() -> Mix {
Mix::new()
}
#[derive(Default)]
pub struct Mix {
parts: Vec<(f64, Box<dyn Iterator<Item = f64> + Send>)>,
}
impl std::fmt::Debug for Mix {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let gains: Vec<f64> = self.parts.iter().map(|(gain, _)| *gain).collect();
f.debug_struct("Mix").field("gains", &gains).finish()
}
}
impl Mix {
pub fn new() -> Self {
Mix::default()
}
#[must_use]
pub fn add(mut self, gain: f64, source: impl Iterator<Item = f64> + Send + 'static) -> Self {
self.parts.push((gain, Box::new(source)));
self
}
}
impl Iterator for Mix {
type Item = f64;
fn next(&mut self) -> Option<f64> {
let mut sum = 0.0;
let mut any = false;
for (gain, source) in &mut self.parts {
if let Some(sample) = source.next() {
sum += *gain * sample;
any = true;
}
}
any.then_some(sum)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum Edge {
#[default]
Rising,
Falling,
}
pub fn trigger(samples: &[f64], level: f64, depth: usize, edge: Edge) -> Option<usize> {
let depth = depth.max(1);
let last = samples.len().checked_sub(depth + 1)?;
(0..=last).find(|&i| {
let after = &samples[i + 1..=i + depth];
match edge {
Edge::Rising => samples[i] <= level && after.iter().all(|s| *s > level),
Edge::Falling => samples[i] >= level && after.iter().all(|s| *s < level),
}
})
}
const NATURALS: [(char, i32); 7] = [
('C', 0),
('D', 2),
('E', 4),
('F', 5),
('G', 7),
('A', 9),
('B', 11),
];
const NAMES: [&str; 12] = [
"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
];
pub fn note_hz(note: &str) -> Option<f64> {
NoteTable::Equal.hz(note)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum NoteTable {
#[default]
Equal,
ScopeTui,
}
const SCOPE_TUI_OCTAVE_0: [f32; 12] = [
16.35, 17.32, 18.35, 19.45, 20.60, 21.83, 23.12, 24.50, 25.96, 27.50, 29.14, 30.87,
];
impl NoteTable {
pub fn default_octave(self) -> i32 {
match self {
NoteTable::Equal => 4,
NoteTable::ScopeTui => 0,
}
}
pub fn hz(self, note: &str) -> Option<f64> {
let from_c0 = parse_note(note, self.default_octave())?;
match self {
NoteTable::Equal => Some(440.0 * 2f64.powf(f64::from(from_c0 - 57) / 12.0)),
NoteTable::ScopeTui => {
let octave = u32::try_from(from_c0.div_euclid(12)).ok()?;
let mut hz = SCOPE_TUI_OCTAVE_0[from_c0.rem_euclid(12) as usize];
for _ in 0..octave {
hz *= 2.0;
}
Some(f64::from(hz))
}
}
}
}
fn parse_note(note: &str, default_octave: i32) -> Option<i32> {
let note = note.trim();
let mut chars = note.chars();
let letter = chars.next()?.to_ascii_uppercase();
let (_, mut semitone) = *NATURALS.iter().find(|(name, _)| *name == letter)?;
let rest = chars.as_str();
let octave_at = rest
.find(|c: char| c.is_ascii_digit() || c == '-')
.unwrap_or(rest.len());
for accidental in rest[..octave_at].chars() {
semitone += match accidental {
'#' | '♯' => 1,
'b' | 'â™' => -1,
_ => return None,
};
}
let octave: i32 = match &rest[octave_at..] {
"" => default_octave,
digits => digits.parse().ok().filter(|o| (-1..=9).contains(o))?,
};
Some(octave * 12 + semitone)
}
pub fn nearest_note(hz: f64) -> Option<(&'static str, i32, f64)> {
if !(hz.is_finite() && hz > 0.0) {
return None;
}
let from_a4 = 12.0 * (hz / 440.0).log2();
let nearest = from_a4.round();
let cents = ((from_a4 - nearest) * 100.0 * 1e9).round() / 1e9;
let from_c = nearest as i64 + 9 + 5 * 12;
if !(0..11 * 12).contains(&from_c) {
return None;
}
let name = NAMES[(from_c % 12) as usize];
Some((name, (from_c / 12 - 1) as i32, cents))
}
pub fn peak_hz(spectrum: &[(f64, f64)]) -> Option<f64> {
let (at, &(hz, level)) = spectrum
.iter()
.enumerate()
.filter(|(_, (hz, level))| *hz > 0.0 && level.is_finite())
.max_by(|a, b| a.1.1.total_cmp(&b.1.1))?;
if level <= 0.0 {
return None;
}
let (Some(&(before_hz, a)), Some(&(after_hz, c))) = (
at.checked_sub(1).and_then(|i| spectrum.get(i)),
spectrum.get(at + 1),
) else {
return Some(hz);
};
if before_hz <= 0.0 || a <= 0.0 || c <= 0.0 {
return Some(hz);
}
let (a, b, c) = (a.ln(), level.ln(), c.ln());
let curve = a - 2.0 * b + c;
if !curve.is_finite() || curve >= 0.0 {
return Some(hz);
}
let shift = (0.5 * (a - c) / curve).clamp(-0.5, 0.5);
let step = if shift < 0.0 {
hz - before_hz
} else {
after_hz - hz
};
Some(hz + shift * step)
}
pub fn bands(spectrum: &[(f64, f64)], n: usize, lo_hz: f64, hi_hz: f64) -> Vec<f64> {
let mut out = vec![0.0; n];
let valid = lo_hz.is_finite() && hi_hz.is_finite() && 0.0 < lo_hz && lo_hz < hi_hz;
if n == 0 || spectrum.is_empty() || !valid {
return out;
}
let edge = |k: usize| lo_hz * (hi_hz / lo_hz).powf(k as f64 / n as f64);
for (k, band) in out.iter_mut().enumerate() {
let (from, to) = (edge(k), edge(k + 1));
let last = k + 1 == n;
let inside = spectrum
.iter()
.filter(|(hz, _)| from <= *hz && (*hz < to || (last && *hz <= to)))
.map(|(_, level)| *level)
.filter(|level| !level.is_nan())
.fold(None, |a: Option<f64>, v| Some(a.map_or(v, |a| a.max(v))));
*band = inside.unwrap_or_else(|| level_at(spectrum, (from * to).sqrt()));
}
out
}
fn level_at(spectrum: &[(f64, f64)], hz: f64) -> f64 {
let after = spectrum.partition_point(|(f, _)| *f < hz);
match (
after.checked_sub(1).map(|i| spectrum[i]),
spectrum.get(after),
) {
(Some((f0, a)), Some(&(f1, b))) if f1 > f0 => a + (b - a) * (hz - f0) / (f1 - f0),
(_, Some(&(_, b))) => b,
(Some((_, a)), None) => a,
(None, None) => 0.0,
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Falloff {
gravity: f64,
levels: Vec<f64>,
speeds: Vec<f64>,
}
impl Falloff {
pub fn gravity(g: f64) -> Self {
Falloff {
gravity: if g.is_finite() { g.max(0.0) } else { 0.0 },
levels: Vec::new(),
speeds: Vec::new(),
}
}
pub fn update(&mut self, levels: &[f64]) -> &[f64] {
self.levels.truncate(levels.len());
self.speeds.truncate(levels.len());
for (i, &level) in levels.iter().enumerate() {
let level = if level.is_nan() {
f64::NEG_INFINITY
} else {
level
};
if i == self.levels.len() {
self.levels.push(level);
self.speeds.push(0.0);
continue;
}
if level >= self.levels[i] || self.gravity == 0.0 {
self.levels[i] = level;
self.speeds[i] = 0.0;
} else {
self.speeds[i] += self.gravity;
self.levels[i] = (self.levels[i] - self.speeds[i]).max(level);
}
}
&self.levels
}
pub fn levels(&self) -> &[f64] {
&self.levels
}
pub fn reset(&mut self) {
self.levels.clear();
self.speeds.clear();
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Reading {
pub hz: f64,
pub note: &'static str,
pub octave: i32,
pub cents: f64,
}
impl Reading {
pub fn name(&self) -> String {
format!("{}{}", self.note, self.octave)
}
pub fn in_tune(&self, cents: f64) -> bool {
self.cents.abs() <= cents
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Tuner {
smoothing: f64,
floor: f64,
hold: usize,
hz: Option<f64>,
quiet: usize,
}
impl Default for Tuner {
fn default() -> Self {
Tuner::new()
}
}
impl Tuner {
pub fn new() -> Self {
Tuner {
smoothing: 0.5,
floor: 0.001,
hold: 4,
hz: None,
quiet: 0,
}
}
#[must_use]
pub fn smoothing(mut self, keep: f64) -> Self {
if keep.is_finite() {
self.smoothing = keep.clamp(0.0, 0.99);
}
self
}
#[must_use]
pub fn floor(mut self, level: f64) -> Self {
if level.is_finite() {
self.floor = level.max(0.0);
}
self
}
#[must_use]
pub fn hold(mut self, spectra: usize) -> Self {
self.hold = spectra;
self
}
pub fn update(&mut self, spectrum: &[(f64, f64)]) -> Option<Reading> {
let loudest = spectrum
.iter()
.filter(|(hz, _)| *hz > 0.0)
.map(|(_, level)| *level)
.fold(0.0f64, f64::max);
let heard = (loudest > self.floor)
.then(|| peak_hz(spectrum))
.flatten()
.filter(|hz| nearest_note(*hz).is_some());
match heard {
Some(hz) => {
self.quiet = 0;
self.hz = Some(match self.hz {
Some(old) if (1200.0 * (hz / old).log2()).abs() < 50.0 => {
(self.smoothing * old.log2() + (1.0 - self.smoothing) * hz.log2()).exp2()
}
_ => hz,
});
}
None => {
self.quiet += 1;
if self.quiet > self.hold {
self.hz = None;
}
}
}
self.reading()
}
pub fn listen(&mut self, samples: &[f64], rate: f64) -> Option<Reading> {
self.update(&spectrum(&hann(samples), rate))
}
pub fn reading(&self) -> Option<Reading> {
let hz = self.hz?;
let (note, octave, cents) = nearest_note(hz)?;
Some(Reading {
hz,
note,
octave,
cents,
})
}
pub fn reset(&mut self) {
self.hz = None;
self.quiet = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_tuner_smooths_within_a_note_and_jumps_to_another() {
let rate = 48_000.0;
let read = |hz: f64| -> Vec<(f64, f64)> {
let samples: Vec<f64> = tone(hz, rate).take(8192).collect();
spectrum(&hann(&samples), rate)
};
let mut tuner = Tuner::new().smoothing(0.5);
let first = tuner.update(&read(440.0)).unwrap();
assert_eq!(first.name(), "A4");
assert!(first.in_tune(1.0), "{first:?}");
let sharp = 440.0 * 2f64.powf(10.0 / 1200.0);
let next = tuner.update(&read(sharp)).unwrap();
assert!((next.cents - 5.0).abs() < 1.0, "{next:?}");
let low = tuner.update(&read(82.41)).unwrap();
assert_eq!(low.name(), "E2");
assert!(low.cents.abs() < 5.0, "{low:?}");
tuner.reset();
assert_eq!(tuner.reading(), None);
}
fn naive(samples: &[f64]) -> Vec<Complex> {
let n = samples.len();
(0..n)
.map(|k| {
samples.iter().enumerate().fold((0.0, 0.0), |acc, (j, s)| {
let angle = -2.0 * PI * (j * k % n) as f64 / n as f64;
(acc.0 + s * angle.cos(), acc.1 + s * angle.sin())
})
})
.collect()
}
fn close(a: &[Complex], b: &[Complex]) -> bool {
a.len() == b.len()
&& a.iter()
.zip(b)
.all(|(x, y)| (x.0 - y.0).abs() < 1e-7 && (x.1 - y.1).abs() < 1e-7)
}
fn random(n: usize) -> Vec<f64> {
let mut state = 0x2545_f491_u64;
(0..n)
.map(|_| {
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1);
(state >> 33) as f64 / (1u64 << 31) as f64 - 1.0
})
.collect()
}
#[test]
fn matches_the_definition_at_every_length() {
for n in [1, 2, 3, 4, 5, 7, 8, 12, 16, 100, 128, 441, 1000] {
let samples = random(n);
assert!(close(&fft(&samples), &naive(&samples)), "length {n}");
}
assert!(fft(&[]).is_empty());
}
#[test]
fn a_tone_peaks_at_its_frequency() {
let rate = 44_100.0;
for hz in [440.0, 1000.0, 12_340.0] {
let tone: Vec<f64> = (0..4410)
.map(|i| (2.0 * PI * hz * i as f64 / rate).sin())
.collect();
let bins = spectrum(&hann(&tone), rate);
assert_eq!(bins.len(), 2206);
let (peak, level) = bins
.iter()
.copied()
.max_by(|a, b| a.1.total_cmp(&b.1))
.unwrap();
assert!((peak - hz).abs() <= 10.0, "{hz}: peak at {peak}");
assert!((level - 4410.0 / 4.0).abs() < 4410.0 * 0.05, "{level}");
}
}
#[test]
fn the_window_tapers_to_zero() {
let window = hann(&[1.0; 8]);
assert_eq!(window[0], 0.0);
assert!((window[4] - 1.0).abs() < 1e-12);
assert!((window[2] - 0.5).abs() < 1e-12);
assert!(hann(&[]).is_empty());
}
#[test]
fn a_constant_is_all_in_the_first_bin() {
let bins = spectrum(&[0.5; 64], 64.0);
assert_eq!(bins.len(), 33);
assert!((bins[0].1 - 32.0).abs() < 1e-9);
assert!(bins[1..].iter().all(|(_, m)| *m < 1e-9));
assert!(spectrum(&[], 48_000.0).is_empty());
}
#[test]
fn the_full_spectrum_mirrors_the_half() {
let samples = noise(9).take(16).collect::<Vec<_>>();
let full = spectrum_full(&samples, 16.0);
let half = spectrum(&samples, 16.0);
assert_eq!(full.len(), 16);
assert_eq!(&full[..9], &half[..]);
for k in 1..8 {
assert!((full[k].1 - full[16 - k].1).abs() < 1e-9);
}
assert!(spectrum_full(&[], 1.0).is_empty());
}
#[test]
fn a_tone_keeps_its_phase_across_buffers() {
let mut wave = tone(440.0, 48_000.0).amplitude(0.5);
let first: Vec<f64> = wave.by_ref().take(100).collect();
let second: Vec<f64> = wave.take(100).collect();
let whole: Vec<f64> = tone(440.0, 48_000.0).amplitude(0.5).take(200).collect();
assert_eq!([first, second].concat(), whole);
let shifted: Vec<f64> = tone(1.0, 4.0).phase(PI / 2.0).take(2).collect();
assert!((shifted[0] - 1.0).abs() < 1e-12 && shifted[1].abs() < 1e-12);
}
#[test]
fn partials_above_nyquist_are_left_out() {
let mix = partials(300.0, 1000.0, &[1.0, 1.0, 1.0]);
assert_eq!(mix.parts.len(), 1);
}
#[test]
fn filtered_noise_is_smoother() {
let white: Vec<f64> = noise(3).take(4096).collect();
let low: Vec<f64> = noise(3).low_pass(100.0, 48_000.0).take(4096).collect();
let roughness = |s: &[f64]| s.windows(2).map(|w| (w[1] - w[0]).abs()).sum::<f64>();
assert!(roughness(&low) * 50.0 < roughness(&white));
assert_ne!(noise(1).next(), noise(2).next());
}
#[test]
fn a_mix_ends_when_every_source_has() {
let a = [1.0, 2.0, 3.0].into_iter();
let b = [10.0].into_iter();
let out: Vec<f64> = mix().add(1.0, a).add(0.5, b).collect();
assert_eq!(out, [6.0, 2.0, 3.0]);
assert_eq!(Mix::new().next(), None);
}
#[test]
fn triggers_respect_the_depth_and_the_end() {
let wave = [0.0, 1.0, -1.0, 1.0, 1.0];
assert_eq!(trigger(&wave, 0.5, 1, Edge::Rising), Some(0));
assert_eq!(trigger(&wave, 0.5, 2, Edge::Rising), Some(2));
assert_eq!(trigger(&wave, 0.5, 3, Edge::Rising), None);
assert_eq!(trigger(&wave, 0.0, 1, Edge::Falling), Some(1));
assert_eq!(trigger(&[0.0], 0.5, 1, Edge::Rising), None);
assert_eq!(trigger(&[], 0.5, 0, Edge::Rising), None);
}
#[test]
fn notes_round_trip() {
for name in ["C-1", "A0", "C4", "F#5", "B9", "G#2"] {
let hz = note_hz(name).unwrap();
let (letter, octave, cents) = nearest_note(hz).unwrap();
assert_eq!(format!("{letter}{octave}"), name);
assert_eq!(cents, 0.0, "{name}");
}
assert_eq!(note_hz("Bb3"), note_hz("A#3"));
assert_eq!(note_hz("B#3"), note_hz("C4"));
assert_eq!(note_hz("Eâ™4"), note_hz("D#4"));
for bad in ["", "X4", "A10", "A-2", "Ax4", "A4.5"] {
assert_eq!(note_hz(bad), None, "{bad}");
}
assert_eq!(nearest_note(0.0), None);
assert_eq!(nearest_note(f64::NAN), None);
assert_eq!(nearest_note(1.0), None);
}
#[test]
fn scope_tuis_table_is_its_octave_0_doubled() {
let t = NoteTable::ScopeTui;
assert_eq!(t.hz("C"), Some(f64::from(16.35f32)));
assert_eq!(t.hz("B0"), Some(f64::from(30.87f32)));
assert_eq!(t.hz("Eb3"), t.hz("D#3"));
assert_eq!(t.hz("A4"), Some(440.0));
assert_eq!(t.hz("B#3"), t.hz("C4"));
assert_eq!(t.hz("C-1"), None);
assert_eq!(t.hz("Cb0"), None);
assert_eq!(t.hz("H"), None);
}
#[test]
fn scope_tuis_buffer_sizes_come_out_sample_for_sample() {
for rate in [44_100u32, 48_000, 96_000] {
for octave in 0..=8 {
for name in NAMES {
let note = format!("{name}{octave}");
let hz = NoteTable::ScopeTui.hz(¬e).unwrap();
let ours = (f64::from(rate) / hz).round() as u32;
let theirs = (rate as f32 * (1.0 / hz as f32)).round() as u32;
assert_eq!(ours, theirs, "{note} at {rate}");
}
}
}
let size = |table: NoteTable| (48_000.0 / table.hz("E2").unwrap()).round();
assert_eq!(size(NoteTable::ScopeTui), 583.0);
assert_eq!(size(NoteTable::Equal), 582.0);
}
#[test]
fn peaks_are_refined_between_bins() {
let rate = 10_240.0;
let samples: Vec<f64> = tone(1003.0, rate).take(1024).collect();
let bins = spectrum(&hann(&samples), rate);
let hz = peak_hz(&bins).unwrap();
assert!((hz - 1003.0).abs() < 1.0, "{hz}");
assert_eq!(peak_hz(&[(0.0, 9.0), (10.0, 1.0)]), Some(10.0));
assert_eq!(peak_hz(&[]), None);
assert_eq!(peak_hz(&[(0.0, 9.0)]), None);
}
#[test]
fn narrow_bands_take_the_level_between_bins() {
let spectrum: Vec<(f64, f64)> = (0..10).map(|i| (i as f64 * 100.0, i as f64)).collect();
let b = bands(&spectrum, 1, 140.0, 160.0);
assert!((b[0] - 1.497).abs() < 0.01, "{b:?}");
assert!(bands(&spectrum, 0, 1.0, 2.0).is_empty());
assert_eq!(bands(&spectrum, 2, 0.0, 100.0), [0.0, 0.0]);
assert_eq!(bands(&[], 2, 1.0, 100.0), [0.0, 0.0]);
assert_eq!(bands(&spectrum, 2, 100.0, 900.0)[1], 9.0);
}
#[test]
fn a_falloff_follows_more_bands_and_nan() {
let mut f = Falloff::gravity(0.5);
assert_eq!(f.update(&[1.0]), [1.0]);
assert_eq!(f.update(&[f64::NAN, 2.0]), [0.5, 2.0]);
assert_eq!(f.update(&[0.0]), [0.0]);
f.reset();
assert!(f.levels().is_empty());
let mut instant = Falloff::gravity(0.0);
instant.update(&[1.0]);
assert_eq!(instant.update(&[0.25]), [0.25]);
}
}