GB: Fixed APU emulation issues

-Super Mario Land 2 - Pops in menu are fixed by immediately updating APU output after a write
-Perfect Dark - Low voice volume is fixed by having the correct output when the channels are disabled (but DAC is still enabled)
-Daiku no Gen - Low voice volume is fixed by keeping square channel output to digital 0 (= analog 1) until its first tick after being enabled (the game does not let the channel tick at all while the voice sample is playing)
This commit is contained in:
Sour 2025-03-21 23:03:01 +09:00
parent 85070a6dc2
commit 929d4dcc20
12 changed files with 124 additions and 33 deletions

View file

@ -40,6 +40,7 @@ public:
if((value & 0xF8) == 0) {
state.Enabled = false;
state.Output = 0;
} else {
//No zombie mode for GBA? (or maybe it behaves differently.)
//Using the GB implementation of zombie mode causes sound issues in some games

View file

@ -47,7 +47,9 @@ void GbaNoiseChannel::ClockLengthCounter()
void GbaNoiseChannel::UpdateOutput()
{
_state.Output = ((_state.ShiftRegister & 0x01) ^ 0x01) * _state.Volume;
if(_state.Enabled) {
_state.Output = ((_state.ShiftRegister & 0x01) ^ 0x01) * _state.Volume;
}
}
void GbaNoiseChannel::ClockEnvelope()
@ -160,6 +162,7 @@ void GbaNoiseChannel::Write(uint16_t addr, uint8_t value)
break;
case 4: {
bool prevEnabled = _state.Enabled;
if(value & 0x80) {
//Writing a value to NRx4 with bit 7 set causes the following things to occur :
@ -191,6 +194,14 @@ void GbaNoiseChannel::Write(uint16_t addr, uint8_t value)
_state.Timer += (_state.Divisor == 1) ? 4 : -4;
}
//Channel volume is reloaded from NRx2.
_state.Volume = _state.EnvVolume;
if(_state.Enabled) {
//Immediately update output if channel was enabled
UpdateOutput();
}
//Channel is enabled, if volume is not 0 or raise volume flag is set
_state.Enabled = _state.EnvRaiseVolume || _state.EnvVolume > 0;
_apu->UpdateEnabledChannels();
@ -207,12 +218,14 @@ void GbaNoiseChannel::Write(uint16_t addr, uint8_t value)
//Volume envelope timer is reloaded with period.
_state.EnvTimer = _state.EnvPeriod;
_state.EnvStopped = false;
//Channel volume is reloaded from NRx2.
_state.Volume = _state.EnvVolume;
}
_state.LengthEnabled = (value & 0x40);
if(!_state.Enabled && prevEnabled) {
_state.Output = 0;
UpdateOutput();
}
break;
}
}

View file

@ -95,6 +95,10 @@ void GbaSquareChannel::ClockLengthCounter()
void GbaSquareChannel::UpdateOutput()
{
if(!_state.Enabled) {
return;
}
_state.Output = _dutySequences[_state.Duty][(_state.DutyPos - 1) & 0x07] * _state.Volume;
}
@ -217,6 +221,7 @@ void GbaSquareChannel::Write(uint16_t addr, uint8_t value)
break;
case 4: {
bool prevEnabled = _state.Enabled;
_state.Frequency = (_state.Frequency & 0xFF) | ((value & 0x07) << 8);
if(value & 0x80) {
@ -278,6 +283,11 @@ void GbaSquareChannel::Write(uint16_t addr, uint8_t value)
}
_state.LengthEnabled = (value & 0x40);
if(!_state.Enabled && prevEnabled) {
_state.Output = 0;
UpdateOutput();
}
break;
}
}

View file

