2.9 MP3 Music Player

A full-featured music player with playback controls and volume adjustment.

Overview

This example creates a complete music player UI. It scans the SD card’s /music/ folder for MP3 files, displays the current track title, and provides play/pause, stop, skip, and volume controls — all driven by the touchscreen.

What You’ll Learn

  • Scanning an SD card for MP3 files

  • Building a music player UI with transport buttons

  • Integrating I2S audio playback with LVGL state management

  • Using a slider widget for volume control

Before You Begin

Complete the setup steps in Preparation before continuing.

Note

Use the recommended Arduino IDE settings in Recommended Board Settings unless this lesson says otherwise.

Open the example sketch from the code package:

LAFVIN-ESP32-S3-Multimedia-Kit-main/2.LVGL/09_LVGL_Music/09_LVGL_Music.ino

Note

Before uploading, create a /music/ folder on your SD card and copy one or more MP3 files into it.

Signal

ESP32-S3

I2S BCLK

GPIO 42

I2S LRC

GPIO 14

I2S DOUT

GPIO 41

SD CMD

GPIO 38

SD CLK

GPIO 39

SD D0

GPIO 40

Upload and Test

  1. Insert a Micro SD card with MP3 files in the /music/ folder.

  2. Connect the board with a USB Type-C data cable.

  3. Open 09_LVGL_Music.ino in Arduino IDE.

  4. In the Tools menu, select ESP32S3 Dev Module as the board and choose the correct serial port.

  5. Click Upload.

  6. The music player screen shows:

    • The current track title at the top

    • Transport buttons: prev, play/pause, stop, next

    • A volume slider with numeric readout

    The player automatically begins playing the first MP3 file it finds. Use the touchscreen to control playback.

LVGL Music Player

How the Code Works

The music player logic is mainly implemented in music_ui.cpp.

Track Loading

The music_ui_prepare_first_track() function scans the linked list populated from /music/ and primes the title label with the first filename without auto-playing it. When the user taps play, music_ui_play_index() constructs the full SD path and calls s_audio.connecttoFS() to start decoding.

static bool music_ui_play_index(int index) {
  if (s_music_count <= 0) {
    s_track_loaded = false;
    s_music_paused = false;
    music_ui_set_title_text(NULL);
    music_ui_refresh_controls();
    return false;
  }

  if (index < 1) {
    index = s_music_count;
  } else if (index > s_music_count) {
    index = 1;
  }

  char *file_name = list_find_node(list_music, index);
  if (file_name == NULL) {
    return false;
  }

  s_audio.stopSong();

  String file_path = String(MUSIC_FOLDER) + "/" + file_name;
  s_audio.connecttoFS(SD_MMC, file_path.c_str());
  s_music_index = index;
  s_track_loaded = true;
  s_music_paused = false;

  music_ui_set_title_text(file_name);
  music_ui_refresh_controls();
  return true;
}

The index is 1-based and wraps around at both ends — going below 1 wraps to s_music_count, and exceeding s_music_count wraps to 1.

Play/Pause & Transport Controls

The music_ui_toggle_play_pause() function implements a three-state toggle: if no track is loaded it starts the current index, otherwise it calls s_audio.pauseResume() and flips the s_music_paused flag. The middle button icon swaps between play and pause symbols accordingly.

static void music_ui_toggle_play_pause() {
  if (s_music_count <= 0) {
    return;
  }

  if (!s_track_loaded) {
    music_ui_play_index(s_music_index > 0 ? s_music_index : 1);
    return;
  }

  s_audio.pauseResume();
  s_music_paused = !s_music_paused;
  music_ui_refresh_controls();
}

The previous (music_ui_play_previous) and next (music_ui_play_next) functions decrement or increment the index with wrap-around, then call music_ui_play_index(). The stop button calls music_ui_stop_current(), which invokes s_audio.stopSong() and resets s_track_loaded.

Volume Control

The volume slider ranges from 0 to 21 and updates the Audio library in real time. A numeric label below the slider shows the current value.

  lv_slider_set_range(ui->volume_slider, 0, 21);
  lv_slider_set_value(ui->volume_slider, kDefaultVolume, LV_ANIM_OFF);

static void on_volume_slider_changed(lv_event_t *event) {
  if (lv_event_get_code(event) != LV_EVENT_VALUE_CHANGED) {
    return;
  }

  int volume = lv_slider_get_value(g_music_ui.volume_slider);
  s_audio.setVolume(volume);
  music_ui_update_volume_label(volume);
}

Audio Loop & Auto-Advance

The music_ui_loop() function, called from the sketch’s loop(), feeds the audio decoder with s_audio.loop() on every iteration. When a track reaches its end, the audio_eof_mp3() callback sets s_play_next_pending = true, and the loop advances to the next track outside the audio callback context.

void music_ui_loop(void) {
  s_audio.loop();

  if (s_play_next_pending) {
    s_play_next_pending = false;
    music_ui_play_next();
  }
}

void audio_eof_mp3(const char *info) {
  Serial.print("eof_mp3     ");
  Serial.println(info);
  s_play_next_pending = true;
}

In the provided sketch:

  • The Audio library is initialised with I2S pinout (BCLK=42, LRC=14, DOUT=41) in music_ui_audio_init()

  • Transport buttons are lv_img objects with transparent backgrounds — only the icon images are visible

  • music_ui_refresh_controls() enables or dims all transport buttons based on whether s_music_count > 0

Try This Next

  • Add more MP3 files to the /music/ folder and see the track list grow

  • Display the track duration or current playback position

  • Add a shuffle or repeat mode button