

























@@ -98,6 +98,10 @@ class TalkModeManager internal constructor(
9898private const val tag = "TalkMode"
9999private const val realtimeSampleRateHz = 24_000
100100private const val realtimeAudioFrameMs = 100
101+private const val realtimeSpeechStartAverageAmplitude = 800
102+private const val realtimeSpeechContinueAverageAmplitude = 800
103+private const val realtimeSpeechStartFrameCount = 3
104+private const val realtimeSpeechHangoverMs = 700L
101105private const val listenWatchdogMs = 12_000L
102106private const val chatFinalWaitWithSubscribeMs = 45_000L
103107private const val chatFinalWaitWithoutSubscribeMs = 6_000L
@@ -161,7 +165,9 @@ class TalkModeManager internal constructor(
161165private val realtimePlaybackLock = Any()
162166private var realtimeAudioTrack: AudioTrack? = null
163167private var realtimePlaybackIdleJob: Job? = null
164-private var realtimePlaybackEndsAtMs = 0L
168+ @Volatile private var realtimePlaybackEndsAtMs = 0L
169+ @Volatile private var realtimeSpeechActiveUntilMs = 0L
170+private var realtimeSpeechCandidateFrames = 0
165171166172 @Volatile private var realtimeOutputSuppressed = false
167173@@ -635,6 +641,7 @@ class TalkModeManager internal constructor(
635641636642 realtimeSessionId = sessionId
637643 realtimeOutputSuppressed = false
644+ resetRealtimeSpeechGate()
638645_isListening.value = true
639646_statusText.value = "Listening"
640647 startRealtimeCapture(sessionId)
@@ -671,6 +678,7 @@ class TalkModeManager internal constructor(
671678 scope.launch(Dispatchers.IO) {
672679for (frame in audioFrames) {
673680if (realtimeSessionId != sessionId) continue
681+if (isRealtimePlaybackActive()) continue
674682val audioBase64 = Base64.encodeToString(frame, Base64.NO_WRAP)
675683val params =
676684 buildJsonObject {
@@ -726,6 +734,7 @@ class TalkModeManager internal constructor(
726734while (coroutineContext.isActive && _isEnabled.value && realtimeSessionId == sessionId) {
727735val read = audioRecord.read(buffer, 0, buffer.size)
728736if (read <= 0) continue
737+if (!shouldAppendRealtimeCapturedFrame(buffer, read)) continue
729738 audioFrames.trySend(buffer.copyOf(read))
730739 }
731740 } catch (err: Throwable) {
@@ -745,6 +754,62 @@ class TalkModeManager internal constructor(
745754 }
746755 }
747756757+private fun shouldAppendRealtimeCapturedFrame(
758+frame: ByteArray,
759+length: Int = frame.size,
760+ ): Boolean {
761+if (isRealtimePlaybackActive()) {
762+ resetRealtimeSpeechGate()
763+return false
764+ }
765+val now = SystemClock.elapsedRealtime()
766+val average = pcm16AverageAmplitude(frame, length)
767+if (now < realtimeSpeechActiveUntilMs) {
768+if (average >= realtimeSpeechContinueAverageAmplitude) {
769+ realtimeSpeechActiveUntilMs = now + realtimeSpeechHangoverMs
770+ }
771+return true
772+ }
773+if (average >= realtimeSpeechStartAverageAmplitude) {
774+ realtimeSpeechCandidateFrames += 1
775+if (realtimeSpeechCandidateFrames >= realtimeSpeechStartFrameCount) {
776+ realtimeSpeechActiveUntilMs = now + realtimeSpeechHangoverMs
777+return true
778+ }
779+return false
780+ }
781+ realtimeSpeechCandidateFrames = 0
782+return false
783+ }
784+785+private fun isRealtimePlaybackActive(): Boolean =
786+_isSpeaking.value || SystemClock.elapsedRealtime() < realtimePlaybackEndsAtMs
787+788+private fun resetRealtimeSpeechGate() {
789+ realtimeSpeechActiveUntilMs = 0L
790+ realtimeSpeechCandidateFrames = 0
791+ }
792+793+private fun pcm16AverageAmplitude(
794+frame: ByteArray,
795+length: Int,
796+ ): Int {
797+var total = 0L
798+var count = 0
799+var index = 0
800+val limit = length - (length % 2)
801+while (index < limit) {
802+val sample =
803+ (frame[index].toInt() and 0xff) or
804+ (frame[index + 1].toInt() shl 8)
805+val amplitude = kotlin.math.abs(sample.toShort().toInt())
806+ total += amplitude
807+ count += 1
808+ index += 2
809+ }
810+return if (count == 0) 0 else (total / count).toInt()
811+ }
812+748813private fun handleRealtimeTalkEvent(payloadJson: String?) {
749814if (payloadJson.isNullOrBlank()) return
750815val obj =
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。