Envelope Follower/Detector for bass

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

Post

I'm interested in designing an EF (envelope follower) for low frequency (bass instruments).

I'm sure there are some 'design tips' for such envelope followers for ~50-200 HZ frequencies and the unique dynamics of bass instruments/tones.

My naive knowledge of this suggests that an RMS (as opposed to peak) detector with some kind of 'hold' mechanism should be the way ('hold' to avoid the Envelope follower "riding" the slow troughs and peaks of the signal itself)?

Any thoughts ?

Post

why don't you implement something, or just test out something in one of the many existing modular prototyping environments, put together some blocks and see how that goes

my thoughts are that "hold" is probably not needed, but i don't know the nature of the problem you're trying to solve
It doesn't matter how it sounds..
..as long as it has BASS and it's LOUD!

irc.libera.chat >>> #kvr

Post

The challenge of low-frequency envelope detection is to come up with a filter that gives an acceptable trade-off between preserving the envelope shape without excessive blurring (and other time-domain problems like phase-distortion or ringing) while still providing good enough attention to filter out the carrier ripple just an octave or two above the upper limit of the "envelope band" (eg. up to 20Hz or so).

My 0.02 cents is to forget about one-poles and simple box-filters and just treat it as a filter design problem.

ps. for RMS you probably don't want to "hold" because that's not RMS then, but if you want true peaks, you can use an all-positive FIR average (not necessarily boxcar) and a sliding-max that matches the FIR length; this way the max-window will carry over small ripples and the FIR averaging can actually hit the true peak value.

pps. I would also generally try to extract the "ideal" envelope first and then if you want to do some envelope dynamics, process those afterwards.. but that's just me :P

Post

Hi Ross

Maybe it would be too slow for your purposes, but in a lookahead RMS compressor where I wanted "minimal bass ripple consistent with fairly fast envelope response" I prefaced a bunch of cascaded boxcar filters with a short "bass hilbert". The boxcar filters did just fine except at low bass. They would also have done fine at low bass, except "slower response than what I wanted" if all the bass smoothing came from the cascaded boxcar filters.

I had earlier experimented with some "wideband hilbert" but the ones I tried added what I considered too much latency. Also the wideband hilbert helped make "very smooth envelopes" with sine wave inputs, but for whatever reason made some waveforms such as ramp and square waves have "less smooth envelopes" than without the wideband hilbert. So that is why I tried the low frequency hilbert, only the bass frequencies too low to smooth with other methods.

However my "fast as possible" rms compressor envelope was rather slow. It was intended to have as little distortion I could get, trimming just one or two dB dynamic range "transparent" with very low ratio, like 1.2:1 or whatever. IOW if a tradeoff between envelope ripple versus envelope response time, for this task I'd rather have low ripple than a fast envelope. Just that the "bass hilbert" reduiced bass ripple, and without the "bass hilbert" the envelope would have needed to be made much slower to get the same amount of bass ripple suppression. So far as I could measure it. I am not an expert.

So maybe this would be too slow for your uses. Or maybe not, dunno.

Here are a few code snippets in reaper jsfx code, which is similar to C. A little well-bahaved "zdf" filter object, and some setup lines instantiating the bass hilbert, and the process lines applying the bass hilbert.

If you like you can download the entire jsfx compressor plugin (which is just a plain text file) here-- http://errnum.com/html/jcjr_rms_compressor.html

In my testing, what I would see, for instance feeding a 20 to 20 kHz slow sine sweep thru just the "LF Hilbert" then the squaring and addition of the two allpass outputs-- At low frequencies the hilbert itself, adding the two squared outputs, gives a pretty smooth DC output. At higher frequencies as the "LF Hilbert" has diminishing effect, a sine wave input yields a sine wave output of 2X frequency, and no negative values. So when you smooth this DC offset, double-frequency sine wave, you get a positive DC "power envelope". Take the square root to get the RMS envelope.

That double-frequency rectification is just what happens when you square a sine wave. It makes filtering easier, Given whatever lowpass smoothing filter you decide to use, if the input frequency is 100 Hz then the squared sine's frequency is 200 Hz, which will be "better smoothed" than the original 100 Hz.

Just that the "LF Hilbert" does a little pre-smoothing on the low frequencies so your main smoother can possibly have "good enough" smoothing at a faster response time.

Maybe most of the above is simple and obvious, but I usually think simple and obvious so what else can your expect? :)

Code: Select all

 //First order trapezoidal filter object
  //Code adapted from Vadim Zavalishin's book "The Art of VA Filter Design"
  FIRST_ORD_FILTTYPE_LOPASS = 0;
  FIRST_ORD_FILTTYPE_HIPASS = 1;
  FIRST_ORD_FILTTYPE_ALLPASS_ADV = 2; //+180 degrees phase shift at DC, descending to 0 degrees phase shift at nyquist
  FIRST_ORD_FILTTYPE_ALLPASS_RET = 3; //0 degrees phase shift at DC, descending to -180 degrees phase shift at nyquist
  
  //FiltTyps: Use one of the above to set the return value of FirstOrdTrapezoidFilter_DoSamp()
  //        : However, all values are simultaneously accessible after calling DoSamp() by reading
  //        : TheFilter.lp, TheFilter.hp, TheFilter.ap_A, TheFilter.ap_R
  //a_FiltFC: Filter center frequency in Hz
  //a_SampRate: Samplerate of filter
  function FirstOrdTrapezoidFilter_Init(a_FiltType, a_FiltFC, a_SampRate)
  (
    this.FT = a_FiltType;
    this.SR = a_SampRate;
    this.Nyquist = floor(this.SR * 0.5);
    this.FC = min(a_FiltFC, this.Nyquist - 1);

    this.s = 0.0;
    this.lp = 0;
    this.hp = 0;
    this.ap_A = 0;
    this.ap_R = 0;
    
    //calculate coefficient 
    this.g = tan($pi * this.FC / this.SR);
    this.g /= (1 + this.g);
  );
  
  function FirstOrdTrapezoidFilter_SetFC(a_FiltFC)
  (
    this.FC = min(a_FiltFC, this.Nyquist - 1);
    this.g = tan($pi * this.FC / this.SR);
    this.g /= (1 + this.g);
  );
  
  //Returns the filtered sample
  function FirstOrdTrapezoidFilter_DoSamp(a_InSamp)
  local (l_v, l_result)
  (
    //Vadim Zavalishin code
    //v = (x-z1_state)*g/(1+g);
    //y = v + z1_state;
    //z1_state = y + v;
    l_v = (a_InSamp - this.s) * this.g;
    this.lp = l_v + this.s;
    this.s = this.lp + l_v;
    this.hp = a_InSamp - this.lp;
    this.ap_A = this.hp - this.lp;
    this.ap_R = this.lp - this.hp;
    
    (this.FT == FIRST_ORD_FILTTYPE_LOPASS) ?
      l_result = this.lp
    :
      (this.FT == FIRST_ORD_FILTTYPE_HIPASS) ?
        l_result = this.hp
      :
        (this.FT == FIRST_ORD_FILTTYPE_ALLPASS_ADV) ?
          l_result = this.ap_A
        :
          l_result = this.ap_R;
    l_result;
  );

//==== Instantiate allpass filters ====
  //Init Allpass filters for "low frequency hilbert" bass ripple reduction, left channel
  o_APFilt_0_L.FirstOrdTrapezoidFilter_Init(FIRST_ORD_FILTTYPE_ALLPASS_ADV, 8.336807, srate);
  o_APFilt_1_L.FirstOrdTrapezoidFilter_Init(FIRST_ORD_FILTTYPE_ALLPASS_RET, 75.031263, srate);
  o_APFilt_2_L.FirstOrdTrapezoidFilter_Init(FIRST_ORD_FILTTYPE_ALLPASS_ADV, 30.195915, srate);
  o_APFilt_3_L.FirstOrdTrapezoidFilter_Init(FIRST_ORD_FILTTYPE_ALLPASS_RET, 271.763235, srate); 

