Here I describe how I managed to get a 1MHz 6502 sequencing, mixing and playing 8 samples in real time, in mono or stereo. For demos and downloads, check out the Drum8 Page.
No Hardware Sound Support
On the Apple II there is no sample buffer or audio clock, not even a DAC, hence the add-on card in Slot 5. The 6502 has to process each sample and clock it out at a consistent rate regardless of what is playing.
I used a layered approach to achieve this. From the top down:
- A sequencer selects a pattern every musical bar, the page of squares you see in the Live Beat Editor,
- A bar is split up into 32nd notes. Any of the 16 sounds can be triggered at these 32 boundaries. The presence of a square in the pattern causes a sound to be allocated to a “voice channel”. The allocation is pre-determined in the Configuration Editor and any playing sound for that voice is interrupted, useful in cases like the open and closed high hat.
- Within a 32nd note, a tight loop executes which:
1) Reads samples from memory,
2) Adds them together (mixing) and outputs them to a DAC
(the specifics depend on the DAC/Voice configuration),
3) Increments the memory pointers for the voices
(and checks when the end of a given audio sound is reached),
4) Also checks a counter which determines when the 32nd note is complete.
Working Around No Audio Clock
To keep the timing (and hence pitch) of the 32nd notes as consistent as possible, Drum8 uses the trick of always playing samples, even when a given voice is quiet/inactive. One page of memory (at $1F00) is used to store “silence”. So silencing a finished voice is just a case of pointing it to the silent page.
This also means that the mixer is always adding up the same number of samples for every output DAC. The Loader uses this fact to prescale and offset all the sounds (including the silent page) so they will add up to a sample centred around the audio zero, 128, which is half the full scale range of an 8 bit DAC.
Finding The Cycles To Mix 8 Voices
In Drum8 V1, using the 6502’s indirect addressing mode to read the samples worked well for 4 voices, at 8kHz sample rate with a few cycles to spare.
Unfortunately It was too slow for 8 simultaneous voices. The problem wasn’t the sample mixing itself – the pre-scaled samples naturally add up without needing tests, shifts, offsets or carry bit handling.
The issue was managing all the pointers for the voice channels. The original code looked like this:
LDA (Voice1L),Y ADC (Voice2L),Y ADC (Voice3L),Y ADC (Voice4L),Y STA $C0D0 ;slot 5 DAC
The indirect references each take 5 cycles compared to 4 for absolute reads/adds, and also requires the Y register to be zero, wasting it in the loop. The solution was to move the sample mixing to Page Zero (addresses $0000-$00FF in the 64k address space). The same mixing code becomes:
Mixer EQU * Voice1 LDA $1F00 ;address overwritten Voice2 ADC $1F00 Voice3 ADC $1F00 Voice4 ADC $1F00 STA $C0D0 JMP MixerInc
For the price of an extra JMP back from page zero (the jump to the mixer is needed in any case), 8-3 = 5 cycles are saved when mixing 8 voices.
The now free Y register is used to store the silence end page, cleaning up the mixer pointer handling, in itself important to keep it all in one page as mentioned below.
Give Me A Few Cycles More
It was close but still too low in pitch for my liking. It got there by rearranging the pointer increment tests so the most often case of only a single byte increment for each voice pointer avoided branches. This came at the cost of a “branch back” after having to increment the high byte, but it sounded OK. The increment code looks like:
MixerInc INC Voice1+1 ;modify code in-place :) BEQ Mix1H Mix2 INC Voice2+1 BEQ Mix2H Mix3 INC Voice3+1 : other voices and test for end of 32nd note JMP Mixer ; back to page zero for next sample Mix1H INC Voice1+2 ; high byte of sample address LDA Voice1+2 EOR Voice1End ; EOR instead of CMP keeps carry bit 0 BNE Mix2 ; reached end page of currently playing sample? STX Voice1+2 ; set the silence page for the voice STY Voice1End BEQ Mix2 Mix2H INC Voice2+2 :
I had to take care that the entire sample pointer incrementing code was assembled into the same page to avoid extra cycles.
Take A Look Around
With Drum8 loaded you can reset into the monitor and look around. You’ll find the sample mixer at $90 and the pointer incrementor near $1B00, part of the mixer plugin that is selected in the Configuration Editor.
The 16 sound sample voice channel, sample start, and end pages are at $60,$70 and $80 respectively.
800G will restart Drum8.