整章目录:Android------- IjkPlayer 源码学习目录

本篇会有很多源代码,请注意阅读每行代码上面的注释。

本篇介绍的主要内容为上图红框圈起部分:

在前面介绍了如何将一个AvPacket解码为一帧数据,并存入缓存队列中。对于视频帧而言,会有video_refresh_thread线程从缓存队列中取数据,然后渲染到界面上。而缓存队列中的音频数据是由谁取出的????又是如何使用的呢???本篇将聊聊音频数据是如何播放的。

如果你还不知道如何解码,请看:Android --- IjkPlayer 阅读native层源码之如何将AvPacket数据解码出一帧数据(六)

如果你还不知道stream_component_open:请看:Android ---- Ijkplayer阅读native层源码之IjkMediaPlayer_prepareAsync(五)


在stream_component_open中,启动音频解码线程audio_thread的同时,也调用了audio_open函数,去创建音频播放器并开启aout_thread线程(用于处理缓存队列中的帧数据)。下面我们将从audio_open函数开始介绍:

audio_open:

/**** @param opaque FFPlayer* @param wanted_channel_layout----期望音频存储顺序 eg,AV_CH_LAYOUT_STEREO* @param wanted_nb_channels----期望声道数,* @param wanted_sample_rate----期望采样率,* @param audio_hw_params----记录最终播放的音频参数对象* @return*/
static int audio_open(FFPlayer *opaque, int64_t wanted_channel_layout, int wanted_nb_channels, int wanted_sample_rate, struct AudioParams *audio_hw_params)
{// 省略。。。。。。// 期望的音频参数wanted_spec.channels = wanted_nb_channels;wanted_spec.freq = wanted_sample_rate;wanted_spec.format = AUDIO_S16SYS;//2bytewanted_spec.silence = 0;wanted_spec.samples = FFMAX(SDL_AUDIO_MIN_BUFFER_SIZE, 2 << av_log2(wanted_spec.freq / SDL_AoutGetAudioPerSecondCallBacks(ffp->aout)));// 注意这个回调函:将需要播放的音频发送给播放器wanted_spec.callback = sdl_audio_callback;wanted_spec.userdata = opaque;// 创建Java层的AudioTrack,// 开启处理队列中音频的线程// 最终播方的音频参数存放在spec中while (SDL_AoutOpenAudio(ffp->aout, &wanted_spec, &spec) < 0) {//如果创建失败,进入改变期望,然后继续创建}// 记录播放的音频参数audio_hw_params->fmt = AV_SAMPLE_FMT_S16;audio_hw_params->freq = spec.freq;audio_hw_params->channel_layout = wanted_channel_layout;audio_hw_params->channels =  spec.channels;audio_hw_params->frame_size = av_samples_get_buffer_size(NULL, audio_hw_params->channels, 1, audio_hw_params->fmt, 1);audio_hw_params->bytes_per_sec = av_samples_get_buffer_size(NULL, audio_hw_params->channels, audio_hw_params->freq, audio_hw_params->fmt, 1);}

上面代码不难理解,准备一些用于创建音频播放器的参数,存储在wanted_spec结构体中。其中的channel_layout、channels、sample_rate三个参数来自于AVCodecContext中。然后将wanted_spec传入SDL_AoutOpenAudio函数,用于创建音频播放器。默认使用的音频格式为AUDIO_S16SYS(AV_SAMPLE_FMT_S16)

SDL_AoutOpenAudio:

这里需要多说两句,对于Android而言:

  1. IjkPlayer 默认使用的播放器是Android的AudioTrack.java,所以open_audio指向的是ijksdl_aout_android_audiotrack.c中aout_open_audio函数;
  2. 但是IjkPlayer还提供了另一种播放器:OpenGLES;如果用户想使用该播放器,需要在Java层设置下面的选项:

    然后系统会构建不同的播放环境,下图:ffpipeline_android.c中的函数

OK,下面以IjkPlayer的默认播放器AudioTrack为例接着分析

aout_open_audio:

aout_open_audio_n:

/**** @param env* @param aout* @param desired 期望的音频参数* @param obtained 创建android Java层的AudioTrack时最终创建音轨时传入的音频参数* @return*/
static int aout_open_audio_n(JNIEnv *env, SDL_Aout *aout, const SDL_AudioSpec *desired, SDL_AudioSpec *obtained)
{// 传入的音频参数opaque->spec = *desired;//创建Java层的AudioTrack对象----- new AudioTrack(...);opaque->atrack = SDL_Android_AudioTrack_new_from_sdl_spec(env, desired);// 获得音频轨所需的最小缓冲区大小----调用AudioTrack.getMinBufferSize(...);opaque->buffer_size = SDL_Android_AudioTrack_get_min_buffer_size(opaque->atrack);// 将音频参数记录到opaque->atrack中if (obtained) {SDL_Android_AudioTrack_get_target_spec(opaque->atrack, obtained);}// 开启线程:将缓存队列中的音频数据发送到播放器中opaque->audio_tid = SDL_CreateThreadEx(&opaque->_audio_tid, aout_thread, aout, "ff_aout_android");return 0;
}

上面代码:一个正常的创建AudioTrack对象的流程。需要注意的是:这里开启了一个新的线程aout_thread,用来发送缓存队列中的音频到播放器。

aout_thread:

static int aout_thread_n(JNIEnv *env, SDL_Aout *aout)
{// 省略。。。。// 每次向音频发送的字节数int copy_size = 256;// 设置线程优先级为高级SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH);// 判断是否退出或者暂停if (!opaque->abort_request && !opaque->pause_on)SDL_Android_AudioTrack_play(env, atrack); // 调用AudiaTrack.play()// 注意:这是一个无限循环while (!opaque->abort_request) {SDL_LockMutex(opaque->wakeup_mutex);// 如果暂停if (!opaque->abort_request && opaque->pause_on) {//调用Java层暂停方法SDL_Android_AudioTrack_pause(env, atrack);// 循环等待while (!opaque->abort_request && opaque->pause_on) {//等待1sSDL_CondWaitTimeout(opaque->wakeup_cond, opaque->wakeup_mutex, 1000);}//判断是否退出或者暂停if (!opaque->abort_request && !opaque->pause_on) {if (opaque->need_flush) {opaque->need_flush = 0;//调用AudiaTrack.flush()SDL_Android_AudioTrack_flush(env, atrack);}// 重新播放音频----调用Android的AudioTrack.play()SDL_Android_AudioTrack_play(env, atrack);}}// 是否需要清空播放器中的缓存if (opaque->need_flush) {opaque->need_flush = 0;SDL_Android_AudioTrack_flush(env, atrack);}// 是否需要设置音量if (opaque->need_set_volume) {opaque->need_set_volume = 0;SDL_Android_AudioTrack_set_volume(env, atrack, opaque->left_volume, opaque->right_volume);}// 是否需要改变播放速度if (opaque->speed_changed) {opaque->speed_changed = 0;SDL_Android_AudioTrack_setSpeed(env, atrack, opaque->speed);}SDL_UnlockMutex(opaque->wakeup_mutex);// 注意这里:拷贝一些音频数据,最终调用ff_fflay.sdl_audio_callbackaudio_cblk(userdata, buffer, copy_size);if (opaque->need_flush) {SDL_Android_AudioTrack_flush(env, atrack);opaque->need_flush = false;}// 是否需要清空播放器中的缓存if (opaque->need_flush) {opaque->need_flush = 0;SDL_Android_AudioTrack_flush(env, atrack);} else {// 向Android的AudioTrack写音频数据,注意:每次只发送copy_size个字节的音频数据int written = SDL_Android_AudioTrack_write(env, atrack, buffer, copy_size);}}SDL_Android_AudioTrack_free(env, atrack);return 0;
}

上面代码循环的做着下面三件事:

  1. 控制音频播放器的状态,eg,暂停、播放速度
  2. audio_cblk:将需要播放的音频数据拷贝到buffer变量中-------(work2)
  3. SDL_Android_AudioTrack_write:将work2返回的音频数据(buffer)发送给播放器 -------(work3)

先介绍简单的work3:将work2返回的音频数据(buffer)发送给播放器;

SDL_Android_AudioTrack_write:

比较简单,调用AudioTrack.write方法:将音频发送给播放器。


work2:

audio_cblk:实际是调用ff_ffplay.c中的sdl_audio_callback函数:

static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)
{省略。。。。// 清空临时缓存if (!ffp || !is) {memset(stream, 0, len);return;}// 获得当前时间ffp->audio_callback_time = av_gettime_relative();// 判端用户是否改变了播放器的播放速率if (ffp->pf_playback_rate_changed) {ffp->pf_playback_rate_changed = 0;// 如果没有使用第三方soundtouch框架处理音频数据,则进入if (!ffp->soundtouch_enable) {// 使用Android audioTrack自带的方法改变播放器的播放速率SDL_AoutSetPlaybackRate(ffp->aout, ffp->pf_playback_rate);}}// 判断用户是否改变播放器单声道的声音,Android ijkplayer默认不支持单声道音量设置,需要自己实现pf_playback_volumeif (ffp->pf_playback_volume_changed) {ffp->pf_playback_volume_changed = 0;SDL_AoutSetPlaybackVolume(ffp->aout, ffp->pf_playback_volume);}// 将一帧音频推送到Android的AudioTrack,然后再处理下一帧。while (len > 0) {int t1=is->audio_buf_index;int t2=is->audio_buf_size;// 向音频缓存队列中获取一帧音频,然后向Android的AudioTrack发送一帧,然后再获取,以此循环// 判断一帧音频是否发送完成,完成就进入if (is->audio_buf_index >= is->audio_buf_size) {// ----------------------------------------------------------// 重点:获取一帧音频数据(可能会被重采样),存放在is->audio_buf中// ----------------------------------------------------------audio_size = audio_decode_frame(ffp);}// 一帧中还没发送到播放器中的剩余数据len1 = is->audio_buf_size - is->audio_buf_index;// 限制拷贝长度最大为len,即每次最多拷贝len个字节if (len1 > len)len1 = len;// muted=0,audio_volume=SDL_MIX_MAXVOLUME,不用管,只需要管audio_buf是否存在// 如果有需要播放的音频数据,进入if (!is->muted && is->audio_buf && is->audio_volume == SDL_MIX_MAXVOLUME)//注意:将audio_buf拷贝到stream//注意:每次只拷贝copy_size字节,而不是一次全拷贝,循环几次将其拷贝完memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);else {// 清空streammemset(stream, 0, len1);if (!is->muted && is->audio_buf)SDL_MixAudio(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1, is->audio_volume);}len -= len1;stream += len1;is->audio_buf_index += len1;}//计算出该帧还剩下多少没有发送给播放器is->audio_write_buf_size = is->audio_buf_size - is->audio_buf_index;int lennnn=is->audio_buf_size - is->audio_buf_index;// 省略。。。
}

