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播放音频的更多相关文章

  1. 搭建rtmp直播流服务之4:videojs和ckPlayer开源播放器二次开发(播放rtmp、hls直播流及普通视频)

    前面几章讲解了使用 nginx-rtmp搭建直播流媒体服务器; ffmpeg推流到nginx-rtmp服务器; java通过命令行调用ffmpeg实现推流服务; 从数据源获取,到使用ffmpeg推流, ...

  2. html 音乐 QQ播放器 外链 代码 播放器 外链 代码

    韩梦飞沙  韩亚飞  313134555@qq.com  yue31313  han_meng_fei_sha QQ播放器 外链 代码 播放器 外链 代码 ======== 歌曲链接 QQ播放器 外链 ...

  3. EasyPlayer RTSP Windows(with ActiveX/OCX插件)播放器支持H.265播放与抓图功能

    EasyPlayer作为业界一款比较优秀的RTSP播放器,一直深受用户的好评,经过了近3年的开发和迭代,从一开始的简单PC版本的RTSP播放功能,到如今支持PC(支持ocx插件).Android.iO ...

  4. EasyPlayerPro Windows播放器进行本地对讲喊话音频采集功能实现

    需求 在安防行业应用中,除了在本地看到摄像机的视频和进行音频监听外,还有一个重要的功能,那就是对讲. EasyPlayerPro-win为了减轻二次开发者的工作量,将本地音频采集也进行了集成: 功能特 ...

  5. Android 仿百度网页音乐播放器圆形图片转圈播放效果

    百度网页音乐播放器的效果  如下 : http://www.baidu.com/baidu?word=%E4%B8%80%E7%9B%B4%E5%BE%88%E5%AE%89%E9%9D%99& ...

  6. Android播放器推荐:可以播放本地音乐、视频、在线播放音乐、视频、网络收音机等

    下载链接:http://www.eoeandroid.com/forum.php?mod=attachment&aid=MTAxNTczfGMyNjNkMzFlfDEzNzY1MzkwNTR8 ...

  7. flash播放器插件与flash播放器的区别

    flash插件是一个网页ActiveX控件,而flash播放器是一个exe的可执行程序.前者用于播放网页中的falsh动画,而后者用于播放本地swf格式文件.

  8. 简单播放器(增加sdl事件控制)

    #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libswscal ...

  9. 22_Android中的本地音乐播放器和网络音乐播放器的编写,本地视频播放器和网络视频播放器,照相机案例,偷拍案例实现

    1 编写以下案例: 当点击了"播放"之后,在手机上的/mnt/sdcard2/natural.mp3就会播放. 2 编写布局文件activity_main.xml <Line ...

  10. 关于Ckpalyer播放器的MP4无法播放问题

    此文是从网上摘要的 有时在本地使用ckplayer来播放视频,flv格式非常容易的就播放了,但是使用mp4格式却显示:加载失败.为什么呢?        首页看下你i的本地站点MIME类型中,是否增加 ...

随机推荐

  1. 低功耗4G模组Air780E快速入门:固件的远程升级

    ​ 今天我们学习Air780E快速入门之固件的远程升级,小伙伴们,学起来吧! 一.生成差分包 合宙的远程升级支持使用合宙云平台和自建服务器,此例程使用的是合宙云平台. 1.1 准备新旧版的core和脚 ...

  2. 《刚刚问世》系列初窥篇-Java+Playwright自动化测试-5-创建首个自动化脚本(详细教程)

    1.简介 前面几篇宏哥介绍了两种(java和maven)环境搭建和浏览器的启动方法,这篇文章宏哥将要介绍第一个自动化测试脚本.前边环境都搭建成功了,浏览器也驱动成功了,那么我们不着急学习其他内容,首先 ...

  3. TreeMap源码分析——基础分析(基于JDK1.6)

    常见的数据结构有数组.链表,还有一种结构也很常见,那就是树.前面介绍的集合类有基于数组的ArrayList,有基于链表的LinkedList,还有链表和数组结合的HashMap,今天介绍基于树的Tre ...

  4. 使用canal将数据同步到ROCKETMQ

    概述 我们需要将数据从MYSQL写入到rocketmq.实现步骤如下: 安装canal.admin 修改配置 server: port: 8849 spring: jackson: date-form ...

  5. Reviewbot 开源 | 这些写 Go 代码的小技巧,你都知道吗?

    Reviewbot 是七牛云开源的一个项目,旨在提供一个自托管的代码审查服务, 方便做 code review/静态检查, 以及自定义工程规范的落地. 自从上了 Reviewbot 之后,我发现有些 ...

  6. mysql基础之增删改查

    标签: mysql 增加数据 -- 增加数据 use myblog; insert into users(username, `password`, realname) values('zhangsa ...

  7. 为了改一行代码,我花了10多天时间,让性能提升了40多倍---Pascal架构GPU在vllm下的模型推理优化

    ChatGPT生成的文章摘要 这篇博客记录了作者在家中使用Pascal显卡运行大型模型时遇到的挑战和解决方案.随着本地大型模型性能的提升,作者选择使用vllm库进行推理.然而,作者遇到了多个技术难题, ...

  8. Vue-Router 面试题 (2023-09-13更新)

    路由导航守卫 和 Vue 实例生成周期钩子函数的执行顺序? 路由导航守卫 都是在 Vue 实例生命周期钩子函数 之前执行的 Vue-Router 有哪几种导航钩子? 1. 全局守卫 全局前置守卫:be ...

  9. win10 ocx控件注册失败的解决办法

    首先注意:ocx放在大部分系统目录无法注册,比如program file ,但windows目录可以. 第一步:关闭防火墙 第二步:确定是否缺少控件运行需要的微软运行库或其他运行环境 这里提供一个挺全 ...

  10. 【C#】接口的基本概念

    目录 基本 什么是接口 接口与抽象类的区别 抽象类 接口 实例 设计接口 基本 什么是接口 C#接口(interface)是:用来定义一种程序的协定 实现接口的类或者结构要与接口的定义严格一致. 有了 ...