625 lines
15 KiB
C++
625 lines
15 KiB
C++
#include "apu.h"
|
|
|
|
// Qt includes
|
|
#include <QDataStream>
|
|
|
|
// system includes
|
|
#include <algorithm>
|
|
|
|
// local includes
|
|
#include "nesemulator.h"
|
|
#include "emusettings.h"
|
|
|
|
Apu::Apu(NesEmulator &emu) :
|
|
QObject(&emu),
|
|
m_emu(emu),
|
|
m_dmc(*this),
|
|
m_nos(*this),
|
|
m_sq1(*this),
|
|
m_sq2(*this),
|
|
m_trl(*this),
|
|
m_lowPassFilter(0.815686), // 14 KHz
|
|
m_highPassFilter1(0.999835), // 90 Hz
|
|
m_highPassFilter2(0.996039) // 442 Hz
|
|
{
|
|
}
|
|
|
|
void Apu::hardReset()
|
|
{
|
|
m_regIoDb = 0;
|
|
m_regIoAddr = 0;
|
|
m_regAccessHappened = false;
|
|
m_regAccessW = false;
|
|
m_seqMode = false;
|
|
m_oddCycle = false;
|
|
m_cycleFt = 0;
|
|
m_cycleE = 4;
|
|
m_cycleF = 4;
|
|
m_cycleL = 4;
|
|
m_oddL = false;
|
|
m_checkIrq = false;
|
|
m_doEnv = false;
|
|
m_doLength = false;
|
|
|
|
m_sq1.apuSq1HardReset();
|
|
m_sq2.apuSq2HardReset();
|
|
m_nos.apuNosHardReset();
|
|
m_dmc.apuDmcHardReset();
|
|
m_trl.apuTrlHardReset();
|
|
|
|
m_irqEnabled = true;
|
|
m_irqFlag = false;
|
|
|
|
m_timer = 0.;
|
|
m_audioX = 0.;
|
|
m_audioX1 = 0.;
|
|
m_audioY = 0.;
|
|
m_audioYClocks = 0.;
|
|
|
|
m_highPassFilter1.reset();
|
|
m_highPassFilter2.reset();
|
|
m_lowPassFilter.reset();
|
|
}
|
|
|
|
void Apu::softReset()
|
|
{
|
|
m_regIoDb = 0;
|
|
m_regIoAddr = 0;
|
|
m_regAccessHappened = false;
|
|
m_regAccessW = false;
|
|
m_seqMode = false;
|
|
m_oddCycle = false;
|
|
m_cycleFt = 0;
|
|
m_cycleE = 4;
|
|
m_cycleF = 4;
|
|
m_cycleL = 4;
|
|
m_oddL = false;
|
|
m_checkIrq = false;
|
|
m_doEnv = false;
|
|
m_doLength = false;
|
|
|
|
m_irqEnabled = true;
|
|
m_irqFlag = false;
|
|
|
|
m_sq1.apuSq1SoftReset();
|
|
m_sq2.apuSq2SoftReset();
|
|
m_trl.apuTrlSoftReset();
|
|
m_nos.apuNosSoftReset();
|
|
m_dmc.apuDmcSoftReset();
|
|
}
|
|
|
|
quint8 Apu::_ioRead(const quint16 address)
|
|
{
|
|
static constexpr std::array<void (Apu::*)(), 0x20> apuRegReadFunc = []() {
|
|
std::array<void (Apu::*)(), 0x20> apuRegReadFunc {};
|
|
for(std::size_t i = 0; i < 0x20; i++)
|
|
apuRegReadFunc[i] = &Apu::blankAccess;
|
|
apuRegReadFunc[0x15] = &Apu::read4015;
|
|
apuRegReadFunc[0x16] = &Apu::read4016;
|
|
apuRegReadFunc[0x17] = &Apu::read4017;
|
|
return apuRegReadFunc;
|
|
}();
|
|
|
|
if(address >= 0x4020)
|
|
return m_emu.memory().board()->readEx(address);
|
|
else
|
|
{
|
|
m_regIoAddr = address & 0x1F;
|
|
m_regAccessHappened = true;
|
|
m_regAccessW = false;
|
|
(this->*apuRegReadFunc[m_regIoAddr])();
|
|
return m_regIoDb;
|
|
}
|
|
}
|
|
|
|
quint8 Apu::ioRead(const quint16 address)
|
|
{
|
|
auto result = _ioRead(address);
|
|
return result;
|
|
}
|
|
|
|
void Apu::ioWrite(const quint16 address, const quint8 value)
|
|
{
|
|
if(address >= 0x4020)
|
|
m_emu.memory().board()->writeEx(address, value);
|
|
else
|
|
{
|
|
m_regIoDb = value;
|
|
m_regIoAddr = address & 0x1F;
|
|
m_regAccessHappened = true;
|
|
m_regAccessW = true;
|
|
}
|
|
}
|
|
|
|
void Apu::blankAccess()
|
|
{
|
|
}
|
|
|
|
void Apu::onRegister4014()
|
|
{
|
|
if(!m_regAccessW)
|
|
return;
|
|
|
|
// oam dma
|
|
m_emu.dma().setOamAddress(m_regIoDb << 8);
|
|
m_emu.dma().assertOamDma();
|
|
}
|
|
|
|
void Apu::onRegister4015()
|
|
{
|
|
if(!m_regAccessW)
|
|
{
|
|
// on reads, do the effects we know
|
|
m_irqFlag = false;
|
|
m_emu.interrupts().removeFlag(Interrupts::IRQ_APU);
|
|
}
|
|
else
|
|
{
|
|
// do a normal write
|
|
m_sq1.apuSq1On4015();
|
|
m_sq2.apuSq2On4015();
|
|
m_nos.apuNosOn4015();
|
|
m_trl.apuTrlOn4015();
|
|
m_dmc.apuDmcOn4015();
|
|
}
|
|
}
|
|
|
|
void Apu::onRegister4016()
|
|
{
|
|
// Only writes accepted
|
|
if(m_regAccessW)
|
|
{
|
|
if(m_inputStrobe && m_regIoDb == 0)
|
|
m_emu.ports().updatePorts();
|
|
|
|
m_inputStrobe = m_regIoDb;
|
|
}
|
|
else
|
|
m_emu.ports().setPort0(m_emu.ports().port0() >> 1);
|
|
}
|
|
|
|
void Apu::onRegister4017()
|
|
{
|
|
if(m_regAccessW)
|
|
{
|
|
m_seqMode = (m_regIoDb & 0x80) != 0;
|
|
m_irqEnabled = (m_regIoDb & 0x40) == 0;
|
|
|
|
// Reset counters
|
|
m_cycleE = -1;
|
|
m_cycleL = -1;
|
|
m_cycleF = -1;
|
|
m_oddL = false;
|
|
// Clock immediately ?
|
|
m_doLength = m_seqMode;
|
|
m_doEnv = m_seqMode;
|
|
// Reset irq
|
|
m_checkIrq = false;
|
|
|
|
if(!m_irqEnabled)
|
|
{
|
|
m_irqFlag = false;
|
|
m_emu.interrupts().removeFlag(Interrupts::IRQ_APU);
|
|
}
|
|
}
|
|
else
|
|
m_emu.ports().setPort1(m_emu.ports().port1() >> 1);
|
|
}
|
|
|
|
void Apu::read4015()
|
|
{
|
|
m_regIoDb = m_regIoDb & 0x20;
|
|
|
|
// Channels enable
|
|
m_sq1.apuSq1Read4015();
|
|
m_sq2.apuSq2Read4015();
|
|
m_nos.apuNosRead4015();
|
|
m_trl.apuTrlRead4015();
|
|
m_dmc.apuDmcRead4015();
|
|
|
|
// IRQ
|
|
if(m_irqFlag)
|
|
m_regIoDb = (m_regIoDb & 0xBF) | 0x40;
|
|
if(m_irqDeltaOccur)
|
|
m_regIoDb = (m_regIoDb & 0x7F) | 0x80;
|
|
}
|
|
|
|
void Apu::read4016()
|
|
{
|
|
m_regIoDb = m_emu.ports().port0() & 1;
|
|
}
|
|
|
|
void Apu::read4017()
|
|
{
|
|
m_regIoDb = m_emu.ports().port1() & 1;
|
|
}
|
|
|
|
void Apu::clock()
|
|
{
|
|
m_oddCycle = !m_oddCycle;
|
|
|
|
if(m_doEnv)
|
|
clockEnvelope();
|
|
|
|
if(m_doLength)
|
|
clockDuration();
|
|
|
|
if(!m_oddCycle)
|
|
{
|
|
// IRQ
|
|
m_cycleF++;
|
|
if(m_cycleF >= m_freqF)
|
|
{
|
|
m_cycleF = -1;
|
|
m_checkIrq = true;
|
|
m_cycleFt = 3;
|
|
}
|
|
|
|
// Envelope
|
|
m_cycleE++;
|
|
if(m_cycleE >= m_freqE)
|
|
{
|
|
m_cycleE = -1;
|
|
// Clock envelope and other units except when:
|
|
// 1 the seq mode is set
|
|
// 2 it is the time of irq check clock
|
|
if(m_checkIrq)
|
|
{
|
|
if(!m_seqMode)
|
|
{
|
|
// this is the 3rd step of mode 0, do a reset
|
|
m_doEnv = true;
|
|
}
|
|
else
|
|
{
|
|
// the next step will be the 4th step of mode 1
|
|
// so, shorten the step then do a reset
|
|
m_cycleE = 4;
|
|
}
|
|
}
|
|
else
|
|
m_doEnv = true;
|
|
}
|
|
|
|
// Length
|
|
m_cycleL++;
|
|
if(m_cycleL >= m_freqL)
|
|
{
|
|
m_oddL = !m_oddL;
|
|
|
|
m_cycleL = m_oddL ? -2 : -1;
|
|
|
|
// Clock duration and sweep except when:
|
|
// 1 the seq mode is set
|
|
// 2 it is the time of irq check clock
|
|
if(m_checkIrq && m_seqMode)
|
|
{
|
|
m_cycleL = 3730;// Next step will be after 7456 - 3730 = 3726 cycles, 2 cycles shorter than e freq
|
|
m_oddL = true;
|
|
}
|
|
else
|
|
{
|
|
m_doLength = true;
|
|
}
|
|
}
|
|
|
|
if(m_regAccessHappened)
|
|
{
|
|
m_regAccessHappened = false;
|
|
switch(m_regIoAddr)
|
|
{
|
|
case 0: m_sq1.apuOnRegister4000(); break;
|
|
case 1: m_sq1.apuOnRegister4001(); break;
|
|
case 2: m_sq1.apuOnRegister4002(); break;
|
|
case 3: m_sq1.apuOnRegister4003(); break;
|
|
case 4: m_sq2.apuOnRegister4004(); break;
|
|
case 5: m_sq2.apuOnRegister4005(); break;
|
|
case 6: m_sq2.apuOnRegister4006(); break;
|
|
case 7: m_sq2.apuOnRegister4007(); break;
|
|
case 8: m_trl.apuOnRegister4008(); break;
|
|
case 9: m_trl.apuOnRegister4009(); break;
|
|
case 10: m_trl.apuOnRegister400A(); break;
|
|
case 11: m_trl.apuOnRegister400B(); break;
|
|
case 12: m_nos.apuOnRegister400C(); break;
|
|
case 13: m_nos.apuOnRegister400D(); break;
|
|
case 14: m_nos.apuOnRegister400E(); break;
|
|
case 15: m_nos.apuOnRegister400F(); break;
|
|
case 16: m_dmc.apuOnRegister4010(); break;
|
|
case 17: m_dmc.apuOnRegister4011(); break;
|
|
case 18: m_dmc.apuOnRegister4012(); break;
|
|
case 19: m_dmc.apuOnRegister4013(); break;
|
|
case 20: onRegister4014(); break;
|
|
case 21: onRegister4015(); break;
|
|
case 22: onRegister4016(); break;
|
|
case 23: onRegister4017(); break;
|
|
case 24:
|
|
case 25:
|
|
case 26:
|
|
case 27:
|
|
case 28:
|
|
case 29:
|
|
case 30:
|
|
case 31: blankAccess(); break;
|
|
}
|
|
}
|
|
|
|
m_sq1.apuSq1Clock();
|
|
m_sq2.apuSq2Clock();
|
|
m_nos.apuNosClock();
|
|
|
|
if(m_emu.memory().board()->enableExternalSound())
|
|
m_emu.memory().board()->onApuClock();
|
|
|
|
//apuUpdatePlayback();
|
|
}
|
|
|
|
m_trl.apuTrlClock();
|
|
m_dmc.apuDmcClock();
|
|
|
|
if(m_emu.memory().board()->enableExternalSound())
|
|
m_emu.memory().board()->onApuClockSingle();
|
|
|
|
updatePlayback();
|
|
|
|
if(m_checkIrq)
|
|
{
|
|
if(!m_seqMode)
|
|
checkIrq();
|
|
|
|
// This is stupid ... :(
|
|
m_cycleFt--;
|
|
if(m_cycleFt == 0)
|
|
m_checkIrq = false;
|
|
}
|
|
}
|
|
|
|
void Apu::clockDuration()
|
|
{
|
|
m_sq1.apuSq1ClockLength();
|
|
m_sq2.apuSq2ClockLength();
|
|
m_nos.apuNosClockLength();
|
|
m_trl.apuTrlClockLength();
|
|
if(m_emu.memory().board()->enableExternalSound())
|
|
m_emu.memory().board()->onApuClockDuration();
|
|
m_doLength = false;
|
|
}
|
|
|
|
void Apu::clockEnvelope()
|
|
{
|
|
m_sq1.apuSq1ClockEnvelope();
|
|
m_sq2.apuSq2ClockEnvelope();
|
|
m_nos.apuNosClockEnvelope();
|
|
m_trl.apuTrlClockEnvelope();
|
|
if(m_emu.memory().board()->enableExternalSound())
|
|
m_emu.memory().board()->onApuClockEnvelope();
|
|
m_doEnv = false;
|
|
}
|
|
|
|
void Apu::checkIrq()
|
|
{
|
|
if(m_irqEnabled)
|
|
m_irqFlag = true;
|
|
if(m_irqFlag)
|
|
m_emu.interrupts().addFlag(Interrupts::IRQ_APU);
|
|
}
|
|
|
|
void Apu::updatePlayback()
|
|
{
|
|
static constexpr std::array<qreal, 32> audioPulseTable = []() constexpr {
|
|
std::array<qreal, 32> audioPulseTable {};
|
|
for(std::size_t i = 1; i < 32; i++)
|
|
audioPulseTable[i] = 95.52 / (8128. / i + 100.);
|
|
return audioPulseTable;
|
|
}();
|
|
|
|
static constexpr std::array<qreal, 204> audioTndTable = []() constexpr {
|
|
std::array<qreal, 204> audioTndTable {};
|
|
for(std::size_t i = 1; i < 204; i++)
|
|
audioTndTable[i] = 163.67 / (24329. / i + 100.);
|
|
return audioTndTable;
|
|
}();
|
|
|
|
// Collect the sample
|
|
m_pulseOut = audioPulseTable[m_sq1.output() + m_sq2.output()];
|
|
m_tndOut = audioTndTable[(3 * m_trl.output()) + (2 * m_nos.output()) + m_dmc.output()];
|
|
|
|
m_audioX = m_pulseOut + m_tndOut;
|
|
|
|
if(m_emu.memory().board()->enableExternalSound())
|
|
{
|
|
m_audioX += m_emu.memory().board()->apuGetSample();
|
|
m_audioX /= 2;
|
|
}
|
|
|
|
m_audioX *= EmuSettings::Audio::internalAmplitude;
|
|
|
|
// An implementation of the "Band-Limited Sound Synthesis" algorithm.
|
|
// http://www.slack.net/~ant/bl-synth
|
|
// Shay Green <hotpop.com@blargg> (swap to e-mail)
|
|
// No sure about this, it just add the sample difference in each step.
|
|
// Maybe here it is acting just like a low-pass filter ...
|
|
if(m_audioX != m_audioX1)
|
|
{
|
|
if(m_audioX > m_audioX1)
|
|
m_audioY += m_audioX - m_audioX1;
|
|
else
|
|
m_audioY += m_audioX1 - m_audioX;
|
|
|
|
m_audioX = m_audioX1;
|
|
}
|
|
|
|
m_audioYClocks++;
|
|
m_timer++;
|
|
|
|
const auto audiotimerratio = m_freqL * 2. * 120. / m_sampleRate;
|
|
|
|
if(m_timer >= audiotimerratio)
|
|
{
|
|
// Clock by output sample rate.
|
|
m_timer -= audiotimerratio;
|
|
|
|
// Get the sum of the samples
|
|
m_audioY /= m_audioYClocks;
|
|
|
|
// Do filtering, add 2 high-pass and one low-pass filters. See http://wiki.nesdev.com/w/index.php/APUMixer
|
|
double audioDcY;
|
|
audioDcY = m_highPassFilter2.doFiltering(m_audioY);// 442 Hz
|
|
audioDcY = m_highPassFilter1.doFiltering(audioDcY);// 90 Hz
|
|
audioDcY = m_lowPassFilter.doFiltering(audioDcY);// 14 KHz
|
|
audioDcY = std::clamp(audioDcY, double(-EmuSettings::Audio::internalPeekLimit), double(EmuSettings::Audio::internalPeekLimit));
|
|
|
|
m_samples.append(audioDcY);
|
|
|
|
m_audioY = 0;
|
|
m_audioYClocks = 0;
|
|
}
|
|
}
|
|
|
|
void Apu::writeState(QDataStream &dataStream) const
|
|
{
|
|
dataStream << m_regIoDb << m_regIoAddr << m_regAccessHappened << m_regAccessW << m_oddCycle << m_irqEnabled << m_irqFlag << m_irqDeltaOccur
|
|
<< m_seqMode << m_cycleF << m_cycleE << m_cycleL << m_oddL << m_cycleFt << m_checkIrq << m_doEnv << m_doLength << m_inputStrobe;
|
|
|
|
m_sq1.apuSq1WriteState(dataStream);
|
|
m_sq2.apuSq2WriteState(dataStream);
|
|
m_nos.apuNosWriteState(dataStream);
|
|
m_trl.apuTrlWriteState(dataStream);
|
|
m_dmc.apuDmcWriteState(dataStream);
|
|
}
|
|
|
|
void Apu::readState(QDataStream &dataStream)
|
|
{
|
|
dataStream >> m_regIoDb >> m_regIoAddr >> m_regAccessHappened >> m_regAccessW >> m_oddCycle >> m_irqEnabled >> m_irqFlag >> m_irqDeltaOccur
|
|
>> m_seqMode >> m_cycleF >> m_cycleE >> m_cycleL >> m_oddL >> m_cycleFt >> m_checkIrq >> m_doEnv >> m_doLength >> m_inputStrobe;
|
|
|
|
m_sq1.apuSq1ReadState(dataStream);
|
|
m_sq2.apuSq2ReadState(dataStream);
|
|
m_nos.apuNosReadState(dataStream);
|
|
m_trl.apuTrlReadState(dataStream);
|
|
m_dmc.apuDmcReadState(dataStream);
|
|
}
|
|
|
|
void Apu::flush()
|
|
{
|
|
m_timer = 0;
|
|
Q_EMIT samplesFinished(m_samples);
|
|
m_samples.clear();
|
|
}
|
|
|
|
bool Apu::oddCycle() const
|
|
{
|
|
return m_oddCycle;
|
|
}
|
|
|
|
NesEmulator &Apu::emu()
|
|
{
|
|
return m_emu;
|
|
}
|
|
|
|
const NesEmulator &Apu::emu() const
|
|
{
|
|
return m_emu;
|
|
}
|
|
|
|
ApuDmc &Apu::dmc()
|
|
{
|
|
return m_dmc;
|
|
}
|
|
|
|
const ApuDmc &Apu::dmc() const
|
|
{
|
|
return m_dmc;
|
|
}
|
|
|
|
ApuNos &Apu::nos()
|
|
{
|
|
return m_nos;
|
|
}
|
|
|
|
const ApuNos &Apu::nos() const
|
|
{
|
|
return m_nos;
|
|
}
|
|
|
|
ApuSq1 &Apu::sq1()
|
|
{
|
|
return m_sq1;
|
|
}
|
|
|
|
const ApuSq1 &Apu::sq1() const
|
|
{
|
|
return m_sq1;
|
|
}
|
|
|
|
ApuSq2 &Apu::sq2()
|
|
{
|
|
return m_sq2;
|
|
}
|
|
|
|
const ApuSq2 &Apu::sq2() const
|
|
{
|
|
return m_sq2;
|
|
}
|
|
|
|
ApuTrl &Apu::trl()
|
|
{
|
|
return m_trl;
|
|
}
|
|
|
|
const ApuTrl &Apu::trl() const
|
|
{
|
|
return m_trl;
|
|
}
|
|
|
|
void Apu::setIrqDeltaOccur(bool irqDeltaOccur)
|
|
{
|
|
m_irqDeltaOccur = irqDeltaOccur;
|
|
}
|
|
|
|
bool Apu::regAccessW() const
|
|
{
|
|
return m_regAccessW;
|
|
}
|
|
|
|
quint8 Apu::regIoDb() const
|
|
{
|
|
return m_regIoDb;
|
|
}
|
|
|
|
void Apu::setRegIoDb(quint8 regIoDb)
|
|
{
|
|
m_regIoDb = regIoDb;
|
|
}
|
|
|
|
bool Apu::regAccessHappened() const
|
|
{
|
|
return m_regAccessHappened;
|
|
}
|
|
|
|
quint8 Apu::regIoAddr() const
|
|
{
|
|
return m_regIoAddr;
|
|
}
|
|
|
|
qint32 Apu::sampleRate() const
|
|
{
|
|
return m_sampleRate;
|
|
}
|
|
|
|
void Apu::setSampleRate(qint32 sampleRate)
|
|
{
|
|
m_sampleRate = sampleRate;
|
|
}
|
|
|
|
const std::array<std::array<quint8, 8>, 4> Apu::m_sqDutyCycleSequences {
|
|
std::array<quint8, 8> { 0, 1, 0, 0, 0, 0, 0, 0 }, // 12.5%
|
|
std::array<quint8, 8> { 0, 1, 1, 0, 0, 0, 0, 0 }, // 25.0%
|
|
std::array<quint8, 8> { 0, 1, 1, 1, 1, 0, 0, 0 }, // 50.0%
|
|
std::array<quint8, 8> { 1, 0, 0, 1, 1, 1, 1, 1 }, // 75.0% (25.0% negated)
|
|
};
|
|
|
|
const std::array<quint8, 32> Apu::m_sqDurationTable {
|
|
0x0A, 0xFE, 0x14, 0x02, 0x28, 0x04, 0x50, 0x06, 0xA0, 0x08, 0x3C, 0x0A, 0x0E, 0x0C, 0x1A, 0x0E,
|
|
0x0C, 0x10, 0x18, 0x12, 0x30, 0x14, 0x60, 0x16, 0xC0, 0x18, 0x48, 0x1A, 0x10, 0x1C, 0x20, 0x1E,
|
|
};
|