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 }