Restore SSI263 continuous phoneme playback from save-state (#1372, PR #1376)

. Defer each SSI263's DSInit() until SSI263::Play() & Update() are called.
. Refactor DSInit(): rename to Init() for consistency with MockingboardCardManager class.
. m_currentActivePhoneme: never return to -1 value, instead OR with kPhonemeLeadoutFlag.
. Save-state: Mockingboard v13: deprecate SS_YAML_KEY_SSI263_ACTIVE_PHONEME.
This commit is contained in:
TomCh
2025-02-08 13:37:50 +00:00
committed by GitHub
parent 648e5cca25
commit 5bc7e495bf
4 changed files with 120 additions and 74 deletions

View File

@@ -870,15 +870,6 @@ void MockingboardCard::InitializeIO(LPBYTE pCxRomPeripheral)
if (g_bDisableDirectSound || g_bDisableDirectSoundMockingboard)
return;
#ifdef NO_DIRECT_X
#else // NO_DIRECT_X
for (UINT i = 0; i < NUM_SSI263; i++)
{
if (!m_MBSubUnit[i].ssi263.DSInit())
break;
}
#endif // NO_DIRECT_X
}
//-----------------------------------------------------------------------------
@@ -1158,7 +1149,8 @@ UINT MockingboardCard::AY8910_LoadSnapshot(YamlLoadHelper& yamlLoadHelper, BYTE
//11: Added: "Bus Driven by AY"
//12: Added: SSI263: SC01 phoneme & active
// Current Mode changed (added bit5 = enableInts)
const UINT kUNIT_VERSION = 12;
//13: Removed SS_YAML_KEY_SSI263_ACTIVE_PHONEME
const UINT kUNIT_VERSION = 13;
#define SS_YAML_KEY_MB_UNIT "Unit"
#define SS_YAML_KEY_AY_CURR_REG "AY Current Register"

View File

@@ -270,7 +270,10 @@ void MockingboardCardManager::UpdateSoundBuffer(void)
// 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(&m_mockingboardVoice, SOUNDBUFFER_SIZE); // ... and Play()
bool bRes = DSZeroVoiceBuffer(&m_mockingboardVoice, SOUNDBUFFER_SIZE); // ... and Play()
LogFileOutput("MBCardMgr: DSZeroVoiceBuffer(), res=%d\n", bRes ? 1 : 0);
if (!bRes)
return;
}
UINT numSamples = GenerateAllSoundData();
@@ -296,14 +299,13 @@ bool MockingboardCardManager::Init(void)
if (!bRes)
return false;
m_mockingboardVoice.bActive = true;
// Volume might've been setup from value in Registry
if (!m_mockingboardVoice.nVolume)
m_mockingboardVoice.nVolume = DSBVOLUME_MAX;
hr = m_mockingboardVoice.lpDSBvoice->SetVolume(m_mockingboardVoice.nVolume);
LogFileOutput("MBCardMgr: SetVolume(), hr=0x%08X\n", hr);
return true;
}

View File

