Teensy 3.1 Audio Library - Envelope limited to 1000 ms?

quarterturn

Well-known member
I've been testing the Teensy 3.1 audio library and find that the envelope object doesn't seem to respond to A, D, or R values greater than 1000 ms, perhaps even a bit shorter.

In my example code below, envelope1 is after the filter and envelope2 feeds a DC signal into the frequency control of the filter. Envelope1 just has a sustain level of 1.0, so basically it's not there. Envelope2 should produce a sound with a quick attack and long release, but it's more like a quick "wow".

I wait 3000 ms before calling noteOff, so there should be plenty of time for phase D to complete.

Bug, or am I doing it wrong?

Code:
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>

// GUItool: begin automatically generated code
AudioSynthWaveformDc     dc1;            //xy=57,359
AudioSynthWaveform       waveform1;      //xy=86,216
AudioEffectEnvelope      envelope2;      //xy=165,283
AudioFilterStateVariable filter1;        //xy=367,277
AudioEffectEnvelope      envelope1;      //xy=554,266
AudioOutputI2S           i2s1;           //xy=570,411
AudioConnection          patchCord1(dc1, envelope2);
AudioConnection          patchCord2(waveform1, 0, filter1, 0);
AudioConnection          patchCord3(envelope2, 0, filter1, 1);
AudioConnection          patchCord4(filter1, 0, envelope1, 0);
AudioConnection          patchCord5(envelope1, 0, i2s1, 0);
AudioConnection          patchCord6(envelope1, 0, i2s1, 1);
AudioControlSGTL5000     audioShield;     //xy=569,528
// GUItool: end automatically generated code

void setup(void)
{

  // Set up
  AudioMemory(8);
  audioShield.enable();
  audioShield.volume(0.45);

  // audio waveform
  waveform1.pulseWidth(0.5);
  waveform1.begin(0.2, 100, WAVEFORM_PULSE);

  // EG for VCA
  envelope1.attack(1);
  envelope1.decay(1);
  envelope1.sustain(1.0);
  envelope1.release(0.0);

  // EG for VCF
  envelope2.attack(100);
  envelope2.decay(2800);
  envelope2.sustain(0.0);
  envelope2.release(0.0);
  
  // DC input to EG for VCF
  dc1.amplitude(0.3);
  
  // VCF setup
  // corner frequency when control is zero
  filter1.frequency(0);
  // resonance
  filter1.resonance(5.0);
  // filter range
  filter1.octaveControl(7);
}

void loop() {
  
  float w;
  float v = 0.3;
  for (uint32_t i =1; i<20; i++) {
    w = i / 20.0;
    waveform1.pulseWidth(w);
    envelope1.noteOn();
    envelope2.noteOn();
    delay(3000);
    envelope1.noteOff();  
    envelope2.noteOff();
    delay(100);
    v = v + 0.1;
    if ( v > 1.0 )
    {
      v = 0.3;
    }
    dc1.amplitude(v);
  }
}
 
Envelope2 should produce a sound with a quick attack and long release, but it's more like a quick "wow".
(Assuming you mean long decay, since you set release to 0.0 and talk about "phase D" later on)

I wait 3000 ms before calling noteOff, so there should be plenty of time for phase D to complete.

From the documentation
http://www.pjrc.com/teensy/gui/?info=AudioEffectEnvelope
The recommended range for each of the 5 timing inputs is 0 to 50 milliseconds. Up to 200 ms can be used, with somewhat reduced accuracy

looking in effect_envelope.h the DAHD&R vlues in milliseconds go to a function milliseconds2count defined as

Code:
	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 > 1103) return 1103; // allow up to 200 ms
		return c;
	}
so any value > 200ms is clamped.

This object is not really a generic envelope. Its a combined envelope and VCA aimed specifically at note on/off articulation. You would probably want a different envelope object for controlling VCF, LFOs and so on over longer time scales.

I'm curious if the decision to clamp values to 200ms was made for performance reasons or whether it was just assumed that no-one would need longer values.

There is a fade object whose times are not clamped;
http://www.pjrc.com/teensy/gui/?info=AudioEffectFade
 
Ah, I didn't see the last part of the documentation - there it is!

I'll look the the source some more and try to see why it's limited to 200 ms.

The fade stuff is not desirable for an ADSR, since you'd have to wrap your own timing around things to know when in and out were completed. The envelope function saves a ton of work tracking and timing envelope states.
 
examine the attack phase:
attack_count gets set in void AudioEffectEnvelope::noteOn(void)

defined in effect_envelope.h:
void attack(float milliseconds) {
attack_count = milliseconds2count(milliseconds);
}

