Saturday, 26 December 2020

2nd SID chip auto detect

I've started working on something that can use a 2nd SID chip if it exists.  While I was going to put a manual setup in I thought I'd have a go at detecting it automatically as well.   I haven't looked at other people's implementations as I thought it'd be better to try and understand the problem from scratch.
 
So far this has only been tested on an emulator, but I think the premise at least is sound if the memory mapping holds up.

SID chips in memory:

SID chips, at least as far as I'm aware, can only be in the $d000-$dfff block of memory, and within that can only occupy two areas:

  • $d400-$d7ff
  • $de00-$dfff
This is because the other VIC-II registers, timers and colour ram also occupy that same 4kb of memory in various places.

The first SID is always at $d400, with the second SID able to occupy any 32-byte block in those two memory spaces. 

Unoccupied blocks behave differently depending on which of the two areas you are trying to detect.   In the first area ($d400-$d7ff) empty blocks are mirrors of the first chip.  So, for example, writing to $d500 acts the same as writing to $d400.

In the second area they don't appear to be mirrored at all, though empty blocks seem to get some crosstalk. (at least relying on the emulator output to be accurate)  So, for example, writing to $de00 won't affect the first chip at $d400.

This means we need two similar but seperate routines to look for a second SID across the available areas.

Detection:

To detect the chip I used the 'random number generator' read register on the SID.  ($1b)  This gives you the output value of the 3rd channel's oscillator.  The oscillator can be read just by setting it's pitch and waveform registers, it doesn't need any ADSR setup or output volume so can work silently.   By setting oscillator 3 to the noise waveform and putting the frequency at max, we have a constantly changing value we can compare against to figure out where the other SID could be.

As mentioned because of the different memory banking we need two detection routines:

1) $d400-$d7ff area:

  1. Set the first SID's pitch and waveform register to $00.
  2. Read the first SID's RNG (random number generator) and store that as the 'last played' value.
  3. Set the second SID's pitch to max ($ff) and waveform to noise ($81)
  4. Read the first SID's RNG value and compare to last played value.
  5. If the RNG value doesn't match the last played value we haven't found the 2nd SID.  This is because $d41b is being overwritten by an unmapped area.
  6. If the RNG value is the same as the last played value it means the memory isn't being mirrored and there is probably another SID there.

So that's the first memory block taken care of.  Of course there is a possibilty the RNG value will match the last played value but the chances are fairly low.

Now, if the second block doesn't have mirroring, surely we can just check the RNG register on the second chip and see if there's any activity?   Well, yes and no.    This is what we're going to use, but because of the apparant crosstalk we'll need to sample a group of values and try and detect it from an average instead.

The second detection routine does exactly the same as the first, but unmapped areas don't give a constant 0 output when reading their RNG register.   If there is a SID there you'll get the stream of random values, but otherwise you'll get zeroes interspersed with $ff and sometimes other values.   The one consistent thing is that there are many more zeroes on unmapped areas than the other values.   I decided to read 16 values from the RNG (one per frame) then use that this sample to decide if a SID exists.   If there are more than an arbitary amount of zeroes in the sample then most likely there isn't a SID there.

2) $de00-$dfff area:

  1. Set the first SID's waveform and pitch to $00.  We probably don't need to use this at all but might as well.
  2. Set the second SID's pitch to max ($ff) and waveform to noise ($81)
  3. Read 16 values from the second SID's RNG register, one per frame.
  4. Scan through the sampled values for any instances of zero, and add them to a tally.
  5. If the tally is under a certain threshold (I used 3 or less) we probably have a SID there.
  6. If the tally is over that threshold there's a lot more zeros in the crosstalk and we don't have a SID there.  

I'm sure there are plenty of flaws in the above, but it'll be interesting to see what does and doesn't work on real machines when I have time.   This was tested with VICE 3.1

Source:

setup_siddetect

    ; We're using indirect addressing to read the second SID.
    ; Set the read address to the first possible one. ($d420)
    
    lda #$20
    sta $02
    lda #$d4
    sta $03
    
    ; Detection loop for $d400-$d7ff area.
    
