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.
This commit is contained in:
parent
6a368c1ac6
commit
1711efa11e
|
@ -40,6 +40,10 @@ pub struct Params {
|
||||||
pub warmth: f32,
|
pub warmth: f32,
|
||||||
/// The 'aura' parameter, should be in the range `[0, pi]`.
|
/// The 'aura' parameter, should be in the range `[0, pi]`.
|
||||||
pub aura: f32,
|
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 {
|
impl HardVacuum {
|
||||||
|
@ -68,8 +72,10 @@ impl HardVacuum {
|
||||||
|
|
||||||
// AW: We're doing all this here so skew isn't incremented by each stage
|
// AW: We're doing all this here so skew isn't incremented by each stage
|
||||||
let skew = {
|
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
|
// AW: skew will be direction/angle
|
||||||
let skew = input - self.last_sample;
|
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
|
// 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();
|
let bridge_rectifier = skew.abs().min(PI).sin();
|
||||||
|
|
||||||
|
|
|
@ -22,12 +22,25 @@ use nih_plug::prelude::*;
|
||||||
mod hard_vacuum;
|
mod hard_vacuum;
|
||||||
mod oversampling;
|
mod oversampling;
|
||||||
|
|
||||||
|
/// The maximum number of samples to process at a time. Used to create scratch buffers for the
|
||||||
|
/// oversampling.
|
||||||
|
const MAX_BLOCK_SIZE: usize = 32;
|
||||||
|
|
||||||
|
/// The 2-logarithm of the oversampling amount to use. 4x oversampling corresponds to factor 2.
|
||||||
|
// FIXME: Set this back to 2
|
||||||
|
const OVERSAMPLING_FACTOR: usize = 2;
|
||||||
|
const OVERSAMPLING_TIMES: usize = 2usize.pow(OVERSAMPLING_FACTOR as u32);
|
||||||
|
|
||||||
|
const MAX_OVERSAMPLED_BLOCK_SIZE: usize = MAX_BLOCK_SIZE * OVERSAMPLING_TIMES;
|
||||||
|
|
||||||
struct SoftVacuum {
|
struct SoftVacuum {
|
||||||
params: Arc<SoftVacuumParams>,
|
params: Arc<SoftVacuumParams>,
|
||||||
|
|
||||||
/// Stores implementations of the Hard Vacuum algorithm for each channel, since each channel
|
/// Stores implementations of the Hard Vacuum algorithm for each channel, since each channel
|
||||||
/// needs to maintain its own state.
|
/// needs to maintain its own state.
|
||||||
hard_vacuum_processors: Vec<hard_vacuum::HardVacuum>,
|
hard_vacuum_processors: Vec<hard_vacuum::HardVacuum>,
|
||||||
|
/// Oversampling for each channel.
|
||||||
|
oversamplers: Vec<oversampling::Lanczos3Oversampler>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// The parameters are the same as in the original plugin, except that they have different value
|
// The parameters are the same as in the original plugin, except that they have different value
|
||||||
|
@ -60,17 +73,26 @@ impl Default for SoftVacuumParams {
|
||||||
// Goes up to 200%, with the second half being nonlinear
|
// Goes up to 200%, with the second half being nonlinear
|
||||||
drive: FloatParam::new("Drive", 0.0, FloatRange::Linear { min: 0.0, max: 2.0 })
|
drive: FloatParam::new("Drive", 0.0, FloatRange::Linear { min: 0.0, max: 2.0 })
|
||||||
.with_unit("%")
|
.with_unit("%")
|
||||||
.with_smoother(SmoothingStyle::Linear(20.0))
|
.with_smoother(
|
||||||
|
SmoothingStyle::Linear(20.0)
|
||||||
|
.for_oversampling_factor(OVERSAMPLING_FACTOR as f32),
|
||||||
|
)
|
||||||
.with_value_to_string(formatters::v2s_f32_percentage(0))
|
.with_value_to_string(formatters::v2s_f32_percentage(0))
|
||||||
.with_string_to_value(formatters::s2v_f32_percentage()),
|
.with_string_to_value(formatters::s2v_f32_percentage()),
|
||||||
warmth: FloatParam::new("Warmth", 0.0, FloatRange::Linear { min: 0.0, max: 1.0 })
|
warmth: FloatParam::new("Warmth", 0.0, FloatRange::Linear { min: 0.0, max: 1.0 })
|
||||||
.with_unit("%")
|
.with_unit("%")
|
||||||
.with_smoother(SmoothingStyle::Linear(10.0))
|
.with_smoother(
|
||||||
|
SmoothingStyle::Linear(10.0)
|
||||||
|
.for_oversampling_factor(OVERSAMPLING_FACTOR as f32),
|
||||||
|
)
|
||||||
.with_value_to_string(formatters::v2s_f32_percentage(0))
|
.with_value_to_string(formatters::v2s_f32_percentage(0))
|
||||||
.with_string_to_value(formatters::s2v_f32_percentage()),
|
.with_string_to_value(formatters::s2v_f32_percentage()),
|
||||||
aura: FloatParam::new("Aura", 0.0, FloatRange::Linear { min: 0.0, max: PI })
|
aura: FloatParam::new("Aura", 0.0, FloatRange::Linear { min: 0.0, max: PI })
|
||||||
.with_unit("%")
|
.with_unit("%")
|
||||||
.with_smoother(SmoothingStyle::Linear(10.0))
|
.with_smoother(
|
||||||
|
SmoothingStyle::Linear(10.0)
|
||||||
|
.for_oversampling_factor(OVERSAMPLING_FACTOR as f32),
|
||||||
|
)
|
||||||
// We're displaying the value as a percentage even though it goes from `[0, pi]`
|
// We're displaying the value as a percentage even though it goes from `[0, pi]`
|
||||||
.with_value_to_string({
|
.with_value_to_string({
|
||||||
let formatter = formatters::v2s_f32_percentage(0);
|
let formatter = formatters::v2s_f32_percentage(0);
|
||||||
|
@ -93,12 +115,18 @@ impl Default for SoftVacuumParams {
|
||||||
)
|
)
|
||||||
.with_unit(" dB")
|
.with_unit(" dB")
|
||||||
// The value does not go down to 0 so we can do logarithmic here
|
// The value does not go down to 0 so we can do logarithmic here
|
||||||
.with_smoother(SmoothingStyle::Logarithmic(10.0))
|
.with_smoother(
|
||||||
|
SmoothingStyle::Logarithmic(10.0)
|
||||||
|
.for_oversampling_factor(OVERSAMPLING_FACTOR as f32),
|
||||||
|
)
|
||||||
.with_value_to_string(formatters::v2s_f32_gain_to_db(2))
|
.with_value_to_string(formatters::v2s_f32_gain_to_db(2))
|
||||||
.with_string_to_value(formatters::s2v_f32_gain_to_db()),
|
.with_string_to_value(formatters::s2v_f32_gain_to_db()),
|
||||||
dry_wet_ratio: FloatParam::new("Mix", 1.0, FloatRange::Linear { min: 0.0, max: 1.0 })
|
dry_wet_ratio: FloatParam::new("Mix", 1.0, FloatRange::Linear { min: 0.0, max: 1.0 })
|
||||||
.with_unit("%")
|
.with_unit("%")
|
||||||
.with_smoother(SmoothingStyle::Linear(10.0))
|
.with_smoother(
|
||||||
|
SmoothingStyle::Linear(10.0)
|
||||||
|
.for_oversampling_factor(OVERSAMPLING_FACTOR as f32),
|
||||||
|
)
|
||||||
.with_value_to_string(formatters::v2s_f32_percentage(0))
|
.with_value_to_string(formatters::v2s_f32_percentage(0))
|
||||||
.with_string_to_value(formatters::s2v_f32_percentage()),
|
.with_string_to_value(formatters::s2v_f32_percentage()),
|
||||||
}
|
}
|
||||||
|
@ -111,6 +139,7 @@ impl Default for SoftVacuum {
|
||||||
params: Arc::new(SoftVacuumParams::default()),
|
params: Arc::new(SoftVacuumParams::default()),
|
||||||
|
|
||||||
hard_vacuum_processors: Vec::new(),
|
hard_vacuum_processors: Vec::new(),
|
||||||
|
oversamplers: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -147,14 +176,24 @@ impl Plugin for SoftVacuum {
|
||||||
&mut self,
|
&mut self,
|
||||||
audio_io_layout: &AudioIOLayout,
|
audio_io_layout: &AudioIOLayout,
|
||||||
_buffer_config: &BufferConfig,
|
_buffer_config: &BufferConfig,
|
||||||
_context: &mut impl InitContext<Self>,
|
context: &mut impl InitContext<Self>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let num_channels = audio_io_layout
|
let num_channels = audio_io_layout
|
||||||
.main_output_channels
|
.main_output_channels
|
||||||
.expect("Plugin was initialized without any outputs")
|
.expect("Plugin was initialized without any outputs")
|
||||||
.get() as usize;
|
.get() as usize;
|
||||||
|
|
||||||
self.hard_vacuum_processors
|
self.hard_vacuum_processors
|
||||||
.resize_with(num_channels, hard_vacuum::HardVacuum::default);
|
.resize_with(num_channels, hard_vacuum::HardVacuum::default);
|
||||||
|
// If the number of stages ever becomes configurable, then this needs to also change the
|
||||||
|
// existinginstances
|
||||||
|
self.oversamplers.resize_with(num_channels, || {
|
||||||
|
oversampling::Lanczos3Oversampler::new(MAX_BLOCK_SIZE, OVERSAMPLING_FACTOR)
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(oversampler) = self.oversamplers.first() {
|
||||||
|
context.set_latency_samples(oversampler.latency() as u32);
|
||||||
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
@ -163,6 +202,10 @@ impl Plugin for SoftVacuum {
|
||||||
for hard_vacuum in &mut self.hard_vacuum_processors {
|
for hard_vacuum in &mut self.hard_vacuum_processors {
|
||||||
hard_vacuum.reset();
|
hard_vacuum.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for oversampler in &mut self.oversamplers {
|
||||||
|
oversampler.reset();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process(
|
fn process(
|
||||||
|
@ -171,25 +214,79 @@ 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: When the oversampling amount becomes dynamic, then we should update the latency here:
|
||||||
// TODO: Dry/wet mixing and output scaling
|
// context.set_latency_samples(self.oversampler.latency() as u32);
|
||||||
// TODO: Oversampling
|
|
||||||
for channel_samples in buffer.iter_samples() {
|
// The Hard Vacuum algorithm makes use of slews, and the aura control amplifies this part.
|
||||||
let output_gain = self.params.output_gain.smoothed.next();
|
// The oversampling rounds out the waveform and reduces those slews. This is a rough
|
||||||
let dry_wet_ratio = self.params.dry_wet_ratio.smoothed.next();
|
// compensation to get the distortion to sound like it normally would. The alternative would
|
||||||
let hard_vacuum_params = hard_vacuum::Params {
|
// be to upsample the slews independently.
|
||||||
drive: self.params.drive.smoothed.next(),
|
// FIXME: Maybe just upsample the slew signal instead, that should be more accurate
|
||||||
warmth: self.params.warmth.smoothed.next(),
|
let slew_oversampling_compensation_factor = (OVERSAMPLING_TIMES - 1) as f32 * 0.7;
|
||||||
aura: self.params.aura.smoothed.next(),
|
|
||||||
};
|
for (_, block) in buffer.iter_blocks(MAX_BLOCK_SIZE) {
|
||||||
|
let block_len = block.samples();
|
||||||
|
let upsampled_block_len = block_len * OVERSAMPLING_TIMES;
|
||||||
|
|
||||||
|
// These are the parameters for the distortion algorithm
|
||||||
|
// TODO: When the oversampling amount becomes dynamic, then the block size here needs to
|
||||||
|
// change depending on the oversampling amount
|
||||||
|
let mut drive = [0.0; MAX_OVERSAMPLED_BLOCK_SIZE];
|
||||||
|
self.params
|
||||||
|
.drive
|
||||||
|
.smoothed
|
||||||
|
.next_block(&mut drive, upsampled_block_len);
|
||||||
|
let mut warmth = [0.0; MAX_OVERSAMPLED_BLOCK_SIZE];
|
||||||
|
self.params
|
||||||
|
.warmth
|
||||||
|
.smoothed
|
||||||
|
.next_block(&mut warmth, upsampled_block_len);
|
||||||
|
let mut aura = [0.0; MAX_OVERSAMPLED_BLOCK_SIZE];
|
||||||
|
self.params
|
||||||
|
.aura
|
||||||
|
.smoothed
|
||||||
|
.next_block(&mut aura, upsampled_block_len);
|
||||||
|
|
||||||
|
// And the general output mixing
|
||||||
|
let mut output_gain = [0.0; MAX_OVERSAMPLED_BLOCK_SIZE];
|
||||||
|
self.params
|
||||||
|
.output_gain
|
||||||
|
.smoothed
|
||||||
|
.next_block(&mut output_gain, upsampled_block_len);
|
||||||
|
let mut dry_wet_ratio = [0.0; MAX_OVERSAMPLED_BLOCK_SIZE];
|
||||||
|
self.params
|
||||||
|
.dry_wet_ratio
|
||||||
|
.smoothed
|
||||||
|
.next_block(&mut dry_wet_ratio, upsampled_block_len);
|
||||||
|
|
||||||
|
for (block_channel, (oversampler, hard_vacuum)) in block.into_iter().zip(
|
||||||
|
self.oversamplers
|
||||||
|
.iter_mut()
|
||||||
|
.zip(self.hard_vacuum_processors.iter_mut()),
|
||||||
|
) {
|
||||||
|
oversampler.process(block_channel, |upsampled| {
|
||||||
|
assert!(upsampled.len() == upsampled_block_len);
|
||||||
|
|
||||||
|
for (sample_idx, sample) in upsampled.iter_mut().enumerate() {
|
||||||
|
// SAFETY: We already made sure that the blocks are equal in size. We could
|
||||||
|
// zip iterators instead but with six iterators that's already a bit
|
||||||
|
// too much without a first class way to zip more than two iterators
|
||||||
|
// together into a single tuple of iterators.
|
||||||
|
let hard_vacuum_params = hard_vacuum::Params {
|
||||||
|
drive: unsafe { *drive.get_unchecked(sample_idx) },
|
||||||
|
warmth: unsafe { *warmth.get_unchecked(sample_idx) },
|
||||||
|
aura: unsafe { *aura.get_unchecked(sample_idx) },
|
||||||
|
|
||||||
|
slew_compensation_factor: slew_oversampling_compensation_factor,
|
||||||
|
};
|
||||||
|
let output_gain = unsafe { *output_gain.get_unchecked(sample_idx) };
|
||||||
|
let dry_wet_ratio = unsafe { *dry_wet_ratio.get_unchecked(sample_idx) };
|
||||||
|
|
||||||
for (sample, hard_vacuum) in channel_samples
|
|
||||||
.into_iter()
|
|
||||||
.zip(self.hard_vacuum_processors.iter_mut())
|
|
||||||
{
|
|
||||||
let distorted = hard_vacuum.process(*sample, &hard_vacuum_params);
|
let distorted = hard_vacuum.process(*sample, &hard_vacuum_params);
|
||||||
*sample =
|
*sample = (distorted * output_gain * dry_wet_ratio)
|
||||||
(distorted * output_gain * dry_wet_ratio) + (*sample * (1.0 - dry_wet_ratio));
|
+ (*sample * (1.0 - dry_wet_ratio));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue