I2S audio
Digital audio in and out with no analog parts at all - a microphone that sends numbers, an amplifier that takes them. The chip generates all three clocks, and every bug is the same one - a buffer that ran dry.
Three clocks and a buffer
The three wires, named
| Signal | Also called | What it is |
|---|---|---|
| BCLK | SCK, SCLK | One pulse per bit |
| WS | LRCLK, LRCK | Which channel this sample belongs to |
| SD | DIN, DOUT | The data itself |
All three come from the ESP32 when it is the master, which is the usual arrangement. The microphone or amplifier just follows.
Parts that work
- INMP441 or ICS-43434 — I2S microphones, 24-bit, three wires.
- MAX98357A — I2S amplifier straight to a speaker, no DAC needed.
- PCM5102 — I2S DAC to a line output, for headphones or a hi-fi.
The thing to design in from the start
Audio in its own FreeRTOS task, on core 1, with a DMA buffer of 40–80 ms. Everything else — Wi-Fi, the display, the SD card — then cannot interrupt the stream, and the clicks that people spend days chasing never appear.
The code
An INMP441 microphone into the ESP32. The samples arrive 24-bit inside a 32-bit slot, which is why the shift is there - without it every level looks 256 times too small.
#include <ESP_I2S.h>
I2SClass i2s;
void setup() {
Serial.begin(115200);
i2s.setPins(4, 5, -1, 6); // BCLK, WS, DOUT, DIN
i2s.begin(I2S_MODE_STD, 16000, I2S_DATA_BIT_WIDTH_32BIT,
I2S_SLOT_MODE_MONO);
}
void loop() {
int32_t frame[256];
size_t n = i2s.readBytes((char *)frame, sizeof(frame));
long peak = 0;
for (size_t i = 0; i < n / 4; i++) {
long s = frame[i] >> 8; // 24 bits in a 32-bit slot
peak = max(peak, labs(s));
}
Serial.println(peak);
}Give audio its own task, pinned to core 1. A loop that also serves a web page will empty the DMA queue eventually, and you hear it as a click.
Same wiring. ibuf is the DMA buffer in bytes - it is the slack that keeps the stream running while your code does something else.
from machine import I2S, Pin
import struct
audio = I2S(0, sck=Pin(4), ws=Pin(5), sd=Pin(6),
mode=I2S.RX, bits=32, format=I2S.MONO,
rate=16000, ibuf=20000)
buf = bytearray(1024)
while True:
n = audio.readinto(buf)
if n:
samples = struct.unpack('<%di' % (n // 4), buf[:n])
print(max(abs(s >> 8) for s in samples))A small ibuf is the cause of clicks and dropouts. 20000 bytes is a reasonable default at 16 kHz, which is a little over half a second.
When it does not work
The microphone's L/R pin is floating. Tie it high or low to choose a channel, and set the matching slot mode in the code.
The DMA buffer emptied. Something blocked - a flash write, a Wi-Fi reconnect, an SD card pausing. Increase the buffer and move audio into its own task.
Missing the shift. 24-bit data in a 32-bit slot needs a right shift of 8 before it means anything.
I2S is digital. It carries numbers, not power. You need an amplifier such as a MAX98357A, which takes I2S in and drives the speaker itself.
Sound is a stream small enough to hold. The next page is one that is not.
Camera capture →Edit this page — content/esp32/i2s-audio.mdx
Discuss this article
Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.