Skip to content

Instantly share code, notes, and snippets.

@ingoogni
Last active June 12, 2025 09:02
Show Gist options
  • Select an option

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

Select an option

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

Revisions

  1. ingoogni revised this gist Jun 9, 2025. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions _modal synthesis
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    Modal synthesis in Nim with a pool for modes, so one excite fast and they still properly decay. Cheesy bell, gong & mallet modes.
  2. ingoogni created this gist Jun 9, 2025.
    87 changes: 87 additions & 0 deletions io.nim
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,87 @@
    #import std/[math]
    import soundio

    type
    SoundSystem* = object
    sio*: ptr SoundIo
    indevice*: ptr SoundIoDevice
    instream*: ptr SoundIoInStream
    outdevice*: ptr SoundIoDevice
    outstream*: ptr SoundIoOutStream
    #outsource*: proc()


    proc `=destroy`(s: SoundSystem) =
    if not isNil s.outstream:
    echo "destroy outstream"
    s.outstream.destroy
    dealloc(s.outstream.userdata)
    if not isNil s.outdevice:
    echo "destroy outdevice"
    s.outdevice.unref
    echo "destroy SoundSystem"
    s.sio.destroy
    echo "Quit"


    proc writeCallback(outStream: ptr SoundIoOutStream, frameCountMin: cint, frameCountMax: cint) {.cdecl.} =
    let csz = sizeof SoundIoChannelArea
    var areas: ptr SoundIoChannelArea
    var framesLeft = frameCountMax
    var err: cint

    while true:
    var frameCount = framesLeft
    err = outStream.beginWrite(areas.addr, frameCount.addr)
    if err > 0:
    quit "Unrecoverable stream error: " & $err.strerror
    if frameCount <= 0:
    break
    let layout = outstream.layout
    let ptrAreas = cast[int](areas)
    for frame in 0..<frameCount:

    let sample = tick()

    for channel in 0..<layout.channelCount:
    let ptrArea = cast[ptr SoundIoChannelArea](ptrAreas + channel*csz)
    var ptrSample = cast[ptr float32](cast[int](ptrArea.pointer) + frame*ptrArea.step)
    ptrSample[] = sample

    err = outstream.endWrite
    if err > 0 and err != cint(SoundIoError.Underflow):
    quit "Unrecoverable stream error: " & $err.strerror
    framesLeft -= frameCount
    if framesLeft <= 0:
    break

    proc newSoundSystem*(): SoundSystem =
    echo "SoundIO version : ", version_string()
    result.sio = soundioCreate()
    if isNil result.sio: quit "out of mem"
    var err = result.sio.connect
    if err > 0: quit "Unable to connect to backend: " & $err.strerror
    echo "Backend: \t", result.sio.currentBackend.name
    result.sio.flushEvents

    echo "Output:"
    let outdevID = result.sio.defaultOutputDeviceIndex
    echo " Device Index: \t", outdevID
    if outdevID < 0: quit "Output device is not found"
    result.outdevice = result.sio.getOutputDevice(outdevID)
    if isNil result.outdevice: quit "out of mem"
    if result.outdevice.probeError > 0: quit "Cannot probe device"
    echo " Device Name:\t", result.outdevice.name

    result.outstream = result.outdevice.outStreamCreate
    result.outstream.write_callback = writeCallback

    err = result.outstream.open
    if err > 0: quit "Unable to open output device: " & $err.strerror
    if result.outstream.layoutError > 0:
    quit "Unable to set channel layout: " & $result.outstream.layoutError.strerror
    err = result.outstream.start
    if err > 0: quit "Unable to start stream: " & $err.strerror



    191 changes: 191 additions & 0 deletions multimodal.nim
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,191 @@
    #inspiration: https://nathan.ho.name/posts/exploring-modal-synthesis/

    import std/[math, random]

    const
    SampleRate {.intdefine.} = 44100
    SRate* = SampleRate.float32
    MaxModalObjects = 32 # Maximum slots in the pool

    type
    Ticker* = object
    tick: uint


    Mode* = object
    partial*: seq[float32]
    phase*: seq[float32]
    amplitude*: seq[float32]
    decayTime*: seq[float32]


    proc initMode*(partial, phase, amplitude, decayTime: seq[float32]): Mode =
    Mode(
    partial: partial,
    phase: phase,
    amplitude: amplitude,
    decayTime: decayTime
    )


    type
    Modal* = object
    frequency*: seq[float32]
    phase: seq[float32]
    amplitude*: seq[float32]
    gain*: float32
    # State
    phaseIncrement: seq[float32]
    decayFactor*: seq[float32]
    envelope*: seq[float32]
    active*: bool


    proc initModal*(mode: Mode, fundamental: float32, gain: float32): Modal =
    let len = mode.partial.len
    var modal = Modal(
    frequency: newSeq[float32](len),
    phase: mode.phase,
    amplitude: mode.amplitude,
    gain: gain,
    phaseIncrement: newSeq[float32](len),
    decayFactor: newSeq[float32](len),
    envelope: mode.amplitude,
    active: false
    )
    for i in 0..<len:
    modal.frequency[i] = mode.partial[i] * fundamental
    modal.phaseIncrement[i] = modal.frequency[i] * Tau.float32 / SRate
    modal.decayFactor[i] = exp(-1.0'f32 / (mode.decayTime[i] * SRate))
    return modal


    proc excite*(modal: var Modal) =
    modal.active = true
    for i in 0..<modal.frequency.len:
    modal.envelope[i] = modal.amplitude[i]


    proc isActive*(modal: Modal): bool =
    if not modal.active:
    return false
    const threshold = 0.001'f32
    for env in modal.envelope:
    if env > threshold:
    return true
    return false


    proc next*(modal: var Modal): float32 =
    if not modal.active:
    return 0.0'f32
    var output = 0.0'f32
    var stillActive = false
    for i in 0..<modal.frequency.len:
    if modal.envelope[i] > 0.001'f32: # Threshold for silence
    let sineWave = sin(modal.phase[i])
    output += sineWave * modal.envelope[i]
    stillActive = true
    modal.phase[i] = (modal.phase[i] + modal.phaseIncrement[i]) mod Tau.float32
    modal.envelope[i] *= modal.decayFactor[i]
    modal.active = stillActive
    return (output / modal.frequency.len.float32) * modal.gain


    type
    ModalPool* = object
    modals: seq[Modal]
    nextIndex: int


    proc initModalPool*(): ModalPool =
    ModalPool(
    modals: newSeq[Modal](MaxModalObjects),
    nextIndex: 0
    )


    proc trigger*(pool: var ModalPool, mode: Mode, fundamental: float32, gain: float32) =
    # Round-Robin, if no inactive slot, override the oldest
    var startIndex = pool.nextIndex
    var found = false
    for i in 0..<MaxModalObjects:
    let idx = (startIndex + i) mod MaxModalObjects
    if not pool.modals[idx].isActive():
    pool.modals[idx] = initModal(mode, fundamental, gain)
    pool.modals[idx].excite()
    pool.nextIndex = (idx + 1) mod MaxModalObjects
    found = true
    break
    if not found:
    pool.modals[pool.nextIndex] = initModal(mode, fundamental, gain)
    pool.modals[pool.nextIndex].excite()
    pool.nextIndex = (pool.nextIndex + 1) mod MaxModalObjects


    proc next*(pool: var ModalPool): float32 =
    var output = 0.0'f32
    for modal in pool.modals.mitems:
    output += modal.next()
    return output


    proc activeCount*(pool: ModalPool): int =
    var count = 0
    for modal in pool.modals:
    if modal.isActive():
    inc count
    return count


    let bellModes* = Mode(
    partial: @[1.0, 2.0, 3.0, 4.2, 5.4, 6.8, 8.1 , 9.6],
    phase: @[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 , 0.0],
    amplitude: @[1.0, 0.8, 0.6, 0.4, 0.3, 0.2, 0.15, 0.3],
    decayTime: @[3.0, 2.5, 2.0, 1.8, 1.5, 1.3, 1.0 , 0.8]
    )

    let gongModes* = Mode(
    partial: @[1.0, 1.6, 2.3, 3.1, 4.7, 6.2, 7.8, 9.4 ],
    phase: @[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ],
    amplitude: @[1.0, 0.9, 0.7, 0.6, 0.4, 0.3, 0.2, 0.15],
    decayTime: @[8.0, 7.5, 7.0, 6.5, 6.0, 5.5, 5.0, 4.5 ]
    )

    let malletModes* = Mode(
    partial: @[1.0, 2.0, 3.0, 4.0, 5.0 , 6.0, 7.0 , 8.0],
    phase: @[0.0, 0.0, 0.0, 0.0, 0.0 , 0.0, 0.0 , 0.0],
    amplitude: @[1.0, 0.7, 0.5, 0.4, 0.25, 0.2, 0.15, 0.1],
    decayTime: @[4.0, 3.5, 3.0, 2.5, 2.0 , 1.8, 1.5 , 1.2]
    )

    when isMainModule:
    var tickerTape = Ticker(tick: 0)
    var modalPool = initModalPool()

    proc tick*(): float =
    # Trigger new modal every 0.005 seconds (200ms)
    if tickerTape.tick.float32 mod (SRate * 1.5'f32) == 0:
    let fundamental = 220'f32
    #init randomized mode(s) here
    let amp = rand(0.5'f32..1.0'f32)
    modalPool.trigger(bellModes, fundamental, amp)
    inc tickerTape.tick
    return modalPool.next()

    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
    echo "Max simultaneous modals: ", MaxModalObjects

    while true:
    ss.sio.flushEvents
    let s = stdin.readLine
    if s == "q":
    break