setup_siddetectloop
    ; Wait for a new frame before looking for the SID just for safety.
    lda $d012
    cmp #$fe
    bne setup_siddetectloop

    ; These subroutines clear the first and second SID registers.
    jsr sid_sidchip_clear   
    jsr setup_sid2ndclear

    ; Set first SID high pitch/waveform on channel 3 to zero.
    lda #$00
    sta $d401+$0e
    lda #$00
    sta $d404+$0e

    ; Read first SID's RNG register and store it.
    lda $d41b
    sta setup_prevvalue

    ; Set second SID high pitch/waveform on channel 3 to a noise waveform at max pitch.
    ldy #$0f
    lda #$ff
    sta ($02),y
    ldy #$12
    lda #$81
    sta ($02),y
    
    ; Compare the first SID's RNG register against the previously recorded value.
    ; If it's the same the second SID must be mapped in this area, otherwise we'd be
    ; getting the second SID's random output mirrored in.
    
    lda $d41b
    cmp setup_prevvalue
    bne setup_siddetect_notfoundyet
    jmp setup_siddetect_found

    ; Move the second SID address up to the next 32-byte block.
    ; If we've reached $d800 (where the colour RAM is) we need to skip to the second
    ; detection routine instead.
    
setup_siddetect_notfoundyet   
    clc
    lda $02
    adc #$20
    sta $02
    cmp #$00
    bne setup_siddetectloop
    inc $03
    lda $03
    cmp #$d8
    beq setup_siddetectskip
    jmp setup_siddetectloop
    
setup_siddetectskip

    ; Start of second detection routine, set it to start reading from $de00.
    lda #$de
    sta $03
    lda #$00
    sta $02
    
setup_sidlastloop
    ; This is pretty much the same as the first detection routine.....
    lda $d012
    cmp #$fe
    bne setup_sidlastloop
    
    jsr sid_sidchip_clear
    jsr setup_sid2ndclear

    lda #$00
    sta $d401+$0e
    lda #$00
    sta $d404+$0e
    ldy #$0f
    lda #$ff
    sta ($02),y
    ldy #$12
    lda #$81
    sta ($02),y

    ; ...until here.  This time we take a sample of 16-bytes from the 2nd SID's
    ; RNG register.  We take one per frame to try and avoid duplicates.
    
    ldy #$1b
    ldx #$0f
setup_siddetect_sampleinput
    lda $d012
    cmp #$fe
    bne setup_siddetect_sampleinput
    lda ($02),y
    sta setup_samplecache,x
    dex
    bpl setup_siddetect_sampleinput

    ; Now we scan through the sample looking for zeros, if we find any they're
    ; added to a tally.
    
    ldx #$00
    stx setup_prevvalue
setup_siddetect_sampleanalyze
    lda setup_samplecache,x
    cmp #$00
    bne setup_siddetect_samplenozero
    inc setup_prevvalue
setup_siddetect_samplenozero   
    inx
    cpx #$10
    bne setup_siddetect_sampleanalyze

    ; If the tally is 3 or less we've probably found a SID chip.  With the RNG
    ; register going it's unlikely it'd find any plus it's default is 0 anyway.
    
    lda setup_prevvalue
    cmp #$03
    bcc setup_siddetect_found

    ; If we haven't found a SID continue moving through the memory blocks.
    ; If we reach $e000 we're at the end of the available memory
    ; space, so set the address to $ffff which we can use as a check for
    ; no second SID existing.
    
    clc
    lda $02
    adc #$20
    sta $02
    cmp #$00
    bne setup_sidlastloop
    inc $03
    lda $03
    cmp #$e0
    bne setup_sidlastloop
    
setup_siddetect_notfound
    lda #$ff
    sta $02
    sta $03

    ; When the SID address is found write it on screen as PETSCII values.
    
setup_siddetect_found

    lda $02
    sta $0400
    lda $03
    sta $0401
    rts

    ; Clear first SID's registers.

sid_sidchip_clear
    ldx #$18
    lda #$00
sid_sidchip_clearloop
    sta $d400,x
    dex
    bpl sid_sidchip_clearloop
    rts

    ; Clear potential second SID's registers.
    
setup_sid2ndclear

    ldy #$00
    tya
setup_sid2ndclearloop   
    sta ($02),y
    iny
    cpy #$1d
    bne setup_sid2ndclearloop
    rts

    ; Variables:
    ; setup_prevvalue is used to store $d41b value in first detect and as the tally in second detect.
    
setup_prevvalue

    .byte $00
    
    ; Array for second detect sample.
    
setup_samplecache
    .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
   



Saturday, 7 March 2020

c64 303

A few days back I uploaded a Twitter video showing a little 303-ish sound simulator on the Commodore 64.

