PyQt实战——实现可视化音频播放器(十三)

embedded/2024/12/28 11:53:13/

系类往期文章:
PyQt5实战——多脚本集合包,前言与环境配置(一)
PyQt5实战——多脚本集合包,UI以及工程布局(二)
PyQt5实战——多脚本集合包,程序入口QMainWindow(三)
PyQt5实战——操作台打印重定向,主界面以及stacklayout使用(四)
PyQt5实战——UTF-8编码器UI页面设计以及按钮连接(五)
PyQt5实战——UTF-8编码器功能的实现(六)
PyQt5实战——翻译器的UI页面设计以及代码实现(七)
PyQt5实战——翻译的实现,第一次爬取微软翻译经验总结(八)
PyQt5实战——翻译的实现,成功爬取微软翻译(可长期使用)经验总结(九)
PyQt实战——使用python提取JSON数据(十)
PyQt实战——随机涂格子的特色进度条(十一)
PyQt实战——实现编码器与进度条之间的通信,使进度条反映编码进度(十二)

前言

通过上一篇文章,我们已经大概了解了PyAudio是一个什么样的库以及给出了相应的示例代码,那么在本文中,我们就要使用PyAudio库,来实现音频的播放,同时呢我们将加上matplotlib库,来绘制音频的波形。通过阅读本文,你将了解到:如何使用PyAudio,将音频文件传入PyAudio,如何绘制音频波形,以及音频波形与音频播放的同步,如何避免音频卡顿问题。

展示

请添加图片描述

播放器思想

  • 首先,我们要通过PyAudio来实现音频的播放功能,通过读取PCM文件,将音频输出给外设(耳机,扬声器等)。
  • 此外,在音频输出的过程中,同时绘制当前读取数据块的折线图。
  • 在当前数据块被读取时,折线图将绘制完成,当数据块被读取完时,数据块所表示的音频将被播放完。
  • 将下一数据块的音频数据读取,然后重复上面的操作
  • __init__:在对象初始化时,将一些PyAudio的参数初始化完成,比如采样率,声道,采样点大小,数据块大小等。并创建画布
  • play:播放方法,关闭上一个音频流,如果有的话,打开音频流,开一个线程来执行播放与绘画
  • load_pcm_audio:从pcm文件中读取数据
  • play_audio:在音频流中写入数据块,并重新绘制折线图
  • update_parameters:更新播放器的参数,如采样率,声道等
  • closeEvent:关闭音频流,释放系统资源

代码展示

下面是 音频播放器的代码,而非播放器UI的代码:

