Adjustable envelope code for the 4.x

Status
Not open for further replies.

john-mike

Well-known member
I finally fixed up my adjustable envelope object I've been messing around with forever. You can get it here.

envelope-examples.jpg

Shape is adjustabe from a sharp exponential curve, 1.0 is a very chonky log for each ADSR stage.
Lengths of the envelope stages and amplitude of sustain is unaffected by shape change.

It can also be triggered works like a standard slope generator. Triggering it will produce the attack and decay stages and then end.

This works well on the teensy 4.x and only takes about 2% processor on the 4.0 but on the 3.2 it’s over 100% due to all the powf use. Each update of envelope has two calls to fscale and then interpolates between them for the other values in the 8 output batch.
If there's demand I'll make a LUT version of it for the 3.x

Let me know what bugs you find!
 
Hello I'm testing it on TSynth using a T4.1. Global variable usage on compile has gone from 279828 bytes (53%) to 312596 bytes (59%) just by including the two files. Using it on both the 12 amp envelopes and 12 filter envelopes, CPU is higher and can hit over 100 when playing a lot of notes rapidly, plus I'm using MarkT's band-limited waveforms too. AudioMemory is still fine at 96. Using the library on just the amp envelopes improves the CPU load and this is how I would use it.

The smoother release is much better. Actually I don't care about attack and decay as these aren't audibly jarring when linear and I want attacks to be fast when set to minimum time. If memory and CPU load improvements can be made by only using it on the release stage, I would choose that, as no one will notice the attack or decay being linear.

I'm getting a frequent click when releasing a note exactly when it's released, which is what would prevent me from implementing it at the moment. You can hear it in this audio file: MoogBass Otherwise, this could be a keeper. Thanks.
 
What is the processor usage of the TSynth usually?

I can't replicate the click. Is it happening in other modes? Is it the resonance of the filter clipping?
 
It usually varies between 35 and 65%. I don't know what you mean by modes. I'll have a further play. It is intermittent as the sample shows.

WCalvert says in another post It does not have the feature to prevent clicks if noteOff() is triggered before reaching sustain.
Could this be it?
 
It would be great to add this to the AudioSynthWaveformDc object too, so that better portamento glides can be made.
 
I think the DC object should be for DC really - perhaps there's a need for a single-shot capable sweep generator class
for portamento and suchlike - in fact such a class could be a superclass for an envelope class.
 
WCalvert says in another post It does not have the feature to prevent clicks if noteOff() is triggered before reaching sustain.
Could this be it?

This adjustable envelop can have note off occur at any time and fade out from there. I tested it a bit to make sure there wasn't an odd state where this would happen but of course I could still be missing something.


It would be great to add this to the AudioSynthWaveformDc object too, so that better portamento glides can be made.

This object already does that. It has a second output that is just the level of the envelope. Only issue is that it needs something coming into the input for the CV out to work. This is something I need to fix.
 
I'm using AudioSynthWaveformDc for portamento but an exponential response instead of a linear one, that mimics capacitor charge/discharge would be preferable.
 
WCalvert says in another post It does not have the feature to prevent clicks if noteOff() is triggered before reaching sustain.
Could this be it?

That was for my envelope code, which has nothing to do with john-mike's code.
 
Yes, but it could be a similar problem. Just a thought.
 
Last edited:
I still haven't been able to replicate the issue.
Is anyone having it?
Is it happening with other presets, UHF?
 
LUT version is a good suggestion. This is done in the 8Bit Shruthi Synthesizer from Mutable instruments.


Code:
// Copyright 2009 Olivier Gillet.
//
// Author: Olivier Gillet (ol.gillet@gmail.com)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
// -----------------------------------------------------------------------------
//
// Envelopes.


#ifndef SHRUTHI_ENVELOPE_H_
#define SHRUTHI_ENVELOPE_H_


#include "avrlib/base.h"
#include "shruthi/patch.h"
#include "shruthi/resources.h"
#include "shruthi/shruthi.h"
#include "avrlib/op.h"


using namespace avrlib;


namespace shruthi {


enum EnvelopeStage {
  ATTACK = 0,
  DECAY = 1,
  SUSTAIN = 2,
  RELEASE = 3,
  DEAD = 4,
  NUM_SEGMENTS,
};




class Envelope {
 public:
  Envelope() { }


  void Init() {
    stage_target_[ATTACK] = 255;
    stage_target_[RELEASE] = 0;
    stage_target_[DEAD] = 0;
    stage_phase_increment_[SUSTAIN] = 0;
    stage_phase_increment_[DEAD] = 0;
  }


  uint8_t stage() { return stage_; }
  uint16_t value() { return value_; }


  void Trigger(uint8_t stage) {
    if (stage == DEAD) {
      value_ = 0;
    }
    a_ = value_ >> 8;
    b_ = stage_target_[stage];
    stage_ = stage;
    phase_ = 0;
    phase_increment_ = stage_phase_increment_[stage];
  }


