Inspiration
I play with drum machines as a hobby, and every "make music in your browser" tool I tried fell over on the same three things: it loaded megabytes of samples, it wanted an account or a backend model, or it simply broke the moment Wi-Fi dropped. I kept wondering how far you could get with nothing but the primitives every browser already ships — oscillators, shaped noise, biquad filters and a clock. PulseGrid is my answer to that question: a complete groovebox that boots from a single HTML file, works on a plane with the radio off, and still sounds like a machine built from 808s and a TB-303.
What it does
PulseGrid is a drum machine, bass sequencer and lead sequencer in one page. Eight synthesized drum voices, a mono acid bass with a chromatic piano roll, and a detuned-saw lead that takes minor triads — all sequenced on 16/32-step grids across four pattern banks.
The feature I built for this hackathon is ✨ Vibe: type a scene ("cyberpunk boss fight", "rainy lofi study in tokyo") and a heuristic engine reads genre, mood, tempo and brightness out of the words, then generates a full drum + bass + lead pattern, and picks the BPM, swing, filter cutoff, reverb and sidechain pump to match. Same prompt, same song, every time — no API call, no model, no network.
Everything else is here too: a rule-based generator across ten styles, euclidean rhythms, mutation, song arrangement with per-bar looping, offline WAV rendering, standard MIDI import/export, shareable URL state, and a fullscreen performance visualizer with three modes. It installs as a PWA and runs with no connection at all.
How we built it
Everything is hand-written vanilla JavaScript — no framework, no bundler, no CDN.
Timing comes first. Notes are never fired from a timer. A 25 ms setTimeout wakes up and schedules whatever falls inside the next ~120 ms of the audio clock, using AudioContext.currentTime. That separates "when we think it is" from "what the sound card is doing", which is why the groove stays tight at any tempo and survives main-thread stutter.
Synthesis is pure math. Kicks are pitch-enveloped sines, snares and claps are noise bursts through band-passes with a short tonal body, the bass is a sawtooth through a high-resonance low-pass with an envelope-driven cutoff. There is not one audio file in the repository.
The formats are hand-rolled, because dependencies were off the table: a WAV writer (RIFF + 16-bit PCM fed from an OfflineAudioContext render), a MIDI writer, URL state encoding, and even the PWA icon PNGs — encoded through a small hand-written PNG encoder on top of node:zlib.
The Vibe engine hashes the prompt with FNV-1a into a 32-bit seed, feeds it to a private mulberry32 RNG, and decides the genre by per-token weighted voting over a bilingual English/Chinese lexicon. Because it is deterministic, it is testable: a zero-dependency suite stubs the DOM, loads every module in a vm sandbox and asserts that the same prompt reproduces the same beat. 43 tests, all green, gated in CI before GitHub Pages deploys.
Challenges we ran into
Bugs that make no sound. The unit tests stub the DOM, so they can never hear anything — a voice can be completely silent while every assertion passes. Two of those shipped. Clicking the bass or lead chips in Sound Design did nothing, because the audition path sent every voice through the drum trigger switch, which had no branch for either. And the dark-mood branch mapped brightness to 0.20–0.38 of the master filter curve — a 78–265 Hz low-pass, enough to mute nearly the entire mix. I found both by writing a throwaway mock AudioContext that counts createOscillator / createBufferSource calls and asserts every voice actually builds a source.
A bad value that outlived its bug. Fixing the filter maths was not enough: the muted cutoff had already been autosaved to localStorage, so anyone who had tried a dark prompt still booted into a near-silent app after the patch. The fix ships a boot-time repair that lifts any restored save below 400 Hz, while leaving hand-dragged values and share links byte-for-byte untouched.
A layout that only broke on real phones. On a 390 px viewport the page measured 847 px wide — the toolbar's utility cluster was a single non-wrapping row and the piano roll carried a min-width: 560px that no breakpoint overrode, so WAV, MIDI, Share and Export sat off-screen. I only caught it because headless Chrome has a ~512 px minimum window and was reporting a fake 390 px: the real measurement had to come from CDP's Emulation.setDeviceMetricsOverride.
Deterministic but not boring. Reproducibility is easy; keeping it musical is not. Genre scoring started as one combined regex per style, which let a stray word like "neon" drown out a clear "dnb chase". Per-token weighted voting fixed it.
What we learned
That the audio clock and the wake-up timer are two different things, and mixing them is the root of every wobbly groove. That a fully green test suite can still ship a silent instrument, so there is a class of bug that only a probe — not an assertion — can find. And that a bug is not fixed until the data it already wrote has been fixed too.
What's next
Pattern-scoped Vibe ("keep the drums, make the lead dreamy"), an FM bell and a granular pad, live collaborative rooms, a single-file embeddable widget, and full keyboard-and-screen-reader accessibility.
Log in or sign up for Devpost to join the conversation.