Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Music

Back in Sound and music, a MUSIC block played a little song with two tracks. Now that you can make instruments, it’s time to give them a whole band to play in.

The parts of a song

A song in a MUSIC block is made of a few things:

  • TEMPO: how fast it goes, in beats a minute.
  • TIMESIG: how many beats a bar has, and which note is a beat. 4, 4 is four quarter notes, and 3, 4 is a waltz.
  • PATTERNs: a few bars each, with a name. A pattern’s length is how many beats it lasts.
  • TRACKs, inside a pattern: each one a line of notes, all playing at once.
  • The ARRANGEMENT: which patterns play, in what order.

A track says everything about itself after !!: its notes, the instrument that plays them, the channel it plays on (give each track of a pattern its own), how loud it is (volume, 0 to 100), where it sits from left to right (pan, -1 to 1), and a name, if you’d like to find it again.

SCREEN 320, 240
MUSIC "waltz" !! loop
    TEMPO 150
    TIMESIG 3, 4
    PATTERN "a" !! length(6)
        TRACK !! name("tune") channel(1) instrument(0) notes("o5 l4 c e g f2 d4")
        TRACK !! name("bass") channel(2) instrument(7) volume(70) notes("o2 l4 c e g g b > d")
    END PATTERN
    PATTERN "b" !! length(6)
        TRACK !! name("tune") channel(1) instrument(0) notes("o4 l4 b > d f e2.")
        TRACK !! name("bass") channel(2) instrument(7) volume(70) notes("o2 l4 g b > d < c e g")
    END PATTERN
    ARRANGEMENT
        PLAY "a"
        PLAY "b"
    END ARRANGEMENT
END MUSIC
PRINT musictempo("waltz"), "beats a minute,", musicbeats("waltz"), "to a bar"
PRINT musicpatterncount("waltz"), "patterns,", trackcount("waltz", "a"), "tracks in each"

// Once through: twelve beats at 150 a minute is under five seconds.
PLAYMUSIC "waltz"
ticks = 0
DO WHILE ticks < 300
    IF COOLDOWN(1) THEN ticks = ticks + 1
LOOP
150 beats a minute, 3 to a bar
2 patterns, 2 tracks in each

!! loop on MUSIC starts the song again when it gets to the end, which is what music under a game usually wants.

A beat

Here’s the drum kit from the last lesson, playing in a song. The kick, the snare and the hats each get a track, so they play at the same time.

repeat on a track plays its notes again and again until the pattern ends, so the hats only need writing once. And in the arrangement, with plays patterns alongside each other: the drums go on under the bass, then under the bass and the tune together.

SCREEN 320, 240
INSTRUMENT 210
    PERCUSSION 1
    LAYER "sine"
        SAMPLE "kick.wav" !! basenote(60)
    END LAYER
END INSTRUMENT
INSTRUMENT 211
    PERCUSSION 1
    LAYER "sine"
        SAMPLE "snare.wav" !! basenote(60)
    END LAYER
END INSTRUMENT
INSTRUMENT 212
    PERCUSSION 1
    LAYER "sine"
        SAMPLE "hat.wav" !! basenote(60)
    END LAYER
END INSTRUMENT

MUSIC "groove" !! loop
    TEMPO 110
    PATTERN "drums" !! length(4)
        TRACK !! name("kick") channel(10) instrument(210) notes("o4 l4 c r c r")
        TRACK !! name("snare") channel(11) instrument(211) notes("o4 l4 r c r c")
        TRACK !! name("hat") channel(12) instrument(212) volume(50) repeat notes("o4 l8 c")
    END PATTERN
    PATTERN "bass" !! length(4)
        TRACK !! channel(2) instrument(53) notes("o2 l8 c c > c < c e e g g")
    END PATTERN
    PATTERN "tune" !! length(4)
        TRACK !! channel(1) instrument(51) volume(60) notes("o5 l8 g e c e g4 a4")
    END PATTERN
    ARRANGEMENT
        PLAY "drums" !! repeat(2)
        PLAY "drums" !! repeat(2) with("bass")
        PLAY "drums" !! repeat(4) with("bass", "tune")
    END ARRANGEMENT
END MUSIC

PLAYMUSIC "groove"
PRINT "playing:", musicplaying()
PAUSE

Playing, stopping and pausing

PLAYMUSIC starts a song, from the beginning, and your program carries on while it plays. !! loop (or !! loop(FALSE)) on PLAYMUSIC says whether it loops this time, whatever the song says. STOPMUSIC stops it.

PAUSEMUSIC stops the song where it is, and RESUMEMUSIC carries on from there, which is just what a game’s pause screen needs. musicplaying() is TRUE while a song is playing, and FALSE once it’s stopped or paused.

SCREEN 320, 240
MUSIC "theme" !! loop
    TEMPO 140
    PATTERN "a"
        TRACK !! channel(1) instrument(0) notes("o4 l8 c e g e c e g e")
        TRACK !! channel(2) instrument(7) notes("o2 l2 c g")
    END PATTERN
    ARRANGEMENT
        PLAY "a"
    END ARRANGEMENT
END MUSIC

// A second of each, to hear it.
FUNCTION hold(ticks)
    t = 0
    DO WHILE t < ticks
        IF COOLDOWN(1) THEN t = t + 1
    LOOP
END FUNCTION

PLAYMUSIC "theme"
PRINT musicplaying()
hold(60)
PAUSEMUSIC
PRINT musicplaying()
hold(60)
RESUMEMUSIC
PRINT musicplaying()
hold(60)
STOPMUSIC
PRINT musicplaying()
1
0
1
0

Crash-tan’s Tip: A song that doesn’t loop still counts as playing once it’s come to its end, until STOPMUSIC. If you need to know it’s done, ask where it’s got to instead.

Where’s it got to?

musicposition() is how far into the song it is, and musiclength("theme") how long the whole song is, both in ticks of the song, four to a beat. One divided by the other is how far along it is, from 0 to 1. (A length can be 0, and nobody divides by 0, so the loop checks first.) Here it is as a bar across the screen:

SCREEN 320, 240
MUSIC "theme"
    TEMPO 160
    PATTERN "a"
        TRACK !! channel(1) instrument(0) notes("o4 l8 c e g e c e g e d f a f d f a f")
    END PATTERN
    ARRANGEMENT
        PLAY "a" !! repeat(4)
    END ARRANGEMENT
END MUSIC

PLAYMUSIC "theme"
length = musiclength("theme")
DO
    IF COOLDOWN(1)
        done = 0
        IF length > 0 THEN done = min(musicposition() / length, 1)
        MEMORYBUFFER "bar"
            LINE (20, 110) -> (300, 130), 8 !! box
            LINE (20, 110) -> (20 + 280 * done, 130), 10 !! box fill
        END MEMORYBUFFER
        FRAME
            CLS
            PUTIMAGE (0, 0), "bar"
            LOCATE 1, 1
            PRINT round(done * 100), "%"
        END FRAME
    END IF
LOOP

Fading

FADEMUSIC "in" starts a song from silence and brings it up over as many seconds as you say. FADEMUSIC "out" takes it down to nothing. It’s a gentle way to start a level, and a gentler way to end one than STOPMUSIC.

SCREEN 320, 240
MUSIC "theme" !! loop
    TEMPO 120
    PATTERN "a"
        TRACK !! channel(1) instrument(3) notes("o4 l2 c e g e")
    END PATTERN
    ARRANGEMENT
        PLAY "a"
    END ARRANGEMENT
END MUSIC

FADEMUSIC "in", "theme", 2
ticks = 0
DO
    IF COOLDOWN(1)
        ticks = ticks + 1
        IF ticks = 300 THEN FADEMUSIC "out", "theme", 2
        FRAME
            CLS
            LOCATE 1, 1
            IF ticks < 300
                PRINT "here it comes..."
            ELSE
                PRINT "and away it goes"
            END IF
        END FRAME
    END IF
LOOP

Changing a song as the game goes

A song isn’t fixed once it’s written. A few words change it while the game runs:

  • SET MUSIC "theme" !! tempo(180) changes its tempo, whether it loops, or its timesig.
  • SET TRACK "theme", "a", 0 !! notes("...") changes a track: the song, the pattern, then which track, counting from 0. Only what you name changes.
  • MUTE TRACK "theme", "a", 1 silences a track, and !! muted(FALSE) brings it back.
  • CREATE MUSIC PATTERN adds a new pattern, CREATE TRACK gives it tracks, and ADD ARRANGEMENT puts it on the end of the arrangement.

I play a song from a recording I make of it when it starts, so a song that’s already playing goes on as it was. What you change is heard the next time it starts. That makes the moment between two levels the time to change it: the next level’s music is the same song, faster.

SCREEN 320, 240
MUSIC "theme" !! loop
    TEMPO 120
    PATTERN "a"
        TRACK !! channel(1) instrument(0) notes("o4 l8 c e g e c e g e")
        TRACK !! channel(2) instrument(7) notes("o2 l2 c g")
    END PATTERN
    ARRANGEMENT
        PLAY "a"
    END ARRANGEMENT
END MUSIC

// Two seconds of each level's music.
FUNCTION hold(ticks)
    t = 0
    DO WHILE t < ticks
        IF COOLDOWN(1) THEN t = t + 1
    LOOP
END FUNCTION

FUNCTION start_level(level)
    SET MUSIC "theme" !! tempo(100 + level * 20)
    IF level >= 3
        SET TRACK "theme", "a", 1 !! notes("o2 l4 c c g g")
    END IF
    PLAYMUSIC "theme"
END FUNCTION

FOR level = 1 ... 3
    start_level(level)
    PRINT "level", level, "at", musictempo("theme")
    hold(120)
NEXT
PRINT tracknotes("theme", "a", 1)
STOPMUSIC
level 1 at 120
level 2 at 140
level 3 at 160
o2 l4 c c g g

Here’s a song built from nothing, as the game goes, the way a game might add a part for each boss you beat:

SCREEN 320, 240
MUSIC "boss" !! loop
    TEMPO 150
    PATTERN "base" !! length(4)
        TRACK !! channel(2) instrument(61) notes("o2 l8 c c c c d# d# f f")
    END PATTERN
    ARRANGEMENT
        PLAY "base"
    END ARRANGEMENT
END MUSIC

CREATE MUSIC PATTERN "boss", "rage", 4
CREATE TRACK "boss", "rage" !! track_name("lead") channel(1) instrument(59) notes("o5 l8 c d# g > c < g d# c4")
CREATE TRACK "boss", "rage" !! track_name("bass") channel(2) instrument(61) notes("o2 l8 c c c c d# d# f f")
ADD ARRANGEMENT "boss", "rage" !! repeat(2)
APPLY MUSIC "boss"

PRINT musicpatterncount("boss"), "patterns"
FOR step = 0 ... arrangementcount("boss") - 1
    PRINT arrangementpattern("boss", step), "x", arrangementrepeat("boss", step)
NEXT

// Once through, the base and the rage twice, then stop.
PLAYMUSIC "boss"
ticks = 0
DO WHILE ticks < 300
    IF COOLDOWN(1) THEN ticks = ticks + 1
LOOP
STOPMUSIC
2 patterns
base x 1
rage x 2

Changes wait in your program until the song is next played. APPLY MUSIC hands them to my audio engine straight away instead, without playing anything.