Playing WAV and MIDI files Using DirectX Audio with Delphi

by Jan 27, 2004

DirectX Overview

Conducting a survey about what game programming
API used by most of game softwares running on PC, I am sure that, DirectX will be on
top list. Nowdays, DirectX had become a mature API for game programming
on Windows platform. It makes game developer's life easier. In old days of game
programming history, game applications were running in DOS. Game developers must code
for everything under the sun. From input hardwares such as mouse, keyboard,
joysticks to output hardwares such as graphics card and sound card. Every
hardwares have their own codes. But now DirectX come to the rescue. It makes game developers focus
on game programming rather than API programming.

In this article, I am going to explain you one of interesting part
of game programming. Sound programming. Without sound, games will be hollow, no moods and emotions developed during playing session.

DirectX has several components for sound programming ie
DirectSound and DirectMusic, but in version 8.0 those components
is combined into single component, ie DirectX Audio. I will explain
it for DirectX 8.0. For version 9.0 you should replace 8 suffix with 9
such IDirectMusicPerformance8 replace it with IDirectMusicPerformance9
and so on. For this article I use DirectX header conversion from Project
JEDI
. Download project source code accompany this article here.

Hold Your Breath and Dive In….

Ok enough overview stuff. Let's start with real stuff. There
are several steps to use DirectX Audio. It can be divided into three main
parts as following:

1. Initialization
2. Playing and stoping your sound
3. Finalization (shutdown)

1.Initialization Part

To start using DirectX Audio, first of all, we must initialize COM by
calling CoInitialize function. This function is declared in ActiveX
unit, so add this unit in your uses clause. This function has one
parameter, fill this parameter with nil upon calling CoInitialize. Once
it's done, we are ready to initialize DirectX Audio. Ok let's continue
with next step..

Initializing Performance

Performance object is a overall manager for music playback.
In one application, usually we only need one performance. To create and
initialize performance we use CoCreateInstance function which is
declared in ActiveX unit. For example


CoCreateInstance(CLSID_DirectMusicPerformance,
                 nil,
                 CLSCTX_INPROC,
                 IID_IDirectMusicPerformance8,
                 FPerformance);
      

Code above will initialize instance of IDirectMusicPerformance8
and store the address of instance in FPerformance. FPerformance
is variable of type IDirectMusicPerformance8. Class ID CLSID_DirectMusicPerformance
and interface ID IID_DirectMusicPerformance8 are both declared
in DirectMusic unit. Be sure to include this unit in your uses
clause. Second parameter is for COM aggregation which is not supported, we set it to nil. Constant CLSCTX_INPROC used here because DirectX is an inproc
COM server. We an check status of CoCreateInstance using Failed or Succeeded function.

After we initialize FPerformance, call InitAudio
function which is member function of IDirectMusicPerformance8. For example


if (FPerformance<>nil) then
begin
     FPerformance.InitAudio(nil,
                            nil,
                            Handle,
                            DMUS_APATH_SHARED_STEREOPLUSREVERB,
                            64,
                            DMUS_AUDIOF_ALL,
                            nilend

First and second parameter is instance of IDirectMusic and
IDirectSound respectively. we set it to nil because we want DirectX Audio
to create IDirectMusic and IDirectSound instance automatically for internal
use of performance. Third parameter is window handle to use for DirectSound
creation. This parameter can be 0 which indicates that foreground window
will be used. Fourth parameter is audiopath type, this value tells
DirectX Audio to create an ordinary music setup with stereo output and
reverb or 0 to use default audiopath. Fifth parameter is number of channel
to be used. Sixth parameter is flag that indicate which feature we want
to use. We want all feature so we set it DMUS_AUDIOF_ALL. Last
parameter is pointer to data structure to specifies parameter of synthesizer.
We want default so we set nil. Up to this point, initializing
performance is complete. Let's continue the journey...

Initializing Loader

Purpose of Loader object is to load DirectMusic object such as sound sample from file or resource before it can be incorporated with performance object. Application should maintain one loader object at a time and not free it until there are no more loading have to be done. Microsoft recommends that we create single global loader object to make object caching done efficiently.

To initialize loader object, we use the CoCreateInstance function. This function is declared in ActiveX unit.


CoCreateInstance(CLSID_DirectMusicLoader,
                 nil,
                 CLSCTX_INPROC,
                 IID_IDirectMusicLoader8,                            
                 FLoader);

You can see that initializing loader object is almost same with initializing performance object. What we need to set is only class ID and interface ID of IDirectMusicLoader8 and of course a variable that will hold pointer to IDirectMusicLoader8 interface.

Loading Sound Sample From File

We have performance. We have loader. Now we need something
to play right? Let's torture our sound card by loading some sounds from file so we can hear our PC
screaming out loud.

To load sound sample from file, we use LoadObjectFromFile function. This function is member of IDirectMusicLoader8. Following code is one of many examples how to do it.


FLoader.LoadObjectFromFile(CLSID_DirectMusicSegment,
                           IID_IDirectMusicSegment8,
                           wchFilename,
                           FSoundSegment);

LoadObjectFromFile will create sound segment and load sound sample stored in filename wchFilename into FSoundSegment. wchFilename type is PWideChar, so be sure to convert your filename if you use string type for filename. Because we want to store sound sample in sound segment, we set first and second parameter with class ID and interface ID of IDirectMusicSegment8.

Download Segment

To incorporated sound sample with performance so we can play it later, we must download
segment containing sound sample to performance. We use member of IDirectMusicSegment8 Download.

FSoundSegment.Download(FPerformance);

Creating Audiopath

Audiopath? Well, this new thing actually reponsible
for managing flow of sound data. When we create performance, DirecX Audio
also create default audiopath for us. If you only need to play one sound
sample at a time, then you can skip this part because you can play it
with default audiopath.

In game applications, we often have to play multiple sounds at a time, such as playing background music while playing explosion sound when game player pushes fire button. Using default audiopath only, will prevent us from playing those sounds simultaneously.

To be able to mix those sounds and play them simultaneously,
we need to create audiopath for each sound segment. To create audiopath,
DirectX Audio provides us with two functions to accomplish this task, i.e
CreateAudioPath and CreateStandardAudioPath. Both functions
is member of IDirectMusicPerformance8 interface but the difference is audiopath
created by first function will have its settings loaded from a configuration
while second one, the caller must define its settings. I prefer second one
because it is simpler.


FPerformance.CreateStandardAudioPath(DMUS_APATH_SHARED_STEREOPLUSREVERB,
                                     64,
                                     TRUE,
                                     FAudioPath);

The code above will create a stereo with reverb audiopath with 64 channels. We set third parameter to TRUE because we want to activate audiopath we just create. The last parameter is where we want to store pointer of IDirectMusicAudioPath8 interface. When we reach this point we are done with initialization part. Let's move to the most interesting part of this article...playing our sound.

2.Playing and stopping your sound

To play sound segment through default audiopath, we can use PlaySegment(). To play sound segment through our previously created audiopath, then we must use PlaySegmentEx(). The later function contains more parameters but it's more flexible. PlaySegment and PlaySegmentEx are both member of IDirectMusicPerformance8 interface.


FPerformance.PlaySegmentEx(FSoundSegment,
                           nil,
                           nil,
                           DMUS_SEGF_SECONDARY,
                           0,
                           FSegmentState,
                           nil,
                           FAudioPath);

First parameter is segment which contains sound
that we want to play. Second is segment name. We can set
it to nil because it's currently not supported. Third one is for transition
effect like fading from one sound to another sound. we set it to nil because
for game, we want it get played immediately. Fourth is segment flag that
we use. DMUS_SEGF_SECONDARY tells DirectX Audio to play our segment as
secondary segment. Using this flag will enable us to mix sound with other
sounds that currently playing. Fifth parameter is time to start play our
sound. We want play it immediately so we set it with zero value. FSegmentState
is variable of type IDirectMusicSegmentState that will recieve status
of segment currently played. Seventh parameter is audiopath or segment
state that will be stop when new segment start playing. We set it to nil
because we want other segments to be played as it was. The last parameter
is audiopath that we want to use. If we want to play using default audiopath
we set it to nil

If you can play you can stop. To stop sound segment from
playing call Stop or StopEx. StopEx is extension of Stop.
I will only explain 5topEx because it's more flexible. Both are member
functions of IDirectMusicPrformance8

FPerformance.StopEx(FSoundSegment,0,0);

First stuff on StopEx parameter list is segment to be stopped. Sceond and third tell DirectX Audio that we want to stop plyback immediately

Hey, finish with your stuff folks. Let's discuss how to properly shutdown DirectX Audio...

3.Finalization (shutdown)

Of course to properly shutdown, we must politely release
all interface instances we already created, otherwise DirectX Audio will
curse us with disaster called memory leak. We must pay attention for releasing
performance object. Before we release it we must call its CloseDown
function because we have made a call to its InitAudio function.

FPerformance.CloseDown;

Optionally we can unload previously downloaded segment before releasing segment by calling its Unload function. According to what Microsoft said CloseDown will also unload all downloaded segments that have not been unloaded, so I think it save to skip this unloading step.

Go to Surface and Get Some Air...

We have enough information. Let us now continue the journey
by creating wrapper class that will simplify playing sound with DirectX
Audio. Classes below are two classes that we will create.

1. TSoundManager class
2. TSoundSample class

1.TSoundManager class

We will encapsulate performance and loader into this class.
It will maintain one performance object, one loader and one list object
to sound samples. This class is responsible for setting up environment and
also for creating and destroying all instances of TSoundSample.
Following code are declaration of TSoundManager. I will not display detail
implementation of this class. You can grab source code here.
I have comment out the code as clear as I can so I hope the everybody
will understand.

type TSoundManager=class(TComponent)
       private
        FPerformanceObject: IDirectMusicPerformance8;
        FLoaderObject: IDirectMusicLoader8;
        FSampleList:TList;
        FHandle: HWND;
        procedure Shutdown;
        procedure ClearSampleList;
        function GetSoundSamples(const indx: integer): TSoundSample;
        function GetSampleCount:integer;
        procedure SetHandle(const Value: HWND);
        procedure CreatePerformance;
        procedure CreateLoader;
       public
        constructor Create(AOwner:TComponent);override;
        destructor Destroy;override;
        function AddSoundSample:TSoundSample;
        procedure InitSoundManager;
        property SoundSamples[const indx:integer]:TSoundSample read GetSoundSamples;
defaultpublished property PerformanceObject:IDirectMusicPerformance8 read FPerformanceObject; property LoaderObject:IDirectMusicLoader8 read FLoaderObject; property SampleCount:integer read GetSampleCount; property Handle:HWND read FHandle write SetHandle; end

2.TSoundSample class

This class is responsible for playback and loading sound. We will then give it method for playing and stop sound and loading sound from file. We also give this class property called Loop which is boolean type for playing sound infinite loop or single loop.
If Loop is TRUE then sound will be played continuesly which is very suitable for playing background music. Following declaration is declaration of TSoundSample.

type TSoundSample=class(TComponent)
     private
       FLoop: boolean;
       FManager: TSoundManager;
       FSoundSegment:IDirectMusicSegment8;
       FAudioPath:IDirectMusicAudioPath8;
       FSampleIndex: integer;
       procedure SetLoop(const Value: boolean);
       procedure SetManager(const Value: TSoundManager);
       procedure SetSampleIndex(const Value: integer);
     public
       constructor Create(AOwner:TComponent);overridedestructor Destroy;overrideprocedure Play;
       procedure Stop;
       procedure LoadFromFile(const filename:stringpublished
       property Segment:IDirectMusicSegment8 read FSoundSegment;
       property Loop:boolean read FLoop write SetLoop;
       property Manager:TSoundManager read FManager write SetManager;
       property SampleIndex:integer read FSampleIndex write SetSampleIndex;
     end

Conclusion

In this article, we have discussed how to setup performance
object, loader object. We also discussed how to load and setup segment
and play it simultaneously with other sound. As you can see it's all very
easy, though it might seem complicated at first time. Hope that this article
help you and not make you scarry with game programming. See you on next
topic.

Article originally contributed by Zamrony P Juhara