XAudio2 Tutorial 7

Author: Jay Tennant

A Brief Look at XAudio2: Using Filters

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 filters on voices. This tutorial will produce output that sounds like it's playing through a telephone speaker, and being obstructed by a wall (in the "Things to Try" section).

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 wave file is available here and at the end. So here is the code:

//by Jay Tennant 3/28/12
//A Brief Look at XAudio2: Using Filters
//demonstrates different band filters
//win32developer.com
//this code provided free, as in public domain; score!

//to get the XAudio2CutoffFrequencyToRadians function, etc.
#define XAUDIO2_HELPER_FUNCTIONS

#include <windows.h>
#include <tchar.h>
#include <xaudio2.h>
#include "staticWave.h"

//the filter settings
XAUDIO2_FILTER_PARAMETERS g_filter;

//XAudio2 objects
IXAudio2* g_engine = NULL;
IXAudio2MasteringVoice* g_master = NULL;
IXAudio2SourceVoice* g_source = NULL;

//custom window creation
void createCustomWindow();

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd )
{
	//required by XAudio2
	CoInitializeEx( NULL, COINIT_MULTITHREADED );

	//create the engine
	if( FAILED( XAudio2Create( &g_engine ) ) )
	{
		CoUninitialize();
		return -1;
	}

	//create the mastering voice
	if( FAILED( g_engine->CreateMasteringVoice( &g_master ) ) )
	{
		g_engine->Release();
		CoUninitialize();
		return -2;
	}

	//load a sound effect
	StaticWave sfx;
	if( !sfx.load( TEXT("thisisatest.wav") ) )
	{
		g_engine->Release();
		CoUninitialize();
		return -3;
	}

	//create source voice
	if( FAILED( g_engine->CreateSourceVoice( &g_source, sfx.wf(), XAUDIO2_VOICE_USEFILTER ) ) )
	{
		g_engine->Release();
		CoUninitialize();
		return -4;
	}

	//start consuming audio
	g_source->Start();

	//create the custom window
	createCustomWindow();

	//main message loop
	XAUDIO2_VOICE_STATE voiceState;
	MSG msg;
	bool quitting = false;
	while( !quitting )
	{
		if( PeekMessage( &msg, 0, 0, 0, PM_REMOVE ) )
		{
			TranslateMessage( &msg );
			DispatchMessage( &msg );
			if( msg.message == WM_QUIT )
				quitting = true;
		}

		g_source->GetState( &voiceState );
		if( voiceState.BuffersQueued < 1 )
			g_source->SubmitSourceBuffer( sfx.buffer() );
	}

	//flush source buffer
	g_source->Stop();
	g_source->FlushSourceBuffers();

	//release the engine, cleanup
	g_engine->Release();
	CoUninitialize();

	return 0;
}

#define ID_BUTTON_APPLY 101

