Qt 结合 SDL2 实现 PCM 音频文件播放

PCM 是原始音频数据,无需解码可直接播放,SDL2 是跨平台的音视频开发库,本文基于 Qt 框架,通过多线程(避免阻塞 UI)+ SDL2 的 Pull 模式实现 PCM 音频播放,完整代码可直接运行。

一、开发环境

  • Qt 版本:5.14
  • SDL2 版本:2.018

二、SDL2 配置(关键)

在 Qt 工程文件(.pro)中添加 SDL2 的头文件和库路径:

复制代码
QT       += core gui

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

CONFIG += c++11

# 开启Qt废弃API警告
DEFINES += QT_DEPRECATED_WARNINGS

# 源文件/头文件/UI文件配置
SOURCES += \
    main.cpp \
    mainwindow.cpp \
    playthread.cpp

HEADERS += \
    mainwindow.h \
    playthread.h

FORMS += \
    mainwindow.ui

# 部署规则(默认)
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

# Windows下SDL2配置(核心)
win32
{
    # SDL2安装根路径(根据自己的实际路径修改)
    SDL_HOME = D:/Projects/SDL2-2.0.18/x86_64-w64-mingw32
    # 引入SDL2头文件目录
    INCLUDEPATH += $${SDL_HOME}/include
    # 引入SDL2库文件目录 + 链接SDL2库
    LIBS += -L $${SDL_HOME}/lib \
            -lSDL2
}

三、完整代码实现

1. 工程文件结构
复制代码
├── mainwindow.h/.cpp  # 主窗口(仅一个播放按钮)
├── playthread.h/.cpp  # 播放线程(核心SDL播放逻辑)
├── main.cpp           # 程序入口
└── xxx.pro            # 工程配置(SDL2依赖)
2. 主窗口代码(UI + 按钮触发)
mainwindow.h
cpp 复制代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void on_playBtn_clicked(); // 播放按钮点击槽函数

private:
    Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
mainwindow.cpp
cpp 复制代码
#include "mainwindow.h"
#include "ui_mainwindow.h"

#include "qdebug.h"
#include "SDL2/SDL.h"
#include "playthread.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
}

MainWindow::~MainWindow()
{
    delete ui;
}

// 播放按钮点击事件:创建并启动播放线程
void MainWindow::on_playBtn_clicked()
{
    playThread *thread = new playThread();
    thread->start();
}
3. 播放线程代码(SDL 核心逻辑)
cpp 复制代码
#ifndef PLAYTHREAD_H
#define PLAYTHREAD_H

#include <QThread>

class playThread : public QThread
{
    Q_OBJECT
public:
    explicit playThread(QObject *parent = nullptr);
    ~playThread();

private:
    void run(); // 线程入口函数

signals:
};

#endif // PLAYTHREAD_H
playthread.cpp
cpp 复制代码
#include "playthread.h"

#include "SDL2/SDL.h"
#include "qdebug.h"
#include "qfile.h"

// 配置PCM文件路径和音频参数(需与PCM文件实际参数匹配)
#define FILENAME "D:/01_20_23_15_07.pcm"
#define SAMPLE_RATE 44100   // 采样率
#define SAMPLE_SIZE 16      // 采样位深
#define CHANNELS 2          // 声道数(2=立体声)
#define BUFFER_SIZE 4096    // 文件读取缓冲区

// 全局临时变量(音频回调函数用)
int bufferLen;
char *bufferData;

// SDL音频回调函数(音频设备主动拉取数据)
void pull_audio_data(void *userdata, Uint8 *stream, int len)
{
    // 清空音频缓冲区
    SDL_memset(stream, 0, len);

    // 无文件数据则返回
    if(bufferLen <= 0)  return;

    // 取「需要填充的长度」和「剩余文件数据长度」的最小值
    len = (len > bufferLen) ? bufferLen : len;

    // 填充PCM数据到音频设备缓冲区
    SDL_MixAudio(stream, (Uint8*)bufferData, len, SDL_MIX_MAXVOLUME);
    bufferData += len; // 移动缓冲区指针
    bufferLen -= len;  // 减少剩余数据长度
}

void playThread::run()
{
    // 1. 初始化SDL音频子系统
    if(SDL_Init(SDL_INIT_AUDIO))
    {
        qDebug () << "SDL_Init error:" << SDL_GetError();
        return;
    }

    // 2. 配置SDL音频参数
    SDL_AudioSpec spec;
    spec.freq = SAMPLE_RATE;          // 采样率
    spec.format = AUDIO_S16LSB;       // 采样格式(S16LE,对应16位深)
    spec.channels = CHANNELS;         // 声道数
    spec.samples = 1024;              // 音频缓冲区样本数(必须是2的幂)
    spec.callback = pull_audio_data;  // 回调函数(拉取数据)

    // 3. 打开SDL音频设备
    if(SDL_OpenAudio(&spec, nullptr))
    {
        qDebug() << "SDL_OpenAudio error:" << SDL_GetError();
        SDL_Quit(); // 清理SDL子系统
        return;
    }

    // 4. 打开PCM文件
    QFile file(FILENAME);
    if(!file.open(QFile::ReadOnly))
    {
        qDebug() << "文件打开失败:" << FILENAME;
        SDL_CloseAudio(); // 关闭音频设备
        SDL_Quit();       // 清理SDL子系统
        return;
    }

    // 5. 开始播放(0=取消暂停)
    SDL_PauseAudio(0);

    // 6. 循环读取PCM文件并供回调函数使用
    char data[BUFFER_SIZE];
    while(!isInterruptionRequested()) // 检测线程中断请求
    {
        bufferLen = file.read(data, BUFFER_SIZE); // 读取文件数据
        if(bufferLen <= 0)  break; // 读取完毕/失败则退出

        bufferData = data; // 赋值给全局缓冲区
        while(bufferLen > 0) // 等待回调函数消耗完当前缓冲区数据
        {
            SDL_Delay(1);
        }
    }

    // 7. 资源释放(逆序)
    file.close();          // 关闭文件
    SDL_CloseAudio();      // 关闭音频设备
    SDL_Quit();            // 清理SDL子系统
}

// 线程析构函数:安全停止线程+释放资源
playThread::playThread(QObject *parent): QThread{parent}
{
    connect(this, &playThread::finished, this, &playThread::deleteLater);
}

playThread::~playThread()
{
    disconnect();
    requestInterruption(); // 请求线程中断
    quit();
    wait();                // 等待线程退出
    qDebug() << this << "析构了";
}
4. 程序入口(main.cpp)
cpp 复制代码
#include "mainwindow.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}

四、核心逻辑解析

  1. 多线程设计 :播放逻辑放在playThread线程中,避免阻塞 Qt 主线程(UI 无卡顿);
  2. SDL Pull 模式 :音频设备通过pull_audio_data回调函数主动拉取 PCM 数据,而非程序推送;
  3. 参数匹配:SDL 的音频参数(采样率、位深、声道数)必须与 PCM 文件一致,否则播放会变调 / 噪音;
  4. 资源安全 :线程析构时通过requestInterruption()中断播放,确保 SDL 资源(设备、文件)正常释放。

五、运行注意事项

  1. 修改FILENAME为实际的 PCM 文件路径;
  2. 确保 PCM 文件参数与代码中SAMPLE_RATE/CHANNELS等一致(可通过 FFmpeg 查看:ffmpeg -i 音频文件 -f pcm -y 输出.pcm);
  3. Windows 下需将SDL2.dll放到编译后的debug/release目录;
  4. Linux 下编译需安装 SDL2 依赖:sudo apt-get install libsdl2-dev

六、常见问题

  1. 播放无声音:检查 PCM 文件路径、音频参数是否匹配、SDL2.dll 是否缺失;
  2. 噪音 / 变调:采样率 / 声道数 / 采样格式与 PCM 文件不匹配;
  3. 程序崩溃:未释放 SDL 资源(如线程强制退出时未关闭音频设备)。
相关推荐
墨笔.丹青1 天前
基于QtQuick开发界面设计出简易的HarmonyUI界面----下
开发语言·前端·javascript
代码无bug抓狂人1 天前
C语言之表达式括号匹配
c语言·开发语言·算法
Nebula_g1 天前
线程进阶: 无人机自动防空平台开发教程(更新)
java·开发语言·数据结构·学习·算法·无人机
沐知全栈开发1 天前
滑块(Slider)在网页设计中的应用与优化
开发语言
又见野草1 天前
C++类和对象(下)
开发语言·c++
rit84324991 天前
基于MATLAB的环境障碍模型构建与蚁群算法路径规划实现
开发语言·算法·matlab
lang201509281 天前
Java JSR 250核心注解全解析
java·开发语言
Wpa.wk1 天前
接口自动化测试 - 请求构造和响应断言 -Rest-assure
开发语言·python·测试工具·接口自动化
czhc11400756631 天前
协议 25
java·开发语言·算法
ae_zr1 天前
QT动态编译应用后,如何快速获取依赖
开发语言·qt