You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1561 lines
45 KiB

/*****************************************************************************
* mmdevice.c : Windows Multimedia Device API audio output plugin for VLC
*****************************************************************************
* Copyright (C) 2012-2017 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 <config.h>
#endif
#define INITGUID
#define COBJMACROS
#define CONST_VTABLE
#include <stdatomic.h>
#include <stdlib.h>
#include <math.h>
#include <assert.h>
#include <audiopolicy.h>
#include <mmdeviceapi.h>
#include <endpointvolume.h>
DEFINE_PROPERTYKEY(PKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd,
0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 14);
#include <vlc_common.h>
#include <vlc_arrays.h>
#include <vlc_plugin.h>
#include <vlc_aout.h>
#include <vlc_charset.h>
#include <vlc_modules.h>
#include "mmdevice.h"
DEFINE_GUID (GUID_VLC_AUD_OUT, 0x4533f59d, 0x59ee, 0x00c6,
0xad, 0xb2, 0xc6, 0x8b, 0x50, 0x1a, 0x66, 0x55);
static int TryEnterMTA(vlc_object_t *obj)
{
HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE);
if (unlikely(FAILED(hr)))
{
msg_Err (obj, "cannot initialize COM (error 0x%lX)", hr);
return -1;
}
return 0;
}
#define TryEnterMTA(o) TryEnterMTA(VLC_OBJECT(o))
static void EnterMTA(void)
{
HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE);
if (unlikely(FAILED(hr)))
abort();
}
static void LeaveMTA(void)
{
CoUninitialize();
}
static char default_device_b[1] = "";
enum device_acquisition_status {
DEVICE_PENDING,
DEVICE_INITIALISATION_FAILED,
DEVICE_ACQUISITION_FAILED,
DEVICE_ACQUIRED,
};
typedef struct
{
struct aout_stream_owner *stream; /**< Underlying audio output stream */
audio_output_t *aout;
IMMDeviceEnumerator *it; /**< Device enumerator, NULL when exiting */
IMMDevice *dev; /**< Selected output device, NULL if none */
struct IMMNotificationClient device_events;
struct IAudioSessionEvents session_events;
struct IAudioVolumeDuckNotification duck;
LONG refs;
unsigned ducks;
float gain; /**< Current software gain volume */
float requested_volume; /**< Requested volume, negative if none */
signed char requested_mute; /**< Requested mute, negative if none */
enum device_acquisition_status device_status;
wchar_t *device_name; /**< device identifier to use, NULL if default */
bool default_device_changed;
HANDLE work_event;
vlc_sem_t init_passed;
vlc_mutex_t lock;
vlc_cond_t ready;
vlc_thread_t thread; /**< Thread for audio session control */
} aout_sys_t;
/* NOTE: The Core Audio API documentation totally fails to specify the thread
* safety (or lack thereof) of the interfaces. This code takes the most
* restrictive assumption: no thread safety. The background thread (MMThread)
* only runs at specified times, namely between the device_ready and
* device_changed events (effectively a thread synchronization barrier, but
* only Windows 8 natively provides such a primitive).
*
* The audio output owner (i.e. the audio output core) is responsible for
* serializing callbacks. This code only needs to be concerned with
* synchronization between the set of audio output callbacks, MMThread()
* and (trivially) the device and session notifications. */
static int DeviceSelect(audio_output_t *, const char *);
static int vlc_FromHR(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))
DeviceSelect(aout, NULL);
return SUCCEEDED(hr) ? 0 : -1;
}
/*** VLC audio output callbacks ***/
static void Play(audio_output_t *aout, block_t *block, vlc_tick_t date)
{
aout_sys_t *sys = aout->sys;
vlc_mutex_lock(&sys->lock);
aout_stream_owner_AppendBlock(sys->stream, block, date);
vlc_mutex_unlock(&sys->lock);
SetEvent(sys->work_event);
}
static void Pause(audio_output_t *aout, bool paused, vlc_tick_t date)
{
aout_sys_t *sys = aout->sys;
HRESULT hr;
EnterMTA();
vlc_mutex_lock(&sys->lock);
hr = aout_stream_owner_Pause(sys->stream, paused);
vlc_mutex_unlock(&sys->lock);
LeaveMTA();
vlc_FromHR(aout, hr);
(void) date;
}
static void Flush(audio_output_t *aout)
{
aout_sys_t *sys = aout->sys;
HRESULT hr;
EnterMTA();
vlc_mutex_lock(&sys->lock);
hr = aout_stream_owner_Flush(sys->stream);
vlc_mutex_unlock(&sys->lock);
LeaveMTA();
vlc_FromHR(aout, hr);
}
static int VolumeSetLocked(audio_output_t *aout, float vol)
{
aout_sys_t *sys = aout->sys;
float gain = 1.f;
vol = vol * vol * vol; /* ISimpleAudioVolume is tapered linearly. */
if (vol > 1.f)
{
gain = vol;
vol = 1.f;
}
sys->gain = gain;
sys->requested_volume = vol;
return 0;
}
static int VolumeSet(audio_output_t *aout, float vol)
{
aout_sys_t *sys = aout->sys;
vlc_mutex_lock(&sys->lock);
int ret = VolumeSetLocked(aout, vol);
aout_GainRequest(aout, sys->gain);
vlc_mutex_unlock(&sys->lock);
SetEvent(sys->work_event);
return ret;
}
static int MuteSet(audio_output_t *aout, bool mute)
{
aout_sys_t *sys = aout->sys;
vlc_mutex_lock(&sys->lock);
sys->requested_mute = mute;
vlc_mutex_unlock(&sys->lock);
SetEvent(sys->work_event);
return 0;
}
/*** Audio session events ***/
static STDMETHODIMP
vlc_AudioSessionEvents_QueryInterface(IAudioSessionEvents *this, REFIID riid,
void **ppv)
{
if (IsEqualIID(riid, &IID_IUnknown)
|| IsEqualIID(riid, &IID_IAudioSessionEvents))
{
*ppv = this;
IUnknown_AddRef(this);
return S_OK;
}
else
{
*ppv = NULL;
return E_NOINTERFACE;
}
}
static STDMETHODIMP_(ULONG)
vlc_AudioSessionEvents_AddRef(IAudioSessionEvents *this)
{
aout_sys_t *sys = container_of(this, aout_sys_t, session_events);
return InterlockedIncrement(&sys->refs);
}
static STDMETHODIMP_(ULONG)
vlc_AudioSessionEvents_Release(IAudioSessionEvents *this)
{
aout_sys_t *sys = container_of(this, aout_sys_t, session_events);
return InterlockedDecrement(&sys->refs);
}
static STDMETHODIMP
vlc_AudioSessionEvents_OnDisplayNameChanged(IAudioSessionEvents *this,
LPCWSTR wname, LPCGUID ctx)
{
aout_sys_t *sys = container_of(this, aout_sys_t, session_events);
audio_output_t *aout = sys->aout;
msg_Dbg(aout, "display name changed: %ls", wname);
(void) ctx;
return S_OK;
}
static STDMETHODIMP
vlc_AudioSessionEvents_OnIconPathChanged(IAudioSessionEvents *this,
LPCWSTR wpath, LPCGUID ctx)
{
aout_sys_t *sys = container_of(this, aout_sys_t, session_events);
audio_output_t *aout = sys->aout;
msg_Dbg(aout, "icon path changed: %ls", wpath);
(void) ctx;
return S_OK;
}
static STDMETHODIMP
vlc_AudioSessionEvents_OnSimpleVolumeChanged(IAudioSessionEvents *this,
float vol, BOOL mute,
LPCGUID ctx)
{
aout_sys_t *sys = container_of(this, aout_sys_t, session_events);
audio_output_t *aout = sys->aout;
msg_Dbg(aout, "simple volume changed: %f, muting %sabled", vol,
mute ? "en" : "dis");
SetEvent(sys->work_event); /* implicit state: vol & mute */
(void) ctx;
return S_OK;
}
static STDMETHODIMP
vlc_AudioSessionEvents_OnChannelVolumeChanged(IAudioSessionEvents *this,
DWORD count, float *vols,
DWORD changed, LPCGUID ctx)
{
aout_sys_t *sys = container_of(this, aout_sys_t, session_events);
audio_output_t *aout = sys->aout;
if (changed != (DWORD)-1)
msg_Dbg(aout, "channel volume %lu of %lu changed: %f", changed, count,
vols[changed]);
else
msg_Dbg(aout, "%lu channels volume changed", count);
(void) ctx;
return S_OK;
}
static STDMETHODIMP
vlc_AudioSessionEvents_OnGroupingParamChanged(IAudioSessionEvents *this,
LPCGUID param, LPCGUID ctx)
{
aout_sys_t *sys = container_of(this, aout_sys_t, session_events);
audio_output_t *aout = sys->aout;
msg_Dbg(aout, "grouping parameter changed");
(void) param;
(void) ctx;
return S_OK;
}
static STDMETHODIMP
vlc_AudioSessionEvents_OnStateChanged(IAudioSessionEvents *this,
AudioSessionState state)
{
aout_sys_t *sys = container_of(this, aout_sys_t, session_events);
audio_output_t *aout = sys->aout;
msg_Dbg(aout, "state changed: %d", state);
return S_OK;
}
static STDMETHODIMP
vlc_AudioSessionEvents_OnSessionDisconnected(IAudioSessionEvents *this,
AudioSessionDisconnectReason reason)
{
aout_sys_t *sys = container_of(this, aout_sys_t, session_events);
audio_output_t *aout = sys->aout;
switch (reason)
{
case DisconnectReasonDeviceRemoval:
msg_Warn(aout, "session disconnected: %s", "device removed");
break;
case DisconnectReasonServerShutdown:
msg_Err(aout, "session disconnected: %s", "service stopped");
return S_OK;
case DisconnectReasonFormatChanged:
msg_Warn(aout, "session disconnected: %s", "format changed");
break;
case DisconnectReasonSessionLogoff:
msg_Err(aout, "session disconnected: %s", "user logged off");
return S_OK;
case DisconnectReasonSessionDisconnected:
msg_Err(aout, "session disconnected: %s", "session disconnected");
return S_OK;
case DisconnectReasonExclusiveModeOverride:
msg_Err(aout, "session disconnected: %s", "stream overridden");
return S_OK;
default:
msg_Warn(aout, "session disconnected: unknown reason %d", reason);
return S_OK;
}
/* NOTE: audio decoder thread should get invalidated device and restart */
return S_OK;
}
static const struct IAudioSessionEventsVtbl vlc_AudioSessionEvents =
{
vlc_AudioSessionEvents_QueryInterface,
vlc_AudioSessionEvents_AddRef,
vlc_AudioSessionEvents_Release,
vlc_AudioSessionEvents_OnDisplayNameChanged,
vlc_AudioSessionEvents_OnIconPathChanged,
vlc_AudioSessionEvents_OnSimpleVolumeChanged,
vlc_AudioSessionEvents_OnChannelVolumeChanged,
vlc_AudioSessionEvents_OnGroupingParamChanged,
vlc_AudioSessionEvents_OnStateChanged,
vlc_AudioSessionEvents_OnSessionDisconnected,
};
static STDMETHODIMP
vlc_AudioVolumeDuckNotification_QueryInterface(
IAudioVolumeDuckNotification *this, REFIID riid, void **ppv)
{
if (IsEqualIID(riid, &IID_IUnknown)
|| IsEqualIID(riid, &IID_IAudioVolumeDuckNotification))
{
*ppv = this;
IUnknown_AddRef(this);
return S_OK;
}
else
{
*ppv = NULL;
return E_NOINTERFACE;
}
}
static STDMETHODIMP_(ULONG)
vlc_AudioVolumeDuckNotification_AddRef(IAudioVolumeDuckNotification *this)
{
aout_sys_t *sys = container_of(this, aout_sys_t, duck);
return InterlockedIncrement(&sys->refs);
}
static STDMETHODIMP_(ULONG)
vlc_AudioVolumeDuckNotification_Release(IAudioVolumeDuckNotification *this)
{
aout_sys_t *sys = container_of(this, aout_sys_t, duck);
return InterlockedDecrement(&sys->refs);
}
static STDMETHODIMP
vlc_AudioVolumeDuckNotification_OnVolumeDuckNotification(
IAudioVolumeDuckNotification *this, LPCWSTR sid, UINT32 count)
{
aout_sys_t *sys = container_of(this, aout_sys_t, duck);
audio_output_t *aout = sys->aout;
msg_Dbg(aout, "volume ducked by %ls of %u sessions", sid, count);
sys->ducks++;
aout_PolicyReport(aout, true);
return S_OK;
}
static STDMETHODIMP
vlc_AudioVolumeDuckNotification_OnVolumeUnduckNotification(
IAudioVolumeDuckNotification *this, LPCWSTR sid)
{
aout_sys_t *sys = container_of(this, aout_sys_t, duck);
audio_output_t *aout = sys->aout;
msg_Dbg(aout, "volume unducked by %ls", sid);
sys->ducks--;
aout_PolicyReport(aout, sys->ducks != 0);
return S_OK;
}
static const struct IAudioVolumeDuckNotificationVtbl vlc_AudioVolumeDuckNotification =
{
vlc_AudioVolumeDuckNotification_QueryInterface,
vlc_AudioVolumeDuckNotification_AddRef,
vlc_AudioVolumeDuckNotification_Release,
vlc_AudioVolumeDuckNotification_OnVolumeDuckNotification,
vlc_AudioVolumeDuckNotification_OnVolumeUnduckNotification,
};
/*** Audio devices ***/
/** Gets the user-readable device name */
static char *DeviceGetFriendlyName(IMMDevice *dev)
{
IPropertyStore *props;
PROPVARIANT v;
HRESULT hr;
hr = IMMDevice_OpenPropertyStore(dev, STGM_READ, &props);
if (FAILED(hr))
return NULL;
char *name = NULL;
PropVariantInit(&v);
hr = IPropertyStore_GetValue(props, &PKEY_Device_FriendlyName, &v);
if (SUCCEEDED(hr))
{
name = FromWide(v.pwszVal);
PropVariantClear(&v);
}
IPropertyStore_Release(props);
return name;
}
static int DeviceHotplugReport(audio_output_t *aout, LPCWSTR wid,
IMMDevice *dev)
{
char *id = FromWide(wid);
if (!id)
return VLC_EGENERIC;
char *name = DeviceGetFriendlyName(dev);
if (name == NULL)
name = id;
aout_HotplugReport(aout, id, name);
free(id);
if (id != name)
free(name);
return VLC_SUCCESS;
}
/** Checks that a device is an output device */
static bool DeviceIsRender(IMMDevice *dev)
{
void *pv;
if (FAILED(IMMDevice_QueryInterface(dev, &IID_IMMEndpoint, &pv)))
return false;
IMMEndpoint *ep = pv;
EDataFlow flow;
HRESULT hr = IMMEndpoint_GetDataFlow(ep, &flow);
IMMEndpoint_Release(ep);
if (FAILED(hr) || flow != eRender)
return false;
DWORD pdwState;
hr = IMMDevice_GetState(dev, &pdwState);
return !FAILED(hr) && pdwState == DEVICE_STATE_ACTIVE;
}
static HRESULT DeviceUpdated(audio_output_t *aout, LPCWSTR wid)
{
aout_sys_t *sys = aout->sys;
HRESULT hr;
IMMDevice *dev;
hr = IMMDeviceEnumerator_GetDevice(sys->it, wid, &dev);
if (FAILED(hr))
return hr;
if (!DeviceIsRender(dev))
{
IMMDevice_Release(dev);
return S_OK;
}
DeviceHotplugReport(aout, wid, dev);
IMMDevice_Release(dev);
return S_OK;
}
static STDMETHODIMP
vlc_MMNotificationClient_QueryInterface(IMMNotificationClient *this,
REFIID riid, void **ppv)
{
if (IsEqualIID(riid, &IID_IUnknown)
|| IsEqualIID(riid, &IID_IMMNotificationClient))
{
*ppv = this;
IUnknown_AddRef(this);
return S_OK;
}
else
{
*ppv = NULL;
return E_NOINTERFACE;
}
}
static STDMETHODIMP_(ULONG)
vlc_MMNotificationClient_AddRef(IMMNotificationClient *this)
{
aout_sys_t *sys = container_of(this, aout_sys_t, device_events);
return InterlockedIncrement(&sys->refs);
}
static STDMETHODIMP_(ULONG)
vlc_MMNotificationClient_Release(IMMNotificationClient *this)
{
aout_sys_t *sys = container_of(this, aout_sys_t, device_events);
return InterlockedDecrement(&sys->refs);
}
static STDMETHODIMP
vlc_MMNotificationClient_OnDefaultDeviceChange(IMMNotificationClient *this,
EDataFlow flow, ERole role,
LPCWSTR wid)
{
aout_sys_t *sys = container_of(this, aout_sys_t, device_events);
audio_output_t *aout = sys->aout;
if (flow != eRender)
return S_OK;
if (role != eConsole)
return S_OK;
vlc_mutex_lock(&sys->lock);
if (sys->device_name == NULL)
{
sys->default_device_changed = true;
aout_RestartRequest(aout, true);
}
vlc_mutex_unlock(&sys->lock);
msg_Dbg(aout, "default device changed: %ls", wid ? wid : L"(disabled)");
return S_OK;
}
static STDMETHODIMP
vlc_MMNotificationClient_OnDeviceAdded(IMMNotificationClient *this,
LPCWSTR wid)
{
aout_sys_t *sys = container_of(this, aout_sys_t, device_events);
audio_output_t *aout = sys->aout;
msg_Dbg(aout, "device %ls added", wid);
return DeviceUpdated(aout, wid);
}
static STDMETHODIMP
vlc_MMNotificationClient_OnDeviceRemoved(IMMNotificationClient *this,
LPCWSTR wid)
{
aout_sys_t *sys = container_of(this, aout_sys_t, device_events);
audio_output_t *aout = sys->aout;
char *id = FromWide(wid);
msg_Dbg(aout, "device %ls removed", wid);
if (unlikely(id == NULL))
return E_OUTOFMEMORY;
aout_HotplugReport(aout, id, NULL);
free(id);
return S_OK;
}
static STDMETHODIMP
vlc_MMNotificationClient_OnDeviceStateChanged(IMMNotificationClient *this,
LPCWSTR wid, DWORD state)
{
aout_sys_t *sys = container_of(this, aout_sys_t, device_events);
audio_output_t *aout = sys->aout;
switch (state) {
case DEVICE_STATE_UNPLUGGED:
msg_Dbg(aout, "device %ls state changed: unplugged", wid);
break;
case DEVICE_STATE_ACTIVE:
msg_Dbg(aout, "device %ls state changed: active", wid);
return DeviceUpdated(aout, wid);
case DEVICE_STATE_DISABLED:
msg_Dbg(aout, "device %ls state changed: disabled", wid);
break;
case DEVICE_STATE_NOTPRESENT:
msg_Dbg(aout, "device %ls state changed: not present", wid);
break;
default:
msg_Dbg(aout, "device %ls state changed: unknown: %08lx", wid, state);
return E_FAIL;
}
/* Unplugged, disabled or notpresent */
char *id = FromWide(wid);
if (unlikely(id == NULL))
return E_OUTOFMEMORY;
aout_HotplugReport(aout, id, NULL);
free(id);
return S_OK;
}
static STDMETHODIMP
vlc_MMNotificationClient_OnPropertyValueChanged(IMMNotificationClient *this,
LPCWSTR wid,
const PROPERTYKEY key)
{
aout_sys_t *sys = container_of(this, aout_sys_t, device_events);
audio_output_t *aout = sys->aout;
if (key.pid == PKEY_Device_FriendlyName.pid)
{
msg_Dbg(aout, "device %ls name changed", wid);
return DeviceUpdated(aout, wid);
}
return S_OK;
}
static const struct IMMNotificationClientVtbl vlc_MMNotificationClient =
{
vlc_MMNotificationClient_QueryInterface,
vlc_MMNotificationClient_AddRef,
vlc_MMNotificationClient_Release,
vlc_MMNotificationClient_OnDeviceStateChanged,
vlc_MMNotificationClient_OnDeviceAdded,
vlc_MMNotificationClient_OnDeviceRemoved,
vlc_MMNotificationClient_OnDefaultDeviceChange,
vlc_MMNotificationClient_OnPropertyValueChanged,
};
static HRESULT DevicesEnum(IMMDeviceEnumerator *it,
void (*added_cb)(void *data, LPCWSTR wid, IMMDevice *dev),
void *added_cb_data)
{
HRESULT hr;
IMMDeviceCollection *devs;
assert(added_cb != NULL);
hr = IMMDeviceEnumerator_EnumAudioEndpoints(it, eRender,
DEVICE_STATE_ACTIVE, &devs);
if (FAILED(hr))
return hr;
UINT count;
hr = IMMDeviceCollection_GetCount(devs, &count);
if (FAILED(hr))
return hr;
for (UINT i = 0; i < count; i++)
{
IMMDevice *dev;
hr = IMMDeviceCollection_Item(devs, i, &dev);
if (FAILED(hr) || !DeviceIsRender(dev))
continue;
/* Unique device ID */
LPWSTR devid;
hr = IMMDevice_GetId(dev, &devid);
if (FAILED(hr))
{
IMMDevice_Release(dev);
continue;
}
added_cb(added_cb_data, devid, dev);
IMMDevice_Release(dev);
CoTaskMemFree(devid);
}
IMMDeviceCollection_Release(devs);
return S_OK;
}
static int DeviceRequestLocked(audio_output_t *aout)
{
aout_sys_t *sys = aout->sys;
assert(sys->device_status == DEVICE_PENDING);
sys->default_device_changed = false;
SetEvent(sys->work_event);
while (sys->device_status == DEVICE_PENDING)
vlc_cond_wait(&sys->ready, &sys->lock);
if (sys->stream != NULL && sys->dev != NULL)
/* Request restart of stream with the new device */
aout_RestartRequest(aout, true);
return (sys->dev != NULL) ? 0 : -1;
}
static int DeviceSelectLocked(audio_output_t *aout, const char *id)
{
aout_sys_t *sys = aout->sys;
assert(sys->device_status != DEVICE_PENDING);
sys->device_status = DEVICE_PENDING;
if (id != NULL && strcmp(id, default_device_b) != 0)
{
sys->device_name = ToWide(id); /* FIXME leak */
if (unlikely(sys->device_name == NULL))
return -1;
}
else
sys->device_name = NULL;
return DeviceRequestLocked(aout);
}
static int DeviceRestartLocked(audio_output_t *aout)
{
aout_sys_t *sys = aout->sys;
assert(sys->device_status != DEVICE_PENDING);
sys->device_status = DEVICE_PENDING;
return DeviceRequestLocked(aout);
}
static int DeviceSelect(audio_output_t *aout, const char *id)
{
aout_sys_t *sys = aout->sys;
vlc_mutex_lock(&sys->lock);
int ret = DeviceSelectLocked(aout, id);
vlc_mutex_unlock(&sys->lock);
return ret;
}
/**
* Main loop
*
* Adjust volume as long as device is unchanged
* */
static void MMSessionMainloop(audio_output_t *aout, ISimpleAudioVolume *volume)
{
aout_sys_t *sys = aout->sys;
HRESULT hr;
bool report_volume = true;
bool report_mute = true;
while (sys->device_status != DEVICE_PENDING)
{
if (volume != NULL)
{
if (sys->requested_volume >= 0.f)
{
hr = ISimpleAudioVolume_SetMasterVolume(volume, sys->requested_volume, NULL);
if (FAILED(hr))
msg_Err(aout, "cannot set master volume (error 0x%lX)",
hr);
report_volume = true;
sys->requested_volume = -1.f;
}
if (report_volume)
{
float level;
hr = ISimpleAudioVolume_GetMasterVolume(volume, &level);
if (SUCCEEDED(hr))
aout_VolumeReport(aout, cbrtf(level * sys->gain));
else
msg_Err(aout, "cannot get master volume (error 0x%lX)", hr);
report_volume = false;
}
if (sys->requested_mute >= 0)
{
BOOL mute = sys->requested_mute ? TRUE : FALSE;
hr = ISimpleAudioVolume_SetMute(volume, mute, NULL);
if (FAILED(hr))
msg_Err(aout, "cannot set mute (error 0x%lX)", hr);
report_mute = true;
sys->requested_mute = -1;
}
if (report_mute)
{
BOOL mute;
hr = ISimpleAudioVolume_GetMute(volume, &mute);
if (SUCCEEDED(hr))
aout_MuteReport(aout, mute != FALSE);
else
msg_Err(aout, "cannot get mute (error 0x%lX)", hr);
report_mute = false;
}
}
DWORD wait_ms = INFINITE;
DWORD ev_count = 1;
HANDLE events[2] = {
sys->work_event,
NULL
};
if (sys->stream != NULL)
{
wait_ms = aout_stream_owner_ProcessTimer(sys->stream);
/* Don't listen to the stream event if the block fifo is empty */
if (sys->stream->chain != NULL)
events[ev_count++] = sys->stream->buffer_ready_event;
}
vlc_mutex_unlock(&sys->lock);
WaitForMultipleObjects(ev_count, events, FALSE, wait_ms);
vlc_mutex_lock(&sys->lock);
if (sys->stream != NULL)
{
hr = aout_stream_owner_PlayAll(sys->stream);
/* Don't call vlc_FromHR here since this function waits for the
* current thread */
if (unlikely(hr == AUDCLNT_E_DEVICE_INVALIDATED ||
hr == AUDCLNT_E_RESOURCES_INVALIDATED))
{
sys->device_name = NULL;
sys->device_status = DEVICE_PENDING;
/* The restart of the stream will be requested asynchronously */
}
}
}
}
/*** Initialization / deinitialization **/
/** MMDevice audio output thread.
* This thread takes cares of the audio session control. Inconveniently enough,
* the audio session control interface must:
* - be created and destroyed from the same thread, and
* - survive across VLC audio output calls.
* The only way to reconcile both requirements is a custom thread.
* The thread also ensure that the COM Multi-Thread Apartment is continuously
* referenced so that MMDevice objects are not destroyed early.
* Furthermore, VolumeSet() and MuteSet() may be called from a thread with a
* COM STA, so that it cannot access the COM MTA for audio controls.
*/
static HRESULT MMSession(audio_output_t *aout, IMMDeviceEnumerator *it)
{
aout_sys_t *sys = aout->sys;
IAudioSessionManager *manager;
IAudioSessionControl *control;
ISimpleAudioVolume *volume;
IAudioEndpointVolume *endpoint;
void *pv;
HRESULT hr;
assert(sys->device_status == DEVICE_PENDING);
assert(sys->dev == NULL);
/* Yes, it's perfectly valid to request the same device, see Start()
* comments. */
if (sys->device_name != NULL) /* Device selected explicitly */
{
hr = IMMDeviceEnumerator_GetDevice(it, sys->device_name, &sys->dev);
if (FAILED(hr))
{
msg_Err(aout, "cannot get selected device %ls (error 0x%lX)",
sys->device_name, hr);
hr = AUDCLNT_E_DEVICE_INVALIDATED;
}
else
{
msg_Dbg(aout, "using selected device %ls", sys->device_name);
sys->device_status = DEVICE_ACQUIRED;
}
}
else
hr = AUDCLNT_E_DEVICE_INVALIDATED;
while (hr == AUDCLNT_E_DEVICE_INVALIDATED)
{ /* Default device selected by policy and with stream routing.
* "Do not use eMultimedia" says MSDN. */
msg_Dbg(aout, "using default device");
hr = IMMDeviceEnumerator_GetDefaultAudioEndpoint(it, eRender,
eConsole, &sys->dev);
if (FAILED(hr))
{
msg_Err(aout, "cannot get default device (error 0x%lX)", hr);
sys->device_status = DEVICE_ACQUISITION_FAILED;
sys->device_name = NULL;
}
else
{
sys->device_status = DEVICE_ACQUIRED;
sys->device_name = NULL;
}
}
vlc_cond_signal(&sys->ready);
vlc_sem_post(&sys->init_passed);
if (FAILED(hr))
{
msg_Err(aout, "cannot get device identifier (error 0x%lX)", hr);
return hr;
}
/* Report actual device */
if (sys->device_name == NULL)
aout_DeviceReport(aout, default_device_b);
else
{
LPWSTR wdevid;
hr = IMMDevice_GetId(sys->dev, &wdevid);
if (SUCCEEDED(hr))
{
char *id = FromWide(wdevid);
CoTaskMemFree(wdevid);
if (likely(id != NULL))
{
aout_DeviceReport(aout, id);
free(id);
}
}
}
/* Create session manager (for controls even w/o active audio client) */
hr = IMMDevice_Activate(sys->dev, &IID_IAudioSessionManager,
CLSCTX_ALL, NULL, &pv);
manager = pv;
if (SUCCEEDED(hr))
{
LPCGUID guid = var_GetBool(aout, "volume-save") ? &GUID_VLC_AUD_OUT : NULL;
/* Register session control */
hr = IAudioSessionManager_GetAudioSessionControl(manager, guid, 0,
&control);
if (SUCCEEDED(hr))
{
char *ua = var_InheritString(aout, "user-agent");
if (ua != NULL)
{
wchar_t *wua = ToWide(ua);
if (likely(wua != NULL))
{
IAudioSessionControl_SetDisplayName(control, wua, NULL);
free(wua);
}
free(ua);
}
IAudioSessionControl_RegisterAudioSessionNotification(control,
&sys->session_events);
}
else
msg_Err(aout, "cannot get session control (error 0x%lX)", hr);
hr = IAudioSessionManager_GetSimpleAudioVolume(manager, guid, FALSE,
&volume);
if (FAILED(hr))
msg_Err(aout, "cannot get simple volume (error 0x%lX)", hr);
/* Try to get version 2 (Windows 7) of the manager & control */
wchar_t *siid = NULL;
hr = IAudioSessionManager_QueryInterface(manager,
&IID_IAudioSessionControl2, &pv);
if (SUCCEEDED(hr))
{
IAudioSessionControl2 *c2 = pv;
IAudioSessionControl2_SetDuckingPreference(c2, FALSE);
hr = IAudioSessionControl2_GetSessionInstanceIdentifier(c2, &siid);
if (FAILED(hr))
siid = NULL;
IAudioSessionControl2_Release(c2);
}
else
msg_Dbg(aout, "version 2 session control unavailable");
hr = IAudioSessionManager_QueryInterface(manager,
&IID_IAudioSessionManager2, &pv);
if (SUCCEEDED(hr))
{
IAudioSessionManager2 *m2 = pv;
IAudioSessionManager2_RegisterDuckNotification(m2, siid,
&sys->duck);
IAudioSessionManager2_Release(m2);
}
else
msg_Dbg(aout, "version 2 session management unavailable");
CoTaskMemFree(siid);
}
else
{
msg_Err(aout, "cannot activate session manager (error 0x%lX)", hr);
control = NULL;
volume = NULL;
}
hr = IMMDevice_Activate(sys->dev, &IID_IAudioEndpointVolume,
CLSCTX_ALL, NULL, &pv);
endpoint = pv;
if (SUCCEEDED(hr))
{
float min, max, inc;
hr = IAudioEndpointVolume_GetVolumeRange(endpoint, &min, &max, &inc);
if (SUCCEEDED(hr))
msg_Dbg(aout, "volume from %+f dB to %+f dB with %f dB increments",
min, max, inc);
else
msg_Err(aout, "cannot get volume range (error 0x%lX)", hr);
}
else
msg_Err(aout, "cannot activate endpoint volume (error 0x%lX)", hr);
MMSessionMainloop(aout, volume);
vlc_mutex_unlock(&sys->lock);
if (endpoint != NULL)
IAudioEndpointVolume_Release(endpoint);
if (manager != NULL)
{ /* Deregister callbacks *without* the lock */
hr = IAudioSessionManager_QueryInterface(manager,
&IID_IAudioSessionManager2, &pv);
if (SUCCEEDED(hr))
{
IAudioSessionManager2 *m2 = pv;
IAudioSessionManager2_UnregisterDuckNotification(m2, &sys->duck);
IAudioSessionManager2_Release(m2);
}
if (volume != NULL)
ISimpleAudioVolume_Release(volume);
if (control != NULL)
{
IAudioSessionControl_UnregisterAudioSessionNotification(control,
&sys->session_events);
IAudioSessionControl_Release(control);
}
IAudioSessionManager_Release(manager);
}
vlc_mutex_lock(&sys->lock);
IMMDevice_Release(sys->dev);
sys->dev = NULL;
return S_OK;
}
static void MMThread_DevicesEnum_Added(void *data, LPCWSTR wid, IMMDevice *dev)
{
audio_output_t *aout = data;
DeviceHotplugReport(aout, wid, dev);
}
static void *MMThread(void *data)
{
audio_output_t *aout = data;
aout_sys_t *sys = aout->sys;
vlc_thread_set_name("vlc-mmdevice");
/* Initialize MMDevice API */
if (TryEnterMTA(aout))
goto error;
void *pv;
HRESULT hr = CoCreateInstance(&CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL,
&IID_IMMDeviceEnumerator, &pv);
if (FAILED(hr))
{
msg_Dbg(aout, "cannot create device enumerator (error 0x%lX)", hr);
LeaveMTA();
goto error;
}
IMMDeviceEnumerator *it = pv;
sys->it = it;
IMMDeviceEnumerator_RegisterEndpointNotificationCallback(it,
&sys->device_events);
hr = DevicesEnum(it, MMThread_DevicesEnum_Added, aout);
if (FAILED(hr))
msg_Warn(aout, "cannot enumerate audio endpoints (error 0x%lX)", hr);
vlc_mutex_lock(&sys->lock);
do
if (sys->device_status != DEVICE_PENDING || FAILED(MMSession(aout, it)))
{
vlc_mutex_unlock(&sys->lock);
WaitForSingleObject(sys->work_event, INFINITE);
vlc_mutex_lock(&sys->lock);
}
while (sys->it != NULL);
vlc_mutex_unlock(&sys->lock);
IMMDeviceEnumerator_UnregisterEndpointNotificationCallback(it,
&sys->device_events);
IMMDeviceEnumerator_Release(it);
LeaveMTA();
return NULL;
error:
sys->device_status = DEVICE_INITIALISATION_FAILED;
vlc_sem_post(&sys->init_passed);
return NULL;
}
/**
* Callback for aout_stream_t to create a stream on the device.
* This can instantiate an IAudioClient or IDirectSound(8) object.
*/
static HRESULT ActivateDevice(void *opaque, REFIID iid, PROPVARIANT *actparms,
void **restrict pv)
{
IMMDevice *dev = opaque;
return IMMDevice_Activate(dev, iid, CLSCTX_ALL, actparms, pv);
}
static int aout_stream_Start(void *func, bool forced, 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 *);
LPCGUID sid = var_InheritBool(s, "volume-save") ? &GUID_VLC_AUD_OUT : NULL;
(void) forced;
*hr = start(s, fmt, sid);
if (*hr == AUDCLNT_E_DEVICE_INVALIDATED)
return VLC_ETIMEOUT;
return SUCCEEDED(*hr) ? VLC_SUCCESS : VLC_EGENERIC;
}
static int Start(audio_output_t *aout, audio_sample_format_t *restrict fmt)
{
aout_sys_t *sys = aout->sys;
const bool b_spdif = AOUT_FMT_SPDIF(fmt);
const bool b_hdmi = AOUT_FMT_HDMI(fmt);
if (b_spdif || b_hdmi)
{
switch (var_InheritInteger(aout, "mmdevice-passthrough"))
{
case MM_PASSTHROUGH_DISABLED:
return -1;
case MM_PASSTHROUGH_ENABLED:
if (b_hdmi)
return -1;
/* fallthrough */
case MM_PASSTHROUGH_ENABLED_HD:
break;
}
}
struct aout_stream_owner *owner =
aout_stream_owner_New(aout, sizeof (*owner), ActivateDevice);
if (unlikely(owner == NULL))
return -1;
aout_stream_t *s = &owner->s;
EnterMTA();
vlc_mutex_lock(&sys->lock);
if ((sys->default_device_changed && DeviceRestartLocked(aout) != 0)
|| sys->dev == NULL)
{
/* Error if the device restart failed or if a request previously
* failed. */
vlc_mutex_unlock(&sys->lock);
LeaveMTA();
aout_stream_owner_Delete(owner);
return -1;
}
module_t *module;
for (;;)
{
char *modlist = var_InheritString(aout, "mmdevice-backend");
HRESULT hr;
owner->device = sys->dev;
module = vlc_module_load(vlc_object_logger(s), "aout stream", modlist,
false, aout_stream_Start, s, fmt, &hr);
free(modlist);
int ret = -1;
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);
}
else if (hr == AUDCLNT_E_DEVICE_INVALIDATED)
{
/* The audio endpoint device has been unplugged, request to
* MMThread the default device and try again. */
ret = DeviceSelectLocked(aout, NULL);
}
if (ret != 0)
break;
}
if (module != NULL)
{
IPropertyStore *props;
HRESULT hr = IMMDevice_OpenPropertyStore(sys->dev, STGM_READ, &props);
if (SUCCEEDED(hr))
{
PROPVARIANT v;
PropVariantInit(&v);
hr = IPropertyStore_GetValue(props, &PKEY_AudioEndpoint_FormFactor, &v);
if (SUCCEEDED(hr))
{
switch (v.uintVal)
{
case Headphones:
case Headset:
aout->current_sink_info.headphones = true;
break;
}
PropVariantClear(&v);
}
IPropertyStore_Release(props);
}
}
if (module == NULL)
{
aout_stream_owner_Delete(owner);
vlc_mutex_unlock(&sys->lock);
LeaveMTA();
return -1;
}
assert (sys->stream == NULL);
sys->stream = owner;
vlc_mutex_unlock(&sys->lock);
LeaveMTA();
aout_GainRequest(aout, sys->gain);
return 0;
}
static void Stop(audio_output_t *aout)
{
aout_sys_t *sys = aout->sys;
assert(sys->stream != NULL);
EnterMTA();
aout_stream_owner_Stop(sys->stream);
LeaveMTA();
aout_stream_owner_Delete(sys->stream);
sys->stream = NULL;
}
static void Close(vlc_object_t *);
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;
aout->sys = sys;
sys->stream = NULL;
sys->aout = aout;
sys->it = NULL;
sys->dev = NULL;
sys->device_events.lpVtbl = &vlc_MMNotificationClient;
sys->session_events.lpVtbl = &vlc_AudioSessionEvents;
sys->duck.lpVtbl = &vlc_AudioVolumeDuckNotification;
sys->refs = 1;
sys->ducks = 0;
sys->gain = 1.f;
sys->requested_volume = -1.f;
sys->requested_mute = -1;
sys->device_name = NULL;
sys->default_device_changed = false;
if (!var_CreateGetBool(aout, "volume-save"))
VolumeSetLocked(aout, var_InheritFloat(aout, "mmdevice-volume"));
vlc_sem_init(&sys->init_passed, 0);
vlc_mutex_init(&sys->lock);
vlc_cond_init(&sys->ready);
sys->work_event = CreateEvent(NULL, FALSE, FALSE, NULL);
if (unlikely(sys->work_event == NULL))
goto error;
aout_HotplugReport(aout, default_device_b, _("Default"));
char *saved_device_b = var_InheritString(aout, "mmdevice-audio-device");
if (saved_device_b != NULL && strcmp(saved_device_b, default_device_b) != 0)
{
sys->device_name = ToWide(saved_device_b); /* FIXME leak */
free(saved_device_b);
if (unlikely(sys->device_name == NULL))
goto error;
}
else
{
free(saved_device_b);
sys->device_name = NULL;
}
sys->device_status = DEVICE_PENDING;
if (vlc_clone(&sys->thread, MMThread, aout))
goto error;
vlc_sem_wait(&sys->init_passed);
if (sys->device_status == DEVICE_INITIALISATION_FAILED)
{
Close(obj);
return VLC_EGENERIC;
}
aout->start = Start;
aout->stop = Stop;
aout->play = Play;
aout->pause = Pause;
aout->flush = Flush;
aout->volume_set = VolumeSet;
aout->mute_set = MuteSet;
aout->device_select = DeviceSelect;
return VLC_SUCCESS;
error:
if (sys->work_event != NULL)
CloseHandle(sys->work_event);
free(sys);
return VLC_EGENERIC;
}
static void Close(vlc_object_t *obj)
{
audio_output_t *aout = (audio_output_t *)obj;
aout_sys_t *sys = aout->sys;
vlc_mutex_lock(&sys->lock);
wchar_t *previous = sys->device_name;
sys->device_name = NULL;
sys->device_status = DEVICE_PENDING; /* break out of MMSession() loop */
sys->it = NULL; /* break out of MMThread() loop */
vlc_mutex_unlock(&sys->lock);
if (previous != NULL)
free(previous);
SetEvent(sys->work_event);
vlc_join(sys->thread, NULL);
CloseHandle(sys->work_event);
free(sys);
}
struct mm_list
{
size_t count;
char **ids;
char **names;
};
static void Reload_DevicesEnum_Added(void *data, LPCWSTR wid, IMMDevice *dev)
{
struct mm_list *list = data;
size_t new_count = list->count + 1;
list->ids = realloc_or_free(list->ids, new_count * sizeof(char *));
list->names = realloc_or_free(list->names, new_count * sizeof(char *));
if (!list->ids || !list->names)
{
free(list->ids);
return;
}
char *id = FromWide(wid);
if (!id)
return;
char *name = DeviceGetFriendlyName(dev);
if (!name && !(name = strdup(id)))
{
free(id);
return;
}
list->ids[list->count] = id;
list->names[list->count] = name;
list->count = new_count;
}
static int ReloadAudioDevices(char const *name, char ***values, char ***descs)
{
bool in_mta = true;
HRESULT hr;
(void) name;
hr = CoInitializeEx(NULL, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE);
if (FAILED(hr)) {
if (hr != RPC_E_CHANGED_MODE)
return -1;
in_mta = false;
}
struct mm_list list = { .count = 0 };
void *it;
hr = CoCreateInstance(&CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL,
&IID_IMMDeviceEnumerator, &it);
if (FAILED(hr))
goto error;
list.ids = malloc(sizeof (char *));
list.names = malloc(sizeof (char *));
if (!list.ids || !list.names)
{
free(list.ids);
goto error;
}
list.ids[0] = strdup("");
list.names[0] = strdup(_("Default"));
if (!list.ids[0] || !list.names[0])
{
free(list.ids[0]);
free(list.ids);
free(list.names);
goto error;
}
list.count++;
DevicesEnum(it, Reload_DevicesEnum_Added, &list);
error:
IMMDeviceEnumerator_Release((IMMDeviceEnumerator *)it);
if (in_mta)
CoUninitialize();
if (list.count > 0)
{
*values = list.ids;
*descs = list.names;
}
return list.count;
}
VLC_CONFIG_STRING_ENUM(ReloadAudioDevices)
#define MM_PASSTHROUGH_TEXT N_( \
"HDMI/SPDIF audio passthrough")
#define MM_PASSTHROUGH_LONGTEXT N_( \
"Change this value if you have issue with HD codecs when using a HDMI receiver.")
static const int pi_mmdevice_passthrough_values[] = {
MM_PASSTHROUGH_DISABLED,
MM_PASSTHROUGH_ENABLED,
MM_PASSTHROUGH_ENABLED_HD,
};
static const char *const ppsz_mmdevice_passthrough_texts[] = {
N_("Disabled"),
N_("Enabled (AC3/DTS only)"),
N_("Enabled"),
};
#define DEVICE_TEXT N_("Output device")
#define DEVICE_LONGTEXT N_("Select your audio output device")
#define VOLUME_TEXT N_("Audio volume")
#define VOLUME_LONGTEXT N_("Audio volume in hundredths of decibels (dB).")
vlc_module_begin()
set_shortname("MMDevice")
set_description(N_("Windows Multimedia Device output"))
set_capability("audio output", 150)
set_subcategory(SUBCAT_AUDIO_AOUT)
set_callbacks(Open, Close)
add_module("mmdevice-backend", "aout stream", "any",
N_("Output back-end"), N_("Audio output back-end interface."))
add_integer( "mmdevice-passthrough", MM_PASSTHROUGH_DEFAULT,
MM_PASSTHROUGH_TEXT, MM_PASSTHROUGH_LONGTEXT )
change_integer_list( pi_mmdevice_passthrough_values,
ppsz_mmdevice_passthrough_texts )
add_string("mmdevice-audio-device", NULL, DEVICE_TEXT, DEVICE_LONGTEXT)
add_float("mmdevice-volume", 1.f, VOLUME_TEXT, VOLUME_LONGTEXT)
change_float_range( 0.f, 1.25f )
vlc_module_end()