上面代码:通过audio_decode_frame拿到一帧需要播放的音频(可能会被重采样),拷贝len个字节的音频数据到stream容器中,并返回给上面的work3使用,然后清空stream容器。如果该帧大小大于len,那么一帧会重复多次拷贝动作(蓝色字描述的动作),直到将一帧全部拷贝完成,才会再调用audio_decode_frame获得下一帧。

audio_decode_frame:

static int audio_decode_frame(FFPlayer *ffp)
{// 省略。。。。do {// 从缓存队列中获得一帧音频数据if (!(af = frame_queue_peek_readable(&is->sampq)))return -1;// 删除上一帧,注意:af还在缓存队列中frame_queue_next(&is->sampq);} while (af->serial != is->audioq.serial);// 计算该帧的大小data_size = av_samples_get_buffer_size(NULL, af->frame->channels,af->frame->nb_samples,af->frame->format, 1);// ----------------------------------------------------------// 重点:获得每个通道期望输出的sample数量,如果使用的是音频去同步视频,这个方法就是计算音频与视频同步的误差值。默认使用的是视频同步音频// ----------------------------------------------------------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)) {AVDictionary *swr_opts = NULL;//释放swr_free(&is->swr_ctx);//如果第一个参数为NULL则创建一个新的SwrContex--重采样上下文// 设置重采样参数is->swr_ctx = swr_alloc_set_opts(NULL,is->audio_tgt.channel_layout, is->audio_tgt.fmt, is->audio_tgt.freq,dec_channel_layout,af->frame->format, af->frame->sample_rate,0, NULL);// 拷贝选项字典av_dict_copy(&swr_opts, ffp->swr_opts, 0);if (af->frame->channel_layout == AV_CH_LAYOUT_5POINT1_BACK)//2 Bits 中心混音电平av_opt_set_double(is->swr_ctx, "center_mix_level", ffp->preset_5_1_center_mix_level, 0);// 设置选项av_opt_set_dict(is->swr_ctx, &swr_opts);av_dict_free(&swr_opts);// 设置用户参数后初始化上下文if (swr_init(is->swr_ctx) < 0) {//失败return -1;}// 记录当前音频数据的参数,如果后面的音频数据的参数和当前的一样,就可以使用同一个重采样上下文is->audio_src.channel_layout = dec_channel_layout;is->audio_src.channels       = af->frame->channels;is->audio_src.freq = af->frame->sample_rate;is->audio_src.fmt = af->frame->format;}// 如果需要重采样if (is->swr_ctx) {// 默认不会用到,采样样本与期望样本不相等,即改变采样sample的数量,改变了一帧的播放时长if (wanted_nb_samples != af->frame->nb_samples) {// 激活重采样补偿(“软”补偿)// 每个样本的PTS值---(wanted_nb_samples - af->frame->nb_samples) * is->audio_tgt.freq / af->frame->sample_rate// 补偿-要补偿的样本的距离数--- wanted_nb_samples * is->audio_tgt.freq / af->frame->sample_rate// 重采样,利用重采样库进行样本的插入或剔除  wanted_nb_samples - af->frame->nb_samples>0表示增加,反之减少if (swr_set_compensation(is->swr_ctx, (wanted_nb_samples - af->frame->nb_samples) * is->audio_tgt.freq / af->frame->sample_rate,wanted_nb_samples * is->audio_tgt.freq / af->frame->sample_rate) < 0) {return -1;}}// 将audio_buf1的大小由audio_buf1_size变为out_sizeav_fast_malloc(&is->audio_buf1, &is->audio_buf1_size, out_size);// 转换音频,返回每个通道的输出样本数。len2 = swr_convert(is->swr_ctx, out, out_count, in, af->frame->nb_samples);// 音频缓冲区可能太小了,重新初始化重采样上下文if (len2 == out_count) {if (swr_init(is->swr_ctx) < 0)swr_free(&is->swr_ctx);}is->audio_buf = is->audio_buf1;// 返回每个样本sample的字节数int bytes_per_sample = av_get_bytes_per_sample(is->audio_tgt.fmt);// 所有通道采集的字节总数,即该帧音频大小resampled_data_size = len2 * is->audio_tgt.channels * bytes_per_sample;
#if defined(__ANDROID__)// 如果使用了soundtouch,且设置的播放速度不等于1.0fif (ffp->soundtouch_enable && ffp->pf_playback_rate != 1.0f && !is->abort_request) {av_fast_malloc(&is->audio_new_buf, &is->audio_new_buf_size, out_size * translate_time);// 因为FFmpeg解码后的数据存放在类型为uint8(无符号8bit)的缓存中,且其存储格式为AV_SAMPLE_FMT_S16-----小端存储// 又因为SoudTouch最低支持16bit的采样位数.// 所以要将其转化为16bit的小端存储。(为啥还是小端,而不是大端,因为其原本就是小端存储存储的,我们只需要将其由8bit小端转化为16bit小端,不要画蛇添足)// 于是就有了下面的for循环了。for (int i = 0; i < (resampled_data_size / 2); i++){// 按小端存储方式由8bit转换为16bitis->audio_new_buf[i] = (is->audio_buf1[i * 2] | (is->audio_buf1[i * 2 + 1] << 8));}// 使用Soudtouch将音频播放速度变快。int ret_len = ijk_soundtouch_translate(is->handle, is->audio_new_buf, (float)(ffp->pf_playback_rate), (float)(1.0f/ffp->pf_playback_rate),resampled_data_size / 2, bytes_per_sample, is->audio_tgt.channels, af->frame->sample_rate);if (ret_len > 0) {is->audio_buf = (uint8_t*)is->audio_new_buf;resampled_data_size = ret_len;} else {translate_time++;goto reload;}}
#endif} else {is->audio_buf = af->frame->data[0];resampled_data_size = data_size;}if (!isnan(af->pts))// 该帧显示时间+该帧采集时间(也是该帧播放完成时间)is->audio_clock = af->pts + (double) af->frame->nb_samples / af->frame->sample_rate;elseis->audio_clock = NAN;return resampled_data_size;
}

上面代码算是音频的核心代码:

  • frame_queue_peek_readable:从缓存队列中获得一帧数据;
  • synchronize_audio:
             根据音频和视频播放时间的误差,计算一个合适的播放量wanted_nb_samples,用来改变音频的播放速度,从而达到音频同步视频的目的。注意:IjkPlayer默认是AV_SYNC_AUDIO_MASTER,即视频同步音频。如果想使用音频同步视频需要在ff_ffplay_def.h中的 函数中设置ffp->av_sync_type 为AV_SYNC_VIDEO_MASTER。如果你想理解音频如何同步视频的,请看:Android --- IjkPlayer 的核心:音视频同步原理(十)
  • swr_set_compensation:
             设置重采样时每一帧的sample数量。如果设置了AV_SYNC_VIDEO_MASTER模式,这个函数对音频同步视频就很重要。因为一帧的播放时长=采样的sample数量 /  采样率。所以该函数会改变一帧的播放时长,达到同步视频的目的
  • swr_convert: 
             转换音频,如果音频的sample rate(采样率)、sample format(采样格式)、channel layout(通道布局)、nb samples个数与播放器设置的参数不等,就需要进行音频转换
  • ijk_soundtouch_translate:

    使用第三放框架soundtouch,改变播放速率,eg,2倍快播。默认使用的是PlaybackParams.setSpeed()控制音频的播放速度。如果想使用soundtouch控制,需要设置该选项:

    然后调用IjkMediaPlayer.java的setSpeed方法设置播放速度


OK,介绍完毕。

Android --- IjkPlayer 阅读native层源码之解码成功后的音频数据如何发送回Android播放(九)相关推荐

  1. Android ---- Ijkplayer阅读native层源码之IjkMediaPlayer_prepareAsync(五)

    整章目录:Android------- IjkPlayer 源码学习目录 本篇会有很多源代码,请注意阅读每行代码上面的注释. 本篇介绍的主要内容为上图红框圈起部分: IjkMediaPlayer_pr ...

  2. android优化中国风应用、完整NBA客户端、动态积分效果、文件传输、小说阅读器等源码...

    Android精选源码 android拖拽下拉关闭效果源码 一款优雅的中国风Android App源码 EasySignSeekBar一个漂亮而强大的自定义view15 android仿蘑菇街,蜜芽宝 ...

  3. 基于Android的看小说APP源码Android本科毕业设计Android小说阅读器、小说APP源码

    基于kotlin + 协程 + MVVM 模式来编写的看小说APP. 完整代码下载地址:基于Android的看小说APP源码Android本科毕业设计Android小说阅读器.小说APP源码 主要框架 ...

  4. android新闻项目、饮食助手、下拉刷新、自定义View进度条、ReactNative阅读器等源码...

    Android精选源码 Android仿照36Kr官方新闻项目课程源码 一个优雅美观的下拉刷新布局,众多样式可选 安卓版本的VegaScroll滚动布局 android物流详情的弹框 健身饮食记录助手 ...

  5. Android四大组件之bindService源码实现详解

        Android四大组件之bindService源码实现详解 Android四大组件源码实现详解系列博客目录: Android应用进程创建流程大揭秘 Android四大组件之bindServic ...

  6. 带着问题分析Framework层源码(一):按键音声音太小,我们该如何增大?

    作为一名Android开发人员,对源码的阅读是必不可少的.但是Android源码那么庞大,从何开始阅读,如何开始阅读,很多人都会感觉无从下手,今天我来带着问题,去带大家分析一下Android源码,并解 ...

  7. Android Input子系统-含实例源码

    Android Input子系统-含实例源码 1 Input子系统作用 Android很多外设都是用到输入输出设备,比如touchscreen,键盘,音量键等,输入 设备对应Android 框架是An ...

  8. Android Q 10.1 KeyMaster源码分析(二) - 各家方案的实现

    写在之前 这两篇文章是我2021年3月初看KeyMaster的笔记,本来打算等分析完KeyMaster和KeyStore以后再一起做成一系列贴出来,后来KeyStore的分析中断了,这一系列的文章就变 ...

  9. rust墙壁升级点什么_分享:如何在阅读Rust项目源码中学习

    今天做了一个Substrate相关的小分享,公开出来. 因为我平时也比较忙,昨天才选定了本次分享的主题,准备比较仓促,细节可能不是很充足,但分享的目的也是给大家提供一个学习的思路,更多的细节大家可以在 ...

