From 4f1a6bb8c45bd69ab3ad1911408e8c2961299d5f Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 21 Aug 2017 11:12:33 -0700 Subject: [PATCH] initial import --- .gitignore | 3 + LICENSE | 24 + Makefile | 19 + README.md | 18 + docs/modulae.html | 857 +++++++++++++++++++++++ docs/modulae.txt | 122 ++++ docs/nucleus.html | 838 ++++++++++++++++++++++ docs/nucleus.txt | 95 +++ docs/player.html | 1606 +++++++++++++++++++++++++++++++++++++++++++ docs/player.txt | 482 +++++++++++++ docs/xmas.html | 976 ++++++++++++++++++++++++++ docs/xmas.txt | 209 ++++++ extract/.gitignore | 7 + extract/2mg.c | 230 +++++++ extract/65816.h | 331 +++++++++ extract/Makefile | 28 + extract/README.md | 47 ++ extract/addresses.h | 410 +++++++++++ extract/decrunch.c | 196 ++++++ extract/disasm.c | 340 +++++++++ extract/dumptbl.c | 55 ++ extract/prodos16.h | 142 ++++ extract/prodos8.h | 54 ++ extract/smartport.h | 45 ++ extract/tools.h | 1288 ++++++++++++++++++++++++++++++++++ extract/trimmusic.c | 47 ++ extract/trimwb.c | 47 ++ fta.html | 14 + fta.js | 529 ++++++++++++++ ftasongs.json | 27 + index.html | 45 ++ main.css | 47 ++ smith.html | 15 + smith.js | 664 ++++++++++++++++++ songs.json | 35 + src/es5503.ts | 158 +++++ src/fta.ts | 110 +++ src/ftaplayer.ts | 169 +++++ src/handle.ts | 57 ++ src/player.ts | 300 ++++++++ src/smith.ts | 113 +++ tsconfig.json | 24 + tsconfig_fta.json | 24 + tslint.json | 18 + 44 files changed, 10865 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 docs/modulae.html create mode 100644 docs/modulae.txt create mode 100644 docs/nucleus.html create mode 100644 docs/nucleus.txt create mode 100644 docs/player.html create mode 100644 docs/player.txt create mode 100644 docs/xmas.html create mode 100644 docs/xmas.txt create mode 100644 extract/.gitignore create mode 100644 extract/2mg.c create mode 100644 extract/65816.h create mode 100644 extract/Makefile create mode 100644 extract/README.md create mode 100644 extract/addresses.h create mode 100644 extract/decrunch.c create mode 100644 extract/disasm.c create mode 100644 extract/dumptbl.c create mode 100644 extract/prodos16.h create mode 100644 extract/prodos8.h create mode 100644 extract/smartport.h create mode 100644 extract/tools.h create mode 100644 extract/trimmusic.c create mode 100644 extract/trimwb.c create mode 100644 fta.html create mode 100644 fta.js create mode 100644 ftasongs.json create mode 100644 index.html create mode 100644 main.css create mode 100644 smith.html create mode 100644 smith.js create mode 100644 songs.json create mode 100644 src/es5503.ts create mode 100644 src/fta.ts create mode 100644 src/ftaplayer.ts create mode 100644 src/handle.ts create mode 100644 src/player.ts create mode 100644 src/smith.ts create mode 100644 tsconfig.json create mode 100644 tsconfig_fta.json create mode 100644 tslint.json 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": [] +}