/***************************************************************************** * winstore.c : Windows Multimedia Device API audio output plugin for VLC ***************************************************************************** * Copyright (C) 2012 RĂ©mi Denis-Courmont * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. *****************************************************************************/ #ifdef HAVE_CONFIG_H # include #endif #define INITGUID #define COBJMACROS #define CONST_VTABLE #include #include #include #include #include // ToWide #include #include "audio_output/mmdevice.h" #include #include DEFINE_GUID (GUID_VLC_AUD_OUT, 0x4533f59d, 0x59ee, 0x00c6, 0xad, 0xb2, 0xc6, 0x8b, 0x50, 0x1a, 0x66, 0x55); static void EnterMTA(void) { HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED); if (unlikely(FAILED(hr))) abort(); } static void LeaveMTA(void) { CoUninitialize(); } struct aout_sys_t { aout_stream_t *stream; /**< Underlying audio output stream */ module_t *module; IAudioClient *client; wchar_t* acquired_device; wchar_t* requested_device; wchar_t* default_device; // read once on open float gain; // IActivateAudioInterfaceCompletionHandler interface IActivateAudioInterfaceCompletionHandler client_locator; vlc_sem_t async_completed; LONG refs; CRITICAL_SECTION lock; }; /* MMDeviceLocator IUnknown methods */ static STDMETHODIMP_(ULONG) MMDeviceLocator_AddRef(IActivateAudioInterfaceCompletionHandler *This) { aout_sys_t *sys = container_of(This, aout_sys_t, client_locator); return InterlockedIncrement(&sys->refs); } static STDMETHODIMP_(ULONG) MMDeviceLocator_Release(IActivateAudioInterfaceCompletionHandler *This) { aout_sys_t *sys = container_of(This, aout_sys_t, client_locator); return InterlockedDecrement(&sys->refs); } static STDMETHODIMP MMDeviceLocator_QueryInterface(IActivateAudioInterfaceCompletionHandler *This, REFIID riid, void **ppv) { if( IsEqualIID(riid, &IID_IUnknown) || IsEqualIID(riid, &IID_IActivateAudioInterfaceCompletionHandler) ) { MMDeviceLocator_AddRef(This); *ppv = This; return S_OK; } *ppv = NULL; if( IsEqualIID(riid, &IID_IAgileObject) ) { return S_OK; } return ResultFromScode( E_NOINTERFACE ); } /* MMDeviceLocator IActivateAudioInterfaceCompletionHandler methods */ static HRESULT MMDeviceLocator_ActivateCompleted(IActivateAudioInterfaceCompletionHandler *This, IActivateAudioInterfaceAsyncOperation *operation) { (void)operation; aout_sys_t *sys = container_of(This, aout_sys_t, client_locator); vlc_sem_post( &sys->async_completed ); return S_OK; } /* MMDeviceLocator vtable */ static const struct IActivateAudioInterfaceCompletionHandlerVtbl MMDeviceLocator_vtable = { MMDeviceLocator_QueryInterface, MMDeviceLocator_AddRef, MMDeviceLocator_Release, MMDeviceLocator_ActivateCompleted, }; static void WaitForAudioClient(audio_output_t *aout) { aout_sys_t *sys = aout->sys; IActivateAudioInterfaceAsyncOperation* asyncOp = NULL; const wchar_t* devId = sys->requested_device ? sys->requested_device : sys->default_device; assert(sys->refs == 0); sys->refs = 0; assert(sys->client == NULL); sys->client = NULL; free(sys->acquired_device); sys->acquired_device = NULL; ActivateAudioInterfaceAsync(devId, &IID_IAudioClient, NULL, &sys->client_locator, &asyncOp); vlc_sem_wait( &sys->async_completed ); if (asyncOp) { HRESULT hr; HRESULT hrActivateResult; IUnknown *audioInterface; hr = IActivateAudioInterfaceAsyncOperation_GetActivateResult(asyncOp, &hrActivateResult, &audioInterface); IActivateAudioInterfaceAsyncOperation_Release(asyncOp); if (unlikely(FAILED(hr))) msg_Dbg(aout, "Failed to get the activation result. (hr=0x%lX)", hr); else if (FAILED(hrActivateResult)) msg_Dbg(aout, "Failed to activate the device. (hr=0x%lX)", hr); else if (unlikely(audioInterface == NULL)) msg_Dbg(aout, "Failed to get the device instance."); else { hr = IUnknown_QueryInterface(audioInterface, &IID_IAudioClient, (void**)&sys->client); IUnknown_Release(audioInterface); if (unlikely(FAILED(hr))) msg_Warn(aout, "The received interface is not a IAudioClient. (hr=0x%lX)", hr); else { sys->acquired_device = wcsdup(devId); char *report = FromWide(devId); if (likely(report)) { aout_DeviceReport(aout, report); free(report); } IAudioClient2 *audioClient2; if (SUCCEEDED(IAudioClient_QueryInterface(sys->client, &IID_IAudioClient2, (void**)&audioClient2)) && audioClient2) { // "BackgroundCapableMedia" does not work in UWP AudioClientProperties props = (AudioClientProperties) { .cbSize = sizeof(props), .bIsOffload = FALSE, .eCategory = AudioCategory_Movie, .Options = AUDCLNT_STREAMOPTIONS_NONE }; if (FAILED(IAudioClient2_SetClientProperties(audioClient2, &props))) { msg_Dbg(aout, "Failed to set audio client properties"); } IAudioClient2_Release(audioClient2); } } } } } static bool SetRequestedDevice(audio_output_t *aout, wchar_t *id) { aout_sys_t* sys = aout->sys; if (sys->requested_device != id) { if (sys->requested_device != sys->default_device) free(sys->requested_device); sys->requested_device = id; return true; } return false; } static int DeviceRequestLocked(audio_output_t *aout) { aout_sys_t *sys = aout->sys; assert(sys->requested_device); WaitForAudioClient(aout); if (sys->stream != NULL && sys->client != NULL) /* Request restart of stream with the new device */ aout_RestartRequest(aout, AOUT_RESTART_OUTPUT); return (sys->client != NULL) ? 0 : -1; } static int DeviceRestartLocked(audio_output_t *aout) { aout_sys_t *sys = aout->sys; if (sys->client) { assert(sys->acquired_device); free(sys->acquired_device); sys->acquired_device = NULL; IAudioClient_Release(sys->client); sys->client = NULL; } return DeviceRequestLocked(aout); } static int DeviceSelectLocked(audio_output_t *aout, const char* id) { aout_sys_t *sys = aout->sys; bool changed; if( id == NULL ) { changed = SetRequestedDevice(aout, sys->default_device); } else { wchar_t *requested_device = ToWide(id); if (unlikely(requested_device == NULL)) return VLC_ENOMEM; changed = SetRequestedDevice(aout, requested_device); } if (!changed) return VLC_EGENERIC; return DeviceRestartLocked(aout); } static int DeviceSelect(audio_output_t *aout, const char* id) { aout_sys_t *sys = aout->sys; EnterCriticalSection(&sys->lock); int ret = DeviceSelectLocked(aout, id); LeaveCriticalSection(&sys->lock); return ret; } static void ResetInvalidatedClient(audio_output_t *aout, HRESULT hr) { /* Select the default device (and restart) on unplug */ if (unlikely(hr == AUDCLNT_E_DEVICE_INVALIDATED || hr == AUDCLNT_E_RESOURCES_INVALIDATED)) { // Select the default device (and restart) on unplug DeviceSelect(aout, NULL); } } static int VolumeSet(audio_output_t *aout, float vol) { aout_sys_t *sys = aout->sys; if( unlikely( sys->client == NULL ) ) return VLC_EGENERIC; HRESULT hr; ISimpleAudioVolume *pc_AudioVolume = NULL; float linear_vol = vol * vol * vol; /* ISimpleAudioVolume is tapered linearly. */ if (linear_vol > 1.f) { sys->gain = linear_vol; linear_vol = 1.f; } else sys->gain = 1.f; aout_GainRequest(aout, sys->gain); hr = IAudioClient_GetService(sys->client, &IID_ISimpleAudioVolume, &pc_AudioVolume); if (FAILED(hr)) { msg_Err(aout, "cannot get volume service (error 0x%lx)", hr); goto done; } hr = ISimpleAudioVolume_SetMasterVolume(pc_AudioVolume, linear_vol, NULL); if (FAILED(hr)) { msg_Err(aout, "cannot set volume (error 0x%lx)", hr); goto done; } aout_VolumeReport(aout, vol); done: if (pc_AudioVolume) ISimpleAudioVolume_Release(pc_AudioVolume); return SUCCEEDED(hr) ? 0 : -1; } static int MuteSet(audio_output_t *aout, bool mute) { aout_sys_t *sys = aout->sys; if( unlikely( sys->client == NULL ) ) return VLC_EGENERIC; HRESULT hr; ISimpleAudioVolume *pc_AudioVolume = NULL; hr = IAudioClient_GetService(sys->client, &IID_ISimpleAudioVolume, &pc_AudioVolume); if (FAILED(hr)) { msg_Err(aout, "cannot get volume service (error 0x%lx)", hr); goto done; } hr = ISimpleAudioVolume_SetMute(pc_AudioVolume, mute, NULL); if (FAILED(hr)) { msg_Err(aout, "cannot set mute (error 0x%lx)", hr); goto done; } aout_MuteReport(aout, mute); done: if (pc_AudioVolume) ISimpleAudioVolume_Release(pc_AudioVolume); return SUCCEEDED(hr) ? 0 : -1; } static int TimeGet(audio_output_t *aout, vlc_tick_t *restrict delay) { aout_sys_t *sys = aout->sys; if( unlikely( sys->client == NULL ) ) return VLC_EGENERIC; HRESULT hr; EnterMTA(); hr = aout_stream_TimeGet(sys->stream, delay); LeaveMTA(); return SUCCEEDED(hr) ? 0 : -1; } static void Play(audio_output_t *aout, block_t *block) { aout_sys_t *sys = aout->sys; if( unlikely( sys->client == NULL ) ) { block_Release(block); return; } EnterMTA(); HRESULT hr = aout_stream_Play(sys->stream, block); LeaveMTA(); ResetInvalidatedClient(aout, hr); } static void Pause(audio_output_t *aout, bool paused, vlc_tick_t date) { aout_sys_t *sys = aout->sys; if( unlikely( sys->client == NULL ) ) return; EnterMTA(); HRESULT hr = aout_stream_Pause(sys->stream, paused); LeaveMTA(); (void) date; ResetInvalidatedClient(aout, hr); } static void Flush(audio_output_t *aout, bool wait) { aout_sys_t *sys = aout->sys; if( unlikely( sys->client == NULL ) ) return; EnterMTA(); HRESULT hr = aout_stream_Flush(sys->stream, wait); LeaveMTA(); ResetInvalidatedClient(aout, hr); } static HRESULT ActivateDevice(void *opaque, REFIID iid, PROPVARIANT *actparms, void **restrict pv) { IAudioClient *client = opaque; if (!IsEqualIID(iid, &IID_IAudioClient)) return E_NOINTERFACE; if (actparms != NULL || client == NULL ) return E_INVALIDARG; IAudioClient_AddRef(client); // as would IMMDevice_Activate do *pv = opaque; return S_OK; } static int aout_stream_Start(void *func, va_list ap) { aout_stream_start_t start = func; aout_stream_t *s = va_arg(ap, aout_stream_t *); audio_sample_format_t *fmt = va_arg(ap, audio_sample_format_t *); HRESULT *hr = va_arg(ap, HRESULT *); *hr = start(s, fmt, &GUID_VLC_AUD_OUT); return SUCCEEDED(*hr) ? VLC_SUCCESS : VLC_EGENERIC; } static void aout_stream_Stop(void *func, va_list ap) { aout_stream_stop_t stop = func; aout_stream_t *s = va_arg(ap, aout_stream_t *); stop(s); } static int Start(audio_output_t *aout, audio_sample_format_t *restrict fmt) { aout_sys_t *sys = aout->sys; HRESULT hr; aout_stream_t *s = vlc_object_create(aout, sizeof (*s)); if (unlikely(s == NULL)) return -1; // Load the "out stream" for the requested device EnterMTA(); EnterCriticalSection(&sys->lock); if (sys->requested_device != NULL) { if (sys->acquired_device == NULL || wcscmp(sys->acquired_device, sys->requested_device)) { // we have a pending request for a new device DeviceRestartLocked(aout); if (sys->client == NULL) { LeaveCriticalSection(&sys->lock); LeaveMTA(); vlc_object_release(s); return -1; } } } s->owner.activate = ActivateDevice; for (;;) { s->owner.device = sys->client; sys->module = vlc_module_load(s, "aout stream", NULL, false, aout_stream_Start, s, fmt, &hr); int ret = -1; if (hr == AUDCLNT_E_DEVICE_INVALIDATED) { // the requested device is not usable, try the default device ret = DeviceSelectLocked(aout, NULL); } else if (hr == AUDCLNT_E_ALREADY_INITIALIZED) { /* From MSDN: "If the initial call to Initialize fails, subsequent * Initialize calls might fail and return error code * E_ALREADY_INITIALIZED, even though the interface has not been * initialized. If this occurs, release the IAudioClient interface * and obtain a new IAudioClient interface from the MMDevice API * before calling Initialize again." * * Therefore, request to MMThread the same device and try again. */ ret = DeviceRestartLocked(aout); } if (ret != VLC_SUCCESS) break; if (sys->client == NULL || sys->module != NULL) break; } // Report the initial volume and mute status to the core if (sys->client != NULL) { ISimpleAudioVolume* pc_AudioVolume = NULL; hr = IAudioClient_GetService(sys->client, &IID_ISimpleAudioVolume, &pc_AudioVolume); if (FAILED(hr)) { msg_Err(aout, "cannot get volume service (error 0x%lx)", hr); goto done; } float vol; hr = ISimpleAudioVolume_GetMasterVolume(pc_AudioVolume, &vol); if (FAILED(hr)) { msg_Err(aout, "cannot get initial volume (error 0x%lX)", hr); goto done; } WINBOOL mute; hr = ISimpleAudioVolume_GetMute(pc_AudioVolume, &mute); if (FAILED(hr)) { msg_Err(aout, "cannot get initial mute (error 0x%lX)", hr); goto done; } aout_VolumeReport(aout, cbrtf(vol * sys->gain)); aout_MuteReport(aout, mute != 0); done: if (pc_AudioVolume) ISimpleAudioVolume_Release(pc_AudioVolume); } LeaveCriticalSection(&sys->lock); LeaveMTA(); if (sys->module == NULL) { vlc_object_release(s); return -1; } if (sys->client) { // the requested device has been used, reset it // we keep the corresponding sys->client until a new request is started SetRequestedDevice(aout, NULL); } assert (sys->stream == NULL); sys->stream = s; return 0; } static void Stop(audio_output_t *aout) { aout_sys_t *sys = aout->sys; assert (sys->stream != NULL); EnterMTA(); vlc_module_unload(sys->stream, sys->module, aout_stream_Stop, sys->stream); LeaveMTA(); vlc_object_release(sys->stream); sys->stream = NULL; } static int Open(vlc_object_t *obj) { audio_output_t *aout = (audio_output_t *)obj; aout_sys_t *sys = malloc(sizeof (*sys)); if (unlikely(sys == NULL)) return VLC_ENOMEM; if (unlikely(FAILED(StringFromIID(&DEVINTERFACE_AUDIO_RENDER, &sys->default_device)))) { msg_Dbg(obj, "Failed to get the default renderer string"); free(sys); return VLC_EGENERIC; } char *psz_default = FromWide(sys->default_device); if (likely(psz_default != NULL)) { aout_HotplugReport(aout, psz_default, _("Default")); free(psz_default); } InitializeCriticalSection(&sys->lock); vlc_sem_init(&sys->async_completed, 0); sys->refs = 0; sys->requested_device = sys->default_device; sys->acquired_device = NULL; sys->client_locator = (IActivateAudioInterfaceCompletionHandler) { &MMDeviceLocator_vtable }; sys->gain = 1.f; aout->sys = sys; sys->stream = NULL; sys->client = NULL; aout->start = Start; aout->stop = Stop; aout->time_get = TimeGet; aout->volume_set = VolumeSet; aout->mute_set = MuteSet; aout->play = Play; aout->pause = Pause; aout->flush = Flush; aout->device_select = DeviceSelect; return VLC_SUCCESS; } static void Close(vlc_object_t *obj) { audio_output_t *aout = (audio_output_t *)obj; aout_sys_t *sys = aout->sys; if(sys->client != NULL) IAudioClient_Release(sys->client); assert(sys->refs == 0); free(sys->acquired_device); if (sys->requested_device != sys->default_device) free(sys->requested_device); CoTaskMemFree(sys->default_device); DeleteCriticalSection(&sys->lock); free(sys); } vlc_module_begin() set_shortname("winstore") set_description("Windows Store audio output") set_capability("audio output", 0) set_category(CAT_AUDIO) set_subcategory(SUBCAT_AUDIO_AOUT) add_shortcut("wasapi") set_callbacks(Open, Close) vlc_module_end()