Stupid question: Compressor from first principles

DSP, Plugin and Host development discussion.
Post Reply New Topic
RELATED
PRODUCTS

Post

I'm trying to develop a compressor from first principles as an intellectual exercise without looking at any existing code.

Suppose the compressor is just looking at peaks, not RMS, to keep it simple.

I've made progress. But a question regarding attack has been bugging me. When the signal goes above the threshold, the compressor computes the amount of gain reduction needed and remembers that the reduction will be applied in the near future (the attack time). The signal is undisturbed until then. But what happens if the signal goes above the threshold AGAIN, at a higher level, before the attack time comes?

Is the upcoming gain reduction amount recomputed according to that higher level? Is it just ignored? Or is it remembered for the attack cycle after this one?

I don't know why my little brain is approaching this as a recursive problem, which I'm sure it's not. :(

Post

It's been several month since I implemented a compressor for a ducking delay but I had a (quick) look at my code and I hope that I might help you a bit. All in all the implementation was much harder than I initially thought. One of the reasons was that I also approached it like you without looking at existing implementation. ;)

One thing that I'd like to make clear is that the attack phase starts as soon as the peak or RMS value crosses the threshold and the release phase starts as soon as it falls beneath the threshold. The attack time is the time that it takes until the full compression is applied. From your post I get the impression that you might think that the attack time is a delay after which we go in one fast jump from no compression to full compression: "and remembers that the reduction will be applied in the near future (the attack time)."

Another important thing is that you cannot compute one attenuation factor from the ratio that you then just apply for all samples. Instead the attenuation factor has to be calculated individually for each sample. I think this insight might help with your question "But what happens if the signal goes above the threshold AGAIN, at a higher level, before the attack time comes?". There is no "recomputation" because it needs to be computed per sample (peak/RMS) value anyway.

Here's a very coarse description of what my code does:
  • Compute a gate buffer. This buffer stores whether the RMS/peak samples are above threshold (gate(i)=1) or beneath the threshold (gate(i)=0).
  • Compute an attenuation buffer that does not take the attack or release into account yet. It contains the resulting attenuation per sample. If the gate is on (1, above threshold) then the attenuation factor is computed using the given peak/RMS value for that sample. If the gate is off (0, below threshold) then a 1 is written for that sample, i.e. no attenuation. If you would apply the resulting attenuation buffer to the input samples it would sound quite brutal because it would act like the attack and release times are 0.
  • The attenuation buffer and the gate buffer are then used in a class which process the attenuation buffer with regards to attack and release. I won't go into the details but it just smooths the attenuation buffer depending on the gate input. As long as the gate is on the attack is "ramped up". If the gate is off the release is applied.
Passed 303 posts. Next stop: 808.

Post

copperx wrote: Wed Jun 17, 2020 5:10 pm When the signal goes above the threshold, the compressor computes the amount of gain reduction needed and remembers that the reduction will be applied in the near future (the attack time). The signal is undisturbed until then.
Actually this isn't the typical case. The signal will be perturbed as the envelope approaches the peak value, at the speed set by the attack time

Post

BlitBit wrote: Wed Jun 17, 2020 8:08 pm It's been several month since I implemented a compressor for a ducking delay but I had a (quick) look at my code and I hope that I might help you a bit.
Thank you so much!!! Your post is invaluable. It does clear up a few misconceptions I had, especially the one about attack and the computation of attenuation.
BlitBit wrote: Wed Jun 17, 2020 8:08 pm Here's a very coarse description of what my code does:
  • Compute a gate buffer. This buffer stores whether the RMS/peak samples are above threshold (gate(i)=1) or beneath the threshold (gate(i)=0).
  • Compute an attenuation buffer that does not take the attack or release into account yet. It contains the resulting attenuation per sample. If the gate is on (1, above threshold) then the attenuation factor is computed using the given peak/RMS value for that sample. If the gate is off (0, below threshold) then a 1 is written for that sample, i.e. no attenuation. If you would apply the resulting attenuation buffer to the input samples it would sound quite brutal because it would act like the attack and release times are 0.
