Inspiration
We kept coming back to two ideas that don't usually live together: Incredibox, where anyone — even someone who has never made music — can build a beat that sounds good in seconds, and r/place, where a single shared canvas becomes a whole community's artifact. What if a Reddit post wasn't something you look at, but something you could hear and play, and what if the thing you were playing belonged to everyone at once?
The Reddit "Games with a Hook" prompt sealed it. The hook we wanted was: open the post → you instantly hear the community's current groove → tap a few pads → save → now everyone hears what you added. No install, no start screen, no "press play." The instrument is the feed.
What it does
JamPad is a collaborative 16-step music sequencer that lives directly inside a Reddit post. Each day, one post is a living \( 6 \times 16 \) beat grid:
- Open it and it's already playing a seeded drum-and-bass-and-melody groove in the day's key.
- Tap pads to place beats; tap a row label to swap that row's instrument from a library of 98 hand-tuned synth sounds (drums, bass, keys, animals pitched to the scale, beatbox voices).
- Give any single beat expression — pitch it up or down the pentatonic, add a ratchet/redoble (1–4 rapid hits), or apply a wave (vibrato / tremolo / wah).
- Hit Save and your draft merges into the shared mix and broadcasts to everyone viewing — live.
Two things keep it from ever sounding bad, and from ever getting griefed into noise. Everything is locked to one key and a minor-pentatonic scale with grid quantization (the Incredibox trick), so any combination of taps is musical. And every action costs a ficha (action point): you hold at most 4, they don't accumulate, and they refill toward 4 every 12 hours. Scarcity is both the rate-limiter and the anti-grief mechanism — removing someone else's beat is allowed (r/place-style) but it costs you.
How we built it
Stack: Devvit Web (Reddit's app platform), Phaser 4 for the crayon-on-cardboard rendering, Tone.js on Web Audio for the clock and synths, and Redis + Devvit realtime for shared state.
Sync state, not audio. Streaming audio between clients is hopeless on mobile. Instead every client runs its own Tone.js transport, aligned to a shared \( t_0 \) and BPM stored in Redis, and re-synthesizes the same grid locally. The authoritative mix is a Redis hash where each cell is a field — ${track}_${step} → the encoded beat — so placing/removing is an atomic hSet/hDel with no transactions.
Optimistic-merge collaboration. You toggle pads freely in a local draft (audible only to you). When a remote user saves, their diff merges into the shared layer underneath your draft — you hear it without losing your unsaved edits. Broadcasting only on save (not per tap) naturally batches updates under Devvit's realtime cap and rate-limits abuse in one move.
Data-driven synthesis. Rather than ship megabytes of samples (Devvit's CSP blocks external audio anyway, and 3rd-party IP is banned), all 98 sounds are procedural. Each Instrument carries params — osc, env, filterHz, noise, glide — plus optional pitch-gesture recipes (chirp, meow, laser, coin, howl) that ramp synth.frequency over the note. Melodic sounds are pitched by column via scaleNote(step) so the whole grid stays in key.
A look that fights "AI slop." Deliberately wobbly hand-drawn crayon-on-cardboard art: a bundled Gochi Hand font, corrugated paper textures generated procedurally on canvas, and every icon a hand-drawn SVG with a feTurbulence roughen filter, rasterized to Phaser textures at boot.
Challenges we ran into
The Reddit feed steals your fingers. Our nicest feature — dragging a wave bar to shape a sound — worked perfectly in fullscreen and was unusable inline. The native feed scroll is a gesture recognizer sitting above the WebView, so no amount of preventDefault, touch-action, pointer capture, or document-level guards could stop it. We tried them all on-device; the user's verdict was "caos." We ultimately removed every drag gesture and turned the wave into a button that cycles presets — the app now has zero drags and works identically inline and expanded.
Realtime that silently did nothing. For days, saves propagated to no one. Two hidden causes: permissions.realtime defaults to false in devvit.json, and — the real killer — Devvit silently drops custom channel names. realtime.send resolved without error while delivering nothing. The fix was to use the raw postId as the channel. Presence (a separate Redis heartbeat) working the whole time is what masked the outage.
Autoplay vs. browser rules. "Just play automatically" collides with the hard rule that Web Audio can't start without a user gesture. We threaded it: audio unlocks on the first pointerdown anywhere (even the swipe that scrolls past the post), a BroadcastChannel coordinates so only one post plays at a time, and iOS — which genuinely cannot start audio from a scroll — falls back to a deliberate tap plus a version-polling resync for when native realtime doesn't fire.
One wide-but-short frame for every device. Reddit fixes the post's width, so a desktop feed card is ~\( 910 \times 620 \) while a phone is tall and narrow. We made layout height-aware — sizing everything by \( s = \min(u,\ H/560) \) where \( u = W/410 \) — so the same scene fills a squat desktop frame and a tall phone without empty gaps.
Accomplishments that we're proud of
- The hook actually lands: open a post and you're inside a playable instrument with no start screen, no install, hearing the community's beat immediately.
- Real-time multiplayer music that survives the constraints of a WebView inside a native app — verified on-device with two screens updating each other live.
- 98 sounds and zero audio assets — every voice is synthesized, keeping the bundle tiny and fully license-clean.
- It literally cannot sound bad, whoever is jamming, thanks to scale-locking and quantization.
- A visual identity — crayon, cardboard, a wordmark built from musical notation — that's warm, screenshot-friendly, and unmistakably not generated.
What we learned
- In the Devvit inline feed, any drag interaction is a trap — design for taps or for expanded mode, period. This is a platform constraint, not a bug to out-clever.
- Sync state, never audio. Independent local clocks aligned to a shared \( t_0 \)/BPM beat any attempt to stream sound.
- Platform defaults bite quietly: an absent
permissionsblock is not the same as its per-property defaults, custom realtime channels vanish without error, and Phaser 4 drops fully-transparent objects from hit-testing (usealpha 0.001, not0). - Constraints are features. The single-key pentatonic that guarantees good music is the same lever that stops griefing, and saving-only broadcasts are both the batching strategy and the abuse limiter.
What's next for JamPad
- Daily seeds & a pattern archive — each day re-rolls key, sound pack, and tempo (FOMO + a return habit), and every finished loop is saved as a tiny remixable JSON, turning the archive into a UGC engine.
- Remix-to-post — replay or fork any past day's groove into a fresh post.
- Deeper expression economy — reviewing the ficha cost of pitch/ratchet/wave live with players.
- Themed sound packs (lofi, 8-bit, jungle, horror) rotating as the "pack of the day."
- Rankings & profiles — subreddit-wide leaderboards for beats placed, streaks, and each community's favorite sound.
Built With
- broadcastchannel
- canvas
- css3
- devvit
- html5
- javascript
- node.js
- phaser.js
- procedural-audio
- redis
- svg
- tonejs
- typescript
- vite
- web-audio-api
- web-workers
- webgl
Log in or sign up for Devpost to join the conversation.