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

qcloud發表於2019-02-19

本文由雲+社群發表

作者: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等同學通力合作,付出了不懈的努力!

此文已由騰訊雲+社群在各渠道釋出

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

相關文章