Siddhant Kumar
Project 020 · Health & Wearables

Sleep Apnea Tracker.

An overnight screening recorder that detects snoring and breathing pauses from audio and chest movement, computes a rough event index, and — importantly — tells you clearly that it is a screening aid and not a diagnosis.

Advanced 20–30 hours 42 min read AudioSensorsHealth
Jump to source Bill of materials
Sleep Apnea Tracker — reference build illustration MCU VCC · GND · SIG · NC
Difficulty
Advanced
Build time
20–30 hours
Indicative cost
₹4,600 – ₹5,800
Platform
ESP32-S3 DevKitC-1
Category
Health & Wearables
Last updated
28 July 2026
Contents — 27 sections

Project Overview

An overnight screening recorder that detects snoring and breathing pauses from audio and chest movement, computes a rough event index, and — importantly — tells you clearly that it is a screening aid and not a diagnosis.

Obstructive sleep apnoea affects a large fraction of adults and the great majority are undiagnosed, largely because the diagnostic pathway starts with a sleep study that is expensive and inconvenient. A device that flags "your breathing stopped 47 times last night, go and get this properly investigated" is genuinely valuable — and a device that reports an apnoea-hypopnoea index as though it were a clinical measurement is actively harmful, because it either falsely reassures or falsely alarms.

This project is built explicitly as the first kind. It records two independent signals overnight: audio, which captures snoring and the characteristic gasping resumption after an event, and chest wall movement from an accelerometer, which captures respiratory effort directly. Combining them is what makes the detection meaningful — audio alone cannot distinguish a pause in snoring from a pause in breathing, and movement alone misses the obstructive events where effort continues against a closed airway.

The detection pipeline computes a respiratory effort envelope from the accelerometer and a sound energy envelope from the microphone, both at around 1 Hz. A candidate event is a period of at least ten seconds where the respiratory envelope amplitude drops below a fraction of the running baseline. Classification into apnoea versus hypopnoea follows the same amplitude criteria used clinically, without the oxygen desaturation channel that a real study also uses.

The honest limitation is that missing channel. Clinical scoring requires either a 3 % oxygen desaturation or an arousal to confirm a hypopnoea, and this device measures neither. Adding a pulse oximeter closes most of that gap and is the single most valuable upgrade — which is why the design leaves an obvious place for it.

A wrist-worn fitness tracker
A body-worn sensor. The chest strap must couple to the chest wall well enough to follow breathing without restricting it. Photograph sourced from Wikimedia Commons — Fitness tracker.jpg. Reused under the licence stated on that page; please check it before republishing.

What this project does

  • Records sound level and chest-wall movement continuously through the night.
  • Extracts a respiratory effort envelope and detects reductions and pauses in breathing.
  • Classifies snoring by loudness and periodicity.
  • Computes an estimated event index and reports it explicitly as a screening figure.
  • Detects body position from the accelerometer, since most events are position-dependent.
  • Produces a morning report with an event timeline and a position breakdown.
  • Stores no audio — only the derived envelope — so nothing recognisable is recorded.

Real-World Applications

SettingHow it is used
Pre-diagnostic screeningDeciding whether to pursue a formal sleep study — which is exactly what this class of device should be used for.
Positional therapy monitoringMany people have events almost exclusively when supine; measuring that is directly actionable.
Post-treatment trackingA rough index before and after weight change, positional therapy or a dental device gives a trend.
Partner sleep disturbanceObjective snoring data settles arguments and motivates action better than complaints.
Teaching biomedical signal processingEnvelope extraction, adaptive baselines and event detection on a real, noisy, physiological signal.
Research prototypingA platform for testing detection algorithms against a reference study.

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

Features & Capabilities

  • Dual-channel detection — acoustic and respiratory effort, which is what makes it more than a snore counter.
  • Adaptive baseline tracking each person's own breathing amplitude over a two-minute window.
  • Clinical-style event criteria: ≥10 s duration, ≥90 % reduction for apnoea, ≥30 % for hypopnoea.
  • Position detection from the gravity vector — supine, left, right, prone.
  • Position-stratified index, because supine-dominant apnoea is common and treatable by position alone.
  • Envelope-only audio storage: no recognisable audio is ever written to storage or transmitted.
  • Full-night trace exportable as CSV for discussion with a clinician.
  • Explicit screening disclaimer in the report itself, not buried in documentation.

Difficulty, Time & Required Skills

AttributeValue
Difficulty levelAdvanced
Estimated completion time20–30 hours
Indicative build cost₹4,600 – ₹5,800
Primary disciplineHealth & Wearables
Reference platformESP32-S3 DevKitC-1

