⤳ Follow Actions — the arrangement composes itself
Ableton's Follow Actions (Live manual §16.7) on the arranger's clips: after a clip plays its bar, it can hand off to another by chance — again / next / any / any-other / stop — so an 8-bar loop stops being static. The manual's own words: "structures that repeat but can also be surprising… never exactly repeat." - per clip-letter action + chance, edited in the grooves tab. - a live overlay (arr.now) diverts playback without rewriting the authored grid; with no follow actions set, playback is byte-identical to before (no regression). - ⤳ marks clips that can divert; the feel travels with the patch. - self-check now guards the follow-action decision logic. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b248d9e629
commit
0a89080e8d
@ -205,6 +205,9 @@
|
||||
#arranger .exprcell.bar { background: rgba(40,48,72,0.7); }
|
||||
#arranger .exprbar { width: 100%; background: rgba(120,150,230,0.35); border-radius: 4px; min-height: 2px; }
|
||||
#arranger .exprbar.lit { background: rgba(140,175,255,0.85); }
|
||||
#arranger .acell.hasfollow { position: relative; }
|
||||
#arranger .acell.hasfollow::after { content: "⤳"; position: absolute; top: -2px; right: 2px;
|
||||
font-size: 9px; color: rgba(255,225,150,0.9); }
|
||||
|
||||
/* the groove grid — piano roll / 808 steps inside a voice's tab */
|
||||
.seqgrid { display: grid; grid-template-columns: repeat(16, 12px); gap: 2px; margin-top: 6px; }
|
||||
@ -1300,7 +1303,8 @@
|
||||
// of groove (A/B/C patterns on the existing grids), "live" means the world
|
||||
// plays that lane, dark means silence. Eight bars, looping on godtime. The
|
||||
// Source stays live; you arrange when the Matter speaks.
|
||||
const arr = { open: false, playing: false, bars: 8, bar: -1, grid: {}, banks: {}, prevMute: {} };
|
||||
const arr = { open: false, playing: false, bars: 8, bar: -1, grid: {}, banks: {}, prevMute: {},
|
||||
follow: {}, now: {} }; // follow: per-clip-letter action+chance · now: the live diverted letter per track
|
||||
let arrPaintBar = null; // the panel's playhead painter, when open
|
||||
let arrBarAbs = -1; // absolute bar counter off the godtime clock
|
||||
function arrBank(key, isNote) {
|
||||
@ -1316,11 +1320,28 @@
|
||||
function arrLane(key) {
|
||||
return arr.grid[key] || (arr.grid[key] = new Array(arr.bars).fill("live"));
|
||||
}
|
||||
// Follow Actions (manual §16.7): a clip can hand off to another by chance, so
|
||||
// an 8-bar loop stops being static — "structures that repeat but can surprise."
|
||||
// Returns the letter/"mute" to divert to, or null = obey the grid as written.
|
||||
function arrFollowNext(key, cur) {
|
||||
const fa = arr.follow[key] && arr.follow[key][cur];
|
||||
if (!fa || !fa.act || !(Math.random() < (fa.chance != null ? fa.chance : 1))) return null;
|
||||
const used = ["A", "B", "C"].filter(L => (arr.grid[key] || []).includes(L));
|
||||
if (!used.length) return null;
|
||||
switch (fa.act) {
|
||||
case "stop": return "mute";
|
||||
case "again": return cur;
|
||||
case "next": { const i = used.indexOf(cur); return used[(i + 1) % used.length]; }
|
||||
case "any": return used[Math.floor(Math.random() * used.length)];
|
||||
case "other": { const o = used.filter(L => L !== cur); return o.length ? o[Math.floor(Math.random() * o.length)] : cur; }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function applyArrColumn() {
|
||||
for (const key in arr.grid) {
|
||||
const n = dests.get(key);
|
||||
if (!n) continue;
|
||||
const st = arr.grid[key][arr.bar] || "live";
|
||||
const st = (arr.now[key] != null ? arr.now[key] : (arr.grid[key][arr.bar] || "live"));
|
||||
const t2 = tw(key);
|
||||
if (st === "mute") {
|
||||
t2.mute = 1;
|
||||
@ -1341,12 +1362,13 @@
|
||||
if (arr.playing) return;
|
||||
arr.playing = true;
|
||||
arr.prevMute = {};
|
||||
arr.now = {}; // start clean — no diversions yet
|
||||
for (const key in arr.grid) arr.prevMute[key] = tw(key).mute;
|
||||
arr.bar = -1; // next bar boundary starts the song
|
||||
}
|
||||
function arrStop() {
|
||||
if (!arr.playing) return;
|
||||
arr.playing = false; arr.bar = -1;
|
||||
arr.playing = false; arr.bar = -1; arr.now = {};
|
||||
for (const key in arr.grid) { // hand the voices back to the world
|
||||
tw(key).mute = arr.prevMute[key] || 0;
|
||||
const sq = seqs[key]; if (sq) sq.on = false;
|
||||
@ -1537,6 +1559,34 @@
|
||||
}
|
||||
body.appendChild(gr);
|
||||
|
||||
// --- Follow Action for the selected clip letter (§16.7): the song composes itself ---
|
||||
const fa = (arr.follow[arrSelKey] && arr.follow[arrSelKey][arrSelLetter]) || { act: "", chance: 1 };
|
||||
const setFA = (patch) => { const fol = arr.follow[arrSelKey] || (arr.follow[arrSelKey] = {});
|
||||
Object.assign(fol[arrSelLetter] || (fol[arrSelLetter] = { act: "", chance: 1 }), patch); };
|
||||
const fr = document.createElement("div"); fr.className = "arow grow";
|
||||
const flbl = document.createElement("span"); flbl.className = "glbl"; flbl.style.minWidth = "auto";
|
||||
flbl.textContent = "after " + arrSelLetter + " →";
|
||||
const fsel = document.createElement("select"); fsel.className = "asel";
|
||||
[["", "— stay"], ["again", "play again"], ["next", "next clip"], ["any", "any clip"], ["other", "any other"], ["stop", "stop"]]
|
||||
.forEach(([v, t]) => { const o = document.createElement("option"); o.value = v; o.textContent = t; if (v === fa.act) o.selected = true; fsel.appendChild(o); });
|
||||
fsel.onchange = () => { setFA({ act: fsel.value }); openArranger(); };
|
||||
fr.appendChild(flbl); fr.appendChild(fsel);
|
||||
if (fa.act) {
|
||||
const cwrap = document.createElement("span"); cwrap.className = "gctl";
|
||||
const clab = document.createElement("span"); clab.className = "glbl"; clab.textContent = "chance";
|
||||
const csl = document.createElement("input"); csl.type = "range"; csl.min = "0"; csl.max = "100";
|
||||
csl.value = String(Math.round((fa.chance != null ? fa.chance : 1) * 100));
|
||||
const cval = document.createElement("span"); cval.className = "gval"; cval.textContent = csl.value + "%";
|
||||
csl.oninput = () => { setFA({ chance: clamp(+csl.value / 100, 0, 1) }); cval.textContent = csl.value + "%"; };
|
||||
cwrap.appendChild(clab); cwrap.appendChild(csl); cwrap.appendChild(cval);
|
||||
fr.appendChild(cwrap);
|
||||
}
|
||||
const fhint = document.createElement("span"); fhint.className = "albl";
|
||||
fhint.textContent = fa.act ? "▶ playing, the arrangement diverts here by chance — never quite the same twice"
|
||||
: "give a clip a follow action and the song writes its own variations";
|
||||
fr.appendChild(fhint);
|
||||
body.appendChild(fr);
|
||||
|
||||
const steps = arrBank(arrSelKey, nSel.isNote)[arrSelLetter];
|
||||
const rows = nSel.isNote ? SEQ_ROWS.length : 1;
|
||||
const grid = document.createElement("div");
|
||||
@ -1642,6 +1692,8 @@
|
||||
const st = lane[b];
|
||||
cell.textContent = st === "live" ? "●" : st === "mute" ? "" : st;
|
||||
cell.className = "acell " + (st === "live" ? "alive" : st === "mute" ? "amute" : "aclip");
|
||||
if ((st === "A" || st === "B" || st === "C") && arr.follow[key] && arr.follow[key][st] && arr.follow[key][st].act)
|
||||
cell.classList.add("hasfollow"); // this clip can divert the song (§16.7)
|
||||
if (arr.playing && b === arr.bar) cell.classList.add("acur");
|
||||
};
|
||||
cell.onclick = () => {
|
||||
@ -2124,6 +2176,18 @@
|
||||
if (!neu || neu.vel[5] !== 0.3 || neu.chance[5] !== 0.5) f.push("migrate-preserve");
|
||||
if (migrateSeq({ steps: [1, 2, 3] }) !== null) f.push("migrate-reject-badlen");
|
||||
if (clamp(0.8 + 1 * 1 * 0.5, 0.05, 1) !== 1) f.push("vel-clamp"); // velo deviation stays in range
|
||||
// follow-action decisions (arrFollowNext) — save/restore arr + Math.random
|
||||
const _rnd = Math.random, _hadF = "__t" in arr.follow, _hadG = "__t" in arr.grid, _f = arr.follow["__t"], _g = arr.grid["__t"];
|
||||
arr.grid["__t"] = ["A", "B", "C", "A"];
|
||||
arr.follow["__t"] = { A: { act: "next", chance: 1 }, B: { act: "stop", chance: 1 } };
|
||||
Math.random = () => 0;
|
||||
if (arrFollowNext("__t", "A") !== "B") f.push("follow-next"); // next after A = B
|
||||
if (arrFollowNext("__t", "B") !== "mute") f.push("follow-stop"); // stop = mute
|
||||
arr.follow["__t"].A.chance = 0; Math.random = () => 0.5;
|
||||
if (arrFollowNext("__t", "A") !== null) f.push("follow-chance0"); // chance 0 never diverts
|
||||
Math.random = _rnd;
|
||||
if (_hadF) arr.follow["__t"] = _f; else delete arr.follow["__t"];
|
||||
if (_hadG) arr.grid["__t"] = _g; else delete arr.grid["__t"];
|
||||
if (f.length) console.error("⚠ groove self-check FAILED:", f); else console.log("✓ groove self-check passed");
|
||||
return f.length === 0;
|
||||
}
|
||||
@ -2134,7 +2198,8 @@
|
||||
curve: r.curve || "lin", gate: r.gate, quantize: r.quantize, label: r.label })),
|
||||
tweaks: JSON.parse(JSON.stringify(tweaks)),
|
||||
seqs: JSON.parse(JSON.stringify(seqs)), // your grooves travel with the patch
|
||||
arr: { grid: JSON.parse(JSON.stringify(arr.grid)), banks: JSON.parse(JSON.stringify(arr.banks)) },
|
||||
arr: { grid: JSON.parse(JSON.stringify(arr.grid)), banks: JSON.parse(JSON.stringify(arr.banks)),
|
||||
follow: JSON.parse(JSON.stringify(arr.follow)) },
|
||||
bpm: godtime.bpm,
|
||||
groove: Object.assign({}, groove), // the feel travels with the patch
|
||||
};
|
||||
@ -2156,6 +2221,7 @@
|
||||
if (p.arr && p.arr.grid && p.arr.banks) { // the arrangement travels too
|
||||
arr.grid = JSON.parse(JSON.stringify(p.arr.grid));
|
||||
arr.banks = JSON.parse(JSON.stringify(p.arr.banks));
|
||||
arr.follow = p.arr.follow ? JSON.parse(JSON.stringify(p.arr.follow)) : {};
|
||||
if (arr.open) openArranger(); // rebuild the panel on the new song
|
||||
}
|
||||
if (typeof p.bpm === "number") godtime.bpm = clamp(p.bpm, 40, 180);
|
||||
@ -2676,6 +2742,11 @@
|
||||
arrBarAbs = barAbs;
|
||||
if (arr.playing) {
|
||||
arr.bar = (arr.bar + 1) % arr.bars;
|
||||
for (const key in arr.grid) { // follow actions divert this bar (else obey the grid)
|
||||
const prev = arr.now[key] != null ? arr.now[key] : arr.grid[key][(arr.bar + arr.bars - 1) % arr.bars];
|
||||
const nxt = (prev === "A" || prev === "B" || prev === "C") ? arrFollowNext(key, prev) : null;
|
||||
arr.now[key] = nxt != null ? nxt : arr.grid[key][arr.bar];
|
||||
}
|
||||
applyArrColumn();
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user