Export (0) Print
Expand All

Recording and Playing Sound with the Waveform Audio Interface

.NET Compact Framework 1.0
 

Geoff Schwab
Excell Data Corporation

Contributions by:
Seth Demsey
Microsoft Corporation

Jonathan Wells
Microsoft Corporation

January 2004

Applies to:
   
Microsoft® .NET Compact Framework 1.0
   Microsoft Visual Studio® .NET 2003

Summary: Learn how to use the Waveform Audio Interface to record and play ".wav" files. (41 printed pages)

Download P/Invoke Library Sample


Contents

Introduction
The Waveform Audio Interface
The Wave File Format
The Wave Class
Playing with WaveOut
Recording with WaveIn
Conclusion

Introduction

The Waveform Audio Interface provides a powerful resource for playing and recording waveform audio files. The sample provided with the P/Invoke Library implements streaming audio playback and recording through a simple audio API.

Playback of audio is as simple as a single method call, while recording can be done with three. Sample usage is demonstrated by the following code:

// Play a wave file
WaveOut wo = new WaveOut();
wo.Play(fileName, 512*1024, 0xffff, 0xffff);

// Record and save a wave file
WaveIn wi = new WaveIn();
wi.Preload(3000, 256*1024);
wi.Start();
...
wi.Save(fileName);

The sample is provided in both C# and VB but for brevity's sake, all sample code in this document is provided in C#.

The Waveform Audio Interface

The Waveform Audio Interface provides several functions for controlling the hardware audio interface and provides the lowest level of control available to Windows Mobile developers. Help on this API is available in the MSDN library section titled "Using the Waveform Audio Interface".

The following functions and structure are used in the sample to record waveform audio:

  • waveInGetNumDevs(): Determines the number of audio drivers available for input.
  • waveInOpen(): Creates an instance of the specified audio device for input.
  • waveInPrepareHeader(): Prepares a WAVEHDR and data block for input.
  • waveInUnprepareHeader(): Releases a previously prepared WAVEHDR and data block.
  • waveInClose(): Closes the specified instance of the audio device.
  • waveInReset(): Stops recording and empties the queue.
  • waveInStart(): Starts recording to the queued buffer.
  • waveInStop(): Stops recording.
  • waveInAddBuffer(): Adds a prepared buffer to the record queue.
  • waveInGetDevCaps(): Gets the capabilities of the specified device
  • WAVEINCAPS: Describes the capabilities of an input device.

The following functions and structure are used in the sample to play waveform audio:

  • waveOutGetNumDevs(): Determines the number of audio drivers available for output.
  • waveOutOpen(): Creates an instance of the specified audio device for output.
  • waveOutGetVolume(): Returns the volume of the specified output device.
  • waveOutSetVolume(): Sets the volume of the specified output device.
  • waveOutPrepareHeader(): Prepares a WAVEHDR and data block for output.
  • waveOutUnprepareHeader (): Releases a previously prepared WAVEHDR and data block.
  • waveOutWrite(): Starts playing the queued buffer.
  • waveOutClose(): Closes the specified instance of the audio device.
  • waveOutReset(): Stops playing and empties the queue.
  • waveOutPause(): Pauses playback.
  • waveOutRestart(): Resumes paused playback.
  • waveOutGetDevCaps(): Gets the capabilities of the specified device
  • WAVEOUTCAPS: Describes the capabilities of an output device.

Both interfaces share a WAVEHDR definition. This structure provides information about and access to an audio block. WAVEHDR is the interface used to pass waveform data buffers to the audio device for playback and recording. To minimize memory use, the sample utilizes multiple buffers, as it is possible to queue them for playback and recording.

Both the playback and recording samples also utilize a MessageWindow for receiving messages from the audio system when a block has finished.

The Wave File Format

The Waveform Audio Interface provides a mechanism for playing and recording raw uncompressed pulse code modulated (PCM) data. The sample code in this article demonstrates how to utilize the .wav file format to load and save PCM audio data that is compatible with the Waveform Audio Interface.

The .wav file format consists of some header information followed by the raw data. Figure 1 shows the file layout.

Figure 1. WAV File Format.

File format description:

  1. char[4] = "RIFF", The characters "RIFF" indicate the start of the RIFF header
  2. Int32 = FileSize – 8, This is the size of the entire file following this data, i.e., the size of the rest of the file
  3. char[4] = "WAVE", The characters "WAVE" indicate the format of the data.
  4. char[4] = "fmt ", The "fmt " characters specify that this is the section of the file describing the format specifically
  5. Int32 = 16, The size of the WAVEFORMATEX data to follow
  6. WAVEFORMATEX (shown below)
    1. UInt16 wFormatTag, only PCM data is supported in this sample
    2. UInt16 nChannels, Number of channels in (1 for mono, 2 for stereo)
    3. UInt32 nSamplesPerSec, Sample rate of the waveform in samples per second
    4. UInt32 nAvgBytesPerSec, Average bytes per second which can be used to determine the time-wise length of the audio
    5. UInt16 nBlockAlign, Specifies how each audio block must be aligned in bytes
    6. UInt16 wBitsPerSample, How many bits represent a single sample (typically 8 or 16)
  7. char[4] = "data", The "data" characters specify that the audio data is next in the file
  8. Int32, The length of the data in bytes
  9. Data, The rest of the file is the actual samples

The Wave Class

The Wave class is provided to encapsulate features that are common to both the WaveIn and WaveOut API's. It provides definitions for constants, error codes, WAVEFORMATEX, WAVEHDR, and functionality for reading from and writing to audio buffers.

The class defines numerous constants and enum values.

WAVE_MAPPER can be used in place of a device identifier when opening an audio device in order to automatically find a best fit.

public const uint WAVE_MAPPER = unchecked((uint)(-1));

CALLBACK_WINDOW is used when opening an audio device to specify that windows messages will be the method for receiving feedback from the audio system.

public const uint CALLBACK_WINDOW  = 0x10000;

The WF_OFFSET constants are defined to provide locations associated with WAVEFORMATEX members in a file. WF_OFFSET_DATA specifies the file location at which actual audio data starts.

private const int WF_OFFSET_FORMATTAG = 20;
private const int WF_OFFSET_CHANNELS = 22;
private const int WF_OFFSET_SAMPLESPERSEC = 24;
private const int WF_OFFSET_AVGBYTESPERSEC = 28;
private const int WF_OFFSET_BLOCKALIGN = 32;
private const int WF_OFFSET_BITSPERSAMPLE = 34;
public const int WF_OFFSET_DATA = 44;

