Click here to Skip to main content
15,892,697 members
Articles / Programming Languages / C#

Visualizing Sound

Rate me:
Please Sign up or sign in to vote.
4.94/5 (24 votes)
6 Nov 2012CPOL7 min read 77.6K   7.7K   67  
Listen or playback sound and visualize the frequency spread.
// -----------------------------------------------------------------------
// <copyright file="SoundSource.cs" company="None.">
//  By Philip R. Braica (HoshiKata@aol.com, VeryMadSci@gmail.com)
//
//  Distributed under the The Code Project Open License (CPOL)
//  http://www.codeproject.com/info/cpol10.aspx
// </copyright>
// -----------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using Microsoft.DirectX.DirectSound;
using Microsoft.Win32.SafeHandles;

namespace SoundFiltering
{
    /// <summary>
    /// Abstracts a DirectX sound input.
    /// </summary>
    public class SoundSource : IDisposable
    {
        #region Public events: ProcessData, ProcessFFTData.
        /// <summary>
        /// Delegate for process data event.
        /// </summary>
        /// <param name="data"></param>
        public delegate void ProcessData_Delegate(float [] data);

        /// <summary>
        /// Process data event.
        /// </summary>
        public event ProcessData_Delegate ProcessData;

        /// <summary>
        /// Process data FFT of entire buffer.
        /// </summary>
        public event ProcessData_Delegate ProcessFFTData;
        #endregion

        #region Protected data.

        /// <summary>
        /// Disposal race condition lock.
        /// </summary>
        protected object m_disposeLock = new object();

        /// <summary>
        /// Is disposed flag.
        /// </summary>
        protected bool m_disposed = false;

        /// <summary>
        /// DirectX capture.
        /// </summary>
        protected Capture m_capture = null;

        /// <summary>
        /// DirectX buffer.
        /// </summary>
        protected CaptureBuffer m_buffer = null;

        /// <summary>
        /// Capture thread.
        /// </summary>
        protected Thread m_captureThread = null;

        /// <summary>
        /// Capture buffer length.
        /// </summary>
        protected int m_captureBufferLength = 0;

        /// <summary>
        /// Notify.
        /// </summary>
        protected Notify m_notify;

        /// <summary>
        /// Event to flag a buffer is ready and full.
        /// </summary>
        protected AutoResetEvent m_bufferPositionEvent = null;

        /// <summary>
        /// Safe wait event from the position event used to pass into DirectX.
        /// </summary>
        protected SafeWaitHandle m_bufferPositionEvent_Handle = null;

        /// <summary>
        /// Manual event used to cause reset.
        /// </summary>
        protected ManualResetEvent m_threadTerminateEvent = null;

        /// <summary>
        /// Maximum buffering to use in seconds.
        /// </summary>
        const int c_bufferTimeInSeconds = 5;

        /// <summary>
        /// Notification points in seconds.
        /// </summary>
        const int c_notifiesInSeconds = 2;
        #endregion

        #region Public properties.
        /// <summary>
        /// Sources have a GUID associated with them.
        /// </summary>
        public Guid Guid { get; set; }

        /// <summary>
        /// Description of the device.
        /// </summary>
        public string Description { get; set; }

        /// <summary>
        /// Is this source the default source?
        /// </summary>
        public bool IsDefault
        {
            get { return Guid == Guid.Empty; }
        }

        /// <summary>
        /// Is this capturing?
        /// </summary>
        public bool IsCapturing { get; protected set; }

        /// <summary>
        /// Get / set the sample rate.
        /// </summary>
        public int SampleRateHz { get; set; }

        #endregion

        #region Constructors and dispose.
        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="id"></param>
        /// <param name="name"></param>
        public SoundSource(Guid id, string name)
        {
            Guid = id;
            Description = name;
        }

        /// <summary>
        /// Constructor.
        /// </summary>
        public SoundSource()
        {
            CaptureDevicesCollection cdc = new CaptureDevicesCollection();
            for (int i = 0; i < cdc.Count; i++)
            {
                if (cdc[i].DriverGuid == Guid.Empty)
                {
                    Guid = cdc[i].DriverGuid;
                    Description = cdc[i].Description;
                    break;
                }
            }

            // Create the events.
            m_bufferPositionEvent = new AutoResetEvent(false);
            m_bufferPositionEvent_Handle = m_bufferPositionEvent.SafeWaitHandle;
            m_threadTerminateEvent = new ManualResetEvent(true);
        }

        /// <summary>
        /// Dispose.
        /// </summary>
        void IDisposable.Dispose()
        {
            Dispose(true);
        }


        /// <summary>
        /// Destructor.
        /// </summary>
        ~SoundSource()
        {
            Dispose(false);
        }

        /// <summary>
        /// Dispose.
        /// </summary>
        /// <param name="disposing"></param>
        public void Dispose(bool disposing)
        {
            lock (m_disposeLock)
            {
                if (m_disposed) return;
                m_disposed = true;
            }
            GC.SuppressFinalize(this);
            if (IsCapturing)
            {
                Stop();
            }
            if (m_bufferPositionEvent_Handle != null)
            {
                m_bufferPositionEvent_Handle.Dispose();
            }
            if (m_bufferPositionEvent != null)
            {
                m_bufferPositionEvent.Close();
            }
            if (m_threadTerminateEvent != null)
            {
                m_threadTerminateEvent.Close();
            }
        }
        #endregion

