From 456a22119eaf25cc65df6bc7bb05c9e8671a0091 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Sun, 2 Apr 2023 16:40:05 +0200 Subject: [PATCH] Port the Hard Vacuum algorithm --- plugins/aw_soft_vacuum/src/hard_vacuum.rs | 88 +++++++++++++++++++++++ plugins/aw_soft_vacuum/src/lib.rs | 19 +++++ 2 files changed, 107 insertions(+) diff --git a/plugins/aw_soft_vacuum/src/hard_vacuum.rs b/plugins/aw_soft_vacuum/src/hard_vacuum.rs index f02b7e22..f18f6705 100644 --- a/plugins/aw_soft_vacuum/src/hard_vacuum.rs +++ b/plugins/aw_soft_vacuum/src/hard_vacuum.rs @@ -15,6 +15,14 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use std::f32::consts::{FRAC_PI_2, PI}; + +use nih_plug::nih_debug_assert; + +/// For some reason this constant is used quite a few times in the Hard Vacuum implementation. I'm +/// pretty sure it's a typo. +const ALMOST_FRAC_PI_2: f32 = 1.557_079_7; + /// Single-channel port of the Hard Vacuum algorithm from /// . #[derive(Debug, Default)] @@ -22,6 +30,18 @@ pub struct HardVacuum { last_sample: f32, } +/// Parameters for the [`HardVacuum`] algorithm. This is a struct to make it easier to reuse the +/// same values for multiple channels. +pub struct Params { + /// The 'drive' parameter, should be in the range `[0, 2]`. Controls both the drive and how many + /// distortion stages are applied. + pub drive: f32, + /// The 'warmth' parameter, should be in the range `[0, 1]`. + pub warmth: f32, + /// The 'aura' parameter, should be in the range `[0, pi]`. + pub aura: f32, +} + impl HardVacuum { /// Reset the processor's state. In this case this only resets the discrete derivative /// calculation. Doesn't make a huge difference but it's still useful to make the effect @@ -29,4 +49,72 @@ impl HardVacuum { pub fn reset(&mut self) { self.last_sample = 0.0; } + + /// Process a sample for a single channel. Because this maintains per-channel internal state, + /// you should use different [`HardVacuum`] objects for each channel when processing + /// multichannel audio. + /// + /// Output scaling and dry/wet mixing should be done externally. + pub fn process(&mut self, input: f32, params: &Params) -> f32 { + // We'll skip a couple unnecessary things here like the dithering and the manual denormal + // evasion + nih_debug_assert!((0.0..=2.0).contains(¶ms.drive)); + nih_debug_assert!((0.0..=1.0).contains(¶ms.warmth)); + nih_debug_assert!((0.0..=PI).contains(¶ms.aura)); + + // These two values are derived from the warmth parameter in an ...interesting way + let scaled_warmth = params.warmth / FRAC_PI_2; + let inverse_warmth = 1.0 - params.warmth; + + // AW: We're doing all this here so skew isn't incremented by each stage + let skew = { + // AW: skew will be direction/angle + let skew = input - self.last_sample; + // AW: for skew we want it to go to zero effect again, so we use full range of the sine + let bridge_rectifier = skew.abs().min(PI).sin(); + + // AW: skew is now sined and clamped and then re-amplified again + // AW @ the `* 1.557` part: cools off sparkliness and crossover distortion + // NOTE: The 1.55707 is presumably a typo in the original plugin. `pi/2` is 1.5707..., + // and this one has an additional 5 in there. + skew.signum() * bridge_rectifier * params.aura * input * ALMOST_FRAC_PI_2 + }; + self.last_sample = input; + + // AW: WE MAKE LOUD NOISE! RAWWWK! + let mut remaining_distortion_stages = if params.drive > 1.0 { + params.drive * params.drive + } else { + params.drive + }; + + // AW: crank up the gain on this so we can make it sing + let mut output = input; + while remaining_distortion_stages > 0.0 { + // AW: full crank stages followed by the proportional one whee. 1 at full warmth to + // 1.5570etc at no warmth + let drive = if remaining_distortion_stages > 1.0 { + ALMOST_FRAC_PI_2 + } else { + remaining_distortion_stages * (1.0 + ((ALMOST_FRAC_PI_2 - 1.0) * inverse_warmth)) + }; + + // AW: set up things so we can do repeated iterations, assuming that wet is always going + // to be 0-1 as in the previous plug. + let bridge_rectifier = (output.abs() + skew).min(FRAC_PI_2).sin(); + // AW: the distortion section. + let bridge_rectifier = bridge_rectifier.mul_add(drive, skew).min(FRAC_PI_2).sin(); + output = if output > 0.0 { + let positive = drive - scaled_warmth; + (output * (1.0 - positive + skew)) + (bridge_rectifier * (positive + skew)) + } else { + let negative = drive + scaled_warmth; + (output * (1.0 - negative + skew)) - (bridge_rectifier * (negative + skew)) + }; + + remaining_distortion_stages -= 1.0; + } + + output + } } diff --git a/plugins/aw_soft_vacuum/src/lib.rs b/plugins/aw_soft_vacuum/src/lib.rs index 1b3a79b9..3698d24f 100644 --- a/plugins/aw_soft_vacuum/src/lib.rs +++ b/plugins/aw_soft_vacuum/src/lib.rs @@ -103,6 +103,25 @@ impl Plugin for SoftVacuum { _aux: &mut AuxiliaryBuffers, _context: &mut impl ProcessContext, ) -> ProcessStatus { + // TODO: Parameters + // TODO: Dry/wet mixing and output scaling + // TODO: Oversampling + for channel_samples in buffer.iter_samples() { + for (sample, hard_vacuum) in channel_samples + .into_iter() + .zip(self.hard_vacuum_processors.iter_mut()) + { + *sample = hard_vacuum.process( + *sample, + &hard_vacuum::Params { + drive: 2.0, + warmth: 0.0, + aura: 0.879_645_94, + }, + ); + } + } + ProcessStatus::Normal } }