don't click here

PSG Instruments Explained (68K Alert)

Discussion in 'Engineering & Reverse Engineering' started by fuzzbuzz, Jun 12, 2006.

  1. fuzzbuzz


    Tech Member
    Many Many Programming/hacking tools...
    $719A4 is the master pointer list for what is in the Nemesis Disassembly as the PSG Pointer Index. Following that are the actual PSG Instruments (I refer to them as Voices):

    Code (Text):
    1.  PSG_Inst_Index: dc.l PSG1, PSG2, PSG3, PSG4, PSG5, PSG6, PSG7, PSG8, PSG9
    2.  PSG1:           dc.b $00,$00,$00,$01,$01,$01,$02,$02,$02,$03,$03,$03,$04,$04,$04,$05; 0
    3.                  dc.b $05,$05,$06,$06,$06,$07,$80; 16
    4.  PSG2:           dc.b $00,$02,$04,$06,$08,$10,$80; 0
    5.  PSG3:           dc.b $00,$00,$01,$01,$02,$02,$03,$03,$04,$04,$05,$05,$06,$06,$07,$07; 0
    6.                  dc.b $80                   ; 16
    7.  PSG4:           dc.b $00,$00,$02,$03,$04,$04,$05,$05,$05,$06,$80; 0
    8.  PSG6:           dc.b $03,$03,$03,$02,$02,$02,$02,$01,$01,$01,$00,$00,$00,$00,$80; 0
    9.  PSG5:           dc.b $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$01,$01,$01,$01,$01,$01; 0
    10.                  dc.b $01,$01,$01,$01,$01,$01,$01,$01,$02,$02,$02,$02,$02,$02,$02,$02; 16
    11.                  dc.b $03,$03,$03,$03,$03,$03,$03,$03,$04,$80; 32
    12.  PSG7:           dc.b $00,$00,$00,$00,$00,$00,$01,$01,$01,$01,$01,$02,$02,$02,$02,$02; 0
    13.                  dc.b $03,$03,$03,$04,$04,$04,$05,$05,$05,$06,$07,$80; 16
    14.  PSG8:           dc.b $00,$00,$00,$00,$00,$01,$01,$01,$01,$01,$02,$02,$02,$02,$02,$02; 0
    15.                  dc.b $03,$03,$03,$03,$03,$04,$04,$04,$04,$04,$05,$05,$05,$05,$05,$06; 16
    16.                  dc.b $06,$06,$06,$06,$07,$07,$07,$80; 32
    17.  PSG9:           dc.b $00,$01,$02,$03,$04,$05,$06,$07,$08,$09,$0A,$0B,$0C,$0D,$0E,$0F; 0
    18.                  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.
    Code (Text):
    1.  HandlePSGVoice:
    2.                  tst.b   $B(a5)             ; Test the voice number
    3.                  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.
    Code (Text):
    1.  _LoadVoiceRt:                              ;
    2.                  move.b  9(a5),d6           ; Copy the Base Volume into D6
    3.                  moveq   #0,d0              ; Clear D0
    4.                  move.b  $B(a5),d0          ; Move the current voice setting into D0
    5.                  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.
    Code (Text):
    1.                  movea.l (Off_PSGInst_Ptr).l,a0; Point A0 to the PSG Instrument pointer index
    2.                  subq.w  #1,d0              ; Subtract from the voice value
    3.                  lsl.w   #2,d0              ; Multiply it by 4 (Long Word pointers)
    4.                  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.
    Code (Text):
    1.                  move.b  $C(a5),d0          ; Move the PSG Voice Index Pointer (VIP) into D0
    2.                  move.b  (a0,d0.w),d0       ; Insert that value (A0 + D0 (our current pointer to the
    3.                                             ;   location in the PSG Voice)) into D0
    4.                  addq.b  #1,$C(a5)          ; Increment the Voice Pointer
    5.                  btst    #7,d0              ; If D0 is >= $80 (the end of the voice data)
    6.                  beq.s   _EvalVoice         ; Jump if it's not
    7.                  cmpi.b  #$80,d0            ; If it's equal to $80
    8.                  beq.s   _EndOfVoiceData    ; Exit the function, and prevent the pointer from incrementing
    9.                                             ;      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.
    Code (Text):
    1.  _EvalVoice:                                ;
    2.                  add.w   d0,d6              ; Add the VoiceValueModifier to the Base Volume
    3.                  cmpi.b  #$10,d6            ; Compare 10 to D6
    4.                  bcs.s   UpdatePSGVolume    ; If it's >= 10, branch
    5.                  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.
    Code (Text):
    1.  UpdatePSGVolume:                           ;
    2.  loc_7297C:                                 ;
    3.                  or.b    1(a5),d6           ; OR the Channel Modifier to the new Volume
    4.                  addi.b  #$10,d6            ; Add $10 to D6 (Increases the Channel Value to Attenuation from Frequency)
    5.                  move.b  d6,($C00011).l     ; Write value to the PSG Port
    7.  _ExitHandleVoice:                          ; Return
    8.                  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.
  2. Tweaker


    Awesome, so lemme just clarify this:
    Code (Text):
    1. PSG1:           dc.b $00,$00,$00,$01,$01,$01,$02,$02,$02,$03,$03,$03,$04,$04,$04,$05; 0
    2.                 dc.b $05,$05,$06,$06,$06,$07,$80; 16
    The standard PSG voice is just a straight, flat square wave (F500). So this pretty much just ups the output of the PSG until it reaches its peak, then sustains at the $80 as long as the note is held?

    If that's how it works, and is what I'm getting from both the format and your explanation (which I must admit, I read quickly), then I could give making PSG voices a try, and that's fucking awesome.

    Hmm... I wonder if a string-like voice is possible? Time to experiment. :o
  3. fuzzbuzz


    Tech Member
    Many Many Programming/hacking tools...
    Well, unfortunately it's just a plain sine (actually, I think it's a square) wave, and it's just a volume envelope. So there's really not much you can to there but dynamics.

    However, if you really wanted to try something awesome, you could devise a way to use all of the tone generators and link the noise generator to tone generator number 3 to make one kick ass PSG voice. That's prolly the best you can do at this point.
  4. Sonic 65

    Sonic 65

    Tech Member
    Just for the heck of it, I found the PSG pointer index in Ristar. There seem to be two of them.

    PSG Pointer Index 1 - C8048
    PSG Pointer Index 2 - C8114
  5. Tweaker



    I'm actually looking for the ones in S3K -- That way I can port over hi-hats the right way, instead of the half assed way I've become accustomed to. =P

    Two of them, though? What's different about the second one? Is it referenced at all?