From d3a2a591ae143d039d8090ed2ba0ef341e9f0e4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:22:23 +0200 Subject: [PATCH] feat(web): per-voice MOD list + pose dropdown Replace the old per-colour single-mod row in the MOD/POSE tab with a per-voice additive list (matVoiceMods) and a curated pose-event dropdown (matVoicePoses), both independent of cdColor. Add OSC handlers for /matrix/voicemod and /matrix/voicepose echoes. Update openFor to request voicemod/get + voicepose/get on expand. Extend colordefs-parse test stub to export all new matrix-state symbols. --- web_realart/public/control/js/voice-editor.js | 183 +++++++++++------- web_realart/test/colordefs-parse.test.mjs | 12 +- 2 files changed, 128 insertions(+), 67 deletions(-) diff --git a/web_realart/public/control/js/voice-editor.js b/web_realart/public/control/js/voice-editor.js index f245e2d..35ac636 100644 --- a/web_realart/public/control/js/voice-editor.js +++ b/web_realart/public/control/js/voice-editor.js @@ -5,6 +5,9 @@ import { MATRIX_MOD_SOURCES, MATRIX_MOD_TARGETS, matColorDefs, saveMatState, VOICE_CLASS, defaultStepMode, isMelodic, + matVoiceMods, matVoicePoses, + MOD_SOURCE_LABELS, MOD_TARGET_LABELS, POSE_EVENT_CHOICES, + parseVoiceMod, parseVoicePose, } from "./matrix-state.js"; // Per-voice step edit mode; initialised from defaultStepMode() on first expand. @@ -97,7 +100,8 @@ export function openFor(vi, containerEl) { send("/matrix/colordefs/get", vi); send("/matrix/steps/get", vi); - send("/matrix/colordefs/get/ext", vi); + send("/matrix/voicemod/get", vi); + send("/matrix/voicepose/get", vi); } export function toggle(vi, containerEl) { @@ -324,75 +328,108 @@ function renderSynthFxTab(vi) { cdTabBodyEl.appendChild(varRow); } -// Renders mod row + pose section into cdTabBodyEl. +// Per-voice MOD (additive free list) + POSE (curated events). Independent of cdColor. +function sendVoiceMod(vi) { + const list = matVoiceMods[vi] || []; + send("/matrix/voicemod", vi, list.length, + ...list.flatMap((b) => [b.source, b.target, +b.depth])); + saveMatState(); +} +function sendVoicePose(vi) { + const list = matVoicePoses[vi] || []; + send("/matrix/voicepose", vi, list.length, + ...list.flatMap((b) => [b.poseId, b.action])); + saveMatState(); +} + +// helpers +function firstTarget(targets) { return targets.find((t) => t !== "none") || "amp"; } +function MOD_SOURCE_ORDER_FIRST() { return "bodyVitesse"; } + function renderModPoseTab(vi) { if (!cdTabBodyEl) return; - const c = cdColor; - const d = matColorDefs[vi][c]; - const targets = MATRIX_MOD_TARGETS[MATRIX_VOICES[vi]] || ["none","amp"]; + const voice = MATRIX_VOICES[vi]; + const targets = MATRIX_MOD_TARGETS[voice] || ["none", "amp"]; - // Mod row - const modDiv = document.createElement("div"); - modDiv.className = "cd-row m" + c; - const src = document.createElement("select"); - MATRIX_MOD_SOURCES.forEach(s=>{ const o=document.createElement("option"); o.value=s; o.textContent=s; src.appendChild(o); }); - const tgt = document.createElement("select"); - targets.forEach(t=>{ const o=document.createElement("option"); o.value=t; o.textContent=t; tgt.appendChild(o); }); - const dep = document.createElement("input"); - dep.type="range"; dep.min=0; dep.max=1; dep.step=0.01; - const mod = d.mod || {source:"none", target:"none", depth:0.5}; - src.value = mod.source; tgt.value = mod.target; dep.value = mod.depth; - const sendMod = () => { - matColorDefs[vi][c].mod = {source:src.value, target:tgt.value, depth:+dep.value}; - saveMatState(); - send("/matrix/colormod", vi, c, src.value, tgt.value, +dep.value); - }; - src.addEventListener("change", sendMod); - tgt.addEventListener("change", sendMod); - dep.addEventListener("change", sendMod); - modDiv.appendChild(mk("src", src)); - modDiv.appendChild(mk("tgt", tgt)); - modDiv.appendChild(mk("depth", dep)); - cdTabBodyEl.appendChild(modDiv); - - // Pose section - const poseDiv = document.createElement("div"); - const addDiv = document.createElement("div"); - const poseId = document.createElement("input"); - poseId.placeholder = "poseId"; - poseId.className = "pose-id"; - const poseAct = document.createElement("select"); - ["trigger","gate"].forEach(a=>{ const o=document.createElement("option"); o.value=a; o.textContent=a; poseAct.appendChild(o); }); - const poseGo = document.createElement("button"); - poseGo.textContent = "+"; - poseGo.addEventListener("click", () => { - if (!poseId.value) return; - const poses = (d.pose || []).filter(b => b.poseId !== poseId.value); - poses.push({poseId: poseId.value, action: poseAct.value}); - matColorDefs[vi][c].pose = poses; - saveMatState(); - send("/matrix/colorpose", vi, c, poses.length, ...poses.flatMap(b => [b.poseId, b.action])); - renderPattern(vi); - }); - [poseId, poseAct, poseGo].forEach(e => addDiv.appendChild(e)); - poseDiv.appendChild(addDiv); - (d.pose || []).forEach(b => { - const line = document.createElement("div"); - line.textContent = `${b.poseId} -> ${b.action}`; + // --- MOD: additive list --- + const modWrap = document.createElement("div"); + modWrap.className = "cd-modlist"; + (matVoiceMods[vi] || []).forEach((b, i) => { + const row = document.createElement("div"); + row.className = "cd-row"; + const src = document.createElement("select"); + MATRIX_MOD_SOURCES.forEach((s) => { const o = document.createElement("option"); + o.value = s; o.textContent = MOD_SOURCE_LABELS[s] || s; src.appendChild(o); }); + src.value = b.source; + const tgt = document.createElement("select"); + targets.forEach((t) => { const o = document.createElement("option"); + o.value = t; o.textContent = MOD_TARGET_LABELS[t] || t; tgt.appendChild(o); }); + tgt.value = b.target; + const dep = document.createElement("input"); + dep.type = "range"; dep.min = 0; dep.max = 1; dep.step = 0.01; dep.value = b.depth; + const meter = document.createElement("span"); + meter.className = "cd-meter"; meter.dataset.src = b.source; const del = document.createElement("button"); - del.textContent = "x"; - del.style.cssText = "width:auto;padding:2px 6px;font-size:10px;"; - del.addEventListener("click", () => { - matColorDefs[vi][c].pose = (d.pose || []).filter(pp => pp.poseId !== b.poseId); - const poses = matColorDefs[vi][c].pose; - saveMatState(); - send("/matrix/colorpose", vi, c, poses.length, ...poses.flatMap(pp => [pp.poseId, pp.action])); - renderPattern(vi); - }); - line.appendChild(del); - poseDiv.appendChild(line); + del.textContent = "x"; del.style.cssText = "width:auto;padding:2px 6px;font-size:10px;"; + const apply = () => { b.source = src.value; b.target = tgt.value; b.depth = +dep.value; + meter.dataset.src = b.source; sendVoiceMod(vi); }; + src.addEventListener("change", apply); + tgt.addEventListener("change", apply); + dep.addEventListener("change", apply); + del.addEventListener("click", () => { matVoiceMods[vi].splice(i, 1); sendVoiceMod(vi); renderPattern(vi); }); + [mk("src", src), mk("cible", tgt), mk("depth", dep), meter, del].forEach((e) => row.appendChild(e)); + modWrap.appendChild(row); }); - cdTabBodyEl.appendChild(poseDiv); + const addMod = document.createElement("button"); + addMod.textContent = "+ modulation"; + addMod.addEventListener("click", () => { + (matVoiceMods[vi] ||= []).push({ source: MOD_SOURCE_ORDER_FIRST(), target: firstTarget(targets), depth: 0.5 }); + sendVoiceMod(vi); renderPattern(vi); + }); + modWrap.appendChild(addMod); + cdTabBodyEl.appendChild(modWrap); + + // --- POSE: curated events --- + const poseWrap = document.createElement("div"); + poseWrap.className = "cd-poselist"; + (matVoicePoses[vi] || []).forEach((b, i) => { + const row = document.createElement("div"); + row.className = "cd-row"; + const ev = document.createElement("select"); + POSE_EVENT_CHOICES.forEach((c) => { const o = document.createElement("option"); + o.value = c.id; o.textContent = c.label; ev.appendChild(o); }); + const custom = document.createElement("input"); + custom.placeholder = "poseId"; custom.className = "pose-id"; + const known = POSE_EVENT_CHOICES.some((c) => c.id === b.poseId); + ev.value = known ? b.poseId : "__custom__"; + custom.value = known ? "" : b.poseId; + custom.hidden = ev.value !== "__custom__"; + const act = document.createElement("select"); + ["trigger", "gate"].forEach((a) => { const o = document.createElement("option"); + o.value = a; o.textContent = a; act.appendChild(o); }); + act.value = b.action; + const del = document.createElement("button"); + del.textContent = "x"; del.style.cssText = "width:auto;padding:2px 6px;font-size:10px;"; + const apply = () => { + custom.hidden = ev.value !== "__custom__"; + b.poseId = ev.value === "__custom__" ? (custom.value || "none") : ev.value; + b.action = act.value; sendVoicePose(vi); + }; + ev.addEventListener("change", apply); + custom.addEventListener("change", apply); + act.addEventListener("change", apply); + del.addEventListener("click", () => { matVoicePoses[vi].splice(i, 1); sendVoicePose(vi); renderPattern(vi); }); + [mk("event", ev), custom, mk("action", act), del].forEach((e) => row.appendChild(e)); + poseWrap.appendChild(row); + }); + const addPose = document.createElement("button"); + addPose.textContent = "+ pose"; + addPose.addEventListener("click", () => { + (matVoicePoses[vi] ||= []).push({ poseId: "danse", action: "trigger" }); + sendVoicePose(vi); renderPattern(vi); + }); + poseWrap.appendChild(addPose); + cdTabBodyEl.appendChild(poseWrap); } // Renders the tab bar into cdPatternEl. @@ -513,6 +550,22 @@ on("/matrix/colorpose", (args) => { } }); +on("/matrix/voicemod", (args) => { + const { vi, list } = parseVoiceMod(args); + if (vi >= 0 && vi < MATRIX_VOICES.length) { + matVoiceMods[vi] = list; saveMatState(); + if (isOpenFor(vi)) renderPattern(vi); + } +}); + +on("/matrix/voicepose", (args) => { + const { vi, list } = parseVoicePose(args); + if (vi >= 0 && vi < MATRIX_VOICES.length) { + matVoicePoses[vi] = list; saveMatState(); + if (isOpenFor(vi)) renderPattern(vi); + } +}); + on("/matrix/stephead", (args) => { // Guard: only act when a voice is open and the STEPS tab is active. if (expandedVoice < 0 || !cdTabBodyEl || activeTab !== "steps") return; diff --git a/web_realart/test/colordefs-parse.test.mjs b/web_realart/test/colordefs-parse.test.mjs index cd04beb..c1238d0 100644 --- a/web_realart/test/colordefs-parse.test.mjs +++ b/web_realart/test/colordefs-parse.test.mjs @@ -23,10 +23,18 @@ export const MATRIX_VOICES=Array.from({length:22},(_,i)=>"v"+i); export const MATRIX_BARS=64; export const MATRIX_INST_CHOICES=[]; export const MATRIX_MOD_SOURCES=[]; -export const MATRIX_MOD_TARGETS=[]; +export const MATRIX_MOD_TARGETS={}; +export const MOD_SOURCE_ORDER=[]; +export const MOD_SOURCE_LABELS={}; +export const MOD_TARGET_LABELS={}; +export const POSE_EVENT_CHOICES=[]; export const matColorDefs=Array.from({length:22},()=>Array.from({length:7},()=>({}))); +export let matVoiceMods=Array.from({length:22},()=>[]); +export let matVoicePoses=Array.from({length:22},()=>[]); +export function parseVoiceMod(args){ return {vi:0,list:[]}; } +export function parseVoicePose(args){ return {vi:0,list:[]}; } export function saveMatState(){} -export const VOICE_CLASS=()=>"perc"; +export const VOICE_CLASS={}; export function defaultStepMode(){ return "drum"; } export function isMelodic(){ return false; } `,