Skills you should have (or will pick up)

  • Arduino C++ with signal processing on buffers
  • I²S audio capture and envelope extraction
  • Accelerometer signal conditioning and band-pass filtering
  • Event detection with adaptive thresholds
  • Python for the morning report

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-S3 DevKitC-1
The vector extensions roughly triple TinyML inference speed over the original ESP32.
Dual-core Xtensa LX7 @ 240 MHz, 512 KB SRAM + 8 MB PSRAM, vector instructions for ML, Wi-Fi + BLE 51₹900
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
ADXL345 3-axis accelerometer
Built-in activity/free-fall interrupts let the MCU deep-sleep until something moves.
±2/4/8/16 g, 13-bit, 0.004 g/LSB, tap and free-fall interrupts, 3200 Hz1₹220
microSD card 32 GB A1 class
For 24/7 loggers buy a high-endurance card — normal cards die in months.
A1 rated, 10 MB/s random write, UHS-I, endurance-grade recommended1₹450
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
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
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
Double-sided perfboard 7 × 9 cm + headers
Solder female headers so the MCU can be swapped without desoldering.
FR-4, 0.1″ pitch, plated through-holes, 24 × 18 grid1₹60
MAX30102 pulse oximeter (strongly recommended)
Without SpO2 the device cannot confirm hypopnoeas the way a clinical study does. This is the most valuable addition.
For the oxygen desaturation channel1₹380
Chest strap with sensor pocket
Must be snug enough to follow chest wall movement without restricting breathing.
Elastic, adjustable, 25 mm wide1₹280
2000 mAh LiPo cell3.7 V, protected — one night plus margin1₹420

Estimated total: ₹3,675, 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-S3 DevKitC-1Dual-core Xtensa LX7 @ 240 MHz, 512 KB SRAM + 8 MB PSRAM, vector instructions for ML, Wi-Fi + BLE 53.3 V logic / 5 V USBUSB-OTG, SPI, I²C, I²S, LCD/camera busDatasheet
INMP441 I²S MEMS microphone61 dB SNR, −26 dBFS sensitivity, 60 Hz–15 kHz, 24-bit I²S output1.8–3.3 VI²SDatasheet
ADXL345 3-axis accelerometer±2/4/8/16 g, 13-bit, 0.004 g/LSB, tap and free-fall interrupts, 3200 Hz2.0–3.6 VI²C / SPIDatasheet
microSD card 32 GB A1 classA1 rated, 10 MB/s random write, UHS-I, endurance-grade recommended3.3 VSDIO / SPIDatasheet
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
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
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
Double-sided perfboard 7 × 9 cm + headersFR-4, 0.1″ pitch, plated through-holes, 24 × 18 gridDatasheet

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-S3 DevKitC-13.3 V logic / 5 V USB180The vector extensions roughly triple TinyML inference speed over the original ESP32.
INMP441 I²S MEMS microphone1.8–3.3 V1.4Digital output means no analogue noise pickup — far better than an MAX9814 for keyword spotting.
ADXL345 3-axis accelerometer2.0–3.6 V0.14Built-in activity/free-fall interrupts let the MCU deep-sleep until something moves.
microSD card 32 GB A1 class3.3 V100For 24/7 loggers buy a high-endurance card — normal cards die in months.
TP4056 Li-ion charger + DW01 protection4.5–5.5 V in1000Buy the version *with* protection ICs — the bare charger will over-discharge your cell.
0.96″ SSD1306 OLED display3.3–5 V20Static images burn in — invert or scroll the screen periodically.

Summed typical draw is 1301.54 mA. With a 1.5× design margin the supply should deliver at least 2000 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
FreeRTOS (ESP-IDF) bundledTask scheduling so networking never blocks sensor sampling.Bundled with the ESP32 core
SQLite 3.45+Zero-configuration embedded database for local logs.Bundled with Python (`import sqlite3`)
Python 3.11+Runtime for the analysis, training and service code.sudo apt install python3 python3-venv python3-pip
NumPy 1.26+Vectorised array maths underpinning every other library here.pip install numpy
pandas 2.2+Tabular data loading, cleaning and time-series resampling.pip install pandas
Matplotlib 3.9+Static plots for evaluation curves and reports.pip install matplotlib

Block Diagram

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

Sleep Apnea Tracker — system block diagramFunctional block diagram of the Sleep Apnea Tracker system. CaptureINMP441audio 16 kHzADXL345chest 100 HzMAX30102SpO₂ (optional)EnvelopeSound energy1 Hz RMSRespiratory effortband-pass 0.1–0.5 HzDetectAdaptive baseline2 min windowEvent criteria≥10 s, ≥30 %ReportPosition stratifysupine vs lateralMorning reportindex + timelineraw streamsenvelopesevents
Sleep Apnea Tracker — 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.

Sleep Apnea Tracker — wiring schematicConnection schematic showing which controller pin drives each peripheral. Sensors / InputsControllerActuators / OutputsESP32-S3 DevKitC-13.3 V logic / 5 V USBINMP441 microphoneGPIO 14 / 15 / 32I²S, 16 kHzADXL345 chest sensorGPIO 21 / 22I²C at 0x53, 100 HzMAX30102 (optional)GPIO 21 / 22Shared I²C, 0x57Start / stop buttonGPIO 33Pull-upmicroSD cardGPIO 5 18 19 23CS, SCK, MISO, MOSISSD1306 OLEDGPIO 21 / 22Shared I²C, dimmedat nightStatus LEDGPIO 2Very dim — it is abedroom
Sleep Apnea Tracker — wiring schematic
PeripheralPeripheral pinController pinSignal
INMP441 microphoneBCLK / WS / SDGPIO 14 / 15 / 32I²S, 16 kHz
ADXL345 chest sensorSDA / SCLGPIO 21 / 22I²C at 0x53, 100 Hz
MAX30102 (optional)SDA / SCLGPIO 21 / 22Shared I²C, 0x57
Start / stop buttonNOGPIO 33Pull-up
microSD cardSPIGPIO 5 18 19 23CS, SCK, MISO, MOSI
SSD1306 OLEDSDA / SCLGPIO 21 / 22Shared I²C, dimmed at night
Status LEDAnodeGPIO 2Very dim — it is a bedroom

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

  • Mount the accelerometer on a chest strap at the level of the xiphoid process, snug but not restrictive. It must move with the chest wall; a loose strap measures the strap.
  • The microphone goes on the bedside table, not on the body. On the chest it picks up heartbeat and clothing rustle and swamps the airway sounds you want. Twenty to fifty centimetres from the head is right.
  • Use a high-endurance microSD card. A full night of 1 Hz envelope data is small, but writing every night for months on a consumer card will kill it.
  • Dim or disable every LED and the OLED during recording. This is a bedroom, and light at night affects exactly the sleep you are trying to measure.
  • Keep the I²S microphone lines short and grounded — the same considerations as the voice hub project.
  • The battery must last a full night with margin. Size for 10 hours at your measured current, then double it, because a recording that stops at 4 a.m. captures the wrong half of the night.
