Recently, ZzFX had a significant update so I took some time to experiment with the library. After losing myself creating sound effects for a theoretical game, I wondered if I could use ZzFX to create game music too.
ZzFXM was born.
Establishing a ZzFXM format
Despite my desire to dive straight into writing a super small music player, my first job was figuring out a format that would allow me to efficiently store the song data. Songs were going to quickly eat up valuable bytes so the format had to describe the song structure and all its instruments in the smallest possible space. I set a target of squeezing 2-3 minutes of music and the playback code into 1.5k after gzip, which is just over 10% of the byte budget of a JS13k games entry.
The first version based on a stripped down version of the MOD format I was familiar with from back in my Atari ST days. MOD stores a song as indexed patterns of channels (or tracks) containing note and effect data — basically breaking a song into small repeatable chunks that are played in a sequence. The ZzFXM equivalent format looked like this:
The first array slot contains the instrument data (the ZzFX params). The second slot holds the pattern data and the third slot is the pattern sequence. Notes are stored across three array slots that define the instrument number, the note and the attenuation. This mirrors the MOD pattern format, allowing each channel to contain a mix of notes and instruments. MOD files also store effect data for each note, allowing composers to apply volume slides, vibrato, tremolo etc. Because of it’s size, ZzFXM doesn’t support these effects during playback.
With the initial format decided, the next step was to write a playback routine. I settled on a quick proof of concept that used
setInterval to step through the channel data and call
zzfx using the relevant instrument arguments, adjusting for pitch and attenuation by varying the frequency and volume values. My plan was to come back and refine the format and playback routines once I had a clearer idea of how everything fits together, and how many bytes it would consume.
I now had a format and the basis of a player but I didn’t have any songs to play. The next job was to build a tool to convert MOD files into ZzFXM format so I could create some test songs.
The first song I converted was "Popcorn". I was taken aback when I played the song — I had a real "awww-yeah" moment. Unfortunately, I played that song so many times during development that I'm now sick to death of it.
Delighted with my progress, I decided to show the "Popcorn" prototype to Frank. He was very excited to hear his sound effects library producing music and exploring the possibilites it presented. It turned out that Frank was also planning to explore music creation with ZzFX, so we decided to collaborate and build upon the prototype.
The first job was to improve the performance of the player. Generating a new set of samples every time a note needed to be played was very CPU intensive - especially for longer instruments. I opted to switch to a song renderer so the entire song could be generated as a single sample and played using the Web Audio API. The renderer had a much smaller footprint than any realtime playback routine I could come up with. It also reduced the runtime CPU load which game loops could make use of instead.
Before we could pre-render music, ZzFX needed a minor modification. It’s a purposely compact library that can generate and play a sound using a single function,
zzfx. ZzFXM needs to create instrument samples without playing them so ZzFX was split into two functions;
zzfxG, which generates sample data — and
zzfxP, which plays it. For compatibility reasons the original
zzfx function was kept but modified to call
zzfxP. A nice side-effect of this change is games can optionally pre-render their sound effects.
We had a song format and renderer. Now it was time to start reducing the footprint of both so they could be included in a JS13k game entry without blowing a huge amount of the file size budget.
First we decided to limit a channel to a single instrument to avoid having to store an instrument reference for each note we wanted to play. The ZzFXM format allows for a variable number of channels so we could still play lots of instruments at the same time. This change actually increased the size of the uncompressed song data but it did introduce repetition into the pattern data, which resulted in a smaller gzip output. Frank came up with the idea of combining the note and attenuation data into a single number, using the integer part as a note index and the decimal part as the attenuation scale, which saved even more space.
These changes allowed us to store note data in a single value, rather than the three in the original format. After gzip we were seeing 20-25% savings in our test song data compared to the original format. Here’s a comparision of the two formats storing the same four note, two channel example data:
After the format changes our demo songs fit comfortably below 1Kb — some of them came in at under 500 bytes!
At this point the ZzFXM player was in a good place. Our only gripe was the rendered output was mono. This was because ZzFX was written to generate and play single channel samples. To play music in stereo, ZzFX needed another small modification and we needed a format tweak.
zzfxP method was modified to accept multiple buffers. If the function is called with single buffer it will play as mono so sound effects render in the centre of the sound stage. If two buffers are passed, each buffer will play from a different speaker.
The ZzFXM format was changed to allow for a new panning slot for each channel. This allows each channel to be independently positioned in the stereo sound stage. Channel positions set per-pattern but panning effects are possible by configuring two channels, one full left and one full right and varying the attenuation of notes in each channel so the sound appears to move around.
Stereo playback did come with a small cost. The byte size and render time for the player increased (we're rendering twice as much data), but we felt the trade-off was worth it. Frank and I scrutinised every feature or idea to ensure it made the best use of the bytes it would need. I wrote a gzip comparison tool so we could evalute how our format changes impacted the song and player size.
JS13K Games Competition
My intention was to wrap up ZzFXM and start working my own entry once the JS13k games competition had started. Unfortunately, I hit a mental block with the theme. I work best when inspiration hits and I can run with an idea but "404" just didn’t trigger that for me — I couldn’t see myself working late into the night developing a generic game and slapping a 404 badge on it.
Since ZzFXM was lacking any mature song authoring tools, I decided to use the time to build a tracker so that other developers and musicians could (hopefully) produce content for their games.
I'd had a few attempts at writing a tracker during the early stages of the project but the format changes were hard to keep on top of, so they quickly became stale. Now we had a release, I could work from a stable format.
Time was the biggest issue for me. JS13k games had already started and I needed to get something built quickly so it could be used to compose songs for the competition. To get a head start, I opted to build the tracker on top of Svelte. In a couple of evenings I had a working tracker. A few evenings later we released the first beta.
Here are some of the JS13k games entires that used ZzFXM:
- Highway 404
- Big Champ
- Wizard with a Shotgun
- 404 Rhythm not found
- The legend of Yeti 404
Frank and I plan to continue to working on the project in our spare time. We already have plenty of ideas for improving ZzFXM and are open to suggestions and feedback, so please feel free to raise issues or submit pull requests at the GitHub repo.