Writing a ZX Spectrum Emulator – Part 1: The Beeper


Writing a ZX Spectrum Emulator

Part 1: The Beeper

By Adrian Brown

 

On the ZXAsm chat channel recently there was a lot of discussion about writing spectrum emulators. Having written a very basic one many (around 20) years ago, I thought it was about time to try and do one properly. While there seems to be plenty of information around about the hardware and aspects of writing emulators, I found there were gaps missing. Ive decided to write these blogs about my work on this topic. Feel free to comment or email me (adrian@zxasm.net) about them, im not saying what im writing is the best way to do things or even correct, this is what I have found as im going along.

I will cover the main flow in more detail in future blogs, but for now we will use the pseudo code

int tstates = 0;
int time = 0;

while (true)
{
    if ((timeGetTime() - time) > (1000 / 50))
    {
        // We want to run at 50fps
        time = timeGetTime();

        // Loop through a frames worth of t-states
        while (tstates < 69888)
        {
            // Execute one instruction
            int inst_tstates = emulator.Execute();

            // Update the display
            emulator.UpdateDisplay(inst_tstates);

            // Update the audio
            emulator.UpdateAudio(inst_tstates, port254);

            tstates += inst_tstates;
        }

        // Signal an interrupt
        emulator.Interrupt();
        tstates -= 69888;
    }
}

This simply runs an instruction, then updates the display and the audio. For this blog we will be looking at the UpdateAudio function.

 

The Beeper port

To produce audio on the spectrum, programmers must alter bit 4 of port 254 between 0 and 1 at the correct rate for the tone required, this has the effect of creating a square wave sound. The quicker you alternate between 0 and 1, the higher the frequency. To emulate the audio we must create this square wave on the computer based on the sample rate of our audio system.
On most computers audio playback is generated at 44100 samples per second with each sample being 16bit. Given the spectrum runs at 50 frames per second, this gives us 44100 / 50 = 882 samples per frame. We know that the spectrum 48k has 69888 T-States per frame, so an audio sample must be generated every 69888/882 = 79 t-states (rounded down).

The quick and dirty method.
The quick method simply counts how many t-states have passed and when we get up to (or over) 79, we write out either 0 or 32767 based on the current state of bit 4 of port 254.

void CEmulator::UpdateAudio(int t_states, unsigned char port254)
{
    // Count up how many t-states have passed
    m_nAudioTStates += t_states;

    if ( m_nAudioTStates >= 79 )
    {
        // Decrease down again
        m_nAudioTStates -= 79;

        // Write the next value out
        pAudioBuffer[nWritePosition++] = ((port254 & 0x10) == 0) ? 0 : 32767

        // See if we have finished writing this frames buffer
        if ( nWritePosition == 882 )
        {
            // Play the audio buffer
            PlayBuffer(pAudioBuffer);
            nWritePosition = 0;
        }
    }
}

This code doesn’t go into details of HOW to play the buffer, just how to create the values in the buffer. There is a counter that we increase by the number of t-states that have passed, when this goes over the magic 79 we write out the next audio sample. In this code I write either 0 or 32767, you can alter the 32767 to a different value to adjust the volume. While this will work and sounds something like you are expecting, the problem is the audio bit of port 254 may have altered several times in those 79 t-states. Its possible that its gone from 0 to 1 and back to 0, yet we will have not produced any tone.
Its worth mentioning that hard coding these values (79, 882 etc) is never a good idea. I generally calculate these into variables. The reason for this is things like the number of t-states per frame and the number of samples per second could alter, using variables allows you to take these into account. I am using the hardcoded values to try and make it easier to follow.

 

The average method

Previously we looked at the quick method, but to fix the issues we need to deal with all the changes that may occur during the 79 t-states. The best method I have found so far is to calculate the average. To do this, every time we enter UpdateAudio first check if this number of t-states will push us over the magic 79. If it doesn’t we can simply add this number to 1’s counter. If it does then we finish adding up the 1’s for this step and then divide this final count by the number of t-states. Eg. If for 40 t-states the bit is 1 and for the remaining 39 its 0, we could output 40 / 79 = 0.506 * 32767 = 16591 instead of the 32767. This average will give a much better approximation of the sound. The code for this is below.

void CEmulator::UpdateAudio(int t_states, unsigned char port254)
{
    // See if this will push us over the number of t-states needed for the beeper
    if ((m_nAudioTStates + t_states) > 79)
    {
        // First write the number needed for this step
        m_nAudioValue += ((port254 & 0x10) == 0) ? 0 : (79 - m_nAudioTStates);

        // Update the audio core
        pAudioBuffer[nWritePosition++] = ((m_nAudioValue * 32767) / 79);

        // See if we have finished writing this frames buffer
        if ( nWritePosition == 882 )
        {
            // Play the audio buffer
            PlayBuffer(pAudioBuffer);

            nWritePosition = 0;
        }

        // Reset the counter
        t_states = (m_nAudioTStates + t_states) - 79;
        m_nAudioValue = 0;
        m_nAudioTStates = 0;
    }

    m_nAudioValue += ((port254 & 0x10) == 0) ? 0 : t_states;
    m_nAudioTStates += t_states;
}

One thing to notice is that in the code we multiply by 32767 before dividing by 79, this means we don’t have to worry about floating point numbers.

Next steps…

This code will play the audio fine, however it is not as efficient as it could be. The trouble is we are processing this code a lot when it might not need to be. The alteration I am looking to do next is to only handle the audio updates when the port254 bit alters. The idea I will be coding is to call the update function when the value changes from 0 to 1 or vice versa, then we can handle how many t-states have passed and write the correct number of samples. To do this we may also need to update and close off a frames worth (882 samples) at the end of a frame.
Ill look to document this when I alter the code.

If you have any questions or suggestions etc, then as mentioned comment or email me… Next time Ill look at the 128k AY Sound chip.