//==== Process a sample thru the allpass filters ====
 //Allpass filter chains to apply "low frequency hilbert"
  //  Which helps minimize low-frequency ripple
  //With the cascaded running sum smoothing, higher frequency ripple naturally well-smoothed
  //  without any "hilbert phase shifting"
  //A "wide band" Hilbert would need more allpass filters
  //  and also introduce more time-delay in the envelopes
  //So the bass is the only area of concern
  ls_AP0_L = o_APFilt_0_L.FirstOrdTrapezoidFilter_DoSamp(InputSample_Left);
  ls_AP0_L = o_APFilt_1_L.FirstOrdTrapezoidFilter_DoSamp(ls_AP0_L); //first allpass series pair
  ls_AP1_L = o_APFilt_2_L.FirstOrdTrapezoidFilter_DoSamp(InputSample_Left);
  ls_AP1_L = o_APFilt_3_L.FirstOrdTrapezoidFilter_DoSamp(ls_AP1_L); //second allpass series pair
  //Square and add the two left channel allpass chains
  ls_HSS_L = ls_AP0_L * ls_AP0_L + ls_AP1_L * ls_AP1_L;
  //and then the ls_HSS_L squared sample is further smoothed as any other squared sample RMS method
  //because of the addition, we get a little gain here, to be compensated elsewhere as desired

Post

JCJR wrote:Maybe it would be too slow for your purposes, but in a lookahead RMS compressor where I wanted "minimal bass ripple consistent with fairly fast envelope response" I prefaced a bunch of cascaded boxcar filters with a short "bass hilbert". The boxcar filters did just fine except at low bass. They would also have done fine at low bass, except "slower response than what I wanted" if all the bass smoothing came from the cascaded boxcar filters.
Just in case it's not obvious, cascading boxcars (of equal length) gives you B-spline kernels that tend towards gaussian as you increase the order (ie. cascade more boxcars). In some sense the result can probably be considered "optimal" as far as the time-domain response goes. Such a filter will also be symmetric in time-domain, which can be a nice thing (or an annoyance) depending on what you plan on using the results for.

As far as Hilbert goes, it only reliably improves the results when the input is close to a sinusoid. While this is not a problem for a narrow low-pass filtered bass-band, the results often worse than naive if you just feed it some wide-band signal as the phase-shifted frequencies can recombine in unexpected ways. For waveforms like saw or square, the harmonics that would normally be "in phase" at the zero-crossing of the sine will actually end up "in phase" at the peak of the cosine, so you can get very high amplitude spikes (and I believe for the ideal continuous case they'd be theoretically infinite).

:)

Post

mystran wrote:
JCJR wrote:Maybe it would be too slow for your purposes, but in a lookahead RMS compressor where I wanted "minimal bass ripple consistent with fairly fast envelope response" I prefaced a bunch of cascaded boxcar filters with a short "bass hilbert". The boxcar filters did just fine except at low bass. They would also have done fine at low bass, except "slower response than what I wanted" if all the bass smoothing came from the cascaded boxcar filters.
As far as Hilbert goes, it only reliably improves the results when the input is close to a sinusoid. While this is not a problem for a narrow low-pass filtered bass-band, the results often worse than naive if you just feed it some wide-band signal as the phase-shifted frequencies can recombine in unexpected ways. For waveforms like saw or square, the harmonics that would normally be "in phase" at the zero-crossing of the sine will actually end up "in phase" at the peak of the cosine, so you can get very high amplitude spikes (and I believe for the ideal continuous case they'd be theoretically infinite).

:)
Thanks mystran

Yeah if I wanted to make a "very smooth" bass guitar envelope follower I would try the bass hilbert to see if it would perform "better" than other alternatives such as peak-holds and such. I used peak holds long ago in some compressors and sometimes it seemed to do good and sometimes not.

Just using a short allpass network which only has "hilbert-ish" properties at low frequencies, doesn't seem to do much damage to proper smoothing of ramps or square waves or whatever. So far as I could measure.

The bass hilbert only "fully does its thing" to a few sine waves in the lower harmonics of a low-frequency ramp or square wave, leaving the higher harmonics relatively un-mutilated, so that the bass hilbert doesn't make smoothing of the higher harmonics an even tougher problem. I agree that a wideband hilbert, in my crude tests, seemed only an improvement for sine waves.

I was surprised that the rube goldberg algorithm delivered flat frequency response after all smoothing stages were applied. Just looking at the "bass hilbert" output of a slow sine sweep, it is "fairly smooth dc" in the bass, morphing into "rectified sine wave" at higher frequencies, with the 2 X amplitude squared sine waves having much bigger peaks than the "smooth dc" in the bass region. But once that signal is wide-band smoothed, it comes out flat frequency response, the post-hilbert smoothing "filling in the higher freq valleys by cutting down the higher freq peaks" resulting in the same DC level all the way up (if fed a constant-amplitude sine sweep input).

Post

