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