前言
日常使用一些音樂(lè)軟件的時(shí)候,在播放詳情頁(yè)經(jīng)??梢钥吹竭@么一些效果:

?
一般都有各式各樣的效果可供切換,并且會(huì)發(fā)現(xiàn)這些變幻都是跟隨著當(dāng)前的音頻同步的,那這個(gè)過(guò)程是如何轉(zhuǎn)換的呢?
Android中已經(jīng)提供了音頻可視化轉(zhuǎn)換的相關(guān)類,基于此轉(zhuǎn)換過(guò)程,封裝了一個(gè)音頻可視化視圖庫(kù)——AudioVisualizeView ,封裝本庫(kù)的目的,一方面在于音頻數(shù)據(jù)轉(zhuǎn)換成可視化過(guò)程的封裝,另一方面則是將這些可視化數(shù)據(jù)如何多元化地呈現(xiàn)在屏幕面前。由于本庫(kù)旨在封裝數(shù)據(jù)解析的流程,目前的效果比較簡(jiǎn)略且種類有限,部分效果預(yù)覽如下:




由于篇幅有限,就不全部羅列出來(lái)了,目前實(shí)現(xiàn)的只是一個(gè)基本的效果,還有很多可以優(yōu)化的細(xì)節(jié),后續(xù)會(huì)繼續(xù)完善。
?
實(shí)現(xiàn)思路
Android為我們提供了一個(gè)獲取音頻頻率數(shù)據(jù)的類——Visualizer ,它的使用方式,是傳入一個(gè) audioSessionId,通過(guò)這個(gè) audioSessionId 可以綁定獲取到對(duì)應(yīng)這個(gè)id的頻率數(shù)據(jù),而 audioSessionId 可以通過(guò) Mediaplayer 播放音頻后獲取,將獲取到的頻率數(shù)據(jù),遍歷繪制出來(lái),具體步驟如下:
1.使用
Mediaplayer播放指定音頻文件,獲取audioSessionId;
2.通過(guò)audioSessionId初始化Visualizer,初始化監(jiān)聽器處理采樣數(shù)據(jù);
3.將采樣數(shù)據(jù)繪制在屏幕上;
?
實(shí)現(xiàn)步驟
1.使用 Mediaplayer 播放指定音頻文件,獲取 audioSessionId
MediaPlayer mediaPlayer = MediaPlayer.create(mContext, Uri.parse(filePath));
if (mediaPlayer == null) {
LogUtils.d("mediaPlayer is null");
return;
}
mediaPlayer.setOnErrorListener(null);
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mediaPlayer) {
mediaPlayer.getAudioSessionId();
}
});
mediaPlayer.start();
關(guān)于 MediaPlayer 播放音頻文件不是本篇的重點(diǎn),這里就不展開闡述了,關(guān)鍵在MediaPlayer的 onPrepared 回調(diào)里面通過(guò) getAudioSessionId 獲取當(dāng)前播放的音頻的會(huì)話id。有了會(huì)話id才能去初始化 Visualizer。
2.通過(guò) audioSessionId 初始化 Visualizer ,初始化監(jiān)聽器處理采樣數(shù)據(jù)
Visualizer visualizer = new Visualizer(audioSessionId);
生成Visualizer實(shí)例之后,為其設(shè)置可視化數(shù)據(jù)的大小,其范圍是Visualizer.getCaptureSizeRange()[0] ~ Visualizer.getCaptureSizeRange()[1],此處設(shè)置為最大值:
visualizer.setCaptureSize(Visualizer.getCaptureSizeRange()[1]);
通過(guò) setDataCaptureListener 為可視化對(duì)象設(shè)置采樣監(jiān)聽數(shù)據(jù)的回調(diào)
visualizer.setDataCaptureListener(new Visualizer.OnDataCaptureListener() {
@Override
public void onWaveFormDataCapture(Visualizer visualizer, byte[] bytes, int samplingRate) {
}
@Override
public void onFftDataCapture(Visualizer visualizer, byte[] fft, int samplingRate) {
float[] model = new float[fft.length / 2 + 1];
model[0] = (byte) Math.abs(fft[1]);
int j = 1;
for (int i = 2; i < mCount *2;) {
model[j] = (float) Math.hypot(fft[i], fft[i + 1]);
i += 2;
j++;
model[j] = (float) Math.abs(fft[j]);
}
//model即為最終用于繪制的數(shù)據(jù)
}
}, Visualizer.getMaxCaptureRate() / 2, false, true);
可以看到有四個(gè)參數(shù),setDataCaptureListener(Visualizer.OnDataCaptureListener listener, int rate, boolean waveform, boolean fft),它們的作用分別如下:
listener:回調(diào)對(duì)象
rate:采樣的頻率,其范圍是0~Visualizer.getMaxCaptureRate(),此處設(shè)置為最大值一半。
waveform:是否獲取波形信息
fft:是否獲取快速傅里葉變換后的數(shù)據(jù)
waveform 和 fft 這兩個(gè)參數(shù),分別決定了 listener 中的兩個(gè)方法是否會(huì)回調(diào),再來(lái)看看 OnDataCaptureListener 中的兩個(gè)方法:
onWaveFormDataCapture:波形數(shù)據(jù)回調(diào)
onFftDataCapture:傅里葉數(shù)據(jù)回調(diào)
我們最終采用的是基于傅里葉快速轉(zhuǎn)換后的數(shù)據(jù)進(jìn)行繪制,所以我們?cè)?onFftDataCapture 方法中對(duì)轉(zhuǎn)換后的數(shù)據(jù)進(jìn)行處理,關(guān)于傅里葉,簡(jiǎn)單來(lái)講就是將時(shí)域轉(zhuǎn)換為頻域的一個(gè)過(guò)程,時(shí)域就是橫坐標(biāo)為時(shí)間維度,就是我們平時(shí)直觀理解的一種維度(習(xí)慣以時(shí)間為軸看待事物),而頻域則是以頻率為軸,就比如播放一個(gè)音頻,頻域是將每一個(gè)時(shí)刻的頻率一一呈現(xiàn)出來(lái),類似于下面這張圖的過(guò)程:

