Android多媒體之認識MP3與內置媒體播放(MediaPlayer)

零、前言

做爲90後,mp3格式的音樂可謂靈魂之友。
小時候帶着耳機,躺在桌子上聽歌看月亮心情依稀。
當某個旋律想起,還會不會浮現某個風景,某我的……,
今天全程單曲播放——梁靜茹-勇氣(獻上頻譜)android

勇氣.png

主要任務:SD卡音樂、網絡音頻流的播放及控制

雙進度.png


MP3的簡介

0.[番外]--說兩句

初中那會仍是物理鍵盤手機,當時內存卡感受很寶貝,2G都大的不得了
一開始只有一個256MB的內存卡,那時誰不喜歡聽音樂,看電子書呢?
當時沒有網,只能讓姐姐幫我下載,我要求:下那種佔內存最小的歌
由於我發現有的都4M,有的0.4M,並且都能聽,當時有歌能聽就行,音質徹底不在乎
當時內存不夠時,我就挑最大內存的歌,記下歌名,忍痛刪掉
如今哪一個最大下哪一個,但對收藏音樂的感受已經沒有了,播放,聽聽就算了c++


1.勇氣歌曲信息分析

勇氣歌曲信息.png

立體聲:聲道數2
採樣率:44.1KHz
位深度:32bit

上篇咱們會求PCM音頻流碼率:採樣率*採樣大小*聲道數 b/s
若是是這個陣容,在PCM會是什麼樣的?
碼率:44100*32*2=2822400bps=2756.25Kbps  
每秒大小:2756.25Kbps/8= 344.53125KB
應占大小:(4*60+1.162)s*344.53125KB/s=83087.8453125B 約81.1M  

PCM幾乎接近完美音質(無損),原裝出品一首81.1M,怎麼大,估計很難接收
複製代碼

2.MP3是一種音頻有損壓縮技術(知識來源,百度百科)
MP3(Moving Picture Experts Group Audio Layer III)是指的是MPEG-1標準中的音頻部分 
MPEG音頻文件的壓縮是一種有損壓縮,MP3音頻具備10:1~12:1的高壓縮率

可見《勇氣》碼率由2756.25Kbps壓縮到320Kbps,壓縮率:8.61:1   
複製代碼

3.MP3壓縮的部分:

上篇說到的心理聲學,根據人耳模型,無損數據中存在大量的冗餘信息
壓縮就是對冗餘的數據進行過濾,或刻意對不重要的信息進行剔除git

利用人耳對高頻聲音信號不敏感的特性,將時域波形信號轉換成頻域信號, 
並劃分紅多個頻段,對不一樣的頻段使用不一樣的壓縮率,對高頻加大壓縮比(甚至忽略信號) 
對低頻信號使用小壓縮比,保證信號不失真。就至關於拋棄人耳基本聽不到的高頻聲音
來換取文件的尺寸,用 *.mp3 格式來儲存
複製代碼

4.壓縮率與音質

腳趾頭想一想都知道,同一文件,同一壓縮技術:
壓縮率越高,過濾的信息越多,文件越小,音質越差
反之亦然,320Kbps能夠算音質很是不錯了
複製代碼

科普就這樣,下面進入今天的重頭戲MediaPlayergithub


2、MediaPlayer簡述

父類/接口:PlayerBase/SubtitleController.Listener/VolumeAutomation
源碼行數:5618  ----通讀hold不住
內部類:27個--其中接口類13個,普通類11個 
構造方法:1個,無參構造
間接構造(方法返回該類實例):5個
方法數:目測120+
字段數:目測90+

複製代碼

Android做爲移動設備,音頻播放的類也就那幾個,MediaPlayer做爲中流砥柱
MediaPlayer是個挺大的類,又和地下黨(native)關係密切,沒有理由不去看看編程


1.先看一下這個看着嚇死人的生命週期

別怕,等會一點一點來看緩存

MediaPlayer生命週期


2.界面

