Oversampler Commentary

overlogo3

Equally as important as writing new code, is reading old code. Sunday, I picked apart the venerable Oversampler, because I'd been considering using its technology in something I'm developing. I don't really want to say what I'm developing yet, but I figured that the remaining GS users have either an emulator or a CF card (or other huge mass-storage), so I could use 5-megabyte sound files instead of MOD/Soundsmith for a background track, to provide a really stunning soundtrack for a year-2007 IIGS product.


Disassembling IIGS OMF-linked files is easier than ever, you just double-click on them in CiderPress. In the space of about 8 hours, I managed to completely understand Oversampler, and I want to share a bit about its technology with you.


You may know that Oversampler loads up to 16KBytes of data from the disk at a time, and if you request, performs "oversampling," or averages the values of every two bytes and inserts the average between them, thus doubling the resolution of a file from say, 23khz to 46khz, for example, for "2x oversampling." It also has a "4x" mode for even greater resolution.


Most of Oversampler is actually desktop interface and windows, so let's dive right into the player code...


; init player
00/03E3: A2 12 0A      LDX #0A12
00/03E6: 22 00 00 E1   JSL E10000  WaitCursor()
00/03EA: 20 7E 09      JSR 097E ; init DOC RAM and registers
00/03ED: 20 4E 0A      JSR 0A4E ; read from disk and stash to DOC (2x$4000)
00/03F0: 90 08         BCC 03FA {+08} ; skip to player if no problems
00/03F2: A2 04 CA      LDX #CA04 ; else set cursor back to error and stop
00/03F5: 22 00 00 E1   JSL E10000  InitCursor()
00/03F9: 60            RTS

Nothing too fancy here, except that I should show you what's done to init the DOC RAM and registers, and load the first chunks from disk.

The routine below is responsible for setting all the registers of the DOC to default values as specified in the table after it, as well as filling up the DOC RAM with a value of $80, or silence. Note the construction of LDA E100CA / AND #0F / ORA #60 / STA E1C03C. If you've programmed the DOC before, you know that this is because the system volume is at E1/00CA, and when we ask the DOC for access to its registers, we must preserve the bits for system volume. 00 is stored in C03E/C03F to indicate the beginning of the DOC's 64K RAM, and then $80 is slammed into C03D 65,535 times. Then, the table at 09C5 in the program code is read, and for each value in the table, a subroutine is called to set the given DOC register to the particular value.

; clear out the DOC RAM to all $80 (baseline)
00/097E: E2 20         SEP #20
00/0980: AF CA 00 E1   LDA E100CA
00/0984: 29 0F         AND #0F
00/0986: 09 60         ORA #60
00/0988: 8F 3C C0 E1   STA E1C03C ; select DOC RAM, auto-increment
00/098C: A9 00         LDA #00
00/098E: 8F 3E C0 E1   STA E1C03E
00/0992: 8F 3F C0 E1   STA E1C03F ; zero it out high and low
00/0996: A0 00         LDY #00
00/0999: A9 80 8D      LDA #8D80 ; LDA #80
00/099C: 23 0B         AND 0B,S ; STA 0B23
00/099E: 8F 3D C0 E1   STA E1C03D ; store data into the doc
00/09A2: C8            INY
00/09A3: D0 F9         BNE 099E {-07}
00/09A5: C2 20         REP #20
00/09A7: A0 00 00      LDY #0000
00/09AA: B9 C5 09      LDA 09C5,Y
00/09AD: C9 FF FF      CMP #FFFF
00/09B0: F0 12         BEQ 09C4 {+12}
00/09B2: 29 FF 00      AND #00FF
00/09B5: AA            TAX
00/09B6: B9 C5 09      LDA 09C5,Y
00/09B9: EB            XBA
00/09BA: 29 FF 00      AND #00FF
00/09BD: 20 3C 09      JSR 093C ; set DOC register
00/09C0: C8            INY
00/09C1: C8            INY
00/09C2: 80 E6         BRA 09AA {-1A}
00/09C4: 60            RTS

; default DOC register settings
00/09C5: 00 00         BRK 00 ; register 00 (freq low0) = 00
00/09C7: 20 08 10      JSR 1008 ; register 20 (freq high0) = 08
00/09CA: 00 30         BRK 30 ; register 10 (freq low16) = 00
00/09CC: 08            PHP ; register 30 (freq high16) = 08
00/09CD: 08            PHP ; register 08 (freq low8) = 00
00/09CE: 00 28         BRK 28 ; register 28 (freq high16) = 08
00/09D0: 08            PHP
00/09D1: 40            RTI ; register 40 (volume 0) = 00
00/09D2: 00 50         BRK 50 ; register 50 (volume 16) = 00
00/09D4: 00 48         BRK 48 ; register 48 (volume 8) = 00
00/09D6: 00 80         BRK 80 ; register 80 (address low0) = 00
00/09D8: 00 88         BRK 88 ; register 88 (address low8) = 00
00/09DA: 00 90         BRK 90 ; register 90 (address low16) = 00
00/09DC: 00 C0         BRK C0 ; register C0 (wavetable info0) = 00
00/09DE: 00 C8         BRK C8 ; register C8 (wavetable info8) = 00
00/09E0: 00 D0         BRK D0 ; register D0 (wavetable info16) = 00
00/09E2: 00 A0         BRK A0 ; register A0 (control 0) = 02 one shot, no int
00/09E4: 02 A8         COP A8 ; register A8 (control 8) = 02 one shot, no int
00/09E6: 02 B0         COP B0 ; register B0 (control 16) = 02 one shot, no int
00/09E8: 02 FF         COP FF ; FFFF - end of register defaults
00/09EA: FF

Let me show you that DOC register-setting routine too, while I'm at it. Notice that Ciderpress' disassembler choked a bit after SEP #20, but I was able to fill in. The LDA E1C03C/BMI loop just waits till the DOC isn't busy, and then of course the LDA E100CA / AND #0F / STA E1C03C construction again. Note that we store $0 into the upper part of C03C this time whereas above we stored $6 there. That's because $6 means, access the DOC RAM in auto-increment mode (where each successive write will write to the next address in DOC RAM), whereas $0 means we want access to the DOC's registers.

; subroutine to set a doc register
; a = data
; x = register number
00/093C: E2 20         SEP #20
00/093E: 48            PHA
00/093F: AF 3C C0 E1   LDA E1C03C ; sound control register
00/0943: 30 FA         BMI 093F {-06} ; loop till it isn't busy
00/0945: AF CA 00 E1   LDA E100CA ; get system volume
; * disassembler error: should be
; * AND #0F   - select DOC registers
; * STA E1C03C
00/094F: 8A            TXA
00/0950: 8F 3E C0 E1   STA E1C03E ; doc register number
00/0954: 68            PLA
00/0955: 8F 3D C0 E1   STA E1C03D ; doc register data
00/0959: C2 20         REP #20
00/095B: 60            RTS

; read a doc register
; a = data
; x = register number
00/095C: E2 20         SEP #20
00/095E: AF 3C C0 E1   LDA E1C03C
00/0962: 30 FA         BMI 095E {-06} ; wait till sound controller not busy
00/0964: AF CA 00 E1   LDA E100CA
; * disassembler error: should be
; * AND #0F   - select DOC registers
; * STA E1C03C
00/096E: 8A            TXA
00/096F: 8F 3E C0 E1   STA E1C03E
00/0973: AF 3D C0 E1   LDA E1C03D
00/0977: AF 3D C0 E1   LDA E1C03D
00/097B: C2 20         REP #20
00/097D: 60            RTS

So now that the DOC RAM and registers are initialized, let's continue with the player. Next is a call to a routine at 0A4E, which loads chunks from disk and shoves them into the DOC RAM. As you might imagine, there's a lot of magic here, so let's take a closer look.

