1
0
Fork 0
nih-plug/plugins/aw_soft_vacuum/src/hard_vacuum.rs
Robbert van der Helm 1711efa11e Add a basic 4x oversampled version of Hard Vacuum
The oversampling amount should be configurable, and it would work better
if the slew signal was oversampled independently instead of doing this
compensation thing.
2023-04-05 18:22:28 +02:00

127 lines
5.6 KiB
Rust

// 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 <https://www.gnu.org/licenses/>.
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
/// <https://github.com/airwindows/airwindows/blob/283343b9e90c28fdb583f27e198f882f268b051b/plugins/LinuxVST/src/HardVacuum/HardVacuumProc.cpp>.
#[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(&params.drive));
nih_debug_assert!((0.0..=1.0).contains(&params.warmth));
nih_debug_assert!((0.0..=PI).contains(&params.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
}
}