[TH] Play the Wav File with ESP32.

บทความนี้เป็นการประยุกต์ใช้ DAC และ MicroPython ของไมโครคอนโทรลเลอร์ ESP32 เพื่อเปิดไฟล์ WAV ซึ่งเป็นไฟล์บันทึกเสียง และนำออกไปยัง DAC ที่เชื่อมต่อกับลำโพงดังภาพที่ 1 โดยไฟล์ที่นำมาใช้นั้นเป็นไฟล์แบบเสียงโมโน (mono) แบบ PCM 8 บิตที่ไม่ได้ถูกบีบอัด และโปรแกรมตัวอย่างรองรับการทำ Sampling Rate ที่ประมาณ 50KHz หรือที่ระดับ 44100

ภาพที่ 1 ตัวอย่างบอร์ดสำหรับทดสอบการทำงานของบทความ

โครงสร้างไฟล์ WAV

ไฟล์ Wav ประกอบด้วยส่วนของหัวไฟล์ที่ใช้สำหรับตรวจสอบประเภทและขนาดของไฟล์ และตามด้วย Chunk ของส่วน fmt สำหรับเก็บรายละเอียดของรูปแบบไฟล์ และ data สำหรับเก็บข้อมูลเสียงดังนี้

จำนวนไบต์ความหมาย
4‘RIFF’
4ขนาดไฟล์
4‘WAVE’
4‘fmt ‘
4ขนาดของส่วน fmt
2ประเภทของออดิโอ
2จำนวนช่องสัญญาณ
4Sample Rate
4Byte Rate
2Block Align
2จำนวนบิตต่อ Sample
4‘data’
4ขนาดของข้อมูล
nข้อมูล

จากตารางจะได้ว่าในการอ่านข้อมูลจากไฟล์จะเริ่มจากการอ่าน 4 ไบต์แรก เพื่อตรวจสอบว่าเป็น ‘RIFF’ หรือไม่ ถ้าใช่จะดำเนินการอ่านขนาดของไฟล์อีก 4 ไบต์ หลังจากนั้นอ่าน 4 ไบต์ถัดมาเพื่อตรวจสอบว่าเป็น ‘WAVE’ หรือไม่ ถ้าใช่หมายถึงเป็นส่วนหัวของไฟล์ WAV

4 ไบต์ถัดมาเป็นข้อความ ‘fmt ‘ สำหรับเป็นการบอกว่าเป็นส่วนของเก็บรายละเอียดของรูปแบบไฟล์เสียง และส่วนสุดท้ายใน ‘data’ เป็นส่วนเก็บข้อมูลเสียงที่ต้องอ่านขึ้นมาเพื่อนำออกไปให้ภาค DAC

ตัวอย่างโปรแกรม

ตัวอย่างโปรแกรมของบทความนี้เป็นการเล่นไฟล์เสียง mono.wav และ mono2.wav ซึ่งผู้อ่านจะต้องอัพโหลดไฟล์ไปไว้ที่บอร์ดของไมโครคอนโทรลเลอร์ก่อนดังภาพที่ 2

ภาพที่ 2 รายการไฟล์ในบอร์ด ESP32

โค้ดตัวอย่างโปรแกรมภาษาไพธอนเป็นดังนี้

import time
import sys
from machine import DAC, Pin, freq
import gc

gc.enable()
gc.collect()
freq(240000000)

dacPin1 = Pin(25) # ต่อกับลำโพง
dacPin2 = Pin(26) # ต่อกับ adcPin1

dac1 = DAC( dacPin1 )
dac2 = DAC( dacPin2 )

