首页 > 其他分享 >ffmpeg简易播放器(4)--使用SDL播放音频

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

时间:2025-01-21 20:14:06浏览次数:1  
标签:播放 ffmpeg -- 音频 SDL audio buf size

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文件

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中添加

find_package(SDL2 REQUIRED)
include_directories(${SDL2_INCLUDE_DIRS})
......
target_link_libraries(${PROJECT_NAME} ${SDL2_LIBS})

使用ffmpeg解码音频

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

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

#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支持的格式。

    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播放相关的东西。

    /*配置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);

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

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

下面写回调函数

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;
    }
}

其中的执行逻辑如下

/*
 * 回调流程
 *
 * 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()

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;
    }
}

标签:播放,ffmpeg,--,音频,SDL,audio,buf,size
From: https://www.cnblogs.com/CrescentWind/p/18684348

相关文章

  • LINQ 查询添加自定义方法
    所有基于LINQ的方法都遵循两种类似的模式之一。它们采用可枚举序列。它们会返回不同的序列或单个值。通过形状的一致性,可以通过编写具有类似形状的方法来扩展LINQ。事实上,自首次引入LINQ以来,.NET库就在许多.NET版本中都获得了新的方法。在本文中,你将看到通过编写遵循......
  • 点分治维护树上修改与查询
    点分治维护树上修改与查询具体方法就是将操作(修改与查询)离线,并打上时间戳,将其挂在点上,这样就可以考虑一个点到另一个点的贡献是否可以在其询问之前到达。对于所有的点分治都要效:避免算到同一个子树中,可以先整体计算后,在分别进入每个子树中,这样就可以不使用动态开点线段树了......
  • 图片内存变大
    平时我们会经常遇到压缩图片内存的情况,但是需要把图片内存变大的情况有人遇到过吗,接下来就是图片变大术的详细教程!将需要处理的图片放在一个文件夹内(例:图片a.png放在D盘根目录下)win+R输入cmd打开命令控制行在命令控制行输入cd+图片所在的目录,如果是在磁盘根目录直接......
  • paddleocr图片文字识别
    介绍:PaddleOCR是由百度开发的一个OCR库,基于深度学习框架PaddlePaddle。PaddleOCR支持多语言文本识别,特别适合中文场景,同时它还提供了丰富的预训练模型。1、安装pip3installpaddlepaddlepip3installpaddleocr2、使用frompaddleocrimportPaddleOCRdefpaddle_image......
  • 解决 WebSocket 连接断开问题:前端心跳机制的实现与优化
    在开发过程中,我们经常会遇到需要实时通信的场景,而WebSocket是一种非常合适的技术选择。然而,在实际使用WebSocket的过程中,我们可能会遇到连接频繁断开的问题。最近,我在一个项目中就遇到了这样的问题,经过一番探索和优化,终于找到了解决方案,现在与大家分享一下。问题背景在项目......
  • P1486 [NOI2004] 郁闷的出纳员
    P1486[NOI2004]郁闷的出纳员题目翻译:维护一个可重数集,共有\(n\)次操作,和一个最小限制\(min\),共有四种操作:\(I\)\(k\)给集合添加\(k\)若\(k<min\)则直接删除(不算入删除个数)\(A\)\(k\)将集合中的所有元素加上\(k\)\(S\)\(k\)将所有元素减少\(k\)并将所有值......
  • P1048 [NOIP2005 普及组] 采药 题解
    原题链接题目大意:采药,每种药只有一株,每株有它的价值和采它所需的时间,现时间有限,请你输出在有限时间内能获得的价值最大是多少。分析:1.这是一个典型的01背包问题(DP)01背包问题的典型特征:有一个限定容量的背包(对应本题中的时间),有物品(每种只有一个)(对应本题中的药株),物品有......
  • 洛谷P1002 [NOIP2002 普及组] 过河卒 题解
    原题链接题目大意:棋盘上A点有一个过河卒,需要走到目标B点。卒行走的规则:向下或向右。同时在棋盘上C点有一个对方的马,该马所在的点和所有跳跃一步可达的点称为对方马的控制点。棋盘用坐标表示,AA点(0,0)、BB点(n,m),同样马的位置坐标是需要给出的。现在要求你计算出......
  • Git原理与应用(三)【远程操作 | 理解分布式 | 推送拉取远程仓库 | 标签管理】
    Git理解分布式版本控制系统远程仓库新建远程仓库克隆远程仓库向远程仓库推送配置Git忽略特殊文件标签管理理解标签创建标签操作标签删除标签理解分布式版本控制系统我们⽬前所说的所有内容(工作区,暂存区,版本库等等),都是在本地!也就是在你的笔记本或者计算机上。而我们......
  • Pandas数据分析 【Series | DataFrame】
    pandas数据分析写在前面001List转化为Series002Dict转化为Series003Series转化为pythonlist004Series转化为DataFrame005借助numpy创建Series006转化Series的数据类型007给Series添加新的元素008将Series对象转换为DataFrame对象009使用字典创建DF010给DataFr......