开发PigMediaStudio依赖库过程记录(二)
在最近的工作中,接触了许多关于MediaPlayer、SoundPool、TextToSpeach的使用,希望能够封装一个MediaPlayer、TextToSpeach功能兼具的library。由此篇博客开始,记录这个依赖库的创建过程。
为什么取这个名字?因为我觉得猪猪憨憨很可爱。
说明: 这算是个学习过程记录,已经写好的代码会随进度而有一定的改变,尤其是方法和变量的命名,在完成功能过程中不会很在意,等完成后会选择恰当方式重新命名,如果需要直接参考,请直接查看当前进度的最后结果
github地址
https://github.com/fytuuu/PigMediaStudio
文章链接:
我们接下来进行SoundPool封装
首先对SoundPool有一些感性的认识:
- SoundPool轻量,更适合游戏等 短促、密集 的音效播放
- 可以给App添加提示音,比如最典型的酷狗音乐打开时候播放“哈喽,酷狗”
- SoundPool可以从apk中导入资源,也可以导入文件资源(前提拿到读写权限)
- SoundPool利用MediaPlayer服务 为音频解码为一个原始16位PCM流,这个特性使得应用程序可以进行流压缩,减少播放音频时解压带来的CPU负载和延迟
- SoundPool顾名思义,使用音效池管理多个音频流,如果超过流的最大数目(这个数目由开发者定),SoundPool会基于优先级自动停止先前正在播放的流
- SoundPool还支持设置语音的品质、音量、播放比率等参数
- SoundPool的 load()方法 每个资源最多只有1M空间,所以不适合播放背景音乐等长音效、高品质音效
- SoundPool无法监听播放进度,只支持(全部)暂停、(全部)继续播放、播放
首先对SoundPool的常用方法有个了解:
1.构造方法
在 API 21 (Android 5.0) 之前可以使用下面这个构造方法
SoundPool(int maxStreams, int streamType, int srcQuality)
-
maxStreams
指定支持多少个声音,SoundPool 对象中允许同时存在的最大流的数量 -
streamType
指定声音类型,在 AudioManager 定义了以下几种流类型
STREAM_VOICE_CALL
STREAM_SYSTEM
STREAM_RING
STREAM_MUSIC
STREAM_ALARM
-
srcQuality
指定声音品质(采样率变换质量),一般直接设置为 0
但在 API 21(Android 5.0) 及以上版本,这个构造方法被废弃了,取而代之的是使用 SoundPool.Builder() 方法
SoundPool.Builder spb = new SoundPool.Builder();
spb.setMaxStreams(10);
spb.setAudioAttributes(null); //转换音频格式
SoundPool sp = spb.build(); //创建SoundPool对象
因此在使用过程中要对系统的版本进行判断,以选择合适的方法
// 版本大于等于 21 (Anndroid 5.0)
SoundPool sp = null;
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
SoundPool.Builder spb = new SoundPool.Builder();
spb.setMaxStreams(10);
spb.setAudioAttributes(null); //转换音频格式
sp = spb.build(); //创建SoundPool对象
} else {
sp = SoundPool(10, AudioManager.STREAM_SYSTEM, 5);
}
2.load()方法 – 加载音频资源/本地音频文件
load() 重载方法,每种方法都会返回一个声音的 ID (int类型,从1开始)
load(Context context, int resId, int priority)
load(String path, int priority)
load(FileDescriptor fd, long offset, long length, int priority)
load(AssetFileDescriptor afd, int priority)
- context 上下文
- resId 资源id
- priority 没什么用的一个参数,建议设置为1,保持和未来的兼容性
- path 文件路径
- FileDescriptor 文件描述符
- AssetFileDescriptor 从 asset 目录读取某个资源文件,用法
- AssetFileDescriptor descriptor = assetManager.openFd(“kugou.mp3”)
注意一下load方法的官方提示
/**
* Load the sound from the specified APK resource.
*
* Note that the extension is dropped. For example, if you want to load
* a sound from the raw resource file "explosion.mp3", you would specify
* "R.raw.explosion" as the resource ID. Note that this means you cannot
* have both an "explosion.wav" and an "explosion.mp3" in the res/raw
* directory.
*
* @param context the application context
* @param resId the resource ID
* @param priority the priority of the sound. Currently has no effect. Use
* a value of 1 for future compatibility.
* @return a sound ID. This value can be used to play or unload the sound.
*/
public int load(Context context, int resId, int priority) {...}
3.play() --播放音频,设置属性
play(int soundID, float leftVolume, float rightVolume, int priority, int loop, float rate)
- soundID load() 返回的声音 ID 号 (从1开始)
- leftVolume 左声道音量设置
- rightVolume 右声道音量设置
- priority 指定播放声音的优先级,数值越高,优先级越大。
- loop 指定是否循环
-1 表示无限循环
0 表示不循环
其它值表示要重复播放的次数 - rate 指定播放速率,播放速率的取值范围是 0.5 至 2.0
1.0 的播放率可以使声音按照其原始频率
2.0 的播放速率,可以使声音按照其原始频率的两倍播放
0.5的播放率,则播放速率是原始频率的一半
4.release() – 用于释放资源
release(int soundID)
release() 方法用于释放所有 SoundPool 对象占据的内存和资源
核心是load方法,play()之前必须load好,load完成会返回一个ID(int类型,从1开始), 这个ID对应指向这个资源文件,play()的时候需要我们将这个资源文件的ID作为参数。
所以我们完成load时候,将资源/文件的某种标志,如文件名或者文件内容作为key,将load返回的ID作为value,往后通过key,把value取出来交给play()会比较方便使用。
那我们开始码起
1. 创建PigSoundPlayer类,用于控制SoundPool
首先我们准备好用于储存资源、文件的HashMap(暂时不考虑key的唯一性问题)。我们给PigSoundPlayer初始化的同时也给SoundPool的属性进行设置。如果不填入,那么默认值为maxStream为1,quality为5,attribute为null。
public class PigSoundPlayer {
private final static String TAG = "PigSoundPlayer";
//用于存储资源文件
private static HashMap<String, Integer> resourceMap = new HashMap<>();
//音效池
private static SoundPool soundPool;
//单例
private static PigSoundPlayer pigSoundPlayer;
//上下文管理者
private static ContextManager contextManager;
//构造器私有
private PigSoundPlayer(){}
//初始化
public static void initSoundPlayer(int maxStreams, AudioAttributes attributes, int quality){
if (pigSoundPlayer == null) pigSoundPlayer = new PigSoundPlayer();
//控制maxStream的错误值
if (maxStreams<=0) maxStreams = 1;
//根据不同版本进行SoundPool的初始化
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
SoundPool.Builder spb = new SoundPool.Builder();
spb.setMaxStreams(maxStreams);
if (attributes!=null )spb.setAudioAttributes(attributes);
soundPool = spb.build();
}else{
soundPool = new SoundPool(maxStreams, AudioManager.STREAM_SYSTEM,quality);
}
}
public static void initSoundPlayer(int maxStreams, int quality){
initSoundPlayer(maxStreams,null,quality);
}
public static void initSoundPlayer(int maxStreams, AudioAttributes attributes){
initSoundPlayer(maxStreams,attributes,5);
}
public static void initSoundPlayer(int maxStreams){
initSoundPlayer(maxStreams,null);
}
public static void initSoundPlayer(){
initSoundPlayer(1);
}
2. 我们再添加一个内部类Loader
进行load操作
Loader
每个方法中的第一个参数tag,是resourceMap
的第一个参数,方便通过字符串标志来从map中找到对应的资源id,当然也可以直接通过返回值获得id
public static class Loader{
private Loader(){}
public int load(String tag,int resId,int priority){
int id = -1;
if (pigSoundPlayer != null){
id = soundPool.load(contextManager.getApplicationContext(),
resId,
priority);
Log.d(TAG,"loadMusic result"+id);
resourceMap.put(tag,id);
}
return id;
}
public int load(String tag,String path,int priority){
int id = -1;
if (pigSoundPlayer != null){
id = soundPool.load(path,priority);
Log.d(TAG,"loadMusic result"+id);
resourceMap.put(tag,id);
}
return id;
}
public int load(String tag,FileDescriptor fd,long offset,long length,int priority){
int id = -1;
if (pigSoundPlayer != null){
id = soundPool.load(fd,offset,length,priority);
resourceMap.put(tag,id);
}
Log.d(TAG,"loadMusic result"+id);
return id;
}
public int load(String tag, AssetFileDescriptor afd,int priority){
int id = -1;
if (pigSoundPlayer != null){
id = soundPool.load(afd,priority);
resourceMap.put(tag,id);
}
Log.d(TAG,"loadMusic result"+id);
return id;
}
}
3. 接下来我们把Loader的方法和播放的方法加进去
其中play()
方法左右声道声音大小一般都会是1.0f,所以给了个简单的方法,只需要给id和loop或者只给id、loop和rate即可
public class PigSoundPlayer{
//...前文代码省略
//预存(预加载)者
private static Loader loader;
//获取对应处理者
public static Loader getLoader(Context context){
if(contextManager == null){
contextManager = new ContextManager(context);
}
if (loader == null){
loader = new Loader();
}
return loader;
}
public static int play(int soundId,int loop){
return play(soundId,1.0f,1.0f,1,loop,1.0f);
}
public static int play(int soundId,int loop,float rate){
return play(soundId,1.0f,1.0f,1,loop,rate);
}
public static int play(int soundId,float leftVolume,float rightVolume,int priority,int loop,float rate){
if (pigSoundPlayer == null){
return 0;
}
return soundPool.play(soundId, leftVolume, rightVolume, priority, loop, rate);
}
public static int play(String tag,int loop){
return play(tag,1.0f,1.0f,1,loop,1.0f);
}
public static int play(String tag,int loop,float rate){
return play(tag,1.0f,1.0f,1,loop,rate);
}
public static int play(String tag,float leftVolumn,float rightVolume,int priority,int loop,float rate){
if (pigSoundPlayer == null){
return 0;
}
int resid = 0;
if(resourceMap.get(tag)!=null){
resid = resourceMap.get(tag);
return soundPool.play(resid,leftVolumn,rightVolume,priority,loop,rate);
}else{
return 0;
}
}
public static class Loader{
private Loader(){}
public int load(String tag,int resId,int priority){
int id = -1;
if (pigSoundPlayer != null){
id = soundPool.load(contextManager.getApplicationContext(),
resId,
priority);
Log.d(TAG,"loadMusic result"+id);
resourceMap.put(tag,id);
}
return id;
}
public int load(String tag,String path,int priority){
int id = -1;
if (pigSoundPlayer != null){
id = soundPool.load(path,priority);
Log.d(TAG,"loadMusic result"+id);
resourceMap.put(tag,id);
}
return id;
}
public int load(String tag,FileDescriptor fd,long offset,long length,int priority){
int id = -1;
if (pigSoundPlayer != null){
id = soundPool.load(fd,offset,length,priority);
resourceMap.put(tag,id);
}
Log.d(TAG,"loadMusic result"+id);
return id;
}
public int load(String tag, AssetFileDescriptor afd,int priority){
int id = -1;
if (pigSoundPlayer != null){
id = soundPool.load(afd,priority);
resourceMap.put(tag,id);
}
Log.d(TAG,"loadMusic result"+id);
return id;
}
}
}
其中的ContextManager
比较简单,这里直接贴代码
public class ContextManager {
private Context context;
private PigSoundPlayer pigSoundPlayer;
public ContextManager(Context context){
this.context = context.getApplicationContext();
}
public Context getApplicationContext(){
return context;
}
public void setContext(Context context){
this.context = context;
}
}
到此为止,我们的PigSoundPlayer就可以使用了
首先在Application的onCreate中,或者在播放或者预存之前,调用PigSoundPlayer.initSoundPlayer()
方法进行初始化
往后,如果需要加载资源,那么就调用PigSoundPlayer.getLoader(getApplicationContext()).load(file.getAbsolutePath,1)
如果需要播放,那么就调用
int stremId = PigSoundPlayer.play(id,0);
4. 我们再考虑一下暂停和继续播放问题
我们看一下SoundPool中的暂停和继续方法
首先是暂停的方法,用于暂停streamID
对应的playback stream
,其中,streamID
是play()
方法的返回值,每个play()
方法返回的streamID
对应的就是当前播放音频的stream
/**
* Pause a playback stream.
*
* Pause the stream specified by the streamID. This is the
* value returned by the play() function. If the stream is
* playing, it will be paused. If the stream is not playing
* (e.g. is stopped or was previously paused), calling this
* function will have no effect.
*
* @param streamID a streamID returned by the play() function
*/
public native final void pause(int streamID);
继续播放的方法,传入的参数streamID
同样是play()
方法的返回值,如果这个streamID
的stream之前被暂停了,那么将会继续播放,如果没有被暂停,那么调用这个方法没有效果。
/**
* Resume a playback stream.
*
* Resume the stream specified by the streamID. This
* is the value returned by the play() function. If the stream
* is paused, this will resume playback. If the stream was not
* previously paused, calling this function will have no effect.
*
* @param streamID a streamID returned by the play() function
*/
public native final void resume(int streamID);
全部暂停,auto自动的意思顾名思义就是给所有被自动暂停的stream打上flag,当调用autoResume()
的时候,所有被autoPause()
的stream都会继续播放。
/**
* Pause all active streams.
*
* Pause all streams that are currently playing. This function
* iterates through all the active streams and pauses any that
* are playing. It also sets a flag so that any streams that
* are playing can be resumed by calling autoResume().
*/
public native final void autoPause();
这里我没有试过,我估计是只会继续播放那些被 autoPause() 暂停的stream
/**
* Resume all previously active streams.
*
* Automatically resumes all streams that were paused in previous
* calls to autoPause().
*/
public native final void autoResume();
停止,关闭资源
/**
* Stop a playback stream.
*
* Stop the stream specified by the streamID. This
* is the value returned by the play() function. If the stream
* is playing, it will be stopped. It also releases any native
* resources associated with this stream. If the stream is not
* playing, it will have no effect.
*
* @param streamID a streamID returned by the play() function
*/
public native final void stop(int streamID);
当然还有一些setVolume
setPriority
setLoop
setRate
等方法,不一一贴出
这里注意一下,rate范围是 0f 到 2.0f
我们把这些方法封装到我们的PigSoundPlayer
中
//...
//暂停
public static void pause(int streamID){
soundPool.pause(streamID);
}
//继续播放
public static void resume(int streamId){
soundPool.resume(streamId);
}
//全部暂停播放
public static void autoPause(){
soundPool.autoPause();
}
//全部继续播放
public static void autoResume(){
soundPool.autoResume();
}
//停止
public static void stop(int streamID){
soundPool.stop(streamID);
}
//设置音量
public static void setVolume(int streamID, int leftVolume, int rightVolume){
soundPool.setVolume(streamID,leftVolume,rightVolume);
}
//设置Priority
public static void setPriority(int streamID, int priority){
soundPool.setPriority(streamID,priority);
}
//设置Rate
public static void setRate(int streamID, float rate){
soundPool.setRate(streamID,rate);
}
//设置loop
public static void setLoop(int streamID,int loop){
soundPool.setLoop(streamID,loop);
}
//...