calculated as:
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 > 1103) return 1103; // allow up to 200 ms
return c;
}

So, attack_count is limited to 1103 by the above.
In this case, inc = 65536 / count >> 3, which is 7

I think there's a duplication of rightwards bit shifting, because you want to ensure you increment in the attack phase by at least 1, which would be the case at about 1600 ms (if you don't shift 3 bits rightwards). I must be missing something though.
 
Yup, it's a numerical precision issue.

For version 1.0, I wanted to limit the times to a range where the code can always be very accurate. It's much easier to widen the allowed range in future versions than it is to later restrict it when/if it turns out the numerical precision is a problem.

You can experiment with longer times by just editing the code to allow them. But the increment become small and the round-off to the nearest integer becomes a larger error. Please let me know how it actually works, if you try this.
 
Yup, it's a numerical precision issue.

For version 1.0, I wanted to limit the times to a range where the code can always be very accurate. It's much easier to widen the allowed range in future versions than it is to later restrict it when/if it turns out the numerical precision is a problem.

You can experiment with longer times by just editing the code to allow them. But the increment become small and the round-off to the nearest integer becomes a larger error. Please let me know how it actually works, if you try this.

Thanks for the reply!

I made the following change to effect_envelope.h:
Code:
private:
	uint16_t milliseconds2count(float milliseconds) {
		if (milliseconds < 0.0) milliseconds = 0.0;
		uint32_t c = ((uint32_t)(milliseconds*SAMPLES_PER_MSEC)+7);
		if (c > 8800) return 8800; // allow up to 1600 ms
		return c;
	}

I tested with a decay value of 1600 ms and it works fine and sounds good too.

Having this much extra envelope phase time will make for nice synth sounds.
 
I've played with this some more, it's not right.

In noteOn, the attack increment is calculated as:
inc = (0x10000 / count ) >> 3;

so, milliseconds2count can't ever return more than 8192, since 65536/8192 = 8, and 8 >> 3 is 1.

But, nonwithstanding, attack is still not working right. Very small values of A produce a long attack, eg. 90 ms sound like 1 second. Much more than that and it's obviously not increasing the level properly as you hear a ramp up until time runs out for the phase, at which point it falls from the max level in phase D.

My guess is that other bitshifting in effect_envelope.cpp needs adjusting.
 
Good grief! I lost a left-shift! effec_envelope.h private milliseconds2count needs to look like this:

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 > 8192) return 8192; // allow up to 1600 ms
return c;
}
 
The potential trouble with allowing count to go all the way to 8192 is the increment becomes a very small number integer. If the increment should have been 1.49, it gets rounded down to 1.0, or if it should have been 1.5 is gets rounded up to 2.0, for about 50% error.

That's why I limited the version 1.0 code to 1103. That means the increment is always 7 or higher. Integer round off can cause no more than 7% error.

Admittedly, that's probably more conservative than it needs to be. Especially with public APIs, where restricting options means breaking programs people have written, I tend to be pretty conservative. It's much easier to add to an API than to take something away or change it in incompatible ways.

I'm happy to increase the range in the official code, but allowing so long a duration that range that the rate of change can be off by 50% worries me.

The real question is how much error in the increment is acceptable?
 
Last edited:
I should note that a count of 8192 is actually 1489 ms, not 1600 ms. A tenth of a second difference is not perceptible.

I appreciate your involvement in minor details like this. The library, the audio shield and the Teensy 3.1 are a great combo.

The potential trouble with allowing count to go all the way to 8192 is the increment becomes a very small number integer. If the increment should have been 1.49, it gets rounded down to 1.0, or if it should have been 1.5 is gets rounded up to 2.0, for about 50% error.

That's why I limited the version 1.0 code to 1103. That means the increment is always 7 or higher. Integer round off can cause no more than 7% error.

Admittedly, that's probably more conservative than it needs to be. Especially with public APIs, where restricting options means breaking programs people have written, I tend to be pretty conservative. It's much easier to add to an API than to take something away or change it in incompatible ways.

I'm happy to increase the range in the official code, but allowing so long a duration that range that the rate of change can be off by 50% worries me.

The real question is how much error in the increment is acceptable?
 
I'm happy to increase the range in the official code, but allowing so long a duration that range that the rate of change can be off by 50% worries me.

The real question is how much error in the increment is acceptable?

I think a good way to handle this is to retain an indication of the intended value, to avoid cumulative error. So one time round the loop the intended increment is 1.49 and gets rounded down to 1; but the next time around the intended total increment is 2.98, the actual current value is 1.0 so the actual increment is 2.0. This avoids multiplyimg up the roundoff error.
 
That's a great idea Nantonos!

