Siddhant Kumar
Project 041 · Environment

Noise Pollution Mapper.

Measures sound the way regulations and human hearing do — A-weighted, time-averaged levels — to build an honest city-wide noise map without recording anyone.

Intermediate 12–18 hours 34 min read AudioSensorsMapping
Jump to source Bill of materials
Noise Pollution Mapper — reference build illustration MCU VCC · GND · SIG · NC
Difficulty
Intermediate
Build time
12–18 hours
Indicative cost
₹2,600 – ₹3,800
Platform
ESP32 DevKit V1 (ESP-WROOM-32)
Category
Environment
Last updated
28 July 2026
Contents — 26 sections

Project Overview

Measures sound the way regulations and human hearing do — A-weighted, time-averaged levels — to build an honest city-wide noise map without recording anyone.

Noise is a genuine pollutant — sustained exposure raises stress, disrupts sleep, and is linked to cardiovascular harm — yet it is one of the least-mapped. Complaints are subjective and sporadic, and official noise surveys are expensive and rare, so a city rarely knows which streets are actually too loud, when, and by how much. This project builds the sensing node for a distributed noise map: a device that measures sound levels the way noise regulations and the human ear do, reports them continuously, and does so without recording audio, so it maps loudness without surveilling conversations.

Measuring sound "properly" means two things that a naive peak-detector gets wrong. First, human hearing is not equally sensitive across frequencies — we hear mid frequencies far better than very low or very high ones — so meters apply A-weighting, a filter that discounts the frequencies our ears discount, giving dB(A), the unit used in virtually every noise regulation. Second, a single instant tells you nothing about a fluctuating quantity like traffic noise, so meters report time-averaged and statistical measures: the equivalent continuous level Leq (the steady level carrying the same energy as the fluctuating real sound over a period) and percentile levels like L90 (the background) and L10 (the intrusive peaks). This node computes those, because they are what make a reading comparable to a limit and to other nodes.

Privacy is a design constraint, not an afterthought. The node computes sound-pressure levels on-device from short analysis windows and transmits only the numbers — Leq, L10, L90, Lmax per interval — never audio, so it can sit in public space without recording what people say. It runs on mains or solar, reports over Wi-Fi or LoRa, and feeds a heat map that shows how loudness varies by street and by hour, revealing the difference between a road that roars at rush hour and a nightlife strip that peaks at midnight. It is honest about being a class-of-instrument below a calibrated sound-level meter — its microphone is not laboratory-grade — but with a reference calibration it produces maps a city can actually use to target the streets that most need quieting.

A city skyline at night
A dense network of nodes maps how loud each street is, and when — data a city rarely has otherwise. Photograph sourced from Wikimedia Commons — Smart city.jpg. Reused under the licence stated on that page; please check it before republishing.

What this project does

  • Measures A-weighted sound-pressure level (dB(A)) the way regulations define it
  • Computes time-averaged Leq and statistical Lmax, L10 and L90 per interval
  • Processes audio on-device and transmits only levels — never recordings
  • Builds a time-and-place noise map from many nodes
  • Runs on mains or solar and reports over Wi-Fi or LoRa
  • Calibrates against a reference sound-level meter for comparable readings
  • Flags when levels exceed day/night regulatory limits

Real-World Applications

SettingHow it is used
City noise mapping and planningRevealing which streets exceed limits and when, so authorities target enforcement, traffic calming or barriers where they matter.
Nightlife / entertainment districtsMonitoring venue and street noise against night limits to balance a lively economy with residents' sleep.
Construction and industry complianceContinuous boundary-noise monitoring against consent limits, with an evidence trail and exceedance alerts.
Community and school environmentsQuantifying noise exposure around homes, hospitals and schools to support quiet-zone advocacy.

Deployment contexts where a build of this kind earns its keep.

Features & Capabilities

  • A-weighting and Leq — the units and averages noise rules actually use
  • Statistical levels (L10/L90) that separate peaks from background
  • Privacy by design: levels transmitted, audio never leaves the device
  • Diurnal noise mapping (rush-hour road vs midnight nightlife)
  • Reference calibration for comparability across the network
  • Day/night limit flagging
  • Cheap enough to deploy as a dense map, not a single meter

Difficulty, Time & Required Skills

AttributeValue
Difficulty levelIntermediate
Estimated completion time12–18 hours
Indicative build cost₹2,600 – ₹3,800
Primary disciplineEnvironment
Reference platformESP32 DevKit V1 (ESP-WROOM-32)

Skills you should have (or will pick up)

  • Reading an I²S MEMS microphone and computing RMS sound levels
  • Implementing A-weighting and time-averaged Leq / percentile levels
  • Calibrating a level meter against a reference dB(A) source
  • Designing for privacy (on-device processing, no audio transmission)
  • Wi-Fi/LoRa reporting and heat-map data feeds

Bill of Materials

Every part below is commonly available from Indian and international hobby-electronics suppliers. Prices are indicative 2026 retail figures in Indian rupees and will drift — treat them as a budgeting guide, not a quotation.

