Making Noise with MMPM/2 - Part 2

From EDM2
Revision as of 12:57, 2 November 2018 by Ak120 (Talk | contribs)

Jump to: navigation, search
Making Noise with MMPM/2 Part 1|Part 2
Edm2-mmpm2.png

by Marc van Woerkom

Introduction

This is the second article on MMPM/2 programming. It was originally planned to be the final one, but two reasons forced me to extend this series:

  1. Semir Patel's fine article in volume 2, issue 1 of EDM/2 already touched most topics I intended to present here, blasting my concept. (Watch out for my upcoming DOOM-style game, featuring a certain editor)
  2. There is so much to cover about MMPM/2 that two parts are simply not enough.

Limitations of Using REXX

When I delved further into MMPM/2 programming it became clear to me that the features accessible from the REXX Media Control Interface (MCI) are very powerful but that there is still more to see, much like the part of an iceberg lying under water. After all, let's face the music (pun intended); MMPM/2 is written in C meaning all of the APIs will be accessible from C. The only APIs provided for REXX are:

  • mciSendString (mciRxSendString)
  • mciGetErrorString (mciRxGetErrorString)
  • mciGetDeviceID (mciRxGetDeviceID)

Compare this with the near 200 MMPM/2 API calls accessible through C/C++.

To be fair, it should be possible to close the gaps since VX-REXX 2.0 from Watcom seems to have multimedia support. However, one must stick to C/C++ if one wants...

  • to get complex feedback (be it a whole structure of information, or synchronization messages)
  • to use the basic Multimedia I/O subsystem (MMIO API)
  • to control streaming and synchronization through the SPI subsystem
  • to use the PM extensions provided by MMPM/2 (i.e. new graphical controls and secondary window support)

This doesn't even include esoteric business like driver development.

After all of this bashing on poor REXX, let's close this section with the biggest advantage of REXX:

It's pretty easy to use and provides a great introduction!

Please read the first part of this series for more information on this topic.

Preliminary Thoughts

Before you start MMPM/2 programming, you need a C or C++ compiler and MMPM/2 bindings. I recommend using either:

  • one of the IBM compilers (C Set++, Firststep) together with the Developer's Toolkit and the MMPM/2 Toolkit (which has been included in the Developer's Toolkit since the 2.1 release)
  • or, the EMX port of the GNU GCC C/C++/Objective C compiler (revisions 0.8h and 2.5.8 or later) together with the MM4EMX package (revision 1.0).

Although this should suffice to recompile the examples given, and you'll get a lot of information from here, you still should acquire some of the literature mentioned at the end of the first part of this series and at the end of this part.

Another great source of information are the computer networks. Often you can contact someone on the Internet news groups or CompuServe fora who can help you or share thoughts with you.

For those with Usenet access I recommend comp.os.os2.multimedia, or (if you somehow feel an urge to polish your German a little bit :-) maus.os2.prog.

So much for the basic preparations. (Hmm...did I already tell you to install MMPM/2?)

Multimedia Programming Using EMX

To state it shortly - it's no problem to do any MMPM/2 programming with EMX.

As an EMX user you have the choice between using the OS/2 and MMPM/2 headers and libraries from the IBM Toolkits or using those of the EMX and MM4EMX packages. Both have their strengths and weaknesses. However, this article series should feature nothing which is specific to one of the above combinations, or even the EMX system.

The MM4EMX Package

The Multimedia for EMX package (MM4EMX) is a freeware package for the EMX development environment. It contains all necessary 32-bit header files and import libraries in the BSD .A and Intel OMF .LIB formats together with source code.

The samples presented and explained in this part of the article series are the ones from MM4EMX (release 1.0). As a side note, please contact the author if you should note incompatibilities.

The Textual Media Control Interface

The easiest way to use the MCI is via its textual interface, using the mciSendString() API call. Below is an example of this.

#
# bach.mci -- play the Bach MIDI file
#
open Sequencer alias midi2 shareable wait
info midi2 product wait
set midi2 time format mmtime wait
load midi2 c:\mmos2\sounds\bach.mid wait
status midi2 length wait
play midi2 wait
close midi2

It opens the Sequencer (MIDI) multimedia device, giving it the alias "midi2". The product info is queried and mmtime is chosen as time format. Then a MIDI file from Bach is loaded into the context of the device, its length is queried and it gets played. Finally the device is closed.

The following example will take a file of MCI command strings and sends them line by line for execution to the MDM via mciSendString(), thus interpreting the file.

//////////////////////////////////////////
// mci -- an interpreter for MCI scripts
//
// using emx 0.8h, mm4emx 1.0
//
//     based on a C program by
//     John J. McDonough
//
// Marc E.E. van Woerkom, 2/94

#include <iostream.h>
#include <fstream.h>

#include <os2.h>

One has to define INCL_OS2MM to include the proper definitions and declarations, i.e. the 32-bit version of the MMPM/2 API with naming conventions conforming to OS/2.

A defined INCL_MCIOS2 will include the MCI support.

#define  INCL_OS2MM
#define  INCL_MCIOS2
#include <os2me.h>

// this sample demonstrates the following MMPM/2 API's:
//
//     mciSendString()      send a MCI command string, receive an answer
//     mciGetErrorString()  look up the error string for the error code