This came about because someone mentioned 3rd of March as 303 day, and that got me remembering I had a simple player from a few years back that never had quite enough 'bite' to the sound.  So I dug that out, played around with the source a bit and added some quick visuals, making sure it was at least 303 bytes or less as a tagline. (it's 301 btw)

I've put the source to the version from the video at the end of this post.  As you can guess this is code I wrote without doing much optimization as I only imagined it would be used in that video. :)  But anyway, enjoy.  Maybe down the line I'll do a much leaner version when I need to use it for something, or post the older version which at least autoruns.

For anyone interested the video was recorded using the VICE emulator, with the 8580 SID and the reSID filter bias set to -500.


Background on the SID chip:

So how do we get a 303-like sound on the Commodore 64?  Well, we have an advantage with this machine because of the unique SID chip, certainly very different to the AY, SN76489 and beepers of other hardware of the time.

It has 3 versatile oscillators (channels) that can produce triangle, saw, square and noise waveforms, plus some combinations of both.  The oscillators can also have sync and ring modulation applied (the latter only on triangle waveform), with the previous channel in the set providing the pitch modulator input value. (this is in a round-robin fashion so Channel 1 gets affected by Channel 3, Channel 2 gets affected by Channel 1 and finally Channel 3 is affected by Channel 2)

Each oscillator has it's own ADSR controls for instrument dynamics, and there's a global filter with low, band and high-pass modes that can be applied to any of the oscillator channels.

If this sounds like quite a powerful feature set for a home computer, you might be interested to know the chip's designer was Bob Yannes who went on to co-found Ensoniq.

As well as these features there are a few more registers at the end of the list that can be used in your code. Firstly there are two Analog to Digital converter registers for the Paddle Controllers, and then there is a Random Number Generator.  This uses the oscillator output from Channel 3 to provide an 8-bit number. To get what we think of as random numbers you set the third oscillator to noise, and use the pitch of that channel to control the speed of changes to the random value.  You can also use this as a low-quality SID sampler, by playing an instrument on channel 3 and then recording the values that come in to memory.  With a bit of manipulation you can play them back through the 4-bit volume register.  I did this in the Type Mismatch demo.

How the player works:

To update the player independently of the main loop we point the hardware interrupt ($0314-$0315) at our music player start point.  This gives us a regular update every vblank (50fps for PAL, 60fps for NTSC) so we have a steady tempo.  As we're using the interrupt in freewheeling mode rather than hitting a specific raster line, it will be triggered wherever the raster beam is when the interrupt is called.  This usually isn't a good idea for things you want to happen at a specific time, however for us this is actually useful later in the player as we'll use the raster beam position to pick values.

The player doesn't have a traditional song/pattern setup, only a single pattern with some hard-coded changes to make it get tweaked a little bit over time.

Every pass a counter (located at $60) counts down until it rolls around to #$ff, when this happens it's time to move onto the next note in the pattern. If this hasn't happened the player jumps to the update portion, this handles things that need to happen continually such as the filter sweep being added or checking if the drums have reached their note-off time.

If the counter is #$ff it's reset to the song's tempo (#$06) and the next note is setup, as well as resetting the filter cut-off value and checking if a drum sound needs to play at this new position.

A pattern is 16 notes long, when all notes are played it checks if the amount of cycles for that pattern has happened.  If not the cycle count is decreased and we continue.  If we've reached the end of the cycle a few things are changed:

* The waveform used for the main melody.  (a selection between saw, pulse and triangle)
* The length of the next pattern cycle from a table.
* The melody and ring-mod source notes.
* The drum check value, to skip some types of drum to add some dynamics to the 'song'.

Some of these changes use 'naughty' self-modifying code to directly alter the code as it's running. (so it changes from what was compiled to something else)  For small-form stuff like this it can really save you some space, though obviously resetting to initial state needs additional code.

Melody

The music data comes from the Kernal and BASIC roms in the machine, this is essentially 'free' data as the Kernal, BASIC and character gfx ROMs are in memory when you switch the machine on.  As we leave the machine in this state we're free to use them.   This data can potentially be any value between 0-255, but as we're looking only for low bass notes we do some BIT math to get something useable.

The C64 oscillators have a full 16-bit range so can be very accurate indeed, however we are only using the high-bit register as the slightly dischordant sound seem to suit these sort of acid lines.

The main melody uses some of the Kernal, I picked $f704 as it had a good string of melodies. (the kernal is at $e000-$ffff by default in memory)

lda $f704,y ; Read byte from Kernal area.
and #$0f ; Remove the top 4 bits of the value so we get a note between 0 and 15.
eor #$03 ; Mess around with the bottom two bits, this is more personal taste for the data I'd chosen.
sta 54273 ; store in channel 1's high frequency value register.

And the ring/sync modulation channel reads from the BASIC kernal.  I picked $a340 as a starting point there. (BASIC is at $a000-$bfff)  This also removes BITs from the data but leaves a range of 0-63, which means you occasionally get harmonics above the bass note that add a bit of flavour to the sound.

At the end of a pattern cycle the upper byte of the songdata reads are increased by one, so $f704 becomes $f804 and $a340 becomes $a440 to give a new set of notes.   Because this was for a video I don't do any checking for wrapping round to the start of memory, so eventually you'll end up just with the same low note for a while as it cycles through (what is probably) empty bytes.

The earlier driver resets the machine after a few cycles so never reaches a blank space in memory.

You could also use the program itself as source data but as you continue coding the melodies will keep changing.  Best to leave that bit until the end.

Drums

The drum setup isn't particularly efficient and I suspect I was going to work on it some more in the old player at some point.  Anyway, it cycles through a loop of BIT values looking for a match in the drum pattern data.  These being:

* $40 - Bass Drum
* $08 - Snare
* $02 - Hi-hat
* $01 - Rest (or indeed anything else, again I think this was going to be extended originally)

If it finds a match relevant values in a few small tables are set for Waveform, Starting Pitch, Pitch Sweep (signed) and the amount of frames to wait before the note-off is played.  This doesn't use the modern way of producing drums by cycling through an instrument table to set the waveform and pitch values per frame.  That is the setup I used in In A Loop a few years back.


The pattern data is stored with the demo, it could use ROM data but finding a coherent beat is actually a lot more difficult than you'd imagine compared to looping melodies.  I could write a program to check for exact matches in the ROM I guess.  I've done something similar trying to cram all of State of the Art's visuals into a 4kb demo.

Changes between the old player and new one:

As I mentioned I didn't think the old player had enough 'bite' to the sound, there's a sort of aggressive overtone to a 303 that is difficult to do with just one channel of the SID.

Originally I was using the second channel as a fake delay for the melody.  This was done by reading 3 steps behind the main melody's pattern, with the second channel playing at a lower volume than the first.   I switched this over to a triangle waveform with sync and ring-tone enabled, which follows it's
own note sequence.  This gives some extra harmonics to the sound when above the melody pitch, but also affects the timbre of the main note when below it.

The other thing I did was change how the filter sweep works.  In the original player the sweep starts at one point and drops down the same amount for every note.   I made two changes, firstly the sweep speed has different values per note, by reading a value from the ROM decided by the position of the raster beam.  So it's quite varied and will actually be different everytime the driver is run.   Every cycle through the pattern the filter start speed is reset to a variable value between 0 and 127, again using a similar setup to the sweep.

For playback I went for the 8580 version of the SID above the older 6581.  The 6581 has a much 'dirtier' sound with filter enabled because the filter adds some distortion to the affected channels, but I found a nice range with the 8580 filter that I preferred this time.

Things that could be added:


Apart from a general refactor and optimization pass (starting with that init code) some possible ideas are:

* Adding Accent and Pitch slide functions to really give that 'acid line' sound.
* Change drums to use instrument tables for more punchy control.
* Autostart when loading.

The old player has a proper BASIC header as it was still way under 303 bytes.  But there's also a workaround that skips a BASIC header and uses only 4 byte, but means your code starts just before the tape buffer area.  The only problem with this one is it's right next to the default screen location, so you either have to move your screen or hide it.  I used this in mus1k by setting the screen colour to the same as the text. :)