ComponentKey specificationQtyApprox. cost
ESP32 DevKit V1 (ESP-WROOM-32)
Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly.
Dual-core Xtensa LX6 @ 240 MHz, 520 KB SRAM, 4 MB flash, Wi-Fi 802.11 b/g/n + BLE 4.2, 34 GPIO, 18× 12-bit ADC, 2× 8-bit DAC1₹450
INMP441 I²S MEMS microphone
Digital output means no analogue noise pickup — far better than an MAX9814 for keyword spotting.
61 dB SNR, −26 dBFS sensitivity, 60 Hz–15 kHz, 24-bit I²S output1₹220
0.96″ SSD1306 OLED display
Static images burn in — invert or scroll the screen periodically.
128 × 64 monochrome, 1.3–3.3 V logic, 100 kHz–400 kHz I²C1₹250
SX1278 LoRa 433 MHz module (Ra-02)
Never power the radio without an antenna — the PA will destroy itself.
−148 dBm sensitivity, +20 dBm output, up to 10 km line of sight, SF7–SF121₹480
20 W 12 V polycrystalline solar panel
Rated watts assume 1000 W/m² — plan for 60–70 % of nameplate in real installs.
Vmp 17.5 V, Imp 1.14 A, Voc 21.6 V, 350 × 290 mm, aluminium frame1₹1,200
TP4056 Li-ion charger + DW01 protection
Buy the version *with* protection ICs — the bare charger will over-discharge your cell.
1 A programmable CC/CV charge to 4.2 V ±1 %, over-discharge and short protection1₹45
18650 Li-ion cell 3400 mAh + holder
Never charge below 0 °C; always use a protected cell or a BMS.
3.7 V nominal, 4.2 V full, 3400 mAh, ~12.6 Wh, 2 C discharge1₹450
Weatherproof mic windshield
Wind noise ruins outdoor SPL without a windscreen
Foam/mesh windscreen + rain hood over the MEMS port, IP-rated housing for electronics1₹250
Reference calibration access
Not hardware — essential for comparable dB(A)
A calibrated sound-level meter or acoustic calibrator to set the node's offset1
Pole/wall mountPositions the mic at a standard height, clear of reflecting surfaces1₹200

Estimated total: ₹3,545, excluding tools, shipping and consumables.

Tools and consumables

  • Soldering iron (temperature controlled, 350 °C) with 0.8 mm 60/40 or lead-free solder
  • Digital multimeter — continuity, DC volts and current ranges
  • Wire strippers, flush cutters and a small set of precision screwdrivers
  • Heat-shrink tubing and a heat gun (or a lighter, carefully)
  • A laptop with a USB port and the toolchain listed above

Hardware Specifications

PartSpecificationSupplyInterfaceReference
ESP32 DevKit V1 (ESP-WROOM-32)Dual-core Xtensa LX6 @ 240 MHz, 520 KB SRAM, 4 MB flash, Wi-Fi 802.11 b/g/n + BLE 4.2, 34 GPIO, 18× 12-bit ADC, 2× 8-bit DAC3.3 V logic / 5 V USBUART, SPI, I²C, I²S, CAN, PWMDatasheet
INMP441 I²S MEMS microphone61 dB SNR, −26 dBFS sensitivity, 60 Hz–15 kHz, 24-bit I²S output1.8–3.3 VI²SDatasheet
0.96″ SSD1306 OLED display128 × 64 monochrome, 1.3–3.3 V logic, 100 kHz–400 kHz I²C3.3–5 VI²C (0x3C)Datasheet
SX1278 LoRa 433 MHz module (Ra-02)−148 dBm sensitivity, +20 dBm output, up to 10 km line of sight, SF7–SF123.3 VSPIDatasheet
20 W 12 V polycrystalline solar panelVmp 17.5 V, Imp 1.14 A, Voc 21.6 V, 350 × 290 mm, aluminium frame12 V nominalMC4 / screw terminalsDatasheet
TP4056 Li-ion charger + DW01 protection1 A programmable CC/CV charge to 4.2 V ±1 %, over-discharge and short protection4.5–5.5 V inmicro-USB / padsDatasheet
18650 Li-ion cell 3400 mAh + holder3.7 V nominal, 4.2 V full, 3400 mAh, ~12.6 Wh, 2 C discharge3.0–4.2 VHolder / spot-welded tabsDatasheet

Consolidated electrical and interface specifications for every active part in the build.

Power Budget & Supply Sizing

Add up the typical active current of every part, then size the supply with at least 50 % headroom so transmit bursts and motor inrush never brown out the controller.

LoadSupply railTypical current (mA)Notes
ESP32 DevKit V1 (ESP-WROOM-32)3.3 V logic / 5 V USB160Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly.
INMP441 I²S MEMS microphone1.8–3.3 V1.4Digital output means no analogue noise pickup — far better than an MAX9814 for keyword spotting.
0.96″ SSD1306 OLED display3.3–5 V20Static images burn in — invert or scroll the screen periodically.
SX1278 LoRa 433 MHz module (Ra-02)3.3 V120Never power the radio without an antenna — the PA will destroy itself.
20 W 12 V polycrystalline solar panel12 V nominal1140Rated watts assume 1000 W/m² — plan for 60–70 % of nameplate in real installs.
TP4056 Li-ion charger + DW01 protection4.5–5.5 V in1000Buy the version *with* protection ICs — the bare charger will over-discharge your cell.

Summed typical draw is 2441.4 mA. With a 1.5× design margin the supply should deliver at least 3700 mA continuously at the stated rail voltage.

Software Requirements & Development Environment

