The Making of Coven Rush

Chapter 3

Music & Sound

A soundtrack synthesised live from a seed, sound effects tuned in the music's key, and the hunt for why the music kept cutting out.

Coven Rush has no sound files. Every kick, choir, frog and snore is synthesised in the browser as you play, and every tune is written from the run's seed. The music is data, like the art's style file. The first music arrived on the afternoon of 4 October, and builder sessions kept reworking it until the last night. Along the way they met one of the most persistent problems of the project, one Ed kept reporting from his playtests: the music cutting out.

A score made of data

The first generative engine landed on 4 October #45. It replaced a placeholder loop with three layers. The first two are pure rules code with no audio in them, so they can be tested like everything else in src/rules/:

Everything a composer would touch lives in config/music-style.json: the tempo, swing, key (A minor, aeolian), six scales, six progressions, synth patches, parts, sections and the arc. A new style is mostly new data. By the end the file held 40 patches, 38 parts and 29 sections.

THE RULES: no audio, seeded, tested WEB AUDIO The game rules/game.ts waves, boot, sieges, knockouts, legends, her place, time scale Conductor rules/musicPlan.ts a section for each 4-bar block, from the arc; builds; boot Score rules/musicScore.ts each sixteenth's notes; melodies from the run's seed, new each pass Engine audio/musicEngine.ts synthesised voices, scheduled 0.6 s ahead Beat clock rules/beat.ts 120 to 140 bpm, each ramp over 8 bars config/music-style.json the music as data 40 patches, 38 parts, 29 sections, a 9-step arc, key and progressions Music audio/music.ts near or far, muffled, crunched by damage Speakers

The proximity mix came along too: the music is louder near a playing soundsystem, muffled in the deep forest, and crunched and wobbling near damage. ?music=drop loops any one section in the game, and ?music=wave7 plays wave 7's music.

Ed's playlist

The style wasn't a guess. Ed sent a screenshot of a playlist into the music builder's session: Caramelldansen, Perturbator, Justice, Billie Eilish, Nitro Fun, Christian LΓΆffler, Worakls, a couple of phonk tracks, and two by Grimes. The builder took only the genres and recorded where each one sits in the run #45: organic melodic house and techno for the forest and clearings, dark synth and French electro for the night drive, chiptune and bubblegum electro, phonk for the finale, and Billie Eilish's dark minimal pop for the whispered breakdowns. Nothing is taken from any track.

The same afternoon, Ed named the two tracks that best set the overall mood: Grimes' "4Γ†M" and Grimes and Janelle MonΓ‘e's "Venus Fly". The style file's note sums them up as dreamy, ethereal layers crashing into hard, distorted, industrial drums with breakbeat energy. #56 went after that sound in two rounds. It started by measuring. A new tool, tools/music-lab/analyse.cjs, renders every section offline and reports loudness, six frequency bands, each part soloed, and a spectrogram. The first measurement found a dull, bass-heavy mix, with about 70% of the energy between 60 and 250 Hz and the hats about 25 dB under the kick. The usual figure is 12 to 15 dB. The mix was rebalanced by the numbers (hats about 18 dB under the kick, claps 11, leads 9, pads 12), and every section came to within about 4 dB of the others.

The second round added the mood: a synthesised choir and sung chops, a distorted kick, metal clanks, a reese bass, string stabs and breakbeats, and three new sections (industrial, breaks, and ether, a dreamy breakdown). The arc became:

WaveStepSoundbpm
0ForestChoir over marimba and a heartbeat kick120
1ClearingDeep house122
2LanternsMelodic techno with strings124
3Night driveDark synth126
4IndustrialDistorted kick, clanks, reese, stabs128
5BreakbeatBroken drums, reese, voices131
6FrenzyBreaks, a flash of chiptune134
7PhonkSliding 808, cowbell137
8FinalePhonk, breaks and industrial140

The builder was frank about its limits. It could measure levels and read spectrograms, but it couldn't hear. So it ended with a seven-question listening checklist for Ed: does the forest choir feel ethereal or cheesy, are the industrial drums hard enough, are the sung chops charming or annoying?

