Adding Sound to Your OS/2 Application

From EDM2
Revision as of 20:55, 19 March 2018 by Ak120 (Talk | contribs)

Jump to: navigation, search

Written by Semir Patel

Introduction

As PC based systems become cheaper and cheaper, soundcards are fastly 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.

// open/acquire sound device and give it the alias "soundcard"
open waveaudio alias soundcard

// load sound file onto device (read into memory) 
load soundcard sample.wav
// play whatever is loaded onto device
play soundcard
// close/release sound device
close soundcard

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:

Command Description Valid Flags Flag Desc. Info Structure.
MCI_CLOSE Closes the sound device None None None
MCI_SET Use to set device info MCI_WAVE_SET_SAMPLESPERSEC Set samples per second MCI_WAVE_SET_PARMS
(MCI_SET) Use to set device info MCI_WAVE_SET_BITSPERSAMPLE Set bits per sample (8 or 16) MCI_WAVE _SET_PARMS
MCI_STOP Stop the audio playback None None None
MCI_SEEK Change pos. in wave file MCI_TO Seek to specified pos. MCI_SEEK_PARMS
(MCI_SEEK) Change pos. to begin of file MCI_TO_START Seek to beginning MCI_SEEK_PARMS
(MCI_SEEK) Change pos. to end of file MCI_TO_END Seek to end MCI_SEEK_PARMS
MCI_PLAY Signals the device to start play MCI_FROM Play starting from this pos. MCI_PLAY_PARMS
(MCI_PLAY) Signals the device to end play MCI_TO Play to this pos. MCI_PLAY_PARMS

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:

  • Decide on the command
  • OR together the appropriate flags for your desired command
  • Fill in the appropriate info structure with only the relevant information

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 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.

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 */

AddSound-sound1.gif

typedef struct _MMAUDIOHEADER {
        ULONG            ulHeaderLength;
        ULONG            ulContentType;
        ULONG            ulMediaType;
        MMXWAV_HEADER    mmXWAVHeader;
        } MMAUDIOHEADER;

typedef struct _MMXWAV_HEADER {
        WAVE_HEADER        WAVEHeader;
        XWAV_HEADERINFO    XWAVHeaderInfo;
        } MMXWAV_HEADER;

typedef struct _WAVE_HEADER {
        USHORT    usFormatTag;
        USHORT    usChannels;           /* mono,stereo or quad */
        ULONG     ulSamplesPerSec;      /* samples per second */
        ULONG     ulAvgBytesPerSec;
        USHORT    usBlockAlign;
        USHORT    usBitsPerSample;      /* bits per sample */
        } WAVE_HEADER;

typedef struct _XWAV_HEADERINFO {
        ULONG    ulAudioLengthInMS;
        ULONG    ulAudioLengthInBytes;  /* length of raw data */
        ULONG    ulReserved;
        } XWAV_HEADERINFO;

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.

/* 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_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.

Operand 1
Ignored
Operand 2
Ignored
Operand 3
Ignored

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 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:

  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

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 memory

We 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 =
     mmAudioHeader.mmXWAVHeader.XWAVHeaderInfo.ulAudioLengthInBytes;
SoundFiles[usSoundFileID].ulSamplesPerSec = 
     mmAudioHeader.mmXWAVHeader.WAVEHeader.ulSamplesPerSec;
SoundFiles[usSoundFileID].usBitsPerSample = 
     mmAudioHeader.mmXWAVHeader.WAVEHeader.usBitsPerSample;
SoundFiles[usSoundFileID].Address = (LONG *)
malloc(SoundFiles[usSoundFileID].ulSize);
mmioRead(hmmioFileHandle,               /* handle of source */
(PSZ)SoundFiles[usSoundFileID].Address, /* ptr to destination */
SoundFiles[usSoundFileID].ulSize);      /* amount to copy */

Finally we release the file by calling mmioClose().

mmioClose(hmmioFileHandle,0 );

Let's go through the 3 steps before calling mciSendCommand().

1. Pick command

In order to open a sound device, an MCI_OPEN must be issued

2. OR flags together

Command Flags
ulOpenFlags =
Function
ULONG MCI_WAIT wait for function to return
MCI_OPEN_PLAYLIST this flag says that you are mounting playlist onto the sound device
the address of the playlist resides in the info structure
MCI_OPEN_TYPE_ID you are specifying a device type in the info structure
MCI_OPEN_SHAREABLE if another process requests the sound device, you are willing to give it up

3. Pick appropriate info structure and fill in relevant info.

In our case, MCI_OPEN_PARMS is the info structure:

typedef 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 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.

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 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.

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 Device

This 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*/

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!!