https://www.cnblogs.com/harlanc/p/9693983.html

目录

  • OpenSL ES & AudioTrack
  • 源码分析
    • 创建播放器音频输出对象
    • 配置并创建音频播放器
    • 音频数据的处理
    • 结束语
    • 参考

正文

一步步实现windows版ijkplayer系列文章之一——Windows10平台编译ffmpeg 4.0.2,生成ffplay
一步步实现windows版ijkplayer系列文章之二——Ijkplayer播放器源码分析之音视频输出——视频篇
一步步实现windows版ijkplayer系列文章之三——Ijkplayer播放器源码分析之音视频输出——音频篇
一步步实现windows版ijkplayer系列文章之四——windows下编译ijkplyer版ffmpeg
一步步实现windows版ijkplayer系列文章之五——使用automake一步步生成makefile
一步步实现windows版ijkplayer系列文章之六——SDL2源码分析之OpenGL ES在windows上的渲染过程
一步步实现windows版ijkplayer系列文章之七——终结篇(附源码)

一步步实现windows版ijkplayer系列文章之三——Ijkplayer播放器源码分析之音视频输出——音频篇

这篇文章的ijkplayer音频源码研究我们还是选择Android平台,它的音频解码是不支持硬解的,音频播放使用的API是OpenSL ES或AudioTrack。

回到顶部

OpenSL ES & AudioTrack

  • OpenSL ES

什么是OpenSL ES?下面来自官网的说明:

OpenSL ES™ is a royalty-free, cross-platform, hardware-accelerated audio API tuned for embedded systems. It provides a standardized, high-performance, low-latency method to access audio functionality for developers of native applications on embedded mobile multimedia devices, enabling straightforward cross-platform deployment of hardware and software audio capabilities, reducing implementation effort, and promoting the market for advanced audio.

可见OpenGL ES是专门为嵌入式设备设计的音频API,所以不适合在PC上使用。

  • AudioTrack

AudioTrack是专门为Android应用提供的java API,显然也不适合在PC上使用。

使用AudioTrack API来输出音频就需要把音频数据从java层拷贝到native层。而OpenSL ES API是Android NDK提供的native接口,它可以在native层直接获取和处理数据,因此为了提高效率,应该使用OpenSL ES API。通过如下java接口设置音频输出API:

  ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "opensles", 0);

Ijkplayer使用jni4android来为AudioTrack的java API自动生成JNI native代码。

我们尽量选择底层的代码来进行研究,因此本篇文章梳理一遍OpenSL ES API在ijkplayer中的使用。

回到顶部

源码分析

创建播放器音频输出对象

调用如下函数生成音频输出对象:

SDL_Aout *SDL_AoutAndroid_CreateForOpenSLES()

创建并初始化Audio Engine:

//创建
SLObjectItf slObject = NULL;
ret = slCreateEngine(&slObject, 0, NULL, 0, NULL, NULL);
CHECK_OPENSL_ERROR(ret, "%s: slCreateEngine() failed", __func__);
opaque->slObject = slObject;
//初始化
ret = (*slObject)->Realize(slObject, SL_BOOLEAN_FALSE);
CHECK_OPENSL_ERROR(ret, "%s: slObject->Realize() failed", __func__);
//获取SLEngine接口对象slEngine
SLEngineItf slEngine = NULL;
ret = (*slObject)->GetInterface(slObject, SL_IID_ENGINE, &slEngine);
CHECK_OPENSL_ERROR(ret, "%s: slObject->GetInterface() failed", __func__);
opaque->slEngine = slEngine;

打开音频输出设备:

//使用slEngine打开输出设备
SLObjectItf slOutputMixObject = NULL;
const SLInterfaceID ids1[] = {SL_IID_VOLUME};
const SLboolean req1[] = {SL_BOOLEAN_FALSE};
ret = (*slEngine)->CreateOutputMix(slEngine, &slOutputMixObject, 1, ids1, req1);
CHECK_OPENSL_ERROR(ret, "%s: slEngine->CreateOutputMix() failed", __func__);
opaque->slOutputMixObject = slOutputMixObject;
//初始化
ret = (*slOutputMixObject)->Realize(slOutputMixObject, SL_BOOLEAN_FALSE);
CHECK_OPENSL_ERROR(ret, "%s: slOutputMixObject->Realize() failed", __func__);

