Skip to content

05_07_03_envelope - Envelope Generator Atoms

Overview

Envelope generators shape the amplitude, timbre, and other parameters of sound over time. This subsystem provides three fundamental envelope types used across all audio synthesis and processing:

  1. ADSREnvelope - Classic four-stage envelope (Attack-Decay-Sustain-Release)
  2. AREnvelope - Simple two-stage envelope (Attack-Release) for percussive sounds
  3. EnvelopeFollower - Tracks the amplitude envelope of incoming audio

🎯 Envelope Types

1. ADSREnvelope - The Synthesizer Workhorse

Purpose: The most common envelope in synthesis. Four stages provide complete control over sound evolution.

Stages: - Attack - Rise from 0 to peak (note on) - Decay - Fall from peak to sustain level - Sustain - Hold at sustain level (while key is held) - Release - Fall to zero (note off)

Features: - Exponential and linear curve options - Velocity sensitivity - Retrigger support (legato mode) - Multi-channel processing - PercussiveEnvelope wrapper (preset for drums)

Typical Applications:

// Amplitude envelope for synth pad
ADSREnvelope<float> amp_env;
amp_env.set_attack_time(0.5f);    // Slow attack (pad)
amp_env.set_decay_time(0.2f);
amp_env.set_sustain_level(0.7f);
amp_env.set_release_time(1.0f);   // Long release (smooth)

// Filter envelope for pluck
ADSREnvelope<float> filter_env;
filter_env.set_attack_time(0.01f);   // Fast attack
filter_env.set_decay_time(0.3f);     // Medium decay
filter_env.set_sustain_level(0.3f);  // Low sustain
filter_env.set_release_time(0.1f);


2. AREnvelope - Percussive Simplicity

Purpose: Two-stage envelope for percussive sounds that don't need sustain.

Stages: - Attack - Rise from 0 to peak - Release - Automatically fall to zero (no need for note_off)

Features: - Simpler than ADSR (more CPU-efficient) - Automatic completion (no sustain stage) - trigger() vs retrigger() modes - DrumEnvelope wrapper (ultra-fast attack) - PluckEnvelope wrapper (instant attack, long decay)

Typical Applications:

// Drum synthesis
DrumEnvelope<float> kick_env;
kick_env.set_decay_time(0.15f);   // 150ms decay
kick_env.trigger(1.0f);

// Plucked bass
PluckEnvelope<float> bass_env;
bass_env.set_decay_time(0.8f);    // Long decay
bass_env.trigger(0.9f);

// Hi-hat
AREnvelope<float> hihat_env;
hihat_env.set_attack_time(0.0001f);  // Instant
hihat_env.set_release_time(0.05f);   // Very short
hihat_env.trigger(1.0f);


3. EnvelopeFollower - Audio-to-Control Conversion

Purpose: Tracks the amplitude envelope of an incoming audio signal.

How it works: 1. Rectify input signal (|x[n]|) 2. Apply asymmetric smoothing (fast attack, slow release) 3. Output smooth envelope curve

Features: - Peak or RMS detection modes - Independent attack/release times - CompressorSidechain wrapper (optimized for dynamics) - PeakMeter wrapper (VU-style metering)

Typical Applications:

// Compressor sidechain
CompressorSidechain<float> sidechain;
sidechain.set_attack_time(0.001f);   // 1ms attack
sidechain.set_release_time(0.1f);    // 100ms release

float envelope = sidechain.process_sample(input);
float gain_reduction = compute_compression(envelope, threshold, ratio);
output = input * gain_reduction;

// Auto-wah effect
EnvelopeFollower<float> follower;
follower.set_attack_time(0.01f);
follower.set_release_time(0.2f);

float envelope = follower.process_sample(input);
float cutoff = 200.0f + envelope * 1800.0f;  // 200-2000 Hz
filter.set_cutoff(cutoff);

// Peak meter
PeakMeter<float> meter;
meter.process_sample(input);
float level_db = meter.get_level_db();  // For GUI display


πŸ“š Quick Start Examples

Example 1: Basic ADSR Envelope

#include "adsr_envelope.h"

