Commit55f36437Recorded18 Sep 2026Repositorysigil-dsp

Add the fm4 four-operator FM voice and the chorus, crush and comp nodes

Message

fm4 is a DX/OPL-style phase-modulation voice: four sine operators from a 4096-entry table, an eight-entry algorithm table (stack, two-into-one, branch, three-into-one, pairs, stack-plus-one, one-into-three, additive), per-operator ratio, level and linear ADSR, feedback on operator 4 as the average of its last two outputs, a global index that is a per-sample argument, and a velocity-to-index curve. A gate rising edge resets phases and restarts the envelopes.

chorus is two triangle-LFO taps on one delay line; crush is tanh drive, bit-depth quantisation and sample-and-hold rate reduction; comp is a feed-forward peak compressor with one-pole attack and release. Every parameter a tracker effects column would move is a compute-call argument.

Bindings live in (sigil dsp osc) and (sigil dsp fx) with specs and docstrings.

Changed
 src/c/dsp.c           | 214 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/c/fm4.c           | 212 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/c/fm4.h           |  83 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/c/fx4.c           | 198 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/c/fx4.h           |  81 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/sigil/dsp/fx.sgl  |  31 +++++++++++++++++++++++--
 src/sigil/dsp/osc.sgl |  30 +++++++++++++++++++++++-
 7 files changed, 846 insertions(+), 3 deletions(-)