Reference toolchain: Arduino IDE 2.3.x with the ESP32 board package 3.x (or PlatformIO on VS Code). Anything newer normally works; anything older may lack the board definitions used here.

  • Install the Arduino IDE 2.3.x (or PlatformIO if you prefer a real editor and dependency locking).
  • Add https://espressif.github.io/arduino-esp32/package_esp32_index.json under File → Preferences → Additional Board Manager URLs, then install esp32 from the Boards Manager.
  • Set the correct port under Tools → Port. On Linux add yourself to the dialout group: sudo usermod -aG dialout $USER and log out and back in.
  • Open the Serial Monitor at 115200 baud — every sketch here logs its state there.
  • Keep File → Preferences → Show verbose output during: compilation switched on while you are debugging build errors.

Required libraries

LibraryWhy it is neededInstall
WiFi (ESP32 core) bundledStation/AP connection management for the ESP32.Bundled with the ESP32 Arduino core
PubSubClient 2.8Lightweight MQTT 3.1.1 client for constrained devices.Library Manager → "PubSubClient" by Nick O'Leary
Adafruit SSD1306 + GFX 2.5.xFramebuffer and text/graphics primitives for the OLED.Library Manager → "Adafruit SSD1306"
ArduinoJson 7.xZero-allocation JSON serialisation and parsing.Library Manager → "ArduinoJson" by Benoit Blanchon
Preferences (NVS) bundledWear-levelled key/value storage in ESP32 flash for settings.Bundled with the ESP32 core
NTPClient / configTime bundledWall-clock time from an NTP server for timestamping.Bundled (`configTime()` on ESP32)

Block Diagram

The block diagram shows the functional decomposition of the system — what senses, what decides, what acts, and where the data ends up.

Noise Pollution Mapper — system block diagramFunctional block diagram of the Noise Pollution Mapper system. Hear (levels only)MEMS micINMP441 I²SOn-device DSPA-weight + RMSSummariseESP32Leq, L10, L90, LmaxCalibratereference offsetLinkWi-Fi/LoRanumbers onlyMapNoise mapstreet × hourAlertlimit exceededrightrightnone
Noise Pollution Mapper — system block diagram

Circuit Diagram & Wiring

Every signal line in the build is shown below, followed by a pin-by-pin connection table you can work through with a multimeter in hand.

Noise Pollution Mapper — wiring schematicConnection schematic showing which controller pin drives each peripheral. Sensors / InputsControllerActuators / OutputsESP32 DevKit V1(ESP-WROOM-32)3.3 V logic / 5 V USBINMP441 micGPIO 33/25/32I²S digital audioINMP441 micGNDChannel selectOLED (opt)GPIO 21/22Local dB(A) displayLoRa/Wi-FiSPI / on-chipLevel telemetry (noaudio)TP4056VIN / 3V3 regSolar/mains supplySolar/mainsTP4056 INCharge source
Noise Pollution Mapper — wiring schematic
PeripheralPeripheral pinController pinSignal
INMP441 micSD/WS/SCKGPIO 33/25/32I²S digital audio
INMP441 micL/RGNDChannel select
OLED (opt)SDA/SCLGPIO 21/22Local dB(A) display
LoRa/Wi-FibusSPI / on-chipLevel telemetry (no audio)
TP4056OUTVIN / 3V3 regSolar/mains supply
Solar/mains+/–TP4056 INCharge source

Wire one row at a time and tick it off — most "it does not work" reports trace back to a single swapped pair.

Wiring explanation

  • Wire the INMP441 as an I²S input (SD, WS, SCK) and tie its L/R pin to select a single channel; the ESP32 I²S peripheral streams samples with no CPU polling.
  • Mount the microphone port through a foam windscreen and rain hood — outdoor wind directly on the MEMS port produces large false low-frequency levels.
  • Position the mic at a standard measurement height on a pole or wall, away from large reflecting surfaces that would inflate levels.
  • Keep the mic away from the node's own switching supplies and any fan; you are measuring the environment, not the enclosure.
  • If solar, ensure the panel/charger noise does not couple into the mic supply.
An ESP32 development board with the ESP-WROOM-32 module and USB connector
ESP32 module computing A-weighted Leq and percentile levels on-device and transmitting only the numbers. Photograph sourced from Wikimedia Commons — ESP32 Espressif ESP-WROOM-32 Dev Board.jpg. Reused under the licence stated on that page; please check it before republishing.

System Architecture

Read the stack from the bottom up: physical hardware, the firmware that drives it, the transport that moves data off the device, and the software a human actually looks at.

Noise Pollution Mapper — architecture stackLayered architecture from hardware to user interface. Hardware layerESP32 DevKit V1 (ESP-WROOM-32) · sensors and actuatorsDriver layerwifi · pubsub · ssd1306 · arduinojsonApplication logicsampling loop · filtering · thresholds · state machineTransport layerWi-Fi/LoRa → gateway → noise heat map (levels only) · TLS · retry andbackoffPresentation layerdashboard · mobile notifications · historical charts
Noise Pollution Mapper — architecture stack

Working Principle

To be a noise measurement rather than a raw amplitude, the node must mimic the ear and the regulation. The ear's frequency response is uneven — most sensitive around 1–4 kHz, much less so at very low and very high frequencies — and A-weighting is the standard filter that shapes the measured spectrum to match, producing dB(A). A truck's low rumble and a whistle of the same physical energy are not equally loud to us, and dB(A) encodes that. Every noise limit in the world is written in dB(A), so applying A-weighting is what makes the node's number comparable to a limit at all.

