ESP32 Walkie-Talkie — Talk With No Internet

Build a real pair of push-to-talk radios: first a safe win over USB, then audio, then the wireless link and an enclosure. No router, no internet, and no risky jump to a lithium cell.

Free · 11 steps· 5 hours· ~₪213 in parts

What you'll have on your desk

The project is built like a test track: you assemble one unit, hear a beep, upload code verified against Arduino-ESP32 3.3.11, copy it to the second unit — and only once the call works do you box them up. ESP-NOW creates a direct link between the two boards; a PTT button makes each unit half-duplex — you either talk or listen, like a real walkie-talkie.

Build preparation

Get everything ready — then build.

Split the components into two identical piles. Pick INMP441 and MAX98357A modules with pre-soldered pins, and a USB cable that carries data — not just charge. First power-up is always over USB; for portability use a standard 5V USB power bank.

10 parts left to prepare

Gather and check off the parts

Parts to buy

Parts and materials

~₪25 Buy on AliExpress
~₪7 Buy on AliExpress
~₪7 Buy on AliExpress
~₪4 Buy on AliExpress
~₪2 Buy on AliExpress

Build it step by step

Steps 1–3 The heart — ESP32 and power

01

Lay out two identical kits

Put two piles on the table: each with an ESP32, an INMP441 microphone, a MAX98357A amplifier, a speaker and a button. For now you work on one pile only. Once it works, copy the same connections to the second unit.

02

Power up safely over USB

Connect the first ESP32 to the computer with a USB cable that suits the board. Do not connect any battery and no wire to the 3V3 pin yet. A small LED on the board should light up.

03

Set up Arduino and verify uploading

Install Arduino IDE 2. In Boards Manager add the 'esp32 by Espressif Systems' package version 3.3.x, select ESP32 Dev Module and the board's port. Open File → Examples → 01.Basics → Blink and upload.

Steps 4–6 Ear and mouth — microphone and speaker

04

Wire the microphone

With USB unplugged, wire the INMP441: VDD→3V3, GND→GND, L/R→GND, SCK→GPIO25, WS→GPIO33, SD→GPIO32. Go wire by wire and read the labels on the module, not their position in a photo.

05

Wire the amplifier and speaker

Wire the MAX98357A: VIN→5V/VIN on the ESP32 board, GND→GND, BCLK→GPIO26, LRC→GPIO27, DIN→GPIO14. Connect the two speaker wires only to SPK+ and SPK− on the amplifier.

06

Make it beep for the first time

Plug USB back in, open a new sketch and paste audio_test_3x.ino. Upload it. The speaker should produce a short, repeating beep. Do not move on to the wireless link until this test works.

audio_test_3x.ino cpp View code
#include <Arduino.h>
#include <ESP_I2S.h>

constexpr int AMP_BCLK = 26;
constexpr int AMP_LRC = 27;
constexpr int AMP_DIN = 14;
constexpr uint32_t SAMPLE_RATE = 16000;

I2SClass Speaker(I2S_NUM_1);

void setup() {
  Serial.begin(115200);
  Speaker.setPins(AMP_BCLK, AMP_LRC, AMP_DIN);
  if (!Speaker.begin(I2S_MODE_STD, SAMPLE_RATE, I2S_DATA_BIT_WIDTH_16BIT,
                     I2S_SLOT_MODE_MONO, I2S_STD_SLOT_BOTH)) {
    Serial.println("I2S ERROR — check BCLK, LRC and DIN");
    while (true) delay(1000);
  }
  Serial.println("AUDIO TEST READY");
}

void loop() {
  for (int i = 0; i < 4000; ++i) {
    const int16_t sample = (i / 10 % 2) ? 5000 : -5000;
    Speaker.write(&sample, sizeof(sample));
  }
  delay(500);
}

Steps 7–9 Speech and the link

07

Add the talk button

Unplug USB. Wire one leg of the PTT button to GPIO4 and the other to GND. If the button has four legs, use two legs on opposite sides of the switch and check continuity while pressing.

08

Upload the verified code

Replace the test sketch with walkie_talkie_3x.ino below and upload. Open Serial Monitor at 115200. When all is well you will see READY — hold PTT to talk.

walkie_talkie_3x.ino cpp View code
#include <Arduino.h>
#include <ESP_I2S.h>
#include <WiFi.h>
#include <esp_now.h>
#include <esp_wifi.h>

// The wiring must match the guide.
constexpr int MIC_SCK = 25;
constexpr int MIC_WS = 33;
constexpr int MIC_SD = 32;
constexpr int AMP_BCLK = 26;
constexpr int AMP_LRC = 27;
constexpr int AMP_DIN = 14;
constexpr int PTT_PIN = 4;

constexpr uint32_t SAMPLE_RATE = 16000;
constexpr uint8_t ESPNOW_CHANNEL = 6;
constexpr size_t FRAME_SAMPLES = 100; // 200 bytes — safely below an ESP-NOW packet.

struct AudioPacket {
  int16_t samples[FRAME_SAMPLES];
};

I2SClass Microphone(I2S_NUM_0);
I2SClass Speaker(I2S_NUM_1);
QueueHandle_t receivedAudio;
volatile bool radioReady = true;
const uint8_t broadcastAddress[] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff};

void onReceive(const esp_now_recv_info_t *, const uint8_t *data, int length) {
  if (length != static_cast<int>(sizeof(AudioPacket))) return;
  AudioPacket packet;
  memcpy(&packet, data, sizeof(packet));
  xQueueSend(receivedAudio, &packet, 0);
}

void onSent(const esp_now_send_info_t *, esp_now_send_status_t) {
  radioReady = true;
}

bool beginRadio() {
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  if (esp_wifi_set_channel(ESPNOW_CHANNEL, WIFI_SECOND_CHAN_NONE) != ESP_OK) return false;
  if (esp_now_init() != ESP_OK) return false;
  if (esp_now_register_recv_cb(onReceive) != ESP_OK) return false;
  if (esp_now_register_send_cb(onSent) != ESP_OK) return false;

  esp_now_peer_info_t peer = {};
  memcpy(peer.peer_addr, broadcastAddress, sizeof(broadcastAddress));
  peer.channel = ESPNOW_CHANNEL;
  peer.ifidx = WIFI_IF_STA;
  peer.encrypt = false;
  return esp_now_add_peer(&peer) == ESP_OK;
}

bool beginAudio() {
  Microphone.setPins(MIC_SCK, MIC_WS, -1, MIC_SD);
  Speaker.setPins(AMP_BCLK, AMP_LRC, AMP_DIN);
  const bool micReady =
      Microphone.begin(I2S_MODE_STD, SAMPLE_RATE, I2S_DATA_BIT_WIDTH_32BIT,
                       I2S_SLOT_MODE_MONO, I2S_STD_SLOT_LEFT);
  const bool speakerReady =
      Speaker.begin(I2S_MODE_STD, SAMPLE_RATE, I2S_DATA_BIT_WIDTH_16BIT,
                    I2S_SLOT_MODE_MONO, I2S_STD_SLOT_BOTH);
  return micReady && speakerReady;
}

void setup() {
  Serial.begin(115200);
  pinMode(PTT_PIN, INPUT_PULLUP);
  receivedAudio = xQueueCreate(4, sizeof(AudioPacket));

  if (!receivedAudio || !beginAudio() || !beginRadio()) {
    Serial.println("SETUP ERROR — check the wiring and restart");
    while (true) delay(1000);
  }
  Serial.println("READY — hold PTT to talk");
}

void loop() {
  if (digitalRead(PTT_PIN) == LOW) {
    int32_t raw[FRAME_SAMPLES];
    AudioPacket packet = {};
    const size_t bytes = Microphone.readBytes(
        reinterpret_cast<char *>(raw), sizeof(raw));
    const size_t samples = min(bytes / sizeof(raw[0]), FRAME_SAMPLES);

    for (size_t i = 0; i < samples; ++i) {
      const int32_t scaled = raw[i] >> 13;
      packet.samples[i] =
          static_cast<int16_t>(constrain(scaled, -32768L, 32767L));
    }
    if (radioReady) {
      radioReady = false;
      if (esp_now_send(broadcastAddress,
                       reinterpret_cast<const uint8_t *>(&packet),
                       sizeof(packet)) != ESP_OK) {
        radioReady = true;
      }
    }
    return;
  }

  AudioPacket packet;
  if (xQueueReceive(receivedAudio, &packet, pdMS_TO_TICKS(5)) == pdTRUE) {
    Speaker.write(packet.samples, sizeof(packet.samples));
  }
}
09

Copy it to the second unit and talk

Build the second unit from the same wiring table and upload the same code to it. Move the two speakers about two metres apart. Press PTT on unit A, say a short sentence, release — and only then answer from unit B.

Steps 10–11 The shell — enclosure and assembly

10

Turn the prototype into something portable

Only once a call works, fit each system into a small project box or a sturdy cardboard enclosure. Cut openings for the speaker, the microphone, the button and the USB cable. Run each ESP32 from a standard 5V USB power bank.

11

Run an acceptance call

Charge both power banks, switch the units on and run three tests: a call in the same room, a call from another room, and a call after a power cycle. Every time: press, wait a beat, speak, release.