ESP32 MP3 Player + Online Radio + Snake Game! SquareLine Studio + LVGL
Discover how to build a full-featured ESP32 project with MP3 playback, internet radio, and games-all controlled with a stylish touchscreen interface.
Last Updated on August 6, 2025 by Engr. Shahzada Fahad
Table of Contents
ESP32 MP3 Player:
ESP32 MP3 Player + Online Radio + Snake Game! SquareLine Studio + LVGL – Have you ever wondered how to build your own MP3 player or even a snake game; all using just one board?
What I am about to show you will take your ESP32 projects to the next level!
This is the second article in our series on the MaTouch ESP32-S3 Parallel TFT with Touch 5” display.
In the first article, we explored the technical specs, discussed all the onboard interfaces, and went through the setup of the LVGL graphics library. But in that article, I didn’t cover the sound playback, and I received so many messages asking how to play MP3 files.
So today… we are doing exactly that!
In the first example, we will learn how to play MP3 files stored on a microSD card using the ESP32-S3. We will create a simple MP3 player; with Play, Stop, Next, and Previous buttons right on the display.
We also display the song name at the bottom. This is a basic project; but it’s super useful for beginners, especially for students and hobbyists who want to learn how to play sound files from SD card.
The goal of this project is simple; teach boys and girls how sound playback works on ESP32-S3, with real hardware and an interactive UI.
In the second example, we make it even better.
We use a dropdown list to show all the available MP3 files on the SD card.
You just select a file, and it plays immediately! You don’t need Next or Previous buttons. You can also adjust the volume with a slider and stop the song anytime.
The selected song name is shown clearly at the top. This is a clean and easy-to-use interface, perfect for learning UI design and SD card integration.
In the third example, we do online streaming by accessing real Internet radio stations.
Yes, we stream songs directly from the internet using WiFi; no SD card needed!
The UI is similar to the second example; but this time, I have added a beautiful background image to make it look futuristic. You can design your own background skins; and it makes a big difference in how your player looks.
I have explained this background image technique before in my smartwatch articles; so you already know how powerful that is.
Finally; just to make things fun; we create a Snake Game!
This one uses buttons on the right side to control the snake’s movement; up, down, left, and right.
You can also pause the game, stop it, and even see the score right on the screen.
The gameplay area is clearly separated with a line, so your buttons never get in the way.
It’s a super fun project and it shows just how flexible the ESP32-S3 with this display can be.
Now, why should you watch this full video and try these 4 examples?
Because once you do, you will have mastered audio playback, UI control, dropdown selection, internet streaming, and game logic; all in one powerful ESP32 project.
Whether you are a student, hobbyist, or even a teacher; this video gives you everything you need to build your own media devices and interactive UIs.
And trust me, it wasn’t easy.
The hardest part of this entire project was finding the correct audio library for playing sound.
There are so many libraries on GitHub; but most of them don’t work or are outdated.
I had to spend hours testing different libraries, just to find the right one that supports both SD card and internet streaming.
But the good news is; I have done all that for you, and now you can just follow along and make it work on the first try.
And let me tell you, you can download all the codes and resources from my Patreon page.
Before we get into the MP3 player and online streaming examples, let’s first talk about how the sound is actually played on this board.
This display has a built-in I2S digital audio amplifier, the MAX98357A.
The MAX98357A is a small and powerful digital-to-analog audio converter. It takes I2S audio data from the ESP32 and converts it into clear analog audio, which you can hear through a speaker.
So, even if you don’t use this exact MaTouch display, you can still build your own audio projects using the MAX98357A breakout board. You can find these tiny modules online; very cheap; and they work perfectly with any ESP32 board.
Simply connect it to the I2S pins of your ESP32, load the code, and you can play MP3 files from SD card or even stream audio online. It’s that simple.
Now listen carefully; because this is very important.
To play MP3 files from the SD card, or to stream online radio stations, you must use the correct audio library. I spent hours searching on GitHub, trying different libraries… and most of them didn’t work. Either they didn’t compile, or they didn’t support I2S audio properly.
Finally, I found the right one and this is what I am using in all the examples.
ESP32_audioI2S_master-2.0.0
Now let me show you how to install the library step-by-step.
While your Arduino IDE is open, go to the top menu and click on:
Sketch → Include Library → Add .ZIP Library
A file browser will open.
Now, browse to the location where you downloaded the library ZIP file.
Select the zip folder, and click the Open button.
If the library is not already installed, Arduino will add it automatically to your libraries.
But in my case, I have already installed this library, so I am just going to click on the No button to skip it.
Example 1: Basic ESP32 MP3 Player from an SD Card
MP3 Player UI – Built with SquareLine Studio:
As you can see, I have designed a very simple MP3 Player UI – it’s made using just 4 buttons and 1 label.
This is kept intentionally simple so that beginners can easily follow along and start playing sound files without any confusion.
Now, if you want to create more advanced and stylish UIs, I have already made many tutorials on SquareLine Studio – so feel free to watch those videos if you are interested in leveling up your UI design skills.
The label simply displays the file name of the currently playing song.
Now let’s talk about the 4 buttons.
I have assigned an event to each button in SquareLine Studio:
If I click on the Stop button and go to the Inspector tab, then scroll down —
You will see I have added an event. The trigger type is set to RELEASED, and the function name is StopFun.
So, whenever the stop button is pressed, this function will be called in the code.
Don’t forget to check the “Do not export” checkbox, so SquareLine Studio doesn’t auto-generate any default code for it.
Next Button:
It calls the NextFun() function.
Play Button:
It calls the PlayFun() function.
Previous Button:
It calls the PreviousFun() function.
This makes the UI fully functional, and with just these four buttons, we have a working MP3 player that can play, stop, and switch between songs; and also show the song name.
Next, you need to download the following code, or if you want the complete package including the SquareLine Studio project, UI assets, and all necessary resources, you can download the full project folder from my Patreon page. This makes it super easy for you to get started; no need to set up everything from scratch. Anyways, let’s upload this code.
And one more thing, make sure to save the following code with the name touch.h in the same folder where your Arduino.ino file is located.
touch code:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include <Wire.h> #include "TAMC_GT911.h" #define TOUCH_GT911 #define TOUCH_GT911_SCL 18 #define TOUCH_GT911_SDA 17 #define TOUCH_GT911_INT -1 #define TOUCH_GT911_RST 38 #define TOUCH_GT911_ROTATION ROTATION_NORMAL TAMC_GT911 ts = TAMC_GT911(TOUCH_GT911_SDA, TOUCH_GT911_SCL, TOUCH_GT911_INT, TOUCH_GT911_RST, 800, 480); void touch_init() { pinMode(TOUCH_GT911_RST, OUTPUT); digitalWrite(TOUCH_GT911_RST, LOW); delay(500); digitalWrite(TOUCH_GT911_RST, HIGH); delay(500); ts.begin(); ts.setRotation(ROTATION_NORMAL); } void TouchonInterrupt(void) { ts.isTouched = true; } |
Basic ESP32 MP3 Player Code:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
#include <lvgl.h> #include <Arduino_GFX_Library.h> #include "touch.h" #include "ui.h" #include <SD_MMC.h> #include "Audio.h" #define GFX_BL 10 #define I2S_DOUT 19 #define I2S_BCLK 20 #define I2S_LRCK 2 #define PIN_SD_CMD 11 #define PIN_SD_CLK 12 #define PIN_SD_D0 13 #define TOUCH_RST 38 // Display buffer static const uint16_t screenWidth = 800; static const uint16_t screenHeight = 480; static lv_disp_draw_buf_t draw_buf; static lv_color_t buf1[screenWidth * 40]; // Double buffer for less flickering static lv_color_t buf2[screenWidth * 40]; Arduino_ESP32RGBPanel *bus = new Arduino_ESP32RGBPanel( GFX_NOT_DEFINED, GFX_NOT_DEFINED, GFX_NOT_DEFINED, 40, 41, 39, 42, 45, 48, 47, 21, 14, 5, 6, 7, 15, 16, 4, 8, 3, 46, 9, 1 ); Arduino_RPi_DPI_RGBPanel *gfx = new Arduino_RPi_DPI_RGBPanel( bus, 800, 0, 8, 4, 8, 480, 0, 8, 4, 8, 1, 16000000, true ); Audio audio; TaskHandle_t audioTaskHandle; String mp3Files[50]; int totalFiles = 0; int currentFile = 0; bool isPlaying = false; // === Display Flush === void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p) { uint32_t w = (area->x2 - area->x1 + 1); uint32_t h = (area->y2 - area->y1 + 1); gfx->draw16bitRGBBitmap(area->x1, area->y1, (uint16_t*)&color_p->full, w, h); lv_disp_flush_ready(disp); } // === Touch Read (Y Axis Flipped) === void my_touchpad_read(lv_indev_drv_t *indev_driver, lv_indev_data_t *data) { ts.read(); if (ts.isTouched) { data->state = LV_INDEV_STATE_PR; // Invert Y axis to correct touch location data->point.x = ts.points[0].x; data->point.y = screenHeight - ts.points[0].y; } else { data->state = LV_INDEV_STATE_REL; } } // === Audio Task (Runs on Core 1) === void TaskAudio(void *param) { while (true) { audio.loop(); vTaskDelay(1); } } // === Play MP3 File === void playMP3(const String &filename) { Serial.println(">> Playing: " + filename); audio.stopSong(); delay(150); // Short settle time audio.connecttoFS(SD_MMC, filename.c_str()); isPlaying = true; // Remove path ("/") from displayed filename String displayName = filename; displayName.replace("/", ""); // Update label text on UI lv_label_set_text(ui_lblsongname, displayName.c_str()); } // === Scan MP3 Files === void scanSD() { File root = SD_MMC.open("/"); while (File file = root.openNextFile()) { String name = file.name(); if (name.endsWith(".mp3") && totalFiles < 50) { mp3Files[totalFiles++] = name; Serial.println("MP3 Found: " + name); } } } // === Button Events === void StopFun(lv_event_t *e) { Serial.println(">> Stop"); audio.stopSong(); isPlaying = false; } void PlayFun(lv_event_t *e) { Serial.println(">> Play"); if (!isPlaying && totalFiles > 0) { playMP3(mp3Files[currentFile]); } } void NextFun(lv_event_t *e) { Serial.println(">> Next"); if (totalFiles == 0) return; currentFile = (currentFile + 1) % totalFiles; playMP3(mp3Files[currentFile]); } void PreviousFun(lv_event_t *e) { Serial.println(">> Previous"); if (totalFiles == 0) return; currentFile = (currentFile - 1 + totalFiles) % totalFiles; playMP3(mp3Files[currentFile]); } // === Setup === void setup() { Serial.begin(115200); lv_init(); touch_init(); // Turn ON backlight pinMode(GFX_BL, OUTPUT); digitalWrite(GFX_BL, LOW); gfx->begin(); gfx->fillScreen(BLACK); // LVGL Display Buffer lv_disp_draw_buf_init(&draw_buf, buf1, buf2, screenWidth * 40); static lv_disp_drv_t disp_drv; lv_disp_drv_init(&disp_drv); disp_drv.hor_res = screenWidth; disp_drv.ver_res = screenHeight; disp_drv.flush_cb = my_disp_flush; disp_drv.draw_buf = &draw_buf; lv_disp_drv_register(&disp_drv); static lv_indev_drv_t indev_drv; lv_indev_drv_init(&indev_drv); indev_drv.type = LV_INDEV_TYPE_POINTER; indev_drv.read_cb = my_touchpad_read; lv_indev_drv_register(&indev_drv); // GUI Init ui_init(); // SD Card Init SD_MMC.setPins(PIN_SD_CLK, PIN_SD_CMD, PIN_SD_D0); if (!SD_MMC.begin("/sdcard", true, true)) { Serial.println("SD Card Mount Failed"); } else { scanSD(); Serial.printf("Found %d MP3 Files\n", totalFiles); } // Audio Init audio.setPinout(I2S_BCLK, I2S_LRCK, I2S_DOUT); audio.setVolume(18); // Start audio task on core 1 xTaskCreatePinnedToCore(TaskAudio, "TaskAudio", 4096, NULL, 1, &audioTaskHandle, 1); // Attach SquareLine Buttons lv_obj_add_event_cb(ui_Button1, PreviousFun, LV_EVENT_CLICKED, NULL); lv_obj_add_event_cb(ui_Button2, PlayFun, LV_EVENT_CLICKED, NULL); lv_obj_add_event_cb(ui_Button3, NextFun, LV_EVENT_CLICKED, NULL); lv_obj_add_event_cb(ui_Button4, StopFun, LV_EVENT_CLICKED, NULL); } // === Loop === void loop() { lv_timer_handler(); // GUI handler (runs on Core 0) delay(1); // Minimal delay to keep LVGL responsive } |
Example 2: Upgrading with a Dropdown Song List
In this second example, I have simplified the MP3 player UI to keep things clean and beginner-friendly. Instead of using Play, Next, and Previous buttons like before, I have added a dropdown list that automatically loads all the .mp3 files from the SD card.
You can simply select any song from the list, and it will start playing instantly. At the top of the display, there’s a label that shows the name of the currently playing file. There’s also a Stop button to stop playback, and a volume slider to adjust the sound level in real-time. This time, I didn’t assign events from SquareLine Studio; all interactions are handled directly through code, giving you more flexibility and control.
ESP32 MP3 Player with Dropdown List – Code:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
#include <lvgl.h> #include <Arduino_GFX_Library.h> #include "touch.h" #include "ui.h" #include <SD_MMC.h> #include "Audio.h" #define GFX_BL 10 #define I2S_DOUT 19 #define I2S_BCLK 20 #define I2S_LRCK 2 #define PIN_SD_CMD 11 #define PIN_SD_CLK 12 #define PIN_SD_D0 13 static const uint16_t screenWidth = 800; static const uint16_t screenHeight = 480; static lv_disp_draw_buf_t draw_buf; static lv_color_t buf[screenWidth * screenHeight / 10]; Arduino_ESP32RGBPanel *bus = new Arduino_ESP32RGBPanel( GFX_NOT_DEFINED, GFX_NOT_DEFINED, GFX_NOT_DEFINED, 40, 41, 39, 42, 45, 48, 47, 21, 14, 5, 6, 7, 15, 16, 4, 8, 3, 46, 9, 1 ); Arduino_RPi_DPI_RGBPanel *gfx = new Arduino_RPi_DPI_RGBPanel( bus, 800, 0, 8, 4, 8, 480, 0, 8, 4, 8, 1, 16000000, true ); Audio audio; TaskHandle_t audioTaskHandle; String mp3Files[50]; int totalFiles = 0; bool isPlaying = false; bool uiNeedsUpdate = false; // === Audio Task === void TaskAudio(void *param) { while (true) { audio.loop(); vTaskDelay(1); } } // === Play MP3 File === void playMP3(const String &filename) { Serial.println(">> Playing: " + filename); audio.stopSong(); delay(150); audio.connecttoFS(SD_MMC, filename.c_str()); isPlaying = true; String displayName = filename; displayName.replace("/", ""); lv_label_set_text(ui_lblsongname, displayName.c_str()); } // === Stop Button Event === void StopFun(lv_event_t *e) { Serial.println(">> Stop"); audio.stopSong(); isPlaying = false; lv_label_set_text(ui_lblsongname, "Stopped"); } // === Volume Slider Event === void VolumeChanged(lv_event_t *e) { int vol = lv_slider_get_value(ui_SliderVolume); audio.setVolume(vol); // 0 to 21 Serial.printf("Volume: %d\n", vol); } // === Dropdown Song Select === void DropdownSelect(lv_event_t *e) { uint16_t index = lv_dropdown_get_selected(ui_Dropdownsongs); if (index < totalFiles) { playMP3(mp3Files[index]); } } // === Display Flush === void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p) { uint32_t w = (area->x2 - area->x1 + 1); uint32_t h = (area->y2 - area->y1 + 1); gfx->draw16bitRGBBitmap(area->x1, area->y1, (uint16_t *)&color_p->full, w, h); lv_disp_flush_ready(disp); } // === Touch Read (Inverted) === void my_touchpad_read(lv_indev_drv_t *indev_driver, lv_indev_data_t *data) { ts.read(); if (ts.isTouched) { data->state = LV_INDEV_STATE_PR; data->point.x = screenWidth - ts.points[0].x; data->point.y = screenHeight - ts.points[0].y; } else { data->state = LV_INDEV_STATE_REL; } } // === Scan SD Card for MP3s === void scanSD() { File root = SD_MMC.open("/"); String list = ""; while (File file = root.openNextFile()) { String name = file.name(); if (name.endsWith(".mp3") && totalFiles < 50) { mp3Files[totalFiles++] = name; Serial.println("MP3 Found: " + name); name.replace("/", ""); list += name; list += "\n"; } } if (totalFiles > 0) { list.remove(list.length() - 1); // Remove last newline lv_dropdown_set_options(ui_Dropdownsongs, list.c_str()); } } // === Setup === void setup() { Serial.begin(115200); lv_init(); touch_init(); pinMode(GFX_BL, OUTPUT); digitalWrite(GFX_BL, LOW); gfx->begin(); gfx->fillScreen(BLACK); lv_disp_draw_buf_init(&draw_buf, buf, NULL, screenWidth * screenHeight / 10); static lv_disp_drv_t disp_drv; lv_disp_drv_init(&disp_drv); disp_drv.hor_res = screenWidth; disp_drv.ver_res = screenHeight; disp_drv.flush_cb = my_disp_flush; disp_drv.draw_buf = &draw_buf; disp_drv.sw_rotate = 1; disp_drv.rotated = LV_DISP_ROT_180; lv_disp_drv_register(&disp_drv); static lv_indev_drv_t indev_drv; lv_indev_drv_init(&indev_drv); indev_drv.type = LV_INDEV_TYPE_POINTER; indev_drv.read_cb = my_touchpad_read; lv_indev_drv_register(&indev_drv); ui_init(); // SD Card Init SD_MMC.setPins(PIN_SD_CLK, PIN_SD_CMD, PIN_SD_D0); if (!SD_MMC.begin("/sdcard", true, true)) { Serial.println("SD Card Mount Failed"); } else { scanSD(); Serial.printf("Found %d MP3 Files\n", totalFiles); } // Audio Init audio.setPinout(I2S_BCLK, I2S_LRCK, I2S_DOUT); audio.setVolume(0); xTaskCreatePinnedToCore(TaskAudio, "TaskAudio", 4096, NULL, 1, &audioTaskHandle, 1); // Event Binding lv_obj_add_event_cb(ui_ButtonStop, StopFun, LV_EVENT_CLICKED, NULL); lv_obj_add_event_cb(ui_SliderVolume, VolumeChanged, LV_EVENT_VALUE_CHANGED, NULL); lv_obj_add_event_cb(ui_Dropdownsongs, DropdownSelect, LV_EVENT_VALUE_CHANGED, NULL); } // === Loop === void loop() { lv_timer_handler(); // only update GUI when needed delay(5); } |
Example 3: Streaming Music with an ESP32 Online Radio
In this 3rd example, we move from playing local files to streaming music from online radio stations. The UI remains the same; simple and user-friendly; with the dropdown now listing all the available radio channels instead of .mp3 files.
The big visual change is the background image, which instantly gives the UI a more polished and futuristic look. If you want to take it even further and add animations, I have already covered this in my analog watch tutorials, where I explained how to animate PNG images; just like the moving clock hands you are seeing now. So with just a few tweaks, you can turn this radio into a professional-looking media player.
ESP32 S3 Streaming Online Radio Stations Code:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
#include <WiFi.h> #include <lvgl.h> #include <Arduino_GFX_Library.h> #include "touch.h" #include "ui.h" #include "Audio.h" #define GFX_BL 10 #define I2S_DOUT 19 #define I2S_BCLK 20 #define I2S_LRCK 2 const char *ssid = "fahad"; const char *password = "fahad123"; static const uint16_t screenWidth = 800; static const uint16_t screenHeight = 480; static lv_disp_draw_buf_t draw_buf; static lv_color_t buf[screenWidth * screenHeight / 10]; Arduino_ESP32RGBPanel *bus = new Arduino_ESP32RGBPanel( GFX_NOT_DEFINED, GFX_NOT_DEFINED, GFX_NOT_DEFINED, 40, 41, 39, 42, 45, 48, 47, 21, 14, 5, 6, 7, 15, 16, 4, 8, 3, 46, 9, 1 ); Arduino_RPi_DPI_RGBPanel *gfx = new Arduino_RPi_DPI_RGBPanel( bus, 800, 0, 8, 4, 8, 480, 0, 8, 4, 8, 1, 16000000, true ); Audio audio; TaskHandle_t audioTaskHandle; String stationNames[] = { "FFH Radio", "Radio Paradise (AAC 320)", "BBC Radio 1 Dance", "Rusongs" }; String stationURLs[] = { "http://mp3.ffh.de/radioffh/hqlivestream.mp3", "http://stream.radioparadise.com/aac-320", "http://bbcmedia.ic.llnwd.net/stream/bbcmedia_radio1_dance_mf_p", "http://listen.rusongs.ru/ru-mp3-128" }; int totalStations = sizeof(stationURLs) / sizeof(stationURLs[0]); int currentStation = 0; bool isPlaying = false; bool uiNeedsUpdate = false; String currentStreamTitle = ""; void TaskAudio(void *param) { while (true) { audio.loop(); vTaskDelay(1); } } void startStream(int index) { audio.stopSong(); delay(150); audio.connecttohost(stationURLs[index].c_str()); isPlaying = true; currentStreamTitle = stationNames[index]; uiNeedsUpdate = true; } void StopFun(lv_event_t *e) { audio.stopSong(); isPlaying = false; currentStreamTitle = "Stopped"; uiNeedsUpdate = true; } void VolumeChanged(lv_event_t *e) { int vol = lv_slider_get_value(ui_SliderVolume); audio.setVolume(vol); Serial.printf("Volume: %d\n", vol); } void DropdownSelect(lv_event_t *e) { uint16_t index = lv_dropdown_get_selected(ui_Dropdownsongs); if (index < totalStations) { currentStation = index; startStream(currentStation); } } void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p) { uint32_t w = (area->x2 - area->x1 + 1); uint32_t h = (area->y2 - area->y1 + 1); gfx->draw16bitRGBBitmap(area->x1, area->y1, (uint16_t *)&color_p->full, w, h); lv_disp_flush_ready(disp); } void my_touchpad_read(lv_indev_drv_t *indev_driver, lv_indev_data_t *data) { ts.read(); if (ts.isTouched) { data->state = LV_INDEV_STATE_PR; data->point.x = screenWidth - ts.points[0].x; data->point.y = screenHeight - ts.points[0].y; } else { data->state = LV_INDEV_STATE_REL; } } void populateStations() { String options = ""; for (int i = 0; i < totalStations; i++) { options += stationNames[i]; if (i < totalStations - 1) options += "\n"; } lv_dropdown_set_options(ui_Dropdownsongs, options.c_str()); } void setup() { Serial.begin(115200); lv_init(); touch_init(); pinMode(GFX_BL, OUTPUT); digitalWrite(GFX_BL, LOW); gfx->begin(); gfx->fillScreen(BLACK); lv_disp_draw_buf_init(&draw_buf, buf, NULL, screenWidth * screenHeight / 10); static lv_disp_drv_t disp_drv; lv_disp_drv_init(&disp_drv); disp_drv.hor_res = screenWidth; disp_drv.ver_res = screenHeight; disp_drv.flush_cb = my_disp_flush; disp_drv.draw_buf = &draw_buf; disp_drv.sw_rotate = 1; disp_drv.rotated = LV_DISP_ROT_180; lv_disp_drv_register(&disp_drv); static lv_indev_drv_t indev_drv; lv_indev_drv_init(&indev_drv); indev_drv.type = LV_INDEV_TYPE_POINTER; indev_drv.read_cb = my_touchpad_read; lv_indev_drv_register(&indev_drv); ui_init(); populateStations(); WiFi.begin(ssid, password); Serial.print("Connecting to WiFi"); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("Connected!"); audio.setPinout(I2S_BCLK, I2S_LRCK, I2S_DOUT); audio.setVolume(0); xTaskCreatePinnedToCore(TaskAudio, "TaskAudio", 4096, NULL, 3, &audioTaskHandle, 1); lv_obj_add_event_cb(ui_ButtonStop, StopFun, LV_EVENT_CLICKED, NULL); lv_obj_add_event_cb(ui_SliderVolume, VolumeChanged, LV_EVENT_VALUE_CHANGED, NULL); lv_obj_add_event_cb(ui_Dropdownsongs, DropdownSelect, LV_EVENT_VALUE_CHANGED, NULL); startStream(currentStation); } void loop() { static uint32_t last_ui = 0; if (millis() - last_ui > 50) { lv_timer_handler(); if (uiNeedsUpdate) { lv_label_set_text(ui_lblsongname, currentStreamTitle.c_str()); uiNeedsUpdate = false; } last_ui = millis(); } delay(1); } |
Example 4: Creating a Snake Game with Arduino GFX
This 4th example is about creating a simple but fun Snake Game—and this time, we are not using SquareLine Studio or even the LVGL library.
Snake Game Code:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
#include <Arduino.h> #include <Arduino_GFX_Library.h> #include "touch.h" #define SCREEN_WIDTH 800 #define SCREEN_HEIGHT 480 #define GAME_AREA_WIDTH 600 #define SNAKE_SIZE 10 #define MAX_SNAKE_LENGTH 200 #define BUTTON_WIDTH 60 #define BUTTON_HEIGHT 60 #define BUTTON_X_START (GAME_AREA_WIDTH + 20) #define BUTTON_Y_START 80 Arduino_ESP32RGBPanel *bus = new Arduino_ESP32RGBPanel( GFX_NOT_DEFINED, GFX_NOT_DEFINED, GFX_NOT_DEFINED, 40, 41, 39, 42, 45, 48, 47, 21, 14, 5, 6, 7, 15, 16, 4, 8, 3, 46, 9, 1 ); Arduino_RPi_DPI_RGBPanel *gfx = new Arduino_RPi_DPI_RGBPanel( bus, SCREEN_WIDTH, 0, 8, 4, 8, SCREEN_HEIGHT, 0, 8, 4, 8, 1, 16000000, true ); int snakeX[MAX_SNAKE_LENGTH]; int snakeY[MAX_SNAKE_LENGTH]; int snakeLength = 5; int dx = SNAKE_SIZE; int dy = 0; int foodX, foodY; int score = 0; bool isPaused = false; bool isStopped = false; void spawnFood() { foodX = random(0, GAME_AREA_WIDTH / SNAKE_SIZE) * SNAKE_SIZE; foodY = random(0, SCREEN_HEIGHT / SNAKE_SIZE) * SNAKE_SIZE; } void drawRect(int x, int y, uint16_t color) { gfx->fillRect(x, y, SNAKE_SIZE, SNAKE_SIZE, color); } void drawUI() { gfx->drawLine(GAME_AREA_WIDTH, 0, GAME_AREA_WIDTH, SCREEN_HEIGHT, WHITE); int bx = BUTTON_X_START; int by = BUTTON_Y_START; // Draw buttons gfx->fillRect(bx + BUTTON_WIDTH, by, BUTTON_WIDTH, BUTTON_HEIGHT, BLUE); // Up gfx->fillRect(bx + BUTTON_WIDTH, by + BUTTON_HEIGHT * 2, BUTTON_WIDTH, BUTTON_HEIGHT, BLUE); // Down gfx->fillRect(bx, by + BUTTON_HEIGHT, BUTTON_WIDTH, BUTTON_HEIGHT, BLUE); // Left gfx->fillRect(bx + BUTTON_WIDTH * 2, by + BUTTON_HEIGHT, BUTTON_WIDTH, BUTTON_HEIGHT, BLUE); // Right gfx->fillRect(bx + BUTTON_WIDTH, by + BUTTON_HEIGHT * 3 + 10, BUTTON_WIDTH, BUTTON_HEIGHT, DARKGREY); // Pause gfx->fillRect(bx + BUTTON_WIDTH, by + BUTTON_HEIGHT * 4 + 20, BUTTON_WIDTH, BUTTON_HEIGHT, RED); // Stop // Labels gfx->setTextSize(1); gfx->setTextColor(WHITE); gfx->setCursor(bx + BUTTON_WIDTH + 18, by + 25); gfx->print("UP"); gfx->setCursor(bx + BUTTON_WIDTH + 10, by + BUTTON_HEIGHT * 2 + 25); gfx->print("DOWN"); gfx->setCursor(bx + 15, by + BUTTON_HEIGHT + 25); gfx->print("LEFT"); gfx->setCursor(bx + BUTTON_WIDTH * 2 + 15, by + BUTTON_HEIGHT + 25); gfx->print("RIGHT"); gfx->setCursor(bx + BUTTON_WIDTH + 10, by + BUTTON_HEIGHT * 3 + 30); gfx->print(isPaused ? "PLAY" : "PAUSE"); gfx->setCursor(bx + BUTTON_WIDTH + 15, by + BUTTON_HEIGHT * 4 + 40); gfx->print("STOP"); // Score gfx->fillRect(bx - 10, SCREEN_HEIGHT - 50, 200, 40, BLACK); gfx->setTextColor(YELLOW); gfx->setCursor(bx + 10, SCREEN_HEIGHT - 35); gfx->setTextSize(2); gfx->print("Score: "); gfx->print(score); } void handleTouch() { ts.read(); if (ts.isTouched) { int x = SCREEN_WIDTH - ts.points[0].x; int y = SCREEN_HEIGHT - ts.points[0].y; int bx = BUTTON_X_START; int by = BUTTON_Y_START; if (x > bx + BUTTON_WIDTH && x < bx + BUTTON_WIDTH * 2 && y > by && y < by + BUTTON_HEIGHT) { dx = 0; dy = -SNAKE_SIZE; } else if (x > bx + BUTTON_WIDTH && x < bx + BUTTON_WIDTH * 2 && y > by + BUTTON_HEIGHT * 2 && y < by + BUTTON_HEIGHT * 3) { dx = 0; dy = SNAKE_SIZE; } else if (x > bx && x < bx + BUTTON_WIDTH && y > by + BUTTON_HEIGHT && y < by + BUTTON_HEIGHT * 2) { dx = -SNAKE_SIZE; dy = 0; } else if (x > bx + BUTTON_WIDTH * 2 && x < bx + BUTTON_WIDTH * 3 && y > by + BUTTON_HEIGHT && y < by + BUTTON_HEIGHT * 2) { dx = SNAKE_SIZE; dy = 0; } else if (x > bx + BUTTON_WIDTH && x < bx + BUTTON_WIDTH * 2 && y > by + BUTTON_HEIGHT * 3 + 10 && y < by + BUTTON_HEIGHT * 4 + 10) { isPaused = !isPaused; drawUI(); } else if (x > bx + BUTTON_WIDTH && x < bx + BUTTON_WIDTH * 2 && y > by + BUTTON_HEIGHT * 4 + 20 && y < by + BUTTON_HEIGHT * 5 + 20) { isStopped = true; } } } void setup() { Serial.begin(115200); touch_init(); gfx->begin(); gfx->fillScreen(BLACK); for (int i = 0; i < snakeLength; i++) { snakeX[i] = 300 - i * SNAKE_SIZE; snakeY[i] = 100; } drawUI(); spawnFood(); } void loop() { handleTouch(); if (isStopped) return; if (isPaused) { delay(100); return; } // Move snake for (int i = snakeLength - 1; i > 0; i--) { snakeX[i] = snakeX[i - 1]; snakeY[i] = snakeY[i - 1]; } snakeX[0] += dx; snakeY[0] += dy; // Wrap around if (snakeX[0] >= GAME_AREA_WIDTH) snakeX[0] = 0; if (snakeX[0] < 0) snakeX[0] = GAME_AREA_WIDTH - SNAKE_SIZE; if (snakeY[0] >= SCREEN_HEIGHT) snakeY[0] = 0; if (snakeY[0] < 0) snakeY[0] = SCREEN_HEIGHT - SNAKE_SIZE; // Food collision if (snakeX[0] == foodX && snakeY[0] == foodY) { if (snakeLength < MAX_SNAKE_LENGTH) snakeLength++; score++; spawnFood(); drawUI(); } // Draw gfx->fillRect(0, 0, GAME_AREA_WIDTH, SCREEN_HEIGHT, BLACK); gfx->drawLine(GAME_AREA_WIDTH, 0, GAME_AREA_WIDTH, SCREEN_HEIGHT, WHITE); for (int i = 0; i < snakeLength; i++) { drawRect(snakeX[i], snakeY[i], i == 0 ? GREEN : YELLOW); } drawRect(foodX, foodY, RED); delay(100); } |
The purpose of this example is to show you that it’s not always necessary to use heavy GUI libraries to build interactive projects. Sometimes, you can achieve great results with just basic graphics and smart coding.
By manually drawing everything on the screen and using touch buttons for controlling the snake, we have created a fully functional game that’s fast and responsive. I also added pause and stop buttons, a score counter, and ensured the game behaves nicely even when the snake hits the screen edge—it wraps around to the other side.
So, that’s all for now.
Watch Video Tutorial:
Discover more from Electronic Clinic
Subscribe to get the latest posts sent to your email.














