Creating a Sound Recorder in C and C#

This article was previously published in my blog, Just Like a Magic.

Overview

This article will focus on how to record sound from an input device and how you can play sound files using Media Control Interface (MCI) in C and C#.

This article does not involve a discussion or even an introduction to MCI. Instead, it provides a technical discussion of what we will need to record and play sound files. If you need an introduction to MCI then refer to the MSDN documentation.

We will begin with a discussion of the types and functions required to do it. Then we will look at how you can use those types and functions in your C or C# application.

Our demonstration examples in this writing will be in C. In the last section we will have a look at .NET and C#. Besides this, there are sample applications written by C and C# attached with the article.

Introduction

Actually, there are many ways to record and play sound files. However, in this writing we will focus on the Media Control Interface (MCI) since it is one of the high-level easy-to-use interfaces for controlling all media types.

You can control a multimedia hardware using MCI by either sending commands like Windows messages or by sending string messages to the MCI. The second approach is preferable in scripting languages. Since we are concentrating on the languages C and C#, we will consider the first approach only.

Because the .NET Framework does not include classes for handling MCI (or multimedia at all,) we will need to dig into the Windows SDK, specifically WinMM.dll (the Windows Multimedia Library) that resides in the system directory.

In order to access this library from your C# application, you will need to create your own marshaling types and Platform Invocation (PInvoke) methods to allow your managed code to communicate with the unmanaged server. That is what the last section of this article is for.

To access this library from your C application, just reference the library object file WinMM.lib in your project. Notice that all of the functions and structures related to the MCI are prefixed with mci.

In the next sections we will talk about the functions and structures that we need to be aware of before digging into coding.

Sending MCI Commands

The key function in Windows multimedia programming using MCI is mciSendCommand(). This function sends a command to the MCI. As we have said, you can program MCI using one of two approaches, you can send Windows-messages-like commands and you can send string messages to the MCI. The key function that we will use to send MCI commands is mciSendCommand(). To send string messages, you can use the function mciSendMessage() that is not covered here.

The definition of the function mciSendCommand() is as follows:

MCIERROR mciSendCommand(
  MCIDEVICEID IDDevice,
  UINT        uMsg,
  DWORD       fdwCommand,
  DWORD_PTR   dwParam
);

This function receives the following four input arguments:

  • IDDevice
    The ID of the device to receive the command. For example, the ID of the input device when recording or the output device if playing. As you know, many devices could be connected to the PC. You can forget about this argument and just pass the function 0 to direct the message to the default device (selected in the Sound applet in the Control Panel).
  • uMsg
    The message (command) to be sent. Common messages are covered in the next few sections.
  • fdwCommand:
    Flags (options) of the message. Every message has its own options. However, all messages share the flags MCI_WAIT, MCI_NOTIFY, and MCI_TEST (covered soon).
  • dwParam
    A structure containing the specific parameter for the command message.

As a result, every command (message) has its name, flags, and structure parameter.

This function returns 0 if successful or an error code otherwise.

The Wait, Notify, and Test Flags

Common flags for all MCI messages are, MCI_WAIT, MCI_NOTIFY, and MCI_TEST.

The Wait flag, MCI_WAIT, indicates that the message should be processed synchronously; in other words that the function would not return until message processing finishes. For example, if you send a play message with the MCI_WAIT then your application would be suspended until the entire file finishes. Therefore, you should not use MCI_WAIT to play or record messages.

The Notify flag, MCI_NOTIFY, is the reverse of the Wait flag. It indicates that the message should be processed asynchronously; in other words that the function would return immediately and does not wait for the completion of the message. When the processing of the message finishes, it sends a MM_MCINOTIFY message to the windows specified in the dwCallback member of the message parameter. This flag should be used to play and record messages.

The wParam value of the MM_MCINOTIFY message is usually set to either MCI_NOTIFY_SUCCESSFUL to indicate command success or MCI_NOTIFY_FAILURE otherwise.

The Test flag, MCI_TEST, checks if the device can process this message, it does not process the message. The function would return 0 if the device can process the message or non-zero otherwise. You will use this flag in rare cases only.

