Last active
June 12, 2025 09:02
-
-
Save ingoogni/4c85ff8add8bb1ea5214bbe38d00f626 to your computer and use it in GitHub Desktop.
Revisions
-
ingoogni revised this gist
Jun 9, 2025 . 1 changed file with 1 addition and 0 deletions.There are no files selected for viewing
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 charactersOriginal 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. -
ingoogni created this gist
Jun 9, 2025 .There are no files selected for viewing
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 charactersOriginal 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 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 charactersOriginal 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