#!/usr/bin/env python3 """ gen-modular-dsp.py — Emit modular-*.dsp files for the Faust "Modular" mode. The modulation matrix has 48 sources × 10 destinations and every amount must be a unique hslider (Faust slider labels are compile-time string literals). Hand- writing 480+ sliders is impractical, so this script produces a fully expanded .dsp file with one hslider per (source, destination) cell plus an explicit 48-term weighted-sum expression for each destination. The generator emits three engines in one run: modular-subtractive.dsp Minimoog-style 3-osc + moog filter voice modular-additive.dsp 64-partial additive voice (spectral descriptors) modular-fm.dsp 4-operator FM voice with full cross-mod matrix Each engine has its own 48×10 modulation matrix and its own set of sound parameters. All modulation (envelopes, LFOs) comes from the shared mod-pool.lib — the engines themselves contain ZERO internal envs/LFOs. Run: python3 gen-modular-dsp.py Re-running this script is reproducible: same inputs produce byte-identical output. Please DO NOT hand-edit the generated .dsp files — edit this generator and re-run it instead. """ import os N_SRC = 48 # --------------------------------------------------------------------------- # Engine definitions. One dict per engine; each has: # name file stem ("modular-.dsp") # title header comment title # description short audio-path description # destinations list of (idx, short_name, doc) — exactly 10 entries # sound_params Faust source for engine sound params (hsliders/buttons) # signal_flow Faust source for derived signals + process line # --------------------------------------------------------------------------- # Common hidden-controls block (identical for all engines) HIDDEN_BLOCK = """\ // --------------------------------------------------------------------------- // Hidden controls // --------------------------------------------------------------------------- freq = hslider("0_Hidden/freq[hidden:1][unit:Hz]", 220, 20, 4000, 0.01); gate = button("0_Hidden/gate[hidden:1]"); vel = hslider("0_Hidden/_vel[hidden:1]", 0.7, 0.0, 1.0, 0.001) : si.smoo; """ # --------------------------------------------------------------------------- # Engine 1 — Subtractive (Minimoog-style) # --------------------------------------------------------------------------- SUBTRACTIVE_DESTS = [ (0, "pitch", "semitones, applied to all osc frequencies"), (1, "osc2_detune", "cents offset added to osc2 detune knob"), (2, "osc3_detune", "cents offset added to osc3 detune knob"), (3, "osc_mix_bal", "-1..+1 crossfades osc1<->osc3"), (4, "noise_level", "added to noise level knob"), (5, "cutoff", "+/-5 octaves of filter cutoff modulation"), (6, "resonance", "added to res knob"), (7, "filter_env_amt", "second independent +/-5 octave cutoff mod channel"), (8, "amp", "added to amp knob (route an ADSR here for a VCA env)"), (9, "pan", "added to pan knob"), ] SUBTRACTIVE_PARAMS = """\ // --------------------------------------------------------------------------- // Group 1 — Oscillators // --------------------------------------------------------------------------- osc1_wave = hslider("1_Oscillators/00_osc1_wave[tooltip:0=saw,0.5=tri,1=square]", 0.0, 0.0, 1.0, 0.001); osc1_range = hslider("1_Oscillators/01_osc1_range[unit:oct]", 0.0, -2.0, 2.0, 0.01); osc1_level = hslider("1_Oscillators/02_osc1_level", 0.8, 0.0, 1.0, 0.001); osc2_wave = hslider("1_Oscillators/03_osc2_wave[tooltip:0=saw,0.5=tri,1=square]", 0.0, 0.0, 1.0, 0.001); osc2_range = hslider("1_Oscillators/04_osc2_range[unit:oct]", 0.0, -2.0, 2.0, 0.01); osc2_detune = hslider("1_Oscillators/05_osc2_detune[unit:ct]", 0.0, -50.0, 50.0, 0.1); osc2_level = hslider("1_Oscillators/06_osc2_level", 0.6, 0.0, 1.0, 0.001); osc3_wave = hslider("1_Oscillators/07_osc3_wave[tooltip:0=saw,0.5=tri,1=square]", 0.5, 0.0, 1.0, 0.001); osc3_range = hslider("1_Oscillators/08_osc3_range[unit:oct]", -1.0, -2.0, 2.0, 0.01); osc3_detune = hslider("1_Oscillators/09_osc3_detune[unit:ct]", 0.0, -50.0, 50.0, 0.1); osc3_level = hslider("1_Oscillators/10_osc3_level", 0.4, 0.0, 1.0, 0.001); osc3_kb_track = hslider("1_Oscillators/11_osc3_kb_track[tooltip:1=tracks keyboard,0=LFO]", 1.0, 0.0, 1.0, 1.0); // --------------------------------------------------------------------------- // Group 2 — Mixer // --------------------------------------------------------------------------- noise_type = hslider("2_Mixer/00_noise_type[tooltip:0=white,1=pink]", 0.0, 0.0, 1.0, 1.0); noise_level = hslider("2_Mixer/01_noise_level", 0.0, 0.0, 1.0, 0.001); mixer_drive = hslider("2_Mixer/02_mixer_drive[tooltip:Pre-filter overdrive]", 1.0, 0.5, 4.0, 0.001); // --------------------------------------------------------------------------- // Group 3 — Filter // --------------------------------------------------------------------------- cutoff = hslider("3_Filter/00_cutoff[scale:log][unit:Hz]", 1200.0, 20.0, 20000.0, 0.1); resonance = hslider("3_Filter/01_resonance", 0.3, 0.0, 1.0, 0.001); filter_kb = hslider("3_Filter/02_filter_kb_track", 0.5, 0.0, 1.0, 0.001); // --------------------------------------------------------------------------- // Group 4 — Master // --------------------------------------------------------------------------- master_level = hslider("4_Master/00_master_level", 0.7, 0.0, 1.0, 0.001); master_glide = hslider("4_Master/01_master_glide[unit:s][scale:log]", 0.0, 0.0, 2.0, 0.001); master_tune = hslider("4_Master/02_master_tune[unit:ct]", 0.0, -50.0, 50.0, 0.1); master_pan = hslider("4_Master/03_master_pan", 0.0, -1.0, 1.0, 0.001); // Static amp floor. At 1.0 the voice is always fully open and d08_amp // modulation is purely additive decoration. Drop to 0 for classic // ADSR-gated VCA behaviour (route s00_d08_amp to an ADSR). base_amp = hslider("4_Master/04_base_amp", 1.0, 0.0, 1.0, 0.001); """ SUBTRACTIVE_FLOW = """\ // --------------------------------------------------------------------------- // Derived signals // --------------------------------------------------------------------------- cent2ratio(c) = pow(2.0, c / 1200.0); oct2ratio(o) = pow(2.0, o); semi2ratio(s) = pow(2.0, s / 12.0); // Portamento glide_tau = 0.0001 + master_glide * 1.0; freq_glided = freq : si.smooth(ba.tau2pole(glide_tau)); // Base (glide + master fine tune) base_freq = freq_glided * cent2ratio(master_tune); // --------------------------------------------------------------------------- // Per-oscillator wavetable (saw → tri → square crossfade) // 0.0 → saw, 0.5 → triangle, 1.0 → square. Output in [-1,+1]. // --------------------------------------------------------------------------- osc_wave(shape, f) = ba.if(shape < 0.5, (1.0 - shape*2.0) * os.lf_saw(f) + (shape*2.0) * os.lf_triangle(f), (1.0 - (shape-0.5)*2.0) * os.lf_triangle(f) + ((shape-0.5)*2.0) * os.lf_squarewave(f)); // --------------------------------------------------------------------------- // Oscillator frequencies (pitch modulation: 1 unit = 12 semitones) // --------------------------------------------------------------------------- pitch_ratio = semi2ratio(mod_pitch(gate) * 12.0); osc1_freq = base_freq * oct2ratio(osc1_range) * pitch_ratio; osc2_freq = base_freq * oct2ratio(osc2_range) * pitch_ratio * cent2ratio(osc2_detune + mod_osc2_detune(gate) * 50.0); osc3_freq_tracked = base_freq * oct2ratio(osc3_range) * pitch_ratio * cent2ratio(osc3_detune + mod_osc3_detune(gate) * 50.0); // When kb_track=0, osc3 becomes a free-running sub/LFO source at ~55 Hz * oct osc3_freq_untracked = 55.0 * oct2ratio(osc3_range) * cent2ratio(osc3_detune + mod_osc3_detune(gate) * 50.0); osc3_freq = osc3_kb_track * osc3_freq_tracked + (1.0 - osc3_kb_track) * osc3_freq_untracked; // --------------------------------------------------------------------------- // Oscillators // --------------------------------------------------------------------------- o1 = osc_wave(osc1_wave, osc1_freq); o2 = osc_wave(osc2_wave, osc2_freq); o3 = osc_wave(osc3_wave, osc3_freq); // Osc mix balance: -1 = all osc1, +1 = all osc3, 0 = both equal. osc2 unaffected. mix_bal = max(-1.0, min(1.0, mod_osc_mix_bal(gate))); mix_t = (mix_bal + 1.0) * 0.5; // 0..1 w_o1 = 1.0 - mix_t; w_o3 = mix_t; // --------------------------------------------------------------------------- // Noise: white (noise_type<0.5) / pink (else) // --------------------------------------------------------------------------- white_noise = no.noise; pink_noise = no.pink_noise; noise_raw = ba.if(noise_type < 0.5, white_noise, pink_noise); noise_lvl = max(0.0, min(1.0, noise_level + mod_noise_level(gate))); // --------------------------------------------------------------------------- // Mixer sum // --------------------------------------------------------------------------- mix_sum = o1 * osc1_level * w_o1 + o2 * osc2_level + o3 * osc3_level * w_o3 + noise_raw * noise_lvl; mix_driven = ma.tanh(mix_sum * mixer_drive); // --------------------------------------------------------------------------- // Filter — Moog ladder. Cutoff is modulated in octaves (±5 oct per mod unit). // --------------------------------------------------------------------------- cutoff_mod_oct = (mod_cutoff(gate) + mod_filter_env_amt(gate)) * 5.0; kb_scale = pow(freq_glided / 440.0, filter_kb); // 1.0 at A4 when kb=1 eff_cutoff_raw = cutoff * kb_scale * oct2ratio(cutoff_mod_oct); eff_cutoff = max(20.0, min(18000.0, eff_cutoff_raw)); eff_res = max(0.0, min(0.99, resonance + mod_resonance(gate))); filtered = mix_driven : ve.moog_vcf(eff_res, eff_cutoff); // --------------------------------------------------------------------------- // Amp and pan // --------------------------------------------------------------------------- // Velocity gain: blend 1.0 (no velocity) -> vel. Referenced here so Faust // keeps the _vel hidden param alive in the JSON descriptor. vel_gain = (1.0 - 0.3) + 0.3 * vel; // 30% velocity sensitivity, fixed // d08_amp modulation is additive on top of base_amp (positive only). A // fully untrained or adversarial matrix therefore cannot silence the // voice while base_amp is high — which is the safety the "main amp gate // on at all times" default relies on. Users who want envelope-gated // voices set base_amp=0 and route a positive ADSR/LFO amount. amp_val = max(0.0, min(1.0, base_amp + max(0.0, mod_amp(gate)))) * master_level * vel_gain; pan_val = max(-1.0, min(1.0, master_pan + mod_pan(gate))); // equal-power pan pan_l = cos((pan_val + 1.0) * 0.25 * ma.PI); pan_r = sin((pan_val + 1.0) * 0.25 * ma.PI); signal = filtered * amp_val; signal_L = signal * pan_l; signal_R = signal * pan_r; process = signal_L, signal_R; """ # --------------------------------------------------------------------------- # Engine 2 — Additive (64-partial spectral descriptors) # --------------------------------------------------------------------------- ADDITIVE_DESTS = [ (0, "pitch", "semitones, applied to the fundamental frequency"), (1, "bright", "high-partial group amplitude boost/cut (replaces bright env)"), (2, "tilt", "added to spectral_tilt knob"), (3, "inharmonicity", "added to inharmonicity knob"), (4, "odd_even", "added to odd/even balance knob"), (5, "formant_ctr", "shifts both formant centre frequencies (harmonic index)"), (6, "formant_depth", "added to formant_depth knob"), (7, "noise_mix", "added to noise_floor knob (clamped 0..1)"), (8, "amp", "master amplitude dest (route an ADSR here for a VCA env)"), (9, "pan", "stereo balance"), ] ADDITIVE_PARAMS = """\ // --------------------------------------------------------------------------- // Group 1 — Spectral Shape (harmonic bank + descriptors) // --------------------------------------------------------------------------- h1_amp = hslider("1_Spectral/00_h1_amp", 0.8, 0.0, 1.0, 0.001); h2_amp = hslider("1_Spectral/01_h2_amp", 0.5, 0.0, 1.0, 0.001); h3_amp = hslider("1_Spectral/02_h3_amp", 0.35, 0.0, 1.0, 0.001); h4_amp = hslider("1_Spectral/03_h4_amp", 0.25, 0.0, 1.0, 0.001); h5_amp = hslider("1_Spectral/04_h5_amp", 0.18, 0.0, 1.0, 0.001); h6_amp = hslider("1_Spectral/05_h6_amp", 0.12, 0.0, 1.0, 0.001); h7_amp = hslider("1_Spectral/06_h7_amp", 0.08, 0.0, 1.0, 0.001); h8_amp = hslider("1_Spectral/07_h8_amp", 0.06, 0.0, 1.0, 0.001); h9_16_amp = hslider("1_Spectral/08_h9_16_amp", 0.05, 0.0, 1.0, 0.001); h17_32_amp = hslider("1_Spectral/09_h17_32_amp", 0.025, 0.0, 1.0, 0.001); h33_64_amp = hslider("1_Spectral/10_h33_64_amp", 0.01, 0.0, 1.0, 0.001); spectral_tilt = hslider("1_Spectral/11_spectral_tilt", 0.0, -1.0, 1.0, 0.001); inharmonicity = hslider("1_Spectral/12_inharmonicity", 0.0, 0.0, 0.15, 0.0001); odd_even = hslider("1_Spectral/13_odd_even", 0.5, 0.0, 1.0, 0.001); // --------------------------------------------------------------------------- // Group 2 — Formants & Noise // --------------------------------------------------------------------------- formant1_freq = hslider("2_Formants/00_formant1_freq[tooltip:Formant 1 harmonic idx]", 3.0, 1.0, 16.0, 0.01); formant2_freq = hslider("2_Formants/01_formant2_freq[tooltip:Formant 2 harmonic idx]", 6.0, 1.0, 16.0, 0.01); formant_depth = hslider("2_Formants/02_formant_depth", 0.0, 0.0, 1.0, 0.001); noise_floor = hslider("2_Formants/03_noise_floor", 0.0, 0.0, 0.2, 0.001); noise_color = hslider("2_Formants/04_noise_color", 0.5, 0.0, 1.0, 0.001); sub_harmonic = hslider("2_Formants/05_sub_harmonic", 0.0, 0.0, 1.0, 0.001); // --------------------------------------------------------------------------- // Group 3 — Master // --------------------------------------------------------------------------- level = hslider("3_Master/00_level", 0.7, 0.0, 1.0, 0.001); fine_tune = hslider("3_Master/01_fine_tune[unit:ct]", 0.0, -50.0, 50.0, 0.1); saturation = hslider("3_Master/02_saturation", 0.0, 0.0, 1.0, 0.001); stereo_phase_spread= hslider("3_Master/03_stereo_phase_spread",0.1, 0.0, 1.0, 0.001); master_pan = hslider("3_Master/04_master_pan", 0.0, -1.0, 1.0, 0.001); // Static amp floor — see subtractive engine for full explanation. base_amp = hslider("3_Master/05_base_amp", 1.0, 0.0, 1.0, 0.001); """ ADDITIVE_FLOW = """\ // --------------------------------------------------------------------------- // Derived signals // --------------------------------------------------------------------------- N = 64; PI = ma.PI; cent2ratio(c) = pow(2.0, c / 1200.0); semi2ratio(s) = pow(2.0, s / 12.0); // Pitch: modulation (semitones), no glide fine_ratio = cent2ratio(fine_tune); pitch_ratio = semi2ratio(mod_pitch(gate) * 12.0); base_freq = freq * fine_ratio * pitch_ratio; // --------------------------------------------------------------------------- // Per-harmonic base amplitude from group sliders // --------------------------------------------------------------------------- group_amp(k) = ba.if(k == 1, h1_amp, ba.if(k == 2, h2_amp, ba.if(k == 3, h3_amp, ba.if(k == 4, h4_amp, ba.if(k == 5, h5_amp, ba.if(k == 6, h6_amp, ba.if(k == 7, h7_amp, ba.if(k == 8, h8_amp, ba.if(k <= 16, h9_16_amp, ba.if(k <= 32, h17_32_amp, h33_64_amp)))))))))); // Spectral tilt (modulated) — amp *= k^tilt eff_tilt = spectral_tilt + mod_tilt(gate); tilt_factor(k) = pow(float(k), eff_tilt); // Odd/even balance (modulated, clamped) eff_odd_even = max(0.0, min(1.0, odd_even + mod_odd_even(gate))); odd_weight = (1.0 - eff_odd_even) * 2.0; even_weight = eff_odd_even * 2.0; odd_even_factor(k) = ba.if(k % 2 == 0, even_weight, odd_weight); // Brightness destination: progressive boost/cut on upper partials. // bright_blend(k): 0 at k=1, 1 at k=N. mod_bright is [-1,+1]. // factor(k) = 1 + bright_blend(k) * mod_bright. At mod=0 -> unity (neutral). bright_blend(k) = float(k - 1) / float(N - 1); mb = mod_bright(gate); bright_factor(k) = 1.0 + bright_blend(k) * mb; // Inharmonicity (modulated, clamped to keep partials monotonic) eff_inharm = max(0.0, min(0.2, inharmonicity + mod_inharmonicity(gate) * 0.15)); // Formant centres (modulated together — mod_formant_ctr shifts both by same amt) eff_formant_shift = mod_formant_ctr(gate) * 8.0; // ±8 harmonics eff_f1 = max(1.0, min(32.0, formant1_freq + eff_formant_shift)); eff_f2 = max(1.0, min(32.0, formant2_freq + eff_formant_shift)); eff_formant_depth = max(0.0, min(1.0, formant_depth + mod_formant_depth(gate))); // Formant shaping — two Gaussian bumps in harmonic-index space sigma_sq = 1.5 * 1.5; formant_bump(k, ctr) = exp(-0.5 * (float(k) - ctr) * (float(k) - ctr) / sigma_sq); formant_factor(k) = 1.0 + eff_formant_depth * (formant_bump(k, eff_f1) + formant_bump(k, eff_f2)); // Combined per-harmonic amplitude harm_amp(k) = group_amp(k) * tilt_factor(k) * odd_even_factor(k) * bright_factor(k) * formant_factor(k); // Inharmonic partial frequency: freq_k = k*f0*(1 + B*(k^2 - 1)) harm_freq(k) = base_freq * float(k) * (1.0 + eff_inharm * (float(k) * float(k) - 1.0)); // Stereo spread — R channel gets a small per-harmonic pitch offset stereo_spread_freq(k) = stereo_phase_spread * float(k) * 0.01; // --------------------------------------------------------------------------- // Additive oscillator sums — L and R // --------------------------------------------------------------------------- additive_L = sum(k, N, harm_amp(k+1) * os.osc(harm_freq(k+1))); additive_R = sum(k, N, harm_amp(k+1) * os.osc(harm_freq(k+1) * (1.0 + stereo_spread_freq(k+1)))); // Sub-harmonic (0.5× fundamental) sub_osc = sub_harmonic * os.osc(base_freq * 0.5); // --------------------------------------------------------------------------- // Noise floor — coloured via one-pole LP // --------------------------------------------------------------------------- eff_noise = max(0.0, min(1.0, noise_floor + mod_noise_mix(gate))); noise_lp_cutoff = 200.0 + (1.0 - noise_color) * 19800.0; noise_signal = no.noise : fi.lowpass(1, noise_lp_cutoff); noise_out = eff_noise * noise_signal; // --------------------------------------------------------------------------- // Soft-clip saturation — tanh waveshaper // --------------------------------------------------------------------------- drive = 1.0 + saturation * 9.0; softclip(x) = ma.tanh(x * drive) / drive; // --------------------------------------------------------------------------- // Velocity gain — keeps _vel alive in the JSON descriptor // --------------------------------------------------------------------------- vel_gain = (1.0 - 0.3) + 0.3 * vel; // --------------------------------------------------------------------------- // Amplitude and pan (both mod-driven) // --------------------------------------------------------------------------- // See subtractive engine for the positive-only mod_amp rationale. amp_val = max(0.0, min(1.0, base_amp + max(0.0, mod_amp(gate)))) * level * vel_gain; pan_val = max(-1.0, min(1.0, master_pan + mod_pan(gate))); pan_l = cos((pan_val + 1.0) * 0.25 * ma.PI); pan_r = sin((pan_val + 1.0) * 0.25 * ma.PI); raw_L = (additive_L + sub_osc + noise_out) * amp_val; raw_R = (additive_R + sub_osc + noise_out) * amp_val; signal_L = softclip(raw_L) * pan_l; signal_R = softclip(raw_R) * pan_r; process = signal_L, signal_R; """ # --------------------------------------------------------------------------- # Engine 3 — FM (4-operator with cross-mod matrix) # --------------------------------------------------------------------------- FM_DESTS = [ (0, "pitch", "semitones added to the base frequency"), (1, "op1_level", "added to op1 level knob"), (2, "op2_level", "added to op2 level knob"), (3, "op3_level", "added to op3 level knob"), (4, "op4_level", "added to op4 level knob"), (5, "cross_mod_global", "global FM depth multiplier on all 12 cross-mod cells"), (6, "feedback_global", "global feedback multiplier on all 4 self-feedback cells"), (7, "global_ratio", "additive scale factor on all 4 operator ratios"), (8, "amp", "master amplitude dest (route an ADSR here for a VCA env)"), (9, "pan", "stereo balance"), ] FM_PARAMS = """\ // --------------------------------------------------------------------------- // Group 1 — Operators (ratio & level per op) // --------------------------------------------------------------------------- op1_ratio = hslider("1_Operators/00_op1_ratio", 1.0, 0.125, 16.0, 0.001); op1_level = hslider("1_Operators/01_op1_level", 0.8, 0.0, 1.0, 0.001); op2_ratio = hslider("1_Operators/02_op2_ratio", 2.0, 0.125, 16.0, 0.001); op2_level = hslider("1_Operators/03_op2_level", 0.6, 0.0, 1.0, 0.001); op3_ratio = hslider("1_Operators/04_op3_ratio", 3.0, 0.125, 16.0, 0.001); op3_level = hslider("1_Operators/05_op3_level", 0.4, 0.0, 1.0, 0.001); op4_ratio = hslider("1_Operators/06_op4_ratio", 0.5, 0.125, 16.0, 0.001); op4_level = hslider("1_Operators/07_op4_level", 0.3, 0.0, 1.0, 0.001); // --------------------------------------------------------------------------- // Group 2 — Cross-mod Matrix (mXY = op X modulates op Y) // --------------------------------------------------------------------------- m12 = hslider("2_CrossMod/00_m12", 0.0, 0.0, 10.0, 0.001); m13 = hslider("2_CrossMod/01_m13", 0.0, 0.0, 10.0, 0.001); m14 = hslider("2_CrossMod/02_m14", 0.0, 0.0, 10.0, 0.001); m21 = hslider("2_CrossMod/03_m21", 1.0, 0.0, 10.0, 0.001); m23 = hslider("2_CrossMod/04_m23", 0.0, 0.0, 10.0, 0.001); m24 = hslider("2_CrossMod/05_m24", 0.0, 0.0, 10.0, 0.001); m31 = hslider("2_CrossMod/06_m31", 0.0, 0.0, 10.0, 0.001); m32 = hslider("2_CrossMod/07_m32", 0.0, 0.0, 10.0, 0.001); m34 = hslider("2_CrossMod/08_m34", 0.0, 0.0, 10.0, 0.001); m41 = hslider("2_CrossMod/09_m41", 0.0, 0.0, 10.0, 0.001); m42 = hslider("2_CrossMod/10_m42", 0.0, 0.0, 10.0, 0.001); m43 = hslider("2_CrossMod/11_m43", 0.0, 0.0, 10.0, 0.001); // --------------------------------------------------------------------------- // Group 3 — Self-feedback // --------------------------------------------------------------------------- fb1 = hslider("3_Feedback/00_fb1", 0.0, 0.0, 1.0, 0.001); fb2 = hslider("3_Feedback/01_fb2", 0.0, 0.0, 1.0, 0.001); fb3 = hslider("3_Feedback/02_fb3", 0.0, 0.0, 1.0, 0.001); fb4 = hslider("3_Feedback/03_fb4", 0.0, 0.0, 1.0, 0.001); // --------------------------------------------------------------------------- // Group 4 — Master // --------------------------------------------------------------------------- master_level = hslider("4_Master/00_master_level", 0.7, 0.0, 1.0, 0.001); fine_tune = hslider("4_Master/01_fine_tune[unit:ct]", 0.0, -50.0, 50.0, 0.1); stereo_spread = hslider("4_Master/02_stereo_spread", 0.1, 0.0, 1.0, 0.001); output_saturation = hslider("4_Master/03_output_saturation", 0.0, 0.0, 1.0, 0.001); output_hp = hslider("4_Master/04_output_hp[unit:Hz]",20.0, 20.0, 200.0,0.1); master_pan = hslider("4_Master/05_master_pan", 0.0, -1.0, 1.0, 0.001); // Static amp floor — see subtractive engine for full explanation. base_amp = hslider("4_Master/06_base_amp", 1.0, 0.0, 1.0, 0.001); """ FM_FLOW = """\ // --------------------------------------------------------------------------- // Derived signals // --------------------------------------------------------------------------- cent2ratio(c) = pow(2.0, c / 1200.0); semi2ratio(s) = pow(2.0, s / 12.0); // Base frequency: fine tune + pitch mod (semitones, ±12) base_freq = freq * cent2ratio(fine_tune) * semi2ratio(mod_pitch(gate) * 12.0); // Velocity gain — keeps _vel alive vel_gain = (1.0 - 0.3) + 0.3 * vel; // --------------------------------------------------------------------------- // Effective per-operator ratios and levels (mod applied) // global_ratio mod is an additive ratio offset (±8) // --------------------------------------------------------------------------- ratio_offset = mod_global_ratio(gate) * 8.0; r1 = max(0.01, op1_ratio + ratio_offset); r2 = max(0.01, op2_ratio + ratio_offset); r3 = max(0.01, op3_ratio + ratio_offset); r4 = max(0.01, op4_ratio + ratio_offset); lev1 = max(0.0, min(1.0, op1_level + mod_op1_level(gate))); lev2 = max(0.0, min(1.0, op2_level + mod_op2_level(gate))); lev3 = max(0.0, min(1.0, op3_level + mod_op3_level(gate))); lev4 = max(0.0, min(1.0, op4_level + mod_op4_level(gate))); // --------------------------------------------------------------------------- // Global multipliers for cross-mod and feedback. // (1 + mod) in [0, 2] roughly; clamp to stay sane. // --------------------------------------------------------------------------- cross_mul = max(0.0, 1.0 + mod_cross_mod_global(gate)); fb_mul = max(0.0, 1.0 + mod_feedback_global(gate)); m12e = m12 * cross_mul; m13e = m13 * cross_mul; m14e = m14 * cross_mul; m21e = m21 * cross_mul; m23e = m23 * cross_mul; m24e = m24 * cross_mul; m31e = m31 * cross_mul; m32e = m32 * cross_mul; m34e = m34 * cross_mul; m41e = m41 * cross_mul; m42e = m42 * cross_mul; m43e = m43 * cross_mul; fb1e = fb1 * fb_mul; fb2e = fb2 * fb_mul; fb3e = fb3 * fb_mul; fb4e = fb4 * fb_mul; // --------------------------------------------------------------------------- // 4-operator FM bus — feedback loop via ~ on a 4-channel bus // --------------------------------------------------------------------------- fmbus(d1,d2,d3,d4) = op1_out, op2_out, op3_out, op4_out with { f1 = base_freq * r1; f2 = base_freq * r2; f3 = base_freq * r3; f4 = base_freq * r4; op1_out = lev1 * os.osc(f1 + (m21e*d2 + m31e*d3 + m41e*d4 + fb1e*d1) * f1); op2_out = lev2 * os.osc(f2 + (m12e*d1 + m32e*d3 + m42e*d4 + fb2e*d2) * f2); op3_out = lev3 * os.osc(f3 + (m13e*d1 + m23e*d2 + m43e*d4 + fb3e*d3) * f3); op4_out = lev4 * os.osc(f4 + (m14e*d1 + m24e*d2 + m34e*d3 + fb4e*d4) * f4); }; // Route 4-channel bus through feedback loop, sum all ops fm_out = (fmbus ~ (si.bus(4))) : (_, _, _, _) :> _; // --------------------------------------------------------------------------- // Post-processing // --------------------------------------------------------------------------- soft_clip(x) = x / max(0.001, 1.0 + output_saturation * abs(x)); hp_out(x) = fi.highpass(1, output_hp, x); // Master amplitude destination (route an ADSR here for VCA) // See subtractive engine for the positive-only mod_amp rationale. amp_val = max(0.0, min(1.0, base_amp + max(0.0, mod_amp(gate)))) * master_level * vel_gain * 0.25; // Pan pan_val = max(-1.0, min(1.0, master_pan + mod_pan(gate))); pan_l = cos((pan_val + 1.0) * 0.25 * ma.PI); pan_r = sin((pan_val + 1.0) * 0.25 * ma.PI); // Stereo spread via slight detuning (apply after main FM sum) spread_cents = stereo_spread * 5.0; spread_l = cent2ratio(spread_cents); spread_r = cent2ratio(0.0 - spread_cents); mono_post = fm_out * amp_val : soft_clip : hp_out; signal_L = mono_post * spread_l * pan_l; signal_R = mono_post * spread_r * pan_r; process = signal_L, signal_R; """ ENGINES = [ { "name": "subtractive", "title": "Minimoog-style subtractive voice", "description": "3 oscillators + noise → mixer → moog ladder filter → amp/pan.", "destinations": SUBTRACTIVE_DESTS, "sound_params": SUBTRACTIVE_PARAMS, "signal_flow": SUBTRACTIVE_FLOW, }, { "name": "additive", "title": "64-partial additive voice", "description": "64 harmonic sines with spectral descriptors (tilt, odd/even, formants).", "destinations": ADDITIVE_DESTS, "sound_params": ADDITIVE_PARAMS, "signal_flow": ADDITIVE_FLOW, }, { "name": "fm", "title": "4-operator FM voice", "description": "4 ops with 12-cell cross-mod matrix and per-op feedback.", "destinations": FM_DESTS, "sound_params": FM_PARAMS, "signal_flow": FM_FLOW, }, ] # --------------------------------------------------------------------------- # Shared matrix-emission helpers # --------------------------------------------------------------------------- def src_name(s): return f"s{s:02d}" def dest_label(d, name): return f"d{d:02d}_{name}" def amt_slider_line(s, d, name): label = f"MM_Matrix/{src_name(s)}_{dest_label(d, name)}" varname = f"amt_{src_name(s)}_{dest_label(d, name)}" return f'{varname} = hslider("{label}", 0.0, -1.0, 1.0, 0.001);' def dest_expression(d, name): varname = f"mod_{name}" lines = [f"{varname}(gate) ="] for s in range(N_SRC): amt_var = f"amt_{src_name(s)}_{dest_label(d, name)}" src_call = f"mp.src{s:02d}(gate)" if s < N_SRC - 1: lines.append(f" {src_call} * {amt_var} +") else: lines.append(f" {src_call} * {amt_var};") return "\n".join(lines) def gen_matrix_sliders(dests): blocks = [] for (d, name, _doc) in dests: blocks.append(f"// Matrix column: destination {d} = {name}") for s in range(N_SRC): blocks.append(amt_slider_line(s, d, name)) blocks.append("") return "\n".join(blocks) def gen_dest_expressions(dests): blocks = [] for (d, name, _doc) in dests: blocks.append(dest_expression(d, name)) blocks.append("") return "\n".join(blocks) def emit_engine(cfg, out_dir): name = cfg["name"] title = cfg["title"] description = cfg["description"] dests = cfg["destinations"] sound_params = cfg["sound_params"] signal_flow = cfg["signal_flow"] n_dest = len(dests) assert n_dest == 10, f"engine {name}: expected 10 destinations, got {n_dest}" # Header comment with per-engine destination docs dest_doc_lines = [] for (d, dname, doc) in dests: dest_doc_lines.append(f"// {d} {dname:<17} {doc}") dest_doc = "\n".join(dest_doc_lines) header = f"""\ // modular-{name}.dsp — {title} for the "Modular" mode. // // AUTO-GENERATED by gen-modular-dsp.py. DO NOT EDIT BY HAND. // Regenerate with: // cd playground/faust && python3 gen-modular-dsp.py // // Audio path: {description} // All envelopes and LFOs come from mod-pool.lib via a 48-source × {n_dest}-dest // modulation matrix. No internal envs/LFOs. // // Modulation destinations ({n_dest}): {dest_doc} // // Hidden controls (driven by noteOn/noteOff, not by the ML engine): // freq, gate, _vel // // Build: // cd playground/faust && ./build.sh // // --------------------------------------------------------------------------- import("stdfaust.lib"); mp = library("mod-pool.lib"); {HIDDEN_BLOCK} {sound_params} // --------------------------------------------------------------------------- // Modulation matrix — 48 sources × {n_dest} destinations. // Each hslider is one "amount" entry in [-1, +1]. Zero = no connection. // Group "MM_Matrix" so the paramMeta parser can recognise matrix params. // --------------------------------------------------------------------------- """ parts = [ header, gen_matrix_sliders(dests), "// ---------------------------------------------------------------------------", "// Destination signals — one per modulation destination. Each is a 48-term", "// weighted sum of sources × amounts. The `gate` argument is threaded through", "// so that mp.src??(gate) can drive ADSR envelopes.", "// ---------------------------------------------------------------------------", "", gen_dest_expressions(dests), signal_flow, ] out_path = os.path.join(out_dir, f"modular-{name}.dsp") with open(out_path, "w") as f: f.write("\n".join(parts)) n_matrix = N_SRC * n_dest print(f"Wrote {out_path}") print(f" destinations: {n_dest}") print(f" matrix sliders: {n_matrix}") def main(): out_dir = os.path.dirname(os.path.abspath(__file__)) for cfg in ENGINES: emit_engine(cfg, out_dir) if __name__ == "__main__": main()