feat: step editor and preset naming
CI build oscope-of / build-check (push) Has been cancelled

This commit is contained in:
L'électron rare
2026-06-27 23:58:57 +02:00
parent 82ac0181a3
commit 7f44ee527c
4 changed files with 261 additions and 2 deletions
+24
View File
@@ -334,5 +334,29 @@ OSCdef(\seqRhythm, { |msg|
});
}, '/seq/rhythm');
OSCdef(\seqRhythmSet, { |msg|
var sel;
~lp.notNil.if({
sel = (~lp[\rhySel] ? 0).clip(0, (~lp[\rhythms].notNil.if({ ~lp[\rhythms].size - 1 }, { 0 })));
~lp[\rhythms][sel] = (
name: (~lp[\rhythms][sel][\name] ? "edit"),
k: msg[1].asString,
s: msg[2].asString,
h: msg[3].asString
);
});
}, '/seq/rhythm/set');
OSCdef(\seqMelodySet, { |msg|
var sel, degs;
~lp.notNil.if({
sel = (~lp[\melSel] ? 0).clip(0, (~lp[\melodies].notNil.if({ ~lp[\melodies].size - 1 }, { 0 })));
(msg.size > 1).if({
degs = (1..(msg.size - 1)).collect { |i| msg[i].asInteger };
~lp[\melodies][sel] = degs;
});
});
}, '/seq/melody/set');
"[data-only/launchpad] 16 patterns + 2 sequencers (melseq+rhythmseq) + clock-sync + vol faders ready".postln;
)
+24
View File
@@ -17,3 +17,27 @@ button.sel { background: #246; border-color: #4af; color: #8cf; }
.seq-block h3 { font-size: 13px; color: #9af; margin: 8px 0 4px; }
.seq-row { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.seq-row button { min-width: 48px; padding: 12px 8px; font-size: 14px; }
/* --- Step editor --- */
.name-row { margin: 6px 0; }
.preset-name { background: #1a1a2e; color: #9af; border: 1px solid #335;
border-radius: 6px; padding: 6px 10px; font-size: 14px; width: 100%; }
.preset-name:focus { outline: none; border-color: #4af; }
.step-grid { margin: 8px 0; }
.step-row { display: flex; align-items: center; gap: 3px; margin: 3px 0; }
.step-row-label { width: 22px; font-size: 11px; color: #666; text-align: center;
flex-shrink: 0; font-weight: 700; letter-spacing: .04em; }
.step-cell { width: 38px; height: 38px; min-width: 38px; background: #1c1c1c;
border: 1px solid #333; border-radius: 4px; cursor: pointer; padding: 0;
flex-shrink: 0; transition: background 0.07s; }
.step-cell:active { opacity: 0.65; }
.step-cell.on { background: #2b6; border-color: #4d9; }
.mel-steps { display: flex; flex-wrap: wrap; gap: 4px; margin: 8px 0; }
.mel-cell input[type=number] { width: 46px; height: 40px; background: #1c1c1c; color: #eee;
border: 1px solid #333; border-radius: 4px; text-align: center; font-size: 13px;
-moz-appearance: textfield; cursor: pointer; }
.mel-cell input[type=number]::-webkit-inner-spin-button,
.mel-cell input[type=number]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.mel-cell input[type=number]:focus { outline: none; border-color: #4af; background: #1a2030; }
+205 -2
View File
@@ -12,29 +12,232 @@ function togglePad(el, name) {
el.classList.toggle("armed", on);
send("/launch", name, on ? 1 : 0);
}
// --- Pattern state (defaults mirror launchpad.scd exactly) ---
const STORAGE_KEY = "avlive.seq";
const DFLT_RHYTHMS = [
{ name: "4floor", k: "1000100010001000", s: "0000100000001000", h: "0010101010101010" },
{ name: "breakbeat", k: "1000101010001000", s: "0000100100001001", h: "0101010101010101" },
{ name: "halftime", k: "1000000010000000", s: "0000000000001000", h: "0010001001000010" },
{ name: "psyroll", k: "1001100110011001", s: "0000100000001000", h: "1111111111111111" },
{ name: "garage", k: "1000001010000010", s: "0000100000001000", h: "0101101001011010" },
{ name: "tribal", k: "1010100010101000", s: "0000101000001010", h: "1010101010101010" },
{ name: "industrial", k: "1100110011001100", s: "0011001100110011", h: "0101010101010101" },
{ name: "jungle", k: "1000100110001001", s: "0000100100001001", h: "1011101110111011" },
];
const DFLT_MELODIES = [
{ name: "Mélodie 1", degrees: [0, 2, 3, 5, 7, 5, 3, 2] },
{ name: "Mélodie 2", degrees: [0, 3, 7, 3, 0, -2] },
{ name: "Mélodie 3", degrees: [0, 0, 7, 0, 3, 5, 2, 0, 3, 5, 7, 3] },
{ name: "Mélodie 4", degrees: [7, 5, 3, 2, 0, -2, 0, 2] },
{ name: "Mélodie 5", degrees: [0, 2, 3, 5, 7, 10, 7, 5, 3, 0, 2, 3, 5, 3, 2, 0] },
{ name: "Mélodie 6", degrees: [0, 3, 7] },
{ name: "Mélodie 7", degrees: [0, 7, 5, 3, 7, 0] },
{ name: "Mélodie 8", degrees: [2, 0, -2, 0, 3, 2, 0, -2, 5, 3, 2, 0] },
];
let state = null;
function seedState() {
state = {
rhythms: DFLT_RHYTHMS.map(r => ({ name: r.name, k: r.k, s: r.s, h: r.h })),
melodies: DFLT_MELODIES.map(m => ({ name: m.name, degrees: [...m.degrees] })),
rhySel: 0,
melSel: 0,
};
}
function loadState() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw);
if (
parsed &&
Array.isArray(parsed.rhythms) && parsed.rhythms.length === 8 &&
Array.isArray(parsed.melodies) && parsed.melodies.length === 8 &&
typeof parsed.rhySel === "number" &&
typeof parsed.melSel === "number"
) {
state = parsed;
return;
}
}
} catch (e) {}
seedState();
}
function saveState() {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) {}
}
// --- Render rhythm 3x16 step grid ---
function renderRhyGrid() {
const container = document.getElementById("rhy-grid");
if (!container) return;
const r = state.rhythms[state.rhySel];
const voices = [
{ key: "k", label: "K", str: r.k },
{ key: "s", label: "S", str: r.s },
{ key: "h", label: "H", str: r.h },
];
container.innerHTML = voices.map(v =>
`<div class="step-row">` +
`<span class="step-row-label">${v.label}</span>` +
Array.from({ length: 16 }, (_, i) => {
const on = (v.str[i] || "0") === "1";
return `<button class="step-cell${on ? " on" : ""}" data-voice="${v.key}" data-step="${i}" aria-pressed="${on}"></button>`;
}).join("") +
`</div>`
).join("");
container.querySelectorAll(".step-cell").forEach(cell =>
cell.addEventListener("click", () => {
const voice = cell.dataset.voice;
const step = +cell.dataset.step;
const preset = state.rhythms[state.rhySel];
const arr = preset[voice].split("");
arr[step] = arr[step] === "1" ? "0" : "1";
preset[voice] = arr.join("");
saveState();
const isOn = arr[step] === "1";
cell.classList.toggle("on", isOn);
cell.setAttribute("aria-pressed", isOn);
send("/seq/rhythm/set", preset.k, preset.s, preset.h);
})
);
}
// --- Render melody degree inputs ---
function renderMelSteps() {
const container = document.getElementById("mel-steps");
if (!container) return;
const m = state.melodies[state.melSel];
container.innerHTML = m.degrees.map((deg, i) =>
`<div class="mel-cell"><input type="number" min="-7" max="14" value="${deg}" data-midx="${i}"></div>`
).join("");
container.querySelectorAll("input[type=number]").forEach(inp =>
inp.addEventListener("change", () => {
const i = +inp.dataset.midx;
const val = Math.max(-7, Math.min(14, isNaN(+inp.value) ? 0 : +inp.value));
inp.value = val;
state.melodies[state.melSel].degrees[i] = val;
saveState();
send("/seq/melody/set", ...state.melodies[state.melSel].degrees);
})
);
}
// --- Update selector button text from state ---
function updateSelLabels(group) {
document.querySelectorAll(`[data-seq="${group}"]`).forEach(btn => {
const idx = +btn.dataset.seqidx;
if (group === "rhythm" && state.rhythms[idx]) {
btn.textContent = state.rhythms[idx].name;
} else if (group === "melody" && state.melodies[idx]) {
btn.textContent = state.melodies[idx].name;
}
});
}
// --- Restore .sel highlight from persisted state ---
function restoreSel(group, idx) {
document.querySelectorAll(`[data-seq="${group}"]`).forEach(b => b.classList.remove("sel"));
const sel = document.querySelector(`[data-seq="${group}"][data-seqidx="${idx}"]`);
if (sel) sel.classList.add("sel");
}
// --- Sync name inputs to selected preset ---
function updateNameInputs() {
const mn = document.getElementById("mel-name");
if (mn) mn.value = state.melodies[state.melSel].name;
const rn = document.getElementById("rhy-name");
if (rn) rn.value = state.rhythms[state.rhySel].name;
}
document.addEventListener("DOMContentLoaded", () => {
loadState();
// Render editors from state
restoreSel("melody", state.melSel);
restoreSel("rhythm", state.rhySel);
updateSelLabels("melody");
updateSelLabels("rhythm");
updateNameInputs();
renderRhyGrid();
renderMelSteps();
// Name input: rename selected preset and update button labels
const melName = document.getElementById("mel-name");
if (melName) {
melName.addEventListener("input", () => {
state.melodies[state.melSel].name = melName.value;
saveState();
updateSelLabels("melody");
});
}
const rhyName = document.getElementById("rhy-name");
if (rhyName) {
rhyName.addEventListener("input", () => {
state.rhythms[state.rhySel].name = rhyName.value;
saveState();
updateSelLabels("rhythm");
});
}
// Pad arm buttons
document.querySelectorAll("[data-pad]").forEach((el) =>
el.addEventListener("click", () => togglePad(el, el.dataset.pad)));
// Generic OSC buttons
document.querySelectorAll("[data-osc]").forEach((el) =>
el.addEventListener("click", () => {
const a = el.dataset.arg;
send(el.dataset.osc, ...(a === undefined ? [] : [isNaN(+a) ? a : +a]));
}));
// Vol sliders
document.querySelectorAll("[data-vol]").forEach((el) =>
el.addEventListener("input", () => send("/launch/vol", el.dataset.vol, +el.value)));
// Tempo
const tempo = document.getElementById("tempo");
tempo && tempo.addEventListener("input", () => send("/launch/tempo", +tempo.value));
// Filter
const filt = document.getElementById("filter");
filt && filt.addEventListener("input", () => send("/control/fx/filter", +filt.value));
// Clear
document.getElementById("clear").addEventListener("click", () => {
armed.clear(); document.querySelectorAll("[data-pad]").forEach((e) => e.classList.remove("armed"));
armed.clear();
document.querySelectorAll("[data-pad]").forEach((e) => e.classList.remove("armed"));
send("/launch/clear");
});
// Seq selector buttons — select preset, reload editor, push OSC
document.querySelectorAll("[data-seq]").forEach((el) =>
el.addEventListener("click", () => {
const group = el.dataset.seq;
const idx = +el.dataset.seqidx;
document.querySelectorAll(`[data-seq="${group}"]`).forEach((b) => b.classList.remove("sel"));
el.classList.add("sel");
send("/seq/" + group, +el.dataset.seqidx);
if (group === "rhythm") {
state.rhySel = idx;
saveState();
renderRhyGrid();
updateNameInputs();
send("/seq/rhythm", idx);
const r = state.rhythms[idx];
send("/seq/rhythm/set", r.k, r.s, r.h);
} else {
state.melSel = idx;
saveState();
renderMelSteps();
updateNameInputs();
send("/seq/melody", idx);
send("/seq/melody/set", ...state.melodies[idx].degrees);
}
}));
});
+8
View File
@@ -73,6 +73,10 @@
<button data-seq="melody" data-seqidx="6">M7</button>
<button data-seq="melody" data-seqidx="7">M8</button>
</div>
<div class="name-row">
<input type="text" class="preset-name" id="mel-name" placeholder="Nom du preset">
</div>
<div id="mel-steps" class="mel-steps"></div>
<input type="range" min="0" max="1.5" step="0.05" value="0.8" data-vol="melseq">
</div>
<div class="seq-block">
@@ -88,6 +92,10 @@
<button data-seq="rhythm" data-seqidx="6">R7</button>
<button data-seq="rhythm" data-seqidx="7">R8</button>
</div>
<div class="name-row">
<input type="text" class="preset-name" id="rhy-name" placeholder="Nom du preset">
</div>
<div id="rhy-grid" class="step-grid"></div>
<input type="range" min="0" max="1.5" step="0.05" value="0.8" data-vol="rhythmseq">
</div>
</section>