XAudio2 Tutorial 4

Author: Jay Tennant

A Brief Look at XAudio2: Events and Asynchronous I/O (Part 2/2)

XAudio2 is a sound API available on the Windows Vista/7+ and XBox 360 platforms. This tutorial aims at demonstrating in brevity how to use Event objects in conjunction with asynchronous I/O in a multithreaded application. The last article focused on an introduction to Event objects. This part focuses on using them with Asynchronous I/O. This discussion is a prerequisite to the tutorial on streaming audio from disk.

The target audience should be at least intermediate level C++ programming. Moderate familiarity with Win32 programming is required.

In this series, we use the rule: code first, ask questions later. The demo text file "Princess of Mars" is available here. So here is the code:

//by Jay Tennant 3/7/12
//A Brief Look at XAudio2: Events and Asynchronous I/O (Part 2/2)
//demonstrates using Events with Asynchronous I/O
//win32developer.com
//this code provided free, as in public domain; score!

#include <windows.h>

//a function that does something while the async operation is pending completion
void DoSomething();

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd )
{
	//get the sector size for the disk
	DWORD dwSectorSize = 0;
	{
		DWORD dwDontCare1, dwDontCare2, dwDontCare3;
		GetDiskFreeSpace( TEXT("C:\\"), &dwDontCare1, &dwSectorSize, &dwDontCare2, &dwDontCare3 );
	}

	//load the file for async I/O
	HANDLE hFile = CreateFile( TEXT("62.txt"), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED | FILE_FLAG_NO_BUFFERING, NULL );

	if( hFile == INVALID_HANDLE_VALUE )
		return -1;

	//create an overlapped struct
	OVERLAPPED overlapped = {0};
	overlapped.hEvent = CreateEvent( NULL, FALSE, FALSE, NULL ); //event is auto-reset

	//create the buffer that receives the content from the file
	char *buffer = (char*)_aligned_malloc( dwSectorSize + 1, dwSectorSize );
	if( buffer == NULL )
	{
		CloseHandle( hFile );
		CloseHandle( overlapped.hEvent );

		return -2;
	}

	//main loop
	while( ( MessageBox( NULL, TEXT("Do you want to load some text?"), TEXT("Main Thread"), MB_YESNO ) == IDYES ) )
	{
		//reset the buffer to be empty
		memset( buffer, 0, dwSectorSize + 1 );

		//load more content from the file asynchronously
		ReadFile( hFile, buffer, dwSectorSize, NULL, &overlapped );

		DWORD dwBytesRead = 0;
		int i = 0;
		bool looping = true;
		while( looping )
		{
			//test for the async I/O to be completed
			if( GetOverlappedResult( hFile, &overlapped, &dwBytesRead, FALSE ) == FALSE )
			{
				switch( GetLastError() )
				{
				case ERROR_IO_INCOMPLETE: //async I/O still pending
					DoSomething();
					i++;
					break;
				case ERROR_HANDLE_EOF: //eof reached
					if( dwBytesRead == 0 )
						strcpy( buffer, "End of File" );
					looping = false;
					break;
				default: //something else is wrong
					looping = false;
				}
			}
			else
				looping = false;
		}

		//update the file pointer position
		overlapped.Offset += dwBytesRead;

		//display the retrieved text
		MessageBoxA( NULL, buffer, "Retrieved text", MB_OK );

		//display the number of times DoSomething was called
		char counts[32] = "";
		MessageBoxA( NULL, _itoa( i, counts, 10 ), "Number of times DoSomething() was called", MB_OK );
	}

	//free the buffer
	_aligned_free( buffer );

	//close the event handle
	CloseHandle( overlapped.hEvent );

	//close the file handle
	CloseHandle( hFile );

	return 0;
}

void DoSomething()
{
}

Amazinc! Do You Sync So? Async So!

The comments in the code explain everything pretty well, so I'll be brief on the analysis.

void DoSomething();

DoSomething is a stub where you can run some additional code while the async operation has not completed.

GetDiskFreeSpace( TEXT("C:\\"), &dwDontCare1, &dwSectorSize, &dwDontCare2, &dwDontCare3 );

This function gets the size in bytes of each sector on the disk. Because we will be reading directly from the disk, and not allowing the system to cache the file, we must read and write to disk on sector boundaries. The only parameters we care about are the first and third. If you really want to get the size of the disk and the available free space, you should use GetDiskFreeSpaceEx instead, unless you're certain the drive is less than 2GB.

HANDLE hFile = CreateFile( TEXT("62.txt"), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED | FILE_FLAG_NO_BUFFERING, NULL );

This opens the file for read access. For XAudio2, we'll be using the same exact parameters (save the first one) for all our operations. Information is available on MSDN about this function.

IMPORTANT! Because FILE_FLAG_NO_BUFFERING is used in the CreateFile function, we are instructing the system to avoid cacheing the data. In doing so, we are accessing the disk itself, and so must abide by certain restrictions, including reading amounts of data and offsets of data in multiples of the disk sector size. Remember, we calculated that size previously. DO NOT attempt to read amounts or offsets of any measure that is not some multiple of the disk sector size, or the GetOverlappedResult call will hang indefinitely.

