Office Noise Detector
A desktop ESP32 that hears the room and tells it to be quiet — MAX9814 mic, active buzzer, and a threshold that calibrates itself to the room at boot.
Office Noise Detector
A desktop ESP32 that hears the room and tells it to be quiet. Low level. High impact.
What it does
The unit sits on a desk and listens. At power-up it spends four seconds learning what the quiet room sounds like, then shows the current noise as a percentage of that learned threshold, with a sweep graph scrolling underneath. When the level holds above 100% for most of a second, the screen turns red with BE QUIET, shhh... below it, and the buzzer sounds three short beeps. If the room stays loud it beeps again every five seconds. The ESP32 and its screen live in their own printed case and connect to the sensor enclosure by cable.
Components
| Part | Module / Spec | Notes |
|---|---|---|
| Board | ideaspark ESP32-WROOM-32 with built-in 1.9” ST7789 TFT (170x320) | Screen is pre-wired on the board; stays in its own case |
| Microphone | MAX9814 electret amp module, 25x15 mm | Run at 3.3 V; GAIN left floating = 60 dB |
| Alert | Active buzzer, ~12 mm dia | Fixed tone, driven with digitalWrite — no PWM needed |
| Prototyping | Mini 170-point breadboard, 35x47x8.5 mm | Carrier for the mic and buzzer modules |
| Power | USB 5 V | Single supply for the whole unit |
| Fasteners | M2 self-tapping screws | Clamp bar over the mic module |
Wiring
| Signal | ESP32 pin | To |
|---|---|---|
| MIC OUT | GPIO34 | MAX9814 OUT |
| MIC VDD | 3V3 | MAX9814 VDD |
| MIC GND | GND | MAX9814 GND |
| MIC GAIN | — | Left unconnected (60 dB) |
| MIC AR | — | Left unconnected (default attack/release) |
| Buzzer + | GPIO25 | Buzzer long leg |
| Buzzer − | GND | Buzzer short leg |
| TFT CS / DC / RST / BLK | GPIO15 / GPIO2 / GPIO4 / GPIO32 | Pre-wired on the ideaspark board |
| TFT SPI SCK / MOSI | GPIO18 / GPIO23 | Pre-wired on the ideaspark board |
Power is USB 5 V with no level shifting anywhere. Two constraints set the pin choice: the MAX9814 runs from 3V3 rather than 5 V, because at 5 V its output swing can exceed the ESP32’s 3.3 V ADC ceiling; and the mic sits on GPIO34, which is input-only and belongs to ADC1. ADC2 pins stop returning usable values the moment WiFi is active, so an analog sensor always goes on ADC1 (GPIO32–39). This build uses no WiFi, but the habit is worth keeping.
Firmware
- Toolchain: Arduino IDE, Arduino-ESP32 core 3.3.10, upload speed 115200. No WiFi.
- Libraries: Adafruit GFX Library, Adafruit ST7735 and ST7789 Library.
- The MAX9814 has no “loudness” output. It gives the audio waveform itself, resting around a ~1.25 V bias — roughly 1550 on the ADC in a silent room. Every 50 ms the sketch measures the average distance of the samples from that resting centre. Averaging rather than taking the single biggest peak is deliberate: one stray ADC spike should not read as a shout.
- The threshold is relative, not a fixed number. At boot
calibrate()listens for four seconds (discarding the first half second while the mic settles) and stores that as the baseline; the trigger point isbaseline x 4.0, with a floor ofbaseline + 20. The baseline then creeps toward the room whenever things are quiet, so a room that gets gradually busier doesn’t leave the detector permanently triggered. Press RST to re-learn from scratch. - The non-obvious part is self-deafening. The buzzer is louder than anything it is listening for, so without protection the first beep triggers the next one forever. Three guards handle it: the mic is ignored for 400 ms after a beep, the level is reset to baseline the moment the beep ends, and baseline learning freezes for 8 seconds while the MAX9814’s automatic gain control recovers from the blast.
- Debouncing in both directions: the level must stay at 100%+ for 800 ms to fire, so a single clap or a dropped stapler is ignored; it must fall under 70% for a full second, with the alarm up at least 3 seconds, before the screen returns to the meter.
- Tuning: raise
LOUD_RATIOif normal talking already hits 100%, lower it if shouting doesn’t. If the graph sits near the top no matter what, tie MAX9814 GAIN to VDD for 40 dB instead of 60 dB and press RST. The serial monitor at 115200 prints level, baseline and threshold four times a second.
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#define LCD_CS 15
#define LCD_DC 2
#define LCD_RST 4
#define LCD_BLK 32
const int MIC_PIN = 34;
const int BUZZER_PIN = 25;
const float LOUD_RATIO = 4.0;
const float MIN_MARGIN = 20;
const uint32_t HOLD_MS = 800;
const uint32_t QUIET_MS = 1000;
const uint32_t ALARM_MIN_MS = 3000;
const uint32_t REBEEP_MS = 5000;
const uint32_t DEAF_MS = 400;
const uint32_t FREEZE_MS = 8000;
const uint32_t WINDOW_MS = 50;
const uint32_t CALIB_MS = 4000;
const float LEVEL_ALPHA = 0.30;
const float BASE_ALPHA = 0.005;
const int GX = 10, GY = 94, GW = 300, GH = 60;
const int PCT_MAX = 150;
const uint16_t COL_GREY = 0x8410;
const uint16_t COL_DARK = 0x4208;
Adafruit_ST7789 lcd = Adafruit_ST7789(LCD_CS, LCD_DC, LCD_RST);
float baseline = 20;
float level = 20;
bool alarmOn = false;
uint32_t alarmStart = 0;
bool everBeeped = false;
uint32_t beepEndMs = 0;
bool isLoud = false; uint32_t loudStart = 0;
bool isQuiet = false; uint32_t quietStart = 0;
uint8_t hist[GW];
int sweepX = 0;
int shownPct = -1;
int shownState = -1;
uint32_t lastFooter = 0;
uint32_t lastSerial = 0;
int measureSwing() {
static float centre = 1550;
long sum = 0;
float absSum = 0;
long n = 0;
uint32_t t0 = millis();
while (millis() - t0 < WINDOW_MS) {
int v = analogRead(MIC_PIN);
sum += v;
absSum += fabsf(v - centre);
n++;
}
if (n == 0) return 0;
centre = (float)sum / n;
return (int)(absSum / n);
}
float threshold() {
float t = baseline * LOUD_RATIO;
if (t < baseline + MIN_MARGIN) t = baseline + MIN_MARGIN;
return t;
}
int pctOf(float lvl) {
float p = (lvl - baseline) * 100.0f / (threshold() - baseline);
if (p < 0) p = 0;
if (p > PCT_MAX) p = PCT_MAX;
return (int)(p + 0.5f);
}
uint16_t colorFor(int pct) {
if (pct < 60) return ST77XX_GREEN;
if (pct < 100) return ST77XX_YELLOW;
return ST77XX_RED;
}
void beep() {
for (int i = 0; i < 3; i++) {
digitalWrite(BUZZER_PIN, HIGH);
delay(150);
digitalWrite(BUZZER_PIN, LOW);
delay(120);
}
everBeeped = true;
beepEndMs = millis();
level = baseline;
isLoud = false;
isQuiet = false;
}
int yFor(int pct) {
return GY + GH - 1 - (pct * (GH - 1)) / PCT_MAX;
}
void drawColumn(int i) {
int x = GX + i;
int p = hist[i];
int top = yFor(p);
lcd.drawFastVLine(x, GY, GH, ST77XX_BLACK);
lcd.drawFastVLine(x, top, GY + GH - top, colorFor(p));
if ((i % 6) < 3) lcd.drawPixel(x, yFor(100), ST77XX_WHITE);
}
void clearAhead() {
for (int k = 0; k < 4; k++) {
lcd.drawFastVLine(GX + (sweepX + k) % GW, GY, GH, ST77XX_BLACK);
}
}
void pushSample(int pct, bool draw) {
hist[sweepX] = (uint8_t)pct;
if (draw) drawColumn(sweepX);
sweepX = (sweepX + 1) % GW;
if (draw) clearAhead();
}
void drawStatic() {
lcd.fillScreen(ST77XX_BLACK);
lcd.setTextSize(2);
lcd.setTextColor(ST77XX_CYAN);
lcd.setCursor(10, 8);
lcd.print("ThreeBoardsLab");
lcd.drawLine(0, 30, 319, 30, ST77XX_CYAN);
lcd.setTextSize(1);
lcd.setTextColor(ST77XX_WHITE);
lcd.setCursor(10, 38);
lcd.print("Noise level:");
lcd.drawRect(GX - 1, GY - 1, GW + 2, GH + 2, COL_DARK);
shownPct = -1;
shownState = -1;
}
void drawMeterScreen() {
drawStatic();
for (int i = 0; i < GW; i++) drawColumn(i);
clearAhead();
}
void drawLevel(int pct) {
if (pct == shownPct) return;
shownPct = pct;
char buf[8];
snprintf(buf, sizeof(buf), "%3d%%", pct);
lcd.setTextSize(4);
lcd.setTextColor(colorFor(pct), ST77XX_BLACK);
lcd.setCursor(10, 50);
lcd.print(buf);
}
void drawState(int pct) {
int st = (pct < 60) ? 0 : (pct < 100) ? 1 : 2;
if (st == shownState) return;
shownState = st;
lcd.setTextSize(2);
lcd.setCursor(150, 58);
if (st == 0) { lcd.setTextColor(ST77XX_GREEN, ST77XX_BLACK); lcd.print("QUIET "); }
else if (st == 1) { lcd.setTextColor(ST77XX_YELLOW, ST77XX_BLACK); lcd.print("RISING "); }
else { lcd.setTextColor(ST77XX_RED, ST77XX_BLACK); lcd.print("TOO LOUD "); }
}
void drawFooter(int swing) {
char buf[56];
snprintf(buf, sizeof(buf), "base %4d thr %4d now %4d ",
(int)baseline, (int)threshold(), swing);
lcd.setTextSize(1);
lcd.setTextColor(COL_GREY, ST77XX_BLACK);
lcd.setCursor(10, 160);
lcd.print(buf);
}
void drawAlarm() {
lcd.fillScreen(ST77XX_RED);
lcd.setTextColor(ST77XX_WHITE);
lcd.setTextSize(5);
lcd.setCursor((320 - 240) / 2, 45);
lcd.print("BE QUIET");
lcd.setTextSize(3);
lcd.setCursor((320 - 126) / 2, 110);
lcd.print("shhh...");
}
void calibrate() {
drawStatic();
lcd.setTextSize(2);
lcd.setTextColor(ST77XX_YELLOW, ST77XX_BLACK);
lcd.setCursor(150, 58);
lcd.print("CALIBRATING");
lcd.setTextSize(1);
lcd.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
lcd.setCursor(GX + 80, GY + GH / 2 - 4);
lcd.print("stay quiet for a moment...");
Serial.println("Calibrating - keep the room quiet...");
uint32_t t0 = millis();
float sum = 0;
int n = 0;
while (millis() - t0 < CALIB_MS) {
int s = measureSwing();
if (millis() - t0 > 500) {
sum += s;
n++;
}
delay(1);
}
baseline = (n > 0) ? sum / n : 20;
if (baseline < 1) baseline = 1;
level = baseline;
Serial.printf("Baseline = %.1f threshold = %.1f\n", baseline, threshold());
for (int i = 0; i < GW; i++) hist[i] = 0;
sweepX = 0;
drawMeterScreen();
}
void setup() {
Serial.begin(115200);
delay(200);
pinMode(BUZZER_PIN, OUTPUT);
digitalWrite(BUZZER_PIN, LOW);
pinMode(LCD_BLK, OUTPUT);
digitalWrite(LCD_BLK, HIGH);
lcd.init(170, 320);
lcd.setRotation(1);
calibrate();
}
void loop() {
int swing = measureSwing();
uint32_t now = millis();
bool deaf = everBeeped && (now - beepEndMs < DEAF_MS);
if (!deaf) level += LEVEL_ALPHA * (swing - level);
bool frozen = alarmOn || (everBeeped && now - beepEndMs < FREEZE_MS);
if (!deaf && !frozen && pctOf(level) < 50) {
baseline += BASE_ALPHA * (level - baseline);
if (baseline < 1) baseline = 1;
}
int pct = pctOf(level);
if (!deaf && pct >= 100) { if (!isLoud) { isLoud = true; loudStart = now; } }
else isLoud = false;
if (!deaf && pct < 70) { if (!isQuiet) { isQuiet = true; quietStart = now; } }
else isQuiet = false;
bool loudLongEnough = isLoud && (now - loudStart >= HOLD_MS);
bool quietLongEnough = isQuiet && (now - quietStart >= QUIET_MS);
if (!alarmOn && loudLongEnough) {
alarmOn = true;
alarmStart = now;
drawAlarm();
Serial.println(">>> BE QUIET");
beep();
} else if (alarmOn) {
if (loudLongEnough && millis() - beepEndMs >= REBEEP_MS) {
Serial.println(">>> still loud - beep again");
beep();
} else if (quietLongEnough && now - alarmStart >= ALARM_MIN_MS) {
alarmOn = false;
Serial.println("--- quiet again");
drawMeterScreen();
}
}
pct = pctOf(level);
pushSample(pct, !alarmOn);
if (!alarmOn) {
drawLevel(pct);
drawState(pct);
if (millis() - lastFooter >= 500) {
lastFooter = millis();
drawFooter(swing);
}
}
if (millis() - lastSerial >= 250) {
lastSerial = millis();
Serial.printf("now=%4d level=%6.1f base=%6.1f thr=%6.1f %3d%%%s\n",
swing, level, baseline, threshold(), pct, alarmOn ? " ALARM" : "");
}
}
Enclosure
- Printer: Bambu Lab P2S
- Filament: standard PLA
- Logo: ThreeBoardsLab mark (icon + wordmark) recessed into the sloped faceplate
- Three printed parts — base, faceplate, clamp bar. The faceplate sits at roughly 12 degrees so the unit reads at desk height.
- The base is 28 mm deeper than the parts inside it need. Dupont connector shells stand about 13 mm above the breadboard and fouled the lid; that depth was added after a test fit, not designed in up front.
- The buzzer drops into a seat with guide ribs instead of a diameter-exact pocket. Ribs grip a range of sizes, which removes the re-print loop when the part measures 12 mm and the model assumed 11.
- The MAX9814 is held by a pocket plus a separate screw-down clamp bar — no glue, and the module still comes out. Its 9 mm capsule dome exits through a 13x11 mm oblong opening offset toward the pin-free end of the PCB.
- Print the faceplate rotated 180 degrees, not mirrored. Mirroring reverses the logo text.