Use SIMD for Diopser
It's pretty damn fast now, especially compared to the JUCE version.
This commit is contained in:
parent
935d952d81
commit
d0f1a79279
|
@ -11,7 +11,8 @@ like a cartoon laser beam, or a psytrance kickdrum. If you are experimenting
|
||||||
with those kinds of settings, then you may want to consider temporarily placing
|
with those kinds of settings, then you may want to consider temporarily placing
|
||||||
a peak limiter after the plugin in case loud resonances start building up.
|
a peak limiter after the plugin in case loud resonances start building up.
|
||||||
|
|
||||||
This is a port from https://github.com/robbert-vdh/diopser.
|
This is a port from https://github.com/robbert-vdh/diopser with more features
|
||||||
|
and much better performance.
|
||||||
|
|
||||||
<sup id="disperser">
|
<sup id="disperser">
|
||||||
*Disperser is a trademark of Kilohearts AB. Diopser is in no way related to
|
*Disperser is a trademark of Kilohearts AB. Diopser is in no way related to
|
||||||
|
|
|
@ -22,6 +22,7 @@ use nih_plug::{
|
||||||
};
|
};
|
||||||
use nih_plug::{BoolParam, FloatParam, IntParam, Params, Range, SmoothingStyle};
|
use nih_plug::{BoolParam, FloatParam, IntParam, Params, Range, SmoothingStyle};
|
||||||
use nih_plug::{Enum, EnumParam};
|
use nih_plug::{Enum, EnumParam};
|
||||||
|
use packed_simd::f32x2;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -49,9 +50,12 @@ struct Diopser {
|
||||||
/// Needed for computing the filter coefficients.
|
/// Needed for computing the filter coefficients.
|
||||||
sample_rate: f32,
|
sample_rate: f32,
|
||||||
|
|
||||||
/// All of the all-pass filters, with one array of serial filters per channelq.
|
/// All of the all-pass filters, with vectorized coefficients so they can be calculated for
|
||||||
/// [DiopserParams::num_stages] controls how many filters are actually active.
|
/// multiple channels at once. [DiopserParams::num_stages] controls how many filters are
|
||||||
filters: Vec<[filter::Biquad; MAX_NUM_FILTERS]>,
|
/// actually active.
|
||||||
|
// FIXME: This was the scalar version, maybe add this back at some point.
|
||||||
|
// filters: Vec<[filter::Biquad<f32>; MAX_NUM_FILTERS]>,
|
||||||
|
filters: [filter::Biquad<f32x2>; MAX_NUM_FILTERS],
|
||||||
/// If this is set at the start of the processing cycle, then the filter coefficients should be
|
/// 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
|
/// updated. For the regular filter parameters we can look at the smoothers, but this is needed
|
||||||
/// when changing the number of active filters.
|
/// when changing the number of active filters.
|
||||||
|
@ -111,7 +115,7 @@ impl Default for Diopser {
|
||||||
|
|
||||||
sample_rate: 1.0,
|
sample_rate: 1.0,
|
||||||
|
|
||||||
filters: Vec::new(),
|
filters: [filter::Biquad::default(); MAX_NUM_FILTERS],
|
||||||
should_update_filters,
|
should_update_filters,
|
||||||
next_filter_smoothing_in: 1,
|
next_filter_smoothing_in: 1,
|
||||||
}
|
}
|
||||||
|
@ -216,19 +220,17 @@ impl Plugin for Diopser {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn accepts_bus_config(&self, config: &BusConfig) -> bool {
|
fn accepts_bus_config(&self, config: &BusConfig) -> bool {
|
||||||
// This works with any symmetrical IO layout
|
// FIXME: The scalar version would work for any IO layout, but this SIMD version can only do
|
||||||
config.num_input_channels == config.num_output_channels && config.num_input_channels > 0
|
// stereo
|
||||||
|
config.num_input_channels == config.num_output_channels && config.num_input_channels == 2
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize(
|
fn initialize(
|
||||||
&mut self,
|
&mut self,
|
||||||
bus_config: &BusConfig,
|
_bus_config: &BusConfig,
|
||||||
buffer_config: &BufferConfig,
|
buffer_config: &BufferConfig,
|
||||||
_context: &mut impl ProcessContext,
|
_context: &mut impl ProcessContext,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
self.filters =
|
|
||||||
vec![[Default::default(); MAX_NUM_FILTERS]; bus_config.num_input_channels as usize];
|
|
||||||
|
|
||||||
// Initialize the filters on the first process call
|
// Initialize the filters on the first process call
|
||||||
self.sample_rate = buffer_config.sample_rate;
|
self.sample_rate = buffer_config.sample_rate;
|
||||||
self.should_update_filters.store(true, Ordering::Release);
|
self.should_update_filters.store(true, Ordering::Release);
|
||||||
|
@ -250,16 +252,25 @@ impl Plugin for Diopser {
|
||||||
for mut channel_samples in buffer.iter_mut() {
|
for mut channel_samples in buffer.iter_mut() {
|
||||||
self.maybe_update_filters(smoothing_interval);
|
self.maybe_update_filters(smoothing_interval);
|
||||||
|
|
||||||
// We get better cache locality by iterating over the filters and then over the channels
|
// We can compute the filters for both channels at once. This version thus now only
|
||||||
for filter_idx in 0..self.params.filter_stages.value as usize {
|
// supports stero audio.
|
||||||
for (channel_idx, filters) in self.filters.iter_mut().enumerate() {
|
let mut samples =
|
||||||
// We can also use `channel_samples.iter_mut()`, but the compiler isn't able to
|
f32x2::new(*unsafe { channel_samples.get_unchecked_mut(0) }, *unsafe {
|
||||||
// optmize that iterator away and it would add a ton of overhead over indexing
|
channel_samples.get_unchecked_mut(1)
|
||||||
// the buffer directly
|
});
|
||||||
let sample = unsafe { channel_samples.get_unchecked_mut(channel_idx) };
|
|
||||||
*sample = filters[filter_idx].process(*sample);
|
// 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)
|
||||||
|
{
|
||||||
|
samples = filter.process(samples);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*unsafe { channel_samples.get_unchecked_mut(0) } = samples.extract(0);
|
||||||
|
*unsafe { channel_samples.get_unchecked_mut(1) } = samples.extract(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
ProcessStatus::Normal
|
ProcessStatus::Normal
|
||||||
|
@ -325,11 +336,10 @@ impl Diopser {
|
||||||
}
|
}
|
||||||
.clamp(MIN_FREQUENCY, max_frequency);
|
.clamp(MIN_FREQUENCY, max_frequency);
|
||||||
|
|
||||||
|
// In the scalar version we'd update every channel's filter's coefficients gere
|
||||||
let coefficients =
|
let coefficients =
|
||||||
filter::BiquadCoefficients::allpass(self.sample_rate, filter_frequency, resonance);
|
filter::BiquadCoefficients::allpass(self.sample_rate, filter_frequency, resonance);
|
||||||
for channel in self.filters.iter_mut() {
|
self.filters[filter_idx].coefficients = coefficients;
|
||||||
channel[filter_idx].coefficients = coefficients;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue