mirror of
https://github.com/SourMesen/Mesen2.git
synced 2025-04-02 10:21:44 -04:00
304 lines
7.8 KiB
C++
304 lines
7.8 KiB
C++
#include "pch.h"
|
|
#include "NES/APU/NesApu.h"
|
|
#include "NES/APU/SquareChannel.h"
|
|
#include "NES/APU/TriangleChannel.h"
|
|
#include "NES/APU/NoiseChannel.h"
|
|
#include "NES/APU/DeltaModulationChannel.h"
|
|
#include "NES/APU/ApuFrameCounter.h"
|
|
#include "NES/NesCpu.h"
|
|
#include "NES/NesConsole.h"
|
|
#include "NES/NesTypes.h"
|
|
#include "NES/NesMemoryManager.h"
|
|
#include "NES/NesSoundMixer.h"
|
|
#include "Shared/Emulator.h"
|
|
#include "Utilities/Serializer.h"
|
|
|
|
NesApu::NesApu(NesConsole* console)
|
|
{
|
|
_region = ConsoleRegion::Auto;
|
|
_apuEnabled = true;
|
|
_needToRun = false;
|
|
|
|
_console = console;
|
|
_mixer = _console->GetSoundMixer();
|
|
_settings = _console->GetEmulator()->GetSettings();
|
|
|
|
_square1.reset(new SquareChannel(AudioChannel::Square1, _console, true));
|
|
_square2.reset(new SquareChannel(AudioChannel::Square2, _console, false));
|
|
_triangle.reset(new TriangleChannel(_console));
|
|
_noise.reset(new NoiseChannel(_console));
|
|
_dmc.reset(new DeltaModulationChannel(_console));
|
|
_frameCounter.reset(new ApuFrameCounter(_console));
|
|
|
|
_console->GetMemoryManager()->RegisterIODevice(_square1.get());
|
|
_console->GetMemoryManager()->RegisterIODevice(_square2.get());
|
|
_console->GetMemoryManager()->RegisterIODevice(_frameCounter.get());
|
|
_console->GetMemoryManager()->RegisterIODevice(_triangle.get());
|
|
_console->GetMemoryManager()->RegisterIODevice(_noise.get());
|
|
_console->GetMemoryManager()->RegisterIODevice(_dmc.get());
|
|
|
|
Reset(false);
|
|
}
|
|
|
|
NesApu::~NesApu()
|
|
{
|
|
}
|
|
|
|
void NesApu::SetRegion(ConsoleRegion region, bool forceInit)
|
|
{
|
|
//Finish the current apu frame before switching model
|
|
Run();
|
|
_frameCounter->SetRegion(region);
|
|
}
|
|
|
|
void NesApu::FrameCounterTick(FrameType type)
|
|
{
|
|
//Quarter & half frame clock envelope & linear counter
|
|
_square1->TickEnvelope();
|
|
_square2->TickEnvelope();
|
|
_triangle->TickLinearCounter();
|
|
_noise->TickEnvelope();
|
|
|
|
if(type == FrameType::HalfFrame) {
|
|
//Half frames clock length counter & sweep
|
|
_square1->TickLengthCounter();
|
|
_square2->TickLengthCounter();
|
|
_triangle->TickLengthCounter();
|
|
_noise->TickLengthCounter();
|
|
|
|
_square1->TickSweep();
|
|
_square2->TickSweep();
|
|
}
|
|
}
|
|
|
|
uint8_t NesApu::GetStatus()
|
|
{
|
|
uint8_t status = 0;
|
|
status |= _square1->GetStatus() ? 0x01 : 0x00;
|
|
status |= _square2->GetStatus() ? 0x02 : 0x00;
|
|
status |= _triangle->GetStatus() ? 0x04 : 0x00;
|
|
status |= _noise->GetStatus() ? 0x08 : 0x00;
|
|
status |= _dmc->GetStatus() ? 0x10 : 0x00;
|
|
status |= _console->GetCpu()->HasIrqSource(IRQSource::FrameCounter) ? 0x40 : 0x00;
|
|
status |= _console->GetCpu()->HasIrqSource(IRQSource::DMC) ? 0x80 : 0x00;
|
|
|
|
return status;
|
|
}
|
|
|
|
uint8_t NesApu::ReadRam(uint16_t addr)
|
|
{
|
|
//$4015 read
|
|
Run();
|
|
|
|
if(addr >= 0x4018 && !_console->GetNesConfig().EnableCpuTestMode) {
|
|
return _console->GetMemoryManager()->GetOpenBus();
|
|
}
|
|
|
|
switch(addr) {
|
|
case 0x4015: {
|
|
uint8_t status = GetStatus() | (_console->GetMemoryManager()->GetInternalOpenBus() & 0x20);
|
|
|
|
//Reading $4015 clears the Frame Counter interrupt flag.
|
|
_console->GetCpu()->ClearIrqSource(IRQSource::FrameCounter);
|
|
|
|
return status;
|
|
}
|
|
|
|
case 0x4018: return _square1->GetOutput() | (_square2->GetOutput() << 4);
|
|
case 0x4019: return _triangle->GetOutput() | (_noise->GetOutput() << 4);
|
|
case 0x401A: return _dmc->GetOutput();
|
|
|
|
default:
|
|
return _console->GetMemoryManager()->GetOpenBus();
|
|
}
|
|
}
|
|
|
|
uint8_t NesApu::PeekRam(uint16_t addr)
|
|
{
|
|
if(_console->GetEmulator()->IsEmulationThread()) {
|
|
//Only run the Apu (to catch up) if we're running this in the emulation thread (not 100% accurate, but we can't run the Apu from any other thread without locking)
|
|
Run();
|
|
}
|
|
return GetStatus();
|
|
}
|
|
|
|
void NesApu::WriteRam(uint16_t addr, uint8_t value)
|
|
{
|
|
//$4015 write
|
|
Run();
|
|
|
|
//Writing to $4015 clears the DMC interrupt flag.
|
|
//This needs to be done before setting the enabled flag for the DMC (because doing so can trigger an IRQ)
|
|
_console->GetCpu()->ClearIrqSource(IRQSource::DMC);
|
|
|
|
_square1->SetEnabled((value & 0x01) == 0x01);
|
|
_square2->SetEnabled((value & 0x02) == 0x02);
|
|
_triangle->SetEnabled((value & 0x04) == 0x04);
|
|
_noise->SetEnabled((value & 0x08) == 0x08);
|
|
_dmc->SetEnabled((value & 0x10) == 0x10);
|
|
}
|
|
|
|
void NesApu::GetMemoryRanges(MemoryRanges &ranges)
|
|
{
|
|
ranges.AddHandler(MemoryOperation::Read, 0x4015);
|
|
ranges.AddHandler(MemoryOperation::Read, 0x4018, 0x401A);
|
|
ranges.AddHandler(MemoryOperation::Write, 0x4015);
|
|
}
|
|
|
|
void NesApu::Run()
|
|
{
|
|
//Update framecounter and all channels
|
|
//This is called:
|
|
//-At the end of a frame
|
|
//-Before Apu registers are read/written to
|
|
//-When a DMC or FrameCounter interrupt needs to be fired
|
|
int32_t cyclesToRun = _currentCycle - _previousCycle;
|
|
|
|
while(cyclesToRun > 0) {
|
|
_previousCycle += _frameCounter->Run(cyclesToRun);
|
|
|
|
//Reload counters set by writes to 4003/4008/400B/400F after running the frame counter to allow the length counter to be clocked first
|
|
//This fixes the test "len_reload_timing" (tests 4 & 5)
|
|
_square1->ReloadLengthCounter();
|
|
_square2->ReloadLengthCounter();
|
|
_noise->ReloadLengthCounter();
|
|
_triangle->ReloadLengthCounter();
|
|
|
|
_square1->Run(_previousCycle);
|
|
_square2->Run(_previousCycle);
|
|
_noise->Run(_previousCycle);
|
|
_triangle->Run(_previousCycle);
|
|
_dmc->Run(_previousCycle);
|
|
}
|
|
}
|
|
|
|
void NesApu::SetNeedToRun()
|
|
{
|
|
_needToRun = true;
|
|
}
|
|
|
|
bool NesApu::NeedToRun(uint32_t currentCycle)
|
|
{
|
|
if(_dmc->NeedToRun() || _needToRun) {
|
|
//Need to run whenever we alter the length counters
|
|
//Need to run every cycle when DMC is running to get accurate emulation (CPU stalling, interaction with sprite DMA, etc.)
|
|
_needToRun = false;
|
|
return true;
|
|
}
|
|
|
|
uint32_t cyclesToRun = currentCycle - _previousCycle;
|
|
return _frameCounter->NeedToRun(cyclesToRun) || _dmc->IrqPending(cyclesToRun);
|
|
}
|
|
|
|
void NesApu::Exec()
|
|
{
|
|
_currentCycle++;
|
|
if(_currentCycle == NesSoundMixer::CycleLength - 1) {
|
|
EndFrame();
|
|
} else if(NeedToRun(_currentCycle)) {
|
|
Run();
|
|
}
|
|
}
|
|
|
|
void NesApu::EndFrame()
|
|
{
|
|
_dmc->ProcessClock();
|
|
Run();
|
|
_square1->EndFrame();
|
|
_square2->EndFrame();
|
|
_triangle->EndFrame();
|
|
_noise->EndFrame();
|
|
_dmc->EndFrame();
|
|
|
|
_mixer->PlayAudioBuffer(_currentCycle);
|
|
|
|
_currentCycle = 0;
|
|
_previousCycle = 0;
|
|
}
|
|
|
|
void NesApu::ProcessCpuClock()
|
|
{
|
|
if(_apuEnabled) {
|
|
Exec();
|
|
}
|
|
}
|
|
|
|
void NesApu::Reset(bool softReset)
|
|
{
|
|
_apuEnabled = true;
|
|
_currentCycle = 0;
|
|
_previousCycle = 0;
|
|
_square1->Reset(softReset);
|
|
_square2->Reset(softReset);
|
|
_triangle->Reset(softReset);
|
|
_noise->Reset(softReset);
|
|
_dmc->Reset(softReset);
|
|
_frameCounter->Reset(softReset);
|
|
}
|
|
|
|
void NesApu::Serialize(Serializer& s)
|
|
{
|
|
if(s.GetFormat() != SerializeFormat::Map) {
|
|
//End the Apu frame - makes it simpler to restore sound after a state reload
|
|
EndFrame();
|
|
}
|
|
|
|
SV(_square1);
|
|
SV(_square2);
|
|
SV(_triangle);
|
|
SV(_noise);
|
|
SV(_dmc);
|
|
SV(_frameCounter);
|
|
}
|
|
|
|
void NesApu::AddExpansionAudioDelta(AudioChannel channel, int16_t delta)
|
|
{
|
|
_mixer->AddDelta(channel, _currentCycle, delta);
|
|
}
|
|
|
|
void NesApu::SetApuStatus(bool enabled)
|
|
{
|
|
_apuEnabled = enabled;
|
|
}
|
|
|
|
bool NesApu::IsApuEnabled()
|
|
{
|
|
//Adding extra lines before/after NMI temporarely turns off the Apu
|
|
//This appears to result in less side-effects than spreading out the Apu's
|
|
//load over the entire PPU frame, like what was done before.
|
|
//This is most likely due to the timing of the Frame Counter & DMC IRQs.
|
|
return _apuEnabled;
|
|
}
|
|
|
|
ConsoleRegion NesApu::GetApuRegion(NesConsole* console)
|
|
{
|
|
ConsoleRegion region = console->GetRegion();
|
|
if(region == ConsoleRegion::Ntsc || region == ConsoleRegion::Dendy) {
|
|
//Dendy APU works with NTSC timings
|
|
return ConsoleRegion::Ntsc;
|
|
} else {
|
|
return region;
|
|
}
|
|
}
|
|
|
|
uint16_t NesApu::GetDmcReadAddress()
|
|
{
|
|
return _dmc->GetDmcReadAddress();
|
|
}
|
|
|
|
void NesApu::SetDmcReadBuffer(uint8_t value)
|
|
{
|
|
_dmc->SetDmcReadBuffer(value);
|
|
}
|
|
|
|
ApuState NesApu::GetState()
|
|
{
|
|
ApuState state;
|
|
state.Dmc = _dmc->GetState();
|
|
state.FrameCounter = _frameCounter->GetState();
|
|
state.Noise = _noise->GetState();
|
|
state.Square1 = _square1->GetState();
|
|
state.Square2 = _square2->GetState();
|
|
state.Triangle = _triangle->GetState();
|
|
return state;
|
|
}
|