Diff
src/c/dsp.cmodified
@@ -14,6 +14,8 @@
14
15
#include "soundpipe.h"
16
#include "ks.h"
+17
#include "fm4.h"
+18
#include "fx4.h"
19
20
/* ============================================================
21
* GLOBAL STATE
@@ -64,6 +66,10 @@ static Value tt_crossfade = SIGIL_UNDEFINED;
66
static Value tt_wavout = SIGIL_UNDEFINED;
67
static Value tt_ftbl = SIGIL_UNDEFINED;
68
static Value tt_ks = SIGIL_UNDEFINED;
+69
static Value tt_fm4 = SIGIL_UNDEFINED;
+70
static Value tt_chorus = SIGIL_UNDEFINED;
+71
static Value tt_crush = SIGIL_UNDEFINED;
+72
static Value tt_comp = SIGIL_UNDEFINED;
73
74
/* ============================================================
75
* HELPERS
@@ -163,6 +169,10 @@ static void destroy_crossfade(void *data) { sp_crossfade *p = (sp_crossfade *)da
169
static void destroy_wavout(void *data) { sp_wavout *p = (sp_wavout *)data; sp_wavout_destroy(&p); }
170
static void destroy_ftbl(void *data) { sp_ftbl *p = (sp_ftbl *)data; sp_ftbl_destroy(&p); }
171
static void destroy_ks(void *data) { sp_ks *p = (sp_ks *)data; sp_ks_destroy(&p); }
+172
static void destroy_fm4(void *data) { sp_fm4 *p = (sp_fm4 *)data; sp_fm4_destroy(&p); }
+173
static void destroy_chorus(void *data) { sp_chorus *p = (sp_chorus *)data; sp_chorus_destroy(&p); }
+174
static void destroy_crush(void *data) { sp_crush *p = (sp_crush *)data; sp_crush_destroy(&p); }
+175
static void destroy_comp(void *data) { sp_comp *p = (sp_comp *)data; sp_comp_destroy(&p); }
176
177
/* ============================================================
178
* TYPE GETTERS
@@ -230,6 +240,10 @@ DEF_GETTER(clamp, sp_clamp, tt_clamp, "clamp")
240
DEF_GETTER(crossfade, sp_crossfade, tt_crossfade, "crossfade")
241
DEF_GETTER(wavout, sp_wavout, tt_wavout, "wavout")
242
DEF_GETTER(ks, sp_ks, tt_ks, "ks")
+243
DEF_GETTER(fm4, sp_fm4, tt_fm4, "fm4")
+244
DEF_GETTER(chorus, sp_chorus, tt_chorus, "chorus")
+245
DEF_GETTER(crush, sp_crush, tt_crush, "crush")
+246
DEF_GETTER(comp, sp_comp, tt_comp, "comp")
247
248
/* ============================================================
249
* CORE — dsp-init, dsp-shutdown, dsp-sample-rate
@@ -1341,6 +1355,176 @@ static Value native_ks_set_excite_gain(SigilVM *vm, int argc, Value *args)
1355
return SIGIL_NIL;
1356
}
1357
+1358
/* ============================================================
+1359
* FM4 — four-operator FM voice (fm4.h)
+1360
* ============================================================ */
+1361
+1362
#define FM4_GET(vm, args, name) \
+1363
sp_fm4 *p = get_fm4(vm, args[0]); \
+1364
if (!p) { sigil__vm_set_error(vm, SIGIL_ERR_TYPE, name ": expected fm4"); return SIGIL_FALSE; }
+1365
+1366
/* (make-fm4 algorithm feedback vel-depth vel-curve) */
+1367
static Value native_make_fm4(SigilVM *vm, int argc, Value *args)
+1368
{
+1369
(void)argc;
+1370
CHECK_SP(vm, "make-fm4");
+1371
int alg = (int)as_number(args[0]);
+1372
if (alg < 0 || alg >= SP_FM4_ALGORITHMS) {
+1373
sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME, "make-fm4: algorithm out of range 0..7");
+1374
return SIGIL_FALSE;
+1375
}
+1376
sp_fm4 *p;
+1377
sp_fm4_create(&p);
+1378
sp_fm4_init(g_sp, p);
+1379
sp_fm4_set_algorithm(p, alg);
+1380
p->feedback = (SPFLOAT)as_number(args[1]);
+1381
p->vel_depth = (SPFLOAT)as_number(args[2]);
+1382
p->vel_curve = (SPFLOAT)as_number(args[3]);
+1383
ENSURE_TAG(vm, tt_fm4, "dsp-fm4");
+1384
return sigil_make_foreign(vm, tt_fm4, p, destroy_fm4, sizeof(sp_fm4));
+1385
}
+1386
+1387
/* (fm4-set-op! fm4 op ratio level attack decay sustain release) — op is 0..3 */
+1388
static Value native_fm4_set_op(SigilVM *vm, int argc, Value *args)
+1389
{
+1390
(void)argc;
+1391
FM4_GET(vm, args, "fm4-set-op!");
+1392
int op = (int)as_number(args[1]);
+1393
if (op < 0 || op >= SP_FM4_OPS) {
+1394
sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME, "fm4-set-op!: operator out of range 0..3");
+1395
return SIGIL_FALSE;
+1396
}
+1397
sp_fm4_set_op(p, op,
+1398
(SPFLOAT)as_number(args[2]), (SPFLOAT)as_number(args[3]),
+1399
(SPFLOAT)as_number(args[4]), (SPFLOAT)as_number(args[5]),
+1400
(SPFLOAT)as_number(args[6]), (SPFLOAT)as_number(args[7]));
+1401
return SIGIL_NIL;
+1402
}
+1403
+1404
/* (fm4-set-algorithm! fm4 algorithm) */
+1405
static Value native_fm4_set_algorithm(SigilVM *vm, int argc, Value *args)
+1406
{
+1407
(void)argc;
+1408
FM4_GET(vm, args, "fm4-set-algorithm!");
+1409
if (sp_fm4_set_algorithm(p, (int)as_number(args[1])) != SP_OK) {
+1410
sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME, "fm4-set-algorithm!: algorithm out of range 0..7");
+1411
return SIGIL_FALSE;
+1412
}
+1413
return SIGIL_NIL;
+1414
}
+1415
+1416
/* (fm4-set-feedback! fm4 feedback) */
+1417
static Value native_fm4_set_feedback(SigilVM *vm, int argc, Value *args)
+1418
{
+1419
(void)argc;
+1420
FM4_GET(vm, args, "fm4-set-feedback!");
+1421
p->feedback = (SPFLOAT)as_number(args[1]);
+1422
return SIGIL_NIL;
+1423
}
+1424
+1425
/* (fm4-compute fm4 freq gate index velocity) -> flonum */
+1426
static Value native_fm4_compute(SigilVM *vm, int argc, Value *args)
+1427
{
+1428
(void)argc;
+1429
FM4_GET(vm, args, "fm4-compute");
+1430
SPFLOAT out = 0.0f;
+1431
sp_fm4_compute(g_sp, p,
+1432
(SPFLOAT)as_number(args[1]), (SPFLOAT)as_number(args[2]),
+1433
(SPFLOAT)as_number(args[3]), (SPFLOAT)as_number(args[4]),
+1434
&out);
+1435
return sigil_flonum((double)out);
+1436
}
+1437
+1438
/* (fm4-algorithm-name algorithm) -> string */
+1439
static Value native_fm4_algorithm_name(SigilVM *vm, int argc, Value *args)
+1440
{
+1441
(void)argc;
+1442
int alg = (int)as_number(args[0]);
+1443
if (alg < 0 || alg >= SP_FM4_ALGORITHMS) return SIGIL_FALSE;
+1444
const char *name = sp_fm4_algorithm_table()[alg].name;
+1445
return sigil_make_string(vm, name, strlen(name));
+1446
}
+1447
+1448
/* ============================================================
+1449
* FATTENERS — chorus, crush, compressor (fx4.h)
+1450
* ============================================================ */
+1451
+1452
/* (make-chorus) */
+1453
static Value native_make_chorus(SigilVM *vm, int argc, Value *args)
+1454
{
+1455
(void)argc; (void)args;
+1456
CHECK_SP(vm, "make-chorus");
+1457
sp_chorus *p;
+1458
sp_chorus_create(&p);
+1459
sp_chorus_init(g_sp, p);
+1460
ENSURE_TAG(vm, tt_chorus, "dsp-chorus");
+1461
return sigil_make_foreign(vm, tt_chorus, p, destroy_chorus,
+1462
sizeof(sp_chorus) + (size_t)p->bufsize * sizeof(SPFLOAT));
+1463
}
+1464
+1465
/* (chorus-compute chorus in rate depth-ms mix) -> flonum */
+1466
static Value native_chorus_compute(SigilVM *vm, int argc, Value *args)
+1467
{
+1468
(void)argc;
+1469
sp_chorus *p = get_chorus(vm, args[0]);
+1470
if (!p) { sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "chorus-compute: expected chorus"); return SIGIL_FALSE; }
+1471
SPFLOAT out = 0.0f;
+1472
sp_chorus_compute(g_sp, p, (SPFLOAT)as_number(args[1]),
+1473
(SPFLOAT)as_number(args[2]), (SPFLOAT)as_number(args[3]),
+1474
(SPFLOAT)as_number(args[4]), &out);
+1475
return sigil_flonum((double)out);
+1476
}
+1477
+1478
/* (make-crush) */
+1479
static Value native_make_crush(SigilVM *vm, int argc, Value *args)
+1480
{
+1481
(void)argc; (void)args;
+1482
CHECK_SP(vm, "make-crush");
+1483
sp_crush *p;
+1484
sp_crush_create(&p);
+1485
sp_crush_init(g_sp, p);
+1486
ENSURE_TAG(vm, tt_crush, "dsp-crush");
+1487
return sigil_make_foreign(vm, tt_crush, p, destroy_crush, sizeof(sp_crush));
+1488
}
+1489
+1490
/* (crush-compute crush in drive bits rate-div) -> flonum */
+1491
static Value native_crush_compute(SigilVM *vm, int argc, Value *args)
+1492
{
+1493
(void)argc;
+1494
sp_crush *p = get_crush(vm, args[0]);
+1495
if (!p) { sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "crush-compute: expected crush"); return SIGIL_FALSE; }
+1496
SPFLOAT out = 0.0f;
+1497
sp_crush_compute(g_sp, p, (SPFLOAT)as_number(args[1]),
+1498
(SPFLOAT)as_number(args[2]), (SPFLOAT)as_number(args[3]),
+1499
(SPFLOAT)as_number(args[4]), &out);
+1500
return sigil_flonum((double)out);
+1501
}
+1502
+1503
/* (make-comp attack release makeup-db) */
+1504
static Value native_make_comp(SigilVM *vm, int argc, Value *args)
+1505
{
+1506
(void)argc;
+1507
CHECK_SP(vm, "make-comp");
+1508
sp_comp *p;
+1509
sp_comp_create(&p);
+1510
sp_comp_init(g_sp, p, (SPFLOAT)as_number(args[0]),
+1511
(SPFLOAT)as_number(args[1]), (SPFLOAT)as_number(args[2]));
+1512
ENSURE_TAG(vm, tt_comp, "dsp-comp");
+1513
return sigil_make_foreign(vm, tt_comp, p, destroy_comp, sizeof(sp_comp));
+1514
}
+1515
+1516
/* (comp-compute comp in threshold-db ratio) -> flonum */
+1517
static Value native_comp_compute(SigilVM *vm, int argc, Value *args)
+1518
{
+1519
(void)argc;
+1520
sp_comp *p = get_comp(vm, args[0]);
+1521
if (!p) { sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "comp-compute: expected comp"); return SIGIL_FALSE; }
+1522
SPFLOAT out = 0.0f;
+1523
sp_comp_compute(g_sp, p, (SPFLOAT)as_number(args[1]),
+1524
(SPFLOAT)as_number(args[2]), (SPFLOAT)as_number(args[3]), &out);
+1525
return sigil_flonum((double)out);
+1526
}
+1527
1528
/* ============================================================
1529
* DYNAMICS / WAVESHAPING
1530
* ============================================================ */
@@ -1832,6 +2016,19 @@ void sigil__init_sigil_dsp_module(SigilVM *vm)
2016
sigil_module_register_native(vm, "ks-set-excite-gain!", native_ks_set_excite_gain,
2017
SIGIL_ARITY_EXACT(2), "Set KS excitation amplitude");
2018
+2019
sigil_module_register_native(vm, "make-fm4", native_make_fm4,
+2020
SIGIL_ARITY_EXACT(4), "Create 4-op FM voice (algorithm feedback vel-depth vel-curve)");
+2021
sigil_module_register_native(vm, "fm4-set-op!", native_fm4_set_op,
+2022
SIGIL_ARITY_EXACT(8), "Set operator (fm4 op ratio level attack decay sustain release)");
+2023
sigil_module_register_native(vm, "fm4-set-algorithm!", native_fm4_set_algorithm,
+2024
SIGIL_ARITY_EXACT(2), "Set fm4 algorithm 0..7");
+2025
sigil_module_register_native(vm, "fm4-set-feedback!", native_fm4_set_feedback,
+2026
SIGIL_ARITY_EXACT(2), "Set fm4 operator-4 feedback 0..1");
+2027
sigil_module_register_native(vm, "fm4-compute", native_fm4_compute,
+2028
SIGIL_ARITY_EXACT(5), "Compute fm4 (fm4 freq gate index velocity) -> sample");
+2029
sigil_module_register_native(vm, "fm4-algorithm-name", native_fm4_algorithm_name,
+2030
SIGIL_ARITY_EXACT(1), "Name of fm4 algorithm 0..7");
+2031
2032
/* Export all osc bindings */
2033
const char *osc_exports[] = {
2034
"make-osc", "make-osc-ft", "osc-compute", "osc-set-freq!", "osc-set-amp!",
@@ -1843,6 +2040,8 @@ void sigil__init_sigil_dsp_module(SigilVM *vm)
2040
"make-phasor", "phasor-compute", "phasor-set-freq!",
2041
"make-ks", "ks-compute", "ks-trigger!",
2042
"ks-set-freq!", "ks-set-decay!", "ks-set-excite-gain!",
+2043
"make-fm4", "fm4-set-op!", "fm4-set-algorithm!", "fm4-set-feedback!",
+2044
"fm4-compute", "fm4-algorithm-name",
2045
NULL
2046
};
2047
for (int i = 0; osc_exports[i]; i++) sigil_module_export(vm, osc_exports[i]);
@@ -1958,11 +2157,26 @@ void sigil__init_sigil_dsp_module(SigilVM *vm)
2157
sigil_module_register_native(vm, "vdelay-set-feedback!", native_vdelay_set_feedback,
2158
SIGIL_ARITY_EXACT(2), "Set variable delay feedback");
2159
+2160
sigil_module_register_native(vm, "make-chorus", native_make_chorus,
+2161
SIGIL_ARITY_EXACT(0), "Create chorus");
+2162
sigil_module_register_native(vm, "chorus-compute", native_chorus_compute,
+2163
SIGIL_ARITY_EXACT(5), "Compute chorus (chorus in rate depth-ms mix) -> sample");
+2164
sigil_module_register_native(vm, "make-crush", native_make_crush,
+2165
SIGIL_ARITY_EXACT(0), "Create crush (drive + bit depth + rate reduction)");
+2166
sigil_module_register_native(vm, "crush-compute", native_crush_compute,
+2167
SIGIL_ARITY_EXACT(5), "Compute crush (crush in drive bits rate-div) -> sample");
+2168
sigil_module_register_native(vm, "make-comp", native_make_comp,
+2169
SIGIL_ARITY_EXACT(3), "Create compressor (attack release makeup-db)");
+2170
sigil_module_register_native(vm, "comp-compute", native_comp_compute,
+2171
SIGIL_ARITY_EXACT(4), "Compute compressor (comp in threshold-db ratio) -> sample");
+2172
2173
const char *fx_exports[] = {
2174
"make-revsc", "revsc-compute", "revsc-set-feedback!", "revsc-set-lpfreq!",
2175
"make-zitarev", "zitarev-compute", "zitarev-set-mix!", "zitarev-set-level!",
2176
"make-delay", "delay-compute", "delay-set-feedback!",
2177
"make-vdelay", "vdelay-compute", "vdelay-set-del!", "vdelay-set-feedback!",
+2178
"make-chorus", "chorus-compute", "make-crush", "crush-compute",
+2179
"make-comp", "comp-compute",
2180
NULL
2181
};
2182
for (int i = 0; fx_exports[i]; i++) sigil_module_export(vm, fx_exports[i]);
src/c/fm4.cadded
@@ -0,0 +1,212 @@
+1
/*
+2
* fm4.c - Four-operator phase-modulation FM voice
+3
*
+4
* Operators are numbered 0..3 (op1..op4 in the user-facing table). Every
+5
* algorithm routes modulation from a higher-numbered operator into a
+6
* lower-numbered one, so computing operators from 3 down to 0 is a valid
+7
* evaluation order for all of them; operator 3 is the feedback operator
+8
* and modulates itself through the average of its last two outputs (the
+9
* DX/OPL form, which keeps the loop stable at high feedback).
+10
*/
+11
+12
#include "fm4.h"
+13
#include <stdlib.h>
+14
#include <string.h>
+15
#include <math.h>
+16
+17
#define FM4_TABLE_BITS 12
+18
#define FM4_TABLE_SIZE (1 << FM4_TABLE_BITS)
+19
#define FM4_TABLE_MASK (FM4_TABLE_SIZE - 1)
+20
#define FM4_TWO_PI 6.283185307179586f
+21
#define FM4_FB_SCALE 3.141592653589793f /* feedback 1.0 = pi radians */
+22
+23
static SPFLOAT fm4_sine[FM4_TABLE_SIZE + 1];
+24
static int fm4_table_ready = 0;
+25
+26
static void fm4_build_table(void)
+27
{
+28
if (fm4_table_ready) return;
+29
for (int i = 0; i <= FM4_TABLE_SIZE; i++) {
+30
double x = (double)i / (double)FM4_TABLE_SIZE;
+31
fm4_sine[i] = (SPFLOAT)sin(x * 6.283185307179586);
+32
}
+33
fm4_table_ready = 1;
+34
}
+35
+36
/* Algorithm table. Operators 0..3 = op1..op4. mod_mask[i] lists who
+37
* modulates operator i; carrier_mask lists who reaches the output. */
+38
static const sp_fm4_algorithm fm4_algorithms[SP_FM4_ALGORITHMS] = {
+39
/* 0 */ { "stack", { 0x2, 0x4, 0x8, 0x0 }, 0x1 }, /* 4->3->2->1 */
+40
/* 1 */ { "two-into-one", { 0x2, 0xC, 0x0, 0x0 }, 0x1 }, /* 4->2, 3->2, 2->1 */
+41
/* 2 */ { "branch", { 0x6, 0x0, 0x8, 0x0 }, 0x1 }, /* 4->3->1, 2->1 */
+42
/* 3 */ { "three-into-one", { 0xE, 0x0, 0x0, 0x0 }, 0x1 }, /* 4,3,2 -> 1 */
+43
/* 4 */ { "pairs", { 0x2, 0x0, 0x8, 0x0 }, 0x5 }, /* 4->3, 2->1 */
+44
/* 5 */ { "stack-plus-one", { 0x0, 0x4, 0x8, 0x0 }, 0x3 }, /* 4->3->2, 1 */
+45
/* 6 */ { "one-into-three", { 0x8, 0x8, 0x8, 0x0 }, 0x7 }, /* 4 -> 1,2,3 */
+46
/* 7 */ { "additive", { 0x0, 0x0, 0x0, 0x0 }, 0xF }, /* all carriers */
+47
};
+48
+49
const sp_fm4_algorithm *sp_fm4_algorithm_table(void)
+50
{
+51
return fm4_algorithms;
+52
}
+53
+54
int sp_fm4_create(sp_fm4 **p)
+55
{
+56
*p = (sp_fm4 *)calloc(1, sizeof(sp_fm4));
+57
return *p ? SP_OK : SP_NOT_OK;
+58
}
+59
+60
int sp_fm4_destroy(sp_fm4 **p)
+61
{
+62
if (!*p) return SP_OK;
+63
free(*p);
+64
*p = NULL;
+65
return SP_OK;
+66
}
+67
+68
int sp_fm4_init(sp_data *sp, sp_fm4 *p)
+69
{
+70
fm4_build_table();
+71
memset(p, 0, sizeof(*p));
+72
p->sr = (SPFLOAT)sp->sr;
+73
p->algorithm = 0;
+74
p->feedback = 0.0f;
+75
p->vel_depth = 0.0f;
+76
p->vel_curve = 1.0f;
+77
for (int i = 0; i < SP_FM4_OPS; i++)
+78
sp_fm4_set_op(p, i, 1.0f, 1.0f, 0.005f, 0.1f, 1.0f, 0.1f);
+79
return SP_OK;
+80
}
+81
+82
int sp_fm4_set_algorithm(sp_fm4 *p, int algorithm)
+83
{
+84
if (algorithm < 0 || algorithm >= SP_FM4_ALGORITHMS) return SP_NOT_OK;
+85
p->algorithm = algorithm;
+86
return SP_OK;
+87
}
+88
+89
int sp_fm4_set_op(sp_fm4 *p, int op, SPFLOAT ratio, SPFLOAT level,
+90
SPFLOAT attack, SPFLOAT decay, SPFLOAT sustain,
+91
SPFLOAT release)
+92
{
+93
if (op < 0 || op >= SP_FM4_OPS) return SP_NOT_OK;
+94
sp_fm4_op *o = &p->op[op];
+95
o->ratio = ratio < 0.0f ? 0.0f : ratio;
+96
o->level = level;
+97
o->attack = attack < 0.0f ? 0.0f : attack;
+98
o->decay = decay < 0.0f ? 0.0f : decay;
+99
o->sustain = sustain < 0.0f ? 0.0f : (sustain > 1.0f ? 1.0f : sustain);
+100
o->release = release < 0.0f ? 0.0f : release;
+101
return SP_OK;
+102
}
+103
+104
/* Linear ADSR, one step. `on` is the gate state this sample. */
+105
static inline SPFLOAT fm4_env_step(sp_fm4_op *o, SPFLOAT sr, int on)
+106
{
+107
switch (o->stage) {
+108
case 1: { /* attack */
+109
SPFLOAT samples = o->attack * sr;
+110
if (samples < 1.0f) { o->env = 1.0f; o->stage = 2; break; }
+111
o->env += 1.0f / samples;
+112
if (o->env >= 1.0f) { o->env = 1.0f; o->stage = 2; }
+113
break;
+114
}
+115
case 2: { /* decay */
+116
SPFLOAT samples = o->decay * sr;
+117
if (samples < 1.0f) { o->env = o->sustain; o->stage = 3; break; }
+118
o->env -= (1.0f - o->sustain) / samples;
+119
if (o->env <= o->sustain) { o->env = o->sustain; o->stage = 3; }
+120
break;
+121
}
+122
case 3: /* sustain */
+123
o->env = o->sustain;
+124
break;
+125
case 4: { /* release */
+126
SPFLOAT samples = o->release * sr;
+127
if (samples < 1.0f) { o->env = 0.0f; o->stage = 0; break; }
+128
o->env -= o->rel_rate;
+129
if (o->env <= 0.0f) { o->env = 0.0f; o->stage = 0; }
+130
break;
+131
}
+132
default: /* idle */
+133
o->env = 0.0f;
+134
break;
+135
}
+136
(void)on;
+137
return o->env;
+138
}
+139
+140
static inline SPFLOAT fm4_sin(SPFLOAT phase01, SPFLOAT mod_rad)
+141
{
+142
SPFLOAT pos = phase01 * (SPFLOAT)FM4_TABLE_SIZE
+143
+ mod_rad * ((SPFLOAT)FM4_TABLE_SIZE / FM4_TWO_PI);
+144
int i = (int)pos;
+145
if ((SPFLOAT)i > pos) i--; /* floor for negative positions */
+146
SPFLOAT frac = pos - (SPFLOAT)i;
+147
i &= FM4_TABLE_MASK;
+148
return fm4_sine[i] + (fm4_sine[i + 1] - fm4_sine[i]) * frac;
+149
}
+150
+151
int sp_fm4_compute(sp_data *sp, sp_fm4 *p, SPFLOAT freq, SPFLOAT gate,
+152
SPFLOAT index, SPFLOAT velocity, SPFLOAT *out)
+153
{
+154
const sp_fm4_algorithm *alg = &fm4_algorithms[p->algorithm];
+155
int on = gate > 0.5f;
+156
+157
if (on && !p->gate_prev) {
+158
/* Note on: key sync + envelope restart from zero. */
+159
for (int i = 0; i < SP_FM4_OPS; i++) {
+160
sp_fm4_op *o = &p->op[i];
+161
o->phase = 0.0f;
+162
o->env = 0.0f;
+163
o->stage = 1;
+164
o->out = o->out_prev = o->out_prev2 = 0.0f;
+165
}
+166
} else if (!on && p->gate_prev) {
+167
for (int i = 0; i < SP_FM4_OPS; i++) {
+168
sp_fm4_op *o = &p->op[i];
+169
if (o->stage != 0) {
+170
SPFLOAT samples = o->release * p->sr;
+171
o->rel_rate = samples < 1.0f ? o->env : o->env / samples;
+172
o->stage = 4;
+173
}
+174
}
+175
}
+176
p->gate_prev = on;
+177
+178
/* Velocity -> index. */
+179
SPFLOAT v = velocity < 0.0f ? 0.0f : (velocity > 1.0f ? 1.0f : velocity);
+180
SPFLOAT vshaped = (p->vel_curve == 1.0f) ? v : powf(v, p->vel_curve);
+181
SPFLOAT index_eff = index * (1.0f - p->vel_depth + p->vel_depth * vshaped);
+182
+183
SPFLOAT sum = 0.0f;
+184
int ncar = 0;
+185
for (int i = SP_FM4_OPS - 1; i >= 0; i--) {
+186
sp_fm4_op *o = &p->op[i];
+187
SPFLOAT env = fm4_env_step(o, p->sr, on);
+188
+189
SPFLOAT mod = 0.0f;
+190
unsigned char m = alg->mod_mask[i];
+191
for (int j = i + 1; j < SP_FM4_OPS; j++)
+192
if (m & (1u << j)) mod += p->op[j].out;
+193
mod *= index_eff;
+194
if (i == SP_FM4_OPS - 1 && p->feedback > 0.0f)
+195
mod += p->feedback * FM4_FB_SCALE * 0.5f * (o->out_prev + o->out_prev2);
+196
+197
SPFLOAT s = fm4_sin(o->phase, mod) * o->level * env;
+198
if (i == SP_FM4_OPS - 1) {
+199
o->out_prev2 = o->out_prev;
+200
o->out_prev = s;
+201
}
+202
o->out = s;
+203
+204
o->phase += freq * o->ratio / p->sr;
+205
while (o->phase >= 1.0f) o->phase -= 1.0f;
+206
while (o->phase < 0.0f) o->phase += 1.0f;
+207
+208
if (alg->carrier_mask & (1u << i)) { sum += s; ncar++; }
+209
}
+210
*out = ncar > 0 ? sum / (SPFLOAT)ncar : 0.0f;
+211
return SP_OK;
+212
}
src/c/fm4.hadded
@@ -0,0 +1,83 @@
+1
/*
+2
* fm4.h - Four-operator phase-modulation FM voice
+3
*
+4
* DX/OPL-style: four sine operators, an algorithm table that says which
+5
* operator modulates which and which operators are carriers, a per-
+6
* operator frequency ratio, level and linear ADSR, feedback on operator 4,
+7
* and a global modulation index that scales every modulator's depth.
+8
*
+9
* Everything here is deterministic given the parameters and the sample
+10
* rate: the sine comes from a 4096-entry table with linear interpolation
+11
* (no libm at run time), so the same inputs produce the same samples on
+12
* every target that runs the same float arithmetic.
+13
*
+14
* Signal-rate inputs (set every sample by the host, motif's render loop):
+15
* freq base frequency (Hz)
+16
* gate > 0.5 is on; a rising edge resets phases and retriggers
+17
* every operator envelope
+18
* index global modulation index (0 = every carrier is a pure sine)
+19
* velocity 0..1, scales the index through the velocity curve
+20
*
+21
* Per-operator parameters (set at init or with sp_fm4_set_op):
+22
* ratio frequency multiple of `freq` (coarse + fine folded in)
+23
* level 0..1 output level (carriers: amplitude; modulators: depth)
+24
* a d s r attack / decay / release in seconds, sustain 0..1
+25
*/
+26
+27
#ifndef SIGIL_DSP_FM4_H
+28
#define SIGIL_DSP_FM4_H
+29
+30
#include "soundpipe.h"
+31
+32
#define SP_FM4_OPS 4
+33
#define SP_FM4_ALGORITHMS 8
+34
+35
typedef struct {
+36
SPFLOAT ratio;
+37
SPFLOAT level;
+38
SPFLOAT attack, decay, sustain, release;
+39
/* state */
+40
SPFLOAT phase; /* 0..1 */
+41
SPFLOAT env; /* current envelope value */
+42
int stage; /* 0 idle, 1 attack, 2 decay, 3 sustain, 4 release */
+43
SPFLOAT out; /* this sample's output */
+44
SPFLOAT out_prev; /* previous sample's output (feedback) */
+45
SPFLOAT out_prev2; /* two samples back (feedback averaging) */
+46
SPFLOAT rel_rate; /* envelope decrement per sample during release */
+47
} sp_fm4_op;
+48
+49
typedef struct sp_fm4 {
+50
int algorithm; /* 0..SP_FM4_ALGORITHMS-1 */
+51
SPFLOAT feedback; /* 0..1; 1 = pi radians of self-modulation on op 4 */
+52
SPFLOAT vel_depth; /* 0..1: how much velocity scales the index */
+53
SPFLOAT vel_curve; /* exponent applied to velocity (1 = linear) */
+54
sp_fm4_op op[SP_FM4_OPS];
+55
int gate_prev;
+56
SPFLOAT sr;
+57
} sp_fm4;
+58
+59
int sp_fm4_create(sp_fm4 **p);
+60
int sp_fm4_destroy(sp_fm4 **p);
+61
/* Defaults: algorithm 0, feedback 0, vel_depth 0, vel_curve 1, every
+62
* operator ratio 1 level 1 ADSR 0.005/0.1/1.0/0.1. */
+63
int sp_fm4_init(sp_data *sp, sp_fm4 *p);
+64
int sp_fm4_set_algorithm(sp_fm4 *p, int algorithm);
+65
int sp_fm4_set_op(sp_fm4 *p, int op, SPFLOAT ratio, SPFLOAT level,
+66
SPFLOAT attack, SPFLOAT decay, SPFLOAT sustain,
+67
SPFLOAT release);
+68
/* One sample. */
+69
int sp_fm4_compute(sp_data *sp, sp_fm4 *p, SPFLOAT freq, SPFLOAT gate,
+70
SPFLOAT index, SPFLOAT velocity, SPFLOAT *out);
+71
+72
/* Algorithm table entries: mod_mask[i] is the bitmask of operators that
+73
* modulate operator i; carrier_mask is the bitmask of operators summed
+74
* into the output. Exposed so hosts can print or validate a table. */
+75
typedef struct {
+76
const char *name;
+77
unsigned char mod_mask[SP_FM4_OPS];
+78
unsigned char carrier_mask;
+79
} sp_fm4_algorithm;
+80
+81
const sp_fm4_algorithm *sp_fm4_algorithm_table(void);
+82
+83
#endif
src/c/fx4.cadded
@@ -0,0 +1,198 @@
+1
/*
+2
* fx4.c - chorus, crush, compressor (see fx4.h)
+3
*/
+4
+5
#include "fx4.h"
+6
#include <stdlib.h>
+7
#include <string.h>
+8
#include <math.h>
+9
+10
/* ============================================================ chorus */
+11
+12
#define CHORUS_CENTRE_MS 15.0f
+13
+14
int sp_chorus_create(sp_chorus **p)
+15
{
+16
*p = (sp_chorus *)calloc(1, sizeof(sp_chorus));
+17
return *p ? SP_OK : SP_NOT_OK;
+18
}
+19
+20
int sp_chorus_destroy(sp_chorus **p)
+21
{
+22
if (!*p) return SP_OK;
+23
free((*p)->buf);
+24
free(*p);
+25
*p = NULL;
+26
return SP_OK;
+27
}
+28
+29
int sp_chorus_init(sp_data *sp, sp_chorus *p)
+30
{
+31
p->sr = (SPFLOAT)sp->sr;
+32
p->bufsize = (int)(SP_CHORUS_MAX_MS * 0.001f * p->sr) + 4;
+33
p->buf = (SPFLOAT *)calloc((size_t)p->bufsize, sizeof(SPFLOAT));
+34
p->pos = 0;
+35
p->lfo_phase = 0.0f;
+36
return p->buf ? SP_OK : SP_NOT_OK;
+37
}
+38
+39
/* Read `delay` samples back from the write position, linear interp. */
+40
static inline SPFLOAT chorus_tap(const sp_chorus *p, SPFLOAT delay)
+41
{
+42
SPFLOAT maxd = (SPFLOAT)(p->bufsize - 2);
+43
if (delay < 1.0f) delay = 1.0f;
+44
if (delay > maxd) delay = maxd;
+45
SPFLOAT rpos = (SPFLOAT)p->pos - delay;
+46
while (rpos < 0.0f) rpos += (SPFLOAT)p->bufsize;
+47
int i0 = (int)rpos;
+48
SPFLOAT frac = rpos - (SPFLOAT)i0;
+49
int i1 = i0 + 1;
+50
if (i1 >= p->bufsize) i1 -= p->bufsize;
+51
return p->buf[i0] + (p->buf[i1] - p->buf[i0]) * frac;
+52
}
+53
+54
/* Triangle in [-1, 1] from a 0..1 phase. */
+55
static inline SPFLOAT tri_lfo(SPFLOAT phase)
+56
{
+57
SPFLOAT t = phase < 0.5f ? phase * 4.0f - 1.0f : 3.0f - phase * 4.0f;
+58
return t;
+59
}
+60
+61
int sp_chorus_compute(sp_data *sp, sp_chorus *p, SPFLOAT in, SPFLOAT rate,
+62
SPFLOAT depth, SPFLOAT mix, SPFLOAT *out)
+63
{
+64
p->buf[p->pos] = in;
+65
+66
SPFLOAT ms_per_sample = 0.001f * p->sr;
+67
SPFLOAT centre = CHORUS_CENTRE_MS * ms_per_sample;
+68
SPFLOAT dev = depth * ms_per_sample;
+69
SPFLOAT ph2 = p->lfo_phase + 0.5f;
+70
if (ph2 >= 1.0f) ph2 -= 1.0f;
+71
SPFLOAT tap1 = chorus_tap(p, centre + dev * tri_lfo(p->lfo_phase));
+72
SPFLOAT tap2 = chorus_tap(p, centre * 0.7f + dev * tri_lfo(ph2));
+73
SPFLOAT wet = 0.5f * (tap1 + tap2);
+74
+75
if (mix < 0.0f) mix = 0.0f;
+76
if (mix > 1.0f) mix = 1.0f;
+77
*out = in * (1.0f - mix) + wet * mix;
+78
+79
p->pos++;
+80
if (p->pos >= p->bufsize) p->pos = 0;
+81
if (rate < 0.0f) rate = 0.0f;
+82
p->lfo_phase += rate / p->sr;
+83
while (p->lfo_phase >= 1.0f) p->lfo_phase -= 1.0f;
+84
return SP_OK;
+85
}
+86
+87
/* ============================================================ crush */
+88
+89
int sp_crush_create(sp_crush **p)
+90
{
+91
*p = (sp_crush *)calloc(1, sizeof(sp_crush));
+92
return *p ? SP_OK : SP_NOT_OK;
+93
}
+94
+95
int sp_crush_destroy(sp_crush **p)
+96
{
+97
if (!*p) return SP_OK;
+98
free(*p);
+99
*p = NULL;
+100
return SP_OK;
+101
}
+102
+103
int sp_crush_init(sp_data *sp, sp_crush *p)
+104
{
+105
p->held = 0.0f;
+106
p->counter = 1.0e9f; /* first sample is always sampled */
+107
return SP_OK;
+108
}
+109
+110
int sp_crush_compute(sp_data *sp, sp_crush *p, SPFLOAT in, SPFLOAT drive,
+111
SPFLOAT bits, SPFLOAT rate_div, SPFLOAT *out)
+112
{
+113
SPFLOAT y = in;
+114
+115
/* Saturation: unity gain at |in| = 1, identity at drive 0. */
+116
if (drive > 0.0f) {
+117
SPFLOAT g = 1.0f + drive;
+118
y = tanhf(y * g) / tanhf(g);
+119
}
+120
+121
/* Bit depth: 16 or more leaves the signal alone. */
+122
int b = (int)bits;
+123
if (b < 1) b = 1;
+124
if (b < 16) {
+125
SPFLOAT steps = (SPFLOAT)(1 << (b - 1));
+126
SPFLOAT q = y * steps;
+127
int qi = (int)(q + (q >= 0.0f ? 0.5f : -0.5f));
+128
y = (SPFLOAT)qi / steps;
+129
}
+130
+131
/* Sample-rate reduction: hold the value for rate_div samples. */
+132
if (rate_div < 1.0f) rate_div = 1.0f;
+133
p->counter += 1.0f;
+134
if (p->counter >= rate_div) {
+135
p->held = y;
+136
p->counter -= rate_div;
+137
if (p->counter > rate_div) p->counter = 0.0f;
+138
}
+139
*out = p->held;
+140
return SP_OK;
+141
}
+142
+143
/* ============================================================ compressor */
+144
+145
int sp_comp_create(sp_comp **p)
+146
{
+147
*p = (sp_comp *)calloc(1, sizeof(sp_comp));
+148
return *p ? SP_OK : SP_NOT_OK;
+149
}
+150
+151
int sp_comp_destroy(sp_comp **p)
+152
{
+153
if (!*p) return SP_OK;
+154
free(*p);
+155
*p = NULL;
+156
return SP_OK;
+157
}
+158
+159
static inline SPFLOAT onepole_coef(SPFLOAT seconds, SPFLOAT sr)
+160
{
+161
if (seconds <= 0.0f) return 0.0f;
+162
return expf(-1.0f / (seconds * sr));
+163
}
+164
+165
int sp_comp_init(sp_data *sp, sp_comp *p, SPFLOAT attack, SPFLOAT release,
+166
SPFLOAT makeup_db)
+167
{
+168
p->sr = (SPFLOAT)sp->sr;
+169
p->attack = attack;
+170
p->release = release;
+171
p->makeup = makeup_db;
+172
p->attack_coef = onepole_coef(attack, p->sr);
+173
p->release_coef = onepole_coef(release, p->sr);
+174
p->makeup_lin = powf(10.0f, makeup_db / 20.0f);
+175
p->env = 0.0f;
+176
return SP_OK;
+177
}
+178
+179
int sp_comp_compute(sp_data *sp, sp_comp *p, SPFLOAT in, SPFLOAT threshold_db,
+180
SPFLOAT ratio, SPFLOAT *out)
+181
{
+182
SPFLOAT level = in < 0.0f ? -in : in;
+183
if (level > p->env)
+184
p->env = p->attack_coef * p->env + (1.0f - p->attack_coef) * level;
+185
else
+186
p->env = p->release_coef * p->env + (1.0f - p->release_coef) * level;
+187
+188
SPFLOAT gain = 1.0f;
+189
if (ratio > 1.0f) {
+190
SPFLOAT thr = powf(10.0f, threshold_db / 20.0f);
+191
if (p->env > thr && thr > 0.0f) {
+192
/* Gain that maps the over-threshold amount through 1/ratio. */
+193
gain = powf(p->env / thr, 1.0f / ratio - 1.0f);
+194
}
+195
}
+196
*out = in * gain * p->makeup_lin;
+197
return SP_OK;
+198
}
src/c/fx4.hadded
@@ -0,0 +1,81 @@
+1
/*
+2
* fx4.h - The tracker fatteners: chorus, crush, compressor
+3
*
+4
* Three small mono effects written for motif's DSP graph. All parameters
+5
* that a tracker effects column would want to move are signal-rate
+6
* arguments to the compute call; the rest are set at init.
+7
*
+8
* sp_chorus two LFO-modulated taps on one delay line, mixed with the dry
+9
* signal. compute(in, rate_hz, depth_ms, mix)
+10
* sp_crush tanh drive, then bit-depth quantise, then sample-and-hold
+11
* rate reduction. compute(in, drive, bits, rate_div)
+12
* sp_comp feed-forward peak compressor with a one-pole attack/release
+13
* envelope. compute(in, threshold_db, ratio); attack, release
+14
* and makeup gain are init-time.
+15
*
+16
* Deterministic: no randomness, no libm on the per-sample path except
+17
* tanhf in the crush drive stage and the dB conversions in the compressor
+18
* (both computed from float inputs the same way on every target).
+19
*/
+20
+21
#ifndef SIGIL_DSP_FX4_H
+22
#define SIGIL_DSP_FX4_H
+23
+24
#include "soundpipe.h"
+25
+26
/* ---- chorus ---- */
+27
+28
#define SP_CHORUS_MAX_MS 40.0f
+29
+30
typedef struct sp_chorus {
+31
SPFLOAT *buf;
+32
int bufsize;
+33
int pos;
+34
SPFLOAT lfo_phase; /* 0..1 */
+35
SPFLOAT sr;
+36
} sp_chorus;
+37
+38
int sp_chorus_create(sp_chorus **p);
+39
int sp_chorus_destroy(sp_chorus **p);
+40
int sp_chorus_init(sp_data *sp, sp_chorus *p);
+41
/* rate in Hz, depth in ms (peak deviation around a 15 ms centre), mix 0..1 */
+42
int sp_chorus_compute(sp_data *sp, sp_chorus *p, SPFLOAT in, SPFLOAT rate,
+43
SPFLOAT depth, SPFLOAT mix, SPFLOAT *out);
+44
+45
/* ---- crush ---- */
+46
+47
typedef struct sp_crush {
+48
SPFLOAT held; /* last sampled value for rate reduction */
+49
SPFLOAT counter; /* samples since last hold */
+50
} sp_crush;
+51
+52
int sp_crush_create(sp_crush **p);
+53
int sp_crush_destroy(sp_crush **p);
+54
int sp_crush_init(sp_data *sp, sp_crush *p);
+55
/* drive >= 0 (0 = clean), bits 1..16 (16 = no quantisation), rate_div >= 1
+56
* (1 = no rate reduction; N = hold every value for N samples) */
+57
int sp_crush_compute(sp_data *sp, sp_crush *p, SPFLOAT in, SPFLOAT drive,
+58
SPFLOAT bits, SPFLOAT rate_div, SPFLOAT *out);
+59
+60
/* ---- compressor ---- */
+61
+62
typedef struct sp_comp {
+63
SPFLOAT attack; /* seconds */
+64
SPFLOAT release; /* seconds */
+65
SPFLOAT makeup; /* dB */
+66
SPFLOAT env; /* envelope follower state (linear) */
+67
SPFLOAT attack_coef;
+68
SPFLOAT release_coef;
+69
SPFLOAT makeup_lin;
+70
SPFLOAT sr;
+71
} sp_comp;
+72
+73
int sp_comp_create(sp_comp **p);
+74
int sp_comp_destroy(sp_comp **p);
+75
int sp_comp_init(sp_data *sp, sp_comp *p, SPFLOAT attack, SPFLOAT release,
+76
SPFLOAT makeup_db);
+77
/* threshold in dBFS (<= 0), ratio >= 1 (1 = no compression) */
+78
int sp_comp_compute(sp_data *sp, sp_comp *p, SPFLOAT in, SPFLOAT threshold_db,
+79
SPFLOAT ratio, SPFLOAT *out);
+80
+81
#endif
src/sigil/dsp/fx.sglmodified
@@ -14,7 +14,10 @@
14
make-revsc revsc-compute revsc-set-feedback! revsc-set-lpfreq!
15
make-zitarev zitarev-compute zitarev-set-mix! zitarev-set-level!
16
make-delay delay-compute delay-set-feedback!
−17
make-vdelay vdelay-compute vdelay-set-del! vdelay-set-feedback!)
+17
make-vdelay vdelay-compute vdelay-set-del! vdelay-set-feedback!
+18
make-chorus chorus-compute
+19
make-crush crush-compute
+20
make-comp comp-compute)
21
22
(begin
23
(define-native (make-revsc feedback lpfreq))
@@ -31,4 +34,28 @@
34
(define-native (make-vdelay maxdel feedback))
35
(define-native (vdelay-compute vdelay input))
36
(define-native (vdelay-set-del! vdelay del))
−34
(define-native (vdelay-set-feedback! vdelay feedback))))
+37
(define-native (vdelay-set-feedback! vdelay feedback))
+38
+39
;; Tracker fatteners (fx4.h). All per-sample parameters are
+40
;; arguments to the compute call so a host can automate them.
+41
;;; Two-tap modulated-delay chorus around a 15 ms centre.
+42
(define-native (make-chorus)
+43
(: -> foreign?))
+44
;;; One sample: LFO `rate` in Hz, `depth-ms` peak deviation, `mix` 0..1
+45
;;; (0 is the dry input unchanged).
+46
(define-native (chorus-compute chorus input rate depth-ms mix)
+47
(: foreign? number? number? number? number? -> number?))
+48
;;; Saturation, bit-depth reduction and sample-rate reduction in one node.
+49
(define-native (make-crush)
+50
(: -> foreign?))
+51
;;; One sample: tanh `drive` (0 = clean), quantise to `bits` (16 = none),
+52
;;; hold each value for `rate-div` samples (1 = none).
+53
(define-native (crush-compute crush input drive bits rate-div)
+54
(: foreign? number? number? number? number? -> number?))
+55
;;; Feed-forward peak compressor; attack and release in seconds,
+56
;;; makeup gain in dB.
+57
(define-native (make-comp attack release makeup-db)
+58
(: number? number? number? -> foreign?))
+59
;;; One sample: `threshold-db` in dBFS, `ratio` >= 1 (1 = bypass).
+60
(define-native (comp-compute comp input threshold-db ratio)
+61
(: foreign? number? number? number? -> number?))))
src/sigil/dsp/osc.sglmodified
@@ -20,7 +20,9 @@
20
make-fosc fosc-compute fosc-set-freq! fosc-set-index! fosc-set-amp!
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!)
+23
ks-set-freq! ks-set-decay! ks-set-excite-gain!
+24
make-fm4 fm4-set-op! fm4-set-algorithm! fm4-set-feedback!
+25
fm4-compute fm4-algorithm-name)
26
27
(begin
28
(define-native make-osc)
@@ -59,6 +61,32 @@
61
(define-native (ks-set-decay! ks decay))
62
(define-native (ks-set-excite-gain! ks gain))
63
+64
;; Four-operator FM voice (fm4.h). Operators are 0..3; algorithm
+65
;; 0..7; feedback 0..1 on operator 3; velocity scales the index
+66
;; through (1 - vel-depth + vel-depth * velocity^vel-curve).
+67
;;; Create a four-operator FM voice. `algorithm` is 0..7 (see
+68
;;; fm4-algorithm-name), `feedback` 0..1 self-modulates operator 3,
+69
;;; and velocity scales the index through
+70
;;; (1 - vel-depth + vel-depth * velocity^vel-curve).
+71
(define-native (make-fm4 algorithm feedback vel-depth vel-curve)
+72
(: number? number? number? number? -> foreign?))
+73
;;; Set operator `op` (0..3): frequency ratio, level 0..1 and a
+74
;;; linear ADSR (attack, decay, release in seconds; sustain 0..1).
+75
(define-native (fm4-set-op! fm4 op ratio level attack decay sustain release)
+76
(: foreign? integer? number? number? number? number? number? number? -> any?))
+77
(define-native (fm4-set-algorithm! fm4 algorithm)
+78
(: foreign? integer? -> any?))
+79
(define-native (fm4-set-feedback! fm4 feedback)
+80
(: foreign? number? -> any?))
+81
;;; One sample. A rising edge on `gate` (> 0.5) resets every
+82
;;; operator's phase and restarts its envelope; index 0 makes each
+83
;;; carrier a pure sine at freq * ratio.
+84
(define-native (fm4-compute fm4 freq gate index velocity)
+85
(: foreign? number? number? number? number? -> number?))
+86
;;; Name of algorithm 0..7, or #f out of range.
+87
(define-native (fm4-algorithm-name algorithm)
+88
(: integer? -> (maybe string?)))
+89
90
;;; Create an oscillator by type.
91
;;; Types: sine, saw, square, triangle, fm
92
(define (make-oscillator type freq amp . opts)