前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android SoundPool 音效播放库

Android SoundPool 音效播放库

作者头像
zinyan.com
发布2023-07-14 11:10:35
6860
发布2023-07-14 11:10:35
举报
文章被收录于专栏:zinyan

1. 介绍

我们如果想在应用中进行播放一些音效,例如提示音,提示短语等简短的音频文件。可以使用 SoundPool 这个工具进行快捷播放。

它利用 MediaCodec 服务为音频解码为一个原始16位 PCM 流。这个特性使得应用程序可以进行流压缩,而无须忍受在播放音频时解压所带来的CPU负载和时延。SoundPool 会将音频解码后进行预编码到内存中。然后再根据需求进行播放。

汇总特性如下:

  1. 单个文件不能大于1M。如果解码的音频超过1兆字节的存储空间,则该音频将被截断。
  2. 可以一次性播放多个音频。通过设置maxStreams设置单个SoundPool中可以播放的最大音频数量。如果播放数量超过最大数量,SoundPool会根据优先级自动关闭先前播放的音频。(PS:默认限制数量maxStreams=1,限制最大数量有助于限制CPU负载,降低音频混合影响视觉效果或UI性能的可能性。)
  3. 可设置循环播放,也可以指定播放次数。
  4. 可以设置播放速度,最大为2倍数,最小为0.5倍数。进行音频的快速播放或者慢速播放。
  5. 可以设置优先级(priority)。优先级从低到高,即数字越高,优先级越高。当调用play()会导致活动流的数量超过创建SoundPoolmaxStreams参数所确定的值时,将使用优先级。在这种情况下,流分配器将停止优先级最低的流。如果有多个流具有相同的低优先级,它将选择最旧的流停止。在新流的优先级低于所有活动流的情况下,新声音将不会播放,play()函数将返回streamID为零。(ps:该功能暂时还没有效果,后续版本会支持优先级配置)
  6. 不用关心各种音频流的生命周期,调用各种streamID的相关方法不会因为找不到播放流而出现各种错误和异常。

以上信息来源于 Android-32 android\media\SoundPool.java 源码中的注释

总而言之就是:

使用SoundPool 可以播放多种音频,甚至可以混音播放。但是不能播放比较大的音频文件。长时间的音频建议使用 MediaPlayer

2. 使用

老版本SoundPool是可以直接new SoundPool()进行创建的,但是自从Android-API 21 之后就被废弃了。改为SoundPool.Builder进行创建SoundPool对象。

PS:SoundPool对象不是一个单例对象,所以,我们其实是可以创建多个SoundPool对象的,但是不建议大量创建,影响性能。

主要步骤为:

  1. 创建SoundPool对象。
  2. 调用soundPool.load() 加载音频文件。加载成功后返回soundId,如果是0就代表加载失败了。
  3. 监听setOnLoadCompleteListener方法,得到音频文件是否加载成功。
  4. 调用soundPool.play()进行音频播放。使用soundId进行播放。播放成功后会返回streamId,我们之后可以通过该streamId进行暂停,恢复,停止,修改循环次数,修改优先级,修改声音等。
  5. 界面关闭时,调用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(音频属性了)。

代码语言:javascript
复制
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_UNKNOWNUSAGE_MEDIAUSAGE_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()方法:

代码语言:javascript
复制
AudioAttributes.Builder attrBuilder = new AudioAttributes.Builder();
attrBuilder.setLegacyStreamType(AudioManager.STREAM_MUSIC);

这个方法主要用来设置从传统流类型推断的属性。AudioAttributes将会通过从遗留流类型派生的信息初始化某些属性。

简单理解就是,我们配置的UsageContentTypeFlage等等信息数据。AudioAttributes会从系统历史痕迹中找到某个音频流的属性,进行复用配置。

官方注释中,建议我们少使用该方法,而应该通过setUsagesetContentType等方法明确设置音频的用法和内容类型等信息。

由于会覆盖我们配置的UsageContentTypeFlageHapticChannelsMuted等方法值。

所以如果使用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()方法进行加载文件。可以从assetsraw,本地磁盘等进行加载音频。

下面介绍这几种加载方式。

例如,从res资源目录下raw文件中加载音频:

代码语言:javascript
复制
soundPool.load(this, R.raw.drill,1);

例如,从assets目录下加载音频文件:从assets目录下的sound文件夹中加载名为zinyan.mp3的音频文件。

代码语言:javascript
复制
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只是以下两个方法才会使用到。

代码语言:javascript
复制
soundPool.play(soundId,1,1,1,0,1f)
soundPool.stop(soundId);

PS:soundIdstreamID并不是同一个值,虽然我们打印输出的时候可能都显示的一样的数。但是并不能代表两个是一致的。

如果你确保该音频文件是一个比较高频使用的音频,那么可以在初始化的时候批量调用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进行存储streamIdsoundId

其中 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停止方法才能中断音频的播放。

代码语言:javascript
复制
soundPool.stop(streamId);//停止
soundPool.pause(streamId);//暂停
soundPool.resume(streamId);//恢复

当我们调用stop停止之后是不能通过resume进行恢复的。

要想恢复,只能是重新调用play方法进行播放。

以上是单个音频流的操作,SoundPool还提供了批量操作的方法:

代码语言:javascript
复制
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进行播放音频时,就不会出现无法播放,播放失败等情况了。

如果觉得本篇内容对你有一点点帮助,希望能够给我点个赞鼓励一下,谢谢。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-03-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 zinyan 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 介绍
  • 2. 使用
    • 2.1 创建 SoundPool
      • 2.1.1 音频属性-AudioAttributes
    • 2.2 加载音频文件
      • 2.3 监听加载状态
        • 2.4 播放音频
          • 2.5 暂停,恢复,停止
            • 2.6 释放资源
            • 3. 小结
            相关产品与服务
            短信
            腾讯云短信(Short Message Service,SMS)可为广大企业级用户提供稳定可靠,安全合规的短信触达服务。用户可快速接入,调用 API / SDK 或者通过控制台即可发送,支持发送验证码、通知类短信和营销短信。国内验证短信秒级触达,99%到达率;国际/港澳台短信覆盖全球200+国家/地区,全球多服务站点,稳定可靠。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档