2022-07-22 21:40:30 +10:00
|
|
|
// Spectral Compressor: an FFT based compressor
|
|
|
|
// Copyright (C) 2021-2022 Robbert van der Helm
|
|
|
|
//
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
|
|
// it under the terms of the GNU General Public License as published by
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
//
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU General Public License for more details.
|
|
|
|
//
|
|
|
|
// You should have received a copy of the GNU General Public License
|
|
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
use nih_plug::prelude::*;
|
2022-07-23 03:28:55 +10:00
|
|
|
use realfft::num_complex::Complex32;
|
2022-07-24 00:03:21 +10:00
|
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
|
|
use std::sync::Arc;
|
2022-07-22 21:40:30 +10:00
|
|
|
|
2022-07-24 00:03:21 +10:00
|
|
|
use crate::SpectralCompressorParams;
|
2022-07-23 02:25:52 +10:00
|
|
|
|
2022-07-25 01:25:58 +10:00
|
|
|
// These are the parameter ID prefixes used for the downwards and upwards cmpression parameters.
|
|
|
|
const DOWNWARDS_NAME_PREFIX: &str = "downwards_";
|
|
|
|
const UPWARDS_NAME_PREFIX: &str = "upwards_";
|
|
|
|
|
2022-07-23 00:54:55 +10:00
|
|
|
/// 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 {
|
2022-07-23 01:29:23 +10:00
|
|
|
/// If set, then the downwards 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_downwards_thresholds: Arc<AtomicBool>,
|
|
|
|
/// The same as `should_update_downwards_thresholds`, but for upwards thresholds.
|
|
|
|
pub should_update_upwards_thresholds: Arc<AtomicBool>,
|
|
|
|
/// If set, then the downwards 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_downwards_ratios: Arc<AtomicBool>,
|
|
|
|
/// The same as `should_update_downwards_ratios`, but for upwards ratios.
|
|
|
|
pub should_update_upwards_ratios: Arc<AtomicBool>,
|
|
|
|
|
2022-07-23 02:00:29 +10:00
|
|
|
/// For each compressor bin, `log2(freq)` where `freq` is the frequency associated with that
|
|
|
|
/// compressor. This is precomputed since all update functions need it.
|
|
|
|
log2_freqs: Vec<f32>,
|
|
|
|
|
2022-07-23 01:29:23 +10:00
|
|
|
/// Downwards compressor thresholds, in linear space.
|
|
|
|
downwards_thresholds: Vec<f32>,
|
2022-07-24 22:15:55 +10:00
|
|
|
/// The start (lower end) of the downwards's knee range, in linear space. This is calculated in
|
|
|
|
/// decibel/log space and then converted to gain to keep everything in linear space.
|
|
|
|
downwards_knee_starts: Vec<f32>,
|
|
|
|
/// The end (upper end) of the downwards's knee range, in linear space.
|
|
|
|
downwards_knee_ends: Vec<f32>,
|
2022-07-23 04:44:36 +10:00
|
|
|
/// The reciprocals of the downwards compressor ratios. At 1.0 the cmopressor won't do anything.
|
|
|
|
/// If [`CompressorBankParams::high_freq_ratio_rolloff`] is set to 1.0, then this will be the
|
|
|
|
/// same for each compressor. We're doing the compression in linear space to avoid a logarithm,
|
|
|
|
/// so the division by the ratio becomes an nth-root, or exponentation by the reciprocal of the
|
|
|
|
/// ratio.
|
|
|
|
downwards_ratio_recips: Vec<f32>,
|
2022-07-24 22:15:55 +10:00
|
|
|
|
2022-07-23 07:48:07 +10:00
|
|
|
/// Upwards compressor thresholds, in linear space.
|
|
|
|
upwards_thresholds: Vec<f32>,
|
2022-07-24 22:15:55 +10:00
|
|
|
/// The start (lower end) of the upwards's knee range, in linear space.
|
|
|
|
upwards_knee_starts: Vec<f32>,
|
|
|
|
/// The end (upper end) of the upwards's knee range, in linear space.
|
|
|
|
upwards_knee_ends: Vec<f32>,
|
2022-07-23 04:44:36 +10:00
|
|
|
/// The same as `downwards_ratio_recipss`, but for the upwards compression.
|
|
|
|
upwards_ratio_recips: Vec<f32>,
|
2022-07-23 00:54:55 +10:00
|
|
|
|
|
|
|
/// The current envelope value for this bin, in linear space. Indexed by
|
|
|
|
/// `[channel_idx][compressor_idx]`.
|
|
|
|
envelopes: Vec<Vec<f32>>,
|
2022-07-25 23:07:21 +10:00
|
|
|
/// 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>>,
|
2022-07-23 03:28:55 +10:00
|
|
|
/// 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,
|
|
|
|
/// The sample rate this compressor bank was configured for. This is used to compute the
|
|
|
|
/// coefficients for the envelope followers in the process function.
|
|
|
|
sample_rate: f32,
|
2022-07-23 00:54:55 +10:00
|
|
|
}
|
|
|
|
|
2022-07-22 22:30:44 +10:00
|
|
|
#[derive(Params)]
|
|
|
|
pub struct ThresholdParams {
|
2022-07-25 00:27:35 +10:00
|
|
|
/// The compressor threshold at the center frequency. When sidechaining is enabled, the input
|
|
|
|
/// signal is gained by the inverse of this value. This replaces the input gain in the original
|
|
|
|
/// Spectral Compressor. In the polynomial below, this is the intercept.
|
|
|
|
#[id = "tresh_global"]
|
2022-07-25 22:13:54 +10:00
|
|
|
pub threshold_db: FloatParam,
|
2022-07-22 22:30:44 +10:00
|
|
|
/// The center frqeuency for the target curve when sidechaining is not enabled. The curve is a
|
|
|
|
/// polynomial `threshold_db + curve_slope*x + curve_curve*(x^2)` that evaluates to a decibel
|
|
|
|
/// value, where `x = log2(center_frequency) - log2(bin_frequency)`. In other words, this is
|
|
|
|
/// evaluated in the log/log domain for decibels and octaves.
|
|
|
|
#[id = "thresh_center_freq"]
|
2022-07-25 22:13:54 +10:00
|
|
|
pub center_frequency: FloatParam,
|
2022-07-22 22:30:44 +10:00
|
|
|
/// The slope for the curve, in the log/log domain. See the polynomial above.
|
|
|
|
#[id = "thresh_curve_slope"]
|
2022-07-25 22:13:54 +10:00
|
|
|
pub curve_slope: FloatParam,
|
2022-07-22 22:30:44 +10:00
|
|
|
/// The, uh, 'curve' for the curve, in the logarithmic domain. This is the third coefficient in
|
|
|
|
/// the quadratic polynomial and controls the parabolic behavior. Positive values turn the curve
|
|
|
|
/// into a v-shaped curve, while negative values attenuate everything outside of the center
|
|
|
|
/// frequency. See the polynomial above.
|
|
|
|
#[id = "thresh_curve_curve"]
|
2022-07-25 22:13:54 +10:00
|
|
|
pub curve_curve: FloatParam,
|
2022-07-25 22:21:51 +10:00
|
|
|
|
|
|
|
/// Controls the type of threshold that should be used. Check [`ThresholdMode`] for more
|
|
|
|
/// information.
|
|
|
|
#[id = "thresh_mode"]
|
|
|
|
pub mode: EnumParam<ThresholdMode>,
|
|
|
|
/// A `[0, 1]` parameter that controls how much of the other channels should be mixed in when
|
|
|
|
/// computing the channel gain value that is then multiplied with he thresholds and knee values
|
|
|
|
/// to the the compression parameters when using the sidechain modes.
|
|
|
|
#[id = "thresh_sc_link"]
|
|
|
|
pub sc_channel_link: FloatParam,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// The type of threshold to use.
|
|
|
|
#[derive(Enum, Debug, PartialEq, Eq)]
|
|
|
|
pub enum ThresholdMode {
|
|
|
|
/// Configure the thresholds to offset pink noise. This means that the slope will receive an
|
|
|
|
/// additional -3 dB/octave slope.
|
|
|
|
#[id = "internal"]
|
|
|
|
#[name = "Pink Noise"]
|
|
|
|
Internal,
|
|
|
|
/// Dynamically reconfigure the thresholds based on a sidechain input. The -3 dB/octave slope
|
|
|
|
/// offset is not applied here so the curve stays true to the sidechain input at the default
|
|
|
|
/// settings. This works by simply multiplying the sidechain gain levels with the precomputed
|
|
|
|
/// threshold, knee start, and knee end values. The sidechain channel linking option determines
|
|
|
|
/// how how much of the other channel values to mix in before multiplying the sidechain gain
|
|
|
|
/// values with the thresholds.
|
|
|
|
#[id = "sidechain"]
|
|
|
|
#[name = "Sidechain"]
|
|
|
|
Sidechain,
|
2022-07-22 22:30:44 +10:00
|
|
|
}
|
|
|
|
|
2022-07-24 02:23:03 +10:00
|
|
|
/// Contains the compressor parameters for both the upwards and downwards compressor banks.
|
|
|
|
#[derive(Params)]
|
2022-07-22 21:40:30 +10:00
|
|
|
pub struct CompressorBankParams {
|
2022-07-24 02:23:03 +10:00
|
|
|
#[nested = "upwards"]
|
2022-07-24 02:24:02 +10:00
|
|
|
pub upwards: Arc<CompressorParams>,
|
2022-07-25 01:17:10 +10:00
|
|
|
#[nested = "downwards"]
|
|
|
|
pub downwards: Arc<CompressorParams>,
|
2022-07-24 00:32:49 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
/// This struct contains the parameters for either the upward or downward compressors. The `Params`
|
2022-07-24 02:23:03 +10:00
|
|
|
/// trait is implemented manually to avoid copy-pasting parameters for both types of compressor.
|
|
|
|
/// Both versions will have a parameter ID and a parameter name prefix to distinguish them.
|
2022-07-24 00:32:49 +10:00
|
|
|
pub struct CompressorParams {
|
2022-07-24 02:23:03 +10:00
|
|
|
/// The prefix to use in the `.param_map()` function so the upwards and downwards compressors
|
|
|
|
/// get unique parameter IDs.
|
|
|
|
param_id_prefix: &'static str,
|
|
|
|
|
2022-07-24 00:32:49 +10:00
|
|
|
/// The compression threshold relative to the target curve.
|
2022-07-25 22:13:54 +10:00
|
|
|
pub threshold_offset_db: FloatParam,
|
2022-07-24 00:32:49 +10:00
|
|
|
/// The compression ratio. At 1.0 the compressor is disengaged.
|
2022-07-25 22:13:54 +10:00
|
|
|
pub ratio: FloatParam,
|
2022-07-22 22:01:55 +10:00
|
|
|
/// A `[0, 1]` scaling factor that causes the compressors for the higher registers to have lower
|
|
|
|
/// ratios than the compressors for the lower registers. The scaling is applied logarithmically
|
2022-07-23 07:44:42 +10:00
|
|
|
/// rather than linearly over the compressors. If this is set to 1.0, then the ratios will be
|
|
|
|
/// the same for every compressor.
|
2022-07-25 22:13:54 +10:00
|
|
|
pub high_freq_ratio_rolloff: FloatParam,
|
2022-07-25 22:18:42 +10:00
|
|
|
/// The compression knee width, in decibels.
|
|
|
|
pub knee_width_db: FloatParam,
|
2022-07-22 21:40:30 +10:00
|
|
|
}
|
|
|
|
|
2022-07-24 02:23:03 +10:00
|
|
|
unsafe impl Params for CompressorParams {
|
2022-07-24 00:32:49 +10:00
|
|
|
fn param_map(&self) -> Vec<(String, ParamPtr, String)> {
|
2022-07-24 02:23:03 +10:00
|
|
|
let prefix = self.param_id_prefix;
|
|
|
|
vec![
|
|
|
|
(
|
|
|
|
format!("{prefix}threshold_offset"),
|
|
|
|
self.threshold_offset_db.as_ptr(),
|
|
|
|
// The parent `CompressorBankParams` struct will add the group here
|
|
|
|
String::new(),
|
|
|
|
),
|
|
|
|
(format!("{prefix}ratio"), self.ratio.as_ptr(), String::new()),
|
|
|
|
(
|
2022-07-25 22:18:42 +10:00
|
|
|
format!("{prefix}high_freq_rolloff"),
|
|
|
|
self.high_freq_ratio_rolloff.as_ptr(),
|
2022-07-24 02:23:03 +10:00
|
|
|
String::new(),
|
|
|
|
),
|
|
|
|
(
|
2022-07-25 22:18:42 +10:00
|
|
|
format!("{prefix}knee"),
|
|
|
|
self.knee_width_db.as_ptr(),
|
2022-07-24 02:23:03 +10:00
|
|
|
String::new(),
|
|
|
|
),
|
|
|
|
]
|
2022-07-24 00:32:49 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-23 00:54:55 +10:00
|
|
|
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 {
|
2022-07-23 01:29:23 +10:00
|
|
|
let should_update_downwards_thresholds =
|
|
|
|
compressor_bank.should_update_downwards_thresholds.clone();
|
|
|
|
let should_update_upwards_thresholds =
|
|
|
|
compressor_bank.should_update_upwards_thresholds.clone();
|
|
|
|
let set_update_both_thresholds = Arc::new(move |_| {
|
|
|
|
should_update_downwards_thresholds.store(true, Ordering::SeqCst);
|
|
|
|
should_update_upwards_thresholds.store(true, Ordering::SeqCst);
|
|
|
|
});
|
2022-07-23 00:54:55 +10:00
|
|
|
|
2022-07-22 22:30:44 +10:00
|
|
|
ThresholdParams {
|
2022-07-25 00:27:35 +10:00
|
|
|
threshold_db: FloatParam::new(
|
|
|
|
"Global Threshold",
|
2022-07-25 02:30:20 +10:00
|
|
|
0.0,
|
2022-07-25 00:27:35 +10:00
|
|
|
FloatRange::Linear {
|
2022-07-25 01:06:20 +10:00
|
|
|
min: -100.0,
|
|
|
|
max: 20.0,
|
2022-07-25 00:27:35 +10:00
|
|
|
},
|
|
|
|
)
|
|
|
|
.with_callback(set_update_both_thresholds.clone())
|
|
|
|
.with_unit(" dB")
|
|
|
|
.with_step_size(0.1),
|
2022-07-23 08:14:01 +10:00
|
|
|
center_frequency: FloatParam::new(
|
|
|
|
"Threshold Center",
|
2022-07-25 02:26:57 +10:00
|
|
|
420.0,
|
2022-07-23 08:14:01 +10:00
|
|
|
FloatRange::Skewed {
|
|
|
|
min: 20.0,
|
|
|
|
max: 20_000.0,
|
|
|
|
factor: FloatRange::skew_factor(-2.0),
|
|
|
|
},
|
|
|
|
)
|
|
|
|
.with_callback(set_update_both_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()),
|
2022-07-23 08:21:55 +10:00
|
|
|
// These are polynomial coefficients that are evaluated in the log/log domain
|
|
|
|
// (octaves/decibels). The global threshold is the intercept.
|
2022-07-22 22:30:44 +10:00
|
|
|
curve_slope: FloatParam::new(
|
|
|
|
"Threshold Slope",
|
|
|
|
0.0,
|
2022-07-25 01:07:35 +10:00
|
|
|
FloatRange::SymmetricalSkewed {
|
2022-07-23 08:03:56 +10:00
|
|
|
min: -36.0,
|
|
|
|
max: 36.0,
|
2022-07-25 01:48:02 +10:00
|
|
|
factor: FloatRange::skew_factor(-2.0),
|
2022-07-25 01:07:35 +10:00
|
|
|
center: 0.0,
|
2022-07-22 22:30:44 +10:00
|
|
|
},
|
|
|
|
)
|
2022-07-23 01:29:23 +10:00
|
|
|
.with_callback(set_update_both_thresholds.clone())
|
2022-07-22 22:30:44 +10:00
|
|
|
.with_unit(" dB/oct")
|
2022-07-25 01:48:02 +10:00
|
|
|
.with_step_size(0.01),
|
2022-07-22 22:30:44 +10:00
|
|
|
curve_curve: FloatParam::new(
|
|
|
|
"Threshold Curve",
|
|
|
|
0.0,
|
2022-07-25 00:24:31 +10:00
|
|
|
FloatRange::SymmetricalSkewed {
|
2022-07-22 22:30:44 +10:00
|
|
|
min: -24.0,
|
|
|
|
max: 24.0,
|
2022-07-25 01:48:02 +10:00
|
|
|
factor: FloatRange::skew_factor(-2.0),
|
2022-07-25 00:24:31 +10:00
|
|
|
center: 0.0,
|
2022-07-22 22:30:44 +10:00
|
|
|
},
|
|
|
|
)
|
2022-07-25 22:21:51 +10:00
|
|
|
.with_callback(set_update_both_thresholds.clone())
|
2022-07-25 00:27:35 +10:00
|
|
|
.with_unit(" dB/oct²")
|
2022-07-25 01:48:02 +10:00
|
|
|
.with_step_size(0.01),
|
2022-07-25 22:21:51 +10:00
|
|
|
|
|
|
|
mode: EnumParam::new("Mode", ThresholdMode::Internal)
|
|
|
|
// Not the most efficient way to do this, but it's a bit cleaner than the
|
|
|
|
// alternative
|
|
|
|
.with_callback(Arc::new(move |_| set_update_both_thresholds(0.0))),
|
|
|
|
sc_channel_link: FloatParam::new(
|
|
|
|
"SC Channel Link",
|
|
|
|
0.0,
|
|
|
|
FloatRange::Linear { min: 0.0, max: 1.0 },
|
|
|
|
)
|
|
|
|
.with_unit("%")
|
|
|
|
.with_value_to_string(formatters::v2s_f32_percentage(0))
|
|
|
|
.with_string_to_value(formatters::s2v_f32_percentage()),
|
2022-07-22 22:30:44 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-23 00:54:55 +10:00
|
|
|
impl CompressorBankParams {
|
2022-07-24 00:32:49 +10:00
|
|
|
/// Create compressor bank parameter objects for both the downwards and upwards compressors of
|
|
|
|
/// `compressor`. Changing the ratio and threshold parameters will cause the compressor to
|
|
|
|
/// recompute its values on the next processing cycle.
|
|
|
|
pub fn new(compressor: &CompressorBank) -> Self {
|
2022-07-22 22:30:44 +10:00
|
|
|
CompressorBankParams {
|
2022-07-24 02:24:02 +10:00
|
|
|
downwards: Arc::new(CompressorParams::new(
|
2022-07-25 01:25:58 +10:00
|
|
|
DOWNWARDS_NAME_PREFIX,
|
2022-07-24 00:32:49 +10:00
|
|
|
"Downwards",
|
|
|
|
compressor.should_update_downwards_thresholds.clone(),
|
|
|
|
compressor.should_update_downwards_ratios.clone(),
|
2022-07-24 02:24:02 +10:00
|
|
|
)),
|
|
|
|
upwards: Arc::new(CompressorParams::new(
|
2022-07-25 01:25:58 +10:00
|
|
|
UPWARDS_NAME_PREFIX,
|
2022-07-24 00:32:49 +10:00
|
|
|
"Upwards",
|
|
|
|
compressor.should_update_upwards_thresholds.clone(),
|
|
|
|
compressor.should_update_upwards_ratios.clone(),
|
2022-07-24 02:24:02 +10:00
|
|
|
)),
|
2022-07-24 00:32:49 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl CompressorParams {
|
|
|
|
/// Create a new [`CompressorBankParams`] object with a prefix for all parameter names. Changing
|
|
|
|
/// any of the threshold or ratio parameters causes the passed atomics to be updated. These
|
|
|
|
/// should be taken from a [`CompressorBank`] so the parameters are linked to it.
|
|
|
|
pub fn new(
|
2022-07-24 02:23:03 +10:00
|
|
|
param_id_prefix: &'static str,
|
2022-07-24 00:32:49 +10:00
|
|
|
name_prefix: &str,
|
|
|
|
should_update_thresholds: Arc<AtomicBool>,
|
|
|
|
should_update_ratios: Arc<AtomicBool>,
|
|
|
|
) -> Self {
|
|
|
|
let set_update_thresholds =
|
|
|
|
Arc::new(move |_| should_update_thresholds.store(true, Ordering::SeqCst));
|
|
|
|
let set_update_ratios =
|
|
|
|
Arc::new(move |_| should_update_ratios.store(true, Ordering::SeqCst));
|
|
|
|
|
|
|
|
CompressorParams {
|
2022-07-24 02:23:03 +10:00
|
|
|
param_id_prefix,
|
|
|
|
|
2022-07-22 21:40:30 +10:00
|
|
|
// TODO: Set nicer default values for these things
|
|
|
|
// As explained above, these offsets are relative to the target curve
|
2022-07-24 00:32:49 +10:00
|
|
|
threshold_offset_db: FloatParam::new(
|
|
|
|
format!("{name_prefix} Offset"),
|
2022-07-25 03:01:28 +10:00
|
|
|
0.0,
|
2022-07-22 21:40:30 +10:00
|
|
|
FloatRange::Linear {
|
|
|
|
min: -50.0,
|
|
|
|
max: 50.0,
|
|
|
|
},
|
|
|
|
)
|
2022-07-24 00:32:49 +10:00
|
|
|
.with_callback(set_update_thresholds)
|
2022-07-22 21:40:30 +10:00
|
|
|
.with_unit(" dB")
|
|
|
|
.with_step_size(0.1),
|
2022-07-24 00:32:49 +10:00
|
|
|
ratio: FloatParam::new(
|
|
|
|
format!("{name_prefix} Ratio"),
|
2022-07-22 21:40:30 +10:00
|
|
|
1.0,
|
|
|
|
FloatRange::Skewed {
|
|
|
|
min: 1.0,
|
|
|
|
max: 300.0,
|
|
|
|
factor: FloatRange::skew_factor(-2.0),
|
|
|
|
},
|
|
|
|
)
|
2022-07-24 00:32:49 +10:00
|
|
|
.with_callback(set_update_ratios.clone())
|
2022-07-23 07:38:51 +10:00
|
|
|
.with_step_size(0.01)
|
|
|
|
.with_value_to_string(formatters::v2s_compression_ratio(2))
|
2022-07-22 21:40:30 +10:00
|
|
|
.with_string_to_value(formatters::s2v_compression_ratio()),
|
2022-07-24 00:32:49 +10:00
|
|
|
high_freq_ratio_rolloff: FloatParam::new(
|
|
|
|
format!("{name_prefix} Hi-Freq Rolloff"),
|
2022-07-25 03:01:28 +10:00
|
|
|
// TODO: Bit of a hacky way to set the default values differently for upwards and
|
|
|
|
// downwards compressors
|
2022-07-25 01:25:58 +10:00
|
|
|
if param_id_prefix == UPWARDS_NAME_PREFIX {
|
|
|
|
0.75
|
|
|
|
} else {
|
2022-07-25 01:43:31 +10:00
|
|
|
// These basically work in the opposite way
|
|
|
|
0.25
|
2022-07-25 01:25:58 +10:00
|
|
|
},
|
2022-07-24 00:32:49 +10:00
|
|
|
FloatRange::Linear { min: 0.0, max: 1.0 },
|
2022-07-22 21:40:30 +10:00
|
|
|
)
|
2022-07-24 00:32:49 +10:00
|
|
|
.with_callback(set_update_ratios)
|
|
|
|
.with_unit("%")
|
|
|
|
.with_value_to_string(formatters::v2s_f32_percentage(0))
|
|
|
|
.with_string_to_value(formatters::s2v_f32_percentage()),
|
|
|
|
knee_width_db: FloatParam::new(
|
|
|
|
format!("{name_prefix} Knee"),
|
2022-07-25 01:25:58 +10:00
|
|
|
6.0,
|
2022-07-22 21:45:09 +10:00
|
|
|
FloatRange::Skewed {
|
|
|
|
min: 0.0,
|
|
|
|
max: 36.0,
|
|
|
|
factor: FloatRange::skew_factor(-1.0),
|
|
|
|
},
|
|
|
|
)
|
|
|
|
.with_unit(" dB")
|
|
|
|
.with_step_size(0.1),
|
2022-07-22 21:40:30 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-07-23 00:54:55 +10:00
|
|
|
|
|
|
|
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 {
|
2022-07-23 01:29:23 +10:00
|
|
|
should_update_downwards_thresholds: Arc::new(AtomicBool::new(true)),
|
|
|
|
should_update_upwards_thresholds: Arc::new(AtomicBool::new(true)),
|
|
|
|
should_update_downwards_ratios: Arc::new(AtomicBool::new(true)),
|
|
|
|
should_update_upwards_ratios: Arc::new(AtomicBool::new(true)),
|
|
|
|
|
2022-07-23 02:00:29 +10:00
|
|
|
log2_freqs: Vec::with_capacity(complex_buffer_len),
|
|
|
|
|
2022-07-23 01:29:23 +10:00
|
|
|
downwards_thresholds: Vec::with_capacity(complex_buffer_len),
|
2022-07-24 22:15:55 +10:00
|
|
|
downwards_knee_starts: Vec::with_capacity(complex_buffer_len),
|
|
|
|
downwards_knee_ends: Vec::with_capacity(complex_buffer_len),
|
2022-07-23 04:44:36 +10:00
|
|
|
downwards_ratio_recips: Vec::with_capacity(complex_buffer_len),
|
2022-07-24 22:15:55 +10:00
|
|
|
|
2022-07-23 07:48:07 +10:00
|
|
|
upwards_thresholds: Vec::with_capacity(complex_buffer_len),
|
2022-07-24 22:15:55 +10:00
|
|
|
upwards_knee_starts: Vec::with_capacity(complex_buffer_len),
|
|
|
|
upwards_knee_ends: Vec::with_capacity(complex_buffer_len),
|
2022-07-23 04:44:36 +10:00
|
|
|
upwards_ratio_recips: Vec::with_capacity(complex_buffer_len),
|
2022-07-23 00:54:55 +10:00
|
|
|
|
|
|
|
envelopes: vec![Vec::with_capacity(complex_buffer_len); num_channels],
|
2022-07-25 23:07:21 +10:00
|
|
|
sidechain_spectrum_magnitudes: vec![
|
|
|
|
Vec::with_capacity(complex_buffer_len);
|
|
|
|
num_channels
|
|
|
|
],
|
2022-07-23 03:28:55 +10:00
|
|
|
window_size: 0,
|
|
|
|
sample_rate: 1.0,
|
2022-07-23 00:54:55 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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;
|
|
|
|
|
2022-07-23 02:00:29 +10:00
|
|
|
self.log2_freqs
|
|
|
|
.reserve_exact(complex_buffer_len.saturating_sub(self.log2_freqs.len()));
|
|
|
|
|
2022-07-23 01:29:23 +10:00
|
|
|
self.downwards_thresholds
|
|
|
|
.reserve_exact(complex_buffer_len.saturating_sub(self.downwards_thresholds.len()));
|
2022-07-23 04:44:36 +10:00
|
|
|
self.downwards_ratio_recips
|
|
|
|
.reserve_exact(complex_buffer_len.saturating_sub(self.downwards_ratio_recips.len()));
|
2022-07-24 22:15:55 +10:00
|
|
|
self.downwards_knee_starts
|
|
|
|
.reserve_exact(complex_buffer_len.saturating_sub(self.downwards_knee_starts.len()));
|
|
|
|
self.downwards_knee_ends
|
|
|
|
.reserve_exact(complex_buffer_len.saturating_sub(self.downwards_knee_ends.len()));
|
|
|
|
|
2022-07-23 07:48:07 +10:00
|
|
|
self.upwards_thresholds
|
|
|
|
.reserve_exact(complex_buffer_len.saturating_sub(self.upwards_thresholds.len()));
|
2022-07-23 04:44:36 +10:00
|
|
|
self.upwards_ratio_recips
|
|
|
|
.reserve_exact(complex_buffer_len.saturating_sub(self.upwards_ratio_recips.len()));
|
2022-07-24 22:15:55 +10:00
|
|
|
self.upwards_knee_starts
|
|
|
|
.reserve_exact(complex_buffer_len.saturating_sub(self.upwards_knee_starts.len()));
|
|
|
|
self.upwards_knee_ends
|
|
|
|
.reserve_exact(complex_buffer_len.saturating_sub(self.upwards_knee_ends.len()));
|
2022-07-23 01:29:23 +10:00
|
|
|
|
2022-07-23 00:54:55 +10:00
|
|
|
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()));
|
|
|
|
}
|
2022-07-25 23:07:21 +10:00
|
|
|
|
|
|
|
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()));
|
|
|
|
}
|
2022-07-23 00:54:55 +10:00
|
|
|
}
|
|
|
|
|
2022-07-23 02:00:29 +10:00
|
|
|
/// Resize the number of compressors to match the current window size. Also precomputes the
|
|
|
|
/// 2-log frequencies for each bin.
|
2022-07-23 00:54:55 +10:00
|
|
|
///
|
|
|
|
/// If the window size is larger than the maximum window size, then this will allocate.
|
2022-07-23 02:00:29 +10:00
|
|
|
pub fn resize(&mut self, buffer_config: &BufferConfig, window_size: usize) {
|
2022-07-23 00:54:55 +10:00
|
|
|
let complex_buffer_len = window_size / 2 + 1;
|
|
|
|
|
2022-07-23 02:00:29 +10:00
|
|
|
// These 2-log frequencies are needed when updating the compressor parameters, so we'll just
|
|
|
|
// precompute them to avoid having to repeat the same expensive computations all the time
|
|
|
|
self.log2_freqs.resize(complex_buffer_len, 0.0);
|
2022-07-25 02:25:46 +10:00
|
|
|
// The first one should always stay at zero, `0.0f32.log2() == NaN`.
|
|
|
|
for (i, log2_freq) in self.log2_freqs.iter_mut().enumerate().skip(1) {
|
2022-07-23 02:00:29 +10:00
|
|
|
let freq = (i as f32 / window_size as f32) * buffer_config.sample_rate;
|
|
|
|
*log2_freq = freq.log2();
|
|
|
|
}
|
|
|
|
|
2022-07-23 01:29:23 +10:00
|
|
|
self.downwards_thresholds.resize(complex_buffer_len, 1.0);
|
2022-07-23 04:44:36 +10:00
|
|
|
self.downwards_ratio_recips.resize(complex_buffer_len, 1.0);
|
2022-07-24 22:15:55 +10:00
|
|
|
self.downwards_knee_starts.resize(complex_buffer_len, 1.0);
|
|
|
|
self.downwards_knee_ends.resize(complex_buffer_len, 1.0);
|
|
|
|
|
2022-07-23 07:48:07 +10:00
|
|
|
self.upwards_thresholds.resize(complex_buffer_len, 1.0);
|
2022-07-23 04:44:36 +10:00
|
|
|
self.upwards_ratio_recips.resize(complex_buffer_len, 1.0);
|
2022-07-24 22:15:55 +10:00
|
|
|
self.upwards_knee_starts.resize(complex_buffer_len, 1.0);
|
|
|
|
self.upwards_knee_ends.resize(complex_buffer_len, 1.0);
|
2022-07-23 01:29:23 +10:00
|
|
|
|
2022-07-23 00:54:55 +10:00
|
|
|
for envelopes in self.envelopes.iter_mut() {
|
|
|
|
envelopes.resize(complex_buffer_len, 0.0);
|
|
|
|
}
|
|
|
|
|
2022-07-25 23:07:21 +10:00
|
|
|
for magnitudes in self.sidechain_spectrum_magnitudes.iter_mut() {
|
|
|
|
magnitudes.resize(complex_buffer_len, 0.0);
|
|
|
|
}
|
|
|
|
|
2022-07-23 03:28:55 +10:00
|
|
|
self.window_size = window_size;
|
|
|
|
self.sample_rate = buffer_config.sample_rate;
|
|
|
|
|
2022-07-23 00:54:55 +10:00
|
|
|
// The compressors need to be updated on the next processing cycle
|
2022-07-23 01:29:23 +10:00
|
|
|
self.should_update_downwards_thresholds
|
|
|
|
.store(true, Ordering::SeqCst);
|
|
|
|
self.should_update_upwards_thresholds
|
|
|
|
.store(true, Ordering::SeqCst);
|
|
|
|
self.should_update_downwards_ratios
|
|
|
|
.store(true, Ordering::SeqCst);
|
|
|
|
self.should_update_upwards_ratios
|
|
|
|
.store(true, Ordering::SeqCst);
|
2022-07-23 00:54:55 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Clear out the envelope followers.
|
|
|
|
pub fn reset(&mut self) {
|
|
|
|
for envelopes in self.envelopes.iter_mut() {
|
|
|
|
envelopes.fill(0.0);
|
|
|
|
}
|
2022-07-25 23:07:21 +10:00
|
|
|
|
|
|
|
// Sidechain data doesn't need to be reset as it will be overwritten immediately before use
|
2022-07-23 00:54:55 +10:00
|
|
|
}
|
|
|
|
|
2022-07-23 03:28:55 +10:00
|
|
|
/// Apply the magnitude compression to a buffer of FFT bins. The compressors are first updated
|
|
|
|
/// if needed. The overlap amount is needed to compute the effective sample rate. The
|
|
|
|
/// `skip_bins_below` argument is used to avoid compressing DC bins, or the neighbouring bins
|
|
|
|
/// the DC signal may have been convolved into because of the Hann window function.
|
|
|
|
pub fn process(
|
|
|
|
&mut self,
|
|
|
|
buffer: &mut [Complex32],
|
2022-07-23 03:41:06 +10:00
|
|
|
channel_idx: usize,
|
2022-07-24 00:03:21 +10:00
|
|
|
params: &SpectralCompressorParams,
|
2022-07-23 03:28:55 +10:00
|
|
|
overlap_times: usize,
|
|
|
|
skip_bins_below: usize,
|
|
|
|
) {
|
2022-07-25 23:07:21 +10:00
|
|
|
nih_debug_assert_eq!(buffer.len(), self.log2_freqs.len());
|
2022-07-23 03:28:55 +10:00
|
|
|
|
|
|
|
self.update_if_needed(params);
|
2022-07-23 03:41:06 +10:00
|
|
|
self.update_envelopes(buffer, channel_idx, params, overlap_times, skip_bins_below);
|
2022-07-26 00:35:34 +10:00
|
|
|
match params.threshold.mode.value() {
|
|
|
|
ThresholdMode::Internal => self.compress(buffer, channel_idx, params, skip_bins_below),
|
|
|
|
ThresholdMode::Sidechain => {
|
|
|
|
self.compress_sidechain(buffer, channel_idx, params, skip_bins_below)
|
|
|
|
}
|
|
|
|
};
|
2022-07-23 03:41:06 +10:00
|
|
|
}
|
|
|
|
|
2022-07-25 23:07:21 +10:00
|
|
|
/// 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);
|
|
|
|
}
|
|
|
|
|
2022-07-23 03:41:06 +10:00
|
|
|
/// Update the envelope followers based on the bin magnetudes.
|
|
|
|
fn update_envelopes(
|
|
|
|
&mut self,
|
|
|
|
buffer: &mut [Complex32],
|
|
|
|
channel_idx: usize,
|
2022-07-24 00:03:21 +10:00
|
|
|
params: &SpectralCompressorParams,
|
2022-07-23 03:41:06 +10:00
|
|
|
overlap_times: usize,
|
|
|
|
skip_bins_below: usize,
|
|
|
|
) {
|
2022-07-23 03:28:55 +10:00
|
|
|
// The coefficient the old envelope value is multiplied by when the current rectified sample
|
|
|
|
// value is above the envelope's value. The 0 to 1 step response retains 36.8% of the old
|
|
|
|
// value after the attack time has elapsed, and current value is 63.2% of the way towards 1.
|
|
|
|
// The effective sample rate needs to compensate for the periodic nature of the STFT
|
|
|
|
// operation. Since with a 2048 sample window and 4x overlap, you'd run this function once
|
|
|
|
// for every 512 samples.
|
|
|
|
let effective_sample_rate =
|
|
|
|
self.sample_rate / (self.window_size as f32 / overlap_times as f32);
|
2022-07-24 00:03:21 +10:00
|
|
|
let attack_old_t = if params.global.compressor_attack_ms.value == 0.0 {
|
2022-07-23 04:11:15 +10:00
|
|
|
0.0
|
|
|
|
} else {
|
2022-07-24 00:03:21 +10:00
|
|
|
(-1.0 / (params.global.compressor_attack_ms.value / 1000.0 * effective_sample_rate))
|
|
|
|
.exp()
|
2022-07-23 04:11:15 +10:00
|
|
|
};
|
2022-07-23 03:41:06 +10:00
|
|
|
let attack_new_t = 1.0 - attack_old_t;
|
|
|
|
// The same as `attack_old_t`, but for the release phase of the envelope follower
|
2022-07-24 00:03:21 +10:00
|
|
|
let release_old_t = if params.global.compressor_release_ms.value == 0.0 {
|
2022-07-23 04:11:15 +10:00
|
|
|
0.0
|
|
|
|
} else {
|
2022-07-24 00:03:21 +10:00
|
|
|
(-1.0 / (params.global.compressor_release_ms.value / 1000.0 * effective_sample_rate))
|
|
|
|
.exp()
|
2022-07-23 04:11:15 +10:00
|
|
|
};
|
2022-07-23 03:41:06 +10:00
|
|
|
let release_new_t = 1.0 - release_old_t;
|
|
|
|
|
|
|
|
for (bin, envelope) in buffer
|
|
|
|
.iter()
|
|
|
|
.zip(self.envelopes[channel_idx].iter_mut())
|
|
|
|
.skip(skip_bins_below)
|
|
|
|
{
|
|
|
|
let magnitude = bin.norm();
|
|
|
|
if *envelope > magnitude {
|
|
|
|
// Release stage
|
|
|
|
*envelope = (release_old_t * *envelope) + (release_new_t * magnitude);
|
|
|
|
} else {
|
|
|
|
// Attack stage
|
|
|
|
*envelope = (attack_old_t * *envelope) + (attack_new_t * magnitude);
|
|
|
|
}
|
|
|
|
}
|
2022-07-23 03:28:55 +10:00
|
|
|
}
|
|
|
|
|
2022-07-25 23:07:21 +10:00
|
|
|
/// 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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-23 07:44:42 +10:00
|
|
|
/// Actually do the thing. [`Self::update_envelopes()`] must have been called before calling
|
|
|
|
/// this.
|
2022-07-26 00:06:55 +10:00
|
|
|
///
|
|
|
|
/// # Panics
|
|
|
|
///
|
|
|
|
/// Panics if the buffer does not have the same length as the one that was passed to the last
|
|
|
|
/// `resize()` call.
|
2022-07-23 07:44:42 +10:00
|
|
|
fn compress(
|
|
|
|
&self,
|
|
|
|
buffer: &mut [Complex32],
|
|
|
|
channel_idx: usize,
|
2022-07-24 00:03:21 +10:00
|
|
|
params: &SpectralCompressorParams,
|
2022-07-23 07:44:42 +10:00
|
|
|
skip_bins_below: usize,
|
|
|
|
) {
|
|
|
|
// Well I'm not sure at all why this scaling works, but it does. With higher knee
|
|
|
|
// bandwidths, the middle values needs to be pushed more towards the post-knee threshold
|
2022-07-25 00:56:01 +10:00
|
|
|
// than with lower knee values. These scaling factors are used as exponents.
|
2022-07-23 07:44:42 +10:00
|
|
|
let downwards_knee_scaling_factor =
|
2022-07-25 23:56:30 +10:00
|
|
|
compute_knee_scaling_factor(params.compressors.downwards.knee_width_db.value);
|
2022-07-25 00:56:01 +10:00
|
|
|
// Note the square root here, since the curve needs to go the other way for the upwards
|
2022-07-25 23:56:30 +10:00
|
|
|
// version
|
2022-07-23 07:44:42 +10:00
|
|
|
let upwards_knee_scaling_factor =
|
2022-07-25 23:56:30 +10:00
|
|
|
compute_knee_scaling_factor(params.compressors.upwards.knee_width_db.value).sqrt();
|
2022-07-23 07:44:42 +10:00
|
|
|
|
2022-07-26 00:06:55 +10:00
|
|
|
assert!(self.downwards_thresholds.len() == buffer.len());
|
|
|
|
assert!(self.downwards_ratio_recips.len() == buffer.len());
|
|
|
|
assert!(self.downwards_knee_starts.len() == buffer.len());
|
|
|
|
assert!(self.downwards_knee_ends.len() == buffer.len());
|
|
|
|
assert!(self.upwards_thresholds.len() == buffer.len());
|
|
|
|
assert!(self.upwards_ratio_recips.len() == buffer.len());
|
|
|
|
assert!(self.upwards_knee_starts.len() == buffer.len());
|
|
|
|
assert!(self.upwards_knee_ends.len() == buffer.len());
|
|
|
|
for (bin_idx, (bin, envelope)) in buffer
|
2022-07-23 07:44:42 +10:00
|
|
|
.iter_mut()
|
|
|
|
.zip(self.envelopes[channel_idx].iter())
|
2022-07-26 00:06:55 +10:00
|
|
|
.enumerate()
|
2022-07-23 07:44:42 +10:00
|
|
|
.skip(skip_bins_below)
|
|
|
|
{
|
|
|
|
// This works by computing a scaling factor, and then scaling the bin magnitudes by that.
|
|
|
|
let mut scale = 1.0;
|
|
|
|
|
|
|
|
// All compression happens in the linear domain to save a logarithm
|
2022-07-26 00:06:55 +10:00
|
|
|
// SAFETY: These sizes were asserted above
|
|
|
|
let downwards_threshold = unsafe { self.downwards_thresholds.get_unchecked(bin_idx) };
|
|
|
|
let downwards_ratio_recip =
|
|
|
|
unsafe { self.downwards_ratio_recips.get_unchecked(bin_idx) };
|
|
|
|
let downwards_knee_start = unsafe { self.downwards_knee_starts.get_unchecked(bin_idx) };
|
|
|
|
let downwards_knee_end = unsafe { self.downwards_knee_ends.get_unchecked(bin_idx) };
|
2022-07-23 07:44:42 +10:00
|
|
|
if *downwards_ratio_recip != 1.0 {
|
2022-07-25 23:45:10 +10:00
|
|
|
scale *= compress_downwards(
|
|
|
|
*envelope,
|
|
|
|
*downwards_threshold,
|
|
|
|
*downwards_ratio_recip,
|
|
|
|
*downwards_knee_start,
|
|
|
|
*downwards_knee_end,
|
|
|
|
downwards_knee_scaling_factor,
|
|
|
|
);
|
2022-07-23 07:44:42 +10:00
|
|
|
}
|
|
|
|
|
2022-07-25 00:56:01 +10:00
|
|
|
// Upwards compression should not happen when the signal is _too_ quiet as we'd only be
|
|
|
|
// amplifying noise
|
2022-07-26 00:06:55 +10:00
|
|
|
let upwards_threshold = unsafe { self.upwards_thresholds.get_unchecked(bin_idx) };
|
|
|
|
let upwards_ratio_recip = unsafe { self.upwards_ratio_recips.get_unchecked(bin_idx) };
|
|
|
|
let upwards_knee_start = unsafe { self.upwards_knee_starts.get_unchecked(bin_idx) };
|
|
|
|
let upwards_knee_end = unsafe { self.upwards_knee_ends.get_unchecked(bin_idx) };
|
2022-07-25 00:56:01 +10:00
|
|
|
if *upwards_ratio_recip != 1.0 && *envelope > 1e-6 {
|
2022-07-25 23:45:10 +10:00
|
|
|
scale *= compress_upwards(
|
|
|
|
*envelope,
|
|
|
|
*upwards_threshold,
|
|
|
|
*upwards_ratio_recip,
|
|
|
|
*upwards_knee_start,
|
|
|
|
*upwards_knee_end,
|
|
|
|
upwards_knee_scaling_factor,
|
|
|
|
);
|
2022-07-25 00:56:01 +10:00
|
|
|
}
|
2022-07-23 07:44:42 +10:00
|
|
|
|
|
|
|
*bin *= scale;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-26 00:35:34 +10:00
|
|
|
/// The same as [`compress()`][Self::compress()], but multiplying the threshold and knee values
|
|
|
|
/// with the sidehcain gains.
|
|
|
|
///
|
|
|
|
/// # Panics
|
|
|
|
///
|
|
|
|
/// Panics if the buffer does not have the same length as the one that was passed to the last
|
|
|
|
/// `resize()` call.
|
|
|
|
fn compress_sidechain(
|
|
|
|
&self,
|
|
|
|
buffer: &mut [Complex32],
|
|
|
|
channel_idx: usize,
|
|
|
|
params: &SpectralCompressorParams,
|
|
|
|
skip_bins_below: usize,
|
|
|
|
) {
|
|
|
|
// See `compress` for more details
|
|
|
|
let downwards_knee_scaling_factor =
|
|
|
|
compute_knee_scaling_factor(params.compressors.downwards.knee_width_db.value);
|
|
|
|
let upwards_knee_scaling_factor =
|
|
|
|
compute_knee_scaling_factor(params.compressors.upwards.knee_width_db.value).sqrt();
|
|
|
|
|
|
|
|
// For the channel linking
|
|
|
|
let num_channels = self.sidechain_spectrum_magnitudes.len() as f32;
|
|
|
|
let other_channels_t = params.threshold.sc_channel_link.value / num_channels;
|
|
|
|
let this_channel_t = 1.0 - (other_channels_t * (num_channels - 1.0));
|
|
|
|
|
|
|
|
assert!(self.sidechain_spectrum_magnitudes[channel_idx].len() == buffer.len());
|
|
|
|
assert!(self.downwards_thresholds.len() == buffer.len());
|
|
|
|
assert!(self.downwards_ratio_recips.len() == buffer.len());
|
|
|
|
assert!(self.downwards_knee_starts.len() == buffer.len());
|
|
|
|
assert!(self.downwards_knee_ends.len() == buffer.len());
|
|
|
|
assert!(self.upwards_thresholds.len() == buffer.len());
|
|
|
|
assert!(self.upwards_ratio_recips.len() == buffer.len());
|
|
|
|
assert!(self.upwards_knee_starts.len() == buffer.len());
|
|
|
|
assert!(self.upwards_knee_ends.len() == buffer.len());
|
|
|
|
for (bin_idx, (bin, envelope)) in buffer
|
|
|
|
.iter_mut()
|
|
|
|
.zip(self.envelopes[channel_idx].iter())
|
|
|
|
.enumerate()
|
|
|
|
.skip(skip_bins_below)
|
|
|
|
{
|
|
|
|
// The idea here is that we scale the compressor thresholds/knee values by the sidechain
|
|
|
|
// signal, thus sort of creating a dynamic multiband compressor
|
|
|
|
let sidechain_scale: f32 = self
|
|
|
|
.sidechain_spectrum_magnitudes
|
|
|
|
.iter()
|
|
|
|
.enumerate()
|
|
|
|
.map(|(sidechain_channel_idx, magnitudes)| {
|
|
|
|
let t = if sidechain_channel_idx == channel_idx {
|
|
|
|
this_channel_t
|
|
|
|
} else {
|
|
|
|
other_channels_t
|
|
|
|
};
|
|
|
|
|
|
|
|
unsafe { magnitudes.get_unchecked(bin_idx) * t }
|
|
|
|
})
|
2022-07-26 00:43:15 +10:00
|
|
|
.sum::<f32>()
|
|
|
|
// The thresholds may never reach zero as they are used in divisions
|
|
|
|
.max(f32::EPSILON);
|
2022-07-26 00:35:34 +10:00
|
|
|
|
|
|
|
let mut scale = 1.0;
|
|
|
|
|
|
|
|
// Notice how the threshold and knee values are scaled here
|
|
|
|
let downwards_threshold =
|
|
|
|
unsafe { self.downwards_thresholds.get_unchecked(bin_idx) * sidechain_scale };
|
|
|
|
let downwards_ratio_recip =
|
|
|
|
unsafe { self.downwards_ratio_recips.get_unchecked(bin_idx) };
|
|
|
|
let downwards_knee_start =
|
|
|
|
unsafe { self.downwards_knee_starts.get_unchecked(bin_idx) * sidechain_scale };
|
|
|
|
let downwards_knee_end =
|
|
|
|
unsafe { self.downwards_knee_ends.get_unchecked(bin_idx) * sidechain_scale };
|
|
|
|
if *downwards_ratio_recip != 1.0 {
|
|
|
|
scale *= compress_downwards(
|
|
|
|
*envelope,
|
|
|
|
downwards_threshold,
|
|
|
|
*downwards_ratio_recip,
|
|
|
|
downwards_knee_start,
|
|
|
|
downwards_knee_end,
|
|
|
|
downwards_knee_scaling_factor,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
let upwards_threshold =
|
|
|
|
unsafe { self.upwards_thresholds.get_unchecked(bin_idx) * sidechain_scale };
|
|
|
|
let upwards_ratio_recip = unsafe { self.upwards_ratio_recips.get_unchecked(bin_idx) };
|
|
|
|
let upwards_knee_start =
|
|
|
|
unsafe { self.upwards_knee_starts.get_unchecked(bin_idx) * sidechain_scale };
|
|
|
|
let upwards_knee_end =
|
|
|
|
unsafe { self.upwards_knee_ends.get_unchecked(bin_idx) * sidechain_scale };
|
|
|
|
if *upwards_ratio_recip != 1.0 && *envelope > 1e-6 {
|
|
|
|
scale *= compress_upwards(
|
|
|
|
*envelope,
|
|
|
|
upwards_threshold,
|
|
|
|
*upwards_ratio_recip,
|
|
|
|
upwards_knee_start,
|
|
|
|
upwards_knee_end,
|
|
|
|
upwards_knee_scaling_factor,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
*bin *= scale;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-23 00:54:55 +10:00
|
|
|
/// 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.
|
2022-07-24 00:03:21 +10:00
|
|
|
fn update_if_needed(&mut self, params: &SpectralCompressorParams) {
|
2022-07-23 02:25:52 +10:00
|
|
|
// The threshold curve is a polynomial in log-log (decibels-octaves) space. The reuslt from
|
|
|
|
// evaluating this needs to be converted to linear gain for the compressors.
|
2022-07-24 00:03:21 +10:00
|
|
|
let intercept = params.threshold.threshold_db.value;
|
2022-07-23 02:25:52 +10:00
|
|
|
// The cheeky 3 additional dB/octave attenuation is to match pink noise with the default
|
2022-07-25 22:24:00 +10:00
|
|
|
// settings. When using sidechaining we explicitly don't want this because the curve should
|
|
|
|
// be a flat offset to the sidechain input at the default settings.
|
|
|
|
let slope = match params.threshold.mode.value() {
|
|
|
|
ThresholdMode::Internal => params.threshold.curve_slope.value - 3.0,
|
|
|
|
ThresholdMode::Sidechain => params.threshold.curve_slope.value,
|
|
|
|
};
|
2022-07-24 00:03:21 +10:00
|
|
|
let curve = params.threshold.curve_curve.value;
|
|
|
|
let log2_center_freq = params.threshold.center_frequency.value.log2();
|
2022-07-23 02:25:52 +10:00
|
|
|
|
2022-07-24 00:32:49 +10:00
|
|
|
let downwards_high_freq_ratio_rolloff =
|
|
|
|
params.compressors.downwards.high_freq_ratio_rolloff.value;
|
|
|
|
let upwards_high_freq_ratio_rolloff =
|
|
|
|
params.compressors.upwards.high_freq_ratio_rolloff.value;
|
2022-07-23 02:38:47 +10:00
|
|
|
let log2_nyquist_freq = self
|
|
|
|
.log2_freqs
|
|
|
|
.last()
|
|
|
|
.expect("The CompressorBank has not yet been resized");
|
|
|
|
|
2022-07-23 02:25:52 +10:00
|
|
|
if self
|
|
|
|
.should_update_downwards_thresholds
|
|
|
|
.compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
|
|
|
|
.is_ok()
|
|
|
|
{
|
2022-07-24 00:32:49 +10:00
|
|
|
let intercept = intercept + params.compressors.downwards.threshold_offset_db.value;
|
2022-07-24 22:15:55 +10:00
|
|
|
for ((log2_freq, threshold), (knee_start, knee_end)) in self
|
2022-07-23 02:25:52 +10:00
|
|
|
.log2_freqs
|
|
|
|
.iter()
|
|
|
|
.zip(self.downwards_thresholds.iter_mut())
|
2022-07-24 22:15:55 +10:00
|
|
|
.zip(
|
|
|
|
self.downwards_knee_starts
|
|
|
|
.iter_mut()
|
|
|
|
.zip(self.downwards_knee_ends.iter_mut()),
|
|
|
|
)
|
2022-07-23 02:25:52 +10:00
|
|
|
{
|
2022-07-23 08:03:22 +10:00
|
|
|
let offset = log2_freq - log2_center_freq;
|
2022-07-23 02:25:52 +10:00
|
|
|
let threshold_db = intercept + (slope * offset) + (curve * offset * offset);
|
2022-07-24 22:15:55 +10:00
|
|
|
let knee_start_db =
|
|
|
|
threshold_db - (params.compressors.downwards.knee_width_db.value / 2.0);
|
|
|
|
let knee_end_db =
|
|
|
|
threshold_db + (params.compressors.downwards.knee_width_db.value / 2.0);
|
|
|
|
|
|
|
|
// This threshold must never reach zero as it's used in divisions to get a gain ratio
|
2022-07-23 07:36:04 +10:00
|
|
|
// above the threshold
|
|
|
|
*threshold = util::db_to_gain(threshold_db).max(f32::EPSILON);
|
2022-07-24 22:15:55 +10:00
|
|
|
*knee_start = util::db_to_gain(knee_start_db).max(f32::EPSILON);
|
|
|
|
*knee_end = util::db_to_gain(knee_end_db).max(f32::EPSILON);
|
2022-07-23 02:25:52 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if self
|
|
|
|
.should_update_upwards_thresholds
|
|
|
|
.compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
|
|
|
|
.is_ok()
|
|
|
|
{
|
2022-07-24 00:32:49 +10:00
|
|
|
let intercept = intercept + params.compressors.upwards.threshold_offset_db.value;
|
2022-07-24 22:15:55 +10:00
|
|
|
for ((log2_freq, threshold), (knee_start, knee_end)) in self
|
2022-07-23 02:25:52 +10:00
|
|
|
.log2_freqs
|
|
|
|
.iter()
|
|
|
|
.zip(self.upwards_thresholds.iter_mut())
|
2022-07-24 22:15:55 +10:00
|
|
|
.zip(
|
|
|
|
self.upwards_knee_starts
|
|
|
|
.iter_mut()
|
|
|
|
.zip(self.upwards_knee_ends.iter_mut()),
|
|
|
|
)
|
2022-07-23 02:25:52 +10:00
|
|
|
{
|
2022-07-23 08:03:22 +10:00
|
|
|
let offset = log2_freq - log2_center_freq;
|
2022-07-23 02:25:52 +10:00
|
|
|
let threshold_db = intercept + (slope * offset) + (curve * offset * offset);
|
2022-07-24 22:15:55 +10:00
|
|
|
let knee_start_db =
|
|
|
|
threshold_db - (params.compressors.upwards.knee_width_db.value / 2.0);
|
|
|
|
let knee_end_db =
|
|
|
|
threshold_db + (params.compressors.upwards.knee_width_db.value / 2.0);
|
|
|
|
|
2022-07-23 07:36:04 +10:00
|
|
|
*threshold = util::db_to_gain(threshold_db).max(f32::EPSILON);
|
2022-07-24 22:15:55 +10:00
|
|
|
*knee_start = util::db_to_gain(knee_start_db).max(f32::EPSILON);
|
|
|
|
*knee_end = util::db_to_gain(knee_end_db).max(f32::EPSILON);
|
2022-07-23 02:25:52 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-23 02:38:47 +10:00
|
|
|
if self
|
|
|
|
.should_update_downwards_ratios
|
|
|
|
.compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
|
|
|
|
.is_ok()
|
|
|
|
{
|
|
|
|
// If the high-frequency rolloff is enabled then higher frequency bins will have their
|
|
|
|
// ratios reduced to reduce harshness. This follows the octave scale.
|
2022-07-24 00:32:49 +10:00
|
|
|
let target_ratio_recip = params.compressors.downwards.ratio.value.recip();
|
|
|
|
if downwards_high_freq_ratio_rolloff == 0.0 {
|
2022-07-23 04:44:36 +10:00
|
|
|
self.downwards_ratio_recips.fill(target_ratio_recip);
|
2022-07-23 02:38:47 +10:00
|
|
|
} else {
|
2022-07-23 04:44:36 +10:00
|
|
|
for (log2_freq, ratio) in self
|
|
|
|
.log2_freqs
|
|
|
|
.iter()
|
|
|
|
.zip(self.downwards_ratio_recips.iter_mut())
|
2022-07-23 02:38:47 +10:00
|
|
|
{
|
|
|
|
let octave_fraction = log2_freq / log2_nyquist_freq;
|
2022-07-24 00:32:49 +10:00
|
|
|
let rolloff_t = octave_fraction * downwards_high_freq_ratio_rolloff;
|
2022-07-23 07:22:39 +10:00
|
|
|
// If the octave fraction times the rolloff amount is high, then this should get
|
|
|
|
// closer to `high_freq_ratio_rolloff` (which is in [0, 1]).
|
|
|
|
*ratio = (target_ratio_recip * (1.0 - rolloff_t)) + rolloff_t;
|
2022-07-23 02:38:47 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if self
|
|
|
|
.should_update_upwards_ratios
|
|
|
|
.compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
|
|
|
|
.is_ok()
|
|
|
|
{
|
2022-07-24 00:32:49 +10:00
|
|
|
let target_ratio_recip = params.compressors.upwards.ratio.value.recip();
|
|
|
|
if upwards_high_freq_ratio_rolloff == 0.0 {
|
2022-07-23 04:44:36 +10:00
|
|
|
self.upwards_ratio_recips.fill(target_ratio_recip);
|
2022-07-23 02:38:47 +10:00
|
|
|
} else {
|
2022-07-23 04:44:36 +10:00
|
|
|
for (log2_freq, ratio) in self
|
|
|
|
.log2_freqs
|
|
|
|
.iter()
|
|
|
|
.zip(self.upwards_ratio_recips.iter_mut())
|
2022-07-23 02:38:47 +10:00
|
|
|
{
|
|
|
|
let octave_fraction = log2_freq / log2_nyquist_freq;
|
2022-07-24 00:32:49 +10:00
|
|
|
let rolloff_t = octave_fraction * upwards_high_freq_ratio_rolloff;
|
2022-07-23 07:22:39 +10:00
|
|
|
*ratio = (target_ratio_recip * (1.0 - rolloff_t)) + rolloff_t;
|
2022-07-23 02:38:47 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-07-23 00:54:55 +10:00
|
|
|
}
|
|
|
|
}
|
2022-07-25 23:45:10 +10:00
|
|
|
|
2022-07-25 23:56:30 +10:00
|
|
|
/// Get the knee scaling factor for converting a linear `[0, 1]` knee range into the correct curve
|
|
|
|
/// for the soft knee. This is used to blend between compression at the knee start to compression at
|
|
|
|
/// the actual threshold. For upwards compression this needs an additional square root.
|
|
|
|
fn compute_knee_scaling_factor(downwards_knee_width_db: f32) -> f32 {
|
|
|
|
((downwards_knee_width_db * 2.0) + 2.0).log2() - 1.0
|
|
|
|
}
|
|
|
|
|
2022-07-25 23:45:10 +10:00
|
|
|
/// Get the compression scaling factor for downwards compression with the supplied parameters. The
|
|
|
|
/// input signal can be multiplied by this factor to get the compressed output signal. All
|
|
|
|
/// parameters are linear gain values.
|
|
|
|
fn compress_downwards(
|
|
|
|
envelope: f32,
|
|
|
|
threshold: f32,
|
|
|
|
ratio_recip: f32,
|
|
|
|
knee_start: f32,
|
|
|
|
knee_end: f32,
|
|
|
|
knee_scaling_factor: f32,
|
|
|
|
) -> f32 {
|
|
|
|
// The soft-knee option will fade in the compression curve when reaching the knee
|
|
|
|
// start until it mtaches the hard-knee curve at the knee-end
|
|
|
|
if envelope >= knee_end {
|
|
|
|
// Because we're working in the linear domain, we care about the ratio between
|
|
|
|
// the threshold and the envelope's current value. And log-space division
|
|
|
|
// becomes linear-space exponentiation by the reciprocal, or taking the nth
|
|
|
|
// root.
|
|
|
|
let threshold_ratio = envelope / threshold;
|
|
|
|
threshold_ratio.powf(ratio_recip) / threshold_ratio
|
|
|
|
} else if envelope >= knee_start {
|
|
|
|
// When the knee width is set to 0 dB, `downwards_knee_start ==
|
|
|
|
// downwards_knee_end` and this branch is never hit
|
|
|
|
let linear_knee_width = knee_end - knee_start;
|
|
|
|
let raw_knee_t = (envelope - knee_start) / linear_knee_width;
|
|
|
|
nih_debug_assert!((0.0..=1.0).contains(&raw_knee_t));
|
|
|
|
|
|
|
|
// TODO: Apart from a small discontinuety in the derivative/slope at the start
|
|
|
|
// of the knee this equation does exactly what you'd expect it to, but it
|
|
|
|
// feels a bit weird. Should probably look for a cleaner way to calculate
|
|
|
|
// this soft knee in linear-space at some point.
|
|
|
|
let knee_t = (1.0 - raw_knee_t).powf(knee_scaling_factor);
|
|
|
|
nih_debug_assert!((0.0..=1.0).contains(&knee_t));
|
|
|
|
|
|
|
|
// We'll linearly interpolate between compression at the knee start and at the
|
|
|
|
// actual threshold based on `knee_t`
|
|
|
|
let knee_ratio = envelope / knee_start;
|
|
|
|
let threshold_ratio = envelope / threshold;
|
|
|
|
(knee_t * (knee_ratio.powf(ratio_recip) / knee_ratio))
|
|
|
|
+ ((1.0 - knee_t) * (threshold_ratio.powf(ratio_recip) / threshold_ratio))
|
|
|
|
} else {
|
|
|
|
1.0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Get the compression scaling factor for upwards compression with the supplied parameters. The
|
|
|
|
/// input signal can be multiplied by this factor to get the compressed output signal. All
|
|
|
|
/// parameters are linear gain values.
|
|
|
|
fn compress_upwards(
|
|
|
|
envelope: f32,
|
|
|
|
threshold: f32,
|
|
|
|
ratio_recip: f32,
|
|
|
|
knee_start: f32,
|
|
|
|
knee_end: f32,
|
|
|
|
knee_scaling_factor: f32,
|
|
|
|
) -> f32 {
|
|
|
|
// This goes the other way around compared to the downwards compression
|
|
|
|
if envelope <= knee_start {
|
|
|
|
// Notice how these ratios are reversed here
|
|
|
|
let threshold_ratio = threshold / envelope;
|
|
|
|
threshold_ratio / threshold_ratio.powf(ratio_recip)
|
|
|
|
} else if envelope <= knee_end {
|
|
|
|
// When the knee width is set to 0 dB, `upwards_knee_start == upwards_knee_end`
|
|
|
|
// and this branch is never hit
|
|
|
|
let linear_knee_width = knee_end - knee_start;
|
|
|
|
let raw_knee_t = (envelope - knee_start) / linear_knee_width;
|
|
|
|
nih_debug_assert!((0.0..=1.0).contains(&raw_knee_t));
|
|
|
|
|
|
|
|
// TODO: Some note the downwards version
|
|
|
|
let knee_t = (1.0 - raw_knee_t).powf(knee_scaling_factor);
|
|
|
|
nih_debug_assert!((0.0..=1.0).contains(&knee_t));
|
|
|
|
|
|
|
|
// The ratios are again inverted here compared to the downwards version
|
|
|
|
let knee_ratio = knee_start / envelope;
|
|
|
|
let threshold_ratio = threshold / envelope;
|
|
|
|
(knee_t * (knee_ratio / knee_ratio.powf(ratio_recip)))
|
|
|
|
+ ((1.0 - knee_t) * (threshold_ratio / threshold_ratio.powf(ratio_recip)))
|
|
|
|
} else {
|
|
|
|
1.0
|
|
|
|
}
|
|
|
|
}
|