Skip to content

Web: live serve dashboard, GitHub Pages landing-page deploy, and a browser-version plan - #5

Merged
chrisschouk merged 24 commits into
mainfrom
claude/audx-web-and-serve
Jun 21, 2026
Merged

Web: live serve dashboard, GitHub Pages landing-page deploy, and a browser-version plan#5
chrisschouk merged 24 commits into
mainfrom
claude/audx-web-and-serve

Conversation

@chrisschouk

Copy link
Copy Markdown
Owner

Answers "can I use audx in the terminal and at a URL?" — three things, scoped honestly.

1. A live dashboard URL (audx serve) ⬆️

Upgraded the read-only companion from plain level-bars into a live step-sequencer grid — per-track 16-step cells with a moving amber playhead, plus per-channel meters, in the same monochrome house style as the promo. Pure stdlib, localhost-only.

audx serve            # → http://127.0.0.1:8080  (watch on your phone while you play)

New /state shape (per-track step grids); covered by tests/test_web.py.

2. The landing page, live at a public URL 🌐

.github/workflows/pages.yml deploys site/ to GitHub Pages on push to main.

One-time setup (yours): repo Settings → Pages → Source = "GitHub Actions". After that it publishes automatically to https://totalaudiopromo.github.io/audx/.

vercel.json + .vercelignore are included too, so you can alternatively connect Vercel's git integration (it serves site/ statically, ignoring the Python/marketing tree). (The Vercel MCP here can't deploy without your CLI auth, so I set up the path you control.)

3. A real browser-playable audx — planned, not hand-waved 📋

docs/web-version-plan.md lays out the honest path: audx is Python (numpy/PortAudio/Textual/mido), so a web version is a core port, not a deploy flag. Three routes (Pyodide / TypeScript+Web Audio / Rust-WASM), a recommended phasing (Pyodide "try it" demo → TS+Web Audio live core with Web MIDI + Push 2 LEDs → parity), golden-vector tests to keep native and web in sync, and effort/risk estimates.

Also

  • docs/releases/v0.3.0.md — prepared release notes (you can paste these into the GitHub release when you tag v0.3.0).

Verification

ruff clean · mypy clean (39 files) · 199 tests pass · _state() produces correct grids (kick = four-on-the-floor, etc.).

The honest status (terminal vs URL)

  • Terminal — yes, fully, today.
  • Landing page at a URL — yes, this PR (one Pages toggle).
  • Local dashboard URL — yes, audx serve.
  • The full instrument playable in a browser — a real project; this PR ships the plan, not the engine.

🤖 Generated with Claude Code


Generated by Claude Code

claude added 20 commits June 21, 2026 04:49
…l offline use

- Add audx/synth.py: pure-numpy procedural drum/perc voices (kick, snare,
  hh, oh, clap, rim, tom, cowbell, perc, ride, crash, shaker, sub) with
  aliases, tuning, and deterministic rendering. No samples required.
- Renderer falls back to the synth kit when a sample is not found, so
  'audx render "kick 4/4"' makes sound with zero setup; --sample is optional.
- New commands: 'audx demo' (renders a full multi-track beat) and
  'audx synths' (lists built-in voices).
- Engine: import sounddevice lazily so the CLI and all offline features work
  without the PortAudio system library; add SynthVoice + synth fallback to the
  live callback; tidy types.
- Types: mypy now checks the whole package cleanly (was 4 files); fix real
  errors in midi/web/calibration/voice/groove; add types-requests.
- Add test_synth, test_render_synth, test_cli, test_more_coverage covering
  the synth kit, synth-fallback rendering, MIDI export, project round-trips,
  DSL operators, and the offline CLI surface. Coverage 27%→43%.
- Fix 'audx mix set <ch> gain -3': allow dash-leading values (ignore_unknown_options)
  so documented negative-dB usage works.
- Autouse fixtures reset the process-global pattern engine between CLI tests.
- Raise CI coverage gate 25→40.
…page

- Rewrite README around the 10-second zero-config demo and the built-in synth
  kit; add badges, DSL reference table, grouped command overview, honest
  'what it isn't yet' section.
