Using Multimedia OS/2 for sound

From EDM2
Jump to: navigation, search

By Roger Orr

Introduction

One of the features which has gone from being a rare item to one of the standard cards in many new PCs is a sound card. This can be a disaster if you work in an open plan office...

OS/2 WARP comes with out-of-the-box support for many standard sound cards, and allows you to configure a range of different sounds when certain actions occur, such as your desktop starting, windows closing, etc.

For example, you may want to change the noise provided by the system when you get an error message box.

This is generated by code such as this fragment:

WinMessageBox( HWND_DESKTOP, HWND_DESKTOP, "Example", NULL,
               0, MB_ERROR | MB_OK | MB_MOVEABLE );

To change the sound produced, open the following: 'OS/2 System' -> 'System Setup' -> 'Sound' and select the 'Error' line in the 'System events' list box. The default sound for this is BELLS.WAV - I prefer CUCKOO.WAV so I select it in the lower list box and close the window.

Now if I get an error I will hear a cuckoo noise. OK, simple to use and you can spend hours of time configuring your machine and annoying your friends.

But if you are a programmer you might wish to add such noises to your own programs - how can you do this? Fortunately OS/2 provides a very simple to use API for the multi-media subsystem.

The simplest Multi-Media API - using REXX

MMPM/2 (as the multi-media support for OS/2 is called) provides an MCI (Media Control Interface) which has two primary interfaces: one using strings and the other a procedural interface.

As it is the easiest to use (and can be used from REXX, C, C++, etc) I will use the string interface for this article. Most of the API is available to this interface, only some of the functions requiring pointers or other programming concepts are not available.

First of all in REXX we can use the MMPM/2 API with the REXX function 'mciRxSendString'. This takes as main argument a command string to go to the MCI.

For example,

mciRxSendString( "play myfile.wav wait", "RetStr", 0, 0 )

might be used to play a wave sound file 'myfile.wav'. That was easy, wasn't it!

I thought a very simple practical problem might be a useful example, and picked on that of inserting one sound into the middle of another to create a new, composite, sound.

I wrote a simple REXX script which does this, see below.

--------------------------- InsertRx.cmd ---------------------------

/* REXX script to insert 1st sound into middle of 2nd sound. */
/* Result is played, and also saved as 'inserted.wav'        */

parse arg file1 file2 scrap
if scrap \=  | file2 =  then do
  say 'Syntax: insert <wav1> <wav2>'
  return
  end

/* Load and initialize Multimedia REXX support */
call RXFUNCADD 'mciRxInit','MCIAPI','mciRxInit'
call mciRxInit

/* Open the default digital audio device for exclusive use */
call SendW 'open waveaudio alias wave'

/* Load the first digital audio file, check length, and copy to clipboard */
call SendW 'load wave ' || file1

RetStr = SendW( 'status wave length' )
if RetStr = 0 then do
  say 'Error: Nothing to insert from "'file1'"'
  signal terminate
  end
say 'wave length to insert =' RetStr 

call SendW 'copy wave'

/* Load the second file, check length, and paste first file into middle */
call SendW 'load wave ' || file2

RetStr = SendW( 'status wave length' )
if RetStr = 0 then do
   say 'Error: Nothing in "'file2'" to insert into'
   signal terminate
   end
say 'original wave length =' RetStr

call SendW 'seek wave to ' || RetStr/2
call SendW 'paste wave'

/* Play the multimedia file */
call SendW 'seek wave to start'
call SendW 'play wave'

/* save to disk */
call SendW 'save wave inserted.wav'

say "Inserted data saved as 'inserted.wav'"

/* Ensure proper termination of Multimedia REXX */
terminate:
call mciRxSendString 'close wave', 'RetStr', 0, 0
call mciRxExit 

exit(0)

