diff --git a/client/audiodevs/WASAPI/CMakeLists.txt b/client/audiodevs/WASAPI/CMakeLists.txt new file mode 100644 index 00000000..e423eb8c --- /dev/null +++ b/client/audiodevs/WASAPI/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.10) +project(audiodev_WASAPI LANGUAGES C) + +add_library(audiodev_WASAPI STATIC + capture_queue.c + format.c + guids.c + wasapi.c +) + +target_link_libraries(audiodev_WASAPI + avrt + lg_common + m + ole32 + uuid +) + +target_include_directories(audiodev_WASAPI + PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}" +) diff --git a/client/audiodevs/WASAPI/capture_queue.c b/client/audiodevs/WASAPI/capture_queue.c new file mode 100644 index 00000000..67deaf78 --- /dev/null +++ b/client/audiodevs/WASAPI/capture_queue.c @@ -0,0 +1,145 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program 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. + * + * 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "capture_queue.h" + +#include +#include +#include + +bool wasapi_captureQueueInit(WasapiCaptureQueue * queue, size_t capacity, + size_t slotSize) +{ + if (!queue || !capacity || !slotSize || capacity == SIZE_MAX) + return false; + + const size_t slotCount = capacity + 1; + if (slotCount > SIZE_MAX / sizeof(*queue->packets) || + slotCount > SIZE_MAX / slotSize) + return false; + + WasapiCapturePacket * packets = calloc(slotCount, sizeof(*packets)); + uint8_t * data = malloc(slotCount * slotSize); + if (!packets || !data) + { + free(data); + free(packets); + return false; + } + + for (size_t i = 0; i < slotCount; ++i) + packets[i].data = data + i * slotSize; + + queue->packets = packets; + queue->data = data; + queue->slotSize = slotSize; + queue->slotCount = slotCount; + atomic_init(&queue->head, 0); + atomic_init(&queue->tail, 0); + return true; +} + +void wasapi_captureQueueFree(WasapiCaptureQueue * queue) +{ + if (!queue) + return; + + free(queue->data); + free(queue->packets); + *queue = (WasapiCaptureQueue) { 0 }; +} + +void wasapi_captureQueueReset(WasapiCaptureQueue * queue) +{ + if (!queue) + return; + + atomic_store_explicit(&queue->head, 0, memory_order_relaxed); + atomic_store_explicit(&queue->tail, 0, memory_order_relaxed); +} + +bool wasapi_captureQueuePush(WasapiCaptureQueue * queue, + const uint8_t * data, size_t bytes, int frames, + const LG_AudioClock * clock) +{ + if (!data || frames < 0) + return false; + + uint8_t * destination = wasapi_captureQueueBegin(queue, bytes); + if (!destination) + return false; + + memcpy(destination, data, bytes); + wasapi_captureQueueCommit(queue, bytes, frames, clock); + return true; +} + +uint8_t * wasapi_captureQueueBegin(WasapiCaptureQueue * queue, size_t bytes) +{ + if (!queue || !queue->packets || bytes > queue->slotSize) + return NULL; + + const size_t head = atomic_load_explicit( + &queue->head, memory_order_relaxed); + const size_t next = (head + 1) % queue->slotCount; + if (next == atomic_load_explicit(&queue->tail, memory_order_acquire)) + return NULL; + + return queue->packets[head].data; +} + +void wasapi_captureQueueCommit(WasapiCaptureQueue * queue, size_t bytes, + int frames, const LG_AudioClock * clock) +{ + const size_t head = atomic_load_explicit( + &queue->head, memory_order_relaxed); + WasapiCapturePacket * packet = &queue->packets[head]; + packet->bytes = bytes; + packet->frames = frames; + packet->clockValid = clock != NULL; + packet->clock = clock ? *clock : (LG_AudioClock) { 0 }; + atomic_store_explicit(&queue->head, + (head + 1) % queue->slotCount, memory_order_release); +} + +const WasapiCapturePacket * wasapi_captureQueueFront( + WasapiCaptureQueue * queue) +{ + if (!queue || !queue->packets) + return NULL; + + const size_t tail = atomic_load_explicit( + &queue->tail, memory_order_relaxed); + if (tail == atomic_load_explicit(&queue->head, memory_order_acquire)) + return NULL; + + return &queue->packets[tail]; +} + +void wasapi_captureQueuePop(WasapiCaptureQueue * queue) +{ + if (!queue || !queue->packets) + return; + + const size_t tail = atomic_load_explicit( + &queue->tail, memory_order_relaxed); + atomic_store_explicit( + &queue->tail, (tail + 1) % queue->slotCount, memory_order_release); +} diff --git a/client/audiodevs/WASAPI/capture_queue.h b/client/audiodevs/WASAPI/capture_queue.h new file mode 100644 index 00000000..3156b431 --- /dev/null +++ b/client/audiodevs/WASAPI/capture_queue.h @@ -0,0 +1,73 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program 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. + * + * 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef _H_LG_WASAPI_CAPTURE_QUEUE_ +#define _H_LG_WASAPI_CAPTURE_QUEUE_ + +#include "interface/audio.h" + +#include +#include +#include +#include + +typedef struct WasapiCapturePacket +{ + uint8_t * data; + size_t bytes; + int frames; + bool clockValid; + LG_AudioClock clock; +} +WasapiCapturePacket; + +typedef struct WasapiCaptureQueue +{ + WasapiCapturePacket * packets; + uint8_t * data; + size_t slotSize; + size_t slotCount; + _Atomic(size_t) head; + _Atomic(size_t) tail; +} +WasapiCaptureQueue; + +bool wasapi_captureQueueInit ( + WasapiCaptureQueue * queue, size_t capacity, + size_t slotSize); +void wasapi_captureQueueFree ( + WasapiCaptureQueue * queue); +void wasapi_captureQueueReset ( + WasapiCaptureQueue * queue); +uint8_t * wasapi_captureQueueBegin ( + WasapiCaptureQueue * queue, size_t bytes); +void wasapi_captureQueueCommit ( + WasapiCaptureQueue * queue, size_t bytes, int frames, + const LG_AudioClock * clock); +bool wasapi_captureQueuePush ( + WasapiCaptureQueue * queue, + const uint8_t * data, size_t bytes, int frames, + const LG_AudioClock * clock); +const WasapiCapturePacket * wasapi_captureQueueFront ( + WasapiCaptureQueue * queue); +void wasapi_captureQueuePop ( + WasapiCaptureQueue * queue); + +#endif diff --git a/client/audiodevs/WASAPI/format.c b/client/audiodevs/WASAPI/format.c new file mode 100644 index 00000000..1ea290ab --- /dev/null +++ b/client/audiodevs/WASAPI/format.c @@ -0,0 +1,210 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program 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. + * + * 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "format.h" + +#include + +#define WASAPI_TICKS_PER_SECOND UINT64_C(10000000) + +#define WASAPI_SPEAKER_FRONT_LEFT UINT32_C(0x00000001) +#define WASAPI_SPEAKER_FRONT_RIGHT UINT32_C(0x00000002) +#define WASAPI_SPEAKER_FRONT_CENTER UINT32_C(0x00000004) +#define WASAPI_SPEAKER_LOW_FREQUENCY UINT32_C(0x00000008) +#define WASAPI_SPEAKER_BACK_LEFT UINT32_C(0x00000010) +#define WASAPI_SPEAKER_BACK_RIGHT UINT32_C(0x00000020) +#define WASAPI_SPEAKER_FRONT_LEFT_OF_CENTER UINT32_C(0x00000040) +#define WASAPI_SPEAKER_FRONT_RIGHT_OF_CENTER UINT32_C(0x00000080) +#define WASAPI_SPEAKER_BACK_CENTER UINT32_C(0x00000100) +#define WASAPI_SPEAKER_SIDE_LEFT UINT32_C(0x00000200) +#define WASAPI_SPEAKER_SIDE_RIGHT UINT32_C(0x00000400) +#define WASAPI_SPEAKER_TOP_CENTER UINT32_C(0x00000800) +#define WASAPI_SPEAKER_TOP_FRONT_LEFT UINT32_C(0x00001000) +#define WASAPI_SPEAKER_TOP_FRONT_CENTER UINT32_C(0x00002000) +#define WASAPI_SPEAKER_TOP_FRONT_RIGHT UINT32_C(0x00004000) +#define WASAPI_SPEAKER_TOP_BACK_LEFT UINT32_C(0x00008000) +#define WASAPI_SPEAKER_TOP_BACK_CENTER UINT32_C(0x00010000) +#define WASAPI_SPEAKER_TOP_BACK_RIGHT UINT32_C(0x00020000) + +void wasapi_controlPendingReset(WasapiControlPending * state) +{ + *state = (WasapiControlPending) { 0 }; +} + +WasapiControlPending wasapi_controlPendingTake(WasapiControlPending * state) +{ + const WasapiControlPending pending = *state; + wasapi_controlPendingReset(state); + return pending; +} + +int wasapi_sampleSize(LG_AudioSampleFormat format) +{ + switch (format) + { + case LG_AUDIO_FMT_U8 : return 1; + case LG_AUDIO_FMT_S16_LE : return 2; + case LG_AUDIO_FMT_S24_LE : return 3; + case LG_AUDIO_FMT_S32_LE : + case LG_AUDIO_FMT_F32_LE : + case LG_AUDIO_FMT_F32_NE : return 4; + case LG_AUDIO_FMT_F64_LE : return 8; + } + + return 0; +} + +static uint32_t wasapi_channelBit(LG_AudioChannel channel) +{ + switch (channel) + { + case LG_AUDIO_CH_UNKNOWN : return 0; + case LG_AUDIO_CH_MONO : + case LG_AUDIO_CH_FRONT_CENTER : + return WASAPI_SPEAKER_FRONT_CENTER; + case LG_AUDIO_CH_FRONT_LEFT : + return WASAPI_SPEAKER_FRONT_LEFT; + case LG_AUDIO_CH_FRONT_RIGHT : + return WASAPI_SPEAKER_FRONT_RIGHT; + case LG_AUDIO_CH_LFE : + return WASAPI_SPEAKER_LOW_FREQUENCY; + case LG_AUDIO_CH_REAR_LEFT : + return WASAPI_SPEAKER_BACK_LEFT; + case LG_AUDIO_CH_REAR_RIGHT : + return WASAPI_SPEAKER_BACK_RIGHT; + case LG_AUDIO_CH_FRONT_LEFT_CENTER : + return WASAPI_SPEAKER_FRONT_LEFT_OF_CENTER; + case LG_AUDIO_CH_FRONT_RIGHT_CENTER : + return WASAPI_SPEAKER_FRONT_RIGHT_OF_CENTER; + case LG_AUDIO_CH_REAR_CENTER : + return WASAPI_SPEAKER_BACK_CENTER; + case LG_AUDIO_CH_SIDE_LEFT : + return WASAPI_SPEAKER_SIDE_LEFT; + case LG_AUDIO_CH_SIDE_RIGHT : + return WASAPI_SPEAKER_SIDE_RIGHT; + case LG_AUDIO_CH_TOP_CENTER : + return WASAPI_SPEAKER_TOP_CENTER; + case LG_AUDIO_CH_TOP_FRONT_LEFT : + return WASAPI_SPEAKER_TOP_FRONT_LEFT; + case LG_AUDIO_CH_TOP_FRONT_CENTER : + return WASAPI_SPEAKER_TOP_FRONT_CENTER; + case LG_AUDIO_CH_TOP_FRONT_RIGHT : + return WASAPI_SPEAKER_TOP_FRONT_RIGHT; + case LG_AUDIO_CH_TOP_REAR_LEFT : + return WASAPI_SPEAKER_TOP_BACK_LEFT; + case LG_AUDIO_CH_TOP_REAR_CENTER : + return WASAPI_SPEAKER_TOP_BACK_CENTER; + case LG_AUDIO_CH_TOP_REAR_RIGHT : + return WASAPI_SPEAKER_TOP_BACK_RIGHT; + } + + return 0; +} + +uint32_t wasapi_channelMask(const LG_AudioFormat * format) +{ + if (!format || !format->channelCount || + format->channelCount > LG_AUDIO_MAX_CHANNELS) + return 0; + + if (format->channelCount == 1 && + format->channels[0] == LG_AUDIO_CH_MONO) + return WASAPI_SPEAKER_FRONT_CENTER; + + uint32_t mask = 0; + uint32_t lastBit = 0; + for (uint8_t i = 0; i < format->channelCount; ++i) + { + if (format->channels[i] == LG_AUDIO_CH_MONO) + return 0; + + const uint32_t bit = wasapi_channelBit(format->channels[i]); + if (!bit || bit <= lastBit) + return 0; + + mask |= bit; + lastBit = bit; + } + + return mask; +} + +bool wasapi_formatInfo(const LG_AudioFormat * format, + WasapiFormatInfo * info) +{ + if (!format || !info || !format->sampleRate || !format->channelCount || + format->channelCount > LG_AUDIO_MAX_CHANNELS) + return false; + + const int sampleSize = wasapi_sampleSize(format->sampleFormat); + const uint32_t channelMask = wasapi_channelMask(format); + const uint32_t blockAlign = (uint32_t)sampleSize * format->channelCount; + const uint64_t bytesPerSecond = (uint64_t)blockAlign * format->sampleRate; + if (!sampleSize || !channelMask || blockAlign > UINT16_MAX || + bytesPerSecond > UINT32_MAX) + return false; + + WasapiSampleType sampleType; + switch (format->sampleFormat) + { + case LG_AUDIO_FMT_U8: + case LG_AUDIO_FMT_S16_LE: + case LG_AUDIO_FMT_S24_LE: + case LG_AUDIO_FMT_S32_LE: + sampleType = WASAPI_SAMPLE_TYPE_PCM; + break; + + case LG_AUDIO_FMT_F32_LE: + case LG_AUDIO_FMT_F64_LE: + case LG_AUDIO_FMT_F32_NE: + sampleType = WASAPI_SAMPLE_TYPE_FLOAT; + break; + + default: + return false; + } + + info->blockAlign = (uint16_t)blockAlign; + info->bytesPerSecond = (uint32_t)bytesPerSecond; + info->bitsPerSample = (uint16_t)(sampleSize * CHAR_BIT); + info->channelMask = channelMask; + info->sampleType = sampleType; + return true; +} + +uint64_t wasapi_framesToDuration(uint32_t frames, uint32_t sampleRate) +{ + if (!sampleRate) + return 0; + + return ((uint64_t)frames * WASAPI_TICKS_PER_SECOND + sampleRate - 1) / + sampleRate; +} + +uint32_t wasapi_availableFrames(uint32_t bufferFrames, + uint32_t paddingFrames) +{ + return paddingFrames < bufferFrames ? bufferFrames - paddingFrames : 0; +} + +uint8_t wasapi_silenceByte(LG_AudioSampleFormat format) +{ + return format == LG_AUDIO_FMT_U8 ? UINT8_C(0x80) : UINT8_C(0); +} diff --git a/client/audiodevs/WASAPI/format.h b/client/audiodevs/WASAPI/format.h new file mode 100644 index 00000000..48f02f5b --- /dev/null +++ b/client/audiodevs/WASAPI/format.h @@ -0,0 +1,65 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program 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. + * + * 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef _H_LG_WASAPI_FORMAT_ +#define _H_LG_WASAPI_FORMAT_ + +#include "interface/audio.h" + +#include + +typedef enum WasapiSampleType +{ + WASAPI_SAMPLE_TYPE_PCM, + WASAPI_SAMPLE_TYPE_FLOAT +} +WasapiSampleType; + +typedef struct WasapiFormatInfo +{ + uint16_t blockAlign; + uint32_t bytesPerSecond; + uint16_t bitsPerSample; + uint32_t channelMask; + WasapiSampleType sampleType; +} +WasapiFormatInfo; + +typedef struct WasapiControlPending +{ + bool volume; + bool mute; +} +WasapiControlPending; + +void wasapi_controlPendingReset (WasapiControlPending * state); +WasapiControlPending wasapi_controlPendingTake (WasapiControlPending * state); + +int wasapi_sampleSize (LG_AudioSampleFormat format); +uint32_t wasapi_channelMask (const LG_AudioFormat * format); +bool wasapi_formatInfo (const LG_AudioFormat * format, + WasapiFormatInfo * info); +uint64_t wasapi_framesToDuration (uint32_t frames, + uint32_t sampleRate); +uint32_t wasapi_availableFrames (uint32_t bufferFrames, + uint32_t paddingFrames); +uint8_t wasapi_silenceByte (LG_AudioSampleFormat format); + +#endif diff --git a/client/audiodevs/WASAPI/guids.c b/client/audiodevs/WASAPI/guids.c new file mode 100644 index 00000000..db182ddd --- /dev/null +++ b/client/audiodevs/WASAPI/guids.c @@ -0,0 +1,24 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program 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. + * + * 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include + +#include +#include diff --git a/client/audiodevs/WASAPI/wasapi.c b/client/audiodevs/WASAPI/wasapi.c new file mode 100644 index 00000000..fa5c750b --- /dev/null +++ b/client/audiodevs/WASAPI/wasapi.c @@ -0,0 +1,1398 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program 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. + * + * 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#define COBJMACROS + +#include "interface/audiodev.h" + +#include "capture_queue.h" +#include "format.h" + +#include "common/debug.h" +#include "common/thread.h" +#include "common/windebug.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#define WASAPI_CAPTURE_QUEUE_PACKETS 8 +#define WASAPI_QPC_TICK_NS INT64_C(100) +#define WASAPI_SENDER_WAIT_MS 100 + +static const GUID wasapi_pcmSubFormat = +{ + UINT32_C(0x00000001), UINT16_C(0x0000), UINT16_C(0x0010), + { 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 } +}; + +static const GUID wasapi_floatSubFormat = +{ + UINT32_C(0x00000003), UINT16_C(0x0000), UINT16_C(0x0010), + { 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 } +}; + +typedef enum +{ + WASAPI_PLAYBACK_COMMAND_NONE, + WASAPI_PLAYBACK_COMMAND_START, + WASAPI_PLAYBACK_COMMAND_STOP, + WASAPI_PLAYBACK_COMMAND_CONTROL, + WASAPI_PLAYBACK_COMMAND_EXIT +} +WasapiPlaybackCommand; + +typedef enum +{ + WASAPI_RECORD_COMMAND_NONE, + WASAPI_RECORD_COMMAND_CONTROL, + WASAPI_RECORD_COMMAND_EXIT +} +WasapiRecordCommand; + +typedef struct +{ + CRITICAL_SECTION commandLock; + bool commandLockInitialized; + LGThread * thread; + HANDLE commandEvent; + HANDLE audioEvent; + HANDLE commandDone; + volatile LONG command; + bool commandResult; + _Atomic(bool) terminated; + + LG_AudioFormat format; + int stride; + int maxPeriodFrames; + int startFrames; + LG_AudioPullFn pullFn; + + LG_AudioFailureFn failureFn; + uint32_t failureCookie; + bool active; + + IAudioClient * client; + IAudioRenderClient * render; + IChannelAudioVolume * channelVolume; + ISimpleAudioVolume * simpleVolume; + UINT32 bufferFrames; + REFERENCE_TIME streamLatency; + _Atomic(uint64_t) latencyUs; + + UINT32 controlChannels; + float controlVolume[LG_AUDIO_MAX_CHANNELS]; + bool controlMute; + WasapiControlPending controlPending; +} +WasapiPlayback; + +typedef struct +{ + CRITICAL_SECTION commandLock; + bool commandLockInitialized; + LGThread * thread; + HANDLE commandEvent; + HANDLE audioEvent; + HANDLE commandDone; + volatile LONG command; + bool commandResult; + _Atomic(bool) terminated; + + LG_AudioFormat format; + int stride; + LG_AudioPushFn pushFn; + LG_AudioFailureFn failureFn; + uint32_t failureCookie; + bool active; + + IAudioClient * client; + IAudioCaptureClient * capture; + IChannelAudioVolume * channelVolume; + ISimpleAudioVolume * simpleVolume; + UINT32 bufferFrames; + uint8_t * silence; + bool discontinuityPending; + WasapiCaptureQueue queue; + LGThread * senderThread; + HANDLE senderEvent; + _Atomic(bool) senderStop; + _Atomic(bool) senderFailed; + + UINT32 controlChannels; + float controlVolume[LG_AUDIO_MAX_CHANNELS]; + bool controlMute; + WasapiControlPending controlPending; +} +WasapiRecord; + +struct Wasapi +{ + WasapiPlayback playback; + WasapiRecord record; +}; + +static struct Wasapi wasapi = { 0 }; + +static bool wasapi_wait(HANDLE handle, const char * operation) +{ + const DWORD result = WaitForSingleObject(handle, INFINITE); + if (result == WAIT_OBJECT_0) + return true; + + const DWORD error = result == WAIT_FAILED ? GetLastError() : ERROR_GEN_FAILURE; + DEBUG_WINERROR(operation, HRESULT_FROM_WIN32(error)); + return false; +} + +static float wasapi_volumeLevel(uint16_t volume) +{ + double level = 9.3234e-7 * pow(1.000211902, volume) - 0.000172787; + if (level < 0.0) + level = 0.0; + if (level > 1.0) + level = 1.0; + return (float)level; +} + +static bool wasapi_waveFormat(const LG_AudioFormat * format, + WAVEFORMATEXTENSIBLE * wave) +{ + WasapiFormatInfo info; + if (!wasapi_formatInfo(format, &info)) + return false; + + memset(wave, 0, sizeof(*wave)); + wave->Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; + wave->Format.nChannels = format->channelCount; + wave->Format.nSamplesPerSec = format->sampleRate; + wave->Format.nAvgBytesPerSec = info.bytesPerSecond; + wave->Format.nBlockAlign = info.blockAlign; + wave->Format.wBitsPerSample = info.bitsPerSample; + wave->Format.cbSize = sizeof(*wave) - sizeof(wave->Format); + wave->Samples.wValidBitsPerSample = wave->Format.wBitsPerSample; + wave->dwChannelMask = info.channelMask; + wave->SubFormat = info.sampleType == WASAPI_SAMPLE_TYPE_PCM ? + wasapi_pcmSubFormat : wasapi_floatSubFormat; + + return true; +} + +static HRESULT wasapi_activate(EDataFlow flow, IAudioClient ** client) +{ + IMMDeviceEnumerator * enumerator = NULL; + IMMDevice * device = NULL; + HRESULT status = CoCreateInstance( + &CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, + &IID_IMMDeviceEnumerator, (void **)&enumerator); + if (FAILED(status)) + { + DEBUG_WINERROR("Failed to create the WASAPI device enumerator", status); + return status; + } + + status = IMMDeviceEnumerator_GetDefaultAudioEndpoint( + enumerator, flow, eConsole, &device); + if (FAILED(status)) + { + DEBUG_WINERROR("Failed to get the default WASAPI endpoint", status); + IMMDeviceEnumerator_Release(enumerator); + return status; + } + + status = IMMDevice_Activate(device, &IID_IAudioClient, CLSCTX_ALL, + NULL, (void **)client); + if (FAILED(status)) + DEBUG_WINERROR("Failed to activate the WASAPI endpoint", status); + + IMMDevice_Release(device); + IMMDeviceEnumerator_Release(enumerator); + return status; +} + +static HANDLE wasapi_enableRealtime(void) +{ + DWORD taskIndex = 0; + return AvSetMmThreadCharacteristicsW(L"Pro Audio", &taskIndex); +} + +static void wasapi_disableRealtime(HANDLE task) +{ + if (task && !AvRevertMmThreadCharacteristics(task)) + DEBUG_WINERROR("Failed to leave the WASAPI MMCSS task", + HRESULT_FROM_WIN32(GetLastError())); +} + +static void wasapi_playbackRelease(void) +{ + if (wasapi.playback.simpleVolume) + { + ISimpleAudioVolume_Release(wasapi.playback.simpleVolume); + wasapi.playback.simpleVolume = NULL; + } + if (wasapi.playback.channelVolume) + { + IChannelAudioVolume_Release(wasapi.playback.channelVolume); + wasapi.playback.channelVolume = NULL; + } + if (wasapi.playback.render) + { + IAudioRenderClient_Release(wasapi.playback.render); + wasapi.playback.render = NULL; + } + if (wasapi.playback.client) + { + IAudioClient_Release(wasapi.playback.client); + wasapi.playback.client = NULL; + } +} + +static bool wasapi_playbackInitialize(void) +{ + WAVEFORMATEXTENSIBLE wave; + if (!wasapi_waveFormat(&wasapi.playback.format, &wave)) + return false; + + HRESULT status = wasapi_activate(eRender, &wasapi.playback.client); + if (FAILED(status)) + return false; + + const DWORD flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK | + AUDCLNT_STREAMFLAGS_NOPERSIST | + AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | + AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY; + + status = IAudioClient_Initialize(wasapi.playback.client, + AUDCLNT_SHAREMODE_SHARED, flags, 0, 0, &wave.Format, NULL); + if (FAILED(status)) + { + DEBUG_WINERROR("Failed to initialize WASAPI playback", status); + return false; + } + + status = IAudioClient_SetEventHandle( + wasapi.playback.client, wasapi.playback.audioEvent); + if (FAILED(status)) + { + DEBUG_WINERROR("Failed to set the WASAPI playback event", status); + return false; + } + + status = IAudioClient_GetBufferSize( + wasapi.playback.client, &wasapi.playback.bufferFrames); + if (FAILED(status) || !wasapi.playback.bufferFrames || + wasapi.playback.bufferFrames > INT_MAX) + { + if (FAILED(status)) + DEBUG_WINERROR("Failed to get the WASAPI playback buffer size", status); + return false; + } + + status = IAudioClient_GetService(wasapi.playback.client, + &IID_IAudioRenderClient, (void **)&wasapi.playback.render); + if (FAILED(status)) + { + DEBUG_WINERROR("Failed to get the WASAPI render service", status); + return false; + } + + status = IAudioClient_GetStreamLatency( + wasapi.playback.client, &wasapi.playback.streamLatency); + if (FAILED(status)) + wasapi.playback.streamLatency = 0; + + IAudioClient_GetService(wasapi.playback.client, + &IID_IChannelAudioVolume, (void **)&wasapi.playback.channelVolume); + IAudioClient_GetService(wasapi.playback.client, + &IID_ISimpleAudioVolume, (void **)&wasapi.playback.simpleVolume); + + wasapi.playback.maxPeriodFrames = wasapi.playback.bufferFrames; + wasapi.playback.startFrames = wasapi.playback.bufferFrames; + return true; +} + +static void wasapi_playbackUpdateLatency(UINT32 queuedFrames) +{ + const uint64_t queuedDuration = wasapi_framesToDuration( + queuedFrames, wasapi.playback.format.sampleRate); + const uint64_t streamLatency = wasapi.playback.streamLatency > 0 ? + (uint64_t)wasapi.playback.streamLatency : 0; + atomic_store_explicit(&wasapi.playback.latencyUs, + (queuedDuration + streamLatency) / 10, memory_order_release); +} + +static bool wasapi_playbackWrite(void) +{ + UINT32 padding = 0; + HRESULT status = IAudioClient_GetCurrentPadding( + wasapi.playback.client, &padding); + if (FAILED(status)) + { + DEBUG_WINERROR("Failed to get WASAPI playback padding", status); + return false; + } + + const UINT32 available = wasapi_availableFrames( + wasapi.playback.bufferFrames, padding); + if (!available) + { + wasapi_playbackUpdateLatency(padding); + return true; + } + + BYTE * data = NULL; + status = IAudioRenderClient_GetBuffer( + wasapi.playback.render, available, &data); + if (FAILED(status)) + { + DEBUG_WINERROR("Failed to acquire the WASAPI playback buffer", status); + return false; + } + + int frames = wasapi.playback.pullFn(data, (int)available); + if (frames < 0 || frames > (int)available) + { + DEBUG_WARN("WASAPI playback returned an invalid frame count: %d", frames); + frames = frames < 0 ? 0 : (int)available; + } + + if (frames < (int)available) + { + memset(data + (size_t)frames * wasapi.playback.stride, + wasapi_silenceByte(wasapi.playback.format.sampleFormat), + (size_t)(available - frames) * wasapi.playback.stride); + } + + status = IAudioRenderClient_ReleaseBuffer( + wasapi.playback.render, available, 0); + if (FAILED(status)) + { + DEBUG_WINERROR("Failed to submit the WASAPI playback buffer", status); + return false; + } + + wasapi_playbackUpdateLatency(padding + available); + return true; +} + +static void wasapi_playbackStopWorker(void) +{ + if (wasapi.playback.active) + { + const HRESULT status = IAudioClient_Stop(wasapi.playback.client); + if (FAILED(status)) + DEBUG_WINERROR("Failed to stop WASAPI playback", status); + wasapi.playback.active = false; + } + + if (wasapi.playback.client) + { + const HRESULT status = IAudioClient_Reset(wasapi.playback.client); + if (FAILED(status) && status != AUDCLNT_E_NOT_STOPPED) + DEBUG_WINERROR("Failed to reset WASAPI playback", status); + } + + wasapi.playback.failureFn = NULL; + wasapi.playback.failureCookie = 0; + atomic_store_explicit( + &wasapi.playback.latencyUs, 0, memory_order_release); +} + +static bool wasapi_playbackStartWorker(void) +{ + if (wasapi.playback.active) + return true; + + HRESULT status = IAudioClient_Reset(wasapi.playback.client); + if (FAILED(status)) + { + DEBUG_WINERROR("Failed to reset WASAPI playback", status); + return false; + } + + if (!wasapi_playbackWrite()) + return false; + + status = IAudioClient_Start(wasapi.playback.client); + if (FAILED(status)) + { + DEBUG_WINERROR("Failed to start WASAPI playback", status); + return false; + } + + wasapi.playback.active = true; + return true; +} + +static void wasapi_playbackApplyControl(void) +{ + const WasapiControlPending pending = + wasapi_controlPendingTake(&wasapi.playback.controlPending); + if (pending.volume && + wasapi.playback.channelVolume) + { + const HRESULT status = IChannelAudioVolume_SetAllVolumes( + wasapi.playback.channelVolume, + wasapi.playback.controlChannels, + wasapi.playback.controlVolume, NULL); + if (FAILED(status)) + DEBUG_WINERROR("Failed to set WASAPI playback volume", status); + } + + if (pending.mute && + wasapi.playback.simpleVolume) + { + const HRESULT status = ISimpleAudioVolume_SetMute( + wasapi.playback.simpleVolume, + wasapi.playback.controlMute, NULL); + if (FAILED(status)) + DEBUG_WINERROR("Failed to set WASAPI playback mute", status); + } +} + +static void wasapi_playbackFail(void) +{ + if (!wasapi.playback.active) + return; + + IAudioClient_Stop(wasapi.playback.client); + wasapi.playback.active = false; + atomic_store_explicit( + &wasapi.playback.latencyUs, 0, memory_order_release); + + LG_AudioFailureFn failureFn = wasapi.playback.failureFn; + const uint32_t failureCookie = wasapi.playback.failureCookie; + wasapi.playback.failureFn = NULL; + wasapi.playback.failureCookie = 0; + if (failureFn) + failureFn(failureCookie); +} + +static int wasapi_playbackThread(void * opaque) +{ + (void)opaque; + + const HRESULT comStatus = CoInitializeEx(NULL, COINIT_MULTITHREADED); + const bool comInitialized = SUCCEEDED(comStatus); + HANDLE realtime = NULL; + if (!comInitialized) + { + DEBUG_WINERROR("Failed to initialize COM for WASAPI playback", comStatus); + wasapi.playback.commandResult = false; + atomic_store_explicit( + &wasapi.playback.terminated, true, memory_order_release); + SetEvent(wasapi.playback.commandDone); + return 0; + } + + realtime = wasapi_enableRealtime(); + wasapi.playback.commandResult = wasapi_playbackInitialize(); + if (!wasapi.playback.commandResult) + atomic_store_explicit( + &wasapi.playback.terminated, true, memory_order_release); + SetEvent(wasapi.playback.commandDone); + if (!wasapi.playback.commandResult) + goto exit; + + const HANDLE events[] = + { + wasapi.playback.commandEvent, + wasapi.playback.audioEvent + }; + + for (;;) + { + const DWORD result = WaitForMultipleObjects( + sizeof(events) / sizeof(*events), events, false, INFINITE); + if (result == WAIT_OBJECT_0) + { + const WasapiPlaybackCommand command = + (WasapiPlaybackCommand)InterlockedExchange( + &wasapi.playback.command, WASAPI_PLAYBACK_COMMAND_NONE); + switch (command) + { + case WASAPI_PLAYBACK_COMMAND_START: + wasapi.playback.commandResult = wasapi_playbackStartWorker(); + if (!wasapi.playback.commandResult) + { + wasapi.playback.failureFn = NULL; + wasapi.playback.failureCookie = 0; + } + SetEvent(wasapi.playback.commandDone); + break; + + case WASAPI_PLAYBACK_COMMAND_STOP: + wasapi_playbackStopWorker(); + wasapi.playback.commandResult = true; + SetEvent(wasapi.playback.commandDone); + break; + + case WASAPI_PLAYBACK_COMMAND_CONTROL: + wasapi_playbackApplyControl(); + wasapi.playback.commandResult = true; + SetEvent(wasapi.playback.commandDone); + break; + + case WASAPI_PLAYBACK_COMMAND_EXIT: + wasapi_playbackStopWorker(); + wasapi.playback.commandResult = true; + goto exitCommand; + + case WASAPI_PLAYBACK_COMMAND_NONE: + wasapi.playback.commandResult = false; + SetEvent(wasapi.playback.commandDone); + break; + } + } + else if (result == WAIT_OBJECT_0 + 1) + { + if (wasapi.playback.active && !wasapi_playbackWrite()) + wasapi_playbackFail(); + } + else + { + const DWORD error = result == WAIT_FAILED ? + GetLastError() : ERROR_GEN_FAILURE; + DEBUG_WINERROR("Failed to wait for WASAPI playback", + HRESULT_FROM_WIN32(error)); + wasapi_playbackFail(); + wasapi.playback.commandResult = false; + goto exitCommand; + } + } + +exitCommand: + wasapi_playbackRelease(); + wasapi_disableRealtime(realtime); + CoUninitialize(); + atomic_store_explicit( + &wasapi.playback.terminated, true, memory_order_release); + SetEvent(wasapi.playback.commandDone); + return 0; + +exit: + wasapi_playbackRelease(); + wasapi_disableRealtime(realtime); + CoUninitialize(); + return 0; +} + +static bool wasapi_playbackSendLocked(WasapiPlaybackCommand command) +{ + if (atomic_load_explicit( + &wasapi.playback.terminated, memory_order_acquire)) + return false; + + InterlockedExchange(&wasapi.playback.command, command); + wasapi.playback.commandResult = false; + const bool signaled = SetEvent(wasapi.playback.commandEvent) != 0; + const bool waited = signaled && wasapi_wait( + wasapi.playback.commandDone, "Failed to wait for WASAPI playback"); + return waited && wasapi.playback.commandResult; +} + +static bool wasapi_playbackSend(WasapiPlaybackCommand command) +{ + EnterCriticalSection(&wasapi.playback.commandLock); + const bool result = wasapi_playbackSendLocked(command); + LeaveCriticalSection(&wasapi.playback.commandLock); + return result; +} + +static void wasapi_playbackClose(void) +{ + EnterCriticalSection(&wasapi.playback.commandLock); + if (wasapi.playback.thread) + { + wasapi_playbackSendLocked(WASAPI_PLAYBACK_COMMAND_EXIT); + lgJoinThread(wasapi.playback.thread, NULL); + wasapi.playback.thread = NULL; + } + + if (wasapi.playback.commandDone) + CloseHandle(wasapi.playback.commandDone); + if (wasapi.playback.audioEvent) + CloseHandle(wasapi.playback.audioEvent); + if (wasapi.playback.commandEvent) + CloseHandle(wasapi.playback.commandEvent); + wasapi.playback.commandDone = NULL; + wasapi.playback.audioEvent = NULL; + wasapi.playback.commandEvent = NULL; + wasapi.playback.pullFn = NULL; + LeaveCriticalSection(&wasapi.playback.commandLock); +} + +static bool wasapi_playbackSetup(const LG_AudioFormat * format, + int requestedPeriodFrames, bool requestResampler, + bool * resamplerEnabled, int * maxPeriodFrames, int * startFrames, + LG_AudioPullFn pullFn) +{ + wasapi_playbackClose(); + *resamplerEnabled = false; + (void)requestResampler; + + WAVEFORMATEXTENSIBLE wave; + if (!format || requestedPeriodFrames <= 0 || !pullFn || + !wasapi_waveFormat(format, &wave)) + return false; + + wasapi.playback.format = *format; + wasapi.playback.stride = wave.Format.nBlockAlign; + wasapi.playback.maxPeriodFrames = 0; + wasapi.playback.startFrames = 0; + wasapi.playback.pullFn = pullFn; + wasapi.playback.command = WASAPI_PLAYBACK_COMMAND_NONE; + atomic_store_explicit( + &wasapi.playback.terminated, false, memory_order_relaxed); + wasapi_controlPendingReset(&wasapi.playback.controlPending); + atomic_store_explicit( + &wasapi.playback.latencyUs, 0, memory_order_relaxed); + + wasapi.playback.commandEvent = CreateEventW(NULL, false, false, NULL); + wasapi.playback.audioEvent = CreateEventW(NULL, false, false, NULL); + wasapi.playback.commandDone = CreateEventW(NULL, false, false, NULL); + if (!wasapi.playback.commandEvent || !wasapi.playback.audioEvent || + !wasapi.playback.commandDone) + { + DEBUG_WINERROR("Failed to create WASAPI playback events", + HRESULT_FROM_WIN32(GetLastError())); + wasapi_playbackClose(); + return false; + } + + if (!lgCreateThread("WASAPI Playback", wasapi_playbackThread, NULL, + &wasapi.playback.thread)) + { + wasapi_playbackClose(); + return false; + } + + const bool initialized = wasapi_wait(wasapi.playback.commandDone, + "Failed to initialize WASAPI playback"); + if (!initialized || !wasapi.playback.commandResult) + { + if (!initialized && !atomic_load_explicit( + &wasapi.playback.terminated, memory_order_acquire)) + { + InterlockedExchange(&wasapi.playback.command, + WASAPI_PLAYBACK_COMMAND_EXIT); + if (!SetEvent(wasapi.playback.commandEvent)) + DEBUG_WINERROR("Failed to stop WASAPI playback initialization", + HRESULT_FROM_WIN32(GetLastError())); + } + lgJoinThread(wasapi.playback.thread, NULL); + wasapi.playback.thread = NULL; + wasapi_playbackClose(); + return false; + } + + *maxPeriodFrames = wasapi.playback.maxPeriodFrames; + *startFrames = wasapi.playback.startFrames; + return true; +} + +static bool wasapi_playbackStart( + LG_AudioFailureFn failureFn, uint32_t failureCookie) +{ + EnterCriticalSection(&wasapi.playback.commandLock); + if (!wasapi.playback.thread) + { + LeaveCriticalSection(&wasapi.playback.commandLock); + return false; + } + wasapi.playback.failureFn = failureFn; + wasapi.playback.failureCookie = failureCookie; + const bool result = + wasapi_playbackSendLocked(WASAPI_PLAYBACK_COMMAND_START); + if (!result) + { + wasapi.playback.failureFn = NULL; + wasapi.playback.failureCookie = 0; + } + LeaveCriticalSection(&wasapi.playback.commandLock); + return result; +} + +static void wasapi_playbackStop(void) +{ + if (wasapi.playback.thread) + wasapi_playbackSend(WASAPI_PLAYBACK_COMMAND_STOP); +} + +static void wasapi_playbackVolume(int channels, const uint16_t volume[]) +{ + if (!volume || channels <= 0) + return; + + EnterCriticalSection(&wasapi.playback.commandLock); + if (!wasapi.playback.thread || + channels != wasapi.playback.format.channelCount) + { + LeaveCriticalSection(&wasapi.playback.commandLock); + return; + } + wasapi.playback.controlChannels = channels; + for (int i = 0; i < channels; ++i) + wasapi.playback.controlVolume[i] = wasapi_volumeLevel(volume[i]); + wasapi.playback.controlPending.volume = true; + wasapi_playbackSendLocked(WASAPI_PLAYBACK_COMMAND_CONTROL); + LeaveCriticalSection(&wasapi.playback.commandLock); +} + +static void wasapi_playbackMute(bool mute) +{ + EnterCriticalSection(&wasapi.playback.commandLock); + if (!wasapi.playback.thread) + { + LeaveCriticalSection(&wasapi.playback.commandLock); + return; + } + + wasapi.playback.controlMute = mute; + wasapi.playback.controlPending.mute = true; + wasapi_playbackSendLocked(WASAPI_PLAYBACK_COMMAND_CONTROL); + LeaveCriticalSection(&wasapi.playback.commandLock); +} + +static uint64_t wasapi_playbackLatency(void) +{ + return atomic_load_explicit( + &wasapi.playback.latencyUs, memory_order_acquire); +} + +static int wasapi_recordSenderThread(void * opaque) +{ + (void)opaque; + bool discontinuityPending = false; + + for (;;) + { + const DWORD result = WaitForSingleObject( + wasapi.record.senderEvent, WASAPI_SENDER_WAIT_MS); + if (atomic_load_explicit( + &wasapi.record.senderStop, memory_order_acquire)) + return 0; + + if (result != WAIT_OBJECT_0 && result != WAIT_TIMEOUT) + { + const DWORD error = result == WAIT_FAILED ? + GetLastError() : ERROR_GEN_FAILURE; + DEBUG_WINERROR("Failed to wait for the WASAPI recording sender", + HRESULT_FROM_WIN32(error)); + atomic_store_explicit( + &wasapi.record.senderFailed, true, memory_order_release); + return 0; + } + + const WasapiCapturePacket * packet; + while ((packet = wasapi_captureQueueFront(&wasapi.record.queue))) + { + if (atomic_load_explicit( + &wasapi.record.senderStop, memory_order_acquire)) + return 0; + + LG_AudioClock clock = packet->clock; + if (packet->clockValid && discontinuityPending) + { + clock.stable = false; + clock.discontinuity = true; + discontinuityPending = false; + } + + const bool accepted = wasapi.record.pushFn(packet->data, + packet->frames, packet->clockValid ? &clock : NULL); + wasapi_captureQueuePop(&wasapi.record.queue); + if (!accepted) + discontinuityPending = true; + } + } +} + +static void wasapi_recordStopSender(void) +{ + atomic_store_explicit( + &wasapi.record.senderStop, true, memory_order_release); + if (wasapi.record.senderEvent) + SetEvent(wasapi.record.senderEvent); + if (wasapi.record.senderThread) + { + lgJoinThread(wasapi.record.senderThread, NULL); + wasapi.record.senderThread = NULL; + } + wasapi_captureQueueFree(&wasapi.record.queue); +} + +static void wasapi_recordRelease(void) +{ + wasapi_recordStopSender(); + + if (wasapi.record.simpleVolume) + { + ISimpleAudioVolume_Release(wasapi.record.simpleVolume); + wasapi.record.simpleVolume = NULL; + } + if (wasapi.record.channelVolume) + { + IChannelAudioVolume_Release(wasapi.record.channelVolume); + wasapi.record.channelVolume = NULL; + } + if (wasapi.record.capture) + { + IAudioCaptureClient_Release(wasapi.record.capture); + wasapi.record.capture = NULL; + } + if (wasapi.record.client) + { + IAudioClient_Release(wasapi.record.client); + wasapi.record.client = NULL; + } + free(wasapi.record.silence); + wasapi.record.silence = NULL; +} + +static bool wasapi_recordInitialize(void) +{ + WAVEFORMATEXTENSIBLE wave; + if (!wasapi_waveFormat(&wasapi.record.format, &wave)) + return false; + + HRESULT status = wasapi_activate(eCapture, &wasapi.record.client); + if (FAILED(status)) + return false; + + const DWORD flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK | + AUDCLNT_STREAMFLAGS_NOPERSIST | + AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | + AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY; + status = IAudioClient_Initialize(wasapi.record.client, + AUDCLNT_SHAREMODE_SHARED, flags, 0, 0, &wave.Format, NULL); + if (FAILED(status)) + { + DEBUG_WINERROR("Failed to initialize WASAPI recording", status); + return false; + } + + status = IAudioClient_SetEventHandle( + wasapi.record.client, wasapi.record.audioEvent); + if (FAILED(status)) + { + DEBUG_WINERROR("Failed to set the WASAPI recording event", status); + return false; + } + + status = IAudioClient_GetBufferSize( + wasapi.record.client, &wasapi.record.bufferFrames); + if (FAILED(status) || !wasapi.record.bufferFrames || + wasapi.record.bufferFrames > INT_MAX) + { + if (FAILED(status)) + DEBUG_WINERROR("Failed to get the WASAPI recording buffer size", status); + return false; + } + + if ((size_t)wasapi.record.bufferFrames > + SIZE_MAX / (size_t)wasapi.record.stride) + return false; + + const size_t silenceSize = (size_t)wasapi.record.bufferFrames * + (size_t)wasapi.record.stride; + wasapi.record.silence = malloc(silenceSize); + if (!wasapi.record.silence) + return false; + memset(wasapi.record.silence, + wasapi_silenceByte(wasapi.record.format.sampleFormat), silenceSize); + + if (!wasapi_captureQueueInit(&wasapi.record.queue, + WASAPI_CAPTURE_QUEUE_PACKETS, silenceSize)) + return false; + + atomic_store_explicit( + &wasapi.record.senderStop, false, memory_order_relaxed); + atomic_store_explicit( + &wasapi.record.senderFailed, false, memory_order_relaxed); + if (!lgCreateThread("WASAPI Recording Sender", wasapi_recordSenderThread, + NULL, &wasapi.record.senderThread)) + return false; + + status = IAudioClient_GetService(wasapi.record.client, + &IID_IAudioCaptureClient, (void **)&wasapi.record.capture); + if (FAILED(status)) + { + DEBUG_WINERROR("Failed to get the WASAPI capture service", status); + return false; + } + + IAudioClient_GetService(wasapi.record.client, + &IID_IChannelAudioVolume, (void **)&wasapi.record.channelVolume); + IAudioClient_GetService(wasapi.record.client, + &IID_ISimpleAudioVolume, (void **)&wasapi.record.simpleVolume); + + status = IAudioClient_Start(wasapi.record.client); + if (FAILED(status)) + { + DEBUG_WINERROR("Failed to start WASAPI recording", status); + return false; + } + + wasapi.record.active = true; + wasapi.record.discontinuityPending = true; + return true; +} + +static void wasapi_recordApplyControl(void) +{ + const WasapiControlPending pending = + wasapi_controlPendingTake(&wasapi.record.controlPending); + if (pending.volume && wasapi.record.channelVolume) + { + const HRESULT status = IChannelAudioVolume_SetAllVolumes( + wasapi.record.channelVolume, wasapi.record.controlChannels, + wasapi.record.controlVolume, NULL); + if (FAILED(status)) + DEBUG_WINERROR("Failed to set WASAPI recording volume", status); + } + + if (pending.mute && wasapi.record.simpleVolume) + { + const HRESULT status = ISimpleAudioVolume_SetMute( + wasapi.record.simpleVolume, wasapi.record.controlMute, NULL); + if (FAILED(status)) + DEBUG_WINERROR("Failed to set WASAPI recording mute", status); + } +} + +static void wasapi_recordFail(void) +{ + if (!wasapi.record.active) + return; + + IAudioClient_Stop(wasapi.record.client); + wasapi.record.active = false; + + LG_AudioFailureFn failureFn = wasapi.record.failureFn; + const uint32_t failureCookie = wasapi.record.failureCookie; + wasapi.record.failureFn = NULL; + wasapi.record.failureCookie = 0; + if (failureFn) + failureFn(failureCookie); +} + +static bool wasapi_recordProcess(void) +{ + for (;;) + { + if (atomic_load_explicit( + &wasapi.record.senderFailed, memory_order_acquire)) + return false; + + UINT32 packetFrames = 0; + HRESULT status = IAudioCaptureClient_GetNextPacketSize( + wasapi.record.capture, &packetFrames); + if (FAILED(status)) + { + DEBUG_WINERROR("Failed to query the WASAPI recording packet", status); + return false; + } + if (!packetFrames) + return true; + + BYTE * data = NULL; + UINT32 frames = 0; + DWORD flags = 0; + UINT64 devicePosition = 0; + UINT64 qpcPosition = 0; + status = IAudioCaptureClient_GetBuffer(wasapi.record.capture, + &data, &frames, &flags, &devicePosition, &qpcPosition); + if (FAILED(status)) + { + DEBUG_WINERROR("Failed to acquire the WASAPI recording buffer", status); + return false; + } + + if (frames > wasapi.record.bufferFrames) + { + IAudioCaptureClient_ReleaseBuffer(wasapi.record.capture, frames); + DEBUG_ERROR("WASAPI returned an oversized recording packet"); + return false; + } + + const bool clockValid = + (flags & AUDCLNT_BUFFERFLAGS_TIMESTAMP_ERROR) == 0 && + qpcPosition <= INT64_MAX / WASAPI_QPC_TICK_NS; + const bool discontinuity = wasapi.record.discontinuityPending || + (flags & AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY) != 0; + LG_AudioClock clock = + { + .position = devicePosition, + .time = qpcPosition <= INT64_MAX / WASAPI_QPC_TICK_NS ? + (int64_t)qpcPosition * WASAPI_QPC_TICK_NS : 0, + .rate = 0.0, + .stable = !discontinuity, + .discontinuity = discontinuity + }; + if (flags & AUDCLNT_BUFFERFLAGS_SILENT) + data = wasapi.record.silence; + else if (!data) + { + IAudioCaptureClient_ReleaseBuffer(wasapi.record.capture, frames); + DEBUG_ERROR("WASAPI returned a recording packet without data"); + return false; + } + + const size_t bytes = + (size_t)frames * (size_t)wasapi.record.stride; + uint8_t * destination = wasapi_captureQueueBegin( + &wasapi.record.queue, bytes); + if (!destination) + { + wasapi.record.discontinuityPending = true; + status = IAudioCaptureClient_ReleaseBuffer( + wasapi.record.capture, frames); + if (FAILED(status)) + { + DEBUG_WINERROR("Failed to release the WASAPI recording buffer", + status); + return false; + } + continue; + } + + memcpy(destination, data, bytes); + status = IAudioCaptureClient_ReleaseBuffer( + wasapi.record.capture, frames); + if (FAILED(status)) + { + DEBUG_WINERROR("Failed to release the WASAPI recording buffer", status); + return false; + } + + wasapi_captureQueueCommit(&wasapi.record.queue, bytes, (int)frames, + clockValid ? &clock : NULL); + wasapi.record.discontinuityPending = !clockValid; + if (!SetEvent(wasapi.record.senderEvent)) + { + DEBUG_WINERROR("Failed to signal the WASAPI recording sender", + HRESULT_FROM_WIN32(GetLastError())); + return false; + } + } +} + +static int wasapi_recordThread(void * opaque) +{ + (void)opaque; + + const HRESULT comStatus = CoInitializeEx(NULL, COINIT_MULTITHREADED); + const bool comInitialized = SUCCEEDED(comStatus); + HANDLE realtime = NULL; + if (!comInitialized) + { + DEBUG_WINERROR("Failed to initialize COM for WASAPI recording", comStatus); + wasapi.record.commandResult = false; + atomic_store_explicit( + &wasapi.record.terminated, true, memory_order_release); + SetEvent(wasapi.record.commandDone); + return 0; + } + + realtime = wasapi_enableRealtime(); + wasapi.record.commandResult = wasapi_recordInitialize(); + if (!wasapi.record.commandResult) + atomic_store_explicit( + &wasapi.record.terminated, true, memory_order_release); + SetEvent(wasapi.record.commandDone); + if (!wasapi.record.commandResult) + goto exit; + + const HANDLE events[] = + { + wasapi.record.commandEvent, + wasapi.record.audioEvent + }; + + for (;;) + { + const DWORD result = WaitForMultipleObjects( + sizeof(events) / sizeof(*events), events, false, INFINITE); + if (result == WAIT_OBJECT_0) + { + const WasapiRecordCommand command = + (WasapiRecordCommand)InterlockedExchange( + &wasapi.record.command, WASAPI_RECORD_COMMAND_NONE); + switch (command) + { + case WASAPI_RECORD_COMMAND_CONTROL: + wasapi_recordApplyControl(); + wasapi.record.commandResult = true; + SetEvent(wasapi.record.commandDone); + break; + + case WASAPI_RECORD_COMMAND_EXIT: + if (wasapi.record.active) + { + const HRESULT status = IAudioClient_Stop(wasapi.record.client); + if (FAILED(status)) + DEBUG_WINERROR("Failed to stop WASAPI recording", status); + wasapi.record.active = false; + } + wasapi.record.failureFn = NULL; + wasapi.record.failureCookie = 0; + wasapi.record.commandResult = true; + goto exitCommand; + + case WASAPI_RECORD_COMMAND_NONE: + wasapi.record.commandResult = false; + SetEvent(wasapi.record.commandDone); + break; + } + } + else if (result == WAIT_OBJECT_0 + 1) + { + if (wasapi.record.active && !wasapi_recordProcess()) + wasapi_recordFail(); + } + else + { + const DWORD error = result == WAIT_FAILED ? + GetLastError() : ERROR_GEN_FAILURE; + DEBUG_WINERROR("Failed to wait for WASAPI recording", + HRESULT_FROM_WIN32(error)); + wasapi_recordFail(); + wasapi.record.commandResult = false; + goto exitCommand; + } + } + +exitCommand: + wasapi_disableRealtime(realtime); + wasapi_recordRelease(); + CoUninitialize(); + atomic_store_explicit( + &wasapi.record.terminated, true, memory_order_release); + SetEvent(wasapi.record.commandDone); + return 0; + +exit: + wasapi_disableRealtime(realtime); + wasapi_recordRelease(); + CoUninitialize(); + return 0; +} + +static bool wasapi_recordSendLocked(WasapiRecordCommand command) +{ + if (atomic_load_explicit( + &wasapi.record.terminated, memory_order_acquire)) + return false; + + InterlockedExchange(&wasapi.record.command, command); + wasapi.record.commandResult = false; + const bool signaled = SetEvent(wasapi.record.commandEvent) != 0; + const bool waited = signaled && wasapi_wait( + wasapi.record.commandDone, "Failed to wait for WASAPI recording"); + return waited && wasapi.record.commandResult; +} + +static void wasapi_recordClose(void) +{ + EnterCriticalSection(&wasapi.record.commandLock); + if (wasapi.record.thread) + { + wasapi_recordSendLocked(WASAPI_RECORD_COMMAND_EXIT); + lgJoinThread(wasapi.record.thread, NULL); + wasapi.record.thread = NULL; + } + + if (wasapi.record.commandDone) + CloseHandle(wasapi.record.commandDone); + if (wasapi.record.audioEvent) + CloseHandle(wasapi.record.audioEvent); + if (wasapi.record.commandEvent) + CloseHandle(wasapi.record.commandEvent); + if (wasapi.record.senderEvent) + CloseHandle(wasapi.record.senderEvent); + wasapi.record.commandDone = NULL; + wasapi.record.audioEvent = NULL; + wasapi.record.commandEvent = NULL; + wasapi.record.senderEvent = NULL; + wasapi.record.pushFn = NULL; + wasapi.record.failureFn = NULL; + wasapi.record.failureCookie = 0; + LeaveCriticalSection(&wasapi.record.commandLock); +} + +static bool wasapi_recordStart(const LG_AudioFormat * format, + LG_AudioPushFn pushFn, LG_AudioFailureFn failureFn, + uint32_t failureCookie) +{ + wasapi_recordClose(); + + WAVEFORMATEXTENSIBLE wave; + if (!format || !pushFn || !wasapi_waveFormat(format, &wave)) + return false; + + wasapi.record.format = *format; + wasapi.record.stride = wave.Format.nBlockAlign; + wasapi.record.pushFn = pushFn; + wasapi.record.failureFn = failureFn; + wasapi.record.failureCookie = failureCookie; + wasapi.record.command = WASAPI_RECORD_COMMAND_NONE; + atomic_store_explicit( + &wasapi.record.terminated, false, memory_order_relaxed); + wasapi_controlPendingReset(&wasapi.record.controlPending); + + wasapi.record.commandEvent = CreateEventW(NULL, false, false, NULL); + wasapi.record.audioEvent = CreateEventW(NULL, false, false, NULL); + wasapi.record.commandDone = CreateEventW(NULL, false, false, NULL); + wasapi.record.senderEvent = CreateEventW(NULL, false, false, NULL); + if (!wasapi.record.commandEvent || !wasapi.record.audioEvent || + !wasapi.record.commandDone || !wasapi.record.senderEvent) + { + DEBUG_WINERROR("Failed to create WASAPI recording events", + HRESULT_FROM_WIN32(GetLastError())); + wasapi_recordClose(); + return false; + } + + if (!lgCreateThread("WASAPI Recording", wasapi_recordThread, NULL, + &wasapi.record.thread)) + { + wasapi_recordClose(); + return false; + } + + const bool initialized = wasapi_wait(wasapi.record.commandDone, + "Failed to initialize WASAPI recording"); + if (!initialized || !wasapi.record.commandResult) + { + if (!initialized && !atomic_load_explicit( + &wasapi.record.terminated, memory_order_acquire)) + { + InterlockedExchange(&wasapi.record.command, + WASAPI_RECORD_COMMAND_EXIT); + if (!SetEvent(wasapi.record.commandEvent)) + DEBUG_WINERROR("Failed to stop WASAPI recording initialization", + HRESULT_FROM_WIN32(GetLastError())); + } + lgJoinThread(wasapi.record.thread, NULL); + wasapi.record.thread = NULL; + wasapi_recordClose(); + return false; + } + + return true; +} + +static void wasapi_recordStop(void) +{ + wasapi_recordClose(); +} + +static void wasapi_recordVolume(int channels, const uint16_t volume[]) +{ + if (!volume || channels <= 0) + return; + + EnterCriticalSection(&wasapi.record.commandLock); + if (!wasapi.record.thread || + channels != wasapi.record.format.channelCount) + { + LeaveCriticalSection(&wasapi.record.commandLock); + return; + } + wasapi.record.controlChannels = channels; + for (int i = 0; i < channels; ++i) + wasapi.record.controlVolume[i] = wasapi_volumeLevel(volume[i]); + wasapi.record.controlPending.volume = true; + wasapi_recordSendLocked(WASAPI_RECORD_COMMAND_CONTROL); + LeaveCriticalSection(&wasapi.record.commandLock); +} + +static void wasapi_recordMute(bool mute) +{ + EnterCriticalSection(&wasapi.record.commandLock); + if (!wasapi.record.thread) + { + LeaveCriticalSection(&wasapi.record.commandLock); + return; + } + + wasapi.record.controlMute = mute; + wasapi.record.controlPending.mute = true; + wasapi_recordSendLocked(WASAPI_RECORD_COMMAND_CONTROL); + LeaveCriticalSection(&wasapi.record.commandLock); +} + +static bool wasapi_init(void) +{ + InitializeCriticalSection(&wasapi.playback.commandLock); + wasapi.playback.commandLockInitialized = true; + InitializeCriticalSection(&wasapi.record.commandLock); + wasapi.record.commandLockInitialized = true; + return true; +} + +static void wasapi_free(void) +{ + wasapi_playbackClose(); + wasapi_recordClose(); + + if (wasapi.playback.commandLockInitialized) + { + DeleteCriticalSection(&wasapi.playback.commandLock); + wasapi.playback.commandLockInitialized = false; + } + if (wasapi.record.commandLockInitialized) + { + DeleteCriticalSection(&wasapi.record.commandLock); + wasapi.record.commandLockInitialized = false; + } +} + +struct LG_AudioDevOps LGAD_WASAPI = +{ + .name = "WASAPI", + .init = wasapi_init, + .free = wasapi_free, + .playback = + { + .setup = wasapi_playbackSetup, + .start = wasapi_playbackStart, + .stop = wasapi_playbackStop, + .volume = wasapi_playbackVolume, + .mute = wasapi_playbackMute, + .latency = wasapi_playbackLatency + }, + .record = + { + .start = wasapi_recordStart, + .stop = wasapi_recordStop, + .volume = wasapi_recordVolume, + .mute = wasapi_recordMute + } +}; diff --git a/client/src/audio.c b/client/src/audio.c index 8c0ff68a..12d1d3f1 100644 --- a/client/src/audio.c +++ b/client/src/audio.c @@ -34,6 +34,7 @@ #include "dynamic/audiodev.h" #include +#include #include #include #include @@ -2307,7 +2308,8 @@ static PlaybackDataResult playbackData(const void * data, size_t frameCount, PlaybackSourceData * sourceData = &audio.playback.sourceData; if (!data || frameCount > (size_t)sourceData->maxSourcePacketFrames) { - DEBUG_ERROR("Invalid playback packet length: %zu frames", frameCount); + DEBUG_ERROR("Invalid playback packet length: %" PRIu64 " frames", + (uint64_t)frameCount); return PLAYBACK_DATA_DROP; } const int frames = frameCount; diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index 0533e12e..01d873b7 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -114,6 +114,34 @@ if(LG_CLIENT_PLATFORM_LINUX) ) endif() +add_executable(wasapi-format-tests + wasapi_format_test.c + ../audiodevs/WASAPI/format.c +) +target_include_directories(wasapi-format-tests PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/../audiodevs/WASAPI" +) +add_test(NAME wasapi-format-tests + COMMAND wasapi-format-tests +) +set_tests_properties(wasapi-format-tests PROPERTIES + TIMEOUT 10 +) + +add_executable(wasapi-capture-queue-tests + wasapi_capture_queue_test.c + ../audiodevs/WASAPI/capture_queue.c +) +target_include_directories(wasapi-capture-queue-tests PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/../audiodevs/WASAPI" +) +add_test(NAME wasapi-capture-queue-tests + COMMAND wasapi-capture-queue-tests +) +set_tests_properties(wasapi-capture-queue-tests PROPERTIES + TIMEOUT 10 +) + if(LG_CLIENT_PLATFORM_WINDOWS) add_executable(common-windows-tests common_windows_test.c diff --git a/client/tests/wasapi_capture_queue_test.c b/client/tests/wasapi_capture_queue_test.c new file mode 100644 index 00000000..a4879766 --- /dev/null +++ b/client/tests/wasapi_capture_queue_test.c @@ -0,0 +1,121 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program 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. + * + * 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "capture_queue.h" + +#include +#include +#include + +#define TEST_ASSERT(expr) \ + do \ + { \ + if (!(expr)) \ + { \ + fprintf(stderr, "%s:%d: assertion failed: %s\n", \ + __FILE__, __LINE__, #expr); \ + return 1; \ + } \ + } \ + while (0) + +static int testQueue(void) +{ + WasapiCaptureQueue queue = { 0 }; + TEST_ASSERT(!wasapi_captureQueueInit(NULL, 2, 4)); + TEST_ASSERT(!wasapi_captureQueueInit(&queue, 0, 4)); + TEST_ASSERT(!wasapi_captureQueueInit(&queue, 2, 0)); + TEST_ASSERT(wasapi_captureQueueInit(&queue, 2, 4)); + + const uint8_t first[] = { 1, 2, 3, 4 }; + const uint8_t second[] = { 5, 6, 7, 8 }; + const uint8_t third[] = { 9, 10, 11, 12 }; + const LG_AudioClock clock = + { + .position = 42, + .time = 1000, + .rate = 0.0, + .stable = true, + .discontinuity = false + }; + + TEST_ASSERT(!wasapi_captureQueueBegin(&queue, sizeof(first) + 1)); + TEST_ASSERT(!wasapi_captureQueuePush( + &queue, first, sizeof(first) + 1, 2, &clock)); + TEST_ASSERT(!wasapi_captureQueuePush( + &queue, NULL, sizeof(first), 2, &clock)); + + uint8_t * destination = wasapi_captureQueueBegin(&queue, sizeof(first)); + TEST_ASSERT(destination); + memcpy(destination, first, sizeof(first)); + TEST_ASSERT(!wasapi_captureQueueFront(&queue)); + wasapi_captureQueueCommit(&queue, sizeof(first), 2, &clock); + + TEST_ASSERT(wasapi_captureQueuePush( + &queue, second, sizeof(second), 2, NULL)); + TEST_ASSERT(!wasapi_captureQueuePush( + &queue, third, sizeof(third), 2, &clock)); + + const WasapiCapturePacket * packet = wasapi_captureQueueFront(&queue); + TEST_ASSERT(packet); + TEST_ASSERT(packet->bytes == sizeof(first)); + TEST_ASSERT(packet->frames == 2); + TEST_ASSERT(packet->clockValid); + TEST_ASSERT(packet->clock.position == clock.position); + TEST_ASSERT(packet->clock.time == clock.time); + TEST_ASSERT(packet->clock.rate == 0.0); + TEST_ASSERT(packet->clock.stable); + TEST_ASSERT(!packet->clock.discontinuity); + TEST_ASSERT(memcmp(packet->data, first, sizeof(first)) == 0); + wasapi_captureQueuePop(&queue); + + TEST_ASSERT(wasapi_captureQueuePush( + &queue, third, sizeof(third), 2, &clock)); + packet = wasapi_captureQueueFront(&queue); + TEST_ASSERT(packet); + TEST_ASSERT(!packet->clockValid); + TEST_ASSERT(memcmp(packet->data, second, sizeof(second)) == 0); + wasapi_captureQueuePop(&queue); + + packet = wasapi_captureQueueFront(&queue); + TEST_ASSERT(packet); + TEST_ASSERT(packet->clockValid); + TEST_ASSERT(memcmp(packet->data, third, sizeof(third)) == 0); + wasapi_captureQueuePop(&queue); + TEST_ASSERT(!wasapi_captureQueueFront(&queue)); + + TEST_ASSERT(wasapi_captureQueuePush( + &queue, first, sizeof(first), 2, &clock)); + wasapi_captureQueueReset(&queue); + TEST_ASSERT(!wasapi_captureQueueFront(&queue)); + TEST_ASSERT(wasapi_captureQueuePush( + &queue, second, sizeof(second), 2, NULL)); + + wasapi_captureQueueFree(&queue); + TEST_ASSERT(!queue.packets); + TEST_ASSERT(!queue.data); + wasapi_captureQueueFree(&queue); + return 0; +} + +int main(void) +{ + return testQueue(); +} diff --git a/client/tests/wasapi_format_test.c b/client/tests/wasapi_format_test.c new file mode 100644 index 00000000..039fd5d1 --- /dev/null +++ b/client/tests/wasapi_format_test.c @@ -0,0 +1,207 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program 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. + * + * 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "format.h" + +#include +#include + +#define TEST_ASSERT(expr) \ + do \ + { \ + if (!(expr)) \ + { \ + fprintf(stderr, "%s:%d: assertion failed: %s\n", \ + __FILE__, __LINE__, #expr); \ + return 1; \ + } \ + } \ + while (0) + +static int testSampleSizes(void) +{ + TEST_ASSERT(wasapi_sampleSize(LG_AUDIO_FMT_U8 ) == 1); + TEST_ASSERT(wasapi_sampleSize(LG_AUDIO_FMT_S16_LE) == 2); + TEST_ASSERT(wasapi_sampleSize(LG_AUDIO_FMT_S24_LE) == 3); + TEST_ASSERT(wasapi_sampleSize(LG_AUDIO_FMT_S32_LE) == 4); + TEST_ASSERT(wasapi_sampleSize(LG_AUDIO_FMT_F32_LE) == 4); + TEST_ASSERT(wasapi_sampleSize(LG_AUDIO_FMT_F32_NE) == 4); + TEST_ASSERT(wasapi_sampleSize(LG_AUDIO_FMT_F64_LE) == 8); + return 0; +} + +static int testChannelMasks(void) +{ + LG_AudioFormat stereo = + { + .channelCount = 2, + .channels = + { + LG_AUDIO_CH_FRONT_LEFT, + LG_AUDIO_CH_FRONT_RIGHT + } + }; + TEST_ASSERT(wasapi_channelMask(&stereo) == UINT32_C(0x3)); + + LG_AudioFormat surround = + { + .channelCount = 6, + .channels = + { + LG_AUDIO_CH_FRONT_LEFT, + LG_AUDIO_CH_FRONT_RIGHT, + LG_AUDIO_CH_FRONT_CENTER, + LG_AUDIO_CH_LFE, + LG_AUDIO_CH_REAR_LEFT, + LG_AUDIO_CH_REAR_RIGHT + } + }; + TEST_ASSERT(wasapi_channelMask(&surround) == UINT32_C(0x3f)); + + LG_AudioFormat mono = + { + .channelCount = 1, + .channels = { LG_AUDIO_CH_MONO } + }; + TEST_ASSERT(wasapi_channelMask(&mono) == UINT32_C(0x4)); + + stereo.channels[0] = LG_AUDIO_CH_UNKNOWN; + TEST_ASSERT(wasapi_channelMask(&stereo) == 0); + stereo.channels[0] = LG_AUDIO_CH_FRONT_RIGHT; + stereo.channels[1] = LG_AUDIO_CH_FRONT_LEFT; + TEST_ASSERT(wasapi_channelMask(&stereo) == 0); + stereo.channels[0] = LG_AUDIO_CH_FRONT_LEFT; + stereo.channels[1] = LG_AUDIO_CH_FRONT_LEFT; + TEST_ASSERT(wasapi_channelMask(&stereo) == 0); + stereo.channels[1] = LG_AUDIO_CH_MONO; + TEST_ASSERT(wasapi_channelMask(&stereo) == 0); + return 0; +} + +static int testFormatInfo(void) +{ + LG_AudioFormat format = + { + .sampleRate = 48000, + .sampleFormat = LG_AUDIO_FMT_S16_LE, + .channelCount = 2, + .channels = + { + LG_AUDIO_CH_FRONT_LEFT, + LG_AUDIO_CH_FRONT_RIGHT + } + }; + WasapiFormatInfo info; + TEST_ASSERT(wasapi_formatInfo(&format, &info)); + TEST_ASSERT(info.blockAlign == 4); + TEST_ASSERT(info.bytesPerSecond == 192000); + TEST_ASSERT(info.bitsPerSample == 16); + TEST_ASSERT(info.channelMask == UINT32_C(0x3)); + TEST_ASSERT(info.sampleType == WASAPI_SAMPLE_TYPE_PCM); + + format.sampleFormat = LG_AUDIO_FMT_F32_LE; + TEST_ASSERT(wasapi_formatInfo(&format, &info)); + TEST_ASSERT(info.blockAlign == 8); + TEST_ASSERT(info.bytesPerSecond == 384000); + TEST_ASSERT(info.bitsPerSample == 32); + TEST_ASSERT(info.sampleType == WASAPI_SAMPLE_TYPE_FLOAT); + + TEST_ASSERT(!wasapi_formatInfo(NULL, &info)); + TEST_ASSERT(!wasapi_formatInfo(&format, NULL)); + + format.sampleRate = 0; + TEST_ASSERT(!wasapi_formatInfo(&format, &info)); + format.sampleRate = 48000; + + format.channelCount = 0; + TEST_ASSERT(!wasapi_formatInfo(&format, &info)); + format.channelCount = LG_AUDIO_MAX_CHANNELS + 1; + TEST_ASSERT(!wasapi_formatInfo(&format, &info)); + format.channelCount = 2; + + format.channels[0] = LG_AUDIO_CH_UNKNOWN; + TEST_ASSERT(!wasapi_formatInfo(&format, &info)); + format.channels[0] = LG_AUDIO_CH_FRONT_RIGHT; + format.channels[1] = LG_AUDIO_CH_FRONT_LEFT; + TEST_ASSERT(!wasapi_formatInfo(&format, &info)); + format.channels[0] = LG_AUDIO_CH_FRONT_LEFT; + format.channels[1] = LG_AUDIO_CH_FRONT_RIGHT; + + format.sampleFormat = (LG_AudioSampleFormat)-1; + TEST_ASSERT(!wasapi_formatInfo(&format, &info)); + format.sampleFormat = LG_AUDIO_FMT_F64_LE; + format.sampleRate = UINT32_MAX; + TEST_ASSERT(!wasapi_formatInfo(&format, &info)); + return 0; +} + +static int testControlPending(void) +{ + WasapiControlPending state = + { + .volume = true, + .mute = true, + }; + + WasapiControlPending pending = wasapi_controlPendingTake(&state); + TEST_ASSERT(pending.volume); + TEST_ASSERT(pending.mute); + TEST_ASSERT(!state.volume); + TEST_ASSERT(!state.mute); + + pending = wasapi_controlPendingTake(&state); + TEST_ASSERT(!pending.volume); + TEST_ASSERT(!pending.mute); + + state.volume = true; + state.mute = true; + wasapi_controlPendingReset(&state); + TEST_ASSERT(!state.volume); + TEST_ASSERT(!state.mute); + return 0; +} + +static int testPeriods(void) +{ + TEST_ASSERT(wasapi_framesToDuration(480, 48000) == UINT64_C(100000)); + TEST_ASSERT(wasapi_framesToDuration(1, 48000) == UINT64_C(209)); + TEST_ASSERT(wasapi_framesToDuration(1, 0) == 0); + + return 0; +} + +static int testBufferMath(void) +{ + TEST_ASSERT(wasapi_availableFrames(1024, 256) == 768); + TEST_ASSERT(wasapi_availableFrames(1024, 1024) == 0); + TEST_ASSERT(wasapi_availableFrames(1024, 2048) == 0); + TEST_ASSERT(wasapi_silenceByte(LG_AUDIO_FMT_U8) == UINT8_C(0x80)); + TEST_ASSERT(wasapi_silenceByte(LG_AUDIO_FMT_S16_LE) == 0); + return 0; +} + +int main(void) +{ + if (testSampleSizes() || testChannelMasks() || testFormatInfo() || + testControlPending() || testPeriods() || testBufferMath()) + return 1; + + return 0; +}