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文件
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简易播放器(4)--使用SDL播放音频的更多相关文章
- 搭建rtmp直播流服务之4:videojs和ckPlayer开源播放器二次开发(播放rtmp、hls直播流及普通视频)
		
前面几章讲解了使用 nginx-rtmp搭建直播流媒体服务器; ffmpeg推流到nginx-rtmp服务器; java通过命令行调用ffmpeg实现推流服务; 从数据源获取,到使用ffmpeg推流, ...
 - html 音乐 QQ播放器 外链 代码 播放器 外链 代码
		
韩梦飞沙 韩亚飞 313134555@qq.com yue31313 han_meng_fei_sha QQ播放器 外链 代码 播放器 外链 代码 ======== 歌曲链接 QQ播放器 外链 ...
 - EasyPlayer RTSP Windows(with ActiveX/OCX插件)播放器支持H.265播放与抓图功能
		
EasyPlayer作为业界一款比较优秀的RTSP播放器,一直深受用户的好评,经过了近3年的开发和迭代,从一开始的简单PC版本的RTSP播放功能,到如今支持PC(支持ocx插件).Android.iO ...
 - EasyPlayerPro Windows播放器进行本地对讲喊话音频采集功能实现
		
需求 在安防行业应用中,除了在本地看到摄像机的视频和进行音频监听外,还有一个重要的功能,那就是对讲. EasyPlayerPro-win为了减轻二次开发者的工作量,将本地音频采集也进行了集成: 功能特 ...
 - Android 仿百度网页音乐播放器圆形图片转圈播放效果
		
百度网页音乐播放器的效果 如下 : http://www.baidu.com/baidu?word=%E4%B8%80%E7%9B%B4%E5%BE%88%E5%AE%89%E9%9D%99& ...
 - Android播放器推荐:可以播放本地音乐、视频、在线播放音乐、视频、网络收音机等
		
下载链接:http://www.eoeandroid.com/forum.php?mod=attachment&aid=MTAxNTczfGMyNjNkMzFlfDEzNzY1MzkwNTR8 ...
 - flash播放器插件与flash播放器的区别
		
flash插件是一个网页ActiveX控件,而flash播放器是一个exe的可执行程序.前者用于播放网页中的falsh动画,而后者用于播放本地swf格式文件.
 - 简单播放器(增加sdl事件控制)
		
#include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libswscal ...
 - 22_Android中的本地音乐播放器和网络音乐播放器的编写,本地视频播放器和网络视频播放器,照相机案例,偷拍案例实现
		
1 编写以下案例: 当点击了"播放"之后,在手机上的/mnt/sdcard2/natural.mp3就会播放. 2 编写布局文件activity_main.xml <Line ...
 - 关于Ckpalyer播放器的MP4无法播放问题
		
此文是从网上摘要的 有时在本地使用ckplayer来播放视频,flv格式非常容易的就播放了,但是使用mp4格式却显示:加载失败.为什么呢? 首页看下你i的本地站点MIME类型中,是否增加 ...
 
随机推荐
- NSScrollView 内容显示不正常问题
			
NSScrollView 内容显示不正常,顶部没有对齐已经后边有空隙,说明Layout的方式错误,采用了Automatic导致的.需要采用如下布局方式才可以.
 - java根据时区转换获取时间的方法
			
方法一: public static void main(String[] args) { // 假设这是从MySQL获取的UTC时间字符串 String utcTimeStr = "202 ...
 - 用 300 行代码手写提炼 Spring 核心原理 [1]
			
系列文章 用 300 行代码手写提炼 Spring 核心原理 [1] 用 300 行代码手写提炼 Spring 核心原理 [2] 用 300 行代码手写提炼 Spring 核心原理 [3] 手写一个 ...
 - 4、oracle进程讲解
			
进程结构 server process服务器进程 前台进程(foreground process):server process(服务器进程) 用户连接到数据库实例以后,暂时可以认为是:对每一个用户连 ...
 - 使用yt-dlp下载youtube高清2k 60fps视频
			
只演示windows下的操作,linux和mac应该差不多的命令行. 首先放上github仓库地址:https://github.com/yt-dlp/yt-dlp 它的介绍: 厉害啊,支持数千个网站 ...
 - Git之常用文件
			
git项目中的特殊文件, 常见文件有.gitignore, .gitkeep 1) .gitkeep git默认是不允许提交一个空的目录到版本库上的, 可以在空的文件夹里面建立一个.gitkeep文件 ...
 - Java8 Lambda编程常用技巧
			
遍历打印List List<Integer> list= Arrays.asList(1,5,6,8,9,32,5,8,7,4,5); list.forEach(System.out::p ...
 - web移动端常见问题(一)
			
1.1物理像素 产生原因:css样式的最小值是1px,不过这个1px只是代表css像素,在高清屏上展示的物理像素要>1(iphone6 1css像素=2物理像素.而iph6p则是1css像素=3 ...
 - canvas(三)绘制矩形
			
1.绘制矩形轨迹 相关语法:ctx.rect(x,y,width,height),根据传入的参数(起始坐标和宽高)用来绘制一个矩形轨迹 注意:ctx.rect()和ctx.lineTo()绘制的都是轨 ...
 - 从Delphi到Lazarus——Delphi转换器
			
0.前言 在Lazarus中使用Delphi编写的文件是一件很简单的事情,这是因为Lazarus可以直接打开Delphi的任何文件,甚至有些文件可以不做任何修改就可以直接使用到你的Lazarus程序中 ...