feat: web matrix arranger tab
CI build oscope-of / build-check (push) Has been cancelled

This commit is contained in:
L'électron rare
2026-06-28 16:55:59 +02:00
parent ee7f349359
commit 2d927107f1
4 changed files with 176 additions and 1 deletions
+23
View File
@@ -91,3 +91,26 @@ button.queued { animation: queued-blink 600ms ease-in-out infinite alternate; }
.scene-nav { display: flex; gap: 8px; margin: 8px 0; }
.scene-nav button { flex: 1; font-size: 16px; padding: 16px; }
/* --- Matrix arranger (16x32) --- */
.matrix-transport { display: flex; gap: 8px; margin: 10px 0; }
.matrix-transport button { flex: 1; padding: 14px 10px; font-size: 14px; }
.matrix-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.mrow { display: flex; align-items: center; }
.mvoice-label { width: 54px; font-size: 10px; color: #888; text-align: right;
padding-right: 6px; flex-shrink: 0; white-space: nowrap; overflow: hidden;
text-overflow: ellipsis; }
.mbar-num { width: 22px; font-size: 9px; color: #555; text-align: center;
flex-shrink: 0; height: 14px; line-height: 14px; }
.mcell { width: 22px; height: 22px; min-width: 22px; border: 1px solid;
border-radius: 2px; cursor: pointer; padding: 0; flex-shrink: 0; }
.mcell:active { opacity: 0.65; }
.mcell.m0 { background: #1a1a1a; border-color: #2a2a2a; }
.mcell.m1 { background: #c22; border-color: #e44; }
.mcell.m2 { background: #1a8; border-color: #2da; }
.mcell.m3 { background: #25b; border-color: #46e; }
.mcell.m4 { background: #990; border-color: #cc0; }
.mcell.m5 { background: #c60; border-color: #e80; }
.mcell.m6 { background: #82a; border-color: #a4d; }
.mcell.playing { box-shadow: 0 0 0 2px rgba(255,255,255,0.28),
inset 0 0 0 1px rgba(255,255,255,0.40); }
+139
View File
@@ -127,8 +127,132 @@ ws.addEventListener("message", (ev) => {
});
return;
}
// /matrix/playhead <bar> — advance playhead column highlight
if (address === "/matrix/playhead") {
const bar = Math.round(Number(args[0]));
if (!Number.isFinite(bar) || bar < 0 || bar >= MATRIX_BARS) return;
if (matPlayhead >= 0) {
for (let vi = 0; vi < MATRIX_VOICES.length; vi++) {
const el = cellRefs[matPlayhead][vi];
if (el) el.classList.remove("playing");
}
}
matPlayhead = bar;
for (let vi = 0; vi < MATRIX_VOICES.length; vi++) {
const el = cellRefs[matPlayhead][vi];
if (el) el.classList.add("playing");
}
return;
}
// /matrix/cell <vi> <bar> <color> — cross-surface cell sync
if (address === "/matrix/cell") {
const vi = Math.round(Number(args[0]));
const bar = Math.round(Number(args[1]));
const color = Math.round(Number(args[2]));
if (vi < 0 || vi >= 16 || bar < 0 || bar >= MATRIX_BARS || color < 0 || color > 6) return;
matGrid[vi][bar] = color;
saveMatState();
const el = cellRefs[bar][vi];
if (el) {
applyMatCellColor(el, color);
if (bar === matPlayhead) el.classList.add("playing");
}
return;
}
});
// --- Matrix (16-voice x 32-bar song arranger) ---
const MATRIX_STORAGE_KEY = "avlive.matrix";
const MATRIX_VOICES = [
"kick","hats","clap","perc","sub","acid","arp","lead",
"stab","pad","ride","rim","tom","reese","bells","sweep"
];
const MATRIX_BARS = 32;
let matGrid = Array.from({ length: 16 }, () => new Array(32).fill(0));
// cellRefs[bar][vi] for O(16) playhead column toggle
const cellRefs = Array.from({ length: 32 }, () => new Array(16).fill(null));
let matPlayhead = -1;
function loadMatState() {
try {
const raw = localStorage.getItem(MATRIX_STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw);
if (
Array.isArray(parsed) && parsed.length === 16 &&
parsed.every(row => Array.isArray(row) && row.length === 32)
) {
matGrid = parsed;
return;
}
}
} catch (_e) {}
matGrid = Array.from({ length: 16 }, () => new Array(32).fill(0));
}
function saveMatState() {
try { localStorage.setItem(MATRIX_STORAGE_KEY, JSON.stringify(matGrid)); } catch (_e) {}
}
function applyMatCellColor(el, color) {
for (let c = 0; c <= 6; c++) el.classList.remove("m" + c);
el.classList.add("m" + color);
el.dataset.state = color;
}
function renderMatrix() {
const container = document.getElementById("matrix-grid");
if (!container) return;
container.innerHTML = "";
// Header row: bar numbers every 4 bars
const headerRow = document.createElement("div");
headerRow.className = "mrow";
const corner = document.createElement("span");
corner.className = "mvoice-label";
headerRow.appendChild(corner);
for (let bar = 0; bar < MATRIX_BARS; bar++) {
const num = document.createElement("span");
num.className = "mbar-num";
num.textContent = (bar % 4 === 0) ? String(bar + 1) : "";
headerRow.appendChild(num);
}
container.appendChild(headerRow);
// Voice rows
for (let vi = 0; vi < MATRIX_VOICES.length; vi++) {
const row = document.createElement("div");
row.className = "mrow";
const label = document.createElement("span");
label.className = "mvoice-label";
label.textContent = MATRIX_VOICES[vi];
row.appendChild(label);
for (let bar = 0; bar < MATRIX_BARS; bar++) {
const color = matGrid[vi][bar];
const cell = document.createElement("button");
cell.className = "mcell m" + color;
cell.dataset.vi = vi;
cell.dataset.bar = bar;
cell.dataset.state = color;
if (bar === matPlayhead) cell.classList.add("playing");
cell.addEventListener("click", () => {
const cur = +cell.dataset.state;
const next = (cur + 1) % 7;
matGrid[vi][bar] = next;
saveMatState();
applyMatCellColor(cell, next);
if (bar === matPlayhead) cell.classList.add("playing");
send("/matrix/cell", vi, bar, next);
});
cellRefs[bar][vi] = cell;
row.appendChild(cell);
}
container.appendChild(row);
}
}
// --- Pattern state (defaults mirror launchpad.scd exactly) ---
const STORAGE_KEY = "avlive.seq";
@@ -276,6 +400,7 @@ function updateNameInputs() {
document.addEventListener("DOMContentLoaded", () => {
loadState();
loadMatState();
// Tab switching
document.querySelectorAll(".tab-btn").forEach((btn) => {
@@ -296,6 +421,7 @@ document.addEventListener("DOMContentLoaded", () => {
updateNameInputs();
renderRhyGrid();
renderMelSteps();
renderMatrix();
// Name input: rename selected preset and update button labels
const melName = document.getElementById("mel-name");
@@ -398,4 +524,17 @@ document.addEventListener("DOMContentLoaded", () => {
if (scenePrev) scenePrev.addEventListener("click", () => send("/scene/prev"));
const sceneNext = document.getElementById("scene-next");
if (sceneNext) sceneNext.addEventListener("click", () => send("/scene/next"));
// Matrix transport
const matPlay = document.getElementById("matrix-play");
if (matPlay) matPlay.addEventListener("click", () => send("/matrix/play"));
const matStop = document.getElementById("matrix-stop");
if (matStop) matStop.addEventListener("click", () => send("/matrix/stop"));
const matClear = document.getElementById("matrix-clear");
if (matClear) matClear.addEventListener("click", () => {
matGrid = Array.from({ length: 16 }, () => new Array(32).fill(0));
saveMatState();
renderMatrix();
send("/matrix/clear");
});
});
+13
View File
@@ -13,6 +13,7 @@
<button class="tab-btn" data-tab="fx">FX / HARMONIE / CONCERT</button>
<button class="tab-btn" data-tab="seq">SÉQUENCEURS</button>
<button class="tab-btn" data-tab="sections">SECTIONS</button>
<button class="tab-btn" data-tab="matrix">MATRICE</button>
</nav>
<div class="tab-panel active" id="tab-live">
@@ -150,4 +151,16 @@
</div>
</section>
</div>
<div class="tab-panel" id="tab-matrix">
<section><h2>Matrice</h2>
<div class="matrix-transport">
<button id="matrix-play">PLAY</button>
<button id="matrix-stop">STOP</button>
<button id="matrix-clear">CLEAR</button>
</div>
<div class="matrix-scroll">
<div id="matrix-grid"></div>
</div>
</section>
</div>
<script src="control.js"></script></body></html>
+1 -1
View File
@@ -114,7 +114,7 @@ feedbackPort.on("ready", () => {
console.log(`[feedback] ecoute :${FEEDBACK_PORT_IN} <- SC engine`);
});
const FEEDBACK_PREFIXES = ["/armed/", "/sync/", "/seq/", "/scene/"];
const FEEDBACK_PREFIXES = ["/armed/", "/sync/", "/seq/", "/scene/", "/matrix/"];
feedbackPort.on("message", (oscMsg) => {
if (!FEEDBACK_PREFIXES.some((p) => oscMsg.address.startsWith(p))) return;
broadcast({ address: oscMsg.address, args: oscMsg.args });