onlimey.
← All notes

· 6 min read

Making sounds for Lime cave, with nothing but oscillators

How every drum, bass, pad and block hit in Lime cave is synthesised live in the browser with the Web Audio API, no samples. Plus a little loop maker built from the same sounds.

Lime cave has no audio files in it. Not one. Every kick, every pad, every glassy note you hear when you fly through a block is built from scratch in your browser, a few milliseconds before it plays, out of the same handful of parts: oscillators, a second of white noise, filters, and volume curves.

That started as laziness (I didn’t want to find, license or host samples) and ended up being the most fun part of the game. This is how those sounds are made. At the bottom there’s a toy, the Lime synth, which puts all of them on a step grid so you can make your own loop.

The whole toolkit

The Web Audio API gives you a graph of nodes. You make a source, run it through some processing, and connect it to the speakers. For Lime cave I only ever use these:

  • OscillatorNode: a sine, triangle, square or sawtooth wave at a pitch. Sines are pure and round, triangles a bit brighter, squares hollow and chippy, saws buzzy and full.
  • A noise buffer: one second of random numbers, made once when the audio starts. Played back through a filter, noise becomes hats, snares, claps and risers.
  • BiquadFilterNode: lowpass, highpass and bandpass filters, to shape what comes through.
  • GainNode: volume. Every single note gets its own, with a quick attack and an exponential fade. That curve is what turns a constant tone into a pluck, a thud or a swell.

Everything else is those four, stacked and wired in different ways.

Drums

The kick is a sine wave that starts at 140 Hz and dives to 42 Hz in about a tenth of a second, while its volume fades out. Your ear hears the pitch drop as the punch.

const o = ac.createOscillator();
o.frequency.setValueAtTime(140, t);
o.frequency.exponentialRampToValueAtTime(42, t + 0.12);

The snare is two things at once: noise through a bandpass filter around 1.9 kHz for the crack, and a short triangle wave falling from 210 Hz to 160 Hz for the body. A bit of it goes to the reverb. The hats are noise through a highpass at 8 kHz, gone in 40 milliseconds.

The Chip wub tune has crunchier versions. Its snare adds a clap in front, which is just three tiny bursts of noise 11 milliseconds apart, and its kick has a square wave click on top. That click turned out to matter a lot. Phone speakers can’t play anything much below 300 Hz, so a pure sine kick is simply silent on a phone. The click is what you actually hear.

Mirage has a darbuka: a doum that’s a sine dropping from 180 Hz to 95 Hz, and a tek that’s a narrow bandpass of noise at 3.4 kHz with a little 820 Hz triangle tick. Played in the maqsum rhythm under the breaks, those two sounds are enough to move the whole tune east.

Bass

The sub is a sine at the chord’s root, with a quiet triangle an octave up mixed in. Like the kick, the sine on its own disappears on small speakers. The octave keeps the bassline there.

The 808 in Badaman slides. Instead of jumping to the next note, it eases there with setTargetAtTime, which glides the pitch exponentially. It’s one line, and it’s the whole character of the sound.

The wobble in Chip wub is the most fun to look at. It’s a sawtooth and two squares through a lowpass filter with a lot of resonance, and the filter’s cutoff is driven by another oscillator, a slow sine LFO. The LFO’s speed is set from the tempo, so the wobble always locks to the beat, and it starts each note closed so the first “wub” lands on the beat rather than halfway through one.

Pads, plucks and echoes

The pads are two sawtooth waves per note, one tuned 7 cents flat and one 7 cents sharp. The tiny difference makes them beat against each other, which is what makes a pad sound wide and alive instead of thin. They go through a lowpass that opens up over the first half of the chord and closes again over the second, so each chord breathes.

The pluck is a triangle with a sine an octave above, a fast attack and a half-second fade, sent into a dotted-eighth echo. The echo is a delay node feeding back into itself through a lowpass, so each repeat is a little darker than the last, like an old tape delay.

Mirage’s glass notes go into a ping-pong echo instead: two delays that feed each other, one panned left and one right, so each repeat bounces across your headphones. The note itself is a sine that starts 35 cents flat and slides up into pitch over 70 milliseconds. You don’t hear it as a slide, just as something soft and slightly liquid.

The reverb is the cheapest trick of the lot. A convolver node needs an “impulse response”, normally a recording of a real room. Mine is 2.4 seconds of random noise that fades away. It sounds like a big soft space, and it costs one loop to make.

Block hits

The hits you make by flying through blocks are where all this comes together. Each tune has five, one for each height in the cave’s gap: low in the gap plays something low, and up by the roof plays something high. For Liquid DnB that’s a little gliding bassline, two tones, a chord stab, a quick run up the chord and a bell.

Two rules keep them sounding good however you fly:

  • They only use the chord that’s playing. Every hit is built from the current chord’s root and voicing, never a fixed note. You can’t play a wrong note, only a different one.
  • They land on the next eighth note. You never pass a block exactly in time, so the hit waits for the next eighth on the grid. It’s quantising, like a drum machine, and it’s why random flying still sounds like a melody.

While a hit plays, the backing ducks a little underneath it, so the note you made is always the loudest thing.

Keeping time

The browser’s audio clock and the page’s clock aren’t the same, and the music has to line up with blocks you can see. So the song runs on the page’s clock, and a scheduler every 30 milliseconds looks a short way ahead and books every note that falls in that window at its exact audio time. The game reads the same grid to place its blocks.

The awkward part was latency. Bluetooth headphones can play sound a quarter of a second late, which is long enough that a hit lands well after you’ve passed the block. The game asks the browser for its output delay where it can, and there’s a little tap-along test for headphones that don’t report it. Then everything simply plays that much early.

Make your own

The Lime synth uses all of these sounds, pulled out of the game into their own module. Pick a tune’s chords, add tracks, and tap steps on a 16-step grid. Drums go soft or loud, notes pick a chord tone, basses pick the root, fifth, octave or third, and the block-hit tracks pick a height in the gap, exactly like flying.

Each track has knobs for volume, tone, echo, space and octave. There are pads to fly through blocks by hand, which you can record into the loop. When you’ve got something you like, you can download it as a WAV that loops cleanly, copy a link to it, or copy it as tune code, which is the whole loop as text.

That last one is the part I’m most interested in. Every tune in the game so far has been written by hand, in code. The next step is letting a loop from the synth become a tune you can fly to.