[Sonic 1 - Github] - Per-Act Music via Bit Math

Discussion in 'Tutorials' started by Inferno, Dec 29, 2020.

  1. Inferno

    Inferno Rom Hacker Member

    Joined:
    Oct 27, 2015
    Messages:
    104
    Location:
    Sky Base Zone, South Island
    As most of you know, there's already guides for per-act music, but... I personally think, after looking at how S3 does it, the guides do it in a rather "hacky" and unoptimized manner.

    Here's where I will describe the differences between this guide and the pre-existing guides:
    - The pre-existing guides use repeated checks to use multiple tables to choose music. This has various drawbacks, one of which being CPU processing time: the full thing, if you go up to Act 4, costs up to 116 cycles!

    - My current method, however, goes about this slightly differently. Instead of using multiple tables and a series of checks, my method just loads the whole zone and act ID and then performs some bit shifting to make it work with the pre-existing setup, whilst extending the MusicList table to have a entry per-act. This costs exactly 40 cycles every time, less than half of Nineko's method. I also avoid having to repeat this via a similar method to Nineko's second guide: saving the current music to a RAM address and then pulling from there when a boss or drowning or invincibility needs to restore the music.

    Now that the comparison has been made, how about I actually get into the guide?

    Step 1: Set-Up
    Your first step will be to find a free RAM address and use it for our "Saved_music" variable. This is what we will use to restore music properly without having to perform hardcoded nonsense elsewhere.

    I will refer to this variable as "Saved_music" for the rest of this guide.

    If you are having issues locating a free RAM address. use this:
    http://info.sonicretro.org/SCHG:Sonic_the_Hedgehog_(16-bit)/RAM_Editing


    Step 2: Level_GetBGM
    This is where the magic begins. Go to this routine, it should look like this:
    Code:
    Level_GetBgm:
    
            tst.w    (f_demo).w
            bmi.s    Level_SkipTtlCard
            moveq    #0,d0
            move.b    (v_zone).w,d0
            cmpi.w    #(id_LZ<<8)+3,(v_zone).w ; is level SBZ3?
            bne.s    Level_BgmNotLZ4    ; if not, branch
            moveq    #5,d0        ; use 5th music (SBZ)
    
        Level_BgmNotLZ4:
            cmpi.w    #(id_SBZ<<8)+2,(v_zone).w ; is level FZ?
            bne.s    Level_PlayBgm    ; if not, branch
            moveq    #6,d0        ; use 6th music (FZ)
    
        Level_PlayBgm:
            lea    (MusicList).l,a1 ; load    music playlist
            move.b    (a1,d0.w),d0
            bsr.w    PlaySound    ; play music
            move.b    #id_TitleCard,(v_objspace+$80).w ; load title card object
    
    There's a LOT of hard-coded nonsense here, but our main focus should be on the move.b instruction for v_zone.

    This is what moves the zone id to d0 to be used later when picking the correct entry.

    First of all, remove all that hardcoded nonsense between that instruction and the lea instruction for MusicList. We won't need it once we are done. It should now look like this:

    Code:
    Level_GetBgm:
            tst.w    (f_demo).w
            bmi.s    Level_SkipTtlCard
            moveq    #0,d0
            move.b    (v_zone).w,d0
            lea    (MusicList).l,a1 ; load    music playlist
            move.b    (a1,d0.w),d0
            bsr.w    PlaySound    ; play music
            move.b    #id_TitleCard,(v_objspace+$80).w ; load title card object
    
    Now, that's better! But it's still picking per-zone, so FZ and SBZ3 won't have the correct music!
    Now, we can begin making our main change.

    Change:
    Code:
            move.b    (v_zone).w,d0
    Into:
    Code:
            move.w    (v_zone).w,d0


    The way Sonic 1's RAM is arranged, the Act ID is right after the Zone ID. This means we just need to change this from moving a byte to moving a word.

    However, we can't just change this, now the calculation has become completely wrong!
    Now the current calculation is (Zone ID * 256) + Act ID!
    This will just lead to us pulling garbage data.

    This is where the bit math of the title comes in.

    Add this instruction after the recently changed line:
    Code:
            ror.b   #2,d0
    This instruction is called rotate right. What it basically does is shift bits to the right and if they move outside of the range of whatever we are shifting, they wrap around to the other side.
    For our purposes, it divides the 256 by 64, leaving 2, and moves the 64 to the end of the calculation, leaving us:
    ((Zone ID * 4) + Act ID) * 64

    We're close. With the 4 there, it'll properly account for S1's 4 acts.
    Now, we need to get rid of that 64. That's where lsr comes in,
    lsr stands for logical shift right. For our purposes, it divides everything by a power of 2 without accounting for if it's negative or postive.
    64 is 2 to the power of 6. If we divide by 64, we'd get just the calculation that we want, so, add this line after the ror:
    Code:
            lsr.w   #6,d0
    This divides the whole thing by 64, leaving us with this calculation:
    (Zone ID * 4) + Act ID


    Now, we are technically ready for per-act music, but we have a few things to do first.
    For now, there's one final thing to do.
    After this line:
    Code:
         move.b    (a1,d0.w),d0
    Add this line:
    Code:
          move.b    d0,(Saved_music).w
    This is our Saved_music variable. Now, we have stored the song ID chosen for the level elsewhere, before the music that plays can be overwritten!


    Step 3: Extending MusicList
    This step's probably the simpliest. Find MusicList. It should look like this:
    Code:
    MusicList:
    
            dc.b bgm_GHZ    ; GHZ
            dc.b bgm_LZ    ; LZ
            dc.b bgm_MZ    ; MZ
            dc.b bgm_SLZ    ; SLZ
            dc.b bgm_SYZ    ; SYZ
            dc.b bgm_SBZ    ; SBZ
            zonewarning MusicList,1
            dc.b bgm_FZ    ; Ending
            even
    ; ===========================================================================
    Replace it with this:
    Code:
    MusicList:
    
            dc.b bgm_GHZ    ; GHZ1
            dc.b bgm_GHZ    ; GHZ2
            dc.b bgm_GHZ    ; GHZ3
            dc.b bgm_GHZ    ; GHZ4
            dc.b bgm_LZ    ; LZ1
            dc.b bgm_LZ    ; LZ2
            dc.b bgm_LZ    ; LZ3
            dc.b bgm_SBZ    ; LZ4
            dc.b bgm_MZ    ; MZ1
            dc.b bgm_MZ    ; MZ2
            dc.b bgm_MZ    ; MZ3
            dc.b bgm_MZ    ; MZ4
            dc.b bgm_SLZ    ; SLZ1
            dc.b bgm_SLZ    ; SLZ2
            dc.b bgm_SLZ    ; SLZ3
            dc.b bgm_SLZ    ; SLZ4
            dc.b bgm_SYZ    ; SYZ1
            dc.b bgm_SYZ    ; SYZ2
            dc.b bgm_SYZ    ; SYZ3
            dc.b bgm_SYZ    ; SYZ4
            dc.b bgm_SBZ    ; SBZ1
            dc.b bgm_SBZ    ; SBZ2
            dc.b bgm_FZ    ; SBZ3
            dc.b bgm_SBZ    ; SBZ4
            dc.b bgm_GHZ    ; GHZ1
            dc.b bgm_GHZ    ; GHZ1
            dc.b bgm_GHZ    ; GHZ1
            dc.b bgm_GHZ    ; GHZ1
            even
    ; ===========================================================================
    This makes it so that the music playlist works properly for vanilla S1.
    SBZ3 is LZ4, so LZ4 has SBZ's music appointed to it, and SBZ3 is FZ, so it has FZ's music appointed to it.


    Step 4: Using Saved_music
    Now we get to save time by just use Saved_music instead of doing hardcoded nonsense!

    Go to ResumeMusic, it should look like this:

    Code:
    ResumeMusic:
            cmpi.w    #12,(v_air).w    ; more than 12 seconds of air left?
            bhi.s    @over12        ; if yes, branch
            move.w    #bgm_LZ,d0    ; play LZ music
            cmpi.w    #(id_LZ<<8)+3,(v_zone).w ; check if level is 0103 (SBZ3)
            bne.s    @notsbz
            move.w    #bgm_SBZ,d0    ; play SBZ music
    
        @notsbz:
            if Revision=0
            else
                tst.b    (v_invinc).w ; is Sonic invincible?
                beq.s    @notinvinc ; if not, branch
                move.w    #bgm_Invincible,d0
        @notinvinc:
                tst.b    (f_lockscreen).w ; is Sonic at a boss?
                beq.s    @playselected ; if not, branch
                move.w    #bgm_Boss,d0
        @playselected:
            endc
    
            jsr    (PlaySound).l
    
        @over12:
            move.w    #30,(v_air).w    ; reset air to 30 seconds
            clr.b    (v_objspace+$340+$32).w
            rts
    ; End of function ResumeMusic
    That LZ and SBZ hardcoded stuff is about to go bye-bye. Replace everything from after the branch to @over12 to right after the Rev0 check in @notsbz with this single line:
    Code:
          move.b    Saved_music,d0
    Tada, hardcoded nonsense goes poof. Make sure to remove the endc as well.

    Now, go to Sonic_Display, and find the local label @chkinvincible.

    You'll see this bit of code:

    Code:
            moveq    #0,d0
            move.b    (v_zone).w,d0
            cmpi.w    #(id_LZ<<8)+3,(v_zone).w ; check if level is SBZ3
            bne.s    @music
            moveq    #5,d0        ; play SBZ music
    
        @music:
            lea    (MusicList2).l,a1
            move.b    (a1,d0.w),d0
            jsr    (PlaySound).l    ; play normal music
    Wait... this looks familiar!

    This is indeed the Level_GetBgm calculations all over again, with a different music list for some reason.
    Replace all that with this:
    Code:
            move.b    Saved_music,d0    ; loads song number from RAM
            jsr    (PlaySound).l    ; play normal music
    That's a lot better!

    Finally, let's deal with the mess that is the bosses.

    In "_incObj/3D Boss - Green Hill (part 2).asm", find loc_179E0. It should look like this:
    Code:
    loc_179E0:
            clr.w    obVelY(a0)
            music    bgm_GHZ,0,0,0        ; play GHZ music
    Replace it with this:
    Code:
    loc_179E0:
            clr.w    obVelY(a0)
            tst.b     (v_invinc).w
            bne.s   @boss_invinc
    
            move.b   Saved_music,d0
            bra.w      @boss_play
    
    @boss_invinc:
            move.b #bgm_Invincible,d0
    
    @boss_play:
            jsr PlaySound
    This code forms the template for the rest of the code.

    Now, go to loc_18112, which is in "_incObj/77 Boss - Labyrinth", it should look like this:
    Code:
    loc_18112:
            music    bgm_LZ,0,0,0        ; play LZ music
            if Revision=0
            else
                clr.b    (f_lockscreen).w
            endc
            bset    #0,obStatus(a0)
            addq.b    #2,ob2ndRout(a0)
    Replace it with this:
    Code:
    loc_18112:
            tst.b     (v_invinc).w
            bne.s   @boss_invinc
    
            move.b   Saved_music,d0
            bra.w      @boss_play
    
    @boss_invinc:
            move.b #bgm_Invincible,d0
    
    @boss_play:
            jsr PlaySound
            clr.b    (f_lockscreen).w
            bset    #0,obStatus(a0)
            addq.b    #2,ob2ndRout(a0)
    Now, go to loc_1856C, which is in "_incObj/73 Boss - Marble", this is how it should look:
    Code:
    loc_1856C:
            clr.w    obVelY(a0)
            music    bgm_MZ,0,0,0        ; play MZ music
    Replace it with this:
    Code:
    loc_1856C:
            clr.w    obVelY(a0)
            tst.b     (v_invinc).w
            bne.s   @boss_invinc
    
            move.b   Saved_music,d0
            bra.w      @boss_play
    
    @boss_invinc:
            move.b #bgm_Invincible,d0
    
    @boss_play:
            jsr PlaySound
    Now, go to loc_18BB4, which is in "_incObj/7A Boss - Star Light", this is how it should look:
    Code:
    loc_18BB4:
            clr.w    obVelY(a0)
            music    bgm_SLZ,0,0,0        ; play SLZ music
    
    Replace it with this:
    Code:
    loc_18BB4:
            clr.w    obVelY(a0)
            tst.b     (v_invinc).w
            bne.s   @boss_invinc
    
            move.b   Saved_music,d0
            bra.w      @boss_play
    
    @boss_invinc:
            move.b #bgm_Invincible,d0
    
    @boss_play:
            jsr PlaySound
    Now, go to loc_194E0, which is in "_incObj/75 Boss - Spring Yard", this is how it should look:
    Code:
    loc_194E0:
            clr.w    obVelY(a0)
            music    bgm_SYZ,0,0,0        ; play SYZ music
    Replace it with this:
    Code:
    loc_194E0:
            clr.w    obVelY(a0)
            tst.b     (v_invinc).w
            bne.s   @boss_invinc
    
            move.b   Saved_music,d0
            bra.w      @boss_play
    
    @boss_invinc:
            move.b #bgm_Invincible,d0
    
    @boss_play:
            jsr PlaySound
    And that should be all for the bosses!

    Conclusion
    Now you should have fully-functioning per-act music!

    If you have any issues, let me know.
     
    Last edited: Dec 30, 2020
    Darkex, Techokami, TomTalker and 8 others like this.
  2. ProjectFM

    ProjectFM Optimistic and self-dependent Member

    Joined:
    Oct 4, 2014
    Messages:
    900
    Location:
    Orono, Maine
    If you are going for efficiency, there are more efficient ways of calculating "(Zone ID * 4) + Act ID". Bit shifting instructions tend to be costly and that cost scales with the number of shifts.

    Using AURORA☆FIELDS' cycle calculator, we can calculate how many cycles it takes to get this result.
    Code:
        move.w    (v_zone).w,d0    ; 12(3/0)
        ror.b    #2,d0        ; 6(1/0) + 4(0/0)
        lsr.w    #6,d0        ; 6(1/0) + 12(0/0)
                    ; total 40
    This result can be recreated without using bit shifting instructions and without requiring any extra registers.
    Code:
        moveq    #0,d0        ; 4(1/0)
        move.b    (v_zone).w,d0    ; 12(3/0)
        add.b    d0,d0        ; 4(1/0)
        add.b    d0,d0        ; 4(1/0)
        add.b    (v_act).w,d0        ; 12(3/0)
                    ;  total 36
    I also tried moving v_zone to an address register because it's faster to receive a value from a register than from RAM, but the result is also 36 cycles. If you want to go further, you can do this calculation before the level starts and save that to a variable so that the result doesn't need to be calculated every time it's used, but it would be unnecessary because this code is so rarely used mid-level.
     
  3. Inferno

    Inferno Rom Hacker Member

    Joined:
    Oct 27, 2015
    Messages:
    104
    Location:
    Sky Base Zone, South Island
    Not updating the guide, but ProjectFM's note is handy, so thanks!

    At least it's only a 4 cycle difference.
     
    TomTalker and ProjectFM like this.
  4. nineko

    nineko I am the Holy Cat Member

    Joined:
    Mar 24, 2008
    Messages:
    1,872
    Location:
    italy
    Nice work, I wrote that guide when I knew next to nothing about 68k ASM, so it's good to finally have an alternative :)

    I'm still glad it's been useful for more than a decade, though, that's the best part of hacking, nothing is ever wasted, just like Sonic QX before xm2smps, and xm3smps before mid2smps, everything's been a stepping stone while striving to reach the perfection :)
     
  5. TomTalker

    TomTalker Newcomer Prospect

    Joined:
    Feb 20, 2021
    Messages:
    10
    Location:
    Emerald Hill
    How do I actually set it up for different music to play each act? ;P
     
    Last edited: Apr 10, 2021
  6. Inferno

    Inferno Rom Hacker Member

    Joined:
    Oct 27, 2015
    Messages:
    104
    Location:
    Sky Base Zone, South Island
    You have to actually add music for it to play. Once you add it, you just modify the MusicList table to have a different entry for that act.
     
    TomTalker and Unnamed like this.
  7. Ashuro

    Ashuro Anti-Cosmic Metal Of Death Member

    Joined:
    Sep 27, 2014
    Messages:
    543
    Location:
    France
    Nevermind, fixed.
     
    Last edited: Jun 12, 2021