]> git.sesse.net Git - vlc/commitdiff
wasapi: audio capture client module (fixes #7205)
authorRémi Denis-Courmont <remi@remlab.net>
Sun, 2 Mar 2014 15:04:35 +0000 (17:04 +0200)
committerRémi Denis-Courmont <remi@remlab.net>
Sun, 22 Mar 2015 19:32:01 +0000 (21:32 +0200)
NEWS
modules/MODULES_LIST
modules/access/Makefile.am
modules/access/wasapi.c [new file with mode: 0644]
po/POTFILES.in

diff --git a/NEWS b/NEWS
index 8b85a6fbbb39775e01813b49692583ca4b088eb9..3daee6e951c46ba08abd921a74fd6e781e3bfdd5 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -18,6 +18,7 @@ Access:
  * Support depayloading Opus from RTP
  * Support sftp username and passwords options and URL
  * New UPnP access module, to list directories without infinite recursions
+ * New WASAPI audio capture module on Windows
 
 Decoder:
  * OMX GPU-zerocopy support for decoding and display on Android using OpenMax IL
index f902bc6a2c5bd70eedb53099fad37e5d0c6a693a..5d7814d4b88baf7e4797f4415bdfa18fa06e7214 100644 (file)
@@ -19,6 +19,7 @@ $Id$
  * access_output_shout: Shoutcast access output
  * access_output_udp: UDP Network access_output module
  * access_realrtsp: Real RTSP access
+ * access_wasapi: WASAPI audio input
  * addonsfsstorage: Local storage extensions repository
  * addonsvorepository: Videolan extensions repository
  * adjust: Contrast/Hue/saturation/Brightness adjust module
index 3088fdbcf686289d15532693fee0cfc79285f4d8..c499d460c60b27f9b40ff4ef63a7947215b5940d 100644 (file)
@@ -105,6 +105,14 @@ if HAVE_QTKIT
 access_LTLIBRARIES += libqtsound_plugin.la
 endif
 
+libaccess_wasapi_plugin_la_SOURCES = access/wasapi.c
+libaccess_wasapi_plugin_la_LIBADD = -lole32 -lksuser
+if HAVE_WASAPI
+if !HAVE_WINSTORE
+access_LTLIBRARIES += libaccess_wasapi_plugin.la
+endif
+endif
+
 
 ### Video capture ###
 
diff --git a/modules/access/wasapi.c b/modules/access/wasapi.c
new file mode 100644 (file)
index 0000000..5712108
--- /dev/null
@@ -0,0 +1,472 @@
+/**
+ * \file wasapi.c
+ * \brief Windows Audio Session API capture plugin for VLC
+ */
+/*****************************************************************************
+ * Copyright (C) 2014-2015 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 <assert.h>
+#include <stdlib.h>
+
+#include <vlc_common.h>
+#include <vlc_aout.h>
+#include <vlc_demux.h>
+#include <vlc_plugin.h>
+#include <mmdeviceapi.h>
+#include <audioclient.h>
+
+static LARGE_INTEGER freq; /* performance counters frequency */
+
+BOOL WINAPI DllMain(HINSTANCE, DWORD, LPVOID); /* avoid warning */
+
+BOOL WINAPI DllMain(HINSTANCE dll, DWORD reason, LPVOID reserved)
+{
+    (void) dll;
+    (void) reserved;
+
+    switch (reason)
+    {
+        case DLL_PROCESS_ATTACH:
+            if (!QueryPerformanceFrequency(&freq))
+                return FALSE;
+            break;
+    }
+    return TRUE;
+}
+
+static UINT64 GetQPC(void)
+{
+    LARGE_INTEGER counter;
+
+    if (!QueryPerformanceCounter(&counter))
+        abort();
+
+    lldiv_t d = lldiv(counter.QuadPart, freq.QuadPart);
+    return (d.quot * 10000000) + ((d.rem * 10000000) / freq.QuadPart);
+}
+
+static_assert(CLOCK_FREQ * 10 == 10000000,
+              "REFERENCE_TIME conversion broken");
+
+static IAudioClient *GetClient(demux_t *demux)
+{
+    IMMDeviceEnumerator *e;
+    IMMDevice *dev;
+    void *pv;
+    HRESULT hr;
+
+    hr = CoCreateInstance(&CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL,
+                          &IID_IMMDeviceEnumerator, &pv);
+    if (FAILED(hr))
+    {
+        msg_Err(demux, "cannot create device enumerator (error 0x%lx)", hr);
+        return NULL;
+    }
+    e = pv;
+
+    hr = IMMDeviceEnumerator_GetDefaultAudioEndpoint(e, eCapture,
+                                                     eCommunications, &dev);
+    IMMDeviceEnumerator_Release(e);
+    if (FAILED(hr))
+    {
+        msg_Err(demux, "cannot get default device (error 0x%lx)", hr);
+        return NULL;
+    }
+
+    hr = IMMDevice_Activate(dev, &IID_IAudioClient, CLSCTX_ALL, NULL, &pv);
+    IMMDevice_Release(dev);
+    if (FAILED(hr))
+        msg_Err(demux, "cannot activate device (error 0x%lx)", hr);
+    return pv;
+}
+
+static int vlc_FromWave(const WAVEFORMATEX *restrict wf,
+                        audio_sample_format_t *restrict fmt)
+{
+    fmt->i_rate = wf->nSamplesPerSec;
+
+    /* As per MSDN, IAudioClient::GetMixFormat() always uses this format. */
+    assert(wf->wFormatTag == WAVE_FORMAT_EXTENSIBLE);
+
+    const WAVEFORMATEXTENSIBLE *wfe = (void *)wf;
+
+    fmt->i_physical_channels = 0;
+    if (wfe->dwChannelMask & SPEAKER_FRONT_LEFT)
+        fmt->i_physical_channels |= AOUT_CHAN_LEFT;
+    if (wfe->dwChannelMask & SPEAKER_FRONT_RIGHT)
+        fmt->i_physical_channels |= AOUT_CHAN_RIGHT;
+    if (wfe->dwChannelMask & SPEAKER_FRONT_CENTER)
+        fmt->i_physical_channels |= AOUT_CHAN_CENTER;
+    if (wfe->dwChannelMask & SPEAKER_LOW_FREQUENCY)
+        fmt->i_physical_channels |= AOUT_CHAN_LFE;
+
+    fmt->i_original_channels = fmt->i_physical_channels;
+    assert(popcount(wfe->dwChannelMask) == wf->nChannels);
+
+    if (IsEqualIID(&wfe->SubFormat, &KSDATAFORMAT_SUBTYPE_PCM))
+    {
+        switch (wf->wBitsPerSample)
+        {
+            case 32:
+                switch (wfe->Samples.wValidBitsPerSample)
+                {
+                    case 32:
+                        fmt->i_format = VLC_CODEC_S32N;
+                        break;
+                    case 24:
+#ifdef WORDS_BIGENDIAN
+                        fmt->i_format = VLC_CODEC_S24B32;
+#else
+                        fmt->i_format = VLC_CODEC_S24L32;
+#endif
+                        break;
+                    default:
+                        return -1;
+                }
+                break;
+            case 24:
+                if (wfe->Samples.wValidBitsPerSample == 24)
+                    fmt->i_format = VLC_CODEC_S24N;
+                else
+                    return -1;
+                break;
+            case 16:
+                if (wfe->Samples.wValidBitsPerSample == 16)
+                    fmt->i_format = VLC_CODEC_S16N;
+                else
+                    return -1;
+                break;
+            case 8:
+                if (wfe->Samples.wValidBitsPerSample == 8)
+                    fmt->i_format = VLC_CODEC_S8;
+                else
+                    return -1;
+                break;
+            default:
+                return -1;
+        }
+    }
+    else if (IsEqualIID(&wfe->SubFormat, &KSDATAFORMAT_SUBTYPE_IEEE_FLOAT))
+    {
+        if (wf->wBitsPerSample != wfe->Samples.wValidBitsPerSample)
+            return -1;
+
+        switch (wf->wBitsPerSample)
+        {
+            case 64:
+                fmt->i_format = VLC_CODEC_FL64;
+                break;
+            case 32:
+                fmt->i_format = VLC_CODEC_FL32;
+                break;
+            default:
+                return -1;
+        }
+    }
+    /*else if (IsEqualIID(&wfe->Subformat, &KSDATAFORMAT_SUBTYPE_DRM)) {} */
+    else if (IsEqualIID(&wfe->SubFormat, &KSDATAFORMAT_SUBTYPE_ALAW))
+        fmt->i_format = VLC_CODEC_ALAW;
+    else if (IsEqualIID(&wfe->SubFormat, &KSDATAFORMAT_SUBTYPE_MULAW))
+        fmt->i_format = VLC_CODEC_MULAW;
+    else if (IsEqualIID(&wfe->SubFormat, &KSDATAFORMAT_SUBTYPE_ADPCM))
+        fmt->i_format = VLC_CODEC_ADPCM_MS;
+    else
+        return -1;
+
+    aout_FormatPrepare(fmt);
+    if (wf->nChannels != fmt->i_channels)
+        return -1;
+
+    return 0;
+}
+
+static es_out_id_t *CreateES(demux_t *demux, IAudioClient *client,
+                             mtime_t caching, size_t *restrict frame_size)
+{
+    es_format_t fmt;
+    WAVEFORMATEX *pwf;
+    HRESULT hr;
+
+    hr = IAudioClient_GetMixFormat(client, &pwf);
+    if (FAILED(hr))
+    {
+        msg_Err(demux, "cannot get mix format (error 0x%lx)", hr);
+        return NULL;
+    }
+
+    es_format_Init(&fmt, AUDIO_ES, 0);
+    if (vlc_FromWave(pwf, &fmt.audio))
+    {
+        msg_Err(demux, "unsupported mix format");
+        CoTaskMemFree(pwf);
+        return NULL;
+    }
+
+    fmt.i_codec = fmt.audio.i_format;
+    fmt.i_bitrate = fmt.audio.i_bitspersample * fmt.audio.i_channels
+                                              * fmt.audio.i_rate;
+    *frame_size = fmt.audio.i_bitspersample * fmt.audio.i_channels / 8;
+
+    DWORD flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK; /* TODO: loopback */
+    /* Request at least thrice the PTS delay */
+    REFERENCE_TIME bufsize = caching * INT64_C(10) * 3;
+
+    hr = IAudioClient_Initialize(client, AUDCLNT_SHAREMODE_SHARED, flags,
+                                 bufsize, 0, pwf, NULL);
+    CoTaskMemFree(pwf);
+    if (FAILED(hr))
+    {
+        msg_Err(demux, "cannot initialize audio client (error 0x%lx)", hr);
+        return NULL;
+    }
+    return es_out_Add(demux->out, &fmt);
+}
+
+struct demux_sys_t
+{
+    IAudioClient *client;
+    es_out_id_t *es;
+
+    size_t frame_size;
+    mtime_t caching;
+    mtime_t start_time;
+
+    HANDLE events[2];
+    union {
+        HANDLE thread;
+        HANDLE ready;
+    };
+};
+
+static unsigned __stdcall Thread(void *data)
+{
+    demux_t *demux = data;
+    demux_sys_t *sys = demux->p_sys;
+    IAudioCaptureClient *capture = NULL;
+    void *pv;
+    HRESULT hr;
+
+    hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
+    assert(SUCCEEDED(hr)); /* COM already allocated by parent thread */
+    SetEvent(sys->ready);
+
+    hr = IAudioClient_GetService(sys->client, &IID_IAudioCaptureClient, &pv);
+    if (FAILED(hr))
+    {
+        msg_Err(demux, "cannot get capture client (error 0x%lx)", hr);
+        goto out;
+    }
+    capture = pv;
+
+    hr = IAudioClient_Start(sys->client);
+    if (FAILED(hr))
+    {
+        msg_Err(demux, "cannot start client (error 0x%lx)", hr);
+        IAudioCaptureClient_Release(capture);
+        goto out;
+    }
+
+    while (WaitForMultipleObjects(2, sys->events, FALSE, INFINITE)
+            != WAIT_OBJECT_0)
+    {
+        BYTE *data;
+        UINT32 frames;
+        DWORD flags;
+        UINT64 qpc;
+        mtime_t pts;
+
+        hr = IAudioCaptureClient_GetBuffer(capture, &data, &frames, &flags,
+                                           NULL, &qpc);
+        if (hr != S_OK)
+            continue;
+
+        pts = mdate() - ((GetQPC() - qpc) / 10);
+
+        es_out_Control(demux->out, ES_OUT_SET_PCR, pts);
+
+        size_t bytes = frames * sys->frame_size;
+        block_t *block = block_Alloc(bytes);
+
+        if (likely(block != NULL)) {
+            memcpy(block->p_buffer, data, bytes);
+            block->i_nb_samples = frames;
+            block->i_pts = block->i_dts = pts;
+            es_out_Send(demux->out, sys->es, block);
+        }
+
+        IAudioCaptureClient_ReleaseBuffer(capture, frames);
+    }
+
+    IAudioClient_Stop(sys->client);
+    IAudioCaptureClient_Release(capture);
+out:
+    CoUninitialize();
+    return 0;
+}
+
+static int Control(demux_t *demux, int query, va_list ap)
+{
+    demux_sys_t *sys = demux->p_sys;
+
+    switch (query)
+    {
+        case DEMUX_GET_TIME:
+            *(va_arg(ap, int64_t *)) = mdate() - sys->start_time;
+            break;
+
+        case DEMUX_GET_PTS_DELAY:
+            *(va_arg(ap, int64_t *)) = sys->caching;
+            break;
+
+        case DEMUX_HAS_UNSUPPORTED_META:
+        case DEMUX_CAN_RECORD:
+        case DEMUX_CAN_PAUSE:
+        case DEMUX_CAN_CONTROL_PACE:
+        case DEMUX_CAN_CONTROL_RATE:
+        case DEMUX_CAN_SEEK:
+            *(va_arg(ap, bool *)) = false;
+            break;
+
+        default:
+            return VLC_EGENERIC;
+    }
+
+    return VLC_SUCCESS;
+}
+
+static int Open(vlc_object_t *obj)
+{
+    demux_t *demux = (demux_t *)obj;
+    HRESULT hr;
+
+    if (demux->psz_location != NULL && demux->psz_location != '\0')
+        return VLC_EGENERIC; /* TODO non-default device */
+
+    demux_sys_t *sys = malloc(sizeof (*sys));
+    if (unlikely(sys == NULL))
+        return VLC_ENOMEM;
+
+    sys->client = NULL;
+    sys->es = NULL;
+    sys->caching = INT64_C(1000) * var_InheritInteger(obj, "live-caching");
+    sys->start_time = mdate();
+    for (unsigned i = 0; i < 2; i++)
+        sys->events[i] = NULL;
+
+    for (unsigned i = 0; i < 2; i++) {
+        sys->events[i] = CreateEvent(NULL, FALSE, FALSE, NULL);
+        if (sys->events[i] == NULL)
+            goto error;
+    }
+
+    hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
+    if (unlikely(FAILED(hr))) {
+        msg_Err(demux, "cannot initialize COM (error 0x%lx)", hr);
+        goto error;
+    }
+
+    sys->client = GetClient(demux);
+    if (sys->client == NULL) {
+        CoUninitialize();
+        goto error;
+    }
+
+    sys->es = CreateES(demux, sys->client, sys->caching, &sys->frame_size);
+    if (sys->es == NULL)
+        goto error;
+
+    hr = IAudioClient_SetEventHandle(sys->client, sys->events[1]);
+    if (FAILED(hr)) {
+        msg_Err(demux, "cannot set event handle (error 0x%lx)", hr);
+        goto error;
+    }
+
+    demux->p_sys = sys;
+
+    sys->ready = CreateEvent(NULL, FALSE, FALSE, NULL);
+    if (sys->ready == NULL)
+        goto error;
+
+    uintptr_t h = _beginthreadex(NULL, 0, Thread, demux, 0, NULL);
+    if (h != 0)
+        WaitForSingleObject(sys->ready, INFINITE);
+    CloseHandle(sys->ready);
+
+    sys->thread = (HANDLE)h;
+    if (sys->thread == NULL)
+        goto error;
+    CoUninitialize();
+
+    demux->pf_demux = NULL;
+    demux->pf_control = Control;
+    return VLC_SUCCESS;
+
+error:
+    if (sys->es != NULL)
+        es_out_Del(demux->out, sys->es);
+    if (sys->client != NULL)
+    {
+        IAudioClient_Release(sys->client);
+        CoUninitialize();
+    }
+    for (unsigned i = 0; i < 2; i++)
+        if (sys->events[i] != NULL)
+            CloseHandle(sys->events[i]);
+    free(sys);
+    return VLC_ENOMEM;
+}
+
+static void Close (vlc_object_t *obj)
+{
+    demux_t *demux = (demux_t *)obj;
+    demux_sys_t *sys = demux->p_sys;
+    HRESULT hr;
+
+    hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
+    assert(SUCCEEDED(hr));
+
+    SetEvent(sys->events[0]);
+    WaitForSingleObject(sys->thread, INFINITE);
+    CloseHandle(sys->thread);
+
+    es_out_Del(demux->out, sys->es);
+    IAudioClient_Release(sys->client);
+    CoUninitialize();
+    for (unsigned i = 0; i < 2; i++)
+        CloseHandle(sys->events[i]);
+    free(sys);
+}
+
+vlc_module_begin()
+    set_shortname(N_("WASAPI"))
+    set_description(N_("Windows Audio Session API input"))
+    set_capability("access_demux", 0)
+    set_category(CAT_INPUT)
+    set_subcategory(SUBCAT_INPUT_ACCESS)
+
+    add_shortcut("wasapi")
+    set_callbacks(Open, Close)
+vlc_module_end()
index 0f697cd001a5f901a4fb6674f1273e210fbd5f6a..e7c680bdf09986bfe4fbaebe5d550d974e66a4ca 100644 (file)
@@ -278,6 +278,7 @@ modules/access/vcdx/vcdplayer.c
 modules/access/vcdx/vcdplayer.h
 modules/access/vdr.c
 modules/access/vnc.c
+modules/access/wasapi.c
 modules/access/zip/zipstream.c
 modules/arm_neon/chroma_yuv.c
 modules/arm_neon/simple_channel_mixer.c