Android Visualizer音頻可視化——讓你的音頻跳動(dòng)起來(lái)

前言

日常使用一些音樂(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ù)覽如下:

SingleVisualize.gif

WaveVisualize.gif

CircleVisualize.gif

NetVisualize.gif

由于篇幅有限,就不全部羅列出來(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ù)

waveformfft 這兩個(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)換動(dòng)圖

由紅色的波形轉(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è)滑菜單的二維表格控件

GitHubGitHubZJY
簡(jiǎn) 書Android小Y
在GitHub上建了一個(gè)炫酷自定義View的集合ZJYWidget,主要是平時(shí)實(shí)現(xiàn)的一些實(shí)用的自定義View源碼及demo,會(huì)長(zhǎng)期維護(hù),歡迎Star~ 如有不足之處或建議還望指正,相互學(xué)習(xí),相互進(jìn)步~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容