@soundor/vst3-host 0.0.0-stage → 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.
Files changed (54) hide show
  1. package/CMakeLists.txt +194 -0
  2. package/CMakePresets.json +115 -0
  3. package/LICENSE +21 -0
  4. package/NOTICE.md +17 -0
  5. package/README.md +40 -2
  6. package/cmake/Vst3HostDependencies.cmake +179 -0
  7. package/dist/cli.d.mts +1 -0
  8. package/dist/cli.mjs +88 -0
  9. package/dist/cli.mjs.map +1 -0
  10. package/dist/index.d.mts +586 -0
  11. package/dist/index.d.mts.map +1 -0
  12. package/dist/index.mjs +1 -0
  13. package/dist/prepare-ayjTVhfk.mjs +1149 -0
  14. package/dist/prepare-ayjTVhfk.mjs.map +1 -0
  15. package/native/src/Bundle.cpp +253 -0
  16. package/native/src/Bundle.h +32 -0
  17. package/native/src/EditorArea.h +92 -0
  18. package/native/src/EditorAreaMac.mm +52 -0
  19. package/native/src/EditorAreaWindows.cpp +66 -0
  20. package/native/src/EditorAreaX11.cpp +512 -0
  21. package/native/src/EditorWindow.h +72 -0
  22. package/native/src/EditorWindowMac.mm +134 -0
  23. package/native/src/EditorWindowWindows.cpp +148 -0
  24. package/native/src/EventLoop.h +50 -0
  25. package/native/src/EventLoopLinux.cpp +204 -0
  26. package/native/src/EventLoopMac.mm +146 -0
  27. package/native/src/EventLoopWindows.cpp +132 -0
  28. package/native/src/HostContext.cpp +217 -0
  29. package/native/src/HostContext.h +116 -0
  30. package/native/src/HostInterface.cpp +1026 -0
  31. package/native/src/HostInterface.h +138 -0
  32. package/native/src/HostWindow.cpp +901 -0
  33. package/native/src/HostWindow.h +187 -0
  34. package/native/src/Ipc.cpp +158 -0
  35. package/native/src/Ipc.h +91 -0
  36. package/native/src/Json.cpp +426 -0
  37. package/native/src/Json.h +85 -0
  38. package/native/src/LiveAudio.cpp +416 -0
  39. package/native/src/LiveAudio.h +121 -0
  40. package/native/src/Main.cpp +178 -0
  41. package/native/src/Paths.h +20 -0
  42. package/native/src/Plugin.cpp +1865 -0
  43. package/native/src/Plugin.h +321 -0
  44. package/native/src/Session.cpp +656 -0
  45. package/native/src/Session.h +82 -0
  46. package/native/src/Settings.cpp +93 -0
  47. package/native/src/Settings.h +23 -0
  48. package/native/src/Version.h.in +11 -0
  49. package/native/src/X11Library.cpp +127 -0
  50. package/native/src/X11Library.h +83 -0
  51. package/native/tools/Embed.cpp +49 -0
  52. package/package.json +58 -3
  53. package/protocol.json +7 -0
  54. package/targets.json +33 -0
