Writing Object Oriented Multimedia I/O Procedures

by Ron Jones

All who have taken the time to explore the OS/2 2.1 operating system have either heard about or experienced the power of its multimedia extensions. Multimedia Presentation Manager/2, or MMPM/2, is an integral part of the OS/2 2.1 operating environment. It brings the reality of digital sounds, images, and software motion video to your desktop.

Multimedia, as one might expect from the name, comes in many formats. Providing support for every popular format on the market would be a daunting task to say the least! Instead, MMPM/2 offers a well designed architecture to let additional format support be plugged in seamlessly.

MMPM/2 I/O Procedures
Input/Output Procedures, affectionately termed IOProcs, are pluggable software units that you can add to the system to provide additional file format or storage system support. IOProcs can be thought of almost as file format device drivers. They plug into the multimedia I/O subsystem (MMIO) and isolate the application developer from file-format-specific information. MMIO is used, literally, by all parts of the multimedia architecture, thereby allowing already written applications to immediately take advantage of the new formats.

For example, let's say a new digital audio format appears on the market. However, neither MMPM/2 nor your audio card currently provides support for this new format. What can be done? You can write an IOProc to solve this problem. With the appropriate IOProc, all correctly programmed applications that exploit the Media Control Interface or the MMIO interface will be able to understand the new files.

Several IOProcs ship with the system to provide native image, audio, and video support. Image IOProcs provide support for OS/2 1.3, OS/2 2.0, Windows 3.0, and IBM MMotion image formats. The native digital audio IOProcs provide support for RIFF Wave, SoundBlaster VOC, IBM AVC, and RIFF Midi formats. The popular AVI format also is provided for software motion video. Although a file format may be supported (for example AVI in software motion video) a compression/decompression algorithm (CODEC) might also be necessary for the system to understand the file's content.

Even though the differences between two formats might be very minor, two separate IOProcs are necessary to support both formats. Just because a separate IOProc is needed to support a new format, you would not want the complex code necessary to support clipboard editing support for audio, or dithering functions for image manipulation, to be duplicated in every IOProc. Thus, you need a method for efficient code reuse. Using the object-oriented principles of inheritance and virtual functions found in the C++ programming language, you can efficiently reuse your code. You'll also gain the benefits of lower maintenance costs and easier source code readability.

The I/O Procedure Base Class Design
The base class design can be found in the file, ioproc.hpp. The IOProc class declares a virtual function for the most commonly-used messages that an IOProc can accept from the MMIO manager. A protected constructor ensures that the base class can not be used as a stand-alone IOProc; you must create a derived class. The destructor is virtual to avoid any problems with derived classes destructing improperly. There also are two pure virtual functions that derived classes must override. The Make method must return a pointer to an instance of the object or zero, while the Destroy(IOProc *pDOA) method must destruct that instance. This mechanism lets you store instance information in the private area of derived classes. One constraint for the derived classes is the use of the pointer to the MMIOINFO structure passed into every method. This pointer lets those methods call MMIO APIs. However, the base class already uses the pExtraInfoStruct field of the MMIOINFO structure to store the pointer to the derived class. Consequently, a derived class must not change the value of that field!  // // IOProc.hpp // class IOProc { private: unsigned long hModuleHandle; unsigned long stub(MMIOINFO * pmmioinfo); unsigned long nls_data, ioproc_name_table; unsigned long ulIOProcType, ulMediaType, ulCapsFlags, ulFourcc; char pDefaultExtm10y; protected: IOProc(unsigned long nlsdata, unsigned long name_table,          unsigned long fourcc, unsigned long IOProcType,           unsigned long MediaType,           unsigned long CapsFlags, char *DefaultFormatExt); virtual ~IOProc; virtual long GetFormatString (long lSearchId,                                 unsigned char *pszFormatString,                                  long lBytes); virtual long GetFormatStringLength (long lSearchId,                                       long *plNameLength); virtual unsigned long GetNLSData (unsigned long *, unsigned long *); // Derived Classes Must Override these methods:

virtual IOProc *Make = 0; virtual Destroy(IOProc *pDOA) = 0; public: unsigned long getModuleHandle const; unsigned long getNLSDataID const; unsigned long getNameTableID const; IOProcsetModuleHandle(unsigned long hMod); virtual unsigned long mmiomGetHeader(MMIOINFO * pmmioinfo,                               void *pHeader, long length); virtual unsigned long mmiomGetFormatInfo(MMIOINFO * pmmioinfo,                               MMFORMATINFO *pMMFormatInfo); virtual unsigned long mmiomGetFormatName(MMIOINFO * pmmioinfo,                               unsigned char *pFormatName, long lBytes); virtual unsigned long mmiomIdentifyFile(MMIOINFO * pmmioinfo,                               unsigned char *pFileName, HMMIO hmmio); virtual unsigned long mmiomIdentifyHmmio(MMIOINFO * pmmioinfo,                               HMMIO hmmio); virtual unsigned long mmiomOpenFile(MMIOINFO * pmmioinfo,                               unsigned char *pFileName); virtual unsigned long mmiomOpenHmmio(MMIOINFO * pmmioinfo,                               HMMIO hmmio); virtual unsigned long mmiomQueryHeaderLength(MMIOINFO * pmmioinfo); virtual unsigned long mmiomRead(MMIOINFO * pmmioinfo,                               unsigned char *pucBuffer,                                long lBytes); virtual unsigned long mmiomSeek(MMIOINFO * pmmioinfo,                               long lByteOffset, long soff); virtual unsigned long mmiomSetHeader(MMIOINFO * pmmioinfo,                               void *pHeader, long length); virtual unsigned long mmiomWrite(MMIOINFO * pmmioinfo,                               unsigned char *pucSource,                                long lBytes); virtual unsigned long mmiomClose(MMIOINFO * pmmioinfo, long lFlags); virtual unsigned long mmiomUserMessage(MMIOINFO * pmmioinfo,                               unsigned short usMsg,                                long lParam1, long lParam2); virtual long handleMMIOMessage(void *pmmioStr, unsigned short usMsg,                               long lParam1,                                long lParam2); };  ''Sample Code 1. Base Class Definition''

