1
0
Fork 0

Compute sidechain magnitude spectra when enabled

This commit is contained in:
Robbert van der Helm 2022-07-25 15:07:21 +02:00
parent 3ffc2f0604
commit 2813f3d827
2 changed files with 196 additions and 52 deletions

View file

@ -70,6 +70,10 @@ pub struct CompressorBank {
/// The current envelope value for this bin, in linear space. Indexed by /// The current envelope value for this bin, in linear space. Indexed by
/// `[channel_idx][compressor_idx]`. /// `[channel_idx][compressor_idx]`.
envelopes: Vec<Vec<f32>>, envelopes: Vec<Vec<f32>>,
/// When sidechaining is enabled, this contains the per-channel frqeuency spectrum magnitudes
/// for the current block. The compressor thresholds and knee values are multiplied by these
/// values to get the effective thresholds.
sidechain_spectrum_magnitudes: Vec<Vec<f32>>,
/// The window size this compressor bank was configured for. This is used to compute the /// The window size this compressor bank was configured for. This is used to compute the
/// coefficients for the envelope followers in the process function. /// coefficients for the envelope followers in the process function.
window_size: usize, window_size: usize,
@ -391,6 +395,10 @@ impl CompressorBank {
upwards_ratio_recips: Vec::with_capacity(complex_buffer_len), upwards_ratio_recips: Vec::with_capacity(complex_buffer_len),
envelopes: vec![Vec::with_capacity(complex_buffer_len); num_channels], envelopes: vec![Vec::with_capacity(complex_buffer_len); num_channels],
sidechain_spectrum_magnitudes: vec![
Vec::with_capacity(complex_buffer_len);
num_channels
],
window_size: 0, window_size: 0,
sample_rate: 1.0, sample_rate: 1.0,
} }
@ -426,6 +434,12 @@ impl CompressorBank {
for envelopes in self.envelopes.iter_mut() { for envelopes in self.envelopes.iter_mut() {
envelopes.reserve_exact(complex_buffer_len.saturating_sub(envelopes.len())); envelopes.reserve_exact(complex_buffer_len.saturating_sub(envelopes.len()));
} }
self.sidechain_spectrum_magnitudes
.resize_with(num_channels, Vec::new);
for magnitudes in self.sidechain_spectrum_magnitudes.iter_mut() {
magnitudes.reserve_exact(complex_buffer_len.saturating_sub(magnitudes.len()));
}
} }
/// Resize the number of compressors to match the current window size. Also precomputes the /// Resize the number of compressors to match the current window size. Also precomputes the
@ -458,6 +472,10 @@ impl CompressorBank {
envelopes.resize(complex_buffer_len, 0.0); envelopes.resize(complex_buffer_len, 0.0);
} }
for magnitudes in self.sidechain_spectrum_magnitudes.iter_mut() {
magnitudes.resize(complex_buffer_len, 0.0);
}
self.window_size = window_size; self.window_size = window_size;
self.sample_rate = buffer_config.sample_rate; self.sample_rate = buffer_config.sample_rate;
@ -477,6 +495,8 @@ impl CompressorBank {
for envelopes in self.envelopes.iter_mut() { for envelopes in self.envelopes.iter_mut() {
envelopes.fill(0.0); envelopes.fill(0.0);
} }
// Sidechain data doesn't need to be reset as it will be overwritten immediately before use
} }
/// Apply the magnitude compression to a buffer of FFT bins. The compressors are first updated /// Apply the magnitude compression to a buffer of FFT bins. The compressors are first updated
@ -491,13 +511,22 @@ impl CompressorBank {
overlap_times: usize, overlap_times: usize,
skip_bins_below: usize, skip_bins_below: usize,
) { ) {
assert_eq!(buffer.len(), self.log2_freqs.len()); nih_debug_assert_eq!(buffer.len(), self.log2_freqs.len());
self.update_if_needed(params); self.update_if_needed(params);
self.update_envelopes(buffer, channel_idx, params, overlap_times, skip_bins_below); self.update_envelopes(buffer, channel_idx, params, overlap_times, skip_bins_below);
self.compress(buffer, channel_idx, params, skip_bins_below); self.compress(buffer, channel_idx, params, skip_bins_below);
} }
/// Set the sidechain frequency spectrum magnitudes just before a [`process()`][Self::process()]
/// call. These will be multiplied with the existing compressor thresholds and knee values to
/// get the effective values for use with sidechaining.
pub fn process_sidechain(&mut self, sc_buffer: &mut [Complex32], channel_idx: usize) {
nih_debug_assert_eq!(sc_buffer.len(), self.log2_freqs.len());
self.update_sidechain_spectra(sc_buffer, channel_idx);
}
/// Update the envelope followers based on the bin magnetudes. /// Update the envelope followers based on the bin magnetudes.
fn update_envelopes( fn update_envelopes(
&mut self, &mut self,
@ -547,6 +576,18 @@ impl CompressorBank {
} }
} }
/// Update the spectral data using the sidechain input
fn update_sidechain_spectra(&mut self, sc_buffer: &mut [Complex32], channel_idx: usize) {
nih_debug_assert!(channel_idx < self.sidechain_spectrum_magnitudes.len());
for (bin, magnitude) in sc_buffer
.iter()
.zip(self.sidechain_spectrum_magnitudes[channel_idx].iter_mut())
{
*magnitude = bin.norm();
}
}
/// Actually do the thing. [`Self::update_envelopes()`] must have been called before calling /// Actually do the thing. [`Self::update_envelopes()`] must have been called before calling
/// this. /// this.
fn compress( fn compress(

View file

@ -52,7 +52,7 @@ struct SpectralCompressor {
buffer_config: BufferConfig, buffer_config: BufferConfig,
/// An adapter that performs most of the overlap-add algorithm for us. /// An adapter that performs most of the overlap-add algorithm for us.
stft: util::StftHelper, stft: util::StftHelper<1>,
/// Contains a Hann window function of the current window length, passed to the overlap-add /// Contains a Hann window function of the current window length, passed to the overlap-add
/// helper. Allocated with a `MAX_WINDOW_SIZE` initial capacity. /// helper. Allocated with a `MAX_WINDOW_SIZE` initial capacity.
window_function: Vec<f32>, window_function: Vec<f32>,
@ -263,6 +263,10 @@ impl Plugin for SpectralCompressor {
const DEFAULT_NUM_INPUTS: u32 = 2; const DEFAULT_NUM_INPUTS: u32 = 2;
const DEFAULT_NUM_OUTPUTS: u32 = 2; const DEFAULT_NUM_OUTPUTS: u32 = 2;
const DEFAULT_AUX_INPUTS: Option<AuxiliaryIOConfig> = Some(AuxiliaryIOConfig {
num_busses: 1,
num_channels: 2,
});
const SAMPLE_ACCURATE_AUTOMATION: bool = true; const SAMPLE_ACCURATE_AUTOMATION: bool = true;
@ -275,8 +279,11 @@ impl Plugin for SpectralCompressor {
} }
fn accepts_bus_config(&self, config: &BusConfig) -> bool { fn accepts_bus_config(&self, config: &BusConfig) -> bool {
// We can support any channel layout // We can support any channel layout as long as the number of channels is consistent
config.num_input_channels == config.num_output_channels && config.num_input_channels > 0 config.num_input_channels == config.num_output_channels
&& config.num_input_channels > 0
&& config.aux_input_busses.num_busses == 1
&& config.aux_input_busses.num_channels == config.num_input_channels
} }
fn initialize( fn initialize(
@ -333,7 +340,7 @@ impl Plugin for SpectralCompressor {
fn process( fn process(
&mut self, &mut self,
buffer: &mut Buffer, buffer: &mut Buffer,
_aux: &mut AuxiliaryBuffers, aux: &mut AuxiliaryBuffers,
context: &mut impl ProcessContext, context: &mut impl ProcessContext,
) -> ProcessStatus { ) -> ProcessStatus {
// If the window size has changed since the last process call, reset the buffers and chance // If the window size has changed since the last process call, reset the buffers and chance
@ -375,53 +382,59 @@ impl Plugin for SpectralCompressor {
// This is mixed in later with latency compensation applied // This is mixed in later with latency compensation applied
self.dry_wet_mixer.write_dry(buffer); self.dry_wet_mixer.write_dry(buffer);
self.stft match self.params.threshold.mode.value() {
.process_overlap_add(buffer, overlap_times, |channel_idx, real_fft_buffer| { compressor_bank::ThresholdMode::Internal => self.stft.process_overlap_add(
// We'll window the input with a Hann function to avoid spectral leakage. The input buffer,
// gain here also contains a compensation factor for the forward FFT to make the overlap_times,
// compressor thresholds make more sense. |channel_idx, real_fft_buffer| {
for (sample, window_sample) in real_fft_buffer.iter_mut().zip(&self.window_function) process_stft_main(
{ channel_idx,
*sample *= window_sample * input_gain; real_fft_buffer,
} &mut self.complex_fft_buffer,
fft_plan,
// RustFFT doesn't actually need a scratch buffer here, so we'll pass an empty &self.window_function,
// buffer instead &self.params,
fft_plan &mut self.compressor_bank,
.r2c_plan input_gain,
.process_with_scratch(real_fft_buffer, &mut self.complex_fft_buffer, &mut []) output_gain,
.unwrap(); overlap_times,
first_non_dc_bin_idx,
// This is where the magic happens )
self.compressor_bank.process( },
&mut self.complex_fft_buffer, ),
channel_idx, compressor_bank::ThresholdMode::Sidechain => self.stft.process_overlap_add_sidechain(
&self.params, buffer,
overlap_times, [&aux.inputs[0]],
first_non_dc_bin_idx, overlap_times,
); |channel_idx, sidechain_buffer_idx, real_fft_buffer| {
if sidechain_buffer_idx.is_some() {
// The DC and other low frequency bins doesn't contain much semantic meaning anymore process_stft_sidechain(
// after all of this, so it only ends up consuming headroom. channel_idx,
if self.params.global.dc_filter.value { real_fft_buffer,
self.complex_fft_buffer[..first_non_dc_bin_idx].fill(Complex32::default()); &mut self.complex_fft_buffer,
} fft_plan,
&self.window_function,
// Inverse FFT back into the scratch buffer. This will be added to a ring buffer &mut self.compressor_bank,
// which gets written back to the host at a one block delay. input_gain,
fft_plan );
.c2r_plan } else {
.process_with_scratch(&mut self.complex_fft_buffer, real_fft_buffer, &mut []) process_stft_main(
.unwrap(); channel_idx,
real_fft_buffer,
// Apply the window function once more to reduce time domain aliasing. The gain &mut self.complex_fft_buffer,
// compensation compensates for the squared Hann window that would be applied if we fft_plan,
// didn't do any processing at all as well as the FFT+IFFT itself. &self.window_function,
for (sample, window_sample) in real_fft_buffer.iter_mut().zip(&self.window_function) &self.params,
{ &mut self.compressor_bank,
*sample *= window_sample * output_gain; input_gain,
} output_gain,
}); overlap_times,
first_non_dc_bin_idx,
)
}
},
),
}
self.dry_wet_mixer.mix_in_dry( self.dry_wet_mixer.mix_in_dry(
buffer, buffer,
@ -465,6 +478,96 @@ impl SpectralCompressor {
} }
} }
// These separate functions are needed to avoid having to either duplicate the main process function
// or always do the sidechain STFT. You can't do partial borrows and call `&mut self` methods at the
// same time.
/// The main process function inside of the STFT callback. If the sidechaining option is
/// enabled, another callback will run just before this to set up the siddechain frequency
/// spectrum magnitudes.
#[allow(clippy::too_many_arguments)]
fn process_stft_main(
channel_idx: usize,
real_fft_buffer: &mut [f32],
complex_fft_buffer: &mut [Complex32],
fft_plan: &mut Plan,
window_function: &[f32],
params: &SpectralCompressorParams,
compressor_bank: &mut compressor_bank::CompressorBank,
input_gain: f32,
output_gain: f32,
overlap_times: usize,
first_non_dc_bin_idx: usize,
) {
// We'll window the input with a Hann function to avoid spectral leakage. The input gain
// here also contains a compensation factor for the forward FFT to make the compressor
// thresholds make more sense.
for (sample, window_sample) in real_fft_buffer.iter_mut().zip(window_function) {
*sample *= window_sample * input_gain;
}
// RustFFT doesn't actually need a scratch buffer here, so we'll pass an empty buffer
// instead
fft_plan
.r2c_plan
.process_with_scratch(real_fft_buffer, complex_fft_buffer, &mut [])
.unwrap();
// This is where the magic happens
compressor_bank.process(
complex_fft_buffer,
channel_idx,
params,
overlap_times,
first_non_dc_bin_idx,
);
// The DC and other low frequency bins doesn't contain much semantic meaning anymore
// after all of this, so it only ends up consuming headroom.
if params.global.dc_filter.value {
complex_fft_buffer[..first_non_dc_bin_idx].fill(Complex32::default());
}
// Inverse FFT back into the scratch buffer. This will be added to a ring buffer
// which gets written back to the host at a one block delay.
fft_plan
.c2r_plan
.process_with_scratch(complex_fft_buffer, real_fft_buffer, &mut [])
.unwrap();
// Apply the window function once more to reduce time domain aliasing. The gain
// compensation compensates for the squared Hann window that would be applied if we
// didn't do any processing at all as well as the FFT+IFFT itself.
for (sample, window_sample) in real_fft_buffer.iter_mut().zip(window_function) {
*sample *= window_sample * output_gain;
}
}
/// The analysis process function inside of the STFT callback used to compute the frequency
/// spectrum magnitudes from the sidechain input if the sidechaining option is enabled. All
/// sidechain channels will be processed before processing the main input
fn process_stft_sidechain(
channel_idx: usize,
real_fft_buffer: &mut [f32],
complex_fft_buffer: &mut [Complex32],
fft_plan: &mut Plan,
window_function: &[f32],
compressor_bank: &mut compressor_bank::CompressorBank,
input_gain: f32,
) {
// The sidechain input should be gained, scaled, and windowed the exact same was as the
// main input as it's used for analysis
for (sample, window_sample) in real_fft_buffer.iter_mut().zip(window_function) {
*sample *= window_sample * input_gain;
}
fft_plan
.r2c_plan
.process_with_scratch(real_fft_buffer, complex_fft_buffer, &mut [])
.unwrap();
compressor_bank.process_sidechain(complex_fft_buffer, channel_idx);
}
impl ClapPlugin for SpectralCompressor { impl ClapPlugin for SpectralCompressor {
const CLAP_ID: &'static str = "nl.robbertvanderhelm.spectral-compressor"; const CLAP_ID: &'static str = "nl.robbertvanderhelm.spectral-compressor";
const CLAP_DESCRIPTION: Option<&'static str> = Some("Turn things into pink noise on demand"); const CLAP_DESCRIPTION: Option<&'static str> = Some("Turn things into pink noise on demand");