This commit is contained in:
@@ -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); }
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user