An extension to the 7-segment GEN envelope example providing retrigger and consistent exponential shaping of each segment slope across different duration values. The retrigger occurs when the envelope is already on and it receives a new gate-on signal. A brief release tapers the prior envelope to 0 over 5 milliseconds, to prevent clicking, before starting the new envelope afresh.
In this design,gen~ never misses a new note. The envelopes always trigger on every new note, even if there are no note-off messages at all. This can greatly simplify the building of a quick-and-simple melody generator.
The retrigger is achieved by adding an additional release phase before the first attack phase, which only is included in the stage sequence when the output of the envelope is on. If note-on messages arrive when the envelope is off, the logic skips the first release phase and starts directly with the normal opening attack phase.
While the provided patch is an extension to the 7-segement example included with Max, here also is what could be an optimal design in GEN for an ADSR envelope with the same core functionality.
In both designs, the experimentation factor is 2.5, set as a float constant on the right of the lower output network, and may easily be tapped through to MAX to make it an adjustable parameter.
Besides lower CPU, the main benefit of my second design here is that the sustain level can change while the envelope is playing, unlike the example provided with MAx/MSP. Note that changing the sustain level also changes the slope of the decay and release phases, and also, rapid changes of the sustain level while the envelope is in the sustain stage causes clicks. Smoothing is therefore required on the sustain level input.
The proposed second design also uses the same internal logic for both the standard envelope release stage, and the brief release stage for retrigger. Hence, besides a ternary object for condition test, the retrigger addition is zero cost in CPU cycles.
Inside the design, there is one simple main loop, which increments both the output step and the stage. In previous envelope designs I had always used separate loops, and I am grateful to Graham Wakefield, Peter McCullogh, and the example author for teaching me a new way. In gen~, all the accumulator objects can only reset to one value (zero by default) unless futzing with attribute value changes in a codebox; but for envelope stage change on gate events, the counter must be set to a non-zero value three different ways: for note-on events, to stage 2; for retrigger events, to stage 1; and for note-off events, to stage 5; so the loop uses a simple adder instead of an accumulator, and ternary operators separately set the new envelope stage upon gate changes in the loop, after the adder increments the appropriate amount from a selector, summing it with the previous value from a history object.
In my method, though, I do not create the output values on the main loop. Instead the loop simply steps incrementally between 0 and 1 for each stage (which in this design also numerates the stage value). So its output simply passes into a 0->1 wrap, and the output calculations are for a linear number sequence between 0 and 1 for every single stage. The wrap output is simply scaled for rising envelope stages, or subtracted from 1 and then scaled for falling envelope stages. Because step calculations for each stage are for a range between 0 and 1, it is easy to shape the exponential curves. I believe this method also reduces the number of selectors, scaling operations, and other mathematics to the absolute minimum required.
To scale the steps in release stages, the current output is sampled and applied to the release as a scaling factor. This requires one float-to-integer conversion from the main loop. However as all gen~ objects operate on floating point, I did not put the integer value from this conversion into the final selector. This was counter-intuitive, as it seems the selector would operate better on integer values. My reasoning was, the selector operates internally on floating-point, so selecting with an integer value in, rather than the original floating-point value, would add a type conversion to the computation.
One remaining optimization would be to use squaring for the exponential factor. Most modern CPUs provide an optimized floating-point machine instruction for squaring that is more efficient than POW, so it is no surprise gen~ contains a specialized object for squaring. However my own taste is for an exponentiation of 2.5, so it the above design includes some overhead for that factor which could be eliminated if squaring is satisfactory.
The quality of one implementation over the other, in terms of accuracy and flexibility, however, may be subjective, so I hesitate to promote my own design over that provided by the vendor as an example. Please contact me if you would like the.maxpat file for it, and I will add it to the archive.
While at first the envelope function seems the most important part of the design, effective trigger design is in fact crucial and complex. This is because, when gate signals arrive from the MAX scheduler in the same sample clock period, gen~ can only process one of them, and discards the others. The exact behavior is not documented and so may change, but currently gen~ appears, from empirical testing, only to accept the last message received from Max in the same sample period.
In many cases this is not a problem for a gen~ design. For envelopes however, if a note-off message is received in the same sample-clock period as a note-on message (as may frequently occur in sequencer designs), the note-off is lost.
Also, if two note-on messages arrive in consecutive sample clock cycles, there is no way for gen~ to receive an intervening note-off message in between them.
Moreover, when two note-on messages arrive in sequence without an intervening note-off message (which either may have been created by the design, or which may have been issued by the MAX scheduler earlier in the same sample-clock cycle as the second note-on message), the gen~ DELTA and CHANGE objects can only detect a velocity difference between the two notes. If the velocities are the same, gen~ has no way of knowing a new note has arrived. If the design does not take into account this signal transition, from the scheduler-driven domain of MAX to the synchronous world of gen~, the trigger would be lost entirely.
Thankfully, now, both these envelope designs themselves retrigger anyway. The method is to add a small fraction to the MIDI note velocity at the MAX level. Then there is a continual fractional difference between each received gate-on message, even if they have the same velocity. And gen~ therefore can detect every single new note-on message and retrigger the envelope properly.
To add the fraction in this design, a MAX subpatch contains a counter which is incremented on each note-on event and loops. Its output is divided down into a fractional value and added to the integer note-on value. The counter does not loop through zero, so the generated fractional component for note-on values are never zero. Consequently in gen~, if the (modulo 1) of the incoming velocity is zero, gen~ knows that the incoming signal is a note-off message. Otherwise, if there is a new fractional value, it must be a new note-on event, and gen~ can react accordingly.
Then internally, the gen~ design performs an integer conversion on the received value to remove the fraction and obtain the original velocity.
And now to dive into gen~'s triggers themselves. Triggers are edge driven, rather than level driven, which requires a slightly different way of thinking than to which one may be accustomed. And as well as the envelope triggers, it is also worthwhile to consider that gate sync signals to other components may also need to retrigger upon new note-on events. For example, oscillators sound very different when at different phases to each other, and one of the best ways to pull them all together is to sync them on each new note start.
However, if not prevented, a flood of note-on messages can continually retrigger oscillator sync, causing buzzing and worse distortions.
Therefore, the gate signal logic provides the note-off trigger to support delaying of consecutive note-on triggers, always generating a note-off pulse first.
While not configured this way by default, this facility does permit other sync'ed components only to react upon the last received note-on message, even if the note-on messages occur continuously across multiple sample periods. However, most implementations prefer to trigger upon the first note-on event. Therefore, by default, the first note-on event received out of many across consecutive sample cycles provides a note-on trigger after a one-sample cycle delay, and the note-on trigger is always preceded by a single-cycle note-off trigger (to ensure that signal transitions occur across zero, as required by all sync inputs). Multiple note-on triggers across consecutive sample periods are discarded to prevent the buzzing and distortion.
Lastly, a quick word on pitch and attack modulation. The envelopes for many real-world instruments have a faster attack and decay when played more loudly. Also, the higher the pitch, the shorter the envelope is for the sound. The envelope modulation accepts velocity and pitch inputs to adjust the duration of the attack and decay phases (for velocity) and also the release phase (for pitch).
This kind of parameter tuning is particularly effective for polyphonic sounds, as the envelope can also adjust tonality movement (as well as depth) depending on pitch and velocity.
A demonstration patch is available in the Synthcore2 bundle.