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();
}
四、核心逻辑解析
- 多线程设计 :播放逻辑放在
playThread线程中,避免阻塞 Qt 主线程(UI 无卡顿); - SDL Pull 模式 :音频设备通过
pull_audio_data回调函数主动拉取 PCM 数据,而非程序推送; - 参数匹配:SDL 的音频参数(采样率、位深、声道数)必须与 PCM 文件一致,否则播放会变调 / 噪音;
- 资源安全 :线程析构时通过
requestInterruption()中断播放,确保 SDL 资源(设备、文件)正常释放。
五、运行注意事项
- 修改
FILENAME为实际的 PCM 文件路径; - 确保 PCM 文件参数与代码中
SAMPLE_RATE/CHANNELS等一致(可通过 FFmpeg 查看:ffmpeg -i 音频文件 -f pcm -y 输出.pcm); - Windows 下需将
SDL2.dll放到编译后的debug/release目录; - Linux 下编译需安装 SDL2 依赖:
sudo apt-get install libsdl2-dev。
六、常见问题
- 播放无声音:检查 PCM 文件路径、音频参数是否匹配、SDL2.dll 是否缺失;
- 噪音 / 变调:采样率 / 声道数 / 采样格式与 PCM 文件不匹配;
- 程序崩溃:未释放 SDL 资源(如线程强制退出时未关闭音频设备)。