Because environmental noise fluctuates constantly, a single instantaneous level is nearly meaningless; the meaningful quantities are time-integrated. The equivalent continuous level, Leq, is the steady dB(A) that would deliver the same total sound energy as the actual fluctuating sound over the measurement period — the honest "average loudness" of a noisy interval. Alongside it, statistical percentile levels describe the shape of the noise: L90 (the level exceeded 90% of the time) is effectively the background or residual level, while L10 (exceeded only 10% of the time) captures the intrusive peaks like passing vehicles. A street with a low L90 but a high L10 is quiet with sharp intrusions; one with a high L90 is relentlessly loud. These distinctions are exactly what a noise map needs to be actionable.

The computation is a pipeline the ESP32 can run in real time without ever storing audio. Samples stream from the I²S microphone; each short analysis window is A-weighted (a small digital filter) and reduced to a mean-square energy; those energies accumulate over the reporting interval to form Leq, while a running histogram of short-window levels yields the percentiles and the maximum. At the end of each interval the node emits a handful of numbers and discards the audio. Nothing that could reconstruct speech ever leaves the device — the privacy property is structural, a consequence of computing statistics and throwing the samples away, not a policy layered on top.

Finally, comparability requires calibration. A MEMS microphone has a sensitivity and the whole chain has a gain, so the raw RMS must be mapped to true dB(A) by comparing the node against a reference sound-level meter or acoustic calibrator and storing the offset. With that offset the node produces readings that line up with a proper meter and with its neighbours, turning a field of cheap nodes into a coherent map. The node is candid that it is a class below laboratory metrology — the microphone is not certified — but calibrated and A-weighted, it is more than good enough to show which streets are too loud, and when, which is the entire point of a noise map.

The maths behind it

Sound-pressure level from RMS

plainSound-pressure level from RMS
For an analysis window, from A-weighted samples x[n]:

  rms = sqrt( (1/N) Σ x[n]^2 )
  SPL_dBA = 20·log10(rms) + CAL_OFFSET

CAL_OFFSET (from reference calibration) maps the mic/ADC
chain to true dB(A). A-weighting is applied to x[n] before
the RMS so the level matches the ear and the regulation.

Equivalent continuous level (Leq)

plainEquivalent continuous level (Leq)
Leq is the energy-average over the interval T:

  Leq = 10·log10( (1/M) Σ 10^(L_i/10) )

where L_i are the short-window SPL_dBA values (M of them
in T). Averaging in the ENERGY domain (10^(L/10)), not the
dB domain, is essential — a few loud windows dominate,
just as they dominate real exposure.

Percentile levels (L10, L90)

plainPercentile levels (L10, L90)
Build a histogram of the short-window L_i over the interval.

  L90 = level exceeded 90% of the time  → background/residual
  L10 = level exceeded 10% of the time  → intrusive peaks
  Lmax = maximum short-window level

High L10 with low L90 = quiet with sharp intrusions;
high L90 = persistently loud. Both matter for a map.

Program Flowchart

The firmware is a single cooperative loop. Nothing blocks for long, so networking, sensing and the user interface all stay responsive.

Noise Pollution Mapper — firmware flowchartControl flow through the main program loop. Stream I²S audio windowA-weight, compute RMS/SPLAccumulate energy +percentilesInterval elapsed?Compute Leq/L10/L90/LmaxStream I²S audio windowCompute Leq/L10/L90/LmaxAbove day/night limit?Flag exceedanceTransmit levelsFlag exceedanceTransmit levels
Noise Pollution Mapper — firmware flowchart

Assembly Instructions

Build on a breadboard first and only commit to solder once the whole system has run for an hour without a fault.

  1. Mount the microphone correctly

    Fit the INMP441 behind a foam windscreen and rain hood, port facing the environment, at a standard measurement height on a pole or wall and clear of large reflecting surfaces.

    Keep the mic away from the node's own supplies and any moving parts so it measures the street, not the box.

  2. Wire and stream the I²S mic

    Connect SD/WS/SCK to the I²S pins and select a single channel. Configure the I²S peripheral to stream continuously so the CPU only processes buffers, never polls.

  3. Set up display, power and link

    Optionally add an OLED to show the live dB(A) locally. Power from mains or solar, and configure Wi-Fi or LoRa for level-only telemetry.

Step-by-Step Implementation Guide