The beat clock

The first engine ran at a fixed 120 bpm, because the visuals pulsed to the same beat. The builder asked Ed whether it should speed up through the run. His answers came back in #49: one arc step per wave, a tempo rising from 120 to about 140 bpm, and the game's beat following the music, changing on phrase boundaries, easing smoothly and never skipping.

So the beat became a clock of its own, g.beat (src/rules/beat.ts), kept as tempo segments, each one a linear ramp. When a wave arrives, its arc step's tempo eases in over 8 bars (tempoRampBars), starting on the 4-bar line where that wave's music drops. Every ramp begins exactly where the old tempo has got to, so the beat count never jumps. Then everything that pulses was moved onto beat-time, beatTime(g.beat, time): game time as it would run at the base tempo. That covers the dancefloor tiles, the lasers, the dancing party animals, the party witches, the rune pulses and the border's sparkle. When the finale is pushing 140 bpm, the forest dances at 140 too.

Never the same pass twice

A wave lasts five minutes, about 150 bars, but each wave's loop was only 40 to 56 bars long. It played note for note two or three times. The overnight brief of 5 to 6 October asked for a 30-minute run that doesn't loop audibly. #162 made the conductor count its passes round the loop. Each new pass seeds its melodies afresh, starts its chords a step further on, and lets its "?" hits fall differently. Each arc step also gained variants, eight new sections built on the old ones (forestArp, deepWalk, darkBreaks, phonkGlide and others), taken in turn on later passes.

To check any of this, a builder needed to hear a whole run without playing one. #185 added tools/sfx/run.mjs, which renders ten minutes of a scripted playthrough offline: the boot at home, a pond, an angry legend, three waves, a partified area, a siege lost, a knockout. It reports loudness minute by minute, and how much of the music repeats itself, measured as 4-second stretches within 1 dB of an earlier one. Over the whole run, the variety work brought that from 19% to 12%. In wave 2 it went from 16% to 8%.

The same run check found the next problem. The boot played its intro as one block for up to five minutes, and its pluck melody was 96% the same every 16 bars. #186 made any block longer than 32 bars turn a new phrase every 16 bars. That brought the repetition down to about 20%.

The Boot: twelve speakers, twelve layers

A run starts with the home speaker ring powering on one stone at a time. On 6 October Ed asked for two things about how that should sound:

There be a cool 'electronic item crackling into life' sound when a runestone becomes a speaker/soundsystem.Ed, 2026-10-06
The music first starts when the first speaker appears, sounding incomplete, and becomes gradually more complete until the last of the 12 appears.Ed, 2026-10-06

#273 built the crackle (src/platform/audio/power.ts, about 1.2 s): static and pops, a capacitor's whine sliding up, a mains buzz swelling and flickering, then the buzz drops out, the cone thumps and a clean tone in the music's key rings out. The twelve stones climb the pentatonic scale in turn, so the boot plays as a phrase. The twelfth, like every wave's soundsystem, gets a fuller chord.

#277 made the music follow the boot. It stays silent until the first stone turns. Each speaker then brings in one of the intro's twelve layers on the bar line after it turns, so its crackle leads into its layer. The order is pad, choir, sub, heartbeat kick, plucks, shaker, strings, offbeat hat, snaps, voice chops, a soft lead and gated stabs. The low-pass filter opens as they arrive. Each layer is just a from value (n/12) on a part of the style's intro, so changing the order is one line. By the twelfth speaker the music is 8 dB fuller than with one.

Legend circles: the music under water

Every area has a legend sleeping in a small clearing. Ed wanted stepping into one to sound like stepping somewhere else:

The music should change when you go into a legend circle in ground mode - the current music becomes very muffled, and there's a new mysterious legend music that layers in (harmoniously) on top of it. Different species have different legend music.Ed, 2026-10-06

#255 did it in two parts. The muffle (muffled in rules/music.ts) closes the music's low-pass and drops its volume, easing in and out over about a second. The legend's own layer is written as data in the style's circle section. Every legend gets a low drone. Each voice family adds its own instruments: deep horns and a purr for the big beasts, flute hoots and bells for birds, a howling choir for canids, a hiss and a horn for the snake, croaks and a whistle for amphibians. Each species then picks its own patterns, melody, rhythm phase, starting chord tone and octave, and a test checks that all 30 sound different. The layer plays in the music's key and chords, on the engine's second output with its own long reverb, so it isn't muffled along with the rest.

Later that afternoon the legend circles began to slow time: the world outside runs at about a tenth of its speed while the witch moves at full speed. Ed wanted the music to go with it:

the music and countdown get ~10x slower ... the music audibly slows down (this is a good audio cue for the transition), the legend circle music is re-written to fit with that.Ed, 2026-10-06

#304 put the engine on game time at the game's time scale, so the bars and the wave countdown stay in step through the slowdown with no separate sync. The notes drop in pitch like a slowing tape: tapePitch is the rate raised to a power, floored so that a tenth of the speed is an octave down. Through the half-second ease, tempo and pitch fall together like a turntable braking. The legend's layer runs on its own clock at full speed, starting in step with the music and taking its chords, and it was rewritten as slow, spacious four-bar phrases of long notes. You can hear it in these: the music winding down, the layer coming in over it, and the music winding back up as she steps out.

Voices without words

The sound effects started on 5 October with #94, all made in the music's key and cued from the rules' events by sfxCues.ts, so the rules know nothing about sound. That same morning Ed gave two directions that set how the game talks:

rapid fire sounds like talking: you are invitingEd, 2026-10-05
animal attacks also sound like animal speech, higher pitched for smaller animals / levels; happy speech from happy animals, angry speech from enraged animalsEd, 2026-10-05

The answer was babble (platform/audio/babble.ts): a voiced source through a vowel's three formant filters, the mouth opening into the vowel over 25 ms, with a breath of consonant before it. Each πŸ’Œ is one syllable in the witch's voice, and each burst of three is a phrase that lifts at the end like an invitation, so holding the button sounds like happy chatter rather than a beeping weapon. Every animal attack is a burst of two to four syllables in the attacker's own voice: higher the smaller and younger it is, each species with its own pitch, formants and source wave, and the mood setting the delivery. Happy is bright and bouncy, enraged is clipped and falling with grit, wild is a grumble. At most four voices speak at once and the farthest gives way, so a fight stays readable.

Within the hour Ed asked whether you could tell what was talking. Each species' call was blended into its babble, driven by data in config/creature-voices.json. There are ten families: birds chirp and trill, canids bark and howl when they turn enraged, big beasts grunt and bellow, small ones squeak and chitter, bugs click and buzz, the snake hisses, amphibians croak, bats squeak high, whistlers whistle, and the lynx yowls. Individual species add touches of their own: the owl hoots, the raven croaks.

Legends got something else:

The legends should sound like whale song; deep and slow.Ed, 2026-10-05

So a legend's voice is a long moan gliding over seconds, a sine and its overtones through a resonant throat that rides the pitch, in a long generated reverb. Happy rises, enraged groans down, and a sleeper moans softly now and then. A day later Ed added "During the restless period, the legend should call out sadly", and #239 wrote the lament: the legend's own species call, lowered and slowed, two to four falling syllables with a sobbing vibrato. It comes more often as the legend nears anger, and it can be heard 160 m away, panned toward its clearing so it draws her there.

The mix pass

By the evening of 5 October there were 32 effects and no agreed idea of how loud any of them should be. The housekeeping round tidied them into one audio folder, split by kind #125, then measured them #126. A new tool, tools/sfx/mix.mjs, renders three scenes exactly as the game mixes them: home, a fight by a soundsystem, and the deep forest with the music far off. It renders the music alone and each sound alone, then prints each sound's short-term loudness over the music under it. The targets it set are still the rule for any change to a volume:

KinddB over the music
Stings (a soundsystem lost, a relic found)0 to +4
Telegraphs (a legend's wind-up)+3 to +6
Her cries ("ouch!")βˆ’2 to +4
πŸ’Œ chimesβˆ’2 to +1
Voicesβˆ’5 to 0
State turns (enraged, happy)βˆ’4 to +4
A chargeβˆ’4 to βˆ’3
A stun's twinklesabout βˆ’10
Ambienceβˆ’12 to βˆ’5

Most of the changes were small, but a few were large. Being knocked back went from βˆ’12 to βˆ’5.7 dB, a stun's twinkles from βˆ’18 to about βˆ’10, and the knocked-down cry from a startling +8.7 to +3.8. The planned shared master limiter was dropped once the busiest scene peaked at 0.55, well under full scale.

The overnight round filled in what was still silent #171: a legend's roar, the dancers' little party shoes tapping on the beat, a pond's lapping and frogs, a picnic's murmur and clinking cups, and the creak and record crackle of the character creator's room. Later came the party spell's scroll bursting #315 and the sea, heard as she nears the beach #301.

The afterparty

When the last soundsystem falls, there is no game-over screen. The party's over, and Ed asked for it to sound that way:

when the soundsystems and speakers are all destroyed, the dance music stops … all the animals go to sleep … We can have nice environmental music and sounds that match each area.Ed, 2026-10-06

#320 winds the music down like a stopped record: over the first 60% of the ease the engine slows and its pitch sinks toward 0.15, then the last stretch fades out and it stops scheduling. Then each area's night comes in (platform/audio/night.ts). There are seven kinds, each a soft bed in the music's key with a few sounds of its own: owls and rustling in the pines, frogs and reeds in the wet places, crickets, moths and chimes in the meadow, the sea far off at home. They crossfade over about 1.5 s at the borders. Near her the sleepers snore, a nasal rasp in and a soft whoosh out, bigger and slower for bigger animals.

The music cutting out

Nothing came back from Ed's playtests more often than this. It came back in four different forms, and each turned out to have a different cause.

  1. "The music stops playing after about 20 seconds." It hadn't stopped. Flying 20 s from home took her past music.farDist (260 m), where the proximity mix dropped the track to 8% through a 380 Hz low-pass. On a laptop's speakers that is 21.9 dB down, which sounds like silence. #151 raised the floor to 0.3 and the muffle to 1000 Hz (βˆ’10 dB as heard), and the Music Lab's CI check now fails if the far mix falls more than 15 dB below the near one.
  2. "The music stops after about two minutes ... It crackles a little and then goes quiet." When the game ran slower than the audio, the engine re-anchored every frame and scheduled the same notes again. At half speed it scheduled 680 sixteenths for 122 distinct ones, each note played about 5.6 times, until the audio thread choked. #246 made it remember the last step scheduled and never schedule it again. It also added a watchdog that resumes a suspended audio context and rebuilds the music after 3 s of unexpected silence.
  3. "smooth most of the time, with occasional freezes of about 0.5 seconds", from a different playtest note. #250 added a stall log, recording every frame of 100 ms or more with its cause. That turned out to matter for the music too.
  4. "The music cuts in and out a lot." The round-14 playtest, and the real diagnosis, below #331.
  5. "Music is still starting and stopping unexpectedly." The last pass, #352: the engine looks further ahead after a long stall, and the game can measure what it sends to the speakers.

The diagnosis

The rules advance in fixed steps, and their clock gains at most MAX_STEP (0.1 s) in a frame. A frame that takes half a second, because of baking art, garbage collection or a new place streaming in, moves the game on by only 0.1 s. The other 0.4 s is simply lost. That's deliberate: the world shouldn't leap forward after a hitch.

The music, though, followed game time. The engine kept its own idea of where game time should be on the audio clock and compared the two every frame. If they drifted more than 0.06 s apart, it re-anchored: it snapped its timeline to the game's clock, which pushed its next notes later. It also only ever scheduled 0.3 s ahead. So a half-second hitch went like this. The notes already scheduled played for 0.3 s. Then nothing was scheduled. When the frame finally ended, the engine saw the game 0.4 s behind, re-anchored, and its next note landed 0.4 s further on. Every hitch left a silence about as long as the game time it had lost, and long flights were full of hitches.

There was a second cause hiding behind the first. Near a damaged soundsystem or damaged home speakers, the music deliberately cut out completely for 0.08 to 0.38 s at random. At half damage and 60 fps that happened about once every 3 seconds.

Before #331: re-anchor on every hitch the engine snapped to game time whenever the two drifted 0.06 s apart; 0.3 s scheduled ahead frames one 0.5 s frame: the game clock gains only 0.1 s (MAX_STEP) notes heard 0.3 s already scheduled silence 0.4 s: the game time lost, re-anchored on music ahead of the game 0 0.4 s always 0: the music jumps to wherever the game clock is After #331: play on, ease back 0.6 s scheduled ahead; re-anchor only on a real jump (time going back, a pause, 6 s out of step) frames one 0.5 s frame: the game clock gains only 0.1 s (MAX_STEP) notes heard 0.6 s already scheduled: the hitch is covered no gap; afterwards each sixteenth runs up to 5% long (MusicEngine.slew) music ahead of the game 0 0.4 s closing 0.05 s a second: in step after 8 s 0s 1s 2s 3s 4s 5s 6s 7s 8s 9s 10s 0s 1s 2s 3s 4s 5s 6s 7s 8s 9s 10s audio time

The fix

The change in #331 was to stop the music waiting for the game:

The builder couldn't test this by flying in the cloud, where the software renderer's frames take one to two seconds each. So it wrote tools/music-lab/flight.cjs. It loads the built game in headless Chromium with sound, stops the game's own loop, and drives the game's real music on the real audio context at 60 frames a second. The game clock loses whatever passes MAX_STEP, as the rules do, and every 4 seconds the tool busy-waits the main thread for 0.15, 0.3, 0.5 or 1.2 s in turn. Eight stalls in 40 seconds:

BuildSeconds with nothing scheduledRe-anchorings
Before #3314.0213, a gap at every stall
After #3311.110
After #3520.71 (the first 1.2 s stall's excess; the second covered)

Everything left after #331 came from the 1.2 s stalls running past the 0.6 s scheduled ahead, which nothing short of scheduling further ahead could avoid. The changelog put it in player words: "The music no longer cuts in and out: it plays straight through hitches and only crossfades."

The last pass

Two playtests later, Ed heard it again:

Music is still starting and stopping unexpectedly. I heard it on the beach. I hear it stop on the dancefloor.Ed, round 16 playtest

#352 checked each layer in turn. The rules asked for the music without a break: a simulated 14 minutes on the dancefloor kept the volume at 1.0 throughout. The engine played it without a break: those same cues fed into the real engine gave 6,724 sixteenths with nothing unscheduled. What was left were stalls longer than 0.6 s, and arriving on the beach or the busy dancefloor is exactly where long stalls happen. Since stalls come in runs, the engine now schedules further ahead after a long frame, 1.5 times that frame and up to 2 s, then eases back down. A run of long stalls now cuts the music once rather than every time. On the beach, the party music now fades to make way for the waves.

Ed also asked a sharper question: "Is there a way for the game to know if anything is being sent to the speakers or not?" The answer was an output meter (src/platform/audio/outputMeter.ts), an analyser on everything that reaches the destination. When the output is under βˆ’60 dBFS while the mix says the music should be heard, the debug overlay shows SILENT, and the playtest log records the silence with her place, the mix, the audio context's state and the nearest main-thread stall. If Ed hears it cut again, his log will say where.

The Music Lab and the checks

The Music Lab (tools/music-lab/) has been there since #45: one self-contained page that plays the engine through the game's own mix: a pretend run, any section on a loop, distance and damage sliders, the hooks, and knobs for the style whose JSON can be copied out. It fetches nothing, so it can be published as it is. It is rebuilt with every music change.

The checks never listen; they measure. Each one exists because something went wrong once: