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;
}
};