Android多媒体之认识MP3与内置媒体播放(MediaPla
零、前言
勇气.png作为90后,mp3格式的音乐可谓灵魂之友。
小时候带着耳机,躺在桌子上听歌看月亮心情依稀。
当某个旋律想起,还会不会浮现某个风景,某个人……,
今天全程单曲播放——梁静茹-勇气(献上频谱)
主要任务: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标准中的音频部分
MPEG音频文件的压缩是一种有损压缩,MPEG3音频编码具有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);//设置图标播放
}
});
四、增加进度的监听
添加进度监听.png使用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());
});
缓存的进度.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.更多关于我
笔名 | 微信 | 爱好 | |
---|---|---|---|
张风捷特烈 | 1981462002 | zdl1994328 | 语言 |
我的github | 我的简书 | 我的掘金 | 个人网站 |
3.声明
1----本文由张风捷特烈原创,转载请注明
2----欢迎广大编程爱好者共同交流
3----个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正
4----看到这里,我在此感谢你的喜欢与支持
icon_wx_200.png