音视频开发进阶指南:基于Android与iOS平台的实践.pdf
http://www.100md.com
2019年12月25日
![]() |
| 第1页 |
![]() |
| 第10页 |
![]() |
| 第14页 |
![]() |
| 第21页 |
![]() |
| 第33页 |
![]() |
| 第650页 |
参见附件(11576KB,701页)。
音视频开发进阶指南是一本关于移动开发的书籍教程,以安卓和苹果手机为实践,讲述了音视频的实战开发教程,作者都是该领域的资深人士,移动开发的朋友可以看看学习!

音视频开发进阶指南介绍
书中首先通过介绍音视频的物理现象与基础概念,帮助读者建立模拟信号到数字信号转化的过程,然后重点介绍了如何在移动端开发音视频项目,其中包括开发中所需要了解的各种知识,如音视频的解码与渲染,采集与编码,音视频的处理与性能优化等;在此基础上最后综合当下*流行的直播场景,介绍如何将书中的已有项目改造、适配成为一个直播产品,进一步帮助读者自由、有效的开发出功能丰富、性能一流的音视频App。
音视频开发进阶指南作者信息
展晓凯
曾工作于淘宝,参与设计开发淘宝旅行的机票搜索业务,曾就职于115网盘参与核心功能的研发,现就职于北京*淘科技有限公司,任音视频架构师,在公司的唱吧、唱吧直播间、火星三条产品线都负责客户端核心的架构设计与开发工作,其中唱吧目前公布的数据已有几亿用户,月活也在千万量级。作者在工作与生活中非常乐于帮助同事与朋友,痴迷于互联网可以推动整个人类历史的发展,同时作为这个大时代的一个小人物,作者也希望帮助更多的人参与到互联网行业中。
魏晓红
多年以来一直从事Android应用的开发工作,在Android点播、直播相关领域有着丰富的经验,由于开发的产品在印度有非常多的Android用户,所以这让其积累了丰富的Android端适配的经验,作者在平时的工作与生活中乐于探讨技术,希望互利网可以改变人类生活。
音视频开发进阶指南章节目录
第1章 音视频基础概念
第2章 移动端环境搭建
第3章 FFmpeg的介绍与使用
第4章 移动平台下的音视频渲染
第5章 实现一款视频播放器
第6章 音视频的采集与编码
第7章 实现一款视频录制应用
第8章 音频效果器的介绍与实践
第9章 视频效果器的介绍与实践
音视频开发进阶指南:基于Android与iOS平台的实践截图


音视频开发进阶指南:基于Android与iOS平台的实践
展晓凯 魏晓红 著
ISBN:978-7-111-58582-4
本书纸版由机械工业出版社于2018年出版,电子版由华章分社(北京华
章图文信息有限公司,北京奥维博世图书发行有限公司)全球范围内制
作与发行。
版权所有,侵权必究
客服热线:+ 86-10-68995265
客服信箱:service@bbbvip.com
官方网址:www.hzmedia.com.cn
新浪微博 @华章数媒
微信公众号 华章电子书(微信号:hzebook)目录 推荐序一
推荐序二
前言
第1章 音视频基础概念
1.1 声音的物理性质
1.1.1 声音是波
1.1.2 声波的三要素
1.1.3 声音的传播介质
1.1.4 回声
1.1.5 共鸣
1.2 数字音频
1.3 音频编码
1.4 图像的物理现象
1.5 图像的数值表示
1.5.1 RGB表示方式
1.5.2 YUV表示方式
1.5.3 YUV和RGB的转化
1.6 视频的编码方式
1.6.1 视频编码
1.6.2 编码概念
1.7 本章小结
第2章 移动端环境搭建
2.1 在iOS上如何搭建一个基础项目
2.2 在Android上如何搭建一个基础项目
2.3 交叉编译的原理与实践
2.3.1 交叉编译的原理
2.3.2 iOS平台交叉编译的实践
2.3.3 Android平台交叉编译的实践
2.3.4 使用LAME编码MP3文件
2.4 本章小结
第3章 FFmpeg的介绍与使用
3.1 FFmpeg的编译与命令行工具的使用
3.1.1 FFmpeg的编译
3.1.2 FFmpeg命令行工具的使用
3.2 FFmpeg API的介绍与使用3.3 FFmpeg源码结构
3.3.1 libavformat与libavcodec介绍
3.3.2 FFmpeg通用API分析
3.3.3 调用FFmpeg解码时用到的函数分析
3.3.4 调用FFmpeg编码时用到的函数分析
3.3.5 面向对象的C语言设计
3.4 本章小结
第4章 移动平台下的音视频渲染
4.1 AudioUnit介绍与实践
4.2 Android平台的音频渲染
4.2.1 AudioTrack的使用
4.2.2 OpenSL ES的使用
4.3 视频渲染
4.3.1 OpenGL ES介绍
4.3.2 OpenGL ES的实践
4.3.3 上下文环境搭建
4.3.4 OpenGL ES中的纹理
4.4 本章小结
第5章 实现一款视频播放器
5.1 架构设计
5.2 解码模块的实现
5.3 音频播放模块的实现
5.3.1 Android平台的音频渲染
5.3.2 iOS平台的音频渲染
5.4 画面播放模块的实现
5.4.1 Android平台的视频渲染
5.4.2 iOS平台的视频渲染
5.5 AVSync模块的实现
5.5.1 维护解码线程
5.5.2 音视频同步
5.6 中控系统串联起各个模块
5.6.1 初始化阶段
5.6.2 运行阶段
5.6.3 销毁阶段
5.7 本章小结
第6章 音视频的采集与编码
6.1 音频的采集6.1.1 Android平台的音频采集
6.1.2 iOS平台的音频采集
6.2 视频画面的采集
6.2.1 Android平台的视频画面采集
6.2.2 iOS平台的视频画面采集
6.3 音频的编码
6.3.1 libfdk_aac编码AAC
6.3.2 Android平台的硬件编码器MediaCodec
6.3.3 iOS平台的硬件编码器AudioToolbox
6.4 视频画面的编码
6.4.1 libx264编码H264
6.4.2 Android平台的硬件编码器MediaCodec
6.4.3 iOS平台的硬件编码器
6.5 本章小结
第7章 实现一款视频录制应用
7.1 视频录制的架构设计
7.2 音频模块的实现
7.2.1 音频队列的实现
7.2.2 Android平台的实现
7.2.3 iOS平台的实现
7.3 音频编码模块的实现
7.3.1 改造编码器
7.3.2 编码器适配器
7.4 画面采集与编码模块的实现
7.4.1 视频队列的实现
7.4.2 Android平台画面编码后入队
7.4.3 iOS平台画面编码后入队
7.5 Mux模块
7.5.1 初始化
7.5.2 封装和输出
7.5.3 销毁资源
7.6 中控系统串联起各个模块
7.7 本章小结
第8章 音频效果器的介绍与实践
8.1 数字音频基础
8.1.1 波形图
8.1.2 频谱图8.1.3 语谱图
8.1.4 深入理解时域与频域
8.2 数字音频处理:快速傅里叶变换
8.3 基本乐理知识
8.3.1 乐谱
8.3.2 音符的音高与十二平均律
8.3.3 音符的时值
8.3.4 节拍
8.3.5 MIDI格式
8.4 混音效果器
8.4.1 均衡效果器
8.4.2 压缩效果器
8.4.3 混响效果器
8.5 效果器实现
8.5.1 Android平台实现效果器
8.5.2 iOS平台实现效果器
8.6 本章小结
第9章 视频效果器的介绍与实践
9.1 图像处理的基本原理
9.1.1 亮度调节
9.1.2 对比度调节
9.1.3 饱和度调节
9.2 图像处理进阶
9.2.1 图像的卷积过程
9.2.2 锐化效果器
9.2.3 高斯模糊算法
9.2.4 双边滤波算法
9.2.5 图层混合介绍
9.3 使用FFmpeg内部的视频滤镜
9.3.1 FFmpeg视频滤镜介绍
9.3.2 滤镜图的构建
9.3.3 使用与销毁滤镜图
9.3.4 常用滤镜介绍
9.4 使用OpenGL ES实现视频滤镜
9.4.1 加水印
9.4.2 添加自定义文字
9.4.3 美颜效果器9.4.4 动图贴纸效果器
9.4.5 主题效果器
9.5 本章小结
第10章 专业的视频录制应用实践
10.1 视频硬件解码器的使用
10.1.1 初始化信息准备
10.1.2 VideoToolbox解码H264
10.1.3 MediaCodec解码H264
10.2 音频效果器的集成
10.2.1 Android音效处理系统的实现
10.2.2 iOS音效处理系统的实现
10.3 一套跨平台的视频效果器的设计与实现
10.4 将特效处理库集成到视频录制项目中
10.4.1 Android平台特效集成
10.4.2 iOS平台特效集成
10.5 本章小结
第11章 直播应用的构建
11.1 直播场景分析
11.2 拉流播放器的构建
11.2.1 Android平台播放器增加后处理过程
11.2.2 iOS平台播放器增加后处理过程
11.3 推流器的构建
11.4 第三方云服务介绍
11.5 礼物系统的实现
11.5.1 Cocos2dX项目的运行原理
11.5.2 关键API详解
11.5.3 实现一款动画
11.6 聊天系统的实现
11.6.1 Android客户端的WebSocket实现
11.6.2 iOS客户端的WebSocket实现
11.7 本章小结
第12章 直播应用中的关键处理
12.1 直播应用的细节分析
12.1.1 推流端细节分析
12.1.2 拉流端细节分析
12.2 推流端的关键处理
12.2.1 自适应码率的实践12.2.2 统计数据保证后续的应对策略
12.3 拉流端的关键处理
12.3.1 重试机制的实践
12.3.2 首屏时间的保证
12.3.3 统计数据保证后续的应对策略
12.4 本章小结
第13章 工欲善其事,必先利其器
13.1 Android平台工具详解
13.1.1 ADB工具的熟练使用
13.1.2 MAT工具检测Java端的内存泄漏
13.1.3 NDK工具详解
13.1.4 Native层的内存泄漏检测
13.1.5 breakpad收集线上Crash
13.2 iOS使用Instruments诊断应用
13.2.1 Debug Navigator
13.2.2 Time Profiler
13.2.3 Allocations
13.2.4 Leaks
13.3 本章小结
附录A 通过Ne10的交叉编译输入理解ndk-build
附录B 编码器的使用细节
附录C 视频的表示与编码推荐序一
我的第一部智能手机是多普达565,当时使用的是Windows Mobile
操作系统,现在看来,不管是操作交互,还是系统的整体能力,都与今
天的智能手机有着天壤之别。但是,即便是那样的操作系统,也已经足
够让当时的我认识到一个真正的操作系统能给一部随身设备赋予的强大
能力。智能手机后来这十几年的发展还是超出了很多人的预料,很难想
象,如果没有现在的高速数据网络和每个人手头的这个小终端,我们的
工作和生活会有多少不方便的地方。
生活在这个时代的程序员是足够幸运的,信息化的无限渗透也意味
着有想法、有能力的程序员对人们生活范围的影响越来越大。我与很多
资深的开发人员都有过交流,基本上能把这些人分成两类。一类是以对
技术本身的钻研为目标的技术人员,他们所关注的是架构是不是足够先
进,可扩展性如何,系统整体的负载能力,遇到错误时的鲁棒性等。总
之,他们内心的成就感来自是否把技术做到了极致,同行(或者自己)
看到的时候,会不会由衷地说这东西真棒。还有一类技术人员,他们的
成就感来自自己的工作成果是否能够直接对使用者产生影响。相对技术
本身的挑战,这类人更在乎自己所做的东西是否真正被身边的人使用,使用者用到自己作品时的感受,以及是否真正给使用者和社会带来了帮
助。两类人没有高低之分,倒有点像理论研究和应用研究的关系,两个
方向相辅相成,彼此成就,彼此推动。
音视频技术的发展正好处在理论和应用的十字路口。各种音视频技
术天生就与老百姓的生活距离很近,拍照、唱歌、小视频、瘦脸、美
颜、大混音,基本上算是大众手机里最常用的一些功能了。这些功能背
后的技术,也会因用户的需求推动而快速发展。从软件到硬件,从各种
人脸识别的算法到越来越强大的摄像头或是专用的DSP芯片,摩尔定律
在这个细分领域的发挥可以算是淋漓尽致了,这也对有志于在这个领域
发展的研发人员提出了更高的要求:一方面,要能沉得下去,音视频相
关的底层技术可以说是CS领域里相当难啃的一块硬骨头,对算法、编
码甚至是数学基础都有很高的要求;另一方面,还要能经常抬起头,不
只是要跟上相关领域的快速发展,也要理解和挖掘用户的真实需求,这
可以算是CS领域里挑战很大同时成就感也很大的困难模式了。
本书的作者展晓凯是音视频领域的权威专家。在几年间的持续研究中,他总结出了一套在音视频领域比较系统的工程实践方法,希望这些
总结能够帮助到对相关领域感兴趣的你。如果能进一步影响更多的人,将是对本书作者最大的鼓励和褒奖。
田然
2017年9月于北京推荐序二
随着智能手机的出现,音视频传感器比以往任何时候都更加接近用
户,可以说,移动互联网时代其实也是音视频内容爆炸的时代。几乎每
个应用都希望能尽可能地开启用户所有的权限,让用户把自己的每时每
刻都上传和分享出来,于是即时聊天IM几乎走进了每一个App。而这一
两年的直播大战的背后,更是让直播这种富媒体几乎变成了各类App的
标配功能,以至于市场对音视频研发人才求之若渴,音视频的学习材料
也一时洛阳纸贵,由此催生出来的各种大小公司、大小产业也是层出不
穷,业内对音视频处理的知识和经验的分享更是如火如荼。然而,音视
频处理实在是一门艰深的学问,从傅里叶变换到差分编码等各种理论,再到充满差异化甚至Bug的安卓设备的现实应用环境,既要在国内复杂
的网络环境下尽力满足用户视听观感的流畅感觉,又希望让小小的移动
设备物尽其用来为用户提供最极致的感官体验,而这些灼烧无数程序员
脑细胞的问题,实在不是一两篇长文就可以简单讲清楚的。若要让一个
几无基础的开发者能够系统地掌握音视频的处理知识,确有必要撰写一
本图书并通过具体实例来详细讲解才行。
我认识本书的作者展晓凯已经五年多了,他几乎是唱吧最勤奋的技
术人员,在唱吧浩瀚的代码库里,到处都留有他的成果。唱吧是中国颇
具影响力的K歌产品之一,从2012年雄踞苹果榜首之后,就再也没有离
开过榜单,多年来其为用户创造了无数新奇的功能和体验,从各种振奋
人心的混响,到节奏感逼人的自动说唱,所有的这些功能,都是出自展
晓凯和他所在的三五个人的小团队之手。而以唱吧的用户体量,他们自
然也遇到了许多问题,从各种花屏、黑屏、啸叫、白噪等通用技术问
题,到用户手机上因形形色色系统或硬件差异而产生的稀奇古怪的问
题,也无一不是展晓凯所在的团队逐个去解决的。可以说,发展到今
天,唱吧几乎拥有业内最丰富的音视频处理经验,而这些经验中的精
华,如今终于有机会整理付梓,实在是唱吧的一点骄傲,也是业内同行
的一份福音。
虽然与展晓凯一起并肩战斗了四五年,但我本人并没有太多机会详
细参与每一个音视频问题的处理,如今通读完这本书的原稿,我深感收
获不小。本书是展晓凯花费了无数心血的作品,其中的每一行代码每一
个实例都来自他日常工作中实际问题的总结。本书也许不是市面上唯一
一本关于音视频处理的著作,但它的出现,足以为市场带来一个特有的完整的视角,并令无数致力于打造移动设备上音视频处理完美体验的程
序员受益!
黄全能
2017年9月于北京前言
为什么要写这本书
整个音视频领域的架构以及开发已经演进了很长时间,从最开始的
广电领域,到PC端的音视频领域,再到本书所介绍的移动端的音视频
领域。尤其在这几年中,移动端音视频领域架构的变化是巨大的。在移
动互联网的发展热潮中,我有幸从事了音视频领域的设计与开发,并且
就职于最时尚的手机KTV——唱吧,这使得我开发出来的东西能够服务
于几亿用户。对于音视频的移动端的应用,不论是开发还是使用,在近
两年都达到了一个高峰,而作为一名工程师,如何高效地开发出一个音
视频App,是一件非常困难的事情,特别是对于不太了解音视频概念的
工程师。我从事软件开发已有7年多的时间,接触音视频领域也已经有5
年多,在整个开发过程中,不同的时间段会遇到不同的挑战,尤其是在
最开始涉足音视频领域的时候,真可谓举步维艰。首先,对于音视频的
基础概念不是特别清楚,再者在工作中边学边做,很难对整个音视频领
域有一个全面的了解,并且市面上没有相关成熟的资料从更高的层次来
介绍音视频领域在移动端的演进与发展。这几年的设计实战与开发经
验,以及带新人入门的众多感触,让我有了写这本书的动力,同时也形
成了这本书的核心内容,我希望通过本书可以帮助更多想要在移动端音
视频领域实现自己想法的工程师,让大家可以顺利地建立起自己的音视
频App。我非常希望能为刚入门的读者或者遇到困难的读者提供帮助,希望大家可以享受整个开发的过程,享受自己开发的产品为人们的生活
带来便利的成就感。另外,从整个音视频开发领域来讲,我也十分希望
能够通过本书贡献出自己的绵薄之力。
读者对象
·产品经理,这部分读者可以从中了解在移动端进行音视频开发会
遇到的很多问题以及对应的优化策略,例如:如何通过音视频的统计数
据为产品提供更加流畅的策略(视频观看的秒开、直播推流的流畅度、视频上传的成功率等)。
·项目经理,这部分读者可以了解很多时下流行的名词与概念,不再会因为几个专业名词就让自己不知所措,并且有助于更好地评估音视
频项目开发中的风险与进度。
·测试人员,这部分读者可以学习在音视频App中由于处理过程不同
而导致的瓶颈问题,书中也提到了一些自动化测试相关的命令以及工
具,可以对CPU的负载情况、内存的占用情况、内存泄漏问题等进行分
析。
·架构师与工程师,这部分读者只需要一点移动开发经验就可以阅
读本书了。当然如果你已经是一个高级移动开发工程师或者架构师,那
么读起本书来将更加游刃有余。再进一步,如果你已经是移动领域的音
视频开发工程师了,那么恭喜你,我们之间将会有一场关于技术领域内
部的对话。
·开设相关课程的高等院校。
如何阅读本书
为了避免说教式的讲解带来枯燥乏味的阅读体验,本书给出了大量
的实例及生产环境下的案例。本书可分为四个部分:第一部分是入门,从理论基础开始讲解,最终会产生两个实践项目;第二部分是提高,基
于第一部分的项目添加特效,形成一个完整的多媒体项目;第三部分是
扩展,结合当下比较流行的直播场景进行实际案例分析;第四部分是工
具,介绍当下大部分可以提高开发以及测试效率的工具。下面是各个章
节的基本介绍。
第1章,介绍音视频的基础概念,其中包括音视频的基础数据格
式、编码后的数据格式以及不同格式之间的相互转换等。
第2章,从零开始讲解如何搭建一个iOS项目和一个Android项目,并且添加C++支持,因为在音视频领域的开发中,有相当一部分的代码
需要用C++来编写,这样就可以做到两个平台(Android和iOS平台)共
用一套代码仓库,以提升开发效率。然后讲解交叉编译,因为在音视频
开发过程中会用到很多第三方开源库,如果将这些库编译到我们的项目
中,势必要进行交叉编译,因此本章会重点讲解这些内容。
第3章,探讨FFmpeg开源库。对于音视频开发来讲,FFmpeg开源库是众所周知也是普遍使用的。本章首先从编译开始,接着是命令行使
用,再到源码结构,最后是API调用,以层层递进的方式对FFmpeg开源
库展开介绍。
第4章,讲解如何利用各自平台的API进行声音与画面的渲染以及解
码,对于画面的渲染,推荐使用OpenGL ES,两个平台可以使用同一个
代码仓库。
第5章,实现一款视频播放器。有了前四章的基础,我们已经完全
可以构建起一个视频播放器了。本书最大的特点就是经过几章基础知识
的学习立即开始一个项目的实践,通过本章的视频播放器项目,我们将
会熟悉播放器是如何工作的。
第6章,重点介绍音视频的采集与编码器。特别是硬件编解码器在
各个平台上的使用,使得应用能够更高效(耗电更少、发热更少、界面
更流畅)地运行在用户的手机上。
第7章,继续开发一个视频录制的新项目,该项目可以使我们更加
熟悉音视频应用在各个平台下的实现。
第8章,讲解如何处理音频流。毕竟让别人听采集出来的干声是很
不礼貌的,本章将利用各种特效来美化采集的声音。
第9章,讲解如何处理视频流,使视频中的颜值变得更高,毕竟爱
美之心人皆有之。
第10章,在第7章的项目基础之上,增加第8章的音频特效和第9章
的视频特效,从而构建一个实际生产过程中的多媒体应用。
第11章,继续以项目作为驱动,详细讲解如何基于之前学习的内容
构建一个直播的应用,重点介绍推流以及拉流端,同时还涉及礼物特
效、聊天以及第三方云服务的内容。
第12章,由于直播应用很难用一章的篇幅讲完,所以本章针对一些
核心的处理进行讲解。
第13章,介绍常用的工具和排错方法,说明在日常开发中如何更有
效率地解决问题,本章内容并不仅限于音视频的开发领域。附录给出一些参考内容。
勘误和支持
由于作者水平有限,编写时间仓促,书中难免会出现一些错误或者
不准确的地方,恳请读者批评指正。为此,我特意创建了一个在线支持
与应急方案的二级站点http:music-video.cn 。你可以将书中的错误发布
在Bug勘误表页面中,同时如果你遇到问题,也可以访问QA页面,我
将尽量在线上为你提供最满意的解答。书中的全部源代码文件都将发布
在这个网站上,我也会及时地进行相应的功能更新。如果你有更多的宝
贵意见,也欢迎发送邮件至我的邮箱zhanxiaokai2008@126.com,我很
期待听到你们的真挚反馈。
致谢
感谢唱吧与唱吧的每一位同事,是这个公司让我的职业生涯发展到
了今天,也是这个公司让我能在音视频领域达到今天的成就,可以说没
有唱吧就不会有这本书的问世。
感谢唱吧的每一位用户,感谢你们对唱吧的长期支持和贡献,没有
你们就不会有唱吧的今天,也就不会有这本书的问世。
感谢我的老婆,感谢你对我工作以及写作的支持,是你在我背后默
默做了很多事情,才让我把更多的时间和精力放到工作以及写作中。
感谢机械工业出版社华章公司的编辑Lisa老师,感谢你的魄力和远
见,在这一年多的时间中始终支持我的写作,正是你的鼓励和帮助引导
我顺利完成全部书稿。
感谢互联网,我们在互联网上迈出的任何一步都是人类历史向前迈
进的一步,感谢众多互联网人的辛苦工作,为我们创造了这么多机遇。
谨以此书献给我最亲爱的家人、同事,以及众多互联网从业者。
展晓凯2017年9月于北京第1章 音视频基础概念
为了避免枯燥的说教式讲解,本章将结合示意图来介绍音频与视频
的基础概念,对于本章的学习,不需要使用任何开发环境。研究数字音
频时,必须要对声学现象有一定的了解,若只研究数字音频而忽略了声
学现象,那就本末倒置了,因为音频技术是为了记录、存储和回放声学
现象才发明的,所以先了解声学现象对学习数字音频是有很大帮助的。
声音产生于自然界,早在没有任何科学研究的时候,就已经存在声音
了,并且各种声音还可以组成动听的音乐;当人类有了记录以及存储声
音的能力之后,就迎来了模拟信号到数字信号的转换,所以本章首先会
介绍如何记录以及存储声音。
相比声音,视频(画面)更易于观察,本章将从图像的物理现象开
始讲解,然后讨论一帧帧的画面是如何描述的,以及视频是如何被记录
和存储到设备中的。
学习完本章之后,相信大家对耳朵能够直接听到的声音,眼睛能够
直接看到的图像会有更深入的认知。1.1 声音的物理性质
1.1.1 声音是波
说到声音,爱好音乐的人首先可能会想到优美的音乐或者是劲爆十
足的舞曲,这些音乐只是声音的一种。音乐是由乐器弹奏或者歌手演唱
而产生的,那么声音是如何产生的呢?回想一下中学物理课本上的定义
——声音是由物体振动而产生的(如图1-1所示)。
图 1-1
如图1-1所示,当小球撞击到音叉的时候,音叉会发生振动,对周
围的空气产生挤压,从而产生声音。声音是一种压力波,当演奏乐器、拍打一扇门或者敲击桌面时,它们的振动都会引起空气有节奏的振动,使周围的空气产生疏密变化,形成疏密相间的纵波(可以理解为石头落入水中激起的波纹),由此就产生了声波,这种现象会一直延续到振动
消失为止。1.1.2 声波的三要素
声波的三要素是频率、振幅和波形,频率代表音阶的高低,振幅代
表响度,波形代表音色。
频率(过零率)越高,波长就越短。低频声响的波长则较长,所以
其可以更容易地绕过障碍物,因此能量衰减就小,声音就会传得远,反
之则会得到完全相反的结论。
响度其实就是能量大小的反映,用不同的力度敲击桌子,声音的大
小势必也会不同。在生活中,分贝常用于描述响度的大小。声音超过一
定的分贝,人类的耳朵就会受不了。
音色其实也不难理解,在同样的音调(频率)和响度(振幅)下,钢琴和小提琴的声音听起来是完全不相同的,因为它们的音色不同。波
的形状决定了其所代表声音的音色,钢琴和小提琴的音色不同就是因为
它们的介质所产生的波形不同。
人类耳朵的听力有一个频率范围,大约是20Hz~20kHz,不过,即
使是在这个频率范围内,不同的频率,听力的感觉也会不一样,业界非
常著名的等响曲线,就是用来描述等响条件下声压级与声波频率关系
的,如图1-2所示。图 1-2
从图1-2中可以看出,人耳对3~4kHz频率范围内的声音比较敏感,而对于较低或较高频率的声音,敏感度就会有所减弱;在声压级较低
时,听觉的频率特性会很不均匀;而在声压级较高时,听觉的频率特性
会变得较为均匀。频率范围较宽的音乐,其声压以80~90dB为最佳,超
过90dB将会损害人耳(105dB为人耳极限)。1.1.3 声音的传播介质
吉他是通过演奏者拨动琴弦来发出声音的,鼓是通过鼓槌敲击鼓面
发出声音的,这些声音的产生都离不开振动,就连我们说话也是因为声
带振动而产生声音的。既然都是振动产生的声音,那为什么吉他、鼓和
人声听起来相差这么大呢?这是因为介质不同。我们的声带振动发出声
音之后,经过口腔、颅腔等局部区域的反射,再经过空气传播到别人的
耳朵里,这就是我们说的话被别人听到的过程,其中包括了最初的发声
介质与颅腔、口腔,还有中间的传播介质等。事实上,声音的传播介质
很广,它可以通过空气、液体和固体进行传播;而且介质不同,传播的
速度也不同,比如,声音在空气中的传播速度为340ms,在蒸馏水中的
传播速度为1497ms,而在铁棒中的传播速度则可以高达5200ms;不
过,声音在真空中是无法传播的。
生活小贴士
在日常生活中,我们也会利用对声音的研究去做一些使我们更舒适
的事情,比如吸音棉和隔音棉,这两种常见产品的发明就是通过研究声
音在传播中的特性而研发出来的。
吸音主要是解决声音反射而产生的嘈杂感,吸音材料可以衰减入射
音源的反射能量,从而达到对原有声源的保真效果,比如录音棚里面的
墙壁上就会使用吸音棉材料。
隔音主要是解决声音的透射而降低主体空间内的吵闹感,隔音棉材
料可以衰减入射音源的透射能量,从而达到主体空间的安静状态,比如
KTV里面的墙壁上就会安装隔音棉材料。1.1.4 回声
当我们在高山或空旷地带高声大喊的时候,经常会听到回声
(echo)。之所以会有回声是因为声音在传播过程中遇到障碍物会反弹
回来,再次被我们听到(如图1-3所示)。
但是,若两种声音传到我们的耳朵里的时差小于80毫秒,我们就无
法区分开这两种声音了,其实在日常生活中,人耳也在收集回声,只不
过由于嘈杂的外界环境以及回声的分贝(衡量声音能量值大小的单位)
比较低,所以我们的耳朵分辨不出这样的声音,或者说是大脑能接收到
但分辨不出。
图 1-31.1.5 共鸣
自然界中有光能、水能,生活中有机械能、电能,其实声音也可以
产生能量,例如两个频率相同的物体,敲击其中一个物体时另一个物体
也会振动发声(如图1-4所示)。
图 1-4
这种现象称为共鸣,共鸣证明了声音传播可以带动另一个物体振
动,也就是说,声音的传播过程也是一种能量的传播过程。1.2 数字音频
1.1节主要介绍了声音的物理现象以及声音中常见的概念,也为后
续的讲解统一了术语,从本节开始,我们将进入数字音频概念的介绍。
为了将模拟信号数字化,本节将分3个概念对数字音频进行讲解,分别是采样、量化和编码。首先要对模拟信号进行采样,所谓采样就是
在时间轴上对信号进行数字化。根据奈奎斯特定理(也称为采样定
理),按比声音最高频率高2倍以上的频率对声音进行采样(也称为AD
转换),1.1节中提到过,对于高质量的音频信号,其频率范围(人耳
能够听到的频率范围)是20Hz~20kHz,所以采样频率一般为
44.1kHz,这样就可以保证采样声音达到20kHz也能被数字化,从而使得
经过数字化处理之后,人耳听到的声音质量不会被降低。而所谓的
44.1kHz就是代表1秒会采样44100次(如图1-5所示)。
那么,具体的每个采样又该如何表示呢?这就涉及将要讲解的第二
个概念:量化。量化是指在幅度轴上对信号进行数字化,比如用16比特
的二进制信号来表示声音的一个采样,而16比特(一个short)所表示的
范围是[-32768,32767],共有65536个可能取值,因此最终模拟的音频
信号在幅度上也分为了65536层(如图1-6所示)。
图 1-5图 1-6
既然每一个量化都是一个采样,那么这么多的采样该如何进行存储
呢?这就涉及将要讲解的第三个概念:编码。所谓编码,就是按照一定
的格式记录采样和量化后的数字数据,比如顺序存储或压缩存储,等
等。
这里面涉及了很多种格式,通常所说的音频的裸数据格式就是脉冲
编码调制(Pulse Code Modulation,PCM)数据。描述一段PCM数据一
般需要以下几个概念:量化格式(sampleFormat)、采样率
(sampleRate)、声道数(channel)。以CD的音质为例:量化格式(有
的地方描述为位深度)为16比特(2字节),采样率为44100,声道数为
2,这些信息就描述了CD的音质。而对于声音格式,还有一个概念用来
描述它的大小,称为数据比特率,即1秒时间内的比特数目,它用于衡
量音频数据单位时间内的容量大小。而对于CD音质的数据,比特率为
多少呢?计算如下:
44100 16 2 = 1378.125kbps
那么在1分钟里,这类CD音质的数据需要占据多大的存储空间呢?
计算如下:
1378.125 60 8 1024 = 10.09MB
当然,如果sampleFormat更加精确(比如用4字节来描述一个采
样),或者sampleRate更加密集(比如48kHz的采样率),那么所占的
存储空间就会更大,同时能够描述的声音细节就会越精确。存储的这段二进制数据即表示将模拟信号转换为数字信号了,以后就可以对这段二
进制数据进行存储、播放、复制,或者进行其他任何操作。
麦克风是如何采集声音的
麦克风里面有一层碳膜,非常薄而且十分敏感。1.1节中介绍过,声音其实是一种纵波,会压缩空气也会压缩这层碳膜,碳膜在受到挤压
时也会发出振动,在碳膜的下方就是一个电极,碳膜在振动的时候会接
触电极,接触时间的长短和频率与声波的振动幅度和频率有关,这样就
完成了声音信号到电信号的转换。之后再经过放大电路处理,就可以实
施后面的采样量化处理了。
前面提到过分贝,那么什么是分贝呢?分贝是用来表示声音强度的
单位。日常生活中听到的声音,若以声压值来表示,由于其变化范围非
常大,可以达到六个数量级以上,同时由于我们的耳朵对声音信号强弱
刺激的反应不是线性的(1.1节中提到过等响曲线),而是呈对数比例
关系,所以引入分贝的概念来表达声学量值。所谓分贝是指两个相同的
物理量(例如,A1和A0)之比取以10为底的对数并乘以10(或20),即:
N= 10 lg(A1 A0)
分贝符号为“dB”,它是无量纲的。式中A0是基准量(或参考
量),A1是被量度量。1.3 音频编码
1.2节中提到了CD音质的数据采样格式,曾计算出每分钟需要的存
储空间约为10.1MB,如果仅仅是将其存放在存储设备(光盘、硬盘)
中,可能是可以接受的,但是若要在网络中实时在线传播的话,那么这
个数据量可能就太大了,所以必须对其进行压缩编码。压缩编码的基本
指标之一就是压缩比,压缩比通常小于1(否则就没有必要去做压缩,因为压缩就是要减小数据容量)。压缩算法包括有损压缩和无损压缩。
无损压缩是指解压后的数据可以完全复原。在常用的压缩格式中,用得
较多的是有损压缩,有损压缩是指解压后的数据不能完全复原,会丢失
一部分信息,压缩比越小,丢失的信息就越多,信号还原后的失真就会
越大。根据不同的应用场景(包括存储设备、传输网络环境、播放设备
等),可以选用不同的压缩编码算法,如PCM、WAV、AAC、MP3、Ogg等。
压缩编码的原理实际上是压缩掉冗余信号,冗余信号是指不能被人
耳感知到的信号,包含人耳听觉范围之外的音频信号以及被掩蔽掉的音
频信号等。人耳听觉范围之外的音频信号在1.2节中已经提到过,所以
在此不再赘述。而被掩蔽掉的音频信号则主要是因为人耳的掩蔽效应,主要表现为频域掩蔽效应与时域掩蔽效应,无论是在时域还是频域上,被掩蔽掉的声音信号都被认为是冗余信息,不进行编码处理。
下面介绍几种常用的压缩编码格式。
(1)WAV编码
PCM(脉冲编码调制)是Pulse Code Modulation的缩写。前面已经
介绍过PCM大致的工作流程,而WAV编码的一种实现(有多种实现方
式,但是都不会进行压缩操作)就是在PCM数据格式的前面加上44字
节,分别用来描述PCM的采样率、声道数、数据格式等信息。
特点:音质非常好,大量软件都支持。
适用场合:多媒体开发的中间文件、保存音乐和音效素材。
(2)MP3编码MP3具有不错的压缩比,使用LAME编码(MP3编码格式的一种实
现)的中高码率的MP3文件,听感上非常接近源WAV文件,当然在不
同的应用场景下,应该调整合适的参数以达到最好的效果。
特点:音质在128Kbits以上表现还不错,压缩比比较高,大量软件
和硬件都支持,兼容性好。
适用场合:高比特率下对兼容性有要求的音乐欣赏。
(3)AAC编码
AAC是新一代的音频有损压缩技术,它通过一些附加的编码技术
(比如PS、SBR等),衍生出了LC-AAC、HE-AAC、HE-AAC v2三种
主要的编码格式。LC-AAC是比较传统的AAC,相对而言,其主要应用
于中高码率场景的编码(≥80Kbits);HE-AAC(相当于AAC+SBR)
主要应用于中低码率场景的编码(≤80Kbits);而新近推出的HE-AAC
v2(相当于AAC+SBR+PS)主要应用于低码率场景的编码
(≤48Kbits)。事实上大部分编码器都设置为≤48Kbits自动启用PS技
术,而>48Kbits则不加PS,相当于普通的HE-AAC。
特点:在小于128Kbits的码率下表现优异,并且多用于视频中的音
频编码。
适用场合:128Kbits以下的音频编码,多用于视频中音频轨的编
码。
(4)Ogg编码
Ogg是一种非常有潜力的编码,在各种码率下都有比较优秀的表
现,尤其是在中低码率场景下。Ogg除了音质好之外,还是完全免费
的,这为Ogg获得更多的支持打好了基础。Ogg有着非常出色的算法,可以用更小的码率达到更好的音质,128Kbits的Ogg比192Kbits甚至更
高码率的MP3还要出色。但目前因为还没有媒体服务软件的支持,因此
基于Ogg的数字广播还无法实现。Ogg目前受支持的情况还不够好,无
论是软件上的还是硬件上的支持,都无法和MP3相提并论。
特点:可以用比MP3更小的码率实现比MP3更好的音质,高中低码
率下均有良好的表现,兼容性不够好,流媒体特性不支持。适用场合:语音聊天的音频消息场景。1.4 图像的物理现象
在学习了音频的相关概念之后,现在开始讨论视频,视频是由一幅
幅图像组成的,所以要学习视频还得从图像学习开始。
与音频的学习方法类似,视频的学习依然是从图像的物理现象开始
回顾,这里需要回顾一下小学做过的三棱镜实验,还记得如何利用三棱
镜将太阳光分解成彩色的光带吗?第一个做这个实验的人是牛顿,各色
光因其所形成的折射角不同而彼此分离,就像彩虹一样,所以白光能够
分解成多种色彩的光。后来人们通过实验证明,红绿蓝三种色光无法被
分解,故称为三原色光,等量的三原色光相加会变为白光,即白光中含
有等量的红光(R)、绿光(G)、蓝光(B)。
在日常生活中,由于光的反射,我们才能看到各类物体的轮廓及颜
色。但是如果将这个理论应用到手机上,那么结论还是这个样子吗?答
案是否定的,因为在黑暗中我们也可以看到手机屏幕上的内容,实际上
人眼能看到手机屏幕上的内容的原理如下。图 1-7
假设一部手机屏幕的分辨率是1280×720,说明水平方向有720个像
素点,垂直方向有1280个像素点,所以整个手机屏幕就有1280×720个像
素点(这也是分辨率的含义)。每个像素点都由三个子像素点组成(如
图1-7所示),这些密密麻麻的子像素点在显微镜下可以看得一清二
楚。当要显示某篇文字或者某幅图像时,就会把这幅图像的每一个像素
点的RGB通道分别对应的屏幕位置上的子像素点绘制到屏幕上,从而显
示整个图像。
所以在黑暗的环境下也能看到手机屏幕上的内容,是因为手机屏幕
是自发光的,而不是通过光的反射才被人们看到的。1.5 图像的数值表示
1.5.1 RGB表示方式
通过1.4节的讲解,我们已经知道任何一个图像都可以由RGB组
成,那么一个像素点的RGB该如何表示呢?音频里面的每一个采样
(sample)均使用16个比特来表示,那么像素里面的子像素又该如何表
示呢?常用的表示方式有以下几种。
·浮点表示:取值范围为0.0~1.0,比如,在OpenGL ES中对每一个
子像素点的表示使用的就是这种表达方式。
·整数表示:取值范围为0~255或者00~FF,8个比特表示一个子像
素,32个比特表示一个像素,这就是类似于某些平台上表示图像格式的
RGBA_8888数据格式。比如,Android平台上RGB_565的表示方法为16
比特模式表示一个像素,R用5个比特来表示,G用6个比特来表示,B用
5个比特来表示。
对于一幅图像,一般使用整数表示方法来进行描述,比如计算一张
1280×720的RGBA_8888图像的大小,可采用如下方式:
1280 720 4 = 3.516MB
这也是位图(bitmap)在内存中所占用的大小,所以每一张图像的
裸数据都是很大的。对于图像的裸数据来讲,直接在网络上进行传输也
是不太可能的,所以就有了图像的压缩格式,比如JPEG压缩:JPEG是
静态图像压缩标准,由ISO制定。JPEG图像压缩算法在提供良好的压缩
性能的同时,具有较好的重建质量。这种算法被广泛应用于图像处理领
域,当然其也是一种有损压缩。在很多网站如淘宝上使用的都是这种压
缩之后的图片,但是,这种压缩不能直接应用于视频压缩,因为对于视
频来讲,还有一个时域上的因素需要考虑,也就是说,不仅仅要考虑帧
内编码,还要考虑帧间编码。视频采用的是更成熟的算法,关于视频压
缩算法的相关内容将会在后续章节(1.6节)进行介绍。1.5.2 YUV表示方式
对于视频帧的裸数据表示,其实更多的是YUV数据格式的表示,YUV主要应用于优化彩色视频信号的传输,使其向后兼容老式黑白电
视。与RGB视频信号传输相比,它最大的优点在于只需要占用极少的频
宽(RGB要求三个独立的视频信号同时传输)。其中“Y”表示明亮度
(Luminance或Luma),也称灰阶值;而“U”和“V”表示的则是色度
(Chrominance或Chroma),它们的作用是描述影像的色彩及饱和度,用于指定像素的颜色。“亮度”是透过RGB输入信号来建立的,方法是将
RGB信号的特定部分叠加到一起。“色度”则定义了颜色的两个方面——
色调与饱和度,分别用Cr和Cb来表示。其中,Cr反映了RGB输入信号
红色部分与RGB信号亮度值之间的差异,而Cb反映的则是RGB输入信
号蓝色部分与RGB信号亮度值之间的差异。
之所以采用YUV色彩空间,是因为它的亮度信号Y和色度信号U、V是分离的。如果只有Y信号分量而没有U、V分量,那么这样表示的图
像就是黑白灰度图像。彩色电视采用YUV空间正是为了用亮度信号Y解
决彩色电视机与黑白电视机的兼容问题,使黑白电视机也能接收彩色电
视信号,最常用的表示形式是Y、U、V都使用8个字节来表示,所以取
值范围就是0~255。在广播电视系统中不传输很低和很高的数值,实际
上是为了防止信号变动造成过载,因而把这“两边”的数值作为“保护
带”,不论是Rec.601还是BT.709的广播电视标准中,Y的取值范围都是
16~235,UV的取值范围都是16~240。
YUV最常用的采样格式是4:2:0,4:2:0并不意味着只有Y、Cb
而没有Cr分量。它指的是对每行扫描线来说,只有一种色度分量是以
2:1的抽样率来存储的。相邻的扫描行存储着不同的色度分量,也就是
说,如果某一行是4:2:0,那么其下一行就是4:0:2,再下一行是
4:2:0,以此类推。对于每个色度分量来说,水平方向和竖直方向的
抽样率都是2:1,所以可以说色度的抽样率是4:1。对非压缩的8比特
量化的视频来说,8×4的一张图片需要占用48字节的内存(如图1-8所
示)。图 1-8
相较于RGB,我们可以计算一帧为1280×720的视频帧,用
YUV420P的格式来表示,其数据量的大小如下:
1280 720 1 + 1280 720 0.5 = 1.318MB
如果fps(1秒的视频帧数目)是24,按照一般电影的长度90分钟来
计算,那么这部电影用YUV420P的数据格式来表示的话,其数据量的
大小就是:
1.318MB 24fps 90min 60s = 166.8GB
所以仅用这种方式来存储电影肯定是不可行的,更别说在网络上进
行流媒体播放了,那么如何对电影进行存储以及流媒体播放呢?答案是
需要进行视频编码,下一节将会讨论视频的编码。1.5.3 YUV和RGB的转化
前面已经讲过,凡是渲染到屏幕上的东西(文字、图片或者其
他),都要转换为RGB的表示形式,那么YUV的表示形式和RGB的表
示形式之间是如何进行转换的呢?对于标清电视601标准,它从YUV转
换到RGB的公式与高清电视709的标准是不同的,通过如下的计算(如
图1-9和图1-10)即可得知。
图 1-9图 1-10
那么什么时候该用哪一种转换呢?比较典型的场景是在iOS平台中
使用摄像头采集出YUV数据之后,上传显卡成为一个纹理ID,这个时
候就需要做YUV到RGB的转换(具体的细节会在后面的章节中详细讲
解)。在iOS的摄像头采集出一帧数据之后(CMSampleBufferRef),我
们可以在其中调用CVBufferGetAttachment来获取YCbCrMatrix,用于决
定使用哪一个矩阵进行转换,对于Android的摄像头,由于其是直接纹
理ID的回调,所以不涉及这个问题。其他场景下需要大家自行寻找对应
的文档,以找出适合的转换矩阵进行转换。1.6 视频的编码方式
1.6.1 视频编码
还记得前面讨论的音频压缩方式吗?音频压缩主要是去除冗余信
息,从而实现数据量的压缩。那么对于视频压缩,又该从哪几方面来对
数据进行压缩呢?其实与前面提到的音频编码类似,视频压缩也是通过
去除冗余信息来进行压缩的。相较于音频数据,视频数据有极强的相关
性,也就是说有大量的冗余信息,包括空间上的冗余信息和时间上的冗
余信息。
使用帧间编码技术可以去除时间上的冗余信息,具体包括以下几个
部分。
·运动补偿:运动补偿是通过先前的局部图像来预测、补偿当前的
局部图像,它是减少帧序列冗余信息的有效方法。
·运动表示:不同区域的图像需要使用不同的运动矢量来描述运动
信息。
·运动估计:运动估计是从视频序列中抽取运动信息的一整套技
术。
使用帧内编码技术可以去除空间上的冗余信息。
还记得前面提到过的图像编码标准JPEG吗?对于视频,ISO同样也
制定了标准:Motion JPEG即MPEG,MPEG算法是适用于动态视频的压
缩算法,它除了对单幅图像进行编码外,还利用图像序列中的相关原则
去除冗余,这样可以大大提高视频的压缩比。截至目前,MPEG的版本
一直在不断更新中,主要包括这样几个版本:Mpeg1(用于VCD)、Mpeg2(用于DVD)、Mpeg4 AVC(现在流媒体使用最多的就是它
了)。
相比较于ISO制定的MPEG的视频压缩标准,ITU-T制定的H.261、H.262、H.263、H.264一系列视频编码标准是一套单独的体系。其中,H.264集中了以往标准的所有优点,并吸取了以往标准的经验,采用的是简洁设计,这使得它比Mpeg4更容易推广。现在使用最多的就是
H.264标准,H.264创造了多参考帧、多块类型、整数变换、帧内预测等
新的压缩技术,使用了更精细的分像素运动矢量(14、18)和新一代
的环路滤波器,这使得压缩性能得到大大提高,系统也变得更加完善。1.6.2 编码概念
1.IPB帧
视频压缩中,每帧都代表着一幅静止的图像。而在进行实际压缩
时,会采取各种算法以减少数据的容量,其中IPB帧就是最常见的一
种。
·I帧:帧内编码帧(intra picture),I帧通常是每个GOP(MPEG所
使用的一种视频压缩技术)的第一个帧,经过适度地压缩,作为随机访
问的参考点,可以当成静态图像。I帧可以看作一个图像经过压缩后的
产物,I帧压缩可以得到6:1的压缩比而不会产生任何可觉察的模糊现
象。I帧压缩可去掉视频的空间冗余信息,下面即将介绍的P帧和B帧是
为了去掉时间冗余信息。
·P帧:前向预测编码帧(predictive-frame),通过将图像序列中前
面已编码帧的时间冗余信息充分去除来压缩传输数据量的编码图像,也
称为预测帧。
·B帧:双向预测内插编码帧(bi-directional interpolated prediction
frame),既考虑源图像序列前面的已编码帧,又顾及源图像序列后面
的已编码帧之间的时间冗余信息,来压缩传输数据量的编码图像,也称
为双向预测帧。
基于上面的定义,我们可以从解码的角度来理解IPB帧。
·I帧自身可以通过视频解压算法解压成一张单独的完整视频画面,所以I帧去掉的是视频帧在空间维度上的冗余信息。
·P帧需要参考其前面的一个I帧或者P帧来解码成一张完整的视频画
面。
·B帧则需要参考其前一个I帧或者P帧及其后面的一个P帧来生成一
张完整的视频画面,所以P帧与B帧去掉的是视频帧在时间维度上的冗
余信息。IDR帧与I帧的理解
在H264的概念中有一个帧称为IDR帧,那么IDR帧与I帧的区别是什
么呢?首先来看一下IDR的英文全称instantaneous decoding refresh
picture,因为H264采用了多帧预测,所以I帧之后的P帧有可能会参考I
帧之前的帧,这就使得在随机访问的时候不能以找到I帧作为参考条
件,因为即使找到I帧,I帧之后的帧还是有可能解析不出来,而IDR帧
就是一种特殊的I帧,即这一帧之后的所有参考帧只会参考到这个IDR
帧,而不会再参考前面的帧。在解码器中,一旦收到一个IDR帧,就会
立即清理参考帧缓冲区,并将IDR帧作为被参考的帧。
2.PTS与DTS
DTS主要用于视频的解码,英文全称是Decoding Time Stamp,PTS
主要用于在解码阶段进行视频的同步和输出,全称是Presentation Time
Stamp。在没有B帧的情况下,DTS和PTS的输出顺序是一样的。因为B
帧打乱了解码和显示的顺序,所以一旦存在B帧,PTS与DTS势必就会
不同,本书后边的章节里会详细讲解如何结合硬件编码器来重新设置
PTS和DTS的值,以便将硬件编码器和FFmpeg结合起来使用。这里先简
单介绍一下FFmpeg中使用的PTS和DTS的概念,FFmpeg中使用
AVPacket结构体来描述解码前或编码后的压缩数据,用AVFrame结构体
来描述解码后或编码前的原始数据。对于视频来说,AVFrame就是视频
的一帧图像,这帧图像什么时候显示给用户,取决于它的PTS。DTS是
AVPacket里的一个成员,表示该压缩包应该在什么时候被解码,如果视
频里各帧的编码是按输入顺序(显示顺序)依次进行的,那么解码和显
示时间应该是一致的,但是事实上,在大多数编解码标准(如H.264或
HEVC)中,编码顺序和输入顺序并不一致,于是才会需要PTS和DTS
这两种不同的时间戳。
3.GOP的概念
两个I帧之间形成的一组图片,就是GOP(Group Of Picture)的概
念。通常在为编码器设置参数的时候,必须要设置gop_size的值,其代
表的是两个I帧之间的帧数目。前面已经讲解过,一个GOP中容量最大
的帧就是I帧,所以相对来讲,gop_size设置得越大,整个画面的质量就
会越好,但是在解码端必须从接收到的第一个I帧开始才可以正确解码
出原始图像,否则会无法正确解码(这也是前面提到的I帧可以作为随
机访问的帧)。在提高视频质量的技巧中,还有个技巧是多使用B帧,一般来说,I的压缩率是7(与JPG差不多),P是20,B可以达到50,可
见使用B帧能节省大量空间,节省出来的空间可以用来更多地保存I帧,这样就能在相同的码率下提供更好的画质。所以我们要根据不同的业务
场景,适当地设置gop_size的大小,以得到更高质量的视频。
结合IPB帧和图1-11,相信大家能够更好地理解PTS与DTS的概念。
图 1-111.7 本章小结
本章的内容到此结束,这里简单复习一下要点。本章首先介绍了音
频的物理现象,进而研究如何存储以及编码音频,然后又讨论了图像的
物理现象,从RGB到YUV,最后讲解了视频的编码操作。本章的内容
理论和概念比较多,难免会枯燥一些,但是了解这些概念是必需的。其
实本章讲解的东西若想要继续深究,还是有很多可供研究的,由于本书
的核心是应用层的开发,因此就不考虑展开来讲了。对于应用层的开发
人员来说,掌握本章的内容就已经足够了,若想要进一步了解相关的知
识,可自行参考其他资料。第2章 移动端环境搭建
第1章学习了音视频的基础知识,本章将介绍如何在移动端(包括
iOS和Android两个平台)搭建开发音视频的环境,以及如何在每一个平
台的开发环境中添加C++支持。由于篇幅有限,在搭建开发环境时,本
章将只以Mac OS操作系统为例进行讲解,对于Linux、Windows等操作
系统的读者,需要大家自行将各种开发环境适配到对应的系统中。但
是,书中所有项目的设计与实现是不区分开发系统环境的。
在音视频的开发过程中,不可能所有的编码、解码以及处理都由开
发者从零开始编写,因此免不了会用到一些第三方库,所以本章还将讲
解交叉编译,并尝试交叉编译几个音视频相关的开源库。在本章的最
后,会使用LAME这个开源的MP3编码库在iOS平台和Android平台上将
一个PCM文件编码为MP3文件,最终将编码后的MP3文件发送到电脑上
即可进行播放,这也是本章要完成的最终目标。2.1 在iOS上如何搭建一个基础项目
1.新建一个iOS项目
首先,在Xcode中选择新建一个项目,会弹出如图2-1所示的界面,然后我们选择一个新项目的模板,一般选择Single View Application模
板。
之后点击Next,进入图2-2所示的界面,这里需要键入产品的名字
及包名,注意产品的名字将作为安装到用户手机上的应用名称,包名则
是该应用的唯一标识,键入完毕之后点击Next。
完成上述步骤之后,就完成了一个iOS项目的创建。查看该项目的
工程文件,会看到Xcode默认是以Story board的形式构建的界面部分,由于我们不希望使用这种形式来构建界面,而是希望使用xib的形式来
构建界面,所以要在Main Interface选项中删除其中的内容,如图2-3所
示。图 2-1图 2-2
图 2-3
接下来,再建立一个界面文件作为应用的第一个界面,而在iOS中
一个界面就是一个xib文件,如图2-4所示。图 2-4
然后点击Next,进入图2-5所示的界面,键入该xib的名字,然后选
择Target为主工程,点击Create。
现在,打开该xib文件,如图2-6所示,首先要在Placeholders选项下
面的File’s Owner中连接视图中的view,然后设定Class的值为
ViewController类。图 2-5
图 2-6
iOS程序运行的时候其入口是main.m,但是苹果公司不希望开发者修改该文件,而是要求开发者去修改应用的入口——AppDelgate.m文
件,该文件中提供了各种生命周期方法,应用启动的时候将会触发的方
法是application:didFinishLaunchingWithOptions,所以修改该生命周期
方法的代码如下:
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen]
bounds]];
UINavigationController navigationController = [[UINavigationController
alloc] initWithRootViewController:[[ViewController alloc]
initWithNibName:@ViewController bundle:nil]];
self.window.rootViewController = navigationController;
[self.window makeKeyAndVisible];
接下来要为该xib拖入一个Encode按钮,并且将该按钮的点击事件
委托给ViewController中的startEncode方法,如图2-7所示。
图 2-7
startEncode方法提供了NSLog来输出一条日志信息。然后运行整个
项目,点击Encode按钮,如果可以看到正常的日志信息,则说明项目搭
建成功了。
2.CocoaPods的介绍与使用开发应用的时候,并不是所有的基础功能都由开发者从零开始开
发,所以经常会用到第三方开源类库。一定要记住,不要重复造轮子!
不过,在引用这些类库时,可能会出现几个令人头疼的问题:第一是所
引用的第三方类库有可能需要依赖于其他的第三方类库;第二是需要使
用已引用库的新功能,需要了解该类库对应的版本号,并且该类库对其
所依赖的其他第三方类库可能也有版本的要求。如果我们仅仅是复制粘
贴,那将会非常麻烦,尤其是对于大型项目来说,有时甚至可能会是一
种灾难。所以在Java Web的开发中,大家通常使用Maven来构建项目。
选择Maven,一方面是基于项目的依赖关系,另一个重要的原因就是为
了方便引用第三方的类库。对于Android的应用开发,现在使用最多的
就是Gradle,一方面是出于打包的需求,另一方面也是为了满足引用第
三方库的需要。对于iOS应用呢?使用最多的则是CocoaPods,它可以为
开发者提供引用众多第三方类库的功能,比如JSONKit、AFNetWorking
等。
了解了CocoaPods的作用之后,接下来就是学习如何安装和使用
CocoaPods来引用第三方库。
(1)安装CocoaPods
首先要安装好Ruby环境,Mac机器上已经自带了Ruby环境,如果是
其他的开发系统,请读者自行安装,关于如何安装Ruby环境,互联网上
提供了大量的资料。安装完Ruby环境之后,使用如下命令就可以安装
CocoaPods了:
sudo gem install cocoapods
如果执行上述命令之后很长时间都没有反应,那么可能是gem所使
用的默认源有问题,可以使用淘宝提供的源来安装CocoaPods,具体的
做法是先删除掉默认的源,操作命令如下:
gem sources --remove https:rubygems.org
然后替换上新的源,命令如下:
gem sources -a http:rubygems-china.oss.aliyuncs.com此时,可以执行以下命令来查看现在的源到底是什么:
gem sources -l
如果出现下面的内容则说明源更改成功了:
CURRENT SOURCES
http:rubygems-china.oss.aliyuncs.com
此时,再一次在终端执行安装CocoaPods的命令:
sudo gem install cocoapods
稍等片刻,就可以将CocoaPods下载到本地并且安装成功了。
(2)使用CocoaPods
前面提到过CocoaPods是用来管理第三方库的工具,那么它是如何
判断项目需要依赖哪一个第三方库的呢?答案是根据根目录下名为
Podfile的配置文件来获知的。因此该配置文件的语法以及如何编写该配
置文件自然就成了本章节学习的重点,下面一起来看一下。
首先输入平台系统的要求:
platform :ios, '7.0'
上述命令代表项目运行在iOS 7.0平台之上,配置文件从第二行开始
就会出现一个从target'Project Name'do到end的代码块,该代码块将用于
配置具体要引入的第三方库。代码块如下所示:
target :ktv do
pod 'Mantle', '1.5'
pod 'AFNetworking', '2.6.0'
end上述配置代表项目需要引进版本号为1.5的Mantle与版本号为2.6.0的
AFNetworking库。最后,在命令行中,进入项目的根目录下执行pod
install命令,如果Podfile配置文件的语法没有问题,那么CocoaPods就会
为项目生成两个文件和一个目录。其中,一个文件的名称是项目名
称.xcworkspace,另一个文件的名称是Podfile.lock,而生成的目录则是
Pods,其中放置了引用第三方库的源码。此时,双击后缀名为
xcworkspace的文件即可打开此项目,查看Xcode目录里的结构,会发现
项目中已经有了刚才新引入的第三方库了。
增加C++支持
在单独编写一个C或C++的项目时,如果该项目需要引用到第三方
库,那么编译阶段需要配置参数“extra-cflags,-I”来指定引用头文件的
位置,链接阶段需要配置参数“ld-flags,-L”来指定静态库的位置,并且
使用-l来指定引用的是哪一个库。在C++的编译中,如果需要在程序的
执行过程中带入一些宏(define的常量),那么就应该在“extra-cflags”的
后面增加自己需要定义的宏,例如-DAUTO_TEST,这就相当于在程序
中编写了如下一行代码:
define AUTO_TEST
那么在Xcode中,应该如何指定头文件、第三方库,以及预定义宏
呢?其实,Xcode里面已经存在了对应的参数设置,只需要在Build
Settings中设置这些参数即可。首先对应于-I来指定头文件的目录,Xcode使用Search Paths来设置头文件的搜索路径,通常会用到Header
Search Paths选项来指定头文件的搜索路径。其中预定义变量
(SRCROOT)和(PROJECT_DIR)都是项目的根目录,可以基于这
两个预定义变量再加上相对路径来指定头文件所在的具体位置;预定义
宏在Other C Flags选项中,可以写入-DAUTO_TEST,表示定义了
AUTO_TEST宏;在指定第三方库时,-L对应到Xcode中就是other Link
flags选项,其中可以写入需要链接的库文件;在Xcode项目中直接添加
一个静态库文件,Xcode会默认在Build phases选项的Link Binary with
Library里加入该静态库,但是如果Xcode没有自动加入该静态库的话,就需要开发者手动加一下,这里其实也是-L的一种表示方式。
接下来,编写一个类Mp3Encoder,负责将PCM数据编码为MP3文件。首先建立Mp3Encoder.cpp和Mp3Encoder.h这两个文件,然后再编写
一个encode方法,该方法将调用printf输出一行日志信息,以代表调用了
这个方法。
现在就为前面搭建起来的iOS项目增加C++的支持,由于OC语法支
持混编,所以开发者仅需要把引用C++的OC类的后缀名改为.mm(OC
类的正常后缀名是.m),就可以和C++一块编译了。所以开发者仅需要
把ViewController的后缀名改为.mm,再包含进Mp3Encoder.h头文件,实
例化该类,最后调用该类的encode方法,代码如下:
Mp3Encoder encoder = new Mp3Encoder;
encoder->encode ;
delete encoder;
如果大家可以看到printf输出的日志信息,则代表已经为该iOS项目
成功添加了C++的支持。是不是很简单呢?其实这也是苹果公司为开发
者提供的便利,下面还会讲解Android是如何添加C++支持的,相较于
iOS平台而言,Android平台要麻烦得多。2.2 在Android上如何搭建一个基础项目
首先要在开发机器上安装Eclipse(尽量选择已经安装好ADT的版
本)与Android的SDK,然后在Eclipse中建立一个Android项目,命名为
Mp3Encoder,如图2-8所示。
图 2-8
在图2-8所示的界面中,我们需要键入项目的名字、包的名字(应
用程序的唯一标识),由于在决定写这本书的时候笔者正在普吉岛度
假,所以就将其命名为com.phuket.tour.mp3encoder了。然后点击Next按钮,进入图2-9所示的界面。
图 2-9
在这一步骤中,可以让IDE建立一个默认的Activity,选中图2-9中
的Create activity复选框,点击Next按钮,进入图2-10所示的界面。图 2-10
这一步不用做任何选择,全部使用默认配置,点击Next,进入图2-
11所示的界面。图 2-11
在图2-11中,选择Blank Activity,代表应用默认是一个空的页面
(后面会加上自己的按钮),点击Next,进入图2-12所示的界面。图 2-12
在图2-12中,键入主Activity的名字为MainActivity,然后点击
Finish,即可成功建立该项目,其目录的结构如图2-13所示。图 2-13
增加C++支持
在Android平台上开发音视频相关的项目,必定会涉及JNI(Java
Native Interface)这一概念。它是一种编程框架,允许运行于JVM的
Java程序去调用本地代码(CC++以及汇编语言的代码)。本地代码通
常会与硬件或操作系统相关联,因而其会在一定程度上破坏Java语言所
宣称的跨平台特性。不过在某些业务场景下这种调用又是必需的,比如
Android系统(framework层面)就采用了大量的JNI手段去调用Native层
的实现库。如果抛开音视频相关的场景,仅仅考虑Android的开发,那
么有哪些场景会用到JNI呢?大约有以下几种情况。
·应用程序需要一些平台相关特性的支持,而Java层没有对应的API
支持(比如OpenSL ES的使用)。
·调用成熟的或者已经存在的、用CC++语言编写的代码库。比如使
用OpenGL ES的视频特效处理库,或者利用FFmpeg、LAME等第三方开源库。
·应用程序的某些关键操作对运行速度有较高的要求。这部分逻辑
可以用C或者汇编语言来编写,再通过JNI向Java层提供访问接口。
在本书后面的项目案例中,熟练使用JNI是最基本的要求,所以在
这里先详细地讲解一下。JNI主要有两种调用形式:第一种也是最常见
的,即Java代码调用Native的代码;第二种则恰好相反,就是在Native层
调用Java代码。由于使用场景最多的是第一种方式,所以本节只介绍这
种方式,而第二种方式的调用在后续的章节中也会用到,到时候读者自
然会学习到。
要完成一个Java层调用Native代码的项目,大致步骤如下。
1)编写一个Java类,并且在某个方法签名的修饰符中加上native修
饰符。
2)使用javac命令编译第1步中的Java类,使之成为一个class文件,如果使用的是Eclipse的IDE,则其将会自动执行javac的编译命令,生成
的class文件将存在于项目目录下的binclasses目录下。
3)使用javah命令将第2步的输出作为输入,生成JNI的头文件。
4)将JNI头文件复制到项目下的jni目录,并且建立一个cpp的实现
文件实现该JNI头文件中的函数。
5)编写Android.mk文件,加入第4步的本地代码,利用ndk-build生
成动态链接库。
6)在Java类中加载第5步生成的动态链接库。
7)在Java类中调用该Native方法。
下面以一个实例来完成上述的步骤,具体如下。
1)在Eclipse的com.phuket.tour.studio包下,建立一个Java文件
Mp3Encoder.java。
2)在上述Java文件中编写一个本地方法:public native void encode;
3)进入对应的class文件所在的目录下,执行下面的命令生成JNI接
口文件:
javah -jni com.phuket.tour.studio.Mp3Encoder
4)然后把该头文件复制到jni目录下,编写一个Mp3Encoder.cpp来
实现该接口文件,这里仅仅输出一行日志。
首先利用“宏定义”定义一个输出日志的宏:
define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG,__VA_ARGS__)
然后定义LOG_TAG为Mp3Encoder,并且实现该encode方法:
JNIEXPORT void JNICALL
Java_com_phuket_tour_studio_Mp3Encoder_encode(JNIEnv env,jobject obj) {
LOGI(encoder encode);
}
5)新建立一个Android.mk文件,并键入以下内容:
LOCAL_PATH := (call my-dir)
include (CLEAR_VARS)
LOCAL_SRC_FILES = .Mp3Encoder.cpp
LOCAL_LDLIBS := -L(SYSROOT)usrlib -llog
LOCAL_MODULE := libaudioencoder
include (BUILD_SHARED_LIBRARY)
上述代码具体每一行的含义,在后续章节中会有详细的解释。现在
最重要的就是把该项目运行起来。
6)在当前目录下执行ndk-build指令,编译出该动态so库。
7)在MainActivity中写入一个静态代码块:static {
System.loadLibrary(audioencoder);
}
写该代码块的目的是将刚刚编译好的so库加载到我们的项目中。
8)在onCreate中调用Mp3Encoder类的encode方法,最后运行该应
用,并且打开logcat视图查看结果。
如果可以在logcat中看到JNI层输出的日志,则代表已经成功地为
Android项目添加了C++的支持。如果仅仅是自己编写代码,而不需要调
用其他的第三方开源库,这样就完全可以了。但是在开发音视频项目
时,肯定会需要依赖很多第三方的库,比如音视频编解码的开源库、音
视频特效处理的开源库,等等,那么如何根据自己的需要添加各种依赖
库呢?这将在2.3.3节中与大家详细讨论,最终笔者会使用LAME库编码
一个MP3的音频文件作为本章的实例。2.3 交叉编译的原理与实践
本节将学习交叉编译,在音视频的开发中了解交叉编译是必需的,因为无论在哪一种移动平台下开发,第三方库都是需要进行交叉编译
的。本节会从交叉编译的原理开始介绍,然后会在两个移动平台下编译
出音视频开发常用的几个库,包括X264、FDK_AAC、LAME,最终将
以LAME库为例进行实践,完成一个将音频的PCM裸数据编码成MP3文
件的实例,以此来证明交叉编译的重要性。2.3.1 交叉编译的原理
先来看一下,如果要在PC上运行一个二进制程序(以源码的方式
进行编译,不要以包管理工具的方式来安装),需要怎样做。
首先,要有这个二进制程序的源代码(有可能是直接下载的,也有
可能是自己编写的代码),然后在PC上进行编译链接生成可执行文
件,最后在Terminal下面去执行该可执行文件。
上述流程中包含了几个角色,首先是要有源代码,然后是要知道最
终运行该二进制程序的机器是哪一个(其实就是本机器),当然,其中
最重要的就是编译器和链接器了,对于C或者C++程序来讲,就是使用
gcc和g++,而该编译器是需要预先安装在机器上的。分析了这么多角
色,总结成一句话就是:使用本机器的编译器,将源代码编译链接成为
一个可以在本机器上运行的程序。这就是正常的编译过程,也称为
Native Compilation,中文译作本机编译。
了解了本机编译之后,再来看一下何为交叉编译。所谓交叉编译,就是在一个平台(如PC)上生成另外一个平台(Android、iOS或者其他
嵌入式设备)的可执行代码。相较于正常编译,下面来看一下交叉编译
的相应角色。首先,最终程序运行的设备就是Android或者iOS设备,源
代码就是从第三方开源网站上下载的源代码,编译机器就是我们的
PC,而编译器也必须要安装到该PC上。但是这里对编译器是有特殊需
求的,最终程序运行的系统必须要提供可运行在PC上的编译器,而该
编译器就是大家常说的交叉工具编译链。
了解了交叉编译之后,大家应该能够理解交叉编译存在的必要性
了。在一般的嵌入式系统开发中,运行程序的目标平台其存储空间和运
算能力都是有限的,尽管现在的iOS和Android设备拥有越来越强劲的计
算能力,但是在这种嵌入式设备中进行本地编译是不太可能的,一则是
因为计算能力的问题,还有一个重要的原因就是编译工具以及整个编译
过程异常繁琐,所以在这种情况下,直接在ARM平台下进行本机编译
几乎是不可能的。而具有更加强劲的计算能力与更大存储空间的PC才
是理想的选择,所以大部分的嵌入式开发平台都提供了本身平台交叉编
译所需要的交叉工具编译链,通过该交叉工具编译链,开发者就能在
PC上编译出可以运行在ARM平台下的程序了。无论是自行安装PC上的编译器,还是下载其他平台(Android或者
iOS)的交叉工具编译链,它们都会提供以下几个工具:CC、AS、AR、LD、NM、GDB。那么,这几个工具到底是做什么用的呢?下面
就来逐一解释一下。
·CC:编译器,对C源文件进行编译处理,生成汇编文件。
·AS:将汇编文件生成目标文件(汇编文件使用的是指令助记符,AS将它翻译成机器码)。
·AR:打包器,用于库操作,可以通过该工具从一个库中删除或者
增加目标代码模块。
·LD:链接器,为前面生成的目标代码分配地址空间,将多个目标
文件链接成一个库或者是可执行文件。
·GDB:调试工具,可以对运行过程中的程序进行代码调试工作。
·STRIP:以最终生成的可执行文件或者库文件作为输入,然后消除
掉其中的源码。
·NM:查看静态库文件中的符号表。
·Objdump:查看静态库或者动态库的方法签名。
了解了这些之后,当读者再进行交叉编译或者使用交叉编译工具链
提供的工具时,就不会感到陌生了,接下来将会在iOS平台和Android平
台分别演示如何交叉编译出几个常用的音视频开源库。
编译器对比
正常编译一个程序的过程如下:
编译:gcc-c main.cpp.libmadmad_decoder.cpp-I.libmadinclude
打包:ar cr..prebuiltlibmedia.a mad_decoder.o
链接:g++-o main main.o-L..prebuilt-l mdedia在这个过程中,gcc、ar、g++是我们用到的三个编译工具,在这里
没有用到的ranlib、gdb、nm、strip等都会包含在PC的编译器中,同样其
他平台提供的交叉工具编译链中也会包含这些命令行工具,比如
Android提供的NDK,其交叉工具编译链中的prebuiltdarwin-x86_64bin
中,就包含了对应的gcc、ar、g++、gdb、strip、nm、ranlib等工具。2.3.2 iOS平台交叉编译的实践
前面提到的目标平台虽然都基于ARM平台,但是随着时间的推
移,平台也在不断地演进,就像armv5到armv6、armv7以及到现在的
arm64,对于iOS平台来讲,每一代手机对应的指令集到底应该是什么
呢?下面就依据iOS设备发布的时间线来逐个看一下。
·armv6:iPhone、iPhone 2、iPhone 3G
·armv7:iPhone 4、iPhone 4S
·armv7s:iPhone 5、iPhone 5S
·arm64:iPhone 5S、iPhone 6(P)、iPhone 6S(P)、iPhone
7(P)
机器对指令集的支持是向下兼容的,因此armv7的指令集是可以运
行在iPhone 5S中的,只是效率没那么高而已。借此机会,先来讨论一下
iOS项目文件中的一项配置,即Build Settings里面的Architectures选项。
Architectures指的是该App支持的指令集,一般情况下,在Xcode中新建
一个项目,其默认的Architectures选项值是Standard
architectures(armv7、arm64),表示该App仅支持armv7和arm64的指令
集;Valid architectures选项指即将编译的指令集,一般设置为armv7、armv7s、arm64,表示一般会编译这三个指令集;Build Active
Architecture Only选项表示是否只编译当前适用的指令集,一般情况下
在Debug的时候设置为YES,以便可以更加快速、高效地调试程序,而
在Release的情况下设置为NO,以便App在各个机器上都能够以最高效
率运行,因为Valid architectures选择的对应指令集是armv7、armv7s和
arm64,在Release下会为各个指令集编译对应的代码,因此最后的ipa体
积基本上翻了3倍。
基于上面的描述,以及设备与指令集平台的对比,大多数情况下,我们在实际的交叉编译过程中只编译armv7与arm64这两个指令集平台下
的库,因为armv7s设备的数量比较少,有armv7来保底完全是可以运行
的,并且armv7到armv7s指令集的变动又比较少,而arm64的变动则比较
大,设备数量也比较多,所以需要单独编译出来,以保证这一批设备可以享受到最优质的运行状况。
1.LAME的交叉编译
首先来看一下LAME库是如何被交叉编译的,在交叉编译之前先介
绍一下LAME库。
LAME简介
LAME是目前非常优秀的一种MP3编码引擎,在业界,转码成MP3
格式的音频文件时,最常用的编码器就是LAME库。当达到320Kbits以
上时,LAME编码出来的音频质量几乎可以和CD的音质相媲美,并且
还能保证整个音频文件的体积非常小,因此若要在移动端平台上编码
MP3文件,使用LAME便成为唯一的选择。
前面在介绍交叉编译原理的时候曾提到过,不论在何种平台上进行
交叉编译,都要知道交叉编译工具链在什么地方。同样,在iOS上进行
交叉编译时,也要搞清楚这个最基本的问题。其实在安装iOS的开发环
境Xcode时,配套的编译器就已经安装好了。开发iOS平台下的App就是
这么方便,不需要再单独下载交叉工具编译链,接下来直接去
SourceForge下载最新的LAME版本,访问链接如下:
https:sourceforge.netprojectslamefileslame3.99
这里选择3.99.5版本,将源码下载下来之后,编写一个
build_armv7.sh脚本,用于编译armv7指令集下的版本,以支持iPhone 5S
及以下的设备,Shell脚本里面的内容如下:
.configure \--disable-shared \--disable-frontend \--host=arm-apple-darwin \--prefix=.thinarmv7 CC=xcrun -sdk iphoneos clang -arch armv7 CFLAGS=-arch armv7 -fembed-bitcode -miphoneos-version-min=7.0 LDFLAGS=-arch armv7 -fembed-bitcode -miphoneos-version-min=7.0
make clean
make -j8
make install下面分别解释一下这几个命令以及选项的意义。configure是符合
GNU标准的软件包发布所必备的命令,所以这里是通过configure的方式
来生成Makefile文件,然后使用make和make install编译和安装整个库。
可使用configure-h命令来查看一下configure的帮助文档,了解LAME的
可选配置项,具体如下。
·--prefix:指定将编译好的库放到哪个目录下,这是GNU大部分库
的标准配置。
·--host:指定最终库要运行的平台。
·CC:指定交叉工具编译链的路径,其实这里就是指定gcc的路径。
·CFLAGS:指定编译时所带的参数。Shell脚本中指定-march是
armv7平台,代表编译的库运行的目标平台是armv7平台;另外Shell脚本
中也指定了打开bitcode选项,这使得使用编译出来的这个库的工程,可
以将enable-bitcode选项设置为YES,如果没有打开该选项,那么其在
Xcode中只能设置为NO,而这对于最终App的运行性能会有一定的影
响。Shell脚本中同时也指定了编译出来的这个库所支持的最低iOS版本
是7.0,如果不配置该参数的话,则默认是iOS 9.0版本,而所使用的编
译出来的这个库的工程,若所支持的最低iOS版本不是9.0的话,Xcode
就会给出警告。
·LDFLAGS:指定链接过程中的参数,同样也要带上bitcode的选项
以及开发者期望App支持的最低iOS版本的选项参数。
·--disable-shared:通常是GNU标准中关闭动态链接库的选项,一般
是在编译出命令行工具的时候,期望命令行工具可以单独使用而不需要
动态链接库的配置。
·--disable-frontend:不编译出LAME的可执行文件。
bitcode
bitcode模式是表明当开发者提交应用(App)到App Store上的时
候,Xcode会将程序编译为一个中间表现形式(bitcode)。App Store会
将该bitcode中间表现形式的代码进行编译优化,链接为64位或者32位的
程序。如果程序中用到了第三方静态库,则必须在编译第三方静态库的时候也开启bitcode,否则在Xcode的Build Setting中必须要关闭bitcode,这对于App来讲可能会造成性能的降低。
同样再看一下arm64指令集下面的编译脚本,建立build_arm64.sh文
件,然后输入以下内容:
.configure \--disable-shared \--disable-frontend \--host=arm-apple-darwin \--prefix=.thinarm64 CC=xcrun -sdk iphoneos clang -arch arm64 CFLAGS=-arch arm64 -fembed-bitcode -miphoneos-version-min=7.0 LDFLAGS=-arch arm64 -fembed-bitcode -miphoneos-version-min=7.0
make clean
make -j8
make install
上述选项配置大部分都与armv7的脚本相同,不同之处在于这里的
CFLAGS指定编译的目标平台是arm64的指令集平台。如果想在模拟器
上运行,那么就需要编译出i386架构下的静态库,而编译i386平台的
Shell脚本也与此类似,仅仅是改变平台架构。为了节省篇幅,后续讨论
FDK AAC及X264的编译脚本时,将只提供armv7的编译方式。
待两个脚本执行完毕之后,就可以去thin-lame目录下寻找对应的
armv7与arm64目录,并且在这两个目录下会看到bin、lib、include、share这四个目录。由于在配置的时候裁剪掉了可执行文件,所以bin目
录下不会有内容;在lib目录下则是链接过程中需要链接的libmp3lame.a
静态库文件;在include目录下则是编译过程所需要引用的头文件。
至此已经编译出了两个指令集平台下的静态库文件与include文件目
录,其实这两个include文件的目录是一样的,随便使用哪一份都可以,但是对于静态库文件,应该如何用呢?这里就会涉及如何合并静态库的
知识,合并静态库应该使用lipo命令,在终端下切换到thin-lame目录下
键入:
lipo -create .arm64liblibmp3lame.a .armv7liblibmp3lame.a -output
libmp3lame.a
这行命令会把两个平台架构下的静态库文件合并到一个libmp3lame.a的静态库文件中,现在来验证一下最终的libmp3lame.a是否
包含armv7与arm64这两个平台架构的静态库,在命令行键入:
file libmp3lame.a
如果看到如下信息,则说明编译成功了:
libmp3lame.a: Mach-O universal binary with 2 architectures: [arm_v7: current ar
archive] [arm64: current ar archive]
libmp3lame.a (for architecture armv7): current ar archive
libmp3lame.a (for architecture arm64): current ar archive
如果在开发过程中编译的第三方库比较多,而同时编译的指令集平
台也比较多,则每次都需要新建几个脚本文件,然后编译出各个平台的
静态库,最终再用lipo命令进行合并,将会非常麻烦。而软件工程师就
是要把重复性的东西做成工具,让工作变得更加简单,所以后续在代码
仓库中会有完整的编译脚本,可以编译出所有架构平台下的静态库,并
且也已经用lipo命令把所有指令集平台下的静态库合并到了一个静态库
文件中(包括即将介绍的FDK_AAC的库及X264库的交叉编译)。
2.FDK_AAC的交叉编译
在交叉编译之前先介绍一下FDK_AAC库。
FDK_AAC简介
FDK_AAC是用来编码和解码AAC格式音频文件的开源库,Android
系统编码和解码AAC所用的就是这个库。开发者Fraunhofer IIS是AAC音
频规范的核心制定者(MP3时代Fraunhofer IIS也是MP3规范的制定
者)。前面章节中已经介绍过AAC有很多种Profile,而FDK_AAC几乎
支持大部分的Profile,并且支持CBR和VBR这两种模式,根据笔者个人
的听感和频谱分析,在同等码率下FDK_AAC比NeroAAC以及faac和
voaac的音质都要好一些。
下面先到SourceForge上下载稳定版本的FDK_AAC:
https:sourceforge.netpopencore-amrfdk-aacciv0.1.4tree然后在根目录下建立build_armv7.sh脚本,在里面写入以下内容:
.configure \--enable-static \--disable-shared \--host=arm-apple-darwin \--prefix=FDK_ROOT_DIRthinarmv7
CC=xcrun -sdk iphoneos clang AS=gas-preprocessor.pl CC
CFLAGS=-arch armv7 -mios-simulator-version-min=7.0 LDFLAGS=-arch armv7 -mios-simulator-version-min=7.0
make clean
make -j8
make install
FDK_AAC的配置选项中要求比LAME多配置一项AS参数,并且需
要安装gas-preprocessor,首先进入下方链接:
https:github.comapplexiaohaogas-preprocessor
下载gas-preprocessor.pl,然后复制到usrlocalbin目录下,修
改usrlocalbingas-preprocessor.pl的文件权限为可执行权限:
chmod 777 usrlocalbingas-preprocessor.pl
这样gas-preprocessor.pl就安装成功了,再次执行上面的Shell脚本,成功之后就可以在thinarmv7目录(当然需要提前建立好该目录)下看
到include和lib这两个目录,在使用该库时,include目录下包含了编译阶
段需要用到的头文件,而lib目录下包含了链接阶段需要用到的静态库文
件。类似于上面的脚本,也可以编译arm64以及i386平台下的静态库,最后再用lipo工具合并静态库文件,其实也可以编写一个Shell脚本完成
上述所有事情,具体可以查看代码仓库中的完整编译脚本。
3.X264的交叉编译
本节将介绍X264开源库的交叉编译,和之前的章节一样,在做交
叉编译之前先向大家介绍一下X264库。
X264简介X264是一个开源的H.264MPEG-4 AVC视频编码函数库,是最好的
有损视频编码器之一。一般的输入是视频帧的YUV表示,输出是编码之
后的H264的数据包,并且支持CBR、VBR模式,可以在编码的过程中
直接改变码率的设置,这在直播的场景中是非常实用的(直播场景下利
用该特点可以做码率自适应)。
可以到下面这个网站上获取X264的源码:
http:www.videolan.orgdevelopersx264.html
当然也可以直接执行:
git clone git:git.videolan.orgx264.git
同样,在根目录下建立build_armv7.sh脚本,然后在里面写入如下
内容:
!binsh
export AS=gas-preprocessor.pl -arch arm -- xcrun -sdk iphoneos clang
export CC=xcrun -sdk iphoneos clang
.configure \--enable-static \--enable-pic \--disable-shared \--host=arm-apple-darwin \--extra-cflags=-arch armv7 -mios-version-min=7.0 \--extra-asflags=-arch armv7 -mios-version-min=7.0 \--extra-ldflags=-arch armv7 -mios-version-min=7.0 \--prefix=.thinarmv7
make clean
make -j8
make install
在执行上述Shell脚本之前,要求在当前目录下预先建立好
thinarmv7目录,然后执行脚本,这样就可以看到在armv7目录下产生的
对应的include及lib目录了,当然也有bin目录,各个目录里面存放的内
容,前面已经介绍过很多遍了,在此不再赘述。对于arm64以及i386架
构平台的编译,在代码仓库中会有一个Shell脚本,它可以编译出所有架
构平台下的静态库,并且已经用lipo工具合并成了一个静态库。2.3.3 Android平台交叉编译的实践
1.深入了解Android NDK
Android原生开发包(NDK)可用于Android平台上的C++开发,NDK不仅仅是一个单一功能的工具,还是一个包含了API、交叉编译
器、链接程序、调试器、构建工具等的综合工具集。
下面大致列举了一下经常会用到的组件。
·ARM、x86的交叉编译器
·构建系统
·Java原生接口头文件
·C库
·Math库
·最小的C++库
·ZLib压缩库
·POSIX线程
·Android日志库
·Android原生应用API
·OpenGL ES(包括EGL)库
·OpenSL ES库
下面来看一下Android所提供的NDK根目录下的结构。
·ndk-build:该Shell脚本是Android NDK构建系统的起始点,一般在项目中仅仅执行这一个命令就可以编译出对应的动态链接库了,后面会
有详细的介绍。
·ndk-gdb:该Shell脚本允许用GUN调试器调试Native代码,并且可
以配置到Eclipse的IDE中,可以做到像调试Java代码一样调试Native的代
码。
·ndk-stack:该Shell脚本可以帮助分析Native代码崩溃时的堆栈信
息,后续会针对Native代码的崩溃进行详细的分析。
·build:该目录包含NDK构建系统的所有模块。
·platforms:该目录包含支持不同Android目标版本的头文件和库文
件,NDK构建系统会根据具体的配置来引用指定平台下的头文件和库文
件。
·toolchains:该目录包含目前NDK所支持的不同平台下的交叉编译
器——ARM、x86、MIPS,其中比较常用的是ARM和x86。构建系统会
根据具体的配置选择不同的交叉编译器。
在了解了NDK的目录结构之后,接下来详细了解一下NDK的编译
脚本语法——Android.mk和Application.mk。
Android.mk是在Android平台上构建一个C或者C++语言编写的程序
系统的Makefile文件,不同的是,Android提供了一系列的内置变量来提
供更加方便的构建语法规则。Application.mk文件实际上是对应用程序
本身进行描述的文件,它描述了应用程序要针对哪些CPU架构打包动态
so包、要构建的是release包还是debug包以及一些编译和链接参数等。
(1)Android.mk
Android.mk分为以下几部分。
·LOCAL_PATH:=(call my-dir),返回当前文件在系统中的路
径,Android.mk文件开始时必须定义该变量。
·include(CLEAR_VARS),表明清除上一次构建过程的所有全
局变量,因为在一个Makefile编译脚本中,会使用大量的全局变量,使
用这行脚本表明需要清除掉所有的全局变量。·LOCAL_SRC_FILES,要编译的C或者Cpp的文件,注意这里不需
要列举头文件,构建系统会自动帮助开发者依赖这些文件。
·LOCAL_STATIC_LIBRARIES,所依赖的静态库文件。
·LOCAL_LDLIBS:=-L(SYSROOT)usrlib-llog-lOpenSLES-
lGLESv2-lEGL-lz,指定编译过程所依赖的NDK提供的动态与静态库,SYSROOT变量代表的是NDK_ROOT下面的目录
NDK_ROOTplatformsandroid-18arch-arm,而在这个目录的usrlib目
录下有很多对应的so的动态库以及.a的静态库。
·LOCAL_CFLAGS,编译C或者Cpp的编译标志,在实际编译的时
候会发送给编译器。比如常用的实例是加上-DAUTO_TEST,然后在代
码中就可以利用条件判断ifdef AUTO_TEST来做一些与自动化测试相
关的事情。
·LOCAL_LDFLAGS,链接标志的可选列表,当对目标文件进行链
接以生成输出文件的时候,将这些标志带给链接器。该指令与
LOCAL_LDLIBS有些类似,一般情况下,该选项会用于指定第三方编
译的静态库,LOCAL_LDLIBS经常用于指定系统的库(比如log、OpenGL ES、EGL等)。
·LOCAL_MODULE,该模块的编译的目标名,用于区分各个模
块,名字必须是唯一并且不包含空格的,如果编译目标是so库,那么该
so库的名字就是lib项目名.so。
·include(BUILD_SHARED_LIBRARY),其实类似的include还有
很多,都是构建系统提供的内置变量,该变量的意义是构建动态库,其
他的内置变量还包括如下几种。
·---BUILD_STATIC_LIBRARY:构建静态库。
·---PREBUILT_STATIC_LIBRARY:对已有的静态库进行包装,使
其成为一个模块。
·---PREBUILT_SHARED_LIBRARY:对已有的动态库进行包装,使其成为一个模块。·---BUILD_EXECUTABLE:构建可执行文件。
构建系统提供的这些内置变量在哪里能够看到呢?它们都在
NDK_ROOTbuildcore目录下,这里面会有所有预先定义好的
Makefile,开发者include一个变量,实际上就是把对应的Makefile包含到
Android.mk中,包括前面提到的CLEAR_VARS,其也是该目录下面的
一个Makefile。
·include(call all-makefiles-under,(LOCAL_PATH)),也是
构建系统提供的变量,该命令会返回该目录下所有子目录的Android.mk
列表。
上面已经清楚地讲解了Android.mk里的基本语法规则,那么,在输
入命令ndk-build之后,系统到底会使用哪些编译器以及打包器和链接器
来编译我们的程序呢?
会使用NDK_ROOTtoolchainsarm-linux-androideabi-
4.8prebuiltdarwin-x86_64bin目录(以Mac平台为例)下面的gcc、g++、ar、ld等工具。同样在该目录下的strip工具将会用于清除so包里面
的源码,nm工具可以供开发者查看静态库下的符号表。
那么进行gcc编译的时候,头文件将放在哪里呢?
NDK_ROOTplatformsandroid-18arch-armusrinclude目录下会存
放编译过程所依赖的头文件。
那么在链接过程中,经常使用的log或者OpenSL ES以及OpenGL ES
等库又将放在哪里呢?
答案其实已在前文中提到过,NDK_ROOTplatformsandroid-
18arch-armusrlib目录下会存放链接过程中所依赖的库文件。
(2)Application.mk
Application.mk分为以下几个部分。
·APP_ABI:=XXX,这里的XXX是指不同的平台,可以选填的有
x86、mips、armeabi、armeabi-v7a、all等,值得一提的是,若选择all则
会构建出所有平台的so,如果不填写该项,那么将默认构建为armeabi平台下的库。由于工作的原因,笔者和Intel的员工打过交道,构建
armeabi-v7a平台的so之所以可以运行在Intel x86架构的CPU平台下,是
因为Intel针对armeabi做了兼容,但是如果想要应用以最小的能耗、最高
的效率运行在Intel x86平台上,则还是要指定构建的so为x86平台。因
此,如果想要提高App的运行性能,则还需要编译出x86平台。类似于
前面介绍的iOS平台,如果不考虑模拟器的话,则仅需要构建armv7与
arm64平台架构,那么对于Android平台呢?对于armv7-a,肯定是要编
译的;至于arm64-v8a这个平台,其实已经占到了50%以上,最好也将其
单独编译出来;同时armv5这个平台的设备还是存在的,当然不同App
在不同架构下的比例也不尽相同,读者可以根据实际场景来决定编译的
平台数目。这里需要注意的是,编译arm64-v8a的时候使用的交叉工具
编译链与之前的armv7所在的目录有比较大的差异,其目录存在于:
·NDK_ROOTtoolchainsaarch64-linux-android-4.9prebuiltdarwin-
x86_64bin
·编译过程中使用的编译工具都存在于上述目录下。
·APP_STL:=gnustl_static,NDK构建系统提供了由Android系统给
出的最小C++运行时库(systemliblibstdc++.so)的C++头文件。然而,NDK带有另一个C++实现,开发者可以在自己的应用程序中使用或链接
它,定义APP_STL可选择它们中的一个,可选项包括:stlport_static、stlport_shared、gnustl_static。
·APP_CPPFLAGS:=-std=gnu++11-fexceptions,指定编译过程的
flag,可以在该选项中开启exception rtti等特性,但是为了效率考虑,最
好关闭rtti。
·NDK_TOOLCHAIN_VERSION=4.8,指定交叉工具编译链里面的
版本号,这里指定使用4.8。
·APP_PLATFORM:=android-9,指定创建的动态库的平台。
·APP_OPTIM:=release,该变量是可选的,用来定
义“release”或“debug”,“release”模式是默认的,并且会生成高度优化的
二进制代码;“debug”模式生成的是未优化的二进制代码,但是可以检
测出很多的BUG,经常用于调试阶段,也相当于在ndk-build指令后边直
接加上参数NDK_DEBUG=1。2.如何交叉编译
上文讲解了NDK的结构,以及构建系统的基本语法。本节将直接对
LAME、FDK_AAC、X264这三个库进行交叉编译。
(1)LAME的交叉编译
在Android的编译中,一般情况下会使用一个Shell脚本文件,指定
好编译器里面的各个工具,然后把对应的Configure的命令与选项开关配
置好,最后执行该Shell脚本:
!binbash
NDK_ROOT=Usersapplesoftandroidandroid-ndk-r9b
PREBUILT=NDK_ROOTtoolchainsarm-linux-androideabi-4.6prebuiltdarwin-x86_64
PLATFORM=NDK_ROOTplatformsandroid-9arch-arm
export PATH=PATH:PREBUILTbin:PLATFORMusrinclude:
export LDFLAGS=-LPLATFORMusrlib -LPREBUILTarm-linux-androideabilib
-march=armv7-a
export CFLAGS=-IPLATFORMusrinclude -march=armv7-a -mfloat-abi=softfp -mfpu=vfp
-ffast-math -O2
export CPPFLAGS=CFLAGS
export CFLAGS=CFLAGS
export CXXFLAGS=CFLAGS
export LDFLAGS=LDFLAGS
export AS=PREBUILTbinarm-linux-androideabi-as
export LD=PREBUILTbinarm-linux-androideabi-ld
export CXX=PREBUILTbinarm-linux-androideabi-g++ --sysroot={PLATFORM}
export CC=PREBUILTbinarm-linux-androideabi-gcc --sysroot={PLATFORM}
-march=armv7-a
export NM=PREBUILTbinarm-linux-androideabi-nm
export STRIP=PREBUILTbinarm-linux-androideabi-strip
export RANLIB=PREBUILTbinarm-linux-androideabi-ranlib
export AR=PREBUILTbinarm-linux-androideabi-ar
.configure --host=arm-linux \--disable-shared \--disable-frontend \--enable-static \--prefix=.armv7a
make clean
make -j8
make install
下面就来针对该脚本的每一行命令进行详细解释。
第一部分是设置NDK_ROOT,并且声明platform和prebuilt,最终配置可在环境变量中查看。
第二部分主要是声明CFLAGS与LDFLAGS,其目的是在编译和链
接阶段找到正确的头文件与链接到正确的库文件。这里需要特别注意的
是,在这两个设置的后边都加上了-march=armv7-a,这相当于是让编译
器知道要编译的目标平台是armv7-a。
第三部分是声明CC、AS、AR、LD、NM、STRIP等工具,具体每
一个工具是做什么用的,前面都已经介绍过了,如果要编译armv5、x86
或者arm64-v8a,那么在代码仓库中会提供全量编译的Shell脚本文件。
第四部分就是使用LAME本身的Configure进行编译裁剪。
第五部分就是使用标准的编译链接和安装。
最终执行脚本成功之后,可以看到在指定的Prefix目录下面,包含
了lib和include目录,里面分别是静态库文件和头文件,这两个目录的作
用在前面已经说过很多遍了,在此不再赘述。
(2)FDK_AAC的交叉编译
!binbash
.configure --host=armv7a \--enable-static \--disable-shared \--prefix=.armv7a
make clean
make -j8
make install
注:这里没有给出声明NDK_ROOT以及各个编译工具的代码。
其实FDK_AAC的交叉编译并没有什么可解说的,配置好环境变量
之后,执行Configure,然后安装就可以了。
(3)X264的交叉编译
!binbash
.configure --prefix=PREFIX \--enable-static \--enable-pic \--enable-strip \--disable-cli \--disable-asm \--extra-cflags=-march=armv7-a -O2 -mfloat-abi=softfp -mfpu=neon \--host=arm-linux \--cross-prefix=PREBUILTbinarm-linux-androideabi- \--sysroot=PLATFORM
注:这里没有给出声明NDK_ROOT以及各个编译工具的代码。
对于X264的编译裁剪,这里有如下几个关键点。
·extra-cflags选项的配置。针对armv7-a的CPU打开了NEON的优化运
行指令,并且打开了O2编译优化,这是非常重要的一点。
·--disable-asm选项的配置。如果不禁用掉asm指令,则意味着将会
禁止neon的指令。2.3.4 使用LAME编码MP3文件
2.3.3节为两个平台交叉编译了多个库,本节就以上文编译出来的
LAME库进行编码工作。本节要实现的目标是,在添加好C++支持的项
目中加入编码MP3文件的功能。当点击按钮的时候,输入的是一个PCM
文件的路径和一个MP3的路径,等运行完毕,电脑上的播放器直接就可
以播放该MP3文件。
首先,用一个C++的类来实现其业务逻辑,输入就是两个char指针
类型的路径,编码成功之后,输出一行编码成功的Log,然后在之前的
项目基础上集成进这段代码,再运行并测试。
1.编码工具类的编写
首先新建两个文件:mp3_encoder.h和mp3_encoder.cpp。
先看一下头文件应该如何编写,头文件其实就是用于定义该类对外
提供的接口。
这里提供的是一个Init接口,输入的是一个PCM FilePath和一个MP3
FilePath,会判定输入文件是否存在、初始化LAME以及初始化输出文件
的资源,返回值是该函数是否成功初始化了所有的相关资源,成功则返
回true,否则返回false。
此外,还要再提供一个encode方法,负责读取PCM数据,并且调用
LAME进行编码,然后将编码之后的数据写入文件。
最后再对外提供一个销毁资源的接口destroy方法,用于关闭所有的
资源。
按照上述分析,建立头文件如下:
class Mp3Encoder {
private:
FILE pcmFile;
FILE mp3File;
lame_t lameClient; public:
Mp3Encoder;
~Mp3Encoder;
int Init(const char pcmFilePath, const char mp3FilePath, int
sampleRate, int channels, int bitRate);
void Encode;
void Destory;
};
既然头文件已经定义好了,接下来就一起来实现。
首先是Init方法的实现,以读二进制文件的方式打开PCM文件,以
写入二进制文件的方式打开MP3文件,然后初始化LAME。具体代码如
下:
int Mp3Encoder::Init(const char pcmFilePath, const char
mp3FilePath, int sampleRate, int channels, int bitRate) {
int ret = -1;
pcmFile = fopen(pcmFilePath, rb);
if(pcmFile) {
mp3File = fopen(mp3FilePath, wb);
if(mp3File) {
lameClient = lame_init;
lame_set_in_samplerate(lameClient, sampleRate);
lame_set_out_samplerate(lameClient, sampleRate);
lame_set_num_channels(lameClient, channels);
lame_set_brate(lameClient, bitRate 1000);
lame_init_params(lameClient);
ret = 0;
}
}
return ret;
}
其次是Encode方法的实现,函数主体是一个循环,每次都会读取一
段bufferSize大小的PCM数据buffer,然后再编码该buffer,但是在编码
buffer之前得把该buffer的左右声道拆分开,再送入到LAME编码器,最
后将编码之后的数据写入MP3文件中。具体代码如下:
void Mp3Encoder::Encode {
int bufferSize = 1024 256;
short buffer = new short[bufferSize 2];
short leftBuffer = new short[bufferSize 4];
short rightBuffer = new short[bufferSize 4];
unsigned char mp3_buffer = new unsigned char[bufferSize];
size_t readBufferSize = 0;
while ((readBufferSize = fread(buffer, 2, bufferSize 2, pcmFile)) > 0) {
for (int i = 0; i < readBufferSize; i++) {
if (i % 2 == 0) { leftBuffer[i 2] = buffer[i];
} else {
rightBuffer[i 2] = buffer[i];
}
}
size_t wroteSize = lame_encode_buffer(lameClient, (short
int ) leftBuffer, (short int ) rightBuffer,(int)(readBufferSize 2), mp3_buffer, bufferSize);
fwrite(mp3_buffer, 1, wroteSize, mp3File);
}
delete[] buffer;
delete[] leftBuffer;
delete[] rightBuffer;
delete[] mp3_buffer;
}
最后是Destroy方法,关闭PCM文件,关闭MP3文件,销毁LAME。
具体代码如下:
void Mp3Encoder::Destory {
if(pcmFile) {
fclose(pcmFile);
}
if(mp3File) {
fclose(mp3File);
lame_close(lameClient);
}
}
实现结束之后,接下来就是把该类集成到iOS和Android客户端。
2.iOS集成
首先打开2.1节开发的项目——添加了C++支持的iOS工程。
然后在ViewController.mm中直接实例化C++类型的类Mp3Encoder,将vocal.pcm的文件放入沙盒中,利用下面这行代码获取PCM的路径:
[[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent: @vocal.pcm];
接着利用下面的代码获取最终要写入的MP3文件的路径:
NSArray paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES);
NSString documentsDirectory = [paths objectAtIndex:0];[documentsDirectory stringByAppendingPathComponent:@vocal.mp3];
最后直接调用Mp3Encoder类的encode方法,编码音频文件。最终编
码方法执行结束,不要忘记调用该C++类的destroy方法,销毁所有资
源。然后运行程序,点击编码按钮,等待编码结束之后,通过
Xcodedevice中的Download沙盒功能,将MP3文件提取出来,读者可以
使用电脑默认的播放器播放该MP3文件,试试看是否能正常播放。
3.Android集成
首先打开2.2节开发的项目——添加了C++支持的Android工程。
然后在com.phuket.tour.studio包下修改Mp3Encoder文件,并写入三
个native方法:
public native int init(String pcmPath, int audioChannels, int bitRate, int
sampleRate, String mp3Path);
public native void encode;
public native void destroy;
接着重新生成JNI接口文件,并且在实现的C++文件中实现这三个
方法。
在init方法中实例化Mp3Encoder类,然后调用初始化方法:
const char pcmPath = env->GetStringUTFChars(pcmPathParam, NULL);
const char mp3Path = env->GetStringUTFChars(mp3PathParam, NULL);
encoder = new Mp3Encoder;
encoder->Init(pcmPath, mp3Path, sampleRate, channels, bitRate);
env->ReleaseStringUTFChars(mp3PathParam, mp3Path);
env->ReleaseStringUTFChars(pcmPathParam, pcmPath);
在encode方法中直接调用Mp3Encoder的encode方法;在destroy方法
中则调用Mp3Encoder的destroy方法;最后,修改Android.mk文件,将最
新的C++文件加入LOCAL_SRC中,并且在include的路径中增加LAME
的头文件所在的路径:
LOCAL_C_INCLUDES := (LOCAL_PATH)..3rdpartylameinclude当然,还需要在链接阶段加入编译出来的静态库,因此需要键入以
下命令:
LOCAL_LDLIBS += -L(LOCAL_PATH)3rdpartylamelib -lmp3lame
这样重新执行ndk-build命令就可以将最新的so库打出来了。 [1]
现在,在MainActivity中,传入正确的输入路径和输出路径(需要
提前准备一段PCM数据放入输入路径下面),等编码结束之后,取出输
出路径的MP3文件,在电脑上播放,确认一下是否正确。
[1] so库是gcc或g++编译过后形成的一种动态库,最终可以被加载到程
序中使用。2.4 本章小结
本章主要介绍了如何创建一个Android项目和一个iOS项目,然后为
这两个项目增加C++支持,接着讨论了交叉编译,最后利用交叉编译出
来的LAME库,在两个平台上完成编码MP3音频文件的功能。本章的内
容是音视频开发的基础内容,希望读者熟练掌握。第3章 FFmpeg的介绍与使用
若要讲解音视频的开发,首先不得不提开源框架FFmpeg。该开源
框架为音视频开发者们提供了非常大的帮助,其也是全世界的音视频开
发工程师都应该掌握的工具。FFmpeg是一套可以用来记录、处理数字
音频、视频,并将其转换为流的开源框架,采用LPL或GPL许可证,提
供了录制、转换以及流化音视频的完整解决方案。它的可移植性或者说
跨平台特性非常强大,可以用在Linux服务器、PC(包括Windows、Mac OS X等)、移动端设备(Android、iOS等移动设备)等平台。名称
中的mpeg来自视频编码标准MPEG,而前缀FF是Fast Forward的首字母
缩写。本章会从编译开始讲解,然后介绍命令行工具的使用,接着会介
绍FFmpeg在代码层面提供给开发者的API,最后会从源码的角度分析一
下整个FFmpeg框架,现在就让我们开始吧。3.1 FFmpeg的编译与命令行工具的使用
3.1.1 FFmpeg的编译
1.FFmpeg编译选项详解
首先到FFmpeg官网上下载稳定版本的FFmpeg源码,本章将会从下
载到的最干净的代码开始逐步进行操作。然后将下载的源码解压到一个
目录中,FFmpeg与大部分GNU软件的编译方式类似,都是通过
configure脚本来实现编译前定制的,这种方式允许用户在编译前对软件
进行裁剪,同时通过对最终运行到的系统以及目标平台的配置来决定对
某些模块设定合适的配置。configure脚本运行完毕之后,会生成
config.mk和config.h这两个文件,分别作用到makefile和源代码的层次,由这两个部分协同实现对编译选项的控制。所以下面先来看看configure
的脚本,可以利用它的help命令来查看其到底提供了哪些选项?
.configure -help
·标准选项:GNU软件例行配置项目,例如安装路径、--prefix=…
等。
·编译、链接选项:默认配置是生成静态库而不是生成动态库,例
如--disable-static、--enable-shared等。
·可执行程序控制选项:决定是否生成FFmpeg、ffplay、ffprobe和
ffserver等。
·模块控制选项:裁剪编译模块,包括整个库的裁剪,例如--disable-
avdevice;一组模块的筛选,例如--disable-decoders;单个模块的裁剪,例如--disable-demuxer。
·能力展示选项:列出当前源代码支持的各种能力集,例如--list-
decoders、--list-encoders。
·其他:允许开发者深度定制,如交叉编译环境配置、自定义编译器参数的设定等。
下面先给出一个总览,大体了解一下FFmpeg的整体结构,如图3-1
所示。
图 3-1
下面这段代码是一个配置实例,用于实现运行于Android系统中的
FFmpeg库的编译:
.configure -prefix=. \--cross-prefix=NDK_TOOLCHAIN_PREFIX \--enable-cross-compile \--arch=arm --target-os=linux \--disable-static -enable-shared \--disable-ffmpeg --disable-ffplay -disable-ffserver -disable-ffprobe
默认的编译会生成4个可执行文件和8个静态库。可执行文件包括用
于转码、推流、Dump媒体文件的ffmpeg、用于播放媒体文件的ffplay、用于获取媒体文件信息的ffprobe,以及作为简单流媒体服务器的
ffserver。8个静态库其实就是FFmpeg的8个模块,具体包括如下内容。
·AVUtil:核心工具库,该模块是最基础的模块之一,下面的许多
其他模块都会依赖该库做一些基本的音视频处理操作。
·AVFormat:文件格式和协议库,该模块是最重要的模块之一,封
装了Protocol层和Demuxer、Muxer层,使得协议和格式对于开发者来说
是透明的。
·AVCodec:编解码库,该模块也是最重要的模块之一,封装了
Codec层,但是有一些Codec是具备自己的License的,FFmpeg是不会默
认添加像libx264、FDK-AAC、lame等库的,但是FFmpeg就像一个平台
一样,可以将其他的第三方的Codec以插件的方式添加进来,然后为开
发者提供统一的接口。
·AVFilter:音视频滤镜库,该模块提供了包括音频特效和视频特效
的处理,在使用FFmpeg的API进行编解码的过程中,直接使用该模块为
音视频数据做特效处理是非常方便同时也非常高效的一种方式。
·AVDevice:输入输出设备库,比如,需要编译出播放声音或者视
频的工具ffplay,就需要确保该模块是打开的,同时也需要libSDL的预
先编译,因为该设备模块播放声音与播放视频使用的都是libSDL库。
·SwrRessample:该模块可用于音频重采样,可以对数字音频进行
声道数、数据格式、采样率等多种基本信息的转换。
·SWScale:该模块是将图像进行格式转换的模块,比如,可以将
YUV的数据转换为RGB的数据。
·PostProc:该模块可用于进行后期处理,当我们使用AVFilter的时
候需要打开该模块的开关,因为Filter中会使用到该模块的一些基础函
数。
如果是比较老的FFmpeg版本,那么有可能还会编译出来avresample
模块,该模块其实也是用于对音频原始数据进行重采样,但是现在已经
被废弃掉了,不再推荐使用该库,而是使用swrresample库进行替代。
如何为FFmpeg平台引入第三方编解码库呢?下面就以最常用的LAME、X264、FDK-AAC进行举例。前面的章节中已经介绍了这三个
库在Android和iOS平台上的交叉编译,现在就假设已经交叉编译出了
LAME、X264、FDK-AAC的静态库与头文件,并且在FFmpeg的源码目
录下建立了external-libs目录,还在其中建立了LAME、X264、FDK-
AAC三个目录,每个目录中的结构都包含了include和lib两个目录,并且
将编译出来的头文件和静态库文件分别都放到了这两个目录下面。
现在修改编译脚本如下。
新增X264编码器需要新增以下脚本:--enable-muxer=h264 \--enable-encoder=libx264 \--enable-libx264 \--extra-cflags=”-Iexternal-libsx264include” \--extra-ldflags=”-Lexternal-libsx264lib” 新增LAME编码器需要新增以下脚本:--enable-muxer=mp3 \--enable-encoder=libmp3lame \--enable-libmp3lame \--extra-cflags=”-Iexternal-libslameinclude” \--extra-ldflags=”-Lexternal-libslamelib” 新增FDK-AAC编码器需要新增以下脚本:--enable-encoder=libfdk_aac \--enable-libfdk_aac \--extra-cflags=”-Iexternal-libsfdk-aacinclude” \--extra-ldflags=”-Lexternal-libsfdk-aaclib” 读者可以按照自己的应用场景,把需要编译进来的第三方库以修改
脚本文件的方式进行编译,然后以命令行模式或者以API调用的方式进
行使用。
在FFmpeg中,有一个类型的filter称为bit stream filter,要想在开发
过程中使用该filter,则需要在编译的过程中打开它。该filter存在的意义
主要是应对某些格式的封装转换行为。比如AAC编码,常见的有两种封
装格式:一种是ADTS格式的流,是AAC定义在MPEG2里面的格式;另外一种是封装在MPEG4里面的格式,这种格式会在每一帧前面拼接一
个用声道、采样率等信息组成的头。开发者完全可以手动拼接该头信
息,即将AAC编码器输出的原始码流(ADTS头+ES流)封装进MP4、FLV或者MOV等格式的容器中时,需要先将ADTS头转换为MPEG-4
AudioSpecficConfig(描述了编码器的配置参数)头,并去掉原始码流
中的ADTS头(只剩下ES流)。但是使用FFmpeg提供好的aac_adtstoasc
类型的bit stream filter可以非常方便地进行转换,FFmpeg为开发者隐藏
了实现的细节,并且提供了更好的代码可读性。若想要正常使用这个
filter,则需要在编译过程中打开下面这个选项:--enable-bsf=aac_adtstoasc
AAC的bit stream filter常常应用在编码的过程中。与音频的AAC编
码格式相对应的是视频中的H264编码,它也有两种封装格式:一种是
MP4封装的格式;一种是裸的H264格式(一般称为annexb封装格式)。
FFmpeg中也提供了对应的bit stream filter,称为H264_mp4toannexb,可
以将MP4封装格式的H264数据包转换为annexb封装格式的H264数据
(其实就是裸的H264的数据)包。当然,也可以手动写代码来实现这
件事情,但是既然FFmpeg提供了这样好用的模块,我们为什么不用
呢?要使用它也只需要在编译过程中打开下面这个选项即可:--enable-bsf=h264_mp4toannexb
H264的bit stream filter常常应用于视频解码过程中,特别是后期在
讲解使用各个平台上提供的硬件解码器时,一定会用到该bit stream
filter。
2.FFmpeg的交叉编译
第2章已经介绍了交叉编译的原理,并且交叉编译出了LAME、FDK_AAC、X264等第三方库,本章也已经介绍了FFmpeg中Configure
的大部分关键语法与定义,因此这里就直接开始交叉编译FFmpeg吧!
不过,对应于Android和iOS平台会有一些通用的配置选项,这里先列出
通用的配置选项,代码如下:
CONFIGURE_FLAGS=--disable-shared \--enable-static \--disable-stripping \--disable-ffmpeg \--disable-ffplay \--disable-ffserver \--disable-ffprobe \--disable-avdevice \--disable-devices \--disable-indevs \--disable-outdevs \--disable-debug \--disable-asm \--disable-yasm \--disable-doc \--enable-small \--enable-dct \--enable-dwt \--enable-lsp \--enable-mdct \--enable-rdft \--enable-fft \--enable-version3 \--enable-nonfree \--disable-filters \--disable-postproc \--disable-bsfs \--enable-bsf=aac_adtstoasc \--enable-bsf=h264_mp4toannexb \--disable-encoders \--enable-encoder=pcm_s16le \--enable-encoder=aac \--enable-encoder=libvo_aacenc \--disable-decoders \--enable-decoder=aac \--enable-decoder=mp3 \--enable-decoder=pcm_s16le \--disable-parsers \--enable-parser=aac \--disable-muxers \--enable-muxer=flv \--enable-muxer=wav \--enable-muxer=adts \--disable-demuxers \--enable-demuxer=flv \--enable-demuxer=wav \--enable-demuxer=aac \--disable-protocols \--enable-protocol=rtmp \--enable-protocol=file \--enable-libfdk_aac \--enable-libx264 \--enable-cross-compile \--prefix=INSTALL_DIR
可以看到为了达到最小的包体积,需要先关掉所有的模块,然后再
打开具体的编解码器、解析器、解复用器、协议,并且这里开启了两个
第三方的Codec:一个是FDK_AAC;另外一个是X264。此处还关闭了命令行工具与帮助文档、输入输出设备、动态库生成,同时还打开了静
态库的生成。由于要进行交叉编译,所以要在倒数第二行打开交叉编译
的选项,在最后一行指定安装库的目标目录。
对于Android平台来讲,Shell脚本需要添加如下内容:
ANDROID_NDK_ROOT=Usersapplesoftandroidandroid-ndk-r9b
PREBUILT=ANDROID_NDK_ROOTtoolchainsarm-linux-androideabi-4.8prebuiltdarwin-x86_64
PLATFORM=ANDROID_NDK_ROOTplatformsandroid-8arch-arm
.configure CONFIGURE_FLAGS \--target-os=linux \--arch=arm \--cross-prefix=PREBUILTbinarm-linux-androideabi- \--sysroot=PLATFORM \--extra-cflags=-marm -march=armv7-a -Ifdk_aacinclude -Ix264 include \--extra-ldflags=-marm -march=armv7-a -Lfdk_aaclib -Lx264 lib
可以看到,上述脚本中指定了运行的平台与架构,指定了编译器、链接器的前缀,以用于执行真正的编译与链接操作,然后给出sysroot和
编译、链接参数。这里是以armv7a作为编译目标架构的,如果读者想自
行编译armv8或者x86的平台,可以查看代码仓库中的编译脚本,这里不
再列举。
对于iOS平台来讲,Shell脚本需要添加如下内容:
.configure CONFIGURE_FLAGS \--target-os=darwin--cc=xcrun -sdk iphoneos clang \--arch=armv7 \--extra-cflags=-arch armv7 -mios-version-min=7.0 -Ifdk_aacinclude
-Ix264include \--extra-ldflags=-arch armv7 -mios-version-min=7.0 -Lfdk_aaclib -Lx264 lib
可以看到,上述脚本中设置了编译器以及目标运行平台,需要说明
的是,这里指定了最小的iOS的运行平台是7.0,否则将这个库集成到
Xcode中的时候会遇到平台不匹配的警告,此外,需要注意的是这里并
没有打开bitcode,这会导致集成进入Xcode之后必须将项目的bitcode选
项关闭掉。若要为编译的库打开bitcode选项,那么在编译参数中增加下
面这行参数就可以了:-fembed-bitcode
3.FFmpeg命令行工具的编译与安装
在介绍FFmpeg的命令行之前,应先安装FFmpeg,前面已经介绍过
了FFmpeg的编译,但那是基于Android平台的交叉编译,而不是安装在
PC上的工具,虽然有些读者可能会说我们做的是移动端上的开发,和
PC上的FFmpeg有什么关系呢?但是不可否认的是,开发者用于开发的
机器上有一套强大的音视频工具,对开发移动端上的音视频项目是非常
重要的。相信有了上文的介绍,在这里介绍FFmpeg的配置与安装应该
会很轻松。
下面给出一个编译脚本config_pc.sh:
!binbash
.configure \--enable-gpl \--disable-shared \--disable-asm \--disable-yasm \--enable-filter=aresample \--enable-bsf=aac_adtstoasc \--enable-small \--enable-dct \--enable-dwt \--enable-lsp \--enable-mdct \--enable-rdft \--enable-fft \--enable-static \--enable-version3 \--enable-nonfree \--enable-encoder=libfdk_aac \--enable-encoder=libx264 \--enable-decoder=mp3 \--disable-decoder=h264_vda \--disable-d3d11va \--disable-dxva2 \--disable-vaapi \--disable-vda \--disable-vdpau \--disable-videotoolbox \--disable-securetransport \--enable-libx264 \--enable-libfdk_aac \--enable-libmp3lame \--extra-cflags=-Ipc_fdk_aacinclude -Ix264_pcinclude -Ipc_lameinclude \--extra-ldflags=-Lpc_fdk_aaclib -Lx264_pclib -Lpc_lamelib \--prefix='UsersappleDesktopffmpegtmp_1'如果该文件没有执行权限,那么请执行以下命令为该文件增加执行
权限:
chmod a+x .config_pc.sh
然后就可以执行该Shell脚本文件,对FFmpeg进行配置了:
.config_pc.sh
该脚本执行结束之后,就来执行以下命令进行编译与安装:
make make install
安装结束之后,进入到prefix指定的目录下查看,具体会看到如下
几个目录。
·bin:编译结束的命令行工具所在的目录,后文会详细介绍该目录
下的工具。
·include:编译结束的头文件都存放在该目录下面,如果要以编写
代码的方式调用FFmpeg的API去完成工作(这也是后面会介绍的内
容),就需要把include中的目录放到includes的配置中(Android下的
makefile文件)或者Header Search Path中(iOS平台下工程文件的配置选
项)。
·lib:其中存放的是编译出来的静态库文件,其在以编写代码的方
式调用FFmpeg的API时会使用到,在编译阶段会使用到上一步提到的
include目录,而在链接阶段则会使用到这个lib目录下面的静态库了。
·share:该目录中存放了一些examples,其中展示了如何使用代码
的方式调用FFmpeg的API,其实可以切换到configure脚本所在的目录,然后执行make examples命令及make install,再到doc下面的example里找
到对应的二进制文件,这样就可以进行调试或者写出自己的测试程序
了。
有的读者可能会发现一个问题,在bin目录的下面没有ffplay,这又是为什么呢?因为ffplay实际上是客户端ffplay.c的C程序编译出来的,该
ffplay.c需要依赖avdevice模块,而avdevice模块使用了sdl的API,如果你
的PC上没有sdl(1.x版本,最常用的就是1.2.0),那么ffplay就会编译不
出来了。所以要想编译出命令行工具ffplay,首先得编译基础库sdl,可
以在自己的PC上利用安装软件包工具进行安装,以Mac OS系统为例,使用brew进行安装,如果没有brew的话,则首先安装brew,可以执行下
面命令进行Homebrew的安装:
ruby -e (curl -fsSL https:raw.githubusercontent.comHomebrewinstallmasterinstall)
等待一段时间,brew就安装好了,之后即可用brew安装sdl,执行下
述命令:
brew install sdl
等待下载并且安装完毕之后,重新执行上述FFmpeg的配置和安装
步骤,待make install结束之后,再去bin目录下就可以找到命令行工具
ffplay了。
当然,还可以使用另外一种方式为开发机器安装FFmpeg,即通过
安装包管理工具的方式进行安装。在Mac OS系统上直接在命令行下键
入以下命令:
brew install ffmpeg
可以看到brew会先下载X264作为视频的编码库,并安装成功,然
后就可以直接使用工具FFmpeg了,但是却没有工具ffplay,如果想安装
ffplay,那么执行如下命令:
brew uninstall ffmpeg
brew install ffmpeg --with-ffplay
可以看到brew会先下载sdl,然后再安装ffplay,注意使用brew安装
的FFmpeg已经是3.0以上的版本,并且使用的sdl也已经是2.0版本了。至此,关于FFmpeg的编译部分就介绍完毕了,现在回顾一下本节
的内容,本节主要介绍了如何控制FFmpeg各个模块的开关,还介绍了
如何将第三方编解码器编译到FFmpeg平台中,接着介绍了bit stream
filter类型的过滤器,然后把FFmpeg交叉编译到Android平台和iOS平台,最后在PC上成功编译出FFmpeg。3.1.2节将会介绍编译出来的FFmpeg命
令行工具,以及在工作中如何使用这些工具来提高处理音视频的效率。3.1.2 FFmpeg命令行工具的使用
前面讲解了如何安装FFmpeg相关的命令行,其中涉及ffmpeg、ffprobe、ffplay以及ffserver等命令行工具,本节将重点介绍ffmpeg、ffprobe与ffplay这三个命令行工具,而ffserver则是作为简单的流媒体服
务器存在的 ......
展晓凯 魏晓红 著
ISBN:978-7-111-58582-4
本书纸版由机械工业出版社于2018年出版,电子版由华章分社(北京华
章图文信息有限公司,北京奥维博世图书发行有限公司)全球范围内制
作与发行。
版权所有,侵权必究
客服热线:+ 86-10-68995265
客服信箱:service@bbbvip.com
官方网址:www.hzmedia.com.cn
新浪微博 @华章数媒
微信公众号 华章电子书(微信号:hzebook)目录 推荐序一
推荐序二
前言
第1章 音视频基础概念
1.1 声音的物理性质
1.1.1 声音是波
1.1.2 声波的三要素
1.1.3 声音的传播介质
1.1.4 回声
1.1.5 共鸣
1.2 数字音频
1.3 音频编码
1.4 图像的物理现象
1.5 图像的数值表示
1.5.1 RGB表示方式
1.5.2 YUV表示方式
1.5.3 YUV和RGB的转化
1.6 视频的编码方式
1.6.1 视频编码
1.6.2 编码概念
1.7 本章小结
第2章 移动端环境搭建
2.1 在iOS上如何搭建一个基础项目
2.2 在Android上如何搭建一个基础项目
2.3 交叉编译的原理与实践
2.3.1 交叉编译的原理
2.3.2 iOS平台交叉编译的实践
2.3.3 Android平台交叉编译的实践
2.3.4 使用LAME编码MP3文件
2.4 本章小结
第3章 FFmpeg的介绍与使用
3.1 FFmpeg的编译与命令行工具的使用
3.1.1 FFmpeg的编译
3.1.2 FFmpeg命令行工具的使用
3.2 FFmpeg API的介绍与使用3.3 FFmpeg源码结构
3.3.1 libavformat与libavcodec介绍
3.3.2 FFmpeg通用API分析
3.3.3 调用FFmpeg解码时用到的函数分析
3.3.4 调用FFmpeg编码时用到的函数分析
3.3.5 面向对象的C语言设计
3.4 本章小结
第4章 移动平台下的音视频渲染
4.1 AudioUnit介绍与实践
4.2 Android平台的音频渲染
4.2.1 AudioTrack的使用
4.2.2 OpenSL ES的使用
4.3 视频渲染
4.3.1 OpenGL ES介绍
4.3.2 OpenGL ES的实践
4.3.3 上下文环境搭建
4.3.4 OpenGL ES中的纹理
4.4 本章小结
第5章 实现一款视频播放器
5.1 架构设计
5.2 解码模块的实现
5.3 音频播放模块的实现
5.3.1 Android平台的音频渲染
5.3.2 iOS平台的音频渲染
5.4 画面播放模块的实现
5.4.1 Android平台的视频渲染
5.4.2 iOS平台的视频渲染
5.5 AVSync模块的实现
5.5.1 维护解码线程
5.5.2 音视频同步
5.6 中控系统串联起各个模块
5.6.1 初始化阶段
5.6.2 运行阶段
5.6.3 销毁阶段
5.7 本章小结
第6章 音视频的采集与编码
6.1 音频的采集6.1.1 Android平台的音频采集
6.1.2 iOS平台的音频采集
6.2 视频画面的采集
6.2.1 Android平台的视频画面采集
6.2.2 iOS平台的视频画面采集
6.3 音频的编码
6.3.1 libfdk_aac编码AAC
6.3.2 Android平台的硬件编码器MediaCodec
6.3.3 iOS平台的硬件编码器AudioToolbox
6.4 视频画面的编码
6.4.1 libx264编码H264
6.4.2 Android平台的硬件编码器MediaCodec
6.4.3 iOS平台的硬件编码器
6.5 本章小结
第7章 实现一款视频录制应用
7.1 视频录制的架构设计
7.2 音频模块的实现
7.2.1 音频队列的实现
7.2.2 Android平台的实现
7.2.3 iOS平台的实现
7.3 音频编码模块的实现
7.3.1 改造编码器
7.3.2 编码器适配器
7.4 画面采集与编码模块的实现
7.4.1 视频队列的实现
7.4.2 Android平台画面编码后入队
7.4.3 iOS平台画面编码后入队
7.5 Mux模块
7.5.1 初始化
7.5.2 封装和输出
7.5.3 销毁资源
7.6 中控系统串联起各个模块
7.7 本章小结
第8章 音频效果器的介绍与实践
8.1 数字音频基础
8.1.1 波形图
8.1.2 频谱图8.1.3 语谱图
8.1.4 深入理解时域与频域
8.2 数字音频处理:快速傅里叶变换
8.3 基本乐理知识
8.3.1 乐谱
8.3.2 音符的音高与十二平均律
8.3.3 音符的时值
8.3.4 节拍
8.3.5 MIDI格式
8.4 混音效果器
8.4.1 均衡效果器
8.4.2 压缩效果器
8.4.3 混响效果器
8.5 效果器实现
8.5.1 Android平台实现效果器
8.5.2 iOS平台实现效果器
8.6 本章小结
第9章 视频效果器的介绍与实践
9.1 图像处理的基本原理
9.1.1 亮度调节
9.1.2 对比度调节
9.1.3 饱和度调节
9.2 图像处理进阶
9.2.1 图像的卷积过程
9.2.2 锐化效果器
9.2.3 高斯模糊算法
9.2.4 双边滤波算法
9.2.5 图层混合介绍
9.3 使用FFmpeg内部的视频滤镜
9.3.1 FFmpeg视频滤镜介绍
9.3.2 滤镜图的构建
9.3.3 使用与销毁滤镜图
9.3.4 常用滤镜介绍
9.4 使用OpenGL ES实现视频滤镜
9.4.1 加水印
9.4.2 添加自定义文字
9.4.3 美颜效果器9.4.4 动图贴纸效果器
9.4.5 主题效果器
9.5 本章小结
第10章 专业的视频录制应用实践
10.1 视频硬件解码器的使用
10.1.1 初始化信息准备
10.1.2 VideoToolbox解码H264
10.1.3 MediaCodec解码H264
10.2 音频效果器的集成
10.2.1 Android音效处理系统的实现
10.2.2 iOS音效处理系统的实现
10.3 一套跨平台的视频效果器的设计与实现
10.4 将特效处理库集成到视频录制项目中
10.4.1 Android平台特效集成
10.4.2 iOS平台特效集成
10.5 本章小结
第11章 直播应用的构建
11.1 直播场景分析
11.2 拉流播放器的构建
11.2.1 Android平台播放器增加后处理过程
11.2.2 iOS平台播放器增加后处理过程
11.3 推流器的构建
11.4 第三方云服务介绍
11.5 礼物系统的实现
11.5.1 Cocos2dX项目的运行原理
11.5.2 关键API详解
11.5.3 实现一款动画
11.6 聊天系统的实现
11.6.1 Android客户端的WebSocket实现
11.6.2 iOS客户端的WebSocket实现
11.7 本章小结
第12章 直播应用中的关键处理
12.1 直播应用的细节分析
12.1.1 推流端细节分析
12.1.2 拉流端细节分析
12.2 推流端的关键处理
12.2.1 自适应码率的实践12.2.2 统计数据保证后续的应对策略
12.3 拉流端的关键处理
12.3.1 重试机制的实践
12.3.2 首屏时间的保证
12.3.3 统计数据保证后续的应对策略
12.4 本章小结
第13章 工欲善其事,必先利其器
13.1 Android平台工具详解
13.1.1 ADB工具的熟练使用
13.1.2 MAT工具检测Java端的内存泄漏
13.1.3 NDK工具详解
13.1.4 Native层的内存泄漏检测
13.1.5 breakpad收集线上Crash
13.2 iOS使用Instruments诊断应用
13.2.1 Debug Navigator
13.2.2 Time Profiler
13.2.3 Allocations
13.2.4 Leaks
13.3 本章小结
附录A 通过Ne10的交叉编译输入理解ndk-build
附录B 编码器的使用细节
附录C 视频的表示与编码推荐序一
我的第一部智能手机是多普达565,当时使用的是Windows Mobile
操作系统,现在看来,不管是操作交互,还是系统的整体能力,都与今
天的智能手机有着天壤之别。但是,即便是那样的操作系统,也已经足
够让当时的我认识到一个真正的操作系统能给一部随身设备赋予的强大
能力。智能手机后来这十几年的发展还是超出了很多人的预料,很难想
象,如果没有现在的高速数据网络和每个人手头的这个小终端,我们的
工作和生活会有多少不方便的地方。
生活在这个时代的程序员是足够幸运的,信息化的无限渗透也意味
着有想法、有能力的程序员对人们生活范围的影响越来越大。我与很多
资深的开发人员都有过交流,基本上能把这些人分成两类。一类是以对
技术本身的钻研为目标的技术人员,他们所关注的是架构是不是足够先
进,可扩展性如何,系统整体的负载能力,遇到错误时的鲁棒性等。总
之,他们内心的成就感来自是否把技术做到了极致,同行(或者自己)
看到的时候,会不会由衷地说这东西真棒。还有一类技术人员,他们的
成就感来自自己的工作成果是否能够直接对使用者产生影响。相对技术
本身的挑战,这类人更在乎自己所做的东西是否真正被身边的人使用,使用者用到自己作品时的感受,以及是否真正给使用者和社会带来了帮
助。两类人没有高低之分,倒有点像理论研究和应用研究的关系,两个
方向相辅相成,彼此成就,彼此推动。
音视频技术的发展正好处在理论和应用的十字路口。各种音视频技
术天生就与老百姓的生活距离很近,拍照、唱歌、小视频、瘦脸、美
颜、大混音,基本上算是大众手机里最常用的一些功能了。这些功能背
后的技术,也会因用户的需求推动而快速发展。从软件到硬件,从各种
人脸识别的算法到越来越强大的摄像头或是专用的DSP芯片,摩尔定律
在这个细分领域的发挥可以算是淋漓尽致了,这也对有志于在这个领域
发展的研发人员提出了更高的要求:一方面,要能沉得下去,音视频相
关的底层技术可以说是CS领域里相当难啃的一块硬骨头,对算法、编
码甚至是数学基础都有很高的要求;另一方面,还要能经常抬起头,不
只是要跟上相关领域的快速发展,也要理解和挖掘用户的真实需求,这
可以算是CS领域里挑战很大同时成就感也很大的困难模式了。
本书的作者展晓凯是音视频领域的权威专家。在几年间的持续研究中,他总结出了一套在音视频领域比较系统的工程实践方法,希望这些
总结能够帮助到对相关领域感兴趣的你。如果能进一步影响更多的人,将是对本书作者最大的鼓励和褒奖。
田然
2017年9月于北京推荐序二
随着智能手机的出现,音视频传感器比以往任何时候都更加接近用
户,可以说,移动互联网时代其实也是音视频内容爆炸的时代。几乎每
个应用都希望能尽可能地开启用户所有的权限,让用户把自己的每时每
刻都上传和分享出来,于是即时聊天IM几乎走进了每一个App。而这一
两年的直播大战的背后,更是让直播这种富媒体几乎变成了各类App的
标配功能,以至于市场对音视频研发人才求之若渴,音视频的学习材料
也一时洛阳纸贵,由此催生出来的各种大小公司、大小产业也是层出不
穷,业内对音视频处理的知识和经验的分享更是如火如荼。然而,音视
频处理实在是一门艰深的学问,从傅里叶变换到差分编码等各种理论,再到充满差异化甚至Bug的安卓设备的现实应用环境,既要在国内复杂
的网络环境下尽力满足用户视听观感的流畅感觉,又希望让小小的移动
设备物尽其用来为用户提供最极致的感官体验,而这些灼烧无数程序员
脑细胞的问题,实在不是一两篇长文就可以简单讲清楚的。若要让一个
几无基础的开发者能够系统地掌握音视频的处理知识,确有必要撰写一
本图书并通过具体实例来详细讲解才行。
我认识本书的作者展晓凯已经五年多了,他几乎是唱吧最勤奋的技
术人员,在唱吧浩瀚的代码库里,到处都留有他的成果。唱吧是中国颇
具影响力的K歌产品之一,从2012年雄踞苹果榜首之后,就再也没有离
开过榜单,多年来其为用户创造了无数新奇的功能和体验,从各种振奋
人心的混响,到节奏感逼人的自动说唱,所有的这些功能,都是出自展
晓凯和他所在的三五个人的小团队之手。而以唱吧的用户体量,他们自
然也遇到了许多问题,从各种花屏、黑屏、啸叫、白噪等通用技术问
题,到用户手机上因形形色色系统或硬件差异而产生的稀奇古怪的问
题,也无一不是展晓凯所在的团队逐个去解决的。可以说,发展到今
天,唱吧几乎拥有业内最丰富的音视频处理经验,而这些经验中的精
华,如今终于有机会整理付梓,实在是唱吧的一点骄傲,也是业内同行
的一份福音。
虽然与展晓凯一起并肩战斗了四五年,但我本人并没有太多机会详
细参与每一个音视频问题的处理,如今通读完这本书的原稿,我深感收
获不小。本书是展晓凯花费了无数心血的作品,其中的每一行代码每一
个实例都来自他日常工作中实际问题的总结。本书也许不是市面上唯一
一本关于音视频处理的著作,但它的出现,足以为市场带来一个特有的完整的视角,并令无数致力于打造移动设备上音视频处理完美体验的程
序员受益!
黄全能
2017年9月于北京前言
为什么要写这本书
整个音视频领域的架构以及开发已经演进了很长时间,从最开始的
广电领域,到PC端的音视频领域,再到本书所介绍的移动端的音视频
领域。尤其在这几年中,移动端音视频领域架构的变化是巨大的。在移
动互联网的发展热潮中,我有幸从事了音视频领域的设计与开发,并且
就职于最时尚的手机KTV——唱吧,这使得我开发出来的东西能够服务
于几亿用户。对于音视频的移动端的应用,不论是开发还是使用,在近
两年都达到了一个高峰,而作为一名工程师,如何高效地开发出一个音
视频App,是一件非常困难的事情,特别是对于不太了解音视频概念的
工程师。我从事软件开发已有7年多的时间,接触音视频领域也已经有5
年多,在整个开发过程中,不同的时间段会遇到不同的挑战,尤其是在
最开始涉足音视频领域的时候,真可谓举步维艰。首先,对于音视频的
基础概念不是特别清楚,再者在工作中边学边做,很难对整个音视频领
域有一个全面的了解,并且市面上没有相关成熟的资料从更高的层次来
介绍音视频领域在移动端的演进与发展。这几年的设计实战与开发经
验,以及带新人入门的众多感触,让我有了写这本书的动力,同时也形
成了这本书的核心内容,我希望通过本书可以帮助更多想要在移动端音
视频领域实现自己想法的工程师,让大家可以顺利地建立起自己的音视
频App。我非常希望能为刚入门的读者或者遇到困难的读者提供帮助,希望大家可以享受整个开发的过程,享受自己开发的产品为人们的生活
带来便利的成就感。另外,从整个音视频开发领域来讲,我也十分希望
能够通过本书贡献出自己的绵薄之力。
读者对象
·产品经理,这部分读者可以从中了解在移动端进行音视频开发会
遇到的很多问题以及对应的优化策略,例如:如何通过音视频的统计数
据为产品提供更加流畅的策略(视频观看的秒开、直播推流的流畅度、视频上传的成功率等)。
·项目经理,这部分读者可以了解很多时下流行的名词与概念,不再会因为几个专业名词就让自己不知所措,并且有助于更好地评估音视
频项目开发中的风险与进度。
·测试人员,这部分读者可以学习在音视频App中由于处理过程不同
而导致的瓶颈问题,书中也提到了一些自动化测试相关的命令以及工
具,可以对CPU的负载情况、内存的占用情况、内存泄漏问题等进行分
析。
·架构师与工程师,这部分读者只需要一点移动开发经验就可以阅
读本书了。当然如果你已经是一个高级移动开发工程师或者架构师,那
么读起本书来将更加游刃有余。再进一步,如果你已经是移动领域的音
视频开发工程师了,那么恭喜你,我们之间将会有一场关于技术领域内
部的对话。
·开设相关课程的高等院校。
如何阅读本书
为了避免说教式的讲解带来枯燥乏味的阅读体验,本书给出了大量
的实例及生产环境下的案例。本书可分为四个部分:第一部分是入门,从理论基础开始讲解,最终会产生两个实践项目;第二部分是提高,基
于第一部分的项目添加特效,形成一个完整的多媒体项目;第三部分是
扩展,结合当下比较流行的直播场景进行实际案例分析;第四部分是工
具,介绍当下大部分可以提高开发以及测试效率的工具。下面是各个章
节的基本介绍。
第1章,介绍音视频的基础概念,其中包括音视频的基础数据格
式、编码后的数据格式以及不同格式之间的相互转换等。
第2章,从零开始讲解如何搭建一个iOS项目和一个Android项目,并且添加C++支持,因为在音视频领域的开发中,有相当一部分的代码
需要用C++来编写,这样就可以做到两个平台(Android和iOS平台)共
用一套代码仓库,以提升开发效率。然后讲解交叉编译,因为在音视频
开发过程中会用到很多第三方开源库,如果将这些库编译到我们的项目
中,势必要进行交叉编译,因此本章会重点讲解这些内容。
第3章,探讨FFmpeg开源库。对于音视频开发来讲,FFmpeg开源库是众所周知也是普遍使用的。本章首先从编译开始,接着是命令行使
用,再到源码结构,最后是API调用,以层层递进的方式对FFmpeg开源
库展开介绍。
第4章,讲解如何利用各自平台的API进行声音与画面的渲染以及解
码,对于画面的渲染,推荐使用OpenGL ES,两个平台可以使用同一个
代码仓库。
第5章,实现一款视频播放器。有了前四章的基础,我们已经完全
可以构建起一个视频播放器了。本书最大的特点就是经过几章基础知识
的学习立即开始一个项目的实践,通过本章的视频播放器项目,我们将
会熟悉播放器是如何工作的。
第6章,重点介绍音视频的采集与编码器。特别是硬件编解码器在
各个平台上的使用,使得应用能够更高效(耗电更少、发热更少、界面
更流畅)地运行在用户的手机上。
第7章,继续开发一个视频录制的新项目,该项目可以使我们更加
熟悉音视频应用在各个平台下的实现。
第8章,讲解如何处理音频流。毕竟让别人听采集出来的干声是很
不礼貌的,本章将利用各种特效来美化采集的声音。
第9章,讲解如何处理视频流,使视频中的颜值变得更高,毕竟爱
美之心人皆有之。
第10章,在第7章的项目基础之上,增加第8章的音频特效和第9章
的视频特效,从而构建一个实际生产过程中的多媒体应用。
第11章,继续以项目作为驱动,详细讲解如何基于之前学习的内容
构建一个直播的应用,重点介绍推流以及拉流端,同时还涉及礼物特
效、聊天以及第三方云服务的内容。
第12章,由于直播应用很难用一章的篇幅讲完,所以本章针对一些
核心的处理进行讲解。
第13章,介绍常用的工具和排错方法,说明在日常开发中如何更有
效率地解决问题,本章内容并不仅限于音视频的开发领域。附录给出一些参考内容。
勘误和支持
由于作者水平有限,编写时间仓促,书中难免会出现一些错误或者
不准确的地方,恳请读者批评指正。为此,我特意创建了一个在线支持
与应急方案的二级站点http:music-video.cn 。你可以将书中的错误发布
在Bug勘误表页面中,同时如果你遇到问题,也可以访问QA页面,我
将尽量在线上为你提供最满意的解答。书中的全部源代码文件都将发布
在这个网站上,我也会及时地进行相应的功能更新。如果你有更多的宝
贵意见,也欢迎发送邮件至我的邮箱zhanxiaokai2008@126.com,我很
期待听到你们的真挚反馈。
致谢
感谢唱吧与唱吧的每一位同事,是这个公司让我的职业生涯发展到
了今天,也是这个公司让我能在音视频领域达到今天的成就,可以说没
有唱吧就不会有这本书的问世。
感谢唱吧的每一位用户,感谢你们对唱吧的长期支持和贡献,没有
你们就不会有唱吧的今天,也就不会有这本书的问世。
感谢我的老婆,感谢你对我工作以及写作的支持,是你在我背后默
默做了很多事情,才让我把更多的时间和精力放到工作以及写作中。
感谢机械工业出版社华章公司的编辑Lisa老师,感谢你的魄力和远
见,在这一年多的时间中始终支持我的写作,正是你的鼓励和帮助引导
我顺利完成全部书稿。
感谢互联网,我们在互联网上迈出的任何一步都是人类历史向前迈
进的一步,感谢众多互联网人的辛苦工作,为我们创造了这么多机遇。
谨以此书献给我最亲爱的家人、同事,以及众多互联网从业者。
展晓凯2017年9月于北京第1章 音视频基础概念
为了避免枯燥的说教式讲解,本章将结合示意图来介绍音频与视频
的基础概念,对于本章的学习,不需要使用任何开发环境。研究数字音
频时,必须要对声学现象有一定的了解,若只研究数字音频而忽略了声
学现象,那就本末倒置了,因为音频技术是为了记录、存储和回放声学
现象才发明的,所以先了解声学现象对学习数字音频是有很大帮助的。
声音产生于自然界,早在没有任何科学研究的时候,就已经存在声音
了,并且各种声音还可以组成动听的音乐;当人类有了记录以及存储声
音的能力之后,就迎来了模拟信号到数字信号的转换,所以本章首先会
介绍如何记录以及存储声音。
相比声音,视频(画面)更易于观察,本章将从图像的物理现象开
始讲解,然后讨论一帧帧的画面是如何描述的,以及视频是如何被记录
和存储到设备中的。
学习完本章之后,相信大家对耳朵能够直接听到的声音,眼睛能够
直接看到的图像会有更深入的认知。1.1 声音的物理性质
1.1.1 声音是波
说到声音,爱好音乐的人首先可能会想到优美的音乐或者是劲爆十
足的舞曲,这些音乐只是声音的一种。音乐是由乐器弹奏或者歌手演唱
而产生的,那么声音是如何产生的呢?回想一下中学物理课本上的定义
——声音是由物体振动而产生的(如图1-1所示)。
图 1-1
如图1-1所示,当小球撞击到音叉的时候,音叉会发生振动,对周
围的空气产生挤压,从而产生声音。声音是一种压力波,当演奏乐器、拍打一扇门或者敲击桌面时,它们的振动都会引起空气有节奏的振动,使周围的空气产生疏密变化,形成疏密相间的纵波(可以理解为石头落入水中激起的波纹),由此就产生了声波,这种现象会一直延续到振动
消失为止。1.1.2 声波的三要素
声波的三要素是频率、振幅和波形,频率代表音阶的高低,振幅代
表响度,波形代表音色。
频率(过零率)越高,波长就越短。低频声响的波长则较长,所以
其可以更容易地绕过障碍物,因此能量衰减就小,声音就会传得远,反
之则会得到完全相反的结论。
响度其实就是能量大小的反映,用不同的力度敲击桌子,声音的大
小势必也会不同。在生活中,分贝常用于描述响度的大小。声音超过一
定的分贝,人类的耳朵就会受不了。
音色其实也不难理解,在同样的音调(频率)和响度(振幅)下,钢琴和小提琴的声音听起来是完全不相同的,因为它们的音色不同。波
的形状决定了其所代表声音的音色,钢琴和小提琴的音色不同就是因为
它们的介质所产生的波形不同。
人类耳朵的听力有一个频率范围,大约是20Hz~20kHz,不过,即
使是在这个频率范围内,不同的频率,听力的感觉也会不一样,业界非
常著名的等响曲线,就是用来描述等响条件下声压级与声波频率关系
的,如图1-2所示。图 1-2
从图1-2中可以看出,人耳对3~4kHz频率范围内的声音比较敏感,而对于较低或较高频率的声音,敏感度就会有所减弱;在声压级较低
时,听觉的频率特性会很不均匀;而在声压级较高时,听觉的频率特性
会变得较为均匀。频率范围较宽的音乐,其声压以80~90dB为最佳,超
过90dB将会损害人耳(105dB为人耳极限)。1.1.3 声音的传播介质
吉他是通过演奏者拨动琴弦来发出声音的,鼓是通过鼓槌敲击鼓面
发出声音的,这些声音的产生都离不开振动,就连我们说话也是因为声
带振动而产生声音的。既然都是振动产生的声音,那为什么吉他、鼓和
人声听起来相差这么大呢?这是因为介质不同。我们的声带振动发出声
音之后,经过口腔、颅腔等局部区域的反射,再经过空气传播到别人的
耳朵里,这就是我们说的话被别人听到的过程,其中包括了最初的发声
介质与颅腔、口腔,还有中间的传播介质等。事实上,声音的传播介质
很广,它可以通过空气、液体和固体进行传播;而且介质不同,传播的
速度也不同,比如,声音在空气中的传播速度为340ms,在蒸馏水中的
传播速度为1497ms,而在铁棒中的传播速度则可以高达5200ms;不
过,声音在真空中是无法传播的。
生活小贴士
在日常生活中,我们也会利用对声音的研究去做一些使我们更舒适
的事情,比如吸音棉和隔音棉,这两种常见产品的发明就是通过研究声
音在传播中的特性而研发出来的。
吸音主要是解决声音反射而产生的嘈杂感,吸音材料可以衰减入射
音源的反射能量,从而达到对原有声源的保真效果,比如录音棚里面的
墙壁上就会使用吸音棉材料。
隔音主要是解决声音的透射而降低主体空间内的吵闹感,隔音棉材
料可以衰减入射音源的透射能量,从而达到主体空间的安静状态,比如
KTV里面的墙壁上就会安装隔音棉材料。1.1.4 回声
当我们在高山或空旷地带高声大喊的时候,经常会听到回声
(echo)。之所以会有回声是因为声音在传播过程中遇到障碍物会反弹
回来,再次被我们听到(如图1-3所示)。
但是,若两种声音传到我们的耳朵里的时差小于80毫秒,我们就无
法区分开这两种声音了,其实在日常生活中,人耳也在收集回声,只不
过由于嘈杂的外界环境以及回声的分贝(衡量声音能量值大小的单位)
比较低,所以我们的耳朵分辨不出这样的声音,或者说是大脑能接收到
但分辨不出。
图 1-31.1.5 共鸣
自然界中有光能、水能,生活中有机械能、电能,其实声音也可以
产生能量,例如两个频率相同的物体,敲击其中一个物体时另一个物体
也会振动发声(如图1-4所示)。
图 1-4
这种现象称为共鸣,共鸣证明了声音传播可以带动另一个物体振
动,也就是说,声音的传播过程也是一种能量的传播过程。1.2 数字音频
1.1节主要介绍了声音的物理现象以及声音中常见的概念,也为后
续的讲解统一了术语,从本节开始,我们将进入数字音频概念的介绍。
为了将模拟信号数字化,本节将分3个概念对数字音频进行讲解,分别是采样、量化和编码。首先要对模拟信号进行采样,所谓采样就是
在时间轴上对信号进行数字化。根据奈奎斯特定理(也称为采样定
理),按比声音最高频率高2倍以上的频率对声音进行采样(也称为AD
转换),1.1节中提到过,对于高质量的音频信号,其频率范围(人耳
能够听到的频率范围)是20Hz~20kHz,所以采样频率一般为
44.1kHz,这样就可以保证采样声音达到20kHz也能被数字化,从而使得
经过数字化处理之后,人耳听到的声音质量不会被降低。而所谓的
44.1kHz就是代表1秒会采样44100次(如图1-5所示)。
那么,具体的每个采样又该如何表示呢?这就涉及将要讲解的第二
个概念:量化。量化是指在幅度轴上对信号进行数字化,比如用16比特
的二进制信号来表示声音的一个采样,而16比特(一个short)所表示的
范围是[-32768,32767],共有65536个可能取值,因此最终模拟的音频
信号在幅度上也分为了65536层(如图1-6所示)。
图 1-5图 1-6
既然每一个量化都是一个采样,那么这么多的采样该如何进行存储
呢?这就涉及将要讲解的第三个概念:编码。所谓编码,就是按照一定
的格式记录采样和量化后的数字数据,比如顺序存储或压缩存储,等
等。
这里面涉及了很多种格式,通常所说的音频的裸数据格式就是脉冲
编码调制(Pulse Code Modulation,PCM)数据。描述一段PCM数据一
般需要以下几个概念:量化格式(sampleFormat)、采样率
(sampleRate)、声道数(channel)。以CD的音质为例:量化格式(有
的地方描述为位深度)为16比特(2字节),采样率为44100,声道数为
2,这些信息就描述了CD的音质。而对于声音格式,还有一个概念用来
描述它的大小,称为数据比特率,即1秒时间内的比特数目,它用于衡
量音频数据单位时间内的容量大小。而对于CD音质的数据,比特率为
多少呢?计算如下:
44100 16 2 = 1378.125kbps
那么在1分钟里,这类CD音质的数据需要占据多大的存储空间呢?
计算如下:
1378.125 60 8 1024 = 10.09MB
当然,如果sampleFormat更加精确(比如用4字节来描述一个采
样),或者sampleRate更加密集(比如48kHz的采样率),那么所占的
存储空间就会更大,同时能够描述的声音细节就会越精确。存储的这段二进制数据即表示将模拟信号转换为数字信号了,以后就可以对这段二
进制数据进行存储、播放、复制,或者进行其他任何操作。
麦克风是如何采集声音的
麦克风里面有一层碳膜,非常薄而且十分敏感。1.1节中介绍过,声音其实是一种纵波,会压缩空气也会压缩这层碳膜,碳膜在受到挤压
时也会发出振动,在碳膜的下方就是一个电极,碳膜在振动的时候会接
触电极,接触时间的长短和频率与声波的振动幅度和频率有关,这样就
完成了声音信号到电信号的转换。之后再经过放大电路处理,就可以实
施后面的采样量化处理了。
前面提到过分贝,那么什么是分贝呢?分贝是用来表示声音强度的
单位。日常生活中听到的声音,若以声压值来表示,由于其变化范围非
常大,可以达到六个数量级以上,同时由于我们的耳朵对声音信号强弱
刺激的反应不是线性的(1.1节中提到过等响曲线),而是呈对数比例
关系,所以引入分贝的概念来表达声学量值。所谓分贝是指两个相同的
物理量(例如,A1和A0)之比取以10为底的对数并乘以10(或20),即:
N= 10 lg(A1 A0)
分贝符号为“dB”,它是无量纲的。式中A0是基准量(或参考
量),A1是被量度量。1.3 音频编码
1.2节中提到了CD音质的数据采样格式,曾计算出每分钟需要的存
储空间约为10.1MB,如果仅仅是将其存放在存储设备(光盘、硬盘)
中,可能是可以接受的,但是若要在网络中实时在线传播的话,那么这
个数据量可能就太大了,所以必须对其进行压缩编码。压缩编码的基本
指标之一就是压缩比,压缩比通常小于1(否则就没有必要去做压缩,因为压缩就是要减小数据容量)。压缩算法包括有损压缩和无损压缩。
无损压缩是指解压后的数据可以完全复原。在常用的压缩格式中,用得
较多的是有损压缩,有损压缩是指解压后的数据不能完全复原,会丢失
一部分信息,压缩比越小,丢失的信息就越多,信号还原后的失真就会
越大。根据不同的应用场景(包括存储设备、传输网络环境、播放设备
等),可以选用不同的压缩编码算法,如PCM、WAV、AAC、MP3、Ogg等。
压缩编码的原理实际上是压缩掉冗余信号,冗余信号是指不能被人
耳感知到的信号,包含人耳听觉范围之外的音频信号以及被掩蔽掉的音
频信号等。人耳听觉范围之外的音频信号在1.2节中已经提到过,所以
在此不再赘述。而被掩蔽掉的音频信号则主要是因为人耳的掩蔽效应,主要表现为频域掩蔽效应与时域掩蔽效应,无论是在时域还是频域上,被掩蔽掉的声音信号都被认为是冗余信息,不进行编码处理。
下面介绍几种常用的压缩编码格式。
(1)WAV编码
PCM(脉冲编码调制)是Pulse Code Modulation的缩写。前面已经
介绍过PCM大致的工作流程,而WAV编码的一种实现(有多种实现方
式,但是都不会进行压缩操作)就是在PCM数据格式的前面加上44字
节,分别用来描述PCM的采样率、声道数、数据格式等信息。
特点:音质非常好,大量软件都支持。
适用场合:多媒体开发的中间文件、保存音乐和音效素材。
(2)MP3编码MP3具有不错的压缩比,使用LAME编码(MP3编码格式的一种实
现)的中高码率的MP3文件,听感上非常接近源WAV文件,当然在不
同的应用场景下,应该调整合适的参数以达到最好的效果。
特点:音质在128Kbits以上表现还不错,压缩比比较高,大量软件
和硬件都支持,兼容性好。
适用场合:高比特率下对兼容性有要求的音乐欣赏。
(3)AAC编码
AAC是新一代的音频有损压缩技术,它通过一些附加的编码技术
(比如PS、SBR等),衍生出了LC-AAC、HE-AAC、HE-AAC v2三种
主要的编码格式。LC-AAC是比较传统的AAC,相对而言,其主要应用
于中高码率场景的编码(≥80Kbits);HE-AAC(相当于AAC+SBR)
主要应用于中低码率场景的编码(≤80Kbits);而新近推出的HE-AAC
v2(相当于AAC+SBR+PS)主要应用于低码率场景的编码
(≤48Kbits)。事实上大部分编码器都设置为≤48Kbits自动启用PS技
术,而>48Kbits则不加PS,相当于普通的HE-AAC。
特点:在小于128Kbits的码率下表现优异,并且多用于视频中的音
频编码。
适用场合:128Kbits以下的音频编码,多用于视频中音频轨的编
码。
(4)Ogg编码
Ogg是一种非常有潜力的编码,在各种码率下都有比较优秀的表
现,尤其是在中低码率场景下。Ogg除了音质好之外,还是完全免费
的,这为Ogg获得更多的支持打好了基础。Ogg有着非常出色的算法,可以用更小的码率达到更好的音质,128Kbits的Ogg比192Kbits甚至更
高码率的MP3还要出色。但目前因为还没有媒体服务软件的支持,因此
基于Ogg的数字广播还无法实现。Ogg目前受支持的情况还不够好,无
论是软件上的还是硬件上的支持,都无法和MP3相提并论。
特点:可以用比MP3更小的码率实现比MP3更好的音质,高中低码
率下均有良好的表现,兼容性不够好,流媒体特性不支持。适用场合:语音聊天的音频消息场景。1.4 图像的物理现象
在学习了音频的相关概念之后,现在开始讨论视频,视频是由一幅
幅图像组成的,所以要学习视频还得从图像学习开始。
与音频的学习方法类似,视频的学习依然是从图像的物理现象开始
回顾,这里需要回顾一下小学做过的三棱镜实验,还记得如何利用三棱
镜将太阳光分解成彩色的光带吗?第一个做这个实验的人是牛顿,各色
光因其所形成的折射角不同而彼此分离,就像彩虹一样,所以白光能够
分解成多种色彩的光。后来人们通过实验证明,红绿蓝三种色光无法被
分解,故称为三原色光,等量的三原色光相加会变为白光,即白光中含
有等量的红光(R)、绿光(G)、蓝光(B)。
在日常生活中,由于光的反射,我们才能看到各类物体的轮廓及颜
色。但是如果将这个理论应用到手机上,那么结论还是这个样子吗?答
案是否定的,因为在黑暗中我们也可以看到手机屏幕上的内容,实际上
人眼能看到手机屏幕上的内容的原理如下。图 1-7
假设一部手机屏幕的分辨率是1280×720,说明水平方向有720个像
素点,垂直方向有1280个像素点,所以整个手机屏幕就有1280×720个像
素点(这也是分辨率的含义)。每个像素点都由三个子像素点组成(如
图1-7所示),这些密密麻麻的子像素点在显微镜下可以看得一清二
楚。当要显示某篇文字或者某幅图像时,就会把这幅图像的每一个像素
点的RGB通道分别对应的屏幕位置上的子像素点绘制到屏幕上,从而显
示整个图像。
所以在黑暗的环境下也能看到手机屏幕上的内容,是因为手机屏幕
是自发光的,而不是通过光的反射才被人们看到的。1.5 图像的数值表示
1.5.1 RGB表示方式
通过1.4节的讲解,我们已经知道任何一个图像都可以由RGB组
成,那么一个像素点的RGB该如何表示呢?音频里面的每一个采样
(sample)均使用16个比特来表示,那么像素里面的子像素又该如何表
示呢?常用的表示方式有以下几种。
·浮点表示:取值范围为0.0~1.0,比如,在OpenGL ES中对每一个
子像素点的表示使用的就是这种表达方式。
·整数表示:取值范围为0~255或者00~FF,8个比特表示一个子像
素,32个比特表示一个像素,这就是类似于某些平台上表示图像格式的
RGBA_8888数据格式。比如,Android平台上RGB_565的表示方法为16
比特模式表示一个像素,R用5个比特来表示,G用6个比特来表示,B用
5个比特来表示。
对于一幅图像,一般使用整数表示方法来进行描述,比如计算一张
1280×720的RGBA_8888图像的大小,可采用如下方式:
1280 720 4 = 3.516MB
这也是位图(bitmap)在内存中所占用的大小,所以每一张图像的
裸数据都是很大的。对于图像的裸数据来讲,直接在网络上进行传输也
是不太可能的,所以就有了图像的压缩格式,比如JPEG压缩:JPEG是
静态图像压缩标准,由ISO制定。JPEG图像压缩算法在提供良好的压缩
性能的同时,具有较好的重建质量。这种算法被广泛应用于图像处理领
域,当然其也是一种有损压缩。在很多网站如淘宝上使用的都是这种压
缩之后的图片,但是,这种压缩不能直接应用于视频压缩,因为对于视
频来讲,还有一个时域上的因素需要考虑,也就是说,不仅仅要考虑帧
内编码,还要考虑帧间编码。视频采用的是更成熟的算法,关于视频压
缩算法的相关内容将会在后续章节(1.6节)进行介绍。1.5.2 YUV表示方式
对于视频帧的裸数据表示,其实更多的是YUV数据格式的表示,YUV主要应用于优化彩色视频信号的传输,使其向后兼容老式黑白电
视。与RGB视频信号传输相比,它最大的优点在于只需要占用极少的频
宽(RGB要求三个独立的视频信号同时传输)。其中“Y”表示明亮度
(Luminance或Luma),也称灰阶值;而“U”和“V”表示的则是色度
(Chrominance或Chroma),它们的作用是描述影像的色彩及饱和度,用于指定像素的颜色。“亮度”是透过RGB输入信号来建立的,方法是将
RGB信号的特定部分叠加到一起。“色度”则定义了颜色的两个方面——
色调与饱和度,分别用Cr和Cb来表示。其中,Cr反映了RGB输入信号
红色部分与RGB信号亮度值之间的差异,而Cb反映的则是RGB输入信
号蓝色部分与RGB信号亮度值之间的差异。
之所以采用YUV色彩空间,是因为它的亮度信号Y和色度信号U、V是分离的。如果只有Y信号分量而没有U、V分量,那么这样表示的图
像就是黑白灰度图像。彩色电视采用YUV空间正是为了用亮度信号Y解
决彩色电视机与黑白电视机的兼容问题,使黑白电视机也能接收彩色电
视信号,最常用的表示形式是Y、U、V都使用8个字节来表示,所以取
值范围就是0~255。在广播电视系统中不传输很低和很高的数值,实际
上是为了防止信号变动造成过载,因而把这“两边”的数值作为“保护
带”,不论是Rec.601还是BT.709的广播电视标准中,Y的取值范围都是
16~235,UV的取值范围都是16~240。
YUV最常用的采样格式是4:2:0,4:2:0并不意味着只有Y、Cb
而没有Cr分量。它指的是对每行扫描线来说,只有一种色度分量是以
2:1的抽样率来存储的。相邻的扫描行存储着不同的色度分量,也就是
说,如果某一行是4:2:0,那么其下一行就是4:0:2,再下一行是
4:2:0,以此类推。对于每个色度分量来说,水平方向和竖直方向的
抽样率都是2:1,所以可以说色度的抽样率是4:1。对非压缩的8比特
量化的视频来说,8×4的一张图片需要占用48字节的内存(如图1-8所
示)。图 1-8
相较于RGB,我们可以计算一帧为1280×720的视频帧,用
YUV420P的格式来表示,其数据量的大小如下:
1280 720 1 + 1280 720 0.5 = 1.318MB
如果fps(1秒的视频帧数目)是24,按照一般电影的长度90分钟来
计算,那么这部电影用YUV420P的数据格式来表示的话,其数据量的
大小就是:
1.318MB 24fps 90min 60s = 166.8GB
所以仅用这种方式来存储电影肯定是不可行的,更别说在网络上进
行流媒体播放了,那么如何对电影进行存储以及流媒体播放呢?答案是
需要进行视频编码,下一节将会讨论视频的编码。1.5.3 YUV和RGB的转化
前面已经讲过,凡是渲染到屏幕上的东西(文字、图片或者其
他),都要转换为RGB的表示形式,那么YUV的表示形式和RGB的表
示形式之间是如何进行转换的呢?对于标清电视601标准,它从YUV转
换到RGB的公式与高清电视709的标准是不同的,通过如下的计算(如
图1-9和图1-10)即可得知。
图 1-9图 1-10
那么什么时候该用哪一种转换呢?比较典型的场景是在iOS平台中
使用摄像头采集出YUV数据之后,上传显卡成为一个纹理ID,这个时
候就需要做YUV到RGB的转换(具体的细节会在后面的章节中详细讲
解)。在iOS的摄像头采集出一帧数据之后(CMSampleBufferRef),我
们可以在其中调用CVBufferGetAttachment来获取YCbCrMatrix,用于决
定使用哪一个矩阵进行转换,对于Android的摄像头,由于其是直接纹
理ID的回调,所以不涉及这个问题。其他场景下需要大家自行寻找对应
的文档,以找出适合的转换矩阵进行转换。1.6 视频的编码方式
1.6.1 视频编码
还记得前面讨论的音频压缩方式吗?音频压缩主要是去除冗余信
息,从而实现数据量的压缩。那么对于视频压缩,又该从哪几方面来对
数据进行压缩呢?其实与前面提到的音频编码类似,视频压缩也是通过
去除冗余信息来进行压缩的。相较于音频数据,视频数据有极强的相关
性,也就是说有大量的冗余信息,包括空间上的冗余信息和时间上的冗
余信息。
使用帧间编码技术可以去除时间上的冗余信息,具体包括以下几个
部分。
·运动补偿:运动补偿是通过先前的局部图像来预测、补偿当前的
局部图像,它是减少帧序列冗余信息的有效方法。
·运动表示:不同区域的图像需要使用不同的运动矢量来描述运动
信息。
·运动估计:运动估计是从视频序列中抽取运动信息的一整套技
术。
使用帧内编码技术可以去除空间上的冗余信息。
还记得前面提到过的图像编码标准JPEG吗?对于视频,ISO同样也
制定了标准:Motion JPEG即MPEG,MPEG算法是适用于动态视频的压
缩算法,它除了对单幅图像进行编码外,还利用图像序列中的相关原则
去除冗余,这样可以大大提高视频的压缩比。截至目前,MPEG的版本
一直在不断更新中,主要包括这样几个版本:Mpeg1(用于VCD)、Mpeg2(用于DVD)、Mpeg4 AVC(现在流媒体使用最多的就是它
了)。
相比较于ISO制定的MPEG的视频压缩标准,ITU-T制定的H.261、H.262、H.263、H.264一系列视频编码标准是一套单独的体系。其中,H.264集中了以往标准的所有优点,并吸取了以往标准的经验,采用的是简洁设计,这使得它比Mpeg4更容易推广。现在使用最多的就是
H.264标准,H.264创造了多参考帧、多块类型、整数变换、帧内预测等
新的压缩技术,使用了更精细的分像素运动矢量(14、18)和新一代
的环路滤波器,这使得压缩性能得到大大提高,系统也变得更加完善。1.6.2 编码概念
1.IPB帧
视频压缩中,每帧都代表着一幅静止的图像。而在进行实际压缩
时,会采取各种算法以减少数据的容量,其中IPB帧就是最常见的一
种。
·I帧:帧内编码帧(intra picture),I帧通常是每个GOP(MPEG所
使用的一种视频压缩技术)的第一个帧,经过适度地压缩,作为随机访
问的参考点,可以当成静态图像。I帧可以看作一个图像经过压缩后的
产物,I帧压缩可以得到6:1的压缩比而不会产生任何可觉察的模糊现
象。I帧压缩可去掉视频的空间冗余信息,下面即将介绍的P帧和B帧是
为了去掉时间冗余信息。
·P帧:前向预测编码帧(predictive-frame),通过将图像序列中前
面已编码帧的时间冗余信息充分去除来压缩传输数据量的编码图像,也
称为预测帧。
·B帧:双向预测内插编码帧(bi-directional interpolated prediction
frame),既考虑源图像序列前面的已编码帧,又顾及源图像序列后面
的已编码帧之间的时间冗余信息,来压缩传输数据量的编码图像,也称
为双向预测帧。
基于上面的定义,我们可以从解码的角度来理解IPB帧。
·I帧自身可以通过视频解压算法解压成一张单独的完整视频画面,所以I帧去掉的是视频帧在空间维度上的冗余信息。
·P帧需要参考其前面的一个I帧或者P帧来解码成一张完整的视频画
面。
·B帧则需要参考其前一个I帧或者P帧及其后面的一个P帧来生成一
张完整的视频画面,所以P帧与B帧去掉的是视频帧在时间维度上的冗
余信息。IDR帧与I帧的理解
在H264的概念中有一个帧称为IDR帧,那么IDR帧与I帧的区别是什
么呢?首先来看一下IDR的英文全称instantaneous decoding refresh
picture,因为H264采用了多帧预测,所以I帧之后的P帧有可能会参考I
帧之前的帧,这就使得在随机访问的时候不能以找到I帧作为参考条
件,因为即使找到I帧,I帧之后的帧还是有可能解析不出来,而IDR帧
就是一种特殊的I帧,即这一帧之后的所有参考帧只会参考到这个IDR
帧,而不会再参考前面的帧。在解码器中,一旦收到一个IDR帧,就会
立即清理参考帧缓冲区,并将IDR帧作为被参考的帧。
2.PTS与DTS
DTS主要用于视频的解码,英文全称是Decoding Time Stamp,PTS
主要用于在解码阶段进行视频的同步和输出,全称是Presentation Time
Stamp。在没有B帧的情况下,DTS和PTS的输出顺序是一样的。因为B
帧打乱了解码和显示的顺序,所以一旦存在B帧,PTS与DTS势必就会
不同,本书后边的章节里会详细讲解如何结合硬件编码器来重新设置
PTS和DTS的值,以便将硬件编码器和FFmpeg结合起来使用。这里先简
单介绍一下FFmpeg中使用的PTS和DTS的概念,FFmpeg中使用
AVPacket结构体来描述解码前或编码后的压缩数据,用AVFrame结构体
来描述解码后或编码前的原始数据。对于视频来说,AVFrame就是视频
的一帧图像,这帧图像什么时候显示给用户,取决于它的PTS。DTS是
AVPacket里的一个成员,表示该压缩包应该在什么时候被解码,如果视
频里各帧的编码是按输入顺序(显示顺序)依次进行的,那么解码和显
示时间应该是一致的,但是事实上,在大多数编解码标准(如H.264或
HEVC)中,编码顺序和输入顺序并不一致,于是才会需要PTS和DTS
这两种不同的时间戳。
3.GOP的概念
两个I帧之间形成的一组图片,就是GOP(Group Of Picture)的概
念。通常在为编码器设置参数的时候,必须要设置gop_size的值,其代
表的是两个I帧之间的帧数目。前面已经讲解过,一个GOP中容量最大
的帧就是I帧,所以相对来讲,gop_size设置得越大,整个画面的质量就
会越好,但是在解码端必须从接收到的第一个I帧开始才可以正确解码
出原始图像,否则会无法正确解码(这也是前面提到的I帧可以作为随
机访问的帧)。在提高视频质量的技巧中,还有个技巧是多使用B帧,一般来说,I的压缩率是7(与JPG差不多),P是20,B可以达到50,可
见使用B帧能节省大量空间,节省出来的空间可以用来更多地保存I帧,这样就能在相同的码率下提供更好的画质。所以我们要根据不同的业务
场景,适当地设置gop_size的大小,以得到更高质量的视频。
结合IPB帧和图1-11,相信大家能够更好地理解PTS与DTS的概念。
图 1-111.7 本章小结
本章的内容到此结束,这里简单复习一下要点。本章首先介绍了音
频的物理现象,进而研究如何存储以及编码音频,然后又讨论了图像的
物理现象,从RGB到YUV,最后讲解了视频的编码操作。本章的内容
理论和概念比较多,难免会枯燥一些,但是了解这些概念是必需的。其
实本章讲解的东西若想要继续深究,还是有很多可供研究的,由于本书
的核心是应用层的开发,因此就不考虑展开来讲了。对于应用层的开发
人员来说,掌握本章的内容就已经足够了,若想要进一步了解相关的知
识,可自行参考其他资料。第2章 移动端环境搭建
第1章学习了音视频的基础知识,本章将介绍如何在移动端(包括
iOS和Android两个平台)搭建开发音视频的环境,以及如何在每一个平
台的开发环境中添加C++支持。由于篇幅有限,在搭建开发环境时,本
章将只以Mac OS操作系统为例进行讲解,对于Linux、Windows等操作
系统的读者,需要大家自行将各种开发环境适配到对应的系统中。但
是,书中所有项目的设计与实现是不区分开发系统环境的。
在音视频的开发过程中,不可能所有的编码、解码以及处理都由开
发者从零开始编写,因此免不了会用到一些第三方库,所以本章还将讲
解交叉编译,并尝试交叉编译几个音视频相关的开源库。在本章的最
后,会使用LAME这个开源的MP3编码库在iOS平台和Android平台上将
一个PCM文件编码为MP3文件,最终将编码后的MP3文件发送到电脑上
即可进行播放,这也是本章要完成的最终目标。2.1 在iOS上如何搭建一个基础项目
1.新建一个iOS项目
首先,在Xcode中选择新建一个项目,会弹出如图2-1所示的界面,然后我们选择一个新项目的模板,一般选择Single View Application模
板。
之后点击Next,进入图2-2所示的界面,这里需要键入产品的名字
及包名,注意产品的名字将作为安装到用户手机上的应用名称,包名则
是该应用的唯一标识,键入完毕之后点击Next。
完成上述步骤之后,就完成了一个iOS项目的创建。查看该项目的
工程文件,会看到Xcode默认是以Story board的形式构建的界面部分,由于我们不希望使用这种形式来构建界面,而是希望使用xib的形式来
构建界面,所以要在Main Interface选项中删除其中的内容,如图2-3所
示。图 2-1图 2-2
图 2-3
接下来,再建立一个界面文件作为应用的第一个界面,而在iOS中
一个界面就是一个xib文件,如图2-4所示。图 2-4
然后点击Next,进入图2-5所示的界面,键入该xib的名字,然后选
择Target为主工程,点击Create。
现在,打开该xib文件,如图2-6所示,首先要在Placeholders选项下
面的File’s Owner中连接视图中的view,然后设定Class的值为
ViewController类。图 2-5
图 2-6
iOS程序运行的时候其入口是main.m,但是苹果公司不希望开发者修改该文件,而是要求开发者去修改应用的入口——AppDelgate.m文
件,该文件中提供了各种生命周期方法,应用启动的时候将会触发的方
法是application:didFinishLaunchingWithOptions,所以修改该生命周期
方法的代码如下:
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen]
bounds]];
UINavigationController navigationController = [[UINavigationController
alloc] initWithRootViewController:[[ViewController alloc]
initWithNibName:@ViewController bundle:nil]];
self.window.rootViewController = navigationController;
[self.window makeKeyAndVisible];
接下来要为该xib拖入一个Encode按钮,并且将该按钮的点击事件
委托给ViewController中的startEncode方法,如图2-7所示。
图 2-7
startEncode方法提供了NSLog来输出一条日志信息。然后运行整个
项目,点击Encode按钮,如果可以看到正常的日志信息,则说明项目搭
建成功了。
2.CocoaPods的介绍与使用开发应用的时候,并不是所有的基础功能都由开发者从零开始开
发,所以经常会用到第三方开源类库。一定要记住,不要重复造轮子!
不过,在引用这些类库时,可能会出现几个令人头疼的问题:第一是所
引用的第三方类库有可能需要依赖于其他的第三方类库;第二是需要使
用已引用库的新功能,需要了解该类库对应的版本号,并且该类库对其
所依赖的其他第三方类库可能也有版本的要求。如果我们仅仅是复制粘
贴,那将会非常麻烦,尤其是对于大型项目来说,有时甚至可能会是一
种灾难。所以在Java Web的开发中,大家通常使用Maven来构建项目。
选择Maven,一方面是基于项目的依赖关系,另一个重要的原因就是为
了方便引用第三方的类库。对于Android的应用开发,现在使用最多的
就是Gradle,一方面是出于打包的需求,另一方面也是为了满足引用第
三方库的需要。对于iOS应用呢?使用最多的则是CocoaPods,它可以为
开发者提供引用众多第三方类库的功能,比如JSONKit、AFNetWorking
等。
了解了CocoaPods的作用之后,接下来就是学习如何安装和使用
CocoaPods来引用第三方库。
(1)安装CocoaPods
首先要安装好Ruby环境,Mac机器上已经自带了Ruby环境,如果是
其他的开发系统,请读者自行安装,关于如何安装Ruby环境,互联网上
提供了大量的资料。安装完Ruby环境之后,使用如下命令就可以安装
CocoaPods了:
sudo gem install cocoapods
如果执行上述命令之后很长时间都没有反应,那么可能是gem所使
用的默认源有问题,可以使用淘宝提供的源来安装CocoaPods,具体的
做法是先删除掉默认的源,操作命令如下:
gem sources --remove https:rubygems.org
然后替换上新的源,命令如下:
gem sources -a http:rubygems-china.oss.aliyuncs.com此时,可以执行以下命令来查看现在的源到底是什么:
gem sources -l
如果出现下面的内容则说明源更改成功了:
CURRENT SOURCES
http:rubygems-china.oss.aliyuncs.com
此时,再一次在终端执行安装CocoaPods的命令:
sudo gem install cocoapods
稍等片刻,就可以将CocoaPods下载到本地并且安装成功了。
(2)使用CocoaPods
前面提到过CocoaPods是用来管理第三方库的工具,那么它是如何
判断项目需要依赖哪一个第三方库的呢?答案是根据根目录下名为
Podfile的配置文件来获知的。因此该配置文件的语法以及如何编写该配
置文件自然就成了本章节学习的重点,下面一起来看一下。
首先输入平台系统的要求:
platform :ios, '7.0'
上述命令代表项目运行在iOS 7.0平台之上,配置文件从第二行开始
就会出现一个从target'Project Name'do到end的代码块,该代码块将用于
配置具体要引入的第三方库。代码块如下所示:
target :ktv do
pod 'Mantle', '1.5'
pod 'AFNetworking', '2.6.0'
end上述配置代表项目需要引进版本号为1.5的Mantle与版本号为2.6.0的
AFNetworking库。最后,在命令行中,进入项目的根目录下执行pod
install命令,如果Podfile配置文件的语法没有问题,那么CocoaPods就会
为项目生成两个文件和一个目录。其中,一个文件的名称是项目名
称.xcworkspace,另一个文件的名称是Podfile.lock,而生成的目录则是
Pods,其中放置了引用第三方库的源码。此时,双击后缀名为
xcworkspace的文件即可打开此项目,查看Xcode目录里的结构,会发现
项目中已经有了刚才新引入的第三方库了。
增加C++支持
在单独编写一个C或C++的项目时,如果该项目需要引用到第三方
库,那么编译阶段需要配置参数“extra-cflags,-I”来指定引用头文件的
位置,链接阶段需要配置参数“ld-flags,-L”来指定静态库的位置,并且
使用-l来指定引用的是哪一个库。在C++的编译中,如果需要在程序的
执行过程中带入一些宏(define的常量),那么就应该在“extra-cflags”的
后面增加自己需要定义的宏,例如-DAUTO_TEST,这就相当于在程序
中编写了如下一行代码:
define AUTO_TEST
那么在Xcode中,应该如何指定头文件、第三方库,以及预定义宏
呢?其实,Xcode里面已经存在了对应的参数设置,只需要在Build
Settings中设置这些参数即可。首先对应于-I来指定头文件的目录,Xcode使用Search Paths来设置头文件的搜索路径,通常会用到Header
Search Paths选项来指定头文件的搜索路径。其中预定义变量
(SRCROOT)和(PROJECT_DIR)都是项目的根目录,可以基于这
两个预定义变量再加上相对路径来指定头文件所在的具体位置;预定义
宏在Other C Flags选项中,可以写入-DAUTO_TEST,表示定义了
AUTO_TEST宏;在指定第三方库时,-L对应到Xcode中就是other Link
flags选项,其中可以写入需要链接的库文件;在Xcode项目中直接添加
一个静态库文件,Xcode会默认在Build phases选项的Link Binary with
Library里加入该静态库,但是如果Xcode没有自动加入该静态库的话,就需要开发者手动加一下,这里其实也是-L的一种表示方式。
接下来,编写一个类Mp3Encoder,负责将PCM数据编码为MP3文件。首先建立Mp3Encoder.cpp和Mp3Encoder.h这两个文件,然后再编写
一个encode方法,该方法将调用printf输出一行日志信息,以代表调用了
这个方法。
现在就为前面搭建起来的iOS项目增加C++的支持,由于OC语法支
持混编,所以开发者仅需要把引用C++的OC类的后缀名改为.mm(OC
类的正常后缀名是.m),就可以和C++一块编译了。所以开发者仅需要
把ViewController的后缀名改为.mm,再包含进Mp3Encoder.h头文件,实
例化该类,最后调用该类的encode方法,代码如下:
Mp3Encoder encoder = new Mp3Encoder;
encoder->encode ;
delete encoder;
如果大家可以看到printf输出的日志信息,则代表已经为该iOS项目
成功添加了C++的支持。是不是很简单呢?其实这也是苹果公司为开发
者提供的便利,下面还会讲解Android是如何添加C++支持的,相较于
iOS平台而言,Android平台要麻烦得多。2.2 在Android上如何搭建一个基础项目
首先要在开发机器上安装Eclipse(尽量选择已经安装好ADT的版
本)与Android的SDK,然后在Eclipse中建立一个Android项目,命名为
Mp3Encoder,如图2-8所示。
图 2-8
在图2-8所示的界面中,我们需要键入项目的名字、包的名字(应
用程序的唯一标识),由于在决定写这本书的时候笔者正在普吉岛度
假,所以就将其命名为com.phuket.tour.mp3encoder了。然后点击Next按钮,进入图2-9所示的界面。
图 2-9
在这一步骤中,可以让IDE建立一个默认的Activity,选中图2-9中
的Create activity复选框,点击Next按钮,进入图2-10所示的界面。图 2-10
这一步不用做任何选择,全部使用默认配置,点击Next,进入图2-
11所示的界面。图 2-11
在图2-11中,选择Blank Activity,代表应用默认是一个空的页面
(后面会加上自己的按钮),点击Next,进入图2-12所示的界面。图 2-12
在图2-12中,键入主Activity的名字为MainActivity,然后点击
Finish,即可成功建立该项目,其目录的结构如图2-13所示。图 2-13
增加C++支持
在Android平台上开发音视频相关的项目,必定会涉及JNI(Java
Native Interface)这一概念。它是一种编程框架,允许运行于JVM的
Java程序去调用本地代码(CC++以及汇编语言的代码)。本地代码通
常会与硬件或操作系统相关联,因而其会在一定程度上破坏Java语言所
宣称的跨平台特性。不过在某些业务场景下这种调用又是必需的,比如
Android系统(framework层面)就采用了大量的JNI手段去调用Native层
的实现库。如果抛开音视频相关的场景,仅仅考虑Android的开发,那
么有哪些场景会用到JNI呢?大约有以下几种情况。
·应用程序需要一些平台相关特性的支持,而Java层没有对应的API
支持(比如OpenSL ES的使用)。
·调用成熟的或者已经存在的、用CC++语言编写的代码库。比如使
用OpenGL ES的视频特效处理库,或者利用FFmpeg、LAME等第三方开源库。
·应用程序的某些关键操作对运行速度有较高的要求。这部分逻辑
可以用C或者汇编语言来编写,再通过JNI向Java层提供访问接口。
在本书后面的项目案例中,熟练使用JNI是最基本的要求,所以在
这里先详细地讲解一下。JNI主要有两种调用形式:第一种也是最常见
的,即Java代码调用Native的代码;第二种则恰好相反,就是在Native层
调用Java代码。由于使用场景最多的是第一种方式,所以本节只介绍这
种方式,而第二种方式的调用在后续的章节中也会用到,到时候读者自
然会学习到。
要完成一个Java层调用Native代码的项目,大致步骤如下。
1)编写一个Java类,并且在某个方法签名的修饰符中加上native修
饰符。
2)使用javac命令编译第1步中的Java类,使之成为一个class文件,如果使用的是Eclipse的IDE,则其将会自动执行javac的编译命令,生成
的class文件将存在于项目目录下的binclasses目录下。
3)使用javah命令将第2步的输出作为输入,生成JNI的头文件。
4)将JNI头文件复制到项目下的jni目录,并且建立一个cpp的实现
文件实现该JNI头文件中的函数。
5)编写Android.mk文件,加入第4步的本地代码,利用ndk-build生
成动态链接库。
6)在Java类中加载第5步生成的动态链接库。
7)在Java类中调用该Native方法。
下面以一个实例来完成上述的步骤,具体如下。
1)在Eclipse的com.phuket.tour.studio包下,建立一个Java文件
Mp3Encoder.java。
2)在上述Java文件中编写一个本地方法:public native void encode;
3)进入对应的class文件所在的目录下,执行下面的命令生成JNI接
口文件:
javah -jni com.phuket.tour.studio.Mp3Encoder
4)然后把该头文件复制到jni目录下,编写一个Mp3Encoder.cpp来
实现该接口文件,这里仅仅输出一行日志。
首先利用“宏定义”定义一个输出日志的宏:
define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG,__VA_ARGS__)
然后定义LOG_TAG为Mp3Encoder,并且实现该encode方法:
JNIEXPORT void JNICALL
Java_com_phuket_tour_studio_Mp3Encoder_encode(JNIEnv env,jobject obj) {
LOGI(encoder encode);
}
5)新建立一个Android.mk文件,并键入以下内容:
LOCAL_PATH := (call my-dir)
include (CLEAR_VARS)
LOCAL_SRC_FILES = .Mp3Encoder.cpp
LOCAL_LDLIBS := -L(SYSROOT)usrlib -llog
LOCAL_MODULE := libaudioencoder
include (BUILD_SHARED_LIBRARY)
上述代码具体每一行的含义,在后续章节中会有详细的解释。现在
最重要的就是把该项目运行起来。
6)在当前目录下执行ndk-build指令,编译出该动态so库。
7)在MainActivity中写入一个静态代码块:static {
System.loadLibrary(audioencoder);
}
写该代码块的目的是将刚刚编译好的so库加载到我们的项目中。
8)在onCreate中调用Mp3Encoder类的encode方法,最后运行该应
用,并且打开logcat视图查看结果。
如果可以在logcat中看到JNI层输出的日志,则代表已经成功地为
Android项目添加了C++的支持。如果仅仅是自己编写代码,而不需要调
用其他的第三方开源库,这样就完全可以了。但是在开发音视频项目
时,肯定会需要依赖很多第三方的库,比如音视频编解码的开源库、音
视频特效处理的开源库,等等,那么如何根据自己的需要添加各种依赖
库呢?这将在2.3.3节中与大家详细讨论,最终笔者会使用LAME库编码
一个MP3的音频文件作为本章的实例。2.3 交叉编译的原理与实践
本节将学习交叉编译,在音视频的开发中了解交叉编译是必需的,因为无论在哪一种移动平台下开发,第三方库都是需要进行交叉编译
的。本节会从交叉编译的原理开始介绍,然后会在两个移动平台下编译
出音视频开发常用的几个库,包括X264、FDK_AAC、LAME,最终将
以LAME库为例进行实践,完成一个将音频的PCM裸数据编码成MP3文
件的实例,以此来证明交叉编译的重要性。2.3.1 交叉编译的原理
先来看一下,如果要在PC上运行一个二进制程序(以源码的方式
进行编译,不要以包管理工具的方式来安装),需要怎样做。
首先,要有这个二进制程序的源代码(有可能是直接下载的,也有
可能是自己编写的代码),然后在PC上进行编译链接生成可执行文
件,最后在Terminal下面去执行该可执行文件。
上述流程中包含了几个角色,首先是要有源代码,然后是要知道最
终运行该二进制程序的机器是哪一个(其实就是本机器),当然,其中
最重要的就是编译器和链接器了,对于C或者C++程序来讲,就是使用
gcc和g++,而该编译器是需要预先安装在机器上的。分析了这么多角
色,总结成一句话就是:使用本机器的编译器,将源代码编译链接成为
一个可以在本机器上运行的程序。这就是正常的编译过程,也称为
Native Compilation,中文译作本机编译。
了解了本机编译之后,再来看一下何为交叉编译。所谓交叉编译,就是在一个平台(如PC)上生成另外一个平台(Android、iOS或者其他
嵌入式设备)的可执行代码。相较于正常编译,下面来看一下交叉编译
的相应角色。首先,最终程序运行的设备就是Android或者iOS设备,源
代码就是从第三方开源网站上下载的源代码,编译机器就是我们的
PC,而编译器也必须要安装到该PC上。但是这里对编译器是有特殊需
求的,最终程序运行的系统必须要提供可运行在PC上的编译器,而该
编译器就是大家常说的交叉工具编译链。
了解了交叉编译之后,大家应该能够理解交叉编译存在的必要性
了。在一般的嵌入式系统开发中,运行程序的目标平台其存储空间和运
算能力都是有限的,尽管现在的iOS和Android设备拥有越来越强劲的计
算能力,但是在这种嵌入式设备中进行本地编译是不太可能的,一则是
因为计算能力的问题,还有一个重要的原因就是编译工具以及整个编译
过程异常繁琐,所以在这种情况下,直接在ARM平台下进行本机编译
几乎是不可能的。而具有更加强劲的计算能力与更大存储空间的PC才
是理想的选择,所以大部分的嵌入式开发平台都提供了本身平台交叉编
译所需要的交叉工具编译链,通过该交叉工具编译链,开发者就能在
PC上编译出可以运行在ARM平台下的程序了。无论是自行安装PC上的编译器,还是下载其他平台(Android或者
iOS)的交叉工具编译链,它们都会提供以下几个工具:CC、AS、AR、LD、NM、GDB。那么,这几个工具到底是做什么用的呢?下面
就来逐一解释一下。
·CC:编译器,对C源文件进行编译处理,生成汇编文件。
·AS:将汇编文件生成目标文件(汇编文件使用的是指令助记符,AS将它翻译成机器码)。
·AR:打包器,用于库操作,可以通过该工具从一个库中删除或者
增加目标代码模块。
·LD:链接器,为前面生成的目标代码分配地址空间,将多个目标
文件链接成一个库或者是可执行文件。
·GDB:调试工具,可以对运行过程中的程序进行代码调试工作。
·STRIP:以最终生成的可执行文件或者库文件作为输入,然后消除
掉其中的源码。
·NM:查看静态库文件中的符号表。
·Objdump:查看静态库或者动态库的方法签名。
了解了这些之后,当读者再进行交叉编译或者使用交叉编译工具链
提供的工具时,就不会感到陌生了,接下来将会在iOS平台和Android平
台分别演示如何交叉编译出几个常用的音视频开源库。
编译器对比
正常编译一个程序的过程如下:
编译:gcc-c main.cpp.libmadmad_decoder.cpp-I.libmadinclude
打包:ar cr..prebuiltlibmedia.a mad_decoder.o
链接:g++-o main main.o-L..prebuilt-l mdedia在这个过程中,gcc、ar、g++是我们用到的三个编译工具,在这里
没有用到的ranlib、gdb、nm、strip等都会包含在PC的编译器中,同样其
他平台提供的交叉工具编译链中也会包含这些命令行工具,比如
Android提供的NDK,其交叉工具编译链中的prebuiltdarwin-x86_64bin
中,就包含了对应的gcc、ar、g++、gdb、strip、nm、ranlib等工具。2.3.2 iOS平台交叉编译的实践
前面提到的目标平台虽然都基于ARM平台,但是随着时间的推
移,平台也在不断地演进,就像armv5到armv6、armv7以及到现在的
arm64,对于iOS平台来讲,每一代手机对应的指令集到底应该是什么
呢?下面就依据iOS设备发布的时间线来逐个看一下。
·armv6:iPhone、iPhone 2、iPhone 3G
·armv7:iPhone 4、iPhone 4S
·armv7s:iPhone 5、iPhone 5S
·arm64:iPhone 5S、iPhone 6(P)、iPhone 6S(P)、iPhone
7(P)
机器对指令集的支持是向下兼容的,因此armv7的指令集是可以运
行在iPhone 5S中的,只是效率没那么高而已。借此机会,先来讨论一下
iOS项目文件中的一项配置,即Build Settings里面的Architectures选项。
Architectures指的是该App支持的指令集,一般情况下,在Xcode中新建
一个项目,其默认的Architectures选项值是Standard
architectures(armv7、arm64),表示该App仅支持armv7和arm64的指令
集;Valid architectures选项指即将编译的指令集,一般设置为armv7、armv7s、arm64,表示一般会编译这三个指令集;Build Active
Architecture Only选项表示是否只编译当前适用的指令集,一般情况下
在Debug的时候设置为YES,以便可以更加快速、高效地调试程序,而
在Release的情况下设置为NO,以便App在各个机器上都能够以最高效
率运行,因为Valid architectures选择的对应指令集是armv7、armv7s和
arm64,在Release下会为各个指令集编译对应的代码,因此最后的ipa体
积基本上翻了3倍。
基于上面的描述,以及设备与指令集平台的对比,大多数情况下,我们在实际的交叉编译过程中只编译armv7与arm64这两个指令集平台下
的库,因为armv7s设备的数量比较少,有armv7来保底完全是可以运行
的,并且armv7到armv7s指令集的变动又比较少,而arm64的变动则比较
大,设备数量也比较多,所以需要单独编译出来,以保证这一批设备可以享受到最优质的运行状况。
1.LAME的交叉编译
首先来看一下LAME库是如何被交叉编译的,在交叉编译之前先介
绍一下LAME库。
LAME简介
LAME是目前非常优秀的一种MP3编码引擎,在业界,转码成MP3
格式的音频文件时,最常用的编码器就是LAME库。当达到320Kbits以
上时,LAME编码出来的音频质量几乎可以和CD的音质相媲美,并且
还能保证整个音频文件的体积非常小,因此若要在移动端平台上编码
MP3文件,使用LAME便成为唯一的选择。
前面在介绍交叉编译原理的时候曾提到过,不论在何种平台上进行
交叉编译,都要知道交叉编译工具链在什么地方。同样,在iOS上进行
交叉编译时,也要搞清楚这个最基本的问题。其实在安装iOS的开发环
境Xcode时,配套的编译器就已经安装好了。开发iOS平台下的App就是
这么方便,不需要再单独下载交叉工具编译链,接下来直接去
SourceForge下载最新的LAME版本,访问链接如下:
https:sourceforge.netprojectslamefileslame3.99
这里选择3.99.5版本,将源码下载下来之后,编写一个
build_armv7.sh脚本,用于编译armv7指令集下的版本,以支持iPhone 5S
及以下的设备,Shell脚本里面的内容如下:
.configure \--disable-shared \--disable-frontend \--host=arm-apple-darwin \--prefix=.thinarmv7 CC=xcrun -sdk iphoneos clang -arch armv7 CFLAGS=-arch armv7 -fembed-bitcode -miphoneos-version-min=7.0 LDFLAGS=-arch armv7 -fembed-bitcode -miphoneos-version-min=7.0
make clean
make -j8
make install下面分别解释一下这几个命令以及选项的意义。configure是符合
GNU标准的软件包发布所必备的命令,所以这里是通过configure的方式
来生成Makefile文件,然后使用make和make install编译和安装整个库。
可使用configure-h命令来查看一下configure的帮助文档,了解LAME的
可选配置项,具体如下。
·--prefix:指定将编译好的库放到哪个目录下,这是GNU大部分库
的标准配置。
·--host:指定最终库要运行的平台。
·CC:指定交叉工具编译链的路径,其实这里就是指定gcc的路径。
·CFLAGS:指定编译时所带的参数。Shell脚本中指定-march是
armv7平台,代表编译的库运行的目标平台是armv7平台;另外Shell脚本
中也指定了打开bitcode选项,这使得使用编译出来的这个库的工程,可
以将enable-bitcode选项设置为YES,如果没有打开该选项,那么其在
Xcode中只能设置为NO,而这对于最终App的运行性能会有一定的影
响。Shell脚本中同时也指定了编译出来的这个库所支持的最低iOS版本
是7.0,如果不配置该参数的话,则默认是iOS 9.0版本,而所使用的编
译出来的这个库的工程,若所支持的最低iOS版本不是9.0的话,Xcode
就会给出警告。
·LDFLAGS:指定链接过程中的参数,同样也要带上bitcode的选项
以及开发者期望App支持的最低iOS版本的选项参数。
·--disable-shared:通常是GNU标准中关闭动态链接库的选项,一般
是在编译出命令行工具的时候,期望命令行工具可以单独使用而不需要
动态链接库的配置。
·--disable-frontend:不编译出LAME的可执行文件。
bitcode
bitcode模式是表明当开发者提交应用(App)到App Store上的时
候,Xcode会将程序编译为一个中间表现形式(bitcode)。App Store会
将该bitcode中间表现形式的代码进行编译优化,链接为64位或者32位的
程序。如果程序中用到了第三方静态库,则必须在编译第三方静态库的时候也开启bitcode,否则在Xcode的Build Setting中必须要关闭bitcode,这对于App来讲可能会造成性能的降低。
同样再看一下arm64指令集下面的编译脚本,建立build_arm64.sh文
件,然后输入以下内容:
.configure \--disable-shared \--disable-frontend \--host=arm-apple-darwin \--prefix=.thinarm64 CC=xcrun -sdk iphoneos clang -arch arm64 CFLAGS=-arch arm64 -fembed-bitcode -miphoneos-version-min=7.0 LDFLAGS=-arch arm64 -fembed-bitcode -miphoneos-version-min=7.0
make clean
make -j8
make install
上述选项配置大部分都与armv7的脚本相同,不同之处在于这里的
CFLAGS指定编译的目标平台是arm64的指令集平台。如果想在模拟器
上运行,那么就需要编译出i386架构下的静态库,而编译i386平台的
Shell脚本也与此类似,仅仅是改变平台架构。为了节省篇幅,后续讨论
FDK AAC及X264的编译脚本时,将只提供armv7的编译方式。
待两个脚本执行完毕之后,就可以去thin-lame目录下寻找对应的
armv7与arm64目录,并且在这两个目录下会看到bin、lib、include、share这四个目录。由于在配置的时候裁剪掉了可执行文件,所以bin目
录下不会有内容;在lib目录下则是链接过程中需要链接的libmp3lame.a
静态库文件;在include目录下则是编译过程所需要引用的头文件。
至此已经编译出了两个指令集平台下的静态库文件与include文件目
录,其实这两个include文件的目录是一样的,随便使用哪一份都可以,但是对于静态库文件,应该如何用呢?这里就会涉及如何合并静态库的
知识,合并静态库应该使用lipo命令,在终端下切换到thin-lame目录下
键入:
lipo -create .arm64liblibmp3lame.a .armv7liblibmp3lame.a -output
libmp3lame.a
这行命令会把两个平台架构下的静态库文件合并到一个libmp3lame.a的静态库文件中,现在来验证一下最终的libmp3lame.a是否
包含armv7与arm64这两个平台架构的静态库,在命令行键入:
file libmp3lame.a
如果看到如下信息,则说明编译成功了:
libmp3lame.a: Mach-O universal binary with 2 architectures: [arm_v7: current ar
archive] [arm64: current ar archive]
libmp3lame.a (for architecture armv7): current ar archive
libmp3lame.a (for architecture arm64): current ar archive
如果在开发过程中编译的第三方库比较多,而同时编译的指令集平
台也比较多,则每次都需要新建几个脚本文件,然后编译出各个平台的
静态库,最终再用lipo命令进行合并,将会非常麻烦。而软件工程师就
是要把重复性的东西做成工具,让工作变得更加简单,所以后续在代码
仓库中会有完整的编译脚本,可以编译出所有架构平台下的静态库,并
且也已经用lipo命令把所有指令集平台下的静态库合并到了一个静态库
文件中(包括即将介绍的FDK_AAC的库及X264库的交叉编译)。
2.FDK_AAC的交叉编译
在交叉编译之前先介绍一下FDK_AAC库。
FDK_AAC简介
FDK_AAC是用来编码和解码AAC格式音频文件的开源库,Android
系统编码和解码AAC所用的就是这个库。开发者Fraunhofer IIS是AAC音
频规范的核心制定者(MP3时代Fraunhofer IIS也是MP3规范的制定
者)。前面章节中已经介绍过AAC有很多种Profile,而FDK_AAC几乎
支持大部分的Profile,并且支持CBR和VBR这两种模式,根据笔者个人
的听感和频谱分析,在同等码率下FDK_AAC比NeroAAC以及faac和
voaac的音质都要好一些。
下面先到SourceForge上下载稳定版本的FDK_AAC:
https:sourceforge.netpopencore-amrfdk-aacciv0.1.4tree然后在根目录下建立build_armv7.sh脚本,在里面写入以下内容:
.configure \--enable-static \--disable-shared \--host=arm-apple-darwin \--prefix=FDK_ROOT_DIRthinarmv7
CC=xcrun -sdk iphoneos clang AS=gas-preprocessor.pl CC
CFLAGS=-arch armv7 -mios-simulator-version-min=7.0 LDFLAGS=-arch armv7 -mios-simulator-version-min=7.0
make clean
make -j8
make install
FDK_AAC的配置选项中要求比LAME多配置一项AS参数,并且需
要安装gas-preprocessor,首先进入下方链接:
https:github.comapplexiaohaogas-preprocessor
下载gas-preprocessor.pl,然后复制到usrlocalbin目录下,修
改usrlocalbingas-preprocessor.pl的文件权限为可执行权限:
chmod 777 usrlocalbingas-preprocessor.pl
这样gas-preprocessor.pl就安装成功了,再次执行上面的Shell脚本,成功之后就可以在thinarmv7目录(当然需要提前建立好该目录)下看
到include和lib这两个目录,在使用该库时,include目录下包含了编译阶
段需要用到的头文件,而lib目录下包含了链接阶段需要用到的静态库文
件。类似于上面的脚本,也可以编译arm64以及i386平台下的静态库,最后再用lipo工具合并静态库文件,其实也可以编写一个Shell脚本完成
上述所有事情,具体可以查看代码仓库中的完整编译脚本。
3.X264的交叉编译
本节将介绍X264开源库的交叉编译,和之前的章节一样,在做交
叉编译之前先向大家介绍一下X264库。
X264简介X264是一个开源的H.264MPEG-4 AVC视频编码函数库,是最好的
有损视频编码器之一。一般的输入是视频帧的YUV表示,输出是编码之
后的H264的数据包,并且支持CBR、VBR模式,可以在编码的过程中
直接改变码率的设置,这在直播的场景中是非常实用的(直播场景下利
用该特点可以做码率自适应)。
可以到下面这个网站上获取X264的源码:
http:www.videolan.orgdevelopersx264.html
当然也可以直接执行:
git clone git:git.videolan.orgx264.git
同样,在根目录下建立build_armv7.sh脚本,然后在里面写入如下
内容:
!binsh
export AS=gas-preprocessor.pl -arch arm -- xcrun -sdk iphoneos clang
export CC=xcrun -sdk iphoneos clang
.configure \--enable-static \--enable-pic \--disable-shared \--host=arm-apple-darwin \--extra-cflags=-arch armv7 -mios-version-min=7.0 \--extra-asflags=-arch armv7 -mios-version-min=7.0 \--extra-ldflags=-arch armv7 -mios-version-min=7.0 \--prefix=.thinarmv7
make clean
make -j8
make install
在执行上述Shell脚本之前,要求在当前目录下预先建立好
thinarmv7目录,然后执行脚本,这样就可以看到在armv7目录下产生的
对应的include及lib目录了,当然也有bin目录,各个目录里面存放的内
容,前面已经介绍过很多遍了,在此不再赘述。对于arm64以及i386架
构平台的编译,在代码仓库中会有一个Shell脚本,它可以编译出所有架
构平台下的静态库,并且已经用lipo工具合并成了一个静态库。2.3.3 Android平台交叉编译的实践
1.深入了解Android NDK
Android原生开发包(NDK)可用于Android平台上的C++开发,NDK不仅仅是一个单一功能的工具,还是一个包含了API、交叉编译
器、链接程序、调试器、构建工具等的综合工具集。
下面大致列举了一下经常会用到的组件。
·ARM、x86的交叉编译器
·构建系统
·Java原生接口头文件
·C库
·Math库
·最小的C++库
·ZLib压缩库
·POSIX线程
·Android日志库
·Android原生应用API
·OpenGL ES(包括EGL)库
·OpenSL ES库
下面来看一下Android所提供的NDK根目录下的结构。
·ndk-build:该Shell脚本是Android NDK构建系统的起始点,一般在项目中仅仅执行这一个命令就可以编译出对应的动态链接库了,后面会
有详细的介绍。
·ndk-gdb:该Shell脚本允许用GUN调试器调试Native代码,并且可
以配置到Eclipse的IDE中,可以做到像调试Java代码一样调试Native的代
码。
·ndk-stack:该Shell脚本可以帮助分析Native代码崩溃时的堆栈信
息,后续会针对Native代码的崩溃进行详细的分析。
·build:该目录包含NDK构建系统的所有模块。
·platforms:该目录包含支持不同Android目标版本的头文件和库文
件,NDK构建系统会根据具体的配置来引用指定平台下的头文件和库文
件。
·toolchains:该目录包含目前NDK所支持的不同平台下的交叉编译
器——ARM、x86、MIPS,其中比较常用的是ARM和x86。构建系统会
根据具体的配置选择不同的交叉编译器。
在了解了NDK的目录结构之后,接下来详细了解一下NDK的编译
脚本语法——Android.mk和Application.mk。
Android.mk是在Android平台上构建一个C或者C++语言编写的程序
系统的Makefile文件,不同的是,Android提供了一系列的内置变量来提
供更加方便的构建语法规则。Application.mk文件实际上是对应用程序
本身进行描述的文件,它描述了应用程序要针对哪些CPU架构打包动态
so包、要构建的是release包还是debug包以及一些编译和链接参数等。
(1)Android.mk
Android.mk分为以下几部分。
·LOCAL_PATH:=(call my-dir),返回当前文件在系统中的路
径,Android.mk文件开始时必须定义该变量。
·include(CLEAR_VARS),表明清除上一次构建过程的所有全
局变量,因为在一个Makefile编译脚本中,会使用大量的全局变量,使
用这行脚本表明需要清除掉所有的全局变量。·LOCAL_SRC_FILES,要编译的C或者Cpp的文件,注意这里不需
要列举头文件,构建系统会自动帮助开发者依赖这些文件。
·LOCAL_STATIC_LIBRARIES,所依赖的静态库文件。
·LOCAL_LDLIBS:=-L(SYSROOT)usrlib-llog-lOpenSLES-
lGLESv2-lEGL-lz,指定编译过程所依赖的NDK提供的动态与静态库,SYSROOT变量代表的是NDK_ROOT下面的目录
NDK_ROOTplatformsandroid-18arch-arm,而在这个目录的usrlib目
录下有很多对应的so的动态库以及.a的静态库。
·LOCAL_CFLAGS,编译C或者Cpp的编译标志,在实际编译的时
候会发送给编译器。比如常用的实例是加上-DAUTO_TEST,然后在代
码中就可以利用条件判断ifdef AUTO_TEST来做一些与自动化测试相
关的事情。
·LOCAL_LDFLAGS,链接标志的可选列表,当对目标文件进行链
接以生成输出文件的时候,将这些标志带给链接器。该指令与
LOCAL_LDLIBS有些类似,一般情况下,该选项会用于指定第三方编
译的静态库,LOCAL_LDLIBS经常用于指定系统的库(比如log、OpenGL ES、EGL等)。
·LOCAL_MODULE,该模块的编译的目标名,用于区分各个模
块,名字必须是唯一并且不包含空格的,如果编译目标是so库,那么该
so库的名字就是lib项目名.so。
·include(BUILD_SHARED_LIBRARY),其实类似的include还有
很多,都是构建系统提供的内置变量,该变量的意义是构建动态库,其
他的内置变量还包括如下几种。
·---BUILD_STATIC_LIBRARY:构建静态库。
·---PREBUILT_STATIC_LIBRARY:对已有的静态库进行包装,使
其成为一个模块。
·---PREBUILT_SHARED_LIBRARY:对已有的动态库进行包装,使其成为一个模块。·---BUILD_EXECUTABLE:构建可执行文件。
构建系统提供的这些内置变量在哪里能够看到呢?它们都在
NDK_ROOTbuildcore目录下,这里面会有所有预先定义好的
Makefile,开发者include一个变量,实际上就是把对应的Makefile包含到
Android.mk中,包括前面提到的CLEAR_VARS,其也是该目录下面的
一个Makefile。
·include(call all-makefiles-under,(LOCAL_PATH)),也是
构建系统提供的变量,该命令会返回该目录下所有子目录的Android.mk
列表。
上面已经清楚地讲解了Android.mk里的基本语法规则,那么,在输
入命令ndk-build之后,系统到底会使用哪些编译器以及打包器和链接器
来编译我们的程序呢?
会使用NDK_ROOTtoolchainsarm-linux-androideabi-
4.8prebuiltdarwin-x86_64bin目录(以Mac平台为例)下面的gcc、g++、ar、ld等工具。同样在该目录下的strip工具将会用于清除so包里面
的源码,nm工具可以供开发者查看静态库下的符号表。
那么进行gcc编译的时候,头文件将放在哪里呢?
NDK_ROOTplatformsandroid-18arch-armusrinclude目录下会存
放编译过程所依赖的头文件。
那么在链接过程中,经常使用的log或者OpenSL ES以及OpenGL ES
等库又将放在哪里呢?
答案其实已在前文中提到过,NDK_ROOTplatformsandroid-
18arch-armusrlib目录下会存放链接过程中所依赖的库文件。
(2)Application.mk
Application.mk分为以下几个部分。
·APP_ABI:=XXX,这里的XXX是指不同的平台,可以选填的有
x86、mips、armeabi、armeabi-v7a、all等,值得一提的是,若选择all则
会构建出所有平台的so,如果不填写该项,那么将默认构建为armeabi平台下的库。由于工作的原因,笔者和Intel的员工打过交道,构建
armeabi-v7a平台的so之所以可以运行在Intel x86架构的CPU平台下,是
因为Intel针对armeabi做了兼容,但是如果想要应用以最小的能耗、最高
的效率运行在Intel x86平台上,则还是要指定构建的so为x86平台。因
此,如果想要提高App的运行性能,则还需要编译出x86平台。类似于
前面介绍的iOS平台,如果不考虑模拟器的话,则仅需要构建armv7与
arm64平台架构,那么对于Android平台呢?对于armv7-a,肯定是要编
译的;至于arm64-v8a这个平台,其实已经占到了50%以上,最好也将其
单独编译出来;同时armv5这个平台的设备还是存在的,当然不同App
在不同架构下的比例也不尽相同,读者可以根据实际场景来决定编译的
平台数目。这里需要注意的是,编译arm64-v8a的时候使用的交叉工具
编译链与之前的armv7所在的目录有比较大的差异,其目录存在于:
·NDK_ROOTtoolchainsaarch64-linux-android-4.9prebuiltdarwin-
x86_64bin
·编译过程中使用的编译工具都存在于上述目录下。
·APP_STL:=gnustl_static,NDK构建系统提供了由Android系统给
出的最小C++运行时库(systemliblibstdc++.so)的C++头文件。然而,NDK带有另一个C++实现,开发者可以在自己的应用程序中使用或链接
它,定义APP_STL可选择它们中的一个,可选项包括:stlport_static、stlport_shared、gnustl_static。
·APP_CPPFLAGS:=-std=gnu++11-fexceptions,指定编译过程的
flag,可以在该选项中开启exception rtti等特性,但是为了效率考虑,最
好关闭rtti。
·NDK_TOOLCHAIN_VERSION=4.8,指定交叉工具编译链里面的
版本号,这里指定使用4.8。
·APP_PLATFORM:=android-9,指定创建的动态库的平台。
·APP_OPTIM:=release,该变量是可选的,用来定
义“release”或“debug”,“release”模式是默认的,并且会生成高度优化的
二进制代码;“debug”模式生成的是未优化的二进制代码,但是可以检
测出很多的BUG,经常用于调试阶段,也相当于在ndk-build指令后边直
接加上参数NDK_DEBUG=1。2.如何交叉编译
上文讲解了NDK的结构,以及构建系统的基本语法。本节将直接对
LAME、FDK_AAC、X264这三个库进行交叉编译。
(1)LAME的交叉编译
在Android的编译中,一般情况下会使用一个Shell脚本文件,指定
好编译器里面的各个工具,然后把对应的Configure的命令与选项开关配
置好,最后执行该Shell脚本:
!binbash
NDK_ROOT=Usersapplesoftandroidandroid-ndk-r9b
PREBUILT=NDK_ROOTtoolchainsarm-linux-androideabi-4.6prebuiltdarwin-x86_64
PLATFORM=NDK_ROOTplatformsandroid-9arch-arm
export PATH=PATH:PREBUILTbin:PLATFORMusrinclude:
export LDFLAGS=-LPLATFORMusrlib -LPREBUILTarm-linux-androideabilib
-march=armv7-a
export CFLAGS=-IPLATFORMusrinclude -march=armv7-a -mfloat-abi=softfp -mfpu=vfp
-ffast-math -O2
export CPPFLAGS=CFLAGS
export CFLAGS=CFLAGS
export CXXFLAGS=CFLAGS
export LDFLAGS=LDFLAGS
export AS=PREBUILTbinarm-linux-androideabi-as
export LD=PREBUILTbinarm-linux-androideabi-ld
export CXX=PREBUILTbinarm-linux-androideabi-g++ --sysroot={PLATFORM}
export CC=PREBUILTbinarm-linux-androideabi-gcc --sysroot={PLATFORM}
-march=armv7-a
export NM=PREBUILTbinarm-linux-androideabi-nm
export STRIP=PREBUILTbinarm-linux-androideabi-strip
export RANLIB=PREBUILTbinarm-linux-androideabi-ranlib
export AR=PREBUILTbinarm-linux-androideabi-ar
.configure --host=arm-linux \--disable-shared \--disable-frontend \--enable-static \--prefix=.armv7a
make clean
make -j8
make install
下面就来针对该脚本的每一行命令进行详细解释。
第一部分是设置NDK_ROOT,并且声明platform和prebuilt,最终配置可在环境变量中查看。
第二部分主要是声明CFLAGS与LDFLAGS,其目的是在编译和链
接阶段找到正确的头文件与链接到正确的库文件。这里需要特别注意的
是,在这两个设置的后边都加上了-march=armv7-a,这相当于是让编译
器知道要编译的目标平台是armv7-a。
第三部分是声明CC、AS、AR、LD、NM、STRIP等工具,具体每
一个工具是做什么用的,前面都已经介绍过了,如果要编译armv5、x86
或者arm64-v8a,那么在代码仓库中会提供全量编译的Shell脚本文件。
第四部分就是使用LAME本身的Configure进行编译裁剪。
第五部分就是使用标准的编译链接和安装。
最终执行脚本成功之后,可以看到在指定的Prefix目录下面,包含
了lib和include目录,里面分别是静态库文件和头文件,这两个目录的作
用在前面已经说过很多遍了,在此不再赘述。
(2)FDK_AAC的交叉编译
!binbash
.configure --host=armv7a \--enable-static \--disable-shared \--prefix=.armv7a
make clean
make -j8
make install
注:这里没有给出声明NDK_ROOT以及各个编译工具的代码。
其实FDK_AAC的交叉编译并没有什么可解说的,配置好环境变量
之后,执行Configure,然后安装就可以了。
(3)X264的交叉编译
!binbash
.configure --prefix=PREFIX \--enable-static \--enable-pic \--enable-strip \--disable-cli \--disable-asm \--extra-cflags=-march=armv7-a -O2 -mfloat-abi=softfp -mfpu=neon \--host=arm-linux \--cross-prefix=PREBUILTbinarm-linux-androideabi- \--sysroot=PLATFORM
注:这里没有给出声明NDK_ROOT以及各个编译工具的代码。
对于X264的编译裁剪,这里有如下几个关键点。
·extra-cflags选项的配置。针对armv7-a的CPU打开了NEON的优化运
行指令,并且打开了O2编译优化,这是非常重要的一点。
·--disable-asm选项的配置。如果不禁用掉asm指令,则意味着将会
禁止neon的指令。2.3.4 使用LAME编码MP3文件
2.3.3节为两个平台交叉编译了多个库,本节就以上文编译出来的
LAME库进行编码工作。本节要实现的目标是,在添加好C++支持的项
目中加入编码MP3文件的功能。当点击按钮的时候,输入的是一个PCM
文件的路径和一个MP3的路径,等运行完毕,电脑上的播放器直接就可
以播放该MP3文件。
首先,用一个C++的类来实现其业务逻辑,输入就是两个char指针
类型的路径,编码成功之后,输出一行编码成功的Log,然后在之前的
项目基础上集成进这段代码,再运行并测试。
1.编码工具类的编写
首先新建两个文件:mp3_encoder.h和mp3_encoder.cpp。
先看一下头文件应该如何编写,头文件其实就是用于定义该类对外
提供的接口。
这里提供的是一个Init接口,输入的是一个PCM FilePath和一个MP3
FilePath,会判定输入文件是否存在、初始化LAME以及初始化输出文件
的资源,返回值是该函数是否成功初始化了所有的相关资源,成功则返
回true,否则返回false。
此外,还要再提供一个encode方法,负责读取PCM数据,并且调用
LAME进行编码,然后将编码之后的数据写入文件。
最后再对外提供一个销毁资源的接口destroy方法,用于关闭所有的
资源。
按照上述分析,建立头文件如下:
class Mp3Encoder {
private:
FILE pcmFile;
FILE mp3File;
lame_t lameClient; public:
Mp3Encoder;
~Mp3Encoder;
int Init(const char pcmFilePath, const char mp3FilePath, int
sampleRate, int channels, int bitRate);
void Encode;
void Destory;
};
既然头文件已经定义好了,接下来就一起来实现。
首先是Init方法的实现,以读二进制文件的方式打开PCM文件,以
写入二进制文件的方式打开MP3文件,然后初始化LAME。具体代码如
下:
int Mp3Encoder::Init(const char pcmFilePath, const char
mp3FilePath, int sampleRate, int channels, int bitRate) {
int ret = -1;
pcmFile = fopen(pcmFilePath, rb);
if(pcmFile) {
mp3File = fopen(mp3FilePath, wb);
if(mp3File) {
lameClient = lame_init;
lame_set_in_samplerate(lameClient, sampleRate);
lame_set_out_samplerate(lameClient, sampleRate);
lame_set_num_channels(lameClient, channels);
lame_set_brate(lameClient, bitRate 1000);
lame_init_params(lameClient);
ret = 0;
}
}
return ret;
}
其次是Encode方法的实现,函数主体是一个循环,每次都会读取一
段bufferSize大小的PCM数据buffer,然后再编码该buffer,但是在编码
buffer之前得把该buffer的左右声道拆分开,再送入到LAME编码器,最
后将编码之后的数据写入MP3文件中。具体代码如下:
void Mp3Encoder::Encode {
int bufferSize = 1024 256;
short buffer = new short[bufferSize 2];
short leftBuffer = new short[bufferSize 4];
short rightBuffer = new short[bufferSize 4];
unsigned char mp3_buffer = new unsigned char[bufferSize];
size_t readBufferSize = 0;
while ((readBufferSize = fread(buffer, 2, bufferSize 2, pcmFile)) > 0) {
for (int i = 0; i < readBufferSize; i++) {
if (i % 2 == 0) { leftBuffer[i 2] = buffer[i];
} else {
rightBuffer[i 2] = buffer[i];
}
}
size_t wroteSize = lame_encode_buffer(lameClient, (short
int ) leftBuffer, (short int ) rightBuffer,(int)(readBufferSize 2), mp3_buffer, bufferSize);
fwrite(mp3_buffer, 1, wroteSize, mp3File);
}
delete[] buffer;
delete[] leftBuffer;
delete[] rightBuffer;
delete[] mp3_buffer;
}
最后是Destroy方法,关闭PCM文件,关闭MP3文件,销毁LAME。
具体代码如下:
void Mp3Encoder::Destory {
if(pcmFile) {
fclose(pcmFile);
}
if(mp3File) {
fclose(mp3File);
lame_close(lameClient);
}
}
实现结束之后,接下来就是把该类集成到iOS和Android客户端。
2.iOS集成
首先打开2.1节开发的项目——添加了C++支持的iOS工程。
然后在ViewController.mm中直接实例化C++类型的类Mp3Encoder,将vocal.pcm的文件放入沙盒中,利用下面这行代码获取PCM的路径:
[[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent: @vocal.pcm];
接着利用下面的代码获取最终要写入的MP3文件的路径:
NSArray paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES);
NSString documentsDirectory = [paths objectAtIndex:0];[documentsDirectory stringByAppendingPathComponent:@vocal.mp3];
最后直接调用Mp3Encoder类的encode方法,编码音频文件。最终编
码方法执行结束,不要忘记调用该C++类的destroy方法,销毁所有资
源。然后运行程序,点击编码按钮,等待编码结束之后,通过
Xcodedevice中的Download沙盒功能,将MP3文件提取出来,读者可以
使用电脑默认的播放器播放该MP3文件,试试看是否能正常播放。
3.Android集成
首先打开2.2节开发的项目——添加了C++支持的Android工程。
然后在com.phuket.tour.studio包下修改Mp3Encoder文件,并写入三
个native方法:
public native int init(String pcmPath, int audioChannels, int bitRate, int
sampleRate, String mp3Path);
public native void encode;
public native void destroy;
接着重新生成JNI接口文件,并且在实现的C++文件中实现这三个
方法。
在init方法中实例化Mp3Encoder类,然后调用初始化方法:
const char pcmPath = env->GetStringUTFChars(pcmPathParam, NULL);
const char mp3Path = env->GetStringUTFChars(mp3PathParam, NULL);
encoder = new Mp3Encoder;
encoder->Init(pcmPath, mp3Path, sampleRate, channels, bitRate);
env->ReleaseStringUTFChars(mp3PathParam, mp3Path);
env->ReleaseStringUTFChars(pcmPathParam, pcmPath);
在encode方法中直接调用Mp3Encoder的encode方法;在destroy方法
中则调用Mp3Encoder的destroy方法;最后,修改Android.mk文件,将最
新的C++文件加入LOCAL_SRC中,并且在include的路径中增加LAME
的头文件所在的路径:
LOCAL_C_INCLUDES := (LOCAL_PATH)..3rdpartylameinclude当然,还需要在链接阶段加入编译出来的静态库,因此需要键入以
下命令:
LOCAL_LDLIBS += -L(LOCAL_PATH)3rdpartylamelib -lmp3lame
这样重新执行ndk-build命令就可以将最新的so库打出来了。 [1]
现在,在MainActivity中,传入正确的输入路径和输出路径(需要
提前准备一段PCM数据放入输入路径下面),等编码结束之后,取出输
出路径的MP3文件,在电脑上播放,确认一下是否正确。
[1] so库是gcc或g++编译过后形成的一种动态库,最终可以被加载到程
序中使用。2.4 本章小结
本章主要介绍了如何创建一个Android项目和一个iOS项目,然后为
这两个项目增加C++支持,接着讨论了交叉编译,最后利用交叉编译出
来的LAME库,在两个平台上完成编码MP3音频文件的功能。本章的内
容是音视频开发的基础内容,希望读者熟练掌握。第3章 FFmpeg的介绍与使用
若要讲解音视频的开发,首先不得不提开源框架FFmpeg。该开源
框架为音视频开发者们提供了非常大的帮助,其也是全世界的音视频开
发工程师都应该掌握的工具。FFmpeg是一套可以用来记录、处理数字
音频、视频,并将其转换为流的开源框架,采用LPL或GPL许可证,提
供了录制、转换以及流化音视频的完整解决方案。它的可移植性或者说
跨平台特性非常强大,可以用在Linux服务器、PC(包括Windows、Mac OS X等)、移动端设备(Android、iOS等移动设备)等平台。名称
中的mpeg来自视频编码标准MPEG,而前缀FF是Fast Forward的首字母
缩写。本章会从编译开始讲解,然后介绍命令行工具的使用,接着会介
绍FFmpeg在代码层面提供给开发者的API,最后会从源码的角度分析一
下整个FFmpeg框架,现在就让我们开始吧。3.1 FFmpeg的编译与命令行工具的使用
3.1.1 FFmpeg的编译
1.FFmpeg编译选项详解
首先到FFmpeg官网上下载稳定版本的FFmpeg源码,本章将会从下
载到的最干净的代码开始逐步进行操作。然后将下载的源码解压到一个
目录中,FFmpeg与大部分GNU软件的编译方式类似,都是通过
configure脚本来实现编译前定制的,这种方式允许用户在编译前对软件
进行裁剪,同时通过对最终运行到的系统以及目标平台的配置来决定对
某些模块设定合适的配置。configure脚本运行完毕之后,会生成
config.mk和config.h这两个文件,分别作用到makefile和源代码的层次,由这两个部分协同实现对编译选项的控制。所以下面先来看看configure
的脚本,可以利用它的help命令来查看其到底提供了哪些选项?
.configure -help
·标准选项:GNU软件例行配置项目,例如安装路径、--prefix=…
等。
·编译、链接选项:默认配置是生成静态库而不是生成动态库,例
如--disable-static、--enable-shared等。
·可执行程序控制选项:决定是否生成FFmpeg、ffplay、ffprobe和
ffserver等。
·模块控制选项:裁剪编译模块,包括整个库的裁剪,例如--disable-
avdevice;一组模块的筛选,例如--disable-decoders;单个模块的裁剪,例如--disable-demuxer。
·能力展示选项:列出当前源代码支持的各种能力集,例如--list-
decoders、--list-encoders。
·其他:允许开发者深度定制,如交叉编译环境配置、自定义编译器参数的设定等。
下面先给出一个总览,大体了解一下FFmpeg的整体结构,如图3-1
所示。
图 3-1
下面这段代码是一个配置实例,用于实现运行于Android系统中的
FFmpeg库的编译:
.configure -prefix=. \--cross-prefix=NDK_TOOLCHAIN_PREFIX \--enable-cross-compile \--arch=arm --target-os=linux \--disable-static -enable-shared \--disable-ffmpeg --disable-ffplay -disable-ffserver -disable-ffprobe
默认的编译会生成4个可执行文件和8个静态库。可执行文件包括用
于转码、推流、Dump媒体文件的ffmpeg、用于播放媒体文件的ffplay、用于获取媒体文件信息的ffprobe,以及作为简单流媒体服务器的
ffserver。8个静态库其实就是FFmpeg的8个模块,具体包括如下内容。
·AVUtil:核心工具库,该模块是最基础的模块之一,下面的许多
其他模块都会依赖该库做一些基本的音视频处理操作。
·AVFormat:文件格式和协议库,该模块是最重要的模块之一,封
装了Protocol层和Demuxer、Muxer层,使得协议和格式对于开发者来说
是透明的。
·AVCodec:编解码库,该模块也是最重要的模块之一,封装了
Codec层,但是有一些Codec是具备自己的License的,FFmpeg是不会默
认添加像libx264、FDK-AAC、lame等库的,但是FFmpeg就像一个平台
一样,可以将其他的第三方的Codec以插件的方式添加进来,然后为开
发者提供统一的接口。
·AVFilter:音视频滤镜库,该模块提供了包括音频特效和视频特效
的处理,在使用FFmpeg的API进行编解码的过程中,直接使用该模块为
音视频数据做特效处理是非常方便同时也非常高效的一种方式。
·AVDevice:输入输出设备库,比如,需要编译出播放声音或者视
频的工具ffplay,就需要确保该模块是打开的,同时也需要libSDL的预
先编译,因为该设备模块播放声音与播放视频使用的都是libSDL库。
·SwrRessample:该模块可用于音频重采样,可以对数字音频进行
声道数、数据格式、采样率等多种基本信息的转换。
·SWScale:该模块是将图像进行格式转换的模块,比如,可以将
YUV的数据转换为RGB的数据。
·PostProc:该模块可用于进行后期处理,当我们使用AVFilter的时
候需要打开该模块的开关,因为Filter中会使用到该模块的一些基础函
数。
如果是比较老的FFmpeg版本,那么有可能还会编译出来avresample
模块,该模块其实也是用于对音频原始数据进行重采样,但是现在已经
被废弃掉了,不再推荐使用该库,而是使用swrresample库进行替代。
如何为FFmpeg平台引入第三方编解码库呢?下面就以最常用的LAME、X264、FDK-AAC进行举例。前面的章节中已经介绍了这三个
库在Android和iOS平台上的交叉编译,现在就假设已经交叉编译出了
LAME、X264、FDK-AAC的静态库与头文件,并且在FFmpeg的源码目
录下建立了external-libs目录,还在其中建立了LAME、X264、FDK-
AAC三个目录,每个目录中的结构都包含了include和lib两个目录,并且
将编译出来的头文件和静态库文件分别都放到了这两个目录下面。
现在修改编译脚本如下。
新增X264编码器需要新增以下脚本:--enable-muxer=h264 \--enable-encoder=libx264 \--enable-libx264 \--extra-cflags=”-Iexternal-libsx264include” \--extra-ldflags=”-Lexternal-libsx264lib” 新增LAME编码器需要新增以下脚本:--enable-muxer=mp3 \--enable-encoder=libmp3lame \--enable-libmp3lame \--extra-cflags=”-Iexternal-libslameinclude” \--extra-ldflags=”-Lexternal-libslamelib” 新增FDK-AAC编码器需要新增以下脚本:--enable-encoder=libfdk_aac \--enable-libfdk_aac \--extra-cflags=”-Iexternal-libsfdk-aacinclude” \--extra-ldflags=”-Lexternal-libsfdk-aaclib” 读者可以按照自己的应用场景,把需要编译进来的第三方库以修改
脚本文件的方式进行编译,然后以命令行模式或者以API调用的方式进
行使用。
在FFmpeg中,有一个类型的filter称为bit stream filter,要想在开发
过程中使用该filter,则需要在编译的过程中打开它。该filter存在的意义
主要是应对某些格式的封装转换行为。比如AAC编码,常见的有两种封
装格式:一种是ADTS格式的流,是AAC定义在MPEG2里面的格式;另外一种是封装在MPEG4里面的格式,这种格式会在每一帧前面拼接一
个用声道、采样率等信息组成的头。开发者完全可以手动拼接该头信
息,即将AAC编码器输出的原始码流(ADTS头+ES流)封装进MP4、FLV或者MOV等格式的容器中时,需要先将ADTS头转换为MPEG-4
AudioSpecficConfig(描述了编码器的配置参数)头,并去掉原始码流
中的ADTS头(只剩下ES流)。但是使用FFmpeg提供好的aac_adtstoasc
类型的bit stream filter可以非常方便地进行转换,FFmpeg为开发者隐藏
了实现的细节,并且提供了更好的代码可读性。若想要正常使用这个
filter,则需要在编译过程中打开下面这个选项:--enable-bsf=aac_adtstoasc
AAC的bit stream filter常常应用在编码的过程中。与音频的AAC编
码格式相对应的是视频中的H264编码,它也有两种封装格式:一种是
MP4封装的格式;一种是裸的H264格式(一般称为annexb封装格式)。
FFmpeg中也提供了对应的bit stream filter,称为H264_mp4toannexb,可
以将MP4封装格式的H264数据包转换为annexb封装格式的H264数据
(其实就是裸的H264的数据)包。当然,也可以手动写代码来实现这
件事情,但是既然FFmpeg提供了这样好用的模块,我们为什么不用
呢?要使用它也只需要在编译过程中打开下面这个选项即可:--enable-bsf=h264_mp4toannexb
H264的bit stream filter常常应用于视频解码过程中,特别是后期在
讲解使用各个平台上提供的硬件解码器时,一定会用到该bit stream
filter。
2.FFmpeg的交叉编译
第2章已经介绍了交叉编译的原理,并且交叉编译出了LAME、FDK_AAC、X264等第三方库,本章也已经介绍了FFmpeg中Configure
的大部分关键语法与定义,因此这里就直接开始交叉编译FFmpeg吧!
不过,对应于Android和iOS平台会有一些通用的配置选项,这里先列出
通用的配置选项,代码如下:
CONFIGURE_FLAGS=--disable-shared \--enable-static \--disable-stripping \--disable-ffmpeg \--disable-ffplay \--disable-ffserver \--disable-ffprobe \--disable-avdevice \--disable-devices \--disable-indevs \--disable-outdevs \--disable-debug \--disable-asm \--disable-yasm \--disable-doc \--enable-small \--enable-dct \--enable-dwt \--enable-lsp \--enable-mdct \--enable-rdft \--enable-fft \--enable-version3 \--enable-nonfree \--disable-filters \--disable-postproc \--disable-bsfs \--enable-bsf=aac_adtstoasc \--enable-bsf=h264_mp4toannexb \--disable-encoders \--enable-encoder=pcm_s16le \--enable-encoder=aac \--enable-encoder=libvo_aacenc \--disable-decoders \--enable-decoder=aac \--enable-decoder=mp3 \--enable-decoder=pcm_s16le \--disable-parsers \--enable-parser=aac \--disable-muxers \--enable-muxer=flv \--enable-muxer=wav \--enable-muxer=adts \--disable-demuxers \--enable-demuxer=flv \--enable-demuxer=wav \--enable-demuxer=aac \--disable-protocols \--enable-protocol=rtmp \--enable-protocol=file \--enable-libfdk_aac \--enable-libx264 \--enable-cross-compile \--prefix=INSTALL_DIR
可以看到为了达到最小的包体积,需要先关掉所有的模块,然后再
打开具体的编解码器、解析器、解复用器、协议,并且这里开启了两个
第三方的Codec:一个是FDK_AAC;另外一个是X264。此处还关闭了命令行工具与帮助文档、输入输出设备、动态库生成,同时还打开了静
态库的生成。由于要进行交叉编译,所以要在倒数第二行打开交叉编译
的选项,在最后一行指定安装库的目标目录。
对于Android平台来讲,Shell脚本需要添加如下内容:
ANDROID_NDK_ROOT=Usersapplesoftandroidandroid-ndk-r9b
PREBUILT=ANDROID_NDK_ROOTtoolchainsarm-linux-androideabi-4.8prebuiltdarwin-x86_64
PLATFORM=ANDROID_NDK_ROOTplatformsandroid-8arch-arm
.configure CONFIGURE_FLAGS \--target-os=linux \--arch=arm \--cross-prefix=PREBUILTbinarm-linux-androideabi- \--sysroot=PLATFORM \--extra-cflags=-marm -march=armv7-a -Ifdk_aacinclude -Ix264 include \--extra-ldflags=-marm -march=armv7-a -Lfdk_aaclib -Lx264 lib
可以看到,上述脚本中指定了运行的平台与架构,指定了编译器、链接器的前缀,以用于执行真正的编译与链接操作,然后给出sysroot和
编译、链接参数。这里是以armv7a作为编译目标架构的,如果读者想自
行编译armv8或者x86的平台,可以查看代码仓库中的编译脚本,这里不
再列举。
对于iOS平台来讲,Shell脚本需要添加如下内容:
.configure CONFIGURE_FLAGS \--target-os=darwin--cc=xcrun -sdk iphoneos clang \--arch=armv7 \--extra-cflags=-arch armv7 -mios-version-min=7.0 -Ifdk_aacinclude
-Ix264include \--extra-ldflags=-arch armv7 -mios-version-min=7.0 -Lfdk_aaclib -Lx264 lib
可以看到,上述脚本中设置了编译器以及目标运行平台,需要说明
的是,这里指定了最小的iOS的运行平台是7.0,否则将这个库集成到
Xcode中的时候会遇到平台不匹配的警告,此外,需要注意的是这里并
没有打开bitcode,这会导致集成进入Xcode之后必须将项目的bitcode选
项关闭掉。若要为编译的库打开bitcode选项,那么在编译参数中增加下
面这行参数就可以了:-fembed-bitcode
3.FFmpeg命令行工具的编译与安装
在介绍FFmpeg的命令行之前,应先安装FFmpeg,前面已经介绍过
了FFmpeg的编译,但那是基于Android平台的交叉编译,而不是安装在
PC上的工具,虽然有些读者可能会说我们做的是移动端上的开发,和
PC上的FFmpeg有什么关系呢?但是不可否认的是,开发者用于开发的
机器上有一套强大的音视频工具,对开发移动端上的音视频项目是非常
重要的。相信有了上文的介绍,在这里介绍FFmpeg的配置与安装应该
会很轻松。
下面给出一个编译脚本config_pc.sh:
!binbash
.configure \--enable-gpl \--disable-shared \--disable-asm \--disable-yasm \--enable-filter=aresample \--enable-bsf=aac_adtstoasc \--enable-small \--enable-dct \--enable-dwt \--enable-lsp \--enable-mdct \--enable-rdft \--enable-fft \--enable-static \--enable-version3 \--enable-nonfree \--enable-encoder=libfdk_aac \--enable-encoder=libx264 \--enable-decoder=mp3 \--disable-decoder=h264_vda \--disable-d3d11va \--disable-dxva2 \--disable-vaapi \--disable-vda \--disable-vdpau \--disable-videotoolbox \--disable-securetransport \--enable-libx264 \--enable-libfdk_aac \--enable-libmp3lame \--extra-cflags=-Ipc_fdk_aacinclude -Ix264_pcinclude -Ipc_lameinclude \--extra-ldflags=-Lpc_fdk_aaclib -Lx264_pclib -Lpc_lamelib \--prefix='UsersappleDesktopffmpegtmp_1'如果该文件没有执行权限,那么请执行以下命令为该文件增加执行
权限:
chmod a+x .config_pc.sh
然后就可以执行该Shell脚本文件,对FFmpeg进行配置了:
.config_pc.sh
该脚本执行结束之后,就来执行以下命令进行编译与安装:
make make install
安装结束之后,进入到prefix指定的目录下查看,具体会看到如下
几个目录。
·bin:编译结束的命令行工具所在的目录,后文会详细介绍该目录
下的工具。
·include:编译结束的头文件都存放在该目录下面,如果要以编写
代码的方式调用FFmpeg的API去完成工作(这也是后面会介绍的内
容),就需要把include中的目录放到includes的配置中(Android下的
makefile文件)或者Header Search Path中(iOS平台下工程文件的配置选
项)。
·lib:其中存放的是编译出来的静态库文件,其在以编写代码的方
式调用FFmpeg的API时会使用到,在编译阶段会使用到上一步提到的
include目录,而在链接阶段则会使用到这个lib目录下面的静态库了。
·share:该目录中存放了一些examples,其中展示了如何使用代码
的方式调用FFmpeg的API,其实可以切换到configure脚本所在的目录,然后执行make examples命令及make install,再到doc下面的example里找
到对应的二进制文件,这样就可以进行调试或者写出自己的测试程序
了。
有的读者可能会发现一个问题,在bin目录的下面没有ffplay,这又是为什么呢?因为ffplay实际上是客户端ffplay.c的C程序编译出来的,该
ffplay.c需要依赖avdevice模块,而avdevice模块使用了sdl的API,如果你
的PC上没有sdl(1.x版本,最常用的就是1.2.0),那么ffplay就会编译不
出来了。所以要想编译出命令行工具ffplay,首先得编译基础库sdl,可
以在自己的PC上利用安装软件包工具进行安装,以Mac OS系统为例,使用brew进行安装,如果没有brew的话,则首先安装brew,可以执行下
面命令进行Homebrew的安装:
ruby -e (curl -fsSL https:raw.githubusercontent.comHomebrewinstallmasterinstall)
等待一段时间,brew就安装好了,之后即可用brew安装sdl,执行下
述命令:
brew install sdl
等待下载并且安装完毕之后,重新执行上述FFmpeg的配置和安装
步骤,待make install结束之后,再去bin目录下就可以找到命令行工具
ffplay了。
当然,还可以使用另外一种方式为开发机器安装FFmpeg,即通过
安装包管理工具的方式进行安装。在Mac OS系统上直接在命令行下键
入以下命令:
brew install ffmpeg
可以看到brew会先下载X264作为视频的编码库,并安装成功,然
后就可以直接使用工具FFmpeg了,但是却没有工具ffplay,如果想安装
ffplay,那么执行如下命令:
brew uninstall ffmpeg
brew install ffmpeg --with-ffplay
可以看到brew会先下载sdl,然后再安装ffplay,注意使用brew安装
的FFmpeg已经是3.0以上的版本,并且使用的sdl也已经是2.0版本了。至此,关于FFmpeg的编译部分就介绍完毕了,现在回顾一下本节
的内容,本节主要介绍了如何控制FFmpeg各个模块的开关,还介绍了
如何将第三方编解码器编译到FFmpeg平台中,接着介绍了bit stream
filter类型的过滤器,然后把FFmpeg交叉编译到Android平台和iOS平台,最后在PC上成功编译出FFmpeg。3.1.2节将会介绍编译出来的FFmpeg命
令行工具,以及在工作中如何使用这些工具来提高处理音视频的效率。3.1.2 FFmpeg命令行工具的使用
前面讲解了如何安装FFmpeg相关的命令行,其中涉及ffmpeg、ffprobe、ffplay以及ffserver等命令行工具,本节将重点介绍ffmpeg、ffprobe与ffplay这三个命令行工具,而ffserver则是作为简单的流媒体服
务器存在的 ......
您现在查看是摘要介绍页, 详见PDF附件(11576KB,701页)。





