Firmware (pico-cp/): the Pico now owns its filesystem by default (boot.py), so it can save the
practice log and write editor-pushed set lists; the drive is read-only to the computer, which also
protects the firmware. Hold button A at power-on for editor mode (drive writable; universal drag).
- Replaced the on-screen touch buttons with an on-device PRACTICE LOG (time · BPM · duration ·
track), newest-first, persisted to /history.json next to programs.json. Plays < 5s aren't logged;
tap a row twice to delete it. Real timestamps once the editor syncs the clock.
- USB-MIDI SysEx receiver: clock-set (0x01 -> RTC) and program-push (0x10 -> write programs.json,
reload, ACK/NAK). disable autoreload so our own writes never self-restart.
- Fixed swing: the parser was discarding the 's' flag, so /2s never swung. Now the scheduler uses a
per-step duration with long-short (2:1, SWING_RATIO 2/3) pairs on even subdivisions, matching the
web engine. Verified: ride:4/2s -> 266/133ms vs straight 200/200.
Editor (editor.html): requestMIDIAccess({sysex:true}); Save to device now pushes programs.json as
SysEx to the device (+ clock sync), waits for ACK, shows "Saved ✓", and falls back to downloading the
file (drag onto the drive in editor mode) when no device answers. Heartbeat also keeps the clock synced.
Web MIDI works in Chromium AND Firefox; the drag fallback covers any browser/OS incl. Safari.
Docs (pico-cp/README, info-kit, README) updated for the two modes, push programming, and the log.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
294 lines
17 KiB
Markdown
294 lines
17 KiB
Markdown
# VARASYS PolyMeter
|
||
|
||
A small **website** built around one **polymetric groove trainer / metronome** engine.
|
||
A landing page is the front door; the main app is the **PM_E‑1 PolyMeter Editor** — a full
|
||
web app where you stack as many "meter lanes" as you like, each its own little metronome
|
||
with a grouping, subdivision, drum voice and a per‑step pattern with accents. Layering lanes
|
||
produces polymeter and true ratio polyrhythm. The same engine drives an ever‑expanding library
|
||
of **form‑factor concepts** (idealized and buildable hardware mockups), ships as an
|
||
**embeddable widget** anyone can drop into their own page, and even runs as **firmware** on a
|
||
real Raspberry Pi Pico build (the **PM_K‑1 Kit**).
|
||
|
||
**Live:** https://metronome.varasys.io · **Source:** https://codeberg.org/VARASYS/metronome
|
||
|
||
Every **deployed page is a single, self‑contained `.html` file** — **zero dependencies**:
|
||
no framework, no CDN libraries, nothing fetched at runtime. They're assembled by a small
|
||
build step (`build.sh`) that inlines a shared engine, the seed set lists, base styling and
|
||
the brand assets (kept in `assets/`) into each page, so the sources stay lean. Every voice is
|
||
**synthesized** in Web Audio — there are no audio samples to load. State (set lists, the
|
||
practice log, theme and UI preferences) lives in `localStorage`.
|
||
|
||
## Pages
|
||
|
||
The site is **one editor + a gallery of form factors**, and each form factor is split into a
|
||
**lean widget page** and a **separate info page**:
|
||
|
||
- **`<device>.html`** — just the live widget (front view, controls, program box). This is what
|
||
`?embed=1` serves and what the landing embeds; it never ships the BOM/narrative.
|
||
- **`info-<device>.html`** — the spec page: it embeds the live widget at the top, then the
|
||
description, dimensioned drawings and a priced **Bill of Materials** (for the buildable hardware).
|
||
|
||
| URL | What |
|
||
|-----|------|
|
||
| [`/`](https://metronome.varasys.io/) `index.html` | **Concepts** — the landing / form‑factor gallery; each box embeds the live widget (Open ↗ / Specs & info ⓘ) |
|
||
| `/editor.html` · `/info-editor.html` | **PM_E‑1 — PolyMeter Editor** (the main app) + its overview |
|
||
| `/kit.html` · `/info-kit.html` | **PM_K‑1 Kit** — buildable Raspberry Pi Pico touchscreen unit (52Pi EP‑0172); info page has the wiring, parts and firmware |
|
||
| `/player.html` · `/info-player.html` | **PM_C‑1 Concept** — idealized concept device (full display + set‑list nav, theme, fullscreen "stage" view) |
|
||
| `/teacher.html` · `/info-teacher.html` | **PM_T‑1 Teacher** — studio / lesson console (colour TFT, arcade buttons, 1/4″ instrument pass‑through with analog click injection) |
|
||
| `/stage.html` · `/info-stage.html` | **PM_S‑1 Stage** — foot‑pedal stompbox (two footswitches, expression‑pedal in, RGB beat light, instrument pass‑through) |
|
||
| `/micro.html` · `/info-micro.html` | **PM_P‑1 Practice** — inline practice bar (instrument in / out pass‑through, clickable thumb‑roller, 14‑segment display) |
|
||
| `/showcase.html` · `/info-showcase.html` | **PM_D‑1 Display** — pyramid display piece; the pendulum is an RGB light bar combining every lane's subdivisions/accents |
|
||
| `/embed.html` · `/embed.js` | embed docs and the drop‑in loader |
|
||
| `/pico-main.py` | the PM_K‑1 MicroPython firmware (download) |
|
||
|
||
The buildable units (Teacher, Stage, Practice, Display, Kit) carry a priced BOM on their info
|
||
page; the Editor (web app) and Concept have none. Every page shares the same VARASYS header
|
||
(official logo with baked‑in tagline, nav, theme toggle). The editor also shows a subtle live
|
||
**program string** of what's loaded — editable, with copy/paste — under the app (press `Enter`
|
||
or paste to apply; see [the share language](#the-share-language)).
|
||
|
||
Because nothing loads from the network, you can save a page (`Ctrl`/`⌘`+`S`) and open it
|
||
straight from disk to run fully offline. One catch from a local `file://`: the browser may not
|
||
persist `localStorage` between sessions, so use **Export all** (set‑list **⋯** menu) to back up.
|
||
|
||
## Features
|
||
|
||
- **Meter lanes** — grouping (odd meters), subdivision (incl. swing), a drum/percussion
|
||
voice, per‑**step dynamics** (accent / normal / ghost / mute), mute, live measure counter.
|
||
- **Sounds** — every voice is **synthesized** in Web Audio: a friendly drum kit
|
||
(`kick`, `snare`, `hatClosed`, …) rendered with the **808 / 909** voices, the 808/909 voices
|
||
by name, and electronic/percussion tones. No samples are loaded.
|
||
- **Per‑lane gain** — a dB trim knob per lane (`@<db>` in the share language), applied at
|
||
schedule time so changing it never stutters playback.
|
||
- **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).
|
||
- **Euclidean rhythms** — spread *k* hits evenly across *n* steps with `(k,n[,rot])`.
|
||
- **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; **cue** across lists and commit
|
||
on a bar/beat boundary with no audible gap (see **Live performance**); each play is logged.
|
||
- **Sharing** — copy a link 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, the editor's program box, and a device's program list. You can
|
||
hand‑write or edit it.
|
||
|
||
### Patch grammar
|
||
|
||
```
|
||
v1 ; t<bpm> [; vol<pct>] [; cd<sec>] [; b<bars>] ; <lane> … [; tr<play>/<mute>] [; rmp<start>/<step>/<every>]
|
||
```
|
||
|
||
| Token | Meaning | Example |
|
||
|-------|---------|---------|
|
||
| `v1` | format version (always first) | `v1` |
|
||
| `t<bpm>` | tempo | `t120` |
|
||
| `vol<pct>` | master volume 0–100 | `vol70` |
|
||
| `cd<sec>` | time countdown, seconds (auto-advance with Continue) | `cd60` |
|
||
| `b<bars>` | segment length in bars (auto-advance with Continue) | `b16` |
|
||
| `tr<play>/<mute>` | gap trainer: play N bars, mute M | `tr2/2` |
|
||
| `rmp<start>/<step>/<every>` | tempo ramp: start BPM, ±step, every N bars | `rmp80/5/4` |
|
||
| `<lane>` | a meter lane (see below) | `kick:4` |
|
||
|
||
Tokens are joined with `;`. `tr` and `rmp` are omitted when off.
|
||
|
||
### Lane grammar
|
||
|
||
```
|
||
<sound> : <grouping> [ / <sub> ] [ (<k>,<n>[,<rot>]) ] [ = <pattern> ] [ @ <db> ] [ ~ ] [ ! ]
|
||
```
|
||
|
||
- **sound** — a synthesized voice. Friendly kit names (rendered with 808/909):
|
||
`beep`, `kick`, `snare`, `rim`, `clap`, `hatClosed`, `hatOpen`, `ride`, `crash`, `tomLow`,
|
||
`tomMid`, `tomHigh`, `tambourine`, `cowbell`, `woodblock`, `claves`; the drum‑machine voices
|
||
by name — `kick808 snare808 clap808 hat808 openHat808 cowbell808 tom808` and
|
||
`kick909 snare909 clap909 hat909 ride909 crash909`; or a **General‑MIDI note number**
|
||
(`36`→kick, `38`→snare, `42`→closed hat, …). Unknown → `beep`.
|
||
- **grouping** — beats per bar, optionally grouped for odd meters: `4`, `3`, `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 — also sets how many **pads** each beat splits into. Append **`s`** for **swing**
|
||
on even subdivisions (`2s`, `4s`) to delay the off‑beats to a 2:1 triplet feel.
|
||
- **`(k,n[,rot])`** — **Euclidean** fill: place `k` hits as evenly as possible across `n`
|
||
steps, optionally rotated by `rot`. e.g. `kick:4(3,8)`.
|
||
- **`=pattern`** — per‑**step dynamics**, one char per pad: **`X`** accent, **`x`** normal,
|
||
**`g`** ghost (soft), **`.`** `-` `_` mute (rest). Length = beats × `sub`. Omit to get the
|
||
default — first step of each beat accented, the rest normal. e.g. `4=.X.X` accents 2 & 4.
|
||
- **`@<db>`** — per‑lane gain trim in decibels, e.g. `@-3` or `@+2`.
|
||
- **`~`** — 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` | accented snare backbeat (2 & 4) |
|
||
| `hatClosed:4/2` | eighth‑note hi‑hats (downbeat of each beat accented) |
|
||
| `ride:4/2s` | **swung** eighth‑note ride |
|
||
| `kick:4(3,8)` | a 3‑over‑8 Euclidean kick |
|
||
| `claves:5~` | 5 evenly across lane 1's bar (5‑over‑4 if lane 1 is `4`) |
|
||
| `hat909:4/2@-4` | eighth 909 hats, trimmed −4 dB |
|
||
| `kick:2+2+3=x..x..x` | 7/8, kick on each group start |
|
||
| **Full:** `v1;t120;kick:4;snare:4=.x.x;hatClosed:4/2;tr2/2` | backbeat groove with gap trainer |
|
||
|
||
### In URLs
|
||
|
||
- **Settings:** `…/#p=<patch>` — readable, e.g. `…/#p=v1;t120;kick:4;claves:5~`
|
||
- **Set list:** `…/#sl=<base64url>` — a JSON `{title, description, items[]}` where each
|
||
item's config is a patch string.
|
||
|
||
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
|
||
|
||
In the set‑list panel's **⋯** menu:
|
||
- **Share settings link** / **Share set‑list link** open a dialog with the link to **Copy**
|
||
or **Open**. The link encodes everything in the URL — nothing is uploaded.
|
||
- **Export all / Import file** back up your set lists and practice log as a JSON file.
|
||
|
||
## Embedding
|
||
|
||
Any form factor can be embedded in another page as a self‑sizing widget. Drop in a
|
||
container and the loader script — it builds an `<iframe>` to the chrome‑stripped
|
||
(`?embed=1`) page, preloads your config string, and auto‑resizes to the content:
|
||
|
||
```html
|
||
<div data-varasys-metronome="micro"
|
||
data-patch="v1;t120;kick:4;snare:4=.X.X;hatClosed:4/2"></div>
|
||
<script src="https://metronome.varasys.io/embed.js"></script>
|
||
```
|
||
|
||
- `data-varasys-metronome` — variant: `editor` · `kit` · `initial` · `teacher` · `stage` · `micro` · `showcase`.
|
||
- `data-patch` — a [patch string](#patch-grammar) (maps to `#p=`); or `data-setlist`
|
||
for a set‑list code (maps to `#sl=`).
|
||
- `data-width` / `data-height` — optional initial size (default `100%` × `300px`;
|
||
height then tracks the widget, which posts `{type:'varasys-h', h}` to the parent).
|
||
|
||
Prefer your own iframe? `…/<variant>.html?embed=1#p=<patch>` works directly. The
|
||
[Concepts landing](index.html) and every `info-*.html` page dogfood this exact mechanism.
|
||
See `/embed.html`.
|
||
|
||
## Build it (hardware) — PM_K‑1 "Kit"
|
||
|
||
The **PM_K‑1 Kit** runs the same engine and program strings on a real device you can build today:
|
||
a **Raspberry Pi Pico** on the **52Pi EP‑0172 "Pico Breadboard Kit Plus"** — a 3.5″ ST7796
|
||
320×480 capacitive‑touch screen (GT911), a PSP joystick, a WS2812 RGB LED, a buzzer and two
|
||
buttons, all pre‑wired. See **`/info-kit.html`** for the pinout, parts (~$45 incl. Pico) and
|
||
flashing steps. Firmware lives in **`pico/`**:
|
||
|
||
- **`pico/main.py`** — single‑file **MicroPython** firmware: an ST7796 driver, GT911 touch,
|
||
WS2812 RGB, PWM buzzer, ADC joystick, baked anti‑aliased fonts, and the polymeter engine.
|
||
It parses the same program strings as the web editor. Flash MicroPython, copy `main.py`,
|
||
edit the `PROGRAMS` list to change grooves. Download: `/pico-main.py`.
|
||
- **`pico/gen_font.py`** — generates the baked anti‑aliased fonts (used by both firmwares).
|
||
- **`pico-cp/`** — a **CircuitPython** edition (download `/pm_k1_circuitpy.zip`): a self‑contained
|
||
appliance. The Pico mounts as a USB drive carrying the firmware + your `programs.json` + an offline
|
||
editor, drives a full lanes/pads touchscreen, **logs practice to `history.json`** on the device, takes
|
||
set lists **pushed from the editor over USB‑MIDI** (with a universal download‑and‑drag fallback), and
|
||
plays **out your computer's speakers over USB‑MIDI** (the editor's **🎹 Device audio**). By default the
|
||
firmware owns the drive (read‑only to the computer, so it's protected); hold **button A** at power‑on for
|
||
editor mode (drive writable). The MicroPython build stays the simple, no‑computer option.
|
||
|
||
## Keyboard shortcuts
|
||
|
||
| Key | Action |
|
||
|-----|--------|
|
||
| `Space` | play / stop (works everywhere except while typing in a text field) |
|
||
| `T` | tap tempo |
|
||
| `←` / `→` | tempo ±1 (`Shift` = ±10) |
|
||
| `A` | add meter lane |
|
||
| `↑` / `↓` / `Home` / `End` | move the **cue** cursor (crosses set lists) |
|
||
| `PgUp` / `PgDn` | cue the previous / next set list |
|
||
| `Enter` | commit the cued item — switches on the next **bar** (smooth) |
|
||
| `Shift`+`Enter` | commit now — switches on the next **beat** (rude) |
|
||
| `N` / `P` | load next / previous immediately (rude quick‑step) |
|
||
| `Alt`+`↑` / `Alt`+`↓` | reorder the cued item |
|
||
| `1`–`9` | enable / silence lane 1–9 |
|
||
| `?` | shortcuts help |
|
||
| `Esc` | close the help / share dialog · cancel an armed switch |
|
||
|
||
(Arrow / navigation keys are left alone while a slider or dropdown is focused, so they still adjust it.)
|
||
|
||
## Live performance
|
||
|
||
The set list is performance-ready: you can line up where you're going next without
|
||
disturbing what's playing, then commit on a musical boundary — no audible gap.
|
||
|
||
- **Cue, then commit.** The arrows / `Home` / `End` / `PgUp` / `PgDn` move a *cue cursor*
|
||
(amber outline) through items — across set lists, without loading anything. **`Enter`**
|
||
commits with a **smooth** cutover at the next **bar**; **`Shift`+`Enter`** is a **rude**
|
||
cutover at the next **beat**. `N` / `P` are immediate rude quick‑steps. `Esc` cancels.
|
||
- **Bar‑length segments.** Give an item a **bar** count (Timers box, or `b<n>`) and a bar
|
||
countdown (▦) shows bars remaining. With **Continue** on, it auto‑advances at the bar
|
||
boundary — so a *song* is just a set list of segments that hand off seamlessly.
|
||
- All transitions keep the clock continuous; the loaded item can live in a set list you're
|
||
not currently viewing (the player names it).
|
||
|
||
## Build
|
||
|
||
Every page is a source that shares code through `@BUILD:*` markers, so they all stay in sync:
|
||
|
||
- `/*@BUILD:include:src/…@*/` inlines a **shared partial** — the audio/scheduler engine
|
||
(`src/engine.js`), the seed set lists (`src/setlists.js`, so every page ships the **same
|
||
default set lists**), base styling (`src/base.css`), the site **header/footer/chrome**
|
||
(`src/header.html`, `src/footer.html`, `src/chrome.js`), the per‑device **program box**
|
||
(`src/progbox.{html,js}`) and the info‑page **live‑widget embed** (`src/infoembed.{html,js}`).
|
||
- `@BUILD:favicon@`, `@BUILD:logo-dark@`, `@BUILD:logo-light@` inline the base64 assets from
|
||
`assets/` (the official logos already include the tagline).
|
||
|
||
`./build.sh` resolves every marker into a self‑contained page in `dist/` (the Concepts landing,
|
||
the editor, the device/form‑factor pages and their `info-*.html`), copies `embed.js` through
|
||
as‑is, and copies the Pico firmware to `dist/pico-main.py`. `dist/` is generated, git‑ignored —
|
||
don't edit it by hand. `deploy.sh` runs the build first, so a deploy always serves freshly
|
||
assembled pages.
|
||
|
||
## Versioning
|
||
|
||
`VERSION` holds the formal version. `deploy.sh` builds, then stamps the served page:
|
||
|
||
- **Formal** — a clean commit tagged `v<VERSION>` → `X.Y.Z`.
|
||
- **Dev** — anything else → `X.Y.Z-dev.<utc-timestamp>.<short-sha>[.dirty]`.
|
||
|
||
Cut a release with `./release.sh [X.Y.Z]` — the optional arg bumps & commits `VERSION`; it then
|
||
tags the current commit `v<VERSION>` (requires a clean tree). Push the tag, then deploy.
|
||
|
||
## Files
|
||
|
||
| File | Purpose |
|
||
|------|---------|
|
||
| `index.html` | the **Concepts** landing / gallery (embeds each widget live) |
|
||
| `editor.html` | the **PM_E‑1 editor** app (source, with `@BUILD:*` markers) |
|
||
| `kit.html` · `player.html` · `teacher.html` · `stage.html` · `micro.html` · `showcase.html` | the device widget pages (PM_K‑1 Kit, PM_C‑1 Concept, Teacher, Stage, PM_P‑1 Practice, PM_D‑1 Display) |
|
||
| `info-*.html` | per‑form‑factor spec pages (embed the live widget + description + dimensions + BOM) |
|
||
| `embed.html` · `embed.js` | embed docs and the drop‑in widget loader |
|
||
| `src/` | shared partials inlined into every page: `engine.js`, `setlists.js`, `base.css`, `header.html`, `footer.html`, `chrome.js`, `progbox.{html,js}`, `infoembed.{html,js}` |
|
||
| `assets/` | base64 blobs inlined at build (`favicon`, `logo-dark`, `logo-light`) |
|
||
| `pico/` | PM_K‑1 MicroPython firmware: `main.py`, `gen_font.py` (font generator), `README.md` |
|
||
| `pico-cp/` | PM_K‑1 CircuitPython edition: `code.py`, `programs.json`, `font_*.bin`, `README.md` (bundled + served as `/pm_k1_circuitpy.zip`) |
|
||
| `build.sh` | resolve markers → self‑contained `dist/` pages (+ `pico-main.py`) |
|
||
| `deploy.sh` | build, then publish to the Caddy web root |
|
||
| `release.sh` | tag a formal version |
|
||
| `VERSION` | formal version string |
|
||
| `LICENSE` | GNU AGPL v3 license text |
|
||
|
||
## License
|
||
|
||
Copyright (C) 2026 Varasys.
|
||
|
||
This program is free software: you can redistribute it and/or modify it under the terms of the
|
||
**GNU Affero General Public License** as published by the Free Software Foundation, either
|
||
version 3 of the License, or (at your option) any later version. See [`LICENSE`](LICENSE).
|
||
|
||
Because the app is served over a network, the AGPL's §13 applies: anyone interacting with a
|
||
hosted instance must be able to get its source — the public repository is
|
||
**<https://codeberg.org/VARASYS/metronome>** (also linked from the in‑app **?** help).
|
||
|
||
### Credits
|
||
|
||
All drum and percussion voices are **synthesized in Web Audio** (808/909‑style and electronic) —
|
||
there are no audio samples. The on‑device fonts (PM_K‑1) are rendered from **DejaVu Sans**.
|