Hi, friends!
Here's a demo featuring 5 glorious channels of lo-fi audio! Making a chipbreak-style tune in Pico-8 has been in the back of my mind ever since @carlc27843 discovered the undocumented PCM channel, and now I can finally check this off my list! After almost 2 years! 😅
All the drums are samples, triggered by watching the tracker with stat(56). The samples are then fed to the PCM channel by monitoring the buffer with stat(108). I'd originally planned on building a 5-channel tracker from this demo, but I had a tough time getting the samples to sync consistently. I'm not sure if that's because of my own shortcomings, or maybe because as @zep literally said, the sync is "not perfect".
For the gfx, the star animations are also triggered by stat(56), whenever a snare sample is played. The road is just one gigantic image using a palette cycle. It looks like this:
Anyway, I had a lot of fun putting this together. I hope you like it! 😃
Special thanks to @pahammond for GEM, which helped keep this cart under the compressed limit!
Here's how this works if you want to try it!
Chopping Samples
First, you'll need to chop your samples in the DAW of your choice. Pico-8's tracker runs at 120Hz. Some conversions:
- 1 tracker tick = 1/120 second
- 1 note = whatever speed (# of tracker ticks) your sfx sequence is / 0.120 = milliseconds per note
- BPM = 7200 (# tracker ticks per minute) / # notes per beat / sfx sequence speed
eg, if your sfx sequence has a speed of 16 and each note in the sequence represents 1/4 beat, then your song is running at 112.5 BPM.
To avoid crackling, samples should end at the 0-crossing, or better, fade out the end. I applied a 10ms fade to the end of each of my samples for this song.
Serializing Samples
Next, you'll need to convert your samples to raw (headerless), monaural unsigned 8-bit 5512Hz PCM files. Audacity is a FOSS DAW that can get the job done. The samples can be any size, but it's worth keeping in mind that the PCM buffer has a max of 2048B (0.372s @ 5512Hz). At least for this demo, they should be 1024B or smaller for percussion, so you have headroom to buffer the next sample while the current one is still being consumed. My implementation here is very primitive, it will crop samples to the appropriate length, but loads them one-shot, so it can't continue to stream a sample larger than the available buffer space.
Next, you can run them through this utility cart:
The cart will serialize the PCM file into an escaped string (using @zep's escape_binary_str() and print it to stdout, your clipboard, and optionally, a specified output file.
The cart accepts the following CLI params:
- -i path to input file
- -p path to output file
pico8 -run -i "mysample.raw" kajatisuzi.p8.png |
I'm not sure if this is just an Audacity thing, but I noticed that my raw sample files always included a null character followed by a random (garbage?) character at the end. The null character makes sense because there should usually be one at the end of a file, but I'm not sure about the following garbage character. the utility cart will chop both of these characters off the end of the serialized strings since they would cause problems when fed to the PCM buffer.
Sample Playback
The load_samples() function is responsible for watching the tracker and feeding data to the PCM channel. See the cart below for a demo and more info. It requires several params, including a tracker-like set of tables. The function is "pure" so you can name them however you want, but you'll need something like this:
--serialized samples we created earlier samples = { "..." } --equivalent of tracker "sfx" sequences sequences = { { --sequence speed s = 16, --up to 32 "notes" referencing sample indexes 1, nil, 1, nil, 1, nil, 1, nil, 1, nil, 1, nil, 1, nil, 1, nil, 1, nil, 1, nil, 1, nil, 1, nil, 1, nil, 1, nil, 1, nil, 1, nil, } } --equivalent to tracker "patterns" patterns = { --references to sequence indexes 1,... } |
and then something like:
while true do --music is playing, --load samples if stat(57) then l_pat_idx, l_note_idx = load_samples(patterns, sequences, samples, stat(54), stat(56), l_pat_idx, l_note_idx) --no music, --and empty pcm buffer elseif stat(108) == 0 then --load first sample l_pat_idx, l_note_idx = load_samples(patterns, sequences, samples, 0, -1, -1, -1) music(0) end end |
If load_samples() detects a sample needs to be buffered, it will multi-return the pattern index (from the patterns table) and note index (from the sequence table) of the sample. You'll need to keep track of these as global variables, so you can pass them back to the function on the next update.
To summarize the function, the params are the above sampler tables, data about the tracker's current position (eg, from stat(54) and stat(56)), and the last pattern/note buffered by the function. It looks for the next note in the sampler tables, determines how many empty bytes need to be fed to the PCM buffer so the sample plays (hopefully) on-time, determines how long the sample can be based on when the next-next note is and/or how many available bytes remain in the buffer (eg, 2048 - stat(108)), and then sends the empty byte padding and sample to the buffer.
Worth mentioning, since this is unsigned 8-bit audio, the 0-crossing is actually 127 rather than 0! (Um, or is it 128??? 😅)
Caveats
load_samples() assumes that whatever pattern # is playing in the tracker (reported by stat(54)) has a corresponding pattern (with an index + 1) in the sampler patterns table. So, if your tracker song starts at pattern 8, your sampler patterns table will need indexes of 9+.
I didn't write any way to track repeats into the function, so if you use it like in the example cart and the tracker repeats back to a lower pattern #, the cart will just stop playing samples.
The draw/debug func in the example cart is a complete mess. Actually, the whole thing is probably a mess, lol
Anyway, I hope someone finds this useful. Please let me know if you make anything with this! 😃
You finally reveal yourself to be doing 5-channels, @ridgekuhn. I thought there was something going on there that gave your music the edge.
Can I say - marvelous. Simply marvelous. And the visuals are spot on too.
One gold star to add to your dozens of others. :) Very well done !
@dw817 lol, this is the first time, i swear! i have no idea what i'm doing w this pcm business either, i'm shocked i got it to work this well! funny enough, i think my pure tracker drums sound way better bc u lose all the high frequencies when converting samples to 5512hz. i'm not sure if i'll attempt this again, at least maybe not for drums? the whacky timing gives me an uneasy feeling when they don't hit on time. anyway, thx as always!!
Great stuff, as always @ridgekuhn 👌
I love this "interactive single" idea for PICO-8 music - very cool & helps immerse the listener into the world of the music.
I think I could make out the PCM bits on this one, but maybe that's also a good sign that they weren't too obvious?
Either way, congrats again on another cool Pico-Track! 😎👍
Oh, I saw this on mastodon first, didn't realize that you could control the car during the music. This is such a cool idea!
@Liquidream Thx so much!!! It was originally just the still image, I had thought about animating the stars/skyline as an EQ visualizer while making the song so I went ahead and did the stars. Then I was like, "hmm, the road should move". Then I realized I needed the streetlights for depth. Then I realized it was a bummer to look at if u couldn't move the car. Then I started researching how scaled sprite racers are made and realized I was getting carried away! 😂 It's just the drums on the PCM channel, tho not necessarily all percussion; the main dancefloor DnB beat and amens/think breaks are samples, the flanged toms are tracker drums.
@caranha I was wondering if anyone who saw the video would know that u could drive the car! I'm glad u found your way here!
This is fantastic. Despite the limitations of PICO-8, you have managed to carve out a distinctive style that is your own. I could listen to one of your tracks blind and know you wrote it. I think this is a fantastic accomplishment.
Amazing work with the PCM samples. Eagerly looking forward to the technical write-up. Massive fan of your work.
@biovoid Thanks so much! See the first comment above for the code and hastily written docs on how to use it. ps, I <3 Harold's Bad Day, and the sound design is super cool, like how u enable the low pass filter when he's in water. It's brilliant!
[Please log in to post a comment]