AR 眼镜之-系统应用音效-实现方案

目录

[📂 前言](#📂 前言)

[AR 眼镜系统版本](#AR 眼镜系统版本)

系统应用音效

[1. 🔱 技术方案](#1. 🔱 技术方案)

[1.1 技术方案概述](#1.1 技术方案概述)

[1.2 实现方案](#1.2 实现方案)

1)初始化

2)播放音效

3)释放资源

[2. 💠 播放音效](#2. 💠 播放音效)

[2.1 静音不播放](#2.1 静音不播放)

[2.2 获取音效默认音量](#2.2 获取音效默认音量)

[3. ⚛️ 单声道空间音效](#3. ⚛️ 单声道空间音效)

[3.1 实现方案](#3.1 实现方案)

[3.2 封装播放单声道空间音效](#3.2 封装播放单声道空间音效)

[4. ✅ 小结](#4. ✅ 小结)

[附录1:StreamType 值](#附录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 实现方案

  1. 屏蔽原生 View 点击音效:android:soundEffectsEnabled="false"

  2. 使用 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()

}
相关推荐
Swuagg3 个月前
AR 眼镜之-蓝牙电话-来电铃声与系统音效
ar眼镜·蓝牙电话·系统音效·来电铃声
BruceGerGer1 年前
flutter开发实战-实现音效soundpool播放音频及控制播放暂停停止设置音量
flutter·音视频·soundpool·exoplayer·sound·音频播放·音效