Adding Sound to Your OS/2 Application

Written by Semir Patel

Introduction
As PC based systems become cheaper and cheaper, soundcards are fast becoming a standard peripheral just like your hard disk or keyboard. Today's PCs, are for the most part, multimedia capable complete with 16bit sound cards and a CD-ROM drive. What better way to take advantage of that 16 bit (OK...so some of you have 8 bit) sound card than to jazz up your OS/2 application with some stunning sound effects. They will definitely give your application a certain edge over your competitor's.

First and foremost, this article is not meant to be an introduction to MMPM/2 programming. This article is aimed at those of you who are asking yourself the question, "How can I add sound to my program in the shortest amount of time possible without trudging through the enormous and very complex MMPM/2 subsystem?" Hopefully, this article will succeed in guiding you from point A to point B without wandering too far off track. It may be a little lean on concept and instructional in style, but it explains only what is necessary to get the job done. In the end, you will have a fully functional thread that you can plug into your already existing application.

This article assumes previous knowledge of C, Presentation Manager, and basic sound terminology. The following hardware and software prerequisites are also needed:
 * OS/2 2.x w/ MMPM/2 support installed
 * OS/2 compiler
 * MMPM/2 Toolkit
 * Sound Card supported by MMPM/2

The MCI String and Command Interfaces
The MMPM/2 subsystem serves as the layer of abstraction between you and your multimedia peripherals. Its sole purpose is to allow you to effectively communicate with and use your multimedia peripherals to their fullest extent. It offers a rich and complex set of standardized APIs with which you can communicate with media devices ranging from CD/LaserDisc players to sound cards.

There are two approaches one can take from this point forward. The first is by using the Media Control Interface (MCI) String Interface. This approach is delightfully simple, far from complex, requires the least amount of time to learn, and can even be accessed directly from IBM's powerful REXX language. The string interface's claim to fame is its amazing simplicity. Playing a sound file is as easy as issuing the following sequence of character strings via the mciSendString function. All other multimedia devices can be accessed in the same general way. It has one shortcoming with respect to waveaudio devices, though - only one sound file per device can be kept in memory at any one point in time. This approach is clearly not suitable for applications that require a few sound files or situations where sounds are expected to be played synchronously in conjunction with events. Having to constantly shuffle sounds in and out from the hard disk can also dramatically increase CPU usage.

As opposed to the MCI string interface, the MCI command interface centres around sending individual commands to a particular media device. It is in many aspects very similar to the message based scheme used in window procedures. The big advantage and our main reason for using the MCI command interface is its low CPU usage and its ability to play one or more sounds that reside in memory rather than on the hard disk. Granted, the command interface is somewhat more complicated and harder to grasp, but it's more than worth it for the efficiency and quick response.

The following list outlines what needs to be done using the MCI command interface to accomplish what the string interface does in four lines. All points will be discussed in detail throughout the article.
 * 1) Open sound file
 * 2) Read into memory buffers
 * 3) Get information about sound file
 * 4) Close sound file
 * 5) Create memory playlist
 * 6) Insert sound file into memory playlist
 * 7) Open sound device
 * 8) Mount memory play list onto sound device
 * 9) Adjust sound device to match characteristics of sound file
 * 10) Issue command to play memory playlist
 * 11) Free memory sound file memory buffers
 * 12) Close sound device

The MCI Command Interface
You can communicate with any media device by sending it specific pre-defined commands. Just like PM window messages have the WM_* prefix, MCI commands and flags have a suffix of MCI_*. You also might have noticed the family of MCI functions is prefixed by mci. These commands coupled with flags of various sorts give you a very high degree of control over a device. For example, with a sound device we can manipulate it - PLAY, STOP, REWIND, etc.) or set certain qualities like samples per second, volume, and bits per sample. The mciSendCommand function's sole purpose is to deliver your commands to a specific media device and to return a message once the command is completed if so specified. mciSendCommand (usDeviceID, /* device ID of media device (sound card, CD, .... ) */ usMessage, /* command to be performed (MCI_PLAY, SEEK, MCI_STOP, ....) */ ulParam1,  /* flags for this command (MCI_WAIT,MCI_NOTIFY, MCI_, ....) */ uParam2,   /* ptr to info structure (MCI_OPEN_PARMS, MCI_SEEK_PARMS, ...) */ usUserParm);/* param returned if MCI_NOTIFY (user defined message WM_USER + x) */ The following are MCI commands with some of their associated flags and information structures that we will be using in our example program:

Flags commmon to all Commands:

 * MCI_WAIT
 * Wait for the operation to finish successfully instead of executing it asynchronously and returning from the function call


 * MCI_NOTIFY
 * Send usUserParam back to calling window (via mciOpenParameters.hwndCallBack more on this below) once the operation has finished.

It is also possible to logically OR flags together to complete two operations in one function call. Of course, all the OR'ed flags must be valid for the particular command you are about to issue.

So before calling mciSendCommand you should: Let's say we want to change the bits per sample and the samples per second of our sound device and also specify that the function not return before completing the command. The follow code should produce the desired effect:
 * Decide on the command
 * OR together the appropriate flags for your desired command
 * Fill in the appropriate info structure with only the relevant information

Wave Audio Structures
As you might already know, wave files are not all the same. First of all, they can either be 8 or 16 bit. Furthermore, they may vary in kHz from 11 to 44. It is imperative that the characteristics of the sound device match those of the wave file. For example, if your wave file is recorded at 11025Hz and your sound device is set at 22050Hz, your output will obviously be distorted. The information we need to know about the wave file is stored in its header. Calling the function mmioGetHeader along with the following structure and a file handle obtained through mmioOpen will extract that information for us. Comments are beside the fields that are of interest to us. Once the header has been obtained, the next step would be to copy the commented fields into an MCI_WAVE_SET_PARMS structure and issue a MCI_SET as in the previous section.

Memory Playlists
A memory playlist is the glue that holds the whole thing together. Without it, all you would have is an open sound device and wave files sitting in memory but no way to play them. Memory playlists are simply a set of instructions. Each entry of a memory playlist consists of one command (the instruction) and up to 3 operands. Memory playlists are very flexible and offer a variety of instructions. Here are a few of interest with along with descriptions of the three operands. Pay special attention to the DATA_OPERATION.

BRANCH_OPERATION
Transfers control to another instruction in the playlist.
 * Operand 1: Ignored
 * Operand 2: The absolute instruction number in the playlist to which control is being transferred. Because the playlist is defined as an array of structures (instruction, operation, and operand values) its first instruction is referenced as array element, index 0. Therefore, the first instruction in the list is 0, the second instruction is 1, and so on.
 * Operand 3: Ignored

DATA_OPERATION
Specifies a data buffer to be played from or recorded into.
 * Operand 1: Long pointer to a buffer in the application.
 * Operand 2: Length of the buffer pointed to by Operand 1.
 * Operand 3: Current position in the buffer. This operand is updated by the system during a recording or playback operation. For a playback operation, it is the number of bytes that have been sent to the output device handler. For a recording operation, it is the number of bytes that have been placed into a user buffer.

The buffer indicated by the DATA instruction must only contain the raw data bytes from the device and cannot include any header information. For example, a buffer for a sequencer device can contain only MIDI multibyte messages, as defined by the International MIDI Association. Therefore, the precise meaning or format of the data is dependent on the current settings of the media device. For example, a wave audio data element is assumed to have the format PCM or ADPCM, number of bits per sample, and so on, that is indicated by the settings of the audio device.

The address range of a DATA statement cannot overlap the address range of any another DATA statement. However, the same DATA statement can be repeated.

EXIT_OPERATION
Indicates the end of the playlist. Using playlist instructions, you can play audio objects in succession from one or more memory buffers. Instructions include branching to and returning from subroutines within the playlist. In addition, the playlist can be modified dynamically by the application while it is being played.
 * Operand 1: Ignored
 * Operand 2: Ignored
 * Operand 3: Ignored

