Commit0854a04dRecorded21 Sep 2026Repositorysigil-dsp
dsp-init sets the context's rate on every call that gives one
Message
The nodes cache the rate when they are built (soundpipe's oscillators and filters at init, the fm4 voice, the chorus, the comp), so a program rendering at more than one rate in its life (motif's player reopened at 24000 on a slow phone, Crash The Stack's live soundtrack, 2026-09-21) built every later graph with the first rate's increments and played it at the new rate: an octave down at half the rate. A (dsp-init sr) before a graph is built now gives that graph sr; built graphs keep theirs; a call without a rate changes nothing once the context exists. The test entry checks a 440 Hz fm4 voice at 24000 and at 48000 both cross zero 880 times a second. Version 0.3.3.
Changed
RELEASES.md | 9 +++++++++
package.sgl | 2 +-
src/c/dsp.c | 17 +++++++++++++----
src/sigil/dsp/test-main.sgl | 22 ++++++++++++++++++++++
4 files changed, 45 insertions(+), 5 deletions(-)Diff
RELEASES.mdmodified
@@ -1,5 +1,14 @@
1
# sigil-dsp Releases 2
+3
## v0.3.3 — 2026-09-21+4
+5
One fix: `(dsp-init sr)` sets the shared context's sample rate on every call that gives one, not only the first. The nodes cache the rate when they are built (soundpipe's oscillators and filters at init, the fm4 voice, the chorus, the comp), so a program that renders at more than one rate in its life — motif's player opened at 48000 and then at 24000 when a slow phone asks for a cheaper render (Crash The Stack's live soundtrack) — built every later graph with the first rate's increments and played it at the new rate: a 24000 render sounded an octave down. Now a `(dsp-init sr)` before a graph is built gives that graph `sr`; graphs already built keep theirs. A call without a rate still changes nothing once the context exists. The test entry checks a 440 Hz fm4 voice built after `(dsp-init 24000)` and one after `(dsp-init 48000)` both cross zero 880 times in a second.+6
+7
## Upgrade notes+8
+9
No binding changed. A program that called `(dsp-init sr)` more than once with different rates and relied on the first winning gets the last one from now on.+10
+11
12
## v0.3.2 — 2026-09-18 13
14
This release gives motif the primitives for a fat, FM-heavy tracker sound: a real multi-operator FM voice instead of the two-operator `fosc`, and the three effects a demoscene mix is made of. Everything is plain C99, builds for wasm32-wasi as well as native, and renders the same samples on both.package.sglmodified
@@ -5,7 +5,7 @@
5
6
(package 7
name: "sigil-dsp"−8
version: "0.3.2"+8
version: "0.3.3" 9
sigil: "0.20" 10
description: "DSP primitives for Sigil (SoundPipe bindings)" 11
url: "https://codeberg.org/sigil/sigil-dsp"src/c/dsp.cmodified
@@ -257,14 +257,23 @@ static double as_number(Value v)
257
return sigil_as_flonum(v); 258
} 259
+260
/* (dsp-init [sample-rate])+261
*+262
* Creates the shared soundpipe context on the first call. A later call+263
* WITH a rate sets the context's rate for the nodes built from then on+264
* (nodes cache it at init: the fm4 voice, the chorus and the comp): a+265
* program that renders at more than one rate in its life (crash the+266
* stack's live soundtrack reopens its player at a lower rate when a+267
* device is slow, 2026-09-21) calls (dsp-init sr) before it builds a+268
* graph for that rate. Nodes already built keep the rate they were built+269
* at. A call without a rate changes nothing once the context exists. */ 270
static Value native_dsp_init(SigilVM *vm, int argc, Value *args) 271
{ 272
(void)vm;−263
if (g_sp) return SIGIL_TRUE;−264
−265
sp_create(&g_sp);+273
if (!g_sp) sp_create(&g_sp); 274
if (argc > 0) {−267
g_sp->sr = (int)as_number(args[0]);+275
int sr = (int)as_number(args[0]);+276
if (sr > 0) g_sp->sr = sr; 277
} 278
return SIGIL_TRUE; 279
}src/sigil/dsp/test-main.sglmodified
@@ -19,6 +19,28 @@
19
;; Core 20
(dsp-init 44100) 21
(display (string-append "Sample rate: " (number->string (dsp-sample-rate)) "\n"))+22
;; The rate follows a later (dsp-init sr): a node built after it+23
;; runs at that rate (fm4 caches the rate at init). 440 Hz for a+24
;; second of samples at 24000 and at 48000 both cross zero 880 times.+25
(display "\n--- The rate after a second dsp-init ---\n")+26
(let ((crossings (lambda (sr)+27
(dsp-init sr)+28
(let ((v (make-fm4 0 0.0 0.0 1.0)))+29
(fm4-set-op! v 0 1.0 1.0 0.0 0.0 1.0 0.0)+30
(let loop ((i 0) (prev 0.0) (z 0))+31
(if (= i sr) z+32
(let ((s (fm4-compute v 440.0 1.0 0.0 1.0)))+33
(loop (+ i 1) s (if (< (* s prev) 0.0) (+ z 1) z)))))))))+34
(let ((at-24000 (crossings 24000))+35
(at-48000 (crossings 48000)))+36
(display (string-append " rate now: " (number->string (dsp-sample-rate)) " (48000 expected)\n"))+37
(display (string-append " 440 Hz over a second: " (number->string at-24000) " crossings at 24000, "+38
(number->string at-48000) " at 48000 (880 expected of each)\n"))+39
(unless (and (= (dsp-sample-rate) 48000)+40
(<= (abs (- at-24000 880)) 4)+41
(<= (abs (- at-48000 880)) 4))+42
(display " FAIL: a node built after (dsp-init sr) does not run at sr\n")+43
(exit 1)))) 44
45
;; Oscillator 46
(display "\n--- Oscillators ---\n")