Compute sidechain magnitude spectra when enabled
This commit is contained in:
parent
3ffc2f0604
commit
2813f3d827
|
@ -70,6 +70,10 @@ pub struct CompressorBank {
|
|||
/// The current envelope value for this bin, in linear space. Indexed by
|
||||
/// `[channel_idx][compressor_idx]`.
|
||||
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
|
||||
/// coefficients for the envelope followers in the process function.
|
||||
window_size: usize,
|
||||
|
@ -391,6 +395,10 @@ impl CompressorBank {
|
|||
upwards_ratio_recips: Vec::with_capacity(complex_buffer_len),
|
||||
|
||||
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,
|
||||
sample_rate: 1.0,
|
||||
}
|
||||
|
@ -426,6 +434,12 @@ impl CompressorBank {
|
|||
for envelopes in self.envelopes.iter_mut() {
|
||||
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
|
||||
|
@ -458,6 +472,10 @@ impl CompressorBank {
|
|||
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.sample_rate = buffer_config.sample_rate;
|
||||
|
||||
|
@ -477,6 +495,8 @@ impl CompressorBank {
|
|||
for envelopes in self.envelopes.iter_mut() {
|
||||
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
|
||||
|
@ -491,13 +511,22 @@ impl CompressorBank {
|
|||
overlap_times: 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_envelopes(buffer, channel_idx, params, overlap_times, 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.
|
||||
fn update_envelopes(
|
||||
&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
|
||||
/// this.
|
||||
fn compress(
|
||||
|
|
|
@ -52,7 +52,7 @@ struct SpectralCompressor {
|
|||
buffer_config: BufferConfig,
|
||||
|
||||
/// 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
|
||||
/// helper. Allocated with a `MAX_WINDOW_SIZE` initial capacity.
|
||||
window_function: Vec<f32>,
|
||||
|
@ -263,6 +263,10 @@ impl Plugin for SpectralCompressor {
|
|||
|
||||
const DEFAULT_NUM_INPUTS: 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;
|
||||
|
||||
|
@ -275,8 +279,11 @@ impl Plugin for SpectralCompressor {
|
|||
}
|
||||
|
||||
fn accepts_bus_config(&self, config: &BusConfig) -> bool {
|
||||
// We can support any channel layout
|
||||
config.num_input_channels == config.num_output_channels && config.num_input_channels > 0
|
||||
// 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.aux_input_busses.num_busses == 1
|
||||
&& config.aux_input_busses.num_channels == config.num_input_channels
|
||||
}
|
||||
|
||||
fn initialize(
|
||||
|
@ -333,7 +340,7 @@ impl Plugin for SpectralCompressor {
|
|||
fn process(
|
||||
&mut self,
|
||||
buffer: &mut Buffer,
|
||||
_aux: &mut AuxiliaryBuffers,
|
||||
aux: &mut AuxiliaryBuffers,
|
||||
context: &mut impl ProcessContext,
|
||||
) -> ProcessStatus {
|
||||
// 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
|
||||
self.dry_wet_mixer.write_dry(buffer);
|
||||
|
||||
self.stft
|
||||
.process_overlap_add(buffer, overlap_times, |channel_idx, real_fft_buffer| {
|
||||
// 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(&self.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, &mut self.complex_fft_buffer, &mut [])
|
||||
.unwrap();
|
||||
|
||||
// This is where the magic happens
|
||||
self.compressor_bank.process(
|
||||
&mut self.complex_fft_buffer,
|
||||
match self.params.threshold.mode.value() {
|
||||
compressor_bank::ThresholdMode::Internal => self.stft.process_overlap_add(
|
||||
buffer,
|
||||
overlap_times,
|
||||
|channel_idx, real_fft_buffer| {
|
||||
process_stft_main(
|
||||
channel_idx,
|
||||
real_fft_buffer,
|
||||
&mut self.complex_fft_buffer,
|
||||
fft_plan,
|
||||
&self.window_function,
|
||||
&self.params,
|
||||
&mut self.compressor_bank,
|
||||
input_gain,
|
||||
output_gain,
|
||||
overlap_times,
|
||||
first_non_dc_bin_idx,
|
||||
)
|
||||
},
|
||||
),
|
||||
compressor_bank::ThresholdMode::Sidechain => self.stft.process_overlap_add_sidechain(
|
||||
buffer,
|
||||
[&aux.inputs[0]],
|
||||
overlap_times,
|
||||
|channel_idx, sidechain_buffer_idx, real_fft_buffer| {
|
||||
if sidechain_buffer_idx.is_some() {
|
||||
process_stft_sidechain(
|
||||
channel_idx,
|
||||
real_fft_buffer,
|
||||
&mut self.complex_fft_buffer,
|
||||
fft_plan,
|
||||
&self.window_function,
|
||||
&mut self.compressor_bank,
|
||||
input_gain,
|
||||
);
|
||||
|
||||
// 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 self.params.global.dc_filter.value {
|
||||
self.complex_fft_buffer[..first_non_dc_bin_idx].fill(Complex32::default());
|
||||
} else {
|
||||
process_stft_main(
|
||||
channel_idx,
|
||||
real_fft_buffer,
|
||||
&mut self.complex_fft_buffer,
|
||||
fft_plan,
|
||||
&self.window_function,
|
||||
&self.params,
|
||||
&mut self.compressor_bank,
|
||||
input_gain,
|
||||
output_gain,
|
||||
overlap_times,
|
||||
first_non_dc_bin_idx,
|
||||
)
|
||||
}
|
||||
|
||||
// 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(&mut self.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(&self.window_function)
|
||||
{
|
||||
*sample *= window_sample * output_gain;
|
||||
},
|
||||
),
|
||||
}
|
||||
});
|
||||
|
||||
self.dry_wet_mixer.mix_in_dry(
|
||||
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 {
|
||||
const CLAP_ID: &'static str = "nl.robbertvanderhelm.spectral-compressor";
|
||||
const CLAP_DESCRIPTION: Option<&'static str> = Some("Turn things into pink noise on demand");
|
||||
|
|
Loading…
Reference in a new issue