/* SendW: issue command and wait. Terminate on error */
SendW:
  parse arg cmdstring
  rc = mciRxSendString( cmdstring || ' wait', 'RetStr', '0', '0' )

  /* Check for an error, call a function to return an error string */
  if rc <> 0 then
  do
     MacRC = mciRxGetErrorString(rc, 'ErrStVar')
     say '"'cmdstring'" failed - rc =' rc', ErrStVar =' ErrStVar
     signal terminate
  end

  return RetStr
--------------------------- InsertRx.cmd ---------------------------

The main points to highlight are: (a) You must initialise and terminate the REXX interface - see the RXFUNCADD, mciRxInit and mciRxExit calls.

(b) I have used a single function 'SendW' to wrapper the calls to mciRxSendString - I can put error handling in one place. For simplicity it jumps to 'terminate' if any error occurs.

I tested this with the following command:

pmrexx insertrx.cmd \mmos2\sounds\beeoong.wav \mmos2\sounds\eeerrupp.wav

Note: you MUST use PMREXX since this example uses the clipboard, which is only available for PM programs. If you miss off the PMREXX line you will get a message like: "paste wave" failed - rc = 70544, ErrStVar = System out of memory.

You can test REXX scripts very easily using PMREXX, and also play with the MCI string interface interactively by using the "mcistrng" program which comes with the MMPM/2 toolkit. (For example on DEVCON 7 CD #3 in \TOOLKITS\WARPTLKT\TOOLKIT\SAMPLES\MM\MCISTRNG)

The next step - using C

However, many of us are writing in C (or C++) and might want to do the same thing in C. It is quite easy - the string interface is again provided, this time as mciSendString(), which takes very similar parameters to the REXX function. One thing you will be more concerned about with a C program is the synchronous nature of the MCI interface when used with 'wait'. If you start playing a 30 second piece you do not want to freeze your application (or the entire user interface) while you do so.

A general solution is to make use of the asynchronous commands, and let MMPM/2 notify you when a command finishes or a cue point is reached. I decided for this program to continue to use the synchronous ('wait') commands but to use two threads, one for the user interface and one for the MCI interface. This maintains the responsiveness to the user while the background thread is working on the sounds.

---------------------------- InsertC.c ----------------------------

/* C program to insert 1st sound into middle of 2nd sound. */
/* Result is played, and also saved as 'inserted.wav'      */ 

#define INCL_MCIOS2
#define INCL_WIN
#include <os2.h>
#include <os2me.h>

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>

HWND hwndList = 0;
char RetStr[ 100 ] = "";

/* lprintf: "printf" to the list box */
void lprintf( char *fmt, ... )
  {
  char buff[ 256 ];
  va_list argptr;
  va_start( argptr, fmt );
  vsprintf( buff, fmt, argptr );
  WinInsertLboxItem( hwndList, LIT_END, buff );
  va_end( argptr );
  }

/* Send: issue a command, return status in RetStr */
int Send( char *cmdstr )
  {
  ULONG rc = mciSendString( cmdstr, RetStr, sizeof( RetStr ), 0, 0 );
  if ( rc != 0 )
     {
     char ErrStVar[ 100 ];
     mciGetErrorString( rc, ErrStVar, sizeof( ErrStVar ) );
     lprintf( "rc = %u, ErrStVar = %s for \"%s\"", rc, ErrStVar, cmdstr );
     }

  return (int)rc;
  }

/* mciThread: thread function to do the MCI work */
extern void _Optlink mciThread( void *pvoid )
  {
  HAB hab = WinInitialize( 0 );
  HMQ hmq = WinCreateMsgQueue( hab, 0 );
  char **argv = (char **)pvoid;
  char cmd[ 100 ] = "";
  int rc = 0;

  /* Open the default digital audio device for exclusive use */
  rc = Send( "open waveaudio alias wave wait" );
  lprintf( "Open return code: %u", rc );

  /* Load the first digital audio file */
  sprintf( cmd, "load wave %s wait", argv[1] );
  rc = Send( cmd );

  rc = Send( "copy wave wait" );
  if ( rc == 0 )
     {
     Send( "status wave length wait" );

     if ( strcmp( RetStr, "0" ) == 0 )
        {
        lprintf( "Error: Nothing to insert from \"%s\"", argv[1] );
        rc = 1;
        }
     else
        {
        lprintf( "wave length to insert = %s", RetStr );
        sprintf( cmd, "load wave %s wait", argv[2] );
        rc = Send( cmd );
        Send( "status wave length wait" );
        if ( strcmp( RetStr, "0" ) == 0 )
           {
           lprintf( "Error: Nothing in \"%s\" to insert into", argv[2] );
           rc = 1;
           }
        else
           lprintf( "original wave length = %s", RetStr );
        }
     }

  if ( rc == 0 )
     {
     sprintf( cmd, "seek wave to %u wait", atoi( RetStr ) / 2 );
     rc = Send( cmd );
     rc = Send( "paste wave wait" );
     }

  if ( rc == 0 )
     {
     Send( "status wave length wait" );
     lprintf( "inserted wave length = %s", RetStr );
     Send( "seek wave to start wait" );

     /* Play the multimedia file, wait for completion */
     rc = Send( "play wave wait" );

     /* save the wave to disk */
     rc = Send( "save wave inserted.wav wait" );
     if ( rc == 0 )
        lprintf( "Inserted data saved as 'inserted.wav'" );
     }

  /* Close the device context */
  Send( "close wave" );

  WinDestroyMsgQueue( hmq );
  WinTerminate( hab );
  }

int main( int argc, char **argv )
  {
  HAB hab = WinInitialize( 0 );
  HMQ hmq = WinCreateMsgQueue( hab, 0 );
  HWND hwnd = 0;
  ULONG fcf = FCF_STANDARD &~ FCF_ICON &~ FCF_MENU &~ FCF_ACCELTABLE;

  if ( argc != 3 )
     {
     WinMessageBox( HWND_DESKTOP, HWND_DESKTOP,
        "Syntax: insert <wav1> <"wav2>",
        "InsertC", 0, MB_OK | MB_ERROR | MB_MOVEABLE );
     return 1;
     }

  hwnd = WinCreateStdWindow( HWND_DESKTOP, WS_VISIBLE, &fcf, WC_LISTBOX,
     "Insert", 0, 0, 0, &hwndList );

  if ( hwnd )
     {
     QMSG qmsg = {0};

     _beginthread( mciThread, NULL, 16384, argv );

     while ( WinGetMsg( hab, &qmsg, 0, 0, 0 ) )
        WinDispatchMsg( hab, &qmsg );
     WinDestroyWindow( hwnd );
     }

  WinDestroyMsgQueue( hmq );
  WinTerminate( hab );
  return 0;
  }
---------------------------- InsertC.c ----------------------------

I compiled the program with the IBM C/Set++ compiler as:

icc /Gm /B"/pm:pm" insertc.c mmpm2.lib

The main points to highlight from the C program are:

(a) The two lines at the top: #define INCL_MCIOS2 #include <os2me.h> are required to define the MMPM/2 MCI interface.
(b) I am using a listbox as the client window for simplicity, and lprintf() simply adds lines to the listbox. (This can be a useful technique for test programs!)
(c) I have used a global variable 'RetStr' for readability.

The program basically does the same thing as the REXX script.

For more complex ideas or more control over the interface you might need to look at the procedural interface, but there is quite a lot you can do even with the simple string interface.

Conclusion

MMPM/2 gives you a simple way to start to experiment with sounds. There are many places where using a sound file rather than merely beeping might aid in program usability or support, and I would encourage you to try out some of the ideas in this article.

Once you have an example of the use of sound I hope you will look further into MMPM/2 - perhaps using the online help (MMPM programming reference or Multimedia with REXX) and/or the sample code in the toolkit.

Roger Orr 31 Aug 1995