ESP32 · I2S · BT · USB-MSC · WLAN · One Button

Yet Another
Media Player

Ein gehäuseloser ESP32-MP3-Player. Ein Knopf. Kein Display erforderlich. Echter Shuffle ohne Doppelungen, Bluetooth-Ausgang, USB-Massenspeicher, WLAN-Webkonsole, NeoPixel-Lightshow, persistente Wiedergabe-History. Fertig.

// Schon wieder ein MP3-Player?

Warum zum Teufel?

Wir sitzen mindestens einmal die Woche mit einer Handvoll Leuten zusammen — essen, reden, spielen Schocken und hören Musik dabei.

Und letzteres ist manchmal ein bisschen anstrengend: Einer mag dieses nicht, eine mag jenes — und wenn der Sound vom Handy kommt, wird ständig dieses nervige Teil rausgekramt, die Brille rausgekramt und auf dem Schmierpad rumgetippt. Oder "Deine Röhre" kommt zum Einsatz — und die nervt mit Werbung. Und manchmal bricht ja auch im besten Deutschland aller Zeiten das Neuland weg.

Kurz: es stresst.

Also: bau einen MP3-Player, den man einfach auf die Box setzt — mit einem einzigen, für alle zugänglichen Button, mit dem man sofort jeden Song skippen kann. Einfach auf den dicken Button draufpatschen. Fertig.

Erste Versuche mit dem Heltec LoRa (weil's grad da war) stießen an Grenzen — da kam die ESP32-S3-DevKit-Variante dazu. Das OLED braucht man eh nicht; dann könnte man auch gleich ein Handy nehmen. Jetzt ist diese Variante im Einsatz, und den ersten Abenden hat sie bereits überstanden.

Eine 32-GB-Speicherkarte mit derzeit rund 1100 Titeln ist im Einsatz. Bis zu 5000 sollten problemlos gehen. Zwei beiliegende Scripts kümmern sich um die SD-Karte: rename_mp3.sh kopiert MP3s in die genormte Verzeichnisstruktur (mit Duplikaterkennung), make_index.py erzeugt danach die index.csv für den Player. Die Dateistruktur auf der SD-Karte sieht so aus:

SD:/
├── index.csv
├── history.csv        ← Wiedergabe-History (automatisch)
├── 01/
│   ├── 0001.mp3
│   ├── 0002.mp3
│   └── ...
├── 02/
│   └── ...
└── 99/
    └── 9999.mp3
Codeberg → Download
// Was kann er

Features

🔀

Echter Shuffle

Fisher-Yates-Algorithmus: alle Tracks werden einmal gespielt, bevor neu gemischt wird — keine Doppelungen, keine Lücken.

💾

Persistenter Shuffle

Shuffle-Liste wird auf der SD-Karte gespeichert, Position im NVS-Flash. Nach einem Reboot geht es genau dort weiter, wo man aufgehört hat.

🔁

Shuffle-Steuerung

reshuffle mischt die Liste sofort neu. Button beim Einschalten halten = Reshuffle ohne Serial. shuffle list [n] zeigt die nächsten n Tracks.

Vorwärts & Rückwärts

History-Stack mit 50 Einträgen. Zurück durch bereits gespielte Titel, vorwärts wieder raus.

🔊

Persistente Lautstärke

Gespeichert im NVS-Flash. Überlebt Reboots. Regelbar von 0 bis 21 in beide Richtungen, mit Richtungsumkehr beim Loslassen.

📋

Wiedergabe-History

Jeder gespielte Track wird in history.csv auf der SD-Karte protokolliert — mit akkumulierter Uptime als Timestamp.

Akkumulierte Uptime

Die Gesamtlaufzeit des Players wird über Reboots hinweg im NVS-Flash aufsummiert und bei jedem Track gespeichert.

🎲

Gewichtete Priorität

Tracks bekommen in der index.csv eine Priorität 0–9 — beeinflusst die Häufigkeit innerhalb der Shuffle-Runde.

🦷

Bluetooth-Ausgang

KCX_BT_EMIT Bluetooth-Transmitter als zweiter Ausgang. Laufzeit-Umschaltung zwischen I2S-DAC und BT per Kommando möglich.

💿

USB-Massenspeicher

SD-Karte per usb on direkt als USB-Laufwerk am PC verfügbar (S3-DevKit, nativer USB). Kein Reboot, kein Kabel umstecken.

🌐

WLAN & Web-Interface

Eigener Access Point "Patsch" (PW: patsch1234). Unter http://192.168.4.1/ steht eine vollwertige Browser-Konsole bereit — alle Kommandos, Log in Echtzeit.

💡

NeoPixel-Lightshow

17-LED-Streifen an GPIO 1: Regenbogen-Welle über Lautstärkebalken im Play-Modus, Orange-Atemeffekt bei Pause, Blitz bei Vol+/−.

📁

Index-Backup & Restore

index new /dd erstellt automatisch eine neue index.csv aus einem Verzeichnis und sichert die alte. index restore N stellt sie wieder her.

🔌

Drei Boards

ESP32 WROOM (Minimalaufbau), ESP32-S3 DevKit (USB-MSC, WLAN, NeoPixel) und Heltec LoRa v3 (eingebautes OLED-Display).

📡

Serielle Konsole

Vollständiges Kommando-Interface per USB-Serial: Dateizugriff (ls/get/put/rm/mkdir), Index verwalten, BT steuern, Ausgabe umschalten.

🎭

Simulations-Modus

Startet ohne SD-Karte mit Fake-Tracks — ideal zum Testen von Button-Logik, Lautstärke und Kommandos ohne echte Hardware.

// Ein Knopf — vier Gesten

Bedienung

1× KURZ
Nächster Titel

Nächster Track aus der gemischten Shuffle-Liste. Kein Track kommt doppelt, bis alle gespielt wurden.

2× KURZ
Vorheriger Titel

Navigiert rückwärts durch die History. Bis zu 50 Tracks zurück möglich. 2. Klick innerhalb ~900ms nach dem 1. Klick.

3× KURZ
Pause / Weiter

Wiedergabe pausieren oder fortsetzen. 3. Klick innerhalb ~900ms nach dem 2. Klick.

LANG HALTEN
Lautstärke regeln

Rauf oder runter — je nach aktueller Richtung. Wiederholt alle 500ms bis du loslässt.

LANG LOSLASSEN
Richtung umkehren

Nächster Lang-Druck geht in die andere Richtung. Kein Durchdrehen bis ans Limit nötig.

// LED-Status

LED-Blinkmuster

Onboard-LED (alle Boards) zeigt den Betriebszustand. Der NeoPixel-Streifen (S3-DevKit) visualisiert zusätzlich die Lautstärke.

Normalbetrieb

900ms — Musik läuft

Lauter

150ms — Lautstärke wird erhöht

Leiser

75ms — Lautstärke wird gesenkt

Endlevel / Pause

Blinken stoppt — Min/Max erreicht oder Pause

// NeoPixel-Streifen (S3-DevKit)

Lightshow

Play — Regenbogen-Welle

Animierte Farb-Welle über den Lautstärkebalken (17 LEDs)

Pause — Atemeffekt

Orange pulsierend über den aktuellen Volume-Balken

Vol+ — Blau-Flash

Ganzer Streifen blau für 600ms

Vol− — Rot-Flash

Ganzer Streifen rot für 600ms

// Fotos

Hardware-Impressionen

// Vorher · Nachher

Nun muss ein Gehäuse her — schnell, einfach, passend, stabil und billig. Da war doch was …

// Verdrahtung

Hardware

Drei Board-Varianten werden unterstützt. Nur eine Zeile in patsch.ino einkommentieren:
(Die letzten Versionen sind aber bislang nur mit dem S3-Devkit getestet worden. Eigenes Feedback gern an ralf@rbag.de)

// #define BOARD_HELTEC_LORA_V3
#define BOARD_ESP32_S3_DEVKIT
// #define BOARD_ESP32_WROOM
// ESP32-S3 DevKit (empfohlen)

S3-DevKit → PCM5102A I2S-DAC

GPIO 14BCK
GPIO 15LCK
GPIO 16DIN
3.3V / 5VVIN
GNDGND, FMT
3.3VXSMT

S3-DevKit → SD-Karte

GPIO 10CS
GPIO 11MOSI
GPIO 13MISO
GPIO 12SCK
3.3V3V3
GNDGND

S3-DevKit → KCX_BT_EMIT

GPIO 17TX (Modul sendet)
GPIO 18RX (Modul empfängt)
GPIO 38LINK (HIGH=verbunden)
GPIO 21CON (LOW-Puls=Pairing)
3.5–5V+5V (Elko 100µF)
GNDPGND + AGND
IN_L/IN_R ← analoger Ausgang PCM5102A

S3-DevKit — Taster & LED

GPIO 0BOOT-Taster (gegen GND)
GPIO 48Onboard NeoPixel
GPIO 1NeoPixel-Streifen DIN (17 LEDs)
// ESP32 WROOM

WROOM → PCM5102A I2S-DAC

GPIO 27BCK
GPIO 26LCK
GPIO 25DIN
3.3V / 5VVIN
GNDGND, FMT
3.3VXSMT

WROOM → SD-Karte

GPIO 5CS
GPIO 23MOSI
GPIO 19MISO
GPIO 18SCK
3.3V3V3
GNDGND

WROOM — Taster & LED

GPIO 33Taster (gegen GND)
GPIO 2Onboard LED

WROOM → KCX_BT_EMIT

GPIO 16TX (Modul sendet)
GPIO 17RX (Modul empfängt)
CON/LINK: nicht verdrahtet
// Heltec LoRa v3

Heltec → I2S-DAC & SD

GPIO 14BCK
GPIO 15LCK
GPIO 13DIN
GPIO 46SD-SCK
GPIO 47SD-MOSI
GPIO 45SD-MISO
GPIO 5SD-CS

Heltec — OLED & Button

GPIO 17OLED SDA
GPIO 18OLED SCL
GPIO 21OLED RST
GPIO 36VEXT (LOW=3.3V an)
GPIO 33Taster (gegen GND)
GPIO 35Onboard LED
// Kompilieren & Flashen

Software

Abhängigkeiten über den Arduino Library Manager installieren:

LibraryVersionZweck
ESP32-audioI2S≥ 2.0MP3-Dekodierung + I2S-Ausgabe
Adafruit NeoPixel≥ 1.11NeoPixel-Streifen (S3-DevKit)
Adafruit SSD1306≥ 2.5OLED (nur Heltec)
Adafruit GFX≥ 1.11OLED (nur Heltec)

SD-Karte befüllen mit rename_mp3.sh:

# Dry-run (zeigt was passieren würde)
./rename_mp3.sh ~/Musik /media/SDCARD

# Wirklich kopieren
./rename_mp3.sh ~/Musik /media/SDCARD --run

# Mit Duplikatprüfung (Dateiränder vergleichen)
./rename_mp3.sh ~/Musik /media/SDCARD --run --diff-edges

# Vollständiger Byte-Vergleich (langsam aber sicher)
./rename_mp3.sh ~/Musik /media/SDCARD --run --diff-full

Danach Index generieren mit make_index.py:

# Index für die gesamte SD-Karte generieren
python3 make_index.py /media/SD --priority 5 -o /media/SD/index.csv

# Nur einen Ordner indizieren
python3 make_index.py /media/SD --include 04 -o /media/SD/index.csv

# Mit Priorität aus Ordnernamen (ambient=1 … hits=9)
python3 make_index.py /media/SD --genre-from-folder -o /media/SD/index.csv

# Vorschau ohne Schreiben
python3 make_index.py /media/SD --dry-run

Format der index.csv:

/01/0001.mp3,,5
/01/0002.mp3,,9
/02/0001.mp3,,3

Felder: Pfad, Genre (optional), Priorität 0–9

Format der history.csv (automatisch angelegt):

uptime_ms,pfad,priorität
183742,/01/0330.mp3,5
184891,/02/0007.mp3,3

Timestamp = akkumulierte Gesamtlaufzeit des Players in Millisekunden.

Flashen mit flash.sh:

# ESP32-S3 DevKit (Port automatisch erkannt)
./patsch/flash.sh s3devkit

# ESP32 WROOM
./patsch/flash.sh wroom

# Heltec LoRa v3
./patsch/flash.sh heltec

# Port explizit angeben
./patsch/flash.sh s3devkit /dev/ttyACM0

# Nur kompilieren, nicht flashen
./patsch/flash.sh --only-compile

Hinweis S3-DevKit: Beim ersten Flash ohne CH343-Adapter muss der Download-Modus manuell ausgelöst werden: BOOT gedrückt halten → kurz RESET → BOOT loslassen. Mit angeschlossenem CH343-Adapter (UART0) funktioniert das Auto-Reset normal.

// Web-Interface

WLAN & Webkonsole

Der ESP32-S3 DevKit spannt beim Start einen eigenen Access Point auf:

SSIDPatsch
Passwortpatsch1234
IP192.168.4.1

Unter http://192.168.4.1/ steht eine Browser-Konsole bereit: Konsolen-Log in Echtzeit (1s-Polling), Eingabefeld für alle seriellen Kommandos. Funktioniert parallel zur USB-Serial-Konsole.

// 115200 Baud

Serielle Konsole

Über USB-Serial (Newline als Zeilenende) oder die Web-Konsole steuerbar:

Wiedergabe
next Nächster Titel (wie Knopf 1×)
prev Vorheriger Titel (wie Knopf 2×)
pause Pause / Weiter umschalten (wie Knopf 3×)
play <n> Titel Nr. n direkt abspielen (1–Tracks)
play /dd/nnnn.mp3 Titel per Pfad abspielen
vol <0-21> Lautstärke direkt setzen
Status & Info
help Befehlsübersicht anzeigen
status RAM, SD, Lautstärke, Board, aktueller Track
shuffle Shuffle-Position anzeigen (pos/gesamt/verbleibend)
shuffle list [n] Nächste n Tracks der Shuffle-Liste anzeigen (Default: 20)
reshuffle Shuffle-Liste sofort neu mischen (auch: Button beim Einschalten halten)
history Anzahl Einträge in history.csv + Gesamtuptime
cat history history.csv vollständig ausgeben
cat index index.csv vollständig ausgeben
Index verwalten
index Neue index.csv zeilenweise hochladen (Leerzeile = Ende)
index new /dd [p] Neue index.csv aus allen .mp3 in /dd erstellen (Prio p, Default 3); alte wird gesichert
index restore N index.csv aus Backup index_N.csv wiederherstellen
index list Vorhandene index-Backups (index_N.csv) auflisten
Dateizugriff (SD-Karte)
ls [pfad] Verzeichnis auflisten (Default: /)
get <datei> Datei als Base64 ausgeben
put <datei> Datei per Base64 hochladen
rm <pfad> Datei oder Verzeichnis löschen
mkdir <pfad> Verzeichnis anlegen
USB-Massenspeicher (S3-DevKit)
usb on SD-Karte als USB-Laufwerk freigeben (Wiedergabe pausiert)
usb off USB-Massenspeicher beenden, Wiedergabe fortsetzen
Audio-Ausgang
output Aktuellen Ausgang anzeigen
output i2s Ausgang: I2S DAC (PCM5102A)
output bt Ausgang: Bluetooth (KCX_BT_EMIT)
Bluetooth (KCX_BT_EMIT)
kcx pair Pairing auslösen (CON-Pin LOW-Puls)
kcx link LINK-Pin lesen (Verbindungsstatus)
kcx on KCX Power ON
kcx off KCX Power OFF
kcx <cmd> AT-Kommando direkt senden (z.B. kcx AT+NAME?)
Diagnose & System
btntest Tasten-Flanken-Log ein/ausschalten
btnreset Button-State zurücksetzen
reboot Uptime + Shuffle sichern, ESP32 neu starten
// Versionsgeschichte

Changelog

v2.2.0 2026-06-17
  • reshuffle: Serial-Kommando mischt die Shuffle-Liste sofort neu
  • Button-Reshuffle: Button beim Einschalten halten = Reshuffle ohne Serial
  • shuffle list [n]: nächste n Tracks der aktuellen Shuffle-Reihenfolge anzeigen
  • Shuffle auf SD: Liste wird als /shuffle.csv auf der SD gespeichert; Position weiterhin im NVS
  • Shuffle-Validierung: korrupte NVS-Blobs werden erkannt und durch Reshuffle ersetzt
v2.1.3 2026-06-07
  • Index-Backup/Restore: index new /dd [p], index restore N, index list
  • copyFile(): interne Hilfsfunktion für atomares Sichern von index.csv
  • EOF-Fix: skipEofPlay verhindert ungewollten Track-Sprung nach Pause
v2.1.0 2026-06-07
  • NeoPixel-Lightshow: 17 LEDs, Regenbogen-Welle über Lautstärkebalken im Play-Modus
  • Pause-Effekt: Orange-Atemeffekt auf Volume-Balken
  • Vol-Flash: Blau (Vol+) / Rot (Vol−) über gesamten Streifen, 600ms
v2.0.9 2026-06-07
  • NeoPixel-Streifen: 5 LEDs, GPIO 1, S3-DevKit — Lautstärkebalken + Statusfarbe (Grün=Play, Orange pulsierend=Pause)
v2.0.7 2026-06-07
  • KCX CON-Pin: kcx pair löst Pairing per GPIO-Puls aus
  • KCX LINK-Pin: kcx link zeigt Verbindungsstatus (S3-DevKit: GPIO 38)
  • S3-DevKit: KCX_CON_PIN=21, KCX_LINK_PIN=38 definiert
v2.0.2 2026-06-06
  • pause: Serial-Kommando zum Umschalten Pause/Weiter
  • flash.sh: Automatischer Fallback auf /dev/ttyACM0 bei S3-DevKit
v2.0.0 2026-06-05
  • PCM5102A I2S-DAC: als primärer Ausgang (statt MAX98357A)
  • KCX_BT_EMIT: Bluetooth-Transmitter als zweiter Ausgang
  • Ausgang umschalten: output i2s / output bt zur Laufzeit
  • ESP32-S3 DevKit: Board-Unterstützung hinzugefügt
  • USB-MSC: SD-Karte als USB-Laufwerk (usb on / usb off)
  • Web-Konsole: WLAN-AP "Patsch", Browser-Interface unter http://192.168.4.1/
  • Dreifach-Klick: Pause/Weiter
  • Simulations-Modus: Betrieb ohne SD-Karte mit Fake-Tracks
  • Dateizugriff: ls, get, put, rm, mkdir per Serial
  • Erweiterte Kommandos: cat history/index, shuffle, kcx *, output *
v1.2.0 2026-06-03
  • Echter Shuffle: Fisher-Yates — alle Tracks einmal, dann neu mischen
  • Persistenter Shuffle: Liste + Position in NVS, überlebt Reboots
  • Wiedergabe-History: history.csv auf SD, append über Reboots hinweg
  • Akkumulierte Uptime: Gesamtlaufzeit in NVS, Timestamp in history.csv
  • LED-Blinkmuster: 900ms normal / 150ms lauter / 75ms leiser
v1.0.0 2026-05-25
  • Erstveröffentlichung
  • Gewichteter Zufalls-Shuffle mit Priorität 1–9
  • History-Stack: bis zu 50 Songs rückwärts
  • Ein-Knopf-Bedienung: Kurz / Doppelklick / Lang
  • Lautstärke persistent via NVS
  • Board-Support: ESP32 WROOM und Heltec LoRa v3
  • OLED-Anzeige (Heltec): Track, Priorität, Position, Lautstärke
  • make_index.py: Index-Generator mit Genre/Priorität-Mapping