Handmade Hero » Forums » Code » Day 19 - Audio Latency
mmozeiko
Mārtiņš Možeiko
1485 posts
1 project
#555 Day 19 - Audio Latency
2 years, 10 months ago Edited by Mārtiņš Možeiko on Dec. 13, 2014, 5:29 p.m.

Casey said on Day 19, that for Windows 7 they should have figured low-latency for audio. They have. It is called Windows Core Audio API and includes Windows Audio Session API (WASAPI). It was introduced in Windows Vista. DirectSound, XAudio2, WinMM are all emulated on Vista+, they all use Core Audio to make them work. And by using that all three (DSound, Xaudio2, WinMM) won't get lowest possible latency.

I modified win32_handmade.cpp code from day 19 to use Core Audio/WASAPI, not DirectSound. On my machine DirectSound works only with FramesOfAudioLatency = 3 frames. Core Audio works fine with FramesOfAudioLatency = 1, so it produces exactly next frame of audio - just as we wanted. Although it sometimes drops few samples in startup at first or second frame (I'm guessing it is because Windows is figuring out or caching something in background). But after that it is smooth. The vertical white lines stay pretty stable and evenly spaced - they sometimes move only a tiny bit, pixel or two.

Obviously this won't work on WinXP. It would be possible to adjust code to use Core Audio if it is available otherwise use DirectSound as Casey talks in Q&A.

Here's is code with win32_handmade.cpp adjusted to use WASAPI: https://gist.github.com/mmozeiko/38c64bb65855d783645c Using WASAPI imho is a bit simpler than DirectSound, no more two region nonsense. Although setup part is just a bit more code that before, but nothing too much crazy (except COM stuff).
norswap
29 posts
#579 Day 19 - Audio Latency
2 years, 10 months ago

Kudos! That's nice to know and I bet it will useful later on.
mrmixer
Simon Anciaux
270 posts
#13197 Day 19 - Audio Latency
2 weeks, 5 days ago

mmozeiko
Although it sometimes drops few samples in startup at first or second frame (I'm guessing it is because Windows is figuring out or caching something in background).

I believe the reason is that you call GlobalSoundClient->Start(); before filling the buffer. From IAudioClient::Start documentation:
To avoid start-up glitches with rendering streams, clients should not call Start until the audio engine has been initially loaded with data by calling the IAudioRenderClient::GetBuffer and IAudioRenderClient::ReleaseBuffer methods on the rendering interface.
Filling the audio buffer at initialization is not the solution since it would introduce latency (since we can't overwrite previous samples). One solution is to start the buffer the first time we fill data in it.

For calculating the samples to write, I believe it's better to do
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int SamplesToWrite = 0;
UINT32 SoundPaddingSize;
if (SUCCEEDED(GlobalSoundClient->GetCurrentPadding(&SoundPaddingSize)))
{
    int MaxSampleCount = (int)(SoundOutput.SecondaryBufferSize - SoundPaddingSize);
    SamplesToWrite = (int) SoundOutput.LatencySampleCount - SoundPaddingSize;
    if (SamplesToWrite < 0)
    {
        SamplesToWrite = 0;
    }
    assert(SamplesToWrite <= MaxSampleCount);
}

/* Instead of
int SamplesToWrite = 0;
UINT32 SoundPaddingSize;
if (SUCCEEDED(GlobalSoundClient->GetCurrentPadding(&SoundPaddingSize)))
{
    SamplesToWrite = (int)(SoundOutput.SecondaryBufferSize - SoundPaddingSize);
    if (SamplesToWrite > SoundOutput.LatencySampleCount)
    {
        SamplesToWrite = SoundOutput.LatencySampleCount;
    }
}
*/

We want x samples (one frame worth + some latency). GetCurrentPadding returns the number of samples that are ready and haven't been read in the buffer. So the number of samples to write should be x - padding.