From a88040c3eff43120b7612afa85aa5cee4e4c5665 Mon Sep 17 00:00:00 2001 From: TomCh Date: Fri, 7 Jun 2024 21:10:33 +0100 Subject: [PATCH] SSI263 - support for core functionality (#175, PR #1301) . A phoneme will continue playing back infinitely; unless the phoneme is changed or CTL=1. . Reset doesn't affect SSI263/SC01 (so phonemes continue to play). . CTL=1 sets "PD" (Power Down / "standby") mode, also set at power-on. . CTL=0 brings device out of "PD" mode, the mode will be set to DR1,DR0 and the phoneme P5-P0 will play. . Setting mode to DR1:0 = %00 just disables A/!R (ie. disables interrupts), but otherwise retains the previous DR1:0 mode. . RESET is not connected to !PD/!RST pin 18. . Support edge-case where RESET can enable ints & assert IRQ. . Power-on: PD=1 (so D7=0), reg4 (Filter Freq)=0xFF. . Support SSI263 IRQ and D7 on a Phasor mode change (including Echo+). . $Csxx I/O mapping (same for Mockingboard and Phasor mode). Other: . SSI263 save-state: support SC01 as a sub-unit of the card. . 6522: Fix reg $F (ORA w/HS) to be identical to reg $1 (ORA). --- source/6522.cpp | 7 +- source/Mockingboard.cpp | 37 ++++-- source/SSI263.cpp | 271 ++++++++++++++++++++++++++++------------ source/SSI263.h | 66 ++++++++-- 4 files changed, 274 insertions(+), 107 deletions(-) diff --git a/source/6522.cpp b/source/6522.cpp index e4101a3a..a5c489db 100644 --- a/source/6522.cpp +++ b/source/6522.cpp @@ -153,6 +153,7 @@ void SY6522::Write(BYTE nReg, BYTE nValue) m_regs.ORB = nValue & m_regs.DDRB; break; case 0x01: // ORA + case 0x0f: // ORA_NO_HS m_regs.ORA = nValue & m_regs.DDRA; break; case 0x02: // DDRB @@ -220,8 +221,6 @@ void SY6522::Write(BYTE nReg, BYTE nValue) m_syncEvent[1]->m_canAssertIRQ = (m_regs.IER & IxR_TIMER2) ? true : false; UpdateIFR(0); break; - case 0x0f: // ORA_NO_HS - break; } } @@ -348,6 +347,7 @@ BYTE SY6522::Read(BYTE nReg) if (m_isMegaAudio) nValue = 0x00; // MegaAudio: IRB just reads as $00 break; case 0x01: // IRA + case 0x0f: // IRA_NO_HS nValue = m_regs.ORA | (m_isBusDriven ? 0x00 : (m_regs.DDRA ^ 0xff)); // NB. Inputs bits driven by AY8913 if in PSG READ mode if (m_isMegaAudio) nValue = 0x00; // MegaAudio: IRA just reads as $00 break; @@ -400,9 +400,6 @@ BYTE SY6522::Read(BYTE nReg) if (m_isMegaAudio) nValue &= 0x7F; break; - case 0x0f: // ORA_NO_HS - nValue = m_regs.ORA; - break; } return nValue; diff --git a/source/Mockingboard.cpp b/source/Mockingboard.cpp index 4e221970..1d3d860f 100644 --- a/source/Mockingboard.cpp +++ b/source/Mockingboard.cpp @@ -405,9 +405,9 @@ bool MockingboardCard::Is6522IRQ(void) irq |= m_MBSubUnit[i].sy6522.GetReg(SY6522::rIFR) & 0x80 ? true : false; // NB. Mockingboard generates IRQ on both 6522s: - // . SSI263's IRQ (A/!R) is routed via the 2nd 6522 (at $Cn80) and must generate a 6502 IRQ (not NMI) - // - NB. 2nd SSI263's IRQ is routed via the 1st 6522 (at $Cn00) and again generates a 6502 IRQ - // . SC-01's IRQ (A/!R) is routed via the 6522 at $Cn00 (NB. Only the Mockingboard "Sound/Speech I" card supports the SC-01) + // . SSI263's IRQ (A/!R) is routed via the 2nd 6522's CA1 input (at $Cn80) and must generate a 6502 IRQ (not NMI) + // - NB. 2nd SSI263's IRQ is routed via the 1st 6522's CA1 input (at $Cn00) and again generates a 6502 IRQ + // . SC-01's IRQ (!A/R) is routed via the 6522 at $Cn00 (NB. Only the Mockingboard "Sound/Speech I" card supports the SC-01) // Phasor's SSI263 IRQ (A/!R) line is *also* wired directly to the 6502's IRQ (as well as the 6522's CA1) return irq; @@ -576,7 +576,7 @@ void MockingboardCard::Reset(const bool powerCycle) // CTRL+RESET or power-cycle m_MBSubUnit[subunit].Reset(QueryType()); m_MBSubUnit[subunit].ssi263.SetCardMode(PH_Mockingboard); // Revert to PH_Mockingboard mode - m_MBSubUnit[subunit].ssi263.Reset(); + m_MBSubUnit[subunit].ssi263.Reset(powerCycle, m_phasorEnable); } // Reset state @@ -668,7 +668,7 @@ BYTE MockingboardCard::IOReadInternal(WORD PC, WORD nAddr, BYTE bWrite, BYTE nVa return MemReadFloatingBus(nExecutedCycles); #endif - // NB. Mockingboard: SSI263.bit7 not readable (TODO: check this with real h/w) + // NB. Mockingboard: SSI263.bit7 not readable const BYTE subunit = QueryType() == CT_SDMusic ? SY6522_DEVICE_A : !(nAddr & 0x80) ? SY6522_DEVICE_A : SY6522_DEVICE_B; const BYTE reg = nAddr & 0xf; return m_MBSubUnit[subunit].sy6522.Read(reg); @@ -751,14 +751,13 @@ BYTE MockingboardCard::IOWriteInternal(WORD PC, WORD nAddr, BYTE bWrite, BYTE nV WriteToORB(SY6522_DEVICE_B); } - bool CS_SSI263_A = (m_phasorMode == PH_Phasor) ? !(nAddr & 0x80) && (nAddr & 0x40) // SSI263 at $Cn4x, $Cn6x - : nAddr & 0x40; // SSI263 at $Cn4x-Cn7x, $CnCx-CnFx - - bool CS_SSI263_B = (m_phasorMode == PH_Phasor) ? !(nAddr & 0x80) && (nAddr & 0x20) // SSI263 at $Cn2x, $Cn6x - : nAddr & 0x20; // SSI263 at $Cn2x-Cn3x, $Cn6x-Cn7x, $CnAx-CnBx, $CnEx-CnFx - if (m_phasorMode == PH_Mockingboard || m_phasorMode == PH_Phasor) // No SSI263 for Echo+ { + // Confirmed that Phasor has no extra logic to map SSI263 (it's the same as Mockingboard's) + bool CS_SSI263_A = nAddr & 0x40; // SSI263 at $Cn4x-Cn7x, $CnCx-CnFx + + bool CS_SSI263_B = nAddr & 0x20; // SSI263 at $Cn2x-Cn3x, $Cn6x-Cn7x, $CnAx-CnBx, $CnEx-CnFx + // NB. Mockingboard mode: writes to $Cn4x/SSI263 also get written to 1st 6522 (have confirmed on real Phasor h/w) if (CS_SSI263_A) // Primary SSI263 m_MBSubUnit[1].ssi263.Write(nAddr&0x7, nValue); // 2nd 6522 is used for 1st speech chip @@ -839,6 +838,8 @@ BYTE MockingboardCard::PhasorIOInternal(WORD PC, WORD nAddr, BYTE bWrite, BYTE n m_phasorClockScaleFactor = 1; else if (m_phasorMode == PH_Phasor) m_phasorClockScaleFactor = 2; + else // undefined mode + m_phasorClockScaleFactor = 1; // TODO: Check for undefined Phasor mode if (m_phasorMode == PH_Mockingboard) { @@ -849,7 +850,7 @@ BYTE MockingboardCard::PhasorIOInternal(WORD PC, WORD nAddr, BYTE bWrite, BYTE n AY8910_InitClock((int)(Get6502BaseClock() * m_phasorClockScaleFactor)); for (UINT i = 0; i < NUM_SSI263; i++) - m_MBSubUnit[i].ssi263.SetCardMode(m_phasorMode); + m_MBSubUnit[i].ssi263.SetCardMode(m_phasorMode); // TODO: Check for undefined Phasor mode #if DBG_SUPPORT_ECHOPLUS if (m_phasorMode == PH_EchoPlus && (nAddr & 0xf) == 0) @@ -1155,7 +1156,9 @@ UINT MockingboardCard::AY8910_LoadSnapshot(YamlLoadHelper& yamlLoadHelper, BYTE // "Reg Address Latch Valid A" + "Reg Address Latch Valid B" // Changed at AppleWin 1.30.14 //11: Added: "Bus Driven by AY" -const UINT kUNIT_VERSION = 11; +//12: Added: SSI263: SC01 phoneme & active +// Current Mode changed (added bit5 = enableInts) +const UINT kUNIT_VERSION = 12; #define SS_YAML_KEY_MB_UNIT "Unit" #define SS_YAML_KEY_AY_CURR_REG "AY Current Register" @@ -1231,6 +1234,8 @@ void MockingboardCard::SaveSnapshot(YamlSaveHelper& yamlSaveHelper) pMB->sy6522.SaveSnapshot(yamlSaveHelper); AY8910_SaveSnapshot(yamlSaveHelper, subunit, AY8913_DEVICE_A, std::string("")); pMB->ssi263.SaveSnapshot(yamlSaveHelper); + if (subunit == 0) // has SC01 + pMB->ssi263.SC01_SaveSnapshot(yamlSaveHelper); yamlSaveHelper.SaveHexUint4(SS_YAML_KEY_MB_UNIT_STATE, pMB->state[0]); yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_AY_CURR_REG, pMB->nAYCurrentRegister[0]); // save all 8 bits (even though top 4 bits should be 0) @@ -1271,6 +1276,8 @@ bool MockingboardCard::LoadSnapshot(YamlLoadHelper& yamlLoadHelper, UINT version UpdateIFRandIRQ(pMB, 0, pMB->sy6522.GetReg(SY6522::rIFR)); // Assert any pending IRQs (GH#677) AY8910_LoadSnapshot(yamlLoadHelper, subunit, AY8913_DEVICE_A, std::string("")); pMB->ssi263.LoadSnapshot(yamlLoadHelper, PH_Mockingboard, version); // Pre: SetVotraxPhoneme() + if (subunit == 0) // has SC01 + pMB->ssi263.SC01_LoadSnapshot(yamlLoadHelper, version); pMB->nAYCurrentRegister[0] = yamlLoadHelper.LoadUint(SS_YAML_KEY_AY_CURR_REG); @@ -1337,6 +1344,8 @@ void MockingboardCard::Phasor_SaveSnapshot(YamlSaveHelper& yamlSaveHelper) AY8910_SaveSnapshot(yamlSaveHelper, subunit, AY8913_DEVICE_A, std::string("-A")); AY8910_SaveSnapshot(yamlSaveHelper, subunit, AY8913_DEVICE_B, std::string("-B")); pMB->ssi263.SaveSnapshot(yamlSaveHelper); + if (subunit == 0) // has SC01 + pMB->ssi263.SC01_SaveSnapshot(yamlSaveHelper); yamlSaveHelper.SaveHexUint4(SS_YAML_KEY_MB_UNIT_STATE, pMB->state[0]); yamlSaveHelper.SaveHexUint4(SS_YAML_KEY_MB_UNIT_STATE_B, pMB->state[1]); @@ -1405,6 +1414,8 @@ bool MockingboardCard::Phasor_LoadSnapshot(YamlLoadHelper& yamlLoadHelper, UINT AY8910_LoadSnapshot(yamlLoadHelper, subunit, AY8913_DEVICE_B, std::string("-B")); } pMB->ssi263.LoadSnapshot(yamlLoadHelper, m_phasorMode, version); // Pre: SetVotraxPhoneme() + if (subunit == 0) // has SC01 + pMB->ssi263.SC01_LoadSnapshot(yamlLoadHelper, version); pMB->nAYCurrentRegister[0] = yamlLoadHelper.LoadUint(SS_YAML_KEY_AY_CURR_REG); if (version >= 10) diff --git a/source/SSI263.cpp b/source/SSI263.cpp index b6e411bd..fc8a9e48 100644 --- a/source/SSI263.cpp +++ b/source/SSI263.cpp @@ -4,7 +4,7 @@ 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-2021, Tom Charlesworth, Michael Pohoreski, Nick Westgate +Copyright (C) 2006-2024, Tom Charlesworth, Michael Pohoreski, Nick Westgate AppleWin is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -23,7 +23,24 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA /* Description: SSI263 emulation * - * Author: Various + * Extra "spec" that's not obvious from the datasheet: (GH#175) + * . Writes to regs 0,1,2 (and reg3.CTL=1) all de-assert the IRQ (and writes to reg3.CTL=0 and regs 4..7 don't) (GH#1197) + * . A phoneme will continue playing back infinitely; unless the phoneme is changed or CTL=1. + * . NB. if silenced (Amplitude=0) it's still playing. + * . The IRQ is set at the end of the phoneme. + * . If IRQ is then cleared, a new IRQ will occur when the phoneme completes again (but need to clear IRQ with a write to reg0, 1 or 2, even for Mockingboard-C). + * . CTL=1 sets "PD" (Power Down / "standby") mode, also set at power-on. + * . Registers can still be changed in this mode. + * . IRQ de-asserted & D7=0. + * . CTL=0 brings device out of "PD" mode, the mode will be set to DR1,DR0 and the phoneme P5-P0 will play. + * . Setting mode to DR1:0 = %00 just disables A/!R (ie. disables interrupts), but otherwise retains the previous DR1:0 mode. + * . If an IRQ was previously asserted then to set DR1:0=%00, you must go via CTL=1, which de-asserts the IRQ. + * . Mockingboard-C: CTRL+RESET is not connected to !PD/!RST pin 18. + * . Phasor: TODO: check with a 'scope. + * . Phasor: with SSI263 ints disabled & reg0's DR1:0 != %00, then CTRL+RESET will cause SSI263 to enable ints & assert IRQ. + * . it's as if the SSI263 does a CTL H->L to pick-up the new DR1:0. (Bug in SSI263? Assume it should remain in PD mode.) + * . but if CTL=1, then CTRL+RESET has no effect. + * . Power-on: PD=1 (so D7=0), reg4 (Filter Freq)=0xFF (other regs are seemingly random?). */ #include "StdAfx.h" @@ -54,26 +71,6 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA const DWORD SAMPLE_RATE_SSI263 = 22050; -// Duration/Phonome -const BYTE DURATION_MODE_MASK = 0xC0; -const BYTE DURATION_SHIFT = 6; -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 @@ -129,7 +126,7 @@ BYTE SSI263::Read(ULONG nExecutedCycles) // . inverted "A/!R" is high for REQ (ie. Request, as phoneme nearly complete) // NB. this doesn't clear the IRQ - return MemReadFloatingBus(m_currentMode & 1, nExecutedCycles); + return MemReadFloatingBus(m_currentMode.D7, nExecutedCycles); } void SSI263::Write(BYTE nReg, BYTE nValue) @@ -141,30 +138,20 @@ void SSI263::Write(BYTE nReg, BYTE nValue) ssiRegs[nReg] = nValue; #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. - // . Empirically writes to regs 0,1 & 2 all clear the IRQ (and writes to 3,4..7 don't) (GH#1197) - // NB. For Mockingboard, A/!R is ack'ed by 6522's PCR handshake and D7 is cleared. - if (m_cardMode == PH_Phasor && nReg <= SSI_RATEINF) + // SSI263 datasheet is not clear, but a write to DURPHON de-asserts the IRQ and clears D7. + // . Empirically writes to regs 0,1,2 (and reg3.CTL=1) all de-assert the IRQ (and writes to reg3.CTL=0 and regs 4..7 don't) (GH#1197) + // NB. The same for Mockingboard as there's no automatic handshake from the 6522 (CA2 isn't connected to the SSI263). So writes to regs 0, 1 or 2 complete the "handshake". + if (nReg <= SSI_RATEINF) { CpuIrqDeassert(IS_SPEECH); - m_currentMode &= ~1; // Clear SSI263's D7 pin + m_currentMode.D7 = 0; } 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); + 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 @@ -172,25 +159,27 @@ void SSI263::Write(BYTE nReg, BYTE nValue) #endif m_durationPhoneme = nValue; + m_isVotraxPhoneme = false; - Play(nValue & PHONEME_MASK); + if ((m_ctrlArtAmp & CONTROL_MASK) == 0) + Play(m_durationPhoneme & PHONEME_MASK); // Play phoneme when *not* in power-down / standby mode break; case SSI_INFLECT: #if LOG_SSI263 - if(g_fh) fprintf(g_fh, "INF = 0x%02X\n", nValue); + if (g_fh) fprintf(g_fh, "INF = 0x%02X\n", nValue); #endif m_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); + if (g_fh) fprintf(g_fh, "RATE = 0x%02X, INF = 0x%02X\n", nValue>>4, nValue&0x0F); #endif m_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); + 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 = (m_ctrlArtAmp & CONTROL_MASK) && !(nValue & CONTROL_MASK); @@ -202,23 +191,25 @@ void SSI263::Write(BYTE nReg, BYTE nValue) if ( ((m_ctrlArtAmp & CONTROL_MASK) && !(nValue & CONTROL_MASK)) || ((nValue&0xF) == 0x0) ) // H->L or amp=0 SSI_Output(); #endif - if((m_ctrlArtAmp & CONTROL_MASK) && !(nValue & CONTROL_MASK)) // H->L + if ((m_ctrlArtAmp & CONTROL_MASK) && !(nValue & CONTROL_MASK)) // H->L { - m_currentMode = m_durationPhoneme & DURATION_MODE_MASK; - if (m_currentMode == MODE_IRQ_DISABLED) - { - // "Disables A/!R output only; does not change previous A/!R response" (SSI263 datasheet) -// CpuIrqDeassert(IS_SPEECH); - } + // NB. Just changed from CTL=1 (power-down) - where IRQ was already de-asserted & D7=0 + // . So CTL H->L never affects IRQ or D7 + SetDeviceModeAndInts(); + + // Device out of power down / "standby" mode, so play phoneme + m_isVotraxPhoneme = false; + Play(m_durationPhoneme & PHONEME_MASK); } m_ctrlArtAmp = nValue; // "Setting the Control bit (CTL) to a logic one puts the device into Power Down mode..." (SSI263 datasheet) + // . this silences the phoneme - actually "turns off the excitation sources and analog circuits" if (m_ctrlArtAmp & CONTROL_MASK) { -// CpuIrqDeassert(IS_SPEECH); -// m_currentMode &= ~1; // Clear SSI263's D7 pin + CpuIrqDeassert(IS_SPEECH); + m_currentMode.D7 = 0; } break; case SSI_FILFREQ: // RegAddr.b2=1 (b1 & b0 are: don't care) @@ -231,6 +222,20 @@ void SSI263::Write(BYTE nReg, BYTE nValue) } } +void SSI263::SetDeviceModeAndInts(void) +{ + if ((m_durationPhoneme & DURATION_MODE_MASK) != MODE_IRQ_DISABLED) + { + m_currentMode.function = (m_durationPhoneme & DURATION_MODE_MASK) >> DURATION_MODE_SHIFT; + m_currentMode.enableInts = 1; + } + else + { + // "Disables A/!R output only; does not change previous A/!R response" (SSI263 datasheet) + m_currentMode.enableInts = 0; + } +} + //----------------------------------------------------------------------------- const BYTE SSI263::m_Votrax2SSI263[/*64*/] = @@ -310,6 +315,7 @@ void SSI263::Votrax_Write(BYTE value) LogOutput("SC01: %02X (= SSI263: %02X)\n", value, m_Votrax2SSI263[value & PHONEME_MASK]); #endif m_isVotraxPhoneme = true; + m_votraxPhoneme = value & PHONEME_MASK; // !A/R: Acknowledge receipt of phoneme data (signal goes from high to low) UpdateIFR(m_device, SY6522::IxR_VOTRAX, 0); @@ -317,7 +323,7 @@ void SSI263::Votrax_Write(BYTE value) // NB. Don't set reg0.DUR, as SC01's phoneme duration doesn't change with pitch (empirically determined from MAME's SC01 emulation) //m_durationPhoneme = value; // Set reg0.DUR = I1:0 (inflection or pitch) m_durationPhoneme = 0; - Play(m_Votrax2SSI263[value & PHONEME_MASK]); + Play(m_Votrax2SSI263[m_votraxPhoneme]); } //----------------------------------------------------------------------------- @@ -366,6 +372,7 @@ void SSI263::Play(unsigned int nPhoneme) m_phonemeAccurateLengthRemaining = m_phonemeLengthRemaining; m_phonemePlaybackAndDebugger = (g_nAppMode == MODE_STEPPING || g_nAppMode == MODE_DEBUG); m_phonemeCompleteByFullSpeed = false; + m_phonemeLeadoutLength = m_phonemeLengthRemaining / 10; // Arbitrary! (TODO: determine a more accurate factor) if (bPause) { @@ -581,18 +588,22 @@ void SSI263::Update(void) //------------- - const double amplitude = !m_isVotraxPhoneme ? (double)(m_ctrlArtAmp & AMPLITUDE_MASK) / (double)AMPLITUDE_MASK : 1.0; + const double amplitude = m_isVotraxPhoneme ? 1.0 + : m_ctrlArtAmp & CONTROL_MASK ? 0.0 // Power-down / standby + : m_filterFreq == FILTER_FREQ_SILENCE ? 0.0 + : (double)(m_ctrlArtAmp & AMPLITUDE_MASK) / (double)AMPLITUDE_MASK; bool bSpeechIRQ = false; { - const BYTE DUR = m_durationPhoneme >> DURATION_SHIFT; + const BYTE DUR = (m_currentMode.function == (MODE_FRAME_IMMEDIATE_INFLECTION >> DURATION_MODE_SHIFT)) ? 3 // Frame timing mode + : m_durationPhoneme >> DURATION_MODE_SHIFT; // Phoneme timing mode const BYTE numSamplesToAvg = (DUR <= 1) ? 1 : (DUR == 2) ? 2 : 4; short* pMixBuffer = &m_mixBufferSSI263[0]; - int zeroSize = nNumSamples; + UINT zeroSize = nNumSamples; if (m_phonemeLengthRemaining && !prefillBufferOnInit) { @@ -633,7 +644,12 @@ void SSI263::Update(void) } if (zeroSize) + { memset(pMixBuffer, 0, zeroSize * sizeof(short)); + + if (!prefillBufferOnInit) + m_phonemeLeadoutLength -= (m_phonemeLeadoutLength > zeroSize) ? zeroSize : m_phonemeLeadoutLength; + } } // @@ -666,8 +682,24 @@ void SSI263::Update(void) { // NB. if m_phonemePlaybackAndDebugger==true, then "m_phonemeAccurateLengthRemaining!=0" must be true. // Since in UpdateAccurateLength(), (when m_phonemePlaybackAndDebugger==true) then m_phonemeAccurateLengthRemaining decs to zero. +#if _DEBUG + if (m_phonemePlaybackAndDebugger) + { + _ASSERT(m_phonemeAccurateLengthRemaining); // Check this! + } +#endif if (!m_phonemePlaybackAndDebugger /*|| m_phonemeAccurateLengthRemaining*/) // superfluous, so commented out (see above) + { UpdateIRQ(); + } + } + + if (m_phonemeLeadoutLength == 0) + { + if (!m_isVotraxPhoneme) + Play(m_durationPhoneme & PHONEME_MASK); // Repeat this phoneme again + else + Play(m_Votrax2SSI263[m_votraxPhoneme]); // Votrax phoneme repeats too (tested in MAME 0.262) } } @@ -689,7 +721,7 @@ void SSI263::UpdateAccurateLength(void) const double nIrqFreq = g_fCurrentCLK6502 / updateInterval + 0.5; // Round-up const int nNumSamplesPerPeriod = (int)((double)(SAMPLE_RATE_SSI263) / nIrqFreq); // Eg. For 60Hz this is 367 - const BYTE DUR = m_durationPhoneme >> DURATION_SHIFT; + const BYTE DUR = m_durationPhoneme >> DURATION_MODE_SHIFT; const UINT numSamples = nNumSamplesPerPeriod * (DUR+1); if (m_phonemeAccurateLengthRemaining > numSamples) @@ -733,32 +765,37 @@ void SSI263::UpdateIRQ(void) // Pre: m_isVotraxPhoneme, m_cardMode, m_device void SSI263::SetSpeechIRQ(void) { - if (!m_isVotraxPhoneme) + if (!m_isVotraxPhoneme && (m_ctrlArtAmp & CONTROL_MASK) == 0) { - // Always set SSI263's D7 pin regardless of SSI263 mode (DR1:0), including MODE_IRQ_DISABLED - m_currentMode |= 1; // Set SSI263's D7 pin - - if ((m_currentMode & DURATION_MODE_MASK) != MODE_IRQ_DISABLED) + if (m_currentMode.enableInts) { if (m_cardMode == PH_Mockingboard) { - if ((GetPCR(m_device) & 1) == 0) // CA1 Latch/Input = 0 (Negative active edge) - UpdateIFR(m_device, 0, SY6522::IxR_SSI263); - if (GetPCR(m_device) == 0x0C) // CA2 Control = b#110 (Low output) - m_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 + if (m_currentMode.D7 == 0) + { + // 6522's PCR = 0x0C (all SSI263 speech routine use this value, but 0x00 will do equally as well!) + // . b3:1 CA2 Control = b#110 (Low output) - not connected + // . b0 CA1 Latch/Input = 0 (Negative active edge) - input from SSI263's A/!R + if ((GetPCR(m_device) & 1) == 0) // Level change from SSI263's A/!R, latch this as an interrupt + UpdateIFR(m_device, 0, SY6522::IxR_SSI263); + } } - else if (m_cardMode == 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) + else if (m_cardMode == PH_Phasor) { + // Phasor (in native mode): SSI263 IRQ (A/!R) pin is connected directly to the 6502's IRQ + // . And Mockingboard mode: A/!R is connected to the 6522's CA1 CpuIrqAssert(IS_SPEECH); } else { - _ASSERT(0); + _ASSERT(m_cardMode == PH_EchoPlus); + // SSI263 not visible from Echo+ mode, but still continues to operate } } + + // Always set SSI263's D7 pin regardless of SSI263 mode (DR1:0), including when SSI263 ints are disabled (via MODE_IRQ_DISABLED) + // NB. Don't set D7 when in power-down / standby mode. + m_currentMode.D7 = 1; } // @@ -766,15 +803,34 @@ void SSI263::SetSpeechIRQ(void) if (m_isVotraxPhoneme && GetPCR(m_device) == 0xB0) { // !A/R: Time-out of old phoneme (signal goes from low to high) - UpdateIFR(m_device, 0, SY6522::IxR_VOTRAX); - - m_isVotraxPhoneme = false; } } //----------------------------------------------------------------------------- +void SSI263::SetCardMode(PHASOR_MODE mode) +{ + const PHASOR_MODE oldCardMode = m_cardMode; + m_cardMode = mode; + + if (oldCardMode == m_cardMode) + return; + + // mode change + + if (m_currentMode.D7 == 1) + { + m_currentMode.D7 = 0; // So that \PH_Mockingboard\ path sets IFR. Post: D7=1 + SetSpeechIRQ(); + } + + if (m_cardMode != PH_Phasor) + CpuIrqDeassert(IS_SPEECH); +} + +//----------------------------------------------------------------------------- + bool SSI263::DSInit(void) { // @@ -811,10 +867,27 @@ void SSI263::DSUninit(void) //----------------------------------------------------------------------------- -void SSI263::Reset(void) +// SSI263 phoneme continues to play after CTRL+RESET (tested on real h/w) +// Votrax phoneme continues to play after CTRL+RESET (tested on MAME 0.262) +void SSI263::Reset(const bool powerCycle, const bool isPhasorCard) { + if (!powerCycle) + { + if (isPhasorCard) + { + // Empirically observed it does CTL H->L to enable ints (and set the device mode?) (GH#175) + // NB. CTRL+RESET doesn't clear m_ctrlArtAmp.CTL (ie. if the device is in power-down/standby mode then ignore RST) + // Speculate that there's a bug in the SSI263 and that RST should put the device into power-down/standby mode (ie. silence the device) + // TODO: Stick a 'scope on !PD/!RST pin 18 to see what the Phasor h/w does. + if ((m_ctrlArtAmp & CONTROL_MASK) == 0) + SetDeviceModeAndInts(); + } + + return; + } + Stop(); - ResetState(); + ResetState(powerCycle); CpuIrqDeassert(IS_SPEECH); } @@ -884,7 +957,7 @@ void SSI263::SaveSnapshot(YamlSaveHelper& yamlSaveHelper) yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_SSI263_REG_RATE_INF, m_rateInflection); yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_SSI263_REG_CTRL_ART_AMP, m_ctrlArtAmp); yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_SSI263_REG_FILTER_FREQ, m_filterFreq); - yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_SSI263_CURRENT_MODE, m_currentMode); + yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_SSI263_CURRENT_MODE, m_currentMode.mode); yamlSaveHelper.SaveBool(SS_YAML_KEY_SSI263_ACTIVE_PHONEME, IsPhonemeActive()); } @@ -898,10 +971,23 @@ void SSI263::LoadSnapshot(YamlLoadHelper& yamlLoadHelper, PHASOR_MODE mode, UINT m_rateInflection = yamlLoadHelper.LoadUint(SS_YAML_KEY_SSI263_REG_RATE_INF); m_ctrlArtAmp = yamlLoadHelper.LoadUint(SS_YAML_KEY_SSI263_REG_CTRL_ART_AMP); m_filterFreq = yamlLoadHelper.LoadUint(SS_YAML_KEY_SSI263_REG_FILTER_FREQ); - m_currentMode = yamlLoadHelper.LoadUint(SS_YAML_KEY_SSI263_CURRENT_MODE); + m_currentMode.mode = yamlLoadHelper.LoadUint(SS_YAML_KEY_SSI263_CURRENT_MODE); bool activePhoneme = (version >= 7) ? yamlLoadHelper.LoadBool(SS_YAML_KEY_SSI263_ACTIVE_PHONEME) : false; m_currentActivePhoneme = !activePhoneme ? -1 : 0x00; // Not important which phoneme, since UpdateIRQ() resets this + if (version < 12) + { + if (m_currentMode.function == 0) // invalid function (but in older versions this was accepted) + { + m_currentMode.function = MODE_PHONEME_TRANSITIONED_INFLECTION >> DURATION_MODE_SHIFT; // Typically this is used + m_currentMode.enableInts = 0; + } + else + { + m_currentMode.enableInts = 1; + } + } + yamlLoadHelper.PopMap(); // @@ -910,7 +996,7 @@ void SSI263::LoadSnapshot(YamlLoadHelper& yamlLoadHelper, PHASOR_MODE mode, UINT SetCardMode(mode); // Only need to directly assert IRQ for Phasor mode (for Mockingboard mode it's done via UpdateIFR() in parent) - if (m_cardMode == PH_Phasor && (m_currentMode & DURATION_MODE_MASK) != MODE_IRQ_DISABLED && (m_currentMode & 1)) + if (m_cardMode == PH_Phasor && (m_ctrlArtAmp & CONTROL_MASK) == 0 && m_currentMode.enableInts && m_currentMode.D7 == 1) CpuIrqAssert(IS_SPEECH); if (IsPhonemeActive()) @@ -918,3 +1004,30 @@ void SSI263::LoadSnapshot(YamlLoadHelper& yamlLoadHelper, PHASOR_MODE mode, UINT m_lastUpdateCycle = GetLastCumulativeCycles(); } + +//============================================================================= + +#define SS_YAML_KEY_SC01 "SC01" +// NB. No version - this is determined by the parent "Mockingboard C" or "Phasor" unit + +#define SS_YAML_KEY_SC01_PHONEME "SC01 Phoneme" +#define SS_YAML_KEY_SC01_ACTIVE_PHONEME "SC01 Active Phoneme" + +void SSI263::SC01_SaveSnapshot(YamlSaveHelper& yamlSaveHelper) +{ + YamlSaveHelper::Label label(yamlSaveHelper, "%s:\n", SS_YAML_KEY_SC01); + + yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_SC01_PHONEME, m_votraxPhoneme); + yamlSaveHelper.SaveBool(SS_YAML_KEY_SC01_ACTIVE_PHONEME, m_isVotraxPhoneme); +} + +void SSI263::SC01_LoadSnapshot(YamlLoadHelper& yamlLoadHelper, UINT version) +{ + if (!yamlLoadHelper.GetSubMap(SS_YAML_KEY_SC01)) + throw std::runtime_error("Card: Expected key: " SS_YAML_KEY_SC01); + + m_votraxPhoneme = yamlLoadHelper.LoadUint(SS_YAML_KEY_SC01_PHONEME); + m_isVotraxPhoneme = yamlLoadHelper.LoadBool(SS_YAML_KEY_SC01_ACTIVE_PHONEME); + + yamlLoadHelper.PopMap(); +} diff --git a/source/SSI263.h b/source/SSI263.h index 3096bed4..a153428f 100644 --- a/source/SSI263.h +++ b/source/SSI263.h @@ -11,17 +11,18 @@ public: m_cardMode = PH_Mockingboard; m_pPhonemeData00 = NULL; - ResetState(); + ResetState(true); } ~SSI263(void) { delete [] m_pPhonemeData00; } - void ResetState(void) + void ResetState(const bool powerCycle) { m_currentActivePhoneme = -1; m_isVotraxPhoneme = false; + m_votraxPhoneme = 0; m_cyclesThisAudioFrame = 0; // @@ -34,6 +35,7 @@ public: m_phonemeAccurateLengthRemaining = 0; m_phonemePlaybackAndDebugger = false; m_phonemeCompleteByFullSpeed = false; + m_phonemeLeadoutLength = 0; // @@ -45,13 +47,19 @@ public: // - m_durationPhoneme = 0; + // After a chip power-on, if the first thing done is CTL=0, then empirically it can be observed that: + // . enableInts = 1 (mostly an SSI263 interrupt occurs, but not always) + // . DR1:0 != b#00 (since enableInts is set to 1, except when no interrupt => DR1:0 == b#00) + m_durationPhoneme = MODE_PHONEME_TRANSITIONED_INFLECTION; // Typical function & phoneme=$00 m_inflection = 0; m_rateInflection = 0; - m_ctrlArtAmp = 0; - m_filterFreq = 0; + m_ctrlArtAmp = powerCycle ? CONTROL_MASK : 0; // Chip power-on, so CTL=1 (power-down / standby) + m_filterFreq = powerCycle ? FILTER_FREQ_SILENCE : 0; // Empirically observed at chip power-on (GH#1302) - m_currentMode = 0; + m_currentMode.mode = 0; + m_currentMode.function = 0; // Set at runtime when CTL=0 + m_currentMode.enableInts = 0; // Set at runtime when CTL=0 + m_currentMode.D7 = 0; // Since in power-down mode (as per SSI263 datasheet) // @@ -60,12 +68,12 @@ public: } void SetDevice(UINT device) { m_device = device; } - void SetCardMode(PHASOR_MODE mode) { m_cardMode = mode; } + void SetCardMode(PHASOR_MODE mode); bool DSInit(void); void DSUninit(void); - void Reset(void); + void Reset(const bool powerCycle, const bool isPhasorCard); bool IsPhonemeActive(void) { return m_currentActivePhoneme >= 0; } BYTE Read(ULONG nExecutedCycles); @@ -85,12 +93,15 @@ public: void SaveSnapshot(class YamlSaveHelper& yamlSaveHelper); void LoadSnapshot(class YamlLoadHelper& yamlLoadHelper, PHASOR_MODE mode, UINT version); + void SC01_SaveSnapshot(YamlSaveHelper& yamlSaveHelper); + void SC01_LoadSnapshot(YamlLoadHelper& yamlLoadHelper, UINT version); private: void Play(unsigned int nPhoneme); void Stop(void); void UpdateIRQ(void); void UpdateAccurateLength(void); + void SetDeviceModeAndInts(void); UINT64 GetLastCumulativeCycles(void); void UpdateIFR(BYTE nDevice, BYTE clr_mask, BYTE set_mask); @@ -105,13 +116,37 @@ private: // + // Duration/Phonome + static const BYTE DURATION_MODE_MASK = 0xC0; + static const BYTE DURATION_MODE_SHIFT = 6; + static const BYTE PHONEME_MASK = 0x3F; + + static const BYTE MODE_PHONEME_TRANSITIONED_INFLECTION = 0xC0; + static const BYTE MODE_PHONEME_IMMEDIATE_INFLECTION = 0x80; + static const BYTE MODE_FRAME_IMMEDIATE_INFLECTION = 0x40; + static const BYTE MODE_IRQ_DISABLED = 0x00; // disable interrupts, but retains one of the 3 modes + + // Rate/Inflection + static const BYTE RATE_MASK = 0xF0; + static const BYTE INFLECTION_MASK_H = 0x08; // I11 + static const BYTE INFLECTION_MASK_L = 0x07; // I2..I0 + + // Ctrl/Art/Amp + static const BYTE ARTICULATION_MASK = 0x70; + static const BYTE AMPLITUDE_MASK = 0x0F; + static const BYTE CONTROL_MASK = 0x80; + + // Filter frequency range + static const BYTE FILTER_FREQ_SILENCE = 0xFF; + UINT m_slot; BYTE m_device; // SSI263 device# which is generating phoneme-complete IRQ (and only required whilst Mockingboard isn't a class) PHASOR_MODE m_cardMode; short* m_pPhonemeData00; - int m_currentActivePhoneme; + int m_currentActivePhoneme; // -1 (if none) or SSI263 or SC01 phoneme bool m_isVotraxPhoneme; + BYTE m_votraxPhoneme; UINT m_cyclesThisAudioFrame; // @@ -124,6 +159,7 @@ private: UINT m_phonemeAccurateLengthRemaining; // length in samples, decremented by cycles executed bool m_phonemePlaybackAndDebugger; bool m_phonemeCompleteByFullSpeed; + UINT m_phonemeLeadoutLength; // length in samples, decremented after \m_phonemeLengthRemaining\ goes to zero. Delay until phoneme repeats // @@ -140,7 +176,17 @@ private: BYTE m_ctrlArtAmp; BYTE m_filterFreq; - BYTE m_currentMode; // b7:6=Mode; b0=D7 pin (for IRQ) + union + { + struct + { + BYTE D7 : 1; // b0=D7 pin (for IRQ) + BYTE reserved : 4; + BYTE enableInts : 1; // b5 = enable A/!R (ie. interrupts) + BYTE function : 2; // b7:6 = function + }; + BYTE mode; + } m_currentMode; // Debug bool m_dbgFirst;