audio_open函数分析

audio_open() 的作用,就如同它的名字那样,就是打开音频设备。流程图如下:

1-1


SDL 库播放音频数据有两种方式。

1,调用层定时往 SDL 接口塞数据。

2,设置SDL回调函数,让 SDL 来主动执行回调函数来取数据。

第二种方式的实时性更好,ffplay 也是用的第二种。


先介绍一下 audio_open() 函数的各个参数,如下:

static int audio_open(void *opaque, int64_t wanted_channel_layout, int wanted_nb_channels, int wanted_sample_rate, struct AudioParams *audio_hw_params)

1,void *opaque,传递给 SDL 回调函数的参数。

2,wanted_channel_layoutwanted_nb_channelswanted_sample_rate,希望用 这样的采样率,声道数,声道布局打开音频硬件设备。

3,struct AudioParams *audio_hw_params,实际打开的音频硬件设备的音频格式信息。这里的 hw 是 Hardware 的意思,也就是硬件,不过不是指硬件编解码加速,而是指打开的硬件设备。

这些参数是这样的,想要的音频格式实际打开的音频格式 不一定一样,例如,MP4 里面的音频流是 48000 采样的,肯定想要用 48000 的格式打开音响设备,这样才能听到最好的音质。但是难免有些音响设备太差,只支持 44100。


下面来分析一下 audio_open() 函数的重点代码,如下:

1-2

第一个重点就是这两个数组变量,next_nb_channels[]next_sample_rates[],这两个变量会让人摸不着头脑,即便看了后面的 while 逻辑,也不太容易看懂他们的含义。

现在我直接剧透这量个变量的作用。首先是 next_nb_channels[],这 其实是一个map表,声道切换映射表。举个例子,如果音响设备不支持 7 声道的数据播放,肯定不能直接报错,还要尝试一下其他声道能不能成功打开设备吧。这个其他声道就是 next_nb_channels[]

  • next_nb_channels[7] = 6,从7声道切换到6声道打开音频设备
  • next_nb_channels[6] = 4,从6声道切换到4声道打开音频设备
  • next_nb_channels[5] = 6,从5声道切换到6声道打开音频设备
  • next_nb_channels[4] = 2,从4声道切换到2声道打开音频设备
  • next_nb_channels[3] = 6,从3声道切换到6声道打开音频设备
  • next_nb_channels[2] = 1,从双声道切换到单声道打开音频设备
  • next_nb_channels[1] = 0,单声道都打不开音频设备,无法再切换,需要降低采样率播放。
  • next_nb_channels[0] = 0,0声道都打不开音频设备,无法再切换,需要降低采样率播放。

next_sample_rates[] 变量存储的仅仅是采样率,当切换所有声道都无法成功打开音频设备,就需要从 next_sample_rates[] 取一个比当前更小的采样率来尝试。

所以可以看到 next_sample_rates[] 的元素都是从小到大的。


audio_open() 函数的第二个重点是 声道数,声道布局等参数的校验,如下:

1-3

ffplay 经常会校验 声道布局 跟 声道数是否一致,可能是担心用户在命令行输入错误的参数。

此外还需注意一下 next_sample_rate_idx 变量,这个变量存的是比 想要打开的音频采样率 更小一点的采样率的索引。

while (next_sample_rate_idx && next_sample_rates[next_sample_rate_idx] >= wanted_spec.freq)
        next_sample_rate_idx--;

audio_open() 函数的第三个重点是 设置打开参数,设置回调函数 ,如下:

1-4

可以看到,ffpaly写死了采样格式,写死成了 AUDIO_S16SYS 格式。也就是说,无论MP4里面的音频采样格式是 64 还是 32 位,还是其他,统统都会先提前转成 AUDIO_S16SYS,然后再丢给 SDL 播放。

另外,wanted_spec.samples 的作用是告诉 SDL 每次回调取多少样本数来播放,实际上就是控制 SDL 每秒调用回调函数的次数。

因为播放采样率是固定的,例如 48000。也就是音频设备每秒要播放 48000 个样本,如果调一次 sdl_audio_callback 只能取4800个样本,那他一秒内就需要调 10 次 sdl_audio_callback

同理,只要固定了调用次数,也就能计算出 SDL 每次回调需要取多少样本数。而 ffplay 就固定了每秒回调 SDL_AUDIO_MAX_CALLBACKS_PER_SEC (30次)。

wanted_spec.samples 的计算方式有点难懂,如下:

wanted_spec.samples = FFMAX(SDL_AUDIO_MIN_BUFFER_SIZE, 2 << av_log2(wanted_spec.freq / SDL_AUDIO_MAX_CALLBACKS_PER_SEC));