Keep in mind that you should choose only one of the three flags, you cannot combine them.

If you didn't specify any of these three flags then the call would be treated asynchronously and you will not be notified when it completes.

Handling MCI Errors

Every MCI command could succeed or fail. If the command succeeds then mciSendCommand() returns 0 (FALSE/false.) Otherwise, it returns the error code.

MCI defines error codes as constants that are prefixed with MCIERR_ like MCIERR_DEVICE_NOT_INSTALLED and MCIERR_FILE_NOT_FOUND (names are self-explanatory.) You can get the friendly error message of the code using the function mciGetErrorString(). The definition of this function is as follows:

BOOL mciGetErrorString(
  DWORD fdwError,
  LPTSTR lpszErrorText,
  UINT cchErrorText
);

This function accepts the following three arguments:

  • fdwError
    Error code returned by the mciSendCommand() function.
  • lpszErrorText
    A string buffer that receives the description of the error.
  • cchErrorText
    Length of the buffer in characters.

The following C example shows how you can display a friendly error message to the user if an error occurred:

    TCHAR szBuffer[256];
    DWORD dwErr;

    dwErr = mciSendCommand(/* arguments */);

    mciGetErrorStringW(dwErr, szBuffer, 256);
    // cchErrorText =
    //     sizeof(szBuffer) / sizeof(szBuffer[0])

Recording from an Input Device

Use the following procedure to record from an input device using MCI:

  1. Open the input device to receive the data from.
  2. Order the MCI to start the recording process.
  3. When you finish, stop the recording process.
  4. Save the record buffer if applicable.
  5. Close the opened device.

Keep in mind that you should check whether or not an error occurred after sending each command. The previous approach to retrieve error messages would be very useful.

Opening the Device

To open a device just pass the open command MCI_OPEN to the mciSendCommand() function along with its flags and parameter.

The parameter of MCI_OPEN is the structure MCI_OPEN_PARMS. This structure contains information about the open command. The definition of this structure is as follows:

typedef struct {
    DWORD_PTR    dwCallback;
    MCIDEVICEID  wDeviceID;
    LPCSTR       lpstrDeviceType;
    LPCSTR       lpstrElementName;
    LPCSTR       lpstrAlias;
} MCI_OPEN_PARMS;

Actually, you will make use of only the third and fourth members, lpstrDeviceType and lpstrElementName, of the structure when you open a device. lpstrDeviceType determines the type of the device (digital-audio, digital-video, and so on) that will be used. In our example that records and plays sound files, we will set this member to "waveaudio" to indicate that we will work with waveform (WAV) data.

lpstrElementName on the other hand, should be set to an empty string (that is "") if you are opening an input device for recording. If you want to play a file, set this member to the full path of that file.

Common flags of the command MCI_OPEN are:

  • The Wait, Notify, and Test flags
    The Wait command is usually used for MCI_OPEN.
  • MCI_OPEN_ELEMENT
    Mandatory. The lpstrDeviceType of the MCI_OPEN_PARMS is set. It is set to "waveaudio" for WAV data.
  • MCI_OPEN_TYPE
    Mandatory. The lpstrElementType of the MCI_OPEN_PARMS is set. It is set to an empty string if recording or a path to a file if you want to play it.

You will always combine the flags MCI_WAIT, MCI_OPEN_ELEMENT, and MCI_OPEN_TYPE for the MCI_OPEN command.

When the function returns, the wDeviceID member of the structure is set to the ID of the device opened. You should keep this ID for future calls on that device until you close it using the close command.

The following C code shows how you can open an input device for recording:

    MCI_OPEN_PARMS parmOpen;
    WORD wDeviceID;

    parmOpen.dwCallback       = 0;
    parmOpen.wDeviceID        = 0;    // the default device
    parmOpen.lpstrDeviceType  = TEXT("waveaudio");
    parmOpen.lpstrElementName = TEXT("");
    parmOpen.lpstrAlias       = 0;

    mciSendCommand(0, // the default device
        MCI_OPEN,
        MCI_WAIT | MCI_OPEN_TYPE | MCI_OPEN_ELEMENT,
        (DWORD)&parmOpen);

    // Keep the device ID for future calls
    wDeviceID = parmOpen.wDeviceID;

