Envelope generators—ADSR code

First, a brief example of how to use the ADSR code:

// create ADSR env
ADSR *env = new ADSR();

// initialize settings
env->setAttackRate(.1 * sampleRate);  // .1 second
env->setDecayRate(.3 * sampleRate);
env->setReleaseRate(5 * sampleRate);
// at some point, by MIDI perhaps, the envelope is gated "on"
// and some time later, it's gated "off"

In your “real-time” thread, where you fill audio buffers for output, you’d use the process command to generate and return the next ADSR output:

// env->process() to generate and return the ADSR output...
outBuf[idx] = filter->process(osc->getOutput()) * env->process();


Create and destroy:


Call the process function at the control rate (which might be once per sample period, or a lower rate if you’ve implemented a lower control sampling rate); it returns the current envelope output. The getOutput function returns the current output only, for convenience:

float process(void);
float getOutput(void);

Set the gate state on (true) or off (false):

void gate(int on);

Set the Attack, Decay, and Release rates (in samples—you can multiply time in seconds by the sample rate), and the sustain level (0.0-1.0); call these when settings change, not at the sample rate:

void setAttackRate(float rate);
void setDecayRate(float rate);
void setReleaseRate(float rate);
void setSustainLevel(float level);

Adjust the curves of the Attack, or Decay and Release segments, from the initial default values (small number such as 0.0001 to 0.01 for mostly-exponential, large numbers like 100 for virtually linear):

void setTargetRatioA(float targetRatio);
void setTargetRatioDR(float targetRatio);

Reset the envelope generator:

void reset(void);

It may be useful to know what state an envelope generator is in, for sophisticated control applications (typically, though, you won’t need this); a state enum is defined for convenience (for example, ADSR::env_sustain is equivalent to the value 3, and indicates that the current state is Sustain):

int getState(void);
    env_idle = 0, env_attack, env_decay, env_sustain, env_release


Calculations that are done in the “real time” thread are written as inline code in the header file. Calculations that are done asynchronously, at “setup time” (which include patch recall and “knob twiddling”), are in the cpp file functions; this includes sub-calculations of the real-time math that are dependent only on setup-time parameters.

Download ADSR source code

This entry was posted in Envelope Generators, Source Code, Synthesizers. Bookmark the permalink.

