1 /*****************************************************************************
2 * mmdevice.c : Windows Multimedia Device API audio output plugin for VLC
3 *****************************************************************************
4 * Copyright (C) 2012 RĂ©mi Denis-Courmont
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.
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.
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 *****************************************************************************/
26 #define _WIN32_WINNT 0x600 /* Windows Vista */
33 #include <audiopolicy.h>
34 #include <mmdeviceapi.h>
36 DEFINE_PROPERTYKEY(PKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd,
37 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 14);
39 #include <vlc_common.h>
40 #include <vlc_plugin.h>
42 #include <vlc_charset.h>
43 #include "audio_output/mmdevice.h"
45 DEFINE_GUID (GUID_VLC_AUD_OUT, 0x4533f59d, 0x59ee, 0x00c6,
46 0xad, 0xb2, 0xc6, 0x8b, 0x50, 0x1a, 0x66, 0x55);
48 static int TryEnterMTA(vlc_object_t *obj)
50 HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
51 if (unlikely(FAILED(hr)))
53 msg_Err (obj, "cannot initialize COM (error 0x%lx)", hr);
58 #define TryEnterMTA(o) TryEnterMTA(VLC_OBJECT(o))
60 static void EnterMTA(void)
62 HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
63 if (unlikely(FAILED(hr)))
67 static void LeaveMTA(void)
75 aout_stream_t *stream; /**< Underlying audio output stream */
77 IMMDevice *dev; /**< Selected output device, NULL if none */
79 ISimpleAudioVolume *volume; /**< Volume setter */
82 IMMDeviceEnumerator *it; /**< Device enumerator, NULL when exiting */
83 /*TODO: IMMNotificationClient*/
85 IAudioSessionManager *manager; /**< Session for the output device */
86 struct IAudioSessionEvents session_events;
89 HANDLE device_changed; /**< Event to reset thread */
90 HANDLE device_ready; /**< Event when thread is reset */
91 vlc_thread_t thread; /**< Thread for audio session control */
95 /* NOTE: The Core Audio API documentation totally fails to specify the thread
96 * safety (or lack thereof) of the interfaces. This code takes the most
97 * restrictive assumption, no thread safety: The background thread (MMThread)
98 * only runs at specified times, namely between the device_ready and
99 * device_changed events (effectively, a thread barrier but only Windows 8
100 * provides thread barriers natively).
102 * The audio output owner (i.e. the audio output core) is responsible for
103 * serializing callbacks. This code only needs to be concerned with
104 * synchronization between the set of audio output callbacks, MMThread()
105 * and (trivially) the device and session notifications. */
107 static int vlc_FromHR(audio_output_t *aout, HRESULT hr)
109 /* Restart on unplug */
110 if (unlikely(hr == AUDCLNT_E_DEVICE_INVALIDATED))
111 aout_RestartRequest(aout, AOUT_RESTART_OUTPUT);
112 return SUCCEEDED(hr) ? 0 : -1;
115 /*** VLC audio output callbacks ***/
116 static int TimeGet(audio_output_t *aout, mtime_t *restrict delay)
118 aout_sys_t *sys = aout->sys;
122 hr = aout_stream_TimeGet(sys->stream, delay);
125 return SUCCEEDED(hr) ? 0 : -1;
128 static void Play(audio_output_t *aout, block_t *block)
130 aout_sys_t *sys = aout->sys;
134 hr = aout_stream_Play(sys->stream, block);
137 vlc_FromHR(aout, hr);
140 static void Pause(audio_output_t *aout, bool paused, mtime_t date)
142 aout_sys_t *sys = aout->sys;
146 hr = aout_stream_Pause(sys->stream, paused);
149 vlc_FromHR(aout, hr);
153 static void Flush(audio_output_t *aout, bool wait)
155 aout_sys_t *sys = aout->sys;
160 { /* Loosy drain emulation */
163 if (SUCCEEDED(aout_stream_TimeGet(sys->stream, &delay)))
164 Sleep((delay / (CLOCK_FREQ / 1000)) + 1);
167 aout_stream_Flush(sys->stream);
173 static int VolumeSet(audio_output_t *aout, float vol)
175 ISimpleAudioVolume *volume = aout->sys->volume;
179 if (TryEnterMTA(aout))
182 HRESULT hr = ISimpleAudioVolume_SetMasterVolume(volume, vol, NULL);
184 msg_Err(aout, "cannot set volume (error 0x%lx)", hr);
187 return FAILED(hr) ? -1 : 0;
190 static int MuteSet(audio_output_t *aout, bool mute)
192 ISimpleAudioVolume *volume = aout->sys->volume;
196 if (TryEnterMTA(aout))
199 HRESULT hr = ISimpleAudioVolume_SetMute(volume, mute ? TRUE : FALSE, NULL);
201 msg_Err(aout, "cannot set volume (error 0x%lx)", hr);
204 return FAILED(hr) ? -1 : 0;
207 #if !VLC_WINSTORE_APP
208 /*** Audio session events ***/
209 static inline aout_sys_t *vlc_AudioSessionEvents_sys(IAudioSessionEvents *this)
211 return (void *)(((char *)this) - offsetof(aout_sys_t, session_events));
215 vlc_AudioSessionEvents_QueryInterface(IAudioSessionEvents *this, REFIID riid,
218 if (IsEqualIID(riid, &IID_IUnknown)
219 || IsEqualIID(riid, &IID_IAudioSessionEvents))
222 IUnknown_AddRef(this);
228 return E_NOINTERFACE;
232 static STDMETHODIMP_(ULONG)
233 vlc_AudioSessionEvents_AddRef(IAudioSessionEvents *this)
235 aout_sys_t *sys = vlc_AudioSessionEvents_sys(this);
236 return InterlockedIncrement(&sys->refs);
239 static STDMETHODIMP_(ULONG)
240 vlc_AudioSessionEvents_Release(IAudioSessionEvents *this)
242 aout_sys_t *sys = vlc_AudioSessionEvents_sys(this);
243 return InterlockedDecrement(&sys->refs);
247 vlc_AudioSessionEvents_OnDisplayNameChanged(IAudioSessionEvents *this,
248 LPCWSTR wname, LPCGUID ctx)
250 aout_sys_t *sys = vlc_AudioSessionEvents_sys(this);
251 audio_output_t *aout = sys->aout;
253 msg_Dbg(aout, "display name changed: %ls", wname);
259 vlc_AudioSessionEvents_OnIconPathChanged(IAudioSessionEvents *this,
260 LPCWSTR wpath, LPCGUID ctx)
262 aout_sys_t *sys = vlc_AudioSessionEvents_sys(this);
263 audio_output_t *aout = sys->aout;
265 msg_Dbg(aout, "icon path changed: %ls", wpath);
271 vlc_AudioSessionEvents_OnSimpleVolumeChanged(IAudioSessionEvents *this,
272 float vol, WINBOOL mute,
275 aout_sys_t *sys = vlc_AudioSessionEvents_sys(this);
276 audio_output_t *aout = sys->aout;
278 msg_Dbg(aout, "simple volume changed: %f, muting %sabled", vol,
279 mute ? "en" : "dis");
280 aout_VolumeReport(aout, vol);
281 aout_MuteReport(aout, mute == TRUE);
287 vlc_AudioSessionEvents_OnChannelVolumeChanged(IAudioSessionEvents *this,
288 DWORD count, float *vols,
289 DWORD changed, LPCGUID ctx)
291 aout_sys_t *sys = vlc_AudioSessionEvents_sys(this);
292 audio_output_t *aout = sys->aout;
294 msg_Dbg(aout, "channel volume %lu of %lu changed: %f", changed, count,
301 vlc_AudioSessionEvents_OnGroupingParamChanged(IAudioSessionEvents *this,
302 LPCGUID param, LPCGUID ctx)
305 aout_sys_t *sys = vlc_AudioSessionEvents_sys(this);
306 audio_output_t *aout = sys->aout;
308 msg_Dbg(aout, "grouping parameter changed");
315 vlc_AudioSessionEvents_OnStateChanged(IAudioSessionEvents *this,
316 AudioSessionState state)
318 aout_sys_t *sys = vlc_AudioSessionEvents_sys(this);
319 audio_output_t *aout = sys->aout;
321 msg_Dbg(aout, "state changed: %d", state);
326 vlc_AudioSessionEvents_OnSessionDisconnected(IAudioSessionEvents *this,
327 AudioSessionDisconnectReason reason)
329 aout_sys_t *sys = vlc_AudioSessionEvents_sys(this);
330 audio_output_t *aout = sys->aout;
334 case DisconnectReasonDeviceRemoval:
335 msg_Warn(aout, "session disconnected: %s", "device removed");
337 case DisconnectReasonServerShutdown:
338 msg_Err(aout, "session disconnected: %s", "service stopped");
340 case DisconnectReasonFormatChanged:
341 msg_Warn(aout, "session disconnected: %s", "format changed");
343 case DisconnectReasonSessionLogoff:
344 msg_Err(aout, "session disconnected: %s", "user logged off");
346 case DisconnectReasonSessionDisconnected:
347 msg_Err(aout, "session disconnected: %s", "session disconnected");
349 case DisconnectReasonExclusiveModeOverride:
350 msg_Err(aout, "session disconnected: %s", "stream overriden");
353 msg_Warn(aout, "session disconnected: unknown reason %d", reason);
356 /* NOTE: audio decoder thread should get invalidated device and restart */
360 static const struct IAudioSessionEventsVtbl vlc_AudioSessionEvents =
362 vlc_AudioSessionEvents_QueryInterface,
363 vlc_AudioSessionEvents_AddRef,
364 vlc_AudioSessionEvents_Release,
366 vlc_AudioSessionEvents_OnDisplayNameChanged,
367 vlc_AudioSessionEvents_OnIconPathChanged,
368 vlc_AudioSessionEvents_OnSimpleVolumeChanged,
369 vlc_AudioSessionEvents_OnChannelVolumeChanged,
370 vlc_AudioSessionEvents_OnGroupingParamChanged,
371 vlc_AudioSessionEvents_OnStateChanged,
372 vlc_AudioSessionEvents_OnSessionDisconnected,
376 /*** Initialization / deinitialization **/
377 static wchar_t *var_InheritWide(vlc_object_t *obj, const char *name)
379 char *v8 = var_InheritString(obj, name);
383 wchar_t *v16 = ToWide(v8);
387 #define var_InheritWide(o,n) var_InheritWide(VLC_OBJECT(o),n)
389 static void MMSession(audio_output_t *aout, aout_sys_t *sys)
391 IAudioSessionControl *control;
394 /* Register session control */
395 if (sys->manager != NULL)
397 hr = IAudioSessionManager_GetSimpleAudioVolume(sys->manager,
399 FALSE, &sys->volume);
401 msg_Err(aout, "cannot get simple volume (error 0x%lx)", hr);
403 hr = IAudioSessionManager_GetAudioSessionControl(sys->manager,
404 &GUID_VLC_AUD_OUT, 0,
407 msg_Err(aout, "cannot get session control (error 0x%lx)", hr);
417 wchar_t *ua = var_InheritWide(aout, "user-agent");
418 IAudioSessionControl_SetDisplayName(control, ua, NULL);
421 IAudioSessionControl_RegisterAudioSessionNotification(control,
422 &sys->session_events);
425 if (sys->volume != NULL)
426 { /* Get current values (_after_ changes notification registration) */
430 hr = ISimpleAudioVolume_GetMute(sys->volume, &mute);
432 msg_Err(aout, "cannot get mute (error 0x%lx)", hr);
434 aout_MuteReport(aout, mute != FALSE);
436 hr = ISimpleAudioVolume_GetMasterVolume(sys->volume, &level);
438 msg_Err(aout, "cannot get mute (error 0x%lx)", hr);
440 aout_VolumeReport(aout, level);
443 SetEvent(sys->device_ready);
444 /* Wait until device change or exit */
445 WaitForSingleObject(sys->device_changed, INFINITE);
447 /* Deregister session control */
450 IAudioSessionControl_UnregisterAudioSessionNotification(control,
451 &sys->session_events);
452 IAudioSessionControl_Release(control);
455 if (sys->volume != NULL)
456 ISimpleAudioVolume_Release(sys->volume);
459 /** MMDevice audio output thread.
460 * This thread takes cares of the audio session control. Inconveniently enough,
461 * the audio session control interface must:
462 * - be created and destroyed from the same thread, and
463 * - survive across VLC audio output calls.
464 * The only way to reconcile both requirements is a custom thread.
465 * The thread also ensure that the COM Multi-Thread Apartment is continuously
466 * referenced so that MMDevice objects are not destroyed early.
468 static void *MMThread(void *data)
470 audio_output_t *aout = data;
471 aout_sys_t *sys = aout->sys;
474 while (sys->it != NULL)
475 MMSession(aout, sys);
480 /*** Audio devices ***/
481 static int DevicesEnum(audio_output_t *aout)
483 aout_sys_t *sys = aout->sys;
485 IMMDeviceCollection *devs;
487 hr = IMMDeviceEnumerator_EnumAudioEndpoints(sys->it, eRender,
488 DEVICE_STATE_ACTIVE, &devs);
491 msg_Warn(aout, "cannot enumerate audio endpoints (error 0x%lx)", hr);
496 hr = IMMDeviceCollection_GetCount(devs, &count);
499 msg_Warn(aout, "cannot count audio endpoints (error 0x%lx)", hr);
505 for (UINT i = 0; i < count; i++)
508 char *id, *name = NULL;
510 hr = IMMDeviceCollection_Item(devs, i, &dev);
514 /* Unique device ID */
516 hr = IMMDevice_GetId(dev, &devid);
519 IMMDevice_Release(dev);
522 id = FromWide(devid);
523 CoTaskMemFree(devid);
525 /* User-readable device name */
526 IPropertyStore *props;
527 hr = IMMDevice_OpenPropertyStore(dev, STGM_READ, &props);
533 hr = IPropertyStore_GetValue(props, &PKEY_Device_FriendlyName, &v);
535 name = FromWide(v.pwszVal);
536 PropVariantClear(&v);
537 IPropertyStore_Release(props);
539 IMMDevice_Release(dev);
541 aout_HotplugReport(aout, id, (name != NULL) ? name : id);
546 IMMDeviceCollection_Release(devs);
549 #endif /* !VLC_WINSTORE_APP */
552 * Opens the selected audio output device.
554 static HRESULT OpenDevice(audio_output_t *aout, const char *devid)
556 aout_sys_t *sys = aout->sys;
557 assert(sys->dev == NULL);
562 sys->dev = var_InheritAddress(aout, "mmdevice-audioclient");
566 if (devid != NULL) /* Device selected explicitly */
568 msg_Dbg(aout, "using selected device %s", devid);
570 wchar_t *wdevid = ToWide(devid);
571 if (likely(wdevid != NULL))
573 hr = IMMDeviceEnumerator_GetDevice(sys->it, wdevid, &sys->dev);
579 else /* Default device selected by policy */
581 msg_Dbg(aout, "using default device");
582 hr = IMMDeviceEnumerator_GetDefaultAudioEndpoint(sys->it, eRender,
583 eConsole, &sys->dev);
585 assert(sys->manager == NULL);
588 msg_Err(aout, "cannot get device (error 0x%lx)", hr);
592 /* Create session manager (for controls even w/o active audio client) */
594 hr = IMMDevice_Activate(sys->dev, &IID_IAudioSessionManager,
595 CLSCTX_ALL, NULL, &pv);
597 msg_Err(aout, "cannot activate session manager (error 0x%lx)", hr);
601 /* Report actual device */
603 hr = IMMDevice_GetId(sys->dev, &wdevid);
606 char *id = FromWide(wdevid);
607 CoTaskMemFree(wdevid);
608 if (likely(id != NULL))
610 aout_DeviceReport(aout, id);
615 SetEvent(sys->device_changed);
616 WaitForSingleObject(sys->device_ready, INFINITE);
618 #endif /* ! VLC_WINSTORE_APP */
622 * Closes the opened audio output device (if any).
624 static void CloseDevice(audio_output_t *aout)
626 aout_sys_t *sys = aout->sys;
628 assert(sys->dev != NULL);
629 #if !VLC_WINSTORE_APP
630 if (sys->manager != NULL)
632 IAudioSessionManager_Release(sys->manager);
636 IMMDevice_Release(sys->dev);
643 static int DeviceSelect(audio_output_t *aout, const char *id)
645 aout_sys_t *sys = aout->sys;
648 if (TryEnterMTA(aout))
651 if (sys->dev != NULL)
654 hr = OpenDevice(aout, id);
655 while (hr == AUDCLNT_E_DEVICE_INVALIDATED)
656 hr = OpenDevice(aout, NULL); /* Fallback to default device */
659 if (sys->stream != NULL)
660 /* Request restart of stream with the new device */
661 aout_RestartRequest(aout, AOUT_RESTART_OUTPUT);
662 return FAILED(hr) ? -1 : 0;
666 * Callback for aout_stream_t to create a stream on the device.
667 * This can instantiate an IAudioClient or IDirectSound(8) object.
669 static HRESULT ActivateDevice(void *opaque, REFIID iid, PROPVARIANT *actparms,
672 IMMDevice *dev = opaque;
674 (void)iid; (void)actparms;
678 return IMMDevice_Activate(dev, iid, CLSCTX_ALL, actparms, pv);
682 static int Start(audio_output_t *aout, audio_sample_format_t *restrict fmt)
684 aout_sys_t *sys = aout->sys;
687 assert (sys->stream == NULL);
688 /* Open the default device if required (to deal with restarts) */
689 if (sys->dev == NULL && FAILED(DeviceSelect(aout, NULL)))
692 aout_stream_t *s = vlc_object_create(aout, sizeof (*s));
693 if (unlikely(s == NULL))
696 s->owner.device = sys->dev;
697 s->owner.activate = ActivateDevice;
700 hr = aout_stream_Start(s, fmt, &GUID_VLC_AUD_OUT);
704 vlc_object_release(s);
707 return vlc_FromHR(aout, hr);
710 static void Stop(audio_output_t *aout)
712 aout_sys_t *sys = aout->sys;
714 assert (sys->stream != NULL);
717 aout_stream_Stop(sys->stream);
720 vlc_object_release(sys->stream);
724 static int Open(vlc_object_t *obj)
726 audio_output_t *aout = (audio_output_t *)obj;
728 if (!aout->b_force && var_InheritBool(aout, "spdif"))
729 /* Fallback to other plugin until pass-through is implemented */
732 aout_sys_t *sys = malloc(sizeof (*sys));
733 if (unlikely(sys == NULL))
740 #if !VLC_WINSTORE_APP
743 sys->session_events.lpVtbl = &vlc_AudioSessionEvents;
746 sys->device_changed = CreateEvent(NULL, FALSE, FALSE, NULL);
747 sys->device_ready = CreateEvent(NULL, FALSE, FALSE, NULL);
748 if (unlikely(sys->device_changed == NULL || sys->device_ready == NULL))
751 /* Initialize MMDevice API */
752 if (TryEnterMTA(aout))
756 HRESULT hr = CoCreateInstance(&CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL,
757 &IID_IMMDeviceEnumerator, &pv);
761 msg_Dbg(aout, "cannot create device enumerator (error 0x%lx)", hr);
765 if (vlc_clone(&sys->thread, MMThread, aout, VLC_THREAD_PRIORITY_LOW))
767 WaitForSingleObject(sys->device_ready, INFINITE);
770 DeviceSelect(aout, NULL); /* Get a device to start with */
771 LeaveMTA(); /* leave MTA after thread has entered MTA */
775 aout->time_get = TimeGet;
779 aout->volume_set = VolumeSet;
780 aout->mute_set = MuteSet;
782 aout->device_select = NULL;
785 aout->device_select = DeviceSelect;
792 IMMDeviceEnumerator_Release(sys->it);
795 if (sys->device_ready != NULL)
796 CloseHandle(sys->device_ready);
797 if (sys->device_changed != NULL)
798 CloseHandle(sys->device_changed);
804 static void Close(vlc_object_t *obj)
806 audio_output_t *aout = (audio_output_t *)obj;
807 aout_sys_t *sys = aout->sys;
809 EnterMTA(); /* enter MTA before thread leaves MTA */
810 if (sys->dev != NULL)
813 #if !VLC_WINSTORE_APP
814 IMMDeviceEnumerator_Release(sys->it);
817 SetEvent(sys->device_changed);
818 vlc_join(sys->thread, NULL);
823 #if !VLC_WINSTORE_APP
824 CloseHandle(sys->device_ready);
825 CloseHandle(sys->device_changed);
831 set_shortname("MMDevice")
832 set_description(N_("Windows Multimedia Device output"))
833 set_capability("audio output", /*150*/0)
835 /* Pointer to the activated AudioClient* */
836 add_integer("mmdevice-audioclient", 0x0, NULL, NULL, true);
838 set_category(CAT_AUDIO)
839 set_subcategory(SUBCAT_AUDIO_AOUT)
840 add_shortcut("wasapi")
841 set_callbacks(Open, Close)