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.
The Recorder with VU and Real Time Graphing
So, what is involved in recording live audio with the WinMM API?
Short Answer
- Open the desired WaveInDevice using waveInOpen.
- Create a number of WAVHDR instances.
- 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.
- 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.
- 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.
-
- if (!Directory .Exists (Application.StartupPath + @"\Safe"))
- Directory .CreateDirectory (Application.StartupPath + @"\Safe");
- if (File.Exists(Application.StartupPath + @"\Safe\TheData.bin"))
- File.Delete (Application.StartupPath + @"\Safe\TheData.bin");
- fs = new FileStream(Application.StartupPath + @"\Safe\TheData.bin", FileMode.OpenOrCreate, FileAccess.ReadWrite);
- bw = new BinaryWriter(fs);
-
- Int32 riffsize = 0, datasize = 0;
-
-
-
-
-
-
-
-
-
-
-
- bw.Write(RIFF);
- bw.Write(riffsize);
- bw.Write(WAVE);
- bw.Write(FMT);
- bw.Write(wavFmt.cbSize - 4);
- bw.Write(wavFmt.wFormatTag);
- bw.Write(wavFmt.nChannels);
- bw.Write(wavFmt.nSamplesPerSec);
- bw.Write(wavFmt.nAvgBytesPerSec);
- bw.Write(wavFmt.nBlockAlign);
- bw.Write(wavFmt.wBitsPerSample);
- bw.Write(DATA);
- bw.Write(datasize);
-
- IntPtr dwCallback = IntPtr.Zero;
- BufferInProc = new WaveDelegate(HandleWaveIn);
- dwCallback = Marshal.GetFunctionPointerForDelegate(BufferInProc);
-
-
-
-
-
-
-
-
-
- rv0 = waveInOpen(ref hWaveIn, InputDeviceIndex, ref wavFmt, dwCallback, 0, (uint)WaveInOpenFlags.CALLBACK_FUNCTION);
- if (0 != rv0)
- 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.
- header = new WAVEHDR[NUMBER_OF_HEADERS ];
- for (int i = 0; i < NUMBER_OF_HEADERS; i++)
- {
- HeaderDataHandle = GCHandle.Alloc(header, GCHandleType.Pinned);
- HeaderData = new byte[size];
- HeaderDataHandle = GCHandle.Alloc(HeaderData, GCHandleType.Pinned);
-
- header[i].lpData = HeaderDataHandle.AddrOfPinnedObject();
- header[i].dwBufferLength = size;
- header[i].dwUser =new IntPtr(i);
-
- rv1 = waveInPrepareHeader(hWaveIn, ref header[i], (uint)Marshal.SizeOf(header[i]));
- if (0 != rv1)
- {
- rv = mciGetErrorString(rv1, errmsg, (uint)errmsg.Capacity);
- return false;
- }
- rv1 = waveInAddBuffer(hWaveIn, ref header[i], size);
- if (0 != rv1)
- {
- rv = mciGetErrorString(rv1, errmsg, (uint)errmsg.Capacity);
- return false;
- }
- }
- rv1 = waveInStart(hWaveIn);
- if (0 != rv1)
- {
- rv = mciGetErrorString(rv1, errmsg, (uint)errmsg.Capacity);
- return false;
- }
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.
-
-
-
-
-
-
-
-
- private void HandleWaveIn(IntPtr hdrvr, int uMsg, int dwUser, ref WAVEHDR waveheader, int dwParam2)
- {
- uint rv1;
-
- lock (lockobject)
- {
- if (uMsg == MM_WIM_DATA )
- {
- try
- {
- uint i = (uint)waveheader.dwUser.ToInt32();
-
-
- byte[] _imageTemp = new byte[waveheader.dwBytesRecorded];
- Marshal.Copy(waveheader.lpData, _imageTemp, 0, (int)waveheader.dwBytesRecorded);
- if (!paused)
- bw.Write(_imageTemp);
- VU(_imageTemp);
- if (!stopstruct.Stopping)
- {
- rv1=waveInAddBuffer(hWaveIn, ref waveheader, size);
- if (rv1 != 0)
- {
- mciGetErrorString(rv1, errmsg, (uint)errmsg.Capacity);
- }
- }
- else
- {
- stopstruct.NumberofStoppedBuffers++;
- rv1 = waveInUnprepareHeader(hWaveIn, ref waveheader, size);
- if (rv1 != 0)
- {
- mciGetErrorString(rv1, errmsg, (uint)errmsg.Capacity);
- }
-
- if (stopstruct.NumberofStoppedBuffers == NUMBER_OF_HEADERS)
- {
- stopstruct.Stopped = true;
- }
- }
-
- }
- catch
- {
- }
- }
- }
- }
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.
- private void timer1_tick(object sender,EventArgs e)
- {
- int i,j;
- short leftlevel,rightlevel;
- LevelEventArgs lea=new LevelEventArgs ();
- if (recording)
- {
- if (LeftMinMax.Count > 1)
- {
- lea.numberofchannels = (byte)wavFmt.nChannels;
- i = LeftMinMax.Count - 1;
- j = RightMinMax.Count - 1;
- MinMax lmm, rmm;
- lmm = LeftMinMax[i];
- if (-1 * lmm.Min > lmm.Max)
- leftlevel = (short)(-1 * lmm.Min);
- else
- leftlevel = lmm.Max;
- lea.leftlevel = leftlevel;
- lea.leftminmax = lmm;
-
- if(wavFmt .nChannels >1)
- {
- rmm = RightMinMax[j];
- if (-1 * rmm.Min > rmm.Max)
- rightlevel = (short)(-1 * rmm.Min);
- else
- rightlevel = rmm.Max;
- lea.rightlevel = rightlevel;
- lea.rightminmax = rmm;
- }
- RaiseLevelEvent(lea);
- }
- if (stopstruct.Stopped)
- {
- timer1.Enabled = false;
- Stop();
- RaiseRecordingStoppedEvent();
- }
-
- }
- }
-
-
- private void Stop()
- {
- uint rv;
- bool rv1;
- if (recording)
- {
- rv = waveInStop(hWaveIn);
- if (0 != rv)
- {
- rv1 = mciGetErrorString(rv, errmsg, (uint)errmsg.Capacity);
- Debug.Print("waveInStop Err " + errmsg);
- }
- else
- {
- rv = waveInClose(hWaveIn);
- if (0 != rv)
- {
- rv1 = mciGetErrorString(rv, errmsg, (uint)errmsg.Capacity);
- Debug.Print("waveInClose Err " + errmsg);
- }
- bw.Close();
- }
- }
- }
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.