· apps · soundbox · music · audio · gato
Soundbox: visual graph audio in the browser, no login
Soundbox in gato: graph + Web Audio in TypeScript — open these repo paths beside the post so every claim maps to real code.
Nodes on a canvas, pulses down the wires — the beat is the diagram.
How to read this (so it is not bland marketing): run the marketing site locally (cd marketing && npm run dev), open this post, and keep soundbox/ in your editor. Every section below points at real files; if a sentence does not trace to code, treat it as opinion.
Start here in the repo: soundbox/src/main.ts → soundbox/src/app/bootstrap.ts → soundbox/src/models/graph.ts.
The hero image above is the real in-app canvas: default graph transport → Kick → Bass, edge timing 1/4+2, palette dock, floating mini-mixers.
Soundbox is gato’s visual audio creation tool: you place nodes (transport, drums, tone/bass, TR-808-style grids, “Rad” melodic grids), connect them with edges, and hit play. It runs in the browser, is served from the main site at /soundbox/, and is built as its own TypeScript + Vite app under soundbox/ in the repo — not a React “Vault” app, but a first-class sibling product on the same platform.
There is no login. Persistence is literally two string keys in StorageService:
// soundbox/src/services/StorageService.ts — lines 9–10
const STORAGE_KEY = "soundbox.project.v1";
const BEATS_KEY = "soundbox.beats.v1";
loadProject() / saveProject() round-trip JSON; loadBeats() / saveBeat() store named snapshots of a Project — see SavedBeat in the same file.
Node vocabulary (one line from the source of truth)
Allowed node kinds are a closed union — not marketing labels, TypeScript:
// soundbox/src/models/graph.ts — line 1
export type NodeType = "transport" | "kick" | "snare" | "hat" | "tone" | "bass" | "tr808" | "rad";
If your UI shows something else, it was migrated out; StorageService still filters unknown type values on load.
Bootstrap: the real wiring diagram
bootstrap is a plain function that instantiates services in dependency order, wires preset resolvers into the audio engine + exporter, and debounces saves to 120 ms:
// soundbox/src/app/bootstrap.ts — lines 21–54 (excerpt)
const paletteService = new PaletteService();
const storageService = new StorageService();
const toneService = new ToneService();
const exportService = new ExportService();
const initialProject = storageService.loadProject() ?? paletteService.createDefaultProject();
const graphService = new GraphService(initialProject);
const transportService = new TransportService(initialProject.bpm);
const audioEngineService = new AudioEngineService();
const renderSceneService = new RenderSceneService(stage);
const pulseFlowService = new PulseFlowService(graphService, transportService);
// …
audioEngineService.setPresetResolver((id) => toneService.getPreset(id));
audioEngineService.setDefaultPresetId(toneService.getActivePresetId());
// …
const scheduleSave = (): void => {
if (saveTimeoutId !== null) {
window.clearTimeout(saveTimeoutId);
}
saveTimeoutId = window.setTimeout(() => {
saveProject();
saveTimeoutId = null;
}, 120);
};
That is the “app shell” — no framework magic, just constructors.
/soundbox/ is not hand-wavy routing
The Vite app is built with base: '/soundbox/' so asset URLs resolve when the bundle is copied under the main gato site:
// soundbox/vite.config.js — lines 3–6
// Served from the main gato site at /soundbox/ (dev proxy + production copy into dist/soundbox)
export default defineConfig({
base: '/soundbox/',
Repo glue: scripts/ensure-soundbox-dist.mjs, scripts/copy-soundbox-to-dist.mjs, and root package.json build chain copy soundbox/dist → dist/soundbox.
What you actually do in Soundbox
- Open Soundbox on gato (or
/soundbox/on your build). - Drag node types from the palette onto the stage (kick, snare, hat, tone, bass, TR-808, Rad, etc. — see
NodeTypeinsoundbox/src/models/graph.ts). - Connect nodes so pulses flow from the transport along edges;
PulseFlowServiceandedgeTimingdecide when downstream nodes fire. - Press play — the overlay calls
audioEngineService.unlock()thenTransportServicetoggles;AudioEngineServiceturns triggers into Web Audio hits (kicks, snares, hats, tonal chords from presets, TR-808 lanes, Rad sequences). - Tweak BPM, pro mode, tone presets (save named presets in
ToneService), and optional export paths wired throughExportService.
The scene is a Three.js orthographic canvas (RenderSceneService): nodes are sprites, edges and animated pulses are rendered in WebGL, and pointer interaction goes through InteractionService so the graph feels like a tactile instrument.
Here is a richer real UI snapshot — same app, but with every drum/tone inspector expanded and an 808 Rhythm Composer visible, so you can see the full surface area rather than just the clean hero graph:
How the pieces fit (high level)
| Piece | Role |
|---|---|
PaletteService | Default projects and palette items (starting graphs). |
GraphService | Source of truth for nodes, edges, BPM, pro mode; subscribe for reactive updates. |
TransportService | Clock: BPM, play/stop, beat index; notifies PulseFlowService. |
PulseFlowService | Turns transport beats into pulses and trigger events along the graph. |
AudioEngineService | AudioContext, synthesis, TR-808 / Rad sequencing, preset preview. |
RenderSceneService | Three.js rendering and hit-testing for the graph. |
StorageService | localStorage load/save for Project + saved beats. |
Bootstrapping wires this together in soundbox/src/app/bootstrap.ts: create services, load or create a project, subscribe the renderer to graph changes, schedule debounced saves (~120 ms after edits).
Try it
- Live: gato.to/soundbox
- Local (main gato dev): after
npm run devfrom repo root, open/soundbox/on that origin —vite.config.jsserves the builtsoundbox/disttree when present (soundboxDevStaticplugin). - Local (marketing blog only):
cd marketing && npm run dev→ Soundbox posts index (this series: visual / pulses / Web Audio).
If soundbox/dist is missing, run npm run build --prefix soundbox (or let predev run ensure-soundbox-dist).
Play it here (live embed)
The iframe below is the actual app on this domain (/soundbox/). First click unlocks audio; after that you can drop palette nodes, connect edges, and hit play. Lazy-loaded so the hero stays the priority asset.
/soundbox/. On staging/production this is the same build the standalone page serves.Sources: soundbox/src/app/bootstrap.ts, soundbox/src/models/graph.ts, soundbox/src/services/StorageService.ts, soundbox/src/services/RenderSceneService.ts, marketing/src/pages/apps/music.astro.