Each method is named, for those familiar with I/O procedures IOProcs, using the message names described in the Subsystem Development Guide found in the MMPM/2 Toolkit documentation. For example, the method invoked in response to the MMIOM_CLOSE message is named mmiomClose(...). Each method passes in only the parameters needed by that message. The parameter names are self-descriptive. For example, instead of using the documentation to find that lParam1</tt> of the MMIOM_SEEK</tt> message is the byte offset of the desired file position, you only have to infer from the naming convention that the lByteOffset</tt> parameter of the mmiomSeek</tt> method holds that information. The naming convention itself is reason enough to use the base class!

The identify and open functions provide a little more than a simple method for retrieving the MMIO message. These actually offer you two options, either override the mmiomxxxxFile</tt> (where xxxx</tt> is either Open or Identify) method or override the mmiomxxxxHmmio method. The mmiomxxxxHmmio</tt> method ensures that the file has been successfully opened with the correct storage system, while the mmiomxxxxFile</tt> method does not. Consequently, only override the mmiomxxxxFile</tt> method when absolutely necessary!

The base class also handles the processing of the MMIOM_GETFORMATNAME</tt> and MMIOM_GETFORMATINFO</tt> messages by using information provided upon construction of an IOProc derived class. To accomplish this, the resource information for an IOProc must be organized into two tables. Sample Code 2 shows the format of these tables.  RCDATA CHARSET_INFO BEGIN MMIO_DEFAULT_CODE_PAGE, MMIO_CC_USA, MMIO_LC_US_ENGLISH, MMIO_DC_US_ENGLISH END RCDATA IOPROC_NAME_TABLE BEGIN HEX_FOURCC,    "My File Format\0", 0,             "\0" END </PRE> ''Sample Code 2. Format of IOProc Tables''

Because the mmiomGetFormatName</tt> and mmiomGetFormatInfo</tt> methods are virtual, you also can override them to intercept the default actions of the base class. This is only necessary if the resource information needs to be organized in a different manner.

Derived classes need only inherit from the IOProc base class and properly construct it. See Sample Code 3. On the constructor, you need to pass in several pieces of information:
 * nlsdata
 * The ID of the first table in Sample Code 2.


 * name_table
 * The ID of the second table in Sample Code 2 that stores the string name of your IOProc.


 * fourcc
 * The FOURCC value of the IOProc.


 * IOProcType
 * The IOProc type; MMIO_IOPROC_FILEFORMAT</tt> in most cases.


 * MediaType
 * The IOProc media type.


 * CapsFlags
 * The capability flags.


 * DefaultFormatExt
 * A char *</tt> pointer to the default extension of the IOProc.

With these parameters, the base class can retrieve resource information and take care of some of the common MMIO messages. IOProc(unsigned long nlsdata, unsigned long name_table,       unsigned long fourcc, unsigned long IOProcType, unsigned long MediaType,        unsigned long CapsFlags, char *DefaultFormatExt);

''Sample Code 3. IOProc Base Clase Constructor''

The file myioproc.cpp shows what must be done in the <tt>_DLL_InitTerm</tt> function and the main entry point function of any IOProc written using this base class design. The global declaration of the IOProc derived class MYIOProc can be found in Sample Code 3. This global class reference gets constructed by the call to <tt>__ctordtorInit</tt> in the <tt>_DLL_InitTerm</tt> function. This call constructs all declared global objects, the key to placing multiple IOProcs in the same DLL. The global objects are destructed by the <tt>__ctordtorTerm</tt> call. The <tt>_DLL_InitTerm</tt> function makes this call as OS/2 unloads the DLL. The only other special call that is needed is the <tt>myIOProc.setModuleHandle(hModule)</tt> call that sets the DLL module handle into the base class. The base class uses this module handle to obtain the resource information described in Sample Code 2. MYIOProc myIOProc(MY_CHARSET_INFO, MY_IOPROC_NAME_TABLE, FOURCC_MY.                  MMIO_IOPROC_FILEFORMAT, MMIO_MEDIATYPE_AUDIO,                   MMIO_CANREADTRANSLATED ! MMIO_CANSEEKTRANSLATED,                   "EXT");

''Sample Code 4. Global Declaration of an IOProc''

The real binary code size advantages come by compiling the base class as a separate DLL or by combining several IOProcs into the same DLL. More than one IOProc can use the base class code. Collecting the common function of IOProcs into classes near the top of the class hierarchy allows for low-maintenance code. If a problem should appear in the base class, there is only one place to look to fix it. And, after the defect is removed, all derived classes will automatically inherit the fix.

Summary
The base class, however, is only the beginning. It is simply the root of a more complex class hierarchy. Further classes can be derived from this base class to provide a more robust class hierarchy for Audio, Image, Video, and Telephony IOProcs. Look for these topics to be included in future issues of The Developer Connection News.

All of the sample source code described in this article can be found on your accompanying Developer Connection CD-ROM.