零, 前言
作为 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 是一种音频有损压缩技术
(知识来源, 百度百科 https://baike.baidu.com/item/MP3/23904 )
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. 先看一下这个看着吓死人的生命周期
别怕, 等会一点一点来看
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. 本文成长记录及勘误表
项目源码 https://github.com/toly1994328/Sound | 日期 | 备注 |
---|---|---|
V0.1-github https://github.com/toly1994328/Sound | 2018-1-4 | Android 多媒体之认识 MP3 与内置媒体播放(MediaPlayer) https://www.jianshu.com/p/ae48f42a5ab4 |
2. 更多关于我
笔名 | 微信 | 爱好 | |
---|---|---|---|
张风捷特烈 | 1981462002 | zdl1994328 | 语言 |
我的 github https://github.com/toly1994328 | 我的简书 https://www.jianshu.com/u/e4e52c116681 | 我的掘金 | 个人网站 http://www.toly1994.com |
3. 声明
1---- 本文由张风捷特烈原创, 转载请注明
2---- 欢迎广大编程爱好者共同交流
3---- 个人能力有限, 如有不正之处欢迎大家批评指证, 必定虚心改正
4---- 看到这里, 我在此感谢你的喜欢与支持
来源: https://juejin.im/post/5c2f1ef8e51d4550fc42aea5