Clicking envelope with Audio Library?

martin42

New member
Hi,

first of all, thanks a bunch for Audio lib, it's overwhelming and It so far.

Im running into problems with envelopes lately, trying to synthesize kick drum. For some unknown reason, there appears to be really loud audible click at end of envelope, mostly when I'm passing low frequencies through it. I'm using in built DAC with 10uF cap for output

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

AudioSynthWaveformDc     dc;
AudioEffectEnvelope      env;
AudioEffectEnvelope      out;
AudioSynthWaveformSineModulated sinFM;
AudioOutputAnalog        dac1;
AudioConnection          patchCord1(dc, env);
AudioConnection          patchCord2(env, sinFM);
AudioConnection          patchCord4(sinFM, out);
AudioConnection          patchCord5(out, dac1);



void setup()
{

    AudioMemory(100);
    dc.amplitude(1);
    env.attack(4);
    env.decay(80);
    env.sustain(0.0);

    out.attack(10);
    out.decay(180);
    out.sustain(0.0);
    out.release(1);

    sinFM.frequency(90);
    sinFM.amplitude(0.75);
}

void loop()
{
    
    env.noteOn();
    out.noteOn();
    delay(250);
    env.noteOff();
    out.noteOff();
    delay(500);
    
    sinFM.frequency(map(analogRead(0), 1023, 0, 50, 150));
    env.decay(map(analogRead(1), 1023, 0, 5, 180));
    out.decay(map(analogRead(2), 1023, 0, 50, 200));
    out.hold(map(analogRead(2), 1024, 0, 50, 200));
}

Am I missing something, or I'm running into some sort of bug?
Thanks a bunch!
 
Last edited:
Is that the exact code you are using? It doesn't compile, for me: trig is used but not defined.

Code:
Arduino: 1.6.0 (Windows 7), TD: 1.21-beta5, Board: "Teensy 3.1, No USB, 96 MHz (overclock), US English"

Using library Audio in folder: D:\Arduino\arduino-1.6.0-td121b5\hardware\teensy\avr\libraries\Audio (legacy)

Using library Wire in folder: D:\Arduino\arduino-1.6.0-td121b5\hardware\teensy\avr\libraries\Wire (legacy)

Using library SPI in folder: D:\Arduino\arduino-1.6.0-td121b5\hardware\teensy\avr\libraries\SPI (legacy)

Using library SD in folder: D:\Arduino\arduino-1.6.0-td121b5\hardware\teensy\avr\libraries\SD (legacy)



D:\Arduino\arduino-1.6.0-td121b5/hardware/tools/arm/bin/arm-none-eabi-g++ -c -g -Os -Wall -ffunction-sections -fdata-sections -MMD -nostdlib -fno-exceptions -felide-constructors -std=gnu++0x -fno-rtti -mthumb -mcpu=cortex-m4 -D__MK20DX256__ -DTEENSYDUINO=121 -DARDUINO=10600 -DF_CPU=96000000 -DARDUINO_ARCH_AVR -DUSB_DISABLED -DLAYOUT_US_ENGLISH -ID:\Arduino\arduino-1.6.0-td121b5\hardware\teensy\avr\cores\teensy3 -ID:\Arduino\arduino-1.6.0-td121b5\hardware\teensy\avr\libraries\Audio -ID:\Arduino\arduino-1.6.0-td121b5\hardware\teensy\avr\libraries\Wire -ID:\Arduino\arduino-1.6.0-td121b5\hardware\teensy\avr\libraries\SPI -ID:\Arduino\arduino-1.6.0-td121b5\hardware\teensy\avr\libraries\SD C:\Users\Chris\AppData\Local\Temp\build7482926829224716626.tmp\audio_env_click.cpp -o C:\Users\Chris\AppData\Local\Temp\build7482926829224716626.tmp\audio_env_click.cpp.o 

audio_env_click.ino: In function 'void loop()':