int main(int argc, char* argv[])
{
    if ( argc != 2 ) {
        cerr << "usage: mci <filename>\n";
        return 1;
    }

    ifstream infile(argv[1]);
    if (!infile) {
        cerr << "error: can't open input file " << argv[1] << '\n';
        return 2;
    }

    int line = 0;

    const bsize = 128;
    char buff[bsize];

    while (!infile.eof()) {
        char c = infile.peek();  // peek one char forward

        if (infile.good()) {

            infile.get(buff, bsize, '\n');

            cout.width(3);
            cout << ++line << ": [" << buff << "]\n";

            if (c != '#') {

Now we finally got a non-comment line into the variable buff.

The next thing to do is to provide a return buffer for mciSendString(), which has to be empty.

   const rsize = 128;
   char rbuff[rsize];
   for (int i=0; i<rsize; i++) rbuff[i] = 0;

   ULONG rc = mciSendString(buff,   // buffer with MCI string
                            rbuff,  // return buffer
                            rsize,  // rbuff size
                            0,      // no callback window handle
                            0);     // no user parameter

   if (rc == MCIERR_SUCCESS) {
       if (rbuff[0])
           cout << "      -> " << rbuff << "\n\n";
   }
   else {

The return code wasn't MCIERR_SUCCESS, so something strange happened. Because an error code is not too enlightening, we use mciGetErrorString() to get a nice string.

  ULONG rc2 = mciGetErrorString(rc,      // error code
                                rbuff,   // return buffer
                                rsize);  // rbuff size

In case the rc is out of range we give up and display the number.

  if (rc2 == MCIERR_SUCCESS)
    cerr << "      -> MCI error: " << rbuff << "\n\n";
  else
    cerr << "      -> error #" << rc << " has occured!\n\n";
    }
  }
  }
    infile.get(c);  // eat the \r after the \n
  }
  return 0;
}

All of this is pretty straight forward. Now we have a tool to try out MCI command strings quick and easy. Below are some other MCI scripts:

#
# boing.mci -- play boing.wav
#
open waveaudio alias wave shareable wait
load wave c:\mmos2\sounds\boing.wav wait
play wave wait
close wave wait

This example shows how the audio CD player applet recognizes CDs, it checks for the 8 byte long ID:

#
# playcd.mci -- play an audio CD
#
open cdaudio01 alias cdda shareable wait
status cdda media present wait
status cdda mode wait
set cdda time format tmsf wait
status cdda volume wait
status cdda number of tracks wait
status cdda length wait
status cdda type track 1 wait
# check unique ID (8 bytes)
info cdda ID wait
# check unique UPC (bcd number)
info cdda UPC wait
seek cdda to start wait
# this provokes an error, for there is no window to notify
play cdda notify
play cdda wait
close cdda wait

The Procedural Media Control Interface

While the ease of the textual MCI is unbeatable, it has some weaknesses in comparison to the procedural MCI.

  • It has to be translated (parsed) internally which needs time.
  • Anything that goes beyond a text string can't be returned as a result.

The following example uses the procedural interface to query an audio CD for it's table of contents.

////////////////////////////////
// cdtoc - audio CD toc sample
//
// using emx 0.8h, mm4emx 1.0
//
//
// Marc E.E. van Woerkom, 2/94
//

#include <os2.h>

#define  INCL_OS2MM
#define  INCL_MCIOS2
#include <os2me.h>

#include <iostream.h>
#include <iomanip.h>

//
// mci_err: translate the MCI return code into an error string
//

void mci_err(ULONG rc)
{
    const rsize = 128;
    char rbuff[rsize];

    ULONG rc2 = mciGetErrorString(rc,      // error code
                                  rbuff,   // return buffer
                                  rsize);  // rbuff size

    if (rc2 == MCIERR_SUCCESS)
        cerr << "MCI error: " << rbuff << "\n\n";
    else
        cerr << "error #" << rc << " has occured!\n\n";
}

This function prints out a time given in MMTIME format. It employs the helper macros ULONG_LWLB, ULONG_LWHB and ULONG_HWLB to get the information out of the ULONG.

//
// print_mmtime: print time given in MMTIME as hh.mm.ss
//

void print_mmtime(ULONG mmtime)
{
    ULONG hms = HMSFROMMM(mmtime);

    // hms packing is: |--|ss|mm|hh|

    int hour = int(ULONG_LWLB(hms));
    int min  = int(ULONG_LWHB(hms));
    int sec  = int(ULONG_HWLB(hms));

    if (hour)
        cout << setw(4) << setfill('0')
             << hour << '.'
             << setfill('0');
    else
        cout << setfill(' ');  // I believe this shouldn't be neccessary

    cout << setw(2) << min << '.';

    cout << setw(2) << setfill('0')
         << sec
         << setfill(' ');       // this neither
}

//
// main
//