python">from random import sample
import sys
import numpy as np
import pyaudio
import matplotlib.pyplot as plt
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QFileDialog
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import threadingclass AudioPlayer(QWidget):def __init__(self,QWidget):super().__init__()self.file = ''self.layout = QVBoxLayout()self.setLayout(self.layout)self.figure = plt.Figure()self.canvas = FigureCanvas(self.figure)self.layout.addWidget(self.canvas)# 初始化PyAudioself.p = pyaudio.PyAudio()# 设置播放参数self.sample_rate = 16000  # 采样率self.channels = 1         # 单声道self.sample_width = 2     # 16位深度self.chunk_size = 1024    # 每次播放的PCM数据块大小# 波形更新self.x_data = np.arange(self.chunk_size)self.y_data = np.zeros(self.chunk_size)self.plot = self.figure.add_subplot(111)self.line, = self.plot.plot(self.x_data, self.y_data)# 去掉坐标轴self.plot.axis('off')# 显式设置坐标轴范围,确保波形线延伸到整个画布self.plot.set_xlim(0, self.chunk_size)  # 设置x轴范围为数据块大小self.plot.set_ylim(-1, 1)  # 设置y轴范围为[-1, 1],16-bit PCM数据的常规范围self.audio_data = Noneself.index = 0self.stream = Nonedef play(self,filename):if self.file != filename:self.file = filenameself.load_pcm_audio(filename)self.index = 0  # 重置播放进度# 如果audio_data为空,或者没有选择音频,直接返回if self.audio_data is None:print("请先选择一个音频文件!")return# 每次播放前重置播放进度和音频流self.index = 0# 如果stream已经存在且正在播放,先停止它if self.stream is not None and self.stream.is_active():self.stream.stop_stream()self.stream.close()# 打开音频流if self.sample_width == 2:samplewith = pyaudio.paInt16elif self.sample_width == 4:samplewith = pyaudio.paInt32self.stream = self.p.open(format=samplewith,channels=self.channels,rate=self.sample_rate,output=True,frames_per_buffer=self.chunk_size)# 启动一个单独的线程来播放音频threading.Thread(target=self.play_audio, daemon=True).start()def load_pcm_audio(self, filename):# 读取PCM音频文件with open(filename, 'rb') as f:self.audio_data = np.frombuffer(f.read(), dtype=np.int16)def play_audio(self):while self.index < len(self.audio_data):chunk = self.audio_data[self.index:self.index + self.chunk_size]if len(chunk) < self.chunk_size and len(chunk) > 0:chunk = np.pad(chunk, (0, self.chunk_size - len(chunk)), 'constant')self.index += self.chunk_sizeself.stream.write(chunk.tobytes())# 更新波形数据self.y_data = chunk / 32768.0  # 16-bit PCM音频数据范围 [-1, 1]self.line.set_ydata(self.y_data)self.canvas.draw()def update_parameters(self, sample_rate, channels, sample_width):self.sample_rate = sample_rateself.channels = channelsself.sample_width = sample_widthdef closeEvent(self, event):if self.stream is not None:self.stream.stop_stream()self.stream.close()self.p.terminate()event.accept()

下面给出代码的详细解释:

_init_

  • 创建画布,绘制图像,并将画布加入到layout
  • 初始化PyAudio
  • 设置播放参数,默认16K采样率,单声道,16比特采样点,数据块大小为1024
  • 初始化坐标轴,在画布中,X轴不变,Y会随着数据块的更新而变化,每次更新折线图便会发生一次改变
  • 去掉坐标轴(美观)
  • X轴的范围是[0,1024],即一个数据块的大小,Y轴的范围是[-1,1],这是16bit PCM数据的常规范围
  • 初始化音频数据变量,索引值,音频流等

play

  • 如果当前的文件与重新选择的文件不一致,则会重新加载音频文件,如果没有音频文件,则会返回错误
  • 重置播放进度
  • 如果当前有音频流存在,暂停并结束它
  • 重新打开音频流
  • 单独开一个线程来执行播放音频和绘制波形

load_pcm_audio

  • 从文件中读取数据,我们来详细解释一下self.audio_data = np.frombuffer(f.read(), dtype=npint16)
  1. f.read()
  • f 是一个文件对象,通常是通过 open 打开文件后得到的。
  • f.read()读取文件内容,并将其作为一个字节串(bytes)返回。
    • 如果文件是二进制文件(比如 .wav.mp3 格式的音频文件),f.read() 会读取文件的所有字节内容。
    • f.read() 返回的数据是一个包含音频原始二进制数据的字节序列。
  1. np.frombuffer()
  • np.frombuffer()NumPy 提供的一个函数,用于从缓冲区(字节串、字节流等)中创建一个 NumPy 数组。
  • 它将原始的二进制数据解释为指定数据类型的数组。
  • 该函数通常用于将文件中的二进制数据(如音频文件)转换为 NumPy 数组,方便进一步处理。
  1. dtype=np.int16
  • dtype参数指定了生成的NumPy数组的数据类型。在这里,dtype=np.int16表示将字节数据转换为 16 位整数(int16)。
    • np.int16 表示每个数据元素是一个 16 位有符号整数(即每个值占 2 个字节)。
    • 16 位整数常用于表示音频数据,因为音频信号通常是通过这种方式存储的,特别是当音频使用 PCM(脉冲编码调制)格式时。
  1. self.audio_data
  • 这段代码将np.frombuffer()返回的NumPy数组赋值给self.audio_dataself.audio_data用于存储读取的音频数据。
    • 该数组的元素是从音频文件中读取的 PCM 数据,每个元素是一个 16 位整数,表示音频样本的幅度值。