// Create envelope
ADSREnvelope<float> env;
env.set_sample_rate(44100.0f);
env.set_attack_time(0.01f);    // 10ms
env.set_decay_time(0.1f);      // 100ms
env.set_sustain_level(0.7f);   // 70%
env.set_release_time(0.5f);    // 500ms

// Trigger note
env.note_on(1.0f);  // Full velocity

// Generate samples
for (size_t i = 0; i < buffer_size; ++i) {
    float env_value = env.generate();
    output[i] = oscillator.generate() * env_value;
}

// Release note
env.note_off();

// Continue generating (release stage)
for (size_t i = 0; i < buffer_size; ++i) {
    float env_value = env.generate();
    output[i] = oscillator.generate() * env_value;
}

Example 2: Drum Synthesis with AR Envelope

#include "ar_envelope.h"

// Create drum envelope
DrumEnvelope<float> env;
env.set_sample_rate(44100.0f);
env.set_decay_time(0.2f);  // 200ms decay

// Trigger drum hit
env.trigger(1.0f);

// Generate drum sound (automatically completes)
while (env.is_active()) {
    float env_value = env.generate();
    float osc = sine_oscillator.generate();
    output[i++] = osc * env_value;
}

Example 3: Envelope Follower for Dynamics

#include "envelope_follower.h"

// Create envelope follower
EnvelopeFollower<float> follower;
follower.set_sample_rate(44100.0f);
follower.set_attack_time(0.005f);   // 5ms attack (capture transients)
follower.set_release_time(0.1f);    // 100ms release (smooth)
follower.set_mode(EnvelopeDetectionMode::PEAK);

// Process audio
for (size_t i = 0; i < buffer_size; ++i) {
    float envelope = follower.process_sample(input[i]);

    // Use for compression
    float threshold = 0.5f;
    float ratio = 4.0f;

    if (envelope > threshold) {
        float excess = envelope - threshold;
        float reduction = excess * (1.0f - 1.0f/ratio);
        float target = envelope - reduction;
        float gain = target / envelope;
        output[i] = input[i] * gain;
    } else {
        output[i] = input[i];
    }
}

Example 4: Multi-Channel Envelope Processing

#include "adsr_envelope.h"
#include "audio_buffer.h"

// Create envelope
ADSREnvelope<float> env;
env.set_sample_rate(44100.0f);
env.note_on(1.0f);

// Process stereo buffer
AudioBuffer<float> buffer(2, 512);  // Stereo, 512 samples

// Fill buffer with oscillator
for (size_t ch = 0; ch < 2; ++ch) {
    for (size_t i = 0; i < 512; ++i) {
        buffer.get_channel_data(ch)[i] = oscillator.generate();
    }
}

// Apply envelope to both channels
env.process(buffer);

🎨 Curve Types

Exponential Curves (Musical, Natural)

Characteristics: - Fast initial change, gradual approach to target - Mimics natural acoustic envelopes - More "musical" sound - Perceived as linear by human ear (logarithmic perception)

When to use: - Most synthesizer envelopes - Amplitude envelopes (perceived linearity) - Filter envelopes (smooth transitions) - General-purpose synthesis

Formula:

y[n] = y[n-1] + coeff * (target - y[n-1])
coeff = 1 - exp(-4.6 / (time * sample_rate))

Linear Curves (Mechanical, Precise)

Characteristics: - Constant rate of change - Mathematically linear - More "mechanical" sound - Perceived as exponential by human ear

When to use: - Special effects - Precise timing requirements - Scientific/test applications - Modulation envelopes (not amplitude)

Formula:

y[n] = y[n-1] + step
step = 1.0 / (time * sample_rate)

Comparison:

// Exponential (default, recommended)
env.set_curve(ADSRCurve::EXPONENTIAL);
env.set_attack_time(0.1f);  // Reaches 99% in 100ms

// Linear (mechanical)
env.set_curve(ADSRCurve::LINEAR);
env.set_attack_time(0.1f);  // Reaches 100% in 100ms


⚑ Performance Characteristics

ADSREnvelope

Operation Complexity Notes
generate() O(1) 2-5 operations per sample
process(buffer) O(N) Linear with buffer size
set_*_time() O(1) Triggers coefficient update (exp/log)
Memory ~40 bytes Per-channel state

