In the previous parts of this series, we learned some functionality of WinMM API and Windows Mixer. You can read those articles from the following links.
Now, for the third part of this tutorial, we will learn how to create an audio recorder in C#. But before that, I recommend you read the second part of the series because this third one is in continuation of the previous one.

Audio Recorder

The Recorder with VU and Real Time Graphing

So, what is involved in recording live audio with the WinMM API?

Short Answer

  1. Open the desired WaveInDevice using waveInOpen.

  2. Create a number of WAVHDR instances.

  3. Pass these instances to the DLL by calling waveInPrepareHeader and WaveInAddBuffer. This tells the DLL that there are buffers available, and gives the DLL permission to use them. Finally, call waveInStart to actually start recording.

  4. When the DLL has filled one of those buffers, it will call a designated callback function that reads that filled buffer and then prepares and sends that buffer back to the DLL. When you are done recording, you must inform the DLL that it can release those buffers back to the system. When all buffers are 'Un-Prepared', you must finally call waveInStop to close the device but it can not be called from within the callback routine.

  5. I actually launched a new thread from the callback routine which calls waveInStop, waveInReset, waveInUnprepareHeader (for each WAVEINHDR) and finally waveInClose.

Long Answer

When the form loads, it calls MixerGetDevCaps, waveInGetDevCaps, and waveOutGetDevCaps to get the available input and output devices. For the recorder, we are interested in the input device retrieved with waveInGetDevCaps. The order in which they are presented to us gives us the device index for each when calling waveInOpen. We must first create a binary file to store the data in when the DLL does record it. We create a binary file called TheData.bin. To this file, we add a dummy wave header that has all known values except the size of the final recording. We must also start a timer that will be used to apprise the UI of our progress.

  1. // create a binary file to hold the recorded data
  2. if (!Directory .Exists (Application.StartupPath + @"\Safe"))
  3. Directory .CreateDirectory (Application.StartupPath + @"\Safe");
  4. if (File.Exists(Application.StartupPath + @"\Safe\TheData.bin"))
  5. File.Delete (Application.StartupPath + @"\Safe\TheData.bin");
  6. fs = new FileStream(Application.StartupPath + @"\Safe\TheData.bin", FileMode.OpenOrCreate, FileAccess.ReadWrite);
  7. bw = new BinaryWriter(fs);
  8. Int32 riffsize = 0, datasize = 0;
  9. //create a dummy wave header to be filled in when done recording
  10. /*
  11. Standard CD Quality Audio
  12. wavFmt.wFormatTag = 1;//pcm
  13. wavFmt.nChannels = 2;//stereo
  14. wavFmt.nSamplesPerSec = 44100;//44100 samples per sec
  15. wavFmt.nAvgBytesPerSec = 176400;//2 channels*2 bytes * 44100 samples
  16. wavFmt.wBitsPerSample = 16;//16 bits per sample
  17. wavFmt.nBlockAlign = (ushort)(wavFmt.nChannels * wavFmt.wBitsPerSample / 8);
  18. wavFmt.cbSize = (ushort)Marshal.SizeOf(wavFmt);
  19. */
  20. bw.Write(RIFF);
  21. bw.Write(riffsize);
  22. bw.Write(WAVE);
  23. bw.Write(FMT);
  24. bw.Write(wavFmt.cbSize - 4);
  25. bw.Write(wavFmt.wFormatTag);
  26. bw.Write(wavFmt.nChannels);
  27. bw.Write(wavFmt.nSamplesPerSec);
  28. bw.Write(wavFmt.nAvgBytesPerSec);
  29. bw.Write(wavFmt.nBlockAlign);
  30. bw.Write(wavFmt.wBitsPerSample);
  31. bw.Write(DATA);
  32. bw.Write(datasize);
  33. IntPtr dwCallback = IntPtr.Zero;// a pointer that will eventually point to our WaveDelegate callback routine(HandleWaveIn)
  34. BufferInProc = new WaveDelegate(HandleWaveIn);//the callback function must be cast as a WaveDelegate
  35. dwCallback = Marshal.GetFunctionPointerForDelegate(BufferInProc);// point our callback pointer to our WaveDelegte function
  36. //open the recording device ...
  37. //hWaveIn will be the handle to the device for all future calls
  38. //InputDeviceIndex is the index of the device as returned to us from a call to waveInGetDevCaps
  39. //wavfmt is the format the DLL will be using to record the audio... it was set in clsPlayer() to be the same standard as used in CD quality audio
  40. //dwCallback is the pointer to our WaveDelegate function (where the recorded data is returned to us)
  41. // 0 dwCallbackInstance ... User - instance data passed to the callback mechanism. This parameter is not used with the window callback mechanism.
  42. // a flag to let the DLL know that we want to use a callback function
  43. rv0 = waveInOpen(ref hWaveIn, InputDeviceIndex, ref wavFmt, dwCallback, 0, (uint)WaveInOpenFlags.CALLBACK_FUNCTION);
  44. if (0 != rv0)
  45. rv = mciGetErrorString(rv0, errmsg, (uint)errmsg.Capacity);

