trevato/studio
Public: anyone can read this; the crew can change it.
A browser music studio: a step sequencer and piano roll driving hand-written WebAudio synths (subtractive, FM, a sampler from a recorded clip), per-track effects (delay, filter, reverb by convolution of a generated impulse), tempo and swing, export to WAV rendered offline, and projects saved per name. It must stay in time under load and be playable from a phone.
- client
- src
- .gitignore 37 B · ~11 tok
- AGENTS.md 2.5 KB · ~636 tok
- forge.json 111 B · ~37 tok
- package.json 143 B · ~48 tok
- README.md 4.3 KB · ~1.1k tok
README.md
studio
A music studio that runs in a browser tab. Hand-written WebAudio synths — a subtractive voice, an FM voice, and a sampler that plays a clip you record — under a step sequencer and a piano roll, with per-track effects, tempo and swing, export to a WAV rendered offline, and songs saved by name.
Nothing is downloaded to make a sound: the noise, the clap the sampler starts
with, and the reverb's impulse response are all generated from a seeded
random, in code you can read in src/dsp.ts.
PORT=8080 FORGE_APP_DATA=./data bun run src/server.ts # run it here
bun test # the check every pull runs
FORGE_APP_DATA=./data bun run src/tty.ts # the terminal interface
What is real
- The sound is real. Every voice is built from oscillators, gains,
biquads, a delay with feedback and a convolver, scheduled on the audio clock. There are no samples in this repository and no audio library.
- The export is real. It renders the song through the same engine on an
OfflineAudioContext, faster than real time, and writes the WAV bytes by
hand (src/wav.ts).
- The sampler's clip is real when you record one: 2.5 seconds from the
microphone, downsampled to 22 050 Hz, stored as 16-bit PCM inside the song. Until you do, it plays a clap synthesised from noise.
- Saving is real. Songs live in SQLite under
FORGE_APP_DATA, one row per
name, and the page you get is server-rendered from the one you asked for.
- Nothing is simulated. No mock data, no placeholder audio.
Playing it
Press Play (or space). Tap a cell in the grid to write a note and tap it again to remove it; drag to paint a run of steps, or, in the piano roll, to stretch the note you just wrote. The pads under the grid play the selected track live. Everything you change is saved under the song's name a moment later — the name field renames, Save is there when you want it now.
On a phone it is the same page: the panels stack, the grid scrolls, and the canvas takes pointer events so a finger draws notes. The transport schedules 150 ms ahead of the audio clock from a worker timer, so a busy main thread does not move the beat; if the page stalls long enough that a note would land late, it is dropped rather than crowded in, and the count appears next to the frame time in the header.
Shape
| file | one concept |
|---|---|
src/project.ts | what a song is, and the only validator for one |
src/schedule.ts | beats: swing, loops, and the window the transport plays |
src/dsp.ts | seeded noise, the reverb impulse, the generated clap |
src/wav.ts | WAV bytes |
src/clip.ts | a recorded clip, small enough to store in a song |
src/music.ts | notes and frequencies |
src/db.ts | songs in SQLite |
src/bundle.ts | the client, built at boot and named by its hash |
src/pages/studio.ts | the server-rendered page |
src/server.ts | routes and the security floor |
src/tty.ts | the same song, in a terminal |
client/voices.ts | the three instruments |
client/effects.ts | one track's filter, delay and reverb |
client/engine.ts | the mixer, live and offline |
client/transport.ts | play, on the audio clock |
client/grid.ts | the sequencer and piano roll canvas |
client/render.ts | export |
The browser and the server share src/: a song is parsed by the same
parseProject in both places, so a knob cannot ask for something the store
would refuse. There are no dependencies — nothing in package.json but the
scripts.
The terminal
The exact command is in FORGE_APP_SSH when the app runs; the page prints it
at the bottom, and says the terminal is off when that variable is unset.
It opens src/tty.ts on the same database: browse the songs, open one, write
and erase notes on the grid, move the tempo, the swing and the bars, add and
remove tracks, change an instrument's kind, rename, delete. Every edit is
saved as it happens. It cannot play anything — the synths are WebAudio and
live in the browser.
What the forge sets
PORT, FORGE_APP_DATA, FORGE_APP_ENV (production or preview),
FORGE_APP_URL, FORGE_APP_COMMIT and FORGE_APP_SSH. forge.json says how
the app runs, is tested, and is reached over ssh. Production and each preview
keep their own songs, because they keep their own FORGE_APP_DATA.