@ -65,6 +65,10 @@ double GbaWaveChannel::GetOutput()
void GbaWaveChannel::UpdateOutput()
{
if(!_state.Enabled) {
return;
}
if(_state.OverrideVolume) {
_state.Output = _state.SampleBuffer * 3 / 4;
} else if(_state.Volume) {
@ -160,6 +164,7 @@ void GbaWaveChannel::Write(uint16_t addr, uint8_t value)
break;
case 4: {
bool prevEnabled = _state.Enabled;
_state.Frequency = (_state.Frequency & 0xFF) | ((value & 0x07) << 8);
if(value & 0x80) {
@ -186,6 +191,11 @@ void GbaWaveChannel::Write(uint16_t addr, uint8_t value)
}
_state.LengthEnabled = (value & 0x40);
if(!_state.Enabled && prevEnabled) {
_state.Output = 0;
UpdateOutput();
}
break;
}
}

View file

@ -105,30 +105,7 @@ void GbApu::Run()
_noise->Exec(minTimer);
_clockCounter += minTimer;
int16_t leftOutput = (
(_square1->GetOutput() * (int32_t)(cfg.Square1Vol & _state.EnableLeftSq1) / 100) +
(_square2->GetOutput() * (int32_t)(cfg.Square2Vol & _state.EnableLeftSq2) / 100) +
(_wave->GetOutput() * (int32_t)(cfg.WaveVol & _state.EnableLeftWave) / 100) +
(_noise->GetOutput() * (int32_t)(cfg.NoiseVol & _state.EnableLeftNoise) / 100)
) * (_state.LeftVolume + 1) * 40;
if(_prevLeftOutput != leftOutput) {
blip_add_delta(_leftChannel, _clockCounter, leftOutput - _prevLeftOutput);
_prevLeftOutput = leftOutput;
}
int16_t rightOutput = (
(_square1->GetOutput() * (int32_t)(cfg.Square1Vol & _state.EnableRightSq1) / 100) +
(_square2->GetOutput() * (int32_t)(cfg.Square2Vol & _state.EnableRightSq2) / 100) +
(_wave->GetOutput() * (int32_t)(cfg.WaveVol & _state.EnableRightWave) / 100) +
(_noise->GetOutput() * (int32_t)(cfg.NoiseVol & _state.EnableRightNoise) / 100)
) * (_state.RightVolume + 1) * 40;
if(_prevRightOutput != rightOutput) {
blip_add_delta(_rightChannel, _clockCounter, rightOutput - _prevRightOutput);
_prevRightOutput = rightOutput;
}
UpdateOutput(cfg);
}
}
@ -137,6 +114,33 @@ void GbApu::Run()
}
}
void GbApu::UpdateOutput(GameboyConfig& cfg)
{
int16_t leftOutput = (
(_square1->GetOutput() * (int32_t)(cfg.Square1Vol & _state.EnableLeftSq1) / 100) +
(_square2->GetOutput() * (int32_t)(cfg.Square2Vol & _state.EnableLeftSq2) / 100) +
(_wave->GetOutput() * (int32_t)(cfg.WaveVol & _state.EnableLeftWave) / 100) +
(_noise->GetOutput() * (int32_t)(cfg.NoiseVol & _state.EnableLeftNoise) / 100)
) * (_state.LeftVolume + 1) * 40;
if(_prevLeftOutput != leftOutput) {
blip_add_delta(_leftChannel, _clockCounter, leftOutput - _prevLeftOutput);
_prevLeftOutput = leftOutput;
}
int16_t rightOutput = (
(_square1->GetOutput() * (int32_t)(cfg.Square1Vol & _state.EnableRightSq1) / 100) +
(_square2->GetOutput() * (int32_t)(cfg.Square2Vol & _state.EnableRightSq2) / 100) +
(_wave->GetOutput() * (int32_t)(cfg.WaveVol & _state.EnableRightWave) / 100) +
(_noise->GetOutput() * (int32_t)(cfg.NoiseVol & _state.EnableRightNoise) / 100)
) * (_state.RightVolume + 1) * 40;
if(_prevRightOutput != rightOutput) {
blip_add_delta(_rightChannel, _clockCounter, rightOutput - _prevRightOutput);
_prevRightOutput = rightOutput;
}
}
void GbApu::PlayQueuedAudio()
{
blip_end_frame(_leftChannel, _clockCounter);
@ -350,6 +354,9 @@ void GbApu::Write(uint16_t addr, uint8_t value)
_wave->WriteRam(addr, value);
break;
}
//Update APU output - some writes can immediately change the output
UpdateOutput(_settings->GetGameboyConfig());
}
uint8_t GbApu::InternalReadCgbRegister(uint16_t addr)

View file

@ -10,6 +10,7 @@ class Emulator;
class Gameboy;
class SoundMixer;
class EmuSettings;
struct GameboyConfig;
class GbApu : public ISerializable
{
@ -46,6 +47,8 @@ private:
uint8_t InternalRead(uint16_t addr);
uint8_t InternalReadCgbRegister(uint16_t addr);
void UpdateOutput(GameboyConfig& cfg);
public:
GbApu();
virtual ~GbApu();

View file

@ -40,6 +40,7 @@ public:
if((value & 0xF8) == 0) {
state.Enabled = false;
state.Output = 0;
} else {
//This implementation of the Zombie mode behavior differs from the description
//found here: https://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware

View file

@ -45,7 +45,9 @@ void GbNoiseChannel::ClockLengthCounter()
void GbNoiseChannel::UpdateOutput()
{
_state.Output = ((_state.ShiftRegister & 0x01) ^ 0x01) * _state.Volume;
if(_state.Enabled) {
_state.Output = ((_state.ShiftRegister & 0x01) ^ 0x01) * _state.Volume;
}
}
void GbNoiseChannel::ClockEnvelope()
@ -158,6 +160,7 @@ void GbNoiseChannel::Write(uint16_t addr, uint8_t value)
break;
case 4: {
bool prevEnabled = _state.Enabled;
if(value & 0x80) {
//Writing a value to NRx4 with bit 7 set causes the following things to occur :
@ -189,6 +192,14 @@ void GbNoiseChannel::Write(uint16_t addr, uint8_t value)
_state.Timer += (_state.Divisor == 1) ? 4 : -4;
}
//Channel volume is reloaded from NRx2.
_state.Volume = _state.EnvVolume;
if(_state.Enabled) {
//Immediately update output if channel was enabled
UpdateOutput();
}
//Channel is enabled, if volume is not 0 or raise volume flag is set
_state.Enabled = _state.EnvRaiseVolume || _state.EnvVolume > 0;
@ -204,12 +215,14 @@ void GbNoiseChannel::Write(uint16_t addr, uint8_t value)
//Volume envelope timer is reloaded with period.
_state.EnvTimer = _state.EnvPeriod;
_state.EnvStopped = false;
//Channel volume is reloaded from NRx2.
_state.Volume = _state.EnvVolume;
}
_apu->ProcessLengthEnableFlag(value, _state.Length, _state.LengthEnabled, _state.Enabled);
if(!_state.Enabled && prevEnabled) {
_state.Output = 0;
UpdateOutput();
}
break;
}
}

