diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 20:36:56 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 20:36:56 +0000 |
commit | 51de1d8436100f725f3576aefa24a2bd2057bc28 (patch) | |
tree | c6d1d5264b6d40a8d7ca34129f36b7d61e188af3 /audio/out/ao_wasapi.c | |
parent | Initial commit. (diff) | |
download | mpv-51de1d8436100f725f3576aefa24a2bd2057bc28.tar.xz mpv-51de1d8436100f725f3576aefa24a2bd2057bc28.zip |
Adding upstream version 0.37.0.upstream/0.37.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'audio/out/ao_wasapi.c')
-rw-r--r-- | audio/out/ao_wasapi.c | 504 |
1 files changed, 504 insertions, 0 deletions
diff --git a/audio/out/ao_wasapi.c b/audio/out/ao_wasapi.c new file mode 100644 index 0000000..b201f26 --- /dev/null +++ b/audio/out/ao_wasapi.c @@ -0,0 +1,504 @@ +/* + * This file is part of mpv. + * + * Original author: Jonathan Yong <10walls@gmail.com> + * + * mpv 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. + * + * mpv 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 mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <math.h> +#include <inttypes.h> +#include <libavutil/mathematics.h> + +#include "options/m_option.h" +#include "osdep/threads.h" +#include "osdep/timer.h" +#include "osdep/io.h" +#include "misc/dispatch.h" +#include "ao_wasapi.h" + +// naive av_rescale for unsigned +static UINT64 uint64_scale(UINT64 x, UINT64 num, UINT64 den) +{ + return (x / den) * num + + ((x % den) * (num / den)) + + ((x % den) * (num % den)) / den; +} + +static HRESULT get_device_delay(struct wasapi_state *state, double *delay_ns) +{ + UINT64 sample_count = atomic_load(&state->sample_count); + UINT64 position, qpc_position; + HRESULT hr; + + hr = IAudioClock_GetPosition(state->pAudioClock, &position, &qpc_position); + EXIT_ON_ERROR(hr); + // GetPosition succeeded, but the result may be + // inaccurate due to the length of the call + // http://msdn.microsoft.com/en-us/library/windows/desktop/dd370889%28v=vs.85%29.aspx + if (hr == S_FALSE) + MP_VERBOSE(state, "Possibly inaccurate device position.\n"); + + // convert position to number of samples careful to avoid overflow + UINT64 sample_position = uint64_scale(position, + state->format.Format.nSamplesPerSec, + state->clock_frequency); + INT64 diff = sample_count - sample_position; + *delay_ns = diff * 1e9 / state->format.Format.nSamplesPerSec; + + // Correct for any delay in IAudioClock_GetPosition above. + // This should normally be very small (<1 us), but just in case. . . + LARGE_INTEGER qpc; + QueryPerformanceCounter(&qpc); + INT64 qpc_diff = av_rescale(qpc.QuadPart, 10000000, state->qpc_frequency.QuadPart) + - qpc_position; + // ignore the above calculation if it yields more than 10 seconds (due to + // possible overflow inside IAudioClock_GetPosition) + if (qpc_diff < 10 * 10000000) { + *delay_ns -= qpc_diff * 100.0; // convert to ns + } else { + MP_VERBOSE(state, "Insane qpc delay correction of %g seconds. " + "Ignoring it.\n", qpc_diff / 10000000.0); + } + + if (sample_count > 0 && *delay_ns <= 0) { + MP_WARN(state, "Under-run: Device delay: %g ns\n", *delay_ns); + } else { + MP_TRACE(state, "Device delay: %g ns\n", *delay_ns); + } + + return S_OK; +exit_label: + MP_ERR(state, "Error getting device delay: %s\n", mp_HRESULT_to_str(hr)); + return hr; +} + +static bool thread_feed(struct ao *ao) +{ + struct wasapi_state *state = ao->priv; + HRESULT hr; + + UINT32 frame_count = state->bufferFrameCount; + UINT32 padding; + hr = IAudioClient_GetCurrentPadding(state->pAudioClient, &padding); + EXIT_ON_ERROR(hr); + bool refill = false; + if (state->share_mode == AUDCLNT_SHAREMODE_SHARED) { + // Return if there's nothing to do. + if (frame_count <= padding) + return false; + // In shared mode, there is only one buffer of size bufferFrameCount. + // We must therefore take care not to overwrite the samples that have + // yet to play. + frame_count -= padding; + } else if (padding >= 2 * frame_count) { + // In exclusive mode, we exchange entire buffers of size + // bufferFrameCount with the device. If there are already two such + // full buffers waiting to play, there is no work to do. + return false; + } else if (padding < frame_count) { + // If there is not at least one full buffer of audio queued to play in + // exclusive mode, call this function again immediately to try and catch + // up and avoid a cascade of under-runs. WASAPI doesn't seem to be smart + // enough to send more feed events when it gets behind. + refill = true; + } + MP_TRACE(ao, "Frame to fill: %"PRIu32". Padding: %"PRIu32"\n", + frame_count, padding); + + double delay_ns; + hr = get_device_delay(state, &delay_ns); + EXIT_ON_ERROR(hr); + // add the buffer delay + delay_ns += frame_count * 1e9 / state->format.Format.nSamplesPerSec; + + BYTE *pData; + hr = IAudioRenderClient_GetBuffer(state->pRenderClient, + frame_count, &pData); + EXIT_ON_ERROR(hr); + + BYTE *data[1] = {pData}; + + ao_read_data_converted(ao, &state->convert_format, + (void **)data, frame_count, + mp_time_ns() + (int64_t)llrint(delay_ns)); + + // note, we can't use ao_read_data return value here since we already + // committed to frame_count above in the GetBuffer call + hr = IAudioRenderClient_ReleaseBuffer(state->pRenderClient, + frame_count, 0); + EXIT_ON_ERROR(hr); + + atomic_fetch_add(&state->sample_count, frame_count); + + return refill; +exit_label: + MP_ERR(state, "Error feeding audio: %s\n", mp_HRESULT_to_str(hr)); + MP_VERBOSE(ao, "Requesting ao reload\n"); + ao_request_reload(ao); + return false; +} + +static void thread_reset(struct ao *ao) +{ + struct wasapi_state *state = ao->priv; + HRESULT hr; + MP_DBG(state, "Thread Reset\n"); + hr = IAudioClient_Stop(state->pAudioClient); + if (FAILED(hr)) + MP_ERR(state, "IAudioClient_Stop returned: %s\n", mp_HRESULT_to_str(hr)); + + hr = IAudioClient_Reset(state->pAudioClient); + if (FAILED(hr)) + MP_ERR(state, "IAudioClient_Reset returned: %s\n", mp_HRESULT_to_str(hr)); + + atomic_store(&state->sample_count, 0); +} + +static void thread_resume(struct ao *ao) +{ + struct wasapi_state *state = ao->priv; + MP_DBG(state, "Thread Resume\n"); + thread_reset(ao); + thread_feed(ao); + + HRESULT hr = IAudioClient_Start(state->pAudioClient); + if (FAILED(hr)) { + MP_ERR(state, "IAudioClient_Start returned %s\n", + mp_HRESULT_to_str(hr)); + } +} + +static void thread_wakeup(void *ptr) +{ + struct ao *ao = ptr; + struct wasapi_state *state = ao->priv; + SetEvent(state->hWake); +} + +static void set_thread_state(struct ao *ao, + enum wasapi_thread_state thread_state) +{ + struct wasapi_state *state = ao->priv; + atomic_store(&state->thread_state, thread_state); + thread_wakeup(ao); +} + +static DWORD __stdcall AudioThread(void *lpParameter) +{ + struct ao *ao = lpParameter; + struct wasapi_state *state = ao->priv; + mp_thread_set_name("ao/wasapi"); + CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); + + state->init_ok = wasapi_thread_init(ao); + SetEvent(state->hInitDone); + if (!state->init_ok) + goto exit_label; + + MP_DBG(ao, "Entering dispatch loop\n"); + while (true) { + if (WaitForSingleObject(state->hWake, INFINITE) != WAIT_OBJECT_0) + MP_ERR(ao, "Unexpected return value from WaitForSingleObject\n"); + + mp_dispatch_queue_process(state->dispatch, 0); + + int thread_state = atomic_load(&state->thread_state); + switch (thread_state) { + case WASAPI_THREAD_FEED: + // fill twice on under-full buffer (see comment in thread_feed) + if (thread_feed(ao) && thread_feed(ao)) + MP_ERR(ao, "Unable to fill buffer fast enough\n"); + break; + case WASAPI_THREAD_RESET: + thread_reset(ao); + break; + case WASAPI_THREAD_RESUME: + thread_resume(ao); + break; + case WASAPI_THREAD_SHUTDOWN: + thread_reset(ao); + goto exit_label; + default: + MP_ERR(ao, "Unhandled thread state: %d\n", thread_state); + } + // the default is to feed unless something else is requested + atomic_compare_exchange_strong(&state->thread_state, &thread_state, + WASAPI_THREAD_FEED); + } +exit_label: + wasapi_thread_uninit(ao); + + CoUninitialize(); + MP_DBG(ao, "Thread return\n"); + return 0; +} + +static void uninit(struct ao *ao) +{ + MP_DBG(ao, "Uninit wasapi\n"); + struct wasapi_state *state = ao->priv; + if (state->hWake) + set_thread_state(ao, WASAPI_THREAD_SHUTDOWN); + + if (state->hAudioThread && + WaitForSingleObject(state->hAudioThread, INFINITE) != WAIT_OBJECT_0) + { + MP_ERR(ao, "Unexpected return value from WaitForSingleObject " + "while waiting for audio thread to terminate\n"); + } + + SAFE_DESTROY(state->hInitDone, CloseHandle(state->hInitDone)); + SAFE_DESTROY(state->hWake, CloseHandle(state->hWake)); + SAFE_DESTROY(state->hAudioThread,CloseHandle(state->hAudioThread)); + + wasapi_change_uninit(ao); + + talloc_free(state->deviceID); + + CoUninitialize(); + MP_DBG(ao, "Uninit wasapi done\n"); +} + +static int init(struct ao *ao) +{ + MP_DBG(ao, "Init wasapi\n"); + CoInitializeEx(NULL, COINIT_MULTITHREADED); + + struct wasapi_state *state = ao->priv; + state->log = ao->log; + + state->opt_exclusive |= ao->init_flags & AO_INIT_EXCLUSIVE; + +#if !HAVE_UWP + state->deviceID = wasapi_find_deviceID(ao); + if (!state->deviceID) { + uninit(ao); + return -1; + } +#endif + + if (state->deviceID) + wasapi_change_init(ao, false); + + state->hInitDone = CreateEventW(NULL, FALSE, FALSE, NULL); + state->hWake = CreateEventW(NULL, FALSE, FALSE, NULL); + if (!state->hInitDone || !state->hWake) { + MP_FATAL(ao, "Error creating events\n"); + uninit(ao); + return -1; + } + + state->dispatch = mp_dispatch_create(state); + mp_dispatch_set_wakeup_fn(state->dispatch, thread_wakeup, ao); + + state->init_ok = false; + state->hAudioThread = CreateThread(NULL, 0, &AudioThread, ao, 0, NULL); + if (!state->hAudioThread) { + MP_FATAL(ao, "Failed to create audio thread\n"); + uninit(ao); + return -1; + } + + WaitForSingleObject(state->hInitDone, INFINITE); // wait on init complete + SAFE_DESTROY(state->hInitDone,CloseHandle(state->hInitDone)); + if (!state->init_ok) { + if (!ao->probing) + MP_FATAL(ao, "Received failure from audio thread\n"); + uninit(ao); + return -1; + } + + MP_DBG(ao, "Init wasapi done\n"); + return 0; +} + +static int thread_control_exclusive(struct ao *ao, enum aocontrol cmd, void *arg) +{ + struct wasapi_state *state = ao->priv; + if (!state->pEndpointVolume) + return CONTROL_UNKNOWN; + + switch (cmd) { + case AOCONTROL_GET_VOLUME: + case AOCONTROL_SET_VOLUME: + if (!(state->vol_hw_support & ENDPOINT_HARDWARE_SUPPORT_VOLUME)) + return CONTROL_FALSE; + break; + case AOCONTROL_GET_MUTE: + case AOCONTROL_SET_MUTE: + if (!(state->vol_hw_support & ENDPOINT_HARDWARE_SUPPORT_MUTE)) + return CONTROL_FALSE; + break; + } + + float volume; + BOOL mute; + switch (cmd) { + case AOCONTROL_GET_VOLUME: + IAudioEndpointVolume_GetMasterVolumeLevelScalar( + state->pEndpointVolume, &volume); + *(float *)arg = volume; + return CONTROL_OK; + case AOCONTROL_SET_VOLUME: + volume = (*(float *)arg) / 100.f; + IAudioEndpointVolume_SetMasterVolumeLevelScalar( + state->pEndpointVolume, volume, NULL); + return CONTROL_OK; + case AOCONTROL_GET_MUTE: + IAudioEndpointVolume_GetMute(state->pEndpointVolume, &mute); + *(bool *)arg = mute; + return CONTROL_OK; + case AOCONTROL_SET_MUTE: + mute = *(bool *)arg; + IAudioEndpointVolume_SetMute(state->pEndpointVolume, mute, NULL); + return CONTROL_OK; + } + return CONTROL_UNKNOWN; +} + +static int thread_control_shared(struct ao *ao, enum aocontrol cmd, void *arg) +{ + struct wasapi_state *state = ao->priv; + if (!state->pAudioVolume) + return CONTROL_UNKNOWN; + + float volume; + BOOL mute; + switch(cmd) { + case AOCONTROL_GET_VOLUME: + ISimpleAudioVolume_GetMasterVolume(state->pAudioVolume, &volume); + *(float *)arg = volume; + return CONTROL_OK; + case AOCONTROL_SET_VOLUME: + volume = (*(float *)arg) / 100.f; + ISimpleAudioVolume_SetMasterVolume(state->pAudioVolume, volume, NULL); + return CONTROL_OK; + case AOCONTROL_GET_MUTE: + ISimpleAudioVolume_GetMute(state->pAudioVolume, &mute); + *(bool *)arg = mute; + return CONTROL_OK; + case AOCONTROL_SET_MUTE: + mute = *(bool *)arg; + ISimpleAudioVolume_SetMute(state->pAudioVolume, mute, NULL); + return CONTROL_OK; + } + return CONTROL_UNKNOWN; +} + +static int thread_control(struct ao *ao, enum aocontrol cmd, void *arg) +{ + struct wasapi_state *state = ao->priv; + + // common to exclusive and shared + switch (cmd) { + case AOCONTROL_UPDATE_STREAM_TITLE: + if (!state->pSessionControl) + return CONTROL_FALSE; + + wchar_t *title = mp_from_utf8(NULL, (const char *)arg); + HRESULT hr = IAudioSessionControl_SetDisplayName(state->pSessionControl, + title,NULL); + talloc_free(title); + + if (SUCCEEDED(hr)) + return CONTROL_OK; + + MP_WARN(ao, "Error setting audio session name: %s\n", + mp_HRESULT_to_str(hr)); + + assert(ao->client_name); + if (!ao->client_name) + return CONTROL_ERROR; + + // Fallback to client name + title = mp_from_utf8(NULL, ao->client_name); + IAudioSessionControl_SetDisplayName(state->pSessionControl, + title, NULL); + talloc_free(title); + + return CONTROL_ERROR; + } + + return state->share_mode == AUDCLNT_SHAREMODE_EXCLUSIVE ? + thread_control_exclusive(ao, cmd, arg) : + thread_control_shared(ao, cmd, arg); +} + +static void run_control(void *p) +{ + void **pp = p; + struct ao *ao = pp[0]; + enum aocontrol cmd = *(enum aocontrol *)pp[1]; + void *arg = pp[2]; + *(int *)pp[3] = thread_control(ao, cmd, arg); +} + +static int control(struct ao *ao, enum aocontrol cmd, void *arg) +{ + struct wasapi_state *state = ao->priv; + int ret; + void *p[] = {ao, &cmd, arg, &ret}; + mp_dispatch_run(state->dispatch, run_control, p); + return ret; +} + +static void audio_reset(struct ao *ao) +{ + set_thread_state(ao, WASAPI_THREAD_RESET); +} + +static void audio_resume(struct ao *ao) +{ + set_thread_state(ao, WASAPI_THREAD_RESUME); +} + +static void hotplug_uninit(struct ao *ao) +{ + MP_DBG(ao, "Hotplug uninit\n"); + wasapi_change_uninit(ao); + CoUninitialize(); +} + +static int hotplug_init(struct ao *ao) +{ + MP_DBG(ao, "Hotplug init\n"); + struct wasapi_state *state = ao->priv; + state->log = ao->log; + CoInitializeEx(NULL, COINIT_MULTITHREADED); + HRESULT hr = wasapi_change_init(ao, true); + EXIT_ON_ERROR(hr); + + return 0; + exit_label: + MP_FATAL(state, "Error setting up audio hotplug: %s\n", mp_HRESULT_to_str(hr)); + hotplug_uninit(ao); + return -1; +} + +#define OPT_BASE_STRUCT struct wasapi_state + +const struct ao_driver audio_out_wasapi = { + .description = "Windows WASAPI audio output (event mode)", + .name = "wasapi", + .init = init, + .uninit = uninit, + .control = control, + .reset = audio_reset, + .start = audio_resume, + .list_devs = wasapi_list_devs, + .hotplug_init = hotplug_init, + .hotplug_uninit = hotplug_uninit, + .priv_size = sizeof(wasapi_state), +}; |