- Add LICENSE (MIT), CHANGELOG.md (Keep a Changelog), CONTRIBUTING.md.
- Add docs/getting-started.md, docs/synth-kit.md and a docs index.
- Add site/ static landing page (dark terminal aesthetic, no build step).
CI was pip-installing an unpinned mypy (2.1.0) and using flake8, so it
diverged from the project's locked tools and failed on stricter
var-annotated/unused-ignore inference that 'uv run mypy' (1.20.2) doesn't
report. Run ruff, mypy and pytest through uv with --all-extras so CI matches
local exactly; drop flake8. fail-fast: false to see all matrix results.
…ideo + PyPI release

Product (team):
- synth: 6 melodic voices (bass, pluck, stab, keys/ep, saw, sine) — band-limited
  saws, pitched via tune; bass is now its own voice. Kit is 20 voices total.
- arrangement: Song/Section model + render_song; new 'audx song render|info'
  build multi-section tracks (intro/verse/drop/outro) from a JSON spec.
- ui: live finger-drum pads (w e r a s d f z x c u i o) trigger synth voices
  on dedicated channels while a project is open.

Marketing:
- Animated terminal demo GIF in README (scripts/make-demo-gif.py, Pillow-only).
- Runnable Remotion promo project (marketing/remotion/) + rendered 1080p MP4
  and poster (marketing/promo/).

Packaging:
- Automated PyPI release via trusted publishing on v* tags (.github/workflows/release.yml);
  PUBLISHING.md updated for the uv build flow.

Tests 123→172, coverage 43%→46%; ruff + mypy clean across the package.
The old promo was static terminal text with hard cuts. Rebuilt it around what
audx actually is — a live instrument:
- build-beat.py renders a real audx beat (synth kit) and emits a frame-accurate
  data track (per-track 16-step grids + hit frames, master RMS envelope, waveform
  peaks) so the motion graphics are driven by the actual audio.
- AudxPromo.tsx: a live step-sequencer grid that fires on the beat with a moving
  playhead, per-track VU meters, and a waveform scope that sweeps in time — with
  the rendered beat as the MP4 soundtrack. Smooth eased crossfades, no hard cuts.
- Electric-neon palette on pure black (cyan/magenta/lime/amber/violet).
- README hero is now the neon sequencer poster linking to the 20s promo.

Rendered artifacts: marketing/promo/audx-promo.mp4 (1080p, with audio) + poster.
… + gradients

Neon + gradients read as cheap/generic. Researched premium audio-brand and motion
direction (Teenage Engineering's muted-palette + single-accent restraint; the 2025
monochrome counter-trend to gradients/glow) and rebuilt accordingly:
- Near-monochrome: warm off-white on true black, a SINGLE amber accent used only
  for the play position. No gradients, no glows, flat fills.
- Sequencer is now a Monome/TE-style monochrome LED matrix: hairline-outlined
  cells, off-white hits, amber only under the thin playhead rule. Flat waveform.
- Swiss layout: passe-partout frame, lowercase monospace labels, generous space.
- Calmer motion: one bezier easing, restrained fades, discrete caret blink.
- Flat monochrome also compresses far better — MP4 2.7MB (was ~10MB).

