Producers found the tracked plugins jumpy: the shared tracker switched notes after 10 ms and retuned every 5 ms, so growls and formant basses flickered and every tracked plugin acted on wrong notes. Shared tracker (common-dsp): - A new note needs a steady read for 40 ms; an octave down 80 ms, an octave up 200 ms (an octave-up read still fits the note's period and counts as agreeing). Inside a note the tuning only moves past a 0.25 semitone deadband. Follow bends keeps the old fast behaviour as an option. - A trust score turns the note unsure while reads disagree; Tracker::active and NoteState report it, and Fallback latches "step back" across note ends. - Notes end when the window start stays below the gate's release level for 20 ms, so short gaps between notes are seen without waiting for the gate. - PitchSource replaces the old (midi, gate, glide) arguments: Audio, MIDI or Note mode (a set note, no detection), plus a key lock (minor, major, root only) that snaps reads to the key. - stems.rs: synthetic stems (growls, wobble, reese, formant, 808 slide, layered drop, noise riser) and a score; examples/tracker_report scores the corpus or real WAV stems. On the corpus the old behaviour makes up to 28 extra note starts per second; the stable default makes none. Plugins: - Anvil, Lathe, Temper, Knurl, Planish, Braze and Forge gain the NOTE pitch source, the key lock and Follow bends (shared common-params crate and a shared header switch in common-ui). - Unsure tracking: Anvil brings the original sub back in place of the sine, Lathe passes the audio through, Knurl fades its texture out; Temper, Planish and Braze hold their last sure tuning. Readouts show the note only when it is sure. - Anvil gains Mode: Replace or Enhance (keeps the original sub and adds the sine under it). - Planish slides its band edges over 6 ms on a new note instead of snapping. - Ballast defaults to Grid parts, which need no tracking.
808 lines
28 KiB
Rust
808 lines
28 KiB
Rust
//! Sub anchor: replaces the band below the split with a clean mono sine that follows the
|
||
//! bass pitch, holds one level for every note and restarts at the same phase on each note.
|
||
//!
|
||
//! The audio path is delayed by a fixed lookahead. Detection runs on the undelayed input, so
|
||
//! a note's pitch is known before its delayed attack leaves the plugin, and the sine starts
|
||
//! exactly on that attack.
|
||
//!
|
||
//! Ghost harmonics add the sub's own harmonics above a chosen frequency, so the note
|
||
//! reads on speakers without sub. The band above the split is measured per harmonic and
|
||
//! only the harmonics the patch lacks are filled: a harmonic the patch already has would
|
||
//! meet the added one at an unrelated phase and could cancel.
|
||
//!
|
||
//! The sine only plays while the note is sure. While tracking is unsure, the original sub
|
||
//! comes back in its place (Replace) or the added sine fades out (Enhance), so a wrong guess
|
||
//! never plays a wrong sub note.
|
||
//!
|
||
//! Kick lock reads the kick from the sidechain. Hits are found on the undelayed sidechain,
|
||
//! so with the lookahead each hit ducks the sub exactly when the delayed kick plays, and
|
||
//! restarts the sub's phase at the kick plus an offset, also before the kick. The audition
|
||
//! plays each offset of a sweep for a few kicks and measures how kick and sub sum in the
|
||
//! low band for each one; what sums loudest is not always what sounds like one hit.
|
||
mod settings;
|
||
use common_dsp::meter::HarmonicMeter;
|
||
use common_dsp::svf::{Svf, SvfCoef};
|
||
use common_dsp::tracker::{Fallback, NoteEvent, Tracker, TrackerConfig};
|
||
use common_dsp::{coefficient, db_to_gain, hz_to_midi, midi_to_hz, Biquad, Crossover};
|
||
pub use settings::{Mode, Settings, Source, KICK_OFFSET_MAX};
|
||
use std::f32::consts::TAU;
|
||
|
||
/// Detectable fundamental range. The lowest sets the lookahead (two periods).
|
||
pub const MIN_HZ: f32 = TrackerConfig::SUB.min_hz;
|
||
pub const MAX_HZ: f32 = TrackerConfig::SUB.max_hz;
|
||
pub const SCOPE: usize = 256;
|
||
const ATTACK_MS: f32 = 2.;
|
||
const RELEASE_MS: f32 = 15.;
|
||
const CROSSFADE_MS: f32 = 3.;
|
||
/// Crossfade between the sine and the original sub when tracking turns unsure or sure.
|
||
const TRUST_MS: f32 = 15.;
|
||
/// Highest Ghost harmonic. Harmonic 10 of a 30 Hz sub is 300 Hz.
|
||
pub const GHOST_TOP: usize = 10;
|
||
/// Ghost harmonics shown and generated: 2..=GHOST_TOP.
|
||
pub const GHOST_COUNT: usize = GHOST_TOP - 1;
|
||
/// Measurement window per note. Resolves harmonics of fundamentals down to about 35 Hz.
|
||
const GHOST_WINDOW_MS: f32 = 60.;
|
||
/// Harmonics fade in over this many octaves above the Ghost edge, so a gliding note
|
||
/// does not switch them on with a step.
|
||
const GHOST_RAMP_OCT: f32 = 0.5;
|
||
/// Offsets the audition steps through, in ms.
|
||
pub const AUDITION: [f32; 13] = [
|
||
-30., -25., -20., -15., -10., -5., 0., 5., 10., 15., 20., 25., 30.,
|
||
];
|
||
/// Kicks each audition offset plays for.
|
||
pub const AUDITION_HITS: u32 = 4;
|
||
/// A kick is a rise of the fast sidechain envelope this far above the slow one...
|
||
const KICK_RISE: f32 = 2.8;
|
||
/// ...above this level (−45 dBFS)...
|
||
const KICK_FLOOR: f32 = 0.0056;
|
||
/// ...at least this long after the previous kick.
|
||
const KICK_HOLD_MS: f32 = 60.;
|
||
/// Kick and sub are compared below this frequency, over this long after each hit.
|
||
const KICK_LOW_HZ: f32 = 150.;
|
||
const SUM_MS: f32 = 60.;
|
||
const DUCK_ATTACK_MS: f32 = 1.;
|
||
const PENDING: usize = 16;
|
||
/// Small speaker listen: a phone-like band, summed to mono.
|
||
pub const PHONE_LOW: f32 = 400.;
|
||
pub const PHONE_HIGH: f32 = 7000.;
|
||
|
||
#[derive(Clone, Copy, Debug, Default)]
|
||
pub struct Monitor {
|
||
/// Hear only the generated sub and Ghost harmonics.
|
||
pub solo: bool,
|
||
/// Hear the output through a phone-like speaker band, in mono.
|
||
pub phone: bool,
|
||
/// Step the kick offset through `AUDITION`, `AUDITION_HITS` kicks each.
|
||
pub audition: bool,
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug)]
|
||
pub struct AnalyzerFrame {
|
||
/// Current sub frequency in Hz, 0 when silent.
|
||
pub hz: f32,
|
||
/// MIDI note of the current sub, rounded.
|
||
pub note: Option<u8>,
|
||
/// Tracking is unsure: the original sub plays instead of the sine.
|
||
pub neutral: bool,
|
||
pub low_db: f32,
|
||
pub sub_db: f32,
|
||
/// Harmonics 2..=GHOST_TOP of the current note, peak dB relative to the sub: what the
|
||
/// patch carries above the split, and what Ghost adds. −120 when absent.
|
||
pub patch_harmonics: [f32; GHOST_COUNT],
|
||
pub ghost_harmonics: [f32; GHOST_COUNT],
|
||
/// Lowest frequency Ghost fills, after the split.
|
||
pub ghost_edge: f32,
|
||
/// Kicks found in the sidechain since the plugin started.
|
||
pub kick_hits: u32,
|
||
/// Kick plus sub against kick and sub apart, low band, dB, after the last hit. +3 is a
|
||
/// full sum of equal levels, 0 unrelated, below 0 they cancel. NaN before a hit.
|
||
pub kick_sum_db: f32,
|
||
/// Offset the sub restarted with on the latest kick, in ms.
|
||
pub kick_offset: f32,
|
||
/// Measured sum per audition offset, dB; NaN where not measured yet.
|
||
pub audition_sums: [f32; AUDITION.len()],
|
||
/// Original low band and new sub, triggered on a rising zero crossing of the sub.
|
||
pub scope_low: [f32; SCOPE],
|
||
pub scope_sub: [f32; SCOPE],
|
||
/// Duration of the scope window in milliseconds.
|
||
pub scope_ms: f32,
|
||
pub latency: u32,
|
||
pub sample_rate: f32,
|
||
}
|
||
impl Default for AnalyzerFrame {
|
||
fn default() -> Self {
|
||
Self {
|
||
hz: 0.,
|
||
note: None,
|
||
neutral: false,
|
||
low_db: -120.,
|
||
sub_db: -120.,
|
||
patch_harmonics: [-120.; GHOST_COUNT],
|
||
ghost_harmonics: [-120.; GHOST_COUNT],
|
||
ghost_edge: 300.,
|
||
kick_hits: 0,
|
||
kick_sum_db: f32::NAN,
|
||
kick_offset: 0.,
|
||
audition_sums: [f32::NAN; AUDITION.len()],
|
||
scope_low: [0.; SCOPE],
|
||
scope_sub: [0.; SCOPE],
|
||
scope_ms: 60.,
|
||
latency: 0,
|
||
sample_rate: 48000.,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, Default)]
|
||
struct Voice {
|
||
phase: f32,
|
||
hz: f32,
|
||
target: f32,
|
||
amp: f32,
|
||
releasing: bool,
|
||
}
|
||
|
||
/// Sine voices and per-note state, driven by tracker events at output time.
|
||
#[derive(Clone, Copy, Debug, Default)]
|
||
struct Synth {
|
||
voice: Voice,
|
||
old: Voice,
|
||
old_gain: f32,
|
||
note_peak: f32,
|
||
/// Set when a note starts or changes pitch; the Ghost measurement starts over.
|
||
fresh: bool,
|
||
}
|
||
impl Synth {
|
||
fn event(&mut self, s: &Settings, e: NoteEvent) {
|
||
match e {
|
||
NoteEvent::Start { pitch, glide } => self.start(s, pitch, glide),
|
||
NoteEvent::Move { pitch } => {
|
||
if !s.snap {
|
||
self.voice.target = midi_to_hz(pitch + 12. * s.octave as f32);
|
||
}
|
||
}
|
||
NoteEvent::Stop => self.voice.releasing = true,
|
||
}
|
||
}
|
||
fn start(&mut self, s: &Settings, pitch: f32, glide: bool) {
|
||
let note = if s.snap { pitch.round() } else { pitch };
|
||
let hz = midi_to_hz(note + 12. * s.octave as f32);
|
||
let audible = self.voice.amp > 1e-4 && !self.voice.releasing;
|
||
if glide && audible {
|
||
self.voice.target = hz;
|
||
return;
|
||
}
|
||
if s.retrigger || !audible {
|
||
if self.voice.amp > 1e-4 {
|
||
// Restarting the phase mid-cycle would click; the old cycle fades out instead.
|
||
self.old = self.voice;
|
||
self.old_gain = 1.;
|
||
}
|
||
self.voice = Voice {
|
||
phase: s.phase / 360.,
|
||
hz,
|
||
target: hz,
|
||
amp: 0.,
|
||
releasing: false,
|
||
};
|
||
} else {
|
||
self.voice.target = hz;
|
||
self.voice.hz = hz;
|
||
}
|
||
self.note_peak = 0.;
|
||
self.fresh = true;
|
||
}
|
||
/// Restarts a sounding sub at the start phase, keeping its pitch.
|
||
fn kick(&mut self, s: &Settings) {
|
||
let v = self.voice;
|
||
if v.amp <= 1e-4 || v.releasing {
|
||
return;
|
||
}
|
||
self.old = v;
|
||
self.old_gain = 1.;
|
||
self.voice = Voice {
|
||
phase: s.phase / 360.,
|
||
amp: 0.,
|
||
..v
|
||
};
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, Default)]
|
||
struct Pending {
|
||
/// Output-time sample.
|
||
at: u64,
|
||
/// Restart the sub (true) or duck it and start measuring (false).
|
||
restart: bool,
|
||
offset: f32,
|
||
/// Audition offset index this hit belongs to.
|
||
slot: Option<usize>,
|
||
}
|
||
|
||
/// Kick detection, scheduling, ducking and the sum measurement.
|
||
struct Kick {
|
||
clock: u64,
|
||
fast: f32,
|
||
slow: f32,
|
||
fast_coef: f32,
|
||
slow_coef: f32,
|
||
hold: usize,
|
||
pending: [Option<Pending>; PENDING],
|
||
delay: Vec<f32>,
|
||
delay_at: usize,
|
||
low: Biquad,
|
||
duck: f32,
|
||
rising: bool,
|
||
attack: f32,
|
||
measure: usize,
|
||
measure_slot: Option<usize>,
|
||
energy: [f32; 3],
|
||
hits: u32,
|
||
sum_db: f32,
|
||
offset: f32,
|
||
auditioning: bool,
|
||
slot: usize,
|
||
slot_hits: u32,
|
||
sums: [f32; AUDITION.len()],
|
||
counts: [u32; AUDITION.len()],
|
||
}
|
||
impl Kick {
|
||
fn new(rate: f32, latency: usize) -> Self {
|
||
Self {
|
||
clock: 0,
|
||
fast: 0.,
|
||
slow: 0.,
|
||
fast_coef: coefficient(rate, 0.5),
|
||
slow_coef: coefficient(rate, 30.),
|
||
hold: 0,
|
||
pending: [None; PENDING],
|
||
delay: vec![0.; latency + 1],
|
||
delay_at: 0,
|
||
low: Biquad::lowpass(rate, KICK_LOW_HZ, std::f32::consts::FRAC_1_SQRT_2),
|
||
duck: 0.,
|
||
rising: false,
|
||
attack: coefficient(rate, DUCK_ATTACK_MS / 3.),
|
||
measure: 0,
|
||
measure_slot: None,
|
||
energy: [0.; 3],
|
||
hits: 0,
|
||
sum_db: f32::NAN,
|
||
offset: 0.,
|
||
auditioning: false,
|
||
slot: 0,
|
||
slot_hits: 0,
|
||
sums: [f32::NAN; AUDITION.len()],
|
||
counts: [0; AUDITION.len()],
|
||
}
|
||
}
|
||
fn reset(&mut self) {
|
||
self.fast = 0.;
|
||
self.slow = 0.;
|
||
self.hold = 0;
|
||
self.pending = [None; PENDING];
|
||
self.delay.fill(0.);
|
||
self.low.reset();
|
||
self.duck = 0.;
|
||
self.rising = false;
|
||
self.measure = 0;
|
||
self.measure_slot = None;
|
||
self.sum_db = f32::NAN;
|
||
self.slot = 0;
|
||
self.slot_hits = 0;
|
||
self.sums = [f32::NAN; AUDITION.len()];
|
||
self.counts = [0; AUDITION.len()];
|
||
}
|
||
fn schedule(&mut self, p: Pending) {
|
||
if let Some(free) = self.pending.iter_mut().find(|p| p.is_none()) {
|
||
*free = Some(p);
|
||
}
|
||
}
|
||
}
|
||
|
||
pub struct Engine {
|
||
rate: f32,
|
||
channels: usize,
|
||
settings: Settings,
|
||
latency: usize,
|
||
delay: Vec<[f32; 2]>,
|
||
delay_at: usize,
|
||
crossover: [Crossover; 2],
|
||
tracker: Tracker,
|
||
synth: Synth,
|
||
glide: f32,
|
||
low_env: f32,
|
||
env_coef: f32,
|
||
smooth: f32,
|
||
level: f32,
|
||
keep: f32,
|
||
output: f32,
|
||
bypass: f32,
|
||
fallback: Fallback,
|
||
neutral: bool,
|
||
/// 1 while the sine plays, 0 while the original sub stands in for it.
|
||
trust: f32,
|
||
trust_coef: f32,
|
||
meter_coef: f32,
|
||
low_power: f32,
|
||
sub_power: f32,
|
||
scope_low: [f32; 2 * SCOPE],
|
||
scope_sub: [f32; 2 * SCOPE],
|
||
scope_at: usize,
|
||
scope_stride: usize,
|
||
scope_count: usize,
|
||
meter: HarmonicMeter,
|
||
/// Linear peak of the patch's harmonic k (index k − 2) above the split, this note.
|
||
patch: [f32; GHOST_COUNT],
|
||
/// Smoothed Ghost gain per harmonic, relative to the sub's peak.
|
||
fill: [f32; GHOST_COUNT],
|
||
phone_coefs: [SvfCoef; 3],
|
||
phone: [Svf; 3],
|
||
kick: Kick,
|
||
}
|
||
|
||
impl Engine {
|
||
pub fn new(rate: f32, channels: usize) -> Result<Self, &'static str> {
|
||
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::SUB);
|
||
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,
|
||
crossover: [Crossover::new(rate, settings.split); 2],
|
||
tracker,
|
||
synth: Synth::default(),
|
||
glide: 1.,
|
||
low_env: 0.,
|
||
env_coef: coefficient(rate, 10.),
|
||
smooth: coefficient(rate, 10.),
|
||
level: db_to_gain(settings.level),
|
||
keep: 0.,
|
||
output: 1.,
|
||
bypass: 0.,
|
||
fallback: Fallback::default(),
|
||
neutral: false,
|
||
trust: 1.,
|
||
trust_coef: coefficient(rate, TRUST_MS / 3.),
|
||
meter_coef: coefficient(rate, 150.),
|
||
low_power: 0.,
|
||
sub_power: 0.,
|
||
scope_low: [0.; 2 * SCOPE],
|
||
scope_sub: [0.; 2 * SCOPE],
|
||
scope_at: 0,
|
||
scope_stride: ((rate * 0.06) / SCOPE as f32).round().max(1.) as usize,
|
||
scope_count: 0,
|
||
meter: HarmonicMeter::new(rate, GHOST_WINDOW_MS),
|
||
patch: [0.; GHOST_COUNT],
|
||
fill: [0.; GHOST_COUNT],
|
||
phone_coefs: [
|
||
// Fourth-order Butterworth high-pass, second-order low-pass.
|
||
SvfCoef::highpass(rate, PHONE_LOW, 0.541),
|
||
SvfCoef::highpass(rate, PHONE_LOW, 1.307),
|
||
SvfCoef::lowpass(rate, PHONE_HIGH.min(0.45 * rate), 0.707),
|
||
],
|
||
phone: [Svf::default(); 3],
|
||
kick: Kick::new(rate, latency),
|
||
};
|
||
e.set_settings(settings)?;
|
||
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()?;
|
||
let old = self.settings;
|
||
if (s.split - old.split).abs() > 1e-3 {
|
||
for x in &mut self.crossover {
|
||
x.retune(self.rate, s.split);
|
||
}
|
||
}
|
||
if self
|
||
.tracker
|
||
.set(s.pitch.source(s.source.mode(), s.gate, s.glide > 0.))
|
||
{
|
||
self.synth.voice.releasing = true;
|
||
}
|
||
self.glide = if s.glide <= 0. {
|
||
1.
|
||
} else {
|
||
coefficient(self.rate, s.glide / 3.)
|
||
};
|
||
self.settings = s;
|
||
Ok(())
|
||
}
|
||
pub fn reset(&mut self) {
|
||
self.delay.fill([0.; 2]);
|
||
for x in &mut self.crossover {
|
||
x.reset();
|
||
}
|
||
self.tracker.reset();
|
||
self.synth = Synth::default();
|
||
self.fallback.reset();
|
||
self.neutral = false;
|
||
self.trust = 1.;
|
||
self.low_env = 0.;
|
||
self.low_power = 0.;
|
||
self.sub_power = 0.;
|
||
self.scope_low = [0.; 2 * SCOPE];
|
||
self.scope_sub = [0.; 2 * SCOPE];
|
||
self.meter.reset();
|
||
self.patch = [0.; GHOST_COUNT];
|
||
self.fill = [0.; GHOST_COUNT];
|
||
for f in &mut self.phone {
|
||
f.reset();
|
||
}
|
||
self.kick.reset();
|
||
}
|
||
/// Lowest frequency Ghost fills.
|
||
fn ghost_edge(&self) -> f32 {
|
||
self.settings.ghost_from.max(self.settings.split)
|
||
}
|
||
/// Moves each Ghost gain toward what the patch lacks at the current note.
|
||
fn update_fill(&mut self) {
|
||
let s = &self.settings;
|
||
let v = self.synth.voice;
|
||
let amount = s.ghost * 0.01;
|
||
let edge = self.ghost_edge();
|
||
let sub = self.level.max(1e-6);
|
||
for (i, fill) in self.fill.iter_mut().enumerate() {
|
||
let k = (i + 2) as f32;
|
||
let f = k * v.hz;
|
||
let target = if amount > 0. && v.hz > 0. && f < 0.45 * self.rate {
|
||
let ramp = ((f / edge).log2() / GHOST_RAMP_OCT).clamp(0., 1.);
|
||
// A 1/k slope, like a saw; at 100 % harmonic k sits at 2/k of the sub.
|
||
let want = amount * 2. / k * ramp;
|
||
(want - self.patch[i] / sub).max(0.)
|
||
} else {
|
||
0.
|
||
};
|
||
*fill += (target - *fill) * self.smooth;
|
||
}
|
||
}
|
||
/// Ghost harmonics for a voice at `phase`, without the voice's amplitude.
|
||
#[inline]
|
||
fn ghost(&self, phase: f32) -> f32 {
|
||
let mut out = 0.;
|
||
for (i, &g) in self.fill.iter().enumerate() {
|
||
if g > 1e-5 {
|
||
out += g * (TAU * ((i + 2) as f32 * phase).fract()).sin();
|
||
}
|
||
}
|
||
out
|
||
}
|
||
/// MIDI note start at the current input sample. Used when the source is MIDI.
|
||
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);
|
||
}
|
||
#[inline]
|
||
fn shape(&self, x: f32) -> f32 {
|
||
let drive = self.settings.drive * 0.01;
|
||
if drive <= 0. {
|
||
return x;
|
||
}
|
||
let k = 1. + 6. * drive;
|
||
(k * x).tanh() / k.tanh()
|
||
}
|
||
/// The shaped sub and the Ghost harmonics, before level and dynamics.
|
||
fn oscillator(&mut self) -> (f32, f32) {
|
||
let attack = 1. / (self.rate * ATTACK_MS * 0.001);
|
||
let release = 1. / (self.rate * RELEASE_MS * 0.001);
|
||
let v = &mut self.synth.voice;
|
||
v.amp = if v.releasing {
|
||
(v.amp - release).max(0.)
|
||
} else {
|
||
(v.amp + attack).min(1.)
|
||
};
|
||
v.hz += (v.target - v.hz) * self.glide;
|
||
let (mut out, mut ghost) = (0., 0.);
|
||
let v = self.synth.voice;
|
||
if v.amp > 0. {
|
||
out += (TAU * v.phase).sin() * v.amp;
|
||
ghost += self.ghost(v.phase) * v.amp;
|
||
self.synth.voice.phase = (v.phase + v.hz / self.rate).fract();
|
||
}
|
||
let s = self.synth;
|
||
if s.old_gain > 0. {
|
||
let o = s.old;
|
||
out += (TAU * o.phase).sin() * o.amp * s.old_gain;
|
||
ghost += self.ghost(o.phase) * o.amp * s.old_gain;
|
||
let s = &mut self.synth;
|
||
s.old.phase = (o.phase + o.hz / self.rate).fract();
|
||
s.old_gain = (s.old_gain - 1. / (self.rate * CROSSFADE_MS * 0.001)).max(0.);
|
||
}
|
||
(self.shape(out), ghost)
|
||
}
|
||
/// One sample without a sidechain.
|
||
pub fn process_sample(&mut self, x: [f32; 2], monitor: Monitor) -> [f32; 2] {
|
||
self.process_with_kick(x, 0., monitor)
|
||
}
|
||
/// Finds kicks in the undelayed sidechain and schedules their duck and restart.
|
||
fn detect_kick(&mut self, side: f32, audition: bool) {
|
||
let rate = self.rate;
|
||
let s = self.settings;
|
||
let k = &mut self.kick;
|
||
if audition != k.auditioning {
|
||
k.auditioning = audition;
|
||
k.slot = 0;
|
||
k.slot_hits = 0;
|
||
k.sums = [f32::NAN; AUDITION.len()];
|
||
k.counts = [0; AUDITION.len()];
|
||
}
|
||
let level = side.abs();
|
||
k.fast += (level - k.fast) * k.fast_coef;
|
||
k.slow += (level - k.slow) * k.slow_coef;
|
||
k.hold = k.hold.saturating_sub(1);
|
||
if !s.kick_lock || k.hold > 0 || k.fast < KICK_FLOOR || k.fast < KICK_RISE * k.slow {
|
||
return;
|
||
}
|
||
k.hold = (rate * KICK_HOLD_MS * 0.001) as usize;
|
||
let (offset, slot) = if audition {
|
||
let slot = k.slot;
|
||
k.slot_hits += 1;
|
||
if k.slot_hits >= AUDITION_HITS {
|
||
k.slot_hits = 0;
|
||
k.slot = (k.slot + 1) % AUDITION.len();
|
||
}
|
||
(AUDITION[slot], Some(slot))
|
||
} else {
|
||
(s.kick_offset, None)
|
||
};
|
||
// The delayed kick leaves the plugin `latency` samples from now.
|
||
let at = k.clock + self.latency as u64;
|
||
let shift = (offset * 0.001 * rate).round() as i64;
|
||
let duck = Pending {
|
||
at,
|
||
restart: false,
|
||
offset,
|
||
slot,
|
||
};
|
||
k.schedule(duck);
|
||
k.schedule(Pending {
|
||
at: (at as i64 + shift).max(0) as u64,
|
||
restart: true,
|
||
..duck
|
||
});
|
||
}
|
||
/// Runs the kick events due at this output sample. Returns the duck gain for the sub.
|
||
fn kick_events(&mut self) -> f32 {
|
||
let s = self.settings;
|
||
let clock = self.kick.clock;
|
||
for i in 0..PENDING {
|
||
let Some(p) = self.kick.pending[i] else {
|
||
continue;
|
||
};
|
||
if p.at > clock {
|
||
continue;
|
||
}
|
||
self.kick.pending[i] = None;
|
||
if p.restart {
|
||
self.synth.kick(&s);
|
||
self.kick.offset = p.offset;
|
||
} else {
|
||
let k = &mut self.kick;
|
||
k.hits += 1;
|
||
k.rising = true;
|
||
k.measure = (self.rate * SUM_MS * 0.001) as usize;
|
||
k.measure_slot = p.slot;
|
||
k.energy = [0.; 3];
|
||
}
|
||
}
|
||
let k = &mut self.kick;
|
||
if k.rising {
|
||
k.duck += (1. - k.duck) * k.attack;
|
||
if k.duck > 0.98 {
|
||
k.rising = false;
|
||
}
|
||
} else {
|
||
k.duck -= k.duck * coefficient(self.rate, s.kick_release / 3.);
|
||
}
|
||
if !s.kick_lock {
|
||
k.duck = 0.;
|
||
}
|
||
1. - (1. - db_to_gain(-s.kick_duck)) * k.duck
|
||
}
|
||
/// Adds one sample of the delayed kick's and the sub's low band to the sum measurement.
|
||
fn measure_kick(&mut self, kick: f32, sub: f32) {
|
||
let k = &mut self.kick;
|
||
if k.measure == 0 {
|
||
return;
|
||
}
|
||
k.energy[0] += (kick + sub) * (kick + sub);
|
||
k.energy[1] += kick * kick;
|
||
k.energy[2] += sub * sub;
|
||
k.measure -= 1;
|
||
if k.measure > 0 {
|
||
return;
|
||
}
|
||
let apart = k.energy[1] + k.energy[2];
|
||
if apart < 1e-9 {
|
||
return;
|
||
}
|
||
k.sum_db = 10. * (k.energy[0] / apart).max(1e-6).log10();
|
||
if let Some(slot) = k.measure_slot {
|
||
let n = k.counts[slot] as f32;
|
||
let old = if n > 0. { k.sums[slot] } else { 0. };
|
||
k.sums[slot] = (old * n + k.sum_db) / (n + 1.);
|
||
k.counts[slot] += 1;
|
||
}
|
||
}
|
||
/// One sample with the kick's sidechain (mono) for Kick lock.
|
||
pub fn process_with_kick(&mut self, x: [f32; 2], kick: f32, monitor: Monitor) -> [f32; 2] {
|
||
self.detect_kick(kick, monitor.audition);
|
||
let len = self.kick.delay.len();
|
||
self.kick.delay[self.kick.delay_at] = kick;
|
||
self.kick.delay_at = (self.kick.delay_at + 1) % len;
|
||
let kick_late = self.kick.delay[self.kick.delay_at];
|
||
let mono = if self.channels == 2 {
|
||
0.5 * (x[0] + x[1])
|
||
} else {
|
||
x[0]
|
||
};
|
||
let (synth, settings) = (&mut self.synth, &self.settings);
|
||
self.tracker.process(mono, |e| synth.event(settings, e));
|
||
self.neutral = self.fallback.neutral(self.tracker.state());
|
||
let len = self.delay.len();
|
||
self.delay[self.delay_at] = x;
|
||
self.delay_at = (self.delay_at + 1) % len;
|
||
// The slot after the newest write is the oldest: exactly `latency` samples ago.
|
||
let y = self.delay[self.delay_at];
|
||
let s = self.settings;
|
||
let (l0, h0) = self.crossover[0].process(y[0]);
|
||
let (l1, h1) = if self.channels == 2 {
|
||
self.crossover[1].process(y[1])
|
||
} else {
|
||
(l0, h0)
|
||
};
|
||
let low = 0.5 * (l0 + l1);
|
||
if std::mem::take(&mut self.synth.fresh) {
|
||
// A new note: measure its harmonics from its first sample. Until the first
|
||
// window completes they count as missing.
|
||
self.meter.reset();
|
||
self.patch = [0.; GHOST_COUNT];
|
||
}
|
||
let v = self.synth.voice;
|
||
let hz = if v.amp > 0. && !v.releasing { v.hz } else { 0. };
|
||
if self.meter.push(0.5 * (h0 + h1), hz) && self.meter.hz() > 0. {
|
||
for (p, db) in self.patch.iter_mut().zip(&self.meter.levels()[1..]) {
|
||
*p = if *db > -119. { db_to_gain(*db) } else { 0. };
|
||
}
|
||
}
|
||
self.low_env += (low * low - self.low_env) * self.env_coef;
|
||
let env = self.low_env.sqrt();
|
||
if !self.synth.voice.releasing {
|
||
self.synth.note_peak = self.synth.note_peak.max(env);
|
||
}
|
||
let follow = if self.synth.note_peak > 1e-6 {
|
||
(env / self.synth.note_peak).min(1.)
|
||
} else {
|
||
1.
|
||
};
|
||
let dynamics = s.dynamics * 0.01;
|
||
let shape = 1. - dynamics + dynamics * follow;
|
||
self.level += (db_to_gain(s.level) - self.level) * self.smooth;
|
||
self.keep += (s.keep * 0.01 - self.keep) * self.smooth;
|
||
self.output += (db_to_gain(s.output) - self.output) * self.smooth;
|
||
self.bypass += (f32::from(s.bypass) - self.bypass) * self.smooth;
|
||
let duck = self.kick_events();
|
||
self.update_fill();
|
||
let (sub, ghost) = self.oscillator();
|
||
self.trust += (f32::from(!self.neutral) - self.trust) * self.trust_coef;
|
||
let gain = self.level * shape * duck * self.trust;
|
||
let sub = sub * gain;
|
||
let generated = sub + ghost * gain;
|
||
if self.settings.kick_lock {
|
||
let kick_low = self.kick.low.process(kick_late);
|
||
self.measure_kick(kick_low, sub);
|
||
}
|
||
self.kick.clock += 1;
|
||
self.low_power += (low * low - self.low_power) * self.meter_coef;
|
||
self.sub_power += (sub * sub - self.sub_power) * self.meter_coef;
|
||
self.scope_count += 1;
|
||
if self.scope_count >= self.scope_stride {
|
||
self.scope_count = 0;
|
||
self.scope_low[self.scope_at] = low;
|
||
self.scope_sub[self.scope_at] = generated;
|
||
self.scope_at = (self.scope_at + 1) % (2 * SCOPE);
|
||
}
|
||
// Replace keeps `keep` of the original, and all of it while the sine is out.
|
||
let keep = match s.mode {
|
||
Mode::Replace => self.keep + (1. - self.keep) * (1. - self.trust),
|
||
Mode::Enhance => 1.,
|
||
};
|
||
let wet = |l: f32, h: f32| {
|
||
if monitor.solo {
|
||
generated
|
||
} else {
|
||
h + keep * l + generated
|
||
}
|
||
};
|
||
let w = [wet(l0, h0), wet(l1, h1)];
|
||
let mut out = [0.; 2];
|
||
for c in 0..self.channels {
|
||
out[c] = (w[c] * self.output) * (1. - self.bypass) + y[c] * self.bypass;
|
||
}
|
||
if monitor.phone {
|
||
let mut m = if self.channels == 2 {
|
||
0.5 * (out[0] + out[1])
|
||
} else {
|
||
out[0]
|
||
};
|
||
for (f, c) in self.phone.iter_mut().zip(&self.phone_coefs) {
|
||
m = f.process(c, m);
|
||
}
|
||
out = [m, if self.channels == 2 { m } else { 0. }];
|
||
}
|
||
out
|
||
}
|
||
pub fn take_analyzer(&self) -> AnalyzerFrame {
|
||
let mut f = AnalyzerFrame {
|
||
latency: self.latency as u32,
|
||
sample_rate: self.rate,
|
||
low_db: 10. * self.low_power.max(1e-12).log10() + 3.01,
|
||
sub_db: 10. * self.sub_power.max(1e-12).log10() + 3.01,
|
||
scope_ms: (self.scope_stride * SCOPE) as f32 / self.rate * 1000.,
|
||
ghost_edge: self.ghost_edge(),
|
||
kick_hits: self.kick.hits,
|
||
kick_sum_db: self.kick.sum_db,
|
||
kick_offset: self.kick.offset,
|
||
audition_sums: self.kick.sums,
|
||
neutral: self.neutral,
|
||
..AnalyzerFrame::default()
|
||
};
|
||
let sub = self.level.max(1e-6);
|
||
let rel = |g: f32| {
|
||
if g > 1e-6 {
|
||
20. * (g / sub).log10()
|
||
} else {
|
||
-120.
|
||
}
|
||
};
|
||
for i in 0..GHOST_COUNT {
|
||
f.patch_harmonics[i] = rel(self.patch[i]);
|
||
f.ghost_harmonics[i] = rel(self.fill[i] * sub);
|
||
}
|
||
let v = self.synth.voice;
|
||
if v.amp > 0. && !v.releasing && !self.neutral {
|
||
f.hz = v.hz;
|
||
f.note = Some(hz_to_midi(v.hz).round().clamp(0., 127.) as u8);
|
||
}
|
||
// Trigger on the latest rising zero crossing that still leaves a full window behind it.
|
||
let n = 2 * SCOPE;
|
||
let oldest = self.scope_at;
|
||
let mut start = oldest;
|
||
for back in 0..SCOPE {
|
||
let i = (oldest + SCOPE - back) % n;
|
||
let prev = (i + n - 1) % n;
|
||
if self.scope_sub[prev] < 0. && self.scope_sub[i] >= 0. {
|
||
start = i;
|
||
break;
|
||
}
|
||
}
|
||
for k in 0..SCOPE {
|
||
let i = (start + k) % n;
|
||
f.scope_low[k] = self.scope_low[i];
|
||
f.scope_sub[k] = self.scope_sub[i];
|
||
}
|
||
f
|
||
}
|
||
}
|