Multiplatform Game Programming in OS/2 - Part 3

From EDM2
Jump to: navigation, search

by Robert Basler

Part 1

Part 2

Part 3

This third part in our series on platform independent game programming using OS/2 will start with some cross-platform programming considerations, then we will get into the details of a few of the third party graphics and sound libraries available for OS/2.

This month we're going to do a bit of programming. I've been working on a cross-platform sound mixing library off and on for some time now. Since I prefer fullscreen graphics to windowed when it comes to getting fully immersed in a game, I needed to find a way to get low-latency streaming sound output in an OS/2 fullscreen. If you prefer a PM program with DIVE or OpenGL graphics, this technique works equally well.

Low Latency Audio with DART

DART (Direct Audio Real Time) is the API designed to provide low-latency streaming audio in OS/2. It was added to OS/2 with Warp 4 to support speech recognition, but has the side benefit of being very useful for games. DART is also available in Warp 3 with current fixpacks. What DART gives you is a direct route to the OS/2 amp-mixer device. The amp-mixer is a piece of code buried deep in the depths of MMPM which provides input and output switching as well as volume, bass, and treble controls for audio signals in OS/2. Everything that is audio eventually passes through the amp-mixer.

When you use DART, you are setting up the amp-mixer device to add a new digital audio stream to the sound card's output. When this digital audio stream is playing the amp-mixer will notify your application every time it needs more audio data. What this audio data contains is completely up to you. In the case of the sample application, I needed a test platform for a digital audio mixer I am working on that supports mixing up to 32 tracks of 8 or 16-bit audio into a single track for output. My mixer can provide output as 8 or 16-bit mono or stereo. If you're interested in how the mixer works and how audio data is layed out in memory, see my article in Gamasutra at http://www.gamasutra.com/features/sound_and_music/19981218/surround_01.htm

Since DART allows you to control the size of the audio buffers, and only requires that it has one buffer that is playing and one that is waiting, the time between when you pull the trigger and you can hear the sound of the gun can be very, very short. For example, adequate game sound running with 11KHz stereo, 16-bit output uses about 11,000x2x2=44,000 bytes per second. With a 4K audio buffer, each buffer takes 91 milliseconds for the sound card to play so with careful coding you could have the average latency down to 91+(91/2)=135 milliseconds which is a barely noticeable delay. This is the perfect situation for a game since you want the audio synchronized with the action. Ever watch a movie with the sound track out of synch? It is very distracting.

Queues Free MCI from its PM Prison

DART is controlled through OS/2's MCI (Media Control Interface). Unfortunately, up until Warp 4, MCI was pretty much limited to PM applications. Fortunately, the Warp 4 release of MMPM included a new option to the MCI_OPEN command, MCI_DOS_QUEUE (this is also available in Warp 3 with current fixpacks.) This option allows an OS/2 application that does not have a PM window, and therefore cannot use a PM message queue for notifications, to use an OS/2 control program queue for notifications instead.

Setting up a queue for notifications is fairly straightforward. Think of a name for the queue, create the queue, open it for this process, then open the amp-mixer device, telling MMPM that you want to use that queue for notifications. Here is the code that does this in the sample application:

/* Create a queue for notifications */
strcpy(QueueName, "\\QUEUES\\S3DQUEUE");
if (DosCreateQueue(&QHandle, QUE_FIFO, QueueName)) return(1L);
if (DosOpenQueue(&OwnerPID, &QHandle, QueueName))  return(1L);
/* Start our Queue monitoring thread */
_beginthread((void(*)(void*))QMonitor, (PVOID)NULL, 65536L, (PVOID)QHandle);
  /* open the mixer device  */
  memset ( &AmpOpenParms, '\0', sizeof ( MCI_AMP_OPEN_PARMS ) );
  AmpOpenParms.usDeviceID = ( USHORT ) 0;
  AmpOpenParms.pszDeviceType = ( PSZ ) MCI_DEVTYPE_AUDIO_AMPMIX;
  AmpOpenParms.hwndCallback = QHandle;
  rc = mciSendCommand( 0,MCI_OPEN,MCI_DOS_QUEUE | MCI_WAIT | MCI_OPEN_TYPE_ID
                      |MCI_OPEN_SHAREABLE,( PVOID ) &AmpOpenParms,0 );

