sdl_audio_callback音频播放线程分析—ffplay.c源码分析

作者:罗上文,微信:Loken1,公众号:FFmpeg弦外之音

音频播放线程是之前在 audio_open() 函数里面创建的,实际上就是回调函数wanted_spec.callback)。当用 SDL 打开音频硬件设备的时候,SDL 库就会创建一个线程,来及时执行回调函数 sdl_audio_callback(),至于 SDL 线程多久回调一次函数,这个我们不需要太关心,只要调 SDL_OpenAudioDevice() 函数的时候设置好相关参数即可。如下:

1-1

上图中,设置了每次回调取的样本数,设置了样本数就相当于设置了回调次数,ffplay 默认是 1秒钟最多回调 30 次 sdl_audio_callback() 函数。


sdl_audio_callback() 函数的参数如下:

static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)

1,void *opaque,实际上就是之前设置的 wanted_spec.userdata,传递的是 VideoState *is 全局管理器。

2,Uint8 *stream,这个指针是 SDL 内部音频数据内存的指针,只要把数据拷贝到这个指针的地址,就能播放声音了。

3,int len,此次回调需要写多少字节的数据进去 stream 指针。

虽然 len 这个参数是 SDL 传递给我们的回调函数的,但是 len 其实是可以根据 wanted_spec的样本数,声道数,格式, 算出来的。

例如,ffplay 播放 juren-5s.mp4 的场景,命令如下:

ffplay -i juren-5s.mp4

在上面的命令中,wanted_spec.samples 会被赋值为 2048,也就是说每次回调,需要往 stream 指针写 2048 个采样。那2048个样本又是多少字节?

由于 SDL_OpenAudioDevice() 打开音频设备的时候写死了 16位格式 AUDIO_S16SYS ,所以一个采样是 2 个字节。然后因为 juren-5s.mp4 文件的音频是双声道的,每个声道都取2048个样本,那就是 2048 * 2 * 2 = 8192 字节。有兴趣可以打印一下 sdl_audio_callback() 里面的 len 变量,在本文命令下,一直都是 8192 字节。


sdl_audio_callback() 函数的流程图如下:

1-2

从上图的流程可以很容易看出, sdl_audio_callback() 函数干的事情,就是调 audio_decode_frame() 函数,把 is->audio_buf 指针指向要传输的音频数据。然后再调 SDL_MixAudioFormat() 把音频数据拷贝给 stream 指针指向的内存。这样 SDL 内部就会读 stream 指针指向的内存来播放。

虽然流程图画得比较简单,但是 sdl_audio_callback() 函数内部的代码逻辑是非常复杂的。下面我们一起来分析一下这里面的重点:

1-3

上图中的 while(len>0){...} 的逻辑简单概括就是从 FrameQueue 读取数据,不断写进入 SDL 的内存,直到写入了 len 大小的字节为止。

audio_decode_frame() 函数就是负责从 FrameQueue 读取 AVFrame,然后 把 is->audio_buf 指针指向 AVFramedata ,如果经过重采样, is->audio_buf 指针会指向重采样后的数据,也就是 is->audio_buf1

audio_decode_frame() 函数的内部逻辑是比较复杂的,本文不分析它的内部逻辑,只是给你讲一下它最后做到了什么事情,这也就是函数封装的意义。

audio_decode_frame() 函数最后做到的事情就是,把is->audio_buf 指针指向可以传输的音频数据,然后返回值代表可以传输的音频数据有多少字节。

至于这个音频数据是在哪个内存里面,我们外部调用不需要关心,这是它的内部实现。

如果 audio_decode_frame() 提取数据的时候发生错误。会返回 -1,然后把 audio_buf 指向 NULL 指针,这样会导致写入静音数据。


要理解上面的 while(len>0){...} 循环,有几个变量是必须要讲解一下的:

1,is->audio_buf,可以传输的音频数据的内存指针,指向内存的第一个字节。也就是开头。

2,is->audio_buf_size,可以传输的音频数据有多大,有多少个字节。

3,is->audio_buf_index,当前已经读取到第几个字节。

理解了这 3 个变量,就容易看懂上图中的 while 逻辑,就是不断拷贝数据到 SDL 的内存,

