Gravity Express

amazing. such cool process

MIDI: No bueno

TL;DR at the bottom of this post.

We've seen some pretty cool audio games already on playdate, and the FM-synthesis / chiptune / oldskool sound of MIDI-music fits the playdate very well, I would say. An additional advantage is that it helps to keep your game size low, as midi just comtains the score / sheet music, with several separate instruments that can be shared among all songs. Typical midi file size is in the order of tens of kilobytes. The SDK has quite a bit to offer for playing MIDI, so I decided to dive into it.

Right off the bat I discovered that simply loading a midi file as a Sequence resulted in no audio being played at all. Makes sense, considering no instruments are loaded by default. The midi player sample project is helpful here, containing both synthesis (sine, sawtooth. noise) instruments and drumkit samples.

I ended up creating an entire playdate app called MIDI Master for the purpose of tuning instruments for any given midi file. The configuration would be saved as json that could in turn be loaded and played in the game by a library called Master Player . Master Player includes 2 sampled instruments: an electronic drumkit to underpin the action and a choir (AAAAAH padum padum AAAHHH) for added drama. More sampled instruments could be added.

I got some great results in the simulator, that I posted before.
Initially, things were looking good on device as well. But when navigating more taxing parts of a level, fps would drop from the target 30 to lows of 24. Turns out 30% of cpu cycles were being eaten by the audio engine at times. Somewhat surprising because midi playback is used on lower-powered hardware as well as the 90Mhz Pentium windows PC I used to play Crazy Gravity. Sure, my music contained more than 15 tracks, and playing multiple samples at the same time would naturally be taxing. By limiting the number of instruments, I could get the audio cpu usage down to 15%. But that's still a big, noticeable difference from the 3% cpu usage I got from playing the same song converted to 64 kbps mono mp3. And that was supposed to be too cpu intensive for in-game use, according to the Inside Playdate documentation. To add insult to injury, that midi file that was just a few kilobytes in size took a full second to load by the sdk. That's a no-go, considering players are expecting to see a single loading screen; when starting the game. At this point I contemplated using a midi-parser written in lua such as this one, so that I could load it in chunks somehow. But this was also the moment where I finally stopped to think about what it was that I was getting myself into. This idea was crazy and not going to work. Could I really load chunks while playing the game without performance impact? I don't think so.
Add to all this that it might be easy to find midi-conversions of popular hit songs online, but very hard to find free-to-use original work.
I was time to kill my darling midi project and convert the midi files I had selected. Just a few clicks in Garageband and it was done. The performance gains were immediate, but I felt sad for all the work I put in and authentic sound of synthesized music I was throwing out. This was not a good day.

My approach could still work for less demanding, puzzle style games. If anyone is interested, I'll add some documentation to get you started with MIDI Master + Master Player.

TL;DR MIDI Music for playdate

Note: this conclusion is misguided. I was looking at wrong data. The new conclusion is that ADPCM is indeed the cpu-friendliest way of playing music. Gravity Express - #58 by Nino

  • loading a midi track is slow, up to a full second for complex arrangements
  • You can achieve some really good sounding results
  • Playback is very taxing on the cpu, in the range of 5-30% for a simple synth melody vs multi-channel sampled instruments.
  • The file size advantage only pays off when using only synths or when shipping 3+ tracks that reuse the same samples, as the sample sets weigh in at 1-3MB each.
  • Finding Unlicensed midi tracks that can be used in a game is hard

I think if you want to go back to a "midi like" approach at some point I would highly recommend looking into music trackers, specifically mod files. They would essentially contain midi data and PCM data all bundled together and it is something many people already work/worked with for classic game type music. Another advantage to trackers is they make the most out of very few track lanes you can easily make a song with 4 lanes in a tracker due to the ability to call on different samples at different times in each lane. I have heard amazing stuff out of even one lane + delay when someone was up for a challenge.

But yeah still it would likely take up more CPU than just loading up an mp3 or wav but there could be advantages to having the music being "live"

1 Like

Somebody has done a mod player for playdate. Sorry I can't recall the details