int main()
{
    cout << "cdtoc -- Audio CD Table of Contents\n\n";

    // open the audio CD device

Each MCI command has its specific parameter structure. All unused fields should be set to 0.

The device to work with (CDaudio) is identified via its name.

    MCI_OPEN_PARMS mop;

    mop.hwndCallback = 0;
    mop.usDeviceID = 0;
    mop.pszDeviceType = MCI_DEVTYPE_CD_AUDIO_NAME;
    mop.pszElementName = 0;

The first parameter of mciSendCommand() is the ID of the device. In this special case it's not necessary - the ID is returned to a field of the parameter structure.

Next is a message specifying the command, MCI_OPEN in this case.

The third parameter contains some so-called message flags. Here they tell the MDM to wait until the open action is completed and to open it in shared mode.

Then a pointer to the parameter structure is given.

The last parameter is usually 0.

    ULONG rc = mciSendCommand(0,
               MCI_OPEN,                       // open message
               MCI_WAIT | MCI_OPEN_SHAREABLE,  // message flags
               &mop,                           // parameters
               0);

    if (rc != MCIERR_SUCCESS) {
        mci_err(rc);
        return 1;
    }

Now we issue a GETTOC command. It's not accessible from the textual MCI, because it returns a bunch of information that doesn't fit into a simple line of text. (Let's hope there is no CD with more than 99 tracks.)

    // ask for the table of contents

    const MAXTOCRECS = 99;
    MCI_TOC_REC mtr[MAXTOCRECS];

    MCI_TOC_PARMS mtp;

    mtp.hwndCallback = 0;
    mtp.pBuf = mtr;
    mtp.ulBufSize = sizeof(mtr);

Note that this time (like in most cases) the device to work with is specified via the ID obtained from MCI_OPEN

    rc = mciSendCommand(mop.usDeviceID,  // device ID
                        MCI_GETTOC,      // get toc message
                        MCI_WAIT,        // message flags
                        &mtp,            // parameters
                        0);

    if (rc != MCIERR_SUCCESS) mci_err(rc);

    // close the device

MCI_CLOSE doesn't have any special parameters, so the parameter structure is of the type MCI_GENERIC_PARMS:

    MCI_GENERIC_PARMS mgp;
    mgp.hwndCallback = 0;

    ULONG rc2 = mciSendCommand(mop.usDeviceID, MCI_CLOSE, MCI_WAIT, &mgp, 0);

    if (rc2 != MCIERR_SUCCESS) mci_err(rc2);

    // now show the TOC, if been successful

    if (rc == MCIERR_SUCCESS) {

OK, MCI_GETTOC was successful, so print out the obtained toc entries:

        int i = 0;

        while (mtr[i].TrackNum) {
            cout << "Track" << setw(3);
            cout << int(mtr[i].TrackNum)
                 << ":  Length ";
            print_mmtime(mtr[i].ulEndAddr - mtr[i].ulStartAddr);
            cout << "  [";
            print_mmtime(mtr[i].ulStartAddr);
            cout << " to ";
            print_mmtime(mtr[i].ulEndAddr);
            cout << "]  Control " << int(mtr[i].Control)
                 << ", Country " << mtr[i].usCountry
                 << ", Owner " << mtr[i].ulOwner
                 << ", #" << mtr[i].ulSerialNum << "\n";
            i++;
        }
    }
 
    // that's all folks!

    return 0;
}

Now let's try CDTOC.EXE on a certain audio CD with "Gorgeous Gals", "Transylvanian Parties" etc. :-)

cdtoc -- Audio CD Table of Contents

Track  1:  Length  4.32  [ 0.02 to  4.34]  Control 1, Country 0, Owner 0, #0
Track  2:  Length  2.46  [ 4.34 to  7.21]  Control 1, Country 0, Owner 0, #0
Track  3:  Length  2.45  [ 7.21 to 10.06]  Control 1, Country 0, Owner 0, #0
Track  4:  Length  3.19  [10.06 to 13.26]  Control 1, Country 0, Owner 0, #0
Track  5:  Length  3.24  [13.26 to 16.50]  Control 1, Country 0, Owner 0, #0
Track  6:  Length  2.12  [16.50 to 19.02]  Control 1, Country 0, Owner 0, #0
Track  7:  Length  3.04  [19.02 to 22.07]  Control 1, Country 0, Owner 0, #0
Track  8:  Length  1.48  [22.07 to 23.55]  Control 1, Country 0, Owner 0, #0
Track  9:  Length  2.31  [23.55 to 26.27]  Control 1, Country 0, Owner 0, #0
Track 10:  Length  2.46  [26.27 to 29.14]  Control 1, Country 0, Owner 0, #0
Track 11:  Length  8.19  [29.14 to 37.33]  Control 1, Country 0, Owner 0, #0
Track 12:  Length  2.54  [37.33 to 40.28]  Control 1, Country 0, Owner 0, #0
Track 13:  Length  3.04  [40.28 to 43.32]  Control 1, Country 0, Owner 0, #0
Track 14:  Length  1.31  [43.32 to 45.04]  Control 1, Country 0, Owner 0, #0

Yup, this is the table of contents of the Rocky Horror Picture Show soundtrack! I chose this CD because track 11 has 3 subtracks (2.46, 3.34, 1.53). However they don't show up here, so I have to guess further what the Control field is good for ("track control field").

Querying MMPM/2 System Values

Remember PM's WinQuerySysValue() API call? MMPM/2 has a counterpart named mciQuerySysValue(). But in contrast to PM there are only less than a dozen MMPM/2 system values defined, which the following example displays.

///////////////////////////////////////////
// mmpmvals - MMPM/2 system values sample
//
// using emx 0.8h, mm4emx 1.0
//
//
// Marc E.E. van Woerkom, 2/94
//

#include <os2.h>

#define  INCL_OS2MM
#define  INCL_MCIOS2
#include <os2me.h>

#include <iostream.h>

//
// main
//

int main()
{
    cout << "mmpmvals -- MMPM/2 System Values\n\n";

    BOOL ClosedCaption;
    mciQuerySysValue(MSV_CLOSEDCAPTION, &ClosedCaption);
    cout << "MSV_CLOSEDCAPTION  :  " << int(ClosedCaption) << "\n";

    ULONG MasterVolume;
    mciQuerySysValue(MSV_MASTERVOLUME, &MasterVolume);
    cout << "MSV_MASTERVOLUME   :  " << int(MasterVolume) << "\n";

    ULONG Headphones;
    mciQuerySysValue(MSV_HEADPHONES, &Headphones);
    cout << "MSV_HEADPHONES     :  " << int(Headphones) << "\n";

    ULONG Speakers;
    mciQuerySysValue(MSV_SPEAKERS, &Speakers);
    cout << "MSV_SPEAKERS       :  " << int(Speakers) << "\n";

    CHAR WorkPath[CCHMAXPATH];
    mciQuerySysValue(MSV_WORKPATH, WorkPath);
    cout << "MSV_WORKPATH       :  " << WorkPath << "\n";

    ULONG SysqOsValue;
    mciQuerySysValue(MSV_SYSQOSVALUE, &SysqOsValue);
    cout << "MSV_SYSQOSVALUE    :  " << int(SysqOsValue) << "\n";

    ULONG SysqOsErrorFlag;
    mciQuerySysValue(MSV_SYSQOSERRORFLAG, &SysqOsErrorFlag);
    cout << "MSV_SYSQOSERRORFLAG:  " << int(SysqOsErrorFlag) << "\n";

    // that's all folks!

    return 0;
}

Running MMPMVALS.EXE on my system yields:

mmpmvals -- MMPM/2 System Values

MSV_CLOSEDCAPTION  :  1
MSV_MASTERVOLUME   :  100
MSV_HEADPHONES     :  1
MSV_SPEAKERS       :  1
MSV_WORKPATH       :  C:\MMOS2
MSV_SYSQOSVALUE    :  65537
MSV_SYSQOSERRORFLAG:  2

A First Rendezvous with the MMIO Subsystem

The MMIO subsystem is responsible for I/O on multimedia files. It comes with several I/O procedures installed, which can handle specific kinds of data. The example below will show the installed MMIO procedures.

//////////////////////////////////////////
// mmiofmts - MMPM/2 mmio formats sample
//
// using emx 0.8h, mm4emx 1.0
//
//
// Marc E.E. van Woerkom, 2/94
//

#include <os2.h>

#define  INCL_OS2MM
#include <os2me.h>

#include <iostream.h>
#include <iomanip.h>

Wrap MMFORMATINFO into a C++ class.

//
// mmformatinfo
//

class mmformatinfo {
    MMFORMATINFO mmfi;
public:
    mmformatinfo(FOURCC IOProc=0);
    MMFORMATINFO* get_addr() { return &mmfi; }
    LONG          get_NameLength() { return mmfi.lNameLength; }

    PSZ           get_DefaultFormatExt() { return mmfi.szDefaultFormatExt;
}

    FOURCC        get_IOProc() { return mmfi.fccIOProc; }
};

This constructor clears the MMFORMATINFO structure and initializes the fccIOProc field with the proper four character code specific to the MMIO procedure and the format it handles. Use 0 as default argument.

mmformatinfo::mmformatinfo(FOURCC IOProc=0)
{
    char* p = (char*) &mmfi;
    for (int i=0; i<sizeof(mmfi); i++) p[i] = 0;

    mmfi.fccIOProc = IOProc;
}

//
// mmio_err: translate MMIO error code into a string
//

void mmio_err(ULONG rc)
{
    cerr << "MMIO error: ";

    char* s;

    switch (rc) {
    case MMIO_SUCCESS:
        s = "SUCCESS (huh?)";
        break;
    case MMIOERR_UNBUFFERED:
        s = "UNBUFFERD";
        break;
    case MMIOERR_INVALID_HANDLE:
        s = "INVALID HANDLE";
        break;
    case MMIOERR_INVALID_PARAMETER:
        s = "INVALID PARAMETER";
        break;
    case MMIOERR_READ_ONLY_FILE:
        s = "READ ONLY FILE";
        break;
    case MMIOERR_WRITE_ONLY_FILE:
        s = "WRITE ONLY FILE";
        break;
    case MMIOERR_WRITE_FAILED:
        s = "WRITE FAILED";
        break;
    case MMIOERR_READ_FAILED:
        s = "READ FAILED";
        break;
    case MMIOERR_SEEK_FAILED:
        s = "SEEK FAILED";
        break;
    case MMIOERR_NO_FLUSH_NEEDED:
        s = "NO FLUSH NEEDED";
        break;
    case MMIOERR_OUTOFMEMORY:
        s = "OUT OF MEMORY";
        break;
    case MMIOERR_CANNOTEXPAND:
        s = "CANNOT EXPAND";
        break;
    case MMIOERR_FREE_FAILED:
        s = "FREE FAILED";
        break;
    case MMIOERR_CHUNKNOTFOUND:
        s = "CHUNK NOT FOUND";
        break;
    case MMIO_ERROR:
        s = "ERROR";
        break;
    case MMIO_WARNING:
        s = "WARNING";
        break;
    case MMIO_CF_FAILURE:
        s = "CF FAILURE";
        break;
    default:
        cerr << rc;
        s = " (hmm...)";
    }

    cerr << s << "\n";
}

//
// main procedure
//
//
// WARNING: The MMPM/2 stops working on my system
//          if I use a string as argument for the FOURCC mask
//          which is none of the registered ones! (e.g. wuff)
//
//          Looks like a MMPM/2 bug to me.
//          (A major confusion of mmio.dll?)
//

int main(int argc, char* argv[])
{
    cout << "mmiofmts -- MMPM/2 MMIO Formats\n\n";


    // parse args

    FOURCC mask = 0;

    if (argc>1) {

The mmioStringToFOURCC() API call translates a string into a four character code.

        mask = mmioStringToFOURCC(argv[1], MMIO_TOUPPER);

A FOURCC is a 32-bit variable, containing 4 characters.

        cout << "mask in use is [";
        char* p = (char*) &mask;
        for (int i=0; i<4; i++) cout << p[i];
        cout << "]\n\n";
    }
 
    // query # of IOProcedures

    mmformatinfo mmfi_spec(mask);

    ULONG NumFormats = 0;

mmioQueryFormatCount() returns the number of installed MMIO procedures.

    ULONG rc = mmioQueryFormatCount(mmfi_spec.get_addr(),
                                    &NumFormats,
                                    0,
                                    0);

    if (rc != MMIO_SUCCESS) {
        mmio_err(rc);
        return 1;
    }

    cout << "formats supported: " << NumFormats;

    if (!NumFormats) return 0;
 
    // get formats

Get all format information via mmioGetFormats().

    mmformatinfo* mmfip = new mmformatinfo[NumFormats];

    ULONG FormatsRead = 0;

    rc = mmioGetFormats(mmfi_spec.get_addr(),
                        NumFormats,
                        mmfip,
                        &FormatsRead,
                        0,
                        0);

    if (rc != MMIO_SUCCESS) {
        mmio_err(rc);
        return 2;
    }

    cout << "  (" << FormatsRead << " formats read)\n\n";
 
    // print information

    cout << "no. 4-cc name                                     leng extn\n\n";

    for (int i=0; i<NumFormats; i++) {
        cout.setf(ios::right, ios::adjustfield);
        cout << setw(2) << i+1 << ":  [";

        FOURCC IOProc = mmfip[i].get_IOProc();
        char* p = (char*) &IOProc;

        for (int j=0; j<4; j++) cout << p[j];
        cout << "]  ";

        LONG NameLength = mmfip[i].get_NameLength();

        if (NameLength) {
            PSZ name = new CHAR[NameLength+1];
            LONG BytesRead = 0;

Extract the name of the format via mmioGetFormatName().

            rc = mmioGetFormatName(mmfip[i].get_addr(),
                                   name,
                                   &BytesRead,
                                   0,
                                   0);

            name[NameLength] = 0;

            if (rc != MMIO_SUCCESS) {
                mmio_err(rc);
                cout << "     ";
            }
            else {
                cout.setf(ios::left, ios::adjustfield);
                cout << setw(40) << name << " ("
                     << setw(2) << BytesRead;
            }

            delete[] name;
        }
        else
            cout << "-" << setw(43) << "( 0";

        cout.setf(ios::left, ios::adjustfield);
        cout << ")  ."
             << setw(3) << mmfip[i].get_DefaultFormatExt() << "\n";
    }

    delete[] mmfip;
 
    // that's all folks!

    return 0;
}

So let's run mmiofmts.exe to display the currently installed MMIO procedures:

mmiofmts -- MMPM/2 MMIO Formats

formats supported: 15  (15 formats read)

no.   4-cc   name                                     leng  extn

 1:  [RDIB]  RIFF DIB Image                           (14)  .RDI
 2:  [AVCI]  IBM AVC Still Video Image                (25)  ._IM
 3:  [MMOT]  IBM MMotion Still Video Image            (29)  .VID
 4:  [AVCA]  IBM AVC ADPCM Digital Audio              (27)  ._AU
 5:  [VOC ]  Creative Labs Voice File                 (24)  .VOC
 6:  [WI30]  MS Windows DIB Image                     (20)  .DIB
 7:  [MIDI]  Midi File Format I/O Procedure           (30)  .MID
 8:  [OS13]  IBM OS/2 1.3 PM Bitmap Image             (28)  .BMP
 9:  [OS20]  IBM OS/2 2.0 BMP                         (16)  .BMP
10:  [AVI ]  AVI IO Procedure                         (16)  .AVI
11:  [WAVE]  RIFF WAVE Digital Audio                  (23)  .WAV
12:  [CF  ]  -                                        ( 0)  .
13:  [BND ]  -                                        ( 0)  .BND
14:  [MEM ]  -                                        ( 0)  .MEM
15:  [DOS ]  -                                        ( 0)  .DOS

Save this utility for the next part of this article series when we go to install our own MMIO procedure!

Memory Playlists

Using an MCI script, playing several soundfiles is not smooth, for the waveaudio device must load each soundfile (element) into its context, delaying the play. A possible work-around is to load the elements into memory before playing starts. This can be achieved using the MMIO waveaudio I/O procedure and the MMPM/2 waveaudio playlist processor.

Loading the waveaudio file via the MMIO subsystem into memory has the advantage that one can extract the necessary data without knowing much about the maybe complicated internal structure of such a file. The .WAV format is a special kind of the more general RIFF (Resource Interchange File Format) multimedia format. For example, the M1.WAV file is 26504 bytes long, but contains only 26460 bytes of pure sound data. Using MMIO we don't have to worry where it is located in the file.

The usual encoding scheme for sound data is pulse code modulation (PCM). This means at a fixed rate per second (the sampling frequency) the amplitude of the signal of each channel is converted by an audio to digital converter into a number. The M1.WAV file was sampled in mono with 11kHz and 8 bit resolution, so it contains 26460 bytes/(11000 bytes/s) = 2.4s worth of audio data.

Note: once loaded into memory, one can easily work with this data in ways such as applying a compression scheme to it, mixing in a second waveaudio file (take the mean of both amplitude values) if you want to play more than one sound at once, adding effects such as echoing (rescale the amplitudes and mix this data with a short delay over the original data), etc.

A playlist is an array of playlist instructions, which is processed by the playlist processor. Among those instructions is not only the command to play a waveaudio file residing in memory, but also flow control instructions which allow jumps, loops and subroutines.

Look at this example, which employs a playlist to play a certain rhythm:

///////////////////////////////////////
// rhythm - waveaudio playlist sample
//
// using emx 0.8h, mm4emx 1.0
//
//
// Marc E.E. van Woerkom, 2/94
//

#include <os2.h>

#define  INCL_OS2MM
#include <os2me.h>

#include <iostream.h>
#include <iomanip.h>

// prototypes

void mci_err(ULONG);
void mmio_err(ULONG);

The MMAUDIOHEADER structure will get the header information of the waveaudio file from the MMIO.

//
// mmaudioheader
//

class mmaudioheader {
    MMAUDIOHEADER mmah;
public:
    mmaudioheader();
    MMAUDIOHEADER* get_addr() { return &mmah; }
    LONG           get_size() { return sizeof(mmah); }
};
 
mmaudioheader::mmaudioheader()
{
    char* p = (char*) &mmah;
    for (int i=0; i<sizeof(mmah); i++) p[i] = 0;
}

The constructor of the mem_wav class will load a waveaudio file (given by its filename) into memory via the MMIO subsystem.

//
// mem_wav: a waveaudio file loaded into memory
//

class mem_wav {
    HMMIO  hmmio;
    PSZ    bptr;
    ULONG  bsize;
    ULONG  SamplesPerSec;
    USHORT BitsPerSample;
public:
    mem_wav(char*);
    ~mem_wav();
    PSZ    get_bptr() { return bptr; }
    ULONG  get_bsize() { return bsize; }
    ULONG  get_SamplesPerSec() { return SamplesPerSec; }
    USHORT get_BitsPerSample() { return BitsPerSample; }
};
 
mem_wav::mem_wav(char* name)
{

The MMIO subsystem looks at the .WAV extension and calls the proper I/O procedure to open it, delivering a handle as the result.

    // open the file

    hmmio = mmioOpen(name, 0, MMIO_READ);

Now get the header information of the waveaudio file.

    // get header

    mmaudioheader mmah;

    ULONG BytesRead = 0;

    ULONG rc = mmioGetHeader(hmmio, mmah.get_addr(), mmah.get_size(),
                             &BytesRead, 0, 0);

    if (rc != MMIO_SUCCESS) mmio_err(rc);

The header contains the length in bytes (needed to allocate the buffer memory), the sampling frequency and the sampling resolution (these settings are needed for a proper reproduction).

    // get some infos about the waveaudio file

    SamplesPerSec = mmah.get_addr()->mmXWAVHeader.WAVEHeader.ulSamplesPerSec;

    BitsPerSample = mmah.get_addr()->mmXWAVHeader.WAVEHeader.usBitsPerSample;

    bsize = mmah.get_addr()->mmXWAVHeader.XWAVHeaderInfo.ulAudioLengthInBytes;

    bptr  = new CHAR[bsize];

The buffer is allocated, now read all information into it.

    // read file

    rc = mmioRead(hmmio, bptr, bsize);

    if (rc == MMIO_ERROR) mmio_err(rc);

    cout << "[file " << name
         << ": read" << setw(7) << rc
         << " bytes of" << setw(7) << bsize << "]\n";

Finally close the file. That's all! (And we didn't need to know anything about RIFF chunks, etc.)

    // close file

    rc = mmioClose(hmmio, 0);
    if (rc != MMIO_SUCCESS) mmio_err(rc);
}