On the subject, if you have really small code (in the 20 bytes range) you can set the start address to $7c and have it autostart for you with no header at all.   I used this in Glitchshifter

303 source for C64:

    ; 303 style by 4mat 2020
    ; Type sys 49152 to play.
   
    ; Written using Dasm assembler. Should be mostly compatible with other assemblers except:   
    ; + The processor line is probably Dasm only unless your assembler handles multiple processors.
    ; + org might be replaced by * or something else.
    ; + Some assemblers use !byte or something else instead of .byte, see your docs.
   
    ; Memory map at bottom of source.


    ; Set start address to $c000. (49152)   
    org $c000
    processor 6502
   
    ; Init Intro.
    ; Disable interrupts while we do our setup.

   
    sei

    ; Copy song data to zero page.
    ; This saves a bit of memory as some assembler commands will only use 2 bytes instead
    ; of 3 if they were in 'normal' memory.

   
    ldy #$00
    ldx #$30
argha
    tya
    sta $60,x
    lda datas-1,x
    sta $0f,x
    dex
    bne argha

    ; Setup soundchip. 
    ; By default I've set all channels to use triangle and sync/ring-mod for the first cycle.


setsound
    lda #$17
    sta 54276,y
    lda #$08
    sta 54275,y
    lda #$00
    sta 54277,y
    lda $1d,x
    sta 54278,y
    lda $10,x
    sta $6b,x
    lda $12,x
    sta 54295,x
    inx
    clc
    tya
    adc #$07
    tay
    cpy #$15
    bne setsound

    ; Point hardware interrupt at the music player section.
   
    lda #<musicloop
    sta $0314
    lda #>musicloop
    sta $0315

    ; Enable interrupts so we're good to start.
    cli

    ; Main loop. 
    ; Only the visuals are updated here. This part runs as fast as the spare CPU
    ; time will allow.

   
