PSG_Inst_Index: dc.l PSG1, PSG2, PSG3, PSG4, PSG5, PSG6, PSG7, PSG8, PSG9 PSG1: dc.b $00,$00,$00,$01,$01,$01,$02,$02,$02,$03,$03,$03,$04,$04,$04,$05; 0 dc.b $05,$05,$06,$06,$06,$07,$80; 16 PSG2: dc.b $00,$02,$04,$06,$08,$10,$80; 0 PSG3: dc.b $00,$00,$01,$01,$02,$02,$03,$03,$04,$04,$05,$05,$06,$06,$07,$07; 0 dc.b $80 ; 16 PSG4: dc.b $00,$00,$02,$03,$04,$04,$05,$05,$05,$06,$80; 0 PSG6: dc.b $03,$03,$03,$02,$02,$02,$02,$01,$01,$01,$00,$00,$00,$00,$80; 0 PSG5: dc.b $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$01,$01,$01,$01,$01,$01; 0 dc.b $01,$01,$01,$01,$01,$01,$01,$01,$02,$02,$02,$02,$02,$02,$02,$02; 16 dc.b $03,$03,$03,$03,$03,$03,$03,$03,$04,$80; 32 PSG7: dc.b $00,$00,$00,$00,$00,$00,$01,$01,$01,$01,$01,$02,$02,$02,$02,$02; 0 dc.b $03,$03,$03,$04,$04,$04,$05,$05,$05,$06,$07,$80; 16 PSG8: dc.b $00,$00,$00,$00,$00,$01,$01,$01,$01,$01,$02,$02,$02,$02,$02,$02; 0 dc.b $03,$03,$03,$03,$03,$04,$04,$04,$04,$04,$05,$05,$05,$05,$05,$06; 16 dc.b $06,$06,$06,$06,$07,$07,$07,$80; 32 PSG9: dc.b $00,$01,$02,$03,$04,$05,$06,$07,$08,$09,$0A,$0B,$0C,$0D,$0E,$0F; 0 dc.b $80 ; 16
If you know how the PSG works, it may seem like voices are impossible when compared to the YM2612 FM voices. The FM voices uses registers in the physical chip to emulate an instrument. The different values of the registers make the sound that's being emulated different. Some of the registers include how fast the sound reaches the maximum volume and then decays, as well as a frequency register, which is where you tell the chip a Hz value, which is really just a note, like C# for example. Using different algorthms and feedback values, the chip is able to emulate a sound that is somewhat recognizable to a physical instrument. On the flip side, the PSG chip, it has basically 2 registers: the frequency register and the volume register. So without all these other complicated registers that the FM chip has, how is a PSG Voice even possible?
Well, it's actually pretty simple. The PSG Voices are really just crude envelopes. Each byte in the PSG voice is just an amount that the Volume of the PSG Tone Generator will be changed. So, take PSG2 for example. Each cycle of the Sound Driver, the volume of the PSG is changed by 00, then 02, then 04, then 06. You've reached the end of the voice when you reach $80. I'm going to try and walk you through the routine to maybe make it a little easier to understand.
This isn't the full routine step by step but it is the basic concept that we're really looking for. HandlePSGVoice, as I've named it, only takes one parameter. A5 is the location in memory to where the current PSG Channel's data is being stored.
HandlePSGVoice: tst.b $B(a5) ; Test the voice number beq.w _ExitHandleVoice ; If it's empty, exit
This is pretty self explanitory. (A5 + $B) is where the voice number is stored for reference in memory. It's voice 0-9, any of the above PSG channels. If no voice is selected (0), then the routine is exited from. Now, this beginning of the function is called each cycle by the main "HandlePSGChan", which is just several calls to KeyOn, KeyOff, and parse the actual channel data to see if a rest is being played, or a note, or a coordination flag.
_LoadVoiceRt: ; move.b 9(a5),d6 ; Copy the Base Volume into D6 moveq #0,d0 ; Clear D0 move.b $B(a5),d0 ; Move the current voice setting into D0 beq.s UpdatePSGVolume ; If there's no voice selected, just update the volume
This is next part is called in two cases. Firstly, when the actual music track is being loaded, each channel is initialized. When the PSG channel is first initialized, the Base Volume is copied to D6, and then thevoice that was stored for the channel when the PSG variables were initialized (in $B(a5)) is moved into D0. The beq.s UpdatePSGVolume is only called if no voice was selected or $B(a5) is equal to 0. Otherwise, the Sound Engine needs to start handling the PSG Voice and figure out what to do with it.
movea.l (Off_PSGInst_Ptr).l,a0; Point A0 to the PSG Instrument pointer index subq.w #1,d0 ; Subtract from the voice value lsl.w #2,d0 ; Multiply it by 4 (Long Word pointers) movea.l (a0,d0.w),a0 ; Choose the instrument from the pointer index accordingly
First, the PSG voice needs to be grabbed from the ROM. THis is done using pointers. The Off_PSGInst_Ptr, is the main list of PSG Voice pointers. It's simply a list of word longs that point to the data of each voice. If you remember from the last bit of code, the voice number from $b(a5) was stored in D0. D0 needs to be decremented by 1 and then multiplied by 4 before we can used Pointer Offsets to set A0 to the beginning of the selected PSG Voice. Why? In computers, they don't start counting at 1, they start counting at 0. So when you're asking for Voice number one, to the computer, you're asking for voice number 0. Also, the voice number needs to be multiplied by 4 because we're dealing with Long Word pointers, which are four bytes in size. To get to each pointer in the Off_PSGInst_Ptr offset list, we need to go four bytes each time to the next one. Finally, whatever number voice we want is stored into A0.
move.b $C(a5),d0 ; Move the PSG Voice Index Pointer (VIP) into D0 move.b (a0,d0.w),d0 ; Insert that value (A0 + D0 (our current pointer to the ; location in the PSG Voice)) into D0 addq.b #1,$C(a5) ; Increment the Voice Pointer btst #7,d0 ; If D0 is >= $80 (the end of the voice data) beq.s _EvalVoice ; Jump if it's not cmpi.b #$80,d0 ; If it's equal to $80 beq.s _EndOfVoiceData ; Exit the function, and prevent the pointer from incrementing ; past the current voice data
$C(a5) is the current position in the voice. So, when we first start off, it's equal to 0. The next cycle, 1. The next, 2, etc... We copy the current position into D0, and then we take (A0(Our pointer to the PSG Voice)+D0(Our Current Position in the PSG Voice)) and copy what it contained there and put it in D0. Then, we increment the current location in the voice ($C(a5)) so that we handle the next byte next time. If the 7th bit of D0 is 1 (10000000), then the data is equal to $80, and that's the end of the PSG voice. If we've reached that, we jump down to _EndOFVoiceData, which simply decrements $C(a5) so that each time this is run, the computer will see we're at the end of the PSGVOice, and then keep us there everytime until a new note is played.
_EvalVoice: ; add.w d0,d6 ; Add the VoiceValueModifier to the Base Volume cmpi.b #$10,d6 ; Compare 10 to D6 bcs.s UpdatePSGVolume ; If it's >= 10, branch moveq #$F,d6 ; Set D6 to $0000000F (muting the current PSG Channel)
So, this is the very difficult code that makes it all happen. Okay, so it's not difficult at all. Not even confusing. What it does, is it adds the Voice Value to the Base Volume. And that is what creates the envelope. Wow, kids. Then, there's a comparison done. If D6 is greater than $10, we actually update the volume, which executes the voice and makes is happen. Now, why only if D6 is greater than $10? Well, if the PSG channel is not being used in a track, its base volume is equal to $00. Values in a PSG voice are not expected to be greater than $F, so if you add $F to $00, it's less than $10. Then, if it is infact less than 10, we set D6 to $F. You'll find out why next.
UpdatePSGVolume: ; loc_7297C: ; or.b 1(a5),d6 ; OR the Channel Modifier to the new Volume addi.b #$10,d6 ; Add $10 to D6 (Increases the Channel Value to Attenuation from Frequency) move.b d6,($C00011).l ; Write value to the PSG Port _ExitHandleVoice: ; Return rts
There are first a few checks of the channel's status byte that I've cut out because they aren't important. If any of them were true, the voice wouldn't be executed and we'd just exit the function. What we do is "OR" D6 with 1(a5) which is actually the channel modifier. The channel modifier is just a value that tells us what channel we're in. It's not as easy as channel 1, 2, or 3, however. Channel 1 = $80, Channel 2 = $A0, and Channel 3 = $C0. When we OR D6 with the channel modifier, we get a special Byte that the PSG can read and handle. For example, if in the last set of code, D6 was less than $10, the byte that we would have for channel 1 would be $8F. The left nybble being the channel number, the right nybble being the attenuation of the channel; $F being muted and $0 being the loudest.
Then, we add $10 to D6. That increases the channel numbers $80->$90, $A0->$B0, and $C0->$D0. Each channel has 2 seperate registers, the first one (ex $80) is for the frequency and the second on (ex $90) is for the volume, or attenuation. Once the byte is ready to be sent to the PSG, it is, and then cycle by cycle, as you play the game, the voice is all fancied up for the PSG.
This may not seem like much, but this is extrememly important to the whole music driver. If you want to know what I'm talking about set bytes $719CC-$719AC to $00. Without these envelopes, the PSG instruments wouldn't have decay rates, and the music would not only sound bland, but it sounds plain weird. Seriously, check it out for yourself. LZ is The best example. I urge you to listen to it, it's quite hilarious.
So, nothing breakthrough, but it's one of the many interesting things I wanted to share with you guys about the Sonic 1 music Driver. I understand almost all of it, but it's really the time that it takes explaining it like this that's the killer. I can hopefully answer any of your questions you guys have.