Teensy exponential envelopes

Rolfdegen

Well-known member
Hello. I use modified (exponential) envelopes from the Teensy audio lib by P.Steffregen in my synthesizer project. When I use effect_envelope.delay the envelope freezes after a while. Paul's original effect_envelope.delay function works without problems.
Thanks for help :)

modified envelope
C++:
/* Audio Library for Teensy 3.X
 * Portions Copyright (c) 2017, Paul Stoffregen, paul@pjrc.com
 * Portions Copyright (c) 2021 Vince R. Pearson
 *
 * Development of this audio library was funded by PJRC.COM, LLC by sales of
 * Teensy and Audio Adaptor boards.  Please support PJRC's efforts to develop
 * open source software by purchasing Teensy or other PJRC products.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice, development funding notice, and this permission
 * notice shall be included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#ifndef effect_envelope_h_
#define effect_envelope_h_
#include "Arduino.h"
#include "AudioStream.h"
#include "utility/dspinst.h"

#define SAMPLES_PER_MSEC (AUDIO_SAMPLE_RATE_EXACT / 1000.0)
#define EXP_ENV_ONE ((int32_t)0x40000000)
// Envelope type. EXP_Nx are attacks with different amounts of negative curvature. EXP_Px have positive curvature.
// EXP_0 has a linear attack.
#define NUM_ENV_TYPES 18 // Linear, Exp -8 through Exp +8

// Fast Sustain Time Constant for Exp Envelope
// Normally changes in sustain level while in sustain are subject to the decay delay setting.
// Here we sense a change in sustain level and kick the state machine into a state with a faster time constant.
// This filter time constant is precalculated during compile.
// Time constant = exp(-Ts/To).
#define FAST_SUSTAIN_KF 0.999773268 // 100 ms
// #define FAST_SUSTAIN_KF 0.999886627 // 200 ms
// #define FAST_SUSTAIN_KF 0.999943312 // 400 ms
// #define FAST_SUSTAIN_KF 0.999971655 // 800 ms
#define FAST_SUSTAIN_K1 (uint32_t)(FAST_SUSTAIN_KF * EXP_ENV_ONE)

// The exponential difference equations are based on the impulse invariant method.
// The equation used for attack is k1 = exp(curveFactor*Ts/To) and k2=(k1-1)/((e^curveFactor)-1)
// This allows different eponential curvatures (positive and negative) with the endpoints constrained
// to start at 0 and end at 1 (normalized) in the specified time interval.
// and ynew = k1*yold+k2 (normalized).
// The funtion for the k2 term is a continous funtion of curveFactor through zero, but it is an indeterminate form at zero that
// This causes problems with finite precision math when approaching zero from either side (like the sinc funtion sin(x)/x but worse).
// It can be expressed as a continous function using a ratio of power series but it is simpler to restrict the values to integers
// between -8 and 8 treating zero as a special case (linear ramp).
//
// The other stages use y(n+1)=k*(y(n)-target)+target where k=exp(-Ts/To)(normalized).

class AudioEffectEnvelopeTS : public AudioStream
{
public:
  AudioEffectEnvelopeTS() : AudioStream(1, inputQueueArray)
  {
    state = 0;
    env_type = -128; // Added for addition of exponential envelope. Default is linear. -8 through 8 are different amounts of positive and negative curvature.
    delay(6.0f);     // default values...
    attack(10.5f);
    hold(2.5f);
    decay(35.0f);
    sustain(0.5f);
    release(300.0f);
    releaseNoteOn(0.0f);
  }
  void noteOn();
  void noteOff();
  FLASHMEM void delay(float milliseconds)
  {
    delay_count = milliseconds2count(milliseconds); // Number of samples is 8 times this number for linear mode.
    __disable_irq();
    if (state == STATE_IDLE_NEXT)
      state = STATE_IDLE; // Force counter reload if in STATE_IDLE_NEXT.
    __enable_irq();
  }

  // Attack curve negative numbers generate negative curvature (like typical exp attack curves).
  // Positive numbers generate positive curvature (accelerating upward).
  // Zero produces linear attack curve.
  // This is used to change the time constant and target value to enforce zero to full scale in
  // the requested attack time.
  // The equation used for the input, (1-e^(curve_factor*Ts/To))(e^curve_factor-1) is singular at curve_factor=0.
  // The precision required starts to blow up as abs(curve_factor) approaches zero at which there is a singularity.
  // The function is however continuous through zero and can be expressed as a ratio of power series but it is easier to restrict the
  // curve factors to be integers and to treat zero as a special case which is a linear ramp.
  // The numbers are additionally restricted to -8 to +8 as numbers outside this range asymptotically approach a step function.

  FLASHMEM void setEnvType(int8_t type)
  {
    env_type = type;
    updateExpAttack(); // Exponential attack parameters need updating.
  }
  FLASHMEM int8_t getEnvType() { return env_type; };

  FLASHMEM void attack(float milliseconds)
  {
    attack_count = milliseconds2count(milliseconds);
    if (attack_count == 0)
      attack_count = 1; //************* added check for div/0
    updateExpAttack();
  }

  FLASHMEM void hold(float milliseconds)
  {
    hold_count = milliseconds2count(milliseconds);
  }
  FLASHMEM void decay(float milliseconds)
  {
    decay_count = milliseconds2count(milliseconds);
    if (decay_count == 0)
      decay_count = 1;
    updateExpDecay();
  }
  FLASHMEM void sustain(float level)
  {
    if (level < 0.0)
      level = 0;
    else if (level > 1.0)
      level = 1.0;
    // Exponential generator uses same sustain variable.
    sustain_mult = level * 1073741824.0;
    __disable_irq();
    if (state == STATE_DECAY || state == STATE_SUSTAIN)
      state = STATE_SUSTAIN_FAST_CHANGE;
    __enable_irq();
  }
  FLASHMEM void release(float milliseconds)
  {
    release_count = milliseconds2count(milliseconds);
    if (release_count == 0)
      release_count = 1;
    updateExpRelease();
  }
  FLASHMEM void releaseNoteOn(float milliseconds)
  {
    release_forced_count = milliseconds2count(milliseconds);
    updateExpReleaseNoteOn();
  }
  // ElectroTechnique 2020 - close the envelope to silence it
  FLASHMEM void close()
  {
    __disable_irq();
    mult_hires = 0; // Zero gain
    inc_hires = 0;  // Same as STATE_DELAY
    state=STATE_IDLE; // Added, VRP. Shouldn't this be in the idle state when closed?
    ysum = 0;
    __enable_irq();
  }
  bool isActive();
  bool isSustain();
  using AudioStream::release;
  virtual void update(void);

private:
  uint16_t milliseconds2count(float milliseconds)
  {
    if (milliseconds < 0.0)
      milliseconds = 0.0;
    uint32_t c = ((uint32_t)(milliseconds * SAMPLES_PER_MSEC) + 7) >> 3;
    if (c > 65535)
      c = 65535; // allow up to 11.88 seconds
    return c;
  }
  FLASHMEM void updateExpDecay() // needed in case sustain changes.
  {
    double k;
    k = exp(-1.0L / (decay_count * 4.0L));
    decay_k = (uint32_t)(EXP_ENV_ONE * k);
    // decay_target=(1.0L-k)*sustain_target;
  }

  void updateExpAttack() // This is needed in case env type changes.
  {
    double k1, k2;
    if (env_type > 8 || env_type < -8)
      return;          // Anything outside this range is a linear envelope.
    if (env_type == 0) // Linear attack. Everything else is exponential.
    {
      attack_k = EXP_ENV_ONE;
      attack_target = (uint32_t)(EXP_ENV_ONE / (attack_count * 8.0L));
    }
    else
    {
      k1 = exp((double)env_type / (attack_count * 8.0L));
      k2 = (k1 - 1.0L) / (exp((double)env_type) - 1.0L);
      attack_k = (uint32_t)(EXP_ENV_ONE * k1);
      attack_target = (uint32_t)(EXP_ENV_ONE * k2);
    }
  }

  FLASHMEM void updateExpRelease()
  {
    double k;
    k = exp(-1.0L / (release_count * 4.0L));
    release_k = (uint32_t)(EXP_ENV_ONE * k);
  }

  FLASHMEM void updateExpReleaseNoteOn()
  {
    double k;
    // Increased forced release rate x4 so it will end before the next note off call.
    // Unlike linear mode this time is dependant on sustain level and release rate. It may be better to implement this state as a linear ramp with a fixed time
    // for consistent behaviour.
    k = exp(-4.0L / (release_forced_count * 8.0L));
    release_forced_k = (uint32_t)(EXP_ENV_ONE * k);
  }

  audio_block_t *inputQueueArray[1];
  static const uint8_t env_type_values[NUM_ENV_TYPES];
  // state
  uint8_t state;      // idle, delay, attack, hold, decay, sustain, release, forced (now idle_next)
  uint16_t count;     // how much time remains in this state, in 8 sample units
  int32_t mult_hires; // attenuation, 0=off, 0x40000000=unity gain
  int32_t inc_hires;  // amount to change mult_hires every 8 samples

  // settings
  uint16_t delay_count;
  uint16_t attack_count;
  uint16_t hold_count;
  uint16_t decay_count;
  int32_t sustain_mult; // Shared with exponential envelope generator.
  uint16_t release_count;
  uint16_t release_forced_count;

  enum
  { // Make this a private class enum set instead of using defines.
    STATE_IDLE,
    STATE_DELAY,
    STATE_ATTACK,
    STATE_HOLD,
    STATE_DECAY,
    STATE_SUSTAIN,
    STATE_SUSTAIN_FAST_CHANGE,
    STATE_RELEASE,
    STATE_FORCED,
    STATE_IDLE_NEXT, // Not used for original linear envelope.
  };
  // Exponential ADSR variables
  int8_t env_type;    // Attack curve type. Limit to -8 to 8 integers for exp curve, -128 for original linear.
  uint32_t exp_count; // same function as count in linear generator but for single samples, not groups of 8.
  int32_t ysum;
  int32_t attack_k;
  int32_t attack_target;
  int32_t decay_k;
  int32_t release_k;
  int32_t release_forced_k;
};

#undef SAMPLES_PER_MSEC
#endif

Code:
/* Audio Library for Teensy 3.X
 * Portions Copyright (c) 2017, Paul Stoffregen, paul@pjrc.com
 * Portions Copyright (c) 2021 Vince R. Pearson
 *
 * Development of this audio library was funded by PJRC.COM, LLC by sales of
 * Teensy and Audio Adaptor boards.  Please support PJRC's efforts to develop
 * open source software by purchasing Teensy or other PJRC products.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice, development funding notice, and this permission
 * notice shall be included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <Arduino.h>
#include "effect_envelope.h"


#define RELEASE_BIAS 256 // based on a 1.31 fixed point integer. This is the level below zero that is the release target and causes the output to go to zero earlier. Otherwise it can take a relatively long time.
// Difference equation for exponential envelope.
#define FORCED_RELEASE_BIAS 0x1000000 // empirically determined with forced release time constant to attain ~5ms between forced release and next attack.
// Form 1 for attack stage: y(n+1) = k1*y(n)+kx using unsigned S1.30 fixed point format
#define EXP_ENV_FILT1(k,y,x) ((uint32_t)(((uint64_t)(y)*(k))>>30)+(uint32_t)(x))
// Form 2 for remaining stages: y(n+1) = k1*(y(n)-x(n))+x(n) using unsigned S1.30 fixed point format
#define EXP_ENV_FILT2(k,y,x) ((((int64_t)(k)*(int64_t)(y-x))>>30)+(x))
#define YSUM2MULT(x) ((x)>>14)

void AudioEffectEnvelopeTS::noteOn(void)
{
  //__disable_irq();
  if(release_forced_count==0)
    state=STATE_IDLE;
  switch(state)
  {

    case STATE_IDLE:
    case STATE_IDLE_NEXT:
      count=delay_count;
      if(count>0)
      {
        state=STATE_DELAY;
        inc_hires=0;
      }
      else
      {
        state=STATE_ATTACK;
        count=attack_count;
      inc_hires = 0x40000000 / (int32_t)count;
      }
      break;  
    case STATE_DELAY:
    case STATE_HOLD:
    case STATE_ATTACK:
    case STATE_DECAY:
    case STATE_SUSTAIN:
    case STATE_SUSTAIN_FAST_CHANGE:
    case STATE_RELEASE:
      state=STATE_FORCED;
      count=release_forced_count;
      inc_hires=(-mult_hires)/(int32_t)count;
    case STATE_FORCED:
      break;
    default:
      state=STATE_RELEASE;
      break;
  }
  //__enable_irq();
}

void AudioEffectEnvelopeTS::noteOff(void)
{
  //__disable_irq();
  switch(state)
  {
    case STATE_IDLE:
    case STATE_IDLE_NEXT:
    case STATE_DELAY:
      state=STATE_IDLE;
      break;
    default:
      state = STATE_RELEASE;
      count = release_count;
      inc_hires = (-mult_hires) / (int32_t)count;
  }
 // __enable_irq();
}

void AudioEffectEnvelopeTS::update(void)
{
  audio_block_t *block;
  uint32_t *p, *end;
  uint32_t sample12, sample34, sample56, sample78, tmp1, tmp2;
  uint32_t exp_mult[8];

  block = receiveWritable();
  if (!block) return;
  if (state == STATE_IDLE) {
    release(block);
    return;
  }
  p = (uint32_t *)(block->data);
  end = p + AUDIO_BLOCK_SAMPLES/2;
  if(env_type==-128)
  { // Original AudioEffectEnvelope class linear envelope.
    while (p < end) {
      // we only care about the state when completing a region
      if (count == 0) {
        if (state == STATE_ATTACK) {
          count = hold_count;
          if (count > 0) {
            state = STATE_HOLD;
            mult_hires = 0x40000000;
            inc_hires = 0;
          } else {
            state = STATE_DECAY;
            count = decay_count;
            inc_hires = (sustain_mult - 0x40000000) / (int32_t)count;
          }
          continue;
        } else if (state == STATE_HOLD) {
          state = STATE_DECAY;
          count = decay_count;
          inc_hires = (sustain_mult - 0x40000000) / (int32_t)count;
          continue;
        } else if (state == STATE_DECAY) {
          state = STATE_SUSTAIN;
          count = 0xFFFF;
          mult_hires = sustain_mult;
          inc_hires = 0;
        } else if (state == STATE_SUSTAIN) {
          count = 0xFFFF;
        } else if (state == STATE_RELEASE) {
          state = STATE_IDLE;
          while (p < end) {
            *p++ = 0;
            *p++ = 0;
            *p++ = 0;
            *p++ = 0;
          }
          break;
        } else if (state == STATE_FORCED) {
          mult_hires = 0;
          count = delay_count;
          if (count > 0) {
            state = STATE_DELAY;
            inc_hires = 0;
          } else {
            state = STATE_ATTACK;
            count = attack_count;
            inc_hires = 0x40000000 / (int32_t)count;
          }
        } else if (state == STATE_DELAY) {
          state = STATE_ATTACK;
          count = attack_count;
          inc_hires = 0x40000000 / count;
          continue;
        }
        else
        {
          state=STATE_IDLE; // If in some unused state switching back into linear mode, set to known state that linear mode uses.
          count=delay_count;
        }
      }

      int32_t mult = mult_hires >> 14;
      int32_t inc = inc_hires >> 17;
      // process 8 samples, using only mult and inc (16 bit resolution)
      sample12 = *p++;
      sample34 = *p++;
      sample56 = *p++;
      sample78 = *p++;
      p -= 4;
      mult += inc;
      tmp1 = signed_multiply_32x16b(mult, sample12);
      mult += inc;
      tmp2 = signed_multiply_32x16t(mult, sample12);
      sample12 = pack_16b_16b(tmp2, tmp1);
      mult += inc;
      tmp1 = signed_multiply_32x16b(mult, sample34);
      mult += inc;
      tmp2 = signed_multiply_32x16t(mult, sample34);
      sample34 = pack_16b_16b(tmp2, tmp1);
      mult += inc;
      tmp1 = signed_multiply_32x16b(mult, sample56);
      mult += inc;
      tmp2 = signed_multiply_32x16t(mult, sample56);
      sample56 = pack_16b_16b(tmp2, tmp1);
      mult += inc;
      tmp1 = signed_multiply_32x16b(mult, sample78);
      mult += inc;
      tmp2 = signed_multiply_32x16t(mult, sample78);
      sample78 = pack_16b_16b(tmp2, tmp1);
      *p++ = sample12;
      *p++ = sample34;
      *p++ = sample56;
      *p++ = sample78;
      // adjust the long-term gain using 30 bit resolution (fix #102)
      // https://github.com/PaulStoffregen/Audio/issues/102
      mult_hires += inc_hires;
      count--;
    }
  }
  else  //Exponential ADSR Vince R. Pearson
  {
    while (p < end)
    {
      for (uint8_t i = 0; i < 8; i++)
      {
        switch(state)
        {
          case STATE_IDLE:
            // Since delay counts may not necessarily start at aligned boundaries of 8 samples,
            // the loop count has to be multiplied by 8
            // and counted down in the for loop, not in the while loop.
            exp_count=((uint32_t)(delay_count))*8;
            ysum=0;
            state=STATE_IDLE_NEXT; // Do this so reinitialization is only done once at every idle state.
            // Falls through to STATE_IDLE_NEXT
           
          case STATE_IDLE_NEXT:
            break; //ysum is zero here
           
          case STATE_DELAY:
            if(exp_count--) break; // ysum is zero here
            state=STATE_ATTACK;
            break;
           
          case STATE_ATTACK:
            ysum=EXP_ENV_FILT1(attack_k,ysum,attack_target);
            if(ysum>=EXP_ENV_ONE)
            {   // The maximum 32 bit value of the envelope has been reached.
              ysum=EXP_ENV_ONE;
              if(hold_count)
              {
                exp_count=((uint32_t)hold_count)*8;
                state=STATE_HOLD;
              }
              else
              {
                state=STATE_DECAY;
                exp_count=((uint32_t)delay_count)*8; // Only used to arbitrarioly define where sustain begins.
              }
            }
            break;
           
          case STATE_HOLD:
            if((exp_count--)==0)
            {
               state=STATE_DECAY; // ysum is maximum here (ENV_MAX_32).
               exp_count=((uint32_t)delay_count)*8;
            }
            break;
           
          case STATE_DECAY:
            if((exp_count--)==0) state=STATE_SUSTAIN;
            // Sustain is only needed to support isRelease(). This happens after delay_count is decremented to zero.
            // The point at which sustain begins after the start of decay is arbitrary on an exponential curve
            // Here it occurs after one time constant (delay_count*8) which is 63.2% down the decay curve.
            ysum=EXP_ENV_FILT2(decay_k,ysum,sustain_mult);
            break;
           
          case STATE_SUSTAIN: // Gets here from decay when delay count is zero. Used to flag when sustain begins.
            ysum=EXP_ENV_FILT2(decay_k,ysum,sustain_mult);
            break;
           
          case STATE_SUSTAIN_FAST_CHANGE: // Only gets here when sustain is changed while in decay or any sustain states.
            ysum=EXP_ENV_FILT2(FAST_SUSTAIN_K1,ysum,sustain_mult);
            break;
           
          case STATE_RELEASE:
            ysum=EXP_ENV_FILT2(release_k,ysum,-RELEASE_BIAS);
            if(ysum<0) ysum=0;
            // Bias added to end release a bit sooner. Value must be checked for underflow since unsigned integers are used.
            // This has affects the longest release settings the most since it is not scaled with the release time constant.
            if(YSUM2MULT(ysum)==0) // All of the useful bits are zero so no reason to stay in this state.
              state=STATE_IDLE;
            break;
           
          case STATE_FORCED:          
            ysum=EXP_ENV_FILT2(release_forced_k,ysum,-FORCED_RELEASE_BIAS);
            if(YSUM2MULT(ysum)<=0)
            {
              state=STATE_ATTACK;// revert to IDLE state when useful bits are zero.
              ysum=0;
            }
          default:
            break;
        }
        exp_mult[i]=YSUM2MULT(ysum);
      }
      // multiply audio samples with 8 envelope samples
      sample12 = *p++;
      sample34 = *p++;
      sample56 = *p++;
      sample78 = *p++;
      p -= 4;
      tmp1 = signed_multiply_32x16b(exp_mult[0], sample12);
      tmp2 = signed_multiply_32x16t(exp_mult[1], sample12);
      sample12 = pack_16b_16b(tmp2, tmp1);
      tmp1 = signed_multiply_32x16b(exp_mult[2], sample34);
      tmp2 = signed_multiply_32x16t(exp_mult[3], sample34);
      sample34 = pack_16b_16b(tmp2, tmp1);
      tmp1 = signed_multiply_32x16b(exp_mult[4], sample56);
      tmp2 = signed_multiply_32x16t(exp_mult[5], sample56);
      sample56 = pack_16b_16b(tmp2, tmp1);
      tmp1 = signed_multiply_32x16b(exp_mult[6], sample78);
      tmp2 = signed_multiply_32x16t(exp_mult[7], sample78);
      sample78 = pack_16b_16b(tmp2, tmp1);
      *p++ = sample12;
      *p++ = sample34;
      *p++ = sample56;
      *p++ = sample78;
    }
  }
  transmit(block);
  release(block);
}

bool AudioEffectEnvelopeTS::isActive()
{
  uint8_t current_state = *(volatile uint8_t *)&state;
  if ((current_state == STATE_IDLE)|| (current_state==STATE_IDLE_NEXT)) return false; // Added STATE_IDLE_NEXT. VRP
  return true;
}

bool AudioEffectEnvelopeTS::isSustain()
{
  uint8_t current_state = *(volatile uint8_t *)&state;
  if (current_state == STATE_SUSTAIN || current_state==STATE_SUSTAIN_FAST_CHANGE) return true;
  return false;
}
 
DISCLAIMER: I haven't run this code, but...

It looks as if the issue is that exp_count doesn't get set in noteOn(). If you trigger a new note exactly as the release phase finishes, so state == STATE_IDLE but an update() hasn't occurred to set exp_count to the delay value, it will have the stale value from the end of the decay phase, which is -232, so your delay suddenly becomes very long indeed.
 
I've tested it, it's very sensitive to the exact value of release time: 86.948ms seems to work consistently for the normal 44.1kHz sample rate and 128-sample audio blocks.

See comments in this code for changes needed to monitor the class's private state, and the proposed fix.
C++:
/*
 * Test of exponential envelope bug: forum thread 75646
 * https://forum.pjrc.com/index.php?threads/teensy-exponential-envelopes.75646/
 *
 * Need to remove private access specifier from the AudioEffectEnvelopeTS
 * class so we can track the state and exp_count values.
 *
 * Bug is incredibly sensitive to the exact release time.
 *
 * Fix is to add the following in noteOn() case STATE_IDLE:
      exp_count=((uint32_t)(delay_count))*8;
      ysum=0;
 */
