Patina detects the played note (30-500 Hz) and remembers, over about 4 s, how high each spectral bump is at a fixed frequency and relative to the note. Bumps that follow the note (a boosted harmonic the patch is built on) get less cut; bumps fixed in Hz (formants, room modes) are cut as before. On a saw line F1-F2 with harmonic 6 boosted, the cut on that harmonic drops from 5.78 dB to 0.44 dB at 100 %, while a fixed formant at 1.5 kHz stays at 5.78 dB. One held note is cut as normal, since the two cases cannot be told apart. - New parameter tone-guard (0-100 %, default 0), appended at the end; 0 % is the old behaviour and old settings load unchanged. - New Bass line preset. - Editor: Tone guard control, NOTE readout and a strip over the spectrum where cuts are held back. - No added latency, no allocation on the audio thread; patina-core now depends on common-dsp.
180 lines
5.7 KiB
Rust
180 lines
5.7 KiB
Rust
use nih_plug::prelude::*;
|
|
use nih_plug_egui::EguiState;
|
|
use patina_core::{Focus, Settings};
|
|
use std::sync::Arc;
|
|
#[derive(Params)]
|
|
pub struct FocusParams {
|
|
#[id = "enabled"]
|
|
pub enabled: FloatParam,
|
|
#[id = "frequency"]
|
|
pub frequency: FloatParam,
|
|
#[id = "width"]
|
|
pub width: FloatParam,
|
|
#[id = "sensitivity"]
|
|
pub sensitivity: FloatParam,
|
|
}
|
|
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)
|
|
}
|
|
fn log(name: &str, v: f32, min: f32, max: f32, unit: &'static str) -> FloatParam {
|
|
FloatParam::new(
|
|
name,
|
|
v,
|
|
FloatRange::Skewed {
|
|
min,
|
|
max,
|
|
factor: FloatRange::skew_factor(-2.),
|
|
},
|
|
)
|
|
.with_unit(unit)
|
|
}
|
|
impl Default for FocusParams {
|
|
fn default() -> Self {
|
|
Self {
|
|
enabled: linear("Enabled", 0., 0., 1., "").with_step_size(1.),
|
|
frequency: log("Frequency", 2000., 20., 20000., " Hz"),
|
|
width: linear("Width", 0.5, 0.05, 2., " oct"),
|
|
sensitivity: linear("Sensitivity", 0., -18., 18., " dB"),
|
|
}
|
|
}
|
|
}
|
|
#[derive(Params)]
|
|
pub struct PatinaParams {
|
|
#[persist = "editor"]
|
|
pub editor: Arc<EguiState>,
|
|
#[id = "depth"]
|
|
pub depth: FloatParam,
|
|
#[id = "sharpness"]
|
|
pub sharpness: FloatParam,
|
|
#[id = "selectivity"]
|
|
pub selectivity: FloatParam,
|
|
#[id = "attack"]
|
|
pub attack: FloatParam,
|
|
#[id = "release"]
|
|
pub release: FloatParam,
|
|
#[id = "transient-guard"]
|
|
pub transient_guard: FloatParam,
|
|
#[id = "low"]
|
|
pub low: FloatParam,
|
|
#[id = "high"]
|
|
pub high: FloatParam,
|
|
#[id = "mix"]
|
|
pub mix: FloatParam,
|
|
#[id = "output"]
|
|
pub output: FloatParam,
|
|
#[id = "tone-guard"]
|
|
pub tone_guard: FloatParam,
|
|
#[id = "bypass"]
|
|
pub bypass: BoolParam,
|
|
#[nested(array, group = "Focus")]
|
|
pub focus: [FocusParams; 4],
|
|
}
|
|
impl Default for PatinaParams {
|
|
fn default() -> Self {
|
|
let d = Settings::default();
|
|
Self {
|
|
editor: EguiState::from_size(1180, 760),
|
|
depth: linear("Depth", d.depth, 0., 20., " dB"),
|
|
sharpness: linear("Sharpness", d.sharpness, 0., 100., " %"),
|
|
selectivity: linear("Selectivity", d.selectivity, 0., 100., " %"),
|
|
attack: log("Attack", d.attack, 1., 200., " ms"),
|
|
release: log("Release", d.release, 10., 1000., " ms"),
|
|
transient_guard: linear("Transient guard", d.transient_guard, 0., 100., " %"),
|
|
low: log("Low frequency", d.low, 20., 19999., " Hz"),
|
|
high: log("High frequency", d.high, 21., 20000., " Hz"),
|
|
mix: linear("Mix", 100., 0., 100., " %"),
|
|
output: linear("Output", 0., -12., 12., " dB"),
|
|
tone_guard: linear("Tone guard", d.tone_guard, 0., 100., " %"),
|
|
bypass: BoolParam::new("Bypass", false).make_bypass(),
|
|
focus: std::array::from_fn(|_| FocusParams::default()),
|
|
}
|
|
}
|
|
}
|
|
impl PatinaParams {
|
|
pub fn floats(&self) -> [&FloatParam; COUNT] {
|
|
[
|
|
&self.depth,
|
|
&self.sharpness,
|
|
&self.selectivity,
|
|
&self.attack,
|
|
&self.release,
|
|
&self.transient_guard,
|
|
&self.low,
|
|
&self.high,
|
|
&self.mix,
|
|
&self.output,
|
|
&self.focus[0].enabled,
|
|
&self.focus[0].frequency,
|
|
&self.focus[0].width,
|
|
&self.focus[0].sensitivity,
|
|
&self.focus[1].enabled,
|
|
&self.focus[1].frequency,
|
|
&self.focus[1].width,
|
|
&self.focus[1].sensitivity,
|
|
&self.focus[2].enabled,
|
|
&self.focus[2].frequency,
|
|
&self.focus[2].width,
|
|
&self.focus[2].sensitivity,
|
|
&self.focus[3].enabled,
|
|
&self.focus[3].frequency,
|
|
&self.focus[3].width,
|
|
&self.focus[3].sensitivity,
|
|
&self.tone_guard,
|
|
]
|
|
}
|
|
pub fn settings(&self) -> Settings {
|
|
let mut s = Settings {
|
|
depth: self.depth.value(),
|
|
sharpness: self.sharpness.value(),
|
|
selectivity: self.selectivity.value(),
|
|
attack: self.attack.value(),
|
|
release: self.release.value(),
|
|
transient_guard: self.transient_guard.value(),
|
|
low: self.low.value(),
|
|
high: self.high.value(),
|
|
mix: self.mix.value(),
|
|
output: self.output.value(),
|
|
bypass: self.bypass.value(),
|
|
focus: std::array::from_fn(|i| Focus {
|
|
enabled: self.focus[i].enabled.value() >= 0.5,
|
|
frequency: self.focus[i].frequency.value(),
|
|
width: self.focus[i].width.value(),
|
|
sensitivity: self.focus[i].sensitivity.value(),
|
|
}),
|
|
tone_guard: self.tone_guard.value(),
|
|
};
|
|
// Independent automation of bounds may cross them; preserve a valid effective range.
|
|
if s.low >= s.high {
|
|
s.low = (s.high - 1.).max(20.);
|
|
}
|
|
s
|
|
}
|
|
}
|
|
/// Host float parameters: ten main controls, four per focus point, then Tone guard.
|
|
pub const COUNT: usize = 27;
|
|
pub fn values(s: &Settings) -> [f32; COUNT] {
|
|
let mut v = [0.; COUNT];
|
|
v[..10].copy_from_slice(&[
|
|
s.depth,
|
|
s.sharpness,
|
|
s.selectivity,
|
|
s.attack,
|
|
s.release,
|
|
s.transient_guard,
|
|
s.low,
|
|
s.high,
|
|
s.mix,
|
|
s.output,
|
|
]);
|
|
for i in 0..4 {
|
|
v[10 + i * 4..14 + i * 4].copy_from_slice(&[
|
|
f32::from(s.focus[i].enabled),
|
|
s.focus[i].frequency,
|
|
s.focus[i].width,
|
|
s.focus[i].sensitivity,
|
|
]);
|
|
}
|
|
v[26] = s.tone_guard;
|
|
v
|
|
}
|