将上述创建的OpenSL ES相关对象保存到SDL_Aout_Opaque中。

设置播放器音频输出对象的回调函数:

aout->free_l       = aout_free_l;
aout->opaque_class = &g_opensles_class;
aout->open_audio   = aout_open_audio;
aout->pause_audio  = aout_pause_audio;
aout->flush_audio  = aout_flush_audio;
aout->close_audio  = aout_close_audio;
aout->set_volume   = aout_set_volume;
aout->func_get_latency_seconds = aout_get_latency_seconds;

配置并创建音频播放器

通过如下函数进行:

static int aout_open_audio(SDL_Aout *aout, const SDL_AudioSpec *desired, SDL_AudioSpec *obtained)
  • 配置数据源

     SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,OPENSLES_BUFFERS};SLDataFormat_PCM *format_pcm = &opaque->format_pcm;format_pcm->formatType       = SL_DATAFORMAT_PCM;format_pcm->numChannels      = desired->channels;format_pcm->samplesPerSec    = desired->freq * 1000; // milli Hzformat_pcm->bitsPerSample    = SL_PCMSAMPLEFORMAT_FIXED_16;format_pcm->containerSize    = SL_PCMSAMPLEFORMAT_FIXED_16;switch (desired->channels) {case 2:format_pcm->channelMask  = SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT;break;case 1:format_pcm->channelMask  = SL_SPEAKER_FRONT_CENTER;break;default:ALOGE("%s, invalid channel %d", __func__, desired->channels);goto fail;}format_pcm->endianness       = SL_BYTEORDER_LITTLEENDIAN;SLDataSource audio_source = {&loc_bufq, format_pcm};
    

  • 配置数据管道

     SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX,opaque->slOutputMixObject};SLDataSink audio_sink = {&loc_outmix, NULL};
    

  • 其它参数

     const SLInterfaceID ids2[] = { SL_IID_ANDROIDSIMPLEBUFFERQUEUE, SL_IID_VOLUME, SL_IID_PLAY };static const SLboolean req2[] = { SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE };
    

  • 创建播放器

     ret = (*slEngine)->CreateAudioPlayer(slEngine, &slPlayerObject, &audio_source,&audio_sink, sizeof(ids2) / sizeof(*ids2),ids2, req2);
    

  • 获取相关接口

      //获取seek和play接口ret = (*slPlayerObject)->GetInterface(slPlayerObject, SL_IID_PLAY, &opaque->slPlayItf);CHECK_OPENSL_ERROR(ret, "%s: slPlayerObject->GetInterface(SL_IID_PLAY) failed", __func__);//音量调节接口ret = (*slPlayerObject)->GetInterface(slPlayerObject, SL_IID_VOLUME, &opaque->slVolumeItf);CHECK_OPENSL_ERROR(ret, "%s: slPlayerObject->GetInterface(SL_IID_VOLUME) failed", __func__);//获取音频输出的BufferQueue接口ret = (*slPlayerObject)->GetInterface(slPlayerObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &opaque->slBufferQueueItf);CHECK_OPENSL_ERROR(ret, "%s: slPlayerObject->GetInterface(SL_IID_ANDROIDSIMPLEBUFFERQUEUE) failed", __func__);
    

  • 设置回调函数

回调函数并不传递音频数据,它只是告诉程序:我已经准备好接受处理(播放)数据了。这时候就可以调用Enqueue向BufferQueue中插入音频数据了。

    ret = (*opaque->slBufferQueueItf)->RegisterCallback(opaque->slBufferQueueItf, aout_opensles_callback, (void*)aout);CHECK_OPENSL_ERROR(ret, "%s: slBufferQueueItf->RegisterCallback() failed", __func__);
  • 初始化其它参数

     opaque->bytes_per_frame   = format_pcm->numChannels * format_pcm->bitsPerSample / 8;//每一帧的bytes数,此处将一个采样点作为一帧opaque->milli_per_buffer  = OPENSLES_BUFLEN;//一个buffer中的音频时长,单位为millisecondsopaque->frames_per_buffer = opaque->milli_per_buffer * format_pcm->samplesPerSec / 1000000; // samplesPerSec is in milli,一个buffer中的音频时长*每秒的样本(帧)数,得到每个音频buffer中的帧数opaque->bytes_per_buffer  = opaque->bytes_per_frame * opaque->frames_per_buffer;//最后求出每个buffer中含有的byte数目。opaque->buffer_capacity   = OPENSLES_BUFFERS * opaque->bytes_per_buffer;
    

