不用 DAC 在 Pi Pico 上播放音訊檔
Raspberry Pico 突然成了我進行嵌入式開發的首選晶片。它的硬體做工精良、堅固耐用,擁有大量看似以巧思與熱情設計的功能(驅動 GPIO 的狀態機就是個殺手級功能!)。它主要的弱點——缺乏連線能力——現在已透過 W 版本獲得解決。資料手冊非常出色,詳盡記錄了晶片的每一個面向。此外,它還獲得了 MicroPython 的良好支援(我大量使用它),而 C SDK 環境也還算不錯,儘管充滿了如當今流行所要求的那種無用的複雜性:一個會再去產生 Makefile 的 cmake 建構系統、一堆用來定義這個那個的檔案(使用的函式庫、除錯輸出⋯⋯),整體而言,為了編譯給微小裝置用的微小程式,根本是嚴重的殺雞用牛刀。不,這還不只如此:所有這些複雜性,竟只是為了替硬體固定、功能固定的裝置(除了 W 與非 W 版本的差異)產生程式。關於當今軟體有多糟的抱怨就先到此為止,但這點必須被記住。
在像這樣的 MCU 上,人們想做的酷炫事情之一就是產生一些聲音。最直覺的做法就是使用晶片內建的 PWM(脈衝寬度調變) 功能。可以將 GPIO 設定為以所需頻率在 0 與 1 之間交替切換,像這樣:
from machine import Pin, PWM
pwm = PWM(Pin(1))
pwm.freq(400)
pwm.duty_u16(1000)假設你已將壓電蜂鳴器接到 Pico 的 GND 與腳位 1,你就會聽到頻率為 400hz 的方波聲音。現在,很少有聲音比方波更難聽了。或許我們可以做得更好。在此我會跳過所有中間步驟,例如產生正弦波,直接跳到播放 wav 檔。一旦你了解做法,就能輕鬆自行產生其他各種波形(正弦波、雜訊、這些波形的包絡等等)。
現在你可能會問自己:如果 Pico 只能將腳位切換為高電位或低電位,我要如何產生播放 wav 檔所需的複雜波形?一個像樣的非方波波形是由不同位階所組成的,所以我會需要一個 DAC(數位類比轉換器)!幸好,我們完全不需要 DAC,只靠 Pico 的單一腳位就能做到這一切。
複雜聲音產生的運作原理
我不想在這裡講太多背景知識。但你只需要知道,如果你不想產生那種只在輸出最小值與最大值之間交替的平庸方波,你就會需要中間的階層,像這樣:
S0: #
S1: ####
S2: ######
S3: #######
S4: ########依此類推,其中 S0 是第一個取樣,S1 是第二個取樣⋯⋯
每個取樣的持續時間取決於取樣頻率,也就是每秒我們改變(播放時)或取樣(錄製時)音訊波形的次數。這意味著要播放複雜的聲音,我們需要讓 Pico 的腳位能夠輸出不同的電壓。
有個技巧可以只用 PWM 在 Pico 上做到這件事,那就是使用頻率非常高的方波,但針對我們想產生的不同電壓使用不同的工作週期。因此我們將輸出設為非常非常高的頻率:
pwm.freq(100000)接著,如果想產生 S0 取樣,我們將工作週期(其值介於 0 到 65535 之間)設為較小的值。如果想產生 S1 取樣,就使用更高的值,依此類推。依序來看,我們可能會想做像這樣的事:
pwm.duty_u16(3000) # S0
pwm.duty_u16(12000) # S1
pwm.duty_u16(18000) # S2
pwm.duty_u16(21000) # S3
pwm.duty_u16(24000) # S4工作週期是指腳位設為 1 的時間相對於設為 0 的時間的比例。工作週期為 65535 代表 100% 的時間腳位為高電位,0% 則代表全程為低電位。所有這些,都是在維持所設定的交替頻率的前提下進行。因此如果我們像用示波器那樣放大來觀察,就能看到在產生 S2 與 S3 取樣時發生了什麼:
S2:
######################
#
#
#
#
######################
#
#
#
#而 S3 則會像這樣:
######################
######################
#
#
#
######################
######################
#
#
#腳位以相同的頻率上下切換,但在 S3 的情況下,處於高電位的時間更長。這會產生更高的平均電壓。這讓我們得以近似模擬出波形。
轉換並播放 WAV 檔
為了播放 wav 檔,我們必須先將其轉換為易於用 MicroPython 讀取的原始格式。我從 SoundCloud 下載了一個內容為「Oh no!」的 wav 檔。所以我的轉換指令會像這樣:
ffmpeg -i ohno.wav -ar 24000 -acodec pcm_u8 -f u8 output.raw請注意,我們將檔案轉換為 8 位元音訊(每個取樣有 256 種不同的輸出位階)。反正我們的 PWM 技巧本來就不會那麼精準地近似那麼多不同的位階,而且我們的資源也有限。你也可以試試 16 位元,但我用這樣就得到了還不錯的成果。
接著,透過 mpremote 將 output.raw 檔上傳到裝置上:
mpremote cp output.raw :現在建立一個名為「play.py」的檔案,或取你喜歡的任何名稱,內容如下:
from machine import Pin, PWM
pwm = PWM(Pin(1))
pwm.freq(100000)
f = open("output.raw","rb")
buf = bytearray(4096)
while f.readinto(buf) > 0:
for sample in buf:
pwm.duty_u16(sample<<8)
x=1
x=1
x=1
x=1
x=1
f.close()我們在這裡做的,只是一次讀取 4096 個取樣,然後根據取樣值依序設定不同的 PWM 工作週期來「播放」它。問題在於,在我們的 PCM(脈衝編碼調變) 檔中每秒有 24000 個取樣(參見 ffmpeg 指令列)。我們要如何確定這與 MicroPython 的速度相符呢?嗯,實際上並不完全吻合,所以我加入了「x=1」陳述式來稍微延遲一下,讓音高聽起來大致正確。
喔,對了,如果你在好奇 sample<<8 是什麼,這只是將 8 位元取樣重新縮放到設定 PWM 工作週期所需的完整 16 位元精確度。
這一切的缺點是播放時會讓你的程式處於忙碌狀態。我還沒測試過,但 MicroPython 支援多執行緒,所以讓一個執行緒來播放音訊可能是可行的做法。
加分題:正弦波聲音產生
# Sin wave
wave=[]
wave_samples = 40
pwm.freq(100000)
for i in range(wave_samples):
x = i/wave_samples*3.14*2
dc = int((1+math.sin(x))*65000)
wave.append(dc)
print(wave)
for i in range(1000):
for dc in wave: pwm.duty_u16(dc)隨機一篇部落格