Add ghost-note dynamics level (4 levels) + help on local/sharing
Pads now cycle accent → normal → ghost → mute. Ghost is a soft hit (gain 0.25) — added as new level value 3 so set lists already saved at the 3-level stage (0/1/2 = mute/normal/accent) keep their meaning with no migration. Share pattern gains a "g" char; the Purdie shuffle's snare ghosts now use it. Ghost pads render faint with a · marker. Help: explain that set lists/items/log live only in localStorage and how to move or share them (Share set-list link / Share settings link / Export-Import). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8171b37e72
commit
d3403df6c2
2 changed files with 20 additions and 13 deletions
|
|
@ -72,7 +72,7 @@ Tokens are joined with `;`. `tr` and `rmp` are omitted when off.
|
||||||
into. Append **`s`** for **swing** on even subdivisions — `2s` (swung eighths) or
|
into. Append **`s`** for **swing** on even subdivisions — `2s` (swung eighths) or
|
||||||
`4s` (swung sixteenths) delay the off‑beats to a triplet (2:1) feel. Omit for quarter.
|
`4s` (swung sixteenths) delay the off‑beats to a triplet (2:1) feel. Omit for quarter.
|
||||||
- **`=pattern`** — per‑**step dynamics**, one char per pad: **`X`** accent, **`x`**
|
- **`=pattern`** — per‑**step dynamics**, one char per pad: **`X`** accent, **`x`**
|
||||||
normal, **`.`** mute (rest). Length = beats per bar × `sub`. Omit to get the
|
normal, **`g`** ghost (soft), **`.`** mute (rest). Length = beats per bar × `sub`. Omit to get the
|
||||||
default — the first step of **each beat** accented, the rest normal (click a pad in
|
default — the first step of **each beat** accented, the rest normal (click a pad in
|
||||||
the UI to cycle accent → normal → mute). e.g. `4=.X.X` accents the backbeat (2 & 4);
|
the UI to cycle accent → normal → mute). e.g. `4=.X.X` accents the backbeat (2 & 4);
|
||||||
`4/2s` is swung eighths with the default accents. (Legacy `x`/`.` on/off patterns and
|
`4/2s` is swung eighths with the default accents. (Legacy `x`/`.` on/off patterns and
|
||||||
|
|
|
||||||
31
index.html
31
index.html
|
|
@ -102,6 +102,8 @@
|
||||||
.led.on { background:var(--lc,#888); box-shadow:0 0 8px var(--lc); color:rgba(0,0,0,.55); }
|
.led.on { background:var(--lc,#888); box-shadow:0 0 8px var(--lc); color:rgba(0,0,0,.55); }
|
||||||
.led.accent { box-shadow:0 0 4px #fff, 0 0 14px var(--lc); }
|
.led.accent { box-shadow:0 0 4px #fff, 0 0 14px var(--lc); }
|
||||||
.led.accent::after { content:"▲"; position:absolute; top:-1px; font-size:7px; color:#fff; }
|
.led.accent::after { content:"▲"; position:absolute; top:-1px; font-size:7px; color:#fff; }
|
||||||
|
.led.ghost { opacity:.4; box-shadow:none; } /* ghost note — lit but faint */
|
||||||
|
.led.ghost::after { content:"·"; position:absolute; top:-4px; font-size:13px; color:#fff; }
|
||||||
.led.playhead { outline:2px solid var(--ring); outline-offset:1px; }
|
.led.playhead { outline:2px solid var(--ring); outline-offset:1px; }
|
||||||
.led.sub { width:20px; height:20px; opacity:.9; } /* subdivision step — smaller than a downbeat */
|
.led.sub { width:20px; height:20px; opacity:.9; } /* subdivision step — smaller than a downbeat */
|
||||||
.led.beatstart { margin-left:11px; } /* extra gap between beats within a group */
|
.led.beatstart { margin-left:11px; } /* extra gap between beats within a group */
|
||||||
|
|
@ -294,7 +296,7 @@
|
||||||
|
|
||||||
<div class="row" style="align-items:center; gap:12px; margin:14px 0 8px">
|
<div class="row" style="align-items:center; gap:12px; margin:14px 0 8px">
|
||||||
<h2 style="font-size:11px; text-transform:uppercase; letter-spacing:1.4px; color:var(--muted); margin:0">Meter lanes</h2>
|
<h2 style="font-size:11px; text-transform:uppercase; letter-spacing:1.4px; color:var(--muted); margin:0">Meter lanes</h2>
|
||||||
<span class="hint" style="margin:0; flex:1">Each beat splits into <i>subdivision</i> pads — click a pad to cycle <b>accent → normal → mute</b>. Pick a <i>swing</i> subdivision for a triplet feel.</span>
|
<span class="hint" style="margin:0; flex:1">Each beat splits into <i>subdivision</i> pads — click a pad to cycle <b>accent → normal → ghost → mute</b>. Pick a <i>swing</i> subdivision for a triplet feel.</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="meters"></div>
|
<div id="meters"></div>
|
||||||
<div style="margin-top:10px"><button class="add" id="addMeterBtn" title="add meter lane (A)">+</button></div>
|
<div style="margin-top:10px"><button class="add" id="addMeterBtn" title="add meter lane (A)">+</button></div>
|
||||||
|
|
@ -363,6 +365,7 @@
|
||||||
</table>
|
</table>
|
||||||
<div class="help-about">
|
<div class="help-about">
|
||||||
<p>Source: <a href="https://git.varasys.io/VARASYS/metronome" target="_blank" rel="noopener">git.varasys.io/VARASYS/metronome</a></p>
|
<p>Source: <a href="https://git.varasys.io/VARASYS/metronome" target="_blank" rel="noopener">git.varasys.io/VARASYS/metronome</a></p>
|
||||||
|
<p><b>Your set lists, items and practice log live only in this browser</b> (localStorage) — nothing is uploaded. To move or share them, use the set-list <b>⋯</b> menu: <b>Share set-list link</b> copies a link encoding the whole set list (open it elsewhere to import a copy); <b>Share settings link</b> shares just the loaded item; <b>Export all / Import file</b> back up everything as a JSON file.</p>
|
||||||
<p>This is a single-page app — save this page (<kbd>Ctrl/⌘+S</kbd>) and open the file to run it fully offline, no server needed. One catch when running from a local <code>file://</code>: it <b>won't auto-save your set list</b> between sessions, so export a backup (set-list <b>⋯</b> menu → <b>Export all</b>) to keep your work.</p>
|
<p>This is a single-page app — save this page (<kbd>Ctrl/⌘+S</kbd>) and open the file to run it fully offline, no server needed. One catch when running from a local <code>file://</code>: it <b>won't auto-save your set list</b> between sessions, so export a backup (set-list <b>⋯</b> menu → <b>Export all</b>) to keep your work.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -525,9 +528,9 @@ function scheduleMeterTick(m, time) {
|
||||||
const tickInBar = ((m.tick % barLen) + barLen) % barLen;
|
const tickInBar = ((m.tick % barLen) + barLen) % barLen;
|
||||||
m.vq.push({ time, step: tickInBar, bar: Math.floor(m.tick / barLen) }); // playhead (per step) + measure (advance even when muted)
|
m.vq.push({ time, step: tickInBar, bar: Math.floor(m.tick / barLen) }); // playhead (per step) + measure (advance even when muted)
|
||||||
if (!m.enabled || isMutedAt(time)) return;
|
if (!m.enabled || isMutedAt(time)) return;
|
||||||
const lvl = m.beatsOn[tickInBar] | 0; // per-step dynamics: 0 = rest, 1 = normal, 2 = accent
|
const lvl = m.beatsOn[tickInBar] | 0; // dynamics: 0 mute · 1 normal · 2 accent · 3 ghost
|
||||||
if (!lvl) return;
|
if (!lvl) return;
|
||||||
playInstrument(m.sound, time, lvl === 2 ? 1.0 : 0.6);
|
playInstrument(m.sound, time, lvl === 2 ? 1.0 : lvl === 3 ? 0.25 : 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reference bar = lane 1's bar; poly lanes fit their beats evenly into it.
|
// Reference bar = lane 1's bar; poly lanes fit their beats evenly into it.
|
||||||
|
|
@ -706,12 +709,15 @@ function buildLaneCard(m) {
|
||||||
recomputeLane(m);
|
recomputeLane(m);
|
||||||
}
|
}
|
||||||
|
|
||||||
const stepDefault = (s) => (s === 0 ? 2 : 1); // default dynamics: first step of each beat accented, rest normal
|
// Per-step dynamics levels: 0 mute · 1 normal · 2 accent · 3 ghost. (Ghost is the new
|
||||||
function normLevel(v, dflt) { // coerce a stored value to a 0/1/2 level
|
// value 3 so set lists saved at the 3-level stage — 0/1/2 — keep their meaning, no migration.)
|
||||||
|
const stepDefault = (s) => (s === 0 ? 2 : 1); // default: first step of each beat accented, rest normal
|
||||||
|
const NEXT_LEVEL = { 2: 1, 1: 3, 3: 0, 0: 2 }; // click cycle: accent → normal → ghost → mute → accent
|
||||||
|
function normLevel(v, dflt) { // coerce a stored value to a level
|
||||||
if (v === true) return dflt === 2 ? 2 : 1; // legacy boolean "on" → keep accent on downbeats
|
if (v === true) return dflt === 2 ? 2 : 1; // legacy boolean "on" → keep accent on downbeats
|
||||||
if (v === false) return 0;
|
if (v === false) return 0;
|
||||||
if (v == null) return dflt;
|
if (v == null) return dflt;
|
||||||
const n = v | 0; return n >= 2 ? 2 : n >= 1 ? 1 : 0;
|
const n = v | 0; return n >= 3 ? 3 : n >= 2 ? 2 : n >= 1 ? 1 : 0;
|
||||||
}
|
}
|
||||||
function recomputeLane(m) {
|
function recomputeLane(m) {
|
||||||
const p = parseGroups(m.groupsStr);
|
const p = parseGroups(m.groupsStr);
|
||||||
|
|
@ -745,8 +751,8 @@ function buildLaneStrip(m) { // one pad per STEP (b
|
||||||
cell.className = "led";
|
cell.className = "led";
|
||||||
cell.textContent = (s === 0) ? (b + 1) : ""; // label downbeats; subdivisions blank
|
cell.textContent = (s === 0) ? (b + 1) : ""; // label downbeats; subdivisions blank
|
||||||
cell.style.cursor = "pointer";
|
cell.style.cursor = "pointer";
|
||||||
cell.title = "click: accent → normal → mute · beat " + (b + 1) + (s ? " · sub " + (s + 1) : "");
|
cell.title = "click: accent → normal → ghost → mute · beat " + (b + 1) + (s ? " · sub " + (s + 1) : "");
|
||||||
cell.addEventListener("click", () => { m.beatsOn[i] = ((m.beatsOn[i] | 0) + 2) % 3; renderLaneStrip(m); }); // 2→1→0→2
|
cell.addEventListener("click", () => { m.beatsOn[i] = NEXT_LEVEL[m.beatsOn[i] | 0]; renderLaneStrip(m); });
|
||||||
m.stripEl.appendChild(cell);
|
m.stripEl.appendChild(cell);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -760,9 +766,10 @@ function renderLaneStrip(m) {
|
||||||
let cls = "led";
|
let cls = "led";
|
||||||
if (!onBeat) cls += " sub"; // subdivision pad (smaller/dimmer)
|
if (!onBeat) cls += " sub"; // subdivision pad (smaller/dimmer)
|
||||||
else if (i > 0 && !gs) cls += " beatstart"; // gap between beats within a group
|
else if (i > 0 && !gs) cls += " beatstart"; // gap between beats within a group
|
||||||
if (lvl >= 1) cls += " on"; // normal or accent → lit
|
if (lvl >= 1) cls += " on"; // normal / accent / ghost → lit
|
||||||
if (gs) cls += " groupstart"; // group divider (layout only)
|
if (gs) cls += " groupstart"; // group divider (layout only)
|
||||||
if (lvl === 2) cls += " accent"; // accented step (▲)
|
if (lvl === 2) cls += " accent"; // accented step (▲)
|
||||||
|
else if (lvl === 3) cls += " ghost"; // ghost note (faint ·)
|
||||||
cell.className = cls;
|
cell.className = cls;
|
||||||
cell.style.setProperty("--lc", m.color);
|
cell.style.setProperty("--lc", m.color);
|
||||||
if (state.running && i === m.currentStep) cell.classList.add("playhead");
|
if (state.running && i === m.currentStep) cell.classList.add("playhead");
|
||||||
|
|
@ -1091,7 +1098,7 @@ function laneCfgToStr(c) {
|
||||||
if (spb !== 1 || c.swing) s += "/" + spb + (c.swing ? "s" : ""); // "/2s" = swung eighths
|
if (spb !== 1 || c.swing) s += "/" + spb + (c.swing ? "s" : ""); // "/2s" = swung eighths
|
||||||
const on = c.beatsOn || []; // per-step dynamics: one char per pad (X accent / x normal / . mute)
|
const on = c.beatsOn || []; // per-step dynamics: one char per pad (X accent / x normal / . mute)
|
||||||
const isDefault = on.length && on.every((v, i) => (v | 0) === ((i % spb) === 0 ? 2 : 1));
|
const isDefault = on.length && on.every((v, i) => (v | 0) === ((i % spb) === 0 ? 2 : 1));
|
||||||
if (on.length && !isDefault) s += "=" + on.map((v) => (v >= 2 ? "X" : v >= 1 ? "x" : ".")).join("");
|
if (on.length && !isDefault) s += "=" + on.map((v) => (v === 3 ? "g" : v >= 2 ? "X" : v >= 1 ? "x" : ".")).join("");
|
||||||
if (c.poly) s += "~";
|
if (c.poly) s += "~";
|
||||||
if (c.enabled === false) s += "!"; // "!" = silenced / disabled
|
if (c.enabled === false) s += "!"; // "!" = silenced / disabled
|
||||||
return s;
|
return s;
|
||||||
|
|
@ -1106,7 +1113,7 @@ function laneStrToCfg(tok) {
|
||||||
if (sl >= 0) { groupsStr = rest.slice(0, sl); const sp = rest.slice(sl + 1); swing = /s$/i.test(sp); sub = parseInt(sp, 10) || 1; }
|
if (sl >= 0) { groupsStr = rest.slice(0, sl); const sp = rest.slice(sl + 1); swing = /s$/i.test(sp); sub = parseInt(sp, 10) || 1; }
|
||||||
const bpb = parseGroups(groupsStr).beatsPerBar;
|
const bpb = parseGroups(groupsStr).beatsPerBar;
|
||||||
// pattern levels: X=accent(2), x/1=normal(1), . / anything else = mute(0); no pattern → default (first of each beat accented)
|
// pattern levels: X=accent(2), x/1=normal(1), . / anything else = mute(0); no pattern → default (first of each beat accented)
|
||||||
const beatsOn = pattern ? pattern.split("").map((ch) => ch === "X" ? 2 : (ch === "x" || ch === "1") ? 1 : 0)
|
const beatsOn = pattern ? pattern.split("").map((ch) => ch === "X" ? 2 : ch === "g" ? 3 : (ch === "x" || ch === "1") ? 1 : 0)
|
||||||
: Array.from({ length: bpb * sub }, (_, i) => ((i % sub) === 0 ? 2 : 1));
|
: Array.from({ length: bpb * sub }, (_, i) => ((i % sub) === 0 ? 2 : 1));
|
||||||
if (!DRUMS[sound]) sound = "beep";
|
if (!DRUMS[sound]) sound = "beep";
|
||||||
return { groupsStr, stepsPerBeat: sub, sound, beatsOn, poly, swing, enabled: !disabled };
|
return { groupsStr, stepsPerBeat: sub, sound, beatsOn, poly, swing, enabled: !disabled };
|
||||||
|
|
@ -1189,7 +1196,7 @@ const SEED_SETLISTS = [
|
||||||
["Four-on-the-floor", "t120;kick:4;snare:4=.x.x;hatClosed:4/2"],
|
["Four-on-the-floor", "t120;kick:4;snare:4=.x.x;hatClosed:4/2"],
|
||||||
["Swing ride", "t150;ride:4/2s;kick:4=X..x;snare:4=.x.x"],
|
["Swing ride", "t150;ride:4/2s;kick:4=X..x;snare:4=.x.x"],
|
||||||
// Purdie half-time shuffle: triplet grid, backbeat on 3, snare ghosts (normal) around it
|
// Purdie half-time shuffle: triplet grid, backbeat on 3, snare ghosts (normal) around it
|
||||||
["Purdie half-time shuffle", "t92;kick:4/3=X....x...x..;snare:4/3=..xx.xX.xx.x;hatClosed:4/3=X.xX.xX.xX.x"],
|
["Purdie half-time shuffle", "t92;kick:4/3=X....x...x..;snare:4/3=..gg.gX.gg.g;hatClosed:4/3=X.xX.xX.xX.x"],
|
||||||
// Samba in 2/4 (16ths): surdo strong on beat 2, steady ganzá, tamborim teleco-teco
|
// Samba in 2/4 (16ths): surdo strong on beat 2, steady ganzá, tamborim teleco-teco
|
||||||
["Samba (2/4)", "t104;tomLow:2/4=x...X...;hatClosed:2/4;woodblock:2/4=X.xx.xX."],
|
["Samba (2/4)", "t104;tomLow:2/4=x...X...;hatClosed:2/4;woodblock:2/4=X.xx.xX."],
|
||||||
// Nañigo / 6/8 bembé bell over a 12/8 grid, low drum on the two main pulses
|
// Nañigo / 6/8 bembé bell over a 12/8 grid, low drum on the two main pulses
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue