@audio/host-vst 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/binding.gyp +20 -0
- package/index.js +167 -0
- package/native/binding.cpp +235 -0
- package/native/host.cpp +423 -0
- package/native/host.h +45 -0
- package/native/vst3.h +232 -0
- package/package.json +40 -0
- package/src/addon.js +26 -0
package/binding.gyp
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"targets": [{
|
|
3
|
+
"target_name": "host_vst",
|
|
4
|
+
"sources": ["native/host.cpp", "native/binding.cpp"],
|
|
5
|
+
"include_dirs": ["native"],
|
|
6
|
+
"cflags_cc": ["-std=c++17", "-O2", "-fno-exceptions"],
|
|
7
|
+
"xcode_settings": {
|
|
8
|
+
"OTHER_CPLUSPLUSFLAGS": ["-std=c++17", "-O2", "-fno-exceptions"],
|
|
9
|
+
"CLANG_CXX_LANGUAGE_STANDARD": "c++17",
|
|
10
|
+
"MACOSX_DEPLOYMENT_TARGET": "10.13"
|
|
11
|
+
},
|
|
12
|
+
"msvs_settings": {
|
|
13
|
+
"VCCLCompilerTool": { "AdditionalOptions": ["/std:c++17", "/O2"] }
|
|
14
|
+
},
|
|
15
|
+
"conditions": [
|
|
16
|
+
["OS=='mac'", { "libraries": ["-framework CoreFoundation"] }],
|
|
17
|
+
["OS=='linux'", { "libraries": ["-ldl"] }]
|
|
18
|
+
]
|
|
19
|
+
}]
|
|
20
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @audio/host-vst
|
|
3
|
+
* VST3 plugin host for audiojs
|
|
4
|
+
*/
|
|
5
|
+
import addon from './src/addon.js'
|
|
6
|
+
import { readdirSync, statSync } from 'fs'
|
|
7
|
+
import { join } from 'path'
|
|
8
|
+
import { execFileSync } from 'child_process'
|
|
9
|
+
|
|
10
|
+
export function load(path, opts = {}) {
|
|
11
|
+
const { sampleRate = 44100, channels = 2, blockSize = 128 } = opts
|
|
12
|
+
const handle = addon.open(path, sampleRate, channels, blockSize)
|
|
13
|
+
let closed = false
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
get name() { return addon.getName(handle) },
|
|
17
|
+
get vendor() { return addon.getVendor(handle) },
|
|
18
|
+
get inputChannels() { return addon.getChannels(handle, 0) },
|
|
19
|
+
get outputChannels() { return addon.getChannels(handle, 1) },
|
|
20
|
+
blockSize,
|
|
21
|
+
|
|
22
|
+
get params() {
|
|
23
|
+
const n = addon.getParamCount(handle)
|
|
24
|
+
const out = []
|
|
25
|
+
for (let i = 0; i < n; i++) {
|
|
26
|
+
const info = addon.getParamInfo(handle, i)
|
|
27
|
+
if (info) out.push(info)
|
|
28
|
+
}
|
|
29
|
+
return out
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
getParam(id) { return addon.getParam(handle, id) },
|
|
33
|
+
setParam(id, value) { addon.setParam(handle, id, value) },
|
|
34
|
+
|
|
35
|
+
process(inputs, outputs) { addon.process(handle, inputs, outputs) },
|
|
36
|
+
processAll(ch) { return processBlocks(addon, handle, ch, blockSize) },
|
|
37
|
+
|
|
38
|
+
getState() { return addon.getState(handle) },
|
|
39
|
+
setState(buf) { addon.setState(handle, buf) },
|
|
40
|
+
|
|
41
|
+
close() {
|
|
42
|
+
if (closed) return
|
|
43
|
+
closed = true
|
|
44
|
+
addon.close(handle)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Scan system for installed VST3 plugins.
|
|
51
|
+
* Returns array of { path, name, vendor, format } for each loadable plugin.
|
|
52
|
+
*/
|
|
53
|
+
export function scan(dirs) {
|
|
54
|
+
return scanDir(dirs || defaultPaths({
|
|
55
|
+
darwin: ['/Library/Audio/Plug-Ins/VST3', '$HOME/Library/Audio/Plug-Ins/VST3'],
|
|
56
|
+
linux: ['$HOME/.vst3', '/usr/lib/vst3', '/usr/local/lib/vst3'],
|
|
57
|
+
win32: ['$PROGRAMFILES/Common Files/VST3'],
|
|
58
|
+
}), '.vst3', 'vst3')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Register a VST3 plugin as an AudioWorkletProcessor.
|
|
63
|
+
*
|
|
64
|
+
* import { AudioWorkletProcessor } from 'web-audio-api'
|
|
65
|
+
* await ctx.audioWorklet.addModule(register(path, AudioWorkletProcessor))
|
|
66
|
+
* const node = new AudioWorkletNode(ctx, 'Plugin Name')
|
|
67
|
+
*/
|
|
68
|
+
export function register(path, BaseClass, opts = {}) {
|
|
69
|
+
const probe = load(path, { ...opts, sampleRate: opts.sampleRate || 44100 })
|
|
70
|
+
const name = probe.name
|
|
71
|
+
const descriptors = probe.params.map(p => ({
|
|
72
|
+
name: p.name,
|
|
73
|
+
defaultValue: p.defaultValue,
|
|
74
|
+
minValue: p.min,
|
|
75
|
+
maxValue: p.max,
|
|
76
|
+
automationRate: 'k-rate'
|
|
77
|
+
}))
|
|
78
|
+
probe.close()
|
|
79
|
+
|
|
80
|
+
return function(scope) {
|
|
81
|
+
class PluginProcessor extends BaseClass {
|
|
82
|
+
static get parameterDescriptors() { return descriptors }
|
|
83
|
+
|
|
84
|
+
constructor(options) {
|
|
85
|
+
super(options)
|
|
86
|
+
this._handle = addon.open(path, scope.sampleRate,
|
|
87
|
+
opts.channels || 2, opts.blockSize || 128)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
process(inputs, outputs) {
|
|
91
|
+
const input = inputs[0], output = outputs[0]
|
|
92
|
+
if (!output || !output.length) return true
|
|
93
|
+
addon.process(this._handle, input.length ? input : null, output)
|
|
94
|
+
return true
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
scope.registerProcessor(name, PluginProcessor)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function defaultPaths(map) {
|
|
103
|
+
const plat = process.platform
|
|
104
|
+
return (map[plat] || []).map(p =>
|
|
105
|
+
p.replace('$HOME', process.env.HOME || process.env.USERPROFILE || '')
|
|
106
|
+
.replace('$PROGRAMFILES', process.env.PROGRAMFILES || 'C:\\Program Files'))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function scanDir(dirs, ext, format) {
|
|
110
|
+
/* Collect all plugin paths */
|
|
111
|
+
const paths = []
|
|
112
|
+
for (const dir of dirs) {
|
|
113
|
+
let entries
|
|
114
|
+
try { entries = readdirSync(dir) } catch { continue }
|
|
115
|
+
for (const name of entries) {
|
|
116
|
+
const full = join(dir, name)
|
|
117
|
+
if (name.endsWith(ext)) { paths.push(full); continue }
|
|
118
|
+
try {
|
|
119
|
+
if (statSync(full).isDirectory())
|
|
120
|
+
for (const sub of readdirSync(full))
|
|
121
|
+
if (sub.endsWith(ext)) paths.push(join(full, sub))
|
|
122
|
+
} catch {}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* Probe each in a subprocess — isolates crashes and ObjC class conflicts */
|
|
127
|
+
const results = []
|
|
128
|
+
for (const path of paths) {
|
|
129
|
+
try {
|
|
130
|
+
const code = `import{load}from'${import.meta.url}';try{const p=load(${JSON.stringify(path)});console.log(JSON.stringify({path:${JSON.stringify(path)},name:p.name,vendor:p.vendor,format:'${format}'}));p.close()}catch{}`
|
|
131
|
+
const out = execFileSync(process.execPath, ['--input-type=module', '-e', code],
|
|
132
|
+
{ timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
133
|
+
const line = out.toString().trim()
|
|
134
|
+
if (line) results.push(JSON.parse(line))
|
|
135
|
+
} catch {}
|
|
136
|
+
}
|
|
137
|
+
return results
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function processBlocks(addon, handle, channels, blockSize) {
|
|
141
|
+
const len = channels[0].length, numCh = channels.length
|
|
142
|
+
const out = Array.from({ length: numCh }, () => new Float32Array(len))
|
|
143
|
+
|
|
144
|
+
const inBlock = Array.from({ length: numCh }, () => new Float32Array(blockSize))
|
|
145
|
+
const outBlock = Array.from({ length: numCh }, () => new Float32Array(blockSize))
|
|
146
|
+
|
|
147
|
+
for (let off = 0; off < len; off += blockSize) {
|
|
148
|
+
const n = Math.min(blockSize, len - off)
|
|
149
|
+
if (n === blockSize) {
|
|
150
|
+
const inSub = [], outSub = []
|
|
151
|
+
for (let c = 0; c < numCh; c++) {
|
|
152
|
+
inSub[c] = channels[c].subarray(off, off + blockSize)
|
|
153
|
+
outSub[c] = out[c].subarray(off, off + blockSize)
|
|
154
|
+
}
|
|
155
|
+
addon.process(handle, inSub, outSub)
|
|
156
|
+
} else {
|
|
157
|
+
for (let c = 0; c < numCh; c++) {
|
|
158
|
+
inBlock[c].fill(0)
|
|
159
|
+
inBlock[c].set(channels[c].subarray(off, off + n))
|
|
160
|
+
outBlock[c].fill(0)
|
|
161
|
+
}
|
|
162
|
+
addon.process(handle, inBlock, outBlock)
|
|
163
|
+
for (let c = 0; c < numCh; c++) out[c].set(outBlock[c].subarray(0, n), off)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return out
|
|
167
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* binding.cpp — NAPI bindings for VST3 host
|
|
3
|
+
*/
|
|
4
|
+
#include <node_api.h>
|
|
5
|
+
#include "host.h"
|
|
6
|
+
|
|
7
|
+
#define NAPI_CALL(env, call) do { \
|
|
8
|
+
napi_status s = (call); \
|
|
9
|
+
if (s != napi_ok) { napi_throw_error(env, NULL, #call " failed"); return NULL; } \
|
|
10
|
+
} while(0)
|
|
11
|
+
|
|
12
|
+
static void destructor(napi_env env, void* data, void* hint) {
|
|
13
|
+
(void)env; (void)hint;
|
|
14
|
+
vst3_destroy((vst3_plugin_t*)data);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* open(path, sampleRate, channels, blockSize) → external */
|
|
18
|
+
static napi_value node_open(napi_env env, napi_callback_info info) {
|
|
19
|
+
size_t argc = 4;
|
|
20
|
+
napi_value argv[4];
|
|
21
|
+
NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
|
|
22
|
+
|
|
23
|
+
char path[2048];
|
|
24
|
+
size_t len;
|
|
25
|
+
NAPI_CALL(env, napi_get_value_string_utf8(env, argv[0], path, sizeof(path), &len));
|
|
26
|
+
|
|
27
|
+
double sampleRate = 44100; int channels = 2, blockSize = 128;
|
|
28
|
+
if (argc > 1) napi_get_value_double(env, argv[1], &sampleRate);
|
|
29
|
+
if (argc > 2) napi_get_value_int32(env, argv[2], &channels);
|
|
30
|
+
if (argc > 3) napi_get_value_int32(env, argv[3], &blockSize);
|
|
31
|
+
|
|
32
|
+
vst3_plugin_t* plugin = vst3_open(path, sampleRate, channels, blockSize);
|
|
33
|
+
if (!plugin) {
|
|
34
|
+
const char* err = vst3_get_error();
|
|
35
|
+
napi_throw_error(env, NULL, err[0] ? err : "Failed to load VST3 plugin");
|
|
36
|
+
return NULL;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
napi_value ext;
|
|
40
|
+
NAPI_CALL(env, napi_create_external(env, plugin, destructor, NULL, &ext));
|
|
41
|
+
return ext;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* close(handle) */
|
|
45
|
+
static napi_value node_close(napi_env env, napi_callback_info info) {
|
|
46
|
+
size_t argc = 1; napi_value argv[1];
|
|
47
|
+
NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
|
|
48
|
+
vst3_plugin_t* p; napi_get_value_external(env, argv[0], (void**)&p);
|
|
49
|
+
vst3_close(p);
|
|
50
|
+
return NULL;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* getName(handle) → string */
|
|
54
|
+
static napi_value node_getName(napi_env env, napi_callback_info info) {
|
|
55
|
+
size_t argc = 1; napi_value argv[1];
|
|
56
|
+
napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
|
|
57
|
+
vst3_plugin_t* p; napi_get_value_external(env, argv[0], (void**)&p);
|
|
58
|
+
napi_value result;
|
|
59
|
+
napi_create_string_utf8(env, vst3_get_name(p), NAPI_AUTO_LENGTH, &result);
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* getVendor(handle) → string */
|
|
64
|
+
static napi_value node_getVendor(napi_env env, napi_callback_info info) {
|
|
65
|
+
size_t argc = 1; napi_value argv[1];
|
|
66
|
+
napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
|
|
67
|
+
vst3_plugin_t* p; napi_get_value_external(env, argv[0], (void**)&p);
|
|
68
|
+
napi_value result;
|
|
69
|
+
napi_create_string_utf8(env, vst3_get_vendor(p), NAPI_AUTO_LENGTH, &result);
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* getChannels(handle, dir) → number */
|
|
74
|
+
static napi_value node_getChannels(napi_env env, napi_callback_info info) {
|
|
75
|
+
size_t argc = 2; napi_value argv[2];
|
|
76
|
+
napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
|
|
77
|
+
vst3_plugin_t* p; napi_get_value_external(env, argv[0], (void**)&p);
|
|
78
|
+
int dir = 0; if (argc > 1) napi_get_value_int32(env, argv[1], &dir);
|
|
79
|
+
napi_value result;
|
|
80
|
+
napi_create_int32(env, vst3_get_channels(p, dir), &result);
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* getParamCount(handle) → number */
|
|
85
|
+
static napi_value node_getParamCount(napi_env env, napi_callback_info info) {
|
|
86
|
+
size_t argc = 1; napi_value argv[1];
|
|
87
|
+
napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
|
|
88
|
+
vst3_plugin_t* p; napi_get_value_external(env, argv[0], (void**)&p);
|
|
89
|
+
napi_value result;
|
|
90
|
+
napi_create_int32(env, vst3_get_param_count(p), &result);
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* getParamInfo(handle, index) → { id, name, min, max, defaultValue, stepCount } */
|
|
95
|
+
static napi_value node_getParamInfo(napi_env env, napi_callback_info info) {
|
|
96
|
+
size_t argc = 2; napi_value argv[2];
|
|
97
|
+
napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
|
|
98
|
+
vst3_plugin_t* p; napi_get_value_external(env, argv[0], (void**)&p);
|
|
99
|
+
int index; napi_get_value_int32(env, argv[1], &index);
|
|
100
|
+
|
|
101
|
+
vst3_param_info_t pi;
|
|
102
|
+
if (vst3_get_param_info(p, index, &pi) != 0) return NULL;
|
|
103
|
+
|
|
104
|
+
napi_value obj, val;
|
|
105
|
+
napi_create_object(env, &obj);
|
|
106
|
+
|
|
107
|
+
napi_create_uint32(env, pi.id, &val); napi_set_named_property(env, obj, "id", val);
|
|
108
|
+
napi_create_string_utf8(env, pi.name, NAPI_AUTO_LENGTH, &val); napi_set_named_property(env, obj, "name", val);
|
|
109
|
+
napi_create_double(env, pi.min, &val); napi_set_named_property(env, obj, "min", val);
|
|
110
|
+
napi_create_double(env, pi.max, &val); napi_set_named_property(env, obj, "max", val);
|
|
111
|
+
napi_create_double(env, pi.defaultValue, &val); napi_set_named_property(env, obj, "defaultValue", val);
|
|
112
|
+
napi_create_int32(env, pi.stepCount, &val); napi_set_named_property(env, obj, "stepCount", val);
|
|
113
|
+
|
|
114
|
+
return obj;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* getParam(handle, id) → number */
|
|
118
|
+
static napi_value node_getParam(napi_env env, napi_callback_info info) {
|
|
119
|
+
size_t argc = 2; napi_value argv[2];
|
|
120
|
+
napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
|
|
121
|
+
vst3_plugin_t* p; napi_get_value_external(env, argv[0], (void**)&p);
|
|
122
|
+
uint32_t id; napi_get_value_uint32(env, argv[1], &id);
|
|
123
|
+
napi_value result;
|
|
124
|
+
napi_create_double(env, vst3_get_param(p, id), &result);
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* setParam(handle, id, value) */
|
|
129
|
+
static napi_value node_setParam(napi_env env, napi_callback_info info) {
|
|
130
|
+
size_t argc = 3; napi_value argv[3];
|
|
131
|
+
napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
|
|
132
|
+
vst3_plugin_t* p; napi_get_value_external(env, argv[0], (void**)&p);
|
|
133
|
+
uint32_t id; napi_get_value_uint32(env, argv[1], &id);
|
|
134
|
+
double val; napi_get_value_double(env, argv[2], &val);
|
|
135
|
+
vst3_set_param(p, id, val);
|
|
136
|
+
return NULL;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* process(handle, inputs, outputs) — inputs/outputs are arrays of Float32Array */
|
|
140
|
+
static napi_value node_process(napi_env env, napi_callback_info info) {
|
|
141
|
+
size_t argc = 3; napi_value argv[3];
|
|
142
|
+
napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
|
|
143
|
+
vst3_plugin_t* p; napi_get_value_external(env, argv[0], (void**)&p);
|
|
144
|
+
|
|
145
|
+
float* inPtrs[8] = {};
|
|
146
|
+
float* outPtrs[8] = {};
|
|
147
|
+
uint32_t numCh = 0;
|
|
148
|
+
size_t numSamples = 0;
|
|
149
|
+
|
|
150
|
+
/* Extract input channel pointers */
|
|
151
|
+
bool hasInputs = false;
|
|
152
|
+
napi_valuetype inputType;
|
|
153
|
+
napi_typeof(env, argv[1], &inputType);
|
|
154
|
+
if (inputType != napi_null && inputType != napi_undefined) {
|
|
155
|
+
hasInputs = true;
|
|
156
|
+
napi_get_array_length(env, argv[1], &numCh);
|
|
157
|
+
for (uint32_t i = 0; i < numCh && i < 8; i++) {
|
|
158
|
+
napi_value el; napi_get_element(env, argv[1], i, &el);
|
|
159
|
+
void* data; size_t len;
|
|
160
|
+
napi_get_typedarray_info(env, el, NULL, &len, &data, NULL, NULL);
|
|
161
|
+
inPtrs[i] = (float*)data;
|
|
162
|
+
if (i == 0) numSamples = len;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* Extract output channel pointers */
|
|
167
|
+
uint32_t outCh;
|
|
168
|
+
napi_get_array_length(env, argv[2], &outCh);
|
|
169
|
+
for (uint32_t i = 0; i < outCh && i < 8; i++) {
|
|
170
|
+
napi_value el; napi_get_element(env, argv[2], i, &el);
|
|
171
|
+
void* data;
|
|
172
|
+
napi_get_typedarray_info(env, el, NULL, NULL, &data, NULL, NULL);
|
|
173
|
+
outPtrs[i] = (float*)data;
|
|
174
|
+
}
|
|
175
|
+
if (!numCh) numCh = outCh;
|
|
176
|
+
if (!numSamples) {
|
|
177
|
+
/* Get length from output if no input */
|
|
178
|
+
napi_value el; napi_get_element(env, argv[2], 0, &el);
|
|
179
|
+
size_t len; napi_get_typedarray_info(env, el, NULL, &len, NULL, NULL, NULL);
|
|
180
|
+
numSamples = len;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
vst3_process(p, hasInputs ? inPtrs : NULL, outPtrs, numCh, (int)numSamples);
|
|
184
|
+
return NULL;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* getState(handle) → Buffer */
|
|
188
|
+
static napi_value node_getState(napi_env env, napi_callback_info info) {
|
|
189
|
+
size_t argc = 1; napi_value argv[1];
|
|
190
|
+
napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
|
|
191
|
+
vst3_plugin_t* p; napi_get_value_external(env, argv[0], (void**)&p);
|
|
192
|
+
|
|
193
|
+
void* data; int size;
|
|
194
|
+
if (vst3_get_state(p, &data, &size) != 0) return NULL;
|
|
195
|
+
|
|
196
|
+
napi_value buf;
|
|
197
|
+
void* bufData;
|
|
198
|
+
napi_create_buffer_copy(env, size, data, &bufData, &buf);
|
|
199
|
+
vst3_free_state(data);
|
|
200
|
+
return buf;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* setState(handle, buffer) */
|
|
204
|
+
static napi_value node_setState(napi_env env, napi_callback_info info) {
|
|
205
|
+
size_t argc = 2; napi_value argv[2];
|
|
206
|
+
napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
|
|
207
|
+
vst3_plugin_t* p; napi_get_value_external(env, argv[0], (void**)&p);
|
|
208
|
+
|
|
209
|
+
void* data; size_t len;
|
|
210
|
+
napi_get_buffer_info(env, argv[1], &data, &len);
|
|
211
|
+
vst3_set_state(p, data, (int)len);
|
|
212
|
+
return NULL;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* Module init */
|
|
216
|
+
static napi_value init(napi_env env, napi_value exports) {
|
|
217
|
+
napi_property_descriptor props[] = {
|
|
218
|
+
{ "open", NULL, node_open, NULL, NULL, NULL, napi_default, NULL },
|
|
219
|
+
{ "close", NULL, node_close, NULL, NULL, NULL, napi_default, NULL },
|
|
220
|
+
{ "getName", NULL, node_getName, NULL, NULL, NULL, napi_default, NULL },
|
|
221
|
+
{ "getVendor", NULL, node_getVendor, NULL, NULL, NULL, napi_default, NULL },
|
|
222
|
+
{ "getChannels", NULL, node_getChannels, NULL, NULL, NULL, napi_default, NULL },
|
|
223
|
+
{ "getParamCount", NULL, node_getParamCount, NULL, NULL, NULL, napi_default, NULL },
|
|
224
|
+
{ "getParamInfo", NULL, node_getParamInfo, NULL, NULL, NULL, napi_default, NULL },
|
|
225
|
+
{ "getParam", NULL, node_getParam, NULL, NULL, NULL, napi_default, NULL },
|
|
226
|
+
{ "setParam", NULL, node_setParam, NULL, NULL, NULL, napi_default, NULL },
|
|
227
|
+
{ "process", NULL, node_process, NULL, NULL, NULL, napi_default, NULL },
|
|
228
|
+
{ "getState", NULL, node_getState, NULL, NULL, NULL, napi_default, NULL },
|
|
229
|
+
{ "setState", NULL, node_setState, NULL, NULL, NULL, napi_default, NULL },
|
|
230
|
+
};
|
|
231
|
+
napi_define_properties(env, exports, 12, props);
|
|
232
|
+
return exports;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
NAPI_MODULE(NODE_GYP_MODULE_NAME, init)
|
package/native/host.cpp
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* host.cpp — VST3 plugin host implementation
|
|
3
|
+
*/
|
|
4
|
+
#include "vst3.h"
|
|
5
|
+
#include "host.h"
|
|
6
|
+
#include <string>
|
|
7
|
+
#include <vector>
|
|
8
|
+
#include <cstring>
|
|
9
|
+
|
|
10
|
+
#ifdef _WIN32
|
|
11
|
+
#define WIN32_LEAN_AND_MEAN
|
|
12
|
+
#include <windows.h>
|
|
13
|
+
#else
|
|
14
|
+
#include <dlfcn.h>
|
|
15
|
+
#include <dirent.h>
|
|
16
|
+
#endif
|
|
17
|
+
|
|
18
|
+
#ifdef __APPLE__
|
|
19
|
+
#include <CoreFoundation/CoreFoundation.h>
|
|
20
|
+
#endif
|
|
21
|
+
|
|
22
|
+
using namespace Steinberg;
|
|
23
|
+
using namespace Steinberg::Vst;
|
|
24
|
+
|
|
25
|
+
/* Non-COM TUIDs for factory->createInstance */
|
|
26
|
+
static const TUID IID_IComponent = {
|
|
27
|
+
(int8)0xE8,0x31,(int8)0xFF,0x31, (int8)0xF2,(int8)0xD5,0x43,0x01,
|
|
28
|
+
(int8)0x92,(int8)0x8E,(int8)0xBB,(int8)0xEE, 0x25,0x69,0x78,0x02 };
|
|
29
|
+
static const TUID IID_FUnknown = {
|
|
30
|
+
0,0,0,0, 0,0,0,0, (int8)0xC0,0,0,0, 0,0,0,0x46 };
|
|
31
|
+
|
|
32
|
+
/* --- Cross-platform dynamic loading --- */
|
|
33
|
+
|
|
34
|
+
#ifdef _WIN32
|
|
35
|
+
static inline void* lib_open(const char* path) { return (void*)LoadLibraryA(path); }
|
|
36
|
+
static inline void* lib_sym(void* lib, const char* name) { return (void*)GetProcAddress((HMODULE)lib, name); }
|
|
37
|
+
static inline void lib_close(void* lib) { FreeLibrary((HMODULE)lib); }
|
|
38
|
+
static inline const char* lib_error() { static char buf[128]; snprintf(buf, sizeof(buf), "error %lu", GetLastError()); return buf; }
|
|
39
|
+
#else
|
|
40
|
+
static inline void* lib_open(const char* path) { return dlopen(path, RTLD_NOW); }
|
|
41
|
+
static inline void* lib_sym(void* lib, const char* name) { return dlsym(lib, name); }
|
|
42
|
+
static inline void lib_close(void* lib) { dlclose(lib); }
|
|
43
|
+
static inline const char* lib_error() { return dlerror(); }
|
|
44
|
+
#endif
|
|
45
|
+
|
|
46
|
+
static thread_local char s_error[512] = {0};
|
|
47
|
+
static void set_error(const char* fmt, const char* detail = "") {
|
|
48
|
+
snprintf(s_error, sizeof(s_error), "%s%s", fmt, detail);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* --- Host-side COM implementations --- */
|
|
52
|
+
|
|
53
|
+
class HostContext : public IHostApplication {
|
|
54
|
+
uint32 ref = 1;
|
|
55
|
+
public:
|
|
56
|
+
tresult queryInterface(const TUID _iid, void** obj) override {
|
|
57
|
+
if (tuid_match(_iid, UID_FUnknown) || tuid_match(_iid, UID_IHostApplication)) {
|
|
58
|
+
addRef(); *obj = this; return kResultOk;
|
|
59
|
+
}
|
|
60
|
+
*obj = nullptr; return kResultFalse;
|
|
61
|
+
}
|
|
62
|
+
uint32 addRef() override { return ++ref; }
|
|
63
|
+
uint32 release() override { if (--ref == 0) { delete this; return 0; } return ref; }
|
|
64
|
+
tresult getName(String128 name) override {
|
|
65
|
+
const char16 n[] = u"audio-host";
|
|
66
|
+
memcpy(name, n, sizeof(n));
|
|
67
|
+
return kResultOk;
|
|
68
|
+
}
|
|
69
|
+
tresult createInstance(const TUID, const TUID, void** obj) override {
|
|
70
|
+
*obj = nullptr; return kNotImplemented;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
class MemStream : public IBStream {
|
|
75
|
+
std::vector<uint8_t> buf;
|
|
76
|
+
int64 pos = 0;
|
|
77
|
+
uint32 ref = 1;
|
|
78
|
+
public:
|
|
79
|
+
tresult queryInterface(const TUID _iid, void** obj) override {
|
|
80
|
+
if (tuid_match(_iid, UID_FUnknown) || tuid_match(_iid, UID_IBStream)) {
|
|
81
|
+
addRef(); *obj = this; return kResultOk;
|
|
82
|
+
}
|
|
83
|
+
*obj = nullptr; return kResultFalse;
|
|
84
|
+
}
|
|
85
|
+
uint32 addRef() override { return ++ref; }
|
|
86
|
+
uint32 release() override { if (--ref == 0) { delete this; return 0; } return ref; }
|
|
87
|
+
tresult read(void* buffer, int32 numBytes, int32* numBytesRead) override {
|
|
88
|
+
int32 avail = (int32)buf.size() - (int32)pos;
|
|
89
|
+
int32 n = numBytes < avail ? numBytes : (avail > 0 ? avail : 0);
|
|
90
|
+
if (n > 0) memcpy(buffer, buf.data() + pos, n);
|
|
91
|
+
pos += n;
|
|
92
|
+
if (numBytesRead) *numBytesRead = n;
|
|
93
|
+
return kResultOk;
|
|
94
|
+
}
|
|
95
|
+
tresult write(void* buffer, int32 numBytes, int32* numBytesWritten) override {
|
|
96
|
+
if (pos + numBytes > (int64)buf.size()) buf.resize(pos + numBytes);
|
|
97
|
+
memcpy(buf.data() + pos, buffer, numBytes);
|
|
98
|
+
pos += numBytes;
|
|
99
|
+
if (numBytesWritten) *numBytesWritten = numBytes;
|
|
100
|
+
return kResultOk;
|
|
101
|
+
}
|
|
102
|
+
tresult seek(int64 p, int32 mode, int64* result) override {
|
|
103
|
+
if (mode == 0) pos = p;
|
|
104
|
+
else if (mode == 1) pos += p;
|
|
105
|
+
else pos = (int64)buf.size() + p;
|
|
106
|
+
if (pos < 0) pos = 0;
|
|
107
|
+
if (result) *result = pos;
|
|
108
|
+
return kResultOk;
|
|
109
|
+
}
|
|
110
|
+
tresult tell(int64* p) override { *p = pos; return kResultOk; }
|
|
111
|
+
const uint8_t* data() const { return buf.data(); }
|
|
112
|
+
size_t size() const { return buf.size(); }
|
|
113
|
+
void setData(const uint8_t* d, size_t s) { buf.assign(d, d + s); pos = 0; }
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/* --- Plugin handle --- */
|
|
117
|
+
|
|
118
|
+
struct vst3_plugin {
|
|
119
|
+
void* lib;
|
|
120
|
+
IPluginFactory* factory;
|
|
121
|
+
IComponent* component;
|
|
122
|
+
IAudioProcessor* processor;
|
|
123
|
+
IEditController* controller;
|
|
124
|
+
bool ctrlSeparate;
|
|
125
|
+
HostContext* ctx;
|
|
126
|
+
int32 inCh, outCh;
|
|
127
|
+
double sampleRate;
|
|
128
|
+
int32 blockSize;
|
|
129
|
+
char name[64];
|
|
130
|
+
char vendor[64];
|
|
131
|
+
#ifdef __APPLE__
|
|
132
|
+
CFBundleRef bundle;
|
|
133
|
+
#endif
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/* --- Helpers --- */
|
|
137
|
+
|
|
138
|
+
/* Find the shared library inside a .vst3 bundle */
|
|
139
|
+
static std::string findBinary(const char* bundlePath) {
|
|
140
|
+
std::string base(bundlePath);
|
|
141
|
+
while (!base.empty() && (base.back() == '/' || base.back() == '\\')) base.pop_back();
|
|
142
|
+
|
|
143
|
+
#ifdef _WIN32
|
|
144
|
+
/* Windows: Contents/x86_64-win/*.vst3 */
|
|
145
|
+
std::string archDir = base + "\\Contents\\x86_64-win";
|
|
146
|
+
WIN32_FIND_DATAA fd;
|
|
147
|
+
HANDLE h = FindFirstFileA((archDir + "\\*.vst3").c_str(), &fd);
|
|
148
|
+
if (h != INVALID_HANDLE_VALUE) {
|
|
149
|
+
std::string result = archDir + "\\" + fd.cFileName;
|
|
150
|
+
FindClose(h);
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
/* Flat .vst3 file (not a bundle) */
|
|
154
|
+
return base;
|
|
155
|
+
#else
|
|
156
|
+
/* macOS: Contents/MacOS/<binary> */
|
|
157
|
+
std::string macosDir = base + "/Contents/MacOS";
|
|
158
|
+
DIR* dir = opendir(macosDir.c_str());
|
|
159
|
+
if (!dir) return "";
|
|
160
|
+
std::string result;
|
|
161
|
+
struct dirent* entry;
|
|
162
|
+
while ((entry = readdir(dir))) {
|
|
163
|
+
if (entry->d_name[0] == '.') continue;
|
|
164
|
+
result = macosDir + "/" + entry->d_name;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
closedir(dir);
|
|
168
|
+
return result;
|
|
169
|
+
#endif
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
static std::string u16to8(const char16* s, int maxLen = 128) {
|
|
173
|
+
std::string r;
|
|
174
|
+
for (int i = 0; i < maxLen && s[i]; i++) {
|
|
175
|
+
char16 c = s[i];
|
|
176
|
+
if (c < 0x80) r += (char)c;
|
|
177
|
+
else if (c < 0x800) { r += (char)(0xC0 | (c >> 6)); r += (char)(0x80 | (c & 0x3F)); }
|
|
178
|
+
else { r += (char)(0xE0 | (c >> 12)); r += (char)(0x80 | ((c >> 6) & 0x3F)); r += (char)(0x80 | (c & 0x3F)); }
|
|
179
|
+
}
|
|
180
|
+
return r;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* --- C API --- */
|
|
184
|
+
|
|
185
|
+
extern "C" {
|
|
186
|
+
|
|
187
|
+
const char* vst3_get_error(void) { return s_error; }
|
|
188
|
+
|
|
189
|
+
vst3_plugin_t* vst3_open(const char* path, double sampleRate, int channels, int blockSize) {
|
|
190
|
+
s_error[0] = 0;
|
|
191
|
+
|
|
192
|
+
std::string dylib = findBinary(path);
|
|
193
|
+
if (dylib.empty()) { set_error("No binary in bundle: ", path); return nullptr; }
|
|
194
|
+
|
|
195
|
+
void* lib = lib_open(dylib.c_str());
|
|
196
|
+
if (!lib) { set_error("dlopen failed: ", lib_error()); return nullptr; }
|
|
197
|
+
|
|
198
|
+
#ifdef __APPLE__
|
|
199
|
+
CFBundleRef bundle = nullptr;
|
|
200
|
+
{
|
|
201
|
+
std::string bp(path);
|
|
202
|
+
while (!bp.empty() && bp.back() == '/') bp.pop_back();
|
|
203
|
+
CFURLRef url = CFURLCreateFromFileSystemRepresentation(
|
|
204
|
+
kCFAllocatorDefault, (const UInt8*)bp.c_str(), bp.size(), true);
|
|
205
|
+
if (url) { bundle = CFBundleCreate(kCFAllocatorDefault, url); CFRelease(url); }
|
|
206
|
+
}
|
|
207
|
+
typedef bool (*BundleEntryFn)(CFBundleRef);
|
|
208
|
+
auto bundleEntry = (BundleEntryFn)lib_sym(lib,"bundleEntry");
|
|
209
|
+
if (bundleEntry) bundleEntry(bundle);
|
|
210
|
+
#endif
|
|
211
|
+
|
|
212
|
+
typedef IPluginFactory* (*GetFactoryFn)();
|
|
213
|
+
auto getFactory = (GetFactoryFn)lib_sym(lib,"GetPluginFactory");
|
|
214
|
+
if (!getFactory) { set_error("No GetPluginFactory"); lib_close(lib); return nullptr; }
|
|
215
|
+
|
|
216
|
+
IPluginFactory* factory = getFactory();
|
|
217
|
+
if (!factory) { set_error("GetPluginFactory returned null"); lib_close(lib); return nullptr; }
|
|
218
|
+
|
|
219
|
+
PClassInfo ci;
|
|
220
|
+
int found = -1;
|
|
221
|
+
for (int32 i = 0; i < factory->countClasses(); i++) {
|
|
222
|
+
factory->getClassInfo(i, &ci);
|
|
223
|
+
if (strcmp(ci.category, "Audio Module Class") == 0) { found = i; break; }
|
|
224
|
+
}
|
|
225
|
+
if (found < 0) { set_error("No Audio Module Class"); factory->release(); lib_close(lib); return nullptr; }
|
|
226
|
+
factory->getClassInfo(found, &ci);
|
|
227
|
+
|
|
228
|
+
PFactoryInfo fi;
|
|
229
|
+
factory->getFactoryInfo(&fi);
|
|
230
|
+
|
|
231
|
+
IComponent* component = nullptr;
|
|
232
|
+
if (factory->createInstance(ci.cid, IID_IComponent, (void**)&component) != kResultOk || !component) {
|
|
233
|
+
set_error("createInstance IComponent failed");
|
|
234
|
+
factory->release(); lib_close(lib); return nullptr;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
HostContext* ctx = new HostContext();
|
|
238
|
+
if (component->initialize((FUnknown*)ctx) != kResultOk) {
|
|
239
|
+
set_error("IComponent::initialize failed");
|
|
240
|
+
component->release(); factory->release(); lib_close(lib); delete ctx; return nullptr;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
IAudioProcessor* processor = nullptr;
|
|
244
|
+
qi((FUnknown*)component, UID_IAudioProcessor, (void**)&processor);
|
|
245
|
+
if (!processor) {
|
|
246
|
+
set_error("No IAudioProcessor");
|
|
247
|
+
component->terminate(); component->release(); factory->release(); lib_close(lib); delete ctx; return nullptr;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* Get IEditController — may be same object or separate class */
|
|
251
|
+
IEditController* controller = nullptr;
|
|
252
|
+
bool ctrlSeparate = false;
|
|
253
|
+
if (qi((FUnknown*)component, UID_IEditController, (void**)&controller) != kResultOk || !controller) {
|
|
254
|
+
TUID ctrlCid;
|
|
255
|
+
if (component->getControllerClassId(ctrlCid) == kResultOk) {
|
|
256
|
+
FUnknown* ctrlObj = nullptr;
|
|
257
|
+
if (factory->createInstance(ctrlCid, IID_FUnknown, (void**)&ctrlObj) == kResultOk && ctrlObj) {
|
|
258
|
+
qi(ctrlObj, UID_IEditController, (void**)&controller);
|
|
259
|
+
if (controller) {
|
|
260
|
+
ctrlSeparate = true;
|
|
261
|
+
controller->initialize((FUnknown*)ctx);
|
|
262
|
+
/* Connect component ↔ controller via IConnectionPoint */
|
|
263
|
+
IConnectionPoint *compCP = nullptr, *ctrlCP = nullptr;
|
|
264
|
+
qi((FUnknown*)component, UID_IConnectionPoint, (void**)&compCP);
|
|
265
|
+
qi((FUnknown*)controller, UID_IConnectionPoint, (void**)&ctrlCP);
|
|
266
|
+
if (compCP && ctrlCP) { compCP->connect(ctrlCP); ctrlCP->connect(compCP); }
|
|
267
|
+
if (compCP) compCP->release();
|
|
268
|
+
if (ctrlCP) ctrlCP->release();
|
|
269
|
+
}
|
|
270
|
+
ctrlObj->release();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
ProcessSetup setup = { kRealtime, kSample32, blockSize, sampleRate };
|
|
276
|
+
processor->setupProcessing(setup);
|
|
277
|
+
|
|
278
|
+
int32 inCh = 0, outCh = 0;
|
|
279
|
+
if (component->getBusCount(kAudio, kInput) > 0) {
|
|
280
|
+
BusInfo bi; component->getBusInfo(kAudio, kInput, 0, bi);
|
|
281
|
+
inCh = bi.channelCount;
|
|
282
|
+
component->activateBus(kAudio, kInput, 0, true);
|
|
283
|
+
}
|
|
284
|
+
if (component->getBusCount(kAudio, kOutput) > 0) {
|
|
285
|
+
BusInfo bi; component->getBusInfo(kAudio, kOutput, 0, bi);
|
|
286
|
+
outCh = bi.channelCount;
|
|
287
|
+
component->activateBus(kAudio, kOutput, 0, true);
|
|
288
|
+
}
|
|
289
|
+
if (inCh == 0) inCh = channels;
|
|
290
|
+
if (outCh == 0) outCh = channels;
|
|
291
|
+
|
|
292
|
+
component->setActive(true);
|
|
293
|
+
processor->setProcessing(true);
|
|
294
|
+
|
|
295
|
+
auto* p = new vst3_plugin();
|
|
296
|
+
p->lib = lib; p->factory = factory; p->component = component;
|
|
297
|
+
p->processor = processor; p->controller = controller; p->ctrlSeparate = ctrlSeparate;
|
|
298
|
+
p->ctx = ctx; p->inCh = inCh; p->outCh = outCh;
|
|
299
|
+
p->sampleRate = sampleRate; p->blockSize = blockSize;
|
|
300
|
+
strncpy(p->name, ci.name, 63); p->name[63] = 0;
|
|
301
|
+
strncpy(p->vendor, fi.vendor, 63); p->vendor[63] = 0;
|
|
302
|
+
#ifdef __APPLE__
|
|
303
|
+
p->bundle = bundle;
|
|
304
|
+
#endif
|
|
305
|
+
return (vst3_plugin_t*)p;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
void vst3_close(vst3_plugin_t* handle) {
|
|
309
|
+
if (!handle) return;
|
|
310
|
+
auto* p = (vst3_plugin*)handle;
|
|
311
|
+
if (!p->lib) return; /* already closed */
|
|
312
|
+
|
|
313
|
+
if (p->processor) { p->processor->setProcessing(false); p->processor->release(); p->processor = nullptr; }
|
|
314
|
+
if (p->component) { p->component->setActive(false); p->component->terminate(); p->component->release(); p->component = nullptr; }
|
|
315
|
+
if (p->controller && p->ctrlSeparate) { p->controller->terminate(); p->controller->release(); }
|
|
316
|
+
p->controller = nullptr;
|
|
317
|
+
if (p->factory) { p->factory->release(); p->factory = nullptr; }
|
|
318
|
+
|
|
319
|
+
typedef bool (*BundleExitFn)();
|
|
320
|
+
auto bundleExit = (BundleExitFn)lib_sym(p->lib,"bundleExit");
|
|
321
|
+
if (bundleExit) bundleExit();
|
|
322
|
+
lib_close(p->lib);
|
|
323
|
+
p->lib = nullptr;
|
|
324
|
+
|
|
325
|
+
#ifdef __APPLE__
|
|
326
|
+
if (p->bundle) { CFRelease(p->bundle); p->bundle = nullptr; }
|
|
327
|
+
#endif
|
|
328
|
+
if (p->ctx) { p->ctx->release(); p->ctx = nullptr; }
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
void vst3_destroy(vst3_plugin_t* handle) {
|
|
332
|
+
if (!handle) return;
|
|
333
|
+
vst3_close(handle);
|
|
334
|
+
delete (vst3_plugin*)handle;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
void vst3_process(vst3_plugin_t* handle, float** inputs, float** outputs, int numChannels, int numSamples) {
|
|
338
|
+
auto* p = (vst3_plugin*)handle;
|
|
339
|
+
if (!p || !p->processor) return;
|
|
340
|
+
|
|
341
|
+
AudioBusBuffers inBus = { numChannels, 0, {} };
|
|
342
|
+
inBus.channelBuffers32 = inputs;
|
|
343
|
+
AudioBusBuffers outBus = { numChannels, 0, {} };
|
|
344
|
+
outBus.channelBuffers32 = outputs;
|
|
345
|
+
|
|
346
|
+
ProcessData data = {};
|
|
347
|
+
data.processMode = kRealtime;
|
|
348
|
+
data.symbolicSampleSize = kSample32;
|
|
349
|
+
data.numSamples = numSamples;
|
|
350
|
+
data.numInputs = inputs ? 1 : 0;
|
|
351
|
+
data.numOutputs = 1;
|
|
352
|
+
data.inputs = inputs ? &inBus : nullptr;
|
|
353
|
+
data.outputs = &outBus;
|
|
354
|
+
data.inputParameterChanges = nullptr;
|
|
355
|
+
data.outputParameterChanges = nullptr;
|
|
356
|
+
data.inputEvents = nullptr;
|
|
357
|
+
data.outputEvents = nullptr;
|
|
358
|
+
|
|
359
|
+
p->processor->process(data);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const char* vst3_get_name(vst3_plugin_t* h) { return h ? ((vst3_plugin*)h)->name : ""; }
|
|
363
|
+
const char* vst3_get_vendor(vst3_plugin_t* h) { return h ? ((vst3_plugin*)h)->vendor : ""; }
|
|
364
|
+
int vst3_get_channels(vst3_plugin_t* h, int dir) {
|
|
365
|
+
if (!h) return 0;
|
|
366
|
+
return dir == 0 ? ((vst3_plugin*)h)->inCh : ((vst3_plugin*)h)->outCh;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
int vst3_get_param_count(vst3_plugin_t* h) {
|
|
370
|
+
auto* p = (vst3_plugin*)h;
|
|
371
|
+
return (p && p->controller) ? p->controller->getParameterCount() : 0;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
int vst3_get_param_info(vst3_plugin_t* h, int index, vst3_param_info_t* out) {
|
|
375
|
+
auto* p = (vst3_plugin*)h;
|
|
376
|
+
if (!p || !p->controller) return -1;
|
|
377
|
+
ParameterInfo info;
|
|
378
|
+
if (p->controller->getParameterInfo(index, info) != kResultOk) return -1;
|
|
379
|
+
out->id = info.id;
|
|
380
|
+
std::string name = u16to8(info.title);
|
|
381
|
+
strncpy(out->name, name.c_str(), 255); out->name[255] = 0;
|
|
382
|
+
out->defaultValue = info.defaultNormalizedValue;
|
|
383
|
+
out->stepCount = info.stepCount;
|
|
384
|
+
out->min = p->controller->normalizedParamToPlain(info.id, 0.0);
|
|
385
|
+
out->max = p->controller->normalizedParamToPlain(info.id, 1.0);
|
|
386
|
+
return 0;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
double vst3_get_param(vst3_plugin_t* h, uint32_t id) {
|
|
390
|
+
auto* p = (vst3_plugin*)h;
|
|
391
|
+
return (p && p->controller) ? p->controller->getParamNormalized(id) : 0;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
void vst3_set_param(vst3_plugin_t* h, uint32_t id, double value) {
|
|
395
|
+
auto* p = (vst3_plugin*)h;
|
|
396
|
+
if (p && p->controller) p->controller->setParamNormalized(id, value);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
int vst3_get_state(vst3_plugin_t* h, void** data, int* size) {
|
|
400
|
+
auto* p = (vst3_plugin*)h;
|
|
401
|
+
if (!p || !p->component) return -1;
|
|
402
|
+
auto* s = new MemStream();
|
|
403
|
+
if (p->component->getState(s) != kResultOk) { s->release(); return -1; }
|
|
404
|
+
*size = (int)s->size();
|
|
405
|
+
*data = malloc(*size);
|
|
406
|
+
memcpy(*data, s->data(), *size);
|
|
407
|
+
s->release();
|
|
408
|
+
return 0;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
int vst3_set_state(vst3_plugin_t* h, const void* data, int size) {
|
|
412
|
+
auto* p = (vst3_plugin*)h;
|
|
413
|
+
if (!p || !p->component) return -1;
|
|
414
|
+
auto* s = new MemStream();
|
|
415
|
+
s->setData((const uint8_t*)data, size);
|
|
416
|
+
tresult r = p->component->setState(s);
|
|
417
|
+
s->release();
|
|
418
|
+
return r == kResultOk ? 0 : -1;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
void vst3_free_state(void* data) { free(data); }
|
|
422
|
+
|
|
423
|
+
} /* extern "C" */
|
package/native/host.h
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* host.h — C API for the VST3 host
|
|
3
|
+
*/
|
|
4
|
+
#pragma once
|
|
5
|
+
|
|
6
|
+
#ifdef __cplusplus
|
|
7
|
+
extern "C" {
|
|
8
|
+
#endif
|
|
9
|
+
|
|
10
|
+
#include <stdint.h>
|
|
11
|
+
|
|
12
|
+
typedef struct vst3_plugin vst3_plugin_t;
|
|
13
|
+
|
|
14
|
+
typedef struct {
|
|
15
|
+
uint32_t id;
|
|
16
|
+
char name[256];
|
|
17
|
+
double min;
|
|
18
|
+
double max;
|
|
19
|
+
double defaultValue;
|
|
20
|
+
int32_t stepCount;
|
|
21
|
+
} vst3_param_info_t;
|
|
22
|
+
|
|
23
|
+
vst3_plugin_t* vst3_open(const char* path, double sampleRate, int channels, int blockSize);
|
|
24
|
+
void vst3_close(vst3_plugin_t* handle); /* idempotent deactivate */
|
|
25
|
+
void vst3_destroy(vst3_plugin_t* handle); /* close + free */
|
|
26
|
+
void vst3_process(vst3_plugin_t* handle, float** inputs, float** outputs, int numChannels, int numSamples);
|
|
27
|
+
|
|
28
|
+
const char* vst3_get_name(vst3_plugin_t* handle);
|
|
29
|
+
const char* vst3_get_vendor(vst3_plugin_t* handle);
|
|
30
|
+
int vst3_get_channels(vst3_plugin_t* handle, int dir); /* 0=in, 1=out */
|
|
31
|
+
|
|
32
|
+
int vst3_get_param_count(vst3_plugin_t* handle);
|
|
33
|
+
int vst3_get_param_info(vst3_plugin_t* handle, int index, vst3_param_info_t* info);
|
|
34
|
+
double vst3_get_param(vst3_plugin_t* handle, uint32_t id);
|
|
35
|
+
void vst3_set_param(vst3_plugin_t* handle, uint32_t id, double value);
|
|
36
|
+
|
|
37
|
+
int vst3_get_state(vst3_plugin_t* handle, void** data, int* size);
|
|
38
|
+
int vst3_set_state(vst3_plugin_t* handle, const void* data, int size);
|
|
39
|
+
void vst3_free_state(void* data);
|
|
40
|
+
|
|
41
|
+
const char* vst3_get_error(void);
|
|
42
|
+
|
|
43
|
+
#ifdef __cplusplus
|
|
44
|
+
}
|
|
45
|
+
#endif
|
package/native/vst3.h
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* vst3.h — Minimal VST3 COM interface definitions for plugin hosting
|
|
3
|
+
*
|
|
4
|
+
* ABI-compatible reimplementation of Steinberg VST3 interfaces.
|
|
5
|
+
* https://github.com/steinbergmedia/vst3sdk
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
#pragma once
|
|
9
|
+
#include <cstdint>
|
|
10
|
+
#include <cstring>
|
|
11
|
+
|
|
12
|
+
namespace Steinberg {
|
|
13
|
+
|
|
14
|
+
typedef int32_t tresult;
|
|
15
|
+
typedef int8_t int8;
|
|
16
|
+
typedef int32_t int32;
|
|
17
|
+
typedef int64_t int64;
|
|
18
|
+
typedef uint32_t uint32;
|
|
19
|
+
typedef uint64_t uint64;
|
|
20
|
+
typedef char16_t char16;
|
|
21
|
+
typedef int8 TUID[16];
|
|
22
|
+
typedef char16 String128[128];
|
|
23
|
+
|
|
24
|
+
enum { kResultOk = 0, kResultFalse = 1, kInvalidArgument = 2,
|
|
25
|
+
kNotImplemented = 3, kInternalError = 4, kNotInitialized = 5 };
|
|
26
|
+
|
|
27
|
+
/* Build TUID at runtime — non-COM (Steinberg SDK) or COM (JUCE) byte order */
|
|
28
|
+
static inline void make_tuid(TUID out, uint32_t l1, uint32_t l2, uint32_t l3, uint32_t l4, bool com) {
|
|
29
|
+
if (!com) {
|
|
30
|
+
out[0]=(int8)(l1>>24); out[1]=(int8)(l1>>16); out[2]=(int8)(l1>>8); out[3]=(int8)l1;
|
|
31
|
+
out[4]=(int8)(l2>>24); out[5]=(int8)(l2>>16); out[6]=(int8)(l2>>8); out[7]=(int8)l2;
|
|
32
|
+
} else {
|
|
33
|
+
out[0]=(int8)l1; out[1]=(int8)(l1>>8); out[2]=(int8)(l1>>16); out[3]=(int8)(l1>>24);
|
|
34
|
+
out[4]=(int8)(l2>>16); out[5]=(int8)(l2>>24); out[6]=(int8)l2; out[7]=(int8)(l2>>8);
|
|
35
|
+
}
|
|
36
|
+
out[8]=(int8)(l3>>24); out[9]=(int8)(l3>>16); out[10]=(int8)(l3>>8); out[11]=(int8)l3;
|
|
37
|
+
out[12]=(int8)(l4>>24); out[13]=(int8)(l4>>16); out[14]=(int8)(l4>>8); out[15]=(int8)l4;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Match TUID against 4x uint32 in either byte order */
|
|
41
|
+
static inline bool tuid_match(const TUID a, uint32_t l1, uint32_t l2, uint32_t l3, uint32_t l4) {
|
|
42
|
+
TUID b;
|
|
43
|
+
make_tuid(b, l1, l2, l3, l4, false);
|
|
44
|
+
if (memcmp(a, b, 16) == 0) return true;
|
|
45
|
+
make_tuid(b, l1, l2, l3, l4, true);
|
|
46
|
+
return memcmp(a, b, 16) == 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Well-known interface IDs (byte-order-independent form) */
|
|
50
|
+
#define UID_FUnknown 0x00000000, 0x00000000, 0xC0000000, 0x00000046
|
|
51
|
+
#define UID_IComponent 0xE831FF31, 0xF2D54301, 0x928EBBEE, 0x25697802
|
|
52
|
+
#define UID_IAudioProcessor 0x42043F99, 0xB7DA453C, 0xA569E79D, 0x9AAEC33D
|
|
53
|
+
#define UID_IEditController 0xDCD7BBE3, 0x7742448D, 0xA874AACC, 0x979C759E
|
|
54
|
+
#define UID_IConnectionPoint 0x70A4156F, 0x6E6E4026, 0x989148BF, 0xAA60D8D1
|
|
55
|
+
#define UID_IHostApplication 0x58E595CC, 0xDB2D4369, 0xA2BF7201, 0x1B2B26B2
|
|
56
|
+
#define UID_IBStream 0xC3BF6EA2, 0x30994523, 0x9124FE83, 0x0C727026
|
|
57
|
+
|
|
58
|
+
/* --- Base interfaces --- */
|
|
59
|
+
|
|
60
|
+
class FUnknown {
|
|
61
|
+
public:
|
|
62
|
+
virtual tresult queryInterface(const TUID _iid, void** obj) = 0;
|
|
63
|
+
virtual uint32 addRef() = 0;
|
|
64
|
+
virtual uint32 release() = 0;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/* queryInterface trying both byte orders (Steinberg SDK vs JUCE) */
|
|
68
|
+
static inline tresult qi(FUnknown* obj, uint32_t l1, uint32_t l2, uint32_t l3, uint32_t l4, void** out) {
|
|
69
|
+
TUID id;
|
|
70
|
+
make_tuid(id, l1, l2, l3, l4, false);
|
|
71
|
+
tresult r = obj->queryInterface(id, out);
|
|
72
|
+
if (r == kResultOk && *out) return r;
|
|
73
|
+
make_tuid(id, l1, l2, l3, l4, true);
|
|
74
|
+
return obj->queryInterface(id, out);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
class IBStream : public FUnknown {
|
|
78
|
+
public:
|
|
79
|
+
virtual tresult read(void* buffer, int32 numBytes, int32* numBytesRead) = 0;
|
|
80
|
+
virtual tresult write(void* buffer, int32 numBytes, int32* numBytesWritten) = 0;
|
|
81
|
+
virtual tresult seek(int64 pos, int32 mode, int64* result) = 0;
|
|
82
|
+
virtual tresult tell(int64* pos) = 0;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
class IPluginBase : public FUnknown {
|
|
86
|
+
public:
|
|
87
|
+
virtual tresult initialize(FUnknown* context) = 0;
|
|
88
|
+
virtual tresult terminate() = 0;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/* --- Factory --- */
|
|
92
|
+
|
|
93
|
+
struct PClassInfo {
|
|
94
|
+
TUID cid;
|
|
95
|
+
int32 cardinality;
|
|
96
|
+
char category[32];
|
|
97
|
+
char name[64];
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
struct PFactoryInfo {
|
|
101
|
+
char vendor[64];
|
|
102
|
+
char url[256];
|
|
103
|
+
char email[128];
|
|
104
|
+
int32 flags;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
class IPluginFactory : public FUnknown {
|
|
108
|
+
public:
|
|
109
|
+
virtual tresult getFactoryInfo(PFactoryInfo* info) = 0;
|
|
110
|
+
virtual int32 countClasses() = 0;
|
|
111
|
+
virtual tresult getClassInfo(int32 index, PClassInfo* info) = 0;
|
|
112
|
+
virtual tresult createInstance(const TUID cid, const TUID iid, void** obj) = 0;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/* --- VST-specific --- */
|
|
116
|
+
|
|
117
|
+
namespace Vst {
|
|
118
|
+
|
|
119
|
+
enum MediaTypes { kAudio = 0, kEvent = 1 };
|
|
120
|
+
enum BusDirections { kInput = 0, kOutput = 1 };
|
|
121
|
+
enum { kSample32 = 0, kSample64 = 1, kRealtime = 0 };
|
|
122
|
+
typedef uint32 ParamID;
|
|
123
|
+
typedef double ParamValue;
|
|
124
|
+
|
|
125
|
+
struct BusInfo {
|
|
126
|
+
int32 mediaType;
|
|
127
|
+
int32 direction;
|
|
128
|
+
int32 channelCount;
|
|
129
|
+
String128 name;
|
|
130
|
+
int32 busType;
|
|
131
|
+
uint32 flags;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
struct ProcessSetup {
|
|
135
|
+
int32 processMode;
|
|
136
|
+
int32 symbolicSampleSize;
|
|
137
|
+
int32 maxSamplesPerBlock;
|
|
138
|
+
double sampleRate;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
struct AudioBusBuffers {
|
|
142
|
+
int32 numChannels;
|
|
143
|
+
uint64 silenceFlags;
|
|
144
|
+
union {
|
|
145
|
+
float** channelBuffers32;
|
|
146
|
+
double** channelBuffers64;
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
struct ProcessData {
|
|
151
|
+
int32 processMode;
|
|
152
|
+
int32 symbolicSampleSize;
|
|
153
|
+
int32 numSamples;
|
|
154
|
+
int32 numInputs;
|
|
155
|
+
int32 numOutputs;
|
|
156
|
+
AudioBusBuffers* inputs;
|
|
157
|
+
AudioBusBuffers* outputs;
|
|
158
|
+
void* inputParameterChanges;
|
|
159
|
+
void* outputParameterChanges;
|
|
160
|
+
void* inputEvents;
|
|
161
|
+
void* outputEvents;
|
|
162
|
+
void* processContext;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
struct ParameterInfo {
|
|
166
|
+
ParamID id;
|
|
167
|
+
String128 title;
|
|
168
|
+
String128 shortTitle;
|
|
169
|
+
String128 units;
|
|
170
|
+
int32 stepCount;
|
|
171
|
+
ParamValue defaultNormalizedValue;
|
|
172
|
+
int32 unitId;
|
|
173
|
+
int32 flags;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
class IComponent : public IPluginBase {
|
|
177
|
+
public:
|
|
178
|
+
virtual tresult getControllerClassId(TUID classId) = 0;
|
|
179
|
+
virtual tresult setIoMode(int32 mode) = 0;
|
|
180
|
+
virtual int32 getBusCount(int32 type, int32 dir) = 0;
|
|
181
|
+
virtual tresult getBusInfo(int32 type, int32 dir, int32 index, BusInfo& bus) = 0;
|
|
182
|
+
virtual tresult getRoutingInfo(void* inInfo, void* outInfo) = 0;
|
|
183
|
+
virtual tresult activateBus(int32 type, int32 dir, int32 index, bool state) = 0;
|
|
184
|
+
virtual tresult setActive(bool state) = 0;
|
|
185
|
+
virtual tresult setState(IBStream* state) = 0;
|
|
186
|
+
virtual tresult getState(IBStream* state) = 0;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
class IAudioProcessor : public FUnknown {
|
|
190
|
+
public:
|
|
191
|
+
virtual tresult setBusArrangements(uint64* inputs, int32 numIns, uint64* outputs, int32 numOuts) = 0;
|
|
192
|
+
virtual tresult getBusArrangement(int32 dir, int32 index, uint64& arr) = 0;
|
|
193
|
+
virtual tresult canProcessSampleSize(int32 symbolicSampleSize) = 0;
|
|
194
|
+
virtual uint32 getLatencySamples() = 0;
|
|
195
|
+
virtual tresult setupProcessing(ProcessSetup& setup) = 0;
|
|
196
|
+
virtual tresult setProcessing(bool state) = 0;
|
|
197
|
+
virtual tresult process(ProcessData& data) = 0;
|
|
198
|
+
virtual uint32 getTailSamples() = 0;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
class IEditController : public IPluginBase {
|
|
202
|
+
public:
|
|
203
|
+
virtual tresult setComponentState(IBStream* state) = 0;
|
|
204
|
+
virtual tresult setState(IBStream* state) = 0;
|
|
205
|
+
virtual tresult getState(IBStream* state) = 0;
|
|
206
|
+
virtual int32 getParameterCount() = 0;
|
|
207
|
+
virtual tresult getParameterInfo(int32 paramIndex, ParameterInfo& info) = 0;
|
|
208
|
+
virtual tresult getParamStringByValue(ParamID id, ParamValue valueNormalized, String128 string) = 0;
|
|
209
|
+
virtual tresult getParamValueByString(ParamID id, char16* string, ParamValue& valueNormalized) = 0;
|
|
210
|
+
virtual ParamValue normalizedParamToPlain(ParamID id, ParamValue valueNormalized) = 0;
|
|
211
|
+
virtual ParamValue plainParamToNormalized(ParamID id, ParamValue plainValue) = 0;
|
|
212
|
+
virtual ParamValue getParamNormalized(ParamID id) = 0;
|
|
213
|
+
virtual tresult setParamNormalized(ParamID id, ParamValue value) = 0;
|
|
214
|
+
virtual tresult setComponentHandler(FUnknown* handler) = 0;
|
|
215
|
+
virtual void* createView(const char* name) = 0;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
class IConnectionPoint : public FUnknown {
|
|
219
|
+
public:
|
|
220
|
+
virtual tresult connect(IConnectionPoint* other) = 0;
|
|
221
|
+
virtual tresult disconnect(IConnectionPoint* other) = 0;
|
|
222
|
+
virtual tresult notify(void* message) = 0;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
class IHostApplication : public FUnknown {
|
|
226
|
+
public:
|
|
227
|
+
virtual tresult getName(String128 name) = 0;
|
|
228
|
+
virtual tresult createInstance(const TUID cid, const TUID iid, void** obj) = 0;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
} // namespace Vst
|
|
232
|
+
} // namespace Steinberg
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@audio/host-vst",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "VST3 plugin host for audiojs",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.js",
|
|
7
|
+
"types": "./index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"default": "./index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"gypfile": true,
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "node-gyp rebuild",
|
|
17
|
+
"test": "node test.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"index.js",
|
|
21
|
+
"index.d.ts",
|
|
22
|
+
"src/",
|
|
23
|
+
"native/",
|
|
24
|
+
"binding.gyp"
|
|
25
|
+
],
|
|
26
|
+
"optionalDependencies": {
|
|
27
|
+
"@audio/host-vst-darwin-arm64": ">=0.1.0",
|
|
28
|
+
"@audio/host-vst-darwin-x64": ">=0.1.0",
|
|
29
|
+
"@audio/host-vst-linux-x64": ">=0.1.0",
|
|
30
|
+
"@audio/host-vst-linux-arm64": ">=0.1.0",
|
|
31
|
+
"@audio/host-vst-win32-x64": ">=0.1.0"
|
|
32
|
+
},
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"tst": "^9.4.0"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/addon.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NAPI addon loader — tries prebuilt, then local build
|
|
3
|
+
*/
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
5
|
+
import { join, dirname } from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { arch, platform } from 'node:os'
|
|
8
|
+
|
|
9
|
+
const require = createRequire(import.meta.url)
|
|
10
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
11
|
+
const plat = `${platform()}-${arch()}`
|
|
12
|
+
|
|
13
|
+
let addon
|
|
14
|
+
const loaders = [
|
|
15
|
+
() => require(`@audio/host-vst-${plat}`),
|
|
16
|
+
() => require(join(root, 'packages', `host-vst-${plat}`, 'host_vst.node')),
|
|
17
|
+
() => require(join(root, 'prebuilds', plat, 'host_vst.node')),
|
|
18
|
+
() => require(join(root, 'build', 'Release', 'host_vst.node')),
|
|
19
|
+
() => require(join(root, 'build', 'Debug', 'host_vst.node')),
|
|
20
|
+
]
|
|
21
|
+
for (const load of loaders) {
|
|
22
|
+
try { addon = load(); break } catch {}
|
|
23
|
+
}
|
|
24
|
+
if (!addon) throw new Error('host-vst addon not found — run `npm run build` or install @audio/host-vst-' + plat)
|
|
25
|
+
|
|
26
|
+
export default addon
|