diff --git a/plugins/aw_soft_vacuum/src/lib.rs b/plugins/aw_soft_vacuum/src/lib.rs index 98ec6365..0778a74b 100644 --- a/plugins/aw_soft_vacuum/src/lib.rs +++ b/plugins/aw_soft_vacuum/src/lib.rs @@ -15,6 +15,7 @@ // along with this program. If not, see . use std::f32::consts::PI; +use std::sync::atomic::Ordering; use std::sync::Arc; use nih_plug::prelude::*; @@ -26,12 +27,14 @@ mod oversampling; /// 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); +/// The 2-logarithm of the maximum oversampling amount to use. 16x oversampling corresponds to factor +/// 4. +const MAX_OVERSAMPLING_FACTOR: usize = 4; +const MAX_OVERSAMPLING_TIMES: usize = oversampling_factor_to_times(MAX_OVERSAMPLING_FACTOR); +const MAX_OVERSAMPLED_BLOCK_SIZE: usize = MAX_BLOCK_SIZE * MAX_OVERSAMPLING_TIMES; -const MAX_OVERSAMPLED_BLOCK_SIZE: usize = MAX_BLOCK_SIZE * OVERSAMPLING_TIMES; +/// This corresponds to 4x oversampling. +const DEFAULT_OVERSAMPLING_FACTOR: usize = 2; struct SoftVacuum { params: Arc, @@ -65,34 +68,45 @@ struct SoftVacuumParams { /// A linear dry/wet mix parameter. #[id = "dry_wet_ratio"] pub dry_wet_ratio: FloatParam, + + /// The current oversampling factor. This is the 2-logarithm of the oversampling amount. 0 + /// corresponds to 1x/no oversampling, 1 to 2x oversampling, 2 to 4x, etc.. + #[id = "oversampling_factor"] + pub oversampling_factor: IntParam, } impl Default for SoftVacuumParams { fn default() -> Self { + // This is set by the `oversampling_factor` parameter and is used by the smoothers of the + // other parmaeters so the oversampling amount always stays in sync + let oversampling_times = Arc::new(AtomicF32::new(oversampling_factor_to_times( + DEFAULT_OVERSAMPLING_FACTOR, + ) as f32)); + Self { // Goes up to 200%, with the second half being nonlinear drive: FloatParam::new("Drive", 0.0, FloatRange::Linear { min: 0.0, max: 2.0 }) .with_unit("%") - .with_smoother( - SmoothingStyle::Linear(20.0) - .for_oversampling_factor(OVERSAMPLING_FACTOR as f32), - ) + .with_smoother(SmoothingStyle::OversamplingAware( + oversampling_times.clone(), + &SmoothingStyle::Linear(20.0), + )) .with_value_to_string(formatters::v2s_f32_percentage(0)) .with_string_to_value(formatters::s2v_f32_percentage()), warmth: FloatParam::new("Warmth", 0.0, FloatRange::Linear { min: 0.0, max: 1.0 }) .with_unit("%") - .with_smoother( - SmoothingStyle::Linear(10.0) - .for_oversampling_factor(OVERSAMPLING_FACTOR as f32), - ) + .with_smoother(SmoothingStyle::OversamplingAware( + oversampling_times.clone(), + &SmoothingStyle::Linear(10.0), + )) .with_value_to_string(formatters::v2s_f32_percentage(0)) .with_string_to_value(formatters::s2v_f32_percentage()), aura: FloatParam::new("Aura", 0.0, FloatRange::Linear { min: 0.0, max: PI }) .with_unit("%") - .with_smoother( - SmoothingStyle::Linear(10.0) - .for_oversampling_factor(OVERSAMPLING_FACTOR as f32), - ) + .with_smoother(SmoothingStyle::OversamplingAware( + oversampling_times.clone(), + &SmoothingStyle::Linear(10.0), + )) // We're displaying the value as a percentage even though it goes from `[0, pi]` .with_value_to_string({ let formatter = formatters::v2s_f32_percentage(0); @@ -115,20 +129,47 @@ impl Default for SoftVacuumParams { ) .with_unit(" dB") // The value does not go down to 0 so we can do logarithmic here - .with_smoother( - SmoothingStyle::Logarithmic(10.0) - .for_oversampling_factor(OVERSAMPLING_FACTOR as f32), - ) + .with_smoother(SmoothingStyle::OversamplingAware( + oversampling_times.clone(), + &SmoothingStyle::Logarithmic(10.0), + )) .with_value_to_string(formatters::v2s_f32_gain_to_db(2)) .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 }) .with_unit("%") - .with_smoother( - SmoothingStyle::Linear(10.0) - .for_oversampling_factor(OVERSAMPLING_FACTOR as f32), - ) + .with_smoother(SmoothingStyle::OversamplingAware( + oversampling_times.clone(), + &SmoothingStyle::Linear(10.0), + )) .with_value_to_string(formatters::v2s_f32_percentage(0)) .with_string_to_value(formatters::s2v_f32_percentage()), + + oversampling_factor: IntParam::new( + "Oversampling", + DEFAULT_OVERSAMPLING_FACTOR as i32, + IntRange::Linear { + min: 0, + max: MAX_OVERSAMPLING_FACTOR as i32, + }, + ) + .with_unit("x") + .with_callback(Arc::new(move |new_factor| { + oversampling_times.store( + oversampling_factor_to_times(new_factor as usize) as f32, + Ordering::Relaxed, + ); + })) + .with_value_to_string(Arc::new(|value| { + // NIH-plug prevents `value` from being out of range and thus negative + let oversampling_times = oversampling_factor_to_times(value as usize); + + oversampling_times.to_string() + })) + .with_string_to_value(Arc::new(|string| { + let oversampling_times: usize = string.parse().ok()?; + + Some(oversampling_times_to_factor(oversampling_times) as i32) + })), } } } @@ -185,14 +226,14 @@ impl Plugin for SoftVacuum { self.hard_vacuum_processors .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) + oversampling::Lanczos3Oversampler::new(MAX_BLOCK_SIZE, MAX_OVERSAMPLING_FACTOR) }); if let Some(oversampler) = self.oversamplers.first() { - context.set_latency_samples(oversampler.latency(OVERSAMPLING_FACTOR)); + context.set_latency_samples( + oversampler.latency(self.params.oversampling_factor.value() as usize), + ); } true @@ -212,25 +253,29 @@ impl Plugin for SoftVacuum { &mut self, buffer: &mut Buffer, _aux: &mut AuxiliaryBuffers, - _context: &mut impl ProcessContext, + context: &mut impl ProcessContext, ) -> ProcessStatus { - // TODO: When the oversampling amount becomes dynamic, then we should update the latency here: - // context.set_latency_samples(self.oversampler.latency() as u32); + let oversampling_factor = self.params.oversampling_factor.value() as usize; + let oversampling_times = oversampling_factor_to_times(oversampling_factor); + + // If the oversampling factor parameter is changed then the host needs to know about the new + // latency + if let Some(oversampler) = self.oversamplers.first() { + context.set_latency_samples(oversampler.latency(oversampling_factor)); + } // The Hard Vacuum algorithm makes use of slews, and the aura control amplifies this part. // The oversampling rounds out the waveform and reduces those slews. This is a rough // compensation to get the distortion to sound like it normally would. The alternative would // be to upsample the slews independently. // FIXME: Maybe just upsample the slew signal instead, that should be more accurate - let slew_oversampling_compensation_factor = (OVERSAMPLING_TIMES - 1) as f32 * 0.7; + let slew_oversampling_compensation_factor = (oversampling_times - 1) as f32 * 0.7; for (_, block) in buffer.iter_blocks(MAX_BLOCK_SIZE) { let block_len = block.samples(); - let upsampled_block_len = block_len * OVERSAMPLING_TIMES; + 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 @@ -264,7 +309,7 @@ impl Plugin for SoftVacuum { .iter_mut() .zip(self.hard_vacuum_processors.iter_mut()), ) { - oversampler.process(block_channel, OVERSAMPLING_FACTOR, |upsampled| { + oversampler.process(block_channel, oversampling_factor, |upsampled| { assert!(upsampled.len() == upsampled_block_len); for (sample_idx, sample) in upsampled.iter_mut().enumerate() { @@ -294,6 +339,15 @@ impl Plugin for SoftVacuum { } } +// Used in the conversion for the oversampling amount parameter +const fn oversampling_factor_to_times(factor: usize) -> usize { + 2usize.pow(factor as u32) +} + +const fn oversampling_times_to_factor(times: usize) -> usize { + times.ilog2() as usize +} + impl ClapPlugin for SoftVacuum { const CLAP_ID: &'static str = "nl.robbertvanderhelm.soft-vacuum"; const CLAP_DESCRIPTION: Option<&'static str> =