//! Note-tracked distortion: the filters around the shaper follow the played note and the //! input level is normalised per note, so every note distorts with the same character. //! //! The audio path is delayed by the tracker's lookahead, so filters and drive are set for a //! note before its attack reaches the shaper. The shaper runs at 4× the sample rate. mod settings; use common_dsp::meter::{HarmonicMeter, HARMONICS}; use common_dsp::oversample::Oversampler; use common_dsp::svf::{Svf, SvfCoef}; use common_dsp::tracker::{Lookahead, NoteEvent, NoteState, PitchFollower, Tracked, TrackerConfig}; use common_dsp::{coefficient, db_to_gain, gain_to_db, hz_to_midi}; pub use settings::{Curve, Settings, Source}; pub const MIN_HZ: f32 = TrackerConfig::BASS.min_hz; pub const MAX_HZ: f32 = TrackerConfig::BASS.max_hz; /// Harmonics shown by the editor. pub const SHOWN: usize = 16; /// RMS level that Normalize brings every note to before Drive. const TARGET_DB: f32 = -12.; const RETUNE: usize = 16; const DRY: usize = 64; #[derive(Clone, Copy, Debug)] pub struct AnalyzerFrame { pub hz: f32, pub note: Option, /// Harmonic levels in dBFS of the delayed input and of the output (index 0 = fundamental). pub input: [f32; SHOWN], pub output: [f32; SHOWN], /// Peak level reaching the shaper, after normalisation and drive, in dB. pub shaper_db: f32, /// Gain applied by Normalize and by Auto gain, in dB. pub normalize_db: f32, pub auto_db: f32, pub latency: u32, pub sample_rate: f32, } impl Default for AnalyzerFrame { fn default() -> Self { Self { hz: 0., note: None, input: [-120.; SHOWN], output: [-120.; SHOWN], shaper_db: -120., normalize_db: 0., auto_db: 0., latency: 0, sample_rate: 48000., } } } /// Per-note input level, measured on the undelayed input so it is known at the attack. #[derive(Clone, Copy, Debug, Default)] struct Level { /// RMS of the undelayed input. ahead: f32, /// Loudest `ahead` since the note started, slowly released. note: f32, } impl Level { fn event(&mut self, e: NoteEvent) { if let NoteEvent::Start { glide: false, .. } = e { // `ahead` already looks one lookahead into the new note. self.note = self.ahead; } } } #[derive(Clone, Copy, Debug)] struct Filters { floor: SvfCoef, push: SvfCoef, bright: SvfCoef, } struct Channel { floor: Svf, push: Svf, bright: Svf, over: Oversampler, dc_x: f32, dc_y: f32, dry: [f32; DRY], } impl Channel { fn new() -> Self { Self { floor: Svf::default(), push: Svf::default(), bright: Svf::default(), over: Oversampler::new(), dc_x: 0., dc_y: 0., dry: [0.; DRY], } } fn reset(&mut self) { self.floor.reset(); self.push.reset(); self.bright.reset(); self.over.reset(); self.dc_x = 0.; self.dc_y = 0.; self.dry = [0.; DRY]; } } struct Smoothed { value: f32, coef: f32, } impl Smoothed { fn new(value: f32, coef: f32) -> Self { Self { value, coef } } #[inline] fn next(&mut self, target: f32) -> f32 { let d = target - self.value; self.value = if d.abs() < 1e-6 { target } else { self.value + d * self.coef }; self.value } } /// The tracked distortion without its tracker: driven by note events aligned with its input. /// Its latency is the oversampling filters'. pub struct Module { rate: f32, channels: usize, settings: Settings, dry_at: usize, sounding: bool, pitch: PitchFollower, hz_coef: f32, countdown: usize, filters: Filters, channel: [Channel; 2], level: Level, rms_coef: f32, release: f32, norm_db: Smoothed, drive: Smoothed, bias: Smoothed, mix: Smoothed, output: Smoothed, bypass: Smoothed, auto_db: Smoothed, power_in: f32, power_out: f32, power_coef: f32, dc_r: f32, shaper_peak: f32, meter_in: HarmonicMeter, meter_out: HarmonicMeter, /// Smoothed controls start at the next settings instead of gliding from defaults. fresh: bool, } impl Module { 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 smooth = coefficient(rate, 10.); let settings = Settings::default(); let mut e = Self { rate, channels, settings, dry_at: 0, sounding: false, pitch: PitchFollower::default(), hz_coef: coefficient(rate, 5.), countdown: 0, filters: Filters { floor: SvfCoef::IDENTITY, push: SvfCoef::IDENTITY, bright: SvfCoef::IDENTITY, }, channel: [Channel::new(), Channel::new()], level: Level::default(), rms_coef: coefficient(rate, 10.), release: 1. - coefficient(rate, 1500.), norm_db: Smoothed::new(0., coefficient(rate, 5.)), drive: Smoothed::new(db_to_gain(settings.drive), smooth), bias: Smoothed::new(0., smooth), mix: Smoothed::new(1., smooth), output: Smoothed::new(1., smooth), bypass: Smoothed::new(0., smooth), auto_db: Smoothed::new(0., coefficient(rate, 50.)), power_in: 0., power_out: 0., power_coef: coefficient(rate, 300.), dc_r: 1. - std::f32::consts::TAU * 10. / rate, shaper_peak: 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 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()?; if std::mem::take(&mut self.fresh) { self.drive.value = db_to_gain(s.drive); self.bias.value = s.bias * 0.008; self.mix.value = s.mix * 0.01; self.output.value = db_to_gain(s.output); self.bypass.value = f32::from(s.bypass); } self.settings = s; Ok(()) } pub fn reset(&mut self) { self.sounding = false; for c in &mut self.channel { c.reset(); } self.level = Level::default(); self.power_in = 0.; self.power_out = 0.; self.shaper_peak = 0.; self.meter_in.reset(); self.meter_out.reset(); self.countdown = 0; self.fresh = true; } fn retune(&mut self, hz: f32) { let s = &self.settings; let limit = 0.45 * self.rate; let tuned = |mult: f32| hz > 0. && mult > 0. && hz * mult < limit; self.filters.floor = if tuned(s.floor) { SvfCoef::highpass(self.rate, hz * s.floor, std::f32::consts::FRAC_1_SQRT_2) } else { SvfCoef::IDENTITY }; let k = s.push_harmonic as f32; self.filters.push = if tuned(k) && s.push_gain != 0. { SvfCoef::bell(self.rate, hz * k, k / 0.6, s.push_gain) } else { SvfCoef::IDENTITY }; self.filters.bright = if tuned(s.bright) { SvfCoef::lowpass(self.rate, hz * s.bright, std::f32::consts::FRAC_1_SQRT_2) } else { SvfCoef::IDENTITY }; } fn run(&mut self, y: [f32; 2]) -> [f32; 2] { let (hz, jump) = self.pitch.step(self.hz_coef); if jump || self.countdown == 0 { self.retune(hz); self.countdown = RETUNE; } self.countdown -= 1; let s = self.settings; let rms_db = 10. * self.level.note.max(1e-12).log10(); // Below −60 dBFS there is nothing to normalise; hold the last gain. let norm_target = if rms_db > -60. { (s.normalize * 0.01 * (TARGET_DB - rms_db)).clamp(-24., 36.) } else { self.norm_db.value }; let norm = db_to_gain(self.norm_db.next(norm_target)); let drive = self.drive.next(db_to_gain(s.drive)); let bias = self.bias.next(s.bias * 0.008); let mix = self.mix.next(s.mix * 0.01); let output = self.output.next(db_to_gain(s.output)); let bypass = self.bypass.next(f32::from(s.bypass)); let curve = s.curve; let offset = curve.shape(bias); let f = self.filters; let dry_at = self.dry_at; let back = (dry_at + DRY - Oversampler::latency()) % DRY; let mut wet = [0.; 2]; let mut dry = [0.; 2]; let mut peak = self.shaper_peak; for c in 0..self.channels { let ch = &mut self.channel[c]; ch.dry[dry_at] = y[c]; dry[c] = ch.dry[back]; let v = ch.floor.process(&f.floor, y[c]); let v = ch.push.process(&f.push, v) * norm * drive; let up = ch.over.up(v); let shaped = up.map(|u| { peak = peak.max(u.abs()); curve.shape(u + bias) - offset }); let v = ch.over.down(shaped); let v = ch.bright.process(&f.bright, v); // DC blocker: bias and asymmetric curves leave an offset. let d = v - ch.dc_x + self.dc_r * ch.dc_y; ch.dc_x = v; ch.dc_y = d; wet[c] = d; } self.shaper_peak = peak; self.dry_at = (dry_at + 1) % DRY; let (dry_mono, wet_mono) = if self.channels == 2 { (0.5 * (dry[0] + dry[1]), 0.5 * (wet[0] + wet[1])) } else { (dry[0], wet[0]) }; self.power_in += (dry_mono * dry_mono - self.power_in) * self.power_coef; self.power_out += (wet_mono * wet_mono - self.power_out) * self.power_coef; let auto_target = if !s.auto_gain { 0. } else if self.power_in > 1e-7 && self.power_out > 1e-9 { (10. * (self.power_in / self.power_out).log10()).clamp(-36., 24.) } else { self.auto_db.value }; let auto = db_to_gain(self.auto_db.next(auto_target)); let mut out = [0.; 2]; for c in 0..self.channels { let v = (mix * wet[c] * auto + (1. - mix) * dry[c]) * output; out[c] = v * (1. - bypass) + dry[c] * bypass; } let tone = if self.sounding { hz } else { 0. }; let o = if self.channels == 2 { 0.5 * (out[0] + out[1]) } else { out[0] }; self.meter_in.push(dry_mono, tone); self.meter_out.push(o, tone); out } /// Editor data. `latency` is left at 0 for the owner to fill in. pub fn take_analyzer(&mut self) -> AnalyzerFrame { let hz = self.pitch.hz(); let mut f = AnalyzerFrame { hz, sample_rate: self.rate, shaper_db: gain_to_db(self.shaper_peak), normalize_db: self.norm_db.value, auto_db: self.auto_db.value, ..AnalyzerFrame::default() }; self.shaper_peak = 0.; f.input .copy_from_slice(&self.meter_in.levels()[..SHOWN.min(HARMONICS)]); f.output .copy_from_slice(&self.meter_out.levels()[..SHOWN.min(HARMONICS)]); if self.sounding && hz > 0. { f.note = Some(hz_to_midi(hz).round().clamp(0., 127.) as u8); } f } } impl Tracked for Module { fn latency(&self) -> usize { Oversampler::latency() } fn ahead(&mut self, mono: f32) { // Level of the input one lookahead ahead of the audio that is about to be shaped. let a = &mut self.level; a.ahead += (mono * mono - a.ahead) * self.rms_coef; a.note = (a.note * self.release).max(a.ahead); } fn event(&mut self, e: NoteEvent) { self.pitch.event(false, e); self.level.event(e); } fn process(&mut self, y: [f32; 2], note: NoteState) -> [f32; 2] { // Unsure tracking sends no retune, so Floor, Push and Bright hold their last sure // tuning; only the readout drops the note. self.sounding = note.sure; self.run(y) } } /// Standalone Temper: its own tracker in front of the [`Module`]. pub struct Engine { channels: usize, lookahead: Lookahead, module: Module, } impl Engine { pub fn new(rate: f32, channels: usize) -> Result { let module = Module::new(rate, channels)?; let mut e = Self { channels, lookahead: Lookahead::new(rate, TrackerConfig::BASS), module, }; // The module already holds the defaults; only the tracker still needs them. e.set_tracker(&Settings::default()); Ok(e) } /// Lookahead plus the oversampling filters. pub fn latency_samples(&self) -> u32 { (self.lookahead.latency() + self.module.latency()) as u32 } pub fn channels(&self) -> usize { self.channels } pub fn settings(&self) -> Settings { self.module.settings() } pub fn set_settings(&mut self, s: Settings) -> Result<(), &'static str> { self.module.set_settings(s)?; self.set_tracker(&s); Ok(()) } fn set_tracker(&mut self, s: &Settings) { self.lookahead .tracker .set(s.pitch.source(s.source.mode(), s.gate, true)); } pub fn reset(&mut self) { self.lookahead.reset(); self.module.reset(); } pub fn note_on(&mut self, note: u8) { self.lookahead.tracker.note_on(note); } pub fn note_off(&mut self, note: u8) { self.lookahead.tracker.note_off(note); } 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] }; self.lookahead.process(x, mono, &mut self.module) } pub fn take_analyzer(&mut self) -> AnalyzerFrame { AnalyzerFrame { latency: self.latency_samples(), ..self.module.take_analyzer() } } }