Android音頻可視化

本文做者:熊鋆洋 (網易雲音樂大前端團隊)html

前言

音頻可視化,顧名思義就是將聲音以視覺的方式呈現出來。如何將音頻信號繪製出來?如何將聲音的變化在視覺上清晰的表現出來,讓視覺和聽覺上的感覺一致?這些在 Android 上如何實現?本文將針對這些問題作出解答,儘可能對 Android 上的音頻可視化實現作一個全面的介紹。前端

傅里葉變換

Android 音頻播放的通常流程是:android

  1. 播放器從本地音頻文件或網絡加載編碼後的音頻數據,解碼爲 pcm 數據寫入 AudioTrack
  2. AudioTrack 將 pcm 數據寫入 FIFO
  3. AudioFlinger 中的 MixerThread 經過 AudioMixer 讀取 FIFO 中的數據進行混音後寫入 HAL 輸出設備進行播放

在這個流程中,直接體現音頻特徵,可用於可視化繪製的是 pcm 數據。但 pcm 表示各採樣時間點上音頻信號強度,看起來雜亂無章,難以體現聽覺感知到的聲音變化。pcm 數據僅可用來繪製體現音頻信號平均強度變化的可視化動效,其餘大部分動效須要使用對 pcm 數據作傅里葉變換後獲得的體現各頻率點上信號強度變化的頻域數據來繪製。git

這裏簡單回顧下傅里葉變換,它將信號從時域轉換爲頻域,通常用於信號頻譜分析,肯定其成分。轉換結果以下圖所示:github

pcm 數據是時間離散的,須要使用離散傅里葉變換(DFT),它將包含 N 個複數的序列 { x n } : = x 0 , x 1 , . . . , x N 1 `\{x_n\}:=x_0, x_1, ..., x_{N-1}` 轉換爲另外一個複數序列 { X k } : = X 0 , X 1 , . . . , X N 1 `\{X_k\}:=X_0, X_1, ..., X_{N-1}` ,計算公式爲:算法

X_k=\sum_{n=0}^{N-1}x_n \cdot e^{-i2 \pi {kn \over N}}=\sum_{n=0}^{N-1}x_n \cdot (cos(2\pi {kn \over N})-i \cdot sin(2\pi {kn \over N}))
複製代碼

直接用上面公式計算長度爲 N 的序列的 DFT,時間複雜度爲 O ( N 2 ) `O(N^2)` ,速度較慢,實際應用中,通常會使用快速傅里葉變換(FFT),將時間複雜度降爲 O ( N l o g ( N ) ) `O(Nlog(N))` api

