Android音頻開發(fā)(7):音樂可視化-FFT頻譜圖

Android 音頻開發(fā) 目錄

  1. Android音頻開發(fā)(1):音頻相關(guān)知識
  2. Android音頻開發(fā)(2):使用AudioRecord錄制pcm格式音頻
  3. Android音頻開發(fā)(3):使用AudioRecord實(shí)現(xiàn)錄音的暫停和恢復(fù)
  4. Android音頻開發(fā)(4):PCM轉(zhuǎn)WAV格式音頻
  5. Android音頻開發(fā)(5):Mp3的錄制 - 編譯Lame源碼
  6. Android音頻開發(fā)(6):Mp3的錄制 - 使用Lame實(shí)時(shí)錄制MP3格式音頻
  7. Android音頻開發(fā)(7):音樂可視化-FFT頻譜圖

項(xiàng)目地址:https://github.com/zhaolewei/MusicVisualizer
視頻演示地址:https://www.bilibili.com/video/av30388154/

一、演示

image

二、實(shí)現(xiàn)

  • 實(shí)現(xiàn)流程:
  1. 使用MediaPlayer播放傳入的音樂,并拿到mediaPlayerId
  2. 使用Visualizer類拿到拿到MediaPlayer播放中的音頻數(shù)據(jù)(wave/fft)
  3. 將數(shù)據(jù)用自定義控件展現(xiàn)出來

三、準(zhǔn)備工作

  • 使用Visualizer需要錄音的動(dòng)態(tài)權(quán)限, 如果播放sd卡音頻需要STORAGE權(quán)限
      private static final String[] PERMISSIONS = new String[]{
            Manifest.permission.RECORD_AUDIO,
            Manifest.permission.MODIFY_AUDIO_SETTINGS
      };
      
      ActivityCompat.requestPermissions(MainActivity.this, PERMISSIONS, 1);
      
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

四、開始播放

private MediaPlayer.OnPreparedListener preparedListener = new 
    /**
     * 播放音頻
     *
     * @param raw 資源文件id
     */
    private  void doPlay(final int raw) {
        try {
            mediaPlayer = MediaPlayer.create(MyApp.getInstance(), raw);
            if (mediaPlayer == null) {
                Logger.e(TAG, "mediaPlayer is null");
                return;
            }

            mediaPlayer.setOnErrorListener(errorListener);
            mediaPlayer.setOnPreparedListener(preparedListener);
        } catch (Exception e) {
            Logger.e(e, TAG, e.getMessage());
        }
    }
    
    /**
    * 獲取MediaPlayerId
    * 可視化類Visualizer需要此參數(shù)
    * @return  MediaPlayerId
    */
    public int getMediaPlayerId() {
        return mediaPlayer.getAudioSessionId();
    }

五、使用可視化類Visualizer獲取當(dāng)前音頻數(shù)據(jù)

Visualizer 有兩個(gè)比較重要的參數(shù)

  1. 設(shè)置可視化數(shù)據(jù)的數(shù)據(jù)大小 范圍[Visualizer.getCaptureSizeRange()[0]~Visualizer.getCaptureSizeRange()[1]]
  2. 社會(huì)可視化數(shù)據(jù)的采集頻率 范圍[0~Visualizer.getMaxCaptureRate()]
  • OnDataCaptureListener 有2個(gè)回調(diào),一個(gè)用于顯示FFT數(shù)據(jù),展示不同頻率的振幅,另一個(gè)用于顯示聲音的波形圖

private Visualizer.OnDataCaptureListener dataCaptureListener = new Visualizer.OnDataCaptureListener() {
        @Override
        public void onWaveFormDataCapture(Visualizer visualizer, final byte[] waveform, int samplingRate) {
            audioView.post(new Runnable() {
                @Override
                public void run() {
                    audioView.setWaveData(waveform);
                }
            });
        }

        @Override
        public void onFftDataCapture(Visualizer visualizer, final byte[] fft, int samplingRate) {
            audioView2.post(new Runnable() {
                @Override
                public void run() {
                    audioView2.setWaveData(fft);
                }
            });
        }
    };
    
private void initVisualizer() {
    try {
        int mediaPlayerId = mediaPlayer.getMediaPlayerId();
        if (visualizer != null) {
            visualizer.release();
        }
        visualizer = new Visualizer(mediaPlayerId);
        
        /**
         *可視化數(shù)據(jù)的大小: getCaptureSizeRange()[0]為最小值,getCaptureSizeRange()[1]為最大值
         */
        int captureSize = Visualizer.getCaptureSizeRange()[1];
        int captureRate = Visualizer.getMaxCaptureRate() * 3 / 4;
        
        visualizer.setCaptureSize(captureSize);
        visualizer.setDataCaptureListener(dataCaptureListener, captureRate, true, true);
        visualizer.setScalingMode(Visualizer.SCALING_MODE_NORMALIZED);
        visualizer.setEnabled(true);
    } catch (Exception e) {
        Logger.e(TAG, "請檢查錄音權(quán)限");
    }
}    

