Click here to Skip to main content
15,867,308 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 139.7K   11.7K   208   70
DSP chains, Complex Fourier, ACM, Visualizers, EQ, Custom controls.. the works.

main.png

How it always starts..

I have a number of large projects on the go, at various stages of completion, and one of them required a Wave class. So.. I did the research, and wrote the classes (originally in DirectSound, which I would recommend for a serious player app, but most people would not have liked the 586 MB dev kit download.. hence this API version). OK, that done, it got me thinking, there's no Wave recorder in Win7, so.. why not make one? For the last two months, (in my spare time ;o), I wrote the UCs, and gradually crafted the project we have here. Now, I am a novice at audio processing, and not about to write some lengthy tutorial rich with theory, but I think there are some good examples of how this can be implemented in C# in this project.

Overview

WAVE files are an audio file format created by Microsoft and IBM, first introduced in 1991 for the Windows 3.1 Operating System. It uses the RIFF (Resource Interchange File Format) bitstream format to store an audio file in chunks, consisting of the Wav file header information and the data subchunk. Wav data is typically encoded using the uncompressed LPCM (Linear Pulse Code Modulation), a digital interpretation of an analog signal, where the magnitude of the analog signal is sampled at regular intervals and mapped to a range of discreet binary values. The data can also be encoded by a range of compression/decompression codecs, like ADPCM, or MP3.

The WaveIn and WaveOut classes in this project are based on two examples I found in C#, Ianier Munoz' Full duplex audio player, and this example on MSDN: Creating a PInvoke library in C#. I wrote my own classes using these as a guide, adding error handling, aligning calls and structs with their C++ equivalents, condensing them into two classes, and adding a number of methods, like: pause, stop, play, resume, device name, volume, position, length etc.

Using the WaveOut class

Properties

  • AvgBytesPerSecond get private set - Average BPS of current track
  • BitsPerSample get private set - Number of allocated buffers
  • BufferSize get set - Internal buffer size
  • BufferCount get set - Number of buffers allocated
  • Channels get private set - Channels in current track
  • Convert2Channels get set - Forced convert to two channels
  • Convert16Bit get set - Forced convert 16 bit
  • Device get set - Playback device ID
  • Length get private set - Data length
  • Playing get private set - Playback status
  • SamplesPerSecond get private set - SPS of the current track

Methods

  • uint GetDeviceCount() - Get the number of playback devices in the system
  • MMSYSERR GetOutputDeviceName(uint deviceId, ref string prodName) - Get the output device name from the device ID
  • bool CopyStream(string file, ref MemoryStream stream) - Copy Wav data to a memory stream with auto conversion
  • uint GetPosition() - Get the playback position in the stream
  • MMSYSERR GetVolume(ref uint left, ref uint right) - Get the current volume level
  • MMSYSERR SetVolume(uint left, uint right) - Set the volume level
  • MMSYSERR Play(string file) - Start playback
  • MMSYSERR Pause() - Pause playback
  • MMSYSERR Resume() - Resume playback
  • MMSYSERR Stop() - Stop playback, and cleanup resources

The most basic setup options would be to call CopyStream, calculate the buffer size and number, and then Play. The properties needed for playback are calculated internally when CopyStream is called, (AvgBytesPerSecond/BitsPerSample/Channels and SamplesPerSecond). These can then be used to determine the ideal buffer size and number.

C#
/// <summary>calculate playback buffer size</summary>
private void CalculatePlayerBufferSize()
{
    this.BufferSize = (uint)this.SamplesPerSecond / this.BitsPerSample;
    this.BufferCount = (uint)this.BitsPerSample;
}

The ACM compression manager is also called automatically via the CopyStream method. If the wFormatTag member of the WAVEFORMATEX structure is anything other than WAVE_FORMAT_PCM, (an uncompressed bit stream), or options to force 16 bit or two channel conversion are set, an instance of the AcmStreamOut class is invoked, and the data stream is decompressed. The MemoryStream is then returned with the playback data. Now, to do this over again, I would probably forego the MemoryStream altogether, and use alloc and pass a pointer to a byte array around. Every time you convert data, it is costly, and given the 'real time' processing requirement, limiting these sorts of conversions is a must.

Using the WaveIn class

Properties

  • AvgBytesPerSecond get private set - Average BPS of the current track
  • BitsPerSample get private set - Number of allocated buffers
  • BufferSize get set - Internal buffer size
  • BufferCount get set - Number of buffers allocated
  • Channels get private set - Channels in current track
  • Convert2Channels get set - Forced convert to two channels
  • Convert16Bit get set - Forced convert 16 bit
  • Device get set - Playback device ID
  • Format get private set - Recording format ID
  • Recording get private set - Recording status
  • SamplesPerSecond get private set - SPS of the current track

Methods

  • uint GetDeviceCount() - Get the number of playback devices in the system
  • MMSYSERR GetInputDeviceName(uint deviceId, ref string prodName) - Get the input device name from the device ID
  • Stream CreateStream(Stream waveData, WAVEFORMATEX format) - Copy header to a stream
  • WAVEFORMATEX WaveFormat(uint rate, ushort bits, ushort channels) - Calculate the WAVEFORMATEX structure
  • MMSYSERR Record() - Begin recording
  • MMSYSERR Pause() - Pause recording
  • MMSYSERR Resume() - Resume recording
  • MMSYSERR Stop() - Stop playback, and cleanup resources

This is very simple to use. Set the recording properties on a WAVEFORMATEX structure (SamplesPerSecond, BitsPerSample, Channels), and pass this with the BufferFillEventHandler and the DeviceId into the class constructor. Then, call Record to begin recording. When finished recording, call the CreateStream method to copy the Wave format header into the stream, then copy the data in.