The queue monitor in the sample program doesn't really do anything other than print messages when something happens of note, like losing and regaining access to the sound device. But it would be used in a real program to make sure that the program knows when it can't make sounds, and perhaps to display an onscreen icon to indicate the situation.

Setting up the Amp-Mixer with DART

Once the mixer is open, you need to use the MCI_MIXSETUP message to set up the mixer and let it know the format of the data you wish to play. The parameters can be set with the MCI_MIXSETUP_PARMS structure below. To get audio playback, you only need to set the ulBitsPerSample, ulFormatTag, ulSamplesPerSec, ulChannels, ulFormatMode, ulDeviceType, and pmixEvent parameters, everything else can be set to 0's.

typedef struct _MCI_MIXSETUP_PARMS {
    HWND            hwndCallback;     /*  Window handle. */
    ULONG           ulBitsPerSample;  /*  Bits per sample. */
    ULONG           ulFormatTag;      /*  Format tag. */
    ULONG           ulSamplesPerSec;  /*  Sampling rate. */
    ULONG           ulChannels;       /*  Number of channels. */
    ULONG           ulFormatMode;     /*  Play or record. */
    ULONG           ulDeviceType;     /*  Device type. */
    ULONG           ulMixHandle;      /*  Mixer handle. */
    PMIXERPROC      pmixWrite;        /*  Entry point. */
    PMIXERPROC      pmixRead;         /*  Entry point. */
    PMIXEREVENT     pmixEvent;        /*  Entry point. */
    PVOID           pExtendedInfo;    /*  Extended information. */
    ULONG           ulBufferSize;     /*  Recommended buffer size. */
    ULONG           ulNumBuffers;     /*  Recommended number of buffers. */
  } MCI_MIXSETUP_PARMS;

/* Setup the mixer for playback of wave data */
 memset( &MixSetupParms, '\0', sizeof( MCI_MIXSETUP_PARMS ) );
 MixSetupParms.ulBitsPerSample = uBitsPerSample;
 MixSetupParms.ulFormatTag = MCI_WAVE_FORMAT_PCM;
 MixSetupParms.ulSamplesPerSec = uSamplesPerSec;
 MixSetupParms.ulChannels = uChannels;
 MixSetupParms.ulFormatMode = MCI_PLAY;
 MixSetupParms.ulDeviceType = MCI_DEVTYPE_WAVEFORM_AUDIO;
 MixSetupParms.pmixEvent    = MyEvent;
 rc = mciSendCommand( usDeviceID,MCI_MIXSETUP,MCI_WAIT | MCI_MIXSETUP_INIT,( PVOID ) &MixSetupParms,0 );

After the MCI_MIXSETUP call, several values are returned in this structure. The ulMixHandle field will be filled in with the handle to the amp-mixer which will be used when we write audio data to the amp-mixer. ulBufferSize is the suggested size for the buffers. Although you can specify any buffer size you like when you allocate your audio buffers for DART, the people who know this system say that often things won't work if you don't use the default buffer size. ulNumBuffers is the recommended number of buffers to allocate within the amp-mixer. In general it is pretty safe to ignore this and allocate the number you think you need, although you must allocate at least three.

To allocate the buffers within the amp-mixer that will be used for the audio data, you use the MCI_BUFFER message. Buffers can be up to 64K each, but as I said above, use the recommended size. The following code allocates the buffers.

 typedef struct _MCI_BUFFER_PARMS {
    HWND      hwndCallback;    /*  Window handle. */
    ULONG     ulStructLength;  /*  Structure length. */
    ULONG     ulNumBuffers;    /*  Number of buffers. */
    ULONG     ulBufferSize;    /*  Suggested buffer size. */
    ULONG     ulMinToStart;    /*  Not used. */
    ULONG     ulSrcStart;      /*  Not used. */
    ULONG     ulTgtStart;      /*  Not used. */
    PVOID     pBufList;        /*  Pointer to list of buffers. */
  } MCI_BUFFER_PARMS;

