使用PyAudio模塊播放音頻流之體會

PyAudio是Python下的一個音頻處理模塊,用於將音頻流輸送到計算機聲卡上。理論上,該模塊能夠播放任何解碼器解碼而成的有效音頻幀。

安裝PyAudio

使用pip工具來安裝PyAudio:

pip3 install pyaudio

不同系統的安裝過程有所不同:

  • Windows下與Django、Flask等純Python模塊無異,不需編譯非Python語言的源碼。
  • 但在Linux下,還須編譯用C/C++編寫的底層音頻庫,這點類似於Node.js下那些擁有C++模塊、要用gyp單獨編譯的軟件包。
    • 需要PortAudio開發包的支持。

CSDN實踐

筆者根據CSDN上的這篇筆記實踐使用PyAudio模塊播放音頻。作者 @一個處女座的程序猿 所演示的格式爲WAV,使用Python自帶的Wave文件處理模塊wave進行解碼。但是,他的筆記寫的不夠完善,爲此筆者特地對代碼進行了修正,並加了更多註釋。

pyaudio:基於pyaudio利用Python編程實現播放音頻mp3、wav等格式文件 - CSDN博客
https://blog.csdn.net/qq_41185868/article/details/80500847

爲了更細緻地探究相關模塊運行時所產生的數據,筆者還將解碼時所產生的幀內容轉換成文本,記錄在文件當中。

最終筆者修改而成的代碼如下:

# 引入模塊
from pyaudio import *
import wave

def play():
    # 用文本文件記錄wave模塊解碼每一幀所產生的內容。注意這裏不是保存爲二進制文件
    dump_buff_file=open(r"Ring01.dup", 'w')
    
    chunk=1                                       # 指定WAV文件的大小
    wf=wave.open(r"Ring01.wav",'rb')              # 打開WAV文件
    p=PyAudio()                                   # 初始化PyAudio模塊
    
    # 打開一個數據流對象,解碼而成的幀將直接通過它播放出來,我們就能聽到聲音啦
    stream=p.open(format=p.get_format_from_width(wf.getsampwidth()), channels=wf.getnchannels(), rate=wf.getframerate(), output=True)
 
    data = wf.readframes(chunk)      # 讀取第一幀數據
    print(data)                        # 以文本形式打印出第一幀數據,實際上是轉義之後的十六進制字符串

    # 播放音頻,並使用while循環繼續讀取並播放後面的幀數
    # 結束的標誌爲wave模塊讀到了空的幀
    while data != b'':   
        stream.write(data)                # 將幀寫入數據流對象中,以此播放之
        data = wf.readframes(chunk)            # 繼續讀取後面的幀
        dump_buff_file.write(str(data) + "\n---------------------------------------\n")                    # 將讀出的幀寫入文件中,每一個幀用分割線隔開以便閱讀
        
    stream.stop_stream()            # 停止數據流
    stream.close()                        # 關閉數據流
    p.terminate()                          # 關閉 PyAudio
    print('play函數結束!')

play()

運行後,指定的文件Ring01.wav就會自動播放,你也能聽到音頻;音頻幀的內容也會在播放的同時自動記錄在Ring01.dup中。dup文件的內容摘錄如下:

