From a199a57b242facb48583bb8776890fba49ce240f Mon Sep 17 00:00:00 2001 From: =?utf8?q?R=C3=A9mi=20Denis-Courmont?= Date: Sun, 2 Mar 2014 17:04:35 +0200 Subject: [PATCH] wasapi: audio capture client module (fixes #7205) --- NEWS | 1 + modules/MODULES_LIST | 1 + modules/access/Makefile.am | 8 + modules/access/wasapi.c | 472 +++++++++++++++++++++++++++++++++++++ po/POTFILES.in | 1 + 5 files changed, 483 insertions(+) create mode 100644 modules/access/wasapi.c diff --git a/NEWS b/NEWS index 8b85a6fbbb..3daee6e951 100644 --- 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 diff --git a/modules/MODULES_LIST b/modules/MODULES_LIST index f902bc6a2c..5d7814d4b8 100644 --- a/modules/MODULES_LIST +++ b/modules/MODULES_LIST @@ -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 diff --git a/modules/access/Makefile.am b/modules/access/Makefile.am index 3088fdbcf6..c499d460c6 100644 --- a/modules/access/Makefile.am +++ b/modules/access/Makefile.am @@ -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 index 0000000000..5712108569 --- /dev/null +++ b/modules/access/wasapi.c @@ -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 +#include + +#include +#include +#include +#include +#include +#include + +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() diff --git a/po/POTFILES.in b/po/POTFILES.in index 0f697cd001..e7c680bdf0 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -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 -- 2.39.2