/* Set up the BufferParms data structure and allocate device buffers from the Amp-Mixer */
MCI_MIX_BUFFER  MixBuffers[MAX_BUFFERS];   /* Device buffers */
BufferParms.ulStructLength = sizeof(MCI_BUFFER_PARMS);
BufferParms.ulNumBuffers = ulNumBuffers;
BufferParms.ulBufferSize = MixSetupParms.ulBufferSize;
BufferParms.pBufList = MixBuffers;
rc = mciSendCommand( usDeviceID,MCI_BUFFER,MCI_WAIT |MCI_ALLOCATE_MEMORY,( PVOID ) &BufferParms,0 );

Before we allocate the buffers, we have to allocate and initialize a bunch of MCI_MIX_BUFFER structures which have the format:

typedef struct _MCI_MIX_BUFFER {
    ULONG     ulStructLength;  /*  Structure length. */
    PVOID     pBuffer;         /*  Pointer to a buffer. */
    ULONG     ulBufferLength;  /*  Length of the buffer. */
    ULONG     ulFlags;         /*  Flags. */
    ULONG     ulUserParm;      /*  User parameter. */
    ULONG     ulTime;          /*  Device time in ms. */
    ULONG     ulReserved1;     /*  Reserved. */
    ULONG     ulReserved2;     /*  Reserved. */
  } MCI_MIX_BUFFER;

The following code initializes the audio buffers by setting the audio data to all 0's and the buffer length field in each buffer to the correct value.

for( ulIndex = 0; ulIndex < ulNumBuffers; ulIndex++) {
    memset( MixBuffers[ ulIndex ].pBuffer, '\0', BufferParms.ulBufferSize );
    MixBuffers[ ulIndex ].ulBufferLength = BufferParms.ulBufferSize;
    }

If your audio data has an end, for example if you are using DART to play a single WAV file and you want it to stop playing at the end, there is a special flag for the last buffer which tells it to stop playing after that buffer. It can be set with:

MixBuffers[ulNumBuffers - 1].ulFlags = MIX_BUFFER_EOS;

The Code Looks Good, but it Won't Start Playing

One thing I discovered writing this program is that DART will not start playing audio until you have provided it with two buffers to play. In the example this is accomplished with the following line:

MixSetupParms.pmixWrite( MixSetupParms.ulMixHandle,&MixBuffers[ulBufferTail],2 );

As soon as this call completes, the amp-mixer takes over and starts pulling buffers using the MyEvent routine.

You'll note that in the above line of code I call the address MixSetupParms.pmixWrite to write the audio data to the mixer. This address is set up by the MCI_MIXSETUP call, but it can also be set by the mixer after the mixer calls the MyEvent routine so don't go storing it elsewhere in your program where the ampmix device can't see it, you'll be asking for hard to find bugs.

Don't Bog Down the System

The MyEvent routine is called by MMPM with a very high priority. It is very important that you don't bog down the system with an excess of code here. To avoid this problem in the sample program, I added the MixingThread which mixes buffers in anticipation of their need by the sound card calling the MyEvent routine. Mixing buffers can be very time consuming on slower hardware, so it is best to put it in its own thread, safely away from the MyEvent handling. I use event semaphores to let the MixingThread know when another buffer is needed.

You will notice that there is error handling code in the MyEvent routine. In a real application you would have to do something about these errors. The only one we might be interested in is ERROR_DEVICE_UNDERRUN which indicates that we aren't able to provide sound data fast enough for the sound card, but if we are getting that error, it is time to either set less ambitious output settings or get a faster computer. I'm pretty sure that ERROR_DEVICE_OVERRUN is only possible when you are recording audio into the computer using DART.

Polite Games SHARE the Sound Card

When the sample program is playing sounds, use another OS/2 program to play a sound. For example, drag and drop a file, or hit the limit of a scroll bar to make the warning noise. You will see a message appear in the 3DSTEST window:

msg=MM_MCIPASSDEVICE: Device id=1 MCI_LOSING_USE

S3DTEST will stop playing when the interrupting sound starts, and when the interrupting sound ends and S3DTEST can start playing sounds again, the following message will appear:

msg=MM_MCIPASSDEVICE: Device id=1 MCI_GAINING_USE

This is a result of using the MCI_OPEN_SHAREABLE flag to the MCI_OPEN call. This is the way to make polite OS/2 gaming audio. Note that if you open a DOS or WinOS/2 window while the program is running, the DOS session won't get access to the audio card. This sharing feature only extends as far as MMPM applications.

At some point I'll have to add some code to the MCI_GAINING_USE routine to skip the audio forward so that when the audio is interrupted it doesn't just pick up where it left off, but instead it starts again in synchronization with the onscreen gaming action.

Cleaning Up

When you are done playing audio, you can free the buffers and close the amp-mixer, then release the queue that we allocated for notifications. By default my mixer provides buffers that are silent, so it will keep playing them until the amp-mixer closes. There is a 5 second delay at the start of the cleanup to make sure that the 5 seconds of buffers that we allocated all play out before we close the amp-mixer rather than just cutting them off.

/* Free the buffers, then uninitialize the mixer and close it. Close the queue. */
rc = mciSendCommand( usDeviceID,MCI_BUFFER,MCI_WAIT | MCI_DEALLOCATE_MEMORY,( PVOID )&BufferParms,0 );
rc = mciSendCommand( usDeviceID,MCI_MIXSETUP,MCI_WAIT | MCI_MIXSETUP_DEINIT,( PVOID ) &MixSetupParms,0 );
rc = mciSendCommand( usDeviceID,MCI_CLOSE,MCI_WAIT ,( PVOID )&GenericParms,0 );
DosCloseQueue(QHandle);

The Sample Application

I have included a sample application (515K) with this column as well as some of the source for it. It is nothing pretty, but it will let you hear me talk. And it uses DART to make sound from an OS/2 fullscreen or windowed session. To run the program, open an OS/2 windowed or fullscreen session and type 3DSTEST. The program will prompt you for what sort of output you want, Stereo or Mono, Normal or through a Dolby Pro-Logic surround sound decoder, 8 or 16-bit output, and whether you want it to swap the left and right channels (in case your speakers are backwards.) It will then setup and start DART, displaying the settings it has used.

To hear some sounds press a number between 1 and 6. There are also prefixes to route the sound to a specific channel, so if you typed L1 it would play an 8-bit mono track through the left channel only. If you are running through a surround sound decoder, you can use the S prefix to play sounds through the surround channel. You can hit the numbers repeatedly to have the tracks play on top of each other. Try hitting 1 several times quickly, it makes a cool echo effect. The 16-bit mono track (3) is a white noise track I was using to calibrate the volume output of the mixer so it isn't much to listen to.

You can adjust the volume of the output with the comma and period keys, comma decreases the volume, period increases it. You can also adjust the volume of the left channel alone with g/h, the right channel with j/k and the surround channel with n/m.

When you are done, hit Esc to end the program. It will take a few seconds for the program to close while it waits for tracks to complete playing and the buffers to flush.

The Source Code

You will note that I have not included complete source for this application. This is mainly because it is part of a much larger project which is still under construction and not very pretty. Much of the code for the mixing part of the sample application is discussed in my article in Gamasutra.

The 3DSTEST.CPP file is a frankenstein of IBM example code from the Multimedia Programming Reference and the OS/2 Warp 4 Toolkit example programs as well as bits and pieces I have added to make things work. It is not extensively documented nor is there much in the way of error recovery but as a working example, it will give you enough information to get your own DART audio routines going. I strongly recommend reading through the Multimedia Programming Reference, part of the Warp 4 toolkit, for detailed syntax and coverage of the multitude of options available for the DART routines.

The book of the month is "Writing Compilers and Interpreters - An Applied Approach using C++" by Ronald Mak (www.wiley.com/compbooks, ISBN: 0-471-11353-0.) Quake and Jedi Knight made scripting languages a big thing in games. They allowed the user to customize every feature of the programs, to create new levels, and even new automated enemies to fight. If you are interested in adding scripting or a programming language to your game, this book is a good place to start. Ronald Mak presents a very readable explanation of how compilers and interpreters work. The book includes a complete Pascal compiler, a Pascal interpreter and an interactive debugger as well as all the source code to show you how they actually work. Professional programmers might dismiss the book because it doesn't provide a C compiler, but they should know better. Pascal is a robust, easy to learn language with very good compile-time error checking that is well suited to use by your average gamer. You can get all the source code from the book at ftp://ftp.wiley.com/public/computer_books/Software_Development/Mak-Writing_Compilers

When I first started looking into adding a programming language to my game, I tried using YACC and LEX, well-known UNIX programs for making compilers. While I was able to make them work, I was never able to figure out what to do with the output programs since the documentation was sparse and I knew little of compiler construction. When I also took into consideration that YACC and LEX are covered by the GNU Public License I found I had to look for other solutions if I wanted to be legal. Ronald Mak presents the tools you need to make your own compilers and they are a very viable solution to this problem.

Some Cool Game Programming Websites

I'm constantly amazed at the amount of interesting game programming information available on the web, but as with everything on the web, quality isn't always easy to find. Here are some of my favourite sites, if you have some of your own, please let me know.

www.gdmag.com and www.gamasutra.com - The websites of Game Developer Magazine. You will not find a better collection of articles on game programming, business, marketing, and all things related to game development. The features archives are spectacular and neatly arranged by topic and the weekly updates are well worth keeping up with.

  • www.edm2.com - The OS/2 programmers site of choice. Lots of meaty articles on all of the subsystems of OS/2 including OpenGL, the high resolution timer, and more.
  • www.gamedev.net - Be prepared to spend a lot of time here. Hundreds of articles on game programming, some are a little dated, but most are cutting edge.
  • www.os2games.com - A great resource for everything relating to gaming on OS/2. Lists of available games, programming info, tricks for getting games to work and more.
  • www.wotsit.org - Wotsit's format? An amazing collection of file format documents, bitmaps, 3D graphics formats, sound files, video files. You name it, they've got it. If you're writing your own libraries for common data formats, this is a great place to get the information you need to start.

This article was written in a bit of a rush after my wedding in June so I haven't had a chance to think about what to cover next, so look forward to a surprise topic next month.

Gaming News

If you are an OS/2 user and a fan of 3D games, there is a new 3D card for you. Riva recently announced the availability of GRADD based video drivers for OS/2 for their new TNT 2 line of video cards. The announcement can be seen at http://www.nvidia.com/3Dgraphics/datasheet/rivatnt2.pdf The OS/2 drivers don't exploit any of the card's 3D features, and they have no plans to add hardware OpenGL support, but if you want a first class gaming card that has support for OS/2, this is one to look at.

Stellar Frontier 0.96 is out, get it at http://www.stardock.com/products/sf/sf.html This game just keeps getting prettier.

Game Developer magazine is an unbeatable resource for game developers, complete with meaty features and industry news every month. The magazine offers free subscriptions to professionals in the game development community. If you feel you are such a person, you can apply at www.gdmag.com

One note on May's column on the poor man's ad blocker. A lot of people wrote me to let me know that updates to the HOSTS file take effect instantly. Checking this fact with PING shows they are correct. Where I was led astray was by Netscape which seems to cache DNS lookups, so if you have looked up an ad site once successfully, it seems to find it next time without looking to the DNS and the HOSTS file. Quitting Netscape doesn't seem to be enough, but I found that if you flush the memory cache after a change to the HOSTS file, it works as it should. Your mileage may vary.