若该文为原创文章,未经允许不得转载
原博主博客地址:https://blog.csdn.net/qq21497936
原博主博客导航:https://blog.csdn.net/qq21497936/article/details/102478062
本文章博客地址:https://blog.csdn.net/qq21497936/article/details/108828879
各位读者,知识无穷而人力有穷,要么改需求,要么找专业人士,要么自己研究

 

前言

  ffmpeg解码音频之后进行播放,本篇使用SDL播放ffmpeg解码音频流转码后的pcm。

 

FFmpeg解码音频

  FFmpeg解码音频的基本流程请参照:《FFmpeg开发笔记(七):ffmpeg解码音频保存为PCM并使用软件播放

 

SDL播放音频

  SDL播放音频的基本流程请参照:《SDL开发笔记(二):音频基础介绍、使用SDL播放音频

 

ffmpeg音频同步

  ffmpeg同步包含音频、视频、字幕等等,此处描述的同步是音频的同步。

基本流程

  

同步关键点

  不改变播放速度的前提下,音频的播放相对容易,本文章暂时未涉及到音视频双轨或多轨同步。
  解码音频后,时间间隔还是计算一下,主要是控制解码的间隔,避免解码过快导致缓存区溢出导致异常。
  解码音频进行重采样之后可以得到指定采样率、声道、数据类型的固定参数,使用SDL用固定参数打开音频,将解码的数据扔向缓存区即可。因为解码的时候其数据量与采样率是对应的,播放的时候也是扔入对应的数据量,所以再不改变音频采样率的前提下,我们是可以偷懒不做音频同步的。
  压入数据缓存区,可以根据播放的回调函数之后数据缓存区的大小进行同步解码压入音频,但是音频与视频不同,音频卡顿的话对音频播放的效果将会大打折扣,导致音频根本无法被顺利的播放,非常影响用户体验,实测需要保留一倍以上的预加载的音频缓冲区,否则等需要的是再加载就已经晚了。
  音频更为复杂的操作涉及到倍速播放、音调改变等等,后续文章会有相应的文章说明。

 

ffmpeg音频同步相关结构体详解

AVCodecContext

  该结构体是编码上下文信息,对文件流进行探测后,就能得到文件流的具体相关信息了,关于编解码的相关信息就在此文件结构中。
  与同步视频显示相关的变量在此详解一下,其他的可以自行去看ffmpeg源码上对于结构体AVCodecContext的定义。

struct AVCodecContext {
AVMediaType codec_type; // 编码器的类型,如视频、音频、字幕等等
AVCodec *codec; // 使用的编码器
enum AVSampleFormat sample_fmt; // 音频采样点的数据格式
int sample_rate; // 每秒的采样率
int channels; // 音频通道数据量
uint64_t channel_layout; // 通道布局
} AVCodecContext;

SwrContext

  重采样的结构体,最关键的是几个参数,输入的采样频率、通道布局、数据格式,输出的采样频率、通道布局、数据格式。
此结构是不透明的。这意味着如果要设置选项,必须使用API,无法直接将属性当作结构体变量进行设置。

  • 属性“out_ch_layout”:输入的通道布局,需要通过通道转换函数转换成通道布局枚举,其通道数与通道布局枚举的值是不同的,
  • 属性“out_sample_fmt”:输入的采样点数据格式,解码流的数据格式即可。
  • 属性“out_sample_rate”:输入的采样频率,解码流的采样频率。
  • 属性“in_ch_layout”:输出的通道布局。
  • 属性“in_sample_fmt”:输出的采样点数据格式。
      
  • 属性“in_sample_rate”:输出的采样频率。
 

Demo源码

void FFmpegManager::testDecodeAudioPlay()
{
QString fileName = "E:/testFile2/1.mp3"; // 输入解码的文件
QFile file("D:/1.pcm"); AVFormatContext *pAVFormatContext = 0; // ffmpeg的全局上下文,所有ffmpeg操作都需要
AVStream *pAVStream = 0; // ffmpeg流信息
AVCodecContext *pAVCodecContext = 0; // ffmpeg编码上下文
AVCodec *pAVCodec = 0; // ffmpeg编码器
AVPacket *pAVPacket = 0; // ffmpag单帧数据包
AVFrame *pAVFrame = 0; // ffmpeg单帧缓存
SwrContext *pSwrContext = 0; // ffmpeg音频转码 SDL_AudioSpec sdlAudioSpec; // sdk音频结构体,用于打开音频播放器 int ret = 0; // 函数执行结果
int audioIndex = -1; // 音频流所在的序号
int numBytes = 0; // 音频采样点字节数
uint8_t * outData[8] = {0}; // 音频缓存区(不带P的)
int dstNbSamples = 0; // 解码目标的采样率 int outChannel = 0; // 重采样后输出的通道
AVSampleFormat outFormat = AV_SAMPLE_FMT_NONE; // 重采样后输出的格式
int outSampleRate = 0; // 重采样后输出的采样率 pAVFormatContext = avformat_alloc_context(); // 分配
pAVPacket = av_packet_alloc(); // 分配
pAVFrame = av_frame_alloc(); // 分配 if(!pAVFormatContext || !pAVPacket || !pAVFrame)
{
LOG << "Failed to alloc";
goto END;
} // 步骤一:注册所有容器和编解码器(也可以只注册一类,如注册容器、注册编码器等)
av_register_all(); // 步骤二:打开文件(ffmpeg成功则返回0)
LOG << "文件:" << fileName << ",是否存在:" << QFile::exists(fileName);
ret = avformat_open_input(&pAVFormatContext, fileName.toUtf8().data(), 0, 0);
if(ret)
{
LOG << "Failed";
goto END;
} // 步骤三:探测流媒体信息
ret = avformat_find_stream_info(pAVFormatContext, 0);
if(ret < 0)
{
LOG << "Failed to avformat_find_stream_info(pAVCodecContext, 0)";
goto END;
} // 步骤四:提取流信息,提取视频信息
for(int index = 0; index < pAVFormatContext->nb_streams; index++)
{
pAVCodecContext = pAVFormatContext->streams[index]->codec;
pAVStream = pAVFormatContext->streams[index];
switch (pAVCodecContext->codec_type)
{
case AVMEDIA_TYPE_UNKNOWN:
LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_UNKNOWN";
break;
case AVMEDIA_TYPE_VIDEO:
LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_VIDEO";
break;
case AVMEDIA_TYPE_AUDIO:
LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_AUDIO";
audioIndex = index;
break;
case AVMEDIA_TYPE_DATA:
LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_DATA";
break;
case AVMEDIA_TYPE_SUBTITLE:
LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_SUBTITLE";
break;
case AVMEDIA_TYPE_ATTACHMENT:
LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_ATTACHMENT";
break;
case AVMEDIA_TYPE_NB:
LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_NB";
break;
default:
break;
}
// 已经找打视频品流
if(audioIndex != -1)
{
break;
}
}
if(audioIndex == -1 || !pAVCodecContext)
{
LOG << "Failed to find video stream";
goto END;
} // 步骤五:对找到的音频流寻解码器
pAVCodec = avcodec_find_decoder(pAVCodecContext->codec_id);
if(!pAVCodec)
{
LOG << "Fialed to avcodec_find_decoder(pAVCodecContext->codec_id):"
<< pAVCodecContext->codec_id;
goto END;
} // 步骤六:打开解码器
ret = avcodec_open2(pAVCodecContext, pAVCodec, NULL);
if(ret)
{
LOG << "Failed to avcodec_open2(pAVCodecContext, pAVCodec, pAVDictionary)";
goto END;
} // 打印
LOG << "解码器名称:" <<pAVCodec->name << endl
<< "通道数:" << pAVCodecContext->channels << endl
<< "通道布局:" << av_get_default_channel_layout(pAVCodecContext->channels) << endl
<< "采样率:" << pAVCodecContext->sample_rate << endl
<< "采样格式:" << pAVCodecContext->sample_fmt; outChannel = 2;
outSampleRate = 44100;
outFormat = AV_SAMPLE_FMT_S16; // 步骤七:获取音频转码器并设置采样参数初始化
pSwrContext = swr_alloc_set_opts(0, // 输入为空,则会分配
av_get_default_channel_layout(outChannel),
outFormat, // 输出的采样频率
outSampleRate, // 输出的格式
av_get_default_channel_layout(pAVCodecContext->channels),
pAVCodecContext->sample_fmt, // 输入的格式
pAVCodecContext->sample_rate, // 输入的采样率
0,
0);
ret = swr_init(pSwrContext);
if(ret < 0)
{
LOG << "Failed to swr_init(pSwrContext);";
goto END;
}
// 最大缓存区,1152个采样样本,16字节,支持最长8个通道
outData[0] = (uint8_t *)av_malloc(1152 * 2 * 8); ret = SDL_Init(SDL_INIT_AUDIO); // SDL步骤一:初始化音频子系统
ret = SDL_Init(SDL_INIT_AUDIO);
if(ret)
{
LOG << "Failed";
return;
}
// SDL步骤二:打开音频设备
sdlAudioSpec.freq = outSampleRate;
sdlAudioSpec.format = AUDIO_S16LSB;
sdlAudioSpec.channels = outChannel;
sdlAudioSpec.silence = 0;
sdlAudioSpec.samples = 1024;
sdlAudioSpec.callback = callBack_fillAudioData;
sdlAudioSpec.userdata = 0; ret = SDL_OpenAudio(&sdlAudioSpec, 0);
if(ret)
{
LOG << "Failed";
return;
} SDL_PauseAudio(0); _audioBuffer = (uint8_t *)malloc(102400);
file.open(QIODevice::WriteOnly | QIODevice::Truncate);
// 步骤八:读取一帧数据的数据包
while(av_read_frame(pAVFormatContext, pAVPacket) >= 0)
{
if(pAVPacket->stream_index == audioIndex)
{
// 步骤九:将封装包发往解码器
ret = avcodec_send_packet(pAVCodecContext, pAVPacket);
if(ret)
{
LOG << "Failed to avcodec_send_packet(pAVCodecContext, pAVPacket) ,ret =" << ret;
break;
}
// 步骤十:从解码器循环拿取数据帧
while(!avcodec_receive_frame(pAVCodecContext, pAVFrame))
{
// nb_samples并不是每个包都相同,遇见过第一个包为47,第二个包开始为1152的
// LOG << pAVFrame->nb_samples;
// 步骤十一:获取每个采样点的字节大小
numBytes = av_get_bytes_per_sample(outFormat);
// 步骤十二:修改采样率参数后,需要重新获取采样点的样本个数
dstNbSamples = av_rescale_rnd(pAVFrame->nb_samples,
outSampleRate,
pAVCodecContext->sample_rate,
AV_ROUND_ZERO);
// 步骤十三:重采样
swr_convert(pSwrContext,
outData,
dstNbSamples,
(const uint8_t **)pAVFrame->data,
pAVFrame->nb_samples);
// 第一次显示
static bool show = true;
if(show)
{
LOG << numBytes << pAVFrame->nb_samples << "to" << dstNbSamples;
show = false;
}
// 缓存区大小,小于一次回调获取的4097就得提前添加,否则声音会开盾
while(_audioLen > 4096 * 1)
// while(_audioLen > 4096 * 0)
{
SDL_Delay(1);
}
_mutex.lock();
memcpy(_audioBuffer + _audioLen, outData[0], numBytes * dstNbSamples * outChannel);
file.write((const char *)outData[0], numBytes * dstNbSamples * outChannel);
_audioLen += numBytes * dstNbSamples * outChannel;
_mutex.unlock();
}
av_free_packet(pAVPacket);
}
}
END:
file.close();
LOG << "释放回收资源";
SDL_CloseAudio();
SDL_Quit();
if(outData[0])
{
av_free(outData[0]);
outData[0] = 0;
LOG << "av_free(outData)";
}
if(pSwrContext)
{
swr_free(&pSwrContext);
pSwrContext = 0;
}
if(pAVFrame)
{
av_frame_free(&pAVFrame);
pAVFrame = 0;
LOG << "av_frame_free(pAVFrame)";
}
if(pAVPacket)
{
av_free_packet(pAVPacket);
pAVPacket = 0;
LOG << "av_free_packet(pAVPacket)";
}
if(pAVCodecContext)
{
avcodec_close(pAVCodecContext);
pAVCodecContext = 0;
LOG << "avcodec_close(pAVCodecContext);";
}
if(pAVFormatContext)
{
avformat_close_input(&pAVFormatContext);
avformat_free_context(pAVFormatContext);
pAVFormatContext = 0;
LOG << "avformat_free_context(pAVFormatContext)";
}
} void FFmpegManager::callBack_fillAudioData(void *userdata, uint8_t *stream, int len)
{
SDL_memset(stream, 0, len); _mutex.lock();
if(_audioLen == 0)
{
_mutex.unlock();
return;
}
LOG << _audioLen << len;
len = (len > _audioLen ? _audioLen : len);
SDL_MixAudio(stream, _audioBuffer, len, SDL_MIX_MAXVOLUME);
_audioLen -= len;
memmove(_audioBuffer, _audioBuffer + len, _audioLen); _mutex.unlock(); // 每次加载4096
// LOG << _audioLen << len;
}
 