I can think of a couple ways this might be implemented. Probably the simplest would be adding an integer post-increment number, which is simply added after the 8 normal ones, in each iteration of the loop. That should cut the cumulative error by a factor of 8.

A more accurate approach would probably increment a 32 bit integer, carrying 16 fractional bits. That should reduce error to pretty much zero, but perhaps it's too much complexity? Or maybe not?


Edit: I've added it to the issue list, so this won't be forgotten months from now, when I will be working on the code again.

https://github.com/PaulStoffregen/Audio/issues/102
 
Last edited:
If anyone's still watching this old thread, I've *finally* fixed this long-standing limitation in the envelope effect. The fix is on github now, and will be released in Teensyduino 1.37 in June-July 2017 time frame.

https://github.com/PaulStoffregen/Audio/commit/8a5d715dda3903d82d4ebc1aececa40131281014

I increased the internal numerical resolution for gain adjustment from 16 to 30 bits. The prior limit of 0.2 seconds for attack, hold, decay & release is now increased to 11.88 seconds.

I also tried to fix the pop that could occur if you call noteOff() before the sustain phase. Could really use some feedback on the effectiveness of this fix, if anyone is willing to try the latest from github...
 
If anyone's still watching this old thread, I've *finally* fixed this long-standing limitation in the envelope effect. The fix is on github now, and will be released in Teensyduino 1.37 in June-July 2017 time frame.

https://github.com/PaulStoffregen/Audio/commit/8a5d715dda3903d82d4ebc1aececa40131281014

I increased the internal numerical resolution for gain adjustment from 16 to 30 bits. The prior limit of 0.2 seconds for attack, hold, decay & release is now increased to 11.88 seconds.

I also tried to fix the pop that could occur if you call noteOff() before the sustain phase. Could really use some feedback on the effectiveness of this fix, if anyone is willing to try the latest from github...

I'll see if I can re-assemble my Teensy 3.x MIDI setup and try this.

Did anything get updated since with the lowpass filter behavior? I see to recall there was an issue with high cutoff frequency and lowest resonance - it breaks out into LOUD white noise, maybe some sort of chaotic behavior in the feedback loop? Also, can the filter yet self-oscillate?

Thanks!
 
If anyone's still watching this old thread, I've *finally* fixed this long-standing limitation in the envelope effect. The fix is on github now, and will be released in Teensyduino 1.37 in June-July 2017 time frame.

https://github.com/PaulStoffregen/Audio/commit/8a5d715dda3903d82d4ebc1aececa40131281014

I increased the internal numerical resolution for gain adjustment from 16 to 30 bits. The prior limit of 0.2 seconds for attack, hold, decay & release is now increased to 11.88 seconds.

I also tried to fix the pop that could occur if you call noteOff() before the sustain phase. Could really use some feedback on the effectiveness of this fix, if anyone is willing to try the latest from github...

I just updated to this new envelope library and so far it is brilliant, so much more useful than the old envelope effect :)

What I'd like to see happen now is a modulation input on the waveform module for vibrato effects, and a modulation input on the delay / delay-ext effects for modulated echo effects, then we can build some amazing digital synthesisers !

Some simple switches would be useful too - for routing signals throughout the synth.
 
If anyone's still watching this old thread, I've *finally* fixed this long-standing limitation in the envelope effect. The fix is on github now, and will be released in Teensyduino 1.37 in June-July 2017 time frame.

https://github.com/PaulStoffregen/Audio/commit/8a5d715dda3903d82d4ebc1aececa40131281014

I increased the internal numerical resolution for gain adjustment from 16 to 30 bits. The prior limit of 0.2 seconds for attack, hold, decay & release is now increased to 11.88 seconds.

I also tried to fix the pop that could occur if you call noteOff() before the sustain phase. Could really use some feedback on the effectiveness of this fix, if anyone is willing to try the latest from github...

The click/pop issue still seems to be there, unfortunately. A click will likely occur where there is a sudden discontinuity in the waveform. Many synths over the years have suffered with this problem! If that is what it is, then one way around it may be to not allow it to snap back to zero quite so quickly, with a quick ramp down. If it were fast enough (say 0.5-1ms), it shouldn't affect the snappiness of the envelope too much. If it had to be longer, then making it switchable would still allow for snappy drums. Just a thought.
 
Any chance you could give me a simple sketch that makes this sound?

If you have a USB or MIDI keyboard, then this demonstrates it.

Code:
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>
#include <MIDI.h>

