mirror of
https://github.com/AppleWin/AppleWin.git
synced 2026-10-24 23:02:25 +00:00
Add Henri's patch
This commit is contained in:
1 parent
6c579da5bd
commit
7d6cc50b65
12 files changed
+2342
-20061
No files matched your search
@@ -121,7 +121,7 @@
|
||||
<ClInclude Include="source\Speaker.h" />
|
||||
<ClInclude Include="source\Speech.h" />
|
||||
<ClInclude Include="source\SSI263.h" />
|
||||
<ClInclude Include="source\SSI263Phonemes.h" />
|
||||
<ClInclude Include="source\SSI263Synth.h" />
|
||||
<ClInclude Include="source\StdAfx.h" />
|
||||
<ClInclude Include="source\StrFormat.h" />
|
||||
<ClInclude Include="source\SynchronousEventManager.h" />
|
||||
@@ -228,6 +228,9 @@
|
||||
<ClCompile Include="source\Speaker.cpp" />
|
||||
<ClCompile Include="source\Speech.cpp" />
|
||||
<ClCompile Include="source\SSI263.cpp" />
|
||||
<ClCompile Include="source\SSI263Synth.cpp">
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="source\StdAfx.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
|
||||
|
||||
@@ -217,6 +217,9 @@
|
||||
<ClCompile Include="source\SSI263.cpp">
|
||||
<Filter>Source Files\Emulator</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="source\SSI263Synth.cpp">
|
||||
<Filter>Source Files\Emulator</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="source\Debugger\Debugger_Disassembler.cpp">
|
||||
<Filter>Source Files\Debugger</Filter>
|
||||
</ClCompile>
|
||||
@@ -465,9 +468,6 @@
|
||||
<ClInclude Include="source\Speech.h">
|
||||
<Filter>Source Files\Emulator</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="source\SSI263Phonemes.h">
|
||||
<Filter>Source Files\_Headers</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="source\StdAfx.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
@@ -564,6 +564,9 @@
|
||||
<ClInclude Include="source\SSI263.h">
|
||||
<Filter>Source Files\Emulator</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="source\SSI263Synth.h">
|
||||
<Filter>Source Files\Emulator</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="source\Debugger\Debugger_Disassembler.h">
|
||||
<Filter>Source Files\Debugger</Filter>
|
||||
</ClInclude>
|
||||
|
||||
+2
-2
@@ -39,9 +39,9 @@
|
||||
|
||||
<h4>Speech chips:</h4>
|
||||
Both the Mockingboard C and Phasor cards support up to two SSI263s and an aftermarket hardware modification for a Votrax SC-01 (the predecessor to the SSI263).<br>
|
||||
Emulation of the SSI263 is basic at the moment and only plays back phonemes at a fixed rate without any support for inflection or filters.<br>
|
||||
SSI263 speech uses a circuit model with pitch, articulation, amplitude and filter controls. It does not use recorded phonemes.<br>
|
||||
Two types of SSI263 are supported: SSI263P (original, with a reset bug) and SSI263AP (updated with the reset bug fixed).<br>
|
||||
Additionally the Votrax SC-01 is <em>simply</em> emulated by remapping SC-01 phonemes to the SSI263's.<br>
|
||||
Votrax SC-01 speech maps its phonemes to a separate SSI263 synth with a fixed voice. This is an approximation; SC-01 pitch inputs are not modelled.<br>
|
||||
|
||||
<br>
|
||||
<p>There are a number of games and demos that support Mockingboard (or Phasor), eg:</p>
|
||||
|
||||
@@ -5,6 +5,7 @@ Copyright (C) 1994-1996, Michael O'Brien
|
||||
Copyright (C) 1999-2001, Oliver Schmidt
|
||||
Copyright (C) 2002-2005, Tom Charlesworth
|
||||
Copyright (C) 2006-2007, Tom Charlesworth, Michael Pohoreski
|
||||
Copyright (C) 2026, Henri Asseily (henri@asseily.com)
|
||||
|
||||
AppleWin is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -1212,7 +1213,8 @@ UINT MockingboardCard::AY8910_LoadSnapshot(YamlLoadHelper& yamlLoadHelper, BYTE
|
||||
// Removed SS_YAML_KEY_VOTRAX_PHONEME (as this has been present in the SC01 subunit since v12!)
|
||||
//14: Added: SSI263: Type
|
||||
//15: Added: AY891x: Type
|
||||
const UINT kUNIT_VERSION = 15;
|
||||
//16: Added native SSI263 synthesis and timing state
|
||||
const UINT kUNIT_VERSION = 16;
|
||||
|
||||
#define SS_YAML_KEY_MB_UNIT "Unit"
|
||||
#define SS_YAML_KEY_AY_CURR_REG "AY Current Register"
|
||||
@@ -1262,6 +1264,10 @@ std::string MockingboardCard::GetSnapshotCardNameSDMusic()
|
||||
|
||||
void MockingboardCard::SaveSnapshot(YamlSaveHelper& yamlSaveHelper)
|
||||
{
|
||||
// Set speech IRQs before saving the 6522 interrupt flags.
|
||||
for (UINT i = 0; i < NUM_SSI263; i++)
|
||||
m_MBSubUnit[i].ssi263.Update();
|
||||
|
||||
if (QueryType() == CT_Phasor)
|
||||
return Phasor_SaveSnapshot(yamlSaveHelper);
|
||||
|
||||
|
||||
+530
-401
File diff suppressed because it is too large.
Load diff
+57
-36
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "MockingboardDefs.h"
|
||||
#include "SoundCore.h"
|
||||
#include "SSI263Synth.h"
|
||||
|
||||
class SSI263
|
||||
{
|
||||
@@ -11,14 +13,9 @@ public:
|
||||
m_device = -1; // undefined
|
||||
m_cardMode = PH_Mockingboard;
|
||||
m_hasSC01 = true; // only for m_device==0
|
||||
m_pPhonemeData00 = NULL;
|
||||
|
||||
ResetState(true);
|
||||
}
|
||||
~SSI263()
|
||||
{
|
||||
delete [] m_pPhonemeData00;
|
||||
}
|
||||
|
||||
void ResetState(const bool powerCycle)
|
||||
{
|
||||
@@ -32,24 +29,20 @@ public:
|
||||
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_phonemeLeadoutLength = 0;
|
||||
if (powerCycle || !m_isVotraxPhoneme)
|
||||
{
|
||||
m_phonemeLengthRemaining = 0;
|
||||
m_phonemeLeadoutLength = 0;
|
||||
m_currSampleMod4 = 0;
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
m_numSamplesError = 0;
|
||||
m_byteOffset = (uint32_t)-1;
|
||||
m_currSampleSum = 0;
|
||||
m_currNumSamples = 0;
|
||||
m_currSampleMod4 = 0;
|
||||
if (powerCycle || !m_isVotraxPhoneme)
|
||||
{
|
||||
m_numSamplesError = 0;
|
||||
m_byteOffset = (uint32_t)-1;
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -60,7 +53,7 @@ public:
|
||||
m_inflection = 0;
|
||||
m_rateInflection = 0;
|
||||
m_ctrlArtAmp = (powerCycle || m_type == SSI263AP) ? 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_filterFreq = powerCycle ? FILTER_FREQ_POWER_ON : 0; // Empirically observed at chip power-on (GH#1302)
|
||||
|
||||
m_currentMode.mode = 0;
|
||||
m_currentMode.function = 0; // Set at runtime when CTL=0
|
||||
@@ -71,6 +64,10 @@ public:
|
||||
|
||||
m_dbgFirst = true;
|
||||
m_dbgStartTime = 0;
|
||||
|
||||
ResetSynthesis(powerCycle);
|
||||
if (powerCycle)
|
||||
ResetVotraxSynthesis();
|
||||
}
|
||||
|
||||
void SetDevice(UINT device) { m_device = device; }
|
||||
@@ -108,11 +105,24 @@ private:
|
||||
// Also valid regardless of m_isVotraxPhoneme state
|
||||
return m_currentActivePhoneme >= 0;
|
||||
}
|
||||
void ResetSynthesis(bool resetClock = true);
|
||||
void ResetVotraxSynthesis();
|
||||
void ConfigureVotrax();
|
||||
void AdvanceSynthesis();
|
||||
void UpdateSynthesis();
|
||||
void AdvanceSpeechTiming();
|
||||
void ResetResponseTiming();
|
||||
void StartResponseTiming();
|
||||
void AdvanceResponseTiming(uint32_t ticks);
|
||||
void CommitResponse();
|
||||
UINT GetResponsePeriod() const { return (16 - (m_rateInflection >> 4)) * 256; }
|
||||
UINT GetDurationPeriod() const { return (4 - (m_durationPhoneme >> 6)) * GetResponsePeriod(); }
|
||||
void SaveSynthesis(class YamlSaveHelper& yamlSaveHelper);
|
||||
void LoadSynthesis(class YamlLoadHelper& yamlLoadHelper);
|
||||
void Play(unsigned int nPhoneme);
|
||||
void Stop();
|
||||
void UpdateIRQ();
|
||||
void RepeatPhoneme();
|
||||
void UpdateAccurateLength();
|
||||
void SetDeviceModeAndInts();
|
||||
|
||||
UINT64 GetLastCumulativeCycles();
|
||||
@@ -132,6 +142,27 @@ private:
|
||||
short m_mixBufferSSI263[m_kDSBufferByteSize / sizeof(short)];
|
||||
VOICE SSI263SingleVoice;
|
||||
|
||||
SSI263Synth m_synth;
|
||||
SSI263Synth m_votraxSynth;
|
||||
bool m_votraxSynthStarted;
|
||||
UINT64 m_synthLastCycle;
|
||||
uint32_t m_synthCpuClock;
|
||||
uint32_t m_synthCycleRemainder;
|
||||
uint32_t m_synthSamplePhase;
|
||||
uint32_t m_speechSamplePhase;
|
||||
short m_synthBuffer[MAX_SAMPLES];
|
||||
UINT m_synthSamples;
|
||||
|
||||
// Native A/R counters use effective SSI XCK, independent of audio samples.
|
||||
bool m_responseActive;
|
||||
bool m_responsePending;
|
||||
bool m_responseReloaded;
|
||||
bool m_durationReloaded;
|
||||
UINT m_responseTicksRemaining;
|
||||
UINT m_durationTicksRemaining;
|
||||
BYTE m_responsePhase;
|
||||
BYTE m_durationPhase;
|
||||
|
||||
//
|
||||
|
||||
// Duration/Phonome
|
||||
@@ -154,15 +185,14 @@ private:
|
||||
static const BYTE AMPLITUDE_MASK = 0x0F;
|
||||
static const BYTE CONTROL_MASK = 0x80;
|
||||
|
||||
// Filter frequency range
|
||||
static const BYTE FILTER_FREQ_SILENCE = 0xFF;
|
||||
// Filter register after power-on
|
||||
static const BYTE FILTER_FREQ_POWER_ON = 0xFF;
|
||||
|
||||
SSI263Type m_type;
|
||||
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;
|
||||
bool m_hasSC01;
|
||||
short* m_pPhonemeData00;
|
||||
|
||||
// ctor/power-cycle: Set to -1
|
||||
// Play(): Set to [$00-$3F] on a write to DURPHON register.
|
||||
@@ -180,22 +210,13 @@ private:
|
||||
|
||||
//
|
||||
|
||||
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;
|
||||
UINT m_phonemeLeadoutLength; // length in samples, decremented after \m_phonemeLengthRemaining\ goes to zero. Delay until phoneme repeats
|
||||
UINT m_phonemeLengthRemaining; // legacy IRQ ticks remaining
|
||||
UINT m_phonemeLeadoutLength; // legacy delay before the next IRQ period
|
||||
|
||||
//
|
||||
|
||||
int m_numSamplesError;
|
||||
uint32_t m_byteOffset;
|
||||
int m_currSampleSum;
|
||||
int m_currNumSamples;
|
||||
UINT m_currSampleMod4;
|
||||
|
||||
// Regs:
|
||||
|
||||
-19617
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,220 @@
|
||||
#pragma once
|
||||
|
||||
/*
|
||||
AppleWin : An Apple //e emulator for Windows
|
||||
|
||||
Copyright (C) 2026, Henri Asseily (henri@asseily.com)
|
||||
|
||||
This program 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.
|
||||
|
||||
This program 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 this program; if not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Portable SSI263 sound state and interface.
|
||||
Adapted from the Appletini ONE project at https://github.com/hasseily/appletini-one
|
||||
*/
|
||||
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
// SSI-263 speech from its parameter ROM and an ideal SC-02 prototype circuit.
|
||||
// Pitch glide, cold state and source levels remain model approximations.
|
||||
// The card owns bus timing and interrupts; this class only produces audio.
|
||||
class SSI263Synth
|
||||
{
|
||||
public:
|
||||
static const uint32_t kSampleRate = 48000;
|
||||
|
||||
explicit SSI263Synth(uint32_t clockHz = 1015625);
|
||||
void Reset(uint32_t clockHz);
|
||||
void SetClock(uint32_t clockHz);
|
||||
// Restore a saved mode, or relatch it on SSI263P reset, without a CTL edge.
|
||||
void SetFunction(uint8_t function);
|
||||
void Write(uint8_t reg, uint8_t value);
|
||||
void Advance(uint32_t ticks);
|
||||
|
||||
// Call once per 48 kHz output sample, after advancing to its clock time.
|
||||
int16_t GetSample();
|
||||
uint32_t GetClockHz() const { return m_clockHz; }
|
||||
|
||||
// Versioned words, independent of struct layout. A failed load changes nothing.
|
||||
std::vector<uint32_t> SaveState() const;
|
||||
bool LoadState(const std::vector<uint32_t>& words);
|
||||
|
||||
private:
|
||||
struct Ramp
|
||||
{
|
||||
int m_value = 0;
|
||||
int m_step = 0;
|
||||
int m_fraction = 8;
|
||||
int m_target = 0;
|
||||
bool m_upward = true;
|
||||
void Retarget(int value);
|
||||
void Step();
|
||||
};
|
||||
|
||||
struct Control
|
||||
{
|
||||
Control();
|
||||
void Write(int reg, int value);
|
||||
void Tick(int amplitudeZero);
|
||||
void RestartDuration();
|
||||
void WriteParameter();
|
||||
void LatchParameter(bool rising, int amplitudeZero);
|
||||
int Target(int selector) const;
|
||||
bool PermitTransition(int selector) const;
|
||||
int ArticulationPeriod() const;
|
||||
int DurationPeriod() const;
|
||||
int AmplitudePeriod() const;
|
||||
|
||||
std::array<int, 5> m_regs = {{0xC0, 0, 0, 0x80, 0xFF}};
|
||||
std::array<Ramp, 8> m_ramps;
|
||||
// F1, F2, F2Q, shared F3/F4, filter gain, voice, noise, unused.
|
||||
std::array<int, 8> m_codes = {{0}};
|
||||
int m_selector = 0;
|
||||
int m_scanPhase = 0;
|
||||
int m_durationPhase = 0;
|
||||
int m_durationLeft = 0;
|
||||
int m_articulationLeft = 0;
|
||||
int m_amplitudeLeft = 0;
|
||||
bool m_active = false;
|
||||
bool m_phoneValid = false;
|
||||
bool m_phoneSetupPending = false;
|
||||
bool m_phoneSetupWindow = false;
|
||||
bool m_controlSetupPending = false;
|
||||
bool m_controlSetupWindow = false;
|
||||
bool m_articulationPending = false;
|
||||
bool m_articulationWindow = false;
|
||||
bool m_amplitudePending = false;
|
||||
bool m_amplitudeWindow = false;
|
||||
bool m_durationPending = false;
|
||||
bool m_durationWindow = false;
|
||||
// -1 denotes a latch whose cold state is unknown.
|
||||
int m_pw0 = -1;
|
||||
int m_pw1 = -1;
|
||||
int m_pw2 = -1;
|
||||
int m_pw3 = -1;
|
||||
int m_pw5 = -1;
|
||||
int m_route = -1;
|
||||
int m_fric1 = -1;
|
||||
int m_fric2 = -1;
|
||||
int m_filterLeft = 1;
|
||||
bool m_filterPhase = false;
|
||||
bool m_filterEdge = false;
|
||||
};
|
||||
|
||||
struct Codes
|
||||
{
|
||||
int m_f1 = 0;
|
||||
int m_f2 = 0;
|
||||
int m_f2q = 0;
|
||||
int m_f3 = 0;
|
||||
int m_f4 = 0;
|
||||
int m_filterAmp = 0;
|
||||
int m_voiceAmp = 0;
|
||||
int m_fricAmp = 0;
|
||||
bool Equals(const Codes& other) const;
|
||||
};
|
||||
|
||||
struct FilterEvent
|
||||
{
|
||||
bool m_phase = false; // false = Phi0; true = Phi1.
|
||||
bool m_phaseEdge = false;
|
||||
Codes m_codes;
|
||||
int m_voiceDrive = 0;
|
||||
int m_fricDrive = 0;
|
||||
bool m_fric1 = false;
|
||||
bool m_fric2 = false;
|
||||
bool m_outputOpen = true;
|
||||
};
|
||||
|
||||
struct Source
|
||||
{
|
||||
Source();
|
||||
void Tick(const Control& control, int inflection);
|
||||
void Settle(const Control& control);
|
||||
bool ResetVoice(const Control& control) const;
|
||||
int AmplitudeZero() const { return (m_amplitude & 14) == 0 ? 1 : 0; }
|
||||
void ShiftNoise();
|
||||
|
||||
FilterEvent m_output;
|
||||
int m_noise1 = 1;
|
||||
int m_noise2 = 0;
|
||||
int m_noise3 = 0;
|
||||
int m_noise4 = 0;
|
||||
int m_noiseCount = 15;
|
||||
int m_voiceLeft = 16384;
|
||||
int m_voiceCount = 15;
|
||||
int m_amplitude = 0;
|
||||
bool m_voiceToggle = false;
|
||||
bool m_envelopeClock = false;
|
||||
bool m_noiseClock = false;
|
||||
bool m_pitchSync = false;
|
||||
bool m_loadPending = false;
|
||||
};
|
||||
|
||||
struct Formant
|
||||
{
|
||||
int m_output = 0;
|
||||
int m_history = 0;
|
||||
int m_fixedPlate = 0;
|
||||
std::array<int, 4> m_plates = {{0}};
|
||||
};
|
||||
|
||||
struct Tract
|
||||
{
|
||||
// Voltages use signed 24-bit Q16; capacitor values are in pF.
|
||||
void Process(const FilterEvent& event);
|
||||
int16_t GetSample() const;
|
||||
static int Saturate(int64_t value);
|
||||
static int Divide(int64_t numerator, int denominator);
|
||||
static int CapSum(int mask, const std::array<int, 4>& caps);
|
||||
static int64_t Charge(int mask, int target, const std::array<int, 4>& plates,
|
||||
const std::array<int, 4>& caps);
|
||||
static int64_t Weighted(int mask, const std::array<int, 4>& plates,
|
||||
const std::array<int, 4>& caps);
|
||||
static void SetPlates(std::array<int, 4>& plates, int mask, int target);
|
||||
void MoveFormant(int index, int mask, int fixedCap, const std::array<int, 4>& caps,
|
||||
int denominator, int64_t extraCharge = 0);
|
||||
|
||||
int m_voice = 0;
|
||||
int m_fric1 = 0;
|
||||
int m_fric2Source = 0;
|
||||
int m_fric2Shape = 0;
|
||||
std::array<int, 4> m_voicePlates = {{0}};
|
||||
std::array<int, 4> m_fric1Plates = {{0}};
|
||||
std::array<int, 4> m_f2qPlates = {{0}};
|
||||
std::array<int, 4> m_filterPlates = {{0}};
|
||||
std::array<Formant, 5> m_formants;
|
||||
int m_c143Plate = 0;
|
||||
int m_c151Plate = 0;
|
||||
int m_c150Delta = 0;
|
||||
int m_c151Delta = 0;
|
||||
int m_output = 0;
|
||||
int m_reconstruction = 0;
|
||||
FilterEvent m_applied;
|
||||
bool m_hasExcitation = false;
|
||||
};
|
||||
|
||||
int LiveInflection() const;
|
||||
void StartPitch();
|
||||
void AdvancePitch();
|
||||
template<class State> void VisitState(State& state);
|
||||
|
||||
uint32_t m_clockHz;
|
||||
Control m_control;
|
||||
Source m_source;
|
||||
Tract m_tract;
|
||||
int m_function = 0;
|
||||
int m_inflection = 0;
|
||||
int m_pitchCadence = 0;
|
||||
bool m_pitchSeeded = false;
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
# SSI263 synthesis tests
|
||||
|
||||
Build from the repository root with a C++14 compiler:
|
||||
|
||||
```sh
|
||||
g++ -std=c++14 -O3 -Wall -Wextra -Werror -DSSI263_SYNTH_TEST source/SSI263Synth.cpp test/SSI263Synth/TestSSI263Synth.cpp -o TestSSI263Synth
|
||||
./TestSSI263Synth
|
||||
```
|
||||
|
||||
Or use a Visual Studio developer command prompt:
|
||||
|
||||
```bat
|
||||
cl /nologo /std:c++14 /O2 /EHsc /W4 /WX /DSSI263_SYNTH_TEST source\SSI263Synth.cpp test\SSI263Synth\TestSSI263Synth.cpp /Fe:TestSSI263Synth.exe
|
||||
TestSSI263Synth.exe
|
||||
```
|
||||
|
||||
The tests cover cold reset, register aliases, independent chips, split clock
|
||||
advances, exact snapshot continuation, invalid snapshots, and all phonemes
|
||||
with extreme register settings. Function restore preserves running synthesis
|
||||
state and selects immediate or transitioned pitch. A cold snapshot also checks
|
||||
the first speech samples after one second of silence. The tests use no Windows
|
||||
or audio device APIs.
|
||||
|
||||
`RenderTrace.cpp` can compare PCM with another implementation. Build it with
|
||||
the same command, replacing `TestSSI263Synth.cpp` and the output name with
|
||||
`RenderTrace.cpp` and `RenderTrace`. Run `RenderTrace events.txt output.pcm`.
|
||||
The output is signed 16-bit little-endian stereo PCM at 48 kHz, with socket 0
|
||||
on the left and socket 1 on the right.
|
||||
|
||||
The trace is plain text. Its first line is:
|
||||
|
||||
```text
|
||||
SSIHOST1 clock_hz start_tick end_tick event_count
|
||||
```
|
||||
|
||||
Each following line contains `tick socket register value`, in time order.
|
||||
All numbers are decimal. Clock ticks use the effective SSI clock, after any
|
||||
board clock divider. Negative ticks allow setup before the first saved sample.
|
||||
Writes at a sample boundary take effect before that sample. Earlier samples
|
||||
still run when `start_tick` selects a later range.
|
||||
|
||||
These tests check the digital model. They do not establish agreement with a
|
||||
physical SSI263, nor test the AppleWin audio mixer or interrupt timing.
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
AppleWin : An Apple //e emulator for Windows
|
||||
|
||||
Copyright (C) 2026, Henri Asseily (henri@asseily.com)
|
||||
|
||||
This program 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.
|
||||
|
||||
This program 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 this program; if not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Render timed SSI263 register writes to stereo PCM.
|
||||
*/
|
||||
|
||||
#include "../../source/SSI263Synth.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct RegisterEvent
|
||||
{
|
||||
int64_t tick;
|
||||
uint32_t socket;
|
||||
uint32_t reg;
|
||||
uint32_t value;
|
||||
};
|
||||
|
||||
static int64_t FloorDivide(int64_t numerator, int64_t denominator)
|
||||
{
|
||||
return numerator / denominator - (numerator % denominator < 0);
|
||||
}
|
||||
|
||||
static int64_t CeilDivide(int64_t numerator, int64_t denominator)
|
||||
{
|
||||
return -FloorDivide(-numerator, denominator);
|
||||
}
|
||||
|
||||
static void WriteSample(std::ostream& output, int16_t sample)
|
||||
{
|
||||
const uint16_t value = static_cast<uint16_t>(sample);
|
||||
output.put(static_cast<char>(value & 255));
|
||||
output.put(static_cast<char>(value >> 8));
|
||||
}
|
||||
|
||||
static void RenderTrace(const char* inputPath, const char* outputPath)
|
||||
{
|
||||
std::ifstream input(inputPath);
|
||||
std::string magic;
|
||||
uint32_t clockHz;
|
||||
int64_t startTick, endTick;
|
||||
size_t eventCount;
|
||||
input >> magic >> clockHz >> startTick >> endTick >> eventCount;
|
||||
if (!input || magic != "SSIHOST1" || clockHz < 100000 || clockHz > 4000000 ||
|
||||
startTick < 0 || endTick <= startTick || endTick > int64_t(clockHz) * 3600 || eventCount > 10000000)
|
||||
throw std::runtime_error("invalid trace header");
|
||||
std::vector<RegisterEvent> events(eventCount);
|
||||
for (size_t index = 0; index < events.size(); index++)
|
||||
{
|
||||
RegisterEvent& event = events[index];
|
||||
input >> event.tick >> event.socket >> event.reg >> event.value;
|
||||
if (!input || event.tick < -int64_t(clockHz) || event.tick >= endTick ||
|
||||
event.socket > 1 || event.reg > 7 || event.value > 255 || (index && event.tick < events[index - 1].tick))
|
||||
throw std::runtime_error("invalid or unsorted register event");
|
||||
}
|
||||
std::string trailing;
|
||||
if (input >> trailing)
|
||||
throw std::runtime_error("unexpected trailing trace data");
|
||||
|
||||
SSI263Synth chips[2] = {SSI263Synth(clockHz), SSI263Synth(clockHz)};
|
||||
const int64_t firstTick = events.empty() ? 0 : std::min<int64_t>(0, events.front().tick);
|
||||
const int64_t firstFrame = FloorDivide(firstTick * SSI263Synth::kSampleRate, clockHz);
|
||||
const int64_t startFrame = CeilDivide(startTick * SSI263Synth::kSampleRate, clockHz);
|
||||
const int64_t endFrame = CeilDivide(endTick * SSI263Synth::kSampleRate, clockHz);
|
||||
int64_t currentTick = CeilDivide(firstFrame * clockHz, SSI263Synth::kSampleRate);
|
||||
size_t nextEvent = 0;
|
||||
std::ofstream output(outputPath, std::ios::binary);
|
||||
if (!output)
|
||||
throw std::runtime_error("cannot open PCM output");
|
||||
const auto Advance = [&](int64_t tick)
|
||||
{
|
||||
int64_t remaining = tick - currentTick;
|
||||
while (remaining > 0)
|
||||
{
|
||||
const uint32_t ticks = static_cast<uint32_t>(std::min<int64_t>(remaining, 0xffffffff));
|
||||
for (SSI263Synth& chip : chips)
|
||||
chip.Advance(ticks);
|
||||
remaining -= ticks;
|
||||
}
|
||||
currentTick = tick;
|
||||
};
|
||||
for (int64_t frame = firstFrame; frame < endFrame; frame++)
|
||||
{
|
||||
const int64_t tick = CeilDivide(frame * clockHz, SSI263Synth::kSampleRate);
|
||||
while (nextEvent < events.size() && events[nextEvent].tick <= tick)
|
||||
{
|
||||
const RegisterEvent& event = events[nextEvent++];
|
||||
Advance(event.tick);
|
||||
chips[event.socket].Write(static_cast<uint8_t>(event.reg), static_cast<uint8_t>(event.value));
|
||||
}
|
||||
Advance(tick);
|
||||
for (SSI263Synth& chip : chips)
|
||||
{
|
||||
const int16_t sample = chip.GetSample();
|
||||
if (frame >= startFrame)
|
||||
WriteSample(output, sample);
|
||||
}
|
||||
}
|
||||
output.close();
|
||||
if (!output)
|
||||
throw std::runtime_error("PCM write failed");
|
||||
std::cout << "{\"frames\":" << endFrame - startFrame << "}\n";
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (argc != 3)
|
||||
throw std::runtime_error("usage: RenderTrace EVENTS OUTPUT.pcm");
|
||||
RenderTrace(argv[1], argv[2]);
|
||||
return 0;
|
||||
}
|
||||
catch (const std::exception& error)
|
||||
{
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
/*
|
||||
AppleWin : An Apple //e emulator for Windows
|
||||
|
||||
Copyright (C) 2026, Henri Asseily (henri@asseily.com)
|
||||
|
||||
This program 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.
|
||||
|
||||
This program 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 this program; if not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
SSI263 synthesis and state regression tests.
|
||||
*/
|
||||
|
||||
#include "../../source/SSI263Synth.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
static const uint32_t kClockHz = 1015625;
|
||||
|
||||
static void Check(bool condition, const char* message)
|
||||
{
|
||||
if (!condition)
|
||||
throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
static void StartVoice(SSI263Synth& synth, uint8_t function = 2)
|
||||
{
|
||||
synth.Write(3, 0x80);
|
||||
synth.Write(0, static_cast<uint8_t>(function << 6));
|
||||
synth.Write(1, 0x6f);
|
||||
synth.Write(2, 0x8e);
|
||||
synth.Write(4, 128);
|
||||
synth.Write(3, 0x5c);
|
||||
synth.Write(0, 0x0e);
|
||||
}
|
||||
|
||||
static std::vector<int16_t> Render(SSI263Synth& synth, uint32_t frames)
|
||||
{
|
||||
std::vector<int16_t> samples;
|
||||
uint64_t previousTick = 0;
|
||||
for (uint32_t frame = 0; frame < frames; frame++)
|
||||
{
|
||||
const uint64_t tick = (uint64_t(frame) * synth.GetClockHz() + SSI263Synth::kSampleRate - 1) / SSI263Synth::kSampleRate;
|
||||
synth.Advance(static_cast<uint32_t>(tick - previousTick));
|
||||
previousTick = tick;
|
||||
samples.push_back(synth.GetSample());
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
|
||||
static void TestReset()
|
||||
{
|
||||
SSI263Synth synth(kClockHz);
|
||||
const std::vector<uint32_t> coldState = synth.SaveState();
|
||||
StartVoice(synth);
|
||||
const std::vector<int16_t> expected = Render(synth, 4800);
|
||||
Check(std::any_of(expected.begin(), expected.end(), [](int16_t value) { return value != 0; }), "voice stayed silent");
|
||||
// FNV-1a over little-endian PCM from the accepted native model, 7 October 2026.
|
||||
uint64_t hash = 14695981039346656037ULL;
|
||||
for (int16_t sample : expected)
|
||||
{
|
||||
const uint16_t value = static_cast<uint16_t>(sample);
|
||||
hash = (hash ^ (value & 255)) * 1099511628211ULL;
|
||||
hash = (hash ^ (value >> 8)) * 1099511628211ULL;
|
||||
}
|
||||
Check(hash == 0xa2a114dcd3b1d24cULL, "voice PCM differs from the reference");
|
||||
synth.Reset(kClockHz);
|
||||
Check(synth.SaveState() == coldState, "reset did not restore cold state");
|
||||
StartVoice(synth);
|
||||
Check(Render(synth, 4800) == expected, "reset changed the PCM sequence");
|
||||
synth.Reset(1022727);
|
||||
Check(synth.GetClockHz() == 1022727, "reset did not select the new clock");
|
||||
}
|
||||
|
||||
static void TestRegisterAliases()
|
||||
{
|
||||
for (uint8_t reg = 0; reg < 8; reg++)
|
||||
{
|
||||
SSI263Synth reference(kClockHz);
|
||||
SSI263Synth alias(kClockHz);
|
||||
StartVoice(reference);
|
||||
StartVoice(alias);
|
||||
const uint8_t decoded = reg < 4 ? reg : 4;
|
||||
for (uint32_t value = 0; value < 256; value++)
|
||||
{
|
||||
reference.Write(decoded, static_cast<uint8_t>(value));
|
||||
alias.Write(reg, static_cast<uint8_t>(value));
|
||||
reference.Advance(37);
|
||||
alias.Advance(37);
|
||||
Check(reference.GetSample() == alias.GetSample(), "register alias changed PCM");
|
||||
Check(reference.SaveState() == alias.SaveState(), "register alias changed state");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void TestAdvanceBatching()
|
||||
{
|
||||
SSI263Synth batched(kClockHz);
|
||||
SSI263Synth singleTick(kClockHz);
|
||||
StartVoice(batched, 3);
|
||||
StartVoice(singleTick, 3);
|
||||
for (uint32_t frame = 0; frame < 4800; frame++)
|
||||
{
|
||||
if (frame % 137 == 0)
|
||||
{
|
||||
batched.Write(1, static_cast<uint8_t>(frame));
|
||||
singleTick.Write(1, static_cast<uint8_t>(frame));
|
||||
}
|
||||
const uint32_t ticks = 1 + frame % 43;
|
||||
batched.Advance(ticks);
|
||||
for (uint32_t tick = 0; tick < ticks; tick++)
|
||||
singleTick.Advance(1);
|
||||
Check(batched.GetSample() == singleTick.GetSample(), "Advance batching changed PCM");
|
||||
}
|
||||
Check(batched.SaveState() == singleTick.SaveState(), "Advance batching changed state");
|
||||
const std::vector<uint32_t> before = batched.SaveState();
|
||||
batched.Advance(0);
|
||||
Check(batched.SaveState() == before, "zero ticks changed state");
|
||||
}
|
||||
|
||||
static void TestChipIndependence()
|
||||
{
|
||||
SSI263Synth reference(kClockHz);
|
||||
SSI263Synth first(kClockHz);
|
||||
SSI263Synth second(1022727);
|
||||
StartVoice(reference);
|
||||
StartVoice(first);
|
||||
StartVoice(second, 3);
|
||||
for (uint32_t frame = 0; frame < 4800; frame++)
|
||||
{
|
||||
reference.Advance(21);
|
||||
first.Advance(21);
|
||||
second.Write(static_cast<uint8_t>(frame & 7), static_cast<uint8_t>(frame * 17));
|
||||
second.Advance(53);
|
||||
second.GetSample();
|
||||
Check(first.GetSample() == reference.GetSample(), "another chip changed PCM");
|
||||
}
|
||||
Check(first.SaveState() == reference.SaveState(), "another chip changed state");
|
||||
}
|
||||
|
||||
static void TestSnapshot()
|
||||
{
|
||||
SSI263Synth original(kClockHz);
|
||||
StartVoice(original, 3);
|
||||
Render(original, 4799);
|
||||
original.Write(1, 0xe5);
|
||||
original.Write(0, 0x2c);
|
||||
original.Advance(7);
|
||||
const std::vector<uint32_t> snapshot = original.SaveState();
|
||||
SSI263Synth restored(1022727);
|
||||
Check(restored.LoadState(snapshot), "valid snapshot was rejected");
|
||||
Check(restored.SaveState() == snapshot, "snapshot did not restore all state");
|
||||
Check(Render(original, 9600) == Render(restored, 9600), "snapshot continuation changed PCM");
|
||||
Check(original.SaveState() == restored.SaveState(), "snapshot continuation changed state");
|
||||
|
||||
const std::vector<uint32_t> before = restored.SaveState();
|
||||
std::vector<uint32_t> invalid = snapshot;
|
||||
invalid[0] = 0xffffffff;
|
||||
Check(!restored.LoadState(invalid), "unknown snapshot version was accepted");
|
||||
Check(restored.SaveState() == before, "bad snapshot version changed state");
|
||||
invalid = snapshot;
|
||||
invalid.pop_back();
|
||||
Check(!restored.LoadState(invalid), "truncated snapshot was accepted");
|
||||
Check(restored.SaveState() == before, "truncated snapshot changed state");
|
||||
invalid = snapshot;
|
||||
invalid.push_back(0);
|
||||
Check(!restored.LoadState(invalid), "oversized snapshot was accepted");
|
||||
Check(restored.SaveState() == before, "oversized snapshot changed state");
|
||||
invalid.clear();
|
||||
Check(!restored.LoadState(invalid), "empty snapshot was accepted");
|
||||
Check(restored.SaveState() == before, "empty snapshot changed state");
|
||||
invalid = snapshot;
|
||||
Check(invalid.back() == 1, "speech did not record prior filter excitation");
|
||||
invalid.back() = 0;
|
||||
Check(!restored.LoadState(invalid), "charged filters accepted an unexcited snapshot flag");
|
||||
Check(restored.SaveState() == before, "invalid filter excitation flag changed state");
|
||||
for (size_t index = 0; index < snapshot.size(); index++)
|
||||
{
|
||||
invalid = snapshot;
|
||||
invalid[index] = 0x80000000;
|
||||
Check(!restored.LoadState(invalid), "out-of-range snapshot field was accepted");
|
||||
Check(restored.SaveState() == before, "out-of-range snapshot field changed state");
|
||||
}
|
||||
|
||||
restored.SetClock(1022727);
|
||||
Check(restored.GetClockHz() == 1022727, "clock change was ignored");
|
||||
restored.SetClock(kClockHz);
|
||||
Check(restored.SaveState() == before, "clock change reset running synthesis state");
|
||||
}
|
||||
|
||||
static void TestRegisterExtremes()
|
||||
{
|
||||
const uint8_t limits[] = {0, 1, 127, 128, 254, 255};
|
||||
for (uint8_t limit : limits)
|
||||
{
|
||||
SSI263Synth synth(kClockHz);
|
||||
StartVoice(synth);
|
||||
synth.Write(1, limit);
|
||||
synth.Write(2, limit);
|
||||
synth.Write(4, limit);
|
||||
for (uint8_t phone = 0; phone < 64; phone++)
|
||||
{
|
||||
synth.Write(0, phone);
|
||||
synth.Write(3, static_cast<uint8_t>(limit & 0x7f));
|
||||
const std::vector<uint32_t> before = synth.SaveState();
|
||||
const std::vector<int16_t> expected = Render(synth, 240);
|
||||
const std::vector<uint32_t> after = synth.SaveState();
|
||||
Check(synth.LoadState(before), "extreme register state was rejected");
|
||||
Check(Render(synth, 240) == expected, "extreme register continuation changed PCM");
|
||||
Check(synth.SaveState() == after, "extreme register continuation changed state");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void TestFunctionRestore()
|
||||
{
|
||||
SSI263Synth synth(kClockHz);
|
||||
StartVoice(synth, 3);
|
||||
Render(synth, 4800);
|
||||
const std::vector<uint32_t> before = synth.SaveState();
|
||||
for (uint8_t function = 0; function < 4; function++)
|
||||
{
|
||||
synth.SetFunction(function);
|
||||
std::vector<uint32_t> expected = before;
|
||||
// Version 1 stores the latched function after the version and clock.
|
||||
expected[2] = function;
|
||||
Check(synth.SaveState() == expected, "function restore changed running synthesis state");
|
||||
}
|
||||
const uint8_t invalidFunctions[] = {4, 255};
|
||||
for (uint8_t function : invalidFunctions)
|
||||
{
|
||||
bool rejected = false;
|
||||
try
|
||||
{
|
||||
synth.SetFunction(function);
|
||||
}
|
||||
catch (const std::invalid_argument&)
|
||||
{
|
||||
rejected = true;
|
||||
}
|
||||
Check(rejected, "invalid function was accepted");
|
||||
Check(synth.SaveState() == before, "invalid function changed state");
|
||||
}
|
||||
|
||||
SSI263Synth immediate(kClockHz);
|
||||
SSI263Synth transitioned(kClockHz);
|
||||
Check(immediate.LoadState(before) && transitioned.LoadState(before), "function test snapshot was rejected");
|
||||
immediate.SetFunction(2);
|
||||
transitioned.SetFunction(3);
|
||||
bool differentPitch = false;
|
||||
for (uint32_t frame = 0; frame < 4800; frame++)
|
||||
{
|
||||
// Keep moving the target so the next divider reload sees an active glide.
|
||||
if (frame % 11 == 0)
|
||||
{
|
||||
const uint8_t inflection = (frame / 11) & 1 ? 255 : 0;
|
||||
immediate.Write(1, inflection);
|
||||
transitioned.Write(1, inflection);
|
||||
}
|
||||
immediate.Advance(21);
|
||||
transitioned.Advance(21);
|
||||
if (immediate.GetSample() != transitioned.GetSample())
|
||||
differentPitch = true;
|
||||
}
|
||||
Check(differentPitch, "restored function did not select pitch glide");
|
||||
}
|
||||
|
||||
static void TestColdSnapshot()
|
||||
{
|
||||
SSI263Synth cold(kClockHz);
|
||||
const std::vector<int16_t> silence = Render(cold, SSI263Synth::kSampleRate);
|
||||
Check(std::all_of(silence.begin(), silence.end(), [](int16_t value) { return value == 0; }), "cold chip produced sound");
|
||||
Check(cold.SaveState().back() == 0, "cold chip recorded filter excitation");
|
||||
SSI263Synth restored(kClockHz);
|
||||
Check(restored.LoadState(cold.SaveState()), "cold snapshot was rejected");
|
||||
StartVoice(cold);
|
||||
StartVoice(restored);
|
||||
const std::vector<int16_t> expected = Render(cold, 4800);
|
||||
Check(std::any_of(expected.begin(), expected.end(), [](int16_t value) { return value != 0; }), "cold chip did not start speech");
|
||||
Check(Render(restored, 4800) == expected, "cold snapshot changed the first speech samples");
|
||||
Check(restored.SaveState() == cold.SaveState(), "cold snapshot changed the first speech state");
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
try
|
||||
{
|
||||
TestReset();
|
||||
TestRegisterAliases();
|
||||
TestAdvanceBatching();
|
||||
TestChipIndependence();
|
||||
TestSnapshot();
|
||||
TestRegisterExtremes();
|
||||
TestFunctionRestore();
|
||||
TestColdSnapshot();
|
||||
std::cout << "SSI263Synth: all tests passed\n";
|
||||
return 0;
|
||||
}
|
||||
catch (const std::exception& error)
|
||||
{
|
||||
std::cerr << "SSI263Synth: " << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user