- 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:
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:
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:
Into:
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:
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:
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:
Add this line:
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:
Replace it with this:
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:
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:
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:
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:
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:
Replace it with this:
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:
Replace it with this:
Now, go to loc_1856C, which is in "_incObj/73 Boss - Marble", this is how it should look:
Replace it with this:
Now, go to loc_18BB4, which is in "_incObj/7A Boss - Star Light", this is how it should look:
Replace it with this:
Finally, go to loc_194E0, which is in "_incObj/75 Boss - Spring Yard", this is how it should look:
Replace it with this:
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.
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
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, we can begin making our main change.
Change:
Code:
move.b (v_zone).w,d0
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
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
(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
Code:
move.b d0,(Saved_music).w
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
; ===========================================================================
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
Code:
move.b Saved_music,d0
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
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
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
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)
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)
Code:
loc_1856C:
clr.w obVelY(a0)
music bgm_MZ,0,0,0 ; play MZ music
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
Code:
loc_18BB4:
clr.w obVelY(a0)
music bgm_SLZ,0,0,0 ; play SLZ music
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
Code:
loc_194E0:
clr.w obVelY(a0)
music bgm_SYZ,0,0,0 ; play SYZ music
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
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.
This result can be recreated without using bit shifting instructions and without requiring any extra registers.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
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.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