基於Orangpi Zero和Linux ALSA實現WIFI無線音箱(三)


作品已經完成,先上源碼:

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正常后,接下來介紹接收端程序中需要使用到的socketpyalsaaduio模塊。

二、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腳本自啟動與定時任務詳解”這個文章有詳細介紹,修改一下系統配置文件即可。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM