Commitb7f8fe2dRecorded18 Apr 2026Repositorysigil-dsp
Add Karplus-Strong plucked-string primitive (make-ks)
Message
- New src/c/ks.c / ks.h: delay-line physical model with 2-tap averaging lowpass in the feedback path. Per-trip gain derived from the -60 dB decay time; buffer sized for lowest supported fundamental (20 Hz) so retuning upward is a pointer trick, not a realloc. - Bindings exported from (sigil dsp osc): make-ks, ks-compute, ks-trigger!, ks-set-freq!, ks-set-decay!, ks-set-excite-gain!. - Gate input drives rising-edge retrigger, so the node slots into existing graph-style voicing without needing a separate trigger input. - Version bump to 0.2.0.
Validated via motif's test-ks subcommand (shipped in the motif patch series on feat/parameterized-synths-and-ks): pitch within ±2 Hz, amplitude below 10% of peak by target decay time, retune tracks.
Changed
package.sgl | 8 +++++---
src/c/dsp.c | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/c/ks.c | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/c/ks.h | 43 +++++++++++++++++++++++++++++++++++++++++++
src/sigil/dsp/osc.sgl | 10 +++++++++-
5 files changed, 262 insertions(+), 4 deletions(-)Diff
package.sglmodified
@@ -5,7 +5,7 @@
5
6
(package 7
name: "sigil-dsp"−8
version: "0.1.0"+8
version: "0.2.0" 9
description: "DSP primitives for Sigil (SoundPipe bindings)" 10
url: "https://codeberg.org/sigil/sigil-dsp" 11
license: "MIT"@@ -88,7 +88,8 @@
88
;; dr_wav implementation 89
"vendor/soundpipe/lib/dr_wav/dr_wav.c" 90
;; Native bindings−91
"src/c/dsp.c")+91
"src/c/dsp.c"+92
"src/c/ks.c") 93
c-include-dirs: '("vendor/soundpipe/h" 94
"vendor/soundpipe/lib/faust" 95
"vendor/soundpipe/lib/dr_wav")@@ -147,7 +148,8 @@
148
"vendor/soundpipe/modules/crossfade.c" 149
"vendor/soundpipe/modules/wavout.c" 150
"vendor/soundpipe/lib/dr_wav/dr_wav.c"−150
"src/c/dsp.c")+151
"src/c/dsp.c"+152
"src/c/ks.c") 153
include-dirs: '("../sigil/packages/sigil-lib/include" 154
"../sigil/packages/sigil-lib/src" 155
"vendor/soundpipe/h"src/c/dsp.cmodified
@@ -13,6 +13,7 @@
13
#include <math.h> 14
15
#include "soundpipe.h"+16
#include "ks.h" 17
18
/* ============================================================ 19
* GLOBAL STATE@@ -62,6 +63,7 @@ static Value tt_clamp = SIGIL_UNDEFINED;
63
static Value tt_crossfade = SIGIL_UNDEFINED; 64
static Value tt_wavout = SIGIL_UNDEFINED; 65
static Value tt_ftbl = SIGIL_UNDEFINED;+66
static Value tt_ks = SIGIL_UNDEFINED; 67
68
/* ============================================================ 69
* HELPERS@@ -160,6 +162,7 @@ static void destroy_clamp(void *data) { sp_clamp *p = (sp_clamp *)data; sp_clamp
162
static void destroy_crossfade(void *data) { sp_crossfade *p = (sp_crossfade *)data; sp_crossfade_destroy(&p); } 163
static void destroy_wavout(void *data) { sp_wavout *p = (sp_wavout *)data; sp_wavout_destroy(&p); } 164
static void destroy_ftbl(void *data) { sp_ftbl *p = (sp_ftbl *)data; sp_ftbl_destroy(&p); }+165
static void destroy_ks(void *data) { sp_ks *p = (sp_ks *)data; sp_ks_destroy(&p); } 166
167
/* ============================================================ 168
* TYPE GETTERS@@ -226,6 +229,7 @@ DEF_GETTER(scale, sp_scale, tt_scale, "scale")
229
DEF_GETTER(clamp, sp_clamp, tt_clamp, "clamp") 230
DEF_GETTER(crossfade, sp_crossfade, tt_crossfade, "crossfade") 231
DEF_GETTER(wavout, sp_wavout, tt_wavout, "wavout")+232
DEF_GETTER(ks, sp_ks, tt_ks, "ks") 233
234
/* ============================================================ 235
* CORE — dsp-init, dsp-shutdown, dsp-sample-rate@@ -1263,6 +1267,80 @@ static Value native_brown_compute(SigilVM *vm, int argc, Value *args)
1267
return sigil_flonum((double)out); 1268
} 1269
+1270
/* ============================================================+1271
* KARPLUS-STRONG+1272
* ============================================================ */+1273
+1274
/* (make-ks freq decay excite-gain) -> <ks> */+1275
static Value native_make_ks(SigilVM *vm, int argc, Value *args)+1276
{+1277
(void)argc;+1278
CHECK_SP(vm, "make-ks");+1279
SPFLOAT freq = (SPFLOAT)sigil_as_flonum(args[0]);+1280
SPFLOAT decay = (SPFLOAT)sigil_as_flonum(args[1]);+1281
SPFLOAT gain = (SPFLOAT)sigil_as_flonum(args[2]);+1282
sp_ks *p;+1283
sp_ks_create(&p);+1284
sp_ks_init(g_sp, p, freq, decay, gain);+1285
ENSURE_TAG(vm, tt_ks, "dsp-ks");+1286
return sigil_make_foreign(vm, tt_ks, p, destroy_ks,+1287
sizeof(sp_ks) + (size_t)p->bufcap * sizeof(SPFLOAT));+1288
}+1289
+1290
/* (ks-compute ks gate) -> flonum */+1291
static Value native_ks_compute(SigilVM *vm, int argc, Value *args)+1292
{+1293
(void)argc;+1294
sp_ks *p = get_ks(vm, args[0]);+1295
if (!p) { sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "ks-compute: expected ks"); return SIGIL_FALSE; }+1296
SPFLOAT gate = (SPFLOAT)sigil_as_flonum(args[1]);+1297
SPFLOAT out = 0.0f;+1298
sp_ks_compute(g_sp, p, &gate, &out);+1299
return sigil_flonum((double)out);+1300
}+1301
+1302
/* (ks-trigger! ks) — manual excitation */+1303
static Value native_ks_trigger(SigilVM *vm, int argc, Value *args)+1304
{+1305
(void)argc;+1306
sp_ks *p = get_ks(vm, args[0]);+1307
if (!p) { sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "ks-trigger!: expected ks"); return SIGIL_FALSE; }+1308
sp_ks_trigger(g_sp, p);+1309
return SIGIL_NIL;+1310
}+1311
+1312
/* (ks-set-freq! ks freq) */+1313
static Value native_ks_set_freq(SigilVM *vm, int argc, Value *args)+1314
{+1315
(void)argc;+1316
sp_ks *p = get_ks(vm, args[0]);+1317
if (!p) { sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "ks-set-freq!: expected ks"); return SIGIL_FALSE; }+1318
p->freq = (SPFLOAT)sigil_as_flonum(args[1]);+1319
sp_ks_retune(g_sp, p);+1320
return SIGIL_NIL;+1321
}+1322
+1323
/* (ks-set-decay! ks decay) */+1324
static Value native_ks_set_decay(SigilVM *vm, int argc, Value *args)+1325
{+1326
(void)argc;+1327
sp_ks *p = get_ks(vm, args[0]);+1328
if (!p) { sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "ks-set-decay!: expected ks"); return SIGIL_FALSE; }+1329
p->decay = (SPFLOAT)sigil_as_flonum(args[1]);+1330
sp_ks_retune(g_sp, p);+1331
return SIGIL_NIL;+1332
}+1333
+1334
/* (ks-set-excite-gain! ks gain) */+1335
static Value native_ks_set_excite_gain(SigilVM *vm, int argc, Value *args)+1336
{+1337
(void)argc;+1338
sp_ks *p = get_ks(vm, args[0]);+1339
if (!p) { sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "ks-set-excite-gain!: expected ks"); return SIGIL_FALSE; }+1340
p->excite_gain = (SPFLOAT)sigil_as_flonum(args[1]);+1341
return SIGIL_NIL;+1342
}+1343
1344
/* ============================================================ 1345
* DYNAMICS / WAVESHAPING 1346
* ============================================================ */@@ -1741,6 +1819,19 @@ void sigil__init_sigil_dsp_module(SigilVM *vm)
1819
sigil_module_register_native(vm, "phasor-set-freq!", native_phasor_set_freq, 1820
SIGIL_ARITY_EXACT(2), "Set phasor frequency"); 1821
+1822
sigil_module_register_native(vm, "make-ks", native_make_ks,+1823
SIGIL_ARITY_EXACT(3), "Create Karplus-Strong (freq decay excite-gain)");+1824
sigil_module_register_native(vm, "ks-compute", native_ks_compute,+1825
SIGIL_ARITY_EXACT(2), "Compute Karplus-Strong (ks gate) -> sample");+1826
sigil_module_register_native(vm, "ks-trigger!", native_ks_trigger,+1827
SIGIL_ARITY_EXACT(1), "Excite Karplus-Strong with noise burst");+1828
sigil_module_register_native(vm, "ks-set-freq!", native_ks_set_freq,+1829
SIGIL_ARITY_EXACT(2), "Set KS fundamental frequency");+1830
sigil_module_register_native(vm, "ks-set-decay!", native_ks_set_decay,+1831
SIGIL_ARITY_EXACT(2), "Set KS -60 dB decay time");+1832
sigil_module_register_native(vm, "ks-set-excite-gain!", native_ks_set_excite_gain,+1833
SIGIL_ARITY_EXACT(2), "Set KS excitation amplitude");+1834
1835
/* Export all osc bindings */ 1836
const char *osc_exports[] = { 1837
"make-osc", "make-osc-ft", "osc-compute", "osc-set-freq!", "osc-set-amp!",@@ -1750,6 +1841,8 @@ void sigil__init_sigil_dsp_module(SigilVM *vm)
1841
"make-bltriangle", "bltriangle-compute", "bltriangle-set-freq!", "bltriangle-set-amp!", 1842
"make-fosc", "fosc-compute", "fosc-set-freq!", "fosc-set-index!", "fosc-set-amp!", 1843
"make-phasor", "phasor-compute", "phasor-set-freq!",+1844
"make-ks", "ks-compute", "ks-trigger!",+1845
"ks-set-freq!", "ks-set-decay!", "ks-set-excite-gain!", 1846
NULL 1847
}; 1848
for (int i = 0; osc_exports[i]; i++) sigil_module_export(vm, osc_exports[i]);src/c/ks.cadded
@@ -0,0 +1,112 @@
+1
/*+2
* ks.c - Karplus-Strong plucked string+3
*+4
* Buffer sized from a ceiling on the lowest intended fundamental+5
* (~20 Hz) so retuning up to any audible pitch is a pointer trick,+6
* not a reallocation.+7
*/+8
+9
#include "ks.h"+10
#include <stdlib.h>+11
#include <string.h>+12
#include <math.h>+13
+14
#define KS_MIN_FREQ 20.0f+15
+16
static uint32_t xorshift32(uint32_t *s)+17
{+18
uint32_t x = *s;+19
x ^= x << 13;+20
x ^= x >> 17;+21
x ^= x << 5;+22
*s = x ? x : 0x1u;+23
return *s;+24
}+25
+26
static SPFLOAT ks_noise(sp_ks *p)+27
{+28
uint32_t r = xorshift32(&p->rng);+29
/* map to [-1, 1] */+30
return ((SPFLOAT)(int32_t)r) * (1.0f / 2147483648.0f);+31
}+32
+33
int sp_ks_create(sp_ks **p)+34
{+35
*p = (sp_ks *)calloc(1, sizeof(sp_ks));+36
return SP_OK;+37
}+38
+39
int sp_ks_destroy(sp_ks **p)+40
{+41
if (!*p) return SP_OK;+42
free((*p)->buf);+43
free(*p);+44
*p = NULL;+45
return SP_OK;+46
}+47
+48
int sp_ks_init(sp_data *sp, sp_ks *p, SPFLOAT freq, SPFLOAT decay,+49
SPFLOAT excite_gain)+50
{+51
if (freq < KS_MIN_FREQ) freq = KS_MIN_FREQ;+52
if (decay <= 0.0f) decay = 0.5f;+53
p->freq = freq;+54
p->decay = decay;+55
p->excite_gain = excite_gain;+56
p->bufcap = (int)ceilf((SPFLOAT)sp->sr / KS_MIN_FREQ) + 2;+57
p->buf = (SPFLOAT *)calloc(p->bufcap, sizeof(SPFLOAT));+58
p->bufpos = 0;+59
p->last = 0.0f;+60
p->gate_prev = 0;+61
p->rng = 0xC0FFEE01u;+62
sp_ks_retune(sp, p);+63
return SP_OK;+64
}+65
+66
int sp_ks_retune(sp_data *sp, sp_ks *p)+67
{+68
SPFLOAT f = p->freq < KS_MIN_FREQ ? KS_MIN_FREQ : p->freq;+69
int n = (int)lroundf((SPFLOAT)sp->sr / f);+70
if (n < 2) n = 2;+71
if (n > p->bufcap) n = p->bufcap;+72
p->bufsize = n;+73
/* Per-trip gain so signal decays to -60 dB after `decay` seconds.+74
* After T seconds the loop runs T*freq times, want g^(T*freq)=1e-3. */+75
SPFLOAT d = p->decay > 0.0f ? p->decay : 0.001f;+76
SPFLOAT g = powf(10.0f, -3.0f / (d * f));+77
if (g > 0.99995f) g = 0.99995f;+78
if (g < 0.0f) g = 0.0f;+79
p->feedback = g;+80
if (p->bufpos >= p->bufsize) p->bufpos = 0;+81
return SP_OK;+82
}+83
+84
int sp_ks_trigger(sp_data *sp, sp_ks *p)+85
{+86
(void)sp;+87
SPFLOAT amp = p->excite_gain;+88
for (int i = 0; i < p->bufsize; i++)+89
p->buf[i] = ks_noise(p) * amp;+90
p->bufpos = 0;+91
p->last = 0.0f;+92
return SP_OK;+93
}+94
+95
int sp_ks_compute(sp_data *sp, sp_ks *p, SPFLOAT *gate, SPFLOAT *out)+96
{+97
/* Rising-edge retrigger on gate */+98
int g_now = (*gate > 0.5f) ? 1 : 0;+99
if (g_now && !p->gate_prev) sp_ks_trigger(sp, p);+100
p->gate_prev = g_now;+101
+102
SPFLOAT y = p->buf[p->bufpos];+103
/* 2-tap averaging lowpass in feedback */+104
SPFLOAT avg = 0.5f * (y + p->last);+105
SPFLOAT newval = avg * p->feedback;+106
p->buf[p->bufpos] = newval;+107
p->last = y;+108
p->bufpos++;+109
if (p->bufpos >= p->bufsize) p->bufpos = 0;+110
*out = y;+111
return SP_OK;+112
}src/c/ks.hadded
@@ -0,0 +1,43 @@
+1
/*+2
* ks.h - Karplus-Strong plucked string primitive+3
*+4
* Feedback delay-line physical model. Delay length N = round(sr/freq).+5
* Each sample: averaging 2-tap lowpass in the feedback loop; per-trip+6
* gain set from the requested -60 dB decay time.+7
*+8
* Self-contained (no SoundPipe dependency beyond SPFLOAT and sp_data)+9
* so motif can duplicate the struct in its render loop.+10
*/+11
+12
#ifndef SIGIL_DSP_KS_H+13
#define SIGIL_DSP_KS_H+14
+15
#include "soundpipe.h"+16
+17
typedef struct sp_ks {+18
SPFLOAT freq; /* fundamental frequency (Hz) */+19
SPFLOAT decay; /* -60 dB decay time (seconds) */+20
SPFLOAT excite_gain; /* excitation amplitude (0..1) */+21
SPFLOAT feedback; /* per-trip feedback coefficient (derived) */+22
SPFLOAT *buf; /* delay line */+23
int bufsize; /* current active length (samples) */+24
int bufcap; /* allocated capacity */+25
int bufpos; /* read/write cursor */+26
SPFLOAT last; /* last output, for 2-tap averaging LP */+27
int gate_prev; /* previous gate sample for edge detection */+28
uint32_t rng; /* xorshift state for excitation noise */+29
} sp_ks;+30
+31
int sp_ks_create(sp_ks **p);+32
int sp_ks_destroy(sp_ks **p);+33
int sp_ks_init(sp_data *sp, sp_ks *p, SPFLOAT freq, SPFLOAT decay,+34
SPFLOAT excite_gain);+35
/* Recompute feedback coefficient + active bufsize from freq/decay. */+36
int sp_ks_retune(sp_data *sp, sp_ks *p);+37
/* Fill the delay line with noise * excite_gain. */+38
int sp_ks_trigger(sp_data *sp, sp_ks *p);+39
/* Per-sample step. `gate` used for rising-edge retrigger; pass 1.0 to+40
* disable retriggering (and call sp_ks_trigger manually). */+41
int sp_ks_compute(sp_data *sp, sp_ks *p, SPFLOAT *gate, SPFLOAT *out);+42
+43
#endifsrc/sigil/dsp/osc.sglmodified
@@ -18,7 +18,9 @@
18
make-blsquare blsquare-compute blsquare-set-freq! blsquare-set-amp! 19
make-bltriangle bltriangle-compute bltriangle-set-freq! bltriangle-set-amp! 20
make-fosc fosc-compute fosc-set-freq! fosc-set-index! fosc-set-amp!−21
make-phasor phasor-compute phasor-set-freq!)+21
make-phasor phasor-compute phasor-set-freq!+22
make-ks ks-compute ks-trigger!+23
ks-set-freq! ks-set-decay! ks-set-excite-gain!) 24
25
(begin 26
(define-native make-osc)@@ -50,6 +52,12 @@
52
(define-native make-phasor) 53
(define-native (phasor-compute phasor)) 54
(define-native (phasor-set-freq! phasor freq))+55
(define-native (make-ks freq decay excite-gain))+56
(define-native (ks-compute ks gate))+57
(define-native (ks-trigger! ks))+58
(define-native (ks-set-freq! ks freq))+59
(define-native (ks-set-decay! ks decay))+60
(define-native (ks-set-excite-gain! ks gain)) 61
62
;;; Create an oscillator by type. 63
;;; Types: sine, saw, square, triangle, fm