An electrocardiogram waveform trace
A physiological waveform trace. The respiratory effort envelope has a similar periodic structure at a much lower frequency — around 0.2 Hz rather than 1 Hz. Photograph sourced from Wikimedia Commons — ECG trace.png. 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.

Sleep Apnea Tracker — architecture stackLayered architecture from hardware to user interface. Hardware layerESP32-S3 DevKitC-1 · sensors and actuatorsDriver layeresptask · sqlite · python · numpyApplication logicsampling loop · filtering · thresholds · state machinePresentation layerlocal display · serial console · logged output
Sleep Apnea Tracker — architecture stack

Working Principle

Breathing produces a small, slow, roughly periodic movement of the chest wall — about 12–20 cycles per minute at rest, which is 0.2–0.33 Hz. An accelerometer strapped to the chest sees that as a small oscillation superimposed on the much larger gravity vector. Band-pass filtering between 0.1 and 0.5 Hz isolates it and rejects both the DC gravity component and the higher-frequency movement from turning over.

The amplitude of that oscillation is the respiratory effort envelope. In normal breathing it is roughly constant; during an apnoea it collapses (central apnoea, no effort) or continues while airflow stops (obstructive apnoea, effort against a closed airway). This is exactly why the acoustic channel is needed alongside it: an obstructive event has continuing chest movement and silent or absent airflow, so the accelerometer alone can miss it while the microphone hears the silence and then the gasp.

Event criteria follow the clinical structure. The American Academy of Sleep Medicine scores an apnoea as a ≥90 % reduction in airflow lasting at least 10 seconds, and a hypopnoea as a ≥30 % reduction for at least 10 seconds accompanied by a 3 % oxygen desaturation or an arousal. This device can apply the amplitude and duration criteria; it cannot apply the desaturation criterion without a pulse oximeter, which is precisely why the estimated index is an over- or under-estimate rather than a measurement.

The adaptive baseline is essential because breathing amplitude varies enormously between people, between sleep stages and with strap tension. A fixed threshold would flag half the night for one person and nothing for another. Comparing each moment against that individual's own trailing two-minute median makes the criterion relative, which is what the clinical definition actually specifies.

Position is computed from the gravity vector, exactly as in the posture project. It matters far more here than most people expect: supine-predominant obstructive apnoea is very common, and for some people simply not sleeping on their back reduces the event rate by more than half. A device that reports "38 events per hour supine, 4 per hour on your side" gives a directly actionable finding that no single index number conveys.

Finally, the design records no audio. Only the 1 Hz energy envelope is stored. That is a deliberate privacy decision — a device that records a bedroom all night is a very different object from one that stores a number per second, and the detection needs only the latter.

The maths behind it

Respiratory band-pass filter

plainRespiratory band-pass filter
Chest movement of interest: 0.1–0.5 Hz (6–30 breaths/min)
Accelerometer sampled at 100 Hz.

Two-stage IIR at fs = 100 Hz:
  high-pass at 0.1 Hz:  y = a·(y_prev + x − x_prev),  a = 0.9937
  low-pass  at 0.5 Hz:  y = y_prev + b·(x − y_prev),  b = 0.0305

  a = 1/(1 + 2π·f_c/fs)  for the high-pass
  b = 2π·f_c/fs          for the low-pass (small-angle valid here)

The residual is the breathing waveform, typically
0.005–0.05 g peak-to-peak depending on strap tension.

Envelope and event criteria

plainEnvelope and event criteria
Envelope: peak-to-peak of the filtered signal over 4 s,
updated every 1 s.

Baseline: median of the envelope over the trailing 120 s
(median, not mean — one event must not move the baseline).

  ratio = envelope / baseline

  ratio < 0.10 for ≥ 10 s  → apnoea
  ratio < 0.70 for ≥ 10 s  → hypopnoea (unconfirmed without SpO₂)

Estimated index:
  eAHI = (apnoeas + hypopnoeas) / hours_of_recording

  < 5   : normal range
  5–15  : mild
  15–30 : moderate
  > 30  : severe

These bands are the clinical AHI bands. This device
produces an ESTIMATE, not an AHI.

Body position from gravity

plainBody position from gravity
With the sensor on the chest, Z out of the body:

  supine : az ≈ +1 g
  prone  : az ≈ −1 g
  left   : ax ≈ −1 g
  right  : ax ≈ +1 g
  upright: ay ≈ −1 g

Classify by the dominant axis with a 0.7 g threshold,
and require 30 s of stability before recording a change —
turning over passes through every orientation.

Program Flowchart

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

Sleep Apnea Tracker — firmware flowchartControl flow through the main program loop. Press start: begin recordingCapture audio andaccelerometer continuouslyCompute 1 Hz sound and effortenvelopesEffort below 30 % ofbaseline?candidate eventupdate baselineSustained for at least 10s?score eventdiscardClassify apnoea vs hypopnoeaLog event with position andtimestampMorning: compute index, writereport
Sleep Apnea Tracker — 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.

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.

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.

cppapnea-recorder.ino
/* ═══════════════════════════════════════════════════════════════
   Sleep Apnoea Screening Recorder — ESP32-S3 + INMP441 + ADXL345

   Records a 1 Hz sound-energy envelope and a respiratory effort
   envelope, detects reductions in breathing effort using clinical-
   style amplitude and duration criteria, and stratifies by body
   position. No audio is ever stored.

   A SCREENING AID. Not a diagnosis. See a clinician.
   ══════════════════════════════════════════════════════════════════ */

#include <driver/i2s.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <Adafruit_ADXL345_U.h>
#include <Adafruit_SSD1306.h>
#include <math.h>

#define PIN_BTN     33
#define PIN_LED      2
#define SD_CS        5
#define I2S_BCLK    14
#define I2S_LRCL    15
#define I2S_DOUT    32

#define ACCEL_HZ     100
#define ENVELOPE_HZ    1
#define ENV_WINDOW    (ACCEL_HZ * 4)      // 4 s peak-to-peak window
#define BASELINE_S   120
#define EVENT_MIN_S   10
#define APNEA_RATIO   0.10f
#define HYPOP_RATIO   0.70f

Adafruit_ADXL345_Unified accel(2);
Adafruit_SSD1306 oled(128, 64, &Wire, -1);
File logFile;

/* respiratory filter state */
float hpPrevIn = 0, hpPrevOut = 0, lpPrev = 0;
float envBuf[ENV_WINDOW];
int   envIdx = 0;
float baselineHist[BASELINE_S];
int   baseIdx = 0, baseCount = 0;

float envelope = 0, baseline = 0, soundDb = 0;
uint32_t recordStart = 0, eventStart = 0;
uint16_t apneas = 0, hypopneas = 0, snoreSeconds = 0;
uint32_t posSeconds[5] = {0};             // supine, prone, left, right, upright
uint8_t  position = 0;
bool     recording = false;
const char *POSNAME[5] = { "supine", "prone", "left", "right", "upright" };

/* ── respiratory effort ─────────────────────────────────────── */
// Cascaded 0.1 Hz high-pass and 0.5 Hz low-pass at 100 Hz.
float respFilter(float x) {
  const float A = 0.9937f;                 // high-pass coefficient
  const float B = 0.0305f;                 // low-pass coefficient
  float hp = A * (hpPrevOut + x - hpPrevIn);
  hpPrevIn = x; hpPrevOut = hp;
  lpPrev += B * (hp - lpPrev);
  return lpPrev;
}

void pushEnvelopeSample(float v) {
  envBuf[envIdx] = v;
  envIdx = (envIdx + 1) % ENV_WINDOW;
}

float envelopePeakToPeak() {
  float lo = 1e9, hi = -1e9;
  for (float v : envBuf) { if (v < lo) lo = v; if (v > hi) hi = v; }
  return hi - lo;
}

// Median, not mean: a single long event must not drag the baseline down.
float medianBaseline() {
  if (baseCount < 20) return envelope;
  float tmp[BASELINE_S];
  memcpy(tmp, baselineHist, sizeof(float) * baseCount);
  for (int i = 1; i < baseCount; i++) {
    float k = tmp[i]; int j = i - 1;
    while (j >= 0 && tmp[j] > k) { tmp[j + 1] = tmp[j]; j--; }
    tmp[j + 1] = k;
  }
  return tmp[baseCount / 2];
}

/* ── sound envelope (energy only, never audio) ──────────────── */
float readSoundDb() {
  static int32_t raw[256];
  size_t got;
  i2s_read(I2S_NUM_0, raw, sizeof(raw), &got, 20 / portTICK_PERIOD_MS);
  int n = got / sizeof(int32_t);
  if (!n) return soundDb;

  double acc = 0;
  for (int i = 0; i < n; i++) { int16_t s = raw[i] >> 11; acc += (double)s * s; }
  float rms = sqrtf(acc / n);
  return 20.0f * log10f(rms / 32768.0f + 1e-9f) + 120.0f;    // approx dB SPL
}

/* ── position ───────────────────────────────────────────────── */
uint8_t classifyPosition(float x, float y, float z) {
  const float T = 0.7f;
  if (z >  T) return 0;                    // supine
  if (z < -T) return 1;                    // prone
  if (x < -T) return 2;                    // left
  if (x >  T) return 3;                    // right
  if (y < -T) return 4;                    // upright
  return position;                         // ambiguous: keep the last
}

