pyaudio库:音频处理

pyaudio文档,大多数变量和接口的定义还是在C版本的PortAudio文档中
PyAudio对象只负责播放音频,不负责从文件中读取二进制数据,所以读取要在外面进行,给到它的是二进制数据,一般会结合wave库一起使用,wave库负责读数据以及获取音频的一些基本信息。下面是一些用例:

import wave
import pyaudio
audio = pyaudio.PyAudio()  # 新建一个PyAudio对象
# wave.open跟python内置的open有点类似,从wf中可以获取音频基本信息
with wave.open(path, "rb") as wf:stream = audio.open(format=pyaudio.paInt16,  # 指定数据类型是int16,也就是一个数据点占2个字节;paInt16=8,paInt32=2,不明其含义,先不管channels=wf.getnchannels(),  # 声道数,1或2rate=wf.getframerate(),  # 采样率,44100或16000居多frames_per_buffer=1024,  # 每个块包含多少帧,详见下文解释output=True)  # 表示这是一个输出流,要对外播放的# getnframes获取整个文件的帧数,readframes读取n帧,两者结合就是读取整个文件所有帧stream.write(wf.readframes(wf.getnframes()))  # 把数据写进流对象stream.stop_stream()  # stop后在start_stream()之前不可再read或writestream.close()  # 关闭这个流对象
audio.terminate()  # 关闭PyAudio对象

需要注意的是,在write之后stream就会阻塞,开始播放音频,如果这个音频文件有三分钟,那么程序就会停在write这三分钟,直到把整个音频播放完毕,再继续往下执行。然而在很多应用中,我们需要的是实时问答,中途可以打断对话,也就是说不应该一次性把一整段话都write进来,应该分批写入。pyaudio自然也考虑到了这个问题,它已经有了可用的解决方案——回调函数callback。回调函数有固定的格式:

callback(in_data,      # 如果input=True,in_data就是录制的数据,否则为Noneframe_count,  # 帧的数量,表示本次回调要读取几帧的数据time_info,    # 一个包含时间信息的dict,略status_flags): # 状态标志位,略pass

回调函数由pyaudio新开一个线程来执行,因此不会阻塞。由于回调函数只有固定的4个参数,诸如文件指针等参数并不在列,因此只能通过类数据成员的方式传给callback函数(全局变量好像不行,因为callback是在另外一个线程执行的)。在给出例子之前,需要明确一些概念(部分解释来自自己的理解,其他来自参考链接):

rate: 采样率,即每秒的帧frame
frame: 帧,指一个时刻上的数据点,多个声道在同一时刻的数据点的集合称为frame
frames_per_buffer: 每个缓冲区保存多少帧frame,一个buffer也称为一个块chunk,每次调用回调函数都会读/写一个块chunk的数据(具体实现还是我们自己写,不是自动的)

因此,当使用int16类型保存数据点时,单声道1个数据点占用2字节,双声道2个数据点占用4个字节,所以双声道1帧就占用4个字节。frames_per_buffer通常为1024,那么1个块就占用4096个字节。那我们接着给出带回调函数的音频播放例子(上一个例子有讲解过的语句这里就不再放注释了):

import pyaudio
import wave
import timepath = "background.wav"
wf = wave.open(path, "rb")
wav_data = wf.readframes(wf.getnframes())
meta = {"seek": 0}  # int变量不能传进函数内部,会有UnboundLocalError,所以给它套上一层壳def callback(in_data, frame_count, time_info, status):# 这是针对流式录制,只有二进制数据没有保存到本地的wav文件时的做法,通过文件指针偏移读取数据start = meta["seek"]meta["seek"] += frame_count * pyaudio.get_sample_size(pyaudio.paInt16) * wf.getnchannels()data = wav_data[start: meta["seek"]]# 如果有保存成wav文件,直接用文件句柄readframes就行,不用像上面那么麻烦
#     data = wf.readframes(frame_count)return (data, pyaudio.paContinue)audio = pyaudio.PyAudio()
stream = audio.open(format=audio.get_format_from_width(wf.getsampwidth()),channels=wf.getnchannels(),rate=wf.getframerate(),output=True,stream_callback=callback)stream.start_stream()# 对callback更进一步的理解见本文下一段的“根据文档原话学pyaudio”以及"根据实验结果推测pyaudio的内部实现"
# 划重点:start_stream()之后stream会开始调用callback函数并把得到的data进行stream.write(),
# 直到状态码变为paComplete就停止读取(一般是到达音频文件末尾)。由于回调函数是通过另开线程调用的,
# 它也需要像平常多线程代码一样有类似join的操作,否则主线程stop_stream()就没了。因此这里的while循环必不可少,
# 当stream仍处于活跃状态(应该就是音频文件还没读完)时,让主线程休眠,也就是让主线程等子线程执行完。
# 而我们又可以在while循环里添加一些条件判断,在整个音频播放完成之前提前跳出循环,结束播放,这样就实现了前面所说的实时问答,打断说话的效果
# stream初始化后到stop_stream()之前,is_active()都是True,stop_stream()之后变成False,start_stream()之后又变成True,它表示的是这个流是否开放读写,实际上也对应音频数据是否读完,因为数据一读完stream就会被stop
while stream.is_active():time.sleep(0.1)stream.stop_stream()
stream.close()
wf.close()audio.terminate()

另外,有些例子会在音频播放结束之后加一句休眠语句,以防音频由于设备延迟还没播完就被关闭了:

time.sleep(stream.get_output_latency())

get_output_latency()方法用于获取设备的输出时间延迟,该值与播放的音频内容无关,与播放音频的设备类型有关,可能与缓冲区大小有关,没有测试过。
输出时间延迟是指音频样本由应用生成到通过耳机插孔或内置扬声器播放之间的时间。
输入延迟时间是指音频信号由设备音频输入(如麦克风)接收到相同音频数据可被应用使用的时间。参考链接
不过实际上我自己不加这个sleep也没有感觉到被突然切断,所以这行代码作用有多大暂时不清楚。

根据文档原话学pyaudio
1.Start processing the audio stream using pyaudio.Stream.start_stream() (4), which will call the callback function repeatedly until that function returns pyaudio.paComplete. Note: stream_callback is called in a separate thread (from the main thread).
1*.在 stream对象调用start_stream()方法之后,会新开一个线程调用回调函数,不停地读取数据进行stream.write(),直到把回调函数返回pyaudio.paComplete之后就不再调用回调函数,接着把stream里头剩下的数据播放完毕。

2.out_data is a byte array whose length should be the (frame_count * channels * bytes-per-channel) if output=True or None if output=False. flag must be either paContinue, paComplete or paAbort (one of PortAudio Callback Return Code). When output=True and out_data does not contain at least frame_count frames, paComplete is assumed for flag.
2*.这里给出了回调函数应当返回的音频块长度(字节),以及何时返回paComplete何时返回paContinue。当音频块是一整块时,返回paContinue,不足一整块时(也即音频数据的最后一个块)返回paComplete。但是根据实验表明,返回paComplete时这个不完整的块不会被播放,但我们需要这一部分音频,因此我们依然要返回paContinue

根据实验结果推测pyaudio的内部实现
1.在回调函数里返回某个固定的块和paContinue。结果:重复播放这个块,陷入死循环
1*.与文档所述相同,只要有paContinue就会一直取数据

2.在回调函数中返回整个块和paComplete。结果:不播放这个块,结束。
2*.与文档所述不同,返回paComplete时这个块即使完整也不会被播放,但我们需要这一个块,因此我们实践中最后一个不完整的块依然要返回paContinue

3.在回调函数中返回不完整的块和paContinue。结果:不播放这个块,结束。
3*.可能代码内部是有做什么检查的,就算给了paContinue,块不是完整的也不给发,所以最后一个不完整的块需要补b"\x00"或者别的什么空数据补足一个块,返回了才会起作用。

4.把块长度调整为5s音频,令程序在加载完第2个块后退出。结果:停顿了几秒钟才开始播音,播了几秒钟就结束了。更细致的实验:计算程序运行时间,令程序在加载完第2个块后退出,用时10.4s,手机测得从程序开始运行到有声音大约需要5秒;令程序在加载完第4个块后退出,用时20.5s。在回调函数中截取数据所需时间非常之短。
使用一段24.5s的音频跟一段21s的音频令其在最后一个块给出paComplete,运行时间相同;令二者在最后一个块给出paContinue,运行时间不变;令二者补足至完整块长度并返回paContinue,音频可以播放完整,运行时间多了一个块长度。每0.5秒输出一次is_active()的值,发现对于上述两段音频,该值都是在第30秒也就是播音结束的时刻(5+25)才变为False
4*.初步推测是代码把回调函数发来的第一个音频块完全加载进流内部之后才开始播音,而且这个加载所需的时间跟按配置的采样率播放完这段音频的时间完全一致在回调函数中停留的时间非常短,但又隔了很久才开始播音,因此时间应该花在把这个块的数据加载进某个地方上了,可能是设备缓冲区啥的。在回调函数调用结束之后,如果收到paComplete,则结束调用死循环,执行stop_stream(),此时is_active()结果将变为False,主线程的sleep死循环被打破。
首先简记播放一个块所需的时间为T,我猜是这样的,有一个缓冲区,代码内部的循环会每隔T时间检测一次缓冲区,如果有数据就拿走播放,没有就不播放(等价于播放b"\x00"序列)。最开始的时候缓冲区为空,因此前T时间设备没有声响;在设备播放从缓冲区中拿到的数据时,线程通过回调函数拿到了下一个T对应的音频数据,放进缓冲区;缓冲区的大小是T时间对应的块长度,也就是只能容纳一个块。设备播放完一个块的数据后又到缓冲区拿数据播放,所以听起来是连续的;缓冲区的数据被拿走后回调函数再次启动,前去拿回一个块的数据放到缓冲区,也不拿多,可能是觉得太多比较占用内存,而且拿肯定比播放的速度快,没有必要拿那么多一直放在那。调用回调函数的程序应该也是一个阻塞的过程,当缓冲区为空时才会启动回调函数,否则就在那干等着。在播放完一个块的音频,准备去缓冲区取数据之前,检查pa状态,如果是paComplete则停止播放,流对象也stop_stream(),退出子线程。如果回调函数给的是paContinue但数据却不是完整的块,状态变量仍会被置为paComplete。在最开始的时候,pa状态是初始值paContinue,去缓冲区取数据,缓冲区数据应该是全b"\x00",所以没有声音;在最后的时候,pa状态变成paComplete,所以不会去播放那个不完整的块。程序是否播放音频,看的并不是缓冲区是否为空,因为内存空间一定有值,它没有“空”的概念,应该是由状态变量pa来控制播不播放。
总结:只有块长度完整且返回值是paContinue的块才会被放入缓冲区;只要被放进缓冲区,一定会被取走播放;在最后一个块播放结束之后,stream.is_active()才会被置为False
因此,正常情况下流式播放并不需要time.sleep(stream.get_output_latency()),因为出while循环的时候就已经播放完毕了。播放的音频最后有缺失一定不是因为sleep得不够久,而一定是回调函数没有补齐最后一个块并给出paContinue的。

搞懂了回调函数的原理之后,录音部分就很简单了:

# 阻塞式录制
import pyaudio
import wave
record_seconds = 3  # 录制时长/秒
pformat = pyaudio.paInt16
channels = 1
rate = 16000  # 采样率/Hzaudio = pyaudio.PyAudio()
stream = audio.open(format=pformat,channels=channels,rate=rate,input=True)wav_data = stream.read(int(rate * record_seconds))
with wave.open("tmp.wav", "wb") as wf:wf.setnchannels(channels)wf.setsampwidth(pyaudio.get_sample_size(pformat))wf.setframerate(rate)wf.writeframes(wav_data)stream.stop_stream()
stream.close()
audio.terminate()
# 非阻塞式录音,省略import语句
path = "record.wav"
data_list = []  # 录制用list会好一点,因为bytes是常量,+操作会一直开辟新存储空间,时间开销大def callback(in_data, frame_count, time_info, status):data_list.append(in_data)# output=False时数据可以直接给b"",但是状态位还是要保持paContinue,如果是paComplete一样会停止录制return b"", pyaudio.paContinuerecord_seconds = 3  # 录制时长/秒
pformat = pyaudio.paInt16
channels = 1
rate = 16000  # 采样率/Hzaudio = pyaudio.PyAudio()
stream = audio.open(format=pformat,channels=channels,rate=rate,input=True,stream_callback=callback)stream.start_stream()t1 = time.time()
# 录制在stop_stream之前应该都是is_active()的,所以这里不能靠它来判断录制是否结束
while time.time() - t1 < record_seconds:time.sleep(0.1)wav_data = b"".join(data_list)
with wave.open("tmp.wav", "wb") as wf:wf.setnchannels(channels)wf.setsampwidth(pyaudio.get_sample_size(pformat))wf.setframerate(rate)wf.writeframes(wav_data)stream.stop_stream()
stream.close()
audio.terminate()

如果对文中内容有疑问或者建议,欢迎在评论区跟我交流讨论~

PyAudio模块的基本使用,阻塞式/非阻塞式地录制/播放音频相关推荐

  1. python gevent模块 下载_Python协程阻塞IO非阻塞IO同步IO异步IO

    Python-协程-阻塞IO-非阻塞IO-同步IO-异步IO 一.协程 协程又称为微线程 CPU 是无法识别协程的,只能识别是线程,协程是由开发人员自己控制的.协程可以在单线程下实现并发的效果(实际计 ...

  2. struct用法_精讲响应式webclient第1篇-响应式非阻塞IO与基础用法

    笔者在之前已经写了一系列的关于RestTemplate的文章,如下: 精讲RestTemplate第1篇-在Spring或非Spring环境下如何使用 精讲RestTemplate第2篇-多种底层HT ...

  3. 系统间通信1:阻塞与非阻塞式通信B

    版权声明:本文引用https://yinwj.blog.csdn.net/article/details/48274255 接上篇:系统间通信1:阻塞与非阻塞式通信A 4.3 NIO通信框架 目前流行 ...

  4. 精讲响应式WebClient第2篇-GET请求阻塞与非阻塞调用方法详解

    本文是精讲响应式WebClient第2篇,前篇的blog访问地址如下: 精讲响应式webclient第1篇-响应式非阻塞IO与基础用法 在上一篇文章为大家介绍了响应式IO模型和WebClient的基本 ...

  5. python网络编程基础(线程与进程、并行与并发、同步与异步、阻塞与非阻塞、CPU密集型与IO密集型)...

    python网络编程基础(线程与进程.并行与并发.同步与异步.阻塞与非阻塞.CPU密集型与IO密集型) 目录 线程与进程并行与并发同步与异步阻塞与非阻塞CPU密集型与IO密集型 线程与进程 进程 前言 ...

  6. 创业笔记-Node.js入门之阻塞与非阻塞

    阻塞与非阻塞 正如此前所提到的,当在请求处理程序中包括非阻塞操作时就会出问题.但是,在说这之前,我们先来看看什么是阻塞操作. 我不想去解释"阻塞"和"非阻塞"的 ...

  7. 那些年让你迷惑的阻塞、非阻塞、异步、同步

    点击上方"方志朋",选择"置顶或者星标" 你的关注意义重大! 在IT圈混饭吃,不管你用什么编程语言.从事前端还是后端,阻塞.非阻塞.异步.同步这些概念,都需要清 ...

  8. 阻塞、非阻塞、多路复用、同步、异步、BIO、NIO、AIO 一锅端

    承接上文的操作系统,关于IO会涉及到阻塞.非阻塞.多路复用.同步.异步.BIO.NIO.AIO等几个知识点.知识点虽然不难但平常经常容易搞混,特此Mark下,与君共勉. 1 阻塞跟非阻塞 1.1 阻塞 ...

  9. 【NIO】阻塞与非阻塞

    "阻塞"与"非阻塞"与"同步"与"异步"不能简单的从字面理解,提供一个从分布式系统角度的回答. 1.同步与异步 同步和异 ...

  10. Verilog初级教程(15)Verilog中的阻塞与非阻塞语句

    文章目录 前言 正文 阻塞赋值 非阻塞赋值 往期回顾 参考资料以及推荐关注 前言 本文通过仿真的方式,形象的说明阻塞赋值以及非阻塞赋值的区别,希望和其他教程相辅相成,共同辅助理解. 正文 阻塞赋值 阻 ...

最新文章

  1. 网络工程制图论文计算机,学习系统与工程制图论文
  2. html拖放数据库字段,HTML5 拖放(Drag 和 Drop)
  3. python导入包相当于什么_Python中使用语句导入模块或包的机制研究
  4. 清华竟然开设:《摸鱼学导论》,这门课火了!
  5. java 中PriorityQueue优先级队列使用方法
  6. win10计算器rsh_酷到你认不出!新Win10计算器上手体验
  7. Android注册时总是出现验证码不正确问题的解决
  8. POJ-Bound Found | 尺取法+绝对值特性
  9. 【渝粤教育】广东开放大学 插画与漫画 形成性考核 (27)
  10. 计算机桌面上的声音图标没了怎么办,右下角小喇叭不见了-电脑桌面右下角有一个调整声音的小喇叭图标没有了,怎么能调出来,电? 爱问知识人...
  11. 基于IDEA Plugin插件开发,撸一个DDD脚手架
  12. NOTION 换深色背景 黑色背景
  13. 套路之王 - 招投标软件项目管理
  14. Android应用程序如何进行系统签名
  15. EasyUI TreeGrid滚动条异常
  16. 磊科路由器如何设置虚拟服务器,nw711磊科路由器设置桥接步骤图文
  17. 考勤月度统计mysql_mysql中跨月统计考勤天数-问答-阿里云开发者社区-阿里云
  18. dos命令使用默认浏览器打开网址
  19. 光流法的三维运动表示
  20. airtest测试网页_Airtest之web自动化(一)

热门文章

  1. WBS——工作分解结构
  2. 酒水饮料类零售库存管理软件app,哪个简单好用?看看这10款
  3. tableau中文版教程pdf_快速入门Tableau详细教程(
  4. livereload(自动刷新)
  5. java cms 源码_PublicCMS开源Java系统 v4.0.190312
  6. python分行政区域汇总_python:编写行政区域三级菜单(day 1)
  7. python提取html表格_用Python抓取HTML表格
  8. 《Java程序设计》期末复习资料
  9. python入门到精通,一篇就够。40个python游戏经典开源项目(开源分享:俄罗斯方块、魂斗罗、植物大战僵尸、飞机大战、超级玛丽...)
  10. python自动答题助手_头脑王者python答题助手