C#
/// <summary>save recording</summary>
/// <returns>success</returns>
private bool RecordingSave(bool create)
{
    try
    {
        Stream sw = _cRecorder.CreateStream(_cStreamMemory, _cWaveFormat);
        byte[] bf = new byte[sw.Length - sw.Position];
        sw.Read(bf, 0, bf.Length);
        sw.Dispose();
        FileStream fs = new FileStream(sfdSave.FileName, 
                            create ? FileMode.Create : FileMode.Append);
        fs.Write(bf, 0, bf.Length);
        fs.Close();
        fs.Dispose();
        mnuItemSave.Enabled = true;
        return true;
    }
    catch
    {
        ErrorPrompt("Bad File Name", "Could Not Save File!", 
                    "The Recording could not be saved.");
        return false;
    }
}

Audio Compression Manager

Properties

  • Convert2Channels get set - Forced convert to two channels
  • Convert16Bit get set - Forced convert 16 bit
  • DataLength get private set - Get data length

Methods

  • SND_RESULT PreConvert(string file) - Convert entire file
  • SND_RESULT Create(Stream waveData, WAVEFORMATEX format) - Initialize settings and create stream
  • SND_RESULT Read(ref byte[] data, uint size) - Read converted byte stream
  • void Close() - Close the stream and converter

Though I see the ACM API referred to as outdated, and would recommend the use of a newer machine like DirectSound, it appears to offer the advantage of being able to leverage the compression features of some codecs, whereas DirectSound may need additional coding to accomplish this. My implementation of this class is based on a VB6 project written by Arne Elster: WaveOut player. The problem I had with his implementation was that data was converted as it streamed in. So I cheated a bit, and created a loop that pre-converted the file before playback begins. Might not be the best implementation for really large files (though I have tried a 4 minute song, with no noticeable lag time).

The converter is called from the WaveOut class using a single call to Preconvert. This calls Create which sets up the conversion stream:

C#
/// <summary>Create the stream and initialize settings</summary>
/// <param name="file">file name</param>
/// <returns>SND_RESULT</returns>
public SND_RESULT Create(string file)
{
    if (!IsValidFile(file))
        return SND_RESULT.SND_ERR_INVALID_SOURCE;
    // find wav chunks data and fmt
    _ckData = GetChunkPos(file, "data");
    _ckInfo = GetChunkPos(file, "fmt ");
    DataLength = _ckData.Length;

    // valid chunks?
    if (_ckData.Start == 0)
        return SND_RESULT.SND_ERR_INVALID_SOURCE;
    if (_ckInfo.Start == 0)
        return SND_RESULT.SND_ERR_INVALID_SOURCE;
    if (_ckInfo.Length < 16)
        return SND_RESULT.SND_ERR_INVALID_SOURCE;

    // open file
    _waveHandle = FileOpen(file, FILE_ACCESS.GENERIC_READ, 
                  FILE_SHARE.FILE_SHARE_READ, FILE_METHOD.OPEN_EXISTING);
    if (_waveHandle == INVALID_HANDLE)
        return SND_RESULT.SND_ERR_INVALID_SOURCE;

    // shrink data chunks with illegal length to file length
    if (FileLength(_waveHandle) < (_ckData.Start + _ckData.Length))
        _ckData.Length = FileLength(_waveHandle) - _ckData.Start;
    // read info chunk
    _btWfx = new byte[_ckInfo.Length];
    FileSeek(_waveHandle, (int)_ckInfo.Start, (uint)SEEK_METHOD.FILE_BEGIN);
    fixed (byte* pBt = _btWfx)
    { FileRead(_waveHandle, pBt, _ckInfo.Length); }

    // copy the header
    uint size = (uint)sizeof(WAVEFORMATEX);
    fixed (byte* pBt = _btWfx) fixed (WAVEFORMATEX* pWv = &_tWFXIn)
    { { RtlMoveMemory(pWv, pBt, size); } }

    // seek to the beginning of the audio data
    FileSeek(_waveHandle, (int)_ckData.Start, (uint)SEEK_METHOD.FILE_BEGIN);

    // init the Audio Compression Manager
    if (InitConversion() != MMSYSERR.NOERROR)
    {
        Close();
        return SND_RESULT.SND_ERR_INTERNAL;
    }
    return SND_RESULT.SND_ERR_SUCCESS;
}

Now you may have noticed that I am passing pointers into the API rather then letting PInvoke make the cast by passing byref. I was having trouble getting this working, so decided to stay as true to the call setup spec as possible, so if the API called for a pointer, I pass it a pointer. Though I doubt that had anything to do with the problem, this was an implementation that worked. The above code sets up the stream, getting chunk sizes, opening a file handle, copying the header, seeking to data, then sets up the conversion stream with InitConversion():

