feat(patina): per-band compressor gain stage with Threshold, Ratio and Knee

Tester feedback asked for a threshold and a ratio. Each band now cuts like a
small compressor: Threshold (dB above the local floor) replaces Selectivity,
Ratio scales the cut per dB above it (inf:1 is the old behaviour), and a
Soft/Hard knee switch sets the curve. A small graph shows bump height in, cut
out. Mix and Output move to the slider panel so the detection panel fits.
common-ui gains rotary_text for knobs with custom value text.
This commit is contained in:
nvrl committed 2026-11-06 17:29:54 +01:00
1 parent 7b989f7685
commit 9ca4e4f9dd
13 files changed
+568 -102

No files matched your search

+9
View File
@@ -105,6 +105,15 @@ impl Plugin for Patina {
state.gestures[i] = false;
}
}
if edited.knee != before.knee {
let knee = match edited.knee {
patina_core::Knee::Soft => params::KneeParam::Soft,
patina_core::Knee::Hard => params::KneeParam::Hard,
};
setter.begin_set_parameter(&params.knee);
setter.set_parameter(&params.knee, knee);
setter.end_set_parameter(&params.knee);
}
if edited.bypass != before.bypass {
setter.begin_set_parameter(&params.bypass);
setter.set_parameter(&params.bypass, edited.bypass);
+64 -9
View File
@@ -1,6 +1,6 @@
use nih_plug::prelude::*;
use nih_plug_egui::EguiState;
use patina_core::{Focus, Settings};
use patina_core::{Focus, Knee, Settings, RATIO_INFINITE};
use std::sync::Arc;
#[derive(Params)]
pub struct FocusParams {
@@ -38,6 +38,13 @@ impl Default for FocusParams {
}
}
}
#[derive(Enum, Debug, PartialEq, Eq, Clone, Copy)]
pub enum KneeParam {
#[id = "soft"]
Soft,
#[id = "hard"]
Hard,
}
#[derive(Params)]
pub struct PatinaParams {
#[persist = "editor"]
@@ -46,8 +53,12 @@ pub struct PatinaParams {
pub depth: FloatParam,
#[id = "sharpness"]
pub sharpness: FloatParam,
#[id = "selectivity"]
pub selectivity: FloatParam,
#[id = "threshold"]
pub threshold: FloatParam,
#[id = "ratio"]
pub ratio: FloatParam,
#[id = "knee"]
pub knee: EnumParam<KneeParam>,
#[id = "attack"]
pub attack: FloatParam,
#[id = "release"]
@@ -76,7 +87,36 @@ impl Default for PatinaParams {
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., " %"),
threshold: linear("Threshold", d.threshold, 0., 18., " dB"),
ratio: FloatParam::new(
"Ratio",
d.ratio,
FloatRange::Skewed {
min: 1.,
max: RATIO_INFINITE,
factor: FloatRange::skew_factor(-1.5),
},
)
.with_value_to_string(Arc::new(ratio_text))
.with_string_to_value(Arc::new(|s| {
let s = s
.trim()
.trim_end_matches(":1")
.trim_end_matches(": 1")
.trim();
if s == "∞" || s.eq_ignore_ascii_case("inf") {
Some(RATIO_INFINITE)
} else {
s.parse().ok()
}
})),
knee: EnumParam::new(
"Knee",
match d.knee {
Knee::Soft => KneeParam::Soft,
Knee::Hard => KneeParam::Hard,
},
),
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., " %"),
@@ -95,7 +135,7 @@ impl PatinaParams {
[
&self.depth,
&self.sharpness,
&self.selectivity,
&self.threshold,
&self.attack,
&self.release,
&self.transient_guard,
@@ -120,13 +160,19 @@ impl PatinaParams {
&self.focus[3].width,
&self.focus[3].sensitivity,
&self.tone_guard,
&self.ratio,
]
}
pub fn settings(&self) -> Settings {
let mut s = Settings {
depth: self.depth.value(),
sharpness: self.sharpness.value(),
selectivity: self.selectivity.value(),
threshold: self.threshold.value(),
ratio: self.ratio.value(),
knee: match self.knee.value() {
KneeParam::Soft => Knee::Soft,
KneeParam::Hard => Knee::Hard,
},
attack: self.attack.value(),
release: self.release.value(),
transient_guard: self.transient_guard.value(),
@@ -150,14 +196,22 @@ impl PatinaParams {
s
}
}
/// Host float parameters: ten main controls, four per focus point, then Tone guard.
pub const COUNT: usize = 27;
/// Ratio as the host shows it: "4.0:1", and "∞:1" at the top of the range.
fn ratio_text(v: f32) -> String {
if v >= RATIO_INFINITE {
"∞:1".into()
} else {
format!("{v:.1}:1")
}
}
/// Host float parameters: ten main controls, four per focus point, Tone guard, then Ratio.
pub const COUNT: usize = 28;
pub fn values(s: &Settings) -> [f32; COUNT] {
let mut v = [0.; COUNT];
v[..10].copy_from_slice(&[
s.depth,
s.sharpness,
s.selectivity,
s.threshold,
s.attack,
s.release,
s.transient_guard,
@@ -175,5 +229,6 @@ pub fn values(s: &Settings) -> [f32; COUNT] {
]);
}
v[26] = s.tone_guard;
v[27] = s.ratio;
v
}