Change SC to work in the decibel domain
This makes the soft-knee function differentiable and the performance should in theory be slightly higher when using these fast gain<->dB conversion functions. This also fixes the high-frequency rolloff not working correctly for the downwards compressors.
This commit is contained in:
parent
92ce737000
commit
1e83d29fab
|
@ -33,6 +33,7 @@ const ENVELOPE_INIT_VALUE: f32 = std::f32::consts::FRAC_1_SQRT_2 / 8.0;
|
|||
|
||||
/// The target frequency for the high frequency ratio rolloff. This is fixed to prevent Spectral
|
||||
/// Compressor from getting brighter as the sample rate increases.
|
||||
#[allow(unused)]
|
||||
const HIGH_FREQ_RATIO_ROLLOFF_FREQUENCY: f32 = 22_050.0;
|
||||
const HIGH_FREQ_RATIO_ROLLOFF_FREQUENCY_LOG2: f32 = 14.428_491;
|
||||
|
||||
|
@ -50,33 +51,38 @@ pub struct CompressorBank {
|
|||
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>,
|
||||
/// If set, then the parameters for the downwards compression soft knee parabola 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_knee_parabolas: Arc<AtomicBool>,
|
||||
/// The same as `should_update_downwards_knee_parabolas`, but for upwards compression.
|
||||
pub should_update_upwards_knee_parabolas: Arc<AtomicBool>,
|
||||
|
||||
/// 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>,
|
||||
|
||||
/// Downwards compressor thresholds, in linear space.
|
||||
downwards_thresholds: Vec<f32>,
|
||||
/// 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>,
|
||||
/// 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>,
|
||||
/// Downwards compressor thresholds, in decibels.
|
||||
downwards_thresholds_db: Vec<f32>,
|
||||
/// The ratios for the the downwards compressors. 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.
|
||||
downwards_ratios: Vec<f32>,
|
||||
/// The knee is modelled as a parabola using the formula `x + a * (x + b)^2`. This is `a` in
|
||||
/// that equation. The formula is taken from the Digital Dynamic Range Compressor Design paper
|
||||
/// by Dimitrios Giannoulis et. al.
|
||||
downwards_knee_parabola_scale: Vec<f32>,
|
||||
/// `b` in the equation from `downwards_knee_parabola_scale`.
|
||||
downwards_knee_parabola_intercept: Vec<f32>,
|
||||
|
||||
/// Upwards compressor thresholds, in linear space.
|
||||
upwards_thresholds: Vec<f32>,
|
||||
/// 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>,
|
||||
/// The same as `downwards_ratio_recipss`, but for the upwards compression.
|
||||
upwards_ratio_recips: Vec<f32>,
|
||||
/// Upwards compressor thresholds, in decibels.
|
||||
upwards_thresholds_db: Vec<f32>,
|
||||
/// The same as `downwards_ratios`, but for the upwards compression.
|
||||
upwards_ratios: Vec<f32>,
|
||||
/// `downwards_knee_parabola_scale`, but for the upwards compressors.
|
||||
upwards_knee_parabola_scale: Vec<f32>,
|
||||
/// `downwards_knee_parabola_intercept`, but for the upwards compressors.
|
||||
upwards_knee_parabola_intercept: Vec<f32>,
|
||||
|
||||
/// The current envelope value for this bin, in linear space. Indexed by
|
||||
/// `[channel_idx][compressor_idx]`.
|
||||
|
@ -187,15 +193,22 @@ pub struct CompressorParams {
|
|||
|
||||
impl ThresholdParams {
|
||||
/// Create a new [`ThresholdParams`] object. Changing any of the threshold parameters causes the
|
||||
/// passed compressor bank's thresholds to be updated.
|
||||
/// passed compressor bank's thresholds and knee parabolas to be updated.
|
||||
pub fn new(compressor_bank: &CompressorBank) -> Self {
|
||||
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 should_update_downwards_knee_parabolas = compressor_bank
|
||||
.should_update_downwards_knee_parabolas
|
||||
.clone();
|
||||
let should_update_upwards_knee_parabolas =
|
||||
compressor_bank.should_update_upwards_knee_parabolas.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);
|
||||
should_update_downwards_knee_parabolas.store(true, Ordering::SeqCst);
|
||||
should_update_upwards_knee_parabolas.store(true, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
ThresholdParams {
|
||||
|
@ -270,19 +283,21 @@ impl ThresholdParams {
|
|||
|
||||
impl CompressorBankParams {
|
||||
/// 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.
|
||||
/// `compressor`. Changing the ratio, threshold, and knee parameters will cause the compressor
|
||||
/// to recompute its values on the next processing cycle.
|
||||
pub fn new(compressor: &CompressorBank) -> Self {
|
||||
CompressorBankParams {
|
||||
downwards: Arc::new(CompressorParams::new(
|
||||
DOWNWARDS_NAME_PREFIX,
|
||||
compressor.should_update_downwards_thresholds.clone(),
|
||||
compressor.should_update_downwards_ratios.clone(),
|
||||
compressor.should_update_downwards_knee_parabolas.clone(),
|
||||
)),
|
||||
upwards: Arc::new(CompressorParams::new(
|
||||
UPWARDS_NAME_PREFIX,
|
||||
compressor.should_update_upwards_thresholds.clone(),
|
||||
compressor.should_update_upwards_ratios.clone(),
|
||||
compressor.should_update_upwards_knee_parabolas.clone(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
@ -290,17 +305,31 @@ impl CompressorBankParams {
|
|||
|
||||
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.
|
||||
/// any of the threshold, ratio, or knee 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(
|
||||
name_prefix: &str,
|
||||
should_update_thresholds: Arc<AtomicBool>,
|
||||
should_update_ratios: Arc<AtomicBool>,
|
||||
should_update_knee_parabolas: 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));
|
||||
let set_update_thresholds = Arc::new({
|
||||
let should_update_knee_parabolas = should_update_knee_parabolas.clone();
|
||||
move |_| {
|
||||
should_update_thresholds.store(true, Ordering::SeqCst);
|
||||
should_update_knee_parabolas.store(true, Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
let set_update_ratios = Arc::new({
|
||||
let should_update_knee_parabolas = should_update_knee_parabolas.clone();
|
||||
move |_| {
|
||||
should_update_ratios.store(true, Ordering::SeqCst);
|
||||
should_update_knee_parabolas.store(true, Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
let set_update_knee_parabolas = Arc::new(move |_| {
|
||||
should_update_knee_parabolas.store(true, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
CompressorParams {
|
||||
// TODO: Set nicer default values for these things
|
||||
|
@ -354,6 +383,7 @@ impl CompressorParams {
|
|||
factor: FloatRange::skew_factor(-1.0),
|
||||
},
|
||||
)
|
||||
.with_callback(set_update_knee_parabolas)
|
||||
.with_unit(" dB")
|
||||
.with_step_size(0.1),
|
||||
}
|
||||
|
@ -371,18 +401,20 @@ impl CompressorBank {
|
|||
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)),
|
||||
should_update_downwards_knee_parabolas: Arc::new(AtomicBool::new(true)),
|
||||
should_update_upwards_knee_parabolas: Arc::new(AtomicBool::new(true)),
|
||||
|
||||
log2_freqs: Vec::with_capacity(complex_buffer_len),
|
||||
|
||||
downwards_thresholds: Vec::with_capacity(complex_buffer_len),
|
||||
downwards_knee_starts: Vec::with_capacity(complex_buffer_len),
|
||||
downwards_knee_ends: Vec::with_capacity(complex_buffer_len),
|
||||
downwards_ratio_recips: Vec::with_capacity(complex_buffer_len),
|
||||
downwards_thresholds_db: Vec::with_capacity(complex_buffer_len),
|
||||
downwards_ratios: Vec::with_capacity(complex_buffer_len),
|
||||
downwards_knee_parabola_scale: Vec::with_capacity(complex_buffer_len),
|
||||
downwards_knee_parabola_intercept: Vec::with_capacity(complex_buffer_len),
|
||||
|
||||
upwards_thresholds: Vec::with_capacity(complex_buffer_len),
|
||||
upwards_knee_starts: Vec::with_capacity(complex_buffer_len),
|
||||
upwards_knee_ends: Vec::with_capacity(complex_buffer_len),
|
||||
upwards_ratio_recips: Vec::with_capacity(complex_buffer_len),
|
||||
upwards_thresholds_db: Vec::with_capacity(complex_buffer_len),
|
||||
upwards_ratios: Vec::with_capacity(complex_buffer_len),
|
||||
upwards_knee_parabola_scale: Vec::with_capacity(complex_buffer_len),
|
||||
upwards_knee_parabola_intercept: Vec::with_capacity(complex_buffer_len),
|
||||
|
||||
envelopes: vec![Vec::with_capacity(complex_buffer_len); num_channels],
|
||||
sidechain_spectrum_magnitudes: vec![
|
||||
|
@ -402,23 +434,27 @@ impl CompressorBank {
|
|||
self.log2_freqs
|
||||
.reserve_exact(complex_buffer_len.saturating_sub(self.log2_freqs.len()));
|
||||
|
||||
self.downwards_thresholds
|
||||
.reserve_exact(complex_buffer_len.saturating_sub(self.downwards_thresholds.len()));
|
||||
self.downwards_ratio_recips
|
||||
.reserve_exact(complex_buffer_len.saturating_sub(self.downwards_ratio_recips.len()));
|
||||
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()));
|
||||
self.downwards_thresholds_db
|
||||
.reserve_exact(complex_buffer_len.saturating_sub(self.downwards_thresholds_db.len()));
|
||||
self.downwards_ratios
|
||||
.reserve_exact(complex_buffer_len.saturating_sub(self.downwards_ratios.len()));
|
||||
self.downwards_knee_parabola_scale.reserve_exact(
|
||||
complex_buffer_len.saturating_sub(self.downwards_knee_parabola_scale.len()),
|
||||
);
|
||||
self.downwards_knee_parabola_intercept.reserve_exact(
|
||||
complex_buffer_len.saturating_sub(self.downwards_knee_parabola_intercept.len()),
|
||||
);
|
||||
|
||||
self.upwards_thresholds
|
||||
.reserve_exact(complex_buffer_len.saturating_sub(self.upwards_thresholds.len()));
|
||||
self.upwards_ratio_recips
|
||||
.reserve_exact(complex_buffer_len.saturating_sub(self.upwards_ratio_recips.len()));
|
||||
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()));
|
||||
self.upwards_thresholds_db
|
||||
.reserve_exact(complex_buffer_len.saturating_sub(self.upwards_thresholds_db.len()));
|
||||
self.upwards_ratios
|
||||
.reserve_exact(complex_buffer_len.saturating_sub(self.upwards_ratios.len()));
|
||||
self.upwards_knee_parabola_scale.reserve_exact(
|
||||
complex_buffer_len.saturating_sub(self.upwards_knee_parabola_scale.len()),
|
||||
);
|
||||
self.upwards_knee_parabola_intercept.reserve_exact(
|
||||
complex_buffer_len.saturating_sub(self.upwards_knee_parabola_intercept.len()),
|
||||
);
|
||||
|
||||
self.envelopes.resize_with(num_channels, Vec::new);
|
||||
for envelopes in self.envelopes.iter_mut() {
|
||||
|
@ -448,15 +484,19 @@ impl CompressorBank {
|
|||
*log2_freq = freq.log2();
|
||||
}
|
||||
|
||||
self.downwards_thresholds.resize(complex_buffer_len, 1.0);
|
||||
self.downwards_ratio_recips.resize(complex_buffer_len, 1.0);
|
||||
self.downwards_knee_starts.resize(complex_buffer_len, 1.0);
|
||||
self.downwards_knee_ends.resize(complex_buffer_len, 1.0);
|
||||
self.downwards_thresholds_db.resize(complex_buffer_len, 1.0);
|
||||
self.downwards_ratios.resize(complex_buffer_len, 1.0);
|
||||
self.downwards_knee_parabola_scale
|
||||
.resize(complex_buffer_len, 1.0);
|
||||
self.downwards_knee_parabola_intercept
|
||||
.resize(complex_buffer_len, 1.0);
|
||||
|
||||
self.upwards_thresholds.resize(complex_buffer_len, 1.0);
|
||||
self.upwards_ratio_recips.resize(complex_buffer_len, 1.0);
|
||||
self.upwards_knee_starts.resize(complex_buffer_len, 1.0);
|
||||
self.upwards_knee_ends.resize(complex_buffer_len, 1.0);
|
||||
self.upwards_thresholds_db.resize(complex_buffer_len, 1.0);
|
||||
self.upwards_ratios.resize(complex_buffer_len, 1.0);
|
||||
self.upwards_knee_parabola_scale
|
||||
.resize(complex_buffer_len, 1.0);
|
||||
self.upwards_knee_parabola_intercept
|
||||
.resize(complex_buffer_len, 1.0);
|
||||
|
||||
for envelopes in self.envelopes.iter_mut() {
|
||||
envelopes.resize(complex_buffer_len, ENVELOPE_INIT_VALUE);
|
||||
|
@ -478,6 +518,10 @@ impl CompressorBank {
|
|||
.store(true, Ordering::SeqCst);
|
||||
self.should_update_upwards_ratios
|
||||
.store(true, Ordering::SeqCst);
|
||||
self.should_update_downwards_knee_parabolas
|
||||
.store(true, Ordering::SeqCst);
|
||||
self.should_update_upwards_knee_parabolas
|
||||
.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// Clear out the envelope followers.
|
||||
|
@ -664,24 +708,17 @@ impl CompressorBank {
|
|||
params: &SpectralCompressorParams,
|
||||
first_non_dc_bin: 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
|
||||
// than with lower knee values. These scaling factors are used as exponents.
|
||||
let downwards_knee_scaling_factor =
|
||||
compute_knee_scaling_factor(params.compressors.downwards.knee_width_db.value());
|
||||
// Note the square root here, since the curve needs to go the other way for the upwards
|
||||
// version
|
||||
let upwards_knee_scaling_factor =
|
||||
compute_knee_scaling_factor(params.compressors.upwards.knee_width_db.value()).sqrt();
|
||||
let downwards_knee_width_db = params.compressors.downwards.knee_width_db.value();
|
||||
let upwards_knee_width_db = params.compressors.upwards.knee_width_db.value();
|
||||
|
||||
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());
|
||||
assert!(self.downwards_thresholds_db.len() == buffer.len());
|
||||
assert!(self.downwards_ratios.len() == buffer.len());
|
||||
assert!(self.downwards_knee_parabola_scale.len() == buffer.len());
|
||||
assert!(self.downwards_knee_parabola_intercept.len() == buffer.len());
|
||||
assert!(self.upwards_thresholds_db.len() == buffer.len());
|
||||
assert!(self.upwards_ratios.len() == buffer.len());
|
||||
assert!(self.upwards_knee_parabola_scale.len() == buffer.len());
|
||||
assert!(self.upwards_knee_parabola_intercept.len() == buffer.len());
|
||||
// NOTE: In the sidechain compression mode these envelopes are computed from the sidechain
|
||||
// signal instead of the main input
|
||||
for (bin_idx, (bin, envelope)) in buffer
|
||||
|
@ -689,45 +726,58 @@ impl CompressorBank {
|
|||
.zip(self.envelopes[channel_idx].iter())
|
||||
.enumerate()
|
||||
{
|
||||
// This works by computing a scaling factor, and then scaling the bin magnitudes by that.
|
||||
let mut scale = 1.0;
|
||||
// We'll apply the transfer curve to the envelope signal, and then scale the complex
|
||||
// `bin` by the gain difference
|
||||
let envelope_db = util::gain_to_db_fast_epsilon(*envelope);
|
||||
|
||||
// All compression happens in the linear domain to save a logarithm
|
||||
// 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) };
|
||||
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 downwards_threshold_db =
|
||||
unsafe { self.downwards_thresholds_db.get_unchecked(bin_idx) };
|
||||
let downwards_ratio = unsafe { self.downwards_ratios.get_unchecked(bin_idx) };
|
||||
let downwards_knee_parabola_scale =
|
||||
unsafe { self.downwards_knee_parabola_scale.get_unchecked(bin_idx) };
|
||||
let downwards_knee_parabola_intercept = unsafe {
|
||||
self.downwards_knee_parabola_intercept
|
||||
.get_unchecked(bin_idx)
|
||||
};
|
||||
let downwards_compressed = compress_downwards(
|
||||
envelope_db,
|
||||
*downwards_threshold_db,
|
||||
*downwards_ratio,
|
||||
downwards_knee_width_db,
|
||||
*downwards_knee_parabola_scale,
|
||||
*downwards_knee_parabola_intercept,
|
||||
);
|
||||
}
|
||||
|
||||
// Upwards compression should not happen when the signal is _too_ quiet as we'd only be
|
||||
// amplifying noise
|
||||
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) };
|
||||
if bin_idx >= first_non_dc_bin && *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,
|
||||
);
|
||||
}
|
||||
// amplifying noise. We also don't want to amplify DC noise and super low frequencies.
|
||||
let upwards_threshold_db = unsafe { self.upwards_thresholds_db.get_unchecked(bin_idx) };
|
||||
let upwards_ratio = unsafe { self.upwards_ratios.get_unchecked(bin_idx) };
|
||||
let upwards_knee_parabola_scale =
|
||||
unsafe { self.upwards_knee_parabola_scale.get_unchecked(bin_idx) };
|
||||
let upwards_knee_parabola_intercept =
|
||||
unsafe { self.upwards_knee_parabola_intercept.get_unchecked(bin_idx) };
|
||||
let upwards_compressed = if bin_idx >= first_non_dc_bin
|
||||
&& *upwards_ratio != 1.0
|
||||
&& envelope_db > util::MINUS_INFINITY_DB
|
||||
{
|
||||
compress_upwards(
|
||||
envelope_db,
|
||||
*upwards_threshold_db,
|
||||
*upwards_ratio,
|
||||
upwards_knee_width_db,
|
||||
*upwards_knee_parabola_scale,
|
||||
*upwards_knee_parabola_intercept,
|
||||
)
|
||||
} else {
|
||||
envelope_db
|
||||
};
|
||||
|
||||
*bin *= scale;
|
||||
// If the comprssed output is -10 dBFS and the envelope follower was at -6 dBFS, then we
|
||||
// want to apply -4 dB of gain to the bin
|
||||
*bin *= util::db_to_gain_fast(
|
||||
downwards_compressed + upwards_compressed - (envelope_db * 2.0),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -745,11 +795,8 @@ impl CompressorBank {
|
|||
params: &SpectralCompressorParams,
|
||||
first_non_dc_bin: 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();
|
||||
let downwards_knee_width_db = params.compressors.downwards.knee_width_db.value();
|
||||
let upwards_knee_width_db = params.compressors.upwards.knee_width_db.value();
|
||||
|
||||
// For the channel linking
|
||||
let num_channels = self.sidechain_spectrum_magnitudes.len() as f32;
|
||||
|
@ -757,19 +804,21 @@ impl CompressorBank {
|
|||
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());
|
||||
assert!(self.downwards_thresholds_db.len() == buffer.len());
|
||||
assert!(self.downwards_ratios.len() == buffer.len());
|
||||
assert!(self.downwards_knee_parabola_scale.len() == buffer.len());
|
||||
assert!(self.downwards_knee_parabola_intercept.len() == buffer.len());
|
||||
assert!(self.upwards_thresholds_db.len() == buffer.len());
|
||||
assert!(self.upwards_ratios.len() == buffer.len());
|
||||
assert!(self.upwards_knee_parabola_scale.len() == buffer.len());
|
||||
assert!(self.upwards_knee_parabola_intercept.len() == buffer.len());
|
||||
for (bin_idx, (bin, envelope)) in buffer
|
||||
.iter_mut()
|
||||
.zip(self.envelopes[channel_idx].iter())
|
||||
.enumerate()
|
||||
{
|
||||
let envelope_db = util::gain_to_db_fast_epsilon(*envelope);
|
||||
|
||||
// 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
|
||||
|
@ -788,56 +837,60 @@ impl CompressorBank {
|
|||
.sum::<f32>()
|
||||
// The thresholds may never reach zero as they are used in divisions
|
||||
.max(f32::EPSILON);
|
||||
|
||||
let mut scale = 1.0;
|
||||
let sidechain_scale_db = util::gain_to_db_fast_epsilon(sidechain_scale);
|
||||
|
||||
// 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 downwards_threshold_db =
|
||||
unsafe { self.downwards_thresholds_db.get_unchecked(bin_idx) + sidechain_scale_db };
|
||||
let downwards_ratio = unsafe { self.downwards_ratios.get_unchecked(bin_idx) };
|
||||
let downwards_knee_parabola_scale =
|
||||
unsafe { self.downwards_knee_parabola_scale.get_unchecked(bin_idx) };
|
||||
let downwards_knee_parabola_intercept = unsafe {
|
||||
self.downwards_knee_parabola_intercept
|
||||
.get_unchecked(bin_idx)
|
||||
};
|
||||
let downwards_compressed = compress_downwards(
|
||||
envelope_db,
|
||||
downwards_threshold_db,
|
||||
*downwards_ratio,
|
||||
downwards_knee_width_db,
|
||||
*downwards_knee_parabola_scale,
|
||||
*downwards_knee_parabola_intercept,
|
||||
);
|
||||
}
|
||||
|
||||
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 bin_idx >= first_non_dc_bin && *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,
|
||||
let upwards_threshold_db =
|
||||
unsafe { self.upwards_thresholds_db.get_unchecked(bin_idx) + sidechain_scale_db };
|
||||
let upwards_ratio = unsafe { self.upwards_ratios.get_unchecked(bin_idx) };
|
||||
let upwards_knee_parabola_scale =
|
||||
unsafe { self.upwards_knee_parabola_scale.get_unchecked(bin_idx) };
|
||||
let upwards_knee_parabola_intercept =
|
||||
unsafe { self.upwards_knee_parabola_intercept.get_unchecked(bin_idx) };
|
||||
let upwards_compressed = if bin_idx >= first_non_dc_bin
|
||||
&& *upwards_ratio != 1.0
|
||||
&& envelope_db > util::MINUS_INFINITY_DB
|
||||
{
|
||||
compress_upwards(
|
||||
envelope_db,
|
||||
upwards_threshold_db,
|
||||
*upwards_ratio,
|
||||
upwards_knee_width_db,
|
||||
*upwards_knee_parabola_scale,
|
||||
*upwards_knee_parabola_intercept,
|
||||
)
|
||||
} else {
|
||||
envelope_db
|
||||
};
|
||||
|
||||
*bin *= util::db_to_gain_fast(
|
||||
downwards_compressed + upwards_compressed - (envelope_db * 2.0),
|
||||
);
|
||||
}
|
||||
|
||||
*bin *= scale;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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, params: &SpectralCompressorParams) {
|
||||
// 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.
|
||||
// The threshold curve is a polynomial in log-log (decibels-octaves) space
|
||||
let intercept = params.threshold.threshold_db.value();
|
||||
// The cheeky 3 additional dB/octave attenuation is to match pink noise with the default
|
||||
// settings. When using sidechaining we explicitly don't want this because the curve should
|
||||
|
@ -851,39 +904,19 @@ impl CompressorBank {
|
|||
let curve = params.threshold.curve_curve.value();
|
||||
let log2_center_freq = params.threshold.center_frequency.value().log2();
|
||||
|
||||
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();
|
||||
|
||||
if self
|
||||
.should_update_downwards_thresholds
|
||||
.compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
|
||||
.is_ok()
|
||||
{
|
||||
let intercept = intercept + params.compressors.downwards.threshold_offset_db.value();
|
||||
for ((log2_freq, threshold), (knee_start, knee_end)) in self
|
||||
for (log2_freq, threshold_db) in self
|
||||
.log2_freqs
|
||||
.iter()
|
||||
.zip(self.downwards_thresholds.iter_mut())
|
||||
.zip(
|
||||
self.downwards_knee_starts
|
||||
.iter_mut()
|
||||
.zip(self.downwards_knee_ends.iter_mut()),
|
||||
)
|
||||
.zip(self.downwards_thresholds_db.iter_mut())
|
||||
{
|
||||
let offset = log2_freq - log2_center_freq;
|
||||
let threshold_db = intercept + (slope * offset) + (curve * offset * offset);
|
||||
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
|
||||
// above the threshold
|
||||
*threshold = util::db_to_gain_fast(threshold_db).max(f32::EPSILON);
|
||||
*knee_start = util::db_to_gain_fast(knee_start_db).max(f32::EPSILON);
|
||||
*knee_end = util::db_to_gain_fast(knee_end_db).max(f32::EPSILON);
|
||||
*threshold_db = intercept + (slope * offset) + (curve * offset * offset);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -893,26 +926,13 @@ impl CompressorBank {
|
|||
.is_ok()
|
||||
{
|
||||
let intercept = intercept + params.compressors.upwards.threshold_offset_db.value();
|
||||
for ((log2_freq, threshold), (knee_start, knee_end)) in self
|
||||
for (log2_freq, threshold_db) in self
|
||||
.log2_freqs
|
||||
.iter()
|
||||
.zip(self.upwards_thresholds.iter_mut())
|
||||
.zip(
|
||||
self.upwards_knee_starts
|
||||
.iter_mut()
|
||||
.zip(self.upwards_knee_ends.iter_mut()),
|
||||
)
|
||||
.zip(self.upwards_thresholds_db.iter_mut())
|
||||
{
|
||||
let offset = log2_freq - log2_center_freq;
|
||||
let threshold_db = intercept + (slope * offset) + (curve * offset * offset);
|
||||
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);
|
||||
|
||||
*threshold = util::db_to_gain_fast(threshold_db);
|
||||
*knee_start = util::db_to_gain_fast(knee_start_db);
|
||||
*knee_end = util::db_to_gain_fast(knee_end_db);
|
||||
*threshold_db = intercept + (slope * offset) + (curve * offset * offset);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -922,22 +942,19 @@ impl CompressorBank {
|
|||
.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.
|
||||
// ratios reduced to reduce harshness. This follows the octave scale. It's easier to do
|
||||
// this cleanly using reciprocals.
|
||||
let target_ratio_recip = params.compressors.downwards.ratio.value().recip();
|
||||
if downwards_high_freq_ratio_rolloff == 0.0 {
|
||||
self.downwards_ratio_recips.fill(target_ratio_recip);
|
||||
} else {
|
||||
for (log2_freq, ratio) in self
|
||||
.log2_freqs
|
||||
.iter()
|
||||
.zip(self.downwards_ratio_recips.iter_mut())
|
||||
{
|
||||
let octave_fraction = log2_freq / HIGH_FREQ_RATIO_ROLLOFF_FREQUENCY;
|
||||
let downwards_high_freq_ratio_rolloff =
|
||||
params.compressors.downwards.high_freq_ratio_rolloff.value();
|
||||
for (log2_freq, ratio) in self.log2_freqs.iter().zip(self.downwards_ratios.iter_mut()) {
|
||||
let octave_fraction = log2_freq / HIGH_FREQ_RATIO_ROLLOFF_FREQUENCY_LOG2;
|
||||
let rolloff_t = octave_fraction * downwards_high_freq_ratio_rolloff;
|
||||
|
||||
// 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;
|
||||
}
|
||||
let ratio_recip = (target_ratio_recip * (1.0 - rolloff_t)) + rolloff_t;
|
||||
*ratio = ratio_recip.recip();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -947,108 +964,124 @@ impl CompressorBank {
|
|||
.is_ok()
|
||||
{
|
||||
let target_ratio_recip = params.compressors.upwards.ratio.value().recip();
|
||||
if upwards_high_freq_ratio_rolloff == 0.0 {
|
||||
self.upwards_ratio_recips.fill(target_ratio_recip);
|
||||
} else {
|
||||
for (log2_freq, ratio) in self
|
||||
.log2_freqs
|
||||
.iter()
|
||||
.zip(self.upwards_ratio_recips.iter_mut())
|
||||
{
|
||||
let upwards_high_freq_ratio_rolloff =
|
||||
params.compressors.upwards.high_freq_ratio_rolloff.value();
|
||||
for (log2_freq, ratio) in self.log2_freqs.iter().zip(self.upwards_ratios.iter_mut()) {
|
||||
let octave_fraction = log2_freq / HIGH_FREQ_RATIO_ROLLOFF_FREQUENCY_LOG2;
|
||||
let rolloff_t = octave_fraction * upwards_high_freq_ratio_rolloff;
|
||||
*ratio = (target_ratio_recip * (1.0 - rolloff_t)) + rolloff_t;
|
||||
|
||||
let ratio_recip = (target_ratio_recip * (1.0 - rolloff_t)) + rolloff_t;
|
||||
*ratio = ratio_recip.recip();
|
||||
}
|
||||
}
|
||||
|
||||
if self
|
||||
.should_update_downwards_knee_parabolas
|
||||
.compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
|
||||
.is_ok()
|
||||
{
|
||||
let downwards_knee_width_db = params.compressors.downwards.knee_width_db.value();
|
||||
for ((ratio, threshold_db), (knee_parabola_scale, knee_parambola_intercept)) in self
|
||||
.downwards_ratios
|
||||
.iter()
|
||||
.zip(self.downwards_thresholds_db.iter())
|
||||
.zip(
|
||||
self.downwards_knee_parabola_scale
|
||||
.iter_mut()
|
||||
.zip(self.downwards_knee_parabola_intercept.iter_mut()),
|
||||
)
|
||||
{
|
||||
// This is the formula from the Digital Dynamic Range Compressor Design paper by
|
||||
// Dimitrios Giannoulis et. al. These are `a` and `b` from the `x + a * (x + b)^2`
|
||||
// respectively used to compute the soft knee respectively.
|
||||
*knee_parabola_scale = if downwards_knee_width_db != 0.0 {
|
||||
(2.0 * downwards_knee_width_db * *ratio).recip()
|
||||
- (2.0 * downwards_knee_width_db).recip()
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
*knee_parambola_intercept = -threshold_db + (downwards_knee_width_db / 2.0);
|
||||
}
|
||||
}
|
||||
|
||||
if self
|
||||
.should_update_upwards_knee_parabolas
|
||||
.compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
|
||||
.is_ok()
|
||||
{
|
||||
let upwards_knee_width_db = params.compressors.upwards.knee_width_db.value();
|
||||
for ((ratio, threshold_db), (knee_parabola_scale, knee_parambola_intercept)) in self
|
||||
.upwards_ratios
|
||||
.iter()
|
||||
.zip(self.upwards_thresholds_db.iter())
|
||||
.zip(
|
||||
self.upwards_knee_parabola_scale
|
||||
.iter_mut()
|
||||
.zip(self.upwards_knee_parabola_intercept.iter_mut()),
|
||||
)
|
||||
{
|
||||
// For the upwards version the scale becomes negated
|
||||
*knee_parabola_scale = if upwards_knee_width_db != 0.0 {
|
||||
-((2.0 * upwards_knee_width_db * *ratio).recip()
|
||||
- (2.0 * upwards_knee_width_db).recip())
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
// And the `+ (knee/2)` becomes `- (knee/2)` in the intercept
|
||||
*knee_parambola_intercept = -threshold_db - (upwards_knee_width_db / 2.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// Apply downwards compression to the input with the supplied parameters. All values are in
|
||||
/// decibels.
|
||||
fn compress_downwards(
|
||||
envelope: f32,
|
||||
threshold: f32,
|
||||
ratio_recip: f32,
|
||||
knee_start: f32,
|
||||
knee_end: f32,
|
||||
knee_scaling_factor: f32,
|
||||
input_db: f32,
|
||||
threshold_db: f32,
|
||||
ratio: f32,
|
||||
knee_width_db: f32,
|
||||
knee_parabola_scale: f32,
|
||||
knee_parabola_intercept: 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))
|
||||
// The soft-knee option will fade in the compression curve when reaching the knee start until it
|
||||
// matches the hard-knee curve at the knee-end
|
||||
let knee_start_db = threshold_db - (knee_width_db / 2.0);
|
||||
let knee_end_db = threshold_db + (knee_width_db / 2.0);
|
||||
if input_db <= knee_start_db {
|
||||
input_db
|
||||
} else if input_db <= knee_end_db {
|
||||
// See the `knee_parabola_intercept` field documentation for the full formula. The entire
|
||||
// osft knee part can be skipped if `knee_width_db == 0.0`.
|
||||
let parabola_x = input_db + knee_parabola_intercept;
|
||||
input_db + (knee_parabola_scale * parabola_x * parabola_x)
|
||||
} else {
|
||||
1.0
|
||||
threshold_db + ((input_db - threshold_db) / ratio)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// Apply upwards compression to the input with the supplied parameters. All values are in
|
||||
/// decibels.
|
||||
fn compress_upwards(
|
||||
envelope: f32,
|
||||
threshold: f32,
|
||||
ratio_recip: f32,
|
||||
knee_start: f32,
|
||||
knee_end: f32,
|
||||
knee_scaling_factor: f32,
|
||||
input_db: f32,
|
||||
threshold_db: f32,
|
||||
ratio: f32,
|
||||
knee_width_db: f32,
|
||||
knee_parabola_scale: f32,
|
||||
knee_parabola_intercept: f32,
|
||||
) -> f32 {
|
||||
// We'll keep the terminology consistent, start is below the threshold, and end is above the
|
||||
// threshold
|
||||
let knee_start_db = threshold_db - (knee_width_db / 2.0);
|
||||
let knee_end_db = threshold_db + (knee_width_db / 2.0);
|
||||
|
||||
// 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)))
|
||||
if input_db >= knee_end_db {
|
||||
input_db
|
||||
} else if input_db >= knee_start_db {
|
||||
let parabola_x = input_db + knee_parabola_intercept;
|
||||
input_db + (knee_parabola_scale * parabola_x * parabola_x)
|
||||
} else {
|
||||
1.0
|
||||
threshold_db + ((input_db - threshold_db) / ratio)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue