Skip to content

Instantly share code, notes, and snippets.

@ingoogni
Last active June 12, 2025 09:02
Show Gist options
  • Save ingoogni/4c85ff8add8bb1ea5214bbe38d00f626 to your computer and use it in GitHub Desktop.
Save ingoogni/4c85ff8add8bb1ea5214bbe38d00f626 to your computer and use it in GitHub Desktop.
Nim Modal synthesis
Modal synthesis in Nim with a pool for modes, so one excite fast and they still properly decay. Cheesy bell, gong & mallet modes.
#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
#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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment