Community project

Pocket All-Day Voice Recorder

ESP32
Photo of Pocket All-Day Voice Recorder
Generated with AI

Danil Maximov

Published November 2, 2026

Build a portable voice recorder that captures audio to a microSD card and streams recordings over Bluetooth. The ESP32 microcontroller manages the INMP441 I²S microphone for high-quality audio input, handles file storage on the memory card, and provides wireless data transfer via BLE. A simple push button starts and stops recording sessions, while the integrated lithium battery and USB charging board keep the device running all day.

This guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions to fit all components into a pocket-sized enclosure. The included Arduino firmware handles audio capture at 8 kHz, automatic file segmentation, and Bluetooth communication for remote playback and file management. Makers will learn how to integrate I²S audio peripherals, manage SD card operations, and implement BLE services on the ESP32 platform.

Wiring diagram

Loading diagram…

Gather all the parts

QtyComponent
1

HiLetgo INMP441 I²S Microphone Module

Omnidirectional 24-bit I²S MEMS microphone module based on the TDK InvenSense INMP441 IC. Outputs digital audio over a 3-wire I²S bus (SCK/BCLK, WS/LRCLK, SD/DOUT). Supply 1.8–3.3 V; native 3.3 V operation with no level shifting required. L/R channel select pin: tie to GND for left-channel mono output or VDD for right-channel mono output. Use the ESP32 Arduino core or ESP-IDF I2S peripheral API; no separate PlatformIO library is required.

1

MicroSD Card Module

SPI-based microSD card adapter module for SPI-capable microcontrollers. Uses MOSI, MISO, SCK, and CS plus power and ground. Many low-cost modules include a 3.3 V regulator and level shifting for 5 V MCU boards, while bare breakouts should be powered and signalled at 3.3 V.

1

Push Button

momentary tactile switch

Momentary push button switch

1

Lithium Ion Polymer Battery - 3.7v 2500mAh

3.7V 2500mAh lithium-ion polymer battery for portable electronics, with a 4.2V fully charged output.

1

Adafruit bq25185 USB / DC / Solar Charger with 5V Boost Board

bq25185 multi-source LiPoly/LiIon charger with power-path support combined with a TPS61023 5V boost output for battery-powered projects that need a regulated 5V rail.

Assemble it in 6 steps

1. Prepare the memory card

Put a reliable 32 GB or larger microSD card into the microSD module before applying power. The card is where the daily recordings are kept.

  • Format the card as FAT32 so the recorder can make WAV files on it.
  • Do not remove the card while the recorder has power; that can damage the recording being written.

2. Connect the microphone

Connect the INMP441 VDD pin to the board 3V3 pin (power), GND to GND (ground), SCK to GPIO4 (timing signal), WS to GPIO5 (timing signal), SD to GPIO6 (audio signal), and L/R to GND (selects one microphone channel). Keep the small microphone opening clear of glue and the enclosure wall.

  • Use short wires for SCK, WS, and SD; short wires help keep the recorded sound clean.
  • Do not connect VDD to 5V; the microphone is a 3.3 V part and 5 V can damage it.

3. Connect the memory-card module

Connect the module VCC to 3V3 (power), GND to GND (ground), MISO to GPIO13 (data from card), MOSI to GPIO11 (data to card), SCK to GPIO12 (clock), and CS to GPIO10 (card select).

  • Use a 3.3 V microSD module or a breakout specifically marked safe at 3.3 V.
  • Do not swap VCC and GND; swapped power can damage the card module and the card.

4. Add the record button

Connect one button leg to GPIO7 (signal) and the other leg to GND (ground). A short press pauses recording, and the next short press starts it again.

  • For a four-legged tactile switch, use two legs from opposite sides of the switch; legs on the same side are already joined together.
  • Do not use GPIO0 for this button; holding the board's BOOT pin during restart can stop normal startup.

5. Build the rechargeable power path

