Andlabs/Time Trax

From Sega Retro

thanks SIk, ValleyBell

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 (a list of big-endian pointers to voices stored as in RAM below; first pointer is invalid)
  • ...

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 big 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
TODO initial values

Total size: $7A bytes

  • $00 byte - xxxxx; if top bit is set, channel is not played
  • $01 byte - xxxxx; loaded from byte $00 of the sound
  • $02 little endian word - current final chip frequency number (after pitch slide processing)
  • $04 little endian word - current chip frequency number for note: after note processing, but before pitch slide 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?
  • $23 byte - signed offset for note values from effect $89
  • $24 byte - current channel volume from effect $8A
  • $25 byte - stores the current FM voice's algorithm; this value is never actually used by the driver, only ever written to
  • $26 byte - note retrigger flag (bit 7 set = retrigger off)
  • $27 byte - conditional flag for some effects; either $00 or $FF
  • $28 little endian word - current pitch slide chip frequency; set to the frequency of the current note at the time of turning on pitch slide and then adjusted each pitch slide step until it reaches the frequency of the next note (this is used to determine slide direction); this process repeats for every note, like the glide mode on analog and modular synths
  • $2A byte - unsigned value to add or subtract from chip frequency number per pitch slide step; if 0, no pitch slide occurs
  • $2B byte - number of ticks per pitch slide step
  • $2C byte - countdown to pitch slide step: as soon as this reaches zero, a pitch slide step occurs
  • $2D byte - current note slide note number; note slide works just like pitch slide
  • $2E byte - unsigned value to add or subtract from note slide note number ($2D) per note slide step; if 0, no note slide occurs
  • $2F byte - number of ticks per note slide step
  • $30 byte - countdown to note slide step; as soon as this reaches zero, a note slide step occurs
  • $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
  • $42 byte - flag for adding signed note offset in $23 ($00 - add, $FF - don't add)
  • 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
  • $72 byte - flag to load new chip frequency number (TODO loaded from offset $2)
  • $73 byte - flag to indicate a volume change is needed
  • $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"?
  • $77 byte - bitfield that states which FM operators are outputs/slots (bit 3 = operator 1, bit 2 = operator 2, bit 1 = operator 3, bit 0 = operator 4) for volume loading
  • $78 byte - identical to $77; both are used by the volume loading code (this one in the first half when applying the global volume to the voice TL; $77 in the second half when applying the channel volume) and are never differentiated otherwise, so this clone may be residue of something that was removed?
  • $79 byte - xxxxx; if top bit is set, channel is not played

Note to Frequency Pipeline

  1. If we should add the note number frequency offset ($42 is $00; effect $9E), add it ($24)
  2. Handle note slides:
    1. If note sliding is off, skip this step; the resultant note value is just the new note value.
    2. Decrement the note slide counter. If it is not zero, STOP THE ENTIRE PIPELINE.
    3. Load the note slide counter with the ticks per step value to start the counter again.
    4. Take the currently playing note slide value and add or subtract the change amount.
    5. If the new note slide value goes past the new note value, set it to the new note value.
    6. The resultant note value is the new note slide value.
  3. Get the frequency number for the resultant note value
  4. 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 big-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 sets a signed offset for note values at the beginning of the note to frequency number conversion; use effects $9E and $9D to turn this on/off
$8A byte set volume TODO write out volume format
$8B turn on note retrigger
$8C turn off note retrigger
$8D byte Sets given channel's (minus 1) conditional flag byte $27 to $FF
$8E Wait for the current channel's conditional flag byte to be set, then clears it. (This can be used to have one channel do nothing while waiting for another channel to finish, so they both sync up.)
$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 Turn on pitch slides. The first argument is the unsigned value to add or subtract on each step; the second is the number of ticks per step. Pitch slides in the driver work by smoothly sliding from the previously playing note to the next note each time a new note is played as fast as and as much as specified here (similar to how pitch slides between notes work on some synthesizers when in "glide mode"). Consequently, a note must be playing (or slides must already be on) when issuing this effect.
$93 Turn off pitch slides.
$94 byte byte Turn on note slides. Note slides work just like pitch slides and the command format is the same. The note slide starts from the currently playing note, taking note offset into consideration (if applicable).
$95 Turn off note slides.
$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 turn off note number offset (the effect of effect $89)
$9E turn on note number offset (the effect of effect $89)
$9F byte byte unknown; used four bars before fading out the bass in the stage 2 theme's bridge and again at the end — possibly a volume slide
$A0 unknown
$A1 unknown; used to fade out the bass in the stage 2 theme's bridge
$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). invalid; undefined behavior
$A7 byte play PCM sample n invalid; undefined behavior
$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) invalid; 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