CPU Usage (estimated): - Exponential curve: ~5 cycles/sample (1 multiply, 1 add, 1 branch) - Linear curve: ~3 cycles/sample (1 add, 1 branch)

AREnvelope

Operation Complexity Notes
generate() O(1) Slightly faster than ADSR (fewer stages)
Memory ~24 bytes Per-channel state

CPU Usage: ~20% less than ADSR (no sustain stage)

EnvelopeFollower

Operation Complexity Notes
process_sample() O(1) 1-2 multiplies, 1-2 adds
Peak mode O(1) 1 abs(), 1 multiply, 1 add
RMS mode O(1) 2 multiplies, 2 adds, 1 sqrt()
Memory ~16 bytes Per-channel state

CPU Usage: - Peak mode: ~3 cycles/sample - RMS mode: ~15 cycles/sample (sqrt is expensive)


πŸ”§ Advanced Techniques

1. Velocity Sensitivity

Control how much note velocity affects envelope output:

ADSREnvelope<float> env;
env.set_velocity_sensitivity(1.0f);  // Full sensitivity (default)
env.note_on(0.5f);  // Half velocity β†’ 50% output level

env.set_velocity_sensitivity(0.0f);  // No sensitivity
env.note_on(0.5f);  // Half velocity β†’ 100% output level

env.set_velocity_sensitivity(0.5f);  // Partial sensitivity
env.note_on(0.5f);  // Half velocity β†’ 75% output level

2. Retrigger Behavior (Legato)

ADSR allows legato retriggering:

env.note_on(1.0f);
// Generate some samples...

// Retrigger without resetting current level (smooth legato)
env.note_on(1.0f);  // Continues from current level

// Hard reset (percussive retriggering)
env.kill();
env.note_on(1.0f);  // Starts from zero

AR provides explicit retrigger modes:

// Hard retrigger (percussive)
env.trigger(1.0f);  // Always starts from zero

// Legato retrigger
env.retrigger(1.0f);  // Continues from current level

3. Envelope Modulation (Envelope-of-Envelope)

Use one envelope to modulate another:

ADSREnvelope<float> amp_env;
ADSREnvelope<float> mod_env;

amp_env.note_on(1.0f);
mod_env.note_on(1.0f);

for (size_t i = 0; i < buffer_size; ++i) {
    float amp = amp_env.generate();
    float mod = mod_env.generate();

    // Modulate amplitude envelope
    float final_amp = amp * (0.5f + 0.5f * mod);

    output[i] = oscillator.generate() * final_amp;
}

4. Dynamic Envelope Times

Modulate envelope times in real-time:

// Map velocity to attack time (soft notes = slow attack)
float velocity = 0.8f;
float attack_time = 0.001f + (1.0f - velocity) * 0.1f;
env.set_attack_time(attack_time);
env.note_on(velocity);

// Map filter cutoff to decay time
float cutoff_normalized = (cutoff - 200.0f) / 1800.0f;
float decay_time = 0.05f + cutoff_normalized * 0.5f;
env.set_decay_time(decay_time);

5. Compressor with RMS Detection

Use envelope follower in RMS mode for smoother compression:

EnvelopeFollower<float> follower;
follower.set_mode(EnvelopeDetectionMode::RMS);
follower.set_attack_time(0.005f);
follower.set_release_time(0.1f);

for (size_t i = 0; i < buffer_size; ++i) {
    float envelope = follower.process_sample(input[i]);

    // Soft-knee compression
    float threshold = 0.5f;
    float ratio = 4.0f;
    float knee = 0.1f;

    float gain = compute_soft_knee_compression(
        envelope, threshold, ratio, knee
    );

    output[i] = input[i] * gain;
}

πŸŽ“ Envelope Design Guidelines

Amplitude Envelopes

Pads/Strings: - Attack: 0.3 - 1.0s (slow, gentle) - Decay: 0.5 - 1.0s - Sustain: 0.7 - 0.9 (high) - Release: 1.0 - 3.0s (long, smooth) - Curve: Exponential

