目录
📂 前言
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 = -1
fun 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 volFloat
val tempRightVol = if (rightVol != -1f) rightVol else volFloat
soundPool.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.simpleName
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) }
/**
* 加载音效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 = -1
fun 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 volFloat
val 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()
}