Browse Source

add

newdev_shunjiawei
wolfplus2048 1 year ago
parent
commit
a7ebcfeb00
  1. 444
      local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt
  2. 4
      local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt

444
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<AudioDeviceInfo>) {
Log.d(TAG, "音频设备连接: ${addedDevices.joinToString { "${it.productName}(${it.type})" }}")
rebuildIfNeeded()
}
override fun onAudioDevicesRemoved(removedDevices: Array<AudioDeviceInfo>) {
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<AudioDeviceInfo>) {
super.onAudioDevicesAdded(addedDevices)
handleDeviceAdded(addedDevices)
}
override fun onAudioDevicesRemoved(removedDevices: Array<AudioDeviceInfo>) {
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<AudioDeviceInfo>) {
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<AudioDeviceInfo>) {
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}"
}
}

4
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"

Loading…
Cancel
Save