View file

@ -92,7 +92,15 @@ void GbSquareChannel::ClockLengthCounter()
void GbSquareChannel::UpdateOutput()
{
_state.Output = _dutySequences[_state.Duty][(_state.DutyPos - 1) & 0x07] * _state.Volume;
if(!_state.Enabled) {
return;
}
if(_state.FirstStep) {
_state.Output = 0;
} else {
_state.Output = _dutySequences[_state.Duty][(_state.DutyPos - 1) & 0x07] * _state.Volume;
}
}
void GbSquareChannel::ClockEnvelope()
@ -147,6 +155,7 @@ void GbSquareChannel::Exec(uint32_t clocksToRun)
if(_state.Timer == 0) {
_state.Timer = (2048 - _state.Frequency) * 4;
_state.DutyPos = (_state.DutyPos + 1) & 0x07;
_state.FirstStep = false;
UpdateOutput();
}
}
@ -214,6 +223,7 @@ void GbSquareChannel::Write(uint16_t addr, uint8_t value)
break;
case 4: {
bool prevEnabled = _state.Enabled;
_state.Frequency = (_state.Frequency & 0xFF) | ((value & 0x07) << 8);
if(value & 0x80) {
@ -239,6 +249,11 @@ void GbSquareChannel::Write(uint16_t addr, uint8_t value)
//"Channel is enabled, if volume is not 0 or raise volume flag is set"
_state.Enabled = _state.EnvRaiseVolume || _state.EnvVolume > 0;
if(_state.Enabled && !prevEnabled) {
//Don't update output until first timer tick
//This is needed to get the correct volume in Daiku no Gen-san
_state.FirstStep = true;
}
//"If length counter is zero, it is set to 64 (256 for wave channel)."
if(_state.Length == 0) {
@ -274,6 +289,11 @@ void GbSquareChannel::Write(uint16_t addr, uint8_t value)
}
_apu->ProcessLengthEnableFlag(value, _state.Length, _state.LengthEnabled, _state.Enabled);
if(!_state.Enabled && prevEnabled) {
_state.Output = 0;
UpdateOutput();
}
break;
}
}
@ -286,5 +306,6 @@ void GbSquareChannel::Serialize(Serializer& s)
SV(_state.Length); SV(_state.LengthEnabled); SV(_state.Enabled); SV(_state.Timer); SV(_state.DutyPos); SV(_state.Output);
SV(_state.SweepNegateCalcDone); SV(_state.EnvStopped);
SV(_state.SweepUpdateDelay);
SV(_state.FirstStep);
SV(_dac);
}

View file

@ -81,6 +81,10 @@ double GbWaveChannel::GetOutput()
void GbWaveChannel::UpdateOutput()
{
if(!_state.Enabled) {
return;
}
if(_state.Volume) {
_state.Output = _state.SampleBuffer >> (_state.Volume - 1);
} else {
@ -159,6 +163,7 @@ void GbWaveChannel::Write(uint16_t addr, uint8_t value)
break;
case 4: {
bool prevEnabled = _state.Enabled;
_state.Frequency = (_state.Frequency & 0xFF) | ((value & 0x07) << 8);
if(value & 0x80) {
@ -189,6 +194,11 @@ void GbWaveChannel::Write(uint16_t addr, uint8_t value)
}
_apu->ProcessLengthEnableFlag(value, _state.Length, _state.LengthEnabled, _state.Enabled);
if(!_state.Enabled && prevEnabled) {
_state.Output = 0;
UpdateOutput();
}
break;
}
}

View file

@ -270,6 +270,7 @@ struct GbSquareState
bool LengthEnabled;
bool Enabled;
bool FirstStep;
uint8_t DutyPos;
uint8_t Output;
};

View file

@ -232,6 +232,7 @@ public struct GbSquareState
[MarshalAs(UnmanagedType.I1)] public bool LengthEnabled;
[MarshalAs(UnmanagedType.I1)] public bool Enabled;
[MarshalAs(UnmanagedType.I1)] public bool FirstStep;
public byte DutyPos;
public byte Output;
}