ffmpeg简易播放器(4)--使用SDL播放音频

SDL(英语:Simple DirectMedia Layer)是一套开放源代码的跨平台多媒体开发函数库,使用C语言写成。SDL提供了数种控制图像、声音、输出入的函数,让开发者只要用相同或是相似的代码就可以开发出跨多个平台(Linux、Windows、Mac OS X等)的应用软件。目前SDL多用于开发游戏、模拟器、媒体播放器等多媒体应用领域(摘自维基百科)。

可以看到SDL可以做的功能非常多,既可以去播放音频也可以去设计GUI界面,但是我们在这里只使用SDL去播放音频。

SDL播放音频的方式

SDL中播放音频有两种模式,第一种是推送模式(push),另一种是拉取模式(pull)。前者是我们主动将音频数据填充到设备播放缓冲区,另一种是SDL主动拉取数据到设备播放缓冲区。这里我们使用的是拉取模式进行播放,这种模式是比较常用的。

本次我们的操作流程为

  • 初始化ffmpeg以及SDL2相关组件
  • 编写SDL2的回调函数
  • 编写解码函数

而SDL2使用拉取模式播放音频的方式是,当设备播放缓冲区的音频数据不足时,SDL2会调用我们提供的回调函数,我们在回调函数中填充音频数据到设备播放缓冲区。这样就实现了音频的播放。

SDL安装

这里像上一篇的ffmpeg一样,我还是使用编译安装加自己写一个FindSDL.cmake去引用SDL库。源码下载,至于编译安装的流程请在互联网中搜索,这里我提供一篇作为参考

这里贴上我的FindSDL.cmake文件

cmake 复制代码
set(SDL2ROOT /path/to/your/sdl2) # 填上安装后的sdl2的文件夹路径

set(SDL2_INCLUDE_DIRS ${SDL2ROOT}/include)

set(SDL2_LIBRARY_DIRS ${SDL2ROOT}/lib)

find_library(SDL2_LIBS SDL2 ${SDL2_LIBRARY_DIRS})

以及在CMakeLists.txt中添加

cmake 复制代码
find_package(SDL2 REQUIRED)
include_directories(${SDL2_INCLUDE_DIRS})
......
target_link_libraries(${PROJECT_NAME} ${SDL2_LIBS})

使用ffmpeg解码音频

播放部分使用的是SDL2,但是将音频文件解码获取数据的流程还是使用ffmpeg。这里我们使用ffmpeg解码音频,然后将解码后的音频数据传给SDL2进行播放。解码的流程大致与上一期解码视频一致,只是当前我们是对音频流进行处理而非视频流。

准备工作如下,包括引入头文件,初始化SDL,打开音频文件,以及配置好解码器。

c++ 复制代码
#include <iostream>
#include <SDL2/SDL.h>
#include <queue>
extern "C"
{
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/opt.h>
#include <libswscale/swscale.h>
#include <libavutil/avutil.h>
#include <libswresample/swresample.h>
}
#define SDL_AUDIO_BUFFER_SIZE 2048
queue<AVPacket> audioPackets; //音频包队列