首先,SDL_AUDIO_MIN_BUFFER_SIZE 宏是 512,所以最小值是 512。但是 后面的用 av_log2 取指数,又 << 位移,是干什么呢?

答:wanted_spec.freq 是采样率,而 SDL_AUDIO_MAX_CALLBACKS_PER_SEC 代表 SDL 每秒调多少次回调函数。两者相除就能得出每次回调需要读取多少个样本数。这个逻辑非常容易理解。

av_log2 取指数 跟 << 位移 只是想把 样本数数量 变成 2 的倍数。这是 SDL 文档建议的,推荐阅读《SDL_OpenAudioDevice》。

疑问:打开音频设备失败之后,会进行降低采样率播放操作,wanted_spec.freq 可能会变,但是 wanted_spec.samples 没有跟着变。我也不清楚会不会有问题。


audio_open() 函数的第四个重点是 用 while 循环不断尝试各种 声道 跟 采样的组合来打开音频硬件设备,如下:

1-5

比较难懂的就是 next_nb_channels[FFMIN(7, wanted_spec.channels)],大家可以假设一下 wanted_spec.channels 如果是 7, next_nb_channels[FFMIN(7, wanted_spec.channels)] 等于多少呢?,推算过程如下:

next_nb_channels[FFMIN(7, 7)]
next_nb_channels[7] = 6

可以看到,如果 7 声道打开失败,就会切换成 6 声道进行尝试,跟上面说的那个映射是一致的。

next_sample_rates[next_sample_rate_idx--] 就是进行降低采样率,再尝试打开音频设备,之前说过,next_sample_rate_idx 是比 想要的采样率更小一点的值。

注意他的那句日志 No more combinations to try, audio open failed,其实这句日志就说明了这段代码的逻辑,就是尝试各种组合


提醒:SDL_OpenAudioDevice() 函数打开音频设备之后,回调函数是不会立即执行的,需要调 SDL_PauseAudioDevice() 来启动音频设备,这样回调函数才会执行。

if ((ret = decoder_start(&is->auddec, audio_thread, "audio_decoder", is)) < 0)
    goto out;
SDL_PauseAudioDevice(audio_dev, 0);
break;

最后一个重点就是,打开硬件设备成功之后,要记录一下是以什么样的采样率,声道数等等格式打开硬件设备的。当 AVFrame 与 硬件音频格式不一致时候需要进行转换。

1-6

audio_hw_params 是指硬件设备参数的意思,我个人看到 hw 第一反应会以为是硬件编解码加速,其实不是。

硬件参数有两个字段需要特别讲解一下 :

1,audio_hw_params->frame_size,一个音频样本占多少内存。

2,audio_hw_params->bytes_per_secaudio_hw_params->freq 数量的音频样本占多少内存。也就是一秒钟要播放多少内存的音频数据。

这两个字段都是用av_samples_get_buffer_size() 来计算的。

注意 :audio_hw_params 实际上就是 is->audio_tgt ,只是在函数内部换了个名字而已。


至此,audio_open() 函数就讲解完毕了,但是还要注意一下这个函数的返回值,如下:

1-7

audio_open() 的返回值,最后是赋值给 is->audio_hw_buf_size 的,如下:

1-8

我们可以看一些,SDL 头文件对这个 spec.size 的定义,如下:

typedef struct SDL_AudioSpec
{
    int freq;                   /**< DSP frequency -- samples per second */
    SDL_AudioFormat format;     /**< Audio data format */
    Uint8 channels;             /**< Number of channels: 1 mono, 2 stereo */
    Uint8 silence;              /**< Audio buffer silence value (calculated) */
    Uint16 samples;             /**< Audio buffer size in sample FRAMES (total samples divided by channel count) */
    Uint16 padding;             /**< Necessary for some compile environments */
    Uint32 size;                /**< Audio buffer size in bytes (calculated) */
    SDL_AudioCallback callback; /**< Callback that feeds the audio device (NULL to use SDL_QueueAudio()). */
    void *userdata;             /**< Userdata passed to callback (ignored for NULL callbacks). */
} SDL_AudioSpec;

Audio buffer size in bytes 指的是 SDL 内部音频数据缓存的大小,代表 SDL线程执行 sdl_audio_callback() 的时候,SDL 硬件内部还有多少字节音频的数据没有播放。

没错,SDL线程并不是没有音频数据可以播放了才调 sdl_audio_callback() 来拿数据,而是他内部还剩 audio_hw_buf_size 长度的数据就会调 sdl_audio_callback() 来拿数据,是提前拿数据的。这个 概念 以及 audio_hw_buf_size 变量 都特别重要,后面会用到。


版权所属 xianwanzhiyin.net 罗上文 2022 all right reserved,powered by Gitbook该文件修订时间: 2022-09-17 22:31:42

results matching ""

    No results matching ""