Refreshed marketing/promo/*.mp4 + poster and docs/assets/promo-poster.png.
- audx jam: real-time play from a MIDI controller or Push 2. Drums mode maps
  GM notes to drum voices and never leaves a pad silent; --chromatic plays a
  melodic voice across the keys. New audx/live.py (pure, tested mapping) wired to
  engine.play_synth (now supports tune_semitones).
- docs/playing-live.md: kid-friendly controller/Push 2 walkthrough; linked from
  README ('Play it live') and the docs index.
- Bump to 0.3.0 (pyproject, package.json, __init__, CHANGELOG cut).
- Push2Lights drives Push 2 pad LEDs over MIDI: SysEx colour-palette entries
  (8-bit RGB split to 7-bit LSB/MSB) + note-on (channel 0) lighting, per the
  official Push 2 MIDI spec. Byte-exact unit tests against the spec example.
- push2_pad_layout: a 13-voice drum kit on the bottom two rows, one colour each.
- audx jam auto-detects a Push 2, paints the kit and flashes each pad on hit
  (--no-lights to disable; lights are drums-mode only). New 'audx push2 lights'
  lights the kit on its own for a quick check.
- Bundle python-rtmidi so mido has a backend — midi/jam/Push 2 work out of the box.
- Port discovery degrades gracefully with no backend/device; guide + README + CHANGELOG updated.
… plan + v0.3.0 notes

- audx serve: upgrade the read-only dashboard to a live step-sequencer grid with a
  moving playhead + per-channel meters, in the monochrome house style (stdlib only).
  New /state shape (per-track 16-step grids); tests in test_web.py.
- Landing page goes live: .github/workflows/pages.yml deploys site/ to GitHub Pages
  (URL https://totalaudiopromo.github.io/audx/ once Pages source = GitHub Actions).
  vercel.json + .vercelignore included for the Vercel git-integration alternative.
- docs/web-version-plan.md: phased, honest plan for a browser-playable audx
  (Pyodide demo → TS/Web Audio core + Web MIDI/Push 2 → parity), with effort/risks.
- docs/releases/v0.3.0.md: prepared release notes.
Standalone 'audx serve' reads its own (empty) process state, so it only shows a
session running in that same process. Add web.serve_in_background() and wire
'audx open --serve [--serve-port]' so the TUI hosts the dashboard in-process — the
session you play now shows up live at http://<machine>:8080/ on any device on the
LAN (read-only). Real HTTP round-trip test; guide + README updated.
…attack plan

- site/play.html: a Pyodide playground that runs the REAL audx DSL parser and
  synth kit client-side (numpy in the browser), mixes to stereo and plays via
  Web Audio. Presets, WAV download, nothing leaves the visitor's machine.
- site/audx_web/webrender.py: render path that dual-imports the real audx package
  (tests/dev) or flat modules (Pyodide), so the in-browser sound matches the CLI.
- No drift: scripts/sync-web-modules.sh copies synth.py/pattern.py into the bundle;
  pages.yml runs it on deploy and triggers on src changes; a test asserts the
  bundled copies equal src/ (tests/test_webrender.py).
- 'play it in your browser' CTA on the landing page; README pointer.
- docs/web-version-plan.md: Phase 1 marked shipped; phases 2-3 expanded into a
  concrete milestone attack plan (golden-vector tests, TS synth+DSL, Web Audio
  scheduler, Web MIDI + Push 2 LEDs, then parity/sharing).
The fidelity-critical core of the browser port. web/src/dsl.ts faithfully ports
src/audx/pattern.py (steps, modifiers, swing), validated against fixtures emitted
from the REAL parser so the browser parses identically to the CLI.

- scripts/gen_web_fixtures.py: emits web/fixtures/{dsl,swing}.json from audx.pattern.
- web/: vitest project; tests/dsl.test.ts asserts TS == Python within 1e-9 across
  30 cases — every grammar branch plus the quirks (16x8 = 16 hits, banker's
  rounding in swung_beat, sample aliases bd->kick/hh->hihat, modifier clamping).
- tests/test_web_fixtures.py: guards the committed fixtures against parser drift.
- ci.yml: new 'web' job regenerates fixtures, fails on drift, runs tsc + vitest.
- docs/web-version-plan.md: M2.2 marked shipped; M2.1 (synth port) is next, reusing
  the same golden-vector harness for per-voice audio references.
Port all 20 synth voices to TypeScript (web/src/synth.ts) and build a real
browser instrument on top of them.

Synth port, golden-vector verified:
- 10 noiseless voices match numpy sample-for-sample (mean err < 2e-4); cowbell +
  the 9 noise voices match structurally (normalized peak, envelope correlation,
  energy); the repitch resampler is checked — 44 vitest assertions.
- scripts/gen_web_fixtures.py emits web/fixtures/synth.json from the REAL synth.
- Fixed a latent bug: 'perc' had no 'percussion' alias, so pattern.py's perc steps
  dropped silently in the synth (and Phase 1). Added the alias in src + TS.

audx studio (site/studio.html + web/src/studio.ts, bundled to site/studio.js):
- 16-step grid sequencer driven by the native synth.
- Sample-accurate Web Audio lookahead scheduler (25ms tick / 100ms ahead) with a
  real swing offset on odd 16ths.
- Per-track mute/solo/volume, add/remove voices, drag-to-paint, audition on click,
  spacebar transport, master meter.
- 'copy as audx DSL' exports the grid as a pattern you can paste into the CLI.
- Brand-cohesive monochrome+amber UI, responsive.

- esbuild bundles the app; CI (web job) typechecks + tests + builds; Pages deploy
  builds site/studio.js. Landing hero now leads with 'open the studio'.
- docs/web-version-plan.md: M2.1, M2.3, M2.4 marked shipped.
Make the studio session durable, shareable and exportable — all logic verified
in vitest (no browser needed).

- web/src/project.ts: compact, URL-safe encode/decode of a session (voices as
  indices, steps as a 16-bit mask, flags). Round-trip tested.
- web/src/render.ts: offline render of the loop to a normalized stereo buffer +
  16-bit WAV, reusing the native synth and the live scheduler's 16th-note + swing
  timing so the file matches what you hear. Length/mute-solo/silence/header tested.
- web/src/types.ts: shared Track/ProjectState + audibleTracks (solo-then-mute).
- studio: 'share link' (whole groove in the URL hash), 'download wav', and
  localStorage autosave that restores on load (hash link wins). Transport inputs
  sync to a loaded session.
- 81 web tests pass; tsc clean; esbuild bundle rebuilt.
- docs/web-version-plan.md: M3.3 + M3.5 marked shipped.
Bring the signature audx hook to the browser: play voices from a controller and
light up a Push 2, using the exact same MIDI bytes the CLI sends.

- web/src/push2.ts: TypeScript port of audx/push2.py — Ableton user SysEx palette
  entries (RGB split into 7+1 bits), reapply, and note_on pad lights. Golden-vector
  tested against the real Python module (scripts capture mido's wire bytes via a
  recording port) — setup stream, flash, and tick-restore all byte-exact.
- web/src/midi.ts: thin Web MIDI glue — pads (notes 36..48) audition voices, the
  transport notes (85/86) drive play/stop, and a connected Push 2 is set up + lit.
- studio: 'connect MIDI / Push 2' button; the sequencer flashes pads as it plays;
  LED revert ticked each animation frame. Hidden when Web MIDI is unavailable.
- Extended the Python fixture drift-guard to cover synth.json + push2.json too
  (matters now that GitHub Actions CI isn't triggering for the PR).
- 85 web tests pass; tsc clean; bundle rebuilt. docs: M2.5 marked shipped.
…track pan

Deepen the instrument with a v2 session model (all logic vitest-verified):

- Per-step velocity: ghost/normal/accent levels (right-click a cell to cycle);
  the scheduler and offline render scale gain per step.
- Multi-bar patterns: 1/2/4 bars (16/32/64 steps); grid, scheduler and render all
  honour the length, with barline gaps in the grid.
- Per-track pan: equal-power law matching StereoPannerNode, applied live (a
  StereoPanner per hit) and in the offline render so the WAV matches playback.

- types.ts: v2 Track (steps as number[] velocities, pan) + ProjectState.bars,
  panGains, resizeSteps.
- project.ts: v2 URL encoding — per-step velocity as /15 hex digits (round-trips
  exactly for the discrete levels), bars, pan. Rejects v1/garbage.
- render.ts: velocity + pan + multi-bar; normalizes on both channels.
- studio: pan slider, bars selector, accent cycling, velocity-styled cells; resize
  steps when bars change.
- 87 web tests pass (project round-trip incl velocity/pan/bars; render pan + velocity
  + multi-bar length); tsc clean; bundle rebuilt. docs: M3.1 expanded.
… fallback)

Drop an audio file on a track (or hit the file button) and it plays instead of the
synth voice, mirroring the CLI's precedence (a resolvable sample wins, else the
built-in synth — arrangement._voice_audio).

- web/src/samples.ts: content-addressed SampleStore; decodeAudioData; raw bytes
  persisted in IndexedDB so samples survive a reload; sampleStereo() feeds the
  offline renderer. Pure ref-hashing is testable; decode + IDB are browser-only.
- types.ts: Track.sampleRef/sampleName + trackUsesSample() precedence helper.
- project.ts: v3 wire carries sample ref + name only (audio stays local); share
  links degrade to synth on a machine without the bytes. Rejects v2/garbage.
- render.ts: optional SampleProvider places stereo sample data (pan/velocity) over
  the synth path; backward compatible.
- studio: per-track 📁 button + drag-a-file-on-a-row, audition uses the track's
  buffer, WAV export passes the provider, samples reloaded from IDB on audio start.
- 94 web tests (new types.test.ts: precedence/pan/audible/resize; render: sample
  provider + fallback; project: v3 round-trip incl sample ref). tsc clean; built.
  docs: M3.4 marked shipped.
Export each audible track as its own WAV, like 'audx render --stems'.

- render.ts: refactor the placement loop into a shared placeTrack() used by both
  renderProject (sum + normalize) and the new renderStems() (one normalized WAV per
  audible track, named NN-<voice|sample>.wav; honours sample/velocity/pan/multi-bar).
- web/src/zip.ts: tiny store-only PKZIP writer (local headers + central directory +
  EOCD, CRC32 per entry) — no dependency. 'download stems' bundles the stems into
  audx-studio's audx-stems.zip.
- studio: 'download stems' button + shared download() helper.
- 100 web tests: renderStems (count/naming/headers, muted excluded), zip structure
  + canonical CRC32 (0xCBF43926), renderProject unchanged after the refactor.
  tsc clean; bundle rebuilt. docs: M3.5 marked shipped.
…erop

Arrange studio grids ('scenes') into a sequence, mirroring arrangement.py's
Section/Song model — with real web<->terminal interop.

- web/src/song.ts: Scene/Song types, timeline() (cumulative bar offsets) and
  totalBars; CLI Song JSON import/export ({bpm, sections, sequence}). The DSL bridge
  reuses the golden-vector-tested parser: trackToDSL emits a grid line, dslToTrack
  rebuilds a track via parsePattern (tiling 1-bar patterns across section bars, CLI
  repetition semantics). A web song plays with "audx song render", and CLI songs open
  in the studio.
- render.ts: renderSong lays sections out along the timeline (bounced for play/WAV),
  honouring sample/velocity/pan/swing per scene.
- studio: a 'song' panel — save grid as scene, click scenes into a sequence, play
  (bounce), download song WAV, export/import Song JSON; persisted to localStorage.
- Parity: gen_web_fixtures.py emits song.json from Song.from_spec().timeline();
  web/tests/song.test.ts asserts timeline parity, JSON round-trip and section
  placement (a hit in section 2 lands after section 1). 112 web tests; tsc clean.
- Caveat (documented): CLI explicit grids are binary, so per-step accents are lossy on
  a CLI round-trip; multi-bar scenes export as tiled 1-bar patterns.

docs: M3.2 marked shipped — Phase 2 + Phase 3 of the web plan now complete.
@chrisschouk
chrisschouk force-pushed the claude/audx-web-and-serve branch from c543e9b to 067f691 Compare June 21, 2026 17:52
claude added 4 commits June 21, 2026 20:29
Song mode played by bouncing to one AudioBuffer; make it first-class by driving the
existing 25ms/100ms lookahead scheduler across the song timeline.

- song.ts: songStepPlan(song) — pure mapping of each global 16th-step to the active
  scene + local step (walks timeline()); length = totalBars*16. Unit-tested.
- studio.ts: extract scheduleHits(tracks, localStep, time, swing) shared by the
  pattern and song transports. A songPlan var routes the scheduler through the song
  (loops); transportSteps() picks the modulus. 'play song (live)' now schedules live —
  instant stop, Push 2 pads flash for the active scene, and the active scene/sequence
  chip highlights with a 'scene · bar x/total' readout. Pattern-play and song-play are
  mutually exclusive. The bounce stays behind song WAV export.
- 108 web tests (new songStepPlan offset + repeat tests); tsc clean; bundle rebuilt.

Note: first push since Actions was re-enabled, so this is the first clean-checkout CI
run for the branch.
main advanced via PR #4 ("Make audx marketable"), which added the same foundational
files this branch already carries (synth.py, push2.py, arrangement.py, site/, …) in a
parallel version — producing add/add conflicts. This branch is a strict superset of
those files (it also rewrote the serve dashboard and added the whole web/ studio), so
the 5 conflicts (README.md, docs/README.md, docs/playing-live.md, site/index.html,
synth.py) were resolved to this branch's versions, which contain main's content plus
the web additions (browser callout, open --serve, percussion alias, second-screen doc).

Verified after merge: ruff + mypy clean, full Python suite green, 108 web tests pass,
tsc clean.
@chrisschouk
chrisschouk merged commit beb0d66 into main Jun 21, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants