通信协议设计 (v4.0)
1. 概述
本协议描述车载音频 DSP 调音工具的三层通信体系:
- 前端 ↔ 后端:Vue3 前端与 ASP.NET Core 8 后端之间的 WebSocket JSON 协议,以及 REST 补充接口
- 后端 ↔ DSP:后端与 DSP 硬件(或 PC 仿真)之间的 TLV 二进制帧协议
- 参数 ID 命名规范:贯穿前后端和 DSP 的参数寻址格式
┌──────────────────────────────────────────┐
│ Frontend (Vue3 + Pinia) │
│ ┌────────────┐ ┌────────┐ ┌──────────┐ │
│ │Chain Builder│ │ Tuning │ │Test/Verify│ │
│ └────────────┘ └────────┘ └──────────┘ │
└────────────────┬─────────────────────────┘
│ JSON / WebSocket (ws://0.0.0.0:5000/ws)
│ HTTP REST (/api/*)
┌────────────────▼─────────────────────────┐
│ Backend (ASP.NET Core 8) │
│ ┌──────────┐ ┌──────┐ ┌─────────────┐ │
│ │WebSocket │ │ REST │ │AudioEngine │ │
│ │ Handler │ │ API │ │(NAudio+DLL) │ │
│ └──────────┘ └──────┘ └─────────────┘ │
│ ┌────────────────────────────────────┐ │
│ │ DebugService │ │
│ │ (metrics / log_stream / WAV) │ │
│ └────────────────────────────────────┘ │
└──────┬───────────────────────┬────────────┘
│ Binary / TCP or Serial │ DLL call (in-process)
┌──────▼──────┐ ┌──────▼──────────────┐
│ DSP Hardware│ │ PC Sim │
│ (Embedded) │ │ DynamicChain.dll │
└─────────────┘ └─────────────────────┘
连接信息
| 项目 | 值 |
|---|---|
| 协议 | WebSocket |
| 地址 | ws://0.0.0.0:5000/ws |
| 文本编码 | UTF-8 |
| 最大帧大小 | 65536 bytes |
| 心跳周期 | 5 秒(前端发送 ping,后端回 pong) |
| 重连策略 | 断线后 3s 自动重试,指数退避最大 30s |
消息格式规范
每条 WebSocket 消息必须包含 type 字段:
2. 前端 → 后端 消息(完整列表)
2.1 基础通信
ping — 心跳探测
响应:pong
2.2 参数操作
set_param — 设置单个参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
instanceId |
string | 是 | 模块实例 ID(格式:moduleType#index) |
paramId |
string | 是 | 参数名(不含通道后缀) |
value |
number / bool / string | 是 | 参数值(UI 单位) |
channel |
int | 可选 | 通道索引(存在时 key 追加 #channel) |
后端存储 key:${instanceId}.${paramId} 或 ${instanceId}.${paramId}#${channel}
响应:set_param_ack
副作用:
1. 触发 DSP 二进制帧发送(异步,不阻塞响应)
2. 广播 param_update 给其他所有已连接客户端
set_params — 兼容别名
与 set_param 相同格式,后端视为等价处理(向后兼容)。
get_param — 查询单个参数
响应:get_param_ack
get_all_params — 查询实例全部参数
用于前端重连后恢复该模块的所有参数状态。
响应:get_all_params_ack
2.3 链路管理
write_link — 保存并下发链路配置
从 v3.0 起,write_link 消息的 payload 字段从旧版扁平 DSPLink 格式升级为 LinkConfig v2.2 格式,支持多端口 mixer 节点与子图(SubGraph)嵌套。完整示例如下:
{
"type": "write_link",
"payload": {
"version": "2.2",
"rootChainId": "root",
"global": { "channels": 20, "sampleRate": 48000, "frameSize": 240 },
"chains": {
"root": {
"id": "root",
"name": "Main Chain",
"nodes": [
{
"instanceId": "gain#1",
"moduleType": "channel_gain_v1",
"order": 0,
"enabled": true,
"position": {"x": 100, "y": 200},
"ports": [
{"id":"input","direction":"input","channels":-1,"sampleRate":-1,"label":"Input","required":true},
{"id":"output","direction":"output","channels":-1,"sampleRate":-1,"label":"Output","required":true}
]
},
{
"instanceId": "mixer#1",
"moduleType": "audio_mixer_v1",
"order": 1,
"enabled": true,
"position": {"x": 400, "y": 200},
"ports": [
{"id":"input_0","direction":"input","channels":-1,"sampleRate":-1,"label":"Ch A","required":true},
{"id":"input_1","direction":"input","channels":-1,"sampleRate":-1,"label":"Ch B","required":false},
{"id":"output","direction":"output","channels":-1,"sampleRate":-1,"label":"Mix","required":true}
]
},
{
"instanceId": "group#1",
"moduleType": "subgraph",
"subGraphId": "eq_stage",
"position": {"x": 700, "y": 200},
"ports": [
{"id":"input","direction":"input","channels":2,"sampleRate":48000,"label":"In","required":true},
{"id":"output","direction":"output","channels":2,"sampleRate":48000,"label":"Out","required":true}
]
}
],
"edges": [
{"id":"e1","fromModule":"gain#1","fromPort":"output","toModule":"mixer#1","toPort":"input_0","channels":2,"sampleRate":48000},
{"id":"e2","fromModule":"mixer#1","fromPort":"output","toModule":"group#1","toPort":"input","channels":2,"sampleRate":48000}
]
},
"eq_stage": {
"id": "eq_stage",
"name": "EQ Stage",
"externalPorts": [
{"id":"input","direction":"input","channels":2,"sampleRate":48000,"label":"In","required":true},
{"id":"output","direction":"output","channels":2,"sampleRate":48000,"label":"Out","required":true}
],
"nodes": [
{
"instanceId": "eq#1",
"moduleType": "common_eq_v1",
"order": 0,
"enabled": true,
"position": {"x": 200, "y": 150},
"ports": [
{"id":"input","direction":"input","channels":-1,"sampleRate":-1,"label":"In","required":true},
{"id":"output","direction":"output","channels":-1,"sampleRate":-1,"label":"Out","required":true}
]
}
],
"edges": []
}
}
}
}
后端处理:
1. 将 payload 序列化为 JSON,写入 ./data/current_link.json
2. 调用 ChainFlattener 展平所有子图,构造 set_link v3 二进制帧下发到 DSP
3. 批量提取各链路节点参数,更新 paramStore
4. 下发各模块初始参数(set_param 帧序列)
响应:write_link_ack
ChainDefinition 字段说明
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id |
string | 是 | 链路唯一标识符,与 chains 对象的 key 相同 |
name |
string | 否 | 可读名称,仅供 UI 显示 |
nodes |
ChainNode[] | 是 | 本链路内的所有节点列表 |
edges |
ChainEdge[] | 是 | 本链路内的所有连线列表 |
externalPorts |
PortDescriptor[] | 否 | 仅子图链路需要,定义子图对外暴露的端口 |
ChainNode 字段说明
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
instanceId |
string | 是 | 模块实例 ID,同一链路内唯一 |
moduleType |
string | 是 | 模块类型字符串;子图节点固定为 "subgraph" |
subGraphId |
string | 子图节点必填 | 指向 chains 中对应子图的 id |
order |
int | 否 | 处理顺序(升序),子图节点可省略 |
enabled |
bool | 否 | 模块是否启用,默认 true |
position |
{x, y} | 否 | 画布坐标,仅供 UI 使用 |
ports |
PortDescriptor[] | 是 | 本节点的端口描述列表 |
PortDescriptor 字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
id |
string | 端口 ID,在同一节点内唯一(如 "input_0"、"output") |
direction |
string | "input" 或 "output" |
channels |
int | 端口通道数;-1 表示继承全局/上游通道数 |
sampleRate |
int | 端口采样率(Hz);-1 表示继承 |
label |
string | UI 显示标签 |
required |
bool | 是否为必连端口;false 表示可选输入 |
SubGraphNode 补充说明
SubGraphNode 是 ChainNode 的子类型,当 moduleType == "subgraph" 时生效:
| 字段 | 类型 | 说明 |
|---|---|---|
subGraphId |
string | 引用 LinkConfig.chains 中的子图 id |
ports |
PortDescriptor[] | 与被引用子图的 externalPorts 一一对应,通道/采样率已解析 |
后端 ChainFlattener 会将子图节点递归展开为平坦模块列表,子图内部节点的 instanceId 在展平后采用点分路径格式(如 "group#1.eq#1")。
ChainEdge 字段说明
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id |
string | 是 | 边的唯一 ID(同一链路内唯一) |
fromModule |
string | 是 | 源节点 instanceId |
fromPort |
string | 是 | 源节点端口 ID |
toModule |
string | 是 | 目标节点 instanceId |
toPort |
string | 是 | 目标节点端口 ID |
channels |
int | 否 | 该连线上已解析的通道数 |
sampleRate |
int | 否 | 该连线上已解析的采样率(Hz) |
read_link — 加载链路配置
从 ./data/current_link.json 加载链路 JSON。
响应:read_link_ack
2.4 DSP 连接
connect_dsp — 连接 DSP 硬件
TCP 模式:
串口模式:
响应:connect_dsp_ack
副作用:连接成功后广播 dsp_status 给所有客户端
disconnect_dsp — 断开 DSP 连接
响应:disconnect_dsp_ack
副作用:广播 dsp_status 给所有客户端
get_status — 查询后端及 DSP 状态
响应:dsp_status
get_dsp_chain — 从 DSP 读取链路结构(仅 test_verify 模式)
从 DSP 实时读取当前运行的链路结构(只读):
响应:get_dsp_chain_ack
2.5 工作模式
set_mode — 切换工作模式
切换工作模式,广播给所有连接客户端:
mode 取值 |
说明 |
|---|---|
chain_builder |
链路构建模式(全功能,可增删模块、修改连接) |
tuning |
调音模式(链路锁定,仅 tunable 参数可调) |
test_verify |
测试验证模式(链路只读,可读写 tunable 参数,可读取 DSP 链路) |
响应:mode_changed(广播给所有客户端)
2.6 PC 仿真
sim_start — 启动 PC 端算法仿真
后端调用 DynamicChain.dll + NAudio 启动音频仿真处理:
{
"type": "sim_start",
"input": {
"type": "soundcard",
"deviceName": "立体声混音 (Realtek HD Audio)"
},
"output": {
"type": "soundcard",
"deviceName": "扬声器 (Realtek HD Audio)"
}
}
文件输入模式:
{
"type": "sim_start",
"input": { "type": "file", "filePath": "./test_audio/white_noise_2ch.wav" },
"output": { "type": "soundcard", "deviceName": "扬声器" }
}
响应:sim_status(广播给所有客户端)
sim_stop — 停止 PC 端算法仿真
响应:sim_status(广播给所有客户端)
list_audio_devices — 枚举系统音频设备
响应:audio_devices_ack
2.7 调试
debug_subscribe — 订阅调试指标周期推送
后端开始按 intervalMs 间隔周期推送 debug_metrics。
debug_unsubscribe — 取消订阅调试指标
wav_capture_start — 在指定模块节点启动 WAV 音频抓取
{
"type": "wav_capture_start",
"instanceId": "ut_delay_20ch#1",
"capturePoint": "output",
"durationMs": 1000
}
| 字段 | 说明 |
|---|---|
instanceId |
目标模块实例 ID |
capturePoint |
"input" 或 "output" |
durationMs |
抓取时长(毫秒) |
响应:wav_capture_ack(立即返回 captureId),抓取完成后推送 wav_capture_done
3. 后端 → 前端 消息(完整列表)
3.1 基础响应
pong — 心跳响应
timestamp 为 UTC 毫秒时间戳,前端用于计算 RTT。
error — 错误响应
3.2 参数响应
set_param_ack — 参数设置确认
成功时:
失败时:
{
"type": "set_param_ack",
"instanceId": "gain#1",
"paramId": "gainDb#0",
"success": false,
"error": "DSP 未连接"
}
get_param_ack — 参数查询响应
{
"type": "get_param_ack",
"instanceId": "gain#1",
"paramId": "gainDb#0",
"value": "-3.5",
"success": true
}
注:value 字段为字符串(paramStore 以字符串存储所有值)。
get_all_params_ack — 全量参数响应
{
"type": "get_all_params_ack",
"instanceId": "gain#1",
"params": {
"enable": "true",
"smoothTime": "10",
"gainDb#0": "-3.0",
"gainDb#1": "-3.0",
"mute#0": "false",
"mute#1": "false",
"phase#0": "false"
}
}
键为去掉 ${instanceId}. 前缀后的 paramId,值均为字符串。
param_update — 多端参数广播
某客户端设置参数后,后端广播给其余所有已连接客户端:
3.3 链路响应
write_link_ack — 链路保存确认
成功时:
失败时:
read_link_ack — 链路加载响应
成功时:
{
"type": "read_link_ack",
"success": true,
"link": { "version": "2.2", "rootChainId": "root", "chains": {...} }
}
文件不存在时:
get_dsp_chain_ack — DSP 链路读取响应
{
"type": "get_dsp_chain_ack",
"success": true,
"link": { "version": "2.2", "rootChainId": "root", "chains": {...} }
}
3.4 DSP 状态
connect_dsp_ack — DSP 连接结果
成功时:
{
"type": "connect_dsp_ack",
"success": true,
"protocol": "tcp",
"target": "192.168.1.100:9000",
"error": null
}
失败时:
disconnect_dsp_ack — DSP 断开确认
dsp_status — DSP 状态推送
客户端连接时主动推送一次,DSP 连接/断开时全体广播:
3.5 模式
mode_changed — 模式变更广播
3.6 PC 仿真
sim_status — 仿真状态推送
仿真状态变更时广播给所有客户端:
正常运行时:
{
"type": "sim_status",
"running": true,
"status": "running",
"inputDevice": "立体声混音 (Realtek HD Audio)",
"outputDevice": "扬声器 (Realtek HD Audio)"
}
失败时:
audio_devices_ack — 音频设备列表
{
"type": "audio_devices_ack",
"inputs": [
{ "id": "dev-001", "name": "立体声混音 (Realtek HD Audio)" },
{ "id": "dev-002", "name": "麦克风阵列 (USB Audio)" }
],
"outputs": [
{ "id": "dev-010", "name": "扬声器 (Realtek HD Audio)" },
{ "id": "dev-011", "name": "耳机 (USB Audio)" }
]
}
3.7 调试推送
debug_metrics — 调试指标周期推送
订阅后按 intervalMs 间隔推送:
{
"type": "debug_metrics",
"timestamp": 1711900000000,
"latency": {
"totalMs": 22.5,
"byModule": [
{ "instanceId": "channel_gain#1", "latencyMs": 0.0 },
{ "instanceId": "ut_delay_20ch#1", "latencyMs": 20.0 }
]
},
"compute": [
{ "instanceId": "channel_gain#1", "cpuUs": 45 },
{ "instanceId": "ut_delay_20ch#1", "cpuUs": 95 }
],
"memory": {
"poolUsed": 204800,
"poolTotal": 1048576,
"byModule": [
{ "instanceId": "channel_gain#1", "bytes": 440 },
{ "instanceId": "ut_delay_20ch#1", "bytes": 77056 }
]
}
}
log_stream — 实时日志流推送
算法库或后端产生的日志实时推送:
{
"type": "log_stream",
"level": "INFO",
"source": "dsp",
"message": "DynamicChain: LoadConfig done, 2 modules",
"timestamp": 1711900000000
}
level |
说明 |
|---|---|
DEBUG |
低优先级诊断信息(高频,默认不显示) |
INFO |
正常运行信息 |
WARN |
警告(如 MemPool 占用超过 80%) |
ERROR |
错误事件 |
source |
说明 |
|---|---|
dsp |
来自 DynamicChain 算法库 |
backend |
来自 ASP.NET Core 后端 |
wav_capture_ack — WAV 抓取启动确认
wav_capture_done — WAV 抓取完成通知
前端收到后通过 REST 接口下载:GET /api/debug/wav/a3f2b1c0
4. REST 接口
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/status |
查询后端及 DSP 状态(JSON) |
| GET | /api/link |
获取当前链路快照(JSON) |
| GET | /api/debug/wav/{captureId} |
下载 WAV 抓取文件(二进制流) |
| GET | /api/debug/metrics |
获取最近一次调试指标快照(JSON) |
| GET | /api/audio/devices |
获取系统音频设备列表(JSON) |
/api/status 响应示例:
{
"status": "online",
"clients": 2,
"version": "3.0",
"dsp": {
"connected": true,
"protocol": "tcp",
"target": "192.168.1.100:9000"
},
"paramCount": 48
}
5. DSP 二进制帧协议(后端 ↔ DSP)
后端与 DSP 硬件之间使用 TLV 风格二进制帧。
5.1 通用帧结构
Offset Size 字段
────── ──── ──────────────────────────────────────
0 1 帧头 0x55
1 1 帧头 0xAA
2 1 命令字 CMD
3 2 序列号 SEQ (uint16 LE, 每帧自增,溢出归零)
5 2 数据长度 LEN (uint16 LE,仅 DATA 段字节数)
7 N 数据段 DATA(由 CMD 决定格式)
7+N 1 校验 CRC8(计算范围:偏移 0 ~ 7+N-1)
总帧长 = 8 + N bytes
5.2 命令字定义
| CMD | 值 | 方向 | 说明 |
|---|---|---|---|
set_param |
0x01 | 后端→DSP | 设置单个模块参数 |
set_link |
0x02 | 后端→DSP | 下发完整链路配置 |
ping |
0x03 | 后端→DSP | 心跳探测(保留) |
param_ack |
0x81 | DSP→后端 | 参数设置确认(保留) |
link_ack |
0x82 | DSP→后端 | 链路加载确认(保留) |
5.3 CMD = 0x01:set_param DATA 格式
Offset Size 字段
────── ──── ──────────────────────────────────────
0 1 instanceId 字节长度 (N1)
1 N1 instanceId (UTF-8,如 "gain#1")
1+N1 1 paramId 字节长度 (N2)
2+N1 N2 paramId (UTF-8,已含通道后缀,如 "gainDb#0")
2+N1+N2 1 值类型 VALUE_TYPE
3+N1+N2 V 值字节 VALUE(长度由 VALUE_TYPE 决定)
值类型编码:
| VALUE_TYPE | 值 | 字节数 | 说明 |
|---|---|---|---|
| float32 | 0x01 | 4 | IEEE 754 单精度,LE |
| int32 | 0x02 | 4 | 有符号整数,LE |
| uint8 | 0x03 | 1 | 布尔值(1=true, 0=false)或 uint8 整数 |
| uint32 | 0x04 | 4 | 无符号整数,LE(用于延迟 samples 等) |
| string | 0x05 | N | UTF-8 字符串(无长度前缀,由 LEN 推导) |
后端类型推断规则(SerializeValue() 函数):
1. 如果 paramId 包含关键字 "Samples" 或 "Idx" → uint32 (0x04)
2. "true" / "false" → uint8 (0x03)
3. 否则尝试 float.TryParse → float32 (0x01)
4. 否则尝试 int.TryParse → int32 (0x02)
5. 其余 → string (0x05)
5.4 CMD = 0x02:set_link DATA 格式
set_link v3(当前版本,CMD=0x02)
v3 采用固定宽度的模块表和连线表,后端 ChainFlattener 负责将 LinkConfig v2.2 中的子图递归展平后再构造此帧。DSP 仅见到平坦的模块列表,对子图结构无感知。
set_link v3 binary frame (CMD=0x02):
Header (6 bytes):
[CMD: uint8 = 0x02]
[DATA_LEN: uint32 LE] -- bytes following CMD byte
[VERSION: uint8 = 0x03]
[NUM_MODULES: uint8]
[NUM_CONNECTIONS: uint8]
[RESERVED: uint8]
Module Table (NUM_MODULES × 88 bytes per entry):
Per module entry (88 bytes total):
instanceId: char[16] null-terminated, e.g. "gain#1\0..."
typeNumId: uint32 LE e.g. 0x10010001
numPorts: uint8 number of valid entries in ports[8]
RESERVED: uint8[3]
ports[8]: 8 × 8 bytes = 64 bytes
Per port (8 bytes):
portId: char[6] null-terminated, e.g. "input\0", "out\0.."
direction: uint8 0=input, 1=output
required: uint8 0=optional, 1=required
channels: uint16 LE 0=inherit
sampleRate: uint16 LE sample rate / 1000 (e.g. 48 for 48000 Hz) — saves space
Note: 16 + 4 + 4 + 64 = 88 bytes per module
Maximum 32 modules → 32 × 88 = 2816 bytes
Connection Table (NUM_CONNECTIONS × 8 bytes per entry):
Per connection entry (8 bytes):
fromModuleIdx: uint8 index into module table
fromPortIdx: uint8 index into fromModule's ports[] array
toModuleIdx: uint8
toPortIdx: uint8
channels: uint16 LE resolved channel count on this wire
sampleRate: uint16 LE sample rate / 1000
Maximum 64 connections → 64 × 8 = 512 bytes
Total maximum frame payload: 6 + 2816 + 512 = 3334 bytes
设计说明:
- 连线表使用模块索引(而非字符串 ID)引用模块,节省帧空间并加快 DSP 端解析速度
- 子图由后端 ChainFlattener 展平;DSP 仅见到平坦模块列表,不感知子图层次结构
- 模块表中的 instanceId 字符串采用点分展平命名(如 "group#1.eq#1"),用于参数寻址(例如 "group#1.eq#1.gainDb#0"),与前端展平后的路径保持一致
DSP 端解析后的行为:
1. 停止音频处理(静音保护)
2. 调用 DynamicChain_Reset() 清空旧链路
3. 按顺序 DynamicChain_AddModule() 重建链路
4. 恢复音频处理
向后兼容说明:v1 与 v2 格式(旧固件)
以下 v1/v2 格式仅供尚未升级至 v3 的旧固件参考。后端可根据 targetVersion 配置决定使用哪种格式。
v1 格式(原始版本):
Offset Size 字段
────── ───── ──────────────────────────────────────
0 2 模块数量 (uint16 LE)
2 ... 模块条目列表(按 order 升序排列)
每个模块条目(v1):
0 4 moduleTypeId (uint32 LE)
4 1 instanceId 字节长度 (N)
5 N instanceId (UTF-8,如 "gain#1")
v2 格式(含端口配置字段):
Offset Size 字段
────── ───── ──────────────────────────────────────
0 2 模块数量 (uint16 LE)
2 ... 模块条目列表(按 order 升序排列)
每个模块条目(v2):
0 4 moduleTypeId (uint32 LE)
4 1 instanceId 字节长度 (N)
5 N instanceId (UTF-8,如 "gain#1")
5+N 2 inputChannels (uint16 LE)
7+N 2 outputChannels (uint16 LE)
9+N 4 inputSampleRate (uint32 LE)
13+N 4 outputSampleRate (uint32 LE)
DSP 可根据帧 LEN 字段推断版本(v2 每个模块条目比 v1 多 12 字节)。
模块类型 ID 定义:
| moduleType 字符串 | moduleTypeId | 说明 |
|---|---|---|
channel_gain_v1 |
0x10010001 | 20 通道增益 |
adaptive_gain_v1 |
0x10010002 | 自适应增益 |
speaker_level_v1 |
0x10010003 | 扬声器电平 |
headroom_v1 |
0x10010004 | Headroom 增益 |
audio_mixer_v1 |
0x10010005 | 多输入混音器 |
ut_delay_20ch_v1 |
0x10020001 | 20 通道延迟 |
common_eq_v1 |
0x10030001 | 通用参数 EQ |
eq_4ch_v1 |
0x10030002 | 4 通道 EQ |
source_eq_v1 |
0x10030003 | 音源 EQ |
speed_eq_v1 |
0x10030004 | 车速 EQ |
ut_eq_20ch_v1 |
0x10030005 | 20 通道 EQ |
limiter_v1 |
0x10040001 | 限幅器 |
mdrc_v1 |
0x10040002 | 多频段压缩 |
surround_v1 |
0x10050001 | 环绕声 |
upmixer_v1 |
0x10050002 | 上混(如 2ch→12ch) |
distortion_v1 |
0x10060001 | 失真效果 |
virtual_bass_v1 |
0x10060002 | 虚拟低音 |
loudness_v1 |
0x10060003 | 响度补偿 |
5.5 CRC8 算法
多项式 0x07(CRC-8/SMBUS),计算范围:偏移 0 到 DATA 末尾(不含 CRC 字节本身):
byte Crc8(byte[] data, int start, int len) {
byte crc = 0;
for (int i = start; i < start + len; i++) {
crc ^= data[i];
for (int j = 0; j < 8; j++)
crc = (crc & 0x80) != 0 ? (byte)((crc << 1) ^ 0x07) : (byte)(crc << 1);
}
return crc;
}
5.6 完整帧示例
示例 1:set_param — 设置 gain#1 的 gainDb#0 = -3.5(float32)
55 AA 帧头
01 CMD = set_param
00 00 SEQ = 0
13 00 LEN = 19 bytes
DATA (19 bytes):
06 instanceId len = 6
67 61 69 6E 23 31 "gain#1"
08 paramId len = 8
67 61 69 6E 44 62 23 30 "gainDb#0"
01 VALUE_TYPE = float32
00 00 60 C0 -3.5 (IEEE 754 LE)
XX CRC8
示例 2:set_param — 设置 delay#1 的 delaySamples#1 = 48(uint32)
55 AA 帧头
01 CMD = set_param
01 00 SEQ = 1
12 00 LEN = 18 bytes
DATA (18 bytes):
07 instanceId len = 7
64 65 6C 61 79 23 31 "delay#1"
10 paramId len = 16
64 65 6C 61 79 53 61 6D
70 6C 65 73 23 31 "delaySamples#1"
04 VALUE_TYPE = uint32
30 00 00 00 48 (LE)
XX CRC8
示例 3:set_link v1 — 下发链路 [gain#1 → delay#1]
55 AA 帧头
02 CMD = set_link
02 00 SEQ = 2
16 00 LEN = 22 bytes
DATA (22 bytes):
02 00 模块数量 = 2
条目 0 (gain#1):
01 00 01 10 moduleTypeId = 0x10010001 (channel_gain_v1, LE)
06 instanceId len = 6
67 61 69 6E 23 31 "gain#1"
条目 1 (delay#1):
01 00 02 10 moduleTypeId = 0x10020001 (ut_delay_20ch_v1, LE)
07 instanceId len = 7
64 65 6C 61 79 23 31 "delay#1"
XX CRC8
6. 参数 ID 命名规范
格式定义
| 格式 | 示例 | 说明 |
|---|---|---|
paramId |
enable |
全局参数(无通道后缀) |
paramId#channel |
gainDb#0 |
逐通道参数(channel 从 0 起) |
paramId#band |
bandFreq#2 |
逐频段参数 |
paramId#band#channel |
bandFreq#2#0 |
逐频段逐通道 |
WebSocket 消息中的参数 ID
前端发送 set_param 时,paramId 字段不含通道后缀,通道通过独立的 channel 字段传递:
后端合并为存储 key:"gain#1.gainDb#0"
DSP 二进制帧中的参数 ID
DSP 帧中的 paramId 字段已包含通道后缀,直接传递完整字符串:
instanceId 格式规范
instanceId = "${moduleTypeShortName}#${index}"
规则:
1. moduleTypeShortName 为 moduleType 去掉 "_v${N}" 后缀后的名称
channel_gain_v1 → channel_gain
ut_delay_20ch_v1 → ut_delay_20ch
2. index 从 1 开始,同类型第 N 个实例的 index 为 N
3. 总长度不超过 31 字节(ASCII)
4. 子图展平后采用点分路径,如 "group#1.eq#1"(总长度不超过 15 字节,
受 set_link v3 模块表 instanceId char[16] 限制)
示例:
channel_gain#1 第 1 个 channel_gain_v1 实例
ut_delay_20ch#1 第 1 个 ut_delay_20ch_v1 实例
channel_gain#2 第 2 个 channel_gain_v1 实例
group#1.eq#1 子图 group#1 内的 eq#1(展平后)
Gain 模块参数 ID 列表
| paramId | 类型 | 单位 | 通道相关 | 说明 |
|---|---|---|---|---|
enable |
bool | — | 否 | 模块使能 |
smoothTime |
float | ms | 否 | 增益平滑时间 |
gainDb#${ch} |
float | dB | 是 | 通道 ch 增益(-96 ~ +24 dB) |
mute#${ch} |
bool | — | 是 | 通道 ch 静音 |
phase#${ch} |
bool | — | 是 | 通道 ch 相位翻转 |
Delay 模块参数 ID 列表
| paramId | 类型 | 单位 | 通道相关 | 说明 |
|---|---|---|---|---|
enable |
bool | — | 否 | 模块使能 |
delaySamples#${ch} |
uint32 | samples | 是 | 通道 ch 延迟(0 ~ 960 samples) |
7. 链路配置 JSON 格式(v2.2)
v2.2 在 v2.1 基础上,将单链路平坦模块列表升级为多链路 chains 映射,支持子图嵌套(SubGraph)与多端口连线(multi-port edge)。旧版 v2.0/v2.1 的单一 input/output 连线假设已被 ChainEdge.fromPort / toPort 取代,可明确指向具名端口 ID。
注意:ChainEdge.fromPort / toPort 替代旧版单一 input/output 假设,明确指向具名端口 ID。这使得 Mixer 等多输入模块的不同输入端口(如
input_0、input_1)可以在同一连线表中被唯一区分。
LinkConfig 字段说明
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
version |
string | 是 | 格式版本,当前为 "2.2" |
rootChainId |
string | 是 | 根链路 id,对应 chains 中的顶层入口 |
global |
object | 是 | 全局音频参数:channels、sampleRate、frameSize |
chains |
map |
是 | 所有链路定义,key 为链路 id |
ChainDefinition 字段说明
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id |
string | 是 | 链路唯一标识符 |
name |
string | 否 | 可读名称 |
nodes |
ChainNode[] | 是 | 本链路内所有节点 |
edges |
ChainEdge[] | 是 | 本链路内所有连线 |
externalPorts |
PortDescriptor[] | 子图必填 | 子图对外暴露的端口,与父图中 SubGraphNode 的 ports 对应 |
PortDescriptor 字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
id |
string | 端口 ID,节点内唯一 |
direction |
string | "input" 或 "output" |
channels |
int | 通道数;-1 表示继承 |
sampleRate |
int | 采样率(Hz);-1 表示继承 |
label |
string | UI 显示标签 |
required |
bool | 是否为必连端口 |
write_link 消息示例(v2.2,含 Mixer 多输入端口与子图)
{
"type": "write_link",
"payload": {
"version": "2.2",
"rootChainId": "root",
"global": { "channels": 20, "sampleRate": 48000, "frameSize": 240 },
"chains": {
"root": {
"id": "root",
"name": "Main Chain",
"nodes": [
{
"instanceId": "gain#1",
"moduleType": "channel_gain_v1",
"order": 0,
"enabled": true,
"position": {"x": 100, "y": 200},
"ports": [
{"id":"input","direction":"input","channels":-1,"sampleRate":-1,"label":"Input","required":true},
{"id":"output","direction":"output","channels":-1,"sampleRate":-1,"label":"Output","required":true}
]
},
{
"instanceId": "mixer#1",
"moduleType": "audio_mixer_v1",
"order": 1,
"enabled": true,
"position": {"x": 400, "y": 200},
"ports": [
{"id":"input_0","direction":"input","channels":-1,"sampleRate":-1,"label":"Ch A","required":true},
{"id":"input_1","direction":"input","channels":-1,"sampleRate":-1,"label":"Ch B","required":false},
{"id":"output","direction":"output","channels":-1,"sampleRate":-1,"label":"Mix","required":true}
]
},
{
"instanceId": "group#1",
"moduleType": "subgraph",
"subGraphId": "eq_stage",
"position": {"x": 700, "y": 200},
"ports": [
{"id":"input","direction":"input","channels":2,"sampleRate":48000,"label":"In","required":true},
{"id":"output","direction":"output","channels":2,"sampleRate":48000,"label":"Out","required":true}
]
}
],
"edges": [
{"id":"e1","fromModule":"gain#1","fromPort":"output","toModule":"mixer#1","toPort":"input_0","channels":2,"sampleRate":48000},
{"id":"e2","fromModule":"mixer#1","fromPort":"output","toModule":"group#1","toPort":"input","channels":2,"sampleRate":48000}
]
},
"eq_stage": {
"id": "eq_stage",
"name": "EQ Stage",
"externalPorts": [
{"id":"input","direction":"input","channels":2,"sampleRate":48000,"label":"In","required":true},
{"id":"output","direction":"output","channels":2,"sampleRate":48000,"label":"Out","required":true}
],
"nodes": [
{
"instanceId": "eq#1",
"moduleType": "common_eq_v1",
"order": 0,
"enabled": true,
"position": {"x": 200, "y": 150},
"ports": [
{"id":"input","direction":"input","channels":-1,"sampleRate":-1,"label":"In","required":true},
{"id":"output","direction":"output","channels":-1,"sampleRate":-1,"label":"Out","required":true}
]
}
],
"edges": []
}
}
}
}
8. 消息流程图
8.1 正常参数设置流程
Frontend Backend DSP Hardware
│ │ │
│── set_param ──────────►│ │
│ ├── Binary Frame (0x01) ──►│
│◄─ set_param_ack ───────┤ │
│ │ │
│ [其他客户端] │
│ ├── param_update ─────────►│ (广播)
8.2 链路下发流程
Frontend Backend DSP Hardware
│ │ │
│── write_link ─────────►│ │
│ ├─ 持久化 JSON 到文件 │
│ ├─ ChainFlattener 展平子图 │
│ ├─ Binary Frame (0x02) ──►│
│◄─ write_link_ack ──────┤ │
│ │ [DSP 重建链路] │
│── set_param (初始参数)──►│ │
│◄─ set_param_ack ───────┤ │
8.3 客户端重连恢复流程
Frontend Backend
│ │
│── WebSocket Connect ──►│
│◄─ dsp_status ──────────┤ (后端主动推送当前 DSP 状态)
│ │
│── read_link ───────────►│
│◄─ read_link_ack ───────┤ (返回链路 JSON)
│ │
│── get_all_params ──────►│ (针对每个模块实例)
│◄─ get_all_params_ack ──┤
8.4 DSP 连接流程
Frontend Backend DSP Hardware
│ │ │
│── connect_dsp ─────────►│ │
│ │── TCP/Serial Connect ───►│
│◄─ connect_dsp_ack ─────┤ │
│◄─ dsp_status ──────────┤ (广播给所有客户端) │
8.5 工作模式切换流程
Frontend Backend 其他客户端
│ │ │
│── set_mode ────────────►│ │
│ ├─ 更新内部模式状态 │
│◄─ mode_changed ────────┤ │
│ ├── mode_changed ──────►│ (广播)
8.6 PC 仿真启动流程
Frontend Backend DynamicChain.dll + NAudio
│ │ │
│── sim_start ───────────►│ │
│ ├── 加载当前链路 JSON │
│ ├── Init DLL + open audio ──►│
│ │ │ (音频处理开始)
│◄─ sim_status ──────────┤ (running=true, 广播) │
│ │ │
│── sim_stop ────────────►│ │
│ ├── Stop audio + unload ────►│
│◄─ sim_status ──────────┤ (running=false, 广播) │
8.7 调试订阅流程
Frontend Backend (DebugService)
│ │
│── debug_subscribe ─────►│ (intervalMs=500)
│ ├─ 启动定时器
│◄─ debug_metrics ───────┤ (每 500ms 推送一次)
│◄─ debug_metrics ───────┤
│◄─ debug_metrics ───────┤
│ ... │
│── debug_unsubscribe ───►│
│ ├─ 停止定时器
8.8 WAV 抓取流程
Frontend Backend DynamicChain.dll
│ │ │
│── wav_capture_start ───►│ │
│ ├── Hook module output ─►│
│◄─ wav_capture_ack ─────┤ (captureId="a3f2b1c0")│
│ │ [抓取 durationMs] │
│ │◄─ 音频数据回调 ─────────┤
│ ├─ 写 WAV 文件 │
│◄─ wav_capture_done ────┤ (fileSizeBytes=2097152)│
│ │ │
│── GET /api/debug/wav/a3f2b1c0 ──────────────────►│ (REST 下载)
8.9 子图导航流程(纯前端操作,不涉及 WebSocket)
子图导航流程(纯前端操作,不涉及 WebSocket):
用户双击 SubGraphNode → useChainStore.navigateInto(subGraphId)
→ currentChainId 更新为 subGraphId
→ ChainCanvas 重绘子图内容
→ BreadcrumbNav 显示导航路径
用户点击面包屑返回 → useChainStore.navigateBack()
→ currentChainId 恢复为父图 id
→ ChainCanvas 重绘父图
用户点击保存/下发链路 → 前端发送 write_link(含完整 LinkConfig v2.2,含子图定义)
→ 后端 ChainFlattener 展平子图
→ 后端生成 set_link v3 二进制帧下发 DSP
子图导航是纯前端的视图切换行为,不产生任何 WebSocket 消息。前端通过 useChainStore 中的 currentChainId 状态决定 ChainCanvas 当前渲染哪个 ChainDefinition。只有在用户主动保存或下发链路时,才触发一次 write_link 消息,其 payload 包含完整的 LinkConfig v2.2(含所有子图定义)。
9. 消息类型汇总表
前端 → 后端
| type | 分组 | 说明 |
|---|---|---|
ping |
基础通信 | 心跳探测 |
set_param |
参数操作 | 设置单个参数 |
set_params |
参数操作 | 兼容别名(与 set_param 等价) |
get_param |
参数操作 | 查询单个参数 |
get_all_params |
参数操作 | 查询实例全量参数 |
write_link |
链路管理 | 保存并下发链路配置;payload: LinkConfig v2.2(含子图定义) |
read_link |
链路管理 | 加载链路配置 |
connect_dsp |
DSP 连接 | 连接 DSP 硬件(TCP 或串口) |
disconnect_dsp |
DSP 连接 | 断开 DSP 连接 |
get_status |
DSP 连接 | 查询后端及 DSP 状态 |
get_dsp_chain |
DSP 连接 | 从 DSP 读取链路结构(仅 test_verify) |
set_mode |
工作模式 | 切换工作模式 |
sim_start |
PC 仿真 | 启动 PC 端算法仿真 |
sim_stop |
PC 仿真 | 停止 PC 端算法仿真 |
list_audio_devices |
PC 仿真 | 枚举系统音频设备 |
debug_subscribe |
调试 | 订阅调试指标周期推送 |
debug_unsubscribe |
调试 | 取消订阅调试指标 |
wav_capture_start |
调试 | 启动模块节点 WAV 抓取 |
多端口连线与子图对协议层透明:前端无需新增消息类型,所有结构变化均封装在
write_link的LinkConfig v2.2payload 内。
后端 → 前端
| type | 分组 | 推送方式 | 说明 |
|---|---|---|---|
pong |
基础响应 | 单播(请求方) | 心跳响应 |
error |
基础响应 | 单播(请求方) | 错误响应 |
set_param_ack |
参数响应 | 单播(请求方) | 参数设置确认 |
get_param_ack |
参数响应 | 单播(请求方) | 参数查询响应 |
get_all_params_ack |
参数响应 | 单播(请求方) | 全量参数响应 |
param_update |
参数响应 | 广播(其他客户端) | 多端参数同步广播 |
write_link_ack |
链路响应 | 单播(请求方) | 链路保存确认 |
read_link_ack |
链路响应 | 单播(请求方) | 链路加载响应 |
get_dsp_chain_ack |
链路响应 | 单播(请求方) | DSP 链路读取响应 |
connect_dsp_ack |
DSP 状态 | 单播(请求方) | DSP 连接结果 |
disconnect_dsp_ack |
DSP 状态 | 单播(请求方) | DSP 断开确认 |
dsp_status |
DSP 状态 | 广播(所有客户端) | DSP 状态推送 |
mode_changed |
模式 | 广播(所有客户端) | 模式变更广播 |
sim_status |
PC 仿真 | 广播(所有客户端) | 仿真状态推送 |
audio_devices_ack |
PC 仿真 | 单播(请求方) | 音频设备列表 |
debug_metrics |
调试推送 | 单播(订阅方) | 调试指标周期推送 |
log_stream |
调试推送 | 广播(所有客户端) | 实时日志流推送 |
wav_capture_ack |
调试推送 | 单播(请求方) | WAV 抓取启动确认 |
wav_capture_done |
调试推送 | 单播(请求方) | WAV 抓取完成通知 |
新增消息类型(v4.0 补充)
前端 → 后端:Preset 管理
// 保存当前模块参数为一套 preset
{ "type": "save_preset", "instanceId": "gain#1", "presetId": "bass_boost", "name": "Bass Boost" }
// 响应: preset_save_ack
// 加载 preset 参数到 UI
{ "type": "load_preset", "instanceId": "gain#1", "presetId": "bass_boost" }
// 响应: preset_data
// 列出某模块的所有 preset
{ "type": "list_presets", "instanceId": "gain#1" }
// 响应: preset_list
// 删除 preset
{ "type": "delete_preset", "instanceId": "gain#1", "presetId": "bass_boost" }
// 响应: preset_delete_ack
前端 → 后端:Profile 管理
// 保存 ambiance profile
{
"type": "save_profile",
"profileId": "hifi_mode", "name": "HIFI",
"modulePresets": { "gain#1": "flat", "eq#1": "hifi_eq", "delay#1": "default" }
}
// 响应: profile_save_ack
{ "type": "list_profiles" } // 响应: profile_list
{ "type": "delete_profile", "profileId": "hifi_mode" } // 响应: profile_delete_ack
// 切换音效模式(向 DSP 发 CMD=0x05)
{ "type": "set_ambiance", "ambianceIndex": 2 }
// 响应: set_ambiance_ack
前端 → 后端:ParamBin 生成
// 将指定 profiles 编译为 ParamBin 并下发 DSP(CMD=0x04)
{
"type": "generate_param_bin",
"targetProfiles": ["hifi_mode", "dynamic_mode", "rock_mode"],
"modulePresetLimits": { "gain#1": 3, "eq#1": 3, "delay#1": 2 }
}
// 响应: param_bin_ack
前端 → 后端:多维参数 set_param(新增 dimIndices)
{
"type": "set_param",
"instanceId": "eq#1",
"paramId": "bandFreq",
"dimIndices": [2, 5], // 新增可选字段:dim0=channel=2, dim1=band=5
"value": "120.0"
}
// 如果 dimIndices 不存在则向后兼容(使用旧 channel 字段)
// 后端编码:channelIdx = (2<<8)|5 = 0x0205
前端 → 后端:子图同步校验
// 触发后端向 DSP 查询平坦模块列表(仅校验,不重建层次结构)
{ "type": "read_dsp_chain" }
// 响应: dsp_chain_echo + 可能跟随 chain_sync_warning
注:get_dsp_chain 行为变更 —— 返回 Backend 存储的 LinkConfig(含子图),不询问 DSP:
后端 → 前端:新增响应消息
// Preset 响应
{ "type": "preset_save_ack", "instanceId": "gain#1", "presetId": "bass_boost" }
{ "type": "preset_data", "instanceId": "gain#1", "presetId": "bass_boost",
"params": { "gainDb[0]": "3.0", "gainDb[1]": "-1.5", "bandFreq[2][5]": "120.0" } }
{ "type": "preset_list", "instanceId": "gain#1",
"presets": [{"presetId":"flat","name":"Flat"},{"presetId":"bass_boost","name":"Bass Boost"}] }
{ "type": "preset_delete_ack", "instanceId": "gain#1", "presetId": "bass_boost" }
// Profile 响应
{ "type": "profile_save_ack", "profileId": "hifi_mode" }
{ "type": "profile_list",
"profiles": [{"profileId":"hifi_mode","name":"HIFI"},{"profileId":"dynamic","name":"动感"}] }
{ "type": "profile_delete_ack", "profileId": "hifi_mode" }
// Ambiance 响应
{ "type": "set_ambiance_ack", "ambianceIndex": 2 }
{ "type": "param_bin_ack", "byteSize": 4096, "numAmbiances": 3, "numModules": 8 }
// 同步校验响应
{ "type": "dsp_chain_echo",
"modules": [{"instanceId":"gain#1","typeId":"channel_gain_v1"},
{"instanceId":"eq#1","typeId":"common_eq_v1"}] }
{ "type": "chain_sync_warning",
"missing": ["group#1.eq#1"], "extra": [],
"message": "DSP 模块列表与存储配置不匹配,建议重新下发链路" }
更新的 DSP Binary 帧格式
set_param CMD=0x01(v3,数字 ID)
旧版(字符串):[CMD=0x01][LEN]["gain#1.gainDb#2\0"][f32] ≈ 30 字节
新版(数字): [CMD=0x01][LEN=9][moduleIdx:u8][paramTypeId:u16LE][channelIdx:u16LE][type:u8=0x01][value:f32LE]
= 14 字节(节省 53%)
channelIdx 编码:
标量/全局:0x0000
1D(通道 2):0x0002
2D(通道2 band5):0x0205 ← (2<<8)|5
load_param_bin CMD=0x04
[CMD=0x04][DATA_LEN:u32LE][ ParamBin 数据 ]
ParamBin 内部格式:
Header (8B): "PCFG" + version:u8 + numAmbiances:u8 + numModules:u8 + reserved:u8
Module Table: numModules × 24B = instanceId:char[16] + typeNumId:u32 + numPresets:u8 + pad:u8[3]
Ambiance Table: numAmbiances × (16+numModules)B = name:char[16] + presetIndices:u8[numModules]
Param Data: per module per preset:
paramCount:u16 + (paramTypeId:u16 + channelIdx:u16 + value:f32)[paramCount]
set_ambiance CMD=0x05
get_chain CMD=0x06(用于同步校验)
请求:[CMD=0x06][DATA_LEN=0:u32LE] = 5 字节
响应:[CMD=0x86][DATA_LEN:u32LE][numModules:u8][pad:u8[3]]
per module (32B): instanceId:char[24] + typeNumId:u32 + pad:u8[4]
paramTypeId 编号规范
格式:u16(高字节=模块类别,低字节=参数序号)
模块类别:
0x01 = gain 0x02 = filter/EQ 0x03 = delay
0x04 = dynamics 0x05 = mixer 0x06 = routing
已定义示例:
0x0101 Gain.gainDb 0x0102 Gain.mute 0x0103 Gain.enable
0x0201 EQ.bandFreq 0x0202 EQ.bandGain 0x0203 EQ.bandQ
0x0204 EQ.bandType 0x0205 EQ.bandEnable 0x0206 EQ.moduleEnable
0x0301 Delay.delaySamples 0x0302 Delay.enable
新增消息类型(v5.1 补充)
2.2 参数操作(续)
apply_params — 前端主导批量参数推送
前端将 preset 参数应用到自身状态后,通过此消息批量推送给后端。后端写入 paramStore 并向 DSP 下发每个参数。
{
"type": "apply_params",
"instanceId": "gain#1",
"params": {
"gainDb#0": "-3.5",
"gainDb#1": "-3.5",
"mute#0": "false",
"mute#1": "false",
"enable": "true"
}
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
instanceId |
string | 是 | 模块实例 ID |
params |
object | 是 | 参数键值对;key 为含通道后缀的 paramId(如 "gainDb#0"),value 为字符串 |
后端行为:
1. 遍历 params,对每条记录:写入 paramStore["{instanceId}.{paramId}"],并异步调用 TryFlushToDspAsync
2. 所有布尔值经 ToStoreString() 确保以小写 "true" / "false" 存储
3. 不广播 param_update(批量覆盖场景,广播由前端自行决定是否补发)
响应:apply_params_ack
与 set_param 对比:
| 特性 | set_param |
apply_params |
|---|---|---|
| 参数数量 | 单个 | 批量 |
广播 param_update |
是 | 否 |
| 主要用途 | UI 实时调参 | Preset 加载后批量推送 |
| 响应类型 | set_param_ack |
apply_params_ack |
get_module_params — 从后端读回参数(回读校验)
前端"Read from backend"操作,用于校验后端 paramStore 中的当前值是否与前端期望一致。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
instanceId |
string | 是 | 模块实例 ID |
后端行为:直接从 paramStore 中过滤该实例的所有参数键,去掉 "{instanceId}." 前缀后返回。不与 DSP 通信。
响应:module_params
{
"type": "module_params",
"instanceId": "gain#1",
"params": {
"gainDb#0": "-3.5",
"gainDb#1": "-3.5",
"mute#0": "false",
"mute#1": "false",
"enable": "true"
}
}
所有值均为字符串,布尔参数保证为小写 "true" / "false"。
2.7 Preset 管理(更新)
save_preset — 保存模块参数快照(已更新)
{
"type": "save_preset",
"instanceId": "gain#1",
"presetId": "bass_boost",
"name": "Bass Boost",
"params": {
"gainDb#0": "3.0",
"gainDb#1": "3.0",
"mute#0": "false",
"enable": "true"
}
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
instanceId |
string | 是 | 模块实例 ID |
presetId |
string | 是 | Preset 标识符(文件名) |
name |
string | 是 | 可读名称(UI 显示) |
params |
object | 否 | 前端提供的参数快照(前端权威模式);若省略则后端从 paramStore 快照(向后兼容) |
后端行为(当 params 字段存在时):
1. 将 params 中每条记录写入 paramStore(经 ToStoreString() 保证布尔小写)
2. 为每条记录调用 TryFlushToDspAsync 异步下发 DSP
3. 将完整 preset(含 params 字段)序列化保存到 data/presets/{instanceId}/{presetId}.json
后端行为(params 字段缺失时,向后兼容):
- 从 paramStore 中快照该实例的当前参数并保存文件
响应:preset_save_ack
list_presets — 列出模块的所有 preset(已更新)
响应:preset_list(现在每个条目包含 params 字段)
{
"type": "preset_list",
"instanceId": "gain#1",
"presets": [
{
"presetId": "flat",
"name": "Flat",
"params": {
"gainDb#0": "0.0",
"gainDb#1": "0.0",
"mute#0": "false",
"enable": "true"
}
},
{
"presetId": "bass_boost",
"name": "Bass Boost",
"params": {
"gainDb#0": "3.0",
"gainDb#1": "3.0",
"mute#0": "false",
"enable": "true"
}
}
]
}
更新说明:原版 preset_list 每个条目只含 presetId 和 name(元数据)。新版从文件中读取完整 preset 并在列表条目中包含 params 字段。前端在启动时收到 preset_list 后,将 params 存入本地 presetDataStore[instanceId][presetId],后续 preset 加载可直接通过 apply_params 推送,无需再发 load_preset 往返请求。
3. 后端 → 前端 消息(v5.1 新增)
apply_params_ack — 批量参数应用确认
module_params — 后端参数回读响应
{
"type": "module_params",
"instanceId": "gain#1",
"params": {
"gainDb#0": "-3.5",
"gainDb#1": "-3.5",
"mute#0": "false",
"enable": "true"
}
}
params 对象的 key 为不含 instanceId. 前缀的 paramId,value 均为字符串;布尔值保证小写。
9. 消息类型汇总表(v5.1 增量)
在原有汇总表基础上,新增以下条目:
前端 → 后端
| type | 分组 | 说明 |
|---|---|---|
apply_params |
参数操作 | 批量写入 paramStore 并下发 DSP;前端主导 preset 加载 |
get_module_params |
参数操作 | 从 paramStore 读回指定实例的全部参数,用于前端回读校验 |
后端 → 前端
| type | 分组 | 推送方式 | 说明 |
|---|---|---|---|
apply_params_ack |
参数响应 | 单播(请求方) | 批量参数应用确认 |
module_params |
参数响应 | 单播(请求方) | 后端参数回读响应 |
已更新消息
| type | 方向 | 变更说明 |
|---|---|---|
save_preset |
前端→后端 | 新增可选 params 字段;前端可直接传参数快照(前端权威模式),省略时后端从 paramStore 快照(向后兼容) |
preset_list |
后端→前端 | 每个 preset 条目新增 params 字段,前端可在启动时预加载所有 preset 数据 |
参数值规范说明(v5.1)
所有经 WebSocket 传输并存入 paramStore 的布尔型参数值,统一使用小写字符串:
| 值类型 | 正确形式 | 错误形式(旧 C# 默认) |
|---|---|---|
| 布尔真 | "true" |
"True" |
| 布尔假 | "false" |
"False" |
这一规范确保前端 JavaScript 使用 === 'true' 进行布尔比较时能够正确匹配,无需额外做大小写转换。后端通过 ToStoreString() 辅助函数在所有写入路径(HandleSetParam、HandleApplyParams、HandleSavePreset、HandleLoadPreset、HandleSetAmbiance)统一保证此规范。