//custom window procedure
LRESULT CALLBACK customWindowProc( HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam )
{
	static HWND m_hCutoffFrequency = NULL, m_hQValue = NULL, m_hUseFilter = NULL;
	static unsigned int m_maxSampleRate = 0;

	switch( Msg )
	{
	case WM_CREATE:
		{
			//create the controls
			CreateWindowEx( 0, TEXT("STATIC"), TEXT("Band (in Hz):"), WS_CHILD | WS_VISIBLE | SS_SIMPLE, 10, 10, 150, 20, hWnd, NULL, GetModuleHandle(NULL), NULL );
			m_hCutoffFrequency = CreateWindowEx( 0, TEXT("EDIT"), TEXT("2200"), WS_CHILD | WS_VISIBLE | ES_NUMBER, 10, 30, 50, 20, hWnd, NULL, GetModuleHandle(NULL), NULL );
			CreateWindowEx( 0, TEXT("STATIC"), TEXT("Q Value (0.667 < Q):"), WS_CHILD | WS_VISIBLE | SS_SIMPLE, 10, 50, 150, 20, hWnd, NULL, GetModuleHandle(NULL), NULL );
			m_hQValue = CreateWindowEx( 0, TEXT("EDIT"), TEXT("5.5"), WS_CHILD | WS_VISIBLE, 10, 70, 50, 20, hWnd, NULL, GetModuleHandle(NULL), NULL );
			m_hUseFilter = CreateWindowEx( 0, TEXT("BUTTON"), TEXT("Use Filter"), WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX, 10, 100, 180, 30, hWnd, NULL, GetModuleHandle(NULL), NULL );
			CreateWindowEx( 0, TEXT("BUTTON"), TEXT("Apply"), WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON, 10, 140, 180, 40, hWnd, (HMENU)ID_BUTTON_APPLY, GetModuleHandle(NULL), NULL );

			XAUDIO2_VOICE_DETAILS voiceDetails;
			g_source->GetVoiceDetails( &voiceDetails );
			m_maxSampleRate = voiceDetails.InputSampleRate;
		} break;
	case WM_DESTROY:
		PostQuitMessage(0);
		break;
	case WM_COMMAND:
		switch( LOWORD( wParam ) )
		{
		case ID_BUTTON_APPLY:
			if( HIWORD( wParam ) == BN_CLICKED )
			{

				if( BST_CHECKED == SendMessage( m_hUseFilter, BM_GETCHECK, 0, 0 ) )
				{
					//identify which filter will be used
					g_filter.Type = BandPassFilter;

					TCHAR buffer[16] = TEXT("");
					unsigned int band = 0;
					float q = 0.0f;

					//read value in edit control
					SendMessage( m_hCutoffFrequency, WM_GETTEXT, sizeof(buffer), (LPARAM)buffer );
					band = _ttoi( buffer );

					//apply that value
					g_filter.Frequency = XAudio2CutoffFrequencyToRadians( band, m_maxSampleRate );

					//reset that value in the edit box
					band = (unsigned int)XAudio2RadiansToCutoffFrequency( g_filter.Frequency, m_maxSampleRate );
					memset( buffer, 0, sizeof(buffer) );
					_itot_s( band, buffer, 10 );
					SendMessage( m_hCutoffFrequency, WM_SETTEXT, -1, (LPARAM)buffer );

					//read value in other edit control
					SendMessage( m_hQValue, WM_GETTEXT, sizeof(buffer), (LPARAM)buffer );
					q = _tstof( buffer );
					q = max( q, 0.667f );

					//apply that value
					g_filter.OneOverQ = 1.0f / q;

					//reset that value in the edit box
					memset( buffer, 0, sizeof(buffer) );
					_stprintf_s( buffer, TEXT("%.3f"), q );
					SendMessage( m_hQValue, WM_SETTEXT, -1, (LPARAM)buffer );
				}
				else
				{
					//acoustically equivalent to not using a filter
					g_filter.Type = LowPassFilter;
					g_filter.Frequency = 1.0f;
					g_filter.OneOverQ = 1.0f;
				}

				//apply the filter to the voice
				g_source->SetFilterParameters( &g_filter );
			}
			break;
		}
		break;
	default:
		return DefWindowProc( hWnd, Msg, wParam, lParam );
	}
	return 0;
}

//create our custom window procedure
void createCustomWindow()
{
	WNDCLASSEX wc = {0};
	wc.cbSize = sizeof(wc);
	wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
	wc.hCursor = LoadCursor( NULL, IDC_ARROW );
	wc.hIcon = LoadIcon( NULL, IDI_APPLICATION );
	wc.hIconSm = LoadIcon( NULL, IDI_APPLICATION );
	wc.hInstance = (HINSTANCE)GetModuleHandle( NULL );
	wc.lpfnWndProc = (WNDPROC)customWindowProc;
	wc.lpszClassName = TEXT("test");
	wc.style = CS_HREDRAW | CS_VREDRAW;

	ATOM atom = RegisterClassEx( &wc );
	if( !atom )
		return;

	HWND hWnd = CreateWindowEx( 0, (LPCTSTR)atom, TEXT("ABLAX: Using Filters"), WS_OVERLAPPEDWINDOW, 50, 50, 220, 240, NULL, NULL, wc.hInstance, NULL );

	if( hWnd == 0 || hWnd == INVALID_HANDLE_VALUE )
		return;

	ShowWindow( hWnd, SW_NORMAL );
	UpdateWindow( hWnd );
}

