/* AppleWin : An Apple //e emulator for Windows Copyright (C) 1994-1996, Michael O'Brien Copyright (C) 1999-2001, Oliver Schmidt Copyright (C) 2002-2005, Tom Charlesworth Copyright (C) 2006-2007, Tom Charlesworth, Michael Pohoreski AppleWin is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. AppleWin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with AppleWin; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /* Description: Mockingboard/Phasor emulation * * Author: Copyright (c) 2002-2006, Tom Charlesworth */ // History: // // v1.12.07.1 (30 Dec 2005) // - Update 6522 TIMERs after every 6502 opcode, giving more precise IRQs // - Minimum TIMER freq is now 0x100 cycles // - Added Phasor support // // v1.12.06.1 (16 July 2005) // - Reworked 6522's ORB -> AY8910 decoder // - Changed MB output so L=All voices from AY0 & AY2 & R=All voices from AY1 & AY3 // - Added crude support for Votrax speech chip (by using SSI263 phonemes) // // v1.12.04.1 (14 Sep 2004) // - Switch MB output from dual-mono to stereo. // - Relaxed TIMER1 freq from ~62Hz (period=0x4000) to ~83Hz (period=0x3000). // // 25 Apr 2004: // - Added basic support for the SSI263 speech chip // // 15 Mar 2004: // - Switched to MAME's AY8910 emulation (includes envelope support) // // v1.12.03 (11 Jan 2004) // - For free-running 6522 timer1 IRQ, reload with current ACCESS_TIMER1 value. // (Fixes Ultima 4/5 playback speed problem.) // // v1.12.01 (24 Nov 2002) // - Shaped the tone waveform more logarithmically // - Added support for MB ena/dis switch on Config dialog // - Added log file support // // v1.12.00 (17 Nov 2002) // - Initial version (no AY8910 envelope support) // // Notes on Votrax chip (on original Mockingboards): // From Crimewave (Penguin Software): // . Init: // . DDRB = 0xFF // . PCR = 0xB0 // . IER = 0x90 // . ORB = 0x03 (PAUSE0) or 0x3F (STOP) // . IRQ: // . ORB = Phoneme value // . IRQ last phoneme complete: // . IER = 0x10 // . ORB = 0x3F (STOP) // #include "StdAfx.h" #include "Mockingboard.h" #include "SaveState_Structs_v1.h" #include "Core.h" #include "CardManager.h" #include "CPU.h" #include "Log.h" #include "Memory.h" #include "SoundCore.h" #include "SynchronousEventManager.h" #include "YamlHelper.h" #include "Riff.h" #include "AY8910.h" #include "SSI263Phonemes.h" #define LOG_SSI263 0 #define LOG_SSI263B 0 // Alternate SSI263 logging (use in conjunction with CPU.cpp's LOG_IRQ_TAKEN_AND_RTI) #define SY6522_DEVICE_A 0 #define SY6522_DEVICE_B 1 #define SLOT4 4 #define SLOT5 5 #define NUM_MB 2 #define NUM_DEVS_PER_MB 2 #define NUM_AY8910 (NUM_MB*NUM_DEVS_PER_MB) #define NUM_SY6522 NUM_AY8910 #define NUM_VOICES_PER_AY8910 3 #define NUM_VOICES (NUM_AY8910*NUM_VOICES_PER_AY8910) // Chip offsets from card base. #define SY6522A_Offset 0x00 #define SY6522B_Offset 0x80 #define SSI263_Offset 0x40 //#define Phasor_SY6522A_CS 4 //#define Phasor_SY6522B_CS 7 //#define Phasor_SY6522A_Offset (1<bTimer1Active = true; if (pMB->sy6522.IER & IxR_TIMER1) // Using 6522 interrupt g_nMBTimerDevice = pMB->nAY8910Number; else if (pMB->sy6522.ACR & RM_FREERUNNING) // Polling 6522 IFR (GH#496) g_nMBTimerDevice = pMB->nAY8910Number; } // The assumption was that timer1 was only active if IER.TIMER1=1 // . Not true, since IFR can be polled (with IER.TIMER1=0) static void StartTimer1_LoadStateV1(SY6522_AY8910* pMB) { if ((pMB->sy6522.IER & IxR_TIMER1) == 0x00) return; pMB->bTimer1Active = true; g_nMBTimerDevice = pMB->nAY8910Number; } static void StopTimer1(SY6522_AY8910* pMB) { pMB->bTimer1Active = false; g_nMBTimerDevice = kTIMERDEVICE_INVALID; } //----------------------------------------------------------------------------- static void StartTimer2(SY6522_AY8910* pMB) { pMB->bTimer2Active = true; // NB. Can't mimic StartTimer1() as that would stomp on global state // TODO: Switch to per-device state } static void StopTimer2(SY6522_AY8910* pMB) { pMB->bTimer2Active = false; } //----------------------------------------------------------------------------- static void SY6522_Write(BYTE nDevice, BYTE nReg, BYTE nValue); static void ResetSY6522(SY6522_AY8910* pMB, const bool powerCycle) { if (powerCycle) { memset(&pMB->sy6522,0,sizeof(SY6522)); pMB->sy6522.TIMER1_LATCH.w = 0xffff; // Some random value (but pick $ffff so it's deterministic) // . NB. if it's too small (< ~$0007) then MB detection routines will fail! } SY6522_Write(pMB->nAY8910Number, 0x0b, 0x00); // ACR = 0x00: T1 one-shot mode SY6522_Write(pMB->nAY8910Number, 0x0d, 0x7f); // IFR = 0x7F: de-assert any IRQs SY6522_Write(pMB->nAY8910Number, 0x0e, 0x7f); // IFE = 0x7F: disable all IRQs StopTimer1(pMB); StopTimer2(pMB); pMB->nAYCurrentRegister = 0; pMB->state = AY_INACTIVE; pMB->stateB = AY_INACTIVE; } //----------------------------------------------------------------------------- static void AY8910_Write(BYTE nDevice, BYTE /*nReg*/, BYTE nValue, BYTE nAYDevice) { g_bMB_RegAccessedFlag = true; SY6522_AY8910* pMB = &g_MB[nDevice]; if ((nValue & 4) == 0) { // RESET: Reset AY8910 only AY8910_reset(nDevice+2*nAYDevice); } else { // Determine the AY8910 inputs int nBDIR = (nValue & 2) ? 1 : 0; const int nBC2 = 1; // Hardwired to +5V int nBC1 = nValue & 1; MockingboardUnitState_e nAYFunc = (MockingboardUnitState_e) ((nBDIR<<2) | (nBC2<<1) | nBC1); MockingboardUnitState_e& state = (nAYDevice == 0) ? pMB->state : pMB->stateB; // GH#659 #if _DEBUG if (!g_bPhasorEnable) _ASSERT(nAYDevice == 0); if (nAYFunc == AY_WRITE || nAYFunc == AY_LATCH) _ASSERT(state == AY_INACTIVE); #endif if (state == AY_INACTIVE) // GH#320: functions only work from inactive state { switch (nAYFunc) { case AY_INACTIVE: // 4: INACTIVE break; case AY_READ: // 5: READ FROM PSG (need to set DDRA to input) if (g_bPhasorEnable) pMB->sy6522.ORA = 0xff & (pMB->sy6522.DDRA ^ 0xff); // Phasor doesn't support reading AY8913s - it just reads 1's for the input bits else pMB->sy6522.ORA = AYReadReg(nDevice+2*nAYDevice, pMB->nAYCurrentRegister) & (pMB->sy6522.DDRA ^ 0xff); break; case AY_WRITE: // 6: WRITE TO PSG _AYWriteReg(nDevice+2*nAYDevice, pMB->nAYCurrentRegister, pMB->sy6522.ORA); break; case AY_LATCH: // 7: LATCH ADDRESS // http://www.worldofspectrum.org/forums/showthread.php?t=23327 // Selecting an unused register number above 0x0f puts the AY into a state where // any values written to the data/address bus are ignored, but can be read back // within a few tens of thousands of cycles before they decay to zero. if(pMB->sy6522.ORA <= 0x0F) pMB->nAYCurrentRegister = pMB->sy6522.ORA & 0x0F; // else Pro-Mockingboard (clone from HK) break; } } state = nAYFunc; } } // TODO: RMW opcodes: dec,inc,asl,lsr,rol,ror (abs16 & abs16,x) + 65C02 trb,tsb (abs16) static UINT GetOpcodeCyclesForRead(BYTE reg) { UINT opcodeCycles = 0; BYTE opcode = 0; bool abs16 = false; bool abs16x = false; bool abs16y = false; bool indx = false; bool indy = false; const BYTE opcodeMinus3 = mem[(regs.pc-3)&0xffff]; const BYTE opcodeMinus2 = mem[(regs.pc-2)&0xffff]; if ( ((opcodeMinus2 & 0x0f) == 0x01) && ((opcodeMinus2 & 0x10) == 0x00) ) // ora (zp,x), and (zp,x), ..., sbc (zp,x) { // NB. this is for read, so don't need to exclude 0x81 / sta (zp,x) opcodeCycles = 6; opcode = opcodeMinus2; indx = true; } else if ( ((opcodeMinus2 & 0x0f) == 0x01) && ((opcodeMinus2 & 0x10) == 0x10) ) // ora (zp),y, and (zp),y, ..., sbc (zp),y { // NB. this is for read, so don't need to exclude 0x91 / sta (zp),y opcodeCycles = 5; opcode = opcodeMinus2; indy = true; } else if ( ((opcodeMinus2 & 0x0f) == 0x02) && ((opcodeMinus2 & 0x10) == 0x10) && GetMainCpu() == CPU_65C02 ) // ora (zp), and (zp), ..., sbc (zp) : 65C02-only { // NB. this is for read, so don't need to exclude 0x92 / sta (zp) opcodeCycles = 5; opcode = opcodeMinus2; } else { if ( (((opcodeMinus3 & 0x0f) == 0x0D) && ((opcodeMinus3 & 0x10) == 0x00)) || // ora abs16, and abs16, ..., sbc abs16 (opcodeMinus3 == 0x2C) || // bit abs16 (opcodeMinus3 == 0xAC) || // ldy abs16 (opcodeMinus3 == 0xAE) || // ldx abs16 (opcodeMinus3 == 0xCC) || // cpy abs16 (opcodeMinus3 == 0xEC) ) // cpx abs16 { } else if ( (opcodeMinus3 == 0xBC) || // ldy abs16,x ((opcodeMinus3 == 0x3C) && GetMainCpu() == CPU_65C02) ) // bit abs16,x : 65C02-only { abs16x = true; } else if ( (opcodeMinus3 == 0xBE) ) // ldx abs16,y { abs16y = true; } else if ((opcodeMinus3 & 0x10) == 0x10) { if ((opcodeMinus3 & 0x0f) == 0x0D) // ora abs16,x, and abs16,x, ..., sbc abs16,x abs16x = true; else if ((opcodeMinus3 & 0x0f) == 0x09) // ora abs16,y, and abs16,y, ..., sbc abs16,y abs16y = true; } else { _ASSERT(0); opcodeCycles = 0; return 0; } opcodeCycles = 4; opcode = opcodeMinus3; abs16 = true; } // WORD addr16 = 0; if (!abs16) { BYTE zp = mem[(regs.pc-1)&0xffff]; if (indx) zp += regs.x; addr16 = (mem[zp] | (mem[(zp+1)&0xff]<<8)); if (indy) addr16 += regs.y; } else { addr16 = mem[(regs.pc-2)&0xffff] | (mem[(regs.pc-1)&0xffff]<<8); if (abs16y) addr16 += regs.y; if (abs16x) addr16 += regs.x; } // Check we've reverse looked-up the 6502 opcode correctly if ((addr16 & 0xF80F) != (0xC000+reg)) { _ASSERT(0); return 0; } return opcodeCycles; } // TODO: RMW opcodes: dec,inc,asl,lsr,rol,ror (abs16 & abs16,x) + 65C02 trb,tsb (abs16) static UINT GetOpcodeCyclesForWrite(BYTE reg) { UINT opcodeCycles = 0; BYTE opcode = 0; bool abs16 = false; const BYTE opcodeMinus3 = mem[(regs.pc-3)&0xffff]; const BYTE opcodeMinus2 = mem[(regs.pc-2)&0xffff]; if ( (opcodeMinus3 == 0x8C) || // sty abs16 (opcodeMinus3 == 0x8D) || // sta abs16 (opcodeMinus3 == 0x8E) ) // stx abs16 { // Eg. FT demos: CHIP, MADEF, MAD2 opcodeCycles = 4; opcode = opcodeMinus3; abs16 = true; } else if ( (opcodeMinus3 == 0x99) || // sta abs16,y (opcodeMinus3 == 0x9D) ) // sta abs16,x { // Eg. Paleotronic microTracker demo opcodeCycles = 5; opcode = opcodeMinus3; abs16 = true; } else if (opcodeMinus2 == 0x81) // sta (zp,x) { opcodeCycles = 6; opcode = opcodeMinus2; } else if (opcodeMinus2 == 0x91) // sta (zp),y { // Eg. FT demos: OMT, PLS opcodeCycles = 6; opcode = opcodeMinus2; } else if (opcodeMinus2 == 0x92 && GetMainCpu() == CPU_65C02) // sta (zp) : 65C02-only { opcodeCycles = 5; opcode = opcodeMinus2; } else if (opcodeMinus3 == 0x9C && GetMainCpu() == CPU_65C02) // stz abs16 : 65C02-only { opcodeCycles = 4; opcode = opcodeMinus3; abs16 = true; } else if (opcodeMinus3 == 0x9E && GetMainCpu() == CPU_65C02) // stz abs16,x : 65C02-only { opcodeCycles = 5; opcode = opcodeMinus3; abs16 = true; } else { _ASSERT(0); opcodeCycles = 0; return 0; } // WORD addr16 = 0; if (!abs16) { BYTE zp = mem[(regs.pc-1)&0xffff]; if (opcode == 0x81) zp += regs.x; addr16 = (mem[zp] | (mem[(zp+1)&0xff]<<8)); if (opcode == 0x91) addr16 += regs.y; } else { addr16 = mem[(regs.pc-2)&0xffff] | (mem[(regs.pc-1)&0xffff]<<8); if (opcode == 0x99) addr16 += regs.y; if (opcode == 0x9D || opcode == 0x9E) addr16 += regs.x; } // Check we've reverse looked-up the 6502 opcode correctly if ((addr16 & 0xF80F) != (0xC000+reg)) { _ASSERT(0); return 0; } return opcodeCycles; } // Insert a new synchronous event whenever the 6522 timer's counter is written. // . NB. it doesn't matter if the timer's interrupt enable (IER) is set or not // - the state of IER is only important when the counter underflows - see: MB_SyncEventCallback() static USHORT SetTimerSyncEvent(UINT id, BYTE reg, USHORT timerLatch) { // NB. This TIMER adjustment value gets subtracted when this current opcode completes, so no need to persist to save-state const UINT opcodeCycleAdjust = GetOpcodeCyclesForWrite(reg); SyncEvent* pSyncEvent = g_syncEvent[id]; if (pSyncEvent->m_active) g_SynchronousEventMgr.Remove(id); pSyncEvent->SetCycles(timerLatch + kExtraTimerCycles + opcodeCycleAdjust); g_SynchronousEventMgr.Insert(pSyncEvent); // It doesn't matter if this overflows (ie. >0xFFFF), since on completion of current opcode it'll be corrected return (USHORT) (timerLatch + opcodeCycleAdjust); } static void UpdateIFR(SY6522_AY8910* pMB, BYTE clr_ifr, BYTE set_ifr=0) { // Need critical section to avoid data-race: main thread & SSI263Thread can both access IFR // . NB. Loading a save-state just directly writes into 6522.IFR (which is fine) if (g_bCritSectionValid) EnterCriticalSection(&g_CriticalSection); { pMB->sy6522.IFR &= ~clr_ifr; pMB->sy6522.IFR |= set_ifr; if (pMB->sy6522.IFR & pMB->sy6522.IER & 0x7F) pMB->sy6522.IFR |= 0x80; else pMB->sy6522.IFR &= 0x7F; } if (g_bCritSectionValid) LeaveCriticalSection(&g_CriticalSection); // Now update the IRQ signal from all 6522s // . OR-sum of all active TIMER1, TIMER2 & SPEECH sources (from all 6522s) UINT bIRQ = 0; for (UINT i=0; isy6522.DDRB; pMB->sy6522.ORB = nValue; if( (pMB->sy6522.DDRB == 0xFF) && (pMB->sy6522.PCR == 0xB0) ) { // Votrax speech data Votrax_Write(nDevice, nValue); break; } if(g_bPhasorEnable) { int nAY_CS = (g_phasorMode == PH_Phasor) ? (~(nValue >> 3) & 3) : 1; if(nAY_CS & 1) AY8910_Write(nDevice, nReg, nValue, 0); if(nAY_CS & 2) AY8910_Write(nDevice, nReg, nValue, 1); } else { AY8910_Write(nDevice, nReg, nValue, 0); } break; } case 0x01: // ORA pMB->sy6522.ORA = nValue & pMB->sy6522.DDRA; break; case 0x02: // DDRB pMB->sy6522.DDRB = nValue; break; case 0x03: // DDRA pMB->sy6522.DDRA = nValue; break; case 0x04: // TIMER1L_COUNTER case 0x06: // TIMER1L_LATCH pMB->sy6522.TIMER1_LATCH.l = nValue; break; case 0x05: // TIMER1H_COUNTER { UpdateIFR(pMB, IxR_TIMER1); // Clear Timer1 Interrupt Flag pMB->sy6522.TIMER1_LATCH.h = nValue; const UINT id = nDevice*kNumTimersPer6522+0; // TIMER1 pMB->sy6522.TIMER1_COUNTER.w = SetTimerSyncEvent(id, nReg, pMB->sy6522.TIMER1_LATCH.w); StartTimer1(pMB); } break; case 0x07: // TIMER1H_LATCH UpdateIFR(pMB, IxR_TIMER1); // Clear Timer1 Interrupt Flag pMB->sy6522.TIMER1_LATCH.h = nValue; break; case 0x08: // TIMER2L pMB->sy6522.TIMER2_LATCH.l = nValue; break; case 0x09: // TIMER2H { UpdateIFR(pMB, IxR_TIMER2); // Clear Timer2 Interrupt Flag pMB->sy6522.TIMER2_LATCH.h = nValue; // NB. Real 6522 doesn't have TIMER2_LATCH.h const UINT id = nDevice*kNumTimersPer6522+1; // TIMER2 pMB->sy6522.TIMER2_COUNTER.w = SetTimerSyncEvent(id, nReg, pMB->sy6522.TIMER2_LATCH.w); StartTimer2(pMB); } break; case 0x0a: // SERIAL_SHIFT break; case 0x0b: // ACR pMB->sy6522.ACR = nValue; break; case 0x0c: // PCR - Used for Speech chip only pMB->sy6522.PCR = nValue; break; case 0x0d: // IFR // - Clear those bits which are set in the lower 7 bits. // - Can't clear bit 7 directly. UpdateIFR(pMB, nValue); break; case 0x0e: // IER if(!(nValue & 0x80)) { // Clear those bits which are set in the lower 7 bits. nValue ^= 0x7F; pMB->sy6522.IER &= nValue; } else { // Set those bits which are set in the lower 7 bits. nValue &= 0x7F; pMB->sy6522.IER |= nValue; } UpdateIFR(pMB, 0); break; case 0x0f: // ORA_NO_HS break; } } //----------------------------------------------------------------------------- static bool CheckTimerUnderflow(USHORT& counter, int& timerIrqDelay, const USHORT nClocks); static int OnTimer1Underflow(USHORT& counter, USHORT latch); static USHORT GetTimer1Counter(BYTE reg, USHORT counter, USHORT latch, int timerIrqDelay) { const UINT opcodeCycleAdjust = GetOpcodeCyclesForRead(reg) - 1; // to compensate for the 4/5/6 cycle read opcode if (CheckTimerUnderflow(counter, timerIrqDelay, opcodeCycleAdjust)) OnTimer1Underflow(counter, latch); return counter; } static USHORT GetTimer2Counter(BYTE reg, USHORT counter) { const UINT opcodeCycleAdjust = GetOpcodeCyclesForRead(reg) - 1; // to compensate for the 4/5/6 cycle read opcode return counter - opcodeCycleAdjust; } static BYTE SY6522_Read(BYTE nDevice, BYTE nReg) { g_bMB_Active = true; SY6522_AY8910* pMB = &g_MB[nDevice]; BYTE nValue = 0x00; switch (nReg) { case 0x00: // ORB nValue = pMB->sy6522.ORB; break; case 0x01: // ORA nValue = pMB->sy6522.ORA; break; case 0x02: // DDRB nValue = pMB->sy6522.DDRB; break; case 0x03: // DDRA nValue = pMB->sy6522.DDRA; break; case 0x04: // TIMER1L_COUNTER // NB. GH#701 (T1C:=0xFFFF, LDA T1C_L[4cy], A==0xFC) nValue = GetTimer1Counter(nReg, pMB->sy6522.TIMER1_COUNTER.w, pMB->sy6522.TIMER1_LATCH.w, pMB->sy6522.timer1IrqDelay) & 0xff; UpdateIFR(pMB, IxR_TIMER1); break; case 0x05: // TIMER1H_COUNTER nValue = GetTimer1Counter(nReg, pMB->sy6522.TIMER1_COUNTER.w, pMB->sy6522.TIMER1_LATCH.w, pMB->sy6522.timer1IrqDelay) >> 8; break; case 0x06: // TIMER1L_LATCH nValue = pMB->sy6522.TIMER1_LATCH.l; break; case 0x07: // TIMER1H_LATCH nValue = pMB->sy6522.TIMER1_LATCH.h; break; case 0x08: // TIMER2L nValue = GetTimer2Counter(nReg, pMB->sy6522.TIMER2_COUNTER.w) & 0xff; UpdateIFR(pMB, IxR_TIMER2); break; case 0x09: // TIMER2H nValue = GetTimer2Counter(nReg, pMB->sy6522.TIMER2_COUNTER.w) >> 8; break; case 0x0a: // SERIAL_SHIFT break; case 0x0b: // ACR nValue = pMB->sy6522.ACR; break; case 0x0c: // PCR nValue = pMB->sy6522.PCR; break; case 0x0d: // IFR nValue = pMB->sy6522.IFR; break; case 0x0e: // IER nValue = 0x80 | pMB->sy6522.IER; // GH#567 break; case 0x0f: // ORA_NO_HS nValue = pMB->sy6522.ORA; break; } return nValue; } //--------------------------------------------------------------------------- static void SSI263_Play(unsigned int nPhoneme); #if 0 typedef struct { BYTE DurationPhoneme; BYTE Inflection; // I10..I3 BYTE RateInflection; BYTE CtrlArtAmp; BYTE FilterFreq; // BYTE CurrentMode; } SSI263A; #endif //static SSI263A nSpeechChip; // Duration/Phonome const BYTE DURATION_MODE_MASK = 0xC0; const BYTE PHONEME_MASK = 0x3F; const BYTE MODE_PHONEME_TRANSITIONED_INFLECTION = 0xC0; // IRQ active const BYTE MODE_PHONEME_IMMEDIATE_INFLECTION = 0x80; // IRQ active const BYTE MODE_FRAME_IMMEDIATE_INFLECTION = 0x40; // IRQ active const BYTE MODE_IRQ_DISABLED = 0x00; // Rate/Inflection const BYTE RATE_MASK = 0xF0; const BYTE INFLECTION_MASK_H = 0x08; // I11 const BYTE INFLECTION_MASK_L = 0x07; // I2..I0 // Ctrl/Art/Amp const BYTE CONTROL_MASK = 0x80; const BYTE ARTICULATION_MASK = 0x70; const BYTE AMPLITUDE_MASK = 0x0F; #if LOG_SSI263B static int ssiRegs[5]={-1,-1,-1,-1,-1}; static int totalDuration_ms = 0; void SSI_Output(void) { int ssi0 = ssiRegs[SSI_DURPHON]; int ssi2 = ssiRegs[SSI_RATEINF]; LogOutput("SSI: "); for (int i=0; i<=4; i++) { char r[3]="--"; if (ssiRegs[i]>=0) sprintf(r,"%02X",ssiRegs[i]); LogOutput("%s ", r); ssiRegs[i] = -1; } if (ssi0 != -1 && ssi2 != -1) { int phonemeDuration_ms = (((16-(ssi2>>4))*4096)/1023) * (4-(ssi0>>6)); totalDuration_ms += phonemeDuration_ms; LogOutput("/ duration = %d (total = %d) ms", phonemeDuration_ms, totalDuration_ms); } LogOutput("\n"); } #endif static BYTE SSI263_Read(BYTE nDevice, ULONG nExecutedCycles) { SY6522_AY8910* pMB = &g_MB[nDevice]; // Regardless of register, just return inverted A/!R in bit7 // . A/!R is low for IRQ return MemReadFloatingBus(pMB->SpeechChip.CurrentMode & 1, nExecutedCycles); } static void SSI263_Write(BYTE nDevice, BYTE nReg, BYTE nValue) { SY6522_AY8910* pMB = &g_MB[nDevice]; #if LOG_SSI263B _ASSERT(nReg < 5); if (nReg>4) nReg=4; if (ssiRegs[nReg]>=0) SSI_Output(); // overwriting a reg ssiRegs[nReg] = nValue; #endif switch(nReg) { case SSI_DURPHON: #if LOG_SSI263 if(g_fh) fprintf(g_fh, "DUR = 0x%02X, PHON = 0x%02X\n\n", nValue>>6, nValue&PHONEME_MASK); LogOutput("DUR = %d, PHON = 0x%02X\n", nValue>>6, nValue&PHONEME_MASK); #endif #if LOG_SSI263B SSI_Output(); #endif // Notes: // . Phasor's text-to-speech playback has no CTL H->L // - ISR just writes CTL=0 (and new ART+AMP values), and writes DUR=x (and new PHON) // - since no CTL H->L, then DUR value doesn't take affect (so continue using previous) // - so the write to DURPHON must clear the IRQ // . Does a write of CTL=0 clear IRQ? (ie. CTL 0->0) // . Does a write of CTL=1 clear IRQ? (ie. CTL 0->1) // - SSI263 datasheet says: "Setting the Control bit (CTL) to a logic one puts the device into Power Down mode..." // . Does phoneme output only happen when CTL=0? (Otherwise device is in PD mode) // SSI263 datasheet is not clear, but a write to DURPHON must clear the IRQ. // NB. For Mockingboard, A/!R is ack'ed by 6522's PCR handshake. if (g_bPhasorEnable && g_phasorMode == PH_Phasor) CpuIrqDeassert(IS_SPEECH); pMB->SpeechChip.CurrentMode &= ~1; // Clear SSI263's D7 pin pMB->SpeechChip.DurationPhoneme = nValue; g_nSSI263Device = nDevice; SSI263_Play(nValue & PHONEME_MASK); break; case SSI_INFLECT: #if LOG_SSI263 if(g_fh) fprintf(g_fh, "INF = 0x%02X\n", nValue); #endif pMB->SpeechChip.Inflection = nValue; break; case SSI_RATEINF: #if LOG_SSI263 if(g_fh) fprintf(g_fh, "RATE = 0x%02X, INF = 0x%02X\n", nValue>>4, nValue&0x0F); #endif pMB->SpeechChip.RateInflection = nValue; break; case SSI_CTTRAMP: #if LOG_SSI263 if(g_fh) fprintf(g_fh, "CTRL = %d, ART = 0x%02X, AMP=0x%02X\n", nValue>>7, (nValue&ARTICULATION_MASK)>>4, nValue&LITUDE_MASK); // { bool H2L = (pMB->SpeechChip.CtrlArtAmp & CONTROL_MASK) && !(nValue & CONTROL_MASK); char newMode[20]; sprintf_s(newMode, sizeof(newMode), "(new mode=%d)", pMB->SpeechChip.DurationPhoneme>>6); LogOutput("CTRL = %d->%d, ART = 0x%02X, AMP=0x%02X %s\n", pMB->SpeechChip.CtrlArtAmp>>7, nValue>>7, (nValue&ARTICULATION_MASK)>>4, nValue&LITUDE_MASK, H2L?newMode:""); } #endif #if LOG_SSI263B if ( ((pMB->SpeechChip.CtrlArtAmp & CONTROL_MASK) && !(nValue & CONTROL_MASK)) || ((nValue&0xF) == 0x0) ) // H->L or amp=0 SSI_Output(); #endif if((pMB->SpeechChip.CtrlArtAmp & CONTROL_MASK) && !(nValue & CONTROL_MASK)) // H->L { pMB->SpeechChip.CurrentMode = pMB->SpeechChip.DurationPhoneme & DURATION_MODE_MASK; if (pMB->SpeechChip.CurrentMode == MODE_IRQ_DISABLED) { // "Disables A/!R output only; does not change previous A/!R response" (SSI263 datasheet) // CpuIrqDeassert(IS_SPEECH); } } pMB->SpeechChip.CtrlArtAmp = nValue; // "Setting the Control bit (CTL) to a logic one puts the device into Power Down mode..." (SSI263 datasheet) if (pMB->SpeechChip.CtrlArtAmp & CONTROL_MASK) { // CpuIrqDeassert(IS_SPEECH); // pMB->SpeechChip.CurrentMode &= ~1; // Clear SSI263's D7 pin } break; case SSI_FILFREQ: // RegAddr.b2=1 (b1 & b0 are: don't care) default: #if LOG_SSI263 if(g_fh) fprintf(g_fh, "FFREQ = 0x%02X\n", nValue); #endif pMB->SpeechChip.FilterFreq = nValue; break; } } //------------------------------------- static BYTE Votrax2SSI263[64] = { 0x02, // 00: EH3 jackEt -> E1 bEnt 0x0A, // 01: EH2 Enlist -> EH nEst 0x0B, // 02: EH1 hEAvy -> EH1 bElt 0x00, // 03: PA0 no sound -> PA 0x28, // 04: DT buTTer -> T Tart 0x08, // 05: A2 mAde -> A mAde 0x08, // 06: A1 mAde -> A mAde 0x2F, // 07: ZH aZure -> Z Zero 0x0E, // 08: AH2 hOnest -> AH gOt 0x07, // 09: I3 inhibIt -> I sIx 0x07, // 0A: I2 Inhibit -> I sIx 0x07, // 0B: I1 inhIbit -> I sIx 0x37, // 0C: M Mat -> More 0x38, // 0D: N suN -> N NiNe 0x24, // 0E: B Bag -> B Bag 0x33, // 0F: V Van -> V Very // 0x32, // 10: CH* CHip -> SCH SHip (!) 0x32, // 11: SH SHop -> SCH SHip 0x2F, // 12: Z Zoo -> Z Zero 0x10, // 13: AW1 lAWful -> AW Office 0x39, // 14: NG thiNG -> NG raNG 0x0F, // 15: AH1 fAther -> AH1 fAther 0x13, // 16: OO1 lOOking -> OO lOOk 0x13, // 17: OO bOOK -> OO lOOk 0x20, // 18: L Land -> L Lift 0x29, // 19: K triCK -> Kit 0x25, // 1A: J* juDGe -> D paiD (!) 0x2C, // 1B: H Hello -> HF Heart 0x26, // 1C: G Get -> KV taG 0x34, // 1D: F Fast -> F Four 0x25, // 1E: D paiD -> D paiD 0x30, // 1F: S paSS -> S Same // 0x08, // 20: A dAY -> A mAde 0x09, // 21: AY dAY -> AI cAre 0x03, // 22: Y1 Yard -> YI Year 0x1B, // 23: UH3 missIOn -> UH3 nUt 0x0E, // 24: AH mOp -> AH gOt 0x27, // 25: P Past -> P Pen 0x11, // 26: O cOld -> O stOre 0x07, // 27: I pIn -> I sIx 0x16, // 28: U mOve -> U tUne 0x05, // 29: Y anY -> AY plEAse 0x28, // 2A: T Tap -> T Tart 0x1D, // 2B: R Red -> R Roof 0x01, // 2C: E mEEt -> E mEEt 0x23, // 2D: W Win -> W Water 0x0C, // 2E: AE dAd -> AE dAd 0x0D, // 2F: AE1 After -> AE1 After // 0x10, // 30: AW2 sAlty -> AW Office 0x1A, // 31: UH2 About -> UH2 whAt 0x19, // 32: UH1 Uncle -> UH1 lOve 0x18, // 33: UH cUp -> UH wOnder 0x11, // 34: O2 fOr -> O stOre 0x11, // 35: O1 abOArd -> O stOre 0x14, // 36: IU yOU -> IU yOU 0x14, // 37: U1 yOU -> IU yOU 0x35, // 38: THV THe -> THV THere 0x36, // 39: TH THin -> TH wiTH 0x1C, // 3A: ER bIrd -> ER bIrd 0x0A, // 3B: EH gEt -> EH nEst 0x01, // 3C: E1 bE -> E mEEt 0x10, // 3D: AW cAll -> AW Office 0x00, // 3E: PA1 no sound -> PA 0x00, // 3F: STOP no sound -> PA }; static void Votrax_Write(BYTE nDevice, BYTE nValue) { g_bVotraxPhoneme = true; // !A/R: Acknowledge receipt of phoneme data (signal goes from high to low) SY6522_AY8910* pMB = &g_MB[nDevice]; UpdateIFR(pMB, IxR_VOTRAX); g_nSSI263Device = nDevice; SSI263_Play(Votrax2SSI263[nValue & PHONEME_MASK]); } //=========================================================================== //#define DBG_MB_UPDATE static UINT64 g_uLastMBUpdateCycle = 0; // Called by: // . MB_UpdateCycles() - when g_nMBTimerDevice == {0,1,2,3} // . MB_PeriodicUpdate() - when g_nMBTimerDevice == kTIMERDEVICE_INVALID static void MB_UpdateInt(void) { if (!MockingboardVoice.bActive) return; if (g_bFullSpeed) { // Keep AY reg writes relative to the current 'frame' // - Required for Ultima3: // . Tune ends // . g_bFullSpeed:=true (disk-spinning) for ~50 frames // . U3 sets AY_ENABLE:=0xFF (as a side-effect, this sets g_bFullSpeed:=false) // o Without this, the write to AY_ENABLE gets ignored (since AY8910's /g_uLastCumulativeCycles/ was last set 50 frame ago) AY8910UpdateSetCycles(); // TODO: // If any AY regs have changed then push them out to the AY chip return; } // if (!g_bMB_RegAccessedFlag) { if(!g_nMB_InActiveCycleCount) { g_nMB_InActiveCycleCount = g_nCumulativeCycles; } else if(g_nCumulativeCycles - g_nMB_InActiveCycleCount > (unsigned __int64)g_fCurrentCLK6502/10) { // After 0.1 sec of Apple time, assume MB is not active g_bMB_Active = false; } } else { g_nMB_InActiveCycleCount = 0; g_bMB_RegAccessedFlag = false; g_bMB_Active = true; } // // For small timer periods, wait for a period of 500cy before updating DirectSound ring-buffer. // NB. A timer period of less than 24cy will yield nNumSamplesPerPeriod=0. const double kMinimumUpdateInterval = 500.0; // Arbitary (500 cycles = 21 samples) const double kMaximumUpdateInterval = (double)(0xFFFF+2); // Max 6522 timer interval (2756 samples) if (g_uLastMBUpdateCycle == 0) g_uLastMBUpdateCycle = g_uLastCumulativeCycles; // Initial call to MB_Update() after reset/power-cycle _ASSERT(g_uLastCumulativeCycles >= g_uLastMBUpdateCycle); double updateInterval = (double)(g_uLastCumulativeCycles - g_uLastMBUpdateCycle); if (updateInterval < kMinimumUpdateInterval) return; if (updateInterval > kMaximumUpdateInterval) updateInterval = kMaximumUpdateInterval; g_uLastMBUpdateCycle = g_uLastCumulativeCycles; const double nIrqFreq = g_fCurrentCLK6502 / updateInterval + 0.5; // Round-up const int nNumSamplesPerPeriod = (int) ((double)SAMPLE_RATE / nIrqFreq); // Eg. For 60Hz this is 735 static int nNumSamplesError = 0; int nNumSamples = nNumSamplesPerPeriod + nNumSamplesError; // Apply correction if(nNumSamples <= 0) nNumSamples = 0; if(nNumSamples > 2*nNumSamplesPerPeriod) nNumSamples = 2*nNumSamplesPerPeriod; if (nNumSamples > SAMPLE_RATE) nNumSamples = SAMPLE_RATE; // Clamp to prevent buffer overflow (bufferSize = SAMPLE_RATE) if(nNumSamples) for(int nChip=0; nChipGetCurrentPosition(&dwCurrentPlayCursor, &dwCurrentWriteCursor); if(FAILED(hr)) return; static DWORD dwByteOffset = (DWORD)-1; if(dwByteOffset == (DWORD)-1) { // First time in this func dwByteOffset = dwCurrentWriteCursor; } else { // Check that our offset isn't between Play & Write positions if(dwCurrentWriteCursor > dwCurrentPlayCursor) { // |-----PxxxxxW-----| if((dwByteOffset > dwCurrentPlayCursor) && (dwByteOffset < dwCurrentWriteCursor)) { #ifdef DBG_MB_UPDATE double fTicksSecs = (double)GetTickCount() / 1000.0; LogOutput("%010.3f: [MBUpdt] PC=%08X, WC=%08X, Diff=%08X, Off=%08X, NS=%08X xxx\n", fTicksSecs, dwCurrentPlayCursor, dwCurrentWriteCursor, dwCurrentWriteCursor-dwCurrentPlayCursor, dwByteOffset, nNumSamples); #endif dwByteOffset = dwCurrentWriteCursor; nNumSamplesError = 0; } } else { // |xxW----------Pxxx| if((dwByteOffset > dwCurrentPlayCursor) || (dwByteOffset < dwCurrentWriteCursor)) { #ifdef DBG_MB_UPDATE double fTicksSecs = (double)GetTickCount() / 1000.0; LogOutput("%010.3f: [MBUpdt] PC=%08X, WC=%08X, Diff=%08X, Off=%08X, NS=%08X XXX\n", fTicksSecs, dwCurrentPlayCursor, dwCurrentWriteCursor, dwCurrentWriteCursor-dwCurrentPlayCursor, dwByteOffset, nNumSamples); #endif dwByteOffset = dwCurrentWriteCursor; nNumSamplesError = 0; } } } int nBytesRemaining = dwByteOffset - dwCurrentPlayCursor; if(nBytesRemaining < 0) nBytesRemaining += g_dwDSBufferSize; // Calc correction factor so that play-buffer doesn't under/overflow const int nErrorInc = SoundCore_GetErrorInc(); if(nBytesRemaining < g_dwDSBufferSize / 4) nNumSamplesError += nErrorInc; // < 0.25 of buffer remaining else if(nBytesRemaining > g_dwDSBufferSize / 2) nNumSamplesError -= nErrorInc; // > 0.50 of buffer remaining else nNumSamplesError = 0; // Acceptable amount of data in buffer #ifdef DBG_MB_UPDATE double fTicksSecs = (double)GetTickCount() / 1000.0; LogOutput("%010.3f: [MBUpdt] PC=%08X, WC=%08X, Diff=%08X, Off=%08X, NS=%08X, NSE=%08X, Interval=%f\n", fTicksSecs, dwCurrentPlayCursor, dwCurrentWriteCursor, dwCurrentWriteCursor - dwCurrentPlayCursor, dwByteOffset, nNumSamples, nNumSamplesError, updateInterval); #endif if(nNumSamples == 0) return; // const double fAttenuation = g_bPhasorEnable ? 2.0/3.0 : 1.0; for(int i=0; i nWaveDataMax) nDataL = nWaveDataMax; if(nDataR < nWaveDataMin) nDataR = nWaveDataMin; else if(nDataR > nWaveDataMax) nDataR = nWaveDataMax; g_nMixBuffer[i*g_nMB_NumChannels+0] = (short)nDataL; // L g_nMixBuffer[i*g_nMB_NumChannels+1] = (short)nDataR; // R } // DWORD dwDSLockedBufferSize0, dwDSLockedBufferSize1; SHORT *pDSLockedBuffer0, *pDSLockedBuffer1; if(!DSGetLock(MockingboardVoice.lpDSBvoice, dwByteOffset, (DWORD)nNumSamples*sizeof(short)*g_nMB_NumChannels, &pDSLockedBuffer0, &dwDSLockedBufferSize0, &pDSLockedBuffer1, &dwDSLockedBufferSize1)) return; memcpy(pDSLockedBuffer0, &g_nMixBuffer[0], dwDSLockedBufferSize0); if(pDSLockedBuffer1) memcpy(pDSLockedBuffer1, &g_nMixBuffer[dwDSLockedBufferSize0/sizeof(short)], dwDSLockedBufferSize1); // Commit sound buffer hr = MockingboardVoice.lpDSBvoice->Unlock((void*)pDSLockedBuffer0, dwDSLockedBufferSize0, (void*)pDSLockedBuffer1, dwDSLockedBufferSize1); dwByteOffset = (dwByteOffset + (DWORD)nNumSamples*sizeof(short)*g_nMB_NumChannels) % g_dwDSBufferSize; #ifdef RIFF_MB RiffPutSamples(&g_nMixBuffer[0], nNumSamples); #endif } static void MB_Update(void) { #ifdef LOG_PERF_TIMINGS extern UINT64 g_timeMB_NoTimer; extern UINT64 g_timeMB_Timer; PerfMarker perfMarker(g_nMBTimerDevice == kTIMERDEVICE_INVALID ? g_timeMB_NoTimer : g_timeMB_Timer); #endif MB_UpdateInt(); } //----------------------------------------------------------------------------- // Called by SSI263Thread(), MB_LoadSnapshot & Phasor_LoadSnapshot // Pre: g_bVotraxPhoneme, g_bPhasorEnable, g_phasorMode static void SetSpeechIRQ(SY6522_AY8910* pMB) { if (!g_bVotraxPhoneme) { // Always set SSI263's D7 pin regardless of SSI263 mode (DR1:0), including MODE_IRQ_DISABLED pMB->SpeechChip.CurrentMode |= 1; // Set SSI263's D7 pin if ((pMB->SpeechChip.CurrentMode & DURATION_MODE_MASK) != MODE_IRQ_DISABLED) { if (!g_bPhasorEnable || (g_bPhasorEnable && g_phasorMode == PH_Mockingboard)) { if ((pMB->sy6522.PCR & 1) == 0) // CA1 Latch/Input = 0 (Negative active edge) UpdateIFR(pMB, 0, IxR_PERIPHERAL); if (pMB->sy6522.PCR == 0x0C) // CA2 Control = b#110 (Low output) pMB->SpeechChip.CurrentMode &= ~1; // Clear SSI263's D7 pin (cleared by 6522's PCR CA1/CA2 handshake) // NB. Don't set CTL=1, as Mockingboard(SMS) speech doesn't work (it sets MODE_IRQ_DISABLED mode during ISR) //pMB->SpeechChip.CtrlArtAmp |= CONTROL_MASK; // 6522's CA2 sets Power Down mode (pin 18), which sets Control bit } else if (g_bPhasorEnable && g_phasorMode == PH_Phasor) // Phasor's SSI263 IRQ (A/!R) line is *also* wired directly to the 6502's IRQ (as well as the 6522's CA1) { CpuIrqAssert(IS_SPEECH); } } } // if (g_bVotraxPhoneme && pMB->sy6522.PCR == 0xB0) { // !A/R: Time-out of old phoneme (signal goes from low to high) UpdateIFR(pMB, 0, IxR_VOTRAX); g_bVotraxPhoneme = false; } } static DWORD WINAPI SSI263Thread(LPVOID lpParameter) { while(1) { DWORD dwWaitResult = WaitForMultipleObjects( g_nNumEvents, // number of handles in array g_hSSI263Event, // array of event handles FALSE, // wait until any one is signaled INFINITE); if((dwWaitResult < WAIT_OBJECT_0) || (dwWaitResult > WAIT_OBJECT_0+g_nNumEvents-1)) continue; dwWaitResult -= WAIT_OBJECT_0; // Determine event # that signaled if(dwWaitResult == (g_nNumEvents-1)) // Termination event break; // Phoneme completed playing if (g_bStopPhoneme) { g_bStopPhoneme = false; continue; } #if LOG_SSI263 //if(g_fh) fprintf(g_fh, "IRQ: Phoneme complete (0x%02X)\n\n", g_nCurrentActivePhoneme); #endif if (g_nCurrentActivePhoneme < 0) continue; // On CTRL+RESET or power-cycle (during phoneme playback): ResetState() is called, which set g_nCurrentActivePhoneme=-1 SSI263Voice[g_nCurrentActivePhoneme].bActive = false; g_nCurrentActivePhoneme = -1; // Phoneme complete, so generate IRQ if necessary SY6522_AY8910* pMB = &g_MB[g_nSSI263Device]; SetSpeechIRQ(pMB); } return 0; } //----------------------------------------------------------------------------- static void SSI263_Play(unsigned int nPhoneme) { #if 1 HRESULT hr; { int nCurrPhoneme = g_nCurrentActivePhoneme; // local copy in case SSI263Thread sets it to -1 if (nCurrPhoneme >= 0) { // A write to DURPHON before previous phoneme has completed g_bStopPhoneme = true; hr = SSI263Voice[nCurrPhoneme].lpDSBvoice->Stop(); // Busy-wait until ACK from SSI263Thread // . required to avoid data-race while ( g_bStopPhoneme && // wait for SSI263Thread to ACK the lpDSBVoice->Stop() g_nCurrentActivePhoneme >= 0) // wait for SSI263Thread to get end of sample event ; g_bStopPhoneme = false; } } g_nCurrentActivePhoneme = nPhoneme; hr = SSI263Voice[g_nCurrentActivePhoneme].lpDSBvoice->SetCurrentPosition(0); if(FAILED(hr)) return; hr = SSI263Voice[g_nCurrentActivePhoneme].lpDSBvoice->Play(0,0,0); // Not looping if(FAILED(hr)) return; SSI263Voice[g_nCurrentActivePhoneme].bActive = true; #else HRESULT hr; bool bPause; if(nPhoneme == 1) nPhoneme = 2; // Missing this sample, so map to phoneme-2 if(nPhoneme == 0) { bPause = true; } else { // nPhoneme--; nPhoneme-=2; // Missing phoneme-1 bPause = false; } DWORD dwDSLockedBufferSize = 0; // Size of the locked DirectSound buffer SHORT* pDSLockedBuffer; hr = SSI263Voice.lpDSBvoice->Stop(); if(!DSGetLock(SSI263Voice.lpDSBvoice, 0, 0, &pDSLockedBuffer, &dwDSLockedBufferSize, NULL, 0)) return; unsigned int nPhonemeShortLength = g_nPhonemeInfo[nPhoneme].nLength; unsigned int nPhonemeByteLength = g_nPhonemeInfo[nPhoneme].nLength * sizeof(SHORT); if(bPause) { // 'pause' length is length of 1st phoneme (arbitrary choice, since don't know real length) memset(pDSLockedBuffer, 0, g_dwMaxPhonemeLen); } else { memcpy(pDSLockedBuffer, &g_nPhonemeData[g_nPhonemeInfo[nPhoneme].nOffset], nPhonemeByteLength); memset(&pDSLockedBuffer[nPhonemeShortLength], 0, g_dwMaxPhonemeLen-nPhonemeByteLength); } #if 0 DSBPOSITIONNOTIFY PositionNotify; PositionNotify.dwOffset = nPhonemeByteLength - 1; // End of phoneme PositionNotify.hEventNotify = g_hSSI263Event[0]; hr = SSI263Voice.lpDSNotify->SetNotificationPositions(1, &PositionNotify); if(FAILED(hr)) { DirectSound_ErrorText(hr); return; } #endif hr = SSI263Voice.lpDSBvoice->Unlock((void*)pDSLockedBuffer, dwDSLockedBufferSize, NULL, 0); if(FAILED(hr)) return; hr = SSI263Voice.lpDSBvoice->Play(0,0,0); // Not looping if(FAILED(hr)) return; SSI263Voice.bActive = true; #endif } //----------------------------------------------------------------------------- static bool MB_DSInit() { LogFileOutput("MB_DSInit\n", g_bMBAvailable); #ifdef NO_DIRECT_X return false; #else // NO_DIRECT_X // // Create single Mockingboard voice // DWORD dwDSLockedBufferSize = 0; // Size of the locked DirectSound buffer SHORT* pDSLockedBuffer; if(!g_bDSAvailable) return false; HRESULT hr = DSGetSoundBuffer(&MockingboardVoice, DSBCAPS_CTRLVOLUME, g_dwDSBufferSize, SAMPLE_RATE, 2); LogFileOutput("MB_DSInit: DSGetSoundBuffer(), hr=0x%08X\n", hr); if(FAILED(hr)) { if(g_fh) fprintf(g_fh, "MB: DSGetSoundBuffer failed (%08X)\n",hr); return false; } bool bRes = DSZeroVoiceBuffer(&MockingboardVoice, "MB", g_dwDSBufferSize); LogFileOutput("MB_DSInit: DSZeroVoiceBuffer(), res=%d\n", bRes ? 1 : 0); if (!bRes) return false; MockingboardVoice.bActive = true; // Volume might've been setup from value in Registry if(!MockingboardVoice.nVolume) MockingboardVoice.nVolume = DSBVOLUME_MAX; hr = MockingboardVoice.lpDSBvoice->SetVolume(MockingboardVoice.nVolume); LogFileOutput("MB_DSInit: SetVolume(), hr=0x%08X\n", hr); //--------------------------------- // // Create SSI263 voice // #if 0 g_dwMaxPhonemeLen = 0; for(int i=0; iQueryInterface(IID_IDirectSoundNotify, (LPVOID *)&SSI263Voice[i].lpDSNotify); //LogFileOutput("MB_DSInit: (%02d) QueryInterface(), hr=0x%08X\n", i, hr); // WARNING: Lock acquired && doing heavy-weight logging if(FAILED(hr)) { if(g_fh) fprintf(g_fh, "SSI263: QueryInterface failed (%08X)\n",hr); return false; } DSBPOSITIONNOTIFY PositionNotify; // PositionNotify.dwOffset = nPhonemeByteLength - 1; // End of buffer PositionNotify.dwOffset = DSBPN_OFFSETSTOP; // End of buffer PositionNotify.hEventNotify = g_hSSI263Event[0]; hr = SSI263Voice[i].lpDSNotify->SetNotificationPositions(1, &PositionNotify); //LogFileOutput("MB_DSInit: (%02d) SetNotificationPositions(), hr=0x%08X\n", i, hr); // WARNING: Lock acquired && doing heavy-weight logging if(FAILED(hr)) { if(g_fh) fprintf(g_fh, "SSI263: SetNotifyPos failed (%08X)\n",hr); return false; } hr = SSI263Voice[i].lpDSBvoice->Unlock((void*)pDSLockedBuffer, dwDSLockedBufferSize, NULL, 0); LogFileOutput("MB_DSInit: (%02d) Unlock(),hr=0x%08X\n", i, hr); if(FAILED(hr)) { if(g_fh) fprintf(g_fh, "SSI263: DSUnlock failed (%08X)\n",hr); return false; } SSI263Voice[i].bActive = false; SSI263Voice[i].nVolume = MockingboardVoice.nVolume; // Use same volume as MB hr = SSI263Voice[i].lpDSBvoice->SetVolume(SSI263Voice[i].nVolume); LogFileOutput("MB_DSInit: (%02d) SetVolume(), hr=0x%08X\n", i, hr); } // DWORD dwThreadId; g_hThread = CreateThread(NULL, // lpThreadAttributes 0, // dwStackSize SSI263Thread, NULL, // lpParameter 0, // dwCreationFlags : 0 = Run immediately &dwThreadId); // lpThreadId LogFileOutput("MB_DSInit: CreateThread(), g_hThread=0x%08X\n", g_hThread); BOOL bRes2 = SetThreadPriority(g_hThread, THREAD_PRIORITY_TIME_CRITICAL); LogFileOutput("MB_DSInit: SetThreadPriority(), bRes=%d\n", bRes2 ? 1 : 0); return true; #endif // NO_DIRECT_X } static void MB_DSUninit() { if(g_hThread) { DWORD dwExitCode; SetEvent(g_hSSI263Event[g_nNumEvents-1]); // Signal to thread that it should exit do { if(GetExitCodeThread(g_hThread, &dwExitCode)) { if(dwExitCode == STILL_ACTIVE) Sleep(10); else break; } } while(1); CloseHandle(g_hThread); g_hThread = NULL; } // if(MockingboardVoice.lpDSBvoice && MockingboardVoice.bActive) { MockingboardVoice.lpDSBvoice->Stop(); MockingboardVoice.bActive = false; } DSReleaseSoundBuffer(&MockingboardVoice); // for(int i=0; i<64; i++) { if(SSI263Voice[i].lpDSBvoice && SSI263Voice[i].bActive) { SSI263Voice[i].lpDSBvoice->Stop(); SSI263Voice[i].bActive = false; } DSReleaseSoundBuffer(&SSI263Voice[i]); } // if(g_hSSI263Event[0]) { CloseHandle(g_hSSI263Event[0]); g_hSSI263Event[0] = NULL; } if(g_hSSI263Event[1]) { CloseHandle(g_hSSI263Event[1]); g_hSSI263Event[1] = NULL; } } //============================================================================= // // ----- ALL GLOBALLY ACCESSIBLE FUNCTIONS ARE BELOW THIS LINE ----- // //============================================================================= static void InitSoundcardType(void) { g_SoundcardType = CT_Empty; // Use CT_Empty to mean: no soundcard g_bPhasorEnable = false; } void MB_Initialize() { InitSoundcardType(); LogFileOutput("MB_Initialize: g_bDisableDirectSound=%d, g_bDisableDirectSoundMockingboard=%d\n", g_bDisableDirectSound, g_bDisableDirectSoundMockingboard); if (g_bDisableDirectSound || g_bDisableDirectSoundMockingboard) { MockingboardVoice.bMute = true; } else { memset(&g_MB,0,sizeof(g_MB)); int i; for(i=0; iStop(); // Reason: 'MB voice is playing' then loading a save-state where 'no MB present' } //----------------------------------------------------------------------------- // NB. Called when /g_fCurrentCLK6502/ changes void MB_Reinitialize() { AY8910_InitClock((int)g_fCurrentCLK6502); // todo: account for g_PhasorClockScaleFactor? // NB. Other calls to AY8910_InitClock() use the constant CLK_6502 } //----------------------------------------------------------------------------- void MB_Destroy() { MB_DSUninit(); for (int i=0; im_active) g_SynchronousEventMgr.Remove(id); delete g_syncEvent[id]; g_syncEvent[id] = NULL; } } //----------------------------------------------------------------------------- static void ResetState() { g_nMBTimerDevice = kTIMERDEVICE_INVALID; MB_SetCumulativeCycles(); g_nSSI263Device = 0; g_nCurrentActivePhoneme = -1; g_bStopPhoneme = false; g_bVotraxPhoneme = false; g_nMB_InActiveCycleCount = 0; g_bMB_RegAccessedFlag = false; g_bMB_Active = false; g_phasorMode = PH_Mockingboard; g_PhasorClockScaleFactor = 1; g_uLastMBUpdateCycle = 0; g_cyclesThisAudioFrame = 0; for (int id = 0; id < kNumSyncEvents; id++) { if (g_syncEvent[id] && g_syncEvent[id]->m_active) g_SynchronousEventMgr.Remove(id); } // Not these, as they don't change on a CTRL+RESET or power-cycle: // g_bMBAvailable = false; // g_SoundcardType = CT_Empty; // Don't uncomment, else _ASSERT will fire in MB_Read() after an F2->MB_Reset() // g_bPhasorEnable = false; } void MB_Reset(const bool powerCycle) // CTRL+RESET or power-cycle { if (!g_bDSAvailable) return; for (int i=0; i>8)&0xf - SLOT4; BYTE nOffset = nAddr&0xff; if(g_bPhasorEnable) { if(nMB != 0) // Slot4 only return MemReadFloatingBus(nExecutedCycles); int CS = 0; if (g_phasorMode == PH_Mockingboard) CS = ( ( nAddr & 0x80 ) >> 7 ) + 1; // 1 or 2 else if (g_phasorMode == PH_Phasor) CS = ( ( nAddr & 0x80 ) >> 6 ) | ( ( nAddr & 0x10 ) >> 4 ); // 0, 1, 2 or 3 else if (g_phasorMode == PH_EchoPlus) CS = 2; BYTE nRes = 0; if(CS & 1) nRes |= SY6522_Read(nMB*NUM_DEVS_PER_MB + SY6522_DEVICE_A, nAddr&0xf); if(CS & 2) nRes |= SY6522_Read(nMB*NUM_DEVS_PER_MB + SY6522_DEVICE_B, nAddr&0xf); bool bAccessedDevice = (CS & 3) ? true : false; bool CS_SSI263 = !(nAddr & 0x80) && (nAddr & 0x60); // SSI263 at $Cn2x and/or $Cn4x if (g_phasorMode == PH_Phasor && CS_SSI263) // NB. Mockingboard mode: SSI263.bit7 not readable { _ASSERT(!bAccessedDevice); if (nAddr & 0x40) // Primary SSI263 nRes = SSI263_Read(nMB*2+1, nExecutedCycles); // SSI263 only drives bit7 if (nAddr & 0x20) // Secondary SSI263 nRes = SSI263_Read(nMB*2+0, nExecutedCycles); // SSI263 only drives bit7 bAccessedDevice = true; } return bAccessedDevice ? nRes : MemReadFloatingBus(nExecutedCycles); } // NB. Mockingboard: SSI263.bit7 not readable (TODO: check this with real h/w) if (nOffset < SY6522B_Offset) return SY6522_Read(nMB*NUM_DEVS_PER_MB + SY6522_DEVICE_A, nAddr&0xf); else return SY6522_Read(nMB*NUM_DEVS_PER_MB + SY6522_DEVICE_B, nAddr&0xf); } //----------------------------------------------------------------------------- static BYTE __stdcall MB_Write(WORD PC, WORD nAddr, BYTE bWrite, BYTE nValue, ULONG nExecutedCycles) { MB_UpdateCycles(nExecutedCycles); #ifdef _DEBUG if(!IS_APPLE2 && MemCheckINTCXROM()) { _ASSERT(0); // Card ROM disabled, so IO_Cxxx() returns the internal ROM return 0; } if(g_SoundcardType == CT_Empty) { _ASSERT(0); // Card unplugged, so IO_Cxxx() returns the floating bus return 0; } #endif // Support 6502/65C02 false-reads of 6522 (GH#52) if ( ((mem[(PC-2)&0xffff] == 0x91) && GetMainCpu() == CPU_6502) || // sta (zp),y - 6502 only (no-PX variant only) (UTAIIe:4-23) (mem[(PC-3)&0xffff] == 0x99) || // sta abs16,y - 6502/65C02, but for 65C02 only the no-PX variant that does the false-read (UTAIIe:4-27) (mem[(PC-3)&0xffff] == 0x9D) ) // sta abs16,x - 6502/65C02, but for 65C02 only the no-PX variant that does the false-read (UTAIIe:4-27) { WORD base; WORD addr16; if (mem[(PC-2)&0xffff] == 0x91) { BYTE zp = mem[(PC-1)&0xffff]; base = (mem[zp] | (mem[(zp+1)&0xff]<<8)); addr16 = base + regs.y; } else { base = mem[(PC-2)&0xffff] | (mem[(PC-1)&0xffff]<<8); addr16 = base + ((mem[(PC-3)&0xffff] == 0x99) ? regs.y : regs.x); } if (((base ^ addr16) >> 8) == 0) // Only the no-PX variant does the false read (to the same I/O SELECT page) { _ASSERT(addr16 == nAddr); if (addr16 == nAddr) // Check we've reverse looked-up the 6502 opcode correctly { if ( ((nAddr&0xf) == 4) || ((nAddr&0xf) == 8) ) // Only reading 6522 reg-4 or reg-8 actually has an effect MB_Read(PC, nAddr, 0, 0, nExecutedCycles); } } } BYTE nMB = ((nAddr>>8)&0xf) - SLOT4; BYTE nOffset = nAddr&0xff; if(g_bPhasorEnable) { if(nMB != 0) // Slot4 only return 0; int CS = 0; if (g_phasorMode == PH_Mockingboard) CS = ( ( nAddr & 0x80 ) >> 7 ) + 1; // 1 or 2 else if (g_phasorMode == PH_Phasor) CS = ( ( nAddr & 0x80 ) >> 6 ) | ( ( nAddr & 0x10 ) >> 4 ); // 0, 1, 2 or 3 else if (g_phasorMode == PH_EchoPlus) CS = 2; if(CS & 1) SY6522_Write(nMB*NUM_DEVS_PER_MB + SY6522_DEVICE_A, nAddr&0xf, nValue); if(CS & 2) SY6522_Write(nMB*NUM_DEVS_PER_MB + SY6522_DEVICE_B, nAddr&0xf, nValue); bool CS_SSI263 = !(nAddr & 0x80) && (nAddr & 0x60); // SSI263 at $Cn2x and/or $Cn4x if ((g_phasorMode == PH_Mockingboard || g_phasorMode == PH_Phasor) && CS_SSI263) // No SSI263 for Echo+ { // NB. Mockingboard mode: writes to $Cn4x/SSI263 also get written to 1st 6522 (have confirmed on real Phasor h/w) _ASSERT( (g_phasorMode == PH_Mockingboard && (CS==0 || CS==1)) || (g_phasorMode == PH_Phasor && (CS==0)) ); if (nAddr & 0x40) // Primary SSI263 SSI263_Write(nMB*2+1, nAddr&0x7, nValue); // 2nd 6522 is used for 1st speech chip if (nAddr & 0x20) // Secondary SSI263 SSI263_Write(nMB*2+0, nAddr&0x7, nValue); // 1st 6522 is used for 2nd speech chip } return 0; } if (nOffset < SY6522B_Offset) SY6522_Write(nMB*NUM_DEVS_PER_MB + SY6522_DEVICE_A, nAddr&0xf, nValue); else SY6522_Write(nMB*NUM_DEVS_PER_MB + SY6522_DEVICE_B, nAddr&0xf, nValue); if (nAddr & 0x40) SSI263_Write(nMB*2+1, nAddr&0x7, nValue); // 2nd 6522 is used for 1st speech chip if (nAddr & 0x20) SSI263_Write(nMB*2+0, nAddr&0x7, nValue); // 1st 6522 is used for 2nd speech chip return 0; } //----------------------------------------------------------------------------- // Phasor's DEVICE SELECT' logic: // . if addr.[b3]==1, then clear the card's mode bits b2:b0 // . if any of addr.[b2:b0] are a logic 1, then set these bits in the card's mode // // Example DEVICE SELECT' accesses for Phasor in slot-4: (from empirical observations on real Phasor h/w) // 1) // . RESET -> Mockingboard mode (b#000) // . $C0C5 -> Phasor mode (b#101) // 2) // . RESET -> Mockingboard mode (b#000) // . $C0C1, then $C0C4 (or $C0C4, then $C0C1) -> Phasor mode (b#101) // . $C0C2 -> Echo+ mode (b#111) // . $C0C5 -> remaining in Echo+ mode (b#111) // So $C0C5 seemingly results in 2 different modes. // static BYTE __stdcall PhasorIO(WORD PC, WORD nAddr, BYTE bWrite, BYTE nValue, ULONG nExecutedCycles) { if (!g_bPhasorEnable) return MemReadFloatingBus(nExecutedCycles); UINT bits = (UINT) g_phasorMode; if (nAddr & 8) bits = 0; bits |= (nAddr & 7); g_phasorMode = (PHASOR_MODE) bits; if (g_phasorMode == PH_Mockingboard || g_phasorMode == PH_EchoPlus) g_PhasorClockScaleFactor = 1; else if (g_phasorMode == PH_Phasor) g_PhasorClockScaleFactor = 2; AY8910_InitClock((int)(Get6502BaseClock() * g_PhasorClockScaleFactor)); return MemReadFloatingBus(nExecutedCycles); } //----------------------------------------------------------------------------- SS_CARDTYPE MB_GetSoundcardType() { return g_SoundcardType; } static void MB_SetSoundcardType(const SS_CARDTYPE NewSoundcardType) { if (NewSoundcardType == g_SoundcardType) return; if (NewSoundcardType == CT_Empty) MB_Mute(); // Call MB_Mute() before setting g_SoundcardType = CT_Empty g_SoundcardType = NewSoundcardType; g_bPhasorEnable = (g_SoundcardType == CT_Phasor); } //----------------------------------------------------------------------------- void MB_InitializeIO(LPBYTE pCxRomPeripheral, UINT uSlot4, UINT uSlot5) { // Mockingboard: Slot 4 & 5 // Phasor : Slot 4 // : Slot 4 & 5 if (GetCardMgr().QuerySlot(SLOT4) != CT_MockingboardC && GetCardMgr().QuerySlot(SLOT4) != CT_Phasor) { MB_SetSoundcardType(CT_Empty); return; } if (GetCardMgr().QuerySlot(SLOT4) == CT_MockingboardC) RegisterIoHandler(uSlot4, IO_Null, IO_Null, MB_Read, MB_Write, NULL, NULL); else // Phasor RegisterIoHandler(uSlot4, PhasorIO, PhasorIO, MB_Read, MB_Write, NULL, NULL); if (GetCardMgr().QuerySlot(SLOT5) == CT_MockingboardC) RegisterIoHandler(uSlot5, IO_Null, IO_Null, MB_Read, MB_Write, NULL, NULL); MB_SetSoundcardType(GetCardMgr().QuerySlot(SLOT4)); if (g_bDisableDirectSound || g_bDisableDirectSoundMockingboard) return; // Sound buffer may have been stopped by MB_InitializeForLoadingSnapshot(). // NB. DSZeroVoiceBuffer() also zeros the sound buffer, so it's better than directly calling IDirectSoundBuffer::Play(): // - without zeroing, then the previous sound buffer can be heard for a fraction of a second // - eg. when doing Mockingboard playback, then loading a save-state which is also doing Mockingboard playback DSZeroVoiceBuffer(&MockingboardVoice, "MB", g_dwDSBufferSize); } //----------------------------------------------------------------------------- void MB_Mute() { if(g_SoundcardType == CT_Empty) return; if(MockingboardVoice.bActive && !MockingboardVoice.bMute) { MockingboardVoice.lpDSBvoice->SetVolume(DSBVOLUME_MIN); MockingboardVoice.bMute = true; } if(g_nCurrentActivePhoneme >= 0) SSI263Voice[g_nCurrentActivePhoneme].lpDSBvoice->SetVolume(DSBVOLUME_MIN); } //----------------------------------------------------------------------------- void MB_Demute() { if(g_SoundcardType == CT_Empty) return; if(MockingboardVoice.bActive && MockingboardVoice.bMute) { MockingboardVoice.lpDSBvoice->SetVolume(MockingboardVoice.nVolume); MockingboardVoice.bMute = false; } if(g_nCurrentActivePhoneme >= 0) SSI263Voice[g_nCurrentActivePhoneme].lpDSBvoice->SetVolume(SSI263Voice[g_nCurrentActivePhoneme].nVolume); } //----------------------------------------------------------------------------- #ifdef _DEBUG void MB_CheckCumulativeCycles() { if (g_SoundcardType == CT_Empty) return; _ASSERT(g_uLastCumulativeCycles == g_nCumulativeCycles); g_uLastCumulativeCycles = g_nCumulativeCycles; } #endif // Called by: ResetState() and Snapshot_LoadState_v2() void MB_SetCumulativeCycles() { g_uLastCumulativeCycles = g_nCumulativeCycles; } // Called by ContinueExecution() at the end of every execution period (~1000 cycles or ~3 cycle when MODE_STEPPING) // NB. Required for FT's TEST LAB #1 player void MB_PeriodicUpdate(UINT executedCycles) { if (g_SoundcardType == CT_Empty) return; if (g_nMBTimerDevice != kTIMERDEVICE_INVALID) return; const UINT kCyclesPerAudioFrame = 1000; g_cyclesThisAudioFrame += executedCycles; if (g_cyclesThisAudioFrame < kCyclesPerAudioFrame) return; g_cyclesThisAudioFrame %= kCyclesPerAudioFrame; MB_Update(); } //----------------------------------------------------------------------------- static bool CheckTimerUnderflow(USHORT& counter, int& timerIrqDelay, const USHORT nClocks) { if (nClocks == 0) return false; int oldTimer = counter; int timer = counter; timer -= nClocks; counter = (USHORT)timer; bool timerIrq = false; if (timerIrqDelay) // Deal with any previous counter underflow which didn't yet result in an IRQ { _ASSERT(timerIrqDelay == 1); timerIrqDelay = 0; timerIrq = true; // if LATCH is very small then could underflow for every opcode... } if (oldTimer >= 0 && timer < 0) // Underflow occurs for 0x0000 -> 0xFFFF { if (timer <= -2) // TIMER = 0xFFFE (or less) timerIrq = true; else // TIMER = 0xFFFF timerIrqDelay = 1; // ...so 1 cycle until IRQ } return timerIrq; } static int OnTimer1Underflow(USHORT& counter, USHORT latch) { int timer = (int)(short)(counter); while (timer < -1) timer += (latch + kExtraTimerCycles); // GH#651: account for underflowed cycles / GH#652: account for extra 2 cycles counter = (USHORT)timer; return (timer == -1) ? 1 : 0; // timer1IrqDelay } // Called by: // . CpuExecute() every ~1000 cycles @ 1MHz // . MB_SyncEventCallback() on a TIMER1/2 underflow // . MB_Read() / MB_Write() (for both normal & full-speed) void MB_UpdateCycles(ULONG uExecutedCycles) { if (g_SoundcardType == CT_Empty) return; CpuCalcCycles(uExecutedCycles); UINT64 uCycles = g_nCumulativeCycles - g_uLastCumulativeCycles; _ASSERT(uCycles >= 0); if (uCycles == 0) return; g_uLastCumulativeCycles = g_nCumulativeCycles; _ASSERT(uCycles < 0x10000 || g_nAppMode == MODE_BENCHMARK); USHORT nClocks = (USHORT)uCycles; for (int i = 0; i < NUM_SY6522; i++) { SY6522_AY8910* pMB = &g_MB[i]; const bool bTimer1Underflow = CheckTimerUnderflow(pMB->sy6522.TIMER1_COUNTER.w, pMB->sy6522.timer1IrqDelay, nClocks); if (bTimer1Underflow) pMB->sy6522.timer1IrqDelay = OnTimer1Underflow(pMB->sy6522.TIMER1_COUNTER.w, pMB->sy6522.TIMER1_LATCH.w); // No TIMER2 latch so "after timing out, the counter will continue to decrement" CheckTimerUnderflow(pMB->sy6522.TIMER2_COUNTER.w, pMB->sy6522.timer2IrqDelay, nClocks); } } //----------------------------------------------------------------------------- static int MB_SyncEventCallback(int id, int /*cycles*/, ULONG uExecutedCycles) { SY6522_AY8910* pMB = &g_MB[id / kNumTimersPer6522]; if ((id & 1) == 0) { _ASSERT(pMB->bTimer1Active); MB_Update(); UpdateIFR(pMB, 0, IxR_TIMER1); MB_UpdateCycles(uExecutedCycles); if ((pMB->sy6522.ACR & RUNMODE) == RM_ONESHOT) { // One-shot mode // - Phasor's playback code uses one-shot mode StopTimer1(pMB); return 0; // Don't repeat event } StartTimer1(pMB); return pMB->sy6522.TIMER1_COUNTER.w + kExtraTimerCycles; } else { _ASSERT(pMB->bTimer2Active); UpdateIFR(pMB, 0, IxR_TIMER2); StopTimer2(pMB); // TIMER2 only runs in one-shot mode return 0; // Don't repeat event } } //----------------------------------------------------------------------------- bool MB_IsActive() { if (!MockingboardVoice.bActive) return false; return g_bMB_Active; } //----------------------------------------------------------------------------- DWORD MB_GetVolume() { return MockingboardVoice.dwUserVolume; } void MB_SetVolume(DWORD dwVolume, DWORD dwVolumeMax) { MockingboardVoice.dwUserVolume = dwVolume; MockingboardVoice.nVolume = NewVolume(dwVolume, dwVolumeMax); if (MockingboardVoice.bActive && !MockingboardVoice.bMute) MockingboardVoice.lpDSBvoice->SetVolume(MockingboardVoice.nVolume); } //=========================================================================== // Called by debugger - Debugger_Display.cpp void MB_GetSnapshot_v1(SS_CARD_MOCKINGBOARD_v1* const pSS, const DWORD dwSlot) { pSS->Hdr.UnitHdr.hdr.v2.Length = sizeof(SS_CARD_MOCKINGBOARD_v1); pSS->Hdr.UnitHdr.hdr.v2.Type = UT_Card; pSS->Hdr.UnitHdr.hdr.v2.Version = 1; pSS->Hdr.Slot = dwSlot; pSS->Hdr.Type = CT_MockingboardC; UINT nMbCardNum = dwSlot - SLOT4; UINT nDeviceNum = nMbCardNum*2; SY6522_AY8910* pMB = &g_MB[nDeviceNum]; for (UINT i=0; iUnit[i].RegsSY6522; BYTE* s = (BYTE*) &pMB->sy6522; for (UINT j=0; j<=9; j++) // regs $00-$09 *d++ = *s++; s = &pMB->sy6522.SERIAL_SHIFT; for (UINT j=0; j<=6; j++) // regs $0A-$0F *d++ = *s++; } // AY8913 for (UINT j=0; j<16; j++) { pSS->Unit[i].RegsAY8910[j] = AYReadReg(nDeviceNum, j); } memcpy(&pSS->Unit[i].RegsSSI263, &pMB->SpeechChip, sizeof(SSI263A)); pSS->Unit[i].nAYCurrentRegister = pMB->nAYCurrentRegister; pSS->Unit[i].bTimer1IrqPending = false; pSS->Unit[i].bTimer2IrqPending = false; pSS->Unit[i].bSpeechIrqPending = false; nDeviceNum++; pMB++; } } //=========================================================================== // Unit version history: // 2: Added: Timer1 & Timer2 active // 3: Added: Unit state - GH#320 // 4: Added: 6522 timerIrqDelay - GH#652 // 5: Added: Unit state-B (Phasor only) - GH#659 // 6: Changed SS_YAML_KEY_PHASOR_MODE from (0,1) to (0,5,7) // Added SS_YAML_KEY_VOTRAX_PHONEME // Removed: redundant SS_YAML_KEY_PHASOR_CLOCK_SCALE_FACTOR const UINT kUNIT_VERSION = 6; const UINT NUM_MB_UNITS = 2; const UINT NUM_PHASOR_UNITS = 2; #define SS_YAML_KEY_MB_UNIT "Unit" #define SS_YAML_KEY_SY6522 "SY6522" #define SS_YAML_KEY_SY6522_REG_ORB "ORB" #define SS_YAML_KEY_SY6522_REG_ORA "ORA" #define SS_YAML_KEY_SY6522_REG_DDRB "DDRB" #define SS_YAML_KEY_SY6522_REG_DDRA "DDRA" #define SS_YAML_KEY_SY6522_REG_T1_COUNTER "Timer1 Counter" #define SS_YAML_KEY_SY6522_REG_T1_LATCH "Timer1 Latch" #define SS_YAML_KEY_SY6522_REG_T2_COUNTER "Timer2 Counter" #define SS_YAML_KEY_SY6522_REG_T2_LATCH "Timer2 Latch" #define SS_YAML_KEY_SY6522_REG_SERIAL_SHIFT "Serial Shift" #define SS_YAML_KEY_SY6522_REG_ACR "ACR" #define SS_YAML_KEY_SY6522_REG_PCR "PCR" #define SS_YAML_KEY_SY6522_REG_IFR "IFR" #define SS_YAML_KEY_SY6522_REG_IER "IER" #define SS_YAML_KEY_SSI263 "SSI263" #define SS_YAML_KEY_SSI263_REG_DUR_PHON "Duration / Phoneme" #define SS_YAML_KEY_SSI263_REG_INF "Inflection" #define SS_YAML_KEY_SSI263_REG_RATE_INF "Rate / Inflection" #define SS_YAML_KEY_SSI263_REG_CTRL_ART_AMP "Control / Articulation / Amplitude" #define SS_YAML_KEY_SSI263_REG_FILTER_FREQ "Filter Frequency" #define SS_YAML_KEY_SSI263_REG_CURRENT_MODE "Current Mode" #define SS_YAML_KEY_AY_CURR_REG "AY Current Register" #define SS_YAML_KEY_MB_UNIT_STATE "Unit State" #define SS_YAML_KEY_MB_UNIT_STATE_B "Unit State-B" // Phasor only #define SS_YAML_KEY_TIMER1_IRQ "Timer1 IRQ Pending" #define SS_YAML_KEY_TIMER2_IRQ "Timer2 IRQ Pending" #define SS_YAML_KEY_SPEECH_IRQ "Speech IRQ Pending" #define SS_YAML_KEY_TIMER1_ACTIVE "Timer1 Active" #define SS_YAML_KEY_TIMER2_ACTIVE "Timer2 Active" #define SS_YAML_KEY_SY6522_TIMER1_IRQ_DELAY "Timer1 IRQ Delay" #define SS_YAML_KEY_SY6522_TIMER2_IRQ_DELAY "Timer2 IRQ Delay" #define SS_YAML_KEY_PHASOR_UNIT "Unit" #define SS_YAML_KEY_PHASOR_CLOCK_SCALE_FACTOR "Clock Scale Factor" // Redundant from v6 #define SS_YAML_KEY_PHASOR_MODE "Mode" #define SS_YAML_KEY_VOTRAX_PHONEME "Votrax Phoneme" std::string MB_GetSnapshotCardName(void) { static const std::string name("Mockingboard C"); return name; } std::string Phasor_GetSnapshotCardName(void) { static const std::string name("Phasor"); return name; } static void SaveSnapshotSY6522(YamlSaveHelper& yamlSaveHelper, SY6522& sy6522) { YamlSaveHelper::Label label(yamlSaveHelper, "%s:\n", SS_YAML_KEY_SY6522); yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_SY6522_REG_ORB, sy6522.ORB); yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_SY6522_REG_ORA, sy6522.ORA); yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_SY6522_REG_DDRB, sy6522.DDRB); yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_SY6522_REG_DDRA, sy6522.DDRA); yamlSaveHelper.SaveHexUint16(SS_YAML_KEY_SY6522_REG_T1_COUNTER, sy6522.TIMER1_COUNTER.w); yamlSaveHelper.SaveHexUint16(SS_YAML_KEY_SY6522_REG_T1_LATCH, sy6522.TIMER1_LATCH.w); yamlSaveHelper.SaveUint(SS_YAML_KEY_SY6522_TIMER1_IRQ_DELAY, sy6522.timer1IrqDelay); // v4 yamlSaveHelper.SaveHexUint16(SS_YAML_KEY_SY6522_REG_T2_COUNTER, sy6522.TIMER2_COUNTER.w); yamlSaveHelper.SaveHexUint16(SS_YAML_KEY_SY6522_REG_T2_LATCH, sy6522.TIMER2_LATCH.w); yamlSaveHelper.SaveUint(SS_YAML_KEY_SY6522_TIMER2_IRQ_DELAY, sy6522.timer2IrqDelay); // v4 yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_SY6522_REG_SERIAL_SHIFT, sy6522.SERIAL_SHIFT); yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_SY6522_REG_ACR, sy6522.ACR); yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_SY6522_REG_PCR, sy6522.PCR); yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_SY6522_REG_IFR, sy6522.IFR); yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_SY6522_REG_IER, sy6522.IER); // NB. No need to write ORA_NO_HS, since same data as ORA, just without handshake } static void SaveSnapshotSSI263(YamlSaveHelper& yamlSaveHelper, SSI263A& ssi263) { YamlSaveHelper::Label label(yamlSaveHelper, "%s:\n", SS_YAML_KEY_SSI263); yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_SSI263_REG_DUR_PHON, ssi263.DurationPhoneme); yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_SSI263_REG_INF, ssi263.Inflection); yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_SSI263_REG_RATE_INF, ssi263.RateInflection); yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_SSI263_REG_CTRL_ART_AMP, ssi263.CtrlArtAmp); yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_SSI263_REG_FILTER_FREQ, ssi263.FilterFreq); yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_SSI263_REG_CURRENT_MODE, ssi263.CurrentMode); } void MB_SaveSnapshot(YamlSaveHelper& yamlSaveHelper, const UINT uSlot) { const UINT nMbCardNum = uSlot - SLOT4; UINT nDeviceNum = nMbCardNum*2; SY6522_AY8910* pMB = &g_MB[nDeviceNum]; YamlSaveHelper::Slot slot(yamlSaveHelper, MB_GetSnapshotCardName(), uSlot, kUNIT_VERSION); // fixme: object should be just 1 Mockingboard card & it will know its slot YamlSaveHelper::Label state(yamlSaveHelper, "%s:\n", SS_YAML_KEY_STATE); yamlSaveHelper.SaveBool(SS_YAML_KEY_VOTRAX_PHONEME, g_bVotraxPhoneme); for(UINT i=0; isy6522); AY8910_SaveSnapshot(yamlSaveHelper, nDeviceNum, std::string("")); SaveSnapshotSSI263(yamlSaveHelper, pMB->SpeechChip); yamlSaveHelper.SaveHexUint4(SS_YAML_KEY_MB_UNIT_STATE, pMB->state); yamlSaveHelper.SaveHexUint4(SS_YAML_KEY_AY_CURR_REG, pMB->nAYCurrentRegister); yamlSaveHelper.Save("%s: %s # Not supported\n", SS_YAML_KEY_TIMER1_IRQ, "false"); yamlSaveHelper.Save("%s: %s # Not supported\n", SS_YAML_KEY_TIMER2_IRQ, "false"); yamlSaveHelper.Save("%s: %s # Not supported\n", SS_YAML_KEY_SPEECH_IRQ, "false"); yamlSaveHelper.SaveBool(SS_YAML_KEY_TIMER1_ACTIVE, pMB->bTimer1Active); yamlSaveHelper.SaveBool(SS_YAML_KEY_TIMER2_ACTIVE, pMB->bTimer2Active); nDeviceNum++; pMB++; } } static void LoadSnapshotSY6522(YamlLoadHelper& yamlLoadHelper, SY6522& sy6522, UINT version) { if (!yamlLoadHelper.GetSubMap(SS_YAML_KEY_SY6522)) throw std::string("Card: Expected key: ") + std::string(SS_YAML_KEY_SY6522); sy6522.ORB = yamlLoadHelper.LoadUint(SS_YAML_KEY_SY6522_REG_ORB); sy6522.ORA = yamlLoadHelper.LoadUint(SS_YAML_KEY_SY6522_REG_ORA); sy6522.DDRB = yamlLoadHelper.LoadUint(SS_YAML_KEY_SY6522_REG_DDRB); sy6522.DDRA = yamlLoadHelper.LoadUint(SS_YAML_KEY_SY6522_REG_DDRA); sy6522.TIMER1_COUNTER.w = yamlLoadHelper.LoadUint(SS_YAML_KEY_SY6522_REG_T1_COUNTER); sy6522.TIMER1_LATCH.w = yamlLoadHelper.LoadUint(SS_YAML_KEY_SY6522_REG_T1_LATCH); sy6522.TIMER2_COUNTER.w = yamlLoadHelper.LoadUint(SS_YAML_KEY_SY6522_REG_T2_COUNTER); sy6522.TIMER2_LATCH.w = yamlLoadHelper.LoadUint(SS_YAML_KEY_SY6522_REG_T2_LATCH); sy6522.SERIAL_SHIFT = yamlLoadHelper.LoadUint(SS_YAML_KEY_SY6522_REG_SERIAL_SHIFT); sy6522.ACR = yamlLoadHelper.LoadUint(SS_YAML_KEY_SY6522_REG_ACR); sy6522.PCR = yamlLoadHelper.LoadUint(SS_YAML_KEY_SY6522_REG_PCR); sy6522.IFR = yamlLoadHelper.LoadUint(SS_YAML_KEY_SY6522_REG_IFR); sy6522.IER = yamlLoadHelper.LoadUint(SS_YAML_KEY_SY6522_REG_IER); sy6522.ORA_NO_HS = 0; // Not saved sy6522.timer1IrqDelay = sy6522.timer2IrqDelay = 0; if (version >= 4) { sy6522.timer1IrqDelay = yamlLoadHelper.LoadUint(SS_YAML_KEY_SY6522_TIMER1_IRQ_DELAY); sy6522.timer2IrqDelay = yamlLoadHelper.LoadUint(SS_YAML_KEY_SY6522_TIMER2_IRQ_DELAY); } yamlLoadHelper.PopMap(); } static void LoadSnapshotSSI263(YamlLoadHelper& yamlLoadHelper, SSI263A& ssi263) { if (!yamlLoadHelper.GetSubMap(SS_YAML_KEY_SSI263)) throw std::string("Card: Expected key: ") + std::string(SS_YAML_KEY_SSI263); ssi263.DurationPhoneme = yamlLoadHelper.LoadUint(SS_YAML_KEY_SSI263_REG_DUR_PHON); ssi263.Inflection = yamlLoadHelper.LoadUint(SS_YAML_KEY_SSI263_REG_INF); ssi263.RateInflection = yamlLoadHelper.LoadUint(SS_YAML_KEY_SSI263_REG_RATE_INF); ssi263.CtrlArtAmp = yamlLoadHelper.LoadUint(SS_YAML_KEY_SSI263_REG_CTRL_ART_AMP); ssi263.FilterFreq = yamlLoadHelper.LoadUint(SS_YAML_KEY_SSI263_REG_FILTER_FREQ); ssi263.CurrentMode = yamlLoadHelper.LoadUint(SS_YAML_KEY_SSI263_REG_CURRENT_MODE); yamlLoadHelper.PopMap(); } bool MB_LoadSnapshot(YamlLoadHelper& yamlLoadHelper, UINT slot, UINT version) { if (slot != 4 && slot != 5) // fixme throw std::string("Card: wrong slot"); if (version < 1 || version > kUNIT_VERSION) throw std::string("Card: wrong version"); g_bVotraxPhoneme = (version >= 6) ? yamlLoadHelper.LoadBool(SS_YAML_KEY_VOTRAX_PHONEME) : false; AY8910UpdateSetCycles(); const UINT nMbCardNum = slot - SLOT4; UINT nDeviceNum = nMbCardNum*2; SY6522_AY8910* pMB = &g_MB[nDeviceNum]; g_nSSI263Device = 0; g_nCurrentActivePhoneme = -1; for(UINT i=0; isy6522, version); UpdateIFR(pMB, 0, pMB->sy6522.IFR); // Assert any pending IRQs (GH#677) AY8910_LoadSnapshot(yamlLoadHelper, nDeviceNum, std::string("")); LoadSnapshotSSI263(yamlLoadHelper, pMB->SpeechChip); pMB->nAYCurrentRegister = yamlLoadHelper.LoadUint(SS_YAML_KEY_AY_CURR_REG); yamlLoadHelper.LoadBool(SS_YAML_KEY_TIMER1_IRQ); // Consume yamlLoadHelper.LoadBool(SS_YAML_KEY_TIMER2_IRQ); // Consume yamlLoadHelper.LoadBool(SS_YAML_KEY_SPEECH_IRQ); // Consume if (version >= 2) { pMB->bTimer1Active = yamlLoadHelper.LoadBool(SS_YAML_KEY_TIMER1_ACTIVE); pMB->bTimer2Active = yamlLoadHelper.LoadBool(SS_YAML_KEY_TIMER2_ACTIVE); } pMB->state = AY_INACTIVE; pMB->stateB = AY_INACTIVE; if (version >= 3) pMB->state = (MockingboardUnitState_e) (yamlLoadHelper.LoadUint(SS_YAML_KEY_MB_UNIT_STATE) & 7); yamlLoadHelper.PopMap(); // if (version == 1) { StartTimer1_LoadStateV1(pMB); // Attempt to start timer } else // version >= 2 { if (pMB->bTimer1Active) StartTimer1(pMB); // Attempt to start timer } if (pMB->bTimer1Active) { const UINT id = nDeviceNum*kNumTimersPer6522+0; // TIMER1 SyncEvent* pSyncEvent = g_syncEvent[id]; pSyncEvent->SetCycles(pMB->sy6522.TIMER1_COUNTER.w + kExtraTimerCycles); // NB. use COUNTER, not LATCH g_SynchronousEventMgr.Insert(pSyncEvent); } if (pMB->bTimer2Active) { const UINT id = nDeviceNum*kNumTimersPer6522+1; // TIMER2 SyncEvent* pSyncEvent = g_syncEvent[id]; pSyncEvent->SetCycles(pMB->sy6522.TIMER2_COUNTER.w + kExtraTimerCycles); // NB. use COUNTER, not LATCH g_SynchronousEventMgr.Insert(pSyncEvent); } // FIXME: currently only support a single speech chip // NB. g_bVotraxPhoneme is never true, as the phoneme playback completes in SSI263Thread() before this point in the save-state. // NB. SpeechChip.DurationPhoneme will mostly be non-zero during speech playback, as this is the SSI263 register, not whether the phonene is active. // FIXME: So possible race-condition between saving-state & SSI263Thread() if (pMB->SpeechChip.DurationPhoneme || g_bVotraxPhoneme) { g_nSSI263Device = nDeviceNum; g_bPhasorEnable = false; SetSpeechIRQ(pMB); } nDeviceNum++; pMB++; } AY8910_InitClock((int)Get6502BaseClock()); // NB. g_SoundcardType & g_bPhasorEnable setup in MB_InitializeIO() -> MB_SetSoundcardType() return true; } void Phasor_SaveSnapshot(YamlSaveHelper& yamlSaveHelper, const UINT uSlot) { if (uSlot != 4) throw std::string("Card: Phasor only supported in slot-4"); UINT nDeviceNum = 0; SY6522_AY8910* pMB = &g_MB[0]; // fixme: Phasor uses MB's slot4(2x6522), slot4(2xSSI263), but slot4+5(4xAY8910) YamlSaveHelper::Slot slot(yamlSaveHelper, Phasor_GetSnapshotCardName(), uSlot, kUNIT_VERSION); // fixme: object should be just 1 Mockingboard card & it will know its slot YamlSaveHelper::Label state(yamlSaveHelper, "%s:\n", SS_YAML_KEY_STATE); yamlSaveHelper.SaveUint(SS_YAML_KEY_PHASOR_MODE, g_phasorMode); yamlSaveHelper.SaveBool(SS_YAML_KEY_VOTRAX_PHONEME, g_bVotraxPhoneme); for(UINT i=0; isy6522); AY8910_SaveSnapshot(yamlSaveHelper, nDeviceNum+0, std::string("-A")); AY8910_SaveSnapshot(yamlSaveHelper, nDeviceNum+1, std::string("-B")); SaveSnapshotSSI263(yamlSaveHelper, pMB->SpeechChip); yamlSaveHelper.SaveHexUint4(SS_YAML_KEY_MB_UNIT_STATE, pMB->state); yamlSaveHelper.SaveHexUint4(SS_YAML_KEY_MB_UNIT_STATE_B, pMB->stateB); yamlSaveHelper.SaveHexUint4(SS_YAML_KEY_AY_CURR_REG, pMB->nAYCurrentRegister); yamlSaveHelper.Save("%s: %s # Not supported\n", SS_YAML_KEY_TIMER1_IRQ, "false"); yamlSaveHelper.Save("%s: %s # Not supported\n", SS_YAML_KEY_TIMER2_IRQ, "false"); yamlSaveHelper.Save("%s: %s # Not supported\n", SS_YAML_KEY_SPEECH_IRQ, "false"); yamlSaveHelper.SaveBool(SS_YAML_KEY_TIMER1_ACTIVE, pMB->bTimer1Active); yamlSaveHelper.SaveBool(SS_YAML_KEY_TIMER2_ACTIVE, pMB->bTimer2Active); nDeviceNum += 2; pMB++; } } bool Phasor_LoadSnapshot(YamlLoadHelper& yamlLoadHelper, UINT slot, UINT version) { if (slot != 4) // fixme throw std::string("Card: wrong slot"); if (version < 1 || version > kUNIT_VERSION) throw std::string("Card: wrong version"); if (version < 6) yamlLoadHelper.LoadUint(SS_YAML_KEY_PHASOR_CLOCK_SCALE_FACTOR); // Consume redundant data UINT phasorMode = yamlLoadHelper.LoadUint(SS_YAML_KEY_PHASOR_MODE); if (version < 6) { if (phasorMode == 0) phasorMode = PH_Mockingboard; else phasorMode = PH_Phasor; } g_phasorMode = (PHASOR_MODE) phasorMode; g_PhasorClockScaleFactor = (g_phasorMode == PH_Phasor) ? 2 : 1; g_bVotraxPhoneme = (version >= 6) ? yamlLoadHelper.LoadBool(SS_YAML_KEY_VOTRAX_PHONEME) : false; AY8910UpdateSetCycles(); UINT nDeviceNum = 0; SY6522_AY8910* pMB = &g_MB[0]; g_nSSI263Device = 0; g_nCurrentActivePhoneme = -1; for(UINT i=0; isy6522, version); UpdateIFR(pMB, 0, pMB->sy6522.IFR); // Assert any pending IRQs (GH#677) AY8910_LoadSnapshot(yamlLoadHelper, nDeviceNum+0, std::string("-A")); AY8910_LoadSnapshot(yamlLoadHelper, nDeviceNum+1, std::string("-B")); LoadSnapshotSSI263(yamlLoadHelper, pMB->SpeechChip); pMB->nAYCurrentRegister = yamlLoadHelper.LoadUint(SS_YAML_KEY_AY_CURR_REG); yamlLoadHelper.LoadBool(SS_YAML_KEY_TIMER1_IRQ); // Consume yamlLoadHelper.LoadBool(SS_YAML_KEY_TIMER2_IRQ); // Consume yamlLoadHelper.LoadBool(SS_YAML_KEY_SPEECH_IRQ); // Consume if (version >= 2) { pMB->bTimer1Active = yamlLoadHelper.LoadBool(SS_YAML_KEY_TIMER1_ACTIVE); pMB->bTimer2Active = yamlLoadHelper.LoadBool(SS_YAML_KEY_TIMER2_ACTIVE); } pMB->state = AY_INACTIVE; pMB->stateB = AY_INACTIVE; if (version >= 3) pMB->state = (MockingboardUnitState_e) (yamlLoadHelper.LoadUint(SS_YAML_KEY_MB_UNIT_STATE) & 7); if (version >= 5) pMB->stateB = (MockingboardUnitState_e) (yamlLoadHelper.LoadUint(SS_YAML_KEY_MB_UNIT_STATE_B) & 7); yamlLoadHelper.PopMap(); // if (version == 1) { StartTimer1_LoadStateV1(pMB); // Attempt to start timer } else // version >= 2 { if (pMB->bTimer1Active) StartTimer1(pMB); // Attempt to start timer } if (pMB->bTimer1Active) { const UINT id = (nDeviceNum/2)*kNumTimersPer6522+0; // TIMER1 SyncEvent* pSyncEvent = g_syncEvent[id]; pSyncEvent->SetCycles(pMB->sy6522.TIMER1_COUNTER.w + kExtraTimerCycles); // NB. use COUNTER, not LATCH g_SynchronousEventMgr.Insert(pSyncEvent); } if (pMB->bTimer2Active) { const UINT id = (nDeviceNum/2)*kNumTimersPer6522+1; // TIMER2 SyncEvent* pSyncEvent = g_syncEvent[id]; pSyncEvent->SetCycles(pMB->sy6522.TIMER2_COUNTER.w + kExtraTimerCycles); // NB. use COUNTER, not LATCH g_SynchronousEventMgr.Insert(pSyncEvent); } // FIXME: currently only support a single speech chip if (pMB->SpeechChip.DurationPhoneme || g_bVotraxPhoneme) { g_nSSI263Device = nDeviceNum+1; // +1 as speech is always 2nd device of the pair g_bPhasorEnable = true; SetSpeechIRQ(pMB); } nDeviceNum += 2; pMB++; } AY8910_InitClock((int)(Get6502BaseClock() * g_PhasorClockScaleFactor)); // NB. g_SoundcardType & g_bPhasorEnable setup in MB_InitializeIO() -> MB_SetSoundcardType() return true; }