commit 4f1a6bb8c45bd69ab3ad1911408e8c2961299d5f Author: Sean Date: Mon Aug 21 11:12:33 2017 -0700 initial import diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..38df182 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.map +*.swp +songs/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..184d2fc --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ + Copyright (c) 2017, Sean Kasun + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d20210b --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +TS = src/es5503.ts src/handle.ts +SSTS = src/smith.ts src/player.ts +FTATS = src/fta.ts src/ftaplayer.ts + +.DELETE_ON_ERROR: + +all: smith.js fta.js + +smith.js: $(TS) $(SSTS) + tsc + +fta.js: $(TS) $(FTATS) + tsc -p tsconfig_fta.json + +check: + tslint $(TS) $(FTATS) $(SSTS) + +clean: + rm -f smith.js fta.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..c34e319 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# Javascript Soundsmith Player + +Soundsmith was a music program released in the late 80s for the Apple IIgs. It +was used by many games and demos for their music. This is an implementation of +the Soundsmith player written in Typescript. + +You can check out a live demo [here](https://seancode.com/soundsmith). + +`src/` contains the sourcecode for the player. + +`extract/` contains a bunch of commandline tools I wrote to help extract music +from demos and other software. + +`docs/` contains documentation on how Soundsmith works, as well as documentation +on how I extracted music from the trickier sources. + +I've also included a second special music player that can play older FTA music, like the +sort found in the Nucleus demo. diff --git a/docs/modulae.html b/docs/modulae.html new file mode 100644 index 0000000..605e097 --- /dev/null +++ b/docs/modulae.html @@ -0,0 +1,857 @@ + + + + + +Extracting music from Modulae + + + + + +
+
+
+

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 intro.song
+

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 demo.song
+

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.

+
+
+
+

