/* * 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 General Public License as published by * the Free Software Foundation; either version 2 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 General Public License for more details. * * You should have received a copy of the GNU General Public License along * with mpv. If not, see . */ #include #include #include #include #include #include #include #include #include "audio/out/ao_wasapi.h" #include "audio/out/ao_wasapi_utils.h" #include "audio/format.h" #include "osdep/timer.h" #include "osdep/io.h" static HRESULT get_device_delay(struct wasapi_state *state, double *delay) { UINT64 sample_count = atomic_load(&state->sample_count); UINT64 position, qpc_position; HRESULT hr; hr = IAudioClock_GetPosition(state->pAudioClock, &position, &qpc_position); /* 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_DBG(state, "Possibly inaccurate device position.\n"); hr = S_OK; } EXIT_ON_ERROR(hr); LARGE_INTEGER qpc_count; QueryPerformanceCounter(&qpc_count); double qpc_diff = (qpc_count.QuadPart * 1e7 / state->qpc_frequency.QuadPart) - qpc_position; position += state->clock_frequency * (uint64_t) (qpc_diff / 1e7); /* convert position to the same base as sample_count */ position = position * state->format.Format.nSamplesPerSec / state->clock_frequency; double diff = sample_count - position; *delay = diff / state->format.Format.nSamplesPerSec; MP_TRACE(state, "Device delay: %g samples (%g ms)\n", diff, *delay * 1000); return S_OK; exit_label: MP_ERR(state, "Error getting device delay: %s\n", mp_HRESULT_to_str(hr)); return hr; } static void thread_feed(struct ao *ao) { struct wasapi_state *state = ao->priv; HRESULT hr; UINT32 frame_count = state->bufferFrameCount; if (state->share_mode == AUDCLNT_SHAREMODE_SHARED) { UINT32 padding = 0; hr = IAudioClient_GetCurrentPadding(state->pAudioClient, &padding); EXIT_ON_ERROR(hr); frame_count -= padding; MP_TRACE(ao, "Frame to fill: %"PRIu32". Padding: %"PRIu32"\n", frame_count, padding); } double delay; hr = get_device_delay(state, &delay); EXIT_ON_ERROR(hr); BYTE *pData; hr = IAudioRenderClient_GetBuffer(state->pRenderClient, frame_count, &pData); EXIT_ON_ERROR(hr); BYTE *data[1] = {pData}; ao_read_data(ao, (void**)data, frame_count, (int64_t) ( mp_time_us() + delay * 1e6 + frame_count * 1e6 / state->format.Format.nSamplesPerSec)); hr = IAudioRenderClient_ReleaseBuffer(state->pRenderClient, frame_count, 0); EXIT_ON_ERROR(hr); atomic_fetch_add(&state->sample_count, frame_count); return; 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; } static void thread_resume(struct ao *ao) { struct wasapi_state *state = ao->priv; HRESULT hr; MP_DBG(state, "Thread Resume\n"); UINT32 padding = 0; hr = IAudioClient_GetCurrentPadding(state->pAudioClient, &padding); if (hr != S_OK) { MP_ERR(state, "IAudioClient_GetCurrentPadding returned %s\n", mp_HRESULT_to_str(hr)); } /* Fill the buffer before starting, but only if there is no audio queued to play. */ /* This prevents overfilling the buffer, which leads to problems in exclusive mode */ if (padding < (UINT32) state->bufferFrameCount) thread_feed(ao); // start feeding next wakeup if something else hasn't been requested int expected = WASAPI_THREAD_RESUME; atomic_compare_exchange_strong(&state->thread_state, &expected, WASAPI_THREAD_FEED); hr = IAudioClient_Start(state->pAudioClient); if (hr != S_OK) MP_ERR(state, "IAudioClient_Start returned %s\n", mp_HRESULT_to_str(hr)); return; } 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); /* we may get S_FALSE if the stream is already stopped */ if (hr != S_OK && hr != S_FALSE) MP_ERR(state, "IAudioClient_Stop returned: %s\n", mp_HRESULT_to_str(hr)); /* we may get S_FALSE if the stream is already reset */ hr = IAudioClient_Reset(state->pAudioClient); if (hr != S_OK && hr != S_FALSE) MP_ERR(state, "IAudioClient_Reset returned: %s\n", mp_HRESULT_to_str(hr)); atomic_store(&state->sample_count, 0); // start feeding next wakeup if something else hasn't been requested int expected = WASAPI_THREAD_RESET; atomic_compare_exchange_strong(&state->thread_state, &expected, WASAPI_THREAD_FEED); return; } static DWORD __stdcall AudioThread(void *lpParameter) { struct ao *ao = lpParameter; struct wasapi_state *state = ao->priv; CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); state->init_ret = wasapi_thread_init(ao); SetEvent(state->hInitDone); if (state->init_ret != S_OK) goto exit_label; MP_DBG(ao, "Entering dispatch loop\n"); while (true) { /* watch events */ HANDLE events[] = {state->hWake}; switch (MsgWaitForMultipleObjects(MP_ARRAY_SIZE(events), events, FALSE, INFINITE, QS_POSTMESSAGE | QS_SENDMESSAGE)) { /* AudioThread wakeup */ case WAIT_OBJECT_0: switch (atomic_load(&state->thread_state)) { case WASAPI_THREAD_FEED: thread_feed(ao); break; case WASAPI_THREAD_RESET: thread_reset(ao); break; case WASAPI_THREAD_RESUME: thread_reset(ao); thread_resume(ao); break; case WASAPI_THREAD_SHUTDOWN: thread_reset(ao); goto exit_label; default: MP_ERR(ao, "Unhandled thread state\n"); goto exit_label; } break; /* messages to dispatch (COM marshalling) */ case (WAIT_OBJECT_0 + MP_ARRAY_SIZE(events)): wasapi_dispatch(ao); break; default: MP_ERR(ao, "Unhandled thread event\n"); goto exit_label; } } exit_label: wasapi_thread_uninit(ao); CoUninitialize(); MP_DBG(ao, "Thread return\n"); return 0; } 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); SetEvent(state->hWake); } static void uninit(struct ao *ao) { MP_DBG(ao, "Uninit wasapi\n"); struct wasapi_state *state = ao->priv; wasapi_release_proxies(state); if (state->hWake) set_thread_state(ao, WASAPI_THREAD_SHUTDOWN); /* wait up to 10 seconds */ if (state->hAudioThread && WaitForSingleObject(state->hAudioThread, 10000) == WAIT_TIMEOUT) { MP_ERR(ao, "Audio loop thread refuses to abort\n"); return; } SAFE_RELEASE(state->hInitDone, CloseHandle(state->hInitDone)); SAFE_RELEASE(state->hWake, CloseHandle(state->hWake)); SAFE_RELEASE(state->hAudioThread,CloseHandle(state->hAudioThread)); CoUninitialize(); MP_DBG(ao, "Uninit wasapi done\n"); } static int init(struct ao *ao) { MP_DBG(ao, "Init wasapi\n"); CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); struct wasapi_state *state = ao->priv; state->log = ao->log; state->hInitDone = CreateEventW(NULL, FALSE, FALSE, NULL); state->hWake = CreateEventW(NULL, FALSE, FALSE, NULL); if (!state->hInitDone || !state->hWake) { MP_ERR(ao, "Error creating events\n"); uninit(ao); return -1; } state->init_ret = E_FAIL; state->hAudioThread = CreateThread(NULL, 0, &AudioThread, ao, 0, NULL); if (!state->hAudioThread) { MP_ERR(ao, "Failed to create audio thread\n"); uninit(ao); return -1; } WaitForSingleObject(state->hInitDone, INFINITE); /* wait on init complete */ SAFE_RELEASE(state->hInitDone,CloseHandle(state->hInitDone)); if (state->init_ret != S_OK) { if (!ao->probing) MP_ERR(ao, "Received failure from audio thread\n"); uninit(ao); return -1; } wasapi_setup_proxies(state); MP_DBG(ao, "Init wasapi done\n"); return 0; } static int control(struct ao *ao, enum aocontrol cmd, void *arg) { struct wasapi_state *state = ao->priv; float volume; BOOL mute; if (state->opt_exclusive) { // exclusive-specific switch (cmd) { case AOCONTROL_GET_VOLUME: case AOCONTROL_SET_VOLUME: if (!state->pEndpointVolumeProxy || !(state->vol_hw_support & ENDPOINT_HARDWARE_SUPPORT_VOLUME)) { return CONTROL_FALSE; } switch (cmd) { case AOCONTROL_GET_VOLUME: IAudioEndpointVolume_GetMasterVolumeLevelScalar(state->pEndpointVolumeProxy, &volume); *(ao_control_vol_t *)arg = (ao_control_vol_t){ .left = 100.0f * volume, .right = 100.0f * volume, }; return CONTROL_OK; case AOCONTROL_SET_VOLUME: volume = ((ao_control_vol_t *)arg)->left / 100.f; IAudioEndpointVolume_SetMasterVolumeLevelScalar(state->pEndpointVolumeProxy, volume, NULL); return CONTROL_OK; } case AOCONTROL_GET_MUTE: case AOCONTROL_SET_MUTE: if (!state->pEndpointVolumeProxy || !(state->vol_hw_support & ENDPOINT_HARDWARE_SUPPORT_MUTE)) { return CONTROL_FALSE; } switch (cmd) { case AOCONTROL_GET_MUTE: IAudioEndpointVolume_GetMute(state->pEndpointVolumeProxy, &mute); *(bool *)arg = mute; return CONTROL_OK; case AOCONTROL_SET_MUTE: mute = *(bool *)arg; IAudioEndpointVolume_SetMute(state->pEndpointVolumeProxy, mute, NULL); return CONTROL_OK; } case AOCONTROL_HAS_PER_APP_VOLUME: return CONTROL_FALSE; } } else { // shared-specific switch (cmd) { case AOCONTROL_GET_VOLUME: case AOCONTROL_SET_VOLUME: case AOCONTROL_GET_MUTE: case AOCONTROL_SET_MUTE: case AOCONTROL_HAS_PER_APP_VOLUME: if (!state->pAudioVolumeProxy) return CONTROL_FALSE; switch(cmd) { case AOCONTROL_GET_VOLUME: ISimpleAudioVolume_GetMasterVolume(state->pAudioVolumeProxy, &volume); *(ao_control_vol_t *)arg = (ao_control_vol_t){ .left = 100.0f * volume, .right = 100.0f * volume, }; return CONTROL_OK; case AOCONTROL_SET_VOLUME: volume = ((ao_control_vol_t *)arg)->left / 100.f; ISimpleAudioVolume_SetMasterVolume(state->pAudioVolumeProxy, volume, NULL); return CONTROL_OK; case AOCONTROL_GET_MUTE: ISimpleAudioVolume_GetMute(state->pAudioVolumeProxy, &mute); *(bool *)arg = mute; return CONTROL_OK; case AOCONTROL_SET_MUTE: mute = *(bool *)arg; ISimpleAudioVolume_SetMute(state->pAudioVolumeProxy, mute, NULL); return CONTROL_OK; case AOCONTROL_HAS_PER_APP_VOLUME: return CONTROL_TRUE; } } } // common to exclusive and shared switch (cmd) { case AOCONTROL_UPDATE_STREAM_TITLE: if (!state->pSessionControlProxy) return CONTROL_FALSE; MP_VERBOSE(state, "Updating stream title to \"%s\"\n", (char*)arg); wchar_t *title = mp_from_utf8(NULL, (char*)arg); wchar_t *tmp = NULL; /* There is a weird race condition in the IAudioSessionControl itself -- it seems that *sometimes* the SetDisplayName does not take effect and it still shows the old title. Use this loop to insist until it works. */ do { IAudioSessionControl_SetDisplayName(state->pSessionControlProxy, title, NULL); SAFE_RELEASE(tmp, CoTaskMemFree(tmp)); IAudioSessionControl_GetDisplayName(state->pSessionControlProxy, &tmp); } while (lstrcmpW(title, tmp)); SAFE_RELEASE(tmp, CoTaskMemFree(tmp)); talloc_free(title); return CONTROL_OK; default: return CONTROL_UNKNOWN; } } 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"); struct wasapi_state *state = ao->priv; wasapi_change_uninit(ao); SAFE_RELEASE(state->pEnumerator, IMMDeviceEnumerator_Release(state->pEnumerator)); 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_APARTMENTTHREADED); HRESULT hr = CoCreateInstance(&CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, &IID_IMMDeviceEnumerator, (void **)&state->pEnumerator); EXIT_ON_ERROR(hr); hr = wasapi_change_init(ao, true); EXIT_ON_ERROR(hr); return 0; exit_label: MP_ERR(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, .resume = audio_resume, .list_devs = wasapi_list_devs, .hotplug_init = hotplug_init, .hotplug_uninit = hotplug_uninit, .priv_size = sizeof(wasapi_state), .options = (const struct m_option[]) { OPT_FLAG("exclusive", opt_exclusive, 0), OPT_STRING("device", opt_device, 0), {NULL}, }, };