The MCI_OPEN_PLAYLIST flag is specified for the MCI_OPEN or MCI_LOAD command message to indicate that the pszElementName field in the MCI_OPEN_PARMS or MCI_LOAD_PARMS data structure is a pointer to a memory playlist. This has the effect of mounting the playlist onto or linking the playlist to the sound device.

For example, if we wanted to play the contents of a memory buffer the command would be DATA_OPERATION with the first and second operands being a pointer to a memory buffer and the length of the memory buffer respectively. When the MMPM/2 subsystem receives a command to play a memory playlist, it simply goes down the list and sequentially executes the commands. Changing the first and second operands dynamically enables you to play any wave file loaded into a memory buffer.

Putting the pieces together
Now that the groundwork has been laid out, let's work on building a fully functional thread thread that can easily be plugged into your already existing program and play waves when requested to do so. We will need to: We'll skip the explanation for part one here since threads are a whole different topic. The code for this is of course in the accompanying zip file.
 * 1) Spawn a thread to act as a server for all sound operations
 * 2) Read the wave files in from disk to memory
 * 3) Open the sound device and mount playlist onto it
 * 4) Accept requests to play wave files until the server is instructed to terminate
 * 5) Close the sound device and free memory buffers
 * 6) End thread

Reading in the wave files from disk to memory
We will use an array of the following structure to store information about each individual sound file. We loop through the array of structures and do the following on each iteration:

First we want to attach a file handle to the wave file so we can operate on it. Next we obtain the header information by calling mmioGetHeader which fills in the mmAudioHeader structure. Now we copy the information from the header structure into our own array of soundfile information and then malloc and copy the raw data into memory. Finally we release the file by calling mmioClose. mmioClose(hmmioFileHandle,0 );

1. Pick command
In order to open a sound device, an MCI_OPEN must be issued

3. Pick appropriate info structure and fill in relevant info.
In our case, MCI_OPEN_PARMS is the info structure: Now this is the part that links the playlist and sound device together. By making the pszElementName of the MCI_OPEN_PARMS structure point to the base address of the playlist, any operations you perform on that device (like MCI_PLAY or MCI_STOP) are performed on the playlist. The mciSendCommand essentially opens the sound device and points the playlist onto it. Note that the device id is hardcoded in as a zero. The actual device id will be returned in mciOpenParameters.usDeviceID. We're going to include one more structure to keep track of the current characteristics of the sound device. This way, if a wave files samples/sec or bits/sample do not match those of the sound device, we know to adjust the sound device. Let's copy the device id into SoundDevice so we can use it on future calls to mciSendCommand. SoundDevice.usSoundDeviceID = mciOpenParameters.usDeviceID; Now the sound device is open and ready to accept commands.

Playing Wave Files
This part of the MCI command interface works exactly like you would work your VCR or tape player. There are commands to PLAY, STOP, PAUSE, REWIND, FAST FORWARD, etc.

Given an index, usSoundFileID, into an array of SOUND_INFO structures, the first step is to modify the playlist so that it points to that waves memory buffer. This is easily done by copying the address and memory buffer size to operands one and two respectively. PlayList[0].ulOperandOne = (ULONG)SoundFiles[usSoundFileID].Address; PlayList[0].ulOperandTwo = SoundFiles[usSoundFileID].Size; Next is the check to make sure that the number of bits per sample and the number of samples per second match for the wave file and the sound device. If they are not identical, then the sound device is changed accordingly. Stop and rewind the playlist just in case a previous wave file is still playing. Now we're ready to issue an MCI_PLAY command: Notice that I neglected to send any flags for MCI_PLAY. It is advisable to send an MCI_WAIT if you want your sound to play to its entirety, but if MCI_WAIT is not sent, it is possible to receive another MCI_PLAY message while a previous sound is still playing. The resulting effect is that the previous sound is abruptly cut short and the new sound is played. Both have their place depending on how you want sounds to react to certain events.

Closing the Sound Device
This is just to make sure we tie up all the loose ends and return the memory buffers to the free memory list.

Conclusion
You should now have a basic understanding of how the MCI Command Interface works and hopefully be able to extend the supplied source code in the zipfile to fit your needs. Let's get some OS/2 apps with some dazzling sound effects out there!!