Android TTS播报音频并且配合AudioManager压低其他音频声音

使用场景

	1. 拦截三方导航返回的即将播放的文本信息,并且加以处理然后播报语音。
	2. 音频焦点管理,协调与其他音频的冲突,如后台播放音乐时导航播报
	3. 提示固定提示音,如超速等滴滴声音
	4. 控制导航语音音量大小

通过 TTS 播放文本信息

  • 什么是 TTS

TTS 就是 TextToSpeech Google 提供的将文字转换为自然语言流的技术,就是通过接收一段文本,转换为声音。具体看百度百科

我这使用场景是在第三方返回语音信息时拦截,然后自己经过处理后播报出去。具体实现的核心简化版代码如下

创建 TTS

	/** Google引擎 */
	const val GOOGLE_TTS_ENGINE = "com.google.android.tts"
    /**
     * 初始化TTS引擎
     */
    private fun createTts() {
    
    
    	// contextRef 的弱引用 防止内存泄漏
        contextRef.get()?.let {
    
     context ->
            tts = if (!isFollowSystem) {
    
    
            	// 我们这里项目做的不是国内的项目所以如果不跟随系统选择的默认的Google引擎
                TextToSpeech(
                        context,
                        {
    
     status -> this@TtsManager.onInit(status) },
                        GOOGLE_TTS_ENGINE)
            } else {
    
    
            	// 手机默认引擎,这个和手机厂商定制应该有关系 
                TextToSpeech(context) {
    
     p0 -> this@TtsManager.onInit(p0) }
            }
            // 设置语言播报速度
            tts?.setSpeechRate(1f)
            // setOnUtteranceProgressListener 设置的是语音播报的开始、结束、异常 等监听事件
            processListenerRef?.get()?.let {
    
    
                tts?.setOnUtteranceProgressListener(it)
            }
        }
    }

TextToSpeech 第一个参数大家都知道,第二个是初始化成功还是失败的回调,第三个是可以指定引擎,一般手机厂商很多会自己内置自己的引擎,默认的话大多应该就是 Google 引擎了。this 里面第四个参数是 packagename 如果本地有其他的引擎可以指定包名,默认null。this 里面第五个参数是如果指定的引擎失败会走默认的引擎初始化。

 public TextToSpeech(Context context, OnInitListener listener, String engine) {
    
    
        this(context, listener, engine, null, true);
    }

我用的手机不是国行的手机,所以默认是 Google 的 tts 引擎。

下载 TTS

由于目标用户特性,我这里如果没有 Google 引擎,会提示下载 Google tts 引擎

  • 先判断是否有 Google 引擎方法
	fun isInstallGoogleTts():Boolean{
    
    
        tts?.engines?.forEach{
    
    
            if(it.name ==  "com.google.android.tts") {
    
    
                return true
            }
        }
        return false
    }
  • 下载 TTS
private const val GOOGLE_TTS_URI = "market://details?id=com.google.android.tts"
val intent = Intent(Intent.ACTION_VIEW)
            intent.data = Uri.parse(GOOGLE_TTS_URI)
            applicationContext.packageManager?.let {
    
    
                if(intent.resolveActivity(it) != null) {
    
    
                    startActivity(intent)
                }
            }

TTS 有 Voice 语音包

  • 根据需要筛选
            tts?.voices?.forEach {
    
    
                // 筛选美式英语
                if (it.locale.language == English
                    && (it.locale.country.toUpperCase(Locale.ROOT) == US
                            || it.locale.country.toUpperCase(Locale.ROOT) == UNITED_STATES)) {
    
    
                    enVoices.add(it)
                    //刷选美式西班牙语
                } else if (it.locale.language == Spanish
                    && (it.locale.country.toUpperCase(Locale.ROOT) == US
                            || it.locale.country.toUpperCase(Locale.ROOT) == UNITED_STATES)) {
    
    
                    spVoices.add(it)
                }
            }

TTS 播放语音

fun speak(text: String, queueModel: Int) {
    
    
        Log.d(TAG, "Voice message: $text")
        utteranceId = TAG + messageId++
        // QUEUE_FLUSH interrupts already speaking messages.
        val error: Int = tts?.speak(text, queueModel, null, utteranceId) ?: -1
        if (error != 0) {
    
    
            Log.e(TAG, "Error when speaking using Android's TextToSpeech: $error")
        }
}
  • speak 参数的含义
    public int speak(final CharSequence text,
                     final int queueMode,
                     final Bundle params,
                     final String utteranceId) {
    
    
        return runAction((ITextToSpeechService service) -> {
    
    
        }, ERROR, "speak");
    }
	@param text :要讲的文本字符串,不超过 4000 个字符
	@param queueMode:要使用的队列策略,主要有两种
		- public static final int QUEUE_ADD = 1; 队列模式,在播放队列的末尾添加新条目。
		- public static final int QUEUE_FLUSH = 0; 主要意思就是前面的删除留下新的
	@param params:请求参数可以为null。就是内置的一些可配置的引擎参数。通过 bundle传入。
	@param utteranceId :此请求的唯一标识符。
	@ return :返回值是代表错误吗 0 成功 , -1 失败

