feat(playground): per-engine param presets for additive + FM (meml-ax4)

Additive engine: 5 presets across 3 tiers (Spectral Basics, Formant Play,
Harmonic Sculptor, Phase Explorer, Full Spectrum). FM engine: 5 presets
across 3 tiers (Simple FM, Bell Tones, Matrix Explorer, Feedback Machine,
Full Matrix).

Engine-aware preset system: applyPreset() dispatches to C15 groupOverrides
or new flat engineParamOverrides depending on active engine. Preset selector
dropdown rebuilds on engine switch. URL ?preset param searches all engines.
This commit is contained in:
w1n5t0n 2026-04-03 18:26:12 +01:00
parent 5a004c6a3e
commit 329b326ede
3 changed files with 632 additions and 43 deletions

View file

@ -15,6 +15,8 @@ import { GamepadInput } from './ui/gamepad.js';
import { HandTracker } from './ui/hand-tracker.js'; import { HandTracker } from './ui/hand-tracker.js';
import { createDevPanel } from './ui/dev-panel.js'; import { createDevPanel } from './ui/dev-panel.js';
import { SYNTH_PRESETS, PRESET_TIERS } from './synth/presets.js'; import { SYNTH_PRESETS, PRESET_TIERS } from './synth/presets.js';
import { ADDITIVE_PRESETS, ADDITIVE_PRESET_TIERS } from './synth/additive-presets.js';
import { FM_PRESETS, FM_PRESET_TIERS } from './synth/fm-presets.js';
import { EOCChain } from './eoc/index.js'; import { EOCChain } from './eoc/index.js';
import { EOCChainUI, moduleFactory } from './ui/eoc-chain-ui.js'; import { EOCChainUI, moduleFactory } from './ui/eoc-chain-ui.js';
import { EngineSwitcher } from './ui/engine-switcher.js'; import { EngineSwitcher } from './ui/engine-switcher.js';
@ -256,11 +258,69 @@ const paramToSection = [];
} }
} }
// ---- Engine-aware preset helpers ----
/**
* Return the preset array for the given engine id.
*/
function getPresetsForEngine(engineId) {
switch (engineId) {
case 'shaper-feedback': return SYNTH_PRESETS;
case 'additive': return ADDITIVE_PRESETS;
case 'fm': return FM_PRESETS;
default: return [];
}
}
/**
* Return the tier labels for the given engine id.
*/
function getPresetTiersForEngine(engineId) {
switch (engineId) {
case 'shaper-feedback': return PRESET_TIERS;
case 'additive': return ADDITIVE_PRESET_TIERS;
case 'fm': return FM_PRESET_TIERS;
default: return [];
}
}
// ---- Engine param overrides (for non-C15 engines) ----
// Flat array of per-param overrides for the current Faust engine.
// null when C15 is active (uses groupOverrides instead).
// Each entry: { min, max, curve, muted, fixedValue }
let engineParamOverrides = null;
/**
* Build a fresh engineParamOverrides array from the current engine's paramMeta.
* All params default to unmuted with [0,1] range and linear curve.
*/
function buildEngineParamOverrides() {
const meta = activeEngine?.paramMeta;
if (!meta) { engineParamOverrides = null; return; }
engineParamOverrides = meta.map(p => ({
min: 0,
max: 1,
curve: 0.5,
muted: false,
fixedValue: 0.5,
}));
}
/** /**
* Apply group overrides (per-param curve + min/max) to a single ML output value. * Apply group overrides (per-param curve + min/max) to a single ML output value.
* Returns the remapped value, or fixedValue if the param is muted. * Returns the remapped value, or fixedValue if the param is muted.
*
* For non-C15 engines, uses engineParamOverrides (flat array).
* For C15, uses the nested groupOverrides/paramToSection system.
*/ */
function applyGroupOverrides(rawValue, paramIndex) { function applyGroupOverrides(rawValue, paramIndex) {
// Non-C15 engine: use flat engine param overrides
if (engineParamOverrides && paramIndex < engineParamOverrides.length) {
const p = engineParamOverrides[paramIndex];
if (p.muted) return p.fixedValue;
return applyGroupOverride(rawValue, p.curve, p.min, p.max);
}
// C15 path: nested section/group overrides
const mapping = paramToSection[paramIndex]; const mapping = paramToSection[paramIndex];
if (!mapping) return rawValue; if (!mapping) return rawValue;
const ov = groupOverrides[mapping.si]; const ov = groupOverrides[mapping.si];
@ -273,6 +333,11 @@ function applyGroupOverrides(rawValue, paramIndex) {
/** Check if param at given index is muted */ /** Check if param at given index is muted */
function isParamMuted(paramIndex) { function isParamMuted(paramIndex) {
// Non-C15 engine: use flat engine param overrides
if (engineParamOverrides && paramIndex < engineParamOverrides.length) {
return engineParamOverrides[paramIndex].muted;
}
// C15 path
const mapping = paramToSection[paramIndex]; const mapping = paramToSection[paramIndex];
if (!mapping) return false; if (!mapping) return false;
return groupOverrides[mapping.si].params[mapping.li].muted; return groupOverrides[mapping.si].params[mapping.li].muted;
@ -282,22 +347,27 @@ function isParamMuted(paramIndex) {
/** /**
* Apply a synth preset by id. * Apply a synth preset by id.
* Sets groupOverrides (muted/active, min/max/curve/fixedValue) for all 126 params, * Engine-aware: uses groupOverrides for C15, engineParamOverrides for Faust engines.
* Sets muted/active, min/max/curve/fixedValue for all params,
* re-routes outputs, saves state, and updates the UI dropdown. * re-routes outputs, saves state, and updates the UI dropdown.
*/ */
function applyPreset(presetId) { function applyPreset(presetId) {
const preset = SYNTH_PRESETS.find(p => p.id === presetId); const engineId = activeEngine?.id ?? 'shaper-feedback';
const presets = getPresetsForEngine(engineId);
const preset = presets.find(p => p.id === presetId);
if (!preset) { if (!preset) {
console.warn(`[NISPS] Unknown preset: ${presetId}`); console.warn(`[NISPS] Unknown preset: ${presetId} (engine: ${engineId})`);
return; return;
} }
const t = tameLevel; const allActive = preset.active === null; // null = all params active
const allActive = preset.active === null; // null = all params active (Tier 4)
const activeSet = allActive ? null : new Set(preset.active); const activeSet = allActive ? null : new Set(preset.active);
const overrides = preset.overrides || {}; const overrides = preset.overrides || {};
const mutedOv = preset.mutedOverrides || {}; const mutedOv = preset.mutedOverrides || {};
if (engineId === 'shaper-feedback') {
// ---- C15 path: uses groupOverrides / paramToSection ----
const t = tameLevel;
for (let i = 0; i < N_SYNTH_OUTPUTS; i++) { for (let i = 0; i < N_SYNTH_OUTPUTS; i++) {
const param = SYNTH_PARAM_MAP[i]; const param = SYNTH_PARAM_MAP[i];
const mapping = paramToSection[i]; const mapping = paramToSection[i];
@ -305,7 +375,6 @@ function applyPreset(presetId) {
const gp = groupOverrides[mapping.si].params[mapping.li]; const gp = groupOverrides[mapping.si].params[mapping.li];
const paramName = param.name; const paramName = param.name;
// Tame-derived defaults for this param
const safeMin = param.safeMin ?? 0; const safeMin = param.safeMin ?? 0;
const safeMax = param.safeMax ?? 1; const safeMax = param.safeMax ?? 1;
const tameMin = safeMin * t; const tameMin = safeMin * t;
@ -331,7 +400,6 @@ function applyPreset(presetId) {
gp.muted = true; gp.muted = true;
const mov = mutedOv[paramName]; const mov = mutedOv[paramName];
gp.fixedValue = (mov && mov.fixedValue !== undefined) ? mov.fixedValue : param.defaultValue; gp.fixedValue = (mov && mov.fixedValue !== undefined) ? mov.fixedValue : param.defaultValue;
// Keep tame-derived ranges for manual unmuting
gp.min = tameMin; gp.min = tameMin;
gp.max = tameMax; gp.max = tameMax;
gp.curve = 0.5; gp.curve = 0.5;
@ -344,6 +412,47 @@ function applyPreset(presetId) {
const secName = SYNTH_SECTIONS[si].name; const secName = SYNTH_SECTIONS[si].name;
groupOverrides[si].curve = (gc[secName] !== undefined) ? gc[secName] : 0.5; groupOverrides[si].curve = (gc[secName] !== undefined) ? gc[secName] : 0.5;
} }
} else {
// ---- Faust engine path: uses engineParamOverrides (flat array) ----
const meta = activeEngine?.paramMeta ?? [];
// Build id -> index lookup
const idToIndex = new Map();
meta.forEach((p, i) => idToIndex.set(p.id, i));
// Ensure engineParamOverrides exists with correct length
if (!engineParamOverrides || engineParamOverrides.length !== meta.length) {
buildEngineParamOverrides();
}
for (let i = 0; i < meta.length; i++) {
const paramId = meta[i].id;
const ep = engineParamOverrides[i];
const isActive = allActive || activeSet.has(paramId);
if (isActive) {
ep.muted = false;
const ov = overrides[paramId];
if (ov) {
ep.min = ov.min !== undefined ? ov.min : 0;
ep.max = ov.max !== undefined ? ov.max : 1;
ep.curve = ov.curve !== undefined ? ov.curve : 0.5;
ep.fixedValue = ov.fixedValue !== undefined ? ov.fixedValue : 0.5;
} else {
ep.min = 0;
ep.max = 1;
ep.curve = 0.5;
ep.fixedValue = 0.5;
}
} else {
ep.muted = true;
const mov = mutedOv[paramId];
ep.fixedValue = (mov && mov.fixedValue !== undefined) ? mov.fixedValue : 0.5;
ep.min = 0;
ep.max = 1;
ep.curve = 0.5;
}
}
}
activeSynthPresetId = presetId; activeSynthPresetId = presetId;
@ -362,7 +471,7 @@ function applyPreset(presetId) {
// Save to storage // Save to storage
saveState(); saveState();
console.log(`[NISPS] Applied synth preset: ${preset.name}`); console.log(`[NISPS] Applied synth preset: ${preset.name} (engine: ${engineId})`);
} }
// ---- SynthVisualizer class ---- // ---- SynthVisualizer class ----
@ -843,6 +952,19 @@ async function setActiveEngine(engine) {
} }
} }
} }
// Build engine-specific param overrides (null for C15, flat array for Faust)
if (engine.id === 'shaper-feedback') {
engineParamOverrides = null;
} else {
buildEngineParamOverrides();
}
// Rebuild the preset selector for the new engine
rebuildPresetSelector();
// Clear active preset — it belongs to the previous engine
activeSynthPresetId = null;
} }
async function resizeMLP(newOutputCount) { async function resizeMLP(newOutputCount) {
@ -1044,9 +1166,10 @@ async function init() {
// Wire synth preset selector // Wire synth preset selector
wireSynthPresets(); wireSynthPresets();
// Check URL ?preset param (overrides localStorage) // Check URL ?preset param (overrides localStorage) — search all engines
const urlPreset = urlParams.get('preset'); const urlPreset = urlParams.get('preset');
if (urlPreset && SYNTH_PRESETS.some(p => p.id === urlPreset)) { const allPresets = [...SYNTH_PRESETS, ...ADDITIVE_PRESETS, ...FM_PRESETS];
if (urlPreset && allPresets.some(p => p.id === urlPreset)) {
applyPreset(urlPreset); applyPreset(urlPreset);
} else if (activeSynthPresetId) { } else if (activeSynthPresetId) {
// Restored from localStorage — sync the dropdown // Restored from localStorage — sync the dropdown
@ -3065,9 +3188,58 @@ function wireKeyboard() {
// ---- Quick play controls ---- // ---- Quick play controls ----
// ---- Synth Preset Selector ---- // ---- Synth Preset Selector ----
/**
* Rebuild the <select> dropdown options for the current engine's presets.
* Called on engine switch and at init time.
*/
function rebuildPresetSelector() {
const $select = document.getElementById('synth-preset-select');
if (!$select) return;
const engineId = activeEngine?.id ?? 'shaper-feedback';
const presets = getPresetsForEngine(engineId);
const tiers = getPresetTiersForEngine(engineId);
// Clear existing options
$select.innerHTML = '';
// Manual option
const manualOpt = document.createElement('option');
manualOpt.value = '';
manualOpt.textContent = 'Manual';
$select.appendChild(manualOpt);
// Group by tier
for (const tier of tiers) {
const group = document.createElement('optgroup');
group.label = tier.label;
for (const preset of presets.filter(p => p.tier === tier.tier)) {
const opt = document.createElement('option');
opt.value = preset.id;
opt.textContent = preset.name;
group.appendChild(opt);
}
if (group.children.length > 0) {
$select.appendChild(group);
}
}
// Sync current selection
if (activeSynthPresetId && presets.some(p => p.id === activeSynthPresetId)) {
$select.value = activeSynthPresetId;
} else {
$select.value = '';
}
}
function wireSynthPresets() { function wireSynthPresets() {
const $select = document.getElementById('synth-preset-select'); const $select = document.getElementById('synth-preset-select');
if (!$select) return; if (!$select) return;
// Build initial options for the active engine
rebuildPresetSelector();
$select.addEventListener('change', () => { $select.addEventListener('change', () => {
const val = $select.value; const val = $select.value;
if (val === '') { if (val === '') {

View file

@ -0,0 +1,204 @@
// Additive synth presets for NISPS playground
// Each preset defines which params are active (ML-controlled) and which are
// muted at safe defaults. Param IDs match faustParamAddress() output from
// additive.json (e.g. '1_spectral_shape_00_h1_amp').
//
// 3 tiers of progressive complexity across 5 presets.
export const ADDITIVE_PRESETS = [
// ======================================================================
// TIER 1 — Beginner (~12-15 active params)
// ======================================================================
{
id: 'additive-1',
name: '1.1',
tier: 1,
description: 'Spectral Basics — H1-H4 amplitudes, global ADSR, spectral tilt, level',
active: [
'1_spectral_shape_00_h1_amp',
'1_spectral_shape_01_h2_amp',
'1_spectral_shape_02_h3_amp',
'1_spectral_shape_03_h4_amp',
'1_spectral_shape_11_spectral_tilt',
'2_temporal_00_attack',
'2_temporal_01_decay',
'2_temporal_02_sustain',
'2_temporal_03_release',
'5_master_00_level',
'5_master_04_saturation',
'3_phase_04_stereo_phase_spread',
],
overrides: {
'1_spectral_shape_00_h1_amp': { min: 0.3, max: 1.0, curve: 0.55 },
'1_spectral_shape_01_h2_amp': { min: 0.0, max: 0.8, curve: 0.45 },
'1_spectral_shape_02_h3_amp': { min: 0.0, max: 0.7, curve: 0.4 },
'1_spectral_shape_03_h4_amp': { min: 0.0, max: 0.6, curve: 0.4 },
'1_spectral_shape_11_spectral_tilt': { min: 0.2, max: 0.8, curve: 0.5 },
'2_temporal_00_attack': { min: 0.0, max: 0.7, curve: 0.35 },
'2_temporal_01_decay': { min: 0.05, max: 0.6, curve: 0.4 },
'2_temporal_02_sustain': { min: 0.2, max: 0.9, curve: 0.55 },
'2_temporal_03_release': { min: 0.1, max: 0.7, curve: 0.45 },
'5_master_00_level': { min: 0.3, max: 0.85, curve: 0.5 },
'5_master_04_saturation': { min: 0.0, max: 0.4, curve: 0.3 },
'3_phase_04_stereo_phase_spread': { min: 0.0, max: 0.5, curve: 0.4 },
},
mutedOverrides: {},
groupCurves: {},
},
{
id: 'additive-2',
name: '1.2',
tier: 1,
description: 'Formant Play — H1-H4, formant shaping, ADSR, level',
active: [
'1_spectral_shape_00_h1_amp',
'1_spectral_shape_01_h2_amp',
'1_spectral_shape_02_h3_amp',
'1_spectral_shape_03_h4_amp',
'4_modulation_07_formant1_freq',
'4_modulation_08_formant2_freq',
'4_modulation_09_formant_depth',
'2_temporal_00_attack',
'2_temporal_01_decay',
'2_temporal_02_sustain',
'2_temporal_03_release',
'5_master_00_level',
'1_spectral_shape_11_spectral_tilt',
],
overrides: {
'1_spectral_shape_00_h1_amp': { min: 0.3, max: 1.0, curve: 0.55 },
'1_spectral_shape_01_h2_amp': { min: 0.0, max: 0.8, curve: 0.45 },
'1_spectral_shape_02_h3_amp': { min: 0.0, max: 0.7, curve: 0.4 },
'1_spectral_shape_03_h4_amp': { min: 0.0, max: 0.6, curve: 0.4 },
'4_modulation_07_formant1_freq': { min: 0.05, max: 0.7, curve: 0.45 },
'4_modulation_08_formant2_freq': { min: 0.1, max: 0.8, curve: 0.5 },
'4_modulation_09_formant_depth': { min: 0.0, max: 0.8, curve: 0.4 },
'2_temporal_00_attack': { min: 0.0, max: 0.6, curve: 0.35 },
'2_temporal_01_decay': { min: 0.05, max: 0.5, curve: 0.4 },
'2_temporal_02_sustain': { min: 0.3, max: 0.9, curve: 0.55 },
'2_temporal_03_release': { min: 0.1, max: 0.65, curve: 0.45 },
'5_master_00_level': { min: 0.3, max: 0.85, curve: 0.5 },
'1_spectral_shape_11_spectral_tilt': { min: 0.2, max: 0.8, curve: 0.5 },
},
mutedOverrides: {},
groupCurves: {},
},
// ======================================================================
// TIER 2 — Intermediate (~25-30 active params)
// ======================================================================
{
id: 'additive-3',
name: '2.1',
tier: 2,
description: 'Harmonic Sculptor — all H amps, spectral tilt, inharmonicity, ADSR, brightness envelope, vibrato, level',
active: [
// All individual harmonics + groups
'1_spectral_shape_00_h1_amp',
'1_spectral_shape_01_h2_amp',
'1_spectral_shape_02_h3_amp',
'1_spectral_shape_03_h4_amp',
'1_spectral_shape_04_h5_amp',
'1_spectral_shape_05_h6_amp',
'1_spectral_shape_06_h7_amp',
'1_spectral_shape_07_h8_amp',
'1_spectral_shape_08_h9_16_amp',
'1_spectral_shape_09_h17_32_amp',
'1_spectral_shape_10_h33_64_amp',
'1_spectral_shape_11_spectral_tilt',
'1_spectral_shape_12_inharmonicity',
// Amplitude ADSR
'2_temporal_00_attack',
'2_temporal_01_decay',
'2_temporal_02_sustain',
'2_temporal_03_release',
// Brightness ADSR
'2_temporal_04_brightness_attack',
'2_temporal_05_brightness_decay',
'2_temporal_06_brightness_sustain',
'2_temporal_07_brightness_release',
// Vibrato
'4_modulation_00_vibrato_rate',
'4_modulation_01_vibrato_depth',
'4_modulation_02_vibrato_delay',
// Master
'5_master_00_level',
'5_master_04_saturation',
],
overrides: {
'1_spectral_shape_12_inharmonicity': { min: 0.0, max: 0.5, curve: 0.3 },
'4_modulation_01_vibrato_depth': { min: 0.0, max: 0.6, curve: 0.35 },
'5_master_04_saturation': { min: 0.0, max: 0.5, curve: 0.35 },
},
mutedOverrides: {},
groupCurves: {},
},
{
id: 'additive-4',
name: '2.2',
tier: 2,
description: 'Phase Explorer — H1-H8, phase randomisation, beating, stereo spread, noise floor, ADSR, formants, level',
active: [
// H1-H8
'1_spectral_shape_00_h1_amp',
'1_spectral_shape_01_h2_amp',
'1_spectral_shape_02_h3_amp',
'1_spectral_shape_03_h4_amp',
'1_spectral_shape_04_h5_amp',
'1_spectral_shape_05_h6_amp',
'1_spectral_shape_06_h7_amp',
'1_spectral_shape_07_h8_amp',
// Phase controls
'3_phase_00_phase_random',
'3_phase_02_beating_depth',
'3_phase_03_beating_rate',
'3_phase_04_stereo_phase_spread',
'3_phase_05_noise_floor',
'3_phase_06_noise_color',
// Amplitude ADSR
'2_temporal_00_attack',
'2_temporal_01_decay',
'2_temporal_02_sustain',
'2_temporal_03_release',
// Formants
'4_modulation_07_formant1_freq',
'4_modulation_08_formant2_freq',
'4_modulation_09_formant_depth',
// Odd/even
'1_spectral_shape_13_odd_even',
// Master
'5_master_00_level',
'5_master_04_saturation',
'3_phase_07_sub_harmonic',
],
overrides: {
'3_phase_05_noise_floor': { min: 0.0, max: 0.6, curve: 0.3 },
'3_phase_02_beating_depth': { min: 0.0, max: 0.6, curve: 0.3 },
'5_master_04_saturation': { min: 0.0, max: 0.5, curve: 0.35 },
},
mutedOverrides: {},
groupCurves: {},
},
// ======================================================================
// TIER 3 — Advanced (all 48 params)
// ======================================================================
{
id: 'additive-5',
name: '3.1',
tier: 3,
description: 'Full Spectrum — all 48 params active, no restrictions',
active: null, // null = all params active
overrides: {},
mutedOverrides: {},
groupCurves: {},
},
];
export const ADDITIVE_PRESET_TIERS = [
{ tier: 1, label: 'Beginner' },
{ tier: 2, label: 'Intermediate' },
{ tier: 3, label: 'Advanced' },
];

View file

@ -0,0 +1,213 @@
// FM synth presets for NISPS playground
// Each preset defines which params are active (ML-controlled) and which are
// muted at safe defaults. Param IDs match faustParamAddress() output from
// fm-matrix.json (e.g. 'operators_op_1_op1_ratio', 'matrix_m21').
//
// 3 tiers of progressive complexity across 5 presets.
export const FM_PRESETS = [
// ======================================================================
// TIER 1 — Beginner (~12-15 active params)
// ======================================================================
{
id: 'fm-1',
name: '1.1',
tier: 1,
description: 'Simple FM — Op1/Op2 ratio+level, m21 mod index, op1 ADSR, master level',
active: [
'operators_op_1_op1_ratio',
'operators_op_1_op1_level',
'operators_op_2_op2_ratio',
'operators_op_2_op2_level',
'matrix_m21',
'operators_op_1_op1_attack',
'operators_op_1_op1_decay',
'operators_op_1_op1_sustain',
'operators_op_1_op1_release',
'master_level',
'master_stereo_spread',
'master_waveform_blend',
],
overrides: {
'operators_op_1_op1_ratio': { min: 0.15, max: 0.65, curve: 0.5 },
'operators_op_2_op2_ratio': { min: 0.15, max: 0.65, curve: 0.5 },
'matrix_m21': { min: 0.0, max: 0.5, curve: 0.4 },
'operators_op_1_op1_attack': { min: 0.0, max: 0.6, curve: 0.35 },
'operators_op_1_op1_decay': { min: 0.05, max: 0.5, curve: 0.4 },
'operators_op_1_op1_sustain': { min: 0.2, max: 0.9, curve: 0.55 },
'operators_op_1_op1_release': { min: 0.1, max: 0.6, curve: 0.45 },
'master_level': { min: 0.3, max: 0.85, curve: 0.5 },
'master_waveform_blend': { min: 0.0, max: 0.5, curve: 0.4 },
},
mutedOverrides: {},
groupCurves: {},
},
{
id: 'fm-2',
name: '1.2',
tier: 1,
description: 'Bell Tones — Op1-3 ratios+levels, m21+m31 indices, op1 ADSR, fb1, level',
active: [
'operators_op_1_op1_ratio',
'operators_op_1_op1_level',
'operators_op_2_op2_ratio',
'operators_op_2_op2_level',
'operators_op_3_op3_ratio',
'operators_op_3_op3_level',
'matrix_m21',
'matrix_m31',
'feedback_fb1',
'operators_op_1_op1_attack',
'operators_op_1_op1_decay',
'operators_op_1_op1_sustain',
'operators_op_1_op1_release',
'master_level',
],
overrides: {
'operators_op_1_op1_ratio': { min: 0.15, max: 0.55, curve: 0.5 },
'operators_op_2_op2_ratio': { min: 0.2, max: 0.7, curve: 0.55 },
'operators_op_3_op3_ratio': { min: 0.2, max: 0.7, curve: 0.55 },
'matrix_m21': { min: 0.0, max: 0.4, curve: 0.35 },
'matrix_m31': { min: 0.0, max: 0.35, curve: 0.3 },
'feedback_fb1': { min: 0.0, max: 0.4, curve: 0.3 },
'operators_op_1_op1_attack': { min: 0.0, max: 0.5, curve: 0.3 },
'operators_op_1_op1_decay': { min: 0.1, max: 0.6, curve: 0.45 },
'operators_op_1_op1_sustain': { min: 0.1, max: 0.7, curve: 0.45 },
'operators_op_1_op1_release': { min: 0.15, max: 0.7, curve: 0.5 },
'master_level': { min: 0.3, max: 0.8, curve: 0.5 },
},
mutedOverrides: {},
groupCurves: {},
},
// ======================================================================
// TIER 2 — Intermediate (~25-30 active params)
// ======================================================================
{
id: 'fm-3',
name: '2.1',
tier: 2,
description: 'Matrix Explorer — all 4 op ratios+levels, 6 cross-mod indices, all 4 ADSRs, LFO, level',
active: [
// All 4 operator ratios + levels
'operators_op_1_op1_ratio',
'operators_op_1_op1_level',
'operators_op_2_op2_ratio',
'operators_op_2_op2_level',
'operators_op_3_op3_ratio',
'operators_op_3_op3_level',
'operators_op_4_op4_ratio',
'operators_op_4_op4_level',
// 6 cross-modulation indices (half the matrix)
'matrix_m21',
'matrix_m31',
'matrix_m41',
'matrix_m32',
'matrix_m42',
'matrix_m43',
// All 4 operator ADSRs (op1 only for now — full for intermediate)
'operators_op_1_op1_attack',
'operators_op_1_op1_decay',
'operators_op_1_op1_sustain',
'operators_op_1_op1_release',
'operators_op_2_op2_attack',
'operators_op_2_op2_decay',
'operators_op_2_op2_sustain',
'operators_op_2_op2_release',
// LFO
'global_lfo_rate',
'global_lfo_depth',
'global_lfo_waveform',
// Master
'master_level',
'master_stereo_spread',
],
overrides: {
'matrix_m21': { min: 0.0, max: 0.5, curve: 0.4 },
'matrix_m31': { min: 0.0, max: 0.45, curve: 0.35 },
'matrix_m41': { min: 0.0, max: 0.4, curve: 0.35 },
'matrix_m32': { min: 0.0, max: 0.4, curve: 0.35 },
'matrix_m42': { min: 0.0, max: 0.35, curve: 0.3 },
'matrix_m43': { min: 0.0, max: 0.35, curve: 0.3 },
'global_lfo_depth': { min: 0.0, max: 0.6, curve: 0.35 },
},
mutedOverrides: {},
groupCurves: {},
},
{
id: 'fm-4',
name: '2.2',
tier: 2,
description: 'Feedback Machine — all op params, all 4 feedbacks, 4 key matrix indices, waveform blend, saturation, level',
active: [
// All 4 operator ratios + levels
'operators_op_1_op1_ratio',
'operators_op_1_op1_level',
'operators_op_2_op2_ratio',
'operators_op_2_op2_level',
'operators_op_3_op3_ratio',
'operators_op_3_op3_level',
'operators_op_4_op4_ratio',
'operators_op_4_op4_level',
// All 4 feedbacks
'feedback_fb1',
'feedback_fb2',
'feedback_fb3',
'feedback_fb4',
// 4 key cross-mod indices
'matrix_m21',
'matrix_m31',
'matrix_m12',
'matrix_m34',
// Op1 + Op2 ADSR
'operators_op_1_op1_attack',
'operators_op_1_op1_decay',
'operators_op_1_op1_sustain',
'operators_op_1_op1_release',
'operators_op_2_op2_attack',
'operators_op_2_op2_decay',
'operators_op_2_op2_sustain',
'operators_op_2_op2_release',
// Waveform, saturation, master
'master_waveform_blend',
'master_output_saturation',
'master_level',
'master_stereo_spread',
],
overrides: {
'feedback_fb1': { min: 0.0, max: 0.6, curve: 0.35 },
'feedback_fb2': { min: 0.0, max: 0.6, curve: 0.35 },
'feedback_fb3': { min: 0.0, max: 0.5, curve: 0.3 },
'feedback_fb4': { min: 0.0, max: 0.5, curve: 0.3 },
'master_output_saturation': { min: 0.0, max: 0.5, curve: 0.35 },
'matrix_m21': { min: 0.0, max: 0.5, curve: 0.4 },
'matrix_m31': { min: 0.0, max: 0.45, curve: 0.35 },
'matrix_m12': { min: 0.0, max: 0.4, curve: 0.35 },
'matrix_m34': { min: 0.0, max: 0.4, curve: 0.35 },
},
mutedOverrides: {},
groupCurves: {},
},
// ======================================================================
// TIER 3 — Advanced (all 55 params)
// ======================================================================
{
id: 'fm-5',
name: '3.1',
tier: 3,
description: 'Full Matrix — all 55 params active, no restrictions',
active: null, // null = all params active
overrides: {},
mutedOverrides: {},
groupCurves: {},
},
];
export const FM_PRESET_TIERS = [
{ tier: 1, label: 'Beginner' },
{ tier: 2, label: 'Intermediate' },
{ tier: 3, label: 'Advanced' },
];