power_play/src/playback/wasapi/playback_wasapi.c
2025-08-11 06:49:09 -05:00

208 lines
6.9 KiB
C

/* WASAPI backend for audio playback
*
* Based on mmozeiko's WASAPI examples
* https://gist.github.com/mmozeiko/5a5b168e61aff4c1eaec0381da62808f#file-win32_wasapi-h
*/
PB_WSP_SharedState PB_WSP_shared_state = ZI;
////////////////////////////////
//~ Startup
void PB_StartupCore(void)
{
__prof;
PB_WSP_SharedState *g = &PB_WSP_shared_state;
PB_WSP_InitializeWasapi();
/* Start playback job */
RunJob(1, PB_WSP_PlaybackJob, JobPool_Audio, JobPriority_High, &g->PB_WSP_PlaybackJob_counter, 0);
OnExit(&PB_WSP_Shutdown);
}
ExitFuncDef(PB_WSP_Shutdown)
{
__prof;
PB_WSP_SharedState *g = &PB_WSP_shared_state;
Atomic32FetchSet(&g->shutdown, 1);
YieldOnCounter(&g->PB_WSP_PlaybackJob_counter);
}
void PB_WSP_InitializeWasapi(void)
{
PB_WSP_SharedState *g = &PB_WSP_shared_state;
u64 sample_rate = PB_SampleRate;
u64 channel_count = 2;
u32 channel_mask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;
/* Create enumerator to get audio device */
IMMDeviceEnumerator *enumerator;
CoCreateInstance(&CLSID_MMDeviceEnumerator, 0, CLSCTX_ALL, &IID_IMMDeviceEnumerator, (LPVOID *)&enumerator);
/* Get default playback device */
IMMDevice *device;
IMMDeviceEnumerator_GetDefaultAudioEndpoint(enumerator, eRender, eConsole, &device);
IMMDeviceEnumerator_Release(enumerator);
/* Create audio client for device */
IMMDevice_Activate(device, &IID_IAudioClient, CLSCTX_ALL, 0, (LPVOID *)&g->client);
IMMDevice_Release(device);
WAVEFORMATEXTENSIBLE format_ex = {
.Format = {
.wFormatTag = WAVE_FORMAT_EXTENSIBLE,
.nChannels = (WORD)channel_count,
.nSamplesPerSec = (WORD)sample_rate,
.nAvgBytesPerSec = (DWORD)(sample_rate * channel_count * sizeof(f32)),
.nBlockAlign = (WORD)(channel_count * sizeof(f32)),
.wBitsPerSample = (WORD)(8 * sizeof(f32)),
.cbSize = sizeof(format_ex) - sizeof(format_ex.Format),
},
.Samples.wValidBitsPerSample = 8 * sizeof(f32),
.dwChannelMask = channel_mask,
.SubFormat = MEDIASUBTYPE_IEEE_FLOAT,
};
WAVEFORMATEX *wfx = &format_ex.Format;
#if 0
b32 client_initialized = 0;
IAudioClient3 *client3;
if (SUCCEEDED(IAudioClient_QueryInterface(g->client, &IID_IAudioClient3, (LPVOID *)&client3)))
{
/* From Martins: Minimum buffer size will typically be 480 samples (10msec @ 48khz)
* but it can be 128 samples (2.66 msec @ 48khz) if driver is properly installed
* see bullet-point instructions here: https://learn.microsoft.com/en-us/windows-hardware/drivers/audio/low-latency-audio#measurement-tools
*/
UINT32 default_period_samples, fundamental_period_samples, min_period_samples, max_period_samples;
IAudioClient3_GetSharedModeEnginePeriod(client3, wfx, &default_period_samples, &fundamental_period_samples, &min_period_samples, &max_period_samples);
const DWORD flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK;
if (SUCCEEDED(IAudioClient3_InitializeSharedAudioStream(client3, flags, min_period_samples, wfx, 0)))
{
client_initialized = 1;
}
IAudioClient3_Release(client3);
}
#else
b32 client_initialized = 0;
#endif
if (!client_initialized)
{
/* Get duration for shared-mode streams, this will typically be 480 samples (10msec @ 48khz) */
REFERENCE_TIME duration;
IAudioClient_GetDevicePeriod(g->client, &duration, 0);
/* Initialize audio playback
*
* NOTE:
* Passing AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM will tell WASAPI to
* always convert to native mixing format. This may introduce latency
* but allows for any input format.
*/
const DWORD flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY;
IAudioClient_Initialize(g->client, AUDCLNT_SHAREMODE_SHARED, flags, duration, 0, wfx, 0);
}
IAudioClient_GetMixFormat(g->client, &g->buffer_format);
/* Set up event handler to wait on */
g->event = CreateEventW(0, 0, 0, 0);
IAudioClient_SetEventHandle(g->client, g->event);
/* Get playback client */
IAudioClient_GetService(g->client, &IID_IAudioRenderClient, (LPVOID *)&g->playback);
/* Start the playback */
IAudioClient_Start(g->client);
/* Get audio buffer size in samples */
IAudioClient_GetBufferSize(g->client, &g->buffer_frames);
}
////////////////////////////////
//~ Wasapi update
PB_WSP_Buff PB_WSP_BeginUpdate(void)
{
__prof;
PB_WSP_SharedState *g = &PB_WSP_shared_state;
PB_WSP_Buff wspbuf = ZI;
/* Get padding frames */
u32 padding_frames;
IAudioClient_GetCurrentPadding(g->client, &padding_frames);
/* Get output buffer from WASAPI */
wspbuf.frames_count = 0;
if (padding_frames <= g->buffer_frames)
{
wspbuf.frames_count = g->buffer_frames - padding_frames;
}
IAudioRenderClient_GetBuffer(g->playback, wspbuf.frames_count, &wspbuf.frames);
return wspbuf;
}
void PB_WSP_EndUpdate(PB_WSP_Buff *wspbuf, MIX_PcmF32 src)
{
__prof;
PB_WSP_SharedState *g = &PB_WSP_shared_state;
u32 frames_in_source = src.count / 2;
u32 frames_in_output = wspbuf->frames_count;
u32 flags = 0;
if (frames_in_source == frames_in_output)
{
/* Copy bytes to output */
u32 bytes_per_sample = g->buffer_format->nBlockAlign / g->buffer_format->nChannels;
u32 write_size = frames_in_source * 2 * bytes_per_sample;
CopyBytes(wspbuf->frames, src.samples, write_size);
}
else
{
/* Submit silence if not enough samples */
flags = AUDCLNT_BUFFERFLAGS_SILENT;
/* This shouldn't occur, mixer should be generating samples equivilent
* to value returned from `PB_WSP_BeginUpdate`. */
Assert(0);
}
#if !AUDIO_ENABLED
flags = AUDCLNT_BUFFERFLAGS_SILENT;
#endif
/* Submit output buffer to WASAPI */
IAudioRenderClient_ReleaseBuffer(g->playback, frames_in_source, flags);
__profframe("Audio");
}
////////////////////////////////
//~ Playback job
JobDef(PB_WSP_PlaybackJob, UNUSED sig, UNUSED id)
{
__prof;
PB_WSP_SharedState *g = &PB_WSP_shared_state;
/* FIXME: If playback fails at any point and mixer stops advancing, we
* need to halt mixer to prevent memory leak when sounds are played. */
/* TODO: Signal counter that running job wiats on, rather than scheduling job manually */
while (!Atomic32Fetch(&g->shutdown))
{
TempArena scratch = BeginScratchNoConflict();
{
__profn("Wasapi wait");
WaitForSingleObject(g->event, INFINITE);
}
{
__profn("Fill sample buffer");
PB_WSP_Buff wspbuf = PB_WSP_BeginUpdate();
MIX_PcmF32 pcm = MIX_MixAllTracks(scratch.arena, wspbuf.frames_count);
PB_WSP_EndUpdate(&wspbuf, pcm);
}
EndScratch(scratch);
}
}