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:
1 parent
7b989f7685
commit
9ca4e4f9dd
13 files changed
+568
-102
No files matched your search
@@ -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(¶ms.knee);
|
||||
setter.set_parameter(¶ms.knee, knee);
|
||||
setter.end_set_parameter(¶ms.knee);
|
||||
}
|
||||
if edited.bypass != before.bypass {
|
||||
setter.begin_set_parameter(¶ms.bypass);
|
||||
setter.set_parameter(¶ms.bypass, edited.bypass);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user