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 |
|||
|
|||
@ -1 +1 @@ |
|||
rootProject.name = "open_ai_service" |
|||
rootProject.name = "speech" |
|||
Loading…
Reference in new issue