; read up to 16 kbytes from disk and stash to DOC, twice
; fills DOC RAM from $0000-$8000 (32k) with or without oversampling
00/0A4E: A9 00 00      LDA #0000
00/0A51: 8D DD 0E      STA 0EDD
00/0A54: 8D DF 0E      STA 0EDF
00/0A57: 20 3C 0E      JSR 0E3C ; seek to file position
00/0A5A: B0 33         BCS 0A8F {+33} ; return if error?
00/0A5C: A6 88         LDX 88
00/0A5E: A4 8A         LDY 8A
00/0A60: 8E C3 0E      STX 0EC3
00/0A63: 8C C5 0E      STY 0EC5 ; give GSOS the pointer to our $4000 bytes
00/0A66: A9 00 40      LDA #4000 ; start with 4000 bytes but scale down if
00/0A69: AE 26 05      LDX 0526 ; oversampling
00/0A6C: F0 04         BEQ 0A72 {+04}
00/0A6E: 4A            LSR ; $2000 bytes if 2x, $1000 bytes if 4x, etc
00/0A6F: CA            DEX
00/0A70: D0 FC         BNE 0A6E {-04}
00/0A72: 8D C7 0E      STA 0EC7
00/0A75: 9C C9 0E      STZ 0EC9 ; EC7/EC9 contains bytes to read, 4000/2000/1000
00/0A78: 20 4F 0E      JSR 0E4F ; do GSOS read
00/0A7B: B0 12         BCS 0A8F {+12} ; return if error
00/0A7D: A2 00 00      LDX #0000
00/0A80: 20 90 0A      JSR 0A90 ; stash data to DOC (x=0)
00/0A83: 20 4F 0E      JSR 0E4F ; do GSOS read
00/0A86: B0 07         BCS 0A8F {+07} ; return if error
00/0A88: A2 01 00      LDX #0001
00/0A8B: 20 90 0A      JSR 0A90 ; stash data to DOC (x=1)
00/0A8E: 18            CLC
00/0A8F: 60            RTS

; this is where we stash data to the DOC
; a, y - trashed
; $88 - pointer to the data
; x=0 - stash data at $0000 in DOC RAM
; x=1 - stash data at $4000 in DOC RAM
00/0A90: E2 20         SEP #20
00/0A92: AF CA 00 E1   LDA E100CA
00/0A96: 29 0F         AND #0F
00/0A98: 09 60         ORA #60
00/0A9A: 8F 3C C0 E1   STA E1C03C ; select DOC RAM, auto-increment
00/0A9E: A9 00         LDA #00
00/0AA0: 8F 3E C0 E1   STA E1C03E ; address low word: $0000
00/0AA4: E0 00 00      CPX #0000
00/0AA7: F0 02         BEQ 0AAB {+02} ; if x=0, do low 16K, else set up for high 16K
00/0AA9: A9 40 8F      LDA #8F40 ; LDA #40
00/0AAC: 3F C0 E1 A0   AND A0E1C0,X ; STA E1C03F for addr high word
00/0AAF: A0 00         LDY #00 ; LDY #0000
00/0AB1: 00 B7         BRK B7 ; LDA [88],Y - $88 is pointer to the $4000 bytes
00/0AB3: 88            DEY ;
00/0AB4: D0 01         BNE 0AB7 {+01}
00/0AB6: 1A            INC ; INC if zero so sound doesn't break
00/0AB7: AE 26 05      LDX 0526
00/0ABA: F0 03         BEQ 0ABF {+03} ; skip oversampling if it's not on?
00/0ABC: 20 D7 0A      JSR 0AD7 ; else perform oversampling
00/0ABF: 8F 3D C0 E1   STA E1C03D ; stash data to DOC
00/0AC3: C8            INY
00/0AC4: CC CB 0E      CPY 0ECB
00/0AC7: 90 E9         BCC 0AB2 {-17} ; keep looping till y=0ECB
00/0AC9: CC C7 0E      CPY 0EC7
00/0ACC: F0 06         BEQ 0AD4 {+06} ; just return if not end of file?
00/0ACE: A9 00         LDA #00
00/0AD0: 8F 3D C0 E1   STA E1C03D ; store final zero for end of sound
00/0AD4: C2 20         REP #20
00/0AD6: 60            RTS

; 2x oversampling
00/0AD7: E0 02 00      CPX #0002
00/0ADA: F0 13         BEQ 0AEF {+13} ; do 4x oversampling if it's on
00/0ADC: 48            PHA
00/0ADD: C2 20         REP #20
00/0ADF: 18            CLC
00/0AE0: 6D 23 0B      ADC 0B23
00/0AE3: 4A            LSR ; (ByteA + ByteB) / 2
00/0AE4: E2 20         SEP #20
00/0AE6: 8F 3D C0 E1   STA E1C03D ; stash data to DOC
00/0AEA: 68            PLA
00/0AEB: 8D 23 0B      STA 0B23
00/0AEE: 60            RTS