mystran wrote:Just in case it's not obvious, cascading boxcars (of equal length) gives you B-spline kernels that tend towards gaussian as you increase the order (ie. cascade more boxcars). In some sense the result can probably be considered "optimal" as far as the time-domain response goes. Such a filter will also be symmetric in time-domain, which can be a nice thing (or an annoyance) depending on what you plan on using the results for.
Thanks mystran

For a bass guitar envelope follower, I probably would not expect to use boxcar filters after the bass hilbert. That is, IF the bass hilbert would even test out useful in that application.

Maybe boxcars would be useful on bass guitar but I'd suspect other smoothing might work better for (in my wild guessing)--
_1_ The envelope rapidly responds to note-start transients
_2_ While also having "as low ripple possible"
_3_ Also paradoxically "quickly decay after a note stops".
It seems a tall order to simultaneously get good performance on all three specs at bass frequencies.

My aforementioned hobby lookahead RMS compressor uses user-adjustable "processing delay" and I almost always set the lookahead to process samples "at the time-center of the FIR". On the web link I posted, near the bottom, are some illustrations of performance differences according to the amount of lookahead selected. Most of the dull detail is brutally beaten about the head and shoulders at this webpage-- http://errnum.com/html/jcjr_rms_compressor.html

From the web page explanation (my running sum boxcar object seems adequately protected against math instabilities)--
Running Sum Cascade: A single Running Sum can perfectly smooth some frequencies with less-perfect smoothing of other frequencies. The Running Sum mechanism and result has similarities to delay-based comb filters as found in chorus/flanger effects.

Similar to a delay-based comb filter-- As input frequency increases, a single Running Sum has a repeating pattern of strong smoothing versus weak smoothing. The “lucky frequencies” get perfectly smoothed and the “unlucky frequencies” have less-perfect smoothing.

For example, if a given Running Sum length naturally has a “lowest perfect smoothing” frequency of 50 Hz-- Then all multiples of 50 Hz are perfectly smoothed-- 50 Hz, 100 Hz, 150 Hz, 200 Hz, etc. Significant smoothing occurs in-between the “perfectly smoothed” frequencies, but the in-between frequencies are not perfectly smoothed.

We can get “excellent smoothing” of all frequencies from a cascade of series-connected Running Sum smoothers. Each Running Sum in the cascade is set to a different sample length. The first Running Sum perfectly smooths some frequencies and does significant smoothing to the in-between frequencies.

A second Running Sum length is selected which “perfectly smooths” another series of frequencies which are located in-between the first stage “perfect frequencies”. The second stage improves smoothing of ALL of the first-stage frequencies. The second stage can't make any frequency of first stage smoothing worse, and the different tuning of the second stage improves smoothing at frequencies where the first stage didn't work so great.

In my tests, a cascade of five Running Sums gives VERY GOOD smoothing of all frequencies above some minimum frequency. The minimum frequency is determined by the Running Sum length.
The cascade is user-adjustable from 10 ms up to 50 ms symmetrical attack/release. The boxcar filter lengths I use for nominal 10 ms AR are-- 2.974 ms, 3.417 ms, 3.924 ms, 4.508 ms, 5.178 ms. For other AR times, the ratios are preserved. For instance at 20 ms AR all time delays would be doubled. There could be math errors. Maybe some other set of time delays would result in better wideband smoothing, dunno.

Post

JCJR wrote: Maybe boxcars would be useful on bass guitar but I'd suspect other smoothing might work better for (in my wild guessing)--
_1_ The envelope rapidly responds to note-start transients
_2_ While also having "as low ripple possible"
_3_ Also paradoxically "quickly decay after a note stops".
It seems a tall order to simultaneously get good performance on all three specs at bass frequencies.
You can give more weight to more recent signal by using an asymmetric window.. like the most obvious alternative to boxcar is to stack one-poles, where setting time-constant to around half the boxcar width (which makes the step-responses of the two agree at around 80%) gives fairly similar overall "feel" but with more emphasis on the recent signal (but cascading multiple passes will still smooth the leading edge as well). With IIR filter you lose the finite window though, so if that's important you could try stacking something like linear ramps from a constant value to zero over some finite period (although I'm not entirely sure how to implement it efficiently then; I suspect it's possible, but never tried).

But yeah, getting good bass envelopes is hard and you probably want to compromise one aspect or another once you know what's important for the given application. :(

Post

