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