用户工具

站点工具


knowledge:electronic:2019032101

Arduino sound level meter and spectrum analyzer

Recently I have been doing some projects with the Arduino electronics platform. One of the projects involved benchmarking certain motors and required me to measure noise levels. I will cover that project in more detail in one of the future articles, but now I would like to write about the process and best practices of measuring sound levels and analyzing frequencies with an Arduino. I will talk about sound, microphones, sampling, FFT and more. This article is going to be targeted towards beginners, who are neither signal processing experts not electronics experts and it will be fairly high-level with links for more thorough reading.

Sound theory

Sound is a wave that moves in space and when it is stored (in digital or analog form) it is represented by a Waveform, which is the amplitude of the wave measured at each point in time at a certain point in space. You can think of this as sound passing through a microphone where it is being measured constantly and the measurements form the waveform. Because we can only measure a finite number of times per time unit, this process of measuring is called sampling and it generates a discrete signal. Furthermore, each sample in time is also made discrete during this process as computers and integrated circuits have finite accuracy and storage.

Sampling of a waveform

Arduino capability for measuring signals and converting them to logic that the micro-controller (MCU) can process is provided by the Analog-to-Digital-Converter (ADC) that is pat of the MCU. Therefore, for an Arduino implementation, this process translates to connecting a measuring device (microphone for sound) to the MCU and sampling the value at the device by the ADC at a constant rate. The Arduino ADC senses voltage levels (typically in the range of 0-5V) and converts them to values in the range of 0 to 1024 (10 bit).

Depending on what we are measuring, sound levels may be very quiet or very loud. On one hand, a microphone on its own is usually not able to provide enough voltage for the Arduino to sense a change. The ADC needs a 5V/1024=4.8mV change to increase the digital value by 1, but a typical “electret microphone” might not provide this amount of voltage change for quiet sounds. For this reason a microphone is often used with an amplifier. On the other hand, a very loud noise and a high-gain amplifier can bring a signal to the max 5V, “over-exposing” or “clipping” it and again bringing us to a situation where the sampling is useless. Therefore it is important to match the device and the amplification level (gain) to each use-case scenario.

Microphone selection

When choosing a microphone for your Arduino you can get one of the available “microphone modules” that combine a microphone with an amplifier or some other logic on a tiny PCB. You can also make your own module, which has the added advantages of being able to control all the different aspects of the mic and amplification. I chose to get a module that is available of-the-shelf because it is easier and faster than making your own.

If your goal is to record sound and get a fixed sound level even in unpredictable situations you will want to get a module with automatic gain like this one based on MAX9814 from Adafruit. Such a module will “normalize” the sound to a set level. This will be the right solution for a scenario where you want to record voice for playback or to run a frequency analysis. Naturally, this isn’t the right choice for measuring sound volume. To measure sound volume and to be able to compare different measurements one to another you need to use a module where the gain is predictable. It doesn’t mean that the gain is fixed, it just means that the gain is configurable by you and does not change automatically.

I have evaluated 3 such modules. It is worth noting that a specific design might be on the market under different names as different manufactures make their own versions of the design with their own model numbers. Look at the board layout and note the main chip so you can identify the device.

MAX4466 based module

I got mine from the Far East but it looks like it is based on an Adafuit design. This module has an adjustable gain that you control with a tiny 1-turn potentiometer. There is a Vcc pin, a ground pin and an analog out pin. The analog pin emits a waveform where “0” is Vcc/2 and the amplitude depends on the gain and the volume of the sound. The MAX4466 chip is an op amp that is specifically optimized for use as a microphone amplifier, which makes this module a great performer and my eventual choice for the project.

“HXJ-17” / “Keyes” module based on an LM393

I got this module from a local electronics store. Not sure who designed it, but it has a multi-turn potentiometer, no amplifier and a LM393 comparator chip. There is a Vcc pin, a ground pin, an analog out pin and a digital out pin. Since this module lacks an amplifier, it is only good for sensing loud sounds, such as claps and knocks. The presence of the LM393 allows you to configure a threshold so the board can generate a digital output when the sound level is above the threshold. The only advantage that I can think of that this would have over implementing a threshold in code is that either 1) the comparator is more sensitive that the ADC of the MCU or 2) you don’t have an MCU in the first place and are wiring this board directly to a relay or a similar IC. Some sellers advertise this module as having an LM393 amplifier, but detailed analysis shows that it is not the case and that the analog out pin is not amplified.

“Sound detection” module based on an LM393

I got this one from the same local shop. This one is similar to the HXJ-17, but it is even simpler. It has a one turn potentiometer and no analog output. Leaving this useful for knowing if there is or isn’t a loud sound.

Analyzing analog input

As the first step, I would suggest you take some time to analyze the analog output of your module to see the baseline and amplitude. I have used the following Arduino function to gather data:

