WinSPC Source
Quick Guide
Introduction
I'm writing this so, to make it easier for those who wish to
learn and experiment Snes9x's altered source code to create an SPC player.
Code seperation
I've tried to seperate code as best as I could but this is how
it seperates to :
Code Modules |
Windows specific |
Main |
WinSPC.* (Application)
Resource.h (app resources) |
GUI |
DlgWinSPC.* (Main Dialog)
DlgOptions.* (Options Dialog)
DlgSettings.* (Settings Dialog) |
Sound Device |
port.* (most code) |
Portable |
Emulation |
port.* (loops, timer)
apu.* (DSP)
apumem.h (Memory Map)
global.cpp (Global Variables)
spc700.* (SPC700) |
Mixing |
soundux.* |
As you can see it seperates alright, with the exception of port.* which is
more of a bridge between windows sound device and emulation.
How WinSPC uses
Snes9x's SPC code
Lets see where to begin. Well, the main concern is port.*, as it is the one
that uses both windows and snes9x's SPC code, so I'll start with it.
The first thing to understand is how sound works in windows (in this case not DirectSound
but WinMultiMedias wave devices), In windows a program starts by finding the audio
device we are going to use (this can be found in port.cpp in the function OpenSoundDevice),
a call to waveOutOpen, with the structures filled in according to the sound device
you want to open, they give you 3 options in how to handle playing audio with
your own mixer 1) Use messages to tell you when you have to mix another buffer,
2) Use a loop to poll the buffers to see if one becomes available (this can be
done from the main process or a seperate thread), 3) use a callback, in my case
I have used a callback which gets called everytime a buffer needs to be filled,
I chose this because 1) messages would be lagged, 2) Modal Dialog boxes in MFC
are easier to create but do not have an ability to have a loop and a thread is
more useful in a multiprocessor system, 3) a direct call to my function would
be much easier to implement and faster.
Now the callback function waveOutProcSPC, is where the emulation and mixing will
occur, it gets called every time a buffer needs to be refilled.
Playing a file would require prefilling the buffers and then sending each of them
to the WaveOut device, by calling waveOutPrepareHeader
to prepare them and waveOutWrite to send them.
Pausing is done by setting a global
variable, and there fore signalling your callback to stop mixing and calling waveOutReset
is supposed to do the same thing, but might not work with every system, so the
variable is more important.
Stoping is exactly the same except you reset what ever you are playing back to the start.
A few last notes about Windows audio. Windows audio is slow, the maximum buffers you will
get out of it is 4 buffers a second, the catch is that emulation for the SPC requires at
least mixing 100 times a second to sound correctly for the majority of the spcs out there.
I did a little work around for this, very simply, I just do a loop where I emulate for 1/100th
of a second and then mix a section of the buffer, I do this until the buffer is full,
with 4 buffers a second and needing to mix 100 times a second that is mixing 25 times
in each buffer.
Now that we have covered the more windows specific part, its now to move on to the more
emulation specific part.
The first place to start is in loadin the SPC files. I do this by first loading the SPC
data into backup variables, I then call a function RestoreSPC, which copies the backup
varibles into the actual variable that will be used in emulation, and calls a list of
other functions to prepare for emulation, so emulation is corrected for the SPC. The data
consists of SPC Registers, SPC RAM, DSP RAM, and ExtraRAM. (Examine LoadSPC)
Next thing to do is to Open the audio device, and setup the mixer variables and buffer
mixing variables. Opening the audio device is windows specific and is pretty much covered
already. The mixer variables are stored in a structure SoundStatus, which is the global
variable so, it has stored in it the audio settings. the buffer mixing variables
are bpb (bytes per buffer), bpm (bytes per mix), spm (samples per mix).
The reason that these variables must be set are because they coincide with the audio device
and need to know how mix the audio for it. (Examine OpenSoundDevice)
Next we will want to play audio but we first need to under stand how to mix it, since
windows requires we send the buffers at a constant rate. So for mixing lets examine
the code :
// Buffer Mixing loop, loops until the buffer is filled
// which is specified by MPB (Mixes per Buffer)
for(i=0;i<MPB;i++){
// Outer Emulation Loop, loops for every 32 cycles until
// 1/100th of a second of emulation has occured
for(int c=0;c<C32PM;c++){
// Inner Emulation Loop of 32 cycles
for(int ic=0;ic<32;ic++){
// Tell the SPC code to emulate one cycle
APU_EXECUTE1();
}
// Does the timer code
IAPU.TimerErrorCounter++;
DoTimer();
}
// Tell the SPC code to mix 100th of the buffer
S9xMixSamples((unsigned char *)(&wh[b].lpData[i*bpm]),spm);
}
OK, that is how mixing is done for WinSPC. When starting to play you do this for each buffer
and then send the buffer to the audio device. (Examine PlaySPC)
I'll take this moment to explain how to alter this loop to use in something like a DOS
emulator/player. In dos you would do emulation in the main loop, so you would just have a
plain loop with out any thing related to mixing, and the main loop would be checking the
keyboard (for example) to see if the user wants to exit, here is an example :
while(!Key[SCAN_ESCAPE]){
for(int ic=0;ic<32;ic++){
APU_EXECUTE1();
}
IAPU.TimerErrorCounter++;
DoTimer();
}
Now in DOS mixing would be done durring a hardware interrupt (similar to the windows callback)
in which a buffer is to be mixed again, so you simply call S9xMixSamples, with enough
to mix the number of buffers a second you are set up for.
That is pretty much as far as I feel like going, I am tired and sleepy, as I write this
so its best if I leave it as is.