@@ -0,0 +1,1149 @@
1
+ import { createRequire } from "node:module";
2
+ import { spawn, spawnSync } from "node:child_process";
3
+ import { EventEmitter } from "node:events";
4
+ import { endianness, homedir } from "node:os";
5
+ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
6
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
+ import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
8
+ import { fileURLToPath } from "node:url";
9
+ import { gunzipSync } from "node:zlib";
10
+ // src/errors.ts
11
+ var Vst3HostError = class extends Error {
12
+ name = "Vst3HostError";
13
+ code;
14
+ details;
15
+ constructor(code, message, details, options) {
16
+ super(message, options);
17
+ this.code = code;
18
+ this.details = details;
19
+ }
20
+ };
21
+ // protocol.json
22
+ var version = 1;
23
+ var magic = "SVH1";
24
+ var maxHeaderBytes = 1048576;
25
+ var defaultMaxPayloadBytes = 268435456;
26
+ // src/protocol.ts
27
+ const PROTOCOL_VERSION = version;
28
+ const MAX_HEADER_BYTES = maxHeaderBytes;
29
+ const DEFAULT_MAX_PAYLOAD_BYTES = defaultMaxPayloadBytes;
30
+ const MAGIC = Buffer.from(magic, "latin1");
31
+ const PREFIX_BYTES = 12;
32
+ function encodeFrame(header, payload = /* @__PURE__ */ new Uint8Array(0)) {
33
+ const json = Buffer.from(JSON.stringify(header), "utf8");
34
+ const prefix = Buffer.alloc(PREFIX_BYTES);
35
+ MAGIC.copy(prefix, 0);
36
+ prefix.writeUInt32LE(json.length, 4);
37
+ prefix.writeUInt32LE(payload.byteLength, 8);
38
+ return payload.byteLength > 0 ? [
39
+ prefix,
40
+ json,
41
+ Buffer.from(payload.buffer, payload.byteOffset, payload.byteLength)
42
+ ] : [prefix, json];
43
+ }
44
+ /** Turns a byte stream into frames; throws on a corrupt or oversized one,
45
+ after which the stream cannot be read any further. */
46
+ var FrameDecoder = class {
47
+ #chunks = [];
48
+ #buffered = 0;
49
+ #pending;
50
+ maxPayloadBytes;
51
+ constructor(maxPayloadBytes = DEFAULT_MAX_PAYLOAD_BYTES) {
52
+ this.maxPayloadBytes = maxPayloadBytes;
53
+ }
54
+ push(chunk) {
55
+ this.#chunks.push(chunk);
56
+ this.#buffered += chunk.length;
57
+ const frames = [];
58
+ while (true) {
59
+ if (this.#pending === void 0) {
60
+ if (this.#buffered < PREFIX_BYTES) break;
61
+ const prefix = this.#take(PREFIX_BYTES);
62
+ if (!prefix.subarray(0, 4).equals(MAGIC)) throw new Error("the host wrote something that is not a protocol frame (bad magic)");
63
+ const header = prefix.readUInt32LE(4);
64
+ const payload = prefix.readUInt32LE(8);
65
+ if (header > MAX_HEADER_BYTES) throw new Error(`a frame header of ${header} bytes exceeds the limit of ${MAX_HEADER_BYTES}`);
66
+ if (payload > this.maxPayloadBytes) throw new Error(`a frame payload of ${payload} bytes exceeds the limit of ${this.maxPayloadBytes}`);
67
+ this.#pending = {
68
+ header,
69
+ payload
70
+ };
71
+ }
72
+ const { header, payload } = this.#pending;
73
+ if (this.#buffered < header + payload) break;
74
+ this.#pending = void 0;
75
+ const text = this.#take(header).toString("utf8");
76
+ let parsed;
77
+ try {
78
+ parsed = JSON.parse(text);
79
+ } catch {
80
+ throw new Error("the host wrote a frame whose header is not JSON");
81
+ }
82
+ frames.push({
83
+ header: parsed,
84
+ payload: this.#take(payload)
85
+ });
86
+ }
87
+ return frames;
88
+ }
89
+ #take(size) {
90
+ if (size === 0) return Buffer.alloc(0);
91
+ const first = this.#chunks[0];
92
+ let result;
93
+ if (first.length >= size) {
94
+ result = first.subarray(0, size);
95
+ if (first.length === size) this.#chunks.shift();
96
+ else this.#chunks[0] = first.subarray(size);
97
+ } else {
98
+ result = Buffer.alloc(size);
99
+ let offset = 0;
100
+ while (offset < size) {
101
+ const chunk = this.#chunks[0];
102
+ const part = Math.min(chunk.length, size - offset);
103
+ chunk.copy(result, offset, 0, part);
104
+ offset += part;
105
+ if (part === chunk.length) this.#chunks.shift();
106
+ else this.#chunks[0] = chunk.subarray(part);
107
+ }
108
+ }
109
+ this.#buffered -= size;
110
+ return result;
111
+ }
112
+ };
113
+ // src/targets.ts
114
+ /** The platform packages a release generates (targets.json). */
115
+ const targets = [
116
+ {
117
+ "target": "linux-x64-gnu",
118
+ "package": "@soundor/vst3-host-linux-x64-gnu",
119
+ "os": ["linux"],
120
+ "cpu": ["x64"],
121
+ "libc": ["glibc"],
122
+ "runner": "ubuntu-24.04",
123
+ "architectures": ["x64"],
124
+ "executable": "bin/soundor-vst3-host"
125
+ },
126
+ {
127
+ "target": "darwin-universal",
128
+ "package": "@soundor/vst3-host-darwin-universal",
129
+ "os": ["darwin"],
130
+ "cpu": ["arm64", "x64"],
131
+ "runner": "macos-15",
132
+ "architectures": ["arm64", "x64"],
133
+ "executable": "bin/soundor-vst3-host"
134
+ },
135
+ {
136
+ "target": "win32-x64",
137
+ "package": "@soundor/vst3-host-win32-x64",
138
+ "os": ["win32"],
139
+ "cpu": ["x64"],
140
+ "runner": "windows-2025",
141
+ "architectures": ["x64"],
142
+ "executable": "bin/soundor-vst3-host.exe"
143
+ }
144
+ ];
145
+ /** This package's root: the directory of its package.json. */
146
+ const packageRoot = fileURLToPath(new URL("..", import.meta.url));
147
+ function ownManifest(root = packageRoot) {
148
+ return JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
149
+ }
150
+ /** A checkout of the repository, not an installed package: the published
151
+ manifest always pins its platform packages. */
152
+ function isSourceWorkspace(root = packageRoot) {
153
+ return ownManifest(root).optionalDependencies === void 0 && existsSync(join(root, "native/src"));
154
+ }
155
+ function currentPlatform() {
156
+ if (process.platform !== "linux") return {
157
+ platform: process.platform,
158
+ arch: process.arch
159
+ };
160
+ const header = process.report.getReport().header;
161
+ return header?.glibcVersionRuntime ? {
162
+ platform: "linux",
163
+ arch: process.arch,
164
+ libc: "glibc",
165
+ glibc: header.glibcVersionRuntime
166
+ } : {
167
+ platform: "linux",
168
+ arch: process.arch,
169
+ libc: "musl"
170
+ };
171
+ }
172
+ /** The target whose executable runs here, if any. */
173
+ function targetFor(platform = currentPlatform()) {
174
+ return targets.find((target) => target.os.includes(platform.platform) && target.cpu.includes(platform.arch) && (target.libc === void 0 || target.libc.includes(platform.libc ?? "")));
175
+ }
176
+ function describePlatform(platform = currentPlatform()) {
177
+ return `${platform.platform}-${platform.arch}${platform.libc ? `-${platform.libc}` : ""}`;
178
+ }
179
+ /** Version comparison of dotted numbers ("2.35" < "2.39"). */
180
+ function versionAtLeast(actual, required) {
181
+ const a = actual.split(".").map(Number);
182
+ const b = required.split(".").map(Number);
183
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
184
+ const x = a[i] ?? 0;
185
+ const y = b[i] ?? 0;
186
+ if (x !== y) return x > y;
187
+ }
188
+ return true;
189
+ }
190
+ // src/inputs.ts
191
+ /** Files, relative to the package root. Directories are walked. */
192
+ const INPUTS = [
193
+ "CMakeLists.txt",
194
+ "CMakePresets.json",
195
+ "protocol.json",
196
+ "cmake",
197
+ "native/src",
198
+ "native/tools"
199
+ ];
200
+ const TEXT = /\.(?:cmake|cpp|c|h|hpp|mm|m|in|json|txt)$/;
201
+ const IGNORED = /(?:^\.|~$|\.(?:swp|tmp|orig|rej)$)/;
202
+ function nativeInputs(root = packageRoot) {
203
+ const files = [];
204
+ const visit = (relative) => {
205
+ const path = join(root, relative);
206
+ let entries;
207
+ try {
208
+ entries = readdirSync(path, { withFileTypes: true });
209
+ } catch (error) {
210
+ if (error.code === "ENOTDIR") {
211
+ files.push(relative);
212
+ return;
213
+ }
214
+ throw error;
215
+ }
216
+ for (const entry of entries) {
217
+ if (IGNORED.test(entry.name)) continue;
218
+ const child = join(relative, entry.name);
219
+ if (entry.isDirectory()) visit(child);
220
+ else if (entry.isFile()) files.push(child);
221
+ else if (entry.isSymbolicLink()) throw new Error(`A native input is a symbolic link: ${child}`);
222
+ }
223
+ };
224
+ for (const input of INPUTS) visit(input);
225
+ const normalized = files.map((file) => file.split(sep).join("/")).sort();
226
+ const { version } = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
227
+ const hash = createHash("sha256").update(`soundor-vst3-host-inputs-v1\0${version}\0`);
228
+ for (const file of normalized) {
229
+ const bytes = readFileSync(join(root, file));
230
+ const content = TEXT.test(file) ? Buffer.from(bytes.toString("utf8").replaceAll("\r\n", "\n")) : bytes;
231
+ hash.update(file).update("\0");
232
+ hash.update(createHash("sha256").update(content).digest()).update("\0");
233
+ }
234
+ return {
235
+ fingerprint: hash.digest("hex"),
236
+ files: normalized,
237
+ version
238
+ };
239
+ }
240
+ // src/provenance.ts
241
+ const PROVENANCE_FILE = "soundor-vst3-host.json";
242
+ function sha256(bytes) {
243
+ return createHash("sha256").update(bytes).digest("hex");
244
+ }
245
+ function sha256File(path) {
246
+ return sha256(readFileSync(path));
247
+ }
248
+ /** The problems with a provenance record, empty when it is well-formed. */
249
+ function provenanceProblems(value) {
250
+ const p = value;
251
+ const problems = [];
252
+ if (p === null || typeof p !== "object") return ["it is not an object"];
253
+ if (p.format !== 1) problems.push(`format is ${p.format}, not 1`);
254
+ if (p.name !== "@soundor/vst3-host") problems.push(`name is ${p.name}`);
255
+ if (typeof p.version !== "string") problems.push("version is missing");
256
+ if (typeof p.protocol !== "number") problems.push("protocol is missing");
257
+ if (typeof p.target !== "string") problems.push("target is missing");
258
+ if (!Array.isArray(p.architectures)) problems.push("architectures are missing");
259
+ if (p.configuration !== "Release") problems.push(`configuration is ${p.configuration}, not Release`);
260
+ if (p.sourceCommit !== null && !/^[0-9a-f]{40}$/.test(p.sourceCommit ?? "")) problems.push("sourceCommit is not a full commit hash");
261
+ if (!/^[0-9a-f]{64}$/.test(p.sourceFingerprint ?? "")) problems.push("sourceFingerprint is not a SHA-256");
262
+ if (typeof p.executable?.path !== "string" || !/^[0-9a-f]{64}$/.test(p.executable.sha256 ?? "") || typeof p.executable.size !== "number") problems.push("executable is not { path, sha256, size }");
263
+ return problems;
264
+ }
265
+ function checkedProvenance(value, where) {
266
+ const problems = provenanceProblems(value);
267
+ if (problems.length) throw new Error(`${where} is not a valid host provenance record: ${problems.join("; ")}`);
268
+ return value;
269
+ }
270
+ // src/resolve.ts
271
+ function selectionFile(root = packageRoot) {
272
+ return join(root, "build", "selected.json");
273
+ }
274
+ const verified = /* @__PURE__ */ new Map();
275
+ function verifyExecutable(path, sha256, what) {
276
+ if (verified.get(path) === sha256) return;
277
+ if (!existsSync(path)) throw new Vst3HostError("executable-not-found", `${what}: ${path} does not exist`);
278
+ if (sha256File(path) !== sha256) throw new Vst3HostError("incompatible-host", `${what}: ${path} is not the executable its record describes (SHA-256 differs)`);
279
+ verified.set(path, sha256);
280
+ }
281
+ function resolveHost(options = {}) {
282
+ const root = options.root ?? packageRoot;
283
+ const explicit = options.executable ?? process.env.SOUNDOR_VST3_HOST_EXECUTABLE;
284
+ if (explicit) {
285
+ const executable = resolve(explicit);
286
+ if (!existsSync(executable)) throw new Vst3HostError("executable-not-found", `the VST3 host executable ${executable} does not exist`);
287
+ return {
288
+ executable,
289
+ source: "explicit",
290
+ reason: options.executable ? "given as `executable`" : "SOUNDOR_VST3_HOST_EXECUTABLE"
291
+ };
292
+ }
293
+ const platform = currentPlatform();
294
+ const target = targetFor(platform);
295
+ if (!target) throw new Vst3HostError("unsupported-platform", `@soundor/vst3-host has no executable for ${describePlatform(platform)}; it ships for Linux x64 (glibc), macOS (arm64 and x64) and Windows x64`);
296
+ const manifest = ownManifest(root);
297
+ if (isSourceWorkspace(root)) {
298
+ const file = selectionFile(root);
299
+ if (!existsSync(file)) throw new Vst3HostError("executable-not-found", "no VST3 host executable is selected in this checkout: run `pnpm --filter @soundor/vst3-host build:native` (prepareHost()) first");
300
+ const selection = JSON.parse(readFileSync(file, "utf8"));
301
+ const { fingerprint } = nativeInputs(root);
302
+ if (selection.sourceFingerprint !== fingerprint || selection.version !== manifest.version) throw new Vst3HostError("executable-not-found", "the selected VST3 host executable was built from other native sources than this checkout has: run `pnpm --filter @soundor/vst3-host build:native` again", {
303
+ selected: selection.sourceFingerprint,
304
+ current: fingerprint
305
+ });
306
+ const executable = isAbsolute(selection.executable) ? selection.executable : join(root, selection.executable);
307
+ verifyExecutable(executable, selection.sha256, "the selected VST3 host");
308
+ return {
309
+ executable,
310
+ source: "selection",
311
+ reason: selection.reason
312
+ };
313
+ }
314
+ let packageJson;
315
+ try {
316
+ packageJson = createRequire(join(root, "package.json")).resolve(`${target.package}/package.json`);
317
+ } catch {
318
+ throw new Vst3HostError("platform-package-missing", `${target.package} is not installed. npm or pnpm installs it with @soundor/vst3-host as an optional dependency for ${describePlatform(platform)}; it is missing when optional dependencies are omitted (--no-optional, --omit=optional) or node_modules came from another platform. Reinstall with optional dependencies.`, {
319
+ package: target.package,
320
+ version: manifest.version
321
+ });
322
+ }
323
+ const directory = dirname(packageJson);
324
+ const platformManifest = JSON.parse(readFileSync(packageJson, "utf8"));
325
+ const provenance = checkedProvenance(JSON.parse(readFileSync(join(directory, PROVENANCE_FILE), "utf8")), `${target.package}/${PROVENANCE_FILE}`);
326
+ if (platformManifest.version !== manifest.version || provenance.version !== manifest.version) throw new Vst3HostError("incompatible-host", `${target.package}@${platformManifest.version} is installed; @soundor/vst3-host@${manifest.version} needs exactly ${manifest.version}. Reinstall so both match.`);
327
+ if (provenance.protocol !== PROTOCOL_VERSION || provenance.target !== target.target) throw new Vst3HostError("incompatible-host", `${target.package} holds a ${provenance.target} executable for protocol ${provenance.protocol}; this client needs ${target.target} and protocol ${PROTOCOL_VERSION}`);
328
+ if (provenance.glibc && platform.glibc && !versionAtLeast(platform.glibc, provenance.glibc)) throw new Vst3HostError("unsupported-platform", `the VST3 host needs glibc ${provenance.glibc} or newer; this system has ${platform.glibc}`);
329
+ const executable = join(directory, target.executable);
330
+ verifyExecutable(executable, provenance.executable.sha256, target.package);
331
+ return {
332
+ executable,
333
+ source: "package",
334
+ reason: `${target.package}@${platformManifest.version}`,
335
+ provenance
336
+ };
337
+ }
338
+ // src/host.ts
339
+ function audioParams(settings) {
340
+ return {
341
+ input: settings.input,
342
+ file: settings.file,
343
+ muted: settings.muted,
344
+ volume: settings.volume
345
+ };
346
+ }
347
+ /**
348
+ * The one parameter with this title, or `unknown-parameter`. A JUCE plugin with
349
+ * a Bypass of its own also has the bypass JUCE adds for hosts, with the same
350
+ * title and the bypass flag: the title means the plugin's own.
351
+ */
352
+ function parameterTitled(parameters, title) {
353
+ let found = parameters.filter((info) => info.title === title);
354
+ if (found.length > 1) {
355
+ const own = found.filter((info) => !info.flags.bypass);
356
+ if (own.length === 1) found = own;
357
+ }
358
+ if (found.length === 1) return found[0];
359
+ throw new Vst3HostError("unknown-parameter", found.length === 0 ? `No parameter is titled "${title}"; the plugin has: ${parameters.map((info) => `"${info.title}"`).join(", ")}` : `${found.length} parameters are titled "${title}"; use getParameters() and their ids`);
360
+ }
361
+ const live = /* @__PURE__ */ new Set();
362
+ let exitHookInstalled = false;
363
+ function track(child) {
364
+ live.add(child);
365
+ child.once("exit", () => live.delete(child));
366
+ if (!exitHookInstalled) {
367
+ exitHookInstalled = true;
368
+ process.once("exit", () => {
369
+ for (const host of live) host.kill("SIGKILL");
370
+ });
371
+ }
372
+ }
373
+ const LITTLE_ENDIAN = endianness() === "LE";
374
+ /** Starts a host process, loads the plugin and resolves once it is ready. */
375
+ async function launchHost(options) {
376
+ if (!LITTLE_ENDIAN) throw new Vst3HostError("unsupported-platform", "the host protocol carries little-endian audio");
377
+ const executable = options.executable ?? resolveHost().executable;
378
+ const maxPayloadBytes = options.maxPayloadBytes ?? DEFAULT_MAX_PAYLOAD_BYTES;
379
+ const args = [
380
+ options.editor ? "--ui" : "--headless",
381
+ "--max-payload",
382
+ String(maxPayloadBytes)
383
+ ];
384
+ const env = {
385
+ ...options.inheritEnv === false ? {} : process.env,
386
+ ...options.env
387
+ };
388
+ for (const key of Object.keys(env)) if (env[key] === void 0) delete env[key];
389
+ const child = spawn(executable, args, {
390
+ cwd: options.cwd,
391
+ env,
392
+ detached: options.detached ?? false,
393
+ stdio: [
394
+ "pipe",
395
+ "pipe",
396
+ "pipe"
397
+ ],
398
+ windowsHide: true
399
+ });
400
+ track(child);
401
+ const host = new Vst3Host(child, {
402
+ maxPayloadBytes,
403
+ requestTimeout: options.requestTimeout ?? 6e4,
404
+ onLog: options.onLog,
405
+ executable
406
+ });
407
+ const startTimeout = options.startTimeout ?? 3e4;
408
+ try {
409
+ await host.ready(startTimeout, options.signal);
410
+ await host.load(options, startTimeout, options.signal);
411
+ return host;
412
+ } catch (error) {
413
+ await host.close();
414
+ throw error;
415
+ }
416
+ }
417
+ /** A running host with one plugin loaded. Requests run one at a time, in
418
+ the order they are made. */
419
+ var Vst3Host = class {
420
+ #child;
421
+ #options;
422
+ #events = new EventEmitter();
423
+ #decoder;
424
+ #queue = [];
425
+ #current;
426
+ #nextId = 1;
427
+ #stderr = "";
428
+ #partialLine = "";
429
+ #exit;
430
+ #exited;
431
+ #failure;
432
+ #closing;
433
+ #closeRequested = false;
434
+ #hello;
435
+ #identity;
436
+ #info;
437
+ /** @internal Use launchHost(). */
438
+ constructor(child, options) {
439
+ this.#child = child;
440
+ this.#options = options;
441
+ this.#decoder = new FrameDecoder(options.maxPayloadBytes);
442
+ this.#events.setMaxListeners(0);
443
+ this.#exited = new Promise((resolve) => {
444
+ child.once("exit", (code, signal) => {
445
+ this.#exit = {
446
+ code,
447
+ signal
448
+ };
449
+ this.#fail(this.#closeRequested ? new Vst3HostError("closed", "the VST3 host was closed") : new Vst3HostError("host-exited", `the VST3 host exited (${signal ? `signal ${signal}` : `code ${code}`})${this.#stderrTail()}`, {
450
+ code,
451
+ signal,
452
+ stderr: this.#stderr.slice(-4096)
453
+ }));
454
+ this.#events.emit("exit", {
455
+ code,
456
+ signal
457
+ });
458
+ resolve({
459
+ code,
460
+ signal
461
+ });
462
+ });
463
+ child.once("error", (error) => {
464
+ if (child.pid !== void 0) return;
465
+ this.#fail(new Vst3HostError("executable-not-found", `the VST3 host could not be started from ${options.executable}: ${error.message}`, { executable: options.executable }, { cause: error }));
466
+ this.#exit = {
467
+ code: null,
468
+ signal: null
469
+ };
470
+ resolve(this.#exit);
471
+ });
472
+ });
473
+ child.stdout.on("data", (chunk) => {
474
+ let frames;
475
+ try {
476
+ frames = this.#decoder.push(chunk);
477
+ } catch (error) {
478
+ this.#fail(new Vst3HostError("protocol-error", error.message));
479
+ child.kill("SIGKILL");
480
+ return;
481
+ }
482
+ for (const frame of frames) this.#receive(frame);
483
+ });
484
+ child.stderr.setEncoding("utf8");
485
+ child.stderr.on("data", (text) => {
486
+ this.#stderr = (this.#stderr + text).slice(-65536);
487
+ if (!this.#options.onLog) return;
488
+ const lines = (this.#partialLine + text).split(/\r?\n/);
489
+ this.#partialLine = lines.pop() ?? "";
490
+ for (const line of lines) this.#options.onLog(line);
491
+ });
492
+ child.stdin.on("error", () => {});
493
+ }
494
+ /** The host process's id. */
495
+ get pid() {
496
+ return this.#child.pid;
497
+ }
498
+ /** What the host reported when it started. */
499
+ get identity() {
500
+ return this.#identity;
501
+ }
502
+ /** The loaded plugin, as of launch or the last recreate/configure. */
503
+ get info() {
504
+ return this.#info;
505
+ }
506
+ /** Resolves when the process has ended. */
507
+ get exited() {
508
+ return this.#exited;
509
+ }
510
+ /** The end of what the host and the plugin wrote to stderr. */
511
+ get diagnostics() {
512
+ return this.#stderr;
513
+ }
514
+ on(event, listener) {
515
+ this.#events.on(event, listener);
516
+ return this;
517
+ }
518
+ off(event, listener) {
519
+ this.#events.off(event, listener);
520
+ return this;
521
+ }
522
+ once(event, listener) {
523
+ this.#events.once(event, listener);
524
+ return this;
525
+ }
526
+ /** @internal */
527
+ async ready(timeout, signal) {
528
+ const identity = await this.#withTimeout(new Promise((resolve, reject) => {
529
+ this.#hello = resolve;
530
+ this.#exited.then(() => reject(this.#failure));
531
+ }), timeout, signal, "start");
532
+ if (identity.protocol !== PROTOCOL_VERSION) throw new Vst3HostError("incompatible-host", `${this.#options.executable} speaks protocol ${identity.protocol}; this client speaks ${PROTOCOL_VERSION}. Use the executable of the same @soundor/vst3-host version.`, { ...identity });
533
+ this.#identity = identity;
534
+ }
535
+ /** @internal */
536
+ async load(options, timeout, signal) {
537
+ const { result } = await this.#request("load", {
538
+ path: options.plugin,
539
+ className: options.className,
540
+ classId: options.classId,
541
+ sampleRate: options.sampleRate,
542
+ blockSize: options.blockSize,
543
+ processMode: options.processMode,
544
+ inputs: options.inputs,
545
+ outputs: options.outputs,
546
+ openEditor: options.editor === true,
547
+ scale: options.scale,
548
+ presentation: options.presentation === void 0 ? "editor" : options.presentation ?? void 0
549
+ }, void 0, {
550
+ timeout,
551
+ signal
552
+ });
553
+ this.#info = result;
554
+ }
555
+ async getInfo(options) {
556
+ this.#info = (await this.#request("info", {}, void 0, options)).result;
557
+ return this.#info;
558
+ }
559
+ async getParameters(options) {
560
+ return (await this.#request("getParameters", {}, void 0, options)).result;
561
+ }
562
+ async getParameter(id, options) {
563
+ return (await this.#request("getParameter", { id }, void 0, options)).result;
564
+ }
565
+ /** Sets the controller's value now and hands the change to the processor
566
+ at the first frame of the next process call. */
567
+ async setParameter(id, value, options) {
568
+ const values = typeof value === "number" ? { normalized: value } : value;
569
+ return (await this.#request("setParameter", {
570
+ id,
571
+ ...values
572
+ }, void 0, options)).result;
573
+ }
574
+ /** A parameter by its title, looked up at each use (see
575
+ {@link parameterTitled}). */
576
+ parameter(title) {
577
+ const find = async (options) => parameterTitled(await this.getParameters(options), title);
578
+ return {
579
+ get: find,
580
+ set: async (value, options) => this.setParameter((await find(options)).id, value, options)
581
+ };
582
+ }
583
+ async getLatency(options) {
584
+ return (await this.#request("getLatency", {}, void 0, options)).result;
585
+ }
586
+ /** Processes `frames` frames, in blocks of at most the block size. */
587
+ async process(request = {}) {
588
+ const info = this.#info;
589
+ const inputs = request.inputs ?? [];
590
+ if (inputs.length > info.buses.audioInputs.length) throw new Vst3HostError("invalid-request", `${inputs.length} input buses were given; "${info.name}" has ${info.buses.audioInputs.length}`);
591
+ let frames = request.frames;
592
+ for (const [index, bus] of inputs.entries()) {
593
+ if (bus == null) continue;
594
+ for (const [channel, samples] of bus.entries()) {
595
+ if (!(samples instanceof Float32Array)) throw new Vst3HostError("invalid-request", `inputs[${index}][${channel}] must be a Float32Array`);
596
+ frames ??= samples.length;
597
+ if (samples.length !== frames) throw new Vst3HostError("invalid-request", `inputs[${index}][${channel}] has ${samples.length} frames; the call processes ${frames}`);
598
+ }
599
+ }
600
+ if (frames === void 0) throw new Vst3HostError("invalid-request", "process() needs `frames` when no input audio is given");
601
+ if (!Number.isInteger(frames) || frames < 0) throw new Vst3HostError("invalid-request", `frames must be a non-negative integer, not ${frames}`);
602
+ const channels = inputs.map((bus) => bus?.length ?? 0);
603
+ const bytes = channels.reduce((sum, count) => sum + count, 0) * frames * 4;
604
+ const outputBytes = info.buses.audioOutputs.filter((bus) => bus.active).reduce((sum, bus) => sum + bus.channels, 0) * frames * 4;
605
+ if (bytes > this.#options.maxPayloadBytes || outputBytes > this.#options.maxPayloadBytes) throw new Vst3HostError("message-too-large", `${frames} frames exceed the ${this.#options.maxPayloadBytes}-byte message limit; process them in several calls (the transport and plugin state carry over) or raise maxPayloadBytes`);
606
+ const payload = new Uint8Array(bytes);
607
+ let offset = 0;
608
+ for (const bus of inputs) for (const samples of bus ?? []) {
609
+ payload.set(new Uint8Array(samples.buffer, samples.byteOffset, samples.byteLength), offset);
610
+ offset += samples.byteLength;
611
+ }
612
+ const { result, payload: output } = await this.#request("process", {
613
+ frames,
614
+ inputs: channels,
615
+ events: request.events,
616
+ parameterChanges: request.parameterChanges,
617
+ transport: request.transport
618
+ }, payload, request);
619
+ const response = result;
620
+ const audio = new Float32Array(output.byteLength / 4);
621
+ new Uint8Array(audio.buffer).set(output);
622
+ let position = 0;
623
+ const outputs = response.outputs.map((count) => Array.from({ length: count }, () => {
624
+ const channel = audio.subarray(position, position + frames);
625
+ position += frames;
626
+ return channel;
627
+ }));
628
+ return {
629
+ ...response,
630
+ outputs
631
+ };
632
+ }
633
+ /** The processor's and the controller's state. Pending parameter changes
634
+ reach the processor first (in a process call of zero frames). */
635
+ async getState(options) {
636
+ const { result, payload } = await this.#request("getState", {}, void 0, options);
637
+ const { componentBytes } = result;
638
+ const bytes = new Uint8Array(payload);
639
+ return {
640
+ component: bytes.slice(0, componentBytes),
641
+ controller: bytes.slice(componentBytes)
642
+ };
643
+ }
644
+ async setState(state, options) {
645
+ const payload = new Uint8Array(state.component.byteLength + state.controller.byteLength);
646
+ payload.set(state.component, 0);
647
+ payload.set(state.controller, state.component.byteLength);
648
+ await this.#request("setState", {
649
+ componentBytes: state.component.byteLength,
650
+ controllerBytes: state.controller.byteLength
651
+ }, payload, options);
652
+ }
653
+ /** Deactivates and reactivates the plugin: it clears delay lines, held
654
+ notes and other processing state. The transport is kept. */
655
+ async reset(options) {
656
+ await this.#request("reset", {}, void 0, options);
657
+ }
658
+ /** A fresh instance of the same class and setup, in the same process.
659
+ The transport starts over. */
660
+ async recreate(options) {
661
+ this.#info = (await this.#request("recreate", {}, void 0, options)).result;
662
+ return this.#info;
663
+ }
664
+ /** A new sample rate, block size, mode or bus layout. What is not given
665
+ stays as it is. */
666
+ async configure(setup, options) {
667
+ this.#info = (await this.#request("configure", setup, void 0, options)).result;
668
+ return this.#info;
669
+ }
670
+ /** Opens the editor, or shows an open one in another presentation (it
671
+ opens again there, at the same size). */
672
+ async openEditor(options = {}) {
673
+ return (await this.#request("openEditor", {
674
+ scale: options.scale,
675
+ presentation: options.presentation
676
+ }, void 0, options)).result;
677
+ }
678
+ async closeEditor(options) {
679
+ return (await this.#request("closeEditor", {}, void 0, options)).result;
680
+ }
681
+ async resizeEditor(width, height, options) {
682
+ return (await this.#request("resizeEditor", {
683
+ width,
684
+ height
685
+ }, void 0, options)).result;
686
+ }
687
+ async getEditor(options) {
688
+ return (await this.#request("getEditor", {}, void 0, options)).result;
689
+ }
690
+ /** Starts live audio through the default output device, with `settings`
691
+ applied first. Fails with `audio-unavailable` without a device.
692
+ While it runs, `process` fails: the plugin is processing live. */
693
+ async startAudio(settings = {}) {
694
+ return (await this.#request("startAudio", audioParams(settings), void 0, settings)).result;
695
+ }
696
+ async stopAudio(options) {
697
+ return (await this.#request("stopAudio", {}, void 0, options)).result;
698
+ }
699
+ /** Changes live audio's input, volume or muting, running or not. */
700
+ async setAudio(settings) {
701
+ return (await this.#request("setAudio", audioParams(settings), void 0, settings)).result;
702
+ }
703
+ async getAudio(options) {
704
+ return (await this.#request("getAudio", {}, void 0, options)).result;
705
+ }
706
+ /** The host's transport, which live audio advances while it plays. */
707
+ async getTransport(options) {
708
+ return (await this.#request("getTransport", {}, void 0, options)).result;
709
+ }
710
+ /** Changes the transport, as `process({ transport })` does, without
711
+ processing: play and stop live audio's transport, or set its tempo. */
712
+ async setTransport(update, options) {
713
+ return (await this.#request("setTransport", { ...update }, void 0, options)).result;
714
+ }
715
+ /** Unloads the plugin and ends the process: politely, then by force.
716
+ Safe to call more than once and after a failure. */
717
+ close(timeout = 1e4) {
718
+ this.#closing ??= (async () => {
719
+ if (this.#exit) return this.#exit;
720
+ this.#closeRequested = true;
721
+ const kill = setTimeout(() => this.#child.kill("SIGKILL"), timeout);
722
+ try {
723
+ if (!this.#failure && this.#identity) await this.#request("shutdown", {}, void 0, { timeout }).catch(() => this.#child.kill("SIGKILL"));
724
+ else this.#child.kill("SIGKILL");
725
+ this.#child.stdin.end();
726
+ return await this.#exited;
727
+ } finally {
728
+ clearTimeout(kill);
729
+ }
730
+ })();
731
+ return this.#closing;
732
+ }
733
+ async [Symbol.asyncDispose]() {
734
+ await this.close();
735
+ }
736
+ #request(method, params, payload, options) {
737
+ if (this.#failure && method !== "shutdown") return Promise.reject(this.#failure);
738
+ if (this.#closing && method !== "shutdown") return Promise.reject(new Vst3HostError("closed", "the VST3 host is closing"));
739
+ if (options?.signal?.aborted) return Promise.reject(this.#aborted(method, options.signal));
740
+ return new Promise((resolve, reject) => {
741
+ const pending = {
742
+ id: this.#nextId++,
743
+ method,
744
+ params,
745
+ payload,
746
+ timeout: options?.timeout ?? this.#options.requestTimeout,
747
+ signal: options?.signal,
748
+ resolve,
749
+ reject,
750
+ settled: false
751
+ };
752
+ if (pending.signal) {
753
+ pending.onAbort = () => {
754
+ const index = this.#queue.indexOf(pending);
755
+ if (index >= 0) this.#queue.splice(index, 1);
756
+ this.#settle(pending, pending !== this.#current);
757
+ reject(this.#aborted(method, pending.signal));
758
+ };
759
+ pending.signal.addEventListener("abort", pending.onAbort, { once: true });
760
+ }
761
+ this.#queue.push(pending);
762
+ this.#pump();
763
+ });
764
+ }
765
+ #aborted(method, signal) {
766
+ return new Vst3HostError("aborted", `${method} was aborted`, void 0, { cause: signal.reason });
767
+ }
768
+ #settle(pending, clearTimer = true) {
769
+ pending.settled = true;
770
+ if (clearTimer) clearTimeout(pending.timer);
771
+ if (pending.onAbort) pending.signal?.removeEventListener("abort", pending.onAbort);
772
+ }
773
+ #pump() {
774
+ if (this.#current || this.#failure) return;
775
+ const next = this.#queue.shift();
776
+ if (!next) return;
777
+ this.#current = next;
778
+ next.timer = setTimeout(() => {
779
+ const error = new Vst3HostError("timeout", `${next.method} did not finish within ${next.timeout} ms; the VST3 host was stopped${this.#stderrTail()}`, {
780
+ method: next.method,
781
+ timeout: next.timeout
782
+ });
783
+ if (!next.settled) {
784
+ this.#settle(next);
785
+ next.reject(error);
786
+ }
787
+ this.#fail(error);
788
+ this.#child.kill("SIGKILL");
789
+ }, next.timeout);
790
+ const frame = encodeFrame({
791
+ id: next.id,
792
+ method: next.method,
793
+ params: next.params
794
+ }, next.payload);
795
+ for (const part of frame) this.#child.stdin.write(part);
796
+ }
797
+ #receive(frame) {
798
+ const header = frame.header;
799
+ if (typeof header.event === "string") {
800
+ if (header.event === "hello") this.#hello?.(header.params);
801
+ else if (header.event === "protocolError") this.#fail(new Vst3HostError("protocol-error", `the host rejected the request stream: ${header.params.message}`));
802
+ else this.#events.emit(header.event, header.params);
803
+ return;
804
+ }
805
+ const pending = this.#current;
806
+ if (!pending || header.id !== pending.id) {
807
+ this.#fail(new Vst3HostError("protocol-error", `the host answered request ${header.id}, not ${pending?.id}`));
808
+ this.#child.kill("SIGKILL");
809
+ return;
810
+ }
811
+ this.#current = void 0;
812
+ clearTimeout(pending.timer);
813
+ if (!pending.settled) {
814
+ this.#settle(pending);
815
+ if (header.error) pending.reject(new Vst3HostError(header.error.code, header.error.message, header.error.details));
816
+ else pending.resolve({
817
+ result: header.result,
818
+ payload: frame.payload
819
+ });
820
+ }
821
+ this.#pump();
822
+ }
823
+ #fail(error) {
824
+ this.#failure ??= error;
825
+ const failed = [...this.#current ? [this.#current] : [], ...this.#queue];
826
+ this.#current = void 0;
827
+ this.#queue = [];
828
+ for (const pending of failed) if (!pending.settled) {
829
+ this.#settle(pending);
830
+ pending.reject(this.#failure);
831
+ }
832
+ }
833
+ #stderrTail() {
834
+ const tail = this.#stderr.trim().split("\n").slice(-12).join("\n");
835
+ return tail ? `\nhost stderr:\n${tail}` : "";
836
+ }
837
+ async #withTimeout(promise, timeout, signal, what) {
838
+ let timer;
839
+ let onAbort;
840
+ try {
841
+ return await Promise.race([promise, new Promise((_, reject) => {
842
+ timer = setTimeout(() => reject(new Vst3HostError("timeout", `the VST3 host did not ${what} within ${timeout} ms${this.#stderrTail()}`)), timeout);
843
+ if (signal) {
844
+ onAbort = () => reject(this.#aborted(what, signal));
845
+ if (signal.aborted) onAbort();
846
+ else signal.addEventListener("abort", onAbort, { once: true });
847
+ }
848
+ })]);
849
+ } finally {
850
+ clearTimeout(timer);
851
+ if (onAbort) signal?.removeEventListener("abort", onAbort);
852
+ }
853
+ }
854
+ };
855
+ // src/npm.ts
856
+ const NPM_REGISTRY = "https://registry.npmjs.org";
857
+ /** Never "missing" for anything but the registry's 404: a network,
858
+ authentication or server error is an error. */
859
+ var RegistryError = class extends Error {
860
+ name = "RegistryError";
861
+ status;
862
+ constructor(message, status, options) {
863
+ super(message, options);
864
+ this.status = status;
865
+ }
866
+ };
867
+ function versionUrl(registry, name, version) {
868
+ return `${registry.replace(/\/$/, "")}/${name.replace("/", "%2f")}/${encodeURIComponent(version)}`;
869
+ }
870
+ /** The version's metadata, or null when the registry says it does not exist. */
871
+ async function versionMetadata(name, version, { registry = NPM_REGISTRY, fetcher = fetch } = {}) {
872
+ const url = versionUrl(registry, name, version);
873
+ let response;
874
+ try {
875
+ response = await fetcher(url, {
876
+ headers: { accept: "application/json" },
877
+ signal: AbortSignal.timeout(3e4)
878
+ });
879
+ } catch (error) {
880
+ throw new RegistryError(`could not reach ${url}: ${error.message}`, void 0, { cause: error });
881
+ }
882
+ if (response.status === 404) return null;
883
+ if (!response.ok) throw new RegistryError(`${url} answered HTTP ${response.status}`, response.status);
884
+ let metadata;
885
+ try {
886
+ metadata = await response.json();
887
+ } catch (error) {
888
+ throw new RegistryError(`${url} did not answer JSON`, response.status, { cause: error });
889
+ }
890
+ if (metadata.name !== name || metadata.version !== version || !metadata.dist?.integrity?.startsWith("sha512-") || typeof metadata.dist.tarball !== "string") throw new RegistryError(`${url} has no ${name}@${version} with a SHA-512 integrity`);
891
+ return metadata;
892
+ }
893
+ function matchesIntegrity(bytes, integrity) {
894
+ if (!integrity.startsWith("sha512-")) return false;
895
+ const expected = Buffer.from(integrity.slice(7), "base64");
896
+ const actual = createHash("sha512").update(bytes).digest();
897
+ return expected.length === actual.length && timingSafeEqual(expected, actual);
898
+ }
899
+ /** The version's tarball, verified against its metadata's integrity. */
900
+ async function downloadTarball(metadata, { registry = NPM_REGISTRY, fetcher = fetch } = {}) {
901
+ const url = new URL(metadata.dist.tarball);
902
+ const allowed = new URL(registry);
903
+ if (url.protocol !== allowed.protocol || url.host !== allowed.host) throw new RegistryError(`${metadata.name}'s tarball must come from ${allowed.origin}, not ${url.origin}`);
904
+ let response;
905
+ try {
906
+ response = await fetcher(url.href, { signal: AbortSignal.timeout(12e4) });
907
+ } catch (error) {
908
+ throw new RegistryError(`could not download ${url.href}: ${error.message}`, void 0, { cause: error });
909
+ }
910
+ if (!response.ok) throw new RegistryError(`${url.href} answered HTTP ${response.status}`, response.status);
911
+ const bytes = Buffer.from(await response.arrayBuffer());
912
+ if (!matchesIntegrity(bytes, metadata.dist.integrity)) throw new RegistryError(`${metadata.name}@${metadata.version}'s tarball failed its SHA-512 integrity check`);
913
+ return bytes;
914
+ }
915
+ /** Every entry of a gzipped tarball (ustar with pax path records, as npm
916
+ writes them). Links and devices are reported as "other". */
917
+ function readTarball(gzipped, maxBytes = 536870912) {
918
+ const tar = gunzipSync(gzipped, { maxOutputLength: maxBytes });
919
+ const entries = [];
920
+ let paxPath;
921
+ for (let offset = 0; offset + 512 <= tar.length;) {
922
+ const header = tar.subarray(offset, offset + 512);
923
+ if (header.every((byte) => byte === 0)) break;
924
+ const field = (start, length) => header.toString("utf8", start, start + length).split("\0")[0];
925
+ const size = Number.parseInt(field(124, 12).trim() || "0", 8);
926
+ if (!Number.isSafeInteger(size) || size < 0 || offset + 512 + size > tar.length) throw new Error("the tarball is truncated or corrupt");
927
+ const typeFlag = String.fromCharCode(header[156]);
928
+ const data = tar.subarray(offset + 512, offset + 512 + size);
929
+ offset += 512 + Math.ceil(size / 512) * 512;
930
+ if (typeFlag === "x") {
931
+ paxPath = /\d+ path=([^\n]*)\n/.exec(data.toString("utf8"))?.[1];
932
+ continue;
933
+ }
934
+ if (typeFlag === "g") continue;
935
+ const prefix = field(345, 155);
936
+ let path = paxPath ?? (prefix ? `${prefix}/${field(0, 100)}` : field(0, 100));
937
+ paxPath = void 0;
938
+ path = path.replace(/^package\//, "");
939
+ entries.push({
940
+ path,
941
+ mode: Number.parseInt(field(100, 8).trim() || "0", 8),
942
+ type: typeFlag === "0" || typeFlag === "\0" ? "file" : typeFlag === "5" ? "directory" : "other",
943
+ data: Buffer.from(data)
944
+ });
945
+ }
946
+ return entries;
947
+ }
948
+ // src/prepare.ts
949
+ function cacheRoot() {
950
+ if (process.env.SOUNDOR_CACHE) return process.env.SOUNDOR_CACHE;
951
+ if (process.platform === "win32") return join(process.env.LOCALAPPDATA ?? homedir(), "Soundor", "cache");
952
+ if (process.platform === "darwin") return join(homedir(), "Library", "Caches", "Soundor");
953
+ return join(process.env.XDG_CACHE_HOME || join(homedir(), ".cache"), "soundor");
954
+ }
955
+ function currentTarget() {
956
+ const platform = currentPlatform();
957
+ const target = targetFor(platform);
958
+ if (!target) throw new Vst3HostError("unsupported-platform", `@soundor/vst3-host has no executable for ${describePlatform(platform)}`);
959
+ return target;
960
+ }
961
+ function writeAtomically(path, data, mode) {
962
+ const partial = `${path}.${randomUUID()}.part`;
963
+ writeFileSync(partial, data);
964
+ if (mode !== void 0) chmodSync(partial, mode);
965
+ renameSync(partial, path);
966
+ }
967
+ async function prepareHost(options = {}) {
968
+ const root = options.root ?? packageRoot;
969
+ const log = options.log ?? ((message) => console.log(message));
970
+ const workspace = isSourceWorkspace(root);
971
+ const target = currentTarget();
972
+ const { fingerprint, version } = nativeInputs(root);
973
+ const artifact = options.artifact ?? process.env.SOUNDOR_VST3_HOST_ARTIFACT;
974
+ const build = options.build ?? ((reason) => buildFromSource(root, fingerprint, reason, log));
975
+ const select = (prepared) => {
976
+ log(`VST3 host: ${prepared.executable} (${prepared.mode}: ${prepared.reason})`);
977
+ if (workspace) {
978
+ mkdirSync(join(root, "build"), { recursive: true });
979
+ const selection = {
980
+ mode: prepared.mode,
981
+ executable: relative(root, prepared.executable),
982
+ sha256: sha256File(prepared.executable),
983
+ sourceFingerprint: fingerprint,
984
+ version,
985
+ target: target.target,
986
+ reason: prepared.reason
987
+ };
988
+ writeAtomically(selectionFile(root), `${JSON.stringify(selection, null, 2)}\n`);
989
+ }
990
+ return prepared;
991
+ };
992
+ if (artifact) return select({
993
+ executable: checkArtifact(artifact, {
994
+ target,
995
+ version,
996
+ fingerprint,
997
+ commit: options.commit ?? process.env.GITHUB_SHA
998
+ }),
999
+ mode: "artifact",
1000
+ reason: `the CI artifact in ${artifact}, matching this checkout`,
1001
+ sourceFingerprint: fingerprint
1002
+ });
1003
+ const mode = options.mode ?? "prebuilt";
1004
+ if (mode !== "source" && !workspace) {
1005
+ const resolved = resolveHost({ root });
1006
+ return {
1007
+ executable: resolved.executable,
1008
+ mode: "prebuilt",
1009
+ reason: resolved.reason,
1010
+ sourceFingerprint: resolved.provenance?.sourceFingerprint ?? fingerprint
1011
+ };
1012
+ }
1013
+ if (mode !== "source") {
1014
+ const cache = options.cache ?? cacheRoot();
1015
+ let published;
1016
+ try {
1017
+ published = await publishedExecutable({
1018
+ target,
1019
+ version,
1020
+ fingerprint,
1021
+ cache,
1022
+ registry: options.registry ?? "https://registry.npmjs.org",
1023
+ fetcher: options.fetcher ?? fetch
1024
+ });
1025
+ } catch (error) {
1026
+ throw new Vst3HostError("selection-failed", `could not check the published VST3 host executable (${error.message}). Build it instead with mode "source" (pnpm --filter @soundor/vst3-host build:native).`, void 0, { cause: error });
1027
+ }
1028
+ if (typeof published === "object") return select(published);
1029
+ const reason = published === "unpublished" ? `${target.package}@${version} is not published` : `the native sources differ from ${target.package}@${version}'s`;
1030
+ if (mode === "published") throw new Vst3HostError("selection-failed", `no published VST3 host executable to use: ${reason}`);
1031
+ log(`VST3 host: building from source (${reason})`);
1032
+ return select(build(`${reason}; built from source`));
1033
+ }
1034
+ return select(build("built from source as requested"));
1035
+ }
1036
+ async function publishedExecutable(context) {
1037
+ const { target, version, fingerprint } = context;
1038
+ const directory = join(context.cache, "vst3-host", "prebuilt", version, target.target);
1039
+ const cachedProvenance = join(directory, PROVENANCE_FILE);
1040
+ if (existsSync(cachedProvenance)) {
1041
+ const provenance = checkedProvenance(JSON.parse(readFileSync(cachedProvenance, "utf8")), cachedProvenance);
1042
+ const executable = join(directory, provenance.executable.path);
1043
+ if (existsSync(executable) && sha256File(executable) === provenance.executable.sha256) {
1044
+ if (provenance.sourceFingerprint !== fingerprint) return "different";
1045
+ return {
1046
+ executable,
1047
+ mode: "prebuilt",
1048
+ reason: `${target.package}@${version}, cached in ${directory}; its native inputs match`,
1049
+ sourceFingerprint: fingerprint
1050
+ };
1051
+ }
1052
+ }
1053
+ const metadata = await versionMetadata(target.package, version, context);
1054
+ if (metadata === null) return "unpublished";
1055
+ const entries = readTarball(await downloadTarball(metadata, context));
1056
+ const record = entries.find((entry) => entry.path === PROVENANCE_FILE);
1057
+ if (!record) throw new Error(`${target.package}@${version} has no ${PROVENANCE_FILE}`);
1058
+ const provenance = checkedProvenance(JSON.parse(record.data.toString("utf8")), `${target.package}@${version}/${PROVENANCE_FILE}`);
1059
+ if (provenance.version !== version || provenance.target !== target.target || provenance.protocol !== PROTOCOL_VERSION) throw new Error(`${target.package}@${version} describes another build (${provenance.target} ${provenance.version})`);
1060
+ const binary = entries.find((entry) => entry.path === provenance.executable.path);
1061
+ if (!binary || sha256(binary.data) !== provenance.executable.sha256) throw new Error(`${target.package}@${version}'s executable does not match its provenance`);
1062
+ mkdirSync(join(directory, "bin"), { recursive: true });
1063
+ const executable = join(directory, provenance.executable.path);
1064
+ writeAtomically(executable, binary.data, 493);
1065
+ writeAtomically(cachedProvenance, `${JSON.stringify(provenance, null, 2)}\n`);
1066
+ if (provenance.sourceFingerprint !== fingerprint) return "different";
1067
+ return {
1068
+ executable,
1069
+ mode: "prebuilt",
1070
+ reason: `${target.package}@${version} from npm; its native inputs match`,
1071
+ sourceFingerprint: fingerprint
1072
+ };
1073
+ }
1074
+ /** A CI artifact: right target, version, sources and commit, intact. */
1075
+ function checkArtifact(directory, expected) {
1076
+ const file = join(directory, PROVENANCE_FILE);
1077
+ if (!existsSync(file)) throw new Vst3HostError("selection-failed", `${directory} has no ${PROVENANCE_FILE}`);
1078
+ let provenance;
1079
+ try {
1080
+ provenance = checkedProvenance(JSON.parse(readFileSync(file, "utf8")), file);
1081
+ } catch (error) {
1082
+ throw new Vst3HostError("selection-failed", error.message);
1083
+ }
1084
+ const problems = [];
1085
+ if (provenance.target !== expected.target.target) problems.push(`it is for ${provenance.target}, not ${expected.target.target}`);
1086
+ if (provenance.version !== expected.version) problems.push(`it is version ${provenance.version}, not ${expected.version}`);
1087
+ if (provenance.protocol !== PROTOCOL_VERSION) problems.push(`it speaks protocol ${provenance.protocol}, not ${PROTOCOL_VERSION}`);
1088
+ if (provenance.sourceFingerprint !== expected.fingerprint) problems.push("it was built from other native sources than this checkout has");
1089
+ if (expected.commit && provenance.sourceCommit !== expected.commit) problems.push(`it was built from ${provenance.sourceCommit}, not ${expected.commit}`);
1090
+ const executable = join(directory, provenance.executable.path);
1091
+ if (!existsSync(executable)) problems.push(`${provenance.executable.path} is missing`);
1092
+ else if (sha256File(executable) !== provenance.executable.sha256) problems.push(`${provenance.executable.path} does not match its SHA-256`);
1093
+ if (problems.length) throw new Vst3HostError("selection-failed", `the VST3 host artifact in ${directory} does not match: ${problems.join("; ")}`);
1094
+ if (process.platform !== "win32") chmodSync(executable, 493);
1095
+ return executable;
1096
+ }
1097
+ /** The build directory for the current toolchain: a build directory keeps
1098
+ the compiler and generator it was configured with. */
1099
+ function sourceBuildDirectory(root = packageRoot) {
1100
+ const toolchain = [
1101
+ process.platform,
1102
+ process.arch,
1103
+ process.env.CMAKE_GENERATOR ?? "",
1104
+ process.env.CC ?? "",
1105
+ process.env.CXX ?? "",
1106
+ process.env.CMAKE_OSX_ARCHITECTURES ?? ""
1107
+ ].join("\0");
1108
+ const id = createHash("sha256").update(toolchain).digest("hex").slice(0, 12);
1109
+ return join(root, "build", `host-${id}`);
1110
+ }
1111
+ function run(command, args, cwd) {
1112
+ const result = spawnSync(command, args, {
1113
+ cwd,
1114
+ stdio: "inherit"
1115
+ });
1116
+ if (result.error) throw new Vst3HostError("selection-failed", `could not run ${command}: ${result.error.message} (building the VST3 host needs CMake and a C++20 compiler)`);
1117
+ if (result.status !== 0) throw new Vst3HostError("selection-failed", `${command} ${args.join(" ")} failed (exit ${result.status})`);
1118
+ }
1119
+ /** An incremental Release build of the executable alone. */
1120
+ function buildFromSource(root, fingerprint, reason, log = console.log) {
1121
+ const directory = sourceBuildDirectory(root);
1122
+ log(`VST3 host: building in ${directory}`);
1123
+ run("cmake", [
1124
+ "-S",
1125
+ root,
1126
+ "-B",
1127
+ directory,
1128
+ "-DCMAKE_BUILD_TYPE=Release"
1129
+ ], root);
1130
+ run("cmake", [
1131
+ "--build",
1132
+ directory,
1133
+ "--config",
1134
+ "Release",
1135
+ "--target",
1136
+ "soundor-vst3-host"
1137
+ ], root);
1138
+ const executable = join(directory, "bin", process.platform === "win32" ? "soundor-vst3-host.exe" : "soundor-vst3-host");
1139
+ if (!existsSync(executable)) throw new Vst3HostError("selection-failed", `the build did not produce ${executable}`);
1140
+ return {
1141
+ executable,
1142
+ mode: "source",
1143
+ reason,
1144
+ sourceFingerprint: fingerprint
1145
+ };
1146
+ }
1147
+ export { nativeInputs as a, PROTOCOL_VERSION as c, resolveHost as i, Vst3HostError as l, Vst3Host as n, targetFor as o, launchHost as r, targets as s, prepareHost as t };
1148
+
1149
+ //# sourceMappingURL=prepare-ayjTVhfk.mjs.map