android pcm频谱_Android音频可视化
本文作者:熊鋆洋 (網易云音樂大前端團隊)
前言
音頻可視化,顧名思義就是將聲音以視覺的方式呈現出來。如何將音頻信號繪制出來?如何將聲音的變化在視覺上清晰的表現出來,讓視覺和聽覺上的感受一致?這些在 Android 上如何實現?本文將針對這些問題做出解答,盡量對 Android 上的音頻可視化實現做一個全面的介紹。
傅里葉變換
Android 音頻播放的一般流程是: 1. 播放器從本地音頻文件或網絡加載編碼后的音頻數據,解碼為 pcm 數據寫入 AudioTrack 2. AudioTrack 將 pcm 數據寫入 FIFO 3. AudioFlinger 中的 MixerThread 通過 AudioMixer 讀取 FIFO 中的數據進行混音后寫入 HAL 輸出設備進行播放
在這個流程中,直接體現音頻特征,可用于可視化繪制的是 pcm 數據。但 pcm 表示各采樣時間點上音頻信號強度,看起來雜亂無章,難以體現聽覺感知到的聲音變化。pcm 數據僅可用來繪制體現音頻信號平均強度變化的可視化動效,其他大部分動效需要使用對 pcm 數據做傅里葉變換后得到的體現各頻率點上信號強度變化的頻域數據來繪制。
這里簡單回顧下傅里葉變換,它將信號從時域轉換為頻域,一般用于信號頻譜分析,確定其成分。轉換結果如下圖所示:
pcm 數據是時間離散的,需要使用離散傅里葉變換(DFT),它將包含 N 個復數的序列 $\{x_n\}:=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)$,速度較慢,實際應用中,一般會使用快速傅里葉變換(FFT),將時間復雜度降為 $O(Nlog(N))$。
計算公式看起來很復雜,但不懂也不會影響我們實現音頻可視化,FFT 的計算可以使用已有的庫,不需要自己來實現。但為了從 FFT 的計算結果得到最終用來繪制的數據,有必要了解以下DFT特性: 輸入全部為實數時,輸出結果滿足共軛對稱性:$X_{N-k}=X_k^*$,因此一般實現只返回一半結果 如原始信號采樣率為 $f_s$,序列長度為 N,輸出頻率分辨率為 $f_s/N$,第 k 個點的頻率為 $kf_s/N$,可用于查找指定頻率范圍在結果中對應的位置 * 如一個頻率對應輸出的實部和虛部為 re 和 im,其模為 $M=\sqrt{re^2+im^2}$,原始信號振幅為 $A=\begin{cases} M/N & DC \\ 2M/N & other \end{cases}$,可用于計算分貝和數據縮放
數據源
提供播放 pcm 數據的 FFT 計算結果的數據源有兩種,一種是 Android 系統提供的 Visualizer 類,這種存在兼容性問題,因此我們引入了另一種自己實現的數據源。同時,我們實現了在不修改上層各動效的數據處理和繪制邏輯的基礎上切換數據源,如下圖所示:
Android Visualizer
系統 Visualizer 提供了方便的 api 來獲取播放音頻的波形或 FFT 數據,一般使用方式是: 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/ \Delta t$ 次回調丟棄一次數據,其中 t 為 FFT 回調時間間隔,$\Delta t$ 為時間間隔差值,如下圖所示:
當時間間隔差值大于回調時間間隔時,每 $t1/t$ 次回調發送一次數據,其中 t1 為所需數據發送時間間隔,t 為 FFT 回調時間間隔,如下圖所示:
采樣就是當外部設置的數據大小小于 FFT 計算結果的數據大小時,對原始 FFT 數據以合適的間隔抽取數據,以滿足設置的要求。
為了讓自定義 Visualizer 返回數據的取值范圍跟系統 Visualizer 一致,從而實現數據源無縫切換,我們需要對 FFT 數據進行縮放。這里就需要用到前面提到的模與振幅的計算了,解碼所得 pcm 數據的取值范圍為 [-1, 1],所以原始信號振幅取值范圍為 [0, 1],即 $2M/N$ 的取值范圍為 [0, 1](繪制時不會用到直流分量,這里不考慮);而系統 Visualizer 返回的 FFT 數據是一個 byte 數組,實部和虛部的取值范圍為 [-128, 128],模的取值范圍為 $[0, 128 \times \sqrt2]$,那么 $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$ 為采樣率,N 為 FFT 窗口大小,$f_r$ 為頻率分辨率,MIN 為頻率范圍起始值,MAX 為頻率范圍結束值。
然后根據動效所需數據點數,對頻率范圍內的 FFT 數據進行采樣或用一個 FFT 數據表示多個數據點。
然后計算分貝:
db=20\log_{10}M
其中 M 為 FFT 數據的模。
然后將分貝轉化為高度:
h=db/MAX\_DB \cdot maxHeight
其中 MAX_DB 是預設的分貝最大值,maxHeight 是當前動效要求的最大高度。
最后對計算出的高度做數據上的平滑處理。
平滑
對最終用來繪制的數據做平滑處理,可以得到更柔和的曲線,達到更好的視覺效果,如下圖所示:
數據平滑算法有很多,我們綜合考慮效果和計算復雜度選擇了 Savitzky–Golay 濾波法,其計算方式如下,對應的窗口大小分別為5、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 線程。
數據處理部分對應 BufferQueue 的 Producer,當 FFT 數據到來時,通過綁定 Looper 線程的 Handler 將數據發送到 Looper 線程中執行數據處理。數據處理時,首先調用 Producer 的 dequeue 方法從 BufferQueue 中獲取空閑的 Buffer,然后對 FFT 數據進行處理,生成需要的數據向 Buffer 中填充,最后調用 Producer 的 queue 方法將 Buffer 加入到 BufferQueue 中的 queued 隊列中。
繪制部分對應 BufferQueue 的 Consumer,調用 Producer 的 queue 方法時會觸發 ConsumerListener 的 onBufferAvailable 回調,在回調中通過綁定主線程的 Handler 切換到主線程消費 Buffer。首先調用 Consumer 的 acquire 方法從 BufferQueue 的 queued 隊列中獲取 Buffer,然后從 Buffer 中取出所需數據來繪制,最后調用 Consumer 的 release 方法將上次的 Buffer 返回給 BufferQueue。
繪制
繪制部分的主要工作是調用系統 Canvas API 將處理后的數據繪制成所需的效果,具體如何使用 API 繪制,隨動效的不同而不同,這里不展開介紹。本節將從對繪制來說比較重要的體驗和性能方面介紹一些動效繪制的優化經驗。
由于 FFT 數據回調的時間間隔大于 16ms,如果只在數據到來時繪制,會產生視覺上的卡頓,為了得到更好的視覺效果,需要在兩次回調之間加入過渡幀,以達到漸變的動畫效果。實現方式是在兩次數據到達的時間間隔內,以上次數據為起點,本次數據為終點,根據當前時間相對于數據到達時間的消逝時間計算當前的高度,不斷重復繪制,如下圖所示:
性能優化有兩大手段:batch 和 cache,在動效繪制時也可以使用這些手段。對于需要繪制多條線或多個點的動效,應該調用 drawLines 或 drawPoints 方法進行批處理,而不是循環調用 drawLine 或 drawPoint 方法,以減少執行時間。
結語
本文介紹了 Android 音頻可視化涉及的背景知識和實現過程,并提供了一些問題解決方案和優化思路。本文專注于通用方案,不涉及特定動效的具體實現,希望讀者能從中受到些許啟發,實現自己的酷炫動效。
參考資料本文發布自 網易云音樂大前端團隊,文章未經授權禁止任何形式的轉載。我們常年招收前端、iOS、Android,如果你準備換工作,又恰好喜歡云音樂,那就加入我們 grp.music-fe@corp.netease.com!
總結
以上是生活随笔為你收集整理的android pcm频谱_Android音频可视化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: auuc 评估指标_广告中增益模型理解
- 下一篇: ecg 幅度_ECG信号中一些运动伪差的