当前位置: 首页 > Web前端 > HTML

QQ音乐的动态歌词是怎么练出来的?

时间:2023-04-02 19:33:47 HTML

本文由云+社区发布作者:QQ音乐技术团队一、背景1、现状歌词浏览已经成为音乐APP的标配,显示和动画效果基本一致,主要是单-线卡拉OK。多行效果和滚动效果。当然,我们也不例外。2.目标我们的目标非常明确。一是提升歌词的基础体验,二是在此基础上提供差异化??的VIP特效,吸引用户激活VIP。2、探索技术方案经过多次需求评审和沟通讨论,各方对需求的目标和细节达成了初步共识。产品希望:炫酷的效果,可以实现逐字动画(位移,翻转,淡入淡出,模糊,粒子效果等),可配置等开发思考:技术架构方案,性能挑战等接下来简单介绍一下介绍确定技术方案的过程。1.技术方案选择这里有两个初步思路,升级现有的歌词组件和开发新的歌词组件。所谓知己知彼,就是通过对主流竞品在移动端的技术方案和PC端同类方案的技术研究和分析。最后将技术方案锁定为以下三种:现有歌词组件升级Shader序列帧动画ASS序列帧动画2.备选技术方案介绍下面简单介绍三种方案的原理和特点,如图下表:换句话说,就是在原生动画开发和帧动画方案之间进行选择。3、技术方案对比下面主要从是否实现特效、开发难度、方案性能、实施成本、跨平台等方面对三种方案进行对比,具体如下表所示:4.通过以上维度确定解决方案综合考虑:现有的歌词组件基本无法实现逐字动画。Shader帧动画开发周期长,实现成本高,逐字动画支持不是很好。ASS实现逐字动画,通过植入动画标签可以实现复杂的特效。它具有开源支持并且是跨平台的。综上所述,ASS方案性价比最高。最终方案也确定采用ASS序列帧动画方案。三、技术架构1、ASS技术工作原理介绍前面简单介绍了ASS字幕和帧动画的原理。我们知道ASS是一种字幕文件格式,属于高级字幕,可以制作出带有特效的华丽字幕。因此,要想在电影或视频上显示ASS特效,首先要做的就是编写ASS特效文件,然后将ASS特效文件解析成序列帧动画位图,最后将这些位图按照特定的顺序进行转换,以一定的帧率播放,可以看到各种特效的动画。如下图:2.如何接入ASS方案2.1合成如下图:首先需要准备显示内容(字幕或歌词内容),比如一个文本文件。有了最基本的文本文件,如何转换成ASS解析器可以解析的ASS文件呢?答案是打K值,指的是给字幕文件加上时间线属性。但是K值是多少?就是ASS中K拉OK的效果标签代码,也就是每一行甚至每一字的时间坐标。有了K值的ASS文件,我们就可以在视频播放器中浏览了,就得到了最基本的逐字染色动画。如果要开发更复杂的特效,需要添加更多的特效标签。对于这部分,可以通过脚本添加动画模板(动画模板是具有特定动画效果的ASS文件),在K值ASS文件中注入动画标签,生成最终的ASS特效文件。至此,一个带有特效的ASS文件就诞生了。2.2分析分析的过程比较简单。解析ASS文件不仅需要ASS文件本身,还需要用于组成ASS文件的字体。这里补充一下,在前面合成的时候,里面的动画模板也需要指定用什么字体来合成。因为会涉及到字体大小、间距等,会影响动画效果和排版。然后回到分析,分析ASS文件加上字体库就可以生成特定序列的帧动画位图。3.技术架构最终解决方案的技术架构:功能划分如下,然后负责存储和合成;客户端负责解析和渲染,呈现给用户最终的动画效果。4.通用性上面提到了该解决方案的通用性和易于重用的特点。那么除了动态歌词,我们还能做什么呢?首先,我们在业务之外,对更高层次的架构进行了抽象,梳理出一个更通用的架构。这里需要补充的是,“字体库”字面意思应该是一堆字体的容器,所以字体库应该存放了很多文字信息等等。但其实不仅是文字还有图形,所以我们的动画效果不能只针对文字,也可以设计一些图形动画效果。所以,这里可以有更多的想象空间。我们在前面的解析过程中提到,解析出一帧一帧的图片后,会直接播放,这样我们就可以实时看到动画效果。那么如果把这些图片保存下来,需要的时候就可以根据业务需要进行播放了。这里可以拆分实时渲染和离线渲染两种方案。这里的渲染提供了两种解决方案:1.实时渲染解析后的位图立即绘制到屏幕上。适用场景:实时性要求高的场景。特点:对系统性能消耗较大,需要注意当前场景的性能开销。2.离线渲染将解析后的位图保存到磁盘,可以在此基础上建立序列帧动画的资源管理。适用场景:适用于异步场景。特点:推荐使用异步线程在后台处理,减少主线程的消耗。您可以根据自己的业务场景和特点,灵活选择或组合这两种方案。以上主要介绍了动态歌词技术方案的实现原理和架构。4.技术难点与挑战在开发过程中,我们遇到了两个重要的问题:一是在运行复杂效果时,动画效果出现肉眼卡顿的现象;另外就是内存的问题,即使是比较简单的效果器,播放后也会占用大量的内存。本文的后半部分将重点介绍卡拉OK如何解决这两个问题。1.卡顿问题描述我们选择了一个比较复杂的效果,其中包含大量的烟雾、花瓣等动画元素,以及位移、变形、模糊等效果。它的每一帧由大约1,000个元素组成。在三星Note3(Android5.0,4核,ARMv7)上,平均只能达到7帧。2、解码和渲染的过程为了解决以上问题,我们需要对ASS从文本文件到渲染到屏幕的整个过程有一个基本的了解。这里我们以Android为例(Ios渲染过程略有不同,其他一致),先看JNI接口:privatenativeintdecodeFrame(longtime,int[]pixels);Java层会传入时间戳time和一个名为pixels的Int数组,time表示当前需要获取哪个时间点的动画效果,libass会分析与该时间点相关的每一行文字,生成一张或多张小图,并得到一系列图片,然后将其合成为一张大图,最后通过像素复制的方式将合成结果输出到像素中。返回Java后,将像素设置为Bitmap,最后交由Canvas进行渲染。3.流程耗时分析通过管理各个关键流程,运行上述复杂效果,我们得到了各个流程的耗时比例:分析46%,合成37%,输出渲染8%,1%为他人。分解成每一帧,以毫秒为单位计算如下:接下来,我们将按照解析、合成、输出、渲染的顺序逐步优化。4.卡顿优化实践1)过滤透明小图前面说过,每一行ass文字都会生成一张或多张小图,这是因为一段文字会被拆解成文字、边框和背景三部分,除此之外,libass不关心这些组件的颜色和透明度。这就导致了这样一个问题:Dialogue:1,0:00:00.00,0:01:00.00,Default,,0,0,0,fx,{\pos(120,100)\1a&HFF&\blur3}全民K歌Whattheaboveasstext实现的是文字镂空效果:1a&HFF&表示文字body是完全透明的,对于这样的透明元素,libass还是会生成小图进行各种处理,但是这个完全没有必要,所以我们对libass做了第一次修改:不会产生无效的透明小图,提高ass解析效率,减少内存分配,对后续合成处理也有积极影响2)PixelsTransparencyjudgment在合成过程中,需要遍历小图的每个像素,拆分成ARGB4通道进行颜色计算dstA=(255*255-(255-k)*(255-dstA))/255;dstB=(k*b+(255-k)*dstB)/255;dstG=(k*g+(255-k)*dstG)/255;dstR=(k*r+(255-k)*dstR)/255;与普通图片??合成不同的是,在歌词动效场景中,小图由文字或虚线等图形组成,常有大量透明像素和完全不透明像素,可以通过判断减少Composite操作:if(k==0){//完全透明,跳过继续;}if(k==255){//完全不透明,直接使用小图的颜色dst=color;continue;}测试5一个在K歌中推出的动效,合成时间减少10%~50%。3)通过透明度的判断可以在一定程度上减少简化计算,但不能完全避免。以Alpha通道的计算为例,包括2次乘法1次除法3次减法,除法特别耗时。因此,对于这些必要的计算,我们将其简化,先进行方程变换:dstA=(255*255-(255-k)*(255-dstA))/255;=(255-(255-k)*(255-dstA)/255);然后替换为255-x=~x和x/255≈x>>8以获得简化结果:dstA=~((~k)*(~dstA))>>8);可以看出,一次计算变成一次乘法和四位运算,实测综合时间减少了26%。4)并行计算经过以上优化后,合成速度快了很多,但这还不够。在合成算法中,像素与像素之间没有联系,因此可以通过并行计算提高合成效率。我们采用了NEON的方案,利用CPU专用模块的128位寄存器同时计算多个像素点,因为ARGB在32位颜色中各占8位,再考虑之后可能实现的16位乘法处理,因此可以利用128位寄存器同时处理8个像素点的计算,达到8倍左右的加速效果,对CPU和帧率起到很大的作用。具体实现如下:5)合成优化前后对比,合成优化已经告一段落,每帧的合成时间从原来的52ms减少到3ms以内。6)取消像素复制输出的过程其实只是一个像素复制操作,就是将合成后的大图输出到JNI传入的Int数组中。除了耗时之外,还会产生额外的Native内存分配。因此,我们优化了这个过程,让合成直接在Int数组中进行,这样原来11ms的输出就完全去掉了。上面说了,当数据到达Java层时,也会调用Bitmap的setPixels方法,将像素信息传递给Bitmap,最后交给Canvas进行绘制,这里的setPixels做的是一样的和刚才输出的过程一样,所有的像素都会被复制一次。因此,我们希望取消这个过程的拷贝,但是Java并没有提供接口让我们获取Bitmap的Buffer,所以我们使用反射方案。优化后渲染时间减少65%。7)双缓冲异步渲染我们知道卡顿的原因是处理一帧的时间太长,达不到我们想要的帧率要求。很容易想到我们是否可以使用多线程同时处理多帧数据呢?结果是失败了,因为libass是单例模式,同时处理多个时间点的分析和综合,会造成一些内部状态混乱,以crash告终。虽然解码不能用多线程,但是渲染和libass无关,还是可以拿出来放到单独的线程中处理。这就引入了一个新问题。解码和渲染线程都在同一内存上运行。他们同时在写和读,数据很容易出错。所以我们申请了一块额外的内存,一块用于解码,一块用于渲染,每次解码完成就进行交换。我们的双缓冲异步渲染方案就是这样出现的。此实现允许libass继续进行而无需等待渲染完成。下一帧数据的解码有效地提高了运动效果的帧率。8)卡顿优化效果总结经过上述优化后,上述低端机Note3的复杂运动效果从原来的7帧增加到15帧。2.内存问题描述在不干扰内存的情况下,卡拉OK线上的一个普通效果播放了3分钟多的作品。Native层内存,这是我们面临的一个很严重的问题,有OOM的风险,系统也可能会频繁的GC导致卡顿1)深入内存分配通过阅读libass的源码,我们了解的更详细ASS分析过程。每个动作文本在libass中被定义为一个事件。首先分析事件中的动画标签和参数,获取某个时刻的所有属性值后,创建文字或图形的轮廓;然后它被处理。进行光栅化处理,再进行拼接、模糊等处理,最后生成小图并重新排列,得到卡住问题中提到的一系列小图。在这样一个过程中,内存分配主要消耗在光栅化和拼接两个过程中,而libass内部已经实现了完整的缓存管理机制,但是其默认缓存比较大,分别为128M和64M。总大小达到了192M,再加上一些其他的内存分配,最大内存使用量会超过200M才稳定下来。另外,libass也提供了一个接口让我们设置缓存的大小,但是只能设置总的缓存大小,不能自定义Bitmap和CompositeBitmap,内部会2:1分配。有了libass的理解,内存问题就变成了:如何找到一个合适的缓存总大小,2:1分配内存是否适合我们的场景。2)找到合适的缓存总大小,统计动态效果。在一次播放过程中,查询缓存M次,查询后命中N次,得到缓存命中率N/M。下图横轴是我们为libass设置的缓存总大小,纵轴是两种缓存的命中率。通过上面的曲线,我们可以得出两个结论:1.随着缓存总大小的增加,新内存获得的收益逐渐减少。对于卡拉OK场景,设置4M~16M比较合理;2、Bitmap和CompositeBitmap的分配不合理,CompositeBitmap可以使用更多的内存。2)找到合适的缓存比例。在卡拉OK线上10多个动效中随机选择5个,统计每个动效1500帧数据对2类缓存的访问请求,并做表。表中数据可以看出,CompositeBitmap需要更大的缓存,平均是Bitmap的1.8倍左右,所以我们将libass中2:1的分配规则调整为1:1.8,最后使用8M内存基本达到原来16M的效果3)内存优化效果设置缓存大小后,内存增长得到控制,处于稳定状态;同时调整分配比例提高了缓存命中率,减少了CPU花在内存分配和光栅化上的时间。总结本文主要介绍动态歌词开发的关键技术和优化策略。技术方案经过多次讨论和预研,采用并行计算大大减少计算时间,优化编译策略解决跨平台问题。在架构设计上,也充分考虑了性能、跨平台、扩展性、组件化、复用性等因素。在实施项目的过程中,John、Harvey、Wing、Comic、Jerry、Rey等团队同学齐心协力,不懈努力!本文已由腾讯云+社区在各渠道发布获取更多新鲜技术干货,可以关注我们的腾讯云技术社区-云家社区公众号和知乎代理号