添加播放等待超时判断

This commit is contained in:
林若思 2026-01-08 17:43:39 +08:00
parent e9eab6ae23
commit 724c5d51e0
4 changed files with 77 additions and 25 deletions

View File

@ -0,0 +1,13 @@
package com.zs.smarthuman.sherpa
/**
* @description:
* @author: lrs
* @date: 2026/1/8 17:39
*/
enum class TimeoutType {
IDLE_TIMEOUT, // 唤醒后全程没说话的闲时超时
INVALID_SPEECH_TIMEOUT // 唤醒后一直无效说话的超时
}
typealias OnTimeoutTip = (TimeoutType) -> Unit

View File

@ -2,17 +2,12 @@ package com.zs.smarthuman.sherpa
import android.content.res.AssetManager import android.content.res.AssetManager
import com.blankj.utilcode.util.LogUtils import com.blankj.utilcode.util.LogUtils
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.ArrayDeque import java.util.ArrayDeque
// 新增:超时类型枚举(区分两种超时)
enum class TimeoutType {
IDLE_TIMEOUT, // 唤醒后全程没说话的闲时超时
INVALID_SPEECH_TIMEOUT // 唤醒后一直无效说话的超时
}
// 新增:提示语回调(外部可通过此获取不同的提示语)
typealias OnTimeoutTip = (TimeoutType) -> Unit
class VoiceController( class VoiceController(
assetManager: AssetManager, assetManager: AssetManager,
private val onWakeup: () -> Unit, private val onWakeup: () -> Unit,
@ -21,7 +16,6 @@ class VoiceController(
maxRecordingSeconds: Int = 10, maxRecordingSeconds: Int = 10,
private val onStateChanged: ((VoiceState) -> Unit)? = null, private val onStateChanged: ((VoiceState) -> Unit)? = null,
private val stopBackendAudio: (() -> Unit)? = null, private val stopBackendAudio: (() -> Unit)? = null,
// 新增:超时提示语回调
private val onTimeoutTip: OnTimeoutTip? = null private val onTimeoutTip: OnTimeoutTip? = null
) { ) {
@ -120,6 +114,11 @@ class VoiceController(
// ========== 补充MIN_EFFECTIVE_SPEECH_RMS 常量和VadManager对齐 ========== // ========== 补充MIN_EFFECTIVE_SPEECH_RMS 常量和VadManager对齐 ==========
private val MIN_EFFECTIVE_SPEECH_RMS = 0.001f private val MIN_EFFECTIVE_SPEECH_RMS = 0.001f
//播放等待超时
private val PLAY_WAIT_TIMEOUT_MS = 3000L
private var playWaitJob: Job? = null
/* ================= 音频入口 ================= */ /* ================= 音频入口 ================= */
fun acceptAudio(samples: FloatArray) { fun acceptAudio(samples: FloatArray) {
cachePreBuffer(samples) cachePreBuffer(samples)
@ -472,7 +471,12 @@ class VoiceController(
} }
fun onPlayStartBackend() { fun onPlayStartBackend() {
LogUtils.d(TAG, "🎶 播放后台音频 | 基线: $currentEnvBaseline") // 仅当上传完成(成功)且状态为 UPLOADING 时,才切换状态
if (state != VoiceState.UPLOADING) {
LogUtils.w(TAG, "🎶 非上传完成状态,禁止切换到 PLAYING_BACKEND | 当前状态: $state")
return
}
LogUtils.d(TAG, "🎶 开始播放后台音频 | 基线: $currentEnvBaseline")
state = VoiceState.PLAYING_BACKEND state = VoiceState.PLAYING_BACKEND
} }
@ -485,13 +489,43 @@ class VoiceController(
fun onUploadFinished(success: Boolean) { fun onUploadFinished(success: Boolean) {
if (state != VoiceState.UPLOADING) return if (state != VoiceState.UPLOADING) return
LogUtils.d(TAG, "📤 上传完成 | 成功: $success | 基线: $currentEnvBaseline") LogUtils.d(TAG, "📤 上传完成 | 成功: $success | 基线: $currentEnvBaseline")
state = if (success) VoiceState.PLAYING_BACKEND
else { if (success) {
// 上传成功:启动协程超时任务
startPlayWaitTimer()
} else {
// 上传失败:取消超时任务,重置状态
cancelPlayWaitTimer()
speechEnableAtMs = System.currentTimeMillis() + SPEECH_COOLDOWN_MS speechEnableAtMs = System.currentTimeMillis() + SPEECH_COOLDOWN_MS
VoiceState.WAIT_SPEECH_COOLDOWN state = VoiceState.WAIT_SPEECH_COOLDOWN
} }
} }
private fun startPlayWaitTimer() {
// 先取消旧任务,避免重复
cancelPlayWaitTimer()
// 启动协程超时任务Dispatchers.Main保证状态修改在主线程
playWaitJob = GlobalScope.launch {
delay(PLAY_WAIT_TIMEOUT_MS) // 挂起3秒无线程阻塞
LogUtils.w(TAG, "⏱ 播放等待超时(${PLAY_WAIT_TIMEOUT_MS}ms自动重置状态")
// 超时后重置状态(加同步锁,避免多线程冲突)
synchronized(this@VoiceController) {
speechEnableAtMs = System.currentTimeMillis() + SPEECH_COOLDOWN_MS
state = VoiceState.WAIT_SPEECH_COOLDOWN
}
}
}
// ================= 替换:取消协程任务 =================
private fun cancelPlayWaitTimer() {
playWaitJob?.cancel() // 取消协程(挂起函数会立即停止)
playWaitJob = null
LogUtils.d(TAG, "🔄 播放等待协程已取消")
}
private fun resetToWaitSpeech() { private fun resetToWaitSpeech() {
LogUtils.d(TAG, "🔄 重置到等待说话 | 基线: $currentEnvBaseline | 已标记无效说话: $hasInvalidSpeech") LogUtils.d(TAG, "🔄 重置到等待说话 | 基线: $currentEnvBaseline | 已标记无效说话: $hasInvalidSpeech")
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
@ -525,6 +559,7 @@ class VoiceController(
currentTimeoutType = TimeoutType.IDLE_TIMEOUT currentTimeoutType = TimeoutType.IDLE_TIMEOUT
LogUtils.d(TAG, "🔄 环境基线已重置 | 新基线: $currentEnvBaseline | 无效说话标记已重置") LogUtils.d(TAG, "🔄 环境基线已重置 | 新基线: $currentEnvBaseline | 无效说话标记已重置")
state = VoiceState.WAIT_WAKEUP state = VoiceState.WAIT_WAKEUP
cancelPlayWaitTimer()
} }
fun release() { fun release() {
@ -536,6 +571,7 @@ class VoiceController(
// 核心新增:释放资源时重置标记 // 核心新增:释放资源时重置标记
hasInvalidSpeech = false hasInvalidSpeech = false
currentTimeoutType = TimeoutType.IDLE_TIMEOUT currentTimeoutType = TimeoutType.IDLE_TIMEOUT
cancelPlayWaitTimer()
} }
private fun cachePreBuffer(samples: FloatArray) { private fun cachePreBuffer(samples: FloatArray) {

View File

@ -148,8 +148,11 @@ class MainActivity : BaseViewModelActivity<ActivityMainBinding, MainViewModel>()
voiceController?.onUploadFinished(false) voiceController?.onUploadFinished(false)
} }
is ApiResult.Success<*> -> { is ApiResult.Success<String> -> {
Toaster.showShort("上传成功") if (!TextUtils.isEmpty(it.data)){
Toaster.showShort(it.data)
}
Toaster.showShort(it)
voiceController?.onUploadFinished(true) voiceController?.onUploadFinished(true)
} }
} }
@ -180,7 +183,7 @@ class MainActivity : BaseViewModelActivity<ActivityMainBinding, MainViewModel>()
onWakeup = { onWakeup = {
Log.d("lrs", "当前状态: 唤醒成功wakeup") Log.d("lrs", "当前状态: 唤醒成功wakeup")
//每次唤醒前都要把前面的音频停掉 //每次唤醒前都要把前面的音频停掉
UnityPlayerHolder.getInstance().cancelPCM() // UnityPlayerHolder.getInstance().cancelPCM()
UnityPlayerHolder.getInstance() UnityPlayerHolder.getInstance()
.sendVoiceToUnity( .sendVoiceToUnity(
voiceInfo = mutableListOf<VoiceBeanResp>().apply { voiceInfo = mutableListOf<VoiceBeanResp>().apply {
@ -200,12 +203,12 @@ class MainActivity : BaseViewModelActivity<ActivityMainBinding, MainViewModel>()
1 1
) )
// loadLocalJsonAndPlay() // loadLocalJsonAndPlay()
// val file = File( val file = File(
// getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)!!.getAbsolutePath(), getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)!!.getAbsolutePath(),
// "xxx.wav" "xxx.wav"
// ) )
// AudioDebugUtil.saveFloatPcmAsWav(audio, file) AudioDebugUtil.saveFloatPcmAsWav(audio, file)
// LogUtils.dTag("audioxx", "WAV saved: ${file.path}, samples=${audio.size}") LogUtils.dTag("audioxx", "WAV saved: ${file.path}, samples=${audio.size}")
// lifecycleScope.launch(Dispatchers.Main) { // lifecycleScope.launch(Dispatchers.Main) {
// //
// mVerticalAnimator?.show() // mVerticalAnimator?.show()

View File

@ -34,8 +34,8 @@ class MainViewModel: BaseViewModel() {
} }
private val _uploadVoiceLiveData = ApiLiveData<Unit>() private val _uploadVoiceLiveData = ApiLiveData<String>()
val uploadVoiceLiveData: LiveData<ApiResult<Unit>> = _uploadVoiceLiveData val uploadVoiceLiveData: LiveData<ApiResult<String>> = _uploadVoiceLiveData
fun uploadVoice(audioVoice: String, sessionCode: Int){ fun uploadVoice(audioVoice: String, sessionCode: Int){
_uploadVoiceLiveData.request { _uploadVoiceLiveData.request {
RxHttp.postJson(ApiService.UPLOAD_RECORD_VOICE_URL) RxHttp.postJson(ApiService.UPLOAD_RECORD_VOICE_URL)