diff --git a/plugins/spectral_compressor/src/compressor_bank.rs b/plugins/spectral_compressor/src/compressor_bank.rs
index eb7fdc7b..910c4185 100644
--- a/plugins/spectral_compressor/src/compressor_bank.rs
+++ b/plugins/spectral_compressor/src/compressor_bank.rs
@@ -14,8 +14,34 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::Arc;
+
use nih_plug::prelude::*;
+/// A bank of compressors so each FFT bin can be compressed individually. The vectors in this struct
+/// will have a capacity of `MAX_WINDOW_SIZE / 2 + 1` and a size that matches the current complex
+/// FFT buffer size. This is stored as a struct of arrays to make SIMD-ing easier in the future.
+pub struct CompressorBank {
+ // TODO: The thresholds and ratios need to be split up in downwards and upwards variants
+ /// If set, then the thresholds should be updated on the next processing cycle. Can be set from
+ /// a parameter value change listener, and is also set when calling `.reset_for_size`.
+ pub should_update_thresholds: Arc,
+ /// If set, then the ratios should be updated on the next processing cycle. Can be set from a
+ /// parameter value change listener, and is also set when calling `.reset_for_size`.
+ pub should_update_ratios: Arc,
+
+ /// Compressor thresholds, in linear space.
+ thresholds: Vec,
+ /// Compressor ratios. If [`CompressorBankParams::high_freq_ratio_rolloff`] is set to 1.0, then
+ /// this will be the same for each compressor.
+ ratios: Vec,
+ /// The current envelope value for this bin, in linear space. Indexed by
+ /// `[channel_idx][compressor_idx]`.
+ envelopes: Vec>,
+ // TODO: Parameters for the envelope followers so we can actuall ydo soemthing useful.
+}
+
#[derive(Params)]
pub struct ThresholdParams {
// TODO: Sidechaining
@@ -83,8 +109,14 @@ pub struct CompressorBankParams {
compressor_release_ms: FloatParam,
}
-impl Default for ThresholdParams {
- fn default() -> Self {
+impl ThresholdParams {
+ /// Create a new [`ThresholdParams`] object. Changing any of the threshold parameters causes the
+ /// passed compressor bank's thresholds to be updated.
+ pub fn new(compressor_bank: &CompressorBank) -> Self {
+ let should_update_thresholds = compressor_bank.should_update_thresholds.clone();
+ let set_update_thresholds =
+ Arc::new(move |_| should_update_thresholds.store(true, Ordering::SeqCst));
+
ThresholdParams {
center_frequency: FloatParam::new(
"Threshold Center",
@@ -95,6 +127,7 @@ impl Default for ThresholdParams {
factor: FloatRange::skew_factor(-2.0),
},
)
+ .with_callback(set_update_thresholds.clone())
// This includes the unit
.with_value_to_string(formatters::v2s_f32_hz_then_khz(0))
.with_string_to_value(formatters::s2v_f32_hz_then_khz()),
@@ -108,6 +141,7 @@ impl Default for ThresholdParams {
max: 50.0,
},
)
+ .with_callback(set_update_thresholds.clone())
.with_unit(" dB")
.with_step_size(0.1),
curve_slope: FloatParam::new(
@@ -118,6 +152,7 @@ impl Default for ThresholdParams {
max: 24.0,
},
)
+ .with_callback(set_update_thresholds.clone())
.with_unit(" dB/oct")
.with_step_size(0.1),
curve_curve: FloatParam::new(
@@ -128,14 +163,24 @@ impl Default for ThresholdParams {
max: 24.0,
},
)
+ .with_callback(set_update_thresholds)
.with_unit(" dB/oct²")
.with_step_size(0.1),
}
}
}
-impl Default for CompressorBankParams {
- fn default() -> Self {
+impl CompressorBankParams {
+ /// Create a new [`CompressorBankParams`] object. Changing any of the threshold or ratio
+ /// parameters causes the passed compressor bank's parameters to be updated.
+ pub fn new(compressor_bank: &CompressorBank) -> Self {
+ let should_update_thresholds = compressor_bank.should_update_thresholds.clone();
+ let set_update_thresholds =
+ Arc::new(move |_| should_update_thresholds.store(true, Ordering::SeqCst));
+ let should_update_ratios = compressor_bank.should_update_ratios.clone();
+ let set_update_ratios =
+ Arc::new(move |_| should_update_ratios.store(true, Ordering::SeqCst));
+
CompressorBankParams {
// TODO: Set nicer default values for these things
// As explained above, these offsets are relative to the target curve
@@ -147,6 +192,7 @@ impl Default for CompressorBankParams {
max: 50.0,
},
)
+ .with_callback(set_update_thresholds.clone())
.with_unit(" dB")
.with_step_size(0.1),
upwards_threshold_offset_db: FloatParam::new(
@@ -157,6 +203,7 @@ impl Default for CompressorBankParams {
max: 50.0,
},
)
+ .with_callback(set_update_thresholds)
.with_unit(" dB")
.with_step_size(0.1),
@@ -165,6 +212,7 @@ impl Default for CompressorBankParams {
0.5,
FloatRange::Linear { min: 0.0, max: 1.0 },
)
+ .with_callback(set_update_ratios.clone())
.with_unit("%")
.with_value_to_string(formatters::v2s_f32_percentage(0))
.with_string_to_value(formatters::s2v_f32_percentage()),
@@ -177,6 +225,7 @@ impl Default for CompressorBankParams {
factor: FloatRange::skew_factor(-2.0),
},
)
+ .with_callback(set_update_ratios.clone())
.with_step_size(0.1)
.with_value_to_string(formatters::v2s_compression_ratio(1))
.with_string_to_value(formatters::s2v_compression_ratio()),
@@ -189,6 +238,7 @@ impl Default for CompressorBankParams {
factor: FloatRange::skew_factor(-2.0),
},
)
+ .with_callback(set_update_ratios)
.with_step_size(0.1)
.with_value_to_string(formatters::v2s_compression_ratio(1))
.with_string_to_value(formatters::s2v_compression_ratio()),
@@ -242,3 +292,65 @@ impl Default for CompressorBankParams {
}
}
}
+
+impl CompressorBank {
+ /// Set up the compressor for the given channel count and maximum FFT window size. The
+ /// compressors won't be initialized yet.
+ pub fn new(num_channels: usize, max_window_size: usize) -> Self {
+ let complex_buffer_len = max_window_size / 2 + 1;
+
+ CompressorBank {
+ should_update_thresholds: Arc::new(AtomicBool::new(true)),
+ should_update_ratios: Arc::new(AtomicBool::new(true)),
+
+ thresholds: Vec::with_capacity(complex_buffer_len),
+ ratios: Vec::with_capacity(complex_buffer_len),
+ envelopes: vec![Vec::with_capacity(complex_buffer_len); num_channels],
+ }
+ }
+
+ /// Change the capacities of the internal buffers to fit new parameters. Use the
+ /// `.reset_for_size()` method to clear the buffers and set the current window size.
+ pub fn update_capacity(&mut self, num_channels: usize, max_window_size: usize) {
+ let complex_buffer_len = max_window_size / 2 + 1;
+
+ self.thresholds
+ .reserve_exact(complex_buffer_len.saturating_sub(self.thresholds.len()));
+ self.ratios
+ .reserve_exact(complex_buffer_len.saturating_sub(self.ratios.len()));
+ self.envelopes.resize_with(num_channels, Vec::new);
+ for envelopes in self.envelopes.iter_mut() {
+ envelopes.reserve_exact(complex_buffer_len.saturating_sub(envelopes.len()));
+ }
+ }
+
+ /// Resize the number of compressors to match the current window size.
+ ///
+ /// If the window size is larger than the maximum window size, then this will allocate.
+ pub fn resize(&mut self, window_size: usize) {
+ let complex_buffer_len = window_size / 2 + 1;
+
+ self.thresholds.resize(complex_buffer_len, 1.0);
+ self.ratios.resize(complex_buffer_len, 1.0);
+ for envelopes in self.envelopes.iter_mut() {
+ envelopes.resize(complex_buffer_len, 0.0);
+ }
+
+ // The compressors need to be updated on the next processing cycle
+ self.should_update_thresholds.store(true, Ordering::SeqCst);
+ self.should_update_ratios.store(true, Ordering::SeqCst);
+ }
+
+ /// Clear out the envelope followers.
+ pub fn reset(&mut self) {
+ for envelopes in self.envelopes.iter_mut() {
+ envelopes.fill(0.0);
+ }
+ }
+
+ /// Update the compressors if needed. This is called just before processing, and the compressors
+ /// are updated in accordance to the atomic flags set on this struct.
+ fn update_if_needed(&mut self) {
+ //
+ }
+}
diff --git a/plugins/spectral_compressor/src/dry_wet_mixer.rs b/plugins/spectral_compressor/src/dry_wet_mixer.rs
index 7c16b58c..fab2f5a1 100644
--- a/plugins/spectral_compressor/src/dry_wet_mixer.rs
+++ b/plugins/spectral_compressor/src/dry_wet_mixer.rs
@@ -50,7 +50,7 @@ impl DryWetMixer {
}
}
- /// Resize the itnernal buffers to fit new parameters.
+ /// Resize the internal buffers to fit new parameters.
pub fn resize(&mut self, num_channels: usize, max_block_size: usize, max_latency: usize) {
let delay_line_len = (max_block_size + max_latency).next_power_of_two();
diff --git a/plugins/spectral_compressor/src/lib.rs b/plugins/spectral_compressor/src/lib.rs
index 77af8254..eb3a3ba8 100644
--- a/plugins/spectral_compressor/src/lib.rs
+++ b/plugins/spectral_compressor/src/lib.rs
@@ -55,6 +55,9 @@ struct SpectralCompressor {
window_function: Vec,
/// A mixer to mix the dry signal back into the processed signal with latency compensation.
dry_wet_mixer: dry_wet_mixer::DryWetMixer,
+ /// Spectral per-bin upwards and downwards compressors with soft-knee settings. This is where
+ /// the magic happens.
+ compressor_bank: compressor_bank::CompressorBank,
/// The algorithms for the FFT and IFFT operations, for each supported order so we can switch
/// between them without replanning or allocations. Initialized during `initialize()`.
@@ -108,14 +111,22 @@ struct SpectralCompressorParams {
impl Default for SpectralCompressor {
fn default() -> Self {
- Self {
- params: Arc::new(SpectralCompressorParams::default()),
+ // Changing any of the compressor threshold or ratio parameters will set an atomic flag in
+ // this object that causes the compressor thresholds and ratios to be recalcualted
+ let compressor_bank = compressor_bank::CompressorBank::new(
+ Self::DEFAULT_NUM_OUTPUTS as usize,
+ MAX_WINDOW_SIZE,
+ );
+
+ SpectralCompressor {
+ params: Arc::new(SpectralCompressorParams::new(&compressor_bank)),
editor_state: editor::default_state(),
// These three will be set to the correct values in the initialize function
stft: util::StftHelper::new(Self::DEFAULT_NUM_OUTPUTS as usize, MAX_WINDOW_SIZE, 0),
window_function: Vec::with_capacity(MAX_WINDOW_SIZE),
dry_wet_mixer: dry_wet_mixer::DryWetMixer::new(0, 0, 0),
+ compressor_bank,
// This is initialized later since we don't want to do non-trivial computations before
// the plugin is initialized
@@ -125,9 +136,11 @@ impl Default for SpectralCompressor {
}
}
-impl Default for SpectralCompressorParams {
- fn default() -> Self {
- Self {
+impl SpectralCompressorParams {
+ /// Create a new [`SpectralCompressorParams`] object. Changing any of the compressor threshold
+ /// or ratio parameters causes the passed compressor bank's parameters to be updated.
+ pub fn new(compressor_bank: &compressor_bank::CompressorBank) -> Self {
+ SpectralCompressorParams {
// TODO: Do still enable per-block smoothing for these settings, because why not
// We don't need any smoothing for these parameters as the overlap-add process will
@@ -171,8 +184,8 @@ impl Default for SpectralCompressorParams {
.with_value_to_string(formatters::v2s_i32_power_of_two())
.with_string_to_value(formatters::s2v_i32_power_of_two()),
- threhold: compressor_bank::ThresholdParams::default(),
- compressors: compressor_bank::CompressorBankParams::default(),
+ threhold: compressor_bank::ThresholdParams::new(compressor_bank),
+ compressors: compressor_bank::CompressorBankParams::new(compressor_bank),
}
}
}
@@ -214,12 +227,13 @@ impl Plugin for SpectralCompressor {
if self.stft.num_channels() != bus_config.num_output_channels as usize {
self.stft = util::StftHelper::new(self.stft.num_channels(), MAX_WINDOW_SIZE, 0);
}
-
self.dry_wet_mixer.resize(
bus_config.num_output_channels as usize,
buffer_config.max_buffer_size as usize,
MAX_WINDOW_SIZE,
);
+ self.compressor_bank
+ .update_capacity(bus_config.num_output_channels as usize, MAX_WINDOW_SIZE);
// Planning with RustFFT is very fast, but it will still allocate we we'll plan all of the
// FFTs we might need in advance
@@ -247,6 +261,7 @@ impl Plugin for SpectralCompressor {
fn reset(&mut self) {
self.dry_wet_mixer.reset();
+ self.compressor_bank.reset();
}
fn process(
@@ -369,6 +384,9 @@ impl SpectralCompressor {
util::window::hann_in_place(&mut self.window_function);
self.complex_fft_buffer
.resize(window_size / 2 + 1, Complex32::default());
+
+ // This also causes the thresholds and ratios to be updated on the next STFT process cycle.
+ self.compressor_bank.resize(window_size);
}
}