C#
/// <summary>Create a stream and size buffers</summary>
/// <returns>bool</returns>
private MMSYSERR InitConversion()
{
    MMSYSERR mmr;

    if (_hStream != INVALID_STREAM_HANDLE)
        CloseConverter();

    _tWFXOut = _tWFXIn;

    if (_tWFXOut.wBitsPerSample < 8)
        _tWFXOut.wBitsPerSample = 8;
    else if (_tWFXOut.wBitsPerSample > 8)
        _tWFXOut.wBitsPerSample = 16;
    // force conversion to 16bit
    if (Convert16Bit)
        _tWFXOut.wBitsPerSample = 16;
    if (Convert2Channels)
        _tWFXOut.nChannels = 2;

    // create the new format
    _tWFXOut = CreateWFX(_tWFXOut.nSamplesPerSec, _tWFXOut.nChannels, 
                         _tWFXOut.wBitsPerSample);

  //  if (_tWFXOut.wFormatTag == WAVE_FORMAT_ADPCM || 
          _tWFXOut.wFormatTag == WAVE_FORMAT_PCM)
  //      _tWFXOut.cbSize = 0;

    // open stream
    fixed (IntPtr* pSt = &_hStream) fixed (byte* pBt = _btWfx) 
           fixed (WAVEFORMATEX* pWOut = &_tWFXOut)
    { { { mmr = acmStreamOpen(pSt, IntPtr.Zero, pBt, pWOut, IntPtr.Zero, 
          UIntPtr.Zero, UIntPtr.Zero, ACM_STREAMOPENF_NONREALTIME); } } }

    // failed, try going to defaults
    if (mmr != MMSYSERR.NOERROR)
    {
        // try changing bps
        if (_tWFXOut.wBitsPerSample == 16)
            _tWFXOut.wBitsPerSample = 8;
        else
            _tWFXOut.wBitsPerSample = 16;

        if (Convert2Channels)
        {
            if (_tWFXIn.nChannels == 1)
                _tWFXOut.nChannels = 1;
        }

        // try again
        fixed (WAVEFORMATEX* pWOut = &_tWFXOut, pWIn = &_tWFXIn) 
                fixed (IntPtr* pSt = &_hStream) fixed (byte* pBt = _btWfx)
        { { { mmr = acmStreamOpen(pSt, IntPtr.Zero, pBt, pWOut, IntPtr.Zero, 
                    UIntPtr.Zero, UIntPtr.Zero, 0); } } }

        // failed
        if (mmr != MMSYSERR.NOERROR)
            return mmr;
    }
    // set size of output buffer
    //Decimal sx = (int)Decimal.Divide(1000 / OUTPUT_BUFFER_MS);
    _iOutputLen = (uint)(_tWFXOut.nAvgBytesPerSec / 2);

    // needed size of input buffer to fill the output buffer
    fixed (uint* pInLen = &_iInputLen)
    { mmr = acmStreamSize(_hStream, _iOutputLen, pInLen, 
              (uint)ACM_STREAMSIZEF.ACM_STREAMSIZEF_DESTINATION); }

    // failed
    if (mmr != MMSYSERR.NOERROR)
    {
        acmStreamClose(_hStream, 0);
        _hStream = INVALID_STREAM_HANDLE;
        return mmr;
    }

    // success
    _btOutput = new byte[_iOutputLen];
    _btInput = new byte[_iInputLen];
    _bInEndOfStream = false;
    _bInFirst = true;
    _iKeepInBuffer = 0;
    return MMSYSERR.NOERROR;
}

This call creates a suitable WAVEFORMATEX structure (CreateWfx), opens a new stream, then prepares the source and destination byte arrays. If you want to know more, I suggest some reading on MSDN and stepping through this class.

FFT

Methods

  • double ComplexOut(int index)
  • void ImagIn(int index, double value)
  • double ImagOut(int index)
  • void NumberOfSamples(int count)
  • void RealIn(int index, double value)
  • double RealOut(int index)
  • void WithTimeWindow(int size)

I couldn't find any examples of a Complex FFT in C#, so I rewrote one I found in VB6: Ulli's Fast Fourier Transformation project used as a base, removed VTable patching, converted it to use pointers, and removed the unneeded operations. The heart of the class is in the GetIt method, where the Real and Imaginary planes are calculated:

C++
private void Butterfly(Sample* ps, Sample* pu, Sample* oj, Sample* ok)
{
    _smpT->Real = pu->Real * ok->Real - pu->Imag * ok->Imag;
    _smpT->Imag = pu->Imag * ok->Real + pu->Real * ok->Imag;
    ok->Real = oj->Real - _smpT->Real;
    oj->Real += _smpT->Real;
    ok->Imag = oj->Imag - _smpT->Imag;
    oj->Imag += _smpT->Imag;
    _dTemp = ps->Real * pu->Real + ps->Imag * pu->Imag;
    pu->Imag += ps->Imag * pu->Real - ps->Real * pu->Imag;
    pu->Real -= _dTemp;
}

private Sample GetIt(int index)
{
    if (!(_bUnknownSize || index > _iUB))
    {
        if (_bProcess)
        {
            _bProcess = false;
            _iStageSz = 1;
            int i = 0, j = 0;
            do
            {
                //divide and conquer
                _iNumBf = _iStageSz;
                _iStageSz = _iNumBf * 2;
                _dTemp = _dPi / _iStageSz;
                _smpS->Real = Math.Sin(_dTemp);
                _smpS->Real = 2 * _smpS->Real * _smpS->Real;
                _smpS->Imag = Math.Sin((_dTemp * 2));

                for (i = 0; i < _iUB + 1; i += _iStageSz)
                {
                    _smpU->Real = 1;
                    _smpU->Imag = 0;
                    for (j = i; j < (i + _iNumBf); j++)
                    {
                        fixed (Sample* pV1 = &_smpValues[j], 
                                pV2 = &_smpValues[j + _iNumBf])
                        { Butterfly(_smpS, _smpU, pV1, pV2); }
                    }
                }
            } while (!(_iStageSz > _iUB));
        }
    }
    return _smpValues[index];
}

You may wonder at the need for pointers throughout various sections of this project. As a test, I wrote this and the IIRFilter class both with and without pointers, and benchmarked the results..

Fixed is broken..

An interesting and revealing test compared classes with and without pointers, with some surprising results: using straight pointers versus variables, the IIRFilter class was an average 22% faster on my AMD 2600, but before optimization, the FFT class was actually 11% slower with pointers. So I expanded my tests, realizing that the difference between the two classes (in that version) was that I was using the fixed statement in several places throughout the FFT class. Here is the revised test:

C#
using System;
using System.Runtime.InteropServices;