Plucks/Guitars: - Attack: 0.001 - 0.01s (instant to fast) - Decay: 0.5 - 2.0s - Sustain: 0.0 - 0.3 (low or none) - Release: 0.1 - 0.5s - Curve: Exponential

Brass: - Attack: 0.05 - 0.2s (medium) - Decay: 0.1 - 0.3s - Sustain: 0.8 - 1.0 (high) - Release: 0.2 - 0.5s - Curve: Exponential

Drums: - Attack: 0.0001 - 0.005s (instant) - Release: 0.05 - 0.5s (depending on drum type) - Curve: Exponential - Use AREnvelope or DrumEnvelope

Filter Envelopes

Typical ranges: - Attack: 0.01 - 0.1s (faster than amplitude) - Decay: 0.1 - 0.5s - Sustain: 0.2 - 0.5 (lower than amplitude) - Release: 0.1 - 0.3s (shorter than amplitude) - Curve: Exponential

Purpose: Shape filter cutoff for timbral evolution

Dynamics Processing

Compressor: - Attack: 0.001 - 0.01s (fast to catch transients) - Release: 0.05 - 0.3s (musical release) - Mode: PEAK (accurate) or RMS (smooth)

Limiter: - Attack: 0.0001 - 0.001s (ultra-fast) - Release: 0.05 - 0.1s (fast recovery) - Mode: PEAK (prevent clipping)

Gate: - Attack: 0.001 - 0.005s (fast) - Release: 0.05 - 0.2s (avoid chatter) - Mode: RMS (smooth gating)


πŸ§ͺ Testing

All envelope atoms include comprehensive test suites:

Build and Run Tests

cd 05_07_03_envelope
mkdir build && cd build
cmake ..
cmake --build .
ctest --output-on-failure

Test Coverage

ADSREnvelope: - βœ… Constructor and initialization (3 tests) - βœ… Parameter setters (10 tests) - βœ… Stage transitions - exponential (7 tests) - βœ… Stage transitions - linear (2 tests) - βœ… Velocity sensitivity (3 tests) - βœ… Retrigger behavior (2 tests) - βœ… Kill function (2 tests) - βœ… Multi-channel processing (3 tests) - βœ… Edge cases (8 tests) - βœ… Reset function (2 tests) - βœ… Percussive envelope wrapper (3 tests) - βœ… Double precision (1 test)

Total: 46 test cases

AREnvelope (planned): - Constructor and initialization - Stage transitions - trigger() vs retrigger() - Wrapper classes (DrumEnvelope, PluckEnvelope)

EnvelopeFollower (planned): - Peak vs RMS detection modes - Attack/release smoothing - Multi-channel processing - Wrapper classes (CompressorSidechain, PeakMeter)


πŸ“‹ API Reference

ADSREnvelope

// Construction
ADSREnvelope();

// Configuration
void set_sample_rate(T sample_rate);
void set_attack_time(T time_seconds);
void set_decay_time(T time_seconds);
void set_sustain_level(T level);  // [0.0, 1.0]
void set_release_time(T time_seconds);
void set_curve(ADSRCurve curve);  // LINEAR or EXPONENTIAL
void set_velocity_sensitivity(T sensitivity);  // [0.0, 1.0]

// Triggering
void note_on(T velocity = 1.0);   // Start attack
void note_off();                  // Start release
void kill();                      // Immediate silence

// Generation
T generate();                     // Generate single sample
void process(AudioBuffer<T>& buffer);  // Process buffer

// Status
bool is_active() const;
ADSRStage get_stage() const;

// Getters
T get_attack_time() const;
T get_decay_time() const;
T get_sustain_level() const;
T get_release_time() const;
ADSRCurve get_curve() const;

// Reset
void reset();

AREnvelope

// Construction
AREnvelope();

// Configuration
void set_sample_rate(T sample_rate);
void set_attack_time(T time_seconds);
void set_release_time(T time_seconds);
void set_curve(ARCurve curve);
void set_velocity_sensitivity(T sensitivity);

// Triggering
void trigger(T velocity = 1.0);    // Hard retrigger (percussive)
void retrigger(T velocity = 1.0);  // Legato retrigger
void kill();

