DeepSeek Harness Plugin Hub

Publish and manage complete Harness Profiles. Discover Plugins for your next setup.

Explore

PluginsPresetsDocsNews

Community

Publish a pluginContactReport an issue

Resources

Plugin Hub on GitHubDeepSeek HarnessSystem statusPrivacy notice
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

Independent and unofficial. Not affiliated with, authorized by, or endorsed by DeepSeek.

Vst3 Studio — DSH Plugin for DeepSeek Harness
DeepSeek Harness Plugin Hub
ProfilesPluginsCategoriesNewsDocsSign inManage Profiles
ProfilesPluginsCategoriesNewsDocsSign in
← Plugins
V

dsh-vst3-studio

Vst3 Studio

DeepSeek Harness (dsh) VST3 audio studio plugin: uses nvst3-host (Node N-API, official Steinberg VST3 SDK) to let AI operate any VST3 plugin in a general-purpose way—scanning, searching by name and modifying parameters in batches, saving and loading sound states, managing sound asset libraries, rend

The plugin will be installed here. Keep web if you are unsure.

npx -y @deepseek-ai/dsh plugin --profile web add github:lwy0v0/dsh-miao-vst3#20ccf56f3bca29d48cbdb56444cbf86c0439e2f3
READMECompatibilityVersions

Description

DeepSeek Harness (dsh) VST3 audio studio plugin: uses nvst3-host (Node N-API, official Steinberg VST3 SDK) to let AI operate any VST3 plugin in a general-purpose way—scanning, searching by name and modifying parameters in batches, saving and loading sound states, managing sound asset libraries, rendering MIDI files or notes to audio, chaining multiple plugin effects, and performing objective audio analysis on the results to form a closed loop. The host runs in an isolated child process, so plugin crashes do not affect dsh.

Compatibility and provenance

Vst3 Studio is published as dsh-vst3-studio and currently resolves to version 0.1.6. The Hub verifies its manifest and preserves the exact installation source for reproducible installs.

DSH compatibility
*
Runtime surfaces
any
Release source
github
Registry updated
9/12/2026

Versions

0.1.6stable
9/12/2026
Latest
0.1.6
DSH
*
HMR
Process restart
Tree shaking
Safe tree shaking not declared
Unpacked size
Unavailable
Files
Unavailable
Surface
any
License
Not declared
Source
github
GitHub
★ 0
Weekly downloads
0
Last push
9/12/2026
View source ↗
README badge

Click the badge to copy Markdown for your README.

Do you maintain this Plugin?Claim benefit · Priority security scan

Verify the GitHub repository declared in package.json to manage this listing. After you claim it, Hub will prioritize a security scan of the current version and publish the result when it passes.

Claim this Plugin →
Report an issue

README

dsh-vst3-studio

DeepSeek Harness(dsh)的 VST3 音频工作室插件:让 AI 直接操作本机的 VST3 插件来捏音色、把 MIDI 渲染成音频、串效果链,并用客观指标自己验证结果。

底层用 nvst3-host(Node N-API 原生模块,封装官方 Steinberg VST3 SDK 3.8,MIT)。所有原生操作跑在隔离子进程里:插件崩溃只死子进程,dsh 不受影响,下一次调用自动重启。

它能做什么

能力工具实测
发现本机插件vst3_scan本机扫到 114 条 → 去重过滤后得到干净的可用清单
看懂一个插件vst3_inspectSerum 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_render4 音符 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_patchexportBundle 一套三件(.vstpreset + .recipe.md + .patch.json);exportAll 批量导出整个库(实测 6/6)
自我验证闭环vst3_analyze音高/RMS/峰值/削波/音头/频谱重心,音高精度 0.0005%
MIDI 资产管理vst3_midi生成/解析 .mid(音名或 MIDI 号,支持 tempo map)

安装

# 在包含本目录的路径下执行;web 是当前 GUI 用的 profile
dsh plugin --profile web add ./dsh-vst3-studio-0.1.6.tgz

安装后重启 dsh,会话里会出现 11 个 vst3_* 工具。详细步骤与验证方法见 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 1Documents\Xfer\Serum Presets(.fxp,可用 Serum 2 自带的旧版导入迁移)
标准 VST3Documents\VST3 Presets、%APPDATA%\VST3 Presets、%ProgramData%\VST3 Presets(macOS / Linux 对应位置同理)
FL StudioDocuments\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 预设块)✅ 能.fxpchain[].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 / 版本回退

适配器里声明的"怪癖"

怪癖来自实测,集中声明在适配器里,而不是散落在渲染引擎各处:

怪癖说明
forceNoteVelocityNexus 实测只在 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「实测出来的怪癖」:力度、静音、流式采样等
  1. 加单元测试:写出的原生文件必须能被自己的解析器读回且 hash 校验通过;条件允许时拿真实厂商文件做逐字节复现。

工具层与渲染引擎都不用改——它们只问适配器。 在 src/core/plugin-adapters.ts 的 ADAPTERS 里加一项即可(工具层与渲染引擎都不用改):

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 对应条目。
  • 同一个插件上有两种"静默改值",都不报错(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,等于导了个它看不见的文件。现在适配器同时声明 (读)与 (写),导出默认走原生格式。

配置项

改配置不要改安装包里的文件,在 profile 补丁里按相同 id 覆盖整行:

$DSH_HOME/profiles/web/cordis.patch.yml:

- 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

开发

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,商用/闭源无授权顾虑。

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 覆盖不需要宿主的动作。