如果拷贝够了 len 字节,is->audio_buf 里面还有数据剩下来,就下一次回调来的时候继续拷贝剩下的数据。

如果当前 is->audio_buf 的数据不足以塞满 len 长度,就循环调 audio_decode_frame() 来提取数据,然后一直到 塞满 len 长度为止。


拷贝数据到 SDL 内存的时候有一个重点,会根据音量来调不同的函数,如下:

1-4

可以看到,如果直接 memcpy 拷贝数据,会是以最大音量进行拷贝。如果我们调整了音量,就会调 SDL_MixAudioFormat() 来拷贝数据。

因为 SDL_MixAudioFormat() 函数有调整音量的功能。


至此,sdl_audio_callback() 函数已经把 SDL 所需的音频数据全部拷贝给它了,就会跳出 while 循环,然后设置音频时钟。如下:

is->audio_write_buf_size = is->audio_buf_size - is->audio_buf_index;
    /* Let's assume the audio driver that is used by SDL has two periods. */
    if (!isnan(is->audio_clock)) {
        set_clock_at(&is->audclk,is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec,is->audio_clock_serial, audio_callback_time / 1000000.0);
        sync_clock_to_slave(&is->extclk, &is->audclk);
    }

上面的代码主要是设置音频的时钟,记录当前这一刻,音频流播放到哪里了。下面分析一下一些变量:

is->audio_write_buf_size 代表当前缓存里还剩多少数据没有拷贝给 SDL 。

is->audio_clock 这个变量是在 audio_decode_frame() 函数里面赋值的,因为 audio_decode_frame() 会从 FrameQueue 里面提取 一个AVFrame,而 is->audio_clock 记录的就是当这个 AVFrame 播放完之后,音频流所处的位置,也就是当 这个 AVFrame 播放完了,音频流 的 pts 是多少。。

理解了 is->audio_clock 就比较容易理解如何确定当前音频流播放到哪里了。

is->audio_clock 记录的是播放完那个 AVFrame 之后的 pts,但是此时此刻 只是把 这个 AVFrame 的内存数据拷贝给了 SDL,SDL 还没开始播放呢?

同时,SDL 还没播放的数据还有它内部的 audio_hw_buf_size 长度的数据,之前在《audio_open函数分析》说过:

SDL线程并不是没有音频数据可以播放了才调 sdl_audio_callback() 来拿数据,而是他内部还剩 audio_hw_buf_size 长度的数据就会调 sdl_audio_callback() 来拿数据,是提前拿数据的。

所以,SDL 内部还剩 audio_hw_buf_size 字节,现在又来取了 len 字节,同时我们的 audio_buf 缓存还剩 audio_write_buf_size字节。总共有 3 块内存等待播放,而这 3 块内存播放完之后的 pts 就是 is->audio_clock ,如下:

1-6

从上图可以看到,我们已经知道播放完这 3 块内存之后,音频流的位置,那就可以反向求音频流当前的播放时刻。而 len 实际上就是等于 audio_hw_buf_size,只是换了下名字,所以可以直接用 audio_hw_buf_size 乘以 2,所以就产生了下面的这句代码:

音频流当前的播放时刻 = is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec

这里要注意 is->audio_clock 的单位是秒。至此,就可以正确设置音频流当前播放到哪里了。

代码里有一句注释,也讲解了为什么要乘以 2。

/* Let's assume the audio driver that is used by SDL has two periods. */

它假设了 SDL 打开的设备里面是有两段缓存的,也就是 audio_hw_buf_size + len


最后的那句代码主要是用音频时钟来同步一下外部时钟,如下:。

sync_clock_to_slave(&is->extclk, &is->audclk);

时钟是用来记录当前的播放时刻的,方便做音视频同步,同步相关的分析,请看以下文章:

1,《音视频同步基础知识

2,《FFplay视频同步分析

3,《FFplay音频同步分析

4,《FFplay外部时钟分析



感谢 NETINT(镕铭微电子) 赞助《FFmpeg原理》免费版一书的服务器费用,下面是 VPU 产品介绍

版权所属 xianwanzhiyin.net 罗上文 2024 all right reserved,powered by Gitbook该文件修订时间: 2023-12-21 11:55:47

results matching ""

    No results matching ""