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 provided4,142
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
13 projects β€’ 134 followers
Sr. Software Engineer at Independent

Comments