Android多媒体之认识MP3与内置媒体播放(MediaPlayer)

零、前言

作为90后,mp3格式的音乐可谓灵魂之友。 小时候带着耳机,躺在桌子上听歌看月亮心情依稀。 当某个旋律想起,还会不会浮现某个风景,某个人……, 今天全程单曲播放——梁静茹-勇气(献上频谱)

勇气.png

主要任务: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-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.先看一下这个看着吓死人的生命周期

别怕,等会一点一点来看

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);//设置图标播放
    }
});

四、增加进度的监听

使用Timer,播放时每秒刷新一次,回调进度,不播放则不刷新 Timer里的TimeTask非主线程,简单用Handler推回主线程刷新视图

添加进度监听.png


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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券