Port the Hard Vacuum algorithm
This commit is contained in:
parent
1abcb02647
commit
456a22119e
|
@ -15,6 +15,14 @@
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// 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
|
/// Single-channel port of the Hard Vacuum algorithm from
|
||||||
/// <https://github.com/airwindows/airwindows/blob/283343b9e90c28fdb583f27e198f882f268b051b/plugins/LinuxVST/src/HardVacuum/HardVacuumProc.cpp>.
|
/// <https://github.com/airwindows/airwindows/blob/283343b9e90c28fdb583f27e198f882f268b051b/plugins/LinuxVST/src/HardVacuum/HardVacuumProc.cpp>.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
|
@ -22,6 +30,18 @@ pub struct HardVacuum {
|
||||||
last_sample: f32,
|
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 {
|
impl HardVacuum {
|
||||||
/// Reset the processor's state. In this case this only resets the discrete derivative
|
/// 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
|
/// 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) {
|
pub fn reset(&mut self) {
|
||||||
self.last_sample = 0.0;
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,6 +103,25 @@ impl Plugin for SoftVacuum {
|
||||||
_aux: &mut AuxiliaryBuffers,
|
_aux: &mut AuxiliaryBuffers,
|
||||||
_context: &mut impl ProcessContext<Self>,
|
_context: &mut impl ProcessContext<Self>,
|
||||||
) -> ProcessStatus {
|
) -> 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
|
ProcessStatus::Normal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue