Bundle
dsh-vst3-studio
DeepSeek Harness (dsh) VST3 音频工作室插件:通过 nvst3-host(Node N-API,官方 Steinberg VST3 SDK)让 AI 以通用方式操作任意 VST3 插件——扫描、按名检索/批量修改参数、存取音色状态、管理音色资产库,把 MIDI 文件或音符渲染成音频,串联多插件效果链,并对结果做客观音频分析以形成闭环。宿主运行在隔离子进程中,插件崩溃不影响 dsh。
- Source
- lwy0v0
- Updated
- Updated yesterday
Readme
# dsh-vst3-studio
DeepSeek Harness(dsh)的 **VST3 音频工作室插件**:让 AI 直接操作本机的 VST3 插件来**捏音色**、把 **MIDI 渲染成音频**、串**效果链**,并用客观指标**自己验证**结果。
底层用 [`nvst3-host`](https://github.com/Henley04/nvst3-host)(Node N-API 原生模块,封装官方 Steinberg VST3 SDK 3.8,MIT)。所有原生操作跑在**隔离子进程**里:插件崩溃只死子进程,dsh 不受影响,下一次调用自动重启。
## 它能做什么
| 能力 | 工具 | 实测 |
| --- | --- | --- |
| 发现本机插件 | `vst3_scan` | 本机扫到 114 条 → 去重过滤后得到干净的可用清单 |
| 看懂一个插件 | `vst3_inspect` | Serum 2:2623 个参数 / 32 个分组 / 0 进 2 出 |
| **按名字捏音色** | `vst3_params` `vst3_set_params` | `[OSC A] A Level`、`[Env 1] Env 1 Attack`(支持 `"250ms"`、`"Off"`、`mode:'plain'`) |
| **MIDI → 音频** | `vst3_render` | 4 音符 MIDI → Serum 2 → WAV,音高实测精确命中 C4/E4/G4/C5 |
| 效果链处理 | `vst3_render` + `inputWav` | 渲染结果 → OTT → 新 WAV(peak 0.284 → 0.572) |
| **音色资产库(带版本历史)** | `vst3_patch` | 同名再存自动升 v1/v2 且保留旧版;两版渲染 peak 0.724 vs 0.056 |
| **批量变体 A/B 对照** | `vst3_variants` | 一次渲 3 个 Attack/音量变体,RMS 0.01170 / 0.01114 / 0.00175 并排对照 |
| **预设库索引** | `vst3_presets` | 本机索引到 **824+ 条**:627 个 `.SerumPreset` + **90 个压缩包内预设**(152MB `.SerumPack` 不解包即可读)+ 107 个 `.fxp` + FL/厂商目录 |
| **预设来源可增长** | `vst3_presets` | 内置常用位置表(19 个来源);用户说"我的预设放在 X"→ `addSource` 持久化,立刻可索引 |
| **标准 VST3 预设互通** | `vst3_patch` | `.vstpreset` 导出 → 导入 → 17 个参数一致 → 渲染出声(peak 1.4830) |
| **导出到项目文件夹** | `vst3_patch` | `exportBundle` 一套三件(`.vstpreset` + `.recipe.md` + `.patch.json`);`exportAll` 批量导出整个库(实测 6/6) |
| **自我验证闭环** | `vst3_analyze` | 音高/RMS/峰值/削波/音头/频谱重心,音高精度 0.0005% |
| MIDI 资产管理 | `vst3_midi` | 生成/解析 `.mid`(音名或 MIDI 号,支持 tempo map) |
## 安装
```sh
# 在包含本目录的路径下执行;web 是当前 GUI 用的 profile
dsh plugin --profile web add ./dsh-vst3-studio-0.1.6.tgz
```
安装后**重启 dsh**,会话里会出现 11 个 `vst3_*` 工具。详细步骤与验证方法见 [`INSTALL.md`](./INSTALL.md)。
> 依赖 `nvst3-host` 自带 win32-x64 / darwin-arm64 / linux-x64 / linux-arm64 预编译二进制,**不需要编译工具链、不需要 VST3 SDK**。
## 快速上手
一次典型的"捏音色 → 出音频 → 验证"流程:
```
1. vst3_env # 确认宿主可用
2. vst3_scan { query: "serum" } # 找到插件路径
3. vst3_inspect { path: ".../Serum2.vst3" } # 看有哪些分组和参数量
4. vst3_params { path: "...", unit: "OSC A", query: "level" } # 找到要改的参数名
5. vst3_midi { action: "create", output: "riff.mid",
notes: [{"pitch":"C4","start":0,"dur":0.35}, ...] }
6. vst3_render { path: ".../Serum2.vst3", midiFile: "riff.mid",
params: [{"name":"A Level","value":0.9},
{"name":"Env 1 Attack","value":"50ms"}] }
→ 返回 WAV 路径 + 峰值/削波/音高分析
7. vst3_analyze { file: "<上一步的 outputWav>" } # 客观复核
8. vst3_patch { action: "save", name: "my-lead", ... } # 满意就沉淀成音色
```
**为什么第 7 步重要**:AI 没有耳朵,只能靠指标判断。渲染结果里已经带了分析,但怀疑时单独复核一遍更稳。
## 工具一览
### `vst3_env`
环境自检:原生模块版本、宿主子进程状态(pid / 启动次数 / 崩溃次数)、生效配置、安全边界。**开工前先调一次**。
### `vst3_scan`
扫描 VST3 插件。默认扫平台默认目录,可用 `dirs` 指定。已自动:
- 过滤掉 `Component Controller Class`(加载它不会出声,是最常见的踩坑点);
- 合并「bundle 路径」与「bundle 内二进制路径」的重复项(上游会为同一个插件返回 4 条);
- 按名称/厂商过滤(`query`)、只看乐器(`instrumentOnly`)。结果缓存 5 分钟。
### `vst3_inspect`
插件详情:基本信息、音频/事件总线通道数、自报延迟与尾音、参数总数与分组、预设 program 列表。**捏音色前必调**:它告诉你这是乐器还是效果器、尾音多长(影响渲染留多长尾巴)。
### `vst3_params`
按 `query` 关键词或 `unit` 分组检索参数,分页返回 `id / name / unit / value(归一化) / plain(可读值) / readOnly`。合成器动辄上千参数(Serum 2 有 2623 个),**必须配合关键词使用**。
### `vst3_set_params`
按名字或 id 批量改参数:
- 数字默认按**归一化 0..1**(VST3 原生口径);
- `mode: "plain"` 按插件显示的物理值;
- 字符串走插件解析,如 `"250ms"`、`"Off"`、`"440Hz"`。
名字支持唯一子串匹配,**命中多个时会报错并列出候选**——这是刻意的:猜错旋钮会让整个调音过程跑偏,而模型会以为改动生效了。
> 本工具只改当前进程内实例,用于试探;要出音频请在 `vst3_render` 的 `chain[].params` 里给,要沉淀用 `vst3_patch`。
### `vst3_patch`
音色资产库与预设互通(**带版本历史**)。动作:
| 动作 | 作用 |
| --- | --- |
| `save` / `capture` | 把"插件 + 参数"沉淀成音色。同名再存自动升版本并保留旧版。`capture` 是带血统的 save:`name` 可省略,自动采用**插件自报的 presetName**;加 `fresh: true` 则用**全新实例**取干净基线(从零新建的起点) |
| `load` / `list` / `delete` | 载入(回读与默认值差异)、列出、删除(含全部版本,可按 `version` 回退) |
| `provenance` | **不加载插件**,直接读状态文件里的段与血统(这音色哪来的、哪个插件版本、schema 几) |
| `recipe` | 导出**人可读的参数配方**(Markdown 表格:参数名 + 界面显示值) |
| `exportVstpreset` | 只导标准 `.vstpreset`(给其它 DAW) |
| `exportBundle` | 一次导出**一套三件**:`.vstpreset` + `.recipe.md` + `.patch.json`,默认落在**项目文件夹的 `vst3-exports/`** |
| `exportAll` | 把整个音色库批量导出成一套套文件(交付 / 备份 / 换机器) |
| `importVstpreset` | 导入别人的 `.vstpreset`,**先让插件真加载验证**再入库 |
### `vst3_midi`
`create` 把音符数组写成 `.mid`(音名 `"C4"` 或 MIDI 号,velocity 支持 `0..1` 或 `1..127`);`inspect` 解析并返回音符/速度/时长。**旋律资产**:同一段 MIDI 换不同音色反复对比,比每次重新描述音符可靠。
### `vst3_variants` ⭐
批量变体 + A/B 对照:给一个基础音色(`path`/`stateFile`/`params`)和一组变体(每个只写要覆盖的参数),逐个渲染成独立 WAV,并返回关键指标对照表(峰值/响度/削波/频谱重心/音高)。
```
vst3_variants {
path: ".../Serum2.vst3", stateFile: "<vst3_patch 存的音色>",
notes: [{"pitch":"C4","start":0,"dur":1.0}],
variants: [
{ "label": "attack-5ms", "params": [{"name":"Env 1 Attack","value":"5ms"}] },
{ "label": "attack-200ms", "params": [{"name":"Env 1 Attack","value":"200ms"}] },
{ "label": "quiet", "params": [{"name":"A Level","value":0.1}] }
]
}
```
**AI 没有耳朵,指标只能帮你排除明显问题(削波、全静音、音高不对),不能判断好不好听**——所以它会把每个变体的 WAV 路径列出来让人试听定夺。
### `vst3_presets` ⭐
预设索引与 program 接口。**它让 AI 能"看见"你的音色库**:
- `index` —— 扫描并统计:Serum 的 `.SerumPreset` 与 **`.SerumPack` 压缩包内部**(不解包整包)、标准 VST3 预设目录里的 `.vstpreset`、旧式 `.fxp`/`.fxb`、FL Studio 的 `.fst`。返回分类/标签/schema 版本分布。
- `search` —— 多关键词 AND 检索(匹配名称/作者/描述/分类/标签/插件名),可按标签组合、分类、作者、格式过滤,可只看尚未收编的。
- `info` —— 某个预设的完整元数据 + **能否被程序化加载的准确判断**。
- `sources` / `addSource` / `removeSource` —— 预设来源管理(内置表 + 用户运行时添加并持久化)。
- `programs` / `select` —— 插件官方 program 列表枚举与切换(含"名字是否有信息量""是否实现 IProgramListData""这次切换是否真的改变了音色")。
> ⚠️ **索引 ≠ 能加载。** 详见下面「预设的加载与保存」一节。
### `vst3_render` ⭐
核心工具,两种模式:
- **合成**:`chain` 第一级放乐器,给 `notes` 或 `midiFile`;
- **处理**:给 `inputWav`,`chain` 放效果器。
每级可带 `params`、`stateFile`、`bypass`。渲染时自动:按插件实际总线配置通道、参数在激活前设好并冲刷、用 `getLatency()` 补偿延迟、按自报尾音 + 能量衰减决定尾部长度、越界补静音。返回 WAV 路径 + 峰值/RMS/削波 + 完整分析 + **可操作的提示**(全静音、削波、参数未生效、连奏重叠都会明说)。
### `vst3_analyze`
对任意 WAV 做客观分析:时长、峰值(dBFS)、响度(RMS)、削波样本数、直流偏置、能量包络、音头时间点、逐段音高(带置信度)、频谱重心(明亮度)。
## 两条捏音色的路线
### 路线 A:从现成预设出发,改几个参数,存成新预设(推荐日常用)
```
1. vst3_presets { action: "search", query: "reese bass", tags: ["Wavetable","Mono"] }
→ AI 从你的库里挑 3-5 个候选,把 source 路径给你
2. 【你手动一次】在插件界面里载入中意的那个(Serum 的预设文件读不了,只有 GUI 能加载)
3. vst3_patch { action: "capture", name: "my-reese-v1", path: "<插件路径>" }
→ 收编成资产,自动带上血统(原预设名/作者/版本)
4. vst3_params / vst3_set_params → 按名字微调(A Level、Filter Cutoff…)
5. vst3_render → 渲染试听 + 客观指标
6. vst3_patch { action: "capture", name: "my-reese-v2", ... } → 存成 v2(v1 保留可回退)
7. vst3_patch { action: "exportBundle", name: "my-reese-v2" } → 导出到项目文件夹
```
`.vstpreset` 还能**反向**走:别人给的 `.vstpreset` 用 `importVstpreset` 直接进来(会先加载验证再入库)。
### 路线 B:从零新建一个音色
```
1. vst3_patch { action: "capture", name: "serum-init", path: "...", fresh: true }
→ fresh 用**全新实例**取状态,拿到插件的干净初始基线(实测 2183 字节 = 纯 Init)
2. vst3_params { query: "level" } / { unit: "OSC A" } → 摸清有哪些振荡器/包络/滤波器参数
3. vst3_set_params,或直接在 vst3_render 的 chain[].params 里给参数 → 一轮轮试
4. vst3_variants { variants: [...] } → 一次渲多个变体 + 指标对照表 + 多个 WAV 供试听
5. vst3_analyze → 确认音高/响度/明亮度符合预期
6. vst3_patch { action: "capture", name: "my-new-lead" } → 沉淀
```
**AI 没有耳朵**,两条路线都靠"渲染 + 客观指标 + 你试听"收敛;`vst3_variants` 就是为这个设计的。
## 预设库扫描:内置位置表 + 可增长
`vst3_presets { action: "sources" }` 列出所有扫描位置及各自找到多少文件。内置表覆盖:
| 类别 | 位置 |
| --- | --- |
| Serum 2 | **从 `Serum2Prefs.json` 读出的实际路径**、`Documents\Xfer\Serum 2 Presets`(含 `Presets\User`) |
| Serum 1 | `Documents\Xfer\Serum Presets`(`.fxp`,可用 Serum 2 自带的旧版导入迁移) |
| 标准 VST3 | `Documents\VST3 Presets`、`%APPDATA%\VST3 Presets`、`%ProgramData%\VST3 Presets`(macOS / Linux 对应位置同理) |
| FL Studio | `Documents\Image-Line\FL Studio\Presets\Plugin presets`、`...\Downloads\Plugin presets` |
| 厂商目录 | Native Instruments / reFX / LennarDigital / u-he / Arturia / Spectrasonics / iZotope / Vital 等在 `Documents` 下的目录 |
**用户说"我的预设放在 X"时**直接加进来,**持久化、重启仍有效**:
```
vst3_presets { action: "addSource", dir: "D:\\我的音色库", label: "我的 Serum 自建预设" }
vst3_presets { action: "removeSource", dir: "D:\\我的音色库" }
```
(配置里的 `presetDirs` 也能加,但那个要改配置 + 重启;`addSource` 是给运行时用的。)
## 导出的产物放哪
**默认落在项目文件夹下的 `vst3-exports/`**(工作区路径由 dsh 会话环境推导,推导不出就退回 dsh 进程当前目录),**具体放哪由 AI 决定**:
```
vst3_patch { action: "exportBundle", name: "my-lead" } # → <项目>/vst3-exports/
vst3_patch { action: "exportBundle", name: "my-lead", output: "D:/交付/音色" } # → 指定目录(绝对或相对)
vst3_patch { action: "exportAll" } # 整个音色库批量导出
```
每次导出一套文件,**主产物是插件自家格式**(适配过的插件),另外附通用格式与配方:
| 文件 | 用途 |
| --- | --- |
| `<名字>.SerumPreset` / `<名字>.fxp` | **插件自家预设格式(主产物)**:Serum 拖进自己的预设库就能在浏览器里看到;Nexus 是 `.fxp`。没适配的插件则没有这个文件,只有下面的 `.vstpreset` |
| `<名字>.vstpreset` | **标准 VST3 预设**,Cubase / Studio One / Reaper 可导入(插件自家浏览器**不认**这种文件) |
| `<名字>.recipe.md` | **人可读参数配方**(参数名 + 界面显示值),照着设就能在插件 GUI 里复现 |
| `<名字>.patch.json` | 清单:血统、schema 版本、全部非默认参数、文件位置 |
只要预设文件用 `vst3_patch { action: "exportPreset", name: "..." }` 单独导出即可(同样默认落到项目目录)。
> 为什么仍然保留 `recipe.md`:`.SerumPreset` 能**写出**(见下节),但读不进来,所以"把一个第三方 Serum 预设搬到别处"这件事,参数配方依然是最稳的路径。
## 插件专门适配层
不同 VST3 插件的"预设机制"差异巨大,而这**直接决定 AI 该怎么干活**。所以有一个适配器注册表,每个插件一个适配器,**读与写都要按插件自己的格式来**:
| 插件 | 预设格式(读) | 能否编程加载 | 导出时默认写成 | AI 的正确做法 | 状态 |
| --- | --- | --- | --- | --- | --- |
| **reFX Nexus** | `.fxp`(公开的 VST2 预设块) | ✅ **能** | **`.fxp`** | `chain[].presetFile` 直接加载 → 改参数 → `capture` 收编 → 导出 `.fxp` | ✅ 已适配(实测 3171 个预设) |
| **Xfer Serum 2** | `.SerumPreset`(私有) | ❌ 不能 | **`.SerumPreset`** | 索引挑候选 → **你在 GUI 载入一次** → `capture` 收编 → 导出 `.SerumPreset` | ✅ 已适配 |
| 其它 VST3 | 未知 / 由 GUI 管理 | 大多不能 | `.vstpreset`(通用) | 从零捏,或 GUI 载入后收编 | 通用回退 |
`vst3_presets` 的返回里每条预设都带 **`directlyLoadable`** 字段(`true` 可以直接加载,`false` 需要 GUI 收编);渲染结果里的 `warnings` 会在预设加载失败或渲染出静音时明确报警,而不是假装成功。
### 用 Nexus 预设(最顺的一条路)
```
1. vst3_presets { action: "search", format: "fxp", query: "bass", limit: 10 }
→ 每条都标注 ✅可直接加载
2. vst3_render { chain: [{ path: "<Nexus.vst3>", presetFile: "<预设.fxp>" }],
notes: [...] }
→ 直接出声(内部会自动处理 Nexus 的力度怪癖)
3. vst3_patch { action: "capture", name: "my-nexus-bass", path: "<Nexus.vst3>",
presetFile: "<预设.fxp>" }
→ 收编成我们的资产(实测:收编后脱离原预设文件复现,peak 完全一致 0.99107)
4. 之后就是常规流程:改参数 / A/B 变体 / 导出 .vstpreset / 版本回退
```
### 适配器里声明的"怪癖"
怪癖来自实测,集中声明在适配器里,而不是散落在渲染引擎各处:
| 怪癖 | 说明 |
| --- | --- |
| `forceNoteVelocity` | **Nexus 实测只在 MIDI 力度 1.0 时出声**;0.95/0.9/0.8/0.5 全部**完全静音**(逐进程隔离验证)。渲染时会自动把力度设为 1.0,并把这件事实报给模型。要控音量请改插件音量参数。 |
| `silentAfterPresetLoad` | 加载不被接受的预设后**静音而不报错**——所以载入预设却渲染出静音时会给出明确警告,而不是报"渲染成功" |
| `streamingSamples` | 采样流式插件,首次发声可能延迟,尾音要留足 |
另外针对插件不稳定(Nexus 在长驻进程里反复加载后**会间歇性崩溃**,实测遇到过一次访问违例):
**宿主子进程崩溃后会自动用干净进程重试一次**,两次都失败才报错并指向日志。崩溃只杀子进程,dsh 不受影响。
### 加一个新插件的适配器
1. 在 `core/` 下新建一个文件(如 `serum.ts`、`nexus.ts`),放**纯函数**:识别、读元数据、取可加载负载、写原生预设。
2. 在 `core/plugin-adapters.ts` 的 `ADAPTERS` 里加一个条目,声明四件事:
| 字段 | 回答的问题 |
| --- | --- |
| `matches(identity)` | 「怎么认出这个插件」(按名字/classId/路径,**别只按厂商**) |
| `presetLoad` | 「它的预设能不能被宿主直接加载」→ 决定 AI 是直接 `presetFile` 还是必须走 GUI 收编 |
| `presetFiles` | 「读」:怎么读元数据、怎么取可加载负载 |
| `presetWrite` | 「写」:导出时**默认写成什么格式**(如 `.SerumPreset`、`.fxp`);没有就回落通用 `.vstpreset` |
| `quirks` | 「实测出来的怪癖」:力度、静音、流式采样等 |
3. 加单元测试:写出的原生文件必须能被**自己的解析器**读回且 hash 校验通过;条件允许时拿**真实厂商文件**做逐字节复现。
工具层与渲染引擎都不用改——它们只问适配器。
在 `src/core/plugin-adapters.ts` 的 `ADAPTERS` 里加一项即可(工具层与渲染引擎都不用改):
```ts
const myAdapter: PluginAdapter = {
id: 'myplugin',
title: '某某插件',
matches: (id) => id.vendor === '某某厂商', // 怎么识别它
presetLoad: 'direct', // 'direct' 还是 'gui-only'
presetFiles: {
extensions: ['.mypreset'],
readMeta: (data, file) => ({ name, formatLabel, directlyLoadable: true }), // 索引用
toLoadPayload: (data, file) => ({ payload, note }), // 怎么变成可加载负载
},
quirks: { forceNoteVelocity: 1.0, silentAfterPresetLoad: true },
presetNote: '一句话说明这个插件的预设机制现状',
}
```
**接口在类型层面强制区分「能直接加载」与「只能 GUI 收编」**,避免把不同插件的预设机制混在一个 `if` 里越写越乱。
## 预设的加载与保存
这是最容易被误解的部分,所以结论都基于实测(不是推测)。
### 一句话结论
**VST3 的标准做法就是"预设 = 组件状态,由宿主管预设文件"**(Steinberg 官方文档原文:*the data of a preset is nothing more than its state*)。本插件的资产库正是这么做的,而且是唯一能覆盖"任意插件"的通道。插件自家的预设格式(Serum 的 `.SerumPreset`)属于它的**私有 GUI 通道**,标准宿主从设计上够不着。
### Serum 专门适配:能做什么、不能做什么
| 事项 | 状态 | 说明 |
| --- | --- | --- |
| 读预设**元数据** | ✅ | `.SerumPreset` = `XferJson` + **明文 JSON**(名称/作者/描述/标签/schema 版本)。本机 626 个工厂预设 + 压缩包内 90 个全部可索引 |
| 校验预设**完整性** | ✅ | 破解出 `hash = md5(zstd 压缩流)`,可验证文件是否损坏 |
| 索引 **`.SerumPack`** | ✅ | 实测是标准 ZIP,**不解压整包**即可读出内部预设元数据 |
| 读音色**血统** | ✅ | 从我们保存的状态信封里读出插件自报的 `presetName`/`presetAuthor`/插件版本/schema 版本——收编时自动命名 |
| **直接加载 `.SerumPreset`** | ❌ | **已用 9 种重建组合证明不可行**(含 schema 版本完全相同、hash 重算正确的 v9 预设)。根因:预设负载含 **GUI/session 节点**(`kUIParam*`、`SerumGUI`、`ClipPlayer`…),而 `IComponent::setState` 只接受纯处理器状态 |
| **写出 `.SerumPreset`** | ✅ | **导出默认就是它**。做法见下:复用状态里 `processor` 段的 zstd 压缩流写回 XferJson 容器,不重新压缩 → hash 天然成立、负载逐字节一致。**已用真实工厂预设做逐字节复现验证**(重建结果与 `PD - Analog Butter.SerumPreset` 完全一致,含 Serum 把版本号写成 `4.0` 这个细节) |
### 「读不进来、却写得出去」是怎么做到的
关键在于**我们本来就有 Serum 自己的那份负载**:`getState()` 吐出来的状态信封里有两个 XferJson 容器——
| 段 | JSON 头 | 内容 |
| --- | --- | --- |
| `processor` | `{"component":"processor", hash, product, version…}` | **音色本体**(msgpack tagged tree,zstd 压缩) |
| `controller` | `{"component":"controller", presetName, presetAuthor…}` | 界面态与预设元数据 |
而 `.SerumPreset` 文件就是一个 XferJson 容器,负载正是**同一份音色本体**,只是 JSON 头换成了 `{"fileType":"SerumPreset", presetName, presetAuthor, tags…}`。
所以写出 = 复用 `processor` 段的压缩流 + 换一个预设头 + `hash = md5(压缩流)`:
**不需要理解那个私有 tagged-tree 的类型枚举**(那正是"读"做不到的原因),也不需要重新压缩。
`vst3_patch` 的导出(`exportBundle` / `exportPreset`)对 Serum 默认就产出 `.SerumPreset`。
> 边界:`.SerumPreset` 里**只有音色**,不含界面态那一段,所以载入后界面上的旋钮位置会回到默认——音色本身不受影响。
### 所以你该怎么用(两条务实路径)
1. **捏新音色**:`vst3_capture` 存进资产库 → 可版本化、可 A/B、可渲染 → `exportBundle` 会同时给出 `.SerumPreset`(拖回 Serum 用)与 `.vstpreset`(给别的 DAW)。
2. **用现成的 Serum 预设**:`vst3_presets search` 让 AI 帮你从 800+ 条里挑候选 → **你在 Serum 界面里载入它(一次几秒)** → `vst3_capture` 收编 → 之后它就被 AI 完全接管,改完再导出成你自己的 `.SerumPreset`。
收编时会自动带上血统,`vst3_patch provenance` 随时能查"这个音色是从哪个预设来的"。
另外 `vst3_patch recipe` 会导出一份**人可读的参数配方**(参数名 + 界面显示值),所以音色还可以被逐项手抄复现。
### 通用 VST3 适配
| 能力 | 说明 |
| --- | --- |
| **标准 `.vstpreset` 导入/导出** | 格式:48 字节头(`'VST3'` + version + 32 字节 classId + int64 chunk 偏移)+ 数据区 + chunk list。我们已能把状态信封拆成 `Comp`/`Cont` 两块,所以互通成本很低。实测往返:导出 → 导入 → 17 个参数一致 → 渲染出声 |
| 标准预设目录扫描 | `Documents\VST3 Presets\<厂商>\<插件>\`、`%APPDATA%\VST3 Presets\...`、`%ProgramData%\VST3 Presets\...` |
| program 列表枚举与切换 | 带"名字是否有信息量""是否实现 `IProgramListData`""**这次切换是否真的改变了音色**"的判断。实测对比:Transient Master 的 128 个 program 有真实预设名(`Drum Crusher` 等,可按名切换且真的变声);Serum 2 的 128 个槽全叫 `Prog N` 且切换后音频**逐位相同**(空槽)——工具会明确告诉你这是空槽,别以为换了音色 |
| 状态保真度 | 状态往返后参数读回值可能有细微差异(Serum 2 实测 12 个包络曲线参数 0.4→0.5),预热对齐后音频差异约 **2.67% 样本、最大 -25dB**。**不要**用"载入后重推全部参数"去修——实测**更差**(9.4%),因为有些参数(`Bank` 是 `kIsProgramChange`、还有只读参数)不该写 |
### 踩过的坑(都已修 + 有回归测试)
- **保存状态前必须冲刷**:`setParameter` 只是排队,要一次 `process()` 才进处理器。不冲刷就 `saveState` 会存下一份 Init 状态(2183 字节)而参数全丢——这曾让"先改参数再保存"静默失效。
- **`process({numSamples:0})` 确实算冲刷**(实测与真实块等效),但**必须无条件执行**,不能只在显式传参数时做。
- `.SerumPack` 虽大(152MB),但只需读中央目录 + 单个条目即可拿到元数据,不必整包解压。
- **宿主子进程绝不能用 Electron 二进制来跑**(dsh 桌面版就是 Electron 应用):直接跑会秒退(退出码 0、连日志都不写);加 `ELECTRON_RUN_AS_NODE=1` 后能跑普通脚本,但 `require('nvst3-host')` 会把进程**直接打崩**(退出码 `0xFFFF7003`,崩在 N-API 加载处,`try/catch` 拦不住)。所以监督器会优先去找系统真 node(PATH → 常见安装位置 → nvm/fnm/volta),找不到才警告式兜底到 Electron,并在第一个候选没握手就退出时自动换下一个。详见 [`INSTALL.md`](./INSTALL.md) 对应条目。
- **同一个插件上有两种"静默改值",都不报错**(0.1.2 起主动告警):显示值带 `%` 的参数(如 Serum 2 的 `Main Vol`、`A Level`)给**裸数字**会被解析成 **100%**(钳到最大);布尔参数写字符串 `"On"` 会被解析成 **Off**。判据是"插件回读值与请求的数值对不上",命中就报 `⚠ 疑似被插件改写`。正确写法:带 `%` 的写 `"55%"`,带时间写 `"1.2s"`,带频率写 `"1200Hz"`,带电平写 `"-9dB"`,布尔量用 plain `1`/`0`。
- **不能凭"参数设成功了"就断定声音变了**:实测同一轮里 `Filter 1 Freq` 给 400/1200/4000Hz 渲出的 WAV **逐字节相同**(MD5 一致),因为路由默认没把振荡器送进滤波器;而 `A WT Pos` 一动,频谱重心立刻从 4673Hz 变成 736Hz。**判断改动是否真生效要比对音频(哈希/指标),不能只看参数回读。**
- **导出目录不能靠猜**:桌面版里 `DSH_SESSION_JSONL` 只注入给 shell 工具、**没有**注入插件进程,所以只靠环境变量的启发式必然失败,导出目录会静默掉到 dsh 的进程工作目录(实测 `D:\Program Files\DSH Desktop`,既不该写也常常写不进去)。0.1.2 起改为:环境变量 → **直接扫 `<DSH_HOME>/sessions/` 取最近写入的会话目录反推工作区** → 进程工作目录 → dsh 自己的目录,并且每一级都**实测可写**才采用。
- **结果渲染的 `if/else` 链别拿 `else` 当兜底**:`vst3_patch` 曾把非 `save/load/list` 的动作全归到 `delete` 分支,于是 `exportBundle`/`capture`/`recipe` 都会假报一句「已删除」,看着像音色库被清空(实际文件一个没动)。已改成显式判 `delete`,并加了集成断言。
- **适配层不能只做"读",还得做"写"**:只做读时,导出对 Serum 也一律落 `.vstpreset`——而 Serum 的浏览器**只认 `.SerumPreset`**,等于导了个它看不见的文件。现在适配器同时声明 `presetFiles`(读)与 `presetWrite`(写),导出默认走原生格式。
- **JSON 里的数字写法会破坏逐字节复现**:Serum 把 schema 版本写成 `"version":4.0`,而 `JSON.parse`→`JSON.stringify` 会规范化成 `4`,重建出的容器就比原文件少 2 字节。语义等价,但既然目标是与厂商产物完全一致,就按它的写法序列化(测试直接拿真实工厂预设做逐字节比对)。
- **厂商名不能单独当插件判据**:Xfer 除了 Serum 还有 OTT / Kickstart / Transient Master,而它们的 VST3 状态**结构极像**(同为 XferJson、同样有 processor/controller 段)。按厂商判定会把 OTT 的音色写成 `.SerumPreset`(归属与后缀都错)。现在按名字/classId/路径判定,厂商只作兜底弱信号。
- **`output.schema` 是 `additionalProperties:false`,多一个字段就整条被拒**(两个实例):(1) 把 `nativePresetFile` 加进**必填**列表却只在导出分支返回 → `save`/`capture`/`list` 全报 `missing required property`;(2) `exportAll` 从 0.1.0 起就返回未声明的 `total`/`returned` → **这个动作一直是坏的**,GUI 里一调就失败。根因是**集成测试直接调 `tool.output.render()`,绕过了 DSH 的返回值校验**。现在集成测试每次调用都过一遍 `test/lib/schema.mjs` 的同构校验器,并新增 `test/tool-schema.test.mjs` 覆盖不需要宿主的动作。
## 配置项
改配置**不要改安装包里的文件**,在 profile 补丁里按相同 `id` 覆盖整行:
`$DSH_HOME/profiles/web/cordis.patch.yml`:
```yaml
- id: dsh-vst3-studio
name: dsh-vst3-studio
config:
scanDirs: [] # 额外扫描目录;空=平台默认位置
allowDirs: ['C:/Program Files/Common Files/VST3'] # 只允许加载这些目录下的插件;空=不限制
assetDir: 'D:/audio/vst3-assets' # 音色资产库(默认 $DSH_HOME/vst3-studio/assets)
renderDir: 'D:/audio/renders' # 渲染输出目录
sampleRate: 48000
maxBlockSize: 512
requestTimeoutMs: 120000 # 普通命令超时
renderTimeoutMs: 600000 # 渲染命令超时(超时会杀掉子进程)
maxRenderSec: 900 # 单次渲染音频总长上限(秒)
bitDepth: 16 # 16 / 24 / 32
maxCrashes: 5 # 60 秒窗口内崩溃超过这个数就停止自动重启
analyzeByDefault: true # 渲染时默认顺带做分析
logFile: 'D:/audio/host.log' # 宿主子进程日志(排查现场用)
nodePath: '' # 跑宿主子进程的 node;空=自动(Electron 桌面版下会自动找系统 node)
# 预设索引相关
presetDirs: [] # 额外要索引的预设目录(除自动发现的之外)
serumPresetPath: '' # 覆盖 Serum 预设根目录;空=从 Serum2Prefs.json 自动读
nexusContentPath: '' # 覆盖 Nexus 库路径;空=注册表 → scanDirs 浅层搜索 → addSource 手动加
includeSerumPacks: true # 是否索引 .SerumPack 压缩包内部的预设
maxPackBytes: 536870912 # 单个压缩包允许读取的上限(字节,默认 512MB)
maxIndexEntries: 5000 # 预设索引条目上限
exportDir: '' # 导出根目录;空=自动(项目文件夹 + /vst3-exports)
```
## 架构
```
dsh 主进程
└─ src/index.ts 插件壳:配置 + 装配 11 个工具
├─ host/supervisor.ts 子进程监督:TCP 回环 IPC、超时杀进程、崩溃自动重启、
│ 运行时解析(优先真 node,Electron 下自动绕开自身)
│ └─ host/worker.ts 宿主子进程:唯一 require('nvst3-host') 的地方
├─ core/render.ts 离线渲染引擎
├─ core/params.ts 参数索引(按名定位、歧义报错、归一化换算)
└─ core/{wav,midi,dsp}.ts 纯函数:音频读写 / SMF / 客观分析
```
**为什么一定要子进程隔离**:VST3 插件是同机第三方原生二进制,加载即在你的用户权限下执行外部代码。同进程加载一旦崩溃会直接带走 dsh。隔离后最坏情况只是子进程死掉。已实测:SIGKILL 子进程后下一次调用自动重启(`spawnCount` +1、崩溃计数 +1);请求超时会强制终止子进程(插件卡死时唯一可靠的自救手段),随后自动恢复。
**为什么 IPC 走 TCP 回环而不是 stdio 管道**:dsh 沙箱会拒绝以管道 stdio 启动的子进程(实测 `spawn EPERM`),而 `fork()` 的 IPC 通道在 Windows 上也是命名管道,同样会被挡。父进程监听 `127.0.0.1` 随机端口 + 每次随机 token 握手,子进程反向连回,全程不碰管道,还能脱离 dsh 单独调试。
## 安全边界
- **加载 VST3 = 执行本机原生代码**。默认不限制路径(通用性优先),但可以用 `allowDirs` 收紧到固定目录。`vst3_env` 会把这个边界如实告诉模型。
- IPC 只监听回环地址,且有随机 token 校验,不会把原生宿主暴露到局域网。
- 渲染有 `maxRenderSec` 上限,防止超长 MIDI 把内存吃光。
## 实测边界(诚实清单)
**能做到**
- 参数级捏音色:改 `Env 1 Attack` 0→0.6 使起音首 20ms 能量差 **227 倍**;`A Level` 1.0 vs 0.2 峰值差 **25 倍**——都是可测的。
- MIDI → 音频忠实:渲染出的音高精确命中目标音(分析器已用已知正弦波标定,误差 0.0005%)。
- 音色可复现:`saveState` 落盘后重新渲染,音乐会按新音色改变(peak 0.284 → 0.724)。
**做不到,别指望**
- **没有 GUI**:点不了插件里的预设浏览器。Serum 的 `.SerumPreset` 是 GUI 层专有格式,**读不了**;只能做 VST3 状态往返。所以"拿现成预设当起点"基本走不通,得从参数捏 + 状态复用。
- **Kontakt 8 这类采样器**:音色库映射依赖 GUI,无 GUI 基本没法用。
- **复音分析不可信**:单音高估计器面对和弦会给"公共周期/虚拟基频"(C-E-G 实测报 65.54Hz,不是任何一个组成音)。连奏重叠时同理——插件会在 `hint` 里明确警告"此时音高是混合结果,不能判断单音准不准",并建议把音符拉开重渲。
- **音高分析范围 58Hz–8kHz**:超出或帧内不足 ~4 个周期时返回 `hz=0` 而不猜。
- **32-bit int / 64-bit float WAV 读入会掉精度**到 float32(下游渲染链本来就是 float32,这是刻意的)。
- **没有审美**:好不好听最终得你的耳朵判断。建议每次改动都渲染出来试听。
## 排错
| 现象 | 原因与处理 |
| --- | --- |
| `无法加载 native 模块 nvst3-host` | 插件目录缺依赖。沙箱环境需 `npm install --ignore-scripts`(预编译二进制随包发布,本就不需要编译) |
| 加载插件报 `VST3_LOAD_FAILED` | 路径错 / 不是有效 VST3 模块。Windows 上 `.vst3` 通常是**目录**,末尾后缀不能省。错误信息尾部若是乱码(原生模块按 ANSI 取值导致),看 `[提示]` 那段即可 |
| 渲染全静音(`peakDbfs: null`) | 该音色需要先打开振荡器/音量参数(找 `Level`/`Enable`/`Volume` 调大),或需要 `stateFile` 载入音色,或这其实是效果器(没有音频输入) |
| 削波样本数 > 0 | 音量参数给大了,或用 `gainDb` 给负值 |
| `stages[].unresolved` 非空 | 参数名写错或有歧义。用 `vst3_params` 核对,歧义时返回的 `candidates` 会列出候选 |
| 命令超时后报"宿主子进程已被强制终止" | 插件在该参数/采样率下死循环。换个参数或插件重试即可(会自动重启) |
| 崩溃次数持续增长 | 某个插件不稳定。换插件;或调大 `maxCrashes` 观察。现场在 `logFile` 里 |
| 连奏时分析只给出一个音高 | 这是正确行为(音符重叠成一段)。要逐音验证音准就把音符拉开 0.1–0.2s |
## 开发
```sh
npm install --ignore-scripts # 沙箱环境必须加 --ignore-scripts
npm run build # tsc → dist/
npm test # 84 个单测(WAV/MIDI/DSP/监督器隔离)
npm run test:integration # 47 项真机集成测试(需要本机有 Serum 2 与 OTT)
```
集成测试会真实加载插件、渲染音频、验证音高,产物落在 `.tmp/itest/`。
> 本机实测性能:Serum 2 渲染 2–3 秒音频耗时 **150–330ms**;分析 10 秒 48kHz 立体声约 **96ms**。
## 许可
MIT。`nvst3-host` 与其内置的 VST3 SDK(自 v3.7.7 起)同为 MIT,商用/闭源无授权顾虑。
Install
dsh plugin --profile web add github:lwy0v0/dsh-miao-vst3
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-vst3-studio from the hub
- This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.