b'\xff\xff\x00\x00'
---------------------------------------
b'\x00\x00\x00\x00'
---------------------------------------
b'\x00\x00\x00\x00'
---------------------------------------
b'\x00\x00\x00\x00'
---------------------------------------
b'\x00\x00\x00\x00'
---------------------------------------
b'\x00\x00\x00\x00'
---------------------------------------
b'\x00\x00\x00\x00'
---------------------------------------
b'\x00\x00\x00\x00'
---------------------------------------
b'\x00\x00\x00\x00'
---------------------------------------
b'\x00\x00\x00\x00'
---------------------------------------
b'\x00\x00\x00\x00'
---------------------------------------
b'\x00\x00\x00\x00'
---------------------------------------
b'\x00\x00\x00\x00'
---------------------------------------
b'\x00\x00\x00\x00'
---------------------------------------
b'\x00\x00\x00\x00'
---------------------------------------
b'\x00\x00\x00\x00'
---------------------------------------
b'\x00\x00\x00\x00'
---------------------------------------
b'\x00\x00\x00\x00'
---------------------------------------
b'\x00\x00\x00\x00'
---------------------------------------
b'\x00\x00\x00\x00'
---------------------------------------
b'\x00\x00\x00\x00'
---------------------------------------
b'\x00\x00\x00\x00'
---------------------------------------
b'\x00\x00\x00\x00'
---------------------------------------
b'\x00\x00\x00\x00'
---------------------------------------
b'\x00\x00\x01\x00'
---------------------------------------
b'\x00\x00\x01\x00'
---------------------------------------
b'\x00\x00\x01\x00'
---------------------------------------
b'\x00\x00\x01\x00'
---------------------------------------
b'\x00\x00\x01\x00'
---------------------------------------
b'\x00\x00\x01\x00'
---------------------------------------
b'\x00\x00\x01\x00'
---------------------------------------
b'\x00\x00\x01\x00'
---------------------------------------
b'\x00\x00\x00\x00'
---------------------------------------
b'\x01\x00\x00\x00'
---------------------------------------
b'\x01\x00\x00\x00'
---------------------------------------
b'\x01\x00\x00\x00'
---------------------------------------
b'\x01\x00\x00\x00'
---------------------------------------
b'\x01\x00\xff\xff'
---------------------------------------
b'\x01\x00\xff\xff'
---------------------------------------
b'\x01\x00\xff\xff'
---------------------------------------
b'\x01\x00\xff\xff'
---------------------------------------
......skipping......
b'\x00\x00\x00\x00'
---------------------------------------
b''
---------------------------------------

print、while與性能瓶頸

原作者的代碼有一處不合理:在while循環內使用print輸出緩衝區的內容。筆者在IDLE中嘗試,結果IDLE直接卡頓,滿屏都是輸出的十六進制轉義文本,滾屏嚴重掉幀,嚴重時直接假死;並且,程序也沒有輸出任何聲音——這讓我一時間以爲PyAudio只會捕捉音頻幀卻不會播放。嘗試註釋掉while循環內的print後,程序流暢正常,且久違的音頻出乎我意料地出現了。

性能反差如此之大,很重要的原因正是會造成性能瓶頸的屏幕輸出。使用Python的print方法將內容輸出到stdout的時候,整個程序的運行會被拖慢,你不得不等待屏幕上的那一行行輸出愛快不快地擠出來。這一現象有可能是因爲屏幕輸出是一個具有阻塞性的工作,部分屏顯的過程未完成,後面的過程就沒法繼續。少量文本的輸出問題不大,但如果是while循環這樣在短時間內可以執行無數代碼的循環,那造成的工作量不堪設想。想象一下,一條雙向兩車道的二級公路,只有一輛車時可以盡情地加速,但是車一多起來,這速度就被拉下來了,嚴重時更會堵出十里長車。

不過,如果輸出的目標不是stdout,而是硬盤上的文件,以上的瓶頸就不復存在了。同樣是在while循環中,筆者讓程序的每一次循環都在寫文件,但程序的運行沒受到任何影響,音頻照樣放,機器根本不卡。當然,輸出到硬盤上,仍會受到IO調度、硬盤本身性能、緩存與傳輸帶寬等因素的制約,但是它們在屏幕輸出的制約面前,根本就只能甘拜下風了。

如果以上的例子還不足以說明屏幕輸出和文件輸出之間的性能有多大區別,那麼不妨用MP3編解碼器lame做個實驗。對比執行以下兩個命令,將一個約5MB大小、128kbps/48000Hz的MP3文件解碼爲WAV格式:

  • test.mp3解碼輸出到test.wav
lame --decode test.mp3 test.wav
  • test.mp3解碼輸出爲WAV格式,輸出於stdout
lame --decode test.mp3 -

結果,前者安安靜靜幾秒至十幾秒搞定;而後者,滿屏都是亂碼輸出,一分鐘過去了都還沒結束,接着命令行窗口卡死,按Ctrl+C也不管用——翻車啦

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章