All nine parts
Most of the soundtrack in Naijapolis was never recorded. It is composed on the fly, from a few musical rules and the state of your city, so a quiet morning in a small town sounds different from a festival night in a busy one.
Music as rules
The score is a four-chord loop in a highlife-flavoured key (C major 7, A minor 7, F major 7, G6: the kind of progression that sounds warm and a little nostalgic). On top of it, a small pseudo-random generator, seeded so that it is repeatable, writes a fresh sixteen-note melody for every bar. The notes are mostly chord tones, always within a singable range, and with small leaps, so even randomness sounds like a tune.
The patterns themselves are tiny: a list of step numbers per instrument. Rhythm that sounds complex comes from layering simple, slightly different cycles.
The mix follows the city
A function takes four facts (time of day, population, how happy people are, whether there is a festival) and returns a loudness for each layer. Nothing is switched on or off abruptly; the layers fade in and out as the numbers change.
The tempo follows the day too: a little faster at noon, slower at golden hour, slowest at night. The change is eased in, a few percent at a time, so the beat never lurches.
const TEMPO = { morning: 96, noon: 104, golden: 88, night: 72 };
// on every step: move a little of the way toward the target
bpm += (TEMPO[timeOfDay] - bpm) * 0.02;
The stumble, and the fix
This is the bug that taught us the most. Browsers give you a very precise audio clock, but timers in JavaScript are only approximately on time, especially when the page is busy drawing a 3D town. The obvious way to make music is to start a note when a timer fires. On a fast laptop that works. On a busy phone, the timer fires late, notes bunch up, and the beat stumbles.
The right idea, borrowed from every serious web audio sequencer, is to schedule ahead. A timer fires often, but its only job is to hand the audio clock the next few notes with exact start times. The audio hardware then plays them on time no matter what the page is doing.
const LOOKAHEAD = 0.7; // seconds of music to hand over in advance
function pump() { // called by setInterval every 100 ms
const now = ctx.currentTime;
if (next < now - 0.08) next = now + 0.05; // we were away (a hidden tab): skip, do not burst
while (next < now + LOOKAHEAD) {
schedule(next, step); // note start times are absolute, on the audio clock
next += 60 / bpm / 4; // one sixteenth note
step = (step + 1) % 16;
}
}
setInterval(pump, 100);
We measured this before changing anything. With only a quarter of a second of notes in hand, a busy phone delivered some of them up to 158 ms late: a whole step at 90 beats per minute. With 0.7 seconds, a stall of up to half a second goes unnoticed. The comment above the constant in the code records that measurement, so the next person does not "tidy" it back to a smaller number.
Graceful fallbacks
Instruments are loaded as a bank of sounds (kalimba, a plucked string, shaker, clave, conga, bass, kick). If the bank is not ready yet, or if the device cannot play it, the same note is played by a plain synthesiser voice instead. The music begins in the first second and improves as things load. And the music waits for the browser to allow sound (browsers refuse to play audio before the player has tapped something); a game that fights the browser loses.
Sounds from the world (rain, traffic, voices, bells) are placed in stereo according to where they are on the screen, and the music is quietly turned down ("ducked") when something important needs to be heard.
What we learned
- Decide when on the clock that cannot lie, and use the unreliable clock only to ask for more work.
- Generative music is mostly constraints. Little rules (stay in the key, move by small steps, favour chord tones) matter more than any amount of randomness.
- Write the measurement next to the constant. Future you will not remember why it is 0.7.
Next: how a player’s city leaves their phone and reaches another player, without the server ever having to believe a word it is told.