It's been a long time so I might be wrong. I'm using the code from day 009 (copied below) assuming that was what lesson 9 meant.
The running sample index isn't "where we are in the audio we want to play". It's tracking the last place we wrote in the audio buffer (more precisely 1 past the last "valid" sample in the buffer, which also indicate the place we need to write next if we don't want a "hole" in the sound samples). We use the running sample index to compute the ByteToLock value which is the first byte where we need to write new samples to.
The target cursor doesn't "goes beyond the sample index by our latency amount". It uses the play cursor, not the sample index. It represent 1 past the last sample we want to write to. It is meant to be as close as possible to the play cursor to reduce latency.
We don't fill from the running index to the play cursor, we fill from the running index (ByteToLock) to the target cursor.
1. The msdn documentation says you should not write between the play cursor and the write cursor but we don't even look at the write cursor. Does it not actually matter?
- You should not write between the play and write cursor. What happens if you do is "undefined". The new audio might be played if you're lucky but it most likely will not.
You could see (meaning it's my mental model and probably not correct) direct sound as having 2 buffers, one that is an actual buffer on the sound card, and one that is a working buffer in main memory (secondary buffer). When we write things in our buffer (the secondary buffer) direct sound will at some point copy some of its values to the primary buffer that will actually be played on the sound card. The copy region will be between the play and write cursor. So if you write in that region, you don't know if where you write has already been copied to the sound card.
- Casey might be writing between the play and write cursor (we would have to compare the target cursor to the write cursor to know). It's not guaranteed to work. The amount of sample the play and write cursor move is less than the latency (write_cursor - play_cursor) so Casey is trying to write as close as possible as the play cursor (to minimize latency) even if it's before the write cursor; but he also writes enough sample to keep a continuous sound in case of frame drops which adds latency because he doesn't overwrite previously written samples.
- DirectSound is an old API, it's emulated on to of WASAPI
since Windows Vista. You can find a version of the handmade hero implemented directly in WASAPI in this thread: Day 19 - Audio Latency
2. Why use a sample index at all - shouldn't we instead start at the play/write cursor and fill the latency amount past there?
The sample index is the last value that was written. The write cursor might be before that point, meaning we wrote more samples in the previous frame than the amount of sample that the write cursor moved. So we still need the running sample index.
3. Again, why use the sample index - it doesn't track time. It arbitrarily tracks where we are in a sine wave, and we increment that value without considering how much time has passed.
A sample is a unit of time. If the buffer is sampled at 48000Hz, a sample is 1/48000 seconds. We don't use the sample index to track the sine wave, we use the tSine value (in day 9) which is incremented after writing each sample.
The following threads might contains additional information:
Day 20: The Tragedy of Cases
[Day 019] - Possible solution to the 3-frame latency
/* From day 009 */
// NOTE(casey): DirectSound output test
DWORD ByteToLock = ((SoundOutput.RunningSampleIndex*SoundOutput.BytesPerSample) %
DWORD TargetCursor =
// TODO(casey): Change this to using a lower latency offset from the playcursor
// when we actually start having sound effects.
if(ByteToLock > TargetCursor)
BytesToWrite = (SoundOutput.SecondaryBufferSize - ByteToLock);
BytesToWrite += TargetCursor;
BytesToWrite = TargetCursor - ByteToLock;
Win32FillSoundBuffer(&SoundOutput, ByteToLock, BytesToWrite);