一、背景
随着高清在用户观影过程中的深度普及,人们已经不仅仅满足于视的享受,更需要听的保证。如何稳定保障音质,甚至增加更多的音效玩法需要一套强大的系统将数据传输、音频实时处理技术、音频输出有效地整合起来;而作为一个可以商业化应用的系统,其应具有高性能、高复用、高可靠的特点,在本文我们将探讨如何打造一套具备这些特性的音频渲染引擎。
二、化繁为简的高复用
考虑高复用的一个基本出发点是基于我们面临的问题的复杂性,如图所示:
如果想覆盖市面基本的用户群体,我们至少需要支持5种操作系统的6种渲染接口;以更少的改动支持不同平台的功能演进,从而降低系统的开发维护成本。
1.技术选型及架构设计
一个完整的音频渲染引擎我们可以划分为以下几个模块:音频接口模块、后处理模块、输出模块、缓存模块、音频焦点管理模块。其中输出模块和音频焦点管理一般是依赖系统接口实现,此部分需要针对性的封装,如android需要通过jni技术来反射调用,iOS等系统则需要利用混编技术进行交叉编译,其余模块基于跨平台和性能的考虑我们采用c++实现,最终可设计出如图架构:
2.渲染接口抽象
上文描述,跨平台的重点是不同平台的渲染接口的封装,对于渲染接口封装可以从两个方向出发,一是渲染流程控制,二是渲染能力。一般的渲染接口均可提供latency获取、音量设置等能力,同时为了保障渲染质量,我们也会加上渲染信息统计等能力。另按照消耗数据方式的不同,渲染接口也可以分为同步写、异步写两大类,其中异步写又可以分为托管型和回调型,大部分的回调型需要系统本身保障数据的生命周期,也为设计抽象增加了难度,如上所述最终我们可以抽象出如图的输出接口:
如图我们可以通过deviceId来实现不同平台的输出模块加载或者同一平台不同接口的平滑切换。
三、追求极致之高性能
通常来说单纯的音频输出本身相对解码等操作不能算作耗能大户,但是随着音频音效不同玩法的涌现,如:变速变调,音量增强,均衡器,空间音频等,音频后处理对于能耗的占用也水涨船高。所以对于后处理模块整体性能把控也变得更加重要。
1.链式pipeline处理
采用pipeline方式的最大好处是可以实现功能解耦、按需加载,随着功能的不断扩展性能也不会由于功能的累积而出现崩坏的情况,这种设计方式可以尽量的节省CPU和内存占用。但是一般基于链表的pipeline实现都会存在插入或删除较复杂的情况,基于以上我们针对性的做了优化:
- 实现了复杂度为o(1)的批量查询算法,一次搞定是否存在指定的多个filter;
- 实现了时间复杂度为o(1)的单filter插入或删除算法;
- 简单驱动,流水线操作,所有操作均由数据驱动,没有额外功耗;
同时pipeline也增加了针对每个filter的耗时监控,如果出现耗时异常的filter可以及时报警,也可以根据filter本身优先级属性来判断是否可以卸载该高耗时filter来保障性能。
2.响应式filter
响应式filter的核心思想是只做被需要的事情,filter仅响应基类和pipeline约定好的接口,且按顺序被pipeline驱动。这样渲染流程和生命周期等的管控均由pipeline和基类配合完成,派生filter可以更加的聚焦自身的业务特性,同时辅助可本地读写的处理buffer,较大的提升性能。其处理流程如图所示:
同时filter的设计更有利于抽象出细粒度的可复用模块,适用于不同音效处理,大大降低代码量,在后续的开发中基于filter的组合变可以产生不同的效果。
3.双链表缓存
从上文表述中可以看出数据流转存在于整个音频渲染周期,甚至对于部分回调型渲染接口,我们还需要保证其在渲染过程中的数据生命周期,所以提升数据存取的效率对于整个引擎的性能都是大有好处的。
对于音频渲染引擎来说其生产者一般为播放器的解码模块,消费者为各平台的渲染接口,生产者产生音频pcm frame的时间大概在几毫秒,而对于采样率为44100,单frame包含1024个采样点的音频帧渲染需要20毫秒,正是这样的速度差导致必然需要数据缓存的地方,而音频渲染引擎一般情况下都需要做后处理,所以在音频渲染引擎中增加缓存模块存储数据先进行后处理再按需进行渲染可以大幅提升性能。
针对音频数据缓存特性我们设计了一种特殊的双链表缓存结构,在共用一组物理缓冲区的基础上,将其逻辑划分为slot queue和data queue两个队列;生产者和消费者只需要面对对应业务的队列操作即可,物理缓冲区循环在两个队列之间进出,在进入slot queue后即可自动释放自身内存,等待下次申请,同时队列也提供同步异步两种获取方式,用以提升不同场景的性能。
四、面面俱到的高可靠
衡量一个音频渲染引擎可靠性可以从这几个方面出发:引擎初始化及渲染过程中的高成功率;数据渲染进度稳定性及信息反馈;数据处理异常的预警、修复、上报能力的健全性。
1.latency处理机制
由于数据传输协议的差异,不同音频输出设备延迟也不尽相同,内置扬声器、有线耳机一般latency在100ms以内,蓝牙耳机等无线传输方案通常延迟都在200ms以上,并且由于内部缓存的存在,渲染过程中总体的音频输出延迟是在动态变化的。
我们对不同平台的获取latency方式进行了封装,配合本地缓存延迟统计,每隔1秒(可配置),以20毫秒为粒度对总体latency进行实时计算并反馈业务方进行音画同步的调整,较好地提高了播中插拔耳机、链接蓝牙耳机、切换前后台等操作时的音画同步准确性。
2. 异常处理
音频渲染中常遇到的问题有无声、卡顿、噪声等,而造成这些问题的因素较多,为了尽可能多地捕获更多异常,同时能准确定位发生异常的模块,我们设计了面向内容的异常检测和面向流程的异常检测这两种方式:
面向内容的异常检测顾名思义就是需要针对音频内容做检查,利用我们filter的设计,在pipeline的最后挂载异常检测filter,对后处理数据做整体性检查,当前优酷的实际应用中就存在着VAD filter专门做音强检测,可以实时上报检测到的异常信息,面向内容的异常检测主要解决流本身以及后处理对内容带来的损坏。
面向流程的异常检测主要是针对音频数据处理和消耗的过程进行监控,在数据处理阶段针对filter做耗时统计,在device阶段是根据音频属性sampleRate对音频数据消耗做监控,一般device针对音频数据吞吐量低于sampleRate的80%可能造成噪声等问题,基于面向流程的机制,我们可以准确定位到具体哪一个filter比较耗时,或者是否device本身存在吞吐数据的问题,并且可以通过卸载filter或者重启device的操作来实现异常修复。
3.成功率保障
音频渲染出错最多在两个地方:引擎初始化阶段由于多实例复用、焦点竞争等原因较大可能初始化失败;后处理阶段,处理算法复杂多样,且存在多个filter同时存在的情况,较容易发生错误。
针对初始化阶段的错误我们引入了焦点管理系统,可根据情况来获取、共享或放弃焦点,保证可用性,同时我们在同一个平台至少采用两种渲染接口,用AB方案增强兼容性,例如在android平台会使用openSL es,但是也会将audiotrack作为备份。
针对后处理阶段的错误我们引入了帧重试机制,某个filter处理过程中遇到错误会立即打断当次pipeline执行,对该音频帧进行重试,保证了渲染过程中数据的完整性。
五、基于“三高”引擎的优酷特色音频处理
上文中大体描述一个高性能、高可靠、高复用音频渲染引擎的架构设计和具体实现,这里我们结合优酷特色的空间音频功能来具体阐述它的实际运转流程。
空间音频能够利用 AirPods Pro 和 iPhone 中的陀螺仪和加速度计来跟踪头部运动和设备位置,比较运动数据,并重新映射声场,因此即使在头部移动时,声场也能固定在 iPhone 上。空间音频是由媒介、手机、输出设备三者密切配合构建出的音频特效,这给想要通过自主开发支持空间音频带来了巨大的技术难度。苹果公司自身推荐的快捷方案也是当前业界使用的主流方案:接入苹果系统播放器,这样只要有适配好的媒介就可以激活空间音频的功能。
使用系统播放器虽然能针对个别视频尽快上线空间音频的功能,却也带来了更多的弊端:无法适配多样的视频格式、无法提供定制的播放体验、无法提供hdr、高倍速等后处理功能。导致用户不得不在很多场景下进行功能取舍,同时对于媒介的严格要求,导致线上海量的单声道或双声道视频无法支持空间音频,这也是为什么在很多宣称支持空间音频的平台上我们总是很难捕捉到空间音频的身影。这里我们将介绍如何依托优酷音频渲染引擎针对全量视频实现媲美系统级的空间音频特效。
如图所示优酷空间音频是综合4种filter实现的音频特效:
- spatial filter主要负责yaw、pitch、roll信息(代表三维空间下右手笛卡尔坐标系各坐标轴对应的旋转角度),具体含义如图:
- resample filter负责实时将音频数据转换为需要格式,在本次空间音频的实施中,我们需要将线上存量的单声道或者双声道音频转换为5.1声道,channelLayout为:left | right | center | low_frequency | side_left | side_right的格式,从而理论上支持线上任何媒介符合空间音频的处理要求;
- 3D audio filter进行音频数据的虚拟环绕声处理,其通过欧拉角进行声场控制;
最终通过VAD filter和pipeline的实时监控,保证在整个特效处理周期内均保证按序、按时、无错地完成渲染流程。
六、展望
依赖当前的“三高”音频渲染引擎,优酷已经实现了诸多实时后处理技术,如:高倍速、空间音频技术、变声等,后续我们将进一步优化该系统,提供用户更多关于音频的技术和产品,敬请期待!