To prevent crackling, hissing or other disturbing signals when no usable signal is at an ADC-input, we use noisegates to keep noise out of our audio-mix. The following snippet shows a stereo-noisegate with adjustable threshold, range-control and adjustable coefficients for attack, release and hold. The calculation of these coefficients can be found down below. For more information see my X/FBAPE-project at GitHub: https://www.github.com/xn--nding-jua/xfbape
VHDL
-- Stereo noisegate with adjustable parameters
-- Christian Noeding, christian@noeding-online.de
-- https://chrisdevblog.com | https://github.com/xn--nding-jua
--
-- Released under GNU General Public License v3
library ieee;
use ieee.std_logic_1164.all;
use ieee.std_logic_unsigned.all;
use ieee.numeric_std.all; -- lib for unsigned and signed
entity noisegate_stereo is
generic (
bit_width : natural range 16 to 48 := 24
);
port (
clk : in std_logic := '0';
sample_l_in : in signed(bit_width - 1 downto 0) := (others=>'0');
sample_r_in : in signed(bit_width - 1 downto 0) := (others=>'0');
sync_in : in std_logic := '0';
threshold : in unsigned(bit_width - 1 downto 0) := (others=>'0'); -- threshold with bitdepth of sample, that will be used to check the audio-level (between 0 and 8388607)
gain_min : in unsigned(7 downto 0) := (others=>'0'); -- minimum gain the user will allow (48dB-range (6dB/bit * 8bit), between 0 and 255)
coeff_attack : in signed(15 downto 0); -- Q15-format, 1 bit sign, 0 bit for integer-part, 15 bit for fraction-part
hold_ticks : in unsigned(15 downto 0) := (others=>'0'); -- 16 bit value for hold_counter = 0...65535/48000 [0...1365ms]
coeff_release : in signed(15 downto 0); -- Q15-format, 1 bit sign, 0 bit for integer-part, 15 bit for fraction-part
sample_l_out : out signed(bit_width - 1 downto 0) := (others=>'0');
sample_r_out : out signed(bit_width - 1 downto 0) := (others=>'0');
sync_out : out std_logic := '0';
gate_closed_out : out std_logic := '0'
);
end noisegate_stereo;
architecture rtl of noisegate_stereo is
-- signals for the state-machines
signal s_SM_Main : natural range 0 to 7 := 0;
type t_SM_Effect is (s_GateClosed, s_Attack, s_GateOpen, s_Hold, s_GateClosing);
signal s_SM_Effect : t_SM_Effect := s_Hold; -- start in hold mode to get release in next cycle
-- signals for processing
signal sample_l : signed(bit_width - 1 downto 0) := (others=>'0');
signal sample_r : signed(bit_width - 1 downto 0) := (others=>'0');
signal coeff : signed(15 downto 0); -- Q15-format, 1 bit for sign, 0 bit for integer-part, 15 bit for fraction-part
signal gain_set : unsigned(7 downto 0) := (others=>'0'); -- desired value that is calculated
signal gain : unsigned(22 downto 0) := (others=>'0'); -- unsigned Q15-format, that will be used for current gain and holds value as gain_z1 for next step
signal hold_counter : unsigned(15 downto 0) := (others=>'0'); -- unsigned 16 bit counter for hold_counter = 0...1365ms
--signals for multiplier
signal mult_in_a : signed(bit_width - 1 downto 0) := (others=>'0');
signal mult_in_b : signed(15 downto 0) := (others=>'0');
signal mult_out : signed((bit_width + 16) - 1 downto 0) := (others=>'0');
begin
-- multiplier
process(mult_in_a, mult_in_b)
begin
mult_out <= mult_in_a * mult_in_b;
end process;
process(clk)
variable gate_closed : std_logic := '0';
begin
if rising_edge(clk) then
if (sync_in = '1' and s_SM_Main = 0) then
-- we are receiving a new sample
-- copy sample to internal signal
sample_l <= sample_l_in;
sample_r <= sample_r_in;
-- check if gate is opened or closed
if ((unsigned(abs(sample_l_in)) > threshold) or (unsigned(abs(sample_r_in)) > threshold)) then
gate_closed := '0';
else
gate_closed := '1';
end if;
-- check state of Effect-Statemachine
case s_SM_Effect is
when s_GateClosed =>
-- check level for entering attack-state
if (gate_closed = '0') then
s_SM_Effect <= s_Attack;
end if;
when s_Attack =>
-- gate is opening
-- set target gain-level to minimum value the user has set
-- set attack-coefficient for LP-filter
-- gain itself will be set via low-pass-filter down below
gain_set <= to_unsigned(255, gain_set'length);
coeff <= coeff_attack;
-- go directly into on-state
-- we could add a attack-state-counter here, but the wait-time
-- will depend on coefficients, so keep it simple
s_SM_Effect <= s_GateOpen;
when s_GateOpen =>
if (gate_closed = '1') then
-- we are below the threshold
-- load counter and enter release-hold-state
hold_counter <= hold_ticks;
s_SM_Effect <= s_Hold;
end if;
when s_Hold =>
-- gate is in hold-state before closing
if (gate_closed = '0') then
-- value is above threshold again -> re-enter s_GateOpen
s_SM_Effect <= s_GateOpen;
else
-- we are above the threshold
-- check hold_counter
if (hold_counter = 0) then
-- we reached end of hold -> enter release-state
s_SM_Effect <= s_GateClosing;
else
-- we still have to stay in hold
-- decrement hold-counter
hold_counter <= hold_counter - 1;
end if;
end if;
when s_GateClosing =>
-- set target gain-level to minimum
-- set release-coefficient for LP-filter
-- gain itself will be set via low-pass-filter down below
gain_set <= gain_min;
coeff <= coeff_release;
-- go directly into closed-state (TODO: we could add a release-state-counter here, but the wait-time will depend on coefficients)
s_SM_Effect <= s_GateClosed;
end case;
s_SM_Main <= s_SM_Main + 1; -- go into next state
elsif (s_SM_Main = 1) then
-- now calculate low-pass-filter: gain = coeff * gain_z1 - coeff * gain + gain
-- first, calculate the front part of filter-equation: part1 = coeff * gain_z1
-- load multiplier
mult_in_a <= resize(signed("0" & std_logic_vector(gain)), mult_in_a'length); -- gain-value (from last sample = z^-1) is in unsigned Q8.15-format -> convert to signed Q8.15
mult_in_b <= coeff; -- coefficient in Q0.15-format
s_SM_Main <= s_SM_Main + 1; -- go into next state
elsif (s_SM_Main = 2) then
-- store first part and calculate next part of filter: coeff * gain
-- convert resulting Q8.30 back to unsigned Q8.15 by removing/ignoring least-significant 15 bits
gain <= unsigned(std_logic_vector(mult_out)(22+15 downto 0+15)); -- we ignore the signed-bit here, as gain is unsigned
-- load multiplier
mult_in_a <= resize(signed("0" & std_logic_vector(gain_set)), mult_in_a'length); -- desired gain is in unsigned Q8.0-format -> convert to signed Q8.0
mult_in_b <= coeff; -- coefficient in Q0.15-format
s_SM_Main <= s_SM_Main + 1; -- go into next state
elsif (s_SM_Main = 3) then
-- calculate full value: gain = coeff * gain_z1 - coeff * gain + gain
-- convert gain_set from unsigned Q8.0 to unsigned Q8.15
-- convert multiplication-result (alpha*gain_set) from signed Q8.15 to unsigned Q8.15
-- calc: (alpha*gain) + (gain_set - alpha*gain_set)
gain <= gain + (unsigned(std_logic_vector(gain_set) & "000000000000000") - unsigned(std_logic_vector(mult_out)(22 downto 0)));
s_SM_Main <= s_SM_Main + 1; -- go into next state
elsif (s_SM_Main = 4) then
-- calculate the output signal
-- load multiplier
mult_in_a <= sample_l; -- audio-sample for left-channel
mult_in_b <= signed("0" & std_logic_vector(gain)(22 downto 8)); -- convert current gain-value from Q8.15 into Q8.7-format
s_SM_Main <= s_SM_Main + 1;
elsif (s_SM_Main = 5) then
-- convert resulting signed Q8.7 back to Q8.0, divide by 256 and store sample into signal
sample_l <= resize(shift_right(mult_out, 8 + 7) , bit_width);
-- load multiplier
mult_in_a <= sample_r; -- audio-sample for right channel
mult_in_b <= signed("0" & std_logic_vector(gain)(22 downto 8)); -- convert current gain-value from Q8.15 into Q8.7-format
s_SM_Main <= s_SM_Main + 1;
elsif (s_SM_Main = 6) then
-- output samples and set sync_out
sample_l_out <= sample_l;
sample_r_out <= resize(shift_right(mult_out, 8 + 7) , bit_width); -- convert resulting signed Q8.7 back to Q8.0, divide by 256 and output signal
-- indicate if gate is closed
if (shift_right(gain, 15) < 250) then
gate_closed_out <= '1';
else
gate_closed_out <= '0';
end if;
sync_out <= '1';
s_SM_Main <= s_SM_Main + 1;
elsif (s_SM_Main = 7) then
-- cleanup and return to state 0
sync_out <= '0';
s_SM_Main <= 0;
end if;
end if;
end process;
end rtl;
The coefficients can be calculated like this:
C
typedef union
{
uint32_t u32;
int32_t s32;
uint16_t u16[2];
int16_t s16[2];
uint8_t u8[4];
int8_t s8[4];
float f;
}data_32b;
typedef union
{
uint16_t u16;
int16_t s16;
uint8_t u8[2];
int8_t s8[2];
}data_16b;
struct sNoisegate {
// user-settings
float threshold = -80; // value between -80 dBfs (no gate) and 0 dBfs (full gate) -> 2^23..2^13.33
float range = 48; // value between 48dB (full range) and 3dB (minimum effect)
float attackTime_ms = 10;
float holdTime_ms = 50;
float releaseTime_ms = 258;
// filter-data
const float audio_bitwidth = 24; // depends on implementation of FPGA
data_32b value_threshold;
const float gainmin_bitwidth = 8; // depends on implementation of FPGA
data_16b value_gainmin;
data_16b value_coeff_attack;
data_16b value_hold_ticks;
data_16b value_coeff_release;
};
void recalcNoiseGate(struct sNoisegate *Noisegate) {
Noisegate->value_threshold.u32 = pow(2, (Noisegate->audio_bitwidth-1) + (Noisegate->threshold/6.0f)) - 1; // only bitwidth-1 can be used for samples, as MSB is for sign
// range of 60dB means that we will reduce the signal on active gate by 60dB. We have to convert logarithmic dB-value into linear value for gain
float value_gainmin_fs = pow(2, Noisegate->gainmin_bitwidth)-1; // maximum allowed value within FPGA
Noisegate->value_gainmin.u16 = saturate_f(value_gainmin_fs/(pow(10, Noisegate->range/20)), 0, value_gainmin_fs);
Noisegate->value_coeff_attack.s16 = round(exp(-2197.22457734f/(audiomixer.sampleRate * Noisegate->attackTime_ms)) * 32767); // convert to Q15
Noisegate->value_hold_ticks.u16 = Noisegate->holdTime_ms * (audiomixer.sampleRate / 1000.0f);
Noisegate->value_coeff_release.s16 = round(exp(-2197.22457734f/(audiomixer.sampleRate * Noisegate->releaseTime_ms)) * 32767); // convert to Q15
}