audio_env_click.ino:40:5: error: 'trig' was not declared in this scope

'trig' was not declared in this scope
 
I'm sorry, I apologize for this. This is part of callback thing for buttons, it accidentally slipped into reduced version I posted up.


Code is exhibiting same behavior when used with callback to trigger envelope or using simple delay() function.

One thing I forgot to mention is that filtering helps a bit to smooth out click, so problem could be related to phase?

EDIT:> I removed line breaking compilation, now it's ok + removed filter, now it's straight from output envelope to dac, but still popping :(
 
Last edited:
I ran your code, and played around with it a bit. I agree there is a loud click at envelope end. The nature of the click depends somewhat on the settings of the two envelopes but I was not able to make it go away by changing envelope settings.

Wrapping the envelope changes in AudioNoInterrupts() and AudioInterrupts() - to make the changes happen at the same time - had no effect.

I also modified the code to use the I2S output on the audio shield, which had no effect (so its unrelated to the built-in DAC) and while I was at it, out a steep low-pass 18kHz biquad filter between the out envelope and the right hand channel (left channel bypassing the filter) in case the click was caused by aliasing. It wasn't.

I'm vaguely wondering whether linear envelopes such as the audio library uses are more succeptible to clicks than exponential envelopes. I have seen clicks in the analog domain when switching a VCA too fast.

There is some sort of DC-detecting and blocking on the SGTL500, I wondered if that was being upset by pulses, but the built-in DAC doesn't have that and still clicks.

The general principle of your sketch (sending a pulse to the fm input on a sine oscillator) seems sound and is the usual way to make tom-like sounds (that or send the pulse to the frequency cutoff of a filter). Basically it all seems good and should work. Sorry not to be more help.
 
That is unfortunate.

What are you describing are steps I took to attempt to get rid of clicks. Disabling interrupts, changing envelopes parameters, adjusting amplitudes... No result. I was hoping that it's problem with smaller output capacitor than recommended. Nope. So I ordered audio adapter for testing but apparently that's not solution either.

I'm lacking time and proper equipment to investigate deeper for now, but fortunately there might be someone more qualified who could elaborate on this.
 
That is unfortunate.

What are you describing are steps I took to attempt to get rid of clicks. Disabling interrupts, changing envelopes parameters, adjusting amplitudes... No result. I was hoping that it's problem with smaller output capacitor than recommended. Nope. So I ordered audio adapter for testing but apparently that's not solution either.

I'm lacking time and proper equipment to investigate deeper for now, but fortunately there might be someone more qualified who could elaborate on this.

Knowing a little bit about signal processing, when I read 'envelope', I understood 'envelope of an oscillating function' or equivalently the amplitude of an analytic extended function (Hilbert transform etc.). Looking into the code I see some sort of filtering, so the code may approximate the envelope and may be standard approach with audio systems. Nevertheless, it is a filter and filters have transients at the beginning and at the end. There are methods to handle these transients.

@Paul or someone knowledgeable, could you point me to a description of the envelope algorithm?
 
I will investigate. Could you please make a small edit to the sample code?

On these 4 lines, analogRead is used:

Code:
    sinFM.frequency(map(analogRead(0), 1023, 0, 50, 150));
    env.decay(map(analogRead(1), 1023, 0, 5, 180));
    out.decay(map(analogRead(2), 1023, 0, 50, 200));
    out.hold(map(analogRead(2), 1024, 0, 50, 200));

Can you edit these to have fixed values? This would really help me to help get you a fix.
 
I'm trying to reproduce the problem, but not having much luck.

I've got the DAC connected to my scope, and I'm listening with a good quality amplified speaker. I'm not hearing any clicks.

Here's what I'm seeing on my oscilloscope.

file.png

file2.png

Here's the code I'm running. Maybe I guessed 4 numbers that don't reproduce the problem?

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

AudioSynthWaveformDc     dc;
AudioEffectEnvelope      env;
AudioEffectEnvelope      out;
AudioSynthWaveformSineModulated sinFM;
AudioOutputAnalog        dac1;
AudioConnection          patchCord1(dc, env);
AudioConnection          patchCord2(env, sinFM);
AudioConnection          patchCord4(sinFM, out);
AudioConnection          patchCord5(out, dac1);

void setup()
{
    pinMode(12, OUTPUT);
    AudioMemory(100);
    dc.amplitude(1);
    env.attack(4);
    env.decay(80);
    env.sustain(0.0);
    out.attack(10);
    out.decay(180);
    out.sustain(0.0);
    out.release(1);
    sinFM.frequency(90);
    sinFM.amplitude(0.75);
}

void loop()
{
    digitalWrite(12, HIGH);
    delayMicroseconds(100);
    digitalWrite(12, LOW);
    env.noteOn();
    out.noteOn();
    delay(250);
    env.noteOff();
    out.noteOff();
    delay(500);
    sinFM.frequency(150);
    env.decay(50);
    out.decay(100);
    out.hold(100);
}
 
Paul, with your code (but edited to use SGTL5000 rather than DAC) I do hear a slight click but its much fainter.

Here is the code I had earlier (with attempts to LPF it one one side) with a clear click:
Code:
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>

// GUItool: begin automatically generated code
AudioSynthWaveformDc     dc;            //xy=97,180
AudioEffectEnvelope      env;      //xy=227,222
AudioEffectEnvelope      out;      //xy=346,306
AudioSynthWaveformSineModulated sinFM;       //xy=364,176
AudioFilterBiquad        biquad;        //xy=408,444
AudioOutputI2S           i2s;           //xy=568,420
AudioControlSGTL5000     sgtl5000;
AudioConnection          patchCord1(dc, env);
AudioConnection          patchCord2(env, sinFM);
AudioConnection          patchCord3(sinFM, out);
AudioConnection          patchCord4(out, 0, i2s, 0);
AudioConnection          patchCord5(out, biquad);
AudioConnection          patchCord6(biquad, 0, i2s, 1);
// GUItool: end automatically generated code


void setup()
{
    sgtl5000.enable();
    sgtl5000.volume(0.6);
    AudioMemory(30);
    dc.amplitude(1);
    env.attack(3);
    env.decay(80);
    env.sustain(0.0);

    out.attack(20);
    out.decay(180);
    out.sustain(0.05);
    out.release(2.0);
    
    biquad.setLowpass(0, 18000, 0.7);
    biquad.setLowpass(1, 19000, 0.7);
    biquad.setLowpass(2, 20000, 0.7);

    sinFM.frequency(90);
    sinFM.amplitude(0.75);
}

void loop()
{
    AudioNoInterrupts();
    env.noteOn();
    out.noteOn();
    AudioInterrupts();
    delay(250);
    AudioNoInterrupts();
    env.noteOff();
    out.noteOff();
    AudioInterrupts();
    delay(500);
    
    /*
    sinFM.frequency(map(analogRead(0), 1023, 0, 50, 150));
    env.decay(map(analogRead(1), 1023, 0, 20, 180));
    out.decay(map(analogRead(2), 1023, 0, 50, 200));
    out.hold(map(analogRead(2), 1024, 0, 50, 200));
    */
    sinFM.frequency(70);
    env.decay(90);
    out.decay(125);
    out.hold(125);
}

Edit: listening on good headphones (ATH M50s) direct into the headphone socket on the audio board.
 
Last edited:
I looked at this again, using Nantonos's numbers. Two problems seem to be happening here.

The worst problem is the envelop effect behaves badly if you call noteOff before it's reached the sustain phase. In msg #11, the "out" envelope has a 20 ms attack, followed by 125 ms hold, followed by 125 ms decay. So it takes 270 ms, plus perhaps up to 3 ms for the library update, to reach the sustain level.

The noteOff code is written with the assumption sustain has already been reached. For now, this is simply a limitation of the software. I'll improve this in a future version, but this not the sort of simple bug that can be fixed quickly and easily. It's a design limitation in the code that's going to take significant work to improve.

In this particular case, the sustain level is very low, only 0.05. The decay phase has to transition from 1.0 to 0.05 in 125 ms. That a rate of gain change of 0.0076 per millisecond. When the noteOff call arrives, it's only had 105 of the expected 125 ms to reduce the gain. So that that point, the gain is still 0.202. Because noteOff isn't designed to handle this case, it has the effect of immediately changing the gain from 0.202 to 0.050. If the signal is near the crest of the sine wave, this sudden change in signal level is heard as a click.

The noteOff function should really check whether the envelope is still in the delay, attack, hold or decay phase. It doesn't, and it uses a simple algorithm that assumes the gain is already at the sustain level. I'm not entirely sure what it should do in those cases, but suddenly jumping the gain to the sustain level is certainly not good.


The other less severe problem is the known issue of numerical precision problems beyond 50 ms. The documentation mentions possible trouble beyond 50 ms settings. The effect of these problems can vary, but it's worst in the case of a long decay phase targeting a very low sustain level. The cumulative round-off errors during the decay phase cause it to slightly "miss" the sustain level. The result is a sudden change in gain (a small click), because the decay phase didn't end at exactly the same gain as the sustain phase implements. The amount of error depends mostly on the length of time. If the sustain level is very low, the perceived effect is to amplify the error (the heard click if listening critically), because it's larger relative to the low gain of the sustain level.

This numerical precision and limitation on lengths of time has been discussed before. It's also a very difficult thing to improve, requiring a substantial redesign of the timing critical code within the envelope object.


I can't really work on either of these issues at this time. They're both going to require a lot of work. Hopefully this explanation at least helps?

For now, you'll have to make sure you delay long enough that the envelope has completed its decay phase before you call noteOff.

Regardless of when you call noteOff, if you use a long decay and low sustain level, you can expect some small clicks with this 1.0 version of the library.
 
Last edited:
Nice analysis. I raised an issue on github to track it, pointing back to your post
https://github.com/PaulStoffregen/Audio/issues/108

I now recall seeing discussion on (analog) envelope generators about retrigger behaviour (where you get a new noteOn before the attack and decay phases are complete), there are various approaches and some are better than others. Seems to be a similar issue, I will dig into that.

It seems to me that in this situation, the decay Edit: I meant release phase should proceed from the actual current level, not from the sustain level. Or rather, it should always proceed from the actual current level (which, in the sustain phase, happens to be the sustain level).
 
Last edited:
It seems to me that in this situation, the decay phase should proceed from the actual current level, not from the sustain level.

Did you mean to say the release phase should proceed from the actual current level? Decay always begins from 1.0. Calling noteOff begins the release phase.

Any research on what the best behavior really should be in the early noteOff cases would really help later this year when I work on the audio library again.
 
Another possible issue is what to do it noteOn is called during any of the phases while the gain is more than zero.

Today, the gain is immediately set to zero, then the delay or attach phase begins. The code has a simple design, where attack always transitions from 0 to 1.0, regardless of the prior state. That's probably also far from the best behavior.
 
I think I can see how to fix the sudden level jumps and it's not too hard. In effect_envelope.h the state of an envelope is maintained in the private part of the class definition:
Code:
	// state
	uint8_t  state;  // idle, delay, attack, hold, decay, sustain, release
	uint16_t count;  // how much time remains in this state, in 8 sample units
	int32_t  mult;   // attenuation, 0=off, 0x10000=unity gain
where the values for state are
Code:
#define STATE_IDLE	0
#define STATE_DELAY	1
#define STATE_ATTACK	2
#define STATE_HOLD	3
#define STATE_DECAY	4
#define STATE_SUSTAIN	5
#define STATE_RELEASE	6
and mult is a 17-bit value :) ranging from 0 to 65536. In other words its almost a 16-bit value, but preserves the ability to set the gain to exactly 0.0 and exactly 1.0.

