Skip to content

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 loop300 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
bash
npm install plonkjs
ts
import { 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:

ts
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:

ts
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.

ts
player.layer("kick", 0, 0.8); // fade the kick out
player.tempo(1.12); // speed up, without a jump

Notes 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:

ts
new Player({ context: audio.ctx, destination: audio.bus("music") }); // debuff
new Player({ context: sound.context, destination: sound.destination }); // Phaser

What 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.