Skip to content

Instantly share code, notes, and snippets.

@ingoogni
Last active June 19, 2025 12:19
Show Gist options
  • Save ingoogni/1e8f92ecc5472e8896ac1cecb8f4ab6a to your computer and use it in GitHub Desktop.
Save ingoogni/1e8f92ecc5472e8896ac1cecb8f4ab6a to your computer and use it in GitHub Desktop.
Iterative Karplus-Strong percussion in Nim
import os, random, math
import iterit
const
SampleRate {.intdefine.} = 44100
SRate* = SampleRate.float
MaxKSDelay* = (SampleRate / 20.0).int #Hz delay line length: SampleRate / Frequency
type
Ticker* = object
tick: uint
Trigger* = object
open: bool
interval: uint
toNext: uint
proc trigger*(interval: uint): iterator: Trigger =
var count: uint = 0
return iterator(): Trigger =
while true:
let cpos = count mod interval
let t = Trigger(
open: cpos == 0,
interval: interval,
toNext: interval - cpos
)
inc count
yield t
proc ksPercussion*[Tf, Tl, Tfltr, Tb: float or iterator: float](
freq: Tf, loss: Tl, fltr: Tfltr, blend: Tb,
excite: iterator: float,
trigger: iterator, #! don't put :Trigger here, otherwise the compiler nags. Why??
sampleRate: float = SRate
): iterator: float =
var
delayLine: array[MaxKSDelay, float] # Fixed len
isActive = false
lastNoiseInput, fltrCoeff, lossValue, halfLoss, output: float
writeIndex, dLineLen, sampleCounter, maxDuration: uint
trig: Trigger
return iterator(): float =
while true:
trig = trigger()
if trig.open:
isActive = true
dLineLen = round(sampleRate / freq.floatOrIter - 0.5).uint
sampleCounter = 0
writeIndex = 0
lastNoiseInput = 0.0
fltrCoeff = fltr.floatOrIter
lossValue = loss.floatOrIter
halfLoss = lossValue / 2.0
maxDuration = trig.interval
if isActive:
if sampleCounter < dLineLen:
# Gradually fill delay line with filtered noise
let noiseInput = excite()
let filteredNoise = (1.0 - fltrCoeff) * noiseInput + fltrCoeff * lastNoiseInput
lastNoiseInput = filteredNoise
delayLine[sampleCounter] = filteredNoise
output = filteredNoise
elif sampleCounter == dLineLen:
# First feedback sample
output = halfLoss * delayLine[0]
delayLine[0] = output
writeIndex = 1
else:
# Main K-S loop: read from positions that are dLineLen samples behind!
let
readPos1 = (sampleCounter - dLineLen) mod dLineLen
readPos2 = (sampleCounter - dLineLen + 1) mod dLineLen
sample1 = delayLine[readPos1]
sample2 = delayLine[readPos2]
p = if rand(1.0) <= blend.floatOrIter: 1 else: 0
# Blend calculation: positive or negative sum
sumSamples = sample2 + sample1
output = halfLoss * (float(1 - p) * sumSamples - float(p) * sumSamples)
let writePos = sampleCounter mod dLineLen
delayLine[writePos] = output
inc sampleCounter
# Simple duration-based cutoff
if sampleCounter > maxDuration:
isActive = false
else:
output = 0.0
yield output
proc whiteNoise*(): iterator: float =
randomize(7)
return iterator(): float =
while true:
yield rand(-1.0..1.0)
proc sinOsc*[Tf, Tp, Ta: float or iterator:float](
freq:Tf, phase:Tp, amp:Ta, sampleRate:float = SRate
): iterator(): float =
var
tick, lastFreq, phaseCorrection:float
let increment = TAU/sampleRate
return iterator(): float {.inline.}=
while true:
let
f = freq.floatOrIter
p = phase.floatOrIter
a = amp.floatOrIter
phaseCorrection += (lastFreq - f) * (tick)
lastFreq = f
yield a * sin((tick * f) + phaseCorrection + p)
tick += increment
proc rndIter*(min, max: float): iterator: float =
randomize(42)
return iterator(): float =
while true:
yield rand(min..max)
when isMainModule:
let
freq = rndIter(25.0, 50.0)
loss = rndIter(0.1, 0.9)
fltr = rndIter(0.1, 0.9)
blend = rndIter(0.1, 0.9)
excite = (whiteNoise() + sinOsc(freq * 4.0, Pi / 2.0, 3.0)) / 3.0
trig = trigger((1 * SampleRate).uint)
ksp = ksPercussion(
freq = freq,
loss = loss,
fltr = fltr,
blend = blend,
excite = excite,
trigger = trig
)
proc tick*(): float =
return ksp()
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