fix(playground): correct SYNTH_SECTIONS alignment and bidirectional slider sync

SVF count was 7 (actual 9), Gap Filter section was missing (6 params),
FB Mix count was 10 (actual 9) — causing cascading mislabeling of all
synth sections from SVF onwards. Also sync volume/tempo changes from
bottom sheet back to quick-play controls.
This commit is contained in:
w1n5t0n 2026-03-22 23:59:08 +02:00
parent baa5d5016d
commit 4e32919494

View file

@ -6,7 +6,7 @@ import { FlowFieldVisualizer } from './ui/visualizer.js';
import { C15Bridge } from './synth/c15-bridge.js'; import { C15Bridge } from './synth/c15-bridge.js';
import { Arpeggiator } from './synth/arpeggiator.js'; import { Arpeggiator } from './synth/arpeggiator.js';
import { MIDIInput } from './synth/midi-input.js'; import { MIDIInput } from './synth/midi-input.js';
import { SYNTH_PARAM_MAP, SYNTH_PARAM_NAMES, SYNTH_PARAM_COLORS, applyTame, applyCurve, applyGroupOverride } from './synth/param-map.js'; import { SYNTH_PARAM_MAP, SYNTH_PARAM_NAMES, SYNTH_PARAM_COLORS, applyCurve, applyGroupOverride } from './synth/param-map.js';
import { GamepadInput } from './ui/gamepad.js'; import { GamepadInput } from './ui/gamepad.js';
// ---- Constants ---- // ---- Constants ----
@ -68,7 +68,7 @@ let arpeggiator = null;
let midiInput = null; let midiInput = null;
let outputMode = 'visual'; let outputMode = 'visual';
let tameLevel = 0; // tame level is now baked into groupOverrides defaults at init time (see ?tame URL param)
let spreadLevel = 0.6; let spreadLevel = 0.6;
let noiseLevel = 0.05; let noiseLevel = 0.05;
const rlExplorationDecay = 0.97; const rlExplorationDecay = 0.97;
@ -111,8 +111,9 @@ const SYNTH_SECTIONS = [
{ name: 'Shp A', count: 6, color: '#ff4466' }, { name: 'Shp A', count: 6, color: '#ff4466' },
{ name: 'Shp B', count: 6, color: '#ff4466' }, { name: 'Shp B', count: 6, color: '#ff4466' },
{ name: 'Comb', count: 8, color: '#44ddaa' }, { name: 'Comb', count: 8, color: '#44ddaa' },
{ name: 'SVF', count: 7, color: '#44ddaa' }, { name: 'SVF', count: 9, color: '#44ddaa' },
{ name: 'FB Mix', count: 10, color: '#ddaa44' }, { name: 'Gap', count: 6, color: '#44bbaa' },
{ name: 'FB Mix', count: 9, color: '#ddaa44' },
{ name: 'Out Mix', count: 14, color: '#ddaa44' }, { name: 'Out Mix', count: 14, color: '#ddaa44' },
{ name: 'Cabinet', count: 8, color: '#aa88dd' }, { name: 'Cabinet', count: 8, color: '#aa88dd' },
{ name: 'Flanger', count: 13, color: '#dd88aa' }, { name: 'Flanger', count: 13, color: '#dd88aa' },
@ -124,12 +125,30 @@ const SYNTH_SECTIONS = [
// ---- Group Overrides: per-group curve + per-param min/max/curve/mute ---- // ---- Group Overrides: per-group curve + per-param min/max/curve/mute ----
// groupOverrides[sectionIndex] = { curve: 0.5, params: [{ min, max, curve, muted, fixedValue }, ...] } // groupOverrides[sectionIndex] = { curve: 0.5, params: [{ min, max, curve, muted, fixedValue }, ...] }
const groupOverrides = SYNTH_SECTIONS.map(sec => ({ // Defaults are seeded from safeMin/safeMax in SYNTH_PARAM_MAP, scaled by the
// ?tame URL parameter (0 = unconstrained, 1 = full safe limits).
// localStorage overrides on load.
const groupOverrides = (() => {
const urlTame = parseFloat(new URLSearchParams(window.location.search).get('tame') ?? '1');
const t = isNaN(urlTame) ? 1 : Math.max(0, Math.min(1, urlTame));
let flatIdx = 0;
return SYNTH_SECTIONS.map(sec => ({
curve: 0.5, curve: 0.5,
params: new Array(sec.count).fill(null).map(() => ({ params: new Array(sec.count).fill(null).map(() => {
min: 0, max: 1, curve: 0.5, muted: false, fixedValue: 0.5, const p = SYNTH_PARAM_MAP[flatIdx++];
})), // Interpolate: at tame=0 use full [0,1]; at tame=1 use [safeMin, safeMax]
const safeMin = p?.safeMin ?? 0;
const safeMax = p?.safeMax ?? 1;
return {
min: safeMin * t,
max: 1 - (1 - safeMax) * t,
curve: 0.5,
muted: false,
fixedValue: 0.5,
};
}),
})); }));
})();
// Build a flat lookup: paramIndex -> { sectionIndex, localIndex } // Build a flat lookup: paramIndex -> { sectionIndex, localIndex }
const paramToSection = []; const paramToSection = [];
@ -550,7 +569,7 @@ function padPresetOutputs(outputs) {
function init() { function init() {
// Parse ?tame URL param // Parse ?tame URL param
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
tameLevel = parseFloat(urlParams.get('tame') ?? '1'); // tame is handled at groupOverrides init time, no longer needed here
spreadLevel = parseFloat(urlParams.get('spread') ?? '0.6'); spreadLevel = parseFloat(urlParams.get('spread') ?? '0.6');
if (isNaN(spreadLevel)) spreadLevel = 0.6; if (isNaN(spreadLevel)) spreadLevel = 0.6;
spreadLevel = Math.max(0, Math.min(1, spreadLevel)); spreadLevel = Math.max(0, Math.min(1, spreadLevel));
@ -906,8 +925,7 @@ function routeOutputs(outputs) {
synthVisualizer.setParams(overridden); synthVisualizer.setParams(overridden);
if (c15 && c15.running) { if (c15 && c15.running) {
for (let i = 0; i < overridden.length && i < SYNTH_PARAM_MAP.length; i++) { for (let i = 0; i < overridden.length && i < SYNTH_PARAM_MAP.length; i++) {
const tamed = applyTame(overridden[i], SYNTH_PARAM_MAP[i], tameLevel); c15.setParameter(SYNTH_PARAM_MAP[i].id, overridden[i]);
c15.setParameter(SYNTH_PARAM_MAP[i].id, tamed);
} }
} }
} else { } else {
@ -1272,6 +1290,8 @@ function wireSynthControls() {
volumeSlider.addEventListener('input', (e) => { volumeSlider.addEventListener('input', (e) => {
c15.setMasterVolume(parseFloat(e.target.value)); c15.setMasterVolume(parseFloat(e.target.value));
const quickVol = document.getElementById('quick-vol');
if (quickVol) quickVol.value = e.target.value;
}); });
arpToggle.addEventListener('click', () => { arpToggle.addEventListener('click', () => {
@ -1293,6 +1313,10 @@ function wireSynthControls() {
const val = parseInt(e.target.value); const val = parseInt(e.target.value);
arpeggiator.bpm = val; arpeggiator.bpm = val;
document.getElementById('tempo-val').textContent = val; document.getElementById('tempo-val').textContent = val;
const quickBpm = document.getElementById('quick-bpm');
const quickBpmVal = document.getElementById('quick-bpm-val');
if (quickBpm) quickBpm.value = val;
if (quickBpmVal) quickBpmVal.textContent = val;
}); });
arpOctaves.addEventListener('input', (e) => { arpOctaves.addEventListener('input', (e) => {