FFmpeg音视频开发_播放 PCM 格式音频数据

一、使用 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

二、使用 FFmpeg + 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 

打开 mainwindow.ui 文件添加一个 Push Button ,并且更名为 playButton,然后右键选择转到槽:

FFmpeg音视频开发_播放 PCM 格式音频数据_第1张图片

在槽函数中添加打印 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 
#include 
#include 

#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() << "播放结束";
}

你可能感兴趣的:(FFmpeg音视频开发_播放 PCM 格式音频数据)