1 /*****************************************************************************
2 * wasapi.c : Windows Audio Session API 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
7 * it 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 *****************************************************************************/
30 #include <audioclient.h>
31 #include <audiopolicy.h>
32 #include <mmdeviceapi.h>
34 #include <vlc_common.h>
35 #include <vlc_plugin.h>
37 #include <vlc_charset.h>
39 DEFINE_GUID (GUID_VLC_AUD_OUT, 0x4533f59d, 0x59ee, 0x00c6,
40 0xad, 0xb2, 0xc6, 0x8b, 0x50, 0x1a, 0x66, 0x55);
42 static int Open(vlc_object_t *);
43 static void Close(vlc_object_t *);
46 set_shortname("WASAPI")
47 set_description(N_("Windows Audio Session output") )
48 set_capability("audio output", 150)
49 set_category(CAT_AUDIO)
50 set_subcategory(SUBCAT_AUDIO_AOUT)
51 add_shortcut("was", "audioclient")
52 set_callbacks(Open, Close)
55 static int TryEnter(vlc_object_t *obj)
57 HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
58 if (unlikely(FAILED(hr)))
60 msg_Err (obj, "cannot initialize COM (error 0x%lx)", hr);
65 #define TryEnter(o) TryEnter(VLC_OBJECT(o))
67 static void Enter(void)
69 HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
70 if (unlikely(FAILED(hr)))
74 static void Leave(void)
83 IAudioRenderClient *render;
88 ISimpleAudioVolume *simple;
90 IAudioSessionControl *control;
91 struct IAudioSessionEvents events;
94 UINT32 frames; /**< Total buffer size (frames) */
95 HANDLE ready; /**< Semaphore from MTA thread */
96 HANDLE done; /**< Semaphore to MTA thread */
100 /*** VLC audio output callbacks ***/
101 static void Play(audio_output_t *aout, block_t *block, mtime_t *restrict drift)
103 aout_sys_t *sys = aout->sys;
107 if (likely(sys->clock != NULL))
111 /* NOTE: this assumes mdate() uses QPC() (which it currently does). */
112 hr = IAudioClock_GetPosition(sys->clock, &pos, &qpcpos);
115 qpcpos = (qpcpos + 5) / 10; /* 100ns -> 1µs */
116 *drift = mdate() - qpcpos;
119 msg_Warn(aout, "cannot get position (error 0x%lx)", hr);
125 hr = IAudioClient_GetCurrentPadding(sys->client, &frames);
128 msg_Err(aout, "cannot get current padding (error 0x%lx)", hr);
132 assert(frames <= sys->frames);
133 frames = sys->frames - frames;
134 if (frames > block->i_nb_samples)
135 frames = block->i_nb_samples;
138 hr = IAudioRenderClient_GetBuffer(sys->render, frames, &dst);
141 msg_Err(aout, "cannot get buffer (error 0x%lx)", hr);
145 const size_t copy = frames * (size_t)aout->format.i_bytes_per_frame;
147 memcpy(dst, block->p_buffer, copy);
148 hr = IAudioRenderClient_ReleaseBuffer(sys->render, frames, 0);
151 msg_Err(aout, "cannot release buffer (error 0x%lx)", hr);
154 IAudioClient_Start(sys->client);
156 block->p_buffer += copy;
157 block->i_buffer -= copy;
158 block->i_nb_samples -= frames;
159 if (block->i_nb_samples == 0)
162 /* Out of buffer space, sleep */
163 msleep(AOUT_MIN_PREPARE_TIME
164 + block->i_nb_samples * CLOCK_FREQ / aout->format.i_rate);
168 block_Release(block);
170 /* Restart on unplug */
171 if (unlikely(hr == AUDCLNT_E_DEVICE_INVALIDATED))
172 var_TriggerCallback(aout, "audio-device");
175 static void Pause(audio_output_t *aout, bool paused, mtime_t date)
177 aout_sys_t *sys = aout->sys;
182 hr = IAudioClient_Stop(sys->client);
184 hr = IAudioClient_Start(sys->client);
186 msg_Warn(aout, "cannot %s stream (error 0x%lx)",
187 paused ? "stop" : "start", hr);
192 static void Flush(audio_output_t *aout, bool wait)
194 aout_sys_t *sys = aout->sys;
198 return; /* Drain not implemented */
201 IAudioClient_Stop(sys->client);
202 hr = IAudioClient_Reset(sys->client);
204 msg_Warn(aout, "cannot reset stream (error 0x%lx)", hr);
208 static int SimpleVolumeSet(audio_output_t *aout, float vol)
210 aout_sys_t *sys = aout->sys;
215 hr = ISimpleAudioVolume_SetMasterVolume(sys->volume.simple, vol, NULL);
217 msg_Warn(aout, "cannot set session volume (error 0x%lx)", hr);
219 return FAILED(hr) ? -1 : 0;
222 static int SimpleMuteSet(audio_output_t *aout, bool mute)
224 aout_sys_t *sys = aout->sys;
229 hr = ISimpleAudioVolume_SetMute(sys->volume.simple, mute, NULL);
231 msg_Warn(aout, "cannot mute session (error 0x%lx)", hr);
233 return FAILED(hr) ? -1 : 0;
237 /*** Audio devices ***/
238 static int DeviceChanged(vlc_object_t *obj, const char *varname,
239 vlc_value_t prev, vlc_value_t cur, void *data)
241 aout_ChannelsRestart(obj, varname, prev, cur, data);
243 if (!var_Type (obj, "wasapi-audio-device"))
244 var_Create (obj, "wasapi-audio-device", VLC_VAR_STRING);
245 var_SetString (obj, "wasapi-audio-device", cur.psz_string);
249 static void GetDevices(vlc_object_t *obj, IMMDeviceEnumerator *it)
252 vlc_value_t val, text;
254 text.psz_string = _("Audio Device");
255 var_Change (obj, "audio-device", VLC_VAR_SETTEXT, &text, NULL);
257 IMMDeviceCollection *devs;
258 hr = IMMDeviceEnumerator_EnumAudioEndpoints(it, eRender,
259 DEVICE_STATE_ACTIVE, &devs);
262 msg_Warn (obj, "cannot enumerate audio endpoints (error 0x%lx)", hr);
267 hr = IMMDeviceCollection_GetCount(devs, &n);
274 hr = IMMDeviceCollection_Item(devs, --n, &dev);
278 /* Unique device ID */
280 hr = IMMDevice_GetId(dev, &devid);
283 IMMDevice_Release(dev);
286 val.psz_string = FromWide(devid);
287 CoTaskMemFree(devid);
288 text.psz_string = val.psz_string;
290 /* User-readable device name */
291 IPropertyStore *props;
292 hr = IMMDevice_OpenPropertyStore(dev, STGM_READ, &props);
299 hr = IPropertyStore_GetValue(props, PKEY_Device_FriendlyName, &v);
301 text.psz_string = FromWide(v.pwszVal);
303 PropVariantClear(&v);
304 IPropertyStore_Release(props);
306 IMMDevice_Release(dev);
308 var_Change(obj, "audio-device", VLC_VAR_ADDCHOICE, &val, &text);
309 if (likely(text.psz_string != val.psz_string))
310 free(text.psz_string);
311 free(val.psz_string);
313 IMMDeviceCollection_Release(devs);
317 /*** Audio session events ***/
318 static inline aout_sys_t *vlc_AudioSessionEvents_sys(IAudioSessionEvents *this)
320 return (aout_sys_t *)(((char *)this) - offsetof(aout_sys_t, events));
324 vlc_AudioSessionEvents_QueryInterface(IAudioSessionEvents *this, REFIID riid,
327 if (IsEqualIID(riid, &IID_IUnknown)
328 || IsEqualIID(riid, &IID_IAudioSessionEvents))
331 IUnknown_AddRef(this);
337 return E_NOINTERFACE;
341 static STDMETHODIMP_(ULONG)
342 vlc_AudioSessionEvents_AddRef(IAudioSessionEvents *this)
344 aout_sys_t *sys = vlc_AudioSessionEvents_sys(this);
345 return InterlockedIncrement(&sys->refs);
348 static STDMETHODIMP_(ULONG)
349 vlc_AudioSessionEvents_Release(IAudioSessionEvents *this)
351 aout_sys_t *sys = vlc_AudioSessionEvents_sys(this);
352 return InterlockedDecrement(&sys->refs);
356 vlc_AudioSessionEvents_OnDisplayNameChanged(IAudioSessionEvents *this,
357 LPCWSTR wname, LPCGUID ctx)
359 aout_sys_t *sys = vlc_AudioSessionEvents_sys(this);
360 audio_output_t *aout = sys->aout;
362 msg_Dbg(aout, "display name changed: %ls", wname);
368 vlc_AudioSessionEvents_OnIconPathChanged(IAudioSessionEvents *this,
369 LPCWSTR wpath, LPCGUID ctx)
371 aout_sys_t *sys = vlc_AudioSessionEvents_sys(this);
372 audio_output_t *aout = sys->aout;
374 msg_Dbg(aout, "icon path changed: %ls", wpath);
380 vlc_AudioSessionEvents_OnSimpleVolumeChanged(IAudioSessionEvents *this, float vol,
381 WINBOOL mute, LPCGUID ctx)
383 aout_sys_t *sys = vlc_AudioSessionEvents_sys(this);
384 audio_output_t *aout = sys->aout;
386 msg_Dbg(aout, "simple volume changed: %f, muting %sabled", vol,
387 mute ? "en" : "dis");
388 aout_VolumeReport(aout, vol);
389 aout_MuteReport(aout, mute == TRUE);
395 vlc_AudioSessionEvents_OnChannelVolumeChanged(IAudioSessionEvents *this,
396 DWORD count, float *vols,
397 DWORD changed, LPCGUID ctx)
399 aout_sys_t *sys = vlc_AudioSessionEvents_sys(this);
400 audio_output_t *aout = sys->aout;
402 msg_Dbg(aout, "channel volume %lu of %lu changed: %f", changed, count,
409 vlc_AudioSessionEvents_OnGroupingParamChanged(IAudioSessionEvents *this,
410 LPCGUID param, LPCGUID ctx)
413 aout_sys_t *sys = vlc_AudioSessionEvents_sys(this);
414 audio_output_t *aout = sys->aout;
416 msg_Dbg(aout, "grouping parameter changed");
423 vlc_AudioSessionEvents_OnStateChanged(IAudioSessionEvents *this,
424 AudioSessionState state)
426 aout_sys_t *sys = vlc_AudioSessionEvents_sys(this);
427 audio_output_t *aout = sys->aout;
429 msg_Dbg(aout, "state changed: %d", state);
434 vlc_AudioSessionEvents_OnSessionDisconnected(IAudioSessionEvents *this,
435 AudioSessionDisconnectReason reason)
437 aout_sys_t *sys = vlc_AudioSessionEvents_sys(this);
438 audio_output_t *aout = sys->aout;
440 msg_Dbg(aout, "session disconnected: reason %d", reason);
444 static const struct IAudioSessionEventsVtbl vlc_AudioSessionEvents =
446 vlc_AudioSessionEvents_QueryInterface,
447 vlc_AudioSessionEvents_AddRef,
448 vlc_AudioSessionEvents_Release,
450 vlc_AudioSessionEvents_OnDisplayNameChanged,
451 vlc_AudioSessionEvents_OnIconPathChanged,
452 vlc_AudioSessionEvents_OnSimpleVolumeChanged,
453 vlc_AudioSessionEvents_OnChannelVolumeChanged,
454 vlc_AudioSessionEvents_OnGroupingParamChanged,
455 vlc_AudioSessionEvents_OnStateChanged,
456 vlc_AudioSessionEvents_OnSessionDisconnected,
460 /*** Initialization / deinitialization **/
461 static void vlc_ToWave(WAVEFORMATEXTENSIBLE *restrict wf,
462 audio_sample_format_t *restrict audio)
464 switch (audio->i_format)
467 audio->i_format = VLC_CODEC_FL32;
469 wf->SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
474 audio->i_format = VLC_CODEC_S16N;
476 wf->SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
480 audio->i_format = VLC_CODEC_FL32;
481 wf->SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
484 aout_FormatPrepare (audio);
486 wf->Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
487 wf->Format.nChannels = audio->i_channels;
488 wf->Format.nSamplesPerSec = audio->i_rate;
489 wf->Format.nAvgBytesPerSec = audio->i_bytes_per_frame * audio->i_rate;
490 wf->Format.nBlockAlign = audio->i_bytes_per_frame;
491 wf->Format.wBitsPerSample = audio->i_bitspersample;
492 wf->Format.cbSize = sizeof (*wf) - sizeof (wf->Format);
494 wf->Samples.wValidBitsPerSample = audio->i_bitspersample;
496 wf->dwChannelMask = 0;
497 if (audio->i_physical_channels & AOUT_CHAN_LEFT)
498 wf->dwChannelMask |= SPEAKER_FRONT_LEFT;
499 if (audio->i_physical_channels & AOUT_CHAN_RIGHT)
500 wf->dwChannelMask |= SPEAKER_FRONT_RIGHT;
501 if (audio->i_physical_channels & AOUT_CHAN_CENTER)
502 wf->dwChannelMask |= SPEAKER_FRONT_CENTER;
503 if (audio->i_physical_channels & AOUT_CHAN_LFE)
504 wf->dwChannelMask |= SPEAKER_LOW_FREQUENCY;
506 if (audio->i_physical_channels & AOUT_CHAN_REARLEFT)
507 wf->dwChannelMask |= SPEAKER_BACK_LEFT;
508 if (audio->i_physical_channels & AOUT_CHAN_REARRIGHT)
509 wf->dwChannelMask |= SPEAKER_BACK_RIGHT;
511 if (audio->i_physical_channels & AOUT_CHAN_REARCENTER)
512 wf->dwChannelMask |= SPEAKER_BACK_CENTER;
513 if (audio->i_physical_channels & AOUT_CHAN_MIDDLELEFT)
514 wf->dwChannelMask |= SPEAKER_SIDE_LEFT;
515 if (audio->i_physical_channels & AOUT_CHAN_MIDDLERIGHT)
516 wf->dwChannelMask |= SPEAKER_SIDE_RIGHT;
520 static int vlc_FromWave(const WAVEFORMATEX *restrict wf,
521 audio_sample_format_t *restrict audio)
523 /* FIXME? different sample format? possible? */
524 audio->i_rate = wf->nSamplesPerSec;
526 if (wf->nChannels != audio->i_channels)
529 aout_FormatPrepare(audio);
533 static wchar_t *var_InheritWide(vlc_object_t *obj, const char *name)
535 char *v8 = var_InheritString(obj, name);
539 wchar_t *v16 = ToWide(v8);
543 #define var_InheritWide(o,n) var_InheritWide(VLC_OBJECT(o),n)
545 static int var_SetWide(vlc_object_t *obj, const char *name, const wchar_t *val)
547 char *str = FromWide(val);
548 if (unlikely(str == NULL))
551 int ret = var_SetString(obj, name, str);
555 #define var_SetWide(o,n,v) var_SetWide(VLC_OBJECT(o),n,v)
557 /* Dummy thread to create and release COM interfaces when needed. */
558 static void MTAThread(void *data)
560 audio_output_t *aout = data;
561 aout_sys_t *sys = aout->sys;
566 hr = IAudioClient_GetService(sys->client, &IID_IAudioRenderClient,
567 (void **)&sys->render);
570 msg_Err(aout, "cannot get audio render service (error 0x%lx)", hr);
574 hr = IAudioClient_GetService(sys->client, &IID_IAudioClock,
575 (void **)&sys->clock);
577 msg_Warn(aout, "cannot get audio clock (error 0x%lx)", hr);
579 /*if (AOUT_FMT_LINEAR(&format) && !exclusive)*/
581 hr = IAudioClient_GetService(sys->client, &IID_ISimpleAudioVolume,
582 (void **)&sys->volume.simple);
585 hr = IAudioClient_GetService(sys->client, &IID_IAudioSessionControl,
586 (void **)&sys->control);
588 msg_Warn(aout, "cannot get audio session control (error 0x%lx)", hr);
591 wchar_t *ua = var_InheritWide(aout, "user-agent");
592 IAudioSessionControl_SetDisplayName(sys->control, ua, NULL);
596 /* do nothing until the audio session terminates */
597 ReleaseSemaphore(sys->ready, 1, NULL);
598 WaitForSingleObject(sys->done, INFINITE);
600 if (sys->control != NULL)
601 IAudioSessionControl_Release(sys->control);
602 /*if (AOUT_FMT_LINEAR(&format) && !exclusive)*/
604 if (sys->volume.simple != NULL)
605 ISimpleAudioVolume_Release(sys->volume.simple);
607 if (sys->clock != NULL)
608 IAudioClock_Release(sys->clock);
609 IAudioRenderClient_Release(sys->render);
612 ReleaseSemaphore(sys->ready, 1, NULL);
615 static int Open(vlc_object_t *obj)
617 audio_output_t *aout = (audio_output_t *)obj;
620 if (AOUT_FMT_SPDIF(&aout->format) && !aout->b_force
621 && var_InheritBool(aout, "spdif"))
622 /* Fallback to other plugin until pass-through is implemented */
625 aout_sys_t *sys = malloc(sizeof (*sys));
626 if (unlikely(sys == NULL))
632 sys->events.lpVtbl = &vlc_AudioSessionEvents;
644 /* Get audio device according to policy */
645 var_Create (aout, "audio-device", VLC_VAR_STRING|VLC_VAR_HASCHOICE);
647 IMMDeviceEnumerator *devs;
648 hr = CoCreateInstance(&CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL,
649 &IID_IMMDeviceEnumerator, (void **)&devs);
652 msg_Dbg(aout, "cannot create device enumerator (error 0x%lx)", hr);
656 // Without configuration item, the variable must be created explicitly.
657 var_Create (aout, "wasapi-audio-device", VLC_VAR_STRING);
658 LPWSTR devid = var_InheritWide (aout, "wasapi-audio-device");
659 var_Destroy (aout, "wasapi-audio-device");
661 IMMDevice *dev = NULL;
664 msg_Dbg (aout, "using selected device %ls", devid);
665 hr = IMMDeviceEnumerator_GetDevice (devs, devid, &dev);
667 msg_Warn(aout, "cannot get audio endpoint (error 0x%lx)", hr);
672 msg_Dbg (aout, "using default device");
673 hr = IMMDeviceEnumerator_GetDefaultAudioEndpoint(devs, eRender,
677 GetDevices(VLC_OBJECT(aout), devs);
678 IMMDeviceEnumerator_Release(devs);
681 msg_Err(aout, "cannot get audio endpoint (error 0x%lx)", hr);
685 hr = IMMDevice_GetId(dev, &devid);
688 msg_Dbg(aout, "using device %ls", devid);
689 var_SetWide (aout, "audio-device", devid);
690 CoTaskMemFree(devid);
693 hr = IMMDevice_Activate(dev, &IID_IAudioClient, CLSCTX_ALL, NULL,
694 (void **)&sys->client);
695 IMMDevice_Release(dev);
698 msg_Err(aout, "cannot activate audio client (error 0x%lx)", hr);
702 /* Configure audio stream */
703 audio_sample_format_t format = aout->format;
704 WAVEFORMATEXTENSIBLE wf;
707 vlc_ToWave(&wf, &format);
708 hr = IAudioClient_IsFormatSupported(sys->client, AUDCLNT_SHAREMODE_SHARED,
712 msg_Err(aout, "cannot negotiate audio format (error 0x%lx)", hr);
719 if (vlc_FromWave(pwf, &format))
722 msg_Err(aout, "unsupported audio format");
725 msg_Dbg(aout, "modified format");
729 hr = IAudioClient_Initialize(sys->client, AUDCLNT_SHAREMODE_SHARED, 0,
730 AOUT_MAX_PREPARE_TIME * 10, 0,
731 (hr == S_OK) ? &wf.Format : pwf,
736 msg_Err(aout, "cannot initialize audio client (error 0x%lx)", hr);
740 hr = IAudioClient_GetBufferSize(sys->client, &sys->frames);
743 msg_Err(aout, "cannot get buffer size (error 0x%lx)", hr);
747 sys->ready = CreateSemaphore(NULL, 0, 1, NULL);
748 sys->done = CreateSemaphore(NULL, 0, 1, NULL);
749 if (unlikely(sys->ready == NULL || sys->done == NULL))
751 /* Note: thread handle released by CRT, ignore it. */
752 if (_beginthread(MTAThread, 0, aout) == (uintptr_t)-1)
755 WaitForSingleObject(sys->ready, INFINITE);
756 if (sys->render == NULL)
761 aout->format = format;
762 aout->pf_play = Play;
763 aout->pf_pause = Pause;
764 aout->pf_flush = Flush;
765 /*if (AOUT_FMT_LINEAR(&format) && !exclusive)*/
767 aout->volume_set = SimpleVolumeSet;
768 aout->mute_set = SimpleMuteSet;
770 if (likely(sys->control != NULL))
771 IAudioSessionControl_RegisterAudioSessionNotification(sys->control,
773 var_AddCallback (aout, "audio-device", DeviceChanged, NULL);
777 if (sys->done != NULL)
778 CloseHandle(sys->done);
779 if (sys->ready != NULL)
780 CloseHandle(sys->done);
781 if (sys->client != NULL)
782 IAudioClient_Release(sys->client);
783 var_Destroy(aout, "audio-device");
784 if (hr == AUDCLNT_E_DEVICE_INVALIDATED)
786 msg_Warn(aout, "device invalidated, retrying");
794 static void Close (vlc_object_t *obj)
796 audio_output_t *aout = (audio_output_t *)obj;
797 aout_sys_t *sys = aout->sys;
800 if (likely(sys->control != NULL))
801 IAudioSessionControl_UnregisterAudioSessionNotification(sys->control,
803 ReleaseSemaphore(sys->done, 1, NULL); /* tell MTA thread to finish */
804 WaitForSingleObject(sys->ready, INFINITE); /* wait for that ^ */
805 IAudioClient_Stop(sys->client); /* should not be needed */
806 IAudioClient_Release(sys->client);
809 var_DelCallback (aout, "audio-device", DeviceChanged, NULL);
810 var_Destroy (aout, "audio-device");
812 CloseHandle(sys->done);
813 CloseHandle(sys->ready);