Player Piano: Maple Leaf Rag

Published 2023-06-12

On this day 154 years ago, Scott Joplin was born. Happy birthday to the King of Ragtime!

Here's a rendition of his famous Maple Leaf Rag, with accompanying animation.

I've been trying to learn this piece on piano for a while. At some point I thought it might be fun to try transcribing it in PICO-8, and this is the result! Information on the music and animation is below:

I tried to stay as true to the score as possible - since I was limited to only 4 channels, I had to make creative use of instruments and effects. So even though it's all just piano, there are 5 different instruments used:

Since this piece makes heavy use of octaves in the bass and the accented parts of the melody, the detune trick was a huge help in getting all of the notes in (there are a lot of notes!). The detune emphasizes the lower note over the higher one, but I think it sounds convincing enough. Even with this trick, there were a handful of places I had to leave a note out of a chord, but I tried to leave out notes which were the least important/noticeable (eg. repeated nearby in either the bass or melody, or notes less important to the character of a chord, like prioritizing the 3rd/7th over the 5th). I think unless you know this piece really well and closely watch the animation, it would be pretty hard to notice the missing notes.

There weren't quite enough sfx slots to fit the last section of the piece, so I ended up arranging that in a separate cart and then putting it into the map data using CSTORE(). Partway through playback, I swap out some sfx that are no longer needed for the ones used in the last section.

The animation is all dynamic, based on the SFX data. Which is nice, because once I got it working I could change the sfx or add new parts and the animation would continue to reflect the music.

As for how it works, I wrote a function GETNOTES() which takes as parameters a time offset and returns a list of the notes that would be playing. I'm using STAT(54) to get the index of the currently playing pattern, and STAT(56) to get the current tick within it. With that information, I can add the time offset and use PEEK() to determine which pattern/sounds would be playing at the requested time and get the note information. By checking the pitch along with which instrument voice is playing a given note, I can figure out which keys should be pressed. For the detune notes that means playing two keys at once an octave apart. For staccato notes or notes with the fade out effect the key lets off earlier in the beat.

For the animation of the actual keys, I call GETNOTES() without a time offset to get the currently playing notes. Each of the keys is rendered with a sprite, and we can look up whether the key's note is being played to determine whether we should draw its pressed or unpressed sprite.

For the piano roll, I call GETNOTES() with a different time offset for each line, and for all the notes returned I use PSET() to change the colour at the coresponding spot on the roll (adjusting for perspective and scaling so the 88 notes of the piano span 44 horizontal pixels). Since it's all offset based on what's currently playing, doing this every frame naturally makes the dots scroll down the screen as if the roll is moving.

If you'd like to see how it works in more detail, the code is reasonable well-commented so take a peek inside :) You could probably use a similar approach to automatically synchronize things in a rhythm game.

I also think it would be cool to transcribe more pieces of piano music and make this into a series -- you could even fit multiple songs in a single cart by using PX9 compression or similar, since the current code doesn't use very many tokens/characters. But it took me quite a while to transcribe just this one piece, and styles with more sustained notes / big chords wouldn't work well with PICO-8's limitations, so I'll leave it at this for now.