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 资源(如线程强制退出时未关闭音频设备)。
相关推荐
一个处女座的程序猿O(∩_∩)O2 小时前
Next.js 文件系统路由深度解析:从原理到实践
开发语言·javascript·ecmascript
炬火初现2 小时前
C++17特性(3)
开发语言·c++
煤炭里de黑猫2 小时前
Python 爬虫进阶:利用 Frida 逆向移动端 App API 以实现高效数据采集
开发语言·爬虫·python
草莓熊Lotso2 小时前
Linux 进程创建与终止全解析:fork 原理 + 退出机制实战
linux·运维·服务器·开发语言·汇编·c++·人工智能
枫叶丹42 小时前
【Qt开发】Qt系统(九)-> Qt TCP Socket
c语言·开发语言·网络·c++·qt·tcp/ip
007php0074 小时前
PHP与Java项目在服务器上的对接准备与过程
java·服务器·开发语言·分布式·面试·职场和发展·php
Evand J5 小时前
【MATLAB程序,一维非线性EKF与RTS】MATLAB,用于一维的位移与速度滤波和RTS平滑/高精度定位,带滤波前后的误差对比
开发语言·matlab·卡尔曼滤波·rts平滑·正向滤波
火云洞红孩儿10 小时前
告别界面孤岛:PyMe如何用一站式流程重塑Python GUI开发?
开发语言·python
叫我辉哥e110 小时前
新手进阶Python:办公看板集成ERP跨系统同步+自动备份+AI异常复盘
开发语言·人工智能·python