![]() |
Adding Sound to Your OS/2 ApplicationWritten by Semir Patel |
IntroductionAs PC based systems become cheaper and cheaper, soundcards are fastly becoming a standard peripheral just like your hard disk or keyboard. Todays PC's, are for the most part, multimedia capable complete with 16bit sound cards and a cdrom 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 thead 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:
The MCI String and Command InterfacesThe 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 comminuicate 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 centers 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.
The MCI Command InterfaceYou 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.
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:
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: MCI_WAVE_SET_PARMS mwspWaveFormParameters; /* info struc for MCI_SET command*/ mwspWaveFormParameters.ulSamplesPerSec = 11025; mwspWaveFormParameters.usBitsPerSample = 8; mciSendCommand (SoundDevice.usSoundDeviceID, /* sound device id. more details on this later */ MCI_SET, /* command to set device */ MCI_WAIT, /* OR'ed flags */ MCI_WAVE_SET_SAMPLESPERSEC, MCI_WAVE_SET_BITSPERSAMPLE, (PVOID)&mwspWaveFormParameters, /* info structure */ 0); /* no notification message*/ Wave Audio StructuresAs 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 throught mmioOpen() will extract that information for us. Comments are beside the fields that are of interest to us. mmioGetHeader /* file handle returned by mmioOpen */ pHeader, /* pointer to header structure */ usHeaderLength, /* size of header structure */ plBytesRead, /* returns # bytes read into header */ ulReserved, /* reserved */ ulFlags); /* reserved */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 PlaylistsA 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. /* playlist structure */ typedef struct pls { ULONG ulCommand; ULONG ulOperandOne; ULONG ulOperandTwo; ULONG ulOperandThree; } PLAY_LIST_STRUCTURE; 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_OPERATIONTransfers control to another instruction in the playlist.
DATA_OPERATIONSpecifies a data buffer to be played from or recorded into.
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_OPERATIONIndicates 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. 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. /* example declaration for a memory playlist with 2 commands and */ /* nulled out operands */ PLAY_LIST_STRUCTURE PlayList [NUMBER_OF_COMMANDS] = { DATA_OPERATION, 0, 0, 0, /* play command */ EXIT_OPERATION, 0, 0, 0 /* terminate playlist */ }; PlayList[0].ulOperandOne = (ULONG)&MemoryBufferWhereRawWaveDataIsStored; PlayList[0].ulOperandTwo = (ULONG)SizeofMemoryBuffer; Now an MCI_PLAY would play the playlist. mciSendCommand (SoundDevice.usSoundDeviceID, MCI_PLAY, MCI_WAIT, (PVOID)&mciOpenParameters, 0); Putting the pieces togetherNow 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. Reading in the wave files from disk to memoryWe will use an array of the following structure to store information about each individual sound file. struct SOUNDINFO{ char szFileName[255]; /* filename..already initialized */ LONG *Address; /* pointer to this wav's memory buffer */ ULONG ulSize; /* size of the wave file */ ULONG ulSamplesPerSec; /* samples per second */ USHORT usBitsPerSample /* bits per sample */ }; struct SOUNDINFO SoundFiles[NUMBER_OF_WAVES];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. hmmioFileHandle = mmioOpen (acFileName, /* file name */ (PMMIOINFO)NULL, /* extra params not needed */ MMIO_READ); /* opens file for reading only */Next we obtain the header information by calling mmioGetHeader() which fills in the mmAudioHeader structure. mmioGetHeader (hmmioFileHandle, /* file handle */ (PVOID)&mmAudioHeader, /* info copied into this struc */ sizeof (MMAUDIOHEADER),/* size of header structure */ (PLONG) &ulBytesRead, /* # bytes read into header */ (ULONG) NULL, /* reserved */ (ULONG) NULL); /* reserved */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. SoundFiles[usSoundFileID].ulSize =Finally we release the file by calling mmioClose(). mmioClose(hmmioFileHandle,0 ); Let's go through the 3 steps before calling mciSendCommand().1. Pick commandIn order to open a sound device, an MCI_OPEN must be issued 2. OR flags together
3. Pick appropriate info structure and fill in relevant info. In our case, MCI_OPEN_PARMS is the info structuretypedef struct MCI_OPEN_PARMS { DWORD hwndCallback /* Window handle */ WORD usDeviceID; /* Device ID */ WORD usReserved0; /* Reserved */ PSZ pszDeviceType; /* Device type */ PSZ pszElementName; /* Element name */ PSZ pszAlias; /* Device alias */ } MCI_OPEN_PARMS; MCI_OPEN_PARMS mciOpenParameters; mciOpenParameters.pszDeviceType = (PSZ) MAKEULONG (MCI_DEVTYPE_WAVEFORM_AUDIO, 1 ); /* specifies that we want a waveform audio device */ mciOpenParameters.hwndCallback = (HWND) NULL; /* no callback window */ mciOpenParameters.pszAlias = (CHAR) NULL; /* no alias */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 moints 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. mciOpenParameters.pszElementName = (PSZ)PlayList[0]; mciSendCommand( 0, /* We don't know the device yet. */ MCI_OPEN, /* MCI message. */ ulOpenFlags, /* Flags for the MCI message. */ (PVOID)&mciOpenParameters, /* Parameters for the message. */ 0); /* Parameter for notify message. */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. struct SOUNDDEVICE { USHORT usSoundDeviceID; /* device id */ ULONG ulSamplesPerSec; /* samps/sec */ USHORT usBitsPerSample; /* bits/samp */ }; struct SOUNDDEVICE SoundDevice;Let's copy the 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 FilesThis 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. MCI_WAVE_SET_PARMS mwspWaveFormParameters; if ((SoundFiles[usSoundFileID].ulSamplesPerSec != SoundDevice.ulSamplesPerSec) || (SoundFiles[usSoundFileID].usBitsPerSample != SoundDevice.usBitsPerSample)) { /* null out structure */ memset( &mwspWaveFormParameters, 0, sizeof(mwspWaveFormParameters)); /* update sound device info */ SoundDevice.ulSamplesPerSec = SoundFiles[usSoundFileID].ulSamplesPerSec; SoundDevice.usBitsPerSample = SoundFiles[usSoundFileID].usBitsPerSample; /* setup the info structure for an MCI_SET */ mwspWaveFormParameters.ulSamplesPerSec = SoundDevice.ulSamplesPerSec; mwspWaveFormParameters.usBitsPerSample = SoundDevice.usBitsPerSample; mciSendCommand(SoundDevice.usSoundDeviceID, /* Device to play the waves. */ MCI_SET, /* MCI message. */ MCI_WAIT | MCI_WAVE_SET_SAMPLESPERSEC | /* Flags for the MCI message. */ MCI_WAVE_SET_BITSPERSAMPLE, (PVOID) &mwspWaveFormParameters, /* Parameters for the message. */ 0); /* Parameter for notify message. */ }Stop and rewind the playlist just in case a previous wave file is still playing. mciSendCommand (SoundDevice.usSoundDeviceID, /*Device to play the waves*/ MCI_STOP, /*MCI Message*/ MCI_WAIT, /*Flags for the MCI message*/ (PVOID) &mciOpenParameters, /*Parameters for the message*/ 0); /*Parameter for notify message*/ mciSendCommand (SoundDevice.usSoundDeviceID, /*Device to play the waves.*/ MCI_SEEK, /*MCI message. */ MCI_WAIT | MCI_TO_START, /*Flags for the MCI message.*/ (PVOID) &mciOpenParameters, /*Parameters for the message.*/ 0); /*Parameter for notify message.*/Now we're ready to issue an MCI_PLAY command mciSendCommand (SoundDevice.usSoundDeviceID, /*Device to play the waves.*/ MCI_PLAY, /*MCI message.*/ 0, /*Flags for the MCI message.*/ (PVOID) &mciOpenParameters, /*Parameters for the message.*/ 0); /*Parameter for notify message.*/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 prevoius sound is still playing. The resulting effect is that the prevoius 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 DeviceThis is just to make sure we tie up all the loose ends and return the memory buffers to the free memory list. for( usCounter=0; usCounter<NUMBER_OF_WAVES; usCounter++ ) free( (VOID *) SoundFiles[usCounter].Address); /* close sound device */ mciSendCommand(usSoundDeviceID, /*Device to play the chimes*/ MCI_CLOSE, /*MCI message*/ MCI_WAIT, /*Flags for MCI message*/ (ULONG) NULL, /*Parameters for the message*/ (ULONG) NULL); /*Parameter for notify*/ ConclusionYou 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!! |