Carey Payette
Published © GPL3+

Windows IoT Core : Sensing Sound Levels

The article shows you how to sense the sound levels in an environment and display them visually in a Universal Windows Application on the Pi

IntermediateFull instructions provided3,487
Windows IoT Core : Sensing Sound Levels

Things used in this project

Hardware components

Adafruit Electret Microphone Amplifier – MAX9814 with Auto Gain Control
×1
MCP3008 – 8 Channel 10-Bit ADC with SPI Interface
×1
Adafruit Pi Cobbler Plus
×1
Adafruit Rapberry Pi 2 with Windows IoT Core
×1
Raspberry Pi 2 Model B
Raspberry Pi 2 Model B
×1

Software apps and online services

Windows 10 IoT Core
Microsoft Windows 10 IoT Core

Story

Read more

Schematics

MCP3008 Pin Out Diagram

MCP3008 Pin Out Diagram

Project Wiring Diagram

•Cobbler 3v3 pin to Breadboard Power Rail
•Cobbler GND pin to Breadboard Ground Rail
•MCP3008 VDD to Breadboard Power Rail
•MCP3008 VREF to Breadboard Power Rail
•MCP3008 AGND to Breadboard Ground Rail
•MCP3008 DGND to Breadboard Ground Rail
•MCP3008 CLK pin to Cobbler SCLK pin
•MCP3008 DOUT pin to Cobbler MISO pin
•MCP3008 DIN pin to Cobbler MOSI pin
•MCP3008 CS/SHDN pin to Cobbler CE0 pin
•Electret Mic GND pin to Breadboard Ground Rail
•Electret Mic VDD pin to Breadboard Power Rail
•Electret Mic GAIN pin to Breadboard Power Rail (not pictured)
•Electret OUT pin to MCP3008 CH0 pin

Code

MCP3008.cs Code Listing

C#
A class that simplifies communication with the MCP3008 chip, as well as provides a Sample method to sample readings over a specified timeframe, and the Map method that is a C# implementation of the Arduino method. (Code documented inline)
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Windows.Devices.Enumeration;
using Windows.Devices.Spi;

namespace PiSoundSensing
{
    /// <summary>
    /// Helper class that assists in reading from MCP3008 channels
    /// using SPI0, Mode0 and Standard Configuration
    /// assuming 3v3 
    /// </summary>
    public class MCP3008
    {
        private SpiDevice _mcp3008 = null;

        /// <summary>
        /// Connects to the MCP3008 on SPI0
        /// </summary>
        /// <returns>True if successful, False otherwise</returns>
        public async Task<bool> Connect()
        {
            var spiSettings = new SpiConnectionSettings(0);//for spi bus index 0
            spiSettings.ClockFrequency = 3600000; //3.6 MHz
            spiSettings.Mode = SpiMode.Mode0;

            string spiQuery = SpiDevice.GetDeviceSelector("SPI0");
            //using Windows.Devices.Enumeration;
            var deviceInfo = await DeviceInformation.FindAllAsync(spiQuery);
            if (deviceInfo != null && deviceInfo.Count > 0)
            {
                _mcp3008 = await SpiDevice.FromIdAsync(deviceInfo[0].Id, spiSettings);
                return true;
            }
            else
            {
                return false;
            }

        }

        /// <summary>
        /// Obtains a single sample reading on the specified channel
        /// </summary>
        /// <param name="channel">Channel to read</param>
        /// <returns>Analog Reading Value</returns>
        public int Read(byte channel)
        {
            //From data sheet -- 1 byte selector for channel 0 on the ADC
            //First Byte sends the Start bit for SPI
            //Second Byte is the Configuration Bit
            //1 - single ended 
            //0 - d2
            //0 - d1
            //0 - d0
            //             S321XXXX <-- single-ended channel selection configure bits
            // Channel 0 = 10000000 = 0x80 OR (8+channel) << 4
            int config = (8 + channel) << 4;
            var transmitBuffer = new byte[3] { 1, (byte)config, 0x00 };
            var receiveBuffer = new byte[3];

            _mcp3008.TransferFullDuplex(transmitBuffer, receiveBuffer);
            //first byte returned is 0 (00000000), 
            //second byte returned we are only interested in the last 2 bits 00000011 (mask of &3) 
            //then shift result 8 bits to make room for the data from the 3rd byte (makes 10 bits total)
            //third byte, need all bits, simply add it to the above result 
            var result = ((receiveBuffer[1] & 3) << 8) + receiveBuffer[2];
            return result;
        }

        /// <summary>
        /// Samples analog reads for a specified duration, on a specified channel and returns a value mapped based on the desired scale 
        /// </summary>
        /// <param name="milliseconds">Sample window duration in milliseconds</param>
        /// <param name="adcChannel">MCP3008 channel to perform the reads</param>
        /// <param name="scaleMax">Maximum value of the desired scale (minimum is by default 0)</param>
        /// <returns></returns>
        public async Task<int> Sample(int milliseconds, byte adcChannel, int scaleMax)
        {
            int retvalue = 0;
            await Task.Run(() => {
                var sw = Stopwatch.StartNew();
                var startMs = sw.ElapsedMilliseconds;
                int peakToPeak = 0;
                int readMin = 1024;
                int readMax = 0;

                while ((sw.ElapsedMilliseconds - startMs) < milliseconds)
                {
                    int sample = Read(adcChannel);
                    
                    if (sample > readMax)
                    {
                        readMax = sample;  // save just the max levels
                    }
                    else if (sample < readMin)
                    {
                        readMin = sample;  // save just the min levels
                    }
                }
                
                sw.Stop();
                peakToPeak = readMax - readMin;  // max - min = peak-peak amplitude
                //var volts = (peakToPeak * 3.3) / 1024;  // convert to volts
              
                //return result in the desired scale
                retvalue = Map(peakToPeak, 0, 1024, 0, scaleMax);
            });

            return retvalue;
        }