波形數(shù)據(jù)和傅里葉數(shù)據(jù)的關(guān)系請看面這張圖:

image

快速傅里葉轉(zhuǎn)換(FFT)詳細(xì)分析: https://zhuanlan.zhihu.com/p/19763358

六、編寫自定義控件,展示數(shù)據(jù)

  1. 處理數(shù)據(jù): visualizer 回調(diào)中的數(shù)據(jù)中是存在負(fù)數(shù)的,需要轉(zhuǎn)換一下,用于顯示
  • 當(dāng)byte 為 -128時(shí)Math.abs(fft[i]) 計(jì)算出來的值會(huì)越界,需要手動(dòng)處理一下

byte 的范圍: -128~127

    /**
     * 預(yù)處理數(shù)據(jù)
     *
     * @return
     */
    private static byte[] readyData(byte[] fft) {
        byte[] newData = new byte[LUMP_COUNT];
        byte abs;
        for (int i = 0; i < LUMP_COUNT; i++) {
            abs = (byte) Math.abs(fft[i]);
            //描述:Math.abs -128時(shí)越界
            newData[i] = abs < 0 ? 127 : abs;
        }
        return newData;
    }
  1. 緊接著就是根據(jù)數(shù)據(jù)去繪制圖形
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        wavePath.reset();

        for (int i = 0; i < LUMP_COUNT; i++) {
            if (waveData == null) {
                canvas.drawRect((LUMP_WIDTH + LUMP_SPACE) * i,
                        LUMP_MAX_HEIGHT - LUMP_MIN_HEIGHT,
                        (LUMP_WIDTH + LUMP_SPACE) * i + LUMP_WIDTH,
                        LUMP_MAX_HEIGHT,
                        lumpPaint);
                continue;
            }

            switch (upShowStyle) {
                case STYLE_HOLLOW_LUMP:
                    drawLump(canvas, i, false);
                    break;
                case STYLE_WAVE:
                    drawWave(canvas, i, false);
                    break;
                default:
                    break;
            }

            switch (downShowStyle) {
                case STYLE_HOLLOW_LUMP:
                    drawLump(canvas, i, true);
                    break;
                case STYLE_WAVE:
                    drawWave(canvas, i, true);
                    break;
                default:
                    break;
            }
        }
    }
    
    /**
     * 繪制矩形條
     */
    private void drawLump(Canvas canvas, int i, boolean reversal) {
        int minus = reversal ? -1 : 1;

        if (waveData[i] < 0) {
            Logger.w("waveData", "waveData[i] < 0 data: %s", waveData[i]);
        }
        float top = (LUMP_MAX_HEIGHT - (LUMP_MIN_HEIGHT + waveData[i] * SCALE) * minus);

        canvas.drawRect(LUMP_SIZE * i,
                top,
                LUMP_SIZE * i + LUMP_WIDTH,
                LUMP_MAX_HEIGHT,
                lumpPaint);
    }

    /**
     * 繪制曲線
     * 這里使用貝塞爾曲線來繪制
     */
    private void drawWave(Canvas canvas, int i, boolean reversal) {
        if (pointList == null || pointList.size() < 2) {
            return;
        }
        float ratio = SCALE * (reversal ? -1 : 1);
        if (i < pointList.size() - 2) {
            Point point = pointList.get(i);
            Point nextPoint = pointList.get(i + 1);
            int midX = (point.x + nextPoint.x) >> 1;
            if (i == 0) {
                wavePath.moveTo(point.x, LUMP_MAX_HEIGHT - point.y * ratio);
            }
            wavePath.cubicTo(midX, LUMP_MAX_HEIGHT - point.y * ratio,
                    midX, LUMP_MAX_HEIGHT - nextPoint.y * ratio,
                    nextPoint.x, LUMP_MAX_HEIGHT - nextPoint.y * ratio);

            canvas.drawPath(wavePath, lumpPaint);
        }
    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,836評論 6 540
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,275評論 3 428
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,904評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,633評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,368評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,736評論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,740評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,919評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,481評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,235評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,427評論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,968評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,656評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,055評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,348評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,160評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,380評論 2 379

推薦閱讀更多精彩內(nèi)容