Algorithm Overview: In the figure above, I illustrate the basic signal flow through my DRC algorithm. It's a feed-forward design with a side-chain that computes the time-varying amount of gain that should be applied. The function of each block is described below:
- Pre-Process Signal: In this block, I apply the "pre-gain", which is the amount of gain that will be applied when the compressor is in its linear regime. Also in this block, I use a high-pass filter to remove any DC offset in the audio signal.
- Level Estimator: For the compressor to vary the gain based on the loudness of the signal, it needs to first estimate the loudness of the signal. That's what this block does.
- Gain Calculator: Using the estimated loudness, this block calculates how much to reduce the gain of the system. It calculates the desired gain reduction knowing the "compression ratio" and "compression threshold" that have been supplied by the user. It also smooths the compressed gain value through time via "attack" and "release" time constants that have been supplied by the user.
- Apply Gain: Once the desired compressed gain value has been calculated by the side-chain, this block applies the gain to the audio signal.
Algorithm Parameters: In the signal flow diagram below, I show the algorithm parameters that are available for the user to set. The main tricky part is how to set the time constant for the Level Estimator. By default, I have this value always scale itself to be 20% of the time constants that the user has set for the Gain Calculator block. If you don't like this default behavior, you can specify your own value using the method setLevelTimeConst_sec().
Example Sketch: As part of including this DRC algorithm in my OpenAudio_ArduinoLibrary (GitHub page here), I made sure to include an example sketch called BasicCompressor_Float. This example code assumes that you have a Teensy Audio Board along with a Teensy 3.5/3.6. Within the code, there are a couple of options:
- USB Audio: For my testing, I chose to send and receive audio over the USB link, instead of via analog audio cables. See this post, if you want more info on how to use the Teensy's USB Audio link. To enable USB audio in this sketch, set DO_USB to a value of one.
- Fast or Slow: Another choice is how to set the values for the compressor's parameters. In my example, I have two sets of parameters. One set gives a "fast" compressor response that is appropriate for quick limiting of very loud sounds. The other set of values gives a "slow" response that can be used as a automatic volume control. For my testing, I chose to use the "fast" response, as shown below.
Test Setup: For my testing, I'm using my breadboard prototype of my Teensy Hearing Aid, which is just a Teensy 3.6 with the Teensy Audio Board (plus microphones, battery, and Bluetooth module, which are not relevant to this test). I've got it plugged into my laptop via USB, which is also used to send and receive audio to the Teensy via the Teensy's ability to do USB Audio. On my laptop, I'm using the Arduino IDE to set the compressor's parameters (again, I'm using the "fast" configuration) and I'm using Audacity to send the audio to the Teensy and to record the audio returned by the Teensy.
Testing, Amplitude Sweep: For my first test, I generated a steady 2kHz tone whose amplitude increases from quiet to loud at a rate of about 6 dB/sec (my test signals are shared here). This is the signal that I'm sending to the Teensy (also shown in blue in the figure below). When sending this signal, the compression algorithm on the Teensy returns the signal shown in orange. If you look at the loudest portion of the audio (starting around time = 8 seconds) the amplitude of the orange output signal looks to be squashed compared to the blue input signal. This is the effect of the compressor! It successfully reduced the dynamic range of the audio, as intended.
Quantifying the Response: One important question is whether the compressor kicked in at the correct amplitude. To answer this question, I assessed the instantaneous amplitude of each signal, as shown in the bottom plot in the figure above. It clearly shows that the compression of the output signal begins at an amplitude that is 15 dB below full-scale. This is exactly the value that was specified in the example code by the call to setThresh_dBFS(). It is pleasing to see that the algorithm works as intended.
Testing, Step Changes in Amplitude: Another important question is whether the compressor is responding at the correct speed to changes in signal amplitude. To answer this question, I generated a test signal with instantaneous step changes in amplitude, as shown in blue in the figure below. The compressor's response is shown in orange.
Quantifying the Attack and Release: When I extract the amplitude of each signal versus time (bottom plot), you can see that the compressor lags in its response to the step changes. When the input signal gets louder (say, at time = 2 sec), the compressor's 5 msec "attack" time constant allows the compressor to response very quickly. But, when the input signal gets quieter (say, at time = 3 sec), the compressor's 200 msec "release" time constant makes it respond more slowly. In my code, these time constants are used in the traditional sense of "time constant", meaning that 63% of the change (in dB) is achieved in one time constant. Based on the figure above, it appears that my compressor is responding at the correct speed. I'm pleased.
Next Steps: With the dynamic range compressor working correctly (and sounding pretty good when used with my Teensy Hearing Aid's microphones and my headphones), I'm well on my way to completing the signal processing hardware and software for a very basic hearing aid. The main component that I'm still missing is some sort of frequency compensation to increase the gain on the frequencies that the listener needs to hear better, while reducing gain on those frequencies that the listener already hears well enough. Stay tuned!
Follow-Up: This algorithm converts to dB and back. When doing the log10(x) and pow(10,x), be sure to call the correct versions and they'll run *much* faster. Check it out here.
This comment has been removed by a blog administrator.
ReplyDeleteThis comment has been removed by a blog administrator.
ReplyDeleteHi.
ReplyDeleteWhy do you want attack time and a knee?
When you have full control of the compressor in software, wouldn´t it be better to have 0ms attack and a soft knee? Graduatly increasing and decreasing the gain in a natural way based on the level estimator..
The primary reason why I've formulated my compressor in terms of attack and release times and knee points is for historical reasons. This is how analog circuits were engineered to be compressors, so the language used by the broader community centers around the notions of attack and release times and knee points.
DeleteA secondary reason for formulating it this way is that it works and sounds decent. For example, the input/output graph showing a knee-point may look artificial, but it does not sound particularly artificial. When I have played with soft knee compression, I don't hear that much difference. And, furthermore, the soft knee isn't particularly better sounding (to my ears), it's just different sounding.
So, I totally agree with you that discussing compression in terms of attack and release times and knee points is old fashioned. But, it works decently well. And, like everyone else getting started in this area, I figured that I'd start with this common reference point.
Thanks so much for your interest!
Chip
How many audio samples do you average over to determine the magnitude? And do you buffer these samples so that you can operate immediately on them before they go out?
ReplyDeleteGreat question. Looking at my own code on GitHub (https://github.com/chipaudette/OpenAudio_ArduinoLibrary/blob/master/AudioEffectCompressor_F32.h), I see that the magnitude is assessed in the method "calcAudioLevel_dB".
DeleteLooking at this function, I see that it computes the audio level on a sample-by-sample basis using a first-order, low-pass IIR filter. It is this low-pass filter that does the averaging that you are asking about. The cutoff frequency of the filter defines how long the averaging is (the averaging period is the inverse of the cutoff frequency). The cutoff frequency is set by the "c1" and "c2" values, which are computed from "level_lp_const".
Looking at where "level_lp_const" is defined, I see that it is either 1/5 of the minimum of the attack and release times, though no shorter than 0.002 seconds (ie, 2 milliseconds). So the averaging for the magnitude estimation is at least 0.002 seconds.
If the system is running at a sample rate of 44kHz, 2 milliseconds is 88 samples.
Chip
Hi! Great work indeed! Do you think your DRC project is suitable as an auto-volume device between a satellite receiver and a TV-Soundbar? I should have enough electronics, software and audio knowledge to use your source code but I would love to get some basic feedback if I'm on the right track. The idea is to use your compressor to balance the volume between loud and silent scenes in movies. With the goal to help my elderly parents watch TV. In the best case I can add some equalizer features to do some voice enhancement. Many thanks.
ReplyDeleteHi, are you still active in this field, I'm doing some audio HW product in quantity, maybe we can cooperate. yubichun@gmail.com
ReplyDelete