此回调函数每执行一次Dequeue会被执行一次。

音频数据的处理

音频数据的处理为典型的生产者消费者模型,解码线程解码出音频数据插入到队列中,音频驱动程序取出数据将声音播放出来。

audio_thread函数为音频解码线程主函数:

static int audio_thread(void *arg){do {ffp_audio_statistic_l(ffp);if ((got_frame = decoder_decode_frame(ffp, &is->auddec, frame, NULL)) < 0)//从PacketQueue中取出pakcet并进行解码,生成一帧数据...if (!(af = frame_queue_peek_writable(&is->sampq)))goto the_end;af->pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);af->pos = frame->pkt_pos;af->serial = is->auddec.pkt_serial;af->duration = av_q2d((AVRational){frame->nb_samples, frame->sample_rate});av_frame_move_ref(af->frame, frame);frame_queue_push(&is->sampq);//将帧数据插入帧队列 FrameQueue}

aout_thread_n 为音频输出线程主函数:

static int aout_thread_n(SDL_Aout *aout){
...SDL_LockMutex(opaque->wakeup_mutex);//如果没有退出播放&&(当前播放器状态为暂停||插入音频BufferQueue中的数据条数大于OPENSLES_BUFFERS)if (!opaque->abort_request && (opaque->pause_on || slState.count >= OPENSLES_BUFFERS)) {//不知道为什么if下面又加了一层while??while (!opaque->abort_request && (opaque->pause_on || slState.count >= OPENSLES_BUFFERS)) {//如果此时为非暂停状态,将播放器状态置为PLAYINGif (!opaque->pause_on) {(*slPlayItf)->SetPlayState(slPlayItf, SL_PLAYSTATE_PLAYING);}//如果暂停或者队列中数据过多,这里都会等待一个条件变量,并将过期时间置为1秒,应该是防止BufferQueue中的数据不再快速增加SDL_CondWaitTimeout(opaque->wakeup_cond, opaque->wakeup_mutex, 1000);SLresult slRet = (*slBufferQueueItf)->GetState(slBufferQueueItf, &slState);if (slRet != SL_RESULT_SUCCESS) {ALOGE("%s: slBufferQueueItf->GetState() failed\n", __func__);SDL_UnlockMutex(opaque->wakeup_mutex);}//暂停播放if (opaque->pause_on)(*slPlayItf)->SetPlayState(slPlayItf, SL_PLAYSTATE_PAUSED);}//恢复播放if (!opaque->abort_request && !opaque->pause_on) {(*slPlayItf)->SetPlayState(slPlayItf, SL_PLAYSTATE_PLAYING);}}...next_buffer = opaque->buffer + next_buffer_index * bytes_per_buffer;next_buffer_index = (next_buffer_index + 1) % OPENSLES_BUFFERS;//调用回调函数生成插入到BufferQueue中的数据audio_cblk(userdata, next_buffer, bytes_per_buffer);//如果需要刷新BufferQueue数据,则清除数据,何时需要清理数据??解释在下面if (opaque->need_flush) {(*slBufferQueueItf)->Clear(slBufferQueueItf);opaque->need_flush = false;}//不知道为什么会判断两次??if (opaque->need_flush) {ALOGE("flush");opaque->need_flush = 0;(*slBufferQueueItf)->Clear(slBufferQueueItf);} else {//最终将数据插入到BufferQueue中。slRet = (*slBufferQueueItf)->Enqueue(slBufferQueueItf, next_buffer, bytes_per_buffer);...
}

以下是为条件变量opaque->wakeup_cond 发送signal的几个函数,目的是让输出线程快速响应

  • static void aout_opensles_callback(SLAndroidSimpleBufferQueueItf caller, void *pContext)

  • static void aout_close_audio(SDL_Aout *aout)

  • static void aout_pause_audio(SDL_Aout *aout, int pause_on)

  • static void aout_flush_audio(SDL_Aout *aout)

  • static void aout_set_volume(SDL_Aout *aout, float left_volume, float right_volume)

  • 第一个为音频播放器的BufferQueue设置的回调函数,每从队列中取出一条数据执行一次,这个可以理解,队列中去除一条数据,立刻唤醒线程Enqueue数据。

  • 第二个为关闭音频播放器的时候调用的函数,立马退出线程。

  • 第三个为暂停/播放音频播放器函数,马上设置播放器状态。

  • 第四个为清空BufferQueue时调用的函数,立刻唤醒线程Enqueue数据。

  • 第五个为设置音量函数,马上设置音量。

通过调用如下函数生成插入到BufferQueue中的数据 :

static void sdl_audio_callback(void *opaque, Uint8 *stream, int len){...if (is->audio_buf_index >= is->audio_buf_size) {//如果buffer中没有数据了,生成新数据。audio_size = audio_decode_frame(ffp);...     if (!is->muted && is->audio_buf && is->audio_volume == SDL_MIX_MAXVOLUME)//直接拷贝到streammemcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);else {memset(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);}}

生成新数据的函数不是对音频数据进行解码,而是对帧数据进行了二次处理,对音频进行了必要的重采样或者变速变调。

static int audio_decode_frame(FFPlayer *ffp){...//重采样len2 = swr_convert(is->swr_ctx, out, out_count, in, af->frame->nb_samples);...//音频变速变调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);...//最后将数据保存到audio_buf中is->audio_buf = (uint8_t*)is->audio_new_buf;...
}

最后一个比较让人困惑的问题是何时才会清理BufferQueue,看一下清理的命令是在何时发出的:

static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)
{...if (is->auddec.pkt_serial != is->audioq.serial) {is->audio_buf_index = is->audio_buf_size;memset(stream, 0, len);// stream += len;// len = 0;SDL_AoutFlushAudio(ffp->aout);break;}...
}

它是在音频输出线程中获取即将插入到BufferQueue的音频数据,调用回调函数时发出的,发出的条件如上所示,其中pkt_serial 为从PacketQueue队列中取出的需要解码的packet的serial,serial为当前PacketQueue队列的serial。也就是说,如果两者不等,就需要清理BufferQueue。这里的serial是要保证前后数据包的连续性,例如发生了Seek,数据不连续,就需要清理旧数据。

注:在播放器中的VideoState成员中,audioq和解码成员auddec中的queue是同一个队列。

decoder_init(&is->auddec, avctx, &is->audioq, is->continue_read_thread);

结束语

笔者从头到尾把和音频输出相关的自认为重要的源码做了一些解释和记录,有些细节没有去深入研究。以后有时间慢慢学习。

参考

音频的相关知识

AAC 到 PCM 音频解码

SoundTouch实现音频变速变调

Android音频开发之OpenSL ES

浅聊OpenSL ES音频开发

ffplay packet queue分析

一步步实现windows版ijkplayer系列文章之三——Ijkplayer播放器源码分析之音视频输出——音频篇相关推荐

  1. 一步步实现windows版ijkplayer系列文章之二——Ijkplayer播放器源码分析之音视频输出——视频篇...

    一步步实现windows版ijkplayer系列文章之一--Windows10平台编译ffmpeg 4.0.2,生成ffplay 一步步实现windows版ijkplayer系列文章之二--Ijkpl ...

  2. 2022最新轻量级影视搜索播放器源码+已修复版

    正文: 2022最新轻量级影视搜索播放器源码+已修复版,因为之前API失效了,需要重新写规则,所以本次我花了点时间修复了下源码,有兴趣自行去研究. 更新日志: 2020/7/14 前端修正了一些显示, ...

  3. idea 线程内存_Java线程池系列之-Java线程池底层源码分析系列(一)

    课程简介: 课程目标:通过本课程学习,深入理解Java线程池,提升自身技术能力与价值. 适用人群:具有Java多线程基础的人群,希望深入理解线程池底层原理的人群. 课程概述:多线程的异步执行方式,虽然 ...

  4. idea 线程内存_Java线程池系列之-Java线程池底层源码分析系列(二)

    课程简介: 课程目标:通过本课程学习,深入理解Java线程池,提升自身技术能力与价值. 适用人群:具有Java多线程基础的人群,希望深入理解线程池底层原理的人群. 课程概述:多线程的异步执行方式,虽然 ...

  5. SkeyePlayer RTSP/RTMP流媒体超低延迟播放器源码解析系列之H264一帧多NAL写MP4录像花屏问题解决方案

    接上一篇[SkeyePlayer源码解析系列之录像写MP4]之续篇,我们来讲解一下关于H264编码格式中的一帧多nal(Network Abstract Layer, 即网络抽象层),关于H264和N ...

  6. Kafka系列二——消息发送send方法源码分析

    文章目录 一.send使用说明 1.1 客户端代码 1.2 ProducerRecord 二.发送过程 2.1 send 2.2 doSend关键代码 2.2.1 RecordAccumulator原 ...

  7. 图解VC++版PE文件解析器源码分析

    该源码下载自 http://download.csdn.net/download/witch_soya/4979587 1 Understand 分析的图表 2 PE结构解析的主要代码简要分析 首先看 ...

  8. producer send源码_RocketMq系列之Producer顺序消息发送源码分析(四)

    有序消息 消息有序指的是可以按照消息的发送顺序来消费. RocketMQ可以严格的保证消息有序.但这个顺序,不是全局顺序,只是分区(queue)顺序. 顺序消息生产者 public static vo ...

  9. 【Java集合学习系列】HashMap实现原理及源码分析

    HashMap特性 hashMap是基于哈希表的Map接口的非同步实现,继承自AbstractMap接口,实现了Map接口(HashTable跟HashMap很像,HashTable中的方法是线程安全 ...

最新文章

  1. Blender和Substance Painter制作科幻装甲视频教程
  2. vs2010 失效后的解决办法
  3. html自动生成工具_关于STM32代码自动生成的工具的进度....
  4. python实现软件的注册功能(机器码+注册码机制)
  5. 第5章-css选择器初级和背景
  6. Ubuntu 找不到libc.so.6
  7. NPM Unexpected end of JSON input while parsing near
  8. jq移除一条html语句,jquery html()删除脚本标签
  9. Struts2配置国际化文件
  10. RN学习(一)——创建第一个RN项目
  11. 安卓GLSurfaceView使用简单范例
  12. C语言程序设计赵山林高媛,C语言程序设计(工业和信息化普通高等教育“十二五”规划教材立项项目)(赵山林高媛)资料.doc...
  13. MVC思想及SpringMVC设计理念
  14. 130个资源网站,总有一个你用得着
  15. 现代大学英语精读第二版(第三册)学习笔记(原文及全文翻译)——6B - They Dared Cocaine—and Lost(尝试可卡因后,他们迷失了)
  16. LeetCode刷题之1436. 旅行终点站
  17. python写安卓游戏_10分钟学会python写游戏脚本!Python其实很简单
  18. 作为一个食品专业的本科生 我对我们专业是彻底失望了[转帖]
  19. 高等数学二从零开始学习的总结笔记(持续更新)
  20. 完整性+存储过程和函数——CHECK / CONSTRAINT / TRIGGER / PROCEDURE/ FUNCTION

热门文章

  1. 商业化广告--体系学习-- 7 -- 行业蓝图篇 --广告产品发展路径
  2. 第三周《java语言程序设计——面向对象入门》学习总结
  3. [CTSC2016]时空旅行
  4. 编译原理:了解编译原理
  5. 七牛云邵杰:视觉智能——视频云新时代
  6. SoftwareSerial库的使用——Arduino软件模拟串口通信
  7. 200行Go代码实现自己的区块链——区块生成与网络通信
  8. 局域网计算机间的传输介质,计算机局域网知识点:传输介质
  9. NLP初学-Word Segmentation(分词)
  10. Ubuntu 18.04安装坚果云后打开出现白板