7.10.9. STRUDEL-09 — ADSR & Envelope Shaping

An ADSR envelope shapes a note’s amplitude over time:

Attack

time from key-down to peak amplitude

Decay

time to fall from the peak to the sustain level

Sustain

held level (0..1) while the note is on

Release

time to fade to silence after key-up

daslang’s strudel uses tempo-aware defaults for ADSR. Instead of fixed numeric defaults, each field defaults to -1 — a sentinel meaning unset. At playback time a resolver fills in sensible values based on which fields the user actually touched:

Nothing set

held tone — sustain = 1, tiny attack/release

Only decay set

percussion — sustain auto-zeroes

sustain set explicitly

always wins

The same idea applies to delaytime: it defaults to -1 and is derived from delaysync (in cycles) and the current cps so the echo locks to tempo automatically.

7.10.9.1. Part A: default envelope — held tone

With no ADSR setters at all, the resolver picks a held-tone envelope: the note rings for its full scheduled duration, instead of decaying to silence in 50ms as in older defaults.

let pat <- note("c4 e4 g4 c5", "sine")

This is the unset → held branch of the resolver.

7.10.9.2. Part B: pluck — decay only

Set decay alone and the resolver assumes a percussive envelope: sustain drops to 0 so each note plucks then decays away. This is the idiom for short, rhythmic synth blips:

let pat <- note("c4 e4 g4 c5", "sine") |> decay(0.15)

No need to spell out sustain(0.0) — the resolver does it because “only decay was set”.

7.10.9.3. Part C: pad — explicit ADSR

Setting all four fields gives full manual control. A pad-style envelope uses a long attack so the note swells in, plus a long release so it fades out gradually after key-up:

let pat <- note("c3", "triangle") |> attack(0.5) |> decay(0.2) |> sustain(0.6) |> release(0.5)

Because sustain was set explicitly, the resolver leaves all four values untouched.

7.10.9.4. Part D: tempo-aware delay defaults

The “unset sentinel + resolver” idea also applies to delaytime. Plain delay(0.6) without delaytime(...) derives delaytime from delaysync (3/16 cycle by default) and the current cps. This gives a dotted-eighth feel that automatically tracks tempo changes:

let pat_slow <- note("c4 ~ e4 ~ g4 ~", "sine") |> decay(0.15) |> delay(0.6)
play(pat_slow, 0.5lf, 4.0lf)

let pat_fast <- note("c4 ~ e4 ~ g4 ~", "sine") |> decay(0.15) |> delay(0.6)
play(pat_fast, 1.0lf, 4.0lf)

Same pattern, two different cps — the echo timing follows the tempo. Setting delaytime (or delaysync) explicitly bypasses the resolver.

7.10.9.5. Part E: velocity via gain

gain multiplies the envelope output. Use it for a velocity curve over a pattern: alternating loud and soft hits while the envelope shape stays constant.

let pat <- stack([
    note("c4", "sine") |> decay(0.15) |> gain(0.9),
    note("e4", "sine") |> decay(0.15) |> gain(0.4),
    note("g4", "sine") |> decay(0.15) |> gain(0.7),
    note("c5", "sine") |> decay(0.15) |> gain(0.3)
])

Each voice keeps the same plucky envelope but a different peak level.