DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

lwy0v0 /

lwy0v0/dsh-miao-vst3

Verified

这是一个deepseek harness项目。可以让模型操作vst3插件,注意:暂时不支持vst2。作为一个让模型捏音色的尝试性项目。目前针对serum与nexus做了适配性优化,其他插件可能存在适配性不足的情况。

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: master@20ccf56f

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_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)

安装

# 在包含本目录的路径下执行;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 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 「实测出来的怪癖」:力度、静音、流式采样等
  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,等于导了个它看不见的文件。现在适配器同时声明 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:

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

—/ 5

No ratings yet

Verified DSH bundle

Commit 20ccf56f3bca

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout