diff --git a/README.md b/README.md new file mode 100644 index 0000000..b17d48f --- /dev/null +++ b/README.md @@ -0,0 +1,143 @@ +# Stackable Metronome + +A browser **polymetric groove trainer / metronome** — and the design mockup for a +Raspberry Pi Pico hardware build. Stack as many "meter lanes" as you like; each is +its own little metronome with a grouping, subdivision, drum voice and per-beat +pattern. Layering lanes produces polymeter and true ratio polyrhythm. + +**Live:** https://metronome.varasys.io + +It's a single page (`index.html`) plus a vendored QR library — no build step, +no framework. State (presets, set lists, practice log, theme) lives in +`localStorage`. + +## Features + +- **Meter lanes** — grouping (odd meters), subdivision, GM drum voice, per‑beat + on/off pattern (rests), mute, live measure counter. +- **Polyrhythm** — a per‑lane *poly* toggle fits a lane's beats evenly into lane 1's + bar (e.g. 5‑over‑4, 3‑over‑2). +- **Practice** — gap/mute trainer (play N / mute M bars) and a tempo ramp with a + start BPM and signed step. +- **Set lists** — named, ordered lists of saved setups; ▶ loads + starts an item, + **N** advances; each play is logged for cross‑day comparison. +- **Sharing** — copy a link (with QR) to your current settings or a whole set list. +- **Theming** — System / Light / Dark. + +## The share language + +A compact, human‑readable text encodes a full configuration (a *patch*). It's what +goes in a share link, and you can hand‑write or edit it. + +### Patch grammar + +``` +v1 ; t [; vol] ; ; … [; tr/] [; rmp//] +``` + +| Token | Meaning | Example | +|-------|---------|---------| +| `v1` | format version (always first) | `v1` | +| `t` | tempo | `t120` | +| `vol` | master volume 0–100 | `vol70` | +| `tr/` | gap trainer: play N bars, mute M | `tr2/2` | +| `rmp//` | tempo ramp: start BPM, ±step, every N bars | `rmp80/5/4` | +| `` | a meter lane (see below) | `kick:4` | + +Tokens are joined with `;`. `tr` and `rmp` are omitted when off. + +### Lane grammar + +``` + : [ / ] [ = ] [ ~ ] [ ! ] +``` + +- **sound** — one of: + `beep`, `kick`, `snare`, `rim`, `clap`, `hatClosed`, `hatOpen`, `ride`, `crash`, + `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. +- **`/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. +- **`~`** — polyrhythm: fit this lane's beats evenly into **lane 1's** bar. +- **`!`** — mute the lane. + +### Examples + +| 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 | +| `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 | +| **Full:** `v1;t120;kick:4;snare:4=.x.x;hatClosed:4/2;tr2/2` | backbeat groove with gap trainer | + +### In URLs + +- **Settings:** `…/#p=` — readable, e.g. + `…/#p=v1;t120;kick:4;claves:5~` +- **Set list:** `…/#sl=` — a JSON `{title, description, items[]}` where + each item's config is a patch string. Used because titles/notes are free text. + +Opening such a link applies the settings (or imports the set list) on load, then +clears the hash so a refresh won't re‑import. + +## Sharing & QR + +In the set‑list panel's **⋯** menu: +- **Share settings link** / **Share set‑list link** open a dialog with the link and + a **QR code** (scan to open on a phone). Copy or Open from there. +- **Export all / Import file** back up presets + set lists + logs as a JSON file. + +QR codes are generated locally by the vendored `qrcode.js`; the link never leaves +your browser. Very long set‑list links may exceed QR capacity — copy those instead. + +## Keyboard shortcuts + +| Key | Action | +|-----|--------| +| `Space` | start / stop | +| `T` | tap tempo | +| `↑` / `↓` | tempo ±1 (`Shift` = ±10) | +| `A` | add meter lane | +| `1`–`9` | mute lane N | +| `R` | toggle the set‑list panel | +| `N` | next set‑list item | +| `?` | shortcuts help | +| `Esc` | close dialog / panel | + +## Versioning + +`VERSION` holds the formal version. `deploy.sh` stamps the served page: + +- **Formal** — a clean commit tagged `v` → `X.Y.Z`. +- **Dev** — anything else → `X.Y.Z-dev..[.dirty]`. + +Cut a release with `./release.sh [X.Y.Z]` (bumps `VERSION` + tags `v`), +then push the tag and deploy. + +## Deploy + +`./deploy.sh` copies `index.html` (version‑stamped) and `qrcode.js` into the Caddy +web root and smoke‑tests the live URL. No restart needed (`file_server` picks up +changes immediately). + +## Files + +| File | Purpose | +|------|---------| +| `index.html` | the whole app | +| `qrcode.js` | vendored QR generator (Kazuhiko Arase, MIT) | +| `deploy.sh` | publish to the Caddy web root | +| `release.sh` | tag a formal version | +| `VERSION` | formal version string | + +## Credits + +QR generation by [qrcode-generator](https://github.com/kazuhikoarase/qrcode-generator) +(© Kazuhiko Arase, MIT). diff --git a/deploy.sh b/deploy.sh index fa68c9a..9c20dba 100755 --- a/deploy.sh +++ b/deploy.sh @@ -37,6 +37,9 @@ fi sed "s|const APP_VERSION = \"[^\"]*\";|const APP_VERSION = \"$BUILD\";|" "$SRC_DIR/index.html" > "$DEST_DIR/index.html" echo "deployed v$BUILD ($(stat -c '%s' "$DEST_DIR/index.html") bytes) -> $DEST_DIR" +# vendored assets served alongside index.html +[[ -f "$SRC_DIR/qrcode.js" ]] && cp "$SRC_DIR/qrcode.js" "$DEST_DIR/qrcode.js" && echo "deployed qrcode.js" + # If real audio samples are added later (see the plan's GM-sample note), # sync that directory too. if [[ -d "$SRC_DIR/samples" ]]; then diff --git a/index.html b/index.html index 9a36e0e..af70aaf 100644 --- a/index.html +++ b/index.html @@ -138,6 +138,9 @@ .overlay { position:fixed; inset:0; background:rgba(0,0,0,.55); display:flex; align-items:center; justify-content:center; z-index:80; } .overlay[hidden] { display:none; } .overlay-box { background:var(--panel-2); border:1px solid var(--edge); border-radius:14px; padding:16px 20px; width:380px; max-width:92vw; box-shadow:0 20px 60px rgba(0,0,0,.6); } + .qr { background:#fff; border-radius:8px; padding:10px; text-align:center; margin-bottom:12px; } + .qr img { display:block; margin:0 auto; image-rendering:pixelated; max-width:100%; } + #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; } .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; } @@ -275,6 +278,18 @@ + + + +