SoundMeter

This code provides basic "sound meter" functionality. I wrote it for a friend at DevelopMentor who wanted to implement an applause meter to judge the coding contests they run there sometimes.

The basic idea is simple: you fire up the SoundMeter, initialize it, and sample it in a loop, like this:

using (SoundMeter meter = new SoundMeter()) { meter.Initialize(SoundMeter.Frequency.Freq11KHz, SoundMeter.DynamicRange.DynRange8Bit, 0.25); meter.Start(); while (!done) { lastValue = meter.LastValue; // Wait until next sample interval } }

The 0.25 value that appears above is an interval over which to do the averaging. So it doesn't make much sense to read LastValue more often than that, since it won't change more often than that.

The way the class works internally is that it

    1. Creates a CaptureBuffer big enough to hold twice the specified interval

    2. Sets notifications at halfway through the buffer and the end of the buffer

    3. Spins up a background thread to receive the notifications (via an AutoResetEvent)

    4. Catches the notification in the background thread, where it reads the buffer and calculates the Root Mean Square (RMS) average of the samples

Since I wrote this code quickly to help someone out, it may not have all the i's dotted nor all the t's crossed. Still, someone might find it helpful, so I'm posting it here.

The Code

using System; using System.Threading; using System.IO; using Microsoft.DirectX; using Microsoft.DirectX.DirectSound; namespace CAndera.DirectSound { public class SoundMeter : IDisposable { public enum Frequency { Freq8KHz = 8000, Freq11KHz = 11025, Freq22KHz = 22050, Freq44KHz = 44100 } public enum DynamicRange { DynRange8Bit = 8, DynRange16Bit = 16 } private double lastValue; private Capture device; private CaptureBuffer buffer; private double interval; private short bitsPerSample; private int samplesPerSecond; private int samplesPerInterval; private Thread thread; private int bufferSize; private int intervalSize; private Notify notify; private AutoResetEvent notifyEvent; private BufferPositionNotify[] notifyPositions; private bool disposed = false; public SoundMeter() { } public void Initialize(Frequency freq, DynamicRange dr, double interval) { thread = new Thread(new ThreadStart(ThreadProc)); device = new Capture(); notifyEvent = new AutoResetEvent(false); bitsPerSample = (short)dr; samplesPerSecond = (int)freq; intervalSize = (int)(interval * samplesPerSecond * bitsPerSample / 8); samplesPerInterval = intervalSize * 8 / bitsPerSample; bufferSize = intervalSize * 2; CaptureBufferDescription desc = new CaptureBufferDescription(); WaveFormat format = new WaveFormat(); format.BitsPerSample = bitsPerSample; format.SamplesPerSecond = samplesPerSecond; format.Channels = 1; format.AverageBytesPerSecond = bitsPerSample * samplesPerSecond / 8; format.BlockAlign = (short)(bitsPerSample / 8); format.FormatTag = WaveFormatTag.Pcm; desc.Format = format; desc.BufferBytes = bufferSize; buffer = new CaptureBuffer(desc, device); notify = new Notify(buffer); notifyPositions = new BufferPositionNotify[2]; notifyPositions[0] = new BufferPositionNotify(); notifyPositions[1] = new BufferPositionNotify(); notifyPositions[0].EventNotifyHandle = notifyEvent.Handle; notifyPositions[0].Offset = intervalSize - (bitsPerSample / 8); notifyPositions[1].EventNotifyHandle = notifyEvent.Handle; notifyPositions[1].Offset = bufferSize - (bitsPerSample / 8); notify.SetNotificationPositions(notifyPositions); thread.Start(); } public void Start() { buffer.Start(true); } public void Stop() { buffer.Stop(); } public double LastValue { get { return lastValue; } } public void Dispose() { Stop(); disposed = true; notifyEvent.Set(); notify.Dispose(); buffer.Dispose(); device.Dispose(); lastValue = 0; } private void ThreadProc() { MemoryStream ms = new MemoryStream(intervalSize); while (!disposed) { bool firstLoop = true; notifyEvent.WaitOne(); if (!disposed) { int currentPos; int readPos; buffer.GetCurrentPosition(out currentPos, out readPos); ms.Position = 0; if (readPos > intervalSize) { buffer.Read(0, ms, intervalSize, LockFlag.None); firstLoop = false; } else if (!firstLoop) { buffer.Read(intervalSize, ms, intervalSize, LockFlag.None); } if (!firstLoop) { ms.Position = 0; lastValue = TimeAverage(ms); } } } } private double TimeAverage(Stream data) { double total = 0; while (data.Position < intervalSize) { double sample = 0; for (int i = 0; i < bitsPerSample / 8; ++i) { sample += ((int)data.ReadByte()) << (i * 8); } if (sample > (1 << (bitsPerSample - 1))) { sample -= (1 << bitsPerSample); } total += sample * sample; } double rms = Math.Sqrt(total / (double)samplesPerInterval); return rms / (1 << (bitsPerSample - 1)); } } }