+ + + diff --git a/docs/modulae.txt b/docs/modulae.txt new file mode 100644 index 0000000..9150422 --- /dev/null +++ b/docs/modulae.txt @@ -0,0 +1,122 @@ +Extracting music from Modulae +============================= + +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`. + +[source,shell] +---- +./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. + +[source,shell] +---- +./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. + +[source,shell] +---- +./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: + +[source,shell] +---- +./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. + +[source,shell] +---- +./decrunch modulae.2mg 828 130 intro.wb raw +./decrunch modulae.2mg 958 5 intro.song +---- + +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. + +[source,shell] +---- +./decrunch modulae.2mg 189 133 demo.wb raw +./decrunch modulae.2mg 162 27 demo.song +---- + +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. diff --git a/docs/nucleus.html b/docs/nucleus.html new file mode 100644 index 0000000..241c6f9 --- /dev/null +++ b/docs/nucleus.html @@ -0,0 +1,838 @@ + + + + + +Nucleus Demo + + + + + +
+
+
+

Unfortunately, the Nucleus demo and Photonix tool do not use the Soundsmith +format for their music. Instead they use a proprietary format. I decided to +reverse engineer this format as well and make a player for them as well.

+

We, of course, start with loading the boot block:

+
+
+
./disasm nucleus.2mg 800 0 1 > boot.s
+

Which lets us discover the loader is in blocks 7—b.

+
+
+
./disasm nucleus.2mg 9600 7 4 > loader.s
+

The loader uses a table starting at $9607, where the first word is the +starting block, the second word is the number of blocks, followed by a dword +loading address. It repeats until the starting block has the high bit +set. Nucleus does not use any compression for its data.

+

Using this, we discover the music player, which will let us discover where all +the music data and wavebanks are located. (It will also be the thing I study +the most in order to write a player).

+
+
+
./disasm nucleus.2mg 81000 333 4 > player.s
+

The sound player is more akin to MIDI than a tracker. The songs are broken into +channels, each channel controlling 4 oscillators. There aren’t any patterns, +there’s just a long list of note frequencies and note lengths for each channel. +This means each channel has its own play head, only advancing to the next note +when the note length elapses. Each channel loops independently as well.

+

The first time the player initializes, it loads a wavebank from $4/0000 into +sound RAM. Then all future initializations load a wavebank from $3/0000. +This means we have two wavebanks, one for the intro song, and the other for all +the main songs in the demo. I use the table from the loader to determine which +blocks are loaded for each memory location.

+
+
+
./decrunch nucleus.2mg 140 128 intro.wb raw
+./decrunch nucleus.2mg 12 128 main.wb raw
+

The player uses a block of data located at $8/0300 to determine all sorts of +information about the songs and their channels. I’ll be calling it songdefs.

+
+
+
./decrunch nucleus.2mg 268 2 songdefs raw
+

This file is divided into chunks. Each chunk is 256 bytes long, and contains +information about a song. So the first chunk contains information about the +intro song. The second chunk contains information about the first main song, +and so on. Using this we can determine there is 1 intro song, and 3 main songs.

+

Each chunk contains instrument data for each channel, as well as where in memory +to find the note data for that song, as well has the playback speed.

+

The word at offset $44 in the chunk determines where the note +data for that specific song is located, inside bank 8. Using this we can +extract the note data for all the songs.

+
+
+
./decrunch nucleus.2mg 284 8 intro.song raw
+./decrunch nucleus.2mg 270 14 main1.song raw
+./decrunch nucleus.2mg 292 7 main2.song raw
+./decrunch nucleus.2mg 299 2 main3.song raw
+

I’ll also divide up the songdefs file to provide easy access to each song’s +instrument data.

+
+
+
dd if=songdefs bs=256 skip=0 count=1 of=intro.inst
+dd if=songdefs bs=256 skip=1 count=1 of=main1.inst
+dd if=songdefs bs=256 skip=2 count=1 of=main2.inst
+dd if=songdefs bs=256 skip=3 count=1 of=main3.inst
+

And that’s all there is to it.

+

Photonix was extracted in a similar way.

+
+
+
+

+ + + diff --git a/docs/nucleus.txt b/docs/nucleus.txt new file mode 100644 index 0000000..2ad2a26 --- /dev/null +++ b/docs/nucleus.txt @@ -0,0 +1,95 @@ +Nucleus Demo +============ + +Unfortunately, the Nucleus demo and Photonix tool do not use the Soundsmith +format for their music. Instead they use a proprietary format. I decided to +reverse engineer this format as well and make a player for them as well. + +We, of course, start with loading the boot block: + +[source,shell] +---- +./disasm nucleus.2mg 800 0 1 > boot.s +---- + +Which lets us discover the loader is in blocks 7--b. + +[source,shell] +---- +./disasm nucleus.2mg 9600 7 4 > loader.s +---- + +The loader uses a table starting at `$9607`, where the first word is the +starting block, the second word is the number of blocks, followed by a dword +loading address. It repeats until the starting block has the high bit +set. Nucleus does not use any compression for its data. + +Using this, we discover the music player, which will let us discover where all +the music data and wavebanks are located. (It will also be the thing I study +the most in order to write a player). + +[source,shell] +---- +./disasm nucleus.2mg 81000 333 4 > player.s +---- + +The sound player is more akin to MIDI than a tracker. The songs are broken into +channels, each channel controlling 4 oscillators. There aren't any patterns, +there's just a long list of note frequencies and note lengths for each channel. +This means each channel has its own play head, only advancing to the next note +when the note length elapses. Each channel loops independently as well. + +The first time the player initializes, it loads a wavebank from `$4/0000` into +sound RAM. Then all future initializations load a wavebank from `$3/0000`. +This means we have two wavebanks, one for the intro song, and the other for all +the main songs in the demo. I use the table from the loader to determine which +blocks are loaded for each memory location. + +[source,shell] +---- +./decrunch nucleus.2mg 140 128 intro.wb raw +./decrunch nucleus.2mg 12 128 main.wb raw +---- + +The player uses a block of data located at `$8/0300` to determine all sorts of +information about the songs and their channels. I'll be calling it `songdefs`. + +[source,shell] +---- +./decrunch nucleus.2mg 268 2 songdefs raw +---- + +This file is divided into chunks. Each chunk is 256 bytes long, and contains +information about a song. So the first chunk contains information about the +intro song. The second chunk contains information about the first main song, +and so on. Using this we can determine there is 1 intro song, and 3 main songs. + +Each chunk contains instrument data for each channel, as well as where in memory +to find the note data for that song, as well has the playback speed. + +The word at offset `$44` in the chunk determines where the note +data for that specific song is located, inside bank 8. Using this we can +extract the note data for all the songs. + +[source,shell] +---- +./decrunch nucleus.2mg 284 8 intro.song raw +./decrunch nucleus.2mg 270 14 main1.song raw +./decrunch nucleus.2mg 292 7 main2.song raw +./decrunch nucleus.2mg 299 2 main3.song raw +---- + +I'll also divide up the songdefs file to provide easy access to each song's +instrument data. + +[source,shell] +---- +dd if=songdefs bs=256 skip=0 count=1 of=intro.inst +dd if=songdefs bs=256 skip=1 count=1 of=main1.inst +dd if=songdefs bs=256 skip=2 count=1 of=main2.inst +dd if=songdefs bs=256 skip=3 count=1 of=main3.inst +---- + +And that's all there is to it. + +Photonix was extracted in a similar way. diff --git a/docs/player.html b/docs/player.html new file mode 100644 index 0000000..be4ebe0 --- /dev/null +++ b/docs/player.html @@ -0,0 +1,1606 @@ + + + + + +DOC info and Player Pseudocode + + + + + +
+
+

DOC

+
+

The DOC in the IIgs is a multi-channel digital oscillator. There are +32 oscillators, operating in pairs. There is 64k of sound RAM, which holds +the wavetable. The oscillators address into soundram and determine what +sound data to send to the speaker.. the oscillators operate at a set +frequency.

+

There are four registers for controlling the DOC.

+
+
+$c03c +
+
+

+ Sound Control. This uses various bits to control the other register modes. +

+
+
+Bit 7 +
+
+

+DOC busy flag. 1 - DOC is busy +

+
+
+Bit 6 +
+
+

+DOC or SoundRAM access. 0 - DOC +

+
+
+Bit 5 +
+
+

+Address auto-increment. 1 - enabled +

+
+
+Bit 4 +
+
+

+reserved +

+
+
+Bits 3—0 +
+
+

+Master volume, 0 - low, 15 - high +

+
+
+
+
+$c03d +
+
+

+ Sound Data. This is used to read and write to and from the DOC and + SoundRAM. If auto-increment is enabled, reading or writing to this register + will auto-increment the address register. Note, when reading, the register + lags by one cycle. You’ll need to throw away the first read after modifying + the address registers. +

+
+
+$c03e +
+
+

+ Address Low. This is the address into either the DOC or the SoundRAM. +

+
+
+$c03f +
+
+

+ Address High. This is the address into SoundRAM. When accessing the DOC, + only the low byte of the address register is used. +

+
+
+
+

DOC Addresses

+

When in DOC mode, you can modify various settings by setting the low address +register to various addresses and writing and reading from the data register. +The following are the various addresses used.

+
+

Oscillator Interrupt $E0

+

Contains which oscillator triggered an interrupt.

+
+
+Bit 7 +
+
+

+ Interrupt occurred, 1 - yes +

+
+
+Bits 5—1 +
+
+

+ Oscillator number that triggered the interrupt +

+
+
+
+
+

Oscillator Enable $E1

+

The number of oscillators running. Multiply the number of desired oscillators +by two, and set. Any number from 2 to 64 is valid. 2 is the default +(1 oscillator).

+
+
+

A/D Converter $E2

+

This is the current value of the analog input.

+
+
+

Wavetable Size $C0--$DF

+

Control the size of the wavetable for each oscillator. $C0 controls +oscillator 0, $DF controls oscillator 31.

+
+
+Bits 5—3 +
+
+

+ Table size. +

+
+
+0 +
+
+

+256 +

+
+
+1 +
+
+

+512 +

+
+
+2 +
+
+

+1024 +

+
+
+3 +
+
+

+2048 +

+
+
+4 +
+
+

+4096 +

+
+
+5 +
+
+

+8192 +

+
+
+6 +
+
+

+16384 +

+
+
+7 +
+
+

+32768 +

+
+
+
+
+Bits 2—0 +
+
+

+ Address resolution. See below for the wavetable address calculation. +

+
+
+
+
+

Oscillator Control $A0--$BF

+

Control the oscillator behavior. $A0 is for oscillator 0, $BF is for +oscillator 31.

+
+
+Bits 7—4 +
+
+

+ Which hardware channel to use. +

+
+
+Bit 3 +
+
+

+ Interrupt enable, 1 - interrupts enabled +

+
+
+Bits 2—1 +
+
+

+ Oscillator mode +

+
+
+0 +
+
+

+Free Run. Starts at beginning of wavetable and repeats same + wavetable. Halts when halt bit is set, or 0 occurs in wavetable. +

+
+
+1 +
+
+

+One Shot. Start at beginning of wavetable, step through once, + stop at end of table. +

+
+
+2 +
+
+

+Sync. When even-numbered oscillator starts, the oscillator above + it will synchronize and begin simulatenously. +

+
+
+3 +
+
+

+Swap. When even-numbered oscillator reaches end of wavetable, + it resetsthe accumulator to 0, sets the halt bit, and clears the + halt bit of the oscillator above it. +

+
+
+
+
+Bit 0 +
+
+

+ Halt bit. 1 - Oscillator is halted. +

+
+
+
+
+

Wavetable Pointers $80--$9F

+

The start page of each oscillator’s wavetable. Each page is 256 bytes long. +$80 is the start page of oscillator 0, $9F is the start page of oscillator 31.

+
+
+

Oscillator Data $60--$7F

+

The last byte read fro the wavetable for each oscillator. $60 is oscillator +0, $7F is oscillator 31.

+
+
+

Volume $40--$5F

+

The oscillator’s volume. The current wavetable data byte is multiplied +by the 8-bit volume to obtain the final output level. $40 is the +volume for oscillator 0, $5F is for oscillator 31.

+
+
+

Frequency High and Low $00--$3F

+

This is a 16-bit value for each oscillator. $00 is the low byte of the +frequency for oscillator 0, $20 is the high byte for oscillator 0.

+

This determines the speed the wavetable is read from memory.

+

Output Frequency = F * SR / (2 ^ (17 + RES))

+

SR = 894.886KHz / (OSC + 2).

+

RES = Wavetable resolution

+

F = 16-bit frequency

+

OSC = number of enabled oscillators

+
+
+
+

Wavetable Address Calculation

+

Each oscillator has a 24-bit accumulator. Each time the oscillator +updates, the 16-bit value from the oscillator’s Frequency is added to +the accumulator. The result is then passed to a multiplexer to determine +the final 16-bit SoundRAM address. The Table Size, Wavetable Pointer, and +Resolution all determine how the multiplexer works. Use the following +table to determine how to calcualte the final address. The Pointer +register determines the high bits of the address, the accumulatr determines +the low bits.

+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Table SizeResolutionPointer RegAccumulator

256

7

P7—P0

A23—A16

256

6

P7—P0

A22—A15

256

5

P7—P0

A21—A14

256

256

0

P7—P0

A16—A9

512

7

P7—P1

A23—A15

512

6

P7—P1

A22—A14

512

512

0

P7—P1

A16—A8

1024

7

P7—P2

A23—A14

1024

6

P7—P2

A22—A13

1024

1024

0

P7—P2

A16—A7

2048

7

P7—P3

A23—A13

2048

6

P7—P3

A22—A12

2048

2048

0

P7—P3

A16—A6

4096

7

P7—P4

A23—A12

4096

6

P7—P4

A22—A11

4096

4096

0

P7—P4

A16—A5

8192

7

P7—P5

A23—A11

8192

6

P7—P5

A22—A10

8192

8192

0

P7—P5

A16—A4

16384

7

P7—P6

A23—A10

16384

6

P7—P6

A22—A9

16384

16384

0

P7—P6

A16—A3

32768

7

P7

A23—A9

32768

6

P7

A22—A8

32768

32768

0

P7

A16—A2

+
+

The 32 oscillators are serviced in sequence. With all oscillators +enabled, the DOC takes 38 microseconds to service all 32. 1.2 microseconds +per oscillator.

+
+
+
+
+

Player

+
+

This is pseudocode for the soundsmith music player. The pseudocode style +is basically C-style, with 68k-style word notation.

+

For example: music[8].w means read a little-endian word from the music +array, starting at byte offset 8.

+
+
+
// parse the song headers and prep audio
+void initSong() {
+  SNDCTL = CURVOL & 0xf;  // set vol, enable DAC, disable autoinc
+  // reset all oscillators to halt + freerun
+  for (int osc = 0xa0; osc < 0xc0; osc++) {
+    SNDADRL = osc;
+    SNDDAT = 1;  // halt
+  }
+
+  // load wavebank into sound RAM
+  SNDCTL = 0x60;  // enable RAM + autoinc
+  SNDADRL = 0;
+  SNDADRH = 0;  // point to beginning of sound RAM
+  // do all 64k of sound RAM
+  for (int addr = 0; addr < 0x10000; addr++) {
+    SNDDAT = wavebank[addr + 2];  // skip num inst at start of wavebank
+  }
+  playing = false;
+
+  SNDINT = 0x945c;  // = jsr $00/945c, this is soundInt()
+  SNDINTH = 0x003c;  // called whenever a channel stops
+
+  SNDCTL = 0;  // DAC, vol = 0, disable autoinc
+
+  // use oscillator 0 as a timer
+  SNDADRL = 0x0;  // Osc 0 Frequency
+  SNDDAT = 0xfa;
+  SNDADRL = 0x20;  // Osc 0 Frequency Hi
+  SNDDAT = 0;
+  SNDADRL = 0x40;  // Osc 0 Volume
+  SNDDAT = 0;  // mute the timer
+  SNDADRL = 0x80;  // Osc 0 Wavetable ptr
+  SNDDAT = 0;
+  SNDADRL = 0xc0;  // Osc 0 Wavetable size
+  SNDDAT = 0;   // 0 = 256 bytes, 0 res
+
+  SNDADRL = 0xe1;  // Enable oscillators
+  SNDDAT = 0x3c;  // 30 oscillators
+  SNDADRL = 0xa0;  // Osc 0 control
+  SNDDAT = 0x8;  // free run mode + interrupts enabled
+
+  // music + header + blockSize = effects1 table (blockSize bytes long)
+  effects1 = &music + 0x258 + music[6].w;
+  // effects1 + blockSize  = effects2 table (blockSize bytes long)
+  effects2 = effects1 + music[6].w;
+  // effects2 + blockSize = stereo table (16 words long)
+  stereoTable = effects2 + music[6].w;
+
+  // load instrument headers
+  int pos = 0;
+  numInst = wavebank[0] & 0xff;
+  for (inst = 0; inst < numInst; inst++) {
+    for (int i = 0; i < 12; i++) {
+      instdef[inst * 12 + i] = wavebank[0x10022 + pos++];
+    }
+    pos += 0x50;
+  }
+  // load compact table
+  for (int y = 0; y < 0x20; y++) {
+    compactTable[y] = wavebank[0x1005e + pos++];
+  }
+}
+
+// start playing the song
+void playSong() {
+  timer = 0;
+  songLen = music[0x1d6];
+  curRow = 0;
+  curPattern = 0;
+  rowOffset = music[0x1d8] * 64 * 14;
+  tempo = music[8].w;
+
+  int pos = 0;
+  for (int i = 0; i < 0x1e; i += 2) {
+    volumeTable[i].w = music[0x2c + pos].w;
+    pos += 0x1e;
+  }
+  playing = true;
+}
+
+// this is called whenever an oscillator halts with interrupts enabled
+void soundInt() {
+  SNDCTL &= 0x9f;  // doc, no auto inc
+  SNDADRL = 0xe0;  // oscillator interrupt
+  SNDDAT &= 0x7f;  // clear interrupt
+  uint8_t osc = (SNDDAT & 0x3e) >> 1;  // get fired oscillator
+  if (osc != 0) {  // wasn't timer
+    SNDADRL = 0xa0 + osc;  // osc control
+    if (SNDDAT & 8) {  // were interrupts enabled?
+      SNDDAT &= 0xfe;  // clear halt bit.. retrig
+    }
+    return;
+  }
+
+  if (!playing)
+    return;
+  timer++;
+  if (timer == tempo) {
+    timer = 0;
+    for (oscillator = 0; oscillator < 0xe; oscillator++) {
+      semitone = music[0x258 + rowOffset];
+      if (semitone == 0 || semitone >= 0x80) {
+        rowOffset++;
+        if (semitone == 0x80) {
+          SNDCTL &= 0x9f;  // DAC mode
+          oscaddr = (oscillator + 1) * 2;
+          SNDADRL = 0xa0 + oscaddr;  // osc control
+          SNDDAT = 1;  // halt
+          SNDADRL = 0xa0 + oscaddr + 1;  // osc control
+          SNDDAT = 1;  // halt pair
+        } else if (semitone == 0x81) {
+          curRow = 0x3f;
+        }
+      } else {
+        uint8_t fx = effects1[rowOffset];
+        uint8_t inst = fx & 0xf0;
+        if (!inst)
+          inst = prevInst[oscillator];
+        prevInst[oscillator] = inst;
+        volumeInt = volumeTable[((inst >> 4) - 1) * 2].w / 2;
+        fx &= 0xf;
+        if (fx == 0) {
+          arpeggio[oscillator] = effects2[rowOffset];
+          arpTone[oscillator] = semitone;
+        } else {
+          arpeggio[oscillator] = 0;
+          if (fx == 3) {
+            volumeInt = effects2[rowOffset] / 2;
+          } else if (fx == 6) {
+            volumeInt -= effects2[rowOffset] / 2;
+            if (volumeInt < 0)
+              volumeInt = 0;
+          } else if (fx == 5) {
+            volumeInt += effects2[rowOffset] / 2;
+            if (volumeInt >= 0x80)
+              volumeInt = 0x7f;
+          } else if (fx == 0xf) {
+            tempo = effects2[rowOffset];
+          }
+          if ((fx == 3 || fx == 5 || fx == 6) && semitone == 0) {
+            while (SNDCTL & 0x80);  // wait for DOC
+            SNDCTL = (SNDCTL | 0x20) & 0xbf;  // DOC + autoinc
+            SNDADRL = 0x40 + (oscillator + 1) * 2;  // osc volume
+            SNDDAT = volumeInt;
+            SNDDAT = volumeInt;  // pair
+          }
+        }
+
+        if (semitone) {
+          oscaddr = (oscillator + 1) * 2;
+          SNDCTL &= 0x9f;  // DOC mode
+          SNDADRL = 0xa0 + oscaddr;  // osc ctl
+          SNDDAT = (SNDDAT & 0xf7) | 1;  // halt, no interrupt
+          SNDADRL = 0xa0 + oscaddr + 1;  // osc ctl pair
+          SNDDAT = (SNDDAT & 0xf7) | 1;  // halt, no interrupt pair
+          inst = (prevInst[oscillator] >> 4) - 1;
+          if (inst < numInst) {
+            int x = inst * 12;
+            while (instruments[x].b < semitone) {
+              x += 6;
+            }
+            oscAptr = instruments[x + 1].b;
+            oscAsiz = instruments[x + 2].b;
+            oscActl = instruments[x + 3].b;
+            if (stereo) {
+              oscActl &= 0xf;
+              if (stereoTable[oscillator * 2])
+                oscActl |= 0x10;
+            }
+            while (instruments[x].b != 0x7f) {
+              x += 6;
+            }
+            x += 6;  // skip last instdef
+            while (instruments[x] < semitone) {
+              x += 6;
+            }
+            oscBptr = instruments[x + 1].b;
+            oscBsiz = instruments[x + 2].b;
+            oscBctl = instruments[x + 3].b;
+            if (stereo) {
+              oscBctl &= 0xf;
+              if (stereoTable[oscillator * 2])
+                oscBctl |= 0x10;
+            }
+            freq = freqTable[semitone * 2].w >> compactTable[inst * 2].w;
+            while (SNDCTL & 0x80);  // wait for DOC
+            SNDCTL = (SNDCTL | 0x20) & 0xbf;  // DOC + autoinc
+            SNDADRL = oscaddr;  // osc freq lo
+            SNDDAT = freq;
+            SNDDAT = freq;  // pair
+            SNDADRL = 0x20 + oscaddr;  // osc freq hi
+            SNDDAT = freq >> 8;
+            SNDDAT = freq >> 8;  // pair
+            SNDADRL = 0x40 + oscaddr;  // osc volume
+            SNDDAT = volumeConversion[volumeInt];
+            SNDDAT = volumeConversion[volumeInt];  // pair
+            SNDADRL = 0x80 + oscaddr;  // osc wavetable ptr
+            SNDDAT = oscAptr;
+            SNDDAT = oscBptr;  // pair
+            SNDADRL = 0xc0 + oscaddr;  // osc wavetable size
+            SNDDAT = oscAsiz;
+            SNDDAT = oscBsiz;  // pair
+            SNDADRL = 0xa0 + oscaddr; // osc ctl
+            SNDDAT = oscActl;
+            SNDDAT = oscBctl;  // pair
+          }
+        }
+        rowOffset++;
+      }
+    }
+    curRow++;
+    if (curRow < 0x40)
+      return;
+    // advance pattern
+    curRow = 0;
+    curPattern++;
+    if (curPattern < songLen) {
+      rowOffset = music[0x1d8 + curPattern] * 64 * 14;
+    } else {  // stopped
+      playing = false;
+    }
+    return;
+  } else {  // between notes.. apply arpeggios
+    for (oscillator = 0; oscillator < 0xe; oscillator++) {
+      if (arpeggio[oscillator]) {
+        switch (timer % 6) {
+          case 1: case 4:
+            arpTone[oscillator] += arpeggio[oscillator] >> 4;
+            break;
+          case 2: case 5:
+            arpTone[oscillator] += arpeggio[oscillator] & 0xf;
+            break;
+          case 0: case 3:
+            arpTone[oscillator] -= arpeggio[oscillator] >> 4;
+            arpTone[oscillator] -= arpeggio[oscillator] & 0xf;
+            break;
+        }
+        freq = freqTable[arpTone[oscillator] * 2].w >> compactTable[oscillator *
+2].w;
+        oscaddr = (oscillator + 1) * 2;
+        while (SNDCTL & 0x80);  // wait for DOC
+        SNDCTL = (SNDCTL | 0x20) & 0xbf;  // DOC + autoinc
+        SNDADRL = oscaddr;  // freq lo
+        SNDDAT = freq;
+        SNDDAT = freq;  // pair
+        SNDADRL = 0x20 + oscaddr;  // freq hi
+        SNDDAT = freq >> 8;
+        SNDDAT = freq >> 8;  // pair
+      }
+    }
+  }
+}
+
+uint8_t volumeConversion[] = {
+  0x00, 0x02, 0x04, 0x05, 0x06, 0x07, 0x09, 0x0a,  // 0
+  0x0c, 0x0d, 0x0f, 0x10, 0x12, 0x13, 0x15, 0x16,  // 8
+  0x18, 0x19, 0x1b, 0x1c, 0x1e, 0x1f, 0x21, 0x22,  // 10
+  0x24, 0x25, 0x27, 0x28, 0x2a, 0x2b, 0x2d, 0x2e,  // 18
+  0x30, 0x31, 0x33, 0x34, 0x36, 0x37, 0x39, 0x3a,  // 20
+  0x3c, 0x3d, 0x3f, 0x40, 0x42, 0x43, 0x45, 0x46,  // 28
+  0x48, 0x49, 0x4b, 0x4c, 0x4e, 0x4f, 0x51, 0x52,  // 30
+  0x54, 0x55, 0x57, 0x58, 0x5a, 0x5b, 0x5d, 0x5e,  // 38
+  0x60, 0x61, 0x63, 0x64, 0x66, 0x67, 0x69, 0x6a,  // 40
+  0x6c, 0x6d, 0x6f, 0x70, 0x72, 0x73, 0x75, 0x76,  // 48
+  0x78, 0x79, 0x7b, 0x7c, 0x7e, 0x7f, 0x81, 0x82,  // 50
+  0x84, 0x85, 0x87, 0x88, 0x8a, 0x8b, 0x8d, 0x8e,  // 58
+  0x90, 0x91, 0x93, 0x94, 0x96, 0x97, 0x99, 0x9a,  // 60
+  0x9c, 0x9d, 0x9f, 0xa0, 0xa2, 0xa3, 0xa5, 0xa6,  // 68
+  0xa8, 0xa9, 0xab, 0xac, 0xae, 0xaf, 0xb1, 0xb2,  // 70
+  0xb4, 0xb5, 0xb7, 0xb8, 0xba, 0xbb, 0xbe, 0xc0  // 78
+};
+uint16_t freqTable[] = {
+  0x0000, 0x0016, 0x0017, 0x0018, 0x001a, 0x001b, 0x001d, 0x001e,
+  0x0020, 0x0022, 0x0024, 0x0026, 0x0029, 0x002b, 0x002e, 0x0031,
+  0x0033, 0x0036, 0x003a, 0x003d, 0x0041, 0x0045, 0x0049, 0x004d,
+  0x0052, 0x0056, 0x005c, 0x0061, 0x0067, 0x006d, 0x0073, 0x007a,
+  0x0081, 0x0089, 0x0091, 0x009a, 0x00a3, 0x00ad, 0x00b7, 0x00c2,
+  0x00ce, 0x00d9, 0x00e6, 0x00f4, 0x0102, 0x0112, 0x0122, 0x0133,
+  0x0146, 0x015a, 0x016f, 0x0184, 0x019b, 0x01b4, 0x01ce, 0x01e9,
+  0x0206, 0x0225, 0x0246, 0x0269, 0x028d, 0x02b4, 0x02dd, 0x0309,
+  0x0337, 0x0368, 0x039c, 0x03d3, 0x040d, 0x044a, 0x048c, 0x04d1,
+  0x051a, 0x0568, 0x05ba, 0x0611, 0x066e, 0x06d0, 0x0737, 0x07a5,
+  0x081a, 0x0895, 0x0918, 0x09a2, 0x0a35, 0x0ad0, 0x0b75, 0x0c23,
+  0x0cdc, 0x0d9f, 0x0e6f, 0x0f4b, 0x1033, 0x112a, 0x122f, 0x1344,
+  0x1469, 0x15a0, 0x16e9, 0x1846, 0x19b7, 0x1b3f, 0x1cde, 0x1e95,
+  0x2066, 0x2254, 0x245e, 0x2688
+};
+
+
+
+

+ + + diff --git a/docs/player.txt b/docs/player.txt new file mode 100644 index 0000000..21bca82 --- /dev/null +++ b/docs/player.txt @@ -0,0 +1,482 @@ +DOC info and Player Pseudocode +============================== + +== DOC + +The DOC in the IIgs is a multi-channel digital oscillator. There are +32 oscillators, operating in pairs. There is 64k of sound RAM, which holds +the wavetable. The oscillators address into soundram and determine what +sound data to send to the speaker.. the oscillators operate at a set +frequency. + +There are four registers for controlling the DOC. + +`$c03c`:: + Sound Control. This uses various bits to control the other register modes. + Bit 7::: DOC busy flag. 1 - DOC is busy + Bit 6::: DOC or SoundRAM access. 0 - DOC + Bit 5::: Address auto-increment. 1 - enabled + Bit 4::: reserved + Bits 3--0::: Master volume, 0 - low, 15 - high +`$c03d`:: + Sound Data. This is used to read and write to and from the DOC and + SoundRAM. If auto-increment is enabled, reading or writing to this register + will auto-increment the address register. Note, when reading, the register + lags by one cycle. You'll need to throw away the first read after modifying + the address registers. +`$c03e`:: + Address Low. This is the address into either the DOC or the SoundRAM. +`$c03f`:: + Address High. This is the address into SoundRAM. When accessing the DOC, + only the low byte of the address register is used. + +=== DOC Addresses + +When in DOC mode, you can modify various settings by setting the low address +register to various addresses and writing and reading from the data register. +The following are the various addresses used. + +==== Oscillator Interrupt $E0 + +Contains which oscillator triggered an interrupt. + +Bit 7:: + Interrupt occurred, 1 - yes +Bits 5--1:: + Oscillator number that triggered the interrupt + +==== Oscillator Enable $E1 + +The number of oscillators running. Multiply the number of desired oscillators +by two, and set. Any number from 2 to 64 is valid. 2 is the default +(1 oscillator). + +==== A/D Converter $E2 + +This is the current value of the analog input. + +==== Wavetable Size $C0--$DF + +Control the size of the wavetable for each oscillator. $C0 controls +oscillator 0, $DF controls oscillator 31. + +Bits 5--3:: + Table size. + 0::: 256 + 1::: 512 + 2::: 1024 + 3::: 2048 + 4::: 4096 + 5::: 8192 + 6::: 16384 + 7::: 32768 +Bits 2--0:: + Address resolution. See below for the wavetable address calculation. + +==== Oscillator Control $A0--$BF + +Control the oscillator behavior. $A0 is for oscillator 0, $BF is for +oscillator 31. + +Bits 7--4:: + Which hardware channel to use. +Bit 3:: + Interrupt enable, 1 - interrupts enabled +Bits 2--1:: + Oscillator mode + 0::: Free Run. Starts at beginning of wavetable and repeats same + wavetable. Halts when halt bit is set, or 0 occurs in wavetable. + 1::: One Shot. Start at beginning of wavetable, step through once, + stop at end of table. + 2::: Sync. When even-numbered oscillator starts, the oscillator above + it will synchronize and begin simulatenously. + 3::: Swap. When even-numbered oscillator reaches end of wavetable, + it resetsthe accumulator to 0, sets the halt bit, and clears the + halt bit of the oscillator above it. +Bit 0:: + Halt bit. 1 - Oscillator is halted. + +==== Wavetable Pointers $80--$9F + +The start page of each oscillator's wavetable. Each page is 256 bytes long. +$80 is the start page of oscillator 0, $9F is the start page of oscillator 31. + +==== Oscillator Data $60--$7F + +The last byte read fro the wavetable for each oscillator. $60 is oscillator +0, $7F is oscillator 31. + +==== Volume $40--$5F + +The oscillator's volume. The current wavetable data byte is multiplied +by the 8-bit volume to obtain the final output level. $40 is the +volume for oscillator 0, $5F is for oscillator 31. + +==== Frequency High and Low $00--$3F + +This is a 16-bit value for each oscillator. $00 is the low byte of the +frequency for oscillator 0, $20 is the high byte for oscillator 0. + +This determines the speed the wavetable is read from memory. + +Output Frequency = F * SR / (2 ^ (17 + RES)) + +SR = 894.886KHz / (OSC + 2). + +RES = Wavetable resolution + +F = 16-bit frequency + +OSC = number of enabled oscillators + +=== Wavetable Address Calculation + +Each oscillator has a 24-bit accumulator. Each time the oscillator +updates, the 16-bit value from the oscillator's Frequency is added to +the accumulator. The result is then passed to a multiplexer to determine +the final 16-bit SoundRAM address. The Table Size, Wavetable Pointer, and +Resolution all determine how the multiplexer works. Use the following +table to determine how to calcualte the final address. The Pointer +register determines the high bits of the address, the accumulatr determines +the low bits. + +[width="50%",options="header"] +|========================== +|Table Size|Resolution|Pointer Reg|Accumulator +|256|7|P7--P0|A23--A16 +|256|6|P7--P0|A22--A15 +|256|5|P7--P0|A21--A14 +|256|...|...|... +|256|0|P7--P0|A16--A9 +|512|7|P7--P1|A23--A15 +|512|6|P7--P1|A22--A14 +|512|...|...|... +|512|0|P7--P1|A16--A8 +|1024|7|P7--P2|A23--A14 +|1024|6|P7--P2|A22--A13 +|1024|...|...|... +|1024|0|P7--P2|A16--A7 +|2048|7|P7--P3|A23--A13 +|2048|6|P7--P3|A22--A12 +|2048|...|...|... +|2048|0|P7--P3|A16--A6 +|4096|7|P7--P4|A23--A12 +|4096|6|P7--P4|A22--A11 +|4096|...|...|... +|4096|0|P7--P4|A16--A5 +|8192|7|P7--P5|A23--A11 +|8192|6|P7--P5|A22--A10 +|8192|...|...|... +|8192|0|P7--P5|A16--A4 +|16384|7|P7--P6|A23--A10 +|16384|6|P7--P6|A22--A9 +|16384|...|...|... +|16384|0|P7--P6|A16--A3 +|32768|7|P7|A23--A9 +|32768|6|P7|A22--A8 +|32768|...|...|... +|32768|0|P7|A16--A2 +|========================== + +The 32 oscillators are serviced in sequence. With all oscillators +enabled, the DOC takes 38 microseconds to service all 32. 1.2 microseconds +per oscillator. + +== Player + +This is pseudocode for the soundsmith music player. The pseudocode style +is basically C-style, with 68k-style word notation. + +For example: `music[8].w` means read a little-endian word from the music +array, starting at byte offset 8. + +[source,c] +---- +// parse the song headers and prep audio +void initSong() { + SNDCTL = CURVOL & 0xf; // set vol, enable DAC, disable autoinc + // reset all oscillators to halt + freerun + for (int osc = 0xa0; osc < 0xc0; osc++) { + SNDADRL = osc; + SNDDAT = 1; // halt + } + + // load wavebank into sound RAM + SNDCTL = 0x60; // enable RAM + autoinc + SNDADRL = 0; + SNDADRH = 0; // point to beginning of sound RAM + // do all 64k of sound RAM + for (int addr = 0; addr < 0x10000; addr++) { + SNDDAT = wavebank[addr + 2]; // skip num inst at start of wavebank + } + playing = false; + + SNDINT = 0x945c; // = jsr $00/945c, this is soundInt() + SNDINTH = 0x003c; // called whenever a channel stops + + SNDCTL = 0; // DAC, vol = 0, disable autoinc + + // use oscillator 0 as a timer + SNDADRL = 0x0; // Osc 0 Frequency + SNDDAT = 0xfa; + SNDADRL = 0x20; // Osc 0 Frequency Hi + SNDDAT = 0; + SNDADRL = 0x40; // Osc 0 Volume + SNDDAT = 0; // mute the timer + SNDADRL = 0x80; // Osc 0 Wavetable ptr + SNDDAT = 0; + SNDADRL = 0xc0; // Osc 0 Wavetable size + SNDDAT = 0; // 0 = 256 bytes, 0 res + + SNDADRL = 0xe1; // Enable oscillators + SNDDAT = 0x3c; // 30 oscillators + SNDADRL = 0xa0; // Osc 0 control + SNDDAT = 0x8; // free run mode + interrupts enabled + + // music + header + blockSize = effects1 table (blockSize bytes long) + effects1 = &music + 0x258 + music[6].w; + // effects1 + blockSize = effects2 table (blockSize bytes long) + effects2 = effects1 + music[6].w; + // effects2 + blockSize = stereo table (16 words long) + stereoTable = effects2 + music[6].w; + + // load instrument headers + int pos = 0; + numInst = wavebank[0] & 0xff; + for (inst = 0; inst < numInst; inst++) { + for (int i = 0; i < 12; i++) { + instdef[inst * 12 + i] = wavebank[0x10022 + pos++]; + } + pos += 0x50; + } + // load compact table + for (int y = 0; y < 0x20; y++) { + compactTable[y] = wavebank[0x1005e + pos++]; + } +} + +// start playing the song +void playSong() { + timer = 0; + songLen = music[0x1d6]; + curRow = 0; + curPattern = 0; + rowOffset = music[0x1d8] * 64 * 14; + tempo = music[8].w; + + int pos = 0; + for (int i = 0; i < 0x1e; i += 2) { + volumeTable[i].w = music[0x2c + pos].w; + pos += 0x1e; + } + playing = true; +} + +// this is called whenever an oscillator halts with interrupts enabled +void soundInt() { + SNDCTL &= 0x9f; // doc, no auto inc + SNDADRL = 0xe0; // oscillator interrupt + SNDDAT &= 0x7f; // clear interrupt + uint8_t osc = (SNDDAT & 0x3e) >> 1; // get fired oscillator + if (osc != 0) { // wasn't timer + SNDADRL = 0xa0 + osc; // osc control + if (SNDDAT & 8) { // were interrupts enabled? + SNDDAT &= 0xfe; // clear halt bit.. retrig + } + return; + } + + if (!playing) + return; + timer++; + if (timer == tempo) { + timer = 0; + for (oscillator = 0; oscillator < 0xe; oscillator++) { + semitone = music[0x258 + rowOffset]; + if (semitone == 0 || semitone >= 0x80) { + rowOffset++; + if (semitone == 0x80) { + SNDCTL &= 0x9f; // DAC mode + oscaddr = (oscillator + 1) * 2; + SNDADRL = 0xa0 + oscaddr; // osc control + SNDDAT = 1; // halt + SNDADRL = 0xa0 + oscaddr + 1; // osc control + SNDDAT = 1; // halt pair + } else if (semitone == 0x81) { + curRow = 0x3f; + } + } else { + uint8_t fx = effects1[rowOffset]; + uint8_t inst = fx & 0xf0; + if (!inst) + inst = prevInst[oscillator]; + prevInst[oscillator] = inst; + volumeInt = volumeTable[((inst >> 4) - 1) * 2].w / 2; + fx &= 0xf; + if (fx == 0) { + arpeggio[oscillator] = effects2[rowOffset]; + arpTone[oscillator] = semitone; + } else { + arpeggio[oscillator] = 0; + if (fx == 3) { + volumeInt = effects2[rowOffset] / 2; + } else if (fx == 6) { + volumeInt -= effects2[rowOffset] / 2; + if (volumeInt < 0) + volumeInt = 0; + } else if (fx == 5) { + volumeInt += effects2[rowOffset] / 2; + if (volumeInt >= 0x80) + volumeInt = 0x7f; + } else if (fx == 0xf) { + tempo = effects2[rowOffset]; + } + if ((fx == 3 || fx == 5 || fx == 6) && semitone == 0) { + while (SNDCTL & 0x80); // wait for DOC + SNDCTL = (SNDCTL | 0x20) & 0xbf; // DOC + autoinc + SNDADRL = 0x40 + (oscillator + 1) * 2; // osc volume + SNDDAT = volumeInt; + SNDDAT = volumeInt; // pair + } + } + + if (semitone) { + oscaddr = (oscillator + 1) * 2; + SNDCTL &= 0x9f; // DOC mode + SNDADRL = 0xa0 + oscaddr; // osc ctl + SNDDAT = (SNDDAT & 0xf7) | 1; // halt, no interrupt + SNDADRL = 0xa0 + oscaddr + 1; // osc ctl pair + SNDDAT = (SNDDAT & 0xf7) | 1; // halt, no interrupt pair + inst = (prevInst[oscillator] >> 4) - 1; + if (inst < numInst) { + int x = inst * 12; + while (instruments[x].b < semitone) { + x += 6; + } + oscAptr = instruments[x + 1].b; + oscAsiz = instruments[x + 2].b; + oscActl = instruments[x + 3].b; + if (stereo) { + oscActl &= 0xf; + if (stereoTable[oscillator * 2]) + oscActl |= 0x10; + } + while (instruments[x].b != 0x7f) { + x += 6; + } + x += 6; // skip last instdef + while (instruments[x] < semitone) { + x += 6; + } + oscBptr = instruments[x + 1].b; + oscBsiz = instruments[x + 2].b; + oscBctl = instruments[x + 3].b; + if (stereo) { + oscBctl &= 0xf; + if (stereoTable[oscillator * 2]) + oscBctl |= 0x10; + } + freq = freqTable[semitone * 2].w >> compactTable[inst * 2].w; + while (SNDCTL & 0x80); // wait for DOC + SNDCTL = (SNDCTL | 0x20) & 0xbf; // DOC + autoinc + SNDADRL = oscaddr; // osc freq lo + SNDDAT = freq; + SNDDAT = freq; // pair + SNDADRL = 0x20 + oscaddr; // osc freq hi + SNDDAT = freq >> 8; + SNDDAT = freq >> 8; // pair + SNDADRL = 0x40 + oscaddr; // osc volume + SNDDAT = volumeConversion[volumeInt]; + SNDDAT = volumeConversion[volumeInt]; // pair + SNDADRL = 0x80 + oscaddr; // osc wavetable ptr + SNDDAT = oscAptr; + SNDDAT = oscBptr; // pair + SNDADRL = 0xc0 + oscaddr; // osc wavetable size + SNDDAT = oscAsiz; + SNDDAT = oscBsiz; // pair + SNDADRL = 0xa0 + oscaddr; // osc ctl + SNDDAT = oscActl; + SNDDAT = oscBctl; // pair + } + } + rowOffset++; + } + } + curRow++; + if (curRow < 0x40) + return; + // advance pattern + curRow = 0; + curPattern++; + if (curPattern < songLen) { + rowOffset = music[0x1d8 + curPattern] * 64 * 14; + } else { // stopped + playing = false; + } + return; + } else { // between notes.. apply arpeggios + for (oscillator = 0; oscillator < 0xe; oscillator++) { + if (arpeggio[oscillator]) { + switch (timer % 6) { + case 1: case 4: + arpTone[oscillator] += arpeggio[oscillator] >> 4; + break; + case 2: case 5: + arpTone[oscillator] += arpeggio[oscillator] & 0xf; + break; + case 0: case 3: + arpTone[oscillator] -= arpeggio[oscillator] >> 4; + arpTone[oscillator] -= arpeggio[oscillator] & 0xf; + break; + } + freq = freqTable[arpTone[oscillator] * 2].w >> compactTable[oscillator * +2].w; + oscaddr = (oscillator + 1) * 2; + while (SNDCTL & 0x80); // wait for DOC + SNDCTL = (SNDCTL | 0x20) & 0xbf; // DOC + autoinc + SNDADRL = oscaddr; // freq lo + SNDDAT = freq; + SNDDAT = freq; // pair + SNDADRL = 0x20 + oscaddr; // freq hi + SNDDAT = freq >> 8; + SNDDAT = freq >> 8; // pair + } + } + } +} + +uint8_t volumeConversion[] = { + 0x00, 0x02, 0x04, 0x05, 0x06, 0x07, 0x09, 0x0a, // 0 + 0x0c, 0x0d, 0x0f, 0x10, 0x12, 0x13, 0x15, 0x16, // 8 + 0x18, 0x19, 0x1b, 0x1c, 0x1e, 0x1f, 0x21, 0x22, // 10 + 0x24, 0x25, 0x27, 0x28, 0x2a, 0x2b, 0x2d, 0x2e, // 18 + 0x30, 0x31, 0x33, 0x34, 0x36, 0x37, 0x39, 0x3a, // 20 + 0x3c, 0x3d, 0x3f, 0x40, 0x42, 0x43, 0x45, 0x46, // 28 + 0x48, 0x49, 0x4b, 0x4c, 0x4e, 0x4f, 0x51, 0x52, // 30 + 0x54, 0x55, 0x57, 0x58, 0x5a, 0x5b, 0x5d, 0x5e, // 38 + 0x60, 0x61, 0x63, 0x64, 0x66, 0x67, 0x69, 0x6a, // 40 + 0x6c, 0x6d, 0x6f, 0x70, 0x72, 0x73, 0x75, 0x76, // 48 + 0x78, 0x79, 0x7b, 0x7c, 0x7e, 0x7f, 0x81, 0x82, // 50 + 0x84, 0x85, 0x87, 0x88, 0x8a, 0x8b, 0x8d, 0x8e, // 58 + 0x90, 0x91, 0x93, 0x94, 0x96, 0x97, 0x99, 0x9a, // 60 + 0x9c, 0x9d, 0x9f, 0xa0, 0xa2, 0xa3, 0xa5, 0xa6, // 68 + 0xa8, 0xa9, 0xab, 0xac, 0xae, 0xaf, 0xb1, 0xb2, // 70 + 0xb4, 0xb5, 0xb7, 0xb8, 0xba, 0xbb, 0xbe, 0xc0 // 78 +}; +uint16_t freqTable[] = { + 0x0000, 0x0016, 0x0017, 0x0018, 0x001a, 0x001b, 0x001d, 0x001e, + 0x0020, 0x0022, 0x0024, 0x0026, 0x0029, 0x002b, 0x002e, 0x0031, + 0x0033, 0x0036, 0x003a, 0x003d, 0x0041, 0x0045, 0x0049, 0x004d, + 0x0052, 0x0056, 0x005c, 0x0061, 0x0067, 0x006d, 0x0073, 0x007a, + 0x0081, 0x0089, 0x0091, 0x009a, 0x00a3, 0x00ad, 0x00b7, 0x00c2, + 0x00ce, 0x00d9, 0x00e6, 0x00f4, 0x0102, 0x0112, 0x0122, 0x0133, + 0x0146, 0x015a, 0x016f, 0x0184, 0x019b, 0x01b4, 0x01ce, 0x01e9, + 0x0206, 0x0225, 0x0246, 0x0269, 0x028d, 0x02b4, 0x02dd, 0x0309, + 0x0337, 0x0368, 0x039c, 0x03d3, 0x040d, 0x044a, 0x048c, 0x04d1, + 0x051a, 0x0568, 0x05ba, 0x0611, 0x066e, 0x06d0, 0x0737, 0x07a5, + 0x081a, 0x0895, 0x0918, 0x09a2, 0x0a35, 0x0ad0, 0x0b75, 0x0c23, + 0x0cdc, 0x0d9f, 0x0e6f, 0x0f4b, 0x1033, 0x112a, 0x122f, 0x1344, + 0x1469, 0x15a0, 0x16e9, 0x1846, 0x19b7, 0x1b3f, 0x1cde, 0x1e95, + 0x2066, 0x2254, 0x245e, 0x2688 +}; +---- diff --git a/docs/xmas.html b/docs/xmas.html new file mode 100644 index 0000000..1c6b1f0 --- /dev/null +++ b/docs/xmas.html @@ -0,0 +1,976 @@ + + + + + +Extracting music from the Xmas demo + + + + + +
+
+
+

This is very similar to the documentation on extracting music from Modulae. +The steps are a little more involved since the Xmas demo is a multi-part +demo, where the code and music for each part is loaded from disk separately.

+

As with Modulae, we’ll start with extracting the initial boot loader.

+
+
+
./disasm xmasdemo.2mg 800 0 1 > boot.s
+

This time, the loading starts at $08e3. It loads blocks 7—17 into +RAM starting at $9000 and then calls the decrunch routine at $0a00.

+

We’ll do the same to extract the main loader.

+
+
+
./decrunch xmasdemo.2mg 7 11 loader
+./disasm loader 9000 > loader.s
+

Again, we track down the loading routine. This time, it’s a function +located at $9c3f. It gets called multiple times, to load the different +parts of the demo. The A register holds the address of the table to use +when loading.

+

The first time the loader is called, it uses the table at $9ac3. This +table only contains the loading screen graphics. So we’ll ignore it.

+
+
+
+

Loading… Music

+
+

The second time the loader is called, it uses the table at $9ad5. +We’ll extract the table offsets as we did in the Modulae example. Using +the relative address of the table from the start of the loader file.

+
+
+
./dumptbl loader ad5
+

We can see a huge chunk of data that is loaded into $e:9000. We’ll +go ahead and disassemble it and look for the music player.

+
+
+
./decrunch xmasdemo.2mg 47 92 loading
+./disasm loading e9000 > loading.s
+

Most of the loading disassembly is actually noise because we disassembled +data.. but we know from the loader that after this block of data is loaded +and decrunched, it calls $f:f000. We can use that to follow the code +flow and inspect the music player.

+

We see that the music player is slightly different from the standard +soundsmith music player. The timer runs on the last channel instead of the +first channel, and the tempo sets the timer frequency based on a lookup +table. Neither of things really matter, they’re just used to lighten the CPU +load a bit.

+

We discover the music starts at $e:9000 and the wavebank starts at $e:e700. +We’ll use the trim functions to extract the data into separate files.

+
+
+
./trimmusic loading 0 loading.song
+./trimwb loading 5700 loading.wb
+
+
+
+

Main Menu Music

+
+

The next time the loader is called, it uses the table at $9ae7.

+
+
+
./dumptbl loader ae7
+

We hunt for the music player, and discover it. We also discover that +the music and wavebank are again combined into a giant block of data.

+
+
+
./decrunch xmasdemo.2mg 139 122 main
+./trimmusic main 0 main.song
+./trimwb main 9600 main.wb
+

After the main menu, the different parts of the demo are selectable by the +user. Making a selection causes the loader to load a unique table which +contains the graphics and the music and a different music player.

+

The process is pretty much the same for each section. We look at the +table, we track down the music player, we use that to determine where +the music and wavetables are.

+

I’ll just summarize each section from here-on out since the process is the +same for each. I will include the dumptbl command for each, though, +so you can see the block table for each section.

+
+
+
+

Section 1 Music

+
+

The music and wavebanks are combined again, and loaded into $7:0000.

+
+
+
./dumptbl loader b09
+./decrunch xmasdemo.2mg 393 135 section1
+./trimmusic section1 0 section1.song
+./trimwb section1 a000 section1.wb
+
+
+
+

Section 2 Music

+
+

The music is loaded into $5:0000, the wavebank is loaded into $3:0000 +and not crunched.

+
+
+
./dumptbl loader b4d
+./decrunch xmasdemo.2mg 682 67 section2.song
+./decrunch xmasdemo.2mg 749 131 section2.wb raw
+

You can pass the song and wavebank back through the trim functions to +trim off the padding.

+
+
+
+

Section 3 Music

+
+

The music and wavebank are combined again, and loaded into $c:0000. +The music doesn’t sound quite right, so I’m thinking it may be patched +elsewhere.

+
+
+
./dumptbl loader b2b
+./decrunch xmasdemo.2mg 538 114 section3
+./trimmusic section3 0 section3.song
+./trimwb section3 8200 section3.wb
+
+
+
+

Section 4 Music

+
+

The music and wavebank are combined again, loaded into $7:0000. +The loader hot-patches the music just after loading it. It sets the word at +$7:0006 to $3b80. This sets the size of each block to $3b80, where +it was previously $3b00. We’ll go ahead and duplicate that patch in +our extract process.

+
+
+
./dumptbl loader b7f
+./decrunch xmasdemo.2mg 915 127 section4
+printf '\x80\x3b' | dd of=section4 bs=1 seek=6 count=2 conv=notrunc
+./trimmusic section4 0 section4.song
+./trimwb section4 b600 section4.wb
+
+
+
+

Section 5 Music

+
+

Section 5 doesn’t have any music. This is a very strange section too, since +it does two different things depending on whether or not you have the 3rd +joystick button held when it launches. Easter egg?

+
+
+
+

Section 6 Music

+
+

Section 6 also is missing music, but it does have a sound effect that +is loaded into $3:0000.

+
+
+
+

Section 7 Music

+
+

The music is loaded into $4:0000 along with the demo code. The wavebank +is loaded into $3:0000. Unfortunately, the wavebank is incomplete for +some reason, making this music unplayable. I haven’t figured out how the +demo patches the wavebank.

+
+
+
./dumptbl loader bbb
+./decrunch xmasdemo.2mg 1078 103 section7
+./decrunch xmasdemo.2mg 1181 56 section7.wb raw
+./trimmusic section7 10000 section7.song
+
+
+
+

Section 8 Music

+
+

This one is tricky. It first loads the table at $9c1b, which loads +uncrunched data into $2000. It then loads the table at $9c2d which +loads more uncrunched data into $3:0000. It then takes $100 bytes from +$2010 and appends them onto the end of the data at $3:0000. Finally, it +uncrunches the data at $3:0000. So we’ll have to do this patch as well.

+
+
+
./dumptbl loader c1b
+./decrunch xmasdemo.2mg 24 16 patch raw
+./dumptbl loader c2d
+./decrunch xmasdemo.2mg 1487 113 crunched raw
+dd if=patch of=crunched skip=16 bs=1 count=256 oflag=append conv=notrunc
+./decrunch crunched 0 0 section8
+./trimmusic section8 1000 section8.song
+./trimwb seciton8 7000 section8.wb
+

And that’s all the music in the xmas demo that I can find.

+
+
+
+

+ + + diff --git a/docs/xmas.txt b/docs/xmas.txt new file mode 100644 index 0000000..770d2e4 --- /dev/null +++ b/docs/xmas.txt @@ -0,0 +1,209 @@ +Extracting music from the Xmas demo +=================================== + +This is very similar to the documentation on extracting music from Modulae. +The steps are a little more involved since the Xmas demo is a multi-part +demo, where the code and music for each part is loaded from disk separately. + +As with Modulae, we'll start with extracting the initial boot loader. + +[source,shell] +---- +./disasm xmasdemo.2mg 800 0 1 > boot.s +---- + +This time, the loading starts at `$08e3`. It loads blocks 7--17 into +RAM starting at `$9000` and then calls the decrunch routine at `$0a00`. + +We'll do the same to extract the main loader. + +[source,shell] +---- +./decrunch xmasdemo.2mg 7 11 loader +./disasm loader 9000 > loader.s +---- + +Again, we track down the loading routine. This time, it's a function +located at `$9c3f`. It gets called multiple times, to load the different +parts of the demo. The `A` register holds the address of the table to use +when loading. + +The first time the loader is called, it uses the table at `$9ac3`. This +table only contains the loading screen graphics. So we'll ignore it. + +== Loading... Music + +The second time the loader is called, it uses the table at `$9ad5`. +We'll extract the table offsets as we did in the Modulae example. Using +the relative address of the table from the start of the loader file. + +[source,shell] +---- +./dumptbl loader ad5 +---- + +We can see a huge chunk of data that is loaded into `$e:9000`. We'll +go ahead and disassemble it and look for the music player. + +[source,shell] +---- +./decrunch xmasdemo.2mg 47 92 loading +./disasm loading e9000 > loading.s +---- + +Most of the loading disassembly is actually noise because we disassembled +data.. but we know from the loader that after this block of data is loaded +and decrunched, it calls `$f:f000`. We can use that to follow the code +flow and inspect the music player. + +We see that the music player is slightly different from the standard +soundsmith music player. The timer runs on the last channel instead of the +first channel, and the tempo sets the timer frequency based on a lookup +table. Neither of things really matter, they're just used to lighten the CPU +load a bit. + +We discover the music starts at `$e:9000` and the wavebank starts at `$e:e700`. +We'll use the trim functions to extract the data into separate files. + +[source,shell] +---- +./trimmusic loading 0 loading.song +./trimwb loading 5700 loading.wb +---- + +== Main Menu Music + +The next time the loader is called, it uses the table at `$9ae7`. + +[source,shell] +---- +./dumptbl loader ae7 +---- + +We hunt for the music player, and discover it. We also discover that +the music and wavebank are again combined into a giant block of data. + +[source,shell] +---- +./decrunch xmasdemo.2mg 139 122 main +./trimmusic main 0 main.song +./trimwb main 9600 main.wb +---- + +After the main menu, the different parts of the demo are selectable by the +user. Making a selection causes the loader to load a unique table which +contains the graphics and the music and a different music player. + +The process is pretty much the same for each section. We look at the +table, we track down the music player, we use that to determine where +the music and wavetables are. + +I'll just summarize each section from here-on out since the process is the +same for each. I will include the dumptbl command for each, though, +so you can see the block table for each section. + +== Section 1 Music + +The music and wavebanks are combined again, and loaded into `$7:0000`. + +[source,shell] +---- +./dumptbl loader b09 +./decrunch xmasdemo.2mg 393 135 section1 +./trimmusic section1 0 section1.song +./trimwb section1 a000 section1.wb +---- + +== Section 2 Music + +The music is loaded into `$5:0000`, the wavebank is loaded into `$3:0000` +and not crunched. + +[source,shell] +---- +./dumptbl loader b4d +./decrunch xmasdemo.2mg 682 67 section2.song +./decrunch xmasdemo.2mg 749 131 section2.wb raw +---- + +You can pass the song and wavebank back through the trim functions to +trim off the padding. + +== Section 3 Music + +The music and wavebank are combined again, and loaded into `$c:0000`. +The music doesn't sound quite right, so I'm thinking it may be patched +elsewhere. + +[source,shell] +---- +./dumptbl loader b2b +./decrunch xmasdemo.2mg 538 114 section3 +./trimmusic section3 0 section3.song +./trimwb section3 8200 section3.wb +---- + +== Section 4 Music + +The music and wavebank are combined again, loaded into `$7:0000`. +The loader hot-patches the music just after loading it. It sets the word at +`$7:0006` to `$3b80`. This sets the size of each block to `$3b80`, where +it was previously `$3b00`. We'll go ahead and duplicate that patch in +our extract process. + +[source,shell] +---- +./dumptbl loader b7f +./decrunch xmasdemo.2mg 915 127 section4 +printf '\x80\x3b' | dd of=section4 bs=1 seek=6 count=2 conv=notrunc +./trimmusic section4 0 section4.song +./trimwb section4 b600 section4.wb +---- + +== Section 5 Music + +Section 5 doesn't have any music. This is a very strange section too, since +it does two different things depending on whether or not you have the 3rd +joystick button held when it launches. Easter egg? + +== Section 6 Music + +Section 6 also is missing music, but it does have a sound effect that +is loaded into `$3:0000`. + +== Section 7 Music + +The music is loaded into `$4:0000` along with the demo code. The wavebank +is loaded into `$3:0000`. Unfortunately, the wavebank is incomplete for +some reason, making this music unplayable. I haven't figured out how the +demo patches the wavebank. + +[source,shell] +---- +./dumptbl loader bbb +./decrunch xmasdemo.2mg 1078 103 section7 +./decrunch xmasdemo.2mg 1181 56 section7.wb raw +./trimmusic section7 10000 section7.song +---- + +== Section 8 Music + +This one is tricky. It first loads the table at `$9c1b`, which loads +uncrunched data into `$2000`. It then loads the table at `$9c2d` which +loads more uncrunched data into `$3:0000`. It then takes `$100` bytes from +`$2010` and appends them onto the end of the data at `$3:0000`. Finally, it +uncrunches the data at `$3:0000`. So we'll have to do this patch as well. + +[source,shell] +---- +./dumptbl loader c1b +./decrunch xmasdemo.2mg 24 16 patch raw +./dumptbl loader c2d +./decrunch xmasdemo.2mg 1487 113 crunched raw +dd if=patch of=crunched skip=16 bs=1 count=256 oflag=append conv=notrunc +./decrunch crunched 0 0 section8 +./trimmusic section8 1000 section8.song +./trimwb seciton8 7000 section8.wb +---- + +And that's all the music in the xmas demo that I can find. diff --git a/extract/.gitignore b/extract/.gitignore new file mode 100644 index 0000000..6dfc120 --- /dev/null +++ b/extract/.gitignore @@ -0,0 +1,7 @@ +*.o +decrunch +dumptbl +disasm +trimmusic +trimwb +2mg diff --git a/extract/2mg.c b/extract/2mg.c new file mode 100644 index 0000000..e940e2e --- /dev/null +++ b/extract/2mg.c @@ -0,0 +1,230 @@ +#include +#include +#include +#include +#include +#include + +#define fourcc(x) (x[0] | (x[1] << 8) | (x[2] << 16) | (x[3] << 24)) + +static void handleDirectory(uint16_t key, uint8_t *disk, uint32_t diskLen); +static void handleEntry(uint8_t *entry, uint8_t *disk, uint32_t diskLen); +static void handleFile(uint16_t key, uint32_t len, char *name, uint8_t *disk, + uint32_t diskLen, int type); + +static inline uint32_t r32(uint8_t *data) { + uint32_t r = *data++; + r |= *data++ << 8; + r |= *data++ << 16; + r |= *data << 24; + return r; +} + +static inline uint16_t r16(uint8_t *data) { + uint16_t r = *data++; + r |= *data << 8; + return r; +} + +int main(int argc, char *argv[]) { + if (argc != 2) { + fprintf(stderr, "Usage: %s \n", argv[0]); + fprintf(stderr, "This will extract all files in the 2mg into the current folder.\n"); + fprintf(stderr, "It will create directories as needed.\n"); + return -1; + } + FILE *f = fopen(argv[1], "rb"); + if (!f) { + fprintf(stderr, "Couldn't open '%s'\n", argv[1]); + return -1; + } + + fseek(f, 0, SEEK_END); + size_t len = ftell(f); + fseek(f, 0, SEEK_SET); + if (len < 64) { + fprintf(stderr, "%s is not a valid 2mg file\n", argv[1]); + fclose(f); + return -1; + } + + uint8_t *header = malloc(64); + fread(header, 64, 1, f); + + if (r32(header) != fourcc("2IMG")) { + fprintf(stderr, "%s is not a valid 2mg file\n", argv[1]); + fclose(f); + return -1; + } + + if (r32(header + 0xc) != 1) { + fprintf(stderr, "Not in ProDOS format\n"); + fclose(f); + return -1; + } + + uint32_t diskLen = r32(header + 0x14) * 512; + uint32_t diskOfs = r32(header + 0x18); + free(header); + + fseek(f, diskOfs, SEEK_SET); + uint8_t *disk = malloc(diskLen); + fread(disk, diskLen, 1, f); + fclose(f); + + handleDirectory(2, disk, diskLen); + free(disk); + return 0; +} + +static void readFilename(uint8_t *filename, uint8_t length, char *outname) { + for (int i = 0; i < length; i++) { + char ch = filename[i]; + if (isalnum(ch) || ch == '_' || ch == '.' || ch ==' ') { + *outname++ = ch; + } else { + *outname++ = 'x'; + char hi = ch >> 4; + char lo = ch & 0xf; + if (hi > 9) + *outname++ = 'a' + (hi - 10); + else + *outname++ = '0' + hi; + if (lo > 9) + *outname++ = 'a' + (lo - 10); + else + *outname++ = '0' + lo; + } + } + *outname = 0; +} + +static void handleDirectory(uint16_t key, uint8_t *disk, uint32_t diskLen) { + uint8_t *block = disk + key * 512; + + if ((block[4] & 0xf0) != 0xf0 && (block[4] & 0xf0) != 0xe0) { + fprintf(stderr, "Corrupted directory header\n"); + return; + } + + char dirname[50]; + readFilename(block + 5, block[4] & 0xf, dirname); + + mkdir(dirname, 0777); + chdir(dirname); + + uint8_t entryLength = block[0x23]; + uint8_t entriesPerBlock= block[0x24]; + uint16_t fileCount = r16(block + 0x25); + uint8_t *entry = block + entryLength + 4; + uint8_t curEntry = 1; + uint16_t curFile = 0; + + while (curFile < fileCount) { + if (entry[0] != 0) { + handleEntry(entry, disk, diskLen); + curFile++; + } + curEntry++; + entry += entryLength; + if (curEntry == entriesPerBlock) { + curEntry = 0; + block = disk + r16(block + 2) * 512; + entry = block + 4; + } + } + + chdir(".."); +} + +static void handleEntry(uint8_t *entry, uint8_t *disk, uint32_t diskLen) { + uint16_t key = r16(entry + 0x11); + uint32_t eof = r32(entry + 0x15) & 0xffffff; + + char filename[50]; + readFilename(entry + 1, entry[0] & 0xf, filename); + + switch (entry[0] & 0xf0) { + case 0x10: + handleFile(key, eof, filename, disk, diskLen, 1); // seedling + break; + case 0x20: + handleFile(key, eof, filename, disk, diskLen, 2); // sapling + break; + case 0x30: + handleFile(key, eof, filename, disk, diskLen, 3); // tree + break; + case 0xd0: + handleDirectory(key, disk, diskLen); + break; + default: + fprintf(stderr, "Unknown file type: %x\n", entry[0] >> 4); + return; + } +} + +static void dumpSeedling(uint8_t *block, uint32_t len, FILE *f) { + if (block == NULL) + fseek(f, len, SEEK_CUR); + else + fwrite(block, len, 1, f); +} + +static void dumpSapling(uint8_t *index, uint32_t len, FILE *f, uint8_t *disk, + uint32_t diskLen) { + if (index == NULL) { + fseek(f, len, SEEK_CUR); + return; + } + while (len > 0) { + uint16_t blockid = index[0] | (index[256] << 8); + uint8_t *block = NULL; + if (blockid && (blockid + 1) * 512 <= diskLen) + block = disk + blockid * 512; + uint32_t blen = len > 512 ? 512 : len; + dumpSeedling(block, blen, f); + len -= blen; + index++; + } +} + +static void dumpTree(uint8_t *index, uint32_t len, FILE *f, uint8_t *disk, + uint32_t diskLen) { + if (index == NULL) { + fseek(f, len, SEEK_CUR); + return; + } + while (len > 0) { + uint16_t blockid = index[0] | (index[256] << 8); + uint8_t *block = NULL; + if (blockid && (blockid + 1) * 512 <= diskLen) + block = disk + blockid * 512; + uint32_t blen = len > 256 * 512 ? 256 * 512 : len; + dumpSapling(block, blen, f, disk, diskLen); + len -= blen; + index++; + } +} + +static void handleFile(uint16_t key, uint32_t len, char *name, uint8_t *disk, + uint32_t diskLen, int type) { + uint8_t *block = disk + key * 512; + FILE *f = fopen(name, "wb"); + if (!f) { + fprintf(stderr, "Failed to create '%s'\n", name); + return; + } + + switch (type) { + case 1: + dumpSeedling(block, len, f); + break; + case 2: + dumpSapling(block, len, f, disk, diskLen); + break; + case 3: + dumpTree(block, len, f, disk, diskLen); + break; + } + fclose(f); +} diff --git a/extract/65816.h b/extract/65816.h new file mode 100644 index 0000000..f0c84c6 --- /dev/null +++ b/extract/65816.h @@ -0,0 +1,331 @@ +#ifndef __65816_H__ +#define __65816_H__ + +typedef enum { + IMP = 0, + IMM, + IMMM, + IMMX, + IMMS, + ABSO, + ABL, + ABX, + ABY, + ABLX, + AIX, + ZP, + ZPX, + ZPY, + ZPS, + IND, + INZ, + INL, + INX, + INY, + INLY, + INS, + REL, + RELL, + BANK, + DB, + DW, + DD +} Address; + +typedef struct { + const char *inst; + Address address; +} Opcode; + +static Opcode opcodes[] = { + {"brk", IMP}, // 00 + {"ora", INX}, // 01 + {"cop", IMP}, // 02 + {"ora", ZPS}, // 03 + {"tsb", ZP}, // 04 + {"ora", ZP}, // 05 + {"asl", ZP}, // 06 + {"ora", INL}, // 07 + {"php", IMP}, // 08 + {"ora", IMMM}, // 09 + {"asl", IMP}, // 0a + {"phd", IMP}, // 0b + {"tsb", ABSO}, // 0c + {"ora", ABSO}, // 0d + {"asl", ABSO}, // 0e + {"ora", ABL}, // 0f + {"bpl", REL}, // 10 + {"ora", INY}, // 11 + {"ora", INZ}, // 12 + {"ora", INS}, // 13 + {"trb", ZP}, // 14 + {"ora", ZPX}, // 15 + {"asl", ZPX}, // 16 + {"ora", INLY}, // 17 + {"clc", IMP}, // 18 + {"ora", ABY}, // 19 + {"inc", IMP}, // 1a + {"tcs", IMP}, // 1b + {"trb", ABSO}, // 1c + {"ora", ABX}, // 1d + {"asl", ABX}, // 1e + {"ora", ABLX}, // 1f + {"jsr", ABSO}, // 20 + {"and", INX}, // 21 + {"jsl", ABL}, // 22 + {"and", ZPS}, // 23 + {"bit", ZP}, // 24 + {"and", ZP}, // 25 + {"rol", ZP}, // 26 + {"and", INL}, // 27 + {"plp", IMP}, // 28 + {"and", IMMM}, // 29 + {"rol", IMP}, // 2a + {"pld", IMP}, // 2b + {"bit", ABSO}, // 2c + {"and", ABSO}, // 2d + {"rol", ABSO}, // 2e + {"and", ABL}, // 2f + {"bmi", REL}, // 30 + {"and", INY}, // 31 + {"and", INZ}, // 32 + {"and", INS}, // 33 + {"bit", ZPX}, // 34 + {"and", ZPX}, // 35 + {"rol", ZPX}, // 36 + {"and", INLY}, // 37 + {"sec", IMP}, // 38 + {"and", ABY}, // 39 + {"dec", IMP}, // 3a + {"tsc", IMP}, // 3b + {"bit", ABX}, // 3c + {"and", ABX}, // 3d + {"rol", ABX}, // 3e + {"and", ABLX}, // 3f + {"rti", IMP}, // 40 + {"eor", INX}, // 41 + {"db", DB}, // 42 + {"eor", ZPS}, // 43 + {"mvp", BANK}, // 44 + {"eor", ZP}, // 45 + {"lsr", ZP}, // 46 + {"eor", INL}, // 47 + {"pha", IMP}, // 48 + {"eor", IMMM}, // 49 + {"lsr", IMP}, // 4a + {"phk", IMP}, // 4b + {"jmp", ABSO}, // 4c + {"eor", ABSO}, // 4d + {"lsr", ABSO}, // 4e + {"eor", ABL}, // 4f + {"bvc", REL}, // 50 + {"eor", INY}, // 51 + {"eor", INZ}, // 52 + {"eor", INS}, // 53 + {"mvn", BANK}, // 54 + {"eor", ZPX}, // 55 + {"lsr", ZPX}, // 56 + {"eor", INLY}, // 57 + {"cli", IMP}, // 58 + {"eor", ABY}, // 59 + {"phy", IMP}, // 5a + {"tcd", IMP}, // 5b + {"jmp", ABL}, // 5c + {"eor", ABX}, // 5d + {"lsr", ABX}, // 5e + {"eor", ABLX}, // 5f + {"rts", IMP}, // 60 + {"adc", INX}, // 61 + {"per", IMP}, // 62 + {"adc", ZPS}, // 63 + {"stz", ZP}, // 64 + {"adc", ZP}, // 65 + {"ror", ZP}, // 66 + {"adc", INL}, // 67 + {"pla", IMP}, // 68 + {"adc", IMMM}, // 69 + {"ror", IMP}, // 6a + {"rtl", IMP}, // 6b + {"jmp", IND}, // 6c + {"adc", ABSO}, // 6d + {"ror", ABSO}, // 6e + {"adc", ABL}, // 6f + {"bvs", REL}, // 70 + {"adc", INY}, // 71 + {"adc", INZ}, // 72 + {"adc", INS}, // 73 + {"stz", ZPX}, // 74 + {"adc", ZPX}, // 75 + {"ror", ZPX}, // 76 + {"adc", INLY}, // 77 + {"sei", IMP}, // 78 + {"adc", ABY}, // 79 + {"ply", IMP}, // 7a + {"tdc", IMP}, // 7b + {"jmp", AIX}, // 7c + {"adc", ABX}, // 7d + {"ror", ABX}, // 7e + {"adc", ABLX}, // 7f + {"bra", REL}, // 80 + {"sta", INX}, // 81 + {"brl", RELL}, // 82 + {"sta", ZPS}, // 83 + {"sty", ZP}, // 84 + {"sta", ZP}, // 85 + {"stx", ZP}, // 86 + {"sta", INL}, // 87 + {"dey", IMP}, // 88 + {"bit", IMMM}, // 89 + {"txa", IMP}, // 8a + {"phb", IMP}, // 8b + {"sty", ABSO}, // 8c + {"sta", ABSO}, // 8d + {"stx", ABSO}, // 8e + {"sta", ABL}, // 8f + {"bcc", REL}, // 90 + {"sta", INY}, // 91 + {"sta", INZ}, // 92 + {"sta", INS}, // 93 + {"sty", ZPX}, // 94 + {"sta", ZPX}, // 95 + {"stx", ZPY}, // 96 + {"sta", INLY}, // 97 + {"tya", IMP}, // 98 + {"sta", ABY}, // 99 + {"txs", IMP}, // 9a + {"txy", IMP}, // 9b + {"stz", ABSO}, // 9c + {"sta", ABX}, // 9d + {"stz", ABX}, // 9e + {"sta", ABLX}, // 9f + {"ldy", IMMX}, // a0 + {"lda", INX}, // a1 + {"ldx", IMMX}, // a2 + {"lda", ZPS}, // a3 + {"ldy", ZP}, // a4 + {"lda", ZP}, // a5 + {"ldx", ZP}, // a6 + {"lda", INL}, // a7 + {"tay", IMP}, // a8 + {"lda", IMMM}, // a9 + {"tax", IMP}, // aa + {"plb", IMP}, // ab + {"ldy", ABSO}, // ac + {"lda", ABSO}, // ad + {"ldx", ABSO}, // ae + {"lda", ABL}, // af + {"bcs", REL}, // b0 + {"lda", INY}, // b1 + {"lda", INZ}, // b2 + {"lda", INS}, // b3 + {"ldy", ZPX}, // b4 + {"lda", ZPX}, // b5 + {"ldx", ZPY}, // b6 + {"lda", INLY}, // b7 + {"clv", IMP}, // b8 + {"lda", ABY}, // b9 + {"tsx", IMP}, // ba + {"tyx", IMP}, // bb + {"ldy", ABX}, // bc + {"lda", ABX}, // bd + {"ldx", ABY}, // be + {"lda", ABLX}, // bf + {"cpy", IMMX}, // c0 + {"cmp", INX}, // c1 + {"rep", IMM}, // c2 + {"cmp", ZPS}, // c3 + {"cpy", ZP}, // c4 + {"cmp", ZP}, // c5 + {"dec", ZP}, // c6 + {"cmp", INL}, // c7 + {"iny", IMP}, // c8 + {"cmp", IMMM}, // c9 + {"dex", IMP}, // ca + {"wai", IMP}, // cb + {"cpy", ABSO}, // cc + {"cmp", ABSO}, // cd + {"dec", ABSO}, // ce + {"cmp", ABL}, // cf + {"bne", REL}, // d0 + {"cmp", INY}, // d1 + {"cmp", INZ}, // d2 + {"cmp", INS}, // d3 + {"pei", IMP}, // d4 + {"cmp", ZPX}, // d5 + {"dec", ZPX}, // d6 + {"cmp", INLY}, // d7 + {"cld", IMP}, // d8 + {"cmp", ABY}, // d9 + {"phx", IMP}, // da + {"stp", IMP}, // db + {"jmp", IND}, // dc + {"cmp", ABX}, // dd + {"dec", ABX}, // de + {"cmp", ABLX}, // df + {"cpx", IMMX}, // e0 + {"sbc", INX}, // e1 + {"sep", IMM}, // e2 + {"sbc", ZPS}, // e3 + {"cpx", ZP}, // e4 + {"sbc", ZP}, // e5 + {"inc", ZP}, // e6 + {"sbc", INL}, // e7 + {"inx", IMP}, // e8 + {"sbc", IMMM}, // e9 + {"nop", IMP}, // ea + {"xba", IMP}, // eb + {"cpx", ABSO}, // ec + {"sbc", ABSO}, // ed + {"inc", ABSO}, // ee + {"sbc", ABL}, // ef + {"beq", REL}, // f0 + {"sbc", INY}, // f1 + {"sbc", INZ}, // f2 + {"sbc", INS}, // f3 + {"pea", IMMS}, // f4 + {"sbc", ZPX}, // f5 + {"inc", ZPX}, // f6 + {"sbc", INLY}, // f7 + {"sed", IMP}, // f8 + {"sbc", ABY}, // f9 + {"plx", IMP}, // fa + {"xce", IMP}, // fb + {"jsr", AIX}, // fc + {"sbc", ABX}, // fd + {"inc", ABX}, // fe + {"sbc", ABLX} // ff +}; + + +uint8_t addressSizes[] = { + 1, // IMP + 2, // IMM + 3, // IMMM + 3, // IMMX + 3, // IMMS + 3, // ABSO + 4, // ABL + 3, // ABX + 3, // ABY + 4, // ABLX + 3, // AIX + 2, // ZP + 2, // ZPX + 2, // ZPY + 2, // ZPS + 3, // IND + 2, // INZ + 2, // INL + 2, // INX + 2, // INY + 2, // INLY + 2, // INS + 2, // REL + 3, // RELL + 3, // BANK + 1, // DB + 2, // DW + 4 // DD +}; + +#endif diff --git a/extract/Makefile b/extract/Makefile new file mode 100644 index 0000000..34f1ee1 --- /dev/null +++ b/extract/Makefile @@ -0,0 +1,28 @@ +CC=clang +CFLAGS=-Wall + +all: decrunch disasm dumptbl trimmusic trimwb 2mg + +decrunch: decrunch.o + $(CC) $(CFLAGS) -o $@ $< + +disasm: disasm.o + $(CC) $(CFLAGS) -o $@ $< + +dumptbl: dumptbl.o + $(CC) $(CFLAGS) -o $@ $< + +trimmusic: trimmusic.o + $(CC) $(CFLAGS) -o $@ $< + +trimwb: trimwb.o + $(CC) $(CFLAGS) -o $@ $< + +2mg: 2mg.o + $(CC) $(CFLAGS) -o $@ $< + +%.o: %.c + $(CC) -c $(CFLAGS) -o $@ $< + +clean: + rm -f *.o decrunch disasm dumptbl trimmusic trimwb 2mg diff --git a/extract/README.md b/extract/README.md new file mode 100644 index 0000000..b5aa5d9 --- /dev/null +++ b/extract/README.md @@ -0,0 +1,47 @@ +This directory contains utiltiies I wrote to extract music from +FTA demos. + +Extracting the music is a multistep process, that involves analyzing +boot loaders to determine where on disk the music is stored (since the +FTA demos don't have filesystems). + +Compile all the utilities by running `make`. + +There are step-by-step tutorials on how to use these tools to extract music from +the various FTA demos inside the docs/ folder. + +Most of these tools work on 2mg images, and are controlled at a 512-byte block level. + +== disasm + +`disasm` is a quick and dirty 65816 disassembler. You specify the starting +block and the number of blocks to disassemble. You also specify the starting address (in hex) +for disassembly. +into memory starting at 0x800. Then redirect the disassembly into a file called boot.s + +== decrunch + +`decrunch` will extract. There's a special `raw` keyword that will skip the +decrunching routine, and thus this routine can also be used to extract random blocks +out of a disk image. + +== dumptbl + +Modulae and the Xmas demo both use a loader that uses tables of blocks and their +destination addresses to load data off of the disk. This tool will parse and +print out those tables. + +== trimmusic and trimwb + +Since the music and wavebanks weren't loaded from files, but instead just loaded +as groups of 512-byte blocks from disk. They should be trimmed to the correct +size afterwards. These routines will determine the proper length of the files +and trim them. Since these files don't necessarily start at the beginning of +the block, you can also specify the starting offset of the song or music inside +the block. + +== 2mg + +This tool is for the disk images that have an actual ProDOS filesystem on them. +Simply pass it a disk image and it will extract the entire disk image, creating +directories as needed. diff --git a/extract/addresses.h b/extract/addresses.h new file mode 100644 index 0000000..49c53aa --- /dev/null +++ b/extract/addresses.h @@ -0,0 +1,410 @@ +#ifndef __ADDRESSES_H__ +#define __ADDRESSES_H__ + +typedef struct { + uint32_t address; + const char *comment; +} MemAddress; + +static MemAddress addresses[] = { + {0x03d0, "Enter BASIC"}, + {0x03d2, "Reconnect DOS"}, + {0x03d9, "Cow Sound"}, + {0x03ea, "Reconnect IO"}, + {0x03f2, "Control-Reset Vector"}, + {0x03f5, "Ampersand Vector"}, + {0x03f8, "Control-Y Vector"}, + {0x0400, "Text Screen"}, + {0x0800, "Text Screen 2"}, + {0x0803, "Enter assembler"}, + {0x2000, "Hires screen"}, + {0x4000, "Hires screen 2"}, + {0x9dbf, "Reconnect DOS 3.3"}, + {0xa56e, "CATALOG"}, + {0xc000, "KBD / 80STOREOFF"}, + {0xc001, "80STOREON"}, + {0xc002, "RDMAINRAM"}, + {0xc003, "RDCARDRAM"}, + {0xc004, "WRMAINRAM"}, + {0xc005, "WRCARDRAM"}, + {0xc006, "SETSLOTCXROM"}, + {0xc007, "SETINTCXROM"}, + {0xc008, "SETSTDZP"}, + {0xc009, "SETALTZP"}, + {0xc00a, "SETINTC3ROM"}, + {0xc00b, "SETSLOTC3ROM"}, + {0xc00c, "CLR80VID"}, + {0xc00d, "SET80VID"}, + {0xc00e, "CLRALTCHAR"}, + {0xc00f, "SETALTCHAR"}, + {0xc010, "KBDSTRB"}, + {0xc011, "RDLCBNK2"}, + {0xc012, "RDLCRAM"}, + {0xc013, "RDRAMRD"}, + {0xc014, "RDRAMWRT"}, + {0xc015, "RDCXROM"}, + {0xc016, "RDALTZP"}, + {0xc017, "RDC3ROM"}, + {0xc018, "RD80STORE"}, + {0xc019, "RDVBL"}, + {0xc01a, "RDTEXT"}, + {0xc01b, "RDMIXED"}, + {0xc01c, "RDPAGE2"}, + {0xc01d, "RDHIRES"}, + {0xc01e, "RDALTCHAR"}, + {0xc01f, "RD80VID"}, + {0xc020, "TAPEOUT"}, + {0xc021, "MONOCOLOR"}, + {0xc022, "TBCOLOR"}, + {0xc023, "VGCINT"}, + {0xc024, "MOUSEDATA"}, + {0xc025, "KEYMODREG"}, + {0xc026, "DATAREG"}, + {0xc027, "KMSTATUS"}, + {0xc028, "ROMBANK"}, + {0xc029, "NEWVIDEO"}, + {0xc02b, "LANGSEL"}, + {0xc02c, "CHARROM"}, + {0xc02d, "SLTROMSEL"}, + {0xc02e, "VERTCNT"}, + {0xc02f, "HORIZCNT"}, + {0xc030, "SPKR"}, + {0xc031, "DISKREG"}, + {0xc032, "SCANINT"}, + {0xc033, "CLOCKDATA"}, + {0xc034, "CLOCKCTL"}, + {0xc035, "SHADOW"}, + {0xc036, "CYAREG"}, + {0xc037, "DMAREG"}, + {0xc038, "SCCBREG"}, + {0xc039, "SCCAREG"}, + {0xc03a, "SCCBDATA"}, + {0xc03b, "SCCADATA"}, + {0xc03c, "SOUNDCTL"}, + {0xc03d, "SOUNDDATA"}, + {0xc03e, "SOUNDADRL"}, + {0xc03f, "SOUNDADRH"}, + {0xc040, "STROBE"}, + {0xc041, "INTEN"}, + {0xc044, "MMDELTAX"}, + {0xc045, "MMDELTAY"}, + {0xc046, "DIAGTYPE"}, + {0xc047, "CLRVBLINT"}, + {0xc048, "CLRXYINT"}, + {0xc050, "TXTCLR"}, + {0xc051, "TXTSET"}, + {0xc052, "MIXCLR"}, + {0xc053, "MIXSET"}, + {0xc054, "TXTPAGE1"}, + {0xc055, "TXTPAGE2"}, + {0xc056, "LORES"}, + {0xc057, "HIRES"}, + {0xc058, "CLRAN0"}, + {0xc059, "SETAN0"}, + {0xc05a, "CLRAN1"}, + {0xc05b, "SETAN1"}, + {0xc05c, "CLRAN2"}, + {0xc05d, "SETAN2"}, + {0xc05e, "DHIRESON"}, + {0xc05f, "DHIRESOFF"}, + {0xc060, "TAPEIN"}, + {0xc061, "RDBTN0"}, + {0xc062, "RDBTN1"}, + {0xc063, "RDBTN2"}, + {0xc064, "PADDL0"}, + {0xc065, "PADDL1"}, + {0xc066, "PADDL2"}, + {0xc067, "PADDL3"}, + {0xc068, "STATEREG"}, + {0xc06d, "TESTREG"}, + {0xc06e, "CLTRM"}, + {0xc06f, "ENTM"}, + {0xc070, "PTRIG"}, + {0xc073, "BANKSEL"}, + {0xc07e, "IOUDISON"}, + {0xc07f, "IOUDISOFF"}, + {0xc081, "ROMIN"}, + {0xc083, "LCBANK2"}, + {0xc08b, "LCBANK1"}, + {0xc0e0, "PH0 off"}, + {0xc0e1, "PH0 on"}, + {0xc0e2, "PH1 off"}, + {0xc0e3, "PH1 on"}, + {0xc0e4, "PH2 off"}, + {0xc0e5, "PH2 on"}, + {0xc0e6, "PH3 off"}, + {0xc0e7, "PH3 on"}, + {0xc0e8, "motor off"}, + {0xc0e9, "motor on"}, + {0xc0ea, "drive 1"}, + {0xc0eb, "drive 2"}, + {0xc0ec, "q6 off"}, + {0xc0ed, "q6 on"}, + {0xc0ee, "q7 off"}, + {0xc0ef, "q7 on"}, + {0xc311, "AUXMOVE"}, + {0xc314, "XFER"}, + {0xc50d, "Smartport"}, + {0xc70d, "Smartport"}, + {0xcfff," CLRROM"}, + {0xd1fc, "Hires Find"}, + {0xd2c9, "Hires bg"}, + {0xd331, "Hires graphics bg"}, + {0xd33a, "Hires DRAW1"}, + {0xd3b9, "Hires SHLOAD"}, + {0xd683, "Clear FOR"}, + {0xdafb, "Carriage Return"}, + {0xe000, "Reset Int Basic"}, + {0xe04b, "IntBASIC LIST"}, + {0xe5ad, "NEW"}, + {0xe5b7, "PLOT"}, + {0xe836, "IntBASIC CHAIN"}, + {0xefec, "IntBASIC RUN"}, + {0xf07c, "IntBASIC LOAD"}, + {0xf0e0, "Leave monitor"}, + {0xf123, "DRAW shape"}, + {0xf14f, "Plot point"}, + {0xf171, "IntBASIC TRACE ON"}, + {0xf176, "IntBASIC TRACE OFF"}, + {0xf30a, "IntBASIC CON"}, + {0xf317, "RESUME"}, + {0xf328, "Clear error"}, + {0xf3de, "HGR"}, + {0xf3e4, "Show hires"}, + {0xf3f2, "Clear hires"}, + {0xf3f6, "Clear hires color"}, + {0xf666, "Enter assembler"}, + {0xf800, "PLOT"}, + {0xf80e, "PLOT1"}, + {0xf819, "HLINE"}, + {0xf828, "VLINE"}, + {0xf832, "CLRSCR"}, + {0xf836, "CLRTOP"}, + {0xf838, "Clear lores y"}, + {0xf83c, "Clear rect"}, + {0xf847, "GBASCALC"}, + {0xf85e, "Add 3 COLOR"}, + {0xf85f, "NXTCOL"}, + {0xf864, "SETCOL"}, + {0xf871, "SCRN"}, + {0xf88c, "INSDS1.2"}, + {0xf88e, "INSDS2"}, + {0xf890, "GET816LEN"}, + {0xf8d0, "INSTDSP"}, + {0xf940, "PRNTYX"}, + {0xf941, "PRNTAX"}, + {0xf944, "PRNTX"}, + {0xf948, "PRBLNK"}, + {0xf94a, "PRBL2"}, + {0xf94c, "Print X blank"}, + {0xf953, "PCADJ"}, + {0xf962, "TEXT2COPY"}, + {0xfa40, "OLDIRQ"}, + {0xfa4c, "BREAK"}, + {0xfa59, "OLDBRK"}, + {0xfa62, "RESET"}, + {0xfaa6, "PWRUP"}, + {0xfaba, "SLOOP"}, + {0xfad7, "REGDSP"}, + {0xfb19, "RTBL"}, + {0xfb1e, "PREAD"}, + {0xfb21, "PREAD4"}, + {0xfb2f, "INIT"}, + {0xfb39, "SETTXT"}, + {0xfb40, "SETGR"}, + {0xfb4b, "SETWND"}, + {0xfb51, "SETWND2"}, + {0xfb5b, "TABV"}, + {0xfb60, "APPLEII"}, + {0xfb6f, "SETPWRC"}, + {0xfb78, "VIDWAIT"}, + {0xfb88, "KBDWAIT"}, + {0xfbb3, "VERSION"}, + {0xfbbf, "ZIDBYTE2"}, + {0xfbc0, "ZIDBYTE"}, + {0xfbc1, "BASCALC"}, + {0xfbdd, "BELL1"}, + {0xfbe2, "BELL1.2"}, + {0xfbe4, "BELL2"}, + {0xfbf0, "STORADV"}, + {0xfbf4, "ADVANCE"}, + {0xfbfd, "VIDOUT"}, + {0xfc10, "BS"}, + {0xfc1a, "UP"}, + {0xfc22, "VTAB"}, + {0xfc24, "VTABZ"}, + {0xfc2c, "ESC"}, + {0xfc42, "CLREOP"}, + {0xfc58, "HOME"}, + {0xfc62, "CR"}, + {0xfc66, "LF"}, + {0xfc70, "SCROLL"}, + {0xfc9c, "CLREOL"}, + {0xfc9e, "CLREOLZ"}, + {0xfca8, "WAIT"}, + {0xfcb4, "NXTA4"}, + {0xfcba, "NXTA1"}, + {0xfcc9, "HEADR"}, + {0xfd0c, "RDKEY"}, + {0xfd10, "FD10"}, + {0xfd18, "RDKEY1"}, + {0xfd1b, "KEYIN"}, + {0xfd35, "RDCHAR"}, + {0xfd5a, "Wait return"}, + {0xfd5c, "Ring bell wait"}, + {0xfd67, "GETLNZ"}, + {0xfd6a, "GETLN"}, + {0xfd6c, "GETLN0"}, + {0xfd6f, "GETLN1"}, + {0xfd75, "Wait line"}, + {0xfd8b, "CROUT1"}, + {0xfd8e, "CROUT"}, + {0xfd92, "PRA1"}, + {0xfda3, "Print memory"}, + {0xfdda, "PRBYTE"}, + {0xfde3, "PRHEX"}, + {0xfded, "COUT"}, + {0xfdf0, "COUT1"}, + {0xfdf6, "COUTZ"}, + {0xfe1f, "IDROUTINE"}, + {0xfe2c, "MOVE"}, + {0xfe5e, "LIST"}, + {0xfe61, "Disassembler"}, + {0xfe80, "INVERSE"}, + {0xfe84, "NORMAL"}, + {0xfe86, "Set I"}, + {0xfe89, "SETKBD"}, + {0xfe8b, "INPORT"}, + {0xfe93, "SETVID"}, + {0xfe95, "OUTPORT"}, + {0xfeb0, "Jump BASIC"}, + {0xfeb6, "GO"}, + {0xfebf, "Display regs"}, + {0xfec2, "Perform trace"}, + {0xfecd, "WRITE"}, + {0xfefd, "READ"}, + {0xff2d, "PRERR"}, + {0xff3a, "BELL"}, + {0xff3f, "RESTORE"}, + {0xff44, "RSTR1"}, + {0xff4a, "SAVE"}, + {0xff4c, "SAV1"}, + {0xff58, "IORTS"}, + {0xff59, "OLDRST"}, + {0xff65, "MON"}, + {0xff69, "MONZ"}, + {0xff6c, "MONZ2"}, + {0xff70, "MONZ4"}, + {0xff8a, "DIG"}, + {0xffa7, "GETNUM"}, + {0xffad, "NXTCHR"}, + {0xffbe, "TOSUB"}, + {0xffc7, "ZMODE"}, + {0xe01e04, "StdText"}, + {0xe01e08, "StdLine"}, + {0xe01e0c, "StdRect"}, + {0xe01e10, "StdRRect"}, + {0xe01e14, "StdOval"}, + {0xe01e18, "StdArc"}, + {0xe01e1c, "StdPoly"}, + {0xe01e20, "StdRgn"}, + {0xe01e24, "StdPixels"}, + {0xe01e28, "StdComment"}, + {0xe01e2c, "StdTxMeas"}, + {0xe01e30, "StdTxBnds"}, + {0xe01e34, "StdGetPic"}, + {0xe01e38, "StdPutPic"}, + {0xe01e98, "ShieldCursor"}, + {0xe01e9c, "UnshieldCursor"}, + {0xe10000, "System Tool dispatch"}, + {0xe10004, "System Tool dispatch"}, + {0xe10008, "User Tool dispatch"}, + {0xe1000c, "User Tool dispatch"}, + {0xe10010, "Interrupt manager"}, + {0xe10014, "COP manager"}, + {0xe10018, "Abort manager"}, + {0xe1001c, "System death manager"}, + {0xe10020, "AppleTalk interrupt"}, + {0xe10024, "Serial interrupt"}, + {0xe10028, "Scanline interrupt"}, + {0xe1002c, "Sound interrupt"}, + {0xe10030, "VBlank interrupt"}, + {0xe10034, "Mouse interrupt"}, + {0xe10038, "250ms interrupt"}, + {0xe1003c, "Keyboard interrupt"}, + {0xe10040, "ADB Response"}, + {0xe10044, "ADB SRQ"}, + {0xe10048, "DA manager"}, + {0xe1004c, "Flush Buffer"}, + {0xe10050, "KbdMicro interrupt"}, + {0xe10054, "1s interrupt"}, + {0xe10058, "External VGC interrupt"}, + {0xe1005c, "Ohter interrupt"}, + {0xe10060, "Cursor update"}, + {0xe10064, "IncBusy"}, + {0xe10068, "DecBusy"}, + {0xe1006c, "Bell vector"}, + {0xe10070, "Break vector"}, + {0xe10074, "Trace vector"}, + {0xe10078, "Step vector"}, + {0xe1007c, "ROM disk"}, + {0xe10080, "ToWriteBram"}, + {0xe10084, "ToReadBram"}, + {0xe10088, "ToWriteTime"}, + {0xe1008c, "ToReadTime"}, + {0xe10090, "ToCtrlPanel"}, + {0xe10094, "ToBramSetup"}, + {0xe10098, "ToPrintMsg8"}, + {0xe1009c, "ToPrintMsg16"}, + {0xe100a0, "Native Ctl-Y"}, + {0xe100a4, "ToAltDispCDA"}, + {0xe100a8, "Prodos 16"}, + {0xe100ac, "OS vector"}, + {0xe100b0, "GS/OS"}, + {0xe100b4, "P8 Switch"}, + {0xe100b8, "Public Flags"}, + {0xe100bc, "OS Kind"}, + {0xe100bd, "OS Boot"}, + {0xe100be, "OS Busy"}, + {0xe100c0, "MsgPtr"}, + {0xe10180, "ToBusyStrip"}, + {0xe10184, "ToStrip"}, + {0xe101b2, "MidiInputPoll"}, + {0xe10200, "Memory manager"}, + {0xe10204, "Set System Speed"}, + {0xe10208, "Slot Arbiter"}, + {0xe10220, "Hypercard callback"}, + {0xe10224, "WordForRTL"}, + {0xe11004, "ATLK Basic"}, + {0xe11008, "ATLK Pascal"}, + {0xe1100c, "ATLK RamGoComp"}, + {0xe11010, "ATLK SoftReset"}, + {0xe11014, "ATLK RamDispatch"}, + {0xe11018, "ATLK RamForbid"}, + {0xe1101c, "ATLK RamPermit"}, + {0xe11020, "ATLK ProEntry"}, + {0xe11022, "ATLK ProDOS"}, + {0xe11026, "ATLK SerStatus"}, + {0xe1102a, "ATLK SerWrite"}, + {0xe1102e, "ATLK SerRead"}, + {0xe1103e, "ATLK PFI"}, + {0xe1d600, "ATLK CmdTable"}, + {0xe1da00, "ATLK TickCount"} +}; + +#define numAddresses (sizeof(addresses) / sizeof(addresses[0])) + +static const char *addressLookup(uint32_t addr) { + for (int i = 0; i < numAddresses; i++) { + if (addresses[i].address >= addr) { + if (addresses[i].address == addr) + return addresses[i].comment; + break; + } + } + if (addr & ~0xffff) + return addressLookup(addr & 0xffff); // try pageless + return NULL; +} + +#endif diff --git a/extract/decrunch.c b/extract/decrunch.c new file mode 100644 index 0000000..58c4180 --- /dev/null +++ b/extract/decrunch.c @@ -0,0 +1,196 @@ +#include +#include +#include +#include + +static inline uint32_t fourcc(const char *p) { + return (p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3]; +} + +static inline uint16_t r16(uint8_t *p) { + uint16_t r = *p++; + r |= *p++ << 8; + return r; +} + +static inline void w16(uint8_t *p, uint16_t v) { + *p++ = v & 0xff; + *p++ = (v >> 8) & 0xff; +} + +static inline uint32_t r24(uint8_t *p) { + uint32_t r = *p++; + r |= *p++ << 8; + r |= *p++ << 16; + return r; +} + +static inline uint32_t r32(uint8_t *p) { + uint32_t r = *p++; + r |= *p++ << 8; + r |= *p++ << 16; + r |= *p++ << 24; + return r; +} + +static inline uint32_t r4(uint8_t *p) { + uint32_t r = *p++ << 24; + r |= *p++ << 16; + r |= *p++ << 8; + r |= *p++; + return r; +} + +static void copyOps(uint8_t **to, uint8_t **from, uint16_t num) { + uint8_t *dest = *to - num; + uint8_t *src = *from - num; + while (num > 0) { + num--; + dest[num] = src[num]; + } + *to = dest; + *from = src; +} + +static uint8_t adjustPage(uint64_t pos, uint16_t delta, uint8_t page) { + uint64_t adjusted = (pos - delta) & ~0xff; + uint64_t orig = pos & ~0xff; + if (adjusted != orig) + page += (orig - adjusted) >> 8; + return page; +} + +static uint32_t decrunch(uint8_t **data) { + uint8_t *ptr = *data; + uint32_t opsOfs = r24(ptr); + uint8_t *ops = ptr + opsOfs; + + w16(ptr, r16(ops)); // overwrite offset to opcodes + w16(ptr + 2, r16(ops + 2)); + uint16_t numCopy = ops[4]; + uint8_t totalPages = ops[5]; + uint32_t outlen = r16(ops + 6) * 256; + *data = realloc(*data, outlen); + ptr = *data + outlen; + ops = *data + opsOfs; + uint8_t page = 0; + if (page < totalPages) + page = adjustPage(ptr - *data, numCopy, page); + if (page > totalPages) + page = totalPages; + copyOps(&ptr, &ops, numCopy); + + do { + uint8_t op = *--ops; + if (op == 0) { + numCopy = 0x100; + } else { + uint16_t moveOfs, numMove; + if (op & 0x80) { + if (op & 0x40) { + numMove = op & 0x3f; + if (numMove < 5) { + numMove <<= 8; + numMove |= *--ops; + } + numCopy = *--ops; + } else { + numMove = 4; + numCopy = op & 0x3f; + if (numCopy == 0x3f) + numCopy = *--ops; + } + moveOfs = *--ops; + if (moveOfs <= page) { + moveOfs <<= 8; + moveOfs |= *--ops; + } else { + moveOfs -= page; + } + } else { + if (op & 0x40) { + moveOfs = *--ops; + numMove = 3; + numCopy = op & 0x3f; + } else { + numCopy = 0; + numMove = 2; + moveOfs = op & 0x3f; + } + } + uint8_t *from = ptr + moveOfs; + if (page < totalPages) + page = adjustPage(ptr - *data, numMove, page); + if (page > totalPages) + page = totalPages; + copyOps(&ptr, &from, numMove); + } + if (numCopy) { + if (page < totalPages) + page = adjustPage(ptr - *data, numCopy, page); + if (page > totalPages) + page = totalPages; + copyOps(&ptr, &ops, numCopy); + } + } while (ops > *data); + + return outlen; +} + + +int main(int argc, char **argv) { + if (argc < 5) { + fprintf(stderr, "Usage: %s block numblocks [raw]\n", argv[0]); + fprintf(stderr," raw is optional, and just saves the block without decrunching\n"); + return -1; + } + FILE *f = fopen(argv[1], "rb"); + if (!f) { + fprintf(stderr, "Couldn't open %s\n", argv[1]); + return -1; + } + fseek(f, 0, SEEK_END); + int len = ftell(f); + fseek(f, 0, SEEK_SET); + uint8_t *data = malloc(len); + fread(data, 1, len, f); + fclose(f); + uint8_t is2mg = 1; + if (len < 0x16 || r4(data) != fourcc("2IMG") || r32(data + 0xc) != 1) + is2mg = 0; + + uint32_t numBlocks = is2mg ? r32(data + 0x14) : len / 512; + uint32_t diskofs = is2mg ? r32(data + 0x18) : 0; + + int block = atoi(argv[2]); + if (block >= numBlocks) { + fprintf(stderr, "Block too large\n"); + return -1; + } + int num = atoi(argv[3]); + if (block + num > numBlocks) { + fprintf(stderr, "Too many blocks\n"); + return -1; + } + + uint8_t *raw = data; + if (is2mg) { + raw = malloc(num * 512); + memcpy(raw, data + diskofs + block * 512, num * 512); + free(data); + } + + if (argc == 6 && strcmp(argv[5], "raw") == 0) { + f = fopen(argv[4], "wb"); + fwrite(raw, 512, num, f); + fclose(f); + return 0; + } + + uint32_t outlen = decrunch(&raw); + + f = fopen(argv[4], "wb"); + fwrite(raw, 1, outlen, f); + fclose(f); + return 0; +} diff --git a/extract/disasm.c b/extract/disasm.c new file mode 100644 index 0000000..f2493df --- /dev/null +++ b/extract/disasm.c @@ -0,0 +1,340 @@ +#include +#include +#include +#include +#include "65816.h" +#include "addresses.h" +#include "tools.h" +#include "prodos8.h" +#include "prodos16.h" +#include "smartport.h" + +static inline uint32_t fourcc(const char *p) { + return (p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3]; +} + +static inline uint16_t r16(uint8_t *p) { + uint16_t r = *p++; + r |= *p++ << 8; + return r; +} + +static inline uint32_t r24(uint8_t *p) { + uint32_t r = *p++; + r |= *p++ << 8; + r |= *p++ << 16; + return r; +} + +static inline uint32_t r32(uint8_t *p) { + uint32_t r = *p++; + r |= *p++ << 8; + r |= *p++ << 16; + r |= *p++ << 24; + return r; +} + +static inline uint32_t r4(uint8_t *p) { + uint32_t r = *p++ << 24; + r |= *p++ << 16; + r |= *p++ << 8; + r |= *p++; + return r; +} + +static void disasm(uint8_t *ptr, uint32_t addr, int len) { + uint8_t *end = ptr + len; + + uint8_t emu = 0; + uint8_t flags = 0x30; + uint16_t x = 0; + uint32_t val; + int8_t delta; + int16_t delta16; + uint32_t d6; + uint8_t smart = 0, dos8 = 0, dos16 = 0; + + while (ptr < end) { + printf("%02x/%04x:", addr >> 16, addr & 0xffff); + uint8_t *start = ptr; + uint8_t opcode = *ptr++; + + const char *inst = opcodes[opcode].inst; + Address mode = opcodes[opcode].address; + + if (smart == 1 || dos8 == 1) { + mode = DB; + inst = "db"; + } + if (smart == 2 || dos8 == 2 || dos16 == 1) { + mode = DW; + inst = "dw"; + smart = 0; + dos8 = 0; + } + if (smart == 3 || dos16 == 2) { + mode = DD; + inst = "dd"; + smart = 0; + dos16 = 0; + } + + + if (start + addressSizes[mode] > end) { + inst = "db"; + mode = DB; + } + uint16_t width = addressSizes[mode]; + if (mode == IMMM && (emu || (flags & 0x20))) + width--; + if (mode == IMMX && (emu || (flags & 0x10))) + width--; + addr += width; + + for (int i = 0; i < width; i++) { + printf(" %02x", start[i]); + } + for (int i = 0; i < 4 - width; i++) { + printf(" "); + } + + printf(" %s", inst); + for (int i = strlen(inst); i < 8; i++) + printf(" "); + + const char *comments = NULL; + + switch (mode) { + case IMP: + break; + case IMM: + val = *ptr++; + printf("#$%02x", val); + if (opcode == 0xe2) + flags |= val; + else if (opcode == 0xc2) + flags &= ~val; + break; + case IMMM: + if ((flags & 0x20) || emu) + printf("#$%02x", *ptr++); + else { + printf("#$%04x", r16(ptr)); ptr += 2; + } + break; + case IMMX: + if ((flags & 0x10) || emu) { + x = *ptr++; + printf("#$%02x", x); + } else { + x = r16(ptr); ptr += 2; + printf("#$%04x", x); + } + break; + case IMMS: + printf("#$%04x", r16(ptr)); ptr += 2; + break; + case ABSO: + val = r16(ptr); ptr += 2; + printf("$%04x", val); + comments = addressLookup(val); + if (comments) + printf(" ; %s", comments); + break; + case ABL: + val = r24(ptr); ptr += 3; + printf("$%02x/%04x", val >> 16, val & 0xffff); + comments = addressLookup(val); + if (comments) + printf(" ; %s", comments); + break; + case ABX: + printf("$%04x, x", r16(ptr)); ptr += 2; + break; + case ABY: + printf("$%04x, y", r16(ptr)); ptr += 2; + break; + case ABLX: + val = r24(ptr); ptr += 3; + printf("$%02x/%04x, x", val >> 16, val & 0xffff); + break; + case AIX: + printf("($%04x, x)", r16(ptr)); ptr += 2; + break; + case ZP: + printf("$%02x", *ptr++); + break; + case ZPX: + printf("$%02x, x", *ptr++); + break; + case ZPY: + printf("$%02x, y", *ptr++); + break; + case ZPS: + printf("$%02x, s", *ptr++); + break; + case IND: + printf("($%04x)", r16(ptr)); ptr += 2; + break; + case INZ: + printf("($%02x)", *ptr++); + break; + case INL: + printf("[$%02x]", *ptr++); + break; + case INX: + printf("($%02x, x)", *ptr++); + break; + case INY: + printf("($%02x), y", *ptr++); + break; + case INLY: + printf("[$%02x], y", *ptr++); + break; + case INS: + printf("($%02x, s), y", *ptr++); + break; + case REL: + delta = *ptr++; + d6 = delta + addr; + printf("$%04x", d6 & 0xffff); + break; + case RELL: + delta16 = r16(ptr); ptr += 2; + d6 = delta16 + addr; + printf("$%02x/%04x", d6 >> 16, d6 & 0xffff); + break; + case BANK: + val = *ptr++; + printf("$%02x, $%02x", *ptr++, val); + break; + case DB: + printf("$%02x", opcode); + break; + case DW: + val = opcode | (*ptr++ << 8); + printf("$%04x", val); + break; + case DD: + printf("%08x", opcode | r24(ptr) << 8); ptr += 3; + break; + } + + if (smart == 1) { + comments = smartportLookup(opcode); + if (comments) + printf(" ; %s", comments); + if (opcode >= 0x40) { + smart++; + } + smart++; + } + + if (dos8 == 1) { + comments = prodos8Lookup(opcode); + if (comments) + printf(" ; %s", comments); + dos8++; + } + + if (dos16 == 1) { + comments = prodos16Lookup(val); + if (comments) + printf(" ; %s", comments); + dos16++; + } + + if (opcode == 0x18 && ptr[0] == 0xfb) { // clc xce + emu = 0; + printf(" ; 16bit mode"); + } + if (opcode == 0x38 && ptr[0] == 0xfb) { // sec xce + emu = 1; + printf(" ; 8bit mode"); + } + + if (opcode == 0xa2) { // ldx + if (ptr[0] == 0x22) { // jsl + if (r24(ptr + 1) == 0xe10000) { // jsl e1:0000 + comments = toolLookup(x); + if (comments) + printf(" ; %s", comments); + } + } + } + + if (opcode == 0x20) { // JSR + if (val == 0xc50d || val == 0xc70d) + smart = 1; + if (val == 0xbf00) + dos8 = 1; + } + + if (opcode == 0x22) { // JSL + if (val == 0xe100a8) + dos16 = 1; + } + + printf("\n"); + + } +} + +int main(int argc, char **argv) { + if (argc < 3) { + fprintf(stderr, "Usage: %s [block] [num]\n", argv[0]); + fprintf(stderr, " startaddr should be in hex\n"); + fprintf(stderr, " block is optional, 0 = default\n"); + fprintf(stderr, " num is number of blocks, optional\n"); + fprintf(stderr," If file isn't a 2mg, block and num are bytes\n"); + return -1; + } + FILE *f = fopen(argv[1], "rb"); + if (!f) { + fprintf(stderr, "Couldn't open %s\n", argv[1]); + return -1; + } + + fseek(f, 0, SEEK_END); + int len = ftell(f); + fseek(f, 0, SEEK_SET); + uint8_t *data = malloc(len); + fread(data, 1, len, f); + fclose(f); + + uint32_t addr = strtol(argv[2], NULL, 16); + + int block = 0; + if (argc > 3) + block = strtol(argv[3], NULL, 10); + + int num = 0; + if (argc > 4) + num = strtol(argv[4], NULL, 10); + + // if it's a 2img, find appropriate block + if (len > 0x16 && r4(data) == fourcc("2IMG") && r32(data + 0xc) == 1) { + uint32_t numBlocks = r32(data + 0x14); + uint32_t diskofs = r32(data + 0x18); + if (block >= numBlocks) { + fprintf(stderr, "Block too large\n"); + return -1; + } + if (num == 0) + num = numBlocks - block; + if (block + num > numBlocks) { + fprintf(stderr, "Too many blocks\n"); + return -1; + } + disasm(data + diskofs + block * 512, addr, num * 512); + } else { + // not a 2img, just a raw file.. + if (num == 0) + num = len - block; + if (block + num > len) { + fprintf(stderr, "num is too long\n"); + return -1; + } + disasm(data + block, addr, num); + } +} diff --git a/extract/dumptbl.c b/extract/dumptbl.c new file mode 100644 index 0000000..60047e0 --- /dev/null +++ b/extract/dumptbl.c @@ -0,0 +1,55 @@ +#include +#include +#include +#include + +static inline uint16_t r16(uint8_t *p) { + uint16_t r = *p++; + r |= *p++ << 8; + return r; +} + +static inline uint32_t r32(uint8_t *p) { + uint32_t r = *p++; + r |= *p++ << 8; + r |= *p++ << 16; + r |= *p++ << 24; + return r; +} + +int main(int argc, char **argv) { + if (argc != 3) { + fprintf(stderr, "Usage: %s
\n", argv[0]); + return -1; + } + FILE *f = fopen(argv[1], "rb"); + if (!f) { + fprintf(stderr, "Couldn't open %s\n", argv[1]); + return -1; + } + fseek(f, 0, SEEK_END); + int len = ftell(f); + fseek(f, 0, SEEK_SET); + uint8_t *data = malloc(len); + fread(data, 1, len, f); + fclose(f); + + uint32_t addr = strtol(argv[2], NULL, 16); + if (addr > len - 4) { + fprintf(stderr, "Address is too large\n"); + return -1; + } + + uint8_t done = 0; + uint16_t start = r16(data + addr); + addr += 2; + do { + uint32_t to = r32(data + addr); addr += 4; + uint32_t pages = r32(data + addr); addr += 4; + if (pages) + printf("Address: $%02x:%04x Block #%d blocks: %d\n", to >> 16, to & 0xffff, start, pages / 2); + start += pages / 2; + if (pages == 0) + done = 1; + } while (!done); +} diff --git a/extract/prodos16.h b/extract/prodos16.h new file mode 100644 index 0000000..b269385 --- /dev/null +++ b/extract/prodos16.h @@ -0,0 +1,142 @@ +#ifndef __PRODOS16_H__ +#define __PRODOS16_H__ + +typedef struct { + uint16_t call; + const char *name; +} Prodos16; + +static Prodos16 prodos16[] = { + {0x0001, "CREATE"}, + {0x0002, "DESTROY"}, + {0x0004, "CHANGE_PATH"}, + {0x0005, "SET_FILE_INFO"}, + {0x0006, "GET_FILE_INFO"}, + {0x0008, "VOLUME"}, + {0x0009, "SET_PREFIX"}, + {0x000a, "GET_PREFIX"}, + {0x000b, "CLEAR_BACKUP_BIT"}, + {0x0010, "OPEN"}, + {0x0011, "NEWLINE"}, + {0x0012, "READ"}, + {0x0013, "WRITE"}, + {0x0014, "CLOSE"}, + {0x0015, "FLUSH"}, + {0x0016, "SET_MARK"}, + {0x0017, "GET_MARK"}, + {0x0018, "SET_EOF"}, + {0x0019, "GET_EOF"}, + {0x001a, "SET_LEVEL"}, + {0x001b, "GET_LEVEL"}, + {0x001c, "GET_DIR_ENTRY"}, + {0x0020, "GET_DEV_NUM"}, + {0x0021, "GET_LAST_DEV"}, + {0x0022, "READ_BLOCK"}, + {0x0023, "WRITE_BLOCK"}, + {0x0024, "FORMAT"}, + {0x0025, "ERASE_DISK"}, + {0x0027, "GET_NAME"}, + {0x0028, "GET_BOOT_VOL"}, + {0x0029, "QUIT"}, + {0x002a, "GET_VERSION"}, + {0x002c, "D_INFO"}, + {0x0031, "ALLOC_INTERRUPT"}, + {0x0032, "DEALLOCATE_INTERRUPT"}, + {0x0101, "Get_LInfo"}, + {0x0102, "Set_LInfo"}, + {0x0103, "Get_Lang"}, + {0x0104, "Set_Lang"}, + {0x0105, "Error"}, + {0x0106, "Set_Variable"}, + {0x0107, "Version"}, + {0x0108, "Read_Indexed"}, + {0x0109, "Init_Wildcard"}, + {0x010a, "Next_Wildcard"}, + {0x010b, "Read_Variable"}, + {0x010c, "ChangeVector"}, + {0x010d, "Execute"}, + {0x010e, "FastFile"}, + {0x010f, "Direction"}, + {0x0110, "Redirect"}, + {0x0113, "Stop"}, + {0x0114, "ExpandDevices"}, + {0x0115, "UnsetVariable"}, + {0x0116, "Export"}, + {0x0117, "PopVariables"}, + {0x0118, "PushVariables"}, + {0x0119, "SetStopFlag"}, + {0x011a, "ConsoleOut"}, + {0x011b, "SetIODevices"}, + {0x011c, "GetIODevices"}, + {0x011d, "GetCommand"}, + {0x2001, "Create"}, + {0x2002, "Destroy"}, + {0x2003, "OSShutdown"}, + {0x2004, "ChangePath"}, + {0x2005, "SetFileInfo"}, + {0x2006, "GetFileInfo"}, + {0x2007, "JudgeName"}, + {0x2008, "Volume"}, + {0x2009, "SetPrefix"}, + {0x200a, "GetPrefix"}, + {0x200b, "ClearBackup"}, + {0x200c, "SetSysPrefs"}, + {0x200d, "Null"}, + {0x200e, "ExpandPath"}, + {0x200f, "GetSysPrefs"}, + {0x2010, "Open"}, + {0x2011, "NewLine"}, + {0x2012, "Read"}, + {0x2013, "Write"}, + {0x2014, "Close"}, + {0x2015, "Flush"}, + {0x2016, "SetMark"}, + {0x2017, "GetMark"}, + {0x2018, "SetEOF"}, + {0x2019, "GetEOF"}, + {0x201a, "SetLevel"}, + {0x201b, "GetLevel"}, + {0x201c, "GetDirEntry"}, + {0x201d, "BeginSession"}, + {0x201e, "EndSession"}, + {0x201f, "SessionStatus"}, + {0x2020, "GetDevNumber"}, + {0x2024, "Format"}, + {0x2025, "EraseDisk"}, + {0x2026, "ResetCache"}, + {0x2027, "GetName"}, + {0x2028, "GetBoolVol"}, + {0x2029, "Quit"}, + {0x202a, "GetVersion"}, + {0x202b, "GetFSTInfo"}, + {0x202c, "DInfo"}, + {0x202d, "DStatus"}, + {0x202e, "DControl"}, + {0x202f, "DRead"}, + {0x2030, "DWrite"}, + {0x2031, "BindInt"}, + {0x2032, "UnbindInt"}, + {0x2033, "FSTSpecific"}, + {0x2034, "AddNotifyProc"}, + {0x2035, "DelNotifyProc"}, + {0x2036, "DRename"}, + {0x2037, "GetStdRefNum"}, + {0x2038, "GetRefNum"}, + {0x2039, "GetRefInfo"}, + {0x203a, "SetStdRefNum"} +}; + +#define numProdos16 (sizeof(prodos16) / sizeof(prodos16[0])) + +static const char *prodos16Lookup(uint16_t call) { + for (int i = 0; i < numProdos16; i++) { + if (prodos16[i].call >= call) { + if (prodos16[i].call == call) + return prodos16[i].name; + break; + } + } + return NULL; +} + +#endif diff --git a/extract/prodos8.h b/extract/prodos8.h new file mode 100644 index 0000000..4af4d68 --- /dev/null +++ b/extract/prodos8.h @@ -0,0 +1,54 @@ +#ifndef __PRODOS8_H__ +#define __PRODOS8_H__ + +typedef struct { + uint16_t call; + const char *name; +} Prodos8; + +static Prodos8 prodos8[] = { + {0x0040, "ALLOC_INTERRUPT"}, + {0x0041, "DEALLOC_INTERRUPT"}, + {0x0042, "AppleTalk"}, + {0x0043, "SpecialOpenFork"}, + {0x0044, "ByteRangeLock"}, + {0x0065, "QUIT"}, + {0x0080, "READ_BLOCK"}, + {0x0081, "WRITE_BLOCK"}, + {0x0082, "GET_TIME"}, + {0x00c0, "CREATE"}, + {0x00c1, "DESTROY"}, + {0x00c2, "RENAME"}, + {0x00c3, "SetFileInfo"}, + {0x00c4, "GetFileInfo"}, + {0x00c5, "ONLINE"}, + {0x00c6, "SET_PREFIX"}, + {0x00c7, "GET_PREFIX"}, + {0x00c8, "OPEN"}, + {0x00c9, "NEWLINE"}, + {0x00ca, "READ"}, + {0x00cb, "WRITE"}, + {0x00cc, "CLOSE"}, + {0x00cd, "FLUSH"}, + {0x00ce, "SET_MARK"}, + {0x00cf, "GET_MARK"}, + {0x00d0, "SET_EOF"}, + {0x00d1, "GET_EOF"}, + {0x00d2, "SET_BUF"}, + {0x00d3, "GET_BUF"} +}; + +#define numProdos8 (sizeof(prodos8) / sizeof(prodos8[0])) + +static const char *prodos8Lookup(uint16_t call) { + for (int i = 0; i < numProdos8; i++) { + if (prodos8[i].call >= call) { + if (prodos8[i].call == call) + return prodos8[i].name; + break; + } + } + return NULL; +} + +#endif diff --git a/extract/smartport.h b/extract/smartport.h new file mode 100644 index 0000000..6d5043e --- /dev/null +++ b/extract/smartport.h @@ -0,0 +1,45 @@ +#ifndef __SMARTPORT_H_ +#define __SMARTPORT_H_ + +typedef struct { + uint8_t call; + const char *name; +} SmartPort; + +static SmartPort smartport[] = { + {0x00, "Status"}, + {0x01, "Read"}, + {0x02, "Write"}, + {0x03, "Format"}, + {0x04, "Control"}, + {0x05, "Init"}, + {0x06, "Open"}, + {0x07, "Close"}, + {0x08, "Read"}, + {0x09, "Write"}, + {0x40, "Status"}, + {0x41, "Read"}, + {0x42, "Write"}, + {0x43, "Format"}, + {0x44, "Control"}, + {0x45, "Init"}, + {0x46, "Open"}, + {0x47, "Close"}, + {0x48, "Read"}, + {0x49, "Write"} +}; + +#define numSmartPort (sizeof(smartport) / sizeof(smartport[0])) + +static const char *smartportLookup(uint8_t call) { + for (int i = 0; i < numSmartPort; i++) { + if (smartport[i].call >= call) { + if (smartport[i].call == call) + return smartport[i].name; + break; + } + } + return NULL; +} + +#endif diff --git a/extract/tools.h b/extract/tools.h new file mode 100644 index 0000000..7cde087 --- /dev/null +++ b/extract/tools.h @@ -0,0 +1,1288 @@ +#ifndef __TOOLS_H__ +#define __TOOLS_H__ + +typedef struct { + uint16_t id; + const char *name; +} Tool; + +static Tool tools[] = { + {0x0101, "TLBootInit"}, + {0x0102, "MMBootInit"}, + {0x0103, "MTBootInit"}, + {0x0104, "QDBootInit"}, + {0x0105, "DeskBootInit"}, + {0x0106, "EMBootInit"}, + {0x0107, "SchBootInit"}, + {0x0108, "SoundBootInit"}, + {0x0109, "ADBBootInit"}, + {0x010a, "SANEBootInit"}, + {0x010b, "IMBootInit"}, + {0x010c, "TextBootInit"}, + {0x010e, "WindBootInit"}, + {0x010f, "MenuBootInit"}, + {0x0110, "CtlBootInit"}, + {0x0111, "LoaderBootInit"}, + {0x0112, "QDAuxBootInit"}, + {0x0113, "PMBootInit"}, + {0x0114, "LEBootInit"}, + {0x0115, "DialogBootInit"}, + {0x0116, "ScrapBootInit"}, + {0x0117, "SFBootInit"}, + {0x0118, "DUBootInit"}, + {0x0119, "NSBootInit"}, + {0x011a, "SeqBootInit"}, + {0x011b, "FMBootInit"}, + {0x011c, "ListBootInit"}, + {0x011d, "ACEBootInit"}, + {0x011e, "ResourceBootInit"}, + {0x0120, "MIDIBootInit"}, + {0x0121, "VDBootInit"}, + {0x0122, "TEBootInit"}, + {0x0123, "MSBootInit"}, + {0x0125, "AnimBootInit"}, + {0x0201, "TLStartUp"}, + {0x0202, "MMStartUp"}, + {0x0203, "MTStartUp"}, + {0x0204, "QDStartUp"}, + {0x0205, "DeskStartUp"}, + {0x0206, "EMStartUp"}, + {0x0207, "SchStartUp"}, + {0x0208, "SoundStartUp"}, + {0x0209, "ADBStartUp"}, + {0x020a, "SANEStartUp"}, + {0x020b, "IMStartUp"}, + {0x020c, "TextStartUp"}, + {0x020e, "WindStartUp"}, + {0x020f, "MenuStartUp"}, + {0x0210, "CtlStartUp"}, + {0x0211, "LoaderStartUp"}, + {0x0212, "QDAuxStartUp"}, + {0x0213, "PMStartUp"}, + {0x0214, "LEStartUp"}, + {0x0215, "DialogStartUp"}, + {0x0216, "ScrapStartUp"}, + {0x0217, "SFStartUp"}, + {0x0218, "DUStartUp"}, + {0x0219, "NSStartUp"}, + {0x021a, "SeqStartUp"}, + {0x021b, "FMStartUp"}, + {0x021c, "ListStartUp"}, + {0x021d, "ACEStartUp"}, + {0x021e, "ResourceStartUp"}, + {0x0220, "MIDIStartUp"}, + {0x0221, "VDStartUp"}, + {0x0222, "TEStartUp"}, + {0x0223, "MSStartUp"}, + {0x0225, "AnimStartUp"}, + {0x0301, "TLShutDown"}, + {0x0302, "MMShutDown"}, + {0x0303, "MTShutDown"}, + {0x0304, "QDShutDown"}, + {0x0305, "DeskShutDown"}, + {0x0306, "EMShutDown"}, + {0x0307, "SchShutDown"}, + {0x0308, "SoundShutDown"}, + {0x0309, "ADBShutDown"}, + {0x030a, "SANEShutDown"}, + {0x030b, "IMShutDown"}, + {0x030c, "TextShutDown"}, + {0x030e, "WindShutDown"}, + {0x030f, "MenuShutDown"}, + {0x0310, "CtlShutDown"}, + {0x0311, "LoaderShutDown"}, + {0x0312, "QDAuxShutDown"}, + {0x0313, "PMShutDown"}, + {0x0314, "LEShutDown"}, + {0x0315, "DialogShutDown"}, + {0x0316, "ScrapShutDown"}, + {0x0317, "SFShutDown"}, + {0x0318, "DUShutDown"}, + {0x0319, "NSShutDown"}, + {0x031a, "SeqShutDown"}, + {0x031b, "FMShutDown"}, + {0x031c, "ListShutDown"}, + {0x031d, "ACEShutDown"}, + {0x031e, "ResourceShutDown"}, + {0x0320, "MIDIShutDown"}, + {0x0321, "VDShutDown"}, + {0x0322, "TEShutDown"}, + {0x0323, "MSShutDown"}, + {0x0325, "AnimShutDown"}, + {0x0401, "TLVersion"}, + {0x0402, "MMVersion"}, + {0x0403, "MTVersion"}, + {0x0404, "QDVersion"}, + {0x0405, "DeskVersion"}, + {0x0406, "EMVersion"}, + {0x0407, "SchVersion"}, + {0x0408, "SoundVersion"}, + {0x0409, "ADBVersion"}, + {0x040a, "SANEVersion"}, + {0x040b, "IMVersion"}, + {0x040c, "TextVersion"}, + {0x040e, "WindVersion"}, + {0x040f, "MenuVersion"}, + {0x0410, "CtlVersion"}, + {0x0411, "LoaderVersion"}, + {0x0412, "QDAuxVersion"}, + {0x0413, "PMVersion"}, + {0x0414, "LEVersion"}, + {0x0415, "DialogVersion"}, + {0x0416, "ScrapVersion"}, + {0x0417, "SFVersion"}, + {0x0418, "DUVersion"}, + {0x0419, "NSVersion"}, + {0x041a, "SeqVersion"}, + {0x041b, "FMVersion"}, + {0x041c, "ListVersion"}, + {0x041d, "ACEVersion"}, + {0x041e, "ResourceVersion"}, + {0x0420, "MIDIVersion"}, + {0x0421, "VDVersion"}, + {0x0422, "TEVersion"}, + {0x0423, "MSVersion"}, + {0x0425, "AnimVersion"}, + {0x0501, "TLReset"}, + {0x0502, "MMReset"}, + {0x0503, "MTReset"}, + {0x0504, "QDReset"}, + {0x0505, "DeskReset"}, + {0x0506, "EMReset"}, + {0x0507, "SchReset"}, + {0x0508, "SoundReset"}, + {0x0509, "ADBReset"}, + {0x050a, "SANEReset"}, + {0x050b, "IMReset"}, + {0x050c, "TextReset"}, + {0x050e, "WindReset"}, + {0x050f, "MenuReset"}, + {0x0510, "CtlReset"}, + {0x0511, "LoaderReset"}, + {0x0512, "QDAuxReset"}, + {0x0513, "PMReset"}, + {0x0514, "LEReset"}, + {0x0515, "DialogReset"}, + {0x0516, "ScrapReset"}, + {0x0517, "SFReset"}, + {0x0518, "DUReset"}, + {0x0519, "NSReset"}, + {0x051a, "SeqReset"}, + {0x051b, "FMReset"}, + {0x051c, "ListReset"}, + {0x051d, "ACEReset"}, + {0x051e, "ResourceReset"}, + {0x0520, "MIDIReset"}, + {0x0521, "VDReset"}, + {0x0522, "TEReset"}, + {0x0523, "MSReset"}, + {0x0525, "AnimReset"}, + {0x0601, "TLStatus"}, + {0x0602, "MMStatus"}, + {0x0603, "MTStatus"}, + {0x0604, "QDStatus"}, + {0x0605, "DeskStatus"}, + {0x0606, "EMStatus"}, + {0x0607, "SchStatus"}, + {0x0608, "SoundStatus"}, + {0x0609, "ADBStatus"}, + {0x060a, "SANEStatus"}, + {0x060b, "IMStatus"}, + {0x060c, "TextStatus"}, + {0x060e, "WindStatus"}, + {0x060f, "MenuStatus"}, + {0x0610, "CtlStatus"}, + {0x0611, "LoaderStatus"}, + {0x0612, "QDAuxStatus"}, + {0x0613, "PMStatus"}, + {0x0614, "LEStatus"}, + {0x0615, "DialogStatus"}, + {0x0616, "ScrapStatus"}, + {0x0617, "SFStatus"}, + {0x0618, "DUStatus"}, + {0x0619, "NSStatus"}, + {0x061a, "SeqStatus"}, + {0x061b, "FMStatus"}, + {0x061c, "ListStatus"}, + {0x061d, "ACEStatus"}, + {0x061e, "ResourceStatus"}, + {0x0620, "MIDIStatus"}, + {0x0621, "VDStatus"}, + {0x0622, "TEStatus"}, + {0x0623, "MSStatus"}, + {0x0625, "AnimStatus"}, + {0x071d, "ACEInfo"}, + {0x0804, "AddPt"}, + {0x0825, "AnimIdleDebug"}, + {0x0901, "GetTSPtr"}, + {0x0902, "NewHandle"}, + {0x0903, "WriteBRam"}, + {0x0904, "GetAddress"}, + {0x0905, "SaveScrn"}, + {0x0906, "DoWindows"}, + {0x0907, "SchAddTask"}, + {0x0908, "WriteRamBlock"}, + {0x0909, "SendInfo"}, + {0x090a, "SANEFP816"}, + {0x090b, "Multiply"}, + {0x090c, "SetInGlobals"}, + {0x090e, "NewWindow"}, + {0x090f, "MenuKey"}, + {0x0910, "NewControl"}, + {0x0911, "InitialLoad"}, + {0x0912, "CopyPixels"}, + {0x0913, "PrDefault"}, + {0x0914, "LENew"}, + {0x0915, "ErrorSound"}, + {0x0916, "UnloadScrap"}, + {0x0917, "SFGetFile"}, + {0x0919, "AllocGen"}, + {0x091a, "SetIncr"}, + {0x091b, "CountFamilies"}, + {0x091c, "CreateList"}, + {0x091d, "ACECompress"}, + {0x091e, "CreateResourceFile"}, + {0x0920, "MIDIControl"}, + {0x0921, "VDInStatus"}, + {0x0922, "TENew"}, + {0x0923, "SetBasicChan"}, + {0x0925, "StartScene"}, + {0x0a01, "SetTSPtr"}, + {0x0a02, "ReAllocHandle"}, + {0x0a03, "ReadBRam"}, + {0x0a04, "GrafOn"}, + {0x0a05, "RestScrn"}, + {0x0a06, "GetNextEvent"}, + {0x0a07, "SchFlush"}, + {0x0a08, "ReadRamBlock"}, + {0x0a09, "ReadKeyMicroData"}, + {0x0a0a, "SANEDecStr816"}, + {0x0a0b, "SDivide"}, + {0x0a0c, "SetOutGlobals"}, + {0x0a0e, "CheckUpdate"}, + {0x0a0f, "GetMenuBar"}, + {0x0a10, "DisposeControl"}, + {0x0a11, "Restart"}, + {0x0a12, "WaitCursor"}, + {0x0a13, "PrValidate"}, + {0x0a14, "LEDispose"}, + {0x0a15, "NewModalDialog"}, + {0x0a16, "LoadScrap"}, + {0x0a17, "SFPutFile"}, + {0x0a19, "DeAlocGen"}, + {0x0a1a, "ClearIncr"}, + {0x0a1b, "FindFamily"}, + {0x0a1c, "SortList"}, + {0x0a1d, "ACEExpand"}, + {0x0a1e, "OpenResourceFile"}, + {0x0a20, "MIDIDevice"}, + {0x0a21, "VDInSetStd"}, + {0x0a22, "TEKill"}, + {0x0a23, "SetMIDIMode"}, + {0x0a25, "StopScene"}, + {0x0b01, "GetFuncPtr"}, + {0x0b02, "RestoreHandle"}, + {0x0b03, "WriteBParam"}, + {0x0b04, "GrafOff"}, + {0x0b05, "SaveAll"}, + {0x0b06, "EventAvail"}, + {0x0b08, "GetTableAddress"}, + {0x0b09, "ReadKeyMicroMemory"}, + {0x0b0a, "SANEElems816"}, + {0x0b0b, "UDivide"}, + {0x0b0c, "SetErrGlobals"}, + {0x0b0e, "CloseWindow"}, + {0x0b0f, "MenuRefresh"}, + {0x0b10, "KillControls"}, + {0x0b11, "LoadSegNum"}, + {0x0b12, "DrawIcon"}, + {0x0b13, "PrStlDialog"}, + {0x0b14, "LESetText"}, + {0x0b15, "NewModelessDialog"}, + {0x0b16, "ZeroScrap"}, + {0x0b17, "SFPGetFile"}, + {0x0b19, "NoteOn"}, + {0x0b1a, "GetTimer"}, + {0x0b1b, "GetFamInfo"}, + {0x0b1c, "NextMember"}, + {0x0b1d, "ACECompBegin"}, + {0x0b1e, "CloseResourceFile"}, + {0x0b20, "MIDIClock"}, + {0x0b21, "VDInGetStd"}, + {0x0b22, "TESetText"}, + {0x0b23, "PlayNote"}, + {0x0b25, "StartFrameTimer"}, + {0x0c01, "GetWAP"}, + {0x0c02, "AddToOOMQueue"}, + {0x0c03, "ReadBParam"}, + {0x0c04, "GetStandardSCB"}, + {0x0c05, "RestAll"}, + {0x0c06, "GetMouse"}, + {0x0c08, "GetSoundVolume"}, + {0x0c09, "Resync"}, + {0x0c0b, "LongMul"}, + {0x0c0c, "GetInGlobals"}, + {0x0c0e, "Destkop"}, + {0x0c0f, "FlashMenuBar"}, + {0x0c10, "SetCtlTitle"}, + {0x0c11, "UnloadSegNum"}, + {0x0c12, "SpecialRect"}, + {0x0c13, "PrJobDialog"}, + {0x0c14, "LEIdle"}, + {0x0c15, "CloseDialog"}, + {0x0c16, "PutScrap"}, + {0x0c17, "SFPPutFile"}, + {0x0c19, "NoteOff"}, + {0x0c1a, "GetLoc"}, + {0x0c1b, "GetFamNum"}, + {0x0c1c, "DrawMember"}, + {0x0c1d, "ACEExpBegin"}, + {0x0c1e, "AddResource"}, + {0x0c20, "MIDIInfo"}, + {0x0c21, "VDInConvAdj"}, + {0x0c22, "TEGetText"}, + {0x0c23, "StopNote"}, + {0x0c25, "StopFrameTimer"}, + {0x0d01, "SetWAP"}, + {0x0d02, "RemoveFromOOMQueue"}, + {0x0d03, "ReadTimeHex"}, + {0x0d04, "InitColorTable"}, + {0x0d06, "Button"}, + {0x0d08, "SetSoundVolume"}, + {0x0d09, "AsyncADBReceive"}, + {0x0d0b, "LongDivide"}, + {0x0d0c, "GetOutGlobals"}, + {0x0d0e, "SetWTitle"}, + {0x0d0f, "InsertMenu"}, + {0x0d10, "GetCtlTitle"}, + {0x0d11, "LoadSegNum"}, + {0x0d12, "SeedFill"}, + {0x0d13, "PrPixelMap"}, + {0x0d14, "LEClick"}, + {0x0d15, "NewDItem"}, + {0x0d16, "GetScrap"}, + {0x0d17, "SFAllCaps"}, + {0x0d19, "AllNotesOff"}, + {0x0d1a, "SeqAllNotesOff"}, + {0x0d1b, "AddFamily"}, + {0x0d1c, "SelectMember"}, + {0x0d1d, "GetACEExpState"}, + {0x0d1e, "UpdateResourceFile"}, + {0x0d20, "MIDIReadPacket"}, + {0x0d21, "VDKeyControl"}, + {0x0d22, "TEGetTextInfo"}, + {0x0d23, "KillAllNotes"}, + {0x0d25, "SetBackgndPort"}, + {0x0e01, "LoadTools"}, + {0x0e03, "WriteTimeHex"}, + {0x0e04, "SetColorTable"}, + {0x0e05, "InstallNDA"}, + {0x0e06, "StillDown"}, + {0x0e08, "FFStartSound"}, + {0x0e09, "SyncADBReceive"}, + {0x0e0b, "FixRatio"}, + {0x0e0c, "GetErrGlobals"}, + {0x0e0e, "GetWTitle"}, + {0x0e0f, "DeleteMenu"}, + {0x0e10, "HideControl"}, + {0x0e11, "UnloadSeg"}, + {0x0e12, "CalcMask"}, + {0x0e13, "PrOpenDoc"}, + {0x0e14, "LESetSelect"}, + {0x0e15, "RemoveDItem"}, + {0x0e16, "GetScrapHandle"}, + {0x0e17, "SFGetFile2"}, + {0x0e19, "NSSetUpdateRate"}, + {0x0e1a, "SetTrkInfo"}, + {0x0e1b, "InstallFont"}, + {0x0e1c, "GetListDefProc"}, + {0x0e1d, "SetACEExpState"}, + {0x0e1e, "LoadResource"}, + {0x0e20, "MIDIWritePacket"}, + {0x0e21, "VDKeyStatus"}, + {0x0e22, "TEIdle"}, + {0x0e23, "SetRecTrack"}, + {0x0e25, "RefreshBack"}, + {0x0f01, "LoadOneTool"}, + {0x0f03, "ReadAsciiTime"}, + {0x0f04, "GetColorTable"}, + {0x0f05, "InstallCDA"}, + {0x0f06, "WaitMouseUp"}, + {0x0f08, "FFStopSound"}, + {0x0f09, "AbsOn"}, + {0x0f0b, "FixMul"}, + {0x0f0c, "SetInputDevice"}, + {0x0f0e, "SetFrameColor"}, + {0x0f0f, "InsertMItem"}, + {0x0f10, "ShowControl"}, + {0x0f11, "GetLoadSegInfo"}, + {0x0f12, "GetSysIcon"}, + {0x0f13, "PrCloseDoc"}, + {0x0f14, "LEActivate"}, + {0x0f15, "ModalDialog"}, + {0x0f16, "GetScrapSize"}, + {0x0f17, "SFPutFile2"}, + {0x0f19, "NSSetUserUpdateRtn"}, + {0x0f1a, "StartSeq"}, + {0x0f1b, "SetPurgeStart"}, + {0x0f1c, "ResetMember"}, + {0x0f1e, "RemoveResource"}, + {0x0f20, "MIDIRecordSeq"}, + {0x0f21, "VDKeySetKCol"}, + {0x0f22, "TEActivate"}, + {0x0f23, "SetPlayTrack"}, + {0x0f25, "StartChar"}, + {0x1001, "UnloadOneTool"}, + {0x1002, "DisposeHandle"}, + {0x1003, "SetVector"}, + {0x1004, "SetColorEntry"}, + {0x1006, "TickCount"}, + {0x1008, "FFSoundStatus"}, + {0x1009, "AbsOff"}, + {0x100b, "FracMul"}, + {0x100c, "SetOutputDevice"}, + {0x100e, "GetFrameColor"}, + {0x100f, "DeleteMItem"}, + {0x1010, "DrawControls"}, + {0x1011, "GetUserID"}, + {0x1012, "PixelMap2Rgn"}, + {0x1013, "PrOpenPage"}, + {0x1014, "LEDeactivate"}, + {0x1015, "IsDialogEvent"}, + {0x1016, "GetScrapPath"}, + {0x1017, "SFPGetFile2"}, + {0x101a, "StepSeq"}, + {0x101b, "CountFonts"}, + {0x101c, "NewList"}, + {0x101e, "MarkResourceChange"}, + {0x1020, "MIDIStopRecord"}, + {0x1021, "VDKeyGetKRCol"}, + {0x1022, "TEDeactivate"}, + {0x1023, "TrackToChan"}, + {0x1025, "MoveChar"}, + {0x1101, "TLMountVolume"}, + {0x1102, "DisposeAll"}, + {0x1103, "GetVector"}, + {0x1104, "GetColorEntry"}, + {0x1105, "ChooseCDA"}, + {0x1106, "GetDBLTime"}, + {0x1108, "FFGeneratorStatus"}, + {0x1109, "ReadAbs"}, + {0x110b, "FixDiv"}, + {0x110c, "SetErrorDevice"}, + {0x110e, "SelectWindow"}, + {0x110f, "GetSysBar"}, + {0x1110, "HiliteControls"}, + {0x1111, "LGetPathname"}, + {0x1113, "PrClosePage"}, + {0x1114, "LEKey"}, + {0x1115, "DialogSelect"}, + {0x1116, "SetScrapPath"}, + {0x1117, "SFPPutFile2"}, + {0x111a, "StopSeq"}, + {0x111b, "FindFontStats"}, + {0x111c, "DrawMember2"}, + {0x111e, "SetCurResourceFile"}, + {0x1120, "MIDIPlaySeq"}, + {0x1121, "VDKeyGetKGCol"}, + {0x1122, "TEClick"}, + {0x1123, "Locate"}, + {0x1125, "GetCharRecPtr"}, + {0x1201, "TLTextMountVolume"}, + {0x1202, "PurgeHandle"}, + {0x1203, "SetHeartBeat"}, + {0x1204, "SetSCB"}, + {0x1206, "GetCaretTime"}, + {0x1208, "SetSoundMIRQV"}, + {0x1209, "SetAbsScale"}, + {0x120b, "FracDiv"}, + {0x120c, "GetInputDevice"}, + {0x120e, "HideWindow"}, + {0x120f, "SetSysBar"}, + {0x1210, "CtlNewRes"}, + {0x1211, "UserShutdown"}, + {0x1213, "PrPicFile"}, + {0x1214, "LECut"}, + {0x1215, "DlgCut"}, + {0x1216, "GetScrapCount"}, + {0x1217, "SFShowInvisible"}, + {0x121a, "SetInstTable"}, + {0x121b, "LoadFont"}, + {0x121c, "NextMember2"}, + {0x121e, "GetCurResourceFile"}, + {0x1220, "MIDIStopPlay"}, + {0x1221, "VDKeyGetKBCol"}, + {0x1222, "TEUpdate"}, + {0x1223, "SetVelComp"}, + {0x1225, "KillChar"}, + {0x1301, "SaveTextState"}, + {0x1302, "PurgeAll"}, + {0x1303, "DelHeartBeat"}, + {0x1304, "GetSCB"}, + {0x1305, "SetDAStrPtr"}, + {0x1306, "SetSwitch"}, + {0x1308, "SetUserSoundIRQV"}, + {0x1309, "GetAbsScale"}, + {0x130b, "FixRound"}, + {0x130c, "GetOutputDevice"}, + {0x130e, "ShowWindow"}, + {0x130f, "FixMenuBar"}, + {0x1310, "FindControl"}, + {0x1311, "RenamePathname"}, + {0x1312, "IBeamCursor"}, + {0x1313, "PrControl"}, + {0x1314, "LECopy"}, + {0x1315, "DlgCopy"}, + {0x1316, "GetScrapState"}, + {0x1317, "SFReScan"}, + {0x131a, "StartInts"}, + {0x131b, "LoadSysFont"}, + {0x131c, "ResetMember2"}, + {0x131e, "SetCurResourceApp"}, + {0x1320, "MIDIConvert"}, + {0x1321, "VDKeySetKDiss"}, + {0x1322, "TEPaintText"}, + {0x1323, "SetMIDIPort"}, + {0x1325, "LoadActor"}, + {0x1401, "RestoreTextState"}, + {0x1403, "ClrHeartBeat"}, + {0x1404, "SetAllSCBs"}, + {0x1405, "GetDAStrPtr"}, + {0x1406, "PostEvent"}, + {0x1408, "FFSoundDoneStatus"}, + {0x1409, "SRQPoll"}, + {0x140b, "FracSqrt"}, + {0x140c, "GetErrorDevice"}, + {0x140e, "SendBehind"}, + {0x140f, "CountMItems"}, + {0x1410, "TestControl"}, + {0x1412, "WhooshRect"}, + {0x1413, "PrError"}, + {0x1414, "LEPaste"}, + {0x1415, "DlgPaste"}, + {0x1417, "SFMultiGet2"}, + {0x141a, "StopInts"}, + {0x141b, "AddFontVar"}, + {0x141c, "SelectMember2"}, + {0x141e, "GetCurResourceApp"}, + {0x1421, "VDKeyGetKDiss"}, + {0x1422, "TEKey"}, + {0x1423, "SetInstrument"}, + {0x1425, "SetCharScript"}, + {0x1501, "MessageCenter"}, + {0x1503, "SysFailMgr"}, + {0x1504, "ClearScreen"}, + {0x1505, "OpenNDA"}, + {0x1506, "FlushEvents"}, + {0x1508, "FFSetUpSound"}, + {0x1509, "SRQRemove"}, + {0x150b, "FracCos"}, + {0x150c, "InitTextDev"}, + {0x150e, "FrontWindow"}, + {0x150f, "NewMenuBar"}, + {0x1510, "TrackControl"}, + {0x1513, "PrSetError"}, + {0x1514, "LEDelete"}, + {0x1515, "DlgDelete"}, + {0x1517, "SFPMultiGet2"}, + {0x151a, "StartSeqRel"}, + {0x151b, "FixFontMenu"}, + {0x151c, "SortList2"}, + {0x151e, "HomeResourceFile"}, + {0x1521, "VDKeySetNKD"}, + {0x1522, "TEUnsupported"}, + {0x1523, "SeqPlayer"}, + {0x1525, "RunAnimScripts"}, + {0x1601, "SetDefaultTPT"}, + {0x1603, "GetAddr"}, + {0x1604, "SetMasterSCB"}, + {0x1605, "CloseNDA"}, + {0x1606, "GetOSEvent"}, + {0x1608, "FFStartPlaying"}, + {0x1609, "ClearSRQTable"}, + {0x160b, "FracSin"}, + {0x160c, "CtlTextDev"}, + {0x160e, "SetInfoDraw"}, + {0x160f, "GetMHandle"}, + {0x1610, "MoveControl"}, + {0x1613, "PrChoosePrinter"}, + {0x1614, "LEInsert"}, + {0x1615, "DrawDialog"}, + {0x161b, "ChooseFont"}, + {0x161c, "NewList2"}, + {0x161e, "WriteResource"}, + {0x1621, "VDKeyGetNKD"}, + {0x1622, "TECut"}, + {0x1623, "SetTempo"}, + {0x1625, "FillAddrTable"}, + {0x1701, "MessageByName"}, + {0x1703, "ReadMouse"}, + {0x1704, "GetMasterSCB"}, + {0x1705, "SystemClick"}, + {0x1706, "OSEventAvail"}, + {0x1708, "SetDOCReg"}, + {0x170b, "FixATan2"}, + {0x170c, "StatusTextDev"}, + {0x170e, "FindWindow"}, + {0x170f, "SetBarColors"}, + {0x1710, "DragControl"}, + {0x1713, "GetDeviceName"}, + {0x1714, "LEUpdate"}, + {0x1715, "Alert"}, + {0x171b, "ItemID2FamNum"}, + {0x171c, "ListKey"}, + {0x171e, "ReleaseResource"}, + {0x1721, "VDOutSetStd"}, + {0x1722, "TECopy"}, + {0x1723, "SetCallBack"}, + {0x1725, "CompileRect"}, + {0x1801, "StartUpTools"}, + {0x1802, "GetHandleSize"}, + {0x1803, "InitMouse"}, + {0x1804, "OpenPort"}, + {0x1805, "SystemEdit"}, + {0x1806, "SetEventMask"}, + {0x1808, "ReadDOCReg"}, + {0x180b, "HiWord"}, + {0x180c, "WriteChar"}, + {0x180e, "TrackGoAway"}, + {0x180f, "GetBarColors"}, + {0x1810, "SetCtlIcons"}, + {0x1813, "PrGetPrinterSpecs"}, + {0x1814, "LETextBox"}, + {0x1815, "StopAlert"}, + {0x181b, "FMSetSysFont"}, + {0x181c, "CompareStrings"}, + {0x181e, "DetachResource"}, + {0x1821, "VDOutGetStd"}, + {0x1822, "TEPaste"}, + {0x1823, "SysExOut"}, + {0x1825, "StartTockTask"}, + {0x1901, "ShutDownTools"}, + {0x1902, "SetHandleSize"}, + {0x1903, "SetMouse"}, + {0x1904, "InitPort"}, + {0x1905, "SystemTask"}, + {0x1906, "FakeMouse"}, + {0x190b, "LoWord"}, + {0x190c, "ErrWriteChar"}, + {0x190e, "MoveWindow"}, + {0x190f, "SetMTitleStart"}, + {0x1910, "SetCtlValue"}, + {0x1913, "PrDevPrChanged"}, + {0x1914, "LEFromScrap"}, + {0x1915, "NoteAlert"}, + {0x191b, "FMGetSysFID"}, + {0x191e, "UniqueResourceID"}, + {0x1921, "VDOutControl"}, + {0x1922, "TEClear"}, + {0x1923, "SetBeat"}, + {0x1925, "FireTockTask"}, + {0x1a01, "GetMsgHandle"}, + {0x1a02, "FindHandle"}, + {0x1a03, "HomeMouse"}, + {0x1a04, "ClosePort"}, + {0x1a05, "SystemEvent"}, + {0x1a06, "SetAutokeyLimit"}, + {0x1a0b, "Long2Fix"}, + {0x1a0c, "WriteLine"}, + {0x1a0e, "DragWindow"}, + {0x1a0f, "GetMTitleStart"}, + {0x1a10, "GetCtlValue"}, + {0x1a13, "PrDevstartup"}, + {0x1a14, "LEToScrap"}, + {0x1a15, "CautionAlert"}, + {0x1a1b, "FMGetCurFID"}, + {0x1a1e, "SetResourceID"}, + {0x1a21, "VDOutStatus"}, + {0x1a22, "TEInsert"}, + {0x1a23, "MIDIMessage"}, + {0x1a25, "SetForegndPort"}, + {0x1b01, "AcceptRequests"}, + {0x1b02, "FreeMem"}, + {0x1b03, "ClearMouse"}, + {0x1b04, "SetPort"}, + {0x1b05, "GetNumNDAs"}, + {0x1b06, "GetKeyTranslation"}, + {0x1b0b, "Fix2Long"}, + {0x1b0c, "ErrWriteLine"}, + {0x1b0e, "GrowWindow"}, + {0x1b0f, "GetMenuMgrPort"}, + {0x1b10, "SetCtlParams"}, + {0x1b13, "PrDevShutdown"}, + {0x1b14, "LEScrapHandle"}, + {0x1b15, "ParamText"}, + {0x1b1b, "FamNum2ItemID"}, + {0x1b1e, "GetResourceAttr"}, + {0x1b21, "VDGetFeatures"}, + {0x1b22, "TEReplace"}, + {0x1b23, "LocateEnd"}, + {0x1b25, "SetAnimWindow"}, + {0x1c01, "SendRequests"}, + {0x1c02, "MaxBlock"}, + {0x1c03, "ClampMouse"}, + {0x1c04, "GetPort"}, + {0x1c05, "CloseNDAbyWinPtr"}, + {0x1c06, "SetKeyTranslation"}, + {0x1c0b, "Fix2Frac"}, + {0x1c0c, "WriteString"}, + {0x1c0e, "SizeWindow"}, + {0x1c0f, "CalcMenuSize"}, + {0x1c10, "GetCtlParams"}, + {0x1c13, "PrDevOpen"}, + {0x1c14, "LEGetScrapLen"}, + {0x1c15, "SetDAFont"}, + {0x1c1b, "InstallWithStats"}, + {0x1c1e, "SetResourceAttr"}, + {0x1c21, "VDInControl"}, + {0x1c22, "TEGetSelection"}, + {0x1c23, "Merge"}, + {0x1d02, "TotalMem"}, + {0x1d03, "GetMouseClamp"}, + {0x1d04, "SetPortLoc"}, + {0x1d05, "CloseAllNDAs"}, + {0x1d0b, "Frac2Fix"}, + {0x1d0c, "ErrWriteString"}, + {0x1d0e, "TaskMaster"}, + {0x1d0f, "SetMTitleWidth"}, + {0x1d10, "DragRect"}, + {0x1d13, "PrDevRead"}, + {0x1d14, "LESetScrapLen"}, + {0x1d1e, "GetResourceSize"}, + {0x1d21, "VDGGControl"}, + {0x1d22, "TESsetSelection"}, + {0x1d23, "DeleteTrack"}, + {0x1e02, "CheckHandle"}, + {0x1e03, "PosMouse"}, + {0x1e04, "GetPortLoc"}, + {0x1e05, "FixAppleMenu"}, + {0x1e0b, "Fix2X"}, + {0x1e0c, "TextWriteBlock"}, + {0x1e0e, "BeginUpdate"}, + {0x1e0f, "GetMTitleWidth"}, + {0x1e10, "GrowSize"}, + {0x1e13, "PrDevWrite"}, + {0x1e14, "LESetHilite"}, + {0x1e15, "GetControlDItem"}, + {0x1e1e, "MatchResourceHandle"}, + {0x1e21, "VDGGStatus"}, + {0x1e22, "TEGetSelectionStyle"}, + {0x1e23, "SetMetro"}, + {0x1f02, "CompactMem"}, + {0x1f03, "ServeMouse"}, + {0x1f04, "SetPortRect"}, + {0x1f05, "AddToRunQ"}, + {0x1f0b, "Frac2X"}, + {0x1f0c, "ErrWriteBlock"}, + {0x1f0e, "EndUpdate"}, + {0x1f0f, "SetMenuFlag"}, + {0x1f10, "GetCtlDPage"}, + {0x1f13, "PrDevClose"}, + {0x1f14, "LESetCaret"}, + {0x1f15, "GetIText"}, + {0x1f1e, "GetOpenFileRefNum"}, + {0x1f22, "TEStyleChange"}, + {0x1f23, "GetMSData"}, + {0x2002, "HLock"}, + {0x2003, "GetNewID"}, + {0x2004, "GetPortRect"}, + {0x2005, "RemoveFromRunQ"}, + {0x200b, "X2FIx"}, + {0x200c, "WriteCString"}, + {0x200e, "GetWMgrPort"}, + {0x200f, "GetMenuFlag"}, + {0x2010, "SetCtlAction"}, + {0x2011, "InitialLoad2"}, + {0x2013, "PrDevStatus"}, + {0x2014, "LETextBox2"}, + {0x2015, "SetIText"}, + {0x201e, "CountTypes"}, + {0x2022, "TEOffsetToPoint"}, + {0x2023, "ConvertToTime"}, + {0x2102, "HLockAll"}, + {0x2103, "DeleteID"}, + {0x2104, "SetPortSize"}, + {0x2105, "RemoveCDA"}, + {0x210b, "X2Frac"}, + {0x210c, "ErrWriteCString"}, + {0x210e, "PinRect"}, + {0x210f, "SetMenuTitle"}, + {0x2110, "GetCtlAction"}, + {0x2111, "GetUserID2"}, + {0x2113, "PrDevAsyncRead"}, + {0x2114, "LESetJust"}, + {0x2115, "SelectIText"}, + {0x211e, "GetIndType"}, + {0x2122, "TEPointToOffset"}, + {0x2123, "ConvertToMeasure"}, + {0x2202, "HUnlock"}, + {0x2203, "StatusID"}, + {0x2204, "MovePortTo"}, + {0x2205, "RemoveNDA"}, + {0x220b, "Int2Hex"}, + {0x220c, "ReadChar"}, + {0x220e, "HiliteWindow"}, + {0x220f, "GetMenuTitle"}, + {0x2210, "SetCtlRefCon"}, + {0x2211, "LGetPathname2"}, + {0x2213, "PrDevWriteBackground"}, + {0x2214, "LEGetTextHand"}, + {0x2215, "HideDItem"}, + {0x221e, "CountResources"}, + {0x2222, "TEGetDefProc"}, + {0x2223, "MSSuspend"}, + {0x2302, "HUnlockAll"}, + {0x2303, "IntSource"}, + {0x2304, "SetOrigin"}, + {0x2305, "GetIndDAInfo"}, + {0x230b, "Long2Hex"}, + {0x230c, "TextReadBlock"}, + {0x230e, "ShowHide"}, + {0x230f, "MenuGlobal"}, + {0x2310, "GetCtlRefCon"}, + {0x2313, "PrDriverVer"}, + {0x2314, "LEGetTextLen"}, + {0x2315, "ShowDItem"}, + {0x231e, "GetIndResource"}, + {0x2322, "TEGetRuler"}, + {0x2323, "MSResume"}, + {0x2402, "SetPurge"}, + {0x2403, "FWEntry"}, + {0x2404, "SetClip"}, + {0x2405, "CallDeskAcc"}, + {0x240b, "Hex2Int"}, + {0x240c, "ReadLine"}, + {0x240e, "BringToFront"}, + {0x240f, "SetMItem"}, + {0x2410, "EraseControl"}, + {0x2413, "PrPortVer"}, + {0x2414, "GetLEDefProc"}, + {0x2415, "FindDItem"}, + {0x241e, "SetResourceLoad"}, + {0x2422, "TESetRuler"}, + {0x2423, "SetTuningTable"}, + {0x2502, "SetPurgeAll"}, + {0x2503, "GetTick"}, + {0x2504, "GetClip"}, + {0x2505, "GetDeskGlobal"}, + {0x250b, "Hex2Long"}, + {0x250e, "WindNewRes"}, + {0x250f, "GetMItem"}, + {0x2510, "DrawOneCtl"}, + {0x2513, "PrGetZoneName"}, + {0x2515, "UpdateDialog"}, + {0x251e, "SetResourceFileDepth"}, + {0x2522, "TEScroll"}, + {0x2523, "GetTuningTable"}, + {0x2603, "PackBytes"}, + {0x2604, "ClipRect"}, + {0x260b, "Int2Dec"}, + {0x260e, "TrackZoom"}, + {0x260f, "SetMItemFlag"}, + {0x2610, "FindTargetCtl"}, + {0x2615, "GetDItemType"}, + {0x261e, "GetMapHandle"}, + {0x2622, "TEGetInternalProc"}, + {0x2623, "SetTrackOut"}, + {0x2703, "UnPackBytes"}, + {0x2704, "HidePen"}, + {0x270b, "Long2Dec"}, + {0x270e, "ZoomWindow"}, + {0x270f, "GetMItemFlag"}, + {0x2710, "MakeNextCtlTarget"}, + {0x2715, "SetDItemType"}, + {0x271e, "LoadAbsResource"}, + {0x2722, "TEGetLastError"}, + {0x2723, "StartMIDIDriver"}, + {0x2802, "PtrToHand"}, + {0x2803, "Munger"}, + {0x2804, "ShowPen"}, + {0x280b, "Dec2Int"}, + {0x280e, "SetWRefCon"}, + {0x280f, "SetMItemBlink"}, + {0x2810, "MakeThisCtlTarget"}, + {0x2813, "PrGetPrinterDrvName"}, + {0x2815, "GetDItemBox"}, + {0x281e, "ResourceConverter"}, + {0x2822, "TECompactRecord"}, + {0x2823, "StopMIDIDriver"}, + {0x2902, "HandToPtr"}, + {0x2903, "GetIRQEnable"}, + {0x2904, "GetPen"}, + {0x290b, "Dec2Long"}, + {0x290e, "GetWRefCon"}, + {0x290f, "MenuNewRes"}, + {0x2910, "SendEventToCtl"}, + {0x2913, "PrGetPortDvrName"}, + {0x2915, "SetDItemBox"}, + {0x2a02, "HandToHand"}, + {0x2a03, "SetAbsClamp"}, + {0x2a04, "SetPenState"}, + {0x2a0b, "HexIt"}, + {0x2a0e, "GetNextWindow"}, + {0x2a0f, "DrawMenuBar"}, + {0x2a10, "GetCtlID"}, + {0x2a13, "PrGetUserName"}, + {0x2a15, "GetFirstDItem"}, + {0x2a1e, "RMFindNamedResource"}, + {0x2b02, "BlockMove"}, + {0x2b03, "GetAbsClamp"}, + {0x2b04, "GetPenState"}, + {0x2b0e, "GetWKind"}, + {0x2b0f, "MenuSelect"}, + {0x2b10, "SetCtlID"}, + {0x2b13, "PrGetNetworkName"}, + {0x2b15, "GetNextDItem"}, + {0x2b1e, "RMGetResourceName"}, + {0x2c03, "SysBeep"}, + {0x2c04, "SetPenSize"}, + {0x2c0e, "GetWFrame"}, + {0x2c0f, "HiliteMenu"}, + {0x2c10, "CallCtlDefProc"}, + {0x2c15, "ModalDialog2"}, + {0x2c1e, "RMLoadNamedResource"}, + {0x2d04, "GetPenSize"}, + {0x2d0e, "SetWFrame"}, + {0x2d0f, "NewMenu"}, + {0x2d10, "NotifyCtls"}, + {0x2d1e, "RMSetResourceName"}, + {0x2e03, "AddToQueue"}, + {0x2e04, "SetPenMode"}, + {0x2e0e, "GetStructRgn"}, + {0x2e0f, "DisposeMenu"}, + {0x2e10, "GetCtlMoreFlags"}, + {0x2e15, "GetDItemValue"}, + {0x2f02, "RealFreeMem"}, + {0x2f03, "DeleteFromQueue"}, + {0x2f04, "GetPenMode"}, + {0x2f0e, "GetContentRgn"}, + {0x2f0f, "InitPalette"}, + {0x2f10, "SetCtlMoreFlags"}, + {0x2f15, "SetDItemValue"}, + {0x3002, "SetHandleID"}, + {0x3004, "SetPenPat"}, + {0x300e, "GetUpdateRgn"}, + {0x300f, "EnableMItem"}, + {0x3010, "GetCtlHandleFromID"}, + {0x3013, "PrDevIsItSafe"}, + {0x3103, "GetInterruptState"}, + {0x3104, "GetPenPat"}, + {0x310e, "GetDefProc"}, + {0x310f, "DisableMItem"}, + {0x3110, "NewControl2"}, + {0x3113, "GetZoneList"}, + {0x3203, "GetIntStateRecSize"}, + {0x3204, "SetPenMask"}, + {0x320e, "SetDefProc"}, + {0x320f, "CheckMItem"}, + {0x3210, "CMLoadResource"}, + {0x3213, "GetMyZone"}, + {0x3215, "GetNewModalDialog"}, + {0x3303, "ReadMouse2"}, + {0x3304, "GetPenMask"}, + {0x330e, "GetWControls"}, + {0x330f, "SetMItemMark"}, + {0x3310, "CMReleaseResource"}, + {0x3313, "GetPrinterList"}, + {0x3315, "GetNewDItem"}, + {0x3403, "GetCodeResConverter"}, + {0x3404, "SetBackPat"}, + {0x340e, "SetOriginMask"}, + {0x340f, "GetMItemMark"}, + {0x3410, "SetCtlParamPtr"}, + {0x3413, "PMUnloadDriver"}, + {0x3415, "GetAlertStage"}, + {0x3503, "GetROMResource"}, + {0x3504, "GetBackPat"}, + {0x350e, "GetInfoRefCon"}, + {0x350f, "SetMItemStyle"}, + {0x3510, "GetCtlParamPtr"}, + {0x3513, "PMLoadDriver"}, + {0x3515, "ResetAlertStage"}, + {0x3603, "ReleaseROMResource"}, + {0x3604, "PenNormal"}, + {0x360e, "SetInfoRefCon"}, + {0x360f, "GetMItemStyle"}, + {0x3613, "PrGetDocName"}, + {0x3615, "DefaultFilter"}, + {0x3703, "ConvSeconds"}, + {0x3704, "SetSolidPenPat"}, + {0x370e, "GetZoomRect"}, + {0x370f, "SetMenuID"}, + {0x3710, "InvalCtls"}, + {0x3713, "PrSetDocName"}, + {0x3715, "GetDefButton"}, + {0x3803, "SysBeep2"}, + {0x3804, "SetSolidBackPat"}, + {0x380e, "SetZoomRect"}, + {0x380f, "SetMItemID"}, + {0x3810, "CtlReserved"}, + {0x3813, "PrGetPgOrientation"}, + {0x3815, "SetDefButton"}, + {0x3903, "VersionString"}, + {0x3904, "SolidPattern"}, + {0x390e, "RefreshDesktop"}, + {0x390f, "SetMenuBar"}, + {0x3910, "FindRadioButton"}, + {0x3915, "DisableDItem"}, + {0x3a03, "WaitUntil"}, + {0x3a04, "MoveTo"}, + {0x3a0e, "InvalRect"}, + {0x3a0f, "SetMItemName"}, + {0x3a10, "SetLETextByID"}, + {0x3a15, "EnableDItem"}, + {0x3b03, "StringToText"}, + {0x3b04, "Move"}, + {0x3b0e, "InvalRgn"}, + {0x3b0f, "GetPopUpDefProc"}, + {0x3b10, "GetLETextByID"}, + {0x3c03, "ShowBootInfo"}, + {0x3c04, "LineTo"}, + {0x3c0e, "ValidRect"}, + {0x3c0f, "PopUpMenuSelect"}, + {0x3d03, "ScanDevices"}, + {0x3d04, "Line"}, + {0x3d0e, "ValidRgn"}, + {0x3d0f, "DrawPopUp"}, + {0x3e04, "SetPicSave"}, + {0x3e0e, "GetContentOrigin"}, + {0x3e0f, "NewMenu2"}, + {0x3f04, "GetPicSave"}, + {0x3f0e, "SetContentOrigin"}, + {0x3f0f, "InsertMItem2"}, + {0x4004, "SetRgnSave"}, + {0x400e, "GetDataSize"}, + {0x400f, "SetMenuTitle2"}, + {0x4104, "GetRgnSave"}, + {0x410e, "SetDataSize"}, + {0x410f, "SetMItem2"}, + {0x4204, "SetPolySave"}, + {0x420e, "GetMaxGrow"}, + {0x420f, "SetMItemName2"}, + {0x4304, "GetPolySave"}, + {0x430e, "SetMaxGrow"}, + {0x430f, "NewMenuBar2"}, + {0x4404, "SetGrafProcs"}, + {0x440e, "GetScroll"}, + {0x4504, "GetGrafProcs"}, + {0x450e, "SetScroll"}, + {0x450f, "HideMenuBar"}, + {0x4604, "SetUserField"}, + {0x460e, "GetPage"}, + {0x460f, "ShowMenuBar"}, + {0x4704, "GetUserField"}, + {0x470e, "SetPage"}, + {0x470f, "SetMItemIcon"}, + {0x4804, "SetSysField"}, + {0x480e, "GetContentDraw"}, + {0x480f, "GetMItemIcon"}, + {0x4904, "GetSysField"}, + {0x490e, "SetContentDraw"}, + {0x490f, "SetMItemStruct"}, + {0x4a04, "SetRect"}, + {0x4a0e, "GetInfoDraw"}, + {0x4a0f, "GetMItemStruct"}, + {0x4b04, "OffsetRect"}, + {0x4b0e, "SetSysWindow"}, + {0x4b0f, "RemoveMItemStruct"}, + {0x4c04, "InsetRect"}, + {0x4c0e, "GetSysWFlag"}, + {0x4c0f, "GetMItemFlag2"}, + {0x4d04, "SectRect"}, + {0x4d0e, "StartDrawing"}, + {0x4d0f, "SetMItemFlag2"}, + {0x4e04, "UnionRect"}, + {0x4e0e, "SetWindowIcons"}, + {0x4e0f, "GetMItemWidth"}, + {0x4f04, "PtInRect"}, + {0x4f0e, "GetRectInfo"}, + {0x4f0f, "GetMItemBlink"}, + {0x5004, "Pt2Rect"}, + {0x500e, "StartInfoDrawing"}, + {0x500f, "InsertPathMItems"}, + {0x5104, "EqualRect"}, + {0x510e, "EndInfoDrawing"}, + {0x5204, "NotEmptyRect"}, + {0x520e, "GetFirstWindow"}, + {0x5304, "FrameRect"}, + {0x530e, "WindDragRect"}, + {0x5404, "PaintRect"}, + {0x540e, "Private01"}, + {0x5504, "EraseRect"}, + {0x550e, "DrawInfoBar"}, + {0x5604, "InvertRect"}, + {0x560e, "WindowGlobal"}, + {0x5704, "FillRect"}, + {0x570e, "SetContentOrigin2"}, + {0x5804, "FrameOval"}, + {0x580e, "GetWindowMgrGlobals"}, + {0x5904, "PaintOval"}, + {0x590e, "AlertWindow"}, + {0x5a04, "EraseOval"}, + {0x5a0e, "StartFrameDrawing"}, + {0x5b04, "InvertOval"}, + {0x5b0e, "EndFrameDrawing"}, + {0x5c04, "FillOVal"}, + {0x5c0e, "ResizeWindow"}, + {0x5d04, "FrameRRect"}, + {0x5d0e, "TaskMasterContent"}, + {0x5e04, "PaintRRect"}, + {0x5e0e, "TaskMasterKey"}, + {0x5f04, "EraseRRect"}, + {0x5f0e, "TaskMasterDA"}, + {0x6004, "InvertRRect"}, + {0x600e, "CompileText"}, + {0x6104, "FillRRect"}, + {0x610e, "NewWindow2"}, + {0x6204, "FrameArc"}, + {0x620e, "ErrorWindow"}, + {0x6304, "PaintArc"}, + {0x630e, "GetAuxWindInfo"}, + {0x6404, "EraseArc"}, + {0x640e, "DoModalWindow"}, + {0x6504, "InvertArc"}, + {0x650e, "MWGetCtlPart"}, + {0x6604, "FillArc"}, + {0x660e, "MWSetMenuProc"}, + {0x6704, "NewRgn"}, + {0x670e, "MWStdDrawProc"}, + {0x6804, "DisposeRgn"}, + {0x680e, "MWSetUpEditMenu"}, + {0x6904, "CopyRgn"}, + {0x690e, "FindCursorCtl"}, + {0x6a04, "SetEmptyRgn"}, + {0x6a0e, "ResizeInfoBar"}, + {0x6b04, "SetRectRgn"}, + {0x6b0e, "HandleDiskInsert"}, + {0x6c04, "RectRgn"}, + {0x6d04, "OpenRgn"}, + {0x6e04, "CloseRgn"}, + {0x6f04, "OffsetRgn"}, + {0x7004, "InsetRgn"}, + {0x7104, "SectRgn"}, + {0x7204, "UnionRgn"}, + {0x7304, "DiffRgn"}, + {0x7404, "XorRgn"}, + {0x7504, "PtInRgn"}, + {0x7604, "RectInRgn"}, + {0x7704, "EqualRgn"}, + {0x7804, "EmptyRgn"}, + {0x7904, "FrameRgn"}, + {0x7a04, "PaintRgn"}, + {0x7b04, "EraseRgn"}, + {0x7c04, "InvertRgn"}, + {0x7d04, "FillRgn"}, + {0x7e04, "ScrollRect"}, + {0x7f04, "PaintPixels"}, + {0x8004, "AddPt"}, + {0x8104, "SubPt"}, + {0x8204, "SetPt"}, + {0x8304, "EqualPt"}, + {0x8404, "LocalToGlobal"}, + {0x8504, "GlobalToLocal"}, + {0x8604, "Random"}, + {0x8704, "SetRandSeed"}, + {0x8804, "GetPixel"}, + {0x8904, "ScalePt"}, + {0x8a04, "MapPt"}, + {0x8b04, "MapRect"}, + {0x8c04, "MapRgn"}, + {0x8d04, "SetStdProcs"}, + {0x8e04, "SetCursor"}, + {0x8f04, "GetCursorAdr"}, + {0x9004, "HideCursor"}, + {0x9104, "ShowCursor"}, + {0x9204, "ObscureCursor"}, + {0x9304, "SetMouseLoc"}, + {0x9404, "SetFont"}, + {0x9504, "GetFont"}, + {0x9604, "GetFontInfo"}, + {0x9704, "GetFontGlobals"}, + {0x9804, "SetFontFlags"}, + {0x9904, "GetFontFlags"}, + {0x9a04, "SetTextFace"}, + {0x9b04, "GetTextFace"}, + {0x9c04, "SetTextMode"}, + {0x9d04, "GetTextMode"}, + {0x9e04, "SetSpaceExtra"}, + {0x9f04, "GetSpaceExtra"}, + {0xa004, "SetForeColor"}, + {0xa104, "GetForeColor"}, + {0xa204, "SetBackColor"}, + {0xa304, "GetBackColor"}, + {0xa404, "DrawChar"}, + {0xa504, "DrawString"}, + {0xa604, "DrawCString"}, + {0xa704, "DrawText"}, + {0xa804, "CharWidth"}, + {0xa904, "StringWidth"}, + {0xaa04, "CStringWidth"}, + {0xab04, "TextWidth"}, + {0xac04, "CharBounds"}, + {0xad04, "StringBounds"}, + {0xae04, "CStringBounds"}, + {0xaf04, "TextBounds"}, + {0xb004, "SetArcRot"}, + {0xb104, "GetArcRot"}, + {0xb204, "SetSysFont"}, + {0xb304, "GetSysFont"}, + {0xb404, "SetVisRgn"}, + {0xb504, "GetVisRgn"}, + {0xb604, "SetIntUse"}, + {0xb704, "OpenPicture"}, + {0xb712, "OpenPicture"}, + {0xb804, "PicComment"}, + {0xb812, "PicComment"}, + {0xb904, "ClosePicture"}, + {0xba04, "DrawPicture"}, + {0xba12, "DrawPicture"}, + {0xbb04, "KillPicture"}, + {0xbb12, "KillPicture"}, + {0xbc04, "FramePoly"}, + {0xbd04, "PaintPoly"}, + {0xbe04, "ErasePoly"}, + {0xbf04, "InvertPoly"}, + {0xc004, "FillPoly"}, + {0xc104, "OpenPoly"}, + {0xc204, "ClosePoly"}, + {0xc304, "KillPoly"}, + {0xc404, "OffsetPoly"}, + {0xc504, "MapPoly"}, + {0xc604, "SetClipHandle"}, + {0xc704, "GetClipHandle"}, + {0xc804, "SetVisHandle"}, + {0xc904, "GetVisHandle"}, + {0xca04, "InitCursor"}, + {0xcb04, "SetBufDims"}, + {0xcc04, "ForceBufDims"}, + {0xcd04, "SaveBufDims"}, + {0xce04, "RestoreBufDims"}, + {0xcf04, "GetFGSize"}, + {0xd004, "SetFontID"}, + {0xd104, "GetFontID"}, + {0xd204, "SetTextSize"}, + {0xd304, "GetTextSize"}, + {0xd404, "SetCharExtra"}, + {0xd504, "GetCharExtra"}, + {0xd604, "PPToPort"}, + {0xd704, "InflateTextBuffer"}, + {0xd804, "GetRomFont"}, + {0xd904, "GetFontLore"}, + {0xda04, "Get640Color"}, + {0xdb04, "Set640Color"} +}; + +#define numTools (sizeof(tools) / sizeof(tools[0])) + +static const char *toolLookup(uint16_t tool) { + for (int i = 0; i < numTools; i++) { + if (tools[i].id >= tool) { + if (tools[i].id == tool) + return tools[i].name; + break; + } + } + return NULL; +} + +#endif diff --git a/extract/trimmusic.c b/extract/trimmusic.c new file mode 100644 index 0000000..163c860 --- /dev/null +++ b/extract/trimmusic.c @@ -0,0 +1,47 @@ +#include +#include +#include +#include + +static inline uint16_t r16(uint8_t *p) { + uint16_t r = *p++; + r |= *p++ << 8; + return r; +} + +int main(int argc, char **argv) { + if (argc != 4) { + fprintf(stderr, "Usage: %s \n", argv[0]); + fprintf(stderr, " start is in hex\n"); + return -1; + } + + FILE *f = fopen(argv[1], "rb"); + if (!f) { + fprintf(stderr, "Couldn't open %s\n", argv[1]); + return -1; + } + fseek(f, 0, SEEK_END); + int len = ftell(f); + fseek(f, 0, SEEK_SET); + uint8_t *data = malloc(len); + fread(data, 1, len, f); + fclose(f); + + uint32_t start = strtol(argv[2], NULL, 16); + if (start > len - 4) { + fprintf(stderr, "Start address is too large\n"); + return -1; + } + + uint32_t blockLen = r16(data + start + 6); + uint32_t fileLen = 600 + blockLen * 3 + 30; + + f = fopen(argv[3], "wb"); + if (!f) { + fprintf(stderr, "Couldn't create %s\n", argv[3]); + return -1; + } + fwrite(data + start, 1, fileLen, f); + fclose(f); +} diff --git a/extract/trimwb.c b/extract/trimwb.c new file mode 100644 index 0000000..e7eb28e --- /dev/null +++ b/extract/trimwb.c @@ -0,0 +1,47 @@ +#include +#include +#include +#include + +static inline uint16_t r16(uint8_t *p) { + uint16_t r = *p++; + r |= *p++ << 8; + return r; +} + +int main(int argc, char **argv) { + if (argc != 4) { + fprintf(stderr, "Usage: %s \n", argv[0]); + fprintf(stderr, " start is in hex\n"); + return -1; + } + + FILE *f = fopen(argv[1], "rb"); + if (!f) { + fprintf(stderr, "Couldn't open %s\n", argv[1]); + return -1; + } + fseek(f, 0, SEEK_END); + int len = ftell(f); + fseek(f, 0, SEEK_SET); + uint8_t *data = malloc(len); + fread(data, 1, len, f); + fclose(f); + + uint32_t start = strtol(argv[2], NULL, 16); + if (start > len - 4) { + fprintf(stderr, "Start address is too large\n"); + return -1; + } + + uint16_t numInst = r16(data + start) & 0xff; + uint32_t fileLen = numInst * 0x5c + 0x1005e + 32; + + f = fopen(argv[3], "wb"); + if (!f) { + fprintf(stderr, "Couldn't create %s\n", argv[3]); + return -1; + } + fwrite(data + start, 1, fileLen, f); + fclose(f); +} diff --git a/fta.html b/fta.html new file mode 100644 index 0000000..d40666a --- /dev/null +++ b/fta.html @@ -0,0 +1,14 @@ + + + FTA Player + + + + +

FTA Player

+
Currently playing: -none-
+
+

Available songs

+
+ + diff --git a/fta.js b/fta.js new file mode 100644 index 0000000..d607000 --- /dev/null +++ b/fta.js @@ -0,0 +1,529 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (_) try { + if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [0, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var _this = this; +var Song = (function () { + function Song() { + } + return Song; +}()); +var FTA = (function () { + function FTA() { + this.player = null; + } + FTA.prototype.getSongList = function (path) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2, new Promise(function (resolve) { + var req = new XMLHttpRequest(); + req.open('GET', path, true); + req.onload = function () { + resolve(JSON.parse(req.responseText)); + }; + req.send(null); + })]; + }); + }); + }; + FTA.prototype.open = function (name, music, wb, inst, delta) { + return __awaiter(this, void 0, void 0, function () { + var _this = this; + var loaded, song, _a, wavebank, _b, instdef, _c, controls, stop_1, play; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + this.name = name; + loaded = document.getElementById('loaded'); + if (loaded) { + loaded.textContent = 'loading...'; + } + _a = Handle.bind; + return [4, this.load(music)]; + case 1: + song = new (_a.apply(Handle, [void 0, _d.sent()]))(); + _b = Handle.bind; + return [4, this.load(wb)]; + case 2: + wavebank = new (_b.apply(Handle, [void 0, _d.sent()]))(); + _c = Handle.bind; + return [4, this.load(inst)]; + case 3: + instdef = new (_c.apply(Handle, [void 0, _d.sent()]))(); + if (this.player) { + this.player.stop(); + } + this.player = new FTAPlayer(song, wavebank, instdef, delta); + if (loaded) { + loaded.textContent = name; + } + controls = document.getElementById('controls'); + if (controls) { + while (controls.firstChild) { + controls.removeChild(controls.firstChild); + } + stop_1 = document.createElement('button'); + stop_1.textContent = '\u23f9'; + stop_1.addEventListener('click', function () { + if (_this.player) { + _this.player.stop(); + } + }); + controls.appendChild(stop_1); + play = document.createElement('button'); + play.textContent = '\u25b6'; + play.addEventListener('click', function () { + if (_this.player) { + _this.player.play(); + } + }); + controls.appendChild(play); + } + return [2]; + } + }); + }); + }; + FTA.prototype.load = function (file) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2, new Promise(function (resolve) { + var req = new XMLHttpRequest(); + req.open('GET', file, true); + req.responseType = 'arraybuffer'; + req.onload = function () { + if (req.response) { + resolve(new Uint8Array(req.response)); + } + }; + req.send(null); + })]; + }); + }); + }; + return FTA; +}()); +document.addEventListener('DOMContentLoaded', function () { return __awaiter(_this, void 0, void 0, function () { + var _this = this; + var fta, songs, list, _i, songs_1, song, row; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + fta = new FTA(); + return [4, fta.getSongList('ftasongs.json')]; + case 1: + songs = _a.sent(); + list = document.getElementById('songlist'); + if (!list) { + return [2]; + } + for (_i = 0, songs_1 = songs; _i < songs_1.length; _i++) { + song = songs_1[_i]; + row = document.createElement('div'); + row.dataset.name = song.name; + row.dataset.music = song.music; + row.dataset.wb = song.wb; + row.dataset.inst = song.inst; + row.dataset.delta = song.delta.toString(10); + row.appendChild(document.createTextNode(song.name)); + row.addEventListener('click', function (event) { return __awaiter(_this, void 0, void 0, function () { + var target; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + target = event.target; + return [4, fta.open(target.dataset.name, target.dataset.music, target.dataset.wb, target.dataset.inst, parseInt(target.dataset.delta, 10))]; + case 1: + _a.sent(); + return [2]; + } + }); + }); }); + list.appendChild(row); + } + return [2]; + } + }); +}); }); +var Channel = (function () { + function Channel() { + this.ticks = 1; + this.offset = 0; + this.pos = 0; + this.notes = []; + this.osc = 0; + this.pointer = 0; + this.size = 0; + this.volume = 0; + this.panning = 0; + this.control = 0; + this.freq = 0; + } + return Channel; +}()); +var FTAPlayer = (function () { + function FTAPlayer(music, wavebank, inst, delta) { + var _this = this; + this.stereo = true; + this.ticksLeft = 0; + this.channels = []; + this.active = []; + this.es5503 = new ES5503(function (osc) { _this.irq(osc); }); + wavebank.seek(0); + this.es5503.setRam(wavebank.read(wavebank.length)); + this.es5503.setEnabled(0x3e); + for (var i = 0; i < 32; i++) { + this.channels.push(new Channel()); + } + inst.seek(0); + var base = inst.r16(); + while (base != 0xffff) { + var ch = inst.r8(); + this.active.push(ch); + this.channels[ch].osc = ch; + this.channels[ch].pointer = inst.r8(); + this.channels[ch].size = inst.r8(); + this.channels[ch].volume = inst.r8(); + this.channels[ch].panning = inst.r8(); + this.channels[ch].control = inst.r8(); + music.seek(base - delta); + var channelLen = music.r16(); + for (var i = 0; i < channelLen; i++) { + var freq = music.r16(); + var time = music.r8(); + this.channels[ch].notes.push({ freq: freq, time: time }); + } + base = inst.r16(); + } + inst.seek(0x46); + this.es5503.setFrequency(31, inst.r16()); + this.es5503.setControl(31, 8); + } + FTAPlayer.prototype.play = function () { + var _this = this; + try { + this.ctx = new AudioContext(); + } + catch (e) { + alert('No audio support'); + return; + } + this.audioNode = this.ctx.createScriptProcessor(0, 0, 2); + this.audioNode.onaudioprocess = function (evt) { + _this.render(evt); + }; + this.audioNode.connect(this.ctx.destination); + }; + FTAPlayer.prototype.stop = function () { + if (this.audioNode) { + this.audioNode.disconnect(); + } + this.audioNode = undefined; + if (this.ctx) { + this.ctx.close(); + } + this.ctx = undefined; + }; + FTAPlayer.prototype.render = function (evt) { + var sampleRate = evt.outputBuffer.sampleRate; + var leftBuf = evt.outputBuffer.getChannelData(0); + var rightBuf = evt.outputBuffer.getChannelData(1); + for (var i = 0; i < evt.outputBuffer.length; i++) { + this.ticksLeft -= 26320; + if (this.ticksLeft <= 0) { + this.ticksLeft += sampleRate; + this.es5503.tick(); + } + var _a = this.es5503.render(), left = _a[0], right = _a[1]; + if (!this.stereo) { + leftBuf[i] = (left + right) * 0.707; + rightBuf[i] = leftBuf[i]; + } + else { + leftBuf[i] = left; + rightBuf[i] = right; + } + } + }; + FTAPlayer.prototype.irq = function (osc) { + if (osc != 31) { + var ch = this.channels[osc & 0xfc]; + this.noteOff(ch); + this.noteOn(ch); + } + else { + for (var _i = 0, _a = this.active; _i < _a.length; _i++) { + var c = _a[_i]; + var ch = this.channels[c]; + ch.ticks--; + if (ch.ticks == 0) { + this.noteOff(ch); + ch.ticks = ch.notes[ch.pos].time; + ch.freq = ch.notes[ch.pos].freq; + ch.pos++; + if (ch.pos >= ch.notes.length) { + ch.pos = 0; + } + this.noteOn(ch); + ch.osc ^= 2; + } + } + } + }; + FTAPlayer.prototype.noteOn = function (ch) { + this.es5503.setFrequency(ch.osc, ch.freq * 2); + this.es5503.setFrequency(ch.osc + 1, ch.freq * 2); + var vol = ch.volume & 0xf; + this.es5503.setVolume(ch.osc, vol << 3); + this.es5503.setVolume(ch.osc + 1, vol << 3); + this.es5503.setVolume(ch.osc ^ 2, vol << 3); + this.es5503.setVolume((ch.osc ^ 2) + 1, vol << 3); + this.es5503.setPointer(ch.osc, ch.pointer); + this.es5503.setPointer(ch.osc + 1, ch.pointer); + this.es5503.setSize(ch.osc, ch.size); + this.es5503.setSize(ch.osc + 1, ch.size); + var a = 2; + var b = 0x12; + if (ch.panning == 0) { + b = 2; + } + else if (ch.panning == 1) { + a = 0x12; + } + this.es5503.setControl(ch.osc, a | ch.control); + this.es5503.setControl(ch.osc + 1, b | ch.control); + }; + FTAPlayer.prototype.noteOff = function (ch) { + this.es5503.setControl(ch.osc, 7); + this.es5503.setControl(ch.osc + 1, 7); + }; + return FTAPlayer; +}()); +var Mode; +(function (Mode) { + Mode[Mode["freeRun"] = 0] = "freeRun"; + Mode[Mode["oneShot"] = 1] = "oneShot"; + Mode[Mode["sync"] = 2] = "sync"; + Mode[Mode["swap"] = 3] = "swap"; +})(Mode || (Mode = {})); +var Oscillator = (function () { + function Oscillator() { + this.pointer = 0; + this.frequency = 0; + this.size = 0; + this.control = 1; + this.volume = 0; + this.data = 0; + this.resolution = 0; + this.accumulator = 0; + this.ptr = 0; + this.shift = 9; + this.max = 0xff; + } + return Oscillator; +}()); +var ES5503 = (function () { + function ES5503(irq) { + this.waveTable = new Float32Array(0x10000); + this.oscillators = []; + this.enabled = 0; + this.waveSizes = [ + 0x100, 0x200, 0x400, 0x800, 0x1000, 0x2000, 0x4000, 0x8000, + ]; + this.waveMasks = [ + 0x1ff00, 0x1fe00, 0x1fc00, 0x1f800, 0x1f000, 0x1e000, 0x1c000, 0x18000, + ]; + this.irq = irq; + for (var i = 0; i < 32; i++) { + this.oscillators.push(new Oscillator()); + } + } + ES5503.prototype.setEnabled = function (enabled) { + this.enabled = enabled >> 1; + }; + ES5503.prototype.setRam = function (bank) { + for (var i = 0; i < bank.length; i++) { + this.waveTable[i] = (bank[i] - 128) / 128; + } + }; + ES5503.prototype.setFrequency = function (osc, freq) { + this.oscillators[osc].frequency = freq; + }; + ES5503.prototype.setVolume = function (osc, vol) { + this.oscillators[osc].volume = vol / 127; + }; + ES5503.prototype.setPointer = function (osc, ptr) { + this.oscillators[osc].pointer = ptr << 8; + this.recalc(osc); + }; + ES5503.prototype.setSize = function (osc, size) { + this.oscillators[osc].size = (size >> 3) & 7; + this.oscillators[osc].resolution = size & 7; + this.recalc(osc); + }; + ES5503.prototype.setControl = function (osc, ctl) { + var prev = this.oscillators[osc].control & 1; + this.oscillators[osc].control = ctl; + var mode = (ctl >> 1) & 3; + if (!(ctl & 1) && prev) { + if (mode == Mode.sync) { + this.oscillators[osc ^ 1].control &= ~1; + this.oscillators[osc ^ 1].accumulator = 0; + } + this.oscillators[osc].accumulator = 0; + } + }; + ES5503.prototype.stop = function (osc) { + this.oscillators[osc].control &= 0xf7; + this.oscillators[osc].control |= 1; + this.oscillators[osc].accumulator = 0; + }; + ES5503.prototype.go = function (osc) { + this.oscillators[osc].control &= ~1; + }; + ES5503.prototype.tick = function () { + for (var osc = 0; osc <= this.enabled; osc++) { + var cur = this.oscillators[osc]; + if (!(cur.control & 1)) { + var base = cur.accumulator >> cur.shift; + var ofs = (base & cur.max) + cur.ptr; + cur.data = this.waveTable[ofs] * cur.volume; + cur.accumulator += cur.frequency; + if (this.waveTable[ofs] == -1) { + this.halted(osc, true); + } + else if (base >= cur.max) { + this.halted(osc, false); + } + } + } + }; + ES5503.prototype.render = function () { + var left = 0; + var right = 0; + for (var osc = 0; osc <= this.enabled; osc++) { + var cur = this.oscillators[osc]; + if (!(cur.control & 1)) { + if (cur.control & 0x10) { + right += cur.data; + } + else { + left += cur.data; + } + } + } + var spread = (this.enabled - 2) / 4; + return [left / spread, right / spread]; + }; + ES5503.prototype.recalc = function (osc) { + var cur = this.oscillators[osc]; + cur.shift = (cur.resolution + 9) - cur.size; + cur.ptr = cur.pointer & this.waveMasks[cur.size]; + cur.max = this.waveSizes[cur.size] - 1; + }; + ES5503.prototype.halted = function (osc, interrupted) { + var cur = this.oscillators[osc]; + var mode = (cur.control >> 1) & 3; + if (interrupted || mode != Mode.freeRun) { + cur.control |= 1; + } + else { + var base = (cur.accumulator >> cur.shift) - cur.max; + cur.accumulator = Math.max(base, 0) << cur.shift; + } + if (mode == Mode.swap) { + var swap = this.oscillators[osc ^ 1]; + swap.control &= ~1; + swap.accumulator = 0; + } + if (cur.control & 8) { + this.irq(osc); + } + }; + return ES5503; +}()); +var Handle = (function () { + function Handle(data) { + this.pos = 0; + this.data = data; + this.length = data.length; + } + Handle.prototype.eof = function () { + return this.pos >= this.length; + }; + Handle.prototype.r8 = function () { + return this.data[this.pos++]; + }; + Handle.prototype.r16 = function () { + var v = this.data[this.pos++]; + v |= this.data[this.pos++] << 8; + return v; + }; + Handle.prototype.r24 = function () { + var v = this.data[this.pos++]; + v |= this.data[this.pos++] << 8; + v |= this.data[this.pos++] << 16; + return v; + }; + Handle.prototype.r32 = function () { + var v = this.data[this.pos++]; + v |= this.data[this.pos++] << 8; + v |= this.data[this.pos++] << 16; + v |= this.data[this.pos++] << 24; + return v >>> 0; + }; + Handle.prototype.r4 = function () { + var r = ''; + for (var i = 0; i < 4; i++) { + r += String.fromCharCode(this.data[this.pos++]); + } + return r; + }; + Handle.prototype.seek = function (pos) { + this.pos = pos; + }; + Handle.prototype.skip = function (len) { + this.pos += len; + }; + Handle.prototype.tell = function () { + return this.pos; + }; + Handle.prototype.read = function (len) { + var oldpos = this.pos; + this.pos += len; + return this.data.subarray(oldpos, this.pos); + }; + return Handle; +}()); +//# sourceMappingURL=fta.js.map \ No newline at end of file diff --git a/ftasongs.json b/ftasongs.json new file mode 100644 index 0000000..85fd5d9 --- /dev/null +++ b/ftasongs.json @@ -0,0 +1,27 @@ +[ + {"name": "Nucleus - Intro", + "music": "songs/nucleus/intro.song", + "wb": "songs/nucleus/intro.wb", + "inst": "songs/nucleus/intro.inst", + "delta": 34304}, + {"name": "Nucleus - Main 1", + "music": "songs/nucleus/main1.song", + "wb": "songs/nucleus/main.wb", + "inst": "songs/nucleus/main1.inst", + "delta": 34304}, + {"name": "Nucleus - Main 2", + "music": "songs/nucleus/main2.song", + "wb": "songs/nucleus/main.wb", + "inst": "songs/nucleus/main2.inst", + "delta": 34304}, + {"name": "Nucleus - Main 3", + "music": "songs/nucleus/main3.song", + "wb": "songs/nucleus/main.wb", + "inst": "songs/nucleus/main3.inst", + "delta": 34304}, + {"name": "Photonix - About", + "music": "songs/photonix/main.song", + "wb": "songs/photonix/main.wb", + "inst": "songs/photonix/main.inst", + "delta": 20480} +] diff --git a/index.html b/index.html new file mode 100644 index 0000000..1b1ab57 --- /dev/null +++ b/index.html @@ -0,0 +1,45 @@ + + + Javascript Soundsmith Player + + + + +

Javascript Soundsmith Player

+

+ This is a 100% javascript Soundsmith Player. Soundsmith was a music program + released in the late 80s for the Apple IIgs. Many games and demos used + Soundsmith for their music. I've included some examples with the player. +

+

+ Go to Soundsmith Player +

+

+ Earlier FTA software didn't use Soundsmith. I've built a specialized player + specifically for them. +

+

+ Go to FTA Player +

+

+ I have included some quick-and-dirty command-line tools to extract + music from FTA demos and other sources inside the extract/ folder. +

+

+ I have documented how I used those tools to reverse engineer demo + organization and extract music in the docs/ folder. In particular, + the Modulae demo, + the Xmas demo, and the + Nucleus demo. +

+

+ I have also documented how the Apple IIgs Ensoniq DOC works, as well as pseudocode + on how the Soundsmith player works. You can read about it + here. +

+

+ Finally, you can check out this project on + GitHub. +

+ + diff --git a/main.css b/main.css new file mode 100644 index 0000000..41cd25f --- /dev/null +++ b/main.css @@ -0,0 +1,47 @@ +body { + background: #444; + color: #898; + margin: 0; + padding: 0; +} +h2 { + font-family: sans-serif; + font-variant: small-caps; + background: #898; + color: #000; + padding-left: 1em; +} +h3 { + font-family: sans-serif; + padding-top: 1em; + padding-left: 1em; + margin: 0; +} +p { + font-family: sans-serif; + padding-left: 1em; + max-width: 80ch; +} +div { + font-family: sans-serif; + padding-left: 1em; +} +a { + text-decoration: none; + font-weight: bold; + color: #cdc; + cursor: pointer; +} +button { + border: 3px double #898; + background: #222; + color: #898; +} +#songlist div { + padding: 0; + cursor: pointer; +} +#songlist div:hover { + background: #888; + color: #cdc; +} diff --git a/smith.html b/smith.html new file mode 100644 index 0000000..6ff4cd0 --- /dev/null +++ b/smith.html @@ -0,0 +1,15 @@ + + + Soundsmith Player + + + + +

Soundsmith Player

+
Currently playing: -none-
+
Pattern: none
+
+

Available songs

+
+ + diff --git a/smith.js b/smith.js new file mode 100644 index 0000000..f178506 --- /dev/null +++ b/smith.js @@ -0,0 +1,664 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (_) try { + if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [0, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var _this = this; +var Song = (function () { + function Song() { + } + return Song; +}()); +var SoundSmith = (function () { + function SoundSmith() { + this.player = null; + } + SoundSmith.prototype.getSongList = function (path) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2, new Promise(function (resolve) { + var req = new XMLHttpRequest(); + req.open('GET', path, true); + req.onload = function () { + resolve(JSON.parse(req.responseText)); + }; + req.send(null); + })]; + }); + }); + }; + SoundSmith.prototype.open = function (name, music, wb) { + return __awaiter(this, void 0, void 0, function () { + var _this = this; + var loaded, info, song, _a, wavebank, _b, controls, stop_1, play; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + this.name = name; + loaded = document.getElementById('loaded'); + if (loaded) { + loaded.textContent = 'loading...'; + } + info = document.getElementById('info'); + _a = Handle.bind; + return [4, this.load(music)]; + case 1: + song = new (_a.apply(Handle, [void 0, _c.sent()]))(); + _b = Handle.bind; + return [4, this.load(wb)]; + case 2: + wavebank = new (_b.apply(Handle, [void 0, _c.sent()]))(); + if (this.player) { + this.player.stop(); + } + this.player = new Player(song, wavebank, function (cur, max) { + if (info) { + if (max == 0) { + info.textContent = 'none'; + } + else { + info.textContent = cur.toString(10) + ' / ' + max.toString(10); + } + } + }); + if (loaded) { + loaded.textContent = name; + } + controls = document.getElementById('controls'); + if (controls) { + while (controls.firstChild) { + controls.removeChild(controls.firstChild); + } + stop_1 = document.createElement('button'); + stop_1.textContent = '\u23f9'; + stop_1.addEventListener('click', function () { + if (_this.player) { + _this.player.stop(); + } + }); + controls.appendChild(stop_1); + play = document.createElement('button'); + play.textContent = '\u25b6'; + play.addEventListener('click', function () { + if (_this.player) { + _this.player.play(); + } + }); + controls.appendChild(play); + } + return [2]; + } + }); + }); + }; + SoundSmith.prototype.load = function (file) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2, new Promise(function (resolve) { + var req = new XMLHttpRequest(); + req.open('GET', file, true); + req.responseType = 'arraybuffer'; + req.onload = function () { + if (req.response) { + resolve(new Uint8Array(req.response)); + } + }; + req.send(null); + })]; + }); + }); + }; + return SoundSmith; +}()); +document.addEventListener('DOMContentLoaded', function () { return __awaiter(_this, void 0, void 0, function () { + var _this = this; + var ss, songs, list, _i, songs_1, song, row; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + ss = new SoundSmith(); + return [4, ss.getSongList('songs.json')]; + case 1: + songs = _a.sent(); + list = document.getElementById('songlist'); + if (!list) { + return [2]; + } + for (_i = 0, songs_1 = songs; _i < songs_1.length; _i++) { + song = songs_1[_i]; + row = document.createElement('div'); + row.dataset.name = song.name; + row.dataset.music = song.music; + row.dataset.wb = song.wb; + row.appendChild(document.createTextNode(song.name)); + row.addEventListener('click', function (event) { return __awaiter(_this, void 0, void 0, function () { + var target; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + target = event.target; + return [4, ss.open(target.dataset.name, target.dataset.music, target.dataset.wb)]; + case 1: + _a.sent(); + return [2]; + } + }); + }); }); + list.appendChild(row); + } + return [2]; + } + }); +}); }); +var Player = (function () { + function Player(music, wavebank, notice) { + var _this = this; + this.stereo = true; + this.timer = 0; + this.tempo = 0; + this.curRow = 0; + this.curPat = 0; + this.orders = []; + this.volTable = []; + this.rowOffset = 0; + this.numInst = 0; + this.ticksLeft = 0; + this.instruments = []; + this.compactTable = new Uint16Array(16); + this.stereoTable = new Uint16Array(16); + this.curInst = new Uint8Array(16); + this.arpeggio = new Uint8Array(16); + this.tone = new Uint8Array(16); + this.frequencies = [ + 0x0000, 0x0016, 0x0017, 0x0018, 0x001a, 0x001b, 0x001d, 0x001e, + 0x0020, 0x0022, 0x0024, 0x0026, 0x0029, 0x002b, 0x002e, 0x0031, + 0x0033, 0x0036, 0x003a, 0x003d, 0x0041, 0x0045, 0x0049, 0x004d, + 0x0052, 0x0056, 0x005c, 0x0061, 0x0067, 0x006d, 0x0073, 0x007a, + 0x0081, 0x0089, 0x0091, 0x009a, 0x00a3, 0x00ad, 0x00b7, 0x00c2, + 0x00ce, 0x00d9, 0x00e6, 0x00f4, 0x0102, 0x0112, 0x0122, 0x0133, + 0x0146, 0x015a, 0x016f, 0x0184, 0x019b, 0x01b4, 0x01ce, 0x01e9, + 0x0206, 0x0225, 0x0246, 0x0269, 0x028d, 0x02b4, 0x02dd, 0x0309, + 0x0337, 0x0368, 0x039c, 0x03d3, 0x040d, 0x044a, 0x048c, 0x04d1, + 0x051a, 0x0568, 0x05ba, 0x0611, 0x066e, 0x06d0, 0x0737, 0x07a5, + 0x081a, 0x0895, 0x0918, 0x09a2, 0x0a35, 0x0ad0, 0x0b75, 0x0c23, + 0x0cdc, 0x0d9f, 0x0e6f, 0x0f4b, 0x1033, 0x112a, 0x122f, 0x1344, + 0x1469, 0x15a0, 0x16e9, 0x1846, 0x19b7, 0x1b3f, 0x1cde, 0x1e95, + 0x2066, 0x2254, 0x245e, 0x2688, + ]; + this.notice = notice; + this.es5503 = new ES5503(function (osc) { _this.irq(osc); }); + this.loadWavebank(wavebank); + music.seek(6); + var blockLen = music.r16(); + this.tempo = music.r16(); + this.es5503.setFrequency(30, 0xfa); + this.es5503.setVolume(30, 0); + this.es5503.setPointer(30, 0); + this.es5503.setSize(30, 0); + this.es5503.setEnabled(0x3c); + this.es5503.setControl(30, 8); + music.seek(0x2c); + for (var i = 0; i < 15; i++) { + this.volTable.push(music.r16()); + music.skip(0x1c); + } + music.seek(0x1d6); + var songLen = music.r16() & 0xff; + for (var i = 0; i < songLen; i++) { + this.orders.push(music.r8() * 64 * 14); + } + music.seek(0x258); + this.notes = music.read(blockLen); + this.effects1 = music.read(blockLen); + this.effects2 = music.read(blockLen); + for (var i = 0; i < 16; i++) { + this.stereoTable[i] = music.r16(); + } + this.rowOffset = this.orders[this.curPat]; + this.notice(this.curPat + 1, this.orders.length); + } + Player.prototype.play = function () { + var _this = this; + try { + this.ctx = new AudioContext(); + } + catch (e) { + alert('No audio support'); + return; + } + this.audioNode = this.ctx.createScriptProcessor(0, 0, 2); + this.audioNode.onaudioprocess = function (evt) { + _this.render(evt); + }; + this.audioNode.connect(this.ctx.destination); + }; + Player.prototype.stop = function () { + if (this.audioNode) { + this.audioNode.disconnect(); + } + this.audioNode = undefined; + if (this.ctx) { + this.ctx.close(); + } + this.ctx = undefined; + }; + Player.prototype.loadWavebank = function (wavebank) { + wavebank.seek(0); + if (wavebank.r4() == 'GSWV') { + var ofs = wavebank.r16(); + this.numInst = wavebank.r8(); + wavebank.skip(this.numInst * 10); + for (var i = 0; i < this.numInst; i++) { + var instLen = (wavebank.r8() + wavebank.r8()) * 6; + this.instruments.push(wavebank.read(instLen)); + } + wavebank.seek(ofs); + var tbl = new Uint8Array(0x10000); + tbl.set(wavebank.read(wavebank.length - ofs)); + } + else { + wavebank.seek(0); + this.numInst = wavebank.r16() & 0xff; + this.es5503.setRam(wavebank.read(0x10000)); + wavebank.seek(0x10022); + for (var i = 0; i < this.numInst; i++) { + this.instruments.push(wavebank.read(12)); + wavebank.skip(0x50); + } + wavebank.skip(0x3c); + for (var i = 0; i < 16; i++) { + this.compactTable[i] = wavebank.r16(); + } + } + }; + Player.prototype.render = function (evt) { + var sampleRate = evt.outputBuffer.sampleRate; + var leftBuf = evt.outputBuffer.getChannelData(0); + var rightBuf = evt.outputBuffer.getChannelData(1); + for (var i = 0; i < evt.outputBuffer.length; i++) { + this.ticksLeft -= 26320; + if (this.ticksLeft <= 0) { + this.ticksLeft += sampleRate; + this.es5503.tick(); + } + var _a = this.es5503.render(), left = _a[0], right = _a[1]; + if (!this.stereo) { + leftBuf[i] = (left + right) * 0.707; + rightBuf[i] = leftBuf[i]; + } + else { + leftBuf[i] = left; + rightBuf[i] = right; + } + } + }; + Player.prototype.irq = function (osc) { + if (osc != 30) { + this.es5503.go(osc); + return; + } + this.timer++; + if (this.timer == this.tempo) { + this.timer = 0; + for (var oscillator = 0; oscillator < 14; oscillator++) { + var semitone = this.notes[this.rowOffset]; + if (semitone == 0 || (semitone & 0x80)) { + this.rowOffset++; + if (semitone == 0x80) { + this.es5503.setControl(oscillator * 2, 1); + this.es5503.setControl(oscillator * 2 + 1, 1); + } + else if (semitone == 0x81) { + this.curRow = 0x3f; + } + } + else { + var fx = this.effects1[this.rowOffset]; + if (fx & 0xf0) { + this.curInst[oscillator] = (fx >> 4) - 1; + } + var inst = this.curInst[oscillator]; + var volume = this.volTable[inst] >> 1; + fx &= 0xf; + if (fx == 0) { + this.arpeggio[oscillator] = this.effects2[this.rowOffset]; + this.tone[oscillator] = semitone; + } + else { + this.arpeggio[oscillator] = 0; + if (fx == 3) { + volume = this.effects2[this.rowOffset] >> 1; + this.es5503.setVolume(oscillator * 2, volume); + this.es5503.setVolume(oscillator * 2 + 1, volume); + } + else if (fx == 6) { + volume -= this.effects2[this.rowOffset] >> 1; + volume = Math.max(volume, 0); + this.es5503.setVolume(oscillator * 2, volume); + this.es5503.setVolume(oscillator * 2 + 1, volume); + } + else if (fx == 5) { + volume += this.effects2[this.rowOffset] >> 1; + volume = Math.min(volume, 0x7f); + this.es5503.setVolume(oscillator * 2, volume); + this.es5503.setVolume(oscillator * 2 + 1, volume); + } + else if (fx == 0xf) { + this.tempo = this.effects2[this.rowOffset]; + } + } + var addr = oscillator * 2; + this.es5503.stop(addr); + this.es5503.stop(addr + 1); + if (inst < this.numInst) { + var x = 0; + while (this.instruments[inst][x] < semitone) { + x += 6; + } + var oscAptr = this.instruments[inst][x + 1]; + var oscAsiz = this.instruments[inst][x + 2]; + var oscActl = this.instruments[inst][x + 3] & 0xf; + if (this.stereoTable[oscillator]) { + oscActl |= 0x10; + } + while (this.instruments[inst][x] != 0x7f) { + x += 6; + } + x += 6; + while (this.instruments[inst][x] < semitone) { + x += 6; + } + var oscBptr = this.instruments[inst][x + 1]; + var oscBsiz = this.instruments[inst][x + 2]; + var oscBctl = this.instruments[inst][x + 3] & 0xf; + if (this.stereoTable[oscillator]) { + oscBctl |= 0x10; + } + var freq = this.frequencies[semitone] >> + this.compactTable[inst]; + this.es5503.setFrequency(addr, freq); + this.es5503.setFrequency(addr + 1, freq); + this.es5503.setVolume(addr, volume); + this.es5503.setVolume(addr + 1, volume); + this.es5503.setPointer(addr, oscAptr); + this.es5503.setPointer(addr + 1, oscBptr); + this.es5503.setSize(addr, oscAsiz); + this.es5503.setSize(addr + 1, oscBsiz); + this.es5503.setControl(addr, oscActl); + this.es5503.setControl(addr + 1, oscBctl); + } + this.rowOffset++; + } + } + this.curRow++; + if (this.curRow < 0x40) { + return; + } + this.curRow = 0; + this.curPat++; + if (this.curPat < this.orders.length) { + this.notice(this.curPat + 1, this.orders.length); + this.rowOffset = this.orders[this.curPat]; + return; + } + this.notice(0, 0); + this.stop(); + return; + } + else { + for (var oscillator = 0; oscillator < 14; oscillator++) { + var a = this.arpeggio[oscillator]; + if (a) { + switch (this.timer % 6) { + case 1: + case 4: + this.tone[oscillator] += a >> 4; + break; + case 2: + case 5: + this.tone[oscillator] += a & 0xf; + break; + case 0: + case 3: + this.tone[oscillator] -= a >> 4; + this.tone[oscillator] -= a & 0xf; + break; + } + var freq = this.frequencies[this.tone[oscillator]] >> + this.compactTable[oscillator]; + var addr = oscillator * 2; + this.es5503.setFrequency(addr, freq); + this.es5503.setFrequency(addr + 1, freq); + } + } + } + }; + return Player; +}()); +var Mode; +(function (Mode) { + Mode[Mode["freeRun"] = 0] = "freeRun"; + Mode[Mode["oneShot"] = 1] = "oneShot"; + Mode[Mode["sync"] = 2] = "sync"; + Mode[Mode["swap"] = 3] = "swap"; +})(Mode || (Mode = {})); +var Oscillator = (function () { + function Oscillator() { + this.pointer = 0; + this.frequency = 0; + this.size = 0; + this.control = 1; + this.volume = 0; + this.data = 0; + this.resolution = 0; + this.accumulator = 0; + this.ptr = 0; + this.shift = 9; + this.max = 0xff; + } + return Oscillator; +}()); +var ES5503 = (function () { + function ES5503(irq) { + this.waveTable = new Float32Array(0x10000); + this.oscillators = []; + this.enabled = 0; + this.waveSizes = [ + 0x100, 0x200, 0x400, 0x800, 0x1000, 0x2000, 0x4000, 0x8000, + ]; + this.waveMasks = [ + 0x1ff00, 0x1fe00, 0x1fc00, 0x1f800, 0x1f000, 0x1e000, 0x1c000, 0x18000, + ]; + this.irq = irq; + for (var i = 0; i < 32; i++) { + this.oscillators.push(new Oscillator()); + } + } + ES5503.prototype.setEnabled = function (enabled) { + this.enabled = enabled >> 1; + }; + ES5503.prototype.setRam = function (bank) { + for (var i = 0; i < bank.length; i++) { + this.waveTable[i] = (bank[i] - 128) / 128; + } + }; + ES5503.prototype.setFrequency = function (osc, freq) { + this.oscillators[osc].frequency = freq; + }; + ES5503.prototype.setVolume = function (osc, vol) { + this.oscillators[osc].volume = vol / 127; + }; + ES5503.prototype.setPointer = function (osc, ptr) { + this.oscillators[osc].pointer = ptr << 8; + this.recalc(osc); + }; + ES5503.prototype.setSize = function (osc, size) { + this.oscillators[osc].size = (size >> 3) & 7; + this.oscillators[osc].resolution = size & 7; + this.recalc(osc); + }; + ES5503.prototype.setControl = function (osc, ctl) { + var prev = this.oscillators[osc].control & 1; + this.oscillators[osc].control = ctl; + var mode = (ctl >> 1) & 3; + if (!(ctl & 1) && prev) { + if (mode == Mode.sync) { + this.oscillators[osc ^ 1].control &= ~1; + this.oscillators[osc ^ 1].accumulator = 0; + } + this.oscillators[osc].accumulator = 0; + } + }; + ES5503.prototype.stop = function (osc) { + this.oscillators[osc].control &= 0xf7; + this.oscillators[osc].control |= 1; + this.oscillators[osc].accumulator = 0; + }; + ES5503.prototype.go = function (osc) { + this.oscillators[osc].control &= ~1; + }; + ES5503.prototype.tick = function () { + for (var osc = 0; osc <= this.enabled; osc++) { + var cur = this.oscillators[osc]; + if (!(cur.control & 1)) { + var base = cur.accumulator >> cur.shift; + var ofs = (base & cur.max) + cur.ptr; + cur.data = this.waveTable[ofs] * cur.volume; + cur.accumulator += cur.frequency; + if (this.waveTable[ofs] == -1) { + this.halted(osc, true); + } + else if (base >= cur.max) { + this.halted(osc, false); + } + } + } + }; + ES5503.prototype.render = function () { + var left = 0; + var right = 0; + for (var osc = 0; osc <= this.enabled; osc++) { + var cur = this.oscillators[osc]; + if (!(cur.control & 1)) { + if (cur.control & 0x10) { + right += cur.data; + } + else { + left += cur.data; + } + } + } + var spread = (this.enabled - 2) / 4; + return [left / spread, right / spread]; + }; + ES5503.prototype.recalc = function (osc) { + var cur = this.oscillators[osc]; + cur.shift = (cur.resolution + 9) - cur.size; + cur.ptr = cur.pointer & this.waveMasks[cur.size]; + cur.max = this.waveSizes[cur.size] - 1; + }; + ES5503.prototype.halted = function (osc, interrupted) { + var cur = this.oscillators[osc]; + var mode = (cur.control >> 1) & 3; + if (interrupted || mode != Mode.freeRun) { + cur.control |= 1; + } + else { + var base = (cur.accumulator >> cur.shift) - cur.max; + cur.accumulator = Math.max(base, 0) << cur.shift; + } + if (mode == Mode.swap) { + var swap = this.oscillators[osc ^ 1]; + swap.control &= ~1; + swap.accumulator = 0; + } + if (cur.control & 8) { + this.irq(osc); + } + }; + return ES5503; +}()); +var Handle = (function () { + function Handle(data) { + this.pos = 0; + this.data = data; + this.length = data.length; + } + Handle.prototype.eof = function () { + return this.pos >= this.length; + }; + Handle.prototype.r8 = function () { + return this.data[this.pos++]; + }; + Handle.prototype.r16 = function () { + var v = this.data[this.pos++]; + v |= this.data[this.pos++] << 8; + return v; + }; + Handle.prototype.r24 = function () { + var v = this.data[this.pos++]; + v |= this.data[this.pos++] << 8; + v |= this.data[this.pos++] << 16; + return v; + }; + Handle.prototype.r32 = function () { + var v = this.data[this.pos++]; + v |= this.data[this.pos++] << 8; + v |= this.data[this.pos++] << 16; + v |= this.data[this.pos++] << 24; + return v >>> 0; + }; + Handle.prototype.r4 = function () { + var r = ''; + for (var i = 0; i < 4; i++) { + r += String.fromCharCode(this.data[this.pos++]); + } + return r; + }; + Handle.prototype.seek = function (pos) { + this.pos = pos; + }; + Handle.prototype.skip = function (len) { + this.pos += len; + }; + Handle.prototype.tell = function () { + return this.pos; + }; + Handle.prototype.read = function (len) { + var oldpos = this.pos; + this.pos += len; + return this.data.subarray(oldpos, this.pos); + }; + return Handle; +}()); +//# sourceMappingURL=smith.js.map \ No newline at end of file diff --git a/songs.json b/songs.json new file mode 100644 index 0000000..cce2ce7 --- /dev/null +++ b/songs.json @@ -0,0 +1,35 @@ +[ + {"name": "Modulae - Intro", + "music": "songs/modulae/intro.song", + "wb": "songs/modulae/intro.wb"}, + {"name": "Modulae - Demo", + "music": "songs/modulae/demo.song", + "wb": "songs/modulae/demo.wb"}, + {"name": "Xmas Demo - Loading", + "music": "songs/xmas/loading.song", + "wb": "songs/xmas/loading.wb"}, + {"name": "Xmas Demo - Menu", + "music": "songs/xmas/main.song", + "wb": "songs/xmas/main.wb"}, + {"name": "Xmas Demo - Bullwinkle: The Sequel", + "music": "songs/xmas/section1.song", + "wb": "songs/xmas/section1.wb"}, + {"name": "Xmas Demo - The Split Demo", + "music": "songs/xmas/section2.song", + "wb": "songs/xmas/section2.wb"}, + {"name": "Xmas Demo - Starwar Fractured Tale", + "music": "songs/xmas/section3.song", + "wb": "songs/xmas/section2.wb"}, + {"name": "Xmas Demo - Christmas Gifts", + "music": "songs/xmas/section4.song", + "wb": "songs/xmas/section4.wb"}, + {"name": "Xmas Demo - Hidden Track", + "music": "songs/xmas/section8.song", + "wb": "songs/xmas/section8.wb"}, + {"name": "Bulla Demo", + "music": "songs/bulla/music.song", + "wb": "songs/bulla/music.wb"}, + {"name": "Soundsmith Intro", + "music": "songs/ss/intro.song", + "wb": "songs/ss/intro.wb"} +] diff --git a/src/es5503.ts b/src/es5503.ts new file mode 100644 index 0000000..340cd29 --- /dev/null +++ b/src/es5503.ts @@ -0,0 +1,158 @@ +/** Copyright 2017 Sean Kasun */ + +enum Mode { + freeRun = 0, + oneShot = 1, + sync = 2, + swap = 3, +} + +class Oscillator { + public pointer: number = 0; + public frequency: number = 0; + public size: number = 0; + public control: number = 1; + public volume: number = 0; + public data: number = 0; + public resolution: number = 0; + public accumulator: number = 0; + public ptr: number = 0; + public shift: number = 9; + public max: number = 0xff; +} + +class ES5503 { + private waveTable: Float32Array = new Float32Array(0x10000); + private oscillators: Oscillator[] = []; + private irq: (osc: number) => void; + private enabled: number = 0; + private waveSizes: number[] = [ + 0x100, 0x200, 0x400, 0x800, 0x1000, 0x2000, 0x4000, 0x8000, + ]; + private waveMasks: number[] = [ + 0x1ff00, 0x1fe00, 0x1fc00, 0x1f800, 0x1f000, 0x1e000, 0x1c000, 0x18000, + ]; + + constructor(irq: (osc: number) => void) { + this.irq = irq; + for (let i: number = 0; i < 32; i++) { + this.oscillators.push(new Oscillator()); + } + } + + public setEnabled(enabled: number): void { + this.enabled = enabled >> 1; + } + + public setRam(bank: Uint8Array): void { + for (let i: number = 0; i < bank.length; i++) { + this.waveTable[i] = (bank[i] - 128) / 128; + } + } + + public setFrequency(osc: number, freq: number): void { + this.oscillators[osc].frequency = freq; + } + + public setVolume(osc: number, vol: number): void { + this.oscillators[osc].volume = vol / 127; + } + + public setPointer(osc: number, ptr: number): void { + this.oscillators[osc].pointer = ptr << 8; + this.recalc(osc); + } + + public setSize(osc: number, size: number): void { + this.oscillators[osc].size = (size >> 3) & 7; + this.oscillators[osc].resolution = size & 7; + this.recalc(osc); + } + + public setControl(osc: number, ctl: number): void { + const prev: number = this.oscillators[osc].control & 1; + this.oscillators[osc].control = ctl; + const mode: Mode = (ctl >> 1) & 3; + // newly triggered? + if (!(ctl & 1) && prev) { + if (mode == Mode.sync) { // trigger pair? + this.oscillators[osc ^ 1].control &= ~1; + this.oscillators[osc ^ 1].accumulator = 0; + } + this.oscillators[osc].accumulator = 0; + } + } + + // halt oscillator without triggering interrupt + public stop(osc: number): void { + this.oscillators[osc].control &= 0xf7; // clear interrupt bit + this.oscillators[osc].control |= 1; + this.oscillators[osc].accumulator = 0; + } + + // unhalt oscillator without triggering swap + public go(osc: number): void { + this.oscillators[osc].control &= ~1; + } + + public tick(): void { + for (let osc: number = 0; osc <= this.enabled; osc++) { + const cur: Oscillator = this.oscillators[osc]; + if (!(cur.control & 1)) { // running? + const base: number = cur.accumulator >> cur.shift; + const ofs: number = (base & cur.max) + cur.ptr; + cur.data = this.waveTable[ofs] * cur.volume; + + cur.accumulator += cur.frequency; + if (this.waveTable[ofs] == -1) { // same as 0 in the data + this.halted(osc, true); + } else if (base >= cur.max) { + this.halted(osc, false); + } + } + } + } + + public render(): [number, number] { + let left: number = 0; + let right: number = 0; + for (let osc: number = 0; osc <= this.enabled; osc++) { + const cur: Oscillator = this.oscillators[osc]; + if (!(cur.control & 1)) { + if (cur.control & 0x10) { + right += cur.data; + } else { + left += cur.data; + } + } + } + const spread: number = (this.enabled - 2) / 4; + return [left / spread, right / spread]; + } + + private recalc(osc: number): void { + const cur: Oscillator = this.oscillators[osc]; + cur.shift = (cur.resolution + 9) - cur.size; + cur.ptr = cur.pointer & this.waveMasks[cur.size]; + cur.max = this.waveSizes[cur.size] - 1; + } + + private halted(osc: number, interrupted: boolean) { + const cur: Oscillator = this.oscillators[osc]; + const mode: Mode = (cur.control >> 1) & 3; + if (interrupted || mode != Mode.freeRun) { + cur.control |= 1; // halt oscillator + } else { + const base: number = (cur.accumulator >> cur.shift) - cur.max; + cur.accumulator = Math.max(base, 0) << cur.shift; + } + if (mode == Mode.swap) { + const swap: Oscillator = this.oscillators[osc ^ 1]; + swap.control &= ~1; // enable pair + swap.accumulator = 0; + } + if (cur.control & 8) { // should we interrupt? + this.irq(osc); + } + } +} diff --git a/src/fta.ts b/src/fta.ts new file mode 100644 index 0000000..4ce86b8 --- /dev/null +++ b/src/fta.ts @@ -0,0 +1,110 @@ +/** Copyright 2017 Sean Kasun */ + +class Song { + public name: string; + public music: string; + public wb: string; + public inst: string; + public delta: number; +} + +class FTA { + private name: string; + private player: FTAPlayer | null = null; + + public async getSongList(path: string): Promise { + return new Promise((resolve: (songs: Song[]) => void) => { + const req: XMLHttpRequest = new XMLHttpRequest(); + req.open('GET', path, true); + req.onload = () => { + resolve(JSON.parse(req.responseText)); + }; + req.send(null); + }); + } + + public async open(name: string, music: string, wb: string, + inst: string, delta: number): Promise { + this.name = name; + const loaded: HTMLElement | null = document.getElementById('loaded'); + if (loaded) { + loaded.textContent = 'loading...'; + } + const song: Handle = new Handle(await this.load(music)); + const wavebank: Handle = new Handle(await this.load(wb)); + const instdef: Handle = new Handle(await this.load(inst)); + + if (this.player) { + this.player.stop(); + } + this.player = new FTAPlayer(song, wavebank, instdef, delta); + if (loaded) { + loaded.textContent = name; + } + + const controls: HTMLElement | null = document.getElementById('controls'); + if (controls) { + while (controls.firstChild) { + controls.removeChild(controls.firstChild); + } + const stop: HTMLButtonElement = document.createElement('button'); + stop.textContent = '\u23f9'; + stop.addEventListener('click', () => { + if (this.player) { + this.player.stop(); + } + }); + controls.appendChild(stop); + const play: HTMLButtonElement = document.createElement('button'); + play.textContent = '\u25b6'; + play.addEventListener('click', () => { + if (this.player) { + this.player.play(); + } + }); + controls.appendChild(play); + } + } + + private async load(file: string): Promise { + return new Promise((resolve) => { + const req: XMLHttpRequest = new XMLHttpRequest(); + req.open('GET', file, true); + req.responseType = 'arraybuffer'; + + req.onload = () => { + if (req.response) { + resolve(new Uint8Array(req.response)); + } + }; + req.send(null); + }); + } +} + +document.addEventListener('DOMContentLoaded', async () => { + const fta: FTA = new FTA(); + const songs: Song[] = await fta.getSongList('ftasongs.json'); + const list: HTMLElement | null = document.getElementById('songlist'); + if (!list) { + return; + } + for (const song of songs) { + const row: HTMLElement = document.createElement('div'); + row.dataset.name = song.name; + row.dataset.music = song.music; + row.dataset.wb = song.wb; + row.dataset.inst = song.inst; + row.dataset.delta = song.delta.toString(10); + row.appendChild(document.createTextNode(song.name)); + row.addEventListener('click', async (event: MouseEvent) => { + const target = event.target as HTMLElement; + await fta.open(target.dataset.name as string, + target.dataset.music as string, + target.dataset.wb as string, + target.dataset.inst as string, + parseInt(target.dataset.delta as string, 10)); + }); + list.appendChild(row); + } +}); diff --git a/src/ftaplayer.ts b/src/ftaplayer.ts new file mode 100644 index 0000000..c2a1976 --- /dev/null +++ b/src/ftaplayer.ts @@ -0,0 +1,169 @@ +/** Copyright 2017 Sean Kasun */ + +interface Note { + freq: number; + time: number; +} + +class Channel { + public ticks: number = 1; + public offset: number = 0; + public pos: number = 0; + public notes: Note[] = []; + public osc: number = 0; + // instrument + public pointer: number = 0; + public size: number = 0; + public volume: number = 0; + public panning: number = 0; + public control: number = 0; + public freq: number = 0; +} + +class FTAPlayer { + private stereo: boolean = true; + private es5503: ES5503; + private ctx?: AudioContext; + private audioNode?: ScriptProcessorNode; + private ticksLeft: number = 0; + private channels: Channel[] = []; + private active: number[] = []; + + constructor(music: Handle, wavebank: Handle, inst: Handle, delta: number) { + this.es5503 = new ES5503((osc: number): void => { this.irq(osc); }); + + wavebank.seek(0); + this.es5503.setRam(wavebank.read(wavebank.length)); + this.es5503.setEnabled(0x3e); + + for (let i: number = 0; i < 32; i++) { + this.channels.push(new Channel()); + } + + inst.seek(0); + let base: number = inst.r16(); + while (base != 0xffff) { + const ch: number = inst.r8(); + this.active.push(ch); + this.channels[ch].osc = ch; + this.channels[ch].pointer = inst.r8(); + this.channels[ch].size = inst.r8(); + this.channels[ch].volume = inst.r8(); + this.channels[ch].panning = inst.r8(); + this.channels[ch].control = inst.r8(); + music.seek(base - delta); + const channelLen: number = music.r16(); + for (let i: number = 0; i < channelLen; i++) { + const freq: number = music.r16(); + const time: number = music.r8(); + this.channels[ch].notes.push({freq, time}); + } + base = inst.r16(); + } + + inst.seek(0x46); + this.es5503.setFrequency(31, inst.r16()); + this.es5503.setControl(31, 8); // freerun + interrupt - halt + } + + public play(): void { + try { + this.ctx = new AudioContext(); + } catch (e) { + alert('No audio support'); + return; + } + this.audioNode = this.ctx.createScriptProcessor(0, 0, 2); + this.audioNode.onaudioprocess = (evt: AudioProcessingEvent) => { + this.render(evt); + }; + this.audioNode.connect(this.ctx.destination); + } + + public stop(): void { + if (this.audioNode) { + this.audioNode.disconnect(); + } + this.audioNode = undefined; + if (this.ctx) { + this.ctx.close(); + } + this.ctx = undefined; + } + + private render(evt: AudioProcessingEvent): void { + const sampleRate: number = evt.outputBuffer.sampleRate; + const leftBuf: Float32Array = evt.outputBuffer.getChannelData(0); + const rightBuf: Float32Array = evt.outputBuffer.getChannelData(1); + + for (let i: number = 0; i < evt.outputBuffer.length; i++) { + // Oscillators update at 26320 Hz + this.ticksLeft -= 26320; + if (this.ticksLeft <= 0) { + this.ticksLeft += sampleRate; + this.es5503.tick(); + } + + const [left, right] = this.es5503.render(); + if (!this.stereo) { // mix down to mono + leftBuf[i] = (left + right) * 0.707; + rightBuf[i] = leftBuf[i]; + } else { + leftBuf[i] = left; + rightBuf[i] = right; + } + } + } + + private irq(osc: number): void { + if (osc != 31) { // not a timer + const ch: Channel = this.channels[osc & 0xfc]; + this.noteOff(ch); + this.noteOn(ch); + } else { + for (const c of this.active) { + const ch: Channel = this.channels[c]; + ch.ticks--; + if (ch.ticks == 0) { + this.noteOff(ch); + ch.ticks = ch.notes[ch.pos].time; + ch.freq = ch.notes[ch.pos].freq; + ch.pos++; + if (ch.pos >= ch.notes.length) { + ch.pos = 0; + } + this.noteOn(ch); + ch.osc ^= 2; // swap oscillator pairs + } + } + } + } + + private noteOn(ch: Channel): void { + this.es5503.setFrequency(ch.osc, ch.freq * 2); + this.es5503.setFrequency(ch.osc + 1, ch.freq * 2); // pair + const vol: number = ch.volume & 0xf; + this.es5503.setVolume(ch.osc, vol << 3); // scale to ch.master + this.es5503.setVolume(ch.osc + 1, vol << 3); // scale to ch.master + this.es5503.setVolume(ch.osc ^ 2, vol << 3); + this.es5503.setVolume((ch.osc ^ 2) + 1, vol << 3); + this.es5503.setPointer(ch.osc, ch.pointer); + this.es5503.setPointer(ch.osc + 1, ch.pointer); + this.es5503.setSize(ch.osc, ch.size); + this.es5503.setSize(ch.osc + 1, ch.size); + let a: number = 2; // one shot left + let b: number = 0x12; // one shot right + if (ch.panning == 0) { // pan left + b = 2; + } else if (ch.panning == 1) { // pan right + a = 0x12; + } + this.es5503.setControl(ch.osc, a | ch.control); + this.es5503.setControl(ch.osc + 1, b | ch.control); + } + + private noteOff(ch: Channel): void { + this.es5503.setControl(ch.osc, 7); // halt + swap - interrupt + this.es5503.setControl(ch.osc + 1, 7); // pair + } +} diff --git a/src/handle.ts b/src/handle.ts new file mode 100644 index 0000000..355e0fd --- /dev/null +++ b/src/handle.ts @@ -0,0 +1,57 @@ +/** Copyright 2017 Sean Kasun */ + +class Handle { + public length: number; + private data: Uint8Array; + private pos: number = 0; + + constructor(data: Uint8Array) { + this.data = data; + this.length = data.length; + } + public eof(): boolean { + return this.pos >= this.length; + } + public r8(): number { + return this.data[this.pos++]; + } + public r16(): number { + let v: number = this.data[this.pos++]; + v |= this.data[this.pos++] << 8; + return v; + } + public r24(): number { + let v: number = this.data[this.pos++]; + v |= this.data[this.pos++] << 8; + v |= this.data[this.pos++] << 16; + return v; + } + public r32(): number { + let v: number = this.data[this.pos++]; + v |= this.data[this.pos++] << 8; + v |= this.data[this.pos++] << 16; + v |= this.data[this.pos++] << 24; + return v >>> 0; // force 32-bit unsigned + } + public r4(): string { + let r: string = ''; + for (let i: number = 0; i < 4; i++) { + r += String.fromCharCode(this.data[this.pos++]); + } + return r; + } + public seek(pos: number): void { + this.pos = pos; + } + public skip(len: number): void { + this.pos += len; + } + public tell(): number { + return this.pos; + } + public read(len: number): Uint8Array { + const oldpos: number = this.pos; + this.pos += len; + return this.data.subarray(oldpos, this.pos); + } +} diff --git a/src/player.ts b/src/player.ts new file mode 100644 index 0000000..56e2021 --- /dev/null +++ b/src/player.ts @@ -0,0 +1,300 @@ +/** Copyright 2017 Sean Kasun */ + +class Player { + private es5503: ES5503; + private ctx?: AudioContext; + private audioNode?: ScriptProcessorNode; + private stereo: boolean = true; + + private timer: number = 0; + private tempo: number = 0; + private curRow: number = 0; + private curPat: number = 0; + private orders: number[] = []; + private volTable: number[] = []; + private rowOffset: number = 0; + private numInst: number = 0; + private ticksLeft: number = 0; + private notes: Uint8Array; + private effects1: Uint8Array; + private effects2: Uint8Array; + private instruments: Uint8Array[] = []; + private compactTable: Uint16Array = new Uint16Array(16); + private stereoTable: Uint16Array = new Uint16Array(16); + private curInst: Uint8Array = new Uint8Array(16); + private arpeggio: Uint8Array = new Uint8Array(16); + private tone: Uint8Array = new Uint8Array(16); + private notice: (pat: number, max: number) => void; + private frequencies: number[] = [ + 0x0000, 0x0016, 0x0017, 0x0018, 0x001a, 0x001b, 0x001d, 0x001e, + 0x0020, 0x0022, 0x0024, 0x0026, 0x0029, 0x002b, 0x002e, 0x0031, + 0x0033, 0x0036, 0x003a, 0x003d, 0x0041, 0x0045, 0x0049, 0x004d, + 0x0052, 0x0056, 0x005c, 0x0061, 0x0067, 0x006d, 0x0073, 0x007a, + 0x0081, 0x0089, 0x0091, 0x009a, 0x00a3, 0x00ad, 0x00b7, 0x00c2, + 0x00ce, 0x00d9, 0x00e6, 0x00f4, 0x0102, 0x0112, 0x0122, 0x0133, + 0x0146, 0x015a, 0x016f, 0x0184, 0x019b, 0x01b4, 0x01ce, 0x01e9, + 0x0206, 0x0225, 0x0246, 0x0269, 0x028d, 0x02b4, 0x02dd, 0x0309, + 0x0337, 0x0368, 0x039c, 0x03d3, 0x040d, 0x044a, 0x048c, 0x04d1, + 0x051a, 0x0568, 0x05ba, 0x0611, 0x066e, 0x06d0, 0x0737, 0x07a5, + 0x081a, 0x0895, 0x0918, 0x09a2, 0x0a35, 0x0ad0, 0x0b75, 0x0c23, + 0x0cdc, 0x0d9f, 0x0e6f, 0x0f4b, 0x1033, 0x112a, 0x122f, 0x1344, + 0x1469, 0x15a0, 0x16e9, 0x1846, 0x19b7, 0x1b3f, 0x1cde, 0x1e95, + 0x2066, 0x2254, 0x245e, 0x2688, + ]; + + constructor(music: Handle, wavebank: Handle, + notice: (pat: number, max: number) => void) { + this.notice = notice; + this.es5503 = new ES5503((osc: number): void => { this.irq(osc); }); + + this.loadWavebank(wavebank); + + music.seek(6); + const blockLen: number = music.r16(); + this.tempo = music.r16(); + this.es5503.setFrequency(30, 0xfa); + this.es5503.setVolume(30, 0); + this.es5503.setPointer(30, 0); + this.es5503.setSize(30, 0); + this.es5503.setEnabled(0x3c); + this.es5503.setControl(30, 8); // freerun + interrupts - halt + + music.seek(0x2c); + for (let i: number = 0; i < 15; i++) { + this.volTable.push(music.r16()); + music.skip(0x1c); + } + + music.seek(0x1d6); + const songLen: number = music.r16() & 0xff; + for (let i: number = 0; i < songLen; i++) { + this.orders.push(music.r8() * 64 * 14); + } + + music.seek(0x258); + this.notes = music.read(blockLen); + this.effects1 = music.read(blockLen); + this.effects2 = music.read(blockLen); + for (let i: number = 0; i < 16; i++) { + this.stereoTable[i] = music.r16(); + } + + this.rowOffset = this.orders[this.curPat]; + this.notice(this.curPat + 1, this.orders.length); + } + + public play(): void { + try { + this.ctx = new AudioContext(); + } catch (e) { + alert('No audio support'); + return; + } + this.audioNode = this.ctx.createScriptProcessor(0, 0, 2); + this.audioNode.onaudioprocess = (evt: AudioProcessingEvent) => { + this.render(evt); + }; + this.audioNode.connect(this.ctx.destination); + } + + public stop(): void { + if (this.audioNode) { + this.audioNode.disconnect(); + } + this.audioNode = undefined; + if (this.ctx) { + this.ctx.close(); + } + this.ctx = undefined; + } + + private loadWavebank(wavebank: Handle): void { + wavebank.seek(0); + if (wavebank.r4() == 'GSWV') { // gswv wavebank + const ofs: number = wavebank.r16(); + this.numInst = wavebank.r8(); + wavebank.skip(this.numInst * 10); // skip instrument names + for (let i: number = 0; i < this.numInst; i++) { + const instLen: number = (wavebank.r8() + wavebank.r8()) * 6; + this.instruments.push(wavebank.read(instLen)); + } + wavebank.seek(ofs); + const tbl: Uint8Array = new Uint8Array(0x10000); + tbl.set(wavebank.read(wavebank.length - ofs)); + } else { // regular wavebank + wavebank.seek(0); + this.numInst = wavebank.r16() & 0xff; + this.es5503.setRam(wavebank.read(0x10000)); + + wavebank.seek(0x10022); + for (let i: number = 0; i < this.numInst; i++) { + this.instruments.push(wavebank.read(12)); + wavebank.skip(0x50); + } + wavebank.skip(0x3c); + for (let i: number = 0; i < 16; i++) { + this.compactTable[i] = wavebank.r16(); + } + } + } + + private render(evt: AudioProcessingEvent): void { + const sampleRate: number = evt.outputBuffer.sampleRate; + const leftBuf: Float32Array = evt.outputBuffer.getChannelData(0); + const rightBuf: Float32Array = evt.outputBuffer.getChannelData(1); + + for (let i: number = 0; i < evt.outputBuffer.length; i++) { + // Oscillators update at 26320 Hz + this.ticksLeft -= 26320; + if (this.ticksLeft <= 0) { + this.ticksLeft += sampleRate; + this.es5503.tick(); + } + + const [left, right] = this.es5503.render(); + if (!this.stereo) { // mix down to mono + leftBuf[i] = (left + right) * 0.707; + rightBuf[i] = leftBuf[i]; + } else { + leftBuf[i] = left; + rightBuf[i] = right; + } + } + } + + private irq(osc: number): void { + if (osc != 30) { // not a timer + this.es5503.go(osc); + return; + } + + this.timer++; + if (this.timer == this.tempo) { + this.timer = 0; + for (let oscillator: number = 0; oscillator < 14; oscillator++) { + const semitone: number = this.notes[this.rowOffset]; + if (semitone == 0 || (semitone & 0x80)) { + this.rowOffset++; + if (semitone == 0x80) { + this.es5503.setControl(oscillator * 2, 1); // halt + this.es5503.setControl(oscillator * 2 + 1, 1); // halt pair + } else if (semitone == 0x81) { + this.curRow = 0x3f; + } + } else { + let fx: number = this.effects1[this.rowOffset]; + if (fx & 0xf0) { // change instrument? + this.curInst[oscillator] = (fx >> 4) - 1; + } + const inst: number = this.curInst[oscillator]; + let volume: number = this.volTable[inst] >> 1; + fx &= 0xf; + if (fx == 0) { + this.arpeggio[oscillator] = this.effects2[this.rowOffset]; + this.tone[oscillator] = semitone; + } else { + this.arpeggio[oscillator] = 0; + if (fx == 3) { + volume = this.effects2[this.rowOffset] >> 1; + this.es5503.setVolume(oscillator * 2, volume); + this.es5503.setVolume(oscillator * 2 + 1, volume); + } else if (fx == 6) { + volume -= this.effects2[this.rowOffset] >> 1; + volume = Math.max(volume, 0); + this.es5503.setVolume(oscillator * 2, volume); + this.es5503.setVolume(oscillator * 2 + 1, volume); + } else if (fx == 5) { + volume += this.effects2[this.rowOffset] >> 1; + volume = Math.min(volume, 0x7f); + this.es5503.setVolume(oscillator * 2, volume); + this.es5503.setVolume(oscillator * 2 + 1, volume); + } else if (fx == 0xf) { + this.tempo = this.effects2[this.rowOffset]; + } + } + + const addr: number = oscillator * 2; + this.es5503.stop(addr); + this.es5503.stop(addr + 1); + if (inst < this.numInst) { + let x = 0; + while (this.instruments[inst][x] < semitone) { + x += 6; + } + const oscAptr: number = this.instruments[inst][x + 1]; + const oscAsiz: number = this.instruments[inst][x + 2]; + let oscActl: number = this.instruments[inst][x + 3] & 0xf; + if (this.stereoTable[oscillator]) { + oscActl |= 0x10; + } + while (this.instruments[inst][x] != 0x7f) { + x += 6; + } + x += 6; // skip last + while (this.instruments[inst][x] < semitone) { + x += 6; + } + const oscBptr: number = this.instruments[inst][x + 1]; + const oscBsiz: number = this.instruments[inst][x + 2]; + let oscBctl: number = this.instruments[inst][x + 3] & 0xf; + if (this.stereoTable[oscillator]) { + oscBctl |= 0x10; + } + const freq: number = this.frequencies[semitone] >> + this.compactTable[inst]; + this.es5503.setFrequency(addr, freq); + this.es5503.setFrequency(addr + 1, freq); // pair + this.es5503.setVolume(addr, volume); + this.es5503.setVolume(addr + 1, volume); // pair + this.es5503.setPointer(addr, oscAptr); + this.es5503.setPointer(addr + 1, oscBptr); // pair + this.es5503.setSize(addr, oscAsiz); + this.es5503.setSize(addr + 1, oscBsiz); // pair + this.es5503.setControl(addr, oscActl); + this.es5503.setControl(addr + 1, oscBctl); // pair + } + this.rowOffset++; + } + } + this.curRow++; + if (this.curRow < 0x40) { + return; + } + // advance pattern + this.curRow = 0; + this.curPat++; + if (this.curPat < this.orders.length) { + this.notice(this.curPat + 1, this.orders.length); + this.rowOffset = this.orders[this.curPat]; + return; + } + // stopped + this.notice(0, 0); + this.stop(); + return; + } else { // between notes. Apply arpeggio + for (let oscillator: number = 0; oscillator < 14; oscillator++) { + const a: number = this.arpeggio[oscillator]; + if (a) { + switch (this.timer % 6) { + case 1: case 4: + this.tone[oscillator] += a >> 4; + break; + case 2: case 5: + this.tone[oscillator] += a & 0xf; + break; + case 0: case 3: + this.tone[oscillator] -= a >> 4; + this.tone[oscillator] -= a & 0xf; + break; + } + const freq: number = this.frequencies[this.tone[oscillator]] >> + this.compactTable[oscillator]; + const addr: number = oscillator * 2; + this.es5503.setFrequency(addr, freq); + this.es5503.setFrequency(addr + 1, freq); // pair + } + } + } + } +} diff --git a/src/smith.ts b/src/smith.ts new file mode 100644 index 0000000..f057a94 --- /dev/null +++ b/src/smith.ts @@ -0,0 +1,113 @@ +/** Copyright 2017 Sean Kasun */ + +class Song { + public name: string; + public music: string; + public wb: string; +} + +class SoundSmith { + private name: string; + private player: Player | null = null; + + public async getSongList(path: string): Promise { + return new Promise((resolve: (songs: Song[]) => void) => { + const req: XMLHttpRequest = new XMLHttpRequest(); + req.open('GET', path, true); + req.onload = () => { + resolve(JSON.parse(req.responseText)); + }; + req.send(null); + }); + } + + public async open(name: string, music: string, wb: string): Promise { + this.name = name; + const loaded: HTMLElement | null = document.getElementById('loaded'); + if (loaded) { + loaded.textContent = 'loading...'; + } + const info: HTMLElement | null = document.getElementById('info'); + + const song: Handle = new Handle(await this.load(music)); + const wavebank: Handle = new Handle(await this.load(wb)); + + if (this.player) { + this.player.stop(); + } + this.player = new Player(song, wavebank, (cur: number, max: number) => { + if (info) { + if (max == 0) { + info.textContent = 'none'; + } else { + info.textContent = cur.toString(10) + ' / ' + max.toString(10); + } + } + }); + + if (loaded) { + loaded.textContent = name; + } + + const controls: HTMLElement | null = document.getElementById('controls'); + if (controls) { + while (controls.firstChild) { + controls.removeChild(controls.firstChild); + } + const stop: HTMLButtonElement = document.createElement('button'); + stop.textContent = '\u23f9'; + stop.addEventListener('click', () => { + if (this.player) { + this.player.stop(); + } + }); + controls.appendChild(stop); + const play: HTMLButtonElement = document.createElement('button'); + play.textContent = '\u25b6'; + play.addEventListener('click', () => { + if (this.player) { + this.player.play(); + } + }); + controls.appendChild(play); + } + } + + private async load(file: string): Promise { + return new Promise((resolve) => { + const req: XMLHttpRequest = new XMLHttpRequest(); + req.open('GET', file, true); + req.responseType = 'arraybuffer'; + + req.onload = () => { + if (req.response) { + resolve(new Uint8Array(req.response)); + } + }; + req.send(null); + }); + } +} + +document.addEventListener('DOMContentLoaded', async () => { + const ss: SoundSmith = new SoundSmith(); + const songs: Song[] = await ss.getSongList('songs.json'); + const list: HTMLElement | null = document.getElementById('songlist'); + if (!list) { + return; + } + for (const song of songs) { + const row: HTMLElement = document.createElement('div'); + row.dataset.name = song.name; + row.dataset.music = song.music; + row.dataset.wb = song.wb; + row.appendChild(document.createTextNode(song.name)); + row.addEventListener('click', async (event: MouseEvent) => { + const target = event.target as HTMLElement; + await ss.open(target.dataset.name as string, + target.dataset.music as string, + target.dataset.wb as string); + }); + list.appendChild(row); + } +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..01c5f6b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "module": "system", + "noEmitOnError": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "removeComments": true, + "strictNullChecks": true, + "outFile": "smith.js", + "target": "ES5", + "lib": ["dom", "es2015"], + "sourceMap": true + }, + "files": [ + "src/smith.ts", + "src/player.ts", + "src/es5503.ts", + "src/handle.ts" + ] +} diff --git a/tsconfig_fta.json b/tsconfig_fta.json new file mode 100644 index 0000000..15ba326 --- /dev/null +++ b/tsconfig_fta.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "module": "system", + "noEmitOnError": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "removeComments": true, + "strictNullChecks": true, + "outFile": "fta.js", + "target": "ES5", + "lib": ["dom", "es2015"], + "sourceMap": true + }, + "files": [ + "src/fta.ts", + "src/ftaplayer.ts", + "src/es5503.ts", + "src/handle.ts" + ] +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..de5fede --- /dev/null +++ b/tslint.json @@ -0,0 +1,18 @@ +{ + "defaultSeverity": "error", + "extends": [ + "tslint:recommended" + ], + "jsRules": {}, + "rules": { + "max-line-length": [true, 80], + "no-bitwise": false, + "triple-equals": false, + "interface-name": false, + "quotemark": [true, "single", "avoid-template", "avoid-escape"], + "no-console": false, + "max-classes-per-file": false, + "object-literal-sort-keys": false + }, + "rulesDirectory": [] +}