ffplay audio输出线程分析

转自:https://zhuanlan.zhihu.com/p/44139512

ffplay的audio输出同样也是通过SDL实现的。

同样地,本文主要介绍audio输出相关内容,且尽量不涉及音视频同步知识,音视频同步将在专门一篇分析。

audio的输出在SDL下是被动输出,即在开启SDL会在需要输出时,回调通知,在回调函数中,SDL会告知要发送多少的数据。(关于SDL音频输出可以参考这篇:https://www.jianshu.com/p/b006e9e9caa6

但是在ffmpeg解码一个AVPacket的音频到AVFrame后,在AVFrame中存储的音频数据大小与SDL回调所需要的数据大概率是不相等的,这就需要再增加一级缓冲区解决问题。

在audio输出时,主要模型如下图:

 

在这个模型中,sdl通过sdl_audio_callback函数向ffplay要音频数据,ffplay将sampq中的数据通过audio_decode_frame函数取出,放入is->audio_buf,然后送出给sdl。在后续回调时先找audio_buf要数据,数据不足的情况下,再调用audio_decode_frame补充audio_buf

注意 audio_decode_frame这个函数名很具有迷惑性,实际上,这个函数是没有解码功能的!这个函数主要是处理sampq到audio_buf的过程,最多只是执行了重采样。

了解了大致的audio输出模型后,再看详细代码。

先看打开sdl音频输出的代码:

//代码在stream_component_open
//先不看filter相关代码,即默认CONFIG_AVFILTER宏为0

//从avctx(即AVCodecContext)中获取音频格式参数
sample_rate    = avctx->sample_rate;
nb_channels    = avctx->channels;
channel_layout = avctx->channel_layout;

//调用audio_open打开sdl音频输出,实际打开的设备参数保存在audio_tgt,返回值表示输出设备的缓冲区大小
if ((ret = audio_open(is, channel_layout, nb_channels, sample_rate, &is->audio_tgt)) < 0)
    goto fail;
is->audio_hw_buf_size = ret;
is->audio_src = is->audio_tgt;

//初始化audio_buf相关参数
is->audio_buf_size  = 0;
is->audio_buf_index = 0;

由于不同的音频输出设备支持的参数不同,音轨的参数不一定能被输出设备支持(此时就需要重采样了),audio_tgt就保存了输出设备参数。

audio_open是ffplay封装的函数,会优先尝试请求参数能否打开输出设备,尝试失败后会自动查找最佳的参数重新尝试。不再具体分析。

audio_src一开始与audio_tgt是一样的,如果输出设备支持音轨参数,那么audio_src可以一直保持与audio_tgt一致,否则将在后面代码中自动修正为音轨参数,并引入重采样机制。

最后初始化了几个audio_buf相关的参数。这里介绍下audio_buf相关的几个变量:

  • audio_buf: 从要输出的AVFrame中取出的音频数据(PCM),如果有必要,则对该数据重采样。
  • audio_buf_size: audio_buf的总大小
  • audio_buf_index: 下一次可读的audio_buf的index位置。
  • audio_write_buf_size:audio_buf已经输出的大小,即audio_buf_size - audio_buf_index

 

audio_open函数内,通过通过SDL_OpenAudioDevice注册sdl_audio_callback函数为音频输出的回调函数。那么,主要的音频输出的逻辑就在sdl_audio_callback函数内了。

static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)
{
    VideoState *is = opaque;
    int audio_size, len1;

    audio_callback_time = av_gettime_relative();

    while (len > 0) {//循环发送,直到发够所需数据长度
        //如果audio_buf消耗完了,就调用audio_decode_frame重新填充audio_buf
        if (is->audio_buf_index >= is->audio_buf_size) {
           audio_size = audio_decode_frame(is);
           if (audio_size < 0) {
                /* if error, just output silence */
               is->audio_buf = NULL;
               is->audio_buf_size = SDL_AUDIO_MIN_BUFFER_SIZE / is->audio_tgt.frame_size * is->audio_tgt.frame_size;
           } else {
               if (is->show_mode != SHOW_MODE_VIDEO)
                   update_sample_display(is, (int16_t *)is->audio_buf, audio_size);
               is->audio_buf_size = audio_size;
           }
           is->audio_buf_index = 0;
        }

        //根据缓冲区剩余大小量力而行
        len1 = is->audio_buf_size - is->audio_buf_index;
        if (len1 > len)
            len1 = len;
        //根据audio_volume决定如何输出audio_buf
        if (!is->muted && is->audio_buf && is->audio_volume == SDL_MIX_MAXVOLUME)
            memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);
        else {
            memset(stream, 0, len1);
            if (!is->muted && is->audio_buf)
                SDL_MixAudioFormat(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, AUDIO_S16SYS, len1, is->audio_volume);
        }
        //调整各buffer
        len -= len1;
        stream += len1;
        is->audio_buf_index += len1;
    }

    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)) {//更新audclk
        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);
    }
}

