Qt+SDL2 实现 WAV 音频播放

前言

本文将手把手教大家用 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

二、整体流程梳理

先给大家讲清楚核心逻辑,避免看代码时懵圈:

  1. 主窗口(MainWindow)放一个 "播放 / 停止" 按钮,点击按钮触发槽函数;
  2. 点击按钮时,创建 / 停止播放线程(playThread),播放逻辑全在子线程里(防止 UI 卡);
  3. 子线程中用 SDL2 初始化音频子系统、加载 WAV 文件、打开音频设备;
  4. SDL 采用 "Pull 模式":音频设备会主动调用回调函数,我们在回调中给设备喂音频数据;
  5. 停止播放时,中断线程、释放 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,文字默认设为 "开始播放",无需其他控件。

五、运行效果

  1. 替换代码中FILENAME为你的 WAV 文件路径(必须是 WAV 格式,SDL_LoadWAV 不支持 MP3);
  2. 编译运行程序,点击 "开始播放" 按钮,就能听到音频播放,按钮文字变成 "停止播放";
  3. 播放过程中点击 "停止播放",音频会停止,按钮文字恢复;
  4. 音频播放完成后,按钮会自动恢复为 "开始播放"。

六、注意事项

  1. 线程安全:音频播放必须放在子线程!如果直接在 UI 线程执行 SDL 逻辑,播放时 UI 会卡死;
  2. 资源释放 :析构函数中一定要requestInterruption()+quit()+wait(),否则线程可能野跑;
  3. 文件路径 :WAV 文件路径用绝对路径(比如D:/in.wav),路径中不要有中文和空格;
  4. SDL 版本:一定要选和 Qt 编译器匹配的 SDL 库(比如 Qt 用 MinGW 64 位,SDL 也要下 MinGW 64 位);
  5. 格式限制:SDL_LoadWAV 只支持 WAV 格式,想播放 MP3 需要额外解码(比如 FFmpeg);
  6. 静音处理 :回调函数中先SDL_memset(stream,0,len),否则填充数据前会有杂音。

七、总结

本文通过 Qt 子线程 + SDL2 的 Pull 模式实现了 WAV 音频播放,核心是理解:

  • Qt 线程:避免 UI 阻塞;
  • SDL Pull 模式:音频设备主动拉取数据,回调函数填充 PCM;
  • 资源管理:线程和 SDL 资源的正确释放。
相关推荐
RTC实战笔记6 小时前
实时互动数字人怎么做,才不是一个只会说话的视频?
音视频·数字人·rtc·数字人接入
用户805533698031 天前
不止三件套:QObject 属性系统全关键字与运行时反射!
c++·qt
xcyxiner1 天前
DicomViewer (vcpkg Windows和ubuntu编译)7
qt
Quz6 天前
QML Hello World 入门示例
qt
xcyxiner9 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner10 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner10 天前
DicomViewer (添加模型类)3
qt
xcyxiner11 天前
DicomViewer (目录调整) 2
qt
xcyxiner11 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
RTC实战笔记12 天前
Android 实时音视频接入教程:媒体补充增强信息(SEI)
音视频·媒体·rtc