int main()
{
    const string filename = "/home/ruby/Desktop/study/qtStudy/myPlayer/mad.mp4";
    AVFormatContext *formatCtx = nullptr;
    AVCodecContext *aCodecCtx = NULL;
    AVCodec *aCodec = NULL;
    int audioStream;

    // 初始化SDL
    if (SDL_Init(SDL_INIT_AUDIO | SDL_INIT_TIMER))
    {
        cout << "SDL_Init failed: " << SDL_GetError() << endl;
        return -1;
    }

    // 打开音频文件
    if (avformat_open_input(&formatCtx, filename.c_str(), nullptr, nullptr) != 0)
    {
        cout << "无法打开音频文件" << endl;
        return -1;
    }

    // 获取流信息
    if (avformat_find_stream_info(formatCtx, nullptr) < 0)
    {
        cout << "无法获取流信息" << endl;
        return -1;
    }

    // 找到音频流
    audioStream = -1;
    for (unsigned int i = 0; i < formatCtx->nb_streams; i++)
    {
        if (formatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
        {
            audioStream = i;
            break;
        }
    }
    if (audioStream == -1)
    {
        cout << "未找到音频流" << endl;
        return -1;
    }

    // 获取解码器
    aCodec = avcodec_find_decoder(formatCtx->streams[audioStream]->codecpar->codec_id);
    if (!aCodec)
    {
        cout << "未找到解码器" << endl;
        return -1;
    }

    // 配置音频参数
    aCodecCtx = avcodec_alloc_context3(aCodec);
    avcodec_parameters_to_context(aCodecCtx, formatCtx->streams[audioStream]->codecpar);
    aCodecCtx->pkt_timebase = formatCtx->streams[audioStream]->time_base;
    // 打开解码器
    if (avcodec_open2(aCodecCtx, aCodec, NULL) < 0)
    {
        cout << "无法打开解码器" << endl;
        return -1;
    }

接下来我们初始化一下重采样器,重采样器是用来将解码后的音频数据转换成我们需要的格式,这里我们将音频数据转换成SDL2支持的格式。

c++ 复制代码
    SwrContext *swrCtx = swr_alloc(); // 申请重采样器内存
    /*设置相关参数*/
    av_opt_set_int(swrCtx, "in_channel_layout", aCodecCtx->channel_layout, 0);
    av_opt_set_int(swrCtx, "out_channel_layout", aCodecCtx->channel_layout, 0);
    av_opt_set_int(swrCtx, "in_sample_rate", aCodecCtx->sample_rate, 0);
    av_opt_set_int(swrCtx, "out_sample_rate", aCodecCtx->sample_rate, 0);
    av_opt_set_sample_fmt(swrCtx, "in_sample_fmt", aCodecCtx->sample_fmt, 0);
    av_opt_set_sample_fmt(swrCtx, "out_sample_fmt", AV_SAMPLE_FMT_S16, 0);
    swr_init(swrCtx); // 初始化重采样器

    AVPacket packet;
    

这样关于解码音频的配置就结束了。下一步来配置SDL2播放相关的东西。

c++ 复制代码
    /*配置SDL音频*/
    SDL_AudioSpec wanted_spec;
    /*配置参数*/
    wanted_spec.freq = aCodecCtx->sample_rate; // 播放音频的采样率
    wanted_spec.format = AUDIO_S16;            // 播放格式,s16即为16位
    wanted_spec.channels = aCodecCtx->channels;// 播放通道数,即声道数
    wanted_spec.silence = 0;`                  // 静音填充数据
    wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;// 回调函数缓冲区大小
    wanted_spec.callback = audio_callback;      // 回调函数入口即函数名
    wanted_spec.userdata = aCodecCtx;           // 用户数据,为void指针类型

    /*打开*/
    SDL_OpenAudio(&wanted_spec, &spec)
    /*开始播放音频,注意在这句之后就开始启用拉取模式开始播放了*/
    SDL_PauseAudio(0);

这里特地说一下回调函数缓冲区大小。缓冲区越大,意味着能存储的数据越多,同理每次获取数据多了调用回调函数的次数就会减少也就是间隔会增大。这样的话对于长时间的音频播放来说,可以减少由于缓冲区数据不足导致的播放中断与卡顿,但是回调函数调用时间过长也会有着音频播放延迟过大。

当然这个延迟过大是针对短时长高次数播放的场景比如语音聊天等,此时可以调少缓冲区来减少延迟。但是对于已知时长且长时间播放的音频来说,可以适当增大缓冲区来减少播放中断。

下面写回调函数

c++ 复制代码
int audio_buf_index = 0;
int audio_buf_size = 0;
void audio_callback(void *userdata, Uint8 *stream, int len)
{
    AVCodecContext *aCodecCtx = (AVCodecContext *)userdata; // 将用户数据转换到AVCodecContext类指针以使用
    int len1; // 当前的数据长度
    int audio_size; // 解码出的数据长度
    while (len > 0)
    {
        if (audio_buf_index >= audio_buf_size)
        {
            audio_size = audio_decode_frame(aCodecCtx, audio_buf, sizeof(audio_buf)); // 去解码一帧数据
            if (audio_size < 0)
            {
                audio_buf_size = 1024;
                memset(audio_buf, 0, audio_buf_size);
            }
            else
            {
                audio_buf_size = audio_size;
            }
            audio_buf_index = 0;
        }
        len1 = audio_buf_size - audio_buf_index;
        if (len1 > len)
            len1 = len;
        memcpy(stream, (uint8_t *)audio_buf + audio_buf_index, len1);
        len -= len1;
        stream += len1;
        audio_buf_index += len1;
    }
}

其中的执行逻辑如下

c++ 复制代码
/*
 * 回调流程
 *
 * audio_buf_size 为解码后的音频数据大小
 * audio_buf_index 指向当前audio_buf_size中的已经加入到stream中的数据位置
 * audio_buf_index < audio_buf_size 时,说明上一次解码的数据还没用完,就接着用上一次解码剩下的数据
 * 否则需要解码新的音频数据
 * 而且注意audio_buf_size以及audio_buf_index均为全局变量,所以在函数调用之间是保持状态的
 * 当audio_buf_size < 0 时,也就是解码失败或者解码数据已经用光的时候,填充静音数据
 *
 * 每次会向stream中写入len个字节的数据,可能会出现len < audio_buf_size - audio_buf_index的情况
 * 这种情况下就会出现audio_buf_size并为使用完,因此会等到下一次回调时继续使用
 */

然后是实现audio_decode_frame()

c++ 复制代码
int audio_decode_frame(AVCodecContext *aCodecCtx, uint8_t *audio_buf, int buf_size)
{
    static AVPacket *pkt = av_packet_alloc();
    static AVFrame *frame = av_frame_alloc();
    int data_size = 0;
    int ret;

    // 首先获取并发送数据包
    if(!audioQueue.empty())
    {
        *pkt = audioQueue.front();
        audioQueue.pop();
    }

    // 发送数据包到解码器
    ret = avcodec_send_packet(aCodecCtx, pkt);
    av_packet_unref(pkt);
    if (ret < 0)
    {
        cout << "发送数据包到解码器失败" << endl;
        return -1;
    }

    // 然后尝试接收解码后的帧
    ret = avcodec_receive_frame(aCodecCtx, frame);
    if (ret == 0)
    {
        // 成功接收到帧,进行重采样处理
        int out_samples = av_rescale_rnd(
            swr_get_delay(swr_ctx, aCodecCtx->sample_rate) + frame->nb_samples,
            frame->sample_rate, // 输出采样率
            frame->sample_rate, // 输入采样率
            AV_ROUND_UP);
        // 计算相同采样时间不同采样频率下的采样数

        int out_buffer_size = av_samples_get_buffer_size(
            NULL,
            aCodecCtx->channels,
            out_samples,
            AV_SAMPLE_FMT_S16,
            1);

        if (out_buffer_size > audio_convert_buf_size)
        {
            av_free(audio_convert_buf);
            audio_convert_buf = (uint8_t *)av_malloc(out_buffer_size);
            audio_convert_buf_size = out_buffer_size;
        }

        // 执行重采样
        ret = swr_convert(
            swr_ctx,
            &audio_convert_buf,
            out_samples,
            (const uint8_t **)frame->data,
            frame->nb_samples);
        if (ret < 0)
        {
            cout << "重采样转换错误" << endl;
            return -1;
        }

        data_size = ret * frame->channels * 2;
        memcpy(audio_buf, audio_convert_buf, data_size);
        return data_size;
    }
    else if (ret == AVERROR_EOF)
    {
        // 解码器已经刷新完所有数据
        return -1;
    }
    else
    {
        // 其他错误
        cout << "解码时发生错误" << endl;
        return -1;
    }
}