  inline void UpdateAttack(uint8_t attack) {
    stage_phase_increment_[ATTACK] = ResourcesManager::Lookup<
        uint16_t, uint8_t>(lut_res_env_portamento_increments, attack);
    phase_increment_ = stage_phase_increment_[stage_];
  }


  inline void Update(
      uint8_t attack,
      uint8_t decay,
      uint8_t sustain,
      uint8_t release) {
    stage_phase_increment_[ATTACK] = ResourcesManager::Lookup<
        uint16_t, uint8_t>(lut_res_env_portamento_increments, attack);
    stage_phase_increment_[DECAY] = ResourcesManager::Lookup<
        uint16_t, uint8_t>(lut_res_env_portamento_increments, decay);
    stage_phase_increment_[RELEASE] = ResourcesManager::Lookup<
        uint16_t, uint8_t>(lut_res_env_portamento_increments, release);
    stage_target_[DECAY] = sustain << 1;
    stage_target_[SUSTAIN] = stage_target_[DECAY];
  }


  uint8_t Render() {
    phase_ += phase_increment_;
    if (phase_ < phase_increment_) {
      value_ = U8MixU16(a_, b_, 255);
      Trigger(++stage_);
    }
    if (phase_increment_) {
      uint8_t step = InterpolateSample(wav_res_env_expo, phase_);
      value_ = U8MixU16(a_, b_, step);
    }
    return stage_ == SUSTAIN ? stage_target_[DECAY] : value_ >> 8;
  }


 private:
  // Phase increments for each stage.
  uint16_t stage_phase_increment_[NUM_SEGMENTS];
  // Value that needs to be reached at the end of each stage.
  uint8_t stage_target_[NUM_SEGMENTS];
  // Current stage.
  uint8_t stage_;


  // Start and end value of the current segment.
  uint8_t a_;
  uint8_t b_;


  // Phase and phase increment.
  uint16_t phase_increment_;
  uint16_t phase_;


  // Current value of the envelope.
  uint16_t value_;


  DISALLOW_COPY_AND_ASSIGN(Envelope);
};


}  // namespace shruthi


#endif // SHRUTHI_ENVELOPE_H_




const prog_uint8_t wav_res_env_expo[] PROGMEM = {
       0,      4,      9,     14,     19,     23,     28,     32,
      37,     41,     45,     49,     53,     57,     61,     65,
      68,     72,     76,     79,     83,     86,     89,     92,
      96,     99,    102,    105,    108,    111,    113,    116,
     119,    121,    124,    127,    129,    132,    134,    136,
     139,    141,    143,    145,    148,    150,    152,    154,
     156,    158,    160,    161,    163,    165,    167,    169,
     170,    172,    174,    175,    177,    178,    180,    181,
     183,    184,    186,    187,    188,    190,    191,    192,
     193,    195,    196,    197,    198,    199,    200,    201,
     202,    203,    205,    206,    206,    207,    208,    209,
     210,    211,    212,    213,    214,    215,    215,    216,
     217,    218,    218,    219,    220,    221,    221,    222,
     223,    223,    224,    225,    225,    226,    226,    227,
     227,    228,    229,    229,    230,    230,    231,    231,
     232,    232,    233,    233,    233,    234,    234,    235,
     235,    236,    236,    236,    237,    237,    238,    238,
     238,    239,    239,    239,    240,    240,    240,    241,
     241,    241,    241,    242,    242,    242,    243,    243,
     243,    243,    244,    244,    244,    244,    245,    245,
     245,    245,    245,    246,    246,    246,    246,    246,
     247,    247,    247,    247,    247,    248,    248,    248,
     248,    248,    248,    248,    249,    249,    249,    249,
     249,    249,    249,    250,    250,    250,    250,    250,
     250,    250,    250,    251,    251,    251,    251,    251,
     251,    251,    251,    251,    251,    252,    252,    252,
     252,    252,    252,    252,    252,    252,    252,    252,
     252,    253,    253,    253,    253,    253,    253,    253,
     253,    253,    253,    253,    253,    253,    253,    253,
     253,    254,    254,    254,    254,    254,    254,    254,
     254,    254,    254,    254,    254,    254,    254,    254,
     254,    254,    254,    254,    254,    254,    254,    255,
     255,
};
 
Last edited:
Hallo John..

Thank you for your development work. I will test it in my synthesizer. :)

Greetings from germany. Rolf
 
Be interesting to know how the CPU load and memory footprint compare to my ExpEnvelope variant, to be found at https://github.com/h4yn0nnym0u5e/Audio/tree/features/expEnvelope, and discussed in this thread: https://forum.pjrc.com/threads/66898-Exponential-envelope-generator-object. Mine doesn't have LUTs, so on-the-fly calculates an (acceptable, I believe) integer approximation to the exponential RC circuit found on most analogue synths: this seems to double the load you get from the baseline linear envelope object. But it can't do a logarithmic attack :( How does yours cope with a change of parameter during a particular state, e.g. lowering sustain when decay has already started?