play_audio

  • 判断index当前音频的进度,如果还没读取完数据,则将audio_data中的数据分块传给chunk
  • 如果chunk的长度小于1024,说明audio_data已经到了最后一个数据块,且大小不等于1024,因此需要在chunk后面补零
  • 更新index音频播放进度
  • chunk数据块的数据写入音频流中
  • chunk数据缩放到[-1,1]中
  • 绘制折线图

这里值得注意的是,为什么当chunk不足1024时,需要啊在chunk后面补零呢,是画布的X轴大小为1024,如果Y轴的数据没有1024个,则无法完成绘画

update_parameters

  • 更新参数方法,供上层UI界面调用

closeEvent

  • 关闭音频流且释放系统资源

值得注意的是,closeEvent方法通常是在窗口关闭事件发生时自动被调用的,它是与窗口或界面关闭相关联的事件处理函数。

event.accept方法是用来标记事件已被处理,表示允许窗口关闭(即立即销毁窗口并退出程序)。如果你不调用event.accept窗口,窗口的关闭可能会被组织或无效。

音频波形与音频播放的同步

如果要实现音频的可视化,音频播放与波形绘制的同步时必不可少的操作,在这里,我们通过PyAudio的流式操作,将音频数据分成数据块来读取并播放,这样的操作思想为实现波形绘制与音频播放的同步奠定了基础。

在每一个音频数据块被读取时,我们将数据块交给PyAudio播放的同时,也制作数据进行波形绘制。这样确保了绘制与播放操作的是一个数据块,不会出现速度不一的情况。

主要的实现在play_audio方法中:

python">    def play_audio(self):while self.index < len(self.audio_data):chunk = self.audio_data[self.index:self.index + self.chunk_size]if len(chunk) < self.chunk_size and len(chunk) > 0:chunk = np.pad(chunk, (0, self.chunk_size - len(chunk)), 'constant')self.index += self.chunk_sizeself.stream.write(chunk.tobytes())# 更新波形数据self.y_data = chunk / 32768.0  # 16-bit PCM音频数据范围 [-1, 1]self.line.set_ydata(self.y_data)self.canvas.draw()

如何避免音频卡顿

在笔者第一次使用PyAudio的功能时,为了实现播放功能,而没有深入了解PyAudio的运行原理,导致在第一次播放时正常,再重复播放几次后会出现卡顿情况,播放次数越多卡顿越厉害,到最后,音频的波形只有几帧了,随后笔者开始对如何实现PyAudio展开了优化,上面是优化后的代码,下面给出优化的心路历程。

UI线程与音频线程的阻塞问题

音频播放是一个需要实时更新的过程,可能会造成UI线程和音频线程的冲突。特别是音频播放涉及IO操作,如果UI更新阻塞了音频播放,可能会导致卡顿。

修改后,通过开子线程的方式,将UI线程与音频线程区分开,使音频播放独立于UI线程执行。

内存管理问题

在之前的代码中,每一次点击播放,都会重新调用一次load_pcm_audio,会重新将整个PCM文件保存在self.audio_data中,如果文件较大,这会消耗大量的内存,尤其是当多次播放时,音频数据会反复加载,可能会导致内存积累,引起卡顿。

修改后,仅有当检测读取文件与上一次读取文件不一致时,才会调用load_pcm_audio,否则将不重新加载PCM文件,直接使用已有数据,使用self.index来控制播放进度。

PyAudio流的管理问题

在之前的代码中,初始化self.p = pyaudio.PyAudio()是放在play方法中,这会导致,每次按键调用play方法时,stream对象就会被重复创建且播放时没有正确地停止和清理,导致音频流的资源没有得到释放,影响性能。

修改后,stream对象在初始化时便被创建,往后在play方法时不重复创建,在结束时被释放。


http://www.ppmy.cn/embedded/149432.html

相关文章

K8s DaemonSet的介绍

1. 什么是 DaemonSet&#xff1f; DaemonSet 是 Kubernetes 中的一种控制器&#xff0c;用于确保每个&#xff08;或某些指定的&#xff09;节点上运行一个 Pod 副本。它是为部署守护进程设计的&#xff0c;例如需要在每个节点上运行的任务或工具。 特点&#xff1a; Pod 会随…

数据结构与算法Python版 平衡二叉查找树AVL

文章目录 一、平衡二叉查找树二、AVL树测试三、AVL树-算法分析 一、平衡二叉查找树 平衡二叉查找树-AVL树的定义 AVL树&#xff1a;在key插入时一直保持平衡的二叉查找树。可以利用AVL树实现抽象数据类型映射Map。与二叉查找树相比&#xff0c;AVL树基本上与二叉查找树的实现…

《异构计算:多元算力聚变,点燃高性能计算新引擎 – CPU、GPU与FPGA算力融合》

数字化浪潮澎湃之际&#xff0c;算力需求呈指数级攀升态势&#xff0c;数据中心亦随之踏上向计算中心深度蜕变之旅&#xff0c;算力作为新兴生产力要素&#xff0c;正重塑产业格局。多元数据形态与丰富场景交相辉映&#xff0c;强力驱动异构计算步入高速发展快车道。 置身 AI 与…

“智能控制的新纪元:2025年机器学习与控制工程国际会议引领变革

ICMLCE 2025 | 机器学习与控制工程国际会议 ✨宝子们&#xff0c;今天要为大家介绍的是一个在机器学习和控制工程领域备受瞩目的国际学术盛会——2025年机器学习与控制工程国际会议&#xff08;ICMLCE 2025&#xff09;。本次大会将在美丽的大理举行&#xff0c;旨在汇聚全球顶…

力扣矩阵-算法模版总结

lc-73.矩阵置零-(时隔14天)-12.27 思路&#xff1a;(23min22s) 1.直接遍历遇0将行列设0肯定不行&#xff0c;会影响后续判断&#xff0c;题目又要求原地算法&#xff0c;那么进一步考虑是否可以将元素为0&#xff0c;其行列需要设为0的位置给存储下来&#xff0c;最后再遍历根据…

Git快速查阅

根据平时使用git的流程来编写。 git init 建立当前目录git仓库 git add . 将当前目录所有文件加入git暂存区&#xff0c;让git进行管理 git status 查看当前暂存区内容 git commit -m commit 描述 将当前暂存区中的内容进行提交&#xff0c;保存为一个commit。 git log 查…

嵌入式硬件面试题

1、请问什么是通孔、盲孔和埋孔&#xff1f;孔径多大可以做机械孔&#xff0c;孔径多小必须做激光孔&#xff1f;请问激光微型孔可以直接打在元件焊盘上吗&#xff0c;为什么&#xff1f; 通孔是贯穿整个PCB的过孔&#xff0c;盲孔是从PCB表层连接到内层的过孔&#xff0c;埋孔…

Matlab个性化绘图第7期—带标记面的三维多组折线图

上一期文章分享了Matlab带标记面的三维折线图&#xff1a; 进一步&#xff0c;再来分享一下带标记面的三维多组折线图。 由于Matlab未收录带标记面的三维多组折线图的绘图函数&#xff0c;因此需要大家自行解决。 本文使用自制的addPlane小工具进行带标记面的三维多组折线图的…