feat: add Lathe harmonic EQ and Temper tracked distortion
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
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
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.
This commit is contained in:
1 parent
f36ff0780c
commit
b3ebe4d952
48 files changed
+9862
-339
No files matched your search
@@ -0,0 +1,405 @@
|
||||
//! Note-tracked distortion: the filters around the shaper follow the played note and the
|
||||
//! input level is normalised per note, so every note distorts with the same character.
|
||||
//!
|
||||
//! The audio path is delayed by the tracker's lookahead, so filters and drive are set for a
|
||||
//! note before its attack reaches the shaper. The shaper runs at 4× the sample rate.
|
||||
mod settings;
|
||||
use common_dsp::meter::{HarmonicMeter, HARMONICS};
|
||||
use common_dsp::oversample::Oversampler;
|
||||
use common_dsp::svf::{Svf, SvfCoef};
|
||||
use common_dsp::tracker::{NoteEvent, PitchFollower, Tracker, TrackerConfig};
|
||||
use common_dsp::{coefficient, db_to_gain, gain_to_db, hz_to_midi};
|
||||
pub use settings::{Curve, Settings, Source};
|
||||
|
||||
pub const MIN_HZ: f32 = TrackerConfig::BASS.min_hz;
|
||||
pub const MAX_HZ: f32 = TrackerConfig::BASS.max_hz;
|
||||
/// Harmonics shown by the editor.
|
||||
pub const SHOWN: usize = 16;
|
||||
/// RMS level that Normalize brings every note to before Drive.
|
||||
const TARGET_DB: f32 = -12.;
|
||||
const RETUNE: usize = 16;
|
||||
const DRY: usize = 64;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct AnalyzerFrame {
|
||||
pub hz: f32,
|
||||
pub note: Option<u8>,
|
||||
/// Harmonic levels in dBFS of the delayed input and of the output (index 0 = fundamental).
|
||||
pub input: [f32; SHOWN],
|
||||
pub output: [f32; SHOWN],
|
||||
/// Peak level reaching the shaper, after normalisation and drive, in dB.
|
||||
pub shaper_db: f32,
|
||||
/// Gain applied by Normalize and by Auto gain, in dB.
|
||||
pub normalize_db: f32,
|
||||
pub auto_db: f32,
|
||||
pub latency: u32,
|
||||
pub sample_rate: f32,
|
||||
}
|
||||
impl Default for AnalyzerFrame {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
hz: 0.,
|
||||
note: None,
|
||||
input: [-120.; SHOWN],
|
||||
output: [-120.; SHOWN],
|
||||
shaper_db: -120.,
|
||||
normalize_db: 0.,
|
||||
auto_db: 0.,
|
||||
latency: 0,
|
||||
sample_rate: 48000.,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-note input level, measured on the undelayed input so it is known at the attack.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
struct Level {
|
||||
/// RMS of the undelayed input.
|
||||
ahead: f32,
|
||||
/// Loudest `ahead` since the note started, slowly released.
|
||||
note: f32,
|
||||
}
|
||||
impl Level {
|
||||
fn event(&mut self, e: NoteEvent) {
|
||||
if let NoteEvent::Start { glide: false, .. } = e {
|
||||
// `ahead` already looks one lookahead into the new note.
|
||||
self.note = self.ahead;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct Filters {
|
||||
floor: SvfCoef,
|
||||
push: SvfCoef,
|
||||
bright: SvfCoef,
|
||||
}
|
||||
|
||||
struct Channel {
|
||||
floor: Svf,
|
||||
push: Svf,
|
||||
bright: Svf,
|
||||
over: Oversampler,
|
||||
dc_x: f32,
|
||||
dc_y: f32,
|
||||
dry: [f32; DRY],
|
||||
}
|
||||
impl Channel {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
floor: Svf::default(),
|
||||
push: Svf::default(),
|
||||
bright: Svf::default(),
|
||||
over: Oversampler::new(),
|
||||
dc_x: 0.,
|
||||
dc_y: 0.,
|
||||
dry: [0.; DRY],
|
||||
}
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.floor.reset();
|
||||
self.push.reset();
|
||||
self.bright.reset();
|
||||
self.over.reset();
|
||||
self.dc_x = 0.;
|
||||
self.dc_y = 0.;
|
||||
self.dry = [0.; DRY];
|
||||
}
|
||||
}
|
||||
|
||||
struct Smoothed {
|
||||
value: f32,
|
||||
coef: f32,
|
||||
}
|
||||
impl Smoothed {
|
||||
fn new(value: f32, coef: f32) -> Self {
|
||||
Self { value, coef }
|
||||
}
|
||||
#[inline]
|
||||
fn next(&mut self, target: f32) -> f32 {
|
||||
let d = target - self.value;
|
||||
self.value = if d.abs() < 1e-6 {
|
||||
target
|
||||
} else {
|
||||
self.value + d * self.coef
|
||||
};
|
||||
self.value
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Engine {
|
||||
rate: f32,
|
||||
channels: usize,
|
||||
settings: Settings,
|
||||
latency: usize,
|
||||
delay: Vec<[f32; 2]>,
|
||||
delay_at: usize,
|
||||
dry_at: usize,
|
||||
tracker: Tracker,
|
||||
pitch: PitchFollower,
|
||||
hz_coef: f32,
|
||||
countdown: usize,
|
||||
filters: Filters,
|
||||
channel: [Channel; 2],
|
||||
level: Level,
|
||||
rms_coef: f32,
|
||||
release: f32,
|
||||
norm_db: Smoothed,
|
||||
drive: Smoothed,
|
||||
bias: Smoothed,
|
||||
mix: Smoothed,
|
||||
output: Smoothed,
|
||||
bypass: Smoothed,
|
||||
auto_db: Smoothed,
|
||||
power_in: f32,
|
||||
power_out: f32,
|
||||
power_coef: f32,
|
||||
dc_r: f32,
|
||||
shaper_peak: f32,
|
||||
meter_in: HarmonicMeter,
|
||||
meter_out: HarmonicMeter,
|
||||
/// Smoothed controls start at the next settings instead of gliding from defaults.
|
||||
fresh: bool,
|
||||
}
|
||||
|
||||
impl Engine {
|
||||
pub fn new(rate: f32, channels: usize) -> Result<Self, &'static str> {
|
||||
if !(8000.0..=768000.).contains(&rate) {
|
||||
return Err("unsupported sample rate");
|
||||
}
|
||||
if !(1..=2).contains(&channels) {
|
||||
return Err("unsupported channel count");
|
||||
}
|
||||
let tracker = Tracker::new(rate, TrackerConfig::BASS);
|
||||
let lookahead = tracker.latency();
|
||||
let smooth = coefficient(rate, 10.);
|
||||
let settings = Settings::default();
|
||||
let mut e = Self {
|
||||
rate,
|
||||
channels,
|
||||
settings,
|
||||
latency: lookahead + Oversampler::latency(),
|
||||
delay: vec![[0.; 2]; lookahead + 1],
|
||||
delay_at: 0,
|
||||
dry_at: 0,
|
||||
tracker,
|
||||
pitch: PitchFollower::default(),
|
||||
hz_coef: coefficient(rate, 5.),
|
||||
countdown: 0,
|
||||
filters: Filters {
|
||||
floor: SvfCoef::IDENTITY,
|
||||
push: SvfCoef::IDENTITY,
|
||||
bright: SvfCoef::IDENTITY,
|
||||
},
|
||||
channel: [Channel::new(), Channel::new()],
|
||||
level: Level::default(),
|
||||
rms_coef: coefficient(rate, 10.),
|
||||
release: 1. - coefficient(rate, 1500.),
|
||||
norm_db: Smoothed::new(0., coefficient(rate, 5.)),
|
||||
drive: Smoothed::new(db_to_gain(settings.drive), smooth),
|
||||
bias: Smoothed::new(0., smooth),
|
||||
mix: Smoothed::new(1., smooth),
|
||||
output: Smoothed::new(1., smooth),
|
||||
bypass: Smoothed::new(0., smooth),
|
||||
auto_db: Smoothed::new(0., coefficient(rate, 50.)),
|
||||
power_in: 0.,
|
||||
power_out: 0.,
|
||||
power_coef: coefficient(rate, 300.),
|
||||
dc_r: 1. - std::f32::consts::TAU * 10. / rate,
|
||||
shaper_peak: 0.,
|
||||
meter_in: HarmonicMeter::new(rate, 85.),
|
||||
meter_out: HarmonicMeter::new(rate, 85.),
|
||||
fresh: true,
|
||||
};
|
||||
e.set_settings(settings)?;
|
||||
e.fresh = true;
|
||||
Ok(e)
|
||||
}
|
||||
/// Lookahead plus the oversampling filters.
|
||||
pub fn latency_samples(&self) -> u32 {
|
||||
self.latency as u32
|
||||
}
|
||||
pub fn channels(&self) -> usize {
|
||||
self.channels
|
||||
}
|
||||
pub fn settings(&self) -> Settings {
|
||||
self.settings
|
||||
}
|
||||
pub fn set_settings(&mut self, s: Settings) -> Result<(), &'static str> {
|
||||
s.validate()?;
|
||||
self.tracker.set(s.source == Source::Midi, s.gate, true);
|
||||
if std::mem::take(&mut self.fresh) {
|
||||
self.drive.value = db_to_gain(s.drive);
|
||||
self.bias.value = s.bias * 0.008;
|
||||
self.mix.value = s.mix * 0.01;
|
||||
self.output.value = db_to_gain(s.output);
|
||||
self.bypass.value = f32::from(s.bypass);
|
||||
}
|
||||
self.settings = s;
|
||||
Ok(())
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.delay.fill([0.; 2]);
|
||||
self.tracker.reset();
|
||||
for c in &mut self.channel {
|
||||
c.reset();
|
||||
}
|
||||
self.level = Level::default();
|
||||
self.power_in = 0.;
|
||||
self.power_out = 0.;
|
||||
self.shaper_peak = 0.;
|
||||
self.meter_in.reset();
|
||||
self.meter_out.reset();
|
||||
self.countdown = 0;
|
||||
self.fresh = true;
|
||||
}
|
||||
pub fn note_on(&mut self, note: u8) {
|
||||
self.tracker.note_on(note);
|
||||
}
|
||||
pub fn note_off(&mut self, note: u8) {
|
||||
self.tracker.note_off(note);
|
||||
}
|
||||
fn retune(&mut self, hz: f32) {
|
||||
let s = &self.settings;
|
||||
let limit = 0.45 * self.rate;
|
||||
let tuned = |mult: f32| hz > 0. && mult > 0. && hz * mult < limit;
|
||||
self.filters.floor = if tuned(s.floor) {
|
||||
SvfCoef::highpass(self.rate, hz * s.floor, std::f32::consts::FRAC_1_SQRT_2)
|
||||
} else {
|
||||
SvfCoef::IDENTITY
|
||||
};
|
||||
let k = s.push_harmonic as f32;
|
||||
self.filters.push = if tuned(k) && s.push_gain != 0. {
|
||||
SvfCoef::bell(self.rate, hz * k, k / 0.6, s.push_gain)
|
||||
} else {
|
||||
SvfCoef::IDENTITY
|
||||
};
|
||||
self.filters.bright = if tuned(s.bright) {
|
||||
SvfCoef::lowpass(self.rate, hz * s.bright, std::f32::consts::FRAC_1_SQRT_2)
|
||||
} else {
|
||||
SvfCoef::IDENTITY
|
||||
};
|
||||
}
|
||||
pub fn process_sample(&mut self, x: [f32; 2]) -> [f32; 2] {
|
||||
let mono = if self.channels == 2 {
|
||||
0.5 * (x[0] + x[1])
|
||||
} else {
|
||||
x[0]
|
||||
};
|
||||
// Level of the input one lookahead ahead of the audio that is about to be shaped.
|
||||
let a = &mut self.level;
|
||||
a.ahead += (mono * mono - a.ahead) * self.rms_coef;
|
||||
a.note = (a.note * self.release).max(a.ahead);
|
||||
let (pitch, level) = (&mut self.pitch, &mut self.level);
|
||||
self.tracker.process(mono, |e| {
|
||||
pitch.event(false, e);
|
||||
level.event(e);
|
||||
});
|
||||
let len = self.delay.len();
|
||||
self.delay[self.delay_at] = x;
|
||||
self.delay_at = (self.delay_at + 1) % len;
|
||||
let y = self.delay[self.delay_at];
|
||||
|
||||
let (hz, jump) = self.pitch.step(self.hz_coef);
|
||||
if jump || self.countdown == 0 {
|
||||
self.retune(hz);
|
||||
self.countdown = RETUNE;
|
||||
}
|
||||
self.countdown -= 1;
|
||||
|
||||
let s = self.settings;
|
||||
let rms_db = 10. * self.level.note.max(1e-12).log10();
|
||||
// Below −60 dBFS there is nothing to normalise; hold the last gain.
|
||||
let norm_target = if rms_db > -60. {
|
||||
(s.normalize * 0.01 * (TARGET_DB - rms_db)).clamp(-24., 36.)
|
||||
} else {
|
||||
self.norm_db.value
|
||||
};
|
||||
let norm = db_to_gain(self.norm_db.next(norm_target));
|
||||
let drive = self.drive.next(db_to_gain(s.drive));
|
||||
let bias = self.bias.next(s.bias * 0.008);
|
||||
let mix = self.mix.next(s.mix * 0.01);
|
||||
let output = self.output.next(db_to_gain(s.output));
|
||||
let bypass = self.bypass.next(f32::from(s.bypass));
|
||||
let curve = s.curve;
|
||||
let offset = curve.shape(bias);
|
||||
let f = self.filters;
|
||||
let dry_at = self.dry_at;
|
||||
let back = (dry_at + DRY - Oversampler::latency()) % DRY;
|
||||
let mut wet = [0.; 2];
|
||||
let mut dry = [0.; 2];
|
||||
let mut peak = self.shaper_peak;
|
||||
for c in 0..self.channels {
|
||||
let ch = &mut self.channel[c];
|
||||
ch.dry[dry_at] = y[c];
|
||||
dry[c] = ch.dry[back];
|
||||
let v = ch.floor.process(&f.floor, y[c]);
|
||||
let v = ch.push.process(&f.push, v) * norm * drive;
|
||||
let up = ch.over.up(v);
|
||||
let shaped = up.map(|u| {
|
||||
peak = peak.max(u.abs());
|
||||
curve.shape(u + bias) - offset
|
||||
});
|
||||
let v = ch.over.down(shaped);
|
||||
let v = ch.bright.process(&f.bright, v);
|
||||
// DC blocker: bias and asymmetric curves leave an offset.
|
||||
let d = v - ch.dc_x + self.dc_r * ch.dc_y;
|
||||
ch.dc_x = v;
|
||||
ch.dc_y = d;
|
||||
wet[c] = d;
|
||||
}
|
||||
self.shaper_peak = peak;
|
||||
self.dry_at = (dry_at + 1) % DRY;
|
||||
|
||||
let (dry_mono, wet_mono) = if self.channels == 2 {
|
||||
(0.5 * (dry[0] + dry[1]), 0.5 * (wet[0] + wet[1]))
|
||||
} else {
|
||||
(dry[0], wet[0])
|
||||
};
|
||||
self.power_in += (dry_mono * dry_mono - self.power_in) * self.power_coef;
|
||||
self.power_out += (wet_mono * wet_mono - self.power_out) * self.power_coef;
|
||||
let auto_target = if !s.auto_gain {
|
||||
0.
|
||||
} else if self.power_in > 1e-7 && self.power_out > 1e-9 {
|
||||
(10. * (self.power_in / self.power_out).log10()).clamp(-36., 24.)
|
||||
} else {
|
||||
self.auto_db.value
|
||||
};
|
||||
let auto = db_to_gain(self.auto_db.next(auto_target));
|
||||
let mut out = [0.; 2];
|
||||
for c in 0..self.channels {
|
||||
let v = (mix * wet[c] * auto + (1. - mix) * dry[c]) * output;
|
||||
out[c] = v * (1. - bypass) + dry[c] * bypass;
|
||||
}
|
||||
let tone = if self.tracker.sounding() { hz } else { 0. };
|
||||
let o = if self.channels == 2 {
|
||||
0.5 * (out[0] + out[1])
|
||||
} else {
|
||||
out[0]
|
||||
};
|
||||
self.meter_in.push(dry_mono, tone);
|
||||
self.meter_out.push(o, tone);
|
||||
out
|
||||
}
|
||||
pub fn take_analyzer(&mut self) -> AnalyzerFrame {
|
||||
let hz = self.pitch.hz();
|
||||
let mut f = AnalyzerFrame {
|
||||
hz,
|
||||
latency: self.latency as u32,
|
||||
sample_rate: self.rate,
|
||||
shaper_db: gain_to_db(self.shaper_peak),
|
||||
normalize_db: self.norm_db.value,
|
||||
auto_db: self.auto_db.value,
|
||||
..AnalyzerFrame::default()
|
||||
};
|
||||
self.shaper_peak = 0.;
|
||||
f.input
|
||||
.copy_from_slice(&self.meter_in.levels()[..SHOWN.min(HARMONICS)]);
|
||||
f.output
|
||||
.copy_from_slice(&self.meter_out.levels()[..SHOWN.min(HARMONICS)]);
|
||||
if self.tracker.sounding() && hz > 0. {
|
||||
f.note = Some(hz_to_midi(hz).round().clamp(0., 127.) as u8);
|
||||
}
|
||||
f
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
//! 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user