目录
📂 前言
AR 眼镜系统版本
系统应用音效
1. 🔱 技术方案
1.1 技术方案概述
1.2 实现方案
1)初始化
2)播放音效
3)释放资源
2. 💠 播放音效
2.1 静音不播放
2.2 获取音效默认音量
3. ⚛️ 单声道空间音效
3.1 实现方案
3.2 封装播放单声道空间音效
4. ✅ 小结
附录1:StreamType 值
附录2:音效播放工具源码
📂 前言
AR 眼镜系统版本
W517 Android9。
系统应用音效
系统应用音效主要包括:通知音效、蓝牙电话音效、点击音效等。
1. 🔱 技术方案
1.1 技术方案概述
系统应用音效都是比较短的,一般采用 Android 推荐的 ogg 格式,直接使用 SoundPool 播放即可。
1.2 实现方案
1)初始化
在适当位置初始化 SoundPool 工具类(比如:在 Activity 的 onCreate 方法中初始化),包括:初始化相应 SoundPool、load 相应音效资源、保存音效资源 ID。
1、初始化相应 SoundPool
private val phoneSP by lazy { SoundPool(20, AudioManager.STREAM_VOICE_CALL, 0) }
private val notifySP by lazy { SoundPool(20, AudioManager.STREAM_NOTIFICATION, 0) }
private val clickSP by lazy { SoundPool(20, AudioManager.STREAM_SYSTEM, 0) }
SoundPool 构造第一个参数为 maxStreams,一般设置为1或10,本文设置为20是有一定风险的(主要是为了规避接通蓝牙电话时,点击蓝牙电话接听的系统音效被蓝牙电话音效通道抢占,导致点击蓝牙电话接听的系统音效无法播放的问题)。
SoundPool 构造第二个参数是 streamType 值,具体值的含义可查看附录1。比如:本文的通知音效使用 STREAM_NOTIFICATION,蓝牙电话使用的 STREAM_VOICE_CALL,点击音效使用的 STREAM_SYSTEM。
2、load 相应音效资源,保存音效资源 ID
/*** 加载音效ID*/
private var hangupId: Int = -1
private var answerId: Int = -1
private var notifyComeId: Int = -1
private var notifyClearId: Int = -1
private var clickId: Int = -1fun init(context: Context) {hangupId = phoneSP.load(context, R.raw.phone_hang_up, 3)answerId = phoneSP.load(context, R.raw.phone_answer, 3)notifyComeId = notifySP.load(context, R.raw.notification_message, 1)notifyClearId = notifySP.load(context, R.raw.notification_clear, 1)clickId = clickSP.load(context, R.raw.click, 1)Log.i(TAG, "init: hangupId = $hangupId,answerId = $answerId,notifyComeId = $notifyComeId,notifyClearId = $notifyClearId,clickId = $clickId")
}
load 方法的第三个参数为声音的优先级,源码注释为:目前不起作用,默认使用1即可。
2)播放音效
在触发音效播放处调用播放音效,包括播放音效通用方法、播放音效特定封装方法。
1、播放音效通用方法
/*** 播放音效*/
fun play(context: Context,soundPool: SoundPool,soundId: Int,resId: Int,leftVol: Float = -1f,rightVol: Float = -1f
) {...// soundId:加载的音频资源的 ID。// leftVolume和rightVolume:左右声道的音量,范围为 0.0(静音)到 1.0(最大音量)。// priority:播放优先级,一般设为 1。// loop:是否循环播放,0 表示不循环,-1 表示无限循环。// rate:播放速率,1.0 表示正常速率,更大的值表示更快的播放速率,0.5 表示慢速播放。soundPool.play(soundId, leftVol, rightVol, 1, 0, 1.0f)...
}
2、播放音效特定封装方法
fun playHangup(context: Context) {Log.i(TAG, "playHangup: resId = ${R.raw.phone_hang_up}")play(context, phoneSP, hangupId, R.raw.phone_hang_up)
}
fun playAnswer(context: Context) {Log.i(TAG, "playAnswer: resId = ${R.raw.phone_answer}")play(context, phoneSP, answerId, R.raw.phone_answer)
}
fun playNotifyCome(context: Context) {Log.i(TAG, "playNotifyCome: resId = ${R.raw.notification_message}")play(context, notifySP, notifyComeId, R.raw.notification_message)
}
fun playNotifyClear(context: Context) {Log.i(TAG, "playNotifyClear: resId = ${R.raw.notification_clear}")play(context, notifySP, notifyClearId, R.raw.notification_clear)
}
3)释放资源
在适当位置释放 SoundPool 对象(比如:在 Activity 的 onDestroy 方法中释放资源)
/*** 释放资源*/
fun release() {phoneSP.release()notifySP.release()
}
2. 💠 播放音效
2.1 静音不播放
判断当前系统对应音效是否静音,如果静音则不播放音效。
/*** 播放音效*/
fun play(context: Context,soundPool: SoundPool,soundId: Int,resId: Int,leftVol: Float = -1f,rightVol: Float = -1f
) {// 1.静音不播放if (isSilent(context)) {Log.i(TAG, "_play: AudioManager RINGER_MODE_SILENT!")return}...
}/*** @return 是否静音*/
private fun isSilent(context: Context) =(context.getSystemService(Context.AUDIO_SERVICE) as AudioManager).ringerMode == AudioManager.RINGER_MODE_SILENT
2.2 获取音效默认音量
设定音效播放前,先获取到系统音效的默认音量大小,在播放时使用系统音效的默认音量值。
/*** 播放音效*/
fun play(context: Context,soundPool: SoundPool,soundId: Int,resId: Int,leftVol: Float = -1f,rightVol: Float = -1f
) {...// 2.获取音效默认音量val volFloat = getVol(context)...
}/*** @return 音效默认音量*/
private fun getVol(context: Context) = 10.0.pow((context.resources.getInteger(com.android.internal.R.integer.config_soundEffectVolumeDb).toFloat() / 20).toDouble()
).toFloat()
3. ⚛️ 单声道空间音效
3.1 实现方案
-
屏蔽原生 View 点击音效:android:soundEffectsEnabled="false"
-
使用 SoundPool 单独设置左右声道音量,假如只播放右声道,则把左声道音量置为0。
3.2 封装播放单声道空间音效
fun playClickLeft(context: Context) {Log.i(TAG, "playClickLeft: resId = ${R.raw.click}")play(context, phoneSP, hangupId, R.raw.click, rightVol = 0f)
}
fun playClickRight(context: Context) {Log.i(TAG, "playClickRight: resId = ${R.raw.click}")play(context, phoneSP, hangupId, R.raw.click, 0f)
}/*** 播放音效*/
fun play(context: Context,soundPool: SoundPool,soundId: Int,resId: Int,leftVol: Float = -1f,rightVol: Float = -1f
) {...// 2.获取音效默认音量val volFloat = getVol(context)val tempLeftVol = if (leftVol != -1f) leftVol else volFloatval tempRightVol = if (rightVol != -1f) rightVol else volFloatsoundPool.play(soundId, tempLeftVol, tempRightVol, 1, 0, 1.0f)...
}
4. ✅ 小结
对于系统应用音效,本文只是一个基础实现方案,更多业务细节请参考产品逻辑去实现。
另外,由于本人能力有限,如有错误,敬请批评指正,谢谢。
附录1:StreamType 值
/** Used to identify the default audio stream volume */
public static final int STREAM_DEFAULT = -1;
/** Used to identify the volume of audio streams for phone calls */
public static final int STREAM_VOICE_CALL = 0;
/** Used to identify the volume of audio streams for system sounds */
public static final int STREAM_SYSTEM = 1;
/** Used to identify the volume of audio streams for the phone ring and message alerts */
public static final int STREAM_RING = 2;
/** Used to identify the volume of audio streams for music playback */
public static final int STREAM_MUSIC = 3;
/** Used to identify the volume of audio streams for alarms */
public static final int STREAM_ALARM = 4;
/** Used to identify the volume of audio streams for notifications */
public static final int STREAM_NOTIFICATION = 5;
/** Used to identify the volume of audio streams for phone calls when connected on bluetooth */
public static final int STREAM_BLUETOOTH_SCO = 6;
/** Used to identify the volume of audio streams for enforced system sounds in certain* countries (e.g camera in Japan) */
public static final int STREAM_SYSTEM_ENFORCED = 7;
/** Used to identify the volume of audio streams for DTMF tones */
public static final int STREAM_DTMF = 8;
/** Used to identify the volume of audio streams exclusively transmitted through the* speaker (TTS) of the device */
public static final int STREAM_TTS = 9;
/** Used to identify the volume of audio streams for accessibility prompts */
public static final int STREAM_ACCESSIBILITY = 10;
附录2:音效播放工具源码
/*** Description: 音效播放工具* CreateDate: 2024/6/26 15:47* Author: agg*/
object SoundPoolTools {private val TAG = SoundPoolTools::class.java.simpleNameprivate val phoneSP by lazy { SoundPool(20, AudioManager.STREAM_VOICE_CALL, 0) }private val notifySP by lazy { SoundPool(20, AudioManager.STREAM_NOTIFICATION, 0) }private val clickSP by lazy { SoundPool(20, AudioManager.STREAM_SYSTEM, 0) }/*** 加载音效ID*/private var hangupId: Int = -1private var answerId: Int = -1private var notifyComeId: Int = -1private var notifyClearId: Int = -1private var clickId: Int = -1fun init(context: Context) {hangupId = phoneSP.load(context, R.raw.phone_hang_up, 3)answerId = phoneSP.load(context, R.raw.phone_answer, 3)notifyComeId = notifySP.load(context, R.raw.notification_message, 1)notifyClearId = notifySP.load(context, R.raw.notification_clear, 1)clickId = clickSP.load(context, R.raw.click, 1)Log.i(TAG, "init: hangupId = $hangupId,answerId = $answerId,notifyComeId = $notifyComeId,notifyClearId = $notifyClearId,clickId = $clickId")}fun playClickLeft(context: Context) {Log.i(TAG, "playClickLeft: resId = ${R.raw.click}")play(context, phoneSP, hangupId, R.raw.click, rightVol = 0f)}fun playClickRight(context: Context) {Log.i(TAG, "playClickRight: resId = ${R.raw.click}")play(context, phoneSP, hangupId, R.raw.click, 0f)}fun playHangup(context: Context) {Log.i(TAG, "playHangup: resId = ${R.raw.phone_hang_up}")play(context, phoneSP, hangupId, R.raw.phone_hang_up)}fun playAnswer(context: Context) {Log.i(TAG, "playAnswer: resId = ${R.raw.phone_answer}")play(context, phoneSP, answerId, R.raw.phone_answer)}fun playNotifyCome(context: Context) {Log.i(TAG, "playNotifyCome: resId = ${R.raw.notification_message}")play(context, notifySP, notifyComeId, R.raw.notification_message)}fun playNotifyClear(context: Context) {Log.i(TAG, "playNotifyClear: resId = ${R.raw.notification_clear}")play(context, notifySP, notifyClearId, R.raw.notification_clear)}/*** 播放音效*/fun play(context: Context,soundPool: SoundPool,soundId: Int,resId: Int,leftVol: Float = -1f,rightVol: Float = -1f) {// 1.静音不播放if (isSilent(context)) {Log.i(TAG, "_play: AudioManager RINGER_MODE_SILENT!")return}// 2.获取音效默认音量val volFloat = getVol(context)val tempLeftVol = if (leftVol != -1f) leftVol else volFloatval tempRightVol = if (rightVol != -1f) rightVol else volFloat// 3.播放音效// soundId:加载的音频资源的 ID。// leftVolume和rightVolume:左右声道的音量,范围为 0.0(静音)到 1.0(最大音量)。// priority:播放优先级,一般设为 1。// loop:是否循环播放,0 表示不循环,-1 表示无限循环。// rate:播放速率,1.0 表示正常速率,更大的值表示更快的播放速率,0.5 表示慢速播放。if (soundId != -1) {Log.i(TAG, "_play: play [direct],soundId = $soundId,resId = $resId")soundPool.play(soundId, tempLeftVol, tempRightVol, 1, 0, 1.0f)} else {Log.i(TAG, "_play: play [load],soundId = $soundId,resId = $resId")soundPool.load(context, resId, 1)soundPool.setOnLoadCompleteListener { _, _, _ ->Log.i(TAG, "_play: play [load -> play],soundId = $soundId,resId = $resId")soundPool.play(soundId, tempLeftVol, tempRightVol, 1, 0, 1.0f)}}}/*** 释放资源*/fun release() {phoneSP.release()notifySP.release()}/*** @return 是否静音*/private fun isSilent(context: Context) =(context.getSystemService(Context.AUDIO_SERVICE) as AudioManager).ringerMode == AudioManager.RINGER_MODE_SILENT/*** @return 音效默认音量*/private fun getVol(context: Context) = 10.0.pow((context.resources.getInteger(com.android.internal.R.integer.config_soundEffectVolumeDb).toFloat() / 20).toDouble()).toFloat()}