Work through these in order. Each step ends in something you can observe, so a failure is always localised to the step you just finished.

  1. Build the level pipeline

    For each I²S buffer, apply the A-weighting filter, compute the window RMS and SPL, accumulate the energy for Leq, and update a level histogram for the percentiles and maximum.

    cppspl-pipeline.ino
    // Accumulators for one reporting interval.
    double  energySum = 0;    // Σ 10^(L/10) for Leq
    uint32_t windows  = 0;
    uint16_t hist[140] = {0}; // 1 dB bins, 0..139 dB(A)
    float    lmax = 0;
    float    CAL_OFFSET;      // from reference calibration
    
    // A-weighting as a cascade of biquads (coeffs precomputed for fs).
    float aWeight(float x) { /* IIR biquad cascade */ return applyBiquads(x); }
    
    // Process one window of N A-weighted samples → SPL, and accumulate.
    void processWindow(const int32_t *buf, int N) {
      double ss = 0;
      for (int i = 0; i < N; i++) {
        float x = aWeight((float)(buf[i] >> 8));   // 24-bit sample, A-weighted
        ss += (double)x * x;
      }
      float rms = sqrt(ss / N);
      float spl = 20.0f * log10f(rms + 1e-9f) + CAL_OFFSET;
    
      energySum += pow(10.0, spl / 10.0);          // energy domain for Leq
      windows++;
      int bin = constrain((int)lroundf(spl), 0, 139);
      hist[bin]++;
      if (spl > lmax) lmax = spl;
    }
    
    // At interval end: derive Leq and percentiles from the accumulators.
    void intervalStats(float &leq, float &l10, float &l90) {
      leq = 10.0f * log10f(energySum / windows);
      uint32_t c = 0, t = windows;
      l90 = l10 = 0;
      for (int b = 139; b >= 0; b--) {             // high → low
        c += hist[b];
        if (!l10 && c >= t * 0.10f) l10 = b;        // exceeded 10% of time
        if (!l90 && c >= t * 0.90f) { l90 = b; break; } // exceeded 90%
      }
    }
    float aWeight(float x)Applies the standard A-weighting filter to each sample so the level reflects how loud humans actually perceive that frequency content — the step that turns raw amplitude into dB(A).
    energySum += pow(10.0, spl / 10.0)Leq is accumulated in the energy domain, not by averaging decibels, so a few loud windows dominate the interval average exactly as they dominate real exposure.
    hist[bin]++A running 1 dB histogram of window levels lets the percentile levels be read out at the end without storing the whole time series.
    if (!l10 && c >= t * 0.10f)Walking the histogram from loud to quiet finds the level exceeded 10% of the time (the intrusive peaks) and 90% of the time (the background) — L10 and L90.
    float CAL_OFFSETThe single calibration constant that maps this microphone-and-ADC chain to true dB(A), set once against a reference so the node agrees with a real meter and with its neighbours.
  2. Report levels and flag exceedances

    At the end of each interval, transmit Leq, L10, L90 and Lmax (numbers only), compare Leq to the applicable day/night limit, flag exceedances, and reset the accumulators.

Complete Source Code

The listing below is complete and compiles as written — there are no elided sections. Read the annotations under each block before you upload it.

cppnoise-mapper.ino
/* ═══════════════════════════════════════════════════════════════
   Noise Pollution Mapper — ESP32 + INMP441 I2S mic

   Measures A-weighted SPL, computes Leq / L10 / L90 / Lmax per
   interval entirely on-device, and transmits ONLY the levels — never
   audio. Feeds a city noise heat map. Calibrated to a reference dB(A).
   ══════════════════════════════════════════════════════════════════ */

#include <WiFi.h>
#include <PubSubClient.h>
#include <driver/i2s.h>
#include <Preferences.h>
#include <math.h>

#define I2S_SD    33
#define I2S_WS    25
#define I2S_SCK   32
#define FS        16000       // sample rate
#define WIN       1600        // 100 ms windows
#define INTERVAL_MS 60000UL   // 1-minute reporting interval

Preferences  prefs;
WiFiClient   net;
PubSubClient mqtt(net);

float    CAL_OFFSET;
double   energySum = 0; uint32_t windows = 0;
uint16_t hist[140]; float lmax = 0;
uint32_t intervalStart = 0;

/* A-weighting IIR (biquad coeffs precomputed for FS = 16 kHz). */
float applyBiquads(float x) {
  static float z1a=0,z2a=0,z1b=0,z2b=0;
  // Two biquad sections approximating the A-weighting curve.
  const float b0a=0.255f,b1a=-0.510f,b2a=0.255f,a1a=-0.734f,a2a=0.181f;
  float ya = b0a*x + z1a; z1a = b1a*x - a1a*ya + z2a; z2a = b2a*x - a2a*ya;
  const float b0b=1.0f,b1b=-2.0f,b2b=1.0f,a1b=-1.889f,a2b=0.895f;
  float yb = b0b*ya + z1b; z1b = b1b*ya - a1b*yb + z2b; z2b = b2b*ya - a2b*yb;
  return yb;
}

void i2sInit() {
  i2s_config_t cfg = {
    .mode=(i2s_mode_t)(I2S_MODE_MASTER|I2S_MODE_RX), .sample_rate=FS,
    .bits_per_sample=I2S_BITS_PER_SAMPLE_32BIT,
    .channel_format=I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format=I2S_COMM_FORMAT_STAND_I2S,
    .intr_alloc_flags=0, .dma_buf_count=8, .dma_buf_len=WIN/4 };
  i2s_pin_config_t pins={ I2S_SCK, I2S_WS, I2S_PIN_NO_CHANGE, I2S_SD };
  i2s_driver_install(I2S_NUM_0,&cfg,0,NULL);
  i2s_set_pin(I2S_NUM_0,&pins);
}

void processWindow(const int32_t *buf,int N){
  double ss=0;
  for(int i=0;i<N;i++){ float x=applyBiquads((float)(buf[i]>>8)); ss+=(double)x*x; }
  float spl=20.0f*log10f(sqrtf(ss/N)+1e-9f)+CAL_OFFSET;
  energySum+=pow(10.0,spl/10.0); windows++;
  int bin=constrain((int)lroundf(spl),0,139); hist[bin]++;
  if(spl>lmax) lmax=spl;
}

