diff --git a/README.md b/README.md
index a8feec4..9e0ac30 100644
--- a/README.md
+++ b/README.md
@@ -58,10 +58,14 @@ Tokens are joined with `;`. `tr` and `rmp` are omitted when off.
`jamblock` (unknown → `beep`).
- **grouping** — beats per bar, optionally grouped for odd meters: `4`, `3`,
`2+2+3`. The first beat of each group is accented.
-- **`/sub`** — subdivision (clicks per beat): `1` quarter (default), `2` eighth,
- `3` triplet, `4` sixteenth, `6` sextuplet. Omit for quarter.
-- **`=pattern`** — per‑beat on/off as `x`/`.`, length = beats per bar. Omit = all on.
- e.g. `=.x.x` puts a backbeat on 2 & 4.
+- **`/sub`** — subdivision: `1` quarter (default), `2` eighth, `3` triplet,
+ `4` sixteenth, `6` sextuplet. This also sets how many **pads** each beat splits
+ into (a beat becomes `sub` individually‑toggleable steps). Omit for quarter.
+- **`=pattern`** — per‑**step** on/off as `x`/`.`, length = beats per bar × `sub`
+ (one char per pad). Omit = all on. e.g. `4=.x.x` is a backbeat on 2 & 4;
+ `4/4=x..x..x.x...x...` is a sixteenth‑grid pattern. A short pattern whose length
+ equals just the beat count is still accepted and expanded across each beat's
+ subdivisions (back‑compat).
- **`~`** — polyrhythm: fit this lane's beats evenly into **lane 1's** bar.
- **`!`** — mute the lane.
diff --git a/index.html b/index.html
index 8a254bc..31a5d02 100644
--- a/index.html
+++ b/index.html
@@ -83,6 +83,8 @@
.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.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 */
.led.groupstart { margin-left:16px; }
.led.groupstart::before { content:""; position:absolute; left:-9px; top:4px; bottom:4px; width:2px; background:var(--muted); }
/* meter lanes — compact single-row controls + strip */
@@ -96,6 +98,7 @@
.bar { font-family:"Courier New",monospace; font-size:12px; color:var(--hot); min-width:46px; text-align:right; }
.cmp { background:var(--panel); color:var(--txt); border:1px solid var(--edge); border-radius:8px; padding:7px 8px; font-size:12px; }
.meter-card .led { width:26px; height:26px; border-radius:6px; }
+ .meter-card .led.sub { width:17px; height:17px; }
.x { background:transparent; border:1px solid var(--edge); color:var(--muted); padding:4px 9px; border-radius:7px; margin-left:auto; }
.x:hover { color:#ff9a8a; border-color:#c0392b; }
.hint { font-size:11px; color:var(--muted); margin-top:8px; }
@@ -166,6 +169,10 @@
.ext-banner { font-size:11px; color:#3a2f10; background:#ffe2a8; border:1px solid #d9a441; border-radius:8px; padding:8px 10px; margin-top:10px; line-height:1.35; }
.ext-banner[hidden] { display:none; }
#shareUrl { width:100%; resize:vertical; background:var(--panel); color:var(--txt); border:1px solid var(--edge); border-radius:8px; padding:8px; font-family:"Courier New",monospace; font-size:12px; }
+ .help-about { margin-top:14px; padding-top:12px; border-top:1px solid var(--edge); font-size:12px; color:var(--muted); line-height:1.45; }
+ .help-about p { margin:0 0 8px; }
+ .help-about p:last-child { margin-bottom:0; }
+ .help-about a { color:#6cb6ff; }
.kbd-table { width:100%; border-collapse:collapse; font-size:13px; }
.kbd-table td { padding:6px 4px; border-bottom:1px solid var(--edge); }
.kbd-table tr:last-child td { border-bottom:none; }
@@ -254,7 +261,7 @@
Meter lanes
- Click a beat pad to toggle it (rest) — e.g. snare on 2 & 4
+ Each beat splits into subdivision pads — click any pad to toggle it (rest). e.g. snare on 2 & 4
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.
+
@@ -473,9 +484,9 @@ function scheduleMeterTick(m, time) {
const tickInBar = ((m.tick % barLen) + barLen) % barLen;
const onBeat = (tickInBar % spb) === 0;
const beatIndex = Math.floor(tickInBar / spb);
- if (onBeat) m.vq.push({ time, beat: beatIndex, bar: Math.floor(m.tick / barLen) }); // playhead + 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.beatsOn[beatIndex]) return; // beat toggled off → rest (incl. its subdivisions)
+ if (!m.beatsOn[tickInBar]) return; // this step toggled off → rest
if (onBeat) {
const groupStart = m.groupStarts.has(beatIndex);
playInstrument(m.sound, time, groupStart ? 1.0 : 0.7); // beat — louder on group start
@@ -511,7 +522,7 @@ function start() {
state.running = true;
if (ramp.on) setBpm(ramp.startBpm); // ramp begins from its start BPM
const t0 = audioCtx.currentTime + 0.08;
- for (const m of meters) { m.tick = 0; m.nextTime = t0; m.vq = []; m.vqPtr = 0; m.currentBeat = -1; m.currentBar = 0; }
+ for (const m of meters) { m.tick = 0; m.nextTime = t0; m.vq = []; m.vqPtr = 0; m.currentStep = -1; m.currentBar = 0; }
masterBeat = 0; masterBeatTime = t0; muteWindows = [];
schedulerTimer = setInterval(scheduler, LOOKAHEAD_MS);
scheduler(); syncStartBtn();
@@ -519,7 +530,7 @@ function start() {
function stop() {
state.running = false;
clearInterval(schedulerTimer); schedulerTimer = null;
- for (const m of meters) m.currentBeat = -1;
+ for (const m of meters) m.currentStep = -1;
syncStartBtn();
}
function setBpm(v) {
@@ -538,10 +549,15 @@ function addMeter(groupsStr = "4", stepsPerBeat = 4, sound = "beep", beatsOn = n
const m = {
id, groupsStr, groups: p.groups, beatsPerBar: p.beatsPerBar, groupStarts: p.groupStarts,
stepsPerBeat, sound, enabled: true, poly: !!poly, color: laneColor(id),
- beatsOn: beatsOn ? beatsOn.slice() : [], // per-beat on/off mask (rests)
- tick: 0, nextTime: 0, vq: [], vqPtr: 0, currentBeat: -1, currentBar: 0,
+ beatsOn: beatsOn ? beatsOn.slice() : [], // per-STEP on/off mask (one entry per pad = beats × subdivision)
+ tick: 0, nextTime: 0, vq: [], vqPtr: 0, currentStep: -1, currentBar: 0,
el: null, stripEl: null, barEl: null,
};
+ // Tell recomputeLane the resolution the incoming mask was authored at, so it can
+ // remap/expand it: matches steps → per-step (new), matches beats → legacy per-beat.
+ if (m.beatsOn.length === p.beatsPerBar * stepsPerBeat) { m._maskBpb = p.beatsPerBar; m._maskSpb = stepsPerBeat; }
+ else if (m.beatsOn.length === p.beatsPerBar) { m._maskBpb = p.beatsPerBar; m._maskSpb = 1; }
+ else { m._maskBpb = 0; m._maskSpb = 1; } // empty/unknown → recompute fills all-on
if (state.running) { m.nextTime = audioCtx.currentTime + 0.05; }
meters.push(m);
buildLaneCard(m);
@@ -575,12 +591,7 @@ function buildLaneCard(m) {
-
-