QQ音樂的動效歌詞是如何實踐的?

本文由雲+社區發表

做者:QQ音樂技術團隊javascript

1、 背景

1. 現狀

歌詞瀏覽已經成爲音樂app的標配,展現和動畫效果也基本上大同小異,主要是單行的逐字染色的卡拉OK效果和多行的滾動效果。固然,咱們也不例外。java

2. 目標

咱們的目標十分明確,一是提高歌詞的基礎體驗,二是在此基礎上,能提供差別化的VIP特效,來吸引用戶開通VIP。算法

2、探索技術方案

通過屢次的需求評審和溝通討論,各方在需求的目標和細節上也達成了初步的統一。 產品的但願 :效果炫酷,能實現逐字動畫(位移,翻轉,漸隱漸現,模糊,粒子特效等),可配置等。開發的思考: 技術架構方案,性能挑戰等,接下來咱們簡單介紹一下肯定技術方案的過程。數組

1. 技術方案選型

這裏最初的思路有兩個方向,升級現有歌詞組件和開發全新歌詞組件。所謂知已知彼,百戰不殆, 經過對移動端面主流競品的技術方案和PC端相似方案的技術調研與分析。最終將技術方案鎖定在如下三種:緩存

  • 現有歌詞組件升級
  • Shader序列幀動畫
  • ASS序列幀動畫

2. 備選技術方案介紹

下面簡單介紹一下三種方案的原理和特色,以下表所示: 多線程

img

總的來講,就是在原生動畫開發和幀動畫方案中進行選擇。架構

3. 技術方案對比

如下主要是從是否實現特效,開發的難度,方案的性能,實現的成本,跨平臺等方面對比三種方案,具體細節以下表所示:app

img

4. 肯定方案

img

經過以上幾個維度的綜合考量:異步

  1. 現有歌詞組件基本上沒法實現逐字動畫。
  2. Shader幀動畫開發週期長,實現成本高,逐字動畫支持不是很好。
  3. ASS實現逐字動畫,可經過植入動畫標籤實現複雜的特效,有開源支持,且跨平臺。
  4. 綜上所述,ASS方案性價比最高。

最終方案也肯定採用ASS序列幀動畫方案。組件化

3、 技術架構

1. ASS技術工做原理介紹

前面簡單介紹了一下什麼ASS字幕和幀動畫的原理。咱們知道ASS是一種字幕文件格式,屬於高級字幕,能夠製做出華麗的特效字幕。因此,要想在電影或者視頻上顯示ASS效果,首先要作的是編寫ASS特效文件,而後再將ASS特效文件解析成序列幀動畫的位圖,最後將這些位圖按照特定的順序和必定的幀率進行播放,就能看到各類特效的動畫。以下圖所示:

img

2. 如何接入ASS方案

2.1 合成

以下下圖所示:,首先,須要準備展現內容(字幕或者歌詞內容),好比一個文本文件,有了最基本的文本文件,怎麼轉換成ASS解析器能解析的ASS文件呢?答案是打K值,打K值是指給字幕文件加上時間軸屬性。而是什麼K值呢,就是ASS中K拉OK的效果標籤代碼,即每行甚至每一個字的時間座標。有了打完K值的ASS文件,咱們就能夠在視頻播放器中瀏覽,也就有了最基本的逐字染色動畫。若是要開發更復雜的特效,就須要加入更多的特效標籤。而這一部分,就能夠經過腳本加上動畫模板(動效模板就是具備特定動畫效果的ASS文件),將動畫標籤注入到打完K值ASS文件中,生成最終的ASS特效文件。至此,一個具備特效的ASS文件就誕生了。

img

2.2 解析

解析的過程相對比較簡單。解析一個ASS文件,不只須要ASS文件自己,還須要知道ASS文件是用什麼字體合成的。這裏補充一下,前面合成的時候,其中的動畫模板也是須要指定是使用哪一種字體來合成的。由於這裏會涉及到字體的大小,間距等,對動畫效果和排版的影響。而後,再回到解析上來,經過ASS文件加上字體庫就能夠解析生成特定序列的幀動畫位圖。

img

3. 技術架構

最終方案的技術架構:功能上劃分以下,後負責存儲和合成;客戶端負責解析和繪製,呈現用戶最終的動畫效果。

4. 通用性

上面提到了這套方案的通用性和易複用的特色。那除了動效歌詞以外,咱們還能夠作些什麼呢?

首先,咱們脫離業務對架構進行更高一層的抽象,梳理出了更通用的架構方。這裏還須要補充一點,「字體庫」,從字面上理解應該是一堆字體的容器,因此字體庫應該是保存了一大堆的文字信息等。但其實不只是文字也能夠是圖形,因此咱們的動畫效果能夠不僅是針對文字的,還能夠設計一些圖形動畫效果。因此,這裏能夠有更多的想像空間。前面解析的過程咱們提到,解析出一幀幀的圖,就拿去直接播放了,這樣咱們就能實時看到動畫效果。那若是把這些圖片保存下來,根據業務需求在須要的時候再播放呢。這裏就能夠拆分出實時渲染和離線渲染兩種方案。

這裏的渲染提供了兩種方案:

1. 實時渲染

將解析出來的位圖當即繪製到屏幕上。

適用場景:實時要求高的場景。

特色: 對系統性能消耗大,須要注意當前場景的性能開銷。

2. 離線渲染

將解析出來的位圖保存到磁盤上,並能夠此基礎上創建序列幀動畫的資源管理。

適用場景:適用於異步化的場景。

特色: 建議採用異步線程在後臺處理,減小對主線程消耗。

你們能夠根據各自業務場景和特色靈活選擇或者組合使用這兩種方案。

以上主要是介紹動效歌詞技術方案的實現原理與架構介紹。

4、技術難點與挑戰

在開發過程當中,咱們遇到了兩個重要的問題:一個是在運行復雜的效果時,動畫效果出現了肉眼可見的卡頓;另外一個則是內存的問題,即便是比較簡單的效果播放之後也會佔用大量的內存。本文後半部分將重點闡述K歌是如何解決這兩個問題的。

1. 卡頓問題描述

咱們選取了一個較爲複雜的效果,包含了大量的煙霧、花瓣等動畫元素 及 位移、形變與模糊等效果,它的每一幀畫面約由1000個元素構成。

img

在三星Note 3(Android 5.0,4核,ARMv7)上運行起來平均只能達到7幀的效果。

2. 解碼與渲染的過程

爲了解決上述問題,咱們須要對ASS由文本文件到渲染至屏幕的整個過程有基本的認識。這裏以Android爲例(Ios在渲染的處理上略有不一樣,而其它是一致的),先看JNI的接口:

private native int decodeFrame(long time, int[] pixels);

Java層會傳入時間戮time及名爲pixels的Int數組,time表明當前須要獲取哪一個時間點的動畫效果,libass接着會對與這一時間點有關的每一行文本進行解析,生成一個或多個的小圖,從而獲得一系列的圖片,而後合成到一個大圖裏面去,最終經過像素拷貝的方式把合成後的結果輸出到pixels,回到Java之後,再把pixels設置至Bitmap,最後交給Canvas進行渲染。

img

3. 過程耗時分析

經過對各關鍵過程的打點並運行前述複雜效果,咱們獲得了各過程的耗時佔比:解析46%、合成37%、輸出與渲染各8%,其它1%。分解到每一幀並以毫秒計算則以下表:

img

接下來,咱們將會按解析、合成、輸出、渲染這樣的順序來逐步優化。

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歌

以上ass文本所實現的是一個文字鏤空效果:

img

1a&HFF&表示文字主體是徹底透明的,而這樣的一個透明的元素,libass依然會生成一個小圖對它進行各類各樣的處理,但這是徹底沒有必要的,因而咱們對libass進行了第一點改造:再也不生成無效的透明小圖,提升ass解析效率的同時也減小了內存的分配,對後續合成的處理也有正向的影響

img

2)像素透明度判斷

在合成的處理中,須要遍歷小圖的每個像素並拆分爲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;

與普通的圖片合成不一樣,在歌詞動效的場景中,小圖由文字或點線之類的圖形構成,每每存在着大量的透明像素及徹底不透明像素,可經過判斷來減小這部分的合成運算:

if(k == 0){   // 徹底透明,跳過
    continue;
} 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 = ~xx / 255 ≈ x >> 8進行替換,獲得簡化後的結果:

dstA = ~((~k) * (~dstA)) >> 8);

可見,一次計算變成了1次乘法與4次位運算,測得合成時間減小了26%。

4)並行計算

通過上述幾項優化,合成速度快了許多,但這還不夠。在合成的算法中,像素點與像素點間是沒有任何聯繫的,因此能夠經過並行計算的方式來提升合成的效率。咱們採用了NEON的解決方案,利用CPU專用模塊的128位寄存器同時對多個像素點進行計算,因32位色彩中ARGB各佔8位,再考慮乘法處理後可能達到的16位,由此,可用128位寄存器同時處理8個像素點的計算,實現約8倍的加速效果,對CPU和幀率可起到明顯的做用。 具體實現以下:

img

5)合成優化先後對比

至此,合成的優化告一段落,每一幀的合成耗時由原來的52ms,降到了3ms之內

img

6)取消像素拷貝

輸出的過程實際上只是作了一次像素拷貝的操做,把合成後的大圖輸出到JNI傳入的Int數組裏面去,除了耗時之後,還會產生額外的一次Native內存分配,因而,咱們優化了這個過程,讓合成直接在Int數組進行,這樣就把原來輸出的11ms徹底去掉了

img

前面提到,數據到了Java層,還會調用Bitmap的setPixels方法把像素信息傳給Bitmap,最後才交給Canvas進行繪製,而這裏的setPixels作的事跟剛剛輸出的過程同樣,會把像素點全都拷貝一次。因此,咱們但願把這一過程的拷貝也給取消掉,但Java並無提供接口給咱們去獲取Bitmap的Buffer,也就採用了反射的方案,優化後,渲染耗時下降了65%。

img

7)雙緩衝異步渲染

咱們知道,卡頓的緣由在於處理一幀的耗時過久,達不到咱們想要的幀率要求,那很容易會想到,咱們是否可使用多線程同時處理多幀數據呢?結果是失敗了,由於libass是單例的模式,同時處理多個時間點的解析合成會致使其內部一些狀態的錯亂,並以crash了結。雖然解碼沒法使用多線程,但渲染與libass無關,仍是能夠拿出來放到一個單獨的線程去處理的。這就引入了一個新的問題,解碼與渲染兩個線程都會操做同一塊內存,一邊在寫、一邊在讀,數據容易出錯。因而,咱們多申請了一塊內存,一個解碼用,一個渲染用,每次解碼完成時進行交換,咱們的雙緩衝異步渲染方案就這樣出現了

img

這一實現讓libass不須要等待渲染的完成就能夠進行下一幀數據的解碼,有效地提升了動效的幀率

8)卡頓優化效果彙總

經歷上述各項優化後,前述複雜動效在低端機Note 3上由原來的7幀達到15幀

img

2. 內存問題描述

在不干預內存的狀況下,在一個3分多鐘的做品上播放了K歌線上的一個普通效果,期間內存的變化見下圖:

img

內存增量達到了180M,且主要是Native層的內存,這是咱們面臨的一個很嚴重的問題,有OOM的風險,系統也有可能所以產生頻繁的GC而引發卡頓

1)深刻內存分配

經過對libass源碼的閱讀,咱們瞭解到了更爲詳細的ASS解析過程

img

每一行動效文本在libass中被定義一個事件,先是對事件中的動畫標籤及參數進行解析,獲得某一瞬間的全部屬性值後建立文字或圖形的輪廓;接着是對它進行柵格化的處理,後續還有拼接、模糊等處理,最終生成小圖並進行重排,就獲得了卡頓問題中所說的一系列小圖。

在這樣的一個過程當中,內存分配主要消耗在柵格化和拼接這2個過程當中,且libass內部已經實現了一套完整的緩存管理機制,只是其默認緩存較大,分別爲128M和64M,總大小達到了192M,再加上些其它的內存分配,最大會佔用超過200M的內存纔會趨於平穩。除此以外,libass還提供了接口給咱們設置緩存的大小,但只能設置總的緩存大小,不能自定義Bitmap和Composite Bitmap分別是多少,其內部會按2:1進行分配。

有了對libass的認識,內存問題也就變成了:如何尋找一個合適的緩存總大小 及 內存的2:1分配是否適合咱們的場景。

2)尋找合適的緩存總大小

統計動效在一次播放的過程當中查詢緩存的次數M,查詢後命中的次數爲N,從而獲得緩存命中率N/M。下圖橫軸表示了咱們給libass設置的緩存總大小,縱軸則是2類緩存的命中率

img

經過上面的曲線,咱們能夠獲得2個結論:1. 隨着緩存總大小的增長,新增內存所得到的收益逐漸變小,對於K歌的場景,設置4M~16M比較合理; 2. Bitmap 與 Composite Bitmap 的分配不合理,可將更多的內存用於Composite Bitmap。

2)尋找合適的緩存比例

從K歌線上的10幾個動效中,隨機選取了5個,統計各個動效處理1500幀數據對2類緩存的訪求並製成了表格

img

經過表格的數據能夠看到,Composite Bitmap須要更大的緩存,平均約爲Bitmap的1.8倍,因而咱們把libass內2:1的分配規則調整爲了1:1.8,最終使用8M的內存基本上達到了原來16M的效果

img

3)內存優化效果

設置緩存大小後,內存增加獲得了控制且處於穩定狀態;而調整分配比例提升了緩存命中率,減小了CPU在內存分配與柵格化等處理上的耗時。

img

小結

本文主要介紹了動效歌詞開發的關鍵技術和優化策略。技術方案經歷了數次討論和預研,採用了並行計算大幅減小運算時間,優化了編譯策略解決了跨平臺問題。在架構設計上,也充分考慮性能,跨平臺,可擴展,組件化,複用性等各方面的因素。在該方案的落地實現過程當中,團隊的John、Harvey、Wing、 Comic,、Jerry、rey等同窗通力合做,付出了不懈的努力!

此文已由騰訊雲+社區在各渠道發佈

獲取更多新鮮技術乾貨,能夠關注咱們騰訊雲技術社區-雲加社區官方號及知乎機構號

相關文章
相關標籤/搜索