The MMSYSERR and WAVERR enums define the constants that can be returned from the WaveIn and WaveOut API's. The enum name followed by _ and the value's name correspond to the original API definition. For example, MMSYSERR_ALLOCATED is defined in Wave as MMSYSERR.ALLOCATED.

private const int WAVERR_BASE = 32;
private const int MMSYSERR_BASE = 0;

public enum MMSYSERR : int
{
   NOERROR = 0,
   ERROR = (MMSYSERR_BASE + 1),
   BADDEVICEID = (MMSYSERR_BASE + 2),
   NOTENABLED = (MMSYSERR_BASE + 3),
   ALLOCATED = (MMSYSERR_BASE + 4),
   INVALHANDLE = (MMSYSERR_BASE + 5),
   NODRIVER = (MMSYSERR_BASE + 6),
   NOMEM = (MMSYSERR_BASE + 7),
   NOTSUPPORTED = (MMSYSERR_BASE + 8),
   BADERRNUM = (MMSYSERR_BASE + 9),
   INVALFLAG = (MMSYSERR_BASE + 10),
   INVALPARAM = (MMSYSERR_BASE + 11),
   HANDLEBUSY = (MMSYSERR_BASE + 12),
   INVALIDALIAS = (MMSYSERR_BASE + 13),
   BADDB = (MMSYSERR_BASE + 14),
   KEYNOTFOUND = (MMSYSERR_BASE + 15),
   READERROR = (MMSYSERR_BASE + 16),
   WRITEERROR = (MMSYSERR_BASE + 17),
   DELETEERROR = (MMSYSERR_BASE + 18),
   VALNOTFOUND = (MMSYSERR_BASE + 19),
   NODRIVERCB = (MMSYSERR_BASE + 20),
   LASTERROR = (MMSYSERR_BASE + 20)
}

private enum WAVERR : int
{
   NONE = 0,
   BADFORMAT = WAVERR_BASE + 0,
   STILLPLAYING = WAVERR_BASE + 1,
   UNPREPARED = WAVERR_BASE + 2,
   SYNC = WAVERR_BASE + 3,
   LASTERROR = WAVERR_BASE + 3
}

The flags associated with the dwFormats and dwSupport members of WAVEOUTCAPS and WAVEINCAPS are also centralized in the Wave class. These flags specify the waveform audio formats, and features supported by the audio device.

// Used by dwFormats
public const uint WAVE_INVALIDFORMAT = 0x00000000;
public const uint WAVE_FORMAT_1M08 = 0x00000001;
public const uint WAVE_FORMAT_1S08 = 0x00000002;
public const uint WAVE_FORMAT_1M16 = 0x00000004;
public const uint WAVE_FORMAT_1S16 = 0x00000008;
public const uint WAVE_FORMAT_2M08 = 0x00000010;
public const uint WAVE_FORMAT_2S08 = 0x00000020;
public const uint WAVE_FORMAT_2M16 = 0x00000040;
public const uint WAVE_FORMAT_2S16 = 0x00000080;
public const uint WAVE_FORMAT_4M08 = 0x00000100;
public const uint WAVE_FORMAT_4S08 = 0x00000200;
public const uint WAVE_FORMAT_4M16 = 0x00000400;
public const uint WAVE_FORMAT_4S16 = 0x00000800;

// Used by dwSupport
public const uint WAVECAPS_PITCH = 0x0001;
public const uint WAVECAPS_PLAYBACKRATE = 0x0002;
public const uint WAVECAPS_VOLUME = 0x0004;
public const uint WAVECAPS_LRVOLUME = 0x0008;
public const uint WAVECAPS_SYNC = 0x0010;
public const uint WAVECAPS_SAMPLEACCURATE = 0x0020;
public const uint WAVECAPS_DIRECTSOUND = 0x0040;

The WAVEHDR class also defines several constants used by the dwFlags member. These flags specify the current state of the audio buffer.

public const int WHDR_DONE = 0x00000001;
public const int WHDR_PREPARED = 0x00000002;
public const int WHDR_BEGINLOOP = 0x00000004;
public const int WHDR_ENDLOOP = 0x00000008;
public const int WHDR_INQUEUE = 0x00000010;

WAVE_FORMAT_PCM is the flag specified by wFormatTag in WAVEFORMATEX if the audio file contains PCM data - the only type supported by this API.

public const int WAVE_FORMAT_PCM = 1;

The WAVEFORMATEX class defines part of the header information found in the .wave file. This class is also used to specify to the waveOutOpen and waveInOpen functions, the requested audio format that the device should use. This class implements several methods:

  • SeekTo: moves the provided Stream's position to the start of the header, assuming that the Stream represents a .wav file
  • Skip: moves the provided Stream's position to the first byte following the header, assuming that the Stream represents a .wav file
  • Read: Reads the members of the class from a BinaryReader in the order that they appear in a .wav file
  • Write: Writes the members of the class to a BinaryWriter in the order that they appear in a .wav file
Note: The Read method reads in the data following the WAVEFORMATEX members, such that after reading, the Stream's position is at the audio data. The Write method, on the other hand, does not write this extra data, as it has no knowledge of the required information.
public class WAVEFORMATEX
{
   public ushort wFormatTag = 0;
   public ushort nChannels = 0;
   public uint nSamplesPerSec = 0;
   public uint nAvgBytesPerSec = 0;
   public ushort nBlockAlign = 0;
   public ushort wBitsPerSample = 0;

   public void SeekTo(Stream fs)
   {
      fs.Seek(WF_OFFSET_FORMATTAG, SeekOrigin.Begin);
   }

   public void Skip(Stream fs)
   {
      fs.Seek(WF_OFFSET_DATA, SeekOrigin.Begin);
   }

   public uint Read(BinaryReader rdr)
   {
      wFormatTag = rdr.ReadUInt16();
      nChannels = rdr.ReadUInt16();
      nSamplesPerSec = rdr.ReadUInt32();
      nAvgBytesPerSec = rdr.ReadUInt32();
      nBlockAlign = rdr.ReadUInt16();
      wBitsPerSample = rdr.ReadUInt16();

      // Unused subchunk Id and size
      uint dataId = rdr.ReadUInt32();
      uint dataLength = rdr.ReadUInt32();

      return dataLength;
   }

   public void Write(BinaryWriter wrtr)
   {
      wrtr.Write(wFormatTag);
      wrtr.Write(nChannels);
      wrtr.Write(nSamplesPerSec);
      wrtr.Write(nAvgBytesPerSec);
      wrtr.Write(nBlockAlign);
      wrtr.Write(wBitsPerSample);
   }
}