我可不想用幾個按鈕點點完事,能好看點,就好看點吧,反正佈局也不費事
這是我寫的播放器從中拆出一個播放條放在這裏用一下
用了之前寫的兩個自定義控件:頂上的播放進度,和按鈕點擊變淺再還原
怎麼自定義的和今天關聯不大,也比較簡單(也本身看源碼),也能夠用按鈕和進度條代替bash

播放條.png


3.先看構造方法
/**
 * Default constructor. Consider using one of the create() methods for
 * synchronously instantiating a MediaPlayer from a Uri or resource.
 * <p>When done with the MediaPlayer, you should call  {@link #release()},
 * to free the resources. If not released, too many MediaPlayer instances may
 * result in an exception.</p>
    默認構造函數。考慮使用create()方法之一從Uri或資源同步地實例化MediaPlayer。
    使用MediaPlayer時,您應該調用release(),釋放資源。
    若是不釋放,太多的MediaPlayer實例可能會致使異常
 */
 
public MediaPlayer() {
    super(new AudioAttributes.Builder().build(),//父類構造
            AudioPlaybackConfiguration.PLAYER_TYPE_JAM_MEDIAPLAYER);
    Looper looper;
    if ((looper = Looper.myLooper()) != null) {
        mEventHandler = new EventHandler(this, looper);
    } else if ((looper = Looper.getMainLooper()) != null) {
        mEventHandler = new EventHandler(this, looper);
    } else {
        mEventHandler = null;
    }
    mTimeProvider = new TimeProvider(this);
    mOpenSubtitleSources = new Vector<InputStream>();
    /* Native setup requires a weak reference to our object.
     * It's easier to create it here than in C++. native_setup須要對對象的弱引用。在這裏比在c++中更容易建立 */ native_setup(new WeakReference<MediaPlayer>(this)); baseRegisterPlayer(); } ---->[在native中setup] private native final void native_setup(Object mediaplayer_this); 複製代碼

4.create()的五個重載方法:

說是5個,核心也就是兩個:即Uri定位資源,以及res的id定義資源服務器

