Metlivi Blog

How to Give Each Played Note a Reverb Decay Based on How Long It Was Held

To give each played note its own duration-dependent reverb, measure the time between its note-on and note-off, map that duration to a decay time, and apply the result to a reverb dedicated to that note. Keep the note’s reverb running after its sound source releases.

This tutorial builds that behavior in SuperCollider, including MIDI input and an overlapping-note test. The intended result is simple: a briefly held note leaves a short tail, while a longer-held note leaves a longer tail—even when their reverbs overlap.

October 29, 20268 min readBy Metlivi Editorial Team
Section 1

Why does each note need a separate reverb?

Consider two overlapping notes. You hold the first for two seconds and tap the second for a quarter of a second. If both feed one reverb, changing its decay for the second note also changes the processing of the first note’s remaining reverberation. A shared decay control cannot preserve two independent settings within the same reverb state.

The routing therefore needs to be: each note’s sound source feeds its own reverb, and those completed voice outputs are mixed together. This is an architectural deduction: independent tails require independent processing state, unless the instrument or effect already provides equivalent internal separation.

A polyphonic patch provides a useful framework for this. Cycling ’74 documents how Max’s poly~ creates voice instances and uses thispoly~ to mark each instance busy or available. For this task, a voice must remain occupied through its reverb tail, rather than becoming available immediately at note-off. Cycling ’74: Polyphony

There is also a timing constraint: during a live performance, the final held duration is unknown until the note ends. The implementation below uses a provisional decay while the key is down and assigns the measured decay at release. If you need the final decay to apply from the beginning, work from note durations already available in a sequence or render individual notes afterward.

Section 2

What should held duration control?

Keep three controls separate: the instrument’s amplitude release, the reverb’s decay, and the wet output level. A longer instrument release continues feeding sound into the reverb. A higher wet level makes the effect louder. Neither operation, by itself, gives the reverb a longer decay.

Decay also does not normally mean an exact stop time. Ableton’s Reverb documentation defines its Decay control as the time taken for the tail to fall to one-thousandth of its initial amplitude, or −60 dB. That distinction matters when comparing a decay setting with the moment a tail becomes inaudible. Ableton: Live Audio Effect Reference, Reverb

For a first implementation, use this explicitly illustrative mapping, with all values in seconds:

decay = min(6.0, max(0.25, 0.25 + 1.5 × heldDuration))

Held duration Assigned decay
0.10 seconds 0.40 seconds
0.25 seconds 0.625 seconds
1.00 second 1.75 seconds
2.00 seconds 3.25 seconds
4.00 seconds 6.00 seconds, capped

These values are design choices, not measured acoustic results. The minimum gives short taps an audible tail; the multiplier sets how strongly holding changes the effect; the maximum bounds the requested decay. Start with this mapping, then adjust those three choices while keeping note velocity and wet level constant.

Section 3

Build a voice that survives note-off

The example uses SuperCollider’s built-in GVerb, which accepts a mono input, produces two output channels, and exposes revtime in seconds. Its documentation also identifies an instantiation CPU spike and artifacts when room size changes. Accordingly, this example keeps room size fixed and is best approached initially with a few simultaneous notes. SuperCollider: GVerb

Each voice contains a small two-oscillator sound source, a short amplitude release, and one reverb. A separate lifetime envelope frees the voice only after its tail allowance has elapsed. SuperCollider’s EnvGen supports sustaining envelopes that release when their gate reaches zero, as well as a completion action that frees the enclosing synth. SuperCollider: EnvGen

Use a fresh SuperCollider session with a working stereo audio output. Evaluate the following complete setup block, then wait for Held-note reverb ready in the Post window. No samples or additional libraries are required. The code has been inspected against the documentation; it has not been executed here because the SuperCollider interpreter is unavailable.

(
s.waitForBoot {
    SynthDef(\heldNoteVerb, {
        |out = 0, freq = 440, amp = 0.035,
        gate = 1, decay = 1, alive = 1|
        var sourceEnv, lifetime, source, wet;

        sourceEnv = EnvGen.kr(
            Env.asr(0.01, 1, 0.02), gate
        );
        lifetime = EnvGen.kr(
            Env.asr(0.01, 1, 0.03), alive,
            doneAction: 2
        );
        source = (
            SinOsc.ar(freq) + (0.25 * SinOsc.ar(freq * 2))
        ) * amp * sourceEnv;
        wet = GVerb.ar(
            source,
            roomsize: 20,
            revtime: Lag.kr(decay, 0.03),
            damping: 0.5,
            drylevel: 0,
            earlyreflevel: 0.2,
            taillevel: 0.5
        );
        Out.ar(out, ((source ! 2) + wet) * lifetime);
    }).add;
    s.sync;

    ~heldVoices = Dictionary.new;
    ~tailJobs = List.new;
    ~voiceGroup = Group.head(s);
    ~voiceKey = { |num, chan, src|
        src.asString ++ ":" ++ chan.asString ++ ":" ++ num.asString
    };
    ~decayFor = { |held|
        (0.25 + (1.5 * held)).clip(0.25, 6.0)
    };

    ~releaseHeld = { |num, chan = 0, src = 0|
        var key, queue, entry, held, decay, node;
        key = ~voiceKey.value(num, chan, src);
        queue = ~heldVoices[key];
        if(queue.notNil) {
            if(queue.notEmpty) {
                entry = queue.removeAt(0);
                node = entry[0];
                held = (Main.elapsedTime - entry[1]).max(0);
                decay = ~decayFor.value(held);
                node.set(\decay, decay, \gate, 0);
                [\note, num, \held, held, \decay, decay].postln;
                ~tailJobs.add(Routine {
                    ((2 * decay) + 0.1).wait;
                    node.set(\alive, 0);
                }.play(SystemClock));
            };
            if(queue.isEmpty) { ~heldVoices.removeAt(key) };
        };
    };

    ~startHeld = { |vel, num, chan = 0, src = 0|
        var key, node;
        if(vel <= 0) {
            ~releaseHeld.value(num, chan, src);
        } {
            key = ~voiceKey.value(num, chan, src);
            if(~heldVoices[key].isNil) {
                ~heldVoices[key] = List.new;
            };
            node = Synth(\heldNoteVerb, [
                \freq, num.midicps,
                \amp, (vel / 127) * 0.035
            ], ~voiceGroup);
            ~heldVoices[key].add([node, Main.elapsedTime]);
        };
    };
    "Held-note reverb ready".postln;
};
)

