feat(playground): engine-aware heatmap + MIDI CC storage on engine switch (meml-qaa)
- Add rebuildHeatmap(paramMeta): resets rawParamValues/_lastSentParams and rebuilds #heatmap-cells from engine paramMeta; colors from SYNTH_PARAM_COLORS for C15, _colorFromGroup() hash for other engines - Replace static SYNTH_PARAM_NAMES/COLORS lookups in buildHeatmap, setHeatmapValue, and showParamPopup with activeEngine.paramMeta[index] lookups - Scope MIDI CC localStorage key per engine via midiCCStorageKey() - Add reloadMidiCCMap() helper; called in setActiveEngine() after engine switch - loadCCMap/saveCCMap in midi-cc-map.js accept optional key parameter
This commit is contained in:
parent
80f95b8db7
commit
6f1107bae2
2 changed files with 113 additions and 24 deletions
|
|
@ -95,7 +95,11 @@ let _eocInited = false; // guard: init once per AudioContext lifetime
|
||||||
|
|
||||||
// ---- MIDI CC state ----
|
// ---- MIDI CC state ----
|
||||||
let midiOutput = null;
|
let midiOutput = null;
|
||||||
let midiCCMap = loadCCMap();
|
// Storage key scoped per engine so CC assignments don't bleed between engines.
|
||||||
|
function midiCCStorageKey() {
|
||||||
|
return `nisps-midi-cc-map:${activeEngine?.id ?? 'shaper-feedback'}`;
|
||||||
|
}
|
||||||
|
let midiCCMap = loadCCMap(midiCCStorageKey());
|
||||||
// Per-param overrides for MIDI CC mode (same shape as visual overrides)
|
// Per-param overrides for MIDI CC mode (same shape as visual overrides)
|
||||||
let midiCCOverrides = midiCCMap.map(p => ({
|
let midiCCOverrides = midiCCMap.map(p => ({
|
||||||
min: p.min, max: p.max, curve: p.curve, frozen: p.muted, fixedValue: p.fixedValue,
|
min: p.min, max: p.max, curve: p.curve, frozen: p.muted, fixedValue: p.fixedValue,
|
||||||
|
|
@ -111,6 +115,18 @@ function _generateCCColors(n) {
|
||||||
}
|
}
|
||||||
_generateCCColors(midiCCMap.length);
|
_generateCCColors(midiCCMap.length);
|
||||||
|
|
||||||
|
/** Reload the MIDI CC map from localStorage for the current engine. */
|
||||||
|
function reloadMidiCCMap() {
|
||||||
|
const saved = loadCCMap(midiCCStorageKey());
|
||||||
|
midiCCMap.length = 0;
|
||||||
|
midiCCMap.push(...saved);
|
||||||
|
midiCCOverrides.length = 0;
|
||||||
|
midiCCOverrides.push(...saved.map(p => ({
|
||||||
|
min: p.min, max: p.max, curve: p.curve, frozen: p.muted, fixedValue: p.fixedValue,
|
||||||
|
})));
|
||||||
|
_generateCCColors(midiCCMap.length);
|
||||||
|
}
|
||||||
|
|
||||||
// tame level is now baked into groupOverrides defaults at init time (see ?tame URL param)
|
// 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;
|
||||||
|
|
@ -648,7 +664,7 @@ class SynthVisualizer {
|
||||||
if (i < 0 || !this._layout) return;
|
if (i < 0 || !this._layout) return;
|
||||||
if (this._barXPositions[i] < 0) return; // muted
|
if (this._barXPositions[i] < 0) return; // muted
|
||||||
|
|
||||||
const name = SYNTH_PARAM_NAMES[i] || `p${i}`;
|
const name = (activeEngine?.paramMeta?.[i]?.name) || SYNTH_PARAM_NAMES[i] || `p${i}`;
|
||||||
const val = this.displayParams[i];
|
const val = this.displayParams[i];
|
||||||
const mapping = paramToSection[i];
|
const mapping = paramToSection[i];
|
||||||
let rangeStr = '0.00 – 1.00';
|
let rangeStr = '0.00 – 1.00';
|
||||||
|
|
@ -782,6 +798,12 @@ async function setActiveEngine(engine) {
|
||||||
EngineSwitcher.setLoading(engine.id, false);
|
EngineSwitcher.setLoading(engine.id, false);
|
||||||
await resizeMLP(engine.paramCount);
|
await resizeMLP(engine.paramCount);
|
||||||
|
|
||||||
|
// Reload MIDI CC map for the new engine (scoped storage key)
|
||||||
|
reloadMidiCCMap();
|
||||||
|
|
||||||
|
// Rebuild heatmap cells from the new engine's paramMeta
|
||||||
|
rebuildHeatmap(engine.paramMeta);
|
||||||
|
|
||||||
// Rewire EOC chain to the new engine's output node
|
// Rewire EOC chain to the new engine's output node
|
||||||
if (eocChain && _eocInited) {
|
if (eocChain && _eocInited) {
|
||||||
const outputNode = engine.getOutputNode();
|
const outputNode = engine.getOutputNode();
|
||||||
|
|
@ -892,8 +914,8 @@ async function init() {
|
||||||
|
|
||||||
$followPill = document.getElementById('follow-pill');
|
$followPill = document.getElementById('follow-pill');
|
||||||
|
|
||||||
// Build heatmap cells
|
// Build heatmap cells (engine-aware: uses activeEngine.paramMeta for synth mode)
|
||||||
buildHeatmap();
|
rebuildHeatmap(activeEngine.paramMeta);
|
||||||
|
|
||||||
// Build raw param sliders
|
// Build raw param sliders
|
||||||
buildEngineParams();
|
buildEngineParams();
|
||||||
|
|
@ -1052,15 +1074,56 @@ async function init() {
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Heatmap helpers ----
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive a stable HSL color string from a group name.
|
||||||
|
* Uses a simple djb2-style hash to map any group string to a consistent hue.
|
||||||
|
*/
|
||||||
|
function _colorFromGroup(group) {
|
||||||
|
let hash = 5381;
|
||||||
|
for (let i = 0; i < group.length; i++) {
|
||||||
|
hash = ((hash << 5) + hash) + group.charCodeAt(i);
|
||||||
|
hash |= 0; // force 32-bit int
|
||||||
|
}
|
||||||
|
const hue = ((hash >>> 0) % 360);
|
||||||
|
return `hsl(${hue}, 70%, 55%)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuild heatmap cells (and reset per-param state arrays) from a paramMeta array.
|
||||||
|
* Call this when the active engine switches to update the heatmap for the new param layout.
|
||||||
|
* @param {Array<{id:string, name:string, group:string}>} paramMeta
|
||||||
|
*/
|
||||||
|
function rebuildHeatmap(paramMeta) {
|
||||||
|
// Resize state arrays to match the new engine's param count
|
||||||
|
rawParamValues = new Array(paramMeta.length).fill(0.5);
|
||||||
|
_lastSentParams = new Float32Array(paramMeta.length);
|
||||||
|
buildHeatmap();
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Heatmap (bar chart style) ----
|
// ---- Heatmap (bar chart style) ----
|
||||||
function buildHeatmap() {
|
function buildHeatmap() {
|
||||||
$heatmapCells.innerHTML = '';
|
$heatmapCells.innerHTML = '';
|
||||||
const isSynth = outputMode === 'synth';
|
const isSynth = outputMode === 'synth';
|
||||||
const isMidiCC = outputMode === 'midi-cc';
|
const isMidiCC = outputMode === 'midi-cc';
|
||||||
const isAudioCanvas = outputMode === 'audio-canvas';
|
const isAudioCanvas = outputMode === 'audio-canvas';
|
||||||
const count = isMidiCC ? midiCCMap.length : isSynth ? N_SYNTH_OUTPUTS : isAudioCanvas ? N_AUDIO_OUTPUTS : N_VISUAL_OUTPUTS;
|
|
||||||
const names = isMidiCC ? midiCCMap.map(p => p.name) : isSynth ? SYNTH_PARAM_NAMES : isAudioCanvas ? AUDIO_PARAM_NAMES : VISUAL_PARAM_NAMES;
|
// For synth mode, derive names and colors from the active engine's paramMeta.
|
||||||
const colors = isMidiCC ? MIDI_CC_PARAM_COLORS : isSynth ? SYNTH_PARAM_COLORS : isAudioCanvas ? AUDIO_PARAM_COLORS : VISUAL_PARAM_COLORS;
|
// For all other modes, use the static arrays as before.
|
||||||
|
let count, names, colors;
|
||||||
|
if (isSynth && activeEngine?.paramMeta) {
|
||||||
|
const meta = activeEngine.paramMeta;
|
||||||
|
count = meta.length;
|
||||||
|
// C15 has curated per-param colors from SYNTH_PARAM_COLORS; other engines derive from group.
|
||||||
|
const useCuratedColors = activeEngine.id === 'shaper-feedback' && SYNTH_PARAM_COLORS.length === meta.length;
|
||||||
|
names = meta.map(p => p.name);
|
||||||
|
colors = useCuratedColors ? SYNTH_PARAM_COLORS : meta.map(p => _colorFromGroup(p.group));
|
||||||
|
} else {
|
||||||
|
count = isMidiCC ? midiCCMap.length : isAudioCanvas ? N_AUDIO_OUTPUTS : N_VISUAL_OUTPUTS;
|
||||||
|
names = isMidiCC ? midiCCMap.map(p => p.name) : isAudioCanvas ? AUDIO_PARAM_NAMES : VISUAL_PARAM_NAMES;
|
||||||
|
colors = isMidiCC ? MIDI_CC_PARAM_COLORS : isAudioCanvas ? AUDIO_PARAM_COLORS : VISUAL_PARAM_COLORS;
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const cell = document.createElement('div');
|
const cell = document.createElement('div');
|
||||||
|
|
@ -1146,8 +1209,17 @@ function setHeatmapValue(paramIndex, e, cell) {
|
||||||
const isSynth = outputMode === 'synth';
|
const isSynth = outputMode === 'synth';
|
||||||
const isMidiCC = outputMode === 'midi-cc';
|
const isMidiCC = outputMode === 'midi-cc';
|
||||||
const isAudioCanvas = outputMode === 'audio-canvas';
|
const isAudioCanvas = outputMode === 'audio-canvas';
|
||||||
const names = isMidiCC ? midiCCMap.map(p => p.name) : isSynth ? SYNTH_PARAM_NAMES : isAudioCanvas ? AUDIO_PARAM_NAMES : VISUAL_PARAM_NAMES;
|
let paramName;
|
||||||
$heatmapTooltip.textContent = `${names[paramIndex]}: ${x.toFixed(3)}`;
|
if (isSynth && activeEngine?.paramMeta?.[paramIndex]) {
|
||||||
|
paramName = activeEngine.paramMeta[paramIndex].name;
|
||||||
|
} else if (isMidiCC) {
|
||||||
|
paramName = midiCCMap[paramIndex]?.name ?? `p${paramIndex}`;
|
||||||
|
} else if (isAudioCanvas) {
|
||||||
|
paramName = AUDIO_PARAM_NAMES[paramIndex] ?? `p${paramIndex}`;
|
||||||
|
} else {
|
||||||
|
paramName = VISUAL_PARAM_NAMES[paramIndex] ?? `p${paramIndex}`;
|
||||||
|
}
|
||||||
|
$heatmapTooltip.textContent = `${paramName}: ${x.toFixed(3)}`;
|
||||||
$heatmapTooltip.classList.add('visible');
|
$heatmapTooltip.classList.add('visible');
|
||||||
$heatmapTooltip.style.left = `${rect.left}px`;
|
$heatmapTooltip.style.left = `${rect.left}px`;
|
||||||
}
|
}
|
||||||
|
|
@ -1213,10 +1285,23 @@ function showParamPopup(paramIndex, cell) {
|
||||||
const isSynth = outputMode === 'synth';
|
const isSynth = outputMode === 'synth';
|
||||||
const isMidiCC = outputMode === 'midi-cc';
|
const isMidiCC = outputMode === 'midi-cc';
|
||||||
const isAudioCanvas = outputMode === 'audio-canvas';
|
const isAudioCanvas = outputMode === 'audio-canvas';
|
||||||
const names = isMidiCC ? midiCCMap.map(p => p.name) : isSynth ? SYNTH_PARAM_NAMES : isAudioCanvas ? AUDIO_PARAM_NAMES : VISUAL_PARAM_NAMES;
|
let name, color;
|
||||||
const colors = isMidiCC ? MIDI_CC_PARAM_COLORS : isSynth ? SYNTH_PARAM_COLORS : isAudioCanvas ? AUDIO_PARAM_COLORS : VISUAL_PARAM_COLORS;
|
if (isSynth && activeEngine?.paramMeta?.[paramIndex]) {
|
||||||
const name = names[paramIndex];
|
const pm = activeEngine.paramMeta[paramIndex];
|
||||||
const color = colors[paramIndex];
|
name = pm.name;
|
||||||
|
// Use curated C15 colors when available, else derive from group.
|
||||||
|
const useCuratedColors = activeEngine.id === 'shaper-feedback' && SYNTH_PARAM_COLORS.length > paramIndex;
|
||||||
|
color = useCuratedColors ? SYNTH_PARAM_COLORS[paramIndex] : _colorFromGroup(pm.group);
|
||||||
|
} else if (isMidiCC) {
|
||||||
|
name = midiCCMap[paramIndex]?.name ?? `p${paramIndex}`;
|
||||||
|
color = MIDI_CC_PARAM_COLORS[paramIndex] ?? '#888';
|
||||||
|
} else if (isAudioCanvas) {
|
||||||
|
name = AUDIO_PARAM_NAMES[paramIndex] ?? `p${paramIndex}`;
|
||||||
|
color = AUDIO_PARAM_COLORS[paramIndex] ?? '#888';
|
||||||
|
} else {
|
||||||
|
name = VISUAL_PARAM_NAMES[paramIndex] ?? `p${paramIndex}`;
|
||||||
|
color = VISUAL_PARAM_COLORS[paramIndex] ?? '#888';
|
||||||
|
}
|
||||||
const ov = getParamOverride(paramIndex);
|
const ov = getParamOverride(paramIndex);
|
||||||
if (!ov) return;
|
if (!ov) return;
|
||||||
|
|
||||||
|
|
@ -1245,7 +1330,7 @@ function showParamPopup(paramIndex, cell) {
|
||||||
header.textContent = nameInput.value;
|
header.textContent = nameInput.value;
|
||||||
buildHeatmap();
|
buildHeatmap();
|
||||||
updateHeatmap(iml.getOutputs());
|
updateHeatmap(iml.getOutputs());
|
||||||
saveCCMap(midiCCMap);
|
saveCCMap(midiCCMap, midiCCStorageKey());
|
||||||
});
|
});
|
||||||
nameRow.appendChild(nameLabel);
|
nameRow.appendChild(nameLabel);
|
||||||
nameRow.appendChild(nameInput);
|
nameRow.appendChild(nameInput);
|
||||||
|
|
@ -1273,7 +1358,7 @@ function showParamPopup(paramIndex, cell) {
|
||||||
buildHeatmap();
|
buildHeatmap();
|
||||||
updateHeatmap(iml.getOutputs());
|
updateHeatmap(iml.getOutputs());
|
||||||
}
|
}
|
||||||
saveCCMap(midiCCMap);
|
saveCCMap(midiCCMap, midiCCStorageKey());
|
||||||
});
|
});
|
||||||
ccRow.appendChild(ccLabel);
|
ccRow.appendChild(ccLabel);
|
||||||
ccRow.appendChild(ccInput);
|
ccRow.appendChild(ccInput);
|
||||||
|
|
@ -1294,7 +1379,7 @@ function showParamPopup(paramIndex, cell) {
|
||||||
chInput.addEventListener('change', () => {
|
chInput.addEventListener('change', () => {
|
||||||
ccParam.channel = Math.max(1, Math.min(16, parseInt(chInput.value) || 1));
|
ccParam.channel = Math.max(1, Math.min(16, parseInt(chInput.value) || 1));
|
||||||
chInput.value = ccParam.channel;
|
chInput.value = ccParam.channel;
|
||||||
saveCCMap(midiCCMap);
|
saveCCMap(midiCCMap, midiCCStorageKey());
|
||||||
});
|
});
|
||||||
chRow.appendChild(chLabel);
|
chRow.appendChild(chLabel);
|
||||||
chRow.appendChild(chInput);
|
chRow.appendChild(chInput);
|
||||||
|
|
@ -2507,7 +2592,7 @@ async function applyMidiCCPreset(preset) {
|
||||||
})));
|
})));
|
||||||
|
|
||||||
_generateCCColors(midiCCMap.length);
|
_generateCCColors(midiCCMap.length);
|
||||||
saveCCMap(midiCCMap);
|
saveCCMap(midiCCMap, midiCCStorageKey());
|
||||||
|
|
||||||
// Resize MLP if in midi-cc mode
|
// Resize MLP if in midi-cc mode
|
||||||
if (outputMode === 'midi-cc') {
|
if (outputMode === 'midi-cc') {
|
||||||
|
|
@ -2579,7 +2664,7 @@ async function initMIDICCControls() {
|
||||||
midiCCMap.push(createCCParam(74, 1));
|
midiCCMap.push(createCCParam(74, 1));
|
||||||
midiCCOverrides.push({ min: 0, max: 1, curve: 0.5, frozen: false, fixedValue: 0.5 });
|
midiCCOverrides.push({ min: 0, max: 1, curve: 0.5, frozen: false, fixedValue: 0.5 });
|
||||||
_generateCCColors(midiCCMap.length);
|
_generateCCColors(midiCCMap.length);
|
||||||
saveCCMap(midiCCMap);
|
saveCCMap(midiCCMap, midiCCStorageKey());
|
||||||
updateCountDisplay();
|
updateCountDisplay();
|
||||||
|
|
||||||
// Resize MLP if we're in midi-cc mode
|
// Resize MLP if we're in midi-cc mode
|
||||||
|
|
@ -2598,7 +2683,7 @@ async function initMIDICCControls() {
|
||||||
midiCCMap.pop();
|
midiCCMap.pop();
|
||||||
midiCCOverrides.pop();
|
midiCCOverrides.pop();
|
||||||
_generateCCColors(midiCCMap.length);
|
_generateCCColors(midiCCMap.length);
|
||||||
saveCCMap(midiCCMap);
|
saveCCMap(midiCCMap, midiCCStorageKey());
|
||||||
updateCountDisplay();
|
updateCountDisplay();
|
||||||
|
|
||||||
if (outputMode === 'midi-cc') {
|
if (outputMode === 'midi-cc') {
|
||||||
|
|
@ -2641,7 +2726,7 @@ async function initMIDICCControls() {
|
||||||
frozen: p.muted ?? false, fixedValue: p.fixedValue ?? 0.5,
|
frozen: p.muted ?? false, fixedValue: p.fixedValue ?? 0.5,
|
||||||
})));
|
})));
|
||||||
_generateCCColors(midiCCMap.length);
|
_generateCCColors(midiCCMap.length);
|
||||||
saveCCMap(midiCCMap);
|
saveCCMap(midiCCMap, midiCCStorageKey());
|
||||||
updateCountDisplay();
|
updateCountDisplay();
|
||||||
|
|
||||||
if (outputMode === 'midi-cc') {
|
if (outputMode === 'midi-cc') {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
// Each param maps one MLP output to a MIDI CC message.
|
// Each param maps one MLP output to a MIDI CC message.
|
||||||
|
|
||||||
const STORAGE_KEY = 'nisps-midi-cc-map';
|
const STORAGE_KEY = 'nisps-midi-cc-map';
|
||||||
|
// Scoped key used when an engine ID is provided (avoids collision between engines).
|
||||||
|
// Falls back to STORAGE_KEY for legacy/unknown callers.
|
||||||
|
|
||||||
// Default starter set — common CC numbers
|
// Default starter set — common CC numbers
|
||||||
const DEFAULT_CC_MAP = [
|
const DEFAULT_CC_MAP = [
|
||||||
|
|
@ -46,11 +48,12 @@ export function createCCParam(cc = 74, channel = 1) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load CC map from localStorage, or return default.
|
* Load CC map from localStorage, or return default.
|
||||||
|
* @param {string} [key] — optional storage key (default: STORAGE_KEY)
|
||||||
* @returns {Array} CC param definitions
|
* @returns {Array} CC param definitions
|
||||||
*/
|
*/
|
||||||
export function loadCCMap() {
|
export function loadCCMap(key = STORAGE_KEY) {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(STORAGE_KEY);
|
const raw = localStorage.getItem(key);
|
||||||
if (raw) {
|
if (raw) {
|
||||||
const saved = JSON.parse(raw);
|
const saved = JSON.parse(raw);
|
||||||
if (Array.isArray(saved) && saved.length > 0) return saved;
|
if (Array.isArray(saved) && saved.length > 0) return saved;
|
||||||
|
|
@ -64,10 +67,11 @@ export function loadCCMap() {
|
||||||
/**
|
/**
|
||||||
* Save CC map to localStorage.
|
* Save CC map to localStorage.
|
||||||
* @param {Array} ccMap
|
* @param {Array} ccMap
|
||||||
|
* @param {string} [key] — optional storage key (default: STORAGE_KEY)
|
||||||
*/
|
*/
|
||||||
export function saveCCMap(ccMap) {
|
export function saveCCMap(ccMap, key = STORAGE_KEY) {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(ccMap));
|
localStorage.setItem(key, JSON.stringify(ccMap));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[MIDI CC] Failed to save map:', e);
|
console.warn('[MIDI CC] Failed to save map:', e);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue