From 724c5d51e004ed993403d430a23e805856997c6e Mon Sep 17 00:00:00 2001 From: ross <3024454314@qq.com> Date: Thu, 8 Jan 2026 17:43:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=92=AD=E6=94=BE=E7=AD=89?= =?UTF-8?q?=E5=BE=85=E8=B6=85=E6=97=B6=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/zs/smarthuman/sherpa/TimeoutType.kt | 13 ++++ .../zs/smarthuman/sherpa/VoiceController.kt | 64 +++++++++++++++---- .../java/com/zs/smarthuman/ui/MainActivity.kt | 21 +++--- .../zs/smarthuman/viewmodel/MainViewModel.kt | 4 +- 4 files changed, 77 insertions(+), 25 deletions(-) create mode 100644 app/src/main/java/com/zs/smarthuman/sherpa/TimeoutType.kt diff --git a/app/src/main/java/com/zs/smarthuman/sherpa/TimeoutType.kt b/app/src/main/java/com/zs/smarthuman/sherpa/TimeoutType.kt new file mode 100644 index 0000000..9bd098f --- /dev/null +++ b/app/src/main/java/com/zs/smarthuman/sherpa/TimeoutType.kt @@ -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 \ No newline at end of file diff --git a/app/src/main/java/com/zs/smarthuman/sherpa/VoiceController.kt b/app/src/main/java/com/zs/smarthuman/sherpa/VoiceController.kt index 2389b5e..c5fb4db 100644 --- a/app/src/main/java/com/zs/smarthuman/sherpa/VoiceController.kt +++ b/app/src/main/java/com/zs/smarthuman/sherpa/VoiceController.kt @@ -2,17 +2,12 @@ package com.zs.smarthuman.sherpa import android.content.res.AssetManager 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 -// 新增:超时类型枚举(区分两种超时) -enum class TimeoutType { - IDLE_TIMEOUT, // 唤醒后全程没说话的闲时超时 - INVALID_SPEECH_TIMEOUT // 唤醒后一直无效说话的超时 -} - -// 新增:提示语回调(外部可通过此获取不同的提示语) -typealias OnTimeoutTip = (TimeoutType) -> Unit - class VoiceController( assetManager: AssetManager, private val onWakeup: () -> Unit, @@ -21,7 +16,6 @@ class VoiceController( maxRecordingSeconds: Int = 10, private val onStateChanged: ((VoiceState) -> Unit)? = null, private val stopBackendAudio: (() -> Unit)? = null, - // 新增:超时提示语回调 private val onTimeoutTip: OnTimeoutTip? = null ) { @@ -120,6 +114,11 @@ class VoiceController( // ========== 补充:MIN_EFFECTIVE_SPEECH_RMS 常量(和VadManager对齐) ========== private val MIN_EFFECTIVE_SPEECH_RMS = 0.001f + + //播放等待超时 + private val PLAY_WAIT_TIMEOUT_MS = 3000L + private var playWaitJob: Job? = null + /* ================= 音频入口 ================= */ fun acceptAudio(samples: FloatArray) { cachePreBuffer(samples) @@ -472,7 +471,12 @@ class VoiceController( } 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 } @@ -485,13 +489,43 @@ class VoiceController( fun onUploadFinished(success: Boolean) { if (state != VoiceState.UPLOADING) return LogUtils.d(TAG, "📤 上传完成 | 成功: $success | 基线: $currentEnvBaseline") - state = if (success) VoiceState.PLAYING_BACKEND - else { + + if (success) { + // 上传成功:启动协程超时任务 + startPlayWaitTimer() + } else { + // 上传失败:取消超时任务,重置状态 + cancelPlayWaitTimer() 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() { LogUtils.d(TAG, "🔄 重置到等待说话 | 基线: $currentEnvBaseline | 已标记无效说话: $hasInvalidSpeech") val now = System.currentTimeMillis() @@ -525,6 +559,7 @@ class VoiceController( currentTimeoutType = TimeoutType.IDLE_TIMEOUT LogUtils.d(TAG, "🔄 环境基线已重置 | 新基线: $currentEnvBaseline | 无效说话标记已重置") state = VoiceState.WAIT_WAKEUP + cancelPlayWaitTimer() } fun release() { @@ -536,6 +571,7 @@ class VoiceController( // 核心新增:释放资源时重置标记 hasInvalidSpeech = false currentTimeoutType = TimeoutType.IDLE_TIMEOUT + cancelPlayWaitTimer() } private fun cachePreBuffer(samples: FloatArray) { diff --git a/app/src/main/java/com/zs/smarthuman/ui/MainActivity.kt b/app/src/main/java/com/zs/smarthuman/ui/MainActivity.kt index 9d30b7f..1a7539b 100644 --- a/app/src/main/java/com/zs/smarthuman/ui/MainActivity.kt +++ b/app/src/main/java/com/zs/smarthuman/ui/MainActivity.kt @@ -148,8 +148,11 @@ class MainActivity : BaseViewModelActivity() voiceController?.onUploadFinished(false) } - is ApiResult.Success<*> -> { - Toaster.showShort("上传成功") + is ApiResult.Success -> { + if (!TextUtils.isEmpty(it.data)){ + Toaster.showShort(it.data) + } + Toaster.showShort(it) voiceController?.onUploadFinished(true) } } @@ -180,7 +183,7 @@ class MainActivity : BaseViewModelActivity() onWakeup = { Log.d("lrs", "当前状态: 唤醒成功wakeup") //每次唤醒前都要把前面的音频停掉 - UnityPlayerHolder.getInstance().cancelPCM() +// UnityPlayerHolder.getInstance().cancelPCM() UnityPlayerHolder.getInstance() .sendVoiceToUnity( voiceInfo = mutableListOf().apply { @@ -200,12 +203,12 @@ class MainActivity : BaseViewModelActivity() 1 ) // loadLocalJsonAndPlay() -// val file = File( -// getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)!!.getAbsolutePath(), -// "xxx.wav" -// ) -// AudioDebugUtil.saveFloatPcmAsWav(audio, file) -// LogUtils.dTag("audioxx", "WAV saved: ${file.path}, samples=${audio.size}") + val file = File( + getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)!!.getAbsolutePath(), + "xxx.wav" + ) + AudioDebugUtil.saveFloatPcmAsWav(audio, file) + LogUtils.dTag("audioxx", "WAV saved: ${file.path}, samples=${audio.size}") // lifecycleScope.launch(Dispatchers.Main) { // // mVerticalAnimator?.show() diff --git a/app/src/main/java/com/zs/smarthuman/viewmodel/MainViewModel.kt b/app/src/main/java/com/zs/smarthuman/viewmodel/MainViewModel.kt index cdc4724..5450f47 100644 --- a/app/src/main/java/com/zs/smarthuman/viewmodel/MainViewModel.kt +++ b/app/src/main/java/com/zs/smarthuman/viewmodel/MainViewModel.kt @@ -34,8 +34,8 @@ class MainViewModel: BaseViewModel() { } - private val _uploadVoiceLiveData = ApiLiveData() - val uploadVoiceLiveData: LiveData> = _uploadVoiceLiveData + private val _uploadVoiceLiveData = ApiLiveData() + val uploadVoiceLiveData: LiveData> = _uploadVoiceLiveData fun uploadVoice(audioVoice: String, sessionCode: Int){ _uploadVoiceLiveData.request { RxHttp.postJson(ApiService.UPLOAD_RECORD_VOICE_URL)