1. 介绍
我们如果想在应用中进行播放一些音效,例如提示音,提示短语等简短的音频文件。可以使用 SoundPool
这个工具进行快捷播放。
它利用 MediaCodec
服务为音频解码为一个原始16位 PCM
流。这个特性使得应用程序可以进行流压缩,而无须忍受在播放音频时解压所带来的CPU负载和时延。SoundPool
会将音频解码后进行预编码到内存中。然后再根据需求进行播放。
汇总特性如下:
- 单个文件不能大于1M。如果解码的音频超过1兆字节的存储空间,则该音频将被截断。
- 可以一次性播放多个音频。通过设置
maxStreams
设置单个SoundPool
中可以播放的最大音频数量。如果播放数量超过最大数量,SoundPool
会根据优先级自动关闭先前播放的音频。(PS:默认限制数量maxStreams=1
,限制最大数量有助于限制CPU负载,降低音频混合影响视觉效果或UI性能的可能性。) - 可设置循环播放,也可以指定播放次数。
- 可以设置播放速度,最大为2倍数,最小为0.5倍数。进行音频的快速播放或者慢速播放。
- 可以设置优先级(
priority
)。优先级从低到高,即数字越高,优先级越高。当调用play()
会导致活动流的数量超过创建SoundPool
时maxStreams
参数所确定的值时,将使用优先级。在这种情况下,流分配器将停止优先级最低的流。如果有多个流具有相同的低优先级,它将选择最旧的流停止。在新流的优先级低于所有活动流的情况下,新声音将不会播放,play()
函数将返回streamID
为零。(ps:该功能暂时还没有效果,后续版本会支持优先级配置) - 不用关心各种音频流的生命周期,调用各种
streamID
的相关方法不会因为找不到播放流而出现各种错误和异常。
以上信息来源于 Android-32 androidmediaSoundPool.java
源码中的注释
总而言之就是:
使用SoundPool
可以播放多种音频,甚至可以混音播放。但是不能播放比较大的音频文件。长时间的音频建议使用 MediaPlayer
。
2. 使用
老版本SoundPool
是可以直接new SoundPool()
进行创建的,但是自从Android-API 21 之后就被废弃了。改为SoundPool.Builder
进行创建SoundPool
对象。
PS:
SoundPool
对象不是一个单例对象,所以,我们其实是可以创建多个SoundPool
对象的,但是不建议大量创建,影响性能。
主要步骤为:
- 创建
SoundPool
对象。 - 调用
soundPool.load()
加载音频文件。加载成功后返回soundId,如果是0就代表加载失败了。 - 监听
setOnLoadCompleteListener
方法,得到音频文件是否加载成功。 - 调用
soundPool.play()
进行音频播放。使用soundId进行播放。播放成功后会返回streamId,我们之后可以通过该streamId进行暂停,恢复,停止,修改循环次数,修改优先级,修改声音等。 - 界面关闭时,调用
soundPool.release()
释放资源。会释放所有加载的音频文件。
2.1 创建 SoundPool
代码语言:javascript复制SoundPool.Builder spb = new SoundPool.Builder();
SoundPool soundPool = spb.build(); //创建SoundPool对象
上述方法就创建了一个soundPool
播放对象了。默认最大 MaxStreams=1
,默认音效为:AudioAttributes.USAGE_MEDIA
。
我们如果想设置最大streams数量,需要通过Builder对象进行设置:
代码语言:javascript复制SoundPool.Builder spb = new SoundPool.Builder();
spb.setMaxStreams(15); //但是不建议将这个值设置的较大,较大会占用比较大的内存空间的。
其次就是配置AudioAttributes
(音频属性了)。
SoundPool.Builder spb = new SoundPool.Builder();
AudioAttributes mAudioAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME).build();
spb.setAudioAttributes(attrBuilder.build());
下面详细介绍音频属性的相关配置项。
2.1.1 音频属性-AudioAttributes
音频属性类中,有很多配置项。这里只是简单介绍部分,更详细的建议大家可以通过源码进行查询了解。
声音用途-usage
那么默认情况下配置的setUsage(AudioAttributes.USAGE_MEDIA)
是什么呢?
是用来描述音频的用途为媒体文件使用的,其他可选配置如下:
代码语言:javascript复制AudioAttributes.USAGE_UNKNOWN://用法未知时要使用的用法值。也就是这个音频预期用途不属于以下定义的
AudioAttributes.USAGE_MEDIA: //当用途为媒体(如音乐或电影配乐)时要使用的用途值。
AudioAttributes.USAGE_VOICE_COMMUNICATION: //当使用是语音通信(如电话或VoIP)时要使用的使用值。
AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING://在呼叫信号中使用时使用的用法值,例如“忙碌”的嘟嘟声或DTMF音调。
AudioAttributes.USAGE_ALARM: //当使用是警报(例如唤醒警报)时要使用的使用值。
AudioAttributes.USAGE_NOTIFICATION://使用情况为通知时要使用的使用情况值。
AudioAttributes.USAGE_NOTIFICATION_RINGTONE://当使用是电话铃声时要使用的使用值。
AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST: //当使用是请求进入/结束通信(如VoIP通信或视频会议)时要使用的使用值。
AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT://当使用是“即时”通信(如聊天或短信)的通知时使用的使用值。
AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED://当用途是通知非即时类型的通信(如电子邮件)时要使用的用途值。
AudioAttributes.USAGE_NOTIFICATION_EVENT: //当使用是为了吸引用户的注意力时要使用的使用值,例如提醒或电池电量不足警告。
AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY: //用于辅助功能时要使用的用法值,例如用于屏幕阅读器。
AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE: //当用途是驾驶或导航方向时要使用的用途值。
AudioAttributes.USAGE_ASSISTANCE_SONIFICATION: //当使用是声音处理时要使用的使用值,例如用户界面声音。
AudioAttributes.USAGE_GAME: //用于游戏音频时要使用的用法值。
AudioAttributes.USAGE_VIRTUAL_SOURCE: //用于虚拟资源生产时的用途值。
AudioAttributes.USAGE_ASSISTANT://用于对用户查询、音频指令或帮助话语的音频响应的用法值。
示例代码如:
代码语言:javascript复制AudioAttributes.Builder attrBuilder = new AudioAttributes.Builder();
attrBuilder.setUsage(AudioAttributes.USAGE_GAME);
当我们不配置setUsage()
的时候,音频属性默认的用途描述为:AudioAttributes.USAGE_INVALID
该值为无效值,仅用于未初始化的用法值。
所以,建议大家还是根据自己的音频文件的使用用途,进行配置相关的用途值。
PS1:这个
Usage
用途值是用来告诉系统,我们这个音频文件是属于什么类型的。 如果关注过手机音量设置,就会知道我们可以针对通知,闹钟,音乐,视频游戏,通话等不同场景设置相关音量。 这个用途决定了我们的音频文件会被系统哪个音量设置进行控制。 PS2:这也就是为啥有些app中的音效在手机媒体音效都禁音了,还在播放。因为它可能将声音的用途标注为了通知铃声等。
首次启动SoundPool
进行播放音频时,没有配置Usage
参数值,这个时候程序触发了系统提示音的播放。
那么我们的SoundPool
调用load()
就会得到返回值为0。音频加载失败。
AudioAttributes
类除了上面的声音用途(Usage)以外。还有一些其他方法:
setContentType(int contentType)
:设置描述音频信号的内容类型的属性,例如语音或音乐。 可选参数如下:AudioAttributes.CONTENT_TYPE_UNKNOWN
: 默认值,当内容类型未知或不是定义的内容类型时要使用的内容类型值。AudioAttributes.CONTENT_TYPE_MOVIE
:当内容类型为配乐(通常伴随电影或电视节目)时要使用的内容类型值AudioAttributes.CONTENT_TYPE_MUSIC
:内容类型为音乐时要使用的内容类型值。AudioAttributes.CONTENT_TYPE_SONIFICATION
:当内容类型是用于伴随用户动作的声音时使用的内容类型值,例如表示按键的嘟嘟声或声音效果,或事件,例如游戏中收到的奖金的声音类型。这些声音大多是合成的或简短的 Foley 音。AudioAttributes.CONTENT_TYPE_SPEECH
:当内容类型为语音时要使用的内容类型值。
setFlage(int flags)
:设置标志的组合。设置的参数将会与已有值进行位运算。参数有两个选项:AudioAttributes.FLAG_AUDIBILITY_ENFORCED
:定义一种行为的标志,其中声音的可听性将由系统确保。AudioAttributes.FLAG_HW_AV_SYNC
:请求使用支持硬件A/V同步的输出流的标志。
setAllowedCapturePolicy(int capturePolicy)
:指定其他应用程序或系统是否可以捕获音频。这个配置的结果会组合在Flags参数中的。AudioAttributes.ALLOW_CAPTURE_BY_ALL
:默认值,指示音频可以被任何应用程序捕获。这个捕获会受到Usage参数的影响,因为涉及敏感操作。从Android API 29 开始只能捕获USAGE_UNKNOWN
,USAGE_MEDIA
和USAGE_GAME
。AudioAttributes.ALLOW_CAPTURE_BY_SYSTEM
:指示音频只能由系统应用程序捕获。系统应用程序可以捕获多种用途,如辅助功能、实时字幕、用户指南等等但要遵守以下限制:1.音频不能离开设备,2.音频不能传递给第三方应用程序,3.音频不能以高于16kHz 16位单声道的质量。AudioAttributes.ALLOW_CAPTURE_BY_NONE
:指示任何应用程序都不会录制音频,即使是系统应用程序也是如此。鼓励使用ALLOW_CAPTURE_BY_SYSTEM
而不是此值,因为系统应用程序为用户提供了重要而有用的功能(如实时字幕和可访问性)。
setHapticChannelsMuted(boolean muted)
: 指定在播放音频触觉耦合数据时是否应静音触觉。默认情况下,触觉通道处于禁用状态。简单理解就是,当在播放音频时。按键声音,触摸反馈等会设置为禁止状态。- true:默认值,设置触觉反馈静音。
- false:设置允许触摸反馈声音。
setIsContentSpatialized(boolean isSpatialized)
:指定是否已经对内容进行了空间化处理。如果有,则将其设置为true将防止诸如双重处理之类的问题。- true:已经对音频内容进行了空间化处理,系统不需要再进行双重处理了。
- false:默认值,没有对音频进行空间化处理。
setSpatializationBehavior(int sb)
:设置使用空间化的行为。主要有两个可选参数:(PS:没有太能理解这个方法的意义,应该是需要更多的音频相关知识才能弄明白吧。)AudioAttributes.SPATIALIZATION_BEHAVIOR_NEVER
:指示与这些属性相关联的音频内容的常量永远不应该被虚拟化。AudioAttributes.SPATIALIZATION_BEHAVIOR_AUTO
:默认值,指示与这些属性相关联的音频内容将遵循默认的平台行为,关于哪些内容将被空间化或不被空间化。
除了上面的方法。我们经常也看到一些分享的代码中,并没有使用上面的方法,而是只使用setLegacyStreamType()
方法:
AudioAttributes.Builder attrBuilder = new AudioAttributes.Builder();
attrBuilder.setLegacyStreamType(AudioManager.STREAM_MUSIC);
这个方法主要用来设置从传统流类型推断的属性。AudioAttributes
将会通过从遗留流类型派生的信息初始化某些属性。
简单理解就是,我们配置的Usage
,ContentType
,Flage
等等信息数据。AudioAttributes
会从系统历史痕迹中找到某个音频流的属性,进行复用配置。
官方注释中,建议我们少使用该方法,而应该通过
setUsage
,setContentType
等方法明确设置音频的用法和内容类型等信息。
由于会覆盖我们配置的Usage
,ContentType
,Flage
,HapticChannelsMuted
等方法值。
所以如果使用setLegacyStreamType
就不要使用上面的配置音频相关信息的方法。因为setLegacyStreamType
优先级高,会覆盖掉我们配置的信息。该方法的建议传参有6个值:
但是首先会先从历史痕迹中获取信息,获取不到的才会按照下面的配置项进行默认初始化。
AudioManager.STREAM_VOICE_CALL
:将会ContentType设置为 CONTENT_TYPE_SPEECH,Usage设置为USAGE_VOICE_COMMUNICATION。AudioManager.STREAM_SYSTEM
:将会ContentType设置为 CONTENT_TYPE_SONIFICATION,Usage设置为USAGE_ASSISTANCE_SONIFICATION。AudioManager.STREAM_RING
:将会ContentType设置为 CONTENT_TYPE_SONIFICATION,Usage设置为USAGE_NOTIFICATION_RINGTONE。AudioManager.STREAM_MUSIC
:将会ContentType设置为 CONTENT_TYPE_MUSIC,Usage设置为USAGE_NOTIFICATION_RINGTONE。AudioManager.STREAM_ALARM
:将会ContentType设置为 CONTENT_TYPE_SONIFICATION,Usage设置为USAGE_NOTIFICATION_RINGTONE。AudioManager.STREAM_NOTIFICATION
:将会ContentType设置为 CONTENT_TYPE_SONIFICATION,Usage设置为USAGE_NOTIFICATION_RINGTONE。
除了上面六个传参外,还可以传一下其他的。这里就不详细说明了。
音效的相关配置到这里就差不多了。我们继续接着处理SoundPool
播放。
2.2 加载音频文件
当我们初始化基本的音频播放器信息之后。我们就可以进行加载音频文件了。
SoundPool
通过load()
方法进行加载文件。可以从assets
,raw
,本地磁盘等进行加载音频。
下面介绍这几种加载方式。
例如,从res
资源目录下raw
文件中加载音频:
soundPool.load(this, R.raw.drill,1);
例如,从assets
目录下加载音频文件:从assets
目录下的sound文件夹中加载名为zinyan.mp3的音频文件。
AssetFileDescriptor descriptor = null;
try {
descriptor = am.openFd("sound/zinyan.mp3");
} catch (IOException e) {
e.printStackTrace();
}
if(descriptor!=null){
soundPool.load(descriptor, 1);
}
例如,从本地磁盘中加载音频文件:
代码语言:javascript复制soundPool.load("本地文件路径", 1);
还可以从FileDescriptor
中加载音频文件进行播放。传offset=0,length=文件大小,protity=1就可以了。
传值中的protity
目前没有效果。为了将来的兼容性,请使用值1。这个值就是所谓的优先级。
PS:常见应用是将部分音频存储在assets目录或者raw目录下。而如果是有比较多音效,那需要进行在线下载后调用FileDescripor进行加载。
当我们使用load()
进行加载音频时,如果音频文件正确那么就会返回一个id。该值为sound Id
。
如果是错误会返回0。代表我们的音频文件并没有被转为PCM流。
在这里我们需要注意一下,SoundID
只是以下两个方法才会使用到。
soundPool.play(soundId,1,1,1,0,1f)
soundPool.stop(soundId);
PS:
soundId
和streamID
并不是同一个值,虽然我们打印输出的时候可能都显示的一样的数。但是并不能代表两个是一致的。
如果你确保该音频文件是一个比较高频使用的音频,那么可以在初始化的时候批量调用load()
方法进行预加载。
之后在需要播放的地方,直接调用soundPool.play
传递该soundId
就可以了。
在实际使用中,提取音频文件到内存。然后可以进行play播放,中间的耗时是非常短的。但是,我们任然不能直接就执行play播放,因为时间再短它也是有耗时的。如果没有加载完成就播放,是没有声音的
2.3 监听加载状态
当我们使用load()
方法进行加载之后,只是将音频文件提取存储在内存中了。这个提取和存储过程是在异步线程中进行操作的。所以并不会影响到我们UI线程的显示。
示例如下:
代码语言:javascript复制//加载完毕,执行音频播放
soundPool.setOnLoadCompleteListener((soundPool, sampleId, status) -> {
Log.e("onLoadComplete", "音频加载状态(0表示加载成功):" status);
int streamID = soundPool.play(soundId, 1, 1, 1, 0, 1f);
});
因为我的音频文件需要动态切换,而且量比较少。所以直接在加载完毕的回调中。
执行了play
播放。
如果是相对固定,并且加载比较多的情况下。建议通过HashMap
进行存储streamId
和soundId
其中 sampleId
就是声音样本ID。也就是load
方法中返回的soundId
。
2.4 播放音频
当我们调用soundPool.play()
方法的时候,该方法调用成功会返回streamId
,如果调用失败就会返回0。
而该方法的完整传值为:
代码语言:javascript复制soundPool.play(int soundID, float leftVolume, float rightVolume,
int priority, int loop, float rate)
soundID
: load()
函数返回的soundID
值,告诉soudPool
要播放哪个音频。
leftVolume
:左侧音量值(范围0.0~1.0)。左声道声音值。
rightVolume
:右侧音量值(范围0.0~1.0)。右声道声音值。
priority
:音频流播放优先级(0=最低优先级,通常默认让设置为1)。
loop
:循环模式(0=无循环,-1=永远循环,其他表示数字表示当前数字对应的循环次数 默认播放的一次。例如循环2次,那么实际播放3次)。
rate
:播放速率(1.0=正常播放,范围为0.5~2.0),也就是0.5倍慢放,1正常,2倍快放。
这些配置,在初始化播放的时候就需要配置上。
我们如果播放成功后想修改声道,优先级(暂时意义没有多大),循环模式,播放速率等。调用相关方法修改即可:
代码语言:javascript复制int streamId = soundPool.play(soundId, 1, 1, 1, 0, 1f);
soundPool.setLoop(streamId,1); //循环一次
soundPool.setVolume(streamId,1,1);
soundPool.setPriority(streamId,1);
soundPool.setRate(streamId,1f);
要注意了,这些修改方法的调用前提是已经执行play
方法得到streamID
之后才有意义。
否则是没有意义和作用的。因为这些修改方法中streamID
传错了也不会触发崩溃等错误的。
相较于
MediaPlayer
。SoundPool因为针对的都是一些快速简单的音效。 所以是没有音频播放结束的回调方法的。我们如果自己想知道音频播放完毕,可以自己写一个时间线程,线程结束后就当音频已经播放完毕了吧。
虽然没有音频结束的监听。但是我们可以针对音频做停止,暂停和恢复等操作。
2.5 暂停,恢复,停止
当我们配置loop
循环模式为-1 无限循环时。我们需要主动调用stop
停止方法才能中断音频的播放。
soundPool.stop(streamId);//停止
soundPool.pause(streamId);//暂停
soundPool.resume(streamId);//恢复
当我们调用stop
停止之后是不能通过resume
进行恢复的。
要想恢复,只能是重新调用play
方法进行播放。
以上是单个音频流的操作,SoundPool
还提供了批量操作的方法:
soundPool.autoPause(); //批量暂停
soundPool.autoResume(); //批量恢复
2.6 释放资源
在一开始就介绍了SoundPool
会将音频文件加载到内存中。
我们操作比较多的音频后,要注意资源的释放。
否则会造成比较大的内存占用。
请注意:当我们调用音频的
stop()
方法时,只是将音频流给回收了,也就是streamId
失效了。 但是soundId
还是生效状态,也就是说load()
方法加载到内存中的资源是并没有被释放的。
释放资源有两种方法,释放某个音频:
代码语言:javascript复制 soundPool.unload(soundId);//移除指定的加载的的音频文件
如果该soundId
指向的音频文件不存在,也不会造成错误的。
上述的方法是移除某一个音频文件的加载,其他加载的音频文件是不会受到影响的。
释放全部音频:
代码语言:javascript复制soundPool.release();
soundPool = null;
当我们,使用release
方法进行操作时,会将load
加载的全部资源进行释放,也会释放SoundPool
对象使用的所有内存和本机资源。简单理解就是soundPool
对象和null没有什么区别了
后面该对象就不能再被使用了。要想使用就需要重新new
一个新对象,并赋值音频属性,加载音频文件等操作。
3. 小结
这里只是介绍了我们如何正确使用SoundPool
以及相关api。如果你看完了整个内容,我相信你在使用SoundPool
进行播放音频时,就不会出现无法播放,播放失败等情况了。
如果觉得本篇内容对你有一点点帮助,希望能够给我点个赞鼓励一下,谢谢。