Skip to content

Commit 5c6a5b8

Browse files
committed
Use a logarithmic-based calculation, instead of the linear one. Does not change much, audibly, but certainly cleaner.
Also, limit the maximum rate of compression adjustment.
1 parent 1578d7b commit 5c6a5b8

File tree

2 files changed

+37
-6
lines changed

2 files changed

+37
-6
lines changed

Readme.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,29 @@ Ok, so how to get started?
8383
*Note*: When adjusting parameters, the status LEDs will briefly change their role from indicating signal and compression levels to a very rough indication of the parameter that was changed. No LEDs active
8484
signifies a very low value, with LEDs lighting up from D12 to D10 in that order for higher values. If either the low or high end of the scale is reached D13 will light up in addition.
8585

86+
## Background: Inside the ATMega black box
87+
88+
The cuircit is simple, you now know how to adjust parameters, but what exactly is happening in the code?
89+
90+
- Sampling windows
91+
- Moving averages
92+
- The actual volume adjustment is then calculated as follows: If the current signal is *n*dB above the threshold level, divide *n* by the *ratio*, and adjust the signal level to an output of *threshold* + *n*/*ratio* dB.
93+
Now, since decibel is a logarithmic scale, that translates to the following pseudo-code (*current* is the current output voltage level, *threshold* is the threshold voltage level):
94+
```
95+
target_level = exp((log(current) - log(threshold))/ratio + log(threshold));
96+
```
97+
Now the ATMega is not exactly fast at floating point math, let alone at calculating logs. The above is just prohibitively slow. Fortunately it can be optimized:
98+
```
99+
target_level = threshold * exp(log(current)/ratio) / exp(log(threshold)/ratio;
100+
// rewriting the division by ratio in the exponent:
101+
target_level = threshold * pow(exp(log(current)), 1/ratio) / pow(exp(log(threshold)), 1/ratio);
102+
// simplifying:
103+
target_level = threshold * pow(current, 1/ratio) / pow (threshold, 1/ratio);
104+
// great! The logs are gone. Now:
105+
target_level = threshold * pow(current/threshold, 1/ratio);
106+
```
107+
Now the ATMega can handle that calculation just fine. As a last step, we simply set the duty-cycle of Pin 3 to be 255 * target_level / current_level.
108+
86109
## More to come
87110

88111
... but writing this up is more work than it may seem. If you find this useful, consider donating a bread crumb or two via Paypal: thomas.friedrichsmeier@gmx.de

compressor.ino

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ int threshold = 18; // minimum signal amplitude before the compressor will kick
2727
float ratio = 3.0; // dampening applied to signals exceeding the threshold. n corresponds to limiting the signal to a level of
2828
// threshold level plus 1/3 of the level in excess of the threshold (if possible: see duty_min, below)
2929
// 1(min) = no attenuation; 20(max), essentially limit to threshold, aggressively
30+
const float max_transition_rate = 1.11; // although the moving averages for attack and release will result in smooth transitions
31+
// of the compression rate in most regular cases sudden signal spikes can result in abrupt transitions, introducing
32+
// additional artefacts. This limits the maximum speed of the transition to +/- 11% of current value.
3033

3134
//// Some further constants that you will probably not have to tweak ////
3235
#define DEBUG 1 // serial communication appears to introduce audible noise ("ticks"), thus debugging is diabled by default
@@ -68,6 +71,7 @@ int32_t now = 0; // start time of current loop
6871
int32_t last = 0; // time of last loop
6972
int duty = 255; // current PWM duty cycle for attenuator switch(es) (0: hard off, 255: no attenuation)
7073
byte display_hold = 0;
74+
float invratio = 1 / ratio; // inverse of ratio. Saves some floating point divisions
7175

7276
#if DEBUG
7377
int it = 0;
@@ -162,7 +166,10 @@ void loop() {
162166
const int attack_threshold = threshold * attack_f;
163167
int attack_duty = 255;
164168
if (attack_mova > attack_threshold) {
165-
const int target_level = (attack_mova - attack_threshold) / ratio + attack_threshold;
169+
const int target_level = attack_threshold * pow ((float) attack_mova / attack_threshold, invratio);
170+
// Instead of the logrithmic volume calculation above, the faster linear one below seems too yield
171+
// acceptable results, too. Hoever, the Arduino is fast enough, so we do the "real" thing.
172+
// const int target_level = (attack_mova - attack_threshold) / ratio + attack_threshold;
166173
attack_duty = (255 * (int32_t) target_level) / attack_mova;
167174
#if DEBUG
168175
if (it == 0) {
@@ -180,20 +187,20 @@ void loop() {
180187
}
181188
// if the new duty setting is _below_ the current, based on attack period, check release window to see, if
182189
// the time has come to release attenuation, yet:
183-
if (attack_duty < duty) duty = attack_duty;
190+
if (attack_duty < duty) duty = max (attack_duty, duty / max_transition_rate);
184191
else {
185192
int release_duty = 255;
186193
const int release_threshold = threshold * release_f;
187194
if (release_mova > release_threshold) {
188-
const int target_level = (release_mova - release_threshold) / ratio + release_threshold;
195+
const int target_level = release_threshold * pow ((float) release_mova / release_threshold, invratio);
189196
release_duty = (255 * (int32_t) target_level) / release_mova;
190197
} else {
191198
release_duty = 255;
192199
}
193-
if (release_duty >= duty) duty = release_duty;
200+
if (release_duty >= duty) duty = min (release_duty, duty * max_transition_rate);
194201
#if DEBUG
195202
else {
196-
Serial.println("waiting for release");
203+
Serial.println("hold");
197204
}
198205
#endif
199206
}
@@ -203,8 +210,9 @@ void loop() {
203210
if ((display_hold < 90) && handleControls()) { // check state of control buttons. If any was pressed, the status LEDs shall not be
204211
// updated for the next half second (they will indicate control status, instead)
205212
display_hold = 100;
213+
invratio = 1 / ratio;
206214
#if DEBUG
207-
Serial.print("threshold");
215+
Serial.print("threshold - ");
208216
Serial.println(threshold);
209217
#endif
210218
}

0 commit comments

Comments
 (0)