The WAVEHDR class is used to supply information about, and access to an audio block. This block is simply a buffer filled with audio data that can be sent to the audio system for playback, or an empty buffer that will be the recipient of recording data. The WAVEHDR class implements the IDisposable interface in order to facilitate a method for freeing memory allocated for the audio block buffer. The WAVEHDR class implements several methods:

  • Read: Allocates a buffer, if one is not already allocated, and reads audio data into it from the BinaryReader
  • Write: Writes any recorded data from the buffer to the BinaryWriter
  • Init: Allocates a buffer, if one is not already allocated of the correct size, and clears the data if specified to
public class WAVEHDR : IDisposable
{
   public IntPtr lpData = IntPtr.Zero;
   public uint dwBufferLength = 0;
   public uint dwBytesRecorded = 0;
   public uint dwUser = 0;
   public uint dwFlags = 0;
   public uint dwLoops = 0;
   public IntPtr lpNext = IntPtr.Zero;
   public uint reserved = 0;

   public MMSYSERR Read(BinaryReader rdr, uint readLength)
   {
      dwBufferLength = readLength;
      byte[] data = new byte[readLength];
      rdr.Read(data, 0, data.Length);

      if (lpData == IntPtr.Zero)
         lpData = Memory.LocalAlloc(Memory.LMEM_FIXED,
            (uint)data.Length);

      if (lpData == IntPtr.Zero)
          return MMSYSERR.NOMEM;

       Marshal.Copy(data, 0, lpData, data.Length);

       return MMSYSERR.NOERROR;
    }

   public Wave.MMSYSERR Write(BinaryWriter wrtr)
   {
      if (lpData == IntPtr.Zero)
         return Wave.MMSYSERR.NOMEM;

      byte[] data = new byte[dwBytesRecorded];
      Marshal.Copy(lpData, data, 0, data.Length);
      wrtr.Write(data);

      return Wave.MMSYSERR.NOERROR;
   }

   public MMSYSERR Init(uint bufferLength, bool init)
   {
      if (lpData != IntPtr.Zero && dwBufferLength < bufferLength)
      {
         Memory.LocalFree(lpData);
         lpData = IntPtr.Zero;
      }

      if (lpData == IntPtr.Zero)
         lpData = Memory.LocalAlloc(Memory.LMEM_FIXED,
            bufferLength);

      dwBufferLength = bufferLength;

      if (lpData == IntPtr.Zero)
         return MMSYSERR.NOMEM;

      if (init)
      {
         for (int i = 0; i < bufferLength; i++)
         {
            Marshal.WriteByte(lpData, i, 0);
         }
      }

      return MMSYSERR.NOERROR;
   }

   public void Dispose()
   {
      if (lpData != IntPtr.Zero)
         Memory.LocalFree(lpData);
   }
}

Playing with WaveOut

The WaveOut class is responsible for encapsulating and providing a simple interface for playing audio files. Due to the complexity of the Waveform Audio Interface API, the architecture is divided into two layers. The WaveFile class encapsulates all low-level API function calls, while the WaveOut class provides a clean interface that encapsulates the WaveFile class and handles the messaging from the audio device through a MessageWindow instance.

To reduce memory use, the sample allows the user to specify a buffer size. If the buffer size is set to 0 then it will be made large enough to contain the entire wave file, otherwise, the audio file will be streamed using two audio buffers, each being half of the specified total size. The streaming mechanism is maintained by loading one buffer while the other plays. Therefore it is important to always have a second buffer queued before the first ends.

This article will make references to blocks and buffers so it is important to understand the difference. Figure 2 describes these differences. A block represents a buffer position in the audio file, whereas a buffer represents the physical memory allocated for loading and playing blocks.

Figure 2. Block and Buffer association.

The general process for creating an instance of WaveOut and playing a .wav file is as follows:

  1. Get the number of output audio devices
  2. Enumerate the devices and select the appropriate one
  3. Open the audio device
  4. Load audio data into WAVEHDR instances (partial if streaming)
  5. Prepare the headers
  6. Write the headers to the audio device (put them in the queue for playback)

The application must then wait for a message from the audio system indicating that an audio block has finished, at which point it will determine what to do next. If the audio block is the last one then playback is stopped and resources are cleaned up, otherwise, the buffer is reused to load the next block while the other buffer is playing.

Note: It is important to note that at the time the message is received indicating an audio block has finished playing, it may be too late to stop the audio system. In other words, the audio playback mechanism may continue to attempt reading from the buffer before the message is processed by the application. Therefore, this sample supplies an empty buffer to the queue when the last valid audio block is being played.

The WaveFile class is implements the IDisposable interface so that any associated WAVEHDR's can be disposed.

public class WaveFile : IDisposable
{

The WaveFile class internally maintains all data necessary to play the file. The m_hwo member is a handle to the selected audio device, m_wfmt contains the data associated with the wave file, and m_whdr is responsible for maintaining the audio block streaming buffers.

   protected IntPtr m_hwo = IntPtr.Zero;
   protected Wave.WAVEFORMATEX m_wfmt = null;
   protected Wave.WAVEHDR[] m_whdr = null;

The size of the streaming buffers is stored in m_bufferSize, while m_numBlocks specifies the total number of blocks needed to the play the file - this is not the same as the number of buffers being used for playback. The m_curBlock member is the block that is currently being played - once again, this is not the same as the current buffer.

   protected int m_bufferSize;
   protected int m_numBlocks;
   protected int m_curBlock;

A BinaryReader instance, which accesses the streaming file, is kept until the entire file is read. This means that this class will have the file open during the duration of playback unless the entire file is loaded into the buffer at the start.

   protected BinaryReader m_rdr = null;

The Done property provides a means for determining if playback is in progress or finished. Any time that the wave file is not in playback, it is considered to be done.

   public bool Done { get { return !m_playing; } }
   protected bool m_playing = false;

The Seconds property specifies the length of the audio in seconds.

   public uint Seconds
     { get { return m_dataLength / m_wfmt.nAvgBytesPerSec; } }
   protected uint m_dataLength;

The constructor for the WaveFile class quite simply allocates the array of audio buffers.

   public WaveFile()
   {
      m_whdr = new Wave.WAVEHDR[2];
   }

The Play method is responsible for starting playback of an audio file. This is accomplished by accessing the file, opening the audio device, setting up the audio buffers, loading the initial block, starting playback of the first buffer, and starting the loading of the second audio buffer on a separate thread.