* @param context 上下文
     * @param uri 資源路徑標示符
     * @param holder 用於顯示視頻的SurfaceHolder,能夠爲空(音頻無視).
     * @param audioAttributes 音頻屬性類對象
     * @param audioSessionId 媒體播放器要使用的音頻會話ID,請參見{AudioManager#generateAudioSessionId()}以得到新會話
     * @return a MediaPlayer object, or null if creation failed
  
    public static MediaPlayer create(Context context, Uri uri, SurfaceHolder holder, AudioAttributes audioAttributes, int audioSessionId) {
        try {
            MediaPlayer mp = new MediaPlayer();//建立MediaPlayer實例
            final AudioAttributes aa = audioAttributes != null ? audioAttributes :
                new AudioAttributes.Builder().build();//音頻屬性爲空,則new一個
            mp.setAudioAttributes(aa);//設置音頻屬性
            mp.setAudioSessionId(audioSessionId);//設置會話ID
            mp.setDataSource(context, uri);//設置資源
            if (holder != null) {//SurfaceHolder不爲空
                mp.setDisplay(holder);//播放SurfaceHolder視頻
            }
            mp.prepare();//準備
            return mp;//返回MediaPlayer實例
        } catch (IOException ex) {
            Log.d(TAG, "create failed:", ex);
            // fall through
        } catch (IllegalArgumentException ex) {
            Log.d(TAG, "create failed:", ex);
            // fall through
        } catch (SecurityException ex) {
            Log.d(TAG, "create failed:", ex);
            // fall through
        }
        return null;
    }
    
---->[三參重載,音頻屬性爲空]
public static MediaPlayer create(Context context, Uri uri, SurfaceHolder holder) {
    int s = AudioSystem.newAudioSessionId();
    return create(context, uri, holder, null, s > 0 ? s : 0);
}

---->[兩參重載,SurfaceHolder爲空]
public static MediaPlayer create(Context context, Uri uri) {
    return create (context, uri, null);
}
複製代碼

從res獲取資源相似,本身看看(資源放在res/raw下)
不多有歌曲直接放在res裏的,放點音效還差很少,但音效播放有更好的選擇微信


3、MediaPlayer的簡單使用

讀取Uri的兩參重載做爲播放音頻文件可謂恰到好處網絡

1.使用Uri播放網絡歌曲

恰好服務器上放了幾首歌,玩玩唄---最簡易版播放
記得權限(我掉坑了)<uses-permission android:name="android.permission.INTERNET"/>

1.1--MusicPlayer封裝類
public class MusicPlayer {
    private MediaPlayer mPlayer;
    private Context mContext;
    
    public MusicPlayer(Context context) {
        mContext = context;
        init();
    }
    
    //初始化
    private void init() {
        Uri uri = Uri.parse("http://www.toly1994.com:8089/file/洛天依.mp3");
        mPlayer = MediaPlayer.create(mContext, uri);
    }
    
    //開始播放
    public void start() {
        mPlayer.start();
    }
}
複製代碼

1.2--Activity中
MusicPlayer musicPlayer = new MusicPlayer(this);//實例化
//點擊播放時
musicPlayer.start();//播放
複製代碼

播放正常,可是從網絡資源初始化MusicPlayer耗時很長
因爲初始化在主線程中進行,因此白屏了好一會,這怎麼能忍


1.3在另外一個線程初始化

未初始化完成時不能播放,return掉

public class MusicPlayer {
    private MediaPlayer mPlayer;
    private Context mContext;

    private boolean isInitialized = false;//是否已初始化
    private Thread initThread;//初始化線程

    public MusicPlayer(Context context) {
        mContext = context;
        initThread = new Thread(this::init);
        initThread.start();
    }

    private void init() {
        Uri uri = Uri.parse("http://www.toly1994.com:8089/file/洛天依.mp3");
        mPlayer = MediaPlayer.create(mContext, uri);
        isInitialized = true;//已初始化
    }

    /**
     * 播放
     */
    public void start() {
        if (!isInitialized) {
            return;
        }
        mPlayer.start();
    }

    /**
     * 銷燬
     */
    public void onDestroyed() {
        if (mPlayer != null) {
            mPlayer.release();//釋放資源
            mPlayer = null;
        }
        isInitialized = false;
    }
}
複製代碼

2.播放本地SD卡音樂

記得加權限:讀寫一塊兒加了吧,免得以後加
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
這個就簡單了,直接該一下Uri就好了

Uri uri = Uri.fromFile(
new File(Environment.getExternalStorageDirectory().getPath(),
"toly/勇氣-梁靜茹-1772728608-1.mp3"));
複製代碼

4、MediaPlayer的生命週期與暫停控制

1.形象一點描述下面幾個生命週期
Idle 狀態:無業遊民
Initialized 狀態:找到工做
Prepared 狀態:找到工做後準備好了明天要帶的東西
Started 狀態:開始工做
Paused 狀態:我要停下喝口茶
Stop 狀態:回家睡覺(想再工做,還必需要準備一下)

End 狀態:功德圓滿,往生極樂
Error狀態:滿身罪孽,遺臭萬年

注:Stop狀態從新播放,需經過prepareAsync()和prepare()回到先前的Prepared狀態從新開始才能夠。  
總感受stop方法有點雞肋...
複製代碼

生命週期一部分.png


2.MusicPlayer暫停播放功能

能夠看出MediaPlayer.create時就已經度過了Idle,Initialized,Prepared狀態

public class MusicPlayer {
    private MediaPlayer mPlayer;
    private Context mContext;

    private boolean isInitialized = false;//是否已初始化
    private Thread initThread;

    public MusicPlayer(Context context) {
        mContext = context;

        initThread = new Thread(this::init);
        initThread.start();
    }

    private void init() {
        Uri uri = Uri.fromFile(new File(Environment.getExternalStorageDirectory().getPath(), "toly/勇氣-梁靜茹-1772728608-1.mp3"));
        mPlayer = MediaPlayer.create(mContext, uri);
        isInitialized = true;
        
        mPlayer.setOnErrorListener((mp, what, extra) -> {
            //處理錯誤
            return false;
        });
    }

    /**
     * 播放
     */
    public void start() {
        //未初始化和正在播放時return
        if (!isInitialized && mPlayer.isPlaying()) {
            return;
        }
        mPlayer.start();
    }
    /**
     * 是否正在播放
     */
    public boolean isPlaying() {
        //未初始化和正在播放時return
        if (!isInitialized) {
            return false;
        }
        return mPlayer.isPlaying();
    }

    /**
     * 銷燬播放器
     */
    public void onDestroyed() {
        if (mPlayer != null) {
            mPlayer.stop();
            mPlayer.release();//釋放資源
            mPlayer = null;
        }
        isInitialized = false;
    }

    /**
     * 中止播放器
     */
    private void stop() {
        if (mPlayer != null && mPlayer.isPlaying()) {
            mPlayer.stop();
        }
    }
    
    /**
     * 暫停播放器
     */
    public void pause() {
        if (mPlayer != null && mPlayer.isPlaying()) {
            mPlayer.pause();
        }
    }
}
複製代碼

3.Activity中的修改

根據musicPlayer的狀態來更改圖標以及播放或暫停

mIdIvCtrl.setOnClickListener(v->{
    if (musicPlayer.isPlaying()) {
        musicPlayer.pause();
        mIdIvCtrl.setImageResource(R.drawable.icon_stop_2);//設置圖標暫停
    } else {
        musicPlayer.start();
        mIdIvCtrl.setImageResource(R.drawable.icon_start_2);//設置圖標播放
    }
});
複製代碼

4、增長進度的監聽

使用Timer,播放時每秒刷新一次,回調進度,不播放則不刷新
Timer裏的TimeTask非主線程,簡單用Handler推回主線程刷新視圖

添加進度監聽.png


1.MusicPlayer修改
//構造函數中
mTimer = new Timer();//建立Timer
mHandler = new Handler();//建立Handler

//開始方法中
mTimer.schedule(new TimerTask() {
    @Override
    public void run() {
        if (isPlaying()) {
            int pos = mPlayer.getCurrentPosition();
            int duration = mPlayer.getDuration();
            mHandler.post(() -> {
                if (mOnSeekListener != null) {
                    mOnSeekListener.OnSeek((int) (pos * 1.f / duration * 100));
                }
            });
        }
    }
}, 0, 1000);

//------------設置進度監聽-----------
public interface OnSeekListener {
    void OnSeek(int per_100);
}
private OnSeekListener mOnSeekListener;
public void setOnSeekListener(OnSeekListener onSeekListener) {
    mOnSeekListener = onSeekListener;
}
複製代碼

2.在Activity中調用監聽
musicPlayer.setOnSeekListener(per_100 -> {
    mIdPvPre.setProgress(per_100);//爲進度條設置進度
});
複製代碼

ok,進度條就怎麼簡單


5、MediaPlayer的監聽

拖動與進度

1.跳轉方法:MusicPlayer
/**
 * 跳轉到
 * @param pre_100 0~100
 */
public void seekTo(int pre_100) {
    pause();
    mPlayer.seekTo((int) (pre_100/100.f*mPlayer.getDuration()));
    start();
}
複製代碼

2.使用跳轉:Activity
mIdPvPre.setOnDragListener(pre_100 -> {
    musicPlayer.seekTo(pre_100);
});
複製代碼

拖動就這麼簡單...


6、其餘的一些監聽方法+網絡音頻流

1.經常使用的幾個監聽:
//當裝載流媒體完畢的時候回調
mPlayer.setOnPreparedListener(mp->{
    L.d("OnPreparedListener"+L.l());
});

//播放完成監聽
mPlayer.setOnCompletionListener(mp -> {
    L.d("CompletionListene"+L.l());
    start();//播放完成再播放--實現單曲循環
});

//seekTo方法完成回調
mPlayer.setOnSeekCompleteListener(mp -> {
    L.d("SeekCompleteListener"+L.l());
});

//網絡流媒體的緩衝變化時回調
mPlayer.setOnBufferingUpdateListener((mp, percent) -> {
    L.d("BufferingUpdateListener" + percent + L.l());
});
複製代碼

2.網絡音頻流

一下說那麼多感受有點繞,Preparing是prepareAsync()函數調用後進入的狀態
和OnPreparedListener.onPrepared()回調配合,適合網絡流的播放
剛纔是經過create()建立的MediaPlayer,源碼中create()調用了prepare()
而想要異步準備,須要本身定義MediaPlayer,因爲異步準備,並且有回調,就不用開線程了

private void init() {
    mPlayer = new MediaPlayer();//1.無業遊民
    Uri uri = Uri.parse("http://www.toly1994.com:8089/file/洛天依.mp3");
    try {
        mPlayer.setDataSource(mContext, uri);//2.找到工做
        mPlayer.prepareAsync();//3.異步準備明天的工做
    } catch (IOException e) {
        e.printStackTrace();
    }
    //當裝載流媒體完畢的時候回調
    mPlayer.setOnPreparedListener(mp -> {//4.準備OK
        L.d("OnPreparedListener" + L.l());
        isInitialized = true;
    });
複製代碼
Preparing 狀態:找到工做後正在準備好了明天要帶的東西
主要是和prepareAsync()配合,會異步準備
完成觸發OnPreparedListener.onPrepared(),進而進入Prepared狀態。

PlaybackCompleted狀態:工做作完了
文件正常播放完畢,而又沒有設置循環播放的話就進入該狀態,並會觸發OnCompletionListener的onCompletion()方法。
複製代碼

4.緩存的進度監聽

一開始讀文件的時候這個緩存監聽沒什麼卵用,但網絡就不同了
網絡緩存時能夠監聽到緩存

//網絡流媒體的緩衝變化時回調
mPlayer.setOnBufferingUpdateListener((mp, percent) -> {
    L.d("BufferingUpdateListener"+percent+L.l());
});
複製代碼

緩存的進度.png


5.雙進度的實現

緩存進度(淡藍色),播放進度(橘黃色),緩存進度能夠看出緩存到哪,拖動也方便

雙進度.png


5.1--NetMusicPlayer處理
//網絡流媒體的緩衝變化時回調
mPlayer.setOnBufferingUpdateListener((mp, percent) -> {
    if (mOnBufferListener != null) {
        mOnBufferListener.OnSeek(percent);
    }
});

 //------------設置緩存進度監聽-----------
 public interface OnBufferListener {
     void OnSeek(int per_100);
 }
 private MusicPlayer.OnBufferListener mOnBufferListener;
 public void setOnBufferListener(MusicPlayer.OnBufferListener onBufferListener) {
     mOnBufferListener = onBufferListener;
 }
複製代碼
5.2--Activity裏回調監聽
musicPlayer.setOnBufferListener(per_100 -> {
    mIdPvPre.setProgress2(per_100);
});
複製代碼

好了,就這樣:留圖鎮樓

完整版.png


後記:捷文規範

1.本文成長記錄及勘誤表
項目源碼 日期 備註
V0.1-github 2018-1-4 Android多媒體之認識MP3與內置媒體播放(MediaPlayer)
2.更多關於我
筆名 QQ 微信 愛好
張風捷特烈 1981462002 zdl1994328 語言
個人github 個人簡書 個人掘金 我的網站
3.聲明

1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大編程愛好者共同交流
3----我的能力有限,若有不正之處歡迎你們批評指證,一定虛心改正
4----看到這裏,我在此感謝你的喜歡與支持


icon_wx_200.png
相關文章
相關標籤/搜索