namespace SpeedTest
{
    class Timing
    {
        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, 
          EntryPoint = "QueryPerformanceCounter", SetLastError = true)]
        private static extern int QueryPerformanceCounter(ref double lpPerformanceCount);

        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, 
          EntryPoint = "QueryPerformanceFrequency", SetLastError = true)]
        private static extern int QueryPerformanceFrequency(ref double lpFrequency);

        private double nFrequency = 0;
        private double nStart = 0;
        private double nNow = 0;

        public Timing()
        {
            QueryPerformanceFrequency(ref nFrequency);
        }

        public void Start()
        {
            QueryPerformanceCounter(ref nStart);
        }

        public double Elapsed()
        {
            QueryPerformanceCounter(ref nNow);
            return 1000 * (nNow - nStart) / nFrequency;
        }
    }
}

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace SpeedTest
{
    unsafe class Program
    {
        [DllImport("ntdll.dll", SetLastError = false)]
        private static extern int RtlCompareMemory(byte[] Source1, 
                                  byte[] Source2, uint length);

        [DllImport("ntdll.dll", SetLastError = false)]
        private static extern int RtlMoveMemory(byte[] Destination, 
                                  byte[] Source, uint length);

        [DllImport("ntdll.dll", SetLastError = false)]
        private static extern int RtlMoveMemory(byte* Destination, 
                                  byte* Source, uint length);

        [DllImport("kernel32", 
                   CharSet = CharSet.Ansi, SetLastError = true)]
        private static extern IntPtr 
                LoadLibrary([MarshalAs(UnmanagedType.LPStr)]string lpFileName);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern bool FreeLibrary(IntPtr hModule);

        [DllImport("kernel32.dll", CharSet = CharSet.Ansi, 
                   ExactSpelling = true, SetLastError = true)]
        private static extern IntPtr GetProcAddress(IntPtr hModule, 
                [MarshalAs(UnmanagedType.LPStr)]string procName);

        private unsafe delegate bool MoveMemoryInvoke(byte* dest, 
                                     byte* source, uint length);

        private static MoveMemoryInvoke MoveMemory;
        private static Timing _Timing = new Timing();

        static void Main(string[] args)
        {
            Console.Title = "SpeedTest";
            ConsoleKeyInfo cki;
            Console.TreatControlCAsInput = true;

            CreateProtoType();
            Test();
            Console.WriteLine("Press Escape key to close");
            do
            {
                cki = Console.ReadKey(true);
            } while (cki.Key != ConsoleKey.Escape);
        }

        static bool CreateProtoType()
        {
            // movemem delegate
            IntPtr hModule = LoadLibrary("ntdll.dll");
            if (hModule == IntPtr.Zero)
                return false;
            IntPtr hProc = GetProcAddress(hModule, "RtlMoveMemory");
            if (hProc == IntPtr.Zero)
                return false;
            MoveMemory = (MoveMemoryInvoke)Marshal.GetDelegateForFunctionPointer(
                                            hProc, typeof(MoveMemoryInvoke));
            FreeLibrary(hModule);
            hModule = IntPtr.Zero;
            return true;
        }

        /// <summary>Bit swapping comparison</summary>
        static void Test()
        {
            // create byte arrayS
            byte[] bt1 = new byte[16];
            byte[] bt2 = new byte[16];

            // load source array
            for (byte i = 0; i < 16; i++)
                bt1[i] = i;

            /////////////////////////////STRAIGHT COPY////////////////////////////////
            Console.WriteLine("Start Test 1: BYTE ARRAY COPY");
            _Timing.Start();
            for (uint i = 0; i < 10000; i++)
                bt2 = bt1;
            if (!Verify(ref bt1, ref bt2))
                Console.WriteLine("Write failed");
            else
                Console.WriteLine("Result: 10k * 16 bytes copied in " + 
                                  _Timing.Elapsed().ToString());
            /////////////////////////////BLOCK COPY///////////////////////////////////
            Console.WriteLine("Start Test 2: BUFFER BLOCK COPY");
            bt2 = new byte[16];
            _Timing.Start();
            for (uint i = 0; i < 10000; i++)
                Buffer.BlockCopy(bt1, 0, bt2, 0, 16);
            if (!Verify(ref bt1, ref bt2))
                Console.WriteLine("Write failed");
            else
                Console.WriteLine("Result: 10k * 16 bytes copied in " + 
                                  _Timing.Elapsed().ToString());
            /////////////////////////////API COPY///////////////////////////////////
            Console.WriteLine("Start Test 3: API COPY");
            bt2 = new byte[16];
            _Timing.Start();
            for (uint i = 0; i < 10000; i++)
                RtlMoveMemory(bt2, bt1, 16);
            if (!Verify(ref bt1, ref bt2))
                Console.WriteLine("Write failed");
            else
                Console.WriteLine("Result: 10k * 16 bytes copied in " + 
                                  _Timing.Elapsed().ToString());
            /////////////////////////////API POINTERS/////////////////////////////////
            Console.WriteLine("Start Test 4: API POINTERS COPY");
            bt2 = new byte[16];
            _Timing.Start();
            fixed (byte* p1 = bt1, p2 = bt2)
            {
                for (uint i = 0; i < 10000; i++)
                    RtlMoveMemory(p2, p1, 16);
            }
            if (!Verify(ref bt1, ref bt2))
                Console.WriteLine("Write failed");
            else
                Console.WriteLine("Result: 10k * 16 bytes copied in " + 
                                  _Timing.Elapsed().ToString());
            /////////////////////////////DELEGATE API/////////////////////////////////
            Console.WriteLine("Start Test 5: API DELEGATE COPY");
            bt2 = new byte[16];
            _Timing.Start();
            fixed (byte* p1 = bt1, p2 = bt2)
            {
                for (uint i = 0; i < 10000; i++)
                    MoveMemory(p2, p1, 16);
            }
            if (!Verify(ref bt1, ref bt2))
                Console.WriteLine("Write failed");
            else
                Console.WriteLine("Result: 10k * 16 bytes copied in " + 
                                  _Timing.Elapsed().ToString());
            Console.WriteLine("");
        }

        static bool Verify(ref byte[] arr1, ref byte[] arr2)
        {
            return (RtlCompareMemory(arr1, arr2, 16) == 16);
        }
    }
}

ptrtest.png

As you can see, I tried a number of different options: straight copy, Buffer.BlockCopy, RtlMoveMemory, pointers inside a fixed statement, and even a function pointer to CopyMemory. The consistently slowest: the function pointer delegate (why do they even make these things if they have so much overhead as to be unusable?); the fastest: Buffer.BlockCopy. Now this was surprising, how can anything be faster then RtlMoveMemory? BlockCopy is likely a wrapper for this function.. the answer is that there is a lot of overhead placed on PInvoke; security token check, param testing etc., most of which is redundant, as most API already have their own internal checks and error returns. Another thing that surprised me was how slow copying pointers inside a fixed statement was. It is 10* faster doing a straight copy than using fixed on my machine. So, the lesson here, pointers are fast, unless using fixed.. avoid running a fixed statement inside a loop, and if possible, initialize the variables as pointers. With that lesson learned, I removed all but one fixed statement in the FFT class, benching it to about 8% faster than its pointer-less counterpart.

