Skip to content

Instantly share code, notes, and snippets.

@stillwwater
Last active September 28, 2025 04:22
Show Gist options
  • Select an option

  • Save stillwwater/f30a5341ae28043ddff7610291b3703d to your computer and use it in GitHub Desktop.

Select an option

Save stillwwater/f30a5341ae28043ddff7610291b3703d to your computer and use it in GitHub Desktop.
WASAPI playback sample
#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 = {&notification_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