Commit773a5a8dRecorded18 Apr 2026Repositorysigil-dsp

Merge branch 'feat/ks-primitive' (v0.2.0 Karplus-Strong primitive)

Changed
 RELEASES.md           |  25 +++++++++++++++++++++++++
 package.sgl           |   8 +++++---
 src/c/dsp.c           |  93 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/c/ks.c            | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/c/ks.h            |  43 +++++++++++++++++++++++++++++++++++++++++++
 src/sigil/dsp/osc.sgl |  10 +++++++++-
 6 files changed, 287 insertions(+), 4 deletions(-)
Diff

A merge. Shown against its first parent, so this is the effect of merging rather than the work of the branch.

RELEASES.mdadded
@@ -0,0 +1,25 @@
+1
# sigil-dsp Releases
+2
+3
## v0.2.0 — 2026-04-18
+4
+5
### Added
+6
- **Karplus-Strong plucked string** primitive exposed through `(sigil dsp osc)`:
+7
`make-ks freq decay excite-gain`, `ks-compute`, `ks-trigger!`,
+8
`ks-set-freq!`, `ks-set-decay!`, `ks-set-excite-gain!`.
+9
+10
Feedback delay-line physical model with a 2-tap averaging lowpass in
+11
the feedback path. Per-trip gain derived from the requested -60 dB
+12
decay time. Delay line is sized for the lowest supported fundamental
+13
(20 Hz) at creation, so retuning upward via `ks-set-freq!` is a
+14
pointer trick, not a realloc. Rising-edge gate retriggers
+15
excitation — callers driving a continuous gate can invoke
+16
`ks-trigger!` manually per note.
+17
+18
Exposed struct layout (`src/c/ks.h`) stays public so downstream
+19
render loops (motif) can pick up the node as a graph primitive.
+20
+21
## v0.1.0 — 2026-03-26
+22
+23
Initial release. SoundPipe-backed oscillators, filters, envelopes,
+24
effects, noise generators, and utility modules exposed through
+25
`(sigil dsp {core,osc,filter,env,fx,noise,util})`.
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
#endif
src/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