Files
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

63 lines
1.9 KiB
Go

package voice
import (
"bytes"
"encoding/binary"
"testing"
)
// 帧头布局必须精确匹配火山协议(错一个 bit 服务端就拒)。
func TestEncodeFrame_HeaderLayout(t *testing.T) {
f := jsonFrame(msgFullClientReq, flagNone, []byte(`{"a":1}`))
if f[0] != 0x11 {
t.Fatalf("byte0 应 0x11,得 %#x", f[0])
}
if f[1] != (msgFullClientReq<<4)|flagNone { // 0x10
t.Fatalf("byte1 应 0x10,得 %#x", f[1])
}
if f[2] != (serialJSON<<4)|compNone { // 0x10
t.Fatalf("byte2(JSON) 应 0x10,得 %#x", f[2])
}
if got := binary.BigEndian.Uint32(f[4:8]); got != 7 {
t.Fatalf("payload 长度应 7,得 %d", got)
}
if !bytes.Equal(f[8:], []byte(`{"a":1}`)) {
t.Fatalf("payload 错:%s", f[8:])
}
}
// 音频帧:raw 序列化 + 最终标记。
func TestAudioFrame_LastFlag(t *testing.T) {
f := audioFrame([]byte{1, 2, 3}, true)
if f[1] != (msgAudioReq<<4)|flagLast { // 0x22
t.Fatalf("音频最终帧 byte1 应 0x22,得 %#x", f[1])
}
if f[2] != (serialRaw<<4)|compNone { // 0x00
t.Fatalf("音频 byte2 应 0x00(raw),得 %#x", f[2])
}
non := audioFrame([]byte{1}, false)
if non[1]&0x0F != flagNone {
t.Fatalf("非最终帧 flags 应 0,得 %#x", non[1]&0x0F)
}
}
// 服务端帧解析:跳 12 字节头(含序列号)取 payload;识别结果类型 0x09。
func TestParseServerFrame(t *testing.T) {
// 造一个响应帧:header(4) + seq(4) + size(4) + payload
body := []byte(`{"type":"final"}`)
resp := make([]byte, 12+len(body))
resp[1] = (msgServerResp << 4) // 0x90
copy(resp[12:], body)
mt, payload, ok := parseServerFrame(resp)
if !ok || mt != msgServerResp {
t.Fatalf("应解析出 msgServerResp,得 mt=%#x ok=%v", mt, ok)
}
if !bytes.Equal(payload, body) {
t.Fatalf("payload 应从第 12 字节起,得 %s", payload)
}
// 过短帧不 panic
if _, _, ok := parseServerFrame([]byte{0x11}); ok {
t.Fatal("过短帧应 ok=false")
}
}