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 # midiviz
**Live MIDI as a picture, not a transcript.** **Live MIDI as a picture.**
`midimon` prints one line per event ("14:0 Control change ch0, controller 29, A MIDI fader sweep emits ~400 events a second. A transcript of those events is
value 15"). That is the right shape for a debugger and the wrong shape for a the right shape for a debugger and the wrong shape for a performer: during a
performer: during a set nobody reads sentences, and a fader sweep emits ~400 set nobody reads sentences, so the words scroll past faster than an eye can
events a second, so the words scroll past faster than an eye can land on them. land on them.
midiviz is a *lens*, not a log. Zero English words render: an event's identity midiviz is a lens built for that moment. Zero English words render on the
is its **position**, its type is a **glyph class**, its value is a **bar plus canvas: an event's identity is its **position**, its type is a **glyph class**,
two hex digits**, and its recency is **brightness**. The layout is the authored its value is a **bar plus two hex digits**, and its recency is **brightness**.
control surface itself — rows A..F down, columns 1..8 across — so a glance The layout is the authored control surface itself, rows A..F down, columns
answers "which orbit did I just touch" without decoding anything. Only what the 1..8 across, so a glance answers "which orbit did I just touch" without
grid cannot place (foreign CCs, notes, bend, program) falls as rain in the decoding anything. (Orbit: the Tidal livecoding term for one pattern's voice;
right-hand gutter, which is itself the signal: *that came from somewhere else*. 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
It was born in ParVagues' livecoding rig, watching signal: that came from somewhere else.
a Novation **Launch Control XL 3** — but it watches any MIDI source, and its
branding is yours to change. 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) ![midiviz, dark theme, mid-performance](screenshots/hero-branded-dark.png)
## Highlights ## 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. 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 - **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. connected-but-untouched control never reads as a hardware fault.
- **Resting state, not just events.** MIDI is edge-triggered, so a filter - **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. 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), 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. centre-detented family filters, detent drawn.
- **Three themes**: `dark` (the cockpit), `light` (pale desktop), `sun` - **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. 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 - **Two frame rates, never a busy loop.** ~30 fps while events arrive, 5 fps
seconds after the last one, precomputed colour LUTs, zero per-frame two seconds after the last one, precomputed colour LUTs, zero per-frame
allocation. Built on a machine that performs live audio. allocation. Built on a machine that performs live audio.
- **Spectrum backdrop** (optional, `s`): the master bus, behind the grid. - **Spectrum backdrop** (optional, `s`): the master bus, behind the grid.
- **Tidal HL feed** (optional, `h`): which orbits the music is actually - **Tidal orbit feed** (optional, `h`): which orbits the music is actually
triggering right now, via a SuperDirt OSC mirror — the answer to "I turned triggering right now, via a SuperDirt OSC mirror. This is the answer to
the knob and nothing happened: is the pattern silent, or is the knob not "I turned the knob and nothing happened: is the pattern silent, or is the
wired?" knob not wired?"
- **Survives everything**: unplug/replug (clean-exit-proof by design), - **Survives everything**: unplug/replug (clean-exit-proof by design),
restarts (theme + scale persist), tiling WMs (the window's size belongs to 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 the WM, the type density to the window), duplicate launches (flock, exit 78,
— a deliberate close is never a fault). so a deliberate close is never treated as a fault).
## The three themes ## 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) | | ![](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 Needs Python ≥ 3.10, [PySide6](https://pypi.org/project/PySide6/), and
`aseqdump` (Debian/Ubuntu: `sudo apt install alsa-utils`). `aseqdump` (Debian/Ubuntu: `sudo apt install alsa-utils`).
...@@ -78,12 +80,13 @@ Keys: `q`/`Esc` quit · `p` pause · `c` clear · `s` spectrum · `t` pin · ...@@ -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 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). window); right-click for the menu (also in the system tray).
Tested on Linux/ALSA (Wayland and X11). Ports are resolved automatically — Tested on Linux/ALSA (Wayland and X11). Ports are resolved automatically: a
a translated/virtual surface port is preferred over raw hardware ports. translated/virtual surface port is preferred over raw hardware ports.
## Make it yours (branding) ## 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 ```sh
MIDIVIZ_BRAND="Coolnaut" \ MIDIVIZ_BRAND="Coolnaut" \
...@@ -93,30 +96,31 @@ MIDIVIZ_WAVE=$HOME/pictures/coolnaut-wave.png \ ...@@ -93,30 +96,31 @@ MIDIVIZ_WAVE=$HOME/pictures/coolnaut-wave.png \
python3 midiviz.py python3 midiviz.py
``` ```
- `MIDIVIZ_BRAND` — spoken name, menu labels - `MIDIVIZ_BRAND` is the spoken name (menu labels)
- `MIDIVIZ_WORDMARK` — the block lettering drawn over row D - `MIDIVIZ_WORDMARK` is the block lettering drawn over row D
- `MIDIVIZ_BRAND_SLUG` — directory under `$XDG_CONFIG_HOME` / `$XDG_RUNTIME_DIR` - `MIDIVIZ_BRAND_SLUG` is the directory under `$XDG_CONFIG_HOME` /
for saved theme, scale, close latch and instance lock (one per brand) `$XDG_RUNTIME_DIR` for the saved theme, scale, close latch and instance
- `MIDIVIZ_WAVE` — any transparent PNG, watermarked faintly behind the grid lock (one per brand)
- `MIDIVIZ_WAVE` is any transparent PNG, watermarked faintly behind the grid
## The wire ## The wire
State has a single legitimate owner, and it is not the viewer. midiviz *reads* 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: two optional producers, and both are plain files, so `cat` is the debugger:
| path | producer | shape | | 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/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",…]}` | | `~/.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 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 wire contract midiviz was born on, not branding. Point your own producer
the same paths, or run everything under your own `MIDIVIZ_BRAND_SLUG` and write at the same paths, or run everything under your own `MIDIVIZ_BRAND_SLUG` and
the state file there. write the state file there.
A reference producer for the LCXL3 (delta integration, translated CC corpus) A reference producer for the LCXL3 (delta integration, translated CC corpus)
lives in the ParVagues rig repository and is not required to run midiviz: lives in the ParVagues rig repository and is not required to run midiviz: on a
on a raw LCXL3 the event grid, gutter, spectrum and themes all work standalone. raw LCXL3 the event grid, gutter, spectrum and themes all work standalone.
## Development ## Development
...@@ -128,7 +132,7 @@ python3 midiviz.py --selftest --screenshot shots/ ...@@ -128,7 +132,7 @@ python3 midiviz.py --selftest --screenshot shots/
`--selftest` is the drawing prover: it renders synthetic events through the `--selftest` is the drawing prover: it renders synthetic events through the
real parser, cycles every theme, resizes through the tiling-WM shapes and 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. window, so this drives the window itself.
Layout, one screenful: Layout, one screenful:
...@@ -139,10 +143,10 @@ Layout, one screenful: ...@@ -139,10 +143,10 @@ Layout, one screenful:
| `midimon.py` | the one parser (`parse_line` / `enrich` / port resolution) | | `midimon.py` | the one parser (`parse_line` / `enrich` / port resolution) |
| `midistream.py` | threaded `aseqdump` reader | | `midistream.py` | threaded `aseqdump` reader |
| `lcxl_grid.py` | THE grid, authored once; everything derives from it | | `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) | | `ui/` | vendored brand assets: wave PNG, display font (Syne 800) |
| `packaging/` | example systemd user unit + desktop entry | | `packaging/` | example systemd user unit + desktop entry |
## License ## 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, ...@@ -2821,7 +2821,7 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
grp.addAction(a) grp.addAction(a)
m.addAction(a) m.addAction(a)
self._theme_acts[name] = 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()) self._act_cycle.triggered.connect(lambda _c=False: self.cycle_theme())
m.addAction(self._act_cycle) m.addAction(self._act_cycle)
...@@ -2864,7 +2864,7 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0, ...@@ -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.setCheckable(True)
self._act_spec.triggered.connect(lambda _c=False: self.toggle_spectro()) self._act_spec.triggered.connect(lambda _c=False: self.toggle_spectro())
m.addAction(self._act_spec) 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.setCheckable(True)
self._act_hl.triggered.connect(lambda _c=False: self.toggle_hl()) self._act_hl.triggered.connect(lambda _c=False: self.toggle_hl())
m.addAction(self._act_hl) m.addAction(self._act_hl)
...@@ -3654,7 +3654,7 @@ def main(argv=None) -> int: ...@@ -3654,7 +3654,7 @@ def main(argv=None) -> int:
help="initial scale, 0.6-2.4 (also +/- at runtime; " help="initial scale, 0.6-2.4 (also +/- at runtime; "
"remembered between runs)") "remembered between runs)")
ap.add_argument("--theme", choices=sorted(THEMES), default=None, 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); " "sun (maximum contrast, for playing outdoors); "
"'d' cycles at runtime and the choice is remembered") "'d' cycles at runtime and the choice is remembered")
ap.add_argument("--no-tray", action="store_true", ap.add_argument("--no-tray", action="store_true",
...@@ -3665,7 +3665,7 @@ def main(argv=None) -> int: ...@@ -3665,7 +3665,7 @@ def main(argv=None) -> int:
ap.add_argument("--spectro-target", ap.add_argument("--spectro-target",
help="capture this node instead of the default sink") help="capture this node instead of the default sink")
ap.add_argument("--no-hl", action="store_true", 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)" "mirror on port %d; 'h' toggles it at runtime)"
% oscfeed.VIZ_PORT) % oscfeed.VIZ_PORT)
ap.add_argument("--selftest", action="store_true", ap.add_argument("--selftest", action="store_true",
...@@ -3701,7 +3701,7 @@ def main(argv=None) -> int: ...@@ -3701,7 +3701,7 @@ def main(argv=None) -> int:
try: try:
fcntl.flock(fh, fcntl.LOCK_EX | fcntl.LOCK_NB) fcntl.flock(fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError: except OSError:
print("midiviz: already running — refusing to duplicate " print("midiviz: already running, refusing to duplicate "
"(focus the existing window)", file=sys.stderr) "(focus the existing window)", file=sys.stderr)
try: try:
import launchers import launchers
...@@ -3731,7 +3731,7 @@ def main(argv=None) -> int: ...@@ -3731,7 +3731,7 @@ def main(argv=None) -> int:
# The theme joins it for the same reason: a `--theme` that lost to a saved # 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 # 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. # 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) % (label or "port %s" % port, theme, scale), file=sys.stderr)
# A blind `aseqdump` (no -p) subscribes to NOTHING -- see the # A blind `aseqdump` (no -p) subscribes to NOTHING -- see the
# WATCH_PREFERENCE comment above -- so start a Reader only once a real # 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