Last active
June 14, 2025 06:47
-
-
Save ingoogni/07249371a39b506b8a5fb0daff304b5f to your computer and use it in GitHub Desktop.
Nim Foogle filter
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
# 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 |
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 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