1.背景
在一些声学测量测试中,经常会用到单频声和白噪声,比如测量单个频率是否失真、测量整个频带下的频响等等。测试过程中不可能经常携带白噪声和大量不同频率的单频声文件。此时拥有一个能够直接播放单频声和白噪声的小软件就显得非常实用了。
本文基于Pyaudio设计了一个单频声和白噪声的播放器,通过简单的设置即可播放指定频率的单频声,而无需在本地存取音频文件。Pyaudio还可以实现音频录制、音频文件写入、音频文件读取等功能,若有需要可以参考官方文档
小程序的设计参考了 Single frequency wave generation using HTML5 audio 网页中的功能。在新版浏览器下,该网页的声音播放功能已经失效。因此本人在学习pyqt5界面设计的同时,在python下实现了上述网页的主要功能。
个人认为,从实用的角度来说,对于理工科专业,掌握一整套 从 基础专业理论 到 算法 再到 配套软件开发 的技能体系是非常有必要的。
2.程序界面及下载链接
2.1 程序界面
软件界面实现了如下功能:
- 单频声或白噪声的单次播放或循环播放,内置采样率为40000Hz。
- 单次播放下的时长可调。
- 单频声播放时频率可调。
- 绘制并显示前600个采样点的波形。
- 播放过程中不会在本地产生音频文件缓存
2.2 下载链接
程序大小为约44M,下载链接如下:
链接:https://pan.baidu.com/s/1dYj4Frs-mL0uS4iFRjEf4g
提取码:pflc
–来自百度网盘超级会员V5的分享
3.代码
想要学习和交流的同学继续往下看。
pyaudio官方教程中只给出了怎么录制并保存音频文件以及读取并播放音频文件,对于想更加直接播放指定信号的同学可以参考以下代码。主要部分都进行了注释。
# _*_coding: UTF-8_*_
# 开发作者 :TXH
# 开发时间 :2021-09-26 10:14
# 文件名称 :Sound_Generator.py
# 开发工具 :Python 3.7 + Pycharm IDE
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt_Learning.单频声和白噪声生成器.单频声和白噪声生成器 import Ui_MainWindow
from PyQt5 import QtCore
from PyQt5.QtCore import QThread,pyqtSlot
import pyaudio
import numpy as np
class QmyDialog(QMainWindow): # Qt界面,主线程
send_para = QtCore.pyqtSignal(list) # 主线程信号
def __init__(self, parent=None):
super().__init__(parent)
self.pause=False
self.statusBar().showMessage('初始化...')
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.ui.radioButton.setChecked(True)
# 初始化绘图窗
self.canvas = self.ui.widget.canvas
self.canvas.ax1 = self.canvas.fig.add_subplot(111)
self.canvas.ax1.get_yaxis().grid(False)
# 创建子线程实例
self.pause,self.cycle =False,False
self.load_para()
self.Play=Play_Thread([self.pause, self.freq, self.dur, False])
self.send_para.connect(self.Play.para_update) # 信号与信号槽的关联
self.rate = 40000
self.statusBar().showMessage('就绪')
def on_radioButton_toggled(self,checked): # 选择正弦模式
if checked:
self.ui.lineEdit.setEnabled(True)
else:
self.ui.lineEdit.setEnabled(False)
def on_radioButton_2_toggled(self, checked): # 选择白噪声模式
if checked:
self.ui.lineEdit.setEnabled(False)
else:
self.ui.lineEdit.setEnabled(True)
@pyqtSlot()
def on_pushButton_clicked(self): # 点击播放按钮
self.pause,self.cycle= False,False # 停止标示符为假,循环标识符为假
self.emit_para() # 发送数据给子线程
self.plot_fig(self.signal[:600]) # 绘图
self.Play.start() # 运行子线程,自动运行run方法
@pyqtSlot()
def on_pushButton_2_clicked(self): # 点击循环播放按钮
self.statusBar().showMessage('循环播放中...')
self.ui.radioButton.setEnabled(False)
self.ui.radioButton_2.setEnabled(False)
self.ui.lineEdit_2.setEnabled(False)
self.pause, self.cycle= False,True # 停止标示符为假,循环标识符为真
self.emit_para()
self.plot_fig(self.signal[:600])
self.Play.start()
@pyqtSlot()
def on_pushButton_3_clicked(self): # 点击停止播放按钮
self.statusBar().showMessage('就绪')
self.pause = True # 停止标示符为真
self.emit_para()
self.ui.radioButton.setEnabled(True)
self.ui.radioButton_2.setEnabled(True)
self.ui.lineEdit_2.setEnabled(True)
def load_para(self):
self.freq = int(self.ui.lineEdit.text())
self.dur = int(self.ui.lineEdit_2.text())
self.isWhite = self.ui.radioButton_2.isChecked()
def plot_fig(self, signal): # 绘制图形函数
self.canvas.ax1.clear()
self.canvas.ax1.plot(signal)
self.canvas.fig.tight_layout()
self.canvas.draw()
def emit_para(self): # 发送list类型的信号给子线程
self.load_para()
if self.cycle:
len= int(self.freq * self.rate)
while len%1000==0 and len>10000:
len = int(len/10)
if self.isWhite:len=len*10
else:
len = int(self.dur * self.rate)
if self.isWhite:
signal = np.random.randn(len)
signal=signal/max(abs(signal))
else:
t = np.arange(len) / self.rate
signal = np.sin(2 * np.pi * self.freq * t)
self.signal = signal.astype('float32')
self.send_para.emit([self.pause, self.signal,self.freq,self.cycle])
class Play_Thread(QThread): # 音频播放子线程
def __init__(self,para_list):
super().__init__()
self.rate=40000 # 采样率
self.frames_per_buffer = 1000 # 音频流缓存大小
self.pause, self.signal, self.freq, self.cycle = para_list
self.player = pyaudio.PyAudio()
def run(self): # 子线程运行函数
signal = self.signal
len=self.signal.size
stream = self.player.open(format=pyaudio.paFloat32, # 设置音频流参数
channels=1,
rate=self.rate,
output=True)
if self.cycle: # 循环播放信号
while not self.pause:
for i in range(len // self.frames_per_buffer):
# 将数据转换为bytes编码格式,此处实现了信号直接播放的功能!免去了存取本地音频文件的过程。
stream.write(bytes(signal[i * self.frames_per_buffer:(i + 1) * self.frames_per_buffer]))
if self.pause:
stream.stop_stream()
stream.close()
break
else: # 单次播放
for i in range(len // self.frames_per_buffer):
stream.write(bytes(signal[i * self.frames_per_buffer:(i + 1) * self.frames_per_buffer]))
if self.pause:
stream.stop_stream()
stream.close()
break
# 播放完关闭音频流
stream.stop_stream()
stream.close()
@pyqtSlot(list)
def para_update(self,para_list): # 子线程信号槽:连接主线程中的list信号
self.pause,self.signal,self.freq,self.cycle=para_list
def __exit__(self):
self.player.terminate()
app = QApplication(sys.argv) # 调用父类构造函数,创建窗体
form = QmyDialog() # 创建UI对象
form.show() # 展示界面
sys.exit(app.exec())