The destructor of the class gets rid of the allocated memory resources.

mem_wav::~mem_wav()
{
    delete[] bptr;
}

Now comes the playlist. A playlist entry is composed of four ULONG variables. The first represents the instruction and the others are its possible arguments.

//
// ple: a playlist entry
//

struct ple {
    ULONG operation;
    ULONG operand1;
    ULONG operand2;
    ULONG operand3;
};

This class represents a playlist. It should be stated that it is tailored for this special example. (For it reads exactly 7 different samples and the playlist is hardcoded into the setup() member function. A general class of this kind should be able to deal with a variable amount of samples and should read the playlist from a data file or a resource block.)

//
// playlist: a waveaudio playlist
//

class playlist {
    ple*     pl;
    int      size, used;
    mem_wav& m1, m2, m3, m4, m5, s1, p1;
    void setup();
public:
    playlist(mem_wav&, mem_wav&, mem_wav&, mem_wav&,
             mem_wav&, mem_wav&, mem_wav&, int);
    ~playlist();
    ple* get_addr() { return pl; }
    int add(ULONG =0, ULONG =0, ULONG =0, ULONG =0);
    int add_branch(ULONG);
    int add_call(ULONG);
    int add_data(mem_wav&);
    int add_exit();
    int add_return();
};

Allocate the playlist entries.

playlist::playlist(mem_wav& M1, mem_wav& M2, mem_wav& M3,
                   mem_wav& M4, mem_wav& M5, mem_wav& S1, mem_wav& P1,
                   int Size)
    : m1(M1), m2(M2), m3(M3), m4(M4), m5(M5), s1(S1), p1(P1),
      size(Size)
{
    if (size < 1) cerr << "error: wrong playlist size!\n";

    pl = new ple[size];
    used = 0;

    setup();
}

playlist::~playlist()
{
    delete[] pl;
}

This member function will fill a playlist entry with the proper values. Note the default arguments. And it returns the number of the current entry which will come in handy when employed in setup().

int playlist::add(ULONG op=0, ULONG opd1=0, ULONG opd2=0, ULONG opd3=0)
{
    if (used >= size) {
        cerr << "error: playlist is too small!\n";
        return -1;
    }

    pl[used].operation = op;
    pl[used].operand1  = opd1;
    pl[used].operand2  = opd2;
    pl[used].operand3  = opd3;

    return used++;
}

A branch operation (jump to a specific entry).

int playlist::add_branch(ULONG addr)
{
    return add(BRANCH_OPERATION, 0, addr);
}

A call operation (call a playlist subroutine).

int playlist::add_call(ULONG addr)
{
    return add(CALL_OPERATION, 0, addr);
}

A data operation (play a waveaudio file from a buffer).

int playlist::add_data(mem_wav& mw)
{
    return add(DATA_OPERATION, ULONG(mw.get_bptr()), mw.get_bsize());
}

An exit operation (end the playlist processing).

int playlist::add_exit()
{
    return add(EXIT_OPERATION);
}

A return operation (return from a playlist subroutine).

int playlist::add_return()
{
    return add(RETURN_OPERATION);
}

This is a hardwired playlist. Note that is ordered in a way that only one forward reference (70) is needed.

void playlist::setup()
{

Jump to the 70th playlist entry.

                  add_branch(70);

This is one of several subroutines. It plays the buffer containing the audio data of M1.WAV thrice.

    ULONG Intro = add_data(m1);
                  add_data(m1);
                  add_data(m1);
                  add_return();

    ULONG A10a =  add_data(m2);
                  add_data(s1);
                  add_data(p1);
                  add_data(m3);
                  add_data(m4);
                  add_data(m5);
                  add_data(m4);
                  add_data(m5);
                  add_data(m4);
                  add_data(m5);
                  add_return();

    ULONG A10 =   add_data(m2);
                  add_data(s1);
                  add_data(p1);
                  add_data(m5);
                  add_data(m4);
                  add_data(m5);
                  add_data(m4);
                  add_data(m5);
                  add_data(m4);
                  add_data(m5);
                  add_return();

    ULONG B10 =   add_data(m2);
                  add_data(s1);
                  add_data(p1);
                  add_data(m1);
                  add_data(m1);
                  add_data(m1);
                  add_data(m1);
                  add_data(m1);
                  add_data(m1);
                  add_data(m1);
                  add_return();

    ULONG C10 =   add_data(m2);
                  add_data(s1);
                  add_data(p1);
                  add_data(m2);
                  add_data(m2);
                  add_data(m2);
                  add_data(m2);
                  add_data(m2);
                  add_data(m2);
                  add_data(m2);
                  add_return();

    ULONG A6 =    add_data(m2);
                  add_data(s1);
                  add_data(p1);
                  add_data(m5);
                  add_data(m4);
                  add_data(m5);
                  add_return();

    ULONG B6 =    add_data(m2);
                  add_data(s1);
                  add_data(p1);
                  add_data(m1);
                  add_data(m1);
                  add_data(m1);
                  add_return();

    ULONG C6 =    add_data(m2);
                  add_data(s1);
                  add_data(p1);
                  add_data(m2);
                  add_data(m2);
                  add_data(m2);
                  add_return();

Well, I didn't count until here. I simply printed out the return code of the next call in a prior version of this source and noted it.

// #70
                  add_call(Intro);
                  add_call(A10a);
                  add_call(B10);
                  add_call(A10);
                  add_call(B10);
                  add_call(A10);
                  add_call(C10);
                  add_call(A6);
                  add_call(B6);
                  add_call(A6);
                  add_call(C10);
                  add_call(A10);
                  add_call(C10);
                  add_call(A10);
                  add_call(A10);
                  add_call(A10);
                  add_call(C10);
                  add_call(A6);
                  add_call(C6);
                  add_call(A6);
                  add_call(B10);
                  add_data(s1);
                  add_exit();
}