Visualizers

vs1.png

The Frequency domain uses the ComplexOut function of the FFT to plot frequency amplitude across 21 bands. The bar graph is drawn using a premade Bitmsap:

C#
private void CreateGraphBar(int width, int height)
{
    int barwidth = ((width - 4) / 21);
    int barheight = height - 2;

    if (_bmpGraphBar != null)
        _bmpGraphBar.Dispose();
    _bmpGraphBar = new Bitmap(barwidth, barheight);

    Rectangle barRect = new Rectangle(0, 0, barwidth, barheight);

    using (Graphics g = Graphics.FromImage(_bmpGraphBar))
    {
        g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
        using (LinearGradientBrush fillBrush = 
           new LinearGradientBrush(barRect, Color.FromArgb(0, 255, 0), 
           Color.FromArgb(255, 0, 0), LinearGradientMode.Vertical))
        {
            Color[] fillColors = { 
                Color.FromArgb(255, 0, 0),
                Color.FromArgb(255, 64, 0),
                Color.FromArgb(255, 128, 0),
                Color.FromArgb(255, 196, 0),
                Color.FromArgb(255, 255, 0),
                Color.FromArgb(196, 255, 0),
                Color.FromArgb(128, 255, 0),
                Color.FromArgb(64, 255, 0),
                Color.FromArgb(0, 255, 0) };

            float[] fillPositions = { 0f, .2f, .4f, .5f, .6f, .7f, .8f, .9f, 1f };
            ColorBlend myBlend = new ColorBlend();
            myBlend.Colors = fillColors;
            myBlend.Positions = fillPositions;
            fillBrush.InterpolationColors = myBlend;
            g.FillRectangle(fillBrush, barRect);
        }
    }

    _cBufferDc = new cStoreDc();
    _cBufferDc.Height = height;
    _cBufferDc.Width = width;
    _cGraphicDc = new cStoreDc();
    _cGraphicDc.Height = _bmpGraphBar.Height;
    _cGraphicDc.Width = _bmpGraphBar.Width;
    _cGraphicDc.SelectImage(_bmpGraphBar);
}

The image is copied into a temporary DC, and drawn using BitBlt. Now, I originally tried to draw this directly with DrawImage, but because of the very slow nature of this method (29 overloads to int), it proved to be a bottleneck, hence the need for BitBlt (4* faster).

The sample data is fed into the FFT through RealIn(), the complex values are normalized, mids are cut with a Hanning window, then represented on the graph by drawing the bar at lengths relative to the value. This is all drawn into a buffer dc, which is then blitted to the PictureBox control.

C#
private void DrawFrequencies(Int16[] intSamples, IntPtr handle, int width, int height)
{
    int i, j;
    int count = FFT_STARTINDEX;
    int barwidth = _bmpGraphBar.Width;
    double[] real = new double[intSamples.Length];
    double complex = 0, band = 0;
    Rectangle rcBand = new Rectangle(0, 0, width, height);

    try
    {
        _FFT.NumberOfSamples(FFT_SAMPLES);
        _FFT.WithTimeWindow(1);
        // load samples
        for (i = 0; i < FFT_SAMPLES; i++)
            _FFT.RealIn(i, intSamples[i]);

        // normalize values and cut them at FFT_MAXAMPLITUDE
        for (i = 0; i < (FFT_SAMPLES / 2) + 1; i++)
        {
            complex = _FFT.ComplexOut(i);
            // normalise
            real[i] = complex / (FFT_SAMPLES / 4) / 32767;
            // cut the output to FFT_MAXAMPLITUDE, so
            // the spectrum doesn't get too small
            if (real[i] > FFT_MAXAMPLITUDE)
                real[i] = FFT_MAXAMPLITUDE;
            real[i] /= FFT_MAXAMPLITUDE;
        }

        for (i = 0; i < FFT_BANDS - 1; i++)
        {
            // average for the current band
            for (j = count; j < count + FFT_BANDWIDTH + 1; j++)
                band += real[j];
            // boost frequencies in the middle with a hanning window,
            // because they have less power then the low ones
            band = (band * (Hanning(i + 3, FFT_BANDS + 3) + 1)) / FFT_BANDWIDTH;
            _dBands[i] = _bEightBit ? band / 8 : band;
            if (_dBands[i] > 1)
                _dBands[i] = 1;
            // skip some bands
            count += FFT_BANDSPACE;
        }

        // backfill
        IntPtr brush = CreateSolidBrush(0x565656);
        RECT rc = new RECT(0, 0, width, height);
        FillRect(_cBufferDc.Hdc, ref rc, brush);
        DeleteObject(brush);

        // draw bands to buffer
        for (i = 0; i < _dBands.Length; i++)
        {
            rcBand.X = (i * barwidth) + (i + 1) * DRW_BARSPACE;
            rcBand.Width = barwidth;
            rcBand.Y = (int)(height - (height * _dBands[i]));
            rcBand.Height = height - (rcBand.Y  + DRW_BARYOFF);
            if (rcBand.Height + rcBand.Y > height)
            {
                rcBand.Height = height - 2;
                rcBand.Y = 1;
            }
            BitBlt(_cBufferDc.Hdc, rcBand.X, rcBand.Y, rcBand.Width, 
                   rcBand.Height, _cGraphicDc.Hdc, 0, rcBand.Y, 0xCC0020);
        }
        // blit in buffer
        IntPtr destDc = GetDC(handle);
        BitBlt(destDc, 0, 0, width, height, _cBufferDc.Hdc, 0, 0, 0xCC0020);
        ReleaseDC(handle, destDc);
    }
    catch { }
}

vs2.png

The Time domain example plots the power of the input stream directly to the graph, based on Jeff Morton's Sound Catcher project, with optimizations and 8 bit processing added.

Digital Signal Processing

Probably the easiest language to translate to C# is straight C. None of the burdens of so many language specific trappings, it is almost a straight copy and paste. IIRFilter is a C# implementation of the Biquad Filter by Tom St Denis. I made changes as necessary, and added a memory faction. As far as I know, memory allocated with HeapAlloc is not subject to garbage collection.

