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需要錄音的動態(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. 社會可視化數(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ì)算出來的值會越界,需要手動處理一下

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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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