Last active
September 28, 2025 04:22
-
-
Save stillwwater/f30a5341ae28043ddff7610291b3703d to your computer and use it in GitHub Desktop.
WASAPI playback sample
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #include "module.h" | |
| #define COBJMACROS | |
| #include <windows.h> | |
| #include <mmdeviceapi.h> | |
| #include <audioclient.h> | |
| struct audio_buffer audio_buffer; | |
| struct audio_device audio_device; | |
| IMMDevice *audio_wasapi_device; | |
| IMMDeviceEnumerator *audio_device_enumerator; | |
| IAudioClient *audio_client; | |
| IAudioRenderClient *audio_renderer; | |
| HANDLE *audio_client_lock; | |
| mutex_t *audio_mutex; | |
| bool audio_default_device_changed; | |
| bool wasapi_supported; | |
| #define audio_disable_if_failed(hr) \ | |
| if (FAILED(hr)) { print("[wasapi] audio disabled 0x%08X\n", (int)hr); wasapi_supported = 0; return; } | |
| static const CLSID MV_CLSID_MMDeviceEnumerator = {0xBCDE0395, 0xE52F, 0x467C, {0x8E, 0x3D, 0xC4, 0x57, 0x92, 0x91, 0x69, 0x2E}}; | |
| static const IID MV_IID_IMMDeviceEnumerator = {0xA95664D2, 0x9614, 0x4F35, {0xA7, 0x46, 0xDE, 0x8D, 0xB6, 0x36, 0x17, 0xE6}}; | |
| static const IID MV_IID_IAudioRenderClient = {0xF294ACFC, 0x3146, 0x4483, {0xA7, 0xBF, 0xAD, 0xDC, 0xA7, 0xC2, 0x60, 0xE2}}; | |
| static const IID MV_IID_IAudioClient = {0x1CB9AD4C, 0xDBFA, 0x4C32, {0xB1, 0x78, 0xC2, 0xF5, 0x68, 0xA7, 0x03, 0xB2}}; | |
| static const IID MV_IID_IMMNotificationClient = {0x7991eec9, 0x7e89, 0x4d85, {0x83, 0x90, 0x6c, 0x70, 0x3c, 0xec, 0x60, 0xc0}}; | |
| // Since this is not C++, we need to create the vtable / COM stuff to get default device | |
| // changed notifications. Alternativaly we could get the default device during the audio | |
| // callback and see if it changed, but that seems like a bad idea. | |
| struct notification_client { | |
| IMMNotificationClient iface; | |
| LONG refcount; | |
| }; | |
| ULONG | |
| notification_client_add_ref(IMMNotificationClient *this) | |
| { | |
| struct notification_client *self = (void *)this; | |
| return xadd(&self->refcount, 1); | |
| } | |
| ULONG | |
| notification_client_release(IMMNotificationClient *this) | |
| { | |
| struct notification_client *self = (void *)this; | |
| ULONG ref = xadd(&self->refcount, -1); | |
| if (ref == 0) | |
| sys_free(self); | |
| return ref; | |
| } | |
| HRESULT | |
| notification_client_query_interface(IMMNotificationClient *this, REFIID riid, void **ppv) | |
| { | |
| if (IsEqualIID(riid, &IID_IUnknown) || | |
| IsEqualIID(riid, &MV_IID_IMMNotificationClient)) { | |
| *ppv = this; | |
| notification_client_add_ref(this); | |
| return S_OK; | |
| } | |
| *ppv = NULL; | |
| return E_NOINTERFACE; | |
| } | |
| HRESULT | |
| notification_client_device_added(IMMNotificationClient *this, LPCWSTR id) | |
| { | |
| (void)this; | |
| (void)id; | |
| return S_OK; | |
| } | |
| HRESULT | |
| notification_client_device_removed(IMMNotificationClient *this, LPCWSTR id) | |
| { | |
| (void)this; | |
| (void)id; | |
| return S_OK; | |
| } | |
| HRESULT | |
| notification_client_device_state_changed(IMMNotificationClient *this, LPCWSTR id, DWORD state) | |
| { | |
| (void)this; | |
| (void)id; | |
| (void)state; | |
| return S_OK; | |
| } | |
| HRESULT | |
| notification_client_default_device_changed( | |
| IMMNotificationClient *this, EDataFlow flow, ERole role, LPCWSTR id) | |
| { | |
| (void)this; | |
| (void)role; | |
| (void)id; | |
| // don't do anything on the IMMDevice thread to avoid deadlocks | |
| if (flow == eRender) | |
| audio_default_device_changed = true; | |
| return S_OK; | |
| } | |
| HRESULT | |
| notification_client_prop_value_changed(IMMNotificationClient *this, LPCWSTR id, const PROPERTYKEY key) | |
| { | |
| (void)this; | |
| (void)id; | |
| (void)key; | |
| return S_OK; | |
| } | |
| IMMNotificationClientVtbl notification_client_vtbl = { | |
| notification_client_query_interface, | |
| notification_client_add_ref, | |
| notification_client_release, | |
| notification_client_device_state_changed, | |
| notification_client_device_added, | |
| notification_client_device_removed, | |
| notification_client_default_device_changed, | |
| notification_client_prop_value_changed, | |
| }; | |
| struct notification_client audio_notification_client = {¬ification_client_vtbl, 1}; | |
| void | |
| audio_test_tone(int freq) | |
| { | |
| unsigned i; | |
| struct audio_buffer *buffer = audio_lock(); | |
| float rate = 2 * (float)M_PI * freq / (float)audio_device.frequency; | |
| for (i = 0; i < buffer->count; ++i) { | |
| float v = sinf(rate * (float)i); | |
| buffer->samples[i].left += v; | |
| buffer->samples[i].right += v; | |
| } | |
| audio_unlock(); | |
| } | |
| void | |
| audio_set_format(WAVEFORMATEX *fmt) | |
| { | |
| UINT16 tag; | |
| WAVEFORMATEXTENSIBLE exfmt; | |
| if (fmt->wFormatTag == WAVE_FORMAT_EXTENSIBLE) { | |
| memcpy(&exfmt, fmt, sizeof exfmt); | |
| tag = EXTRACT_WAVEFORMATEX_ID(&exfmt.SubFormat); | |
| } else { | |
| tag = fmt->wFormatTag; | |
| exfmt.Format = *fmt; | |
| exfmt.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; | |
| exfmt.Samples.wValidBitsPerSample = exfmt.Format.wBitsPerSample; | |
| exfmt.dwChannelMask = 0; | |
| } | |
| switch (tag) { | |
| case WAVE_FORMAT_IEEE_FLOAT: | |
| audio_device.floatformat = true; | |
| break; | |
| case WAVE_FORMAT_PCM: | |
| audio_device.floatformat = false; | |
| break; | |
| default: | |
| wasapi_supported = false; | |
| print("[wasapi] unknown mix format 0x%08X\n", tag); | |
| } | |
| // TODO check if 5.1, 7.1 surround works. Probably need to request stereo | |
| // in shared mode | |
| audio_device.channels = exfmt.Format.nChannels; | |
| audio_device.formatbits = exfmt.Format.wBitsPerSample; | |
| audio_device.frequency = exfmt.Format.nSamplesPerSec; | |
| } | |
| void | |
| audio_device_init(void) | |
| { | |
| HRESULT hr; | |
| WAVEFORMATEX *fmt; | |
| unsigned wishsamples; | |
| REFERENCE_TIME bufferduration; | |
| UINT32 bufsize; | |
| profile(); | |
| if (audio_wasapi_device) IMMDevice_Release(audio_wasapi_device); | |
| if (audio_client) IAudioClient_Release(audio_client); | |
| if (audio_renderer) IAudioRenderClient_Release(audio_renderer); | |
| // find suitable audio device | |
| hr = IMMDeviceEnumerator_GetDefaultAudioEndpoint(audio_device_enumerator, eRender, eConsole, &audio_wasapi_device); | |
| audio_disable_if_failed(hr); | |
| hr = IMMDevice_Activate(audio_wasapi_device, &MV_IID_IAudioClient, CLSCTX_ALL, NULL, (void **)&audio_client); | |
| audio_disable_if_failed(hr); | |
| // init audio client with a suitable format | |
| // try to get the formt we want | |
| WAVEFORMATEX wfmt = {0}; | |
| wfmt.wFormatTag = WAVE_FORMAT_IEEE_FLOAT; | |
| wfmt.nChannels = 2; | |
| wfmt.nSamplesPerSec = MIX_NOMINAL_SAMPLE_RATE; | |
| wfmt.nAvgBytesPerSec = wfmt.nSamplesPerSec * wfmt.nChannels * sizeof(float); | |
| wfmt.nBlockAlign = 8; | |
| wfmt.wBitsPerSample = 32; | |
| wfmt.cbSize = 0; | |
| fmt = &wfmt; | |
| for (;;) { | |
| audio_set_format(fmt); | |
| if (audio_device.frequency <= 11025) | |
| wishsamples = 256; | |
| else if (audio_device.frequency <= 22050) | |
| wishsamples = 512; | |
| else if (audio_device.frequency <= 44100) | |
| wishsamples = 1024; | |
| else if (audio_device.frequency <= 56000) | |
| wishsamples = 2048; | |
| else | |
| wishsamples = 4096; | |
| bufferduration = (REFERENCE_TIME)wishsamples * 10000000 / audio_device.frequency; | |
| hr = IAudioClient_Initialize( | |
| audio_client, | |
| AUDCLNT_SHAREMODE_SHARED, | |
| AUDCLNT_STREAMFLAGS_EVENTCALLBACK // block on event | |
| | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY // high quality resampler | |
| | AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM, // resampler | |
| bufferduration, 0, fmt, NULL); | |
| if (hr == AUDCLNT_E_UNSUPPORTED_FORMAT) { | |
| print("[wasapi] couldn't use the mixer format, falling back to device default\n"); | |
| hr = IAudioClient_GetMixFormat(audio_client, &fmt); | |
| audio_disable_if_failed(hr); | |
| continue; | |
| } | |
| break; | |
| } | |
| audio_disable_if_failed(hr); | |
| // blocking mode | |
| hr = IAudioClient_SetEventHandle(audio_client, audio_client_lock); | |
| audio_disable_if_failed(hr); | |
| // alloc mixer buffer | |
| hr = IAudioClient_GetBufferSize(audio_client, &bufsize); | |
| usize count = next_power_of_two(bufsize * 10); | |
| if (audio_buffer.count < count) { | |
| print("[wasapi] mixer buffer resized, new size: %d\n", (int)count); | |
| sys_mutex_enter(audio_mutex); | |
| sys_free(audio_buffer.samples); | |
| audio_buffer.count = count; | |
| audio_buffer.mask = audio_buffer.count - 1; | |
| audio_buffer.samples = sys_alloc(H_OS, audio_buffer.count * sizeof *audio_buffer.samples); | |
| sys_mutex_leave(audio_mutex); | |
| } | |
| // init audio renderer | |
| hr = IAudioClient_GetService(audio_client, &MV_IID_IAudioRenderClient, (void **)&audio_renderer); | |
| audio_disable_if_failed(hr); | |
| // start playback | |
| hr = IAudioClient_Start(audio_client); | |
| audio_disable_if_failed(hr); | |
| wasapi_supported = true; | |
| audio_device.init = wasapi_supported; | |
| } | |
| void | |
| audio_thread_init(void) | |
| { | |
| HRESULT hr; | |
| UINT32 bufsize; | |
| profile(); | |
| // COM init stuff | |
| hr = CoInitializeEx(NULL, COINIT_MULTITHREADED); | |
| audio_disable_if_failed(hr); | |
| hr = CoCreateInstance(&MV_CLSID_MMDeviceEnumerator, | |
| NULL, | |
| CLSCTX_ALL, &MV_IID_IMMDeviceEnumerator, | |
| (void **) &audio_device_enumerator); | |
| audio_disable_if_failed(hr); | |
| audio_mutex = sys_mutex(); | |
| audio_client_lock = CreateEventA(NULL, 0, 0, NULL); | |
| audio_device_init(); | |
| // get notified if the default device changes | |
| // if this doesn't work we won't get notified of device changed, which is fine | |
| IMMDeviceEnumerator_RegisterEndpointNotificationCallback(audio_device_enumerator, | |
| (IMMNotificationClient *)&audio_notification_client); | |
| if (wasapi_supported) { | |
| IAudioClient_GetBufferSize(audio_client, &bufsize); | |
| print("\naudio_init:\n"); | |
| print("Driver: WASAPI\n"); | |
| print("Sample Rate: %dHz\n", audio_device.frequency); | |
| print("Channels: %d\n", audio_device.channels); | |
| print("Driver buffer size: %d (%g ms)\n", bufsize, bufsize * 1000.0 / audio_device.frequency); | |
| print("Mixer buffer size: %d (%g ms)\n", audio_buffer.count, | |
| audio_buffer.count * 1000.0 / audio_device.frequency); | |
| if (audio_device.floatformat) | |
| print("Format: WAVE_FORMAT_IEEE_FLOAT\n\n"); | |
| else | |
| print("Format: WAVE_FORMAT_PCM (%d)\n\n", audio_device.formatbits); | |
| } | |
| } | |
| int | |
| audio_thread_main(void *args) | |
| { | |
| UINT32 npadding; | |
| UINT32 navail; | |
| BYTE *client_buffer; | |
| DWORD flags = 0; | |
| UINT32 bufsize; | |
| (void)args; | |
| // wasapi init is very slow so do it off the main thread | |
| audio_thread_init(); | |
| // don't try to render anything on the first request to clear the device buffer if it | |
| // was holding on to anything for some reason | |
| flags = AUDCLNT_BUFFERFLAGS_SILENT; | |
| for (;;) { | |
| WaitForSingleObject(audio_client_lock, INFINITE); | |
| if (!wasapi_supported) | |
| continue; | |
| if (audio_default_device_changed) { | |
| print("[wasapi] audio device changed\n"); | |
| audio_default_device_changed = false; | |
| audio_device_init(); | |
| flags = AUDCLNT_BUFFERFLAGS_SILENT; | |
| continue; | |
| } | |
| profile("audio_thread_main"); | |
| IAudioClient_GetBufferSize(audio_client, &bufsize); | |
| IAudioClient_GetCurrentPadding(audio_client, &npadding); | |
| navail = bufsize - npadding; | |
| IAudioRenderClient_GetBuffer(audio_renderer, navail, &client_buffer); | |
| if (!client_buffer) { | |
| // lost device | |
| Sleep(0); // yield | |
| continue; | |
| } | |
| mix_resolve(client_buffer, navail, audio_device.frequency); | |
| IAudioRenderClient_ReleaseBuffer(audio_renderer, navail, flags); | |
| flags = 0; | |
| } | |
| return 0; | |
| } | |
| void | |
| audio_init(void) | |
| { | |
| sys_thread("Audio Thread (WASAPI)", THREAD_AUDIO, SYS_PRIORITY_HIGH, audio_thread_main, NULL); | |
| } | |
| struct audio_buffer * | |
| audio_lock(void) | |
| { | |
| if (!audio_device.init || !audio_buffer.samples) | |
| return NULL; | |
| sys_mutex_enter(audio_mutex); | |
| return &audio_buffer; | |
| } | |
| void | |
| audio_unlock(void) | |
| { | |
| sys_mutex_leave(audio_mutex); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment