Fundamentals of Audio and Video Programming for Games (Pro-Developer)
When you open up this project in Microsoft Visual Studio .NET, you will see that there are five source files:
-
DSound3DLayer.cpp -
Extended_dsutil.cpp -
RumpusEnvironment.cpp -
RumpusSFX.cpp -
RumpusSFXparam.cpp
There is also one header file in addition to the resource header file,
The organization of the code is very similar to the code for the Cacophony tool.
The RumpusSFX.h File
The
class cSoundEffect { private: char filename[MAX_PATH]; int setting[max_settings]; int status; DWORD tickLength; DWORD ticksElapsed; int iSound; int iBuffer; D3DVECTOR position; D3DVECTOR velocity; D3DVECTOR facing; CSoundFXData* SoundFXData;
The header file contains defines for all 28 settings, one for each of the values that can be set by the UI. The final data member in this class, SoundFXData , is a pointer to an object containing all the special effects parameters, so we will discuss it in the next chapter. The three members that we need to explain now are the three vectors: position, velocity and facing.
The D3DVECTOR type is defined both in d3dtypes.h and dsound .h, and is a small but useful structure.
typedef struct _D3DVECTOR { float x; float y; float z; } D3DVECTOR;
The three vectors obviously store the position, velocity and orientation of objects on the grid. It is easy to see why position is held as a vector. Velocities are also defined as vectors, with the values identifying the velocity in the x, y and z direction. The orientation vector similarly defines the orientation of the object in the x, y, and z directions. The advantage of holding these values all as vectors is that it becomes easy to work with all of them in a similar way. In the case of orientation, DirectSound normalizes the vector, so that all three values total either 1.0 or -1.0. This normalization is done for you, but it does mean that if you retrieve the values that are being used, they may not be identical to the ones that you set.
You ll need 9th-grade trigonometry and geometry to program in Direct3D, so it s back to school for some developers. To examine how vectors can be used, examine the following code for the setConeFacing method.
Void setConeFacing() { switch( setting[cone_facing] ) { case 0: facing.x = 0.0f; facing.y = 0.0f; facing.z = -1.0f; break; case 1: facing.x = velocity.x; facing.y = velocity.y; facing.z = velocity.z; break; case 2: facing.x = -1.0f * velocity.x; facing.y = -1.0f * velocity.y; facing.z = -1.0f * velocity.z; break; } }
The value of setting[cone_facing] can be 0 (facing down the grid), 1 (facing forwards) or 2 (facing backwards ). It is fairly easy to add other orientations to this list. For example, if you wanted to add an option to direct the sound cone 90 degrees out to the right or left side of the object (at the same height of the object), we could add the two options shown in the following code.
case right_hand_side: facing.x = velocity.z; facing.y = velocity.y; facing.z = -1.0 * velocity.x; break; case left_hand_side: facing.x = -1.0 * velocity.z; facing.y = velocity.y; facing.z = velocity.x; break;
If you want to do more complicated rotations than 90 degrees, you will have to use sine and cosine functions, however at 90 degrees, these conveniently turn out to be 1 and 0.
The setInitialPositionAndVelocity method calls the setConeFacing method after initializing the position according to the values in the setting array. Velocity is converted from meters per second to meters per tick by dividing the velocity by the number of ticks per second.
The setInitialPosition and setInitialVelocity methods take a vector as an input parameter and are used only when a random sound is being initiated by another sound, so in this case, the random sound simply inherits the vectors of the originating sound.
The next method to look at in the cSoundEffect class is updatePosition . This simply provides a reasonable bounce when the sound object strikes the edges of the grid. The object is rotated about the y-axis (so the y-coordinate is left untouched) by one radian until it is safely back on the grid again. A radian, for those of you whose memory of 9th-grade math has faded, is a measurement of an angle such that 2 * pi radians equals 360 degrees. This makes one radian equal to 57.2957795 degrees, approximately. The advantage of radians is that they make the calculations involving pi much easier, so are often used as the low-level method for storing angles, as is the case with DirectX. However, they are heinously unintuitive for humans to handle, so it is often safer on your sanity to deal with degrees or compass points when dealing with orientations, and use appropriate conversion functions when passing the angles down into an SDK. In this case, use the Windows SDK functions available in the math.h header file.
Most of the other methods in the cSoundEffect class simply set or retrieve data, except for the writeEffect and readEffect methods, that save and load the settings for an effect, respectively. The same format is used as for the Cacophony tool (setting number followed by value, terminated in 999), except that the special-effects parameter settings are also written out.
Now, look at the cOneSound class in the following code.
class cOneSound { private: char filename[MAX_PATH]; int nBuffers; bool loaded; bool ambient;
This is a very similar class to the one of the same name in the Cacophony tool, and it has the same purpose: one object of this class is created for each wave file that is required. The new data member is the ambient Boolean, which is simply set to true if the sound is to be played everywhere at equal volume.
The methods in this class are all simple get and set methods.
These two classes are used extensively in the UI layer of the Rumpus tool, all of which is in the
The RumpusSFX.cpp File
This source file starts with a number of global variables and tables, which we will describe later in this chapter. The first function to examine is the WinMain function that starts the application. The two lines added to the WinMain function from our previous tools (High5 and Cacophony) contain the calls CoInitialize and CoUninitialize . These two calls are only necessary if you want to enable the special effects that we address in the next chapter; for 3-D sound without special effects there is no need to call them, so they have no effect on anything discussed in this chapter. Although the calls themselves initialize and terminate much of the Component Object Model (COM), it is not necessary to know the inner workings of COM, only the situations where you need to make these calls.
Following the WinMain function, there is a section on functions supporting the Add or Edit Sound dialog box.
The first method, ambientSoundSettings , simply turns on or off a whole range of the features of the dialog box. An ambient sound does not require any 3-D settings, for obvious reasons.
The retrieveSound function is called when the dialog box opens up, to populate all the settings with those held in the array of soundEffect objects. If no sound is already loaded, then the sound name will be empty and all the settings will be at their default values.
The OnInitSoundDialog function does the usual initialization of a dialog box, sets the ranges for all the sliders, and initializes the special-effects edit box. It then either calls retrieveSound, or sets the appropriate settings for the listener, depending on whether you are editing a sound or the listener s settings. Remember that the listener is treated very similarly to a sound effect in the UI, each being given a position and velocity. Note also in this case that the words The Listener are entered in the file name edit box.
The OnSoundSliderChanged function is called when any of the position or velocity sliders are moved. The default range of a slider is zero to 100 (zero being at the left or top of a slider), but to make the UI slightly more intuitive, the code reverses this behavior for y and z positions and velocities. (Since these sliders are vertical, it seems more intuitive to have zero at the bottom of the slider, rather than at the top.) The range of the position sliders is left at 100, but the range of the velocities is set from -25 to 25.
The ShowSoundFileDialog function contains standard code for showing the Open File dialog box, and gives options to browse for and click on a file.
The SoundDlgProc function is the callback for the Add or Edit Sound dialog box. When the user sets variables, they are all stored in the temporary array gTempSettings , and not directly into the soundEffect array, simply to ensure that if the user cancels out of the dialog box, then nothing changes.
The next section of this source file deals with the functions supporting the tracking of sounds on the grid, and starts with the definition and declaration of the structure shown in the following code.
struct soundTrackerStruct { int life; // The life of the pixels in ticks. long px; // The x-coordinate on grid. long pz; // The z-coordinate on grid. int color; bool moving; }; struct soundTrackerStruct soundTracker[max_tracks];
We need to color one pixel on the grid for every entry in the soundTracker array. The define max_tracks sets the size of this array at 1650, which is simply a number large enough to cover ten sounds plus one listener, with an average of ten pixels per shape and a lifespan of 15 ticks (so, 11 x 10 x 15 = 1650). It does not matter if the occasional pixel is missed, as the grid is constantly being updated, and since we re aiming for artistic effect rather than technical merit here.
The numberShape structure holds bit patterns for the numbers 1 through 10, and a circle representing the listener. If the sound is located at point x, z, then the number 1 is drawn by coloring the pixels x-1, z-3, then x, z-3, and so on. A clunky but effective way that helps represent the sounds in the table with moving points on the grid.
The first function, cleanPoint , sets any pixel back to its original light or dark green. Because we know that the RGB value for light green is 0x0000FF00 and the value for dark green is 0x00008000, we can hard-code these colors to avoid any complexity in having to save overwritten values.
The findSoundTracker function searches the entire array looking for an empty slot to insert a pixel entry, or returns the slot number if there already is an entry with the same x- and z-coordinates. This function is used by the newSoundTrackPoint function, which takes as input grid coordinates, the shape to draw, and a Boolean indicating whether the shape is moving or not.
The first thing that the newSoundTrackPoint function does is convert 3-D world coordinates to 2-D grid coordinates. Next, the color is set (hard-coded RGB values again, for white, red and black).
The main processing of this function takes each point required by the numberShape table, finds a slot in the soundTracker table for it to go into, and then populates the members of that slot with the color, coordinates, moving flag and lifespan of the pixel. The maximum life of a pixel has been set to 15 ticks.
The drawSoundTracks function methodically goes through the entire soundTracker table, setting the pixels on the grid to the appropriate color and decrementing their lifespan. When the life is reduced to 1, the pixel is cleared and the life member is set to 0 so that the slot can be recycled.
The cleanGrid function is called to initialize the whole process. It sets the color for each pixel in the entire grid, and then initializes the soundTracker structure by setting all the life members to 0.
The third section of this source file contains the functions supporting the main dialog box.
The Rumpus tool has the ability to start and stop any sound within the 100-second playing time, and these values are shown in the UI by the setTimeBox function.
The displaySound function takes the entries made in the Add or Edit Sound dialog box and displays them in the main dialog box.
The wipeClean function clears all the sound entries out, and is called when the user clicks New on the main dialog box just before a Rumpus file is loaded, or simply when the main dialog box is initialized by the OnInitMainDialog function.
OnInitMainDialog does not do anything very exciting; it simply initializes the main dialog box settings, and then calls initDirectSound . This function is in the
The initDirectSound Function
The initDirectSound function, used in our previous samples, needs to be expanded for use with 3-D sounds.
void initDirectSound( HWND hwndDlg ) { int i; g_pSoundManager = new CSoundManager(); for (i=0; i<max_sounds; i++) g_pSound[i] = NULL; if (g_pSoundManager != NULL) { if (FAILED(g_pSoundManager->Initialize( hwndDlg, DSSCL_PRIORITY))) { soundWorking = false; } else { soundWorking = true; if (FAILED(g_pSoundManager-> Get3DListenerInterface( &g_pDSListener ))) sound3DWorking = false; else { // Get listener parameters. g_dsListenerParams.dwSize = sizeof(DS3DLISTENER); g_pDSListener->GetAllParameters( &g_dsListenerParams ); sound3DWorking = true; } } } else soundWorking = false; }
The new code in this function is shown in bold. The Get3DListenerInterface call is to a method of the CSoundManager class, which is one of the utility classes provided originally in dsutil .cpp. This method returns a pointer to the 3-D Listener interface associated with the primary sound buffer. Having successfully retrieved this pointer, a call is made using it to the GetAllParameters method. This is a DirectSound SDK method that fills in a DS3DLISTENER structure with all the current settings for the listener. The structure is listed in the following code.
typedef struct { DWORD dwSize; D3DVECTOR vPosition; D3DVECTOR vVelocity; D3DVECTOR vOrientFront; D3DVECTOR vOrientTop; D3DVALUE flDistanceFactor; D3DVALUE flRolloffFactor; D3DVALUE flDopplerFactor; } DS3DLISTENER, *LPDS3DLISTENER;
Notice that the global distance factor, rolloff factor, and Doppler factor are stored in this structure, along with the position, velocity and orientation of the listener. In the Rumpus tool, we only change the position and velocity of the listener; the orientation (facing up the z-axis) is left at its default if the listener does not move. If the listener does move, then the vOrientFront vector is set to be identical to the vVelocity vector, which would seem to be the most obvious option (where the listener is looking in the direction that they are going). Since we do not change the vOrientTop vector in this tool, the listener never looks up or down.
It is a good idea to make the GetAllParameters call for the listener here. Not only does this allow you to examine the defaults using a debugger, which is usually a valuable thing to do, but the current settings can also be altered and sent back to the DirectSound SDK with a call to SetAllParameters using the same structure. Several other interfaces of DirectSound also have GetAllParameters and SetAllParameters methods that work in a very similar way.
Notice also in the initDirectSound code that we have added the flag sound3DWorking . This is simply to allow for the unlikely event of 2-D sound initializing correctly and 3-D sound failing, in which case, we do not want to deny our users the 2-D sound features of our application.
Back in the
Next, we try to load the required wave files. In this case, a test is done to see if the files are required for ambient or 3-D sounds, and then the loadSound or load3DSound functions are called. These two functions are in the
The loadSound function has not changed much since the Cacophony tool, the only difference being that the buffer flags have been set to support only volume changes ( DSBCAPS_CTRLVOLUME ), and not frequency or panning changes as we now only use this function for ambient sounds. There is a bit more to the load3DSound function in this tool, as shown in the following code.
bool load3DSound(int iS, char filename[], int nBuffers) { if (sound3DWorking) { DWORD bufferFlags = DSBCAPS_CTRL3D DSBCAPS_CTRLFX; // Delete any running sound. stopSound(iS); // Free any previous sound, and make a new one. SAFE_DELETE( g_pSound[iS] ); // Load the wave file into a DirectSound buffer. if (FAILED(g_pSoundManager-> Create( &g_pSound[iS] ,filename, bufferFlags, DS3DALG_HRTF_FULL, nBuffers ))) return false; return true; } else return false; }
The first point to notice in this method is that the buffer flags have been set to DSBCAPS_CTRL3D DSBCAPS_CTRLFX , which will enable both 3-D and special effects. If you do not require special effects, then do not set the DSBCAPS_CTRLFX flag, as this will reduce some unnecessary processing.
Also, notice that the Create function has been called with the parameter DS3DALG_HRTF_FULL . This parameter is not nearly as interesting as it first appears. What it does is sets the 3-D algorithm DirectSound is to use if the buffer is created in software and not in hardware (that is, in virtual rather than physical memory). Most sound cards have an HRTF (head-relative transfer function) algorithm encoded onto them, which will be used by DirectSound if the buffer is created in hardware. If the buffer is created in software, then the parameter DS3DALG_HRTF_FULL will ensure that a full implementation of an HRTF function is applied to the buffer. The parameter can also be set to DS3DALG_HRTF_LIGHT , which provides less accuracy in the HRTF processing, but at less cost in CPU cycles, or DS3DALG_NO_VIRTUALIZATION , which provides no 3-D processing, and instead maps the sound to the normal left and right speakers .
HRTF refers to algorithms that map sounds to two speakers to mimic how a person hears sounds from the 3-D world. Most critics of the algorithms report that they perform quite well if the sound source is in front of the listener, but the results are not as good for sounds that originate above or behind the listener. Judge them for yourself.
The Create function of the CSoundManager class (in the
// Create the sound. *ppSound = new CSound( apDSBuffer, dwDSBufferSize, dwNumBuffers, pWaveFile, dwCreationFlags );
This creates a new CSound object for the loaded wave file, and there are quite a few additions to the creation function to support 3-D sound and effects.
CSound::CSound( LPDIRECTSOUNDBUFFER* apDSBuffer, DWORD dwDSBufferSize, DWORD dwNumBuffers, CWaveFile* pWaveFile, DWORD dwCreationFlags ) { DWORD i; m_apDSBuffer = new LPDIRECTSOUNDBUFFER[dwNumBuffers]; if (m_apDSBuffer != NULL) { for( i=0; i<dwNumBuffers; i++ ) m_apDSBuffer[i] = apDSBuffer[i]; m_dwDSBufferSize = dwDSBufferSize; m_dwNumBuffers = dwNumBuffers; m_pWaveFile = pWaveFile; m_dwCreationFlags = dwCreationFlags; for (i = 0; i<dwNumBuffers; i++) FillBufferWithSound( m_apDSBuffer[i], FALSE ); // Initializing for 3-D processing. m_pDS3DBuffer = new LPDIRECTSOUND3DBUFFER[dwNumBuffers]; m_pdsBufferParams = new DS3DBUFFER[dwNumBuffers]; for( i=0; i<dwNumBuffers; i++ ) { m_pDS3DBuffer[i] = NULL; m_apDSBuffer[i]-> QueryInterface(IID_IDirectSound3DBuffer, (VOID**) &m_pDS3DBuffer[i] ); m_pdsBufferParams[i].dwSize = sizeof(DS3DBUFFER); m_pDS3DBuffer[i] -> GetAllParameters( &m_pdsBufferParams[i] ); } // Special effects. pFXManager = new CSoundFXManager*[dwNumBuffers]; for( i=0; i<dwNumBuffers; i++ ) { pFXManager[i] = new CSoundFXManager( ); pFXManager[i] ->Initialize( GetBuffer(i), TRUE); } } }
The first lines of code have not changed from the Cacophony tool; they first create the requested number of buffers, and then fill them in with the sound data. The new code is shown in bold.
The variable m_pDS3DBuffer is an array of pointers to DirectSound 3-D buffer interfaces. It is filled in with the calls to QueryInterface in the loop that follows . The m_pdsBufferParams array sets up one DS3DBUFFER structure for each of the sound buffers. This structure is filled in with the call to GetAllParameters , using the interface pointer that was just received from the QueryInterface call.
The important point about these DS3DBUFFER structures is that they are copies for your own use. DirectSound keeps its own inaccessible copy; you retrieve the current state using GetAllParameters , change those parameters as appropriate, and then send them back to the DirectSound SDK using the SetAllParameters call. There are some situations where you might want to change the defaults immediately, and call SetAllParameters in the creation function listed previously, so now would be a good time to examine the DS3DBUFFER structure.
typedef struct _DS3DBUFFER { DWORD dwSize; D3DVECTOR vPosition; D3DVECTOR vVelocity; DWORD dwInsideConeAngle; DWORD dwOutsideConeAngle; D3DVECTOR vConeOrientation; LONG lConeOutsideVolume; D3DVALUE flMinDistance; D3DVALUE flMaxDistance; DWORD dwMode; } DS3DBUFFER, *LPDS3DBUFFER;
The first parameter should be set before making any calls at all, and is always set equal to sizeof(DS3DBUFFER). One reason for this data member, which you will see in many Microsoft SDK structures, is that it can make code more resilient to changes to the data structure itself (for example, the adding of a new data member to the structure).
You should recognize the rest of the data members from the previous discussions about vectors, sound cones, and minimum and maximum distances. However, the final parameter, dwMode , is new. You will almost always want to leave this parameter at its default of DS3DMODE_NORMAL , which simply means that the sound will move in the same 3-D space as the listener. However, there are two other options. One is DS3DMODE_DISABLE , which disables all 3-D processing so that the sound appears to originate inside the listener s head. The other option is DS3DMODE_HEADRELATIVE , which instructs the DirectSound SDK to interpret the position, velocity, and orientation as relative and not absolute vectors. So, in this case, the DirectSound SDK will effectively add the position, velocity, and orientation of the sound to the position, velocity, and orientation of the listener to come up with absolute values for the sound.
There may be certain special sounds that you would like to position relative to the listener (for example, an angelic voice off to the left, and a devilish one off to the right, which obviously would move completely relative to the listener). In this case, you need to add a parameter to the Create method of the CSoundManager class to instruct the DirectSound SDK to use head-relative positioning for this particular sound. Assuming that the parameter is a Boolean flag called fSetHeadRelative, then you would need to add both this parameter and the following lines of code to the CSound creation function after the code that calls GetAllParameters for the sound buffer.
if (fSetHeadRelative) { m_pdsBufferParams[i].dwMode = DS3DMODE_HEADRELATIVE; m_pDS3DBuffer[i] -> SetAllParameters( &m_pdsBufferParams[i] ); }
Most applications do not use head-relative sounds, but perhaps there is room for some interesting effects here.
As a design note, we decided to include the 3-D interface pointers and buffer structure as members of the CSound class. This makes sense for the purpose of this tool, but it is possible that you may want this information to be held elsewhere in your application s data, and be attached to the CSound class in some other way. For example, this second method may help in avoiding duplicate copies of an object s position and velocity.
Still in the CSound creation function, the section of code beginning with the comment // Special effects initializes the special effects data. This is only necessary if you want special-effects processing, and we will go over this code in the next chapter.
This ends our discussion of the functions and methods called from the analyzeSoundEffects function in the
The initRumpus function starts off by calling the analyzeSoundEffects and cleanGrid functions (which we have already described), and then starts the timer by changing g_ticks from -1 to 0. Then, initRumpus initializes the movement of the sounds on the grid with the following lines of code.
for (int index=0; index<max_soundsPlusListener; index++) soundEffect[index].setInitialPositionAndVelocity(); Set3DListenerProperties(soundEffect[the_listener].getPositionVector(), soundEffect[the_listener].getVelocityVector(),false, true,gDoppler,gDistance,gRolloff);
The first loop sets the position, velocity, and cone facing for each of the sound effects tabled in the Rumpus UI, along with the listener. The values used to do the initialization are taken from the setting array held for each cSoundEffect object (refer to the setInitialPositionAndVelocity method for objects of this class).
The second call extracts the information for the listener, and calls down into the DirectSound SDK to initialize the listener object.
Similar to the 3-D interface pointer and parameters structure held for each sound in the CSound class, we have an interface pointer and structure for the listener. However, instead of embedding it in a class, we declared them in the
LPDIRECTSOUND3DLISTENER g_pDSListener = NULL; // 3-D listener object. DS3DLISTENER g_dsListenerParams; // Listener properties.
It would, of course, be perfectly possible to have embedded these into the CSoundManager class, which might make some design sense. However, for this sample we kept them as global variables.
With this data declared, we can call the Set3DListenerProperties function.
void Set3DListenerProperties(D3DVECTOR* pvPosition, D3DVECTOR* pvVelocity, bool moving, bool initialize, float doppler, float distance, float rolloff) { int apply; if (sound3DWorking) { memcpy(&g_dsListenerParams.vPosition, pvPosition, sizeof(D3DVECTOR)); memcpy(&g_dsListenerParams.vVelocity, pvVelocity, sizeof(D3DVECTOR)); if (moving) memcpy( &g_dsListenerParams.vOrientFront, pvVelocity, sizeof(D3DVECTOR) ); if (initialize) { apply = DS3D_IMMEDIATE; g_dsListenerParams.flDopplerFactor = doppler; g_dsListenerParams.flDistanceFactor = distance; g_dsListenerParams.flRolloffFactor = rolloff; } else apply = DS3D_DEFERRED; if( g_pDSListener ) g_pDSListener -> SetAllParameters( &g_dsListenerParams, apply ); } }
The first two calls to memcpy faithfully copy the position and velocity vectors. The orientation of the listener is only set if the listener is moving, and therefore has a legitimate velocity vector. Remember that the Rumpus tool default simply orientates the listener so that they face up towards the top of the grid.
Although listener position and velocity are likely to change throughout an application, the Doppler factor, distance factor and rolloff factor are not, so we only set these three factors if the initialize flag is set. Note that our external declaration of Set3DListenerProperties sets defaults for these three parameters, so there is no need to set values for them to use the function.
extern void Set3DListenerProperties(D3DVECTOR* pvPosition, D3DVECTOR* pvVelocity, bool moving, bool initialize = false, float doppler = 1.0f, float distance = 1.0f, float rolloff = 1.0f );
The final call in Set3DListenerProperties uses the global interface pointer g_pDSListener to call the DirectSound SDK SetAllParameters method, with the g_dsListenerParams structure containing the new settings as the first parameter.
The story is made slightly more complicated by the second parameter for SetAllParameters , the requirement for a setting DS3D_IMMEDIATE or DS3D_DEFERRED . If DS3D_IMMEDIATE is used as the second parameter for SetAllParameters , then the changes come into effect immediately. In theory, this is almost always what you want, however in practice, changes to the data members of either DS3DBUFFER or DS3DLISTENER structures are costly and can result in glitches in the quality of sound output if constant changes are sent to the sound mixer that forms the engine of DirectSound. If the DS3D_DEFERRED setting is used, then none of the new data will apply until a call is made to the DirectSound SDK method CommitDeferredSettings . You will notice in the Set3DListenerProperties method that the initialization settings are applied immediately, and will not result in any glitches because no sounds are being played. However, those changes that are made during the course of play are deferred, waiting for a suitable time to commit them all.
If you look at the OnTimer function, you will notice a call to a wrapper function commitAllSettings, which occurs only after all the changes are made to each sound and the listener at the end of each tick period. This minimalizes any glitches that may occur due to the changes, and also increases the efficiency of DirectSound. The following wrapper method is only there to keep all references to the g_pDSListener pointer in one source file.
void commitAllSettings() { if (g_pDSListener) g_pDSListener -> CommitDeferredSettings(); }
Although the DirectSound SDK method CommitDeferredSettings is made on the listener object, it actually also applies to all of the deferred settings on all of the sounds.
This ends our discussion of the initRumpus function. The stopRumpus function is fairly simple and doesn t introduce anything new. This brings us to the all-important OnTimer function that runs to several pages of code.
The OnTimer function is called once every tenth of a second in the Rumpus tool. The first short section of code deals with the situation before the Play button has been clicked.
for (i=0; i<max_soundsPlusListener; i++) { if (soundEffect[i].getProposed3DSound() OR i == the_listener) { newSoundTrackPoint( (int) soundEffect[i].getPositionX(), (int)soundEffect[i].getPositionZ(),i,false); } }
The getProposed3DSound method simply returns true if the sound slot has been filled with a 3-D sound, as opposed to being empty or containing an ambient sound. If there are requests for 3-D sounds, then the appropriate numbers are drawn on the grid, along with the circle for the listener.
The rest of the code is run when the Play button has been clicked. We start by showing the code for the listener.
bool movedListener = soundEffect[the_listener].updatePosition(); newSoundTrackPoint((int)soundEffect[the_listener].getPositionX(), (int)soundEffect[the_listener].getPositionZ(), the_listener,movedListener); if (movedListener) { Set3DListenerProperties(soundEffect[the_listener].getPositionVector(), soundEffect[the_listener].getVelocityVector(), movedListener); }
The first line of code updates the listener s position and returns true if it is moving. A call is then made to the newSoundTrackPoint function to draw the white circle which represents the listener on the grid. If the listener is moving, then the Set3DListenerProperties call is made, but only with the first three parameters of the call. The fourth parameter will default to false , and indicate that this is not an initialization call. This is all that is needed to update the listener s properties.
We won t list all the lines of code here for playing sounds, but will concentrate on the salient calls. The first loop checks to see if any sound should be started, and if it is an ambient sound, then the following call is made.
playAmbientSoundBuffer( soundEffect[i].getSoundIndex(), soundEffect[i].getBufferIndex(), soundEffect[i].getOneSetting(outside_volume), true);
The playAmbientSoundBuffer function (in the
bool playAmbientSoundBuffer(int iS, int iB, int V, bool looping) { // Ambient sounds are always centrally positioned. if (soundWorking AND g_pSound[iS] != NULL) { long actualVolume = calcuateVolumeChange(V); DWORD dwFlags = 0; if (looping) dwFlags = DSBPLAY_LOOPING; if (FAILED(g_pSound[iS] ->PlayBuffer( iB, 0, dwFlags, actualVolume, DSBPAN_CENTER, NO_FREQUENCY_CHANGE))) return false; else return true; } else return false; }
Notice that we are using a utility function, calculateVolumeChange , to calculate the desired volume as a percentage of the recorded volume. The definition of this function is in the
long calcuateVolumeChange(int percent) { if (percent <= 0) return DSBVOLUME_MIN; if (percent > 0 && percent < 100) { double ratio = (double) percent / 100.0f; double hundredthsOfDeciBels = 100 * 20 * log10(ratio); return DSBVOLUME_MAX + (long) hundredthsOfDeciBels; } return DSBVOLUME_MAX; }
This function uses the same underlying math as the calculateVolumeFromDistance function described in Chapter 2 (the 20 * Log10(ratio) statement). An input percentage of 50 implies that you want the sound to appear to originate twice as far away as the original recording, so 6.02 decibels is deducted from the original volume. Note that as all the ratios will have a value less than 1, and that the log10 function will return negative numbers for these values, then we add the value for hundredthsOfDecibels rather than subtract it to avoid the double negative.
Back in the playAmbientSoundBuffer function, it is usual to loop ambient sounds, so the dwFlags parameter is set to DSBPLAY_LOOPING . Then the PlayBuffer method of the CSound class is called to begin playing the sound.
Back in the OnTimer function, the next major call is to Set3DSoundProperties .
Set3DSoundProperties( soundEffect[i].getSoundIndex(), soundEffect[i].getBufferIndex(), soundEffect[i].getPositionVector(), soundEffect[i].getVelocityVector(), soundEffect[i].getConeVector(), true, soundEffect[i].getOneSetting(volume_radius), soundEffect[i].getOneSetting(inside_cone), soundEffect[i].getOneSetting(outside_cone), soundEffect[i].getOneSetting(outside_volume)); play3DSoundBuffer( soundEffect[i].getSoundIndex(), soundEffect[i].getBufferIndex(), true, rSFX, FXData);
The first call passes through our wrapper layer to the following method of the CSound class.
HRESULT CSound::Set3DSoundProperties(int index, D3DVECTOR* pvPosition, D3DVECTOR* pvVelocity, D3DVECTOR* pvCone, bool initialize, int maxVradius, int insideCone, int outsideCone, int outsideVolume) { if (index < (int) m_dwNumBuffers) { memcpy( &m_pdsBufferParams[index].vPosition, pvPosition, sizeof(D3DVECTOR) ); memcpy( &m_pdsBufferParams[index].vVelocity, pvVelocity, sizeof(D3DVECTOR) ); memcpy( &m_pdsBufferParams[index].vConeOrientation, pvCone, sizeof(D3DVECTOR) ); if (initialize) { m_pdsBufferParams[index].flMinDistance = (float) maxVradius; m_pdsBufferParams[index].dwInsideConeAngle = insideCone; m_pdsBufferParams[index].dwOutsideConeAngle = outsideCone; m_pdsBufferParams[index].lConeOutsideVolume = outsideVolume; } m_pDS3DBuffer[index] -> SetAllParameters( &m_pdsBufferParams[index], DS3D_DEFERRED); return S_OK; } else return S_FALSE; }
Notice that this method is very similar to the Set3DListenerProperties method. The first three memcpy calls copy the position, velocity and orientation vectors. Only during initialization are the minimum distance and cone parameters set. Of course, it is possible to have a sound effect with a constantly varying sound cone (for example, a rotating siren), and altering our previous code to account for this would be as simple as a name change (just change the name of the initialize flag to something like fResetConeParameters ).
Finally, we call the DirectSound SDK method SetAllParameters , with the address of the structure holding the parameters ( m_pdsBufferParams[index]) and the DS3D_DEFERRED flag set. The index , of course, refers to the buffer index for that sound, which will often be zero as only one buffer of the sound is required. In most cases, we recommend using the DS3D_DEFERRED flag, which we previously explained in the section on the Set3DListenerProperties method. The use of this flag means that the changes are deferred and will take effect only when a call is made to CommitAllSettings .
These calls change the 3-D settings for the sound, but do not actually make it start playing; that is done with the next call in the OnTimer function, play3DSoundBuffer .
play3DSoundBuffer( soundEffect[i].getSoundIndex(), soundEffect[i].getBufferIndex(), true, rSFX, FXData);
For now, ignore the rSFX and FXData parameters, as these refer to special effects that we will discuss in the next chapter. If you follow the play3DSoundBuffer call through the
HRESULT CSound::Play3DBuffer(int iB, DWORD dwPriority, DWORD dwFlags ) { HRESULT hr; BOOL bRestored; if( m_apDSBuffer == NULL ) return CO_E_NOTINITIALIZED; LPDIRECTSOUNDBUFFER pDSB = m_apDSBuffer[ iB ]; if( pDSB == NULL ) return DXTRACE_ERR( TEXT("Play3DBuffer"), E_FAIL ); // Restore the buffer if it was lost. if( FAILED( hr = RestoreBuffer( pDSB, &bRestored ) ) ) return DXTRACE_ERR( TEXT("RestoreBuffer"), hr ); if( bRestored ) { // The buffer was restored, so we need to fill it with new data. if( FAILED( hr = FillBufferWithSound( pDSB, FALSE ) ) ) return DXTRACE_ERR( TEXT("FillBufferWithSound"), hr ); // Make DirectSound do pre-processing on sound effects. Reset(); } hr = pDSB -> Play( 0, dwPriority, dwFlags ); return hr; }
Most of the code in the Play3DBuffer method is concerned with recovering the buffer should some other application have taken it. Otherwise, it comes down to the Play call. The first of the two parameters is always 0 (it is one of those reserved parameters), and the second parameter should also always be 0, unless the sound buffer was created using the DSBCAPS_LOCDEFER flag (which we did not do in this case). The third parameter can either be 0 or DSBPLAY_LOOPING .
Now go back out to the OnTimer function in the
The next chunk of code in the OnTimer function more or less repeats the previous section, but applies to sounds that are triggered at the given random frequency. Nearly at the end of the code example are the following lines of code.
if (soundEffect[i].getOneSetting(ambient_sound) == 0) { bool movedSound = soundEffect[i].updatePosition(); newSoundTrackPoint( (int) soundEffect[i].getPositionX(), (int)soundEffect[i].getPositionZ(),i,movedSound); if (movedSound) Set3DSoundProperties( soundEffect[i].getSoundIndex(), soundEffect[i].getBufferIndex(), soundEffect[i].getPositionVector(), soundEffect[i].getVelocityVector(), soundEffect[i].getConeVector(), false ); }
This is very similar to the code shown earlier in this chapter for the OnTimer function that tests to see if the listener is moving, but in this case, the code refers to the sounds rather than to the listener. First, the sound position is updated and grid points are set, and then if the sound has moved, a shortened call to Set3DSoundProperties is made to pass the new position into the DirectSound SDK.
At the end of the OnTimer function, there is a call to commitAllSettings , which was discussed earlier, and then finally, a call to drawSoundTracks to actually render the pixels stored as sound tracker points.
The final functions of the