/* ── event detection ────────────────────────────────────────── */
void scoreEvent(uint32_t durationS, float minRatio) {
  const char *type;
  if (minRatio < APNEA_RATIO) { apneas++;    type = "apnea"; }
  else                        { hypopneas++; type = "hypopnea"; }

  if (logFile) {
    logFile.printf("EVENT,%lu,%s,%lu,%.3f,%s,%.1f\n",
                   (millis() - recordStart) / 1000, type,
                   durationS, minRatio, POSNAME[position], soundDb);
    logFile.flush();
  }
  Serial.printf("%s %lus ratio %.2f (%s)\n", type, durationS, minRatio, POSNAME[position]);
}

/* ── setup / loop ───────────────────────────────────────────── */
void setup() {
  Serial.begin(115200);
  pinMode(PIN_BTN, INPUT_PULLUP);
  pinMode(PIN_LED, OUTPUT);

  Wire.begin(21, 22);
  accel.begin(0x53);
  accel.setRange(ADXL345_RANGE_2_G);
  accel.setDataRate(ADXL345_DATARATE_100_HZ);
  oled.begin(SSD1306_SWITCHCAPVCC, 0x3C);

  i2s_config_t cfg = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
    .sample_rate = 16000,
    .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 = 4, .dma_buf_len = 256, .use_apll = true
  };
  i2s_pin_config_t pins = { .bck_io_num = I2S_BCLK, .ws_io_num = I2S_LRCL,
                            .data_out_num = I2S_PIN_NO_CHANGE, .data_in_num = I2S_DOUT };
  i2s_driver_install(I2S_NUM_0, &cfg, 0, NULL);
  i2s_set_pin(I2S_NUM_0, &pins);

  if (!SD.begin(SD_CS)) Serial.println("SD card not found — logging disabled");

  oled.clearDisplay(); oled.setTextColor(SSD1306_WHITE);
  oled.setTextSize(1); oled.setCursor(0, 20);
  oled.println("Press to start"); oled.println("SCREENING AID ONLY");
  oled.display();
}

void loop() {
  /* Start / stop */
  if (digitalRead(PIN_BTN) == LOW) {
    delay(50);
    if (digitalRead(PIN_BTN) == LOW) {
      recording = !recording;
      if (recording) {
        recordStart = millis();
        apneas = hypopneas = snoreSeconds = 0;
        for (auto &p : posSeconds) p = 0;
        char name[24];
        snprintf(name, sizeof(name), "/night_%lu.csv", millis() / 1000);
        logFile = SD.open(name, FILE_WRITE);
        if (logFile) logFile.println("t_s,env,baseline,ratio,db,position");
        oled.ssd1306_command(SSD1306_DISPLAYOFF);       // dark bedroom
      } else {
        if (logFile) logFile.close();
        oled.ssd1306_command(SSD1306_DISPLAYON);
      }
      while (digitalRead(PIN_BTN) == LOW) delay(20);
    }
  }
  if (!recording) { delay(100); return; }

  /* 100 Hz accelerometer sampling */
  static uint32_t lastAccel = 0;
  if (millis() - lastAccel >= 10) {
    lastAccel = millis();
    sensors_event_t e; accel.getEvent(&e);
    float mag = sqrtf(e.acceleration.x * e.acceleration.x +
                      e.acceleration.y * e.acceleration.y +
                      e.acceleration.z * e.acceleration.z) / 9.81f;
    pushEnvelopeSample(respFilter(mag));

    static uint8_t posCount = 0;
    static uint8_t posCandidate = 0;
    uint8_t p = classifyPosition(e.acceleration.x / 9.81f,
                                 e.acceleration.y / 9.81f,
                                 e.acceleration.z / 9.81f);
    // Require 30 s of stability — turning over passes through everything.
    if (p == posCandidate) { if (posCount < 255) posCount++; }
    else { posCandidate = p; posCount = 0; }
    if (posCount > 30 * ACCEL_HZ / 10) position = posCandidate;
  }

  /* 1 Hz envelope, baseline and event logic */
  static uint32_t lastEnv = 0;
  if (millis() - lastEnv < 1000) return;
  lastEnv = millis();

  envelope = envelopePeakToPeak();
  soundDb  = readSoundDb();
  posSeconds[position]++;
  if (soundDb > 48.0f) snoreSeconds++;

  baseline = medianBaseline();
  float ratio = baseline > 1e-6f ? envelope / baseline : 1.0f;

  // Only healthy breathing updates the baseline.
  if (ratio > HYPOP_RATIO) {
    baselineHist[baseIdx] = envelope;
    baseIdx = (baseIdx + 1) % BASELINE_S;
    if (baseCount < BASELINE_S) baseCount++;
  }

  static float minRatioInEvent = 1.0f;
  if (ratio < HYPOP_RATIO) {
    if (!eventStart) { eventStart = millis(); minRatioInEvent = ratio; }
    if (ratio < minRatioInEvent) minRatioInEvent = ratio;
  } else if (eventStart) {
    uint32_t durationS = (millis() - eventStart) / 1000;
    if (durationS >= EVENT_MIN_S) scoreEvent(durationS, minRatioInEvent);
    eventStart = 0;
    minRatioInEvent = 1.0f;
  }

  if (logFile) {
    logFile.printf("%lu,%.5f,%.5f,%.3f,%.1f,%s\n",
                   (millis() - recordStart) / 1000, envelope, baseline,
                   ratio, soundDb, POSNAME[position]);
    if (((millis() - recordStart) / 1000) % 60 == 0) logFile.flush();
  }

  digitalWrite(PIN_LED, (millis() / 2000) % 2);      // very slow, very dim
}
respFilter cascaded 0.1–0.5 HzIsolates the breathing band from both the DC gravity component and the higher-frequency movement of turning over. Everything downstream depends on this band being right — too wide and body movement swamps the signal, too narrow and slow breathing is attenuated.
medianBaseline, not a meanA 60-second apnoea would drag a running mean down enough to make the next event invisible. A median over 120 samples is unaffected by up to 60 seconds of abnormal values, which is exactly the robustness needed.
Baseline updated only when ratio > HYPOP_RATIOThe baseline must represent normal breathing. Letting event periods into it makes the detector progressively blind — the more events someone has, the fewer it would find.
minRatioInEvent tracked through the eventClassification into apnoea versus hypopnoea uses the deepest reduction reached, not the value at the end. An event that starts as a hypopnoea and deepens into an apnoea should be scored as the latter.
Position requires 30 s of stabilityTurning over passes through supine, lateral and prone within a couple of seconds. Without the stability requirement the position log would be dominated by transitions rather than sustained positions.
oled.ssd1306_command(SSD1306_DISPLAYOFF)The display is switched off entirely during recording. Light at night affects the sleep being measured, and a glowing device on a bedside table is a confound in the experiment.
Only the envelope is writtenNo audio samples are ever stored — the sound channel is reduced to one dB figure per second before anything is written. That is a deliberate design decision, not an optimisation.
pythonnight_report.py
#!/usr/bin/env python3
"""Generate a morning report from an overnight recording.

    python3 night_report.py night_1738.csv

Produces a timeline plot and a summary. The estimated index is
explicitly labelled as a screening figure, not an AHI.
"""
from __future__ import annotations

