NES APU synchronization problems

Discuss emulation of the Nintendo Entertainment System and Famicom.

Moderator: Moderators

Post Reply
LuckyLuke
Posts: 6
Joined: Fri Feb 14, 2020 9:00 am

NES APU synchronization problems

Post by LuckyLuke » Thu Mar 26, 2020 4:23 am

Hi, I'm writing a NES emulator, and I'm trying to implement the APU. I'm using SDL to communicate with the sound card, and I sync my emu with the audio thread by calling the main clock function inside the audio callback:

Code: Select all

void Game::UpdateStateSyncWithAudio()
{
	double cyclesPerBuffer = 5.369318 * 1000000 * 512 / 48000;
	double cyclesPerSample = cyclesPerBuffer / 512;

	double cyclesDone = 0.0;

	for (int i = 0; i < cyclesPerBuffer; i++)
	{
		GetInstance().nes.Clock();

		cyclesDone += 1.0;

		if (cyclesDone >= cyclesPerSample)
		{
			AudioSystem::GetInstance().EnqueueSample(GetInstance().nes.GetAudioSample());
			cyclesDone -= cyclesPerSample;
		}
	}
}
I calculate the cycles needed to fill a buffer with samples, 512 in my case, and then advance the emulation, sampling the APU output (only ulse wave 1 and 2 at the moment) when a sample period has elapsed.
PPU works fine, and all runs smoothly.
My APU kind of works, I can hear tunes coming from the speakers, even if I hear lots of pops and cracks. My main issue is with the length counter and the sweeper (I haven't written code for the envelope yet).

The main clock function is this:

Code: Select all

void Bus::Clock()      // TODO: CPU should be done with instruction before triggering NMI (or being suspended by DMA)
{
    ppu->Clock();   

    apu->Clock();      // APU clock == CPU clock / 2

    if (systemClockCount % 3 == 0)                    // CPU clock = PPU clock / 3, DMA and CPU share same clock
        if (DMAActive)                                // if DMA is active, suspend CPU
        {
            if (DMADummy)                             // dummy DMA cycle
            {
                if (systemClockCount / 3 % 2 == 0)    // another dummy cycle if CPU/DMA clock cycle is odd
                    DMADummy = false;
            }
            else
            {
                if (systemClockCount / 3 % 2 == 0)    // even CPU/DMA cycle: read data from system RAM
                    DMAData = Read(DMAAddress++);
                else                                  // odd CPU/DMA cycle: write data to OAM
                {
                    Write(0x2003, (uint8_t)(DMAAddress & 0xFF) - 1);
                    Write(0x2004, DMAData);
                    
                    if (DMAAddress >> 8 != DMAPage)   // if 256 bytes have been written, disable DMA
                        DMAActive = false;
                }
            }
        }
        else
            cpu->Clock();

    if (ppu->InterruptAsserted())    // check if PPU has triggered the vertical blank interrupt
    {
        ppu->AcknowledgeInterrupt();
        cpu->NMI();  
    }

    systemClockCount++;
}
So my APU clock function is called at the same "speed" of the PPU clock. Inside I have a section of code where I check whether the clock count is divisible by 3, so I get the CPU clock rate and increment a value which stores the frame counter * 2. If the counter reaches the quarter or half frame values I clock the relevant APU units (length counter, sweeper):

Code: Select all


if (systemClockCount % 3 == 0)  // frame counter is clocked every other CPU clock, but we keep double the frame counts (CPU clock resolution)
	{
		if (frameCounterMode == 0)  // 4-step sequence mode
		{
			if (doubleFrameCounter == 3728 * 2 + 1)
				;
			else if (doubleFrameCounter == 7456 * 2 + 1)
			{
				pulseWave1.lengthCounter.Clock(); 
				pulseWave1.sweeper.Clock();

				pulseWave2.lengthCounter.Clock();
				pulseWave2.sweeper.Clock();
			}
			else if (doubleFrameCounter == 11185 * 2 + 1)
				;
			else if (doubleFrameCounter == 14914 * 2 + 1)
			{
				pulseWave1.lengthCounter.Clock();
				pulseWave1.sweeper.Clock(); 

				pulseWave2.lengthCounter.Clock();
				pulseWave2.sweeper.Clock();
			}

			doubleFrameCounter++;

			if (doubleFrameCounter == 14915 * 2)
				doubleFrameCounter = 0;
		}
	}

same for 5-step sequence.
But the notes never stops, they change in frequency but play endlessly.
If I remove the if (systemClockCount % 3 == 0) , I clock the APU with the same frequency as the PPU, the notes stop, but are too long anyway.

This seems strange, am I missing something here?

Post Reply