diff --git a/README.md b/README.md index ca23a15..fd4ef06 100644 --- a/README.md +++ b/README.md @@ -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 `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`** - 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 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 diff --git a/index.html b/index.html index f1f4e4c..fa72d63 100644 --- a/index.html +++ b/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.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.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.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 */ @@ -294,7 +296,7 @@

Meter lanes

- Each beat splits into subdivision pads — click a pad to cycle accent → normal → mute. Pick a swing subdivision for a triplet feel. + Each beat splits into subdivision pads — click a pad to cycle accent → normal → ghost → mute. Pick a swing subdivision for a triplet feel.
@@ -363,6 +365,7 @@

Source: git.varasys.io/VARASYS/metronome

+

Your set lists, items and practice log live only in this browser (localStorage) — nothing is uploaded. To move or share them, use the set-list menu: Share set-list link copies a link encoding the whole set list (open it elsewhere to import a copy); Share settings link shares just the loaded item; Export all / Import file back up everything as a JSON file.

This is a single-page app — save this page (Ctrl/⌘+S) and open the file to run it fully offline, no server needed. One catch when running from a local file://: it won't auto-save your set list between sessions, so export a backup (set-list menu → Export all) to keep your work.

@@ -525,9 +528,9 @@ function scheduleMeterTick(m, time) { 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) 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; - 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. @@ -706,12 +709,15 @@ function buildLaneCard(m) { recomputeLane(m); } -const stepDefault = (s) => (s === 0 ? 2 : 1); // default dynamics: first step of each beat accented, rest normal -function normLevel(v, dflt) { // coerce a stored value to a 0/1/2 level +// Per-step dynamics levels: 0 mute · 1 normal · 2 accent · 3 ghost. (Ghost is the new +// 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 === false) return 0; 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) { const p = parseGroups(m.groupsStr); @@ -745,8 +751,8 @@ function buildLaneStrip(m) { // one pad per STEP (b cell.className = "led"; cell.textContent = (s === 0) ? (b + 1) : ""; // label downbeats; subdivisions blank cell.style.cursor = "pointer"; - cell.title = "click: accent → normal → 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.title = "click: accent → normal → ghost → mute · beat " + (b + 1) + (s ? " · sub " + (s + 1) : ""); + cell.addEventListener("click", () => { m.beatsOn[i] = NEXT_LEVEL[m.beatsOn[i] | 0]; renderLaneStrip(m); }); m.stripEl.appendChild(cell); } } @@ -760,9 +766,10 @@ function renderLaneStrip(m) { let cls = "led"; if (!onBeat) cls += " sub"; // subdivision pad (smaller/dimmer) 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 (lvl === 2) cls += " accent"; // accented step (▲) + else if (lvl === 3) cls += " ghost"; // ghost note (faint ·) cell.className = cls; cell.style.setProperty("--lc", m.color); 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 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)); - 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.enabled === false) s += "!"; // "!" = silenced / disabled 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; } 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) - 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)); if (!DRUMS[sound]) sound = "beep"; 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"], ["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", "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 (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