最新文章

  1. 使用 html 标签嵌入Silverlight程序的一点小问题
  2. Linux查看文件大小的几种方法
  3. python random.choice报错_如何解决mtrand.RandomState.choice中的内存错误...
  4. redis 分布式锁的 5个坑,真是又大又深
  5. APP导航菜单系列Axure模板原型
  6. 2019年度十大网络小说:玄幻小说独占六部,都市小说一本超神
  7. 一篇文章看明白 Activity 与 Window 与 View 之间的关系
  8. 依行科技日常实习面经
  9. 汇编语言程序设计实验——字符统计
  10. excel对同一个单元格中的内容去重
  11. 循环控制语句break,continue
  12. mysql取三个数据类型_MySQL(三)数据类型
  13. 数据库系统概论第三单元基础知识(一)
  14. RFID在身份证中的应用
  15. 第六届中国软件质量年会邀请函
  16. Windows系统 Docker 相关命令报错
  17. DEM、TIN与栅格之间的关系
  18. 思科数据中心网络会议 PPT下载
  19. html使用mysql数据库数据类型_MySQL数据库常见的数据类型
  20. python 获取列名_python获取Pandas列名的几种方法

热门文章

  1. 安装VMware虚拟机后,网络适配器找不到VMnet8和VMnet1解决方法。
  2. 工程力学(17)—应力状态和强度理论
  3. 编码器基础知识大扫盲
  4. 计算机网络到底讲了些什么
  5. JavaScript 进阶篇的学习~
  6. 如何设置计算机自动连接宽带,宽带自动连接设置,小编教你电脑怎么设置宽带自动连接...
  7. Ant Design of Vue +TS 表单动态增加数据验证卧坑姿势
  8. 每日获取强智教务系统课表,并发送短信到学生手机!爬虫真牛逼!
  9. 一键复制吱口令,支付宝红包js代码
  10. windows 10 超级优化提速 附系统服务列表纯净