During update(), the sustain level is set when the decay phase ends:
Code:
else if (state == STATE_DECAY) {
				state = STATE_SUSTAIN;
				count = 0xFFFF;
				mult = sustain_mult;
				inc = 0;
			}

Turning now to the noteOn and noteOff functions, noteOn incorrectly assumes that it is only called when state is idle and sets mult to zero. If that assumption is incorrect, you will get a click as the level abruptly drops to zero. (That hasn't been reported as a bug yet, but it will be as soon as someone tries retriggering envelopes that are still going).
Code:
void AudioEffectEnvelope::noteOn(void)
{
	__disable_irq();
	mult = 0;
	count = delay_count;
	if (count > 0) {
		state = STATE_DELAY;
		inc = 0;
	} else {
		state = STATE_ATTACK;
		count = attack_count;
		inc = (0x10000 / count) >> 3;
	}
	__enable_irq();
}

noteOff assumes that it is called when state is sustain, but then needlessly sets mult to the sustain level anyway. If the assumption is correct, this does nothing since that was the value it had before. If the assumption is incorrect, this gives a click.
Code:
void AudioEffectEnvelope::noteOff(void)
{
	__disable_irq();
	state = STATE_RELEASE;
	count = release_count;
	mult = sustain_mult;
	inc = (-mult / ((int32_t)count << 3));
	__enable_irq();
}

In both cases I think the fix is to not set mult in these functions but to leave it at the current level. I'm going to test that out and, if it works well, send a pull request. I'm going to defer the precision loss on longer envelopes issue to a later pull request, to keep the changes small and focussed.
 
Just to clarify guys, I've been experiencing all of these issues, but it's not been a major concern for me so far because I knew the Audio library is still under pretty heavy development. It's very stable and this is more than enough for me for now!

but yes, with finger drumming on my buttons I often get some funny sound artefacts, sometimes cliks, but also some "squidge" sounds and background noise as the envelope decays. It might be worth me recording a little video for us all to clap our ears around the challenge. The moment the envelope ends the noise goes away.

As soon as I'm done playing with Teensy-LC I'll happily test any pull requests, i've shelved my main project since it arrived. I'm ready to roll out the fix straight to a punishing test environment, can do you a before and after video if that would help. Just need to do it in the daytime if i use the speakers though - my office is next door to the twins bedroom :)
 
