|
|
发表于 2026-5-31 07:52:25
|
显示全部楼层
一、核心原理
Coze 返回的 conversation.audio.delta 事件里,data.content 是 PCM 音频片段的 Base64 编码:
格式:24kHz, 16bit, 单声道, Little-Endian 的原始 PCM 数据片段
特点:流式分块返回,不是完整文件,无法直接用 Audio 组件播放,需要自己做「解码→缓冲→流式播放」
处理流程(App 端通用):
Base64 解码:把字符串还原成二进制 PCM 数据
入队缓冲:按顺序存入播放队列,避免乱序
流式播放:用 AudioContext(iOS/Android/JS)或 AVAudioPlayer 处理 PCM 流
二、通用处理步骤
1. 解析 WebSocket 事件
收到消息后,先判断事件类型:
if (event.data.event_type === "conversation.audio.delta") {
const base64Str = event.data.content;
// 下一步:解码 + 入队
}
2. Base64 解码
把 Base64 字符串转成二进制 PCM 数据:
// JS/TS
function base64ToPCM(base64Str) {
const binaryString = atob(base64Str);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
三、App 端播放实现(分平台)
方案 1:iOS(Swift)
解码 Base64:
swift
func base64ToPCM(_ base64Str: String) -> Data? {
return Data(base64Encoded: base64Str)
}
使用 AVAudioEngine 流式播放 PCM:
swift
let format = AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: 24000, channels: 1, interleaved: true)!
let audioEngine = AVAudioEngine()
let playerNode = AVAudioPlayerNode()
audioEngine.attach(playerNode)
audioEngine.connect(playerNode, to: audioEngine.outputNode, format: format)
try? audioEngine.start()
// 收到数据时入队
func enqueuePCMData(_ data: Data) {
let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: UInt32(data.count / 2))!
buffer.frameLength = buffer.frameCapacity
let channelData = buffer.int16ChannelData![0]
data.withUnsafeBytes {
memcpy(channelData, $0.baseAddress, data.count)
}
playerNode.scheduleBuffer(buffer)
if !playerNode.isPlaying {
playerNode.play()
}
}
方案 2:Android(Kotlin)
解码 Base64:
kotlin
fun base64ToPCM(base64Str: String): ByteArray {
return Base64.decode(base64Str, Base64.DEFAULT)
}
使用 AudioTrack 播放 PCM:
kotlin
val sampleRate = 24000
val channelConfig = AudioFormat.CHANNEL_OUT_MONO
val audioFormat = AudioFormat.ENCODING_PCM_16BIT
val bufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat)
val audioTrack = AudioTrack(
AudioManager.STREAM_MUSIC,
sampleRate,
channelConfig,
audioFormat,
bufferSize,
AudioTrack.MODE_STREAM
)
audioTrack.play()
// 收到数据时写入
fun writePCMData(data: ByteArray) {
audioTrack.write(data, 0, data.size)
}
方案 3:Uniapp / 小程序(兼容 iOS/Android)
解码 + 缓冲队列:
const audioQueue = [];
let isPlaying = false;
function processAudioDelta(base64Str) {
const bytes = base64ToPCM(base64Str);
audioQueue.push(bytes);
if (!isPlaying) playNext();
}
使用 innerAudioContext + 转 WAV(兼容方案):
因为小程序无法直接播放 PCM,需要把片段拼接成完整 WAV 文件后再播放:
function pcmToWAV(pcmData) {
// 构造 WAV 头(24kHz, 16bit, 单声道)
const header = new Uint8Array(44);
header.set([0x52,0x49,0x46,0x46], 0); // RIFF
const fileLength = pcmData.length + 36;
header.set([fileLength & 0xff, (fileLength >> 8) & 0xff, (fileLength >> 16) & 0xff, (fileLength >> 24) & 0xff], 4);
header.set([0x57,0x41,0x56,0x45], 8); // WAVE
header.set([0x66,0x6D,0x74,0x20], 12); // fmt
header.set([16,0,0,0], 16); // fmt length
header.set([1,0], 20); // PCM
header.set([1,0], 22); // mono
header.set([0x80,0xbb,0,0], 24); // 24000
header.set([0,0x77,0x01,0], 28); // byte rate
header.set([2,0], 32); // block align
header.set([16,0], 34); // bit depth
header.set([0x64,0x61,0x74,0x61], 36); // data
const dataLength = pcmData.length;
header.set([dataLength & 0xff, (dataLength >> 8) & 0xff, (dataLength >> 16) & 0xff, (dataLength >> 24) & 0xff], 40);
const wav = new Uint8Array(44 + pcmData.length);
wav.set(header, 0);
wav.set(pcmData, 44);
return wav;
}
四、关键注意事项
音频格式确认:Coze 默认返回的是 24kHz, 16bit, 单声道 PCM,你的播放器必须和这个格式一致,否则会出现杂音 / 播放失败。
队列管理:收到的 delta 是分块的,必须按顺序入队,不能乱序播放。
播放中断:收到新的对话请求时,要清空队列并停止当前播放,避免多段音频叠加。
iOS 权限:需要申请 AVAudioSession 权限,并设置 playback 模式。 |
|