AudioSynthWaveformSine   sine1;          //xy=226,136
AudioEffectEnvelope      envelope1;      //xy=377,136
AudioMixer4              mixer1;         //xy=533,139
AudioOutputI2S           i2s1;           //xy=727,116
AudioConnection          patchCord1(sine1, envelope1);
AudioConnection          patchCord2(envelope1, 0, mixer1, 0);
AudioConnection          patchCord3(mixer1, 0, i2s1, 0);
AudioConnection          patchCord4(mixer1, 0, i2s1, 1);
AudioControlSGTL5000     sgtl5000_1;     //xy=557,203

void setup() {
  MIDI.begin(MIDI_CHANNEL_OMNI);
  MIDI.setHandleNoteOff(noteOffReceived);
  MIDI.setHandleNoteOn(noteOnReceived);
  usbMIDI.begin();
  usbMIDI.setHandleNoteOff(noteOffReceived);
  usbMIDI.setHandleNoteOn(noteOnReceived);

  AudioMemory(10);        
  sgtl5000_1.enable();
  sgtl5000_1.volume(0.5);

  mixer1.gain(0, 0.5);
  mixer1.gain(1, 0.0);
  mixer1.gain(2, 0.0);
  mixer1.gain(3, 0.0);

  envelope1.delay(0.0);
  envelope1.attack(2000.0);
  envelope1.hold(0.0);
  envelope1.decay(0.0);
  envelope1.sustain(1.0);
  envelope1.release(1000.0);
}

void loop() {
  MIDI.read();
  usbMIDI.read();
}

void noteOnReceived(byte channel, byte note, byte velocity) {
  if (velocity == 0) {
    envelope1.noteOff();
    return;
  }
  sine1.amplitude(0.5);
  sine1.frequency(440.0);
  envelope1.noteOn();
}

void noteOffReceived(byte channel, byte note, byte velocity) {
  envelope1.noteOff();
}
 
Last edited:
Any chance you could give me a simple sketch that makes this sound?

and here's one if you don't have a keyboard...

The clicks are more subtle like this, but they are still there for me.
Interestingly they don't happen every time.

Code:
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>

AudioSynthWaveformSine   sine1;          //xy=226,136
AudioEffectEnvelope      envelope1;      //xy=377,136
AudioMixer4              mixer1;         //xy=533,139
AudioOutputI2S           i2s1;           //xy=727,116
AudioConnection          patchCord1(sine1, envelope1);
AudioConnection          patchCord2(envelope1, 0, mixer1, 0);
AudioConnection          patchCord3(mixer1, 0, i2s1, 0);
AudioConnection          patchCord4(mixer1, 0, i2s1, 1);
AudioControlSGTL5000     sgtl5000_1;     //xy=557,203

void setup() {
  
  AudioMemory(10);         
  sgtl5000_1.enable();
  sgtl5000_1.volume(0.5);

  mixer1.gain(0, 0.5);
  mixer1.gain(1, 0.0);
  mixer1.gain(2, 0.0);
  mixer1.gain(3, 0.0);

  envelope1.delay(0.0);
  envelope1.attack(2000.0);
  envelope1.hold(0.0);
  envelope1.decay(0.0);
  envelope1.sustain(1.0);
  envelope1.release(1000.0);

  sine1.amplitude(0.5);
  sine1.frequency(440.0);

}

void loop() {
  envelope1.noteOn();
  delay(100.0);
  envelope1.noteOff();
  delay(500.0);
}
 
Another way to possibly fix the issue, would be to ensure that the waveform starts and ends exactly at a zero-crossing (amplitude=0). But I'm guessing that that may be more tricky as the audio is always delivered in 128 sample packets? If so, the last few samples could be faded, perhaps? .... I'm sure you've tried various methods of this!!
- great library, by the way :)
 
When this came up in my audio processing class I think the answer was that the corners of the envelope are always smoothed so there are no corners. Discontinuities in the rate of change make clicks.

So convert the trapezoidal envelope into a smooth curve of samples at instantiation, and simply convolve the envelope samples with the audio stream at run time.

But I see the envelope that DirtBox_Rich gave you also has some 0 duration elements in the middle, those might be even more discontinuous.
 
Do you think this could be accomplished by adding 1, 2 or even 3 more linear slopes for short durations to smooth the transition?

It's easy to say things like "simply convolve the envelope samples", but do actually do them requires quite a bit of memory and processor time. I'm planning to add a piecewise control waveform synthesis object, which will allow exactly this sort of thing when used as an input to the multiplier object. But doing an actual exponential waveform and then the multiply step is much, much more CPU and memory intensive than the envelope's linear ramping.
 
Hi, I've tried the last update,
I have very low knowledge of programming, but if it helps, I hear that the problem is in the transition to sustain, if the sustain is not in a value 1 there are always clicks
* I can not contribute more ... I'm sorry
Thanks for this great library. I'm having a great time.
 
Back
Top