Plug the LiPo battery connector into the charger board BAT connection. Connect the charger board 5V output to the ESP32 VIN or 5V pin (power) and its GND output to ESP32 GND (ground). Charge the battery through the charger board USB-C socket, not through the ESP32 USB socket while it is attached to this battery power path.

  • The charger board's USB-C socket is the normal charging connection; its charge light should show that it is charging.
  • Use only a protected single-cell 3.7 V LiPo battery. Reversed battery wiring, puncturing the pouch, or using a damaged swollen battery can cause fire.

6. Fit the parts into a pocket case

Place the battery flat in a non-metal case, keep the microphone opening facing a small hole in the case, and keep the microSD card reachable. Add strain relief so pulling a cable cannot tear a battery lead from a board.

  • Keep the ESP32 antenna end clear of the battery and large metal objects so the phone can find it over Bluetooth.
  • Never enclose a LiPo battery where it can be crushed, bent, or heated.

Review all connections

1. Connections between "battery_1" and "ESP32"

Functionbattery_1ESP32
powerBAT+ → Adafruit bq25185 USB / DC / Solar Charger with 5V Boost Board BATEXT
groundBAT- → Adafruit bq25185 USB / DC / Solar Charger with 5V Boost Board GNDEXT

2. Connections between "power_1" and "ESP32"

Functionpower_1ESP32
power5V → regulated power to the board VIN pinVIN
groundGND → common groundGND

3. Connections between "mic_1" and "ESP32"

Functionmic_1ESP32
powerVDD → 3.3 V microphone power3V3
groundGND → microphone groundGND
digitalSCK → I2S bit clockGPIO 4
digitalWS → I2S left-right clockGPIO 5
dataSD → I2S microphone audio dataGPIO 6
groundL/R → select left channel for mono recordingGND

4. Connections between "sd_1" and "ESP32"

Functionsd_1ESP32
powerVCC → 3.3 V card module power3V3
groundGND → card module groundGND
spiMISO → data from the memory cardGPIO 13
spiMOSI → data to the memory cardGPIO 11
spiSCK → memory-card clockGPIO 12
spiCS → memory-card selectGPIO 10

5. Connections between "record_button_1" and "ESP32"

Functionrecord_button_1ESP32
groundGND → button groundGND
digitalSIGNAL → start or stop recordingGPIO 7

Deploy the firmware

#include <Arduino.h>
#include <SPI.h>
#include <SD.h>
#include <driver/i2s.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

constexpr int MIC_BCLK_PIN = 4;
constexpr int MIC_LRCLK_PIN = 5;
constexpr int MIC_DOUT_PIN = 6;
constexpr int SD_CS_PIN = 10;
constexpr int SD_MOSI_PIN = 11;
constexpr int SD_SCK_PIN = 12;
constexpr int SD_MISO_PIN = 13;
constexpr int BUTTON_PIN = 7;
constexpr uint32_t SAMPLE_RATE = 8000;
constexpr uint32_t SEGMENT_SECONDS = 600;
constexpr size_t I2S_BUFFER_BYTES = 1024;
constexpr size_t BLE_CHUNK_BYTES = 180;
constexpr uint8_t BLE_PACKETS_PER_LOOP = 12;

static const char *SERVICE_UUID = "9b7d0001-2d45-4d4c-9f57-2d6d4e6f1001";
static const char *COMMAND_UUID = "9b7d0002-2d45-4d4c-9f57-2d6d4e6f1001";
static const char *STATUS_UUID = "9b7d0003-2d45-4d4c-9f57-2d6d4e6f1001";
static const char *DATA_UUID = "9b7d0004-2d45-4d4c-9f57-2d6d4e6f1001";

struct __attribute__((packed)) WavHeader {
  char riff[4];
  uint32_t fileSizeMinus8;
  char wave[4];
  char fmt[4];
  uint32_t fmtSize;
  uint16_t audioFormat;
  uint16_t channels;
  uint32_t sampleRate;
  uint32_t byteRate;
  uint16_t blockAlign;
  uint16_t bitsPerSample;
  char data[4];
  uint32_t dataSize;
};

BLECharacteristic *statusCharacteristic = nullptr;
BLECharacteristic *dataCharacteristic = nullptr;
File recordingFile;
File transferFile;
char currentFileName[20] = {0};
uint32_t currentDataBytes = 0;
uint32_t segmentStartedMs = 0;
uint32_t nextFileNumber = 0;
uint32_t transferSequence = 0;
bool recording = false;
bool transferActive = false;
bool transferRequested = false;
bool buttonWasDown = false;

