init
This commit is contained in:
31
lib_audio_dsp/python/audio_dsp/dsp/drc/__init__.py
Normal file
31
lib_audio_dsp/python/audio_dsp/dsp/drc/__init__.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# Copyright 2024-2025 XMOS LIMITED.
|
||||
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
"""This sub-package contains all the dynamic range control (DRC)
|
||||
DSP components.
|
||||
"""
|
||||
|
||||
from audio_dsp.dsp.drc.drc import (
|
||||
compressor_limiter_base as compressor_limiter_base,
|
||||
envelope_detector_peak as envelope_detector_peak,
|
||||
envelope_detector_rms as envelope_detector_rms,
|
||||
limiter_peak as limiter_peak,
|
||||
hard_limiter_peak as hard_limiter_peak,
|
||||
limiter_rms as limiter_rms,
|
||||
compressor_rms as compressor_rms,
|
||||
compressor_rms_softknee as compressor_rms_softknee,
|
||||
clipper as clipper,
|
||||
)
|
||||
from audio_dsp.dsp.drc.stereo_compressor_limiter import (
|
||||
limiter_peak_stereo as limiter_peak_stereo,
|
||||
compressor_rms_stereo as compressor_rms_stereo,
|
||||
)
|
||||
|
||||
from audio_dsp.dsp.drc.sidechain import (
|
||||
compressor_rms_sidechain_mono as compressor_rms_sidechain_mono,
|
||||
compressor_rms_sidechain_stereo as compressor_rms_sidechain_stereo,
|
||||
)
|
||||
|
||||
from audio_dsp.dsp.drc.expander import (
|
||||
noise_gate as noise_gate,
|
||||
noise_suppressor_expander as noise_suppressor_expander,
|
||||
)
|
||||
1070
lib_audio_dsp/python/audio_dsp/dsp/drc/drc.py
Normal file
1070
lib_audio_dsp/python/audio_dsp/dsp/drc/drc.py
Normal file
File diff suppressed because it is too large
Load Diff
299
lib_audio_dsp/python/audio_dsp/dsp/drc/drc_utils.py
Normal file
299
lib_audio_dsp/python/audio_dsp/dsp/drc/drc_utils.py
Normal file
@@ -0,0 +1,299 @@
|
||||
# Copyright 2024-2025 XMOS LIMITED.
|
||||
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
"""Generic utilities for the dynamic range control DSP blocks."""
|
||||
|
||||
from audio_dsp.dsp import utils as utils
|
||||
import numpy as np
|
||||
from math import sqrt, isqrt
|
||||
import warnings
|
||||
|
||||
from audio_dsp.dsp.types import float32
|
||||
|
||||
|
||||
FLT_MIN = np.finfo(float).tiny
|
||||
|
||||
# Q format for the drc alphas and gains
|
||||
Q_alpha = 31
|
||||
|
||||
|
||||
def calculate_rms_threshold(threshold_db, Q_sig) -> tuple[float, int]:
|
||||
"""
|
||||
Calculate the linear RMS threshold in floating and fixed point from a
|
||||
target threshold in decibels.
|
||||
If the threshold is higher than representable in the fixed point
|
||||
format, it is saturated.
|
||||
"""
|
||||
return calculate_threshold(threshold_db, Q_sig, power=True)
|
||||
|
||||
|
||||
def calculate_peak_threshold(threshold_db, Q_sig) -> tuple[float, int]:
|
||||
"""
|
||||
Calculate the linear peak threshold in floating and fixed point from a
|
||||
target threshold in decibels.
|
||||
If the threshold is higher than representable in the fixed point
|
||||
format, it is saturated.
|
||||
"""
|
||||
return calculate_threshold(threshold_db, Q_sig, power=False)
|
||||
|
||||
|
||||
def calculate_threshold(threshold_db, Q_sig, power=False) -> tuple[float, int]:
|
||||
"""
|
||||
Calculate the linear threshold in floating and fixed point from a
|
||||
target threshold in decibels.
|
||||
If the threshold is higher than representable in the fixed point
|
||||
format, it is saturated.
|
||||
"""
|
||||
if power:
|
||||
threshold = utils.db_pow2gain(threshold_db)
|
||||
else:
|
||||
threshold = utils.db2gain(threshold_db)
|
||||
|
||||
threshold = utils.saturate_float(threshold, Q_sig)
|
||||
|
||||
if power:
|
||||
new_threshold_db = utils.db_pow(threshold)
|
||||
else:
|
||||
new_threshold_db = utils.db(threshold)
|
||||
|
||||
if threshold_db != new_threshold_db:
|
||||
warnings.warn(
|
||||
"Threshold %.2f not representable in Q format Q%d, saturating to %.2f"
|
||||
% (threshold_db, Q_sig, new_threshold_db),
|
||||
UserWarning,
|
||||
)
|
||||
|
||||
threshold_int = utils.float_to_fixed(threshold, Q_sig)
|
||||
|
||||
# this avoids division by zero for expanders
|
||||
threshold_int = max(1, threshold_int)
|
||||
|
||||
return threshold, threshold_int
|
||||
|
||||
|
||||
def alpha_from_time(attack_or_release_time, fs):
|
||||
"""
|
||||
Calculate the exponential moving average time constant from an
|
||||
attack/release time in seconds.
|
||||
|
||||
Attack times simplified from McNally, seem pretty close.
|
||||
Assumes the time constant of a digital filter is the -3 dB
|
||||
point where abs(H(z))**2 = 0.5.
|
||||
|
||||
This is also approximately the time constant of a first order
|
||||
system, `alpha = 1 - exp(-T/tau)`, where `T` is the sample period
|
||||
and `tau` is the time constant.
|
||||
|
||||
attack/release time can't be faster than the length of 2
|
||||
samples, and alpha can't be greater than 1. This function will
|
||||
saturate to those values.
|
||||
"""
|
||||
if attack_or_release_time < 0:
|
||||
warnings.warn(
|
||||
"Attack/release time must not be negative. For the fastest possible "
|
||||
"attack/release time, use zero. Time set to zero",
|
||||
UserWarning,
|
||||
)
|
||||
attack_or_release_time = 0
|
||||
|
||||
T = 1 / fs
|
||||
alpha = 2 * T / (attack_or_release_time + FLT_MIN)
|
||||
|
||||
if alpha > 1:
|
||||
alpha = 1
|
||||
warnings.warn(
|
||||
"Attack or release time too fast for sample rate, setting as fast as possible.",
|
||||
UserWarning,
|
||||
)
|
||||
|
||||
alpha_int = utils.int32(round(alpha * 2**31)) if alpha != 1.0 else utils.int32(2**31 - 1)
|
||||
|
||||
# This is possible if alpha > (4/fs)*(2**31), which is 49.7 hours @ 48kHz,
|
||||
# in which case you should probably use a lower sample rate.
|
||||
if alpha_int <= 0:
|
||||
warnings.warn(
|
||||
"alpha not > 0, this is possible if attack/release time > (4/fs)*(2**31)."
|
||||
"Setting alpha_int to 0 (no smoothing)",
|
||||
UserWarning,
|
||||
)
|
||||
alpha_int = 0
|
||||
|
||||
return alpha, alpha_int
|
||||
|
||||
|
||||
def rms_compressor_slope_from_ratio(ratio):
|
||||
"""Convert a compressor ratio to the slope, where the slope is
|
||||
defined as (1 - 1 / ratio) / 2.0. The division by 2 compensates for
|
||||
the RMS envelope detector returning the RMS².
|
||||
"""
|
||||
if ratio < 1:
|
||||
warnings.warn("Compressor ratio must be >= 1, setting ratio to 1", UserWarning)
|
||||
ratio = 1
|
||||
|
||||
slope = (1 - 1 / ratio) / 2.0
|
||||
slope_f32 = float32(slope)
|
||||
return slope, slope_f32
|
||||
|
||||
|
||||
def peak_expander_slope_from_ratio(ratio):
|
||||
"""Convert an expander ratio to the slope, where the slope is
|
||||
defined as (1 - ratio).
|
||||
"""
|
||||
if ratio < 1:
|
||||
warnings.warn("Expander ratio must be >= 1, setting ratio to 1", UserWarning)
|
||||
ratio = 1
|
||||
|
||||
slope = 1 - ratio
|
||||
slope_f32 = float32(slope)
|
||||
return slope, slope_f32
|
||||
|
||||
|
||||
def calc_ema_xcore(x, y, alpha):
|
||||
"""Calculate fixed-point exponential moving average, given that alpha is in Q_alpha format."""
|
||||
acc = int(x) << Q_alpha
|
||||
mul = utils.int32(y - x)
|
||||
acc += mul * alpha
|
||||
x = utils.int32_mult_sat_extract(acc, 1, Q_alpha)
|
||||
return x
|
||||
|
||||
|
||||
def apply_gain_xcore(sample: int, gain: int) -> int:
|
||||
"""Apply the gain to a sample using fixed-point math. Assumes that gain is in Q_alpha format."""
|
||||
acc = 1 << (Q_alpha - 1)
|
||||
acc += sample * gain
|
||||
y = utils.int32_mult_sat_extract(acc, 1, Q_alpha)
|
||||
return y
|
||||
|
||||
|
||||
def limiter_peak_gain_calc(envelope, threshold, slope=None):
|
||||
"""Calculate the float gain for the current sample."""
|
||||
new_gain = threshold / envelope
|
||||
new_gain = min(1, new_gain)
|
||||
return new_gain
|
||||
|
||||
|
||||
def limiter_peak_gain_calc_xcore(envelope_int, threshold_int, slope=None):
|
||||
"""Calculate the int gain for the current sample."""
|
||||
if threshold_int >= envelope_int:
|
||||
new_gain_int = utils.int32(0x7FFFFFFF)
|
||||
else:
|
||||
new_gain_int = int(threshold_int) << 31
|
||||
new_gain_int = utils.int32(new_gain_int // envelope_int)
|
||||
return new_gain_int
|
||||
|
||||
|
||||
def limiter_rms_gain_calc(envelope, threshold, slope=None):
|
||||
"""Calculate the float gain for the current sample.
|
||||
|
||||
Note that as the RMS envelope detector returns x**2, we need to
|
||||
sqrt the gain.
|
||||
|
||||
"""
|
||||
new_gain = sqrt(threshold / envelope)
|
||||
new_gain = min(1, new_gain)
|
||||
return new_gain
|
||||
|
||||
|
||||
def limiter_rms_gain_calc_xcore(envelope_int, threshold_int, slope=None):
|
||||
"""Calculate the int gain for the current sample.
|
||||
|
||||
Note that as the RMS envelope detector returns x**2, we need to
|
||||
sqrt the gain.
|
||||
|
||||
"""
|
||||
if threshold_int >= envelope_int:
|
||||
new_gain_int = utils.int32(0x7FFFFFFF)
|
||||
else:
|
||||
new_gain_int = int(threshold_int) << 31
|
||||
new_gain_int = utils.int32(new_gain_int // envelope_int)
|
||||
new_gain_int = utils.int32(isqrt(new_gain_int * (1 << 31)))
|
||||
return new_gain_int
|
||||
|
||||
|
||||
def compressor_rms_gain_calc(envelope, threshold, slope=None):
|
||||
"""Calculate the float gain for the current sample.
|
||||
|
||||
Note that as the RMS envelope detector returns x**2, we need to
|
||||
sqrt the gain. Slope is used instead of ratio to allow the gain
|
||||
calculation to avoid the log domain.
|
||||
|
||||
"""
|
||||
# if envelope below threshold, apply unity gain, otherwise scale
|
||||
# down
|
||||
new_gain = (threshold / envelope) ** slope
|
||||
new_gain = min(1, new_gain)
|
||||
return new_gain
|
||||
|
||||
|
||||
def compressor_rms_gain_calc_xcore(envelope_int, threshold_int, slope_f32=None):
|
||||
"""Calculate the int gain for the current sample.
|
||||
|
||||
Note that as the RMS envelope detector returns x**2, we need to
|
||||
sqrt the gain. Slope is used instead of ratio to allow the gain
|
||||
calculation to avoid the log domain.
|
||||
|
||||
"""
|
||||
# if envelope below threshold, apply unity gain, otherwise scale
|
||||
# down
|
||||
|
||||
if slope_f32 > float32(0) and threshold_int < envelope_int:
|
||||
new_gain_int = int(threshold_int) << 31
|
||||
new_gain_int = utils.int32(new_gain_int // envelope_int)
|
||||
new_gain_f32 = float32(new_gain_int * 2**-31) ** slope_f32
|
||||
# division result could be < 1 in int32, but ==1 in f32
|
||||
if new_gain_f32 >= 1.0:
|
||||
return utils.int32(0x7FFFFFFF)
|
||||
new_gain_int = (new_gain_f32 * float32(2**31)).as_int32()
|
||||
else:
|
||||
new_gain_int = utils.int32(0x7FFFFFFF)
|
||||
|
||||
return new_gain_int
|
||||
|
||||
|
||||
def noise_gate_gain_calc(envelope, threshold, slope=None):
|
||||
"""Calculate the float gain for the current sample."""
|
||||
if envelope < threshold:
|
||||
new_gain = 0
|
||||
else:
|
||||
new_gain = 1
|
||||
return new_gain
|
||||
|
||||
|
||||
def noise_gate_gain_calc_xcore(envelope_int, threshold_int, slope_int=None):
|
||||
"""Calculate the int gain for the current sample."""
|
||||
if envelope_int < threshold_int:
|
||||
new_gain_int = utils.int32(0)
|
||||
else:
|
||||
new_gain_int = utils.int32((1 << 31) - 1)
|
||||
return new_gain_int
|
||||
|
||||
|
||||
def noise_suppressor_expander_gain_calc(envelope, threshold, slope):
|
||||
"""Calculate the float gain for the current sample."""
|
||||
# if envelope above threshold, apply unity gain, otherwise scale
|
||||
# down
|
||||
new_gain = (threshold / envelope) ** slope
|
||||
new_gain = min(1, new_gain)
|
||||
return new_gain
|
||||
|
||||
|
||||
def noise_suppressor_expander_gain_calc_xcore(envelope_int, threshold_int, slope_f32):
|
||||
"""Calculate the int gain for the current sample."""
|
||||
# if envelope above threshold, apply unity gain, otherwise scale
|
||||
# down
|
||||
# note this is rearranged to (envelope / threshold) ** -slope in order
|
||||
# to be similar to the compressor implementation, which also allows
|
||||
# 1/threshold to be precomputed
|
||||
invt = utils.int64(((1 << 63) - 1) // threshold_int)
|
||||
if -slope_f32 > float32(0) and threshold_int > envelope_int:
|
||||
# this looks a bit scary, but as long as envelope < threshold,
|
||||
# it can't overflow
|
||||
new_gain_int = utils.int64(envelope_int * invt)
|
||||
new_gain_f32 = float32(new_gain_int * 2**-63) ** -slope_f32
|
||||
# division result could be < 1 in int32, but ==1 in f32
|
||||
if new_gain_f32 >= 1.0:
|
||||
return utils.int32(0x7FFFFFFF)
|
||||
new_gain_int = (new_gain_f32 * float32(2**31)).as_int32()
|
||||
else:
|
||||
new_gain_int = utils.int32(0x7FFFFFFF)
|
||||
|
||||
return new_gain_int
|
||||
250
lib_audio_dsp/python/audio_dsp/dsp/drc/expander.py
Normal file
250
lib_audio_dsp/python/audio_dsp/dsp/drc/expander.py
Normal file
@@ -0,0 +1,250 @@
|
||||
# Copyright 2024-2025 XMOS LIMITED.
|
||||
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
"""The expander DSP blocks."""
|
||||
|
||||
import numpy as np
|
||||
|
||||
from audio_dsp.dsp import utils as utils
|
||||
from audio_dsp.dsp import generic as dspg
|
||||
import audio_dsp.dsp.drc.drc_utils as drcu
|
||||
from audio_dsp.dsp.types import float32
|
||||
from audio_dsp.dsp.drc.drc import compressor_limiter_base, peak_compressor_limiter_base
|
||||
|
||||
FLT_MIN = np.finfo(float).tiny
|
||||
|
||||
|
||||
class expander_base(compressor_limiter_base):
|
||||
"""
|
||||
A base class for expanders (including noise suppressors).
|
||||
|
||||
Expanders differ from compressors in that they reduce the level of a
|
||||
signal when it falls below a threshold (instead of above). The
|
||||
attack time is still defined as how quickly the gain is changed
|
||||
after the envelope exceeds the threshold.
|
||||
|
||||
Expanders, noise gates and noise suppressors have very similar
|
||||
structures, with differences in the gain calculation. All the shared
|
||||
code and parameters are calculated in this base class.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
n_chans : int
|
||||
number of parallel channels the expander runs on. The
|
||||
channels are expanded separately, only the constant
|
||||
parameters are shared.
|
||||
threshold_db : float
|
||||
Threshold in decibels below which expansion occurs. This cannot
|
||||
be greater than the maximum value representable in
|
||||
Q_SIG format, and will saturate to that value.
|
||||
attack_t : float
|
||||
Attack time of the expander in seconds. This cannot be
|
||||
faster than 2/fs seconds, and saturates to that
|
||||
value. Exceptionally large attack times may converge to zero.
|
||||
release_t: float
|
||||
Release time of the expander in seconds. This cannot
|
||||
be faster than 2/fs seconds, and saturates to that
|
||||
value. Exceptionally large release times may converge to zero.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
threshold : float
|
||||
Value below which expanding occurs for floating point
|
||||
processing.
|
||||
threshold_int : int
|
||||
Value below which expanding occurs for int32 fixed
|
||||
point processing.
|
||||
|
||||
"""
|
||||
|
||||
def reset_state(self):
|
||||
"""Reset the envelope detector to 1 and the gain to 1, so the
|
||||
gate starts off.
|
||||
"""
|
||||
if self.env_detector is not None:
|
||||
self.env_detector.envelope = [1] * self.n_chans
|
||||
self.env_detector.envelope_int = [utils.int32(2**self.Q_sig - 1)] * self.n_chans
|
||||
self.gain = [1] * self.n_chans
|
||||
self.gain_int = [2**31 - 1] * self.n_chans
|
||||
|
||||
def process(self, sample, channel=0):
|
||||
"""
|
||||
Update the envelope for a signal, then calculate and apply the
|
||||
required gain for expanding, using floating point
|
||||
maths.
|
||||
|
||||
Take one new sample and return the expanded sample.
|
||||
Input should be scaled with 0 dB = 1.0.
|
||||
|
||||
"""
|
||||
# get envelope from envelope detector
|
||||
envelope = self.env_detector.process(sample, channel)
|
||||
# avoid /0
|
||||
envelope = np.maximum(envelope, np.finfo(float).tiny)
|
||||
|
||||
# calculate the gain, this function should be defined by the
|
||||
# child class
|
||||
new_gain = self.gain_calc(envelope, self.threshold, self.slope) # type: ignore : base inits to None
|
||||
|
||||
# see if we're attacking or decaying
|
||||
if new_gain < self.gain[channel]:
|
||||
# below threshold, gain < unity
|
||||
alpha = self.release_alpha
|
||||
else:
|
||||
# above threshold, gain = unity
|
||||
alpha = self.attack_alpha
|
||||
|
||||
# do exponential moving average
|
||||
self.gain[channel] = ((1 - alpha) * self.gain[channel]) + (alpha * new_gain)
|
||||
|
||||
# apply gain to input
|
||||
y = self.gain[channel] * sample
|
||||
return y, new_gain, envelope
|
||||
|
||||
def process_xcore(self, sample, channel=0, return_int=False):
|
||||
"""
|
||||
Update the envelope for a signal, then calculate and apply the
|
||||
required gain for expanding, using int32 fixed point
|
||||
maths.
|
||||
|
||||
Take one new sample and return the expanded sample.
|
||||
Input should be scaled with 0 dB = 1.0.
|
||||
|
||||
"""
|
||||
sample_int = utils.float_to_fixed(sample, self.Q_sig)
|
||||
# get envelope from envelope detector
|
||||
envelope_int = self.env_detector.process_xcore(sample_int, channel)
|
||||
# avoid /0
|
||||
envelope_int = max(envelope_int, 1)
|
||||
|
||||
# if envelope below threshold, apply unity gain, otherwise scale
|
||||
# down
|
||||
new_gain_int = self.gain_calc_xcore(envelope_int, self.threshold_int, self.slope_f32) # pyright: ignore : base inits to None
|
||||
|
||||
# see if we're attacking or decaying
|
||||
if new_gain_int < self.gain_int[channel]:
|
||||
# below threshold, gain < unity
|
||||
alpha = self.release_alpha_int
|
||||
else:
|
||||
# above threshold, gain = unity
|
||||
alpha = self.attack_alpha_int
|
||||
|
||||
# do exponential moving average
|
||||
self.gain_int[channel] = drcu.calc_ema_xcore(self.gain_int[channel], new_gain_int, alpha)
|
||||
|
||||
# apply gain
|
||||
y = drcu.apply_gain_xcore(sample_int, self.gain_int[channel])
|
||||
|
||||
if return_int:
|
||||
return y, new_gain_int, envelope_int
|
||||
else:
|
||||
return (
|
||||
utils.fixed_to_float(y, self.Q_sig),
|
||||
utils.fixed_to_float(new_gain_int, self.Q_alpha),
|
||||
utils.fixed_to_float(envelope_int, self.Q_sig),
|
||||
)
|
||||
|
||||
|
||||
class peak_expander_base(expander_base, peak_compressor_limiter_base):
|
||||
"""A generic expander base class that uses a peak envelope detector.
|
||||
|
||||
Inheritance from expander_base is prioritised over
|
||||
peak_compressor_limiter_base due to the order in the definition. To
|
||||
confirm this, peak_expander_base.__mro__ can be inspected.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
threshold_db : float
|
||||
Threshold in decibels below which expansion occurs. This cannot
|
||||
be greater than the maximum value representable in
|
||||
Q_SIG format, and will saturate to that value.
|
||||
"""
|
||||
|
||||
|
||||
class noise_gate(peak_expander_base):
|
||||
"""A noise gate that reduces the level of an audio signal when it
|
||||
falls below a threshold.
|
||||
|
||||
When the signal envelope falls below the threshold, the gain applied
|
||||
to the signal is reduced to 0 (based on the release time). When the
|
||||
envelope returns above the threshold, the gain applied to the signal
|
||||
is increased to 1 over the attack time.
|
||||
|
||||
The initial state of the noise gate is with the gate open (no
|
||||
attenuation), assuming a full scale signal has been present before
|
||||
t = 0.
|
||||
"""
|
||||
|
||||
def __init__(self, fs, n_chans, threshold_db, attack_t, release_t, Q_sig=dspg.Q_SIG):
|
||||
super().__init__(fs, n_chans, threshold_db, attack_t, release_t, Q_sig)
|
||||
|
||||
# set the gain calculation function handles
|
||||
self.gain_calc = drcu.noise_gate_gain_calc
|
||||
self.gain_calc_xcore = drcu.noise_gate_gain_calc_xcore
|
||||
|
||||
|
||||
class noise_suppressor_expander(peak_expander_base):
|
||||
"""A noise suppressor that reduces the level of an audio signal when
|
||||
it falls below a threshold. This is also known as an expander.
|
||||
|
||||
When the signal envelope falls below the threshold, the gain applied
|
||||
to the signal is reduced relative to the expansion ratio over the
|
||||
release time. When the envelope returns above the threshold, the
|
||||
gain applied to the signal is increased to 1 over the attack time.
|
||||
|
||||
The initial state of the noise suppressor is with the suppression
|
||||
off, assuming a full scale signal has been present before
|
||||
t = 0.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ratio : float
|
||||
The expansion ratio applied to the signal when the envelope
|
||||
falls below the threshold.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
ratio : float
|
||||
slope : float
|
||||
The slope factor of the expander, defined as
|
||||
`slope = 1 - ratio`.
|
||||
slope_f32 : float32
|
||||
The slope factor of the expander, used for int32 to float32
|
||||
processing.
|
||||
"""
|
||||
|
||||
def __init__(self, fs, n_chans, ratio, threshold_db, attack_t, release_t, Q_sig=dspg.Q_SIG):
|
||||
super().__init__(fs, n_chans, threshold_db, attack_t, release_t, Q_sig)
|
||||
|
||||
# property calculates the slopes as well
|
||||
self.ratio = ratio
|
||||
|
||||
# set the gain calculation function handles
|
||||
self.gain_calc = drcu.noise_suppressor_expander_gain_calc
|
||||
self.gain_calc_xcore = drcu.noise_suppressor_expander_gain_calc_xcore
|
||||
|
||||
@property
|
||||
def ratio(self):
|
||||
"""Expansion gain ratio applied when the signal is below the
|
||||
threshold; changing this property also updates the slope used in
|
||||
the fixed and floating point implementation.
|
||||
"""
|
||||
return self._ratio
|
||||
|
||||
@ratio.setter
|
||||
def ratio(self, value):
|
||||
self._ratio = value
|
||||
self.slope, self.slope_f32 = drcu.peak_expander_slope_from_ratio(self.ratio)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
nse = noise_suppressor_expander(48000, 1, 3, -20, 0.01, 0.1)
|
||||
ing, outg = nse.get_gain_curve()
|
||||
|
||||
plt.plot(ing, outg)
|
||||
plt.axis("equal")
|
||||
plt.xlim([ing[0], ing[-1]])
|
||||
plt.ylim([ing[0], ing[-1]])
|
||||
plt.grid()
|
||||
plt.show()
|
||||
413
lib_audio_dsp/python/audio_dsp/dsp/drc/sidechain.py
Normal file
413
lib_audio_dsp/python/audio_dsp/dsp/drc/sidechain.py
Normal file
@@ -0,0 +1,413 @@
|
||||
# Copyright 2024-2025 XMOS LIMITED.
|
||||
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
"""The sidechain compressor DSP blocks."""
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
from audio_dsp.dsp import utils as utils
|
||||
from audio_dsp.dsp import generic as dspg
|
||||
import audio_dsp.dsp.drc.drc_utils as drcu
|
||||
from audio_dsp.dsp.types import float32
|
||||
|
||||
from audio_dsp.dsp.drc.drc import rms_compressor_limiter_base
|
||||
from audio_dsp.dsp.drc.stereo_compressor_limiter import rms_compressor_limiter_stereo_base
|
||||
|
||||
|
||||
class compressor_rms_sidechain_mono(rms_compressor_limiter_base):
|
||||
"""
|
||||
A mono sidechain compressor based on the RMS value of the signal.
|
||||
When the RMS envelope of the signal exceeds the threshold, the
|
||||
signal amplitude is reduced by the compression ratio.
|
||||
|
||||
The threshold sets the value above which compression occurs. The
|
||||
ratio sets how much the signal is compressed. A ratio of 1 results
|
||||
in no compression, while a ratio of infinity results in the same
|
||||
behaviour as a limiter. The attack time sets how fast the compressor
|
||||
starts compressing. The release time sets how long the signal takes
|
||||
to ramp up to it's original level after the envelope is below the
|
||||
threshold.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ratio : float
|
||||
Compression gain ratio applied when the signal is above the
|
||||
threshold
|
||||
|
||||
Attributes
|
||||
----------
|
||||
ratio : float
|
||||
slope : float
|
||||
The slope factor of the compressor, defined as
|
||||
`slope = (1 - 1/ratio) / 2`.
|
||||
slope_f32 : float32
|
||||
The slope factor of the compressor, used for int32 to float32
|
||||
processing.
|
||||
"""
|
||||
|
||||
def __init__(self, fs, ratio, threshold_db, attack_t, release_t, Q_sig=dspg.Q_SIG):
|
||||
super().__init__(fs, 1, threshold_db, attack_t, release_t, Q_sig)
|
||||
|
||||
# property calculates the slopes as well
|
||||
self.ratio = ratio
|
||||
|
||||
# set the gain calculation function handles
|
||||
self.gain_calc = drcu.compressor_rms_gain_calc
|
||||
self.gain_calc_xcore = drcu.compressor_rms_gain_calc_xcore
|
||||
|
||||
@property
|
||||
def ratio(self):
|
||||
"""Compression gain ratio applied when the signal is above the
|
||||
threshold; changing this property also updates the slope used in
|
||||
the fixed and floating point implementation.
|
||||
"""
|
||||
return self._ratio
|
||||
|
||||
@ratio.setter
|
||||
def ratio(self, value):
|
||||
self._ratio = value
|
||||
self.slope, self.slope_f32 = drcu.rms_compressor_slope_from_ratio(self.ratio)
|
||||
|
||||
def reset_state(self):
|
||||
"""Reset the envelope detectors to 0 and the gain to 1."""
|
||||
if self.env_detector:
|
||||
self.env_detector.reset_state()
|
||||
self.gain = 1
|
||||
self.gain_int = 2**31 - 1
|
||||
|
||||
def process(self, input_sample: float, detect_sample: float): # type: ignore : overloading base class
|
||||
"""
|
||||
Update the envelope for the detection signal, then calculate and
|
||||
apply the required gain for compression/limiting, and apply to
|
||||
the input signal using floating point maths.
|
||||
|
||||
Take one new sample and return the compressed/limited sample.
|
||||
Input should be scaled with 0 dB = 1.0.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
input_sample : float
|
||||
The input sample to be compressed.
|
||||
detect_sample : float
|
||||
The sample used by the envelope detector to determine the
|
||||
amount of compression to apply to the input_sample.
|
||||
"""
|
||||
# get envelope from envelope detector
|
||||
envelope = self.env_detector.process(detect_sample)
|
||||
# avoid /0
|
||||
envelope = np.maximum(envelope, np.finfo(float).tiny)
|
||||
|
||||
# calculate the gain, this function should be defined by the
|
||||
# child class
|
||||
new_gain = self.gain_calc(envelope, self.threshold, self.slope)
|
||||
|
||||
# see if we're attacking or decaying
|
||||
if new_gain < self.gain:
|
||||
alpha = self.attack_alpha
|
||||
else:
|
||||
alpha = self.release_alpha
|
||||
|
||||
# do exponential moving average
|
||||
self.gain = ((1 - alpha) * self.gain) + (alpha * new_gain)
|
||||
|
||||
# apply gain to input
|
||||
y = self.gain * input_sample
|
||||
return y, new_gain, envelope
|
||||
|
||||
def process_xcore(self, input_sample: float, detect_sample: float): # type: ignore : overloading base class
|
||||
"""
|
||||
Update the envelope for the detection signal, then calculate and
|
||||
apply the required gain for compression/limiting, and apply to
|
||||
the input signal using int32 maths.
|
||||
|
||||
Take one new sample and return the compressed/limited sample.
|
||||
Input should be scaled with 0 dB = 1.0.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
input_sample : float
|
||||
The input sample to be compressed.
|
||||
detect_sample : float
|
||||
The sample used by the envelope detector to determine the
|
||||
amount of compression to apply to the input_sample.
|
||||
|
||||
"""
|
||||
# quantize
|
||||
sample_int = utils.float_to_fixed(input_sample, self.Q_sig)
|
||||
detect_sample_int = utils.float_to_fixed(detect_sample, self.Q_sig)
|
||||
|
||||
# get envelope from envelope detector
|
||||
envelope_int = self.env_detector.process_xcore(detect_sample_int)
|
||||
# avoid /0
|
||||
envelope_int = max(envelope_int, 1)
|
||||
assert isinstance(envelope_int, int)
|
||||
|
||||
# if envelope below threshold, apply unity gain, otherwise scale
|
||||
# down
|
||||
new_gain_int = self.gain_calc_xcore(envelope_int, self.threshold_int, self.slope_f32)
|
||||
|
||||
# see if we're attacking or decaying
|
||||
if new_gain_int < self.gain_int:
|
||||
alpha = self.attack_alpha_int
|
||||
else:
|
||||
alpha = self.release_alpha_int
|
||||
|
||||
# do exponential moving average
|
||||
self.gain_int = drcu.calc_ema_xcore(self.gain_int, new_gain_int, alpha)
|
||||
|
||||
# apply gain
|
||||
y = drcu.apply_gain_xcore(sample_int, self.gain_int)
|
||||
|
||||
return (
|
||||
utils.fixed_to_float(y, self.Q_sig),
|
||||
utils.fixed_to_float(new_gain_int, self.Q_alpha),
|
||||
utils.fixed_to_float(envelope_int, self.Q_sig),
|
||||
)
|
||||
|
||||
def process_frame(self, frame):
|
||||
"""
|
||||
Take a list frames of samples and return the processed frames.
|
||||
|
||||
A frame is defined as a list of 1-D numpy arrays, where the
|
||||
number of arrays is equal to the number of channels, and the
|
||||
length of the arrays is equal to the frame size.
|
||||
|
||||
When calling self.process only take the first output.
|
||||
|
||||
"""
|
||||
assert len(frame) == 2
|
||||
frame_size = frame[0].shape[0]
|
||||
output = np.zeros(frame_size)
|
||||
for sample in range(frame_size):
|
||||
output[sample] = self.process(frame[0][sample], frame[1][sample])[0]
|
||||
|
||||
return [output]
|
||||
|
||||
def process_frame_xcore(self, frame):
|
||||
"""
|
||||
Take a list frames of samples and return the processed frames,
|
||||
using a bit exact xcore implementation.
|
||||
A frame is defined as a list of 1-D numpy arrays, where the
|
||||
number of arrays is equal to the number of channels, and the
|
||||
length of the arrays is equal to the frame size.
|
||||
|
||||
When calling self.process_xcore only take the first output.
|
||||
|
||||
"""
|
||||
assert len(frame) == 2
|
||||
frame_size = frame[0].shape[0]
|
||||
output = np.zeros(frame_size)
|
||||
for sample in range(frame_size):
|
||||
output[sample] = self.process_xcore(frame[0][sample], frame[1][sample])[0]
|
||||
|
||||
return [output]
|
||||
|
||||
|
||||
class compressor_rms_sidechain_stereo(rms_compressor_limiter_stereo_base):
|
||||
"""
|
||||
A stereo sidechain compressor based on the RMS value of the signal.
|
||||
When the RMS envelope of the signal exceeds the threshold, the
|
||||
signal amplitude is reduced by the compression ratio. The same
|
||||
compression is applied to both channels, using the highest
|
||||
individual channel envelope.
|
||||
|
||||
The threshold sets the value above which compression occurs. The
|
||||
ratio sets how much the signal is compressed. A ratio of 1 results
|
||||
in no compression, while a ratio of infinity results in the same
|
||||
behaviour as a limiter. The attack time sets how fast the compressor
|
||||
starts compressing. The release time sets how long the signal takes
|
||||
to ramp up to it's original level after the envelope is below the
|
||||
threshold.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ratio : float
|
||||
Compression gain ratio applied when the signal is above the
|
||||
threshold
|
||||
|
||||
Attributes
|
||||
----------
|
||||
ratio : float
|
||||
slope : float
|
||||
The slope factor of the compressor, defined as
|
||||
`slope = (1 - 1/ratio)`.
|
||||
slope_f32 : float32
|
||||
The slope factor of the compressor, used for int32 to float32
|
||||
processing.
|
||||
"""
|
||||
|
||||
def __init__(self, fs, ratio, threshold_db, attack_t, release_t, Q_sig=dspg.Q_SIG):
|
||||
n_chans = 2
|
||||
super().__init__(fs, n_chans, threshold_db, attack_t, release_t, Q_sig)
|
||||
|
||||
# property calculates the slopes as well
|
||||
self.ratio = ratio
|
||||
|
||||
# set the gain calculation function handles
|
||||
self.gain_calc = drcu.compressor_rms_gain_calc
|
||||
self.gain_calc_xcore = drcu.compressor_rms_gain_calc_xcore
|
||||
|
||||
@property
|
||||
def ratio(self):
|
||||
"""Compression gain ratio applied when the signal is above the
|
||||
threshold; changing this property also updates the slope used in
|
||||
the fixed and floating point implementation.
|
||||
"""
|
||||
return self._ratio
|
||||
|
||||
@ratio.setter
|
||||
def ratio(self, value):
|
||||
self._ratio = value
|
||||
self.slope, self.slope_f32 = drcu.rms_compressor_slope_from_ratio(self.ratio)
|
||||
|
||||
def process_channels(self, input_samples: list[float], detect_samples: list[float]): # type: ignore : override base class
|
||||
"""
|
||||
Update the envelopes for a detection signal, then calculate and
|
||||
apply the required gain for compression/limiting to the input,
|
||||
using floating point maths. The same gain is applied to both
|
||||
stereo channels.
|
||||
|
||||
Take one new sample and return the compressed/limited sample.
|
||||
Input should be scaled with 0 dB = 1.0.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
input_samples : list[float]
|
||||
List of input samples to be compressed.
|
||||
detect_samples : list[float]
|
||||
List of samples used by the envelope detector to determine the
|
||||
amount of compression to apply to the input_sample.
|
||||
|
||||
"""
|
||||
# get envelope from envelope detector
|
||||
env0 = self.env_detector.process(detect_samples[0], 0)
|
||||
env1 = self.env_detector.process(detect_samples[1], 1)
|
||||
envelope = np.maximum(env0, env1)
|
||||
# avoid /0
|
||||
envelope = np.maximum(envelope, np.finfo(float).tiny)
|
||||
|
||||
# calculate the gain, this function should be defined by the
|
||||
# child class
|
||||
new_gain = self.gain_calc(envelope, self.threshold, self.slope) # type: ignore : base inits to None
|
||||
|
||||
# see if we're attacking or decaying
|
||||
if new_gain < self.gain:
|
||||
alpha = self.attack_alpha
|
||||
else:
|
||||
alpha = self.release_alpha
|
||||
|
||||
# do exponential moving average
|
||||
self.gain = ((1 - alpha) * self.gain) + (alpha * new_gain)
|
||||
|
||||
# apply gain to input
|
||||
y = self.gain * input_samples
|
||||
return y, new_gain, envelope
|
||||
|
||||
def process_channels_xcore( # type: ignore : override base class
|
||||
self, input_samples: list[float], detect_samples: list[float]
|
||||
):
|
||||
"""
|
||||
Update the envelopes for a detection signal, then calculate and
|
||||
apply the required gain for compression/limiting to the input,
|
||||
using int32 fixed point maths. The same gain is applied to both
|
||||
stereo channels.
|
||||
|
||||
Take one new sample and return the compressed/limited sample.
|
||||
Input should be scaled with 0 dB = 1.0.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
input_samples : list[float]
|
||||
List of input samples to be compressed.
|
||||
detect_samples : list[float]
|
||||
List of samples used by the envelope detector to determine the
|
||||
amount of compression to apply to the input_sample.
|
||||
|
||||
"""
|
||||
# quantize
|
||||
samples_int = [int(0)] * len(input_samples)
|
||||
detect_samples_int = [int(0)] * len(input_samples)
|
||||
for i in range(len(input_samples)):
|
||||
samples_int[i] = utils.float_to_fixed(input_samples[i], self.Q_sig)
|
||||
detect_samples_int[i] = utils.float_to_fixed(detect_samples[i], self.Q_sig)
|
||||
|
||||
# get envelope from envelope detector
|
||||
env0 = self.env_detector.process_xcore(detect_samples_int[0], 0)
|
||||
env1 = self.env_detector.process_xcore(detect_samples_int[1], 1)
|
||||
envelope_int = np.maximum(env0, env1)
|
||||
# avoid /0
|
||||
envelope_int = max(envelope_int, 1)
|
||||
|
||||
# if envelope below threshold, apply unity gain, otherwise scale
|
||||
# down
|
||||
new_gain_int = self.gain_calc_xcore(envelope_int, self.threshold_int, self.slope_f32) # type: ignore : base inits to None
|
||||
|
||||
# see if we're attacking or decaying
|
||||
if new_gain_int < self.gain_int:
|
||||
alpha = self.attack_alpha_int
|
||||
else:
|
||||
alpha = self.release_alpha_int
|
||||
|
||||
# do exponential moving average
|
||||
self.gain_int = drcu.calc_ema_xcore(self.gain_int, new_gain_int, alpha)
|
||||
|
||||
y = [0.0] * self.n_chans
|
||||
# apply gain in int32
|
||||
for i in range(len(input_samples)):
|
||||
y_uq = drcu.apply_gain_xcore(samples_int[i], self.gain_int)
|
||||
y[i] = utils.fixed_to_float(y_uq, self.Q_sig)
|
||||
|
||||
return (
|
||||
y,
|
||||
utils.fixed_to_float(new_gain_int, self.Q_alpha),
|
||||
utils.fixed_to_float(envelope_int, self.Q_sig),
|
||||
)
|
||||
|
||||
def process_frame(self, frame):
|
||||
"""
|
||||
Take a list frames of samples and return the processed frames.
|
||||
|
||||
A frame is defined as a list of 1-D numpy arrays, where the
|
||||
number of arrays is equal to the number of channels, and the
|
||||
length of the arrays is equal to the frame size.
|
||||
|
||||
When calling self.process only take the first output.
|
||||
|
||||
"""
|
||||
assert len(frame) == 4, "has to be dual stereo"
|
||||
frame_size = frame[0].shape[0]
|
||||
output = deepcopy(frame[0:2])
|
||||
for sample in range(frame_size):
|
||||
out_samples = self.process_channels(
|
||||
[frame[0][sample], frame[1][sample]],
|
||||
[frame[2][sample], frame[3][sample]],
|
||||
)[0]
|
||||
output[0][sample] = out_samples[0]
|
||||
output[1][sample] = out_samples[1]
|
||||
return output
|
||||
|
||||
def process_frame_xcore(self, frame):
|
||||
"""
|
||||
Take a list frames of samples and return the processed frames,
|
||||
using a bit exact xcore implementation.
|
||||
A frame is defined as a list of 1-D numpy arrays, where the
|
||||
number of arrays is equal to the number of channels, and the
|
||||
length of the arrays is equal to the frame size.
|
||||
|
||||
When calling self.process_xcore only take the first output.
|
||||
|
||||
"""
|
||||
assert len(frame) == 4, "has to be dual stereo"
|
||||
frame_size = frame[0].shape[0]
|
||||
output = deepcopy(frame[0:2])
|
||||
for sample in range(frame_size):
|
||||
out_samples = self.process_channels_xcore(
|
||||
[frame[0][sample], frame[1][sample]],
|
||||
[frame[2][sample], frame[3][sample]],
|
||||
)[0]
|
||||
output[0][sample] = out_samples[0]
|
||||
output[1][sample] = out_samples[1]
|
||||
|
||||
return output
|
||||
@@ -0,0 +1,440 @@
|
||||
# Copyright 2024-2025 XMOS LIMITED.
|
||||
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
"""DSP blocks for stereo compressors and limiters."""
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
import numpy as np
|
||||
|
||||
from audio_dsp.dsp import utils as utils
|
||||
from audio_dsp.dsp import generic as dspg
|
||||
from audio_dsp.dsp.drc import envelope_detector_peak, envelope_detector_rms
|
||||
|
||||
import audio_dsp.dsp.drc.drc_utils as drcu
|
||||
from audio_dsp.dsp.types import float32
|
||||
|
||||
|
||||
class compressor_limiter_stereo_base(dspg.dsp_block):
|
||||
"""
|
||||
A base class shared by stereo compressor and limiter objects.
|
||||
|
||||
Stereo compressors and limiters work in a similar way to normal
|
||||
compressor_limiter_base objects, but they process two channels and
|
||||
apply the same gain to both channels.
|
||||
|
||||
The compressor and limiter have very similar structures, with
|
||||
differences in the gain calculation. All the shared code and
|
||||
parameters are calculated in this base class.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
n_chans : int
|
||||
Number of parallel channels the compressor/limiter runs on. The
|
||||
channels are limited/compressed separately, only the constant
|
||||
parameters are shared.
|
||||
attack_t : float
|
||||
Attack time of the compressor/limiter in seconds. This cannot be
|
||||
faster than 2/fs seconds, and saturates to that
|
||||
value. Exceptionally large attack times may converge to zero.
|
||||
release_t: float
|
||||
Release time of the compressor/limiter in seconds. This cannot
|
||||
be faster than 2/fs seconds, and saturates to that
|
||||
value. Exceptionally large release times may converge to zero.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
threshold_db : float
|
||||
attack_t : float
|
||||
release_t : float
|
||||
threshold : float
|
||||
Value above which compression/limiting occurs for floating point
|
||||
processing.
|
||||
gain : list[float]
|
||||
Current gain to be applied to the signal for each channel for
|
||||
floating point processing.
|
||||
attack_alpha : float
|
||||
Attack time parameter used for exponential moving average in
|
||||
floating point processing.
|
||||
release_alpha : float
|
||||
Release time parameter used for exponential moving average in
|
||||
floating point processing.
|
||||
threshold_int : int
|
||||
Value above which compression/limiting occurs for int32 fixed
|
||||
point processing.
|
||||
gain_int : list[int]
|
||||
Current gain to be applied to the signal for each channel for
|
||||
int32 fixed point processing.
|
||||
attack_alpha_int : int
|
||||
attack_alpha in 32-bit int format.
|
||||
release_alpha_int : int
|
||||
release_alpha in 32-bit int format.
|
||||
gain_calc : function
|
||||
function pointer to floating point gain calculation function.
|
||||
gain_calc_int : function
|
||||
function pointer to fixed point gain calculation function.
|
||||
"""
|
||||
|
||||
def __init__(self, fs, n_chans, attack_t, release_t, Q_sig=dspg.Q_SIG):
|
||||
assert n_chans == 2, "has to be stereo"
|
||||
super().__init__(fs, n_chans, Q_sig)
|
||||
|
||||
self.Q_alpha = drcu.Q_alpha
|
||||
assert self.Q_alpha == 31, "When changing this the reset value will have to be updated"
|
||||
|
||||
# setting attack and release times sets the EWM coeffs in this and
|
||||
# the envelope detector
|
||||
self.attack_t = attack_t
|
||||
self.release_t = release_t
|
||||
|
||||
# slope only used by compressors, but needs to be set for gain_calc API
|
||||
self.slope = None
|
||||
self.slope_f32 = None
|
||||
|
||||
# set the gain calculation function handles
|
||||
self.gain_calc = None
|
||||
self.gain_calc_xcore = None
|
||||
|
||||
# initialise gain states
|
||||
self.reset_state()
|
||||
|
||||
@property
|
||||
def attack_t(self):
|
||||
"""The attack time in seconds. Changing this also sets the EWM alpha values."""
|
||||
return self._attack_t
|
||||
|
||||
@attack_t.setter
|
||||
def attack_t(self, value):
|
||||
self._attack_t = value
|
||||
# calculate EWM alpha from time constant
|
||||
self.attack_alpha, self.attack_alpha_int = drcu.alpha_from_time(self._attack_t, self.fs)
|
||||
# update the envelope detector
|
||||
self.env_detector.attack_t = self.attack_t
|
||||
|
||||
@property
|
||||
def release_t(self):
|
||||
"""The release time in seconds. Changing this also sets the EWM alpha values."""
|
||||
return self._release_t
|
||||
|
||||
@release_t.setter
|
||||
def release_t(self, value):
|
||||
self._release_t = value
|
||||
# calculate EWM alpha from time constant
|
||||
self.release_alpha, self.release_alpha_int = drcu.alpha_from_time(self._release_t, self.fs)
|
||||
# update the envelope detector
|
||||
self.env_detector.release_t = self.release_t
|
||||
|
||||
def reset_state(self):
|
||||
"""Reset the envelope detectors to 0 and the gain to 1."""
|
||||
if self.env_detector:
|
||||
self.env_detector.reset_state()
|
||||
self.gain = 1
|
||||
self.gain_int = 2**31 - 1
|
||||
|
||||
def process_channels(self, sample_list: list[float]): # pyright: ignore overload
|
||||
"""
|
||||
Update the envelopes for a signal, then calculate and apply the
|
||||
required gain for compression/limiting, using floating point
|
||||
maths. The same gain is applied to both stereo channels.
|
||||
|
||||
Take one new pair of samples and return the compressed/limited
|
||||
samples.
|
||||
Input should be scaled with 0 dB = 1.0.
|
||||
|
||||
"""
|
||||
# get envelope from envelope detector
|
||||
env0 = self.env_detector.process(sample_list[0], 0) # type: ignore : base inits to None
|
||||
env1 = self.env_detector.process(sample_list[1], 1) # type: ignore : base inits to None
|
||||
envelope = np.maximum(env0, env1)
|
||||
# avoid /0
|
||||
envelope = np.maximum(envelope, np.finfo(float).tiny)
|
||||
|
||||
# calculate the gain, this function should be defined by the
|
||||
# child class
|
||||
new_gain = self.gain_calc(envelope, self.threshold, self.slope) # type: ignore : base inits to None
|
||||
|
||||
# see if we're attacking or decaying
|
||||
if new_gain < self.gain:
|
||||
alpha = self.attack_alpha
|
||||
else:
|
||||
alpha = self.release_alpha
|
||||
|
||||
# do exponential moving average
|
||||
self.gain = ((1 - alpha) * self.gain) + (alpha * new_gain)
|
||||
|
||||
# apply gain to input
|
||||
y = self.gain * sample_list
|
||||
return y, new_gain, envelope
|
||||
|
||||
def process_channels_xcore(self, sample_list: list[float]): # pyright: ignore overload
|
||||
"""
|
||||
Update the envelopes for a signal, then calculate and apply the
|
||||
required gain for compression/limiting, using int32 fixed point
|
||||
maths. The same gain is applied to both stereo channels.
|
||||
|
||||
Take one new pair of samples and return the compressed/limited
|
||||
samples. The float input sample is quantized to int32, and
|
||||
returned to float before outputting. Input should be scaled with
|
||||
0 dB = 1.0.
|
||||
|
||||
"""
|
||||
samples_int = [int(0)] * len(sample_list)
|
||||
for i in range(len(sample_list)):
|
||||
samples_int[i] = utils.float_to_fixed(sample_list[i], self.Q_sig)
|
||||
|
||||
# get envelope from envelope detector
|
||||
env0_int = self.env_detector.process_xcore(samples_int[0], 0) # type: ignore : base inits to None
|
||||
env1_int = self.env_detector.process_xcore(samples_int[1], 1) # type: ignore : base inits to None
|
||||
envelope_int = max(env0_int, env1_int)
|
||||
# avoid /0
|
||||
envelope_int = max(envelope_int, 1)
|
||||
|
||||
# if envelope below threshold, apply unity gain, otherwise scale
|
||||
# down
|
||||
new_gain_int = self.gain_calc_xcore(envelope_int, self.threshold_int, self.slope_f32) # type: ignore : base inits to None
|
||||
|
||||
# see if we're attacking or decaying
|
||||
if new_gain_int < self.gain_int:
|
||||
alpha = self.attack_alpha_int
|
||||
else:
|
||||
alpha = self.release_alpha_int
|
||||
|
||||
# do exponential moving average
|
||||
self.gain_int = drcu.calc_ema_xcore(self.gain_int, new_gain_int, alpha)
|
||||
|
||||
y = []
|
||||
|
||||
for sample_int in samples_int:
|
||||
y_uq = drcu.apply_gain_xcore(sample_int, self.gain_int)
|
||||
y.append(utils.fixed_to_float(y_uq, self.Q_sig))
|
||||
|
||||
return (
|
||||
y,
|
||||
utils.fixed_to_float(new_gain_int, self.Q_alpha),
|
||||
utils.fixed_to_float(envelope_int, self.Q_sig),
|
||||
)
|
||||
|
||||
def process_frame(self, frame: list[np.ndarray]):
|
||||
"""
|
||||
Take a list frames of samples and return the processed frames.
|
||||
|
||||
A frame is defined as a list of 1-D numpy arrays, where the
|
||||
number of arrays is equal to the number of channels, and the
|
||||
length of the arrays is equal to the frame size.
|
||||
|
||||
When calling self.process_channels only take the first output.
|
||||
|
||||
"""
|
||||
n_outputs = len(frame)
|
||||
assert n_outputs == 2, "has to be stereo"
|
||||
frame_size = frame[0].shape[0]
|
||||
output = deepcopy(frame)
|
||||
for sample in range(frame_size):
|
||||
out_samples = self.process_channels([frame[0][sample], frame[1][sample]])[0]
|
||||
output[0][sample] = out_samples[0]
|
||||
output[1][sample] = out_samples[1]
|
||||
return output
|
||||
|
||||
def process_frame_xcore(self, frame: list[np.ndarray]):
|
||||
"""
|
||||
Take a list frames of samples and return the processed frames,
|
||||
using a bit exact xcore implementation.
|
||||
A frame is defined as a list of 1-D numpy arrays, where the
|
||||
number of arrays is equal to the number of channels, and the
|
||||
length of the arrays is equal to the frame size.
|
||||
|
||||
When calling self.process_channel_xcore only take the first output.
|
||||
|
||||
"""
|
||||
n_outputs = len(frame)
|
||||
assert n_outputs == 2, "has to be stereo"
|
||||
frame_size = frame[0].shape[0]
|
||||
output = deepcopy(frame)
|
||||
for sample in range(frame_size):
|
||||
out_samples = self.process_channels_xcore([frame[0][sample], frame[1][sample]])[0]
|
||||
output[0][sample] = out_samples[0]
|
||||
output[1][sample] = out_samples[1]
|
||||
|
||||
return output
|
||||
|
||||
|
||||
class peak_compressor_limiter_stereo_base(compressor_limiter_stereo_base):
|
||||
"""
|
||||
A compressor/limiter with a peak envelope detector.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
threshold_db : float
|
||||
Threshold in decibels above which limiting occurs.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
threshold_db : float
|
||||
env_detector : envelope_detector_peak
|
||||
Peak envelope detector object used to calculate the envelope of
|
||||
the signal.
|
||||
"""
|
||||
|
||||
def __init__(self, fs, n_chans, threshold_db, attack_t, release_t, Q_sig=dspg.Q_SIG):
|
||||
self.env_detector = envelope_detector_peak(
|
||||
fs,
|
||||
n_chans=n_chans,
|
||||
attack_t=attack_t,
|
||||
release_t=release_t,
|
||||
Q_sig=Q_sig,
|
||||
)
|
||||
|
||||
super().__init__(fs, n_chans, attack_t, release_t, Q_sig)
|
||||
|
||||
# threshold_db should be a property of the child class that sets
|
||||
# threshold_int and threshold
|
||||
self.threshold_db = threshold_db
|
||||
|
||||
@property
|
||||
def threshold_db(self):
|
||||
"""The threshold in decibels; changing this property also
|
||||
updates the fixed and floating point thresholds in linear
|
||||
gain.
|
||||
"""
|
||||
return self._threshold_db
|
||||
|
||||
@threshold_db.setter
|
||||
def threshold_db(self, value):
|
||||
self._threshold_db = value
|
||||
self.threshold, self.threshold_int = drcu.calculate_threshold(
|
||||
self._threshold_db, self.Q_sig
|
||||
)
|
||||
|
||||
|
||||
class rms_compressor_limiter_stereo_base(compressor_limiter_stereo_base):
|
||||
"""
|
||||
A compressor/limiter with an RMS envelope detector.
|
||||
|
||||
Note the threshold is saved in the power domain, as the RMS envelope
|
||||
detector returns x².
|
||||
|
||||
Parameters
|
||||
----------
|
||||
threshold_db : float
|
||||
Threshold in decibels above which limiting occurs.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
env_detector : envelope_detector_rms
|
||||
RMS envelope detector object used to calculate the envelope of
|
||||
the signal.
|
||||
threshold : float
|
||||
Value above which compression/limiting occurs for floating point
|
||||
processing. Note the threshold is saved in the power domain, as
|
||||
the RMS envelope detector returns x².
|
||||
"""
|
||||
|
||||
def __init__(self, fs, n_chans, threshold_db, attack_t, release_t, Q_sig=dspg.Q_SIG):
|
||||
self.env_detector = envelope_detector_rms(
|
||||
fs,
|
||||
n_chans=n_chans,
|
||||
attack_t=attack_t,
|
||||
release_t=release_t,
|
||||
Q_sig=Q_sig,
|
||||
)
|
||||
|
||||
super().__init__(fs, n_chans, attack_t, release_t, Q_sig)
|
||||
|
||||
# threshold_db should be a property of the child class that sets
|
||||
# threshold_int and threshold
|
||||
self.threshold_db = threshold_db
|
||||
|
||||
@property
|
||||
def threshold_db(self):
|
||||
"""The threshold in decibels; changing this property also
|
||||
updates the fixed and floating point thresholds in linear
|
||||
gain.
|
||||
"""
|
||||
return self._threshold_db
|
||||
|
||||
@threshold_db.setter
|
||||
def threshold_db(self, value):
|
||||
self._threshold_db = value
|
||||
self.threshold, self.threshold_int = drcu.calculate_threshold(
|
||||
self._threshold_db, self.Q_sig, power=True
|
||||
)
|
||||
|
||||
|
||||
class limiter_peak_stereo(peak_compressor_limiter_stereo_base):
|
||||
"""
|
||||
A stereo limiter based on the peak value of the signal. When the
|
||||
peak envelope of either signal channel, exceeds the threshold, the
|
||||
amplitudes of both channels are reduced.
|
||||
|
||||
The threshold set the value above which limiting occurs. The attack
|
||||
time sets how fast the limiter starts limiting. The release time
|
||||
sets how long the signal takes to ramp up to it's original level
|
||||
after the envelope is below the threshold.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
threshold_dB : float
|
||||
Threshold in decibels above which limiting occurs.
|
||||
"""
|
||||
|
||||
def __init__(self, fs, threshold_dB, attack_t, release_t, Q_sig=dspg.Q_SIG):
|
||||
n_chans = 2
|
||||
super().__init__(fs, n_chans, threshold_dB, attack_t, release_t, Q_sig)
|
||||
|
||||
# set the gain calculation function handles
|
||||
self.gain_calc = drcu.limiter_peak_gain_calc
|
||||
self.gain_calc_xcore = drcu.limiter_peak_gain_calc_xcore
|
||||
|
||||
|
||||
class compressor_rms_stereo(rms_compressor_limiter_stereo_base):
|
||||
"""
|
||||
A stereo compressor based on the RMS value of the signal. When the
|
||||
RMS envelope of the either signal channel exceeds the threshold, the
|
||||
amplitudes of both channels are reduced by the compression ratio.
|
||||
|
||||
The threshold sets the value above which compression occurs. The
|
||||
ratio sets how much the signal is compressed. A ratio of 1 results
|
||||
in no compression, while a ratio of infinity results in the same
|
||||
behaviour as a limiter. The attack time sets how fast the compressor
|
||||
starts compressing. The release time sets how long the signal takes
|
||||
to ramp up to it's original level after the envelope is below the
|
||||
threshold.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
threshold_dB : float
|
||||
Threshold in decibels above which limiting occurs.
|
||||
ratio : float
|
||||
Compression gain ratio applied when the signal is above the
|
||||
threshold
|
||||
|
||||
Attributes
|
||||
----------
|
||||
ratio : float
|
||||
slope : float
|
||||
The slope factor of the compressor, defined as
|
||||
`slope = (1 - 1/ratio) / 2`.
|
||||
slope_f32 : float32
|
||||
The slope factor of the compressor, used for int32 to float32
|
||||
processing.
|
||||
"""
|
||||
|
||||
def __init__(self, fs, ratio, threshold_dB, attack_t, release_t, Q_sig=dspg.Q_SIG):
|
||||
n_chans = 2
|
||||
super().__init__(fs, n_chans, threshold_dB, attack_t, release_t, Q_sig)
|
||||
|
||||
# property calculates the slopes as well
|
||||
self.ratio = ratio
|
||||
|
||||
# set the gain calculation function handles
|
||||
self.gain_calc = drcu.compressor_rms_gain_calc
|
||||
self.gain_calc_xcore = drcu.compressor_rms_gain_calc_xcore
|
||||
|
||||
@property
|
||||
def ratio(self):
|
||||
"""Compression gain ratio applied when the signal is above the threshold."""
|
||||
return self._ratio
|
||||
|
||||
@ratio.setter
|
||||
def ratio(self, value):
|
||||
self._ratio = value
|
||||
self.slope, self.slope_f32 = drcu.rms_compressor_slope_from_ratio(self.ratio)
|
||||
Reference in New Issue
Block a user