sdl_audio_callback函数是一个典型的缓冲区输出过程,看代码和注释应该可以理解。具体看3个细节:

  • 输出audio_buf到stream,如果audio_volume为最大音量,则只需memcpy复制给stream即可。否则,可以利用SDL_MixAudioFormat进行音量调整和混音
  • 如果audio_buf消耗完了,就调用audio_decode_frame重新填充audio_buf。接下来会继续分析audio_decode_frame函数
  • set_clock_at更新audclk时,audio_clock是当前audio_buf的显示结束时间(pts+duration),由于audio driver本身会持有一小块缓冲区,典型地,会是两块交替使用,所以有2 * is->audio_hw_buf_size.(这里为何还要加audio_write_buf_size,表示不能理解。有理解的希望能赐教)

 

接下来看下audio_decode_frame(省略重采样代码):

static int audio_decode_frame(VideoState *is)
{
    int data_size, resampled_data_size;
    int64_t dec_channel_layout;
    av_unused double audio_clock0;
    int wanted_nb_samples;
    Frame *af;

    if (is->paused)//暂停状态,返回-1,sdl_audio_callback会处理为输出静音
        return -1;

    do {//1. 从sampq取一帧,必要时丢帧
        if (!(af = frame_queue_peek_readable(&is->sampq)))
            return -1;
        frame_queue_next(&is->sampq);
    } while (af->serial != is->audioq.serial);

    //2. 计算这一帧的字节数
    data_size = av_samples_get_buffer_size(NULL, af->frame->channels,
                                           af->frame->nb_samples,
                                           af->frame->format, 1);

    //[]计算dec_channel_layout,用于确认是否需要重新初始化重采样(难道af->channel_layout不可靠?不理解)
    dec_channel_layout =
        (af->frame->channel_layout && af->frame->channels == av_get_channel_layout_nb_channels(af->frame->channel_layout)) ?
        af->frame->channel_layout : av_get_default_channel_layout(af->frame->channels);
    wanted_nb_samples = synchronize_audio(is, af->frame->nb_samples);

    //[]判断是否需要重新初始化重采样
    if (af->frame->format        != is->audio_src.fmt            ||
        dec_channel_layout       != is->audio_src.channel_layout ||
        af->frame->sample_rate   != is->audio_src.freq           ||
        (wanted_nb_samples       != af->frame->nb_samples && !is->swr_ctx)) {
        //……
    }

    //3. 获取这一帧的数据
    if (is->swr_ctx) {//[]如果初始化了重采样,则对这一帧数据重采样输出
    }else {
        is->audio_buf = af->frame->data[0];
        resampled_data_size = data_size;
    }

    audio_clock0 = is->audio_clock;//audio_clock0用于打印调试信息

    //4. 更新audio_clock,audio_clock_serial
    /* update the audio clock with the pts */
    if (!isnan(af->pts))
        is->audio_clock = af->pts + (double) af->frame->nb_samples / af->frame->sample_rate;
    else
        is->audio_clock = NAN;
    is->audio_clock_serial = af->serial;

    return resampled_data_size;//返回audio_buf的数据大小
}

audio_decode_frame并没有真正意义上的decode代码,最多是进行了重采样。主流程有以下步骤:

  1. 从sampq取一帧,必要时丢帧。如发生了seek,此时serial会不连续,就需要丢帧处理
  2. 计算这一帧的字节数。通过av_samples_get_buffer_size可以方便计算出结果
  3. 获取这一帧的数据。对于frame格式和输出设备不同的,需要重采样;如果格式相同,则直接拷贝指针输出即可。总之,需要在audio_buf中保存与输出设备格式相同的音频数据
  4. 更新audio_clock,audio_clock_serial。用于设置audclk.

在省略了重采样代码后看,相对容易理解。

至此,音频输出的主要代码就分析完了。中间我们省略了filter和resample相关的代码,有研究后再补充。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值