How to play multiple
audio files from ESP32
with MAX98357A
What You Need
Parts
- ESP32 Dev Board (38-pin) or ESP32-C3
- MAX98357A I2S amplifier module
- 4-8Ω speaker (1W or above)
- Jumper wires
- USB-C cable for flashing
Software
- Arduino IDE with ESP32 board support
ffmpeg- convert audio files- Python 3 - WAV to header conversion
How It Works
The hardware is the same as Step 2 - the MAX98357A receives I2S audio from the ESP32, converts it to analogue, and drives the speaker. Three signal wires, no capacitors, no DAC.
This guide adds multiple clips. Each clip is a separate C array stored in flash via PROGMEM and included as a header file. The sketch works through all clips in sequence and loops forever. For button-triggered playback and custom sequence control, see Step 4 - How to trigger audio sequences with a button on ESP32 with MAX98357A →
Wiring
| MAX98357A Pin | ESP32 Dev Board | ESP32-C3 | Notes |
|---|---|---|---|
| VIN | 5V | 5V | 5V recommended - 3.3V works at lower volume |
| GND | GND | GND | |
| LRC | GPIO14 | GPIO5 | I2S word select (LRCLK) |
| BCLK | GPIO27 | GPIO4 | I2S bit clock |
| DIN | GPIO13 | GPIO6 | I2S data out |
| SD | Not connected | Not connected | Leave floating - amp enabled |
| GAIN | Not connected | Not connected | Leave floating - 9dB gain |
Before You Start
These guides are ordered by complexity but each one stands alone - if you believe and have courage, stand tall and boldly continue. If this is your first time, start at Step 1 - How to wire the MAX98357A I2S amp with ESP32 →
Page outline
- Create a folder and cut each clip into it
- Save the converter script
- Run the batch - one header file per clip
- Flash the sketch
Preparing Your Clips
Create a folder, cut your clips into it one by one, then run the batch converter to produce a header file for each clip.
Create a clips folder
Create a folder to hold your WAV files:
Copy and paste into terminal
mkdir ~/myclips Name your files clip1.wav, clip2.wav, clip3.wav and so on. The sketch is already set up for these names - use them and it works without any editing.
Find your clip in a video
Open the video in a media player such as VLC. Play through until you find the section you want. Note the timestamp at the bottom of the player for where your clip starts and where it ends.
Use ffmpeg to cut just that section and save it directly into your clips folder:
Copy and paste into terminal
ffmpeg -i ~/Videos/myvideo.mp4 -ss 00:01:23 -to 00:01:27 ~/myclips/clip1.wav What the flags mean:
-i ~/Videos/myvideo.mp4- your input file. Not sure of the path? Drag the file into a terminal and the full path pastes in automatically.-ss 00:01:23- start cutting at this timestamp-to 00:01:27- stop cutting at this timestamp~/myclips/clip1.wav- the output filename. Name your clipsclip1.wav,clip2.wav,clip3.wavetc. and the sketch works without any changes.
Examples:
If the source file is in your home folder:
Copy and paste into terminal
ffmpeg -i ~/mysound.wav -ss 00:01:23 -to 00:01:27 ~/myclips/clip1.wav If the source file is in Downloads:
Copy and paste into terminal
ffmpeg -i ~/Downloads/mysound.mp3 -ss 00:01:23 -to 00:01:27 ~/myclips/clip1.wav If the source is a video - ffmpeg extracts the audio automatically:
Copy and paste into terminal
ffmpeg -i ~/Videos/myvideo.mp4 -ss 00:01:23 -to 00:01:27 ~/myclips/clip1.wav If the filename has spaces - wrap it in quotes:
Copy and paste into terminal
ffmpeg -i "/home/j/My Audio File.wav" -ss 00:01:23 -to 00:01:27 ~/myclips/clip1.wav Repeat for each clip, changing the timestamps and filename each time. Once all clips are in the folder, move on.
New script - wav_to_i2s_header_multi.py
Save this as a new file - wav_to_i2s_header_multi.py. It takes a third argument for the variable name so each header is unique. How to save a script →
import sys, wave, struct
input_file = sys.argv[1]
output_file = sys.argv[2]
var_name = sys.argv[3] if len(sys.argv) > 3 else 'audio'
with wave.open(input_file, 'rb') as f:
raw = f.readframes(f.getnframes())
samples = struct.unpack('<' + str(len(raw)//2) + 'h', raw)
lines = [
'#pragma once',
'#include <pgmspace.h>',
'const int16_t ' + var_name + '_data[] PROGMEM = {',
]
chunks = [str(s) for s in samples]
for i in range(0, len(chunks), 16):
lines.append(' ' + ', '.join(chunks[i:i+16]) + ',')
lines.append('};')
lines.append('const size_t ' + var_name + '_len = sizeof(' + var_name + '_data);')
with open(output_file, 'w') as f:
f.write('\n'.join(lines) + '\n')
print('Done: ' + str(len(samples)) + ' samples -> ' + output_file) Convert all clips
Once all your WAVs are in the folder, run this once to convert the whole batch. Each .h file is named after its source file automatically.
Copy and paste into terminal
for f in ~/myclips/*.wav; do
name=$(basename "$f" .wav)
ffmpeg -y -i "$f" -ar 22050 -ac 1 -acodec pcm_s16le "/tmp/${name}_i2s.wav"
python3 ~/wav_to_i2s_header_multi.py "/tmp/${name}_i2s.wav" "${name}.h" "$name"
done The .h files land in your current directory - move them all into your sketch folder alongside the .ino file.
SAMPLE_RATE in the sketch, or clips will play at the wrong speed.
*.wav to match your files: *.mp3, *.m4a, *.mp4, etc.
The Sketch
Plays every clip in order, repeating forever.
clip4.wav, clip5.wav etc. to your clips folder. Then add a matching #include line and a matching entry in the clips[] array - one line in each per clip.
/*
* We stand on the shoulders of giants when we build
* with knowledge gained from others' efforts.
* That doesn't make us giants. Be humble.
* Create with care. Open source is the way.
*
* MAX98357A I2S - Multiple Clips, Auto Sequence
* -----------------------------------------------
* Plays all clips in order, repeating forever.
*
* Board: ESP32 Dev Board (38-pin) or ESP32-C3
* Amp: MAX98357A
*
* Wiring (ESP32 Dev Board):
* BCLK -> GPIO27 LRC -> GPIO14 DIN -> GPIO13
*
* Wiring (ESP32-C3):
* BCLK -> GPIO4 LRC -> GPIO5 DIN -> GPIO6
*
* Open source - MIT Licence
* Electronic Zoology - field notes from the garage
* https://electroniczoology.com/guides/how-to-play-multiple-audio-files-esp32-max98357a
*/
#include <driver/i2s.h>
#include "clip1.h"
#include "clip2.h"
#include "clip3.h"
//#include "clip4.h"
//#include "clip5.h"
//#include "clip6.h"
//#include "clip7.h"
//#include "clip8.h"
//#include "clip9.h"
//#include "clip10.h"
// ESP32 Dev Board (38-pin)
#define I2S_BCLK 27
#define I2S_LRCLK 14
#define I2S_DOUT 13
// ESP32-C3 - comment out the three lines above and uncomment these:
//#define I2S_BCLK 4
//#define I2S_LRCLK 5
//#define I2S_DOUT 6
#define SAMPLE_RATE 22050 // must match -ar value used in ffmpeg conversion
#define VOLUME 0.8f
#define BUF_SAMPLES 256
int16_t i2s_buf[BUF_SAMPLES * 2];
struct Clip {
const int16_t* data;
size_t len;
};
// Add or remove rows to match your clips.
// Also update the #include lines above.
const Clip clips[] = {
{ clip1_data, clip1_len },
{ clip2_data, clip2_len },
{ clip3_data, clip3_len },
//{ clip4_data, clip4_len },
//{ clip5_data, clip5_len },
//{ clip6_data, clip6_len },
//{ clip7_data, clip7_len },
//{ clip8_data, clip8_len },
//{ clip9_data, clip9_len },
//{ clip10_data, clip10_len },
};
const int CLIP_COUNT = sizeof(clips) / sizeof(clips[0]);
void playClip(const Clip& c) {
size_t total = c.len / sizeof(int16_t);
size_t written;
for (size_t pos = 0; pos < total; pos += BUF_SAMPLES) {
size_t count = min((size_t)BUF_SAMPLES, total - pos);
for (size_t i = 0; i < count; i++) {
int16_t s = (int16_t)(c.data[pos + i] * VOLUME);
i2s_buf[i * 2] = s;
i2s_buf[i * 2 + 1] = s;
}
i2s_write(I2S_NUM_0, i2s_buf, count * 2 * sizeof(int16_t), &written, portMAX_DELAY);
}
}
void setup() {
i2s_config_t cfg = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
.sample_rate = SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 8,
.dma_buf_len = 64,
.use_apll = false,
.tx_desc_auto_clear = true,
};
i2s_pin_config_t pins = {
.bck_io_num = I2S_BCLK,
.ws_io_num = I2S_LRCLK,
.data_out_num = I2S_DOUT,
.data_in_num = I2S_PIN_NO_CHANGE,
};
i2s_driver_install(I2S_NUM_0, &cfg, 0, NULL);
i2s_set_pin(I2S_NUM_0, &pins);
}
void loop() {
for (int i = 0; i < CLIP_COUNT; i++) {
playClip(clips[i]);
delay(500);
}
} Troubleshooting
Linker error: multiple definition of ‘audio_data’
- Two header files are using the same variable name. Name your clips
clip1.wav,clip2.wavetc. - each must have a unique name.
Sketch too large - won’t fit in flash
- Switch to the Huge APP partition scheme in Arduino IDE under Tools → Partition Scheme. This gives around 1.2MB for your sketch and clips combined.
- Drop the sample rate: re-convert your clips with a lower
-arvalue in ffmpeg and updateSAMPLE_RATEin the sketch to match. Lower rates produce smaller files. - Trim clips shorter.
Compile error: ‘clip1_data’ was not declared in this scope
- The
#includeline at the top of the sketch doesn’t match the.hfilename you generated. Check thatclip1.his in your sketch folder and that the filename in the#includematches exactly.