Understanding Playdate's asset files
Understanding Playdate's asset files
Hello there, It's been a long time! How have you been ? I've been really busy with the Playdate lately and I have a project I'm working on (but I'll share that in a few months) but today I wanted to shine the light on a not that widely understood part of the Playdate SDK ; The way of handling asset files. Today I'll specifically focus on the PlayDate Audio files (PDA files) since it's been where I've done most research.
Chapter 1 : Inspiration
When I first got the Playdate, it reminded me of an era of technology I was born too late to know ; the 80-2000's era of technology and of course ; Walkmans, MP3 players etc.... My first project idea was a utility suite for the Playdate which I had to put in halt for technical issues of me not understanding how to build UI with the Playdate SDK. Anyways during my research for that project I found kicooya a playdate MP3 player and I was really impressed at the fact that it could convert MP3 files to PDA files on the fly.
Piece of Trivia :
If you didn't know, the playdate can only play video/audio files that have been encoded into the .PDA (for audio) or .PDV (for video) format. This is due to the hardware limitations primarily but also SDK limitations because Panic probably didn't thought this would be a requested feature as they intended the SDK to be used for gamedev and not app creation.
Chapter 2 : Investigation starts
So I was in a call with two friends from the playdate community ; NullPointer (Sam) and Remi. And this issue just got back to me and curiosity got the best of me. Here I am now writing this blogpost because I've started investigations to finally make my dream MP3 player.
I found this blogpost about someone who had the same problem and matt
a moderator had answered with a piece of code to convert WAV
audio files to PDA
on runtime using the Playdate SDK :
function wav2pda(filename)
local fil = playdate.file
local wav = fil.open(filename, fil.kFileRead)
local datalen = fil.getSize(filename) - 44 local pda = fil.open(string.sub(filename,0,#filename-4)..".pda", fil.kFileWrite) wav:seek(44) pda:write("Playdate AUD\68\172\0\2") -- 44kHz 16 bit mono
local offset = 0 while offset < datalen do
local n = math.min(datalen-offset, 1024)
pda:write(wav:read(n)) offset += n end wav:close() pda:close()
end
local snd = playdate.sound
function playdate.AButtonDown()
local s = snd.sample.new(2, snd.kFormat16bitMono) -- 2 seconds
snd.micinput.recordToSample(s, function() s:save("test.wav") wav2pda("test.wav") end ) end
function playdate.BButtonDown()
snd.sample.new("test"):play() end
function playdate.update() end
With that we were off to a great start but I needed to understand what this code actually did.
So I decided to add comments describing step by step what this algorithm was doing and I gained some insight about how the PDA
audio files work.
function wav2pda(filename)
local fil = playdate.file
local wav = fil.open(filename, fil.kFileRead)
local datalen = fil.getSize(filename) - 44 -- Only the data, not counting the WAV header
local pda = fil.open(string.sub(filename,0,#filename-4)..".pda", fil.kFileWrite) -- the string.sub changes the extension for .wav to .pda
wav:seek(44) -- Skip the 44 bytes (WAV header)
pda:write("Playdate AUD\68\172\0\2") -- 44kHz 16 bit mono
local offset = 0
-- Reading the WAV file chunk by chunk and saving the raw values (padded from 0 to 1024) to the pda file
while offset < datalen do
local n = math.min(datalen-offset, 1024)
pda:write(wav:read(n))
offset += n
end
-- Close everything
wav:close()
pda:close()
end
local snd = playdate.sound
function playdate.AButtonDown()
local s = snd.sample.new(2, snd.kFormat16bitMono) -- 2 seconds
snd.micinput.recordToSample(s,
function()
s:save("test.wav")
wav2pda("test.wav")
end
)
end
function playdate.BButtonDown()
snd.sample.new("test"):play()
end
function playdate.update() end
Now we can see that pda
is basically a stripped-down version of WAV without the header part (the first 44 bytes).
Great! But what about that header that we add ; pda:write("Playdate AUD\68\172\0\2") -- 44kHz 16 bit mono
? Well the comment added by matt has graciously given us the key to understand how it works. We can see it starts by "Playdate AUD"
which stands for Playdate Audio. But what about those last 4 bytes in the string ?
Chapter 3 : Those last 4 bytes in the string...
Well to know I saw only one option : take a look at .pda
files generated from different audio files to make a list of what each byte sequence corresponded to. First we can take a look at how many Audio formats Playdate's audio files support. According to the Playdate SDK :
playdate.sound.sample:getFormat()
Returns the format of the sample, one of
- playdate.sound.kFormat8bitMono
- playdate.sound.kFormat8bitStereo
- playdate.sound.kFormat16bitMono
- playdate.sound.kFormat16bitStereo
Excellent.
Now my first idea is to jump into Audacity and record a sample sound and export it in multiple fashions to understand what the last 4 bytes mean. Here are my findings :
- So
\68\172\0\2
-> 44kHz 16 bit mono (playdate.sound.kFormat16bitMono
) - And
\68\172\0\5
-> 44kHz 16 bit stereo (playdate.sound.kFormat16bitStereo
) Figuring the rest of the byte sequences is left as an exercice to the reader. But here is how to figure it out : - Create a sound file
- Export it in WAV/MP3 with the kHz and format you want (8 or 16 bits, mono or stereo)
- Open the file in a hex editor (I used the online hex editor hexed.it)
- Check the last 4 bytes of the first address (on hexed.it the last 4 bytes of the 1st line)
- Remember those are in hexadecimal, you need to convert them to base 10 integers. And there you go you figured out the byte sequence for the format you used !
Conclusion : Actually doing something with this
Ok now what can we do with these things ? Well At the time of writing this I do not know but I'm sure you can find something! I believe in the vibrant Playdate dev community... they'll do their magic and create some awesome stuff. If you want to share your projects with this you can do so by sending me an email (hello@oxey405.com) or just joining my discord!