Tutorials

A gen~ Tale: Follow(er) The Leader

The story goes like this....

I’m working away on my Max patch and I realize that I could really use an envelope follower — but not just any old envelope follower. I’m working with some very quick rhythmic elements (sometimes at audio rate), so my envelope follower is going to need to be as sample-accurate as possible.

Since the gen~ object runs on a per-sample basis internally, it’s perfect for this type of accuracy. I head over to check out the gen~ examples folder to see if there’s one there already.

Nope. There's no example!

But here’s one of my favorite things about Max, almost all the examples and patches can serve another purpose with the addition (or removal) of a few patch cords or objects. What you're about to see is an example of just that.

What I do find is this example of an implementation of the Max slide~ object in gen~. You can find it here:

Help > Examples > Gen > gen~.slide

When I open the gen~ object up and look at its internals, I quickly realize that I can switch it to a follower with some nifty additions.

This slide~ example patch works by monitoring the output. You can see the output running back up and into the history operator, which allows you to have a feedback loop (it adds one sample of delay).

That output value then runs into the right inlets of the > (greater than) operator and the minus - (minus) operator.

The > operator is receiving the input value (It’s either one or zero in the beginning) and — depending on whether this is greater than or less than the value into the > operator's right inlet — it sends a one or zero to the ? operator.

The ? operator selects between the second or the third inlet depending on whether the first argument is true (1) or false (0). The value sent to the second inlet of the ? operator will be passed to the outlet of the ? operator if the value at the first inlet is true and the value sent to the third inlet of the ? operator will be passed to the outlet if the value into the ? operator is false.

The key object in this Gen patcher that gives us our envelope is the - (minus) operator. Each time our input value switches between one or zero, the - operator is subtracting the current output value (minus 1 sample of delay because of history) from either one or zero. This value is then multiplied by the slide up and slide down values, which in turn are run into our ? operator (which is functioning as a Boolean switch here), thus giving us our slide (or its soon-to-be envelope).

So how do we turn this into an envelope follower?

The key ingredient in converting this patch to an envelope is to turn the toggle switch, converting the one or zero switches into a trigger inside the gen~ patcher that will accept a real-time audio signal and generate a one or zero, depending on the signal value passing the threshold amount we've set.

To do that, we need to declare the threshold as a parameter value using the param operator with the following attributes set: param threshold @min 0 @max 1 @default .5.

This does two things:

  1. It sets the initial state of our > operator (.5)

  2. It also allows us to have our attribute accessible via an attrui object outside of the gen~ patch, giving us the ability to change the threshold in real-time in the parent Max patcher.

Now let’s add a > (greater than) operator, and used our named parameter threshold. Here's what our patching looks like:

We want to add one more object just above the > threshold operator to give us greater accuracy when detecting the threshold.

Let’s add an abs (absolute value) operator. This will give us the absolute value of our incoming signal, translating any negative values to positive ones.

Now that we've modified the slide up/slide down portion of the patch, all we need to do to trigger our envelope is to send in an audio signal to the first inlet of our gen~ patch and set the threshold to our desired amount.

Set the envelope up to 0 and envelope down (this is the amount in samples) to something lower. If we play in a drum loop, we can see the envelope being generated each time the signal goes above our threshold.

We have a follower!

Ok, now let’s do some fun stuff with our follower — we’re almost done here.

Let’s pipe the audio through to an additional * operator to allow us to use the envelope as a transient shaper of our incoming signal.

Run the audio from the outlet of the in 1 operator directly to the left inlet of the * operator. We’re adding our envelope follower output to the right inlet of the * operator to shape the audio.

But before we do that, we want to add a little more control to our envelope follower.

Something that a lot of envelope followers allow you to do is to invert the follower. This is useful as a control signal and for shaping in certain ways, such as ducking other sounds for the kick drum etc.

We’re going to do this with two operators, and also add one more param operator so we have a simple switch accessible from outside of the gen~ object that allows us to invert our envelope.

Beneath the + operator, let's add a !- 1 (subtraction with the inlets reversed) operator.

We need to expose the flip param to the Max patcher outside of our gen~ object by adding another param operator: param flip @min 0 @max 1 @default 0

Once we've set up the parameter, we'll add a named version of the mix operator (mix flip), and connect a patch cord from the outlet of the + operator to the input of the !- operator and to the left input of the mix flip operator.

Connect a patch cord from the outlet of the !- 1 operator to the right inlet of the mix flip operator, and a patch cord from the outlet of the mix flip operator to the right inlet of the * (multiply) operator.

