侧边栏壁纸
  • 累计撰写 416 篇文章
  • 累计创建 65 个标签
  • 累计收到 150 条评论

目 录CONTENT

文章目录

Android SoundPool 音效播放库

Z同学
2023-03-19 / 0 评论 / 1 点赞 / 634 阅读 / 5,574 字
温馨提示:
本文最后更新于 2023-03-21,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

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

SoundPool.Builder spb = new SoundPool.Builder();
SoundPool soundPool = spb.build();      //创建SoundPool对象

上述方法就创建了一个soundPool播放对象了。默认最大 MaxStreams=1,默认音效为:AudioAttributes.USAGE_MEDIA

我们如果想设置最大streams数量,需要通过Builder对象进行设置:

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)是什么呢?

是用来描述音频的用途为媒体文件使用的,其他可选配置如下:

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://用于对用户查询、音频指令或帮助话语的音频响应的用法值。

示例代码如:

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()方法:

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文件中加载音频:

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);
}

例如,从本地磁盘中加载音频文件:

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:soundIdstreamID并不是同一个值,虽然我们打印输出的时候可能都显示的一样的数。但是并不能代表两个是一致的。

如果你确保该音频文件是一个比较高频使用的音频,那么可以在初始化的时候批量调用load()方法进行预加载。

之后在需要播放的地方,直接调用soundPool.play 传递该soundId就可以了。

在实际使用中,提取音频文件到内存。然后可以进行play播放,中间的耗时是非常短的。但是,我们任然不能直接就执行play播放,因为时间再短它也是有耗时的。如果没有加载完成就播放,是没有声音的

2.3 监听加载状态

当我们使用load()方法进行加载之后,只是将音频文件提取存储在内存中了。这个提取和存储过程是在异步线程中进行操作的。所以并不会影响到我们UI线程的显示。

示例如下:

//加载完毕,执行音频播放
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。

而该方法的完整传值为:

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倍快放。

这些配置,在初始化播放的时候就需要配置上。

我们如果播放成功后想修改声道,优先级(暂时意义没有多大),循环模式,播放速率等。调用相关方法修改即可:

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()方法加载到内存中的资源是并没有被释放的。

释放资源有两种方法,释放某个音频:

 soundPool.unload(soundId);//移除指定的加载的的音频文件

如果该soundId指向的音频文件不存在,也不会造成错误的。

上述的方法是移除某一个音频文件的加载,其他加载的音频文件是不会受到影响的。

释放全部音频:

soundPool.release();
soundPool = null;

当我们,使用release方法进行操作时,会将load加载的全部资源进行释放,也会释放SoundPool对象使用的所有内存和本机资源。简单理解就是soundPool对象和null没有什么区别了

后面该对象就不能再被使用了。要想使用就需要重新new一个新对象,并赋值音频属性,加载音频文件等操作。

3. 小结

这里只是介绍了我们如何正确使用SoundPool以及相关api。如果你看完了整个内容,我相信你在使用SoundPool进行播放音频时,就不会出现无法播放,播放失败等情况了。

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

1

评论区