// Part of SAASound copyright 1998-2018 Dave Hooper // // SAAImpl.cpp: implementation of the CSAASound class. // the bones of the 'virtual SAA-1099' emulation // // the actual sound generation is carried out in the other classes; // this class provides the output stage and the external interface only // ////////////////////////////////////////////////////////////////////// #include "SAASound.h" #include "types.h" #include "SAAImpl.h" #include "defns.h" ////////////////////////////////////////////////////////////////////// // Construction/Destruction ////////////////////////////////////////////////////////////////////// CSAASoundInternal::CSAASoundInternal() : m_chip(), m_uParam(0), m_uParamRate(0), m_nClockRate(EXTERNAL_CLK_HZ), m_nSampleRate(SAMPLE_RATE_HZ), m_nOversample(DEFAULT_OVERSAMPLE), m_bHighpass(false), filterout_z1_left_mixed(0), filterout_z1_right_mixed(0) { #ifdef USE_CONFIG_FILE m_Config.ReadConfig(); #endif #if defined(DEBUGSAA) m_dbgfile.open(_T(DEBUG_SAA_REGISTER_LOG), std::ios_base::out); m_pcmfile.open(_T(DEBUG_SAA_PCM_LOG), std::ios_base::out | std::ios_base::binary); #elif defined(USE_CONFIG_FILE) if (m_Config.m_bGenerateRegisterLogs) m_dbgfile.open(m_Config.m_strRegisterLogPath, std::ios_base::out); if (m_Config.m_bGeneratePcmLogs) m_pcmfile.open(m_Config.m_strPcmOutputPath, std::ios_base::out | std::ios_base::binary); if (m_Config.m_bGeneratePcmLogs && m_Config.m_bGeneratePcmSeparateChannels) { for (int i = 0; i < 6; i++) { m_channel_pcmfile[i].open(m_Config.getChannelPcmOutputPath(i), std::ios_base::out | std::ios_base::binary); } } #endif // set parameters // TODO support defaults and overrides from config file // m_chip.SetSoundParameters(SAAP_FILTER | SAAP_11025 | SAAP_8BIT | SAAP_MONO); // reset the virtual SAA // m_chip.Clear(); m_chip._SetClockRate(m_nClockRate); m_chip._SetOversample(m_nOversample); } CSAASoundInternal::~CSAASoundInternal() { // } ////////////////////////////////////////////////////////////////////// // CSAASound members ////////////////////////////////////////////////////////////////////// void CSAASoundInternal::SetClockRate(unsigned int nClockRate) { m_nClockRate = nClockRate; m_chip._SetClockRate(m_nClockRate); } void CSAASoundInternal::Clear(void) { // reinitialises virtual SAA: // sets reg 28 to 0x02; - sync and disabled // sets regs 00-31 (except 28) to 0x00; // sets reg 28 to 0x00; // sets current reg to 0 WriteAddressData(28,2); for (int i=31; i>=0; i--) { if (i!=28) WriteAddressData(i,0); } WriteAddressData(28,0); WriteAddress(0); } void CSAASoundInternal::WriteData(BYTE nData) { // originated from an OUT 255,d call m_chip._WriteData(nData); #if defined(DEBUGSAA) || defined(USE_CONFIG_FILE) #ifdef USE_CONFIG_FILE if (m_Config.m_bGenerateRegisterLogs) { #endif m_dbgfile << m_nDebugSample << " " << (int)m_chip._ReadAddress() << ":" << (int)nData << std::endl; #ifdef USE_CONFIG_FILE } #endif #endif } void CSAASoundInternal::WriteAddress(BYTE nReg) { // originated from an OUT 511,r call m_chip._WriteAddress(nReg); #if defined(DEBUGSAA) || defined(USE_CONFIG_FILE) #ifdef USE_CONFIG_FILE if (m_Config.m_bGenerateRegisterLogs) { #endif m_dbgfile << m_nDebugSample << " " << (int)nReg << ":"; if (nReg==24) { m_dbgfile << ""; } else if (nReg==25) { m_dbgfile << ""; } m_dbgfile << std::endl; #ifdef USE_CONFIG_FILE } #endif #endif } void CSAASoundInternal::WriteAddressData(BYTE nReg, BYTE nData) { // performs WriteAddress(nReg) followed by WriteData(nData) m_chip._WriteAddress(nReg); m_chip._WriteData(nData); } #ifdef DEBUG BYTE CSAASoundInternal::ReadAddress(void) { // Not a real hardware function of the SAA-1099, which is write-only return(m_chip._ReadAddress()); } #else BYTE CSAASoundInternal::ReadAddress(void) { // Not a real hardware function of the SAA-1099, which is write-only return(0); } #endif void CSAASoundInternal::SetSoundParameters(SAAPARAM uParam) { // set samplerate properties from uParam (deprecated but still supported) unsigned int nSampleRate = m_nSampleRate; switch (uParam & SAAP_MASK_SAMPLERATE) { case SAAP_44100: nSampleRate = 44100; m_uParamRate = (m_uParamRate & ~SAAP_MASK_SAMPLERATE) | SAAP_44100; break; case SAAP_22050: nSampleRate = 22050; m_uParamRate = (m_uParamRate & ~SAAP_MASK_SAMPLERATE) | SAAP_22050; break; case SAAP_11025: nSampleRate = 11025; m_uParamRate = (m_uParamRate & ~SAAP_MASK_SAMPLERATE) | SAAP_11025; break; case 0:// change nothing! default: break; } if (nSampleRate != m_nSampleRate) { m_nSampleRate = nSampleRate; m_chip._SetSampleRate(m_nSampleRate); } // set filter properties from uParam m_uParam = (m_uParam & ~SAAP_MASK_FILTER) | (uParam & SAAP_MASK_FILTER); } void CSAASoundInternal::SetSampleRate(unsigned int nSampleRate) { if (nSampleRate != m_nSampleRate) { m_nSampleRate = nSampleRate; m_chip._SetSampleRate(m_nSampleRate); } } void CSAASoundInternal::SetOversample(unsigned int nOversample) { if (nOversample != m_nOversample) { m_nOversample = nOversample; m_chip._SetOversample(m_nOversample); } } SAAPARAM CSAASoundInternal::GetCurrentSoundParameters(void) { return m_uParam | m_uParamRate; } unsigned short CSAASoundInternal::GetCurrentBytesPerSample(void) { // 16 bit stereo => 4 bytes per sample return 4; } /*static*/ unsigned short CSAASound::GetBytesPerSample(SAAPARAM uParam) { // 16 bit stereo => 4 bytes per sample switch (uParam & (SAAP_MASK_CHANNELS | SAAP_MASK_BITDEPTH)) { case SAAP_STEREO | SAAP_16BIT: return 4; default: return 0; } } unsigned int CSAASoundInternal::GetCurrentSampleRate(void) { return CSAASound::GetSampleRate(m_uParamRate); } /*static*/ unsigned int CSAASound::GetSampleRate(SAAPARAM uParam) // static member function { switch (uParam & SAAP_MASK_SAMPLERATE) { case SAAP_11025: return 11025; case SAAP_22050: return 22050; case SAAP_44100: return 44100; default: return 0; } } #if defined(USE_CONFIG_FILE) || (defined(DEFAULT_BOOST) && DEFAULT_BOOST>1) #define DO_BOOST #endif void scale_for_output(unsigned int left_input, unsigned int right_input, double oversample_scalar, bool highpass, double boost, double& filterout_z1_left, double& filterout_z1_right, BYTE* &pBuffer) { double float_left = (double)left_input; double float_right = (double)right_input; float_left /= oversample_scalar; float_right /= oversample_scalar; // scale output into good range float_left *= DEFAULT_UNBOOSTED_MULTIPLIER; float_right *= DEFAULT_UNBOOSTED_MULTIPLIER; if (highpass) { /* cutoff = 5 Hz (say) const double b1 = exp(-2.0 * M_PI * (Fc/Fs)) const double a0 = 1.0 - b1; */ const double b1 = 0.99928787; const double a0 = 1.0 - b1; filterout_z1_left = float_left * a0 + filterout_z1_left * b1; filterout_z1_right = float_right * a0 + filterout_z1_right * b1; float_left -= filterout_z1_left; float_right -= filterout_z1_right; } // multiply by boost, if defined #if defined(DO_BOOST) float_left *= boost; float_right *= boost; #endif // convert to 16-bit signed range with hard clipping signed short left_output = (signed short)(float_left > 32767 ? 32767 : float_left < -32768 ? -32768 : float_left); signed short right_output = (signed short)(float_right > 32767 ? 32767 : float_right < -32768 ? -32768 : float_right); *pBuffer++ = left_output & 0x00ff; *pBuffer++ = (left_output >> 8) & 0x00ff; *pBuffer++ = right_output & 0x00ff; *pBuffer++ = (right_output >> 8) & 0x00ff; } void CSAASoundInternal::GenerateMany(BYTE* pBuffer, unsigned int nSamples, DivDispatchOscBuffer** oscBuf) { unsigned int left_mixed, right_mixed; #if defined(DEBUGSAA) || defined(USE_CONFIG_FILE) BYTE* pBufferStart = pBuffer; unsigned int nTotalSamples = nSamples; #endif #if defined(DO_BOOST) #if defined(USE_CONFIG_FILE) double nBoost = m_Config.m_nBoost; #else double nBoost = DEFAULT_BOOST; #endif #else double nBoost = DEFAULT_BOOST; #endif double oversample = double(1 << m_nOversample); #if defined(USE_CONFIG_FILE) static double filterout_z1_left_0 = 0, filterout_z1_right_0 = 0; static double filterout_z1_left_1 = 0, filterout_z1_right_1 = 0; static double filterout_z1_left_2 = 0, filterout_z1_right_2 = 0; static double filterout_z1_left_3 = 0, filterout_z1_right_3 = 0; static double filterout_z1_left_4 = 0, filterout_z1_right_4 = 0; static double filterout_z1_left_5 = 0, filterout_z1_right_5 = 0; if (m_Config.m_bGeneratePcmLogs && m_Config.m_bGeneratePcmSeparateChannels) { unsigned int left0, right0, left1, right1, left2, right2, left3, right3, left4, right4, left5, right5; BYTE* pChannelBufferPtr[6] = { m_pChannelBuffer[0], m_pChannelBuffer[1], m_pChannelBuffer[2], m_pChannelBuffer[3], m_pChannelBuffer[4], m_pChannelBuffer[5] }; while (nSamples--) { m_chip._TickAndOutputSeparate(left_mixed, right_mixed, left0, right0, left1, right1, left2, right2, left3, right3, left4, right4, left5, right5); scale_for_output(left_mixed, right_mixed, oversample, m_bHighpass, nBoost, filterout_z1_left_mixed, filterout_z1_right_mixed, pBuffer); // and the separate channels scale_for_output(left0, right0, oversample, m_bHighpass, nBoost, filterout_z1_left_0, filterout_z1_right_0, pChannelBufferPtr[0]); scale_for_output(left1, right1, oversample, m_bHighpass, nBoost, filterout_z1_left_1, filterout_z1_right_1, pChannelBufferPtr[1]); scale_for_output(left2, right2, oversample, m_bHighpass, nBoost, filterout_z1_left_2, filterout_z1_right_2, pChannelBufferPtr[2]); scale_for_output(left3, right3, oversample, m_bHighpass, nBoost, filterout_z1_left_3, filterout_z1_right_3, pChannelBufferPtr[3]); scale_for_output(left4, right4, oversample, m_bHighpass, nBoost, filterout_z1_left_4, filterout_z1_right_4, pChannelBufferPtr[4]); scale_for_output(left5, right5, oversample, m_bHighpass, nBoost, filterout_z1_left_5, filterout_z1_right_5, pChannelBufferPtr[5]); // flush channel output PCM buffers when full if (pChannelBufferPtr[0] >= m_pChannelBuffer[0] + CHANNEL_BUFFER_SIZE) { for (int i = 0; i < 6; i++) { m_channel_pcmfile[i].write((const char*)m_pChannelBuffer[i], CHANNEL_BUFFER_SIZE); pChannelBufferPtr[i] = m_pChannelBuffer[i]; } } } // flush remaining channel PCM output data if (pChannelBufferPtr[0] >= m_pChannelBuffer[0]) { for (int i = 0; i < 6; i++) { m_channel_pcmfile[i].write((const char*)m_pChannelBuffer[i], pChannelBufferPtr[i]-m_pChannelBuffer[i]); } } } else { #endif while (nSamples--) { m_chip._TickAndOutputStereo(left_mixed, right_mixed, oscBuf); scale_for_output(left_mixed, right_mixed, oversample, m_bHighpass, nBoost, filterout_z1_left_mixed, filterout_z1_right_mixed, pBuffer); } #if defined(USE_CONFIG_FILE) } #endif #if defined(DEBUGSAA) || defined(USE_CONFIG_FILE) #ifdef USE_CONFIG_FILE if (m_Config.m_bGeneratePcmLogs) { #endif m_pcmfile.write((const char *)pBufferStart, nTotalSamples * (unsigned int)GetCurrentBytesPerSample()); m_nDebugSample += nTotalSamples; #ifdef USE_CONFIG_FILE } #endif #endif } /////////////////////////////////////////////////////// LPCSAASOUND SAAAPI CreateCSAASound(void) { return (new CSAASoundInternal); } void SAAAPI DestroyCSAASound(LPCSAASOUND object) { delete (object); } /* thoughts on lowpass filtering as part of oversampling. I tried this and really it didn't seem to make a lot of (audible) difference. // lowpass oversample filter adds complexity and not particularly audibly better than simple averaging. // use_lowpass_oversample_filter_average_output adds an additional averaging step to the output of the oversample // filter. this seems critical, because without this, the raw output of the lowpass filter is full of aliases // If use_lowpass_oversample_filter is False, then the _average_output flag is ignored. // Default, use_lowpass_oversample_filter is False, it sounds just fine really. //#define USE_LOWPASS_OVERSAMPLE_FILTER #undef USE_LOWPASS_OVERSAMPLE_FILTER //#define USE_LOWPASS_OVERSAMPLE_FILTER_AVERAGE_OUTPUT #undef USE_LOWPASS_OVERSAMPLE_FILTER_AVERAGE_OUTPUT #ifdef USE_LOWPASS_OVERSAMPLE_FILTER static double oversample_lp_filterout_z1_left_stages[10] = { 0,0,0,0,0,0,0,0,0,0 }; static double oversample_lp_filterout_z1_right_stages[10] = { 0,0,0,0,0,0,0,0,0,0 }; double averaged_filterout_left = 0.0, averaged_filterout_right = 0.0; const int nStages = 10; for (int i = 0; i < 1 << m_nOversample; i++) { Noise[0]->Tick(); Noise[1]->Tick(); f_left = f_right = 0; for (int c = 0; c < 6; c++) { Amp[c]->TickAndOutputStereo(temp_left, temp_right); f_left += (double)temp_left; f_right += (double)temp_right; } // apply lowpass here. // HACK: ASSUME m_nOversample is 64 (I was experimenting only using the 64x oversample anyway) // therefore Fs = 44100*64 // let's set Fc = 10kHz // so Fc/Fs = 0.00354308390022675736961451247166 // const double b1 = exp(-2.0 * M_PI * (Fc/Fs)) // const double a0 = 1.0 - b1; // const double b1 = 0.9779841137335348363722276130195; const double b1 = 0.977; const double a0 = 1.0 - b1; oversample_lp_filterout_z1_left_stages[0] = f_left * a0 + oversample_lp_filterout_z1_left_stages[0] * b1; for (int stage = 1; stage < nStages; stage++) oversample_lp_filterout_z1_left_stages[stage] = oversample_lp_filterout_z1_left_stages[stage - 1] * a0 + oversample_lp_filterout_z1_left_stages[stage] * b1; oversample_lp_filterout_z1_right_stages[0] = f_right * a0 + oversample_lp_filterout_z1_right_stages[0] * b1; for (int stage = 1; stage < nStages; stage++) oversample_lp_filterout_z1_right_stages[stage] = oversample_lp_filterout_z1_right_stages[stage - 1] * a0 + oversample_lp_filterout_z1_right_stages[stage] * b1; #ifdef USE_LOWPASS_OVERSAMPLE_FILTER_AVERAGE_OUTPUT averaged_filterout_left += oversample_lp_filterout_4z1_left; averaged_filterout_right += oversample_lp_filterout_4z1_right; #endif } // by the end of this loop we will have computed the oversample lowpass filter m_nOversample times // and yielded exactly ONE sample output. #ifdef USE_LOWPASS_OVERSAMPLE_FILTER_AVERAGE_OUTPUT f_left = averaged_filterout_left / (1 << m_nOversample); f_right = averaged_filterout_right / (1 << m_nOversample); #else f_left = oversample_lp_filterout_z1_left_stages[nStages - 1]; f_right = oversample_lp_filterout_z1_right_stages[nStages - 1]; #endif #else // do the simple 1/N averaging which is easier and sounds good enough #endif */