Did you mean to say the release phase should proceed from the actual current level? Decay always begins from 1.0. Calling noteOff begins the release phase.

Any research on what the best behavior really should be in the early noteOff cases would really help later this year when I work on the audio library again.

Yes exactly that is what I meant. Sorry. Release phase.
 
In both cases I think the fix is to not set mult in these functions but to leave it at the current level.

The tricky part is getting all 3 state variables computed correctly under all circumstances. If you leave mult untouched, you have to somehow compute the increment and count to compensate. It's much easier said than done.
 
but yes, with finger drumming on my buttons I often get some funny sound artefacts, sometimes cliks, but also some "squidge" sounds and background noise as the envelope decays.

If you can turn any of these into reproducible test cases, I will investigate and work on improvements later this year.

Perhaps you could create an array to store a history of the last several dozen parameters you used on various audio library calls. Maybe add a pushbutton that prints the array to the serial monitor or write it to the SD card or eeeprom? When you hear an artifact, press the button. Then you'll have a short list of numbers to try plugging into the code as fixed constants to reliably reproduce the artifact.

Yeah, I know that's a lot of trouble. But I do investigate this stuff, and later this year I will be making a lot of improvements to the library.
 
It shouldn't be too hard, As far as audio my box isn't all that complicated yet, I've stripped out all the effects and such to focus on sequencing and sample playback (arguably the important bits :) ). In fact its just a few mixers and play RAW from flash through envelopes. So yeah, no problem - as soon as I put the Teensy-LC down i'll try and get some recordings and settings.
 
Hi,

I found this thread today because I've been building up a synthesizer in teensy audio, and when playing with the envelope in teensy audio i'm definitely hearing a click at the end of the note when any of my timing parameters are over 50ms.

If there has been a new release of the library since this thread was active, maybe I don't have it installed? Not sure how to check that in Arduino.

-mykle-
 
Okay, did that!

Here's my impressions:

In my test synth I have midi knobs knobs hooked up to modify the ADSR params of the envelope object, so I can adjust them as I go, and a fifth knob to adjust the maximum value that the other knobs' midi values are scaled to. https://github.com/myklemykle/bleep/blob/master/bleep.ino This is running on Teensy 3.2 + paul's Audio board, into Sony headphones.

With this I'm testing at 50ms, 200ms, 1000ms, 2000ms and 5000ms, using all four waveform types.

This change is definitely an improvement. The clicky noises are gone, and I'm able to set long envelope times, which is a big win.

I found three issues of debatable significance:

* After the change, I did briefly experienced a bug: a distinct click when transitioning from an attack of 1000ms to a decay of 1000ms. I had it distinctly on every note. But then I twiddled the attack & decay values down & up again, and it went away. It wasn't a hard click, more like a momentary gain error. But I can't reproduce it reliably. What state (if any) persists in the envelope between triggers, that could have caused this?

* Odd audio artifacts appear when manipulating sustain value during an envelope pass. (It doesn't appear possible to manipulate the other three values, alas.)

* The most noteworth thing is that with the decay set very close to 0 & the sustain set long, I hear what sounds like a slight increase in volume, oror else perhaps a momentary silence, when transitioning from the sustain level to the release slope. Possibly this is an audio illusion? When I switched back to unaltered code I could hear the same phenomenon (plus clicks), so I don't think it's introduced by the change.
 
Last edited:
Different Approach to Envelope (introduce the possibility to add portamento)

Hi guys,
I added the envelope generator to my project with the latest Paul's new Audio dist (from 11/15), for some reason I had a hard time to get it to work.
I have found a great example from another member here (Thank you Mykle!), except that for the Sustain i had to use another trick.

I am posting for others to get up and running more quickly.
Paul, maybe this could be a good (?) practical example for using the Envelope object.

I'd really like to have a longer "Release", I will look at this later (I understand some of you are looking at this).

Thank you all!

//vvvvvvvvvvvvvvvvvvvvvvvvvvvvv
// Begin borrowed code from Mykle
//

static int maxEnvelopeTime = 6000; // ms

// How to turn a midi CC integer 0-127 into a arbitrarily large number of miliseconds?
// let's have it be exponential between 0 and some higest value.
int ccToMs(byte midival, int maxMs){
return maxMs * midival * midival / 16129; // 127^2
}


// ^^^^^^^^^^^^^
// End Borrowed Code
//^^^^^^^^^^^^^^^^^^^^^

// my MIDI implementation

......

case 73: // attack
env_attack = ccToMs(d2, maxEnvelopeTime);
envelope1.attack(env_attack);
Serial.println(String("attack ") + env_attack);
break;

case 75: // decay
env_decay = ccToMs(d2, maxEnvelopeTime);
envelope1.decay(env_decay);
Serial.println(String("decay ") + env_decay);
break;

case 79: // sustain
env_sustain = d2 / 127.0; // NOTE: had to trick div into a float by using the '.' . Otherwise the value was going to be 0 most of the time
envelope1.sustain(env_sustain);
Serial.println(String("Sustain ") + env_sustain);
break;

case 72: // release
env_release = ccToMs(d2, maxEnvelopeTime);
envelope1.release(env_release);
Serial.println(String("Release ") + env_release);
break;
 
Back
Top