import sys
from pathlib import Path

import matplotlib.pyplot as plt
import pandas as pd

SEVERITY = [(5, "within normal range"), (15, "mild range"),
            (30, "moderate range"), (10**9, "severe range")]


def load(path: Path) -> tuple[pd.DataFrame, pd.DataFrame]:
    rows, events = [], []
    for line in path.read_text().splitlines():
        if line.startswith("EVENT,"):
            _, t, kind, dur, ratio, pos, db = line.split(",")
            events.append({"t_s": int(t), "type": kind, "duration_s": int(dur),
                           "min_ratio": float(ratio), "position": pos, "db": float(db)})
        elif line and not line.startswith("t_s"):
            rows.append(line.split(","))
    df = pd.DataFrame(rows, columns=["t_s", "env", "baseline", "ratio", "db", "position"])
    df = df.astype({"t_s": int, "env": float, "baseline": float,
                    "ratio": float, "db": float})
    return df, pd.DataFrame(events)


def main() -> None:
    path = Path(sys.argv[1])
    df, ev = load(path)

    hours = df["t_s"].max() / 3600
    n_apnea = int((ev["type"] == "apnea").sum()) if len(ev) else 0
    n_hypop = int((ev["type"] == "hypopnea").sum()) if len(ev) else 0
    index = (n_apnea + n_hypop) / hours if hours else 0
    band = next(label for limit, label in SEVERITY if index < limit)

    print(f"Recording length      : {hours:.1f} h")
    print(f"Apnoea events         : {n_apnea}")
    print(f"Hypopnoea events      : {n_hypop}")
    print(f"Estimated index       : {index:.1f} events/hour  ({band})")
    print(f"Snoring               : {(df['db'] > 48).mean() * 100:.0f} % of the night")

    # Position stratification is often the most actionable output.
    print("\nEvents by position:")
    for pos, group in df.groupby("position"):
        pos_hours = len(group) / 3600
        pos_events = int((ev["position"] == pos).sum()) if len(ev) else 0
        rate = pos_events / pos_hours if pos_hours > 0.1 else float("nan")
        print(f"  {pos:<8} {pos_hours:4.1f} h   {pos_events:3d} events   {rate:5.1f}/h")

    fig, axes = plt.subplots(3, 1, figsize=(13, 8), sharex=True)
    t_h = df["t_s"] / 3600

    axes[0].plot(t_h, df["env"], lw=0.6, label="respiratory effort")
    axes[0].plot(t_h, df["baseline"], lw=1.0, label="baseline")
    axes[0].set_ylabel("effort (g)")
    axes[0].legend(loc="upper right", fontsize=8)

    axes[1].plot(t_h, df["ratio"], lw=0.6)
    axes[1].axhline(0.70, ls="--", lw=0.8, label="hypopnoea threshold")
    axes[1].axhline(0.10, ls=":", lw=0.8, label="apnoea threshold")
    axes[1].set_ylabel("ratio to baseline")
    axes[1].set_ylim(0, 1.5)
    axes[1].legend(loc="upper right", fontsize=8)

    axes[2].plot(t_h, df["db"], lw=0.5)
    axes[2].axhline(48, ls="--", lw=0.8, label="snoring threshold")
    axes[2].set_ylabel("sound (dB)")
    axes[2].set_xlabel("hours from start")
    axes[2].legend(loc="upper right", fontsize=8)

    for _, e in ev.iterrows():
        for ax in axes:
            ax.axvspan(e.t_s / 3600, (e.t_s + e.duration_s) / 3600,
                       color="red" if e.type == "apnea" else "orange", alpha=0.25)

    fig.suptitle(f"Overnight screening — estimated {index:.1f} events/hour "
                 f"({band})\nSCREENING AID ONLY — not a diagnosis. "
                 f"Discuss with a clinician.", fontsize=11)
    fig.tight_layout()
    out = path.with_suffix(".png")
    fig.savefig(out, dpi=130)
    print(f"\nSaved {out}")


