Thanapon Tapala

Backend Developer

Embedded Developer

Smart Farmer

Maker

Thanapon Tapala

Backend Developer

Embedded Developer

Smart Farmer

Maker

Blog Post

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

July 19, 2025 Arduino, ESP32, ESP8266, Music
🎵 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 ทุกคน! 🎵✨

Taggs: