]> git.sesse.net Git - vlc/blob - modules/audio_output/wasapi.c
wasapi: print an error rather than abort in volume setting error
[vlc] / modules / audio_output / wasapi.c
1 /*****************************************************************************
2  * wasapi.c : Windows Audio Session API 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
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.
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
28 #include <assert.h>
29 #include <audioclient.h>
30 #include <audiopolicy.h>
31 #include <mmdeviceapi.h>
32
33 #include <vlc_common.h>
34 #include <vlc_plugin.h>
35 #include <vlc_aout.h>
36 #include <vlc_charset.h>
37
38 static int Open(vlc_object_t *);
39 static void Close(vlc_object_t *);
40
41 vlc_module_begin()
42     set_shortname("WASAPI")
43     set_description(N_("Windows Audio Session output") )
44     set_capability("audio output", 150)
45     set_category(CAT_AUDIO)
46     set_subcategory(SUBCAT_AUDIO_AOUT)
47     add_shortcut("was", "audioclient")
48     set_callbacks(Open, Close)
49 vlc_module_end()
50
51 static int TryEnter(vlc_object_t *obj)
52 {
53     HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
54     if (unlikely(FAILED(hr)))
55     {
56         msg_Err (obj, "cannot initialize COM (error 0x%lx)", hr);
57         return -1;
58     }
59     return 0;
60 }
61 #define TryEnter(o) TryEnter(VLC_OBJECT(o))
62
63 static void Enter(void)
64 {
65     HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
66     if (unlikely(FAILED(hr)))
67         abort();
68 }
69
70 static void Leave(void)
71 {
72     CoUninitialize();
73 }
74
75 struct aout_sys_t
76 {
77     IAudioClient *client;
78     IAudioRenderClient *render;
79     IAudioClock *clock;
80     union
81     {
82         ISimpleAudioVolume *simple;
83     } volume;
84     IAudioSessionControl *control;
85     UINT32 frames; /**< Total buffer size (frames) */
86     HANDLE ready; /**< Semaphore from MTA thread */
87     HANDLE done; /**< Semaphore to MTA thread */
88 };
89
90 static void Play(audio_output_t *aout, block_t *block, mtime_t *restrict drift)
91 {
92     aout_sys_t *sys = aout->sys;
93     HRESULT hr;
94
95     Enter();
96     if (likely(sys->clock != NULL))
97     {
98         UINT64 pos, qpcpos;
99
100         IAudioClock_GetPosition(sys->clock, &pos, &qpcpos);
101         qpcpos = (qpcpos + 5) / 10; /* 100ns -> 1µs */
102         /* NOTE: this assumes mdate() uses QPC() (which it currently does). */
103         *drift = mdate() - qpcpos;
104     }
105
106     for (;;)
107     {
108         UINT32 frames;
109         hr = IAudioClient_GetCurrentPadding(sys->client, &frames);
110         if (FAILED(hr))
111         {
112             msg_Err(aout, "cannot get current padding (error 0x%lx)", hr);
113             break;
114         }
115
116         assert(frames <= sys->frames);
117         frames = sys->frames - frames;
118         if (frames > block->i_nb_samples)
119             frames = block->i_nb_samples;
120
121         BYTE *dst;
122         hr = IAudioRenderClient_GetBuffer(sys->render, frames, &dst);
123         if (FAILED(hr))
124         {
125             msg_Err(aout, "cannot get buffer (error 0x%lx)", hr);
126             break;
127         }
128
129         const size_t copy = frames * (size_t)aout->format.i_bytes_per_frame;
130
131         memcpy(dst, block->p_buffer, copy);
132         hr = IAudioRenderClient_ReleaseBuffer(sys->render, frames, 0);
133         if (FAILED(hr))
134         {
135             msg_Err(aout, "cannot release buffer (error 0x%lx)", hr);
136             break;
137         }
138         IAudioClient_Start(sys->client);
139
140         block->p_buffer += copy;
141         block->i_buffer -= copy;
142         block->i_nb_samples -= frames;
143         if (block->i_nb_samples == 0)
144             break; /* done */
145
146         /* Out of buffer space, sleep */
147         msleep(AOUT_MIN_PREPARE_TIME
148              + block->i_nb_samples * CLOCK_FREQ / aout->format.i_rate);
149     }
150
151     Leave();
152     block_Release(block);
153 }
154
155 static void Pause(audio_output_t *aout, bool paused, mtime_t date)
156 {
157     aout_sys_t *sys = aout->sys;
158     HRESULT hr;
159
160     Enter();
161     if (paused)
162         hr = IAudioClient_Stop(sys->client);
163     else
164         hr = IAudioClient_Start(sys->client);
165     if (FAILED(hr))
166         msg_Warn(aout, "cannot %s stream (error 0x%lx)",
167                  paused ? "stop" : "start", hr);
168     Leave();
169     (void) date;
170 }
171
172 static void Flush(audio_output_t *aout, bool wait)
173 {
174     aout_sys_t *sys = aout->sys;
175     HRESULT hr;
176
177     if (wait)
178         return; /* Drain not implemented */
179
180     Enter();
181     IAudioClient_Stop(sys->client);
182     hr = IAudioClient_Reset(sys->client);
183     if (FAILED(hr))
184         msg_Warn(aout, "cannot reset stream (error 0x%lx)", hr);
185     Leave();
186 }
187
188 static int SimpleVolumeSet(audio_output_t *aout, float vol)
189 {
190     aout_sys_t *sys = aout->sys;
191     HRESULT hr;
192
193     if (TryEnter(aout))
194         return -1;
195     hr = ISimpleAudioVolume_SetMasterVolume(sys->volume.simple, vol, NULL);
196     if (FAILED(hr))
197         msg_Warn(aout, "cannot set session volume (error 0x%lx)", hr);
198     Leave();
199     return FAILED(hr) ? -1 : 0;
200 }
201
202 static int SimpleMuteSet(audio_output_t *aout, bool mute)
203 {
204     aout_sys_t *sys = aout->sys;
205     HRESULT hr;
206
207     if (TryEnter(aout))
208         return -1;
209     hr = ISimpleAudioVolume_SetMute(sys->volume.simple, mute, NULL);
210     if (FAILED(hr))
211         msg_Warn(aout, "cannot mute session (error 0x%lx)", hr);
212     Leave();
213     return FAILED(hr) ? -1 : 0;
214 }
215
216 static int DeviceChanged(vlc_object_t *obj, const char *varname,
217                          vlc_value_t prev, vlc_value_t cur, void *data)
218 {
219     aout_ChannelsRestart(obj, varname, prev, cur, data);
220
221     if (!var_Type (obj, "wasapi-audio-device"))
222         var_Create (obj, "wasapi-audio-device", VLC_VAR_STRING);
223     var_SetString (obj, "wasapi-audio-device", cur.psz_string);
224     return VLC_SUCCESS;
225 }
226
227 static void GetDevices(vlc_object_t *obj, IMMDeviceEnumerator *it)
228 {
229     HRESULT hr;
230     vlc_value_t val, text;
231
232     text.psz_string = _("Audio Device");
233     var_Change (obj, "audio-device", VLC_VAR_SETTEXT, &text, NULL);
234
235     IMMDeviceCollection *devs;
236     hr = IMMDeviceEnumerator_EnumAudioEndpoints(it, eRender,
237                                                 DEVICE_STATE_ACTIVE, &devs);
238     if (FAILED(hr))
239     {
240         msg_Warn (obj, "cannot enumerate audio endpoints (error 0x%lx)", hr);
241         return;
242     }
243
244     UINT n;
245     hr = IMMDeviceCollection_GetCount(devs, &n);
246     if (FAILED(hr))
247         n = 0;
248     while (n > 0)
249     {
250         IMMDevice *dev;
251
252         hr = IMMDeviceCollection_Item(devs, --n, &dev);
253         if (FAILED(hr))
254             continue;
255
256         /* Unique device ID */
257         LPWSTR devid;
258         hr = IMMDevice_GetId(dev, &devid);
259         if (FAILED(hr))
260         {
261             IMMDevice_Release(dev);
262             continue;
263         }
264         val.psz_string = FromWide(devid);
265         CoTaskMemFree(devid);
266         text.psz_string = val.psz_string;
267
268         /* User-readable device name */
269         IPropertyStore *props;
270         hr = IMMDevice_OpenPropertyStore(dev, STGM_READ, &props);
271         if (SUCCEEDED(hr))
272         {
273             PROPVARIANT v;
274
275             PropVariantInit(&v);
276 #ifdef FIXED
277             hr = IPropertyStore_GetValue(props, PKEY_Device_FriendlyName, &v);
278             if (SUCCEEDED(hr))
279                 text.psz_string = FromWide(v.pwszVal);
280 #endif
281             PropVariantClear(&v);
282             IPropertyStore_Release(props);
283         }
284         IMMDevice_Release(dev);
285
286         var_Change(obj, "audio-device", VLC_VAR_ADDCHOICE, &val, &text);
287         if (likely(text.psz_string != val.psz_string))
288             free(text.psz_string);
289         free(val.psz_string);
290     }
291     IMMDeviceCollection_Release(devs);
292 }
293
294
295 static void vlc_ToWave(WAVEFORMATEXTENSIBLE *restrict wf,
296                        audio_sample_format_t *restrict audio)
297 {
298     switch (audio->i_format)
299     {
300         case VLC_CODEC_FL64:
301             audio->i_format = VLC_CODEC_FL32;
302         case VLC_CODEC_FL32:
303             wf->SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
304             break;
305
306         case VLC_CODEC_S8:
307         case VLC_CODEC_U8:
308             audio->i_format = VLC_CODEC_S16N;
309         case VLC_CODEC_S16N:
310             wf->SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
311             break;
312
313         default:
314             audio->i_format = VLC_CODEC_FL32;
315             wf->SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
316             break;
317      }
318      aout_FormatPrepare (audio);
319
320      wf->Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
321      wf->Format.nChannels = audio->i_channels;
322      wf->Format.nSamplesPerSec = audio->i_rate;
323      wf->Format.nAvgBytesPerSec = audio->i_bytes_per_frame * audio->i_rate;
324      wf->Format.nBlockAlign = audio->i_bytes_per_frame;
325      wf->Format.wBitsPerSample = audio->i_bitspersample;
326      wf->Format.cbSize = sizeof (*wf) - sizeof (wf->Format);
327
328      wf->Samples.wValidBitsPerSample = audio->i_bitspersample;
329
330      wf->dwChannelMask = 0;
331      if (audio->i_physical_channels & AOUT_CHAN_LEFT)
332          wf->dwChannelMask |= SPEAKER_FRONT_LEFT;
333      if (audio->i_physical_channels & AOUT_CHAN_RIGHT)
334          wf->dwChannelMask |= SPEAKER_FRONT_RIGHT;
335      if (audio->i_physical_channels & AOUT_CHAN_CENTER)
336          wf->dwChannelMask |= SPEAKER_FRONT_CENTER;
337      if (audio->i_physical_channels & AOUT_CHAN_LFE)
338          wf->dwChannelMask |= SPEAKER_LOW_FREQUENCY;
339      // TODO: reorder
340      if (audio->i_physical_channels & AOUT_CHAN_REARLEFT)
341          wf->dwChannelMask |= SPEAKER_BACK_LEFT;
342      if (audio->i_physical_channels & AOUT_CHAN_REARRIGHT)
343          wf->dwChannelMask |= SPEAKER_BACK_RIGHT;
344      /* ... */
345      if (audio->i_physical_channels & AOUT_CHAN_REARCENTER)
346          wf->dwChannelMask |= SPEAKER_BACK_CENTER;
347      if (audio->i_physical_channels & AOUT_CHAN_MIDDLELEFT)
348          wf->dwChannelMask |= SPEAKER_SIDE_LEFT;
349      if (audio->i_physical_channels & AOUT_CHAN_MIDDLERIGHT)
350          wf->dwChannelMask |= SPEAKER_SIDE_RIGHT;
351      /* ... */
352 }
353
354 static int vlc_FromWave(const WAVEFORMATEX *restrict wf,
355                         audio_sample_format_t *restrict audio)
356 {
357     /* FIXME? different sample format? possible? */
358     audio->i_rate = wf->nSamplesPerSec;
359     /* FIXME */
360     if (wf->nChannels != audio->i_channels)
361         return -1;
362
363     aout_FormatPrepare(audio);
364     return 0;
365 }
366
367 static wchar_t *var_InheritWide(vlc_object_t *obj, const char *name)
368 {
369     char *v8 = var_InheritString(obj, name);
370     if (v8 == NULL)
371         return NULL;
372
373     wchar_t *v16 = ToWide(v8);
374     free(v8);
375     return v16;
376 }
377 #define var_InheritWide(o,n) var_InheritWide(VLC_OBJECT(o),n)
378
379 static int var_SetWide(vlc_object_t *obj, const char *name, const wchar_t *val)
380 {
381     char *str = FromWide(val);
382     if (unlikely(str == NULL))
383         return VLC_ENOMEM;
384
385     int ret = var_SetString(obj, name, str);
386     free(str);
387     return ret;
388 }
389 #define var_SetWide(o,n,v) var_SetWide(VLC_OBJECT(o),n,v)
390
391 /* Dummy thread to create and release COM interfaces when needed. */
392 static void MTAThread(void *data)
393 {
394     audio_output_t *aout = data;
395     aout_sys_t *sys = aout->sys;
396     HRESULT hr;
397
398     Enter();
399
400     hr = IAudioClient_GetService(sys->client, &IID_IAudioRenderClient,
401                                  (void **)&sys->render);
402     if (FAILED(hr))
403     {
404         msg_Err(aout, "cannot get audio render service (error 0x%lx)", hr);
405         goto fail;
406     }
407
408     hr = IAudioClient_GetService(sys->client, &IID_IAudioClock,
409                                  (void **)&sys->clock);
410     if (FAILED(hr))
411         msg_Warn(aout, "cannot get audio clock (error 0x%lx)", hr);
412
413     /*if (AOUT_FMT_LINEAR(&format) && !exclusive)*/
414     {
415         hr = IAudioClient_GetService(sys->client, &IID_ISimpleAudioVolume,
416                                      (void **)&sys->volume.simple);
417     }
418
419     hr = IAudioClient_GetService(sys->client, &IID_IAudioSessionControl,
420                                  (void **)&sys->control);
421     if (FAILED(hr))
422         msg_Warn(aout, "cannot get audio session control (error 0x%lx)", hr);
423     else
424     {
425         wchar_t *ua = var_InheritWide(aout, "user-agent");
426         IAudioSessionControl_SetDisplayName(sys->control, ua, NULL);
427         free(ua);
428     }
429
430     /* do nothing until the audio session terminates */
431     ReleaseSemaphore(sys->ready, 1, NULL);
432     WaitForSingleObject(sys->done, INFINITE);
433
434     if (sys->control != NULL)
435         IAudioSessionControl_Release(sys->control);
436     /*if (AOUT_FMT_LINEAR(&format) && !exclusive)*/
437     {
438         if (sys->volume.simple != NULL)
439             ISimpleAudioVolume_Release(sys->volume.simple);
440     }
441     if (sys->clock != NULL)
442         IAudioClock_Release(sys->clock);
443     IAudioRenderClient_Release(sys->render);
444 fail:
445     Leave();
446     ReleaseSemaphore(sys->ready, 1, NULL);
447 }
448
449 static int Open(vlc_object_t *obj)
450 {
451     audio_output_t *aout = (audio_output_t *)obj;
452     HRESULT hr;
453
454     if (AOUT_FMT_SPDIF(&aout->format) && !aout->b_force
455      && var_InheritBool(aout, "spdif"))
456         /* Fallback to other plugin until pass-through is implemented */
457         return VLC_EGENERIC;
458
459     aout_sys_t *sys = malloc(sizeof (*sys));
460     if (unlikely(sys == NULL))
461         return VLC_ENOMEM;
462     sys->client = NULL;
463     sys->render = NULL;
464     sys->clock = NULL;
465     sys->ready = NULL;
466     sys->done = NULL;
467     aout->sys = sys;
468
469     if (TryEnter(aout))
470     {
471         free(sys);
472         return VLC_EGENERIC;
473     }
474
475     /* Get audio device according to policy */
476     var_Create (aout, "audio-device", VLC_VAR_STRING|VLC_VAR_HASCHOICE);
477
478     IMMDeviceEnumerator *devs;
479     hr = CoCreateInstance(&CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL,
480                           &IID_IMMDeviceEnumerator, (void **)&devs);
481     if (FAILED(hr))
482     {
483         msg_Dbg(aout, "cannot create device enumerator (error 0x%lx)", hr);
484         goto error;
485     }
486
487     // Without configuration item, the variable must be created explicitly.
488     var_Create (aout, "wasapi-audio-device", VLC_VAR_STRING);
489     LPWSTR devid = var_InheritWide (aout, "wasapi-audio-device");
490     var_Destroy (aout, "wasapi-audio-device");
491
492     IMMDevice *dev = NULL;
493     if (devid != NULL)
494     {
495         msg_Dbg (aout, "using selected device %ls", devid);
496         hr = IMMDeviceEnumerator_GetDevice (devs, devid, &dev);
497         if (FAILED(hr))
498             msg_Warn(aout, "cannot get audio endpoint (error 0x%lx)", hr);
499         free (devid);
500     }
501     if (dev == NULL)
502     {
503         msg_Dbg (aout, "using default device");
504         hr = IMMDeviceEnumerator_GetDefaultAudioEndpoint(devs, eRender,
505                                                          eConsole, &dev);
506     }
507
508     GetDevices(VLC_OBJECT(aout), devs);
509     IMMDeviceEnumerator_Release(devs);
510     if (FAILED(hr))
511     {
512         msg_Err(aout, "cannot get audio endpoint (error 0x%lx)", hr);
513         goto error;
514     }
515
516     hr = IMMDevice_GetId(dev, &devid);
517     if (SUCCEEDED(hr))
518     {
519         msg_Dbg(aout, "using device %ls", devid);
520         var_SetWide (aout, "audio-device", devid);
521         CoTaskMemFree(devid);
522     }
523
524     hr = IMMDevice_Activate(dev, &IID_IAudioClient, CLSCTX_ALL, NULL,
525                             (void **)&sys->client);
526     IMMDevice_Release(dev);
527     if (FAILED(hr))
528     {
529         msg_Err(aout, "cannot activate audio client (error 0x%lx)", hr);
530         goto error;
531     }
532
533     /* Configure audio stream */
534     audio_sample_format_t format = aout->format;
535     WAVEFORMATEXTENSIBLE wf;
536     WAVEFORMATEX *pwf;
537
538     vlc_ToWave(&wf, &format);
539     hr = IAudioClient_IsFormatSupported(sys->client, AUDCLNT_SHAREMODE_SHARED,
540                                         &wf.Format, &pwf);
541     // TODO: deal with (hr == AUDCLNT_E_DEVICE_INVALIDATED) ?
542     if (FAILED(hr))
543     {
544         msg_Err(aout, "cannot negotiate audio format (error 0x%lx)", hr);
545         goto error;
546     }
547
548     if (hr == S_FALSE)
549     {
550         assert(pwf != NULL);
551         if (vlc_FromWave(pwf, &format))
552         {
553             CoTaskMemFree(pwf);
554             msg_Err(aout, "unsupported audio format");
555             goto error;
556         }
557         msg_Dbg(aout, "modified format");
558     }
559     else
560         assert(pwf == NULL);
561     hr = IAudioClient_Initialize(sys->client, AUDCLNT_SHAREMODE_SHARED, 0,
562                                  AOUT_MAX_PREPARE_TIME * 10, 0,
563                                  (hr == S_OK) ? &wf.Format : pwf, NULL);
564     CoTaskMemFree(pwf);
565     if (FAILED(hr))
566     {
567         msg_Err(aout, "cannot initialize audio client (error 0x%lx)", hr);
568         goto error;
569     }
570
571     hr = IAudioClient_GetBufferSize(sys->client, &sys->frames);
572     if (FAILED(hr))
573     {
574         msg_Err(aout, "cannot get buffer size (error 0x%lx)", hr);
575         goto error;
576     }
577
578     sys->ready = CreateSemaphore(NULL, 0, 1, NULL);
579     sys->done = CreateSemaphore(NULL, 0, 1, NULL);
580     if (unlikely(sys->ready == NULL || sys->done == NULL))
581         goto error;
582     /* Note: thread handle released by CRT, ignore it. */
583     if (_beginthread(MTAThread, 0, aout) == (uintptr_t)-1)
584         goto error;
585
586     WaitForSingleObject(sys->ready, INFINITE);
587     if (sys->render == NULL)
588         goto error;
589
590     Leave();
591
592     aout->format = format;
593     aout->pf_play = Play;
594     aout->pf_pause = Pause;
595     aout->pf_flush = Flush;
596     /*if (AOUT_FMT_LINEAR(&format) && !exclusive)*/
597     {
598         aout->volume_set = SimpleVolumeSet;
599         aout->mute_set = SimpleMuteSet;
600     }
601     var_AddCallback (aout, "audio-device", DeviceChanged, NULL);
602
603     return VLC_SUCCESS;
604 error:
605     if (sys->done != NULL)
606         CloseHandle(sys->done);
607     if (sys->ready != NULL)
608         CloseHandle(sys->done);
609     if (sys->client != NULL)
610         IAudioClient_Release(sys->client);
611     Leave();
612     var_Destroy (aout, "audio-device");
613     free(sys);
614     return VLC_EGENERIC;
615 }
616
617 static void Close (vlc_object_t *obj)
618 {
619     audio_output_t *aout = (audio_output_t *)obj;
620     aout_sys_t *sys = aout->sys;
621
622     Enter();
623     ReleaseSemaphore(sys->done, 1, NULL); /* tell MTA thread to finish */
624     WaitForSingleObject(sys->ready, INFINITE); /* wait for that ^ */
625     IAudioClient_Stop(sys->client); /* should not be needed */
626     IAudioClient_Release(sys->client);
627     Leave();
628
629     var_DelCallback (aout, "audio-device", DeviceChanged, NULL);
630     var_Destroy (aout, "audio-device");
631
632     CloseHandle(sys->done);
633     CloseHandle(sys->ready);
634     free(sys);
635 }