ESP32 + DFPlayer Mini で作る、ブラウザ操作できる Wi-Fi オーディオプレーヤー
ESP32 と DFPlayer Mini を使って、スマホのブラウザから操作できる音楽プレーヤーを作ったので紹介します。
自分用のメモも兼ねているので、間違いやより良い方法があればぜひ教えてください。
ESP32 が Wi-Fi アクセスポイントとして動作し、アプリ不要で以下のような操作ができます。
- 再生・一時停止・前後スキップ
- 音量調整(スライダー)
- イコライザ変更(NORMAL / POP / ROCK / JAZZ / CLASSIC / BASS)
- 再生時間のリアルタイム表示
- 再生中は波紋アニメーションが動く UI
DFPlayer Mini は MP3 のメタデータが取得しづらいため、1曲3分想定でタイマー管理し、3分に達したら次の曲に進めるという割り切り仕様にしています。
作ったものの概要
- ESP32 が Wi-Fi アクセスポイント(AP)+ Web サーバとして動作
- DFPlayer Mini で microSD 上の MP3 を再生
- スマホ / PC からブラウザで Web UI にアクセス
- 再生 / 一時停止 / 前の曲 / 次の曲
- 音量スライダー
- イコライザ選択
- 再生時間のリアルタイム表示
- 再生中は波紋アニメーションが動く円形 UI
「ESP32_Music」という Wi-Fi に接続するだけで、アプリなしで音楽プレーヤーとして使えます。
用意するもの
- ESP32 開発ボード
- DFPlayer Mini
- スピーカー(8Ω推奨)
- microSDカード(音楽ファイル用)
- USBケーブル(PC と ESP32 接続用)
- Arduino IDE または PlatformIO
配線
DFPlayer Mini と ESP32 の接続は以下の通り。
| DFPlayer Mini | ESP32 GPIO | 備考 |
|---|---|---|
| VCC | 5V | 電源 |
| GND | GND | グラウンド |
| TX | GPIO0 | ESP32 RX (Serial1) |
| RX | GPIO4 | ESP32 TX (Serial1) |
| SPK_1 | スピーカー | |
| SPK_2 | スピーカー |
ポイント:
-
ESP32 と DFPlayer の GND は必ず共通にする
-
DFPlayer の電源は 5V 推奨(3.3V だと不安定になりやすい)
-
ESP32 → DFPlayer Mini(TX→RX)は 3.3V → 5V 系への入力になるので、
厳密には レベル変換 or 分圧 を入れるのが安全。
(動いてはいますが、長期運用や個体差まで考えると対策した方が安心)
SDカードの構成
DFPlayer Mini にはファイル名のルールがあります。
/mp3/[0001.mp](http://0001.mp3)
/mp3/[0002.mp](http://0002.mp3)
/mp3/[0003.mp](http://0003.mp3)
...
基本的に、番号順に再生されます。
Web UI の構成
ESP32 が提供する Web UI は、以下のような画面構成です。
- 中央に円形のプレーヤー(波紋アニメーション)
- 再生 / 一時停止ボタン
- 前の曲 / 次の曲
- 再生位置バー(進行状況)
- 再生時間表示(経過/想定トラック長)
- 音量スライダー(0〜30)
- イコライザ選択プルダウン
スマホから操作すると、ネイティブアプリっぽい操作感になります。
仕組み
1. ESP32 を Wi-Fi AP + DNS + Web サーバにする
WiFi.softAP(ssid, password);
dnsServer.start(DNS_PORT, "*", myIP);
- ESP32 が
ESP32_Musicという SSID でアクセスポイントを立てます - DNS を全ドメイン
*→ ESP32 の IP に向けることで、簡易的な キャプティブポータル的挙動にしています
OS によっては自動でログインページが開きますが、開かない場合はブラウザで http://192.168.4.1 を開けば UI にアクセスできます。
2. Web UI → ESP32 へ fetch でコマンド送信
ブラウザ側(JavaScript)からは、fetch() でシンプルな HTTP GET を投げています。
// 再生
fetch('/command?cmd=play');
// 音量変更
fetch('/command?cmd=volume&value=15');
コマンドは cmd と value で表現しておき、
ESP32 側でパースして DFPlayer Mini を操作します。
3. ESP32 → DFPlayer Mini を制御
myDFPlayer.start(); // 再生開始
myDFPlayer.pause(); // 一時停止
[myDFPlayer.next](http://myDFPlayer.next)(); // 次の曲
myDFPlayer.previous(); // 前の曲
myDFPlayer.volume(volume); // 音量設定
myDFPlayer.EQ(eqSetting); // イコライザ設定
HardwareSerial myHardwareSerial(1); を使って Serial1 経由で DFPlayer Mini を制御しています。
4. 再生時間の管理
MP3 の長さをメタデータから取らず、1曲3分(180秒)と仮定してタイマーで管理します。
- ESP32 側で 1 秒ごとに
elapsedTimeをインクリメント -
elapsedTime >= trackLength(=180秒)で次の曲にスキップ -
/track-statusエンドポイントで JSON を返し、フロント側の JavaScript から 1 秒ごとにポーリング
server.on("/track-status", handleTrackStatus);
フロントエンド側では setInterval() で /track-status を 1 秒ごとに叩き、
プログレスバーと時間表示を更新しています。
コード全文
以下が動作するスケッチの全体です。
#include <WiFi.h>
#include <WebServer.h>
#include <DNSServer.h>
#include "DFRobotDFPlayerMini.h"
// WiFi設定
const char* ssid = "ESP32_Music";
const char* password = "";
// DNSサーバーとWebサーバーの初期化
DNSServer dnsServer;
WebServer server(80);
const byte DNS_PORT = 53;
// DFPlayer Mini設定
HardwareSerial myHardwareSerial(1); // Serial1 (RX: GPIO0, TX: GPIO4)
DFRobotDFPlayerMini myDFPlayer;
// 再生時間管理
unsigned long previousMillis = 0;
unsigned long interval = 1000; // 1秒間隔
int elapsedTime = 0;
int trackLength = 180; // 曲の長さ(秒): 3分想定
bool isPlaying = false; // 再生状態を追跡
// 現在のイコライザモード
int currentEqMode = 0; // 初期値: NORMAL
// HTMLコンテンツ
const char htmlPage[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>オーディオプレイヤー</title>
<style>
body {
font-family: 'Arial', sans-serif;
background: linear-gradient(135deg, #141e30, #243b55);
color: #fff;
margin: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
}
.player-container {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 250px;
height: 250px;
background: radial-gradient(circle, #4e73df, #2a518c);
border-radius: 50%;
box-shadow: 0 0 30px rgba(78, 115, 223, 0.6);
overflow: hidden;
}
.wave {
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(78, 115, 223, 0.3);
transform: translate(-50%, -50%);
animation: none;
}
.wave.active {
animation: expandCircle 1.5s infinite;
}
@keyframes expandCircle {
0% {
width: 0;
height: 0;
opacity: 0.8;
}
100% {
width: 250px;
height: 250px;
opacity: 0;
}
}
.horizontal-controls {
margin-top: 20px;
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
}
.vertical-controls {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 20px;
width: 300px;
}
.button {
background: white;
color: #4e73df;
font-size: 1rem;
font-weight: bold;
padding: 10px 15px;
border: none;
border-radius: 5px;
cursor: pointer;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
.button:hover {
background: #f0f0f0;
}
.progress-bar, .volume-control, .equalizer {
width: 100%;
}
.progress-bar input, .volume-control input {
width: 100%;
}
.equalizer select {
width: 100%;
padding: 5px;
}
.time-display,
.volume-display {
font-size: 1.2rem;
font-weight: bold;
text-align: center;
}
</style>
</head>
<body>
<div class="player-container">
<div class="wave"></div>
<div class="wave"></div>
<div class="wave"></div>
</div>
<div class="horizontal-controls">
<button id="prev" class="button">⏮️ 前の曲</button>
<button id="play-pause" class="button">▶️ 再生</button>
<button id="next" class="button">次の曲 ⏭️</button>
</div>
<div class="vertical-controls">
<div class="progress-bar">
<label for="progress">進行:</label>
<input type="range" id="progress" min="0" max="100" value="0" readonly>
</div>
<div class="time-display" id="time-display">
00:00 / 00:00
</div>
<div class="volume-control">
<label for="volume">音量:</label>
<input type="range" id="volume" min="0" max="30" value="15">
<div class="volume-display" id="volume-display">
音量: 15
</div>
</div>
<div class="equalizer">
<label for="equalizer">イコライザ:</label>
<select id="equalizer">
<option value="0">NORMAL</option>
<option value="1">POP</option>
<option value="2">ROCK</option>
<option value="3">JAZZ</option>
<option value="4">CLASSIC</option>
<option value="5">BASS</option>
</select>
</div>
</div>
<script>
const playPauseButton = document.getElementById('play-pause');
const prevButton = document.getElementById('prev');
const nextButton = document.getElementById('next');
const volumeSlider = document.getElementById('volume');
const volumeDisplay = document.getElementById('volume-display');
const equalizerDropdown = document.getElementById('equalizer');
const progressBar = document.getElementById('progress');
const timeDisplay = document.getElementById('time-display');
const waves = document.querySelectorAll('.wave');
let isPlaying = false;
// 再生/一時停止の切り替え
playPauseButton.addEventListener('click', () => {
isPlaying = !isPlaying;
playPauseButton.textContent = isPlaying ? '⏸️ 一時停止' : '▶️ 再生';
waves.forEach(wave => wave.classList.toggle('active', isPlaying));
fetch(`/command?cmd=${isPlaying ? 'play' : 'pause'}`);
});
// 前の曲
prevButton.addEventListener('click', () => {
fetch('/command?cmd=prev');
});
// 次の曲
nextButton.addEventListener('click', () => {
fetch('/command?cmd=next');
});
// 音量調整
volumeSlider.addEventListener('input', (event) => {
const volume = [event.target](http://event.target).value;
volumeDisplay.textContent = `音量: ${volume}`;
fetch(`/command?cmd=volume&value=${volume}`);
});
// イコライザ変更
equalizerDropdown.addEventListener('change', (event) => {
const eqSetting = [event.target](http://event.target).value;
fetch(`/command?cmd=eq&value=${eqSetting}`);
});
// 時間のフォーマット変換
function formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
}
// 再生状況の取得と更新
setInterval(() => {
fetch('/track-status')
.then(response => response.json())
.then(data => {
progressBar.max = data.trackLength;
progressBar.value = data.elapsedTime;
timeDisplay.textContent = `${formatTime(data.elapsedTime)} / ${formatTime(data.trackLength)}`;
});
}, 1000);
</script>
</body>
</html>
)rawliteral";
void handleRoot() {
server.sendHeader("Content-Type", "text/html; charset=UTF-8");
server.send_P(200, "text/html", htmlPage);
}
// コマンド処理
void handleCommand() {
if (server.hasArg("cmd")) {
String cmd = server.arg("cmd");
Serial.println(cmd);
if (cmd == "play") {
myDFPlayer.start();
isPlaying = true;
} else if (cmd == "pause") {
myDFPlayer.pause();
isPlaying = false;
} else if (cmd == "next") {
[myDFPlayer.next](http://myDFPlayer.next)();
elapsedTime = 0;
} else if (cmd == "prev") {
myDFPlayer.previous();
elapsedTime = 0;
} else if (cmd == "volume" && server.hasArg("value")) {
int volume = server.arg("value").toInt();
myDFPlayer.volume(volume);
} else if (cmd == "eq" && server.hasArg("value")) {
int eqSetting = server.arg("value").toInt();
myDFPlayer.EQ(eqSetting);
currentEqMode = eqSetting;
}
}
server.send(200, "text/plain", "OK");
}
// 再生状況をJSONで送信
void handleTrackStatus() {
String json = "{\\"elapsedTime\\":" + String(elapsedTime) +
",\\"trackLength\\":" + String(trackLength) + "}";
server.send(200, "application/json", json);
}
void setup() {
Serial.begin(115200);
// DFPlayer Miniの初期化
myHardwareSerial.begin(9600, SERIAL_8N1, 0, 4); // RX: GPIO0, TX: GPIO4
if (!myDFPlayer.begin(myHardwareSerial)) {
Serial.println("DFPlayer Mini 初期化失敗");
while (true); // 初期化失敗時に停止
}
myDFPlayer.volume(15); // 初期音量設定
// WiFiアクセスポイント設定
WiFi.softAP(ssid, password);
IPAddress myIP = WiFi.softAPIP();
Serial.print("アクセスポイントIP: ");
Serial.println(myIP);
// DNSサーバーの開始
dnsServer.start(DNS_PORT, "*", myIP);
// HTTPハンドラ登録
server.on("/", handleRoot);
server.on("/command", handleCommand);
server.on("/track-status", handleTrackStatus);
// サーバー開始
server.begin();
Serial.println("HTTPサーバー開始完了");
}
void loop() {
// DNSリクエストの処理
dnsServer.processNextRequest();
// HTTPクライアントリクエストの処理
server.handleClient();
// 再生中の場合、経過時間を更新
if (isPlaying) {
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
elapsedTime++;
if (elapsedTime >= trackLength) {
[myDFPlayer.next](http://myDFPlayer.next)();
elapsedTime = 0;
}
}
}
}
使い方
- 上記スケッチを ESP32 に書き込む
- ESP32 が起動すると
ESP32_Musicという Wi-Fi が出現 - スマホ / PC で
ESP32_Musicに接続 - ブラウザで
http://192.168.4.1にアクセス - 表示された Web UI から再生・停止・音量・イコライザなどを操作
※ 一部の端末では、接続すると自動的にログインページとしてブラウザが開くこともあります。
トラブルシューティング
DFPlayer が初期化失敗する
- TX / RX の配線を再確認(TX ↔ RX で接続する)
- 電源不足(USB だけだとギリギリな場合あり)
- SDカードの相性・フォーマット(FAT32 でフォーマット推奨)
ノイズが乗る
- GND をしっかり共通にしていない
- スピーカーのインピーダンスが低すぎる
- 配線が長く、電源ラインのノイズを拾っている
曲が途中で止まる
- SDカードの速度が遅い(Class10 以上推奨)
- 接触不良(microSD の挿し直し・別カードで確認)
今後の拡張アイデア
- 曲名の表示(番号 → タイトルに変換するテーブルを持つなど)
- SDカード内のファイル一覧を Web UI に出して選曲できるようにする
- MQTT や WebSocket を使って、ネットワーク越しに遠隔操作
- 再生中の音に合わせて LED を光らせる VU メーター的な演出
今回のコードは↓にもあります。
Discussion