C#
/// <summary>Allocate heap memory</summary>
/// <param name="size">size desired</param>
/// <returns>memory address</returns>
private biquad* Alloc(int size)
{
    return HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (uint)size);
}

/// <summary>Release heap memory</summary>
/// <param name="pmem">memory address</param>
private void Free(biquad* b)
{
    HeapFree(GetProcessHeap(), 0, b);
}
Using IIRFilter

The biquad structures are created using the BiQuadFilter method. This returns a pointer to a new biquad structure. These structures should be declared as pointers to avoid unnecessary conversions:

C#
private IIR _eqBands = new IIR();
private unsafe biquad* _bq100Left;
private unsafe biquad* _bq200Left;
...

There are seven types of filter options with the biquad. I was only able to get five of them working. The other two may require some other pre-processing, and I was unable to find any examples of it in use.

  • type(HSH), gain, center freq, rate, bandwidth-(HSH, 4, 4000, sps, 1): A high-shelf filter passes all frequencies, but increasing or reducing frequencies above the cutoff frequency by a specified amount.
  • type(LPF), start freq, cutoff freq, sample rate, banwidth-(LPF, 8000, 10000, sps, 1): A low-pass filter is used to cut unwanted high-frequency signals.
  • type(LSH), gain, cutoff freq, sample rate, banwidth-(LSH, .5, 80, sps, 1): A low-shelf filter passes all frequencies, but increasing or reducing frequencies below the cutoff frequency by a specified amount.
  • type(NOTCH), gain, center freq, rate, bandwidth-(NOTCH, 1, 20, sps, 1): A notch filter or band-rejection filter is a filter that passes most frequencies unaltered, but attenuates those in a specific range to very low levels.
  • type(PEQ), gain, center freq, sample rate, banwidth-(PEQ, 8, 400, sps, 1): A peak EQ filter makes a peak or a dip in the frequency response, commonly used in graphic equalizers.
  • type(BPF), start freq, cutoff freq, sample rate, banwidth-(BPF, 10, 3200, sps, 1)??: A band-pass filter passes a limited range of frequencies.
  • type(HPF), center freq, cutoff freq, sample rate, banwidth-(LPF, 10, 40, sps, 1)??: A high-pass filter passes high frequencies fairly well; it is helpful as a filter to cut any unwanted low frequency components.

Example from LoadEq():

C#
// peak filter at 100hz
_bq100Left = _eqBands.BiQuadFilter(IIR.Filter.PEQ, msLeftFreq100.Value, 100, 
                                   this.SamplesPerSecond, 1);
...
// cut high harmonic with LowPass filter
_bqLPF = _eqBands.BiQuadFilter(IIR.Filter.LPF, 8000, 10000, this.SamplesPerSecond, 1);
// boost mid harmonic with HighShelf filter
_bqHPF = _eqBands.BiQuadFilter(IIR.Filter.HSH, 4, 4000, this.SamplesPerSecond, 1);

Data from the Player or Recorder callbacks is passed through the ProcessEq method and loops through the byte array, modifying the byte per the filter arrangement.

C#
/// <summary>process dsp chain</summary>
private void ProcessEq(ref byte[] buffer)
{
    int len = buffer.Length;
    int i = 0;

    unsafe
    {
        if (this.Channels == 1)
        {
            do
            {
                // filters
                if (this.IsHighPassOn)
                {
                    _eqBands.BiQuad(ref buffer[i], _bqHPF);
                }
                if (this.IsLowPassOn)
                {
                    _eqBands.BiQuad(ref buffer[i], _bqLPF);
                }
                // eq  
                if (this.IsEqOn)
                {
                    _eqBands.BiQuad(ref buffer[i], _bq100Left);
                    _eqBands.BiQuad(ref buffer[i], _bq200Left);
                    _eqBands.BiQuad(ref buffer[i], _bq400Left);
                    _eqBands.BiQuad(ref buffer[i], _bq800Left);
                    _eqBands.BiQuad(ref buffer[i], _bq1600Left);
                    _eqBands.BiQuad(ref buffer[i], _bq3200Left);
                    _eqBands.BiQuad(ref buffer[i], _bq6400Left);
                }
                i++;
            } while (i < len);
        }
        else
        {
            len -= 2;
            i = 0;
            do
            {
                // filters
                if (this.IsHighPassOn)
                {
                    _eqBands.BiQuad(ref buffer[i], _bqHPF);
                    _eqBands.BiQuad(ref buffer[i + 1], _bqHPF);
                }
               if (this.IsLowPassOn)
                {
                    _eqBands.BiQuad(ref buffer[i], _bqLPF);
                    _eqBands.BiQuad(ref buffer[i + 1], _bqLPF);
                }
                // eq  
                if (this.IsEqOn)
                {
                    // left channel
                    _eqBands.BiQuad(ref buffer[i], _bq100Left);
                    _eqBands.BiQuad(ref buffer[i], _bq200Left);
                    _eqBands.BiQuad(ref buffer[i], _bq400Left);
                    _eqBands.BiQuad(ref buffer[i], _bq800Left);
                    _eqBands.BiQuad(ref buffer[i], _bq1600Left);
                    _eqBands.BiQuad(ref buffer[i], _bq3200Left);
                    _eqBands.BiQuad(ref buffer[i], _bq6400Left);
                    // right channel
                    _eqBands.BiQuad(ref buffer[i + 1], _bq100Right);
                    _eqBands.BiQuad(ref buffer[i + 1], _bq200Right);
                    _eqBands.BiQuad(ref buffer[i + 1], _bq400Right);
                    _eqBands.BiQuad(ref buffer[i + 1], _bq800Right);
                    _eqBands.BiQuad(ref buffer[i + 1], _bq1600Right);
                    _eqBands.BiQuad(ref buffer[i + 1], _bq3200Right);
                    _eqBands.BiQuad(ref buffer[i + 1], _bq6400Right);
                }
                i += 2;
            } while (i < len);
        }
    }
}

Mixer

