Skip to content

Instantly share code, notes, and snippets.

@ingoogni
Created June 18, 2025 10:20
Show Gist options
  • Save ingoogni/cfb66d86eab19f2f574a6bb64b617f0b to your computer and use it in GitHub Desktop.
Save ingoogni/cfb66d86eab19f2f574a6bb64b617f0b to your computer and use it in GitHub Desktop.
ADSR in Nim (after Nigel Redmon)
# Original C++ code:
#
# Created by Nigel Redmon on 12/18/12.
# EarLevel Engineering: earlevel.com
# Copyright 2012 Nigel Redmon
#
# For a complete explanation of the ADSR envelope generator and code,
# read the series of articles by the author, starting here:
# http://www.earlevel.com/main/2013/06/01/envelope-generators/
#
# License:
#
# This source code is provided as is, without warranty.
# You may copy and distribute verbatim copies of this document.
# You may modify and use this source code to create binary code for your own purposes, free or commercial.
#
# 1.01 2016-01-02 njr added calcCoef to SetTargetRatio functions that were in the ADSR widget but missing in this code
# 1.02 2017-01-04 njr in calcCoef, checked for rate 0, to support non-IEEE compliant compilers
# 1.03 2020-04-08 njr changed float to double; large target ratio and rate resulted in exp returning 1 in calcCoef
#-------------------------------------------------------------------------
#
# https://www.earlevel.com/main/2013/06/01/envelope-generators/
# https://www.earlevel.com/main/2013/06/02/envelope-generators-adsr-part-2/
# https://www.earlevel.com/main/2013/06/03/envelope-generators-adsr-code/
# https://www.youtube.com/watch?v=0oreYmOWgYE
# https://www.earlevel.com/main/2013/06/23/envelope-generators-adsr-widget/
# https://web.archive.org/web/20250000000000*/https://www.earlevel.com/main/2013/06/23/envelope-generators-adsr-widget/
import std/[math, random]
const
SampleRate {.intdefine.} = 44100
SRate* = SampleRate.float
type
EnvState = enum
envIdle,
envAttack,
envDecay,
envSustain,
envRelease
ADSR* = ref object
state: EnvState
sampleRate: float
output: float
attackRate: float # in seconds
decayRate: float
releaseRate: float
sustainLevel: float
attackCoef: float
decayCoef: float
releaseCoef: float
targetRatioA: float
targetRatioDR: float
attackBase: float
decayBase: float
releaseBase: float
gate: bool
proc calcCoef(rate: float, targetRatio: float): float =
return if rate <= 0: 0.0
else: exp(-log10((1.0 + targetRatio) / targetRatio) / rate)
proc setAttackRate*(adsr: ADSR, rate: float) =
## set attackRate in seconds
adsr.attackRate = rate * adsr.sampleRate
adsr.attackCoef = calcCoef(rate, adsr.targetRatioA)
adsr.attackBase = (1.0 + adsr.targetRatioA) * (1.0 - adsr.attackCoef)
proc setDecayRate*(adsr: ADSR, rate: float) =
## set decay rate in seconds
adsr.decayRate = rate * adsr.sampleRate
adsr.decayCoef = calcCoef(rate, adsr.targetRatioDR)
adsr.decayBase = (adsr.sustainLevel - adsr.targetRatioDR) *
(1.0 - adsr.decayCoef)
proc setReleaseRate*(adsr: ADSR, rate: float) =
## set release rate in seconds
adsr.releaseRate = rate * adsr.sampleRate
adsr.releaseCoef = calcCoef(rate, adsr.targetRatioDR)
adsr.releaseBase = -adsr.targetRatioDR * (1.0 - adsr.releaseCoef)
proc setSustainLevel*(adsr: ADSR, level: float) =
## set sustain level [0.0, 1.0]
adsr.sustainLevel = level
adsr.decayBase = (adsr.sustainLevel - adsr.targetRatioDR) *
(1.0 - adsr.decayCoef)
proc setTargetRatioA*(adsr: ADSR, targetRatio: float) =
## set curvature of attack ratio 0: fast rise sharp bend, 1: linear
## https://www.earlevel.com/main/2013/06/23/envelope-generators-adsr-widget/
adsr.targetRatioA = if targetRatio >= 0.000000001: targetRatio
else: 0.000000001 #-180 dB
adsr.attackCoef = calcCoef(adsr.attackRate, adsr.targetRatioA);
adsr.attackBase = (1.0 + adsr.targetRatioA) * (1.0 - adsr.attackCoef);
proc setTargetRatioDR*(adsr: ADSR, targetRatio: float) =
## set curvature for decay and release 0: fast descent, sharp bend 1: linear
## https://www.earlevel.com/main/2013/06/23/envelope-generators-adsr-widget/
adsr.targetRatioDR = if targetRatio >= 0.000000001: targetRatio
else: 0.000000001 #-180 dB
adsr.decayCoef = calcCoef(adsr.decayRate, adsr.targetRatioDR)
adsr.releaseCoef = calcCoef(adsr.releaseRate, adsr.targetRatioDR)
adsr.decayBase = (adsr.sustainLevel - adsr.targetRatioDR) * (1.0 - adsr.decayCoef)
adsr.releaseBase = -adsr.targetRatioDR * (1.0 - adsr.releaseCoef)
proc getState*(adsr: ADSR): EnvState =
adsr.state
proc reset*(adsr: ADSR) =
adsr.state = envIdle
adsr.output = 0.0
proc initADSR*(a, d, s, r: float, sampleRate: float = SRate): ADSR =
result = ADSR(sampleRate: sampleRate)
result.reset()
result.gate = false
result.setAttackRate(a)
result.setDecayRate(d)
result.setReleaseRate(r)
result.setSustainLevel(s)
result.setTargetRatioA(0.3)
result.setTargetRatioDR(0.0001)
proc adsrGate*(adsr: ADSR, gate: bool)=
if gate and not adsr.gate:
adsr.state = envAttack
adsr.gate = true
elif not gate and adsr.state != envIdle:
adsr.state = envRelease
adsr.gate = false
proc next*(adsr: ADSR): float =
case adsr.state:
of envIdle:
discard
of envAttack:
adsr.output = adsr.attackBase + adsr.output * adsr.attackCoef;
if adsr.output >= 1.0:
adsr.output = 1.0
adsr.state = envDecay
of envDecay:
adsr.output = adsr.decayBase + adsr.output * adsr.decayCoef;
if adsr.output <= adsr.sustainLevel:
adsr.output = adsr.sustainLevel
adsr.state = env_sustain
of envSustain:
discard
of envRelease:
adsr.output = adsr.releaseBase + adsr.output * adsr.releaseCoef;
if adsr.output <= 0.0:
adsr.output = 0.0
adsr.state = env_idle
return adsr.output
proc envelope*(
sample: iterator: float or float,
gate: iterator: bool,
adsr: ADSR
): iterator(): float =
return iterator(): float =
while true:
adsr.adsrGate(gate()) #manage state
yield sample() * adsr.next()
when isMainModule:
import iterit, iteroscillator
proc triggerIter*(interval: int): iterator: bool =
var count = 0
return iterator(): bool =
while true:
let output = count mod interval == 0
inc count
yield output
proc tGate*(
openTime: float or iterator: float,
triggerPulse: iterator: bool,
sampleRate: float = SRate
): iterator(): bool =
return iterator(): bool =
var cnt = 0
while true:
let ns = int(openTime.floatOrIter * sampleRate)
let trig = triggerPulse()
if not trig:
if cnt == 0:
yield false
elif cnt > 0 and cnt < ns:
inc cnt
yield true
elif cnt >= ns:
cnt = 0
yield false
else:
cnt = 1
yield true
let sine0 = sawOsc(120.0, Pi*0.5, 1.0)
let trig0 = triggerIter(int(2 * SampleRate))
let gate0 = tGate(1.9, trig0) # gate should be at least a tad shorter than the trigger interval
let adsr0 = initADSR(0.3, 0.2, 0.4, 0.2)
let env0 = envelope(sine0, gate0, adsr0)
let sine1 = sinOsc(90.0, Pi*0.5, 1.0)
let trig1 = triggerIter(int(0.4 * SampleRate))
let gate1 = tGate(0.25, trig1)
let adsr1 = initADSR(0.01, 0.01, 0.01, 0.2)
let env1 = envelope(sine1, gate1, adsr1)
let output = (env0 / 3.0) + env1
proc tick*(): float =
#inc tickerTape.tick
return output()
include io #libSoundIO
var ss = newSoundSystem()
ss.outstream.sampleRate = SampleRate
let outstream = ss.outstream
let sampleRate = outstream.sampleRate.toFloat
echo "Format:\t\t", outstream.format
echo "Sample Rate:\t", sampleRate
echo "Latency:\t", outstream.softwareLatency
while true:
ss.sio.flushEvents
let s = stdin.readLine
if s == "q":
break
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment