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.
294 lines
9.7 KiB
Rust
294 lines
9.7 KiB
Rust
use anvil_core::{Mode, Settings, Source, KICK_OFFSET_MAX};
|
|
use common_params::PitchParams;
|
|
use nih_plug::prelude::*;
|
|
use nih_plug_egui::EguiState;
|
|
use std::sync::Arc;
|
|
|
|
#[derive(Enum, Debug, PartialEq, Eq, Clone, Copy)]
|
|
pub enum SourceParam {
|
|
#[id = "audio"]
|
|
Audio,
|
|
#[id = "midi"]
|
|
#[name = "MIDI"]
|
|
Midi,
|
|
#[id = "note"]
|
|
Note,
|
|
}
|
|
impl From<SourceParam> for Source {
|
|
fn from(s: SourceParam) -> Self {
|
|
match s {
|
|
SourceParam::Audio => Source::Audio,
|
|
SourceParam::Midi => Source::Midi,
|
|
SourceParam::Note => Source::Note,
|
|
}
|
|
}
|
|
}
|
|
impl From<Source> for SourceParam {
|
|
fn from(s: Source) -> Self {
|
|
match s {
|
|
Source::Audio => SourceParam::Audio,
|
|
Source::Midi => SourceParam::Midi,
|
|
Source::Note => SourceParam::Note,
|
|
}
|
|
}
|
|
}
|
|
#[derive(Enum, Debug, PartialEq, Eq, Clone, Copy)]
|
|
pub enum ModeParam {
|
|
#[id = "replace"]
|
|
Replace,
|
|
#[id = "enhance"]
|
|
Enhance,
|
|
}
|
|
impl From<ModeParam> for Mode {
|
|
fn from(m: ModeParam) -> Self {
|
|
match m {
|
|
ModeParam::Replace => Mode::Replace,
|
|
ModeParam::Enhance => Mode::Enhance,
|
|
}
|
|
}
|
|
}
|
|
impl From<Mode> for ModeParam {
|
|
fn from(m: Mode) -> Self {
|
|
match m {
|
|
Mode::Replace => ModeParam::Replace,
|
|
Mode::Enhance => ModeParam::Enhance,
|
|
}
|
|
}
|
|
}
|
|
fn linear(name: &str, v: f32, min: f32, max: f32, unit: &'static str) -> FloatParam {
|
|
FloatParam::new(name, v, FloatRange::Linear { min, max }).with_unit(unit)
|
|
}
|
|
#[derive(Params)]
|
|
pub struct AnvilParams {
|
|
#[persist = "editor"]
|
|
pub editor: Arc<EguiState>,
|
|
#[id = "source"]
|
|
pub source: EnumParam<SourceParam>,
|
|
#[nested(group = "Pitch")]
|
|
pub pitch: PitchParams,
|
|
#[id = "mode"]
|
|
pub mode: EnumParam<ModeParam>,
|
|
#[id = "level"]
|
|
pub level: FloatParam,
|
|
#[id = "split"]
|
|
pub split: FloatParam,
|
|
#[id = "dynamics"]
|
|
pub dynamics: FloatParam,
|
|
#[id = "drive"]
|
|
pub drive: FloatParam,
|
|
#[id = "ghost"]
|
|
pub ghost: FloatParam,
|
|
#[id = "ghost_from"]
|
|
pub ghost_from: FloatParam,
|
|
#[id = "octave"]
|
|
pub octave: IntParam,
|
|
#[id = "glide"]
|
|
pub glide: FloatParam,
|
|
#[id = "phase"]
|
|
pub phase: FloatParam,
|
|
#[id = "gate"]
|
|
pub gate: FloatParam,
|
|
#[id = "keep"]
|
|
pub keep: FloatParam,
|
|
#[id = "output"]
|
|
pub output: FloatParam,
|
|
#[id = "retrigger"]
|
|
pub retrigger: BoolParam,
|
|
#[id = "snap"]
|
|
pub snap: BoolParam,
|
|
#[id = "bypass"]
|
|
pub bypass: BoolParam,
|
|
#[id = "kick_lock"]
|
|
pub kick_lock: BoolParam,
|
|
#[id = "kick_duck"]
|
|
pub kick_duck: FloatParam,
|
|
#[id = "kick_release"]
|
|
pub kick_release: FloatParam,
|
|
#[id = "kick_offset"]
|
|
pub kick_offset: FloatParam,
|
|
}
|
|
impl Default for AnvilParams {
|
|
fn default() -> Self {
|
|
let d = Settings::default();
|
|
Self {
|
|
editor: EguiState::from_size(anvil_ui::WIDTH, anvil_ui::HEIGHT),
|
|
source: EnumParam::new("Pitch source", SourceParam::Audio),
|
|
pitch: PitchParams::default(),
|
|
mode: EnumParam::new("Mode", ModeParam::Replace),
|
|
level: linear("Sub level", d.level, -30., 0., " dBFS"),
|
|
split: FloatParam::new(
|
|
"Split",
|
|
d.split,
|
|
FloatRange::Skewed {
|
|
min: 40.,
|
|
max: 200.,
|
|
factor: FloatRange::skew_factor(-1.),
|
|
},
|
|
)
|
|
.with_unit(" Hz"),
|
|
dynamics: linear("Dynamics", d.dynamics, 0., 100., " %"),
|
|
drive: linear("Drive", d.drive, 0., 100., " %"),
|
|
ghost: linear("Ghost", d.ghost, 0., 100., " %"),
|
|
ghost_from: FloatParam::new(
|
|
"Ghost from",
|
|
d.ghost_from,
|
|
FloatRange::Skewed {
|
|
min: 150.,
|
|
max: 600.,
|
|
factor: FloatRange::skew_factor(-1.),
|
|
},
|
|
)
|
|
.with_unit(" Hz"),
|
|
octave: IntParam::new("Octave", d.octave, IntRange::Linear { min: -2, max: 1 }),
|
|
glide: linear("Glide", d.glide, 0., 200., " ms"),
|
|
phase: linear("Phase", d.phase, 0., 360., "°"),
|
|
gate: linear("Gate", d.gate, -60., -20., " dB"),
|
|
keep: linear("Keep", d.keep, 0., 100., " %"),
|
|
output: linear("Output", d.output, -12., 12., " dB"),
|
|
retrigger: BoolParam::new("Retrigger", d.retrigger),
|
|
snap: BoolParam::new("Snap to semitone", d.snap),
|
|
bypass: BoolParam::new("Bypass", false).make_bypass(),
|
|
kick_lock: BoolParam::new("Kick lock", d.kick_lock),
|
|
kick_duck: linear("Kick duck", d.kick_duck, 0., 24., " dB"),
|
|
kick_release: linear("Kick release", d.kick_release, 10., 300., " ms"),
|
|
kick_offset: linear(
|
|
"Kick offset",
|
|
d.kick_offset,
|
|
-KICK_OFFSET_MAX,
|
|
KICK_OFFSET_MAX,
|
|
" ms",
|
|
),
|
|
}
|
|
}
|
|
}
|
|
impl AnvilParams {
|
|
pub fn settings(&self) -> Settings {
|
|
Settings {
|
|
source: self.source.value().into(),
|
|
pitch: self.pitch.pitch(),
|
|
mode: self.mode.value().into(),
|
|
level: self.level.value(),
|
|
split: self.split.value(),
|
|
dynamics: self.dynamics.value(),
|
|
drive: self.drive.value(),
|
|
ghost: self.ghost.value(),
|
|
ghost_from: self.ghost_from.value(),
|
|
octave: self.octave.value(),
|
|
glide: self.glide.value(),
|
|
phase: self.phase.value(),
|
|
gate: self.gate.value(),
|
|
keep: self.keep.value(),
|
|
output: self.output.value(),
|
|
retrigger: self.retrigger.value(),
|
|
snap: self.snap.value(),
|
|
bypass: self.bypass.value(),
|
|
kick_lock: self.kick_lock.value(),
|
|
kick_duck: self.kick_duck.value(),
|
|
kick_release: self.kick_release.value(),
|
|
kick_offset: self.kick_offset.value(),
|
|
}
|
|
}
|
|
/// Pushes every changed editor value to the host. Continuous controls hold one
|
|
/// gesture while the pointer is down; switches are set in a single gesture.
|
|
pub fn apply(
|
|
&self,
|
|
setter: &ParamSetter,
|
|
old: &Settings,
|
|
new: &Settings,
|
|
dragging: bool,
|
|
open: &mut [bool; 14],
|
|
) {
|
|
let floats: [(&FloatParam, f32, f32); 13] = [
|
|
(&self.level, old.level, new.level),
|
|
(&self.split, old.split, new.split),
|
|
(&self.dynamics, old.dynamics, new.dynamics),
|
|
(&self.drive, old.drive, new.drive),
|
|
(&self.glide, old.glide, new.glide),
|
|
(&self.phase, old.phase, new.phase),
|
|
(&self.gate, old.gate, new.gate),
|
|
(&self.keep, old.keep, new.keep),
|
|
(&self.ghost, old.ghost, new.ghost),
|
|
(&self.ghost_from, old.ghost_from, new.ghost_from),
|
|
(&self.kick_duck, old.kick_duck, new.kick_duck),
|
|
(&self.kick_release, old.kick_release, new.kick_release),
|
|
(&self.kick_offset, old.kick_offset, new.kick_offset),
|
|
];
|
|
let mut continuous = |i: usize, changed: bool, set: &dyn Fn()| {
|
|
if changed {
|
|
if !open[i] {
|
|
open[i] = true;
|
|
set_begin(setter, self, i);
|
|
}
|
|
set();
|
|
}
|
|
if !dragging && open[i] {
|
|
set_end(setter, self, i);
|
|
open[i] = false;
|
|
}
|
|
};
|
|
for (i, (p, a, b)) in floats.into_iter().enumerate() {
|
|
continuous(i, a != b, &|| setter.set_parameter(p, b));
|
|
}
|
|
continuous(13, old.output != new.output, &|| {
|
|
setter.set_parameter(&self.output, new.output)
|
|
});
|
|
let once = |changed: bool, f: &dyn Fn()| {
|
|
if changed {
|
|
f();
|
|
}
|
|
};
|
|
once(new.octave != old.octave, &|| {
|
|
setter.begin_set_parameter(&self.octave);
|
|
setter.set_parameter(&self.octave, new.octave);
|
|
setter.end_set_parameter(&self.octave);
|
|
});
|
|
for (p, a, b) in [
|
|
(&self.retrigger, old.retrigger, new.retrigger),
|
|
(&self.snap, old.snap, new.snap),
|
|
(&self.bypass, old.bypass, new.bypass),
|
|
(&self.kick_lock, old.kick_lock, new.kick_lock),
|
|
] {
|
|
once(a != b, &|| {
|
|
setter.begin_set_parameter(p);
|
|
setter.set_parameter(p, b);
|
|
setter.end_set_parameter(p);
|
|
});
|
|
}
|
|
once(new.source != old.source, &|| {
|
|
setter.begin_set_parameter(&self.source);
|
|
setter.set_parameter(&self.source, new.source.into());
|
|
setter.end_set_parameter(&self.source);
|
|
});
|
|
once(new.mode != old.mode, &|| {
|
|
setter.begin_set_parameter(&self.mode);
|
|
setter.set_parameter(&self.mode, new.mode.into());
|
|
setter.end_set_parameter(&self.mode);
|
|
});
|
|
self.pitch.apply(setter, &old.pitch, &new.pitch);
|
|
}
|
|
fn continuous(&self, i: usize) -> &FloatParam {
|
|
[
|
|
&self.level,
|
|
&self.split,
|
|
&self.dynamics,
|
|
&self.drive,
|
|
&self.glide,
|
|
&self.phase,
|
|
&self.gate,
|
|
&self.keep,
|
|
&self.ghost,
|
|
&self.ghost_from,
|
|
&self.kick_duck,
|
|
&self.kick_release,
|
|
&self.kick_offset,
|
|
&self.output,
|
|
][i]
|
|
}
|
|
}
|
|
fn set_begin(setter: &ParamSetter, p: &AnvilParams, i: usize) {
|
|
setter.begin_set_parameter(p.continuous(i));
|
|
}
|
|
fn set_end(setter: &ParamSetter, p: &AnvilParams, i: usize) {
|
|
setter.end_set_parameter(p.continuous(i));
|
|
}
|