DEV Community

Cover image for Why Your Generated Tone Clicks, and How an Envelope Fixes It
Michelle Wiginton
Michelle Wiginton

Posted on

Why Your Generated Tone Clicks, and How an Envelope Fixes It

If you have generated a pure tone in code and played it back, you may have noticed a small click at the start, the end, or both. The tone itself is clean, but the edges are not. That click is not a bug in your sine wave. It is a real and well understood artifact, and the fix is a technique you will reuse in every sound you ever synthesize: an envelope.

This piece builds directly on generating a basic tone from scratch. We take a tone that clicks, look at the actual sample values to see why, and apply an envelope to smooth it. Everything is plain C++ with no libraries, and every number here is captured from a real run of the code.

Where the click comes from

A tone is a list of samples tracing a sine wave. A speaker turns those samples into sound by physically moving: the sample value sets the position of the speaker cone at each instant, where 0 is its resting position and larger values push it further forward or pull it back. Playing the tone moves the cone in and out 44,100 times a second to recreate the wave.

When playback starts, the cone is at rest, at position 0. But the first sample of the tone is usually not 0. It is wherever the wave happens to be at that instant, and if that value is far from zero, the cone has to move from rest to that position in a single sample step, about 22 microseconds at this sample rate.

That near instant movement is the click. A cone moving gradually pushes the air smoothly and produces a smooth sound. A cone forced to a distant position in one sample makes a sharp, abrupt movement of the air, which your ear hears as a click or pop.

You can see it directly in the numbers. Here are the first six samples of a plain 440 Hz tone at half amplitude:

n=0   raw=0
n=1   raw=1026
n=2   raw=2048
n=3   raw=3063
n=4   raw=4065
n=5   raw=5051
Enter fullscreen mode Exit fullscreen mode

The wave leaves zero and climbs fast. Between the silence before playback and sample 1, the signal jumps by 1026 in one step. The same thing happens at the end: if the tone stops while the wave is partway through a cycle, the signal drops from some large value straight back to silence, another instant jump, another click. In the raw tone here, the last three samples are:

-3063, -2048, -1026
Enter fullscreen mode Exit fullscreen mode

It stops mid wave, well away from zero. That final leap to silence is the ending click.

The fix: multiply by an envelope

The cure is to make the sound start at zero volume and rise to full over a short time, then fall back to zero before it ends. Instead of the wave beginning and ending at full strength, it eases in and eases out. That gentle ramp is called an envelope.

An envelope is just a second number, between 0.0 and 1.0, that you multiply each sample by. At the very start the envelope is 0.0, so the output is silent no matter what the wave is doing. Over the next few milliseconds it rises to 1.0, letting the wave reach full volume. It holds at 1.0 through the middle, then falls back to 0.0 over the final stretch.

The rising part is called the attack, the falling part the release. Here that is a 10 millisecond attack and a 50 millisecond release. In samples, at 44,100 samples per second:

Attack ramp:  441 samples  (10 ms)
Release ramp: 2205 samples  (50 ms)
Enter fullscreen mode Exit fullscreen mode

The code is the tone loop from the previous article with one extra factor. For each sample you compute the wave as usual, then compute a gain based on where you are in the tone, and multiply:

const int attack  = static_cast<int>(0.01 * sampleRate);  // 10 ms
const int release = static_cast<int>(0.05 * sampleRate);  // 50 ms

for (int n = 0; n < total; ++n) {
    double t = static_cast<double>(n) / sampleRate;
    double wave = amplitude * std::sin(2.0 * M_PI * frequency * t);

    double gain = 1.0;
    if (n < attack)                gain = static_cast<double>(n) / attack;
    else if (n > total - release)  gain = static_cast<double>(total - n) / release;

    samples[n] = static_cast<int16_t>(wave * gain * 32767);
}
Enter fullscreen mode Exit fullscreen mode

The two conditions are the whole idea. In the attack region, n / attack runs from 0.0 up to 1.0 as n climbs. In the release region, (total - n) / release runs from 1.0 back down to 0.0 as you approach the end. Everywhere in between, gain stays at 1.0 and the wave passes through untouched.

What the numbers look like now

Run the enveloped version and compare the first six samples to the raw ones:

n=0   raw=0      env=0
n=1   raw=1026   env=2
n=2   raw=2048   env=9
n=3   raw=3063   env=20
n=4   raw=4065   env=36
n=5   raw=5051   env=57
Enter fullscreen mode Exit fullscreen mode

The raw signal jumps to 1026 immediately. The enveloped signal creeps up: 2, 9, 20, 36, 57. There is no sudden leap, so there is no click. The jump between the silence before playback and the first real sample has gone from 1026 down to 2.

The end behaves the same way. Where the raw tone stopped mid-wave at -1026, the enveloped tone fades to nothing:

last 3 samples:  -4, -1, 0
Enter fullscreen mode Exit fullscreen mode

It arrives at silence gently instead of falling off a cliff.

Two things worth confirming, because a fix that changes the sound in other ways would be a poor fix. The pitch is untouched: a frequency analysis of the enveloped tone still reads a dominant frequency of 440.0 Hz. And the loudness is preserved where it matters: the peak sample value in the sustained middle is still 16383, exactly the half amplitude we asked for. The envelope only touches the edges. The body of the tone is identical.

Why this matters beyond the click

Removing the click is the immediate payoff, but the envelope is a bigger idea than that. The shape of a sound's volume over time is a large part of what makes it recognizable. A plucked string jumps to full volume instantly and decays slowly. An organ note comes up gently and holds flat until you release the key. A drum is almost all attack and a fast decay with no sustain at all. Same possible pitches, completely different instruments, and much of the difference is the envelope.

The attack and release here are the two simplest pieces of that. The classic full version adds two more segments, a decay right after the attack and a sustain level to hold during the note, giving the four part shape known as ADSR (attack, decay, sustain, release). Everything in this article is the A and R of that; adding D and S is a natural next step.

Takeaways

  1. A click happens when the signal jumps instantly between silence and a large value, at the start or the end of a tone.
  2. An envelope is a gain from 0.0 to 1.0 that you multiply each sample by, easing the sound in and out.
  3. The attack is the fade in, the release is the fade out. Both are short ramps measured in milliseconds.
  4. It is one extra factor in the sample loop, and it leaves pitch and peak loudness untouched, changing only the edges.
  5. The shape of that gain over time is a major part of what distinguishes one instrument from another, which is why envelopes matter far beyond fixing a click.

The full program is a small extension of the basic tone generator and runs with nothing but a C++ compiler.

Top comments (0)