跳转至
DRAFT

XiStudio · 后端架构设计 v1.0

设计定位

本文档描述后端(backend_csharp)在 Source/Sink Phase 8 改造中的架构角色。涵盖从旧"双轨改写"向新"纯透传"的迁移、服务分层、链路构建与参数下发、以及跨平台音频引擎。

1. 后端的核心职责

职责 负责服务 关键决策
WebSocket 消息路由 WebSocket/* 80+ 消息类型
链路编译与验证 LinkFrameBuilder 模块 ID 透传 vs 改写
DSP 帧构建 LinkFrameBuilder / BinaryFrameBuilder 拓扑排序、端口传播
参数下发 PresetProfileService 参数生命周期四层模型
音频引擎驱动 AudioEngineService WASAPI callback 单点驱动
设备枚举 AudioIoFactory 跨平台驱动选择
项目文件管理 ProjectService 工程保存与加载
监控与诊断 MetricsAggregator 性能指标收集

2. 架构目标 vs 当前实现

2.1 链路管理:从双轨改写到纯透传

架构目标(Phase 8)

后端在 LinkFrameBuilder彻底移除改写逻辑

  • source_v1source_gen_v1 不再支持,直接抛出 unknown_module_id 错误
  • 所有 10 个 Phase 8 source 模块(source_sine_v1 等)直接透传,不做任何改写
  • sink_v1 映射到 sink_v1_dsp,作为 DSP 侧内部 PCM 缓冲终点
  • 后端零参与 source/sink 的"外部注入" vs "内部生成"分化

当前实现(后端已支持)

查看 backend_csharp/Services/Link/LinkFrameBuilder.cs(第 20-40 行):

// Phase 8 已知 source moduleId 集合(直接透传,不做任何改写)
private static readonly HashSet<string> _phase8SourceModuleIds = new(StringComparer.OrdinalIgnoreCase)
{
    "source_sine_v1", "source_sweep_v1", "source_triangle_v1", "source_square_v1",
    "source_saw_v1", "source_noise_white_v1", "source_noise_pink_v1",
    "source_noise_brown_v1", "source_wav_v1", "source_device_v1",
};

// 旧 source_v1 / source_gen_v1 不再支持
if (baseId == "source_v1" || baseId == "source_gen_v1")
    throw new ArgumentException(
        $"unknown_module_id: {moduleId}. Use Phase 8 modules: source_sine_v1, source_sweep_v1, ...");

// 所有 Phase 8 source 模块直接透传
if (_phase8SourceModuleIds.Contains(effectiveModuleId)) {
    // 不改写 moduleId,直接查表 _typeIdMap
}

结论:后端已准备好支持 Phase 8 模块体系,不存在双轨改写的架构债。

2.2 音频引擎驱动:单点驱动模型

架构目标(Q3 决定)

后端应采用 WASAPI callback(或 PortAudio callback)单点驱动模型:

  • 每个 audio block(通常 64 sample @ 48 kHz = 1.33 ms)触发一次 callback
  • Callback 内依次调用:
  • 设备输入读取(若有 source_device_v1
  • DSPAlgo_Process() 处理链路
  • 设备输出写入(若有 sink_v1_dsp
  • 完全避免后端独立线程 (BackgroundService) 与 callback 并行的双重驱动

当前实现(后端计划中)

旧版 AudioEngineService 仍使用 BackgroundService 线程模型。Phase 8 实施计划中会创建 WasapiDrivenEngine.cs,替换为 callback 驱动。

关键改进

  • 后端业务逻辑线程(WebSocket handling / parameter update)与 DSP Process 线程通过无锁 SPSC queue 通信
  • SetParam 消息写入 queue → DSP block 边界读取 queue → 应用参数变更
  • 避免运行中的 race condition(旧 mixer "5kHz 随后丢失"事故根因)

架构目标

后端在参数下发时严格区分两类操作:

操作 触发场景 下发方式
SetLink 链路拓扑改变、模块添加移除、端口重新配置 重新调用 DSPAlgo_SetLink(新链路二进制帧)
SetParam 参数值变更(不涉及拓扑) 调用 DSPAlgo_SetParamDSPAlgo_SetParamBulk

当前实现

PresetProfileService.cs 已实现四层参数模型:

  • L1:ModuleDef 出厂默认值(前端管理)
  • L2:link.json 中 instance.paramValues
  • L3:ModulePreset 快照(文件系统存储)
  • L4:运行时 paramStore(ConcurrentDictionary)

后端正确处理 SetLink 与 SetParam 的分离,Preset 加载通过批量 SetParam 实现。

3. 服务分层架构

3.1 WebSocket 消息路由

层级 组件 职责
入口 AudioEngineController WebSocket 连接管理、消息解析分发
业务逻辑 LinkService / PresetProfileService 链路编辑、参数管理
音频驱动 AudioEngineService / AudioIoFactory 引擎启停、设备切换
监控 MetricsAggregator CPU / 内存 / 丢帧统计

3.2 链路构建流程

LinkJson (from frontend)
LinkFrameBuilder.BuildFromLinkJson()
  ├─ 解析 modules[] / connections[]
  ├─ 拓扑排序(DFS)
  ├─ 映射 moduleId → typeId
  ├─ 调用 BinaryFrameBuilder 生成二进制帧
  └─ 返回 (frameBytes, instanceOrder[], sourceInstances, sinkInstances)
AudioEngineService.SetLink()
  ├─ P/Invoke DSPAlgo_Create(frameBytes)
  └─ 记录 instanceOrder[] 用于后续 SetParam

3.3 参数下发流程

Frontend SetParam Message
PresetProfileService.UpdateParam({instanceId, paramId, value})
  ├─ 更新 paramStore[$"{instanceId}.{paramId}"] = value
  ├─ 序列化为 SetParam 二进制帧
  └─ P/Invoke DSPAlgo_SetParam(frameBytes)
DSP 处理参数变更(Process 线程读取)

4. 跨平台音频驱动

4.1 IAudioIo 接口隔离

public interface IAudioIo {
    // 设备枚举
    AudioDeviceInfo[] GetInputDevices();
    AudioDeviceInfo[] GetOutputDevices();

    // 驱动开始/停止
    void Start(Action<float[]> processCallback, int blockSize, int sampleRate);
    void Stop();

    // 设备切换(热切换,不停止引擎)
    void SwitchInputDevice(string deviceId);
    void SwitchOutputDevice(string deviceId);
}

4.2 实现清单

平台 实现 状态 驱动
Windows WindowsAudioIo WASAPI + ASIO
Linux PortAudioIo 计划中 PortAudio
macOS PortAudioIo 计划中 PortAudio
测试 MockAudioIo 定时驱动模拟

5. Phase 8 后端改动清单

文件 改动 优先级
Services/Link/LinkFrameBuilder.cs 移除旧 source_gen_v1 改写逻辑,支持 10 个 Phase 8 模块 P0
Services/Link/LinkPropagator.cs 新建:端口信息传播(PortInfo propagation) P0
Services/AudioEngine/WasapiDrivenEngine.cs 新建:callback 驱动模型替换 BackgroundService P0
Services/Metrics/MetricsAggregator.cs 新建:CPU% / memory / 丢帧 / 平均参数更新延迟统计 P1
Services/Project/ProjectService.cs 新建:工程文件 Save/Load(link + presets/profiles) P1
Services/Preset/PresetProfileService.cs 微调:DQ6 选项 A(SetAmbiance 内部调 HandleSetAmbiance) P2
Services/AudioEngine/AudioEngineService.cs 梳理参数化 blockSize/sampleRate,移除硬编码 P2

6. 关键决策点

6.1 后端不再做 source 改写(P0 级)

旧做法:后端在 LinkFrameBuilder 中强制把 source_v1 改成 source_gen_v1,同时 AudioEngineService 用影子槽位外部注入。

新做法:后端完全移除这套改写逻辑,所有 source 模块由前端直接指定,后端纯透传。

收益: - 消除了"虚模块 + 影子槽位"的复杂度 - 减少参数状态同步的风险点 - 符合用户"对架构来说都不用多线程"的设想

6.2 callback 单点驱动(P0 级)

旧做法:BackgroundService 线程独立运行,与 WASAPI callback 并行,通过 ConcurrentQueue 通信。

新做法:整个 DSP Process 与参数下发都在 WASAPI callback 线程内完成,后端业务线程只负责消息分发。

收益: - 避免双重驱动导致的信号丢失 / 覆盖 - 精确定时(callback 粒度) - 符合 RT 音频领域最佳实践

6.3 链路透传、参数隔离(P0 级)

后端完全向外透传链路拓扑,只负责序列化为 DSP 二进制帧。参数生命周期完全由前端与 DSP 协同管理。

7. 当前状态汇总

方面 状态 备注
LinkFrameBuilder 改造 ✅ 已支持 Phase 8 代码中已有 10 个模块的 typeId 映射
callback 驱动 🔄 计划中 需创建 WasapiDrivenEngine.cs
PortAudio 支持 🔄 计划中 Linux/macOS 暂用 MockAudioIo
参数四层模型 ✅ 已实现 PresetProfileService 完整支持
项目 Save/Load 🔄 计划中 需创建 ProjectService
监控指标体系 🔄 计划中 需创建 MetricsAggregator

8. 后端对前端/DSP 的协议

8.1 LinkJson 格式约定

{
  "id": "link1",
  "name": "My Signal Chain",
  "modules": [
    {
      "instanceId": "src_sine_1",
      "moduleId": "source_sine_v1",
      "paramValues": {
        "frequencyHz": 1000,
        "amplitudeDb": -6
      }
    }
  ],
  "connections": [...]
}

关键点:moduleId 必须来自已注册的 10 个 Phase 8 模块,否则报 unknown_module_id 错误。

8.2 SetParam 二进制帧格式

后端将参数变更序列化为二进制帧,通过 P/Invoke 传递给 DLL:

[Format ID: 0xA5 0x02] [Frame Length: 4B] [Payload: {instanceIdx, paramIdx, value}...]

9. 结论

后端在 Phase 8 改造中已经做好了模块体系的纯透传、参数生命周期的清晰分离。剩余工作聚焦于:

  1. callback 驱动:确保时序正确,消除双重驱动风险
  2. 跨平台一致性:PortAudio 集成与 Linux/macOS 测试
  3. 监控与诊断:完善性能指标收集,便于后期优化