@@ -215,7 +215,7 @@ void SSI263::Write(BYTE nReg, BYTE nValue)
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);
if (g_fh) fprintf(g_fh, "FFREQ = 0x%02X\n", nValue);
#endif
m_filterFreq = nValue;
break;
@@ -330,19 +330,6 @@ void SSI263::Votrax_Write(BYTE value)
void SSI263::Play(unsigned int nPhoneme)
{
if (!SSI263SingleVoice.lpDSBvoice)
{
return;
}
if (!SSI263SingleVoice.bActive)
{
bool bRes = DSZeroVoiceBuffer(&SSI263SingleVoice, m_kDSBufferByteSize);
LogFileOutput("SSI263::Play: DSZeroVoiceBuffer(), res=%d\n", bRes ? 1 : 0);
if (!bRes)
return;
}
if (m_dbgFirst)
{
m_dbgStartTime = g_nCumulativeCycles;
@@ -352,7 +339,7 @@ void SSI263::Play(unsigned int nPhoneme)
}
#if LOG_SSI263 || LOG_SSI263B || LOG_SC01
if (m_currentActivePhoneme != -1)
if (m_currentActivePhoneme != -1 && !(m_currentActivePhoneme & kPhonemeLeadoutFlag))
LogOutput("Overlapping phonemes: current=%02X, next=%02X\n", m_currentActivePhoneme&0xff, nPhoneme&0xff);
#endif
m_currentActivePhoneme = nPhoneme;
@@ -406,18 +393,38 @@ void SSI263::Stop(void)
//-----------------------------------------------------------------------------
void SSI263::PeriodicUpdate(UINT executedCycles)
{
const UINT kCyclesPerAudioFrame = 1000;
m_cyclesThisAudioFrame += executedCycles;
if (m_cyclesThisAudioFrame < kCyclesPerAudioFrame)
return;
m_cyclesThisAudioFrame %= kCyclesPerAudioFrame;
Update();
}
//-----------------------------------------------------------------------------
//#define DBG_SSI263_UPDATE // NB. This outputs for all active SSI263 ring-buffers (eg. for mb-audit this may be 2 or 4)
// Called by:
// . PeriodicUpdate()
void SSI263::Update(void)
{
UpdateAccurateLength();
if (!SSI263SingleVoice.bActive)
if (!IsPhonemeActive())
return;
if (g_bFullSpeed) // NB. if true, then it's irrespective of IsPhonemeActive()
if (!SSI263SingleVoice.lpDSBvoice || !SSI263SingleVoice.bActive)
{
if (!DSInit())
return;
}
UpdateAccurateLength();
if (g_bFullSpeed) // NB. if true, then it's irrespective of IsPhonemeActive() - see MockingboardCard::IsActiveToPreventFullSpeed()
{
if (m_phonemeLengthRemaining)
{
@@ -665,7 +672,7 @@ void SSI263::Update(void)
return;
memcpy(pDSLockedBuffer0, &m_mixBufferSSI263[0], dwDSLockedBufferSize0);
if(pDSLockedBuffer1)
if (pDSLockedBuffer1)
memcpy(pDSLockedBuffer1, &m_mixBufferSSI263[dwDSLockedBufferSize0/sizeof(short)], dwDSLockedBufferSize1);
// Commit sound buffer
@@ -694,18 +701,31 @@ void SSI263::Update(void)
}
}
if (m_phonemeLeadoutLength == 0)
RepeatPhoneme();
}
// Called by:
// . Update()
// . LoadSnapshot()
void SSI263::RepeatPhoneme(void)
{
if (m_phonemeLeadoutLength != 0)
return;
if (!m_isVotraxPhoneme)
{
if (!m_isVotraxPhoneme)
{
if ((m_ctrlArtAmp & CONTROL_MASK) == 0)
Play(m_durationPhoneme & PHONEME_MASK); // Repeat this phoneme again
}
// else // GH#1318 - remove for now, as TR v5.1 can start with repeating phoneme in debugger 'g' mode!
// {
// Play(m_Votrax2SSI263[m_votraxPhoneme]); // Votrax phoneme repeats too (tested in MAME 0.262)
// }
// _ASSERT(m_currentActivePhoneme & kPhonemeLeadoutFlag); // Remove for now, as ASSERT triggers for mb-audit v1.56 in debugger stepping ('g') mode.
if ((m_ctrlArtAmp & CONTROL_MASK) == 0)
Play(m_durationPhoneme & PHONEME_MASK); // Repeat this phoneme again
m_currentActivePhoneme &= PHONEME_MASK; // Clear kPhonemeLeadoutFlag
}
// else // GH#1318 - remove for now, as TR v5.1 can start with repeating phoneme in debugger 'g' mode!
// {
// Play(m_Votrax2SSI263[m_votraxPhoneme]); // Votrax phoneme repeats too (tested in MAME 0.262)
// }
}
//-----------------------------------------------------------------------------
@@ -750,7 +770,8 @@ void SSI263::UpdateIRQ(void)
m_phonemeLengthRemaining = m_phonemeAccurateLengthRemaining = 0; // Prevent an IRQ from the other source
_ASSERT(m_currentActivePhoneme != -1);
m_currentActivePhoneme = -1;
_ASSERT((m_currentActivePhoneme & kPhonemeLeadoutFlag) == 0);
m_currentActivePhoneme |= kPhonemeLeadoutFlag;
if (m_dbgFirst && m_dbgStartTime)
{
@@ -836,26 +857,47 @@ void SSI263::SetCardMode(PHASOR_MODE mode)
//-----------------------------------------------------------------------------
// Cf. void MockingboardCardManager::UpdateSoundBuffer(void)
bool SSI263::DSInit(void)
{
//
// Create single SSI263 voice
//
if (!SSI263SingleVoice.lpDSBvoice)
{
if (g_bDisableDirectSound || g_bDisableDirectSoundMockingboard)
return false;
if (!Init())
return false;
}
if (!SSI263SingleVoice.bActive)
{
bool bRes = DSZeroVoiceBuffer(&SSI263SingleVoice, m_kDSBufferByteSize); // ... and Play()
LogFileOutput("SSI263: DSZeroVoiceBuffer(), res=%d\n", bRes ? 1 : 0);
if (!bRes)
return false;
}
return true;
}
// Cf. bool MockingboardCardManager::Init(void)
bool SSI263::Init(void)
{
if (!DSAvailable())
return false;
HRESULT hr = DSGetSoundBuffer(&SSI263SingleVoice, m_kDSBufferByteSize, SAMPLE_RATE_SSI263, m_kNumChannels, "SSI263");
LogFileOutput("SSI263::DSInit: DSGetSoundBuffer(), hr=0x%08X\n", hr);
LogFileOutput("SSI263: DSGetSoundBuffer(), hr=0x%08X\n", hr);
if (FAILED(hr))
{
LogFileOutput("SSI263::DSInit: DSGetSoundBuffer failed (%08X)\n", hr);
LogFileOutput("SSI263: DSGetSoundBuffer failed (%08X)\n", hr);
return false;
}
// Don't DirectSoundBuffer::Play() via DSZeroVoiceBuffer() - instead wait until this SSI263 is actually first used
// . different to Speaker & Mockingboard ring buffers
// . NB. we have 2x SSI263 per MB card, and it's rare if 1 is used (and *extremely* rare if 2 are used!)
// . Not so rare, as TotalReplay (at boot) will try to detect an SSI263 (by playing a $00 phoneme).
// Volume might've been setup from value in Registry
if (!SSI263SingleVoice.nVolume)
@@ -929,20 +971,6 @@ void SSI263::SetVolume(uint32_t dwVolume, uint32_t dwVolumeMax)
SSI263SingleVoice.lpDSBvoice->SetVolume(SSI263SingleVoice.nVolume);
}
//-----------------------------------------------------------------------------
void SSI263::PeriodicUpdate(UINT executedCycles)
{
const UINT kCyclesPerAudioFrame = 1000;
m_cyclesThisAudioFrame += executedCycles;
if (m_cyclesThisAudioFrame < kCyclesPerAudioFrame)
return;
m_cyclesThisAudioFrame %= kCyclesPerAudioFrame;
Update();
}
//=============================================================================
#define SS_YAML_KEY_SSI263 "SSI263"
@@ -954,7 +982,7 @@ void SSI263::PeriodicUpdate(UINT executedCycles)
#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_CURRENT_MODE "Current Mode"
#define SS_YAML_KEY_SSI263_ACTIVE_PHONEME "Active Phoneme"
#define SS_YAML_KEY_SSI263_ACTIVE_PHONEME "Active Phoneme" // v13: deprecated
void SSI263::SaveSnapshot(YamlSaveHelper& yamlSaveHelper)
{
@@ -966,7 +994,6 @@ void SSI263::SaveSnapshot(YamlSaveHelper& yamlSaveHelper)
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.mode);
yamlSaveHelper.SaveBool(SS_YAML_KEY_SSI263_ACTIVE_PHONEME, IsPhonemeActive());
}
void SSI263::LoadSnapshot(YamlLoadHelper& yamlLoadHelper, PHASOR_MODE mode, UINT version)
@@ -980,8 +1007,11 @@ void SSI263::LoadSnapshot(YamlLoadHelper& yamlLoadHelper, PHASOR_MODE mode, UINT
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.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 >= 7 && version < 13)
yamlLoadHelper.LoadBool(SS_YAML_KEY_SSI263_ACTIVE_PHONEME); // Consume redundant data
m_currentActivePhoneme = !IsPhonemeActive() ? -1 : 0x00; // Not important which phoneme, since UpdateIRQ() resets this
if (version < 12)
{
@@ -1004,11 +1034,17 @@ 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_ctrlArtAmp & CONTROL_MASK) == 0 && m_currentMode.enableInts && m_currentMode.D7 == 1)
if (m_cardMode == PH_Phasor && IsPhonemeActive() && m_currentMode.enableInts && m_currentMode.D7 == 1)
CpuIrqAssert(IS_SPEECH);
if (IsPhonemeActive())
{
// NB. Save-state doesn't preserve the play-position within the phoneme.
// It just sets IRQ (and SSI263.D7) for "phoneme complete"; and restarts it from the beginning.
// This may cause problems for timing sensitive code (eg. mb-audit).
UpdateIRQ(); // Pre: m_device, m_cardMode
RepeatPhoneme();
}
m_lastUpdateCycle = GetLastCumulativeCycles();
}

