feat(patina): Tone guard for bass lines

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.
This commit is contained in:
nvrl committed 2026-11-06 12:24:11 +01:00
1 parent 6d868a63c4
commit 53bc26d14d
15 files changed
+391 -10

No files matched your search

+8 -2
View File
@@ -62,6 +62,8 @@ pub struct PatinaParams {
pub mix: FloatParam,
#[id = "output"]
pub output: FloatParam,
#[id = "tone-guard"]
pub tone_guard: FloatParam,
#[id = "bypass"]
pub bypass: BoolParam,
#[nested(array, group = "Focus")]
@@ -82,6 +84,7 @@ impl Default for PatinaParams {
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()),
}
@@ -116,6 +119,7 @@ impl PatinaParams {
&self.focus[3].frequency,
&self.focus[3].width,
&self.focus[3].sensitivity,
&self.tone_guard,
]
}
pub fn settings(&self) -> Settings {
@@ -137,6 +141,7 @@ impl PatinaParams {
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 {
@@ -145,8 +150,8 @@ impl PatinaParams {
s
}
}
/// Host float parameters: ten main controls, then four per focus point.
pub const COUNT: usize = 26;
/// 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(&[
@@ -169,5 +174,6 @@ pub fn values(s: &Settings) -> [f32; COUNT] {
s.focus[i].sensitivity,
]);
}
v[26] = s.tone_guard;
v
}