跨层数据结构参考手册 v2.0
1. 概述
本文档描述车载音频 DSP 调音工具的跨层数据结构对应关系,以及第三方算法模块开发所需实现的完整接口规范。
系统数据在三层之间经历如下变换流水线:
Frontend (TypeScript) Backend (C#) DSP C
LinkConfig v2.2 ─ws JSON→ LinkConfig C# model
ChainFlattener
FlattenedLinkConfig ─build→ set_link v3 frame
─parse→ DynamicChain
ModuleInstance[]
DynChainConnection[]
关键约定:
- Sub-graph 对 DSP 透明:前端的 SubGraphNode 由后端 ChainFlattener 展平为普通模块(实例 ID 加 group 前缀),DSP 只接收平坦模块列表
- 端口显式化:所有模块端口均有具名 ID,连接通过 fromPort/toPort 字段精确描述
- Fan-in 支持:DSP 维护独立连接表,每个模块拥有独立输出缓冲区,支持 Mixer 等多输入模块
2. 层间数据流向图
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ Frontend (TypeScript / Vue3) │
│ │
│ LinkConfig { │
│ version: '2.2' │
│ rootChainId: 'root' │
│ global: { channels, sampleRate, frameSize } │
│ chains: { │
│ root: ChainDefinition { │
│ nodes: [ChainNode | SubGraphNode][] ← SubGraphNode.moduleType='subgraph' │
│ edges: ChainEdge[] { fromModule, fromPort, toModule, toPort } │
│ } │
│ subchain_1: ChainDefinition { externalPorts[], nodes[], edges[] } │
│ } │
│ } │
└─────────────────────────────┬───────────────────────────────────────────────────────┘
│ write_link (WebSocket JSON)
▼
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ Backend (C# / ASP.NET Core 8) │
│ │
│ ① Deserialize → LinkConfig (C# record, mirrors TypeScript exactly) │
│ ② ChainFlattener.Flatten() → FlattenedLinkConfig { │
│ Modules: FlatModule[] { InstanceId, TypeId, Ports: PortConfig[] } │
│ Connections: FlatConnection[] { FromModule, FromPort, ToModule, ToPort, Ch, Sr }│
│ } │
│ Sub-graph 展平规则:group#1 内的 delay#1 → instanceId = "group#1.delay#1" │
│ ③ BinaryFrameBuilder.BuildLinkFrameV3() → byte[] │
└─────────────────────────────┬───────────────────────────────────────────────────────┘
│ set_link v3 binary frame (TCP/Serial TLV)
▼
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ DSP C Algorithm Layer │
│ │
│ DynamicChain_ParseLinkFrame() → DynamicChain { │
│ instances[]: ModuleInstance { │
│ instanceId, typeNumId, portCfg: ModulePortCfg, pState, ppOut[] │
│ } │
│ connections[]: DynChainConnection { │
│ fromModuleIdx, fromPortIdx, toModuleIdx, toPortIdx, channels, sampleRate │
│ } │
│ orderedIndices[]: topological sort │
│ } │
│ DynamicChain_Process() → fan-in aware, per-module output buffer execution │
└─────────────────────────────────────────────────────────────────────────────────────┘
3. 前端数据结构(TypeScript)
完整定义位于 frontend/src/types/chain.ts 和 frontend/src/types/module.ts。
3.1 链路配置 (types/chain.ts)
/** 单个端口描述符 */
export interface PortDescriptor {
id: string // 端口 ID: "input", "input_0", "input_1", "output"
direction: 'input' | 'output'
channels: number // -1 = 继承上游
sampleRate: number // -1 = 继承上游
label: string // 界面显示名
required: boolean // true = 必须连接,false = 可选
}
/** 普通模块节点 */
export interface ChainNode {
instanceId: string // 实例唯一 ID,格式: "moduleType#index"
moduleType: string // 模块类型 ID,对应注册表键
order: number // 处理顺序(拓扑排序后由后端填写)
enabled: boolean // false = bypass
position: { x: number; y: number } // 画布坐标(仅 UI 用,不影响 DSP)
ports: PortDescriptor[] // 所有端口(输入 + 输出)
}
/** 子图组节点(封装嵌套链路) */
export interface SubGraphNode {
instanceId: string // "group#1"
moduleType: 'subgraph' // 固定值,区分普通节点
subGraphId: string // 指向 LinkConfig.chains 中的子图 key
position: { x: number; y: number }
ports: PortDescriptor[] // 镜像子图的 externalPorts
}
/** 连线(从某模块的某端口到另一模块的某端口) */
export interface ChainEdge {
id: string
fromModule: string // 源模块 instanceId
fromPort: string // 源端口 ID(如 "output")
toModule: string // 目标模块 instanceId
toPort: string // 目标端口 ID(如 "input_0", "input_1")
channels: number // 前端 ChainCanvas 自动推算
sampleRate: number // 前端 ChainCanvas 自动推算
}
/** 独立链路定义(根链路和子图均使用此类型) */
export interface ChainDefinition {
id: string
name: string
nodes: (ChainNode | SubGraphNode)[]
edges: ChainEdge[]
externalPorts?: PortDescriptor[] // 仅子图:暴露给父图的端口接口
}
/** 根配置(替代旧版 DSPLink) */
export interface LinkConfig {
version: '2.2'
rootChainId: string
global: {
channels: number // 系统默认通道数(如 20)
sampleRate: number // 默认采样率(如 48000)
frameSize: number // 每帧采样点数(如 240)
}
chains: Record<string, ChainDefinition> // key = chain id
}
3.2 模块注册 (types/module.ts)
/** 参数 Schema */
export interface ParamSchema {
paramId: string // 参数 ID,如 "gainDb", "delaySamples"
paramTypeId: number // NEW: u16 numeric ID for binary protocol
type: 'float' | 'int' | 'bool' | 'string'
min?: number
max?: number
step?: number
default: number | boolean | string
unit?: string // "dB", "ms", "Hz"
label: string
dimensions?: ParamDimension[] // REPLACES old 'channels: number' field
// dimensions undefined/empty → scalar param (no channel indexing)
// dimensions.length == 1 → channel strip UI (backward compat)
// dimensions.length == 2 → 2D grid UI (e.g. EQ 10ch×10band)
role: 'tunable' | 'system' // tunable: Tuning模式可改; system: 仅Chain Builder可改
}
/** 模块类型描述符(注册表条目) */
export interface ModuleDescriptor {
typeId: string
displayName: string
category: string // "gain" | "delay" | "filter" | "mixer" | "routing" | ...
defaultPorts: PortDescriptor[] // 模块默认端口布局(替代旧 portRule 字段)
maxDynamicInputPorts?: number // 可选:Mixer 允许用户动态增删输入端口的最大数量
paramSchemas: ParamSchema[]
vueComponent?: string // 专属参数面板组件名(未指定则用 GenericModulePanel)
}
type AppMode = 'chain_builder' | 'tuning' | 'test_verify'
3.3 字段对照(前端 → 旧版对比)
| 旧字段 (v2.1) | 新字段 (v2.2) | 说明 |
|---|---|---|
ChainNode.inputChannels |
ChainNode.ports[?].channels (direction=input) |
用端口数组替代平坦字段 |
ChainNode.outputChannels |
ChainNode.ports[?].channels (direction=output) |
同上 |
ChainNode.inputSampleRate |
ChainNode.ports[?].sampleRate (direction=input) |
同上 |
ChainNode.outputSampleRate |
ChainNode.ports[?].sampleRate (direction=output) |
同上 |
ChainEdge.fromInstanceId |
ChainEdge.fromModule |
命名统一 |
ChainEdge.toInstanceId |
ChainEdge.toModule |
命名统一 |
ChainEdge.(no port) |
ChainEdge.fromPort / toPort |
新增端口 ID 引用 |
DSPLink |
LinkConfig |
结构重命名,增加 chains 字典 |
DSPLink.modules[] |
LinkConfig.chains.root.nodes[] |
模块移入 ChainDefinition |
ModuleDescriptor.portRule |
ModuleDescriptor.defaultPorts |
用描述符数组替代枚举 |
ParamSchema.channels |
ParamSchema.dimensions |
多维寻址替代单一通道数 |
SubGraphNode (不存在) |
SubGraphNode (新增) |
子图支持 |
4. ID 策略:字符串 vs 数字
4.1 字符串 ID 优缺点
| 维度 | 说明 |
|---|---|
| ✅ 人类可读 | "gain#1.gainDb#2" 直接理解,调试日志清晰 |
| ✅ JSON 天然支持 | 无需额外序列化层,前后端 schema 直接对齐 |
| ✅ 自描述 | 无需查表,错误信息直接有意义 |
| ✅ 扩展性强 | 新模块命名即可,无 ID 分配冲突 |
| ❌ 带宽大 | UART/Serial: ~25-30 字节/param vs 数字 5-9 字节 |
| ❌ DSP 比较慢 | strcmp O(n),500 参数 EQ 需 500 次字符比较 |
| ❌ ROM 占用多 | 字符串常量 vs 整数枚举 |
| ❌ 大 MCU 不适用 | 嵌入式 DSP 内存有限,字符串处理开销大 |
4.2 数字 ID 优缺点
| 维度 | 说明 |
|---|---|
| ✅ 极度紧凑 | moduleIdx(1B)+paramTypeId(2B)+channelIdx(2B)=5字节/param |
| ✅ O(1) 查找 | switch(paramTypeId) 编译为跳转表,无字符串比较 |
| ✅ UART 带宽节省 75%+ | 14字节/param vs 30字节 |
| ✅ MCU 友好 | 无动态内存,无字符串处理 |
| ❌ 可读性差 | 0x0201 需查表才知是 EQ.bandFreq |
| ❌ 中心协调 | 新模块需要统一分配 ID,防止碰撞 |
| ❌ 调试门槛 | 需要随身携带映射表 |
4.3 混合方案(推荐)
┌──────────────────────────────────────────────────────────────────┐
│ Layer │ ID Type │ Reason │
│─────────────────────────┼─────────────┼──────────────────────────│
│ Frontend ↔ Backend JSON │ String │ 可读,schema一致,工具友好│
│ Backend ↔ DSP Binary │ Numeric │ 紧凑,DSP处理快,带宽低 │
│ DSP Debug Log │ String │ 可选,宏编译开关 │
└──────────────────────────────────────────────────────────────────┘
Backend 维护映射表:
string "gain#1.gainDb#2" → (moduleIdx=0, paramTypeId=0x0101, channelIdx=2)
通过 ParamIdMapper 服务完成转换
4.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 0x0104 = Gain.smoothMs
0x0201 = EQ.bandFreq 0x0202 = EQ.bandGain
0x0203 = EQ.bandQ 0x0204 = EQ.bandType
0x0205 = EQ.bandEnable 0x0206 = EQ.moduleEnable
0x0301 = Delay.delaySamples 0x0302 = Delay.enable
5. 多维参数寻址 (EQ 等多通道多波段模块)
5.1 问题背景
EQ with 10 channels × 10 bands × 5 param types = 500 parameter combinations. 旧方案:枚举 500 个 ParamSchema 条目 → 不可维护。
5.2 解决方案:ParamDimension
前端 TypeScript 类型定义:
export interface ParamDimension {
name: string // "channel", "band", "filter_stage"
count: number // how many elements in this dimension
labels?: string[] // optional labels: ["ch0".."ch9"], ["60Hz","200Hz"...]
}
export interface ParamSchema {
paramId: string
paramTypeId: number // NEW: u16 numeric ID for binary protocol
// ...other fields...
dimensions?: ParamDimension[] // REPLACES old 'channels: number' field
// dimensions undefined/empty → scalar param (no channel indexing)
// dimensions.length == 1 → channel strip UI (backward compat)
// dimensions.length == 2 → 2D grid UI (e.g. EQ 10ch×10band)
}
EQ ModuleDescriptor 示例:
{
typeId: "common_eq_v1",
defaultPorts: [...],
paramSchemas: [
{ paramId:"bandFreq", paramTypeId:0x0201, type:"float", min:20, max:20000, unit:"Hz",
dimensions:[{name:"channel",count:10},{name:"band",count:10}], role:"tunable" },
{ paramId:"bandGain", paramTypeId:0x0202, type:"float", min:-20, max:20, unit:"dB",
dimensions:[{name:"channel",count:10},{name:"band",count:10}], role:"tunable" },
{ paramId:"bandQ", paramTypeId:0x0203, type:"float", min:0.1, max:20,
dimensions:[{name:"channel",count:10},{name:"band",count:10}], role:"tunable" },
{ paramId:"bandType", paramTypeId:0x0204, type:"int",
dimensions:[{name:"channel",count:10},{name:"band",count:10}], role:"system" },
{ paramId:"enable", paramTypeId:0x0206, type:"bool", role:"system" } // scalar
]
}
5.3 后端 channelIdx 编码
ushort EncodeChannelIdx(int[]? dimIndices) => dimIndices switch {
null or [] => 0, // scalar
[var d0] => (ushort)d0, // 1D
[var d0, var d1] => (ushort)((d0 << 8) | d1), // 2D: ch×band
_ => throw new InvalidOperationException()
};
5.4 DSP 侧解码
#define CHAN_IDX_DIM0(idx) ((uint8_t)((idx) >> 8)) /* channel */
#define CHAN_IDX_DIM1(idx) ((uint8_t)((idx) & 0xFF)) /* band */
/* In EQ setParamNum: */
case EQ_PARAM_BAND_FREQ: {
uint8_t ch = CHAN_IDX_DIM0(channelIdx);
uint8_t band = CHAN_IDX_DIM1(channelIdx);
eq->bands[ch][band].targetFreq = *(float*)pValue;
return DYNCHAIN_OK;
}
5.5 参数条目数量对比
| 模块 | 旧方案 | 新方案 |
|---|---|---|
| Gain (20ch) | 20 × ParamSchema entries for gainDb | 1 ParamSchema entry with dimensions:[{count:20}] |
| EQ (10ch×10band×5type) | 500 entries | 5 entries with dimensions:[{count:10},{count:10}] |
| Delay (20ch) | 20 entries | 1 entry with dimensions:[{count:20}] |
6. 音效模式管理数据结构 (ModulePreset / AmbianceProfile / ParamBin)
6.1 三层层次结构
ModulePreset (每模块一套参数快照)
presetId, name, instanceId
params: { "gainDb[0]":"3.0", "gainDb[1]":"-1.5" }
↓ N个ModulePreset组合成
AmbianceProfile (一种音效模式)
profileId, name ("HIFI", "动感")
modulePresets: { "gain#1":"flat", "eq#1":"hifi_eq", "delay#1":"default" }
↓ M个AmbianceProfile打包成
ParamBin (二进制导出包, DSP侧一次性加载)
┌─────────────────────────────────────────────┐
│ Header: magic "PCFG", numAmbiances, numModules│
├─────────────────────────────────────────────┤
│ Module Table: instanceId[], numPresets[] │
├─────────────────────────────────────────────┤
│ Ambiance Table: name[], presetIndices[][] │
├─────────────────────────────────────────────┤
│ Param Data: [module][preset] → paramBlock │
└─────────────────────────────────────────────┘
6.2 前端 TypeScript 类型
export interface ModulePreset {
presetId: string
name: string
instanceId: string
params: Record<string, string>
// Key format: "paramId" (scalar), "paramId[d0]" (1D), "paramId[d0][d1]" (2D)
// Value: string representation of the param value
}
export interface AmbianceProfile {
profileId: string
name: string // "HIFI", "动感", "Rock"
icon?: string
modulePresets: Record<string, string> // instanceId → presetId
}
export interface ParamBinConfig {
targetProfiles: AmbianceProfile[]
modulePresetLimits: Record<string, number> // max presets allowed per module
}
6.3 后端 C# 类型
public record ModulePreset(
string InstanceId, string PresetId, string Name,
Dictionary<string, string> Params // key: "paramId[d0][d1]", value: "3.14"
);
public record AmbianceProfile(
string ProfileId, string Name,
Dictionary<string, string> ModulePresets // instanceId → presetId
);
public record ParamBinConfig(
List<AmbianceProfile> TargetProfiles,
Dictionary<string, int> ModulePresetLimits
);
6.4 DSP C 结构体
#define DYNCHAIN_MAX_AMBIANCES 16
#define DYNCHAIN_MAX_PRESETS_PER_MODULE 8
typedef struct ParamEntry_ {
uint16_t paramTypeId;
uint16_t channelIdx;
float value;
} ParamEntry;
typedef struct ModuleParamBlock_ {
uint16_t paramCount;
uint16_t pad;
ParamEntry params[/* paramCount */]; /* VLA, allocated from pool */
} ModuleParamBlock;
typedef struct AmbianceEntry_ {
char name[16];
uint8_t presetIndices[DYNCHAIN_MAX_MODULES];
} AmbianceEntry;
typedef struct ParamBinRuntime_ {
uint8_t numAmbiances;
uint8_t numModules;
uint8_t currentAmbianceIdx;
uint8_t pad;
AmbianceEntry ambiances[DYNCHAIN_MAX_AMBIANCES];
uint8_t numPresets[DYNCHAIN_MAX_MODULES];
ModuleParamBlock *modulePresets[DYNCHAIN_MAX_MODULES][DYNCHAIN_MAX_PRESETS_PER_MODULE];
} ParamBinRuntime;
6.5 音效系统三层字段映射
| 概念 | Frontend TS | Backend C# | DSP C |
|---|---|---|---|
| 模块参数快照 | ModulePreset |
ModulePreset record |
ModuleParamBlock |
| 音效模式 | AmbianceProfile |
AmbianceProfile record |
AmbianceEntry |
| 导出包 | ParamBinConfig |
ParamBinConfig record |
ParamBinRuntime |
| 参数键 | "bandFreq[2][5]" |
Dictionary<string,string> key |
paramTypeId:u16 + channelIdx:u16 |
| 切换操作 | set_ambiance WS msg |
CMD=0x05 binary |
DynChain_SetAmbiance(idx) |
| 加载操作 | generate_param_bin WS |
CMD=0x04 binary |
DynChain_LoadParamBin() |
7. 子图链路回读设计
7.1 问题背景
DSP 只感知平坦模块列表。旧版 get_dsp_chain 要求 DSP 回传自身链路,这会丢失子图层次结构信息。
7.2 解决方案
Frontend Backend DSP
| | |
|---get_dsp_chain------→ | |
| | (reads current_link.json)
|←--dsp_chain (LinkConfig v2.2 with sub-graphs)-- |
| [sub-graph hierarchy preserved] |
|---read_dsp_chain-----→ | |
| |---get_chain CMD=0x06→ |
| |←--flat module list--- |
| | ChainSyncService.Validate()
| | (compare flat vs stored)
|←--chain_sync_warning-- | (if mismatch) |
7.3 核心原则
LinkConfig(含子图层次)由 Backend 持久化为唯一真实来源(current_link.json)get_dsp_chain= 后端查询(快速,无 DSP 往返)read_dsp_chain= 仅用于可选校验,不用于重建层次结构
8. 后端数据结构(C#)
完整定义位于 Backend/Models/LinkConfig.cs 和 Backend/Models/FlattenedLinkConfig.cs。
8.1 链路 JSON 模型(镜像前端)
// 精确镜像前端 TypeScript 类型(用于 JSON 反序列化)
public record PortDescriptorConfig(
string Id,
string Direction, // "input" | "output"
int Channels, // -1 = inherit
int SampleRate, // -1 = inherit
string Label,
bool Required
);
public record ChainEdgeConfig(
string Id,
string FromModule,
string FromPort,
string ToModule,
string ToPort,
int Channels,
int SampleRate
);
public record ChainNodeConfig(
string InstanceId,
string ModuleType,
int Order,
bool Enabled,
PositionConfig Position,
List<PortDescriptorConfig> Ports
);
// SubGraphNodeConfig 继承 ChainNodeConfig,ModuleType 固定为 "subgraph"
public record SubGraphNodeConfig(
string InstanceId,
string SubGraphId,
PositionConfig Position,
List<PortDescriptorConfig> Ports
) : ChainNodeConfig(InstanceId, "subgraph", 0, true, Position, Ports);
public record ChainDefinitionConfig(
string Id,
string Name,
List<ChainNodeConfig> Nodes, // 含 SubGraphNodeConfig
List<ChainEdgeConfig> Edges,
List<PortDescriptorConfig>? ExternalPorts // null 表示根链路
);
public record GlobalConfig(int Channels, int SampleRate, int FrameSize);
public record LinkConfig(
string Version, // "2.2"
string RootChainId,
GlobalConfig Global,
Dictionary<string, ChainDefinitionConfig> Chains
);
8.2 展平后模型
// 经 ChainFlattener 展平后的中间表示(用于 BinaryFrameBuilder)
public record PortConfig(
string PortId,
string Direction, // "input" | "output"
int Channels,
int SampleRate
);
public record FlatModule(
string InstanceId, // 已展平:"group#1.delay#1"
string TypeId, // 模块类型字符串:"channel_gain_v1"
uint TypeNumId, // 对应 DSP module_type_id.h 中的数值
List<PortConfig> Ports
);
public record FlatConnection(
string FromModule, // 已展平的 instanceId
string FromPort, // 端口 ID
string ToModule,
string ToPort,
int Channels,
int SampleRate
);
public class FlattenedLinkConfig
{
public List<FlatModule> Modules { get; } = new();
public List<FlatConnection> Connections { get; } = new();
}
8.3 二进制帧布局(set_link v3)
由 BinaryFrameBuilder.BuildLinkFrameV3(FlattenedLinkConfig flat) 生成:
┌──────────────────────────────────────────────────────────────┐
│ TLV Header │
│ CMD: uint8 = 0x02 │
│ DATA_LEN: uint32 LE (字节数,不含 CMD 本身) │
├──────────────────────────────────────────────────────────────┤
│ Frame Header (4 bytes) │
│ VERSION: uint8 = 0x03 │
│ NUM_MODULES: uint8 (最多 32) │
│ NUM_CONNECTIONS: uint8 (最多 64) │
│ RESERVED: uint8 │
├──────────────────────────────────────────────────────────────┤
│ Module Table (NUM_MODULES × 88 bytes) │
│ Per module entry: │
│ instanceId: char[16] null-terminated │
│ typeNumId: uint32 LE │
│ numPorts: uint8 │
│ RESERVED: uint8[3] │
│ ports[8]: 8 × 8 bytes │
│ portId: char[6] null-terminated │
│ direction: uint8 0=in, 1=out │
│ required: uint8 │
│ channels: uint16 LE 0=inherit │
│ sr_div1k: uint16 LE sampleRate/1000 (48→48000Hz) │
├──────────────────────────────────────────────────────────────┤
│ Connection Table (NUM_CONNECTIONS × 8 bytes) │
│ Per connection entry: │
│ fromModuleIdx: uint8 模块表索引 │
│ fromPortIdx: uint8 该模块 ports[] 内的索引 │
│ toModuleIdx: uint8 │
│ toPortIdx: uint8 │
│ channels: uint16 LE │
│ sr_div1k: uint16 LE │
└──────────────────────────────────────────────────────────────┘
最大帧大小:5 + 4 + 32×88 + 64×8 = 5 + 4 + 2816 + 512 = 3337 bytes
9. DSP C 数据结构
完整定义位于 dspalgo/framework/ 目录下各头文件。
9.1 端口描述符
/* dynchain_port.h */
#define DYNCHAIN_MAX_PORTS_PER_MODULE 8
#define DYNCHAIN_PORT_ID_LEN 16
#define DYNCHAIN_PORT_DIR_IN 0
#define DYNCHAIN_PORT_DIR_OUT 1
#define DYNCHAIN_CH_INHERIT 0 /* channels = 0: 继承连接边的通道数 */
#define DYNCHAIN_SR_INHERIT 0 /* sampleRate = 0: 继承连接边的采样率 */
typedef struct PortSpec_ {
char portId[DYNCHAIN_PORT_ID_LEN];
uint8_t direction; /* DYNCHAIN_PORT_DIR_IN / _OUT */
uint8_t required; /* 1 = 必须连接 */
uint16_t channels; /* 0 = inherit */
uint32_t sampleRate; /* 0 = inherit */
} PortSpec;
typedef struct ModulePortCfg_ {
uint8_t numPorts;
PortSpec ports[DYNCHAIN_MAX_PORTS_PER_MODULE];
} ModulePortCfg;
9.2 处理输入
/* 单个输入端口的音频数据,由框架在调用 Process 前填充 */
typedef struct ProcessInput_ {
float **ppData; /* [ch][sample],未连接时为 NULL */
uint16_t channels; /* 此路的实际通道数 */
uint32_t sampleRate;
char portId[DYNCHAIN_PORT_ID_LEN]; /* 对应哪个输入端口 */
} ProcessInput;
9.3 模块接口函数表
/* module_interface.h */
/* 【必须实现】返回模块运行态内存需求(字节) */
typedef uint32_t (*ModuleGetMemSizeFn)(const ModulePortCfg *pPortCfg);
/* 【必须实现】初始化模块实例 */
typedef int32_t (*ModuleInitFn)(void *pSelf, const ModulePortCfg *pPortCfg);
/*
* 【必须实现】处理一帧音频
* inputs[0..numInputs-1] : 各输入端口的音频数据(由框架收集上游输出填充)
* ppOut : 输出缓冲区 [ch][sample](框架分配)
* outChannels : 输出通道数(由 portCfg 决定)
* nbSamples : 每帧采样点数
* 单输入模块(Gain/Delay/EQ)直接访问 inputs[0].ppData;
* 多输入模块(Mixer)遍历 inputs[0..numInputs-1]。
*/
typedef int32_t (*ModuleProcessFn)(
void *pSelf,
ProcessInput inputs[],
uint8_t numInputs,
float **ppOut,
uint16_t outChannels,
uint32_t nbSamples
);
/* 【必须实现】字符串路径设置参数(调试/JSON) */
typedef int32_t (*ModuleSetParamFn)(void *pSelf, const char *paramId,
const void *pValue, uint32_t valueSize);
/* 【必须实现】字符串路径读取参数 */
typedef int32_t (*ModuleGetParamFn)(void *pSelf, const char *paramId,
void *pOut, uint32_t bufSize, uint32_t *pWritten);
/* 【必须实现】数字路径设置参数(binary 协议) */
typedef int32_t (*ModuleSetParamNumFn)(void *pSelf, uint16_t paramTypeId,
uint16_t channelIdx,
const void *pValue, uint32_t valueSize);
/* 【必须实现】数字路径读取参数 */
typedef int32_t (*ModuleGetParamNumFn)(void *pSelf, uint16_t paramTypeId,
uint16_t channelIdx,
void *pOut, uint32_t bufSize, uint32_t *pWritten);
/* 【可选,可填 NULL】声明默认端口配置(供 ModuleRegistry 注册时使用) */
typedef int32_t (*ModuleGetDefaultPortsFn)(ModulePortCfg *pOut);
/* 【可选,可填 NULL】验证端口配置合法性(如 Mixer 输入数量检查) */
typedef int32_t (*ModuleValidatePortsFn)(const ModulePortCfg *pCfg);
/* 【可选,可填 NULL】销毁,释放模块内部动态资源(MemPool 外的资源) */
typedef void (*ModuleDestroyFn)(void *pSelf);
typedef struct ModuleFuncTable_ {
/* 必须实现 */
ModuleGetMemSizeFn getMemSize;
ModuleInitFn init;
ModuleProcessFn process;
ModuleSetParamFn setParam;
ModuleGetParamFn getParam;
ModuleSetParamNumFn setParamNum;
ModuleGetParamNumFn getParamNum;
/* 可选(NULL = 使用框架默认行为) */
ModuleGetDefaultPortsFn getDefaultPorts;
ModuleValidatePortsFn validatePorts;
ModuleDestroyFn destroy;
} ModuleFuncTable;
9.4 模块实例与连接表
/* dynchain_core.h */
#define DYNCHAIN_MAX_MODULES 32
#define DYNCHAIN_MAX_CONNECTIONS 64
#define DYNCHAIN_INSTANCE_ID_LEN 24 /* "group#1.delay#1\0..." */
#define DYNCHAIN_TYPE_ID_LEN 32
typedef struct ModuleInstance_ {
char instanceId[DYNCHAIN_INSTANCE_ID_LEN];
char typeId[DYNCHAIN_TYPE_ID_LEN];
uint32_t typeNumId;
const ModuleFuncTable *def;
void *pState; /* 状态内存(来自 MemPool) */
ModulePortCfg portCfg; /* 端口配置 */
float *outputBufferMem; /* 输出缓冲区内存(来自 MemPool) */
float *ppOut[32]; /* ppOut[ch] 指针数组 */
uint16_t resolvedOutChannels; /* 已解析的输出通道数 */
CaptureHook inputCapture;
CaptureHook outputCapture;
DebugMetrics metrics;
} ModuleInstance;
typedef struct DynChainConnection_ {
uint8_t fromModuleIdx; /* 索引入 DynamicChain.instances[] */
uint8_t fromPortIdx; /* 索引入该模块 portCfg.ports[] */
uint8_t toModuleIdx;
uint8_t toPortIdx;
uint16_t channels; /* 此路解析后通道数 */
uint32_t sampleRate; /* 此路解析后采样率 */
} DynChainConnection;
typedef struct DynamicChain_ {
ModuleInstance instances[DYNCHAIN_MAX_MODULES];
uint8_t numInstances;
DynChainConnection connections[DYNCHAIN_MAX_CONNECTIONS];
uint8_t numConnections;
int8_t orderedIndices[DYNCHAIN_MAX_MODULES]; /* 拓扑排序结果 */
MemPool pool;
DynChain_LogCallback logCb;
} DynamicChain;
10. 三层字段对应关系表
| 概念 | Frontend (TypeScript) | Backend (C#) | DSP C |
|---|---|---|---|
| 链路配置根对象 | LinkConfig |
LinkConfig |
DynamicChain |
| 链路定义 | ChainDefinition |
ChainDefinitionConfig |
- (展平后消失) |
| 模块节点 | ChainNode |
ChainNodeConfig → FlatModule |
ModuleInstance |
| 子图节点 | SubGraphNode |
SubGraphNodeConfig → 展平为普通模块 |
- (展平后消失) |
| 模块实例 ID | instanceId: string |
InstanceId: string |
instanceId: char[24] |
| 模块类型 | moduleType: string |
TypeId: string |
typeId: char[32] + typeNumId: uint32 |
| 端口描述符 | PortDescriptor |
PortDescriptorConfig → PortConfig |
PortSpec |
| 端口 ID | port.id: string |
PortId: string → portIdx: uint8(帧内) |
portId: char[16] → portIdx(连接表) |
| 端口方向 | 'input'\|'output' |
"input"\|"output" |
uint8 (0=in,1=out) |
| 通道数 | channels: number (-1=继承) |
Channels: int (-1=继承) |
uint16 (0=继承) |
| 采样率 | sampleRate: number (-1=继承) |
SampleRate: int (-1=继承) |
uint32 (0=继承) |
| 连线 | ChainEdge |
ChainEdgeConfig → FlatConnection |
DynChainConnection |
| 连线端口引用 | fromPort: string (端口 ID) |
FromPort: string → fromPortIdx: uint8 |
fromPortIdx: uint8 |
| 多输入 fan-in | N 条 ChainEdge 指向不同 toPort |
N 条 FlatConnection |
N 条 DynChainConnection → ProcessInput[] |
| 子图展平前缀 | SubGraphNode.instanceId |
ChainFlattener 加前缀 |
instanceId = "group#1.moduleName" |
| 处理顺序 | ChainNode.order (拓扑索引) |
由展平后拓扑排序计算 | orderedIndices[] |
| 参数寻址(字符串) | "instanceId.paramId#ch" |
ParamStore key |
setParam("instanceId.paramId#ch") |
| 参数寻址(数字) | paramTypeId: number (u16) |
ParamIdMapper 转换 |
setParamNum(paramTypeId, channelIdx) |
| 参数角色 | ParamSchema.role |
ModuleRegistry._paramRoles |
- (无对应,仅上位机逻辑) |
| 模块参数快照 | ModulePreset |
ModulePreset record |
ModuleParamBlock |
| 音效模式 | AmbianceProfile |
AmbianceProfile record |
AmbianceEntry |
| 导出包 | ParamBinConfig |
ParamBinConfig record |
ParamBinRuntime |
| 链路层次权威 | 前端发送 write_link |
Backend current_link.json(唯一真实来源) |
DSP 平坦模块列表(仅用于校验) |
11. 第三方模块开发接口
本节定义第三方开发者在 DSP C 侧开发新模块时必须满足的完整接口规范。
11.1 必须实现的函数列表
| 函数 | 签名 | 必须 | 说明 |
|---|---|---|---|
getMemSize |
uint32_t fn(const ModulePortCfg*) |
✅ | 内存需求 |
init |
int32_t fn(void*, const ModulePortCfg*) |
✅ | 初始化 |
process |
int32_t fn(void*, ProcessInput[], uint8_t, float**, uint16_t, uint32_t) |
✅ | 每帧处理 |
setParam |
int32_t fn(void*, const char*, const void*, uint32_t) |
✅ | 字符串路径(调试/JSON) |
getParam |
int32_t fn(void*, const char*, void*, uint32_t, uint32_t*) |
✅ | 字符串路径 |
setParamNum |
int32_t fn(void*, uint16_t, uint16_t, const void*, uint32_t) |
✅ | 数字路径(binary协议) |
getParamNum |
int32_t fn(void*, uint16_t, uint16_t, void*, uint32_t, uint32_t*) |
✅ | 数字路径 |
getDefaultPorts |
int32_t fn(ModulePortCfg*) |
— | 可选 |
validatePorts |
int32_t fn(const ModulePortCfg*) |
— | 可选 |
destroy |
void fn(void*) |
— | 可选 |
11.2 可选实现的函数
| 函数 | 说明 | 为 NULL 时的默认行为 |
|---|---|---|
getDefaultPorts |
声明模块默认端口布局(供注册表填充 UI 用) | 框架使用 1 input + 1 output 的 passthrough 默认值 |
validatePorts |
检查端口配置合法性(如 Mixer 输入数范围) | 总是返回 DYNCHAIN_OK(不做检查) |
destroy |
释放模块内部动态分配的资源(MemPool 外) | 不做任何操作(适合纯静态内存模块) |
11.3 参数 ID 命名规范
paramId 格式(来自后端转发):
"instanceId.paramName#channelIndex"
示例:
"gain#1.gainDb#0" → gain#1 实例,通道 0 的增益
"gain#1.gainDb#1" → gain#1 实例,通道 1 的增益
"eq#1.bandFreq#2" → eq#1 实例,第 2 个 band 的频率(通道索引即 band 索引)
"gain#1.enable#0" → enable 参数(通道 0,实际全局生效)
setParam 收到的 paramId 已去掉实例前缀:
框架调用 pInst->def->setParam(pSelf, "gainDb#0", pValue, size)
(instanceId 前缀由框架剥离,模块只处理 "paramName#ch" 部分)
setParamNum 收到数字 ID(无需字符串解析):
框架调用 pInst->def->setParamNum(pSelf, 0x0101, channelIdx, pValue, size)
模块通过 switch(paramTypeId) 直接跳转,O(1) 查找
11.4 返回值规范
#define DYNCHAIN_OK 0
#define DYNCHAIN_ERR_NULL_PTR -1
#define DYNCHAIN_ERR_INVALID_PARAM -2
#define DYNCHAIN_ERR_OUT_OF_MEMORY -3
#define DYNCHAIN_ERR_UNKNOWN_PARAM -4 /* setParam/getParam 不认识此 paramId 时返回 */
#define DYNCHAIN_ERR_INVALID_PORT_CFG -5 /* validatePorts 检查失败时返回 */
#define DYNCHAIN_ERR_NOT_IMPLEMENTED -6 /* 可选函数尚未实现时返回 */
#define DYNCHAIN_ERR_UNSUPPORTED_VERSION -7
11.5 配套 ParamSchema JSON 规范
前端 ModuleDescriptor.paramSchemas 需与 DSP 模块的 setParam/setParamNum 完全对应。每个参数条目:
{
"paramId": "gainDb",
"paramTypeId": 257,
"type": "float",
"min": -120.0,
"max": 12.0,
"step": 0.1,
"default": 0.0,
"unit": "dB",
"label": "增益",
"dimensions": [{ "name": "channel", "count": 20 }],
"role": "tunable"
}
dimensions[0].count > 0 表示每通道独立参数,框架会为每个维度组合分别调用 setParamNum(paramTypeId, channelIdx, ...),其中 channelIdx 由 EncodeChannelIdx 编码。
11.6 最简 Passthrough 模块示例
/* ── passthrough_module.c ─────────────────────────────────── */
#include "dynchain_core.h"
#include <string.h>
/* 无需运行态内存(纯透传),但至少分配 1 字节 */
static uint32_t Passthrough_GetMemSize(const ModulePortCfg *pCfg)
{
(void)pCfg;
return 1;
}
static int32_t Passthrough_Init(void *pSelf, const ModulePortCfg *pCfg)
{
(void)pSelf; (void)pCfg;
return DYNCHAIN_OK;
}
static int32_t Passthrough_Process(void *pSelf,
ProcessInput inputs[], uint8_t numInputs,
float **ppOut, uint16_t outChannels, uint32_t nbSamples)
{
(void)pSelf;
if (numInputs == 0 || inputs[0].ppData == NULL) return DYNCHAIN_OK;
/* 直接复制输入到输出 */
uint16_t ch = (inputs[0].channels < outChannels) ? inputs[0].channels : outChannels;
for (uint16_t c = 0; c < ch; c++) {
memcpy(ppOut[c], inputs[0].ppData[c], nbSamples * sizeof(float));
}
return DYNCHAIN_OK;
}
static int32_t Passthrough_SetParam(void *pSelf, const char *paramId,
const void *pValue, uint32_t valueSize)
{
(void)pSelf; (void)pValue; (void)valueSize;
return DYNCHAIN_ERR_UNKNOWN_PARAM; /* 无参数 */
}
static int32_t Passthrough_GetParam(void *pSelf, const char *paramId,
void *pOut, uint32_t bufSize, uint32_t *pWritten)
{
(void)pSelf; (void)pOut; (void)bufSize;
*pWritten = 0;
return DYNCHAIN_ERR_UNKNOWN_PARAM;
}
static int32_t Passthrough_SetParamNum(void *pSelf, uint16_t paramTypeId,
uint16_t channelIdx, const void *pValue, uint32_t valueSize)
{
(void)pSelf; (void)paramTypeId; (void)channelIdx; (void)pValue; (void)valueSize;
return DYNCHAIN_ERR_UNKNOWN_PARAM; /* 无参数 */
}
static int32_t Passthrough_GetParamNum(void *pSelf, uint16_t paramTypeId,
uint16_t channelIdx, void *pOut, uint32_t bufSize, uint32_t *pWritten)
{
(void)pSelf; (void)paramTypeId; (void)channelIdx; (void)pOut; (void)bufSize;
*pWritten = 0;
return DYNCHAIN_ERR_UNKNOWN_PARAM;
}
static int32_t Passthrough_GetDefaultPorts(ModulePortCfg *pOut)
{
pOut->numPorts = 2;
strncpy(pOut->ports[0].portId, "input", DYNCHAIN_PORT_ID_LEN-1);
pOut->ports[0].direction = DYNCHAIN_PORT_DIR_IN;
pOut->ports[0].required = 1;
pOut->ports[0].channels = DYNCHAIN_CH_INHERIT;
pOut->ports[0].sampleRate= DYNCHAIN_SR_INHERIT;
strncpy(pOut->ports[1].portId, "output", DYNCHAIN_PORT_ID_LEN-1);
pOut->ports[1].direction = DYNCHAIN_PORT_DIR_OUT;
pOut->ports[1].required = 1;
pOut->ports[1].channels = DYNCHAIN_CH_INHERIT;
pOut->ports[1].sampleRate= DYNCHAIN_SR_INHERIT;
return DYNCHAIN_OK;
}
/* 注册函数表 */
const ModuleFuncTable g_PassthroughFuncTable = {
.getMemSize = Passthrough_GetMemSize,
.init = Passthrough_Init,
.process = Passthrough_Process,
.setParam = Passthrough_SetParam,
.getParam = Passthrough_GetParam,
.setParamNum = Passthrough_SetParamNum,
.getParamNum = Passthrough_GetParamNum,
.getDefaultPorts= Passthrough_GetDefaultPorts,
.validatePorts = NULL, /* 不需要 */
.destroy = NULL, /* 不需要 */
};
11.7 第三方模块集成检查清单
| 步骤 | 内容 | 检查项 |
|---|---|---|
| 1 | 实现 C 函数表 | 7 个必须函数全部非 NULL;process 使用新多输入签名 |
| 2 | 注册到 ModuleRegistry | 调用 ModuleRegistry_Register(typeNumId, typeIdStr, &g_FuncTable) |
| 3 | 分配 typeNumId | 在 module_type_id.h 中添加唯一数值常量(高 16 位=类别,低 16 位=模块号) |
| 4 | 编写 ParamSchema JSON | paramId、paramTypeId、type、min/max/default、role 与 DSP setParam/setParamNum 一致 |
| 5 | 编写 ModuleDescriptor | typeId、defaultPorts(与 getDefaultPorts 返回匹配)、paramSchemas(含 dimensions) |
| 6 | 注册到前端 moduleRegistry.ts | registerModule(descriptor) |
| 7 | 实现 setParamNum/getParamNum |
使用 YOURMODULE_PARAM_* 常量,switch 分支覆盖所有 paramTypeId |
| 8 | 定义参数 ID 常量头文件 | 在 yourmodule_param_ids.h 中按高低字节约定定义 YOURMODULE_PARAM_* 常量 |
| 9 | (可选) 编写专属 Vue 面板 | GainModulePanel.vue 为参考实现 |
| 10 | CMakeLists.txt | 将新模块 .c 文件加入 dynchain_sources 列表 |
12. 常见数据类型说明
| 类型 | Frontend | Backend | DSP C |
|---|---|---|---|
| 实例 ID 字符串 | string |
string |
char[24] |
| 类型 ID 字符串 | string |
string |
char[32] |
| 类型 ID 数值 | - | uint TypeNumId |
uint32_t typeNumId |
| 通道数(继承) | number (-1) |
int (-1) |
uint16_t (0) |
| 采样率(继承) | number (-1) |
int (-1) |
uint32_t (0) |
| 浮点参数 | number |
float / double |
float |
| 帧大小 | global.frameSize |
GlobalConfig.FrameSize |
nbSamples (传入 Process) |
| 参数类型 ID | paramTypeId: number (u16) |
ushort ParamTypeId |
uint16_t paramTypeId |
| 通道维度索引 | dimensions[].count |
EncodeChannelIdx(int[]) |
uint16_t channelIdx (编码后) |
本文档随架构演进持续更新。当前版本对应系统架构 v3.0,文档版本 v2.0。