#define MicSamples (1024*2)
#define MicPin A0
 
// measure basic properties of the input signal
// determine if analog or digital, determine range and average.
void MeasureAnalog()
{
    long signalAvg = 0, signalMax = 0, signalMin = 1024, t0 = millis();
    for (int i = 0; i < MicSamples; i++)
    {
        int k = analogRead(MicPin);
        signalMin = min(signalMin, k);
        signalMax = max(signalMax, k);
        signalAvg += k;
    }
    signalAvg /= MicSamples;
 
    // print
    Serial.print("Time: " + String(millis() - t0));
    Serial.print(" Min: " + String(signalMin));
    Serial.print(" Max: " + String(signalMax));
    Serial.print(" Avg: " + String(signalAvg));
    Serial.print(" Span: " + String(signalMax - signalMin));
    Serial.print(", " + String(signalMax - signalAvg));
    Serial.print(", " + String(signalAvg - signalMin));
    Serial.println("");
}

You can then make some sounds at different volume levels and see how your average, min, max and span values respond. Looking at the result you might see that you need to adjust the gain potentiometer such that you utilize the max span for your sound levels while not overdoing it so not to clip your signal.

Implementing accurate sampling with 3.3V reference and free running

The analogRead function of Arduino makes it simple to get a digital value of an analog pin. It was implemented with single sample collection in mind. When sampling sound it is important to take our samples at a constant rate and to take each sample accurately. To achieve these two properties we will change a couple of things.

First, we will configure the ADC to use 3.3V as the analog reference voltage. The reason for this is that the 3.3V is usually more stable than the 5V. The 5V can fluctuate up and down especially when the Arduino is getting its power from the USB connection. The 3.3V is coming from a linear regulator on the Arduino board and can be connected to the ARef pin of the Arduino. This calibrates our ADC to map the 0 to 3.3V range of the analog input to the 0 to 1024 range of the digital values. For this to happen on the electronics level you need to feed your module with 3.3V and to connect the Arduino ARef pin to 3.3V. Make sure that your module is capable of operating at this voltage.

Use the following code to configure this mode:

analogReference(EXTERNAL); // 3.3V to AREF

Second, we will configure the ADC to work in “free-running” mode and read the sample values directly from internal registers, bypassing analogRead. As mentioned, analogRead is designed to read one value at a time and will perform initialization of the ADC for each read, something that we better eliminate. This will allow us to get a sampling rate that is more predictable.

Setup “free-running” mode with this code:

// register explanation: http://maxembedded.com/2011/06/the-adc-of-the-avr/
// 7 =&gt; switch to divider=128, default 9.6khz sampling
ADCSRA = 0xe0+7; // "ADC Enable", "ADC Start Conversion", "ADC Auto Trigger Enable" and divider.
ADMUX = 0x0; // Use adc0 (hardcoded, doesn't use MicPin). Use ARef pin for analog reference (same as analogReference(EXTERNAL)).
#ifndef Use3.3
ADMUX |= 0x40; // Use Vcc for analog reference.
#endif
DIDR0 = 0x01; // turn off the digital input for adc0

Read a batch of samples with this code:

for (int i = 0; i < MicSamples; i++)
{
    while (!(ADCSRA & /*0x10*/_BV(ADIF))); // wait for adc to be ready (ADIF)
    sbi(ADCSRA, ADIF); // restart adc
    byte m = ADCL; // fetch adc data
    byte j = ADCH;
    int k = ((int)j << 8) | m; // form into an int
    // work with k
}