   public Wave.MMSYSERR Play(uint curDevice, String fileName,
      IntPtr hwnd, int bufferSize, ushort volLeft, ushort volRight)
   {
      if (m_playing)
         return Wave.MMSYSERR.NOERROR;

      if (!File.Exists(fileName))
         return Wave.MMSYSERR.ERROR;

      FileInfo fi = new FileInfo(fileName);
      if ((fi.Attributes & FileAttributes.ReadOnly) != 0)
         fi.Attributes -= FileAttributes.ReadOnly;

      FileStream strm = new FileStream(fileName, FileMode.Open);
      if (strm == null)
         return Wave.MMSYSERR.ERROR;

      m_rdr = new BinaryReader(strm);
      if (m_rdr == null)
         return Wave.MMSYSERR.ERROR;

      m_wfmt = new Wave.WAVEFORMATEX();
      m_wfmt.SeekTo(strm);

      m_dataLength = m_wfmt.Read(m_rdr);
      if (m_dataLength == 0)
         return Wave.MMSYSERR.ERROR;

      Wave.MMSYSERR result = waveOutOpen(ref m_hwo, curDevice,
         m_wfmt, hwnd, 0, Wave.CALLBACK_WINDOW);
      if (result != Wave.MMSYSERR.NOERROR)
         return result;

      if (m_rdr.BaseStream.Length - Wave.WF_OFFSET_DATA !=
         m_dataLength)
         return Wave.MMSYSERR.READERROR;

      if (bufferSize == 0)
         m_bufferSize = (int)m_dataLength;
      else
         m_bufferSize = bufferSize / 2;

      if (m_bufferSize % m_wfmt.nBlockAlign != 0)
         m_bufferSize += m_wfmt.nBlockAlign - (m_bufferSize %
            m_wfmt.nBlockAlign);

      m_numBlocks = (int)(m_dataLength / m_bufferSize);
      if (m_dataLength % m_bufferSize != 0)
         m_numBlocks++;

      m_whdr[0] = new Wave.WAVEHDR();
      m_whdr[1] = new Wave.WAVEHDR();

      result = ReadBuffer(0);
      if (result != Wave.MMSYSERR.NOERROR)
         return result;

      if (m_numBlocks == 1)
      {
         m_rdr.BaseStream.Close();
         m_rdr.Close();
         m_rdr = null;
      }

      SetVolume(volLeft, volRight);

      result = waveOutWrite(m_hwo, m_whdr[0],
         (uint)Marshal.SizeOf(m_whdr[0]));
      if (result != Wave.MMSYSERR.NOERROR)
         return result;

      Thread loadThread = new Thread(new ThreadStart(LoadBuffer));
      loadThread.Start();

      m_curBlock = 0;

      m_playing = true;

      return Wave.MMSYSERR.NOERROR;
   }

The ReadBuffer method is responsible for reading audio data from the cached BinaryReader into the specified audio buffer and preparing the WAVEHDR with the audio system. If this is the last block then the method determines how much data is remaining in the file and sets the buffer size accordingly.

Note: A WAVEHDR must be prepared after the buffer is allocated and before it is written to the audio system.
   protected Wave.MMSYSERR ReadBuffer(int bufIndex)
   {
      uint readLength = (uint)m_bufferSize;
      if (bufIndex < m_numBlocks)
      {
         uint remainingDataLength = (uint)(m_rdr.BaseStream.Length –
            m_rdr.BaseStream.Position);
         if (m_bufferSize > remainingDataLength)
            readLength = remainingDataLength;
      }

      Wave.MMSYSERR result = m_whdr[bufIndex].Read(m_rdr,
         readLength);
      if (result != Wave.MMSYSERR.NOERROR)
         return result;

      if ((m_whdr[bufIndex].dwFlags & Wave.WHDR_PREPARED) == 0)
      {
         return waveOutPrepareHeader(m_hwo, m_whdr[bufIndex],
            (uint)Marshal.SizeOf(m_whdr[bufIndex]));
      }

      return Wave.MMSYSERR.NOERROR;
   }

The CreateBuffer method allocates a buffer, if not already allocated, and clears its data. This method is specifically used to create the padding buffer at the end of playback. A padding buffer will always be the last buffer in the queue before playback is stopped.

   protected Wave.MMSYSERR CreateBuffer(int bufIndex)
   {
      Wave.MMSYSERR result = 
         m_whdr[bufIndex].Init((uint)m_bufferSize, true);
      if (result != Wave.MMSYSERR.NOERROR)
         return result;

      if ((m_whdr[bufIndex].dwFlags & Wave.WHDR_PREPARED) == 0)
      {
         return waveOutPrepareHeader(m_hwo, m_whdr[bufIndex],
            (uint)Marshal.SizeOf(m_whdr[bufIndex]));
      }

      return Wave.MMSYSERR.NOERROR;
   }

The LoadBuffer method determines whether the next buffer in the queue should be loaded from the file or created as a padding buffer based on the previously queued block. If the block currently playing is the last block then the padding buffer is created, otherwise the next block is loaded into whichever buffer is not currently busy playing.

   public void LoadBuffer()
   {
      int readBuffer = (m_curBlock + 3) % 2;

      lock(m_whdr[readBuffer])
      {
         if (m_curBlock == m_numBlocks - 1)
            CreateBuffer(readBuffer);
         else
            ReadBuffer(readBuffer);

         waveOutWrite(m_hwo, m_whdr[readBuffer],
            (uint)Marshal.SizeOf(m_whdr[readBuffer]));
      }
   }

The BlockDone method is called in response to the MM_WOM_DONE message. This method will stop playback if the last block has finished playing, otherwise it will start loading the next block on a separate thread.

   public void BlockDone()
   {
      m_curBlock++;

      if (m_curBlock < m_numBlocks)
      {
         Debug.Assert((m_whdr[(m_curBlock + 2) % 2].dwFlags &
             Wave.WHDR_INQUEUE) != 0,
            "ERROR: A sound block finished before the subsequent block was written.");

         Thread loadThread = new Thread(new
            ThreadStart(LoadBuffer));
         loadThread.Start();
      }
      else if (m_curBlock == m_numBlocks)
      {
         Stop();
      }
   }

The Stop method stops all playback and cleans up any allocated resources.

Note: When implementing a resource intensive sound library that replays sounds numerous times it may make sense to keep the buffers allocated until the sound is unloaded. This could be especially useful in the case of smaller audio files that do not need to be streamed and can be kept in memory instead of reloaded every time they are played.
   public void Stop()
   {
      waveOutReset(m_hwo);

      m_playing = false;

      if (m_rdr != null)
      {
         m_rdr.BaseStream.Close();
         m_rdr.Close();
         m_rdr = null;
      }

      for (int i = 0; i < 2; i++)
      {
         if (m_whdr[i] != null)
         {
            lock(m_whdr[i])
            {
               if (m_hwo != IntPtr.Zero)
                  waveOutUnprepareHeader(m_hwo, m_whdr[i],
                     (uint)Marshal.SizeOf(m_whdr[i]));

               m_whdr[i].Dispose();
               m_whdr[i] = null;
            }
         }
      }

      if (m_hwo != IntPtr.Zero)
         waveOutClose(m_hwo);

      m_hwo = IntPtr.Zero;
      m_wfmt = null;

   }

The Resume method resumes playback of a paused sound. It is not necessary to track the state of the sound because waveOutRestart is safe to call on sounds that are not paused.

