跳转至

后端架构设计 (v5.0)

1. 技术栈

组件 技术 说明
运行时 .NET 8 跨平台支持
Web 框架 ASP.NET Core 8 WebSocket + REST API
WebSocket 内置 Microsoft.AspNetCore.WebSockets 全双工通信
序列化 System.Text.Json JSON 消息解析
TCP 传输 System.Net.Sockets.TcpClient 与 DSP 硬件通信
串口传输 System.IO.Ports.SerialPort Windows 串口通信
持久化 本地 JSON 文件 链路配置存储
并发容器 ConcurrentDictionary 线程安全参数存储
音频 I/O NAudio 2.x (WASAPI) PC 仿真音频采集与播放
跨平台音频 PortAudio 19.x 跨平台音频备选方案
算法调用 DllImport / P/Invoke 调用 C 算法 DLL(DynamicChain)
测试脚本 YamlDotNet 解析 YAML 格式自动化测试脚本

2. 整体架构

┌──────────────────────────────────────────────────────────────────────────────────┐
│                              ASP.NET Core 8 Host                                  │
│                                                                                   │
│  ┌──────────────────┐    ┌──────────────────────────────────────────────────────┐ │
│  │    REST API       │    │               WebSocket Server (/ws)                 │ │
│  │  /api/status      │    │    ConcurrentDictionary<clientId, WebSocket>         │ │
│  │  /api/link        │    └─────────────────────┬────────────────────────────────┘ │
│  │  /api/debug/wav   │                          │                                 │
│  │  /api/debug/      │                          │ HandleMessageAsync()            │
│  │    metrics        │                          │                                 │
│  │  /api/audio/      │         ┌────────────────▼────────────────────────────┐   │
│  │    devices        │         │           消息路由层 (type switch)            │   │
│  └──────────────────┘         └──┬──────┬──────┬──────┬──────┬──────┬───────┘   │
│                                  │      │      │      │      │      │            │
│                           set_   │ link │ dsp  │ ping │chain │sim_* │ debug_*    │
│                           param  │      │      │      │      │      │ set_mode   │
│                                  ▼      ▼      ▼      ▼      ▼      ▼            │
│                                                                                   │
│  ┌──────────────────┐  ┌──────────────────┐  ┌──────────────────────────────┐   │
│  │   ParamStore     │  │  ChainService    │  │   DspConnectionService       │   │
│  │ ConcDict<k,v>    │  │  JSON 持久化     │  │   TCP / SerialPort           │   │
│  │ "iid.pid#ch"→str │  │  链路解析/验证   │  └────────────┬─────────────────┘   │
│  └────────┬─────────┘  └────────┬─────────┘               │                    │
│           │                     │                          │                    │
│           │            ┌────────▼─────────────────────┐   │                    │
│           │            │        ChainFlattener         │   │                    │
│           │            │  (flatten sub-graphs,         │   │                    │
│           │            │   resolve port specs)         │   │                    │
│           │            └────────┬─────────────────────┘   │                    │
│           │                     │                          │                    │
│           │            ┌────────▼──────────────────────────▼──────────────┐    │
│           │            │               BinaryFrameBuilder                  │    │
│           │            │   BuildSetParamFrame() / BuildLinkFrameV3()       │    │
│           │            └───────────────────────────────────┬──────────────┘    │
│           │                                                │ byte[]             │
│           │                              ┌─────────────────▼──────────────┐    │
│           │                              │          DspConnection          │    │
│           │                              │       TCP / SerialPort          │    │
│           │                              └─────────────────────────────────┘    │
│           │                                                                      │
│           │   set_param 双轨路由                                                  │
│           │   ┌─────────────────────────────────────────────────────────────┐   │
│           │   │  实体 DSP 在线  →  BinaryFrameBuilder → DspConnection       │   │
│           │   │  PC 仿真运行中  →  AudioEngineService.SetParam() (P/Invoke)  │   │
│           │   └─────────────────────────────────────────────────────────────┘   │
│           │                                                                      │
│           │          ┌───────────────────────────────────────────────────────┐  │
│           │          │           AudioEngineService(PC 仿真服务)             │  │
│           │          │   NAudio WASAPI 采集/播放 + DynChainInterop P/Invoke   │  │
│           │          │                                                         │  │
│           │          │   ┌──────────────────────────────────────────────┐     │  │
│           │          │   │           DynamicChain.dll (C 算法库)         │     │  │
│           │          │   │  DynChain_Create / LoadConfig / SetParam      │     │  │
│           │          │   │  DynChain_Process / GetMetrics / SetLogCb     │     │  │
│           │          │   └──────────────────────────────────────────────┘     │  │
│           │          └───────────────────────────────────────────────────────┘  │
│           │                                                                      │
│           │          ┌──────────────────────┐  ┌──────────────────────────┐    │
│           │          │   DebugDataService   │  │  TestAutomationService   │    │
│           │          │   指标/日志/WAV 抓取  │  │  YAML 测试脚本执行引擎   │    │
│           │          └──────────────────────┘  └──────────────────────────┘    │
│           │                                                                      │
│           └───────────────── broadcast param_update ──────────────────────────  │
│                                                                                   │
│  ┌──────────────────────────────────────────────────────────────────────────┐   │
│  │              WorkModeManager(模式管理)                                    │   │
│  │  _currentMode: chain_builder | tuning | test_verify                       │   │
│  │  参数角色验证 (tunable/system) + write_link 权限检查                       │   │
│  └──────────────────────────────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────────────────────────────────┘

write_link 处理流水线

write_link →  ChainService (JSON persist)
              ChainFlattener
              (flatten sub-graphs,
               resolve port specs)
           BinaryFrameBuilder
           BuildLinkFrameV3()
             DspConnection

3. 各模块详细设计

3.1 WebSocket Server

职责:管理多客户端连接生命周期,接收/发送 JSON 消息。

连接管理

// 每个连接分配唯一 8 字符 ID
ConcurrentDictionary<string, WebSocket> _clients

// 连接建立时:
// 1. 分配 clientId
// 2. 推送 dsp_status(让客户端立即知道当前连接状态)
// 3. 进入接收循环

接收循环

// 缓冲区 65536 bytes,支持分片消息合并
while (!result.CloseStatus.HasValue) {
    string json = Encoding.UTF8.GetString(buffer, 0, result.Count)
    await HandleMessageAsync(clientId, ws, json)
    result = await ws.ReceiveAsync(...)
}

3.2 消息路由层

路由表

type Handler 说明
ping HandlePing() 返回 pong + UTC 时间戳
set_param HandleSetParam() 设置单个参数,触发 DSP 发送(支持双轨路由)
set_params HandleSetParam() 兼容别名(同 set_param)
get_param HandleGetParam() 查询单个参数
get_all_params HandleGetAllParams() 查询实例全量参数
write_link HandleWriteLink() 保存链路 JSON,触发 DSP 链路下发(受模式权限限制)
read_link HandleReadLink() 从文件加载链路 JSON
connect_dsp HandleConnectDsp() 建立 TCP/串口连接
disconnect_dsp HandleDisconnectDsp() 断开 DSP 连接
get_status BuildDspStatusMessage() 返回服务端状态
set_mode HandleSetMode() 切换工作模式(chain_builder/tuning/test_verify)
get_dsp_chain HandleGetDspChain() 获取当前 DSP 链路配置
sim_start HandleSimStart() 启动 PC 仿真(指定输入/输出声卡)
sim_stop HandleSimStop() 停止 PC 仿真
list_audio_devices HandleListAudioDevices() 枚举系统音频设备列表
debug_subscribe HandleDebugSubscribe() 订阅调试数据推送(指定推送间隔)
debug_unsubscribe HandleDebugUnsubscribe() 取消订阅调试数据推送
wav_capture_start HandleWavCaptureStart() 在指定节点启动 WAV 数据抓取
其他 Error(...) 返回 error 消息

3.3 ParamStore(全局参数存储)

数据结构

// 线程安全的全局参数字典
ConcurrentDictionary<string, string> _paramStore

// Key 格式: "${instanceId}.${paramId}"(paramId 已含通道后缀)
// 例:
//   "gain#1.enable"          → "true"
//   "gain#1.gainDb#0"        → "-3.5"
//   "delay#1.delaySamples#3" → "96"

注意:值统一以字符串存储(来自 JSON 的 .ToString()),由 BinaryFrameBuilder 解析类型。

set_param 完整流程

前端消息: { type:"set_param", instanceId:"gain#1", paramId:"gainDb", value:-3.5, channel:0 }

1. 拼接 storeKey = "gain#1.gainDb#0"
2. paramStore["gain#1.gainDb#0"] = "-3.5"
3. TryFlushToDspAsync("gain#1", "gainDb#0", "-3.5")  [fire & forget]
   → BinaryFrameBuilder.BuildSetParamFrame()
   → dspConn.SendAsync(frame)
4. BroadcastExcept(senderWs, { type:"param_update", ... })  [fire & forget]
5. 返回 { type:"set_param_ack", success:true }

3.4 ChainService(链路管理服务)

职责: - 接收前端的链路 JSON,持久化到本地文件 - 解析链路配置,验证模块 instanceId 合法性 - 将原始链路 JSON(含子图)持久化存储,供 ChainFlattener 消费

class ChainService {
    private readonly string _linkPath = "./data/current_link.json";
    private string? _currentLinkJson;

    // 保存链路 JSON(覆盖写入,含子图结构)
    public async Task SaveAsync(LinkConfig linkConfig)

    // 读取链路 JSON(文件不存在返回 null)
    public async Task<string?> LoadLinkAsync()
}

3.4a ChainFlattener(链路展平服务)

职责:将前端的 LinkConfig(可能包含子图)转换为 DSP 可理解的平坦 FlattenedLinkConfig

子图展平规则: 1. 对当前链路中的每个 SubGraphNode,递归展平其子 ChainDefinition 2. 将子链路中所有模块实例 ID 加上 "${subGraphNodeInstanceId}." 前缀(例如 "group#1.delay#1") 3. 映射子图外部端口连接:进出 SubGraphNode 的连接重映射到子图边界处的实际内部模块 4. 递归执行直到不再存在子图节点

// Services/ChainFlattener.cs
public class ChainFlattener
{
    public FlattenedLinkConfig Flatten(LinkConfig config)
    {
        var flat = new FlattenedLinkConfig();
        FlattenChain(config, config.RootChainId, "", flat);
        return flat;
    }

    private void FlattenChain(LinkConfig config, string chainId, string prefix, FlattenedLinkConfig flat)
    {
        var chain = config.Chains[chainId];
        foreach (var node in chain.Nodes)
        {
            if (node is SubGraphNodeConfig sg)
            {
                // recurse with prefix extended
                FlattenChain(config, sg.SubGraphId, prefix + sg.InstanceId + ".", flat);
                // remap edges crossing the sub-graph boundary
                RemapSubGraphEdges(config, chain, sg, prefix, flat);
            }
            else
            {
                flat.Modules.Add(new FlatModule {
                    InstanceId = prefix + node.InstanceId,
                    TypeId     = node.ModuleType,
                    Ports      = node.Ports.Select(p => new PortConfig(p)).ToList()
                });
            }
        }
        // Add intra-chain connections (not crossing sub-graph boundaries)
        foreach (var edge in chain.Edges)
        {
            if (!IsSubGraphBoundaryEdge(chain, edge))
            {
                flat.Connections.Add(new FlatConnection {
                    FromModule  = prefix + edge.FromModule,
                    FromPort    = edge.FromPort,
                    ToModule    = prefix + edge.ToModule,
                    ToPort      = edge.ToPort,
                    Channels    = edge.Channels,
                    SampleRate  = edge.SampleRate
                });
            }
        }
    }
}

3.5 ConversionService(参数转换服务)

职责:将前端 UI 单位的参数值转换为 DSP 内部格式(供后续扩展,当前版本参数以字符串透传)。

static class ConversionService {
    // dB → linear(增益模块)
    static float DbToLinear(float dB)
    static float LinearToDb(float linear)

    // 距离/时间 → 采样点数(延迟模块)
    static int CmToSamples(float cm, int sampleRate, float speedOfSound = 343f)
    static int MsToSamples(float ms, int sampleRate)

    // EQ 参数 → Biquad 系数(5 个浮点数)
    static float[] CalculateBiquadCoefficients(float freq, float gainDb, float q,
                                                string type, int sampleRate)
}

参数转换时机: - 当前版本:后端不做转换,原值透传到 DSP;DSP 侧接收后解析 - 未来版本:对 Gain 模块,后端可在发送时自动将 gainDb 转为 linear; 对 Delay 模块,如前端发送 cmms,后端可转换为 samples

3.6 ModuleRegistry(模块注册表)

职责:后端维护模块类型定义,用于参数验证、DSP 二进制帧中的模块类型 ID 映射,以及参数角色管理。

class ModuleRegistry {
    // 模块类型 → DSP 模块类型 ID(用于二进制帧)
    private readonly Dictionary<string, uint> _moduleTypeIdMap = new() {
        { "channel_gain_v1",    0x16673001 },
        { "ut_delay_20ch_v1",   0x16672001 },
        { "common_eq_v1",       0x16676001 },
        // ...更多模块
    };

    // 参数角色:tunable = 可调;system = 仅 Chain Builder 可改
    private readonly Dictionary<string, Dictionary<string, string>> _paramRoles = new() {
        ["channel_gain_v1"] = new() {
            ["enable"]     = "system",
            ["smoothTime"] = "system",
            ["gainDb"]     = "tunable",
            ["mute"]       = "tunable",
            ["phase"]      = "tunable",
        },
        ["ut_delay_20ch_v1"] = new() {
            ["enable"]        = "system",
            ["delaySamples"]  = "tunable",
        },
    };

    // 获取 DSP 模块类型 ID
    public uint GetDspModuleTypeId(string moduleType)

    // 验证 instanceId 格式("moduleType#index")
    public bool ValidateInstanceId(string instanceId)

    // 解析 instanceId → (moduleType, index)
    public (string moduleType, int index) ParseInstanceId(string instanceId)

    // 获取参数角色(tunable / system)
    public string GetParamRole(string instanceId, string paramId)
}

3.7 BinaryFrameBuilder(帧构造器)

职责:将高层逻辑(字符串参数、展平后的链路配置)序列化为 DSP 二进制帧。

帧格式(详见 Protocol_Design.md):

[55 AA] [CMD 1B] [SEQ 2B LE] [LEN 2B LE] [DATA...] [CRC8 1B]

static class BinaryFrameBuilder {
    private static ushort _seq = 0;

    // 构造 set_param 帧(CMD = 0x01)
    static byte[] BuildSetParamFrame(string instanceId, string paramId, string valueStr)

    // 构造 set_link 帧 v3(CMD = 0x02),接收展平后的链路配置
    // 连接条目使用模块索引(uint8)和端口索引(uint8),而非字符串名称
    static byte[] BuildLinkFrameV3(FlattenedLinkConfig flat)

    // 内部:值类型推断 + 序列化
    static (byte valueType, byte[] valueBytes) SerializeValue(string valueStr)

    // 内部:CRC8/SMBUS(多项式 0x07)
    static byte Crc8(byte[] data, int start, int len)
}

BuildLinkFrameV3 构建步骤: 1. 构建模块表(numModules 个条目) 2. 对每条连接,将 fromModule 字符串解析为模块索引,将 fromPort 字符串解析为该模块端口数组中的端口索引 3. 用索引构建连接表(保持二进制帧紧凑,适配嵌入式 DSP)

set_link DATA 格式(CMD = 0x02,v3):

Offset  Size   字段
0       2      模块数量 (uint16 LE)
2       N*M    模块条目列表:
               每个条目:
                 4B    moduleTypeId (uint32 LE)
                 1B    instanceId 字节长度
                 NB    instanceId (UTF-8)
                 1B    端口数量
                 P*K   端口条目列表:
                         1B    portId 字节长度
                         NB    portId (UTF-8)
                         1B    direction (0=input,1=output)
                         2B    channels (int16 LE, -1=inherit)
                         2B    sampleRate (int16 LE, -1=inherit)
连接段:
0       2      连接数量 (uint16 LE)
2       C*6    连接条目列表:
               每个条目:
                 1B    fromModule 索引 (uint8)
                 1B    fromPort 索引 (uint8,在该模块端口数组中)
                 1B    toModule 索引 (uint8)
                 1B    toPort 索引 (uint8,在该模块端口数组中)
                 2B    channels (int16 LE)
                 2B    sampleRate (int16 LE)

3.8 DspConnection(DSP 通信层)

职责:统一封装 TCP 和串口两种连接方式。

class DspConnection {
    public bool IsConnected { get; private set; }
    public string ConnectionType { get; private set; }  // "tcp"|"serial"|"none"
    public string Target { get; private set; }           // "host:port" 或 "COM3@115200"

    public Task<bool> ConnectTcpAsync(string host, int port)
    public Task<bool> ConnectSerialAsync(string comPort, int baudRate)
    public Task DisconnectAsync()
    public Task SendAsync(byte[] data)

    // 接收 DSP 响应(为后续双向通信预留)
    public Task StartReceiveLoopAsync(Func<byte[], Task> onReceived)
}

3.9 广播机制

// 向除 except 之外的所有已连接客户端广播消息
async Task BroadcastExcept(WebSocket? except, string json)

// 向所有已连接客户端广播消息
async Task BroadcastAll(string json)

// 使用场景:
// 1. set_param 后广播 param_update(多端同步)
// 2. DSP 连接/断开后广播 dsp_status(所有端感知状态变化)
// 3. write_link 后广播 link_updated(可选,通知其他端链路已更新)
// 4. set_mode 后广播 mode_changed(所有端同步模式状态)
// 5. sim_start/stop 后广播 sim_status(所有端感知仿真状态)
// 6. DLL 日志回调触发 log_stream 推送(所有订阅端)

3.10 WorkModeManager(模式管理)

职责:维护全局工作模式状态,根据模式对操作权限进行管控。

模式状态

后端维护当前全局工作模式,初始为 chain_builder

// 全局模式状态(thread-safe)
private volatile string _currentMode = "chain_builder";
// 合法值:chain_builder | tuning | test_verify

set_mode 处理

// 消息:{ type: "set_mode", mode: "tuning" }
async Task HandleSetMode(string clientId, WebSocket ws, JsonElement msg) {
    string mode = msg.GetProperty("mode").GetString()!;
    if (mode is not ("chain_builder" or "tuning" or "test_verify")) {
        await SendError(ws, $"无效模式: {mode}");
        return;
    }
    _currentMode = mode;
    // 广播模式变更给所有客户端
    await BroadcastAll(JsonSerializer.Serialize(new {
        type = "mode_changed", mode = _currentMode
    }));
}

参数角色检查(Tuning 模式)

HandleSetParam 中增加角色验证:

// Tuning 模式下,拒绝修改 system 参数
if (_currentMode == "tuning") {
    string role = _moduleRegistry.GetParamRole(msg.instanceId, msg.paramId);
    if (role == "system") {
        await Send(ws, new { type="set_param_ack", success=false,
                              error="调音模式下不允许修改系统参数" });
        return;
    }
}

链路写入权限检查

// tuning / test_verify 模式下拒绝 write_link
if (_currentMode != "chain_builder") {
    await Send(ws, new { type="write_link_ack", success=false,
                          error=$"当前模式({_currentMode})不允许修改链路结构" });
    return;
}

3.11 AudioEngineService(PC 仿真服务)

设计目标:通过 AudioEngineService 调用 DynamicChain.dll(C 算法库),结合 NAudio WASAPI 实现 PC 侧实时音频仿真,与实体 DSP 接口保持一致。

DLL P/Invoke 封装Interop/DynChainInterop.cs):

internal static class DynChainInterop {
    private const string DllName = "DynamicChain";

