// Part of SAASound copyright 1998-2018 Dave Hooper // // SAAFreq.cpp: implementation of the CSAAFreq class. // only 7-bit fractional accuracy on oscillator periods. I may consider fixing that. // ////////////////////////////////////////////////////////////////////// #include "SAASound.h" #include "types.h" #include "SAANoise.h" #include "SAAEnv.h" #include "SAAFreq.h" #include "defns.h" #ifdef SAAFREQ_FIXED_CLOCKRATE // 'load in' the data for the static frequency lookup table // precomputed for a fixed clockrate // See: tools/freqdat.py const unsigned long CSAAFreq::m_FreqTable[2048] = { #include "SAAFreq.dat" }; #else unsigned long CSAAFreq::m_FreqTable[2048]; unsigned long CSAAFreq::m_nClockRate = 0; #endif // SAAFREQ_FIXED_CLOCKRATE const int INITIAL_LEVEL = 1; ////////////////////////////////////////////////////////////////////// // Construction/Destruction ////////////////////////////////////////////////////////////////////// CSAAFreq::CSAAFreq(CSAANoise * const NoiseGenerator, CSAAEnv * const EnvGenerator) : m_nCounter(0), m_nAdd(0), m_nCounter_low(0), m_nOversample(0), m_nCounterLimit_low(1), m_nLevel(INITIAL_LEVEL), m_nCurrentOffset(0), m_nCurrentOctave(0), m_nNextOffset(0), m_nNextOctave(0), m_bIgnoreOffsetData(false), m_bNewData(false), m_bSync(false), m_nSampleRate(SAMPLE_RATE_HZ), m_pcConnectedNoiseGenerator(NoiseGenerator), m_pcConnectedEnvGenerator(EnvGenerator), m_nConnectedMode((NoiseGenerator == NULL) ? ((EnvGenerator == NULL) ? 0 : 1) : 2) { _SetClockRate(EXTERNAL_CLK_HZ); SetAdd(); // current octave, current offset } CSAAFreq::~CSAAFreq() { // Nothing to do } void CSAAFreq::SetFreqOffset(BYTE nOffset) { // nOffset between 0 and 255 if (!m_bSync) { m_nNextOffset = nOffset; m_bNewData=true; if (m_nNextOctave==m_nCurrentOctave) { // According to Philips, if you send the SAA-1099 // new Octave data and then new Offset data in that // order, on the next half-cycle of the current frequency // generator, ONLY the octave data is acted upon. // The offset data will be acted upon next time. // ?? TEST CASE : if you set the octave and then the offset // but the octave you set it to is the same one it already was. // Will this ignore the offset data? // Do you get the same behaviour if you set offset THEN octave // even if you set octave to the same value it was before? m_bIgnoreOffsetData=true; } } else { // updates straightaway if m_bSync m_bNewData=false; m_bIgnoreOffsetData = false; m_nCurrentOffset = nOffset; m_nNextOffset = nOffset; m_nCurrentOctave = m_nNextOctave; SetAdd(); } } void CSAAFreq::SetFreqOctave(BYTE nOctave) { // nOctave between 0 and 7 if (!m_bSync) { m_nNextOctave = nOctave; m_bNewData=true; m_bIgnoreOffsetData = false; } else { // updates straightaway if m_bSync m_bNewData=false; m_bIgnoreOffsetData = false; m_nCurrentOctave = nOctave; m_nNextOctave = nOctave; m_nCurrentOffset = m_nNextOffset; SetAdd(); } } void CSAAFreq::UpdateOctaveOffsetData(void) { // loads the buffered new octave and new offset data into the current registers // and sets up the new frequency for this frequency generator (i.e. sets up m_nAdd) // - called during Sync, and called when waveform half-cycle completes // How the SAA-1099 really treats new data: // if only new octave data is present, // then set new period based on just the octave data // Otherwise, if only new offset data is present, // then set new period based on just the offset data // Otherwise, if new octave data is present, and new offset data is present, // and the offset data was set BEFORE the octave data, // then set new period based on both the octave and offset data // Else, if the offset data came AFTER the new octave data // then set new period based on JUST THE OCTAVE DATA, and continue // signalling the offset data as 'new', so it will be acted upon // next half-cycle // // Weird, I know. But that's how it works. Philips even documented as much. if (!m_bNewData) { // optimise for the most common case! No new data! return; } m_nCurrentOctave=m_nNextOctave; if (!m_bIgnoreOffsetData) { m_nCurrentOffset=m_nNextOffset; m_bNewData=false; } m_bIgnoreOffsetData=false; SetAdd(); } void CSAAFreq::_SetSampleRate(unsigned int nSampleRate) { m_nSampleRate = nSampleRate; } void CSAAFreq::_SetOversample(unsigned int oversample) { // oversample is a power of 2 i.e. // if oversample == 2 then 4x oversample // if oversample == 6 then 64x oversample if (oversample < m_nOversample) { m_nCounter_low <<= (m_nOversample - oversample); } else { m_nCounter_low >>= (oversample - m_nOversample); } m_nCounterLimit_low = 1<= (m_nSampleRate<<12)) { m_nCounter -= (m_nSampleRate<<12); m_nCounter_low++; if (m_nCounter_low >= m_nCounterLimit_low) { // period elapsed for (at least) one half-cycle of // current frequency m_nCounter_low = 0; // flip state - from 0 to 1 or vice versa m_nLevel = 1 - m_nLevel; // trigger any connected devices switch (m_nConnectedMode) { case 1: // env trigger m_pcConnectedEnvGenerator->InternalClock(); break; case 2: // noise trigger m_pcConnectedNoiseGenerator->Trigger(); break; default: // do nothing break; } // get new frequency (set period length m_nAdd) if new data is waiting: UpdateOctaveOffsetData(); } } return m_nLevel; } void CSAAFreq::SetAdd(void) { // nOctave between 0 and 7; nOffset between 0 and 255 // Used to be: // m_nAdd = (15625 << nOctave) / (511 - nOffset); // Now just table lookup: m_nAdd = m_FreqTable[m_nCurrentOctave<<8 | m_nCurrentOffset]; } void CSAAFreq::Sync(bool bSync) { m_bSync = bSync; // update straightaway if m_bSync if (m_bSync) { m_nCounter = 0; m_nCounter_low = 0; // this seems to need to be required to make the Fred59 SPACE DEMO audio work correctly m_nLevel = INITIAL_LEVEL; m_nCurrentOctave=m_nNextOctave; m_nCurrentOffset=m_nNextOffset; SetAdd(); } }