Created
June 18, 2025 10:20
-
-
Save ingoogni/cfb66d86eab19f2f574a6bb64b617f0b to your computer and use it in GitHub Desktop.
ADSR in Nim (after Nigel Redmon)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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