Referring to this Pulp project?

Maybe this?

Hey everyone, I just launched the Itch page for Gravity Express! The game itself will launch January 2023

Follow me on Itch to be notified when the game is released!


Christmas 2023 is coming 11 months early!

1 Like

I check the Dev Forum semi-regularly and always see the Gravity Express post. So happy that its finally got a release date since I can't wait to play! congrats!

1 Like

Errata: Music cpu demands

Earlier, I wrote a long post about how mp3 would only use 3% of cpu power. Well, I was wrong and it's high time to correct the misinformation.

You see, when you have your playdate connected to your computer and press cmd+D in the simulator, you get a device info dialog like this:

In the example above, an MP3 song is playing. Note how the audio part of the chart shows 2.4% cpu usage.

Let's compare that to a screenshot from the same situation without music playing:

Audio usage dropped significantly from 2.4% to 0.6%.
However, the game portion of this graph dropped much more severely, from 57% to 35%. The total impact of this MP3 song then comes to 1.8% music + 21.6% game = 23.4%

At the time I wrote my original post, I was so focused on comparing the audio parameter that I completely glossed over this huge difference. Sorry! (this can be explained because I looked at the MIDI situation first. MIDI is handled by the audio engine and would have an impact of 5-30% on the audio part of the chart with neglible impact on the game part).

From my MP3 testing, an interesting performance factor became apparant: sample rate. This is bizarre, because the whole thing about MP3 compression is that it stores frequencies, rather than samples. This is the core concept behind MP3 that makes it's files so much smaller than WAV.
However, sample rate is still a thing when decoding MP3: the sound chip needs samples to send to the speaker, so the sample rate is the frequency at which the audio samples are reconstructed. Higher frequency = higer cpu usage. The 22khz and 44khz files, however, are exactly the same size.

This can be adjusted easily in Audacity:

Updated cpu cost comparision
Song: Smooth and Cool by Nico Staf from Youtube Audio Library, 3:58 duration.

ADPCM 2.6MB: 1% game + 3% audio = 4%
MP3 32 kbit mono* 22khz 0.95MB: 16% game + 3% audio = 19%
MP3 48 kbit mono* 22khz 1.4MB: 17% game + 3% audio = 20%
MP3 48 kbit mono* 44khz 1.4MB : 21% game + 3% audio = 24%
MIDI playback: 5-30% audio

The Inside Playdate documentation was right all-along: ADPCM is the cpu-friendliest music format.
What's best for your project depends on your situation. You might even go with MP3 for the least complex levels in your game and use adpcm for the demanding levels. I was pleasantly surprised by the quality of 32kbps MP3 and decided to go with the 22khz mono variant.

* technically saved as joint stereo, meaning a single channel is played on both left and right speakers. Mono would be interpreted as playing on the left channel while the right channel is silent


Gravity Express will be available January 20th!


A very Happy New Year indeed!Because Gravity Express, the game I have been working on for the past 7 months, is DONE!

Follow me on the game's Itch page to be notified of the impending release, which will be January 20th.I promised myself I would finish this project in 2022.

Last friday, I realized that for the first time, I had a playdate game sitting on my hard drive that I would be proud of and happy with to share with the world. So I smashed that goal with 1 day to spare. Yay!In fact, my fingers are aching to just push that button and upload it to Itch to see what you all think of it. My better judgement tells me that it's probably better to wait and see whether the testers manage to break the game, chill a bit, and play some of that juicy Steam Sale goodness. So that I will be ready to watch the socials come release day.


Over the last holiday period I hunkered down to grind at those tasks that I had been putting off: creating tiny 164x46 pixel preview clips and a 56x56 thumbnails for each of the 25 levels in the game, which took about 20 minutes per level to do. The result is pretty glorious tho, even if I do say so myself.
If you aren't following the game yet, this is the time to do it so that you can play it on release day.

If you aren't following the game yet, this is the time to do it so that you can play it on release day. The link is Gravity Express by Nino [Gravity Express]


Ditching the import statement to improve boot times

As per the docs, the import statement is a function available in the Playdate SDK that is not part of standard Lua. Conversely, the require statement is in every Lua distribution except for Playdate. The import statements are not run by the playdate, but instead by the pdc compiler. import "otherfile" basically means: read otherfile.lua and paste everything you find there at the current position in the current file. This way, you as developer can keep your projects organized, while the playdate only has to run a single file to load your entire game.

I can't remember whether the rationale is explained somewhere, but there are some apparent advantages to import: we would rather wait a second for the game to load and then have a smooth experience, rather than have stutters during gameplay when gunshot.wav is loaded for the first time when you press the A button. That's the wrong kind of bullet time. File access is considered slow on playdate, so we save on the overhead of loading 50 tiny lua files when we just load the main.lua. Note: pdc gives compiled lua files the extension pdz. The playdate actually never sees any lua file, just main.pdz.

This works well for a sample games like Level1-1, which is so small that the game can pull off a seamless transition between the launch card animation and the game's start screen. Level 1-1's main.pdz weighs in at 28 kB.

Gravity Express's total compiled code is 475% the size of Level 1-1's: or 133 KB. It's not just code that gets loaded though. When running the code, images, sounds and other assets will have to be fetched from disk.

Let's have a look at the screen recording using mirror:

Here is some timing debug logging from a playdate without Mirror running (Mirror puts extra strain on the cpu, so should be eliminated from the equation)


0.8010885  hoi *
1.4860770  Start rendered

* "hoi" is "hi" in Dutch.
I wrote this line some 15 years ago. Remember that this started out as a homebrew game for the PlayStation Portable. I'm keeping it in there for nostalgic reasons

The "hoi" line is a benchmark for the earliest moment after the game is started that code is run. It is at the top of my main.lua.

We see that loading the pdz file takes 0.8 seconds, and then more loading is done until at time t=1.49 seconds, our first frame of the start screen is rendered.

Most players would find this acceptable, I think. Many Season games take a comparable time to load, albeit while showing a "loading" message on screen. What bothers me about it though, is that it breaks up the swish (launch animation) -> swoosh (start screen appears) transition. 1.4s is not long enough to get worried that the device has crashed, but it is long enough that you don't see the swish-swoosh as a single transition.

Let's also have a look at file size

main.pdz: 188 KB
other pdz's: 1.6 MB
total: 1.8 MB

That is an enormous amount of data, for compiled code. Remember that Level 1-1 only had a main.pdz of 28KB.

What is happening here is that the sdk's promise of importing lua files only once is not kept when your project structure contains subdirectories and you use import like this:

import "../credits/CreditsScreen.lua"

The lua extension is never used in the samples, but without it, pdc can't find the file in my case. The result is that code gets combined and duplicated all over the place. (we might import startScreen in main.lua and then again in the credits screen so that we can show the start screen again when exiting the credits. We now have 2 copies of the startScreen code in our pdx.)
I filed a bug for that here. I don't believe it has been addressed yet. Luckily, we can replace import by something much better and more suitable for the project, using only 8 lines of code.

I now have two reasons to look into this issue:

  • decreasing pdx file size
  • improving startup speed


Allright, let's look at that new code. I cheeckily called the function require because the name is still available and gets the proper syntax highlighting in a code editor:

-- must be placed in the root of the source directory, eg. at the top of main.lua
local run <const> =

--- All paths that have already been required
local requiredPaths <const> = {}  

--- @param sourcePath path relative to source directory  
function require(sourcePath)  
  if requiredPaths[sourcePath] then  
    print("SKIP: Already required", sourcePath)  
  print("RUN " .. sourcePath)  
  requiredPaths[sourcePath] = true  
  return run(sourcePath)  

Here is a sample usage, that only loads the settings screen when the player selects an option from the menu:

        -- this code is only executed when The Settings menu item is selected
        require "lua/settings/SettingsScreen"
        -- pushScreen is part of my Scene management system

