//! Harmonic EQ: bands sit on the harmonics of the played note instead of fixed frequencies, //! so a cut on harmonic 3 stays on harmonic 3 on every note. //! //! The audio path is delayed by the tracker's lookahead, so the filters are already tuned to //! a note when its attack leaves the plugin. mod settings; use common_dsp::meter::HarmonicMeter; use common_dsp::svf::{Svf, SvfCoef}; use common_dsp::tracker::{PitchFollower, Tracker, TrackerConfig}; use common_dsp::{coefficient, db_to_gain, hz_to_midi}; pub use settings::{Settings, Source, GAIN_MAX, GAIN_MIN, HARMONICS}; pub const MIN_HZ: f32 = TrackerConfig::BASS.min_hz; pub const MAX_HZ: f32 = TrackerConfig::BASS.max_hz; /// Harmonic bands plus the bands halfway between them. const BANDS: usize = 2 * HARMONICS; /// Filter coefficients are recomputed this often, in samples. const RETUNE: usize = 16; const GAIN_MS: f32 = 20.; #[derive(Clone, Copy, Debug)] pub struct AnalyzerFrame { /// Fundamental the filters are tuned to, 0 before the first note. pub hz: f32, /// Sounding note, rounded; None between notes. pub note: Option, /// Harmonic levels in dBFS of the delayed input and of the output (index 0 = fundamental). pub input: [f32; HARMONICS], pub output: [f32; HARMONICS], pub latency: u32, pub sample_rate: f32, } impl Default for AnalyzerFrame { fn default() -> Self { Self { hz: 0., note: None, input: [-120.; HARMONICS], output: [-120.; HARMONICS], latency: 0, sample_rate: 48000., } } } pub struct Engine { rate: f32, channels: usize, settings: Settings, latency: usize, delay: Vec<[f32; 2]>, delay_at: usize, tracker: Tracker, pitch: PitchFollower, hz: f32, hz_coef: f32, countdown: usize, coefs: [SvfCoef; BANDS], active: [bool; BANDS], gains: [f32; BANDS], state: [[Svf; BANDS]; 2], gain_coef: f32, smooth: f32, mix: f32, output: f32, bypass: f32, meter_in: HarmonicMeter, meter_out: HarmonicMeter, /// Smoothed controls start at the next settings instead of gliding from defaults. fresh: bool, } impl Engine { pub fn new(rate: f32, channels: usize) -> Result { if !(8000.0..=768000.).contains(&rate) { return Err("unsupported sample rate"); } if !(1..=2).contains(&channels) { return Err("unsupported channel count"); } let tracker = Tracker::new(rate, TrackerConfig::BASS); let latency = tracker.latency(); let settings = Settings::default(); let mut e = Self { rate, channels, settings, latency, delay: vec![[0.; 2]; latency + 1], delay_at: 0, tracker, pitch: PitchFollower::default(), hz: 0., hz_coef: 1., countdown: 0, coefs: [SvfCoef::IDENTITY; BANDS], active: [false; BANDS], gains: [0.; BANDS], state: [[Svf::default(); BANDS]; 2], gain_coef: coefficient(rate / RETUNE as f32, GAIN_MS), smooth: coefficient(rate, 10.), mix: 1., output: 1., bypass: 0., meter_in: HarmonicMeter::new(rate, 85.), meter_out: HarmonicMeter::new(rate, 85.), fresh: true, }; e.set_settings(settings)?; e.fresh = true; Ok(e) } pub fn latency_samples(&self) -> u32 { self.latency as u32 } pub fn channels(&self) -> usize { self.channels } pub fn settings(&self) -> Settings { self.settings } pub fn set_settings(&mut self, s: Settings) -> Result<(), &'static str> { s.validate()?; self.tracker .set(s.source == Source::Midi, s.gate, s.smooth > 0.); self.hz_coef = coefficient(self.rate, s.smooth / 3.); self.settings = s; if std::mem::take(&mut self.fresh) { self.mix = s.mix * 0.01; self.output = db_to_gain(s.output); self.bypass = f32::from(s.bypass); self.gains = self.targets(); } Ok(()) } pub fn reset(&mut self) { self.delay.fill([0.; 2]); self.tracker.reset(); for ch in &mut self.state { for f in ch.iter_mut() { f.reset(); } } self.meter_in.reset(); self.meter_out.reset(); self.countdown = 0; self.fresh = true; } pub fn note_on(&mut self, note: u8) { self.tracker.note_on(note); } pub fn note_off(&mut self, note: u8) { self.tracker.note_off(note); } /// Gain targets in dB of every band for the current settings. fn targets(&self) -> [f32; BANDS] { let s = &self.settings; let mut t = [0.; BANDS]; for k in 1..=s.harmonics as usize { t[k - 1] = s.effective(k); t[HARMONICS + k - 1] = s.between; } t } fn retune(&mut self, jump: bool) { let s = self.settings; let targets = self.targets(); let width = s.width * 0.01; for b in 0..BANDS { self.gains[b] = if jump { targets[b] } else { self.gains[b] + (targets[b] - self.gains[b]) * self.gain_coef }; if (self.gains[b] - targets[b]).abs() < 1e-4 { self.gains[b] = targets[b]; } let order = if b < HARMONICS { (b + 1) as f32 } else { (b - HARMONICS + 1) as f32 + 0.5 }; let centre = order * self.hz; let audible = self.hz > 0. && centre < 0.45 * self.rate; if audible && !self.active[b] { for ch in &mut self.state { ch[b].reset(); } } self.active[b] = audible && self.gains[b] != 0.; if self.active[b] { self.coefs[b] = SvfCoef::bell(self.rate, centre, order / width, self.gains[b]); } } } pub fn process_sample(&mut self, x: [f32; 2]) -> [f32; 2] { let mono = if self.channels == 2 { 0.5 * (x[0] + x[1]) } else { x[0] }; let (pitch, snap) = (&mut self.pitch, self.settings.snap); self.tracker.process(mono, |e| pitch.event(snap, e)); let len = self.delay.len(); self.delay[self.delay_at] = x; self.delay_at = (self.delay_at + 1) % len; let y = self.delay[self.delay_at]; let first = self.hz == 0.; let (hz, jump) = self.pitch.step(self.hz_coef); let first = first && jump; self.hz = hz; if jump || self.countdown == 0 { self.retune(first); self.countdown = RETUNE; } self.countdown -= 1; let s = self.settings; self.mix += (s.mix * 0.01 - self.mix) * self.smooth; self.output += (db_to_gain(s.output) - self.output) * self.smooth; self.bypass += (f32::from(s.bypass) - self.bypass) * self.smooth; let mut out = [0.; 2]; for c in 0..self.channels { let mut v = y[c]; for b in 0..BANDS { if self.active[b] { v = self.state[c][b].process(&self.coefs[b], v); } } let wet = (self.mix * v + (1. - self.mix) * y[c]) * self.output; out[c] = wet * (1. - self.bypass) + y[c] * self.bypass; } let hz = if self.tracker.sounding() { self.hz } else { 0. }; let (dry, wet) = if self.channels == 2 { (0.5 * (y[0] + y[1]), 0.5 * (out[0] + out[1])) } else { (y[0], out[0]) }; self.meter_in.push(dry, hz); self.meter_out.push(wet, hz); out } pub fn take_analyzer(&self) -> AnalyzerFrame { let mut f = AnalyzerFrame { hz: self.hz, latency: self.latency as u32, sample_rate: self.rate, input: *self.meter_in.levels(), output: *self.meter_out.levels(), ..AnalyzerFrame::default() }; if self.tracker.sounding() && self.hz > 0. { f.note = Some(hz_to_midi(self.hz).round().clamp(0., 127.) as u8); } f } }