Third, you can also adjust the speed of the ADC. By default the ADC is running at 1:128 of the MCU speed (mode #7). Each sample takes the ADC about 13 clock cycles to get processed. So by default we get 16Mhz/128/13=9846Hz sampling. If we want to sample at double the rate we can change the divider to be 64 instead.

Here is an example of how to set divider to 32 (mode #5) which equals a sampling rate of 16Mhz/32/13~=38Khz:

// macros
// http://yaab-arduino.blogspot.co.il/2015/02/fast-sampling-from-analog-input.html
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &amp;= ~_BV(bit))
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
 
// 1 0 1 = mode 5 = divider 32 = 38.4Khz
sbi(ADCSRA, ADPS2);
cbi(ADCSRA, ADPS1);
sbi(ADCSRA, ADPS0);

You can see all three of these code snippets implemented together in the source code of the project at github.

With this logic in place we can get decent waveform data for the Arduino to process.

Sound level measurement

Theory

The sound level is defined as the amplitude of the waveform and can be measured per set of samples which represent a part of a signal.

Peak Envelopes

For an ideal sine signal, the amplitude would be the max sample, but in practice it is easy for some samples to be outliers and to affect the max value significantly. For this reason it is more practical to use a metric that takes all the sample values into account. You can use an average, but it is more common to use Root Mean Square (RMS) which will give more “weight” to higher values.

The relationship between amplitude and RMS for sine waves is known and is amplitude= sqrt(2)*RMS. If we assume that a sound waveform is similar to a sine waveform we can use this relationship to estimate a stable amplitude based on an RMS value that we calculate.

Waveform with an RMS envelope

The values we are dealing with are relative and not absolute. After all we are using some value of gain to tune the volume levels to our digital 10bit range. It is very common to work with relative values when processing sound waveforms. The volume is then measured as a ratio from some other “reference point” value. A common unit used to express the ratio is decibel (dB). Resulting a formula:

dB=10*log10(v/v0)

Where dB is the level is dB units, v is the sample value and v0 is the reference value.

Since sound pressure is a field quantity a ratio of squares is used and the value “2” in the log becomes “20” [due to log(a^b)=b*log(a)] :

dB=20*log10(v/v0)

I was trying to achieve relative measurements, I chose v0 as the max possible amplitude (1024/2 for a 10bit ADC). This yields dB measurements which are specific to a certain gain and my device, but as long as the gain remains fixed, I can take several measurements and make valid comparisons between them. If you are looking to measure absolute sound levels you would need to calculate your level relative to a standard agreed sound pressure baseline value of 20 micropascals, which is the typical threshold of perception of an average human. Practically, this is typically done by attaching a special calibration device to the microphone. The device generates sound at a fixed dB level and you can adjust your calculations such that your dB measurement matches the dB value of the calibration device.

When using a reference value that is higher than your samples (max amplitude), your dB values would be negative and smaller as you approach the max.

When using a reference value that is lower than your samples (threshold of perception), your dB values would be positive and larger as you approach the max.

To make this even more complex, several factors affect measurement in practice. First, the human ear is not equally sensitive to all frequencies. It is typical to apply different weights to different frequency ranges. One such a unit of measurement is called dBA, but there are others with slightly different weights. Second, your microphone might not have equal sensitivity to all frequencies. Third, your speakers might not have equal ability to reproduce all frequencies at the same exact level. These complexities require very accurate and expensive equipment together with special calibration procedures to be able to measure sound levels correctly per standards. You need to understand that your ability to measure sound level with the setup described here is pretty rudimentary and suitable for rough relative measurements only.

Implementation

Let’s recap that our values are 0 to 1024 which stand for [-max,max] with 1024/2=512 being “0”. We will retrieve and process sample for some time, where the standard defines 1 second as “Slow” and 125ms as “Fast”. For each sample, we will measure the distance from the “0” to the sample value, which is the amplitude of that sample. Then we can do simple calculations for max, average and RMS. The values on our scale can be “normalized” to percentage of max amplitude or using dB or both. Here is a relevant code sample:

// consts
#define AmpMax (1024 / 2)
#define MicSamples (1024*2) // Three of these time-weightings have been internationally standardised, 'S' (1 s) originally called Slow, 'F' (125 ms) originally called Fast and 'I' (35 ms) originally called Impulse.
 
// modes
#define ADCFlow // read data from adc with free-run (not interupt). much better data, dc low. hardcoded for A0.
 
// calculate volume level of the signal and print to serial and LCD
void MeasureVolume()
{
    long soundVolAvg = 0, soundVolMax = 0, soundVolRMS = 0, t0 = millis();
    for (int i = 0; i < MicSamples; i++)
    {
#ifdef ADCFlow
        while (!(ADCSRA & /*0x10*/_BV(ADIF))); // wait for adc to be ready (ADIF)
        sbi(ADCSRA, ADIF); // restart adc
        byte m = ADCL; // fetch adc data
        byte j = ADCH;
        int k = ((int)j << 8) | m; // form into an int
#else
        int k = analogRead(MicPin);
#endif
        int amp = abs(k - AmpMax);
        amp <<= VolumeGainFactorBits;
        soundVolMax = max(soundVolMax, amp);
        soundVolAvg += amp;
        soundVolRMS += ((long)amp*amp);
    }
    soundVolAvg /= MicSamples;
    soundVolRMS /= MicSamples;
    float soundVolRMSflt = sqrt(soundVolRMS);
    float dB = 20.0*log10(soundVolRMSflt/AmpMax);
 
    // convert from 0 to 100
    soundVolAvg = 100 * soundVolAvg / AmpMax; 
    soundVolMax = 100 * soundVolMax / AmpMax; 
    soundVolRMSflt = 100 * soundVolRMSflt / AmpMax;
    soundVolRMS = 10 * soundVolRMSflt / 7; // RMS to estimate peak (RMS is 0.7 of the peak in sin)
 
    // print
    Serial.print("Time: " + String(millis() - t0));
    Serial.print(" Amp: Max: " + String(soundVolMax));
    Serial.print("% Avg: " + String(soundVolAvg));
    Serial.print("% RMS: " + String(soundVolRMS));
    Serial.println("% dB: " + String(dB,3));
}

So now with proper module and calibration you can measure sound level of different events or devices and compare them one to the other.

Frequency analysis with FHT

What if you want to “break” the sound into individual frequencies and measure or visualize each individual frequency? Can this be done with Arduino? The answer is that it can be done relatively easily thanks to some existing libraries. To turn signals from a time domain to a frequency domain you would generally use a Fourier transform. Such transforms are used for signals of different types, sound, images, radio transmissions, etc. Each signal type has its own properties and the transform that best suits a sound signal is the Discrete Hartley Transform (DHT). DHT will work with discrete, real values which form our waveform. To implement DHT we will use Fast Hartley Transform (FHT) and specifically the ArduinoFHT library.

The Arduino FHT library works with vectors of 16 to 256 samples. This size is denoted as N. In this project I will be using N=256 to achieve maximum resolution, but you may use smaller values if you are short on memory or processing power.

First, the algorithm takes N real numbers and results in N/2 complex numbers. Then we can pass the data to another function to calculate the magnitude of the complex numbers to get N/2 bins. In the end we get N/2 bins, each covering a frequency range of sampling_rate/N Hz. The highest value of the last bin will be sampling_rate/2 . The reasons for this relate to signal processing theory, specifically aliasing and Nyquist law. In practice, if you want to avoid any strange effects, such as higher frequencies “folding” over lower frequencies, you will need to make sure to use a sampling rate that is twice the highest frequency you expect to have in the sound signal. Otherwise you are not sampling fast enough. You should also not over sample, as it will result in low ADC accuracy and wasting of FHT bins on ranges that don’t appear in the signal. I found the value of 20Khz to be a good upper frequency based on the range of my microphone and on the range of typical human hearing. As a result the, sampling at 38.4Khz (divider=32) seemed optimal.

So for N=256 and sampling_rate=38.4Khz we get 128 150hz bins with the first been holding the magnitude value of 0-150hz and the last bin holding the magnitude value of 19050-19200hz. We can now focus on specific bins that interest us, send the values of all the bins over serial connection, store the values, display them in some way, etc.

One of the fun ways to use the data, especially when troubleshooting and developing is to visualize with an analyser. Load the following FHT example code to the Arduino or adapt it to your needs. It gets the samples, runs FHT on the data and sends it in binary form over serial. Your Arduino should be connected to a computer running Processing development environment. In Processing, load the “FHT 128 channel analyser” project. I had to make a change to the project to make it compatible with Processing 3.0 . To do so, move the call to “size” function from within the “setup” function to a new function called “settings”.

Analyzer

Another way to analyze the data is for the Arduino to send it over serial in textual form, let it run for some time, then copy it from the serial monitor and paste it in a spreadsheet. For example using a code that is similar to this:

void MeasureFHT()
{
    long t0 = micros();
    for (int i = 0; i < FHT_N; i++) { // save 256 samples
        while (!(ADCSRA & /*0x10*/_BV(ADIF))); // wait for adc to be ready (ADIF)
        sbi(ADCSRA, ADIF); // restart adc
        byte m = ADCL; // fetch adc data
        byte j = ADCH;
        int k = ((int)j << 8) | m; // form into an int
        k -= 0x0200; // form into a signed int
        k <<= 6; // form into a 16b signed int
        fht_input[i] = k; // put real data into bins
    }
    long dt = micros() - t0;
    fht_window(); // window the data for better frequency response
    fht_reorder(); // reorder the data before doing the fht
    fht_run(); // process the data in the fht
    fht_mag_log();
 
    // print as text
    for (int i = 0; i < FHT_N / 2; i++)
    {
        Serial.print(FreqOutData[i]);
        Serial.print(',');
    }
    long sample_rate = FHT_N * 1000000l / dt;
    Serial.print(dt);
    Serial.print(',');
    Serial.println(sample_rate);
}

Then you can format the data in a spreadsheet, such as Excel, as a “3-D Surface” mesh graph. For example, see a graph of a Frequency Sweep from 1hz to 5000hz as captured and analyzed by the Arduino and FHT:

Mesh of FHT frequency sweep

Summary

My code for this project can be found at github for you to experiment with.

The Arduino can be used for relative sound level measurement and for frequency analysis/visualization. One just needs a microphone to match the use case, an Arduino, some coding and optionally the FHT library. Have fun and let me know in the comments if you make something nice using such a setup.

https://blog.yavilevich.com/2016/08/arduino-sound-level-meter-and-spectrum-analyzer/

knowledge/electronic/2019032101.txt · 最后更改: 2019/03/21 12:51 由 弘毅