Next, we have to create several WAVEHDR structures that the DLL can use to put the recorded data and send it back to us. We need at least two because while one is being examined by us, the other is being filled in. Once we define a structure, we must tell the DLL to prepare it with waveInPrepareHeader and then we call waveInAddBuffer to add it to the DLL's queue. When all structures have been added, call to actually start recording.

  1. header = new WAVEHDR[NUMBER_OF_HEADERS ];// WAVEHDR structures * 4(NUMBER_OF_HEADERS)
  2. for (int i = 0; i < NUMBER_OF_HEADERS; i++)
  3. {
  4. HeaderDataHandle = GCHandle.Alloc(header, GCHandleType.Pinned);
  5. HeaderData = new byte[size]; //.1 seconds worth of bytes
  6. HeaderDataHandle = GCHandle.Alloc(HeaderData, GCHandleType.Pinned);
  7. header[i].lpData = HeaderDataHandle.AddrOfPinnedObject();// a pointer to where the data will be stores
  8. header[i].dwBufferLength = size;// let the DLL know how big the buffer is
  9. header[i].dwUser =new IntPtr(i);// not really important to us here ... we only use it for debug purposes
  10. rv1 = waveInPrepareHeader(hWaveIn, ref header[i], (uint)Marshal.SizeOf(header[i]));//tell the DLL to prepare the header
  11. if (0 != rv1)
  12. {
  13. rv = mciGetErrorString(rv1, errmsg, (uint)errmsg.Capacity);
  14. return false;
  15. }
  16. rv1 = waveInAddBuffer(hWaveIn, ref header[i], size);// tell the tell that it is ready to use
  17. if (0 != rv1)
  18. {
  19. rv = mciGetErrorString(rv1, errmsg, (uint)errmsg.Capacity);
  20. return false;
  21. }
  22. }
  23. rv1 = waveInStart(hWaveIn);// start recording
  24. if (0 != rv1)
  25. {
  26. rv = mciGetErrorString(rv1, errmsg, (uint)errmsg.Capacity);
  27. return false;
  28. }

The callback function is what gets called when the DLL is finished filling one of the WAVEHDR structures. The callback function must create a managed byte array to hold the recorded data and copy the data from the pointer to the byte array. If we are not paused (monitoring only), the function must write the retrieved data to the aforementioned binary file (TheData.bin). Here, we also must find the min and max short values that are present in the data returned (in this example, we are recording two channels with sixteen-bit samples for each channel. We must, therefore, turn the raw byte array returned to us into two separate Int16 arrays and examine them for the min and max value) to be used elsewhere for VU and Plotting functions done through the separate timer function. At this point, we are ready to return the structure to the DLL by calling waveInAddBuffer.