loop
    ; Take the current filter cut-off value and add 65 to it (so it's in the Petscii area
    ; of the character set)

    lda $62
    adc #$41
    ; Use the random number generator to read the oscillator value from channel 3 and use
    ; this as the X offset position.  Because channel 3 is where the drums are, for snares
    ; and hihats this will be using the noise waveform.

    ldx $d41b
    ; Store the petscii char into the screen area.  This is repeated 4 times to fill the 4
    ; pages of the screen.  I used a slight offset from the usual $0400 start position to
    ; move the visuals around a bit.

    sta $03e8,x
    sta $04e8,x
    sta $05e8,x
    sta $06e8,x
    jmp loop

    ; Music player.
musicloop
    ; Decrease tempo tick, if it goes minus (#$ff) we need a new note, otherwise we can jump
    ; to the instrument update part.

    dec $60
    bmi musicloop2
    bpl updatedrums
    ; Get new note in the pattern.
musicloop2
    ; Reset tempo tick to full. (in our case #$06 which is about 125 bpm)
    lda #$06
    sta $60

    ; Make new value to add to filter cut-off for this note.
    ; Using the current raster beam value ($d012) as an offset, read a value from the
    ; Kernal ROM and use only the lower 4-bit value.  Then add the current number of
    ; loop cycles ($6b) to make each value slightly different.

    ldy $d012
    lda $e144,y
    and #$0f
    adc $6b
    sta filtsweep+$01
   
    ; Get new note values, the current pattern position stored in $61.   
    ldy $61
   
    ; First the main melody, using the lower 4-bits from some of the Kernal ROM to only
    ; use bass values between 0 (silence) and 15.  The extra 'eor #$03' is more for
    ; personal taste with the different melodies.

chan1
    lda $f704,y
    and #$0f
    eor #$03
    sta 54273

    ; Now the sync/ring-mod channel, using data from the BASIC ROM, but only taking
    ; the low 6-bits.

chan2
    lda $a340,y
    and #$3f   
    sta 54273+7

    ; Set new Drum
    ; This checks against 4 possible BIT values to see if a drum needs to be played.
    ; ($40 = Bass Drum, $08 = Snare , $02 = Hihat , $01 = Rest)  
 
noresetsq
    ldx #$00
drumcheck
    ; Get next position value from drum rhythm table.
    lda $30,y
    ; Check if value matches the current BIT value indexed.
    and $20,x
    beq nobit
    ; If it does this means we have a new drum to play.
    ; Firstly set the waveform from that table.  Also store it at $66 so we can apply
    ; note off later.

    lda $24,x
    sta 54276+14
    sta $66
    ; Set the drum pitch, this is placed directly in a variable as we do work on this
    ; value when adding the pitch sweep.

    lda $27,x
    sta $67
    ; Set the drum pitch sweep.
    lda $2a,x
    sta $68
    ; Finally set the timer value before applying the note-off on the drum.
    lda $2d,x
    sta $69
nobit
    dex
    bpl drumcheck

    ; Increase the pattern position by 1 and AND the value by #$0f so it's always
    ; between 00-15.

    iny
    tya
    and #$0f
    sta $61
    ; Check if the value is 00, if not we don't need to decrease the amount of pattern
    ; cycles yet.

    bne noupdate

    ; Decrease the amount of pattern cycles and check if this is #$ff yet.  If not we
    ; don't need to create a new pattern yet.

    dec $6b
    bpl noupdate   

    ; When the pattern cycles are complete it's time to create a new pattern.
   
    ; Firstly we do some self-modifying code to the initial value of the drum check loop.
    ; This means we don't always get the same drum beat by dropping out checks for the
    ; snare and hi-hats.

    dec noresetsq+$01
    lda noresetsq+$01
    and #$03
    sta noresetsq+$01
    ; We also use this value to change the melody line's waveform, from the table at $15-$18.
    tax
    lda $15,x
    sta 54276
    ; We also change the amount of pattern cycles for the next pattern from the
    ; table at $19-$1c.

    lda $19,x
    sta $6b

    ; Now we do some more self-modifying code to change the memory position to read the
    ; note data from.  This increase the high memory value by one for the main melody and
    ; the ring-tone channel.  This does mean that eventually both values will reach past the
    ; end of memory and reset back to $0000.  As mentioned in the docs as this was for a video
    ; I didn't add any checking for this occurance, however the older version of the player resets
    ; the machine to avoid this happening.

    inc chan1+$02
    inc chan2+$02

    ; This resets the starting value of the filter cut-off value.  It works very similar to
    ; the filter sweep setup though we start with a 7-bit value, divide it by half and then add
    ; the current pattern position value to it.   

noupdate
    ldy $d012
    adc $e948,y
    and #$7f
    lsr
    adc $61
    sta $62   
   
    ; This is where the player falls through to on every frame.  This updates the filter and
    ; drums and then sends an acknowledgement to the timer system that this routine has finished.
       
    ; Check the tempo tick against the current drum's note-off value.  If it's the same switch off
    ; the waveform's gate (bit 1) so the release part of the ADSR gets activated
.
updatedrums
    lda $60
    cmp $69
    bne nodrumgate
    dec $66
    lda $66
    sta 54276+14
nodrumgate
    ; Add the drum's pitch sweep value to the current pitch and store it in channel 3's high pitch
    ; register.  Note that we don't do the math directly on the register because the SID is write
    ; only when enabled.

    clc
    lda $67
    adc $68
    sta $67
    sta 54273+14
   
    ; Set current filter cut-off value to the cut-off register.
    lda $62
    sta 54294
    ; Decrease the cut-off value by the current filter sweep value. (note this was set in
    ; self-modifying code earlier)  If the value is already below zero don't store it in the
    ; variable so it only stays at this value.

filtsweep
    sbc #$00
    bmi filtnot
    sta $62

    ; End of music driver, call to IO system that we've ended our routine.   
    ; As we have the full default system enabled we need to use $ea31 rather than the
    ; less cpu-heavy $ea81

filtnot
    jmp $ea31

datas
    .byte $05,$07                  
siddata   
    .byte $f3,$1f,$00            
basswaves
    .byte $11,$21,$41,$21      
length   
    .byte $01,$03,$03,$03      
vols
    .byte $a9,$3c,$79               
btt
    .byte $40,$02,$08,$00            
wav
    .byte $41,$81,$81                
not
    .byte $0a,$ff,$20                
plu
    .byte $ff,$fe,$fc                
del
    .byte $03,$02,$01                
beat
    .byte $40,$01,$02,$01,$08,$01,$02,$01,$40,$01,$02,$01,$08,$01,$02,$40

; Memory map:

; $10 = Initial pattern cycles value for first pattern. (datas)
; $12 = SID Filter values for filter type/volume and resonance/channel allocation. (siddata)
; $15 = Melody line waveform table. (basswaves)
; $19 = Melody loop cycle length table. (length)
; $1d = SID Channel sustain and release values. (Attack/Decay are always zero) (vols)
; $20 = Drum BIT value check table. (btt)
; $24 = Drum Waveform table. (silence isn't stored in drum values) (wav)
; $27 = Drum starting Pitch value table. (not)
; $2a = Drum Pitch addition signed value table. (plu)
; $2d = Drum ticks before note off value table. (del)
; $30 = Drum beat pattern table. (beat)

; $60 = Current note timing tick.
; $61 = Current note position in pattern.
; $62 = Filter cut-off value.
; $66 = Drum Waveform setting for use with note-off.
; $67 = Current drum pitch.
; $68 = Pitch value to add to drum pitch every frame. (signed value)
; $69 = Drum note-off timer.
; $6b = number of cycles to loop the current pattern