void reportInterval(){
  if(!windows) return;
  float leq=10.0f*log10f(energySum/windows);
  uint32_t c=0,t=windows; float l10=0,l90=0;
  for(int b=139;b>=0;b--){ c+=hist[b];
    if(!l10 && c>=t*0.10f) l10=b;
    if(!l90 && c>=t*0.90f){ l90=b; break; } }

  bool night = isNight();                    // stricter limits at night
  float limit = night?45.0f:55.0f;           // example dB(A) limits
  bool exceed = leq>limit;

  char b[200];
  snprintf(b,sizeof b,
    "{\"node\":1,\"leq\":%.1f,\"l10\":%.0f,\"l90\":%.0f,"
    "\"lmax\":%.1f,\"limit\":%.0f,\"exceed\":%d}",
    leq,l10,l90,lmax,limit,exceed?1:0);
  mqtt.publish("noise/node/1/levels", b);    // NUMBERS ONLY — no audio

  energySum=0; windows=0; lmax=0;
  for(int i=0;i<140;i++) hist[i]=0;          // reset for next interval
}

void setup(){
  Serial.begin(115200);
  prefs.begin("noise",true);
  CAL_OFFSET=prefs.getFloat("cal",90.0f);    // from reference calibration
  prefs.end();
  i2sInit();
  WiFi.begin(WIFI_SSID,WIFI_PASS);
  mqtt.setServer(MQTT_HOST,1883);
  intervalStart=millis();
}

void loop(){
  if(!mqtt.connected()&&WiFi.status()==WL_CONNECTED) mqtt.connect("noise-1");
  mqtt.loop();

  static int32_t buf[WIN];
  size_t br;
  i2s_read(I2S_NUM_0,buf,sizeof buf,&br,portMAX_DELAY);
  processWindow(buf,br/4);                    // one ~100 ms window

  if(millis()-intervalStart>=INTERVAL_MS){
    reportInterval();
    intervalStart=millis();
  }
}
float applyBiquads(float x)Runs each audio sample through a small IIR filter cascade that approximates the A-weighting curve, so the computed level matches human hearing and the regulatory dB(A) definition.
energySum+=pow(10.0,spl/10.0)Accumulates window energy so the interval Leq is a true energy average — the measure that reflects real noise exposure rather than a plain decibel mean.
mqtt.publish("noise/node/1/levels", b); // NUMBERS ONLYOnly the computed levels are ever transmitted; the audio buffer is processed and overwritten, so the privacy guarantee is structural — there is no recording to leak.
float limit = night?45.0f:55.0fApplies stricter night-time limits, matching how regulations and health concerns treat night noise, so exceedance flags mean the right thing at the right hour.
CAL_OFFSET=prefs.getFloat("cal",90.0f)Loads the single reference-calibration offset that turns this microphone chain's RMS into true dB(A), making the node comparable to a real meter and to the rest of the network.

Configuration & Calibration

Configuration steps

  • Set CAL_OFFSET from a reference calibration so the node reports true dB(A).
  • Set the day/night dB(A) limits and the day/night boundary times for your jurisdiction.
  • Choose the reporting interval (1 min is common; 15 min Leq is also standard for surveys).
  • Select Wi-Fi/MQTT or LoRa, and confirm the payload carries levels only, never audio.

Calibration procedure

An uncalibrated sensor produces confident, precise, wrong numbers. Do this once per physical unit and record the constants.

  1. Reference offset

    Place the node beside a calibrated sound-level meter (or use an acoustic calibrator) across a range of levels and set CAL_OFFSET so the node's dB(A) matches the reference.

  2. A-weighting check

    Verify with tones that the node discounts very low and very high frequencies relative to mid frequencies, confirming the A-weighting filter is working.

  3. Wind/self-noise floor

    In a quiet space confirm the node's noise floor is well below the levels you care about, and that the windscreen suppresses wind-induced false levels outdoors.

Network Architecture & Connectivity

Noise Pollution Mapper — network topologyPath taken by telemetry from field node to end user. Edge nodesGatewayCloudClientsNoise nodeESP32 + micOther nodesacross the cityWi-Fi / LoRaCity gatewayto MQTTMQTT 1883Broker + heat maplevels + limitsHeat mapstreet × hourPhoneexceedance alerts
Noise Pollution Mapper — network topology

Communication protocol

Each node publishes Leq/L10/L90/Lmax and an exceedance flag per interval. The payload is numeric only — audio never leaves the device — so a dense public network maps loudness without any surveillance capability.

Topic / endpointDirectionPayload
noise/node/1/levelsnode → brokerLeq, L10, L90, Lmax, limit, exceed
noise/node/1/statusnode → brokerbattery, RSSI, calibration date
noise/node/1/configbroker → nodelimits, interval, calibration offset

Message contract between the device and the broker.

Cloud platform configuration

A broker feeds a heat map that renders Leq by location and hour, so a city sees which streets breach limits and when — rush-hour roads versus late-night nightlife strips emerge clearly.

Dashboard setup

A time-sliderable heat map plus per-node daily curves of Leq/L10/L90, with exceedance shading against the day/night limits.

Mobile app integration

Alerts when a monitored location exceeds its night or day limit, useful for residents and for compliance monitoring.