   public Wave.MMSYSERR Resume()
   {
      return waveOutRestart(m_hwo);
   }

The Pause method pauses playback until Resume is called to restart it.

   public Wave.MMSYSERR Pause()
   {
      return waveOutPause(m_hwo);
   }

GetVolume returns the volume level set for this specific sound.

   public Wave.MMSYSERR GetVolume(ref ushort volLeft,
      ref ushort volRight)
   {
      uint vol = 0;

      Wave.MMSYSERR result = waveOutGetVolume(m_hwo, ref vol);
      if (result != Wave.MMSYSERR.NOERROR)
         return result;

      volLeft = (ushort)(vol & 0x0000ffff);
      volRight = (ushort)(vol >> 16);

      return Wave.MMSYSERR.NOERROR;
   }

The SetVolume method sets the volume level for this specific sound.

   public Wave.MMSYSERR SetVolume(ushort volLeft, ushort volRight)
   {
      uint vol = ((uint)volLeft & 0x0000ffff) |
         ((uint)volRight << 16);
      return waveOutSetVolume(m_hwo, vol);
   }

Dispose stops playback and cleans up an allocated resources.

   public void Dispose()
   {
      Stop();
   }
}

The SoundMessageWindow is derived from the MessageWindow class and handles the passing of the audio system messages back to the application. The constructor for this class takes an instance of WaveOut as its parameter. This instance is cached and later used to notify the system of audio messages.

public class SoundMessageWindow : MessageWindow
{
   public const int MM_WOM_OPEN  = 0x03BB;
   public const int MM_WOM_CLOSE = 0x03BC;
   public const int MM_WOM_DONE  = 0x03BD;

   protected WaveOut m_wo = null;

   public SoundMessageWindow(WaveOut wo)
   {
      m_wo = wo;
   }

   protected override void WndProc(ref Message msg)
   {
      switch (msg.Msg)
      {
         case MM_WOM_DONE:
            m_wo.BlockDone(msg.WParam);
            break;

      }
      base.WndProc(ref msg);
   }
}

The WaveOut class maintains two data members: an instance of SoundMessageWindow, and an instance of WaveFile which will be used to play an audio file.

   protected SoundMessageWindow m_msgWindow = null;
   protected WaveFile m_file = null;

The constructor for WaveOut instantiates the SoundMessageWindow and WaveFile instances.

   public WaveOut()
   {
      m_msgWindow = new SoundMessageWindow(this);
      m_file = new WaveFile();
   }

The NumDevices method provides the number of available audio output devices.

   public uint NumDevices()
   {
      return (uint)waveOutGetNumDevs();
   }

GetDeviceName uses WAVEOUTCAPS to determine the name of the specified device.

   public Wave.MMSYSERR GetDeviceName(uint deviceId,
      ref string prodName)
   {
      WAVEOUTCAPS caps = new WAVEOUTCAPS();
      Wave.MMSYSERR result = waveOutGetDevCaps(deviceId, caps,
         caps.Size);
      if (result != Wave.MMSYSERR.NOERROR)
         return result;

      prodName = caps.szPname;

      return Wave.MMSYSERR.NOERROR;
   }

The following methods simply encapsulate the corresponding WaveFile methods.

   public bool Done()
   {
      if (m_file != null)
         return m_file.Done;

      return true;
   }

   public Wave.MMSYSERR Pause()
   {
      if (m_file != null)
         return m_file.Pause();

      return Wave.MMSYSERR.NOERROR;
   }

   public Wave.MMSYSERR Resume()
   {
      if (m_file != null)
         return m_file.Resume();

      return Wave.MMSYSERR.NOERROR;
   }

   public uint Seconds()
   {
      if (m_file != null)
         return m_file.Seconds;

      return 0;
   }

   public Wave.MMSYSERR Play(string fileName, int bufferSize,
      ushort volLeft, ushort volRight)
   {
      if (m_file != null)
         return m_file.Play(0, fileName, m_msgWindow.Hwnd, bufferSize,
            volLeft, volRight);

      return Wave.MMSYSERR.ERROR;
   }

   public void BlockDone(IntPtr hwo)
   {
      if (m_file != null)
         m_file.BlockDone();
   }

   public void Dispose()
   {
      if (m_msgWindow != null)
         m_msgWindow.Dispose();

      if (m_file != null)
         m_file.Dispose();
   }