Starting Recording

After you have opened the input device, you can order the MCI to start the recording process using the command MCI_RECORD. This command requires an opened input device and a parameter of type MCI_RECORD_PARMS. The definition of this structure is as follows:

typedef struct {
    DWORD_PTR dwCallback;
    DWORD     dwFrom;
    DWORD     dwTo;
} MCI_RECORD_PARMS;


Members this structure defines are as follows:

  • dwCallback
    The window handle that should be called after the processing of the command finishes if MCI_NOTIFY is specified in command flags.
  • dwFrom
    Indicates the position of the buffer (in thousands of a second) to start recording from. In most cases this would be set to zero.
  • dwTo
    Indicates the position of the buffer to stop recording when it is reached. Unless you want to record for a given period, this member should be zero.

Common flags for MCI_RECORD are:

  • MCI_WAIT, MCI_NOTIFY, and MCI_TEST
    Usually you will use the MCI_NOTIFY flag. If so, you should handle the MM_MCINOTIFY message.
  • MCI_FROM
    Set if you used the dwFrom member of the parameter.
  • MCI_TO
    Set if you used the dwTo member of the parameter.

If you want to record for a specific period then set the dwTo member of the structure to that specific period (in thousands of seconds) and combine your flags with MCI_TO. When this period ends, MCI automatically stops the recording process and it sends a MM_MCINOTIFY message if you have set MCI_NOTIFY in the flags.

The following C example shows how you can start recording:

    MCI_RECORD_PARMS parmRec;

    parmRec.dwCallback = 0;
    parmRec.dwFrom       = 0;
    parmRec.dwTo       = 0;

    // We do not need a notification message
    // we will send a Stop command, when
    // we finish.
    mciSendCommand(wDeviceID, MCI_RECORD, 0, (DWORD)&parmRec);

The following code shows how you can record for a specific period:

    MCI_RECORD_PARMS parmRec;

    parmRec.dwCallback = hWnd;    // window handle
    parmRec.dwFrom       = 0;
    parmRec.dwTo       = 30000; // 30 seconds

    // Notify me when you finish the 30 seconds
    mciSendCommand(wDeviceID, MCI_RECORD,
        MCI_NOTIFY | MCI_TO, (DWORD)&parmRec);

Pausing Recording

To pause the recording process, just pass the MCI_PAUSE command to the MCI. This command accepts a parameter of the structure MCI_GENERIC_PARMS that is defined as follows:

typedef struct {
    DWORD_PTR dwCallback;
} MCI_GENERIC_PARMS;

This structure contains only one member, dwCallback. As you know, if you specify MCI_NOTIFY in the command flags, MCI will send a MM_MCINOTIFY message to the window specified in this member.

MCI_PAUSE does not have specific flags, just the Wait, Notify, and Test flags.

The following C example shows how you can pause the recording process. Notice that you should already be recording or an error would be returned by the mciSendCommand().

    MCI_GENERIC_PARMS parmGen;

    parmGen.dwCallback = 0;

    mciSendCommand(wDeviceID, MCI_PAUSE, MCI_WAIT, (DWORD)&parmGen);

Resuming Recording

To resume after pausing, you can send a MCI_RESUME command to MCI. This command is very similar to the MCI_PAUSE command. It accepts the same parameter and the same flags. The example is the same, just change the command name.

Stopping Recording

After you finish recording you will need to stop the recording process. To accomplish this, pass the MCI_STOP command along with the device ID and its parameter to the MCI.

This command is the same as MCI_PAUSE and MCI_RESUME. It accepts the same flags and the same parameters, and the example is identical too, just change the command name.

Retrieving Buffer Length

How long have you been recording? This could be easily answered with the MCI_STATUS command. This command queries MCI and retrieves information about the current session.

MCI_STATUS accepts a structure parameter of type MCI_STATUS_PARMS that is defined as follows:

typedef struct {
    DWORD_PTR dwCallback;
    DWORD     dwReturn;
    DWORD     dwItem;
    DWORD     dwTrack;
} MCI_STATUS_PARMS;

This structure defines the following members:

  • dwCallback
    Discussed previously.
  • dwReturn
    After returning from the call, it should contain the return value of the query.
  • dwItem
    The item to query about.
  • dwTracks
    Length or number of tracks (specific to some query items).

Common flags that MCI_STATUS accepts:

  • The Wait, Notify, and Test flags
    You will usually use the Wait flag.
  • MCI_STATUS_ITEM
    Mandatory in most cases. Indicates that the dwItem of the structure is set.

If you want to query MCI about specific information then pass the MCI_STATUS command to MCI along with MCI_STATUS_ITEM and MCI_WAIT flags, and set the dwItem field of the parameter to one of the following values (some are for output devices):

  • MCI_STATUS_LENGTH
    Retrieves the total length of the buffer (in thousands of a second).
  • MCI_STATUS_MODE
    Retrieves the current mode of the device that can be one of the following values (names are self-explanatory):
    • MCI_MODE_NOT_READY
    • MCI_MODE_PAUSE
    • MCI_MODE_PLAY
    • MCI_MODE_STOP
    • MCI_MODE_RECORD
    • MCI_MODE_SEEK
  • MCI_STATUS_NUMBER_OF_TRACKS
    Retrieves the total number of tracks.
  • MCI_STATUS_POSITION
    Retrieves the current position (in thousands of a second).
  • MCI_STATUS_READY
    Returns TRUE (true in C#) if the device is ready or FALSE (false in C#) otherwise.

When the function returns, the dwReturn field of the structure should contain the result of the query item selected.

The following C example retrieves the length of the buffer recorded:

    MCI_STATUS_PARMS parmStatus;

    parmStatus.dwCallback = 0;
    parmStatus.dwReturn   = 0;
    parmStatus.dwItem     = MCI_STATUS_LENGTH;
    parmStatus.dwTrack    = 0;

    mciSendCommand(0, MCI_STATUS,
        MCI_WAIT | MCI_STATUS_ITEM, (DWORD)&parmStatus);

    // Display the number of seconds
    // parmStatus.dwReturn / 1000

Saving the Recorded Buffer

Before you close the device, you can save the current recorded buffer in an audio (waveform) file. The command MCI_SAVE orders MCI to save the buffer to the file specified in its structure parameter.

The parameter of MCI_SAVE is a MCI_SAVE_PARMS structure and it is defined as follows:

typedef struct {
    DWORD_PTR  dwCallback;
    LPCTSTR    lpfilename;
} MCI_SAVE_PARMS;

This structure contains only two members, dwCallback (discussed before) and lpfilename. lpfilename points to a string buffer containing the name and full path of the file to save.

MCI_SAVE accepts the following few flags:

  • The Wait, Notify, and Test flags
    You will always use the Wait (MCI_WAIT) flag.
  • MCI_SAVE_FILE
    Mandatory. You will always set this flag. It indicates that lpfilename contains the path of the target file.

The following example saves the recorded buffer to the file "recording.wav":

    MCI_SAVE_PARMS parmSave;

    parmSave.dwCallback = 0;
    parmSave.lpfilename = TEXT("recording.wav");
    // save to the current directory

    mciSendCommand(wDeviceID, MCI_SAVE,
        MCI_WAIT | MCI_SAVE_FILE, (DWORD)&parmSave);

Closing the Device

You strictly should always close the device when you finish working with it, and this is done through the MCI_CLOSE command that accepts a parameter of type MCI_GENERIC_PARMS (discussed before.)

MCI_CLOSE accepts only the Wait, Notify, and Test flags.

The following C example closes the opened device:

    MCI_GENERIC_PARMS parmsGen;

    parmsGen.dwCallback = 0;

    mciSendCommand(wDeviceID, MCI_CLOSE, MCI_WAIT, (DWORD)&parmsGen);


Playing a Sound File

Keeping in mind what you have learned from the previous section would be very helpful in our discussion of how to play sound files using MCI.

Actually, the same rules and commands that were applied in the previous section, will be applied here.

Use the following procedure to play a sound file usingMCI:

  1. Load the audio file. This automatically opens the output device.
  2. Order the MCI to start the playing process.
  3. Close the opened device when you finish.

Again, you should check whether or not an error occurred after each call to mciSendCommand().

Loading the File

As you know, to open a multimedia device, you pass MCI the MCI_OPEN command. To specify a file to be loaded, specify the full path of the file in the lpstrElementName field of the MCI_OPEN_PARMS structure.

The following C example shows how you can open an output device and load a file for playing:

    MCI_OPEN_PARMS parmOpen;
    WORD wDeviceID;

    parmOpen.dwCallback       = 0;
    parmOpen.wDeviceID        = 0;    // the default device
    parmOpen.lpstrDeviceType  = TEXT("waveaudio");
    parmOpen.lpstrElementName = TEXT("recording.wav");
    parmOpen.lpstrAlias       = 0;

    mciSendCommand(0, // the default device
        MCI_OPEN,
        MCI_WAIT | MCI_OPEN_TYPE | MCI_OPEN_ELEMENT,
        (DWORD)&parmOpen);

    // Keep the device ID for future calls
    wDeviceID = parmOpen.wDeviceID;

Playing the File

To start playing the currently loaded file, you can use the MCI_PLAY command. This command accepts a structure parameter of type MCI_PLAY_PARMS that is defined as follows:

typedef struct {
    DWORD_PTR dwCallback;
    DWORD     dwFrom;
    DWORD     dwTo;
} MCI_PLAY_PARMS;

At first sight, you notice that it is identical to the MCI_RECORD_PARMS. Actually, you are right. This structure defines the same members as the MCI_RECORD_PARMS structure. In addition, the description of the members is the same.

dwFrom specifies the position (in thousands of a second) where to start playing. dwTo on the other hand specifies the position (in thousands of a second too) where to end playing. If you set dwFrom, you will need to set the flag MCI_FROM. Conversely, if you set dwTo, you will need to set the flag MCI_TO. If you need to play the file from the start to the end, leave both members and do not specify either MCI_FROM or MCI_TO.

You will most likely combine the MCI_PLAY flags with the Notify flag, MCI_NOTIFY, to allow the code to continue execution while the file plays. If you do then your application would receive a MM_MCINOTIFY message to the window specified in the dwCallback member of the parameter structure.

The following C example shows how to start playing a file:

    MCI_PLAY_PARMS parmPlay;

    // Play the file
    // from the start to the end

    parmRec.dwCallback = 0;
    parmRec.dwFrom       = 0;
    parmRec.dwTo       = 0;

    // notify me when you finish
    mciSendCommand(wDeviceID, MCI_PLAY,
        MCI_NOTIFY, (DWORD)&parmRec);

And the following code plays only three minutes from the file:

    MCI_PLAY_PARMS parmPlay;

    // play only 3 minutes
    parmRec.dwCallback = 0;
    parmRec.dwFrom       = 0;
    // 3 * 60 * 1000
    parmRec.dwTo       = 180000;

    // notify me when you finish
    mciSendCommand(wDeviceID, MCI_PLAY,
        MCI_NOTIFY | MCI_TO, (DWORD)&parmRec);

Pausing

As you know, you can pause the playback using the MCI_PAUSE command that accepts the MCI_GENERIC_PARMS parameter.

Resuming

To resume the playback after pausing, you can use the MCI_RESUME command discussed before.

Retrieving Current Position

To retrieve the current position of the file, pass the MCI_STATUS command along with its flags (MCI_WAIT and MCI_STATUS_ITEM) and its parameter (MCI_STATUS_PARMS) to the MCI. Do not forget to set the dwItem member of the structure to MCI_STATUS_POSITION to retrieve the current position (in thousands of a second) in the dwReturn member of the structure. The following C example demonstrates this:

    MCI_STATUS_PARMS parmStatus;

    parmStatus.dwCallback = 0;
    parmStatus.dwReturn   = 0;
    parmStatus.dwItem     = MCI_STATUS_POSITION;
    parmStatus.dwTrack    = 0;

    mciSendCommand(wDeviceID, MCI_STATUS,
        MCI_WAIT | MCI_STATUS_ITEM, (DWORD)&parmStatus);

    // Display the current position
    // parmStatus.dwReturn / 1000

Retrieving File Length

Like retrieving current position, you can retrieve the full file length the same way. However, you will need to specify the item MCI_STATUS_LENGTH instead.

Seeking a Specific Position

To change the current position, pass MCI the command MCI_SEEK. This command accepts the structure parameter MCI_SEEK_PARMS that is defined as follows:

typedef struct {
    DWORD_PTR dwCallback;
    DWORD     dwTo;
} MCI_SEEK_PARMS;

This parameter defines the following members:

  • dwCallback
    The window to be notified with the MCI_MMNOTIFY message if the call was carried out asynchronously using the MCI_NOTIFY flag.
  • dwTo
    The position (in thousands of a second) to jump to.

The command MCI_SEEK accepts the following flags:

  • The Wait, Notify, and Test flags
    You will always use the Wait flag.
  • MCI_SEEK_TO_END
    If specified, it would jump to the end of the file.
  • MCI_SEEK_TO_START
    If specified, it would jump to the start of the file.
  • MCI_TO
    If specified, MCI would use the value in the dwTo member of the parameter structure.

The following C example jumps to the start of the file:

    MCI_SEEK_PARMS parmSeek;

    parmStatus.dwCallback = 0;
    parmStatus.dwTo          = 0;

    mciSendCommand(wDeviceID, MCI_SEEK,
        MCI_WAIT | MCI_SEEK_TO_START, (DWORD)&parmSeek);

And the following example jumps to the third minute of the file:

    MCI_SEEK_PARMS parmSeek;

    parmStatus.dwCallback = 0;
    parmStatus.dwTo          = 180000;

    mciSendCommand(wDeviceID, MCI_SEEK,
        MCI_WAIT | MCI_TO, (DWORD)&parmSeek);

Closing the Device

You should close the device as soon as you finish working with it, and this is done (as you know) through the MCI_CLOSE command.

In a Nutshell

The following table summarizes the commands that you will usually use with audio files along with their parameter structures and flags.

Command Input/Output Parameter Structure Commonly Used Flags
MCI_OPEN In/Out MCI_OPEN_PARMS MCI_WAIT, MCI_OPEN_ELEMENT, and MCI_OPEN_TYPE
MCI_RECORD In MCI_RECORD_PARMS (none) or MCI_NOTIFY
MCI_PLAY Out MCI_PLAY_PARMS MCI_NOTIFY
MCI_PAUSE In/Out MCI_GENERIC_PARMS MCI_WAIT
MCI_RESUME In/Out MCI_GENERIC_PARMS MCI_WAIT
MCI_STOP In/Out MCI_GENERIC_PARMS MCI_WAIT
MCI_SEEK Out MCI_SEEK_PARMS MCI_WAIT and MCI_TO / MCI_SEEK_TO_START / MCI_SEEK_TO_END
MCI_SAVE In MCI_SAVE_PARMS MCI_WAIT and MCI_SAVE_FILE
MCI_STATUS In/Out MCI_STATUS_PARMS MCI_WAIT and MCI_STATUS_ITEM
MCI_CLOSE In/Out MCI_GENERIC_PARMS MCI_WAIT

MCI and .NET

Creating the Managed Signature

Because .NET does not support MCI and it does not allow you to call unmanaged code directly, you will need to create your own marshaling types and PInvoke methods.

Keep in mind that you can get the handle of a window using the Control.Handle property.

The following class is the managed signature of our unmanaged structures and functions along with the required constants:

internal static class SafeNativeMethods
{
    // Constants

    public const string WaveAudio = "waveaudio";

    public const uint MM_MCINOTIFY = 0x3B9;

    public const uint MCI_NOTIFY_SUCCESSFUL = 0x0001;
    public const uint MCI_NOTIFY_SUPERSEDED = 0x0002;
    public const uint MCI_NOTIFY_ABORTED = 0x0004;
    public const uint MCI_NOTIFY_FAILURE = 0x0008;

    public const uint MCI_OPEN = 0x0803;
    public const uint MCI_CLOSE = 0x0804;
    public const uint MCI_PLAY = 0x0806;
    public const uint MCI_SEEK = 0x0807;
    public const uint MCI_STOP = 0x0808;
    public const uint MCI_PAUSE = 0x0809;
    public const uint MCI_RECORD = 0x080F;
    public const uint MCI_RESUME = 0x0855;
    public const uint MCI_SAVE = 0x0813;
    public const uint MCI_LOAD = 0x0850;
    public const uint MCI_STATUS = 0x0814;

    public const uint MCI_SAVE_FILE = 0x00000100;
    public const uint MCI_OPEN_ELEMENT = 0x00000200;
    public const uint MCI_OPEN_TYPE = 0x00002000;
    public const uint MCI_LOAD_FILE = 0x00000100;
    public const uint MCI_STATUS_POSITION = 0x00000002;
    public const uint MCI_STATUS_LENGTH = 0x00000001;
    public const uint MCI_STATUS_ITEM = 0x00000100;

    public const uint MCI_NOTIFY = 0x00000001;
    public const uint MCI_WAIT = 0x00000002;
    public const uint MCI_FROM = 0x00000004;
    public const uint MCI_TO = 0x00000008;

    // Structures

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public struct MCI_OPEN_PARMS
    {
        public IntPtr dwCallback;
        public uint wDeviceID;
        public IntPtr lpstrDeviceType;
        public IntPtr lpstrElementName;
        public IntPtr lpstrAlias;
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public struct MCI_RECORD_PARMS
    {
        public IntPtr dwCallback;
        public uint dwFrom;
        public uint dwTo;
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public struct MCI_PLAY_PARMS
    {
        public IntPtr dwCallback;
        public uint dwFrom;
        public uint dwTo;
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public struct MCI_GENERIC_PARMS
    {
        public IntPtr dwCallback;
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public struct MCI_SEEK_PARMS
    {
        public IntPtr dwCallback;
        public uint dwTo;
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public struct MCI_SAVE_PARMS
    {
        public IntPtr dwCallback;
        public IntPtr lpfilename;
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public struct MCI_STATUS_PARMS
    {
        public IntPtr dwCallback;
        public uint dwReturn;
        public uint dwItem;
        public uint dwTrack;
    } ;

    // Functions

    [DllImport("winmm.dll", CharSet = CharSet.Ansi,
        BestFitMapping = true, ThrowOnUnmappableChar = true)]
    [return: MarshalAs(UnmanagedType.U4)]
    public static extern uint mciSendCommand(
        uint mciId,
        uint uMsg,
        uint dwParam1,
        IntPtr dwParam2);

    [DllImport("winmm.dll", CharSet = CharSet.Ansi,
        BestFitMapping = true, ThrowOnUnmappableChar = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool mciGetErrorString(
        uint mcierr,
        [MarshalAs(UnmanagedType.LPStr)]
        System.Text.StringBuilder pszText,
        uint cchText);
}

Receiving MCI_MMNOTIFY

In C, you can handle the MCI_MMNOTIFY message in the way you handle any other message. Just add the handler to the window procedure (WndProc) function.

In .NET, you do not have a WndProc function. However, .NET emulates this function using the protected Control.WndProc() function. You can override this function in your form and do the required processing. The following example demonstrates this:

public partial class MainForm : Form
{
    . . .

    protected override void WndProc(ref Message m)
    {
        if (m.Msg == SafeNativeMethods.MM_MCINOTIFY)
        {
            // Handle the message
        }

        // DO NOT REMOVE the following line
        base.WndProc(ref m);
    }
}

Sample Code

Attached to this article is the sample applications, SampleRec (C#) and SMPREC (C).