Commit d1921a7b by PLN (Algolia)

deslop the copy: one term per concept, em dashes out of user-facing strings

menu and --help now both say theme (the menu said 'Next page'), the orbit
feed says Tidal orbit feed (HL was unexplained jargon), console messages lost
their em dashes, and the README swaps its rebuttal cadence for plain
sentences while explaining 'orbit' on first use. PRODUCT.md captures the
register and voice for future passes.
parent 56baad3a
# Product
## Register
product
## Users
Live performers and MIDI musicians who watch a control surface while playing:
livecoders (Tidal/Strudel/Supercollider), electronic performers, anyone with a
MIDI controller and a screen near the stage. They are mid-performance,
glancing, not reading; the tool must answer "what did I just touch and what is
resting where" in under a second. Secondary audience: developers who want a
readable example of a small, disciplined Qt/GLib-free ALSA visualizer.
## Product Purpose
midiviz turns a live MIDI stream into a picture: position, glyph, bar, hex,
brightness. Zero English words on the canvas. It exists because transcripts of
MIDI events are unreadable at performance speed (~400 events/second on a fader
sweep). Success: a performer glances, knows the surface state, returns to the
music.
## Brand Personality
The Ship's Bridge (ParVagues' design language): an instrument panel, not a
toy. Three words: legible, calm, precise. Voice in copy: dry, specific,
competent; the tool explains itself in one line and gets out of the way. No
hype, no exclamation marks, no marketing adjectives.
## Anti-references
- Synth-vendor marketing pages (neon gradients, "unleash your creativity").
- Dashboard slop: glassmorphism, decorative charts, rounded-everything.
- Em-dash-heavy aphoristic prose that performs cleverness instead of
documenting the tool.
## Design Principles
- Glance, don't read: the picture carries the information; copy only orients.
- Measure, don't decorate: every visual channel encodes data.
- Honest failure: when a layer is missing, it stays dark; the chrome says so.
- The rig is the contract: wire paths and port names are documented, not
invented.
## Accessibility & Inclusion
Three themes exist because one contrast profile cannot serve every ambient
light (dark stage, pale desktop, direct sun). The `sun` theme is the
accessibility tier: near-white, near-black ink at every decay level, thick
bars, no CRT texture. Test suite holds every theme to a mapped-but-idle
visibility contract.
# midiviz
**Live MIDI as a picture, not a transcript.**
`midimon` prints one line per event ("14:0 Control change ch0, controller 29,
value 15"). That is the right shape for a debugger and the wrong shape for a
performer: during a set nobody reads sentences, and a fader sweep emits ~400
events a second, so the words scroll past faster than an eye can land on them.
midiviz is a *lens*, not a log. Zero English words render: an event's identity
is its **position**, its type is a **glyph class**, its value is a **bar plus
two hex digits**, and its recency is **brightness**. The layout is the authored
control surface itself — rows A..F down, columns 1..8 across — so a glance
answers "which orbit did I just touch" without decoding anything. Only what the
grid cannot place (foreign CCs, notes, bend, program) falls as rain in the
right-hand gutter, which is itself the signal: *that came from somewhere else*.
It was born in ParVagues' livecoding rig, watching
a Novation **Launch Control XL 3** — but it watches any MIDI source, and its
branding is yours to change.
**Live MIDI as a picture.**
A MIDI fader sweep emits ~400 events a second. A transcript of those events is
the right shape for a debugger and the wrong shape for a performer: during a
set nobody reads sentences, so the words scroll past faster than an eye can
land on them.
midiviz is a lens built for that moment. Zero English words render on the
canvas: an event's identity is its **position**, its type is a **glyph class**,
its value is a **bar plus two hex digits**, and its recency is **brightness**.
The layout is the authored control surface itself, rows A..F down, columns
1..8 across, so a glance answers "which orbit did I just touch" without
decoding anything. (Orbit: the Tidal livecoding term for one pattern's voice;
column N is orbit N.) Only what the grid cannot place (foreign CCs, notes,
bend, program) falls as rain in the right-hand gutter, which is itself the
signal: that came from somewhere else.
It was born in ParVagues' livecoding rig, watching a Novation
**Launch Control XL 3**. It watches any MIDI source, and its branding is yours
to change.
![midiviz, dark theme, mid-performance](screenshots/hero-branded-dark.png)
## Highlights
- **The grid is the surface.** Column *N* is orbit *N*; the picture is the
- **The grid is the surface.** Column N is orbit N; the picture is the
authored LCXL3 control layout (`lcxl_grid.py`), not an arbitrary CC table.
- **Mapped-but-idle is never black.** Every grid cell keeps a floor tint, so a
connected-but-untouched control never reads as a hardware fault.
- **Resting state, not just events.** MIDI is edge-triggered, so a filter
parked at zero and one parked at the top look identical once the glow fades.
If a driver publishes a state file (last-value-wins, atomically written),
midiviz paints what each control is *resting at* — three zones for the
midiviz paints what each control is *resting at*: three zones for the
centre-detented family filters, detent drawn.
- **Three themes**: `dark` (the cockpit), `light` (pale desktop), `sun`
(near-white, heavy ink — for playing outdoors). Colours are data; every
(near-white, heavy ink, for playing outdoors). Colours are data; every
theme is held to the same visibility contract by the test suite.
- **Two frame rates, not a busy loop.** ~30 fps while events arrive, 5 fps two
seconds after the last one, precomputed colour LUTs, zero per-frame
- **Two frame rates, never a busy loop.** ~30 fps while events arrive, 5 fps
two seconds after the last one, precomputed colour LUTs, zero per-frame
allocation. Built on a machine that performs live audio.
- **Spectrum backdrop** (optional, `s`): the master bus, behind the grid.
- **Tidal HL feed** (optional, `h`): which orbits the music is actually
triggering right now, via a SuperDirt OSC mirror — the answer to "I turned
the knob and nothing happened: is the pattern silent, or is the knob not
wired?"
- **Tidal orbit feed** (optional, `h`): which orbits the music is actually
triggering right now, via a SuperDirt OSC mirror. This is the answer to
"I turned the knob and nothing happened: is the pattern silent, or is the
knob not wired?"
- **Survives everything**: unplug/replug (clean-exit-proof by design),
restarts (theme + scale persist), tiling WMs (the window's size belongs to
the WM, the type density to the window), duplicate launches (flock, exit 78
— a deliberate close is never a fault).
the WM, the type density to the window), duplicate launches (flock, exit 78,
so a deliberate close is never treated as a fault).
## The three themes
| dark — the cockpit | light — pale desktop | sun — outdoors |
| dark, the cockpit | light, pale desktop | sun, outdoors |
|---|---|---|
| ![](screenshots/theme-dark.png) | ![](screenshots/theme-light.png) | ![](screenshots/theme-sun.png) |
## Install & run
## Install and run
Needs Python ≥ 3.10, [PySide6](https://pypi.org/project/PySide6/), and
`aseqdump` (Debian/Ubuntu: `sudo apt install alsa-utils`).
......@@ -78,12 +80,13 @@ Keys: `q`/`Esc` quit · `p` pause · `c` clear · `s` spectrum · `t` pin ·
Drag the body to move, the edges and corners to resize (the type follows the
window); right-click for the menu (also in the system tray).
Tested on Linux/ALSA (Wayland and X11). Ports are resolved automatically —
a translated/virtual surface port is preferred over raw hardware ports.
Tested on Linux/ALSA (Wayland and X11). Ports are resolved automatically: a
translated/virtual surface port is preferred over raw hardware ports.
## Make it yours (branding)
midiviz ships wearing **ParVagues** and changes nothing about how it works:
midiviz ships wearing **ParVagues**, and changing the name changes nothing
about how it works:
```sh
MIDIVIZ_BRAND="Coolnaut" \
......@@ -93,30 +96,31 @@ MIDIVIZ_WAVE=$HOME/pictures/coolnaut-wave.png \
python3 midiviz.py
```
- `MIDIVIZ_BRAND` — spoken name, menu labels
- `MIDIVIZ_WORDMARK` — the block lettering drawn over row D
- `MIDIVIZ_BRAND_SLUG` — directory under `$XDG_CONFIG_HOME` / `$XDG_RUNTIME_DIR`
for saved theme, scale, close latch and instance lock (one per brand)
- `MIDIVIZ_WAVE` — any transparent PNG, watermarked faintly behind the grid
- `MIDIVIZ_BRAND` is the spoken name (menu labels)
- `MIDIVIZ_WORDMARK` is the block lettering drawn over row D
- `MIDIVIZ_BRAND_SLUG` is the directory under `$XDG_CONFIG_HOME` /
`$XDG_RUNTIME_DIR` for the saved theme, scale, close latch and instance
lock (one per brand)
- `MIDIVIZ_WAVE` is any transparent PNG, watermarked faintly behind the grid
## The wire
State has a single legitimate owner, and it is not the viewer. midiviz *reads*
two optional producers, and both are plain files — `cat` is the debugger:
State has a single legitimate owner, and it is not the viewer. midiviz reads
two optional producers, and both are plain files, so `cat` is the debugger:
| path | producer | shape |
|---|---|---|
| `~/.cache/parvagues/surface-state.json` | any driver that integrates relative encoders and owns the absolute values | 32 values, last-value-wins, monotonic `seq`, written atomically |
| `~/.cache/parvagues/eval-events.jsonl` | an editor publishing which orbits the loaded track triggers | one JSON object per line: `{"t":…, "path":…, "d":["d1",…]}` |
Missing files are fine — those layers simply stay dark. The parvagues paths are
the wire contract midiviz was born on, not branding; point your own producer at
the same paths, or run everything under your own `MIDIVIZ_BRAND_SLUG` and write
the state file there.
Missing files are fine: those layers simply stay dark. The parvagues paths are
the wire contract midiviz was born on, not branding. Point your own producer
at the same paths, or run everything under your own `MIDIVIZ_BRAND_SLUG` and
write the state file there.
A reference producer for the LCXL3 (delta integration, translated CC corpus)
lives in the ParVagues rig repository and is not required to run midiviz:
on a raw LCXL3 the event grid, gutter, spectrum and themes all work standalone.
lives in the ParVagues rig repository and is not required to run midiviz: on a
raw LCXL3 the event grid, gutter, spectrum and themes all work standalone.
## Development
......@@ -128,7 +132,7 @@ python3 midiviz.py --selftest --screenshot shots/
`--selftest` is the drawing prover: it renders synthetic events through the
real parser, cycles every theme, resizes through the tiling-WM shapes and
checks the paint output — green tests on pure functions prove nothing about a
checks the paint output. Green tests on pure functions say little about a
window, so this drives the window itself.
Layout, one screenful:
......@@ -139,10 +143,10 @@ Layout, one screenful:
| `midimon.py` | the one parser (`parse_line` / `enrich` / port resolution) |
| `midistream.py` | threaded `aseqdump` reader |
| `lcxl_grid.py` | THE grid, authored once; everything derives from it |
| `oscfeed.py` | the Tidal HL feed (SuperDirt OSC mirror) |
| `oscfeed.py` | the Tidal orbit feed (SuperDirt OSC mirror) |
| `ui/` | vendored brand assets: wave PNG, display font (Syne 800) |
| `packaging/` | example systemd user unit + desktop entry |
## License
GPL-3.0-or-later — see [LICENSE](LICENSE).
GPL-3.0-or-later, see [LICENSE](LICENSE).
......@@ -2821,7 +2821,7 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
grp.addAction(a)
m.addAction(a)
self._theme_acts[name] = a
self._act_cycle = QtGui.QAction("Next page\td", self)
self._act_cycle = QtGui.QAction("Next theme\td", self)
self._act_cycle.triggered.connect(lambda _c=False: self.cycle_theme())
m.addAction(self._act_cycle)
......@@ -2864,7 +2864,7 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
self._act_spec.setCheckable(True)
self._act_spec.triggered.connect(lambda _c=False: self.toggle_spectro())
m.addAction(self._act_spec)
self._act_hl = QtGui.QAction("Tidal HL feed\th", self)
self._act_hl = QtGui.QAction("Tidal orbit feed\th", self)
self._act_hl.setCheckable(True)
self._act_hl.triggered.connect(lambda _c=False: self.toggle_hl())
m.addAction(self._act_hl)
......@@ -3654,7 +3654,7 @@ def main(argv=None) -> int:
help="initial scale, 0.6-2.4 (also +/- at runtime; "
"remembered between runs)")
ap.add_argument("--theme", choices=sorted(THEMES), default=None,
help="palette: dark (the cockpit), light (pale desktop), "
help="theme: dark (the cockpit), light (pale desktop), "
"sun (maximum contrast, for playing outdoors); "
"'d' cycles at runtime and the choice is remembered")
ap.add_argument("--no-tray", action="store_true",
......@@ -3665,7 +3665,7 @@ def main(argv=None) -> int:
ap.add_argument("--spectro-target",
help="capture this node instead of the default sink")
ap.add_argument("--no-hl", action="store_true",
help="do not subscribe to the Tidal HL feed (SuperDirt "
help="do not subscribe to the Tidal orbit feed (SuperDirt "
"mirror on port %d; 'h' toggles it at runtime)"
% oscfeed.VIZ_PORT)
ap.add_argument("--selftest", action="store_true",
......@@ -3701,7 +3701,7 @@ def main(argv=None) -> int:
try:
fcntl.flock(fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError:
print("midiviz: already running — refusing to duplicate "
print("midiviz: already running, refusing to duplicate "
"(focus the existing window)", file=sys.stderr)
try:
import launchers
......@@ -3731,7 +3731,7 @@ def main(argv=None) -> int:
# The theme joins it for the same reason: a `--theme` that lost to a saved
# one, or a saved one that lost to a corrupt file, is otherwise a silent
# surprise, and the window has no words to explain itself with.
print("⚓ midiviz — %s · theme %s · %.2f× · q/Esc/Ctrl-C to quit · right-click for the menu"
print("⚓ midiviz · %s · theme %s · %.2f× · q/Esc/Ctrl-C to quit · right-click for the menu"
% (label or "port %s" % port, theme, scale), file=sys.stderr)
# A blind `aseqdump` (no -p) subscribes to NOTHING -- see the
# WATCH_PREFERENCE comment above -- so start a Reader only once a real
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment