1
0
Fork 0

Port the Hard Vacuum algorithm

This commit is contained in:
Robbert van der Helm 2023-04-02 16:40:05 +02:00
parent 1abcb02647
commit 456a22119e
2 changed files with 107 additions and 0 deletions

View file

@ -15,6 +15,14 @@
// 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)]
@ -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(&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 = {
// 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
}
}

View file

@ -103,6 +103,25 @@ impl Plugin for SoftVacuum {
_aux: &mut AuxiliaryBuffers,
_context: &mut impl ProcessContext<Self>,
) -> 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
}
}