diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj index f77c076bb..ee71d89e8 100644 --- a/Core/Core.vcxproj +++ b/Core/Core.vcxproj @@ -64,6 +64,7 @@ + diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters index 2af307f63..bda5512f7 100644 --- a/Core/Core.vcxproj.filters +++ b/Core/Core.vcxproj.filters @@ -2586,6 +2586,9 @@ NES\Mappers\Nintendo + + NES\Mappers\FDS + SMS diff --git a/Core/NES/Mappers/FDS/BaseFdsChannel.h b/Core/NES/Mappers/FDS/BaseFdsChannel.h index 0bdd4ad04..2cc886613 100644 --- a/Core/NES/Mappers/FDS/BaseFdsChannel.h +++ b/Core/NES/Mappers/FDS/BaseFdsChannel.h @@ -61,7 +61,6 @@ class BaseFdsChannel : public ISerializable _timer--; if(_timer == 0) { ResetTimer(); - if(_volumeIncrease && _gain < 32) { _gain++; } else if(!_volumeIncrease && _gain > 0) { diff --git a/Core/NES/Mappers/FDS/FDS_LUT_norm.h b/Core/NES/Mappers/FDS/FDS_LUT_norm.h new file mode 100644 index 000000000..902d81594 --- /dev/null +++ b/Core/NES/Mappers/FDS/FDS_LUT_norm.h @@ -0,0 +1,66 @@ +constexpr float FDS_LUT_norm[64] = { + 0.0, + 0.011465572752058506, + 0.0233255997300148, + 0.03682375326752663, + 0.04673049971461296, + 0.061058409512043, + 0.07386837154626846, + 0.09098339825868607, + 0.09331251680850983, + 0.10951442271471024, + 0.12239760905504227, + 0.1415075808763504, + 0.1483096331357956, + 0.16800136864185333, + 0.18295270204544067, + 0.20666064321994781, + 0.18730713427066803, + 0.20707780122756958, + 0.2199448049068451, + 0.2434738427400589, + 0.24558208882808685, + 0.26910510659217834, + 0.28437408804893494, + 0.312602698802948, + 0.29783013463020325, + 0.32291364669799805, + 0.33768296241760254, + 0.3677976429462433, + 0.36768317222595215, + 0.3981393277645111, + 0.4163309335708618, + 0.4529191851615906, + 0.3774969279766083, + 0.40523025393486023, + 0.41825342178344727, + 0.45081785321235657, + 0.44415655732154846, + 0.4759543240070343, + 0.4921776056289673, + 0.5303648114204407, + 0.4969009757041931, + 0.5296915769577026, + 0.5450936555862427, + 0.5845786333084106, + 0.576058030128479, + 0.6141541004180908, + 0.6341322660446167, + 0.6813235282897949, + 0.6059473752975464, + 0.6424090266227722, + 0.6577944159507751, + 0.7020274996757507, + 0.6884633898735046, + 0.7309963703155518, + 0.7510766983032227, + 0.802958607673645, + 0.7523477077484131, + 0.7961556911468506, + 0.8153874278068542, + 0.869225800037384, + 0.8552623987197876, + 0.9073793292045593, + 0.9342948198318481, + 1.0 +}; diff --git a/Core/NES/Mappers/FDS/FdsAudio.cpp b/Core/NES/Mappers/FDS/FdsAudio.cpp index 0dd4ee81e..34bfaa960 100644 --- a/Core/NES/Mappers/FDS/FdsAudio.cpp +++ b/Core/NES/Mappers/FDS/FdsAudio.cpp @@ -8,6 +8,7 @@ #include "NES/APU/NesApu.h" #include "NES/APU/BaseExpansionAudio.h" #include "Utilities/Serializer.h" +#include "FDS_LUT_norm.h" void FdsAudio::Serialize(Serializer& s) { @@ -16,12 +17,12 @@ void FdsAudio::Serialize(Serializer& s) SVArray(_waveTable, 64); SV(_volume); SV(_mod); - SV(_waveWriteEnabled); SV(_disableEnvelopes); SV(_haltWaveform); SV(_masterVolume); SV(_waveOverflowCounter); SV(_wavePitch); SV(_wavePosition); SV(_lastOutput); + SV(_waveWriteEnabled); SV(_disableEnvelopes); SV(_haltWaveform); SV(_masterVolume); SV(_waveAccumulator); SV(_waveM2Counter); SV(_wavePitch); SV(_wavePosition); SV(_lastOutput); SV(_lastGain); } void FdsAudio::ClockAudio() { - int frequency = _volume.GetFrequency(); + uint16_t frequency = _volume.GetFrequency(); if(!_haltWaveform && !_disableEnvelopes) { _volume.TickEnvelope(); if(_mod.TickEnvelope()) { @@ -29,31 +30,42 @@ void FdsAudio::ClockAudio() } } - if(_mod.TickModulator()) { + // TODO: check if modulator and wave units are ticked on the same M2 cycle + if(_mod.TickModulator(_haltWaveform)) { //Modulator was ticked, update wave pitch _mod.UpdateOutput(frequency); } - if(_haltWaveform) { - _wavePosition = 0; - UpdateOutput(); - } else { - UpdateOutput(); + if(++_waveM2Counter == 16) { + if(_haltWaveform) { + _waveAccumulator = 0; + } else { + + if(!_waveWriteEnabled) { + _waveAccumulator += (frequency * _mod.GetOutput()) & 0xFFFFF; + if(_waveAccumulator > 0xFFFFFF) { + _waveAccumulator -= 0x1000000; + } - if(frequency + _mod.GetOutput() > 0 && !_waveWriteEnabled) { - _waveOverflowCounter += frequency + _mod.GetOutput(); - if(_waveOverflowCounter < frequency + _mod.GetOutput()) { - _wavePosition = (_wavePosition + 1) & 0x3F; + _wavePosition = (_waveAccumulator >> 18) & 0x3F; } } + _waveM2Counter = 0; } + UpdateOutput(); } void FdsAudio::UpdateOutput() { - uint32_t level = std::min((int)_volume.GetGain(), 32) * WaveVolumeTable[_masterVolume]; - uint8_t outputLevel = (_waveTable[_wavePosition] * level) / 1152; + // "Changes to the volume envelope only take effect while the wavetable + // pointer (top 6 bits of wave accumulator) is 0." + if(_wavePosition == 0) { + _lastGain = _volume.GetGain(); + } + uint8_t level = std::min(_lastGain, uint8_t(32)); + // volume level is PWM, but can be approximated linearly + uint8_t outputLevel = uint8_t(DACTable[_waveTable[_wavePosition]][_masterVolume] * float(level)); if(_lastOutput != outputLevel) { _console->GetApu()->AddExpansionAudioDelta(AudioChannel::FDS, outputLevel - _lastOutput); @@ -63,6 +75,15 @@ void FdsAudio::UpdateOutput() FdsAudio::FdsAudio(NesConsole* console) : BaseExpansionAudio(console) { + // initialize DAC LUT + // data comes from plgDavid's DC capture of an FDS's DAC output + // data capture shared from the NESDev server + // TODO: generate data based from FDS decap DAC schematics + for(int masterlevel = 0; masterlevel < 4; masterlevel++) { + for(int wavelevel = 0; wavelevel < 64; wavelevel++) { + DACTable[wavelevel][masterlevel] = (FDS_LUT_norm[wavelevel] * 64.0 * float(WaveVolumeTable[masterlevel])) / 1152.0; + } + } } uint8_t FdsAudio::ReadRegister(uint16_t addr) @@ -79,9 +100,29 @@ uint8_t FdsAudio::ReadRegister(uint16_t addr) } else if(addr == 0x4090) { value &= 0xC0; value |= _volume.GetGain(); + } else if(addr == 0x4091) { + // Wave accumulator + value &= 0xC0; + value |= (_waveAccumulator >> 12) & 0xFF; } else if(addr == 0x4092) { value &= 0xC0; value |= _mod.GetGain(); + } else if(addr == 0x4093) { + // Mod accumulator + value &= 0xC0; + value |= (_mod.GetModAccumulator() >> 5) & 0x7F; + } else if(addr == 0x4094) { + // wave pitch intermediate result + value &= 0xC0; + value |= (_mod.GetOutput() >> 4) & 0xFF; + } else if(addr == 0x4096) { + // wavetable position + value &= 0xC0; + value |= _wavePosition & 0x3F; + } else if(addr == 0x4097) { + // mod counter value + value &= 0xC0; + value |= _mod.GetCounter() & 0x7F; } return value; @@ -150,8 +191,7 @@ void FdsAudio::GetMapperStateEntries(vector& entries) entries.push_back(MapperStateEntry("$4082/3.0-11", "Frequency", _volume.GetFrequency(), MapperStateValueType::Number16)); entries.push_back(MapperStateEntry("$4083.6", "Volume/Mod Envelopes Disabled", _disableEnvelopes, MapperStateValueType::Bool)); - //todo emulation logic + this based on new info - //entries.push_back(MapperStateEntry("$4083.7", "Halt Wave Form", _haltWaveform, MapperStateValueType::Bool)); + entries.push_back(MapperStateEntry("$4083.7", "Halt Wave Form", _haltWaveform, MapperStateValueType::Bool)); entries.push_back(MapperStateEntry("", "Gain", _volume.GetGain(), MapperStateValueType::Number8)); @@ -165,10 +205,9 @@ void FdsAudio::GetMapperStateEntries(vector& entries) entries.push_back(MapperStateEntry("$4086/7.0-11", "Frequency", _mod.GetFrequency(), MapperStateValueType::Number16)); - //todo emulation logic + this based on new info - //entries.push_back(MapperStateEntry("$4087.6", "???", false, MapperStateValueType::Bool)); + entries.push_back(MapperStateEntry("$4087.6", "Force Tick Modulator", _mod.GetForceCarryOut(), MapperStateValueType::Bool)); - entries.push_back(MapperStateEntry("$4087.7", "Disabled", _mod.IsModulationDisabled(), MapperStateValueType::Bool)); + entries.push_back(MapperStateEntry("$4087.7", "Counter Disabled", _mod.IsModulationCounterDisabled(), MapperStateValueType::Bool)); entries.push_back(MapperStateEntry("", "Gain", _mod.GetGain(), MapperStateValueType::Number8)); entries.push_back(MapperStateEntry("", "Mod Output", std::to_string(_mod.GetOutput()))); diff --git a/Core/NES/Mappers/FDS/FdsAudio.h b/Core/NES/Mappers/FDS/FdsAudio.h index abc116586..df9f74467 100644 --- a/Core/NES/Mappers/FDS/FdsAudio.h +++ b/Core/NES/Mappers/FDS/FdsAudio.h @@ -13,6 +13,7 @@ class FdsAudio : public BaseExpansionAudio { private: const uint32_t WaveVolumeTable[4] = { 36, 24, 17, 14 }; + float DACTable[64][4] = {}; //Register values uint8_t _waveTable[64] = {}; @@ -27,11 +28,13 @@ class FdsAudio : public BaseExpansionAudio uint8_t _masterVolume = 0; //Internal values - uint16_t _waveOverflowCounter = 0; + uint32_t _waveAccumulator = 0; //24-bit accumulator + uint8_t _waveM2Counter = 0; int32_t _wavePitch = 0; uint8_t _wavePosition = 0; uint8_t _lastOutput = 0; + uint8_t _lastGain = 0; protected: void Serialize(Serializer& s) override; diff --git a/Core/NES/Mappers/FDS/ModChannel.h b/Core/NES/Mappers/FDS/ModChannel.h index 4374ed88e..bafa219a8 100644 --- a/Core/NES/Mappers/FDS/ModChannel.h +++ b/Core/NES/Mappers/FDS/ModChannel.h @@ -5,24 +5,39 @@ class ModChannel : public BaseFdsChannel { private: - const int32_t ModReset = 0xFF; - const int32_t _modLut[8] = { 0,1,2,4,ModReset,-4,-2,-1 }; + const int16_t ModReset = 0xFF; + const int16_t _modLut[8] = { 0,1,2,4,ModReset,-4,-2,-1 }; int8_t _counter = 0; - bool _modulationDisabled = false; + bool _modCounterDisabled = false; + bool _forceCarryOut = false; - uint8_t _modTable[64] = {}; + uint32_t _modAccumulator = 0; //18-bit accumulator + uint8_t _modM2Counter = 0; + uint8_t _modTable[32] = {}; uint8_t _modTablePosition = 0; - uint16_t _overflowCounter = 0; - int32_t _output = 0; + uint8_t _output = 0; protected: void Serialize(Serializer& s) override { BaseFdsChannel::Serialize(s); - SVArray(_modTable, 64); - SV(_counter); SV(_modulationDisabled); SV(_modTablePosition); SV(_overflowCounter); SV(_output); + SVArray(_modTable, 32); + SV(_counter); SV(_modCounterDisabled); SV(_forceCarryOut); SV(_modTablePosition); SV(_modAccumulator); SV(_modM2Counter); SV(_output); + } + + void IncrementAccumulator(uint32_t value) + { + _modAccumulator += value; + if(_modAccumulator > 0x3FFFF) { + _modAccumulator -= 0x40000; + } + } + + void UpdateModPosition() + { + _modTablePosition = (_modAccumulator >> 13) & 0x1F; } public: @@ -38,93 +53,102 @@ class ModChannel : public BaseFdsChannel break; case 0x4087: BaseFdsChannel::WriteReg(addr, value); - _modulationDisabled = (value & 0x80) == 0x80; - if(_modulationDisabled) { - _overflowCounter = 0; + _modCounterDisabled = (value & 0x80) == 0x80; + // "4087.6 forces a carry out from bit 11." + _forceCarryOut = (value & 0x40) == 0x40; + if(_modCounterDisabled) { + // "Bits 0-12 are reset by 4087.7=1. Bits 13-17 have no reset." + _modAccumulator &= 0x3E000; } break; } } + bool TickEnvelope() + { + if(!_envelopeOff && _masterSpeed > 0) { + _timer--; + if(_timer == 0) { + ResetTimer(); + + if(_volumeIncrease && _gain < 32) { + _gain++; + } else if(!_volumeIncrease && _gain > 0) { + _gain--; + } + return true; + } + } + return false; + } + void WriteModTable(uint8_t value) { //"This register has no effect unless the mod unit is disabled via the high bit of $4087." - if(_modulationDisabled) { - _modTable[_modTablePosition & 0x3F] = value & 0x07; - _modTable[(_modTablePosition + 1) & 0x3F] = value & 0x07; - _modTablePosition = (_modTablePosition + 2) & 0x3F; + if(_modCounterDisabled) { + // "Writing $4088 increments the address (bits 13-17) when 4087.7=1." + _modTable[_modTablePosition] = value & 0x07; + IncrementAccumulator(0x2000); + UpdateModPosition(); } } void UpdateCounter(int8_t value) { - _counter = value; - if(_counter >= 64) { - _counter -= 128; - } else if(_counter < -64) { - _counter += 128; + // "The mod table counter is stopped, that's all. + // The freq mod formula is ALWAYS in effect, 4084/4085 still modify the wave frequency." + if(!_modCounterDisabled) { + _counter = value; + if(_counter >= 64) { + _counter -= 128; + } else if(_counter < -64) { + _counter += 128; + } } } bool IsEnabled() { - return !_modulationDisabled && _frequency > 0; + return _frequency > 0; } - bool TickModulator() + bool TickModulator(bool haltWaveform) { - if(IsEnabled()) { - _overflowCounter += _frequency; - - if(_overflowCounter < _frequency) { - //Overflowed, tick the modulator - int32_t offset = _modLut[_modTable[_modTablePosition]]; + // $4083.7 also stops the mod table accumulator + if(IsEnabled() && !haltWaveform && ++_modM2Counter == 16) { + IncrementAccumulator(_frequency); + + // "On a carry out from bit 11, update the mod counter (increment $4085 with modtable)." + // "4087.6 forces a carry out from bit 11." + if((_modAccumulator & 0xFFF) < _frequency || _forceCarryOut) { + int16_t offset = _modLut[_modTable[_modTablePosition]]; UpdateCounter(offset == ModReset ? 0 : _counter + offset); - - _modTablePosition = (_modTablePosition + 1) & 0x3F; - - return true; + UpdateModPosition(); } + + _modM2Counter = 0; + return true; } return false; } void UpdateOutput(uint16_t volumePitch) { - //Code from NesDev Wiki - + // code from new info by loopy + // https://forums.nesdev.org/viewtopic.php?p=232662#p232662 // pitch = $4082/4083 (12-bit unsigned pitch value) // counter = $4085 (7-bit signed mod counter) // gain = $4084 (6-bit unsigned mod gain) - // 1. multiply counter by gain, lose lowest 4 bits of result but "round" in a strange way int32_t temp = _counter * _gain; - int32_t remainder = temp & 0xF; - temp >>= 4; - if(remainder > 0 && (temp & 0x80) == 0) { - temp += _counter < 0 ? -1 : 2; - } - - // 2. wrap if a certain range is exceeded - if(temp >= 192) { - temp -= 256; - } else if(temp < -64) { - temp += 256; - } - - // 3. multiply result by pitch, then round to nearest while dropping 6 bits - temp = volumePitch * temp; - remainder = temp & 0x3F; - temp >>= 6; - if(remainder >= 32) { - temp += 1; - } - - // final mod result is in temp + if((temp & 0x0f) && !(temp & 0x800)) + temp += 0x20; + temp += 0x400; + temp = (temp >> 4) & 0xff; _output = temp; } - int32_t GetOutput() + uint8_t GetOutput() { return _output; } @@ -134,8 +158,18 @@ class ModChannel : public BaseFdsChannel return _counter; } - bool IsModulationDisabled() + uint32_t GetModAccumulator() + { + return _modAccumulator; + } + + bool GetForceCarryOut() + { + return _forceCarryOut; + } + + bool IsModulationCounterDisabled() { - return _modulationDisabled; + return _modCounterDisabled; } };