An Audio Recorder Using Winmm.dll

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

Video Preview

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.   
  9. Int32 riffsize = 0, datasize = 0;  
  10. //create a dummy wave header to be filled in when done recording  
  11. /* 
  12.     Standard CD Quality Audio 
  13.     wavFmt.wFormatTag = 1;//pcm 
  14.     wavFmt.nChannels = 2;//stereo 
  15.     wavFmt.nSamplesPerSec = 44100;//44100 samples per sec 
  16.     wavFmt.nAvgBytesPerSec = 176400;//2 channels*2 bytes * 44100 samples  
  17.     wavFmt.wBitsPerSample = 16;//16 bits per sample 
  18.     wavFmt.nBlockAlign = (ushort)(wavFmt.nChannels * wavFmt.wBitsPerSample / 8); 
  19.     wavFmt.cbSize = (ushort)Marshal.SizeOf(wavFmt); 
  20. */  
  21. bw.Write(RIFF);  
  22. bw.Write(riffsize);  
  23. bw.Write(WAVE);  
  24. bw.Write(FMT);  
  25. bw.Write(wavFmt.cbSize - 4);  
  26. bw.Write(wavFmt.wFormatTag);  
  27. bw.Write(wavFmt.nChannels);  
  28. bw.Write(wavFmt.nSamplesPerSec);  
  29. bw.Write(wavFmt.nAvgBytesPerSec);  
  30. bw.Write(wavFmt.nBlockAlign);  
  31. bw.Write(wavFmt.wBitsPerSample);  
  32. bw.Write(DATA);  
  33. bw.Write(datasize);  
  34.   
  35. IntPtr dwCallback = IntPtr.Zero;// a pointer that will eventually point to our WaveDelegate callback routine(HandleWaveIn)  
  36. BufferInProc = new WaveDelegate(HandleWaveIn);//the callback function must be cast as a WaveDelegate  
  37. dwCallback = Marshal.GetFunctionPointerForDelegate(BufferInProc);// point our callback pointer to our WaveDelegte function  
  38.   
  39. //open the recording device ...  
  40.   
  41. //hWaveIn will be the handle to the device for all future calls  
  42. //InputDeviceIndex is the index of the device as returned to us from a call to waveInGetDevCaps  
  43. //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  
  44. //dwCallback is the pointer to our WaveDelegate function (where the recorded data is returned to us)  
  45. // 0 dwCallbackInstance ...  User - instance data passed to the callback mechanism. This parameter is not used with the window callback mechanism.  
  46. // a flag to let the DLL know that we want to use a callback function  
  47. rv0 = waveInOpen(ref hWaveIn, InputDeviceIndex, ref wavFmt, dwCallback, 0, (uint)WaveInOpenFlags.CALLBACK_FUNCTION);  
  48. if (0 != rv0)  
  49.     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.   
  8.     header[i].lpData = HeaderDataHandle.AddrOfPinnedObject();// a pointer to where the data will be stores  
  9.     header[i].dwBufferLength = size;// let the DLL know how big the buffer is  
  10.     header[i].dwUser =new IntPtr(i);// not really important to us here ... we only use it for debug purposes  
  11.   
  12.     rv1 = waveInPrepareHeader(hWaveIn, ref header[i], (uint)Marshal.SizeOf(header[i]));//tell the DLL to prepare the header  
  13.     if (0 != rv1)  
  14.     {  
  15.         rv = mciGetErrorString(rv1, errmsg, (uint)errmsg.Capacity);  
  16.         return false;  
  17.     }  
  18.     rv1 = waveInAddBuffer(hWaveIn, ref header[i], size);// tell the tell that it is ready to use  
  19.     if (0 != rv1)  
  20.     {  
  21.         rv = mciGetErrorString(rv1, errmsg, (uint)errmsg.Capacity);  
  22.         return false;  
  23.     }  
  24. }  
  25. rv1 = waveInStart(hWaveIn);// start recording  
  26. if (0 != rv1)  
  27. {  
  28.     rv = mciGetErrorString(rv1, errmsg, (uint)errmsg.Capacity);  
  29.     return false;  
  30. }  

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

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.   
  22.             if(wavFmt .nChannels >1)  
  23.             {  
  24.                 rmm = RightMinMax[j];  
  25.                 if (-1 * rmm.Min > rmm.Max)  
  26.                     rightlevel = (short)(-1 * rmm.Min);  
  27.                 else  
  28.                     rightlevel = rmm.Max;  
  29.                 lea.rightlevel = rightlevel;  
  30.                 lea.rightminmax = rmm;  
  31.             }  
  32.             RaiseLevelEvent(lea); // call the UI to Plot and do VU  
  33.         }  
  34.         if (stopstruct.Stopped)  
  35.         {  
  36.             timer1.Enabled = false;  
  37.             Stop();// close the waveindevice  
  38.             RaiseRecordingStoppedEvent();  
  39.         }  
  40.   
  41.     }  
  42. }  
  43.   
  44. // when the waveinhandler has no more headers to stop adding, the timer will call this function to close out the device  
  45. private   void Stop()  
  46. {  
  47.     uint rv;  
  48.     bool rv1;  
  49.     if (recording)  
  50.     {  
  51.         rv = waveInStop(hWaveIn);// Infor the DLL that we are not recording anymore  
  52.         if (0 != rv)  
  53.         {  
  54.             rv1 = mciGetErrorString(rv, errmsg, (uint)errmsg.Capacity);  
  55.             Debug.Print("waveInStop Err " + errmsg);  
  56.         }  
  57.         else  
  58.         {  
  59.             rv = waveInClose(hWaveIn);// close the recording device  
  60.             if (0 != rv)  
  61.             {  
  62.                 rv1 = mciGetErrorString(rv, errmsg, (uint)errmsg.Capacity);  
  63.                 Debug.Print("waveInClose Err " + errmsg);  
  64.             }  
  65.             bw.Close();  
  66.         }  
  67.     }  
  68. }  

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.