Click here to Skip to main content
15,897,371 members
Articles / Programming Languages / C#

Personal Wave Recorder

Rate me:
Please Sign up or sign in to vote.
4.97/5 (94 votes)
19 Mar 2010CPOL15 min read 142.5K   11.7K   208  
DSP chains, Complex Fourier, ACM, Visualizers, EQ, Custom controls.. the works.
#region About
// Based (largely on) code by: Ianier Munoz | ianier@hotmail.com
// http://www.codeproject.com/KB/audio-video/cswavrec.aspx?fid=16677&fr=276#xx0xx
// for original licence refer to his code
// msdn article: Creating a PInvoke library in C# - http://msdn.microsoft.com/en-us/library/aa446550.aspx

// ToDo:
// format - done
// migrate wavenative (we don't need headers in c#) - done
// add record, pause, stop, resume, device count, device desc - done
// fix threading issues - done
// add properties - done
// add error handling - done
// comment - done
#endregion

#region Directives
using System;
using System.IO;
using System.Threading;
using System.Runtime.InteropServices;
#endregion

namespace WaveLib
{
    #region Public Delegates
    public delegate void BufferDoneEventHandler(IntPtr data, uint size);
    #endregion

    internal class WaveInBuffer : IDisposable
    {
        #region Constants
        public const int MM_WOM_OPEN = 0x3BB;
        public const int MM_WOM_CLOSE = 0x3BC;
        public const int MM_WOM_DONE = 0x3BD;
        public const int MM_WIM_OPEN = 0x3BE;
        public const int MM_WIM_CLOSE = 0x3BF;
        public const int MM_WIM_DATA = 0x3C0;
        #endregion

        #region API
        [DllImport("winmm.dll")]
        private static extern MMSYSERR waveInPrepareHeader(IntPtr hWaveIn, ref WAVEHDR lpWaveInHdr, int uSize);
        [DllImport("winmm.dll")]
        private static extern MMSYSERR waveInUnprepareHeader(IntPtr hWaveIn, ref WAVEHDR lpWaveInHdr, int uSize);
        [DllImport("winmm.dll")]
        private static extern MMSYSERR waveInAddBuffer(IntPtr hwi, ref WAVEHDR pwh, int cbwh);
        #endregion

        #region Fields
        private bool _bRecording;
		private IntPtr _pWaveIn;
		private byte[] _cHeaderData;
        public WaveInBuffer _cNextBuffer;
		private AutoResetEvent _cRecordEvent = new AutoResetEvent(false);
		private WAVEHDR _cHeader;
		private GCHandle _hHeaderHandle;
		private GCHandle _hHeaderDataHandle;
        #endregion

        #region Constructor
        public WaveInBuffer(IntPtr waveInHandle, uint size)
        {
            _pWaveIn = waveInHandle;
            _hHeaderHandle = GCHandle.Alloc(_cHeader, GCHandleType.Pinned);
            _cHeader.dwUser = (IntPtr)GCHandle.Alloc(this);
            _cHeaderData = new byte[size];
            _hHeaderDataHandle = GCHandle.Alloc(_cHeaderData, GCHandleType.Pinned);
            _cHeader.lpData = _hHeaderDataHandle.AddrOfPinnedObject();
            _cHeader.dwBufferLength = size;
            waveInPrepareHeader(_pWaveIn, ref _cHeader, Marshal.SizeOf(_cHeader));
        }
        ~WaveInBuffer()
        {
            Dispose();
        }
        #endregion

        #region Callback
        internal static void WaveInProc(IntPtr hdrvr, int uMsg, int dwUser, ref WAVEHDR wavhdr, int dwParam2)
		{
			if (uMsg == MM_WIM_DATA)
			{
				try
				{
					GCHandle h = (GCHandle)wavhdr.dwUser;
					WaveInBuffer buf = (WaveInBuffer)h.Target;
					buf.OnCompleted();
				}
				catch { }
			}
        }
        #endregion

        #region Properties
        public uint Size
		{
			get { return _cHeader.dwBufferLength; }
		}

		public IntPtr Data
		{
			get { return _cHeader.lpData; }
        }
        #endregion

        #region Methods
        public bool Record()
		{
			lock(this)
			{
				_cRecordEvent.Reset();
				_bRecording = waveInAddBuffer(_pWaveIn, ref _cHeader, Marshal.SizeOf(_cHeader)) == MMSYSERR.NOERROR;
				return _bRecording;
			}
		}

