SMPS fixes and expansions

Discussion in 'Discussion and Q&A Archive' started by Clownacy, Nov 26, 2014.

Thread Status:
Not open for further replies.
  1. Clownacy

    Clownacy Retired Staff lolololo Member

    Joined:
    Aug 15, 2014
    Messages:
    1,012
    SMPS is an interesting beast, and it seems, no matter which variant your hack uses, there's always room for improvement. Collected here are several of my fixes and expansions that altogether cover the three forms of SMPS used in the classic Sonic trilogy. This topic is here to encourage the sharing of fixes and expansions that others have made.

    Note that all of my code targets the Macro AS Assembler.

    Added FM6 support for Extra Life system (Sonic 1's driver)

    When playing through S1's special stages, you may happen upon 100 rings and the resultant extra life. After the jingle stops playing, however, you'll find that something was lost: the special stage's theme loses a track, FM6 to be precise. But why?

    As mentioned before, FM6 and the DAC share a channel, so you can only have one playing at a time. The is done through the changing of some variables. Now, when the extra life jingle kicks in, the DAC is set to play, and it plays. Following that, track restoration takes place, allowing the interrupted song to continue. The problem is that FM6 isn't re-enabled. Because of this, the DAC will have priority of the channel, but nothing plays on the DAC track, so the song loses a channel overall.

    What we need to do is find the code that restores the track data, and then make it re-enable FM6 if needed. To do that, go to cfFadeInToPrevious, and scroll down, until just above cfSetTempoDivider. There, you will see this:

    movea.l a3,a5

    Directly below it, insert this:


    tst.b v_dac_playback_control(a6) ; is the DAC channel running?
    bmi.s .DAC ; if not, branch

    moveq #$2B,d0 ; DAC enable/disable register
    moveq #0,d1 ; Disable DAC
    jsr WriteFMI(pc)
    .DAC:

    Now FM6 will be restored when needed.

    Improved 68k Sega sound playback code (Sonic 1's driver)

    So, who remembers

    this thing? Looking at it, it needed all the cycles it could get, but not all chances were taken, so the code is pretty unoptimised because of it.

    Here's the old version with a cycle count added:

    PlaySega:
    lea (SegaPCM).l,a2 ; 12(3/0)
    move.l #(SegaPCM_End-SegaPCM),d3 ; 12(3/0)
    move.b #$2A,(ym2612_a0).l ; 20(4/1)
    PlayPCM_Loop:
    move.b (a2)+,(ym2612_d0).l ; 20(4/1)
    move.w #$14,d0 ; 8(2/0)
    dbf d0,* ; 10(2/0)
    subi.l #1,d3 ; 16(3/0)
    beq.s return_PlayPCM ; 10(2/0) (taken), 8(1/0) (not taken)
    lea (v_jpadhold1).w,a0 ; 8(2/0)
    lea ($A10003).l,a1 ; 12(3/0)
    jsr (Joypad_Read).w ; 18 (2/2)
    btst #7,(v_jpadhold1).w ; 16(4/0)
    bne.s return_PlayPCM ; 10(2/0) (taken), 8(1/0) (not taken)
    bra.s PlayPCM_Loop ; 10(2/0)
    return_PlayPCM:
    addq.w #4,sp
    rts

    And here's an optimised version I cooked up.


    ; Macro to wait for when the YM2612 isn't busy. Requires a0 = ym2612_a0
    waitYM macro
    nop ; 4(1/0) ; Gotta give the YM2612 some time to read
    nop ; 4(1/0)
    ; If you're gonna overclock your 68k, you may need to pad this out with more 'nop's to avoid missed writes
    loop:
    tst.b (a0) ; 8(2/0)
    bmi.s loop ; 10(2/0) | 8(1/0)
    endm

    PlaySega:
    lea (ym2612_a0).l,a0 ; 12(3/0)
    lea (SegaPCM).l,a2 ; 12(3/0)
    lea (ym2612_d0).l,a3 ; 12(3/0)
    lea (v_jpadhold1).w,a4 ; 8(2/0)
    lea ($A10003).l,a5 ; 12(3/0)
    move.w #(SegaPCM_End-SegaPCM)-1,d3 ; 8(2/0)
    waitYM
    move.b #$2A,(a0) ; 12(2/1)
    waitYM
    PlayPCM_Loop
    move.b (a2)+,(a3) ; 12(2/1)
    moveq #$18,d0 ; 4(1/0)
    dbf d0,* ; 10(2/0) (loop), 14(3/0) (not looped)
    movea.w a4,a0 ; 4(1/0)
    movea.l a5,a1 ; 4(1/0)
    jsr (Joypad_Read).w ; 18 (2/2)
    tst.b (a4) ; 8(2/0)
    bmi.s return_PlayPCM ; 10(2/0) (taken), 8(1/0) (not taken)
    dbf d3,PlayPCM_Loop ; 10(2/0) (loop), 14(3/0) (not looped)
    return_PlayPCM:
    addq.w #4,sp
    rts

    The improved cycle count allows for greater playback speed, but be aware, too high a playback speed may lead to missed writes.

    Smaller FM frequencies table (Sonic 1's driver)

    Many other SMPS 68k drivers have a smaller

    FM_Notes table than what's in Sonic 1. This is achieved through additions to FMSetFreq. You can port this by doing the following:

    First, let's study an SMPS 68k Type 2 driver's FMSetFreq. Here's Super Shinobi II's (ValleyBell disasm):

    GetFMFreq: ; CODE XREF: ProcessFMTrack+20p

    ; FUNCTION CHUNK AT 00069996 SIZE 00000008 BYTES

    subi.b #$80,d5
    beq.s loc_69996
    add.b 8(a5),d5
    andi.l #$7F,d5
    divu.w #$C,d5 ; new instruction
    swap d5 ; new instruction
    lsl.w #1,d5
    lea FMFreqs,a0
    move.w (a0,d5.w),d6
    swap d5 ; new instruction
    andi.w #7,d5 ; new instruction
    moveq #$B,d0 ; new instruction
    lsl.w d0,d5 ; new instruction
    or.w d5,d6 ; new instruction
    move.w d6,$10(a5)
    rts
    ; End of function GetFMFreq

    As you can see, there are several new instructions. These use FM_Notes/FMFreqs, not just as raw data, as Sonic 1 does, but as base data, to which the input note is added to, calculating the appropriate octave. Because of this, data for each and every octave is no longer needed, allowing you to get away with a much smaller table, which covers only the one.

    Let's see about fitting this in Sonic 1's driver (modified early(?) SMPS 68k Type 1b).


    FMSetFreq:
    subi.b #$80,d5 ; Make it a zero-based index
    beq.s TrackSetRest
    add.b zTrackKeyOffset(a5),d5 ; Add track key displacement
    andi.w #$7F,d5 ; Clear high byte and sign bit
    ; first set of new instructions goes here
    lsl.w #1,d5
    lea FM_Notes(pc),a0
    move.w (a0,d5.w),d6
    ; second set of new instructions goes here
    move.w d6,zTrackFreq(a5) ; Store new frequency
    rts
    ; End of function FMSetFreq

    Self explanatory. Next up is replacing Sonic 1's FM_Notes with Super Shinobi II's FMFreqs:

    Find FM_Notes and delete all but the first dc.w line. That's all.

    Note that this can be optimised a little, not the additions, but the stock code. For starters, the 'lsl.w #1,d5' can be replaced with 'add d5,d5', which is faster. Also, if you move FM_Notes closer to FMSetFreq, you can replace 'lea FM_Notes(pc),a0' and the line after it with 'move.w FM_Notes(pc,d5.w),d6' (in fact, if you didn't add the new code, you could make this line write straight to zTrackFreq(a5) instead of using d6 to do it).

    Correctly handling S3+'s incorrect coord. flag usage (Sonic 1 & 2's driver)

    Anyone who's tried to port VVZ act 1's music to S1 or S2's driver will be met with an unusual bug: some channels lag behind. The cause is that some coord. flags are used incorrectly. Namely two FM-only flags on a PSG channel. The reason why S3K's driver doesn't suffer from any side-effects is because it has some additional code to deal with this. We'll be porting this code over to both of these drivers, creating a workaround for this error. You could correct the flags in the music itself, but we're adding this for 'catch all's sake.

    First, let's examine cfSetPSGTone...

    cfSetPSGTone

    S1 (modified early(?) SMPS 68k Type 1b):
    ; loc_72E26:
    cfSetPSGTone:
    move.b (a4)+,zTrackVoiceIndex(a5) ; Set current PSG tone
    rts

    S2 (modified Z80 port of S1's driver):

    ;zloc_F8E
    cfSetPSGTone:
    ld (ix+8),a ; Set current PSG tone
    ret

    S3K (modified SMPS Z80 Type 2):

    ;loc_E58
    cfSetPSGTone:
    bit 7, (ix+zTrackVoiceControl) ; Is this a PSG track?
    ret z ; Return if not

    ;loc_E5D
    cfStoreNewVoice:
    ld (ix+zTrackVoiceIndex), a ; Store voice
    ret


    While S1 and S2 are largely the same, S3K has a check for what channel is using the flag. This needs porting.

    S1:

    ; loc_72E26:
    cfSetPSGTone:
    tst.b zTrackVoiceControl(a5) ; Is this a PSG track?
    bpl.s + ; Return if not
    move.b (a4)+,zTrackVoiceIndex(a5) ; Set current PSG tone
    + rts

    S2:

    ;zloc_F8E
    cfSetPSGTone:
    bit 7, (ix+1) ; Is this a PSG track?
    ret z ; Return if not
    ld (ix+8),a ; Set current PSG tone
    ret


    That's that done. But that's only one of them: we still need to add to the FM flag, cfSetVoice.

    cfSetVoice

    Again, here's a comparison of the three...

    S1:

    ; loc_72C26:
    cfSetVoice:
    moveq #0,d0
    move.b (a4)+,d0 ; Get new voice
    move.b d0,zTrackVoiceIndex(a5) ; Store it
    btst #2,zTrackPlaybackControl(a5) ; Is SFX overriding this track?
    bne.w locret_72CAA ; Return if yes
    movea.l v_voice_ptr(a6),a1 ; Music voice pointer
    tst.b f_voice_selector(a6) ; Are we updating a music track?
    beq.s SetVoice ; If yes, branch
    movea.l zTrackVoicePtr(a5),a1 ; SFX track voice pointer
    tst.b f_voice_selector(a6) ; Are we updating a SFX track?
    bmi.s SetVoice ; If yes, branch
    movea.l v_special_voice_ptr(a6),a1 ; Special SFX voice pointer

    S2:

    ;zloc_E03
    cfSetVoice:
    ld (ix+8),a ; Set current voice
    ld c,a ; a -> c (saving for later, if we go to cfSetVoiceCont)
    bit 2,(ix+0) ; If "SFX is overriding this track" bit set...
    ret nz ; .. return!
    push hl ; Save 'hl'
    call cfSetVoiceCont ; Set the new voice!
    pop hl ; Restore 'hl'
    ret

    S3K:

    ;loc_D2E
    cfSetVoice:
    bit 7, (ix+zTrackVoiceControl) ; Is this a PSG track?
    jr nz, zSetVoicePSG ; Branch if yes
    call zSetMaxRelRate ; Set minimum D1L/RR for channel
    ld a, (de) ; Get voice index
    ld (ix+zTrackVoiceIndex), a ; Store to track RAM
    or a ; Is it negative?
    jp p, zSetVoiceUpload ; Branch if not
    inc de ; Advance pointer
    ld a, (de) ; Get song ID whose bank is desired
    ld (ix+zTrackVoiceSongID), a ; Store to track RAM and fall-through


    Once again, S3K has a check (and even a new subroutine, but we don't need that). Let's port this.

    S1:

    ; loc_72C26:
    cfSetVoice:
    moveq #0,d0
    move.b (a4)+,d0 ; Get new voice
    move.b d0,zTrackVoiceIndex(a5) ; Store it
    tst.b zTrackVoiceControl(a5) ; Is this a PSG track?
    bmi.s locret_72CAA ; Return if yes
    btst #2,zTrackPlaybackControl(a5) ; Is SFX overriding this track?
    bne.w locret_72CAA ; Return if yes
    movea.l v_voice_ptr(a6),a1 ; Music voice pointer
    tst.b f_voice_selector(a6) ; Are we updating a music track?
    beq.s SetVoice ; If yes, branch
    movea.l zTrackVoicePtr(a5),a1 ; SFX track voice pointer
    tst.b f_voice_selector(a6) ; Are we updating a SFX track?
    bmi.s SetVoice ; If yes, branch
    movea.l v_special_voice_ptr(a6),a1 ; Special SFX voice pointer

    S2:

    ;zloc_E03
    cfSetVoice:
    ld (ix+8),a ; Set current voice
    bit 7,(ix+1) ; Is this a PSG track?
    ret nz ; Return if yes
    ld c,a ; a -> c (saving for later, if we go to cfSetVoiceCont)
    bit 2,(ix+0) ; If "SFX is overriding this track" bit set...
    ret nz ; .. return!
    push hl ; Save 'hl'
    call cfSetVoiceCont ; Set the new voice!
    pop hl ; Restore 'hl'
    ret

    Done. Wouldn't be surprised if this fixes other songs.

    Smaller FM frequencies table (Sonic 2's driver)

    Because of the nature of SMPS Z80 drivers, you may prioritise size over speed. If so, you may want to use a much smaller FM frequencies table, as seen in S3K's driver (modified SMPS Z80 Type 2). Despite being cousins in terms of relation (S2's driver is a Z80 port of an evolved SMPS 68k), S3K's code works near-perfectly in S2's driver, so we'll see about porting across this feature, using the very code S3K uses.

    First, you need to follow the driver-related instructions here to relocate the frequency tables, otherwise the DAC playback will be broken. Note that adding the S3K frequencies is optional, and not required for this.

    zFMSetFreq, in s2.sounddriver.asm. Above 'add a,a', add this code:

    ld d,8 ; Each octave above the first adds this to frequency high bits
    ld e,0Ch ; 12 notes per octave
    ex af,af' ; Exchange af with af'
    push af
    xor a ; Clear a (which will clear a')

    - ex af,af' ; Exchange af with af'
    sub e ; Subtract 1 octave from the note
    jr c,+ ; If this is less than zero, we are done
    ex af,af' ; Exchange af with af'
    add a,d ; One octave up
    jr - ; Loop

    +
    add a,e ; Add 1 octave back (so note index is positive)

    (You can find the unmodified version of this code in S3K's driver under zGetNextNote_cont)

    This is responsible for the bulk of the calculation. In S2's driver, af' is used by the DAC playback, so we'll need to preserve its contents, as done by the 'push af'.

    After 'ld de,(zFrequencies)', add in this code:


    ex af,af' ; Exchange af with af'
    or d ; a = high bits of frequency (including octave bits, which were in a)
    ld d,a ; h = high bits of frequency (including octave bits)
    pop af
    ex af,af' ; Exchange af with af'

    This code has seen significant modification from its source: using the correct register for the pointer, and restoring af'.

    Now navigate to zFrequencies, and remove all but the first line of values.

    You will need to follow the next guide to fix a bug inherited from the new code.

    Correctly handling unusual note pitches (Sonic 3 & Knuckles' driver)

    If you happen to have S3K's driver in your S1 or S2 hack, you'll likely have S2's spin dash release (or S1's SBZ teleporter) sound in your hack as well. However, in doing so, you'll trigger a strange bug: the spin dash release/teleport sound will play incorrectly.

    The cause is that, in the sound's track headers, FM5 will have a pitch of $90, which is unusual, as most other SFXs use values much closer to $00. It is possible that this value is a mistake. In fact, S3K's version of the sound has a pitch of $00 instead, avoiding the bug.

    $90 is $10 with the sign bit set. $10 being much closer to $00, and even sounds exactly the same if used instead of $90. In fact, in S2's unmodified zFMSetFreq, the sign bit would be lost in the calculations. Most obviously at 'add a,a', where the sign bit would be lost to overflow. Our problem is that the code in S3K's driver (which you would have ported in the above guide) doesn't have a chance to remove the sign bit, so it counts the octaves of the track incorrectly, leading to the sound playing incorrectly. While we could change the pitch value of the sound itself, a safer, more effective solution would be to modify S3K's code to remove the sign bit:

    Under zGetNextNote_cont (zFMSetFreq, if you're here for the S2 port), above 'ld d, 8', add the instruction 'res 7,a' (this is similar to what Sonic 1's driver does). This will remove the troublesome bit before the calculations are made. Providing a sound workaround for these strange values.
     
    Last edited by a moderator: Nov 26, 2014
  2. nineko

    nineko I am the Holy Cat Member

    Joined:
    Mar 24, 2008
    Messages:
    1,901
    Location:
    italy
    Ah, I would have loved to have the fix for the FM6/DAC thing when I worked on my hack back in the day. The SM64 Slide music sounds so bad without its FM6 :(
     
  3. ThomasThePencil

    ThomasThePencil resident psycho Member

    Joined:
    Jan 29, 2013
    Messages:
    910
    Location:
    the united states. where else?
    If I may ask, is that last one also the reason that Sonic 2's spindash rev sound, when used, plays a weird sine wave shortly after ending? Or is that in need of a different fix?

    (This is most noticeable in my hack if you rev up the spindash in FDZ1 and let it remain charged up for a bit.)
     
    Last edited by a moderator: Nov 27, 2014
  4. Clownacy

    Clownacy Retired Staff lolololo Member

    Joined:
    Aug 15, 2014
    Messages:
    1,012
    No clue. Never heard of it.
     
  5. Irixion

    Irixion Well-Known Member Member

    Joined:
    Aug 11, 2007
    Messages:
    670
    Location:
    Ontario, Canada
    You probably missed a step porting the driver, or mucked something up. I had that issue when I was porting the vanilla clone driver into Sonic 1.
     
Thread Status:
Not open for further replies.