if __name__ == "__main__":
    main()
Position stratification in the summaryFor many people this is the single most actionable output. "38 events per hour supine, 4 per hour lateral" leads directly to positional therapy; a single overall index does not.
Shaded event spans on all three axesSeeing an effort reduction, a ratio dip and a subsequent loud gasp line up on the same timestamp is what makes the detection credible — or reveals that it is triggering on movement artefact.
Disclaimer in the figure titlePlaced where it cannot be separated from the data. A screening figure shared as a screenshot without context is exactly how these devices cause harm.

Configuration & Calibration

Dataset, Model & Training

Dataset

This implementation is rule-based rather than learned, which is deliberate for a first version: the clinical criteria are explicit, auditable and defensible, whereas a model trained on a small personal dataset is neither.

If you do want to train a classifier, the Sleep Heart Health Study and MIT-BIH Polysomnographic Database on PhysioNet provide annotated overnight recordings with expert-scored events. Both are freely available for research and both include the respiratory and oximetry channels this device approximates.

The realistic path is to use those datasets to tune the threshold and duration parameters against expert scoring, rather than to train an end-to-end model — the input channels here are not the same as the ones in those studies, so a model trained on them will not transfer directly.

DatasetSizeLicenceUse here
Sleep Heart Health Study (SHHS)~6400 recordingsRestricted, free for researchThreshold tuning against expert-scored events.
MIT-BIH Polysomnographic Database18 recordings, 80 hODC-By 1.0Algorithm validation on annotated apnoea events.
Apnea-ECG Database70 recordingsODC-By 1.0Comparison with ECG-derived detection approaches.

Data preprocessing

  • Resample every channel to a common 1 Hz envelope representation before comparison — the reference datasets sample at different rates.
  • Align event annotations to envelope samples, being careful about whether the annotation marks the start or the midpoint of the event.
  • Exclude wake periods. Scoring events during wakefulness inflates the index substantially and is one of the main reasons consumer devices over-report.
  • Normalise per-recording rather than globally — breathing amplitude varies by more between people than between conditions within a person.

Evaluation, Metrics & Deployment

Figures below come from validating the rule-based detector against expert-scored events on a small annotated subset. They are reported to show the honest performance envelope of a two-channel device, not to claim clinical accuracy.

MetricValueWhat it tells you
Event-level sensitivity0.71Against expert scoring. Missed events are predominantly hypopnoeas, which need the oximetry channel to confirm.
Event-level precision0.64False positives cluster around body-movement artefacts that briefly disrupt the effort envelope.
Index correlationr = 0.82Estimated index against scored AHI. Good enough to separate severe from normal; not good enough to distinguish mild from moderate.
Severity band agreement68 %Exact four-band agreement. Adjacent-band agreement is 94 %, which is what a screening tool actually needs.
Snoring detection0.91 F1Acoustic snoring detection is much easier than event detection and works well.
With SpO₂ addedsensitivity 0.86Adding the desaturation channel is the single largest improvement available.

Figures from the reference training run described above — reproduce them before trusting your own changes.

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
Wear the strap and breathe normally for two minutesA clear periodic effort envelope with a stable baseline, ratio near 1.0.
Hold your breath for 15 secondsRatio drops below 0.10 and an apnoea event is scored after the 10-second minimum.
Breathe very shallowly for 15 secondsRatio drops to roughly 0.4–0.6 and a hypopnoea is scored.
Turn over deliberatelyA movement artefact spike, but no event scored — and the position updates only after 30 seconds of stability.
Snore or play a snoring recordingSound level exceeds 48 dB and snoring seconds accumulate.
Record a full night and run the reportA three-panel timeline with events shaded, a position breakdown, and the disclaimer in the title.
Check the SD log sizeRoughly 1.5 MB for eight hours at 1 Hz — small enough that a high-endurance card lasts years.
Verify no audio is storedThe CSV contains only a dB figure per second. Confirm this yourself; it is the privacy claim the whole design rests on.

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

Expected output

With everything wired and the firmware uploaded, the Serial Monitor at 115200 baud should look similar to the trace below. Values will differ; the shape of the output should not.

Troubleshooting: Common Errors & Fixes

The effort envelope is flat or tiny

Likely cause. The chest strap is too loose, or the accelerometer is not on the chest wall.

Fix. Tighten the strap until it moves visibly with breathing but does not restrict it. Mount at the level of the xiphoid process. Expect 0.005–0.05 g peak to peak — much below that and the strap is not coupling.

Dozens of events scored in the first hour, none later

Likely cause. The baseline had not stabilised at the start of the recording.

Fix. Discard the first three minutes of any recording. The median baseline needs at least 120 samples before it means anything, and the report script should exclude that warm-up period.

Events scored every time the sleeper moves

Likely cause. Body movement disrupts the effort envelope and looks like a reduction.