Just one question: so these buffers are arrays with the length of a block, and you compute these per block, not per sample?

Post

Your average compressor would typically use some sort of envelope follower that essentially computes the desired gain reduction, then if we are currently compressing less we adjust the current gain reduction towards the target using attack dynamics and if we are currently compressing more we use release dynamics to gradually reduce the compression. So in a sense, you are never actually compressing by the computed amount, but rather you are constantly (ie. every sample) adjusting the gain reduction towards the current desired amount with the speed (or more generally dynamics) depending on whether we should be compressing more (use attack) or less (use release).

So most of the time when you see "attack time" and "release time" on a compressor, those are not really the times that the compressor spends on attack and release phase, but rather just the times that it would theoretically take to perform an attack or release (or in many cases some percentage of it, when IIR dynamics are used) if we had an input signal that suddently went from 0 to 1 and then after the attack is finished from 1 to 0 again; they don't really control the attack and release "times" as much as just the speeds at which you track the signal.

Post

So most of the time when you see "attack time" and "release time" on a compressor, those are not really the times that the compressor spends on attack and release phase, but rather just the times that it would theoretically take to perform an attack or release (or in many cases some percentage of it, when IIR dynamics are used) if we had an input signal that suddenly went from 0 to 1 and then after the attack is finished from 1 to 0 again; they don't really control the attack and release "times" as much as just the speeds at which you track the signal.
The idea of the envelope follower is brilliant and solves most of my questions. Not having one makes the compressor algorithm hard to reason. It's really clever, although I imagine envelope followers are DSP 101.

This makes me wonder, are there compressors out there that don't use envelope followers, and if so, are there alternative designs?

Post

copperx wrote: Thu Jun 18, 2020 3:43 am This makes me wonder, are there compressors out there that don't use envelope followers, and if so, are there alternative designs?
There are a whole lot of weird things out there, but for traditional compressor you pretty much need some kind of envelope follower. That said my description should be taken as a very high-level overview as there are probably just about as many variations on the basic concept as there are different compressors.

Post

copperx wrote: Wed Jun 17, 2020 8:52 pm
BlitBit wrote: Wed Jun 17, 2020 8:08 pm It's been several month since I implemented a compressor for a ducking delay but I had a (quick) look at my code and I hope that I might help you a bit.
Thank you so much!!! Your post is invaluable. It does clear up a few misconceptions I had, especially the one about attack and the computation of attenuation.
You're welcome! Happy that could help you a bit. :)
copperx wrote: Wed Jun 17, 2020 8:52 pm
BlitBit wrote: Wed Jun 17, 2020 8:08 pm Here's a very coarse description of what my code does:
  • Compute a gate buffer. This buffer stores whether the RMS/peak samples are above threshold (gate(i)=1) or beneath the threshold (gate(i)=0).
  • Compute an attenuation buffer that does not take the attack or release into account yet. It contains the resulting attenuation per sample. If the gate is on (1, above threshold) then the attenuation factor is computed using the given peak/RMS value for that sample. If the gate is off (0, below threshold) then a 1 is written for that sample, i.e. no attenuation. If you would apply the resulting attenuation buffer to the input samples it would sound quite brutal because it would act like the attack and release times are 0.
Just one question: so these buffers are arrays with the length of a block, and you compute these per block, not per sample?
Yes, I compute them in whole blocks as they arrive through the plugin interface. For example I have one buffer for the peak/RMS values that is filled by looking at every sample of the current buffer and computing the peak/RMS value for each sample. The peak/RMS value at position i of that buffer then belongs to the sample at position i.

The peak/RMS buffer is then in turn used to compute the gate buffer and so on.

One advantage of going block wise through the computations is that the CPU cache is kept hot which gives better performance.
Passed 303 posts. Next stop: 808.

Post Reply

Return to “DSP and Plugin Development”