memlnaut-nisps/playground/faust/fm-matrix.dsp
w1n5t0n 67d2230ba8 feat(playground/faust): full 56-param FM matrix synth DSP (meml-wgg)
- Full 4-operator FM with continuous 4×4 routing matrix (12 cross-mod pairs)
- Compiles to 46KB WASM via faust -lang wasm
- 55 visible NISPS params (pitch_glide missing from DSP, tracked)
- Includes fm-matrix-processor.js AudioWorklet + JSON param descriptor
2026-04-03 17:41:54 +01:00

243 lines
12 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// fm-matrix.dsp — Full 56-parameter 4-operator FM synthesizer with continuous routing matrix
//
// Design: 4 operators with fully continuous N×N cross-modulation matrix.
// No fixed algorithm slots — the algorithm emerges from modulation index values.
// When all routing matrix indices are near 0: additive synthesis.
// As indices grow: increasingly complex FM timbres emerge.
//
// Parameter ordering (56 total, maps to MLP output indices 055):
// 023: Operator Core × 4 (ratio, level, attack, decay, sustain, release)
// 2435: Cross-Modulation Matrix (12 directed pairs)
// 3639: Self-Feedback × 4
// 4047: Global Modulation (LFO, pitch env, velocity)
// 4855: Master (level, vel sens, glide, fine tune, waveform blend, stereo spread,
// output saturation, output HP)
//
// Hidden params (not in the 56-param NISPS list):
// freq — set by noteOn (Hz)
// gate — set by noteOn/noteOff (0/1)
// _vel — set by noteOn (velocity 01)
//
// FM implementation: each operator phase-modulates others via a 1-sample delayed
// feedback bus. The 4 operator outputs are collected into a 4-channel bus and
// looped via ~. This is the standard Faust FM pattern.
//
// Build:
// nix-shell -p faust --run \
// "faust -lang wasm -cn fm_matrix fm-matrix.dsp -o fm-matrix.wasm"
import("stdfaust.lib");
// ---------------------------------------------------------------------------
// Hidden control params (driven by noteOn/noteOff, not NISPS)
// ---------------------------------------------------------------------------
freq = hslider("Master/freq[hidden:1][unit:Hz]", 220, 20, 4000, 0.01);
gate = button("Master/gate[hidden:1]");
vel = hslider("Master/_vel[hidden:1]", 0.7, 0.0, 1.0, 0.001) : si.smoo;
// ---------------------------------------------------------------------------
// Group 1 — Operator Core × 4 (params 023)
// ---------------------------------------------------------------------------
op1_ratio = hslider("Operators/Op 1/op1_ratio", 1.0, 0.125, 16.0, 0.001) : si.smoo;
op1_level = hslider("Operators/Op 1/op1_level", 0.8, 0.0, 1.0, 0.001) : si.smoo;
op1_attack = hslider("Operators/Op 1/op1_attack", 0.01, 0.001, 5.0, 0.001);
op1_decay = hslider("Operators/Op 1/op1_decay", 0.3, 0.001, 10.0, 0.001);
op1_sustain = hslider("Operators/Op 1/op1_sustain", 0.7, 0.0, 1.0, 0.001);
op1_release = hslider("Operators/Op 1/op1_release", 0.5, 0.01, 10.0, 0.001);
op2_ratio = hslider("Operators/Op 2/op2_ratio", 2.0, 0.125, 16.0, 0.001) : si.smoo;
op2_level = hslider("Operators/Op 2/op2_level", 0.6, 0.0, 1.0, 0.001) : si.smoo;
op2_attack = hslider("Operators/Op 2/op2_attack", 0.01, 0.001, 5.0, 0.001);
op2_decay = hslider("Operators/Op 2/op2_decay", 0.3, 0.001, 10.0, 0.001);
op2_sustain = hslider("Operators/Op 2/op2_sustain", 0.7, 0.0, 1.0, 0.001);
op2_release = hslider("Operators/Op 2/op2_release", 0.5, 0.01, 10.0, 0.001);
op3_ratio = hslider("Operators/Op 3/op3_ratio", 3.0, 0.125, 16.0, 0.001) : si.smoo;
op3_level = hslider("Operators/Op 3/op3_level", 0.4, 0.0, 1.0, 0.001) : si.smoo;
op3_attack = hslider("Operators/Op 3/op3_attack", 0.01, 0.001, 5.0, 0.001);
op3_decay = hslider("Operators/Op 3/op3_decay", 0.3, 0.001, 10.0, 0.001);
op3_sustain = hslider("Operators/Op 3/op3_sustain", 0.7, 0.0, 1.0, 0.001);
op3_release = hslider("Operators/Op 3/op3_release", 0.5, 0.01, 10.0, 0.001);
op4_ratio = hslider("Operators/Op 4/op4_ratio", 0.5, 0.125, 16.0, 0.001) : si.smoo;
op4_level = hslider("Operators/Op 4/op4_level", 0.3, 0.0, 1.0, 0.001) : si.smoo;
op4_attack = hslider("Operators/Op 4/op4_attack", 0.01, 0.001, 5.0, 0.001);
op4_decay = hslider("Operators/Op 4/op4_decay", 0.3, 0.001, 10.0, 0.001);
op4_sustain = hslider("Operators/Op 4/op4_sustain", 0.7, 0.0, 1.0, 0.001);
op4_release = hslider("Operators/Op 4/op4_release", 0.5, 0.01, 10.0, 0.001);
// ---------------------------------------------------------------------------
// Group 2 — Cross-Modulation Matrix (params 2435)
// mXY = op X modulates op Y (X's output is added to Y's phase)
// ---------------------------------------------------------------------------
m12 = hslider("Matrix/m12", 0.0, 0.0, 10.0, 0.001) : si.smoo;
m13 = hslider("Matrix/m13", 0.0, 0.0, 10.0, 0.001) : si.smoo;
m14 = hslider("Matrix/m14", 0.0, 0.0, 10.0, 0.001) : si.smoo;
m21 = hslider("Matrix/m21", 1.0, 0.0, 10.0, 0.001) : si.smoo;
m23 = hslider("Matrix/m23", 0.0, 0.0, 10.0, 0.001) : si.smoo;
m24 = hslider("Matrix/m24", 0.0, 0.0, 10.0, 0.001) : si.smoo;
m31 = hslider("Matrix/m31", 0.0, 0.0, 10.0, 0.001) : si.smoo;
m32 = hslider("Matrix/m32", 0.0, 0.0, 10.0, 0.001) : si.smoo;
m34 = hslider("Matrix/m34", 0.0, 0.0, 10.0, 0.001) : si.smoo;
m41 = hslider("Matrix/m41", 0.0, 0.0, 10.0, 0.001) : si.smoo;
m42 = hslider("Matrix/m42", 0.0, 0.0, 10.0, 0.001) : si.smoo;
m43 = hslider("Matrix/m43", 0.0, 0.0, 10.0, 0.001) : si.smoo;
// ---------------------------------------------------------------------------
// Group 3 — Self-Feedback × 4 (params 3639)
// ---------------------------------------------------------------------------
fb1 = hslider("Feedback/fb1", 0.0, 0.0, 1.0, 0.001) : si.smoo;
fb2 = hslider("Feedback/fb2", 0.0, 0.0, 1.0, 0.001) : si.smoo;
fb3 = hslider("Feedback/fb3", 0.0, 0.0, 1.0, 0.001) : si.smoo;
fb4 = hslider("Feedback/fb4", 0.0, 0.0, 1.0, 0.001) : si.smoo;
// ---------------------------------------------------------------------------
// Group 4 — Global Modulation (params 4047)
// ---------------------------------------------------------------------------
lfo_rate = hslider("Global/lfo_rate", 5.0, 0.01, 20.0, 0.001);
lfo_depth = hslider("Global/lfo_depth", 0.0, 0.0, 1.0, 0.001) : si.smoo;
lfo_pitch = hslider("Global/lfo_pitch", 0.5, 0.0, 1.0, 0.001) : si.smoo;
lfo_levels = hslider("Global/lfo_levels", 0.5, 0.0, 1.0, 0.001) : si.smoo;
lfo_waveform = hslider("Global/lfo_waveform", 0.0, 0.0, 1.0, 0.001);
pitch_env_amount = hslider("Global/pitch_env_amount", 0.0, 0.0, 1.0, 0.001) : si.smoo;
pitch_env_decay = hslider("Global/pitch_env_decay", 0.1, 0.001, 2.0, 0.001);
vel_index_scale = hslider("Global/vel_index_scale", 0.0, 0.0, 1.0, 0.001) : si.smoo;
// ---------------------------------------------------------------------------
// Group 5 — Master (params 4855)
// ---------------------------------------------------------------------------
master_level = hslider("Master/level", 0.7, 0.0, 1.0, 0.001) : si.smoo;
vel_sens = hslider("Master/vel_sens", 0.5, 0.0, 1.0, 0.001);
pitch_glide = hslider("Master/pitch_glide", 0.0, 0.0, 10.0, 0.001);
fine_tune = hslider("Master/fine_tune[unit:cents]", 0.0, -50.0, 50.0, 0.01);
waveform_blend = hslider("Master/waveform_blend", 0.0, 0.0, 1.0, 0.001) : si.smoo;
stereo_spread = hslider("Master/stereo_spread", 0.1, 0.0, 1.0, 0.001) : si.smoo;
output_saturation = hslider("Master/output_saturation", 0.0, 0.0, 1.0, 0.001) : si.smoo;
output_hp = hslider("Master/output_hp[unit:Hz]", 20.0, 20.0, 200.0, 0.1);
// ---------------------------------------------------------------------------
// Derived values
// ---------------------------------------------------------------------------
// Cents → frequency ratio
cent2ratio(c) = pow(2.0, c / 1200.0);
// Base frequency with fine-tune
base_freq = freq * cent2ratio(fine_tune);
// Velocity gain: blend between 1.0 (no velocity) and actual velocity
vel_gain = (1.0 - vel_sens) + vel_sens * vel;
// Velocity-scaled FM index multiplier
// vel_index_scale=0: velocity doesn't affect FM indices
// vel_index_scale=1: FM indices are fully scaled by velocity
vel_idx_mult = (1.0 - vel_index_scale) + vel_index_scale * vel;
// LFO: 4 waveforms crossfaded
// Segments: [0,0.333)=sine→tri, [0.333,0.667)=tri→saw, [0.667,1]=saw→square
lfo_sig =
ba.if(lfo_waveform < 0.333,
(1.0 - lfo_waveform*3.0) * os.osc(lfo_rate) + lfo_waveform*3.0 * os.triangle(lfo_rate),
ba.if(lfo_waveform < 0.667,
(1.0 - (lfo_waveform-0.333)*3.0) * os.triangle(lfo_rate) + (lfo_waveform-0.333)*3.0 * os.sawtooth(lfo_rate),
(1.0 - (lfo_waveform-0.667)*3.0) * os.sawtooth(lfo_rate) + (lfo_waveform-0.667)*3.0 * os.square(lfo_rate)));
// Pitch envelope: fast attack, configurable decay, triggered by gate
pitch_env = en.ar(0.001, pitch_env_decay, gate);
// Modulated frequency: fine-tune + LFO pitch + pitch envelope
// LFO pitch: ±0.0833 semitones per unit depth (subtle vibrato)
// Pitch env: up to +1 octave (ratio +1.0 = +octave)
lfo_pitch_offset = lfo_sig * lfo_depth * lfo_pitch * 0.0083; // ~0.1 semitone max
pitch_env_offset = pitch_env * pitch_env_amount; // 0..1 oct
mod_freq = base_freq * pow(2.0, lfo_pitch_offset + pitch_env_offset);
// LFO level modulation (tremolo): 1.0 when depth=0, dips to (1-0.5*depth*lfo_levels) at trough
lfo_level_factor = 1.0 - lfo_depth * lfo_levels * 0.5 * (1.0 - lfo_sig) * 0.5;
// Per-operator oscillator: blend sine (0) → triangle (1)
op_osc(f) = (1.0 - waveform_blend) * os.osc(f) + waveform_blend * os.triangle(f);
// Per-operator ADSR envelopes
env1 = en.adsr(op1_attack, op1_decay, op1_sustain, op1_release, gate);
env2 = en.adsr(op2_attack, op2_decay, op2_sustain, op2_release, gate);
env3 = en.adsr(op3_attack, op3_decay, op3_sustain, op3_release, gate);
env4 = en.adsr(op4_attack, op4_decay, op4_sustain, op4_release, gate);
// ---------------------------------------------------------------------------
// 4-Operator FM Routing
//
// Implementation strategy: build a recursive 4-channel bus using ~
// The bus carries (op1_out, op2_out, op3_out, op4_out) with 1-sample delay.
// Each frame, we compute new outputs from delayed previous outputs.
//
// fmbus: (d1,d2,d3,d4) → (op1_out,op2_out,op3_out,op4_out)
// where dN are the 1-sample-delayed previous outputs (via ~)
//
// FM phase deviation: deviation_hz = index * modulator_out * carrier_freq
// This is the standard "frequency modulation" formula where modulator_out ∈ [-1,1]
// ---------------------------------------------------------------------------
fmbus(d1,d2,d3,d4) =
op1_out, op2_out, op3_out, op4_out
with {
// Effective indices scaled by velocity
m12e = m12 * vel_idx_mult; m13e = m13 * vel_idx_mult; m14e = m14 * vel_idx_mult;
m21e = m21 * vel_idx_mult; m23e = m23 * vel_idx_mult; m24e = m24 * vel_idx_mult;
m31e = m31 * vel_idx_mult; m32e = m32 * vel_idx_mult; m34e = m34 * vel_idx_mult;
m41e = m41 * vel_idx_mult; m42e = m42 * vel_idx_mult; m43e = m43 * vel_idx_mult;
fb1e = fb1 * vel_idx_mult; fb2e = fb2 * vel_idx_mult;
fb3e = fb3 * vel_idx_mult; fb4e = fb4 * vel_idx_mult;
f1 = mod_freq * op1_ratio;
f2 = mod_freq * op2_ratio;
f3 = mod_freq * op3_ratio;
f4 = mod_freq * op4_ratio;
op1_out = env1 * op1_level * lfo_level_factor * vel_gain *
op_osc(f1 + (m21e*d2 + m31e*d3 + m41e*d4 + fb1e*d1) * f1);
op2_out = env2 * op2_level * lfo_level_factor * vel_gain *
op_osc(f2 + (m12e*d1 + m32e*d3 + m42e*d4 + fb2e*d2) * f2);
op3_out = env3 * op3_level * lfo_level_factor * vel_gain *
op_osc(f3 + (m13e*d1 + m23e*d2 + m43e*d4 + fb3e*d3) * f3);
op4_out = env4 * op4_level * lfo_level_factor * vel_gain *
op_osc(f4 + (m14e*d1 + m24e*d2 + m34e*d3 + fb4e*d4) * f4);
};
// Route 4-channel bus through feedback loop (introduces mandatory 1-sample delay)
// Output of fmbus is (op1,op2,op3,op4); sum all for audio out
fm_out = (fmbus ~ (si.bus(4))) : (_, _, _, _) :> _;
// ---------------------------------------------------------------------------
// Post-processing
// ---------------------------------------------------------------------------
// Soft clip: tanh approximation (drive controlled by output_saturation)
// At 0: linear passthrough. At 1: clips at ±~0.6
soft_clip(x) = x / max(0.001, 1.0 + output_saturation * abs(x));
// Stereo spread via slight pitch detuning L vs R
spread_cents = stereo_spread * 5.0; // ±5 cents max
// HP filter removes DC offset from heavy feedback FM
hp_out(x) = fi.highpass(1, output_hp, x);
// ---------------------------------------------------------------------------
// process: mono FM sum → scale → soft-clip → HP → stereo spread
// ---------------------------------------------------------------------------
process =
fm_out
: *(master_level * 0.25) // 4 ops summing: scale to safe range
: soft_clip
: hp_out
<: *(cent2ratio(spread_cents)), *(cent2ratio(0.0 - spread_cents));