don't click here

How to improve PCM playback quality with Sonic 2's sound driver

Discussion in 'Engineering & Reverse Engineering' started by Clownacy, Dec 30, 2014.

  1. Clownacy


    Tech Member
    So, get this: in Sonic 2, if you go to the sound test and play sound 7A, you get this horrible version of the Sega chant, but it sounds fine on the Sega screen. If you have an ear for it, apparently even the DAC drums are like that. As Vladikcomper said before, this is because of the Z80 being stopped frequently. Thing is, there are many times when it's being stopped for no flaming reason. For the most part, anyway. Here, we'll be doing what we can to fix that.

    Unfortunately, hardware users won't see much benefit from this. The Sega sound only sounds slightly less strangled. It seems DMA still does a huge number on sound quality. Emulators sound better, though. What can't be helped is the DAC playback. As, no matter how much we optimise things on the 68k side, the Z80 will interrupt itself with V-Int, butchering the DAC quality. The Sega sound works around that by disabling interrupts, but that comes at the cost of pausing the entire sound driver. That said, hope you like splash screens. If that's your cup of tea, onto the guide!

    First let's get something minor out of the way.

    Replacing the current music pause/unpause system
    The way the current system works will interfere with our upcoming modifications, so we're effectively gonna cut it out. But we still need the basic functionality, so we'll replace it with something more... direct.

    Open s2.constants.asm, and find the definitions of MusID_Pause and MusID_Unpause. Make them $7F and $80, respectively.

    Now find the only use of MusID_Pause in s2.asm. It should be but a single instruction:

    Code (ASM):
    1.     move.b  #MusID_Pause,(Music_to_play).w  ; pause music

    The address it writes to is part of a relay system we're going to be removing, sndDriverInput. You can study it yourself to see how it operates, but all you need to know is that it has some special-case code for MusID_Pause/Unpause, which writes $7F or $80 to zStopMusic. So, to take sndDriverInput out of the equation, we'll do this ourselves, which is why we changed MusID_Pause/Unpause's definitions from a sound ID to the data they'd eventually represent. Surround this lone instruction with stopZ80 and startZ80 (since the 68k can't write to the Z80's address space otherwise), and change the address '(Music_to_play).w' to '(Z80_RAM+zStopMusic).l'. Done. Now onto MusID_Unpause.

    You should know the drill. It's exactly the same: stick a stopZ80 before the instruction, and a startZ80 after it; then change the destination address to '(Z80_RAM+zStopMusic).l'. Do this for all instances.

    It's safe to build and test, but hang on! None of the sound commands are working! Music doesn't fade, the Sega chant doesn't play... At least the new pause works, but we gotta fix that bug!

    Open s2.sounddriver.asm. Again, there's some special-case code that we gotta deal with. Go to zPlaySoundByIndex, and you'll find that it uses MusID_Pause. It's part of a check to invalidate sound IDs $FE and $FF, which used to be the original MusID_Pause/Unpause. That isn't the case anymore, and the constant is no longer appropriate, causing the sound commands to be invalidated, as the original MusID_Pause/Unpause would have.

    Simply remove the check and the following conditional return, and you'll be as good as gold.

    One last little change to make, though: what's gonna happen to sound IDs $FE and $FF? They're now unused, and are even buggy: try building your ROM and playing those sounds in the sound test. That isn't normal. The fix is simple: shift the sound commands to occupy the now-unused slots. A nice bonus to this is that you free up two sound IDs!

    Back in s2.constants.asm, add 2 to all the sound commands: from MusID_StopSFX to MusID_Stop.

    That's that done. There's still one last bug, but that'll be fixed later on.

    Optimising Z80 stops
    There are a lot of times where the Z80 is stopped, even when it doesn't need to be. For example, joypad-related reads and writes. We can do without all that unnecessary junk stopping the Z80. We'll also be removing the stops and starts used for sndDriverInput, which is actually an area where the Z80 should be stopped, but since we'll be removing that, we won't be needing the Z80 to be stopped either. Remember to not remove the ones you added when replacing the pause system.

    Look for Z80 stops and starts here:
    1. VintSub0
    2. loc_4C4
    3. loc_54A
    4. Vint0_noWater
    5. VintSub14
    6. VintSub8
    7. loc_748
    8. Vint10_specialStage
    9. loc_86E
    10. VintSubA
    11. SS_PNTA_Transfer_Table
    12. VintSub1A
    13. VintSubC
    14. loc_BD6
    15. VintSub18
    16. VintSub16
    17. Do_ControllerPal
    18. H_Int
    19. JoypadInit
    20. ClearScreen
    21. EndingSequence

    Removing the 68k-side sound queuing system
    The sound driver itself already has a queuing system, so all this one serves to do is waste cycles and time: as you may have seen when removing all those Z80 stops, sndDriverInput is ran, and pauses the Z80, on every V-Int, and even on H-Int. That's a lot of time for the Z80 to be stopped, so let's get rid of that.

    Find and delete sndDriverInput, then remove every branch and jump to it. Bonus: in doing that, we've eliminated that last bug I mentioned earlier.

    Of course, in doing that, we've also effectively destroyed the sound queuing. But we'll fix that.

    Overhauling PlaySound subroutines
    What happens more often? V-Int, or a sound being queued? A frame passing by, or Sonic picking up a ring?

    For limiting Z80 idling, PlaySound is simply more appropriate. In fact, S3K has it this way (not that its PCM playback is much better. See the above section on Z80 stops). What we're going to do is merge sndDriverInput and the PlaySound subroutines, regaining the functionality using code that isn't run so excessively often.

    Let's start from the top: with PlayMusic. Replace it with this:
    Code (ASM):
    1. ; sub_135E:
    2. PlayMusic:
    3.     stopZ80                 ; Stop the Z80 so the 68k can write to Z80 RAM
    4.     cmpi.b  #$80,(Z80_RAM+zQueueToPlay).l   ; If this (zQueueToPlay) isn't $80, 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+zQueueToPlay).l ; Queue sound
    7.     startZ80                ; Start the Z80 back up again so the sound driver can continue functioning
    8.     rts
    10. + ;.usebackupqueue
    11.     move.b  d0,(Z80_RAM+zSFXUnknown).l  ; Queue sound
    12.     startZ80                ; Start the Z80 back up again so the sound driver can continue functioning
    13.     rts
    14. ; End of function PlayMusic

    Next up is PlaySound. Replace it with this:
    Code (ASM):
    1. ; sub_137C:
    2. PlaySoundLocal:
    3.     tst.b   render_flags(a0)
    4.     bpl.s   ++
    5. ; sub_1370
    6. PlaySound:
    7.     stopZ80                 ; Stop the Z80 so the 68k can write to Z80 RAM
    8.     move.b  d0,(Z80_RAM+zSFXToPlay).l   ; Queue sound
    9.     startZ80                ; Start the Z80 back up again so the sound driver can continue functioning
    10. +   rts
    11. ; End of function PlaySound

    And, finally, PlaySoundStereo:
    Code (ASM):
    1. ; sub_1376:
    2. PlaySoundStereo:
    3.     stopZ80                 ; Stop the Z80 so the 68k can write to Z80 RAM
    4.     move.b  d0,(Z80_RAM+zSFXStereoToPlay).l ; Queue sound
    5.     startZ80                ; Start the Z80 back up again so the sound driver can continue functioning
    6.     rts
    7. ; End of function PlaySoundStereo

    Delete the original PlaySoundLocal. Note that if you followed my previous guide on extending the sound IDs from starting at $80 to $00, then those 'cmpi.b #$80's should be replaced with 'tst.b's.

    Repairing the unused queue
    Now, note that we're making use of an unused sound queue in PlayMusic as a 'backup queue'. This is to replace a lost functionality, where sndDriverInput would 'nag' the sound driver to ensure that a sound would be queued in the next frame if zQueueToPlay isn't currently available. The problem is that, like the one in Sonic 1's driver, this unused queue is half-implemented. Without a backup queue, music writes would be missed, so we've gotta repair this one.

    In s2.sounddriver.asm, find zInitMusicPlayback. In there, you'll see some data being backed up, including two queues: the SFX queue and the stereo queue. Note that the unused queue is not. We'll have to add that.

    Underneath the third 'push bc', add this:
    Code (Text):
    1.     ld  c,(ix+0Bh)      ; unused queue slot
    2.     push    bc

    And, underneath the line '; Restore those queue/flags:', add this:
    Code (Text):
    1.     pop bc
    2.     ld  (ix+0Bh),c      ; unused queue slot

    And with that, we're done.

    Try playing the Sega sound in the sound test now (remembering that its ID is now 7C). This time, after the work we've done, it's closer to how it is on the Sega screen. Now PCM playback is a bit more consistent in its quality. You've also freed up two sound IDs, and you've freed up RAM variables Music_to_play through Music_to_play_2.

    Note that the entire guide has consisted of improving the PCM playback through the 68k side of things. The driver itself is pretty much untouched. Like I said, straight up improving the PCM stream loop would require modifications to the actual loop.

    There is one last thing you may need to do. For me, in Regen, the Sega chant on the Sega screen plays itself twice if I let it run. Removing the sound under loc_3A3E6 fixes it. I'm cautious of recommending it because I don't know what the code is meant to do in the first place. Normally, if the Sega chant sound is queued while the Sega chant sound is playing, it's ignored. But it seems the improved playback causes the sound to end without delay, so this new sound plays just as the first ends, making it play twice.
  2. saxman


    Oldbie Tech Member
    Fantastic job! This is very exciting stuff for me considering sound is one of my favorite aspects of game design.

    May I incorporate your solution into my Saxman's Sonic Boom engine for its next release?
  3. Clownacy


    Tech Member
    Sure thing! I've incorporated other users' guides into my driver before, so I'm fine with the same being done with mine.
  4. Chilly Willy

    Chilly Willy

    Tech Member
    Doom 32X
    There's a reason the read joypad routines stopped the Z80 - there's a bug in the IO chip where if the Z80 accesses the 68000 side at the same time the 68000 reads the joypads, the IO chip starts using a short Z80 cycle for all subsequent Z80 accesses of the 68000 side. On old (slow) roms, that meant that the cycle was too fast for the rom to respond to the Z80, and it got garbage. To prevent problems, you had to stop the Z80 just in case it was about to access the 68000 side, read the pads, then release the Z80. Roms got faster - fast enough that the short cycles no longer mattered, so games quit stopping the Z80 to read the pads. Any modern flash cart is fast enough for the short cycles not to matter. Emulators don't emulate this bug, either. So while you can get away with not stopping the Z80 in this instance, there WAS a valid reason for it at the time.