Skip to content

Commit

Permalink
feat: component matching + bypass (original + buffers)
Browse files Browse the repository at this point in the history
  • Loading branch information
SolarLiner committed Jan 18, 2024
1 parent e3e0542 commit e029584
Show file tree
Hide file tree
Showing 2 changed files with 164 additions and 27 deletions.
86 changes: 75 additions & 11 deletions src/dsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,48 @@ use valib::filters::statespace::StateSpace;
use valib::saturators::Slew;
use valib::Scalar;

#[derive(Debug, Copy, Clone)]
pub struct Bypass<T> {
pub inner: T,
pub active: bool,
}

impl<T, const N: usize> DSP<N, N> for Bypass<T>
where
T: DSP<N, N>,
{
type Sample = T::Sample;

fn process(&mut self, x: [Self::Sample; N]) -> [Self::Sample; N] {
if self.active {
self.inner.process(x)
} else {
x
}
}

fn latency(&self) -> usize {
if self.active {
self.inner.latency()
} else {
0
}
}

fn reset(&mut self) {
self.inner.reset();
}
}

impl<T> Bypass<T> {
pub fn new(inner: T) -> Self {
Self {
inner,
active: true,
}
}
}

#[derive(Debug, Copy, Clone)]
pub struct InputStage<T: Scalar> {
pub gain: T,
Expand Down Expand Up @@ -40,38 +82,59 @@ impl<T: Scalar> InputStage<T> {
}
}

fn crossover_half<T: Scalar>(x: T, a: T, b: T) -> T {
T::simd_ln(T::simd_exp(b * x) + T::from_f64(10.0).simd_powf(a)) / b
}

fn crossover<T: Scalar>(x: T, a: T, b: T) -> T {
crossover_half(x, a, b) - crossover_half(-x, a, b)
}

#[derive(Debug, Copy, Clone)]
pub struct ClipperStage<T: Scalar>(StateSpace<T, 1, 3, 1>, Slew<T>);
pub struct ClipperStage<T: Scalar> {
state_space: StateSpace<T, 1, 3, 1>,
pub crossover: (T, T),
pub(crate) slew: Slew<T>,
}

impl<T: Scalar> DSP<1, 1> for ClipperStage<T> {
type Sample = T;

#[replace_float_literals(Self::Sample::from_f64(literal))]
fn process(&mut self, x: [Self::Sample; 1]) -> [Self::Sample; 1] {
let [y] = self.0.process(x);
self.1.process([y.simd_asinh()])
let [y] = self.state_space.process(x);
let y = y.simd_asinh().simd_clamp(-4.5, 4.5);
let [y] = self.slew.process([y]);
let y = crossover(y, self.crossover.0, self.crossover.1);
[y]
}

fn latency(&self) -> usize {
self.0.latency() + self.1.latency()
self.state_space.latency() + self.slew.latency()
}

fn reset(&mut self) {
self.0.reset();
self.1.reset();
self.state_space.reset();
self.slew.reset();
}
}

impl<T: Scalar> ClipperStage<T> {
#[replace_float_literals(T::from_f64(literal))]
pub fn new(samplerate: T, dist: T) -> Self {
let dt = samplerate.simd_recip();
Self(crate::gen::clipper(dt, dist), Slew::new(1e4 * dt))
Self {
state_space: crate::gen::clipper(dt, dist),
crossover: (0.0, 30.0),
slew: Slew::new(1e4 * dt),
}
}

pub fn set_params(&mut self, samplerate: T, dist: T) {
let dt = samplerate.simd_recip();
self.0.update_matrices(&crate::gen::clipper(dt, dist));
self.1.set_max_diff(T::from_f64(1e5), samplerate);
self.state_space
.update_matrices(&crate::gen::clipper(dt, dist));
self.slew.set_max_diff(T::from_f64(1e5), samplerate);
}
}

