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