First stab at adding Mockingboard support.

Currently only supports one Mockingboard in slot 4, but it should be
fairly trivial to add another in slot 5.  Tested with Ultima 3, 4 & 5,
and Mockingboard disk #1.  Also, added some fixes to correct the timing
of the 6502 and sound; I believe I have a good understanding of it now,
even though there's still work to do to keep the main CPU thread from
starving the audio thread (which still happens, but less often now).
This commit is contained in:
Shamus Hammons 2018-09-09 22:47:51 -05:00
parent 01d41f5b9d
commit 92fbd44509
17 changed files with 1347 additions and 320 deletions

10
.gitignore vendored
View File

@ -1,7 +1,15 @@
apple2
apple2.log
*.log
*.zip
*.state*
*.bak
disks/
gmon.out
obj/
bugs/
reference/
src/gui/foooked/
ROMs/bin2c
ROMs/from-applewin/
res/
docs/

View File

@ -113,6 +113,7 @@ OBJS = \
obj/firmware.o \
obj/floppy.o \
obj/log.o \
obj/mos6522via.o \
obj/mmu.o \
obj/sdlemu_config.o \
obj/settings.o \

View File

@ -2,7 +2,7 @@
// Apple 2 SDL Portable Apple Emulator
//
// by James Hammons
// © 2017 Underground Software
// © 2018 Underground Software
//
// Parts loosely inspired by AppleWin by Tom Charlesworth which was based on
// AppleWin by Oliver Schmidt which was based on AppleWin by Michael O'Brien.
@ -24,11 +24,11 @@
// STILL TO DO:
//
// - Port to SDL [DONE]
// - GUI goodies
// - Weed out unneeded functions [DONE]
// - Disk I/O [DONE]
// - 128K IIe related stuff [DONE]
// - State loading/saving
// - State loading/saving [DONE]
// - GUI goodies
//
// BUGS:
//
@ -45,9 +45,11 @@
#include <stdlib.h>
#include <string>
#include <time.h>
#include "ay8910.h"
#include "firmware.h"
#include "floppy.h"
#include "log.h"
#include "mos6522via.h"
#include "mmu.h"
#include "settings.h"
#include "sound.h"
@ -75,6 +77,15 @@ FloppyDrive floppyDrive;
bool powerStateChangeRequested = false;
uint64_t frameCycleStart;
#if 0
uint32_t frameTicks = 0;
uint32_t frameTime[60];
#else
uint64_t frameTicks = 0;
uint64_t frameTime[60];
#endif
uint32_t frameTimePtr = 0;
// Exported variables
uint8_t lastKeyPressed = 0;
@ -94,11 +105,15 @@ bool dhires = false;
uint8_t lcState = 0x02;
static bool running = true; // Machine running state flag...
#if 0
static uint32_t startTicks;
#else
static uint64_t startTicks;
#endif
static bool pauseMode = false;
static bool fullscreenDebounce = false;
static bool capsLock = false;
static bool capsLockDebounce = false;
//static bool capsLock = false;
//static bool capsLockDebounce = false;
static bool resetKeyDown = false;
static int8_t hideMouseTimeout = 60;
@ -113,6 +128,7 @@ static uint8_t keyDelay = 0;
static void SaveApple2State(const char * filename);
static bool LoadApple2State(const char * filename);
static void ResetApple2State(void);
static void AppleTimer(uint16_t);
// Local timer callback functions
@ -129,12 +145,15 @@ static bool cpuFinished = false;
// NB: Apple //e Manual sez 6502 is running @ 1,022,727 Hz
// This is a lie. At the end of each 65 cycle line, there is an elongated
// cycle (the so-called 'long' cycle) that throws the calcs out of whack.
// So actually, it's supposed to be 1,020,484.32 Hz
// Let's try a thread...
//
// Here's how it works: Execute 1 frame's worth, then sleep. Other stuff wakes
// it up.
//
static uint32_t sampleCount;
static uint64_t sampleClock, lastSampleClock;
int CPUThreadFunc(void * data)
{
// Mutex must be locked for conditional to work...
@ -142,11 +161,15 @@ int CPUThreadFunc(void * data)
SDL_mutex * cpuMutex = SDL_CreateMutex();
#ifdef CPU_THREAD_OVERFLOW_COMPENSATION
float overflow = 0.0;
// float overflow = 0.0;
#endif
do
{
uint64_t cpuFrameTickStart = SDL_GetPerformanceCounter();
uint64_t oldClock = mainCPU.clock;
sampleCount = 0;
sampleClock = lastSampleClock = mainCPU.clock;
// decrement mainSem...
#ifdef THREAD_DEBUGGING
WriteLog("CPU: SDL_SemWait(mainSem);\n");
@ -163,43 +186,48 @@ WriteLog("CPU: SDL_SemWait(mainSem);\n");
#ifdef THREAD_DEBUGGING
WriteLog("CPU: Execute65C02(&mainCPU, cycles);\n");
#endif
// for(int i=0; i<800; i++)
for(int i=0; i<786; i++)
// for(int i=0; i<786; i++)
for(int i=0; i<262; i++)
{
uint32_t cycles = 21;
// overflow += 0.333333334;
overflow += 0.666666667;
// uint32_t cycles = 21;
// overflow += 0.666666667;
if (overflow > 1.0)
{
cycles++;
overflow -= 1.0;
}
// if (overflow > 1.0)
// {
// cycles++;
// overflow -= 1.0;
// }
// If the CTRL+Reset key combo is being held, make sure the RESET
// line stays asserted:
if (resetKeyDown)
mainCPU.cpuFlags |= V65C02_ASSERT_LINE_RESET;
Execute65C02(&mainCPU, cycles);
WriteSampleToBuffer();
// Execute65C02(&mainCPU, cycles);
Execute65C02(&mainCPU, 65);
// WriteSampleToBuffer();
// According to "Understanding The Apple IIe", VBL asserted after
// the last byte of the screen is read and let go on the first read
// of the first byte of the screen. We now know that the screen
// starts on line #6 and ends on line #197 (of the vertical
// counter--actual VBLANK happens on lines 230 thru 233).
vbl = ((i > 17) && (i < 592) ? true : false);
// vbl = ((i > 17) && (i < 592) ? true : false);
vbl = ((i >= 6) && (i <= 197) ? true : false);
}
WriteLog("*** Frame ran for %d cycles (%.3lf µs, %d samples).\n", mainCPU.clock - oldClock, ((double)(SDL_GetPerformanceCounter() - cpuFrameTickStart) * 1000000.0) / (double)SDL_GetPerformanceFrequency(), sampleCount);
// frameTicks = ((SDL_GetPerformanceCounter() - startTicks) * 1000) / SDL_GetPerformanceFrequency();
/*
Other timings from UTA2E:
Power-up reset 32.6 msec / 512 horizontal scans
Flash cycle 1.87 Hz / Vertical freq./32
Delay before auto repeat 534-801 msec / 32-48 vertical scans
Auto repeat frequency 15 Hz / Vertical freq./4
Vertical frequency 59.94 Hz
Horizontal frequency 15,734 Hz
1 NTSC frame = 17030 cycles
Vertical frequency 59.94 Hz (actually, 59.92 Hz [59.92274339401])
Horizontal frequency 15,734 Hz (actually, 15700 Hz)
1 NTSC frame = 17030 cycles (N.B.: this works out to 1021800 cycles per sec.)
NTSC clock frequency ("composite" freq.) is 1.02048432 MHz, which is 14.31818 x (65 / (65 x 14 + 2)) MHz.
1 line = 65 cycles
70 blank lines for top margin, 192 lines for screen, (35 & 35?)
VA-C,V0-5 is upcounter starting at 011111010 ($FA) to 111111111 ($1FF)
@ -297,7 +325,7 @@ bool LoadImg(char * filename, uint8_t * ram, int size)
}
const uint8_t stateHeader[19] = "APPLE2SAVESTATE1.0";
const uint8_t stateHeader[19] = "APPLE2SAVESTATE1.1";
static void SaveApple2State(const char * filename)
{
WriteLog("Main: Saving Apple2 state...\n");
@ -406,6 +434,7 @@ static bool LoadApple2State(const char * filename)
// Make sure things are in a sane state before execution :-P
mainCPU.RdMem = AppleReadMem;
mainCPU.WrMem = AppleWriteMem;
mainCPU.Timer = AppleTimer;
ResetMMUPointers();
return true;
@ -428,6 +457,14 @@ static void ResetApple2State(void)
dhires = false;
lcState = 0x02;
ResetMMUPointers();
ResetMBVIAs();
#ifdef USE_NEW_AY8910
AYReset(0);
AYReset(1);
#else
AY8910_reset(0);
AY8910_reset(1);
#endif
// Without this, you can wedge the system :-/
memset(ram, 0, 0x10000);
@ -436,6 +473,76 @@ static void ResetApple2State(void)
}
static double cyclesForSample = 0;
static void AppleTimer(uint16_t cycles)
{
// Handle PHI2 clocked stuff here...
bool via1T1HitZero = (mbvia[0].timer1counter <= cycles ? true : false);
bool via2T1HitZero = (mbvia[1].timer1counter <= cycles ? true : false);
mbvia[0].timer1counter -= cycles;
mbvia[0].timer2counter -= cycles;
mbvia[1].timer1counter -= cycles;
mbvia[1].timer2counter -= cycles;
if (via1T1HitZero)
{
if (mbvia[0].acr & 0x40)
{
mbvia[0].timer1counter += mbvia[0].timer1latch;
if (mbvia[0].ier & 0x40)
{
mbvia[0].ifr |= (0x80 | 0x40);
AssertLine(V65C02_ASSERT_LINE_IRQ);
}
}
else
{
mbvia[0].ier &= 0x3F; // Disable T1 interrupt (VIA #1)
}
}
if (via2T1HitZero)
{
if (mbvia[1].acr & 0x40)
{
mbvia[1].timer1counter += mbvia[1].timer1latch;
if (mbvia[1].ier & 0x40)
{
mbvia[1].ifr |= (0x80 | 0x40);
AssertLine(V65C02_ASSERT_LINE_NMI);
}
}
else
{
mbvia[1].ier &= 0x3F; // Disable T1 interrupt (VIA #2)
}
}
#if 1
// Handle sound
// 21.26009 cycles per sample @ 48000 (running @ 1,020,484.32 Hz)
// Noooooope. We need ~801 cycles per frame. Averaging about 786, so missing 15 or so.
// 16.688154500083 ms = 1 frame
cyclesForSample += (double)cycles;
if (cyclesForSample >= 21.26009)
{
#if 0
sampleClock = GetCurrentV65C02Clock();
WriteLog(" cyclesForSample = %lf (%d samples, cycles=%d)\n", cyclesForSample, sampleClock - lastSampleClock, cycles);
sampleCount++;
lastSampleClock = sampleClock;
#endif
WriteSampleToBuffer();
cyclesForSample -= 21.26009;
}
#endif
}
#ifdef CPU_CLOCK_CHECKING
uint8_t counter = 0;
uint32_t totalCPU = 0;
@ -501,10 +608,21 @@ int main(int /*argc*/, char * /*argv*/[])
SetupAddressMap();
ResetMMUPointers();
// Set up Mockingboard
memset(&mbvia[0], 0, sizeof(MOS6522VIA));
memset(&mbvia[1], 0, sizeof(MOS6522VIA));
//(at some point this shite will have to go into the state file...)
#ifdef USE_NEW_AY8910
AYInit();
#else
AY8910_InitAll(1020484, 48000);
#endif
// Set up V65C02 execution context
memset(&mainCPU, 0, sizeof(V65C02REGS));
mainCPU.RdMem = AppleReadMem;
mainCPU.WrMem = AppleWriteMem;
mainCPU.Timer = AppleTimer;
mainCPU.cpuFlags |= V65C02_ASSERT_LINE_RESET;
if (!LoadImg(settings.BIOSPath, rom + 0xC000, 0x4000))
@ -522,6 +640,7 @@ int main(int /*argc*/, char * /*argv*/[])
}
GUI::Init(sdlRenderer);
WriteLog("About to initialize audio...\n");
SoundInit();
@ -532,6 +651,20 @@ int main(int /*argc*/, char * /*argv*/[])
WriteLog("Unable to use Apple2 state file \"%s\"!\n", settings.autoStatePath);
}
/*
So, how to run this then? Right now, we have two separate threads, one for the CPU and one for audio. The screen refresh is tied to the CPU thread.
To do this properly, however, we need to execute approximately 1,020,484.32 cycles per second, and we need to tie the AY/regular sound to this rate. Video should happen still approximately 60 times a second, even though the real thing has a frame rate of 59.92 Hz.
Right now, the speed of the CPU is tied to the host system's refresh rate (which seems to be somewhere around 59.9 Hz). Under this regime, the sound thread is starved much of the time, which means there's a timing mismatch somewhere between the sound thread and the CPU thread and the video (main) thread.
Other considerations: Even though we know the exact amount of cycles for one frame (17030 cycles to be exact), because the video frame rate is slightly under 60 (~59.92) the amount of time those cycles take can't be tied to the host system refresh rate, as they won't be the same (it will have about 8,000 or so more cycles in one second than it should have using 60 frames per second as the base frequency). However, we do know that the system clock is 14.318180 MHz, and we do know that 1 out of every 65 cycles will take 2 extra ticks of the system clock (cycles are normally 14 ticks of the system clock). So, by virtue of this, we know how long one frame is in seconds exactly (which would be (((65 * 14) + 2) * 262) / 14318180 = 16.688154500083 milliseconds).
So we need to decouple the CPU thread from the host video thread, and have the CPU frame run at its rate so that it will complete its running in its alloted time. We also need to have a little bit of cushion for the sound thread, so that its buffer doesn't starve. Assuming we get the timing correct, it will pull ahead and fall behind and all average out in the end.
*/
running = true;
InitializeEventList();
// Set frame to fire at 1/60 s interval
@ -553,7 +686,9 @@ int main(int /*argc*/, char * /*argv*/[])
while (running)
{
#ifdef CPU_CLOCK_CHECKING
double timeToNextEvent = GetTimeToNextEvent();
#endif
#ifndef THREADED_65C02
Execute65C02(&mainCPU, USEC_TO_M6502_CYCLES(timeToNextEvent));
@ -609,7 +744,8 @@ WriteLog("Main: SDL_DestroyCond(cpuCond);\n");
// Letters a-z (lowercase)
//
// N.B.: The Apple //e keyboard maps its shift characters like most modern US
// keyboards, so this table should suffice for the shifted keys just fine.
// keyboards, so this table should suffice for the shifted keys just
// fine.
//
uint8_t apple2e_keycode[4][56] = {
{ // Normal
@ -656,6 +792,9 @@ static void FrameCallback(void)
SDL_Event event;
uint8_t keyIndex;
frameTimePtr = (frameTimePtr + 1) % 60;
frameTime[frameTimePtr] = startTicks;
while (SDL_PollEvent(&event))
{
switch (event.type)
@ -766,7 +905,7 @@ static void FrameCallback(void)
keyDown = true;
// Handle Caps Lock
if (capsLock
if ((SDL_GetModState() & KMOD_CAPS)
&& (lastKeyPressed >= 0x61) && (lastKeyPressed <= 0x7A))
lastKeyPressed -= 0x20;
@ -814,7 +953,15 @@ static void FrameCallback(void)
closedAppleDown = true;
// Toggle the disassembly process
else if (event.key.keysym.sym == SDLK_F11)
{
dumpDis = !dumpDis;
SpawnMessage("Trace: %s", (dumpDis ? "ON" : "off"));
}
else if (event.key.keysym.sym == SDLK_F12)
{
logAYInternal = !logAYInternal;
SpawnMessage("AY Trace: %s", (logAYInternal ? "ON" : "off"));
}
/*else if (event.key.keysym.sym == SDLK_F9)
{
@ -831,6 +978,8 @@ static void FrameCallback(void)
TogglePalette();
else if (event.key.keysym.sym == SDLK_F3)
CycleScreenTypes();
else if (event.key.keysym.sym == SDLK_F4)
ToggleTickDisplay();
else if (event.key.keysym.sym == SDLK_F5)
{
VolumeDown();
@ -851,6 +1000,17 @@ static void FrameCallback(void)
SpawnMessage("Volume: %s", volStr);
}
else if (event.key.keysym.sym == SDLK_F7)
{
// 4th root of 2 is ~1.18920711500272 (~1.5 dB)
maxVolume /= 1.4142135f; // This attenuates by ~3 dB
SpawnMessage("MB Volume: %d", (int)maxVolume);
}
else if (event.key.keysym.sym == SDLK_F8)
{
maxVolume *= 1.4142135f;
SpawnMessage("MB Volume: %d", (int)maxVolume);
}
else if (event.key.keysym.sym == SDLK_F12)
{
if (!fullscreenDebounce)
@ -859,22 +1019,12 @@ static void FrameCallback(void)
fullscreenDebounce = true;
}
}
else if (event.key.keysym.sym == SDLK_CAPSLOCK)
{
if (!capsLockDebounce)
{
capsLock = !capsLock;
capsLockDebounce = true;
}
}
break;
case SDL_KEYUP:
if (event.key.keysym.sym == SDLK_F12)
fullscreenDebounce = false;
else if (event.key.keysym.sym == SDLK_CAPSLOCK)
capsLockDebounce = false;
// Paddle buttons 0 & 1
else if (event.key.keysym.sym == SDLK_LALT)
openAppleDown = false;
@ -1005,17 +1155,29 @@ if (counter == 60)
#endif
// This is the problem: If you set the interval to 16, it runs faster than
// 1/60s per frame. If you set it to 17, it runs slower. What we need is to
// have it do 16 for one frame, then 17 for two others. Then it should average
// out to 1/60s per frame every 3 frames. [And now it does!]
// 1/60s per frame. If you set it to 17, it runs slower. What we need is to
// have it do 16 for one frame, then 17 for two others. Then it should average
// out to 1/60s per frame every 3 frames. [And now it does!]
// Maybe we need a higher resolution timer, as the SDL_GetTicks() (in ms) seems
// to jitter all over the place...
frameCount = (frameCount + 1) % 3;
uint32_t waitFrameTime = 17 - (frameCount == 0 ? 1 : 0);
// uint32_t waitFrameTime = 17 - (frameCount == 0 ? 1 : 0);
// Get number of ticks burned in this frame, for displaying elsewhere
#if 0
frameTicks = SDL_GetTicks() - startTicks;
#else
frameTicks = ((SDL_GetPerformanceCounter() - startTicks) * 1000) / SDL_GetPerformanceFrequency();
#endif
// Wait for next frame...
while (SDL_GetTicks() - startTicks < waitFrameTime)
SDL_Delay(1);
// while (SDL_GetTicks() - startTicks < waitFrameTime)
// SDL_Delay(1);
#if 0
startTicks = SDL_GetTicks();
#else
startTicks = SDL_GetPerformanceCounter();
#endif
#if 0
uint64_t cpuCycles = GetCurrentV65C02Clock();
uint32_t cyclesBurned = (uint32_t)(cpuCycles - lastCPUCycles);

View File

@ -35,4 +35,12 @@ extern bool ioudis;
extern bool dhires;
extern uint8_t lcState;
extern uint64_t frameCycleStart;
#if 0
extern uint32_t frameTicks;
extern uint32_t frameTime[];
#else
extern uint64_t frameTicks;
extern uint64_t frameTime[];
#endif
extern uint32_t frameTimePtr;

View File

@ -1,3 +1,406 @@
// AY-3-8910 Emulator
//
// This was written mainly from the General Instruments datasheet for the 8910
// part. I would have used the one from MAME, but it was so poorly written and
// so utterly incomprehensible that I decided to start from scratch to see if I
// could do any better; and so here we are. I did use a bit of code from
// MAME's AY-3-8910 RNG, as it was just too neat not to use. :-)
//
// by James Hammons
// (C) 2018 Underground Software
//
#include "ay8910.h"
#include <string.h> // for memset()
#include "log.h"
#include "sound.h"
struct AY_3_8910
{
// User visible registers
uint16_t period[3]; // Channel A-C period
int16_t volume[3]; // Channel A-C volume (non-envelope mode)
bool envEnable[3]; // Channel A-C envelope enable
bool toneEnable[3]; // Channel A-C tone enable
bool noiseEnable[3]; // Channel A-C noise enable
uint16_t noisePeriod; // Noise period (5 bits * 16)
uint32_t envPeriod; // Envelope period (16 bits * 256)
bool envAttack; // Envelope Attack bit
bool envAlternate; // Envelope Alternate bit
bool envHold; // Envelope Hold bit
// Internal registers
uint16_t count[3]; // Channel A-C current count
bool state[3]; // Channel A-C current state
uint16_t noiseCount; // Noise current count
bool noiseState; // Noise state
uint32_t envCount[3]; // Envelope current count
int16_t envDirection[3];// Envelope direction (rising, 0, or falling)
uint32_t prng; // Psuedo RNG (17 bits)
};
// Maximum volume that can be generated by one voice
float maxVolume = 8192.0f;
// Normalized volumes (zero to one) for AY-3-8910 output, in 16 steps
static float normalizedVolume[16];// = {};
// AY-3-8910 register IDs
enum { AY_AFINE = 0, AY_ACOARSE, AY_BFINE, AY_BCOARSE, AY_CFINE, AY_CCOARSE,
AY_NOISEPER, AY_ENABLE, AY_AVOL, AY_BVOL, AY_CVOL, AY_EFINE, AY_ECOARSE,
AY_ESHAPE, AY_PORTA, AY_PORTB };
// Chip structs (for up to four separate chips)
static AY_3_8910 ay[4];
void AYInit(void)
{
for(int chip=0; chip<4; chip++)
AYReset(chip);
// Our normalized volume levels are from 0 to -48 dB, in 3 dB steps.
// N.B.: It's 3dB steps because those sound the best. Dunno what it really
// is, as nothing in the documentation tells you (it only says that
// each channel's volume is normalized from 0 to 1.0V).
float level = 1.0f;
for(int i=15; i>=0; i--)
{
normalizedVolume[i] = level;
level /= 1.4125375446228; // 10.0 ^ (3.0 / 20.0) = 3 dB
}
// In order to get a scale that goes from 0 to 1 smoothly, we renormalize
// our volumes so that volume[0] is actually 0, and volume[15] is 1.
// Basically, we're sliding the curve down the Y-axis so that volume[0]
// touches the X-axis, then stretching the result so that it fits into the
// interval (0, 1).
float vol0 = normalizedVolume[0];
float vol15 = normalizedVolume[15] - vol0;
for(int i=0; i<16; i++)
normalizedVolume[i] = (normalizedVolume[i] - vol0) / vol15;
#if 0
WriteLog("\nRenormalized volume, level (max=%d):\n", (int)maxVolume);
for(int i=0; i<16; i++)
WriteLog("%lf, %d\n", normalizedVolume[i], (int)(normalizedVolume[i] * maxVolume));
WriteLog("\n");
#endif
}
/*
Renormalized:
0.000000, 0
0.002333, 13
0.005628, 33
0.010283, 61
0.016859, 101
0.026146, 156
0.039266, 235
0.057797, 346
0.083974, 503
0.120949, 725
0.173178, 1039
0.246954, 1481
0.351165, 2106
0.498366, 2990
0.706294, 4237
1.000000, 6000
*/
void AYReset(int chipNum)
{
memset(&ay[chipNum], 0, sizeof(struct AY_3_8910));
ay[chipNum].prng = 1; // Set correct PRNG seed
}
void AYWrite(int chipNum, int reg, int value)
{
#if 0
static char regname[16][32] = {
"AY_AFINE ",
"AY_ACOARSE ",
"AY_BFINE ",
"AY_BCOARSE ",
"AY_CFINE ",
"AY_CCOARSE ",
"AY_NOISEPER",
"AY_ENABLE ",
"AY_AVOL ",
"AY_BVOL ",
"AY_CVOL ",
"AY_EFINE ",
"AY_ECOARSE ",
"AY_ESHAPE ",
"AY_PORTA ",
"AY_PORTB "
};
WriteLog("*** AY(%d) Reg: %s = $%02X\n", chipNum, regname[reg], value);
#endif
AY_3_8910 * chip = &ay[chipNum];
value &= 0xFF; // Ensure passed in value is no larger than 8 bits
switch (reg)
{
case AY_AFINE:
// The square wave period is the passed in value times 16, so we handle
// that here.
chip->period[0] = (chip->period[0] & 0xF000) | (value << 4);
break;
case AY_ACOARSE:
chip->period[0] = ((value & 0x0F) << 12) | (chip->period[0] & 0xFF0);
break;
case AY_BFINE:
chip->period[1] = (chip->period[1] & 0xF000) | (value << 4);
break;
case AY_BCOARSE:
chip->period[1] = ((value & 0x0F) << 12) | (chip->period[1] & 0xFF0);
break;
case AY_CFINE:
chip->period[2] = (chip->period[2] & 0xF000) | (value << 4);
break;
case AY_CCOARSE:
chip->period[2] = ((value & 0x0F) << 12) | (chip->period[2] & 0xFF0);
break;
case AY_NOISEPER:
// Like the square wave period, the value is the what's passed * 16.
chip->noisePeriod = (value & 0x1F) << 4;
break;
case AY_ENABLE:
chip->toneEnable[0] = (value & 0x01 ? false : true);
chip->toneEnable[1] = (value & 0x02 ? false : true);
chip->toneEnable[2] = (value & 0x04 ? false : true);
chip->noiseEnable[0] = (value & 0x08 ? false : true);
chip->noiseEnable[1] = (value & 0x10 ? false : true);
chip->noiseEnable[2] = (value & 0x20 ? false : true);
break;
case AY_AVOL:
chip->volume[0] = value & 0x0F;
chip->envEnable[0] = (value & 0x10 ? true : false);
if (chip->envEnable[0])
{
chip->envCount[0] = 0;
chip->volume[0] = (chip->envAttack ? 0 : 15);
chip->envDirection[0] = (chip->envAttack ? 1 : -1);
}
break;
case AY_BVOL:
chip->volume[1] = value & 0x0F;
chip->envEnable[1] = (value & 0x10 ? true : false);
if (chip->envEnable[1])
{
chip->envCount[1] = 0;
chip->volume[1] = (chip->envAttack ? 0 : 15);
chip->envDirection[1] = (chip->envAttack ? 1 : -1);
}
break;
case AY_CVOL:
chip->volume[2] = value & 0x0F;
chip->envEnable[2] = (value & 0x10 ? true : false);
if (chip->envEnable[2])
{
chip->envCount[2] = 0;
chip->volume[2] = (chip->envAttack ? 0 : 15);
chip->envDirection[2] = (chip->envAttack ? 1 : -1);
}
break;
case AY_EFINE:
// The envelope period is 256 times the passed in value
chip->envPeriod = (chip->envPeriod & 0xFF0000) | (value << 8);
break;
case AY_ECOARSE:
chip->envPeriod = (value << 16) | (chip->envPeriod & 0xFF00);
break;
case AY_ESHAPE:
chip->envAttack = (value & 0x04 ? true : false);
chip->envAlternate = (value & 0x02 ? true : false);
chip->envHold = (value & 0x01 ? true : false);
// If the Continue bit is *not* set, the Alternate bit is forced to the
// Attack bit, and Hold is forced on.
if (!(value & 0x08))
{
chip->envAlternate = chip->envAttack;
chip->envHold = true;
}
// Reset all voice envelope counts...
for(int i=0; i<3; i++)
{
chip->envCount[i] = 0;
chip->envDirection[i] = (chip->envAttack ? 1 : -1);
// Only reset the volume if the envelope is enabled!
if (chip->envEnable[i])
chip->volume[i] = (chip->envAttack ? 0 : 15);
}
break;
}
}
//
// Generate one sample and quit
//
bool logAYInternal = false;
uint16_t AYGetSample(int chipNum)
{
AY_3_8910 * chip = &ay[chipNum];
uint16_t sample = 0;
// Number of cycles per second to run the PSG is the 6502 clock rate
// divided by the host sample rate
const static double exactCycles = 1020484.32 / (double)SAMPLE_RATE;
static double overflow = 0;
int fullCycles = (int)exactCycles;
overflow += exactCycles - (double)fullCycles;
if (overflow >= 1.0)
{
fullCycles++;
overflow -= 1.0;
}
for(int i=0; i<fullCycles; i++)
{
for(int j=0; j<3; j++)
{
// Tone generators only run if the corresponding voice is enabled.
// N.B.: We also reject any period set that is less than 2.
if (chip->toneEnable[j] && (chip->period[j] > 16))
{
chip->count[j]++;
// It's (period / 2) because one full period of a square wave
// is 0 for half of its period and 1 for the other half!
if (chip->count[j] > (chip->period[j] / 2))
{
chip->count[j] = 0;
chip->state[j] = !chip->state[j];
}
}
// Envelope generator only runs if the corresponding voice flag is
// enabled.
if (chip->envEnable[j])
{
chip->envCount[j]++;
// It's (EP / 16) because there are 16 volume steps in each EP.
if (chip->envCount[j] > (chip->envPeriod / 16))
{
// Attack 0 = \, 1 = / (attack lasts one EP)
// Alternate = mirror envelope's last attack
// Hold = run 1 EP, hold at level (Alternate XOR Attack)
chip->envCount[j] = 0;
// We've hit a point where we need to make a change to the
// envelope's volume, so do it:
chip->volume[j] += chip->envDirection[j];
// If we hit the end of the EP, change the state of the
// envelope according to the envelope's variables.
if ((chip->volume[j] > 15) || (chip->volume[j] < 0))
{
// Hold means we set the volume to (Alternate XOR
// Attack) and stay there after the Attack EP.
if (chip->envHold)
{
chip->volume[j] = (chip->envAttack != chip->envAlternate ? 15: 0);
chip->envDirection[j] = 0;
}
else
{
// If the Alternate bit is set, we mirror the
// Attack pattern; otherwise we reset it to the
// whatever level was set by the Attack bit.
if (chip->envAlternate)
{
chip->envDirection[j] = -chip->envDirection[j];
chip->volume[j] += chip->envDirection[j];
}
else
chip->volume[j] = (chip->envAttack ? 0 : 15);
}
}
}
}
}
// Noise generator (the PRNG) runs all the time:
chip->noiseCount++;
if (chip->noiseCount > chip->noisePeriod)
{
chip->noiseCount = 0;
// The following is from MAME's AY-3-8910 code:
// The Pseudo Random Number Generator of the 8910 is a 17-bit shift
// register. The input to the shift register is bit0 XOR bit3 (bit0
// is the output). This was verified on AY-3-8910 and YM2149 chips.
// The following is a fast way to compute bit17 = bit0 ^ bit3.
// Instead of doing all the logic operations, we only check bit0,
// relying on the fact that after three shifts of the register,
// what now is bit3 will become bit0, and will invert, if
// necessary, bit14, which previously was bit17.
if (chip->prng & 0x00001)
{
// This version is called the "Galois configuration".
chip->prng ^= 0x24000;
// The noise wave *toggles* when a one shows up in bit0...
chip->noiseState = !chip->noiseState;
}
chip->prng >>= 1;
}
}
// We mix channels A-C here into one sample, because the Mockingboard just
// sums the output of the AY-3-8910 by tying their lines together.
// We also handle the various cases (of which there are four) of mixing
// pure tones and "noise" tones together.
for(int i=0; i<3; i++)
{
// Set the volume level scaled by the maximum volume (which can be
// altered outside of this module).
int level = (int)(normalizedVolume[chip->volume[i]] * maxVolume);
if (chip->toneEnable[i] && !chip->noiseEnable[i])
sample += (chip->state[i] ? level : 0);
else if (!chip->toneEnable[i] && chip->noiseEnable[i])
sample += (chip->noiseState ? level : 0);
else if (chip->toneEnable[i] && chip->noiseEnable[i])
sample += (chip->state[i] & chip->noiseState ? level : 0);
else if (!chip->toneEnable[i] && !chip->noiseEnable[i])
sample += level;
}
if (logAYInternal)
{
WriteLog(" (%d) State A,B,C: %s %s %s, Sample: $%04X, P: $%X, $%X, $%X\n", chipNum, (chip->state[0] ? "1" : "0"), (chip->state[1] ? "1" : "0"), (chip->state[2] ? "1" : "0"), sample, chip->period[0], chip->period[1], chip->period[2]);
}
return sample;
}
// STUFF TO DELETE...
#if 0
/***************************************************************************
ay8910.cpp
@ -29,9 +432,6 @@
// JLH: Commented out MAME specific crap
#include "ay8910.h"
#include <string.h> // for memset()
#define MAX_OUTPUT 0x7FFF
// See AY8910_set_clock() for definition of STEP
@ -41,13 +441,8 @@ struct AY8910
{
int Channel;
int SampleRate;
// mem_read_handler PortAread;
// mem_read_handler PortBread;
// mem_write_handler PortAwrite;
// mem_write_handler PortBwrite;
int register_latch;
unsigned char Regs[16];
int lastEnable;
unsigned int UpdateStep;
int PeriodA, PeriodB, PeriodC, PeriodN, PeriodE;
int CountA, CountB, CountC, CountN, CountE;
@ -60,7 +455,8 @@ struct AY8910
unsigned int VolTable[32];
};
/* register id's */
static struct AY8910 AYPSG[MAX_8910]; /* array of PSG's */
#define AY_AFINE (0)
#define AY_ACOARSE (1)
#define AY_BFINE (2)
@ -75,30 +471,47 @@ struct AY8910
#define AY_EFINE (11)
#define AY_ECOARSE (12)
#define AY_ESHAPE (13)
#define AY_PORTA (14)
#define AY_PORTB (15)
static struct AY8910 AYPSG[MAX_8910]; /* array of PSG's */
//#define AY_PORTA (14)
//#define AY_PORTB (15)
void _AYWriteReg(int n, int r, int v)
{
struct AY8910 *PSG = &AYPSG[n];
#if 1
static char regname[16][32] = {
"AY_AFINE ",
"AY_ACOARSE ",
"AY_BFINE ",
"AY_BCOARSE ",
"AY_CFINE ",
"AY_CCOARSE ",
"AY_NOISEPER",
"AY_ENABLE ",
"AY_AVOL ",
"AY_BVOL ",
"AY_CVOL ",
"AY_EFINE ",
"AY_ECOARSE ",
"AY_ESHAPE ",
"AY_PORTA ",
"AY_PORTB "
};
WriteLog("*** AY(%d) Reg: %s = $%02X\n", n, regname[r], v);
#endif
struct AY8910 * PSG = &AYPSG[n];
int old;
PSG->Regs[r] = v;
/* A note about the period of tones, noise and envelope: for speed reasons, *
* we count down from the period to 0, but careful studies of the chip *
* output prove that it instead counts up from 0 until the counter becomes *
* greater or equal to the period. This is an important difference when the *
* program is rapidly changing the period to modulate the sound. *
* To compensate for the difference, when the period is changed we adjust *
* our internal counter. *
* Also, note that period = 0 is the same as period = 1. This is mentioned *
* in the YM2203 data sheets. However, this does NOT apply to the Envelope *
/* A note about the period of tones, noise and envelope: for speed reasons,
* we count down from the period to 0, but careful studies of the chip
* output prove that it instead counts up from 0 until the counter becomes
* greater or equal to the period. This is an important difference when the
* program is rapidly changing the period to modulate the sound.
* To compensate for the difference, when the period is changed we adjust
* our internal counter.
* Also, note that period = 0 is the same as period = 1. This is mentioned
* in the YM2203 data sheets. However, this does NOT apply to the Envelope
* period. In that case, period = 0 is half as period = 1. */
switch (r)
{
@ -106,7 +519,8 @@ void _AYWriteReg(int n, int r, int v)
case AY_ACOARSE:
PSG->Regs[AY_ACOARSE] &= 0x0F;
old = PSG->PeriodA;
PSG->PeriodA = (PSG->Regs[AY_AFINE] + 256 * PSG->Regs[AY_ACOARSE]) * PSG->UpdateStep;
// PSG->PeriodA = (PSG->Regs[AY_AFINE] + 256 * PSG->Regs[AY_ACOARSE]) * PSG->UpdateStep;
PSG->PeriodA = ((PSG->Regs[AY_ACOARSE] << 8) | PSG->Regs[AY_AFINE]) * PSG->UpdateStep;
if (PSG->PeriodA == 0)
PSG->PeriodA = PSG->UpdateStep;
@ -157,39 +571,45 @@ void _AYWriteReg(int n, int r, int v)
if (PSG->CountN <= 0)
PSG->CountN = 1;
break;
case AY_ENABLE:
/* case AY_ENABLE:
if ((PSG->lastEnable == -1) ||
((PSG->lastEnable & 0x40) != (PSG->Regs[AY_ENABLE] & 0x40)))
{
/* write out 0xff if port set to input */
// if (PSG->PortAwrite)
// (*PSG->PortAwrite)(0, (UINT8) ((PSG->Regs[AY_ENABLE] & 0x40) ? PSG->Regs[AY_PORTA] : 0xff)); // [TC: UINT8 cast]
// write out $FF if port set to input
if (PSG->PortAwrite)
(*PSG->PortAwrite)(0, (UINT8) ((PSG->Regs[AY_ENABLE] & 0x40) ? PSG->Regs[AY_PORTA] : 0xff)); // [TC: UINT8 cast]
}
if ((PSG->lastEnable == -1) ||
((PSG->lastEnable & 0x80) != (PSG->Regs[AY_ENABLE] & 0x80)))
{
/* write out 0xff if port set to input */
// if (PSG->PortBwrite)
// (*PSG->PortBwrite)(0, (UINT8) ((PSG->Regs[AY_ENABLE] & 0x80) ? PSG->Regs[AY_PORTB] : 0xff)); // [TC: UINT8 cast]
// write out $FF if port set to input
if (PSG->PortBwrite)
(*PSG->PortBwrite)(0, (UINT8) ((PSG->Regs[AY_ENABLE] & 0x80) ? PSG->Regs[AY_PORTB] : 0xff)); // [TC: UINT8 cast]
}
PSG->lastEnable = PSG->Regs[AY_ENABLE];
break;
break;*/
case AY_AVOL:
PSG->Regs[AY_AVOL] &= 0x1F;
PSG->EnvelopeA = PSG->Regs[AY_AVOL] & 0x10;
PSG->VolA = PSG->EnvelopeA ? PSG->VolE : PSG->VolTable[PSG->Regs[AY_AVOL] ? PSG->Regs[AY_AVOL]*2+1 : 0];
PSG->VolA = (PSG->EnvelopeA ? PSG->VolE :
(PSG->VolTable[PSG->Regs[AY_AVOL] ? PSG->Regs[AY_AVOL] * 2 + 1
: 0]));
break;
case AY_BVOL:
PSG->Regs[AY_BVOL] &= 0x1F;
PSG->EnvelopeB = PSG->Regs[AY_BVOL] & 0x10;
PSG->VolB = PSG->EnvelopeB ? PSG->VolE : PSG->VolTable[PSG->Regs[AY_BVOL] ? PSG->Regs[AY_BVOL]*2+1 : 0];
PSG->VolB = (PSG->EnvelopeB ? PSG->VolE :
(PSG->VolTable[PSG->Regs[AY_BVOL] ? PSG->Regs[AY_BVOL] * 2 + 1
: 0]));
break;
case AY_CVOL:
PSG->Regs[AY_CVOL] &= 0x1F;
PSG->EnvelopeC = PSG->Regs[AY_CVOL] & 0x10;
PSG->VolC = PSG->EnvelopeC ? PSG->VolE : PSG->VolTable[PSG->Regs[AY_CVOL] ? PSG->Regs[AY_CVOL]*2+1 : 0];
PSG->VolC = (PSG->EnvelopeC ? PSG->VolE
: (PSG->VolTable[PSG->Regs[AY_CVOL] ? PSG->Regs[AY_CVOL] * 2 + 1
: 0]));
break;
case AY_EFINE:
case AY_ECOARSE:
@ -232,7 +652,7 @@ void _AYWriteReg(int n, int r, int v)
just a smoother curve, we always use the YM2149 behaviour.
*/
PSG->Regs[AY_ESHAPE] &= 0x0F;
PSG->Attack = (PSG->Regs[AY_ESHAPE] & 0x04) ? 0x1F : 0x00;
PSG->Attack = (PSG->Regs[AY_ESHAPE] & 0x04 ? 0x1F : 0x00);
if ((PSG->Regs[AY_ESHAPE] & 0x08) == 0)
{
@ -260,60 +680,65 @@ void _AYWriteReg(int n, int r, int v)
if (PSG->EnvelopeC)
PSG->VolC = PSG->VolE;
break;
case AY_PORTA:
/* case AY_PORTA:
if (PSG->Regs[AY_ENABLE] & 0x40)
{
// if (PSG->PortAwrite)
// (*PSG->PortAwrite)(0, PSG->Regs[AY_PORTA]);
// else
// logerror("PC %04x: warning - write %02x to 8910 #%d Port A\n",activecpu_get_pc(),PSG->Regs[AY_PORTA],n);
if (PSG->PortAwrite)
(*PSG->PortAwrite)(0, PSG->Regs[AY_PORTA]);
else
logerror("PC %04x: warning - write %02x to 8910 #%d Port A\n",activecpu_get_pc(),PSG->Regs[AY_PORTA],n);
}
else
{
// logerror("warning: write to 8910 #%d Port A set as input - ignored\n",n);
logerror("warning: write to 8910 #%d Port A set as input - ignored\n",n);
}
break;
case AY_PORTB:
if (PSG->Regs[AY_ENABLE] & 0x80)
{
// if (PSG->PortBwrite)
// (*PSG->PortBwrite)(0, PSG->Regs[AY_PORTB]);
// else
// logerror("PC %04x: warning - write %02x to 8910 #%d Port B\n",activecpu_get_pc(),PSG->Regs[AY_PORTB],n);
if (PSG->PortBwrite)
(*PSG->PortBwrite)(0, PSG->Regs[AY_PORTB]);
else
logerror("PC %04x: warning - write %02x to 8910 #%d Port B\n",activecpu_get_pc(),PSG->Regs[AY_PORTB],n);
}
else
{
// logerror("warning: write to 8910 #%d Port B set as input - ignored\n",n);
logerror("warning: write to 8910 #%d Port B set as input - ignored\n",n);
}
break;
break;*/
}
}
//#define DEBUG_AY
// /length/ is the number of samples we require
// NB. This should be called at twice the 6522 IRQ rate or (eg) 60Hz if no IRQ.
void AY8910Update(int chip, int16_t ** buffer, int length) // [TC: Removed static]
{
#ifdef DEBUG_AY
WriteLog("AY8910Update: chip=%d, buffer=%X, length=%d\n", chip, buffer, length);
#endif
struct AY8910 * PSG = &AYPSG[chip];
int16_t * buf1, * buf2, * buf3;
int outn;
buf1 = buffer[0];
buf2 = buffer[1];
buf3 = buffer[2];
int16_t * buf1 = buffer[0];
int16_t * buf2 = buffer[1];
int16_t * buf3 = buffer[2];
/* The 8910 has three outputs, each output is the mix of one of the three *
* tone generators and of the (single) noise generator. The two are mixed *
* BEFORE going into the DAC. The formula to mix each channel is: *
* (ToneOn | ToneDisable) & (NoiseOn | NoiseDisable). *
* Note that this means that if both tone and noise are disabled, the output *
* is 1, not 0, and can be modulated changing the volume. *
* *
* If the channels are disabled, set their output to 1, and increase the *
* counter, if necessary, so they will not be inverted during this update. *
* Setting the output to 1 is necessary because a disabled channel is locked *
* into the ON state (see above); and it has no effect if the volume is 0. *
* If the volume is 0, increase the counter, but don't touch the output. */
/* The 8910 has three outputs, each output is the mix of one of the three
* tone generators and of the (single) noise generator. The two are mixed
* BEFORE going into the DAC. The formula to mix each channel is:
* (ToneOn | ToneDisable) & (NoiseOn | NoiseDisable).
* Note that this means that if both tone and noise are disabled, the
* output is 1, not 0, and can be modulated changing the volume.
*
* If the channels are disabled, set their output to 1, and increase the
* counter, if necessary, so they will not be inverted during this update.
* Setting the output to 1 is necessary because a disabled channel is
* locked into the ON state (see above); and it has no effect if the volume
* is 0. If the volume is 0, increase the counter, but don't touch the
* output.
*/
// N.B.: The bits in AY_ENABLE (0-5) are all active LOW, which means if the
// channel bit is set, it is DISABLED. 5-3 are noise, 2-0 tone.
if (PSG->Regs[AY_ENABLE] & 0x01)
{
if (PSG->CountA <= length * STEP)
@ -323,9 +748,11 @@ void AY8910Update(int chip, int16_t ** buffer, int length) // [TC: Removed stati
}
else if (PSG->Regs[AY_AVOL] == 0)
{
/* note that I do count += length, NOT count = length + 1. You might think *
* it's the same since the volume is 0, but doing the latter could cause *
* interferencies when the program is rapidly modulating the volume. */
/* note that I do count += length, NOT count = length + 1. You might
* think it's the same since the volume is 0, but doing the latter
* could cause interferencies when the program is rapidly modulating
* the volume.
*/
if (PSG->CountA <= length * STEP)
PSG->CountA += length * STEP;
}
@ -356,33 +783,39 @@ void AY8910Update(int chip, int16_t ** buffer, int length) // [TC: Removed stati
PSG->CountC += length * STEP;
}
/* for the noise channel we must not touch OutputN - it's also not necessary *
* since we use outn. */
/* for the noise channel we must not touch OutputN - it's also not
* necessary since we use outn. */
if ((PSG->Regs[AY_ENABLE] & 0x38) == 0x38) /* all off */
if (PSG->CountN <= length * STEP)
PSG->CountN += length * STEP;
outn = (PSG->OutputN | PSG->Regs[AY_ENABLE]);
int outn = (PSG->OutputN | PSG->Regs[AY_ENABLE]);
#ifdef DEBUG_AY
WriteLog("AY8910Update: Stepping into while (length)...\n");
#endif
/* buffering loop */
while (length)
{
int vola, volb, volc;
int left;
/* vola, volb and volc keep track of how long each square wave stays
* in the 1 position during the sample period.
*/
int vola = 0, volb = 0, volc = 0;
int left = STEP;
/* vola, volb and volc keep track of how long each square wave stays *
* in the 1 position during the sample period. */
vola = volb = volc = 0;
left = STEP;
#ifdef DEBUG_AY
WriteLog("AY8910Update: Stepping into inner do loop... (length=%d)\n", length);
#endif
do
{
int nextevent;
if (PSG->CountN < left)
nextevent = PSG->CountN;
else
nextevent = left;
int nextevent = (PSG->CountN < left ? PSG->CountN : left);
//Note: nextevent is 0 here when first initialized...
//so let's try this:
if (nextevent == 0)
left = 0;
#ifdef DEBUG_AY
WriteLog("AY8910Update: nextevent=$%X, left=$%X\n", nextevent, left);
#endif
if (outn & 0x08)
{
@ -390,14 +823,14 @@ void AY8910Update(int chip, int16_t ** buffer, int length) // [TC: Removed stati
vola += PSG->CountA;
PSG->CountA -= nextevent;
/* PeriodA is the half period of the square wave. Here, in each *
* loop I add PeriodA twice, so that at the end of the loop the *
* square wave is in the same status (0 or 1) it was at the start. *
* vola is also incremented by PeriodA, since the wave has been 1 *
* exactly half of the time, regardless of the initial position. *
* If we exit the loop in the middle, OutputA has to be inverted *
* and vola incremented only if the exit status of the square *
* wave is 1. */
/* PeriodA is the half period of the square wave. Here, in each
* loop I add PeriodA twice, so that at the end of the loop the
* square wave is in the same status (0 or 1) it was at the
* start. vola is also incremented by PeriodA, since the wave
* has been 1 exactly half of the time, regardless of the
* initial position. If we exit the loop in the middle, OutputA
* has to be inverted and vola incremented only if the exit
* status of the square wave is 1. */
while (PSG->CountA <= 0)
{
PSG->CountA += PSG->PeriodA;
@ -408,6 +841,7 @@ void AY8910Update(int chip, int16_t ** buffer, int length) // [TC: Removed stati
if (PSG->OutputA)
vola += PSG->PeriodA;
break;
}
@ -421,6 +855,7 @@ void AY8910Update(int chip, int16_t ** buffer, int length) // [TC: Removed stati
else
{
PSG->CountA -= nextevent;
while (PSG->CountA <= 0)
{
PSG->CountA += PSG->PeriodA;
@ -530,21 +965,21 @@ void AY8910Update(int chip, int16_t ** buffer, int length) // [TC: Removed stati
if (PSG->CountN <= 0)
{
/* Is noise output going to change? */
if ((PSG->RNG + 1) & 0x00002) /* (bit0^bit1)? */
if ((PSG->RNG + 1) & 0x00002) // (bit0 XOR bit1) == 1?
{
PSG->OutputN = ~PSG->OutputN;
outn = (PSG->OutputN | PSG->Regs[AY_ENABLE]);
}
/* The Random Number Generator of the 8910 is a 17-bit shift *
* register. The input to the shift register is bit0 XOR bit3 *
* (bit0 is the output). This was verified on AY-3-8910 and *
* YM2149 chips. *
* *
* The following is a fast way to compute bit17 = bit0^bit3. *
* Instead of doing all the logic operations, we only check *
* bit0, relying on the fact that after three shifts of the *
* register, what now is bit3 will become bit0, and will *
/* The Random Number Generator of the 8910 is a 17-bit shift
* register. The input to the shift register is bit0 XOR bit3
* (bit0 is the output). This was verified on AY-3-8910 and
* YM2149 chips.
*
* The following is a fast way to compute bit17 = bit0^bit3.
* Instead of doing all the logic operations, we only check
* bit0, relying on the fact that after three shifts of the
* register, what now is bit3 will become bit0, and will
* invert, if necessary, bit14, which previously was bit17. */
if (PSG->RNG & 0x00001)
PSG->RNG ^= 0x24000; /* This version is called the "Galois configuration". */
@ -557,6 +992,9 @@ void AY8910Update(int chip, int16_t ** buffer, int length) // [TC: Removed stati
}
while (left > 0);
#ifdef DEBUG_AY
WriteLog("AY8910Update: About to update envelope...\n");
#endif
/* update envelope */
if (PSG->Holding == 0)
{
@ -564,12 +1002,19 @@ void AY8910Update(int chip, int16_t ** buffer, int length) // [TC: Removed stati
if (PSG->CountE <= 0)
{
do
#ifdef DEBUG_AY
WriteLog("AY8910Update: About to enter do loop... (CountEnv = $%X, CountE =$%X, PeriodE = $%X)\n", PSG->CountEnv, PSG->CountE, PSG->PeriodE);
#endif
// JLH: Sanity check...
if (PSG->PeriodE > 0)
{
PSG->CountEnv--;
PSG->CountE += PSG->PeriodE;
do
{
PSG->CountEnv--;
PSG->CountE += PSG->PeriodE;
}
while (PSG->CountE <= 0);
}
while (PSG->CountE <= 0);
/* check envelope current position */
if (PSG->CountEnv < 0)
@ -584,8 +1029,8 @@ void AY8910Update(int chip, int16_t ** buffer, int length) // [TC: Removed stati
}
else
{
/* if CountEnv has looped an odd number of times (usually 1), *
* invert the output. */
/* if CountEnv has looped an odd number of times
* (usually 1), invert the output. */
if (PSG->Alternate && (PSG->CountEnv & 0x20))
PSG->Attack ^= 0x1F;
@ -594,6 +1039,7 @@ void AY8910Update(int chip, int16_t ** buffer, int length) // [TC: Removed stati
}
PSG->VolE = PSG->VolTable[PSG->CountEnv ^ PSG->Attack];
/* reload volume */
if (PSG->EnvelopeA)
PSG->VolA = PSG->VolE;
@ -606,7 +1052,7 @@ void AY8910Update(int chip, int16_t ** buffer, int length) // [TC: Removed stati
}
}
#if 0
#if 1
*(buf1++) = (vola * PSG->VolA) / STEP;
*(buf2++) = (volb * PSG->VolB) / STEP;
*(buf3++) = (volc * PSG->VolC) / STEP;
@ -646,90 +1092,84 @@ void AY8910Update(int chip, int16_t ** buffer, int length) // [TC: Removed stati
#endif
length--;
}
#ifdef DEBUG_AY
WriteLog("AY8910Update: Done.\n");
#endif
}
static void AY8910_set_clock(int chip, int clock)
{
struct AY8910 * PSG = &AYPSG[chip];
// struct AY8910 * PSG = &AYPSG[chip];
/* The step clock for the tone and noise generators is the chip clock *
* divided by 8; for the envelope generator of the AY-3-8910, it is half *
* that much (clock/16), but the envelope of the YM2149 goes twice as *
* fast, therefore again clock/8. *
* Here we calculate the number of steps which happen during one sample *
* at the given sample rate. No. of events = sample rate / (clock/8). *
* STEP is a multiplier used to turn the fraction into a fixed point *
* number. */
PSG->UpdateStep = (unsigned int)(((double)STEP * PSG->SampleRate * 8 + clock / 2) / clock); // [TC: unsigned int cast]
/* The step clock for the tone and noise generators is the chip clock
* divided by 8; for the envelope generator of the AY-3-8910, it is half
* that much (clock/16), but the envelope of the YM2149 goes twice as
* fast, therefore again clock/8.
* Here we calculate the number of steps which happen during one sample
* at the given sample rate. No. of events = sample rate / (clock/8).
* STEP is a multiplier used to turn the fraction into a fixed point
* number.
*/
AYPSG[chip].UpdateStep = (unsigned int)(((double)STEP * AYPSG[chip].SampleRate * 8 + clock / 2) / clock); // [TC: unsigned int cast]
}
static void build_mixer_table(int chip)
{
struct AY8910 * PSG = &AYPSG[chip];
/* calculate the volume->voltage conversion table */
/* The AY-3-8910 has 16 levels, in a logarithmic scale (3dB per step) */
/* The YM2149 still has 16 levels for the tone generators, but 32 for */
/* the envelope generator (1.5dB per step). */
/* calculate the volume->voltage conversion table
* The AY-3-8910 has 16 levels, in a logarithmic scale (3dB per step)
* The YM2149 still has 16 levels for the tone generators, but 32 for
* the envelope generator (1.5dB per step).
*/
double out = MAX_OUTPUT;
for(int i=31; i>0; i--)
{
PSG->VolTable[i] = (unsigned int)(out + 0.5); /* round to nearest */ // [TC: unsigned int cast]
AYPSG[chip].VolTable[i] = (unsigned int)(out + 0.5); /* round to nearest */ // [TC: unsigned int cast]
out /= 1.188502227; /* = 10 ^ (1.5/20) = 1.5dB */
}
PSG->VolTable[0] = 0;
AYPSG[chip].VolTable[0] = 0;
}
void AY8910_reset(int chip)
{
int i;
struct AY8910 * PSG = &AYPSG[chip];
AYPSG[chip].register_latch = 0;
AYPSG[chip].RNG = 1;
AYPSG[chip].OutputA = 0;
AYPSG[chip].OutputB = 0;
AYPSG[chip].OutputC = 0;
AYPSG[chip].OutputN = 0xFF;
PSG->register_latch = 0;
PSG->RNG = 1;
PSG->OutputA = 0;
PSG->OutputB = 0;
PSG->OutputC = 0;
PSG->OutputN = 0xFF;
PSG->lastEnable = -1; /* force a write */
for(i=0; i<AY_PORTA; i++)
_AYWriteReg(chip, i, 0); /* AYWriteReg() uses the timer system; we cannot */
/* call it at this time because the timer system */
/* has not been initialized. */
for(int i=0; i<=AY_ESHAPE; i++)
_AYWriteReg(chip, i, 0); /* AYWriteReg() uses the timer system; we
* cannot call it at this time because the
* timer system has not been initialized. */
}
// This stuff looks like Tom's code, so let's streamline and un-MSHungarianize this shit:
// [DONE]
// N.B.: Looks like 'clock' is the 65C02 clock rate, and 'sampleRate' is the
// sample rate set by the audio subsystem.
void AY8910_InitAll(int clock, int sampleRate)
{
for(int chip=0; chip<MAX_8910; chip++)
{
struct AY8910 * PSG = &AYPSG[chip];
memset(PSG, 0, sizeof(struct AY8910));
PSG->SampleRate = sampleRate;
memset(&AYPSG[chip], 0, sizeof(struct AY8910));
AYPSG[chip].SampleRate = sampleRate;
AY8910_set_clock(chip, clock);
build_mixer_table(chip);
}
}
void AY8910_InitClock(int clock)
{
for(int chip=0; chip<MAX_8910; chip++)
AY8910_set_clock(chip, clock);
}
#endif
uint8_t * AY8910_GetRegsPtr(uint16_t chipNum)
{
if (chipNum >= MAX_8910)
return NULL;
return &AYPSG[chipNum].Regs[0];
}

View File

@ -3,14 +3,29 @@
#include <stdint.h>
#define USE_NEW_AY8910
#define MAX_8910 4
#ifndef USE_NEW_AY8910
void _AYWriteReg(int n, int r, int v);
void AY8910_reset(int chip);
void AY8910Update(int chip, int16_t ** buffer, int length);
void AY8910_InitAll(int clock, int sampleRate);
void AY8910_InitClock(int clock);
uint8_t * AY8910_GetRegsPtr(uint16_t chipNum);
#else
// Exported functions
void AYInit(void);
void AYReset(int chipNum);
void AYWrite(int chipNum, int reg, int value);
uint16_t AYGetSample(int chipNum);
// Exported variables
extern bool logAYInternal;
extern float maxVolume;
#endif
#endif

View File

@ -13,8 +13,10 @@
#include "mmu.h"
#include "apple2.h"
#include "ay8910.h"
#include "firmware.h"
#include "log.h"
#include "mos6522via.h"
#include "sound.h"
#include "video.h"
@ -69,6 +71,7 @@ uint8_t * mainMemoryHGRW = &ram[0x2000]; // $2000 - $3FFF (write)
uint8_t * slotMemory = &rom[0xC100]; // $C100 - $CFFF
uint8_t * slot3Memory = &rom[0xC300]; // $C300 - $C3FF
uint8_t * slot4Memory = &rom[0xC400]; // $C400 - $C4FF
uint8_t * slot6Memory = &diskROM[0]; // $C600 - $C6FF
uint8_t * lcBankMemoryR = &ram[0xD000]; // $D000 - $DFFF (read)
uint8_t * lcBankMemoryW = &ram[0xD000]; // $D000 - $DFFF (write)
@ -126,6 +129,8 @@ void SwitchIOUDIS(uint16_t, uint8_t);
uint8_t Slot6R(uint16_t);
void Slot6W(uint16_t, uint8_t);
void HandleSlot6(uint16_t, uint8_t);
uint8_t MBRead(uint16_t);
void MBWrite(uint16_t, uint8_t);
uint8_t ReadButton0(uint16_t);
uint8_t ReadButton1(uint16_t);
uint8_t ReadPaddle0(uint16_t);
@ -196,10 +201,10 @@ AddressMap memoryMap[] = {
// This will overlay the slotMemory accessors for slot 6 ROM
{ 0xC300, 0xC3FF, AM_ROM, &slot3Memory, 0, 0, 0 },
{ 0xC600, 0xC6FF, AM_ROM, &slot6Memory, 0, 0, 0 },
{ 0xC400, 0xC4FF, AM_READ_WRITE, 0, 0, MBRead, MBWrite },
{ 0xD000, 0xDFFF, AM_BANKED, &lcBankMemoryR, &lcBankMemoryW, 0, 0 },
{ 0xE000, 0xFFFF, AM_BANKED, &upperMemoryR, &upperMemoryW, 0, 0 },
// { 0x0000, 0x0000, AM_END_OF_LIST, 0, 0, 0, 0 }
ADDRESS_MAP_END
};
@ -861,6 +866,208 @@ void HandleSlot6(uint16_t address, uint8_t byte)
}
uint8_t MBRead(uint16_t address)
{
#if 1
// Not sure [Seems to work OK]
if (!slotCXROM)
{
return slot4Memory[address & 0x00FF];
}
#endif
uint8_t regNum = address & 0x0F;
uint8_t chipNum = (address & 0x80) >> 7;
#if 0
WriteLog("MBRead: address = %X [chip %d, reg %X, clock=$%X]\n", address & 0xFF, chipNum, regNum, GetCurrentV65C02Clock());
#endif
switch (regNum)
{
case 0x00:
return mbvia[chipNum].orb & mbvia[chipNum].ddrb;
case 0x01:
return mbvia[chipNum].ora & mbvia[chipNum].ddra;
case 0x02:
return mbvia[chipNum].ddrb;
case 0x03:
return mbvia[chipNum].ddra;
case 0x04:
return mbvia[chipNum].timer1counter & 0xFF;
case 0x05:
return (mbvia[chipNum].timer1counter & 0xFF00) >> 8;
case 0x06:
return mbvia[chipNum].timer1latch & 0xFF;
case 0x07:
return (mbvia[chipNum].timer1latch & 0xFF00) >> 8;
case 0x08:
return mbvia[chipNum].timer2counter & 0xFF;
case 0x09:
return (mbvia[chipNum].timer2counter & 0xFF00) >> 8;
case 0x0B:
return mbvia[chipNum].acr;
case 0x0D:
return (mbvia[chipNum].ifr & 0x7F)
| (mbvia[chipNum].ifr & 0x7F ? 0x80 : 0);
case 0x0E:
return mbvia[chipNum].ier | 0x80;
default:
WriteLog("Unhandled 6522 register %X read (chip %d)\n", regNum, chipNum);
}
return 0;
}
static uint8_t regLatch[2];
void MBWrite(uint16_t address, uint8_t byte)
{
uint8_t regNum = address & 0x0F;
uint8_t chipNum = (address & 0x80) >> 7;
/*
NOTES:
bit 7 = L/R channel select (AY chip 1 versus AY chip 2)
0 = Left, 1 = Right
Reg. B is connected to BC1, BDIR, RST' (bits 0, 1, 2)
Left VIA IRQ line is tied to 6502 IRQ line
Rght VIA IRQ line is tied to 6502 NMI line
Register Function
-------- -------------------------
0 Output Register B
1 Output Register A
2 Data Direction Register B
3 Data Direction Register A
4 Timer 1 Low byte counter (& latch)
5 Timer 1 Hgh byte counter (& latch)
6 Timer 1 Low byte latch
7 Timer 1 Hgh byte latch (& reset IRQ flag)
B Aux Control Register
D Interrupt Flag Register
E Interrupt Enable Register
bit 6 of ACR is like so:
0: Timed interrupt each time Timer 1 is loaded
1: Continuous interrupts
bit 7 enables PB7 (bit 6 controls output type):
0: One shot output
1: Square wave output
*/
#if 0
WriteLog("MBWrite: address = %X, byte= %X [clock=$%X]", address & 0xFF, byte, GetCurrentV65C02Clock());
if (regNum == 0)
WriteLog("[OUTB -> %s%s%s]\n", (byte & 0x01 ? "BC1" : ""), (byte & 0x02 ? " BDIR" : ""), (byte & 0x04 ? " RST'" : ""));
else if (regNum == 1)
WriteLog("[OUTA -> %02X]\n", byte);
else if (regNum == 2)
WriteLog("[DDRB -> %02X]\n", byte);
else if (regNum == 3)
WriteLog("[DDRA -> %02X]\n", byte);
else
WriteLog("\n");
#endif
switch (regNum)
{
case 0x00:
// Control of the AY-3-8912 is thru this port pretty much...
mbvia[chipNum].orb = byte;
if ((byte & 0x04) == 0)
#ifdef USE_NEW_AY8910
AYReset(chipNum);
#else
AY8910_reset(chipNum);
#endif
else if ((byte & 0x03) == 0x03)
regLatch[chipNum] = mbvia[chipNum].ora;
else if ((byte & 0x03) == 0x02)
#ifdef USE_NEW_AY8910
AYWrite(chipNum, regLatch[chipNum], mbvia[chipNum].ora);
#else
_AYWriteReg(chipNum, regLatch[chipNum], mbvia[chipNum].ora);
#endif
break;
case 0x01:
mbvia[chipNum].ora = byte;
break;
case 0x02:
mbvia[chipNum].ddrb = byte;
break;
case 0x03:
mbvia[chipNum].ddra = byte;
break;
case 0x04:
mbvia[chipNum].timer1latch = (mbvia[chipNum].timer1latch & 0xFF00)
| byte;
break;
case 0x05:
mbvia[chipNum].timer1latch = (mbvia[chipNum].timer1latch & 0x00FF)
| (((uint16_t)byte) << 8);
mbvia[chipNum].timer1counter = mbvia[chipNum].timer1latch;
mbvia[chipNum].ifr &= 0x3F; // Clear T1 interrupt flag
break;
case 0x06:
mbvia[chipNum].timer1latch = (mbvia[chipNum].timer1latch & 0xFF00)
| byte;
break;
case 0x07:
mbvia[chipNum].timer1latch = (mbvia[chipNum].timer1latch & 0x00FF)
| (((uint16_t)byte) << 8);
mbvia[chipNum].ifr &= 0x3F; // Clear T1 interrupt flag
break;
case 0x0B:
mbvia[chipNum].acr = byte;
break;
case 0x0D:
mbvia[chipNum].ifr &= ~byte;
break;
case 0x0E:
if (byte & 0x80)
// Setting bits in the IER
mbvia[chipNum].ier |= byte;
else
// Clearing bits in the IER
mbvia[chipNum].ier &= ~byte;
break;
default:
WriteLog("Unhandled 6522 register $%X write $%02X (chip %d)\n", regNum, byte, chipNum);
}
}
uint8_t ReadButton0(uint16_t)
{
return (uint8_t)openAppleDown << 7;
@ -900,9 +1107,9 @@ uint8_t ReadDHIRES(uint16_t)
// it actually sees the RAM access done by the video generation hardware. Some
// programs exploit this, so we emulate it here.
// N.B.: frameCycles will be off by the true amount because this only increments
// by the amount of a speaker cycle, not the cycle count when the access
// happens... !!! FIX !!!
// N.B.: frameCycles will be off by the true amount because this only
// increments by the amount of a speaker cycle, not the cycle count when
// the access happens... !!! FIX !!!
uint8_t ReadFloatingBus(uint16_t)
{
// Get the currently elapsed cycle count for this frame

20
src/mos6522via.cpp Normal file
View File

@ -0,0 +1,20 @@
// Mockingboard support (6522 interface)
//
// by James Hammons
// (C) 2018 Underground Software
//
#include "mos6522via.h"
#include <string.h> // for memset()
MOS6522VIA mbvia[4];
void ResetMBVIAs(void)
{
for(int i=0; i<4; i++)
memset(&mbvia[i], 0, sizeof(MOS6522VIA));
}

31
src/mos6522via.h Normal file
View File

@ -0,0 +1,31 @@
// Mockingboard support
//
// by James Hammons
// (C) 2018 Underground Software
//
#ifndef __MOS6522VIA_H__
#define __MOS6522VIA_H__
#include <stdint.h>
struct MOS6522VIA
{
uint8_t orb, ora; // Output Register B, A
uint8_t ddrb, ddra; // Data Direction Register B, A
uint16_t timer1counter; // Timer 1 Counter
uint16_t timer1latch; // Timer 1 Latch
uint16_t timer2counter; // Timer 2 Counter
uint8_t acr; // Auxillary Control Register
uint8_t ifr; // Interrupt Flags Register
uint8_t ier; // Interrupt Enable Register
};
extern MOS6522VIA mbvia[];
void ResetMBVIAs(void);
#endif // __MOS6522VIA_H__

View File

@ -18,6 +18,7 @@
#include <SDL2/SDL.h>
#include "sdlemu_config.h"
#include "log.h"
#include "video.h"
using namespace std;
@ -53,6 +54,9 @@ void LoadSettings(void)
settings.renderType = sdlemu_getval_int("renderType", 0);
settings.autoStateSaving = sdlemu_getval_bool("autoSaveState", true);
settings.winX = sdlemu_getval_int("windowX", 250);
settings.winY = sdlemu_getval_int("windowY", 100);
// Keybindings in order of U, D, L, R, C, B, A, Op, Pa, 0-9, #, *
settings.p1KeyBindings[0] = sdlemu_getval_int("p1k_up", SDL_SCANCODE_UP);
settings.p1KeyBindings[1] = sdlemu_getval_int("p1k_down", SDL_SCANCODE_DOWN);
@ -110,6 +114,7 @@ void LoadSettings(void)
//
void SaveSettings(void)
{
SDL_GetWindowPosition(sdlWindow, &settings.winX, &settings.winY);
}

View File

@ -32,6 +32,11 @@ struct Settings
uint32_t renderType;
bool autoStateSaving; // Auto-state loading/saving on entry/exit
// Window settings
int winX;
int winY;
// Keybindings in order of U, D, L, R, C, B, A, Op, Pa, 0-9, #, *
uint16_t p1KeyBindings[21];

View File

@ -24,13 +24,13 @@
#include <string.h> // For memset, memcpy
#include <SDL2/SDL.h>
#include "ay8910.h"
#include "log.h"
// Useful defines
//#define DEBUG
#define SAMPLE_RATE (48000.0)
#define SAMPLES_PER_FRAME (SAMPLE_RATE / 60.0)
#define CYCLES_PER_SAMPLE (1024000.0 / SAMPLE_RATE)
// 32K ought to be enough for anybody
@ -45,13 +45,13 @@ static SDL_AudioSpec desired, obtained;
static SDL_AudioDeviceID device;
static bool soundInitialized = false;
static bool speakerState = false;
static int16_t soundBuffer[SOUND_BUFFER_SIZE];
static uint16_t soundBuffer[SOUND_BUFFER_SIZE];
static uint32_t soundBufferPos;
static uint64_t lastToggleCycles;
static SDL_cond * conditional = NULL;
static SDL_mutex * mutex = NULL;
static SDL_mutex * mutex2 = NULL;
static int16_t sample;
//static uint64_t lastToggleCycles;
//static SDL_cond * conditional = NULL;
//static SDL_mutex * mutex = NULL;
//static SDL_mutex * mutex2 = NULL;
static uint16_t sample;
static uint8_t ampPtr = 12; // Start with -2047 - +2047
static int16_t amplitude[17] = { 0, 1, 2, 3, 7, 15, 31, 63, 127, 255,
511, 1023, 2047, 4095, 8191, 16383, 32767 };
@ -67,10 +67,10 @@ static void SDLSoundCallback(void * userdata, Uint8 * buffer, int length);
void SoundInit(void)
{
SDL_zero(desired);
desired.freq = SAMPLE_RATE; // SDL will do conversion on the fly, if it can't get the exact rate. Nice!
desired.format = AUDIO_S16SYS; // This uses the native endian (for portability)...
desired.freq = SAMPLE_RATE; // SDL will do conversion on the fly, if it can't get the exact rate. Nice!
desired.format = AUDIO_U16SYS; // This uses the native endian (for portability)...
desired.channels = 1;
desired.samples = 512; // Let's try a 1/2K buffer (can always go lower)
desired.samples = 512; // Let's try a 1/2K buffer
desired.callback = SDLSoundCallback;
device = SDL_OpenAudioDevice(NULL, 0, &desired, &obtained, 0);
@ -81,11 +81,11 @@ void SoundInit(void)
return;
}
conditional = SDL_CreateCond();
mutex = SDL_CreateMutex();
mutex2 = SDL_CreateMutex();// Let's try real signalling...
// conditional = SDL_CreateCond();
// mutex = SDL_CreateMutex();
// mutex2 = SDL_CreateMutex();// Let's try real signalling...
soundBufferPos = 0;
lastToggleCycles = 0;
// lastToggleCycles = 0;
sample = desired.silence; // ? wilwok ? yes
SDL_PauseAudioDevice(device, 0); // Start playback!
@ -103,9 +103,9 @@ void SoundDone(void)
{
SDL_PauseAudioDevice(device, 1);
SDL_CloseAudioDevice(device);
SDL_DestroyCond(conditional);
SDL_DestroyMutex(mutex);
SDL_DestroyMutex(mutex2);
// SDL_DestroyCond(conditional);
// SDL_DestroyMutex(mutex);
// SDL_DestroyMutex(mutex2);
WriteLog("Sound: Done.\n");
}
}
@ -128,28 +128,31 @@ void SoundResume(void)
//
// Sound card callback handler
//
static uint32_t sndFrmCnt = 0;
static uint32_t lastStarve = 0;
static void SDLSoundCallback(void * /*userdata*/, Uint8 * buffer8, int length8)
{
sndFrmCnt++;
//WriteLog("SDLSoundCallback(): begin (soundBufferPos=%i)\n", soundBufferPos);
// The sound buffer should only starve when starting which will cause it to
// lag behind the emulation at most by around 1 frame...
// (Actually, this should never happen since we fill the buffer beforehand.)
// (But, then again, if the sound hasn't been toggled for a while, then this
// makes perfect sense as the buffer won't have been filled at all!)
// (Should NOT starve now, now that we properly handle frame edges...)
// Let's try using a mutex for shared resource consumption...
//Actually, I think Lock/UnlockAudio() does this already...
//WriteLog("SDLSoundCallback: soundBufferPos = %i\n", soundBufferPos);
SDL_mutexP(mutex2);
// SDL_mutexP(mutex2);
// Recast this as a 16-bit type...
int16_t * buffer = (int16_t *)buffer8;
uint16_t * buffer = (uint16_t *)buffer8;
uint32_t length = (uint32_t)length8 / 2;
//WriteLog("SDLSoundCallback(): filling buffer...\n");
if (soundBufferPos < length)
{
WriteLog("*** Sound buffer starved (%d short) *** [%d delta %d]\n", length - soundBufferPos, sndFrmCnt, sndFrmCnt - lastStarve);
lastStarve = sndFrmCnt;
#if 1
for(uint32_t i=0; i<length; i++)
buffer[i] = desired.silence;
#else
// The sound buffer is starved...
for(uint32_t i=0; i<soundBufferPos; i++)
buffer[i] = soundBuffer[i];
@ -160,6 +163,7 @@ static void SDLSoundCallback(void * /*userdata*/, Uint8 * buffer8, int length8)
// Reset soundBufferPos to start of buffer...
soundBufferPos = 0;
#endif
}
else
{
@ -176,9 +180,9 @@ static void SDLSoundCallback(void * /*userdata*/, Uint8 * buffer8, int length8)
// Free the mutex...
//WriteLog("SDLSoundCallback(): SDL_mutexV(mutex2)\n");
SDL_mutexV(mutex2);
// SDL_mutexV(mutex2);
// Wake up any threads waiting for the buffer to drain...
SDL_CondSignal(conditional);
// SDL_CondSignal(conditional);
//WriteLog("SDLSoundCallback(): end\n");
}
@ -186,23 +190,43 @@ static void SDLSoundCallback(void * /*userdata*/, Uint8 * buffer8, int length8)
// This is called by the main CPU thread every ~21.666 cycles.
void WriteSampleToBuffer(void)
{
#ifdef USE_NEW_AY8910
uint16_t s1 = AYGetSample(0);
uint16_t s2 = AYGetSample(1);
uint16_t adjustedMockingboard = s1 + s2;
#else
int16_t s1, s2, s3, s4, s5, s6;
int16_t * bufPtrs[6] = { &s1, &s2, &s3, &s4, &s5, &s6 };
AY8910Update(0, bufPtrs, 1);
AY8910Update(1, &bufPtrs[3], 1);
int16_t adjustedMockingboard = (s1 / 8) + (s2 / 8) + (s3 / 8)
+ (s4 / 8) + (s5 / 8) + (s6 / 8);
#endif
//need to do this *before* mixing, as by this time, it's too late and the sample is probably already oversaturated
// adjustedMockingboard /= 8;
//WriteLog("WriteSampleToBuffer(): SDL_mutexP(mutex2)\n");
SDL_mutexP(mutex2);
// SDL_mutexP(mutex2);
// This should almost never happen, but, if it does...
while (soundBufferPos >= (SOUND_BUFFER_SIZE - 1))
{
//WriteLog("WriteSampleToBuffer(): Waiting for sound thread. soundBufferPos=%i, SOUNDBUFFERSIZE-1=%i\n", soundBufferPos, SOUND_BUFFER_SIZE-1);
SDL_mutexV(mutex2); // Release it so sound thread can get it,
SDL_mutexP(mutex); // Must lock the mutex for the cond to work properly...
SDL_CondWait(conditional, mutex); // Sleep/wait for the sound thread
SDL_mutexV(mutex); // Must unlock the mutex for the cond to work properly...
SDL_mutexP(mutex2); // Re-lock it until we're done with it...
// SDL_mutexV(mutex2); // Release it so sound thread can get it,
// SDL_mutexP(mutex); // Must lock the mutex for the cond to work properly...
// SDL_CondWait(conditional, mutex); // Sleep/wait for the sound thread
// SDL_mutexV(mutex); // Must unlock the mutex for the cond to work properly...
// SDL_mutexP(mutex2); // Re-lock it until we're done with it...
SDL_Delay(1);
}
soundBuffer[soundBufferPos++] = sample;
SDL_LockAudioDevice(device);
soundBuffer[soundBufferPos++] = sample + adjustedMockingboard;
SDL_UnlockAudioDevice(device);
// soundBuffer[soundBufferPos++] = sample;
//WriteLog("WriteSampleToBuffer(): SDL_mutexV(mutex2)\n");
SDL_mutexV(mutex2);
// SDL_mutexV(mutex2);
}
@ -212,7 +236,7 @@ void ToggleSpeaker(void)
return;
speakerState = !speakerState;
sample = (speakerState ? amplitude[ampPtr] : -amplitude[ampPtr]);
sample = (speakerState ? amplitude[ampPtr] : 0);//-amplitude[ampPtr]);
}

View File

@ -2,7 +2,7 @@
// SOUND.H
//
// by James Hammons
// (C) 2004 Underground Software
// (C) 2004-2018 Underground Software
//
#ifndef __SOUND_H__
@ -10,6 +10,8 @@
#include <stdint.h>
#define SAMPLE_RATE (48000.0)
// Global variables (exported)
@ -26,3 +28,4 @@ void VolumeDown(void);
uint8_t GetVolume(void);
#endif // __SOUND_H__

View File

@ -132,6 +132,7 @@ static uint8_t CPUCycles[256] = {
2, 5, 5, 2, 4, 4, 6, 2, 2, 4, 4, 2, 4, 4, 6, 2 };
#endif
#if 0
static uint8_t _6502Cycles[256] = {
7, 6, 2, 8, 3, 3, 5, 5, 3, 2, 2, 2, 4, 4, 6, 6,
2, 5, 2, 8, 4, 4, 6, 6, 2, 4, 2, 7, 4, 4, 6, 7,
@ -167,6 +168,7 @@ static uint8_t _65C02Cycles[256] = {
2, 5, 5, 2, 4, 4, 6, 2, 2, 4, 3, 2, 4, 4, 6, 2,
2, 6, 2, 2, 3, 3, 5, 2, 2, 2, 2, 2, 4, 4, 6, 2,
2, 5, 5, 2, 4, 4, 6, 2, 2, 4, 4, 2, 4, 4, 6, 2 };
#endif
#if 0
// ExtraCycles:
@ -1128,9 +1130,10 @@ BEQ Relative BEQ Oper F0 2 2
#define HANDLE_BRANCH_TAKEN(m) \
{ \
uint16_t oldpc = regs.pc; \
uint16_t oldpc = regs.pc; \
regs.pc += m; \
regs.clock++; \
\
if ((oldpc ^ regs.pc) & 0xFF00) \
regs.clock++; \
}
@ -2888,6 +2891,23 @@ void Execute65C02(V65C02REGS * context, uint32_t cycles)
while (regs.clock < endCycles)
{
#if 0
static bool weGo = false;
if (regs.pc == 0x80AE)
{
dumpDis = true;
weGo = true;
}
else if (regs.pc == 0xFCA8 && weGo)
{
dumpDis = false;
WriteLog("\n*** DELAY (A=$%02X)\n\n", regs.a);
}
else if (regs.pc == 0xFCB3 && weGo)
{
dumpDis = true;
}
#endif
#if 0
/*if (regs.pc == 0x4007)
{
dumpDis = true;
@ -3012,12 +3032,22 @@ if (dumpDis)
//if (!(regs.cpuFlags & V65C02_STATE_ILLEGAL_INST))
//instCount[opcode]++;
exec_op[opcode](); // Execute that opcode...
// We need this because the opcode execute could add 1 or 2 cycles
uint64_t clockSave = regs.clock;
// Execute that opcode...
exec_op[opcode]();
regs.clock += CPUCycles[opcode];
// Tell the timer function how many PHI2s have elapsed
if (regs.Timer)
// regs.Timer(CPUCycles[opcode]);
regs.Timer(regs.clock - clockSave);
#ifdef __DEBUG__
if (dumpDis)
WriteLog(" [PC=%04X, SP=%04X, CC=%s%s.%s%s%s%s%s, A=%02X, X=%02X, Y=%02X]\n",
regs.pc, 0x0100 + regs.sp,
WriteLog(" [PC=%04X, SP=01%02X, CC=%s%s.%s%s%s%s%s, A=%02X, X=%02X, Y=%02X]\n",
regs.pc, regs.sp,
(regs.cc & FLAG_N ? "N" : "-"), (regs.cc & FLAG_V ? "V" : "-"),
(regs.cc & FLAG_B ? "B" : "-"), (regs.cc & FLAG_D ? "D" : "-"),
(regs.cc & FLAG_I ? "I" : "-"), (regs.cc & FLAG_Z ? "Z" : "-"),
@ -3052,11 +3082,9 @@ if (regs.pc == 0xFBD8)
{
// Not sure about this...
regs.sp = 0xFF;
regs.cc = FLAG_B | FLAG_I; // Reset the CC register
regs.cc = FLAG_I; // Reset the CC register
regs.pc = RdMemW(0xFFFC); // And load PC with the RESET vector
// context->cpuFlags &= ~V65C02_ASSERT_LINE_RESET;
// regs.cpuFlags &= ~V65C02_ASSERT_LINE_RESET;
context->cpuFlags = 0; // Clear CPU flags...
regs.cpuFlags = 0;
#ifdef __DEBUG__
@ -3085,6 +3113,8 @@ WriteLog("\n*** NMI ***\n\n");
{
#ifdef __DEBUG__
WriteLog("\n*** IRQ ***\n\n");
WriteLog("Clock=$%X\n", regs.clock);
//dumpDis = true;
#endif
regs.WrMem(0x0100 + regs.sp--, regs.pc >> 8); // Save PC and CC
regs.WrMem(0x0100 + regs.sp--, regs.pc & 0xFF);
@ -3118,3 +3148,12 @@ uint64_t GetCurrentV65C02Clock(void)
return regs.clock;
}
//
// Assert 65C02 line in current context
//
void AssertLine(uint16_t flags)
{
regs.cpuFlags |= flags;
}

View File

@ -12,14 +12,14 @@
// Useful defines
#define FLAG_N 0x80 // Negative
#define FLAG_V 0x40 // oVerflow
#define FLAG_UNK 0x20 // ??? (always set when read?)
#define FLAG_B 0x10 // Break
#define FLAG_D 0x08 // Decimal
#define FLAG_I 0x04 // Interrupt
#define FLAG_Z 0x02 // Zero
#define FLAG_C 0x01 // Carry
#define FLAG_N 0x80 // Negative
#define FLAG_V 0x40 // oVerflow
#define FLAG_UNK 0x20 // ??? (always set when read?)
#define FLAG_B 0x10 // Break
#define FLAG_D 0x08 // Decimal
#define FLAG_I 0x04 // Interrupt
#define FLAG_Z 0x02 // Zero
#define FLAG_C 0x01 // Carry
#define V65C02_ASSERT_LINE_RESET 0x0001 // v65C02 RESET line
#define V65C02_ASSERT_LINE_IRQ 0x0002 // v65C02 IRQ line
@ -31,18 +31,18 @@
struct V65C02REGS
{
uint16_t pc; // 65C02 PC register
uint8_t cc; // 65C02 Condition Code register
uint8_t sp; // 65C02 System stack pointer (bound to $01xx)
uint8_t a; // 65C02 A register
uint8_t x; // 65C02 X index register
uint8_t y; // 65C02 Y register
// uint32_t clock; // 65C02 clock (@ 1 MHz, wraps at 71.5 minutes)
uint64_t clock; // 65C02 clock (@ 1 MHz, wraps at 570,842 years)
uint16_t pc; // 65C02 PC register
uint8_t cc; // 65C02 Condition Code register
uint8_t sp; // 65C02 System stack pointer (bound to $01xx)
uint8_t a; // 65C02 A register
uint8_t x; // 65C02 X index register
uint8_t y; // 65C02 Y register
uint64_t clock; // 65C02 clock (@ 1 MHz, wraps at 570,842 years)
uint8_t (* RdMem)(uint16_t); // Address of BYTE read routine
void (* WrMem)(uint16_t, uint8_t); // Address of BYTE write routine
uint16_t cpuFlags; // v65C02 IRQ/RESET flags
uint64_t overflow; // # of cycles we went over last time through
void (* Timer)(uint16_t); // Address of Timer routine
uint16_t cpuFlags; // v65C02 IRQ/RESET flags
uint64_t overflow; // # of cycles we went over last time through
};
// Global variables (exported)
@ -53,5 +53,7 @@ extern bool dumpDis;
void Execute65C02(V65C02REGS *, uint32_t); // Function to execute 65C02 instructions
uint64_t GetCurrentV65C02Clock(void); // Get the clock of the currently executing CPU
void AssertLine(uint16_t); // Assert 65C02 line in current context
#endif // __V65C02_H__

View File

@ -4,7 +4,7 @@
// All the video modes that a real Apple 2 supports are handled here
//
// by James Hammons
// (c) 2005-2017 Underground Software
// (c) 2005-2018 Underground Software
//
// JLH = James Hammons <jlhamm@acm.org>
//
@ -20,7 +20,8 @@
// like white mono does [DONE]
// - Double HiRes [DONE]
// - 80 column text [DONE]
// - Fix OSD text display so that it's visible no matter what background is there [DONE]
// - Fix OSD text display so that it's visible no matter what background is
// there [DONE]
//
// Display routines seem MUCH slower now... !!! INVESTIGATE !!! [not anymore]
@ -74,13 +75,14 @@ bool hiRes = false;
bool alternateCharset = false;
bool col80Mode = false;
SDL_Renderer * sdlRenderer = NULL;
SDL_Window * sdlWindow = NULL;
// Local variables
static SDL_Window * sdlWindow = NULL;
static SDL_Texture * sdlTexture = NULL;
static uint32_t * scrBuffer;
static int scrPitch;
static bool showFrameTicks = false;
// We set up the colors this way so that they'll be endian safe
// when we cast them to a uint32_t. Note that the format is RGBA.
@ -236,7 +238,6 @@ uint16_t appleHiresToMono[0x200] = {
0x207F, 0x387F, 0x267F, 0x3E7F, 0x21FF, 0x39FF, 0x27FF, 0x3FFF // $Fx
};
//static uint8_t blurTable[0x800][8]; // Color TV blur table
static uint8_t blurTable[0x80][8]; // Color TV blur table
static uint8_t mirrorTable[0x100];
static uint32_t * palette = (uint32_t *)altColors;
@ -262,34 +263,6 @@ void SetupBlurTable(void)
// Odd. Doing the bit patterns from 0-$7F doesn't work, but going
// from 0-$7FF stepping by 16 does. Hm.
// Well, it seems that going from 0-$7F doesn't have enough precision to do the job.
#if 0
// for(uint16_t bitPat=0; bitPat<0x800; bitPat++)
for(uint16_t bitPat=0; bitPat<0x80; bitPat++)
{
/* uint16_t w3 = bitPat & 0x888;
uint16_t w2 = bitPat & 0x444;
uint16_t w1 = bitPat & 0x222;
uint16_t w0 = bitPat & 0x111;*/
uint16_t w3 = bitPat & 0x88;
uint16_t w2 = bitPat & 0x44;
uint16_t w1 = bitPat & 0x22;
uint16_t w0 = bitPat & 0x11;
uint16_t blurred3 = (w3 | (w3 >> 1) | (w3 >> 2) | (w3 >> 3)) & 0x00FF;
uint16_t blurred2 = (w2 | (w2 >> 1) | (w2 >> 2) | (w2 >> 3)) & 0x00FF;
uint16_t blurred1 = (w1 | (w1 >> 1) | (w1 >> 2) | (w1 >> 3)) & 0x00FF;
uint16_t blurred0 = (w0 | (w0 >> 1) | (w0 >> 2) | (w0 >> 3)) & 0x00FF;
for(int8_t i=7; i>=0; i--)
{
uint8_t color = (((blurred0 >> i) & 0x01) << 3)
| (((blurred1 >> i) & 0x01) << 2)
| (((blurred2 >> i) & 0x01) << 1)
| ((blurred3 >> i) & 0x01);
blurTable[bitPat][7 - i] = color;
}
}
#else
for(uint16_t bitPat=0; bitPat<0x800; bitPat+=0x10)
{
uint16_t w0 = bitPat & 0x111, w1 = bitPat & 0x222, w2 = bitPat & 0x444, w3 = bitPat & 0x888;
@ -308,7 +281,6 @@ void SetupBlurTable(void)
blurTable[bitPat >> 4][7 - i] = color;
}
}
#endif
for(int i=0; i<256; i++)
{
@ -352,6 +324,12 @@ void CycleScreenTypes(void)
}
void ToggleTickDisplay(void)
{
showFrameTicks = !showFrameTicks;
}
static uint32_t msgTicks = 0;
static char message[4096];
@ -367,31 +345,39 @@ void SpawnMessage(const char * text, ...)
}
static void DrawString2(uint32_t x, uint32_t y, uint32_t color);
static void DrawString2(uint32_t x, uint32_t y, uint32_t color, char * msg);
static void DrawString(void)
{
//This approach works, and seems to be fast enough... Though it probably would
//be better to make the oversized font to match this one...
for(uint32_t x=7; x<=9; x++)
for(uint32_t y=7; y<=9; y++)
DrawString2(x, y, 0x00000000);
DrawString2(x, y, 0x00000000, message);
DrawString2(8, 8, 0x0020FF20);
DrawString2(8, 8, 0x0020FF20, message);
}
static void DrawString2(uint32_t x, uint32_t y, uint32_t color)
static void DrawString(uint32_t x, uint32_t y, uint32_t color, char * msg)
{
//uint32_t x = 8, y = 8;
uint32_t length = strlen(message), address = x + (y * VIRTUAL_SCREEN_WIDTH);
// uint32_t color = 0x0020FF20;
//This could be done ahead of time, instead of on each pixel...
//(Now it is!)
//This approach works, and seems to be fast enough... Though it probably would
//be better to make the oversized font to match this one...
for(uint32_t xx=x-1; xx<=x+1; xx++)
for(uint32_t yy=y-1; yy<=y+1; yy++)
DrawString2(xx, yy, 0x00000000, msg);
DrawString2(x, y, color, msg);
}
static void DrawString2(uint32_t x, uint32_t y, uint32_t color, char * msg)
{
uint32_t length = strlen(msg), address = x + (y * VIRTUAL_SCREEN_WIDTH);
uint8_t nBlue = (color >> 16) & 0xFF, nGreen = (color >> 8) & 0xFF, nRed = color & 0xFF;
for(uint32_t i=0; i<length; i++)
{
uint8_t c = message[i];
uint8_t c = msg[i];
c = (c < 32 ? 0 : c - 32);
uint32_t fontAddr = (uint32_t)c * FONT_WIDTH * FONT_HEIGHT;
@ -399,14 +385,6 @@ static void DrawString2(uint32_t x, uint32_t y, uint32_t color)
{
for(uint32_t xx=0; xx<FONT_WIDTH; xx++)
{
/* uint8_t fontTrans = font1[fontAddr++];
// uint32_t newTrans = (fontTrans * transparency / 255) << 24;
uint32_t newTrans = fontTrans << 24;
uint32_t pixel = newTrans | color;
*(scrBuffer + address + xx + (yy * VIRTUAL_SCREEN_WIDTH)) = pixel;//*/
// uint8_t trans = font1[fontAddr++];
uint8_t trans = font2[fontAddr++];
if (trans)
@ -438,6 +416,65 @@ static void DrawString2(uint32_t x, uint32_t y, uint32_t color)
}
static void DrawFrameTicks(void)
{
uint32_t color = 0x00FF2020;
uint32_t address = 8 + (24 * VIRTUAL_SCREEN_WIDTH);
for(uint32_t i=0; i<17; i++)
{
for(uint32_t yy=0; yy<5; yy++)
{
for(uint32_t xx=0; xx<9; xx++)
{
//THIS IS NOT ENDIAN SAFE
//NB: Setting the alpha channel here does nothing.
*(scrBuffer + address + xx + (yy * VIRTUAL_SCREEN_WIDTH)) = 0x7F000000;
}
}
address += (5 * VIRTUAL_SCREEN_WIDTH);
}
address = 8 + (24 * VIRTUAL_SCREEN_WIDTH);
// frameTicks is the amount of time remaining; so to show the amount
// consumed, we subtract it from 17.
uint32_t bars = 17 - frameTicks;
if (bars & 0x80000000)
bars = 0;
for(uint32_t i=0; i<17; i++)
{
for(uint32_t yy=1; yy<4; yy++)
{
for(uint32_t xx=1; xx<8; xx++)
{
//THIS IS NOT ENDIAN SAFE
//NB: Setting the alpha channel here does nothing.
*(scrBuffer + address + xx + (yy * VIRTUAL_SCREEN_WIDTH)) = (i < bars ? color : 0x003F0000);
}
}
address += (5 * VIRTUAL_SCREEN_WIDTH);
}
static char msg[32];
if ((frameTimePtr % 15) == 0)
{
// uint32_t prevClock = (frameTimePtr + 1) % 60;
uint64_t prevClock = (frameTimePtr + 1) % 60;
// float fps = 59.0f / (((float)frameTime[frameTimePtr] - (float)frameTime[prevClock]) / 1000.0f);
double fps = 59.0 / ((double)(frameTime[frameTimePtr] - frameTime[prevClock]) / (double)SDL_GetPerformanceFrequency());
sprintf(msg, "%.1lf FPS", fps);
}
DrawString(20, 24, color, msg);
}
static void Render40ColumnTextLine(uint8_t line)
{
uint32_t pixelOn = (screenType == ST_GREEN_MONO ? 0xFF61FF61 : 0xFFFFFFFF);
@ -1090,6 +1127,9 @@ void RenderVideoFrame(void)
DrawString();
msgTicks--;
}
if (showFrameTicks)
DrawFrameTicks();
}
@ -1104,19 +1144,35 @@ bool InitVideo(void)
return false;
}
// int retVal = SDL_CreateWindowAndRenderer(VIRTUAL_SCREEN_WIDTH * 2, VIRTUAL_SCREEN_HEIGHT * 2, SDL_WINDOW_OPENGL, &sdlWindow, &sdlRenderer);
#if 0
int retVal = SDL_CreateWindowAndRenderer(VIRTUAL_SCREEN_WIDTH * 2, VIRTUAL_SCREEN_HEIGHT * 2, 0, &sdlWindow, &sdlRenderer);
// int retVal = SDL_CreateWindowAndRenderer(VIRTUAL_SCREEN_WIDTH * 1, VIRTUAL_SCREEN_HEIGHT * 1, 0, &sdlWindow, &sdlRenderer);
if (retVal != 0)
{
WriteLog("Video: Could not window and/or renderer: %s\n", SDL_GetError());
WriteLog("Video: Could not create window and/or renderer: %s\n", SDL_GetError());
return false;
}
#else
sdlWindow = SDL_CreateWindow("Apple2", settings.winX, settings.winY, VIRTUAL_SCREEN_WIDTH * 2, VIRTUAL_SCREEN_HEIGHT * 2, 0);
if (sdlWindow == NULL)
{
WriteLog("Video: Could not create window: %s\n", SDL_GetError());
return false;
}
// Make the scaled rendering look smoother.
sdlRenderer = SDL_CreateRenderer(sdlWindow, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (sdlRenderer == NULL)
{
WriteLog("Video: Could not create renderer: %s\n", SDL_GetError());
return false;
}
#endif
// Make sure what we put there is what we get:
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear");
// SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "nearest");
SDL_RenderSetLogicalSize(sdlRenderer, VIRTUAL_SCREEN_WIDTH, VIRTUAL_SCREEN_HEIGHT);
// Set the application's icon & title...

View File

@ -23,6 +23,7 @@ extern bool hiRes;
extern bool alternateCharset;
extern bool col80Mode;
extern SDL_Renderer * sdlRenderer;
extern SDL_Window * sdlWindow;
// Functions (exported)
@ -34,7 +35,7 @@ bool InitVideo(void);
void VideoDone(void);
void RenderAppleScreen(SDL_Renderer *);
void ToggleFullScreen(void);
void ToggleTickDisplay(void);
// Exported crap