Expand Down Expand Up @@ -109,19 +172,20 @@ impl<T: Scalar> ToneStage<T> {

#[derive(Debug, Copy, Clone)]
pub struct OutputStage<T: Scalar> {
pub inner: StateSpace<T, 1, 2, 1>,
pub inner: Bypass<StateSpace<T, 1, 2, 1>>,
pub gain: T,
}

impl<T: Scalar> OutputStage<T> {
pub fn new(samplerate: T, gain: T) -> Self {
Self {
inner: crate::gen::output(samplerate.simd_recip()),
inner: Bypass::new(crate::gen::output(samplerate.simd_recip())),
gain,
}
}
pub fn set_samplerate(&mut self, samplerate: T) {
self.inner
.inner
.update_matrices(&crate::gen::output(samplerate.simd_recip()));
}
}
Expand Down
105 changes: 89 additions & 16 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,21 @@ use valib::dsp::utils::{slice_to_mono_block, slice_to_mono_block_mut};
use valib::dsp::{DSPBlock, DSP};
use valib::oversample::Oversample;
use valib::simd::{AutoF64x2, SimdValue};
use valib::util::lerp;
use valib::Scalar;

use crate::dsp::{ClipperStage, InputStage, OutputStage, ToneStage};
use crate::dsp::{Bypass, ClipperStage, InputStage, OutputStage, ToneStage};

mod dsp;
mod gen;

type Sample = AutoF64x2;
type Dsp = Series<(
InputStage<Sample>,
ClipperStage<Sample>,
ToneStage<Sample>,
Bypass<InputStage<Sample>>,
Bypass<Series<(ClipperStage<Sample>, ToneStage<Sample>)>>,
OutputStage<Sample>,
)>;

const OVERSAMPLE: usize = 4;
const MAX_BLOCK_SIZE: usize = 512;

Expand All @@ -40,6 +42,12 @@ struct Ts404Params {
tone: FloatParam,
#[id = "level"]
out_level: FloatParam,
#[id = "cmpmat"]
component_matching: FloatParam,
#[id = "bypass"]
bypass: BoolParam,
#[id = "byp_io"]
io_bypass: BoolParam,
}

impl Default for Ts404 {
Expand All @@ -48,9 +56,11 @@ impl Default for Ts404 {
Self {
params: Arc::new(Ts404Params::default()),
dsp: Series((
InputStage::new(samplerate, Sample::splat(1.0)),
ClipperStage::new(samplerate, Sample::splat(0.1)),
ToneStage::new(samplerate, Sample::splat(0.5)),
Bypass::new(InputStage::new(samplerate, Sample::splat(1.0))),
Bypass::new(Series((
ClipperStage::new(samplerate, Sample::splat(0.1)),
ToneStage::new(samplerate, Sample::splat(0.5)),
))),
OutputStage::new(samplerate, Sample::splat(1.0)),
)),
oversample: Oversample::new(OVERSAMPLE, MAX_BLOCK_SIZE),
Expand All @@ -72,16 +82,16 @@ impl Default for Ts404Params {
)
.with_unit("dB")
.with_value_to_string(formatters::v2s_f32_gain_to_db(2))
.with_string_to_value(formatters::s2v_f32_gain_to_db())
.with_smoother(SmoothingStyle::Linear(50.0)),
.with_string_to_value(formatters::s2v_f32_gain_to_db()),
// .with_smoother(SmoothingStyle::Linear(50.0)),
dist: FloatParam::new("Distortion", 0.1, FloatRange::Linear { min: 0.0, max: 1.0 })
.with_unit("%")
.with_smoother(SmoothingStyle::Linear(50.0))
// .with_smoother(SmoothingStyle::Linear(50.0))
.with_value_to_string(formatters::v2s_f32_percentage(2))
.with_string_to_value(formatters::s2v_f32_percentage()),
tone: FloatParam::new("Tone", 0.5, FloatRange::Linear { min: 0.0, max: 1.0 })
.with_unit("%")
.with_smoother(SmoothingStyle::Linear(50.0))
// .with_smoother(SmoothingStyle::Linear(50.0))
.with_value_to_string(formatters::v2s_f32_percentage(2))
.with_string_to_value(formatters::s2v_f32_percentage()),
out_level: FloatParam::new(
Expand All @@ -94,13 +104,38 @@ impl Default for Ts404Params {
},
)
.with_unit("dB")
.with_smoother(SmoothingStyle::Linear(50.0))
// .with_smoother(SmoothingStyle::Linear(50.0))
.with_value_to_string(formatters::v2s_f32_gain_to_db(2))
.with_string_to_value(formatters::s2v_f32_gain_to_db()),
component_matching: FloatParam::new(
"Component Matching",
1.,
FloatRange::Linear { min: 0.0, max: 1.0 },
)
.with_unit("%")
// .with_smoother(SmoothingStyle::Linear(10.0))
.with_string_to_value(formatters::s2v_f32_percentage())
.with_value_to_string(formatters::v2s_f32_percentage(0)),
bypass: BoolParam::new("Bypass", false),
io_bypass: BoolParam::new("I/O Buffers Bypass", false),
}
}
}

fn component_matching_slew_rate(samplerate: Sample, normalized: Sample) -> Sample {
let min = Sample::splat(db_to_gain_fast(30.0) as _);
let max = Sample::splat(db_to_gain_fast(100.0) as _);
lerp(normalized, min, max) / samplerate
}

fn component_matching_crossover(normalized: Sample) -> (Sample, Sample) {
let min = Sample::splat(10.0);
let max = Sample::splat(0.5);
let a = lerp(normalized, min, max);
let b = Sample::from_f64(10.0);
(a, b)
}

impl Plugin for Ts404 {
const NAME: &'static str = "TS-404";
const VENDOR: &'static str = "SolarLiner";
Expand Down Expand Up @@ -149,11 +184,30 @@ impl Plugin for Ts404 {
_context: &mut impl InitContext<Self>,
) -> bool {
let samplerate = Sample::splat(buffer_config.sample_rate as _);
let Series((input, clipping, tone, output)) = &mut self.dsp;
input.set_samplerate(samplerate);
let Series((
Bypass {
inner: input,
active: input_active,
},
Bypass {
inner: Series((clipping, tone)),
active: main_active,
},
output,
)) = &mut self.dsp;

let component_matching = Sample::from_f64(self.params.component_matching.value() as _);

let io_active = !self.params.io_bypass.value();
*main_active = !self.params.bypass.value();
*input_active = io_active;
output.inner.active = io_active;
input.gain = Sample::splat(self.params.drive.value() as _);
clipping.set_params(samplerate, Sample::splat(self.params.dist.value() as _));
clipping.crossover = component_matching_crossover(component_matching);
clipping.slew.max_diff = component_matching_slew_rate(samplerate, component_matching);
tone.update_params(samplerate, Sample::splat(self.params.tone.value() as _));
output.set_samplerate(samplerate);
output.gain = Sample::splat(self.params.out_level.value() as _);
true
}

Expand All @@ -168,9 +222,27 @@ impl Plugin for Ts404 {
context: &mut impl ProcessContext<Self>,
) -> ProcessStatus {
let samplerate = Sample::splat(context.transport().sample_rate as _);
let Series((input, clipping, tone, output)) = &mut self.dsp;
let Series((
Bypass {
inner: input,
active: input_active,
},
Bypass {
inner: Series((clipping, tone)),
active: main_active,
},
output,
)) = &mut self.dsp;

let component_matching = Sample::from_f64(self.params.component_matching.value() as _);
let io_active = !self.params.io_bypass.value();
*main_active = !self.params.bypass.value();
*input_active = io_active;
output.inner.active = io_active;
input.gain = Sample::splat(self.params.drive.value() as _);
clipping.set_params(samplerate, Sample::splat(self.params.dist.value() as _));
clipping.crossover = component_matching_crossover(component_matching);
clipping.slew.max_diff = component_matching_slew_rate(samplerate, component_matching);
tone.update_params(samplerate, Sample::splat(self.params.tone.value() as _));
output.gain = Sample::splat(self.params.out_level.value() as _);

Expand Down Expand Up @@ -220,6 +292,7 @@ fn safety_clipper(buffer: &mut Buffer) {
}
}
}

impl ClapPlugin for Ts404 {
const CLAP_ID: &'static str = "dev.solarliner.ts404";
const CLAP_DESCRIPTION: Option<&'static str> =
Expand Down

0 comments on commit e029584

Please sign in to comment.