        #region Public stuff: GetSources, GetDefaultSource, Start, Stop
        /// <summary>
        /// Get a list of possible things.
        /// </summary>
        /// <returns></returns>
        public static List<SoundSource> GetSources()
        {
            CaptureDevicesCollection captureDevices = new CaptureDevicesCollection();
            List<SoundSource> deviceList = new List<SoundSource>();
            foreach (DeviceInformation captureDevice in captureDevices)
            {
                deviceList.Add(new SoundSource(captureDevice.DriverGuid, captureDevice.Description));
            }
            return deviceList;
        }

        /// <summary>
        /// Starts capture process.
        /// </summary>
        public void Start()
        {
            if (IsCapturing)
            {
                return;
            }
            IsCapturing = true;

            m_capture = new Capture(Guid);
            WaveFormat waveFormat = new WaveFormat();

            // If an exception occurs below it's because one of these things is set wrong.
            // format.SamplesPerSecond = capture.Caps.Format96KhzStereo16Bit ? 96000 : capture.Caps.Format48KhzStereo16Bit ? 48000 : capture.Caps.Format44KhzStereo16Bit ? 44000 : 22000;
            waveFormat.Channels = (short)m_capture.Caps.Channels; // Read in the channels
            waveFormat.BitsPerSample = 16; // 16 bits per sample.
            // voice is around 40 - 8000 kHz to get plenty of range over 2x is required so 11k not enough prefer 22k.
            waveFormat.SamplesPerSecond = 22000;
            waveFormat.FormatTag = WaveFormatTag.Pcm;
            waveFormat.BlockAlign = (short)((waveFormat.Channels * waveFormat.BitsPerSample + 7) / 8);
            waveFormat.AverageBytesPerSecond = waveFormat.BlockAlign * waveFormat.SamplesPerSecond;

            m_captureBufferLength = waveFormat.AverageBytesPerSecond * c_bufferTimeInSeconds;
            CaptureBufferDescription captureBufferDescription = new CaptureBufferDescription();
            captureBufferDescription.Format = waveFormat;
            captureBufferDescription.BufferBytes = m_captureBufferLength;
            m_buffer = new CaptureBuffer(captureBufferDescription, m_capture);

            int waitHandleCount = c_bufferTimeInSeconds * c_notifiesInSeconds;
            BufferPositionNotify[] positions = new BufferPositionNotify[waitHandleCount];
            for (int i = 0; i < waitHandleCount; i++)
            {
                BufferPositionNotify position = new BufferPositionNotify();
                position.Offset = (i + 1) * m_captureBufferLength / positions.Length - 1;
                position.EventNotifyHandle = m_bufferPositionEvent_Handle.DangerousGetHandle();
                positions[i] = position;
            }

            m_notify = new Notify(m_buffer);
            m_notify.SetNotificationPositions(positions);

            m_threadTerminateEvent.Reset();
            m_captureThread = new Thread(new ThreadStart(ThreadLoop));
            m_captureThread.Name = "Sound capture";
            m_captureThread.Start();
        }

        /// <summary>
        /// Stops capture process.
        /// </summary>
        public void Stop()
        {
            if (IsCapturing)
            {
                IsCapturing = false;

                m_threadTerminateEvent.Set();
                m_captureThread.Join();

                m_notify.Dispose();
                m_buffer.Dispose();
                m_capture.Dispose();
            }
        }
        #endregion

        #region Worker protected stuff.
        private void ThreadLoop()
        {
            m_buffer.Start(true);
            try
            {
                int nextCapturePosition = 0;
                WaitHandle[] handles = new WaitHandle[] {m_threadTerminateEvent, m_bufferPositionEvent };
                while (WaitHandle.WaitAny(handles) > 0)
                {
                    int capturePosition, readPosition;
                    m_buffer.GetCurrentPosition(out capturePosition, out readPosition);

                    int lockSize = readPosition - nextCapturePosition;
                    if (lockSize < 0) lockSize += m_captureBufferLength;
                    if ((lockSize & 1) != 0) lockSize--;

                    int itemsCount = lockSize >> 1;

                    short[] data = (short[])m_buffer.Read(nextCapturePosition, typeof(short), LockFlag.None, itemsCount);
                    processData(data);
                    nextCapturePosition = (nextCapturePosition + lockSize) % m_captureBufferLength;
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            finally
            {
                m_buffer.Stop();
            }
        }

        /// <summary>
        /// Processes the captured data.
        /// </summary>
        /// <param name="data">Captured data</param>
        protected void processData(short[] data)
        {
            ProcessData_Delegate pdt = ProcessData;
            ProcessData_Delegate pfd = ProcessFFTData;
            if ((pdt == null) && (pfd == null)) return;

            int size = 2;
            while (size < data.Length)
            {
                size = size * 2;
            }
            float[] xf = new float[size];
            float[] xt = new float[data.Length];
            for (int i = 0; i < data.Length; i++)
            {
                float v = (float)data[i];
                xf[i] = v;
                xt[i] = v;
            }
            for (int i = data.Length; i < size; i++)
            {
                xf[i] = 0;
            }
       
            if (pdt != null)
            {
                pdt(xt);
            }
            if (pfd != null)
            {
                float[] fft = new float[xf.Length];
                Math.FFT.Forward(xf, fft);
                pfd(fft);
            }
        }
        #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
Technical Lead
United States United States
Phil is a Principal Software developer focusing on weird yet practical algorithms that run the gamut of embedded and desktop (PID loops, Kalman filters, FFTs, client-server SOAP bindings, ASIC design, communication protocols, game engines, robotics).

In his personal life he is a part time mad scientist, full time dad, and studies small circle jujitsu, plays guitar and piano.

Comments and Discussions