    [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
    public static extern int DynChain_Create(out IntPtr ppChain);

    [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
    public static extern int DynChain_Destroy(IntPtr pChain);

    [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
    public static extern int DynChain_LoadConfig(IntPtr pChain,
        [MarshalAs(UnmanagedType.LPUTF8Str)] string configJson, uint len);

    [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
    public static extern int DynChain_SetParam(IntPtr pChain,
        [MarshalAs(UnmanagedType.LPUTF8Str)] string instanceId,
        [MarshalAs(UnmanagedType.LPUTF8Str)] string paramId,
        IntPtr pValue, uint size);

    // Process:ppIn/ppOut 为 float*[] 指针数组(需 unsafe 或 GCHandle 固定)
    [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
    public static extern unsafe int DynChain_Process(IntPtr pChain,
        float** ppIn, float** ppOut, uint nbSamples);

    // 调试接口
    [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
    public static extern int DynChain_GetMetrics(IntPtr pChain, IntPtr pMetricsJson, uint bufSize);

    [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
    public static extern int DynChain_SetLogCallback(IntPtr pChain, LogCallbackDelegate cb);

    [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
    public static extern int DynChain_SetCaptureHook(IntPtr pChain,
        [MarshalAs(UnmanagedType.LPUTF8Str)] string instanceId,
        [MarshalAs(UnmanagedType.LPUTF8Str)] string capturePoint,
        int durationMs,
        [MarshalAs(UnmanagedType.LPUTF8Str)] string captureId);

    public delegate void LogCallbackDelegate(int level, string message);
}

AudioEngineService 实现Services/AudioEngineService.cs):

class AudioEngineService : IDisposable {
    private IntPtr         _chain = IntPtr.Zero;
    private WasapiCapture? _capture;
    private WasapiOut?     _output;
    private bool           _running;

    public bool IsRunning => _running;

    // 启动仿真:输入声卡 + 输出声卡
    public async Task StartAsync(string inputDeviceId, string outputDeviceId) {
        // 1. 创建 DynamicChain 实例
        DynChainInterop.DynChain_Create(out _chain);

        // 2. 注册日志回调
        DynChainInterop.DynChain_SetLogCallback(_chain, OnDspLog);

        // 3. 加载当前链路配置
        string linkJson = await _chainService.LoadLinkAsync() ?? "{}";
        DynChainInterop.DynChain_LoadConfig(_chain, linkJson, (uint)linkJson.Length);

        // 4. 配置 NAudio WASAPI
        _capture = new WasapiCapture(GetDevice(inputDeviceId)) {
            ShareMode = AudioClientShareMode.Exclusive
        };
        _output = new WasapiOut(GetDevice(outputDeviceId), AudioClientShareMode.Exclusive, false, 10);

        // 5. 连接音频回调
        _capture.DataAvailable += OnAudioData;
        _capture.StartRecording();
        _running = true;
    }

    private unsafe void OnAudioData(object sender, WaveInEventArgs e) {
        // 将 NAudio 交错 PCM 转换为分离通道浮点数
        // 调用 DynChain_Process
        // 写入输出缓冲
        // (需固定内存以传递 float** 指针)
        GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency;
    }

    // 对外暴露:供 HandleSetParam 调用(PC 仿真时直接注入参数,无需发二进制帧)
    public void SetParam(string instanceId, string paramId, float value) {
        if (_chain == IntPtr.Zero) return;
        unsafe {
            DynChainInterop.DynChain_SetParam(_chain, instanceId, paramId, &value, sizeof(float));
        }
    }

    public void Stop() {
        _running = false;
        _capture?.StopRecording();
        if (_chain != IntPtr.Zero) {
            DynChainInterop.DynChain_Destroy(_chain);
            _chain = IntPtr.Zero;
        }
    }
}

消息路由集成(set_param 双轨发送)

// set_param 双轨发送:实体 DSP 走二进制帧,PC 仿真走 P/Invoke
if (_dspConn.IsConnected) {
    byte[] frame = BinaryFrameBuilder.BuildSetParamFrame(instanceId, paramId, valueStr);
    await _dspConn.SendAsync(frame);
}
if (_audioEngine.IsRunning) {
    // 直接注入到算法链(转换为 float/int/bool)
    _audioEngine.SetParam(instanceId, paramId, ConvertToFloat(valueStr));
}

sim_start / sim_stop / list_audio_devices 消息处理

// sim_start: { type:"sim_start", input:{type:"soundcard",deviceName:"..."}, output:{...} }
async Task HandleSimStart(WebSocket ws, JsonElement msg) {
    string inputDev  = msg.GetProperty("input").GetProperty("deviceName").GetString()!;
    string outputDev = msg.GetProperty("output").GetProperty("deviceName").GetString()!;
    await _audioEngine.StartAsync(inputDev, outputDev);
    await Send(ws, new { type = "sim_status", running = true, status = "running" });
    await BroadcastAll(JsonSerializer.Serialize(new { type="sim_status", running=true }));
}

// sim_stop: { type:"sim_stop" }
async Task HandleSimStop(WebSocket ws) {
    _audioEngine.Stop();
    await BroadcastAll(JsonSerializer.Serialize(new {
        type = "sim_status", running = false, status = "stopped"
    }));
}

// list_audio_devices: 枚举系统声卡设备
async Task HandleListAudioDevices(WebSocket ws) {
    // 枚举 MMDevice 列表(输入设备 + 输出设备)
    var devices = EnumerateWasapiDevices();
    await Send(ws, new { type = "audio_devices_ack", devices });
}

3.12 DebugDataService(调试数据服务)

职责:从算法库采集运行时指标,向已订阅客户端定期推送调试数据,管理 WAV 节点抓取钩子,转发算法库日志。

指标采集与推送

// Services/DebugDataService.cs
class DebugDataService {
    private IntPtr _chain;  // 引用 AudioEngineService 的 DynamicChain 实例
    private readonly ConcurrentDictionary<string, int> _subscribedClients = new();
    private int _intervalMs = 500;

    // 从算法库采集指标,返回 JSON
    public string CollectMetricsJson() {
        var buf = new byte[4096];
        unsafe { fixed (byte* p = buf) {
            DynChainInterop.DynChain_GetMetrics(_chain, (IntPtr)p, (uint)buf.Length);
        }}
        return Encoding.UTF8.GetString(buf).TrimEnd('\0');
    }

    // 订阅管理
    public void Subscribe(string clientId, int intervalMs) {
        _subscribedClients[clientId] = intervalMs;
        _intervalMs = intervalMs;
    }
    public void Unsubscribe(string clientId) => _subscribedClients.TryRemove(clientId, out _);

    // 周期推送(后端定时器触发,推送给已订阅的客户端)
    public async Task PushMetricsLoop(CancellationToken ct) {
        while (!ct.IsCancellationRequested) {
            if (_subscribedClients.Count > 0) {
                string json = BuildDebugMetricsMessage();
                await BroadcastTo(_subscribedClients.Keys, json);
            }
            await Task.Delay(_intervalMs, ct);
        }
    }
}

WAV 节点抓取

// wav_capture_start: { type:"wav_capture_start", instanceId:"...", capturePoint:"output", durationMs:1000 }
async Task HandleWavCaptureStart(WebSocket ws, JsonElement msg) {
    string instanceId   = msg.GetProperty("instanceId").GetString()!;
    string capturePoint = msg.GetProperty("capturePoint").GetString()!;
    int    durationMs   = msg.GetProperty("durationMs").GetInt32();

    string captureId = Guid.NewGuid().ToString("N")[..8];

    // 在算法库注册抓取 Hook
    DynChainInterop.DynChain_SetCaptureHook(_chain, instanceId, capturePoint,
        durationMs, captureId);

    await Send(ws, new { type = "wav_capture_ack", captureId });

    // 算法库回调完成时通知前端
    _captureCallbacks[captureId] = async (wavPath) => {
        await Send(ws, new {
            type = "wav_capture_done",
            captureId,
            fileSizeBytes = new FileInfo(wavPath).Length
        });
    };
}

// REST:GET /api/debug/wav/{captureId}  → 返回 WAV 文件二进制流

日志流转发

// 算法库日志回调 → WebSocket log_stream 推送
private void OnDspLog(int level, string message) {
    string levelStr = level switch {
        0 => "DEBUG", 1 => "INFO", 2 => "WARN", 3 => "ERROR", _ => "INFO"
    };
    string json = JsonSerializer.Serialize(new {
        type      = "log_stream",
        level     = levelStr,
        source    = "dsp",
        message,
        timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
    });
    _ = BroadcastAll(json);   // fire & forget
}

debug_subscribe / debug_unsubscribe 消息

{ "type": "debug_subscribe", "intervalMs": 500 }
{ "type": "debug_unsubscribe" }
async Task HandleDebugSubscribe(string clientId, JsonElement msg) {
    int intervalMs = msg.GetProperty("intervalMs").GetInt32();
    _debugService.Subscribe(clientId, intervalMs);
}

async Task HandleDebugUnsubscribe(string clientId) {
    _debugService.Unsubscribe(clientId);
}

3.13 TestAutomationService(自动化测试)

职责:解析 YAML 测试脚本,按步骤自动执行参数设置、延时等待、WAV 抓取、RMS 验证,并生成 JUnit XML 报告。

YAML 测试脚本执行

// Services/TestAutomationService.cs
class TestAutomationService {
    // 运行单个 YAML 测试文件
    public async Task<TestResult> RunTestAsync(string yamlPath) {
        var script = ParseYaml(yamlPath);
        var result = new TestResult { Name = script.Name };

        // 确保目标连接
        await ConnectTarget(script.Target);

        // 按 steps 顺序执行
        foreach (var step in script.Steps) {
            switch (step.Action) {
                case "set_param":
                    await ExecuteSetParam(step);
                    break;
                case "wait_ms":
                    await Task.Delay(step.Ms);
                    break;
                case "capture_wav":
                    await ExecuteCapture(step);
                    break;
                case "verify_rms":
                    bool pass = VerifyRms(step);
                    result.Assertions.Add(new AssertionResult {
                        Description = $"RMS@ch{step.Channel}",
                        Pass        = pass,
                        Expected    = step.ExpectedRmsDb,
                        Actual      = MeasureRms(step.File, step.Channel)
                    });
                    break;
            }
        }

        result.Pass = result.Assertions.All(a => a.Pass);
        return result;
    }

    // 批量运行,输出 JUnit XML
    public async Task RunSuiteAsync(string suiteDir, string reportPath) {
        var files   = Directory.GetFiles(suiteDir, "*.yaml");
        var results = await Task.WhenAll(files.Select(RunTestAsync));
        ExportJUnitXml(results, reportPath);
    }

    // WAV 文件 RMS 验证(dBFS)
    float MeasureRms(string wavPath, int channel) {
        using var reader = new WaveFileReader(wavPath);
        float sumSq = 0; int count = 0;
        // 读取指定通道样本,跳过其他通道,累加平方和
        float rms = (float)Math.Sqrt(sumSq / count);
        return 20f * (float)Math.Log10(rms + 1e-10f);
    }
}

命令行入口

// Program.cs 扩展
if (args.Contains("--test")) {
    string yamlPath = args[Array.IndexOf(args, "--test") + 1];
    var result = await testService.RunTestAsync(yamlPath);
    Console.WriteLine(result.Pass ? "PASS" : "FAIL");
    Environment.Exit(result.Pass ? 0 : 1);
}
if (args.Contains("--test-suite")) {
    string dir    = args[Array.IndexOf(args, "--test-suite") + 1];
    string report = args.Contains("--report")
                    ? args[Array.IndexOf(args, "--report") + 1]
                    : "./test_report.xml";
    await testService.RunSuiteAsync(dir, report);
}

4. 数据模型

4.1 链路配置(Models/LinkConfig.cs

v3.0 起,后端数据模型精确镜像前端 TypeScript LinkConfig v2.2 结构,替代原 DspLinkConfig

// Models/LinkConfig.cs
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
);

public record SubGraphNodeConfig(
    string                    InstanceId,
    string /* = "subgraph" */ ModuleType,
    string                    SubGraphId,
    PositionConfig            Position,
    List<PortDescriptorConfig> Ports
) : ChainNodeConfig(InstanceId, ModuleType, 0, true, Position, Ports);

public record ChainDefinitionConfig(
    string                     Id,
    string                     Name,
    List<ChainNodeConfig>      Nodes,   // includes SubGraphNodeConfig
    List<ChainEdgeConfig>      Edges,
    List<PortDescriptorConfig>? ExternalPorts = null
);

public record LinkConfig(
    string  Version,       // "2.2"
    string  RootChainId,
    GlobalConfig Global,
    Dictionary<string, ChainDefinitionConfig> Chains
);

4.2 展平后链路配置(Models/FlattenedLinkConfig.cs

// Models/FlattenedLinkConfig.cs

// After flattening:
public record PortConfig(string PortId, string Direction, int Channels, int SampleRate);

public record FlatModule(string InstanceId, string TypeId, List<PortConfig> Ports);

public record FlatConnection(
    string FromModule, string FromPort,
    string ToModule,   string ToPort,
    int    Channels,   int SampleRate
);

public record FlattenedLinkConfig(
    List<FlatModule>     Modules,
    List<FlatConnection> Connections
);

4.3 全局与位置配置(Models/GlobalConfig.cs

public class GlobalConfig {
    public int Channels { get; set; }
    public int SampleRate { get; set; }
    public int FrameSize { get; set; }
}

public record PositionConfig(float X, float Y);

4.4 DSP 状态(Models/DspStatus.cs

public class DspStatus {
    public bool IsConnected { get; set; }
    public string Protocol { get; set; }     // "tcp"|"serial"|"none"
    public string Target { get; set; }        // "192.168.1.100:9000"
    public int SampleRate { get; set; }
    public int Channels { get; set; }
}

4.5 工作模式枚举(Models/WorkMode.cs

public enum WorkMode {
    ChainBuilder,   // chain_builder:可修改链路结构和所有参数
    Tuning,         // tuning:只允许修改 tunable 参数,禁止修改链路
    TestVerify      // test_verify:自动化测试模式,禁止修改链路
}

4.6 测试脚本模型(Models/TestScript.cs

// 对应 YAML 测试脚本结构
public class TestScript {
    public string Name { get; set; }           // 测试名称
    public string Target { get; set; }         // 连接目标(sim / tcp:host:port / serial:COM3)
    public string Chain { get; set; }          // 使用的链路配置文件路径
    public List<TestStep> Steps { get; set; }  // 步骤列表
}

public class TestStep {
    public string Action { get; set; }         // set_param / wait_ms / capture_wav / verify_rms
    public string InstanceId { get; set; }
    public string ParamId { get; set; }
    public object Value { get; set; }
    public int Ms { get; set; }                // wait_ms 用
    public string File { get; set; }           // capture_wav / verify_rms 用
    public int Channel { get; set; }           // verify_rms 用
    public float ExpectedRmsDb { get; set; }   // verify_rms 期望值
    public float ToleranceDb { get; set; }     // verify_rms 容差
}

public class TestResult {
    public string Name { get; set; }
    public bool Pass { get; set; }
    public List<AssertionResult> Assertions { get; set; } = new();
}

public class AssertionResult {
    public string Description { get; set; }
    public bool Pass { get; set; }
    public float Expected { get; set; }
    public float Actual { get; set; }
}

4.7 调试指标模型(Models/DebugMetrics.cs

public class DebugMetrics {
    public long TimestampMs { get; set; }          // UTC 毫秒时间戳
    public float CpuLoadPercent { get; set; }       // 算法链 CPU 占用(%)
    public int DroppedFrames { get; set; }          // 丢帧计数
    public Dictionary<string, ModuleMetrics> Modules { get; set; }
}

public class ModuleMetrics {
    public string InstanceId { get; set; }
    public float ProcessTimeUs { get; set; }        // 单帧处理耗时(μs)
    public float[] OutputRmsDb { get; set; }        // 各通道输出 RMS(dBFS)
}

5. 消息路由表

type Handler 说明
ping HandlePing() 返回 pong + UTC 时间戳
set_param HandleSetParam() 设置单个参数;双轨路由至实体 DSP 和/或 PC 仿真
set_params HandleSetParam() 兼容别名(同 set_param)
get_param HandleGetParam() 查询单个参数
get_all_params HandleGetAllParams() 查询实例全量参数
write_link HandleWriteLink() 保存链路 JSON,触发 DSP 链路下发(受模式权限限制)
read_link HandleReadLink() 从文件加载链路 JSON
get_dsp_chain HandleGetDspChain() 获取当前 DSP 链路配置
connect_dsp HandleConnectDsp() 建立 TCP/串口连接
disconnect_dsp HandleDisconnectDsp() 断开 DSP 连接
get_status BuildDspStatusMessage() 返回服务端状态(含仿真状态和当前模式)
set_mode HandleSetMode() 切换工作模式,广播 mode_changed
sim_start HandleSimStart() 启动 PC 仿真(指定输入/输出声卡)
sim_stop HandleSimStop() 停止 PC 仿真
list_audio_devices HandleListAudioDevices() 枚举 WASAPI 音频设备列表
debug_subscribe HandleDebugSubscribe() 订阅调试数据定期推送
debug_unsubscribe HandleDebugUnsubscribe() 取消订阅调试数据推送
wav_capture_start HandleWavCaptureStart() 在指定节点启动 WAV 数据抓取
其他 Error(...) 返回 error 消息

6. 关键数据流

6.1 set_param 完整流程

Frontend
  → WS: { type:'set_param', instanceId:'gain#1', paramId:'gainDb', value:-3.5, channel:0 }

Backend
  1. HandleSetParam():
     a. 工作模式检查:若 _currentMode == "tuning" 且 paramRole == "system" → 拒绝
     b. storeKey = "gain#1.gainDb#0"
     c. paramStore[storeKey] = "-3.5"
     d. [async] BinaryFrameBuilder.BuildSetParamFrame("gain#1","gainDb#0","-3.5") → bytes
                DspConnection.SendAsync(bytes)          (实体 DSP 在线时执行)
     e. [async] _audioEngine.SetParam("gain#1","gainDb#0", -3.5f)  (PC 仿真运行时执行)
     f. [async] BroadcastExcept(senderWs, {type:"param_update", instanceId:"gain#1",
                                            paramId:"gainDb#0", value:"-3.5"})
  2. → WS: { type:'set_param_ack', instanceId:'gain#1', paramId:'gainDb#0', success:true }
Frontend
  → WS: { type:'write_link', link: { version:"2.2", rootChainId:"main", chains:{...}, ... } }

Backend
  1. HandleWriteLink():
     a. 工作模式检查:若 _currentMode != "chain_builder" → 拒绝并返回错误
     b. linkConfig = JsonSerializer.Deserialize<LinkConfig>(msg)
     c. await _chainService.SaveAsync(linkConfig)     → ./data/current_link.json(含子图)
     d. [async] var flat = _chainFlattener.Flatten(linkConfig)
        → 递归展平所有子图,前缀拼接实例 ID
        → 重映射子图边界连接
     e. [async] var frame = _frameBuilder.BuildLinkFrameV3(flat)
        → 构建模块表(带端口描述)
        → 将连接中的模块/端口字符串解析为索引
        → 序列化为 v3 二进制帧
     f. await _dspConnection.SendAsync(frame)
  2. → WS: { type:'write_link_ack', success:true }

6.3 客户端重连恢复

Frontend (新连接)
  ← WS: { type:'dsp_status', connected:true, protocol:'tcp', target:'...' }  [后端主动推送]

Frontend
  → WS: { type:'read_link' }
  ← WS: { type:'read_link_ack', success:true, link:{...} }

Frontend
  → WS: { type:'get_all_params', instanceId:'gain#1' }
  ← WS: { type:'get_all_params_ack', instanceId:'gain#1', params:{"gainDb#0":"-3.5",...} }

6.4 PC 仿真 set_param 双轨路由流程

Frontend
  → WS: { type:'set_param', instanceId:'delay#1', paramId:'delaySamples', value:96, channel:2 }

Backend (PC 仿真运行中,实体 DSP 也已连接)
  1. paramStore["delay#1.delaySamples#2"] = "96"
  2. 双轨并发发送:
     轨道 A(实体 DSP):
       BinaryFrameBuilder.BuildSetParamFrame("delay#1","delaySamples#2","96") → bytes
       DspConnection.SendAsync(bytes)   [TCP/串口]
     轨道 B(PC 仿真):
       _audioEngine.SetParam("delay#1","delaySamples#2", 96f)
       → DynChainInterop.DynChain_SetParam(_chain, ...) [P/Invoke]
  3. BroadcastExcept(senderWs, param_update)
  4. → WS: { type:'set_param_ack', success:true }

6.5 调试指标采集推送流程

Frontend
  → WS: { type:'debug_subscribe', intervalMs:500 }

Backend
  1. HandleDebugSubscribe() → _debugService.Subscribe(clientId, 500)
  2. PushMetricsLoop(后台定时任务,500ms 间隔):
     a. DynChain_GetMetrics(_chain, buf, bufSize) → metricsJson
     b. 构造 debug_metrics 消息:
        { type:'debug_metrics', timestamp:..., cpuLoad:..., modules:{...} }
     c. BroadcastTo(subscribedClients, json)

Frontend
  ← WS: { type:'debug_metrics', timestamp:1700000000000, cpuLoad:12.5,
           modules:{ "gain#1":{ processTimeUs:45.2, outputRmsDb:[-18.3,...] } } }
  ...(每 500ms 推送一次)

Frontend
  → WS: { type:'debug_unsubscribe' }
  → 停止推送

7. 文件结构

backend/
├── Program.cs                      应用入口;注册服务;支持 --test / --test-suite 命令行
├── Handlers/
│   └── WebSocketHandler.cs         WebSocket 连接管理 + 消息路由(含全部新消息类型)
├── Services/
│   ├── ParamService.cs             参数存储 (ConcurrentDictionary)
│   ├── ChainService.cs             链路持久化与解析
│   ├── ChainFlattener.cs           新增:链路展平(子图→平坦模块列表)
│   ├── DspConnectionService.cs     TCP/串口通信封装
│   ├── ConversionService.cs        参数单位转换
│   ├── ModuleRegistryService.cs    模块注册表 + 参数角色表 GetParamRole()
│   ├── AudioEngineService.cs       PC 仿真(NAudio WASAPI + P/Invoke DLL)
│   ├── DebugDataService.cs         调试指标采集、日志转发、WAV 抓取管理
│   └── TestAutomationService.cs    YAML 测试脚本执行引擎
├── Interop/
│   └── DynChainInterop.cs          DynamicChain.dll P/Invoke 声明
├── Models/
│   ├── LinkConfig.cs               新增:前端链路 JSON v2.2 对应 C# 模型
│   ├── FlattenedLinkConfig.cs      新增:展平后的链路配置
│   ├── DspStatus.cs                DSP 状态数据模型
│   ├── BinaryFrame.cs              帧构造器(BuildSetParamFrame / BuildLinkFrameV3)
│   ├── WorkMode.cs                 工作模式枚举
│   ├── TestScript.cs               YAML 测试脚本数据模型
│   └── DebugMetrics.cs             调试指标数据模型
├── Controllers/
│   ├── StatusController.cs         GET /api/status(含仿真状态与当前模式)
│   ├── LinkController.cs           GET /api/link
│   └── DebugController.cs          GET /api/debug/wav/{captureId}、GET /api/debug/metrics
├── data/
│   └── current_link.json           链路快照(运行时写入,含完整子图结构)
├── tests/
│   ├── test_gain_precision.yaml    增益精度测试脚本
│   └── test_delay_accuracy.yaml    延迟精度测试脚本
└── appsettings.json                配置(端口、串口、AudioEngine、Debug 等)

8. 配置参数(appsettings.json

{
  "Backend": {
    "ListenUrl": "http://0.0.0.0:5000",
    "DataDirectory": "./data",
    "WsBufferSize": 65536,
    "CorsAllowAll": true
  },
  "Dsp": {
    "DefaultProtocol": "none",
    "DefaultTcpHost": "192.168.1.100",
    "DefaultTcpPort": 9000,
    "DefaultComPort": "COM3",
    "DefaultBaudRate": 115200
  },
  "AudioEngine": {
    "DllPath": "./DynamicChain.dll",
    "DefaultInputDevice": "",
    "DefaultOutputDevice": "",
    "FrameSize": 240,
    "SampleRate": 48000
  },
  "Debug": {
    "DefaultMetricsIntervalMs": 500,
    "WavCaptureDir": "./data/captures",
    "MaxLogBuffer": 2000
  }
}

9. REST API

方法 路径 说明 响应
GET /api/status 查询服务端状态(含仿真状态、当前工作模式) { status, clients, version, dsp, simRunning, mode, paramCount }
GET /api/link 获取当前链路 JSON 链路 JSON 内容(文件不存在返回 404)
GET /api/debug/wav/{captureId} 下载指定 captureId 的 WAV 抓取文件 WAV 文件二进制流(Content-Type: audio/wav)
GET /api/debug/metrics 获取当前调试指标快照 DebugMetrics JSON(PC 仿真未运行时返回 503)
GET /api/audio/devices 枚举系统 WASAPI 音频设备列表 { inputs:[...], outputs:[...] }

10. 扩展点

需求 实现方式
接收 DSP 响应 DspConnection.StartReceiveLoopAsync() 解析 ACK 帧,推送给前端
参数值范围验证 ParamService.SetParam() 中查询 ModuleRegistry 的 Schema
自动 dB→linear 转换 ConversionService.DbToLinear()BuildSetParamFrame() 前调用
链路变更通知 write_link 后广播 link_updated 事件给所有客户端
多 DSP 支持 DspConnectionService 改为 Dictionary<string, DspConnection>
参数变更日志 HandleSetParam() 追加 CSV/SQLite 写入
PortAudio 备选方案 AudioEngineService 中 NAudio 替换或包装为 PortAudio 19.x,实现 Linux/macOS 支持
测试报告扩展 TestAutomationService.ExportJUnitXml() 改为支持 HTML 或 Allure 报告格式
子图嵌套深度限制 ChainFlattener 中增加递归深度计数器,超限时返回错误
展平结果缓存 ChainFlattener 对未变更链路缓存 FlattenedLinkConfig,避免重复展平

11. 已知限制

限制 说明
无 DSP 接收解析 当前仅发送,不解析 DSP 硬件回包
无参数范围验证 前端传来的 value 未做范围检查
串口仅 Windows SerialPort 在 Linux 需要 /dev/ttyUSBx
无 TLS WebSocket 为 ws://,生产环境需 wss://
内存存储 重启后 paramStore 清空(可通过 read_link + get_all_params 恢复)
链路下发无 ACK BuildLinkFrameV3 fire-and-forget,无法确认 DSP 是否成功加载
WASAPI 仅 Windows NAudio WASAPI 模式不支持 Linux/macOS;跨平台需改用 PortAudio 19.x
DLL 平台限制 DynamicChain.dll 为 Windows x64 二进制,跨平台需对应平台构建版本
GC 延迟风险 OnAudioData 回调中使用 GCLatencyMode.SustainedLowLatency 降低但无法消除 GC 停顿
子图循环引用 ChainFlattener 当前不检测链路图中的循环子图引用,可能导致无限递归

新增:混合 ID 策略、音效模式管理与子图同步

Binary set_param 帧格式更新(v3)

旧版(字符串路径):[CMD=0x01][LEN][paramId_string\0][value:f32] ≈ 30 字节

新版(数字路径):[CMD=0x01][LEN=9][moduleIdx:u8][paramTypeId:u16LE][channelIdx:u16LE][valueType:u8=0x01][value:f32LE] = 14 字节(节省 53%)

HandleSetParam 路由逻辑:

// WebSocket JSON 仍使用字符串 paramId
var msg = JsonSerializer.Deserialize<SetParamMsg>(rawMsg);
// dimIndices → channelIdx 编码
ushort channelIdx = msg.DimIndices switch {
    null or []       => (ushort)(msg.Channel ?? 0),
    [var d0]         => (ushort)d0,
    [var d0, var d1] => (ushort)((d0 << 8) | d1),
    _ => throw new InvalidOperationException()
};
// 字符串 → 数字 ID 映射
var (moduleIdx, paramTypeId) = _paramIdMapper.Resolve(msg.InstanceId, msg.ParamId);
// 构建数字二进制帧
var frame = _frameBuilder.BuildSetParamFrameV3(moduleIdx, paramTypeId, channelIdx, float.Parse(msg.Value));
await _dspConnection.SendAsync(frame);

ParamIdMapper 服务(Services/ParamIdMapper.cs

public class ParamIdMapper
{
    private Dictionary<string, byte> _moduleIndices = new();   // instanceId → idx
    private Dictionary<(string typeId, string paramId), ushort> _paramTypeIds = new();

    // 在 write_link 展平后调用重建
    public void RebuildFromFlatConfig(FlattenedLinkConfig flat, IModuleRegistry registry)
    {
        _moduleIndices.Clear();
        for (byte i = 0; i < flat.Modules.Count; i++)
            _moduleIndices[flat.Modules[i].InstanceId] = i;
        // 从 ModuleRegistry 中读取每个 typeId 的 paramTypeId 映射
        foreach (var mod in flat.Modules)
        {
            var schemas = registry.GetParamSchemas(mod.TypeId);
            foreach (var s in schemas)
                _paramTypeIds[(mod.TypeId, s.ParamId)] = s.ParamTypeId;
        }
    }

    public (byte moduleIdx, ushort paramTypeId) Resolve(string instanceId, string paramId)
    {
        var typeId = _flatModules[instanceId].TypeId;
        return (_moduleIndices[instanceId], _paramTypeIds[(typeId, paramId)]);
    }
}

ChainSyncService(Services/ChainSyncService.cs

子图回读问题解决方案:

// get_dsp_chain → 直接返回 Backend 存储的 LinkConfig(含子图,不询问 DSP)
case "get_dsp_chain":
    var link = await _chainService.LoadLinkAsync();
    await SendAsync(ws, new { type = "dsp_chain", chain = link });
    break;

// read_dsp_chain → 向 DSP 发 CMD=0x06,获取平坦模块列表(仅用于校验)
case "read_dsp_chain":
    await _dspConnection.SendAsync(new byte[]{ 0x06,0,0,0,0 });
    // DSP 回应 CMD=0x86,由 DspResponseHandler 触发 ChainSyncService.ValidateAsync()
    break;
public class ChainSyncService
{
    public async Task ValidateAsync(List<DspModuleReport> dspModules)
    {
        var stored = await _chainSvc.LoadLinkAsync();
        var flat = _flattener.Flatten(stored);
        var missing = flat.Modules.Where(m => !dspModules.Any(d => d.InstanceId == m.InstanceId)).ToList();
        var extra   = dspModules.Where(d => !flat.Modules.Any(m => m.InstanceId == d.InstanceId)).ToList();
        if (missing.Any() || extra.Any())
            await _ws.BroadcastAsync(new { type="chain_sync_warning", missing, extra });
    }
}

PresetService(Services/PresetService.cs

// 存储路径: data/presets/{instanceId}/{presetId}.json
public class PresetService
{
    public Task<List<ModulePreset>> ListPresetsAsync(string instanceId);
    public Task SavePresetAsync(ModulePreset preset);
    public Task<ModulePreset?> LoadPresetAsync(string instanceId, string presetId);
    public Task DeletePresetAsync(string instanceId, string presetId);
}
// WS handlers: save_preset, load_preset, delete_preset, list_presets
// Responses:   preset_save_ack, preset_data, preset_delete_ack, preset_list

ProfileService(Services/ProfileService.cs

// 存储路径: data/profiles/{profileId}.json
public class ProfileService
{
    public Task<List<AmbianceProfile>> ListProfilesAsync();
    public Task SaveProfileAsync(AmbianceProfile profile);
    public Task<AmbianceProfile?> LoadProfileAsync(string profileId);
    public Task DeleteProfileAsync(string profileId);
}
// WS handlers: save_profile, load_profile, delete_profile, list_profiles
// set_ambiance → 发 CMD=0x05 到 DSP

ParamBinGenerator(Services/ParamBinGenerator.cs

// 新增 DSP 命令:CMD=0x04(下发 ParamBin),CMD=0x05(切换 ambiance)
public class ParamBinGenerator
{
    /*
     * ParamBin 格式:
     * Header(8): "PCFG" + version:u8 + numAmbiances:u8 + numModules:u8 + reserved:u8
     * Module Table(numModules×24): instanceId:char[16] + typeNumId:u32 + numPresets:u8 + pad:u8[3]
     * Ambiance Table(numAmbiances×(16+numModules)): name:char[16] + presetIndices:u8[numModules]
     * Param Data: per module per preset: paramCount:u16 + (typeId:u16+chIdx:u16+val:f32)[N]
     */
    public async Task<byte[]> GenerateAsync(ParamBinConfig config) { ... }
    public async Task SendToDspAsync(byte[] binData) { ... }  // CMD=0x04
}

// WS handler:
case "generate_param_bin":
    var cfg = JsonSerializer.Deserialize<ParamBinConfig>(msg);
    var bin = await _paramBinGenerator.GenerateAsync(cfg);
    await _paramBinGenerator.SendToDspAsync(bin);
    await SendAsync(ws, new { type="param_bin_ack", byteSize=bin.Length, numAmbiances=cfg.TargetProfiles.Count });
    break;

case "set_ambiance":
    await _dspConnection.SendAsync(new byte[]{ 0x05,1,0,0,0,(byte)msg.AmbianceIndex });
    await SendAsync(ws, new { type="set_ambiance_ack", ambianceIndex=msg.AmbianceIndex });
    break;

新增数据模型(Models/Preset.cs

public record ModulePreset(
    string InstanceId, string PresetId, string Name,
    Dictionary<string, string> Params  // key: "paramId[d0][d1]", value: 字符串
);
public record AmbianceProfile(
    string ProfileId, string Name,
    Dictionary<string, string> ModulePresets  // instanceId → presetId
);
public record ParamBinConfig(
    List<AmbianceProfile> TargetProfiles,
    Dictionary<string, int> ModulePresetLimits
);
public record DspModuleReport(string InstanceId, string TypeId);

新增文件结构

Backend/
├── Services/
│   ├── ChainSyncService.cs      子图同步校验(比对存储 vs DSP 回读)
│   ├── PresetService.cs         模块级 preset 持久化
│   ├── ProfileService.cs        全局音效模式持久化
│   ├── ParamBinGenerator.cs     ParamBin 二进制生成与下发
│   └── ParamIdMapper.cs         字符串↔数字 ID 映射表
├── Models/
│   └── Preset.cs                ModulePreset / AmbianceProfile / ParamBinConfig
└── data/
    ├── current_link.json        完整 LinkConfig(含子图,权威来源)
    ├── presets/                 {instanceId}/{presetId}.json
    └── profiles/                {profileId}.json

新增:IDspTransport 传输接口 与 AudioEngine 服务 (v5.0)

设计背景

DspConnection 类将 TCP 和 Serial 混在一起,耦合度较高。v5.0 重构为接口+实现分离:前端 Vue3 负责 AudioEngine 操作 UI(声卡选择、资源监控),后端通过 WebSocket 暴露 audio_engine_* 消息。


IDspTransport 接口

// Services/Transport/IDspTransport.cs
namespace TuningTool.Backend.Services.Transport
{
    public interface IDspTransport : IAsyncDisposable
    {
        bool IsConnected { get; }
        string Target { get; }            // "192.168.1.100:9000" or "COM3@115200"
        string Protocol { get; }          // "tcp" | "serial" | "none"

        Task<bool> ConnectAsync();
        Task DisconnectAsync();

        /// <summary>发送二进制帧(fire-and-forget,内部队列)</summary>
        Task DeliverAsync(byte[] frame);

        /// <summary>注册接收回调(DSP主动上报时触发)</summary>
        void RegisterReceiver(Action<byte[]> onReceive);
    }
}

TcpTransport

// Services/Transport/TcpTransport.cs
public class TcpTransport : IDspTransport
{
    private TcpClient? _client;
    private NetworkStream? _stream;
    private readonly string _host;
    private readonly int _port;
    private Action<byte[]>? _receiver;
    private CancellationTokenSource? _cts;

    public bool IsConnected => _client?.Connected == true && _stream != null;
    public string Target    => $"{_host}:{_port}";
    public string Protocol  => "tcp";

    public TcpTransport(string host, int port) { _host = host; _port = port; }

    public async Task<bool> ConnectAsync()
    {
        _client = new TcpClient();
        await _client.ConnectAsync(_host, _port);
        _stream = _client.GetStream();
        _cts = new CancellationTokenSource();
        _ = ReceiveLoopAsync(_cts.Token);
        return true;
    }

    public async Task DeliverAsync(byte[] frame)
    {
        if (_stream != null)
            await _stream.WriteAsync(frame);
    }

    public void RegisterReceiver(Action<byte[]> onReceive) => _receiver = onReceive;

    private async Task ReceiveLoopAsync(CancellationToken ct)
    {
        var buf = new byte[4096];
        while (!ct.IsCancellationRequested && _stream != null)
        {
            int n = await _stream.ReadAsync(buf, ct);
            if (n > 0) _receiver?.Invoke(buf[..n]);
        }
    }

    public async Task DisconnectAsync()
    {
        _cts?.Cancel();
        _stream?.Close(); _client?.Close();
        _stream = null; _client = null;
    }

    public async ValueTask DisposeAsync() => await DisconnectAsync();
}

SerialTransport

// Services/Transport/SerialTransport.cs
public class SerialTransport : IDspTransport
{
    private SerialPort? _port;
    private readonly string _comPort;
    private readonly int _baudRate;
    private Action<byte[]>? _receiver;

    public bool IsConnected => _port?.IsOpen == true;
    public string Target    => $"{_comPort}@{_baudRate}";
    public string Protocol  => "serial";

    public SerialTransport(string comPort, int baudRate = 115200)
    { _comPort = comPort; _baudRate = baudRate; }

    public Task<bool> ConnectAsync()
    {
        _port = new SerialPort(_comPort, _baudRate, Parity.None, 8, StopBits.One);
        _port.DataReceived += (_, __) =>
        {
            int n = _port.BytesToRead;
            var buf = new byte[n];
            _port.Read(buf, 0, n);
            _receiver?.Invoke(buf);
        };
        _port.Open();
        return Task.FromResult(true);
    }

    public Task DeliverAsync(byte[] frame)
    {
        _port?.Write(frame, 0, frame.Length);
        return Task.CompletedTask;
    }

    public void RegisterReceiver(Action<byte[]> onReceive) => _receiver = onReceive;

    public Task DisconnectAsync() { _port?.Close(); return Task.CompletedTask; }
    public ValueTask DisposeAsync() { _port?.Dispose(); return ValueTask.CompletedTask; }
}

AudioEngine 服务(PC 仿真声卡管理)

设计决策:UI 放在前端

PC 仿真音频引擎的操作 UI(声卡选择、挂载模块列表、系统资源)集成在 Vue3 前端,通过 WebSocket 消息与后端通信。优点: - 统一 UI 风格(Vue3 组件) - 无需额外 WinForms/WPF 窗口 - 可嵌入主界面侧边栏或浮动面板

后端 AudioEngineService 负责: 1. 枚举 WASAPI 声卡设备(依赖 NAudio) 2. 初始化/切换输出设备 3. 定期推送 CPU/内存占用 4. 管理挂载的算法模块链路

// Services/AudioEngineService.cs
// NuGet: NAudio (2.2.1)
using NAudio.CoreAudioApi;

public class AudioEngineService
{
    private MMDevice? _currentDevice;

    /// <summary>枚举所有 WASAPI 输出设备</summary>
    public List<AudioDeviceInfo> ListOutputDevices()
    {
        var enumerator = new MMDeviceEnumerator();
        return enumerator
            .EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active)
            .Select((d, i) => new AudioDeviceInfo
            {
                Index      = i,
                Id         = d.ID,
                Name       = d.FriendlyName,
                SampleRate = d.AudioClient.MixFormat.SampleRate,
                Channels   = d.AudioClient.MixFormat.Channels
            }).ToList();
    }

    public bool SetDevice(string deviceId) { /* 初始化 WASAPI 渲染 */ return true; }

    /// <summary>实时资源快照(由定时推送协程调用)</summary>
    public AudioEngineStatus GetStatus()
    {
        var proc = System.Diagnostics.Process.GetCurrentProcess();
        return new AudioEngineStatus
        {
            CpuPercent    = 0,   // 可接入 PerformanceCounter
            MemoryMB      = (int)(proc.WorkingSet64 / 1024 / 1024),
            IsRunning     = _currentDevice != null,
            DeviceName    = _currentDevice?.FriendlyName ?? "未选择",
            SampleRate    = _currentDevice?.AudioClient?.MixFormat?.SampleRate ?? 0,
            LoadedModules = new List<string>() // 来自 DynChain
        };
    }
}

public record AudioDeviceInfo
{
    public int    Index      { get; init; }
    public string Id         { get; init; } = "";
    public string Name       { get; init; } = "";
    public int    SampleRate { get; init; }
    public int    Channels   { get; init; }
}

public record AudioEngineStatus
{
    public int          CpuPercent    { get; init; }
    public int          MemoryMB      { get; init; }
    public bool         IsRunning     { get; init; }
    public string       DeviceName    { get; init; } = "";
    public int          SampleRate    { get; init; }
    public List<string> LoadedModules { get; init; } = new();
}

新增 WebSocket 消息(AudioEngine)

前端→后端 (C→S)

type 说明 关键字段
list_audio_devices 枚举声卡设备
set_audio_device 选择声卡 deviceId: string
start_audio_engine 启动音频引擎 sampleRate?: number
stop_audio_engine 停止音频引擎
get_audio_engine_status 获取当前状态

后端→前端 (S→C)

type 说明 关键字段
audio_device_list 设备列表响应 devices: AudioDeviceInfo[]
audio_device_ack 选择响应 success: bool, deviceName: string
audio_engine_status 状态(含资源) AudioEngineStatus 对象
audio_engine_started 引擎已启动 deviceName: string
audio_engine_stopped 引擎已停止

后端每 2 秒通过定时器推送一次 audio_engine_status 到所有已连接客户端。


新增文件结构(v5.0 增量)

Backend/
├── Services/
│   ├── Transport/
│   │   ├── IDspTransport.cs
│   │   ├── TcpTransport.cs
│   │   └── SerialTransport.cs
│   └── AudioEngineService.cs
└── Models/
    └── AudioEngineModels.cs   (AudioDeviceInfo, AudioEngineStatus)


新增:ToStoreString 布尔修正、apply_params、get_module_params、Preset 架构更新 (v5.1)

ToStoreString 辅助函数(布尔值大小写修正)

问题背景:C# 的 JsonElement.ToString() 对 JSON true/false 返回大写首字母的 "True"/"False"。前端 JavaScript 使用严格相等 === 'true' 进行布尔判断,导致从 paramStore 读回的布尔参数无法匹配。

修复方案:新增 ToStoreString() 辅助函数,统一将 JSON 布尔值序列化为小写字符串:

// Program.cs, ~line 574
static string ToStoreString(JsonElement el) => el.ValueKind switch {
    JsonValueKind.True  => "true",
    JsonValueKind.False => "false",
    _                   => el.ToString()
};

应用范围:所有将 JsonElement 值写入 paramStore 的路径均已改为调用 ToStoreString(),具体包括:

Handler 说明
HandleSetParam 单参数写入
HandleSavePreset Preset 参数快照
HandleLoadPreset 从文件加载 preset 后写回 paramStore
HandleApplyParams 批量参数写入(新增)
HandleSetAmbiance 音效模式切换时参数写入

paramStore 值规范(更新后):

布尔参数:统一存为小写 "true" / "false"(而非 C# 原生的 "True" / "False")
数值参数:数字的字符串表示,如 "-3.5"、"96"
其他参数:原始字符串值

HandleApplyParams(批量参数应用)

消息类型apply_params

设计背景:前端主导的 preset 加载架构。前端将 preset 参数应用到自身 Pinia store 后,通过 apply_params 批量推送给后端,后端写入 paramStore 并向 DSP 下发。此设计使前端成为参数状态的权威来源,避免了后端 preset 文件中参数与前端状态不同步的问题。

接收消息格式

{
  "type": "apply_params",
  "instanceId": "gain#1",
  "params": {
    "gainDb#0": "-3.5",
    "gainDb#1": "-3.5",
    "mute#0": "false",
    "enable": "true"
  }
}

处理逻辑

async Task HandleApplyParams(WebSocket ws, JsonElement msg) {
    string instanceId = msg.GetProperty("instanceId").GetString()!;
    var paramsEl = msg.GetProperty("params");

    foreach (var kv in paramsEl.EnumerateObject()) {
        string storeKey = $"{instanceId}.{kv.Name}";
        // ToStoreString 确保布尔值以小写存储
        string storeVal = ToStoreString(kv.Value);
        _paramStore[storeKey] = storeVal;
        // 异步向 DSP 下发每个参数(fire & forget)
        _ = TryFlushToDspAsync(instanceId, kv.Name, storeVal);
    }

    await Send(ws, new {
        type       = "apply_params_ack",
        instanceId = instanceId,
        success    = true
    });
}

响应消息

{
  "type": "apply_params_ack",
  "instanceId": "gain#1",
  "success": true
}

set_param 的区别

特性 set_param apply_params
参数数量 单个 批量(任意多个)
广播 param_update 否(批量覆盖,不广播)
主要用途 UI 实时调参 前端主导 preset 加载后推送
响应类型 set_param_ack apply_params_ack

HandleGetModuleParams(后端参数回读)

消息类型get_module_params

设计背景:前端"Read from backend"按钮的后端实现,用于读回校验——确认后端 paramStore 中确实收到了前端推送的参数,供调试和验证使用。

接收消息格式

{
  "type": "get_module_params",
  "instanceId": "gain#1"
}

处理逻辑

async Task HandleGetModuleParams(WebSocket ws, JsonElement msg) {
    string instanceId = msg.GetProperty("instanceId").GetString()!;
    string prefix = instanceId + ".";

    // 从 paramStore 中过滤出该实例的所有参数,去掉前缀
    var result = _paramStore
        .Where(kv => kv.Key.StartsWith(prefix))
        .ToDictionary(
            kv => kv.Key[prefix.Length..],  // 去掉 "instanceId." 前缀
            kv => kv.Value
        );

    await Send(ws, new {
        type       = "module_params",
        instanceId = instanceId,
        @params    = result
    });
}

响应消息

{
  "type": "module_params",
  "instanceId": "gain#1",
  "params": {
    "gainDb#0": "-3.5",
    "gainDb#1": "-3.5",
    "mute#0": "false",
    "mute#1": "false",
    "enable": "true"
  }
}

get_all_params / get_all_params_ack 的关系

get_module_params / module_params 是专为"前端回读校验"场景设计的简化接口,语义明确(只读 paramStore,不涉及 DSP 通信),与通用的 get_all_params 并行存在。


HandleSavePreset 更新(前端参数权威架构)

更新说明:原版 HandleSavePreset 仅从 paramStore 快照参数(后端权威模式)。新版同时支持前端在消息体中直接传入 params,即前端权威模式。

更新后的消息格式

{
  "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"
  }
}

params 字段为可选。若前端提供 params,后端: 1. 将 params 中每条记录写入 paramStore(通过 ToStoreString() 保证布尔小写) 2. 为每条记录调用 TryFlushToDspAsync(异步下发到 DSP) 3. 将 params 保存到 preset 文件中

若前端未提供 params(向后兼容),后端从 paramStore 中为该 instanceId 快照参数并保存。

Preset 文件格式(含 params 字段):

{
  "instanceId": "gain#1",
  "presetId":   "bass_boost",
  "name":       "Bass Boost",
  "params": {
    "gainDb#0": "3.0",
    "gainDb#1": "3.0",
    "mute#0":   "false",
    "enable":   "true"
  }
}


HandleListPresets 更新(包含 params 字段)

更新说明:原版 list_presets 响应只返回 preset 的元数据(presetIdname)。新版从文件中读取完整 preset 内容,在列表条目中包含 params 字段,使前端在启动时可以一次性将所有 preset 的参数加载到 presetDataStore,无需在每次切换 preset 时单独发送 load_preset 请求。

响应消息更新

旧版(仅元数据):

{
  "type": "preset_list",
  "instanceId": "gain#1",
  "presets": [
    { "presetId": "flat",       "name": "Flat" },
    { "presetId": "bass_boost", "name": "Bass Boost" }
  ]
}

新版(含 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 后,将每个条目的 params 存入 presetDataStore[instanceId][presetId],后续 preset 加载通过 apply_params 直接推送,无需再次请求后端。


消息路由表更新(v5.1 增量)

## 5. 消息路由表 中新增以下条目:

type Handler 说明
apply_params HandleApplyParams() 批量写入 paramStore 并下发 DSP;前端主导 preset 加载
get_module_params HandleGetModuleParams() 读取 paramStore 中某实例的全部参数;供前端回读校验

ParamStore 注意事项更新

布尔值存储规范(v5.1 起强制):

paramStore 中所有布尔类型参数值必须使用小写字符串 "true" / "false"。任何将 JsonElement 写入 paramStore 的路径均须经过 ToStoreString() 处理,而非直接调用 JsonElement.ToString()

错误示例:_paramStore[key] = el.ToString()         → 可能存入 "True" / "False"
正确示例:_paramStore[key] = ToStoreString(el)     → 始终存入 "true" / "false"

影响的前端逻辑:前端在比较 paramStore 中读回的布尔值时,使用 === 'true' 能够正确匹配,无需额外做大小写转换。


新增(v6.0):C DLL 集成架构、多核 DSP 仿真、ChangeThread、CodeGen


12. C DLL 与 C# AudioEngine 集成架构

12.1 设计背景

DynamicChain.dll(或 Linux 下的 libDynamicChain.so)是由 C 语言实现的 DSP 算法框架。C# 后端通过 P/Invoke 调用该 DLL,实现 PC 侧实时音频仿真。集成的核心挑战有三点:

  1. 内存安全:C 侧管理自身堆内存,C# GC 不能移动传给 C 的缓冲区
  2. 音频数据布局:NAudio WASAPI 返回交错(interleaved)PCM,DLL 期望平面(planar)float**
  3. DLL 双态构建:同一套 C 源码分别构建 嵌入式静态库(.a/.lib)和 PC 动态库(.dll/.so),C# 只调用后者

12.2 DLL 双态构建策略

dspalgo/
├── build/
│   ├── CMakeLists_pc.cmake       → 目标: DynamicChain.dll (x64, cdecl, dllexport)
│   └── CMakeLists_dsp.cmake      → 目标: libDynamicChain.a (裸机, Cortex-A/SHARC)

PC 构建关键 CMake 配置:

# CMakeLists_pc.cmake
add_library(DynamicChain SHARED
    framework/dynchain_core.c
    framework/dynchain_registry.c
    framework/dynchain_parser.c
    framework/dynchain_topo.c
    platform/platform_dynamic.c
    modules/gain/gain_module.c
    modules/delay/delay_module.c
    # ... 其他模块
)
target_compile_definitions(DynamicChain PRIVATE
    DYNCHAIN_EXPORT          # 触发 __declspec(dllexport) / __attribute__((visibility("default")))
    PLATFORM_PC              # 启用 malloc/free 路径
    FRAME_SIZE=240
    MAX_SAMPLE_RATE=48000
)
set_target_properties(DynamicChain PROPERTIES
    C_STANDARD 11
    POSITION_INDEPENDENT_CODE ON
)

导出宏(dynchain_interface.h):

#ifdef DYNCHAIN_EXPORT
  #ifdef _WIN32
    #define DYNCHAIN_API __declspec(dllexport) __cdecl
  #else
    #define DYNCHAIN_API __attribute__((visibility("default")))
  #endif
#else
  #define DYNCHAIN_API
#endif

12.3 P/Invoke 层设计(对齐 DSPAlgo v7)

// Interop/DynChainInterop.cs
// 对齐 DSPAlgo Architecture v7:
//   - SetParam/GetParam 接口去掉 channelIdx,仅保留 (instanceId, paramId, pData, dataSize)
//   - 模块接口统一为 ModuleInstance* 第一参数

internal static class DynChainInterop
{
    private const string DllName = "DynamicChain";

    // ── 生命周期 ─────────────────────────────────────────────────
    /// <summary>分配并初始化 DynamicChain 实例(平台层负责内存)</summary>
    [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
    public static extern int DynChain_GetMemSize(out uint pSize);

    [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
    public static extern int DynChain_Init(IntPtr pMem, uint memSize, out IntPtr ppChain);

    [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
    public static extern int DynChain_Destroy(IntPtr pChain);

    // ── 链路配置 ─────────────────────────────────────────────────
    /// <summary>从二进制帧加载链路配置(对应 write_link 下发的帧)</summary>
    [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
    public static extern int DynChain_LoadLinkFrame(IntPtr pChain,
        [In] byte[] frame, uint frameLen);

    /// <summary>热重建链路(不重置参数,用于 ChangeThread 后重新排布)</summary>
    [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
    public static extern int DynChain_Rebuild(IntPtr pChain);

    // ── 参数 ─────────────────────────────────────────────────────
    /// <summary>
    /// v7 简化接口:paramId 内嵌通道信息(如 "gainDb#0"),无独立 channelIdx。
    /// pData 指向 float/int32/uint8,dataSize 为字节数。
    /// </summary>
    [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
    public static extern int DynChain_SetParam(IntPtr pChain,
        [MarshalAs(UnmanagedType.LPUTF8Str)] string instanceId,
        [MarshalAs(UnmanagedType.LPUTF8Str)] string paramId,
        IntPtr pData, uint dataSize);

    [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
    public static extern int DynChain_GetParam(IntPtr pChain,
        [MarshalAs(UnmanagedType.LPUTF8Str)] string instanceId,
        [MarshalAs(UnmanagedType.LPUTF8Str)] string paramId,
        IntPtr pData, uint dataSize);

    // ── 音频处理 ─────────────────────────────────────────────────
    /// <summary>
    /// ppIn/ppOut:float*[] 平面(planar)通道指针数组,需 GCHandle 固定。
    /// nbSamples:本帧采样点数(= FrameSize,默认 240)。
    /// </summary>
    [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
    public static extern unsafe int DynChain_Process(IntPtr pChain,
        float** ppIn, int inChannels,
        float** ppOut, int outChannels,
        uint nbSamples);

    // ── 调试 ─────────────────────────────────────────────────────
    [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
    public static extern int DynChain_GetMetrics(IntPtr pChain,
        IntPtr pJsonBuf, uint bufSize);

    [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
    public static extern int DynChain_SetLogCallback(IntPtr pChain,
        LogCallbackDelegate cb);

    [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
    public static extern int DynChain_SetCaptureHook(IntPtr pChain,
        [MarshalAs(UnmanagedType.LPUTF8Str)] string instanceId,
        [MarshalAs(UnmanagedType.LPUTF8Str)] string capturePoint,
        int durationMs,
        [MarshalAs(UnmanagedType.LPUTF8Str)] string captureId);

    public delegate void LogCallbackDelegate(int level,
        [MarshalAs(UnmanagedType.LPUTF8Str)] string message);
}

12.4 内存管理策略

资源 分配方 C# 侧管理方式
DynamicChain 实例状态 DLL 内部(platform_dynamic.c malloc) DynChain_Destroy() 时释放
模块 pState 缓冲区 DLL 内部 同上
音频帧 float[] 缓冲区 C# 侧 GCHandle.Alloc(buf, GCHandleType.Pinned) 固定
参数传递临时缓冲 C# 栈 stackalloc + unsafe fixed
Wire 数据缓冲区 DLL 内部(Init 时一次性分配) 不可在 Process 期间重新分配

音频缓冲区固定示例

// Services/AudioEngineCore.cs
private GCHandle[]  _inHandles;
private GCHandle[]  _outHandles;
private float[][]   _inBufs;   // [channels][frameSize]
private float[][]   _outBufs;

private unsafe IntPtr AllocPlanarPtrs(float[][] bufs, GCHandle[] handles, int channels, int frameSize)
{
    // 分配一个 float*[] 指针数组(非 GC 托管内存)
    var ptrArray = (float**)Marshal.AllocHGlobal(channels * sizeof(float*));
    for (int ch = 0; ch < channels; ch++)
    {
        handles[ch] = GCHandle.Alloc(bufs[ch], GCHandleType.Pinned);
        ptrArray[ch] = (float*)handles[ch].AddrOfPinnedObject();
    }
    return (IntPtr)ptrArray;
}

private void FreePlanarPtrs(IntPtr ptrArray, GCHandle[] handles)
{
    foreach (var h in handles) if (h.IsAllocated) h.Free();
    Marshal.FreeHGlobal(ptrArray);
}

12.5 音频数据格式转换(Interleaved ↔ Planar)

NAudio WASAPI DataAvailable 回调提供交错(interleaved)格式,DLL 要求平面(planar)格式:

// 交错 → 平面(WASAPI Int16LE → float planar)
private void DeinterleaveToFloat(byte[] rawBytes, int byteCount,
                                  float[][] planar, int channels, int frameSize)
{
    int sampleCount = byteCount / (channels * 2);  // Int16 = 2 bytes
    for (int i = 0; i < sampleCount && i < frameSize; i++)
        for (int ch = 0; ch < channels; ch++)
        {
            short s = BitConverter.ToInt16(rawBytes, (i * channels + ch) * 2);
            planar[ch][i] = s / 32768f;
        }
}

// 平面 → 交错(float planar → WASAPI Int16LE)
private void InterleaveFromFloat(float[][] planar, byte[] rawBytes,
                                  int channels, int frameSize)
{
    for (int i = 0; i < frameSize; i++)
        for (int ch = 0; ch < channels; ch++)
        {
            short s = (short)Math.Clamp(planar[ch][i] * 32767f, -32768f, 32767f);
            BitConverter.TryWriteBytes(rawBytes.AsSpan((i * channels + ch) * 2), s);
        }
}

12.6 DLL 加载与多 DLL 支持

自定义模块 DLL(CodeGen 生成后编译的模块库)可通过 NativeLibrary.Load() 动态注册到 DynamicChain:

// Services/DllModuleLoader.cs
public class DllModuleLoader
{
    // 已加载的额外模块 DLL 句柄
    private readonly List<IntPtr> _loadedLibs = new();

    /// <summary>动态加载自定义模块 DLL,向 DynamicChain 注册其模块表</summary>
    public bool LoadModuleDll(IntPtr pChain, string dllPath)
    {
        // 1. 加载 DLL
        IntPtr libHandle = NativeLibrary.Load(dllPath);
        if (libHandle == IntPtr.Zero) return false;

        // 2. 找到注册入口函数:每个模块 DLL 必须导出 "RegisterModules(pChain)"
        if (!NativeLibrary.TryGetExport(libHandle, "RegisterModules", out IntPtr regFn))
            return false;

        // 3. 调用注册函数
        var registerDelegate = Marshal.GetDelegateForFunctionPointer<RegisterModulesDelegate>(regFn);
        int ret = registerDelegate(pChain);

        if (ret == 0) _loadedLibs.Add(libHandle);
        return ret == 0;
    }

    public void UnloadAll()
    {
        foreach (var h in _loadedLibs) NativeLibrary.Free(h);
        _loadedLibs.Clear();
    }

    private delegate int RegisterModulesDelegate(IntPtr pChain);
}

13. 多核 DSP 仿真架构(类 AWE Server)

13.1 设计目标

真实 DSP 硬件通常为多核架构(如 ADI ADSP-21569 含 2 个 SHARC+ 核,ARM+DSP 异构多核等)。PC 侧仿真需要:

  • 用多线程模拟多个 DSP 核心,每个核心有独立算法链
  • 核心间通过环形缓冲区传递音频数据
  • 支持每个核心独立配置线程数(模拟核心内部流水线/SIMD 并行)
  • 核心间帧同步(barrier),保证延迟确定性
  • 实时资源监控(每核 CPU 占用、帧时间)

这与 AWE Server 的设计思路高度吻合:AWE Server 也将算法链分布在多个处理线程(subsystem)上,各 subsystem 独立运行并通过 RouteBlock 传递信号。

13.2 核心架构图

┌─────────────────────────────────────────────────────────────────────────┐
│                        AudioEngineService(主协调者)                     │
│                                                                         │
│   NAudio WASAPI ──►  AudioInputRouter                                   │
│                            │                                            │
│          ┌─────────────────┼──────────────────┐                         │
│          ▼                 ▼                  ▼                         │
│   ┌─────────────┐  ┌─────────────┐   ┌─────────────┐                   │
│   │  DspCore #0  │  │  DspCore #1  │  │  DspCore #2  │  ...             │
│   │ (Thread T0)  │  │ (Thread T1)  │  │ (Thread T2)  │                  │
│   │ gain#1       │  │ eq#1         │  │ mdrc#1       │                   │
│   │ delay#1      │  │ mixer#1      │  │ limiter#1    │                   │
│   │ DynChain.dll │  │ DynChain.dll │  │ DynChain.dll │                   │
│   └──────┬───────┘  └──────┬───────┘  └──────┬───────┘                  │
│          │  RingBuffer      │  RingBuffer      │                         │
│          └─────────────────┴──────────────────┘                         │
│                             │                                            │
│                    AudioOutputMixer                                      │
│                             │                                            │
│                    NAudio WASAPI ◄──                                     │
└─────────────────────────────────────────────────────────────────────────┘

13.3 DspCoreSimulator(单核仿真)

// Services/Simulation/DspCoreSimulator.cs
public class DspCoreSimulator : IDisposable
{
    public int CoreId { get; }
    public string Name { get; }          // "Core-0", "Core-1" ...
    public bool IsRunning { get; private set; }

    // 该核心上挂载的模块实例 ID 列表
    public IReadOnlyList<string> AssignedModules => _assignedModules;

    // 性能统计
    public float CpuLoadPercent { get; private set; }
    public float LastFrameTimeUs { get; private set; }
    public int DroppedFrames { get; private set; }

    // ── 内部字段 ───────────────────────────────────────
    private readonly List<string>   _assignedModules = new();
    private IntPtr                  _chain = IntPtr.Zero;
    private Thread?                 _thread;
    private CancellationTokenSource _cts = new();

    // 输入/输出帧缓冲环(lock-free,容量 4 帧)
    private readonly AudioRingBuffer _inputRing;
    private readonly AudioRingBuffer _outputRing;

    // 核心内部线程数(模拟 DSP 核心内多线程/SIMD 并行,如 Helios 双线程)
    private int _intraThreadCount = 1;

    // ── 帧同步屏障(由 DspCorePool 统一管理)─────────────
    internal Barrier? FrameBarrier;

    public DspCoreSimulator(int coreId, int channels, int frameSize, int sampleRate)
    {
        CoreId     = coreId;
        Name       = $"Core-{coreId}";
        _inputRing = new AudioRingBuffer(channels, frameSize, capacity: 4);
        _outputRing = new AudioRingBuffer(channels, frameSize, capacity: 4);
    }

    public void Start()
    {
        // 为该核心创建独立的 DynChain 实例
        DynChainInterop.DynChain_GetMemSize(out uint sz);
        IntPtr mem = Marshal.AllocHGlobal((int)sz);
        DynChainInterop.DynChain_Init(mem, sz, out _chain);

        IsRunning = true;
        _thread = new Thread(ProcessLoop)
        {
            Name     = $"DSP-{Name}",
            Priority = ThreadPriority.AboveNormal,  // 模拟 DSP 实时优先级
            IsBackground = true
        };
        _thread.Start();
    }

    public void Stop()
    {
        IsRunning = false;
        _cts.Cancel();
        _thread?.Join(500);
        if (_chain != IntPtr.Zero)
        {
            DynChainInterop.DynChain_Destroy(_chain);
            _chain = IntPtr.Zero;
        }
    }

    /// <summary>
    /// 核心内部线程数设置(模拟 DSP 核内多线程,如 SHARC+ Dual-Issue 或 ARM NEON)。
    /// intraCount=1:单线程顺序执行;intraCount=N:模块级并行(需 DLL 支持 SIMD hint)
    /// </summary>
    public void SetIntraThreadCount(int count)
    {
        _intraThreadCount = Math.Clamp(count, 1, 8);
        // 通知 DynChain 设置并行处理数(DLL 端通过 thread hint 决定是否展开循环)
        if (_chain != IntPtr.Zero)
        {
            unsafe
            {
                int hint = _intraThreadCount;
                DynChainInterop.DynChain_SetParam(_chain, "__sys__", "intraThreadHint",
                    (IntPtr)(&hint), sizeof(int));
            }
        }
    }

    /// <summary>将音频帧推入该核心的输入缓冲(由 AudioInputRouter 调用)</summary>
    public bool PushInputFrame(float[][] samples) => _inputRing.TryWrite(samples);

    /// <summary>从该核心的输出缓冲取出处理结果(由 AudioOutputMixer 调用)</summary>
    public bool PopOutputFrame(float[][] samples) => _outputRing.TryRead(samples);

    // 在独立线程中循环处理帧
    private unsafe void ProcessLoop()
    {
        var inBufs  = AllocPlanarBuffers(_inputRing.Channels, _inputRing.FrameSize);
        var outBufs = AllocPlanarBuffers(_inputRing.Channels, _inputRing.FrameSize);

        var inHandles  = new GCHandle[inBufs.Length];
        var outHandles = new GCHandle[outBufs.Length];

        // 固定缓冲区,避免 GC 移动
        for (int i = 0; i < inBufs.Length;  i++) inHandles[i]  = GCHandle.Alloc(inBufs[i], GCHandleType.Pinned);
        for (int i = 0; i < outBufs.Length; i++) outHandles[i] = GCHandle.Alloc(outBufs[i], GCHandleType.Pinned);

        var inPtrs  = (float**)Marshal.AllocHGlobal(inBufs.Length  * sizeof(float*));
        var outPtrs = (float**)Marshal.AllocHGlobal(outBufs.Length * sizeof(float*));
        for (int i = 0; i < inBufs.Length;  i++) inPtrs[i]  = (float*)inHandles[i].AddrOfPinnedObject();
        for (int i = 0; i < outBufs.Length; i++) outPtrs[i] = (float*)outHandles[i].AddrOfPinnedObject();

        var sw = System.Diagnostics.Stopwatch.StartNew();

        while (!_cts.Token.IsCancellationRequested)
        {
            // 等待输入帧
            if (!_inputRing.TryRead(inBufs, spinWaitUs: 200))
            {
                DroppedFrames++;
                continue;
            }

            sw.Restart();

            // 在核心内多线程 hint 下处理(DLL 内部决策是否并行)
            DynChainInterop.DynChain_Process(_chain,
                inPtrs,  inBufs.Length,
                outPtrs, outBufs.Length,
                (uint)_inputRing.FrameSize);

            LastFrameTimeUs = (float)(sw.Elapsed.TotalMilliseconds * 1000);

            // 写出输出帧
            _outputRing.TryWrite(outBufs);

            // 帧同步屏障:等待同一 tick 内所有核心完成本帧
            FrameBarrier?.SignalAndWait();
        }

        // 清理
        for (int i = 0; i < inBufs.Length;  i++) inHandles[i].Free();
        for (int i = 0; i < outBufs.Length; i++) outHandles[i].Free();
        Marshal.FreeHGlobal((IntPtr)inPtrs);
        Marshal.FreeHGlobal((IntPtr)outPtrs);
    }

    private static float[][] AllocPlanarBuffers(int channels, int frameSize)
    {
        var bufs = new float[channels][];
        for (int i = 0; i < channels; i++) bufs[i] = new float[frameSize];
        return bufs;
    }

    public void Dispose() => Stop();
}

13.4 DspCorePool(核心池协调者)

// Services/Simulation/DspCorePool.cs
public class DspCorePool : IDisposable
{
    private readonly List<DspCoreSimulator> _cores = new();
    private Barrier? _frameBarrier;
    private readonly int _channels;
    private readonly int _frameSize;
    private readonly int _sampleRate;

    // instanceId → coreId 映射(由 ChangeThread 机制维护)
    private readonly Dictionary<string, int> _moduleThreadMap = new();

    public IReadOnlyList<DspCoreSimulator> Cores => _cores;

    public DspCorePool(int channels, int frameSize, int sampleRate)
    {
        _channels = channels; _frameSize = frameSize; _sampleRate = sampleRate;
    }

    /// <summary>按 coreCount 初始化核心池,创建帧同步屏障</summary>
    public void Initialize(int coreCount)
    {
        foreach (var c in _cores) c.Dispose();
        _cores.Clear();

        for (int i = 0; i < coreCount; i++)
            _cores.Add(new DspCoreSimulator(i, _channels, _frameSize, _sampleRate));

        // 帧同步屏障:所有核心 + 1(I/O 路由线程)
        _frameBarrier = new Barrier(coreCount + 1);
        foreach (var c in _cores) c.FrameBarrier = _frameBarrier;
    }

    /// <summary>启动所有核心线程</summary>
    public void StartAll()
    {
        foreach (var c in _cores) c.Start();
    }

    /// <summary>停止所有核心线程</summary>
    public void StopAll()
    {
        foreach (var c in _cores) c.Stop();
    }

    /// <summary>
    /// 将模块分配到指定核心(ChangeThread 操作)。
    /// 调用后需重建受影响的核心链路(RebuildCore)。
    /// </summary>
    public void AssignModule(string instanceId, int coreId)
    {
        if (coreId < 0 || coreId >= _cores.Count)
            throw new ArgumentOutOfRangeException(nameof(coreId));
        _moduleThreadMap[instanceId] = coreId;
    }

    public int? GetModuleCoreId(string instanceId)
        => _moduleThreadMap.TryGetValue(instanceId, out int id) ? id : null;

    public Dictionary<string, int> GetAllAssignments()
        => new(_moduleThreadMap);

    /// <summary>重建指定核心的 DynChain 链路(分配变更后调用)</summary>
    public async Task RebuildCoreAsync(int coreId, FlattenedLinkConfig fullFlat,
                                       BinaryFrameBuilder builder)
    {
        var core = _cores[coreId];
        // 按 coreId 过滤模块
        var coreModules = fullFlat.Modules
            .Where(m => (_moduleThreadMap.TryGetValue(m.InstanceId, out int tid) ? tid : 0) == coreId)
            .ToList();
        var coreConnections = fullFlat.Connections
            .Where(c => coreModules.Any(m => m.InstanceId == c.FromModule)
                     || coreModules.Any(m => m.InstanceId == c.ToModule))
            .ToList();
        var coreFlat = new FlattenedLinkConfig(coreModules, coreConnections);

        byte[] frame = builder.BuildLinkFrameV3(coreFlat);
        // 暂停核心处理,下发新链路帧,重建
        DynChainInterop.DynChain_LoadLinkFrame(core.ChainHandle, frame, (uint)frame.Length);
        DynChainInterop.DynChain_Rebuild(core.ChainHandle);
    }

    /// <summary>向对应核心发送 SetParam(通过 moduleThreadMap 路由)</summary>
    public unsafe void SetParam(string instanceId, string paramId, float value)
    {
        int coreId = _moduleThreadMap.TryGetValue(instanceId, out int id) ? id : 0;
        if (coreId < _cores.Count)
            _cores[coreId].SetParam(instanceId, paramId, value);
    }

    public void Dispose()
    {
        StopAll();
        foreach (var c in _cores) c.Dispose();
        _cores.Clear();
    }
}

13.5 AudioRingBuffer(核间音频传输)

// Services/Simulation/AudioRingBuffer.cs
/// <summary>
/// 单生产者单消费者 lock-free 环形帧缓冲(float planar)。
/// 容量 capacity 帧,超出时生产者丢弃(模拟 DSP 硬件 FIFO 溢出行为)。
/// </summary>
public class AudioRingBuffer
{
    public int Channels  { get; }
    public int FrameSize { get; }

    private readonly float[][][] _frames;     // [capacity][channels][frameSize]
    private volatile int _writeIdx = 0;
    private volatile int _readIdx  = 0;
    private readonly int _capacity;

    public AudioRingBuffer(int channels, int frameSize, int capacity = 4)
    {
        Channels  = channels;
        FrameSize = frameSize;
        _capacity = capacity;
        _frames   = new float[capacity][][];
        for (int i = 0; i < capacity; i++)
        {
            _frames[i] = new float[channels][];
            for (int ch = 0; ch < channels; ch++)
                _frames[i][ch] = new float[frameSize];
        }
    }

    public bool TryWrite(float[][] src)
    {
        int next = (_writeIdx + 1) % _capacity;
        if (next == _readIdx) return false;  // 满,丢帧
        for (int ch = 0; ch < Channels; ch++)
            Array.Copy(src[ch], _frames[_writeIdx][ch], FrameSize);
        _writeIdx = next;
        return true;
    }

    public bool TryRead(float[][] dst, int spinWaitUs = 0)
    {
        if (_readIdx == _writeIdx)
        {
            if (spinWaitUs > 0) Thread.SpinWait(spinWaitUs * 10);
            return _readIdx != _writeIdx;
        }
        for (int ch = 0; ch < Channels; ch++)
            Array.Copy(_frames[_readIdx][ch], dst[ch], FrameSize);
        _readIdx = (_readIdx + 1) % _capacity;
        return true;
    }
}

13.6 多核仿真 WebSocket 消息

前端→后端(C→S)

type 说明 关键字段
sim_set_core_count 设置仿真核心数 count: number
sim_set_core_intra_threads 设置指定核心内部线程数 coreId: number, intraThreads: number
sim_get_core_status 获取所有核心状态

后端→前端(S→C)

type 说明 关键字段
sim_core_status 各核心状态快照 cores: CoreStatus[](含 cpuLoad, frameTimeUs, droppedFrames, assignedModules)
sim_core_count_ack 核心数变更确认 count: number, success: bool

后端每 500ms 定时推送一次 sim_core_status(与 debug_metrics 合并推送):

{
  "type": "sim_core_status",
  "timestamp": 1700000000000,
  "cores": [
    {
      "coreId": 0,
      "name": "Core-0",
      "cpuLoadPercent": 12.5,
      "lastFrameTimeUs": 1240,
      "droppedFrames": 0,
      "intraThreads": 2,
      "assignedModules": ["gain#1", "delay#1"]
    },
    {
      "coreId": 1,
      "name": "Core-1",
      "cpuLoadPercent": 28.3,
      "lastFrameTimeUs": 2710,
      "droppedFrames": 0,
      "intraThreads": 1,
      "assignedModules": ["eq#1", "mdrc#1", "limiter#1"]
    }
  ]
}

14. ChangeThread 线程分配系统

14.1 功能定位

ChangeThread 允许用户(通过前端 UI)将某个算法模块从当前所在的仿真核心(线程)迁移到另一个核心。这对应实际 DSP 开发中将模块从一个处理器核心移到另一个核心的工程实践。

是否在 DSP 硬件上支持:取决于 DSP 厂商 framework 是否提供运行时 core-to-core 迁移。部分厂商(如 ADI)需要重新烧写链路配置;部分 RTOS 框架(如 AWE)支持动态迁移。前端 UI 在连接实体 DSP 时应通过 dsp_capabilities 消息查询支持情况,不支持时禁用 ChangeThread 操作。

14.2 前端 UI 设计要点

前端在链路编辑画布中,每个模块节点上提供 "Thread" 下拉菜单(仅在 PC 仿真运行时可用):

  • 显示当前所属核心(如 Core-0
  • 列出所有可用核心(Core-0 ~ Core-N
  • 选择后发送 assign_module_thread 消息
  • 模块节点颜色/标签随核心分配变化(不同核心对应不同色调)

14.3 WebSocket 协议

前端→后端(C→S)

// 分配单个模块到指定核心
{
  "type": "assign_module_thread",
  "instanceId": "eq#1",
  "coreId": 1
}

// 批量分配(可选,用于链路初始化时一次性设定所有分配)
{
  "type": "assign_module_threads_batch",
  "assignments": {
    "gain#1":   0,
    "delay#1":  0,
    "eq#1":     1,
    "mdrc#1":   1,
    "limiter#1": 1
  }
}

// 查询当前分配表
{ "type": "get_thread_assignments" }

后端→前端(S→C)

// assign_module_thread 响应
{
  "type": "assign_module_thread_ack",
  "instanceId": "eq#1",
  "coreId": 1,
  "success": true,
  "rebuildRequired": true   // 是否需要重建链路(通常为 true)
}

// 分配表快照
{
  "type": "thread_assignments",
  "assignments": {
    "gain#1":    0,
    "delay#1":   0,
    "eq#1":      1,
    "mdrc#1":    1,
    "limiter#1": 1
  }
}

// 重建完成通知(RebuildCore 完成后广播)
{
  "type": "sim_rebuild_done",
  "affectedCores": [0, 1],
  "success": true
}

14.4 后端 ThreadAssignmentService

// Services/ThreadAssignmentService.cs
public class ThreadAssignmentService
{
    // 全局分配表(instanceId → coreId)
    private readonly ConcurrentDictionary<string, int> _assignments = new();
    private readonly DspCorePool _corePool;
    private readonly ChainFlattener _flattener;
    private readonly BinaryFrameBuilder _frameBuilder;
    private readonly ChainService _chainService;

    public ThreadAssignmentService(DspCorePool pool, ChainFlattener flattener,
                                   BinaryFrameBuilder builder, ChainService chainSvc)
    {
        _corePool     = pool;
        _flattener    = flattener;
        _frameBuilder = builder;
        _chainService = chainSvc;
    }

    /// <summary>
    /// 分配模块到核心,并异步重建受影响核心的链路。
    /// 返回受影响的 coreId 列表。
    /// </summary>
    public async Task<int[]> AssignModuleAsync(string instanceId, int coreId)
    {
        int oldCore = _assignments.TryGetValue(instanceId, out int old) ? old : 0;
        _assignments[instanceId] = coreId;
        _corePool.AssignModule(instanceId, coreId);

        // 读取当前完整链路,重建受影响的核心
        var linkJson = await _chainService.LoadLinkAsync() ?? "{}";
        var linkConfig = JsonSerializer.Deserialize<LinkConfig>(linkJson)!;
        var flat = _flattener.Flatten(linkConfig);

        var affectedCores = new HashSet<int> { oldCore, coreId };
        foreach (int aid in affectedCores)
            await _corePool.RebuildCoreAsync(aid, flat, _frameBuilder);

        return affectedCores.ToArray();
    }

    /// <summary>批量更新分配表并重建所有受影响核心</summary>
    public async Task<int[]> AssignBatchAsync(Dictionary<string, int> assignments)
    {
        var affected = new HashSet<int>();
        foreach (var (iid, cid) in assignments)
        {
            if (_assignments.TryGetValue(iid, out int old)) affected.Add(old);
            affected.Add(cid);
            _assignments[iid] = cid;
            _corePool.AssignModule(iid, cid);
        }
        var linkJson = await _chainService.LoadLinkAsync() ?? "{}";
        var linkConfig = JsonSerializer.Deserialize<LinkConfig>(linkJson)!;
        var flat = _flattener.Flatten(linkConfig);

        foreach (int aid in affected)
            await _corePool.RebuildCoreAsync(aid, flat, _frameBuilder);

        return affected.ToArray();
    }

    public Dictionary<string, int> GetAllAssignments() => new(_assignments);
}

write_link 触发后,ChainFlattener 对链路展平,DspCorePool 按分配表拆分模块列表:

write_link
ChainFlattener.Flatten(linkConfig)
    │ FlattenedLinkConfig(全局模块列表)
ThreadAssignmentService.AssignDefaultsIfMissing()
    │ 未分配的模块默认放到 Core-0
DspCorePool.RebuildAllCores(flat, frameBuilder)
    │ 按 _moduleThreadMap 分别为每个核心生成 sub-flat
各 DspCoreSimulator.LoadLinkFrame(subFrame)
各核心独立运行新链路

14.6 DSP 硬件侧 ChangeThread 支持查询

// 后端在连接 DSP 后查询其能力(发 CMD=0x07)
// DSP 回包中包含 supportsRuntimeCoreAssign 标志

// 后端广播给所有前端
{
  "type": "dsp_capabilities",
  "supportsRuntimeCoreAssign": false,
  "coreCount": 2,
  "maxModulesPerCore": 32
}

前端收到 supportsRuntimeCoreAssign: false 时,将模块的 Thread 下拉菜单置灰,并显示提示 "当前 DSP 固件不支持运行时核心迁移,请在 Chain Builder 模式下完成分配后重新下发链路。"


15. CodeGen 代码生成系统

15.1 功能定位

CodeGen 系统允许用户通过前端 UI 定义自定义 DSP 算法模块,后端自动生成符合 DSPAlgo Architecture v7 标准的 C 代码框架和 CMake 构建脚本,用户只需填入实际算法逻辑,即可编译出可挂载到 DynamicChain 的模块 DLL。

工作流程

前端 UI
  定义模块(名称、参数、端口)
WebSocket: codegen_module
后端 CodeGenService
  生成 .h / .c / CMakeLists.txt / manifest.json
REST: GET /api/codegen/download/{jobId}
  返回 .zip
用户解压,填写算法实现
cmake --build  →  CustomModule.dll
WebSocket: load_module_dll  (DllModuleLoader)
DynamicChain 注册新模块,前端模块库刷新

15.2 前端模块定义接口

// types/codegen.ts
interface CodegenModuleSpec {
  moduleName: string;         // 模块类名(PascalCase),如 "CustomLimiter"
  typeId: string;             // "0x16679001"(十六进制字符串,用户自定义或自动分配)
  description?: string;       // 模块说明
  params: CodegenParamSpec[];
  inputPorts: CodegenPortSpec[];
  outputPorts: CodegenPortSpec[];
}

interface CodegenParamSpec {
  name: string;               // 参数名,如 "threshold"
  paramTypeId: number;        // 参数类型 ID(uint16),在模块内唯一
  dataType: 'float' | 'int32' | 'uint8';
  defaultValue: number;
  minValue?: number;
  maxValue?: number;
  channelized: boolean;       // true:每通道独立参数(数组);false:全局参数
  description?: string;
}

interface CodegenPortSpec {
  name: string;               // 端口名,如 "in", "out"
  channels: number;           // -1=继承
  sampleRate: number;         // -1=继承
}

WebSocket 请求消息

{
  "type": "codegen_module",
  "spec": {
    "moduleName": "CustomLimiter",
    "typeId": "0x16679001",
    "description": "用户自定义限幅器",
    "params": [
      { "name": "threshold", "paramTypeId": 1, "dataType": "float",
        "defaultValue": -6.0, "minValue": -60.0, "maxValue": 0.0,
        "channelized": false, "description": "限幅阈值(dBFS)" },
      { "name": "releaseMs", "paramTypeId": 2, "dataType": "float",
        "defaultValue": 100.0, "minValue": 1.0, "maxValue": 2000.0,
        "channelized": false, "description": "释放时间(ms)" }
    ],
    "inputPorts":  [{ "name": "in",  "channels": -1, "sampleRate": -1 }],
    "outputPorts": [{ "name": "out", "channels": -1, "sampleRate": -1 }]
  }
}

15.3 CodeGenService

// Services/CodeGenService.cs
public class CodeGenService
{
    private readonly string _outputDir;   // "./data/codegen"

    public CodeGenService(IConfiguration config)
    {
        _outputDir = config["CodeGen:OutputDir"] ?? "./data/codegen";
        Directory.CreateDirectory(_outputDir);
    }

    /// <summary>根据模块规格生成所有代码文件,返回 jobId(目录名)</summary>
    public async Task<CodeGenResult> GenerateAsync(CodegenModuleSpec spec)
    {
        string jobId  = $"{spec.moduleName}_{DateTime.UtcNow:yyyyMMdd_HHmmss}";
        string jobDir = Path.Combine(_outputDir, jobId);
        Directory.CreateDirectory(jobDir);

        var files = new List<GeneratedFile>
        {
            GenerateHeader(spec),
            GenerateImpl(spec),
            GenerateRegister(spec),
            GenerateCMake(spec),
            GenerateManifest(spec)
        };

        foreach (var f in files)
            await File.WriteAllTextAsync(Path.Combine(jobDir, f.Name), f.Content);

        // 打包 zip
        string zipPath = Path.Combine(_outputDir, $"{jobId}.zip");
        System.IO.Compression.ZipFile.CreateFromDirectory(jobDir, zipPath);

        return new CodeGenResult { JobId = jobId, Files = files, ZipPath = zipPath };
    }

    // ── 生成头文件 ────────────────────────────────────────────────────────
    private GeneratedFile GenerateHeader(CodegenModuleSpec spec)
    {
        string macro = spec.moduleName.ToUpper();
        var sb = new System.Text.StringBuilder();
        sb.AppendLine($"/* Auto-generated by TuningTool CodeGen. DO NOT EDIT header guard. */");
        sb.AppendLine($"#ifndef {macro}_MODULE_H");
        sb.AppendLine($"#define {macro}_MODULE_H");
        sb.AppendLine();
        sb.AppendLine("#include \"module_interface.h\"");
        sb.AppendLine("#include \"dynchain_types.h\"");
        sb.AppendLine();
        sb.AppendLine($"/* Module type ID */");
        sb.AppendLine($"#define {macro}_TYPEID  {spec.TypeId}U");
        sb.AppendLine($"#define {macro}_TYPENAME \"{spec.ModuleName}\"");
        sb.AppendLine();
        sb.AppendLine($"/* Parameter type IDs */");
        foreach (var p in spec.Params)
            sb.AppendLine($"#define {macro}_PARAM_{p.Name.ToUpper()}  {p.ParamTypeId}U");
        sb.AppendLine();
        sb.AppendLine($"/* Parameter struct */");
        sb.AppendLine($"typedef struct {{");
        sb.AppendLine($"    uint8_t  enable;");
        foreach (var p in spec.Params)
        {
            string ctype = p.DataType switch { "float" => "float", "int32" => "int32_t", _ => "uint8_t" };
            string arrSuffix = p.Channelized ? "[DYNCHAIN_MAX_CHANNELS]" : "";
            sb.AppendLine($"    {ctype,-10} {p.Name}{arrSuffix};  /* {p.Description ?? ""} */");
        }
        sb.AppendLine($"}} {spec.ModuleName}Params;");
        sb.AppendLine();
        sb.AppendLine($"/* Runtime state (user-defined, placed after params in pState) */");
        sb.AppendLine($"typedef struct {{");
        sb.AppendLine($"    {spec.ModuleName}Params params;");
        sb.AppendLine($"    /* TODO: add runtime state fields here */");
        sb.AppendLine($"}} {spec.ModuleName}State;");
        sb.AppendLine();
        sb.AppendLine($"/* Standard module interface functions */");
        sb.AppendLine($"uint32_t {spec.ModuleName}_GetMemSize(const ModuleInstance* pInst);");
        sb.AppendLine($"int32_t  {spec.ModuleName}_Init     (ModuleInstance* pInst);");
        sb.AppendLine($"int32_t  {spec.ModuleName}_Process  (ModuleInstance* pInst);");
        sb.AppendLine($"int32_t  {spec.ModuleName}_SetParam (ModuleInstance* pInst,");
        sb.AppendLine($"             uint16_t paramId, const void* pData, uint32_t dataSize);");
        sb.AppendLine($"int32_t  {spec.ModuleName}_GetParam (ModuleInstance* pInst,");
        sb.AppendLine($"             uint16_t paramId, void* pData, uint32_t dataSize);");
        sb.AppendLine();
        sb.AppendLine($"/* DLL registration entry point (called by DllModuleLoader) */");
        sb.AppendLine($"DYNCHAIN_API int32_t RegisterModules(void* pChain);");
        sb.AppendLine();
        sb.AppendLine($"#endif /* {macro}_MODULE_H */");
        return new GeneratedFile($"{spec.ModuleName}.h", sb.ToString());
    }

    // ── 生成实现文件 ──────────────────────────────────────────────────────
    private GeneratedFile GenerateImpl(CodegenModuleSpec spec)
    {
        string macro = spec.moduleName.ToUpper();
        var sb = new System.Text.StringBuilder();
        sb.AppendLine($"/* Auto-generated by TuningTool CodeGen. Fill in TODO sections. */");
        sb.AppendLine($"#include \"{spec.ModuleName}.h\"");
        sb.AppendLine($"#include <string.h>");
        sb.AppendLine($"#include <math.h>");
        sb.AppendLine();
        sb.AppendLine($"/* Helper: get typed state pointer */");
        sb.AppendLine($"#define GET_STATE(pInst) (({spec.ModuleName}State*)(pInst)->pState)");
        sb.AppendLine();

        // GetMemSize
        sb.AppendLine($"uint32_t {spec.ModuleName}_GetMemSize(const ModuleInstance* pInst) {{");
        sb.AppendLine($"    (void)pInst;");
        sb.AppendLine($"    return (uint32_t)sizeof({spec.ModuleName}State);");
        sb.AppendLine($"}}");
        sb.AppendLine();

        // Init
        sb.AppendLine($"int32_t {spec.ModuleName}_Init(ModuleInstance* pInst) {{");
        sb.AppendLine($"    {spec.ModuleName}State* s = GET_STATE(pInst);");
        sb.AppendLine($"    memset(s, 0, sizeof(*s));");
        sb.AppendLine($"    /* Default params */");
        sb.AppendLine($"    s->params.enable = 1;");
        foreach (var p in spec.Params)
            sb.AppendLine($"    /* s->params.{p.Name} = {p.DefaultValue}f; */");
        sb.AppendLine($"    /* TODO: initialize runtime state */");
        sb.AppendLine($"    return 0;");
        sb.AppendLine($"}}");
        sb.AppendLine();

        // Process
        sb.AppendLine($"int32_t {spec.ModuleName}_Process(ModuleInstance* pInst) {{");
        sb.AppendLine($"    {spec.ModuleName}State* s = GET_STATE(pInst);");
        sb.AppendLine($"    if (!s->params.enable) {{");
        sb.AppendLine($"        /* Pass-through when disabled */");
        sb.AppendLine($"        WireInstance* pIn  = pInst->pWires[0];  /* first input wire */");
        sb.AppendLine($"        WireInstance* pOut = pInst->pWires[{spec.InputPorts.Count}]; /* first output wire */");
        sb.AppendLine($"        uint32_t ch, n = Wire_BlockSize(pIn);");
        sb.AppendLine($"        for (ch = 0; ch < Wire_Channels(pIn); ch++)");
        sb.AppendLine($"            memcpy(Wire_ChanPtr(pOut,ch), Wire_ChanPtr(pIn,ch), n*sizeof(float));");
        sb.AppendLine($"        return 0;");
        sb.AppendLine($"    }}");
        sb.AppendLine();
        sb.AppendLine($"    /* TODO: implement DSP algorithm */");
        sb.AppendLine($"    return 0;");
        sb.AppendLine($"}}");
        sb.AppendLine();

        // SetParam
        sb.AppendLine($"int32_t {spec.ModuleName}_SetParam(ModuleInstance* pInst,");
        sb.AppendLine($"        uint16_t paramId, const void* pData, uint32_t dataSize) {{");
        sb.AppendLine($"    {spec.ModuleName}State* s = GET_STATE(pInst);");
        sb.AppendLine($"    switch (paramId) {{");
        sb.AppendLine($"    case 0: /* enable */");
        sb.AppendLine($"        s->params.enable = *(const uint8_t*)pData; return 0;");
        foreach (var p in spec.Params)
        {
            string cast = p.DataType switch { "float" => "float", "int32" => "int32_t", _ => "uint8_t" };
            sb.AppendLine($"    case {macro}_PARAM_{p.Name.ToUpper()}:");
            sb.AppendLine($"        s->params.{p.Name} = *({cast}*)pData; return 0;");
        }
        sb.AppendLine($"    default: return -1;");
        sb.AppendLine($"    }}");
        sb.AppendLine($"}}");
        sb.AppendLine();

        // GetParam
        sb.AppendLine($"int32_t {spec.ModuleName}_GetParam(ModuleInstance* pInst,");
        sb.AppendLine($"        uint16_t paramId, void* pData, uint32_t dataSize) {{");
        sb.AppendLine($"    {spec.ModuleName}State* s = GET_STATE(pInst);");
        sb.AppendLine($"    switch (paramId) {{");
        sb.AppendLine($"    case 0: *( uint8_t*)pData = s->params.enable; return 0;");
        foreach (var p in spec.Params)
        {
            string cast = p.DataType switch { "float" => "float", "int32" => "int32_t", _ => "uint8_t" };
            sb.AppendLine($"    case {macro}_PARAM_{p.Name.ToUpper()}: *({cast}*)pData = s->params.{p.Name}; return 0;");
        }
        sb.AppendLine($"    default: return -1;");
        sb.AppendLine($"    }}");
        sb.AppendLine($"}}");
        return new GeneratedFile($"{spec.ModuleName}.c", sb.ToString());
    }

    // ── 生成注册文件 ──────────────────────────────────────────────────────
    private GeneratedFile GenerateRegister(CodegenModuleSpec spec)
    {
        string macro = spec.moduleName.ToUpper();
        var sb = new System.Text.StringBuilder();
        sb.AppendLine($"/* Auto-generated module registration entry point */");
        sb.AppendLine($"#include \"{spec.ModuleName}.h\"");
        sb.AppendLine($"#include \"dynchain_interface.h\"");
        sb.AppendLine();
        sb.AppendLine($"static const ModuleFuncTable k_{spec.ModuleName}FuncTable = {{");
        sb.AppendLine($"    .typeNumId  = {macro}_TYPEID,");
        sb.AppendLine($"    .typeName   = {macro}_TYPENAME,");
        sb.AppendLine($"    .getMemSize = {spec.ModuleName}_GetMemSize,");
        sb.AppendLine($"    .init       = {spec.ModuleName}_Init,");
        sb.AppendLine($"    .process    = {spec.ModuleName}_Process,");
        sb.AppendLine($"    .setParam   = {spec.ModuleName}_SetParam,");
        sb.AppendLine($"    .getParam   = {spec.ModuleName}_GetParam,");
        sb.AppendLine($"}};");
        sb.AppendLine();
        sb.AppendLine($"DYNCHAIN_API int32_t RegisterModules(void* pChain) {{");
        sb.AppendLine($"    return DynChain_RegisterModule(pChain, &k_{spec.ModuleName}FuncTable);");
        sb.AppendLine($"}}");
        return new GeneratedFile($"{spec.ModuleName}_register.c", sb.ToString());
    }

    // ── 生成 CMakeLists.txt ───────────────────────────────────────────────
    private GeneratedFile GenerateCMake(CodegenModuleSpec spec)
    {
        string macro = spec.moduleName.ToUpper();
        var sb = new System.Text.StringBuilder();
        sb.AppendLine($"# Auto-generated CMakeLists for {spec.ModuleName}");
        sb.AppendLine($"cmake_minimum_required(VERSION 3.16)");
        sb.AppendLine($"project({spec.ModuleName} C)");
        sb.AppendLine();
        sb.AppendLine($"# Point to dspalgo/include");
        sb.AppendLine($"set(DSPALGO_INCLUDE_DIR \"${{CMAKE_SOURCE_DIR}}/../dspalgo/include\"");
        sb.AppendLine($"    CACHE PATH \"Path to dspalgo/include\")");
        sb.AppendLine();
        sb.AppendLine($"add_library({spec.ModuleName} SHARED");
        sb.AppendLine($"    {spec.ModuleName}.c");
        sb.AppendLine($"    {spec.ModuleName}_register.c");
        sb.AppendLine($")");
        sb.AppendLine();
        sb.AppendLine($"target_include_directories({spec.ModuleName} PRIVATE");
        sb.AppendLine($"    ${{DSPALGO_INCLUDE_DIR}}");
        sb.AppendLine($"    ${{CMAKE_SOURCE_DIR}}");
        sb.AppendLine($")");
        sb.AppendLine();
        sb.AppendLine($"target_compile_definitions({spec.ModuleName} PRIVATE");
        sb.AppendLine($"    DYNCHAIN_EXPORT");
        sb.AppendLine($"    PLATFORM_PC");
        sb.AppendLine($")");
        sb.AppendLine();
        sb.AppendLine($"# Link against main DynamicChain DLL import lib (Windows)");
        sb.AppendLine($"if(WIN32)");
        sb.AppendLine($"    target_link_libraries({spec.ModuleName} PRIVATE");
        sb.AppendLine($"        \"${{CMAKE_SOURCE_DIR}}/../DynamicChain.lib\")");
        sb.AppendLine($"endif()");
        sb.AppendLine();
        sb.AppendLine($"set_target_properties({spec.ModuleName} PROPERTIES");
        sb.AppendLine($"    C_STANDARD 11");
        sb.AppendLine($"    OUTPUT_NAME \"{spec.ModuleName}\"");
        sb.AppendLine($")");
        return new GeneratedFile("CMakeLists.txt", sb.ToString());
    }

    // ── 生成模块描述清单(供前端模块库加载用)────────────────────────────
    private GeneratedFile GenerateManifest(CodegenModuleSpec spec)
    {
        var manifest = new
        {
            moduleName  = spec.ModuleName,
            typeId      = spec.TypeId,
            description = spec.Description,
            version     = "1.0",
            inputPorts  = spec.InputPorts.Select(p => new {
                id = p.Name, direction = "input", channels = p.Channels, sampleRate = p.SampleRate
            }),
            outputPorts = spec.OutputPorts.Select(p => new {
                id = p.Name, direction = "output", channels = p.Channels, sampleRate = p.SampleRate
            }),
            params = spec.Params.Select(p => new {
                name        = p.Name,
                paramTypeId = p.ParamTypeId,
                dataType    = p.DataType,
                defaultValue = p.DefaultValue,
                minValue    = p.MinValue,
                maxValue    = p.MaxValue,
                channelized = p.Channelized,
                description = p.Description
            })
        };
        string json = JsonSerializer.Serialize(manifest,
            new JsonSerializerOptions { WriteIndented = true });
        return new GeneratedFile("module_manifest.json", json);
    }
}

public record CodeGenResult(string JobId, List<GeneratedFile> Files, string ZipPath);
public record GeneratedFile(string Name, string Content);

15.4 REST API 端点

方法 路径 说明 响应
POST /api/codegen/module 生成模块代码 { jobId, files:[{name,content}] }
GET /api/codegen/download/{jobId} 下载生成的 .zip 包 ZIP 文件二进制流
GET /api/codegen/templates 列出可用的模块模板 { templates:[{id,name,description}] }
POST /api/codegen/load-dll 热加载已编译的模块 DLL { success, loadedModules:[...] }

15.5 WebSocket 消息(CodeGen)

type 方向 说明
codegen_module C→S 提交模块定义,触发代码生成
codegen_result S→C 返回生成结果(jobId + 文件列表)
load_module_dll C→S 热加载指定路径的模块 DLL
module_dll_loaded S→C DLL 加载成功,包含新模块的 typeId 和 typeName
refresh_module_library S→C 广播通知所有客户端刷新模块库(新模块可用)

前端模块库刷新流程

用户上传已编译的 CustomLimiter.dll
  → WebSocket: load_module_dll { dllPath: "./data/modules/CustomLimiter.dll" }
  → 后端 DllModuleLoader.LoadModuleDll()
  → DynChain 注册 CustomLimiter(typeId=0x16679001)
  → 后端读取该 DLL 旁的 module_manifest.json
  → WebSocket 广播: module_dll_loaded { typeId, typeName, manifest }
  → WebSocket 广播: refresh_module_library
前端收到 refresh_module_library
  → 发送 get_module_library
  → 后端返回完整模块列表(含新增的 CustomLimiter)
  → 前端模块库面板刷新,CustomLimiter 出现在列表中

16. 文件结构更新(v6.0 增量)

backend/
├── Services/
│   ├── Simulation/
│   │   ├── DspCoreSimulator.cs      单核仿真(独立线程 + DynChain 实例)
│   │   ├── DspCorePool.cs           核心池协调者(初始化、路由、帧同步屏障)
│   │   └── AudioRingBuffer.cs       核间 lock-free 音频帧缓冲
│   ├── ThreadAssignmentService.cs   ChangeThread 模块→核心分配管理
│   ├── CodeGenService.cs            代码生成(头文件/实现/CMake/Manifest)
│   └── DllModuleLoader.cs           自定义模块 DLL 动态加载与注册
├── Interop/
│   └── DynChainInterop.cs           更新:对齐 DSPAlgo v7 接口
├── Models/
│   └── CodeGenModels.cs             CodegenModuleSpec / CodeGenResult / GeneratedFile
├── Controllers/
│   └── CodeGenController.cs         REST: /api/codegen/*
└── data/
    ├── codegen/                     生成的代码文件(按 jobId 子目录组织)
    └── modules/                     用户上传的自定义模块 DLL

17. 消息路由表更新(v6.0 增量)

## 5. 消息路由表 中新增以下条目:

type Handler 说明
sim_set_core_count HandleSimSetCoreCount() 设置 PC 仿真核心数,重建核心池
sim_set_core_intra_threads HandleSimSetCoreIntraThreads() 设置指定核心内部线程数(模拟 DSP 核内并行)
sim_get_core_status HandleSimGetCoreStatus() 获取所有核心实时状态
assign_module_thread HandleAssignModuleThread() 将模块分配到指定核心;触发受影响核心重建
assign_module_threads_batch HandleAssignModuleThreadsBatch() 批量更新模块→核心分配表
get_thread_assignments HandleGetThreadAssignments() 获取当前完整分配表
codegen_module HandleCodegenModule() 提交模块定义,生成 C 代码框架
load_module_dll HandleLoadModuleDll() 热加载自定义模块 DLL,向 DynChain 注册

18. 配置参数更新(v6.0 增量)

appsettings.jsonAudioEngine 节中新增:

{
  "AudioEngine": {
    "DllPath":            "./DynamicChain.dll",
    "DefaultInputDevice": "",
    "DefaultOutputDevice":"",
    "FrameSize":          240,
    "SampleRate":         48000,
    "SimCoreCount":       2,
    "SimFrameSyncMode":   "barrier",
    "InputRingCapacity":  4,
    "OutputRingCapacity": 4
  },
  "CodeGen": {
    "OutputDir":          "./data/codegen",
    "ModulesDir":         "./data/modules",
    "DspalgoIncludeDir":  "../dspalgo/include"
  }
}

19. 扩展点(v6.0 新增)

需求 实现方式
DSP 硬件 ChangeThread 连接后查询 dsp_capabilities,发 CMD=0x08(core assign),硬件侧需 firmware 支持
核心间信号路由可视化 前端在链路画布中以不同颜色区分模块所属核心,边(连线)跨核时显示虚线
自定义模块 DLL 热卸载 NativeLibrary.Free() + DynChain_UnregisterModule() + 广播 refresh_module_library
CodeGen 模板扩展 data/codegen/templates/ 目录下放置 .hbs Handlebars 模板,CodeGenService 使用 Handlebars.Net 渲染
自动 typeId 分配 CodeGenService 维护 typeId_registry.json,为新模块自动分配不冲突的 typeId(基于项目保留范围 0x16679000~0x1667FFFF)
仿真 MCPS 限额 DspCoreSimulator 增加 MaxMcpsLimit 配置,超过阈值时触发 sim_overload 警告广播
跨平台支持 DynChainInteropDllName 改为运行时判断:Windows = "DynamicChain.dll",Linux = "libDynamicChain.so"

新增(v7.0):Legacy 兼容通信模式


20. Legacy 兼容模式总体设计

20.1 背景与定位

除了基于 DynamicChain + 链路构建的新式工作流,系统还需要兼容老版本调音工具的通信模式。老版调音工具通过 SocketClient 类直接以 PID(Parameter ID) 地址对设备进行读写,无需链路配置,是一个纯粹的通信工具。

Legacy 模式的核心特征: - 无链路构建:不发送链路帧,设备固件中的信号链已由工厂固化 - PID 直寻址:每个参数对应唯一的 4 字节 PID - 配置文件驱动 UI:前端通过导入一个 JSON 配置文件自动生成控件界面 - 兼容老协议帧:后端使用与老版 SocketClient 相同的帧格式(UART ADI 帧 / ETH 二进制帧 / ETH AWE 文本帧) - Preset/Profile 支持:基于 PID 原始字节做参数快照

20.2 工作模式切换

系统在原有三种模式(chain_builder / tuning / test_verify)基础上增加 legacy 模式:

set_mode: "legacy"   →  进入 Legacy 兼容模式
                         禁用 write_link / sim_start / codegen 等 DynChain 功能
                         启用 legacy_* 消息处理器
模式 链路构建 DynChain 仿真 Legacy 通信 CodeGen
chain_builder
tuning
test_verify
legacy

20.3 整体架构图

┌─────────────────────────────────────────────────────────────────────┐
│                        Frontend(Vue3)                               │
│                                                                     │
│   ┌─────────────────────────────────────────────────────────────┐  │
│   │  LegacyConfigImport                                          │  │
│   │  导入 JSON 配置文件 → 自动生成控件面板                         │  │
│   └─────────────────────────────────────────────────────────────┘  │
│           │ legacy_load_config               legacy_config_ui ↓     │
└───────────┼──────────────────────────────────────────────────────────┘
            │ WebSocket
┌───────────▼──────────────────────────────────────────────────────────┐
│                        Backend(ASP.NET Core 8)                      │
│                                                                       │
│  ┌─────────────────┐   ┌──────────────────────────────────────────┐  │
│  │ LegacyConfigSvc │   │        消息路由(legacy_* 消息类型)        │  │
│  │ 解析导入的JSON   │   │  legacy_set_param / legacy_get_param      │  │
│  │ 生成 UI 描述符  │   │  legacy_save_preset / legacy_load_preset  │  │
│  └────────┬────────┘   │  legacy_save_profile / legacy_load_profile│  │
│           │            └──────────────────────┬───────────────────┘  │
│           │                                   │                      │
│  ┌────────▼────────────────────────────────────▼──────────────────┐  │
│  │              LegacyCommService                                   │  │
│  │  协议类型: uart_adi / eth / eth_awe                              │  │
│  │  ┌────────────────────────────────────────────────────────┐     │  │
│  │  │ LegacyFrameBuilder                                      │     │  │
│  │  │  BuildUartAdiWriteFrame() / BuildUartAdiReadFrame()     │     │  │
│  │  │  BuildEthWriteFrame()    / BuildEthReadFrame()          │     │  │
│  │  │  BuildAweWriteCmd()      / BuildAweReadCmd()            │     │  │
│  │  │  CRC32()                                                │     │  │
│  │  └────────────────────────────────────────────────────────┘     │  │
│  │                                                                  │  │
│  │  LegacyConversionEngine                                         │  │
│  │  dBtoLinear / linearTodB / msToSamples / customFormula          │  │
│  └──────────────────────────────┬───────────────────────────────────┘  │
│                                 │ IDspTransport                      │
│                          ┌──────▼──────┐                             │
│                          │ TcpTransport │                             │
│                          │ SerialTrans  │                             │
│                          └─────────────┘                             │
│                                                                       │
│  ┌─────────────────────────────────────────────────────────────────┐ │
│  │  LegacyPresetService                                             │ │
│  │  data/legacy/presets/{moduleGroup}/{presetId}.json               │ │
│  │  data/legacy/profiles/{profileId}.json                           │ │
│  └─────────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────────┘

21. Legacy 配置文件格式(JSON)

21.1 文件结构

前端导入的 JSON 配置文件完整描述设备的参数空间、UI 布局和转换规则:

{
  "version": "1.0",
  "deviceName": "Car Audio DSP v2",
  "description": "门限/延迟/EQ 调音配置",

  "connection": {
    "protocol": "uart_adi",
    "defaultTarget": "COM3",
    "defaultBaudRate": 115200,
    "defaultTcpHost": "192.168.1.141",
    "defaultTcpPort": 15007,
    "defaultAweBuffer": "Esefun1.TuneBuf[0],Esefun1"
  },

  "moduleGroups": [
    {
      "id": "gain_ctrl",
      "name": "增益控制",
      "modules": [
        {
          "id": "master_gain",
          "name": "主增益",
          "parameters": [
            {
              "id":        "master_gain_db",
              "name":      "增益 (dB)",
              "pid":       "0x16670010",
              "dataType":  "float",
              "dataSize":  4,
              "channels":  1,
              "control":   "slider",
              "min":       -60.0,
              "max":       12.0,
              "step":      0.1,
              "defaultValue": 0.0,
              "readBack":  true,
              "conversion": {
                "type": "dBtoLinear",
                "toDevice": "Math.pow(10, x / 20)",
                "fromDevice": "20 * Math.log10(x)"
              }
            },
            {
              "id":        "master_mute",
              "name":      "静音",
              "pid":       "0x16670011",
              "dataType":  "uint8",
              "dataSize":  4,
              "channels":  1,
              "control":   "toggle",
              "defaultValue": 0,
              "conversion": { "type": "identity" }
            }
          ]
        }
      ]
    },
    {
      "id": "delay_ctrl",
      "name": "延迟控制",
      "modules": [
        {
          "id": "channel_delay",
          "name": "通道延迟",
          "parameters": [
            {
              "id":        "delay_ch0",
              "name":      "Ch0 延迟 (ms)",
              "pid":       "0x16672010",
              "dataType":  "int32",
              "dataSize":  4,
              "channels":  1,
              "control":   "slider",
              "min":       0.0,
              "max":       500.0,
              "step":      0.02,
              "defaultValue": 0.0,
              "conversion": {
                "type": "msToSamples",
                "sampleRate": 48000,
                "toDevice":   "Math.round(x * 48000 / 1000)",
                "fromDevice": "x * 1000 / 48000"
              }
            }
          ]
        }
      ]
    },
    {
      "id": "eq_band",
      "name": "参数均衡",
      "modules": [
        {
          "id": "peq_band1",
          "name": "Band 1",
          "parameters": [
            {
              "id":        "peq_b1_gain",
              "name":      "增益 (dB)",
              "pid":       "0x16676010",
              "dataType":  "floatArray",
              "dataSize":  20,
              "channels":  5,
              "control":   "slider",
              "min":       -15.0,
              "max":       15.0,
              "step":      0.1,
              "defaultValue": 0.0,
              "layout":    "perChannel",
              "conversion": { "type": "identity" }
            }
          ]
        }
      ]
    }
  ]
}

21.2 字段说明

connection 节

字段 说明
protocol uart_adi / eth / eth_awe
defaultTarget 串口名(COM3)或 TCP 主机(192.168.1.141
defaultBaudRate 串口波特率(默认 115200)
defaultAweBuffer AWE 协议的缓冲区名,格式:"bufferVar,subsystemName",如 "Esefun1.TuneBuf[0],Esefun1"

parameter 节

字段 类型 说明
pid string 十六进制 PID,如 "0x16670010",解析为 uint32
dataType string float/int32/int16/uint8/floatArray/int32Array/bytes
dataSize int 下发字节数(单通道)。floatArray 时为所有通道总字节数
channels int 数组长度(floatArray 等多通道参数)
control string 前端控件类型:slider/toggle/dropdown/display/numberInput
readBack bool 是否支持读回(发 GET 帧)
conversion.type string identity/dBtoLinear/linearTodB/msToSamples/samplesToMs/formula
conversion.toDevice string 写设备前的转换公式(JS 表达式,变量 x
conversion.fromDevice string 读回后的反转换公式

22. Legacy 通信协议实现

22.1 UART ADI 帧格式(对齐 SocketClient)

写请求帧(小数据,负载 < 1009 字节):
┌────────┬─────────┬───────┬──────────┬──────────────┬──────────┬─────────┐
│ Header │ FrameID │ CMDWR │ PID      │ SizePayload  │ Data     │ CRC32   │
│ 2B     │ 2B LE   │ 1B    │ 4B LE    │ 2B 字节序交换 │ N bytes  │ 4B LE   │
│ AA 55  │ 0x0000  │ 0x01  │ pid uint │ swapped      │ payload  │ crc32   │
└────────┴─────────┴───────┴──────────┴──────────────┴──────────┴─────────┘
注:SizePayload 字节序交换:len_L=(size&0xFF)<<8 | size>>8(对应 SocketClient 的大小端转换逻辑)

读请求帧:
│ Header │ FrameID │ CMDWR │ PID   │ SizePayload │ CRC32 │
│ AA 55  │ 0x0000  │ 0x00  │ 4B LE │ 2B 交换     │ 4B LE │

读/写响应帧:
┌────────┬─────────┬─────────┬──────────────┬──────────┬─────────┐
│ Header │ FrameID │ ErrCode │ SizePayload  │ Data     │ CRC32   │
│ 2B     │ 2B LE   │ 1B      │ 2B           │ N bytes  │ 4B LE   │
│ AA 55  │         │ 0=OK    │ 读请求时有效  │          │         │
└────────┴─────────┴─────────┴──────────────┴──────────┴─────────┘

大数据分包帧(负载 ≥ 1009 字节,每包 900 字节):
┌────────┬─────────┬───────┬──────────────┬──────────────────────────────┬──────────┬─────────┐
│ Header │ FrameID │ CMDWR │ PID=0x166700 │ PackageHeader(12B)           │ Data     │ CRC32   │
│ AA 55  │         │ 0x01  │ 04 (大包PID) │ CMDPID(4)+TotalSz(4)+Pos(4)  │ ≤900B    │ 4B LE   │
└────────┴─────────┴───────┴──────────────┴──────────────────────────────┴──────────┴─────────┘

22.2 ETH 二进制帧格式

写命令(adb_com_format_setCmd + Data):
┌────────────┬──────────┬────────────┬──────────────┬──────────────────┐
│ cmdType    │ Param_ID │ Param_Size │ packagesz    │ Data             │
│ 4B LE      │ 4B LE    │ 4B LE      │ 4B LE        │ packagesz bytes  │
│ 0x44332211 │ pid      │ totalSize  │ ≤100000      │                  │
└────────────┴──────────┴────────────┴──────────────┴──────────────────┘
读命令:cmdType = 0x11223344,Data 为空

ETH 无 CRC32 校验。

22.3 ETH AWE 文本帧格式

写命令(文本,换行符结尾):
  {subsystem},write_int_array,{bufName},{cmdMagic},0x{pid_hex},0x{size_hex},{data_big_endian}...

读触发(3步流程):
  Step1 Write:  {subsystem},write_int_array,{bufName},0xAABBCCDD,0x{pid_hex},0x{size_hex}\n
  Step2 Read:   read_int_array,{readStartBuf},3\n   (读响应大小)
  Step3 Read:   read_int_array,{readStartBuf},{responseSize}\n  (读响应数据)

数据格式:大端十六进制字符串,每 4 字节表示为 "0xAABBCCDD",逗号分隔。

22.4 LegacyFrameBuilder 实现

// Services/Legacy/LegacyFrameBuilder.cs

public class LegacyFrameBuilder
{
    private static ushort _frameId = 0;

    private const ushort UART_ADI_HEADER = 0x55AA;   // 小端存储 → 线上字节 AA 55
    private const byte   UART_CMD_WRITE  = 0x01;
    private const byte   UART_CMD_READ   = 0x00;
    private const uint   UART_LARGE_PID  = 0x16670004; // 大包传输 PID

    // ── UART ADI 帧 ──────────────────────────────────────────────────

    /// <summary>构建小数据写帧(负载 < 1009 字节)</summary>
    public byte[] BuildUartWriteFrame(uint pid, byte[] payload)
    {
        ushort frameId = _frameId++;
        ushort swappedSize = SwapUInt16((ushort)payload.Length);

        // 帧头: AA55(2) + FrameID(2) + CMDWR(1) + PID(4) + SizeSwapped(2)
        var header = new byte[11];
        BitConverter.GetBytes(UART_ADI_HEADER).CopyTo(header, 0);   // AA 55
        BitConverter.GetBytes(frameId).CopyTo(header, 2);
        header[4] = UART_CMD_WRITE;
        BitConverter.GetBytes(pid).CopyTo(header, 5);
        BitConverter.GetBytes(swappedSize).CopyTo(header, 9);

        return AppendCrc32(Concat(header, payload));
    }

    /// <summary>构建读请求帧</summary>
    public byte[] BuildUartReadFrame(uint pid, int readSize)
    {
        ushort frameId = _frameId++;
        ushort swappedSize = SwapUInt16((ushort)readSize);

        var header = new byte[11];
        BitConverter.GetBytes(UART_ADI_HEADER).CopyTo(header, 0);
        BitConverter.GetBytes(frameId).CopyTo(header, 2);
        header[4] = UART_CMD_READ;
        BitConverter.GetBytes(pid).CopyTo(header, 5);
        BitConverter.GetBytes(swappedSize).CopyTo(header, 9);

        return AppendCrc32(header);
    }

    /// <summary>构建大数据分包帧序列(每包 900 字节)</summary>
    public IEnumerable<byte[]> BuildUartLargeFrames(uint pid, byte[] payload)
    {
        int pos = 0;
        while (pos < payload.Length)
        {
            int chunkSize = Math.Min(900, payload.Length - pos);
            var chunk = payload[pos..(pos + chunkSize)];

            // 分包 PID = 0x16670004,负载 = 12字节包头 + 数据块
            ushort swappedSize = SwapUInt16((ushort)(chunkSize + 12));
            var frameHeader = new byte[11];
            BitConverter.GetBytes(UART_ADI_HEADER).CopyTo(frameHeader, 0);
            BitConverter.GetBytes(_frameId++).CopyTo(frameHeader, 2);
            frameHeader[4] = UART_CMD_WRITE;
            BitConverter.GetBytes(UART_LARGE_PID).CopyTo(frameHeader, 5);
            BitConverter.GetBytes(swappedSize).CopyTo(frameHeader, 9);

            // 12 字节包头: CMDPID(4) + TotalPayloadSize(4) + PayloadPosition(4)
            var pkgHeader = new byte[12];
            BitConverter.GetBytes(pid).CopyTo(pkgHeader, 0);
            BitConverter.GetBytes((uint)payload.Length).CopyTo(pkgHeader, 4);
            BitConverter.GetBytes((uint)pos).CopyTo(pkgHeader, 8);

            yield return AppendCrc32(Concat(frameHeader, pkgHeader, chunk));
            pos += chunkSize;
        }
    }

    /// <summary>解析 UART 响应帧:返回 (errCode, payload);CRC 校验失败抛出异常</summary>
    public (byte errCode, byte[] payload) ParseUartResponse(byte[] response)
    {
        if (response.Length < 11)
            throw new InvalidDataException("响应帧过短");

        // 验证帧头
        ushort hdr = BitConverter.ToUInt16(response, 0);
        if (hdr != UART_ADI_HEADER)
            throw new InvalidDataException($"帧头不匹配: 0x{hdr:X4}");

        byte errCode   = response[4];
        ushort payloadLen = BitConverter.ToUInt16(response, 5);

        // 验证 CRC32(对前 7 字节 + payload 计算)
        int dataLen = 7 + payloadLen;
        uint expectedCrc = BitConverter.ToUInt32(response, dataLen);
        uint actualCrc   = ComputeCrc32(response, 0, (uint)dataLen);
        if (expectedCrc != actualCrc)
            throw new InvalidDataException($"CRC32 校验失败: expected={expectedCrc:X8}, actual={actualCrc:X8}");

        byte[] payload = response[7..(7 + payloadLen)];
        return (errCode, payload);
    }

    // ── ETH 二进制帧 ────────────────────────────────────────────────

    /// <summary>构建 ETH 写帧(分包迭代器)</summary>
    public IEnumerable<byte[]> BuildEthWriteFrames(uint pid, byte[] payload)
    {
        int pos = 0;
        const int MAX_PKG = 100_000;
        while (pos < payload.Length)
        {
            int chunkSize = Math.Min(MAX_PKG, payload.Length - pos);
            var hdr = new byte[16];
            BitConverter.GetBytes(0x44332211u).CopyTo(hdr, 0);   // cmdType = write
            BitConverter.GetBytes(pid).CopyTo(hdr, 4);
            BitConverter.GetBytes((uint)payload.Length).CopyTo(hdr, 8);
            BitConverter.GetBytes((uint)chunkSize).CopyTo(hdr, 12);
            yield return Concat(hdr, payload[pos..(pos + chunkSize)]);
            pos += chunkSize;
        }
    }

    /// <summary>构建 ETH 读帧</summary>
    public byte[] BuildEthReadFrame(uint pid, int readSize)
    {
        var hdr = new byte[16];
        BitConverter.GetBytes(0x11223344u).CopyTo(hdr, 0);        // cmdType = read
        BitConverter.GetBytes(pid).CopyTo(hdr, 4);
        BitConverter.GetBytes((uint)readSize).CopyTo(hdr, 8);
        BitConverter.GetBytes(0u).CopyTo(hdr, 12);
        return hdr;
    }

    // ── AWE 文本帧 ──────────────────────────────────────────────────

    /// <summary>构建 AWE 写命令字符串</summary>
    public string BuildAweWriteCmd(string subsystem, string bufName,
                                   uint pid, byte[] payload)
    {
        string pidHex  = $"0x{pid:x8}";
        string sizeHex = $"0x{payload.Length / 4:x8}";
        string dataStr = string.Join(",", ChunkByFour(payload)
            .Select(w => $"0x{w[3]:x2}{w[2]:x2}{w[1]:x2}{w[0]:x2}"));  // 大端排列
        return $"{subsystem},write_int_array,{bufName},0x44332211,{pidHex},{sizeHex},{dataStr}\n";
    }

    /// <summary>构建 AWE 读触发命令(Step1 写)</summary>
    public string BuildAweReadTriggerCmd(string subsystem, string bufName,
                                         uint pid, int readSize)
    {
        string pidHex  = $"0x{pid:x8}";
        string sizeHex = $"0x{readSize / 4:x8}";
        return $"{subsystem},write_int_array,{bufName},0xAABBCCDD,{pidHex},{sizeHex}\n";
    }

    /// <summary>构建 AWE 读回命令(Step2/Step3)</summary>
    public string BuildAweReadDataCmd(string readStartBuf, int count)
        => $"read_int_array,{readStartBuf},{count}\n";

    // ── 内部工具 ─────────────────────────────────────────────────────

    private static ushort SwapUInt16(ushort v)
        => (ushort)((v & 0xFF) << 8 | (v >> 8));

    private static byte[] AppendCrc32(byte[] data)
    {
        uint crc = ComputeCrc32(data, 0, (uint)data.Length);
        var result = new byte[data.Length + 4];
        data.CopyTo(result, 0);
        BitConverter.GetBytes(crc).CopyTo(result, data.Length);
        return result;
    }

    private static byte[] Concat(params byte[][] parts)
    {
        int total = parts.Sum(p => p.Length);
        var result = new byte[total];
        int pos = 0;
        foreach (var p in parts) { p.CopyTo(result, pos); pos += p.Length; }
        return result;
    }

    private static IEnumerable<byte[]> ChunkByFour(byte[] data)
    {
        for (int i = 0; i < data.Length; i += 4)
            yield return data[i..Math.Min(i + 4, data.Length)];
    }

    // CRC32 标准查表法(多项式 0xEDB88320,兼容 SocketClient)
    private static readonly uint[] Crc32Tab = BuildCrc32Table();
    private static uint[] BuildCrc32Table()
    {
        var table = new uint[256];
        for (uint i = 0; i < 256; i++)
        {
            uint c = i;
            for (int k = 0; k < 8; k++)
                c = (c & 1) != 0 ? (0xEDB88320u ^ (c >> 1)) : (c >> 1);
            table[i] = c;
        }
        return table;
    }

    public static uint ComputeCrc32(byte[] data, uint offset, uint len)
    {
        uint crc = 0xFFFFFFFF;
        for (uint i = offset; i < offset + len; i++)
            crc = Crc32Tab[(crc ^ data[i]) & 0xFF] ^ (crc >> 8);
        return crc ^ 0xFFFFFFFF;
    }
}

22.5 LegacyCommService(通信服务)

// Services/Legacy/LegacyCommService.cs
public class LegacyCommService
{
    private IDspTransport? _transport;
    private readonly LegacyFrameBuilder _builder = new();

    public string Protocol { get; private set; } = "none";  // uart_adi / eth / eth_awe
    public bool IsConnected => _transport?.IsConnected ?? false;

    // AWE 协议专用配置
    private string _aweBufName    = "Esefun1.TuneBuf[0]";
    private string _aweReadBuf    = "Esefun1.TuneBuf[3]";
    private string _aweSubsystem  = "Esefun1";

    public async Task ConnectAsync(LegacyConnectionConfig cfg)
    {
        Protocol = cfg.Protocol;
        _transport = cfg.Protocol switch
        {
            "uart_adi" => new SerialTransport(cfg.Target, cfg.BaudRate),
            "eth"      => new TcpTransport(cfg.TcpHost, cfg.TcpPort),
            "eth_awe"  => new TcpTransport(cfg.TcpHost, cfg.TcpPort),
            _ => throw new NotSupportedException($"不支持的协议: {cfg.Protocol}")
        };
        if (cfg.Protocol == "eth_awe" && cfg.AweBuffer != null)
        {
            var parts = cfg.AweBuffer.Split(',');
            _aweBufName   = parts[0];
            _aweSubsystem = parts.Length > 1 ? parts[1] : "Esefun1";
            _aweReadBuf   = System.Text.RegularExpressions.Regex
                            .Replace(_aweBufName, @"\[\d+\]", "[3]");
        }
        await _transport.ConnectAsync();
    }

    public async Task DisconnectAsync()
    {
        if (_transport != null)
        {
            await _transport.DisconnectAsync();
            _transport = null;
        }
    }

    /// <summary>发送写参数帧(自动处理分包和大数据)</summary>
    public async Task<bool> WriteParamAsync(uint pid, byte[] rawBytes)
    {
        if (_transport == null) return false;

        switch (Protocol)
        {
            case "uart_adi":
                if (rawBytes.Length < 1009)
                {
                    // 单帧
                    var frame = _builder.BuildUartWriteFrame(pid, rawBytes);
                    await _transport.DeliverAsync(frame);
                    var resp = await ReceiveUartResponseAsync();
                    var (err, _) = _builder.ParseUartResponse(resp);
                    return err == 0;
                }
                else
                {
                    // 分包发送
                    foreach (var chunk in _builder.BuildUartLargeFrames(pid, rawBytes))
                    {
                        await _transport.DeliverAsync(chunk);
                        var resp = await ReceiveUartResponseAsync();
                        var (err, _) = _builder.ParseUartResponse(resp);
                        if (err != 0) return false;
                        await Task.Delay(100);  // 分包间隔(对齐 SocketClient Thread.Sleep(100))
                    }
                    return true;
                }

            case "eth":
                foreach (var frame in _builder.BuildEthWriteFrames(pid, rawBytes))
                    await _transport.DeliverAsync(frame);
                return true;

            case "eth_awe":
                string cmd = _builder.BuildAweWriteCmd(_aweSubsystem, _aweBufName,
                                                        pid, rawBytes);
                await _transport.DeliverAsync(System.Text.Encoding.ASCII.GetBytes(cmd));
                return true;

            default:
                return false;
        }
    }

    /// <summary>发送读参数帧,返回原始字节</summary>
    public async Task<byte[]?> ReadParamAsync(uint pid, int readSize)
    {
        if (_transport == null) return null;

        switch (Protocol)
        {
            case "uart_adi":
                var reqFrame = _builder.BuildUartReadFrame(pid, readSize);
                await _transport.DeliverAsync(reqFrame);
                var resp = await ReceiveUartResponseAsync();
                var (err, payload) = _builder.ParseUartResponse(resp);
                return err == 0 ? payload : null;

            case "eth":
                var readFrame = _builder.BuildEthReadFrame(pid, readSize);
                await _transport.DeliverAsync(readFrame);
                return await ReceiveEthResponseAsync(readSize);

            case "eth_awe":
                // Step1: 触发写
                string trig = _builder.BuildAweReadTriggerCmd(_aweSubsystem, _aweBufName, pid, readSize);
                await _transport.DeliverAsync(System.Text.Encoding.ASCII.GetBytes(trig));
                // Step2: 读大小
                string step2 = _builder.BuildAweReadDataCmd(_aweReadBuf, 3);
                await _transport.DeliverAsync(System.Text.Encoding.ASCII.GetBytes(step2));
                var sizeResp = await ReceiveAweResponseAsync();
                int actualSize = ParseAweSize(sizeResp);
                // Step3: 读数据
                string step3 = _builder.BuildAweReadDataCmd(_aweReadBuf, actualSize);
                await _transport.DeliverAsync(System.Text.Encoding.ASCII.GetBytes(step3));
                var dataResp = await ReceiveAweResponseAsync();
                return ParseAweData(dataResp);

            default:
                return null;
        }
    }

    private async Task<byte[]> ReceiveUartResponseAsync(int timeoutMs = 500)
    {
        // 使用 IDspTransport.RegisterReceiver 接收回调
        var tcs = new TaskCompletionSource<byte[]>();
        _transport!.RegisterReceiver(data => tcs.TrySetResult(data));
        using var cts = new CancellationTokenSource(timeoutMs);
        cts.Token.Register(() => tcs.TrySetException(new TimeoutException("UART 响应超时")));
        return await tcs.Task;
    }

    private async Task<byte[]> ReceiveEthResponseAsync(int expectedSize, int timeoutMs = 500)
    {
        var tcs = new TaskCompletionSource<byte[]>();
        var buf = new List<byte>();
        _transport!.RegisterReceiver(data =>
        {
            buf.AddRange(data);
            if (buf.Count >= expectedSize) tcs.TrySetResult(buf.ToArray());
        });
        using var cts = new CancellationTokenSource(timeoutMs);
        cts.Token.Register(() => tcs.TrySetResult(buf.ToArray()));
        return await tcs.Task;
    }

    private async Task<string> ReceiveAweResponseAsync(int timeoutMs = 5000)
    {
        var tcs = new TaskCompletionSource<string>();
        _transport!.RegisterReceiver(data =>
        {
            string s = System.Text.Encoding.ASCII.GetString(data);
            if (s.Contains('\n')) tcs.TrySetResult(s);
        });
        using var cts = new CancellationTokenSource(timeoutMs);
        cts.Token.Register(() => tcs.TrySetResult(""));
        return await tcs.Task;
    }

    private static int ParseAweSize(string resp)
    {
        // AWE 响应格式:以十六进制字符串返回,提取第三个 uint32
        var parts = resp.Trim().Split(',', ' ');
        if (parts.Length >= 3 && parts[2].StartsWith("0x"))
            return (int)Convert.ToUInt32(parts[2], 16);
        return 0;
    }

    private static byte[] ParseAweData(string resp)
    {
        var parts = resp.Trim().Split(',', ' ')
            .Where(s => s.StartsWith("0x")).ToArray();
        var result = new List<byte>();
        foreach (var p in parts)
        {
            uint val = Convert.ToUInt32(p, 16);
            // 大端转小端
            result.Add((byte)(val >> 24));
            result.Add((byte)(val >> 16));
            result.Add((byte)(val >> 8));
            result.Add((byte)(val));
        }
        return result.ToArray();
    }
}

public record LegacyConnectionConfig(
    string Protocol,         // "uart_adi" | "eth" | "eth_awe"
    string Target,           // 串口名或 TCP Host
    int    BaudRate,
    string TcpHost,
    int    TcpPort,
    string? AweBuffer        // "Esefun1.TuneBuf[0],Esefun1"
);

23. LegacyConversionEngine(参数值转换)

// Services/Legacy/LegacyConversionEngine.cs
public static class LegacyConversionEngine
{
    /// <summary>
    /// 将前端 UI 值转换为设备原始字节(按 parameter.conversion 规则)。
    /// </summary>
    public static byte[] ToDeviceBytes(LegacyParamDef param, double uiValue)
    {
        double deviceValue = ApplyFormula(param.Conversion.ToDevice, uiValue);
        return Serialize(param.DataType, deviceValue, param.DataSize);
    }

    /// <summary>将设备原始字节反转换为 UI 值</summary>
    public static double FromDeviceBytes(LegacyParamDef param, byte[] raw)
    {
        double deviceValue = Deserialize(param.DataType, raw);
        return ApplyFormula(param.Conversion.FromDevice, deviceValue);
    }

    /// <summary>对浮点数组参数(floatArray)按通道转换</summary>
    public static byte[] ToDeviceBytesArray(LegacyParamDef param, double[] uiValues)
    {
        var result = new List<byte>();
        foreach (double v in uiValues)
        {
            double dv = ApplyFormula(param.Conversion.ToDevice, v);
            result.AddRange(SerializeSingle(param.DataType, dv));
        }
        return result.ToArray();
    }

    // ── 内置转换公式 ──────────────────────────────────────────────────
    private static double ApplyFormula(string formula, double x) => formula switch
    {
        null or "" or "x"         => x,
        "dBtoLinear"              => Math.Pow(10.0, x / 20.0),
        "linearTodB"              => 20.0 * Math.Log10(Math.Max(x, 1e-10)),
        "msToSamples"             => Math.Round(x * 48000.0 / 1000.0),
        "samplesToMs"             => x * 1000.0 / 48000.0,
        _                         => EvalSimple(formula, x)   // 简单内联公式
    };

    /// <summary>
    /// 支持简单单变量公式(不引入外部 JS 引擎),支持:
    ///   x * k, x / k, x + k, x - k
    ///   Math.pow(10, x/20), 20 * Math.log10(x), Math.round(x * k / m)
    /// 复杂公式请在 conversion.type 中使用内置别名。
    /// </summary>
    private static double EvalSimple(string expr, double x)
    {
        // 简单线性: "x * 0.001" 或 "x / 1000"
        if (System.Text.RegularExpressions.Regex.IsMatch(expr, @"^x\s*[*/]\s*[\d.]+$"))
        {
            var m = System.Text.RegularExpressions.Regex.Match(expr, @"([*/])\s*([\d.]+)");
            double k = double.Parse(m.Groups[2].Value);
            return m.Groups[1].Value == "*" ? x * k : x / k;
        }
        // dB 互换简写
        if (expr.Contains("pow(10") && expr.Contains("/20")) return Math.Pow(10.0, x / 20.0);
        if (expr.Contains("log10"))  return 20.0 * Math.Log10(Math.Max(x, 1e-10));
        if (expr.Contains("round") && expr.Contains("48000")) return Math.Round(x * 48000.0 / 1000.0);
        return x;   // fallback: identity
    }

    // ── 序列化/反序列化 ───────────────────────────────────────────────
    private static byte[] Serialize(string dataType, double value, int totalSize)
    {
        var buf = new byte[totalSize];
        switch (dataType)
        {
            case "float":
                BitConverter.GetBytes((float)value).CopyTo(buf, 0); break;
            case "int32":
                BitConverter.GetBytes((int)Math.Round(value)).CopyTo(buf, 0); break;
            case "int16":
                BitConverter.GetBytes((short)Math.Round(value)).CopyTo(buf, 0); break;
            case "uint8":
                buf[0] = (byte)Math.Round(value); break;
        }
        return buf;
    }

    private static byte[] SerializeSingle(string dataType, double value)
        => Serialize(dataType, value, dataType switch {
            "float" => 4, "int32" => 4, "int16" => 2, _ => 4 });

    private static double Deserialize(string dataType, byte[] raw) => dataType switch
    {
        "float"      => BitConverter.ToSingle(raw, 0),
        "int32"      => BitConverter.ToInt32(raw, 0),
        "int16"      => BitConverter.ToInt16(raw, 0),
        "uint8"      => raw[0],
        "floatArray" => BitConverter.ToSingle(raw, 0),  // 取第一通道
        _            => 0.0
    };
}

24. LegacyConfigService(配置管理)

// Services/Legacy/LegacyConfigService.cs
public class LegacyConfigService
{
    private LegacyDeviceConfig? _config;

    /// <summary>加载并解析前端上传的配置 JSON</summary>
    public LegacyConfigLoadResult Load(string configJson)
    {
        _config = JsonSerializer.Deserialize<LegacyDeviceConfig>(configJson,
            new JsonSerializerOptions { PropertyNameCaseInsensitive = true });

        if (_config == null)
            return new LegacyConfigLoadResult(false, "JSON 解析失败", null);

        // 生成前端 UI 描述符
        var uiDescriptor = BuildUiDescriptor(_config);
        return new LegacyConfigLoadResult(true, null, uiDescriptor);
    }

    public LegacyDeviceConfig? Config => _config;

    public LegacyParamDef? FindParam(string paramId)
        => _config?.ModuleGroups
            .SelectMany(g => g.Modules)
            .SelectMany(m => m.Parameters)
            .FirstOrDefault(p => p.Id == paramId);

    public LegacyParamDef? FindParamByPid(uint pid)
        => _config?.ModuleGroups
            .SelectMany(g => g.Modules)
            .SelectMany(m => m.Parameters)
            .FirstOrDefault(p => p.PidUInt32 == pid);

    /// <summary>
    /// 将配置文件结构转换为前端控件描述符(前端据此动态渲染 UI)
    /// </summary>
    private static LegacyUiDescriptor BuildUiDescriptor(LegacyDeviceConfig cfg)
    {
        var groups = cfg.ModuleGroups.Select(g => new UiGroupDescriptor
        {
            Id   = g.Id,
            Name = g.Name,
            Modules = g.Modules.Select(m => new UiModuleDescriptor
            {
                Id   = m.Id,
                Name = m.Name,
                Controls = m.Parameters.Select(p => new UiControlDescriptor
                {
                    ParamId      = p.Id,
                    Name         = p.Name,
                    Pid          = p.Pid,
                    ControlType  = p.Control,
                    DataType     = p.DataType,
                    DataSize     = p.DataSize,
                    Channels     = p.Channels,
                    Min          = p.Min,
                    Max          = p.Max,
                    Step         = p.Step,
                    DefaultValue = p.DefaultValue,
                    ReadBack     = p.ReadBack,
                    Layout       = p.Layout
                }).ToList()
            }).ToList()
        }).ToList();

        return new LegacyUiDescriptor
        {
            DeviceName  = cfg.DeviceName,
            Description = cfg.Description,
            Connection  = cfg.Connection,
            Groups      = groups
        };
    }
}

25. Legacy Preset / Profile 系统

25.1 Preset 设计

Legacy 模式的 Preset 基于 PID → 原始字节 映射,不依赖 instanceId。存储格式:

// data/legacy/presets/{moduleGroupId}/{presetId}.json
{
  "presetId":    "bass_boost",
  "name":        "Bass Boost",
  "groupId":     "eq_band",
  "description": "低频增强",
  "params": {
    "0x16676010": "3FC00000",
    "0x16676014": "BFC00000",
    "0x16672010": "00000060"
  }
}

params 字段:键为 PID 十六进制字符串,值为设备原始字节的十六进制字符串(SocketClient 风格)。

25.2 Profile 设计

Profile 是跨 moduleGroup 的 Preset 组合:

// data/legacy/profiles/{profileId}.json
{
  "profileId": "night_mode",
  "name":      "夜间模式",
  "presets": {
    "gain_ctrl":  "low_volume",
    "eq_band":    "soft_eq",
    "delay_ctrl": "standard_delay"
  }
}

25.3 LegacyPresetService

// Services/Legacy/LegacyPresetService.cs
public class LegacyPresetService
{
    private readonly string _presetDir;
    private readonly string _profileDir;

    public LegacyPresetService(IConfiguration cfg)
    {
        _presetDir  = cfg["Legacy:PresetDir"]  ?? "./data/legacy/presets";
        _profileDir = cfg["Legacy:ProfileDir"] ?? "./data/legacy/profiles";
        Directory.CreateDirectory(_presetDir);
        Directory.CreateDirectory(_profileDir);
    }

    // ── Preset CRUD ─────────────────────────────────────────────────

    public async Task SavePresetAsync(LegacyPreset preset)
    {
        string dir = Path.Combine(_presetDir, preset.GroupId);
        Directory.CreateDirectory(dir);
        string path = Path.Combine(dir, $"{preset.PresetId}.json");
        await File.WriteAllTextAsync(path,
            JsonSerializer.Serialize(preset, new JsonSerializerOptions { WriteIndented = true }));
    }

    public async Task<LegacyPreset?> LoadPresetAsync(string groupId, string presetId)
    {
        string path = Path.Combine(_presetDir, groupId, $"{presetId}.json");
        if (!File.Exists(path)) return null;
        string json = await File.ReadAllTextAsync(path);
        return JsonSerializer.Deserialize<LegacyPreset>(json);
    }

    public async Task<List<LegacyPreset>> ListPresetsAsync(string groupId)
    {
        string dir = Path.Combine(_presetDir, groupId);
        if (!Directory.Exists(dir)) return new();
        var result = new List<LegacyPreset>();
        foreach (var f in Directory.GetFiles(dir, "*.json"))
        {
            var p = JsonSerializer.Deserialize<LegacyPreset>(await File.ReadAllTextAsync(f));
            if (p != null) result.Add(p);
        }
        return result;
    }

    public Task DeletePresetAsync(string groupId, string presetId)
    {
        string path = Path.Combine(_presetDir, groupId, $"{presetId}.json");
        if (File.Exists(path)) File.Delete(path);
        return Task.CompletedTask;
    }

    // ── Profile CRUD ─────────────────────────────────────────────────

    public async Task SaveProfileAsync(LegacyProfile profile)
    {
        string path = Path.Combine(_profileDir, $"{profile.ProfileId}.json");
        await File.WriteAllTextAsync(path,
            JsonSerializer.Serialize(profile, new JsonSerializerOptions { WriteIndented = true }));
    }

    public async Task<LegacyProfile?> LoadProfileAsync(string profileId)
    {
        string path = Path.Combine(_profileDir, $"{profileId}.json");
        if (!File.Exists(path)) return null;
        return JsonSerializer.Deserialize<LegacyProfile>(await File.ReadAllTextAsync(path));
    }

    public async Task<List<LegacyProfile>> ListProfilesAsync()
    {
        var result = new List<LegacyProfile>();
        foreach (var f in Directory.GetFiles(_profileDir, "*.json"))
        {
            var p = JsonSerializer.Deserialize<LegacyProfile>(await File.ReadAllTextAsync(f));
            if (p != null) result.Add(p);
        }
        return result;
    }
}

public record LegacyPreset(
    string PresetId, string Name, string GroupId, string Description,
    Dictionary<string, string> Params  // PID hex → raw bytes hex
);
public record LegacyProfile(
    string ProfileId, string Name,
    Dictionary<string, string> Presets  // groupId → presetId
);

26. WebSocket 消息协议(Legacy 模式)

26.1 消息路由表(Legacy 增量)

type 方向 Handler 说明
legacy_load_config C→S HandleLegacyLoadConfig() 上传配置 JSON;返回 UI 描述符
legacy_connect C→S HandleLegacyConnect() 建立 UART/TCP 连接(Legacy 模式)
legacy_disconnect C→S HandleLegacyDisconnect() 断开 Legacy 连接
legacy_set_param C→S HandleLegacySetParam() 写参数(自动转换 + 帧组装)
legacy_get_param C→S HandleLegacyGetParam() 读参数(自动反转换)
legacy_set_params_batch C→S HandleLegacySetParamsBatch() 批量写参数(apply preset 时用)
legacy_save_preset C→S HandleLegacySavePreset() 保存 Preset
legacy_load_preset C→S HandleLegacyLoadPreset() 加载 Preset(读文件 + 批量下发)
legacy_delete_preset C→S HandleLegacyDeletePreset() 删除 Preset
legacy_list_presets C→S HandleLegacyListPresets() 列出指定 groupId 的 Preset
legacy_save_profile C→S HandleLegacySaveProfile() 保存 Profile
legacy_load_profile C→S HandleLegacyLoadProfile() 加载 Profile(多组 Preset 批量下发)
legacy_list_profiles C→S HandleLegacyListProfiles() 列出所有 Profile
legacy_get_status C→S HandleLegacyGetStatus() 返回连接状态 + 当前配置名

26.2 消息详细格式

legacy_load_config(上传配置文件)

// 前端 → 后端
{
  "type": "legacy_load_config",
  "config": { /* LegacyDeviceConfig 完整 JSON */ }
}

// 后端 → 前端(返回 UI 描述符)
{
  "type": "legacy_config_loaded",
  "success": true,
  "ui": {
    "deviceName": "Car Audio DSP v2",
    "groups": [
      {
        "id": "gain_ctrl",
        "name": "增益控制",
        "modules": [
          {
            "id": "master_gain",
            "name": "主增益",
            "controls": [
              {
                "paramId": "master_gain_db",
                "name": "增益 (dB)",
                "pid": "0x16670010",
                "controlType": "slider",
                "dataType": "float",
                "dataSize": 4,
                "channels": 1,
                "min": -60.0,
                "max": 12.0,
                "step": 0.1,
                "defaultValue": 0.0,
                "readBack": true
              }
            ]
          }
        ]
      }
    ]
  }
}

legacy_connect

// 前端 → 后端
{
  "type": "legacy_connect",
  "protocol": "uart_adi",
  "target": "COM3",
  "baudRate": 115200
}
// 或 TCP:
{
  "type": "legacy_connect",
  "protocol": "eth",
  "tcpHost": "192.168.1.141",
  "tcpPort": 15007
}
// 或 AWE:
{
  "type": "legacy_connect",
  "protocol": "eth_awe",
  "tcpHost": "192.168.1.100",
  "tcpPort": 15007,
  "aweBuffer": "Esefun1.TuneBuf[0],Esefun1"
}

// 后端 → 前端
{ "type": "legacy_connect_ack", "success": true, "protocol": "uart_adi", "target": "COM3" }

legacy_set_param(写参数)

// 前端 → 后端(UI 值,可选 rawHex 直接下发)
{
  "type": "legacy_set_param",
  "paramId": "master_gain_db",
  "value": -6.0,
  "channel": 0
}
// 或直接指定 pid + rawHex(绕过转换):
{
  "type": "legacy_set_param",
  "pid": "0x16670010",
  "rawHex": "3DCCCCCD",
  "dataSize": 4
}

// 后端 → 前端
{
  "type": "legacy_set_param_ack",
  "paramId": "master_gain_db",
  "pid": "0x16670010",
  "success": true
}

legacy_get_param(读参数)

// 前端 → 后端
{
  "type": "legacy_get_param",
  "paramId": "master_gain_db"
}

// 后端 → 前端
{
  "type": "legacy_get_param_ack",
  "paramId": "master_gain_db",
  "pid": "0x16670010",
  "rawHex": "3DCCCCCD",
  "value": -6.0,
  "success": true
}

legacy_save_preset

{
  "type": "legacy_save_preset",
  "presetId": "bass_boost",
  "name": "Bass Boost",
  "groupId": "eq_band",
  "params": {
    "0x16676010": "3FC00000",
    "0x16676014": "BFC00000"
  }
}
// 响应
{ "type": "legacy_preset_ack", "action": "save", "presetId": "bass_boost", "success": true }

legacy_load_preset

// 前端请求
{ "type": "legacy_load_preset", "groupId": "eq_band", "presetId": "bass_boost" }

// 后端:
// 1. 从文件加载 Preset.params
// 2. 对每个 PID 调用 LegacyCommService.WriteParamAsync()(不做转换,直接下发 rawHex)
// 3. 广播 param 更新给所有前端
{ "type": "legacy_preset_loaded",
  "presetId": "bass_boost",
  "groupId": "eq_band",
  "params": { "0x16676010": "3FC00000", ... },
  "success": true }

legacy_load_profile

{ "type": "legacy_load_profile", "profileId": "night_mode" }

// 后端按 presets map 依次加载每个 groupId 的 Preset
{ "type": "legacy_profile_loaded",
  "profileId": "night_mode",
  "loadedPresets": { "gain_ctrl": "low_volume", "eq_band": "soft_eq" },
  "success": true }

27. 数据流示例

27.1 Legacy 模式完整工作流

① 前端切换到 Legacy 模式
   → WS: { type: "set_mode", mode: "legacy" }
   ← WS: { type: "mode_changed", mode: "legacy" }

② 导入配置文件
   → WS: { type: "legacy_load_config", config: { ... } }
   ← WS: { type: "legacy_config_loaded", ui: { groups: [...] } }
   前端据 ui.groups 动态渲染控件面板

③ 建立连接
   → WS: { type: "legacy_connect", protocol: "uart_adi", target: "COM3", baudRate: 115200 }
   ← WS: { type: "legacy_connect_ack", success: true }

④ 调参(UI 滑块拖动)
   → WS: { type: "legacy_set_param", paramId: "master_gain_db", value: -6.0 }
   后端:查配置 → pid=0x16670010, dBtoLinear(-6) = 0.5012 → float LE 字节
            → LegacyFrameBuilder.BuildUartWriteFrame(0x16670010, [0D CC CC 3F])
            → 发送帧 + 等待响应
   ← WS: { type: "legacy_set_param_ack", paramId: "master_gain_db", success: true }
   ← WS: { type: "legacy_param_update", paramId: "master_gain_db", value: -6.0 }(广播给其他客户端)

⑤ 保存 Preset
   前端将当前所有控件值以 rawHex 形式打包
   → WS: { type: "legacy_save_preset", presetId: "test1", name: "测试1",
            groupId: "gain_ctrl", params: { "0x16670010": "0DCCCC3F", ... } }
   ← WS: { type: "legacy_preset_ack", action: "save", success: true }

⑥ 加载 Preset(一键下发)
   → WS: { type: "legacy_load_preset", groupId: "gain_ctrl", presetId: "test1" }
   后端:读文件 → 按 PID 逐条调 WriteParamAsync → 完成
   ← WS: { type: "legacy_preset_loaded", presetId: "test1", success: true }
   ← WS: { type: "legacy_params_applied", params: { "master_gain_db": -6.0, ... } }(广播)

27.2 UART ADI 帧级数据流(-6dB 写入)

UI 值:-6.0 dB
↓ LegacyConversionEngine.ToDeviceBytes("float", dBtoLinear(-6.0))
  → deviceValue = 10^(-6/20) = 0.50119
  → float LE bytes: [DF 7C 00 3F]

↓ LegacyFrameBuilder.BuildUartWriteFrame(pid=0x16670010, payload=[DF 7C 00 3F])
  → header.Header    = 0x55AA  (wire bytes: AA 55)
  → header.FrameID   = 0x0000  (wire bytes: 00 00)
  → header.CMDWR     = 0x01
  → header.PID       = 0x16670010 (LE: 10 00 67 16)
  → SizePayload(交换) = SwapUInt16(4) = 0x0400 (wire bytes: 00 04)
  → frame data: AA 55 | 00 00 | 01 | 10 00 67 16 | 00 04 | DF 7C 00 3F
  → CRC32(frame[0..14]) → append 4 bytes
  → 最终帧(19 bytes): AA 55 00 00 01 10 00 67 16 00 04 DF 7C 00 3F XX XX XX XX

↓ SerialTransport.DeliverAsync(frame)

↑ 设备应答(11 bytes): AA 55 00 00 00 00 00 + CRC(4)
                                       ↑ ErrCode=0 ✓

28. 文件结构更新(v7.0 增量)

backend/
├── Services/
│   └── Legacy/
│       ├── LegacyCommService.cs       连接/读写/协议适配
│       ├── LegacyFrameBuilder.cs      UART ADI / ETH / AWE 帧组装与解析
│       ├── LegacyConversionEngine.cs  参数值↔设备字节 转换引擎
│       ├── LegacyConfigService.cs     配置文件解析 + UI 描述符生成
│       └── LegacyPresetService.cs     Preset/Profile 持久化
├── Models/
│   └── LegacyModels.cs
│         LegacyDeviceConfig / LegacyModuleGroup / LegacyModule
│         LegacyParamDef / LegacyConversionDef
│         LegacyUiDescriptor / UiGroupDescriptor / UiModuleDescriptor / UiControlDescriptor
│         LegacyPreset / LegacyProfile
│         LegacyConnectionConfig
└── data/
    └── legacy/
        ├── presets/
        │   ├── gain_ctrl/
        │   │   ├── flat.json
        │   │   └── bass_boost.json
        │   └── eq_band/
        │       └── soft_eq.json
        └── profiles/
            └── night_mode.json

29. 配置参数更新(v7.0 增量)

{
  "Legacy": {
    "PresetDir":   "./data/legacy/presets",
    "ProfileDir":  "./data/legacy/profiles",
    "ConfigDir":   "./data/legacy/configs",
    "UartTimeout": 500,
    "UartLargePackageIntervalMs": 100,
    "EthMaxPackageSize": 100000,
    "AdbMaxPackageSize": 2000
  }
}

30. 扩展点(v7.0 新增)

需求 实现方式
QPST 支持 新增 QpstTransport : IDspTransport,封装 QACT RTC API(GetRTCDataV2/SetRTCDataV2),需引入 QUTestApp 依赖
ADB 支持 新增 AdbTransport : IDspTransport,通过 adb shell cat/push 实现 FIFO 传输
I2C/SPI 支持 新增 AardvarkTransport : IDspTransport,调用 Total Phase Aardvark API
多协议同时下发 LegacyCommService 改为 List<IDspTransport>,写参数时广播到所有传输层
配置文件版本管理 LegacyConfigService 维护 ./data/legacy/configs/ 目录,支持多设备配置切换
参数读回轮询 新增 legacy_start_readback_poll / legacy_stop_readback_poll 消息,按 intervalMs 定期读回并推送
十六进制编辑器模式 前端特殊控件 controlType: "hexEditor",直接编辑 rawHex 后通过 legacy_set_param.rawHex 下发