Difference between revisions of "Andlabs/Time Trax"
From Sega Retro
Line 201: | Line 201: | ||
| colspan=2| unknown | | colspan=2| unknown | ||
|- | |- | ||
− | | $9F byte byte || unknown | + | | $9F byte byte |
+ | | colspan=2| unknown | ||
|- | |- | ||
− | | $A0 || unknown | + | | $A0 |
+ | | colspan=2| unknown | ||
|- | |- | ||
− | | $A1 || unknown | + | | $A1 |
+ | | colspan=2| unknown | ||
|- | |- | ||
− | | $A2 byte || unknown | + | | $A2 byte |
+ | | colspan=2| unknown | ||
|- | |- | ||
− | | $A3 || unknown | + | | $A3 |
+ | | colspan=2| unknown | ||
|- | |- | ||
− | | $A4 byte || unknown | + | | $A4 byte |
+ | | colspan=2| unknown | ||
|- | |- | ||
− | | $A5 || unknown | + | | $A5 |
+ | | colspan=2| unknown | ||
|- | |- | ||
| $A6 byte || Set LFO; LFO is global to the entire YM2612. 0 turns off, 1..8 sets LFO frequency to 0..7 (respectively). | | $A6 byte || Set LFO; LFO is global to the entire YM2612. 0 turns off, 1..8 sets LFO frequency to 0..7 (respectively). |
Revision as of 16:08, 27 October 2013
thanks SIk, ValleyBell
Contents
There are only two sound banks.
- $F0000 - PCM samples
- $F8000 - music and SFX
everything fits neatly into its bank
Commands
Commands are two bytes each, the first byte being the argument and the second byte being the command itself. There are $20 bytes granted for commands (so up to $10 commands at once), and another byte in RAM used to determine where the command write pointer is. (Another byte stores where it used to be; this is used to tell if we have new commands.)
Commands, where $xx indicates argument:
- $00 - play sound $xx ($00..$05 music, $06..$nn SFX)
- $01 - ??
- $02 - ??
- $03 - ??
- $04 - ??
- $05 - ??
- $06 - ??
Sound Bank
The beginning of this bank consists of the following:
- $0 byte - ???
- $1 big-endian word - bank pointer to list of songs
- $3 big-endian word - bank pointer to list of FM voices
- ...
The list of songs consists of just a flat list of big-endian bank pointers.
Song Format
The first thing in each song is a list of n channel headers; their form is:
- $00 byte - xxxxxxxxxxx; loaded to byte $01 of the in-RAM channel data structure, if $FF, end the list (and thus stop loading the sound, as there is nothing left to do)
- $01 byte - channel number:
- $00..$05 - FM1..FM6
- $06 - PSG1
- $07 - PSG2
If the value at $00 is xxxxxx than the value already loaded into the channel's $01h, then the following two bytes are merely skipped:
- $02 little endian word - channel data pointer; laoded to byte $07 of the in-RAM channel data structure
Z80 RAM Channel Data Structure
- TODO describe which effects change what
- $00 byte - xxxxx; if top bit is set, channel is not played
- $01 byte - xxxxx; loaded from byte $00 of the sound
- xxxx
- $04 little endian word - current chip frequency number (after processing)
- $06 little endian word - channel data pointer; loaded from byte $03 of the sound
- $08 byte - current note byte
- $09 byte - current amount of ticks remaining in note (so 1 means 1 tick left, then 0 means new note)
- $0A [4]struct { start-pointer little endian word; count byte } (size: 12 bytes) - four saved looop counter objects
- $16 byte - current loop offset
- $17 byte - current call stack pointer
- $18 [4]little endian word (size: 8 bytes) - call stack; grows up
- $20 byte - saved note duration?
- $21 byte - song data panning value?
- $22 byte - cached chip panning value?
- xxxx
- $26 byte - note retrigger flag (bit 7 set = retrigger off)
- $27 byte - conditional flag for some effects; either $00 or $FF
- xxxx
- $31 byte - if nonzero, value of offset $09 to trigger a key off at
- $32 byte - if nonzero, value of (offset $33 - offset $09) to trigger a key off at
- $33 byte - current note duration (loaded with new note and saved)
- xxxx
- $58..$71 - FM voice data (TODO what is this on PSG?)
- $58 byte - algo/FB
- $59 byte - AMS/FMS
- $5A, $5B, $5C, $5D byte - DT/MULT for op 1, 2, 3, 4 respectively
- $5E, $5F, $60, $61 byte - TL for op 1, 2, 3, 4 respectively
- $62, $63, $64, $65 byte - AR/RS for op 1, 2, 3, 4 respectively
- $66, $67, $68, $69 byte - DR/AM for op 1, 2, 3, 4 respectively
- $6A, $6B, $6C, $6D byte - SR for op 1, 2, 3, 4 respectively
- $6E, $6F, $70, $71 byte - RR/SL for op 1, 2, 3, 4 respectively
- xxxx
- $74 byte - flag to reload panning?
- $75 byte - flag to reload FM voice (TODO PSG?)
- $76 byte - signed value to add to channel frequency value when it is accessed to get final channel frequency value
- TODO call this "fine frequency adjustment"?
- xxxx
- $79 byte - xxxxx; if top bit is set, channel is not played
Note to Frequency Pipeline
- xxxxx
- Get the frequency number for the new note value
- Add the 16-bit sign extended value from byte offset $76 to the base frequency number to get the final frequency number and store it in offset $04
Channel Binary Format
A byte is read from the current pointer
- If it is >= $80, then it is a command.
- If it is zero, then this is a rest.
- If $20 is zero, then the next byte is read to indicate new note duration (?).
- Otherwise, it is a note.
- If $20 is zero, then the next byte is read to indicate new note duration (?).
Effects
Byte | FM Channels | PSG Channels |
---|---|---|
$80 | stop channel | |
$81 little-endian-word | jump to address | |
$82 byte | start a loop that iterates x times; up to 4 loops can be nested | |
$83 | end loop | |
$84 big-endian-word | call subroutine at given address; up to 4 subroutine calls can be nested | |
$85 | return from subroutine | |
$86 byte | save note duration (byte $20 of RAM data structure) | |
$87 byte | set panning (TODO values) | unknown, but has to do with setting noise modes |
$88 byte | load FM voice n | invalid; see below |
$88 | invalid; see above | unknown, but has to do with setting noise modes |
$89 byte | unknown | |
$8A byte | unknown | |
$8B | turn on note retrigger | |
$8C | turn off note retrigger | |
$8D byte | sets given channel's (minus 1) conditional flag $27 to $FF | |
$8E | if current channel's $27 is $00, set $09 to 1 and don't advance song data pointer past this effect; otherwise set it to $00 and continue | |
$8F big-endian-word | if current channel's $27 is $00, jump to this word; otherwise set it to $00 and see #effect-8F-bug | |
$90 | same as $83, except first if current channel's $27 is $FF; set it to $00 and end the loop early | |
$91 byte big-endian-word-list | do something, then jump to one of the big-endian words; the byte is the number of such words there are | |
$92 byte byte | unknown | |
$93 | unknown | |
$94 byte byte | unknown | |
$95 | unknown | |
$96 byte | schedule a key off to happen when n ticks remain in a note; overrides effect $98 | invalid; undefined behavior |
$97 | removes the scheduled key off set by $96 | invalid; undefined behavior |
$98 byte | schedule key off to happen after n ticks from the start of a note; overrides effect $96 | invalid; undefined behavior |
$99 | removes the scheduled key off set by $98 | invalid; undefined behavior |
$9A byte byte byte byte byte byte | unknown, then does $9B | |
$9B | unknown | |
$9C | unknown | |
$9D | unknown | |
$9E | unknown | |
$9F byte byte | unknown | |
$A0 | unknown | |
$A1 | unknown | |
$A2 byte | unknown | |
$A3 | unknown | |
$A4 byte | unknown | |
$A5 | unknown | |
$A6 byte | Set LFO; LFO is global to the entire YM2612. 0 turns off, 1..8 sets LFO frequency to 0..7 (respectively). | |
$A7 byte | play PCM sample n | |
$A8 byte | set signed value to add to chip frequency number (offset $76) | |
$A9 | clear signed value to add to chip frequency number (offset $76), meaning nothing gets added | |
$AA byte | unknown | |
$AB | unknown | |
(else) | undefined behavior |
effect-8F-bug
Effect $8F is intended to be a conditional jump: if $27 is $00, jump to the given big-endian word, otherwise set it to $00, skip it and continue processing. Except there's a bug in that last step:
ROM:1010 inc de ; skip it ROM:1011 inc de ROM:1012 ld (ix+6), e ROM:1015 ld (ix+7), e ; BUG ROM:1018 ret
The line marked BUG should be
ROM:1015 ld (ix+7), d
The effect thanks to the bug is it sets both bytes to the low byte of the next data byte, so if the condition is false, instead of going to (for example) $ABCD, it will go to $CDCD instead.
PCM Sample Bank
The top of this bank contains pointer-length pairs for PCM samples. Pointers are relative to the Z80 memory map (so they are bank pointers). Pointers and lengths are big endian (this is NOT how things usually are done!)
Or in other words
- $0 word - first sample pointer BIG ENDIAN
- $2 word - first sample length BIG ENDIAN
- $4 word - second sample pointer BIG ENDIAN
- $6 word - second sample length BIG ENDIAN
- $8 word - third sample pointer BIG ENDIAN
- $A word - third sample length BIG ENDIAN
and so on until the first PCM data byte
PCM Sample Playback
PCM samples are played back through a buffer: the game reads $80 bytes of sample data, then plays back one byte of ample data every so often. Buffer filling is done all at once and in groups of 8 bytes, with another PCM data write after each group of 8 bytes.
Though the sample buffer is $80 bytes long, the game has two consecutive buffers, switching after one has been played through. Why he doesn't just use one $100-byte-long buffer is beyond me; maybe he wanted it to load to the second buffer while playing back from the first?
pcm-bug
There appears to be a bug in the buffer loading code:
ROM:0B18 ld hl, (PCMSampleLength) ROM:0B1B ld bc, 80h ; 'Ç' ROM:0B1E sbc hl, bc ROM:0B20 jp m, loc_C67 ; stops sample playback ROM:0B23 ld (PCMSampleLength), hl
if I am reading this correctly, the game will stop playing samples if it cannot fill a buffer completely, leaving the tail end of samples unplayed:
00f0000: 8024 166e 9692 048d 9b1f 0a70 a58f 0869 .$.n.......p...i 00f0010: adf8 156f c367 0975 ccdc 0579 d255 2080 ...o.g.u...y.U . 00f0020: f2d5 093b ____ ____ ____ ____ ____ ____ ...;____________
notice how none of those lengths (except one) are aligned