Hello!
After failing miserably to implement WASAPI on my own, I am trying to follow the example made by Martins for Day 19 (https://gist.github.com/mmozeiko/38c64bb65855d783645c#file-win32_handmade-cpp).
Currently I'm doing the buffer fill on a separate thread, writing directly the test sine wave (no secondary buffer yet).
The problem is that the sound doesn't start until ~1 second passes, even tho data is being written in the buffer since the beginning.
Any hint on what could be the problem?
#include "Mmdeviceapi.h" #include "audioclient.h" #include "avrt.h" global HANDLE refillEvent; global IAudioClient* audioClient; global IAudioRenderClient* renderClient; global WAVEFORMATEX waveformat; global u32 bufferSize; #define framesOfAudioLatency 1 #define gameUpdateHz 60 s32 latencySampleCount; s32 secondaryBufferSize; inline void audio_init() { waveformat.wFormatTag = WAVE_FORMAT_PCM; waveformat.nChannels = 2; waveformat.nSamplesPerSec = 48000; waveformat.wBitsPerSample = 16; waveformat.nBlockAlign = waveformat.nChannels * waveformat.wBitsPerSample / 8; waveformat.nAvgBytesPerSec = waveformat.nSamplesPerSec * waveformat.nBlockAlign; waveformat.cbSize = 0; latencySampleCount = framesOfAudioLatency * (waveformat.nSamplesPerSec / gameUpdateHz); secondaryBufferSize = waveformat.nSamplesPerSec; REFERENCE_TIME bufferDuration = 10000000ULL * secondaryBufferSize / waveformat.nSamplesPerSec; /////////////// HRESULT hr; hr = CoInitializeEx(NULL, COINIT_SPEED_OVER_MEMORY); assert(hr == S_OK); IMMDeviceEnumerator* pEnumerator; hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (void**)&pEnumerator); assert(hr == S_OK); IMMDevice* device; hr = pEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, &device); assert(hr == S_OK); hr = device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, NULL, (void**)&audioClient); assert(hr == S_OK); hr = audioClient->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_NOPERSIST, bufferDuration, 0, &waveformat, NULL); assert(hr == S_OK); hr = audioClient->GetService(__uuidof(IAudioRenderClient), (void**)&renderClient); assert(hr == S_OK); hr = audioClient->GetBufferSize(&bufferSize); assert(hr == S_OK); refillEvent = CreateEventEx(NULL, NULL, 0, EVENT_MODIFY_STATE | SYNCHRONIZE); hr = audioClient->SetEventHandle(refillEvent); assert(hr == S_OK); BYTE* data = NULL; hr = renderClient->GetBuffer(bufferSize, &data); assert(hr == S_OK); hr = renderClient->ReleaseBuffer(bufferSize, AUDCLNT_BUFFERFLAGS_SILENT); assert(hr == S_OK); hr = audioClient->Start(); assert(hr == S_OK); } #define M_PI 3.1416 #define TAU (M_PI * 2) inline void sine_wave(s32 samplesToWrite, s16 *samples, s32 toneHz = 256) { static u32 runningSampleIndex = 0; s32 samplesPerSec = waveformat.nSamplesPerSec; s32 squareWavePeriod = samplesPerSec / toneHz; s32 halfSquareWavePeriod = (squareWavePeriod / 2); s16* at = samples; for(s32 i = 0; i < samplesToWrite; i++) { s16 sampleValue = ((runningSampleIndex / halfSquareWavePeriod) % 2) ? 1000 : -1000; *at++ = sampleValue; *at++ = sampleValue; runningSampleIndex++; } } inline void audio_job(void* jobData) { DWORD avrtTaskIndex; AvSetMmThreadCharacteristicsA("Pro Audio", &avrtTaskIndex); while(true) { DWORD res = WaitForSingleObject(refillEvent, INFINITE); if(res == WAIT_OBJECT_0) { s32 samplesToWrite = 0; u32 soundPaddingSize; audioClient->GetCurrentPadding(&soundPaddingSize); samplesToWrite = secondaryBufferSize - soundPaddingSize; if(samplesToWrite > latencySampleCount) { samplesToWrite = latencySampleCount; } if(samplesToWrite > 0) { BYTE* buffer = NULL; HRESULT hr = renderClient->GetBuffer(samplesToWrite, &buffer); assert(hr == S_OK); s16* dest = (s16*)buffer; sine_wave(samplesToWrite, dest); hr = renderClient->ReleaseBuffer(samplesToWrite, 0); assert(hr == S_OK); } } } }
If you want to use wasapi from main thread (which is not ideal) then you need submit it with smaller buffer sizes than one second. To avoid delays ideally it should be just with one frame time + small overhead. But better way is to use wasapi buffer submission from separate thread - which can run with very tiny buffer size (thus low latency). Then your main thread would just prepare info in buffer for this audio thread. We've talked about this approach in this discord thread: https://discord.com/channels/239737791225790464/981439725275611146/981439729327288380
Forum supports markdown. Format code by placing three backticks ``` on new line before and after code.
Any hint on what could be the problem
I might be wrong, but when you do the buffer initialization, you are actually putting 1 second of silence in the buffer (assuming your buffer is one second). In WASAPI, this means that when you add further data in the buffer it will only be played after the one second of silence that you previously submitted.
If I remember correctly, you can't overwrite data previously submitted with WASAPI. So you could try to remove the following lines in the init function, and just call audioClient->Start()
the first time you write the actual audio data in the buffer.
hr = renderClient->GetBuffer(bufferSize, &data); hr = renderClient->ReleaseBuffer(bufferSize, AUDCLNT_BUFFERFLAGS_SILENT); hr = audioClient->Start();
You'll still need to write as few sample as possible if you want low latency. Also what mmozeiko said is probably a good idea, but you can start off with the modification I suggested.
Oh of course! I thought that was just clearing the buffer, didn't realise it was submitting a full buffer (one second) of silence. It works fine after removing those two lines, no delay.
And yes, my idea is to implement it as mmozeiko says, but I want to go step by step.
Thank you both!