plonk: a whole game soundtrack in 2 kB
plonk is a music engine for web games that makes songs from code. A song is a few hundred bytes of text, the engine is about 2 kB brotli, and one compressed music loop is usually 300 kB to 1 MB. gloop, the match-3 game we're building for Poki, added 2.2 kB to its build for its entire soundtrack. Try the playground to hear it.
| Size (brotli) | |
|---|---|
| A typical compressed music loop | 300 kB to 1 MB |
| plonk's engine (esbuild, minified) | about 2.1 kB |
gloop's soundtrack, engine included (from its debuff build report) | 2.2 kB |
npm install plonkjsimport { Player } from "plonkjs";
new Player().play({
bpm: 112,
instruments: {
lead: { wave: "square", decay: 0.14, sustain: 0.35, echo: 0.3 },
kick: { wave: "sine", drop: 24, decay: 0.12, sustain: 0 },
},
sections: { a: { lead: "C5 . E5 G5 . E5 C5 .", kick: "x . . . x . . ." } },
order: ["a"],
});Why we made it
We draw everything in gloop with code: the jelly critters, the goo, the lettering. It keeps the game small and every detail tweakable. The music was the odd one out: a few synth beeps, because one real track would have been bigger than the rest of the game put together.
For Poki that size matters. The download decides how fast a player gets from a click to playing, and Poki measures exactly that. So the music had to be made the same way as the art.
Songs are text
Each step is a sixteenth note: C5 is a note, C4+E4+G4 a chord, x a drum hit, . a rest and - a held note. A song is sections of those patterns, played in order:
sections: {
a: {
lead: "C5 . E5 G5 . E5 C5 . | D5 . F5 A5 . F5 D5 .",
bass: "C3 . . C3 . . G2 . | D3 . . D3 . . A2 .",
kick: "x . . . x . . .",
},
},Short patterns repeat to fill a section, so a one-bar drum loop runs under a long melody. Instruments are oscillators or filtered noise with an envelope, plus a resonant filter that can sweep on each note, detune, a pitch drop, vibrato and a tempo-synced echo. gloop's squelchy bass is one line:
bass: { wave: "sawtooth", octave: -1, volume: 0.22, filter: { cutoff: 380, q: 9, sweep: 1400 } },Because songs are text, coding agents can write and edit them too. notes() turns a song into a list of timed notes, so an agent (or a test) can check music it can't hear.
Music that reacts
gloop has one theme and bends it. On the map it plays at 85% speed with the drums out. In a level the drums come in. At three moves left it speeds up by 12%. When the level ends the drums drop away under the jingle.
player.layer("kick", 0, 0.8); // fade the kick out
player.tempo(1.12); // speed up, without a jumpNotes are scheduled ahead on the audio clock rather than on frames, so a dropped frame in the game doesn't throw the music off.
Any engine
plonk has no dependencies. Give it your engine's audio context and a node to play into, and the music follows the game's volume and mute:
new Player({ context: audio.ctx, destination: audio.bus("music") }); // debuff
new Player({ context: sound.context, destination: sound.destination }); // PhaserWhat it isn't
It's a synth: square, sawtooth, triangle and sine waves and filtered noise. It won't play recorded instruments or audio files, and it isn't a replacement for a recorded score. For a bouncy, chiptune-flavoured soundtrack that costs almost nothing, it fits.
Open the playground, change a pattern, and hear it play. Show us what you make on Discord.