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

来源:https://www.cnblogs.com/qcloud1001/archive/2019/02/19/10403034.html
-Advertisement-
Play Games

本文由雲+社區發表 作者:QQ音樂技術團隊 一、 背景 1. 現狀 歌詞瀏覽已經成為音樂app的標配,展示和動畫效果也基本上大同小異,主要是單行的逐字染色的卡拉OK效果和多行的滾動效果。當然,我們也不例外。 2. 目標 我們的目標十分明確,一是提升歌詞的基礎體驗,二是在此基礎上,能提供差異化的VIP ...


本文由雲+社區發表

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

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

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在Android 插件化技術中(宿主app和插件app設置相同的sharedUserId),動態載入apk有兩種方式: 一種是將資源主題包的apk安裝到手機上再讀取apk內的資源,這種方式的原理是將宿主app和插件app設置相同的sharedUserId,這樣兩個app將會在同一個進程中運行,並可以 ...
  • 今天,直接用AS在小米手機上運行安裝的時候總是報錯:INSTALL_FAILED_USER_RESTRICTED,於是乎,通過以下方式解決: 在開發者選項將USB安裝打開,然後,哈,解決了。記錄一下。 ...
  • Flutter 即學即用系列博客新篇章來了,UI 初窺,帶你探索神秘夢幻的 UI 世界 ...
  • APP開發離不開註冊登錄功能,但是註冊登錄功能開發需要後臺資料庫的支持,對於一些初學者或者對後臺數據 不熟悉的同學來說可能會有些困難。本文介紹一下後端雲: 1. Bmob是國內起步較早的雲後端服務平臺,提供了雲資料庫、消息推送、即時通訊、安全驗證、移動支付等豐富的 功能服務,且這些服務有個人免費版。 ...
  • 首先補充查詢的方法: 1.獲取要素圖層 var layer = webscene.layers.getItemAt(1); //如獲取視圖上已經顯示了的圖層 2.創建查詢對象 var query = layer.createQuery(); 3.設置查詢語句where語句 query.where = ...
  • 過年回來第一篇博客,可能說的不是很清楚,而且心情可能也不是特別的high,雖然今天是元宵,我還在辦公室11.30在加班,但就是想把寫過的代碼記下來,怕以後可能真的忘了。(心將塞未塞,欲塞未滿) VUE+ElementUI 的表單編輯,刪除,保存,取消功能 VUE的表單 當然,表單還有下拉選擇框,ra ...
  • eslint 設置 warning 級別,在 開發編譯失敗的原因,報錯如下: Module build failed: Module failed because of a eslint warning的原因,為自己當時即使是eslint 設置 warning 規則,在熱更新都會報錯,無法編譯通過的 ...
  • 有三種表現形式,css屬性首碼法,選擇器首碼法,以及IE條件註釋法。 css屬性首碼法 IE6+ css hack: Selector { _property: value; }IE7+ css hack: Selector { *+property: value; }IE8+ css hack: ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...