Skip to content

Instantly share code, notes, and snippets.

@ingoogni
Last active June 14, 2025 06:47
Show Gist options
  • Save ingoogni/07249371a39b506b8a5fb0daff304b5f to your computer and use it in GitHub Desktop.
Save ingoogni/07249371a39b506b8a5fb0daff304b5f to your computer and use it in GitHub Desktop.
Nim Foogle filter
# https://www.musicdsp.org/en/latest/Synthesis/11-weird-synthesis.html
# "Karplus-Strong-inspired comb filter effect?"
# Processes incoming audio continuously, not just initial excitation
import std/[math, random]
import iterit
import iteroscillator
const
SampleRate {.intdefine.} = 44100
SRate* = SampleRate.float
type
Ticker* = object
tick: uint
Foogler* = ref object
delayLine: array[256, float]
writePos: int
# Tap positions in delay line
tap1Pos: float
tap2Pos: float
# Parameters
feedback: float # 0.0 to 0.999
dampingCutoff: float # Low-pass filter cutoff frequency
# State for hf damping
filterState: float
filterCoeff: float
proc initFoogler*(sampleRate: float = SRate): Foogler =
## "Foogler" delay effect
result = Foogler(
writePos: 0,
tap1Pos: 0.3, # 30% into delay line
tap2Pos: 0.7, # 70% into delay line
feedback: 0.5, # 50% feedback
dampingCutoff: 8000.0, # 8kHz cutoff
filterState: 0.0
)
# Filter coefficient for simple 1-pole low-pass
# cutoff = sampleRate * coeff / (2 * PI)
result.filterCoeff = 2.0 * PI * result.dampingCutoff / sampleRate
if result.filterCoeff > 1.0:
result.filterCoeff = 1.0
proc setTapPositions*(f: var Foogler, tap1, tap2: float) =
## Set the positions of the two taps (0.0 to 1.0)
f.tap1Pos = clamp(tap1, 0.0, 1.0)
f.tap2Pos = clamp(tap2, 0.0, 1.0)
proc setFeedback*(f: var Foogler, feedback: float) =
## Set feedback amount (0.0 to 0.999)
f.feedback = clamp(feedback, 0.0, 0.999)
proc setDamping*(f: var Foogler, cutoffHz: float, sampleRate: float = SRate) =
## Set the high-frequency damping cutoff
f.dampingCutoff = cutoffHz
f.filterCoeff = 2.0 * PI * cutoffHz / sampleRate
if f.filterCoeff > 1.0:
f.filterCoeff = 1.0
proc readTap(f: Foogler, tapPos: float): float =
## Read from delay line at specified tap position
let delaySamples = tapPos * 255.0 # 0-255 range
let intDelay = int(delaySamples)
let fracDelay = delaySamples - float(intDelay)
# Read positions with wraparound
let readPos1 = (f.writePos - intDelay - 1 + 256) mod 256
let readPos2 = (f.writePos - intDelay - 2 + 256) mod 256
# Linear interpolation, fractional delay
let sample1 = f.delayLine[readPos1]
let sample2 = f.delayLine[readPos2]
return sample1 * (1.0 - fracDelay) + sample2 * fracDelay
proc process*(input: float, f: Foogler): float =
## Process a single sample through the Foogler
let tap1Output = f.readTap(f.tap1Pos)
let tap2Output = f.readTap(f.tap2Pos)
let tapsOutput = (tap1Output + tap2Output) * 0.5
# simple 1-pole low-pass
f.filterState = f.filterState + f.filterCoeff * (tapsOutput - f.filterState)
let feedbackSample = f.filterState * f.feedback
# Write to delay line (input + feedback)
f.delayLine[f.writePos] = input + feedbackSample
f.writePos = (f.writePos + 1) mod 256
return f.filterState
proc foogle*(input, tap1, tap2, feedback, cutoffHz: iterator: float or float, f: Foogler): iterator(): float =
return iterator(): float =
while true:
# add something for control rate here, if needed, when parameter is iterator
f.setTapPositions(tap1.floatOrIter, tap2.floatOrIter)
f.setFeedback(feedback.floatOrIter)
f.setDamping(cutOffHz.floatOrIter)
yield input.floatOrIter.process(f)
proc foogle*(input: iterator: float or float, f: Foogler): iterator(): float =
return iterator(): float =
while true:
yield input.floatOrIter.process(f)
when isMainModule:
import strformat
var tickerTape = Ticker(tick: 0)
var foogler = initFoogler(SRate)
foogler.setTapPositions(0.09, 0.11)
foogler.setFeedback(0.999)
foogler.setDamping(6000.0, SRate)
echo "\nFoogler parameters:"
echo fmt"Tap 1 position: {foogler.tap1Pos:.2f}"
echo fmt"Tap 2 position: {foogler.tap2Pos:.2f}"
echo fmt"Feedback: {foogler.feedback:.2f}"
echo fmt"Damping cutoff: {foogler.dampingCutoff:.0f} Hz"
let input1 = pingOsc(30.0, 0.0, 5.0)
let sinput = sinOsc(70.0, 0.0, 1.0)
let input = sawOsc(120.0, 0.0, 1.0) + input1 + sinput
let pr = input.foogle(foogler)
# rather chaotic
proc tick*(): float =
if tickerTape.tick mod 10000 == 0:
foogler.setTapPositions(rand(0.09..0.49), rand(0.50..0.90))
foogler.setFeedback(rand(0.6..0.9999))
inc tickerTape.tick
let v = pr()
return v
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
#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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment