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. 使用JConsole监控进程、线程、内存、cpu、类情况

    Jconsole简介: Jconsole是一个JMX兼容的监视工具.它使用Java虚拟机的JMX机制来提供运行在Java平台的应用程序的性能与资源耗费信息. 监控进程使用方法如下:由于JConsole ...

  2. laravel框架之ORM操作

    Laravel 支持原生的 SQL 查询.流畅的查询构造器 和 Eloquent ORM 三种查询方式: 流畅的查询构造器(简称DB),它是为创建和运行数据库查询提供的一个接口,支持大部分数据库操作, ...

  3. redis之性能优化

    1 redis-cli命令的 --stat选项 关于stat选项,官网也是介绍的比较简单.使用redis-cli命令加上stat选项可以实时监视redis实例,比如当前节点内存中缓存的 key总数以及 ...

  4. monitor磁盘空间不足警告

    虚拟机安装ceph时,执行ceph -s monitor主机遇到了 mon c101(monitor主机名) is low on available space 错误 这是我找到的解决办法 monit ...

  5. Mybatis 实现多字段动态排序

    背景 在项目的开发过程中,可能会遇到对数据表多个字段进行排序的需求(第一句话就这么难懂,不要害怕,万事开头难,结尾更难,开玩笑哒),结合需求轻松易懂. 需求 现在有一张User表 男同学先按 age ...

  6. 微服务应用整合SEATA实现分布式事务

    概要 seata 是alibaba 出的一款分布式事务管理器,他有侵入性小,实现简单等特点.我们能够使用seata 实现分布式事务管理, 是微服务必备的组件.他可以实现在微服务之间的事务管理,也可以实 ...

  7. ZCMU-1129

    数学公式题罢了 学长 1.斯特灵公式: 2.对数公式(因为以10为底,得到的是10^x,所以最后向下取整加上1): #include<cstdio> #include<cmath&g ...

  8. [Java] Stream流使用最多的方式

    Java 中 Stream 流的用法全解析 在 Java 编程中,Stream 流提供了一种高效.便捷的方式来处理集合数据.它可以让我们以声明式的方式对数据进行各种操作,如过滤.映射.排序.聚合等,大 ...

  9. Qt在linux下实现程序编译后版本号自增的脚本

    #! /bin/bash rm -rf temp.cpp num=0 while read line do if [ $num -eq 3 ];then array=(`echo $line | tr ...

  10. 【C语言】【二级】将所指字符串中所有下标为奇数位置上的字母转换成大写

    题目 请编写一个函数fun,它的功能是:将ss所指字符串中所有下标为奇数位置上的字母转换成大写(若该位置上不是字母,则不转换). 例如,若输入" abc4EFG",则应输出&quo ...