// Soft Vacuum: Airwindows Hard Vacuum port with oversampling // Copyright (C) 2023 Robbert van der Helm // Copyright (c) 2018 Chris Johnson // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // 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)] 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, /// A factor slews should be multiplied with. Set above 1.0 when oversampling to compensate for /// the waveform becoming smoother. pub slew_compensation_factor: 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 /// deterministic. 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 = { // NOTE: The `slew_compensation_factor` is an addition to make the algorithm behave more // consistently when oversampling // AW: skew will be direction/angle let skew = (input - self.last_sample) * params.slew_compensation_factor; // 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 } }