做爲90後,mp3格式的音樂可謂靈魂之友。
小時候帶着耳機,躺在桌子上聽歌看月亮心情依稀。
當某個旋律想起,還會不會浮現某個風景,某我的……,
今天全程單曲播放——梁靜茹-勇氣(獻上頻譜)android
SD卡音樂、網絡音頻流的播放及控制
[番外]--說兩句
初中那會仍是物理鍵盤手機,當時內存卡感受很寶貝,2G都大的不得了
一開始只有一個256MB的內存卡,那時誰不喜歡聽音樂,看電子書呢?
當時沒有網,只能讓姐姐幫我下載,我要求:下那種佔內存最小的歌
由於我發現有的都4M,有的0.4M,並且都能聽,當時有歌能聽就行,音質徹底不在乎
當時內存不夠時,我就挑最大內存的歌,記下歌名,忍痛刪掉
如今哪一個最大下哪一個,但對收藏音樂的感受已經沒有了,播放,聽聽就算了c++
立體聲:聲道數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,怎麼大,估計很難接收
複製代碼
MP3是一種音頻有損壓縮技術
(知識來源,百度百科)MP3(Moving Picture Experts Group Audio Layer III)是指的是MPEG-1標準中的音頻部分
MPEG音頻文件的壓縮是一種有損壓縮,MP3音頻具備10:1~12:1的高壓縮率
可見《勇氣》碼率由2756.25Kbps壓縮到320Kbps,壓縮率:8.61:1
複製代碼
上篇說到的
心理聲學
,根據人耳模型,無損數據中存在大量的冗餘信息
壓縮就是對冗餘的數據進行過濾,或刻意對不重要的信息進行剔除git
利用人耳對高頻聲音信號不敏感的特性,將時域波形信號轉換成頻域信號,
並劃分紅多個頻段,對不一樣的頻段使用不一樣的壓縮率,對高頻加大壓縮比(甚至忽略信號)
對低頻信號使用小壓縮比,保證信號不失真。就至關於拋棄人耳基本聽不到的高頻聲音
來換取文件的尺寸,用 *.mp3 格式來儲存
複製代碼
腳趾頭想一想都知道,同一文件,同一壓縮技術:
壓縮率越高,過濾的信息越多,文件越小,音質越差
反之亦然,320Kbps能夠算音質很是不錯了
複製代碼
科普就這樣,下面進入今天的重頭戲
MediaPlayer
github
父類/接口:PlayerBase/SubtitleController.Listener/VolumeAutomation
源碼行數:5618 ----通讀hold不住
內部類:27個--其中接口類13個,普通類11個
構造方法:1個,無參構造
間接構造(方法返回該類實例):5個
方法數:目測120+
字段數:目測90+
複製代碼
Android做爲移動設備,音頻播放的類也就那幾個,MediaPlayer做爲中流砥柱
MediaPlayer是個挺大的類,又和地下黨(native)關係密切,沒有理由不去看看編程
別怕,等會一點一點來看緩存
我可不想用幾個按鈕點點完事,能好看點,就好看點吧,反正佈局也不費事
這是我寫的播放器從中拆出一個播放條放在這裏用一下
用了之前寫的兩個自定義控件:頂上的播放進度,和按鈕點擊變淺再還原
怎麼自定義的和今天關聯不大,也比較簡單(也本身看源碼),也能夠用按鈕和進度條代替bash
/**
* 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); 複製代碼
說是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裏的,放點音效還差很少,但音效播放有更好的選擇微信
讀取Uri的兩參重載做爲播放音頻文件可謂恰到好處網絡
恰好服務器上放了幾首歌,玩玩唄---最簡易版播放
記得權限(我掉坑了)<uses-permission android:name="android.permission.INTERNET"/>
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();
}
}
複製代碼
MusicPlayer musicPlayer = new MusicPlayer(this);//實例化
//點擊播放時
musicPlayer.start();//播放
複製代碼
播放正常,可是從網絡資源初始化MusicPlayer耗時很長
因爲初始化在主線程中進行,因此白屏了好一會,這怎麼能忍
未初始化完成時不能播放,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;
}
}
複製代碼
記得加權限:讀寫一塊兒加了吧,免得以後加
<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"));
複製代碼
Idle 狀態:無業遊民
Initialized 狀態:找到工做
Prepared 狀態:找到工做後準備好了明天要帶的東西
Started 狀態:開始工做
Paused 狀態:我要停下喝口茶
Stop 狀態:回家睡覺(想再工做,還必需要準備一下)
End 狀態:功德圓滿,往生極樂
Error狀態:滿身罪孽,遺臭萬年
注:Stop狀態從新播放,需經過prepareAsync()和prepare()回到先前的Prepared狀態從新開始才能夠。
總感受stop方法有點雞肋...
複製代碼
能夠看出
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();
}
}
}
複製代碼
根據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推回主線程刷新視圖
//構造函數中
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;
}
複製代碼
musicPlayer.setOnSeekListener(per_100 -> {
mIdPvPre.setProgress(per_100);//爲進度條設置進度
});
複製代碼
ok,進度條就怎麼簡單
MusicPlayer
/**
* 跳轉到
* @param pre_100 0~100
*/
public void seekTo(int pre_100) {
pause();
mPlayer.seekTo((int) (pre_100/100.f*mPlayer.getDuration()));
start();
}
複製代碼
mIdPvPre.setOnDragListener(pre_100 -> {
musicPlayer.seekTo(pre_100);
});
複製代碼
拖動就這麼簡單...
//當裝載流媒體完畢的時候回調
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());
});
複製代碼
一下說那麼多感受有點繞,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()方法。
複製代碼
一開始讀文件的時候這個緩存監聽沒什麼卵用,但網絡就不同了
網絡緩存時能夠監聽到緩存
//網絡流媒體的緩衝變化時回調
mPlayer.setOnBufferingUpdateListener((mp, percent) -> {
L.d("BufferingUpdateListener"+percent+L.l());
});
複製代碼
緩存進度(淡藍色),播放進度(橘黃色),緩存進度能夠看出緩存到哪,拖動也方便
//網絡流媒體的緩衝變化時回調
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;
}
複製代碼
musicPlayer.setOnBufferListener(per_100 -> {
mIdPvPre.setProgress2(per_100);
});
複製代碼
好了,就這樣:留圖鎮樓
項目源碼 | 日期 | 備註 |
---|---|---|
V0.1-github | 2018-1-4 | Android多媒體之認識MP3與內置媒體播放(MediaPlayer) |
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
個人github | 個人簡書 | 個人掘金 | 我的網站 |
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大編程愛好者共同交流
3----我的能力有限,若有不正之處歡迎你們批評指證,一定虛心改正
4----看到這裏,我在此感謝你的喜歡與支持