Skip to content

Instantly share code, notes, and snippets.

@ingoogni
Created June 18, 2025 10:20
Show Gist options
  • Select an option

  • Save ingoogni/cfb66d86eab19f2f574a6bb64b617f0b to your computer and use it in GitHub Desktop.

Select an option

Save ingoogni/cfb66d86eab19f2f574a6bb64b617f0b to your computer and use it in GitHub Desktop.

Revisions

  1. ingoogni created this gist Jun 18, 2025.
    255 changes: 255 additions & 0 deletions iteradsr.nim
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,255 @@
    # 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