OVERLAPPED overlapped = {0};
overlapped.hEvent = CreateEvent( NULL, FALSE, FALSE, NULL ); //event is auto-reset

The OVERLAPPED structure is defined as:

typedef struct _OVERLAPPED {
  ULONG_PTR Internal;
  ULONG_PTR InternalHigh;
  union {
    struct {
      DWORD Offset;
      DWORD OffsetHigh;
    } ;
    PVOID Pointer;
  } ;
  HANDLE    hEvent;
}OVERLAPPED, *LPOVERLAPPED;

The only members we're interested in are hEvent and Offset. The hEvent is the event object we create that is signaled when the asynchronous read operation finishes. The Offset member allows us to specify where in the file we should begin reading. All other members can be set to 0.

char *buffer = (char*)_aligned_malloc( dwSectorSize + 1, dwSectorSize );

_aligned_malloc() allocates some data that is aligned on a byte boundary that is specified in the second parameter. Because we are reading directly from the disk (see CreateFile note), we must allocate the buffer to be sector-aligned.

ReadFile( hFile, buffer, dwSectorSize, NULL, &overlapped );

This reads the file, filling the specified buffer. We send it the OVERLAPPED structure instance, which will reset the hEvent member. The file is read asynchronously from this call, so we cannot guarantee anything inside the buffer yet. We must wait for the next function to complete successfully:

if( GetOverlappedResult( hFile, &overlapped, &dwBytesRead, FALSE ) == FALSE )

This function checks whether the asynchronous operation has finished. The function is defined as:

BOOL WINAPI GetOverlappedResult(
  __in   HANDLE hFile,
  __in   LPOVERLAPPED lpOverlapped,
  __out  LPDWORD lpNumberOfBytesTransferred,
  __in   BOOL bWait
);

Notably, if we set the last parameter to TRUE, this function would block until the operation completed. (Doesn't make a lot of sense for asynchronous operations, but oh well.) So we set the last parameter to FALSE.

When GetOverlappedResult returns FALSE, it also posts an error code, which can be retrieved through GetLastError():

switch( GetLastError() )
{
case ERROR_IO_INCOMPLETE: //async I/O still pending
	DoSomething();
	i++;
	break;
case ERROR_HANDLE_EOF: //eof reached
	if( dwBytesRead == 0 )
		strcpy( buffer, "End of File" );
	looping = false;
	break;
default: //something else is wrong
	looping = false;
}

IMPORTANT! GetOverlappedResult will return FALSE even if it completes the operation if it reaches the end-of-file. So it's important to catch the EOF error and react appropriately.

overlapped.Offset += dwBytesRead;

We're updating the offset of the file write pointer by the number of bytes successfully read in. Pretty straight forward.

_aligned_free( buffer );

The buffer that was allocated with _aligned_malloc() must be released with the corresponding _aligned_free().

//close the event handle
CloseHandle( overlapped.hEvent );

//close the file handle
CloseHandle( hFile );

And whatever handles we create, must be closed. And that about wraps it up!

Well, almost. A final note about ReadFile. Though we are achieving asynchronous file I/O reads, it is possible that ReadFile() can block for several milliseconds to read the file table before starting the asynchronous operation. Neither XAudio2 nor the rest of the engine should be penalized by that, so in the XAudio2 tutorial on streaming sound, we will actually be using synchonous file reads in a separate thread. It's technically the same thing, only the ReadFile will be encapsulated in the multithreaded operation.

Don't worry, what you learned was not for naught! We still depend on events and the OVERLAPPED structure. We just won't be using GetOverlappedResult.

Things to Try

Load a different text file to get to the end of file sooner. Return the file pointer to the beginning when you reach the end.
Actually Do Something in the DoSomething() stub.
Read a book from Project Gutenberg, linked below. Maybe that "Princess of Mars" book.
Move on to the next tutorial, where you'll actually use this prerequisite with XAudio2!

Additional Information:

Demo text file: Princess of Mars

In accord with the licensing on redistributing the demo text file "Princess of Mars" by Edgar Rice Burroughs, the file was named 62.txt in the zip file 62.zip, and the following paragraph is required:

This eBook is for the use of anyone anywhere at no cost and with
almost no restrictions whatsoever.  You may copy it, give it away or
re-use it under the terms of the Project Gutenberg License included
with this eBook or online at www.gutenberg.net
Free ebooks from Project Gutenberg: http://www.gutenberg.org
MSDN entry on Synchronization and Overlapped I/O: http://msdn.microsoft.com/en-us/library/ms686358(VS.85).aspx
MSDN entry on File Buffering, and FILE_FLAG_NO_BUFFERING:http://msdn.microsoft.com/en-us/library/windows/desktop/cc644950(v=vs.85).aspx


Next tutorial

Tutorial 5 - XAudio2: Streaming a Wave from Disk