This class represents the waveaudio device together with an associated playlist. The characteristics of the mem_wav given to the constructor are used for the processing of the whole playlist.

//
// waveaudio: a waveaudio device
//

class waveaudio {
    MCI_OPEN_PARMS mop;
public:
    waveaudio(playlist&, mem_wav&);
    ~waveaudio();
    void play();
};

waveaudio::waveaudio(playlist& pl, mem_wav& mw)
{

Open the waveaudio device via an MCI command message in a way that it will use a playlist as data.

    // open device

    mop.hwndCallback   = 0;
    mop.usDeviceID     = 0;
    mop.pszDeviceType  = MCI_DEVTYPE_WAVEFORM_AUDIO_NAME;
    mop.pszElementName = PSZ(pl.get_addr());

    ULONG rc = mciSendCommand(0,
                              MCI_OPEN,                        // open message
                              MCI_WAIT | MCI_OPEN_SHAREABLE |  // message flags
                              MCI_OPEN_PLAYLIST, 
                              &mop,                            // parameters
                              0);
    if (rc != MCIERR_SUCCESS) mci_err(rc);

If these values aren't set via MCI_SET, the waveaudio data will be played with improper speed or even be garbled.

    // set device parameters

    MCI_WAVE_SET_PARMS wsp;

    wsp.hwndCallback    = 0;
    wsp.ulSamplesPerSec = mw.get_SamplesPerSec();
    wsp.usBitsPerSample = mw.get_BitsPerSample();

    rc = mciSendCommand(mop.usDeviceID,
                        MCI_SET,
                        MCI_WAIT |
                        MCI_WAVE_SET_SAMPLESPERSEC |
                        MCI_WAVE_SET_BITSPERSAMPLE,
                        &wsp,
                        0);

    if (rc != MCIERR_SUCCESS) mci_err(rc);
}

