From 2f803915eb0a69c61f9dfa5a6793e5fd94e0d90f Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Tue, 15 Feb 2022 18:30:45 +0100 Subject: [PATCH] Allow compiling Diopser without SIMD --- .github/workflows/test.yml | 4 +- README.md | 2 +- plugins/diopser/Cargo.toml | 7 ++- plugins/diopser/README.md | 16 ++++++ plugins/diopser/src/filter.rs | 5 +- plugins/diopser/src/lib.rs | 98 ++++++++++++++++++++++++++--------- 6 files changed, 103 insertions(+), 29 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7413694e..90999b63 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,7 +51,9 @@ jobs: - name: Set up Rust toolchain uses: actions-rs/toolchain@v1 with: - # FIXME: Needed for SIMD + # FIXME: Needed for SIMD. Diopser can be compiled without SIMD support + # though, we'd actually need to test whether both versions + # compile toolchain: nightly profile: minimal default: true diff --git a/README.md b/README.md index 7ec832c8..9f09a8c1 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ great but it has only been tested under Wine with NIH-plug works with the latest stable Rust compiler. -After installing [Rust](https://rustup.rs/) you can compile any of the plugins +After installing [Rust](https://rustup.rs/), you can compile any of the plugins in the `plugins` directory in the following way, replacing `gain` with the name of the plugin: diff --git a/plugins/diopser/Cargo.toml b/plugins/diopser/Cargo.toml index 3dfd78ea..1fdbc0e4 100644 --- a/plugins/diopser/Cargo.toml +++ b/plugins/diopser/Cargo.toml @@ -8,7 +8,12 @@ license = "GPL-3.0-or-later" [lib] crate-type = ["cdylib"] +[features] +default = ["simd"] +# Make it go fast, vroom +simd = ["packed_simd"] + [dependencies] nih_plug = { path = "../../", features = ["assert_process_allocs"] } -packed_simd = { version = "0.3.6", package = "packed_simd_2" } +packed_simd = { version = "0.3.6", package = "packed_simd_2", optional = true } diff --git a/plugins/diopser/README.md b/plugins/diopser/README.md index fa9280c4..12ff6721 100644 --- a/plugins/diopser/README.md +++ b/plugins/diopser/README.md @@ -19,6 +19,22 @@ and much better performance. Disperser or Kilohearts AB. +### Building + +After installing [Rust](https://rustup.rs/) you can compile Diopser as follows + +```shell +cargo xtask bundle gain --release --bundle-vst3 +``` + +If you don't have access to a nightly compiler (`rustup default nightly && rustup update`), +then you can compile a version without SIMD at a 2x penalty by disabling the +SIMD feature: + +```shell +cargo xtask bundle gain --release --bundle-vst3 --no-default-features +``` + ## Download There are currently no prebuilt downloads available. Check this repository's diff --git a/plugins/diopser/src/filter.rs b/plugins/diopser/src/filter.rs index 173216bf..97b88451 100644 --- a/plugins/diopser/src/filter.rs +++ b/plugins/diopser/src/filter.rs @@ -14,10 +14,12 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use packed_simd::f32x2; use std::f32::consts; use std::ops::{Add, Mul, Sub}; +#[cfg(feature = "simd")] +use packed_simd::f32x2; + /// A simple biquad filter with functions for generating coefficients for an all-pass filter. /// /// Based on . @@ -127,6 +129,7 @@ impl SimdType for f32 { } } +#[cfg(feature = "simd")] impl SimdType for f32x2 { fn from_f32(value: f32) -> Self { f32x2::splat(value) diff --git a/plugins/diopser/src/lib.rs b/plugins/diopser/src/lib.rs index 9cf52af4..b0b3948b 100644 --- a/plugins/diopser/src/lib.rs +++ b/plugins/diopser/src/lib.rs @@ -22,11 +22,13 @@ use nih_plug::{ }; use nih_plug::{BoolParam, FloatParam, IntParam, Params, Range, SmoothingStyle}; use nih_plug::{Enum, EnumParam}; -use packed_simd::f32x2; use std::pin::Pin; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +#[cfg(feature = "simd")] +use packed_simd::f32x2; + mod filter; /// How many all-pass filters we can have in series at most. The filter stages parameter determines @@ -44,6 +46,9 @@ const MAX_AUTOMATION_STEP_SIZE: u32 = 512; // - A GUI // - A panic switch (maybe also as a trigger-like parameter) to reset all filter states may also be // useful +// +// TODO: Decide on whether to keep the scalar version or to just only support SIMD. Issue is that +// packed_simd requires a nightly compiler. struct Diopser { params: Pin>, @@ -51,11 +56,13 @@ struct Diopser { sample_rate: f32, /// All of the all-pass filters, with vectorized coefficients so they can be calculated for - /// multiple channels at once. [DiopserParams::num_stages] controls how many filters are + /// multiple channels at once. [DiopserParams::num_stages] controls how many filters are /// actually active. - // FIXME: This was the scalar version, maybe add this back at some point. - // filters: Vec<[filter::Biquad; MAX_NUM_FILTERS]>, + #[cfg(feature = "simd")] filters: [filter::Biquad; MAX_NUM_FILTERS], + #[cfg(not(feature = "simd"))] + 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. @@ -115,7 +122,11 @@ impl Default for Diopser { sample_rate: 1.0, + #[cfg(feature = "simd")] filters: [filter::Biquad::default(); MAX_NUM_FILTERS], + #[cfg(not(feature = "simd"))] + filters: Vec::new(), + should_update_filters, next_filter_smoothing_in: 1, } @@ -220,9 +231,17 @@ impl Plugin for Diopser { } fn accepts_bus_config(&self, config: &BusConfig) -> bool { - // FIXME: The scalar version would work for any IO layout, but this SIMD version can only do - // stereo - config.num_input_channels == config.num_output_channels && config.num_input_channels == 2 + // The scalar version can handle any channel config, while the SIMD version can only do + // stereo + #[cfg(feature = "simd")] + { + config.num_input_channels == config.num_output_channels + && config.num_input_channels == 2 + } + #[cfg(not(feature = "simd"))] + { + config.num_input_channels == config.num_output_channels && config.num_input_channels > 0 + } } fn initialize( @@ -231,6 +250,14 @@ impl Plugin for Diopser { buffer_config: &BufferConfig, _context: &mut impl ProcessContext, ) -> bool { + #[cfg(not(feature = "simd"))] + { + 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); @@ -252,25 +279,38 @@ impl Plugin for Diopser { for mut channel_samples in buffer.iter_mut() { self.maybe_update_filters(smoothing_interval); - // We can compute the filters for both channels at once. This version thus now only - // supports stero audio. - let mut samples = - f32x2::new(*unsafe { channel_samples.get_unchecked_mut(0) }, *unsafe { - channel_samples.get_unchecked_mut(1) - }); - - // Iterating over the filters and then over the channels would get us better cache - // locality even in the scalar version - for filter in self - .filters - .iter_mut() - .take(self.params.filter_stages.value as usize) + // We can compute the filters for both channels at once. The SIMD version thus now only + // supports steroo audio. + #[cfg(feature = "simd")] { - samples = filter.process(samples); + let mut samples = + f32x2::new(*unsafe { channel_samples.get_unchecked_mut(0) }, *unsafe { + channel_samples.get_unchecked_mut(1) + }); + + for filter in self + .filters + .iter_mut() + .take(self.params.filter_stages.value as usize) + { + samples = filter.process(samples); + } + + *unsafe { channel_samples.get_unchecked_mut(0) } = samples.extract(0); + *unsafe { channel_samples.get_unchecked_mut(1) } = samples.extract(1); } - *unsafe { channel_samples.get_unchecked_mut(0) } = samples.extract(0); - *unsafe { channel_samples.get_unchecked_mut(1) } = samples.extract(1); + #[cfg(not(feature = "simd"))] + // 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 { + for (channel_idx, filters) in self.filters.iter_mut().enumerate() { + // We can also use `channel_samples.iter_mut()`, but the compiler isn't able to + // optmize that iterator away and it would add a ton of overhead over indexing + // the buffer directly + let sample = unsafe { channel_samples.get_unchecked_mut(channel_idx) }; + *sample = filters[filter_idx].process(*sample); + } + } } ProcessStatus::Normal @@ -336,10 +376,18 @@ impl Diopser { } .clamp(MIN_FREQUENCY, max_frequency); - // In the scalar version we'd update every channel's filter's coefficients gere let coefficients = filter::BiquadCoefficients::allpass(self.sample_rate, filter_frequency, resonance); - self.filters[filter_idx].coefficients = coefficients; + + #[cfg(feature = "simd")] + { + self.filters[filter_idx].coefficients = coefficients; + } + + #[cfg(not(feature = "simd"))] + for channel in self.filters.iter_mut() { + channel[filter_idx].coefficients = coefficients; + } } } }