; 4x oversampling
00/0AEF: 48            PHA
00/0AF0: C2 20         REP #20
00/0AF2: 18            CLC
00/0AF3: 6D 23 0B      ADC 0B23
00/0AF6: 4A            LSR
00/0AF7: 8D 21 0B      STA 0B21 ; (ByteA + ByteB) / 2
00/0AFA: 18            CLC
00/0AFB: 6D 23 0B      ADC 0B23
00/0AFE: 4A            LSR ; Point halfway between oversample byte and ByteA
00/0AFF: E2 20         SEP #20
00/0B01: 8F 3D C0 E1   STA E1C03D ; ... to DOC
00/0B05: AD 21 0B      LDA 0B21
00/0B08: 8F 3D C0 E1   STA E1C03D ; Byte A to DOC
00/0B0C: 68            PLA
00/0B0D: 8D 23 0B      STA 0B23
00/0B10: C2 20         REP #20
00/0B12: 18            CLC
00/0B13: 6D 21 0B      ADC 0B21
00/0B16: 4A            LSR ; Point halfway between oversample byte and ByteB
00/0B17: E2 20         SEP #20
00/0B19: 8F 3D C0 E1   STA E1C03D ; ... to DOC
00/0B1D: AD 23 0B      LDA 0B23 ; and get ByteB ready to slam in there next
00/0B20: 60            RTS

