需求
- 打开音频文件
- 录音
- 播放打开的音频文件或录音
- 将音频文件转为频谱图等(具体需要转换图的形式后续增加)
- 裁剪音频
- 将音频中人声与背景音乐声音的分离(需要自己训练模型)
界面代码
选择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 0
将Matplotlib生成的图像用pyqt5展示出来
绘制WAV文件的波形
librosa.display.waveplot
librosa.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
利用matplotlib中FigureCanvasXAgg将图像渲染到qt上
采用的技术:基于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")