前言
本文将手把手教大家用 qt + sdl2 实现 wav 音频文件的播放功能。核心思路是:把音频播放逻辑放在 qt 子线程中(避免阻塞 ui),通过 sdl2 的 “拉取(pull)模式” 实现音频播放,点击主窗口的按钮就能切换 “播放 / 停止” 状态。
一、环境准备
1. 基础环境
- 安装 qt(推荐 qt 5/6,mingw 编译器,本文以 qt 5.15 为例);
- 下载 sdl2 开发库:sdl2 官网,选择对应系统的开发包(比如 windows 下的 mingw 版本)。
2. sdl2 配置
下载后解压 sdl2,在 qt 项目的.pro文件中添加 sdl2 的头文件和库路径(替换成你的 sdl2 路径):
# sdl2头文件路径 includepath += d:/sdl2-2.28.0/x86_64-w64-mingw32/include # sdl2库文件路径 libs += -ld:/sdl2-2.28.0/x86_64-w64-mingw32/lib -lsdl2
二、整体流程梳理
先给大家讲清楚核心逻辑,避免看代码时懵圈:
- 主窗口(mainwindow)放一个 “播放 / 停止” 按钮,点击按钮触发槽函数;
- 点击按钮时,创建 / 停止播放线程(playthread),播放逻辑全在子线程里(防止 ui 卡);
- 子线程中用 sdl2 初始化音频子系统、加载 wav 文件、打开音频设备;
- sdl 采用 “pull 模式”:音频设备会主动调用回调函数,我们在回调中给设备喂音频数据;
- 停止播放时,中断线程、释放 sdl 资源,保证程序不崩溃。
三、逐文件解析代码
1. 程序入口:main.cpp
这是 qt 程序的标准入口,没复杂逻辑,就是创建应用、显示主窗口。
#include "mainwindow.h"
#include <qapplication>
int main(int argc, char *argv[])
{
// 创建qt应用对象,管理程序生命周期
qapplication a(argc, argv);
// 创建主窗口对象
mainwindow w;
// 显示主窗口
w.show();
// 进入qt的事件循环(等待用户操作,比如点击按钮)
return a.exec();
}
2. 播放线程头文件:playthread.h
定义一个继承自qthread的播放线程类,重写run方法(线程的核心执行逻辑)。
#ifndef playthread_h
#define playthread_h
// qt线程头文件
#include <qthread>
// 播放线程类,继承qthread
class playthread : public qthread
{
// qt信号槽必须的宏
q_object
private:
// 重写qthread的run方法,线程启动后会执行这里的逻辑
void run();
public:
// 构造函数,parent是父对象(qt的父子机制自动管理内存)
explicit playthread(qobject *parent = nullptr);
// 析构函数,释放资源
~playthread();
signals:
// 暂时没定义信号,后续可扩展(比如播放完成信号)
};
#endif // playthread_h
3. 主窗口头文件:mainwindow.h
主窗口类,包含播放按钮的槽函数、播放线程的指针。
#ifndef mainwindow_h
#define mainwindow_h
#include <qmainwindow>
// 包含播放线程的头文件
#include "playthread.h"
// qt的ui命名空间(.ui文件自动生成的代码)
qt_begin_namespace
namespace ui { class mainwindow; }
qt_end_namespace
class mainwindow : public qmainwindow
{
q_object
public:
mainwindow(qwidget *parent = nullptr);
~mainwindow();
private slots:
// 播放按钮点击的槽函数(和ui里的按钮关联)
void on_playbtn_clicked();
private:
// 指向ui界面的指针(自动生成)
ui::mainwindow *ui;
// 播放线程的指针,初始化为nullptr
playthread *_playthread = nullptr;
};
#endif // mainwindow_h
4. 主窗口实现:mainwindow.cpp
核心是按钮点击逻辑,处理 “播放 / 停止” 的切换,以及线程的创建和销毁。
#include "mainwindow.h"
#include "ui_mainwindow.h"
// sdl2头文件(音频相关)
#include <sdl2/sdl.h>
// qt调试输出
#include <qdebug>
// 主窗口构造函数
mainwindow::mainwindow(qwidget *parent)
: qmainwindow(parent)
, ui(new ui::mainwindow)
{
// 初始化ui界面(加载按钮等控件)
ui->setupui(this);
}
// 主窗口析构函数
mainwindow::~mainwindow()
{
// 释放ui资源
delete ui;
}
// 播放按钮点击触发的槽函数
void mainwindow::on_playbtn_clicked()
{
// 如果线程已存在(正在播放),则停止播放
if(_playthread)
{
// 向线程发送“中断请求”
_playthread->requestinterruption();
// 清空线程指针
_playthread = nullptr;
// 按钮文字改回“开始播放”
ui->playbtn->settext("开始播放");
}
else // 线程不存在(未播放),则开始播放
{
// 创建播放线程,父对象设为主窗口(自动管理内存)
_playthread = new playthread(this);
// 启动线程(会执行playthread的run方法)
_playthread->start();
// 监听线程结束信号:线程播放完成后,重置状态
connect(_playthread,&playthread::finished,
[this]()
{
_playthread = nullptr;
ui->playbtn->settext("开始播放");
});
// 按钮文字改成“停止播放”
ui->playbtn->settext("停止播放");
}
}
5. 播放线程实现:playthread.cp
这是音频播放的核心逻辑,包含 sdl2 的初始化、wav 加载、音频回调、资源释放等。
#include "playthread.h"
#include <sdl2/sdl.h>
#include <qdebug>
#include <qfile>
// !!!替换成你自己的wav文件路径!!!
#define filename "d:/in.wav"
// 音频缓冲区结构体:给sdl回调函数传递pcm数据和长度
typedef struct
{
int len = 0; // 剩余未播放的pcm数据长度
int pulllen = 0; // 本次要填充的pcm数据长度
uint8 *data = nullptr; // 指向pcm数据的指针
}audiobuffer;
// 播放线程构造函数
playthread::playthread(qobject *parent):qthread{parent}
{
// 线程结束后自动销毁(避免内存泄漏)
connect(this,&playthread::finished,this,&playthread::deletelater);
}
// 播放线程析构函数(关键:安全停止线程+释放资源)
playthread::~playthread()
{
// 断开所有信号槽
disconnect();
// 发送中断请求
requestinterruption();
// 退出线程事件循环
quit();
// 等待线程结束(防止线程还在运行就销毁)
wait();
qdebug() << this << "析构了";
}
// sdl音频回调函数(音频设备会主动调用这个函数“拉取”数据)
// userdata:自定义数据(这里传audiobuffer)
// stream:音频设备的缓冲区,需要往里面填pcm数据
// len:音频设备希望填充的字节数
void pull_audio_data(void *userdata, uint8 *stream, int len)
{
qdebug() << "音频设备拉取数据,期望长度:" << len;
// 第一步:清空stream(静音处理,避免填充数据前有杂音)
sdl_memset(stream,0,len);
// 取出我们传递的audiobuffer
audiobuffer *buffer = (audiobuffer*)userdata;
// 如果没有可用的pcm数据,直接返回(静音)
if(buffer->len <= 0) return;
// 第二步:确定本次要填充的长度(取“设备期望长度”和“剩余数据长度”的最小值)
buffer->pulllen = (len > buffer->len) ? buffer->len : len;
// 第三步:填充pcm数据到音频设备缓冲区
// sdl_mixaudio:混合音频(这里直接用最大音量)
sdl_mixaudio(stream, buffer->data, buffer->pulllen, sdl_mix_maxvolume);
// 第四步:更新缓冲区状态(已播放的数据要跳过)
buffer->data += buffer->pulllen; // 指针后移,指向剩余数据
buffer->len -= buffer->pulllen; // 剩余长度减少
}
// 线程的核心执行函数(播放逻辑全在这里)
void playthread::run()
{
// 1. 初始化sdl的音频子系统
if(sdl_init(sdl_init_audio)) // 非0表示失败
{
qdebug() << "sdl初始化失败:" << sdl_geterror();
return;
}
// 2. 加载wav文件
sdl_audiospec spec; // 存储wav文件的音频规格(采样率、声道数等)
uint8 *data = nullptr; // 指向wav文件的pcm原始数据
uint32 len = 0; // pcm数据的总长度
if(!sdl_loadwav(filename,&spec,&data,&len)) // 加载失败返回0
{
qdebug() << "加载wav文件失败:" << sdl_geterror();
sdl_quit(); // 失败则释放sdl资源
return;
}
// 3. 配置音频播放参数
spec.samples = 1024; // 音频缓冲区的样本数(常用1024)
spec.callback = pull_audio_data; // 设置音频回调函数(设备拉数据时调用)
audiobuffer buffer; // 创建音频缓冲区
buffer.data = data; // 绑定pcm数据
buffer.len = len; // 绑定pcm数据长度
spec.userdata = &buffer; // 给回调函数传自定义数据(audiobuffer)
// 4. 打开音频设备
if(sdl_openaudio(&spec,nullptr)) // 失败返回非0
{
qdebug() << "打开音频设备失败:" << sdl_geterror();
sdl_freewav(data); // 释放wav数据
sdl_quit(); // 释放sdl资源
return;
}
// 5. 开始播放(0=取消暂停,1=暂停)
sdl_pauseaudio(0);
// 6. 计算音频参数(用于后续等待播放完成)
int samplesize = sdl_audio_bitsize(spec.format); // 每个样本的位数(比如16位)
// 每个样本的字节数 = (位数 * 声道数) / 8(8位=1字节)
int bytespersample = (samplesize * spec.channels) >> 3;
// 7. 主线程循环:等待播放完成/收到中断请求
while(!isinterruptionrequested()) // 只要没收到“停止”请求,就循环
{
// 如果还有未播放的pcm数据,继续等待(回调函数会自动处理)
if(buffer.len > 0) continue;
// 8. 播放完成:等待最后一批数据播放完毕
if(buffer.len <= 0)
{
// 计算最后一批数据的播放时长(毫秒)
int samples = buffer.pulllen / bytespersample; // 样本数
int ms = samples * 1000 / spec.freq; // 时长=样本数/采样率*1000
sdl_delay(ms); // 等待播放完成
break; // 退出循环
}
}
// 9. 释放资源(重中之重!)
sdl_freewav(data); // 释放wav文件的pcm数据
sdl_closeaudio(); // 关闭音频设备
sdl_quit(); // 释放sdl所有子系统
}
四、ui 设计
在 qt designer 中给主窗口拖一个qpushbutton,命名为playbtn,文字默认设为 “开始播放”,无需其他控件。
五、运行效果
- 替换代码中
filename为你的 wav 文件路径(必须是 wav 格式,sdl_loadwav 不支持 mp3); - 编译运行程序,点击 “开始播放” 按钮,就能听到音频播放,按钮文字变成 “停止播放”;
- 播放过程中点击 “停止播放”,音频会停止,按钮文字恢复;
- 音频播放完成后,按钮会自动恢复为 “开始播放”。
六、注意事项
- 线程安全:音频播放必须放在子线程!如果直接在 ui 线程执行 sdl 逻辑,播放时 ui 会卡死;
- 资源释放:析构函数中一定要
requestinterruption()+quit()+wait(),否则线程可能野跑; - 文件路径:wav 文件路径用绝对路径(比如
d:/in.wav),路径中不要有中文和空格; - sdl 版本:一定要选和 qt 编译器匹配的 sdl 库(比如 qt 用 mingw 64 位,sdl 也要下 mingw 64 位);
- 格式限制:sdl_loadwav 只支持 wav 格式,想播放 mp3 需要额外解码(比如 ffmpeg);
- 静音处理:回调函数中先
sdl_memset(stream,0,len),否则填充数据前会有杂音。
七、总结
本文通过 qt 子线程 + sdl2 的 pull 模式实现了 wav 音频播放,核心是理解:
- qt 线程:避免 ui 阻塞;
- sdl pull 模式:音频设备主动拉取数据,回调函数填充 pcm;
- 资源管理:线程和 sdl 资源的正确释放。
以上就是使用qt+sdl2实现wav音频播放功能的详细内容,更多关于qt sdl2实现wav音频播放的资料请关注代码网其它相关文章!
发表评论