XiStudio · Source 模块架构 v1.1
关键改动(v1.1 vs v7.0)
v7.0 是架构审查文档,重点讨论改造决策与 Phase 8 实施计划。v1.1 聚焦于前后端 DSP 三层一体的架构设计,采用"架构目标 vs 当前实现"双栏格式,直接参考三层设计文档:
- 前端架构:Frontend Implementation
- 后端架构:Backend Architecture
- DSP 架构:DSP Algorithm Architecture
1. 总体架构:三层协同模型
Source 模块涉及三个独立的代码库与三个架构层,它们在 Phase 8 中达成清晰的职责分工:
| 层级 | 代码库 | 核心职责 | 参考文档 |
|---|---|---|---|
| UI 层 | frontend_vue3 |
信号类型选择、参数调试面板、链路可视化 | 20-frontend.md |
| 业务层 | backend_csharp |
链路编译、参数下发、音频引擎驱动 | Backend Architecture |
| 处理层 | dsp_algo |
信号生成、文件回放、实时处理(RT 约束) | DSP Architecture |
2. 架构目标 vs 当前实现
2.1 模块体系:10 个独立模块
| 架构目标 | 当前实现 | 状态 |
|---|---|---|
概念:旧 source_v1 单体分化为 10 个职责清晰的独立模块 |
10 个模块 (sine/sweep/triangle/square/saw/noise_white/pink/brown/wav/device) 完全实现 | ✅ 已就位 |
| 参数集:每个模块只有与其信号属性相关的参数(无条件分支地狱) | 所有参数已按业务属性分类(system/tunable/readonly/structural) | ✅ 已就位 |
| UX:前端库中每个模块独立卡片,用户可视化选择 | 前端 moduleLibrary.ts 中已预定义 10 个 ModuleDef,moduleIcons.ts 有对应图标 |
✅ 已就位 |
| 后端支持:后端不做模块改写,直接透传 moduleId | LinkFrameBuilder 已移除旧改写逻辑,支持所有 10 个 Phase 8 模块 | ✅ 已就位 |
总结:10 个独立模块方案在三层已全部落地,无架构债。
2.2 实时处理约束:零独立线程(Q3 决定)
| 架构目标 | 当前实现 | 状态 |
|---|---|---|
| 约束:DSP 处理完全由后端 WASAPI callback(或 PortAudio callback)单点驱动,业务层零独立线程 | 后端旧 BackgroundService 线程模型需迁移;新 WasapiDrivenEngine.cs 在计划中 | 🔄 计划中 |
| 参数更新:SetParam 通过无锁 SPSC queue 推入 DSP,在 block 边界应用 | 后端框架已支持,PresetProfileService 维护 paramStore;细节需 callback 模型配合 | 🔄 计划中 |
| WAV 回放:整段文件预读入内存(加载时 I/O),运行时 Process 线程零 I/O | source_wav_v1 已实现内存预读逻辑,seek 仅改指针 | ✅ 已就位 |
| 设备采集:后端独立 capture 线程采样,通过无锁 queue 注入 DSP;DSP passthrough | source_device_v1 为 passthrough 设计,后端 capture 线程待实现 | 🔄 计划中 |
总结:RT 约束在 DSP 侧已完全遵守;后端线程模型迁移(callback 驱动)是 Phase 8 重点。
2.3 参数生命周期:四层清晰分离
| 架构目标 | 当前实现 | 状态 |
|---|---|---|
| L1 ModuleDef 默认值 | 前端 moduleLibrary.ts 中每个 ModuleDef 都有完整参数默认值 | ✅ 已就位 |
| L2 Link 中的 instance.paramValues | link.json 中保存每个 instance 的当前参数值 | ✅ 已就位 |
| L3 ModulePreset 快照 | 前端可保存多个预设,后端文件系统存储 (./data/presets/{instanceId}/{presetId}.json) | ✅ 已就位 |
| L4 运行时 paramStore | 后端 PresetProfileService._paramStore (ConcurrentDictionary) 维护运行时参数值 | ✅ 已就位 |
| SetLink 初始化:链路部署时,L2 层的 instance.paramValues 推入后端 DSP | LinkFrameBuilder 解析 link.json → PresetProfileService 推入 paramStore → DSPAlgo_SetLink 下发 | ✅ 已就位 |
| SetParam 变更:运行中通过 SetParam 更新参数,仅涉及 L4 层,不触发 DSP SetLink | PresetProfileService.UpdateParam() → DSPAlgo_SetParam() | ✅ 已就位 |
总结:参数四层模型在三层已完全实现,SetLink 与 SetParam 清晰分离。
2.4 信号类型 vs 参数关系
目标设计
graph TB
User["用户选择信号类型"]
User -->|Tone| ToneGrp["Tone 类(5 个)<br/>sine/sweep/triangle/square/saw"]
User -->|Noise| NoiseGrp["Noise 类(3 个)<br/>white/pink/brown"]
User -->|File| FileGrp["File 类(1 个)<br/>wav"]
User -->|Device| DevGrp["Device 类(1 个)<br/>device"]
ToneGrp -->|各模块参数集| T1["sine_v1:<br/>frequency/amplitude/phase<br/>channelPhaseOffset"]
ToneGrp -->|各模块参数集| T2["sweep_v1:<br/>startFreq/endFreq/duration<br/>sweepType/mode"]
ToneGrp -->|各模块参数集| T3["triangle_v1:<br/>frequency/amplitude/symmetry"]
ToneGrp -->|各模块参数集| T4["square_v1:<br/>frequency/amplitude/dutyCycle"]
ToneGrp -->|各模块参数集| T5["saw_v1:<br/>frequency/amplitude/direction"]
NoiseGrp -->|各模块参数集| N1["noise_white_v1:<br/>distribution/channelCorrelation<br/>seed"]
NoiseGrp -->|各模块参数集| N2["noise_pink_v1:<br/>+ algorithm"]
NoiseGrp -->|各模块参数集| N3["noise_brown_v1:<br/>+ integratorLeakage"]
FileGrp -->|参数集| F1["wav_v1:<br/>filePath (system)<br/>loop/startOffset/readPos (tunable)<br/>durationSec/memoryMB (readonly)"]
DevGrp -->|参数集| D1["device_v1:<br/>deviceId/inputChannels (system)<br/>gainDb (tunable)"]
当前实现
所有 10 个模块已在 DSP 侧(source_*.c)完全实现,参数集合已按业务属性标记。前端库中预定义了所有 10 个 ModuleDef,用户可直接选择使用。
结论:信号类型 ↔ 参数映射在设计与实现中完全一致。
3. 三层详细架构
3.1 前端层(UI 与状态管理)
核心特征:
- 单一 UI 入口:旧 source_v1 + sourceType selector → 新 10 个 ModuleDef 独立卡片
- 动态端口:每个模块在 linkStore 中动态创建输出端口
- 参数调试:SourceTuningDialog.vue 已删除(v1.1 后迁至各模块独立 tuning 面板)
- WebSocket 交互:
set_param/set_link消息通过 socket 下发
关键文件:
frontend_vue3/src/stores/moduleLibrary.ts— 10 个 ModuleDef 定义frontend_vue3/src/stores/linkStore.ts— 链路与端口管理frontend_vue3/src/types/module.ts— 类型定义frontend_vue3/src/components/SourceTuningDialog.vue— 参数调试面板(已过时)
3.2 后端层(链路编译与参数下发)
核心特征:
- 纯透传:LinkFrameBuilder 不做模块改写,所有 source_*_v1 直接映射 typeId
- 拓扑排序:DFS 计算链路执行顺序,避免循环
- 参数生命周期:四层模型清晰分离,SetLink ≠ SetParam
- callback 驱动(待实现):移除 BackgroundService,采用 WASAPI callback 单点驱动
关键文件:
backend_csharp/Services/Link/LinkFrameBuilder.cs— 链路编译(已支持 10 个模块)backend_csharp/Services/Link/LinkPropagator.cs— 端口传播(待实现)backend_csharp/Services/Preset/PresetProfileService.cs— 参数管理backend_csharp/Services/AudioEngine/WasapiDrivenEngine.cs— callback 驱动(待实现)
3.3 DSP 层(实时处理)
核心特征:
- 10 个独立模块:source_sine_v1 / source_sweep_v1 / ... / source_device_v1
- 共享基础设施:source_common.h/c(PhaseAccumulator / DbToLinear / RandState / ParamRamp 等)
- 零独立线程:所有处理在 DSP Process 线程内完成,RT 约束完全遵守
- WAV 预读入内存:加载时 I/O,运行时 zero-copy seek;支持拖动进度条(AWE 体验)
- 设备采集 passthrough:后端 capture 线程负责采样,DSP passthrough
关键文件:
dsp_algo/modules/source/source_sine.c/h~source_device.c/h— 10 个模块dsp_algo/modules/source/source_common.c/h— 共享 APIdsp_algo/framework/module_registry_all.c— 模块注册dsp_algo/include/module_type_id.h— typeId 分配
4. 信号流与线程模型
4.1 完整信号流
前端用户选择 "sine 1000 Hz"
↓
前端创建 ModuleInstance { moduleId: "source_sine_v1", paramValues: { frequencyHz: 1000 } }
↓
前端生成 link.json,通过 set_link 消息发送
↓
后端 LinkFrameBuilder 接收
├─ 解析 moduleId: "source_sine_v1" → typeId: 0x10080010
├─ 拓扑排序
├─ 调用 BinaryFrameBuilder 生成 DSP 二进制帧
└─ P/Invoke DSPAlgo_SetLink(frameBytes)
↓
DSP 链路重建
├─ 创建 source_sine_v1 模块实例
├─ 初始化参数(frequencyHz = 1000)
└─ 注册到链路执行队列
↓
音频 callback 触发(WASAPI 48 kHz,block=64)
├─ source_sine Process: 每个采样 = sin(2π * 1000 * t)
├─ mixer / sink 依次处理
└─ 输出 64 个采样到设备
↓
每 block(1.33 ms)重复
4.2 参数更新流程
前端拖动频率滑块 → frequencyHz = 2000
↓
前端发送 set_param { instanceId: "src_sine_1", paramId: "frequencyHz", value: 2000 }
↓
后端 PresetProfileService.UpdateParam()
├─ 更新 paramStore["src_sine_1.frequencyHz"] = 2000
├─ 序列化为 DSP SetParam 帧
└─ P/Invoke DSPAlgo_SetParam(frameBytes)
↓
DSP Process 线程下一个 block 边界
├─ 从参数队列读取新值
├─ source_sine 模块 SetParam() 应用新值
└─ 下一 block 的波形频率已改为 2000 Hz(延迟 ≤ 1 block)
4.3 线程模型(Phase 8 目标)
WASAPI 驱动线程(RT)
├─ capture() → samples → capture_ringbuffer(若有 device source)
├─ DSPAlgo_Process() → 单次执行整条链路
│ ├─ source_sine_v1: 生成 sine 采样
│ ├─ mixer: 求和
│ └─ sink_v1_dsp: 写入环形缓冲
└─ render() → sink_ringbuffer → device output
后端业务线程(非 RT)
├─ WebSocket 接收 set_param 消息
├─ 写入无锁 SPSC queue
└─ (不阻塞 DSP Process)
DSP Process 线程(RT,由 callback 驱动)
└─ 每 block 边界从 queue 读取参数变更(原子操作)
5. 模块参数速查表
5.1 Tone 类(5 个)
| 参数 | sine | sweep | triangle | square | saw |
|---|---|---|---|---|---|
| frequencyHz | ✅ | startFrequencyHz / endFrequencyHz | ✅ | ✅ | ✅ |
| amplitudeDb | ✅ | ✅ | ✅ | ✅ | ✅ |
| phaseDeg | ✅ | — | ✅ | ✅ | ✅ |
| channelPhaseOffsetDeg | ✅ | ✅ | ✅ | ✅ | ✅ |
| outputChannels | ✅ | ✅ | ✅ | ✅ | ✅ |
| —— | — | — | — | — | — |
| durationSec | — | ✅ | — | — | — |
| sweepType | — | ✅ (linear/log) | — | — | — |
| mode | — | ✅ (one_shot/loop/ping_pong) | — | — | — |
| symmetry | — | — | ✅ | — | — |
| dutyCycle | — | — | — | ✅ | — |
| direction | — | — | — | — | ✅ |
5.2 Noise 类(3 个)
| 参数 | white | pink | brown |
|---|---|---|---|
| amplitudeDb | ✅ | ✅ | ✅ |
| distribution | ✅ | ✅ | ✅ |
| channelCorrelation | ✅ | ✅ | ✅ |
| seed | ✅ | ✅ | ✅ |
| outputChannels | ✅ | ✅ | ✅ |
| —— | — | — | — |
| algorithm | — | ✅ | — |
| integratorLeakage | — | — | ✅ |
5.3 文件 / 设备
| 参数 | wav | device |
|---|---|---|
| filePath (system) | ✅ | deviceId (system) |
| loop (tunable) | ✅ | inputChannels (system) |
| startOffsetSec (tunable) | ✅ | gainDb (tunable) |
| readPosSec (tunable) | ✅ | — |
| durationSec (readonly) | ✅ | — |
| sampleRate (readonly) | ✅ | — |
| memoryMB (readonly) | ✅ | — |
6. Phase 8 实施路线图
| 阶段 | 任务 | 对标文档 | 预计完成 |
|---|---|---|---|
| 1 | DSP 10 个模块单元测试(≥3 个/模块) | DSP Architecture §6 | Phase 8 Week 2 |
| 2 | 后端 callback 驱动实现(WasapiDrivenEngine.cs) | Backend Architecture §5 | Phase 8 Week 2-3 |
| 3 | 前端拆 10 个 ModuleDef 卡片、更新 tuning 面板 | Frontend Implementation | Phase 8 Week 3 |
| 4 | 端口传播与 PortInfo 同步 | 三层协同 | Phase 8 Week 4 |
| 5 | 集成测试与 mixer 场景验证 | — | Phase 8 Week 4-5 |
| 6 | 监控指标上线(MetricsAggregator) | Backend Architecture §5 | Phase 8 Week 5 |
| 7 | 文档完善与发布 | 本文档 v1.1+ | Phase 8 Week 5 |
7. 已知限制 & 未来方向
7.1 当前阶段的限制
| 限制 | 原因 | 解决方案 |
|---|---|---|
| WAV 回放不支持超大文件(>100 MB) | 内存预读设计 | Phase 9 新增 source_wav_stream_v1(流式读取) |
| 设备采集没有内置 EQ / preprocessing | 优先级低 | 通过 mixer 外部链接 EQ 模块 |
| Noise 分布算法不支持自定义 CDF | 通用性足够 | 用户需要时可扩展 distribution enum |
7.2 未来增强(Phase 9+)
- 设备采集支持多 device 并行混音
- WAV 流式回放与网络音频源
- 新增 impulse / chirp / DTMF 生成器
- Thread-safe 参数 ramp(现在仅 IIR 平滑)
8. 关键决策总结
8.1 为什么 10 个独立模块?
旧 source_gen_v1 单体参数互相条件依赖("如果 waveform=sine,则 symmetry 无意义"),维护复杂。独立模块让每个模块参数集清晰,易于测试、复用、后期扩展。
8.2 为什么 WAV 要内存预读?
DSP Process 线程预算 1.33 ms,任何磁盘 I/O(通常 10-100 ms)都导致 underrun。要支持拖动进度条,需要 O(1) seek,只能预读整段。
8.3 为什么设备采集不在 DSP 侧做?
WASAPI capture 是后端音频驱动的职责,与特定平台绑定。DSP 侧模块应与音频 API 解耦,通过无锁 queue 接收后端注入的采样。
8.4 为什么 callback 驱动而非 BackgroundService?
BackgroundService 与 callback 并行运行,双重驱动导致信号丢失 / 覆盖(旧"5kHz 随后丢失"事故)。Callback 驱动是 RT 音频领域标准做法,确保精确定时与无竞争。
9. 结论
v1.1 文档描述的 10 模块拆分方案在前后端 DSP 三层已全部落地:
- ✅ DSP 10 个模块完全实现(dsp_algo/modules/source/)
- ✅ 后端支持 10 个模块的透传与参数管理(LinkFrameBuilder / PresetProfileService)
- ✅ 前端库中预定义 10 个 ModuleDef(moduleLibrary.ts)
- 🔄 后端 callback 驱动迁移在进行中(Phase 8)
- 🔄 前端 UI 卡片化与 tuning 面板拆分在规划中(Phase 8)
本文档是 Source 模块的终极架构说明书,与 Sink 模块架构 对称。详细实现细节分别参考三层设计文档。
1. 执行摘要
1.1 三句话版
- DSP 库 100% 符合用户设想:
source_v1(passthrough)+source_gen_v1(一锅炖生成器)+sink_v1_dsp(环形缓冲)已齐全。 - 后端
LinkFrameBuilder双轨错位:强制把source_v1改写为source_gen_v1下发,同时AudioEngineService用影子槽位外部注入,两路并行导致 mixer 场景"5kHz 随后丢失"类事故。 - 方案 C 拆分落地:10 个独立 DSP source 模块(1 device + 1 wav + 3 noise + 5 tone)+ 后端纯透传 + WASAPI callback 单点驱动 + 保留现有 ModulePreset/Profile 两层预设(切 preset = 批量 SetParam,不重建 link)。
1.2 用户已确认的 10 项决策(v7.0 approved)
| 编号 | 问题 | 决策 |
|---|---|---|
| Q1 | 改造层级 | C 彻底拆分 |
| Q2 | 拆分粒度 | 10 模块(1 device + 1 wav + 3 noise + 5 tone),噪声参数按信号本身属性决定 |
| Q3 | 线程模型 | DSP 单 Process 线程 + SetParam 独立线程 + Device capture 独立线程 + Wav 预读入内存(Phase 8 单核运行 + 框架 ThreadChange-ready) |
| Q4 | 前端 UI | 独立卡片 |
| Q5 | link.json 向后兼容 | 不兼容(老 link.json 直接报错要求改造) |
| Q6 | md-style-guide 绑定 | 同意(已落地于 doc-code-sync-policy.md D0-company) |
| Q7 | SetParam vs SetLink 区分 + preset 关系 | 见 §10 + §11 详细模型 |
| Q8 | Preset/Profile 四点需求 | 见 §10.11 逐条回执(需求 ½ 已满足;需求 3 选项 A;需求 4 §10.12) |
| Q9 | wav 与 device DSP 侧独立 | wav 内嵌解码 + 内存预读;device passthrough + capture 独立线程(见 §4.6.1 / §4.6.2) |
| Q10 | 工程还原现场 | 不打包 + runtimeState + presets/profiles 目录同步(见 §10.12) |
2. 用户设想与独立设计
2.1 用户原话
dspalgo 算法库默认是接收上层的数据的,source 其实就是一个类似 alsa 输入设备,在 dsp 中就是一个信号的生成器一个文件,如果链路中有 source 并且是 DSP 中运行,那就自己产生信号;按照信号链路格式产生数据;sink 同理;PC 端产生信号注入 dspalgo 的入口,dsp 里面的 source 透传数据;对于架构来说都不用多线程。
source_module_gen.h中集成了所有的测试信号,拆分出来多个模块对用户更友好,对链路也更友好:正弦、sweep、三角、粉噪、wav、pc 的输入设备等。关于 link 中的 param 参数其实有一部分就是 preset 参数;setlink 的行为,preset 参数更新后当前 link 中的参数还是会按照激活的 preset 进行更新么?也就是说 link 中的 preset 结构是"全部的 preset 和当前激活的 preset",那一套再工作,和 setparam 中切换一套参数之间的逻辑应该怎么处理?
2.2 独立架构设计(类比 ALSA / AWE)
设计原则:
- DSP Node:带
Init / Process / SetParam的模块 - 单一职责:每个 source 模块只做一件事(sine 模块只生成正弦,noise_pink 只生成粉噪声)
- 双态 source:passthrough(外部注入)+ 多种 generator(自产信号)
- 业务层零独立线程:由 WASAPI callback 单点驱动 DSP Process
2.3 数据流(单一驱动点)
flowchart LR
subgraph PC["PC 端 · 业务层零独立线程"]
FR["File / Device Reader"]
WR["WAV Writer / WASAPI Render"]
CB["WASAPI callback<br/>唯一驱动点"]
end
subgraph DSP["DSP 库 · 纯函数"]
S1["source_ext_v1<br/>passthrough"]
SGS["source_sine_v1<br/>source_sweep_v1<br/>source_triangle_v1<br/>source_square_v1<br/>source_saw_v1"]
SGN["source_noise_white_v1<br/>source_noise_pink_v1<br/>source_noise_brown_v1"]
MIX["mixer_v2"]
GAIN["gain_v1"]
SK["sink_v1_capture<br/>ring buffer"]
end
FR -->|PCM in| CB
CB -->|ppHwIn| S1
CB -.->|"DSPAlgo_Process()"| DSP
S1 --> MIX
SGS --> MIX
SGN --> MIX
MIX --> GAIN --> SK
SK -->|ppHwOut| CB
CB -->|PCM out| WR
class FR,WR,CB xyL4;
class S1,SGS,SGN,MIX,GAIN,SK xyL3;
class PC xySgL4;
class DSP xySgL3;
3. 当前三端实现现状
3.1 前端(frontend_vue3)
- 前端只有 2 个硬编码端点模块:
source_v1(category=source)+sink_v1(category=sink) - Source 靠
sourceType ∈ {tone, noise, file, device}区分 4 种 - Sink 靠
outputMode ∈ {wav, device}+wavLogEnabled区分 - 下发 LinkConfig 里 source 永远
moduleId: "source_v1"
3.2 后端(backend_csharp)· 双轨并行
轨道 1 · LinkFrameBuilder 改写:
if (baseId == "source_v1") {
// 无论 sourceType 是什么,都改成 gen
effectiveModuleId = "source_gen_v1";
useExternal = (sourceType in {file, device}) ? 1 : 0;
}
if (baseId == "sink_v1") {
effectiveModuleId = "sink_v1_dsp";
}
轨道 2 · AudioEngineService 影子槽位(BackgroundService 线程):
_pcSourceSlots/_pcSinkSlots/_deviceInjectionSlots三套字典- 每 block:读 file / WASAPI capture →
DSPAlgo_SetSourceBuffer→Process→DSPAlgo_GetSinkBuffer→ 写 WAV / 推 WASAPI
3.3 DSP 算法库(dsp_algo)· 已就位
| typeId | 模块名 | 文件 | 行数 | 行为 |
|---|---|---|---|---|
0x10080001 |
source_v1 |
source_module.c |
86 | Passthrough,依赖 ppHwIn |
0x1008???? |
source_gen_v1 |
source_module_gen.c |
280 | 一锅炖:7 波形 + 4 噪声类型 + 2 分布 + 2 通道模式 |
0x10090004 |
sink_v1_dsp |
sink_module.c |
148 | 环形缓冲 + GetParam(PARAM_SINK_READ_PCM) 外部读取 |
source_module_gen.c 内部 SetParam 有 10 个 case,参数重叠严重。
4. 核心问题清单 + 10 模块最终清单
4.1 P0 级问题(架构错位)
| # | 问题 | 位置 | 后果 |
|---|---|---|---|
| P0.1 | 后端强制把 source_v1 改写成 source_gen_v1 |
LinkFrameBuilder.cs |
file/device 时生成器内部状态与外部注入数据并存 |
| P0.2 | 双轨路径(虚模块 + 影子槽位注入) | AudioEngineService |
mixer 场景下两路信号互相覆盖 |
4.2 P1 级问题(实现复杂度)
| # | 问题 | 后果 |
|---|---|---|
| P1.1 | 业务层独立线程(BackgroundService) | 违反"不用多线程"设想,与 WASAPI 并存有双重驱动风险 |
| P1.2 | source_gen_v1 单模块参数耦合 |
280 行 switch-case 地狱,条件有效参数多 |
4.3 P2 级问题(UX / 语义)
| # | 问题 |
|---|---|
| P2.1 | 前端 UI 无显式"生成器 vs 外部注入"区分 |
| P2.2 | 链路动态换音源成本高(stop/start 或改参数重建) |
4.4 P3 级问题(遗留)
- P3.1 "5kHz 随后丢失"事故未固化 post-mortem
4.5 拆分目标清单(用户决策)
| 类别 | 数量 | 模块 |
|---|---|---|
| 外部采集 | 1 | source_device_v1(WASAPI capture) |
| 文件回放 | 1 | source_wav_v1(WAV 文件循环/单次播放) |
| 噪声 | 3 | source_noise_white_v1 / source_noise_pink_v1 / source_noise_brown_v1 |
| Tone | 5 | source_sine_v1 / source_sweep_v1 / source_triangle_v1 / source_square_v1 / source_saw_v1 |
| 合计 | 10 | — |
4.6 10 模块最终清单 · 参数 schema
v4.0(对齐 Q9)调整说明:用户明确"DSP 侧也要支持读取 WAV",且 device 与 wav 必须是独立模块。结合 Q3 线程模型(DSP 单 Process 线程),两者的 DSP 侧实现差异如下:
source_device_v1:DSP 侧 passthrough,PC 端 WASAPI capture 独立线程通过DSPAlgo_SetSourceBuffer注入(因为 capture 数据无法提前拿到)。source_wav_v1:DSP 侧 内嵌 WAV 解码器 + 整段预读入内存(加载阶段一次性 I/O,Process 线程内零 I/O),类似 AWE 支持拖动进度条;不经过SetSourceBuffer外部注入。
4.6.1 source_device_v1(PC WASAPI capture 注入 · DSP 侧 passthrough)
| Param | 类型 | 默认 | role | 说明 |
|---|---|---|---|---|
| enable | bool | true | system | 启用 |
| deviceId | string | "" | system | WASAPI 设备 ID(前端下拉选择) |
| inputChannels | int[] | [0] | system | 从设备拉哪几个 channel |
| gainDb | float | 0 | tunable | 注入增益补偿 |
DSP 行为:Process 几乎空,依赖 ppHwIn 通过 DSPAlgo_SetSourceBuffer 外部注入。
PC 端线程:必须在后端起一个独立的 WASAPI capture 线程(MMCSS "Pro Audio" 或 Normal + lock-free SPSC ring buffer),因为声卡 API 的 capture 数据无法提前拿到。capture 线程不触发 DSPAlgo_Process(),只把采样写入 ring buffer;DSP Process 线程在 block 边界从 ring buffer 读取注入 ppHwIn。详见 §12。
4.6.2 source_wav_v1(WAV 文件回放 · DSP 侧内嵌解码 + 内存预读)
| Param | 类型 | 默认 | role | 说明 |
|---|---|---|---|---|
| enable | bool | true | system | 启用 |
| filePath | string | "" | structural | WAV 文件路径(变更触发 SetLink,见 §11.5) |
| loop | bool | true | tunable | 是否循环播放 |
| startOffsetSec | float | 0 | tunable | 起始偏移(秒) |
| gainDb | float | 0 | tunable | 播放增益 |
| readPosSec | float | 0 | tunable | 当前读取位置(秒)· 可写,支持拖动进度条 |
| durationSec | float | 0 | readonly | 总时长(秒)· 加载完成后由 DSP 填回 |
| sampleRate | int | 0 | readonly | WAV 原始采样率(加载后回填) |
| numChannels | int | 0 | readonly | WAV 通道数(加载后回填) |
| memoryMB | float | 0 | readonly | 当前占用内存(MB) |
DSP 行为:
- 加载阶段(SetLink / SetParam(filePath) 触发,可在非实时线程执行):
- 打开 WAV 文件,解码整段 PCM 到内部堆 buffer
samples[totalFrames * numChannels] - 必要时做采样率转换到系统 sampleRate
- 填回
durationSec / sampleRate / numChannels / memoryMB只读参数并广播param_bulk_update - 运行阶段(Process 线程内零 I/O):
- 按
readPos索引读取预解码样本 → 应用gainDb→ 输出 readPos += blockSize,loop=true时到末尾回绕- 收到
readPosSec写入 → 原子 seek(仅改索引,不做 I/O)
DSP 侧数据结构草图:
typedef struct {
// 运行时(Process 线程只读)
float* samples;
uint64_t totalFrames;
uint32_t wavSampleRate;
uint32_t numChannels;
// 运行时可变(SetParam 线程写 / Process 线程读 · 用原子)
_Atomic uint64_t readPos;
_Atomic float gainLin;
_Atomic int loop;
_Atomic int enable;
// 加载状态
int loaded;
char filePath[260];
} wav_module_state_t;
WAV 预读入内存的原因:
- Process 线程是 RT 线程(典型 block=64/48kHz=1.33ms 预算),任何磁盘 I/O 都会触发 underrun
- 要支持拖动进度条(Q3 明确的 AWE 体验),如果是实时流式读取,seek 后需要重新解码,延迟不可控
- WAV 通常 <100MB,整段读入内存成本可接受;超大文件场景留作
source_wav_stream_v1未来扩展 - 对齐用户原话:"对比 AWE 他是可以拖动进度条的,那一定不是随时读取 file 吧"
4.6.3 source_noise_white_v1(白噪声)
| Param | 类型 | 默认 | 说明 |
|---|---|---|---|
| enable | bool | true | 启用 |
| amplitudeDb | float | -20 | 幅度(dBFS) |
| distribution | enum | gaussian | gaussian | uniform |
| channelCorrelation | enum | uncorrelated | correlated(所有通道同源)| uncorrelated(每通道独立随机) |
| outputChannels | int | 2 | 输出通道数 |
| seed | int | 0 | 随机种子(0 = 每次不同) |
信号属性驱动参数:
- distribution:白噪声频谱平坦,但样本分布形状影响峰值因子。
gaussian更接近自然热噪声,uniform峰值固定便于测试 - channelCorrelation:立体声测试需要区分"左右同源"和"左右独立"
- seed:可复现测试必备
4.6.4 source_noise_pink_v1(粉噪声)
| Param | 类型 | 默认 | 说明 |
|---|---|---|---|
| enable | bool | true | 启用 |
| amplitudeDb | float | -20 | 幅度(dBFS) |
| distribution | enum | gaussian | gaussian | uniform |
| channelCorrelation | enum | uncorrelated | correlated | uncorrelated |
| outputChannels | int | 2 | 输出通道数 |
| seed | int | 0 | 随机种子 |
| algorithm | enum | voss_mccartney | voss_mccartney(推荐)| filter_network(FIR 近似) |
4.6.5 source_noise_brown_v1(布朗噪声 / 红噪声,-6 dB/oct)
| Param | 类型 | 默认 | 说明 |
|---|---|---|---|
| enable | bool | true | 启用 |
| amplitudeDb | float | -20 | 幅度 |
| distribution | enum | gaussian | gaussian | uniform |
| channelCorrelation | enum | uncorrelated | correlated | uncorrelated |
| outputChannels | int | 2 | 输出通道数 |
| seed | int | 0 | 随机种子 |
| integratorLeakage | float | 0.02 | 积分器泄漏系数(防止 DC 漂移,0.01-0.1 合理) |
4.6.6 source_sine_v1(正弦波)
| Param | 类型 | 默认 | 说明 |
|---|---|---|---|
| enable | bool | true | 启用 |
| frequencyHz | float | 1000 | 频率 |
| amplitudeDb | float | -6 | 幅度 |
| phaseDeg | float | 0 | 初始相位 |
| outputChannels | int | 2 | 输出通道数 |
| channelPhaseOffsetDeg | float | 0 | 各通道相位差 |
4.6.7 source_sweep_v1(扫频信号)
| Param | 类型 | 默认 | 说明 |
|---|---|---|---|
| enable | bool | true | 启用 |
| startFrequencyHz | float | 20 | 起始频率 |
| endFrequencyHz | float | 20000 | 结束频率 |
| amplitudeDb | float | -6 | 幅度 |
| durationSec | float | 10 | 扫频周期 |
| sweepType | enum | logarithmic | linear | logarithmic |
| mode | enum | one_shot | one_shot | loop | ping_pong |
| outputChannels | int | 2 | 输出通道数 |
4.6.8 source_triangle_v1(三角波)
| Param | 类型 | 默认 | 说明 |
|---|---|---|---|
| enable | bool | true | 启用 |
| frequencyHz | float | 1000 | 频率 |
| amplitudeDb | float | -6 | 幅度 |
| phaseDeg | float | 0 | 初始相位 |
| symmetry | float | 0.5 | 对称度(0.5=等腰;<0.5 左倾;>0.5 右倾) |
| outputChannels | int | 2 | 输出通道数 |
4.6.9 source_square_v1(方波)
| Param | 类型 | 默认 | 说明 |
|---|---|---|---|
| enable | bool | true | 启用 |
| frequencyHz | float | 1000 | 频率 |
| amplitudeDb | float | -6 | 幅度 |
| phaseDeg | float | 0 | 初始相位 |
| dutyCycle | float | 0.5 | 占空比(0-1) |
| outputChannels | int | 2 | 输出通道数 |
4.6.10 source_saw_v1(锯齿波)
| Param | 类型 | 默认 | 说明 |
|---|---|---|---|
| enable | bool | true | 启用 |
| frequencyHz | float | 1000 | 频率 |
| amplitudeDb | float | -6 | 幅度 |
| phaseDeg | float | 0 | 初始相位 |
| direction | enum | rising | rising(上升锯齿)| falling(下降锯齿) |
| outputChannels | int | 2 | 输出通道数 |
4.7 共享基础设施(拟抽到 source_common.h)
| 共享 API | 用途 |
|---|---|
SourceCommon_PhaseAccumulator_Tick() |
相位累加(sine/triangle/square/saw 共用) |
SourceCommon_DbToLinear(db) |
dB → 线性增益转换 |
SourceCommon_FillChannels() |
单通道信号复制到多通道 + 相位偏移 |
SourceCommon_RandState 结构 |
随机数状态(白/粉/棕噪声共用) |
SourceCommon_GaussianFromUniform() |
Box-Muller 变换 |
SourceCommon_ParamRamp_Linear() / SourceCommon_ParamRamp_IIR() |
参数平滑(§12.3.1) |
5. 三档方案对比
| 维度 | A 小修 | B 中度 | C 拆分(选定) |
|---|---|---|---|
| 解决 P0 双轨 | ✅ | ✅ | ✅ |
| 解决 P1 业务线程 | ❌ | ✅ | ✅ |
| 解决 P1 一锅炖 | ❌ | ❌ | ✅ |
| 解决 P2 UI 语义 | ❌ | 部分 | ✅ |
| 工作量(人天) | 1-2 | 5-7 | 10-14(v6 修订为 16,见 §19.4) |
| 符合用户设想度 | 40% | 75% | 100% |
用户选 C。具体 Phase 8 迁移路径与人天预算详见 Phase 8 实施计划 v1.0 §2(6 阶段 16 天)。
6. 方案 C 迁移路径(概览)
Phase 8 的 6 阶段(准备 / DSP 拆分 / 后端简化 / 前端拆分 / 监控实现 / 回归测试 / 文档合并)详细步骤见 Phase 8 实施计划 v1.0 §2。本章节仅保留骨架索引,避免与实施计划内容重复。
| 阶段 | 主题 | 人天(v7.0) |
|---|---|---|
| 1 | 准备(建分支、UnitTest_Mixer 基线) | 1 |
| 2 | DSP 拆 10 模块 + source_common | 3 |
| 3 | 后端简化 + LinkPropagator + Manifest C API | 3.5 |
| 4 | 前端拆 10 ModuleDef + PortInfo | 3 |
| 5 | 监控实现(§14 全量) | 2 |
| 6 | 回归 + 新测试(每模块 ≥ 3 测试) | 2.5 |
| 7 | 文档 + 合并 | 1 |
| 合计 | — | 16 |
7. 项目规范盲点
md-style-guide.mdv1.1:本 review 严格遵守doc-numbering.mdv1.0:本文档doc_id: D3-SYS-ARCH-001,路径D3-architecture/system/source-sink-architecture-review-v7.mddoc-code-sync-policy.mdv1.0:本文档归类为 D3 架构级(Docs-First 初稿 + 漂移容忍,§3.5)
8. Q1-Q10 决策回执(v7.0 全部 approved)
Q1 改造层级
- C — 彻底拆分 10-14 天(v7.0 调整为 16 天,见 §6)
Q2 拆分粒度
- 10 模块(1 device + 1 wav + 3 noise + 5 tone)
- 噪声参数按信号属性设计:
distribution+channelCorrelation+ 模块专属(pinkalgorithm、brownintegratorLeakage)
Q3 线程模型
- v4.0 精确化 + v6.0 ThreadChange-ready:DSP 单 Process 线程 + SetParam 独立队列 + Device capture 独立线程 + Wav 预读入内存
- Phase 8 运行时单核;框架设计 ThreadChange-ready(§12.9 + §15.8,§15.8 详情见 system-capabilities-roadmap.md §3.1)
Q4 前端 UI
- 独立卡片:UI 上每种信号独立节点,UI 面板按类别分组
Q5 link.json 向后兼容
- 不兼容:老 link.json 直接报
unknown_module_id错误,用户必须重建
Q6 md-style-guide 绑定
- 同意:已落地于
D0-company/05-standards/doc-code-sync-policy.mdv1.0
Q7 SetParam vs SetLink 区分 + preset 关系
- 见 §10 + §11 详细模型
Q8 / Q9 / Q10(v4.0 已确认)
- Q8 · Preset/Profile 四点需求:见 §10.11 / §10.12
- Q9 · wav 与 device DSP 侧独立模块:见 §4.6.1 / §4.6.2
- Q10 · 工程还原现场:见 §10.12
9. 参考文件路径速查
DSP 库
| 文件 | 拟改动 |
|---|---|
dsp_algo/modules/source/source_module.c |
重命名/重构为 source_ext.c 或废弃 |
dsp_algo/modules/source/source_module_gen.c |
删除(Q5 不兼容) |
dsp_algo/modules/source/source_common.h/c |
新建 |
dsp_algo/modules/source/source_sine.c ~ source_saw.c |
新建 5 个 |
dsp_algo/modules/source/source_noise_{white,pink,brown}.c |
新建 3 个 |
dsp_algo/modules/source/source_wav.c / source_device.c |
新建 2 个 |
dsp_algo/modules/sink/sink_module.c |
保留 |
dsp_algo/framework/module_registry_all.c |
增加 10 个注册 + manifest 字段 |
dsp_algo/include/module_type_id.h |
分配新 TypeNumId 10 个 + 预留 thread_change_v1 0x100C0001 + 3 个格式转换 0x100D0001-3 |
dsp_algo/framework/dynchain_core.h |
ModuleInstance 加 coreId / processOrder |
dsp_algo/include/module_manifest.h |
新建(§17) |
后端
| 文件 | 拟改动 |
|---|---|
backend_csharp/Services/Link/LinkFrameBuilder.cs |
移除改写逻辑 + 移除 blockSize/sampleRate 硬编码 |
backend_csharp/Services/Link/LinkPropagator.cs |
新建(§16) |
backend_csharp/Services/AudioEngineService.cs |
降级为管理器 + _blockSize=64 参数化 |
backend_csharp/Services/Audio/WasapiDrivenEngine.cs |
新建(Q3 决定后) |
backend_csharp/Services/Preset/PresetProfileService.cs |
微调(apply_link 成功后内部调 HandleSetAmbiance,DQ6 选项 A) |
backend_csharp/Services/Metrics/MetricsAggregator.cs |
新建(§14) |
backend_csharp/Services/Project/ProjectService.cs |
新建(§10.12 Save/Load Project) |
前端
| 文件 | 拟改动 |
|---|---|
frontend_vue3/src/stores/moduleLibrary.ts |
拆 10 个 ModuleDef + 加 portInfo / paramInfo.structural / runtime hint |
frontend_vue3/src/stores/linkStore.ts |
小幅调整 |
frontend_vue3/src/stores/audioEngineStore.ts |
中等调整(移除 blockSize/sampleRate 硬编码) |
frontend_vue3/src/stores/presetStore.ts |
无需改动 |
frontend_vue3/src/stores/metricsStore.ts |
新建(§14) |
frontend_vue3/src/types/module.ts |
加 ModuleInstance.coreId?: number(默认 0) |
测试 baseline
backend_csharp/test/integrationtest/UnitTest_Mixer/link/link.json(需重写)backend_csharp/test/integrationtest/UnitTest_Mixer/integrationtest/devices_mixer.json
10. 参数生命周期模型(Q7 核心)
本章是 v3.0 新增核心章节,v7.0 定稿保留全文。回答用户 Q7:"link 中的 param 和 preset 参数的关系?setlink 后 preset 参数更新,当前 link 参数还会按激活的 preset 更新吗?"
10.1 四层参数模型(基于现有代码实现)
flowchart TB
L1["层 1 · ModuleDef 默认值<br/>前端 moduleLibrary.ts<br/>出厂默认 (gainDb=0)"]
L2["层 2 · Link 中的 instance.paramValues<br/>DSPLink.modules[x].paramValues<br/>链路部署时的初始值"]
L3["层 3 · ModulePreset 快照<br/>./data/current_pro/presets/{instanceId}/{presetId}.json<br/>{presetId, name, instanceId, params}"]
L4["层 4 · 运行时 paramStore<br/>后端 ConcurrentDictionary<br/>key: {instanceId}.{paramId}"]
PROFILE["Profile 映射表<br/>./data/current_pro/profiles/{profileId}.json<br/>modulePresets: {instanceId → presetId}"]
L1 -->|"SetLink 初始化"| L2
L2 -->|"SetLink 推到后端"| L4
L3 -->|"LoadPreset 应用<br/>批量 SetParam"| L4
L4 -->|"运行时变更 SetParam"| L4
PROFILE -.->|"SetAmbiance 展开"| L3
L4 -->|"SavePreset 落盘"| L3
10.2 四层的代码证据
| 层 | 位置 | 数据结构 |
|---|---|---|
| 层 1 ModuleDef | frontend_vue3/src/types/module.ts ModuleDef.params |
模块出厂默认(所有 instance 共享) |
| 层 2 Link | ModuleInstance.paramValues: Map<paramId, value> |
每个 instance 独立,随 link 导出导入 |
| 层 3 ModulePreset | ModulePreset { presetId, name, instanceId, params } |
绑定到 instanceId 的命名快照 |
| 层 4 paramStore | backend_csharp/Services/Preset/PresetProfileService.cs _paramStore |
key = $"{instanceId}.{paramId}" |
10.3 Link 与 ModulePreset 的关系(核心答案)
关键结论:
- Link 本身不存储 ModulePreset 列表,只在
runtimeState.moduleActivePreset里记录"当前每个 instance 激活了哪个 presetId" - ModulePreset 独立存储在文件系统:
./data/current_pro/presets/{instanceId}/{presetId}.json - Profile 只是"instanceId → presetId"的映射表,不复制 param 值
export interface DSPLink {
id: string
name: string
version: string
modules: ModuleInstance[] // ← 只有 paramValues,没有 presets 列表
connections: LinkConnection[]
runtimeState?: {
activeProfileId?: string // ← 当前激活的 Profile(可选)
moduleActivePreset?: Record<string, string> // ← instanceId → presetId(轻量引用)
}
}
export interface ModuleInstance {
instanceId: string
moduleId: string
paramValues: Map<string, number | boolean | string> // ← 当前运行值
// ...
}
export interface ModulePreset {
presetId: string
name: string
instanceId: string // ← 绑定到 instance(不是 moduleType)
params: Record<string, string | number | boolean>
}
10.4 SetLink 时 param 如何初始化
后端处理(v7.0 定稿,含 DQ6 选项 A 自动展开 profile):
- 解析
link.json→ 遍历modules[].paramValues→ 逐个写入_paramStore[$"{instanceId}.{paramId}"] - 序列化 paramStore 为 DSP 二进制帧 →
DSPAlgo_SetLink下发 - 如果
runtimeState.activeProfileId非空 → 后端内部调PresetProfileService.HandleSetAmbiance(activeProfileId)(DQ6 选项 A 拍板) - 如果
runtimeState.moduleActivePreset非空但 activeProfileId 为空 → 逐个内部调HandleLoadPreset(instanceId, presetId) - 广播
apply_link_ack+set_ambiance_ack(若适用)+ 多个param_bulk_update
10.5 "link 中的 param 一部分就是 preset 参数"的正确理解
用户原话的精确含义(基于代码):
link.modules[x].paramValues= 所有参数的当前运行值- 其中部分参数碰巧等于某个 ModulePreset 的 params(因为用户刚 LoadPreset 过,paramValues 被批量覆盖了)
- 但
paramValues不是 preset 的引用,是独立的值拷贝
三种同步状态:
| 状态 | paramValues | ModulePreset | 现象 |
|---|---|---|---|
| 同步 | = Preset.params | 刚 LoadPreset | UI 显示 Preset A ✓ |
| 已修改 | ≠ Preset.params | 用户改了某参数 | UI 显示 Preset A (modified) |
| 未关联 | 独立值 | 无激活 preset | UI 显示 Custom |
10.6 "SetLink 后 preset 参数更新,当前 link 参数还按激活的 preset 更新吗"
v7.0 最终答案(DQ7 确认"不考虑"跨前端同步后):
- 同一前端内部:preset 文件更新不自动触发 paramStore 刷新,只有显式调用 LoadPreset(或批量 ApplyParams)才把 preset.params 刷到 paramStore + DSP
- 跨前端:不考虑(DQ7 用户明确,"同一条链路一个参数只由一个前端处理,多前端协作场景不存在")
如果用户期望 preset 保存后当前 link 生效 → 必须主动 LoadPreset。
10.7 "link 中 preset 结构是'全部 preset + 当前激活 preset'"的讨论
两种模型对比:
| 维度 | 用户设想(内嵌) | 当前实现(外部文件) |
|---|---|---|
| Preset 存储 | link.modules[].presets | ./presets/{instanceId}/{presetId}.json |
| link.json 体积 | 大(含所有 preset) | 小(只含当前值 + 引用) |
| Preset 共享 | 不可(绑定到 instance) | 不可(同样绑定) |
| 导出/导入 | 一键打包 | 需导出 link + preset 目录 |
| Preset 编辑 | 改 link | 改独立文件 |
| 推荐场景 | "一个 link 自包含所有方案" | 大型项目(preset 数量多) |
v7.0 决策(保留当前外部文件模型):
- 已实现且验证可用
- preset 数量多时 link.json 不会膨胀
- ModulePreset 独立 CRUD 更灵活
- Profile 层已提供"跨 module 集中管理"能力
10.8 "激活的 preset 那一套工作 vs SetParam 切换一套参数"的逻辑
两者完全等价,都是"批量 SetParam"。PresetProfileService.HandleLoadPreset 源码:
public async Task<string> HandleLoadPreset(JsonElement root) {
// ... 读 preset 文件 ...
if (paramsEl.ValueKind == JsonValueKind.Object) {
foreach (var kv in paramsEl.EnumerateObject()) {
var storeKey = $"{instanceId}.{kv.Name}";
_paramStore[storeKey] = ToStoreString(kv.Value);
_ = _dspSessionService.TryFlushToDspAsync(instanceId, kv.Name, ...);
}
_broadcastExcept(null, Json(new {
type = "param_bulk_update",
instanceId,
@params = paramsEl
}));
}
}
所以实际上只有"SetParam"路径:LoadPreset = 从文件读一组 param → 批量调 SetParam → 广播 param_bulk_update。
10.9 Profile(Ambiance)的展开语义
HandleSetAmbiance 再往上一层:
profileId→ 读 profile 文件 → 得到modulePresets: {instanceId → presetId}- 遍历每个 instance → 读对应 preset 文件 → 批量写 paramStore + 下发 DSP
- 广播
set_ambiance_ack
所以 Profile 本质是"批量展开多个 ModulePreset,再批量 SetParam",对 DSP 来说是纯 SetParam 级操作,不涉及 SetLink。
10.10 参数生命周期总结(一句话版)
DSP 只认 SetLink + SetParam 两种操作。ModulePreset 是 instance 级参数快照存储,Profile 是"instance → preset"映射表,切 preset/profile 都等价于"批量 SetParam",不触发 SetLink。
10.11 Q8 四点需求逐条回执(v7.0 全部 approved)
用户 Q8 原话:当前的模式应该也是合理的,确认一下我的需求是否能满足就可以:
- Module 当前选中的 preset 参数变化可以实时更新生效
- 参数变化不会影响 link,除了特殊会改变链路结构的参数
- 当链路 link 重构下发 SetLink 的时候,需要当前激活的 profile 的参数在链路中生效
- 保存加载工程能够体现 profile 和 preset 的关系,并且记得保存工程的时候使用的当前激活的 profile
需求 1 · 当前选中的 preset 参数变化实时生效
状态:✅ 已满足(现有实现即可)。
参数变化先走 set_param 路径 → paramStore + DSP 即时生效;"保存到 preset"只是把当前 paramStore 切片写文件。
需求 2 · 参数变化不影响 link(除非改链路结构)
状态:✅ 已满足,"结构性参数" vs "运行性参数" 的边界见 §11.5。
需求 3 · SetLink 时激活的 profile 参数在链路中生效
状态:✅ v7.0 已决策 DQ6 选项 A——后端 LinkController 在 apply_link 成功后内部调用 PresetProfileService.HandleSetAmbiance(activeProfileId)(见 §10.4 步骤 3)。
sequenceDiagram
participant FE as 前端
participant LC as LinkController
participant PS as paramStore
participant PP as PresetProfileService
participant DSP as DSP Algo
FE->>LC: apply_link { link, runtimeState }
LC->>LC: 解析 link.modules → 写 paramStore 初始值
LC->>DSP: DSPAlgo_SetLink (二进制帧)
alt runtimeState.activeProfileId 存在
LC->>PP: HandleSetAmbiance(activeProfileId)
PP->>PP: 读 profile → 遍历 modulePresets
loop 每个 instance
PP->>PS: 覆盖 paramStore[instanceId.*] = preset.params
PP->>DSP: 批量 SetParam
end
PP-->>FE: 广播 set_ambiance_ack + 多个 param_bulk_update
else runtimeState.moduleActivePreset 存在(未激活 profile)
loop 每个 (instanceId → presetId)
PP->>PS: HandleLoadPreset
PP->>DSP: 批量 SetParam
end
end
LC-->>FE: apply_link_ack
需求 4 · 保存加载工程体现 profile+preset 关系
状态:✅ v7.0 DQ5 决策"Phase 8 实现基本骨架"(后端落地 SaveProject/LoadProject 核心流程,UI 完善留 Phase 9),详见 §10.12。
10.12 工程还原现场(Save/Load Project · Q10 对齐)
用户 Q10 原话:可以不打包,但是能够还原现场就可以,要求在 Q8 中有提到。
"还原现场"的定义(对齐 Q8.4):用户重新打开工程时,整套 link 拓扑 + 参数 + 激活的 profile + 每个 instance 当前选中的 preset 完整复现,DSP 跑出的信号应当与关闭工程前完全一致。
10.12.1 工程数据边界
| 组成 | 当前位置 | 还原作用 |
|---|---|---|
| Link 拓扑 + 初始参数 | ./data/current_pro/link.json |
重建 modules + connections + paramValues |
| 当前运行快照 | 后端 paramStore(内存) | 保存时刷回 link.modules[].paramValues |
| 激活 profile | link.runtimeState.activeProfileId |
加载时 SetLink 后自动展开 |
| 每 instance 激活 preset | link.runtimeState.moduleActivePreset |
加载时 per-instance preset 覆盖 |
| ModulePreset 定义 | ./data/current_pro/presets/{instanceId}/{presetId}.json |
必须能读到 |
| AmbianceProfile 定义 | ./data/current_pro/profiles/{profileId}.json |
必须能读到 |
10.12.2 Save Project 流程
sequenceDiagram
participant UI as 前端
participant BE as 后端 ProjectService (新增)
participant PS as paramStore
participant FS as 文件系统
UI->>BE: save_project { projectName }
BE->>PS: 读取当前全部 paramStore
BE->>BE: 回刷到 link.modules[].paramValues
BE->>BE: 读取 activeProfileId + moduleActivePreset
BE->>BE: 写入 link.runtimeState
BE->>FS: 写 {projectDir}/link.json
BE->>FS: 拷贝 ./data/current_pro/presets/ → {projectDir}/presets/
BE->>FS: 拷贝 ./data/current_pro/profiles/ → {projectDir}/profiles/
BE-->>UI: save_project_ack { path }
关键点:
- paramStore 是唯一真源:保存时必须先把 paramStore →
modules[].paramValues - runtimeState 必填:
activeProfileId和moduleActivePreset是还原现场的关键 - presets/profiles 目录同步拷贝:不打包成 zip(用户 Q10 "可以不打包")
10.12.3 Load Project 流程
sequenceDiagram
participant UI as 前端
participant BE as 后端 ProjectService
participant LC as LinkController
participant PP as PresetProfileService
participant FS as 文件系统
UI->>BE: load_project { projectPath }
BE->>FS: 读 {projectDir}/link.json
BE->>FS: 拷贝 {projectDir}/presets/ → ./data/current_pro/presets/
BE->>FS: 拷贝 {projectDir}/profiles/ → ./data/current_pro/profiles/
BE->>LC: apply_link(link, runtimeState)
LC->>LC: 初始化 paramStore = link.modules[].paramValues
LC->>LC: DSPAlgo_SetLink
alt runtimeState.activeProfileId 存在
LC->>PP: HandleSetAmbiance(activeProfileId)
else runtimeState.moduleActivePreset 存在
loop 每个 (instanceId → presetId)
LC->>PP: HandleLoadPreset(instanceId, presetId)
end
end
BE-->>UI: load_project_ack { link, runtimeState }
UI->>UI: 恢复 UI 激活状态
10.12.4 还原现场正确性检查清单
| 检查项 | 方法 |
|---|---|
| DSP 实际运行参数 = 保存前一致 | 比对 paramStore dump |
| 前端 UI 高亮 profile = 保存前 | 比对 runtimeState.activeProfileId |
| 每个 instance 高亮的 preset = 保存前 | 比对 runtimeState.moduleActivePreset |
| Preset 文件齐全 | 遍历 moduleActivePreset.values() |
| Profile 文件齐全 | 若 activeProfileId 非空,确认文件存在 |
10.12.5 边界情况处理
| 场景 | 处理 |
|---|---|
| 工程目录缺 preset 文件 | 加载时警告 + 降级为纯 paramValues |
| 工程目录缺 profile 文件但有 moduleActivePreset | 忽略 profileId,按 moduleActivePreset 展开 |
| runtimeState 字段缺失 | 兼容老工程,按 paramValues 直接应用 |
| preset/profile 冲突 | 工程目录优先覆盖 current_pro |
10.12.6 Phase 8 交付范围(DQ5 确认)
DQ5 决策:Phase 8 实现基本骨架:
- ✅ 后端
ProjectService.SaveProject / LoadProject核心 API - ✅ WS 协议
save_project/load_project - ✅ 基本 UI 按钮(菜单"保存工程"/"加载工程")
- ❌ UI 完善(工程列表、最近打开、缩略图等留 Phase 9)
11. SetParam vs SetLink 决策矩阵
11.1 核心判据
SetLink 触发条件(必须重建链路):
- 新增/删除模块实例(instance)
- 新增/删除连接(connection)
- 改模块类型(例如
source_sine_v1→source_sweep_v1) - 改端口规格(channel 数 / sample rate / 数据类型)
- Wire 拓扑变化(重新 propagate)
SetParam 触发条件(只改运行值):
- 改参数值(gainDb / frequencyHz / mute 等)
- 切 ModulePreset(批量 SetParam)
- 切 AmbianceProfile(更大规模批量 SetParam)
- Mixer 矩阵内部元素变化(mixer_v2 的
mixSrc[row][col])
11.2 决策表
| 变更类型 | 示例 | 走 SetLink 还是 SetParam |
|---|---|---|
| 加一个 source 节点 | 在链路里加 source_sine_v1 | SetLink |
| 删一个 sink 节点 | 移除 sink_v1 | SetLink |
| 改模块类型 | source_sine_v1 → source_sweep_v1 | SetLink |
| 改连线 | source → gain → sink 变成 source → mixer → gain → sink | SetLink |
| 改参数 | gain.gainDb: 0 → -6 | SetParam |
| 改 sine 频率 | frequencyHz: 1000 → 5000 | SetParam |
| 切 ModulePreset | LoadPreset("sine_1", "preset_A") | 批量 SetParam |
| 切 Profile | SetAmbiance("live_show") | 批量 SetParam |
| 改 mixer 矩阵某个元素 | mixer.mixSrc[2][3] = 0.5 | SetParam |
| 改 mixer 矩阵维度 | mixer 输入通道数 2 → 4 | SetLink |
| 改 sourceType 下拉 | tone → file | SetLink(拆分后不同 moduleId) |
| 开关 enable | enable: true → false | SetParam |
| 新增 preset | SavePreset | 无 DSP 动作 |
| 新建 link | 从模板新建链路 | SetLink |
11.3 mode 约束(chain_builder / tuning / test_verify)
| 模式 | 可改参数 | 可改结构 |
|---|---|---|
| chain_builder | ✅ 全开 | ✅ 全开 |
| tuning | ✅ role=tunable | ❌ 锁定 |
| test_verify | ✅ role=system 允许 | ❌ 锁定 |
切 preset 受 mode 约束:tuning 模式下,若 preset 包含 role=system 参数(如 enable),这些参数被忽略不下发,只下发 tunable 参数。
11.4 协议速查
| 协议 | 用途 | WS type | payload |
|---|---|---|---|
| SetLink | 重建链路 | apply_link |
{ link: DSPLink } |
| SetParam 单个 | 改一个参数 | set_param |
{ instanceId, paramId, value } |
| 批量 SetParam | 批量改参数 | apply_params |
{ instanceId, params: {...} } |
| LoadPreset | 加载 ModulePreset | load_preset |
{ instanceId, presetId } |
| SetAmbiance | 切 Profile | set_ambiance |
{ profileId } |
| SavePreset | 保存 ModulePreset | save_preset |
{ instanceId, presetId, name, params } |
| SaveProject | 保存整个工程 | save_project |
{ projectName, projectPath } |
| LoadProject | 加载整个工程 | load_project |
{ projectPath } |
11.5 结构性参数 vs 运行性参数分类(v4.0 新增)
为了精确实现 Q8 需求 2,需要把所有参数按"是否影响链路结构"分为两大类。
11.5.1 结构性参数(structural · 触发 SetLink)
| 参数类型 | 示例模块 · 参数 | 为什么是结构性 |
|---|---|---|
| 文件路径 | source_wav_v1.filePath |
DSP 重新加载整个文件到内存 buffer |
| 设备 ID | source_device_v1.deviceId |
切换 WASAPI capture 源 |
| 输出通道数 | 任意模块 · outputChannels |
端口规格变化 |
| Mixer 矩阵维度 | mixer_v2.numInputs / numOutputs |
矩阵尺寸变化 |
| 采样率 | 系统级 · sampleRate |
全链路影响 |
| 输入通道选择 | source_device_v1.inputChannels |
capture 线程改 channel mask |
| ThreadChange 目标核 | thread_change_v1.targetCoreId |
链路分段重绑(Phase 9+) |
| ThreadChange buffer | thread_change_v1.bufferFrames |
跨线程 ring buffer 重建 |
约定:前端 UI 显式提示 "修改此参数会重建链路(~10ms 中断)",避免误操作。
11.5.2 运行性参数(tunable/system · 走 SetParam)
| 参数类型 | 示例 |
|---|---|
| 增益 | gain.gainDb / source_*.amplitudeDb / source_*.gainDb |
| 频率 | source_sine_v1.frequencyHz |
| 相位 | phaseDeg / channelPhaseOffsetDeg |
| 波形形状 | symmetry / dutyCycle / direction |
| 噪声属性 | distribution / channelCorrelation / seed / integratorLeakage |
| 扫频内部 | startFrequencyHz / endFrequencyHz / durationSec / sweepType / mode |
| WAV 回放 | loop / startOffsetSec / gainDb / readPosSec(拖动进度条) |
| Enable | 任意模块 · enable |
| Mixer 矩阵元素 | mixer_v2.mixSrc[row][col] |
11.5.3 前端强制校验
moduleLibrary.ts 的每个 ParamDef 应当新增 structural: boolean 字段(默认 false = 运行性),UI 的参数变更 handler 按此分派:
function handleParamChange(instanceId: string, paramId: string, value: any) {
const def = getParamDef(instanceId, paramId)
if (def.structural) {
requestRebuildLink({ instanceId, paramId, value })
} else {
send({ type: 'set_param', instanceId, paramId, value })
}
}
12. DSP 单 Process 线程模型(v7.0 核心架构决策)
用户 Q3 反馈(汇总 v4 + v5 + v6):音频的产生者只有一个线程;Process 优先级最高;set/get 在另外的线程;所有参数变更在 Process 内执行避免并发 + 参数平滑避免 pop;Device capture 必须独立线程(声卡 API 数据无法提前拿到);Wav 预读入内存(对齐 AWE 拖动进度条体验)。Phase 8 单核运行但框架 ThreadChange-ready。
§12 核心语义:
- DSP Process 必须且只能在一个线程(最高优先级,时序真源)
- SetParam / GetParam 在另一个线程(最低优先级,不打断 Process 写入)
- 自产信号源(tone/noise/sweep)完全在 Process 线程内算(零 I/O)
- Device capture 必须独立线程(声卡 API 数据不可提前拿到)
- Wav 输入不需要独立线程(一次性预读入内存)
12.1 线程拓扑
flowchart TB
subgraph RT["RT 优先级 · MMCSS Pro Audio<br/>唯一的 DSP_Process 驱动点"]
PROC["Process 线程<br/>- 驱动 DSPAlgo_Process()<br/>- 自产 tone/noise/sweep<br/>- 索引 wav 预读 samples<br/>- 消费 capture ring buffer<br/>- 推 WASAPI render buffer"]
end
subgraph CAP["Normal/AboveNormal 优先级"]
CAP_TH["Device Capture 线程<br/>- WASAPI capture 回调<br/>- 写 lock-free SPSC ring buffer"]
end
subgraph SET["Normal 优先级 · ASP.NET 请求线程池"]
SET_TH["SetParam 线程(池)<br/>- 接 WS set_param / apply_params<br/>- 写 lock-free param queue"]
GET_TH["GetParam 线程<br/>- 从 paramStore 读快照"]
end
subgraph LOAD["非 RT · 后台线程"]
LOAD_TH["Wav 加载线程<br/>- 收到 filePath 触发<br/>- 解码整段 WAV 到堆 buffer"]
end
CAP_TH -->|lock-free SPSC| PROC
SET_TH -->|lock-free MPSC| PROC
LOAD_TH -->|原子 swap samples*| PROC
PROC -->|samples_readPos| PROC
GET_TH -.->|读 paramStore| SET_TH
12.2 各线程职责精确定义
| 线程 | 优先级 | 数量 | 职责 | 禁止事项 |
|---|---|---|---|---|
| DSP Process 线程 | MMCSS "Pro Audio" | 1 严格唯一 | 按 block 节拍调 DSPAlgo_Process(),驱动所有模块,读 capture ring buffer,写 render buffer |
禁 malloc、禁锁、禁磁盘 I/O、禁系统调用阻塞 |
| Device Capture 线程 | Normal/MMCSS | 每个 source_device_v1 实例一条 |
WASAPI capture 回调,采样写入 lock-free SPSC ring buffer | 禁调用 DSPAlgo_Process() |
| SetParam 线程(池) | Normal/BelowNormal(最低) | 共享 Kestrel 池 | 接收 WS set_param,写入 paramStore,投递到 MPSC 队列 | 禁直接调 DSPAlgo_SetParam |
| Wav 加载线程 | Normal(后台任务) | 每次加载一条 | 解码整段 WAV 到新 buffer,原子 swap | 禁在加载中调 SetParam(filePath) |
| GetParam 线程 | Normal | 共享 Kestrel 池 | 只读 paramStore 快照 | 禁写 paramStore |
12.3 SetParam 无锁投递机制(v5.0 澄清 · v7.0 approved)
§12.3 四条核心约束:
- 所有参数变更最终在 Process 线程内执行(包括
set_param/apply_params/load_preset/set_ambiance)。SetParam 线程永远不直接调DSPAlgo_SetParam。 - SetParam 线程优先级最低(Normal 甚至 BelowNormal)。防止反过来被 Process 高优先级打断导致参数写入中途数据异常。
- Process 在 block 边界 drain 队列 → 批量应用参数变更 → 调 Process 本体。参数变更应用时机原子(block 间无并发)。
- 参数平滑(淡入淡出):大步长参数走斜坡平滑(ramp over N samples),避免 pop/click。
sequenceDiagram
participant FE as 前端
participant KS as Kestrel 线程池
participant PQ as Lock-free MPSC Queue
participant PT as Process 线程
participant DSP as DSP Algo
FE->>KS: WS set_param { instanceId, paramId, value }
KS->>KS: 写 paramStore[instanceId.paramId] = value
KS->>PQ: Enqueue(ParamUpdate)
KS-->>FE: ack(立即返回)
loop 每个 block 边界(典型 1.33ms @ 48kHz/64)
PT->>PQ: Drain 当前队列
PT->>DSP: 批量 DSPAlgo_SetParam
PT->>DSP: DSPAlgo_Process(block)
end
数据结构(C# 端):
readonly Channel<ParamUpdate> _paramQueue = Channel.CreateUnbounded<ParamUpdate>(
new UnboundedChannelOptions {
SingleWriter = false, // 多线程写
SingleReader = true // Process 独占读
});
struct ParamUpdate {
public int moduleInstanceIdx;
public int paramIdx;
public ValueUnion value;
public ulong seqNo;
}
12.3.1 参数平滑(淡入淡出)策略(DQ9 approved)
v7.0 分级策略:
| 参数类别 | 平滑策略 | 典型时间常数 | 例子 |
|---|---|---|---|
| 幅度类(gain/amp) | 线性 ramp 或 IIR 低通 | 30 ms(实时调音) / 100 ms(SetAmbiance 批量) | gain.gainDb / amplitudeDb |
| 频率类 | IIR 低通(避免 pitch 跳跃) | 20-100 ms | source_sine_v1.frequencyHz |
| 系数类(EQ/filter) | block 内线性插值到新系数 | 1-3 block | eq.coefficients |
| 开关/枚举类 | 不平滑,瞬时切换 | 0 | enable / mode / direction |
| 结构性类 | 不适用(走 SetLink) | — | outputChannels / filePath |
实现策略:采用方案 C 混合——framework 提供 smoother 工具(SourceCommon_ParamRamp_Linear/IIR),模块按需嵌入。实时调音 30ms 线性 ramp;SetAmbiance 100ms 较慢 ramp。
12.4 Device Capture 独立线程 · lock-free ring buffer
sequenceDiagram
participant DRV as 声卡驱动
participant CT as Capture 线程
participant RB as Lock-free SPSC Ring Buffer
participant PT as Process 线程
participant DSP as DSP Algo
loop WASAPI 硬件节拍
DRV->>CT: AudioDataAvailable(samples)
CT->>RB: Write(samples)(lock-free)
end
loop Process 每个 block
PT->>RB: Read(blockSize)(lock-free;若不足补 0)
PT->>DSP: DSPAlgo_SetSourceBuffer(instanceId, samples)
PT->>DSP: DSPAlgo_Process(block)
end
Buffer 容量(DQ3 approved):ringCapacity = blockSize × 8(约 10.6ms @ 48k/64)。
Underrun 处理策略:默认 A 补零(听感短时静音)。
12.5 Wav 预读入内存 · 加载 vs 运行
核心语义:source_wav_v1 生命周期只有两阶段 I/O:
- 加载阶段(一次性):非 RT 线程解码整段 WAV → 新 buffer → 原子 swap 到模块
- 运行阶段(每个 block):Process 线程按
readPos索引读 samples → 零 I/O
stateDiagram-v2
[*] --> Idle: Init
Idle --> Loading: SetParam(filePath)
Loading --> Loading: 后台解码
Loading --> Ready: 解码完成,原子 swap samples*
Ready --> Playing: enable=true
Playing --> Playing: Process 按 readPos 索引读
Playing --> Seeking: SetParam(readPosSec)
Seeking --> Playing: 原子改 readPos(无 I/O)
Playing --> Ready: enable=false
Ready --> Loading: SetParam(filePath) 变更
原子 swap 实现(避免 Process 线程读半加载 buffer):
// 加载线程
float* newBuffer = malloc(totalFrames * numChannels * sizeof(float));
// ... 解码 ...
atomic_store(&wav->totalFrames, newTotalFrames); // 先写大小
atomic_exchange(&wav->samples, newBuffer); // 后 swap 指针(release)
// Process 线程
float* cur = atomic_load(&wav->samples); // acquire
uint64_t n = atomic_load(&wav->totalFrames);
if (cur && n > 0) { /* 索引读取 */ }
内存上限(DQ2 approved):MAX_WAV_MEMORY_MB = 512,超限拒绝加载。
12.6 实时性预算 · 按 port 规格动态计算
用户 Q3 原话:"这个 process 最终产生的数据是否能满足最终的输出设备的消耗速度?"
答案:只要 每个 block 的 DSP 计算时间 < block 时长,就能满足。
预算公式(v6.0 修正为动态,不再硬编码 64/48k):
典型取值:
| blockSize | sampleRate | blockDuration |
|---|---|---|
| 32 | 48000 | 0.67 ms |
| 64 | 48000 | 1.33 ms |
| 128 | 48000 | 2.67 ms |
| 64 | 96000 | 0.67 ms |
| 64 | 192000 | 0.33 ms |
典型场景测量(Phase 8 准入标准):
| 场景 | 模块数 | 期望 Process 耗时 | 余量 |
|---|---|---|---|
| 1 source + gain + sink(baseline) | 3 | < 0.1 ms | > 90% |
| 4 source + mixer + gain + sink(典型) | 7 | < 0.3 ms | > 75% |
| 10 source + mixer + 3 EQ + compressor + sink(极限) | 16 | < 0.8 ms | > 40% |
CPU 负载告警(DQ4 approved):黄色 >70%,红色 >90%。
12.7 与旧方案(BackgroundService)对比
| 维度 | 旧(BackgroundService) | 新(DSP 单 Process 线程) |
|---|---|---|
| Process 驱动源 | .NET Timer | WASAPI render callback(硬件时钟) |
| Process 线程数 | 1 个 BackgroundService 线程 | 1 个 MMCSS Pro Audio 线程 |
| Process 精度 | ±1-10ms 抖动 | ±0.1ms(硬件) |
| SetParam 线程 | BackgroundService 内部同步调 | 独立池(不阻塞 Process) |
| Device capture | BackgroundService 拉取 | 独立线程 + ring buffer |
| Wav 输入 | BackgroundService 磁盘读 | 预读入内存,Process 零 I/O |
| 双轨风险 | ✅ 存在 | ❌ 消除(单点驱动) |
| 用户设想贴合度 | ~60% | ~95% |
12.8 Q3 推荐方案(v4.0)
采纳"DSP 单 Process 线程 + SetParam 无锁队列 + Device capture 独立线程 + Wav 预读入内存"模型。具体等价于 §12.3-v3.0 的"混合方案(Producer + Callback)",但 Producer 拆为两类(capture 必要 + wav 一次性),Callback = WASAPI render callback 作为 Process 触发器兼 Process 线程本身。
12.9 Q3 最终决策(v6.0 + v7.0 approved · ThreadChange-ready)
用户 v6.0 反馈:Q3:先按照 v4 处理,但是要按照 ThreadChange 做框架设计,避免 Phase 9 做的时候又要推翻 Phase 8 重新来。这个需求不是当前任务够不够跑的问题,是之后的主流架构都需要支持多 DSP,多 thread 协作问题。
v7.0 Q3 最终决策(DQ10 approved):Phase 8 按 v4/v5 单核单 Process 线程实施(运行时只跑 1 核),但框架设计必须 ThreadChange-ready——Phase 9 接入 ThreadChange 时无需重构,只是"启用更多 core 线程 + 新增 thread_change_v1 实例"。
Phase 8 运行时行为:
- 单个 DSP Process 线程(core 0,MMCSS Pro Audio)
- SetParam 独立线程池 + lock-free MPSC 队列
- Device capture 独立线程 + lock-free SPSC ring buffer
- Wav 预读入内存(加载阶段一次性 I/O)
Phase 8 框架设计的 ThreadChange-ready 要求(详细兼容性清单见 phase8-implementation-plan.md §4 + system-capabilities-roadmap.md §3.1):
-
DSPAlgo_Process()签名加int coreId参数(v7 阶段总传 0) -
ModuleInstance加coreId/processOrder字段(默认 0) -
link_graph使用List<CoreSegment>分段数据结构(v7 阶段总共 1 段) -
Scheduler接口支持多线程调度(v7 阶段只注册 1 个 core 线程) -
DSPAlgo_SetLink二进制帧预留coreIdArray字段 -
module_type_id.h预留0x100C0001 thread_change_v1类型 ID - 监控指标扩展 per-core 维度(v7 阶段只上报 core 0)
如果 Phase 8 实施中发现无法兼容(v6.0 用户授权的 fallback):直接按多核模型重构,人天从 16 扩到 20-22。
13. 决策清单汇总(v7.0 全部 approved)
13.1 已确认需求(Q4.0 回执)
| 原决策项 | v4.0 状态 | 实现位置 |
|---|---|---|
| Q8 · ModulePreset 自动同步 + 四点需求 | ✅ 已确认 | §10.11 + §10.12 |
| Q9 · wav vs device 是否合并 | ✅ 保持独立 + DSP 侧真正不同 | §4.6.1 / §4.6.2 |
| Q10 · link.json 是否打包 preset | ✅ 不打包但能还原现场 | §10.12 |
13.2 剩余 Q 延伸问题的 v7.0 最终答案
Q3 · 线程模型最终采纳方案
v7.0 approved(DQ1 + DQ10):§12.8 推荐方案 + ThreadChange-ready(DQ10)。
Q8 · 延伸 preset 跨前端同步
v7.0 approved(DQ7):不考虑(用户明确"同一条链路一个参数只由一个前端处理,多前端协作场景不存在")。
13.3 DQ1-DQ25 人类确认清单(v7.0 approved)
| 编号 | 议题 | v7.0 最终决策 | 备注 |
|---|---|---|---|
| DQ1 | §12.8 线程模型推荐方案 | ✅ 采纳(单 Process + SetParam 队列 + capture 独立 + wav 预读) | 默认建议 |
| DQ2 | wav 模块最大内存上限(MAX_WAV_MEMORY_MB) | ✅ 512 MB | 默认建议 |
| DQ3 | capture ring buffer 容量 | ✅ N=8(约 10.6ms @ 48k/64) | 默认建议 |
| DQ4 | Process CPU 负载告警阈值 | ✅ 黄 70% / 红 90% | 默认建议 |
| DQ5 | SaveProject / LoadProject Phase 8 范围 | ✅ Phase 8 实现基本骨架(后端核心 + 基础 UI),UI 完善留 Phase 9 | 用户明确 |
| DQ6 | SetLink 自动展开 activeProfile | ✅ 选项 A · 后端内部调 SetAmbiance | 用户明确 |
| DQ7 | preset 跨前端自动同步 | ❌ 不考虑(同一参数单前端处理) | 用户明确 |
| DQ8 | §14 监控指标范围 | ✅ 全量(整体 CPU + per-module + 内存 + underrun/overrun) | 用户明确 |
| DQ9 | §12.3.1 参数平滑策略 | ✅ 幅度类 30ms ramp / SetAmbiance 批量 100ms | 默认建议 |
| DQ10 | §15 ThreadChange Phase 8 实施方式 | ✅ 运行时单核 + 框架 ThreadChange-ready | 默认建议 |
| DQ11 | ThreadChange 是否用 SetThreadAffinityMask 硬绑定 | ✅ 硬绑定(MMCSS Pro Audio + affinity) | 默认建议 |
| DQ12 | thread_change_v1.bufferFrames 默认 |
✅ 128(= 2×blockSize@64) | 默认建议 |
| DQ13 | Phase 8 是否实现全链路 totalLatency 累计 | ✅ 是(MetricsAggregator 顺便算) | 默认建议 |
| DQ14 | link 状态机前端展示 | ✅ 简单状态徽章(RUNNING / FADEOUT 两态) | 默认建议 |
| DQ15 | Phase 8 是否支持多链路 | ❌ 单链路 clean tear-down,多链路 Phase 10 | 默认建议 |
| DQ16 | Phase 8 错误处理最小范围 | ✅ underrun/overrun + fatal 广播,crash 恢复 Phase 9 | 默认建议 |
| DQ17 | 新 10 source 模块单测 | ✅ 每模块 ≥ 3 测试(基础 / 参数边界 / 多通道) | 默认建议 |
| DQ18 | link.json 预留 schemaVersion | ✅ "schemaVersion": "8.0" 头部字段 |
默认建议 |
| DQ19 | mode 三端一致性范围 | ✅ 前端 readonly + 后端校验,DSP 侧校验 Phase 9 | 默认建议 |
| DQ20 | Phase 8 实时 WS log 回流 | ❌ 文件 log + 前端"打开日志"按钮,实时 WS 回流 Phase 9 | 默认建议 |
| DQ21 | resampler/channel_remap/format_convert 三模块 Phase 8 实施 | ❌ 只预留 TypeNumId + stub,Phase 9 按需求实现 | 默认建议 |
| DQ22 | i18n / 离线模式 / 插件扩展 Phase 8 考虑 | ❌ 全部 Phase 10+ | 默认建议 |
| DQ23 | §16 PortInfo propagate Phase 8 实施 | ✅ pass-through(单一格式链路) | 默认建议 |
| DQ24 | §17 Module Manifest Phase 8 实施 | ✅ static metadata + runtime metrics 导出 | 默认建议 |
| DQ25 | Phase 8 人天预算 12 → 16 | ✅ 接受 16 天(承担 PortInfo + Manifest + 完整监控) | 默认建议 |
14. 附录 · 相关文档
- Phase 8 实施计划 v1.0 — 基于本 review DQ25 全确认版的 Core Loop + 8 Demo + 16 天预算 + 三端改动清单
- 系统能力路线图 v1.0 — Phase 9+ 十议题 A-J + ThreadChange 详细设计 + 格式转换模块规范
- D0-company/05-standards/md-style-guide.md v1.1(本文档遵守)
- D0-company/05-standards/doc-numbering.md v1.0(本文档编号规则)
- D0-company/05-standards/doc-code-sync-policy.md v1.0(本文档分层归属:D3 架构级)
版本历史
| 版本 | 日期 | 状态 | 变更 |
|---|---|---|---|
| v1.0 | 2026-05-07 | superseded | 首次发布 · 诊断双轨错位 + 三档方案 |
| v2.0 | 2026-05-07 | superseded | 加入方案 C 拆分建议 + md-style-guide 合规 |
| v3.0 | 2026-05-07 | superseded | 用户决策回执 + 10 模块最终清单 + §10 参数生命周期 + §11 SetParam/SetLink 决策矩阵 |
| v4.0 | 2026-05-07 | superseded | Q3 线程模型精确化(§12 重写为 DSP 单 Process 线程)+ Q8 四点需求回执 + §10.12 工程还原现场 + §11.5 结构性/运行性参数分类 + DQ1-DQ7 |
| v5.0 | 2026-05-08 | superseded | §12.3 SetParam 优先级最低 + 参数平滑三级策略 + §14 性能监控指标体系 + §15 ThreadChange 设计 + DQ8-DQ12 |
| v6.0 | 2026-05-08 | superseded | Q3 ThreadChange-ready + §16 PortInfo propagate + §17 Module Manifest + §18 系统级能力盘点 A-J + §19 Phase 8 最小闭环 + DQ13-DQ25 |
| v7.0 | 2026-05-08 | approved | DQ1-DQ25 全员人类确认(DQ5/DQ6/DQ7/DQ8 用户明确,其余默认采纳)· 文档拆分为三份(本主文档瘦身保留架构讨论,§14/§15/§16/§17 实施细节拆至 phase8-implementation-plan.md,§18 + ThreadChange 详细设计拆至 system-capabilities-roadmap.md)· 从 work-cline/docs/ 迁入 D3-architecture/system/(进 MkDocs 公共导航)· status: review → approved |
Source / Sink 架构 Review · v7.0 · 2026-05-08 · © Xisound AlgoDepartment · doc_id: D3-SYS-ARCH-001
对本 review 有意见?请提 PR 或联系 algo-team@xisound.com。