Files
bass-foundry/plugins/temper/core/src/settings.rs
T
nvrl b3ebe4d952
Check and Windows preview / check (anvil) (push) Successful in 2m8s
Check and Windows preview / check (crucible) (push) Successful in 1m32s
Check and Windows preview / check (lathe) (push) Successful in 2m13s
Check and Windows preview / check (patina) (push) Successful in 2m5s
Check and Windows preview / check (rivet) (push) Successful in 1m34s
Check and Windows preview / check (temper) (push) Successful in 2m12s
Check and Windows preview / previews (push) Successful in 1m19s
Check and Windows preview / windows (push) Successful in 2m13s
Independent plugin release / publish (push) Successful in 8s
Independent plugin release / plan (push) Successful in 6s
Independent plugin release / build (push) Successful in 2m57s
feat: add Lathe harmonic EQ and Temper tracked distortion
Lathe places EQ bands on harmonics 1-32 of the played note, so a cut on
harmonic 3 stays on harmonic 3 on every note. Temper normalises each
note's level before an oversampled shaper and tunes its floor, push and
bright filters to the note, so every note distorts the same.

The note tracker moves from Anvil into common-dsp, together with a
state-variable filter, 4x oversampler and harmonic meter. Anvil's
behaviour and editor render are unchanged.

docs/ideas.md collects all plugin ideas and their status.
2026-11-06 06:08:51 +01:00

139 lines
4.4 KiB
Rust

//! Host-independent controls. Levels in dB, harmonic positions as multiples of the fundamental.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
pub enum Source {
/// Pitch and note starts are detected from the incoming bass.
#[default]
Audio,
/// Notes come from MIDI note input.
Midi,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
pub enum Curve {
/// tanh: smooth, odd harmonics, rounded top.
#[default]
Soft,
/// Clipping with a narrow knee: dense, square-like.
Hard,
/// Sine wavefolder: drive folds the wave back, adding moving upper harmonics.
Fold,
/// Asymmetric: the negative half clips earlier, adding even harmonics.
Tube,
}
impl Curve {
pub const ALL: [Curve; 4] = [Curve::Soft, Curve::Hard, Curve::Fold, Curve::Tube];
pub fn name(self) -> &'static str {
match self {
Curve::Soft => "Soft",
Curve::Hard => "Hard",
Curve::Fold => "Fold",
Curve::Tube => "Tube",
}
}
/// Transfer function; maps 0 to 0.
#[inline]
pub fn shape(self, u: f32) -> f32 {
match self {
Curve::Soft => u.tanh(),
Curve::Hard => {
let u = u.clamp(-8., 8.);
u / (1. + u.abs().powi(8)).powf(0.125)
}
// Scaled so full drive gives about five folds; more is mostly aliasing noise.
Curve::Fold => (std::f32::consts::FRAC_PI_2 * 0.4 * u.clamp(-64., 64.)).sin(),
Curve::Tube => {
if u >= 0. {
u.tanh()
} else {
0.6 * (u / 0.6).tanh()
}
}
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct Settings {
pub source: Source,
/// Gain into the shaper after level normalisation.
pub drive: f32,
pub curve: Curve,
/// Asymmetry in percent: shifts the curve's operating point, adding even harmonics.
pub bias: f32,
/// How far the input level is normalised per note before the shaper, in percent. At 100 %
/// a quiet patch and a loud patch are driven the same.
pub normalize: f32,
/// Highpass in front of the shaper at this multiple of the fundamental; 0 is off.
pub floor: f32,
/// Harmonic that `push_gain` boosts or cuts before the shaper.
pub push_harmonic: u32,
pub push_gain: f32,
/// Lowpass after the shaper at this harmonic number; 0 is off.
pub bright: f32,
/// Matches output loudness to the input, so Drive changes character, not level.
pub auto_gain: bool,
/// Detection threshold in dBFS for note starts (Audio source).
pub gate: f32,
pub mix: f32,
pub output: f32,
pub bypass: bool,
}
impl Default for Settings {
fn default() -> Self {
Self {
source: Source::Audio,
drive: 12.,
curve: Curve::Soft,
bias: 0.,
normalize: 100.,
floor: 0.,
push_harmonic: 3,
push_gain: 0.,
bright: 0.,
auto_gain: true,
gate: -42.,
mix: 100.,
output: 0.,
bypass: false,
}
}
}
impl Settings {
pub fn validate(&self) -> Result<(), &'static str> {
let finite = [
self.drive,
self.bias,
self.normalize,
self.floor,
self.push_gain,
self.bright,
self.gate,
self.mix,
self.output,
]
.iter()
.all(|v| v.is_finite());
if !finite {
return Err("non-finite setting");
}
let ok = (0.0..=36.).contains(&self.drive)
&& (0.0..=100.).contains(&self.bias)
&& (0.0..=100.).contains(&self.normalize)
&& (self.floor == 0. || (0.25..=2.).contains(&self.floor))
&& (1..=16).contains(&self.push_harmonic)
&& (-12.0..=12.).contains(&self.push_gain)
&& (self.bright == 0. || (4.0..=64.).contains(&self.bright))
&& (-60.0..=-20.).contains(&self.gate)
&& (0.0..=100.).contains(&self.mix)
&& (-12.0..=12.).contains(&self.output);
if ok {
Ok(())
} else {
Err("setting out of range")
}
}
}