diff --git a/README.md b/README.md index b1affa4a..cd124e62 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,8 @@ yourself. - [**Diopser**](plugins/diopser) is a totally original phase rotation plugin. Useful for oomphing up kickdrums and basses, transforming synths into their evil phase-y cousin, and making everything sound like a cheap Sci-Fi laser - beam. + beam. **This is an unfinished port of an existing plugin, right now it is not + in a usable state yet.** ## Framework diff --git a/plugins/diopser/README.md b/plugins/diopser/README.md index 5a72e9bd..abd0d213 100644 --- a/plugins/diopser/README.md +++ b/plugins/diopser/README.md @@ -1,5 +1,7 @@ # Diopser +**Right now it is not in a usable state yet.** + You were expecting Disperser[ยน](#disperser), but it was me, Diopser! Diopser lets you rotate the phase of a signal around a specific frequency diff --git a/plugins/diopser/src/filter.rs b/plugins/diopser/src/filter.rs index 92c8c583..6a44a12a 100644 --- a/plugins/diopser/src/filter.rs +++ b/plugins/diopser/src/filter.rs @@ -19,6 +19,7 @@ use std::f32::consts; /// A simple biquad filter with functions for generating coefficients for an all-pass filter. /// /// Based on . +#[derive(Clone, Copy, Debug)] pub struct Biquad { pub coefficients: BiquadCoefficients, s1: f32, @@ -27,6 +28,7 @@ pub struct Biquad { /// The coefficients `[b0, b1, b2, a1, a2]` for [Biquad]. These coefficients are all prenormalized, /// i.e. they have been divided by `a0`. +#[derive(Clone, Copy, Debug)] pub struct BiquadCoefficients { b0: f32, b1: f32, @@ -85,6 +87,7 @@ impl BiquadCoefficients { let b2 = (1.0 + alpha) / a0; let a1 = (-2.0 * cos_omega0) / a0; let a2 = (1.0 - alpha) / a0; + Self { b0, b1, b2, a1, a2 } } } diff --git a/plugins/diopser/src/lib.rs b/plugins/diopser/src/lib.rs index 10b6b375..2ba5a89f 100644 --- a/plugins/diopser/src/lib.rs +++ b/plugins/diopser/src/lib.rs @@ -17,37 +17,131 @@ #[macro_use] extern crate nih_plug; -use nih_plug::Params; +use nih_plug::{formatters, BoolParam, FloatParam, IntParam, Params, Range, SmoothingStyle}; use nih_plug::{ Buffer, BufferConfig, BusConfig, Plugin, ProcessContext, ProcessStatus, Vst3Plugin, }; use std::pin::Pin; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; mod filter; +/// How many all-pass filters we can have in series at most. The filter stages parameter determines +/// how many filters are actually active. +const MAX_NUM_FILTERS: usize = 512; + +// An incomplete list of unported features includes: +// - Actually add the parameters +// - Filter spread +// - Lower resolution smoothing for more efficient automation +// +// And after that I'll need to add a GUI struct Diopser { params: Pin>, + + /// Needed for computing the filter coefficients. + sample_rate: f32, + + /// All of the all-pass filters, with one array of serial filters per channelq. + /// [DiopserParams::num_stages] controls how many filters are actually active. + filters: Vec<[filter::Biquad; MAX_NUM_FILTERS]>, + /// If this is set at the start of the processing cycle, then the filter coefficients should be + /// updated. For the regular filter parameters we can look at the smoothers, but this is needed + /// when changing the number of active filters. + should_update_filters: Arc, } +// TODO: Some combinations of parameters can cause really loud resonance. We should limit the +// resonance and filter stages parameter ranges in the GUI until the user unlocks. #[derive(Params)] -struct DiopserParams {} +struct DiopserParams { + /// The number of all-pass filters applied in series. + #[id = "stages"] + filter_stages: IntParam, + + /// The filter's cutoff frequqency. When this is applied, the filters are spread around this + /// frequency. + #[id = "cutoff"] + filter_frequency: FloatParam, + /// The Q parameter for the filters. + #[id = "res"] + filter_resonance: FloatParam, + + /// Very important. + #[id = "ignore"] + very_important: BoolParam, +} impl Default for Diopser { fn default() -> Self { + let should_update_filters = Arc::new(AtomicBool::new(false)); + Self { - params: Box::pin(DiopserParams::default()), + params: Box::pin(DiopserParams::new(should_update_filters.clone())), + + sample_rate: 1.0, + + filters: Vec::new(), + should_update_filters, } } } -impl Default for DiopserParams { - fn default() -> Self { - Self {} +impl DiopserParams { + pub fn new(should_update_filters: Arc) -> Self { + let trigger_filter_update = + Arc::new(move |_| should_update_filters.store(true, Ordering::Release)); + + Self { + filter_stages: IntParam::new( + "Filter Stages", + 0, + Range::Linear { + min: 0, + max: MAX_NUM_FILTERS as i32, + }, + ) + .with_callback(trigger_filter_update), + + // Smoothed parameters don't need the callback as we can loop at whether the smoother is + // still smoothing + filter_frequency: FloatParam::new( + "Filter Frequency", + 200.0, + Range::Skewed { + min: 5.0, // This must never reach 0 + max: 20_000.0, + factor: Range::skew_factor(-2.5), + }, + ) + // This needs quite a bit of smoothing to avoid artifacts + .with_smoother(SmoothingStyle::Logarithmic(100.0)) + .with_unit(" Hz") + .with_value_to_string(formatters::f32_rounded(0)), + filter_resonance: FloatParam::new( + "Filter Resonance", + // The actual default neutral Q-value would be `sqrt(2) / 2`, but this value + // produces slightly less ringing. + 0.5, + Range::Skewed { + min: 0.01, // This must also never reach 0 + max: 30.0, + factor: Range::skew_factor(-2.5), + }, + ) + .with_smoother(SmoothingStyle::Logarithmic(100.0)) + .with_value_to_string(formatters::f32_rounded(2)), + + very_important: BoolParam::new("Don't touch this", true).with_value_to_string( + Arc::new(|value| String::from(if value { "please don't" } else { "stop it" })), + ), + } } } impl Plugin for Diopser { - const NAME: &'static str = "Diopser"; + const NAME: &'static str = "Diopser (WIP port)"; const VENDOR: &'static str = "Robbert van der Helm"; const URL: &'static str = "https://github.com/robbert-vdh/nih-plug"; const EMAIL: &'static str = "mail@robbertvanderhelm.nl"; @@ -68,22 +162,74 @@ impl Plugin for Diopser { fn initialize( &mut self, - _bus_config: &BusConfig, - _buffer_config: &BufferConfig, + bus_config: &BusConfig, + buffer_config: &BufferConfig, _context: &mut impl ProcessContext, ) -> bool { + self.filters = + vec![[Default::default(); MAX_NUM_FILTERS]; bus_config.num_input_channels as usize]; + + // Initialize the filters on the first process call + self.sample_rate = buffer_config.sample_rate; + self.should_update_filters.store(true, Ordering::Release); + true } fn process( &mut self, - _buffer: &mut Buffer, + buffer: &mut Buffer, _context: &mut impl ProcessContext, ) -> ProcessStatus { + for mut channel_samples in buffer.iter_mut() { + // Since this is an expensive operation, only update the filters when it's actually + // necessary + let should_update_filters = self + .should_update_filters + .compare_exchange(true, false, Ordering::Acquire, Ordering::Relaxed) + .is_ok() + || self.params.filter_frequency.smoothed.is_smoothing() + || self.params.filter_resonance.smoothed.is_smoothing(); + if should_update_filters { + self.update_filters(); + } + + // We get better cache locality by iterating over the filters and then over the channels + for filter_idx in 0..self.params.filter_stages.value as usize { + // Because of this filter_idx outer loop we can't directly iterate over + // `channel_samples` as the iterator would be empty after the first loop + for (channel_idx, filters) in + (0..channel_samples.len()).zip(self.filters.iter_mut()) + { + // SAFETY: The bounds have already been checked here + let sample = unsafe { channel_samples.get_unchecked_mut(channel_idx) }; + *sample = filters[filter_idx].process(*sample); + } + } + } + ProcessStatus::Normal } } +impl Diopser { + fn update_filters(&mut self) { + let coefficients = filter::BiquadCoefficients::allpass( + self.sample_rate, + self.params.filter_frequency.smoothed.next(), + self.params.filter_resonance.smoothed.next(), + ); + for channel in self.filters.iter_mut() { + for filter in channel + .iter_mut() + .take(self.params.filter_stages.value as usize) + { + filter.coefficients = coefficients; + } + } + } +} + impl Vst3Plugin for Diopser { const VST3_CLASS_ID: [u8; 16] = *b"DiopserPlugRvdH."; const VST3_CATEGORIES: &'static str = "Fx|Filter";