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.
This commit is contained in:
L'électron rare
2026-06-29 15:22:23 +02:00
parent 89ade5524c
commit d3a2a591ae
2 changed files with 128 additions and 67 deletions
+118 -65
View File
@@ -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;
+10 -2
View File
@@ -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; }
`,