Crash-tan Crash-tan

Can you do multi-track music in code? Why, yes you can!

Crash BASIC 1.51.0 Release Notes

written by Crash-tan

YOUR MUSIC IS CODE NOW

Multi-track songs have always been something you made in the Music Editor and then played. Good songs, but finished ones — the program could start them, stop them, fade them, and that was the whole conversation.

Now a program can write a song, and rewrite it while it plays.

A MUSIC block lays a song out the way the editor does, in text you can read:

MUSIC "battle"
  TEMPO 140
  LOOP ON
  PATTERN "verse"
    TRACK CHANNEL(1) INSTRUMENT(12) VOLUME(80) NOTES "o4 c d e f"
    TRACK CHANNEL(10) VOLUME(90) REPEAT NOTES "c8 c8 r8 c8"
  END PATTERN
  ARRANGEMENT
    PLAY "verse" REPEAT 4
  END ARRANGEMENT
END MUSIC

PLAYMUSIC "battle"

A song, from nothing, with no .cbm file anywhere. Patterns hold tracks, each track is one part on one channel with its own instrument, volume and pan, and the arrangement plays those patterns in order — REPEAT to hold on one, WITH to layer several at once so a drum loop rides under a melody without merging into it.

The block edits as happily as it declares. Point it at a song your Music Editor made and it changes only what it mentions: a block that sets just TEMPO leaves every pattern and the whole arrangement exactly as you drew them. And running the same block twice is safe — re-declaring a pattern replaces its tracks rather than stacking a second copy on top, so a level restart can't quietly double every part.

AND YOU CAN REACH INTO IT MID-GAME

Whole-song statements are for setting a scene. For reacting to one, there's a statement per property:

SET MUSIC "battle" TEMPO(160)
SET TRACK "battle", "verse", 1 VOLUME(20) PAN(-0.4)
MUTE TRACK "battle", "verse", 2

SET TRACK is a partial update, and that's the point — setting a volume leaves the notes, the instrument and the pan untouched. So you can build every layer of a song up front and bring them in as the game escalates: drums when combat starts, a counter-melody when the player is close to winning, the bass dropping out when they're nearly dead. MUTE TRACK silences a part without removing it, SOLO TRACK plays only what you've soloed, and both are readable so you never have to shadow your own mixer state in variables.

Patterns and arrangements are editable too — CREATE MUSIC PATTERN, CREATE TRACK, ADD ARRANGEMENT, INSERT ARRANGEMENT, DELETE ARRANGEMENT — and a family of queries reads the whole thing back: MUSICTEMPO, TRACKCOUNT, TRACKNOTES$, ARRANGEMENTPATTERN$ and friends.

One thing to know: music is rendered ahead of being heard, so a change lands on the next PLAYMUSIC rather than reaching into a song already sounding. Set your layers, then start the track.

MUSIC THAT WRITES ITSELF

This is the part I'm most pleased with. A track's notes can come from a function instead of a string:

SET TRACK "ambient", "drift", 0 GENERATOR "MakeArp"

FUNCTION MakeArp$(pattern$, track, repeat)
  DIM notes(4) AS STRING
  notes(0) = "c"
  notes(1) = "e"
  notes(2) = "g"
  notes(3) = "b"
  seq$ = "o4 "
  FOR i = 0 TO 15
    seq$ = seq$ + notes(INT(RND * 4)) + "16"
  NEXT
  RETURN seq$
END FUNCTION

You return MML — exactly what you'd have typed into NOTES — and the song plays it. The function runs once per arrangement repeat, every time the song starts. So PLAY "chorus" REPEAT 4 gets four different passes instead of the same bar four times over, and starting the song again gets four new ones. Endless music that never quite repeats, from a dozen lines.

The repeat parameter is the pass number, which is where the musicality lives — check for the last time round and return a fill instead of the usual bar. You don't have to take all three parameters; a generator that ignores its context can be declared FUNCTION Bass$() and nothing else changes. GENERATOR "" hands the track back to written notes whenever you want it back.

YOUR GAME STARTS FASTER

Read this one if you ship music. Turning a song into sound is the expensive part — for a dense song with sampled or wavetable instruments it can take seconds — and until now every game paid for its entire music bank at startup, whether it played a note of it or not. A big bank meant a long wait on a black screen before anything happened.

That doesn't happen any more. Loading a bank is now nearly instant, and the rendering happens when the music is actually needed.

Which means you decide where the wait goes:

LOADMUSIC SONG "battle"     ' render it now, behind your loading screen
PLAYMUSIC "battle"          ' instant

LOADMUSIC waits until the song is ready, so when it returns you know the music will start on time. Run it inside an ASYNC function and the rendering happens off to the side while your loading screen keeps drawing. Name a .cbm file to bring in a per-level bank — banks merge, so a level's music adds to what you already had rather than replacing it — and add SONG to pay for just the one song the next scene needs.

Nothing breaks if you ignore all of this. A song you never preload still plays; PLAYMUSIC renders it first. What changes is when you wait: your game now starts quickly and the first play of a heavy song may take a beat, where before the wait was at launch. If a song needs to start on a precise cue, LOADMUSIC it beforehand and it will.

THE PLAYHEAD WORKS

MUSICPOSITION() and MUSICLENGTH() both used to answer 0, no matter what was playing. They answer properly now, both in ticks — four per beat — so they divide straight into each other:

total = MUSICLENGTH("battle")
IF total > 0 THEN progress = MUSICPOSITION() / total

Progress bars, cues timed to a point in the music, a boss arriving on the second chorus — all without keeping a second clock of your own and hoping it stays in step. A looping song's position cycles with the loop instead of counting up forever, a one-shot's runs on past the end of its music so you can time an outro, and a paused song holds still.

A PAUSE NOW ACTUALLY PAUSES

Speaking of which: while music was paused, its clock kept running. Nothing was audible, but time was passing as far as the song was concerned — so EVENTSON cue points could fire during the pause, against music nobody heard, and be gone when playback resumed.

Paused music now consumes no song time at all. Cues wait where they are and fire when the music reaches them, however long you leave it. Pause a game mid-song, come back, and the music picks up exactly where it left off — cues included.