跳转至

跨层数据结构参考手册 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.tsfrontend/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.csBackend/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();
}

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 ChainNodeConfigFlatModule ModuleInstance
子图节点 SubGraphNode SubGraphNodeConfig → 展平为普通模块 - (展平后消失)
模块实例 ID instanceId: string InstanceId: string instanceId: char[24]
模块类型 moduleType: string TypeId: string typeId: char[32] + typeNumId: uint32
端口描述符 PortDescriptor PortDescriptorConfigPortConfig PortSpec
端口 ID port.id: string PortId: stringportIdx: 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 ChainEdgeConfigFlatConnection DynChainConnection
连线端口引用 fromPort: string (端口 ID) FromPort: stringfromPortIdx: uint8 fromPortIdx: uint8
多输入 fan-in N 条 ChainEdge 指向不同 toPort N 条 FlatConnection N 条 DynChainConnectionProcessInput[]
子图展平前缀 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, ...),其中 channelIdxEncodeChannelIdx 编码。

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。