选择python,页面采用第三方库pyqt5。 对于界面的布局,简单如下所示:
# -*- coding: utf-8 -*- import sys from PyQt5.QtWidgets import QApplication, QWidget, QMainWindow, QAction, qApp class Example(QMainWindow): def __init__(self): super().__init__() self.initUI() # 界面绘制交给InitUi方法 def initUI(self): # 设置窗口的位置和大小 self.setGeometry(300, 300, 800, 520) # 创建一个菜单栏 menubar = self.menuBar() # 添加菜单 fileMenu = menubar.addMenu('&选择文件') imageMenu = menubar.addMenu('&更多图像') functionMenu = menubar.addMenu('&功能') filterMenu = menubar.addMenu('&音频提取') # self.statusBar() openAction = QAction('打开文件', self) openAction.setShortcut('Ctrl+F') openAction.setStatusTip('打开所需的音频文件') openAction.triggered.connect(self.getfile) spectrogramAction = QAction('频谱图', self) spectrogramAction.triggered.connect(self.getfile) recordAction = QAction('录音', self) recordAction.triggered.connect(self.getfile) playAction = QAction('播放', self) playAction.triggered.connect(self.getfile) cutAction = QAction('截取', self) cutAction.triggered.connect(self.getfile) separateAction = QAction('音频分离', self) separateAction.triggered.connect(self.getfile) # 添加事件 fileMenu.addAction(openAction) imageMenu.addAction(spectrogramAction) functionMenu.addAction(recordAction) functionMenu.addAction(playAction) functionMenu.addAction(cutAction) filterMenu.addAction(separateAction) # 显示窗口 self.show() def getfile(self): return 0 if __name__ == '__main__': # 创建应用程序和对象 app = QApplication(sys.argv) ex = Example() sys.exit(app.exec_())大体样子:
功能介绍,首先要打开我们所选择的wav文件,其代码如下:
openfile_name = QFileDialog.getOpenFileName(self, '选择文件', '', 'wav files(*.wav)')其次,要弹出弹框,对于弹出弹框,采用的是MDI框架框架,即主要组件为QMdiArea和QMdiSubWindow,其中QMdiArea一般使用于主窗口中,用于容纳多个子窗口QMdiSubWindow。 主要代码如下:
class MainWindow(QMainWindow): count = 0 def __init__(self,parent=None): super(MainWindow, self).__init__(parent) self.initUI() # 界面绘制交给InitUi方法 # 实例化Qmidarea区域 self.mdi = QMdiArea() # cascadeSubWindows():安排子窗口在Mdi区域级联显示 self.mdi.cascadeSubWindows() # 设置为中间控件 self.setCentralWidget(self.mdi) def initUI(self): ... def getfile(self): openfile_name = QFileDialog.getOpenFileName(self, '选择文件', '', 'wav files(*.wav)') if openfile_name[0]!='': print(openfile_name) self.count = self.count + 1 # 实例化多文档界面对象 sub = QMdiSubWindow() # 向sub内部添加控件 sub.setWidget(QTextEdit()) sub.setWindowTitle("subWindow %d" % self.count) self.mdi.addSubWindow(sub) sub.show() else: print("取消音频文件") return 0librosa.display.waveplot(y,sr = 22050,max_points = 50000.0,x_axis =‘time’,offset = 0.0,max_sr = 1000,ax = None, kwargs ) 绘制波形的幅度包络线。
如果y是单声道的,则在[-abs(y),abs(y)]之间绘制一条填充曲线。如果y为立体声,则在[-abs(y [1]),abs(y [0])]之间绘制曲线,以便分别在轴的上方和下方绘制左通道和右通道。 在绘制之前,长信号(duration >= max_points)被下采样至最高max_sr。参数:
y :np.ndarray [shape =(n,)或(2,n)] 音频时间序列(单声道或立体声)sr :数字> 0 [标量] y的采样率max_points :正数或无 要绘制的最大时间点数:如果max_points超过y的持续时间,则对y进行下采样。如果为None,则不执行下采样。x_axis :str或无 显示x轴刻度和刻度标记。可接受的值为: ‘time’ :标记以毫秒,秒,分钟或小时显示。值以秒为单位绘制。 ‘s’:标记显示为秒。 ‘ms’:标记以毫秒为单位显示。 “lag”:与时间一样,但超过中途点则视为负值。 ‘lag_s’:与滞后相同,但以秒为单位。 ‘lag_ms’:与lag相同,但以毫秒为单位。 None,‘none’或’off’:刻度线和刻度线标记被隐藏。ax:matplotlib.axes.Axes or None 要绘制的轴,而不是默认的plt.gca()。offset:float 水平偏移(以秒为单位)以开始波形图max_sr :数字> 0 [标量] 可视化的最大采样率具体可参考原文档https://librosa.org/librosa/generated/librosa.display.waveplot.html
采用的技术:基于PyQt Canvas Matplotlib图形绘制 (1)首先实现一个画布 画布MatplotlibWidget继承MyMplCanvas, MyMplCanvas继承matplotlib.backends.backend_qt5agg.FigureCanvasQTAgg类 其中MyMplCanvas画布中通过tup,将画布分为一个或者两个子plot。
from matplotlib.figure import Figure from matplotlib.backends.backend_qt5 import NavigationToolbar2QT as NavigationToolbar from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas import matplotlib.pyplot as plt from PyQt5 import QtWidgets, QtCore, QtGui from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5.QtWidgets import QSizePolicy from matplotlib import axes import matplotlib matplotlib.use("Qt5Agg") # 画布控件继承自matplotlib.backends.backend_qt5agg.FigureCanvasQTAgg类 class MyMplCanvas(FigureCanvas): # tup(x1,x2)——x1->行,x2->列,你明明全是tup(1,1) def __init__(self, tup, width=5, height=4, dpi=100): plt.rcParams['font.family'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False self.fig = Figure(figsize=(width, height), dpi=dpi) self.tup=tup self.checknumber(self.tup[0]*self.tup[1]) # 调用基类的初始化函数 FigureCanvas.__init__(self, self.fig) # self.setParent(parent) # 尺寸缩放策略 FigureCanvas.setSizePolicy(self, QSizePolicy.Expanding, QSizePolicy.Expanding) FigureCanvas.updateGeometry(self) def checknumber(self,number): if number==1: self.createaxes1() if number==2: self.createaxes2() def createaxes1(self): self.axes = self.fig.add_subplot(111) def createaxes2(self): self.axes1 = self.fig.add_subplot(211) self.axes2 = self.fig.add_subplot(212) # 再继承一个自定义画布控件类 class MatplotlibWidget(QWidget): def __init__(self, tup, parent=None): self.tup = tup super(MatplotlibWidget, self).__init__(parent) self.initUi() def initUi(self): self.layout = QVBoxLayout(self) self.mpl = MyMplCanvas(self.tup, width=5, height=4, dpi=100) self.mpl_ntb = NavigationToolbar(self.mpl, self) self.layout.addWidget(self.mpl) self.layout.addWidget(self.mpl_ntb) def getaxes(self): return self.mpl.axes def getaxes1(self): return self.mpl.axes1 def getaxes2(self): return self.mpl.axes2 def getntb(self): return self.mpl_ntb def getfig(self): return self.mpl.fig(2)将音频数据在画布上加载 用了线程,因为可能会同时打开多个音频,打开音频的操作需要异步操作。因此用到threading,具体threading使用查看本作者前一篇博文。 在重定义的run函数中,首先ibrosa.load(self.audiofile, sr=None)加载了音频, 再librosa.display.waveplot(y, sr, x_axis=‘time’, ax=self.widget.getaxes())展示在画布上
import librosa import threading import librosa.display from Matplotlib import MatplotlibWidget class WaveDisplayThreadEx(threading.Thread): def __init__(self,audiofile): threading.Thread.__init__(self) self.audiofile=audiofile self.tup=(1,1) self.widget=MatplotlibWidget.MatplotlibWidget(self.tup) # self.bDisplay = True def run(self): y, sr = librosa.load(self.audiofile, sr=None) librosa.display.waveplot(y, sr, x_axis='time', ax=self.widget.getaxes()) self.widget.show() # def stopdisplay(self): # self.bDisplay = False def getwidget(self): return self.widget(3)调用显示音频波形图 start()时候会调用run()
def getfile(self): self.openfile_name = QFileDialog.getOpenFileName(self, '选择文件', '', 'wav files(*.wav)') if self.openfile_name[0] != '': print(self.openfile_name) self.count = self.count + 1 # 实例化多文档界面对象 sub = QMdiSubWindow() # 向sub内部添加控件 waveui = WaveDisplayThreadEx(self.openfile_name[0]) waveui.start() sub.setWidget(waveui.getwidget()) sub.setWindowTitle("%s" % self.openfile_name[0]) sub.setGeometry(0, 0, 800, 480) self.mdi.addSubWindow(sub) sub.show() else: print("取消音频文件") return 0涉及到的代码段有recordUi.py(录音功能界面)、Record.py(录音)、WaveDisplay.py(录音过程中实时更新波形图)
参考连接https://blog.csdn.net/liang19890820/article/details/51537246
class recordUi(QWidget): def __init__(self, parent=None): super(recordUi, self).__init__(parent) self.setupUi() def setupUi(self): self.resize(543, 416) self.verticalLayout = QtWidgets.QVBoxLayout(self) self.verticalLayout.setContentsMargins(0, 0, 0, 0) self.verticalLayout.setObjectName("verticalLayout") self.verticalLayout.addWidget(self.widget) self.horizontalLayout = QtWidgets.QHBoxLayout() self.horizontalLayout.setObjectName("horizontalLayout") self.pushButton_2 = QtWidgets.QPushButton(self) self.pushButton_2.setObjectName("pushButton_2") self.pushButton_2.setText('开始录音') self.pushButton_2.clicked.connect(self.record) self.horizontalLayout.addWidget(self.pushButton_2) self.pushButton = QtWidgets.QPushButton(self) self.pushButton.setObjectName("pushButton") self.pushButton.setText('停止录音') self.pushButton.clicked.connect(self.stoprecord) self.horizontalLayout.addWidget(self.pushButton) self.verticalLayout.addLayout(self.horizontalLayout)需要wave模块与pyaudio模块相互作用,共同实现。其中wave实现音频文件的存储,pyaudio实现音频文件的录音。 wave写操作:
def save_wave_file(filename,data): '''save the date to the wavfile''' wf=wave.open(filename,'wb') wf.setnchannels(channels)#声道 wf.setsampwidth(sampwidth)#采样字节 1 or 2 wf.setframerate(framerate)#采样频率 8000 or 16000 wf.writeframes(b"".join(data))#https://stackoverflow.com/questions/32071536/typeerror-sequence-item-0-expected-str-instance-bytes-found wf.close()pyaudio录音
def get_audio(filepath): isstart = str(input("是否开始录音? (是/否)")) #输出提示文本,input接收一个值,转为str,赋值给aa if isstart == str("是"): pa = PyAudio() stream = pa.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK) print("*" * 10, "开始录音:请在5秒内输入语音") frames = [] # 定义一个列表 for i in range(0, int(RATE / CHUNK * RECORD_SECONDS)): # 循环,采样率 44100 / 1024 * 5 data = stream.read(CHUNK) # 读取chunk个字节 保存到data中 frames.append(data) # 向列表frames中添加数据data print(frames) print("*" * 10, "录音结束\n") stream.stop_stream() stream.close() # 关闭 pa.terminate() # 终结 save_wave_file(pa, filepath, frames) elif isstart == str("否"): exit() else: print("无效输入,请重新选择") get_audio(filepath)参考代码有https://blog.csdn.net/qq_29934825/article/details/82982737 https://blog.csdn.net/qq_36387683/article/details/91901815 https://blog.csdn.net/c602273091/article/details/46502527#pyaudio 结合以上两个功能,即可实现录音的整体功能,如下: 在录音时候利用while循环进行声音的录入,判定条件为bRecord,该值的改变,由停止录音按钮上绑定的一个函数调用stoprecord()函数实现
def stoprecord(self): self.timer.stop() self.record.stoprecord()录音实际实现类
import pyaudio import librosa import wave import librosa.display from PyQt5.QtCore import * from Matplotlib import MatplotlibWidget from pydub import AudioSegment class RecordThread(QThread): def __init__(self,audiofile='./record.wav',parent=None): super(RecordThread,self).__init__(parent) self.bRecord=True self.audiofile=audiofile self.chunk=1024 self.format=pyaudio.paInt16 self.channels=2 self.rate=16000 def run(self): audio=pyaudio.PyAudio() wavfile=wave.open(self.audiofile,"wb") wavfile.setnchannels(self.channels) wavfile.setsampwidth(audio.get_sample_size(self.format)) wavfile.setframerate(self.rate) wavstream=audio.open(format=self.format, channels=self.channels, rate=self.rate, input=True, frames_per_buffer=self.chunk) # self.dynamic_plot() while self.bRecord: wavfile.writeframes(wavstream.read(self.chunk)) wavstream.stop_stream() wavstream.close() def stoprecord(self): self.bRecord=False def __del__(self): self.working=False self.wait()对于一个反复重复的过程,我们可以使用pt中的QTimer,它提供了定时器信号和单触发定时器。
QTimer *timer = new QTimer(this); connect(timer, SIGNAL(timeout()), this, SLOT(update())); timer->start(1000);
start()之后,每秒都会调用update()。 参考链接https://blog.csdn.net/zz2862625432/article/details/79550285
在本功能实现中: recordUI.py中定义的函数,setUI中的按钮会调用以下函数功能。
def record(self): self.record=RecordThread() self.record.start() self.start_dynamic() def start_dynamic(self): time.sleep(0.2) self.wave_display.update() self.timer=QTimer(self) self.timer.timeout.connect(lambda: self.wave_display.update()) self.timer.start(100)以上代码中start(100)即为每100ms会自动执行一次update()函数 以下对应的是self.wave_display.update()函数的具体实现。
from math import pi import librosa import wave import librosa.display import numpy as np from PyQt5.QtCore import * from numpy.ma import sin from scipy.io import wavfile from Matplotlib import MatplotlibWidget from pydub import AudioSegment class WaveDisplayThread: def __init__(self,widget,audiofile='./record.wav',parent=None): self.audiofile=audiofile self.bDisplay=True self.widget=widget self.partfile='./part.wav' self.start_time = 0 self.end_time = 100 self.axes=self.widget.getaxes() self.ntb=self.widget.getntb() def stopdisplay(self): self.bDisplay=False def update(self): self.axes.clear() self.get_ms_part_wav(self.audiofile, self.start_time, self.end_time, self.partfile) y, sr = librosa.load(self.partfile, sr=None) librosa.display.waveplot(y, sr, ax=self.axes) self.ntb.draw() self.start_time += 100 self.end_time += 100 def getwidget(self): return self.widget # 按照每100sm的初末位置对音频进行截取,获取到当前100sm的音频文件保存在part.wav中,再显示出来波形,方式与上一部分的一致 def get_ms_part_wav(self,main_wav_path,start_time,end_time,part_wav_path): start_time=int(start_time) end_time=int(end_time) sound=AudioSegment.from_wav(main_wav_path) part=sound[start_time:end_time] part.export(part_wav_path,format="wav")