1 /*****************************************************************************
2  * winstore.c : Windows Multimedia Device API audio output plugin for VLC
3  *****************************************************************************
4  * Copyright (C) 2012 Rémi Denis-Courmont
5  *
6  * This program is free software; you can redistribute it and/or modify it
7  * under the terms of the GNU Lesser General Public License as published by
8  * the Free Software Foundation; either version 2.1 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  * GNU Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public License
17  * along with this program; if not, write to the Free Software Foundation,
18  * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
19  *****************************************************************************/
20 
21 #ifdef HAVE_CONFIG_H
22 # include <config.h>
23 #endif
24 
25 #define INITGUID
26 #define COBJMACROS
27 #define CONST_VTABLE
28 
29 #include <audiopolicy.h>
30 
31 #include <vlc_common.h>
32 #include <vlc_plugin.h>
33 #include <vlc_aout.h>
34 #include <vlc_charset.h> // ToWide
35 #include <vlc_modules.h>
36 #include "audio_output/mmdevice.h"
37 
38 #include <audioclient.h>
39 #include <mmdeviceapi.h>
40 
41 DEFINE_GUID (GUID_VLC_AUD_OUT, 0x4533f59d, 0x59ee, 0x00c6,
42    0xad, 0xb2, 0xc6, 0x8b, 0x50, 0x1a, 0x66, 0x55);
43 
EnterMTA(void)44 static void EnterMTA(void)
45 {
46     HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
47     if (unlikely(FAILED(hr)))
48         abort();
49 }
50 
LeaveMTA(void)51 static void LeaveMTA(void)
52 {
53     CoUninitialize();
54 }
55 
56 struct aout_sys_t
57 {
58     aout_stream_t *stream; /**< Underlying audio output stream */
59     module_t *module;
60     IAudioClient *client;
61     wchar_t* acquired_device;
62     wchar_t* requested_device;
63     wchar_t* default_device; // read once on open
64 
65     float gain;
66 
67     // IActivateAudioInterfaceCompletionHandler interface
68     IActivateAudioInterfaceCompletionHandler client_locator;
69     vlc_sem_t async_completed;
70     LONG refs;
71     CRITICAL_SECTION lock;
72 };
73 
74 
75 /* MMDeviceLocator IUnknown methods */
MMDeviceLocator_AddRef(IActivateAudioInterfaceCompletionHandler * This)76 static STDMETHODIMP_(ULONG) MMDeviceLocator_AddRef(IActivateAudioInterfaceCompletionHandler *This)
77 {
78     aout_sys_t *sys = container_of(This, aout_sys_t, client_locator);
79     return InterlockedIncrement(&sys->refs);
80 }
81 
MMDeviceLocator_Release(IActivateAudioInterfaceCompletionHandler * This)82 static STDMETHODIMP_(ULONG) MMDeviceLocator_Release(IActivateAudioInterfaceCompletionHandler *This)
83 {
84     aout_sys_t *sys = container_of(This, aout_sys_t, client_locator);
85     return InterlockedDecrement(&sys->refs);
86 }
87 
MMDeviceLocator_QueryInterface(IActivateAudioInterfaceCompletionHandler * This,REFIID riid,void ** ppv)88 static STDMETHODIMP MMDeviceLocator_QueryInterface(IActivateAudioInterfaceCompletionHandler *This,
89                                                    REFIID riid, void **ppv)
90 {
91     if( IsEqualIID(riid, &IID_IUnknown) ||
92         IsEqualIID(riid, &IID_IActivateAudioInterfaceCompletionHandler) )
93     {
94         MMDeviceLocator_AddRef(This);
95         *ppv = This;
96         return S_OK;
97     }
98     *ppv = NULL;
99     if( IsEqualIID(riid, &IID_IAgileObject) )
100     {
101         return S_OK;
102     }
103     return ResultFromScode( E_NOINTERFACE );
104 }
105 
106 /* MMDeviceLocator IActivateAudioInterfaceCompletionHandler methods */
MMDeviceLocator_ActivateCompleted(IActivateAudioInterfaceCompletionHandler * This,IActivateAudioInterfaceAsyncOperation * operation)107 static HRESULT MMDeviceLocator_ActivateCompleted(IActivateAudioInterfaceCompletionHandler *This,
108                                                  IActivateAudioInterfaceAsyncOperation *operation)
109 {
110     (void)operation;
111     aout_sys_t *sys = container_of(This, aout_sys_t, client_locator);
112     vlc_sem_post( &sys->async_completed );
113     return S_OK;
114 }
115 
116 /* MMDeviceLocator vtable */
117 static const struct IActivateAudioInterfaceCompletionHandlerVtbl MMDeviceLocator_vtable =
118 {
119     MMDeviceLocator_QueryInterface,
120     MMDeviceLocator_AddRef,
121     MMDeviceLocator_Release,
122 
123     MMDeviceLocator_ActivateCompleted,
124 };
125 
WaitForAudioClient(audio_output_t * aout)126 static void WaitForAudioClient(audio_output_t *aout)
127 {
128     aout_sys_t *sys = aout->sys;
129     IActivateAudioInterfaceAsyncOperation* asyncOp = NULL;
130 
131     const wchar_t* devId = sys->requested_device ? sys->requested_device : sys->default_device;
132 
133     assert(sys->refs == 0);
134     sys->refs = 0;
135     assert(sys->client == NULL);
136     sys->client = NULL;
137     free(sys->acquired_device);
138     sys->acquired_device = NULL;
139     ActivateAudioInterfaceAsync(devId, &IID_IAudioClient, NULL, &sys->client_locator, &asyncOp);
140 
141     vlc_sem_wait( &sys->async_completed );
142 
143     if (asyncOp)
144     {
145         HRESULT hr;
146         HRESULT hrActivateResult;
147         IUnknown *audioInterface;
148 
149         hr = IActivateAudioInterfaceAsyncOperation_GetActivateResult(asyncOp, &hrActivateResult, &audioInterface);
150         IActivateAudioInterfaceAsyncOperation_Release(asyncOp);
151         if (unlikely(FAILED(hr)))
152             msg_Dbg(aout, "Failed to get the activation result. (hr=0x%lX)", hr);
153         else if (FAILED(hrActivateResult))
154             msg_Dbg(aout, "Failed to activate the device. (hr=0x%lX)", hr);
155         else if (unlikely(audioInterface == NULL))
156             msg_Dbg(aout, "Failed to get the device instance.");
157         else
158         {
159             hr = IUnknown_QueryInterface(audioInterface, &IID_IAudioClient, (void**)&sys->client);
160             IUnknown_Release(audioInterface);
161             if (unlikely(FAILED(hr)))
162                 msg_Warn(aout, "The received interface is not a IAudioClient. (hr=0x%lX)", hr);
163             else
164             {
165                 sys->acquired_device = wcsdup(devId);
166 
167                 char *report = FromWide(devId);
168                 if (likely(report))
169                 {
170                     aout_DeviceReport(aout, report);
171                     free(report);
172                 }
173 
174                 IAudioClient2 *audioClient2;
175                 if (SUCCEEDED(IAudioClient_QueryInterface(sys->client, &IID_IAudioClient2, (void**)&audioClient2))
176                     && audioClient2)
177                 {
178                     // "BackgroundCapableMedia" does not work in UWP
179                     AudioClientProperties props = (AudioClientProperties) {
180                         .cbSize = sizeof(props),
181                         .bIsOffload = FALSE,
182                         .eCategory = AudioCategory_Movie,
183                         .Options = AUDCLNT_STREAMOPTIONS_NONE
184                     };
185                     if (FAILED(IAudioClient2_SetClientProperties(audioClient2, &props))) {
186                         msg_Dbg(aout, "Failed to set audio client properties");
187                     }
188                     IAudioClient2_Release(audioClient2);
189                 }
190             }
191         }
192     }
193 }
194 
SetRequestedDevice(audio_output_t * aout,wchar_t * id)195 static bool SetRequestedDevice(audio_output_t *aout, wchar_t *id)
196 {
197     aout_sys_t* sys = aout->sys;
198     if (sys->requested_device != id)
199     {
200         if (sys->requested_device != sys->default_device)
201             free(sys->requested_device);
202         sys->requested_device = id;
203         return true;
204     }
205     return false;
206 }
207 
DeviceRequestLocked(audio_output_t * aout)208 static int DeviceRequestLocked(audio_output_t *aout)
209 {
210     aout_sys_t *sys = aout->sys;
211     assert(sys->requested_device);
212 
213     WaitForAudioClient(aout);
214 
215     if (sys->stream != NULL && sys->client != NULL)
216         /* Request restart of stream with the new device */
217         aout_RestartRequest(aout, AOUT_RESTART_OUTPUT);
218     return (sys->client != NULL) ? 0 : -1;
219 }
220 
DeviceRestartLocked(audio_output_t * aout)221 static int DeviceRestartLocked(audio_output_t *aout)
222 {
223     aout_sys_t *sys = aout->sys;
224 
225     if (sys->client)
226     {
227         assert(sys->acquired_device);
228         free(sys->acquired_device);
229         sys->acquired_device = NULL;
230         IAudioClient_Release(sys->client);
231         sys->client = NULL;
232     }
233 
234     return DeviceRequestLocked(aout);
235 }
236 
DeviceSelectLocked(audio_output_t * aout,const char * id)237 static int DeviceSelectLocked(audio_output_t *aout, const char* id)
238 {
239     aout_sys_t *sys = aout->sys;
240     bool changed;
241     if( id == NULL )
242     {
243         changed = SetRequestedDevice(aout, sys->default_device);
244     }
245     else
246     {
247         wchar_t *requested_device = ToWide(id);
248         if (unlikely(requested_device == NULL))
249             return VLC_ENOMEM;
250         changed = SetRequestedDevice(aout, requested_device);
251     }
252     if (!changed)
253         return VLC_EGENERIC;
254     return DeviceRestartLocked(aout);
255 }
256 
DeviceSelect(audio_output_t * aout,const char * id)257 static int DeviceSelect(audio_output_t *aout, const char* id)
258 {
259     aout_sys_t *sys = aout->sys;
260     EnterCriticalSection(&sys->lock);
261     int ret = DeviceSelectLocked(aout, id);
262     LeaveCriticalSection(&sys->lock);
263     return ret;
264 }
265 
ResetInvalidatedClient(audio_output_t * aout,HRESULT hr)266 static void ResetInvalidatedClient(audio_output_t *aout, HRESULT hr)
267 {
268     /* Select the default device (and restart) on unplug */
269     if (unlikely(hr == AUDCLNT_E_DEVICE_INVALIDATED ||
270                  hr == AUDCLNT_E_RESOURCES_INVALIDATED))
271     {
272         // Select the default device (and restart) on unplug
273         DeviceSelect(aout, NULL);
274     }
275 }
276 
VolumeSet(audio_output_t * aout,float vol)277 static int VolumeSet(audio_output_t *aout, float vol)
278 {
279     aout_sys_t *sys = aout->sys;
280     if( unlikely( sys->client == NULL ) )
281         return VLC_EGENERIC;
282     HRESULT hr;
283     ISimpleAudioVolume *pc_AudioVolume = NULL;
284 
285     float linear_vol = vol * vol * vol; /* ISimpleAudioVolume is tapered linearly. */
286 
287     if (linear_vol > 1.f)
288     {
289         sys->gain = linear_vol;
290         linear_vol = 1.f;
291     }
292     else
293         sys->gain = 1.f;
294 
295     aout_GainRequest(aout, sys->gain);
296 
297     hr = IAudioClient_GetService(sys->client, &IID_ISimpleAudioVolume, &pc_AudioVolume);
298     if (FAILED(hr))
299     {
300         msg_Err(aout, "cannot get volume service (error 0x%lx)", hr);
301         goto done;
302     }
303 
304     hr = ISimpleAudioVolume_SetMasterVolume(pc_AudioVolume, linear_vol, NULL);
305     if (FAILED(hr))
306     {
307         msg_Err(aout, "cannot set volume (error 0x%lx)", hr);
308         goto done;
309     }
310 
311     aout_VolumeReport(aout, vol);
312 
313 done:
314     if (pc_AudioVolume)
315         ISimpleAudioVolume_Release(pc_AudioVolume);
316 
317     return SUCCEEDED(hr) ? 0 : -1;
318 }
319 
MuteSet(audio_output_t * aout,bool mute)320 static int MuteSet(audio_output_t *aout, bool mute)
321 {
322     aout_sys_t *sys = aout->sys;
323     if( unlikely( sys->client == NULL ) )
324         return VLC_EGENERIC;
325     HRESULT hr;
326     ISimpleAudioVolume *pc_AudioVolume = NULL;
327 
328     hr = IAudioClient_GetService(sys->client, &IID_ISimpleAudioVolume, &pc_AudioVolume);
329     if (FAILED(hr))
330     {
331         msg_Err(aout, "cannot get volume service (error 0x%lx)", hr);
332         goto done;
333     }
334 
335     hr = ISimpleAudioVolume_SetMute(pc_AudioVolume, mute, NULL);
336     if (FAILED(hr))
337     {
338         msg_Err(aout, "cannot set mute (error 0x%lx)", hr);
339         goto done;
340     }
341 
342     float vol;
343     hr = ISimpleAudioVolume_GetMasterVolume(pc_AudioVolume, &vol);
344     if (FAILED(hr))
345     {
346         msg_Err(aout, "cannot get volume (error 0x%lX)", hr);
347         goto done;
348     }
349 
350     aout_VolumeReport(aout, cbrtf(vol * sys->gain));
351     aout_MuteReport(aout, mute);
352 
353 done:
354     if (pc_AudioVolume)
355         ISimpleAudioVolume_Release(pc_AudioVolume);
356 
357     return SUCCEEDED(hr) ? 0 : -1;
358 }
359 
TimeGet(audio_output_t * aout,mtime_t * restrict delay)360 static int TimeGet(audio_output_t *aout, mtime_t *restrict delay)
361 {
362     aout_sys_t *sys = aout->sys;
363     if( unlikely( sys->client == NULL ) )
364         return VLC_EGENERIC;
365     HRESULT hr;
366 
367     EnterMTA();
368     hr = aout_stream_TimeGet(sys->stream, delay);
369     LeaveMTA();
370 
371     return SUCCEEDED(hr) ? 0 : -1;
372 }
373 
Play(audio_output_t * aout,block_t * block)374 static void Play(audio_output_t *aout, block_t *block)
375 {
376     aout_sys_t *sys = aout->sys;
377     if( unlikely( sys->client == NULL ) )
378         return;
379 
380     EnterMTA();
381     HRESULT hr = aout_stream_Play(sys->stream, block);
382     LeaveMTA();
383 
384     ResetInvalidatedClient(aout, hr);
385 }
386 
Pause(audio_output_t * aout,bool paused,mtime_t date)387 static void Pause(audio_output_t *aout, bool paused, mtime_t date)
388 {
389     aout_sys_t *sys = aout->sys;
390     if( unlikely( sys->client == NULL ) )
391         return;
392 
393     EnterMTA();
394     HRESULT hr = aout_stream_Pause(sys->stream, paused);
395     LeaveMTA();
396 
397     (void) date;
398     ResetInvalidatedClient(aout, hr);
399 }
400 
Flush(audio_output_t * aout,bool wait)401 static void Flush(audio_output_t *aout, bool wait)
402 {
403     aout_sys_t *sys = aout->sys;
404     if( unlikely( sys->client == NULL ) )
405         return;
406 
407     EnterMTA();
408     HRESULT hr = aout_stream_Flush(sys->stream, wait);
409     LeaveMTA();
410 
411     ResetInvalidatedClient(aout, hr);
412 }
413 
ActivateDevice(void * opaque,REFIID iid,PROPVARIANT * actparms,void ** restrict pv)414 static HRESULT ActivateDevice(void *opaque, REFIID iid, PROPVARIANT *actparms,
415                               void **restrict pv)
416 {
417     IAudioClient *client = opaque;
418 
419     if (!IsEqualIID(iid, &IID_IAudioClient))
420         return E_NOINTERFACE;
421     if (actparms != NULL || client == NULL )
422         return E_INVALIDARG;
423 
424     IAudioClient_AddRef(client); // as would IMMDevice_Activate do
425     *pv = opaque;
426 
427     return S_OK;
428 }
429 
aout_stream_Start(void * func,va_list ap)430 static int aout_stream_Start(void *func, va_list ap)
431 {
432     aout_stream_start_t start = func;
433     aout_stream_t *s = va_arg(ap, aout_stream_t *);
434     audio_sample_format_t *fmt = va_arg(ap, audio_sample_format_t *);
435     HRESULT *hr = va_arg(ap, HRESULT *);
436 
437     *hr = start(s, fmt, &GUID_VLC_AUD_OUT);
438     return SUCCEEDED(*hr) ? VLC_SUCCESS : VLC_EGENERIC;
439 }
440 
aout_stream_Stop(void * func,va_list ap)441 static void aout_stream_Stop(void *func, va_list ap)
442 {
443     aout_stream_stop_t stop = func;
444     aout_stream_t *s = va_arg(ap, aout_stream_t *);
445 
446     stop(s);
447 }
448 
Start(audio_output_t * aout,audio_sample_format_t * restrict fmt)449 static int Start(audio_output_t *aout, audio_sample_format_t *restrict fmt)
450 {
451     aout_sys_t *sys = aout->sys;
452     HRESULT hr;
453 
454     aout_stream_t *s = vlc_object_create(aout, sizeof (*s));
455     if (unlikely(s == NULL))
456         return -1;
457 
458     // Load the "out stream" for the requested device
459     EnterMTA();
460     EnterCriticalSection(&sys->lock);
461 
462     if (sys->requested_device != NULL)
463     {
464         if (sys->acquired_device == NULL || wcscmp(sys->acquired_device, sys->requested_device))
465         {
466             // we have a pending request for a new device
467             DeviceRestartLocked(aout);
468             if (sys->client == NULL)
469             {
470                 LeaveCriticalSection(&sys->lock);
471                 LeaveMTA();
472                 vlc_object_release(s);
473                 return -1;
474             }
475         }
476     }
477 
478     s->owner.activate = ActivateDevice;
479     for (;;)
480     {
481         s->owner.device = sys->client;
482         sys->module = vlc_module_load(s, "aout stream", NULL, false,
483                                       aout_stream_Start, s, fmt, &hr);
484 
485         int ret = -1;
486         if (hr == AUDCLNT_E_DEVICE_INVALIDATED)
487         {
488             // the requested device is not usable, try the default device
489             ret = DeviceSelectLocked(aout, NULL);
490         }
491         else if (hr == AUDCLNT_E_ALREADY_INITIALIZED)
492         {
493             /* From MSDN: "If the initial call to Initialize fails, subsequent
494              * Initialize calls might fail and return error code
495              * E_ALREADY_INITIALIZED, even though the interface has not been
496              * initialized. If this occurs, release the IAudioClient interface
497              * and obtain a new IAudioClient interface from the MMDevice API
498              * before calling Initialize again."
499              *
500              * Therefore, request to MMThread the same device and try again. */
501 
502             ret = DeviceRestartLocked(aout);
503         }
504         if (ret != VLC_SUCCESS)
505             break;
506 
507         if (sys->client == NULL || sys->module != NULL)
508             break;
509     }
510 
511     LeaveCriticalSection(&sys->lock);
512     LeaveMTA();
513 
514     if (sys->module == NULL)
515     {
516         vlc_object_release(s);
517         return -1;
518     }
519 
520     if (sys->client)
521     {
522         // the requested device has been used, reset it
523         // we keep the corresponding sys->client until a new request is started
524         SetRequestedDevice(aout, NULL);
525     }
526 
527     assert (sys->stream == NULL);
528     sys->stream = s;
529     return 0;
530 }
531 
Stop(audio_output_t * aout)532 static void Stop(audio_output_t *aout)
533 {
534     aout_sys_t *sys = aout->sys;
535 
536     assert (sys->stream != NULL);
537 
538     EnterMTA();
539     vlc_module_unload(sys->stream, sys->module, aout_stream_Stop, sys->stream);
540     LeaveMTA();
541 
542     vlc_object_release(sys->stream);
543     sys->stream = NULL;
544 }
545 
Open(vlc_object_t * obj)546 static int Open(vlc_object_t *obj)
547 {
548     audio_output_t *aout = (audio_output_t *)obj;
549 
550     aout_sys_t *sys = malloc(sizeof (*sys));
551     if (unlikely(sys == NULL))
552         return VLC_ENOMEM;
553 
554     if (unlikely(FAILED(StringFromIID(&DEVINTERFACE_AUDIO_RENDER, &sys->default_device))))
555     {
556         msg_Dbg(obj, "Failed to get the default renderer string");
557         free(sys);
558         return VLC_EGENERIC;
559     }
560 
561     char *psz_default = FromWide(sys->default_device);
562     if (likely(psz_default != NULL))
563     {
564         aout_HotplugReport(aout, psz_default, _("Default"));
565         free(psz_default);
566     }
567 
568     InitializeCriticalSection(&sys->lock);
569 
570     vlc_sem_init(&sys->async_completed, 0);
571     sys->refs = 0;
572     sys->requested_device = sys->default_device;
573     sys->acquired_device = NULL;
574     sys->client_locator = (IActivateAudioInterfaceCompletionHandler) { &MMDeviceLocator_vtable };
575     sys->gain = 1.f;
576 
577     aout->sys = sys;
578     sys->stream = NULL;
579     sys->client = NULL;
580     aout->start = Start;
581     aout->stop = Stop;
582     aout->time_get = TimeGet;
583     aout->volume_set = VolumeSet;
584     aout->mute_set = MuteSet;
585     aout->play = Play;
586     aout->pause = Pause;
587     aout->flush = Flush;
588     aout->device_select = DeviceSelect;
589     return VLC_SUCCESS;
590 }
591 
Close(vlc_object_t * obj)592 static void Close(vlc_object_t *obj)
593 {
594     audio_output_t *aout = (audio_output_t *)obj;
595     aout_sys_t *sys = aout->sys;
596 
597     if(sys->client != NULL)
598         IAudioClient_Release(sys->client);
599 
600     assert(sys->refs == 0);
601 
602     free(sys->acquired_device);
603     if (sys->requested_device != sys->default_device)
604         free(sys->requested_device);
605     CoTaskMemFree(sys->default_device);
606     DeleteCriticalSection(&sys->lock);
607 
608     free(sys);
609 }
610 
611 vlc_module_begin()
612     set_shortname("winstore")
613     set_description("Windows Store audio output")
614     set_capability("audio output", 0)
615     set_category(CAT_AUDIO)
616     set_subcategory(SUBCAT_AUDIO_AOUT)
617     add_shortcut("wasapi")
618     set_callbacks(Open, Close)
619 vlc_module_end()
620