void sendStatus(const String &message) {
  if (statusCharacteristic != nullptr) {
    statusCharacteristic->setValue(message.c_str());
    statusCharacteristic->notify();
  }
}

void writeWavHeader(File &file, uint32_t dataBytes) {
  WavHeader header = {
    {'R', 'I', 'F', 'F'},
    static_cast<uint32_t>(36 + dataBytes),
    {'W', 'A', 'V', 'E'},
    {'f', 'm', 't', ' '},
    16,
    1,
    1,
    SAMPLE_RATE,
    SAMPLE_RATE * 2,
    2,
    16,
    {'d', 'a', 't', 'a'},
    dataBytes
  };
  file.seek(0);
  file.write(reinterpret_cast<const uint8_t *>(&header), sizeof(header));
}

bool openNewSegment() {
  if (recordingFile) {
    writeWavHeader(recordingFile, currentDataBytes);
    recordingFile.close();
  }

  do {
    snprintf(currentFileName, sizeof(currentFileName), "/REC%05lu.WAV", static_cast<unsigned long>(nextFileNumber++));
  } while (SD.exists(currentFileName));

  recordingFile = SD.open(currentFileName, FILE_WRITE);
  if (!recordingFile) {
    sendStatus("ERROR: cannot create recording");
    return false;
  }
  currentDataBytes = 0;
  writeWavHeader(recordingFile, 0);
  segmentStartedMs = millis();
  sendStatus(String("RECORDING:") + currentFileName);
  return true;
}

void stopRecording() {
  if (recordingFile) {
    writeWavHeader(recordingFile, currentDataBytes);
    recordingFile.close();
  }
  recording = false;
  sendStatus("RECORDING_STOPPED");
}

void configureI2S() {
  i2s_config_t config = {};
  config.mode = static_cast<i2s_mode_t>(I2S_MODE_MASTER | I2S_MODE_RX);
  config.sample_rate = SAMPLE_RATE;
  config.bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT;
  config.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT;
  config.communication_format = I2S_COMM_FORMAT_STAND_I2S;
  config.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1;
  config.dma_buf_count = 8;
  config.dma_buf_len = 256;
  config.use_apll = false;
  config.tx_desc_auto_clear = false;
  config.fixed_mclk = 0;
  i2s_driver_install(I2S_NUM_0, &config, 0, nullptr);

  i2s_pin_config_t pins = {};
  pins.bck_io_num = MIC_BCLK_PIN;
  pins.ws_io_num = MIC_LRCLK_PIN;
  pins.data_out_num = I2S_PIN_NO_CHANGE;
  pins.data_in_num = MIC_DOUT_PIN;
  i2s_set_pin(I2S_NUM_0, &pins);
  i2s_zero_dma_buffer(I2S_NUM_0);
}

String findNextCompletedFile() {
  File root = SD.open("/");
  if (!root) return String();
  File candidate;
  while (true) {
    File entry = root.openNextFile();
    if (!entry) break;
    String name = String(entry.name());
    bool usable = !entry.isDirectory() && name.startsWith("/REC") && name.endsWith(".WAV") && name != String(currentFileName);
    entry.close();
    if (usable) {
      root.close();
      return name;
    }
  }
  root.close();
  return String();
}

void beginTransfer() {
  if (transferActive) return;
  String path = findNextCompletedFile();
  if (path.length() == 0) {
    sendStatus("NO_COMPLETED_FILE");
    return;
  }
  transferFile = SD.open(path, FILE_READ);
  if (!transferFile) {
    sendStatus("ERROR: cannot open file");
    return;
  }
  transferSequence = 0;
  transferActive = true;
  sendStatus(String("BEGIN:") + path + ":" + String(transferFile.size()));
}

