Check and Windows preview / check (anvil) (push) Successful in 2m8s
Check and Windows preview / check (crucible) (push) Successful in 1m32s
Check and Windows preview / check (lathe) (push) Successful in 2m13s
Check and Windows preview / check (patina) (push) Successful in 2m5s
Check and Windows preview / check (rivet) (push) Successful in 1m34s
Check and Windows preview / check (temper) (push) Successful in 2m12s
Check and Windows preview / previews (push) Successful in 1m19s
Check and Windows preview / windows (push) Successful in 2m13s
Independent plugin release / publish (push) Successful in 8s
Independent plugin release / plan (push) Successful in 6s
Independent plugin release / build (push) Successful in 2m57s
Lathe places EQ bands on harmonics 1-32 of the played note, so a cut on harmonic 3 stays on harmonic 3 on every note. Temper normalises each note's level before an oversampled shaper and tunes its floor, push and bright filters to the note, so every note distorts the same. The note tracker moves from Anvil into common-dsp, together with a state-variable filter, 4x oversampler and harmonic meter. Anvil's behaviour and editor render are unchanged. docs/ideas.md collects all plugin ideas and their status.
92 lines
3.0 KiB
Rust
92 lines
3.0 KiB
Rust
//! Levels of the harmonics of a known fundamental, measured with Hann-windowed Goertzel
|
||
//! filters. Work is spread evenly over the samples, so there is no per-window spike.
|
||
use std::f32::consts::TAU;
|
||
|
||
pub const HARMONICS: usize = 32;
|
||
|
||
pub struct HarmonicMeter {
|
||
rate: f32,
|
||
window: Vec<f32>,
|
||
count: usize,
|
||
hz: f32,
|
||
coef: [f32; HARMONICS],
|
||
s1: [f32; HARMONICS],
|
||
s2: [f32; HARMONICS],
|
||
levels: [f32; HARMONICS],
|
||
}
|
||
impl HarmonicMeter {
|
||
/// Window of about `ms`; 85 ms resolves harmonics of fundamentals down to 30 Hz.
|
||
pub fn new(rate: f32, ms: f32) -> Self {
|
||
let n = ((rate * ms * 0.001) as usize).max(64);
|
||
Self {
|
||
rate,
|
||
window: (0..n)
|
||
.map(|i| 0.5 - 0.5 * (TAU * i as f32 / n as f32).cos())
|
||
.collect(),
|
||
count: 0,
|
||
hz: 0.,
|
||
coef: [0.; HARMONICS],
|
||
s1: [0.; HARMONICS],
|
||
s2: [0.; HARMONICS],
|
||
levels: [-120.; HARMONICS],
|
||
}
|
||
}
|
||
/// Peak level in dBFS of each harmonic (index 0 is the fundamental) from the last
|
||
/// complete window. Harmonics above 0.45 × the sample rate read −120.
|
||
pub fn levels(&self) -> &[f32; HARMONICS] {
|
||
&self.levels
|
||
}
|
||
/// Fundamental of the last complete window, 0 if none.
|
||
pub fn hz(&self) -> f32 {
|
||
self.hz
|
||
}
|
||
/// Feeds one sample. `hz` is read at the start of each window (0 = no note: skip it).
|
||
/// Returns true when a window completed and `levels` changed.
|
||
#[inline]
|
||
pub fn push(&mut self, x: f32, hz: f32) -> bool {
|
||
if self.count == 0 {
|
||
self.hz = hz;
|
||
for k in 0..HARMONICS {
|
||
let f = hz * (k + 1) as f32;
|
||
self.coef[k] = if hz > 0. && f < 0.45 * self.rate {
|
||
2. * (TAU * f / self.rate).cos()
|
||
} else {
|
||
0.
|
||
};
|
||
}
|
||
self.s1 = [0.; HARMONICS];
|
||
self.s2 = [0.; HARMONICS];
|
||
}
|
||
let w = x * self.window[self.count];
|
||
for k in 0..HARMONICS {
|
||
let s = w + self.coef[k] * self.s1[k] - self.s2[k];
|
||
self.s2[k] = self.s1[k];
|
||
self.s1[k] = s;
|
||
}
|
||
self.count += 1;
|
||
if self.count < self.window.len() {
|
||
return false;
|
||
}
|
||
self.count = 0;
|
||
let n = self.window.len() as f32;
|
||
for k in 0..HARMONICS {
|
||
let f = self.hz * (k + 1) as f32;
|
||
self.levels[k] = if self.hz > 0. && f < 0.45 * self.rate {
|
||
let c = self.coef[k];
|
||
let p =
|
||
self.s1[k] * self.s1[k] + self.s2[k] * self.s2[k] - c * self.s1[k] * self.s2[k];
|
||
// Hann coherent gain is 0.5: a sine of peak A gives |X| = A·N/4.
|
||
let amp = 4. * p.max(0.).sqrt() / n;
|
||
20. * amp.max(1e-6).log10()
|
||
} else {
|
||
-120.
|
||
};
|
||
}
|
||
true
|
||
}
|
||
pub fn reset(&mut self) {
|
||
self.count = 0;
|
||
self.levels = [-120.; HARMONICS];
|
||
}
|
||
}
|