merge: matrix loop region + timeline UI
Bring the editable loop region/seek (matrix.scd) and the web timeline
header (origin/main f9da19f) into the feature branch. Disjoint from the
local commits, clean auto-merge.
This commit is contained in:
@@ -23,6 +23,8 @@
|
||||
~matBase = ~matBase ? IdentityDictionary.new;
|
||||
~matBeatsPerBar = ~matBeatsPerBar ? 4;
|
||||
~matLastColor = ~matLastColor ? Array.fill(~matVoices.size, -1);
|
||||
~lp[\matLoopStart] = ~lp[\matLoopStart] ? 0;
|
||||
~lp[\matLoopEnd] = ~lp[\matLoopEnd] ? (~matBars - 1);
|
||||
|
||||
// -- Persistence env init (nil-guarded) --
|
||||
~matDir = ~matDir ? "~/.config/av-live/matrices".standardizePath;
|
||||
@@ -81,6 +83,12 @@ File.exists(~matDir).not.if({ ("mkdir -p " ++ ~matDir.quote).systemCmd });
|
||||
})
|
||||
};
|
||||
|
||||
// -- ~matNextBar : compute next playhead bar clipped within the loop region --
|
||||
// Wraps nb back to loop start when nb overshoots le or undershoots ls.
|
||||
~matNextBar = { var nb = ~lp[\matBar] + 1; var ls = ~lp[\matLoopStart] ? 0; var le = ~lp[\matLoopEnd] ? (~matBars - 1);
|
||||
((nb > le) or: { nb < ls }).if({ nb = ls });
|
||||
nb.clip(0, ~matBars - 1) };
|
||||
|
||||
// -- ~matApplyBar : set each voice's Pdef to its cell color and (re)play or stop --
|
||||
// Re-sourcing a playing Pdef updates it at the next quant boundary (smooth for v1).
|
||||
~matApplyBar = { |bar|
|
||||
@@ -122,6 +130,19 @@ File.exists(~matDir).not.if({ ("mkdir -p " ++ ~matDir.quote).systemCmd });
|
||||
~toscSend !? { ~toscSend.("/matrix/trig", vi, amp.clip(0, 1)) }
|
||||
};
|
||||
|
||||
// -- ~matLoopPush : send current loop region to surfaces --
|
||||
~matLoopPush = { ~toscSend !? { ~toscSend.("/matrix/loop", ~lp[\matLoopStart] ? 0, ~lp[\matLoopEnd] ? (~matBars - 1)) } };
|
||||
|
||||
// -- ~matSetLoop : set loop region (auto-ordered) and push to surfaces --
|
||||
~matSetLoop = { |s, e| var a = s.asInteger.clip(0, ~matBars - 1); var b = e.asInteger.clip(0, ~matBars - 1);
|
||||
(a <= b).if({ ~lp[\matLoopStart] = a; ~lp[\matLoopEnd] = b }, { ~lp[\matLoopStart] = b; ~lp[\matLoopEnd] = a });
|
||||
~matLoopPush.() };
|
||||
|
||||
// -- ~matSeek : jump playhead to bar; re-applies if playing --
|
||||
~matSeek = { |bar| ~lp[\matBar] = bar.asInteger.clip(0, ~matBars - 1);
|
||||
~toscSend !? { ~toscSend.("/matrix/playhead", ~lp[\matBar]) };
|
||||
~lp[\matPlaying].if({ ~matApplyBar.(~lp[\matBar]) }) };
|
||||
|
||||
// -- ~matStop : halt the playhead Routine and silence all matrix voices --
|
||||
~matStop = {
|
||||
~matRoutine !? { ~matRoutine.stop };
|
||||
@@ -139,13 +160,13 @@ File.exists(~matDir).not.if({ ("mkdir -p " ++ ~matDir.quote).systemCmd });
|
||||
var clock = ~lp[\clock] ? TempoClock.default;
|
||||
~matRoutine !? { ~matRoutine.stop };
|
||||
~lp[\matPlaying] = true;
|
||||
~lp[\matBar] = 0;
|
||||
~lp[\matBar] = ~lp[\matLoopStart] ? 0;
|
||||
~matRoutine = Routine({
|
||||
loop {
|
||||
~matApplyBar.(~lp[\matBar]);
|
||||
~toscSend !? { ~toscSend.("/matrix/playhead", ~lp[\matBar]) };
|
||||
~matBeatsPerBar.wait;
|
||||
~lp[\matBar] = (~lp[\matBar] + 1) % ~matBars
|
||||
~lp[\matBar] = ~matNextBar.value;
|
||||
}
|
||||
}).play(clock, quant: ~matBeatsPerBar);
|
||||
~matPush.()
|
||||
@@ -227,7 +248,8 @@ File.exists(~matDir).not.if({ ("mkdir -p " ++ ~matDir.quote).systemCmd });
|
||||
~matListPush = {
|
||||
try {
|
||||
var names = (~matNames.(~matPresetDir) ++ ~matNames.(~matDir)).as(Set).asArray.sort;
|
||||
~toscSend !? { ~toscSend.valueArray(["/matrix/list"] ++ names) }
|
||||
~toscSend !? { ~toscSend.valueArray(["/matrix/list"] ++ names) };
|
||||
~matLoopPush.()
|
||||
} { |e|
|
||||
("[matrix] listPush err: " ++ e.class.name).postln
|
||||
}
|
||||
@@ -308,5 +330,15 @@ OSCdef(\mat_list, { |msg, time, addr|
|
||||
~matListPush.()
|
||||
}, '/matrix/list');
|
||||
|
||||
OSCdef(\mat_loop, { |msg, time, addr|
|
||||
~toscTouch !? { ~toscTouch.(addr) };
|
||||
~matSetLoop.(msg[1].asInteger, msg[2].asInteger)
|
||||
}, '/matrix/loop');
|
||||
|
||||
OSCdef(\mat_seek, { |msg, time, addr|
|
||||
~toscTouch !? { ~toscTouch.(addr) };
|
||||
~matSeek.(msg[1].asInteger)
|
||||
}, '/matrix/seek');
|
||||
|
||||
"[matrix] ready".postln;
|
||||
)
|
||||
|
||||
@@ -141,6 +141,36 @@ pass = pass and: { ~trigLog[0] == "/matrix/trig" };
|
||||
pass = pass and: { ~trigLog[1] == 5 };
|
||||
pass = pass and: { ~trigLog[2] == 1.0 }; // color 2 spec[\amp]=1.0; base has no \amp
|
||||
|
||||
// -- Loop region --
|
||||
pass = pass and: { ~matNextBar.notNil and: { ~matSetLoop.notNil and: { ~matSeek.notNil } } };
|
||||
|
||||
~matSetLoop.(4, 8);
|
||||
pass = pass and: { ~lp[\matLoopStart] == 4 };
|
||||
pass = pass and: { ~lp[\matLoopEnd] == 8 };
|
||||
|
||||
// Swapped args must auto-order
|
||||
~matSetLoop.(10, 6);
|
||||
pass = pass and: { ~lp[\matLoopStart] == 6 };
|
||||
pass = pass and: { ~lp[\matLoopEnd] == 10 };
|
||||
|
||||
// Wrap: loop 2..4
|
||||
~matSetLoop.(2, 4);
|
||||
~lp[\matBar] = 4;
|
||||
pass = pass and: { ~matNextBar.value == 2 }; // at le -> wraps to loop start
|
||||
~lp[\matBar] = 2;
|
||||
pass = pass and: { ~matNextBar.value == 3 }; // inside region -> advances
|
||||
~lp[\matBar] = 0;
|
||||
pass = pass and: { ~matNextBar.value == 2 }; // below ls -> snap to loop start
|
||||
|
||||
// Seek
|
||||
~matSeek.(7);
|
||||
pass = pass and: { ~lp[\matBar] == 7 };
|
||||
~matSeek.(99);
|
||||
pass = pass and: { ~lp[\matBar] == 31 }; // clamped to ~matBars - 1
|
||||
|
||||
// Reset loop to full range
|
||||
~matSetLoop.(0, 31);
|
||||
|
||||
pass.if(
|
||||
{ "TEST PASS".postln },
|
||||
{ "TEST FAIL".postln }
|
||||
|
||||
@@ -129,3 +129,16 @@ button.queued { animation: queued-blink 600ms ease-in-out infinite alternate; }
|
||||
.mat-action-btn { width: auto; padding: 8px 14px; font-size: 14px; flex-shrink: 0; }
|
||||
.mat-refresh-btn { width: auto; padding: 8px 12px; font-size: 16px; flex-shrink: 0;
|
||||
background: #1a2a1a; border-color: #2a4a2a; color: #4d9; }
|
||||
.mat-loop-full-btn { width: auto; padding: 8px 14px; font-size: 13px; margin: 4px 0;
|
||||
background: #1a1a2e; border-color: #335; color: #9af; }
|
||||
|
||||
/* --- Matrix timeline header --- */
|
||||
.matrix-timeline { display: flex; gap: 1px; margin: 2px 0; touch-action: none; }
|
||||
.tl-spacer { width: 54px; flex-shrink: 0; }
|
||||
.tl-bar { width: 22px; min-width: 22px; height: 20px; flex-shrink: 0;
|
||||
background: #1a1a1a; border: 1px solid #2a2a2a; border-radius: 2px;
|
||||
cursor: pointer; font-size: 9px; color: #555; text-align: center;
|
||||
line-height: 20px; user-select: none; -webkit-user-select: none; }
|
||||
.tl-bar.in-loop { background: #1a2a1a; border-color: #2a5a2a; color: #4d9; }
|
||||
.tl-bar.tl-head { background: #fa0; border-color: #fc0; color: #000; }
|
||||
.tl-bar.tl-head.in-loop { background: #fa0; border-color: #fc0; color: #000; }
|
||||
|
||||
@@ -143,6 +143,19 @@ ws.addEventListener("message", (ev) => {
|
||||
const el = cellRefs[matPlayhead][vi];
|
||||
if (el) el.classList.add("playing");
|
||||
}
|
||||
updateTimelineHead(bar);
|
||||
return;
|
||||
}
|
||||
|
||||
// /matrix/loop <start> <end> — loop region feedback
|
||||
if (address === "/matrix/loop") {
|
||||
const s = Math.round(Number(args[0]));
|
||||
const e = Math.round(Number(args[1]));
|
||||
if (Number.isFinite(s) && Number.isFinite(e) && s >= 0 && e < MATRIX_BARS) {
|
||||
matLoopStart = Math.min(s, e);
|
||||
matLoopEnd = Math.max(s, e);
|
||||
updateTimelineLoop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -211,6 +224,8 @@ 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;
|
||||
let matLoopStart = 0;
|
||||
let matLoopEnd = 31;
|
||||
|
||||
function loadMatState() {
|
||||
try {
|
||||
@@ -291,6 +306,107 @@ function renderMatrix() {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Matrix timeline: 32-bar loop region + playhead ---
|
||||
function renderTimeline() {
|
||||
const tl = document.getElementById("matrix-timeline");
|
||||
if (!tl) return;
|
||||
tl.innerHTML = "";
|
||||
const spacer = document.createElement("span");
|
||||
spacer.className = "tl-spacer";
|
||||
tl.appendChild(spacer);
|
||||
for (let bar = 0; bar < MATRIX_BARS; bar++) {
|
||||
const cell = document.createElement("span");
|
||||
cell.className = "tl-bar";
|
||||
cell.dataset.bar = bar;
|
||||
if (bar % 4 === 0) cell.textContent = String(bar + 1);
|
||||
if (bar >= matLoopStart && bar <= matLoopEnd) cell.classList.add("in-loop");
|
||||
if (bar === matPlayhead) cell.classList.add("tl-head");
|
||||
tl.appendChild(cell);
|
||||
}
|
||||
}
|
||||
|
||||
function updateTimelineLoop() {
|
||||
const tl = document.getElementById("matrix-timeline");
|
||||
if (!tl) return;
|
||||
tl.querySelectorAll(".tl-bar").forEach((cell) => {
|
||||
const b = +cell.dataset.bar;
|
||||
cell.classList.toggle("in-loop", b >= matLoopStart && b <= matLoopEnd);
|
||||
});
|
||||
}
|
||||
|
||||
function updateTimelineHead(bar) {
|
||||
const tl = document.getElementById("matrix-timeline");
|
||||
if (!tl) return;
|
||||
const prev = tl.querySelector(".tl-head");
|
||||
if (prev) prev.classList.remove("tl-head");
|
||||
const next = tl.querySelector(`.tl-bar[data-bar="${bar}"]`);
|
||||
if (next) next.classList.add("tl-head");
|
||||
}
|
||||
|
||||
function initTimelinePointer() {
|
||||
const tl = document.getElementById("matrix-timeline");
|
||||
if (!tl) return;
|
||||
let dragStart = -1;
|
||||
let dragging = false;
|
||||
|
||||
function barFromPoint(x, y) {
|
||||
const el = document.elementFromPoint(x, y);
|
||||
if (!el) return -1;
|
||||
const barEl = el.classList && el.classList.contains("tl-bar")
|
||||
? el
|
||||
: (el.closest ? el.closest(".tl-bar") : null);
|
||||
if (!barEl) return -1;
|
||||
const b = +barEl.dataset.bar;
|
||||
return (Number.isFinite(b) && b >= 0 && b < MATRIX_BARS) ? b : -1;
|
||||
}
|
||||
|
||||
function previewLoop(start, end) {
|
||||
const lo = Math.min(start, end);
|
||||
const hi = Math.max(start, end);
|
||||
tl.querySelectorAll(".tl-bar").forEach((cell) => {
|
||||
const b = +cell.dataset.bar;
|
||||
cell.classList.toggle("in-loop", b >= lo && b <= hi);
|
||||
});
|
||||
}
|
||||
|
||||
tl.addEventListener("pointerdown", (e) => {
|
||||
const bar = barFromPoint(e.clientX, e.clientY);
|
||||
if (bar < 0) return;
|
||||
e.preventDefault();
|
||||
tl.setPointerCapture(e.pointerId);
|
||||
dragStart = bar;
|
||||
dragging = true;
|
||||
});
|
||||
|
||||
tl.addEventListener("pointermove", (e) => {
|
||||
if (!dragging) return;
|
||||
const cur = barFromPoint(e.clientX, e.clientY);
|
||||
if (cur >= 0) previewLoop(dragStart, cur);
|
||||
});
|
||||
|
||||
tl.addEventListener("pointerup", (e) => {
|
||||
if (!dragging) return;
|
||||
dragging = false;
|
||||
tl.releasePointerCapture(e.pointerId);
|
||||
const end = barFromPoint(e.clientX, e.clientY);
|
||||
const validEnd = end >= 0 ? end : dragStart;
|
||||
if (validEnd === dragStart) {
|
||||
send("/matrix/seek", dragStart);
|
||||
} else {
|
||||
const lo = Math.min(dragStart, validEnd);
|
||||
const hi = Math.max(dragStart, validEnd);
|
||||
send("/matrix/loop", lo, hi);
|
||||
}
|
||||
dragStart = -1;
|
||||
});
|
||||
|
||||
tl.addEventListener("pointercancel", () => {
|
||||
dragging = false;
|
||||
dragStart = -1;
|
||||
updateTimelineLoop();
|
||||
});
|
||||
}
|
||||
|
||||
// --- Matrix audio-reactive glow (trigger envelope; math in matrix_glow.js) ---
|
||||
const voiceLevel = new Array(MATRIX_VOICES.length).fill(0);
|
||||
let glowRaf = null;
|
||||
@@ -516,6 +632,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
renderRhyGrid();
|
||||
renderMelSteps();
|
||||
renderMatrix();
|
||||
renderTimeline();
|
||||
initTimelinePointer();
|
||||
|
||||
// Name input: rename selected preset and update button labels
|
||||
const melName = document.getElementById("mel-name");
|
||||
@@ -619,6 +737,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const sceneNext = document.getElementById("scene-next");
|
||||
if (sceneNext) sceneNext.addEventListener("click", () => send("/scene/next"));
|
||||
|
||||
// Matrix loop full reset
|
||||
const matLoopFull = document.getElementById("matrix-loop-full");
|
||||
if (matLoopFull) matLoopFull.addEventListener("click", () => send("/matrix/loop", 0, MATRIX_BARS - 1));
|
||||
|
||||
// Matrix transport
|
||||
const matPlay = document.getElementById("matrix-play");
|
||||
if (matPlay) matPlay.addEventListener("click", () => send("/matrix/play"));
|
||||
|
||||
@@ -165,7 +165,9 @@
|
||||
<button id="matrix-load" class="mat-action-btn">CHARGER</button>
|
||||
<button id="matrix-refresh" class="mat-refresh-btn" title="Rafraichir la liste">↻</button>
|
||||
</div>
|
||||
<button id="matrix-loop-full" class="mat-action-btn mat-loop-full-btn">BOUCLE COMPLETE</button>
|
||||
<div class="matrix-scroll">
|
||||
<div id="matrix-timeline" class="matrix-timeline"></div>
|
||||
<div id="matrix-grid"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user