工程模板v1.4.0

  对应工程模板v1.4.0:增加解码音频转码使用SDL播放

 
 

FFmpeg开发笔记(八):ffmpeg解码音频并使用SDL同步音频播放的更多相关文章

  1. FFmpeg开发笔记(五):ffmpeg解码的基本流程详解(ffmpeg3新解码api)

    若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...

  2. FFmpeg开发笔记(四):ffmpeg解码的基本流程详解

    若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...

  3. FFmpeg开发笔记(九):ffmpeg解码rtsp流并使用SDL同步播放

    前言   ffmpeg播放rtsp网络流和摄像头流.   Demo   使用ffmpeg播放局域网rtsp1080p海康摄像头:延迟0.2s,存在马赛克     使用ffmpeg播放网络rtsp文件流 ...

  4. FFmpeg开发笔记(三):ffmpeg介绍、windows编译以及开发环境搭建

    前言   本篇章是对之前windows环境的补充,之前windows的是无需进行编译的,此篇使用源码进行编译,版本就使用3.4.8.   FFmpeg简介   FFmpeg是领先的多媒体框架,能够解码 ...

  5. FFmpeg开发笔记(十):ffmpeg在ubuntu上的交叉编译移植到海思HI35xx平台

    FFmpeg和SDL开发专栏(点击传送门) 上一篇:<FFmpeg开发笔记(九):ffmpeg解码rtsp流并使用SDL同步播放>下一篇:敬请期待   前言   将ffmpeg移植到海思H ...

  6. FFmpeg开发笔记(一)搭建Linux系统的开发环境

    对于初学者来说,如何搭建FFmpeg的开发环境是个不小的拦路虎,因为FFmpeg用到了许多第三方开发包,所以要先编译这些第三方源码,之后才能给FFmpeg集成编译好的第三方库.不过考虑到刚开始仅仅调用 ...

  7. ffmpeg学习笔记-初识ffmpeg

    ffmpeg用来对音视频进行处理,那么在使用ffmpeg前就需要ffmpeg有一个大概的了解,这里使用雷神的ppt素材进行整理,以便于复习 音视频基础知识 视频播放器的原理 播放视频的流程大致如下: ...

  8. FFmpeg开发笔记(二)搭建Windows系统的开发环境

    由于Linux系统比较专业,个人电脑很少安装Linux,反而大都安装Windows系统,因此提高了FFmpeg的学习门槛,毕竟在Windows系统搭建FFmpeg的开发环境还是比较麻烦的.不过若有已经 ...

  9. netty权威指南学习笔记八——编解码技术之JBoss Marshalling

    JBoss Marshalling 是一个java序列化包,对JDK默认的序列化框架进行了优化,但又保持跟java.io.Serializable接口的兼容,同时增加了一些可调参数和附加特性,这些参数 ...

  10. 《ArcGIS Runtime SDK for Android开发笔记》——数据制作篇:发布具有同步能力的FeatureService服务

    1.前言 从ArcGIS 10.2.1开始推出离在线一体化技术之后,数据的离在线一体化编辑一直是大家所关注的一个热点.数据存储在企业级地理数据库中,通过ArcGIS桌面软件加载后配图处理,并发布到Ar ...