mystran wrote:You can give more weight to more recent signal by using an asymmetric window.. like the most obvious alternative to boxcar is to stack one-poles, where setting time-constant to around half the boxcar width (which makes the step-responses of the two agree at around 80%) gives fairly similar overall "feel" but with more emphasis on the recent signal (but cascading multiple passes will still smooth the leading edge as well). With IIR filter you lose the finite window though, so if that's important you could try stacking something like linear ramps from a constant value to zero over some finite period (although I'm not entirely sure how to implement it efficiently then; I suspect it's possible, but never tried).

But yeah, getting good bass envelopes is hard and you probably want to compromise one aspect or another once you know what's important for the given application. :(
Thanks mystran

I've played with cascaded 1 poles, both unadorned 1 poles and 1 poles configured as attack-release stages (different time constant according to the input-vs-envelope level). Probably similar to what you mention, I noticed that 2 or more 1 poles, the shape of the attack and release gets a sigmoid shape, rather than the convex-shaped attack, concave-shaped release of a single 1 pole.

The sigmoid attack/release shapes are not as "perfectly symmetrical" as a gaussian FIR, but in my ignorance I liked the shape because it seemed to promise fewer bad artifacts in a dynamics effect. You know better the math and such, but think I understand that sudden changes in the control are more likely to make IM distortions. So an attack or release that starts gradual, gets steeper, then ends gradual (sigmoid-like) seems less likely to make artifacts. But dunno if the same advantage would confer to a bass guitar envelope follower or whatever.

I've stayed away from trying resonant filters for envelopes, not expecting any benefit from overshoot or ringing. But maybe sometimes overshoot/ringing would be useful somehow, dunno. Its just why I've not tried anything except stacked one-poles for envelopes.

Dunno if I implemented it efficiently, but used linear crossfades a great deal in sequencer coding.

I got what seemed like "good luck" drawing straight lines on a hobby limiter. Keep meaning to "clean it up" tidy enough to open-source publish. It works fine but the code is just a little too untidy. That particular application, there is a substantial lookahead buffering, about 50 ms and maybe a little more wouldn't hurt. The "trick" that I think makes it easier to think about, is that it makes no effort to smooth ANYTHING when the signal is below the limiter brickwall threshold.

So the buffer remembering the lookahead envelope, as long as the signal is below threshold the envelope buffer gets filled with values of 1.0. And the gain calc has a gain of 1.0 when the envelope buffer has a value of 1.0. For instance if thresh is 0.707 and the input signal comes in at 1.0, it would write 1 / 0.707 = 1.414 into the envelope buffer, and then draw straight lines down to 1.0, into the past and future from that location.

The "straight line drawing" into the past an future only writes new values if the new value is greater than the value currently in the envelope buffer.

Actually its a little more complicated because it looks for sample runs of over-threshold signal. For instance if we get a consecutive 65 samples over-thresh, it counts the length of the over and remembers the max sample in that run of overs. Then it draws a flat line at the max over value over the duration of the run of overs, and draws straight lines down to 1.0 into the past and future from the beginning and end of that flat-line over segment. It has some rules about how far to draw the straight lines depending on peak level and peak run length. Then the geometric straight-line envelope is further "rounded off" attempting to avoid artifacts caused by sudden angle-changes in this envelope made out of overlapped straight lines. Also a final adjustable-release IIR stage that modulates the release time according to how big a percentage of time is spent over-threshold. Very fast release time for rare small brief overs, and longer release time as the limiter goes into heavy limiting.

So it doesn't eat much CPU at all so long as input < thresh, ignoring the signal completely unless it rises above thresh. Probably a cpu pig if hitting lots of overs, with all the overlapping line-drawing into the past and future. And it needs significant lookahead to work at all, at least in this implementation.

Apologies so long-winded talking about nothing. Just that it was easier for me to figure how to get some use out of "drawing straight envelope lines" if it was only done for over-threshold inputs.

In the past I'd thought how to use "drawing straight envelope lines" smoothing all the way down to -inf, or all the way down to -96 dB or whatever, and it didn't seem as clear how to get good use out of it. Plus, at least the way I was doing it in the limiter, it would really burn cpu cycles if CONSTANTLY drawing overlapping straight lines into the past and future, regardless of input level.

Had thought of trying something of the same approach for a lookahead peak compressor some time or the other if I ever get the motivation.

Post Reply

Return to “DSP and Plugin Development”