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

張風捷特烈發表於2019-01-04

零、前言

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

勇氣.png

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

雙進度.png


MP3的簡介

0.[番外]--說兩句

初中那會還是物理鍵盤手機,當時記憶體卡感覺很寶貝,2G都大的不得了
一開始只有一個256MB的記憶體卡,那時誰不喜歡聽音樂,看電子書呢?
當時沒有網,只能讓姐姐幫我下載,我要求:下那種佔記憶體最小的歌
因為我發現有的都4M,有的0.4M,而且都能聽,當時有歌能聽就行,音質完全不在意
當時記憶體不夠時,我就挑最大記憶體的歌,記下歌名,忍痛刪掉
現在哪個最大下哪個,但對收藏音樂的感覺已經沒有了,播放,聽聽就算了


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壓縮的部分:

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

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

4.壓縮率與音質

腳趾頭想想都知道,同一檔案,同一壓縮技術:
壓縮率越高,過濾的資訊越多,檔案越小,音質越差
反之亦然,320Kbps可以算音質非常不錯了
複製程式碼

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


二、MediaPlayer簡述

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

複製程式碼

Android作為移動裝置,音訊播放的類也就那幾個,MediaPlayer作為中流砥柱
MediaPlayer是個挺大的類,又和地下黨(native)關係密切,沒有理由不去看看


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

別怕,等會一點一點來看

MediaPlayer生命週期


2.介面

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

播放條.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裡的,放點音效還差不多,但音效播放有更好的選擇


三、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"));
複製程式碼

四、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);//設定圖示播放
    }
});
複製程式碼

四、增加進度的監聽

使用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,進度條就怎麼簡單


五、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);
});
複製程式碼

拖動就這麼簡單...


六、其他的一些監聽方法+網路音訊流

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

相關文章