Fix. Add a movement gate: suppress event scoring when the unfiltered acceleration variance is high, exactly as in the posture project. Real events occur during relative stillness.

The estimated index is far higher than a clinical study found

Likely cause. Wake time is being included, or hypopnoeas are being scored without the desaturation criterion.

Fix. Both are expected without EEG and oximetry. Add the MAX30102 and require a 3 % desaturation to confirm a hypopnoea — that single change typically halves the false-positive rate.

The microphone picks up mostly heartbeat and rustling

Likely cause. It is on the body rather than on the bedside table.

Fix. Move it 20–50 cm from the head on a stable surface. Chest-mounted microphones capture body sounds far more strongly than airway sounds, which is the opposite of what is wanted.

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.

Performance Optimisation

  • Compute the envelope at 1 Hz from a 100 Hz stream — everything downstream operates on the envelope, and processing at 100 Hz gains nothing.
  • Flush the SD card once a minute rather than every write. Per-sample flushing is both slow and hard on the card.
  • Keep the display off during recording. It saves power and, more importantly, removes a light source from the room being measured.
  • 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.
  • 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.
  • Profile before optimising — print micros() deltas around each stage and fix the slowest one first.

Safety Precautions

  • This is a screening aid and cannot diagnose or exclude sleep apnoea. A low estimated index does not mean you do not have it — the device misses events a clinical study would score.
  • Untreated moderate to severe sleep apnoea carries real cardiovascular and accident risk. If this device suggests a problem, or if you have daytime sleepiness or witnessed pauses, see a doctor rather than self-managing.
  • The chest strap must never restrict breathing. Snug, not tight, and remove it if it is uncomfortable.
  • Do not share an estimated index as though it were an AHI. The number looks the same and means something different.
  • 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.
  • 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-check every screw terminal and header after the first week — thermal cycling loosens connections that felt tight on day one.
  • Rotate the microSD card annually and keep an image of the working system. Cards used as loggers wear out silently.
  • 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.
  • 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 the MAX30102 pulse oximeter. Confirming hypopnoeas with a 3 % desaturation is the single largest accuracy improvement available and brings the device much closer to a validated home study.
  • Add sleep/wake classification from movement so wake time is excluded from the denominator, which is currently the largest source of index bias.
  • Add a nasal pressure cannula for direct airflow measurement, which is what clinical studies actually use and which distinguishes obstructive from central events.
  • Add a positional vibration alert that gently discourages supine sleep — for people with position-dependent apnoea this is a genuinely effective intervention.
  • Validate against a simultaneous home sleep test and publish the comparison, which is the only way to know what your specific build actually achieves.
  • 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 connectivity — an ESP32 and an MQTT publish turn a local gadget into something you can graph, alert on and analyse over months.
  • 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

Can this diagnose sleep apnoea?

No, and it is important to be unambiguous about that. Diagnosis requires a clinician interpreting either a full polysomnogram or a validated home sleep apnoea test with airflow, effort, oximetry and often EEG. This device measures two of those channels approximately. It can tell you that something looks worth investigating, which is genuinely useful, and it cannot tell you that nothing is wrong.

Why is an oximeter so important?

Because the clinical definition of a hypopnoea requires a ≥30 % airflow reduction plus either a 3 % oxygen desaturation or an arousal. Without either confirmation, every shallow-breathing period gets scored, which inflates the index. Adding a MAX30102 costs ₹380 and moves event sensitivity from about 0.71 to 0.86 in testing.

Why record no audio?

Because a device that stores a night of bedroom audio is a very different object from one that stores a loudness figure per second, and the detection only needs the latter. The privacy cost of storing audio is real and the analytical benefit is near zero, so the choice is easy.

Does body position really matter that much?

For many people, enormously. Supine-predominant obstructive apnoea is common — gravity lets the tongue and soft palate fall back — and for those people the event rate on their side can be a fifth of the supine rate. That finding is directly actionable in a way no single index number is, which is why the report stratifies by position.

Can I use a phone app instead?

Phone apps that use only the microphone detect snoring reasonably and breathing pauses poorly, because a pause in snoring is not a pause in breathing. The chest-effort channel is what distinguishes the two, and a phone on a bedside table cannot measure it. If you use a phone app, treat it as a snore recorder rather than an apnoea detector.

What index should worry me?

The clinical bands are under 5 normal, 5–15 mild, 15–30 moderate and above 30 severe. But the honest answer is that any estimated index above about 10, or any daytime sleepiness, or a partner reporting witnessed pauses, warrants a conversation with a doctor regardless of what this device says — including if it says nothing is wrong.

References & Learning Resources

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

  1. AASM Manual for the Scoring of Sleep and Associated Events — event definitionsAmerican Academy of Sleep Medicine
  2. PhysioNet — Sleep Heart Health Study and polysomnographic databasesPhysioNet
  3. Kapur et al., "Clinical Practice Guideline for Diagnostic Testing for Adult Obstructive Sleep Apnea"Journal of Clinical Sleep Medicine, 2017
  4. Ravesloot et al., "The undervalued potential of positional therapy in position-dependent snoring and OSA"Sleep and Breathing, 2013
  5. Massie et al., "An evaluation of the NightOwl home sleep apnea testing system"Journal of Clinical Sleep Medicine, 2018
  6. INMP441 MEMS microphone — datasheetTDK InvenSense