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 b3cffcbd1..a761c015c 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,9 +1,6 @@ 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 @@ -11,38 +8,46 @@ import android.media.AudioFormat import android.media.AudioManager import android.media.AudioTrack import android.os.Build +import android.os.Handler +import android.os.Looper +import android.util.Log import com.deep_voice.speech.tts.AudioDataListener import com.deep_voice.speech.tts.AudioOutputDevice /** - * 简化的音频流播放器 - * 基于位置标记触发播放完成回调 + * 高性能音频播放器 - 16kHz采样率 + * 设计要点: + * • 永远使用 USAGE_MEDIA + MODE_NORMAL 媒体播放通路 + * • 利用 AudioDeviceCallback 监听设备热插拔 + * • API 31+ 使用 setPreferredDevice() 精准路由 + * • 有耳机自动从耳机播放,无耳机自动切到扬声器 */ -class BytedanceAudioPlayer(private val context: Context) : AudioDataListener { +class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, AudioDeviceCallback() { companion object { private const val TAG = "BytedanceAudioPlayer" - private const val SAMPLE_RATE = 24000 + private const val SAMPLE_RATE = 16000 // 固定16kHz采样率 private const val CHANNEL_CONFIG = AudioFormat.CHANNEL_OUT_MONO private const val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT - private const val VOLUME_REDUCTION = 0.7f // 降低音量以减少回音 } + private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager private var audioTrack: AudioTrack? = null - private var isFirstData = true // 是否是第一次接收数据 - private var totalBytesWritten = 0 // 总共写入的字节数 - private var sessionActive = true // 会话是否活跃 - 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 isFirstData = true + private var totalBytesWritten = 0 + private var sessionActive = true // 回调 private var onPlayStarted: (() -> Unit)? = null private var onPlayCompleted: (() -> Unit)? = null + // 耳机设备类型(优先级顺序) + private val headsetTypes = intArrayOf( + AudioDeviceInfo.TYPE_WIRED_HEADSET, + AudioDeviceInfo.TYPE_WIRED_HEADPHONES, + AudioDeviceInfo.TYPE_BLUETOOTH_A2DP + // TYPE_BLUETOOTH_LE 在较新的 API 中才有,这里先不包含 + ) + // 播放位置监听器 private val playbackListener = object : AudioTrack.OnPlaybackPositionUpdateListener { override fun onMarkerReached(track: AudioTrack) { @@ -55,10 +60,113 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener { } init { - audioManager = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager - initAudioTrack() + // 注册设备热插拔监听 + audioManager.registerAudioDeviceCallback(this, Handler(Looper.getMainLooper())) + buildTrack(selectOutputDevice()) // 首次创建 + } + + /** + * 监听输出设备变化 + */ + override fun onAudioDevicesAdded(addedDevices: Array) { + Log.d(TAG, "音频设备连接: ${addedDevices.joinToString { "${it.productName}(${it.type})" }}") + rebuildIfNeeded() + } + + override fun onAudioDevicesRemoved(removedDevices: Array) { + Log.d(TAG, "音频设备断开: ${removedDevices.joinToString { "${it.productName}(${it.type})" }}") + rebuildIfNeeded() + } + + /** + * 检查设备变化,必要时重建AudioTrack + */ + private fun rebuildIfNeeded() { + val newDevice = selectOutputDevice() + val currentDevice = audioTrack?.preferredDevice + + // 设备发生变化时重建 + if (newDevice?.id != currentDevice?.id) { + Log.d(TAG, "设备切换: ${currentDevice?.productName} -> ${newDevice?.productName}") + + // 保存当前状态 + val wasPlaying = audioTrack?.playState == AudioTrack.PLAYSTATE_PLAYING + val savedFirstData = isFirstData + val savedTotalBytes = totalBytesWritten + val savedSessionActive = sessionActive + + // 重建AudioTrack + buildTrack(newDevice) + + // 恢复状态 + if (wasPlaying) { + isFirstData = savedFirstData + totalBytesWritten = savedTotalBytes + sessionActive = savedSessionActive + } + } + } + + /** + * 选择当前优先输出设备(耳机优先) + */ + private fun selectOutputDevice(): AudioDeviceInfo? { + return audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS) + .firstOrNull { it.type in headsetTypes } + } + + /** + * 构建16kHz单声道AudioTrack + */ + private fun buildTrack(device: AudioDeviceInfo?) { + audioTrack?.release() + + val minBufferSize = AudioTrack.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT) + + audioTrack = AudioTrack.Builder() + .setAudioAttributes( + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) // 统一媒体通路 + .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) + .build() + ) + .setAudioFormat( + AudioFormat.Builder() + .setSampleRate(SAMPLE_RATE) + .setEncoding(AUDIO_FORMAT) + .setChannelMask(CHANNEL_CONFIG) + .build() + ) + .setTransferMode(AudioTrack.MODE_STREAM) + .setBufferSizeInBytes(minBufferSize * 2) // 小缓冲区保证低延迟 + .apply { + // 低延迟模式 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY) + } + } + .build().also { track -> + // API 31+ 精准路由到指定设备 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && device != null) { + track.preferredDevice = device + Log.d(TAG, "设置首选设备: ${device.productName} (${device.type})") + } + + // 设置播放监听器 + track.setPlaybackPositionUpdateListener(playbackListener) + + // 开始播放 + track.play() + } + + // 配置音频管理器 - 统一使用 MODE_NORMAL + audioManager.mode = AudioManager.MODE_NORMAL + // 切换扬声器开关仅在无耳机时打开 + audioManager.isSpeakerphoneOn = (device == null) + + val deviceName = device?.productName ?: "扬声器" + Log.d(TAG, "AudioTrack已创建 - 输出设备: $deviceName, 采样率: ${SAMPLE_RATE}Hz") } - /** * 开始新的播放会话 @@ -74,9 +182,7 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener { track.flush() track.play() } - } ?: initAudioTrack() - - startDeviceMonitoring() + } ?: buildTrack(selectOutputDevice()) } /** @@ -94,9 +200,9 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener { track.setNotificationMarkerPosition(totalFrames) } catch (e: Exception) { val durationMs = (totalFrames * 1000L) / SAMPLE_RATE - android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ + Handler(Looper.getMainLooper()).postDelayed({ onPlayCompleted?.invoke() - }, durationMs + 500) + }, durationMs + 100) // 更短的延迟 } } else { onPlayCompleted?.invoke() @@ -127,7 +233,7 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener { } } } catch (e: Exception) { - // 忽略写入失败 + Log.w(TAG, "音频数据写入失败: ${e.message}") } } @@ -144,8 +250,6 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener { } } - stopDeviceMonitoring() - if (!isFirstData) { onPlayCompleted?.invoke() } @@ -158,7 +262,7 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener { stop() audioTrack?.release() audioTrack = null - stopDeviceMonitoring() + audioManager.unregisterAudioDeviceCallback(this) } /** @@ -190,283 +294,31 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener { } /** - * 设置音频输出设备 + * 设置音频输出设备(兼容接口) + * 注意:此实现依赖自动设备切换,手动设置可能被热插拔覆盖 */ fun setAudioOutputDevice(device: AudioOutputDevice) { - if (audioOutputDevice != device) { - audioOutputDevice = device - - // 应用新的音频路由设置 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - // API 31+ 可以动态更改设备 - setPreferredDeviceForTrack() - } else { - // API 30 及以下需要重新配置 - configureAudioRouting() - } - } + // 本实现专注于自动设备切换 + // 手动设置会在下次设备变化时被自动逻辑覆盖 + Log.d(TAG, "setAudioOutputDevice被调用,但本实现依赖自动设备切换") } - /** - * 初始化 AudioTrack + * 获取当前路由设备信息(用于调试) */ - private fun initAudioTrack() { - audioTrack?.release() - - val minBufferSize = AudioTrack.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT) - val bufferSize = minBufferSize * 2 // 使用较小的缓冲区以降低延迟 - - audioTrack = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val builder = AudioTrack.Builder() - .setAudioAttributes( - AudioAttributes.Builder() - .setUsage( - // 扬声器模式下默认使用语音通信模式以启用回声消除 - if (audioOutputDevice == AudioOutputDevice.SPEAKER) - AudioAttributes.USAGE_VOICE_COMMUNICATION - else AudioAttributes.USAGE_MEDIA - ) - .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) - .build() - ) - .setAudioFormat( - AudioFormat.Builder() - .setEncoding(AUDIO_FORMAT) - .setSampleRate(SAMPLE_RATE) - .setChannelMask(CHANNEL_CONFIG) - .build() - ) - .setBufferSizeInBytes(bufferSize) - .setTransferMode(AudioTrack.MODE_STREAM) - - // API 26+ 设置低延迟模式 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - builder.setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY) - } - - builder.build() + fun getCurrentDevice(): String { + val routedDevice = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + audioTrack?.routedDevice } else { - @Suppress("DEPRECATION") - AudioTrack( - AudioManager.STREAM_MUSIC, - SAMPLE_RATE, - CHANNEL_CONFIG, - AUDIO_FORMAT, - bufferSize, - AudioTrack.MODE_STREAM - ) - } - - audioTrack?.setPlaybackPositionUpdateListener(playbackListener) - - // 设置音量以减少回音 - audioTrack?.setVolume(VOLUME_REDUCTION) - - 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 - - audioManager?.let { manager -> - audioTrack?.let { track -> - when (audioOutputDevice) { - AudioOutputDevice.DEFAULT -> { - track.setPreferredDevice(null) - manager.isSpeakerphoneOn = false - // 默认模式下使用通信模式以获得更好的音频处理 - manager.mode = AudioManager.MODE_IN_COMMUNICATION - } - - AudioOutputDevice.SPEAKER -> { - val speaker = manager.getDevices(AudioManager.GET_DEVICES_OUTPUTS) - .firstOrNull { it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER } - speaker?.let { track.setPreferredDevice(it) } - // 扬声器模式下始终启用通信模式以获得回声消除 - manager.mode = AudioManager.MODE_IN_COMMUNICATION - manager.isSpeakerphoneOn = true - } - - 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) - // 耳机模式下可以使用普通模式 - manager.mode = AudioManager.MODE_NORMAL - manager.isSpeakerphoneOn = false - } - } - } - } - } - - /** - * 配置音频路由(API 30 及以下) - */ - private fun configureAudioRouting() { - audioManager?.let { manager -> - when (audioOutputDevice) { - AudioOutputDevice.DEFAULT -> { - // 默认模式下使用通信模式以获得更好的音频处理 - manager.mode = AudioManager.MODE_IN_COMMUNICATION - manager.isSpeakerphoneOn = false - } - - AudioOutputDevice.SPEAKER -> { - // 扬声器模式下始终启用通信模式以获得回声消除 - manager.mode = AudioManager.MODE_IN_COMMUNICATION - manager.isSpeakerphoneOn = true - } - - AudioOutputDevice.HEADPHONES -> { - // 耳机模式下可以使用普通模式 - manager.mode = AudioManager.MODE_NORMAL - 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) + 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() + val preferredDevice = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + audioTrack?.preferredDevice } else { - configureAudioRouting() + null } - } - - /** - * 切换到扬声器模式 - */ - private fun switchToSpeakerMode() { - audioOutputDevice = AudioOutputDevice.SPEAKER - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - setPreferredDeviceForTrack() - } else { - configureAudioRouting() - } + return "路由设备: ${routedDevice?.productName}, 首选设备: ${preferredDevice?.productName}" } } \ No newline at end of file diff --git a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt index b3cd7e75f..bb446a994 100644 --- a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt +++ b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt @@ -117,8 +117,8 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope { private var appId: String = "" private var accessToken: String = "" private var speaker: String = "zh_female_shuangkuaisisi_emo_v2_mars_bigtts" // 默认语音,使用Java示例的音色 - private var format: String = "pcm" // 使用mp3格式 - private var sampleRate: Int = 24000 // 使用24kHz采样率 + private var format: String = "pcm" // pcm 格式 + private var sampleRate: Int = 16000 // 使用16kHz采样率 private var enableTimestamp: Boolean = false private var language: String = "zh-CN"