Last active
June 19, 2025 12:19
-
-
Save ingoogni/1e8f92ecc5472e8896ac1cecb8f4ab6a to your computer and use it in GitHub Desktop.
Iterative Karplus-Strong percussion in Nim
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 characters
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