The following P/Invoke declarations are defined in the WaveOut class. These methods are detailed in the comments of the code accompanying the P/Invoke Library sample.

   [DllImport ("coredll.dll")]
   protected static extern int waveOutGetNumDevs();

   [DllImport ("coredll.dll")]
   private static extern Wave.MMSYSERR waveOutOpen(ref IntPtr phwi,
      uint uDeviceID, Wave.WAVEFORMATEX pwfx, IntPtr dwCallback,
      uint dwInstance, uint fdwOpen);

   [DllImport ("coredll.dll")]
   protected static extern Wave.MMSYSERR waveOutGetVolume(IntPtr hwo,
      ref uint pdwVolume);

   [DllImport ("coredll.dll")]
   protected static extern Wave.MMSYSERR waveOutSetVolume(IntPtr hwo,
      uint dwVolume);

   [DllImport ("coredll.dll")]
   private static extern Wave.MMSYSERR waveOutPrepareHeader(IntPtr hwo,
      Wave.WAVEHDR pwh, uint cbwh);

   [DllImport ("coredll.dll")]
   private static extern Wave.MMSYSERR waveOutWrite(IntPtr hwo,
      Wave.WAVEHDR pwh, uint cbwh);

   [DllImport ("coredll.dll")]
   private static extern Wave.MMSYSERR waveOutUnprepareHeader(IntPtr
      hwo, Wave.WAVEHDR pwh, uint cbwh);

   [DllImport ("coredll.dll")]
   protected static extern Wave.MMSYSERR waveOutClose(IntPtr hwo);

   [DllImport ("coredll.dll")]
   protected static extern Wave.MMSYSERR waveOutReset(IntPtr hwo); 

   [DllImport ("coredll.dll")]
   protected static extern Wave.MMSYSERR waveOutPause(IntPtr hwo);
 
   [DllImport ("coredll.dll")]
   protected static extern Wave.MMSYSERR waveOutRestart(IntPtr hwo);

   protected class MMTIME
   {
      public uint wType = 0;
      public uint cb = 0;

      // Padding because this is actually a union
      public uint pad = 0;
   } 

   // Used by MMTIME.wType
   protected const uint TIME_MS = 0x0001;
   protected const uint TIME_SAMPLES = 0x0002;
   protected const uint TIME_BYTES = 0x0004;
   protected const uint TIME_TICKS = 0x0020;

   [DllImport ("coredll.dll")]
   protected static extern int waveOutGetPosition(IntPtr hwo,
      MMTIME pmmt, uint cbmmt); 

   protected class WAVEOUTCAPS
   {
      const uint WAVEOUTCAPS_SIZE = 84;

      public static implicit operator byte[](WAVEOUTCAPS caps)
      {
          return caps.m_data;
      }

      private byte[] m_data = null;
      public uint Size
         { get { return (uint)WAVEOUTCAPS_SIZE; } }

      public ushort wMid
         { get { return BitConverter.ToUInt16(m_data, 0); } }
      public ushort wPid
         { get { return BitConverter.ToUInt16(m_data, 2); } }
      public uint vDriverVersion
         { get { return BitConverter.ToUInt32(m_data, 4); } }
      public uint dwFormats
         { get { return BitConverter.ToUInt32(m_data, 72); } }
      public ushort wChannels
         { get { return BitConverter.ToUInt16(m_data, 76); } }
      public ushort wReserved1
         { get { return BitConverter.ToUInt16(m_data, 78); } }
      public uint dwSupport
         { get { return BitConverter.ToUInt16(m_data, 80); } }

      public WAVEOUTCAPS()
      {
         m_data = new byte[WAVEOUTCAPS_SIZE];
      }

      public string szPname
      {
         get
         {
            char[] bytes = new char[32];
            for (int i = 0; i < 32; i++)
            {
               bytes[i] = (char)BitConverter.ToUInt16(m_data,
                  i * 2 + 8);
            }

            return new string(bytes);
         }
      }
   }

   [DllImport ("coredll.dll")]
   protected static extern Wave.MMSYSERR waveOutGetDevCaps(uint
      uDeviceID, byte[] pwoc, uint cbwoc);

Recording with WaveIn

The WaveIn class is responsible for encapsulating and providing a simple interface for recording and saving audio files. Due to the complexity of the Waveform Audio Interface API, the architecture is divided into two layers. The WaveFile class encapsulates all low-level API function calls, while the WaveIn class provides a clean interface that encapsulates the WaveFile class and handles the messaging from the audio device through a MessageWindow instance.

To reduce memory use, the sample allows the user to specify a buffer size. If the buffer size is set to 0 then it will be made large enough to contain the entire wave file, otherwise, the audio being recorded will be streamed to buffers that are created only when needed. The streaming mechanism is maintained by creating and queuing one buffer while the other records. Therefore it is important to always have a second buffer queued before the first ends.

Unlike WaveOut, the terms block and buffer are interchangeable when discussing the WaveIn class. This is because a new buffer has to be allocated for each block, whereas WaveOut reuses buffers as blocks progress through the file.

The general process for creating an instance of WaveIn and recording a .wav file is as follows:

  1. Get the number of input audio devices
  2. Enumerate the devices and select the appropriate one
  3. Open the audio device
  4. Allocate buffers and create WAVEHDR instances (partial if streaming)
  5. Prepare the headers
  6. Add the headers to the queue
  7. Start recording until stopped or buffer runs out

The application must then wait for a message from the audio system indicating that an audio block has finished, at which point it will determine what to do next. If the audio block is the last one then recording is stopped, otherwise, a new buffer is created to record the next block.

Note: It is important to note that at the time the message is received indicating an audio block has finished recording, it may be too late to stop the audio system. In other words, the audio recording mechanism may continue to attempt writing to the buffer before the message is processed by the application. Therefore, this sample supplies an empty buffer to the queue when the last valid audio block is being recorded.

The WaveIn class provides a method for saving the recorded blocks to a file. If the blocks are not saved before a subsequent recording is started then the resources are freed and the previous data is lost.

The WaveFile class implements the IDisposable interface so that any associated WAVEHDR's can be disposed.

public class WaveIn
{
   protected class WaveFile : IDisposable
   {

The m_hwi member is a handle to the opened audio device, m_wfmt contains information about the desired input waveform, and m_whdr is the array of buffers that will be allocated for recording.

      protected IntPtr m_hwi = IntPtr.Zero;
      protected Wave.WAVEFORMATEX m_wfmt = null;
      protected Wave.WAVEHDR[] m_whdr = null;

The size of each streaming buffer is stored in m_bufferSize, while m_numBlocks specifies the total number of blocks needed to record the specified maximum sound length. The m_curBlock member is the block that is currently being recorded. The m_inited member tracks whether the buffers have been allocated for recording and the m_maxDataLength member contains the maximum size, in bytes of the entire recording.

      protected bool m_inited = false;
      protected int m_curBlock;
      protected int m_numBlocks;
      protected uint m_bufferSize;
      protected uint m_maxDataLength;

The Done method specifies whether the recording has finished. Recording will go until either the maximum specified length is reached or it is stopped with a call to Stop.

      public bool Done { get { return !m_recording; } }
      protected bool m_recording = false;

The Preload function is the first step in starting a recording. This method must be called to initialize the buffers and sound system before recording. Recording is started in two steps so as to alleviate any lag in the starting of recording due to initialization. This method opens the audio device and allocates, prepares, and adds buffers to the recording queue.