		public void WaitFor()
		{
			if (_bRecording)
				_bRecording = _cRecordEvent.WaitOne();
			else
				Thread.Sleep(0);
		}

		private void OnCompleted()
		{
			_cRecordEvent.Set();
			_bRecording = false;
        }
        #endregion

        #region Dispose
        public void Dispose()
        {
            if (_cHeader.lpData != IntPtr.Zero)
            {
                waveInUnprepareHeader(_pWaveIn, ref _cHeader, Marshal.SizeOf(_cHeader));
                _hHeaderHandle.Free();
                _cHeader.lpData = IntPtr.Zero;
            }
            _cRecordEvent.Close();
            if (_hHeaderDataHandle.IsAllocated)
                _hHeaderDataHandle.Free();
            GC.SuppressFinalize(this);
        }
        #endregion
	}

	public class WaveInRecorder : IDisposable
    {
        #region Constants
        private const int CALLBACK_FUNCTION = 0x00030000;
        private const int WAVE_FORMAT_PCM = 1;
        #endregion

        #region Structs
        [StructLayout(LayoutKind.Sequential, Pack = 4)]
        private struct WAVEINCAPS
        {
            public short wMid;
            public short wPid;
            public int vDriverVersion;
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
            public char[] szPname;
            public uint dwFormats;
            public short wChannels;
            public short wReserved;
        }
        #endregion

        #region API
        [DllImport("winmm.dll")]
        private static extern MMSYSERR waveInOpen(out IntPtr phwi, int uDeviceID, ref WAVEFORMATEX lpFormat, WaveDelegate dwCallback, int dwInstance, int dwFlags);
        [DllImport("winmm.dll")]
        private static extern MMSYSERR waveInStart(IntPtr hwi);
        [DllImport("winmm.dll")]
        private static extern MMSYSERR waveInStop(IntPtr hwi);
        [DllImport("winmm.dll")]
        private static extern MMSYSERR waveInReset(IntPtr hwi);
        [DllImport("winmm.dll")]
        private static extern MMSYSERR waveInClose(IntPtr hwi);
        [DllImport("winmm.dll")]
        private static extern int waveInGetNumDevs();
        [DllImport("winmm.dll")]
        private static extern MMSYSERR waveInGetDevCaps(uint uDeviceID, out WAVEINCAPS pwoc, uint cbwoc);
        #endregion

        #region Fields/Delegates
        private bool _bFinished;
        private IntPtr _pWaveIn;
        private WAVEFORMATEX _tWaveFormat;
		private WaveInBuffer _cBuffers;
		private WaveInBuffer _cCurrentBuffer;
		private Thread _cThread;
		private BufferDoneEventHandler _eDoneProc;
        public delegate void WaveDelegate(IntPtr hdrvr, int uMsg, int dwUser, ref WAVEHDR wavhdr, int dwParam2);
		private WaveDelegate _cBufferProc = new WaveDelegate(WaveInBuffer.WaveInProc);
        #endregion

        #region Properties
        /// <summary>Play status</summary>
        public bool Recording { get; private set; }
        /// <summary>Channels in current track</summary>
        public ushort Channels { get; private set; }
        /// <summary>BPS in current track</summary>
        public ushort BitsPerSample { get; private set; }
        /// <summary>SPS of current track</summary>
        public uint SamplesPerSecond { get; private set; }
        /// <summary>Average BPS of current track</summary>
        public uint AvgBytesPerSecond { get; private set; }
        /// <summary>Recording format id</summary>
        public int Format { get; private set; }
        /// <summary>Internal buffer size</summary>
        public uint BufferSize { get; set; }
        /// <summary>Number of allocated buffers</summary>
        public uint BufferCount { get; set; }
        /// <summary>Recording device id</summary>
        public int Device { get; set; }
        #endregion

        #region Constructor
        public WaveInRecorder()
        {
        }
        /// <summary>Initialze class and set defaults</summary>
        /// <param name="device">device id</param>
        /// <param name="format">WAVEFORMATEX format description</param>
        /// <param name="doneProc">data callback</param>
        public WaveInRecorder(int device, ref WAVEFORMATEX format, BufferDoneEventHandler doneProc)
		{
			_eDoneProc = doneProc;
            format = WaveFormat(format.nSamplesPerSec, format.wBitsPerSample, format.nChannels);
            _tWaveFormat = format;
            this.Device = device;
            this.BufferSize = 4096;
            this.BufferCount = 4;
            this.Channels = _tWaveFormat.nChannels;
            this.BitsPerSample = _tWaveFormat.wBitsPerSample;
            this.SamplesPerSecond = _tWaveFormat.nSamplesPerSec;
            this.AvgBytesPerSecond = _tWaveFormat.nAvgBytesPerSec;
            this.Format = _tWaveFormat.wFormatTag;
		}
		~WaveInRecorder()
		{
			Dispose();
		}
        #endregion

