作品已經完成,先上源碼:
https://files.cnblogs.com/files/qzrzq1/WIFISpeaker.zip
全文包含三篇,這是第三篇,主要講述接收端程序的原理和過程。
第一篇:基於Orangpi Zero和Linux ALSA實現WIFI無線音箱(一)
第二篇:基於Orangpi Zero和Linux ALSA實現WIFI無線音箱(二)
以下是正文:
在進行接收端程序開發前,首先要了解Orangpi Zero的聲音設備。
Orangpi可以通過ALSA(The Advanced Linux Sound Architecture )訪問系統的聲音設備。
一、查找並確定Orangpi Zero的聲音設備
要使用ALSA,首先就是要能正確找到聲音設備,作者在使用alsa的時候,嘗試過使用armbian官網(鏈接地址)上三個不同的鏡像,發現有些armbian鏡像有問題,不知道是什么原因,分別是:
(1)、基於Ubuntu Xenial的Armbian鏡像,版本號3.4.113(下載地址)
(2)、基於Ubuntu Xenial的Armbian鏡像,版本號4.14.14(下載地址)
(3)、基於Debian Stretch的Armbian鏡像,版本號4.14.14(下載地址)
這三個鏡像中,只有第一個能找到聲卡設備,其他兩個都提示無聲卡設備。作者只能使用第一個鏡像。
在armbian中,使用以下命令即可看到聲卡設備。
aplay -l
如上圖所示,在OrangePi Zero中,共有兩個聲卡設備,一個card0是audiocodec,指的是板載的TV接口,另一個card1是sndhdmi,指的是HDMI輸出接口,其中card0是默認聲卡設備,因為TV接口在開發板上有直接引出,而且只需3線(左聲道、右聲道、地),本作品直接使用TV接口作為音頻輸出。硬件電路如圖如下所示。
如果使用aplay命令顯示出來的card0不是我們想要的默認聲卡設備,那就要進行更改了,更改方法可以參考“linux alsa音頻架構的配置與使用”這個文章。
此外,alsa還有一個虛擬的配置界面,alsamixer,利用它可以方便的設置聲卡音量、配置聲卡、靜音等功能,類似windows桌面右下角的聲音管理器。要打開alsamixer,直接使用alsamixer命令即可,具體的使用方法,可以參考“Linux下的音量控制器alsamixer”這篇文章,界面如下圖所示。
alsamixer
設置之后,利用aplay命令測試一下能否播放音樂,如果TV接口播放音頻正常,接下來就可以開始接收端的程序開發了。
#播放測試音樂 aplay test.wav
測試alsa正常后,接下來介紹接收端程序中需要使用到的socket和pyalsaaduio模塊。
二、socket模塊
socket模塊使用比較簡單,首先獲取本機IP,然后初始化socket為UDP模式,並綁定IP地址和端口號,就可以開始接受數據包了。主要涉及的函數包括:
#創建socket socket.socket([family[, type[, proto]]]) #連接遠程地址 socket.connect(address) #綁定socket的IP地址和端口號 socket.bind(address) #從socket接收數據包 socket.recvfrom(bufsize[, flags]) #關閉socket socket.close()
socket模塊的使用比較簡單,網上有很多范例,這里不再詳細說明。
三、pyalsaaudio模塊
pyalsaaudio(下載地址)是一個用於python中訪問ALSA API的模塊,利用這個模塊,用戶可以很輕松的在程序中訪問Orangpi Zero的PCM和混音器設備,這個模塊的使用說明和范例在這個鏈接地址里有。
1、安裝pyalsaaudio模塊
依次安裝python對應版本的setuptools、python-dev、libasoud2-dev和pyalsaaudio包即可。其中python-dev包與所使用的python版本有關,可以使用python3 -V命令查看python版本,本作品armbian系統預裝了python3.5,所以要安裝python3.5-dev包。依次執行以下命令。
(1)、安裝python3-setuptools命令:
apt-get install python3-setuptools
(2)、安裝python3.5-dev命令:
apt-get install python3.5-dev
(3)、安裝libasoud2-dev命令:
apt-get install libasound2-dev
(4)最后,使用python的pip3命令安裝pyalsaaudio模塊:
pip3 install pyalsaaudio
(5)上一步中的pip3命令,是為了與python2區分的,armbian中預安裝了python2和python3,作者使用的是python3,如果直接使用pip命令,系統就會給python2安裝pyalsaaudio模塊了,所以這里需要注意。如果提示沒有pip3命令,那就需要使用以下命令安裝pip3。安裝之后就可以使用pip3命令操作第4步了。
apt-get install python3-pip
四、接收端程序設計
接收端比較簡單,在Python環境下直接使用socket和pyalsaaudio模塊即可快速實現數據包的接收和播放,主要使用的pyalsaaudio模塊函數如下。
#默認的構造函數 #系統初始化alsa device,系統默認按以下參數配置:PCM、44.1kHz、雙通道、周期大小32幀 #Sample format: PCM_FORMAT_S16_LE #Rate: 44100 Hz #Channels: 2 #Period size: 32 frames class alsaaudio.PCM(type=PCM_PLAYBACK, mode=PCM_NORMAL, device='default', cardindex=-1) #設置采樣率,以Hz為單位。 #典型值是8000(電話)、16000、44100(CD音質)、48000(DVD音質)、96000 PCM.setrate(rate) #設置周期大小,用戶程序每次處理音頻數據的幀數, #即用戶程序每次要寫入(播放)/讀取(錄音)的數據大小 #以幀為單位,一幀就是一次采樣的字節數 PCM.setperiodsize(period) #寫入待播放的音頻數據。 #data的數據長度必須是幀大小的整數倍, 並且等於周期大小。 #如果小於周期大小,則實際不會播放,直到程序把數據按照周期大小完全寫入。 PCM.write(data)
在《基於Orangpi Zero和Linux ALSA實現WIFI無線音箱(二)》中,作者設定了發送來的數據包前40個字節為識別數據格式的包頭,真正的音頻數據是從第41字節開始。包頭數據的40個字節,實際就是C里的WAVEFORMATEX結構體,包含采樣率、通道數、位深度信息,在python中,需要對這個結構體(數據包的開始的40字節)的數據進行解析讀取,這樣,才能正確設置pyalsaaudio的PCM類對象。
要實現上述功能,在C里,可以直接把數據包首地址強制轉換成WAVEFORMATEX結構體類型的指針,再訪問各個成員變量即可,可是在python里,沒有地址和指針的概念,需要使用struct模塊中的pack和unpack函數。
struct模塊的pcak和unpcak函數是用來處理C結構數據的,通過它們可以實現對字節數組的解釋。例如WAVEFORMATEX結構體的第2~3字節(以0開始)為通道數,第4~7字節為采樣率,unpack函數可以把這些字節數組按照設定的要求進行轉換。兩個函數的詳細用法,可以參考“Python中struct.pack()和struct.unpack()用法詳細說明”這篇文章。
最后,接收端程序設計的流程和源碼如下:
1、初始化socket
2、初始化PCM類對象
3、從socket接受數據(阻塞式)
4、解釋數據包頭
5、每隔1s判斷數據包頭指定的格式跟當前格式是否一致,如果不一致,則關閉PCM類對象並重新初始化
6、播放從第41字節開始的音頻數據
注意:程序中音頻格式只做了對采樣率的判斷,沒對位深度、通道數等信息的判斷,有興趣的讀者可以自行添加。
import socket import alsaaudio import struct import time #函數:獲取IP地址 def GetHostIP(): try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(('1.1.1.1', 80)) ip = s.getsockname()[0] finally: s.close() return ip #以下是主程序 RecCount = 0 #默認的PCM音頻格式,參考C里面的WAVEFORMATEX結構體 #格式標識wFormatTag = 0xfe #通道數nChannels = 2 #采樣率nSamplesPerSec = 48000Hz #波特率nAvgBytesPerSec = 192000 #塊對齊nBlockAlign = 4 #位深度wBitsPerSample = 16 list_pwfx = [65534, 2, 48000, 192000, 4, 16] Local_IP=GetHostIP() print('說明') print('1.本機ip:%s:12321'%(Local_IP)) print('2.默認按照48000Hz、雙通道、16位PCM格式播放') print('3.發送端發出的數據包前40個字節為音頻格式信息,接收端(本程序)每隔1s會解釋一次包頭,讀取並自動修改播放器采樣率信息(如發生變化)') print('4.注意:接收端(本程序)只支持11025、12000、44100、48000這4種采樣率的自動切換,不支持修改通道數、位深度等其他信息的切換。') print('5.發送端如果在后台(如Windows的音頻管理器)修改了采樣率,必須重新點擊‘啟動’按鈕,才能重新發生正確的音頻流') #初始化socket sss = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) sss.bind((Local_IP, 12321)) #系統初始化alsa device,系統默認按以下參數配置 #Sample format: PCM_FORMAT_S16_LE #Rate: 44100 Hz #Channels: 2 #Period size: 32 frames device = alsaaudio.PCM() #修改默認采樣率為48kHz device.setrate(list_pwfx[2]) #修改緩沖區大小(以幀為單位,0.1s是4800幀) device.setperiodsize(list_pwfx[2]//10) Lasttime =time.time() while 1: #申請20k字節緩沖區 BytesRecv,ServerAddr = sss.recvfrom(20000) #這里是為了讓程序自動更改播放音頻的采樣率,如果距離上次設置采樣率的時刻大於1s, #則讀取數據包的頭40個字節,判斷服務器傳過來的數據采樣率有無變化,重新設置采樣率, #只支持在11025、12000、44100和48000間切換 Nowtime = time.time() if (Nowtime-Lasttime) > 1 : Lasttime = Nowtime #解釋包頭(只取前16字節),具體請參考C里面的WAVEFORMATEX結構體或文件開頭的說明 #注意struct.unpack返回值是一個元組 tuple_pwfx_temp = struct.unpack('HHLLHH',BytesRecv[:16]) #print(tuple_pwfx_temp) if tuple_pwfx_temp[2] != list_pwfx[2]: print('采樣率發生變化!') if tuple_pwfx_temp[2] in [11025,12000,44100,48000]: #把元組轉換為列表,再賦值修改采樣率 list_pwfx[2] =list(tuple_pwfx_temp)[2] #關閉設備並重新初始化設備 device.close() device = alsaaudio.PCM() device.setrate(list_pwfx[2]) device.setperiodsize(list_pwfx[2]//10) print('采樣率正確,修改采樣率為%s'%(list_pwfx[2])) else: print('采樣率錯誤!'%(list_pwfx[2])) #將socket接收到的數據送到device播放 #收到的數據包,第41字節開始才是音頻數據 device.write(BytesRecv[40:]) print('RecCount=%s'%(RecCount),end='\r') RecCount+=1 device.close() sss.close()
同時運行發送端程序和接收端程序,在發送端打開音樂播放器,這個時候,OrangPi接的音箱應該能播放音樂了。
五、設置python腳本開機自啟動
好了,最后一步就是把這個python腳本設定成開機自啟動,這樣就不用每次登錄OrangPi Zero運行這個腳本,linux下實現python腳本開機自動啟動的方法也簡單,“Linux下Python腳本自啟動與定時任務詳解”這個文章有詳細介紹,修改一下系統配置文件即可。