def playWavFile( fName ):
    monoFile = open(fName,"rb")
    mark = monoFile.read(4)
    if (mark != b'RIFF'):
        print("ไม่ใช้ WAV!")
        monoFile.close()
        sys.exit(1)
    fileSize = int.from_bytes(monoFile.read(4),"little")
    print("File size = {} bytes".format(fileSize))
    fileType = monoFile.read(4)
    if (fileType != b'WAVE'):
        print("ไม่ใช้ WAV!!")
        monoFile.close()
        sys.exit(2)

    chunk = monoFile.read(4)
    lengthFormat = 0
    audioFormat = 0
    numChannels = 0
    sampleRate = 0
    byteRate = 0
    blockAlign = 0

    if (chunk == b'fmt '):
        lengthFormat = int.from_bytes(monoFile.read(4),"little")
        audioFormat = int.from_bytes(monoFile.read(2),"little") 
        numChannels = int.from_bytes(monoFile.read(2),"little")
        sampleRate = int.from_bytes(monoFile.read(4),"little")
        byteRate = int.from_bytes(monoFile.read(4),"little") 
        blockAlign = int.from_bytes(monoFile.read(2),"little") 
        bitsPerSample = int.from_bytes(monoFile.read(2),"little")
    
        print("Length of format data = {}".format(lengthFormat))
        print("Audio's format = {}".format(audioFormat))
        print("Number of channel(s) = {}".format(numChannels))
        print("Sample rate = {}".format(sampleRate))
        print("Byte rate = {}".format(byteRate))
        print("Block align = {}".format(blockAlign))
        print("Bits per sample = {}".format(bitsPerSample))
        
        minValue = 255
        maxValue = 0
    
        chunk = monoFile.read(4)
        if (chunk != b'data'):
            print("ไม่ใช้ WAV!!!!")
            monoFile.close()
            sys.exit(5)
        dataSize = int.from_bytes(monoFile.read(4),"little")
        print("Data size = {}".format(dataSize))
        if (bitsPerSample > 8):
            print("ไม่รองรับข้อมูลที่มากกว่า 8 บืต")
            monoFile.close()
            sys.exit(6)
        buffer = monoFile.read(dataSize)
        # find min/max
        for i in range(len(buffer)):
            if (buffer[i] > maxValue):
                maxValue = buffer[i]
            if (buffer[i]<minValue):
                minValue = buffer[i]
        # normalize
        xScale = 255.0/(maxValue-minValue)
        # play
        tm = int(1000000/sampleRate)
        for i in range(len(buffer)):
            data = int(((buffer[i]-minValue)*xScale))
            dac1.write( data )  
            time.sleep_us(tm)
        print("---------------------------")
    
    if (audioFormat != 1):
        print("ไม่รองรับกรณีที่ไม่ใช้ PCM!!!")
        monoFile.close()
        sys.exit(3)
    monoFile.close()
    dac1.write( 0 )
    
############### main program
playWavFile("/mono.wav")
time.sleep_ms(1000)
playWavFile("/mono2.wav")
time.sleep_ms(1000)

จากโค้ดจะพบว่าในขั้นตอนของการอ่านข้อมูลเสียงจะทำการค้นหาค่าน้อยสุดและมากสุดเพื่อใช้สำหรับปรับสัดส่วนของข้อมูลให้ข้อมูลค่าน้อยสุดไปอยู่ที่ 0 และค่าสูงสุดเป็น 255 หลังจากนั้นในการนำออกจะนำค่าที่ต้องการส่งออกลบออกด้วยค่าน้อยสุดและคูณกับ xScale เพื่อปรับค่าให้อยู่ในช่วง 0 ถึง 255

นอกจากนี้ได้สร้างตัวแปร tm สำหรับหาค่าหน่วงเวลาแบบคร่าว ๆ โดยนำค่า Sample Rate ไปหา 1000000 เพื่อนำค่าไปหน่วงในระดับไมโครวินาที ทำให้รองรับการส่งสัญญาณเสียงได้ใกล้เคียงความจริงมากขึ้น

เมื่อโปรแกรมทำงานจะรายงานข้อมูลของไฟล์ที่เปิดใช้งานดังตัวอย่างผลลัพธ์ในภาพที่ 3 และทำการอ่านข้อมูลเสียงส่งไปให้ DAC

ภาพที่ 3 ตัวอย่างผลลัพธ์ของการแสดงผลจากโปรแกรม

สรุป

จากบทความนี้จะพบว่าผู้อ่านจะสามารถเปิดไฟล์ WAV และอ่านข้อมูลมาได้อย่างถูกต้อง แต่ไฟล์ที่สามารถนำไปประมวลผลเพื่อนำออกไปยัง DAC จะต้องถูกแปลงให้เป็นแบบ MONO และเป็นข้อมูลแบบ 8 บิตที่ไม่ได้ถูกบีบอัดเท่านั้น ซึ่งทางทีมเราหวังว่าผู้อ่านจะนำไปปรับปรุงต่อไปให้ดีขึ้น และสุดท้ายขอให้สนุกกับการเขียนโปรแกรมครับ

(C) 2020-2022, โดย อ.ดนัย เจษฎาฐิติกุล/อ.จารุต บุศราทิจ
ปรับปรุงเมื่อ 2021-12-02, 2022-01-27