PM_K-1 0.0.8: built-in playlists (baked, read-only) vs user playlists (separate)

The standard editor defaults (Styles / Practice / Song) are baked into app.py as
BUILTIN_SETLISTS (ASCII-fied — emoji/accents would break the 7-bit push + the fonts),
so they update with firmware and the user can't change or delete them. User playlists
live separately in programs.json and are merged after the built-ins.

Device:
- Set-list model: self.setlists = built-ins + user lists (deduped by normalized title,
  so a baked built-in always wins). load()/goto() work within the current list.
- Navigation: a set-list "tab" (small, above the title) shows playlist + position,
  muted for built-in / cyan for user; TAP it to switch playlists. Joystick L/R = item.
- SysEx 0x10 (push) writes programs.json -> rebuild user lists; built-ins untouched.
- Shipped programs.json is now empty ({"setlists":[]}) — built-ins come from firmware.

Editor:
- "Save to device" now syncs only YOUR set lists (filters out the built-in seeds) in the
  new {setlists:[...]} format; warns if you have none. Load-from-device imports both the
  new multi-list and old flat formats.

Verified in the harness: 3 read-only built-ins, set-list switching, user-list merge +
dedup of a pushed "styles", and the ramp engine on a built-in track (80->84->88, +4/4 bars).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Me Here 2026-05-29 12:29:09 -05:00
parent 88104e3d5c
commit 2d243c9ef8
4 changed files with 116 additions and 136 deletions

View file