        #region ThreadProc
		private void ThreadProc()
		{
			while (!_bFinished)
			{
				Advance();
				if (_eDoneProc != null && !_bFinished)
					_eDoneProc(_cCurrentBuffer.Data, _cCurrentBuffer.Size);
				_cCurrentBuffer.Record();
			}
		}
        #endregion

        #region Public Methods
        /// <summary>Begin recording</summary>
        /// <returns>MMSYSERR</returns>
        public MMSYSERR Record()
        {
            MMSYSERR mmr = waveInOpen(out _pWaveIn, Device, ref _tWaveFormat, _cBufferProc, 0, CALLBACK_FUNCTION);
            if (mmr == MMSYSERR.NOERROR)
            {
                AllocateBuffers(BufferSize, BufferCount);
                for (uint i = 0; i < BufferCount; i++)
                {
                    SelectNextBuffer();
                    _cCurrentBuffer.Record();
                }
                waveInStart(_pWaveIn);
                _cThread = new Thread(new ThreadStart(ThreadProc));
                _cThread.Start();
                Recording = true;
            }
            return mmr;
        }

        /// <summary>Stop recording and reset</summary>
        /// <returns>MMSYSERR</returns>
        public MMSYSERR Stop()
        {
            MMSYSERR mmr = MMSYSERR.ERROR;
            try
            {
                _bFinished = true;
                if (_pWaveIn != IntPtr.Zero)
                    waveInReset(_pWaveIn);
                if (_cThread != null)
                    _cThread.Abort();
                _eDoneProc = null;
                FreeBuffers();
                if (_pWaveIn != IntPtr.Zero)
                    mmr = waveInClose(_pWaveIn);
                else
                    mmr = MMSYSERR.NOERROR;
            }
            finally
            {
                _cThread = null;
                _pWaveIn = IntPtr.Zero;
                Recording = false;
            }
            return mmr;
        }

        /// <summary>Pause recording</summary>
        /// <returns>MMSYSERR</returns>
        public MMSYSERR Pause()
        {
            _cThread.Suspend();
            Recording = false;
            return waveInStop(_pWaveIn);
        }

        /// <summary>Resujme recording</summary>
        /// <returns>MMSYSERR</returns>
        public MMSYSERR Resume()
        {
            _cThread.Resume();
            Recording = true;
            return waveInStart(_pWaveIn);
        }

        /// <summary>Get the number of playback devices in the system</summary>
        /// <returns>number of devices</returns>
        public int GetDeviceCount()
		{
			return waveInGetNumDevs();
		}

        /// <summary>Get the input device name from device id</summary>
        /// <param name="deviceId">device id</param>
        /// <param name="prodName">returns device name</param>
        /// <returns>MMSYSERR</returns>
        public MMSYSERR GetInputDeviceName(uint deviceId, ref string prodName)
        {
            WAVEINCAPS caps = new WAVEINCAPS();
            MMSYSERR result = waveInGetDevCaps(deviceId, out caps, (uint)Marshal.SizeOf(caps));
            if (result != MMSYSERR.NOERROR)
                return result;
            prodName = new string(caps.szPname);
            if (prodName.Contains("(") && !(prodName.Contains(")")))
            {
                prodName = prodName.Substring(0, prodName.IndexOf("("));
            }
            else if (prodName.Contains(")"))
            {
                if (prodName.IndexOf(")") > 8)
                {
                    prodName = prodName.Substring(0, prodName.IndexOf(")") + 1);
                }
            }
            return MMSYSERR.NOERROR;
        }

        /// <summary>Calculate a WAVEFORMATEX structure</summary>
        /// <param name="rate">bit rate</param>
        /// <param name="bits">bits per sample</param>
        /// <param name="channels">number of channels</param>
        /// <returns></returns>
        public WAVEFORMATEX WaveFormat(uint rate, ushort bits, ushort channels)
        {
            WAVEFORMATEX wfx = new WAVEFORMATEX();
            wfx.wFormatTag = WAVE_FORMAT_PCM;
            wfx.nChannels = channels;
            wfx.nSamplesPerSec = rate;
            wfx.wBitsPerSample = bits;
            wfx.nBlockAlign = (ushort)(channels * (bits / 8));
            wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign;
            wfx.cbSize = 0;
            return wfx;
        }

