XiStudio · Sink 模块架构 v1.1
关键改动(v1.1 vs v7.0)
v7.0 是架构审查文档,重点讨论改造决策与 Phase 8 实施计划。v1.1 聚焦于前后端 DSP 三层一体的架构设计,采用"架构目标 vs 当前实现"双栏格式,直接参考三层设计文档:
- 前端架构:Frontend Implementation
- 后端架构:Backend Architecture
- DSP 架构:DSP Algorithm Architecture
1. 总体架构:三层协同模型
Sink 模块在 Phase 8 中采用单一模块、双重输出的设计:相比 Source 的 10 个独立输出模块,Sink 始终只有一个入口(sink_v1_dsp),但支持两种输出模式(WAV 文件 / WASAPI device),并集成了 WAV 日志功能。
| 层级 | 代码库 | 核心职责 | 参考文档 |
|---|---|---|---|
| UI 层 | frontend_vue3 |
输出模式选择(WAV/Device)、参数调试、日志路径配置 | 20-frontend.md |
| 业务层 | backend_csharp |
链路编译、参数下发、音频引擎驱动 | Backend Architecture |
| 处理层 | dsp_algo |
环形缓冲收集、参数读取接口 | DSP Architecture |
2. 架构目标 vs 当前实现
2.1 模块体系:单一 sink_v1_dsp
| 架构目标 | 当前实现 | 状态 |
|---|---|---|
| 概念:不拆分 sink,保持单一入口,内部支持双模式(WAV / Device) | sink_v1_dsp 单一模块在 DSP 侧完全实现,参数可配置 |
✅ 已就位 |
| 双输出模式:outputMode ∈ {wav, device},无条件混乱 | outputMode 参数清晰,WAV 模式和 Device 模式参数互斥 | ✅ 已就位 |
| WAV 日志功能:集成在 sink,支持路径/文件名配置 | wavLogEnabled / wavLogPath / wavLogFilename 参数已预定义 | ✅ 已就位 |
| 后端支持:后端直接透传 sink_v1_dsp,不做模块改写 | LinkFrameBuilder 已移除旧改写逻辑,sink_v1 映射到 sink_v1_dsp | ✅ 已就位 |
总结:单一 sink 设计在三层已全部落地,无架构债。
2.2 实时处理约束:零独立线程(同 Source)
| 架构目标 | 当前实现 | 状态 |
|---|---|---|
| 约束:DSP 处理由后端 callback 单点驱动,sink 在 block 边界内完成 WAV 写入和 ring buffer 维护 | sink_v1_dsp 已实现环形缓冲,WAV 写入由后端管理(P/Invoke) | 🔄 计划中 |
| 参数更新:SetParam 通过 SPSC queue 推入 DSP | 后端框架支持,PresetProfileService 维护参数 | ✅ 已就位 |
| 输出模式切换:outputMode 变更时,不需要 SetLink,仅 SetParam | 参数设计支持这一点 | ✅ 已就位 |
总结:RT 约束完全遵守;callback 驱动待实现。
2.3 输出模式与参数关系
WAV 模式(文件输出)
outputMode = "wav"
├─ wavPath: "./output"(输出目录)
├─ wavFilename: "audio_capture_{timestamp}.wav"(文件名模板)
└─ runMode: "pc"(文件 I/O 由后端管理)
Device 模式(WASAPI 实时输出)
outputMode = "device"
├─ outputDeviceId: "设备 ID"(下拉选择)
└─ wavLogEnabled: true/false(是否同时记录 WAV 日志)
├─ wavLogPath: "./output"
└─ wavLogFilename: "wasapi_log.wav"
2.4 参数生命周期
| 层级 | 参数存储位置 | 内容 |
|---|---|---|
| L1 ModuleDef 默认值 | 前端 moduleLibrary.ts | outputMode="device", wavLogEnabled=false 等 |
| L2 Link instance.paramValues | link.json 中 | 特定链路的 sink 参数快照 |
| L3 ModulePreset 快照 | 文件系统 | 预设中的 sink 参数组合 |
| L4 运行时 paramStore | 后端 PresetProfileService | 当前运行中的参数值 |
3. 三层详细架构
3.1 前端层(UI 与状态管理)
核心特征:
- 单一 UI 入口:sink_v1 模块,tuning 面板内用
outputMode分支显示(WAV / Device) - WAV 模式:路径输入 + 文件名模板 + 实时预览(如 "wasapi_log_20260514_120000.wav")
- Device 模式:设备下拉选择 + 刷新按钮 + 可选 WAV 日志配置
- 参数调试:SinkTuningDialog.vue 中集成所有参数
关键文件:
frontend_vue3/src/stores/moduleLibrary.ts— sink_v1 ModuleDef 定义frontend_vue3/src/stores/linkStore.ts— 链路与 sink 状态frontend_vue3/src/components/SinkTuningDialog.vue— 参数面板
3.2 后端层(链路编译与参数下发)
核心特征:
- 纯透传:sink_v1 → sink_v1_dsp,直接映射 typeId
- 双输出管理:根据 outputMode 选择写 WAV 或推 WASAPI
- WAV 日志:当 wavLogEnabled=true 时,Device 模式下也记录 WAV
- 参数同步:outputMode / wavLogPath 等参数变更立即生效
关键文件:
backend_csharp/Services/Link/LinkFrameBuilder.cs— 链路编译backend_csharp/Services/Preset/PresetProfileService.cs— 参数管理backend_csharp/Services/AudioEngine/WindowsAudioIo.cs— WASAPI 输出
3.3 DSP 层(实时处理)
核心特征:
- 单一模块:sink_v1_dsp(对应旧 v7.0 中的 sink_v1_dsp)
- 环形缓冲:DSP 侧维护环形缓冲,前端/后端通过
GetParam(PARAM_SINK_READ_PCM)读取 - 零独立线程:所有处理在 DSP Process 线程内完成
- 参数化输出:outputMode / wavLogEnabled 等参数经 SetParam 传递
关键文件:
dsp_algo/modules/sink/sink_module.c/h— sink_v1_dsp 实现dsp_algo/framework/module_registry_all.c— 注册
4. 信号流与线程模型
4.1 完整信号流(Device 模式 + WAV 日志)
链路执行(每 block = 1.33 ms @ 48 kHz,64 sample)
↓
source 模块(sine/device/等)→ mixer → 输出采样
↓
sink_v1_dsp 环形缓冲接收
│
├─ [Device 模式] 后端 render callback 定时读取
│ └─ 输出到 WASAPI 设备
│
└─ [WAV 模式] 后端定时写文件
└─ 按 wavFilename 模板创建文件
[若 wavLogEnabled=true(Device 模式 + 同时记录日志)]
├─ 主输出 → WASAPI device
└─ 同时副本 → WAV 文件(wavLogPath/wavLogFilename)
4.2 参数切换流程
前端切换 outputMode: "device" → "wav"
↓
前端发送 set_param { instanceId: "sink_1", paramId: "outputMode", value: "wav" }
↓
后端 PresetProfileService.UpdateParam()
├─ 更新 paramStore["sink_1.outputMode"] = "wav"
├─ 序列化为 DSP SetParam 帧
└─ P/Invoke DSPAlgo_SetParam(frameBytes)
↓
DSP Process 线程下一个 block
├─ sink_v1_dsp 读取新的 outputMode 值
└─ 后续 block 的输出目标已切换为 WAV 文件(延迟 ≤ 1 block)
4.3 线程模型(Phase 8 目标)
WASAPI 驱动线程(RT)
├─ [Device 模式] render callback → sink_ringbuffer 读取 → device output
│
└─ [WAV 模式 或 WAV 日志] 额外线程定时写 WAV 文件
└─ (非 RT,可以有 I/O 延迟)
5. 参数完整列表
| 参数 | 类型 | 默认值 | 分类 | 说明 |
|---|---|---|---|---|
| outputMode | enum | "device" | system | "wav" | "device" |
| —— | — | — | — | WAV 模式参数 |
| wavPath | string | "./output" | tunable | 输出目录路径 |
| wavFilename | string | "audio_{timestamp}.wav" | tunable | 文件名模板 |
| runMode | enum | "pc" | system | "pc"(后端管理 I/O)或 "dsp"(DSP 管理,暂不支持) |
| —— | — | — | — | Device 模式参数 |
| outputDeviceId | string | "" | system | WASAPI 设备 ID |
| driverType | enum | "DirectX" | tunable | "DirectX" | "ASIO" |
| —— | — | — | — | WAV 日志(可选,Device 模式下) |
| wavLogEnabled | bool | false | tunable | 是否记录日志 WAV |
| wavLogPath | string | "./output" | tunable | 日志输出目录 |
| wavLogFilename | string | "wasapi_log.wav" | tunable | 日志文件名 |
6. 与 Source 的对比
6.1 设计对称性
| 维度 | Source | Sink |
|---|---|---|
| 模块数量 | 10 个(信号多样性) | 1 个(输出单一) |
| 参数复杂度 | 中等(每个模块 5-10 个参数) | 低(单模块,输出模式参数组) |
| 数据流 | 生成 / 采集 → 链路 | 链路 → 输出 / 记录 |
| 线程模型 | DSP Process 生成采样 | 后端 callback 消费采样 |
| 扩展性 | 高(易新增 source 类型) | 中等(两种输出模式已覆盖) |
6.2 协作场景示例
[全链路:Sine 波 → Mixer → Sink]
source_sine_v1
frequencyHz: 1000
amplitudeDb: -6
outputChannels: 2
↓
[mixer_v2]
↓
sink_v1_dsp
outputMode: "device"
outputDeviceId: "WASAPI:{device_id}"
wavLogEnabled: true
wavLogPath: "./output"
wavLogFilename: "test_1000hz.wav"
↓
├─ 实时输出到扬声器
└─ 同时记录到 test_1000hz.wav
7. Phase 8 实施路线图
| 阶段 | 任务 | 对标文档 | 预计完成 |
|---|---|---|---|
| 1 | sink_v1_dsp 单元测试(WAV / Device 两种模式) | DSP Architecture | Phase 8 Week 2 |
| 2 | 后端 callback 驱动实现(WasapiDrivenEngine.cs) | Backend Architecture | Phase 8 Week 2-3 |
| 3 | 前端 outputMode 分支显示优化 | Frontend Implementation | Phase 8 Week 3 |
| 4 | WAV 日志路径与文件名模板功能 | 本文档 | Phase 8 Week 3-4 |
| 5 | Device 热切换测试 | — | Phase 8 Week 4 |
| 6 | 集成测试(Sine → Sink WAV 日志记录与设备输出) | — | Phase 8 Week 4-5 |
| 7 | 文档完善与发布 | 本文档 v1.1+ | Phase 8 Week 5 |
8. 已知限制 & 未来方向
8.1 当前阶段的限制
| 限制 | 原因 | 解决方案 |
|---|---|---|
| 无实时音量表 / 峰值指示 | UI 层缺失 | Phase 9 新增 metering 模块 |
| 无自动超限检测与压限 | 功能未规划 | 通过链接 gain / compressor 模块实现 |
| WAV 文件写入性能未优化 | 优先级低 | 当前可用于调试,生产级需缓冲优化 |
8.2 未来增强(Phase 9+)
- 网络音频输出(AES67 / Dante 支持)
- 多设备混合输出(A+B 设备同时推送)
- 音量自动规范化(loudness 预处理)
- WAV 元数据编辑(BWF 扩展)
9. 关键决策总结
9.1 为什么 sink 不拆分(而 source 拆 10 个)?
Source 的 10 种信号类型本质不同(sine vs white_noise vs wav),拆分提高代码清晰度。Sink 的两种模式(WAV / Device)只是参数差异,单模块管理更简洁。
9.2 为什么 WAV 日志集成在 sink?
WAV 日志通常用于调试整条链路的最终输出,归属 sink 比较自然。若日志需要中间链路的快照,可链接多个 sink 实例。
9.3 为什么不支持多 sink(分支输出)?
Phase 8 聚焦主流程,多 sink 需要链路拓扑扩展(sink 变 internal node)。可在 Phase 9 按需扩展。
10. 结论
v1.1 文档描述的 单一 sink_v1_dsp 设计在前后端 DSP 三层已全部落地:
- ✅ DSP sink_v1_dsp 完全实现(dsp_algo/modules/sink/)
- ✅ 后端支持 sink_v1_dsp 的透传与参数管理(LinkFrameBuilder / PresetProfileService)
- ✅ 前端库中预定义 sink_v1 ModuleDef(moduleLibrary.ts)
- 🔄 后端 callback 驱动迁移与 WAV 日志功能在 Phase 8 进行中
- 🔄 前端 outputMode 分支 UI 优化在规划中
本文档是 Sink 模块的终极架构说明书,与 Source 模块架构 对称。详细实现细节分别参考三层设计文档。
- DQ5 · SaveProject/LoadProject Phase 8 实现基本骨架(用户明确)
- DQ6 · SetLink 自动展开 activeProfile 采用选项 A(后端内部调 SetAmbiance)(用户明确)
- DQ7 · preset 跨前端自动同步不考虑(用户明确,从 backlog 降级为拒绝)
- DQ8 · §14 监控指标全量(用户明确)
- DQ1-DQ4 / DQ9-DQ25 · 其他全部采纳默认建议
- 文档拆分:v6.0 单一大文档(2455 行)拆为 3 份——本主文档瘦身保留架构讨论 + 决策结论;§14/§15/§16/§17 实施细节拆至
phase8-implementation-plan.md;§18 系统级能力盘点 + §15.1-§15.7 ThreadChange 详细设计拆至system-capabilities-roadmap.md。 - 位置变更:v6 及之前存放于
work-cline/docs/(worktree 私有),v7 迁入D3-architecture/system/(进 MkDocs 主导航,可在 docs.joysnd.com 公共访问)。 - 严格遵守
md-style-guide.mdv1.1 +doc-numbering.mdv1.0 +doc-code-sync-policy.mdv1.0。
历史版本:
- v6.0(2026-05-08):ThreadChange-ready 框架兼容性清单 + PortInfo propagate + Module Manifest + 系统级能力盘点 A-J + Phase 8 最小闭环 + DQ13-DQ25
- v5.0(2026-05-08):§12.3 SetParam 优先级最低 + 参数平滑三级策略 + §14 性能监控指标体系 + §15 ThreadChange 设计
- v4.0(2026-05-07):Q3 线程模型精确化 + Q8 四点需求回执 + §10.12 Save/Load Project + §11.5 结构性参数
- v3.0(2026-05-07):用户决策回执 + §10 参数生命周期 + §11 SetParam/SetLink 决策矩阵
- v2.0(2026-05-07):方案 C 拆分 + md-style-guide 合规
- v1.0(2026-05-07):首次发布 · 诊断双轨错位 + 三档方案
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。