🎵 ESP32 + Bluetooth Speaker เล่นเพลงผ่าน A2DP

🌟 มาทำอะไรกันวันนี้นะ?
เชื่อว่าหลายๆคนคงจะเคยมีปัญหาตอนที่อยากเล่นเพลงจากมือถือไปที่ลำโพง Bluetooth กันไม่น้อยแน่ๆ แต่บางทีก็อยากมีอะไรที่ ควบคุมได้มากกว่านี้ หน่อย เช่น เล่นไฟล์เสียงจากเซิร์ฟเวอร์เอง หรือสั่งงานผ่าน API
วันนี้เลยขอแนะนำ ESP32 Bluetooth Speaker Sender ซึ่งเป็นโปรเจคที่สนุกมากๆ สำหรับการสร้างระบบเล่นเสียงไร้สายที่เราสามารถควบคุมผ่าน Web Interface และ REST API ได้เลย!
แต่เดี๋ยวก่อนนน ทำไมต้องยุ่งยากขนาดนี้ล่ะ? 🤔
เพราะว่าเวลาเราทำเองเนี่ย มันเป็นของเราเต็มๆ ปรับแต่งได้ตามใจ แถมยังได้เรียนรู้เทคโนโลยีใหม่ๆ ด้วย มันดีกว่าซื้อของสำเร็จเยอะเลยนะ 5555
Github: https://github.com/toygame/ESP32-Bluetooth-Speaker-Sender?tab=readme-ov-file
🎯 จริงๆ แล้วมันทำอะไรได้บ้างนะ?
🔊 เรื่องเล่นเสียงเนี่ย
- รองรับไฟล์ WAV: เล่นไฟล์เสียง 44.1kHz, 16-bit, Stereo PCM ได้เลย (คุณภาพดีมาก!)
- Bluetooth A2DP Source: ส่งเสียงไปที่ลำโพง Bluetooth แบบไร้สาย เหมือนมือถือเลย
- Streaming แบบ Real-time: อ่านไฟล์จาก SPIFFS แล้วส่งทีละนิด ไม่กิน RAM มากจนเคอร์แตก 😅
- ประหยัด Memory: ไม่ต้องโหลดไฟล์ทั้งก้อนลง RAM หรอก เครื่องเล็กๆ รับไหวแน่ๆ
🌐 ส่วน Web Interface ที่เท่ห์มาก
- Responsive Design: ใช้ได้ทั้งมือถือและคอมพิวเตอร์ ใช้ง่ายมาก!
- Real-time Status: ดูสถานะแบบ real-time ได้ รู้ว่าเครื่องกำลังทำอะไรอยู่
- Manual Refresh: เอา auto-refresh ออกแล้ว เพราะมันทำให้ watchdog timeout (เจ็บปวดมา 🥲)
🔗 REST API ที่ใช้ง่าย
- Play Control: สั่งเล่น/หยุดเสียงผ่าน API ได้เลย
- Volume Control: ปรับเสียง 0-100% แบบง่ายๆ ไม่ต้องคิดว่า Bluetooth มันใช้ 0-127
- Status Monitoring: เช็คสถานะระบบได้ตลอดเวลา ว่าเครื่องยังอยู่มั้ย 555
- Flow Control: ป้องกันการเล่นซ้อนกัน ไม่ให้เสียงมันปั่นป่วน
📡 WiFi ที่เชื่อมต่อเองได้
- Auto-Connect: ใส่ WiFi password ครั้งเดียว แล้วมันจะจำเอง
- Web Server: เปิด HTTP Server พอร์ต 80 ใช้ได้เลย ไม่ต้องใส่พอร์ตแปลกๆ
- ใช้ใน LAN: เข้าถึงผ่าน IP ในบ้าน สะดวกมาก ไม่ต้องพึ่ง internet
🛠️ ลงมือทำกันเลยดีกว่า!
📋 เตรียมของก่อนเริ่มนะ:
✅ ESP32 Development Board (จริงๆ ใช้อันไหนก็ได้ แต่แนะนำ DevKit v1)
✅ Bluetooth Speaker (ต้องรองรับ A2DP นะ ปกติลำโพงมือถือก็ได้แล้ว)
✅ MicroSD Card (optional แต่ถ้าอยากใส่เพลงเยอะๆ ก็เอามาด้วย)
✅ USB Cable สำหรับเสียบโปรแกรม (อันเดียวกับที่ชาร์จมือถือ Android)
✅ WiFi ที่ใช้ได้ในบ้าน
เฮ้ย ไม่มี MicroSD Card ได้มั้ย? ได้สิ! ESP32 มี SPIFFS ให้ใช้ ใส่ไฟล์เสียงได้ประมาณ 4MB เลยนะ
💾 เตรียมไฟล์เสียงก่อน:
# 1. สร้างโฟลเดอร์ data/ (ใน project ของเรานะ)
mkdir data
# 2. ใส่ไฟล์ play1.wav เข้าไป
# 🚨 สำคัญ: ต้องเป็น 44.1kHz, 16-bit, Stereo PCM เท่านั้น!
# ถ้าไฟล์ไม่ใช่ format นี้ มันจะเล่นไม่ได้ หรือเสียงแตก 😵💫
cp your_audio.wav data/play1.wav
# 3. อัปโหลดไฟล์ไปที่ ESP32
pio run --target uploadfs
Pro tip: ถ้าอยากแปลงไฟล์เสียง แนะนำใช้ Audacity ฟรีดี ใช้ง่าย!
⚙️ แก้ไขโค้ดนิดหน่อย:
// src/main.cpp
void setup() {
Serial.begin(115200);
// ใส่ชื่อ WiFi กับรหัสผ่านของบ้านเรา
controller.setWiFiCredentials("YourWiFi", "YourPassword");
// ตั้งชื่อ Bluetooth (จะแสดงเวลาจับคู่กับลำโพง)
controller.setBluetoothDeviceName("AL-01");
// ระบุไฟล์เสียงที่จะเล่น
controller.setAudioFile("/play1.wav");
controller.setVolume(80); // เริ่มที่ 80% ดีนะ ไม่ดังจนแตกใส
// ตั้งให้ส่งสถานะทุก 10 วินาที
controller.setStatusUpdateInterval(10000);
}
🚀 ทดสอบเลย:
# 1. Build แล้ว upload firmware
pio run --target upload
# 2. เปิด Serial Monitor ดูการทำงาน
pio device monitor
# 3. รอดู IP Address ใน Serial Monitor
# แล้วเข้าไปที่ http://192.168.1.xxx/ ใน browser
หมายเหตุ: ถ้า upload ไม่ได้ ลองกดปุ่ม BOOT บน ESP32 ค้างไว้ระหว่าง upload นะ 😉
💻 มาลองใช้งานกันดูสิ!
🎮 หน้าเว็บหลักสวยๆ
พอเข้าไปที่ IP Address ของ ESP32 แล้วจะเจอกับหน้าเว็บสวยๆ แบบนี้:
🎵 ESP32 Audio Player
Web-controlled Bluetooth Audio Streaming
[▶ Play Audio] [⏹ Stop Audio] [📊 Check Status] [🔄 Refresh]
System Information
├── Audio Status: ⏸ STANDBY
├── Volume: 🔊 80%
├── Free Memory: 45,678 bytes
├── IP Address: 192.168.1.100
└── WiFi Signal: -45 dBm
🎛️ ลองกดปุ่มต่างๆ ดูสิ:
1. กดเล่นเสียง
คลิก [▶ Play Audio] แล้วลำโพงจะเริ่มส่งเสียงออกมา! 🎵
→ ESP32 จะส่งสัญญาณผ่าน Bluetooth ไปหาลำโพง
→ Status จะเปลี่ยนเป็น "🎵 PLAYING" ให้ดู
→ ถ้าลำโพงยังไม่ได้จับคู่ มันจะบอกว่าไม่ได้เชื่อมต่อนะ
2. หยุดเล่น
คลิก [⏹ Stop Audio] ก็หยุดทันที
→ เสียงจะหยุดเลย ไม่ต้องรอให้เพลงจบ
→ Status กลับไปเป็น "⏸ STANDBY"
→ พร้อมรับคำสั่งใหม่ได้เลย
3. เช็คสถานะ
คลิก [📊 Check Status] จะได้ข้อมูลเพิ่มเติม
→ ดู Memory ที่เหลือ (ถ้าต่ำกว่า 10KB มันจะเตือนนะ)
→ เช็คการเชื่อมต่อ WiFi และ Bluetooth
→ ดูความแรงสัญญาณ WiFi ด้วย
4. รีเฟรชแบบ Manual
คลิก [🔄 Refresh] เพื่ออัปเดตข้อมูล
→ เราเอา auto-refresh ออกแล้ว เพราะมันทำให้ watchdog timeout
→ กดเองดีกว่า ประหยัด resources ด้วย 😅
🔌 REST API สำหรับคนที่ชอบเล่น curl
📡 สั่งงานผ่าน API แบบโปรๆ
เออ ใครที่เป็น developer คงอยากใช้ API มากกว่ากดปุ่มใน browser อ่ะนะ? เอาล่ะ มาลองเล่นกัน!
เริ่มเล่นเสียง:
curl -X GET http://192.168.1.100/play1
# มันจะตอบกลับมาแบบนี้:
# ✅ 200: "Audio playback started!" (เล่นได้แล้ว!)
# ⚠️ 409: "Audio already playing - request skipped" (เล่นอยู่แล้วจ้า ข้ามไป)
# ❌ 500: "Failed to start audio playback" (เล่นไม่ได้ มีบางอย่างผิดพลาด)
# ❌ 503: "Low memory - service temporarily unavailable" (RAM เหลือน้อย รอหน่อย)
สังเกตมั้ย? ถ้าเราส่ง API ซ้ำขณะที่มันกำลังเล่นอยู่ มันจะข้ามไปทันที ไม่ให้เสียงมันชนกัน 👌
หยุดเล่นเสียง:
curl -X GET http://192.168.1.100/stop
# Response:
# ✅ 200: "Audio playback stopped!" (หยุดแล้ว!)
# ⚠️ 409: "Audio not playing - nothing to stop" (มันไม่ได้เล่นอยู่นี่นา)
# ❌ 500: "Audio player not available" (ระบบเสียงมีปัญหา)
🔊 ปรับเสียงผ่าน API
ดูเสียงตอนนี้:
curl -X GET http://192.168.1.100/volume
# จะได้:
{
"volume": 80,
"range": "0-100"
}
ปรับระดับเสียง:
curl -X POST http://192.168.1.100/volume
-d "volume=75"
# สำเร็จ:
{
"status": "success",
"volume": 75
}
# ใส่ผิด (เกิน 100 หรือติดลบ):
{
"error": "Volume must be 0-100"
}
📊 เช็คสถานะแบบละเอียด
curl -X GET http://192.168.1.100/status
# ได้ข้อมูลแบบนี้:
Status: PLAYING
Volume: 75%
Free Heap: 42,156 bytes
WiFi IP: 192.168.1.100
WiFi Signal: -42 dBm
Bluetooth: Connected
ข้อมูลเพียบเลย! ดู Memory, WiFi, Bluetooth ได้หมด เหมาะสำหรับ monitoring หรือสร้าง dashboard
💡 API Integration Examples:
Python Script:
import requests
import time
ESP32_IP = "http://192.168.1.100"
def play_audio():
response = requests.get(f"{ESP32_IP}/play1")
print(f"Play: {response.text}")
def set_volume(level):
response = requests.post(f"{ESP32_IP}/volume",
data={"volume": level})
print(f"Volume: {response.json()}")
def get_status():
response = requests.get(f"{ESP32_IP}/status")
print(f"Status: {response.text}")
# Usage Example
set_volume(60)
play_audio()
time.sleep(2)
get_status()
JavaScript (Web App):
class ESP32Controller {
constructor(ip) {
this.baseURL = `http://${ip}`;
}
async playAudio() {
const response = await fetch(`${this.baseURL}/play1`);
return await response.text();
}
async setVolume(level) {
const response = await fetch(`${this.baseURL}/volume`, {
method: 'POST',
body: `volume=${level}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
return await response.json();
}
}
// Usage
const esp32 = new ESP32Controller('192.168.1.100');
esp32.setVolume(80).then(console.log);
esp32.playAudio().then(console.log);
⚙️ เทคนิคการทำงานภายใน
📦 System Architecture
├── 🎵 AudioPlayer
│ ├── WAV file parsing
│ ├── PCM data streaming
│ └── Playback state management
├── 📡 BluetoothAudioSource
│ ├── A2DP protocol handling
│ ├── Audio data callback
│ └── Volume control (0-127)
├── 🌐 WiFiManager
│ ├── Connection management
│ ├── IP assignment
│ └── Signal monitoring
├── 🔗 WebServerManager
│ ├── HTTP server (AsyncTCP)
│ ├── REST API endpoints
│ └── Web UI rendering
└── 🎛️ AudioPlayerController
├── System coordination
├── Component lifecycle
└── Status monitoring
📻 Audio Streaming Technology
WAV File Processing:
bool AudioPlayer::prepareAudioFile(const char* filename) {
// 1. เปิดไฟล์จาก SPIFFS
audioFile = SPIFFS.open(filename, "r");
// 2. อ่าน WAV Header (44 bytes)
audioFile.read(wavHeader, 44);
// 3. Validate Format
if (strncmp((char*)wavHeader, "RIFF", 4) != 0) {
return false;
}
// 4. Extract Audio Parameters
channels = *(uint16_t*)(wavHeader + 22);
sampleRate = *(uint32_t*)(wavHeader + 24);
bitsPerSample = *(uint16_t*)(wavHeader + 34);
// 5. Calculate Data Size
dataSize = *(uint32_t*)(wavHeader + 40);
return true;
}
Real-time PCM Streaming:
int32_t AudioPlayer::getAudioData(AudioFrame* data, int32_t frameCount) {
// 1. Memory Health Check
if (ESP.getFreeHeap() < 5000) {
stopPlayback();
return 0;
}
// 2. Read PCM Data from SPIFFS
for (int i = 0; i < frameCount; i++) {
if (audioFile.available() >= 4) {
// Left Channel (16-bit)
int16_t left = audioFile.read() | (audioFile.read() << 8);
// Right Channel (16-bit)
int16_t right = audioFile.read() | (audioFile.read() << 8);
data[i].channel1 = left;
data[i].channel2 = right;
}
}
return frameCount;
}
🔊 Volume Mapping Algorithm
ระบบใช้การแปลงค่าระดับเสียงระหว่าง User-friendly (0-100%) และ Bluetooth Protocol (0-127):
// User API: 0-100% → Bluetooth: 0-127
uint8_t AudioPlayerController::mapPercentageToVolume(uint8_t percentage) {
if (percentage == 0) return 0;
if (percentage >= 100) return 127;
// Linear mapping with precision
return (uint8_t)((percentage * 127UL) / 100UL);
}
// Bluetooth: 0-127 → User API: 0-100%
uint8_t AudioPlayerController::mapVolumeToPercentage(uint8_t value127) {
if (value127 == 0) return 0;
if (value127 >= 127) return 100;
return (uint8_t)((value127 * 100UL) / 127UL);
}
🛡️ การแก้ไขปัญหาและเพิ่มประสิทธิภาพ
⏰ Watchdog Timeout Prevention
ปัญหาหลักที่พบคือ Task Watchdog Timeout จาก AsyncTCP:
// แก้ไขปัญหา Watchdog ใน Main Loop
void AudioPlayerController::update() {
if (!systemReady) return;
// 🔧 Reset Watchdog Timer
esp_task_wdt_reset();
// 📊 Periodic Status Update
if (millis() - lastStatusUpdate > statusUpdateInterval) {
printSystemStatus();
lastStatusUpdate = millis();
}
// ⚡ Yield to Other Tasks
yield();
delay(50); // Reduced from 100ms
}
AsyncTCP Configuration:
# platformio.ini - Optimized Settings
build_flags =
-D CONFIG_ASYNC_TCP_MAX_ACK_TIME=3000
-D CONFIG_ASYNC_TCP_PRIORITY=5 ; ลด Priority
-D CONFIG_ASYNC_TCP_QUEUE_SIZE=32 ; ลด Queue Size
-D CONFIG_ASYNC_TCP_STACK_SIZE=8192 ; เพิ่ม Stack Size
-D CONFIG_ESP_TASK_WDT_TIMEOUT_S=10 ; เพิ่ม Timeout
-D CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=false
🔄 Flow Control Implementation
การป้องกันการเล่นเสียงซ้อนกัน:
// Before: ปัญหาการเล่นซ้อนกัน
/play1 + /play1 + /play1 → Conflict!
// After: Flow Control
STANDBY ──[/play1]→ PLAYING ──[complete]→ STANDBY
│
[/stop]
│
↓
STANDBY
// Implementation:
if (audioPlayer->getState() == PLAYING) {
request->send(409, "text/plain",
"Audio already playing - request skipped");
return; // ข้ามการเล่นซ้อน
}
💾 Memory Protection
ระบบป้องกันการทำงานเมื่อ Memory ต่ำ:
// API Level Protection
if (ESP.getFreeHeap() < 10000) {
request->send(503, "text/plain",
"Low memory - service temporarily unavailable");
return;
}
// Audio Callback Protection
if (ESP.getFreeHeap() < 5000) {
Serial.printf("CRITICAL: Very low memory: %d bytesn",
ESP.getFreeHeap());
stopPlayback(); // หยุดเล่นเพื่อคืน Memory
}
📊 Build Optimization
การจัดการขนาด Firmware และ SPIFFS:
# audio_partitions.csv - Custom Partition Table
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x1E0000, ; ~1.9MB สำหรับ Firmware
spiffs, data, spiffs, 0x1F0000,0x200000, ; 2MB สำหรับไฟล์เสียง
# Compiler Optimization
build_flags =
-Os ; Size Optimization
-ffunction-sections ; Function-level Linking
-fdata-sections ; Data-level Linking
-D CORE_DEBUG_LEVEL=0 ; Disable Debug Messages
🎉 สรุปมั่วๆ แต่ได้เรื่อง
ว้าว! โปรเจค ESP32 Bluetooth Speaker Sender นี้มันเจ๋งจริงๆ นะ ใครที่ลองทำตามแล้วคงจะรู้สึกว่า ESP32 มันเก่งกว่าที่คิดเยอะเลย!
🚀 เอาไปใช้ทำอะไรได้บ้าง:
- 🏠 Smart Home: ทำระบบเสียงในบ้าน เปิดเพลงตอนเช้าอัตโนมัติ
- 🏢 ที่ทำงาน: ระบบประกาศ หรือ background music แบบควบคุมได้
- 🔬 Prototype: ต้นแบบสำหรับผลิตภัณฑ์เสียงแบบต่างๆ
ที่สำคัญ โปรเจคนี้แสดงให้เห็นว่า ESP32 ทำอะไรได้มากกว่าที่คิด แค่ตัวเล็กๆ แต่สามารถสร้างระบบ IoT ที่ซับซ้อนได้แบบเต็มๆ! 💪
Happy Coding ทุกคน! 🎵✨