This is a walkthrough for how I located and extracted the music from FTA’s Modulae demo, straight from the 2mg disk image.

We’ll begin by disassembling the boot block. The boot block is the first block on disk, and is always loaded into $0800.

./disasm modulae.2mg 800 0 1 > boot.s

This command says to extract one 512-byte block starting at block 0, into memory location $800 and disassemble it.

If we analyze the disassembled boot block, we will see that the function at $08f9 loads a block from disk. Following that backward, we discover that at $08c9, it loads blocks 7—13 into RAM starting at $9400. Then it calls a function at $0a00 and passes it the address of the RAM it just loaded.

Blocks 7—13 contain the main loader, it’s a routine responsible for loading the rest of the disk into various parts of RAM. The call to $0a00 decrunches the loader. To help with that, I built a tool that can properly decrunch those blocks.

./decrunch modulae.2mg 7 6 loader

This basically extracts 6 blocks from the disk image, starting at block 7. It decrunches them, and saves the result into a file called loader.

Now we can disassemble the loader.

./disasm loader 9400 > loader.s

The disasm command will disassemble the entire file if the offset and size arguements are omitted. It also will work on a byte-level if the file passed isn’t a 2mg disk image.

We remember from the previous disassembly that these blocks should be loaded into RAM at $9400, so we pass that address to disasm.

Now we inspect the loader disassembly. After a bit of inspection, we can see that the main loading is done at $9ad6. It uses a table of addresses located at $9bad to determine which blocks to load and where in RAM to put them. It then later runs the decruncher (still at $0a00) on various blocks of RAM.

This table is the thing we care about. The first word is the starting block on disk, which is followed by a bunch of 8-byte records. The first dword of each record is the target address, the second dword is the number of 256-byte pages to load. It loads these sequentially from the disk, beginning at the starting block. It ends when a record is 0 pages long.

I built a dumptbl tool that makes it easier to parse this table into human readable format, telling us which blocks to load and where. We can then choose to inspect the various blocks, one by one and determine which ones we care about.

The table we want to dump is at $9bad, we need to calculate that position relative to the start of the file to determine its disk offset.

$9bad - $9400 = $07ad

Thus, we call dumptbl with the following:

./dumptbl loader 7ad

(Note how we pass loader which is the unpacked binary, and not loader.s which is the disassembly).

Using this table, we can inspect various blocks.

The very first entry in the table is actually interesting. It consists of 58 blocks, starting at block 16 on disk, uncrunched. If we look at this data, it’s actually the “And now, FTA Presents…” raw audio data. It’s stored as an unsigned 8-bit PCM data. It’s in mono, and should be played back at 11,025 Hz. I went ahead and slapped a .wav header onto the audio data to save it for posterity. You’ll find it in the songs/modulae folder.

I also found a music player. Starting at block 801, 27 blocks are loaded into $1000.

The music player uses a wavebank loaded into $9:0000, and music data at $a:0400. Sure enough, we can find those blocks in the table used by the main loader. The music data is crunched, the sound data isn’t.

./decrunch modulae.2mg 828 130 intro.wb raw
./decrunch modulae.2mg 958 5

Now, there should technically be two different music files in Modulae. One for the intro, and the other for the main demo. The music player doesn’t seem to reference them, so I’m guessing a second music player is actually loaded at another point. Instead of hunting for it, I decide to check the entries of the main loader. Sure enough, I find another song and wavebank.

./decrunch modulae.2mg 189 133 demo.wb raw
./decrunch modulae.2mg 162 27

And thus we have both songs extracted. The final step is to trim off the excess padding. Since the songs are padded to block boundaries, they have useless extra padding at the end. trimwb and trimmusic will calculate the proper length of the wavebank and song files and output trimmed down versions.