void serviceTransfer() {
  if (transferRequested) {
    transferRequested = false;
    beginTransfer();
  }
  if (!transferActive || !transferFile) return;

  uint8_t packet[BLE_CHUNK_BYTES + 4];
  for (uint8_t n = 0; n < BLE_PACKETS_PER_LOOP && transferActive; ++n) {
    size_t bytesRead = transferFile.read(packet + 4, BLE_CHUNK_BYTES);
    if (bytesRead == 0) {
      transferFile.close();
      transferActive = false;
      sendStatus("END");
      break;
    }
    packet[0] = static_cast<uint8_t>(transferSequence & 0xFF);
    packet[1] = static_cast<uint8_t>((transferSequence >> 8) & 0xFF);
    packet[2] = static_cast<uint8_t>((transferSequence >> 16) & 0xFF);
    packet[3] = static_cast<uint8_t>((transferSequence >> 24) & 0xFF);
    dataCharacteristic->setValue(packet, bytesRead + 4);
    dataCharacteristic->notify();
    ++transferSequence;
  }
}

class RecorderCommandCallbacks : public BLECharacteristicCallbacks {
  void onWrite(BLECharacteristic *characteristic) override {
    String value = characteristic->getValue();
    if (value == "NEXT") {
      transferRequested = true;
    } else if (value == "STATUS") {
      sendStatus(recording ? "RECORDING" : "PAUSED");
    }
  }
};

void configureBle() {
  BLEDevice::init("Pocket Recorder");
  BLEServer *server = BLEDevice::createServer();
  BLEService *service = server->createService(SERVICE_UUID);
  BLECharacteristic *commandCharacteristic = service->createCharacteristic(
    COMMAND_UUID, BLECharacteristic::PROPERTY_WRITE);
  commandCharacteristic->setCallbacks(new RecorderCommandCallbacks());
  statusCharacteristic = service->createCharacteristic(
    STATUS_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
  dataCharacteristic = service->createCharacteristic(
    DATA_UUID, BLECharacteristic::PROPERTY_NOTIFY);
  statusCharacteristic->addDescriptor(new BLE2902());
  dataCharacteristic->addDescriptor(new BLE2902());
  statusCharacteristic->setValue("READY");
  service->start();
  BLEAdvertising *advertising = BLEDevice::getAdvertising();
  advertising->addServiceUUID(SERVICE_UUID);
  advertising->setScanResponse(true);
  advertising->start();
}

void captureAudio() {
  int32_t rawSamples[I2S_BUFFER_BYTES / sizeof(int32_t)];
  size_t bytesRead = 0;
  esp_err_t result = i2s_read(I2S_NUM_0, rawSamples, sizeof(rawSamples), &bytesRead, portMAX_DELAY);
  if (result != ESP_OK || bytesRead == 0 || !recordingFile) return;

  const size_t sampleCount = bytesRead / sizeof(int32_t);
  int16_t monoSamples[I2S_BUFFER_BYTES / sizeof(int32_t)];
  for (size_t i = 0; i < sampleCount; ++i) {
    int32_t scaled = rawSamples[i] >> 14;
    if (scaled > 32767) scaled = 32767;
    if (scaled < -32768) scaled = -32768;
    monoSamples[i] = static_cast<int16_t>(scaled);
  }
  size_t written = recordingFile.write(reinterpret_cast<const uint8_t *>(monoSamples), sampleCount * sizeof(int16_t));
  currentDataBytes += written;

  if (millis() - segmentStartedMs >= SEGMENT_SECONDS * 1000UL) {
    openNewSegment();
  }
}

void serviceButton() {
  bool buttonDown = digitalRead(BUTTON_PIN) == LOW;
  if (buttonDown && !buttonWasDown) {
    if (recording) {
      stopRecording();
    } else {
      recording = openNewSegment();
    }
  }
  buttonWasDown = buttonDown;
}

void setup() {
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  Serial.begin(115200);
  SPI.begin(SD_SCK_PIN, SD_MISO_PIN, SD_MOSI_PIN, SD_CS_PIN);
  if (!SD.begin(SD_CS_PIN, SPI)) {
    Serial.println("microSD card failed");
    return;
  }
  configureI2S();
  configureBle();
  recording = openNewSegment();
}

void loop() {
  serviceButton();
  if (recording) {
    captureAudio();
  }
  serviceTransfer();
  delay(1);
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace, diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik