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

本文由雲+社區發表

作者:QQ音樂技術團隊

一、 背景

1. 現狀

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

2. 目標

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

二、探索技術方案

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

1. 技術方案選型

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

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

2. 備選技術方案介紹

下面簡單介紹一下三種方案的原理和特點,如下表所示:

img

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

3. 技術方案對比

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

img

4. 確定方案

img

通過以上幾個維度的綜合考量:

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

最終方案也確定採用ASS序列幀動畫方案。

三、 技術架構

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. 離線渲染

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

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

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

大家可以根據各自業務場景和特點靈活選擇或者組合使用這兩種方案。

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

四、技術難點與挑戰

在開發過程中,我們遇到了兩個重要的問題:一個是在運行復雜的效果時,動畫效果出現了肉眼可見的卡頓;另一個則是內存的問題,即使是比較簡單的效果播放以後也會佔用大量的內存。本文後半部分將重點闡述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等同學通力合作,付出了不懈的努力!

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章