Lesson 6.1. Speaker. MP3 player



  • The purpose of this lesson

    Hi! Today we will learn how to play audio files of MP3 format using the built-in DAC. Write a simple player (Fig. 1).

    Figure 1. Welcome screen

    Brief theory

    Digital-to-analog Converter (DAC) – a device for converting digital (usually binary) code into an analog signal (current, voltage or charge). Digital-to-analog converters are the interface between the discrete digital world and analog signals. The signal from DAC without interpolation on the background of an ideal signal is shown in figure 2.

    Figure 2

    In M5STACK the DAC outputs correspond to the 25 and contacts 26 (Fig. 2.1).

    Note that the built-in speaker is connected to 25 pins in parallel. 26 the contact is free and can be used as a linear output. By default, both contacts are enabled, use AudioOutputI2S for configuration

    Figure 2.1

    MP3 audio of the third level, developed by a team of MPEG file format to store the audio information. MP3 is one of the most common and popular digital audio encoding formats. It is widely used in file sharing networks for evaluation download of music. The format can be played in almost all popular operating systems, on most portable audio players, and is supported by all modern models of the music centers and DVD players.

    More information on the Wiki: https://en.wikipedia.org/wiki/MP3

    The development of libraries for ESP32 and ESP8266 to work with popular audio formats, including MP3, is the user GitHub earlephilhower https://github.com/earlephilhower, reference to the library https://github.com/earlephilhower/ESP8266Audio

    List of components for the lesson

    • M5STACK;
    • USB-C cable.

    Begin!

    Step 1. Draw a sketch

    Draw a sketch of our player (Fig. 3). The name of the previous, current and next track will be displayed at the bottom of the screen. The name of the current track will be made black standard font size 3. The side tracks will be grayed out in standard size 2 font. In the center of the screen will be a time line of gray color, which will move the red label. In the upper right corner add four gray pillars that mimic the sound spectrum. The album cover will be located in the left corner.

    Figure 3. Sketch of the project

    Step 2. Logotype

    Let's use the standard graphical editor to make a logo (Fig. 3.1), which will be displayed on the screen when you turn on the device.

    Figure 3.1. The logo of the player

    Don't forget to convert and connect:

    extern unsigned char logo[];
    

    Let's draw our logo from the drawGUI () function of setup():

    void drawGUI() {
      M5.Lcd.drawBitmap(0, 0, 320, 150, (uint16_t *)logo);
      M5.Lcd.setTextColor(0x7bef);
      drawTrackList();
      while (true)
      {
        if (m5.BtnB.wasPressed())
        {
          M5.Lcd.fillRect(0, 0, 320, 240, 0x0000);
          M5.Lcd.fillRoundRect(0, 0, 320, 240, 7, 0xffff); 
          drawTrackList();
          break; 
        }
        m5.update();
      }
    }
    

    Please note that I use the design to work with the SD card-I told about it in the 5th lesson.

    void setup(){
      M5.begin();
      WiFi.mode(WIFI_OFF);
      M5.Lcd.fillRoundRect(0, 0, 320, 240, 7, 0xffff);
      M5.Lcd.setTextColor(0x7bef);
      M5.Lcd.setTextSize(2);
      M5.Lcd.drawBitmap(30, 75, 59, 59, (uint16_t *)timer_logo);
      M5.Lcd.setCursor(110, 90);
      M5.Lcd.print("STARTING...");
      M5.Lcd.setCursor(110, 110);
      M5.Lcd.print("WAIT A MOMENT");
      if (!SD.begin())
      {
        M5.Lcd.fillRoundRect(0, 0, 320, 240, 7, 0xffff);
        M5.Lcd.drawBitmap(50, 70, 62, 115, (uint16_t *)insertsd_logo);
        M5.Lcd.setCursor(130, 70);
        M5.Lcd.print("INSERT");
        M5.Lcd.setCursor(130, 90);
        M5.Lcd.print("THE TF-CARD");
        M5.Lcd.setCursor(130, 110);
        M5.Lcd.print("AND TAP");
        M5.Lcd.setCursor(130, 130);
        M5.Lcd.setTextColor(0xe8e4);
        M5.Lcd.print("POWER");
        M5.Lcd.setTextColor(0x7bef);
        M5.Lcd.print(" BUTTON"); 
        while(true);
      }
      if (!createTrackList("/"))
      {
        M5.Lcd.fillRoundRect(0, 0, 320, 240, 7, 0xffff);
        M5.Lcd.drawBitmap(30, 75, 59, 59, (uint16_t *)error_logo);
        M5.Lcd.setCursor(110, 70);
        M5.Lcd.print("ADD MP3 FILES");
        M5.Lcd.setCursor(110, 90);
        M5.Lcd.print("TO THE TF-CARD");
        M5.Lcd.setCursor(110, 110);
        M5.Lcd.print("AND TAP");
        M5.Lcd.setCursor(110, 130);
        M5.Lcd.setTextColor(0xe8e4);
        M5.Lcd.print("POWER");
        M5.Lcd.setTextColor(0x7bef);
        M5.Lcd.print(" BUTTON");
        while(true);
      }
      drawGUI();
      play('m');
    }
    

    Step 3. Adding libraries

    To use other libraries, you need to add it. You can download it in the appropriate paragraph in the section Download -> Library. In order to add a library you need to launch Arduino IDE select the menu section Sketch -> Include Library -> Add .ZIP Library... (rice. 4, 4.1).

    Figure 4. Adding a library in the Arduino IDE

    Figure 4.1. Required libraries into a ZIP-archive

    When libraries are added, you can attach them to a new project:

    #include <M5Stack.h>
    #include <WiFi.h>
    #include "AudioFileSourceSD.h"
    #include "AudioFileSourceID3.h"
    #include "AudioGeneratorMP3.h"
    #include "AudioOutputI2S.h"
    

    Step 4. Use the engine. The case for MP3

    Don't ask "what is it for?"- we'll know it later:

    AudioGeneratorMP3 *mp3;
    AudioFileSourceSD *file;
    AudioOutputI2S *out;
    AudioFileSourceID3 *id3; 
    bool playing = true;
    

    Step 5. Make a playlist

    Let's make a structure containing fields: label (path to mp3-file, the same track name), timePos - time (memory area) on which the track is paused, pointers to neighboring tracks left and right:

    struct Track
    {
      String label;
      int timePos;
      Track *left;
      Track *right;
    };
    

    Declare a dynamic list, in fact, our playlist:

    Track *trackList;
    

    To create a playlist, let's write a simple function that takes as an argument the path where MP3 files are located. If nothing is found, the function returns false:

    bool createTrackList(String dir) {
      int i = 0;
      File root = SD.open(strToChar(dir));
      if (root)
      {
        while (true)
        {
          File entry =  root.openNextFile();
          if (!entry) break;
          if (!entry.isDirectory())
          {
            String ext = parseString(cntChar(entry.name(), '.'), '.', entry.name());
            if (ext == "mp3") 
            {
              i++;
              Track *tmp = new Track;
              tmp->label = entry.name();
              tmp->timePos = 0;
              tmp->right = tmp; 
              if (trackList == NULL)
              {
                tmp->left = tmp;
                trackList = tmp;
              }
              else
              {
                tmp->left = trackList;
                trackList->right = tmp;
                trackList = trackList->right;
              }
            }
          }
          entry.close();
        }
        if (i > 1)
        {
          do
          {
            trackList = trackList->left;
          } while(trackList != trackList->left);
        }
        root.close();
      }
      if (i > 0)
        return true;
      return false;
    }
    

    Note that the leftmost and rightmost tracks are self-contained, not NULL

    Step 6. Tracklist drawing is easy!

    String labelCut(int from, int to, String str) {    
      String tmp = str.substring(1, posChar(str, '.'));
      if (str.length() > to)
        tmp = tmp.substring(from, to);
      return tmp;
    }
    
    void drawTrackList() {
      M5.Lcd.fillRect(0, 130, 320, 75, 0xffff);
    
      if (trackList->left != trackList)
      {
        M5.Lcd.setTextSize(2);
        M5.Lcd.setTextColor(0x7bef);
        M5.Lcd.setCursor(10, 130);
        M5.Lcd.print(labelCut(0, 22, (trackList->left)->label));
      }
     
      M5.Lcd.setTextSize(3); 
      M5.Lcd.setTextColor(0x0000);
      M5.Lcd.setCursor(10, 155);
      M5.Lcd.print(labelCut(0, 16, (trackList->label)));
    
      if (trackList->right != trackList)
      {
        M5.Lcd.setTextSize(2);
        M5.Lcd.setTextColor(0x7bef);
        M5.Lcd.setCursor(10, 185);
        M5.Lcd.print(labelCut(0, 22, (trackList->right)->label));
      }
    }
    

    Step 7. Timeline

    unsigned long drawTimeline_previousMillis = 0;
    void drawTimeline() {
      currentMillis = millis();
      if (currentMillis - drawTimeline_previousMillis > 250)
      {
        int x = 30;
        int y = 110;
        int width = 260;
        int heightLine = 2;
        int heightMark = 20;
        int widthMark = 2;
        int yClear = y - (heightMark / 2);
        int wClear = width + (widthMark / 2);
        
        drawTimeline_previousMillis = currentMillis;
        M5.Lcd.fillRect(x, yClear, wClear, heightMark, 0xffff);
        M5.Lcd.fillRect(x, y, width, heightLine, 0x7bef);
        int size_ = id3->getSize();
        int pos_ = id3->getPos();
        int xPos = x + ((pos_ * (width - (widthMark / 2))) / size_);
        M5.Lcd.fillRect(xPos, yClear, widthMark, heightMark, 0xe8e4);
      }
    }
    

    Step 8. Spectrum emulator

    unsigned long genSpectrum_previousMillis = 0;
    void genSpectrum() {
      currentMillis = millis();
      if (currentMillis - genSpectrum_previousMillis > 100)
      {
        genSpectrum_previousMillis = currentMillis;
        drawSpectrum(random(0,101), random(0,101), random(0,101), random(0,101));
      }
    }
    
    void drawSpectrum(int a, int b, int c, int d) { // %
      int x = 195;
      int y = 30;
      int padding = 10;
      int height = 30;
      int width = 15;
    
      int aH = ((a * height) / 100);
      int aY = y + (height - aH);
      M5.Lcd.fillRect(x, y, width, height, 0xffff);
      M5.Lcd.fillRect(x, aY, width, aH, 0x7bef); //0xe8e4
      
      int bH = ((b * height) / 100);
      int bY = y + (height - bH);
      int bX = x + width + padding;
      M5.Lcd.fillRect(bX, y, width, height, 0xffff);
      M5.Lcd.fillRect(bX, bY, width, bH, 0x7bef); //0xff80
    
      int cH = ((c * height) / 100);
      int cY = y + (height - cH);
      int cX = bX + width + padding;
      M5.Lcd.fillRect(cX, y, width, height, 0xffff);
      M5.Lcd.fillRect(cX, cY, width, cH, 0x7bef);//0x2589
    
      int dH = ((d * height) / 100);
      int dY = y + (height - dH);
      int dX = cX + width + padding;;
      M5.Lcd.fillRect(dX, y, width, height, 0xffff);
      M5.Lcd.fillRect(dX, dY, width, dH, 0x7bef);//0x051d
    }
    

    Step 9. Work with engine MP3

    In order to play MP3's from a playlist write a function Play(char), which as argument takes the instruction. If the argument is set to' l', the pointer in the dynamic list will be shifted to the left and the track will start playing on the left. Similarly for the track on the right. If the argument is set to' m', it means play back silence. If you pass any other argument, it would mean 't' (this) - play current, i.e. the one pointed to by the pointer.

    bool play(char dir) {
      switch (dir)
      {
        case 'r':
          if (trackList == trackList->right) return false;
          trackList->timePos = 0;
          trackList = trackList->right;       
        break;
        case 'l':
        if (trackList == trackList->left) return false;
          trackList->timePos = 0;
          trackList = trackList->left;
        break;
        case 'm': // mute
          delete file;
          delete out;
          delete mp3;
          mp3 = NULL;
          file = NULL;
          out = NULL;
          file = new AudioFileSourceSD("/");
          id3 = new AudioFileSourceID3(file);
          out = new AudioOutputI2S(0, 1);
          out->SetOutputModeMono(true);
          mp3 = new AudioGeneratorMP3();
          mp3->begin(id3, out);
          playing = false;
        return true;
        default:
          if (playing)
          {
            trackList->timePos = id3->getPos();
            play('m');
            return true;
          }
        break;
      }
      drawCover();
      mp3->stop();
      delete file;
      delete out;
      delete mp3;
      mp3 = NULL;
      file = NULL;
      out = NULL;
      file = new AudioFileSourceSD(strToChar(trackList->label));
      id3 = new AudioFileSourceID3(file);
      id3->seek(trackList->timePos, 1);
      out = new AudioOutputI2S(0, 1);
      out->SetOutputModeMono(true);
      mp3 = new AudioGeneratorMP3();
      mp3->begin(id3, out);
      playing = true;
      return true;
    }
    

    Step 10. Loop

    void loop(){ 
      if (m5.BtnA.wasPressed())
      {
        play('l');
        drawTrackList();
      }
    
      if (m5.BtnB.wasPressed())
      {
        play('t');
      }
    
      if (m5.BtnC.wasPressed())
      {
        play('r');
        drawTrackList();
      }
    

    the design below is subject to little change. We will add playback in order of play ('r'), pause control of playing, rendering of dynamic data of genSpectrum () and drawTimeline():

      if (playing)
      {
        if (mp3->isRunning())
        {
          if (!mp3->loop())
          {
            mp3->stop();
            playing = false;
            play('r');
            drawTrackList();
          }
        }
        else
        {
          delay(1000);
        }
        genSpectrum();
        drawTimeline();
      }
      m5.update();
    }
    

    Step 11. LAUNCH!

    Everything works and looks good enough, the only thing is the crackle when switching songs. The cause has not yet been set.

    Figure 5. Playback screen

    Homework

    • Task 1 level of difficulty: add control of the existence of the JPG files of the album artwork on TF card. If the album cover is missing, add a draft instead (you can download in Download - > Images - > HomeWork1);
    • Task 2 difficulty levels: add a running line for long track titles.
    • Task 3 difficulty level: use ID3 to extract the album cover from the MP3 file to avoid using external JPG files;
    • Task 4 difficulty levels: instead of emulating the spectrum, implement a fast Fourier transform. The first pillar is 100 Hz, the second - 600 Hz, the third - 1500 Hz, the fourth 3000 Hz.

    Download



  • @dimi Hi Dimi,
    Another excellent lesson..... look forward to testing this and also testing the line out capability.



  • @Dimi great stuff! you are a true wizard!

    question - how to add headphone jack to m5? is there an easy and elegant solution for this? perhaps with the line out stuff you pointed us to?



  • @m5dude thanks! you can use alt text



  • @dimi
    where do the pins connect to? GND and 25?

    if i wanted to connect a headphone to M5 I just do the same thing but with the headphone jack input?



  • This post is deleted!


  • @m5dude @m5dude use 26, 25 is occupied by built-in speaker. Yes, use ordinary headphones and everything will work. just be careful not to spoil your hearing!



  • He @Dimi , this is a great tutorial on creating a fantastic little music player!
    I'm looking to use my m5Stack in my car, connected to the aux in of my stereo.
    I'd also like to incorporate a mode to stream audio via Bluetooth from my phone, but that'll take a fair bit of further homework to get working 😂



  • Hi @Dimi, thanks for the amazing tutorial.

    Can I ask a question?
    I want to make an m5stack program that would play an mp3 file if I send a trigger via UDP, and would stop if I send a different trigger via UDP.
    I followed some of your code.

    But when I sent the trigger, the mp3 only played the first second and kept repeating the same time position. It wouldn't play the entire mp3 song.

    This is the code that I used:


    #include <M5Stack.h>
    #include <WiFi.h>
    #include <WiFiUdp.h>
    #include "AudioFileSourceSD.h"
    #include "AudioFileSourceID3.h"
    #include "AudioGeneratorMP3.h"
    #include "AudioOutputI2S.h"
    #define N 1024

    bool playing = true;
    AudioGeneratorMP3 *mp3;
    AudioFileSourceSD *file;
    AudioOutputI2S *out;
    AudioFileSourceID3 *id3;

    const char* ssid = "wifiname";
    const char* password = "wifipassword";
    const int port = 5555;

    // The udp library class
    WiFiUDP udp;

    void print_wifi_state(){
    M5.Lcd.clear(BLACK); // clear LCD
    M5.Lcd.setTextColor(YELLOW);
    M5.Lcd.setCursor(3, 3);
    M5.Lcd.println("");
    M5.Lcd.println("WiFi connected.");
    M5.Lcd.print("IP address: ");
    M5.Lcd.println(WiFi.localIP());
    M5.Lcd.print("Port: ");
    M5.Lcd.println(port);
    }

    void setup_wifi(){
    M5.Lcd.setTextColor(RED);
    M5.Lcd.setTextSize(2);
    M5.Lcd.setCursor(3, 10);
    M5.Lcd.print("Connecting to ");
    M5.Lcd.println(ssid);

    // setup wifi
    WiFi.mode(WIFI_STA); // WIFI_AP, WIFI_STA, WIFI_AP_STA or WIFI_OFF
    WiFi.begin(ssid, password);
    // WiFi.begin();

    // Connecting ..
    while (WiFi.status() != WL_CONNECTED) {
    delay(100);
    M5.Lcd.print(".");
    }

    // print state
    print_wifi_state();

    udp.begin(port);
    

    }

    void setup() {
    M5.begin();
    M5.Speaker.setVolume(5);
    play('m');
    // setup wifi
    setup_wifi();

    }

    bool play(char dir){
    switch(dir)
    {
    case 'm':
    delete file;
    delete out;
    delete mp3;
    mp3 = NULL;
    file = NULL;
    out = NULL;
    file = new AudioFileSourceSD("/");
    id3 = new AudioFileSourceID3(file);
    out = new AudioOutputI2S(0, 1);
    out->SetOutputModeMono(true);
    mp3 = new AudioGeneratorMP3();
    mp3->begin(id3, out);
    playing = false;
    return true;
    default:
    if(playing){
    play('m');
    return true;
    }
    break;
    }
    mp3->stop();
    delete file;
    delete out;
    delete mp3;
    mp3 = NULL;
    file = NULL;
    out = NULL;
    file = new AudioFileSourceSD("/RainDrizzle.mp3");
    id3 = new AudioFileSourceID3(file);
    id3->seek(trackList->timePos, 1);
    out = new AudioOutputI2S(0, 1);
    out->SetOutputModeMono(true);
    mp3 = new AudioGeneratorMP3();
    mp3->begin(id3, out);
    playing = true;

    return true;
    }

    void loop() {
    char packetBuffer[N];
    int packetSize = udp.parsePacket();

    // get packet
    if (packetSize){

    int len = udp.read(packetBuffer, packetSize);
    
    if (len > 0){
      packetBuffer[len] = '\0'; // end
    }
    

    }

    if(strcmp(packetBuffer,"start")==0){
    // print param
    M5.Lcd.clear(BLACK);
    M5.Lcd.setCursor(3, 3);
    M5.Lcd.setTextColor(GREEN);
    M5.Lcd.println(packetBuffer);
    
    play('t');
    
    }
    
    if(strcmp(packetBuffer,"stop")==0){
    M5.Lcd.clear(BLACK);
    M5.Lcd.setCursor(3, 3);
    M5.Lcd.setTextColor(GREEN);
    M5.Lcd.println(packetBuffer);
    play('m');
    }
    
    if(playing){
      if(mp3->isRunning()){
        if(!mp3->loop()){
          mp3->stop();
          playing = false;
        }
      }
      else{
        delay(1000);
      }
    

    }
    M5.update();
    }


    Thank you in advance.