使用Qt+SDL2实现WAV音频播放功能

 更新时间:2026年02月04日 09:54:37   作者:xmRao  
本文介绍了如何使用Qt和SDL2实现WAV音频文件的播放功能,通过将音频播放逻辑放在Qt子线程中,避免了UI阻塞,并使用SDL2的拉取模式实现音频播放,需要的朋友可以参考下

前言

本文将手把手教大家用 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 程序的标准入口,没复杂逻辑,就是创建应用、显示主窗口。

#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,文字默认设为 “开始播放”,无需其他控件。

五、运行效果

  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 资源的正确释放。

以上就是使用Qt+SDL2实现WAV音频播放功能的详细内容,更多关于Qt SDL2实现WAV音频播放的资料请关注脚本之家其它相关文章!

相关文章

  • 深入理解C语言 static、extern与指针函数

    深入理解C语言 static、extern与指针函数

    这篇文章主要介绍了C语言 static、extern与指针函数,有需要的朋友可以参考一下
    2013-12-12
  • C++静态成员变量和静态成员函数的使用方法总结

    C++静态成员变量和静态成员函数的使用方法总结

    下面小编就为大家带来一篇C++静态成员变量和静态成员函数的使用方法总结。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-01-01
  • 深入理解二叉树的非递归遍历

    深入理解二叉树的非递归遍历

    本篇文章是对二叉树的非递归遍历进行了详细的分析介绍,需要的朋友参考下
    2013-05-05
  • 解析C++中临时对象的产生情况

    解析C++中临时对象的产生情况

    临时对象的产生和销毁都是有成本的,都会影响程序的执行性能和效率,所以如果能了解临时对象产生的原因,就可以提升程序的性能和效率,下面小编就来和大家聊聊临时对象产生的几种情况吧
    2023-06-06
  • c++ 让程序开机自动启动的方法

    c++ 让程序开机自动启动的方法

    这篇文章主要介绍了c++ 让程序开机自动启动的方法,需要的朋友可以参考下
    2017-09-09
  • 10行C++代码实现高性能HTTP服务

    10行C++代码实现高性能HTTP服务

    这篇文章主要介绍了10行C++代码如何实现高性能HTTP服务,帮助大家更好的理解和学习使用c++,感兴趣的朋友可以了解下
    2021-04-04
  • 利用Matlab绘制好看的旋转九边形

    利用Matlab绘制好看的旋转九边形

    这篇文章主要为大家介绍了如何利用Matlab绘制超好看的旋转九边形。文中的示例代码讲解详细,对我们学习Matlab有一定帮助,需要的可以参考一下
    2022-03-03
  • C语言数据结构哈希表详解

    C语言数据结构哈希表详解

    哈希表是一种根据关键码去寻找值的数据映射结构,该结构通过把关键码映射的位置去寻找存放值的地方,说起来可能感觉有点复杂,我想我举个例子你就会明白了,最典型的的例子就是字典
    2022-02-02
  • C语言入门篇--定义宏#define的概述

    C语言入门篇--定义宏#define的概述

    本篇文章是C语言系列基础篇,适合c语言刚入门的朋友,本文对关于c语言的定义宏#define作了简要的概述,希望可以帮助大家快速入门c语言的世界,更好的理解c语言
    2021-08-08
  • C++实现将图片转换为马赛克效果的示例代码

    C++实现将图片转换为马赛克效果的示例代码

    这篇文章主要为大家详细介绍了C++如何实现将图片转换为马赛克效果,文中的示例代码讲解详细,具有一定的借鉴价值,感兴趣的小伙伴可以了解一下
    2023-01-01

最新评论