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
}
}