由紅色的波形轉(zhuǎn)換為了藍(lán)色的頻譜,也就是我們下一步要繪制的數(shù)據(jù),更多關(guān)于傅里葉轉(zhuǎn)換的詳細(xì)分析可以參考 傅里葉分析之掐死教程
onFftDataCapture 中返回的byte數(shù)組是快速傅里葉轉(zhuǎn)換之后的數(shù)據(jù),但我們還需要針對(duì)它做以下處理:
1.快速傅里葉變換返回的是512個(gè)復(fù)數(shù),下標(biāo)為單是實(shí)數(shù),下標(biāo)為雙的是虛數(shù),對(duì)每一組復(fù)數(shù)進(jìn)行計(jì)算即為最終可繪制的數(shù)據(jù):
float[] model = new float[fft.length / 2 + 1];
int j = 1;
for (int i = 2; i < mCount*2;) {
model[j] = (float) Math.hypot(fft[i], fft[i + 1]);
i += 2;
j++;
}
2.由于返回的byte數(shù)據(jù)有可能為負(fù),所以要取絕對(duì)值處理:
model[0] = (float) Math.abs(fft[1]);
...
model[j] = (float) Math.abs(fft[j]);
?
最后,還要記得調(diào)用 setEnabled 為 true:
visualizer.setEnabled(true);
才能正?;卣{(diào)出FFT數(shù)據(jù),另外記得在退出頁(yè)面的時(shí)候,調(diào)用 setEnabled(false) 避免下回再次打開的時(shí)候出現(xiàn)異常。
?
3.將采樣數(shù)據(jù)繪制在屏幕上
前面已經(jīng)處理并得到了最終的byte數(shù)組,可以針對(duì)需要展示的頻數(shù)進(jìn)行遍歷,比如說(shuō)繪制最常見的水平線可視化樣式(一條橫線打底,上面是多條豎線展示不同的頻率),如下圖:

在View的onDraw里面繪制:
for (int i = 0; i < mSpectrumCount; i++) {
float startX = getWidth() * i / mSpectrumCount;
float startY = getHeight() / 2;
float stopX = getWidth() * i / mSpectrumCount;
float stopY = getHeight() / 2 - mRawAudioBytes[i];
canvas.drawLine(startX, startY,stopX, stopY, mPaint);
}
這里 mRawAudioBytes 即上一步處理完成之后的數(shù)據(jù),開始播放音頻之后,在Visualizer的回調(diào)方法會(huì)不斷返回FFT處理之后的數(shù)據(jù),我們?cè)趯?duì)傅里葉數(shù)據(jù)處理完成之后不斷調(diào)用刷新即可:
@Override
public void onFftDataCapture(Visualizer visualizer, byte[] fft, int samplingRate) {
//....此處省略上一步取絕對(duì)值和hypot的處理的代碼
mRawAudioBytes = parseData;
invalidate();
}
同理,其他的可視化效果,也是基于這樣的一個(gè)邏輯,可以根據(jù)返回的數(shù)據(jù)繪制自己想要的效果。
其他
以上只是完成了基本的效果,考慮到其可拓展性,定義了FFT數(shù)據(jù)處理之后的回調(diào)接口,如果以上效果皆不滿足需求,可以在任意場(chǎng)景實(shí)現(xiàn)本庫(kù)中的 VisualizeCallback 接口,重寫 onFftDataCapture 方法,如下:
@Override
public void onFftDataCapture(byte[] parseData) {
//使用parseData的數(shù)據(jù)去自定義繪制具體場(chǎng)景的動(dòng)畫
}
?
結(jié)語(yǔ)
完整代碼已傳至Github:AudioVisualizeView——一個(gè)基于傅里葉快速轉(zhuǎn)換的音頻可視化庫(kù),目前的效果還有圓形、網(wǎng)狀、波浪等效果,后續(xù)會(huì)繼續(xù)借鑒常見的一些音頻可視化效果進(jìn)行更新,歡迎issue和star~
?
歡迎關(guān)注 Android小Y 的簡(jiǎn)書,更多Android精選自定義View
『Android自定義View實(shí)戰(zhàn)』實(shí)現(xiàn)一個(gè)小清新的彈出式圓環(huán)菜單
『Android自定義View實(shí)戰(zhàn)』玩轉(zhuǎn)PathMeasure之自定義支付結(jié)果動(dòng)畫
『Android自定義View實(shí)戰(zhàn)』自定義弧形旋轉(zhuǎn)菜單欄——衛(wèi)星菜單
『Android自定義View實(shí)戰(zhàn)』Android自定義帶側(cè)滑菜單的二維表格控件
GitHub:GitHubZJY
簡(jiǎn) 書:Android小Y
在GitHub上建了一個(gè)炫酷自定義View的集合ZJYWidget,主要是平時(shí)實(shí)現(xiàn)的一些實(shí)用的自定義View源碼及demo,會(huì)長(zhǎng)期維護(hù),歡迎Star~ 如有不足之處或建議還望指正,相互學(xué)習(xí),相互進(jìn)步~