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.
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.
Not updating the guide, but ProjectFM's note is handy, so thanks! At least it's only a 4 cycle difference.
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
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.
Hiya. Okay this seems like a small issue, but I can't get my head over this. I have done as mentioned above and I end up with this in error log. Now I know for a fact it says that both of them aren't defined. But I just can't find the right asm where it is located. I only have this issue
I'm not quite sure what you're asking, but it looks like the error is pointing to the file "sonic.asm" (in the root folder in your disassembly), at lines 7165 and 7168. Looking at the symbols, it looks like those are under ResumeMusic, I think? Either way, if you use something like Notepad+, you can simply view the line count along the left side of the document. What is likely happening, is that the temporary '@' labels are now too far from whatever instruction is branching to them, so all you'll need to do is rename them to something unique, and not include the @ symbol.
Oh. Maybe that must be the problem I am having. I will make sure to see that properly and maybe update it in this exact post if I got it working correctly with no issues.
More accurately: you can't have a non-temporary label between temporary labels: it won't find it otherwise.