Just when I think I'm out.. they pull me back in again! I thought I was done with this last weekend, and started testing it on XP and Vista64. XP had a couple interface issues which were easily resolved, and Vista seemed to work without issue.. until I tried the volume control. Apparently, waveOutGetVolume/waveOutSetVolume do nothing in Vista. Searching solutions on MSDN, I came across some bizarre workarounds that seemed too abstract to be necessary. I had written a mixer class in VB6 years ago, and decided to try translating that as my first option. Now, while searching for examples on the mixerXX API, I came across three examples, all of which crashed 64bit Vista with memory errors. After translating my own class, I started running into the same issue.. at first, I thought I had made a mistake on a struct size (unions), but after checking and rechecking, that was clearly not the case. It turns out that my implementation had one thing in common with the other three.. they all used Marshal.AllocHGlobal to allocate memory for the struct pointer. As soon as I changed that (and I used VirtualAlloc.. not the best choice because it allocates 4KB pages.. should be changed to HeapAlloc or LocalAlloc), it worked just fine..

C#
private MMSYSERR GetVolumeInfo(IntPtr hmixer, int ctrlType, ref MIXERCONTROL mxc)
{
    MMSYSERR err = MMSYSERR.NOERROR;
    try
    {
        IntPtr hmem = IntPtr.Zero;
        MIXERLINECONTROLS mxlc = new MIXERLINECONTROLS();
        mxlc.cbStruct = (uint)Marshal.SizeOf(mxlc);
        MIXERLINE mxl = new MIXERLINE();
        mxl.cbStruct = (uint)Marshal.SizeOf(mxl);
        mxl.dwComponentType = (uint)MIXERLINE_COMPONENTTYPE.DST_SPEAKERS;
        err = mixerGetLineInfo(hmixer, ref mxl, MIXER_GETLINEINFOF.COMPONENTTYPE);

        if (err == MMSYSERR.NOERROR)
        {
            mxlc.dwLineID = (uint)mxl.dwLineID;
            mxlc.dwControlID = (uint)ctrlType;
            mxlc.cControls = 1;
            mxlc.cbmxctrl = (uint)Marshal.SizeOf(mxc);
            hmem = malloc(Marshal.SizeOf(mxlc));
            mxlc.pamxctrl = hmem;
            mxc.cbStruct = (uint)Marshal.SizeOf(mxc);
            err = mixerGetLineControls(hmixer, ref mxlc, 
                           MIXER_GETLINECONTROLSF_ONEBYTYPE);

            if (err == MMSYSERR.NOERROR)
            {
                mxc = (MIXERCONTROL)Marshal.PtrToStructure(mxlc.pamxctrl, 
                                            typeof(MIXERCONTROL));
                if (hmem != IntPtr.Zero)
                    free(hmem, Marshal.SizeOf(mxc));
                return err;
            }
            if (hmem != IntPtr.Zero)
                free(hmem, Marshal.SizeOf(mxc));
        }
        return err;
    }
    catch { return err; }
}

private IntPtr malloc(int size)
{
    return VirtualAlloc(IntPtr.Zero, (uint)size, MEM_COMMIT, PAGE_READWRITE);
}

private void free(IntPtr m, int size)
{
    VirtualFree(m, (uint)size, MEM_RELEASE);
}

private MMSYSERR SetVolume(IntPtr hmixer, MIXERCONTROL mxc, uint volume)
{
    IntPtr hmem = IntPtr.Zero;
    MMSYSERR err = MMSYSERR.NOERROR;
    MIXERCONTROLDETAILS mxcd = new MIXERCONTROLDETAILS();
    MIXERCONTROLDETAILS_UNSIGNED vol = new MIXERCONTROLDETAILS_UNSIGNED();

    try
    {
        mxcd.hwndOwner = IntPtr.Zero;
        mxcd.dwControlID = mxc.dwControlID;
        mxcd.cbStruct = (uint)Marshal.SizeOf(mxcd);
        mxcd.cbDetails = (uint)Marshal.SizeOf(vol);
        mxcd.cChannels = 1;
        vol.value = volume;
        hmem = malloc(Marshal.SizeOf(vol));
        mxcd.paDetails = hmem;

        Marshal.StructureToPtr(vol, mxcd.paDetails, true);
        err = mixerSetControlDetails(hmixer, ref mxcd, 0x0);
        if (hmem != IntPtr.Zero)
            free(hmem, Marshal.SizeOf(vol));
        return err;
    }
    catch { return err; }
}

Control Summary

eq.png

Slider Control

