作为90后,mp3格式的音乐可谓灵魂之友。 小时候带着耳机,躺在桌子上听歌看月亮心情依稀。 当某个旋律想起,还会不会浮现某个风景,某个人……, 今天全程单曲播放——梁静茹-勇气(献上频谱)
勇气.png
SD卡音乐、网络音频流的播放及控制
双进度.png
[番外]--说两句
初中那会还是物理键盘手机,当时内存卡感觉很宝贝,2G都大的不得了 一开始只有一个256MB的内存卡,那时谁不喜欢听音乐,看电子书呢? 当时没有网,只能让姐姐帮我下载,我要求:下那种占内存最小的歌 因为我发现有的都4M,有的0.4M,而且都能听,当时有歌能听就行,音质完全不在意 当时内存不够时,我就挑最大内存的歌,记下歌名,忍痛删掉 现在哪个最大下哪个,但对收藏音乐的感觉已经没有了,播放,听听就算了
勇气歌曲信息.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,怎么大,估计很难接收
MP3是一种音频有损压缩技术
(知识来源,百度百科)MP3(Moving Picture Experts Group Audio Layer III)是指的是MPEG-1标准中的音频部分
MPEG音频文件的压缩是一种有损压缩,MP3音频具有10:1~12:1的高压缩率
可见《勇气》码率由2756.25Kbps压缩到320Kbps,压缩率:8.61:1
上篇说到的
心理声学
,根据人耳模型,无损数据中存在大量的冗余信息 压缩就是对冗余的数据进行过滤,或刻意对不重要的信息进行剔除
利用人耳对高频声音信号不敏感的特性,将时域波形信号转换成频域信号,
并划分成多个频段,对不同的频段使用不同的压缩率,对高频加大压缩比(甚至忽略信号)
对低频信号使用小压缩比,保证信号不失真。就相当于抛弃人耳基本听不到的高频声音
来换取文件的尺寸,用 *.mp3 格式来储存
脚趾头想想都知道,同一文件,同一压缩技术:
压缩率越高,过滤的信息越多,文件越小,音质越差
反之亦然,320Kbps可以算音质非常不错了
科普就这样,下面进入今天的重头戏
MediaPlayer
父类/接口:PlayerBase/SubtitleController.Listener/VolumeAutomation
源码行数:5618 ----通读hold不住
内部类:27个--其中接口类13个,普通类11个
构造方法:1个,无参构造
间接构造(方法返回该类实例):5个
方法数:目测120+
字段数:目测90+
Android作为移动设备,音频播放的类也就那几个,MediaPlayer作为中流砥柱 MediaPlayer是个挺大的类,又和地下党(native)关系密切,没有理由不去看看
别怕,等会一点一点来看
MediaPlayer生命周期
我可不想用几个按钮点点完事,能好看点,就好看点吧,反正布局也不费事 这是我写的播放器从中拆出一个播放条放在这里用一下 用了以前写的两个自定义控件:顶上的播放进度,和按钮点击变浅再还原 怎么自定义的和今天关联不大,也比较简单(也自己看源码),也可以用按钮和进度条代替
播放条.png
/**
* 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方法有点鸡肋...
生命周期一部分.png
可以看出
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推回主线程刷新视图
添加进度监听.png
//构造函数中
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());
});
缓存的进度.png
缓存进度(淡蓝色),播放进度(橘黄色),缓存进度可以看出缓存到哪,拖动也方便
双进度.png
//网络流媒体的缓冲变化时回调
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);
});
好了,就这样:留图镇楼
完整版.png