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

Inferno

Blazing Creator
Member
Messages
19
Location
Sky Base Zone, South Island
Brought over from SSRG, mostly because it's a useful guide. There's an added note towards the bottom!

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
Finally, 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.

Notes:
The following is a post @ProjectFM made in response to the original tutorial on SSRG, which shows another way of doing things.

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.
 
Back
Top