/* * sound.c * * This file is part of Emu48 * * Copyright (C) 2013 Christoph Gießelink * */ #include "pch.h" #include "Emu48.h" // #define DEBUG_SOUND // switch for sound debug purpose // #define SINE_APPROX // sine signal approximation #define SAMPLES_PER_SEC 44100 // sound sampling rate #define MILLISEC_PER_BUFFER 20 // time period of each sound buffer #define NO_OF_BUFFERS 3 // number of reserve buffers before playing typedef struct _MSAMPLE { LPBYTE pbyData; DWORD dwBufferLength; DWORD dwPosition; // buffer admin part DWORD dwIndex; // index to count no. of sample buffers struct _MSAMPLE* pNext; // pointer to next sample buffer } MSAMPLE, *PMSAMPLE; DWORD dwWaveVol = 64; // wave sound volume DWORD dwWaveTime = MILLISEC_PER_BUFFER; // time period (in ms) of each sound buffer static HWAVEOUT hWaveDevice = NULL; // handle to the waveform-audio output device static HANDLE hThreadWave = NULL; // thread handle of sound message handler static DWORD dwThreadWaveId = 0; // thread id of sound message handler static UINT uHeaders = 0; // no. of sending wave headers static PMSAMPLE psHead = NULL; // head of sound samples static PMSAMPLE psTail = NULL; // tail of sound samples static CRITICAL_SECTION csSoundLock; // critical section for sound emulation static DWORD dwSoundBufferLength; // sound buffer length for the given time period static VOID FlushSample(VOID); // // sound message handler thread // static DWORD WINAPI SoundWndProc(LPVOID pParam) { MSG msg; while (GetMessage(&msg, NULL, 0, 0)) { if (msg.message == MM_WOM_DONE) { HWAVEOUT hwo = (HWAVEOUT) msg.wParam; PWAVEHDR pwh = (PWAVEHDR) msg.lParam; VERIFY(waveOutUnprepareHeader(hwo,pwh,sizeof(*pwh)) == MMSYSERR_NOERROR); free(pwh->lpData); // free waveform data free(pwh); // free wavefom header _ASSERT(uHeaders > 0); --uHeaders; // finished header FlushSample(); // check for new sample if (uHeaders == 0) // no wave headers in transmission { bSoundSlow = FALSE; // no sound slow down bEnableSlow = TRUE; // reenable CPU slow down possibility } } } return 0; UNREFERENCED_PARAMETER(pParam); } // // create sound message handler thread // static BOOL CreateWaveThread(VOID) { _ASSERT(hThreadWave == NULL); // create sound message handler thread hThreadWave = CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)&SoundWndProc,NULL,0,&dwThreadWaveId); return hThreadWave != NULL; } // // destroy sound message handler thread // static VOID DestroyWaveThread(VOID) { if (hThreadWave != NULL) // sound message handler thread running { // shut down thread while (!PostThreadMessage(dwThreadWaveId,WM_QUIT,0,0)) Sleep(0); WaitForSingleObject(hThreadWave,INFINITE); CloseHandle(hThreadWave); hThreadWave = NULL; } return; } // // add sample buffer to tail of sample job list // static __inline VOID AddSoundBuf(PMSAMPLE psData) { _ASSERT(psData != NULL); // there must be a sample psData->pNext = NULL; // last sample in job list // add sample to list EnterCriticalSection(&csSoundLock); { if (psTail == NULL) // root { psData->dwIndex = 0; // this is the root index _ASSERT(psHead == NULL); psHead = psTail = psData; // add sample at head } else // add at tail { // use next index psData->dwIndex = psTail->dwIndex + 1; psTail->pNext = psData; // add sample at tail psTail = psData; } } LeaveCriticalSection(&csSoundLock); return; } // // remove sample buffer from head of sample job list // static __inline BOOL GetSoundBuf(PMSAMPLE *ppsData) { BOOL bSucc; EnterCriticalSection(&csSoundLock); { if ((bSucc = (psHead != NULL))) // data in head { *ppsData = psHead; // get sample psHead = psHead->pNext; // and remove it from head if (psHead == NULL) // was last one in head { psTail = NULL; // so tail is also the last one } } } LeaveCriticalSection(&csSoundLock); return bSucc; } // // number of sample buffers in sample job list // static DWORD GetSoundBufSize(VOID) { DWORD dwNoSamples; EnterCriticalSection(&csSoundLock); { // no. of samples in buffer dwNoSamples = (psTail == NULL) ? 0 : (psTail->dwIndex - psHead->dwIndex) + 1; } LeaveCriticalSection(&csSoundLock); return dwNoSamples; } // // allocate new sample buffer and add the // buffer to the tail of the sample job list // static __inline BOOL AllocSample(PMSAMPLE *ppsData) { // alloc new sample buffer *ppsData = (PMSAMPLE) malloc(sizeof(**ppsData)); if (*ppsData != NULL) { (*ppsData)->dwPosition = 0; // begin of buffer (*ppsData)->dwBufferLength = dwSoundBufferLength; (*ppsData)->pbyData = (LPBYTE) malloc((*ppsData)->dwBufferLength); if ((*ppsData)->pbyData != NULL) { // buffers allocated _ASSERT(*ppsData != NULL && (*ppsData)->pbyData != NULL); AddSoundBuf(*ppsData); // add sample buffer to list } else { free(*ppsData); // data alloc failed, delete sample buffer *ppsData = NULL; } } return *ppsData != NULL; } // // write samples to sample buffer // static BOOL AddSamples(DWORD dwSamples, BYTE byLevel) { PMSAMPLE psData; DWORD dwBufSamples; #if defined SINE_APPROX INT w,s,ss,x,y; #endif BOOL bSucc = TRUE; if (dwSamples == 0) return TRUE; // nothing to add #if defined SINE_APPROX // calculate constants w = (INT) (byLevel - 0x80); // max. wave level s = (INT) dwSamples; // interval length (pi) ss = s * s; // interval length ^ 2 x = 1; // sample no. #endif EnterCriticalSection(&csSoundLock); { psData = psTail; // get last sample buffer do { // number of free sound samples in current buffer dwBufSamples = (psData != NULL) ? (psData->dwBufferLength - psData->dwPosition) : 0; if (dwBufSamples == 0) // sample buffer is full { // alloc new sample buffer VERIFY(bSucc = AllocSample(&psData)); if (!bSucc) break; _ASSERT( psData != NULL && psData->pbyData != NULL && psData->dwPosition == 0); dwBufSamples = psData->dwBufferLength; } if (dwSamples < dwBufSamples) // free sample buffer is larger then needed dwBufSamples = dwSamples; // fill only the necessary no. of samples dwSamples -= dwBufSamples; // remaining samples after buffer fill // fill buffer with level for beep #if defined SINE_APPROX for (; dwBufSamples > 0; --dwBufSamples) { // sine approximation function y = w - w * (4 * x * (x - s) + ss ) / ss; ++x; // next sample psData->pbyData[psData->dwPosition++] = (BYTE) (y + 0x80); } #else FillMemory(&psData->pbyData[psData->dwPosition],dwBufSamples,byLevel); psData->dwPosition += dwBufSamples; #endif } while (dwSamples > 0); } LeaveCriticalSection(&csSoundLock); return bSucc; } // // write sample buffer from head of sample job list // to waveform-audio output device and delete the // sample buffer control from head of sample job list // static VOID FlushSample(VOID) { PMSAMPLE psData; _ASSERT(hWaveDevice != NULL); if (GetSoundBuf(&psData) == TRUE) // fetch sample buffer { PWAVEHDR pwh; // allocate new wave header if ((pwh = (PWAVEHDR) malloc(sizeof(*pwh))) != NULL) { pwh->lpData = (LPSTR) psData->pbyData; pwh->dwBufferLength = psData->dwPosition; pwh->dwBytesRecorded = 0; pwh->dwUser = 0; pwh->dwFlags = 0; pwh->dwLoops = 0; ++uHeaders; // add header // prepare sample VERIFY(waveOutPrepareHeader(hWaveDevice,pwh,sizeof(*pwh)) == MMSYSERR_NOERROR); // send sample VERIFY(waveOutWrite(hWaveDevice,pwh,sizeof(*pwh)) == MMSYSERR_NOERROR); } free(psData); // delete sample buffer } return; } // // 44.1 kHz, mono, 8-bit waveform-audio output device available // BOOL SoundAvailable(UINT uDeviceID) { WAVEOUTCAPS woc; return waveOutGetDevCaps(uDeviceID,&woc,sizeof(woc)) == MMSYSERR_NOERROR && (woc.dwFormats & WAVE_FORMAT_4M08) != 0; } // // get the device ID of the current waveform-audio output device // BOOL SoundGetDeviceID(UINT *puDeviceID) { BOOL bSucc = FALSE; if (hWaveDevice) // have sound device { bSucc = (waveOutGetID(hWaveDevice,puDeviceID) == MMSYSERR_NOERROR); } return bSucc; } // // open waveform-audio output device // BOOL SoundOpen(UINT uDeviceID) { // check if sound device is already open if (hWaveDevice == NULL && SoundAvailable(uDeviceID)) { WAVEFORMATEX wf; BOOL bSucc; wf.wFormatTag = WAVE_FORMAT_PCM; wf.nChannels = 1; wf.nSamplesPerSec = SAMPLES_PER_SEC; wf.wBitsPerSample = 8; wf.nBlockAlign = wf.nChannels * wf.wBitsPerSample / 8; wf.nAvgBytesPerSec = wf.nBlockAlign * wf.nSamplesPerSec; wf.cbSize = 0; InitializeCriticalSection(&csSoundLock); // sound buffer length for the given time period dwSoundBufferLength = SAMPLES_PER_SEC * dwWaveTime / 1000; if ((bSucc = CreateWaveThread())) // create sound message handler { // create a sound device, use the CALLBACK_THREAD flag because with the // CALLBACK_FUNCTION flag unfortunately the called callback function // can only call a specific set of Windows functions. Attempting to // call other functions at the wrong time will result in a deadlock. bSucc = (waveOutOpen(&hWaveDevice,uDeviceID,&wf,dwThreadWaveId,0,CALLBACK_THREAD) == MMSYSERR_NOERROR); } if (!bSucc) { DestroyWaveThread(); // shut down message thread DeleteCriticalSection(&csSoundLock); hWaveDevice = NULL; } } return hWaveDevice != NULL; } // // close waveform-audio output device // VOID SoundClose(VOID) { if (hWaveDevice != NULL) { EnterCriticalSection(&csSoundLock); { while (psHead != NULL) // cleanup remaining sample buffers { PMSAMPLE psNext = psHead->pNext; free(psHead->pbyData); free(psHead); psHead = psNext; } psTail = NULL; } LeaveCriticalSection(&csSoundLock); // abandon all pending wave headers VERIFY(waveOutReset(hWaveDevice) == MMSYSERR_NOERROR); DestroyWaveThread(); // shut down message thread VERIFY(waveOutClose(hWaveDevice) == MMSYSERR_NOERROR); DeleteCriticalSection(&csSoundLock); hWaveDevice = NULL; } uHeaders = 0; // no wave headers in transmission bSoundSlow = FALSE; // no sound slow down bEnableSlow = TRUE; // reenable CPU slow down possibility return; } // // calculate the wave level from the beeper bit state // static BYTE WaveLevel(WORD wOut) { wOut >>= 11; // mask out beeper bit OR[11] return (BYTE) (wOut & 0x01) + 1; // return 1 or 2 } // // decode change of beeper OUT bits // VOID SoundOut(CHIPSET* w, WORD wOut) { static DWORD dwLastCyc; // last timer value at beeper bit change DWORD dwCycles,dwDiffSatCycles,dwDiffCycles,dwCpuFreq,dwSamples; BYTE byWaveLevel; // sound device not opened or waveform-audio output device not available if (hWaveDevice == NULL) return; // actual timestamp dwCycles = (DWORD) (w->cycles & 0xFFFFFFFF); dwDiffSatCycles = dwCycles - dwLastCyc; // time difference from syncpoint in original Saturn cycles // theoretical CPU frequency from given T2CYCLES dwCpuFreq = dwT2Cycles * 16384; if (dwDiffSatCycles > dwCpuFreq / 2) // frequency < 1 Hz { dwLastCyc = dwCycles; // initial call for start beeping return; } // estimated CPU cycles for Clarke/Yorke chip dwDiffCycles = (cCurrentRomType == 'S') ? (dwDiffSatCycles * 26) / 25 // Clarke * 1.04 : (dwDiffSatCycles * 11) / 10; // Yorke * 1.10 // adjust original CPU cycles w->cycles += (dwDiffCycles - dwDiffSatCycles); dwLastCyc = (DWORD) (w->cycles & 0xFFFFFFFF); // new syncpoint // calculate no. of sound samples from CPU cycles, !! intermediate result maybe > 32bit !! dwSamples = (DWORD) ((2 * (QWORD) dwDiffCycles + 1) * SAMPLES_PER_SEC / 2 / dwCpuFreq); if (dwSamples == 0) // frequency too high -> play nothing return; #if defined DEBUG_SOUND { TCHAR buffer[256]; // calculate rounded time in us QWORD lDuration = 1000000 * (2 * (QWORD) dwDiffCycles + 1) / (2 * dwCpuFreq); wsprintf(buffer,_T("State %u: Time = %I64u us f = %u Hz, Time = %I64u us f = %u Hz\n"), wOut >> 11,lDuration,(DWORD) (1000000 / 2 / lDuration), (QWORD) dwSamples * 1000000 / SAMPLES_PER_SEC,SAMPLES_PER_SEC / 2 / dwSamples); OutputDebugString(buffer); } #endif // begin of beep if (uHeaders == 0 && GetSoundBufSize() == 0) { // use silence buffers to start output engine AddSamples(dwSoundBufferLength * NO_OF_BUFFERS,0x80); } // offset for wave level byWaveLevel = 0x80 + (BYTE) (dwWaveVol * (WaveLevel(wOut) - WaveLevel(w->out)) / 2); AddSamples(dwSamples,byWaveLevel); // add samples to latest wave sample buffer if (GetSoundBufSize() > NO_OF_BUFFERS) // have more than 3 wave sample buffers { FlushSample(); // send 2 of them FlushSample(); } // ran out of buffers -> disable CPU slow down InitAdjustSpeed(); // init variables if necessary bEnableSlow = (GetSoundBufSize() > 1); if (bSoundSlow == FALSE) { InitAdjustSpeed(); // init variables if necessary bSoundSlow = TRUE; // CPU slow down } return; } // // beep with frequency (Hz) and duration (ms) // VOID SoundBeep(DWORD dwFrequency, DWORD dwDuration) { QWORD lPeriods; DWORD dwSamples; BYTE byLevel; // waveform-audio output device opened and have frequency if (hWaveDevice && dwFrequency > 0) { // samples for 1/2 of time period dwSamples = SAMPLES_PER_SEC / 2 / dwFrequency; // overall half periods lPeriods = (QWORD) dwFrequency * dwDuration / 500; while (lPeriods-- > 0) // create sample buffers { // signal level byLevel = 0x80 + (BYTE) ((((DWORD) lPeriods & 1) * 2 - 1) * (dwWaveVol / 2)); AddSamples(dwSamples,byLevel); // add half period sample } while (GetSoundBufSize() > 0) // samples in job list FlushSample(); // send sample buffer } Sleep(dwDuration); return; }