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); } }