Devlog #007 — A Soundtrack That Listens
PULSE//ZERO's soundtrack ships with zero audio files — every note is synthesised from PCM at startup. It's a layered score in D minor that reads the run's intensity and breathes with it: layers fade in as the heat climbs, and event stings punctuate the moments that matter.
Devlog #006 was about the Living Grid — the reactive stage that ripples, surges and recoils with what you’re doing. This entry is about the thing playing underneath it: the soundtrack. And the two are closer cousins than they look. Both read the same signal — the run’s intensity — and both lean in when the run heats up. The grid is what the run looks like as it intensifies; the music is what it sounds like. Neither is on a timeline; both are listening.
No files, all code
The headline first: PULSE//ZERO has no audio files. Not one .wav, not one
.ogg. The entire soundtrack — menu bed, gameplay layers, the game-over sting,
even the boot bleeps — is procedurally synthesised as raw PCM in GDScript when
the game starts up, behind the Northforge splash.
Why go to that trouble? Two reasons, one practical and one creative. The practical one is that it keeps the repo honest. Audio assets are big, binary, and miserable to diff; a synthesised score is a few hundred lines of code that lives happily in git, weighs nothing to clone, and produces about 1.2 MB of generated audio in memory rather than on disk. The creative one matters more: when the music is made of code, the music can be wired directly into the game’s state. A pre-rendered track can only ever play back. A synthesised one can respond.
Under the hood, every sound is built from a small set of synthesis primitives. There’s one note function that runs a phase-accumulating oscillator — a sine, a three-harmonic saw, or a three-harmonic square — through an ADSR envelope and an optional one-pole low-pass filter. Percussion is its own family: a kick is a pitch-swept sine (190 Hz down to 44 Hz over 85 ms), hi-hats are high-passed noise, the snare is noise plus a short tone. Here’s the oscillator core, which is about as exotic as it gets:
ph += freq / SR
if ph >= 1.0: ph -= 1.0
match wv:
1: # 3-harmonic saw
smp = (sin(ph * TAU) - sin(ph2 * TAU) * 0.5 + sin(ph3 * TAU) * 0.333) * 0.60
2: # 3-harmonic square
smp = (sin(ph * TAU) + sin(ph2 * TAU) * 0.333 + sin(ph3 * TAU) * 0.20) * 0.65
_: # sine
smp = sin(ph * TAU)
That’s the whole instrument. Everything else is arrangement — which notes, which envelope, which filter cutoff — written as data and rendered once into looping buffers at boot.
Layers, not a track
The gameplay score isn’t a single piece of music. It’s six independent layers,
all in D minor and locked to the same 130 BPM grid, each one its own
AudioStreamPlayer running its own looping buffer:
- a sub bass groove on a syncopated 16th pattern,
- a drum layer — four-on-the-floor kick, snare on the backbeat, hats,
- an arp ascending and descending through Dm7,
- a pad holding the chord underneath everything,
- a lead that stabs out a D5→C5→B♭4→A4 line, and
- an overdrive layer of sub rumble and noise shimmer held in reserve.
They all play at once, always in sync. What changes is how loud each one is — and that’s the whole trick.
Intensity drives the mix
The run collapses into a single intensity value between 0 and 1 — the same idea the Adaptive Director uses to decide who shows up and how hard (we covered that brain back in Devlog #003). For the music, intensity mostly tracks your combo multiplier: as your multiplier climbs from 4 toward 12, intensity ramps from 0 to 1. Spawn-phase changes set a floor under it, so the music can’t drop below the pressure the arena is actually applying.
One function maps that number onto the mix. Each layer fades in at its own rate, so the arrangement builds rather than just getting louder:
func _apply_intensity(blend: float) -> void:
var t := maxf(_intensity, _anomaly_floor)
_fade(_p_sub, lerpf(-12.0, VDB_FULL, t), blend)
_fade(_p_drum, lerpf(-18.0, VDB_FULL, clampf(t * 3.0, 0.0, 1.0)), blend)
_fade(_p_arp, lerpf(-22.0, VDB_FULL + 1.0, clampf(t * 2.5, 0.0, 1.0)), blend)
_fade(_p_pad, VDB_PAD, blend)
_fade(_p_lead, lerpf(VDB_OFF, VDB_LEAD, clampf((t - 0.5) * 2.0, 0.0, 1.0)), blend)
The pad is the constant — it’s always there, holding the room together. The drums come up three times faster than intensity, so they’re audible early. The arp is close behind. The lead stays silent until you’re past the halfway mark, then crashes in for the back half of the curve. So a cautious run sits on bass and pad; a hot streak earns the full kit and a screaming lead. You don’t toggle the music — you play it into existence, and every fade is eased over a couple of seconds so it never snaps.
Stings for the moments that land
Intensity handles the slow build. Sharp events get their own treatment. Take a bomb, a hit, or a power-up and the overdrive layer slams in for a few seconds then fades back out — successive events just reset the timer, so a frantic stretch stays lit. Finish a directive and a short ascending sting plays over the top, synthesised on the spot from a set of semitone offsets above the tonic; richer chords read as higher rarity. And when a mini-boss arrives, the score doesn’t just nudge — intensity is pinned near maximum and the overdrive layer is held for the entire fight, so the whole mix locks into its most aggressive state until the boss departs.
This is why we cared about synthesising in the first place. A soundtrack that listens makes moments land. The drop into a mini-boss, the swell of a long combo, the gut-punch of the game-over sting descending through the key — those aren’t cues we triggered from a timeline. They’re the music reacting to the same events the rest of the game reacts to, which is exactly what couples it so tightly to the grid: both are reading the run.
Next time
We keep talking about intensity as if it only comes from how you play — your combo, your survival time. But the arena has its own opinions. Devlog #008 is about Grid Anomalies: environmental hazards and events that reshape the floor mid-run, push their own intensity floor into systems like this one, and generally make the stage a participant rather than a backdrop. As always: we build in the open.
- pulse-zero
- audio
- systems