Notice there isn't a lot there that you haven't already seen. We call a subroutine (that isn't listed here) to do the GSOS seek and read into a $4000 (16k) byte buffer that was allocated earlier with _NewHandle. If we're doing 2x oversampling, we tell GSOS to only read $2000 bytes because the sample size will be doubled in DOC RAM. Similarly, only read $1000 bytes for 4x oversampling. Then, the data is written to the DOC, and as we do so, the oversampling calculations are performed, if enabled, and $00 bytes are transformed to $01 bytes to keep the oscillator from halting when they are encountered. The other thing to notice is that it happens twice. Once, DOC RAM is filled from $0000 to $3FFF, and the second time it is filled from $4000 to $7FFF, so now we've got 32K (half) of DOC RAM full of sound data.

And we return to the main player...

; open player window, draw controls
00/03FA: 48            PHA
00/03FB: 48            PHA
00/03FC: F4 00 00      PEA 0000
00/03FF: F4 00 00      PEA 0000
00/0402: F4 00 00      PEA 0000
00/0405: F4 00 00      PEA 0000
00/0408: F4 00 00      PEA 0000
00/040B: F4 77 0B      PEA 0B77 ; 0B77 = window redraw callback
00/040E: F4 00 00      PEA 0000
00/0411: F4 00 00      PEA 0000
00/0414: F4 02 00      PEA 0002
00/0417: F4 00 00      PEA 0000
00/041A: F4 F8 0F      PEA 0FF8
00/041D: F4 0E 80      PEA 800E
00/0420: A2 0E 61      LDX #610E
00/0423: 22 00 00 E1   JSL E10000 NewWindow2(@T,RC/4,@draw,@def,pDesc,pRef/4,rType):@W
00/0427: 68            PLA
00/0428: 8D 14 05      STA 0514
00/042B: 68            PLA
00/042C: 8D 16 05      STA 0516
00/042F: 20 EB 09      JSR 09EB ; set up keyboard interrupts

Nothing fancy about that, just a call to NewWindow2... except for the last part there. Keyboard interrupts? Yeah, Oversampler uses its own keyboard interrupt handler to tell if you pressed ESC so it can stop playing. That handler is nothing fancy, and it doesn't provide insight on Oversampler technology though, so I'm skipping it. Let's continue...

00/0432: A9 FF FF      LDA #FFFF
00/0435: 8D 18 05      STA 0518
00/0438: AD 16 05      LDA 0516
00/043B: 48            PHA
00/043C: AD 14 05      LDA 0514
00/043F: 48            PHA
00/0440: A2 10 10      LDX #1010
00/0443: 22 00 00 E1   JSL E10000  DrawControls(@Wind)
00/0447: A0 00 00      LDY #0000
00/044A: B9 28 05      LDA 0528,Y ; set up DOC registers to default
00/044D: C9 FF FF      CMP #FFFF
00/0450: F0 12         BEQ 0464 {+12}
00/0452: 29 FF 00      AND #00FF
00/0455: AA            TAX
00/0456: B9 28 05      LDA 0528,Y
00/0459: EB            XBA
00/045A: 29 FF 00      AND #00FF
00/045D: 20 3C 09      JSR 093C ; set DOC register
00/0460: C8            INY
00/0461: C8            INY
00/0462: 80 E6         BRA 044A {-1A} ; loop till we're done setting them
; at this point, 0 is stopped, 8 and 16 are playing?
; oscillator 0 - echo-delayed channel if any
;                (otherwise it plays same as 16)
; oscillator 8 - quiet, used as timer for irqs
; oscillator 16 - "main" channel

I'm not interested in the call to _DrawControls, but you see there at 44A, we start setting up DOC registers from a table again, and that table is:

; DOC register defaults for player
00/0528: 00 E3         BRK E3 ; 00 (freq0lo) - E3
00/052A: 20 01 10      JSR 1001 ; 20 (freq0hi) - 01 - 483 for freq0?
00/052D: E3 30         SBC 30,S ; 10 (freq16lo) - E3
00/052F: 01 08         ORA (08,X) ; 30 (freq16hi) - 01 - 483 for freq16
00/0531: E3 28         SBC 28,S ; 08 (freq8lo) - E3
00/0533: 01 40         ORA (40,X) ; 28 (freq8hi) - 01 - 483 for freq8
; 40 (volume0) - FF
00/0535: FF 50 FF 48   SBC 48FF50,X ; 50 (volume16) - FF
; 48 (volume8) - 00
00/0539: 00 80         BRK 80 ; 80 (addr0) - 00
00/053B: 00 88         BRK 88 ; 88 (addr8) - 80
00/053D: 80 90         BRA 04CF {-70} ; 90 (addr16) - 00
00/053F: 00 C0         BRK C0 ; C0 (multi0) - 3F (play 32k sound)
00/0541: 3F C8 3E D0   AND D03EC8,X ; C8 (multi8) - 3E (32k sound, different resolution)
00/0545: 3F A0 01 A8   AND A801A0,X ; D0 (multi16) - 3F (play 32k sound)
; A0 (control0) - 01 (free run, halt, no int)
00/0549: 08            PHP ; A8 (control8) - 08 (free run, no halt, int)
00/054A: B0 10         BCS 055C {+10} ; B0 (control16) - 10 (free run, no halt, no int)
00/054C: FF FF A9 FF   SBC FFA9FF,X ; FFFF - end of table

Note that the frequency doesn't have to be 483, that is a default that's changed by the slider bars elsewhere. Same for frequency. But the biggest thing is the last three registers changed: Oscillator 1 is set up so that it would run in free-run mode with no interrupts, as soon as the halt bit is taken off. This is because it's the "echo delay" channel and starts after the other two. Oscillator 16 plays the same sound and starts immediately, because its halt bit isn't set. Oscillator 8 had its volume set to 0, because Oversampler uses it as a timer of sorts. Note that interrupts are enabled on it, meaning that when it gets to the end of sound data, it is going to throw a Sound IRQ, which is going to be very important soon. Note also that timer Oscillator 8 is missing a resolution bit, causing it to "play" every other byte and thus throw IRQ in half the time as the other oscillators take to play the sample. This is very helpful for the "refilling" process as you may imagine, so that when the two "live" oscillators are playing sound from the upper part, we can refill the lower, and vice-versa.

00/0464: AE 22 05      LDX 0522 ; echo delay
00/0467: F0 13         BEQ 047C {+13} ; wait a certain number of vblanks
00/0469: E2 20         SEP #20
00/046B: AF 19 C0 00   LDA 00C019  r:RDVBLBAR
00/046F: 10 FA         BPL 046B {-06}
00/0471: AF 19 C0 00   LDA 00C019  r:RDVBLBAR
00/0475: 30 FA         BMI 0471 {-06} ; wait for vblank(s?)
00/0477: C2 20         REP #20
00/0479: CA            DEX
00/047A: D0 ED         BNE 0469 {-13}
00/047C: A2 A0 00      LDX #00A0 ; register $A0 - Control 0
00/047F: A9 00 00      LDA #0000 ; value $00 - free run, no interrupt, not halt
00/0482: 20 3C 09      JSR 093C ; set DOC register
; at this point 0 is playing first chunk from disk
; (delayed if applicable)

All we did here was read the value of the echo delay, and wait a few vblanks, then turn Oscillator 1 loose. So at this point, 1 is playing delayed if asked, 16 is playing non-delayed, and 8 is the timer.

00/0485: 9C 1A 05      STZ 051A
00/0488: 9C 8C 05      STZ 058C
00/048B: 9C 8E 05      STZ 058E
00/048E: A9 FF FF      LDA #FFFF
00/0491: 8D 1C 05      STA 051C ; init the high/low flipflop variable
; main player loop?
00/0494: AD 8C 05      LDA 058C
00/0497: D0 36         BNE 04CF {+36} ; branch to stop/return if we hit the end
00/0499: AD AB 05      LDA 05AB ; read keypress register
00/049C: C9 1B 00      CMP #001B ; if ESC
00/049F: F0 2E         BEQ 04CF {+2E} ; stop player and quit
00/04A1: AD 1C 05      LDA 051C
00/04A4: D0 EE         BNE 0494 {-12} ; keep looping till 051C is zero (int on osc 8)
; interrupt happened (generator finished playing)
00/04A6: CE 1C 05      DEC 051C ; set 051C back to FFFF
00/04A9: 20 4F 0E      JSR 0E4F ; read chunk from soundfile
00/04AC: B0 21         BCS 04CF {+21} ; stop if error
00/04AE: AE 8E 05      LDX 058E
00/04B1: 20 90 0A      JSR 0A90 ; stash data to DOC (high/low $4000 depends on 058e)
00/04B4: AD 8E 05      LDA 058E
00/04B7: 49 01 00      EOR #0001
00/04BA: 8D 8E 05      STA 058E ; flip 058E between 0000 and 0001
00/04BD: AD 1C 05      LDA 051C
00/04C0: D0 D2         BNE 0494 {-2E} ; keep looping if interrupt didn't happen already
; interrupt happened while we were refilling?
00/04C2: EE 1A 05      INC 051A
00/04C5: AD 1A 05      LDA 051A
00/04C8: C9 03 00      CMP #0003 ; got here 3 times? sucks, let's bail
00/04CB: F0 06         BEQ 04D3 {+06} ; stop player, draw alert, and exit
00/04CD: 80 C5         BRA 0494 {-3B}
00/04CF: 20 EF 04      JSR 04EF ; stop player
00/04D2: 60            RTS

Above is the main code of the sound player. A few variables are zeroed out to start. 051A is a counter that keeps track of how many times an interrupt happened while we were refilling the DOC. If that happens 3 times, we throw an alert window (which I imagine says something like "too fast, dude!") and bail out. 058C is a quit flag set elsewhere, and 058E is flipped back and forth between 01 and 00, and is used to provide the x register for the call to refill the DOC RAM (recall that the x register determines whether high or low 16k is refilled in that subroutine). 051C is set by an interrupt handler that was installed by _SetSoundMIRQV when we started up. Let's have a look at that interrupt handler real quick:

; sound MIRQVector that got installed at startup
00/0555: 08            PHP
00/0556: 8B            PHB
00/0557: 0B            PHD
00/0558: C2 30         REP #30
00/055A: 4B            PHK
00/055B: AB            PLB
00/055C: AD 9B 00      LDA 009B ; original data bank register
00/055F: 5B            TCD
00/0560: A2 E0 00      LDX #00E0 ; register #E0: Oscillator Interrupt Register
00/0563: 20 5C 09      JSR 095C ; read DOC register
00/0566: 89 80 00      BIT #0080 ; is an oscillator generating an interrupt?
00/0569: F0 0B         BEQ 0576 {+0B} ; branch if not
00/056B: 29 3E 00      AND #003E ; mask out all bits except osc number
00/056E: C9 10 00      CMP #0010 ; is it oscillator 8?
00/0571: D0 03         BNE 0576 {+03} ; if not, don't zero-out 051C
00/0573: 9C 1C 05      STZ 051C ; zero-out 051C to show that an int happened
00/0576: A2 A0 00      LDX #00A0 ; register #A0: Control Register 0
00/0579: 20 5C 09      JSR 095C ; read DOC register
00/057C: 29 01 00      AND #0001 ; is it halted?
00/057F: F0 06         BEQ 0587 {+06} ; return if not, else halt them all
00/0581: CE 8C 05      DEC 058C ; set 058C to show that we've hit the end
00/0584: 20 2C 09      JSR 092C ; halt all voices
00/0587: 2B            PLD
00/0588: AB            PLB
00/0589: 28            PLP
00/058A: 18            CLC
00/058B: 6B            RTL

So all we do in the interrupt handler is clear the interrupt, and check to see if it was oscillator 8 that threw it. If it was oscillator 8 (the timer oscillator, recall), then we alter 051C, which is used by the player loop to know when the DOC RAM needs refilling from disk.

And there we have it, all the secret sauce contained within Oversampler. Now just think for a moment, what if some silly person decided to fill several disk partitions with sound files to play through this engine, while scrolling a plane of tiled graphics with Generic Tile Engine? We could have something truly interesting, couldn't we? More soon.


-Chris