随机推荐

  1. shell的date的部分处理--需要记住..

    在Linux中,可以使用date命令获取日期, date 获取当前完整日期 date --date="3 days ago" 获取3天前的完整日期 date --date=&quo ...

  2. [转帖]VMware Converter (P2V迁移)问题汇总

    https://www.dinghui.org/vmware-converter-p2v.html VMware vCenter Converter Standalone,是一种用于将虚拟机和物理机转 ...

  3. [转帖]高性能网络实战:借助 eBPF 来优化负载均衡的性能

    https://zhuanlan.zhihu.com/p/592981662 网络性能优化,eBPF 是如何发挥作用的呢? 本篇文章,我就以最常用的负载均衡器为例,带你一起来看看如何借助 eBPF 来 ...

  4. [转帖]03-rsync传输模式(本地传输、远程方式传输、守护进程模式传输)

    https://developer.aliyun.com/article/885801?spm=a2c6h.24874632.expert-profile.282.7c46cfe9h5DxWK 简介: ...

  5. 飞腾2000+银河麒麟v10安装redis的注意事项

    先说一下结论 无法复用ubuntu上面编译的二进制文件 无法直接使用docker官网下面的arm64的镜像运行 无法直接使用redis6.0.10最新版本编译运行 可以使用redis5.0.4 进行编 ...

  6. CPU 大拿的作品

    http://nieyong.github.io/wiki_cpu/index.html 改天学习写一下.

  7. RedisSyncer同步引擎的设计与实现

    RedisSyncer一款通过replication协议模拟slave来获取源Redis节点数据并写入目标Redis从而实现数据同步的Redis同步中间件. 该项目主要包括以下子项目: redis 同 ...

  8. git撤销推送到远端仓库的提交commit信息

    场景描述 有些时候,我们完成功能后,高兴的推送到远端. 推送到远端之后,我们才发现写错分支了. 这个时候,一万匹马在在内心奔腾而过. 然而,难受是没有用的,我们需要撤销推送到远端的代码 git log ...

  9. 洛谷P3101 题解

    输入格式 第 \(1\) 行,三个整数 \(m,n,t\). 第 \(2\) 到 \(m+1\) 行,\(m\) 个整数,表示海拔高度. 第 \(2+m\) 到 \(2m+1\) 行,\(m\) 个整 ...

  10. 数字预失真(DPD)小试

    前言 射频功放的增益响应并非线性的,受到放大管饱和效应的影响,功放不可避免地出现非线性.甚至具有记忆效应的失真.这种非线性失真不仅产生高阶谐波,还会产生互调干扰,降低带内信噪比,影响带外信号.因此,需 ...