audio_thread音频解码线程分析

Posted Loken2020

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了audio_thread音频解码线程分析相关的知识,希望对你有一定的参考价值。

之前在 stream_component_open() 里面的 decode_start() 函数开启了 audio_thread 线程,如下:

audio_thread 线程主要是负责 解码 PacketQueue 队列里面的 AVPacket 的,解码出来 AVFrame,然后丢给入口滤镜,再从出口滤镜把 AVFrame 读出来,再插入 FrameQueue 队列。

流程图如下:


如上,audio_thread 函数一开始就进入 一个 do...while... ,不断地调 decoder_decode_frame() 函数来解码出 AVFrame,然后把 AVFrame 往 入口滤镜 丢,再循环调 av_buffersink_get_frame_flags(),不断从出口滤镜收割经过 Filter 的AVFrame,最后调 frame_queue_push() 把 AVFrame 插入 FrameQueue 队列。

但是如果解码出来的 AVFrame 的音频格式与入口滤镜要求的音频格式不一样,会重建滤镜reconfigure),如下:

首先,开始的入口滤镜 (audio_filter_src)的音频格式 是直接从解码器实例(avctx)里面取的,如下:

ffplay.c 2648行
is->audio_filter_src.freq           = avctx->sample_rate;
is->audio_filter_src.channels       = avctx->channels;
is->audio_filter_src.channel_layout = get_valid_channel_layout(avctx->channel_layout, avctx->channels);
is->audio_filter_src.fmt            = avctx->sample_fmt;

解码器实例(avctx)的音频信息又是从 容器层的流信息里面取出来的。

ffpaly.c 2592 行
ret = avcodec_parameters_to_context(avctx, ic->streams[stream_index]->codecpar);

所以以下 3 种场景会重新创建滤镜:

1,容器层记录的采样率等信息是错误的,与实际解码出来的不符。

2,解码过程中,中途解码出来的 AVFrame 的采样率,声道数或者采样格式 出现变动,与上一次解码出来的 AVFrame 不一样。

3,进行了快进快退操作,因为快进快退会导致 is->auddec.pkt_serial 递增。详情请阅读《FFplay序列号分析》。我也不知道为什么序列号变了要重建滤镜。

这3种情况,ffplay 都会处理,只要解码出来的 AVFrame 跟入口滤镜的格式不一致,都会重建滤镜,把入口滤镜的格式设置为当前的 AVFrame 的格式,这样滤镜处理才不会出错。

补充:last_serial 变量一开始是 -1,而 is->auddec.pkt_serial 一开始是 0,所以一开始是必然会执行一次 reconfigure 操作。

由于每次读取出口滤镜的数据,都会用 while 循环把缓存刷完,不会留数据在滤镜容器里面,所以重建滤镜不会导致音频数据丢失。我圈一下代码里面的重点,如下:


audio_thread 线程的逻辑比较简单,复杂的地方都封装在它调用的子函数里面,所以本文简单讲解一下,audio_thread() 里面调用的各个函数的作用。

1,decoder_decode_frame(),从 PacketQueue 里面解码出来 AVFrame,此函数会阻塞,直到解码出来 AVFrame,或者返回错误。这个函数有 3 个返回值。

  • 返回 1,获取到 AVFrame 。
  • 返回 0 ,获取不到 AVFrame ,0 代表已经解码完MP4的所有AVPacket。这种情况一般是 ffplay 播放完了整个 MP4 文件,窗口画面停在最后一帧。但是由于你可以按 C 键重新循环播放,所以即便返回 0 也不能退出 audio_thread 线程。
  • 返回 -1,代表 PacketQueue 队列关闭了(abort_request)。返回 -1 会导致 audio_thread() 函数用 goto the_end 跳出 dowhlle 循环,跳出循环之后,audio_thread 线程就会自己结束了。返回 -1 通常是因为关闭了 ffplay 播放器。

更详细的分析请阅读《decoder_decode_frame函数分析》。

2,configure_audio_filters(),创建音频滤镜函数,之前在《FFplay音频滤镜分析》已经讲过此函数。

3,av_buffersrc_add_frame(),往入口滤镜发送 AVFrame

4,av_buffersink_get_frame_flags(),从出口滤镜读取 AVFrame

滤镜相关的函数推荐阅读 FFmpeg实战之路 一章的 《FFmpeg的scale滤镜介绍

5,frame_queue_peek_writable(),从 FrameQueue 里面取一个可以写的 Frame 出来。此函数也可能会阻塞

6,frame_queue_push(),这个函数有点奇怪,他其实不是把之前的 Frame 塞进去队列,而是把队列的写索引值 +1。

跟 FrameQueue队列相关的函数都在 《FrameQueue队列分析》一文中。


audio_thread() 函数最后还有一个重点,就是当 出口滤镜 结束的时候,finished 就会设置为 非 0 。

if (ret == AVERROR_EOF)
    is->auddec.finished = is->auddec.pkt_serial;

提示:只有往入口滤镜发送了 NULL 的 AVFrame ,出口滤镜才会结束。读完数据 跟 结束是两种状态,读完代表滤镜暂时没有数据可读,但是只要再往入口滤镜 发 AVFrame,出口滤镜就会又有数据可读。

而结束,代表不会再有 AVFrame 往入口滤镜发。


至此,audio_thread() 线程分析完毕。


推荐一个零声学院免费公开课程,个人觉得老师讲得不错,分享给大家:

Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,立即学习

如何分析音频回调

【中文标题】如何分析音频回调【英文标题】:How to profile audio callback 【发布时间】:2014-01-27 09:04:55 【问题描述】:

我正在写一个音乐播放器。我已经做了很多工作来提高我的音频回调的性能:

所有解码等当然是在单独的线程中完成的。这会填满一些缓冲区。

为了避免任何锁定,我避免了任何互斥体,并仅基于原子操作对所有相关结构进行编码。它基本上是一个无锁 FIFO。

我尝试通过在所有分配的内存上使用mlock 来避免页面错误。

我通过thread_policy_set(类似于here)将我的线程设置为实时约束。

有时仍然会发生下溢。我想知道如何调试它,因为我想知道是什么导致了它们。

我正在考虑一种方法来跟踪音频回调的当前执行,如果它花费的时间超过 2 毫秒左右。但是我该怎么做呢?

另外,它可能仍会读取一些内存,从而导致页面错误。我该如何调试这些?

回调中的所有代码仍然有些复杂。也许只是太复杂了。我可以通过引入另一个间接来解决这个问题,并通过仅使用一个简单的环形缓冲区使代码真正最小化。这会带来更多的延迟,我不确定这是否真的是问题所在。

【问题讨论】:

你看过 Instruments 吗? Time Profiler & System Trace(显示线程调度)应该会给你一些洞察力。 您的目标缓冲区大小/采样率是多少?即您在回调中总共有多少时间?只是问一下,因为我们有软件可以在 @RhythmicFistman:我这样做了,但并没有太大帮助,因为它们只显示平均数字而且看起来不错。下溢很少发生。我需要以某种方式找到一种方法来仅在需要太长时间时才捕获跟踪。 @stijn:即使在 48kHz 时也会发生这种情况,但理想情况下我希望它也支持 96kHz 或 192kHz。我尝试了几种缓冲区大小,但没有太大帮助。但也许这也是我的中间层 PortAudio 的问题。当我让操作系统自动选择缓冲区大小时,我得到了最好的结果。 作为第一步,然后尝试确定是否是您的代码而不是导致问题的任何底层层?找出代码在回调中花费的最小/最大时间量。如果它从未接近最大可能数量,那么您的代码不是罪魁祸首。 【参考方案1】:

我会尝试的是,如果我有一个应该在 2ms 内完成的程序,我会在进入程序时设置一个 2ms 的闹钟中断,并在退出程序时清除它。 即使加班很少发生,这也一定能搞定。

所以当中断发生时,我可以在调试器中捕获它并检查堆栈。 这会在做需要额外时间的事情时抓住它吗? 也许,也许不是,但多做几次,一定会发现一些有趣的事情。

我要做的另一件事就是在回调本身中寻找加速。 为此,我会在它运行时随机手动暂停它多次,并且每次都检查堆栈。 我会简单地忽略回调不在堆栈上的任何样本。 对于剩余的样本,回调在堆栈上,它将位于回调状态序列中的随机位置,因此很有可能如果它正在做任何优化可以节省大量时间的事情,我会看到它在做它。

【讨论】:

我认为即使是回调中的系统调用也被认为是坏的,因为它们可能会阻塞,但对于调试来说,这可能还可以。 :) @Albert:如果回调中有阻塞的系统调用,那么暂停会找到它们,如果它们占用了大量时间。如果他们不这样做,那么就没有必要担心他们。顺便说一句,我忘了提到在回调周围放置一个长循环的想法,这样你的停顿肯定会落在其中。然后在你加速之后,去掉循环,它就会飞起来。

以上是关于audio_thread音频解码线程分析的主要内容,如果未能解决你的问题,请参考以下文章

如何分析音频回调

音视频同步

AAC音频格式分析

实时音频编解码之十三 Opus编码-SILK编码-噪声整形分析

Android音频开发(三)——音频编解码

实时音频编解码之十三 Opus编码-SILK编码-噪声整形分析