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.
122 lines
3.7 KiB
Rust
122 lines
3.7 KiB
Rust
//! Host parameters shared by every tracked plugin: Note mode's note, the key lock and
|
|
//! Follow bends. Plugins nest [`PitchParams`] in their own parameter struct.
|
|
use common_dsp::tracker::{Pitch, Scale, NOTE_MAX, NOTE_MIN};
|
|
use common_dsp::note_name;
|
|
use nih_plug::prelude::*;
|
|
use std::sync::Arc;
|
|
|
|
#[derive(Enum, Debug, PartialEq, Eq, Clone, Copy)]
|
|
pub enum ScaleParam {
|
|
#[id = "free"]
|
|
#[name = "Off"]
|
|
Free,
|
|
#[id = "minor"]
|
|
Minor,
|
|
#[id = "major"]
|
|
Major,
|
|
#[id = "root"]
|
|
#[name = "Root only"]
|
|
Root,
|
|
}
|
|
impl From<ScaleParam> for Scale {
|
|
fn from(s: ScaleParam) -> Self {
|
|
match s {
|
|
ScaleParam::Free => Scale::Free,
|
|
ScaleParam::Minor => Scale::Minor,
|
|
ScaleParam::Major => Scale::Major,
|
|
ScaleParam::Root => Scale::Root,
|
|
}
|
|
}
|
|
}
|
|
impl From<Scale> for ScaleParam {
|
|
fn from(s: Scale) -> Self {
|
|
match s {
|
|
Scale::Free => ScaleParam::Free,
|
|
Scale::Minor => ScaleParam::Minor,
|
|
Scale::Major => ScaleParam::Major,
|
|
Scale::Root => ScaleParam::Root,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Params)]
|
|
pub struct PitchParams {
|
|
/// The note played in Note mode; its pitch class is the key root.
|
|
#[id = "note"]
|
|
pub note: IntParam,
|
|
#[id = "scale"]
|
|
pub scale: EnumParam<ScaleParam>,
|
|
#[id = "bends"]
|
|
pub bends: BoolParam,
|
|
}
|
|
impl Default for PitchParams {
|
|
fn default() -> Self {
|
|
let d = Pitch::default();
|
|
Self {
|
|
note: IntParam::new(
|
|
"Note",
|
|
d.note as i32,
|
|
IntRange::Linear {
|
|
min: NOTE_MIN as i32,
|
|
max: NOTE_MAX as i32,
|
|
},
|
|
)
|
|
.with_value_to_string(Arc::new(|v| note_name(v.clamp(0, 127) as u8)))
|
|
.with_string_to_value(Arc::new(|s| parse_note(s).map(i32::from))),
|
|
scale: EnumParam::new("Key lock", ScaleParam::Free),
|
|
bends: BoolParam::new("Follow bends", d.bends),
|
|
}
|
|
}
|
|
}
|
|
impl PitchParams {
|
|
pub fn pitch(&self) -> Pitch {
|
|
Pitch {
|
|
note: (self.note.value().clamp(NOTE_MIN as i32, NOTE_MAX as i32)) as u8,
|
|
scale: self.scale.value().into(),
|
|
bends: self.bends.value(),
|
|
}
|
|
}
|
|
/// Pushes editor changes to the host, one gesture per change.
|
|
pub fn apply(&self, setter: &ParamSetter, old: &Pitch, new: &Pitch) {
|
|
if old.note != new.note {
|
|
setter.begin_set_parameter(&self.note);
|
|
setter.set_parameter(&self.note, new.note as i32);
|
|
setter.end_set_parameter(&self.note);
|
|
}
|
|
if old.scale != new.scale {
|
|
setter.begin_set_parameter(&self.scale);
|
|
setter.set_parameter(&self.scale, new.scale.into());
|
|
setter.end_set_parameter(&self.scale);
|
|
}
|
|
if old.bends != new.bends {
|
|
setter.begin_set_parameter(&self.bends);
|
|
setter.set_parameter(&self.bends, new.bends);
|
|
setter.end_set_parameter(&self.bends);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Reads a note name such as "F1" within the Note range.
|
|
pub fn parse_note(text: &str) -> Option<u8> {
|
|
common_dsp::parse_note(text).filter(|n| (NOTE_MIN..=NOTE_MAX).contains(n))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
#[test]
|
|
fn note_names_round_trip() {
|
|
for n in NOTE_MIN..=NOTE_MAX {
|
|
assert_eq!(parse_note(¬e_name(n)), Some(n));
|
|
}
|
|
assert_eq!(parse_note("f1"), Some(29));
|
|
assert_eq!(parse_note("H2"), None);
|
|
}
|
|
#[test]
|
|
fn scale_maps_both_ways() {
|
|
for s in [Scale::Free, Scale::Minor, Scale::Major, Scale::Root] {
|
|
assert_eq!(Scale::from(ScaleParam::from(s)), s);
|
|
}
|
|
}
|
|
}
|