Reasoning about the ring buffer in XAudio2

Hello Handmade community!

I have a bit of a problem with my understanding of the ring buffer. I saw the episodes that implemented DirectSound, and wanted to implement XAudio2 myself as an exercise and made it work. I think the wrong thing I did is I wrote the sine wave for the whole buffer every frame, not taking into account where the play cursor is, which in XAudio2 is how many running samples are processed, to the best of my knowledge.

So, I started to work on ideas for how the ring buffer might work here.

The basic algorithm is:

  1. Figure out how many running samples are processed
  2. Set the write region ahead of the play cursor by a few audio frames (4 bytes)
  3. Region 1 Frames = Buffer Frame Count - Write Buffer Offset
  4. Region 2 Frames = Play Buffer Offset
  5. Write region 1 from the write cursor till the end of buffer
  6. Write region 2 from the beginning of the buffer till the play cursor

And this should intentionally leave out the samples between the play and write cursors.

EDIT: I though I should include other data to be helpful Buffer size: 48000 samples / second * 2 channels * 2 bytes * 2 seconds = 384000 bytes

1 Audio frame = 4 bytes. 384000 / 4 = 96000 Audio Frames, in total

I tried a lot of revisions on the code, and boiled it down to this simplistic version to see what is wrong:

		XAUDIO2_VOICE_STATE XAudio2_Voice_State = {};
		XAudio2_Source_Voice->GetState(&XAudio2_Voice_State, 0);
		u64 Audio_Buffer_Frames = Game_Sound.AudioSizeInBytes / 4;
		u64 Play_Cursor_Position = XAudio2_Voice_State.SamplesPlayed % Audio_Buffer_Frames;
		u64 Write_Cursor_Position = (XAudio2_Voice_State.SamplesPlayed + 24000) % Audio_Buffer_Frames;
		u64 Region_1_Frames = Audio_Buffer_Frames - Write_Cursor_Position;
		u64 Region_2_Frames = Play_Cursor_Position;
		Game_Sound.AudioAt = Game_Sound.AudioBegin + (Write_Cursor_Position * 4);

		static f64 Sine_Phase = 0.0f;
		f32 Tone_Hz = 220.0f;
		u32 Tone_Volume = 3000;
		f32 Samples_Per_Cycle = (f32)Game_Sound.AudioSamplingRate / Tone_Hz;

		if(Region_1_Frames > 0 && Play_Cursor_Position > 0)
		{
			u32 *Audio_Sample = (u32*)Game_Sound.AudioAt;
			for(u32 Frame_Index = 0; Frame_Index < Region_1_Frames; ++Frame_Index)
			{	
				Sine_Phase += ((2.0f * Pi) / Samples_Per_Cycle);
				i16 Sample = (i16)(sinf(Sine_Phase) * Tone_Volume);
				*Audio_Sample++ = Sample + (Sample << 16);
			}
		}
		if(Region_2_Frames > 0 && Play_Cursor_Position > 0)
		{
			u32 *Audio_Sample = (u32*)Game_Sound.AudioBegin;
			for(u32 Frame_Index = 0; Frame_Index < Region_2_Frames; ++Frame_Index)
			{	
				Sine_Phase += ((2.0f * Pi) / Samples_Per_Cycle);
				i16 Sample = (i16)(sinf(Sine_Phase) * Tone_Volume);
				*Audio_Sample++ = Sample + (Sample << 16);
			}
		}

I am still trying to reason about it with pen & paper, think about what could be a better solution. But I believe I've hit the ceiling of my experience here. Help would be much appreciated.

If you want to manage audio buffer & submission on lower level I would suggest to use WASAPI instead. It is way simpler and more straight forward to implement it there than with Xaudio2 abstractions (which is layer on top of WASAPI anyway).

I have example of WASAPI emulating DirectSound play/write cursors here:
https://gist.github.com/mmozeiko/5a5b168e61aff4c1eaec0381da62808f#file-win32_wasapi-h
See example.c code above it for how to use it.


Edited by Mārtiņš Možeiko on

That is one thing I had in mind. I was afraid after reading XAudio2's documentation that it is a fully virualised API, and that when I thought I was writing into the sound buffer, I was writing the data I want and the SubmitSourceBuffer call handles things behind the scenes at an extra layer. Thank you so much for that link, I definitely have to take a look.


Replying to mmozeiko (#30615)