diff --git a/README.md b/README.md
index 40e8186..ca23a15 100644
--- a/README.md
+++ b/README.md
@@ -66,15 +66,17 @@ Tokens are joined with `;`. `tr` and `rmp` are omitted when off.
`tomLow`, `tomMid`, `tomHigh`, `tambourine`, `cowbell`, `woodblock`, `claves`,
`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.
+ `2+2+3`. Groups get a visual divider; accents are per‑step (see `=pattern`).
- **`/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).
+ 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
+ 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
+ short beat‑count patterns still parse.)
- **`~`** — polyrhythm: fit this lane's beats evenly into **lane 1's** bar.
- **`!`** — mute the lane.
@@ -83,8 +85,9 @@ Tokens are joined with `;`. `tr` and `rmp` are omitted when off.
| Patch / lane | What it is |
|---|---|
| `kick:4` | kick on 4 quarter beats |
-| `snare:4=.x.x` | snare backbeat (2 & 4) |
-| `hatClosed:4/2` | eighth‑note hi‑hats |
+| `snare:4=.X.X` | accented snare backbeat (2 & 4) |
+| `hatClosed:4/2` | eighth‑note hi‑hats (downbeat of each beat accented) |
+| `ride:4/2s` | **swung** eighth‑note ride |
| `claves:5~` | 5 evenly across lane 1's bar (5‑over‑4 if lane 1 is `4`) |
| `kick:2+2+3=x..x..x` | 7/8, kick on each group start |
| `cowbell:3+2/2` | 5/4 grouped 3+2, eighth subdivision |
diff --git a/index.html b/index.html
index d155a8c..2891715 100644
--- a/index.html
+++ b/index.html
@@ -294,7 +294,7 @@
Meter lanes
- Each beat splits into subdivision pads — click any pad to toggle it (rest). e.g. snare on 2 & 4
+ Each beat splits into subdivision pads — click a pad to cycle accent → normal → mute. Pick a swing subdivision for a triplet feel.
@@ -523,24 +523,24 @@ function scheduleMeterTick(m, time) {
const spb = m.stepsPerBeat;
const barLen = m.beatsPerBar * spb;
const tickInBar = ((m.tick % barLen) + barLen) % barLen;
- const onBeat = (tickInBar % spb) === 0;
- const beatIndex = Math.floor(tickInBar / spb);
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[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
- } else {
- playInstrument(m.sound, time, 0.4); // subdivision — same voice, softer
- }
+ const lvl = m.beatsOn[tickInBar] | 0; // per-step dynamics: 0 = rest, 1 = normal, 2 = accent
+ if (!lvl) return;
+ playInstrument(m.sound, time, lvl === 2 ? 1.0 : 0.6);
}
// Reference bar = lane 1's bar; poly lanes fit their beats evenly into it.
function refBarDur() { return (meters.length ? meters[0].beatsPerBar : 4) * (60 / state.bpm); }
-function laneStepDur(m) {
- if (m.poly) return refBarDur() / (m.beatsPerBar * m.stepsPerBeat); // true ratio polyrhythm
- return (60 / state.bpm) / m.stepsPerBeat; // normal: shared quarter-note grid
+const SWING_RATIO = 2 / 3; // triplet swing: the off-beat lands on the last triplet
+function laneStepDur(m, tick) {
+ if (m.poly) return refBarDur() / (m.beatsPerBar * m.stepsPerBeat); // true ratio polyrhythm (no swing)
+ const beat = 60 / state.bpm;
+ if (m.swing && m.stepsPerBeat % 2 === 0) { // swing even subdivisions (8ths, 16ths): long–short pairs
+ const pairDur = beat / (m.stepsPerBeat / 2);
+ return ((tick % m.stepsPerBeat) % 2) === 0 ? SWING_RATIO * pairDur : (1 - SWING_RATIO) * pairDur;
+ }
+ return beat / m.stepsPerBeat; // straight: shared even grid
}
function scheduler() {
@@ -552,8 +552,8 @@ function scheduler() {
for (const m of meters) {
while (m.nextTime < cap) {
scheduleMeterTick(m, m.nextTime);
+ m.nextTime += laneStepDur(m, m.tick); // duration of the step just scheduled (swing makes pairs uneven)
m.tick++;
- m.nextTime += laneStepDur(m);
}
}
// Boundary reached → swap to the new segment seamlessly (scheduler keeps running).
@@ -624,13 +624,13 @@ function setBpm(v) {
========================================================================= */
function laneColor(id) { return `hsl(${(id * 67) % 360} 70% 62%)`; }
-function addMeter(groupsStr = "4", stepsPerBeat = 4, sound = "beep", beatsOn = null, poly = false) {
+function addMeter(groupsStr = "4", stepsPerBeat = 4, sound = "beep", beatsOn = null, poly = false, swing = false) {
const id = ++meterSeq;
const p = parseGroups(groupsStr);
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-STEP on/off mask (one entry per pad = beats × subdivision)
+ stepsPerBeat, sound, enabled: true, poly: !!poly, swing: !!swing, color: laneColor(id),
+ beatsOn: beatsOn ? beatsOn.slice() : [], // per-STEP dynamics mask (one entry per pad: 0 mute / 1 normal / 2 accent)
tick: 0, nextTime: 0, vq: [], vqPtr: 0, currentStep: -1, currentBar: 0,
el: null, stripEl: null, barEl: null,
};
@@ -672,9 +672,10 @@ function buildLaneCard(m) {
-