      public Wave.MMSYSERR Preload(uint curDevice, IntPtr hwnd,
         int maxRecordLength_ms, int bufferSize)
      {
         if (m_recording)
            return Wave.MMSYSERR.ERROR;

         if (m_inited)
         {
            Stop();
            FreeWaveBuffers();
         }

         WAVEINCAPS caps = new WAVEINCAPS();
         waveInGetDevCaps(0, caps, caps.Size);

         if ((caps.dwFormats & Wave.WAVE_FORMAT_2S16) == 0)
            return Wave.MMSYSERR.NOTSUPPORTED;

         m_wfmt = new Wave.WAVEFORMATEX();
         m_wfmt.wFormatTag = Wave.WAVE_FORMAT_PCM;
         m_wfmt.wBitsPerSample = 16;
         m_wfmt.nChannels = 2;
         m_wfmt.nSamplesPerSec = 22050;
         m_wfmt.nAvgBytesPerSec = (uint)(m_wfmt.nSamplesPerSec *
            m_wfmt.nChannels * (m_wfmt.wBitsPerSample / 8));
         m_wfmt.nBlockAlign = (ushort)(m_wfmt.wBitsPerSample *
            m_wfmt.nChannels / 8);

         Wave.MMSYSERR result = waveInOpen(ref m_hwi, curDevice,
            m_wfmt, hwnd, 0, Wave.CALLBACK_WINDOW);
         if (result != Wave.MMSYSERR.NOERROR)
            return result;

         if (bufferSize == 0)
            return Wave.MMSYSERR.ERROR;

         m_bufferSize = (uint)bufferSize;

         if (m_bufferSize % m_wfmt.nBlockAlign != 0)
            m_bufferSize += m_wfmt.nBlockAlign - (m_bufferSize %
               m_wfmt.nBlockAlign);

         m_maxDataLength = (uint)(m_wfmt.nAvgBytesPerSec *
            maxRecordLength_ms / 1000);
         m_numBlocks = (int)(m_maxDataLength / m_bufferSize);
         if (m_maxDataLength % m_bufferSize != 0)
            m_numBlocks++;

         m_whdr = new Wave.WAVEHDR[m_numBlocks + 1];

         m_whdr[0] = new Wave.WAVEHDR();
         m_whdr[1] = new Wave.WAVEHDR();

         result = InitBuffer(0);
         if (result != Wave.MMSYSERR.NOERROR)
            return result;

         result = InitBuffer(1);
         if (result != Wave.MMSYSERR.NOERROR)
            return result;

         m_curBlock = 0;
         m_inited = true;

         return Wave.MMSYSERR.NOERROR;
      }

The BlockDone method is called in response to the MM_WIM_DATA message, indicating that a block has finished recording. If the block is the last block in the file then recording is stopped, otherwise a new buffer is allocated and added to the queue.

      public void BlockDone()
      {
         m_curBlock++;

         if (m_curBlock < m_numBlocks)
         {
            InitBuffer(m_curBlock + 1);
         }
         else if (m_curBlock == m_numBlocks)
         {
            Stop();
         }
      }

The InitBuffer method allocates memory for recording a single block of audio data. In the case of the last block, the buffer is set to the size required to record for the remainder of the time needed.

      public Wave.MMSYSERR InitBuffer(int bufIndex)
      {
         uint writeLength = (uint)m_bufferSize;
         if (bufIndex < m_numBlocks)
         {
            uint remainingDataLength = (uint)(m_maxDataLength –
               bufIndex * m_bufferSize);
            if (m_bufferSize > remainingDataLength)
               writeLength = remainingDataLength;
         }

         if (m_whdr[bufIndex] == null)
            m_whdr[bufIndex] = new Wave.WAVEHDR();

         Wave.MMSYSERR result = m_whdr[bufIndex].Init(writeLength,
            false);
         if (result != Wave.MMSYSERR.NOERROR)
            return result;

         result = waveInPrepareHeader(m_hwi, m_whdr[bufIndex],
            (uint)Marshal.SizeOf(m_whdr[bufIndex]));
         if (result != Wave.MMSYSERR.NOERROR)
            return result;

         return waveInAddBuffer(m_hwi, m_whdr[bufIndex],
            (uint)Marshal.SizeOf(m_whdr[bufIndex]));

      }

The Start method begins recording. Preload must be called prior to Start or it will return with an error.

      public Wave.MMSYSERR Start()
      {
         if (!m_inited || m_recording)
            return Wave.MMSYSERR.ERROR;

         Wave.MMSYSERR result = waveInStart(m_hwi);
         if (result != Wave.MMSYSERR.NOERROR)
            return result;

         m_recording = true;

         return Wave.MMSYSERR.NOERROR;
      }

The FreeWaveBuffers method frees any resources allocated for recording. This is a separate method because the audio buffers need to be kept after recording finishes to give the user the opportunity to save them. If the buffers are saved or recording is started before they are saved, then the buffers will be freed at that time.

      private void FreeWaveBuffers()
      {
         m_inited = false;

         if (m_whdr != null)
         {
            for(int i = 0; i < m_whdr.Length; i++)
            {
               if (m_whdr[i] != null)
               {
                  waveInUnprepareHeader(m_hwi, m_whdr[i],
                     (uint)Marshal.SizeOf(m_whdr[i]));

                  m_whdr[i].Dispose();
                  m_whdr[i] = null;
               }
            }

            m_whdr = null;
         }

         waveInClose(m_hwi);
      
         m_hwi = IntPtr.Zero;
      }

The WriteChars method is a helper function for the Save method. It simply writes the characters of a string to the BinaryWriter. This is used instead of writing the string directly to prevent the length from being written.

      private void WriteChars(BinaryWriter wrtr, string text)
      {
         for (int i = 0; i < text.Length; i++)
         {
            char c = (char)text[i];
            wrtr.Write(c);
         }
      }

The Save method saves the recording to a file. The method writes all of the appropriate header information and then iterates through each buffer and writes the recorded data.

      public Wave.MMSYSERR Save(string fileName)
      {
         if (!m_inited)
            return Wave.MMSYSERR.ERROR;

         if (m_recording)
            Stop();

         FileStream strm = null;
         BinaryWriter wrtr = null;

         try
         {
            if (File.Exists(fileName))
            {
               FileInfo fi = new FileInfo(fileName);
               if ((fi.Attributes & FileAttributes.ReadOnly) != 0)
                  fi.Attributes -= FileAttributes.ReadOnly;

               strm = new FileStream(fileName, FileMode.Truncate);
            }
            else
            {
               strm = new FileStream(fileName, FileMode.Create);
            }

            if (strm == null)
               return Wave.MMSYSERR.ERROR;

            wrtr = new BinaryWriter(strm);
            if (wrtr == null)
               return Wave.MMSYSERR.ERROR;

            uint totalSize = 0;
            for (int i = 0; i < m_numBlocks; i++)
            {
               if (m_whdr[i] != null)
                  totalSize += m_whdr[i].dwBytesRecorded;
            }

            int chunkSize = (int)(36 + totalSize);

            WriteChars(wrtr, "RIFF");
            wrtr.Write(chunkSize);
            WriteChars(wrtr, "WAVEfmt ");
            wrtr.Write((int)16);
            m_wfmt.Write(wrtr);
            WriteChars(wrtr, "data");
            wrtr.Write((int)totalSize);

            for (int i = 0; i < m_numBlocks; i++)
            {
               if (m_whdr[i] != null)
               {
                  Wave.MMSYSERR result = m_whdr[i].Write(wrtr);
                  if (result != Wave.MMSYSERR.NOERROR)
                     return result;
               }
            }

            return Wave.MMSYSERR.NOERROR;
         }
         finally
         {
            FreeWaveBuffers();

            if (strm != null)
               strm.Close();

            if (wrtr != null)
               wrtr.Close();
         }
      }

The Stop method stops recording and leaves the state of the record buffers so they can be saved.

