1 module soundtab.audio.playback; 2 3 import core.sys.windows.windows; 4 import core.sys.windows.objidl; 5 import core.sys.windows.wtypes; 6 import wasapi.coreaudio; 7 import wasapi.comutils; 8 import dlangui.core.logger; 9 import std..string; 10 import core.thread; 11 import soundtab.audio.audiosource; 12 13 HRESULT GetStreamFormat(AUDCLNT_SHAREMODE mode, IAudioClient _audioClient, ref WAVEFORMATEXTENSIBLE mixFormat) { 14 HRESULT hr; 15 WAVEFORMATEXTENSIBLE format; 16 format.cbSize = WAVEFORMATEXTENSIBLE.sizeof; 17 format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; 18 format.wBitsPerSample = 32; 19 20 int[] sampleRates = [48000, 96000, 44100, 192000]; 21 22 // FLOAT 23 format.nChannels = 2; 24 format.wValidBitsPerSample = 32; 25 format.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT; 26 format.SubFormat = MEDIASUBTYPE_IEEE_FLOAT; 27 foreach(rate; sampleRates) { 28 format.nSamplesPerSec = rate; 29 format.nAvgBytesPerSec = format.nSamplesPerSec * format.nChannels * format.wBitsPerSample / 8; 30 format.nBlockAlign = cast(WORD)(format.wBitsPerSample * format.nChannels / 8); 31 WAVEFORMATEXTENSIBLE * match; 32 hr = _audioClient.IsFormatSupported(mode, cast(WAVEFORMATEX*)&format, cast(WAVEFORMATEX**)&match); 33 if (hr == S_OK || hr == S_FALSE) { 34 if (!match) 35 match = &format; 36 if ((*match).wFormatTag == WAVE_FORMAT_EXTENSIBLE) { 37 mixFormat = *match; 38 } else { 39 mixFormat.Format = match.Format; 40 } 41 Log.d("Found supported FLOAT format: samplesPerSec=", mixFormat.nSamplesPerSec, " nChannels=", mixFormat.nChannels, " bitsPerSample=", mixFormat.wBitsPerSample); 42 return S_OK; 43 } 44 } 45 46 // PCM 32 47 format.wValidBitsPerSample = 32; 48 format.wBitsPerSample = 32; 49 format.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT; 50 format.SubFormat = MEDIASUBTYPE_PCM; 51 foreach(rate; sampleRates) { 52 format.nSamplesPerSec = rate; 53 format.nAvgBytesPerSec = format.nSamplesPerSec * format.nChannels * format.wBitsPerSample / 8; 54 format.nBlockAlign = cast(WORD)(format.wBitsPerSample * format.nChannels / 8); 55 WAVEFORMATEXTENSIBLE * match; 56 hr = _audioClient.IsFormatSupported(mode, cast(WAVEFORMATEX*)&format, cast(WAVEFORMATEX**)&match); 57 if (hr == S_OK || hr == S_FALSE) { 58 if (!match) 59 match = &format; 60 if ((*match).wFormatTag == WAVE_FORMAT_EXTENSIBLE) { 61 mixFormat = *match; 62 } else { 63 mixFormat.Format = match.Format; 64 } 65 Log.d("Found supported PCM32 format: samplesPerSec=", mixFormat.nSamplesPerSec, " nChannels=", mixFormat.nChannels, " bitsPerSample=", mixFormat.wBitsPerSample); 66 return S_OK; 67 } 68 } 69 70 // PCM 24 71 format.wValidBitsPerSample = 24; 72 format.wBitsPerSample = 32; 73 format.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT; 74 format.SubFormat = MEDIASUBTYPE_PCM; 75 foreach(rate; sampleRates) { 76 format.nSamplesPerSec = rate; 77 format.nAvgBytesPerSec = format.nSamplesPerSec * format.nChannels * format.wBitsPerSample / 8; 78 format.nBlockAlign = cast(WORD)(format.wBitsPerSample * format.nChannels / 8); 79 WAVEFORMATEXTENSIBLE * match; 80 hr = _audioClient.IsFormatSupported(mode, cast(WAVEFORMATEX*)&format, cast(WAVEFORMATEX**)&match); 81 if (hr == S_OK || hr == S_FALSE) { 82 if (!match) 83 match = &format; 84 if ((*match).wFormatTag == WAVE_FORMAT_EXTENSIBLE) { 85 mixFormat = *match; 86 } else { 87 mixFormat.Format = match.Format; 88 } 89 Log.d("Found supported PCM24 format: samplesPerSec=", mixFormat.nSamplesPerSec, " nChannels=", mixFormat.nChannels, " bitsPerSample=", mixFormat.wBitsPerSample); 90 return S_OK; 91 } 92 } 93 94 // PCM 16 95 format.wValidBitsPerSample = 16; 96 format.wBitsPerSample = 16; 97 format.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT; 98 format.SubFormat = MEDIASUBTYPE_PCM; 99 foreach(rate; sampleRates) { 100 format.nSamplesPerSec = rate; 101 format.nAvgBytesPerSec = format.nSamplesPerSec * format.nChannels * format.wBitsPerSample / 8; 102 format.nBlockAlign = cast(WORD)(format.wBitsPerSample * format.nChannels / 8); 103 WAVEFORMATEXTENSIBLE * match; 104 hr = _audioClient.IsFormatSupported(mode, cast(WAVEFORMATEX*)&format, cast(WAVEFORMATEX**)&match); 105 if (hr == S_OK || hr == S_FALSE) { 106 if (!match) 107 match = &format; 108 if ((*match).wFormatTag == WAVE_FORMAT_EXTENSIBLE) { 109 mixFormat = *match; 110 } else { 111 mixFormat.Format = match.Format; 112 } 113 Log.d("Found supported PCM16 format: samplesPerSec=", mixFormat.nSamplesPerSec, " nChannels=", mixFormat.nChannels, " bitsPerSample=", mixFormat.wBitsPerSample); 114 return S_OK; 115 } 116 } 117 118 119 120 format.cbSize = WAVEFORMATEX.sizeof; 121 format.wFormatTag = WAVE_FORMAT_PCM; 122 format.wBitsPerSample = 16; 123 foreach(rate; sampleRates) { 124 format.nSamplesPerSec = rate; 125 format.nAvgBytesPerSec = format.nSamplesPerSec * format.nChannels * format.wBitsPerSample / 8; 126 format.nBlockAlign = cast(WORD)(format.wBitsPerSample * format.nChannels / 8); 127 format.SubFormat = MEDIASUBTYPE_IEEE_FLOAT; 128 WAVEFORMATEXTENSIBLE * match; 129 hr = _audioClient.IsFormatSupported(mode, cast(WAVEFORMATEX*)&format, cast(WAVEFORMATEX**)&match); 130 if (hr == S_OK || hr == S_FALSE) { 131 if (!match) 132 match = &format; 133 if ((*match).wFormatTag == WAVE_FORMAT_EXTENSIBLE) { 134 mixFormat = *match; 135 } else { 136 mixFormat.Format = match.Format; 137 } 138 Log.d("Found supported format: samplesPerSec=", mixFormat.nSamplesPerSec, " nChannels=", mixFormat.nChannels, " bitsPerSample=", mixFormat.wBitsPerSample); 139 return S_OK; 140 } 141 } 142 return E_FAIL; 143 } 144 145 /// audio playback thread 146 /// call start() to enable thread 147 /// use paused property to pause thread 148 class AudioPlayback : Thread { 149 150 import core.sync.mutex; 151 private Mutex _mutex; 152 void lock() { _mutex.lock(); } 153 void unlock() { _mutex.unlock(); } 154 155 this() { 156 super(&run); 157 _mutex = new Mutex(); 158 _devices = new MMDevices(); 159 _devices.init(); 160 //MMDevice[] devices = getDevices(); 161 //if (devices.length > 0) 162 // setDevice(devices[0]); 163 } 164 ~this() { 165 stop(); 166 if (_devices) { 167 destroy(_devices); 168 _devices = null; 169 } 170 } 171 private AudioSource _synth; 172 private MMDevices _devices; 173 void setSynth(AudioSource synth) { 174 lockedPausedAction({ 175 _synth = synth; 176 }); 177 } 178 179 private bool _running; 180 private bool _paused; 181 private bool _stopped; 182 183 private string _stateString = "No device selected"; 184 @property string stateString() { 185 lock(); 186 scope(exit)unlock(); 187 return _stateString; 188 } 189 190 private MMDevice _requestedDevice; 191 private bool _requestedExclusive; 192 private int _requestedMinFrameMillis; 193 194 private void lockedPausedAction(void delegate() action) { 195 bool oldPaused = _paused; 196 { 197 lock(); 198 scope(exit)unlock(); 199 action(); 200 } 201 if (running) { 202 _paused = true; 203 // pause to apply changed settings 204 sleep(dur!"msecs"(20)); 205 _paused = oldPaused; 206 } 207 } 208 209 private void updateStateString(string deviceName, bool paused, bool exclusive, int bufferMillis) { 210 char[] res; 211 if (deviceName.length) { 212 res ~= deviceName; 213 if (paused) { 214 res ~= " [paused]"; 215 } else { 216 if (exclusive) 217 res ~= " [exclusive mode] "; 218 else 219 res ~= " [shared mode] "; 220 if (bufferMillis) { 221 import std.conv : to; 222 res ~= "buffer:"; 223 res ~= to!string(bufferMillis); 224 res ~= "ms"; 225 } 226 } 227 } else { 228 res ~= "[no playback device selected]"; 229 } 230 lock(); 231 scope(exit)unlock(); 232 _stateString = res.dup; 233 } 234 235 /// sets active device 236 public void setDevice(MMDevice device, bool exclusive = true, int minFrameMillis = 3) { 237 lockedPausedAction({ 238 _requestedDevice = device; 239 _requestedExclusive = exclusive; 240 _requestedMinFrameMillis = minFrameMillis; 241 }); 242 } 243 private MMDevice _currentDevice; 244 245 /// returns list of available devices, default is first 246 MMDevice[] getDevices() { 247 return _devices.getPlaybackDevices(); 248 } 249 250 /// returns true if playback thread is running 251 @property bool running() { return _running; } 252 /// get pause status 253 @property bool paused() { return _paused; } 254 /// play/stop 255 @property void paused(bool pausedFlag) { 256 if (_paused != pausedFlag) { 257 _paused = pausedFlag; 258 sleep(dur!"msecs"(10)); 259 } 260 } 261 262 void stop() { 263 if (_running) { 264 _stopped = true; 265 while (_running) 266 sleep(dur!"msecs"(10)); 267 join(false); 268 _running = false; 269 } 270 } 271 272 private ComAutoPtr!IAudioClient _audioClient; 273 274 275 /// returns true if hr is error 276 private bool checkError(HRESULT hr, string msg = "AUDIO ERROR") { 277 if (hr) { 278 Log.e(msg, " hresult=", "%08x".format(hr), " lastError=", GetLastError()); 279 return true; 280 } 281 return false; 282 } 283 284 WAVEFORMATEXTENSIBLE _format; 285 HRESULT SetFormat(AudioSource pMySource, WAVEFORMATEX * fmt) { 286 SampleFormat sampleFormat = SampleFormat.float32; 287 int samplesPerSecond = 44100; 288 int channels = 2; 289 int bitsPerSample = 16; 290 int blockAlign = 4; 291 if (fmt.wFormatTag == WAVE_FORMAT_EXTENSIBLE) { 292 WAVEFORMATEXTENSIBLE * formatEx = cast(WAVEFORMATEXTENSIBLE*)fmt; 293 _format = *formatEx; 294 sampleFormat = (_format.SubFormat == MEDIASUBTYPE_IEEE_FLOAT) ? SampleFormat.float32 : (_format.wBitsPerSample == 16 ? SampleFormat.signed16 : SampleFormat.signed32); 295 channels = _format.nChannels; 296 samplesPerSecond = _format.nSamplesPerSec; 297 bitsPerSample = _format.wBitsPerSample; 298 blockAlign = _format.nBlockAlign; 299 } else { 300 _format = *fmt; 301 sampleFormat = (_format.wFormatTag == WAVE_FORMAT_IEEE_FLOAT) ? SampleFormat.float32 : (_format.wBitsPerSample == 16 ? SampleFormat.signed16 : SampleFormat.signed32); 302 channels = _format.nChannels; 303 samplesPerSecond = _format.nSamplesPerSec; 304 bitsPerSample = _format.wBitsPerSample; 305 blockAlign = _format.nBlockAlign; 306 } 307 if (pMySource) 308 pMySource.setFormat(sampleFormat, channels, samplesPerSecond, bitsPerSample, blockAlign); 309 return S_OK; 310 } 311 312 private void playbackForDevice(MMDevice dev, bool exclusive, int minFrameMillis) { 313 Log.d("playbackForDevice ", dev); 314 AudioSource pMySource = _synth; 315 HANDLE hEvent, hTask; 316 if (!pMySource) 317 return; 318 if (!_currentDevice || _currentDevice.id != dev.id || _audioClient.isNull) { 319 // setting new device 320 _audioClient = _devices.getAudioClient(dev.id); 321 if (_audioClient.isNull) { 322 sleep(dur!"msecs"(10)); 323 return; 324 } 325 _currentDevice = dev; 326 } 327 if (_audioClient.isNull || _paused || _stopped) 328 return; 329 // current device is selected 330 UINT32 bufferSize; 331 REFERENCE_TIME defaultDevicePeriod, minimumDevicePeriod; 332 REFERENCE_TIME streamLatency; 333 WAVEFORMATEX * mixFormat; 334 HRESULT hr; 335 if(exclusive) { 336 // Call a helper function to negotiate with the audio 337 // device for an exclusive-mode stream format. 338 hr = GetStreamFormat(AUDCLNT_SHAREMODE.AUDCLNT_SHAREMODE_EXCLUSIVE, _audioClient, _format); 339 if (hr) { 340 return; 341 } 342 mixFormat = cast(WAVEFORMATEX*)&_format; 343 } else { 344 hr = _audioClient.GetMixFormat(mixFormat); 345 } 346 const REFTIMES_PER_SEC = 10000000; //10000000; 347 const REFTIMES_PER_MILLISEC = REFTIMES_PER_SEC / 1000; 348 //REFERENCE_TIME hnsRequestedDuration = REFTIMES_PER_SEC; 349 hr = _audioClient.GetDevicePeriod(defaultDevicePeriod, minimumDevicePeriod); 350 Log.d("defPeriod=", defaultDevicePeriod, " minPeriod=", minimumDevicePeriod); 351 if (exclusive) { 352 REFERENCE_TIME requestedPeriod = minimumDevicePeriod; 353 for(int n = 1; n < 10; n++) { 354 requestedPeriod = minimumDevicePeriod * n; 355 if (requestedPeriod >= minFrameMillis * 10000) 356 break; 357 } 358 Log.d("exclusive mode, requested period=", requestedPeriod); 359 hr = _audioClient.Initialize( 360 AUDCLNT_SHAREMODE.AUDCLNT_SHAREMODE_EXCLUSIVE, 361 AUDCLNT_STREAMFLAGS_EVENTCALLBACK, 362 requestedPeriod, //minimumDevicePeriod, //hnsRequestedDuration, 363 requestedPeriod, //hnsRequestedDuration, // 0 364 mixFormat, 365 null); 366 //updateStateString(dev.friendlyName, false, exclusive, cast(int)(requestedPeriod / 10000)); 367 } else { 368 hr = _audioClient.Initialize( 369 AUDCLNT_SHAREMODE.AUDCLNT_SHAREMODE_SHARED, 370 0, 371 defaultDevicePeriod, //minimumDevicePeriod, //hnsRequestedDuration, 372 0, //hnsRequestedDuration, // 0 373 mixFormat, 374 null); 375 } 376 if (checkError(hr, "AudioClient.Initialize failed")) return; 377 378 UINT32 bufferFrameCount; 379 380 hr = SetFormat(pMySource, mixFormat); 381 if (exclusive) { 382 hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); 383 hr = _audioClient.SetEventHandle(hEvent); 384 if (checkError(hr, "AudioClient.SetEventHandle failed")) return; 385 } 386 387 hr = _audioClient.GetBufferSize(bufferFrameCount); 388 if (checkError(hr, "AudioClient.GetBufferSize failed")) return; 389 //if (!exclusive) { 390 int millis = cast(int)(1000 * bufferFrameCount / mixFormat.nSamplesPerSec); 391 updateStateString(dev.friendlyName, false, exclusive, millis); 392 //} 393 Log.d("Buffer frame count: ", bufferFrameCount); 394 hr = _audioClient.GetStreamLatency(streamLatency); 395 if (checkError(hr, "AudioClient.GetStreamLatency failed")) return; 396 hr = _audioClient.GetDevicePeriod(defaultDevicePeriod, minimumDevicePeriod); 397 if (checkError(hr, "AudioClient.GetDevicePeriod failed")) return; 398 Log.d("Found audio client with bufferSize=", bufferFrameCount, " latency=", streamLatency, " defPeriod=", defaultDevicePeriod, " minPeriod=", minimumDevicePeriod); 399 ComAutoPtr!IAudioRenderClient pRenderClient; 400 hr = _audioClient.GetService( 401 IID_IAudioRenderClient, 402 cast(void**)&pRenderClient.ptr); 403 if (checkError(hr, "AudioClient.GetService failed")) return; 404 // Grab the entire buffer for the initial fill operation. 405 BYTE *pData; 406 DWORD flags; 407 hr = pRenderClient.GetBuffer(bufferFrameCount, pData); 408 if (checkError(hr, "RenderClient.GetBuffer failed")) return; 409 pMySource.loadData(bufferFrameCount, pData, flags); 410 hr = pRenderClient.ReleaseBuffer(bufferFrameCount, flags); 411 if (checkError(hr, "pRenderClient.ReleaseBuffer failed")) return; 412 // Calculate the actual duration of the allocated buffer. 413 REFERENCE_TIME hnsActualDuration; 414 hnsActualDuration = cast(long)REFTIMES_PER_SEC * bufferFrameCount / mixFormat.nSamplesPerSec; 415 416 417 // Ask MMCSS to temporarily boost the thread priority 418 // to reduce glitches while the low-latency stream plays. 419 if (exclusive) { 420 hTask = setHighThreadPriority(); 421 //hTask = cast(void*)1; //AvSetMmThreadCharacteristicsA("Pro Audio".ptr, taskIndex); 422 if (!hTask) { 423 hr = E_FAIL; 424 if (checkError(hr, "AvSetMmThreadCharacteristics() failed")) return; 425 } 426 } 427 428 hr = _audioClient.Start(); // Start playing. 429 if (checkError(hr, "audioClient.Start() failed")) return; 430 // Each loop fills about half of the shared buffer. 431 while (flags != AUDCLNT_BUFFERFLAGS.AUDCLNT_BUFFERFLAGS_SILENT) 432 { 433 if (_paused || _stopped) 434 break; 435 UINT32 numFramesAvailable; 436 UINT32 numFramesPadding; 437 438 if (exclusive) { 439 // Wait for next buffer event to be signaled. 440 DWORD retval = WaitForSingleObject(hEvent, 1000); 441 if (retval != WAIT_OBJECT_0) 442 { 443 // Event handle timed out after a 2-second wait. 444 break; 445 } 446 numFramesAvailable = bufferFrameCount; 447 } else { 448 449 // Sleep for half the buffer duration. 450 Sleep(cast(DWORD)(hnsActualDuration/REFTIMES_PER_MILLISEC/2)); 451 452 // See how much buffer space is available. 453 hr = _audioClient.GetCurrentPadding(numFramesPadding); 454 if (checkError(hr, "audioClient.GetCurrentPadding() failed")) break; 455 456 numFramesAvailable = bufferFrameCount - numFramesPadding; 457 } 458 459 460 // Grab all the available space in the shared buffer. 461 hr = pRenderClient.GetBuffer(numFramesAvailable, pData); 462 if (checkError(hr, "RenderClient.GetBuffer() failed")) break; 463 464 //Log.d("before loadData frames=", numFramesAvailable); 465 // Get next 1/2-second of data from the audio source. 466 hr = pMySource.loadData(numFramesAvailable, pData, flags); 467 //Log.d("after loadData"); 468 469 hr = pRenderClient.ReleaseBuffer(numFramesAvailable, flags); 470 if (checkError(hr, "RenderClient.ReleaseBuffer() failed")) break; 471 } 472 // Wait for last data in buffer to play before stopping. 473 Sleep(cast(DWORD)(hnsActualDuration/REFTIMES_PER_MILLISEC/2)); 474 hr = _audioClient.Stop(); 475 if (hEvent) 476 { 477 CloseHandle(hEvent); 478 } 479 restoreThreadPriority(hTask); 480 if (checkError(hr, "audioClient.Stop() failed")) return; 481 } 482 483 private void run() { 484 _running = true; 485 auto hr = CoInitialize(null); 486 priority = PRIORITY_MAX; 487 try { 488 while (!_stopped) { 489 MMDevice dev; 490 bool exclusive; 491 int minFrame; 492 { 493 lock(); 494 scope(exit)unlock(); 495 dev = _requestedDevice; 496 exclusive = _requestedExclusive; 497 minFrame = _requestedMinFrameMillis; 498 } 499 if (!dev) { 500 // waiting for device is set 501 sleep(dur!"msecs"(10)); 502 continue; 503 } 504 if (_paused) { 505 updateStateString(dev ? dev.friendlyName : null, true, exclusive, 0); 506 sleep(dur!"msecs"(10)); 507 continue; 508 } 509 if (_stopped) 510 break; 511 playbackForDevice(dev, exclusive, minFrame); 512 _audioClient.clear(); 513 } 514 } catch (Exception e) { 515 Log.e("Exception in playback thread"); 516 } 517 _running = false; 518 } 519 }