// Generation
T generate();
void process(AudioBuffer<T>& buffer);

// Status
bool is_active() const;
ARStage get_stage() const;

// Getters
T get_attack_time() const;
T get_release_time() const;
ARCurve get_curve() const;

// Reset
void reset();

EnvelopeFollower

// Construction
EnvelopeFollower();

// Configuration
void set_sample_rate(T sample_rate);
void set_attack_time(T time_seconds);
void set_release_time(T time_seconds);
void set_mode(EnvelopeDetectionMode mode);  // PEAK or RMS

// Processing
T process_sample(T input);
void process(AudioBuffer<T>& buffer);

// Status
T get_envelope_level() const;

// Getters
T get_attack_time() const;
T get_release_time() const;
EnvelopeDetectionMode get_mode() const;

// Reset
void reset();

πŸ”— Integration with Other Atoms

With Oscillators

#include "adsr_envelope.h"
#include "sine_oscillator.h"

ADSREnvelope<float> amp_env;
SineOscillator<float> osc;

amp_env.note_on(1.0f);
osc.set_frequency(440.0f);

for (size_t i = 0; i < buffer_size; ++i) {
    float env = amp_env.generate();
    float osc_out = osc.generate();
    output[i] = osc_out * env;
}

With Filters

#include "adsr_envelope.h"
#include "svf_filter.h"

ADSREnvelope<float> filter_env;
SVFFilter<float> filter;

filter_env.note_on(1.0f);

for (size_t i = 0; i < buffer_size; ++i) {
    float env = filter_env.generate();

    // Modulate filter cutoff (200 Hz - 5000 Hz)
    float cutoff = 200.0f + env * 4800.0f;
    filter.set_cutoff(cutoff);

    output[i] = filter.process_sample(input[i]);
}

Dynamics Chain

#include "envelope_follower.h"
#include "onepole_filter.h"

EnvelopeFollower<float> sidechain;
OnePoleFilter<float> smoother;

sidechain.set_attack_time(0.005f);
sidechain.set_release_time(0.1f);
smoother.set_cutoff(10.0f);  // 10 Hz smoothing

for (size_t i = 0; i < buffer_size; ++i) {
    float envelope = sidechain.process_sample(input[i]);
    float smoothed = smoother.process_sample(envelope);

    float gain_reduction = compute_compressor_gain(smoothed);
    output[i] = input[i] * gain_reduction;
}

πŸ“¦ Files

05_07_03_envelope/
β”œβ”€β”€ include/
β”‚   β”œβ”€β”€ adsr_envelope.h         (~465 LOC) - Classic ADSR envelope
β”‚   β”œβ”€β”€ ar_envelope.h           (~400 LOC) - Simple AR envelope
β”‚   └── envelope_follower.h     (~350 LOC) - Audio amplitude tracker
β”œβ”€β”€ tests/
β”‚   β”œβ”€β”€ test_adsr_envelope.cpp  (~900 LOC) - 46 comprehensive tests
β”‚   β”œβ”€β”€ test_ar_envelope.cpp    (planned)
β”‚   └── test_envelope_follower.cpp (planned)
β”œβ”€β”€ CMakeLists.txt              - Build configuration
β”œβ”€β”€ README.md                   - This file
└── STATUS.md                   - Development progress tracking

🎯 Status

Component Implementation Tests Documentation
ADSREnvelope βœ… Complete βœ… 46 tests βœ… Complete
AREnvelope βœ… Complete πŸ“‹ Planned βœ… Complete
EnvelopeFollower βœ… Complete πŸ“‹ Planned βœ… Complete

Overall Progress: 70% (Headers complete, tests partial)


πŸ“š References

  • "Synthesizer Basics" - Brent Hurtig (ADSR fundamentals)
  • "The Computer Music Tutorial" - Curtis Roads (envelope theory)
  • "Designing Software Synthesizer Plug-Ins in C++" - Will Pirkle (implementation)
  • "Digital Audio Signal Processing" - Udo ZΓΆlzer (envelope follower)
  • "The Art of VA Filter Design" - Vadim Zavalishin (TPT envelopes)

Last Updated: 2025-10-10 Maintainer: AudioLab Core Team