      public void Stop()
      {
         waveInReset(m_hwi);

         m_recording = false;
      }

Dispose stops any recording in progress and frees all resources.

      public void Dispose()
      {
         Stop();

         FreeWaveBuffers();
      }
   }

The SoundMessageWindow is derived from the MessageWindow class and handles the passing of the audio system messages back to the application. The constructor for this class takes an instance of WaveIn as its parameter. This instance is cached and later used to notify the system of audio messages.

   protected class SoundMessageWindow : MessageWindow
   {
      // note custom window messages are after WM_USER = 0x400
      public const int MM_WIM_OPEN = 0x3BE;
      public const int MM_WIM_CLOSE = 0x3BF;
      public const int MM_WIM_DATA = 0x3C0;

      protected WaveIn m_wi = null;

      public SoundMessageWindow(WaveIn wi)
      {
         m_wi = wi;
      }

      protected override void WndProc(ref Message msg)
      {
         switch(msg.Msg)
         {
            case MM_WIM_DATA:
            {
               if (m_wi != null)
                  m_wi.BlockDone();
            }
            break;
         }
         base.WndProc(ref msg);
      }
   }

The WaveIn class maintains two data members: m_msgWindow is an instance of SoundMessageWindow, and m_file is an instance of WaveFile.

   protected SoundMessageWindow m_msgWindow = null;
   protected WaveFile m_file = null;

The WaveIn constructor instantiates the data members.

   public WaveIn()
   {
      m_msgWindow = new SoundMessageWindow(this);
      m_file = new WaveFile();
   }

NumDevices returns the number of available input audio devices.

   public uint NumDevices()
   {
      return (uint)waveInGetNumDevs();
   }

The GetDeviceName method returns the name of the specified input audio device.

   public Wave.MMSYSERR GetDeviceName(uint deviceId,
      ref string prodName)
   {
      WAVEINCAPS caps = new WAVEINCAPS();
      Wave.MMSYSERR result = waveInGetDevCaps(deviceId, caps,
         caps.Size);
      if (result != Wave.MMSYSERR.NOERROR)
         return result;

      prodName = caps.szPname;

      return Wave.MMSYSERR.NOERROR;
   }

The following functions are encapsulations of the corresponding WaveFile methods.

   public void BlockDone()
   {
      m_file.BlockDone();
   }

   public Wave.MMSYSERR Preload(int maxRecordLength_ms, int bufferSize)
   {
      if (m_file != null)
         return m_file.Preload(0, m_msgWindow.Hwnd, maxRecordLength_ms,
            bufferSize);

      return Wave.MMSYSERR.NOERROR;
   }

   public void Stop()
   {
      if (m_file != null)
         m_file.Stop();
   }

   public Wave.MMSYSERR Start()
   {
      if (m_file != null)
         return m_file.Start();

      return Wave.MMSYSERR.NOERROR;
   }

   public bool Done()
   {
      return m_file.Done;
   }

   public Wave.MMSYSERR Save(string fileName)
   {
      if (m_file != null)
         return m_file.Save(fileName);

      return Wave.MMSYSERR.NOERROR;
   }


   public void Dispose()
   {
      m_msgWindow.Dispose();

      if (m_file != null)
         m_file.Dispose();
   }

The WaveIn class implements the following P/Invoke definitions. Detailed documentation regarding these methods is available directly in the comments of the P/Invoke Library sample code.

   [DllImport ("coredll.dll")]
   protected static extern int waveInGetNumDevs();

   [DllImport ("coredll.dll")]
   private static extern Wave.MMSYSERR waveInOpen(ref IntPtr phwi,
      uint uDeviceID, Wave.WAVEFORMATEX pwfx, IntPtr dwCallback,
      uint dwInstance, uint fdwOpen);

   [DllImport ("coredll.dll")]
   private static extern Wave.MMSYSERR waveInPrepareHeader(IntPtr hwi,
      Wave.WAVEHDR pwh, uint cbwh);

   [DllImport ("coredll.dll")]
   private static extern Wave.MMSYSERR waveInUnprepareHeader(IntPtr
      hwi, Wave.WAVEHDR pwh, uint cbwh);

   [DllImport ("coredll.dll")]
   protected static extern Wave.MMSYSERR waveInClose(IntPtr hwi);

   [DllImport ("coredll.dll")]
   protected static extern Wave.MMSYSERR waveInReset(IntPtr hwi); 

   [DllImport ("coredll.dll")]
   protected static extern Wave.MMSYSERR waveInStart(IntPtr hwi);
 
   [DllImport ("coredll.dll")]
   protected static extern Wave.MMSYSERR waveInStop(IntPtr hwi);

   [DllImport ("coredll.dll")]
   private static extern Wave.MMSYSERR waveInAddBuffer(IntPtr hwi,
      Wave.WAVEHDR pwh, uint cbwh);
   
   protected class WAVEINCAPS
   {
      const uint WAVEINCAPS_SIZE = 80;

      public static implicit operator byte[](WAVEINCAPS caps)
      {
         return caps.m_data;
      }

      private byte[] m_data = null;
      public uint Size { get { return (uint)WAVEINCAPS_SIZE; } }

      public ushort wMid
         { get { return BitConverter.ToUInt16(m_data, 0); } }
      public ushort wPid
         { get { return BitConverter.ToUInt16(m_data, 2); } }
      public uint vDriverVersion
         { get { return BitConverter.ToUInt32(m_data, 4); } }
      public uint dwFormats
         { get { return BitConverter.ToUInt32(m_data, 72); } }
      public ushort wChannels
         { get { return BitConverter.ToUInt16(m_data, 76); } }
      public ushort wReserved1
         { get { return BitConverter.ToUInt16(m_data, 78); } }

      public WAVEINCAPS()
      {
         m_data = new byte[WAVEINCAPS_SIZE];
      }

      public string szPname
      {
         get
         {
            char[] bytes = new char[32];
            for (int i = 0; i < 32; i++)
            {
               bytes[i] = (char)BitConverter.ToUInt16(m_data,
                 i * 2 + 8);
            }

            return new string(bytes);
         }
      }
   }

   [DllImport ("coredll.dll")]
   protected static extern Wave.MMSYSERR waveInGetDevCaps(uint
      uDeviceID, byte[] pwic, uint cbwic);
}

Conclusion

The code provided in the P/Invoke Library sample can easily be modified to provide a robust and efficient sound engine utilizing the Waveform Audio Interface. This API provides complete control of the audio system and buffering to the developer.

Show:
© 2014 Microsoft