Files
sundynix-agentix/sundynix-gateway/internal/voice/frame.go
T
Blizzard b526b21dee feat(voice): 火山语音 WS 二进制帧编解码(ASR/TTS 共用核心)
从官方参考实现核实的 V3 协议帧格式,港到 Go:byte0=0x11、byte1=(msgType<<4)|flags、
byte2=序列化<<4|压缩、byte3=0x00、大端 uint32 payload 长度、payload;服务端响应含 4B
序列号故 payload 从第 12 字节起。encodeFrame/jsonFrame/audioFrame/parseServerFrame +
msgType(0x01 config/0x02 音频/0x09 结果/0x0F 错误)与 flags(0x02 最终)常量。
3 个单测钉死头布局/音频最终帧/响应解析(确定性,不依赖网络)。

ASR 初始配置 JSON、WS 连接与流式识别(接 onAudio)是下一步网络层实现。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 17:22:09 +08:00

76 lines
2.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package voice
import "encoding/binary"
// 火山语音 WebSocket 二进制帧编解码(ASR / TTS 共用)。协议(从官方参考实现核实):
//
// byte0 = 0x11 版本(0001) + 头长(0001=4字节)
// byte1 = (msgType<<4) | flags
// byte2 = (serialization<<4) | compression
// byte3 = 0x00 保留
// byte4..7 = uint32 大端 payload 长度
// byte8.. = payloadJSON 或 raw 音频)
//
// 服务端响应帧含 4 字节序列号,故 payload 从第 12 字节起(header4 + seq4 + size4)。
// 消息类型(高 4bit)。
const (
msgFullClientReq byte = 0x01 // 初始配置请求(JSON
msgAudioReq byte = 0x02 // 音频帧
msgServerResp byte = 0x09 // 服务端识别/合成结果
msgServerError byte = 0x0F // 服务端错误
)
// flags(低 4bit)。
const (
flagNone byte = 0x00
flagLast byte = 0x02 // 最终/结束帧(最后一帧音频、或收尾)
)
// 序列化方式(byte2 高 4bit)。
const (
serialRaw byte = 0x00 // raw(音频帧)
serialJSON byte = 0x01 // JSON(配置/文本)
)
const compNone byte = 0x00 // 不压缩
// encodeFrame 组装一帧。JSON payload 用 serialJSONraw 音频用 serialRaw。
func encodeFrame(msgType, flags, serialization byte, payload []byte) []byte {
buf := make([]byte, 8+len(payload))
buf[0] = 0x11
buf[1] = (msgType << 4) | (flags & 0x0F)
buf[2] = (serialization << 4) | compNone
buf[3] = 0x00
binary.BigEndian.PutUint32(buf[4:8], uint32(len(payload)))
copy(buf[8:], payload)
return buf
}
// jsonFrame 组装一帧 JSON 消息(如 ASR 初始配置、TTS 文本)。
func jsonFrame(msgType, flags byte, payload []byte) []byte {
return encodeFrame(msgType, flags, serialJSON, payload)
}
// audioFrame 组装一帧 raw 音频;last=true 时打最终标记。
func audioFrame(pcm []byte, last bool) []byte {
flags := flagNone
if last {
flags = flagLast
}
return encodeFrame(msgAudioReq, flags, serialRaw, pcm)
}
// parseServerFrame 解析服务端帧:返回消息类型 + payload。响应含 4B 序列号 → payload 从第 12 字节起。
// 帧不足以解析时 ok=false。
func parseServerFrame(data []byte) (msgType byte, payload []byte, ok bool) {
if len(data) < 4 {
return 0, nil, false
}
msgType = (data[1] >> 4) & 0x0F
if len(data) >= 12 {
return msgType, data[12:], true
}
return msgType, nil, true // 无 payload 的控制帧(如纯确认)
}