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 android\media\SoundPool.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
上述方法就创建了一个soundPool播放对象了。默认最大 MaxStreams=1,默认音效为:AudioAttributes.USAGE_MEDIA。
我们如果想设置最大streams数量,需要通过Builder对象进行设置:
其次就是配置AudioAttributes(音频属性了)。
下面详细介绍音频属性的相关配置项。
2.1.1 音频属性-AudioAttributes
音频属性类中,有很多配置项。这里只是简单介绍部分,更详细的建议大家可以通过源码进行查询了解。
声音用途-usage
那么默认情况下配置的setUsage(AudioAttributes.USAGE_MEDIA)是什么呢?
是用来描述音频的用途为媒体文件使用的,其他可选配置如下:
示例代码如:
当我们不配置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将会通过从遗留流类型派生的信息初始化某些属性。
简单理解就是,我们配置的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文件中加载音频:
例如,从assets目录下加载音频文件:从assets目录下的sound文件夹中加载名为zinyan.mp3的音频文件。
例如,从本地磁盘中加载音频文件:
还可以从FileDescriptor中加载音频文件进行播放。传offset=0,length=文件大小,protity=1就可以了。
传值中的protity 目前没有效果。为了将来的兼容性,请使用值1。这个值就是所谓的优先级。
PS:常见应用是将部分音频存储在assets目录或者raw目录下。而如果是有比较多音效,那需要进行在线下载后调用FileDescripor进行加载。
当我们使用load()进行加载音频时,如果音频文件正确那么就会返回一个id。该值为sound Id。
如果是错误会返回0。代表我们的音频文件并没有被转为PCM流。
在这里我们需要注意一下,SoundID只是以下两个方法才会使用到。
PS:soundId 和streamID并不是同一个值,虽然我们打印输出的时候可能都显示的一样的数。但是并不能代表两个是一致的。
如果你确保该音频文件是一个比较高频使用的音频,那么可以在初始化的时候批量调用load()方法进行预加载。
之后在需要播放的地方,直接调用soundPool.play 传递该soundId就可以了。
在实际使用中,提取音频文件到内存。然后可以进行play播放,中间的耗时是非常短的。但是,我们任然不能直接就执行play播放,因为时间再短它也是有耗时的。如果没有加载完成就播放,是没有声音的
2.3 监听加载状态
当我们使用load()方法进行加载之后,只是将音频文件提取存储在内存中了。这个提取和存储过程是在异步线程中进行操作的。所以并不会影响到我们UI线程的显示。
示例如下:
因为我的音频文件需要动态切换,而且量比较少。所以直接在加载完毕的回调中。
执行了play播放。
如果是相对固定,并且加载比较多的情况下。建议通过HashMap进行存储streamId和soundId
其中 sampleId就是声音样本ID。也就是load方法中返回的soundId。
2.4 播放音频
当我们调用soundPool.play()方法的时候,该方法调用成功会返回streamId,如果调用失败就会返回0。
而该方法的完整传值为:
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倍快放。
这些配置,在初始化播放的时候就需要配置上。
我们如果播放成功后想修改声道,优先级(暂时意义没有多大),循环模式,播放速率等。调用相关方法修改即可:
要注意了,这些修改方法的调用前提是已经执行play方法得到streamID之后才有意义。
否则是没有意义和作用的。因为这些修改方法中streamID传错了也不会触发崩溃等错误的。
相较于MediaPlayer。SoundPool因为针对的都是一些快速简单的音效。
所以是没有音频播放结束的回调方法的。我们如果自己想知道音频播放完毕,可以自己写一个时间线程,线程结束后就当音频已经播放完毕了吧。
虽然没有音频结束的监听。但是我们可以针对音频做停止,暂停和恢复等操作。
2.5 暂停,恢复,停止
当我们配置loop循环模式为-1 无限循环时。我们需要主动调用stop停止方法才能中断音频的播放。
当我们调用stop停止之后是不能通过resume进行恢复的。
要想恢复,只能是重新调用play方法进行播放。
以上是单个音频流的操作,SoundPool还提供了批量操作的方法:
2.6 释放资源
在一开始就介绍了SoundPool会将音频文件加载到内存中。
我们操作比较多的音频后,要注意资源的释放。
否则会造成比较大的内存占用。
请注意:当我们调用音频的stop()方法时,只是将音频流给回收了,也就是streamId失效了。
但是soundId还是生效状态,也就是说load()方法加载到内存中的资源是并没有被释放的。
释放资源有两种方法,释放某个音频:
如果该soundId指向的音频文件不存在,也不会造成错误的。
上述的方法是移除某一个音频文件的加载,其他加载的音频文件是不会受到影响的。
释放全部音频:
当我们,使用release方法进行操作时,会将load加载的全部资源进行释放,也会释放SoundPool对象使用的所有内存和本机资源。简单理解就是soundPool对象和null没有什么区别了
后面该对象就不能再被使用了。要想使用就需要重新new一个新对象,并赋值音频属性,加载音频文件等操作。
3. 小结
这里只是介绍了我们如何正确使用SoundPool以及相关api。如果你看完了整个内容,我相信你在使用SoundPool进行播放音频时,就不会出现无法播放,播放失败等情况了。
如果觉得本篇内容对你有一点点帮助,希望能够给我点个赞鼓励一下,谢谢。