Medabots / medarot3

Medarot 3 GBC disassembly/translation
35 stars 6 forks source link

“Robottle fight” voice sample cuts out halfway on real GBA hardware #183

Open Blaziken257 opened 1 year ago

Blaziken257 commented 1 year ago

Hardware used

Flashcarts used

Versions

Has been present since the very first English patch (0.1.0) and remains as of 0.4.1+EN+nightly.20230611, and occurs consistently. Consistently does not occur in the retail Japanese game. Occurs in both Kabuto and Kuwagata versions.

Description

The “Robottle fight” voice sample, which is heard at the start of a Robattle, cuts out halfway on real GBA hardware: Only the “Robottle” part is heard, but the “fight” part is not. This issue happens consistently in all English patches tested. No other voice sample in the game is known to have this issue.

This issue does not occur in BGB in GBC mode, but a different issue occurs when played in GBC on GBA mode. (This can be done by right-clicking BGB -> Options -> System tab -> Check "detect GBA".) Instead of being silent, the "fight" part has a buzzing sound overlapping the normal "fight" voice. Due to the different behavior between BGB and real GBA hardware, this issue appears to be a strange corner case. (Making this even stranger is that Medarot 3 doesn't even check the b register on boot, which is normally done to detect if the game is run on a GBA.)

It is possible that the issue is timing or lag related, as if you hack the game to disable double speed mode (this can be done by unchecking the "double speed" box in the bgb IO map), even more severe buzzing occurs with all voice samples.

This issue has been untested on other flashcarts. It was also untested on an original GBA as I do not have a working one to test with.

Audio files

Various audio (MP3) files are attached.

Robottle Fight.zip

VariantXYZ commented 1 year ago

Verified the issue doesn't occur on 0.1.0~57, but occurs on 0.1.0~56. This was just a text update though, so an error in the text update was probably masking the actual root cause.

VariantXYZ commented 1 year ago

The whole voiceover setup is super timing dependent... Will probably spend some time disassembling it and maybe reimplementing it.

VariantXYZ commented 1 year ago

0A:44AF is what actually does the robattle stuff, including setting up the text and calling the function

3779 calls the playback and sets things up 37c7 is the actual voice sample playback function, calls 3833 which is some arbitrary delay function

VariantXYZ commented 1 year ago

https://github.com/Medabots/medarot3/assets/3066132/81f40730-f8c8-4f15-a028-1abb9f64a93d

https://github.com/Medabots/medarot3/assets/3066132/63ab3f2e-de0a-482f-bb72-53747f8b1580

Depending on how you affect the timing it, it actually happens in both the original and our patch. Note the volume differences too...

Blaziken257 commented 7 months ago

It's been mentioned that the Japanese version also has this issue, but I looked at part of a YouTube playlist of Medarot 3 (see here: https://www.youtube.com/playlist?list=PLNL-hT-h2KcRnB1E7ZkP8Lokv14Nm1B79), in Japanese, on the Game Boy Player (which is affected by the "Robattle fight" issue in the English patch). This issue doesn't seem to be present in Japanese on real hardware so it might be an emulation issue. Below is a list of timestamps through the end of Chapter 2. This will be updated over time with more timestamps. Unless otherwise noted, a timestamp has "Robattle fight" as its voice clip.

Video in playlist Link & Timestamp Remarks
1 01:41 Cutscene
1 02:37 Chapter 1
1 07:52 Cutscene
1 06:18 Cutscene
1 11:08
1 13:53
2 07:16
2 15:53
3 02:53
3 06:53
3 13:27
3 17:53
3 22:08
3 31:49 Chapter 2
4 10:41
4 19:19
5 00:49
5 05:16
5 10:29
6 02:04
6 05:10
6 11:37 Cutscene
6 12:02 Same cutscene
6 12:24
6 25:42
6 29:13 Chapter 3
nitro2k01 commented 7 months ago

The root cause of this issue is that GBC audio on GBA is very different internally from GBC audio on GBC, which causes a problem with the playback method that this and some other games use.

The playback method works by playing ultrasonic tone and varying the volume of the sound channel. The ultrasonic tone is inaudible or filtered out, and what's left is a varying DC offset that's used to play one sample at a time.

For example a square wave at volume F might output 0F0F0F0F... and that gets averaged to 7.5 "units of volume". Or volume C might output 0C0C0C0C... and that gets averaged to 6 "units of volume". This is used to play back a sample.

On GBC, the audio mixing is analog. The combined signal is happily output from the SoC, and the ultrasonic tone is (hopefully) filtered out by various components on the GBC meant for suppressing interference. (For example the C14 filter capacitor connected in parallel with the internal speaker, or the EM1-EM5 EMI filters connected in series with the headphone jack.)

On GBA, the audio mixing is digital, and what's worse, more or less using the nearest neighbor sampling method. It's sampling periodically at a rate specified by the GBA BIOS during the boot process. This means that in the example above it might only pick every 2nd or 4th sample and happen to always pick the active signal level, in which case the sample is heard. Or it might align so that it always reads zeros. It essentially works or not based on dumb luck.

Except that's not what happens at all, because of what is probably a bug in the code. First look at the initialization code in Sound_PlaySampleFragment:

  ld a, $FF
  ldh [H_RegNR13], a
  ldh [H_RegNR23], a
  ld a, $78
  ldh [H_RegNR12], a
  ldh [H_RegNR22], a ;Envelope 0xFF, sweep disabled
  ld a, $87
  ldh [H_RegNR14], a
  ldh [H_RegNR24], a ;Frequency 0x2FF, consecutive mode

The comment is actually wrong. The pitch value is initialized to $7FF. This is the highest frequency the channel can produce, 131072 Hz, and it's what you would expect from this playback method.

For contrast, this is what's done later in .pcmLoop.

  ld a, $FF ; 0 in Telefang.
  ldh [H_RegNR13], a
  ldh [H_RegNR23], a
  ld a, $81 ; $80 in Telefang.
  ldh [H_RegNR14], a
  ldh [H_RegNR24], a

This sets the pitch value to $1FF which gives a frequency of 131072/(2048-511) = 85.28 Hz. This was likely meant to be $7FF. What this means instead is that the programmer, probably unknowingly, discovered a sample playback technique I've been toying with. In this method, you retrigger the channel periodically to both set the volume, and reset the phase position. The channel goes through a sequence of 8 steps, which can be either "high" or "low", and this normally creates a 12.5%, 25%, 50% or 75% square wave. This is described in Pan Docs.

But the phase position can be reset partially, to the start of the closest step. (Unless you turn the APU off and on again.) So if you let the channel run freely for a while without resetting it or keeping precise track of the time that has elapsed, you can get a similar effect as above where you only get zeros as output.

In the first playback method described the issue is that a ultrasonic square wave doesn't align with the sampling if the GBA's audio mixer and only samples zeros. But this method accidentally isn't used.

In the second playback described, the issue is that we might wait the wrong amount of time and lock the channel in a phase position where it only outputs zeros. So we've basically gone from one playback method that might've worked by dumb luck, through a bug, to another playback method that works by dumb luck but in a different way.

So, what's actually happening, exactly? We can reset the APU by turning it off and then on again to reset the phase position of CH1 and CH2. The game does this... kind of:

  xor a
  ldh [H_RegNR52], a ;Disable sound hardware, resetting all state
  call Sound_OpenSampleData

.fragmentLoop
  call Sound_ExitSampleMode ; In Telefang this call is missing.
  call Sound_PrepareSampleFragment
  di ; In Telefang this is before the call to the Sound_PlaySample wrapper function instead.
  call Sound_PlaySampleFragment
  ei ; In Telefang this is after the call to the Sound_PlaySample wrapper function instead.
  ; Later loop back to .fragmentLoop.

The problems more specifically are this:

  1. While the APU is properly reset when playing the first sample fragment, it's not reset on subsequent sample fragments because the APU is never turned off again.
  2. When ei is executed, pending VBlank and LCD interrupts are called. This takes an undefined amount of time, and throws off the timings. If it still happens to work after that, it is (not to repeat myself too much) by dumb luck.

The quick fix would be to rearrange the code a little bit:

Sound_PlaySample::
  push af
  push bc
  push de
  push hl
  call Sound_OpenSampleData

.fragmentLoop
  call Sound_PrepareSampleFragment
  di ; In Telefang this is before the call to the Sound_PlaySample wrapper function instead.
  xor a
  ldh [H_RegNR52], a ;Disable sound hardware, resetting all state
  call Sound_PlaySampleFragment
  call Sound_ExitSampleMode ; In Telefang this call is missing.
  ei ; In Telefang this is after the call to the Sound_PlaySample wrapper function instead.
  ld a, [W_Sound_SampleFragmentCount]
  dec a
  ld [W_Sound_SampleFragmentCount], a
  jr nz, .fragmentLoop
  call Sound_ExitSampleMode
  pop hl
  pop de
  pop bc
  pop af
  rst $18  ; Absent from Telefang (since Telefang uses a wrapper function instead).
  ret

What we're doing here is putting all timing critical code inside the di/ei fence. I've also moved call Sound_ExitSampleMode to after the sample fragment is finished playing which should minimize interference noises after a sample fragment is finished playing. Although if you want to have something that doesn't just work by dumb luck, I could rewrite the whole sample playback more properly. My fingers are itching. :)

VariantXYZ commented 7 months ago

Thanks for the detailed explanation @nitro2k01 .

Also CC: @andwhyisit

Although if you want to have something that doesn't just work by dumb luck, I could rewrite the whole sample playback more properly. My fingers are itching. :)

If you're interested, a PR is always welcome :). We're also fairly active in the development discord https://discord.gg/VqqGXzXE (#medarot-3-translation)