The ubiquitous slider control. This is the third and the last version of the control (I'll update all of my controls used here sometime soon).

buttons.png

Glow Buttons

My little glow buttons. I think I'll use this as a template for future WinForms User Controls. No API, simple framework, and only took a few hours to write.

The mirror effect was created by copying the original image to a new bitmap with a decreased height, flipping it, then drawing it semi-transparent:

C#
/// <summary>Create the Mirror image</summary>
private void CreateMirror()
{
    if (_bmpMirror != null)
        _bmpMirror.Dispose();

    int height = (int)(this.Image.Height * .7f);
    int width = (int)(this.Image.Width * 1f);
    Rectangle imageRect = new Rectangle(0, 0, width, height);
    _bmpMirror = new Bitmap(imageRect.Width, imageRect.Height);

    using (Graphics g = Graphics.FromImage(_bmpMirror))
        g.DrawImage(this.Image, imageRect);
    _bmpMirror.RotateFlip(RotateFlipType.Rotate180FlipX);
}

/// <summary>Draw a mirror effect</summary>
private void DrawMirror(Graphics g, Rectangle bounds)
{
   // Rectangle imageRect = GetImageBounds(bounds, this.Image);
    bounds.Y = bounds.Bottom;
    bounds.Height = _bmpMirror.Height;
    bounds.Width = _bmpMirror.Width;
    using (ImageAttributes ia = new ImageAttributes())
    {
        ColorMatrix cm = new ColorMatrix();
        cm.Matrix00 = 1f;           //r
        cm.Matrix11 = 1f;           //g
        cm.Matrix22 = 1f;           //b
        cm.Matrix33 = MIRROR_LEVEL; //a
        cm.Matrix44 = 1f;           //w

        ia.SetColorMatrix(cm);
        g.DrawImage(_bmpMirror, bounds, 0, 0, _bmpMirror.Width, 
                    _bmpMirror.Height, GraphicsUnit.Pixel, ia);
    }
}

rcm.png

RCM -Lite

This one has really been a pain... especially in XP. There were many times when I thought, great.. it seems to be working perfectly, only to test it on XP and watch it explode. This version though, I am glad to say, was tested on Vista64, XP Professional, and Win7, and it seems to be working well on all of them (cross my fingers). I won't go into the code here, there are two other articles here that do, but I think this app was a good demonstration of what RCM can do in the right context. Anyways, you can consider this the last version of the library.

menu.png

ContextMenuRenderer

Just what it says.. A ToolStrip renderer designed for this project (but highly modifiable):

Properties

  • CheckImageColor get set - the checkbox image color
  • FocusedItemBorderColor get set - the focused item border color
  • FocusedItemForeColor get set - the focused item forecolor
  • FocusedItemGradientBegin get set - the starting color of the focused item gradient
  • FocusedItemGradientEnd get set - the ending color of the focused item gradient
  • MenuBackGroundColor get set - the background color
  • MenuBorderColorDark get set - the dark border color
  • MenuBorderColorLight get set - the light border color
  • MenuImageMarginColor get set - the border strip color
  • MenuImageMarginText get set - the border strip text
  • MenuItemForeColor get set - the forecolor
  • SeperatorInnerColor get set - the separator inner color
  • SeperatorOuterColor get set - the separator outer color

tooltip.png

CustomToolTip

A custom gradient tooltip class with all the trimmings..

Properties

  • TipBounds get set - the tip size and position
  • Captionr get set - the body of the tooltip text
  • DelayTime get set - delay before tip is shown
  • ForeColor get set - caption forecolor
  • GradientBegin get set - the starting color of the tip gradient
  • GradientEnd get set - the end color of the tip gradient
  • ItemImage get set - the tip image
  • MaximumLength get set - the maximum length of the tip
  • TextRightToLeft get set - render text right to left
  • Title get set - tip title
  • VisibleTime get set - the time the tip remains visible

Methods

  • void Start(string title, string caption, Image image, Rectangle bounds) - Start timer
  • void Start(string title, string caption, Image image, Point pt) - Start the tooltip timer
  • void Stop() - Stop the timer and close
  • void Dispose() - Release resources

Updates

  • Fixed a font issue in Win7
  • Title and stats load on menu open
  • EQ reworked for scroll
  • Fixed byte alignment error in ProcessEq
  • Some graphics tuning
  • Adjusted input stream buffer size in AcmStreamOut
  • Sundry fixes and adjustments

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

 
GeneralMy vote of 5 Pin
Roy Ben Shabat4-Sep-15 2:51
professionalRoy Ben Shabat4-Sep-15 2:51 
BugSound comes from the left only. Pin
Mustafa Sami Salt25-May-15 11:26
Mustafa Sami Salt25-May-15 11:26 
QuestionQustions about gain of IIR filter. Pin
wang_yanjing18-Mar-15 22:32
wang_yanjing18-Mar-15 22:32 
QuestionDisplay wave in csharp Pin
roya.irani12-Jun-14 22:15
roya.irani12-Jun-14 22:15 
GeneralMy vote of 5 Pin
simviews3-Jun-13 9:32
simviews3-Jun-13 9:32 
GeneralMy vote of 3 Pin
jfriedman23-Jan-13 8:43
jfriedman23-Jan-13 8:43 
GeneralMy vote of 5 Pin
onelopez18-Sep-12 14:01
onelopez18-Sep-12 14:01 
GeneralMy vote of 5 Pin
Wendelius19-May-12 2:56
mentorWendelius19-May-12 2:56 
GeneralMy vote of 5 Pin
Philip Liebscher1-May-12 10:30
Philip Liebscher1-May-12 10:30 
QuestionProblem occurred with timer for recording Pin
khagia30-Oct-11 23:40
khagia30-Oct-11 23:40 
GeneralMy vote of 5 Pin
Filip D'haene11-Oct-11 10:49
Filip D'haene11-Oct-11 10:49 
QuestionBest Pin
Anubhava Dimri1-Sep-11 0:49
Anubhava Dimri1-Sep-11 0:49 
QuestionThank you. Pin
Sergey Chepurin4-Jul-11 9:36
Sergey Chepurin4-Jul-11 9:36 
GeneralMy vote of 5 Pin
Roger Wright9-May-11 10:53
professionalRoger Wright9-May-11 10:53 
GeneralThe Best Pin
Steven.Pinto20006-May-11 21:33
Steven.Pinto20006-May-11 21:33 
GeneralStrange result of function GetInputDeviceName Pin
tumanovalex22-Mar-11 6:27
tumanovalex22-Mar-11 6:27 
GeneralMy vote of 5 Pin
partha chakraborti9-Mar-11 19:35
partha chakraborti9-Mar-11 19:35 
GeneralMy vote of 5 Pin
Dang Phi Son5-Feb-11 17:04
Dang Phi Son5-Feb-11 17:04 
QuestionHow to calculate the frequency and amplitude in time? Pin
gmajkun20-Dec-10 20:13
gmajkun20-Dec-10 20:13 
GeneralExelent Pin
steve23496-Dec-10 6:23
steve23496-Dec-10 6:23 
Question2 question . Pin
Kazuhito Sano26-Nov-10 12:09
Kazuhito Sano26-Nov-10 12:09 
AnswerRe: 2 question (Good Solution) Pin
John Underhill26-Mar-11 9:49
John Underhill26-Mar-11 9:49 
GeneralVery good. My vote of 5. Pin
The Manoj Kumar21-Oct-10 13:16
The Manoj Kumar21-Oct-10 13:16 
QuestionCould anyone record on x64? Pin
yasaralper23-Aug-10 1:48
yasaralper23-Aug-10 1:48 
GeneralQuestion Pin
hd4478013-Aug-10 5:35
hd4478013-Aug-10 5:35 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.