前言
本文将手把手教大家用 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 程序的标准入口,没复杂逻辑,就是创建应用、显示主窗口。
cpp
#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方法(线程的核心执行逻辑)。
cpp
#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
主窗口类,包含播放按钮的槽函数、播放线程的指针。
cpp
#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
核心是按钮点击逻辑,处理 "播放 / 停止" 的切换,以及线程的创建和销毁。
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 加载、音频回调、资源释放等。
cpp
#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 资源的正确释放。