Improve SSI263 (PR #939)
New class for SSI263 (2 instances per MB/Phasor card) - support 2nd SSI263 at $Cn20 connected to 6522-A (at $Cn00) Switch from one-shot phoneme playback to ring-buffer Use a new dedicated ring-buffer (distinct from the AY8910 ring buffer) Gets rid of: SSI263Thread, the 2 events, and the 64 voices Support the 2-bit Duration b7:6 of phoneme register for both SC01 & SSI263 (to fix #929) Updated save-state Updated IRQ logging (output cycle & 6522 source)
This commit is contained in:
parent
fb6433fb7a
commit
96bbc0c435
13 changed files with 1240 additions and 811 deletions
|
@ -789,6 +789,14 @@
|
|||
RelativePath=".\source\Speech.h"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath=".\source\SSI263.cpp"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath=".\source\SSI263.h"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath=".\source\SynchronousEventManager.cpp"
|
||||
>
|
||||
|
|
|
@ -104,6 +104,7 @@
|
|||
<ClInclude Include="source\SoundCore.h" />
|
||||
<ClInclude Include="source\Speaker.h" />
|
||||
<ClInclude Include="source\Speech.h" />
|
||||
<ClInclude Include="source\SSI263.h" />
|
||||
<ClInclude Include="source\SSI263Phonemes.h" />
|
||||
<ClInclude Include="source\StdAfx.h" />
|
||||
<ClInclude Include="source\SynchronousEventManager.h" />
|
||||
|
@ -195,6 +196,7 @@
|
|||
<ClCompile Include="source\SoundCore.cpp" />
|
||||
<ClCompile Include="source\Speaker.cpp" />
|
||||
<ClCompile Include="source\Speech.cpp" />
|
||||
<ClCompile Include="source\SSI263.cpp" />
|
||||
<ClCompile Include="source\StdAfx.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug v141_xp|Win32'">Create</PrecompiledHeader>
|
||||
|
|
|
@ -220,6 +220,9 @@
|
|||
<ClCompile Include="source\Windows\Win32Frame.cpp">
|
||||
<Filter>Source Files\Windows</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="source\SSI263.cpp">
|
||||
<Filter>Source Files\Emulator</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="source\Debugger\Debugger_Disassembler.cpp">
|
||||
<Filter>Source Files\Debugger</Filter>
|
||||
</ClCompile>
|
||||
|
@ -525,6 +528,9 @@
|
|||
<ClInclude Include="source\Windows\Win32Frame.h">
|
||||
<Filter>Source Files\Windows</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="source\SSI263.h">
|
||||
<Filter>Source Files\Emulator</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="source\Debugger\Debugger_Disassembler.h">
|
||||
<Filter>Source Files\Debugger</Filter>
|
||||
</ClInclude>
|
||||
|
|
|
@ -436,7 +436,13 @@ static __forceinline void IRQ(ULONG& uExecutedCycles, BOOL& flagc, BOOL& flagn,
|
|||
UINT uExtraCycles = 0; // Needed for CYC(a) macro
|
||||
CYC(7)
|
||||
#if defined(_DEBUG) && LOG_IRQ_TAKEN_AND_RTI
|
||||
LogOutput("IRQ\n");
|
||||
std::string irq6522;
|
||||
MB_Get6522IrqDescription(irq6522);
|
||||
const char* pSrc = (g_bmIRQ & 1) ? irq6522.c_str() :
|
||||
(g_bmIRQ & 2) ? "SPEECH" :
|
||||
(g_bmIRQ & 4) ? "SSC" :
|
||||
(g_bmIRQ & 8) ? "MOUSE" : "UNKNOWN";
|
||||
LogOutput("IRQ (%08X) (%s)\n", (UINT)g_nCycleIrqStart, pSrc);
|
||||
#endif
|
||||
CheckSynchronousInterruptSources(7, uExecutedCycles);
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -2,6 +2,15 @@
|
|||
|
||||
#include "Card.h"
|
||||
|
||||
enum PHASOR_MODE {PH_Mockingboard=0, PH_UNDEF1, PH_UNDEF2, PH_UNDEF3, PH_UNDEF4, PH_Phasor/*=5*/, PH_UNDEF6, PH_EchoPlus/*=7*/};
|
||||
|
||||
// IFR & IER:
|
||||
#define IxR_SSI263 (1<<1)
|
||||
#define IxR_VOTRAX (1<<4)
|
||||
#define IxR_TIMER2 (1<<5)
|
||||
#define IxR_TIMER1 (1<<6)
|
||||
|
||||
|
||||
void MB_Initialize();
|
||||
void MB_Reinitialize();
|
||||
void MB_Destroy();
|
||||
|
@ -9,7 +18,7 @@ void MB_Reset(const bool powerCycle);
|
|||
void MB_InitializeForLoadingSnapshot(void);
|
||||
void MB_InitializeIO(LPBYTE pCxRomPeripheral, UINT uSlot4, UINT uSlot5);
|
||||
void MB_Mute();
|
||||
void MB_Demute();
|
||||
void MB_Unmute();
|
||||
#ifdef _DEBUG
|
||||
void MB_CheckCumulativeCycles(); // DEBUG
|
||||
#endif
|
||||
|
@ -21,6 +30,11 @@ SS_CARDTYPE MB_GetSoundcardType();
|
|||
bool MB_IsActive();
|
||||
DWORD MB_GetVolume();
|
||||
void MB_SetVolume(DWORD dwVolume, DWORD dwVolumeMax);
|
||||
void MB_Get6522IrqDescription(std::string& desc);
|
||||
|
||||
UINT64 MB_GetLastCumulativeCycles(void);
|
||||
void MB_UpdateIFR(BYTE nDevice, BYTE clr_mask, BYTE set_mask);
|
||||
BYTE MB_GetPCR(BYTE nDevice);
|
||||
|
||||
void MB_GetSnapshot_v1(struct SS_CARD_MOCKINGBOARD_v1* const pSS, const DWORD dwSlot); // For debugger
|
||||
std::string MB_GetSnapshotCardName(void);
|
||||
|
|
868
source/SSI263.cpp
Normal file
868
source/SSI263.cpp
Normal file
|
@ -0,0 +1,868 @@
|
|||
/*
|
||||
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
|
||||
|
||||
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: SSI263 emulation
|
||||
*
|
||||
* Author: Various
|
||||
*/
|
||||
|
||||
#include "StdAfx.h"
|
||||
|
||||
#include "Core.h"
|
||||
#include "CPU.h"
|
||||
#include "Log.h"
|
||||
#include "Memory.h"
|
||||
#include "SoundCore.h"
|
||||
#include "SSI263.h"
|
||||
#include "SSI263Phonemes.h"
|
||||
|
||||
#include "YamlHelper.h"
|
||||
|
||||
#define LOG_SSI263 0
|
||||
#define LOG_SSI263B 0 // Alternate SSI263 logging (use in conjunction with CPU.cpp's LOG_IRQ_TAKEN_AND_RTI)
|
||||
|
||||
// SSI263A registers:
|
||||
#define SSI_DURPHON 0x00
|
||||
#define SSI_INFLECT 0x01
|
||||
#define SSI_RATEINF 0x02
|
||||
#define SSI_CTTRAMP 0x03
|
||||
#define SSI_FILFREQ 0x04
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
BYTE SSI263::Read(ULONG nExecutedCycles)
|
||||
{
|
||||
// Regardless of register, just return inverted A/!R in bit7
|
||||
// . inverted "A/!R" is high for REQ (ie. Request, as phoneme nearly complete)
|
||||
|
||||
return MemReadFloatingBus(m_currentMode & 1, nExecutedCycles);
|
||||
}
|
||||
|
||||
void SSI263::Write(BYTE nReg, BYTE nValue)
|
||||
{
|
||||
#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 (m_cardMode == PH_Phasor)
|
||||
CpuIrqDeassert(IS_SPEECH);
|
||||
|
||||
m_currentMode &= ~1; // Clear SSI263's D7 pin
|
||||
|
||||
m_durationPhoneme = nValue;
|
||||
|
||||
Play(nValue & PHONEME_MASK);
|
||||
break;
|
||||
case SSI_INFLECT:
|
||||
#if LOG_SSI263
|
||||
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);
|
||||
#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);
|
||||
//
|
||||
{
|
||||
bool H2L = (m_ctrlArtAmp & CONTROL_MASK) && !(nValue & CONTROL_MASK);
|
||||
char newMode[20];
|
||||
sprintf_s(newMode, sizeof(newMode), "(new mode=%d)", m_durationPhoneme>>6);
|
||||
LogOutput("CTRL = %d->%d, ART = 0x%02X, AMP=0x%02X %s\n", m_ctrlArtAmp>>7, nValue>>7, (nValue&ARTICULATION_MASK)>>4, nValue&LITUDE_MASK, H2L?newMode:"");
|
||||
}
|
||||
#endif
|
||||
#if LOG_SSI263B
|
||||
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
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
m_ctrlArtAmp = nValue;
|
||||
|
||||
// "Setting the Control bit (CTL) to a logic one puts the device into Power Down mode..." (SSI263 datasheet)
|
||||
if (m_ctrlArtAmp & CONTROL_MASK)
|
||||
{
|
||||
// CpuIrqDeassert(IS_SPEECH);
|
||||
// m_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
|
||||
m_filterFreq = nValue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
const BYTE SSI263::m_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
|
||||
};
|
||||
|
||||
void SSI263::Votrax_Write(BYTE value)
|
||||
{
|
||||
m_isVotraxPhoneme = true;
|
||||
|
||||
// !A/R: Acknowledge receipt of phoneme data (signal goes from high to low)
|
||||
MB_UpdateIFR(m_device, IxR_VOTRAX, 0);
|
||||
|
||||
m_durationPhoneme = value; // Set reg0.DUR = I1:0 (inflection or pitch)
|
||||
Play(m_Votrax2SSI263[value & PHONEME_MASK]);
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void SSI263::Play(unsigned int nPhoneme)
|
||||
{
|
||||
if (!SSI263SingleVoice.bActive)
|
||||
{
|
||||
bool bRes = DSZeroVoiceBuffer(&SSI263SingleVoice, m_kDSBufferSize);
|
||||
LogFileOutput("SSI263::Play: DSZeroVoiceBuffer(), res=%d\n", bRes ? 1 : 0);
|
||||
if (!bRes)
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_dbgFirst)
|
||||
{
|
||||
m_dbgStartTime = g_nCumulativeCycles;
|
||||
LogOutput("1st phoneme = 0x%02X\n", nPhoneme);
|
||||
}
|
||||
|
||||
m_currentActivePhoneme = nPhoneme;
|
||||
|
||||
bool bPause = false;
|
||||
|
||||
if (nPhoneme == 1)
|
||||
nPhoneme = 2; // Missing this sample, so map to phoneme-2
|
||||
|
||||
if (nPhoneme == 0)
|
||||
bPause = true;
|
||||
else
|
||||
nPhoneme-=2; // Missing phoneme-1
|
||||
|
||||
m_phonemeLengthRemaining = g_nPhonemeInfo[nPhoneme].nLength;
|
||||
|
||||
m_phonemeAccurateLengthRemaining = m_phonemeLengthRemaining;
|
||||
m_phonemePlaybackAndDebugger = (g_nAppMode == MODE_STEPPING || g_nAppMode == MODE_DEBUG);
|
||||
m_phonemeCompleteByFullSpeed = false;
|
||||
|
||||
if (bPause)
|
||||
{
|
||||
if (!m_pPhonemeData00)
|
||||
{
|
||||
// 'pause' length is length of 1st phoneme (arbitrary choice, since don't know real length)
|
||||
m_pPhonemeData00 = new short [m_phonemeLengthRemaining];
|
||||
memset(m_pPhonemeData00, 0x00, m_phonemeLengthRemaining*sizeof(short));
|
||||
}
|
||||
|
||||
m_pPhonemeData = m_pPhonemeData00;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_pPhonemeData = (const short*) &g_nPhonemeData[g_nPhonemeInfo[nPhoneme].nOffset];
|
||||
}
|
||||
|
||||
m_currSampleSum = 0;
|
||||
m_currNumSamples = 0;
|
||||
m_currSampleMod4 = 0;
|
||||
}
|
||||
|
||||
void SSI263::Stop(void)
|
||||
{
|
||||
if (SSI263SingleVoice.lpDSBvoice && SSI263SingleVoice.bActive)
|
||||
DSVoiceStop(&SSI263SingleVoice);
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
//#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)
|
||||
return;
|
||||
|
||||
if (g_bFullSpeed) // ie. only true when IsPhonemeActive() is true
|
||||
{
|
||||
if (m_phonemeLengthRemaining)
|
||||
{
|
||||
// Willy Byte does SSI263 detection with drive motor on
|
||||
m_phonemeLengthRemaining = 0;
|
||||
if (m_dbgFirst) LogOutput("1st phoneme short-circuited by fullspeed\n");
|
||||
|
||||
if (m_phonemeAccurateLengthRemaining)
|
||||
m_phonemeCompleteByFullSpeed = true; // Let UpdateAccurateLength() call UpdateIRQ()
|
||||
else
|
||||
UpdateIRQ();
|
||||
}
|
||||
|
||||
m_updateWasFullSpeed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
const bool nowNormalSpeed = m_updateWasFullSpeed; // Just transitioned from full-speed to normal speed
|
||||
m_updateWasFullSpeed = false;
|
||||
|
||||
// NB. next call to this function: nowNormalSpeed = false
|
||||
if (nowNormalSpeed)
|
||||
m_byteOffset = (DWORD)-1; // ...which resets m_numSamplesError below
|
||||
|
||||
//-------------
|
||||
|
||||
DWORD dwCurrentPlayCursor, dwCurrentWriteCursor;
|
||||
HRESULT hr = SSI263SingleVoice.lpDSBvoice->GetCurrentPosition(&dwCurrentPlayCursor, &dwCurrentWriteCursor);
|
||||
if (FAILED(hr))
|
||||
return;
|
||||
|
||||
bool prefillBufferOnInit = false;
|
||||
|
||||
if (m_byteOffset == (DWORD)-1)
|
||||
{
|
||||
// First time in this func (or transitioned from full-speed to normal speed, or a ring-buffer reset)
|
||||
#ifdef DBG_SSI263_UPDATE
|
||||
double fTicksSecs = (double)GetTickCount() / 1000.0;
|
||||
LogOutput("%010.3f: [SSUpdtInit%1d]PC=%08X, WC=%08X, Diff=%08X, Off=%08X xxx\n", fTicksSecs, m_device, dwCurrentPlayCursor, dwCurrentWriteCursor, dwCurrentWriteCursor - dwCurrentPlayCursor, m_byteOffset);
|
||||
#endif
|
||||
m_byteOffset = dwCurrentWriteCursor;
|
||||
m_numSamplesError = 0;
|
||||
prefillBufferOnInit = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Check that our offset isn't between Play & Write positions
|
||||
|
||||
if (dwCurrentWriteCursor > dwCurrentPlayCursor)
|
||||
{
|
||||
// |-----PxxxxxW-----|
|
||||
if ((m_byteOffset > dwCurrentPlayCursor) && (m_byteOffset < dwCurrentWriteCursor))
|
||||
{
|
||||
#ifdef DBG_SSI263_UPDATE
|
||||
double fTicksSecs = (double)GetTickCount() / 1000.0;
|
||||
LogOutput("%010.3f: [SSUpdt%1d] PC=%08X, WC=%08X, Diff=%08X, Off=%08X xxx\n", fTicksSecs, m_device, dwCurrentPlayCursor, dwCurrentWriteCursor, dwCurrentWriteCursor - dwCurrentPlayCursor, m_byteOffset);
|
||||
#endif
|
||||
m_byteOffset = dwCurrentWriteCursor;
|
||||
m_numSamplesError = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// |xxW----------Pxxx|
|
||||
if ((m_byteOffset > dwCurrentPlayCursor) || (m_byteOffset < dwCurrentWriteCursor))
|
||||
{
|
||||
#ifdef DBG_SSI263_UPDATE
|
||||
double fTicksSecs = (double)GetTickCount() / 1000.0;
|
||||
LogOutput("%010.3f: [SSUpdt%1d] PC=%08X, WC=%08X, Diff=%08X, Off=%08X XXX\n", fTicksSecs, m_device, dwCurrentPlayCursor, dwCurrentWriteCursor, dwCurrentWriteCursor - dwCurrentPlayCursor, m_byteOffset);
|
||||
#endif
|
||||
m_byteOffset = dwCurrentWriteCursor;
|
||||
m_numSamplesError = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//-------------
|
||||
|
||||
const UINT kMinBytesInBuffer = m_kDSBufferSize / 4; // 25% full
|
||||
int nNumSamples = 0;
|
||||
double updateInterval = 0.0;
|
||||
|
||||
if (prefillBufferOnInit)
|
||||
{
|
||||
// Just prefill first 25% of buffer with zeros:
|
||||
// . so we have a quarter buffer of silence/lag before the real sample data begins.
|
||||
// . NB. this is fine, since it's the steady state; and it's likely that no actual data will ever occur during this initial time.
|
||||
// This means that the '1st phoneme playback time' (in cycles) will be a bit longer for subsequent times.
|
||||
|
||||
m_lastUpdateCycle = MB_GetLastCumulativeCycles();
|
||||
|
||||
nNumSamples = kMinBytesInBuffer / sizeof(short);
|
||||
memset(&m_mixBufferSSI263[0], 0, nNumSamples);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 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 (1372 samples)
|
||||
|
||||
if (m_lastUpdateCycle == 0)
|
||||
m_lastUpdateCycle = MB_GetLastCumulativeCycles(); // Initial call to SSI263_Update() after reset/power-cycle
|
||||
|
||||
_ASSERT(MB_GetLastCumulativeCycles() >= m_lastUpdateCycle);
|
||||
updateInterval = (double)(MB_GetLastCumulativeCycles() - m_lastUpdateCycle);
|
||||
if (updateInterval < kMinimumUpdateInterval)
|
||||
return;
|
||||
if (updateInterval > kMaximumUpdateInterval)
|
||||
updateInterval = kMaximumUpdateInterval;
|
||||
|
||||
m_lastUpdateCycle = MB_GetLastCumulativeCycles();
|
||||
|
||||
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
|
||||
|
||||
nNumSamples = nNumSamplesPerPeriod + m_numSamplesError; // Apply correction
|
||||
if (nNumSamples <= 0)
|
||||
nNumSamples = 0;
|
||||
if (nNumSamples > 2 * nNumSamplesPerPeriod)
|
||||
nNumSamples = 2 * nNumSamplesPerPeriod;
|
||||
|
||||
if (nNumSamples > m_kDSBufferSize)
|
||||
nNumSamples = m_kDSBufferSize; // Clamp to prevent buffer overflow
|
||||
|
||||
// if (nNumSamples)
|
||||
// { /* Generate new sample data - ie. could merge from all the SSI263 sources */ }
|
||||
|
||||
//
|
||||
|
||||
int nBytesRemaining = m_byteOffset - dwCurrentPlayCursor;
|
||||
if (nBytesRemaining < 0)
|
||||
nBytesRemaining += m_kDSBufferSize;
|
||||
|
||||
// Calc correction factor so that play-buffer doesn't under/overflow
|
||||
const int nErrorInc = SoundCore_GetErrorInc();
|
||||
if (nBytesRemaining < kMinBytesInBuffer)
|
||||
m_numSamplesError += nErrorInc; // < 0.25 of buffer remaining
|
||||
else if (nBytesRemaining > m_kDSBufferSize / 2)
|
||||
m_numSamplesError -= nErrorInc; // > 0.50 of buffer remaining
|
||||
else
|
||||
m_numSamplesError = 0; // Acceptable amount of data in buffer
|
||||
}
|
||||
|
||||
#if defined(DBG_SSI263_UPDATE)
|
||||
double fTicksSecs = (double)GetTickCount() / 1000.0;
|
||||
LogOutput("%010.3f: [SSUpdt%1d] PC=%08X, WC=%08X, Diff=%08X, Off=%08X, NS=%08X, NSE=%08X, Interval=%f\n", fTicksSecs, m_device, dwCurrentPlayCursor, dwCurrentWriteCursor, dwCurrentWriteCursor - dwCurrentPlayCursor, m_byteOffset, nNumSamples, m_numSamplesError, updateInterval);
|
||||
#endif
|
||||
|
||||
if (nNumSamples == 0)
|
||||
{
|
||||
if (m_numSamplesError)
|
||||
{
|
||||
// Reset ring-buffer if we've had a major interruption, eg. F7 (enter debugger), F8 (configure), F11/12 (save-state), Pause, etc
|
||||
// - this can cause Apple II SSI263 detection code to fail (when either timing one or a sequence of phonemes)
|
||||
// When the AppleWin code restarts and reads the ring-buffer position it'll be at a random point, and maybe nearly full (>50% full)
|
||||
// - so the code waits until it drains (nNumSamples=0 each time)
|
||||
// - but it takes a large number of calls to this func to drain to an acceptable level
|
||||
m_byteOffset = (DWORD)-1;
|
||||
#if defined(DBG_SSI263_UPDATE)
|
||||
double fTicksSecs = (double)GetTickCount() / 1000.0;
|
||||
LogOutput("%010.3f: [SSUpdt%1d] Reset ring-buffer\n", fTicksSecs, m_device);
|
||||
#endif
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
//-------------
|
||||
|
||||
bool bSpeechIRQ = false;
|
||||
|
||||
{
|
||||
const BYTE DUR = m_durationPhoneme >> DURATION_SHIFT;
|
||||
const BYTE numSamplesToAvg = (DUR <= 1) ? 1 :
|
||||
(DUR == 2) ? 2 :
|
||||
4;
|
||||
|
||||
short* pMixBuffer = &m_mixBufferSSI263[0];
|
||||
int zeroSize = nNumSamples;
|
||||
|
||||
if (m_phonemeLengthRemaining && !prefillBufferOnInit)
|
||||
{
|
||||
UINT samplesWritten = 0;
|
||||
while (samplesWritten < (UINT)nNumSamples)
|
||||
{
|
||||
m_currSampleSum += (int)*m_pPhonemeData;
|
||||
m_currNumSamples++;
|
||||
|
||||
m_pPhonemeData++;
|
||||
m_phonemeLengthRemaining--;
|
||||
|
||||
if (m_currNumSamples == numSamplesToAvg)
|
||||
{
|
||||
*pMixBuffer++ = (short)(m_currSampleSum / numSamplesToAvg);
|
||||
samplesWritten++;
|
||||
m_currSampleSum = 0;
|
||||
m_currNumSamples = 0;
|
||||
}
|
||||
|
||||
m_currSampleMod4 = (m_currSampleMod4 + 1) & 3;
|
||||
if (DUR == 1 && m_currSampleMod4 == 3 && m_phonemeLengthRemaining)
|
||||
{
|
||||
m_pPhonemeData++;
|
||||
m_phonemeLengthRemaining--;
|
||||
}
|
||||
|
||||
if (!m_phonemeLengthRemaining)
|
||||
{
|
||||
bSpeechIRQ = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
zeroSize = nNumSamples - samplesWritten;
|
||||
_ASSERT(zeroSize >= 0);
|
||||
}
|
||||
|
||||
if (zeroSize)
|
||||
memset(pMixBuffer, 0, zeroSize * sizeof(short));
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
DWORD dwDSLockedBufferSize0, dwDSLockedBufferSize1;
|
||||
short *pDSLockedBuffer0, *pDSLockedBuffer1;
|
||||
|
||||
if (!DSGetLock(SSI263SingleVoice.lpDSBvoice,
|
||||
m_byteOffset, (DWORD)nNumSamples*sizeof(short)*m_kNumChannels,
|
||||
&pDSLockedBuffer0, &dwDSLockedBufferSize0,
|
||||
&pDSLockedBuffer1, &dwDSLockedBufferSize1))
|
||||
return;
|
||||
|
||||
memcpy(pDSLockedBuffer0, &m_mixBufferSSI263[0], dwDSLockedBufferSize0);
|
||||
if(pDSLockedBuffer1)
|
||||
memcpy(pDSLockedBuffer1, &m_mixBufferSSI263[dwDSLockedBufferSize0/sizeof(short)], dwDSLockedBufferSize1);
|
||||
|
||||
// Commit sound buffer
|
||||
hr = SSI263SingleVoice.lpDSBvoice->Unlock((void*)pDSLockedBuffer0, dwDSLockedBufferSize0,
|
||||
(void*)pDSLockedBuffer1, dwDSLockedBufferSize1);
|
||||
if (FAILED(hr))
|
||||
return;
|
||||
|
||||
m_byteOffset = (m_byteOffset + (DWORD)nNumSamples*sizeof(short)*m_kNumChannels) % m_kDSBufferSize;
|
||||
|
||||
//
|
||||
|
||||
if (bSpeechIRQ)
|
||||
{
|
||||
// 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 (!m_phonemePlaybackAndDebugger /*|| m_phonemeAccurateLengthRemaining*/) // superfluous, so commented out (see above)
|
||||
UpdateIRQ();
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
// The primary way for phonemes to generate IRQ is via the ring-buffer in Update(),
|
||||
// but when single-stepping (eg. timing-sensitive SSI263 detection code), then this secondary method is used.
|
||||
void SSI263::UpdateAccurateLength(void)
|
||||
{
|
||||
if (!m_phonemeAccurateLengthRemaining)
|
||||
return;
|
||||
|
||||
if (m_lastUpdateCycle == 0)
|
||||
return;
|
||||
|
||||
double updateInterval = (double)(MB_GetLastCumulativeCycles() - m_lastUpdateCycle);
|
||||
|
||||
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 UINT numSamples = nNumSamplesPerPeriod * (DUR+1);
|
||||
if (m_phonemeAccurateLengthRemaining > numSamples)
|
||||
{
|
||||
m_phonemeAccurateLengthRemaining -= numSamples;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_phonemeAccurateLengthRemaining = 0;
|
||||
if (m_phonemePlaybackAndDebugger || m_phonemeCompleteByFullSpeed)
|
||||
UpdateIRQ();
|
||||
}
|
||||
}
|
||||
|
||||
// Called by:
|
||||
// . Update() when m_phonemeLengthRemaining -> 0
|
||||
// . UpdateAccurateLength() when m_phonemeAccurateLengthRemaining -> 0
|
||||
// . LoadSnapshot()
|
||||
void SSI263::UpdateIRQ(void)
|
||||
{
|
||||
m_phonemeLengthRemaining = m_phonemeAccurateLengthRemaining = 0; // Prevent an IRQ from the other source
|
||||
|
||||
_ASSERT(m_currentActivePhoneme != -1);
|
||||
m_currentActivePhoneme = -1;
|
||||
|
||||
if (m_dbgFirst && m_dbgStartTime)
|
||||
{
|
||||
UINT64 diff = g_nCumulativeCycles - m_dbgStartTime;
|
||||
LogOutput("1st phoneme playback time = 0x%08X cy\n", (UINT32)diff);
|
||||
m_dbgFirst = false;
|
||||
}
|
||||
|
||||
// Phoneme complete, so generate IRQ if necessary
|
||||
SetSpeechIRQ();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
// Pre: m_isVotraxPhoneme, m_cardMode, m_device
|
||||
void SSI263::SetSpeechIRQ(void)
|
||||
{
|
||||
if (!m_isVotraxPhoneme)
|
||||
{
|
||||
// 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_cardMode == PH_Mockingboard)
|
||||
{
|
||||
if ((MB_GetPCR(m_device) & 1) == 0) // CA1 Latch/Input = 0 (Negative active edge)
|
||||
MB_UpdateIFR(m_device, 0, IxR_SSI263);
|
||||
if (MB_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
|
||||
}
|
||||
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)
|
||||
{
|
||||
CpuIrqAssert(IS_SPEECH);
|
||||
}
|
||||
else
|
||||
{
|
||||
_ASSERT(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
if (m_isVotraxPhoneme && MB_GetPCR(m_device) == 0xB0)
|
||||
{
|
||||
// !A/R: Time-out of old phoneme (signal goes from low to high)
|
||||
|
||||
MB_UpdateIFR(m_device, 0, IxR_VOTRAX);
|
||||
|
||||
m_isVotraxPhoneme = false;
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
bool SSI263::DSInit(void)
|
||||
{
|
||||
//
|
||||
// Create single SSI263 voice
|
||||
//
|
||||
|
||||
HRESULT hr = DSGetSoundBuffer(&SSI263SingleVoice, DSBCAPS_CTRLVOLUME, m_kDSBufferSize, SAMPLE_RATE_SSI263, m_kNumChannels, "SSI263");
|
||||
LogFileOutput("SSI263::DSInit: DSGetSoundBuffer(), hr=0x%08X\n", hr);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
LogFileOutput("SSI263::DSInit: 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!)
|
||||
|
||||
// Volume might've been setup from value in Registry
|
||||
if (!SSI263SingleVoice.nVolume)
|
||||
SSI263SingleVoice.nVolume = DSBVOLUME_MAX;
|
||||
|
||||
hr = SSI263SingleVoice.lpDSBvoice->SetVolume(SSI263SingleVoice.nVolume);
|
||||
LogFileOutput("SSI263::DSInit: SetVolume(), hr=0x%08X\n", hr);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SSI263::DSUninit(void)
|
||||
{
|
||||
Stop();
|
||||
DSReleaseSoundBuffer(&SSI263SingleVoice);
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void SSI263::Reset(void)
|
||||
{
|
||||
Stop();
|
||||
ResetState();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void SSI263::Mute(void)
|
||||
{
|
||||
if (SSI263SingleVoice.bActive && !SSI263SingleVoice.bMute)
|
||||
{
|
||||
SSI263SingleVoice.lpDSBvoice->SetVolume(DSBVOLUME_MIN);
|
||||
SSI263SingleVoice.bMute = true;
|
||||
}
|
||||
}
|
||||
|
||||
void SSI263::Unmute(void)
|
||||
{
|
||||
if (SSI263SingleVoice.bActive && SSI263SingleVoice.bMute)
|
||||
{
|
||||
SSI263SingleVoice.lpDSBvoice->SetVolume(SSI263SingleVoice.nVolume);
|
||||
SSI263SingleVoice.bMute = false;
|
||||
}
|
||||
}
|
||||
|
||||
void SSI263::SetVolume(DWORD dwVolume, DWORD dwVolumeMax)
|
||||
{
|
||||
SSI263SingleVoice.dwUserVolume = dwVolume;
|
||||
|
||||
SSI263SingleVoice.nVolume = NewVolume(dwVolume, dwVolumeMax);
|
||||
|
||||
if (SSI263SingleVoice.bActive && !SSI263SingleVoice.bMute)
|
||||
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"
|
||||
// NB. No version - this is determined by the parent "Mockingboard C" or "Phasor" unit
|
||||
|
||||
#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_SSI263_REG_ACTIVE_PHONEME "Active Phoneme"
|
||||
|
||||
void SSI263::SaveSnapshot(YamlSaveHelper& yamlSaveHelper)
|
||||
{
|
||||
YamlSaveHelper::Label label(yamlSaveHelper, "%s:\n", SS_YAML_KEY_SSI263);
|
||||
|
||||
yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_SSI263_REG_DUR_PHON, m_durationPhoneme);
|
||||
yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_SSI263_REG_INF, m_inflection);
|
||||
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_REG_CURRENT_MODE, m_currentMode);
|
||||
yamlSaveHelper.SaveBool(SS_YAML_KEY_SSI263_REG_ACTIVE_PHONEME, IsPhonemeActive());
|
||||
}
|
||||
|
||||
void SSI263::LoadSnapshot(YamlLoadHelper& yamlLoadHelper, UINT device, PHASOR_MODE mode, UINT version)
|
||||
{
|
||||
if (!yamlLoadHelper.GetSubMap(SS_YAML_KEY_SSI263))
|
||||
throw std::string("Card: Expected key: ") + std::string(SS_YAML_KEY_SSI263);
|
||||
|
||||
m_durationPhoneme = yamlLoadHelper.LoadUint(SS_YAML_KEY_SSI263_REG_DUR_PHON);
|
||||
m_inflection = yamlLoadHelper.LoadUint(SS_YAML_KEY_SSI263_REG_INF);
|
||||
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_REG_CURRENT_MODE);
|
||||
bool activePhoneme = (version >= 7) ? yamlLoadHelper.LoadBool(SS_YAML_KEY_SSI263_REG_ACTIVE_PHONEME) : false;
|
||||
m_currentActivePhoneme = !activePhoneme ? -1 : 0x00; // Not important which phoneme, since UpdateIRQ() resets this
|
||||
|
||||
yamlLoadHelper.PopMap();
|
||||
|
||||
//
|
||||
|
||||
_ASSERT(m_device != -1);
|
||||
SetCardMode(mode);
|
||||
|
||||
if (IsPhonemeActive())
|
||||
UpdateIRQ(); // Pre: m_device, m_cardMode
|
||||
|
||||
m_lastUpdateCycle = MB_GetLastCumulativeCycles();
|
||||
}
|
143
source/SSI263.h
Normal file
143
source/SSI263.h
Normal file
|
@ -0,0 +1,143 @@
|
|||
#pragma once
|
||||
|
||||
#include "Mockingboard.h" // enum PHASOR_MODE
|
||||
|
||||
class SSI263
|
||||
{
|
||||
public:
|
||||
SSI263(void)
|
||||
{
|
||||
m_device = -1; // undefined
|
||||
m_cardMode = PH_Mockingboard;
|
||||
m_pPhonemeData00 = NULL;
|
||||
|
||||
ResetState();
|
||||
}
|
||||
~SSI263(void)
|
||||
{
|
||||
delete [] m_pPhonemeData00;
|
||||
}
|
||||
|
||||
void ResetState(void)
|
||||
{
|
||||
m_currentActivePhoneme = -1;
|
||||
m_isVotraxPhoneme = false;
|
||||
m_cyclesThisAudioFrame = 0;
|
||||
|
||||
//
|
||||
|
||||
m_lastUpdateCycle = 0;
|
||||
m_updateWasFullSpeed = false;
|
||||
|
||||
m_pPhonemeData = NULL;
|
||||
m_phonemeLengthRemaining = 0;
|
||||
m_phonemeAccurateLengthRemaining = 0;
|
||||
m_phonemePlaybackAndDebugger = false;
|
||||
m_phonemeCompleteByFullSpeed = false;
|
||||
|
||||
//
|
||||
|
||||
m_numSamplesError = 0;
|
||||
m_byteOffset = (DWORD)-1;
|
||||
m_currSampleSum = 0;
|
||||
m_currNumSamples = 0;
|
||||
m_currSampleMod4 = 0;
|
||||
|
||||
//
|
||||
|
||||
m_durationPhoneme = 0;
|
||||
m_inflection = 0;
|
||||
m_rateInflection = 0;
|
||||
m_ctrlArtAmp = 0;
|
||||
m_filterFreq = 0;
|
||||
|
||||
m_currentMode = 0;
|
||||
|
||||
//
|
||||
|
||||
m_dbgFirst = true;
|
||||
m_dbgStartTime = 0;
|
||||
}
|
||||
|
||||
bool DSInit(void);
|
||||
void DSUninit(void);
|
||||
|
||||
void Reset(void);
|
||||
bool IsPhonemeActive(void) { return m_currentActivePhoneme >= 0; }
|
||||
|
||||
BYTE Read(ULONG nExecutedCycles);
|
||||
void Write(BYTE nReg, BYTE nValue);
|
||||
|
||||
void Mute(void);
|
||||
void Unmute(void);
|
||||
void SetVolume(DWORD dwVolume, DWORD dwVolumeMax);
|
||||
|
||||
void PeriodicUpdate(UINT executedCycles);
|
||||
void Update(void);
|
||||
void SetSpeechIRQ(void);
|
||||
|
||||
void Votrax_Write(BYTE nValue);
|
||||
bool GetVotraxPhoneme(void) { return m_isVotraxPhoneme; }
|
||||
void SetVotraxPhoneme(bool value) { m_isVotraxPhoneme = value; }
|
||||
|
||||
void SetCardMode(PHASOR_MODE mode) { m_cardMode = mode; }
|
||||
void SetDevice(UINT device) { m_device = device; }
|
||||
|
||||
void SaveSnapshot(class YamlSaveHelper& yamlSaveHelper);
|
||||
void LoadSnapshot(class YamlLoadHelper& yamlLoadHelper, UINT device, PHASOR_MODE mode, UINT version);
|
||||
|
||||
private:
|
||||
void Play(unsigned int nPhoneme);
|
||||
void Stop(void);
|
||||
void UpdateIRQ(void);
|
||||
void UpdateAccurateLength(void);
|
||||
|
||||
static const BYTE m_Votrax2SSI263[/*64*/];
|
||||
|
||||
static const unsigned short m_kNumChannels = 1;
|
||||
static const DWORD m_kDSBufferSize = MAX_SAMPLES * sizeof(short) * m_kNumChannels;
|
||||
short m_mixBufferSSI263[m_kDSBufferSize / sizeof(short)];
|
||||
VOICE SSI263SingleVoice;
|
||||
|
||||
//
|
||||
|
||||
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;
|
||||
bool m_isVotraxPhoneme;
|
||||
UINT m_cyclesThisAudioFrame;
|
||||
|
||||
//
|
||||
|
||||
UINT64 m_lastUpdateCycle;
|
||||
bool m_updateWasFullSpeed;
|
||||
|
||||
const short* m_pPhonemeData;
|
||||
UINT m_phonemeLengthRemaining; // length in samples, decremented as space becomes available in the ring-buffer
|
||||
UINT m_phonemeAccurateLengthRemaining; // length in samples, decremented by cycles executed
|
||||
bool m_phonemePlaybackAndDebugger;
|
||||
bool m_phonemeCompleteByFullSpeed;
|
||||
|
||||
//
|
||||
|
||||
int m_numSamplesError;
|
||||
DWORD m_byteOffset;
|
||||
int m_currSampleSum;
|
||||
int m_currNumSamples;
|
||||
UINT m_currSampleMod4;
|
||||
|
||||
// Regs:
|
||||
BYTE m_durationPhoneme;
|
||||
BYTE m_inflection; // I10..I3
|
||||
BYTE m_rateInflection;
|
||||
BYTE m_ctrlArtAmp;
|
||||
BYTE m_filterFreq;
|
||||
|
||||
BYTE m_currentMode; // b7:6=Mode; b0=D7 pin (for IRQ)
|
||||
|
||||
// Debug
|
||||
bool m_dbgFirst;
|
||||
UINT64 m_dbgStartTime;
|
||||
};
|
|
@ -48,7 +48,7 @@ static LPDIRECTSOUND g_lpDS = NULL;
|
|||
|
||||
// Used for muting & fading:
|
||||
|
||||
static const UINT uMAX_VOICES = 66; // 64 phonemes + spkr + mockingboard
|
||||
static const UINT uMAX_VOICES = 6; // 4x SSI263 + spkr + mockingboard
|
||||
static UINT g_uNumVoices = 0;
|
||||
static VOICE* g_pVoices[uMAX_VOICES] = {NULL};
|
||||
|
||||
|
@ -167,8 +167,10 @@ bool DSGetLock(LPDIRECTSOUNDBUFFER pVoice, DWORD dwOffset, DWORD dwBytes,
|
|||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
HRESULT DSGetSoundBuffer(VOICE* pVoice, DWORD dwFlags, DWORD dwBufferSize, DWORD nSampleRate, int nChannels)
|
||||
HRESULT DSGetSoundBuffer(VOICE* pVoice, DWORD dwFlags, DWORD dwBufferSize, DWORD nSampleRate, int nChannels, const char* pszDevName)
|
||||
{
|
||||
pVoice->name = pszDevName;
|
||||
|
||||
WAVEFORMATEX wavfmt;
|
||||
DSBUFFERDESC dsbdesc;
|
||||
|
||||
|
@ -226,29 +228,41 @@ void DSReleaseSoundBuffer(VOICE* pVoice)
|
|||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
bool DSZeroVoiceBuffer(PVOICE Voice, const char* pszDevName, DWORD dwBufferSize)
|
||||
bool DSVoiceStop(PVOICE Voice)
|
||||
{
|
||||
#ifdef NO_DIRECT_X
|
||||
|
||||
return false;
|
||||
#else
|
||||
_ASSERT(Voice->lpDSBvoice);
|
||||
HRESULT hr = Voice->lpDSBvoice->Stop();
|
||||
if(FAILED(hr))
|
||||
{
|
||||
if(g_fh) fprintf(g_fh, "%s: DSStop failed (%08X)\n", Voice->name.c_str(), hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
Voice->bActive = false;
|
||||
return true;
|
||||
#endif // NO_DIRECT_X
|
||||
}
|
||||
|
||||
// Use this to Play()
|
||||
bool DSZeroVoiceBuffer(PVOICE Voice, DWORD dwBufferSize)
|
||||
{
|
||||
#ifdef NO_DIRECT_X
|
||||
return false;
|
||||
#else
|
||||
|
||||
DWORD dwDSLockedBufferSize = 0; // Size of the locked DirectSound buffer
|
||||
SHORT* pDSLockedBuffer;
|
||||
|
||||
_ASSERT(Voice->lpDSBvoice);
|
||||
HRESULT hr = Voice->lpDSBvoice->Stop();
|
||||
if(FAILED(hr))
|
||||
{
|
||||
if(g_fh) fprintf(g_fh, "%s: DSStop failed (%08X)\n",pszDevName,hr);
|
||||
if (!DSVoiceStop(Voice))
|
||||
return false;
|
||||
}
|
||||
|
||||
hr = DSGetLock(Voice->lpDSBvoice, 0, 0, &pDSLockedBuffer, &dwDSLockedBufferSize, NULL, 0);
|
||||
HRESULT hr = DSGetLock(Voice->lpDSBvoice, 0, 0, &pDSLockedBuffer, &dwDSLockedBufferSize, NULL, 0);
|
||||
if(FAILED(hr))
|
||||
{
|
||||
if(g_fh) fprintf(g_fh, "%s: DSGetLock failed (%08X)\n",pszDevName,hr);
|
||||
if(g_fh) fprintf(g_fh, "%s: DSGetLock failed (%08X)\n", Voice->name.c_str(), hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -258,24 +272,25 @@ bool DSZeroVoiceBuffer(PVOICE Voice, const char* pszDevName, DWORD dwBufferSize)
|
|||
hr = Voice->lpDSBvoice->Unlock((void*)pDSLockedBuffer, dwDSLockedBufferSize, NULL, 0);
|
||||
if(FAILED(hr))
|
||||
{
|
||||
if(g_fh) fprintf(g_fh, "%s: DSUnlock failed (%08X)\n",pszDevName,hr);
|
||||
if(g_fh) fprintf(g_fh, "%s: DSUnlock failed (%08X)\n", Voice->name.c_str(), hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
hr = Voice->lpDSBvoice->Play(0,0,DSBPLAY_LOOPING);
|
||||
if(FAILED(hr))
|
||||
{
|
||||
if(g_fh) fprintf(g_fh, "%s: DSPlay failed (%08X)\n",pszDevName,hr);
|
||||
if(g_fh) fprintf(g_fh, "%s: DSPlay failed (%08X)\n", Voice->name.c_str(), hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
Voice->bActive = true;
|
||||
return true;
|
||||
#endif // NO_DIRECT_X
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
bool DSZeroVoiceWritableBuffer(PVOICE Voice, const char* pszDevName, DWORD dwBufferSize)
|
||||
bool DSZeroVoiceWritableBuffer(PVOICE Voice, DWORD dwBufferSize)
|
||||
{
|
||||
DWORD dwDSLockedBufferSize0=0, dwDSLockedBufferSize1=0;
|
||||
SHORT *pDSLockedBuffer0, *pDSLockedBuffer1;
|
||||
|
@ -287,7 +302,7 @@ bool DSZeroVoiceWritableBuffer(PVOICE Voice, const char* pszDevName, DWORD dwBuf
|
|||
&pDSLockedBuffer1, &dwDSLockedBufferSize1);
|
||||
if(FAILED(hr))
|
||||
{
|
||||
if(g_fh) fprintf(g_fh, "%s: DSGetLock failed (%08X)\n",pszDevName,hr);
|
||||
if(g_fh) fprintf(g_fh, "%s: DSGetLock failed (%08X)\n", Voice->name.c_str(), hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -299,7 +314,7 @@ bool DSZeroVoiceWritableBuffer(PVOICE Voice, const char* pszDevName, DWORD dwBuf
|
|||
(void*)pDSLockedBuffer1, dwDSLockedBufferSize1);
|
||||
if(FAILED(hr))
|
||||
{
|
||||
if(g_fh) fprintf(g_fh, "%s: DSUnlock failed (%08X)\n",pszDevName,hr);
|
||||
if(g_fh) fprintf(g_fh, "%s: DSUnlock failed (%08X)\n", Voice->name.c_str(), hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -428,7 +443,7 @@ void SoundCore_SetFade(eFADE FadeType)
|
|||
if(g_nAppMode == MODE_DEBUG)
|
||||
return;
|
||||
|
||||
// Fade in/out for speaker, the others are demuted/muted here
|
||||
// Fade in/out for speaker, the others are unmuted/muted here
|
||||
if(FadeType != FADE_NONE)
|
||||
{
|
||||
for(UINT i=0; i<g_uNumVoices; i++)
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
//#define RIFF_SPKR
|
||||
//#define RIFF_MB
|
||||
|
||||
typedef struct
|
||||
struct VOICE
|
||||
{
|
||||
LPDIRECTSOUNDBUFFER lpDSBvoice;
|
||||
LPDIRECTSOUNDNOTIFY lpDSNotify;
|
||||
|
@ -19,18 +19,35 @@ typedef struct
|
|||
DWORD dwUserVolume; // Volume from slider on Property Sheet (0=Max)
|
||||
bool bIsSpeaker;
|
||||
bool bRecentlyActive; // (Speaker only) false after 0.2s of speaker inactivity
|
||||
} VOICE, *PVOICE;
|
||||
std::string name;
|
||||
|
||||
VOICE(void)
|
||||
{
|
||||
lpDSBvoice = NULL;
|
||||
lpDSNotify = NULL;
|
||||
bActive = false;
|
||||
bMute = false;
|
||||
nVolume = 0;
|
||||
nFadeVolume = 0;
|
||||
dwUserVolume = 0;
|
||||
bIsSpeaker = false;
|
||||
bRecentlyActive = false;
|
||||
name = "";
|
||||
}
|
||||
};
|
||||
|
||||
typedef VOICE* PVOICE;
|
||||
|
||||
bool DSGetLock(LPDIRECTSOUNDBUFFER pVoice, DWORD dwOffset, DWORD dwBytes,
|
||||
SHORT** ppDSLockedBuffer0, DWORD* pdwDSLockedBufferSize0,
|
||||
SHORT** ppDSLockedBuffer1, DWORD* pdwDSLockedBufferSize1);
|
||||
|
||||
HRESULT DSGetSoundBuffer(VOICE* pVoice, DWORD dwFlags, DWORD dwBufferSize, DWORD nSampleRate, int nChannels);
|
||||
HRESULT DSGetSoundBuffer(VOICE* pVoice, DWORD dwFlags, DWORD dwBufferSize, DWORD nSampleRate, int nChannels, const char* pszDevName);
|
||||
void DSReleaseSoundBuffer(VOICE* pVoice);
|
||||
|
||||
bool DSZeroVoiceBuffer(PVOICE Voice, const char* pszDevName, DWORD dwBufferSize);
|
||||
bool DSZeroVoiceWritableBuffer(PVOICE Voice, const char* pszDevName, DWORD dwBufferSize);
|
||||
bool DSVoiceStop(PVOICE Voice);
|
||||
bool DSZeroVoiceBuffer(PVOICE Voice, DWORD dwBufferSize);
|
||||
bool DSZeroVoiceWritableBuffer(PVOICE Voice, DWORD dwBufferSize);
|
||||
|
||||
enum eFADE {FADE_NONE, FADE_IN, FADE_OUT};
|
||||
void SoundCore_SetFade(eFADE FadeType);
|
||||
|
|
|
@ -81,7 +81,7 @@ bool g_bQuieterSpeaker = false;
|
|||
static unsigned __int64 g_nSpkrQuietCycleCount = 0;
|
||||
static unsigned __int64 g_nSpkrLastCycle = 0;
|
||||
static bool g_bSpkrToggleFlag = false;
|
||||
static VOICE SpeakerVoice = {0};
|
||||
static VOICE SpeakerVoice;
|
||||
static bool g_bSpkrAvailable = false;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
@ -287,7 +287,7 @@ void SpkrReset()
|
|||
InitRemainderBuffer();
|
||||
Spkr_SubmitWaveBuffer(NULL, 0);
|
||||
Spkr_SetActive(false);
|
||||
Spkr_Demute();
|
||||
Spkr_Unmute();
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
|
@ -673,7 +673,7 @@ static ULONG Spkr_SubmitWaveBuffer(short* pSpeakerBuffer, ULONG nNumSamples)
|
|||
|
||||
// Don't call DSZeroVoiceBuffer() - get noise with "VIA AC'97 Enhanced Audio Controller"
|
||||
// . I guess SpeakerVoice.Stop() isn't really working and the new zero buffer causes noise corruption when submitted.
|
||||
DSZeroVoiceWritableBuffer(&SpeakerVoice, "Spkr", g_dwDSSpkrBufferSize);
|
||||
DSZeroVoiceWritableBuffer(&SpeakerVoice, g_dwDSSpkrBufferSize);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
@ -813,7 +813,7 @@ void Spkr_Mute()
|
|||
}
|
||||
}
|
||||
|
||||
void Spkr_Demute()
|
||||
void Spkr_Unmute()
|
||||
{
|
||||
if(SpeakerVoice.bActive && SpeakerVoice.bMute)
|
||||
{
|
||||
|
@ -881,14 +881,14 @@ bool Spkr_DSInit()
|
|||
|
||||
SpeakerVoice.bIsSpeaker = true;
|
||||
|
||||
HRESULT hr = DSGetSoundBuffer(&SpeakerVoice, DSBCAPS_CTRLVOLUME, g_dwDSSpkrBufferSize, SPKR_SAMPLE_RATE, 1);
|
||||
HRESULT hr = DSGetSoundBuffer(&SpeakerVoice, DSBCAPS_CTRLVOLUME, g_dwDSSpkrBufferSize, SPKR_SAMPLE_RATE, 1, "Spkr");
|
||||
if(FAILED(hr))
|
||||
{
|
||||
if(g_fh) fprintf(g_fh, "Spkr: DSGetSoundBuffer failed (%08X)\n",hr);
|
||||
LogFileOutput("Spkr: DSGetSoundBuffer failed (%08X)\n", hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!DSZeroVoiceBuffer(&SpeakerVoice, "Spkr", g_dwDSSpkrBufferSize))
|
||||
if(!DSZeroVoiceBuffer(&SpeakerVoice, g_dwDSSpkrBufferSize))
|
||||
return false;
|
||||
|
||||
SpeakerVoice.bActive = true;
|
||||
|
@ -920,10 +920,7 @@ bool Spkr_DSInit()
|
|||
static void Spkr_DSUninit()
|
||||
{
|
||||
if(SpeakerVoice.lpDSBvoice && SpeakerVoice.bActive)
|
||||
{
|
||||
SpeakerVoice.lpDSBvoice->Stop();
|
||||
SpeakerVoice.bActive = false;
|
||||
}
|
||||
DSVoiceStop(&SpeakerVoice);
|
||||
|
||||
DSReleaseSoundBuffer(&SpeakerVoice);
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ void SpkrUpdate_Timer();
|
|||
DWORD SpkrGetVolume();
|
||||
void SpkrSetVolume(DWORD dwVolume, DWORD dwVolumeMax);
|
||||
void Spkr_Mute();
|
||||
void Spkr_Demute();
|
||||
void Spkr_Unmute();
|
||||
bool Spkr_IsActive();
|
||||
bool Spkr_DSInit();
|
||||
void SpkrSaveSnapshot(class YamlSaveHelper& yamlSaveHelper);
|
||||
|
|
|
@ -190,8 +190,8 @@ static void ContinueExecution(void)
|
|||
if (bWasFullSpeed)
|
||||
GetFrame().VideoRedrawScreenAfterFullSpeed(g_dwCyclesThisFrame);
|
||||
|
||||
// Don't call Spkr_Demute()
|
||||
MB_Demute();
|
||||
// Don't call Spkr_Unmute()
|
||||
MB_Unmute();
|
||||
SysClk_StartTimerUsec(nExecutionPeriodUsec);
|
||||
|
||||
// Switch to higher priority, eg. for audio (BUG #015394)
|
||||
|
|
Loading…
Add table
Reference in a new issue