音频工具(pyqt)

  • Post author:
  • Post category:其他




需求

  • 打开音频文件
  • 录音
  • 播放打开的音频文件或录音
  • 将音频文件转为频谱图等(具体需要转换图的形式后续增加)
  • 裁剪音频
  • 将音频中人声与背景音乐声音的分离(需要自己训练模型)



界面代码

选择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")



版权声明:本文为invisibleI原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。