@ -347,7 +347,7 @@
<button id="exportBtn">⭳ Export all (file)</button> <button id="exportBtn">⭳ Export all (file)</button>
<button id="importBtn">⭱ Import file…</button> <button id="importBtn">⭱ Import file…</button>
<input type="file" id="importFile" accept="application/json" style="display:none"> <input type="file" id="importFile" accept="application/json" style="display:none">
<button id="saveDeviceBtn" title="Write this set list to a PM_K-1 device drive (programs.json)">📟 Save to device</button> <button id="saveDeviceBtn" title="Sync your own set lists to the PM_K-1 (the built-in playlists are baked into its firmware)">📟 Save to device</button>
<button id="loadDeviceBtn" title="Load a device's programs.json into a new set list">📥 Load from device</button> <button id="loadDeviceBtn" title="Load a device's programs.json into a new set list">📥 Load from device</button>
<button id="updateFwBtn" title="Check &amp; update the PM_K-1 firmware over USB-MIDI (A/B with auto-rollback)">⬆ Update firmware…</button> <button id="updateFwBtn" title="Check &amp; update the PM_K-1 firmware over USB-MIDI (A/B with auto-rollback)">⬆ Update firmware…</button>
<button id="clearLogBtn">🗑 Clear log</button> <button id="clearLogBtn">🗑 Clear log</button>
@ -1075,9 +1075,16 @@ function shareSetlist() {
/* Device (PM_K-1) programs.json — the same grooves the firmware reads. /* Device (PM_K-1) programs.json — the same grooves the firmware reads.
Save: writes the active set list straight onto the CIRCUITPY drive (File System Access, Save: writes the active set list straight onto the CIRCUITPY drive (File System Access,
Chrome/Edge) or downloads it to drag on. Load: reads a programs.json into a new set list. */ Chrome/Edge) or downloads it to drag on. Load: reads a programs.json into a new set list. */
// The PM_K-1's built-in playlists are baked into its firmware (they update with firmware and are
// read-only). "Save to device" syncs only YOUR set lists — i.e. the ones that aren't the built-in demos.
function userSetlists() {
const seed = new Set(SEED_SETLISTS.map((s) => s.title));
return setlists.filter((sl) => !seed.has(sl.title));
}
function programsJSON() { function programsJSON() {
const sl = getSL(); if (!sl) return null; return JSON.stringify({ setlists: userSetlists().map((sl) => ({
return JSON.stringify({ title: sl.title || "PolyMeter", programs: sl.items.map((it) => ({ name: it.name, prog: setupToPatch(it) })) }, null, 2); title: sl.title || "My set list",
programs: sl.items.map((it) => ({ name: it.name, prog: setupToPatch(it) })) })) }, null, 2);
} }
function _downloadPrograms(json) { function _downloadPrograms(json) {
const a = document.createElement("a"); const a = document.createElement("a");
@ -1085,7 +1092,9 @@ function _downloadPrograms(json) {
a.download = "programs.json"; a.click(); URL.revokeObjectURL(a.href); a.download = "programs.json"; a.click(); URL.revokeObjectURL(a.href);
} }
async function saveToDevice() { async function saveToDevice() {
const json = programsJSON(); if (!json) return alert("No set list selected to save."); const uls = userSetlists();
if (!uls.length) return alert("No custom set lists to save.\n\nThe built-in playlists (Styles / Practice / Song) are baked into the device firmware — they're always there, update with firmware, and can't be changed. Create your own set list and it'll save here.");
const json = programsJSON();
// Primary: push to the device over USB-MIDI SysEx (Chromium/Firefox); the firmware writes programs.json. // Primary: push to the device over USB-MIDI SysEx (Chromium/Firefox); the firmware writes programs.json.
if (await _ensureMidi() && _midiOutputs().length) { if (await _ensureMidi() && _midiOutputs().length) {
const ascii = json.replace(/[\u0080-\uFFFF]/g, (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0")); // 7-bit-safe JSON const ascii = json.replace(/[\u0080-\uFFFF]/g, (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0")); // 7-bit-safe JSON
@ -1094,7 +1103,7 @@ async function saveToDevice() {
bytes.push(0xF7); bytes.push(0xF7);
_send(_clockSysex()); _send(bytes); _send(_clockSysex()); _send(bytes);
const ok = await new Promise((res) => { _saveCb = res; setTimeout(() => { if (_saveCb) { _saveCb = null; res(null); } }, 2500); }); const ok = await new Promise((res) => { _saveCb = res; setTimeout(() => { if (_saveCb) { _saveCb = null; res(null); } }, 2500); });
if (ok === true) { alert("Saved to device ✓ — it reloaded with your set list."); return; } if (ok === true) { alert("Saved to device ✓ — " + uls.length + " set list(s) synced (alongside the built-ins)."); return; }
_downloadPrograms(json); _downloadPrograms(json);
alert(ok === false alert(ok === false
? "The device is in editor mode (drive writable by the computer), so I downloaded programs.json — drag it onto the CIRCUITPY drive." ? "The device is in editor mode (drive writable by the computer), so I downloaded programs.json — drag it onto the CIRCUITPY drive."
@ -1108,11 +1117,17 @@ async function saveToDevice() {
function importPrograms(text) { function importPrograms(text) {
try { try {
const d = JSON.parse(text); const d = JSON.parse(text);
const items = (d.programs || []).map((p) => ({ name: p.name || "Item", ...patchToSetup(p.prog) })); const lists = Array.isArray(d.setlists) ? d.setlists : [{ title: d.title, programs: d.programs || [] }];
if (!items.length) return alert("No programs found in that file."); let added = 0;
setlists.push({ title: d.title || "Device", description: "", items }); activeSL = setlists.length - 1; activeItem = 0; for (const sl of lists) {
saveSetlists(); renderSetlists(); applySetup(items[0]); const items = (sl.programs || []).map((p) => ({ name: p.name || "Item", ...patchToSetup(p.prog) }));
alert("Loaded " + items.length + " grooves from the device into a new set list."); if (!items.length) continue;
setlists.push({ title: sl.title || "Device", description: "", items }); added++;
}
if (!added) return alert("No programs found in that file.");
activeSL = setlists.length - 1; activeItem = 0;
saveSetlists(); renderSetlists(); applySetup(setlists[activeSL].items[0]);
alert("Loaded " + added + " set list(s) from the device.");
} catch (e) { alert("Load failed: " + e.message); } } catch (e) { alert("Load failed: " + e.message); }
} }
async function loadFromDevice() { async function loadFromDevice() {

View file

@ -18,7 +18,7 @@
import board, busio, digitalio, analogio, pwmio, displayio, vectorio, time, json, gc, os, supervisor import board, busio, digitalio, analogio, pwmio, displayio, vectorio, time, json, gc, os, supervisor
supervisor.runtime.autoreload = False # we write our own files (log + pushed programs); never self-restart supervisor.runtime.autoreload = False # we write our own files (log + pushed programs); never self-restart
APP_VERSION = "0.0.7" # firmware version (the A/B updater pushes/compares this) APP_VERSION = "0.0.8" # firmware version (the A/B updater pushes/compares this)
try: try:
import rtc # set from the editor's clock SysEx so the log has real timestamps import rtc # set from the editor's clock SysEx so the log has real timestamps
except ImportError: except ImportError:
@ -62,13 +62,39 @@ P_SDA, P_SCL = board.GP8, board.GP9
P_RGB, P_BUZ, P_BTNA, P_BTNB = board.GP12, board.GP13, board.GP15, board.GP14 P_RGB, P_BUZ, P_BTNA, P_BTNB = board.GP12, board.GP13, board.GP15, board.GP14
P_JOYX, P_JOYY = board.GP26, board.GP27 P_JOYX, P_JOYY = board.GP26, board.GP27
# ----- baked default grooves (used only if programs.json is missing/bad) ----- # ----- BUILT-IN playlists: the standard defaults from the web editor, baked in here so they update with
DEFAULT_PROGRAMS = [ # firmware and the user can't change/delete them. User playlists live separately in programs.json
("Four on the floor", "t120;kick:4;snare:4=.x.x;hatClosed:4/2"), # (pushed from the editor) and never touch these. (ASCII only - it's pushed 7-bit + the fonts are ASCII.)
("Swing ride", "t150;ride:4/2s;kick:4=X..x;snare:4=.x.x"), BUILTIN_SETLISTS = [
("7/8 (2+2+3)", "t130;kick:2+2+3=x..x..x;hatClosed:2+2+3/2"), ("Styles", [
("5 over 4", "t100;kick:4;claves:5~"), ("Four-on-the-floor", "t120;kick:4;snare:4=.x.x;hatClosed:4/2"),
("Straight click", "t120;beep:4"), ("Swing ride", "t150;ride:4/2s;kick:4=X..x;snare:4=.x.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 (2/4)", "t104;tomLow:2/4=x...X...;hatClosed:2/4;woodblock:2/4=X.xx.xX."),
("Nanigo (6/8 bembe)", "t130;cowbell:4/3=X.xx.x.xx.x.;kick:4/3=X.....X.....;hatClosed:4/3=..x..x..x..x"),
("6/8 groove", "t100;kick:3+3=x..x..;snare:3+3=...x..;hatClosed:3+3/2"),
("7/8 (2+2+3)", "t130;kick:2+2+3=x..x..x;hatClosed:2+2+3/2"),
("5/4 (3+2)", "t112;kick:3+2=x..x.;snare:3+2=..x..;hatClosed:3+2/2"),
]),
("Practice", [
("5 over 4 polyrhythm", "t100;kick:4;claves:5~"),
("3 over 2 hemiola", "t96;woodblock:2;cowbell:3~"),
("2 & 4 & 3 over one bar", "t100;kick:3;cowbell:2~;claves:4~"),
("Triplet hats", "t100;kick:4;snare:4=.x.x;hatClosed:4/3"),
("Accents - cycle the pads", "t92;kick:4=X..X;snare:4=.X.X;hatClosed:4/2"),
("Tempo builder 80 up", "t80;woodblock:4;rmp80/4/4"),
("Gap trainer (play 2 / rest 2)", "t100;kick:4;hatClosed:4/2;tr2/2"),
]),
("Song (continuous)", [
("Intro - hats & kick", "t88;b8;kick:4=X.x.;hatClosed:4/2=gggggggg"),
("Groove in - backbeat", "t88;b16;kick:4=X.x.;snare:4=.X.X;hatClosed:4/2"),
("Half-time shuffle", "t92;b12;kick:4/3=X....x...x..;snare:4/3=..gg.gX.gg.g;hatClosed:4/3=X.xX.xX.xX.x"),
("Build - ramp 92-120", "t92;b16;rmp92/4/2;kick:4;snare:4=.X.X;hatClosed:4/2"),
("Four-on-the-floor (909)", "t124;b18;kick909:4;clap909:4=.X.X;hat909:4/2=.X.X.X.X"),
("Samba break (2/4)", "t116;b24;tomLow:2/4=x...X...;hatClosed:2/4;woodblock:2/4=X.xx.xX."),
("Peak - 16ths", "t132;b16;kick:4=X..x;snare:4=.X.X;hatClosed:4/4"),
("Outro - ramp down", "t132;b8;rmp132/-7/1;kick:4=X..x;hatClosed:4/2=gggggggg"),
]),
] ]
# ============================== COLOURS (0xRRGGBB; displayio handles 565) ============================== # ============================== COLOURS (0xRRGGBB; displayio handles 565) ==============================
@ -87,7 +113,7 @@ SOUND_GM = {"kick":36,"kick808":36,"kick909":36, "snare":38,"snare808":38,"snare
GM_DEFAULT = 37 GM_DEFAULT = 37
MIDI_VEL = {2: 120, 1: 90, 3: 45} # accent / normal / ghost MIDI_VEL = {2: 120, 1: 90, 3: 45} # accent / normal / ghost
MAXLANES = 5 # lanes shown on the pad grid (extras still play) MAXLANES = 5 # lanes shown on the pad grid (extras still play)
GRID_TOP = 158 # top of the pad grid (leaves room for time/bar/ramp rows) GRID_TOP = 158 # top of the pad grid (leaves room for time/bar/ramp/tab rows)
LOG_TOP, LOG_ROWH, LOG_ROWS = 302, 16, 9 # practice-history log area (below the pad grid) LOG_TOP, LOG_ROWH, LOG_ROWS = 302, 16, 9 # practice-history log area (below the pad grid)
MIN_LOG_SEC = 5 # don't log plays shorter than this MIN_LOG_SEC = 5 # don't log plays shorter than this
PAD_DIM = (0x10161E, 0x0A3A52, 0x4A3010, 0x2A1D4A) # idle pad: mute / normal / accent / ghost PAD_DIM = (0x10161E, 0x0A3A52, 0x4A3010, 0x2A1D4A) # idle pad: mute / normal / accent / ghost
@ -249,15 +275,28 @@ def _parse_lane(tok):
else: levels.append(0) else: levels.append(0)
return {'sound': sound, 'sub': sub, 'swing': swing, 'steps': steps, 'levels': levels, 'poly': poly, 'mute': mute} return {'sound': sound, 'sub': sub, 'swing': swing, 'steps': steps, 'levels': levels, 'poly': poly, 'mute': mute}
def load_programs(): def _slkey(t): # normalise a title for built-in/user de-duplication
return "".join(c.lower() for c in t if c.isalnum())
def load_user_setlists():
# User playlists from /programs.json (pushed by the editor). New {setlists:[{title,programs:[..]}]} form,
# or the old flat {programs:[..]} (one list). Built-ins are baked in BUILTIN_SETLISTS, never here.
try: try:
with open("/programs.json") as f: with open("/programs.json") as f: d = json.load(f)
d = json.load(f)
progs = [(p["name"], p["prog"]) for p in d["programs"]]
if progs: return progs
except Exception as e: except Exception as e:
print("programs.json:", e) print("programs.json:", e); return []
return DEFAULT_PROGRAMS def items_of(pl): return [(p.get("name", "?"), p.get("prog", "")) for p in pl if p.get("prog")]
out = []
try:
if isinstance(d.get("setlists"), list):
for sl in d["setlists"]:
it = items_of(sl.get("programs", []))
if it: out.append((sl.get("title", "My set list"), it))
elif isinstance(d.get("programs"), list):
it = items_of(d["programs"])
if it: out.append((d.get("title", "My set list"), it))
except Exception as e:
print("setlists:", e)
return out
# ============================== GT911 TOUCH ============================== # ============================== GT911 TOUCH ==============================
class GT911: class GT911:
@ -355,7 +394,7 @@ class App:
self._touchDown = False; self._touchSeen = 0 self._touchDown = False; self._touchSeen = 0
self.running = False; self.bpm = 120; self.idx = 0; self.lanes = []; self.bars = 0; self.rgb = (0, 0, 0) self.running = False; self.bpm = 120; self.idx = 0; self.lanes = []; self.bars = 0; self.rgb = (0, 0, 0)
self.ramp = None; self.trainer = None; self._lastbar = -1; self._muted = False; self._ramp_base = 120 self.ramp = None; self.trainer = None; self._lastbar = -1; self._muted = False; self._ramp_base = 120
self.programs = load_programs() self.sl = 0; self.rebuild_setlists() # built-in playlists (baked) + user playlists (programs.json)
self.dirty = True self.dirty = True
self.pad_pal = displayio.Palette(8) # 0-3 idle levels (mute/normal/accent/ghost), 4-7 the lit playhead self.pad_pal = displayio.Palette(8) # 0-3 idle levels (mute/normal/accent/ghost), 4-7 the lit playhead
for i in range(4): self.pad_pal[i] = PAD_DIM[i]; self.pad_pal[i + 4] = PAD_LIT[i] for i in range(4): self.pad_pal[i] = PAD_DIM[i]; self.pad_pal[i + 4] = PAD_LIT[i]
@ -418,9 +457,26 @@ class App:
self.dirty = True self.dirty = True
# ---------- program ---------- # ---------- program ----------
def rebuild_setlists(self):
# built-in playlists first (read-only), then user playlists from programs.json (a baked title always wins)
self.setlists = [{'title': t, 'items': it, 'builtin': True} for t, it in BUILTIN_SETLISTS]
seen = set(_slkey(t) for t, _ in BUILTIN_SETLISTS)
for t, it in load_user_setlists():
if _slkey(t) in seen: continue
seen.add(_slkey(t)); self.setlists.append({'title': t, 'items': it, 'builtin': False})
if self.sl >= len(self.setlists): self.sl = 0
def switch_setlist(self, delta=1):
if len(self.setlists) < 2: return
was = self.running
if was: self.running = False; self._log_play()
self.sl = (self.sl + delta) % len(self.setlists)
self.load(0)
if was: self.running = True; self._reset_clock(); self._start_play()
self.led_rest(); self.draw_meters()
def load(self, i): def load(self, i):
n = len(self.programs); self.idx = i % n items = self.setlists[self.sl]['items']
self.name, prog = self.programs[self.idx] self.idx = i % len(items)
self.name, prog = items[self.idx]
self.bpm, self.lanes, self.bars, self.ramp, self.trainer = parse_program(prog) self.bpm, self.lanes, self.bars, self.ramp, self.trainer = parse_program(prog)
self.master = self.lanes[0]; self._ramp_base = self.bpm; self._lastbar = -1; self._muted = False self.master = self.lanes[0]; self._ramp_base = self.bpm; self._lastbar = -1; self._muted = False
self._reset_clock(); self.draw_bpm(); self.draw_status(); self.draw_train() self._reset_clock(); self.draw_bpm(); self.draw_status(); self.draw_train()
@ -549,7 +605,9 @@ class App:
if pt: if pt:
self._touchSeen = nowms self._touchSeen = nowms
if not self._touchDown: if not self._touchDown:
self._touchDown = True; self._tap_log(pt[0], pt[1]) self._touchDown = True
if 112 <= pt[1] <= 154: self.switch_setlist(1) # tap the set-list tab/title -> next playlist
else: self._tap_log(pt[0], pt[1])
elif self._touchDown and (nowms - self._touchSeen) > 0.14: elif self._touchDown and (nowms - self._touchSeen) > 0.14:
self._touchDown = False self._touchDown = False
# USB-MIDI in: any byte = a host is listening (heartbeat); also assemble SysEx (clock / pushed programs) # USB-MIDI in: any byte = a host is listening (heartbeat); also assemble SysEx (clock / pushed programs)
@ -571,14 +629,16 @@ class App:
# ---------- drawing ---------- # ---------- drawing ----------
def draw_bpm(self): def draw_bpm(self):
self._place(self.g_bpm, str(self.bpm), 0, 44, C_TXT, C_BG, FONT_L, right_edge=WIDTH-12) self._place(self.g_bpm, str(self.bpm), 0, 44, C_TXT, C_BG, FONT_L, right_edge=WIDTH-12)
def draw_status(self): # title (bright) + track number set apart (dim, right) def draw_status(self): # set-list tab (small, tap to switch) above the item title
self._place(self.g_name, self.name[:22], 12, 130, C_TXT, C_BG, FONT_M) sl = self.setlists[self.sl]
self._place(self.g_idx, "%d/%d" % (self.idx + 1, len(self.programs)), 0, 134, # tab: playlist + position; muted = built-in (read-only), cyan = your own
C_DIM, C_BG, FONT_S, right_edge=WIDTH - 12) self._place(self.g_idx, "%s %d/%d" % (sl['title'][:14], self.idx + 1, len(sl['items'])),
12, 118, C_MUTE if sl['builtin'] else C_CYAN, C_BG, FONT_S)
self._place(self.g_name, self.name[:22], 12, 134, C_TXT, C_BG, FONT_M)
def draw_train(self): # ramp + gap-trainer indicators (symbol + params), when set def draw_train(self): # ramp + gap-trainer indicators (symbol + params), when set
g = self.g_train g = self.g_train
while len(g): g.pop() while len(g): g.pop()
x = 12; y = 104 x = 12; y = 100
if self.ramp: if self.ramp:
up = self.ramp['amt'] >= 0 up = self.ramp['amt'] >= 0
pts = [(0, 9), (12, 9), (12, 0)] if up else [(0, 0), (0, 9), (12, 9)] # rising / falling ramp pts = [(0, 9), (12, 9), (12, 0)] if up else [(0, 0), (0, 9), (12, 9)] # rising / falling ramp
@ -739,14 +799,13 @@ class App:
except Exception: pass except Exception: pass
elif cmd == 0x02: # version query -> reply 0x03 + APP_VERSION elif cmd == 0x02: # version query -> reply 0x03 + APP_VERSION
if self.midi: self.midi.write(bytes([0xF0, 0x7D, 0x03]) + APP_VERSION.encode() + bytes([0xF7])) if self.midi: self.midi.write(bytes([0xF0, 0x7D, 0x03]) + APP_VERSION.encode() + bytes([0xF7]))
elif cmd == 0x10: # write /programs.json pushed from the editor, then reload elif cmd == 0x10: # write /programs.json (user playlists) pushed from the editor
try: try:
with open("/programs.json", "wb") as f: f.write(bytes(sx[2:])) with open("/programs.json", "wb") as f: f.write(bytes(sx[2:]))
self.programs = load_programs(); self.idx = min(self.idx, len(self.programs) - 1) self.rebuild_setlists(); self.load(0) # built-ins untouched; show the refreshed lists
self.load(self.idx) self._ack(True)
if self.midi: self.midi.write(bytes([0xF0, 0x7D, 0x7F, 0xF7])) # ACK ok except Exception:
except OSError: self._ack(False) # read-only (editor mode) etc.
if self.midi: self.midi.write(bytes([0xF0, 0x7D, 0x7E, 0xF7])) # NAK: read-only (editor mode)
# A/B firmware update, sent as small flow-controlled chunks (a single huge SysEx overruns the # A/B firmware update, sent as small flow-controlled chunks (a single huge SysEx overruns the
# USB-MIDI input buffer and arrives corrupt). begin(0x21,len) -> data(0x22)* -> commit(0x23). # USB-MIDI input buffer and arrives corrupt). begin(0x21,len) -> data(0x22)* -> commit(0x23).
elif cmd == 0x21: # BEGIN: open the staging file; sx[2:6] = expected length (7-bit) elif cmd == 0x21: # BEGIN: open the staging file; sx[2:6] = expected length (7-bit)

View file

@ -1,97 +1,3 @@
{ {
"title": "PolyMeter", "setlists": []
"programs": [
{
"name": "Four on the floor",
"prog": "t120;kick:4;snare:4=.x.x;hatClosed:4/2"
},
{
"name": "Swing ride",
"prog": "t150;ride:4/2s;kick:4=X..x;snare:4=.x.x"
},
{
"name": "Purdie half shuffle",
"prog": "t92;kick:4/3=X....x...x..;snare:4/3=..gg.gX.gg.g;hatClosed:4/3=X.xX.xX.xX.x"
},
{
"name": "Samba (2/4)",
"prog": "t104;tomLow:2/4=x...X...;hatClosed:2/4;woodblock:2/4=X.xx.xX."
},
{
"name": "Nanigo (6/8 bembe)",
"prog": "t130;cowbell:4/3=X.xx.x.xx.x.;kick:4/3=X.....X.....;hatClosed:4/3=..x..x..x..x"
},
{
"name": "6/8 groove",
"prog": "t100;kick:3+3=x..x..;snare:3+3=...x..;hatClosed:3+3/2"
},
{
"name": "7/8 (2+2+3)",
"prog": "t130;kick:2+2+3=x..x..x;hatClosed:2+2+3/2"
},
{
"name": "5/4 (3+2)",
"prog": "t112;kick:3+2=x..x.;snare:3+2=..x..;hatClosed:3+2/2"
},
{
"name": "5 over 4 polyrhythm",
"prog": "t100;kick:4;claves:5~"
},
{
"name": "3 over 2 hemiola",
"prog": "t96;woodblock:2;cowbell:3~"
},
{
"name": "2 & 4 & 3 per bar",
"prog": "t100;kick:3;cowbell:2~;claves:4~"
},
{
"name": "Triplet hats",
"prog": "t100;kick:4;snare:4=.x.x;hatClosed:4/3"
},
{
"name": "Accents",
"prog": "t92;kick:4=X..X;snare:4=.X.X;hatClosed:4/2"
},
{
"name": "Tempo builder 80+",
"prog": "t80;woodblock:4;rmp80/4/4"
},
{
"name": "Gap trainer 2/2",
"prog": "t100;kick:4;hatClosed:4/2;tr2/2"
},
{
"name": "Intro - hats+kick",
"prog": "t88;kick:4=X.x.;hatClosed:4/2=gggggggg"
},
{
"name": "Groove in",
"prog": "t88;kick:4=X.x.;snare:4=.X.X;hatClosed:4/2"
},
{
"name": "Half-time shuffle",
"prog": "t92;kick:4/3=X....x...x..;snare:4/3=..gg.gX.gg.g;hatClosed:4/3=X.xX.xX.xX.x"
},
{
"name": "Build 92 to 120",
"prog": "t92;kick:4;snare:4=.X.X;hatClosed:4/2"
},
{
"name": "Four-floor (909)",
"prog": "t124;kick909:4;clap909:4=.X.X;hat909:4/2=.X.X.X.X"
},
{
"name": "Samba break",
"prog": "t116;tomLow:2/4=x...X...;hatClosed:2/4;woodblock:2/4=X.xx.xX."
},
{
"name": "Peak - 16ths",
"prog": "t132;kick:4=X..x;snare:4=.X.X;hatClosed:4/4"
},
{
"name": "Outro",
"prog": "t132;kick:4=X..x;hatClosed:4/2=gggggggg"
}
]
} }