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.
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
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
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.
Gather and check off the parts
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.
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.
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.
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.
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.
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.
#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);
}
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.
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.
#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));
}
}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.
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.
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.