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
-
Video demonstration (YouTube): https://youtu.be/D6pnG0Ha0yw
-
Source codes (GitHub): https://github.com/dsiberia9s/mp3-player-m5stack
-
Image converters (Yandex Disk): https://yadi.sk/d/dOj_EyU_3TfhWV
-
Images (Yandex Disk): https://yadi.sk/d/_wGruXC73TfhZ6
-
Library (Yandex Disk): https://yadi.sk/d/GX--lQ3v3Tfhcf
-
TF card files (Yandex Disk): https://yadi.sk/d/15YnN5tC3Tfhg5
-
@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
-
@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! -
-
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 1024bool 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.