If we are ready to stop recording, we do not call waveInAddBuffer. Instead, we call waveInUnprepareHeader for each structure that we have created. When all of the structures have been un-prepared, we set a flag that the timer will examine independantly.

  1. /// <summary>
  2. /// Our WaveDelegate function
  3. /// </summary>
  4. /// <param name="hdrvr"></param>
  5. /// <param name="uMsg"></param>
  6. /// <param name="dwUser"></param>
  7. /// <param name="waveheader">the place where the recorded data is stored</param>
  8. /// <param name="dwParam2"></param>
  9. private void HandleWaveIn(IntPtr hdrvr, int uMsg, int dwUser, ref WAVEHDR waveheader, int dwParam2)
  10. {
  11. uint rv1;
  12. lock (lockobject)// critical section
  13. {
  14. if (uMsg == MM_WIM_DATA )//&& recording)
  15. {
  16. try
  17. {
  18. uint i = (uint)waveheader.dwUser.ToInt32();// for debug purposes only
  19. // Debug.Print("User "+i.ToString());// try to not do this because it takes a lot of time
  20. byte[] _imageTemp = new byte[waveheader.dwBytesRecorded];// create an array that is big enough to hold the data
  21. Marshal.Copy(waveheader.lpData, _imageTemp, 0, (int)waveheader.dwBytesRecorded);// copy that data
  22. if (!paused)// if we are not paused
  23. bw.Write(_imageTemp);//write the data to a file
  24. VU(_imageTemp);// find the min and max for this sample so we can do VU and Plotting (from a timer function ... not here)
  25. if (!stopstruct.Stopping)// not stopping so add the header back to the queue
  26. {
  27. rv1=waveInAddBuffer(hWaveIn, ref waveheader, size);
  28. if (rv1 != 0)// if not success then get the associated error message
  29. {
  30. mciGetErrorString(rv1, errmsg, (uint)errmsg.Capacity);
  31. }
  32. }
  33. else// stopping
  34. {
  35. stopstruct.NumberofStoppedBuffers++;// keep track of the buffers that are finished
  36. rv1 = waveInUnprepareHeader(hWaveIn, ref waveheader, size);// un-prepare the headers as they come back
  37. if (rv1 != 0)// if not success then get the associated error message
  38. {
  39. mciGetErrorString(rv1, errmsg, (uint)errmsg.Capacity);
  40. }
  41. if (stopstruct.NumberofStoppedBuffers == NUMBER_OF_HEADERS)// when they are all done set a flag that we are done
  42. {
  43. stopstruct.Stopped = true;
  44. }
  45. }
  46. }
  47. catch
  48. {
  49. }
  50. }
  51. }
  52. }

The timer function is where we inform the UI of levels or that the recorder has actually finished. It is responsible for closing the waveindevice. Finally, the wave file must be created from binary file and the UI can the load it to play back and/or save elsewhere.

  1. private void timer1_tick(object sender,EventArgs e)
  2. {
  3. int i,j;
  4. short leftlevel,rightlevel;
  5. LevelEventArgs lea=new LevelEventArgs ();
  6. if (recording)
  7. {
  8. if (LeftMinMax.Count > 1)
  9. {
  10. lea.numberofchannels = (byte)wavFmt.nChannels;//arguments for VU and Plotting culled from the min amx info that was obtained from the callback
  11. i = LeftMinMax.Count - 1;
  12. j = RightMinMax.Count - 1;
  13. MinMax lmm, rmm;
  14. lmm = LeftMinMax[i];
  15. if (-1 * lmm.Min > lmm.Max)
  16. leftlevel = (short)(-1 * lmm.Min);
  17. else
  18. leftlevel = lmm.Max;
  19. lea.leftlevel = leftlevel;
  20. lea.leftminmax = lmm;
  21. if(wavFmt .nChannels >1)
  22. {
  23. rmm = RightMinMax[j];
  24. if (-1 * rmm.Min > rmm.Max)
  25. rightlevel = (short)(-1 * rmm.Min);
  26. else
  27. rightlevel = rmm.Max;
  28. lea.rightlevel = rightlevel;
  29. lea.rightminmax = rmm;
  30. }
  31. RaiseLevelEvent(lea); // call the UI to Plot and do VU
  32. }
  33. if (stopstruct.Stopped)
  34. {
  35. timer1.Enabled = false;
  36. Stop();// close the waveindevice
  37. RaiseRecordingStoppedEvent();
  38. }
  39. }
  40. }
  41. // when the waveinhandler has no more headers to stop adding, the timer will call this function to close out the device
  42. private void Stop()
  43. {
  44. uint rv;
  45. bool rv1;
  46. if (recording)
  47. {
  48. rv = waveInStop(hWaveIn);// Infor the DLL that we are not recording anymore
  49. if (0 != rv)
  50. {
  51. rv1 = mciGetErrorString(rv, errmsg, (uint)errmsg.Capacity);
  52. Debug.Print("waveInStop Err " + errmsg);
  53. }
  54. else
  55. {
  56. rv = waveInClose(hWaveIn);// close the recording device
  57. if (0 != rv)
  58. {
  59. rv1 = mciGetErrorString(rv, errmsg, (uint)errmsg.Capacity);
  60. Debug.Print("waveInClose Err " + errmsg);
  61. }
  62. bw.Close();
  63. }
  64. }
  65. }

I won't go into the UI functions because I do believe that if you have got through this narative so far, you are more than capable of creating a better UI than I have.