#include <Audio.h>
#include "expenv.h" // include my local copy - you may not need this

// GUItool: begin automatically generated code
AudioSynthWaveform       wav;      //xy=382,345
AudioEffectEnvelopeTS    env;      //xy=595,344
AudioOutputI2S           i2sOut;           //xy=789,343

AudioConnection          patchCord1(wav, env);
AudioConnection          patchCord2(env, 0, i2sOut, 0);
AudioConnection          patchCord3(env, 0, i2sOut, 1);

AudioControlSGTL5000     sgtl5000;     //xy=811,407
// GUItool: end automatically generated code

void setup()
{
  while (!Serial)
    ;
    
  AudioMemory(10);
 
  sgtl5000.enable();
  sgtl5000.volume(0.5f);

  wav.begin(0.5f, 220.0f,WAVEFORM_SINE);

  env.setEnvType(-4); // ensure we're using exponential version
  env.delay(10);      // must have a non-zero delay to trigger the bug

  Serial.println("Started");
}

uint8_t lastState = 111;
elapsedMillis ts;
float releaseTime = 86.948f; // unsafe value
void loop()
{
  if (!env.isActive())
  {
    Serial.println();
    env.release(releaseTime);
    env.noteOn();
    ts = 0;
  }
 
  if (env.isSustain() && ts > 45)
  {
    env.noteOff();
  }

  if (lastState != env.state)
  {
    lastState = env.state;
    Serial.printf("t=%d; state=%d; exp_count=%lu\n",(int) ts,lastState,env.exp_count);
  }

  if (ts > 2000) // taking too long, must be wedged
  {
    if (ts % 250 == 0)
    {
      Serial.printf("t=%d; state=%d; exp_count=%lu\n",(int) ts,lastState,env.exp_count);     
      delay(2);
    }       
  }

  if (ts > 10000) // unwedge the system
  {
    ts = 0;
    env.exp_count = 1000;
  }
}
 