Security considerations

  • Sign each node's levels so the public map cannot be spoofed with fake quiet/loud readings.
  • Guarantee and document that no audio is transmitted or stored — the privacy property is the network's social licence.
  • Authenticate configuration/calibration pushes to nodes.

Testing Procedure & Expected Output

Test from the bottom up. Confirm power, then each sensor in isolation, then the integrated loop — the first failing step tells you exactly where to look.

TestWhat you should see
Play a 1 kHz tone at a known levelNode dB(A) matches the reference meter after calibration
Play equal-energy low and high tonesA-weighting discounts them relative to mid frequencies
Create brief loud events amid quietLmax and L10 rise; L90 (background) stays low
Run a steady loud sourceL90 rises toward Leq — persistently loud signature
Inspect the transmitted payloadOnly levels present; no audio field anywhere
Compare day vs night intervalsCorrect (stricter) night limit applied to exceedance flag

Bench-test checklist. If a row fails, stop and fix it before moving on.

Expected output

The heat map colours each node by Leq for the selected hour; a node view shows Leq/L10/L90/Lmax over the day and marks limit exceedances.

jsonnoise-levels.json
{
  "node": 1,
  "leq": 63.4,
  "l10": 68,
  "l90": 54,
  "lmax": 81.2,
  "limit": 55,
  "exceed": 1
}

Here the interval Leq of 63.4 dB(A) exceeds the 55 dB(A) day limit, with an L90 of 54 (persistently busy) and an L10 of 68 (frequent traffic peaks) — a genuinely loud street, not one quiet stretch spoiled by a single event.

A Grafana time-series dashboard
A dashboard turns the level streams into a time-and-place noise heat map. Photograph sourced from Wikimedia Commons — Grafana dashboard.png. Reused under the licence stated on that page; please check it before republishing.

Troubleshooting: Common Errors & Fixes

Levels far too high outdoors, worst when windy

Likely cause. Wind on the MEMS port

Fix. Fit a foam windscreen and rain hood; the correlation with wind confirms the cause

Node disagrees with a reference meter

Likely cause. Uncalibrated, or wrong CAL_OFFSET

Fix. Recalibrate against the reference across a range of levels

Leq seems dominated by rare events

Likely cause. That is correct — energy averaging weights loud windows heavily

Fix. Report L90 alongside Leq to show the background separately

Low-frequency readings look inflated

Likely cause. A-weighting not applied or filter wrong

Fix. Verify the A-weighting biquad coefficients for your sample rate

Privacy concern raised

Likely cause. Misunderstanding — no audio is sent

Fix. Show that only numeric levels are transmitted and audio buffers are discarded on-device

The sketch will not upload — "Failed to connect" or "avrdude: stk500_recv()"

Likely cause. The bootloader is not being reached: wrong port, wrong board, a serial monitor holding the port open, or a USB cable that only carries power.

Fix. Close every serial monitor, confirm Tools → Board and Port, and swap to a known data-capable USB cable. On an ESP32 hold BOOT while the IDE prints "Connecting…", then release. If a peripheral is wired to the UART pins (GPIO 1/3 on ESP32, D0/D1 on Uno) unplug it — it fights the programmer.

The board resets in a loop, or the serial monitor prints "Brownout detector was triggered"

Likely cause. The supply cannot deliver peak current. Wi-Fi transmit bursts, relay coils and servos all pull far more than their average draw.

Fix. Power peripherals from a separate regulated supply with a common ground rather than from the board 5 V pin. Add a 470–1000 µF electrolytic capacitor across the supply near the load, and use a real power adapter rather than a laptop USB port.

Serial monitor shows garbage characters

Likely cause. Baud rate mismatch between Serial.begin() and the monitor, or a floating/shared UART line.

Fix. Set the monitor to 115200 to match the sketch. If it still garbles, the crystal or the USB bridge is being confused by noise — shorten the cable and keep motor wiring away from the USB lead.

An I²C device is not detected

Likely cause. Wrong address, missing pull-ups, swapped SDA/SCL, or a bus too long for the pull-up value.

Fix. Run an I²C scanner sketch first — it should print the device address. Most breakout boards include 4.7 kΩ pull-ups, but if you have chained four of them the parallel resistance is too low; remove the pull-ups from all but one board. Keep the bus under 30 cm at 100 kHz.

Wi-Fi connects but MQTT never does (state -2)

Likely cause. Wrong broker address or port, a firewall in the way, or the broker requiring credentials the sketch is not sending.

Fix. Test from a laptop on the same network first: mosquitto_sub -h <broker> -t "#" -v. If that works, the problem is on the device — check the IP literal, port 1883 (or 8883 for TLS), and that client.setServer() runs before connect(). PubSubClient state codes are documented in its header.

Readings arrive for a while and then stop

Likely cause. The Wi-Fi or MQTT session dropped and the sketch never reconnects, or the broker dropped the client on keep-alive timeout.

Fix. Never assume the link stays up. Check WiFi.status() and client.connected() at the top of every loop and reconnect with exponential backoff. Add a watchdog so a wedged network stack reboots the device instead of going silent.

