使用PyAudio模块播放音频流之体会

python基础

浏览数:508

2019-11-1

AD:资源代下载服务

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也不管用——翻车啦

作者:爱拼安小匠