Close the waveaudio device.

waveaudio::~waveaudio()
{
    // close device

    MCI_GENERIC_PARMS mgp;

    mgp.hwndCallback = 0;

    ULONG rc = mciSendCommand(mop.usDeviceID,
                              MCI_CLOSE,
                              MCI_WAIT,
                              &mgp,
                              0);

    if (rc != MCIERR_SUCCESS) mci_err(rc);
}

Set the waveaudio device on 'play'.

void waveaudio::play()
{
    // play the playlist

    MCI_PLAY_PARMS mpp;

    mpp.hwndCallback = 0;

    ULONG rc = mciSendCommand(mop.usDeviceID,
                              MCI_PLAY,
                              MCI_WAIT,
                              &mpp,
                              0);

    if (rc != MCIERR_SUCCESS) mci_err(rc);
}

Routines to print pretty error messages.

//
// mci_err: translate the MCI return code into an error string
//

void mci_err(ULONG rc)
{
    const rsize = 128;
    char rbuff[rsize];

    ULONG rc2 = mciGetErrorString(rc,      // error code
                                  rbuff,   // return buffer
                                  rsize);  // rbuff size

    if (rc2 == MCIERR_SUCCESS)
        cerr << "MCI error: " << rbuff << "\n\n";
    else
        cerr << "error #" << rc << " has occured!\n\n";
}

//
// mmio_err: translate MMIO error code into a string
//

void mmio_err(ULONG rc)
{
    cerr << "MMIO error: ";

    char* s;

    switch (rc) {
    case MMIO_SUCCESS:
        s = "SUCCESS (huh?)";
        break;
    case MMIOERR_UNBUFFERED:
        s = "UNBUFFERD";
        break;
    case MMIOERR_INVALID_HANDLE:
        s = "INVALID HANDLE";
        break;
    case MMIOERR_INVALID_PARAMETER:
        s = "INVALID PARAMETER";
        break;
    case MMIOERR_READ_ONLY_FILE:
        s = "READ ONLY FILE";
        break;
    case MMIOERR_WRITE_ONLY_FILE:
        s = "WRITE ONLY FILE";
        break;
    case MMIOERR_WRITE_FAILED:
        s = "WRITE FAILED";
        break;
    case MMIOERR_READ_FAILED:
        s = "READ FAILED";
        break;
    case MMIOERR_SEEK_FAILED:
        s = "SEEK FAILED";
        break;
    case MMIOERR_NO_FLUSH_NEEDED:
        s = "NO FLUSH NEEDED";
        break;
    case MMIOERR_OUTOFMEMORY:
        s = "OUT OF MEMORY";
        break;
    case MMIOERR_CANNOTEXPAND:
        s = "CANNOT EXPAND";
        break;
    case MMIOERR_FREE_FAILED:
        s = "FREE FAILED";
        break;
    case MMIOERR_CHUNKNOTFOUND:
        s = "CHUNK NOT FOUND";
        break;
    case MMIO_ERROR:
        s = "ERROR";
        break;
    case MMIO_WARNING:
        s = "WARNING";
        break;
    case MMIO_CF_FAILURE:
        s = "CF FAILURE";
        break;
    default:
        cerr << rc;
        s = " (hmm...)";
    }

    cerr << s << "\n";
}

Main function. Perhaps you should start reading this example from here.

//
// main
//

int main()
{
    cout << "rhythm -- a Rhythm Generator\n\n"; 

    // load waveaudio files into memory

    cout << "loading waveaudio files into memory ...\n\n";

    mem_wav m1("m1.wav");
    mem_wav m2("m2.wav");
    mem_wav m3("m3.wav");
    mem_wav m4("m4.wav");
    mem_wav m5("m5.wav");
    mem_wav s1("s1.wav");
    mem_wav p1("p1.wav");

    // set up playlist

    cout << "\nsetting up playlist ...\n\n";

    playlist pl(m1, m2, m3, m4, m5, s1, p1, 100);

    // play playlist

    cout << "and ... go!   (you should pump up the volume :-)\n\n";

    waveaudio wav(pl, m1);
    wav.play();

    // that's all folks!

    cout << "... done. yeah!\n";

    return 0;
}

Try it!

Note that this example is not too far away from the .MOD file playing mechanism. The hardest thing for an extension in this direction is probably getting the proper .MOD file definition. Since there was an unconfirmed report on Usenet that IBM may deliver a .MOD MMIO procedure in the next MMPM/2 release, I personally won't put time into something like that.

What's Next?

At least two important topics are still on my list:

  • Expect to see more on the MMIO subsystem. Procedures for handling compressed waveaudiofiles are under development.
  • The PM extensions (graphical buttons, circular sliders and the secondary windows support) cry for some nice examples.

More Literature on MMPM/2

IBM Doc. S53G-2166:
OS/2 Online Book Collection CD-ROM.
This CD-ROM contains 144 different OS/2 manuals in *.boo format and readers for OS/2 and DOS. (Highly recommended!)