計算公式看起來很複雜,但不懂也不會影響咱們實現音頻可視化,FFT 的計算可使用已有的庫,不須要本身來實現。但爲了從 FFT 的計算結果獲得最終用來繪製的數據,有必要了解如下DFT特性:數組

  • 輸入所有爲實數時,輸出結果知足共軛對稱性: X N k = X k `X_{N-k}=X_k^*` ,所以通常實現只返回一半結果
  • 如原始信號採樣率爲 f s `f_s` ,序列長度爲 N,輸出頻率分辨率爲 f s / N `f_s/N` ,第 k 個點的頻率爲 k f s / N `kf_s/N` ,可用於查找指定頻率範圍在結果中對應的位置
  • 如一個頻率對應輸出的實部和虛部爲 re 和 im,其模爲 M = r e 2 + i m 2 `M=\sqrt{re^2+im^2}` ,原始信號振幅爲 A = { M / N D C 2 M / N o t h e r `A=\begin{cases} M/N & DC \\ 2M/N & other \end{cases}` ,可用於計算分貝和數據縮放

數據源

提供播放 pcm 數據的 FFT 計算結果的數據源有兩種,一種是 Android 系統提供的 Visualizer 類,這種存在兼容性問題,所以咱們引入了另外一種本身實現的數據源。同時,咱們實現了在不修改上層各動效的數據處理和繪製邏輯的基礎上切換數據源,以下圖所示:性能優化

Android Visualizer

系統 Visualizer 提供了方便的 api 來獲取播放音頻的波形或 FFT 數據,通常使用方式是:markdown

  1. 用 audio session ID 建立 Visualizer對象,傳 0 可獲取混音後的可視化數據,傳特定播放器或 AudioTrack 所使用的 audio session 的 ID,可獲取它們所播放音頻的可視化數據
  2. 調 setCaptureSize 方法設置每次獲取的數據大小,調 setDataCaptureListener 方法設置數據回調並指定獲取數據頻率(即回調頻率)和數據類型(波形或 FFT)
  3. 調 setEnabled 方法開始獲取數據,再也不須要時調 release 方法釋放資源

更詳細的 api 信息可查看官方文檔

系統 Visualizer 輸出的數據大小正比於音量,當音量爲 0 時,輸出也爲 0,可視化效果會隨音量變化。

使用系統 Visualizer 存在兼容性問題,在有些機型上會致使系統音效失效,如要在全部機型上都能無反作用地展現動效,須要實現自定義 Visualizer

自定義 Visualizer

做爲跟系統 Visualizer 功能一致的數據源,自定義 Visualizer 需具有兩個功能:

  • 獲取 pcm 數據,計算 FFT
  • 以指定頻率和大小發送 FFT 數據

實現第一個功能首先要獲取播放音頻的 pcm 數據,這要求使用的播放器可以提供 pcm 數據,咱們的播放器是本身實現的,可以知足這個要求。咱們對播放器進行了擴展,增長了收集解碼後的 pcm 數據計算 FFT 的功能。

因爲不一樣音頻採樣率不一樣,而計算 FFT 時採用固定的窗口大小,致使 FFT 計算結果回調頻率隨播放音頻改變,同時指定的數據大小可能跟計算結果的大小不一樣,所以要實現第二個功能,須要對計算結果作固定頻率和採樣等處理。

另外,咱們的播放器在播放進程中運行,而實際使用 FFT 數據的動效頁面運行於主進程中,因此還須要跨進程傳輸數據。

綜上,自定義 Visualizer 的總體流程是:在播放進程 native 層中計算 FFT,經過 JNI 調用,把計算結果回調給Java 層,而後經過 AIDL 把 FFT 數據傳遞給主進程進行後續的數據處理和發送操做。以下圖所示:

固定頻率須要將可變的 FFT 計算結果回調頻率轉換爲外部設置的 Visualizer 回調頻率,以下圖所示:

根據所需數據發送時間間隔和 FFT 回調時間間隔差值的不一樣,咱們採用兩種不一樣的方式。

當時間間隔差值小於等於回調時間間隔時,每 t / Δ t `t/ \Delta t` 次回調丟棄一次數據,其中 t 爲 FFT 回調時間間隔, Δ t `\Delta t` 爲時間間隔差值,以下圖所示:

當時間間隔差值大於回調時間間隔時,每 t 1 / t `t1/t` 次回調發送一次數據,其中 t1 爲所需數據發送時間間隔,t 爲 FFT 回調時間間隔,以下圖所示:

採樣就是當外部設置的數據大小小於 FFT 計算結果的數據大小時,對原始 FFT 數據以合適的間隔抽取數據,以知足設置的要求。

爲了讓自定義 Visualizer 返回數據的取值範圍跟系統 Visualizer 一致,從而實現數據源無縫切換,咱們須要對 FFT 數據進行縮放。這裏就須要用到前面提到的模與振幅的計算了,解碼所得 pcm 數據的取值範圍爲 [-1, 1],因此原始信號振幅取值範圍爲 [0, 1],即 2 M / N `2M/N` 的取值範圍爲 [0, 1](繪製時不會用到直流份量,這裏不考慮);而系統 Visualizer 返回的 FFT 數據是一個 byte 數組,實部和虛部的取值範圍爲 [-128, 128],模的取值範圍爲 [ 0 , 128 × 2 ] `[0, 128 \times \sqrt2]` ,那麼 2 M / N × 128 × 2 `2M/N \times 128 \times \sqrt2` 的取值範圍跟系統 Visualizer 輸出 FFT 的模的取值範圍一致。因爲繪製不會用到相位信息,咱們能夠將用上述方式縮放後的值做爲輸出 FFT 數據的實部,並把虛部設爲 0。

因爲數據發送的頻率較高,爲了不頻繁建立對象致使內存抖動,咱們採用對象池來保存數據數組對象,每次從對象池中獲取所需大小的數組對象,填充採樣數據後加入到隊列中等待發送,數據消費完後將數組對象返回到對象池中。

數據處理

不一樣動效的具體數據處理方式不一樣,忽略細節上的差別,雲音樂現有的動效中,除了宇宙塵埃和孤獨星球,其餘的處理流程基本一致,以下圖所示:

首先根據動效選擇的頻率範圍計算所需的頻率數據在 FFT 數組中的索引位置:

f_r=f_s/N, start=\lceil MIN/f_r \rceil, end=\lfloor MAX/f_r \rfloor
複製代碼

其中 f s `f_s` 爲採樣率,N 爲 FFT 窗口大小, f r `f_r` 爲頻率分辨率,MIN 爲頻率範圍起始值,MAX 爲頻率範圍結束值。

而後根據動效所需數據點數,對頻率範圍內的 FFT 數據進行採樣或用一個 FFT 數據表示多個數據點。

而後計算分貝:

db=20\log_{10}M
複製代碼

其中 M 爲 FFT 數據的模。

而後將分貝轉化爲高度:

h=db/MAX\_DB \cdot maxHeight
複製代碼

其中 MAX_DB 是預設的分貝最大值,maxHeight 是當前動效要求的最大高度。

最後對計算出的高度作數據上的平滑處理。

平滑

對最終用來繪製的數據作平滑處理,能夠獲得更柔和的曲線,達到更好的視覺效果,以下圖所示:

數據平滑算法有不少,咱們綜合考慮效果和計算複雜度選擇了 Savitzky–Golay 濾波法,其計算方式以下,對應的窗口大小分別爲五、7 和 9,能夠按需選擇不一樣的窗口大小。

Y_i={1 \over 35}(-3y_{i-2}+12y_{i-1}+17y_i+12y_{i+1}-3y_{i+2})
複製代碼
Y_i={1 \over 21}(-2y_{i-3}+3y_{i-2}+6y_{i-1}+7y_i+6y_{i+1}+3y_{i+2}-2y_{i+3})
複製代碼
Y_i={1 \over 231}(-21y_{i-4}+14y_{i-3}+39y_{i-2}+54y_{i-1}+59y_i+54y_{i+1}+39y_{i+2}+14y_{i+3}-21y_{i+4})
複製代碼

通過平滑處理後數據的變化以下圖所示:

BufferQueue

有些動效的數據處理計算比較複雜,爲提高並行性,減小主線程耗時,咱們借鑑系統圖形框架中 BufferQueue 的思想,實現了一個簡單的承載動效繪製數據,鏈接數據處理和繪製的 BufferQueue,其工做過程以下圖所示:

在使用 BufferQueue 的動效繪製類初始化時,根據須要建立一個合適大小的 BufferQueue,並啓動用於執行數據處理的 Looper 線程。

數據處理部分對應 BufferQueueProducer,當 FFT 數據到來時,經過綁定 Looper 線程的 Handler 將數據發送到 Looper 線程中執行數據處理。數據處理時,首先調用 Producerdequeue 方法從 BufferQueue 中獲取空閒的 Buffer,而後對 FFT 數據進行處理,生成須要的數據向 Buffer 中填充,最後調用 Producerqueue 方法將 Buffer 加入到 BufferQueue 中的 queued 隊列中。

繪製部分對應 BufferQueueConsumer,調用 Producerqueue 方法時會觸發 ConsumerListeneronBufferAvailable 回調,在回調中經過綁定主線程的 Handler 切換到主線程消費 Buffer。首先調用 Consumeracquire 方法從 BufferQueuequeued 隊列中獲取 Buffer,而後從 Buffer 中取出所需數據來繪製,最後調用 Consumerrelease 方法將上次的 Buffer 返回給 BufferQueue

繪製

繪製部分的主要工做是調用系統 Canvas API 將處理後的數據繪製成所需的效果,具體如何使用 API 繪製,隨動效的不一樣而不一樣,這裏不展開介紹。本節將從對繪製來講比較重要的體驗和性能方面介紹一些動效繪製的優化經驗。

因爲 FFT 數據回調的時間間隔大於 16ms,若是隻在數據到來時繪製,會產生視覺上的卡頓,爲了獲得更好的視覺效果,須要在兩次回調之間加入過渡幀,以達到漸變的動畫效果。實現方式是在兩次數據到達的時間間隔內,以上次數據爲起點,本次數據爲終點,根據當前時間相對於數據到達時間的消逝時間計算當前的高度,不斷重複繪製,以下圖所示:

性能優化有兩大手段:batch 和 cache,在動效繪製時也可使用這些手段。對於須要繪製多條線或多個點的動效,應該調用 drawLinesdrawPoints 方法進行批處理,而不是循環調用 drawLinedrawPoint 方法,以減小執行時間。

結語

本文介紹了 Android 音頻可視化涉及的背景知識和實現過程,並提供了一些問題解決方案和優化思路。本文專一於通用方案,不涉及特定動效的具體實現,但願讀者能從中受到些許啓發,實現本身的酷炫動效。

參考資料

本文發佈自 網易雲音樂大前端團隊,文章未經受權禁止任何形式的轉載。咱們常年招收前端、iOS、Android,若是你準備換工做,又剛好喜歡雲音樂,那就加入咱們 grp.music-fe@corp.netease.com

相關文章
相關標籤/搜索