From d7e4dfa6c5842087998508a4e571855ad75daaca Mon Sep 17 00:00:00 2001 From: wolfplus2048 <30993207+wolfplus2048@users.noreply.github.com> Date: Sun, 22 Jun 2025 13:16:24 +0100 Subject: [PATCH] add --- .../azure_speech/AzureTtsHelper.kt | 5 - .../bytedance_speech/BytedanceAudioPlayer.kt | 315 ++++++++++++------ .../chat_api/ChatApiService.kt | 3 +- .../kotlin/com/deep_voice/speech/TtsEvents.kt | 1 - 4 files changed, 214 insertions(+), 110 deletions(-) diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt index 0465e5e4c..ea9d42588 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt @@ -232,11 +232,6 @@ class AzureTtsHelper(private val context: Context) : ITtsService { manager.isSpeakerphoneOn = false // 注意:Android不能强制路由到耳机,只能在耳机已连接时使用 } - AudioOutputDevice.EARPIECE -> { - // 强制使用听筒 - manager.mode = AudioManager.MODE_IN_COMMUNICATION - manager.isSpeakerphoneOn = false - } } } } diff --git a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt index a3f2c972b..ac7cf0afd 100644 --- a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt +++ b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt @@ -1,12 +1,16 @@ package com.deep_voice.bytedance_speech +import android.content.BroadcastReceiver import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.media.AudioAttributes +import android.media.AudioDeviceCallback +import android.media.AudioDeviceInfo import android.media.AudioFormat import android.media.AudioManager import android.media.AudioTrack import android.os.Build -import android.util.Log import com.deep_voice.speech.tts.AudioDataListener import com.deep_voice.speech.tts.AudioOutputDevice @@ -29,6 +33,11 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener { private var audioOutputDevice = AudioOutputDevice.DEFAULT // 音频输出设备 private var audioManager: AudioManager? = null + // 设备监听相关 + private var deviceCallback: AudioDeviceCallback? = null + private var noisyReceiver: BroadcastReceiver? = null + private var isMonitoringDevices = false + // 回调 private var onPlayStarted: (() -> Unit)? = null private var onPlayCompleted: (() -> Unit)? = null @@ -36,7 +45,6 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener { // 播放位置监听器 private val playbackListener = object : AudioTrack.OnPlaybackPositionUpdateListener { override fun onMarkerReached(track: AudioTrack) { - Log.d(TAG, "播放到达标记位置: ${track.playbackHeadPosition}") onPlayCompleted?.invoke() } @@ -46,7 +54,6 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener { } init { - Log.d(TAG, "BytedanceAudioPlayer 初始化") audioManager = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager initAudioTrack() } @@ -56,24 +63,19 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener { * 开始新的播放会话 */ fun startSession() { - Log.d(TAG, "开始新会话") - - // 重置状态 isFirstData = true totalBytesWritten = 0 sessionActive = true - // 重置 AudioTrack audioTrack?.let { track -> if (track.state == AudioTrack.STATE_INITIALIZED) { - // 停止并清空缓冲区 track.pause() track.flush() - // 重新开始播放 track.play() - Log.d(TAG, "AudioTrack 已重置") } - } ?: initAudioTrack() // 如果没有初始化,则初始化 + } ?: initAudioTrack() + + startDeviceMonitoring() } /** @@ -82,28 +84,20 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener { fun endSession() { if (!sessionActive) return - Log.d(TAG, "数据流结束,总共写入字节数: $totalBytesWritten") sessionActive = false audioTrack?.let { track -> if (totalBytesWritten > 0) { - // 计算总帧数(16-bit 单声道,每帧2字节) val totalFrames = totalBytesWritten / 2 - - // 设置标记位置 try { track.setNotificationMarkerPosition(totalFrames) - Log.d(TAG, "设置播放完成标记位置: $totalFrames") } catch (e: Exception) { - Log.e(TAG, "设置标记失败: ${e.message}") - // 设置失败时,使用延迟触发作为后备 val durationMs = (totalFrames * 1000L) / SAMPLE_RATE android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ onPlayCompleted?.invoke() }, durationMs + 500) } } else { - // 没有数据,直接触发完成 onPlayCompleted?.invoke() } } @@ -113,37 +107,26 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener { * 接收音频数据 */ override fun onAudioData(data: ByteArray) { - if (data.isEmpty()) return - if (!sessionActive) { - Log.w(TAG, "收到音频数据但会话未激活,忽略数据") - return - } - if (audioTrack == null || audioTrack?.state != AudioTrack.STATE_INITIALIZED) { - Log.w(TAG, "收到音频数据但 AudioTrack 未初始化或状态不正确,忽略数据") - return - } + if (data.isEmpty() || !sessionActive) return + if (audioTrack?.state != AudioTrack.STATE_INITIALIZED) return + try { audioTrack?.let { track -> - if (track.state == AudioTrack.STATE_INITIALIZED) { - val bytesWritten = track.write(data, 0, data.size) + val bytesWritten = track.write(data, 0, data.size) + + if (bytesWritten > 0) { + if (isFirstData) { + isFirstData = false + onPlayStarted?.invoke() + } - if (bytesWritten > 0) { - // 第一次写入数据时自动触发开始回调 - if (isFirstData) { - isFirstData = false - onPlayStarted?.invoke() - Log.d(TAG, "播放开始") - } - - // 累计写入字节数 - if (sessionActive) { - totalBytesWritten += bytesWritten - } + if (sessionActive) { + totalBytesWritten += bytesWritten } } } } catch (e: Exception) { - Log.e(TAG, "写入音频数据失败: ${e.message}") + // 忽略写入失败 } } @@ -151,8 +134,6 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener { * 停止播放 */ fun stop() { - Log.d(TAG, "停止播放") - sessionActive = false audioTrack?.let { track -> @@ -162,7 +143,8 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener { } } - // 如果已经开始播放,触发完成回调 + stopDeviceMonitoring() + if (!isFirstData) { onPlayCompleted?.invoke() } @@ -172,12 +154,10 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener { * 释放资源 */ fun release() { - Log.d(TAG, "释放资源") - stop() - audioTrack?.release() audioTrack = null + stopDeviceMonitoring() } /** @@ -214,13 +194,14 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener { fun setAudioOutputDevice(device: AudioOutputDevice) { if (audioOutputDevice != device) { audioOutputDevice = device - // 如果AudioTrack已初始化,需要重新创建以应用新的输出设备设置 - if (audioTrack != null) { - val wasPlaying = isPlaying() - initAudioTrack() - if (wasPlaying) { - audioTrack?.play() - } + + // 应用新的音频路由设置 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // API 31+ 可以动态更改设备 + setPreferredDeviceForTrack() + } else { + // API 30 及以下需要重新配置 + configureAudioRouting() } } } @@ -229,43 +210,19 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener { * 初始化 AudioTrack */ private fun initAudioTrack() { - // 释放旧实例 audioTrack?.release() - // 计算缓冲区大小 val minBufferSize = AudioTrack.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT) val bufferSize = minBufferSize * 2 - // 根据输出设备配置AudioAttributes - val audioAttributes = when (audioOutputDevice) { - AudioOutputDevice.EARPIECE -> { - // 听筒模式 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) - .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) - .build() - } else { - null - } - } - else -> { - // 默认、耳机、扬声器模式 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + audioTrack = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + AudioTrack.Builder() + .setAudioAttributes( AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) .build() - } else { - null - } - } - } - - // 创建 AudioTrack - audioTrack = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && audioAttributes != null) { - AudioTrack.Builder() - .setAudioAttributes(audioAttributes) + ) .setAudioFormat( AudioFormat.Builder() .setEncoding(AUDIO_FORMAT) @@ -278,12 +235,8 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener { .build() } else { @Suppress("DEPRECATION") - val streamType = when (audioOutputDevice) { - AudioOutputDevice.EARPIECE -> AudioManager.STREAM_VOICE_CALL - else -> AudioManager.STREAM_MUSIC - } AudioTrack( - streamType, + AudioManager.STREAM_MUSIC, SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT, @@ -292,46 +245,202 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener { ) } - // 设置播放位置监听器 audioTrack?.setPlaybackPositionUpdateListener(playbackListener) - // 配置音频路由 - configureAudioRouting() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + setPreferredDeviceForTrack() + } else { + configureAudioRouting() + } - // 开始播放 audioTrack?.play() + } + + /** + * API 31+ 使用 setPreferredDevice 设置音频输出设备 + */ + private fun setPreferredDeviceForTrack() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return - Log.d(TAG, "AudioTrack 初始化成功") + audioManager?.let { manager -> + audioTrack?.let { track -> + when (audioOutputDevice) { + AudioOutputDevice.DEFAULT -> { + track.setPreferredDevice(null) + manager.isSpeakerphoneOn = false + manager.mode = AudioManager.MODE_NORMAL + } + + AudioOutputDevice.SPEAKER -> { + val speaker = manager.getDevices(AudioManager.GET_DEVICES_OUTPUTS) + .firstOrNull { it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER } + speaker?.let { track.setPreferredDevice(it) } + } + + AudioOutputDevice.HEADPHONES -> { + val headphones = manager.getDevices(AudioManager.GET_DEVICES_OUTPUTS) + .firstOrNull { device -> + device.type == AudioDeviceInfo.TYPE_WIRED_HEADSET || + device.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES || + device.type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP || + device.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO + } + headphones?.let { track.setPreferredDevice(it) } + ?: track.setPreferredDevice(null) + } + } + } + } } /** - * 配置音频路由 + * 配置音频路由(API 30 及以下) */ private fun configureAudioRouting() { audioManager?.let { manager -> when (audioOutputDevice) { AudioOutputDevice.DEFAULT -> { - // 默认模式:系统自动选择 manager.mode = AudioManager.MODE_NORMAL manager.isSpeakerphoneOn = false } + AudioOutputDevice.SPEAKER -> { - // 强制使用扬声器 manager.mode = AudioManager.MODE_NORMAL manager.isSpeakerphoneOn = true } + AudioOutputDevice.HEADPHONES -> { - // 强制使用耳机(如果已连接) manager.mode = AudioManager.MODE_NORMAL manager.isSpeakerphoneOn = false - // 注意:Android不能强制路由到耳机,只能在耳机已连接时使用 } - AudioOutputDevice.EARPIECE -> { - // 强制使用听筒 - manager.mode = AudioManager.MODE_IN_COMMUNICATION - manager.isSpeakerphoneOn = false + } + } + } + + /** + * 开始监听设备变化 + */ + private fun startDeviceMonitoring() { + if (isMonitoringDevices) return + + // API 23+ 使用 AudioDeviceCallback + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + deviceCallback = object : AudioDeviceCallback() { + override fun onAudioDevicesAdded(addedDevices: Array) { + super.onAudioDevicesAdded(addedDevices) + handleDeviceAdded(addedDevices) + } + + override fun onAudioDevicesRemoved(removedDevices: Array) { + super.onAudioDevicesRemoved(removedDevices) + handleDeviceRemoved(removedDevices) } } + audioManager?.registerAudioDeviceCallback(deviceCallback, null) + } + + // 所有版本都监听 NOISY 广播(耳机拔出) + noisyReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == AudioManager.ACTION_AUDIO_BECOMING_NOISY) { + handleNoisyAudioEvent() + } + } + } + context.registerReceiver(noisyReceiver, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)) + + isMonitoringDevices = true + } + + /** + * 停止监听设备变化 + */ + private fun stopDeviceMonitoring() { + if (!isMonitoringDevices) return + + // 注销 AudioDeviceCallback + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + deviceCallback?.let { + audioManager?.unregisterAudioDeviceCallback(it) + } + deviceCallback = null + } + + // 注销广播接收器 + noisyReceiver?.let { + try { + context.unregisterReceiver(it) + } catch (e: Exception) { + // 忽略已注销的异常 + } + } + noisyReceiver = null + + isMonitoringDevices = false + } + + /** + * 处理设备添加 + */ + private fun handleDeviceAdded(devices: Array) { + val hasHeadphones = devices.any { device -> + device.type == AudioDeviceInfo.TYPE_WIRED_HEADSET || + device.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES || + device.type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP || + device.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO + } + + if (hasHeadphones) { + switchToDefaultMode() + } + } + + /** + * 处理设备移除 + */ + private fun handleDeviceRemoved(devices: Array) { + val hasHeadphones = devices.any { device -> + device.type == AudioDeviceInfo.TYPE_WIRED_HEADSET || + device.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES || + device.type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP || + device.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO + } + + if (hasHeadphones) { + switchToSpeakerMode() + } + } + + /** + * 处理音频变得嘈杂事件(通常是耳机拔出) + */ + private fun handleNoisyAudioEvent() { + switchToSpeakerMode() + } + + /** + * 切换到 DEFAULT 模式 + */ + private fun switchToDefaultMode() { + audioOutputDevice = AudioOutputDevice.DEFAULT + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + setPreferredDeviceForTrack() + } else { + configureAudioRouting() + } + } + + /** + * 切换到扬声器模式 + */ + private fun switchToSpeakerMode() { + audioOutputDevice = AudioOutputDevice.SPEAKER + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + setPreferredDeviceForTrack() + } else { + configureAudioRouting() } } } \ No newline at end of file diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt index 13f74a898..55485623b 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt @@ -218,7 +218,8 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor mcpConfigJson = mcpServer // 异步初始化MCP客户端 launch { - initializeMcpClient(mcpServer) + // initializeMcpClient(mcpServer) + initializeMcpClient("{}") } isInitialized = apiKey.isNotEmpty() diff --git a/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/TtsEvents.kt b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/TtsEvents.kt index 6758c6f05..47bea05bf 100644 --- a/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/TtsEvents.kt +++ b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/TtsEvents.kt @@ -67,5 +67,4 @@ enum class AudioOutputDevice { DEFAULT, // 默认(如果有耳机选耳机,否则使用系统扬声器) HEADPHONES, // 强制使用耳机 SPEAKER, // 强制使用扬声器 - EARPIECE // 强制使用听筒 } \ No newline at end of file