We’re in business!

Now we can go back to Max and run some audio into our gen~ envelope follower, dial in the threshold and also invert the follower to our liking.


Here's our final gen~ patch:

Final gen~ envelope follower

And the Max patch:


Check out this video of the patch in action and download the example below.

Happy patching!

by Tom Hall on 2020年3月4日 16:20

Graham Wakefield's icon

Graham Wakefield

3月 06 2020 | 9:07 午後

nice!

do.while's icon

do.while

3月 07 2020 | 5:46 午前

this is real homework ! trully enjoyed the process Tom, a good part of such 'revenging' is to keep concentrate own documentation about all the processes we find puprosively keyful. draw data, reference formulas etc. this might become your pierced external memory device. Such story like yours will just add up to docs even with gifs as reading mostly sucks once you write too much :D anyhow, life is a document. and this post should land on MEDIUM

do.while's icon

do.while

3月 07 2020 | 5:47 午前

much love tom !

do.while's icon

do.while

3月 07 2020 | 5:53 午前

@JF it takes time to get into detailed post (#edit might even take years), u dont need to wonder why not everyone jumped in yet.
it just doenst look good as the first post, speak for yourself like with your "...but i will thank you..." my gosh

simon ho's icon

simon ho

3月 18 2021 | 7:15 午後

Thanks tom :)

daddymax's icon

daddymax

3月 21 2021 | 7:50 午後

Good post and nice patch - it seems to be a brilliantly tight follower.

Ernest's icon

Ernest

3月 21 2021 | 8:04 午後

That's a really nice design. Unfortunately, I had a stroke and I am almost entirely deaf, so I can't compare it to the envelope follower in Husserl, which shares one stepwise envelope follower in the monophonic gen~ object which can operate either as compressor, limiter, or simply gain control. It is in codebox so that unnecessary processing can be turned off depending on the mode:

// ramps to new val, incr = 1/#samples
ramp0(in1, linc){
 History prev;
 prev = prev + linc * (in1 -prev);        
 return prev;
} 
// combined ctrl +o/p 
gaincompander3(in1, type, amp, thresh, ratio, att, rel, lclk){ 
// in1         = Signal to limit or compress.
// type        = whether to limit or compress
// amp         = limiter output amp
// thresh     = amplitude to start compression
// ratio    = compression ratio after level exceeds amplitude
// att, rel     = attack and release in samples
// lclk      = 0 causes resampling of level.
// Return:    Limited signal. 
 History comp0(0), lim0(0), comp1(1), lim1(1), gain(1), gain2(1), 
   rate, rate2;
 x = abs(in1);
 if(lclk> 0){
  if (type==3){         //compressor
     gain = (comp1 > thresh)? amp * (thresh +(comp1 -thresh)/ratio) : amp;
     rate = (comp1 > comp0)?  att : rel;
  } else if (type == 1){    // limiter    
     gain  = (lim1 > 0)?      amp /lim1 : 0;
     rate =  (lim1 > lim0)?   att : rel;
  } else {              // off
     rate = (gain2 > gain)?   att : rel;
     gain = amp;
  }
  comp0    = comp1;
  lim0    = lim1;
  comp1 = 1;
  lim1    = 1;
 } else {
  if (x > thresh && x >comp1) comp1 = x;
  if (x > lim1)               lim1  = x;
 }
 if (change(in1 >=0, init =-9999999) !=0){
  gain2 = gain;
  rate2 = rate;
 }
 return in1 * ramp0(gain2, rate2);
}
----------------------- MAIN -------------------------------
Param limon, limlvl, limthresh, limratio, limatt, limrel;
History init, lclk, lclk0, ltime, linc;
if(init==0){
    ltime         = 10/samplerate;
    linc          = 10/samplerate;
    init        = 1;
}
lclk0        = (lclk0>1)? 0 : lclk0 + ltime;
lclk         = change(lclk0 >.5);
out1 = compander3(in1, limon, limlvl, limthresh, 
                  limratio, limatt, limrel, lclk); 

Maybe you could tell me what you think? The implementation is available for download at https://www.yofiel.com/max.php

Benjamin Whateley's icon

Benjamin Whateley

7月 11 2023 | 9:53 午前

This is great thank you!

I found the peaks a little rough when I use it to shape a low-freq sound like a kick, I played around with some smoothing, but found that placing a high-pass filter before the [abs] object prevents the follower from following the low frequencies present in the input and causing the rough sound, attributable to uni-polar amplitude modulation.

Thanks (: