一、使用 ffplay 命令行程序播放
首先使用 ffmpeg 命令行程序抽出 pcm 数据:
$ ffmpeg -i 那又如何.mp3 -ar 44100 -ac 2 -f s16le -acodec pcm_s16le out.pcm
使用 ffplay 命令行程序播放 pcm 数据:
$ ffplay -ar 44100 -ac 2 -f s16le out.pcm
-ar 采样率
-ac 声道数
-f 采样格式
在 Mac 平台使用 ffmpeg -formats | grep PCM
查看更多采样格式:
DE alaw PCM A-law
DE f32be PCM 32-bit floating-point big-endian
DE f32le PCM 32-bit floating-point little-endian
DE f64be PCM 64-bit floating-point big-endian
DE f64le PCM 64-bit floating-point little-endian
DE mulaw PCM mu-law
DE s16be PCM signed 16-bit big-endian
DE s16le PCM signed 16-bit little-endian
DE s24be PCM signed 24-bit big-endian
DE s24le PCM signed 24-bit little-endian
DE s32be PCM signed 32-bit big-endian
DE s32le PCM signed 32-bit little-endian
DE s8 PCM signed 8-bit
DE u16be PCM unsigned 16-bit big-endian
DE u16le PCM unsigned 16-bit little-endian
DE u24be PCM unsigned 24-bit big-endian
DE u24le PCM unsigned 24-bit little-endian
DE u32be PCM unsigned 32-bit big-endian
DE u32le PCM unsigned 32-bit little-endian
DE u8 PCM unsigned 8-bit
DE vidc PCM Archimedes VIDC
二、使用 SDL 编程实现 PCM 播放
1、安装 sdl2(如果已安装忽略这一步,如果是使用Homebrew安装的 FFmpeg 也可以省略这一步,因为通过 brew 安装的 FFmpeg 依赖了 sdl2):
$ brew install sdl2
2、然后使用 Qt 新建一个名为 02_play_pcm_example 的工程
$ tree
.
|____playthread.cpp
|____mainwindow.h
|____mainwindow.ui
|____mainwindow.cpp
|____02_play_pcm_example.pro
|____main.cpp
|____playthread.h
3、在 04_sdl_play_pcm.pro 文件中配置 SDL 头文件和静态库的位置(如果没有安装 SDL 需要先安装,):
INCLUDEPATH += -I "/usr/local/Cellar/sdl2/2.0.14_1/include"
LIBS += -L /usr/local/Cellar/sdl2/2.0.14_1/lib -lSDL2
接着在 mainwindow.cpp 中引入 sdl2 头文件:
#include <SDL2/SDL.h>
打开 mainwindow.ui 文件添加一个 Push Button ,并且更名为 playButton,然后右键选择转到槽:
在槽函数中添加打印 SDL版本号的代码:
void MainWindow::on_playButton_clicked()
{
SDL_version version;
SDL_VERSION(&version);
qDebug() << version.major << version.minor << version.patch;
}
运行程序点击播放PCM按钮,会打印:
13:52:48: Starting /Users/mac/Desktop/QtWorkSpace/build-04_sdl_play_pcm-Desktop_Qt_6_0_2_clang_64bit-Debug/04_sdl_play_pcm.app/Contents/MacOS/04_sdl_play_pcm ...
2 0 14
说明我们引入 sdl2 成功了!
4、接下来初始化 SDL 子系统:
if (SDL_Init(SDL_INIT_AUDIO)) {
qDebug() << "初始化 SDL 失败:" << SDL_GetError();
return;
}
atexit(SDL_Quit);
初始化成功后,就可以使用 SDL 子系统完成相应的任务了,当完成所有工作需要退出程序时,必须使用 SDL_Quit
清除所有子系统。如果初始化失败,使用 SDL_GetError
函数获取错误原因。atexit
是 C 语言标准库函数,作用是向系统注册传进来的函数,以便程序结束时调用该函数,此处希望程序结束时清空 SDL 所有子系统。
flags 参数取值:
// 定时器
#define SDL_INIT_TIMER 0x00000001u
// 音频
#define SDL_INIT_AUDIO 0x00000010u
// 视频
#define SDL_INIT_VIDEO 0x00000020u /**< SDL_INIT_VIDEO implies SDL_INIT_EVENTS */
// 游戏控制杆
#define SDL_INIT_JOYSTICK 0x00000200u /**< SDL_INIT_JOYSTICK implies SDL_INIT_EVENTS */
// 触摸屏
#define SDL_INIT_HAPTIC 0x00001000u
// 游戏控制器
#define SDL_INIT_GAMECONTROLLER 0x00002000u /**< SDL_INIT_GAMECONTROLLER implies SDL_INIT_JOYSTICK */
// 事件
#define SDL_INIT_EVENTS 0x00004000u
// 传感器
#define SDL_INIT_SENSOR 0x00008000u
// 错误捕获
#define SDL_INIT_NOPARACHUTE 0x00100000u /**< compatibility; this flag is ignored. */
// 全部子系统
#define SDL_INIT_EVERYTHING ( \
SDL_INIT_TIMER | SDL_INIT_AUDIO | SDL_INIT_VIDEO | SDL_INIT_EVENTS | \
SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC | SDL_INIT_GAMECONTROLLER | SDL_INIT_SENSOR \
)
初始化成功返回 0,初始化失败函数返回值为 -1,函数只接受各个子系统的常量作为参数。初始化音频子系统,传入参数 SDL_INIT_AUDIO
;初始化视频子系统传入 SDL_INIT_VIDEO
;并且可初始化一个或者多个子系统,例如同时初始化音频和视频子系统,传入 SDL_INIT_AUDIO | SDL_INIT_VIDEO
。
5、打开音频设备:
SDL_AudioSpec spec;
spec.freq = sampleRate;
spec.format = format;
spec.channels = channels;
spec.samples = 1024;
spec.callback = call_back;
// 打开音频设备
if (SDL_OpenAudio(&spec, nullptr)) {
qDebug() << "打开音频设备失败:" << SDL_GetError();
SDL_Quit();
return;
}
SDL_OpenAudio 有两个参数:
extern DECLSPEC int SDLCALL SDL_OpenAudio(SDL_AudioSpec * desired, SDL_AudioSpec * obtained);
desired:期望参数,播放的音频对应的参数;
obtained:实际硬件设备参数,可传 nullptr;
SDL_AudioSpec 结构体:
typedef struct SDL_AudioSpec
{
// 采样率
int freq; /**< DSP frequency -- samples per second */
// 音频数据格式
SDL_AudioFormat format; /**< Audio data format */
// 声道数
Uint8 channels; /**< Number of channels: 1 mono, 2 stereo */
// 音频缓冲区静音值
Uint8 silence; /**< Audio buffer silence value (calculated) */
// 采样帧大小
Uint16 samples; /**< Audio buffer size in sample FRAMES (total samples divided by channel count) */
// 兼容性参数
Uint16 padding; /**< Necessary for some compile environments */
// 音频缓冲区大小
Uint32 size; /**< Audio buffer size in bytes (calculated) */
// 填充音频缓冲区回调函数
SDL_AudioCallback callback; /**< Callback that feeds the audio device (NULL to use SDL_QueueAudio()). */
// 用户自定义数据,
void *userdata; /**< Userdata passed to callback (ignored for NULL callbacks). */
} SDL_AudioSpec;
回调函数:
typedef void (SDLCALL * SDL_AudioCallback) (void *userdata, Uint8 * stream, int len);
当音频设备需要更多数据时会调用该函数;
6、打开 PCM 文件:
QFile file(filePath);
if (!file.open(QFile::ReadOnly)) {
qDebug() << "打开 PCM 文件失败:" << filePath;
SDL_CloseAudio();
SDL_Quit();
return;
}
7、开始播放:
SDL_PauseAudio(0);
参数 pause_on 设置为 0 开始播放音频数据;设置为 1 播放静音值;设置为 0 时 SDL 会调用我们提供的回调函数:
void call_back(void *userdata, Uint8 * stream, int len)
{
// SDL2之后需要先清空需要填充的音频缓冲区
SDL_memset(stream, 0, len);
if (bufferLen <= 0) return;
int readLen = len < bufferLen ? len : bufferLen;
// 填充音频缓冲区
SDL_MixAudio(stream, (Uint8 *)bufferData, readLen, SDL_MIX_MAXVOLUME);
bufferData += readLen;
bufferLen -= readLen;
}
回调函数:
typedef void (SDLCALL * SDL_AudioCallback) (void *userdata, Uint8 * stream, int len);
userdata:SDL_AudioSpec 结构体中用户自定义的数据,可不用;
stream:指向音频缓冲区的指针;
len:音频缓冲区大小;
混音函数:
extern DECLSPEC void SDLCALL SDL_MixAudio(Uint8 * dst, const Uint8 * src, Uint32 len, int volume);
dst:目标数据,这里传入音频缓冲区指针 stream;
src:音频数据,这里传入我们读出的 PCM 数据;
len:音频数据长度,这里传入音频缓冲区大小 len;
volume:音量,范围 0~128,这里我们传入 SDL_MIX_MAXVOLUME,注意此参数并不会修改硬件音量;
8、读取 PCM 文件:
char data[BUFFER_SIZE]; // 4096
while (!isInterruptionRequested()) {
bufferLen = file.read(data, BUFFER_SIZE);
if (bufferLen < 0) break;
bufferData = data;
// 延时等待音频播放完毕
while (bufferLen > 0) {
SDL_Delay(1);
}
}
9、播放结束,最后关闭音频设备,清除 SDL 子系统:
// 停止音频处理,关闭音频设备
SDL_CloseAudio();
// 清除所有初始化的 SDL 子系统
SDL_Quit();
三、代码
#include <QDebug>
#include <QFile>
#include <SDL2/SDL.h>
#define BUFFER_SIZE 4096
QString filePath;
int sampleRate; // 44100
int format; // AUDIO_S16LSB
int channels; // 2
int bufferLen;
char *bufferData;
// SDL 回调取每一帧播放数据
void call_back(void *userdata, Uint8 * stream, int len)
{
// SDL2之后需要先清空需要填充的音频缓冲区
SDL_memset(stream, 0, len);
if (bufferLen <= 0) return;
int readLen = len < bufferLen ? len : bufferLen;
// 填充音频缓冲区
SDL_MixAudio(stream, (Uint8 *)bufferData, readLen, SDL_MIX_MAXVOLUME);
bufferData += readLen;
bufferLen -= readLen;
}
PlayThread::PlayThread(QObject *parent) : QThread(parent)
{
connect(this, &PlayThread::finished, this, &PlayThread::deleteLater);
}
PlayThread::~PlayThread()
{
disconnect();
requestInterruption();
quit();
wait();
qDebug() << "PlayThread 析构函数";
}
void PlayThread::run()
{
qDebug() << "开始播放";
// 初始化 SDL 音频子系统
if (SDL_Init(SDL_INIT_AUDIO)) {
qDebug() << "初始化 SDL 失败:" << SDL_GetError();
return;
}
// atexit 是 C 语言标准库函数,作用是向系统注册传进来的函数,以便程序结束时调用该函数
// 程序结束时清空 SDL 所有子系统
atexit(SDL_Quit);
SDL_AudioSpec spec;
spec.freq = sampleRate;
spec.format = format; //AUDIO_S16LSB
spec.channels = channels;
spec.samples = 1024;
spec.callback = fill_audio;
// 打开音频设备
if (SDL_OpenAudio(&spec, nullptr)) {
qDebug() << "打开音频设备失败:" << SDL_GetError();
SDL_Quit();
return;
}
// 打开 PCM 文件
QFile file(filePath);
if (!file.open(QFile::ReadOnly)) {
qDebug() << "打开 PCM 文件失败:" << filePath;
SDL_CloseAudio();
SDL_Quit();
return;
}
// 开始播放
SDL_PauseAudio(0);
// 读取 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);
}
}
// 停止音频处理,关闭音频设备
SDL_CloseAudio();
// 关闭所有 SDL 子系统,清理 SDL 所占资源
SDL_Quit();
qDebug() << "播放结束";
}
网友评论