1 /*****************************************************************************\
2      Snes9x - Portable Super Nintendo Entertainment System (TM) emulator.
3                 This file is licensed under the Snes9x License.
4    For further information, consult the LICENSE file in the root directory.
5 \*****************************************************************************/
6 
7 #include "CXAudio2.h"
8 #include "../snes9x.h"
9 #include "../apu/apu.h"
10 #include "wsnes9x.h"
11 #include <process.h>
12 #include "dxerr.h"
13 #include "commctrl.h"
14 #include <assert.h>
15 
16 /* CXAudio2
17 	Implements audio output through XAudio2.
18 	Basic idea: one master voice and one source voice are created;
19 	the source voice consumes buffers queued by PushBuffer, plays them through
20 	the master voice and calls OnBufferEnd after each buffer.
21 	ProcessSound copies new samples into the buffer and queues them for playback.
22 */
23 
24 /*  Construction/Destruction
25 */
CXAudio2(void)26 CXAudio2::CXAudio2(void)
27 {
28 	pXAudio2 = NULL;
29 	pSourceVoice = NULL;
30 	pMasterVoice = NULL;
31 
32 	sum_bufferSize = singleBufferBytes \
33 		= singleBufferSamples = blockCount = 0;
34 	soundBuffer = NULL;
35 	initDone = false;
36 }
37 
~CXAudio2(void)38 CXAudio2::~CXAudio2(void)
39 {
40 	DeInitXAudio2();
41 }
42 
DlgXAudio2InitError(HWND hDlg,UINT msg,WPARAM wParam,LPARAM lParam)43 INT_PTR CALLBACK DlgXAudio2InitError(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
44 {
45 	switch (msg)
46 	{
47 	case WM_INITDIALOG:
48 		{
49 			// display warning icon and set text of sys control (too long for resource)
50 			HICON aIcn = LoadIcon(NULL, IDI_WARNING);
51 			SendDlgItemMessage(hDlg, IDC_STATIC_ICON, STM_SETICON, (WPARAM)aIcn, 0);
52 			SetDlgItemText(hDlg, IDC_SYSLINK_DX, TEXT("Unable to initialize XAudio2.\
53 You will not be able to hear any sound effects or music while playing.\n\n\
54 This is usually caused by not having a recent DirectX9 runtime installed.\n\
55 You can download the most recent DirectX9 runtime here:\n\n\
56 <a href=\"https://www.microsoft.com/en-us/download/details.aspx?id=35\">https://www.microsoft.com/en-us/download/details.aspx?id=35</a>"));
57 
58 			// center dialog on parent
59 			HWND parent = GetParent(hDlg);
60 			RECT rcParent, rcSelf;
61 			GetWindowRect(parent, &rcParent);
62 			GetWindowRect(hDlg, &rcSelf);
63 
64 			SetWindowPos(hDlg,
65 				HWND_TOP,
66 				rcParent.left + ((rcParent.right - rcParent.left) - (rcSelf.right - rcSelf.left)) / 2,
67 				rcParent.top + ((rcParent.bottom - rcParent.top) - (rcSelf.bottom - rcSelf.top)) / 2,
68 				0, 0,
69 				SWP_NOSIZE);
70 		}
71 		return true;
72 	case WM_COMMAND:
73 		switch (LOWORD(wParam))
74 		{
75 		case IDOK:
76 			if (HIWORD(wParam) == BN_CLICKED) {
77 				EndDialog(hDlg, IDOK);
78 				return TRUE;
79 			}
80 		}
81 		break;
82 	case WM_NOTIFY:
83 		switch (((LPNMHDR)lParam)->code)
84 		{
85 			case NM_CLICK:          // Fall through to the next case.
86 			case NM_RETURN:
87 			{
88 				PNMLINK pNMLink = (PNMLINK)lParam;
89 				LITEM   item = pNMLink->item;
90 
91 				// open link with registered application
92 				ShellExecute(NULL, L"open", item.szUrl, NULL, NULL, SW_SHOW);
93 				break;
94 			}
95 		}
96 	}
97 
98 	return FALSE;
99 }
100 
101 /*  CXAudio2::InitXAudio2
102 initializes the XAudio2 object
103 -----
104 returns true if successful, false otherwise
105 */
InitXAudio2(void)106 bool CXAudio2::InitXAudio2(void)
107 {
108 	if(initDone)
109 		return true;
110 
111 	HRESULT hr;
112 	if ( FAILED(hr = XAudio2Create( &pXAudio2, 0 , XAUDIO2_DEFAULT_PROCESSOR ) ) ) {
113 		DXTRACE_ERR_MSGBOX(TEXT("Unable to create XAudio2 object."),hr);
114 		DialogBox(GUI.hInstance, MAKEINTRESOURCE(IDD_DIALOG_XAUDIO2_INIT_ERROR), GUI.hWnd, DlgXAudio2InitError);
115 		return false;
116 	}
117 	initDone = true;
118 	return true;
119 }
120 
121 /*  CXAudio2::InitVoices
122 initializes the voice objects with the current audio settings
123 -----
124 returns true if successful, false otherwise
125 */
InitVoices(void)126 bool CXAudio2::InitVoices(void)
127 {
128 	HRESULT hr;
129     // subtract -1, we added "Default" as first index
130     int device_index = FindDeviceIndex(GUI.AudioDevice) - 1;
131     if (device_index < 0)
132         device_index = 0;
133 
134 	if ( FAILED(hr = pXAudio2->CreateMasteringVoice( &pMasterVoice, 2,
135 		Settings.SoundPlaybackRate, 0, device_index, NULL ) ) ) {
136 			DXTRACE_ERR_MSGBOX(TEXT("Unable to create mastering voice."),hr);
137 			return false;
138 	}
139 
140 	WAVEFORMATEX wfx;
141 	wfx.wFormatTag = WAVE_FORMAT_PCM;
142     wfx.nChannels = 2;
143     wfx.nSamplesPerSec = Settings.SoundPlaybackRate;
144     wfx.nBlockAlign = 2 * 2;
145     wfx.wBitsPerSample = 16;
146     wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign;
147     wfx.cbSize = 0;
148 
149 	if( FAILED(hr = pXAudio2->CreateSourceVoice(&pSourceVoice, (WAVEFORMATEX*)&wfx,
150 		XAUDIO2_VOICE_NOSRC, XAUDIO2_DEFAULT_FREQ_RATIO, this, NULL, NULL ) ) ) {
151 			DXTRACE_ERR_MSGBOX(TEXT("Unable to create source voice."),hr);
152 			return false;
153 	}
154 
155 	return true;
156 }
157 
158 /*  CXAudio2::DeInitXAudio2
159 deinitializes all objects
160 */
DeInitXAudio2(void)161 void CXAudio2::DeInitXAudio2(void)
162 {
163 	initDone = false;
164 	DeInitVoices();
165 	if(pXAudio2) {
166 		pXAudio2->Release();
167 		pXAudio2 = NULL;
168 	}
169 }
170 
171 /*  CXAudio2::DeInitVoices
172 deinitializes the voice objects and buffers
173 */
DeInitVoices(void)174 void CXAudio2::DeInitVoices(void)
175 {
176 	if(pSourceVoice) {
177 		StopPlayback();
178 		pSourceVoice->DestroyVoice();
179 		pSourceVoice = NULL;
180 	}
181 	if(pMasterVoice) {
182 		pMasterVoice->DestroyVoice();
183 		pMasterVoice = NULL;
184 	}
185 	if(soundBuffer) {
186 		delete [] soundBuffer;
187 		soundBuffer = NULL;
188 	}
189 }
190 
191 /*  CXAudio2::OnBufferEnd
192 callback function called by the source voice
193 IN:
194 pBufferContext		-	unused
195 */
OnBufferEnd(void * pBufferContext)196 void CXAudio2::OnBufferEnd(void *pBufferContext)
197 {
198 	InterlockedDecrement(&bufferCount);
199     SetEvent(GUI.SoundSyncEvent);
200 }
201 
202 /*  CXAudio2::PushBuffer
203 pushes one buffer onto the source voice playback queue
204 IN:
205 AudioBytes		-	size of the buffer
206 pAudioData		-	pointer to the buffer
207 pContext		-	context passed to the callback, currently unused
208 */
PushBuffer(UINT32 AudioBytes,BYTE * pAudioData,void * pContext)209 void CXAudio2::PushBuffer(UINT32 AudioBytes,BYTE *pAudioData,void *pContext)
210 {
211 	XAUDIO2_BUFFER xa2buffer={0};
212 	xa2buffer.AudioBytes=AudioBytes;
213 	xa2buffer.pAudioData=pAudioData;
214 	xa2buffer.pContext=pContext;
215 	InterlockedIncrement(&bufferCount);
216 	pSourceVoice->SubmitSourceBuffer(&xa2buffer);
217 }
218 
219 /*  CXAudio2::SetupSound
220 applies current sound settings by recreating the voice objects and buffers
221 -----
222 returns true if successful, false otherwise
223 */
SetupSound()224 bool CXAudio2::SetupSound()
225 {
226 	if(!initDone)
227 		return false;
228 
229 	DeInitVoices();
230 
231 	blockCount = 8;
232 	UINT32 blockTime = GUI.SoundBufferSize / blockCount;
233 
234 	singleBufferSamples = (Settings.SoundPlaybackRate * blockTime) / 1000;
235     singleBufferSamples *= 2;
236 	singleBufferBytes = singleBufferSamples * 2;
237 	sum_bufferSize = singleBufferBytes * blockCount;
238 
239     if (InitVoices())
240     {
241 		soundBuffer = new uint8[sum_bufferSize];
242 		writeOffset = 0;
243 		partialOffset = 0;
244     }
245 	else {
246 		DeInitVoices();
247 		return false;
248 	}
249 
250 	bufferCount = 0;
251 
252 	BeginPlayback();
253 
254     return true;
255 }
256 
SetVolume(double volume)257 void CXAudio2::SetVolume(double volume)
258 {
259 	pSourceVoice->SetVolume(volume);
260 }
261 
BeginPlayback()262 void CXAudio2::BeginPlayback()
263 {
264 	pSourceVoice->Start(0);
265 }
266 
StopPlayback()267 void CXAudio2::StopPlayback()
268 {
269 	pSourceVoice->Stop(0);
270 }
271 
GetAvailableBytes()272 int CXAudio2::GetAvailableBytes()
273 {
274     return ((blockCount - bufferCount) * singleBufferBytes) - partialOffset;
275 }
276 
277 /*  CXAudio2::ProcessSound
278 The mixing function called by the sound core when new samples are available.
279 SoundBuffer is divided into blockCount blocks. If there are enough available samples and a free block,
280 the block is filled and queued to the source voice. bufferCount is increased by pushbuffer and decreased by
281 the OnBufferComplete callback.
282 */
ProcessSound()283 void CXAudio2::ProcessSound()
284 {
285 	int freeBytes = GetAvailableBytes();
286 
287 	if (Settings.DynamicRateControl)
288 	{
289 		S9xUpdateDynamicRate(freeBytes, sum_bufferSize);
290 	}
291 
292 	UINT32 availableSamples;
293 
294 	availableSamples = S9xGetSampleCount();
295 
296 	if (Settings.DynamicRateControl && !Settings.SoundSync)
297 	{
298 		// Using rate control, we should always keep the emulator's sound buffers empty to
299 		// maintain an accurate measurement.
300 		if (availableSamples > (freeBytes >> 1))
301 		{
302 			S9xClearSamples();
303 			return;
304 		}
305 	}
306 
307 	if(!initDone)
308 		return;
309 
310     if(Settings.SoundSync && !Settings.TurboMode && !Settings.Mute)
311     {
312         // no sound sync when speed is not set to 100%
313         while((freeBytes >> 1) < availableSamples)
314         {
315             ResetEvent(GUI.SoundSyncEvent);
316             if(!GUI.AllowSoundSync || WaitForSingleObject(GUI.SoundSyncEvent, 1000) != WAIT_OBJECT_0)
317             {
318                 S9xClearSamples();
319                 return;
320             }
321             freeBytes = GetAvailableBytes();
322         }
323     }
324 
325 	if (partialOffset != 0)	{
326 		assert(partialOffset < singleBufferBytes);
327 		assert(bufferCount < blockCount);
328 		BYTE *offsetBuffer = soundBuffer + writeOffset + partialOffset;
329 		UINT32 samplesleftinblock = (singleBufferBytes - partialOffset) >> 1;
330 
331 		if (availableSamples < samplesleftinblock)
332 		{
333 			S9xMixSamples(offsetBuffer, availableSamples);
334             partialOffset += availableSamples << 1;
335 			assert(partialOffset < singleBufferBytes);
336 			availableSamples = 0;
337 		}
338 		else
339 		{
340 			S9xMixSamples(offsetBuffer, samplesleftinblock);
341 			partialOffset = 0;
342 			availableSamples -= samplesleftinblock;
343 			PushBuffer(singleBufferBytes, soundBuffer + writeOffset, NULL);
344 			writeOffset += singleBufferBytes;
345 			writeOffset %= sum_bufferSize;
346 		}
347 	}
348 
349 	while (availableSamples >= singleBufferSamples && bufferCount < blockCount) {
350 		BYTE *curBuffer = soundBuffer + writeOffset;
351 		S9xMixSamples(curBuffer, singleBufferSamples);
352 		PushBuffer(singleBufferBytes, curBuffer, NULL);
353 		writeOffset += singleBufferBytes;
354 		writeOffset %= sum_bufferSize;
355 		availableSamples -= singleBufferSamples;
356 	}
357 
358 	// need to check this is less than a single buffer, otherwise we have a race condition with bufferCount
359 	if (availableSamples > 0 && availableSamples < singleBufferSamples && bufferCount < blockCount) {
360 		S9xMixSamples(soundBuffer + writeOffset, availableSamples);
361 		partialOffset = availableSamples << 1;
362 		assert(partialOffset < singleBufferBytes);
363 	}
364 }
365 
366 /*  CXAudio2::GetDeviceList
367 get a list of the available output devices
368 -----
369 returns a vector of display names
370 */
GetDeviceList()371 std::vector<std::wstring> CXAudio2::GetDeviceList()
372 {
373     std::vector<std::wstring> device_list;
374 
375     if (pXAudio2)
376     {
377         UINT32 num_devices;
378         pXAudio2->GetDeviceCount(&num_devices);
379 
380         device_list.push_back(_T("Default"));
381 
382         for (unsigned int i = 0; i < num_devices; i++)
383         {
384             XAUDIO2_DEVICE_DETAILS device_details;
385             if (SUCCEEDED(pXAudio2->GetDeviceDetails(i, &device_details)))
386             {
387                 device_list.push_back(device_details.DisplayName);
388             }
389         }
390     }
391 
392     return device_list;
393 }
394 
395 /*  CXAudio2::FindDeviceIndex
396 find a device name in the list of possible output devices
397 -----
398 returns the index in the device list returned by GetDeviceList
399 */
FindDeviceIndex(TCHAR * audio_device)400 int CXAudio2::FindDeviceIndex(TCHAR *audio_device)
401 {
402     std::vector<std::wstring> device_list = GetDeviceList();
403 
404     int index = 0;
405 
406     for (int i = 0; i < device_list.size(); i++)
407     {
408         if (_tcsstr(device_list[i].c_str(), audio_device) != NULL)
409         {
410             index = i;
411             break;
412         }
413     }
414 
415     return index;
416 }
417