The ordering at release matters: the voice receives its decay setting and source gate change together. The reverb continues processing while the source fades out over 20 milliseconds. After approximately twice the assigned decay plus a small allowance, the entire voice fades out and frees itself.

That final allowance is a resource-management choice, not a measurement of silence. Increase it if the remaining tail is audible at cleanup. For a larger instrument, replace the fixed allowance with output-level monitoring plus a maximum lifetime. The example also retains completed job references until cleanup, so it is a demonstration rather than an indefinitely running instrument.

Section 4

Connect played MIDI notes to the correct voice

A duration measurement belongs to a particular note instance. Tracking only the most recent note-on timestamp fails as soon as notes overlap. The setup instead stores each voice and its start time under a key containing source, channel, and pitch.

SuperCollider’s MIDI guide demonstrates initializing MIDI, connecting inputs, and saving synth references so note-off can release the appropriate sound. Its MIDIdef reference specifies callback arguments in the order value, note number, channel, and source. SuperCollider: Using MIDI, SuperCollider: MIDIdef

After the setup reports ready, evaluate this block to play the instrument from connected MIDI inputs:

(
MIDIClient.init;
MIDIIn.connectAll;

MIDIdef.noteOn(\heldVerbOn, { |vel, num, chan, src|
    ~startHeld.value(vel, num, chan, src);
});
MIDIdef.noteOff(\heldVerbOff, { |vel, num, chan, src|
    ~releaseHeld.value(num, chan, src);
});
)

The example defines “held” as the interval between received note events. It does not implement sustain-pedal behavior. If you want pedal-extended duration, defer the affected release operations until the pedal lifts, then measure that longer interval.

Repeated presses of the same pitch are stored in a queue and released oldest first. This is an explicit matching policy for overlapping same-pitch events on one source and channel. Unmatched note-offs do nothing; an unmatched note-on remains active until cleanup. Check event pairing before extending the instrument.

Section 5

Test overlapping tails before changing the sound

First test the behavior without a keyboard. The following sequence uses the same event functions as the MIDI input: note 60 lasts two seconds, while note 67 starts halfway through it and lasts a quarter of a second. Their expected assigned decays are approximately 3.25 and 0.625 seconds.

(
~heldTest = Routine {
    ~startHeld.value(100, 60);
    1.0.wait;
    ~startHeld.value(100, 67);
    0.25.wait;
    ~releaseHeld.value(67);
    0.75.wait;
    ~releaseHeld.value(60);
}.play(SystemClock);
)

SystemClock provides scheduling in seconds, which keeps this test independent of a musical tempo setting. Small differences between printed and requested durations can occur in actual operation. SuperCollider: SystemClock

Listen for the higher note’s short tail while the lower note continues, followed by the lower note’s longer tail. Then reverse the durations. This second pass helps establish that duration drives the result rather than pitch. Read the printed held-duration and decay pairs alongside the audio.

To stop and remove this example, evaluate the following cleanup block after installing the MIDI handlers. It also cancels pending tail jobs before removing their voices:

(
MIDIdef(\heldVerbOn).free;
MIDIdef(\heldVerbOff).free;
if(~heldTest.notNil) { ~heldTest.stop };
~tailJobs.do { |job| job.stop };
~tailJobs.clear;
~voiceGroup.free;
~heldVoices.clear;
)
Section 6

How do you diagnose tails that behave incorrectly?

If every tail changes when one note releases, inspect the audio routing. A per-note timestamp still produces shared behavior when its result controls one reverb after the voice mix. Move the reverb into each voice, or route isolated note audio to separate processing instances.

If the tail disappears exactly at note-off, inspect voice lifetime. The amplitude envelope should stop the sound source without freeing the reverb containing that source. In a Max implementation, preserve the voice’s busy state until its wet output has finished, following the documented poly~ allocation model. Cycling ’74: Polyphony

If the decay numbers are wrong, print the event key and timestamps before changing the reverb. In Max, the corresponding timing building block is timer, which measures elapsed time between events and defaults to milliseconds. Convert units explicitly when the destination expects seconds. Cycling ’74: timer

If performance becomes uneven, reduce simultaneous voices and inspect the reverb choice before expanding the patch. This demonstration does not enforce a voice limit, and each new note creates another reverb. A production instrument needs an explicit allocation policy that accounts for both held notes and still-ringing tails.

Once the timing and overlap tests work, replace the oscillator with your chosen per-voice sound source. Keep the same sequence: allocate a voice, record its start time, measure at release, assign its decay, and retain its reverb until the tail has finished. That sequence makes held duration a repeatable performance control.

Section 7

Sources

Related reading

Keep exploring this topic