View File

@@ -20,9 +20,13 @@ public:
void ResetState(const bool powerCycle)
{
m_currentActivePhoneme = -1;
m_isVotraxPhoneme = false;
m_votraxPhoneme = 0;
if (powerCycle)
{
m_currentActivePhoneme = -1;
m_isVotraxPhoneme = false; // SC01 has no RESET pin
m_votraxPhoneme = 0; // SC01 has no RESET pin
}
m_cyclesThisAudioFrame = 0;
//
@@ -70,14 +74,13 @@ public:
void SetDevice(UINT device) { m_device = device; }
void SetCardMode(PHASOR_MODE mode);
bool DSInit(void);
void DSUninit(void);
void Reset(const bool powerCycle, const bool isPhasorCard);
bool IsPhonemeActive(void)
{
if (!m_isVotraxPhoneme)
return (m_ctrlArtAmp & CONTROL_MASK) == 0 && m_currentActivePhoneme >= 0;
return (m_ctrlArtAmp & CONTROL_MASK) == 0; // if SSI263.CONTROL=0 then "m_currentActivePhoneme >= 0" must be true
else
return m_currentActivePhoneme >= 0;
}
@@ -106,6 +109,7 @@ private:
void Play(unsigned int nPhoneme);
void Stop(void);
void UpdateIRQ(void);
void RepeatPhoneme(void);
void UpdateAccurateLength(void);
void SetDeviceModeAndInts(void);
@@ -113,6 +117,9 @@ private:
void UpdateIFR(BYTE nDevice, BYTE clr_mask, BYTE set_mask);
BYTE GetPCR(BYTE nDevice);
bool Init(void);
bool DSInit(void);
static const BYTE m_Votrax2SSI263[/*64*/];
static const unsigned short m_kNumChannels = 1;
@@ -150,7 +157,16 @@ private:
PHASOR_MODE m_cardMode;
short* m_pPhonemeData00;
int m_currentActivePhoneme; // -1 (if none) or SSI263 or SC01 phoneme
// ctor/power-cycle: Set to -1
// Play(): Set to [$00-$3F] on a write to DURPHON register.
// UpdateIRQ(): OR'd with kPhonemeLeadoutFlag when phoneme ends.
// RepeatPhoneme(): AND'd with PHONEME_MASK, if SSI263.CONTROL==0 call Play() again.
// SSI263.CONTROL 1->0 will call Play().
// NB. Can be used to detect overlapping phonemes in Play().
// NB. For SSI263 (and SC01) once >=0 then this remains the case (even when SSI263.CONTROL=1).
static const UINT kPhonemeLeadoutFlag = 0x100;
int m_currentActivePhoneme; // -1 (if none) or SSI263/SC01 phoneme (& can be OR'd with kPhonemeLeadoutFlag)
bool m_isVotraxPhoneme;
BYTE m_votraxPhoneme;
UINT m_cyclesThisAudioFrame;