零、前言
作為90後,mp3格式的音樂可謂靈魂之友。
小時候帶著耳機,躺在桌子上聽歌看月亮心情依稀。
當某個旋律想起,還會不會浮現某個風景,某個人……,
今天全程單曲播放——梁靜茹-勇氣(獻上頻譜)
主要任務:SD卡音樂、網路音訊流的播放及控制
MP3的簡介
0.[番外]--說兩句
初中那會還是物理鍵盤手機,當時記憶體卡感覺很寶貝,2G都大的不得了
一開始只有一個256MB的記憶體卡,那時誰不喜歡聽音樂,看電子書呢?
當時沒有網,只能讓姐姐幫我下載,我要求:下那種佔記憶體最小的歌
因為我發現有的都4M,有的0.4M,而且都能聽,當時有歌能聽就行,音質完全不在意
當時記憶體不夠時,我就挑最大記憶體的歌,記下歌名,忍痛刪掉
現在哪個最大下哪個,但對收藏音樂的感覺已經沒有了,播放,聽聽就算了
1.勇氣歌曲資訊分析
立體聲:聲道數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.先看一下這個看著嚇死人的生命週期
別怕,等會一點一點來看
2.介面
我可不想用幾個按鈕點點完事,能好看點,就好看點吧,反正佈局也不費事
這是我寫的播放器從中拆出一個播放條放在這裡用一下
用了以前寫的兩個自定義控制元件:頂上的播放進度,和按鈕點選變淺再還原
怎麼自定義的和今天關聯不大,也比較簡單(也自己看原始碼),也可以用按鈕和進度條代替
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方法有點雞肋...
複製程式碼
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推回主執行緒重新整理檢視
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());
});
複製程式碼
5.雙進度的實現
快取進度(淡藍色),播放進度(橘黃色),快取進度可以看出快取到哪,拖動也方便
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);
});
複製程式碼
好了,就這樣:留圖鎮樓
後記:捷文規範
1.本文成長記錄及勘誤表
專案原始碼 | 日期 | 備註 |
---|---|---|
V0.1-github | 2018-1-4 | Android多媒體之認識MP3與內建媒體播放(MediaPlayer) |
2.更多關於我
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
我的github | 我的簡書 | 我的掘金 | 個人網站 |
3.宣告
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援