跳转至
DRAFT

XiStudio · Sink 模块架构 v1.1

关键改动(v1.1 vs v7.0)

v7.0 是架构审查文档,重点讨论改造决策与 Phase 8 实施计划。v1.1 聚焦于前后端 DSP 三层一体的架构设计,采用"架构目标 vs 当前实现"双栏格式,直接参考三层设计文档:


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.md v1.1 + doc-numbering.md v1.0 + doc-code-sync-policy.md v1.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 三句话版

  1. DSP 库 100% 符合用户设想source_v1(passthrough)+ source_gen_v1(一锅炖生成器)+ sink_v1_dsp(环形缓冲)已齐全。
  2. 后端 LinkFrameBuilder 双轨错位:强制把 source_v1 改写为 source_gen_v1 下发,同时 AudioEngineService 用影子槽位外部注入,两路并行导致 mixer 场景"5kHz 随后丢失"类事故。
  3. 方案 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_SetSourceBufferProcessDSPAlgo_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 行为

  1. 加载阶段(SetLink / SetParam(filePath) 触发,可在非实时线程执行):
  2. 打开 WAV 文件,解码整段 PCM 到内部堆 buffer samples[totalFrames * numChannels]
  3. 必要时做采样率转换到系统 sampleRate
  4. 填回 durationSec / sampleRate / numChannels / memoryMB 只读参数并广播 param_bulk_update
  5. 运行阶段(Process 线程内零 I/O):
  6. readPos 索引读取预解码样本 → 应用 gainDb → 输出
  7. readPos += blockSizeloop=true 时到末尾回绕
  8. 收到 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.md v1.1:本 review 严格遵守
  • doc-numbering.md v1.0:本文档 doc_id: D3-SYS-ARCH-001,路径 D3-architecture/system/source-sink-architecture-review-v7.md
  • doc-code-sync-policy.md v1.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 + 模块专属(pink algorithm、brown integratorLeakage

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.md v1.0
  • 见 §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 ModuleInstancecoreId / 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}"

关键结论

  1. Link 本身不存储 ModulePreset 列表,只在 runtimeState.moduleActivePreset 里记录"当前每个 instance 激活了哪个 presetId"
  2. ModulePreset 独立存储在文件系统:./data/current_pro/presets/{instanceId}/{presetId}.json
  3. 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>
}

后端处理(v7.0 定稿,含 DQ6 选项 A 自动展开 profile):

  1. 解析 link.json → 遍历 modules[].paramValues → 逐个写入 _paramStore[$"{instanceId}.{paramId}"]
  2. 序列化 paramStore 为 DSP 二进制帧 → DSPAlgo_SetLink 下发
  3. 如果 runtimeState.activeProfileId 非空 → 后端内部调 PresetProfileService.HandleSetAmbiance(activeProfileId)(DQ6 选项 A 拍板)
  4. 如果 runtimeState.moduleActivePreset 非空但 activeProfileId 为空 → 逐个内部调 HandleLoadPreset(instanceId, presetId)
  5. 广播 apply_link_ack + set_ambiance_ack(若适用)+ 多个 param_bulk_update

用户原话的精确含义(基于代码):

  • 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

v7.0 最终答案(DQ7 确认"不考虑"跨前端同步后):

  • 同一前端内部:preset 文件更新不自动触发 paramStore 刷新,只有显式调用 LoadPreset(或批量 ApplyParams)才把 preset.params 刷到 paramStore + DSP
  • 跨前端不考虑(DQ7 用户明确,"同一条链路一个参数只由一个前端处理,多前端协作场景不存在")

如果用户期望 preset 保存后当前 link 生效 → 必须主动 LoadPreset。

两种模型对比

维度 用户设想(内嵌) 当前实现(外部文件)
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 原话:当前的模式应该也是合理的,确认一下我的需求是否能满足就可以:

  1. Module 当前选中的 preset 参数变化可以实时更新生效
  2. 参数变化不会影响 link,除了特殊会改变链路结构的参数
  3. 当链路 link 重构下发 SetLink 的时候,需要当前激活的 profile 的参数在链路中生效
  4. 保存加载工程能够体现 profile 和 preset 的关系,并且记得保存工程的时候使用的当前激活的 profile

需求 1 · 当前选中的 preset 参数变化实时生效

状态:✅ 已满足(现有实现即可)。

参数变化先走 set_param 路径 → paramStore + DSP 即时生效;"保存到 preset"只是把当前 paramStore 切片写文件。

状态:✅ 已满足"结构性参数" vs "运行性参数" 的边界见 §11.5。

状态:✅ 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 必填activeProfileIdmoduleActivePreset 是还原现场的关键
  • 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.1 核心判据

SetLink 触发条件(必须重建链路):

  • 新增/删除模块实例(instance)
  • 新增/删除连接(connection)
  • 改模块类型(例如 source_sine_v1source_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,需要把所有参数按"是否影响链路结构"分为两大类。

参数类型 示例模块 · 参数 为什么是结构性
文件路径 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 核心语义:

  1. DSP Process 必须且只能在一个线程(最高优先级,时序真源)
  2. SetParam / GetParam 在另一个线程最低优先级,不打断 Process 写入)
  3. 自产信号源(tone/noise/sweep)完全在 Process 线程内算(零 I/O)
  4. Device capture 必须独立线程(声卡 API 数据不可提前拿到)
  5. 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 四条核心约束:

  1. 所有参数变更最终在 Process 线程内执行(包括 set_param / apply_params / load_preset / set_ambiance)。SetParam 线程永远不直接调 DSPAlgo_SetParam
  2. SetParam 线程优先级最低(Normal 甚至 BelowNormal)。防止反过来被 Process 高优先级打断导致参数写入中途数据异常。
  3. Process 在 block 边界 drain 队列 → 批量应用参数变更 → 调 Process 本体。参数变更应用时机原子(block 间无并发)。
  4. 参数平滑(淡入淡出):大步长参数走斜坡平滑(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:

  1. 加载阶段(一次性):非 RT 线程解码整段 WAV → 新 buffer → 原子 swap 到模块
  2. 运行阶段(每个 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):

blockDurationUs = blockSize / sampleRate × 1,000,000

预算 = blockDurationUs × 安全系数(80%)

典型取值:

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)
  • ModuleInstancecoreId / 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. 附录 · 相关文档


版本历史

版本 日期 状态 变更
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。