Cheers

Jonathan
 
Yes, it does the job. No noises. CPU usage on a T4.1 is up a bit.

Testing with patch 1 Solina with current linear envelope and this adjustable one:

Code:
             resting   1 note   4 notes    unison
Linear       41         42          45          49
Expo        44         45          47          56

This is just with the amp envelopes. The filters are still linear.

I've noticed when attack shape is positive and attack time is long (seconds) the initial start, jumps. Negative values are fine.
Screenshot 2021-04-28 184808.jpg


Also can you put
Code:
 uint16_t lut[9][256]  = {...}
into flash memory?

And
Code:
      float flev =  (powf(level, curve));
is no longer used.
 
Last edited:
e.g. lowering sustain when decay has already started?
Yeah it seems to handle that kind of thing well as it's always calculating the offset and attenuation of the decay and release.

I was using fscale before and interpolating between values but it was far too much work on the 3.x and even on the 4.x I needed to speed it up for an upcoming project that needs lots of voices.


UHF:
Yeah the log LUT has a very quick jump at the beginning that can cause a click. You can see it easily on this spreadsheet I used to generate them.
I haven't seen it make a separate little artifact, which is what it look like there. Realistically log is not super useful over .3 I think but what you have there doesn't look that crazy. What setting sis you have it at?

Yes it should be a constant! But that is harder than it seems on the 4.x. PROGMEM isn't for T4x and FLASHMEM and static const won't compile for me in the library. They work fine in the .ino but have no effect on memory usage....

And I removed that other line.
Thanks for catching these.
 
Last edited:
Yeah it seems to handle that kind of thing well as it's always calculating the offset and attenuation of the decay and release.
Just out of interest, I took a look...
  • ADR phase [timing] changes don't seem to take effect in real time, but wait until the next envelope*
  • A sustain level change during decay results in a glitch
  • Note-on (with no intervening note-off) during hold re-starts attack immediately from zero (my preference for this is to do nothing)
I didn't investigate curve changes during the relevant phases... CPU load seems to average about 7600 cycles, compared to the original linear envelope's 830 - that's based on my test harness which spends most of its time in the CPU-intensive ADR phases.

*this looks benign for short envelopes, but I'd imagine it would be a pain if you set a long attack time and then turned the knob down, for example...

Cheers

Jonathan
 
Right you can't change the lengths of ADR yet. I'm still trying to find the best way to do that.

I was thinking the interpolation for them would be tricky but I just did it to fix the decay glitch and it seems to work.
The new version I just put up has an interpolated sustain level so changing it at any time shouldn't make any artifacts.

I hadn't checked note on without a note off. It should probably gone it your right.

Thanks for checking it out so thoroughly, h4yn0nnym0u5e!


I realized the only way to keep the LUT in flash was to put it outside the library and pass it along to it.
I haven't been able to find a way to pass multidirectional arrays so it's now just a 2d one but I changed the function to make it work. See the setup on how to use it.

https://github.com/BleepLabs/adjustable_envelope_example
 
Right you can't change the lengths of ADR yet. I'm still trying to find the best way to do that.

The new version I just put up has an interpolated sustain level so changing it at any time shouldn't make any artifacts.

I hadn't checked note on without a note off. It should probably gone it your right.

Thanks for checking it out so thoroughly, h4yn0nnym0u5e!
Thanks for being so understanding - having posted I felt I might have come across as a bit over-critical! I think it's great that there's so many options for people to choose from, depending on their needs (efficiency vs. versatility etc.)...

Changing the ADR lengths essentially makes a nonsense of the "count" value that gets pre-calculated - I dealt with it by keeping it as a safety net, but actually terminating the state at a target level: 1.0 for attack, <sustain> for decay, and 0.0 for release. Don't know if that would work for your code.

At some point I should probably post my stress-testing harness, it helped a lot with my take on updated code: essentially I never start the audio engine, but call the update functions directly and use the AudioPlayQueue and AudioRecordQueue to insert known values and read the results out in a non-time-critical manner. I did two versions, one which does note on/off at different envelope states, and one which changes envelope parameters. Having trawled a few threads there seems to be a common issue of "X works great but creates glitches when I do Y" (sometimes without much clue as to what Y is...).

Cheers

Jonathan
 
No worries! I didn't think you were but, yeah, it's hard to sound direct without sounding critical on forum posts hah.

essentially I never start the audio engine, but call the update functions
Oh that's an interesting way of doing it.
 
Would it be possible to get just the envelope value and send it to the DAC?
I was looking at the .cpp/.h but i have no idea where to start messing with it
 
Would it be possible to get just the envelope value and send it to the DAC?
Yes I'll try and add that in soon.
In the mean time you can use an envelope with an AudioSynthWaveformDc object as an input with it's level turned all the way up
 
Status
Not open for further replies.
Back
Top