Frederic saw that one envelope froze by doing following test. Play a sequencer, and all notes should be heard. Then while sequencer is playing, play on the keyboard. Suddenly one of the envelopes is dead (you can hear it very well when you stop playing but the sequencer plays, one note is missing).
However, if you continue playing the keyboard, the dead note may come back to life.

Frederic modified the code and tested it the same way, didn't have the problem anymore, but it's very hard to be sure.

In the main I changed following:
C:
FLASHMEM void updateAttack() //!!!FRE
{
    for (size_t i = 0; i < 8; i++){
            ampEnvelope.attack(ampAttack);
            ampEnvelope.hold(300);
    }
}

Note that the code becomes much simpler by using a for loop in stead of listing 8 times the same thing. I recommend that you change the code for all 0-7 lists. And in the effect envelope.cpp I added the two lines: starting at line 63

Env_Fred.png
 
Last edited:
Is this all referencing some code you haven't posted? Jeannie? Is that code fragment from the main() function? Does it help reproduce the issue? What relevance does the advice in italics have to the envelope freezing issue?

As far as I know I don't have "a sequencer" or "the keyboard", and in any case it sounds like a very manual test that is unlikely to give you consistently reproducible results, i.e. failure every time (or at least within a very short timespan) with unmodified code, and success over an extended period (say 100x your slowest failure time) with the fix in place. Hence your finding that "it's very hard to be sure"...

I tried to track down the origin of the exponential envelope code (Vince R. Pearson, I'm guessing) but with no luck, so it's not possible to submit a PR for him. Can you point me at the original repository? It's noticeable (to me) that his updated / improved code still has the original PJRC bug that the envelope doesn't update properly when fed with NULL blocks (i.e. silence). My PR for that has been in place for nearly two years now: who knows, maybe it'll get merged in Teensyduino 1.60 :unsure:
 
Back
Top