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
|
||||
// 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(¶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,
|
||||
_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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue