45 changed files with 1658 additions and 1043 deletions
@ -0,0 +1,329 @@ |
|||||
|
package com.deep_voice.bytedance_speech |
||||
|
|
||||
|
import android.media.AudioAttributes |
||||
|
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 kotlinx.coroutines.CoroutineScope |
||||
|
import kotlinx.coroutines.Dispatchers |
||||
|
import kotlinx.coroutines.Job |
||||
|
import kotlinx.coroutines.SupervisorJob |
||||
|
import kotlinx.coroutines.cancelChildren |
||||
|
import kotlinx.coroutines.delay |
||||
|
import kotlinx.coroutines.isActive |
||||
|
import kotlinx.coroutines.launch |
||||
|
import kotlinx.coroutines.withContext |
||||
|
import java.util.concurrent.ConcurrentLinkedQueue |
||||
|
import java.util.concurrent.atomic.AtomicBoolean |
||||
|
import java.util.concurrent.CancellationException |
||||
|
/** |
||||
|
* 字节跳动语音合成音频播放器 |
||||
|
* |
||||
|
* 使用AudioTrack播放PCM格式音频流,基于Kotlin协程实现异步处理 |
||||
|
*/ |
||||
|
class BytedanceAudioPlayer : AudioDataListener, CoroutineScope { |
||||
|
companion object { |
||||
|
private const val TAG = "BytedanceAudioPlayer" |
||||
|
private const val SAMPLE_RATE = 24000 // 样本率 |
||||
|
private const val CHANNEL_CONFIG = AudioFormat.CHANNEL_OUT_MONO // 单声道 |
||||
|
private const val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT // 16位PCM |
||||
|
private const val IDLE_TIMEOUT_MS = 800L // 无数据超时时间 |
||||
|
} |
||||
|
|
||||
|
// 协程相关 |
||||
|
private val job = SupervisorJob() |
||||
|
override val coroutineContext = Dispatchers.IO + job |
||||
|
|
||||
|
// 音频处理相关 |
||||
|
private var audioTrack: AudioTrack? = null |
||||
|
private val isPlaying = AtomicBoolean(false) |
||||
|
private val isPaused = AtomicBoolean(false) |
||||
|
|
||||
|
// 播放状态 |
||||
|
private val playStarted = AtomicBoolean(false) |
||||
|
private val completionReported = AtomicBoolean(false) |
||||
|
|
||||
|
// 使用简单的并发队列,保证线程安全 |
||||
|
private val audioDataQueue = ConcurrentLinkedQueue<ByteArray>() |
||||
|
|
||||
|
// 播放状态监听 |
||||
|
private var onPlayStarted: (() -> Unit)? = null |
||||
|
private var onPlayCompleted: (() -> Unit)? = null |
||||
|
private var onError: ((String) -> Unit)? = null |
||||
|
|
||||
|
// 处理协程 |
||||
|
private var processingJob: Job? = null |
||||
|
private var lastDataTime = 0L |
||||
|
|
||||
|
init { |
||||
|
Log.d(TAG, "BytedanceAudioPlayer初始化") |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 开始播放 |
||||
|
*/ |
||||
|
fun start() { |
||||
|
if (isPlaying.getAndSet(true)) return |
||||
|
|
||||
|
isPaused.set(false) |
||||
|
playStarted.set(false) |
||||
|
completionReported.set(false) |
||||
|
lastDataTime = System.currentTimeMillis() |
||||
|
|
||||
|
processingJob = launch { |
||||
|
try { |
||||
|
initAudioTrack() |
||||
|
|
||||
|
// 处理队列中的音频数据 |
||||
|
while (isActive && isPlaying.get()) { |
||||
|
processQueuedAudio() |
||||
|
|
||||
|
// 检查是否播放完成 |
||||
|
checkPlaybackCompletion() |
||||
|
|
||||
|
// 队列为空时短暂延迟 |
||||
|
if (audioDataQueue.isEmpty()) { |
||||
|
delay(10) |
||||
|
} |
||||
|
} |
||||
|
} catch (e: CancellationException) { |
||||
|
// 协程被取消,正常行为 |
||||
|
} catch (e: Exception) { |
||||
|
Log.e(TAG, "播放错误: ${e.message}") |
||||
|
onError?.invoke("播放错误: ${e.message}") |
||||
|
stopInternal(false) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 暂停播放 |
||||
|
*/ |
||||
|
fun pause() { |
||||
|
if (!isPlaying.get() || isPaused.getAndSet(true)) return |
||||
|
audioTrack?.pause() |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 恢复播放 |
||||
|
*/ |
||||
|
fun resume() { |
||||
|
if (!isPlaying.get() || !isPaused.getAndSet(false)) return |
||||
|
audioTrack?.play() |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 停止播放 |
||||
|
*/ |
||||
|
fun stop() { |
||||
|
Log.d(TAG, "停止播放") |
||||
|
stopInternal(true) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 内部停止处理 |
||||
|
*/ |
||||
|
private fun stopInternal(reportCompletion: Boolean) { |
||||
|
if (!isPlaying.getAndSet(false)) return |
||||
|
|
||||
|
isPaused.set(false) |
||||
|
processingJob?.cancel() |
||||
|
|
||||
|
audioTrack?.stop() |
||||
|
// 不在stopInternal中释放资源,只停止播放 |
||||
|
|
||||
|
// 清空队列 |
||||
|
audioDataQueue.clear() |
||||
|
|
||||
|
// 播放完成通知 |
||||
|
if (reportCompletion && playStarted.get() && !completionReported.getAndSet(true)) { |
||||
|
launch(Dispatchers.Main) { |
||||
|
onPlayCompleted?.invoke() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 释放资源 |
||||
|
*/ |
||||
|
fun release() { |
||||
|
stopInternal(false) |
||||
|
|
||||
|
// 在release方法中释放AudioTrack资源 |
||||
|
audioTrack?.release() |
||||
|
audioTrack = null |
||||
|
|
||||
|
// 释放协程资源 |
||||
|
job.cancel() |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置播放开始回调 |
||||
|
*/ |
||||
|
fun setOnPlayStarted(listener: () -> Unit) { |
||||
|
onPlayStarted = listener |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置播放完成回调 |
||||
|
*/ |
||||
|
fun setOnPlayCompleted(listener: () -> Unit) { |
||||
|
onPlayCompleted = listener |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置错误回调 |
||||
|
*/ |
||||
|
fun setOnError(listener: (String) -> Unit) { |
||||
|
onError = listener |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 接收音频数据(实现AudioDataListener接口) |
||||
|
*/ |
||||
|
override fun onAudioData(data: ByteArray) { |
||||
|
if (data.isEmpty()) return |
||||
|
|
||||
|
lastDataTime = System.currentTimeMillis() |
||||
|
|
||||
|
if (!isPlaying.get()) { |
||||
|
start() |
||||
|
} |
||||
|
|
||||
|
// 添加到队列 - 简单有效,保证FIFO顺序 |
||||
|
audioDataQueue.add(data.copyOf()) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 检查是否正在播放 |
||||
|
*/ |
||||
|
fun isPlaying(): Boolean = isPlaying.get() && !isPaused.get() |
||||
|
|
||||
|
/** |
||||
|
* 处理队列中的音频数据 |
||||
|
*/ |
||||
|
private suspend fun processQueuedAudio() { |
||||
|
if (isPaused.get() || !isPlaying.get() || !isActive) return |
||||
|
|
||||
|
// 获取并处理队列中的数据 |
||||
|
val data = audioDataQueue.poll() ?: return |
||||
|
|
||||
|
// 写入音频数据 |
||||
|
val result = audioTrack?.write(data, 0, data.size) ?: 0 |
||||
|
|
||||
|
if (result > 0) { |
||||
|
// 标记播放开始 |
||||
|
if (!playStarted.getAndSet(true)) { |
||||
|
withContext(Dispatchers.Main) { |
||||
|
onPlayStarted?.invoke() |
||||
|
} |
||||
|
} |
||||
|
} else if (result < 0) { |
||||
|
// 处理错误 |
||||
|
handleAudioTrackError(result) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 检查播放是否完成(超时无数据) |
||||
|
*/ |
||||
|
private suspend fun checkPlaybackCompletion() { |
||||
|
val currentTime = System.currentTimeMillis() |
||||
|
|
||||
|
// 超时判断 - 队列为空且超过超时时间 |
||||
|
if (playStarted.get() && audioDataQueue.isEmpty() && |
||||
|
currentTime - lastDataTime > IDLE_TIMEOUT_MS && isPlaying.get()) { |
||||
|
|
||||
|
// 播放完成 |
||||
|
if (!completionReported.getAndSet(true)) { |
||||
|
withContext(Dispatchers.Main) { |
||||
|
onPlayCompleted?.invoke() |
||||
|
} |
||||
|
stopInternal(false) // 已报告,不需要再次报告 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 处理AudioTrack错误 |
||||
|
*/ |
||||
|
private fun handleAudioTrackError(result: Int) { |
||||
|
val errorMsg = when(result) { |
||||
|
AudioTrack.ERROR_INVALID_OPERATION -> "无效操作" |
||||
|
AudioTrack.ERROR_BAD_VALUE -> "参数错误" |
||||
|
AudioTrack.ERROR_DEAD_OBJECT -> "对象已销毁" |
||||
|
else -> "未知错误" |
||||
|
} |
||||
|
|
||||
|
// 对象已销毁时尝试重建 |
||||
|
if (result == AudioTrack.ERROR_DEAD_OBJECT) { |
||||
|
try { |
||||
|
audioTrack?.release() |
||||
|
initAudioTrack() |
||||
|
} catch (e: Exception) { |
||||
|
Log.e(TAG, "重建AudioTrack失败: ${e.message}") |
||||
|
throw e |
||||
|
} |
||||
|
} else { |
||||
|
Log.e(TAG, "AudioTrack错误: $errorMsg") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 初始化AudioTrack |
||||
|
*/ |
||||
|
private fun initAudioTrack() { |
||||
|
val minBufferSize = AudioTrack.getMinBufferSize( |
||||
|
SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT |
||||
|
) |
||||
|
|
||||
|
if (minBufferSize == AudioTrack.ERROR || minBufferSize == AudioTrack.ERROR_BAD_VALUE) { |
||||
|
throw IllegalStateException("无法获取有效的音频缓冲区大小") |
||||
|
} |
||||
|
|
||||
|
// 使用较大的缓冲区提高稳定性 |
||||
|
val bufferSize = minBufferSize * 4 |
||||
|
|
||||
|
// 创建AudioTrack |
||||
|
audioTrack = if (Build.VERSION.SDK_INT >= 23) { // Android M (6.0) |
||||
|
AudioTrack.Builder() |
||||
|
.setAudioAttributes( |
||||
|
AudioAttributes.Builder() |
||||
|
.setUsage(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) |
||||
|
.build() |
||||
|
} else { |
||||
|
@Suppress("DEPRECATION") |
||||
|
AudioTrack( |
||||
|
AudioManager.STREAM_MUSIC, |
||||
|
SAMPLE_RATE, |
||||
|
CHANNEL_CONFIG, |
||||
|
AUDIO_FORMAT, |
||||
|
bufferSize, |
||||
|
AudioTrack.MODE_STREAM |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// 检查初始化状态 |
||||
|
if (audioTrack?.state != AudioTrack.STATE_INITIALIZED) { |
||||
|
throw IllegalStateException("AudioTrack初始化失败") |
||||
|
} |
||||
|
|
||||
|
// 开始播放 |
||||
|
audioTrack?.play() |
||||
|
} |
||||
|
} |
||||
Binary file not shown.
Binary file not shown.
@ -1,2 +1,2 @@ |
|||||
#Sun May 11 13:39:53 IST 2025 |
#Fri May 16 09:26:08 IST 2025 |
||||
gradle.version=8.10 |
gradle.version=8.10 |
||||
|
|||||
@ -1 +1 @@ |
|||||
rootProject.name = "open_ai_service" |
rootProject.name = "speech" |
||||
Loading…
Reference in new issue