Performance Optimisation

  • Stream audio via I²S DMA and process per buffer; the ESP32 handles A-weighting and RMS in real time without storing audio.
  • Use a running histogram for percentiles instead of buffering the time series — constant memory per interval.
  • Report compact numeric packets once per interval; the heavy lifting is already done on-device.
  • On solar, the mic and CPU are the main draw; a modest panel covers continuous operation.
  • Replace every delay() with a millis() comparison — blocking delays are the single most common cause of dropped readings.
  • Sample sensors on a fixed cadence and publish on a slower one; you almost never need to transmit at the sampling rate.
  • Move networking into its own FreeRTOS task so a slow DNS lookup cannot stall the control loop.
  • Use uint8_t / uint16_t where the range allows; on an 8-bit AVR a 32-bit add costs four times as much.
  • Batch several samples into one MQTT publish. Radio time, not CPU time, dominates the energy budget.
  • Set the MQTT keep-alive to a value that matches your reporting interval so the broker does not churn reconnections.
  • For battery builds use deep sleep between samples: an ESP32 drops from ~160 mA awake to about 10 µA asleep, which is the difference between days and months of runtime.

Safety Precautions

  • Publish and honour the privacy design: levels only, no audio recorded or transmitted — this is the node's social licence to sit in public space.
  • Present readings as indicative unless certified hardware is used; a MEMS node is below Class-1/2 metrology.
  • Mount nodes safely at height, clear of traffic and tampering.
  • Keep the lithium battery and charger sheltered from weather.
  • Lithium cells vent and burn when abused. Only use protected cells or a proper BMS, never charge below 0 °C, and never leave a charging pack unattended on a wooden desk.
  • Never power an RF module without its antenna fitted — the reflected power destroys the output stage. Check your local licence-free band and duty-cycle limits before transmitting.
  • Wear eye protection when soldering or cutting, and solder in a ventilated space — rosin flux fumes are a respiratory irritant.
  • Power the circuit through a bench supply with a current limit while you are testing. A 300 mA limit turns a wiring mistake into a beep instead of a dead board.
  • Disconnect power before changing any wiring. Hot-plugging a sensor onto a live bus is the fastest way to lose a controller.

Maintenance

  • Re-verify the reference calibration periodically; microphone sensitivity can drift.
  • Inspect and replace the windscreen as it degrades; a perished windscreen lets wind noise back in.
  • Check the mic port for dust/insects blocking the acoustic path.
  • Confirm day/night limits and clock sync remain correct so exceedance flags stay meaningful.
  • Re-check every screw terminal and header after the first week — thermal cycling loosens connections that felt tight on day one.
  • Log pack voltage. When resting voltage after a full charge drops below about 4.0 V, the cell is near end of life — replace it.
  • Wash the panel every few weeks in dusty conditions; a visible dust film costs 15–25 % of the harvest.
  • Keep the broker and dashboard containers patched, and rotate device credentials at least once a year.
  • Recalibrate at the interval given in the calibration section, and keep the constants in a text file next to the firmware — not only in flash.
  • Keep a short logbook of firmware versions and what changed. Six months later you will not remember why that constant is 1.083.

Future Improvements & Upgrades

A working v1 is a platform, not a finish line. These are the upgrades that add the most capability for the least rework.

  • Add coarse spectral bands (octave levels) for source classification without recording audio.
  • Run an on-device classifier to tag noise type (traffic, aircraft, music) from spectral features, still transmitting no audio.
  • Fuse many nodes with a propagation model for a continuous city noise surface between sensors.
  • Correlate exceedances with traffic and event calendars to attribute and target interventions.
  • Design a proper PCB. Once the breadboard version has run for a month, moving to a two-layer board removes the intermittent-contact failures that dominate prototype faults.
  • Add over-the-air firmware updates so you never have to physically reach a deployed node again.
  • Add persistent local storage (microSD or the on-chip flash) so a network outage does not create a hole in your data.
  • Move configuration out of the source: a captive-portal setup page or a JSON config file makes the build reusable without a recompile.
  • Add a battery and solar option so the unit survives a power cut and can be sited away from a socket.
  • Write a small test harness that feeds synthetic sensor values through the decision logic, so you can validate thresholds without physically triggering the event.

Frequently Asked Questions

Does it record what people say?

No. Audio is processed on-device into sound levels and immediately discarded; only numbers (Leq, L10, L90, Lmax) are transmitted. There is no recording to leak — the privacy is structural.

What is A-weighting and why use it?

A filter that discounts frequencies the human ear is less sensitive to, giving dB(A). Every noise regulation is written in dB(A), so applying A-weighting is what makes the reading comparable to a limit.

Why report Leq, L10 and L90 instead of one number?

Noise fluctuates. Leq is the energy-average loudness, L90 is the background, and L10 the intrusive peaks. Together they distinguish a relentlessly loud street from a quiet one with occasional events.

Is it as accurate as a professional meter?

No — a MEMS node is below Class-1/2 metrology. But calibrated against a reference and A-weighted, it is accurate enough to map which streets are too loud and when, which is the goal.

Why does wind ruin outdoor readings?

Wind hitting the microphone port creates large false low-frequency levels. A foam windscreen suppresses it; outdoors it is essential.

References & Learning Resources

These are the primary sources worth reading in full. Manufacturer datasheets always outrank forum posts when the two disagree.

  1. A-weighting and sound-level measurementReference
  2. Equivalent continuous sound level (Leq)Reference
  3. WHO — environmental noise guidelines and healthWHO
  4. INMP441 I²S MEMS microphone (datasheet)TDK InvenSense
  5. Noise mapping — methods and percentile levelsReference