Don't get me wrong, I still use inport to combine multiple files into one. The Startscreen imports StartView and StartViewModel, for example. From main, we require Startscreen. Main has no reference to LevelSelectScreen though, and that is the reason why main, and thus our boot time, can remain small.
The StartViewModel contains code to require LevelSelectScreen, but it is only executed when the user selects the level select option in the startscreen.

What we expect to find in our pdx file now are a main.pdx and several *Screen.pdz files. This is indeed the case. But the LevelSelectViewModel.pdz and -View are there as well. That's strange.
I think the reason is that pdc will compile all Lua files that are not imported as pdz in the pdx. The unexpected files are indeed imported, but there is no import chain going back to main.
I manually deleted the unexpected files and indeed the game still runs. So, I created a bash script to find and delete redundant files from the pdx on every build. And finally arrived at the expected numbers above:

0.3018594  hoi
0.4702041  RUN lua/start/startScreen
0.5456690  Start rendered

main.pdz: 55 KB
other pdzs: 76 KB
total: 133 KB

Avery nice result that saves 1.5 MB in file size and almost one second of boot time!
I feel it is just fast enough now to make the swish-swoosh feel like a connected animation. Judge for yourself by watching the video above. Again, remeber that screen capturing imposes an extra slowdown on the device


This thread is a goldmine of information! I had seen some mentions of require but never to this detail and explanation. Thank you for sharing the knowledge but also thank you for Gravity Express! :smiley:

1 Like

Thanks @limitlis,

These blog posts take a bit more time to write than I care to admit, and it's sometimes a bit painful to think that I could have spend the time on development... Or vacuuming my house.

So the appreciation means a lot!


Great work Nino.

Worth the effort!

1 Like

I like your require function implementation!
Under the hood, import is actually a normal lua_CFunction living in the firmware that the compiler uses to place dependencies in the same PDZ file. It's called like a normal function when decompiled.

This means it works at runtime as well! (I'm sorry if I spoiled a lot of work for you...)

The wait is finally over!

After 7 months of after-hours development, Gravity Express 1.0 has been wrapped up, transported to the launchpad and sent off to the stars and beyond.

This moment was celebrated on the Tiny Yellow Machine live show, where our host @Gant_Produx made a heroic effort to navigate a tiny triangular rocket through distant caves in search for cargo, keys and other loot. Watch the entire live show on Twitch or Youtube (link attached).

Get the win, take the cake

In the last minutes of the liveshow, a grand prize was unveiled: A Gravity Express Congratulatory Cake™️ will be sent to first player who finishes the game. No need to get all the achievements, just be the first one to see the credits roll; and a sweet surprise will be delivered to your doorstep!* (Hopefully, if the employee responsible for the delivery doesn't decide to smash their face into a one-way gate or make close contact with a cannon ball). Good luck!

*:information_source: Conditions apply. See below for instructions on how to claim your prize.

Welcome to Gravity Express, foolhardy pilot!

Gravity Express has been thoroughly tested and is ready to go. Get your copy now and claim that delicious prize!

P.S.: The :cake: is not a lie

Gravity Express Congratulatory Cake™️ Rules and Regulations

Pilot; to receive this most coveted prize, you must:

  • Have completed all 25 stages + the super secret endgame
  • Have seen the credits roll, and taken a picture of the super secret QR-code at the bottom of the credits screen.
  • Use your smartphone or other device to visit the website in the QR-code
  • Noted the time and date + your timezone when you visited the website for verification
  • Be living in a quadrant of the Galaxy where a bakery is close by. Said bakery must offer ordering through their website usng credit card or PayPal. Preferred are bakeries that offer cakes where a picture can be sent to be displayed on the cake, but this is no requirement.
  • Contact me on Discord or email to discuss further delivery details. Include the picture you took of the QR code.

It is not required to have unlocked all achievements, and simulator players are welcome to participate as well.


It's as good as it looks! The traditional controls feel right—I haven't even tried the crank yet!

I'm very interested to know what people are using after 1 hour of play.
I myself am more a believer in the d-pad indeed.
I'll create a poll on discord to find out.


No doubt in my mind that this is the slickest Playdate game so far. Top tier production!