Add the fm4 four-operator FM voice and the chorus, crush and comp nodes
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.
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(-)src/c/dsp.cmodified
#include "soundpipe.h"#include "ks.h"#include "fm4.h"#include "fx4.h"/* ============================================================ * GLOBAL STATEstatic Value tt_wavout = SIGIL_UNDEFINED;static Value tt_ftbl = SIGIL_UNDEFINED;static Value tt_ks = SIGIL_UNDEFINED;static Value tt_fm4 = SIGIL_UNDEFINED;static Value tt_chorus = SIGIL_UNDEFINED;static Value tt_crush = SIGIL_UNDEFINED;static Value tt_comp = SIGIL_UNDEFINED;/* ============================================================ * HELPERSstatic void destroy_wavout(void *data) { sp_wavout *p = (sp_wavout *)data; sp_wavout_destroy(&p); }static void destroy_ftbl(void *data) { sp_ftbl *p = (sp_ftbl *)data; sp_ftbl_destroy(&p); }static void destroy_ks(void *data) { sp_ks *p = (sp_ks *)data; sp_ks_destroy(&p); }static void destroy_fm4(void *data) { sp_fm4 *p = (sp_fm4 *)data; sp_fm4_destroy(&p); }static void destroy_chorus(void *data) { sp_chorus *p = (sp_chorus *)data; sp_chorus_destroy(&p); }static void destroy_crush(void *data) { sp_crush *p = (sp_crush *)data; sp_crush_destroy(&p); }static void destroy_comp(void *data) { sp_comp *p = (sp_comp *)data; sp_comp_destroy(&p); }/* ============================================================ * TYPE GETTERSDEF_GETTER(crossfade, sp_crossfade, tt_crossfade, "crossfade")DEF_GETTER(wavout, sp_wavout, tt_wavout, "wavout")DEF_GETTER(ks, sp_ks, tt_ks, "ks")DEF_GETTER(fm4, sp_fm4, tt_fm4, "fm4")DEF_GETTER(chorus, sp_chorus, tt_chorus, "chorus")DEF_GETTER(crush, sp_crush, tt_crush, "crush")DEF_GETTER(comp, sp_comp, tt_comp, "comp")/* ============================================================ * CORE — dsp-init, dsp-shutdown, dsp-sample-rate return SIGIL_NIL;}/* ============================================================ * FM4 — four-operator FM voice (fm4.h) * ============================================================ */#define FM4_GET(vm, args, name) \ sp_fm4 *p = get_fm4(vm, args[0]); \ if (!p) { sigil__vm_set_error(vm, SIGIL_ERR_TYPE, name ": expected fm4"); return SIGIL_FALSE; }/* (make-fm4 algorithm feedback vel-depth vel-curve) */static Value native_make_fm4(SigilVM *vm, int argc, Value *args){ (void)argc; CHECK_SP(vm, "make-fm4"); int alg = (int)as_number(args[0]); if (alg < 0 || alg >= SP_FM4_ALGORITHMS) { sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME, "make-fm4: algorithm out of range 0..7"); return SIGIL_FALSE; } sp_fm4 *p; sp_fm4_create(&p); sp_fm4_init(g_sp, p); sp_fm4_set_algorithm(p, alg); p->feedback = (SPFLOAT)as_number(args[1]); p->vel_depth = (SPFLOAT)as_number(args[2]); p->vel_curve = (SPFLOAT)as_number(args[3]); ENSURE_TAG(vm, tt_fm4, "dsp-fm4"); return sigil_make_foreign(vm, tt_fm4, p, destroy_fm4, sizeof(sp_fm4));}/* (fm4-set-op! fm4 op ratio level attack decay sustain release) — op is 0..3 */static Value native_fm4_set_op(SigilVM *vm, int argc, Value *args){ (void)argc; FM4_GET(vm, args, "fm4-set-op!"); int op = (int)as_number(args[1]); if (op < 0 || op >= SP_FM4_OPS) { sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME, "fm4-set-op!: operator out of range 0..3"); return SIGIL_FALSE; } sp_fm4_set_op(p, op, (SPFLOAT)as_number(args[2]), (SPFLOAT)as_number(args[3]), (SPFLOAT)as_number(args[4]), (SPFLOAT)as_number(args[5]), (SPFLOAT)as_number(args[6]), (SPFLOAT)as_number(args[7])); return SIGIL_NIL;}/* (fm4-set-algorithm! fm4 algorithm) */static Value native_fm4_set_algorithm(SigilVM *vm, int argc, Value *args){ (void)argc; FM4_GET(vm, args, "fm4-set-algorithm!"); if (sp_fm4_set_algorithm(p, (int)as_number(args[1])) != SP_OK) { sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME, "fm4-set-algorithm!: algorithm out of range 0..7"); return SIGIL_FALSE; } return SIGIL_NIL;}/* (fm4-set-feedback! fm4 feedback) */static Value native_fm4_set_feedback(SigilVM *vm, int argc, Value *args){ (void)argc; FM4_GET(vm, args, "fm4-set-feedback!"); p->feedback = (SPFLOAT)as_number(args[1]); return SIGIL_NIL;}/* (fm4-compute fm4 freq gate index velocity) -> flonum */static Value native_fm4_compute(SigilVM *vm, int argc, Value *args){ (void)argc; FM4_GET(vm, args, "fm4-compute"); SPFLOAT out = 0.0f; sp_fm4_compute(g_sp, p, (SPFLOAT)as_number(args[1]), (SPFLOAT)as_number(args[2]), (SPFLOAT)as_number(args[3]), (SPFLOAT)as_number(args[4]), &out); return sigil_flonum((double)out);}/* (fm4-algorithm-name algorithm) -> string */static Value native_fm4_algorithm_name(SigilVM *vm, int argc, Value *args){ (void)argc; int alg = (int)as_number(args[0]); if (alg < 0 || alg >= SP_FM4_ALGORITHMS) return SIGIL_FALSE; const char *name = sp_fm4_algorithm_table()[alg].name; return sigil_make_string(vm, name, strlen(name));}/* ============================================================ * FATTENERS — chorus, crush, compressor (fx4.h) * ============================================================ *//* (make-chorus) */static Value native_make_chorus(SigilVM *vm, int argc, Value *args){ (void)argc; (void)args; CHECK_SP(vm, "make-chorus"); sp_chorus *p; sp_chorus_create(&p); sp_chorus_init(g_sp, p); ENSURE_TAG(vm, tt_chorus, "dsp-chorus"); return sigil_make_foreign(vm, tt_chorus, p, destroy_chorus, sizeof(sp_chorus) + (size_t)p->bufsize * sizeof(SPFLOAT));}/* (chorus-compute chorus in rate depth-ms mix) -> flonum */static Value native_chorus_compute(SigilVM *vm, int argc, Value *args){ (void)argc; sp_chorus *p = get_chorus(vm, args[0]); if (!p) { sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "chorus-compute: expected chorus"); return SIGIL_FALSE; } SPFLOAT out = 0.0f; sp_chorus_compute(g_sp, p, (SPFLOAT)as_number(args[1]), (SPFLOAT)as_number(args[2]), (SPFLOAT)as_number(args[3]), (SPFLOAT)as_number(args[4]), &out); return sigil_flonum((double)out);}/* (make-crush) */static Value native_make_crush(SigilVM *vm, int argc, Value *args){ (void)argc; (void)args; CHECK_SP(vm, "make-crush"); sp_crush *p; sp_crush_create(&p); sp_crush_init(g_sp, p); ENSURE_TAG(vm, tt_crush, "dsp-crush"); return sigil_make_foreign(vm, tt_crush, p, destroy_crush, sizeof(sp_crush));}/* (crush-compute crush in drive bits rate-div) -> flonum */static Value native_crush_compute(SigilVM *vm, int argc, Value *args){ (void)argc; sp_crush *p = get_crush(vm, args[0]); if (!p) { sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "crush-compute: expected crush"); return SIGIL_FALSE; } SPFLOAT out = 0.0f; sp_crush_compute(g_sp, p, (SPFLOAT)as_number(args[1]), (SPFLOAT)as_number(args[2]), (SPFLOAT)as_number(args[3]), (SPFLOAT)as_number(args[4]), &out); return sigil_flonum((double)out);}/* (make-comp attack release makeup-db) */static Value native_make_comp(SigilVM *vm, int argc, Value *args){ (void)argc; CHECK_SP(vm, "make-comp"); sp_comp *p; sp_comp_create(&p); sp_comp_init(g_sp, p, (SPFLOAT)as_number(args[0]), (SPFLOAT)as_number(args[1]), (SPFLOAT)as_number(args[2])); ENSURE_TAG(vm, tt_comp, "dsp-comp"); return sigil_make_foreign(vm, tt_comp, p, destroy_comp, sizeof(sp_comp));}/* (comp-compute comp in threshold-db ratio) -> flonum */static Value native_comp_compute(SigilVM *vm, int argc, Value *args){ (void)argc; sp_comp *p = get_comp(vm, args[0]); if (!p) { sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "comp-compute: expected comp"); return SIGIL_FALSE; } SPFLOAT out = 0.0f; sp_comp_compute(g_sp, p, (SPFLOAT)as_number(args[1]), (SPFLOAT)as_number(args[2]), (SPFLOAT)as_number(args[3]), &out); return sigil_flonum((double)out);}/* ============================================================ * DYNAMICS / WAVESHAPING * ============================================================ */ sigil_module_register_native(vm, "ks-set-excite-gain!", native_ks_set_excite_gain, SIGIL_ARITY_EXACT(2), "Set KS excitation amplitude"); sigil_module_register_native(vm, "make-fm4", native_make_fm4, SIGIL_ARITY_EXACT(4), "Create 4-op FM voice (algorithm feedback vel-depth vel-curve)"); sigil_module_register_native(vm, "fm4-set-op!", native_fm4_set_op, SIGIL_ARITY_EXACT(8), "Set operator (fm4 op ratio level attack decay sustain release)"); sigil_module_register_native(vm, "fm4-set-algorithm!", native_fm4_set_algorithm, SIGIL_ARITY_EXACT(2), "Set fm4 algorithm 0..7"); sigil_module_register_native(vm, "fm4-set-feedback!", native_fm4_set_feedback, SIGIL_ARITY_EXACT(2), "Set fm4 operator-4 feedback 0..1"); sigil_module_register_native(vm, "fm4-compute", native_fm4_compute, SIGIL_ARITY_EXACT(5), "Compute fm4 (fm4 freq gate index velocity) -> sample"); sigil_module_register_native(vm, "fm4-algorithm-name", native_fm4_algorithm_name, SIGIL_ARITY_EXACT(1), "Name of fm4 algorithm 0..7"); /* Export all osc bindings */ const char *osc_exports[] = { "make-osc", "make-osc-ft", "osc-compute", "osc-set-freq!", "osc-set-amp!", "make-phasor", "phasor-compute", "phasor-set-freq!", "make-ks", "ks-compute", "ks-trigger!", "ks-set-freq!", "ks-set-decay!", "ks-set-excite-gain!", "make-fm4", "fm4-set-op!", "fm4-set-algorithm!", "fm4-set-feedback!", "fm4-compute", "fm4-algorithm-name", NULL }; for (int i = 0; osc_exports[i]; i++) sigil_module_export(vm, osc_exports[i]); sigil_module_register_native(vm, "vdelay-set-feedback!", native_vdelay_set_feedback, SIGIL_ARITY_EXACT(2), "Set variable delay feedback"); sigil_module_register_native(vm, "make-chorus", native_make_chorus, SIGIL_ARITY_EXACT(0), "Create chorus"); sigil_module_register_native(vm, "chorus-compute", native_chorus_compute, SIGIL_ARITY_EXACT(5), "Compute chorus (chorus in rate depth-ms mix) -> sample"); sigil_module_register_native(vm, "make-crush", native_make_crush, SIGIL_ARITY_EXACT(0), "Create crush (drive + bit depth + rate reduction)"); sigil_module_register_native(vm, "crush-compute", native_crush_compute, SIGIL_ARITY_EXACT(5), "Compute crush (crush in drive bits rate-div) -> sample"); sigil_module_register_native(vm, "make-comp", native_make_comp, SIGIL_ARITY_EXACT(3), "Create compressor (attack release makeup-db)"); sigil_module_register_native(vm, "comp-compute", native_comp_compute, SIGIL_ARITY_EXACT(4), "Compute compressor (comp in threshold-db ratio) -> sample"); const char *fx_exports[] = { "make-revsc", "revsc-compute", "revsc-set-feedback!", "revsc-set-lpfreq!", "make-zitarev", "zitarev-compute", "zitarev-set-mix!", "zitarev-set-level!", "make-delay", "delay-compute", "delay-set-feedback!", "make-vdelay", "vdelay-compute", "vdelay-set-del!", "vdelay-set-feedback!", "make-chorus", "chorus-compute", "make-crush", "crush-compute", "make-comp", "comp-compute", NULL }; for (int i = 0; fx_exports[i]; i++) sigil_module_export(vm, fx_exports[i]);src/c/fm4.cadded
/* * fm4.c - Four-operator phase-modulation FM voice * * Operators are numbered 0..3 (op1..op4 in the user-facing table). Every * algorithm routes modulation from a higher-numbered operator into a * lower-numbered one, so computing operators from 3 down to 0 is a valid * evaluation order for all of them; operator 3 is the feedback operator * and modulates itself through the average of its last two outputs (the * DX/OPL form, which keeps the loop stable at high feedback). */#include "fm4.h"#include <stdlib.h>#include <string.h>#include <math.h>#define FM4_TABLE_BITS 12#define FM4_TABLE_SIZE (1 << FM4_TABLE_BITS)#define FM4_TABLE_MASK (FM4_TABLE_SIZE - 1)#define FM4_TWO_PI 6.283185307179586f#define FM4_FB_SCALE 3.141592653589793f /* feedback 1.0 = pi radians */static SPFLOAT fm4_sine[FM4_TABLE_SIZE + 1];static int fm4_table_ready = 0;static void fm4_build_table(void){ if (fm4_table_ready) return; for (int i = 0; i <= FM4_TABLE_SIZE; i++) { double x = (double)i / (double)FM4_TABLE_SIZE; fm4_sine[i] = (SPFLOAT)sin(x * 6.283185307179586); } fm4_table_ready = 1;}/* Algorithm table. Operators 0..3 = op1..op4. mod_mask[i] lists who * modulates operator i; carrier_mask lists who reaches the output. */static const sp_fm4_algorithm fm4_algorithms[SP_FM4_ALGORITHMS] = { /* 0 */ { "stack", { 0x2, 0x4, 0x8, 0x0 }, 0x1 }, /* 4->3->2->1 */ /* 1 */ { "two-into-one", { 0x2, 0xC, 0x0, 0x0 }, 0x1 }, /* 4->2, 3->2, 2->1 */ /* 2 */ { "branch", { 0x6, 0x0, 0x8, 0x0 }, 0x1 }, /* 4->3->1, 2->1 */ /* 3 */ { "three-into-one", { 0xE, 0x0, 0x0, 0x0 }, 0x1 }, /* 4,3,2 -> 1 */ /* 4 */ { "pairs", { 0x2, 0x0, 0x8, 0x0 }, 0x5 }, /* 4->3, 2->1 */ /* 5 */ { "stack-plus-one", { 0x0, 0x4, 0x8, 0x0 }, 0x3 }, /* 4->3->2, 1 */ /* 6 */ { "one-into-three", { 0x8, 0x8, 0x8, 0x0 }, 0x7 }, /* 4 -> 1,2,3 */ /* 7 */ { "additive", { 0x0, 0x0, 0x0, 0x0 }, 0xF }, /* all carriers */};const sp_fm4_algorithm *sp_fm4_algorithm_table(void){ return fm4_algorithms;}int sp_fm4_create(sp_fm4 **p){ *p = (sp_fm4 *)calloc(1, sizeof(sp_fm4)); return *p ? SP_OK : SP_NOT_OK;}int sp_fm4_destroy(sp_fm4 **p){ if (!*p) return SP_OK; free(*p); *p = NULL; return SP_OK;}int sp_fm4_init(sp_data *sp, sp_fm4 *p){ fm4_build_table(); memset(p, 0, sizeof(*p)); p->sr = (SPFLOAT)sp->sr; p->algorithm = 0; p->feedback = 0.0f; p->vel_depth = 0.0f; p->vel_curve = 1.0f; for (int i = 0; i < SP_FM4_OPS; i++) sp_fm4_set_op(p, i, 1.0f, 1.0f, 0.005f, 0.1f, 1.0f, 0.1f); return SP_OK;}int sp_fm4_set_algorithm(sp_fm4 *p, int algorithm){ if (algorithm < 0 || algorithm >= SP_FM4_ALGORITHMS) return SP_NOT_OK; p->algorithm = algorithm; return SP_OK;}int sp_fm4_set_op(sp_fm4 *p, int op, SPFLOAT ratio, SPFLOAT level, SPFLOAT attack, SPFLOAT decay, SPFLOAT sustain, SPFLOAT release){ if (op < 0 || op >= SP_FM4_OPS) return SP_NOT_OK; sp_fm4_op *o = &p->op[op]; o->ratio = ratio < 0.0f ? 0.0f : ratio; o->level = level; o->attack = attack < 0.0f ? 0.0f : attack; o->decay = decay < 0.0f ? 0.0f : decay; o->sustain = sustain < 0.0f ? 0.0f : (sustain > 1.0f ? 1.0f : sustain); o->release = release < 0.0f ? 0.0f : release; return SP_OK;}/* Linear ADSR, one step. `on` is the gate state this sample. */static inline SPFLOAT fm4_env_step(sp_fm4_op *o, SPFLOAT sr, int on){ switch (o->stage) { case 1: { /* attack */ SPFLOAT samples = o->attack * sr; if (samples < 1.0f) { o->env = 1.0f; o->stage = 2; break; } o->env += 1.0f / samples; if (o->env >= 1.0f) { o->env = 1.0f; o->stage = 2; } break; } case 2: { /* decay */ SPFLOAT samples = o->decay * sr; if (samples < 1.0f) { o->env = o->sustain; o->stage = 3; break; } o->env -= (1.0f - o->sustain) / samples; if (o->env <= o->sustain) { o->env = o->sustain; o->stage = 3; } break; } case 3: /* sustain */ o->env = o->sustain; break; case 4: { /* release */ SPFLOAT samples = o->release * sr; if (samples < 1.0f) { o->env = 0.0f; o->stage = 0; break; } o->env -= o->rel_rate; if (o->env <= 0.0f) { o->env = 0.0f; o->stage = 0; } break; } default: /* idle */ o->env = 0.0f; break; } (void)on; return o->env;}static inline SPFLOAT fm4_sin(SPFLOAT phase01, SPFLOAT mod_rad){ SPFLOAT pos = phase01 * (SPFLOAT)FM4_TABLE_SIZE + mod_rad * ((SPFLOAT)FM4_TABLE_SIZE / FM4_TWO_PI); int i = (int)pos; if ((SPFLOAT)i > pos) i--; /* floor for negative positions */ SPFLOAT frac = pos - (SPFLOAT)i; i &= FM4_TABLE_MASK; return fm4_sine[i] + (fm4_sine[i + 1] - fm4_sine[i]) * frac;}int sp_fm4_compute(sp_data *sp, sp_fm4 *p, SPFLOAT freq, SPFLOAT gate, SPFLOAT index, SPFLOAT velocity, SPFLOAT *out){ const sp_fm4_algorithm *alg = &fm4_algorithms[p->algorithm]; int on = gate > 0.5f; if (on && !p->gate_prev) { /* Note on: key sync + envelope restart from zero. */ for (int i = 0; i < SP_FM4_OPS; i++) { sp_fm4_op *o = &p->op[i]; o->phase = 0.0f; o->env = 0.0f; o->stage = 1; o->out = o->out_prev = o->out_prev2 = 0.0f; } } else if (!on && p->gate_prev) { for (int i = 0; i < SP_FM4_OPS; i++) { sp_fm4_op *o = &p->op[i]; if (o->stage != 0) { SPFLOAT samples = o->release * p->sr; o->rel_rate = samples < 1.0f ? o->env : o->env / samples; o->stage = 4; } } } p->gate_prev = on; /* Velocity -> index. */ SPFLOAT v = velocity < 0.0f ? 0.0f : (velocity > 1.0f ? 1.0f : velocity); SPFLOAT vshaped = (p->vel_curve == 1.0f) ? v : powf(v, p->vel_curve); SPFLOAT index_eff = index * (1.0f - p->vel_depth + p->vel_depth * vshaped); SPFLOAT sum = 0.0f; int ncar = 0; for (int i = SP_FM4_OPS - 1; i >= 0; i--) { sp_fm4_op *o = &p->op[i]; SPFLOAT env = fm4_env_step(o, p->sr, on); SPFLOAT mod = 0.0f; unsigned char m = alg->mod_mask[i]; for (int j = i + 1; j < SP_FM4_OPS; j++) if (m & (1u << j)) mod += p->op[j].out; mod *= index_eff; if (i == SP_FM4_OPS - 1 && p->feedback > 0.0f) mod += p->feedback * FM4_FB_SCALE * 0.5f * (o->out_prev + o->out_prev2); SPFLOAT s = fm4_sin(o->phase, mod) * o->level * env; if (i == SP_FM4_OPS - 1) { o->out_prev2 = o->out_prev; o->out_prev = s; } o->out = s; o->phase += freq * o->ratio / p->sr; while (o->phase >= 1.0f) o->phase -= 1.0f; while (o->phase < 0.0f) o->phase += 1.0f; if (alg->carrier_mask & (1u << i)) { sum += s; ncar++; } } *out = ncar > 0 ? sum / (SPFLOAT)ncar : 0.0f; return SP_OK;}src/c/fm4.hadded
/* * fm4.h - Four-operator phase-modulation FM voice * * DX/OPL-style: four sine operators, an algorithm table that says which * operator modulates which and which operators are carriers, a per- * operator frequency ratio, level and linear ADSR, feedback on operator 4, * and a global modulation index that scales every modulator's depth. * * Everything here is deterministic given the parameters and the sample * rate: the sine comes from a 4096-entry table with linear interpolation * (no libm at run time), so the same inputs produce the same samples on * every target that runs the same float arithmetic. * * Signal-rate inputs (set every sample by the host, motif's render loop): * freq base frequency (Hz) * gate > 0.5 is on; a rising edge resets phases and retriggers * every operator envelope * index global modulation index (0 = every carrier is a pure sine) * velocity 0..1, scales the index through the velocity curve * * Per-operator parameters (set at init or with sp_fm4_set_op): * ratio frequency multiple of `freq` (coarse + fine folded in) * level 0..1 output level (carriers: amplitude; modulators: depth) * a d s r attack / decay / release in seconds, sustain 0..1 */#ifndef SIGIL_DSP_FM4_H#define SIGIL_DSP_FM4_H#include "soundpipe.h"#define SP_FM4_OPS 4#define SP_FM4_ALGORITHMS 8typedef struct { SPFLOAT ratio; SPFLOAT level; SPFLOAT attack, decay, sustain, release; /* state */ SPFLOAT phase; /* 0..1 */ SPFLOAT env; /* current envelope value */ int stage; /* 0 idle, 1 attack, 2 decay, 3 sustain, 4 release */ SPFLOAT out; /* this sample's output */ SPFLOAT out_prev; /* previous sample's output (feedback) */ SPFLOAT out_prev2; /* two samples back (feedback averaging) */ SPFLOAT rel_rate; /* envelope decrement per sample during release */} sp_fm4_op;typedef struct sp_fm4 { int algorithm; /* 0..SP_FM4_ALGORITHMS-1 */ SPFLOAT feedback; /* 0..1; 1 = pi radians of self-modulation on op 4 */ SPFLOAT vel_depth; /* 0..1: how much velocity scales the index */ SPFLOAT vel_curve; /* exponent applied to velocity (1 = linear) */ sp_fm4_op op[SP_FM4_OPS]; int gate_prev; SPFLOAT sr;} sp_fm4;int sp_fm4_create(sp_fm4 **p);int sp_fm4_destroy(sp_fm4 **p);/* Defaults: algorithm 0, feedback 0, vel_depth 0, vel_curve 1, every * operator ratio 1 level 1 ADSR 0.005/0.1/1.0/0.1. */int sp_fm4_init(sp_data *sp, sp_fm4 *p);int sp_fm4_set_algorithm(sp_fm4 *p, int algorithm);int sp_fm4_set_op(sp_fm4 *p, int op, SPFLOAT ratio, SPFLOAT level, SPFLOAT attack, SPFLOAT decay, SPFLOAT sustain, SPFLOAT release);/* One sample. */int sp_fm4_compute(sp_data *sp, sp_fm4 *p, SPFLOAT freq, SPFLOAT gate, SPFLOAT index, SPFLOAT velocity, SPFLOAT *out);/* Algorithm table entries: mod_mask[i] is the bitmask of operators that * modulate operator i; carrier_mask is the bitmask of operators summed * into the output. Exposed so hosts can print or validate a table. */typedef struct { const char *name; unsigned char mod_mask[SP_FM4_OPS]; unsigned char carrier_mask;} sp_fm4_algorithm;const sp_fm4_algorithm *sp_fm4_algorithm_table(void);#endifsrc/c/fx4.cadded
/* * fx4.c - chorus, crush, compressor (see fx4.h) */#include "fx4.h"#include <stdlib.h>#include <string.h>#include <math.h>/* ============================================================ chorus */#define CHORUS_CENTRE_MS 15.0fint sp_chorus_create(sp_chorus **p){ *p = (sp_chorus *)calloc(1, sizeof(sp_chorus)); return *p ? SP_OK : SP_NOT_OK;}int sp_chorus_destroy(sp_chorus **p){ if (!*p) return SP_OK; free((*p)->buf); free(*p); *p = NULL; return SP_OK;}int sp_chorus_init(sp_data *sp, sp_chorus *p){ p->sr = (SPFLOAT)sp->sr; p->bufsize = (int)(SP_CHORUS_MAX_MS * 0.001f * p->sr) + 4; p->buf = (SPFLOAT *)calloc((size_t)p->bufsize, sizeof(SPFLOAT)); p->pos = 0; p->lfo_phase = 0.0f; return p->buf ? SP_OK : SP_NOT_OK;}/* Read `delay` samples back from the write position, linear interp. */static inline SPFLOAT chorus_tap(const sp_chorus *p, SPFLOAT delay){ SPFLOAT maxd = (SPFLOAT)(p->bufsize - 2); if (delay < 1.0f) delay = 1.0f; if (delay > maxd) delay = maxd; SPFLOAT rpos = (SPFLOAT)p->pos - delay; while (rpos < 0.0f) rpos += (SPFLOAT)p->bufsize; int i0 = (int)rpos; SPFLOAT frac = rpos - (SPFLOAT)i0; int i1 = i0 + 1; if (i1 >= p->bufsize) i1 -= p->bufsize; return p->buf[i0] + (p->buf[i1] - p->buf[i0]) * frac;}/* Triangle in [-1, 1] from a 0..1 phase. */static inline SPFLOAT tri_lfo(SPFLOAT phase){ SPFLOAT t = phase < 0.5f ? phase * 4.0f - 1.0f : 3.0f - phase * 4.0f; return t;}int sp_chorus_compute(sp_data *sp, sp_chorus *p, SPFLOAT in, SPFLOAT rate, SPFLOAT depth, SPFLOAT mix, SPFLOAT *out){ p->buf[p->pos] = in; SPFLOAT ms_per_sample = 0.001f * p->sr; SPFLOAT centre = CHORUS_CENTRE_MS * ms_per_sample; SPFLOAT dev = depth * ms_per_sample; SPFLOAT ph2 = p->lfo_phase + 0.5f; if (ph2 >= 1.0f) ph2 -= 1.0f; SPFLOAT tap1 = chorus_tap(p, centre + dev * tri_lfo(p->lfo_phase)); SPFLOAT tap2 = chorus_tap(p, centre * 0.7f + dev * tri_lfo(ph2)); SPFLOAT wet = 0.5f * (tap1 + tap2); if (mix < 0.0f) mix = 0.0f; if (mix > 1.0f) mix = 1.0f; *out = in * (1.0f - mix) + wet * mix; p->pos++; if (p->pos >= p->bufsize) p->pos = 0; if (rate < 0.0f) rate = 0.0f; p->lfo_phase += rate / p->sr; while (p->lfo_phase >= 1.0f) p->lfo_phase -= 1.0f; return SP_OK;}/* ============================================================ crush */int sp_crush_create(sp_crush **p){ *p = (sp_crush *)calloc(1, sizeof(sp_crush)); return *p ? SP_OK : SP_NOT_OK;}int sp_crush_destroy(sp_crush **p){ if (!*p) return SP_OK; free(*p); *p = NULL; return SP_OK;}int sp_crush_init(sp_data *sp, sp_crush *p){ p->held = 0.0f; p->counter = 1.0e9f; /* first sample is always sampled */ return SP_OK;}int sp_crush_compute(sp_data *sp, sp_crush *p, SPFLOAT in, SPFLOAT drive, SPFLOAT bits, SPFLOAT rate_div, SPFLOAT *out){ SPFLOAT y = in; /* Saturation: unity gain at |in| = 1, identity at drive 0. */ if (drive > 0.0f) { SPFLOAT g = 1.0f + drive; y = tanhf(y * g) / tanhf(g); } /* Bit depth: 16 or more leaves the signal alone. */ int b = (int)bits; if (b < 1) b = 1; if (b < 16) { SPFLOAT steps = (SPFLOAT)(1 << (b - 1)); SPFLOAT q = y * steps; int qi = (int)(q + (q >= 0.0f ? 0.5f : -0.5f)); y = (SPFLOAT)qi / steps; } /* Sample-rate reduction: hold the value for rate_div samples. */ if (rate_div < 1.0f) rate_div = 1.0f; p->counter += 1.0f; if (p->counter >= rate_div) { p->held = y; p->counter -= rate_div; if (p->counter > rate_div) p->counter = 0.0f; } *out = p->held; return SP_OK;}/* ============================================================ compressor */int sp_comp_create(sp_comp **p){ *p = (sp_comp *)calloc(1, sizeof(sp_comp)); return *p ? SP_OK : SP_NOT_OK;}int sp_comp_destroy(sp_comp **p){ if (!*p) return SP_OK; free(*p); *p = NULL; return SP_OK;}static inline SPFLOAT onepole_coef(SPFLOAT seconds, SPFLOAT sr){ if (seconds <= 0.0f) return 0.0f; return expf(-1.0f / (seconds * sr));}int sp_comp_init(sp_data *sp, sp_comp *p, SPFLOAT attack, SPFLOAT release, SPFLOAT makeup_db){ p->sr = (SPFLOAT)sp->sr; p->attack = attack; p->release = release; p->makeup = makeup_db; p->attack_coef = onepole_coef(attack, p->sr); p->release_coef = onepole_coef(release, p->sr); p->makeup_lin = powf(10.0f, makeup_db / 20.0f); p->env = 0.0f; return SP_OK;}int sp_comp_compute(sp_data *sp, sp_comp *p, SPFLOAT in, SPFLOAT threshold_db, SPFLOAT ratio, SPFLOAT *out){ SPFLOAT level = in < 0.0f ? -in : in; if (level > p->env) p->env = p->attack_coef * p->env + (1.0f - p->attack_coef) * level; else p->env = p->release_coef * p->env + (1.0f - p->release_coef) * level; SPFLOAT gain = 1.0f; if (ratio > 1.0f) { SPFLOAT thr = powf(10.0f, threshold_db / 20.0f); if (p->env > thr && thr > 0.0f) { /* Gain that maps the over-threshold amount through 1/ratio. */ gain = powf(p->env / thr, 1.0f / ratio - 1.0f); } } *out = in * gain * p->makeup_lin; return SP_OK;}src/c/fx4.hadded
/* * fx4.h - The tracker fatteners: chorus, crush, compressor * * Three small mono effects written for motif's DSP graph. All parameters * that a tracker effects column would want to move are signal-rate * arguments to the compute call; the rest are set at init. * * sp_chorus two LFO-modulated taps on one delay line, mixed with the dry * signal. compute(in, rate_hz, depth_ms, mix) * sp_crush tanh drive, then bit-depth quantise, then sample-and-hold * rate reduction. compute(in, drive, bits, rate_div) * sp_comp feed-forward peak compressor with a one-pole attack/release * envelope. compute(in, threshold_db, ratio); attack, release * and makeup gain are init-time. * * Deterministic: no randomness, no libm on the per-sample path except * tanhf in the crush drive stage and the dB conversions in the compressor * (both computed from float inputs the same way on every target). */#ifndef SIGIL_DSP_FX4_H#define SIGIL_DSP_FX4_H#include "soundpipe.h"/* ---- chorus ---- */#define SP_CHORUS_MAX_MS 40.0ftypedef struct sp_chorus { SPFLOAT *buf; int bufsize; int pos; SPFLOAT lfo_phase; /* 0..1 */ SPFLOAT sr;} sp_chorus;int sp_chorus_create(sp_chorus **p);int sp_chorus_destroy(sp_chorus **p);int sp_chorus_init(sp_data *sp, sp_chorus *p);/* rate in Hz, depth in ms (peak deviation around a 15 ms centre), mix 0..1 */int sp_chorus_compute(sp_data *sp, sp_chorus *p, SPFLOAT in, SPFLOAT rate, SPFLOAT depth, SPFLOAT mix, SPFLOAT *out);/* ---- crush ---- */typedef struct sp_crush { SPFLOAT held; /* last sampled value for rate reduction */ SPFLOAT counter; /* samples since last hold */} sp_crush;int sp_crush_create(sp_crush **p);int sp_crush_destroy(sp_crush **p);int sp_crush_init(sp_data *sp, sp_crush *p);/* drive >= 0 (0 = clean), bits 1..16 (16 = no quantisation), rate_div >= 1 * (1 = no rate reduction; N = hold every value for N samples) */int sp_crush_compute(sp_data *sp, sp_crush *p, SPFLOAT in, SPFLOAT drive, SPFLOAT bits, SPFLOAT rate_div, SPFLOAT *out);/* ---- compressor ---- */typedef struct sp_comp { SPFLOAT attack; /* seconds */ SPFLOAT release; /* seconds */ SPFLOAT makeup; /* dB */ SPFLOAT env; /* envelope follower state (linear) */ SPFLOAT attack_coef; SPFLOAT release_coef; SPFLOAT makeup_lin; SPFLOAT sr;} sp_comp;int sp_comp_create(sp_comp **p);int sp_comp_destroy(sp_comp **p);int sp_comp_init(sp_data *sp, sp_comp *p, SPFLOAT attack, SPFLOAT release, SPFLOAT makeup_db);/* threshold in dBFS (<= 0), ratio >= 1 (1 = no compression) */int sp_comp_compute(sp_data *sp, sp_comp *p, SPFLOAT in, SPFLOAT threshold_db, SPFLOAT ratio, SPFLOAT *out);#endifsrc/sigil/dsp/fx.sglmodified
make-revsc revsc-compute revsc-set-feedback! revsc-set-lpfreq! make-zitarev zitarev-compute zitarev-set-mix! zitarev-set-level! make-delay delay-compute delay-set-feedback! make-vdelay vdelay-compute vdelay-set-del! vdelay-set-feedback!) make-vdelay vdelay-compute vdelay-set-del! vdelay-set-feedback! make-chorus chorus-compute make-crush crush-compute make-comp comp-compute) (begin (define-native (make-revsc feedback lpfreq)) (define-native (make-vdelay maxdel feedback)) (define-native (vdelay-compute vdelay input)) (define-native (vdelay-set-del! vdelay del)) (define-native (vdelay-set-feedback! vdelay feedback)))) (define-native (vdelay-set-feedback! vdelay feedback)) ;; Tracker fatteners (fx4.h). All per-sample parameters are ;; arguments to the compute call so a host can automate them. ;;; Two-tap modulated-delay chorus around a 15 ms centre. (define-native (make-chorus) (: -> foreign?)) ;;; One sample: LFO `rate` in Hz, `depth-ms` peak deviation, `mix` 0..1 ;;; (0 is the dry input unchanged). (define-native (chorus-compute chorus input rate depth-ms mix) (: foreign? number? number? number? number? -> number?)) ;;; Saturation, bit-depth reduction and sample-rate reduction in one node. (define-native (make-crush) (: -> foreign?)) ;;; One sample: tanh `drive` (0 = clean), quantise to `bits` (16 = none), ;;; hold each value for `rate-div` samples (1 = none). (define-native (crush-compute crush input drive bits rate-div) (: foreign? number? number? number? number? -> number?)) ;;; Feed-forward peak compressor; attack and release in seconds, ;;; makeup gain in dB. (define-native (make-comp attack release makeup-db) (: number? number? number? -> foreign?)) ;;; One sample: `threshold-db` in dBFS, `ratio` >= 1 (1 = bypass). (define-native (comp-compute comp input threshold-db ratio) (: foreign? number? number? number? -> number?))))src/sigil/dsp/osc.sglmodified
make-fosc fosc-compute fosc-set-freq! fosc-set-index! fosc-set-amp! make-phasor phasor-compute phasor-set-freq! make-ks ks-compute ks-trigger! ks-set-freq! ks-set-decay! ks-set-excite-gain!) ks-set-freq! ks-set-decay! ks-set-excite-gain! make-fm4 fm4-set-op! fm4-set-algorithm! fm4-set-feedback! fm4-compute fm4-algorithm-name) (begin (define-native make-osc) (define-native (ks-set-decay! ks decay)) (define-native (ks-set-excite-gain! ks gain)) ;; Four-operator FM voice (fm4.h). Operators are 0..3; algorithm ;; 0..7; feedback 0..1 on operator 3; velocity scales the index ;; through (1 - vel-depth + vel-depth * velocity^vel-curve). ;;; Create a four-operator FM voice. `algorithm` is 0..7 (see ;;; fm4-algorithm-name), `feedback` 0..1 self-modulates operator 3, ;;; and velocity scales the index through ;;; (1 - vel-depth + vel-depth * velocity^vel-curve). (define-native (make-fm4 algorithm feedback vel-depth vel-curve) (: number? number? number? number? -> foreign?)) ;;; Set operator `op` (0..3): frequency ratio, level 0..1 and a ;;; linear ADSR (attack, decay, release in seconds; sustain 0..1). (define-native (fm4-set-op! fm4 op ratio level attack decay sustain release) (: foreign? integer? number? number? number? number? number? number? -> any?)) (define-native (fm4-set-algorithm! fm4 algorithm) (: foreign? integer? -> any?)) (define-native (fm4-set-feedback! fm4 feedback) (: foreign? number? -> any?)) ;;; One sample. A rising edge on `gate` (> 0.5) resets every ;;; operator's phase and restarts its envelope; index 0 makes each ;;; carrier a pure sine at freq * ratio. (define-native (fm4-compute fm4 freq gate index velocity) (: foreign? number? number? number? number? -> number?)) ;;; Name of algorithm 0..7, or #f out of range. (define-native (fm4-algorithm-name algorithm) (: integer? -> (maybe string?))) ;;; Create an oscillator by type. ;;; Types: sine, saw, square, triangle, fm (define (make-oscillator type freq amp . opts)