        /// <summary>
        /// Similar to the Arduino map function, this function adjusts the scale of the value returned
        /// </summary>
        /// <param name="value">value to map to a new scale</param>
        /// <param name="currentScaleLow">low value of the scale from which value was read</param>
        /// <param name="currentScaleHigh">high value of the scale form which value was read</param>
        /// <param name="targetScaleLow">low value of the target scale</param>
        /// <param name="targetScaleHigh">high value of the target scale</param>
        /// <returns>The equivalent of value on the target scale</returns>
        private int Map(int value, int currentScaleLow, int currentScaleHigh, int targetScaleLow, int targetScaleHigh)
        {
            decimal valueD = value;
            decimal currentScaleLowD = currentScaleLow;
            decimal currentScaleHighD = currentScaleHigh;
            decimal targetScaleLowD = targetScaleLow;
            decimal targetScaleHighD = targetScaleHigh;
            decimal result = (valueD - currentScaleLowD) / (currentScaleHighD - currentScaleLowD) * (targetScaleHighD - targetScaleLowD) + targetScaleLowD;

            int retvalue = Convert.ToInt32(Math.Ceiling(result));
            return retvalue;
        }
    }
}

MainPage.xaml Snippet

XML
Place this XAML in the Grid element of MainPage.xaml
        <StackPanel Orientation="Vertical" Margin="25,0">
            <TextBlock x:Name="txtStatus" Margin="0,25,0,0">Configuring ...</TextBlock>
            <StackPanel Orientation="Horizontal" Margin="0,25,0,0">
                <Button x:Name="btnStartSampling" Height="75" Width="75" Background="LightGreen" Margin="0,0,50,0" IsEnabled="False" Click="btnStartSampling_Click">Start</Button>
                <Button x:Name="btnEndSampling" Height="75" Width="75" Background="#FFFF505D" IsEnabled="False" Click="btnEndSampling_Click">End</Button>
            </StackPanel>
            <TextBlock Margin="0,25,0,0">Volume:</TextBlock>
            <ProgressBar x:Name="pbVolume" Background="#7F9AF1EF" Height="33" Foreground="#FF1F0C9D" />
        </StackPanel>

MainPage.xaml.cs Code Listing

C#
Full code listing of the UI implementation
using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace PiSoundSensing
{
    /// <summary>
    /// Sound Sensing Main User Interface
    /// </summary>
    public sealed partial class MainPage : Page
    {
        //an instance of MCP3008 used to assist in communication with the chip
        private MCP3008 _adc = new MCP3008();
        //a timer that will be used to sample audio in the current environment
        private DispatcherTimer _timer = new DispatcherTimer();
     
        public MainPage()
        {
            this.InitializeComponent();
            
            Setup();
        }

        /// <summary>
        /// Initializes the connection to the MCP3008 chip
        /// </summary>
        private async void Setup()
        {
            //connect to the ADC
            bool isConnected = await _adc.Connect();
            if (!isConnected)
            {
                txtStatus.Text = "There was a problem connecting to the MCP3008, please check your wiring and try again.";
            }
            else
            {
                btnStartSampling.IsEnabled = true;
                btnEndSampling.IsEnabled = true;
                txtStatus.Text = "";

                //retrieve sample from microphone every 100ms
                _timer.Interval = TimeSpan.FromMilliseconds(100);
                _timer.Tick += timer_Tick;

                //set scale of the progress bar, on the scale of 0 to 100: a percentage
                pbVolume.Minimum = 0;
                pbVolume.Maximum = 100;
            }
        }

        /// <summary>
        /// When actively sampling audio, this method will be called every 100ms and sample audio through
        /// the MCP3008 chip from the Electret Microphone Amplifier, retrieve it on a scale of 0-100 
        /// and display the value visually in the Progress Bar
        /// </summary>
        /// <param name="sender">ignore</param>
        /// <param name="e">ignore</param>
        private async void timer_Tick(object sender, object e)
        {
            //retrieve volume in the scale of 0 to 100 
            int volume = await _adc.Sample(50, 0, 100);
            pbVolume.Value = volume;
        }

        /// <summary>
        /// Begins the timer to sample audio every 100ms
        /// </summary>
        /// <param name="sender">Start Button</param>
        /// <param name="e">ignore</param>
        private void btnStartSampling_Click(object sender, RoutedEventArgs e)
        {
            _timer.Start();
        }

        /// <summary>
        /// Stops the timer to suspend sampling audio
        /// </summary>
        /// <param name="sender">Stop Button</param>
        /// <param name="e">ignore</param>
        private void btnEndSampling_Click(object sender, RoutedEventArgs e)
        {
            _timer.Stop();
        }
    }
}

GitHub Link

Full Source Code

Credits

Carey Payette

Carey Payette

13 projects • 131 followers
Sr. Software Engineer at Independent

Comments