don't click here

Utility S1-friendly improved Sonic 2 sound driver

Discussion in 'Engineering & Reverse Engineering' started by Clownacy, Apr 19, 2015.

  1. Clownacy

    Clownacy

    Tech Member
    1,061
    607
    93
    Have you ever wanted to replace your Sonic 1 sound driver with Sonic 2's? Less buggy, doesn't need loads of (non-Z80) RAM, frees up a little of the 68k, and is more compatible with the original music and SFX than S3K's driver.

    It's certainly possible, but bumps in the road are plentiful, and soon, limitations of S2's driver will get in your way. The driver isn't the most usable: compression woes, the mess of porting the thing in the first place, there are only two music banks, one DAC bank, and a very limited number of music slots. Not to mention, the driver itself has very little room for additions to the code. There's also the total lack of support for Special SFX, which GHZ's waterfall uses, and the MZ push SFX behaviour.

    Over time, there have come guides that correct these, and it seems the only hurdle left is S1 support. The process is a little... involved, so, rather than a guide, I think something pre-made would be better. I might still eventually split some of this into guides, but that's for another time.

    I suppose I should bring up one interesting barrier, sound driver compression: Sonic 2's disasm assembles and compresses the driver at build time. The compression used is Saxman. S3K's and S1's disasms don't do this. Until recently, S1 didn't compress anything at all, but the addition of a disassembled Z80 DAC driver necessitated the addition of a compress-to-Kosinski function, which S3K also has. Changing this would require the user compile a modified s1p2bin. It would be easier to just make the driver settle for Kosinski compression, which I think is better, anyway.

    Anyhow, here's a features list.

    Features:
    • No Saxman-compressed music - Free up a lot of Z80 RAM
    • Driver is Kosinski-compressed - Better compression, so it seems. Also, S1's disasm supports this by default
    • Support for >2 music banks
    • Support for >1 DAC banks
    • Additional bugfixes
    • Smaller FM frequencies table - More RAM
    • S3K PSG frequency range
    • Track RAM optimisation - More RAM
    • Greater sound ID range - $01-$FF
    • No sndDriverInput - Writes are direct, a la S3K
    • Restored broken sound queue - Used for music :specialed:
    • DAC auto-bankswitch - No need to worry about sample alignment
    • SMPS2ASM
    • Toggleable features - What you do need, and what you don't
      • Special SFX support
      • MZ block push SFX support
      • CPZ's gloop SFX support
      • Spin dash rev SFX support
      • Sonic 2 PSG envelopes
      • Sonic 2 DAC samples

    Download (code) | Download (ROM)

    S1 VS S2:
    There's a very significant difference between Sonic 1's and 2's drivers: the Mega Drive has two processors - the 68k and the Z80. The 68k is responsible for just about everything; and the Z80 is just the sound processor. Sonic 1 used a driver (SMPS 68k) that was mostly handled by the 68k, which is great for freeing up the Z80, but the game never really took advantage of that, so all it served to do was make a game, that already had performance issues, slightly worse off. So, for Sonic 2, the driver was ported to the Z80.

    Despite this, Sonic 2's driver is not your usual SMPS Z80 (the same kind of driver S3K uses). SMPS 68k and SMPS Z80 do some things slightly different, and Sonic 2's driver, being a port of SMPS 68k, still behaves as SMPS 68k. This makes it more compatible with Sonic 1/Sonic 2 music and SFX than S3K's driver, while still being Z80-based. A 'best of both worlds' in some lights.

    Of course, if an S3K music/SFX collection is more your thing, then S3K's driver might be a better choice. The incompatibility goes two ways.

    Installation:
    Note: You'll need a copy of the S2 disassembly and flamewing's SMPS2ASM song pack (songs-sfx-v4a.7z). Also, your S1 disasm must be the AS Git branch

    Disassembly files
    We need to strip out the old driver's files, and replace them. Delete s1.sounddriver.asm and the sound folder, then extract the new driver's 7z file to your disasm. Go into the new sound folder and extract the contents of the s1 folder from songs-sfx-v4a.7z into it. Following that, go into the sound folder of the S2 disasm, and copy across the DAC and PCM folders.

    sonic.asm
    Open sonic.asm. Make sure that OptimiseSound is set to 0. Find the label 'SoundDriver', and replace the line with this:

    Code (ASM):
    1.     include "s2.sounddriver.stuff.asm"
    Find and delete SoundDriverLoad. Our new file contains a replacement. Also, remove the 'bsr.w' to it in GM_Title, and change the 'bsr.w' to it above MainGameLoop to a 'jsr'.

    Remove both 'jsr's to UpdateMusic. These just updated the old driver, the new one will do that on its own.

    sub PlaySound.asm
    Now open _incobj/sub PlaySound.asm, and delete PlaySound_Unused. Replace PlaySound and PlaySound_Special with these:

    Code (ASM):
    1. PlaySound:
    2.         stopZ80                        ; Stop the Z80 so the 68k can write to Z80 RAM
    3.         waitZ80
    4.         tst.b    (Z80_RAM+zAbsVar.QueueToPlay).l        ; If this (zQueueToPlay) isn't $00, the driver is processing a previous sound request.
    5.         bne.s    +                    ; So we'll put this sound in a backup queue
    6.         move.b    d0,(Z80_RAM+zAbsVar.QueueToPlay).l    ; Queue sound
    7.         startZ80                    ; Start the Z80 back up again so the sound driver can continue functioning
    8.         rts
    9. +
    10.         move.b    d0,(Z80_RAM+zAbsVar.SFXUnknown).l    ; Queue sound
    11.         startZ80                    ; Start the Z80 back up again so the sound driver can continue functioning
    12.         rts
    Code (ASM):
    1. PlaySound_Special:
    2.         stopZ80                        ; Stop the Z80 so the 68k can write to Z80 RAM
    3.         waitZ80
    4.         tst.b    (Z80_RAM+zAbsVar.SFXToPlay).l        ; Is this queue occupied?
    5.         bne.s    +                    ; If so, we'll put this sound in a different queue
    6.         move.b    d0,(Z80_RAM+zAbsVar.SFXToPlay).l    ; Queue sound
    7.         startZ80                    ; Start the Z80 back up again so the sound driver can continue functioning
    8.         rts
    9. +
    10.         move.b    d0,(Z80_RAM+zAbsVar.SFXStereoToPlay).l    ; Queue sound
    11.         startZ80                    ; Start the Z80 back up again so the sound driver can continue functioning
    12.         rts

    PauseGame.asm
    Open _inc/PauseGame.asm. The new pause system is fairly different from both S1 and S2. Start by replacing this...

    Code (ASM):
    1.         move.b    #1,(v_snddriver_ram+f_stopmusic).w ; pause music
    ...with this:

    Code (ASM):
    1.         stopZ80
    2.         waitZ80
    3.         move.b    #MusID_Pause,(Z80_RAM+zAbsVar.StopMusic).l    ; pause music
    4.         startZ80
    And replace both of these...

    Code (ASM):
    1.         move.b    #$80,(v_snddriver_ram+f_stopmusic).w
    ...with this:

    Code (ASM):
    1.         stopZ80
    2.         waitZ80
    3.         move.b    #MusID_Unpause,(Z80_RAM+zAbsVar.StopMusic).l
    4.         startZ80

    Constants.asm
    Open Constants.asm and remove the following now-useless constants:
    • Size_of_SegaPCM
    • Size_of_DAC_driver_guess
    • z80_dac3_pitch
    • z80_dac_status
    • z80_dac_sample

    Also remove the data under 'Sound driver constants'.

    Now we'll want to adjust the sound ID constants to read the new tables. Find '; Background music', and set bgm__First to $01, then perform the following replacements:
    • '4)' -> '3)'
    • 'MusicIndex' -> 'zMasterPlaylist'
    • 'ptr_musend' -> 'zMusIDPtr__End'

    Set sfx__First to 'bgm__Last+1', then perform the following replacements:
    • '4)' -> '2)'
    • ptr_sndend -> SndPtr__End

    Set spec__First to 'sfx__Last+1', then perform the following replacements:
    • '4)' -> '2)'
    • ptr_specend -> SpecPtr__End

    Replace everything from flg__First to flg__Last with this:

    Code (ASM):
    1. flg__First   equ $FA
    2. sfx_Stop:   equ ((CmdPtr_StopSFX-zCommandIndex)/2)+flg__First
    3. bgm_Fade:   equ ((CmdPtr_FadeOut-zCommandIndex)/2)+flg__First
    4. sfx_Sega:   equ ((CmdPtr_SegaSound-zCommandIndex)/2)+flg__First
    5. bgm_Speedup:   equ ((CmdPtr_SpeedUp-zCommandIndex)/2)+flg__First
    6. bgm_Slowdown:   equ ((CmdPtr_SlowDown-zCommandIndex)/2)+flg__First
    7. bgm_Stop:   equ ((CmdPtr_Stop-zCommandIndex)/2)+flg__First
    8. flg__Last:   equ ((CmdPtr__End-zCommandIndex-2)/2)+flg__First

    Variables.asm
    Open Variables.asm, and delete everything from this...

    Code (ASM):
    1. v_snddriver_ram:  = $FFFFF000 ; start of RAM for the sound driver data ($5C0 bytes)
    ...to this:

    Code (ASM):
    1. ; =================================================================================
    2. ; From here on, no longer relative to sound driver RAM
    3. ; =================================================================================
    That $600 bytes of RAM is now completely free.

    Fixing up the SEGA screen:
    Sonic 1 relied on the driver to occupy the 68k and produce good quality PCM playback. This Z80 driver won't be able to do that, so we'll have to alter the SEGA screen to handle things the S2 way, so the menu remains static for long enough, and does not make the chant sound awful.

    In sonic.asm, find Sega_WaitEnd. Change the write to v_vbla_routine above Sega_WaitEnd to #2, and the write below it to #$14. Also change the write to v_demolength to $B4, this will extend how long the screen is to remain idle, letting the SEGA chant play.

    Find VBla_14, and delete the label. After the 'rts', add this:

    Code (ASM):
    1. VBla_14:                ; XREF: VBla_Index
    2.         move.b    (v_vbla_count+3).w,d0
    3.         andi.w    #$F,d0
    4.         bne.s    .skipread
    5.  
    6.         stopZ80
    7.         waitZ80
    8.         bsr.w    ReadJoypads
    9.         startZ80
    10.  
    11. .skipread:
    12.         tst.w    (v_demolength).w
    13.         beq.w    .end
    14.         subq.w    #1,(v_demolength).w
    15.  
    16. .end:
    17.         rts
    That should do it. As you can see, we're changing which V-Int routine to use, and adjusting it to still read controller input, while still being light on the Z80, so the playback isn't horribly scratchy. But we're not done yet...

    Correcting silence on screenmode change:
    Another change from S1 to S2: Check out S1's GM_Sega, and compare it to S2's SegaScreen. S1 uses the 'sfx' macro (which uses the SFX sound queue), while S2 uses PlayMusic (which uses the music sound queue). In S2's driver, while playing the SEGA chant, the driver will only stop if there's something in the first music queue. Now, this driver currently (v1/v1.1) accounts for both the first music and SFX queue, but, really, it would be better to just use the music queue exclusively.

    Go to the following code, and change the 'sfx' macro to a 'music' macro.
    • GM_Sega
    • GM_Title
    • GM_Ending

    Expanding sound test:
    We need to make the sound test span values $00-$FF. It also wouldn't hurt to add navigational features to button A and C.

    In sonic.asm, go to LevelSelect and change this...

    Code (ASM):
    1.         andi.b    #btnABC+btnStart,(v_jpadpress1).w ; is A, B, C, or Start pressed?


    ...into this:

    Code (ASM):
    1.         andi.b    #btnB+btnStart,(v_jpadpress1).w ; is B or Start pressed?


    Then change this...

    Code (ASM):
    1.         bne.s    LevSel_Level_SS    ; if not, go to    Level/SS subroutine


    ...into this:

    Code (ASM):
    1.         beq.s    .checkB                ; if so, branch
    2.         andi.b    #btnStart,(v_jpadpress1).w    ; is start pressed?
    3.         beq.s    LevelSelect            ; if not, branch
    4.         bra.s    LevSel_Level_SS            ; if so, go to    Level/SS subroutine
    5.  
    6. .checkB:
    7.         andi.b    #btnB,(v_jpadpress1).w    ; is B pressed?
    8.         beq.s    LevelSelect        ; if not, branch
    9.  
    10. .soundtest:


    After that, remove this line:

    Code (ASM):
    1.         addi.w    #$80,d0


    Then delete these lines:

    Code (ASM):
    1.         ; This is a workaround for a bug, see Sound_ChkValue for more.
    2.         ; Once you've fixed the bugs there, comment these four instructions out
    3.         cmpi.w    #bgm__Last+1,d0    ; is sound $80-$93 being played?
    4.         blo.s    LevSel_PlaySnd    ; if yes, branch
    5.         cmpi.w    #sfx__First,d0    ; is sound $94-$9F being played?
    6.         blo.s    LevelSelect    ; if yes, branch
    7.  
    8. LevSel_PlaySnd:


    After that, go to LevSel_SndTest, and change this...

    Code (ASM):
    1.         andi.b    #btnR+btnL,d1    ; is left/right    pressed?


    ...into this:

    Code (ASM):
    1.         andi.b    #btnA+btnC+btnR+btnL,d1    ; is left/right/A/C pressed?


    and remove this:

    Code (ASM):
    1.         bhs.s    LevSel_Right
    2.         moveq    #$4F,d0        ; if sound test    moves below 0, set to $4F


    And then change this...

    Code (ASM):
    1.         btst    #bitR,d1    ; is right pressed?
    2.         beq.s    LevSel_Refresh2    ; if not, branch
    3.         addq.w    #1,d0        ; add 1    to sound test
    4.         cmpi.w    #$50,d0
    5.         blo.s    LevSel_Refresh2
    6.         moveq    #0,d0        ; if sound test    moves above $4F, set to    0
    7.  
    8. LevSel_Refresh2:


    ...into this:

    Code (ASM):
    1.         btst    #bitR,d1    ; is right pressed?
    2.         beq.s    LevSel_ButtonA    ; if not, branch
    3.         addq.w    #1,d0        ; add 1    to sound test
    4.  
    5. LevSel_ButtonA:
    6.         btst    #bitA,d1    ; is A pressed?
    7.         beq.s    LevSel_ButtonC    ; if not, branch
    8.         addi.b    #$10,d0        ; add $10 to sound test
    9.         bcc.s    LevSel_ButtonC    ; did the addition overflow?
    10.         moveq    #0,d0        ; if so, set value to $00
    11.  
    12. LevSel_ButtonC:
    13.         btst    #bitC,d1    ; is C pressed?
    14.         beq.s    LevSel_Refresh2    ; if not, branch
    15.         subi.b    #$10,d0        ; subtract $10 from sound test
    16.         bcc.s    LevSel_Refresh2
    17.         cmpi.b    #$F0,d0
    18.         beq.s    LevSel_Refresh2    ; do not set to 0 if already at 0
    19.         moveq    #0,d0        ; if the subtraction overflowed, set value to $00
    20.  
    21. LevSel_Refresh2:


    Now go to LevSel_DrawSnd, and remove this line:

    Code (ASM):
    1.         addi.w    #$80,d0

    Removing needless Z80 stops:
    I think Vladikcomper does well in explaining this one: Just look through sonic.asm for instances of 'stopZ80', 'waitZ80', and 'startZ80', and remove them. They should all be in the V-Int routines and JoypadInit. It just so happens that all instances in stock sonic.asm are needless. Transfer-related stuff, joypad-related stuff; the joypad-related stuff at least had a point, depending on the quality of ROM hardware.

    Changelog:
    v1.0 - 19/04/2015 (DD/MM/YYYY)
    - Followed every goddamn S2 driver guide under the sun
    - Further optimised DAC channel register setting
    - Removed music compression
    - Changed driver's compression to Kosinski
    - Ported some fixes from Sonic 1's driver
    - Optimised track RAM (zTrack.len = 24h)
    - Added SMPS2ASM
    - Restored Special SFX system
    - Restored MZ block push SFX
    - Made S2-exclusive features toggleable; off by default
    - Made some minor optimisations (ix-relative -> absolute)
    - Made Sega chant stop if SFX queue is occupied (S2 only used the music queue - S1 is just stupid)
    - Optimised Sega chant playback loop to make up for the above fix
    - Changed ensure1byteoffset to produce a message instead of a warning (that got annoying fast)

    v1.1 - 24/04/2015
    - Added a new bugfix (PSG fade)
    - Stripped down SMPS2ASM core
    - Added new driver optimisations (size)
    - Improved bugfix descriptions
    - Corrected (my) bug in 1-up music backup ('SFX overriding' bit was not cleared)
    - Optimised some Special SFX code
    - Improved cfOpF9 description
    - Added another bugfix (accidental delay on first frame of music playback)
    - Switched to S3K's tempo algorithm for the above fix
    - Made necessary changes to SMPS2ASM
    - Optimised zMasterPlaylist's entry size (3 bytes)
    - Moved zDACDecodeTbl to align better
    - Changed 'jr' to 'jp' in zWriteToDAC
    - Corrected (my) bug in gloop SFX code being skipped
    - Removed (my) redundant bankswitch under zloc_F1D
    - Renamed 's2.sounddriver.compatibility.asm' to 's2.sounddriver.stuff.asm'

    Credits:
    Valley Bell - Bugfixes, S2Beta4 driver disassembly (source of Push SFX support)
    Flamewing - SMPS2ASM, >$1F songs expansion, pointing out S3K's additional frequency values, improvement for DAC channel register setting
    Vladikcomper - Elements of the DAC-related improvements
    Clownacy - Making the driver good
     
    Last edited: Mar 9, 2024
  2. Clownacy

    Clownacy

    Tech Member
    1,061
    607
    93
    Update - v1.1


    - Added a new bugfix (PSG fade)
    - Stripped down SMPS2ASM core
    - Added new driver optimisations (size)
    - Improved bugfix descriptions
    - Corrected (my) bug in 1-up music backup ('SFX overriding' bit was not cleared)
    - Optimised some Special SFX code
    - Improved cfOpF9 description
    - Added another bugfix (accidental delay on first frame of music playback)
    - Switched to S3K's tempo algorithm for the above fix
    - Made necessary changes to SMPS2ASM
    - Optimised zMasterPlaylist's entry size (3 bytes)
    - Moved zDACDecodeTbl to align better
    - Changed 'jr' to 'jp' in zWriteToDAC
    - Corrected (my) bug in gloop SFX code being skipped
    - Removed (my) redundant bankswitch under zloc_F1D
    - Renamed 's2.sounddriver.compatibility.asm' to 's2.sounddriver.stuff.asm'
     
  3. Caverns 4

    Caverns 4

    Member
    346
    0
    16
    Sonic: Retold
    I saw some of the fixes you made to Sonic 2 itself on GitHub - Very impressive stuff.

    One thing I do like about the Sonic 2 Sound Driver is that is uses the Z80 Processor as you mentioned instead of just tossing more work onto the 68k(Not that the 68k couldn't handle that and more, that thing's a beast for the time). That in itself is a good enough reason for me to use this over the Clone Driver, which is still good work.
    I do have one question though. Since this is essentially a SMPS2ASM Compatible Sonic 2 Sound Driver with tweaks and bugfixes, how friendly is it for use in Sonic 2 itself? Forgive me if it's a stupid question, I don't really now how hard the process of integrating SMPS2ASM support is, and therefore how little or how much you had to change.
     
  4. MainMemory

    MainMemory

    Kate the Wolf Tech Member
    4,742
    338
    63
    SonLVL
    SMPS2ASM can be used without any modifications to the sound driver (just define SonicDriverVer = 2 and include _smps2asm_inc.asm before any ASM'd songs), and Sonic 2's driver is capable of playing music made for the S1 driver without any modifications. What does require modifications, is adding the DAC samples and PSG envelopes required for S3K/S3D songs, adding more music/DAC banks that more music requires, and bugfixes.
     
  5. Clownacy

    Clownacy

    Tech Member
    1,061
    607
    93
    This driver's currently in a form where an S2 equivalent would just be a stock S2 driver with all the guides applied. I still need to make some guides on restoring the Special SFX system, MZ push SFX, and optimising track RAM, but otherwise, it's all there. Even most of the bugfixes and code optimisations have been merged into the S2 Git disasm. Check the links in the credits for all the guides used.

    Otherwise... porting this back to S2 would require modifying s2p2bin.exe to compress the driver in Kosinski, or modify SoundDriverLoad to decompress from Saxman. You'd also need to replace the music, SFX, their pointers, speed-up tempos, and all that other stuff with their S2 equivalents. You would also need to remove SndDriverInput, and replace the pause feature with this driver's equivalent. The toggles at the start of the driver would also need to be changed, so the Spin Dash, gloop, DACs and PSGs work. That's not even going into the redundancies within stuff.asm that will cause errors.