音频焦点管理

音频焦点管理就是解决播报时是否有其他音频正在播报,比如导航中,后台放着音乐,前面右转播报时降低音乐音量保证用户可以听清楚导航声音。

通过 android,media 下的 AudioFocusRequest 的音频焦点管理类和 AudioManager 获取音频焦点。核心简化代码如下

  • 上面介绍了通过 TTS 播放导航信息播报,则播报之前要获取到音频焦点,结束之后要释放焦点,否则后台音乐的音量不会变回去。

  • 首先获取 AudioFocusRequest

/** audioFocus请求参数*/
    private var audioRequest: AudioFocusRequest? = null
    private fun createFocusRequest(){
    
    
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    
    
            audioRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)
                .setAudioAttributes(audioAttributes)
                .setOnAudioFocusChangeListener {
    
    
                    Log.d(TAG, "init Audio Focus: $it")
                }
                .setAcceptsDelayedFocusGain(false)
                .build()
        }
	}

通过 AudioFocusRequest 的 Builder 来构建对象,audioAttributes 设置一些属性如下:

    /** audioFocus属性 */
    private val audioAttributes: AudioAttributes by lazy {
    
    
        audioAttr?:AudioAttributes.Builder()
            .setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
            .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
            .build()
    }
  • setUsage : 设置用途,因为我这里时导航,所以选择的 USAGE_ASSISTANCE_NAVIGATION_GUIDANCE 导航用途
    • USAGE_MEDIA:表示音频的用途是多媒体。
      USAGE_VOICE_COMMUNICATION:表示音频的用途是语音通信。
      USAGE_NOTIFICATION:表示音频的用途是通知。
      USAGE_ALARM:表示音频的用途是闹钟。
      USAGE_ASSISTANCE_ACCESSIBILITY:表示音频的用途是辅助功能和无障碍功能。
  • setContentType: 设置描述音频信号(如语音或音乐)的内容类型的属性。以下是其中一些类型
    • CONTENT_TYPE_MUSIC:表示音频的内容类型是音乐。
      CONTENT_TYPE_SPEECH:表示音频的内容类型是语音。
      CONTENT_TYPE_MOVIE:表示音频的内容类型是电影。
      CONTENT_TYPE_SONIFICATION:表示音频的内容类型是提示音或系统声音。
  • setFlags(int flags):设置音频的标志。
  • AudioFocusRequest 的 .setAcceptsDelayedFocusGain(false) 属性

setAcceptsDelayedFocusGain() 是 AudioAttributes.Builder 类中的一个属性,用于指示音频源是否支持延迟获取焦点(delayed focus gain)。
在 Android 中,当多个应用程序同时请求使用音频焦点时,系统需要决定哪个应用程序具有焦点,以便控制音频的播放和暂停。默认情况下,如果一个应用程序请求获取音频焦点,系统会立即将焦点转移到该应用程序,但在某些情况下,这可能会引起不必要的中断和干扰。例如,如果用户正在使用语音助手(例如 Google Assistant)进行语音识别,那么在识别期间可能不希望其他应用程序突然播放声音并中断语音识别。为了避免这种情况,系统支持延迟获取焦点,即在请求获取焦点后,系统会等待一段时间以确定焦点是否真的需要转移,如果不需要,则会保留焦点。
如果一个应用程序支持延迟获取焦点,那么当它请求获取焦点时,系统会通知它是否已经获得了焦点,并告诉它何时将焦点转移到该应用程序。这使得应用程序可以在等待焦点时继续播放音频,而不必担心在焦点转移时被中断。
因此,setAcceptsDelayedFocusGain() 属性的意义是指示音频源是否支持延迟获取焦点。如果一个音频源支持延迟获取焦点,那么应该将该属性设置为 true,否则应该将其设置为 false。如果不确定是否需要支持延迟获取焦点,则可以使用默认值 false。

我这里因为要求实时性延迟的话会很危险,所以设置成了 false

  • 下一步获取 AudioManager
    private val audioManager: AudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager

请求音频焦点

  • 我这里时在 TTS 开始播报之谦时候获取焦点,播放结束或者中断的时候移除,TTS回调如下:
private val utteranceProgressListener = object : UtteranceProgressListener() {
    
    
        override fun onStart(utteranceId: String?) {
    
    
        	// 获取焦点
            audioFocusManager.requestAudioFocus()
            // 根据配置选择是否增加音量
            increaseVolume()
        }

        override fun onDone(utteranceId: String?) {
    
    
        	// 释放焦点
            audioFocusManager.abandonAudioFocus()
            // 还原音量
            recoverVolume()
        }

        @Deprecated("Deprecated in Java")
        override fun onError(utteranceId: String?) {
    
    
        }

        override fun onError(utteranceId: String?, errorCode: Int) {
    
    
            super.onError(utteranceId, errorCode)
            // 释放焦点
            audioFocusManager.abandonAudioFocus()
             // 还原音量
            recoverVolume()
        }

        override fun onStop(utteranceId: String?, interrupted: Boolean) {
    
    
            super.onStop(utteranceId, interrupted)
            //1.使用speak的QUEUE_FLUSH模式,打断上一个,上一个不会回调onDone,但是会回调onStop
            //2.直接stop也会回调onStop
            audioFocusManager.abandonAudioFocus()
             // 还原音量
            recoverVolume()
        }
    }
/**
     * 请求音频焦点,通过 audioManager 喝 audioFocusRequest来请求焦点
     *
     * @return 音频焦点请求结果
     */
    fun requestAudioFocus(): Int {
    
    
        try {
    
    
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    
    
                audioRequest?.let {
    
    
                    return audioManager.requestAudioFocus(it)
                }
                return AudioManager.AUDIOFOCUS_REQUEST_FAILED
            } else {
    
    
            	// 这个已经弃用
                return audioManager.requestAudioFocus(
                    {
    
    
                        Log.d(TAG, "requestAudioFocus Audio Focus: $it")
                    },
                    AudioManager.STREAM_ACCESSIBILITY,
                    AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
                )
            }
        } catch (e: IllegalArgumentException) {
    
    
            e.printStackTrace()
        }
        return AudioManager.AUDIOFOCUS_REQUEST_FAILED
    }

释放焦点

    /**
     * 释放audio焦点
     */
    fun abandonAudioFocus() {
    
    
        audioRequest?.let {
    
    
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    
    
                audioManager.abandonAudioFocusRequest(it)
            } else {
    
    
            	// 弃用的方法
                audioManager.abandonAudioFocus{
    
    
                    Log.d(TAG, "abandonAudioFocus Audio Focus: $it")
                }
            }
        }
    }

通过 AudioManager 增加或者减少音量

    private fun increaseVolume() {
    
    
    	// 判断用户开关是否开启
        if (isVolumeUp()) {
    
    
            //记录原始音量
            recordOriginVolume = getVolume()
            //静音情况下直接返回
            if (recordOriginVolume <= 0) {
    
    
                return
            }
            // 设置音量,固定增加量,我们这里增加了原来的百分之三十
            setVolume(recordOriginVolume + increaseVolumeNum)
            //记录修改后的音量
            recordTargetVolume = getVolume()
            isVolumeUpping = true
        }
    }
  • 获取当前系统设置音量
    private fun getVolume(): Int {
    
    
        return try {
    
    
            audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
        } catch (e: Exception) {
    
    
            TPLogUtils.e(TAG, "getVolume() error : message:${e.message}")
            0
        }
    }

audioManager 的 getStreamVolume() 的类型有以下几种可以根据需求选择

STREAM_VOICE_CALL:语音通话音频流。
STREAM_SYSTEM:系统提示音和声音效果音频流。
STREAM_RING:电话铃声音频流。
STREAM_NOTIFICATION:通知提示音和通知音效音频流。
STREAM_ALARM:闹钟提示音频流。
STREAM_DTMF:拨号按键音频流。
STREAM_ACCESSIBILITY:辅助功能音频流
  • setVolume 设置音量
    private fun setVolume(volumeIndex: Int, flags: Int = 0) {
    
    
        try {
    
    
            audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volumeIndex, flags)
        } catch (e: Exception) {
    
    
            TPLogUtils.e(
                TAG,
                "setVolume() error : message:${e.message},volumeIndex:$volumeIndex,flags:$flags"
            )
        }
    }

音量设置完成,播放完成以后记得设置回原来的值,以免越来越大。

AudioManager 常用的 API

  • AudioManager 类的一些常用 API:
setStreamVolume(int streamType, int index, int flags):设置指定流类型(streamType)的音量值(index),其中标志(flags)通常设置为 0getStreamVolume(int streamType):获取指定流类型(streamType)的当前音量值。
setMode(int mode):设置音频模式(mode),例如音乐播放或电话通话。
setSpeakerphoneOn(boolean on):设置是否使用扬声器(speakerphone)进行音频输出。
isSpeakerphoneOn():检查当前是否使用扬声器(speakerphone)进行音频输出。
setMicrophoneMute(boolean on):设置麦克风静音状态。
isMicrophoneMute():检查当前麦克风是否处于静音状态。
setWiredHeadsetOn(boolean on):设置是否插入有线耳机。
isWiredHeadsetOn():检查当前是否插入有线耳机。
setBluetoothScoOn(boolean on):设置是否使用蓝牙 SCO 音频通道。
startBluetoothSco():开始使用蓝牙 SCO 音频通道。
stopBluetoothSco():停止使用蓝牙 SCO 音频通道。
isBluetoothScoOn():检查当前是否使用蓝牙 SCO 音频通道。
setBluetoothScoHeadset(AudioDevice bluetoothScoHeadset):设置当前蓝牙耳机设备。
isBluetoothScoAvailableOffCall():检查当前是否支持在非通话状态下使用蓝牙 SCO 音频通道。

通过这些 API ,可以满足大多数场景下的语音播报需求。

猜你喜欢

转载自blog.csdn.net/ldxlz224/article/details/130532614
今日推荐