ElectronicZoologyfield notes from the garage
Audio • ESP32

How to play multiple
audio files from ESP32
with MAX98357A

Mode: Auto sequence - loops forever
Board: ESP32 Dev Board (38-pin) or ESP32-C3
Amp: MAX98357A
MAX98357A I2S Series - Step 3 of 4 - Multiple clips, auto sequence
✓ Confirmed Working

What You Need

Software

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 →

Flash memory budget Multiple clips add up fast. The ESP32 has around 1.2MB available under the Huge APP partition scheme - see how ESP32 flash memory works. Lower sample rates mean smaller files - run your own tests to find what sounds acceptable for your speaker and use case.

Wiring

MAX98357A PinESP32 Dev BoardESP32-C3Notes
VIN5V5V5V recommended - 3.3V works at lower volume
GNDGNDGND
LRCGPIO14GPIO5I2S word select (LRCLK)
BCLKGPIO27GPIO4I2S bit clock
DINGPIO13GPIO6I2S data out
SDNot connectedNot connectedLeave floating - amp enabled
GAINNot connectedNot connectedLeave 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

  1. Create a folder and cut each clip into it
  2. Save the converter script
  3. Run the batch - one header file per clip
  4. 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.

Reading the timestamp: In VLC the current position shows in the bottom left as HH:MM:SS. Pause at the start of your clip, write down the time, then do the same for the end.

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 clips clip1.wav, clip2.wav, clip3.wav etc. 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.

What sample rate should I use? Run your own tests - what sounds acceptable depends on your speaker and use case. 22050 is a reasonable starting point. 8000 works well on many speakers and produces much smaller files. Whatever value you use here must match SAMPLE_RATE in the sketch, or clips will play at the wrong speed.
MP3, M4A, video files? ffmpeg handles any format - just change *.wav to match your files: *.mp3, *.m4a, *.mp4, etc.

The Sketch

Plays every clip in order, repeating forever.

Adding or removing clips: add 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);
  }
}
Prefer analogue audio? The How to play audio from ESP32 with PAM8403 guide covers a DAC-based alternative - no I2S required.

Troubleshooting

Linker error: multiple definition of ‘audio_data’

  • Two header files are using the same variable name. Name your clips clip1.wav, clip2.wav etc. - 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 -ar value in ffmpeg and update SAMPLE_RATE in the sketch to match. Lower rates produce smaller files.
  • Trim clips shorter.

Compile error: ‘clip1_data’ was not declared in this scope

  • The #include line at the top of the sketch doesn’t match the .h filename you generated. Check that clip1.h is in your sketch folder and that the filename in the #include matches exactly.

Take a look at some of our other guides

Add a display How to use the SSD1306 OLED display with ESP32 → - show the current track name or clip number on screen.
Running out of flash? How ESP32 flash memory works → - understand partition schemes and your storage budget.
Trigger clips wirelessly How to use ESP-NOW to connect two ESP32s without a router → - send playback commands from another board with no router.