49 Responses to Envelope generators—ADSR code

  1. Mark Heath says:

    thanks for sharing this code, I’ve tried making my own ADSR envelope generators once before, but this one is nicer.

  2. Hi Nigel,

    this is an interesting articles series. Allow me to add a few remarks:

    While your ADSR approach looks fine on “paper” (or better: It looks fine on the screen), it can fail miserably is practice. Let me explain in a technical manner to keep it short. The problem is that your branches are not bandlimited. These corners create infinitely highly frequency content which – in a discrete environment – will simply alias (in this case: “mirror”) back to lower frequencies.

    What does it mean? It mean that the pretty lines you are “drawing” in your the app above only look so well because they have been build to look fine in your specific case. Your example does not show the true continuous waveform the ADSR module is really meant to generate. In some words, you cut half of the sampling theorem and expect useful results. This doesn’t work as expected, to say the least. 🙂

    In reality, the heavy aliasing your approach produces will become visible in the form of wild oscillations and overshoots, instead of the nice lines your example seems to produce. Upsampling helps to makes to make visible, but measuring the analogue output with an oscilloscope is even better.

    In a real world application, you’ll want to get rid of the “corners”, i.e. you’ll want to band-limit the algorithm somehow. 🙂

    • Nigel Redmon says:

      Hi Fabien,

      Thanks for your comments, but you are mistaken here. The envelopes are entirely synchronous with the sample rate, so there is no aliasing. The mistake you’re making is in thinking that sharp corners equates with aliasing. Sharp corners that fall in between sample points and move around producing aliasing. It’s the modulation that’s perceived as aliasing, not the corners.

      For example, listen to the 5-bit version of the digital waveform here. It doesn’t alias, despite toggling between positive and negative maximums, because it’s synchronous with the sample rate. (Some might argue that it really does alias, but the alias components are in exact harmonic relations and therefore are equivalent to harmonic distortion. That’s not true in this case, however. It’s a digital wave that “is what it is”.)

      Even if the envelopes were not synchronous with the sample rate, the error would be transient and not perceived as aliasing, unless you cycled the envelope at audio rate. But because it’s synchronous, even turning the envelope into an oscillator wouldn’t result in aliasing, unless you terminated the envelope early and re-triggered with a varying period (it’s not possible to trig between samples). The only reason to do that would be to attempt a crude approximation of a ADSR waveshaper (crude because it can’t be triggered at fractional sample times). This is an ADSR envelope generator, not an oscillator waveshaper. And most of the analog ADSRs it emulates can’t serve as an oscillator either.


  3. Michael Kraft says:

    Hi Nigel,

    just a question. I havn´t tried yet to use your ADSR, but I think it has an Attack curve that is rising inverse exponentially. That would mean, it rises first very quick and slows down its rising-rate as it approaches its upper limit. If I am not mistaken, that should be the other way round, rising first slowly and accelerate its rising speed as it progresses. That is e.g. how a fade-in has to proceed to sound naturally.

    Am I wrong?

    The decay and release curves instead are perfect as they are (descending first fast and then slower).

    • Nigel Redmon says:

      Hi Michael,

      I do address this in the articles, and also in the video (check that first, probably the easiest to understand with the graphics). Your thought process is correct if you are thinking that for an audio ramp (fade in), you’d want an exponential rise. But instruments attacks don’t work that way—nothing I can think of in nature has an attack that starts slowly and increases exponentially. A linear attack is probably the most useful overall, and hardware envelope generators approximated a linear attack cheaply with a truncated reverse exponential. If you do want a constant (linear in dB) ramp using a modular analog synth, you’d use a linear ramp generator and an exponential VCA. I considered adding adding exponentially rising attacks to the ADSR, but I didn’t want to get far from emulating typical analog hardware. And in practical use, such a curve is used rarely.


  4. Bart Bralski says:

    Hi there,

    First off all thanx for all the great info and the nicely documented code!

    I have converted the ADSR-code (with a bit of help from a C++ to Java converter) and reworked the output and optimized the code to work with Processing.

    Find the ADSR processing sketch here.

    I’m calling the code from a method :

    void generateADSR() {
    ADSR adsr1 = new ADSR();

    adsr1.setAttackRate(12); // attackTime in ticks... // use adsr1.setAttackRate(12 * samplerate)

    int noteLength = 75;
    int[] attackDecaySustain = new int[noteLength];
    int[] release = new int[outputArray.length-noteLength];

    for (int i = 0; i < noteLength; ++i) {
    attackDecaySustain[i] = int(adsr1.getOutput()*(175 + 0.5)-80); //175 is the amplitude, -80 is an offset value
    for (int i = 0; i < outputArray.length-noteLength; ++i) {
    outputArray = concat(attackDecaySustain, release);

    It’s not realtime… but I use it to render an envelope I use as a sort of lookup table.
    I hope somebody will find this helpfull.

  5. Peter Janson says:

    Very nice article. Thank you!

  6. Mark Miles says:

    Thanks for this code. I’m looking forward to testing it, but I have a doubt: when you apply the envelope to a sampled percussive sound such as a piano, a guitar or a drum, which is already sampled with its own natural decay, you don’t want to add further decay, resulting in shortening the decay in an unnatural way. For this reason an inverse exponential decay is applied. How can I achieve this with your code? It would also be interesting to add a ‘hold’ state between attack and decay, in case you have to apply envelope to a percussive sample with looped tail.

  7. Nigel Redmon says:

    I don’t understand about inverse exponential…that would make a very abrupt cutoff, and would sound very unnatural. I believe that exponential decays are applied to the exponential reverb tails in convolution reverbs to adjust the tails, for instance.

    Yes, “hold”, “delay”, and “cycle” would be useful states.

  8. Daniel Doubleday says:

    Thanks for sharing Nigel!

    I have one question: Don’t you need to re-calcCoef when changing the target ratios?
    For instance for attack:

    void ADSR::setAttackRate(float rate) {
    attackRate = rate;
    attackCoef = calcCoef(rate, targetRatioA);
    attackBase = (1.0 + targetRatioA) * (1.0 – attackCoef);

    void ADSR::setTargetRatioA(float targetRatio) {
    if (targetRatio < 0.000000001)
    targetRatio = 0.000000001; // -180 dB
    targetRatioA = targetRatio;

    • Nigel Redmon says:

      Yes, Daniel, you are correct. I did a late optimization to pull some partial calculations outside of the “process” function (AttackBase, etc.). That created some new dependancies, and I didn’t catch all of them (interestingly, the javascript version, translated from the c++ code, that is used in the adsr widget accounts for them, so maybe I posted a non-final version of the c++ code inadvertently). At the moment, I’m wishing I didn’t optimized quite that much (faster process function, at the expense of tutorial clarity), but I’ll update the code when I get a chance. Thanks!


      PS—The old code does work correctly when the target ratios are set first. That’s the normal/correct way to use the ADSR (initialize the ADSR with the curve settings you want, or use the defaults, then control the A, D, S, R settings), so it’s unlikely people got snagged with the old code, fortunately.

      PPS—Code has been updated.

  9. Pete says:


    I really appreciate that you’ve made this lucid material available – explanation and code.

    I’ve been adapting this to a scenario where I need to guarantee the times for each state are sample-accurate, so iterating based on a number of samples rather than relying on the value to trip the next state. I may have found a bug! 🙂

    Where you calculate the coefficient as:

    exp(-log((1.0 + targetRatio) / targetRatio) / rate);

    I found I get better results for the decay and release states if I calculate it as

    return exp(-log((distance + targetRatio) / targetRatio) / rate);

    where “distance” is the absolute difference between the start and end values of the particular state – i.e. 1.0 – sustainLevel for decay state and simply sustainLevel for the release state.

    After this change, I find the number of steps taken for a stage’s output value to reach its terminus is much closer to the “rate” param – and also I can use arbitrary values outside the range 0.0 – 1.0 for the start and end values of states.

    I freely admit that I don’t know what I’m doing here – thus very much in need of your tutorials (hence my gratitude for that!) – and so I may be confused. Any clarification welcome. I’ll post a link to a paste of my test code in a separate comment, in case any filters don’t like it 🙂

    • Nigel Redmon says:

      Hi Pete—There’s nothing wrong with doing the rate based on distance. However, I was going for how hardware ADSRs usually work. In that case, changing the sustain level does not change the rate, as it would with your method. In hardware, the decay and release knobs are changing a resistance and therefore the bleed rate of a capacitor. So, changing the sustain level wouldn’t affect that rate. In fact, that’s why I called it rate (SetDecayRate, etc.) and not time, even though you can set them in number of samples (per full range move), which is time.

      • Pete says:

        Thanks for the explanation – that makes a lot of sense!

        As you’re probably aware, your work was used in a DSP book – “Designing Software Synthesizer Plug-Ins in C++: For RackAFX, VST3, and Audio”, which I found in google books while investigating exponential decay. But there (e.g. p.299) , the author’s functions look like they’re setting a time in ms

        “double dSamples = m_dSampleRate*(m_dReleaseTime_mSec/1000.0);”

        But then dSamples goes on to play the exact role played by your “rate” param in setting the coefficient and base (which is named “offset” in the book.) This is what misled me into thinking the rate param was intended as “time to complete a given state (in samples.)” (So I guess I should be posting on the author’s blog and not yours!)

        So anyway, if I understand you correctly, typical hardware behaviour would be for the decay and release times to change as the sustain level was adjusted? That’s very interesting in itself!

        • Nigel Redmon says:

          Well, I could have just said something totally wrong (about analog synths usually having decay rate unaffected by sustain level)—sorry, a bit overwhelmed at this time. But I can say with confidence that I chose constant rate and not time because I didn’t want to recalculate or have the rate change while the user was turning the sustain level. There’s nothing inherently wrong with either way, just not sure off the top of my head which way analog synths typically did it at the moment!

          And I just found out about Will Pirkle referencing me in his book—cool! I own it, have been too busy to read it.

          • Pete says:

            “because I didn’t want to recalculate or have the rate change while the user was turning the sustain level”

            – insert sound of lightbulb going on –


          • Will Pirkle says:

            Have a look at the MIDI Manufacturer’s Association DLS Level 1 (or better) Level 2 Specs for a software synth which is very much based off of the typical hardware synth architecture. You can get the Level 1 Spec here:


            It violates the license for me to re-publish the statements about the EG here, but you can read them in the spec. The Level 2 spec is sightly more detailed in its description on how the sustain level affects the *actual* decay/release times, and that the times the user sets with the controls are really full-scale-to-zero times, not the times that the state machine stays in the decay or release state. I would advise trying to track down the L2 spec if possible.

            All the best,

          • Nigel Redmon says:

            PS—Yes, I think my original assertion is correct. If I had a functioning o-scope, I’d just hook my old Aries AR-312 ADSR (basically what I was modeling) to it and give times, but a glance at the schematic looks like it’s constant rate. I did a real crude and quick check with Diva, which is supposed to be a faithful Minimoog envelope, and it’s pretty clear that the knob is controlling rate. For instance, recording noise through the VCA with a moderate Decay, and looking at the final release with Sustain full and again at one-third, the release decay from full is clearly substantially longer than the release from the lower sustain level.

  10. Pete says:

    Some test code related to the above post is at a .com site where bins can be pasted, raw/HkPcny6x

  11. Sean says:

    Thank you Nigel for the clear ADSR implementation overview.

    I’m a little late to the party but nonetheless I have one question about the code:

    In the .cpp, in the constructor, the attack, decay and release rates are all set to 0 (lines 27 – 29). Wouldn’t this cause a division by 0 in the subsequent calculations of the coefficients (line 57)? Doesn’t this cause issues at runtime?

    Thanks again for this blog, it’s a very useful resource.

    • Nigel Redmon says:

      Sean, you may be late to the party, but you brought something! Yes…the code works if the C++ implementation follows the IEEE standard, but C++ is not bound to. That is, -log(…)/0 results in -inf, and exp(-inf) = 0—the IEE standard guarantees it, but C++ doesn’t (works fine in Xcode/LLVM, for instance, but who knows what it won’t work in). I’ll rev the code to make sure it works in all C++. Thanks!


  12. JD says:

    Hi Nigel,
    Thanks for making this available. I am implementing your logic/math in HDL for an FPGA.

    I have a dumb question:
    In the decay and release states, how exactly is the output value supposed to decrease?
    In the decay case, I see:

    output = decayBase + output * decayCoef;

    Are these not all positive operands? How can this decay the envelope? Same question for the release case.

    • Nigel Redmon says:

      Yes, thinking about the math can be confounding at times. Don’t think of RC decay as subtracting. Think of it as being a percentage—always less than 100%—of what it was the last instant. So, output = .9 * output. Except in this case we’re not heading to zero but a higher level for sustain, so we need to add than back in.

      • JD says:

        Thank you. I was able to get my implementation working.
        Wonder if there is a name for this type of math where the output depends on previous outputs.

  13. Makabongwe says:

    Hi there Nigel. I’m working on a synthesizer and I found your code to be very clear and understandable. I’m using a microcontroller to test the code, I have a function that generates a sound wave and I’m not sure how to include your idea in my code so that I get ADSR behavior. How do I test your idea with a sound I have generated?

    • Nigel Redmon says:

      Just create a new envelope, set it, then multiply your signal samples by the “process” output; somewhere, you are triggering the ADSR, of course:

      // initialization
          // create
          ADSR *env = new ADSR();
          // set
          env->setAttackRate(.1 * sampleRate);
          env->setDecayRate(.3 * sampleRate);
          env->setReleaseRate(5 * sampleRate);
      // control: start/end envelope, change envelope params...
          // start envelope
      // audio processing thread
          // for each sample
          out = in * env->process());
  14. MIDIculous says:

    I’m having an issue with the existing code: It doesn’t behave correctly when you trigger a new note while another note is still fading out.

  15. Tim says:

    Why can the decay and release ratios only to be set as the same? Is there some advantage in that, or disadvantage or no point in having them separate?

    • Nigel Redmon says:

      It’s simply a design choice—most often, if you have a reason for one to be linear, you want the other to be linear too for the same reason. I’m usually shooting for enough simplicity that the main concepts are easy to understand, so I’m not trying to cover everything (cycling, different kinds of triggering, hold and delay controls). In this case, it would have been simpler yet to hard-code the curves, just as hardware ADSRs do, but I liked the idea of this feature.

  16. Tim says:

    if I understand this correctly, assuming the attack rate, decay rate and release rate are zero, and the sustain level is 1.0. There should be no envelope and the maximum level will be almost instantly reached when gated on, and then almost instantly reduced to zero when gated off. Is that correct?

  17. Tim says:

    It seems like the log section does not work so well, practically speaking. It does rise or drop very quickly to a point close to maximum or minimum, and then slowly increases or dissipates from there as expected. But the level is already so close to maximum or minimum after the initial quick rise, that I can’t hear any difference from that point onwards. What do you think about that?

    • Tim says:

      actually, I can just hear it, and see the difference on the oscilloscope, but what happens is that the last part of the release is so low in amplitude that effectively there is a what seems like a delay between the end of the release and the retrigger, when the amplitude is so low as to not be noticed.

  18. Tim says:

    Can you help me with this? I am using 24 bit floats and they are being overrun at long attack, decay and release ratio’s (10 seconds). 32 bit float is ok, but I don’t why to change them all due to memory requirements. Do you know which variable or variables would be overrun?

    • Nigel Redmon says:

      I don’t know what you mean by 24 bit floats—I don’t know what the mantissa and exponent limitations are. (Do you mean 24-bit fixed point?) But besides that, it’s an iterative calculation, so the time doesn’t matter. However, the longer the time, the closer the multiplier is to 1. So without looking at the code, it’s possible you’re having a quantization problem. But from your description I don’t really understand what you’re seeing, so I’m just throwing that out there.

      • Tim says:

        hi, I mean a 24 bit floating point number, as in “float releaseRate;”
        one sign bit, 8 exponent bits, 15 mantissa bits.
        32 bit float is same but 23 mantissa bits. Hmmm a quantization problem.

  19. Tim says:

    Hi, I am using the code slightly modified, to create an automatic ADSR envelope. The code itself gates the waveform on and also gates it into the release state. But I am having problems with attack, decay and release rates less than about 50mS. There are a lot of gaps in the output. For example, an attack rate of 50mS and a release rate of 50mS, with a sustain level of 0, and no sustain time, produces a quick triangle type up/down waveform as expected, and I can hear it going, dit, dit, dit, dit, dit, as it quickly turns on and off. But when the rates are lowered to say 30mS, its more like dit, dit, dit……. dit, dit, dit ……. dit, dit, dit.
    As if something cannot keep up the pace.

    I wonder if it is a result of my system with its 10mS sample rate, or something else.

    • Nigel Redmon says:

      I’m not sure how interpret that, whether you’re triggering the ADSR as you intend, but look at ADSR::gate. The logic is pretty simple, you are put in the attack state (or remain in the attack state) when called with gate=true. You can get more elaborate with how you want to trigger by also looking at ADSR::getState, perhaps calling ADSR::reset if you want to force retriggering from zero before gating on.

  20. Tim says:

    I am wondering how the following is usually done. The attack rate is the time taken to get from min to max. Assuming the sustain level is zero, then decay rate is the time taken to get from max to min. However, if the sustain level is say half, then the decay rate only takes half the time to get to the sustain level.

    That means that when a user is entering data to control the envelope, the decay time cannot be a direct number like the attack rate but instead has to be divided by the SustainLevel. Similar for the Release rate. Is that the way it is usually done, or do users factor in the proportion when setting the rates.

    • Nigel Redmon says:

      I discuss the distinction between rate and time in the “part 2” article. In hardware ADSRs, the rates are controlled by charging and discharging capacitors. The rate is controlled by the potentiometer directly. That means that at a given setting of the decay knob, the rate of decay does not change if you change the sustain level knob. So, yes, if the sustain level changes, the decay takes less time because the level is closer, the rate unchanged.

  21. Tim says:

    How can the envelope be inverted? So the attack is a fall rather than a rise, and then rise rather than a decay, sustain is the same, and then a release to rise.

    how can it be reversed?

    and what is the effect of an envelope shaping another envelope?

    • Nigel Redmon says:

      Not 100% sure this is what you’re after, but 1 – output gives you that. Or simply -output if you want to start and end at zero.

    • jakob skouborg (Jaffasplaffa) says:

      Tim, do you mean inverting the envelopes function, so the sound is actually on when not holding a key and off when hoding a key?

      I think you can do it by multiplying the envelope output by its own range in minus and then plus 1 to get it back in range.

      So if the envelope range is 1:

      (Envelope out * -1) +1

      This will give you an envelope that is ON all the time and when you hit the keys it will shot the sound off.

  22. Lakshit Goel says:

    How to I create a ADSR generator sequencer in ARM assembly on my discoboard? Please help like how to code that?

  23. MikeDB says:

    Hi Nigel
    You make an interesting point

    Have you done any work on what difference to the sound a lower control sampling rate for ADSRs makes in digital synths ?

    Obviously at some really low rate you would hear the steps, whilst at the audio sampling rate it should be perfect, but I was wondering if there is some lower control sampling rate for ADSRs where the output still sounds the same but you’ve saved a lot of processor overhead.

    Any thoughts ?


    • Nigel Redmon says:

      No, I’ve not done such experiments, but a few thoughts: 1) Reaktor would probably be a good place to experiment, as it has a separate, settable control rate (defaulting to 400 Hz, or 2.5 ms, but with settings ranging from 25 Hz to 3200 Hz). 2) You could do a hybrid approach, where the control path runs at a lower rate, but goes through smoothing filters that run at audio rate. That way, you could have many lower-rate modulators that feed a single smoother for a particular parameter. The catch is that if you want a relatively slow control rate and therefore relatively slow filter, yet require quick changes sometimes (fast attack on envelopes), you might need to rest the filter. IIRC, the Oberheim Xpander had smoothers (passive RC) on the software envelopes, but switches to bypass/reset them for a fast attack. The OB-8 did something like this too, but didn’t have software envelopes.

      You probably don’t need to do #2, but it’s an interesting thought for some cases where control must be slower or rougher than the audio can put up with. For instance, it’s great for smoothing UI panel controls that might not get sampled often enough, or automation that may cause undesirable jumps.

Leave a Reply to Nigel Redmon Cancel reply
