android繪制播放音頻的波形圖

之前做過(guò)android的錄音油吭,編輯(裁剪和合成(WAV格式))雕憔,思路大概是從麥克風(fēng)獲取音頻的詳細(xì)數(shù)據(jù)填充到list集合中逾雄,再將這些數(shù)據(jù)經(jīng)過(guò)計(jì)算畫(huà)到屏幕上含懊,算是實(shí)時(shí)錄制的波形圖!之后有一段時(shí)間沒(méi)碰過(guò)那個(gè)項(xiàng)目了筐摘,雖然功能是做出來(lái)了卒茬,但是還不算是完整的,那要是播放的時(shí)候呢咖熟?播放的時(shí)候怎么實(shí)時(shí)動(dòng)態(tài)的獲取音頻數(shù)據(jù)來(lái)繪制呢圃酵?思考良久,在逛github的時(shí)候馍管,發(fā)現(xiàn)了這個(gè)功能郭赐!在這里做個(gè)記錄,也給沒(méi)有這方面知識(shí)的朋友們做個(gè)補(bǔ)充确沸,分享一下捌锭!

OK,先看效果圖吧罗捎!

這里寫(xiě)圖片描述

這個(gè)效果圖是線性和圓形的音頻傅里葉數(shù)據(jù)圖形观谦,當(dāng)然還有柱狀的效果圖,這里并沒(méi)有展示桨菜,整完這篇博客后豁状,大家可以自己下載demo自己運(yùn)行看看效果。

獲取音頻播放的實(shí)時(shí)數(shù)據(jù)并繪制雷激,涉及到android提供的一個(gè)類替蔬,Visualizer,這個(gè)類可以捕獲使用MediaPlayer的時(shí)候音頻數(shù)據(jù)屎暇,主要返回兩種類型的數(shù)據(jù),一種是音頻的波形數(shù)據(jù)驻粟,一種是傅里葉數(shù)據(jù)(未考究)根悼,android系統(tǒng)中關(guān)于這個(gè)類的描述幾乎為0凶异,并不像其它類會(huì)有大把英文注釋!很煩挤巡。剩彬。

代碼使用方法如下:

這里寫(xiě)圖片描述

`

很簡(jiǎn)單吧!只需要實(shí)現(xiàn)一個(gè)捕獲監(jiān)聽(tīng)即可矿卑!

OK喉恋,我們?cè)賮?lái)看看柱狀圖。
效果圖如下:

這里寫(xiě)圖片描述

界面的效果有點(diǎn)糙母廷?不急轻黑,我們理解了原理,之后慢慢改唄琴昆!哪有什么東西都是現(xiàn)成的氓鄙?

先看自定義的界面展示的代碼:

/**
 * Copyright 2011, Felix Palmer
 *
 * Licensed under the MIT license:
 * http://creativecommons.org/licenses/MIT/
 */
package com.tian.audio.wave.widget;

import java.util.HashSet;
import java.util.Set;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff.Mode;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.media.MediaPlayer;
import android.media.audiofx.Visualizer;
import android.util.AttributeSet;
import android.view.View;

import com.tian.audio.wave.dao.AudioData;
import com.tian.audio.wave.dao.FFTData;
import com.tian.audio.wave.renderer.Renderer;


/**
 * A class that draws visualizations of data received from a
 * {@link Visualizer.OnDataCaptureListener#onWaveFormDataCapture } and
 * {@link Visualizer.OnDataCaptureListener#onFftDataCapture }
 */
public class VisualizerView extends View {
  private static final String TAG = "VisualizerView";

  private byte[] mBytes;
  private byte[] mFFTBytes;
  private Rect mRect = new Rect();
  private Visualizer mVisualizer;

  private Set<Renderer> mRenderers;

  private Paint mFlashPaint = new Paint();
  private Paint mFadePaint = new Paint();

  public VisualizerView(Context context, AttributeSet attrs, int defStyle){
    super(context, attrs);
    init();
  }

  public VisualizerView(Context context, AttributeSet attrs)
  {
    this(context, attrs, 0);
  }

  public VisualizerView(Context context)
  {
    this(context, null, 0);
  }

  private void init() {
    mBytes = null;
    mFFTBytes = null;

    mFlashPaint.setColor(Color.argb(122, 255, 255, 255));
    mFadePaint.setColor(Color.argb(238, 255, 255, 255)); // Adjust alpha to change how quickly the image fades
    mFadePaint.setXfermode(new PorterDuffXfermode(Mode.MULTIPLY));

    mRenderers = new HashSet<Renderer>();
  }

  /**
   * Links the visualizer to a player
   * @param player - MediaPlayer instance to link to
   */
  public void link(MediaPlayer player){
    if(player == null)
    {
      throw new NullPointerException("Cannot link to null MediaPlayer");
    }

    // Create the Visualizer object and attach it to our media player.
    mVisualizer = new Visualizer(player.getAudioSessionId());
    mVisualizer.setCaptureSize(Visualizer.getCaptureSizeRange()[1]);

    // Pass through Visualizer data to VisualizerView
    Visualizer.OnDataCaptureListener captureListener = new Visualizer.OnDataCaptureListener(){

      //捕獲波形數(shù)據(jù)
      @Override
      public void onWaveFormDataCapture(Visualizer visualizer, byte[] bytes,
          int samplingRate){
        updateVisualizer(bytes);
      }

      //捕獲傅里葉數(shù)據(jù)
      @Override
      public void onFftDataCapture(Visualizer visualizer, byte[] bytes,
          int samplingRate){
        updateVisualizerFFT(bytes);
      }
    };

    mVisualizer.setDataCaptureListener(captureListener,
        Visualizer.getMaxCaptureRate() / 2, true, true);

    // Enabled Visualizer and disable when we're done with the stream
    mVisualizer.setEnabled(true);
    player.setOnCompletionListener(new MediaPlayer.OnCompletionListener(){
      @Override
      public void onCompletion(MediaPlayer mediaPlayer){
        mVisualizer.setEnabled(false);
      }
    });
  }

  public void addRenderer(Renderer renderer){
    if(renderer != null){
      mRenderers.add(renderer);
    }
  }

  public void clearRenderers()
  {
    mRenderers.clear();
  }

  /**
   * Call to release the resources used by VisualizerView. Like with the
   * MediaPlayer it is good practice to call this method
   */
  public void release()
  {
    mVisualizer.release();
  }

  /**
   * Pass data to the visualizer. Typically this will be obtained from the
   * Android Visualizer.OnDataCaptureListener call back. See
   * {@link Visualizer.OnDataCaptureListener#onWaveFormDataCapture }
   * @param bytes
   */
  public void updateVisualizer(byte[] bytes) {
    mBytes = bytes;
    invalidate();
  }

  /**
   * Pass FFT data to the visualizer. Typically this will be obtained from the
   * Android Visualizer.OnDataCaptureListener call back. See
   * {@link Visualizer.OnDataCaptureListener#onFftDataCapture }
   * @param bytes
   */
  public void updateVisualizerFFT(byte[] bytes) {
    mFFTBytes = bytes;
    invalidate();
  }

  boolean mFlash = false;

  /**
   * Call this to make the visualizer flash. Useful for flashing at the start
   * of a song/loop etc...
   */
  public void flash() {
    mFlash = true;
    invalidate();
  }

  Bitmap mCanvasBitmap;
  Canvas mCanvas;


  @Override
  protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    // Create canvas once we're ready to draw
    mRect.set(0, 0, getWidth(), getHeight());

    if(mCanvasBitmap == null){
      mCanvasBitmap = Bitmap.createBitmap(canvas.getWidth(), canvas.getHeight(), Config.ARGB_8888);
    }
    if(mCanvas == null){
      mCanvas = new Canvas(mCanvasBitmap);
    }

    if (mBytes != null) {
      // Render all audio renderers
      AudioData audioData = new AudioData(mBytes);
      for(Renderer r : mRenderers){
        r.render(mCanvas, audioData, mRect);
      }
    }

    if (mFFTBytes != null) {
      // Render all FFT renderers
      FFTData fftData = new FFTData(mFFTBytes);
      for(Renderer r : mRenderers){
        r.render(mCanvas, fftData, mRect);
      }
    }
    // 漸變產(chǎn)生的陰影的效果
    mCanvas.drawPaint(mFadePaint);

    if(mFlash){
      mFlash = false;
      mCanvas.drawPaint(mFlashPaint);
    }

    canvas.drawBitmap(mCanvasBitmap, new Matrix(), null);
  }
}

這個(gè)類很簡(jiǎn)單,對(duì)不同的展示界面進(jìn)行了簡(jiǎn)單的封裝业舍,主要的繪制那肯定在onDraw方法體抖拦!而在獲取到音頻數(shù)據(jù)的時(shí)候,將Visualizer捕獲到的音頻數(shù)據(jù)(傅里葉)舷暮,抽取出來(lái)進(jìn)行invalidate();重新繪制态罪,所以其他的我們可以跳過(guò),直接看onDraw方法體下面,也包括怎樣產(chǎn)生的陰影效果向臀!

抽取的父類處理:

package com.tian.audio.wave.renderer;
import android.graphics.Canvas;
import android.graphics.Rect;

import com.tian.audio.wave.dao.AudioData;
import com.tian.audio.wave.dao.FFTData;


abstract public class Renderer{
  // Have these as members, so we don't have to re-create them each time
  protected float[] mPoints;
  protected float[] mFFTPoints;
  public Renderer()
  {
  }

  // As the display of raw/FFT audio will usually look different, subclasses
  // will typically only implement one of the below methods
  /**
   * Implement this method to render the audio data onto the canvas
   * @param canvas - Canvas to draw on
   * @param data - Data to render
   * @param rect - Rect to render into
   */
  abstract public void onRender(Canvas canvas, AudioData data, Rect rect);

  /**
   * Implement this method to render the FFT audio data onto the canvas
   * @param canvas - Canvas to draw on
   * @param data - Data to render
   * @param rect - Rect to render into
   */
  abstract public void onRender(Canvas canvas, FFTData data, Rect rect);


  // These methods should actually be called for rendering
  /**
   * Render the audio data onto the canvas
   * @param canvas - Canvas to draw on
   * @param data - Data to render
   * @param rect - Rect to render into
   */
  final public void render(Canvas canvas, AudioData data, Rect rect)
  {
    if (mPoints == null || mPoints.length < data.bytes.length * 4) {
      mPoints = new float[data.bytes.length * 4];
    }

    onRender(canvas, data, rect);
  }

  /**
   * Render the FFT data onto the canvas
   * @param canvas - Canvas to draw on
   * @param data - Data to render
   * @param rect - Rect to render into
   */
  final public void render(Canvas canvas, FFTData data, Rect rect)
  {
    if (mFFTPoints == null || mFFTPoints.length < data.bytes.length * 4) {
      mFFTPoints = new float[data.bytes.length * 4];
    }

    onRender(canvas, data, rect);
  }
}

繪制數(shù)據(jù)類:

/**
 * Copyright 2011, Felix Palmer
 *
 * Licensed under the MIT license:
 * http://creativecommons.org/licenses/MIT/
 */
package com.tian.audio.wave.renderer;

import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;

import com.tian.audio.wave.dao.AudioData;
import com.tian.audio.wave.dao.FFTData;


/**
 * 操作畫(huà)筆進(jìn)行各個(gè)bar的繪制工作
 */
public class BarGraphRenderer extends Renderer{
  private int mDivisions;
  private Paint mPaint;
  private boolean mTop;

  /**
   * Renders the FFT data as a series of lines, in histogram form
   * @param divisions - must be a power of 2. Controls how many lines to draw
   * @param paint - Paint to draw lines with
   * @param top - whether to draw the lines at the top of the canvas, or the bottom
   */
  public BarGraphRenderer(int divisions,
                          Paint paint,
                          boolean top){
    super();
    mDivisions = divisions;
    mPaint = paint;
    mTop = top;
  }

  @Override
  public void onRender(Canvas canvas, AudioData data, Rect rect){
    // Do nothing, we only display FFT data
  }

  @Override
  public void onRender(Canvas canvas, FFTData data, Rect rect){
    for (int i = 0; i < data.bytes.length / mDivisions; i++) {

      mFFTPoints[i * 4] = i * 4 * mDivisions;
      mFFTPoints[i * 4 + 2] = i * 4 * mDivisions;

      byte rfk = data.bytes[mDivisions * i];//間隔倍數(shù)
      byte ifk = data.bytes[mDivisions * i + 1];

      float magnitude = (rfk * rfk + ifk * ifk);
      int dbValue = (int) (10 * Math.log10(magnitude));

      if(mTop){
        mFFTPoints[i * 4 + 1] = 0;
        mFFTPoints[i * 4 + 3] = (dbValue * 2 - 10);
      }else{
        mFFTPoints[i * 4 + 1] = rect.height();
        mFFTPoints[i * 4 + 3] = rect.height() - (dbValue * 2 - 10);
      }
    }

    canvas.drawLines(mFFTPoints, mPaint);
  }
}

布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@drawable/bg"
    android:orientation="vertical" >

    <FrameLayout
        android:layout_width="fill_parent"
        android:layout_height="0dp"
        android:layout_margin="10dp"
        android:layout_weight="1"
        android:background="#000" >

        <com.tian.audio.wave.widget.VisualizerView
            android:id="@+id/visualizerView"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent" >
        </com.tian.audio.wave.widget.VisualizerView>
    </FrameLayout>

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_weight="0" >

        <Button
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:layout_weight="0.25"
            android:onClick="barPressed"
            android:text="Bar" >
        </Button>

        <Button
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:layout_weight="0.25"
            android:onClick="circlePressed"
            android:text="Circle" >
        </Button>

        <Button
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:layout_weight="0.25"
            android:onClick="circleBarPressed"
            android:text="Circle Bar" >
        </Button>
            
        <Button
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:layout_weight="0.25"
            android:onClick="linePressed"
            android:text="Line" >
        </Button>

        <Button
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:layout_weight="0.25"
            android:onClick="clearPressed"
            android:text="Clear" >
        </Button>
    </LinearLayout>

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_weight="0" >

        <Button
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:layout_weight="0.5"
            android:onClick="startPressed"
            android:text="Start" >
        </Button>

        <Button
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:layout_weight="0.5"
            android:onClick="stopPressed"
            android:text="Stop" >
        </Button>
    </LinearLayout>

</LinearLayout>

使用代碼:

// Methods for adding renderers to visualizer
    private void addBarGraphRenderers(){
        
        //底部柱狀條
        Paint paint = new Paint();
        paint.setStrokeWidth(50f);
        paint.setAntiAlias(true);
        paint.setColor(Color.argb(200, 56, 138, 252));
        BarGraphRenderer barGraphRendererBottom = new BarGraphRenderer(16, paint, false);
        mVisualizerView.addRenderer(barGraphRendererBottom);

        //頂部柱狀條
        Paint paint2 = new Paint();
        paint2.setStrokeWidth(12f);
        paint2.setAntiAlias(true);
        paint2.setColor(Color.argb(200, 181, 111, 233));
        BarGraphRenderer barGraphRendererTop = new BarGraphRenderer(4, paint2, true);
        mVisualizerView.addRenderer(barGraphRendererTop);
    }

OK,不知道的诸狭,這個(gè)功能是很難做券膀,知道后,就很簡(jiǎn)單了驯遇!哈哈芹彬。。

github地址(大家下載的話叉庐,順手給個(gè)star舒帮,也是對(duì)作者的鼓勵(lì)!謝謝啦6傅):

https://github.com/T-chuangxin/AudioWaveShow

每天進(jìn)步一點(diǎn)點(diǎn)玩郊,時(shí)間會(huì)讓你成為巨人!加油枉阵!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末译红,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子兴溜,更是在濱河造成了極大的恐慌侦厚,老刑警劉巖耻陕,帶你破解...
    沈念sama閱讀 216,470評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異刨沦,居然都是意外死亡诗宣,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門想诅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)召庞,“玉大人,你說(shuō)我怎么就攤上這事来破±鹤疲” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,577評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵讳癌,是天一觀的道長(zhǎng)穿稳。 經(jīng)常有香客問(wèn)我,道長(zhǎng)晌坤,這世上最難降的妖魔是什么逢艘? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,176評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮骤菠,結(jié)果婚禮上它改,老公的妹妹穿的比我還像新娘。我一直安慰自己商乎,他們只是感情好央拖,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著鹉戚,像睡著了一般鲜戒。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上抹凳,一...
    開(kāi)封第一講書(shū)人閱讀 51,155評(píng)論 1 299
  • 那天遏餐,我揣著相機(jī)與錄音,去河邊找鬼赢底。 笑死失都,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的幸冻。 我是一名探鬼主播粹庞,決...
    沈念sama閱讀 40,041評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼洽损!你這毒婦竟也來(lái)了庞溜?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,903評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤趁啸,失蹤者是張志新(化名)和其女友劉穎强缘,沒(méi)想到半個(gè)月后督惰,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體不傅,經(jīng)...
    沈念sama閱讀 45,319評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡旅掂,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了访娶。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片商虐。...
    茶點(diǎn)故事閱讀 39,703評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖崖疤,靈堂內(nèi)的尸體忽然破棺而出秘车,到底是詐尸還是另有隱情,我是刑警寧澤劫哼,帶...
    沈念sama閱讀 35,417評(píng)論 5 343
  • 正文 年R本政府宣布叮趴,位于F島的核電站,受9級(jí)特大地震影響权烧,放射性物質(zhì)發(fā)生泄漏眯亦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評(píng)論 3 325
  • 文/蒙蒙 一般码、第九天 我趴在偏房一處隱蔽的房頂上張望妻率。 院中可真熱鬧,春花似錦板祝、人聲如沸宫静。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,664評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)孤里。三九已至,卻和暖如春橘洞,著一層夾襖步出監(jiān)牢的瞬間捌袜,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,818評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工震檩, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留琢蛤,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,711評(píng)論 2 368
  • 正文 我出身青樓抛虏,卻偏偏與公主長(zhǎng)得像博其,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子迂猴,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評(píng)論 2 353

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