The helper staticWave.h and waveInfo.h headers available in the last tutorial. When you run this sample, check the box to "Use" the filter, then click the "Apply" button. You can apply changes to the filter parameters as its running the voice.

Telephone

On running the demo, the sound has a filtered quality as if its being played through a telephone speaker. This is achieved using a "band pass" filter. There are 4 different filters available: "low pass", "band pass", "high pass", and "notch". What does each of those mean?

Briefly, each filter allows us to specify a "cutoff frequency", and an attenuation factor. For a "low pass" filter, think of it as allowing all frequencies lower than the cutoff frequency to continue. Anything above that cutoff is attenuated to silence.

A "high pass" filter will allow frequencies higher than the cutoff frequency to continue unchanged, and those that are lower will be attenuated to silence.

The "band pass" filter works by specifying a band--or a particular frequency--that will be the only frequency that is unchanged. All frequencies above and below it will be attenuated to silence.

Finally, a "notch" filter is the opposite of a "band pass" filter: the band that is chosen will be silence, and the surrounding frequencies will attenuate to full volume.

And now to analyze the demo:

#define XAUDIO2_HELPER_FUNCTIONS

To be able to use helpful conversion functions, we must define this value before including xaudio2.h. The two conversion functions we use have to do with converting between radian frequency and the cutoff frequency, which are declared as:

float XAudio2RadiansToCutoffFrequency(
         float Radians,
         float SampleRate
)
float XAudio2CutoffFrequencyToRadians(
         float CutoffFrequency,
         UINT32 SampleRate
)

The first of those two functions is used in the demo to set the edit control's text appropriately, in case an invalid value was entered. The second runs an equation to convert the desired cutoff frequency to the radian frequency, which XAudio2 uses. The conversion equation looks like:

2 * sin(pi * (desired filter cutoff frequency) / sampleRate)

IMPORTANT! There is a maximum cutoff frequency, which is the source voice's sample rate divided by 6. If we enter a value larger than that into the helper function, it will clamp the result from 0.0 to 1.0 without complaining.

g_engine->CreateSourceVoice( &g_source, sfx.wf(), XAUDIO2_VOICE_USEFILTER )

To use a filter parameter on a source voice to affect all output, we must specify it at creation as one of the creation flags. We can modify the filter through the IXAudio2Voice::SetFilterParameters() call.

IMPORTANT! There is a very similar flag, XAUDIO2_SEND_USEFILTER, but that is used only when setting the output voices in a XAUDIO2_VOICE_SENDS chain. So don't use that flag here!

g_source->SetFilterParameters( &g_filter );

This point in the window procedure is where the filter parameters are set. The XAUDIO2_FILTER_PARAMETERS structure is defined as:

typedef struct XAUDIO2_FILTER_PARAMETERS {
    XAUDIO2_FILTER_TYPE Type;
    float Frequency;
    float OneOverQ;
} XAUDIO2_FILTER_PARAMETERS;

The Type member can only be "LowPassFilter", "HighPassFilter", "BandPassFilter", or "NotchFilter". The Frequency is the radian frequency, which must be between 0.0 and 1.0. And the attenuation factor (also known as "Q") is stored as its reciprocal in the structure as OneOverQ.

g_filter.Type = LowPassFilter;
g_filter.Frequency = 1.0f;
g_filter.OneOverQ = 1.0f;

Intuitively, by setting the filter type to be a "low pass" filter, and by setting the cutoff frequency (in radian frequency) to be the highest frequency, and the attenuation to be 1, the effect acoustically is that the filter is being bypassed, even though it is not. Thus, it will sound as if the filter has been "turned off".

And that wraps it up!

Things to Try

Set the filter to sound as if the source is playing through drywall. (Band 250Hz, Q 1.2)
Experiment with other Band and Q values
Experiment with other Filter types

Additional Information

Demo wave



Next tutorial

Tutorial 8 - XAudio2: Adjusting the frequency ratio