        /// <summary>Copies header to a stream</summary>
        /// <param name="waveData">Wav data stream</param>
        /// <param name="format">WAVEFORMATEX wav format</param>
        /// <returns>Stream</returns>
        public Stream CreateStream(Stream waveData, WAVEFORMATEX format)
        {
            MemoryStream stream = new MemoryStream();
            BinaryWriter writer = new BinaryWriter(stream);

            writer.Write(System.Text.Encoding.ASCII.GetBytes("RIFF".ToCharArray()));
            writer.Write((Int32)(waveData.Length + 36)); //File length minus first 8 bytes of RIFF description
            writer.Write(System.Text.Encoding.ASCII.GetBytes("WAVEfmt ".ToCharArray()));
            writer.Write((Int32)16); //length of following chunk: 16
            writer.Write((Int16)format.wFormatTag);
            writer.Write((Int16)format.nChannels);
            writer.Write((Int32)format.nSamplesPerSec);
            writer.Write((Int32)format.nAvgBytesPerSec);
            writer.Write((Int16)format.nBlockAlign);
            writer.Write((Int16)format.wBitsPerSample);
            writer.Write(System.Text.Encoding.ASCII.GetBytes("data".ToCharArray()));
            writer.Write((Int32)waveData.Length);

            waveData.Seek(0, SeekOrigin.Begin);
            byte[] b = new byte[waveData.Length];
            waveData.Read(b, 0, (int)waveData.Length);
            writer.Write(b);
            writer.Seek(0, SeekOrigin.Begin);
            return stream;
        }
        #endregion

        #region Private Methods
        /// <summary>Allocate internal buffers</summary>
        /// <param name="bufferSize">size of buffer</param>
        /// <param name="bufferCount">number of buffers to allocate</param>
		private void AllocateBuffers(uint bufferSize, uint bufferCount)
		{
			FreeBuffers();
			if (bufferCount > 0)
			{
				_cBuffers = new WaveInBuffer(_pWaveIn, bufferSize);
				WaveInBuffer Prev = _cBuffers;
				try
				{
					for (int i = 1; i < bufferCount; i++)
					{
						WaveInBuffer Buf = new WaveInBuffer(_pWaveIn, bufferSize);
						Prev._cNextBuffer = Buf;
						Prev = Buf;
					}
				}
				finally
				{
					Prev._cNextBuffer = _cBuffers;
				}
			}
		}

        /// <summary>Free the internal buffers</summary>
		private void FreeBuffers()
		{
			_cCurrentBuffer = null;
			if (_cBuffers != null)
			{
				WaveInBuffer first = _cBuffers;
				_cBuffers = null;

				WaveInBuffer current = first;
				do
				{
					WaveInBuffer Next = current._cNextBuffer;
					current.Dispose();
					current = Next;
				} while(current != first);
			}
		}

        /// <summary>Advance to the next buffer</summary>
		private void Advance()
		{
			SelectNextBuffer();
			_cCurrentBuffer.WaitFor();
		}

        /// <summary>Select next internal buffer</summary>
		private void SelectNextBuffer()
		{
			_cCurrentBuffer = _cCurrentBuffer == null ? _cBuffers : _cCurrentBuffer._cNextBuffer;
		}

        /// <summary>Wait for buffers</summary>
		private void WaitForAllBuffers()
		{
			WaveInBuffer buf = _cBuffers;
			while (buf._cNextBuffer != _cBuffers)
			{
				buf.WaitFor();
				buf = buf._cNextBuffer;
			}
		}
        #endregion

        #region Dispose
        /// <summary>Dispose and release resources</summary>
        public void Dispose()
		{
            Stop();
			GC.SuppressFinalize(this);
		}
        #endregion
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Network Administrator vtdev.com
Canada Canada
Network and programming specialist. Started in C, and have learned about 14 languages since then. Cisco programmer, and lately writing a lot of C# and WPF code, (learning Java too). If I can dream it up, I can probably put it to code. My software company, (VTDev), is on the verge of releasing a couple of very cool things.. keep you posted.

Comments and Discussions