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_v1与source_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 随后丢失"事故根因)
2.3 参数下发:SetParam / SetLink 清晰边界
架构目标
后端在参数下发时严格区分两类操作:
| 操作 | 触发场景 | 下发方式 |
|---|---|---|
| SetLink | 链路拓扑改变、模块添加移除、端口重新配置 | 重新调用 DSPAlgo_SetLink(新链路二进制帧) |
| SetParam | 参数值变更(不涉及拓扑) | 调用 DSPAlgo_SetParam 或 DSPAlgo_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:
9. 结论
后端在 Phase 8 改造中已经做好了模块体系的纯透传、参数生命周期的清晰分离。剩余工作聚焦于:
- callback 驱动:确保时序正确,消除双重驱动风险
- 跨平台一致性:PortAudio 集成与 Linux/macOS 测试
- 监控与诊断:完善性能指标收集,便于后期优化