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:
parent
88104e3d5c
commit
2d243c9ef8
4 changed files with 116 additions and 136 deletions
35
editor.html
35
editor.html
|
|
@ -347,7 +347,7 @@
|
|||
<button id="exportBtn">⭳ Export all (file)</button>
|
||||
<button id="importBtn">⭱ Import file…</button>
|
||||
<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="updateFwBtn" title="Check & update the PM_K-1 firmware over USB-MIDI (A/B with auto-rollback)">⬆ Update firmware…</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.
|
||||
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. */
|
||||
// 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() {
|
||||
const sl = getSL(); if (!sl) return null;
|
||||
return JSON.stringify({ title: sl.title || "PolyMeter", programs: sl.items.map((it) => ({ name: it.name, prog: setupToPatch(it) })) }, null, 2);
|
||||
return JSON.stringify({ setlists: userSetlists().map((sl) => ({
|
||||
title: sl.title || "My set list",
|
||||
programs: sl.items.map((it) => ({ name: it.name, prog: setupToPatch(it) })) })) }, null, 2);
|
||||
}
|
||||
function _downloadPrograms(json) {
|
||||
const a = document.createElement("a");
|
||||
|
|
@ -1085,7 +1092,9 @@ function _downloadPrograms(json) {
|
|||
a.download = "programs.json"; a.click(); URL.revokeObjectURL(a.href);
|
||||
}
|
||||
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.
|
||||
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
|
||||
|
|
@ -1094,7 +1103,7 @@ async function saveToDevice() {
|
|||
bytes.push(0xF7);
|
||||
_send(_clockSysex()); _send(bytes);
|
||||
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);
|
||||
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."
|
||||
|
|
@ -1108,11 +1117,17 @@ async function saveToDevice() {
|
|||
function importPrograms(text) {
|
||||
try {
|
||||
const d = JSON.parse(text);
|
||||
const items = (d.programs || []).map((p) => ({ name: p.name || "Item", ...patchToSetup(p.prog) }));
|
||||
if (!items.length) return alert("No programs found in that file.");
|
||||
setlists.push({ title: d.title || "Device", description: "", items }); activeSL = setlists.length - 1; activeItem = 0;
|
||||
saveSetlists(); renderSetlists(); applySetup(items[0]);
|
||||
alert("Loaded " + items.length + " grooves from the device into a new set list.");
|
||||
const lists = Array.isArray(d.setlists) ? d.setlists : [{ title: d.title, programs: d.programs || [] }];
|
||||
let added = 0;
|
||||
for (const sl of lists) {
|
||||
const items = (sl.programs || []).map((p) => ({ name: p.name || "Item", ...patchToSetup(p.prog) }));
|
||||
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); }
|
||||
}
|
||||
async function loadFromDevice() {
|
||||
|
|
|
|||
Binary file not shown.
117
pico-cp/app.py
117
pico-cp/app.py
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
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
|
||||
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:
|
||||
import rtc # set from the editor's clock SysEx so the log has real timestamps
|
||||
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_JOYX, P_JOYY = board.GP26, board.GP27
|
||||
|
||||
# ----- baked default grooves (used only if programs.json is missing/bad) -----
|
||||
DEFAULT_PROGRAMS = [
|
||||
("Four on the floor", "t120;kick:4;snare:4=.x.x;hatClosed:4/2"),
|
||||
# ----- BUILT-IN playlists: the standard defaults from the web editor, baked in here so they update with
|
||||
# firmware and the user can't change/delete them. User playlists live separately in programs.json
|
||||
# (pushed from the editor) and never touch these. (ASCII only - it's pushed 7-bit + the fonts are ASCII.)
|
||||
BUILTIN_SETLISTS = [
|
||||
("Styles", [
|
||||
("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", "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 over 4", "t100;kick:4;claves:5~"),
|
||||
("Straight click", "t120;beep:4"),
|
||||
("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) ==============================
|
||||
|
|
@ -87,7 +113,7 @@ SOUND_GM = {"kick":36,"kick808":36,"kick909":36, "snare":38,"snare808":38,"snare
|
|||
GM_DEFAULT = 37
|
||||
MIDI_VEL = {2: 120, 1: 90, 3: 45} # accent / normal / ghost
|
||||
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)
|
||||
MIN_LOG_SEC = 5 # don't log plays shorter than this
|
||||
PAD_DIM = (0x10161E, 0x0A3A52, 0x4A3010, 0x2A1D4A) # idle pad: mute / normal / accent / ghost
|
||||
|
|
@ -249,15 +275,28 @@ def _parse_lane(tok):
|
|||
else: levels.append(0)
|
||||
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:
|
||||
with open("/programs.json") as f:
|
||||
d = json.load(f)
|
||||
progs = [(p["name"], p["prog"]) for p in d["programs"]]
|
||||
if progs: return progs
|
||||
with open("/programs.json") as f: d = json.load(f)
|
||||
except Exception as e:
|
||||
print("programs.json:", e)
|
||||
return DEFAULT_PROGRAMS
|
||||
print("programs.json:", e); return []
|
||||
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 ==============================
|
||||
class GT911:
|
||||
|
|
@ -355,7 +394,7 @@ class App:
|
|||
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.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.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]
|
||||
|
|
@ -418,9 +457,26 @@ class App:
|
|||
self.dirty = True
|
||||
|
||||
# ---------- 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):
|
||||
n = len(self.programs); self.idx = i % n
|
||||
self.name, prog = self.programs[self.idx]
|
||||
items = self.setlists[self.sl]['items']
|
||||
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.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()
|
||||
|
|
@ -549,7 +605,9 @@ class App:
|
|||
if pt:
|
||||
self._touchSeen = nowms
|
||||
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:
|
||||
self._touchDown = False
|
||||
# USB-MIDI in: any byte = a host is listening (heartbeat); also assemble SysEx (clock / pushed programs)
|
||||
|
|
@ -571,14 +629,16 @@ class App:
|
|||
# ---------- drawing ----------
|
||||
def draw_bpm(self):
|
||||
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)
|
||||
self._place(self.g_name, self.name[:22], 12, 130, C_TXT, C_BG, FONT_M)
|
||||
self._place(self.g_idx, "%d/%d" % (self.idx + 1, len(self.programs)), 0, 134,
|
||||
C_DIM, C_BG, FONT_S, right_edge=WIDTH - 12)
|
||||
def draw_status(self): # set-list tab (small, tap to switch) above the item title
|
||||
sl = self.setlists[self.sl]
|
||||
# tab: playlist + position; muted = built-in (read-only), cyan = your own
|
||||
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
|
||||
g = self.g_train
|
||||
while len(g): g.pop()
|
||||
x = 12; y = 104
|
||||
x = 12; y = 100
|
||||
if self.ramp:
|
||||
up = self.ramp['amt'] >= 0
|
||||
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
|
||||
elif cmd == 0x02: # version query -> reply 0x03 + APP_VERSION
|
||||
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:
|
||||
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.load(self.idx)
|
||||
if self.midi: self.midi.write(bytes([0xF0, 0x7D, 0x7F, 0xF7])) # ACK ok
|
||||
except OSError:
|
||||
if self.midi: self.midi.write(bytes([0xF0, 0x7D, 0x7E, 0xF7])) # NAK: read-only (editor mode)
|
||||
self.rebuild_setlists(); self.load(0) # built-ins untouched; show the refreshed lists
|
||||
self._ack(True)
|
||||
except Exception:
|
||||
self._ack(False) # read-only (editor mode) etc.
|
||||
# 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).
|
||||
elif cmd == 0x21: # BEGIN: open the staging file; sx[2:6] = expected length (7-bit)
|
||||
|
|
|
|||
|
|
@ -1,97 +1,3 @@
|
|||
{
|
||||
"title": "PolyMeter",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
"setlists": []
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue