nvst3-host
Production-grade VST3 host for Node.js — load, automate, and process audio through any VST3 plugin with zero-copy buffers and prebuilt native binaries.
nvst3-host brings the VST3 plugin ecosystem to Node.js. Load any VST3 plugin, push audio through it with zero-copy Float32Array buffers, automate parameters, send MIDI events, save and restore plugin state, and react to plugin-initiated restart notifications — all from JavaScript, with no GUI, no DAW, and no compiler toolchain required at install time.
It is built on the official Steinberg VST3 SDK (v3.8.0, MIT-licensed) and ships prebuilt native binaries for Windows, macOS (Intel + Apple Silicon), and Linux.
Features
- Discovery — scan platform-default VST3 locations, recursively scan an arbitrary directory, or inspect a single
.vst3module without instantiating the DSP. - Lifecycle —
load(),setActive(bool),setProcessing(bool),dispose()(idempotent), and[Symbol.dispose]enabling theusingkeyword for scope-based cleanup. - Audio processing — zero-copy
Float32Array/Float64Arraychannel buffers; configurable sample size (32 or 64 bit); configurable process mode (realtime/offline/prefetch); parameter-flush blocks viaprocess({ numSamples: 0 }); silence-flag propagation on input and output buses; tail-samples query (returnsNumber.POSITIVE_INFINITYforkInfiniteTail); latency query. - Parameters —
getParameter/setParameter/setParameters(atomic batch),getParameterInfo,formatParameter,parseParameter(string → normalized),plainToNormalized/normalizedToPlain;ParameterFlags.IsProgramChangehonored by plugins that expose program-change parameters. - MIDI / events — structured
MidiEvents (NoteOn / NoteOff / PolyPressure / Controller / ProgramChange / ChannelPressure / PitchBend / SysEx) plus rawaddMidiBytes; optionalnoteIdon NoteOn/NoteOff/PolyPressure for note-expression targeting; output event retrieval viatakeOutputEvents(). - State persistence —
saveState()writes a versionedNST3envelope (4-byte magic, 1-byte version, length-prefixed component + controller blobs);loadState()auto-detects the envelope and falls back to legacy single-blob loading for backward compatibility with 0.1.0 state files. - Units & programs —
IUnitInfoenumeration (getUnitCount,getUnitInfo,getProgramListCount,getProgramListInfo,getProgramName,selectProgram,getCurrentUnit,getUnitByBusInfo); per-program and per-unit bulk data viaIProgramListData/IUnitData(getProgramData,setProgramData,getUnitData,setUnitData). - Note expression —
INoteExpressionControllerenumeration (getNoteExpressionCount,getNoteExpressionInfo) andaddNoteExpressionEvent({ noteId, typeId, value, sampleOffset? })queuing. - Keyswitches —
IKeyswitchControllerenumeration (getKeyswitchCount,getKeyswitchInfo). - Bus management — runtime
getBusList,getBusInfo,activateBus(toggle individual buses while inactive); speaker-arrangement negotiation viasetBusArrangement/getBusArrangementwith theSpeakerArrangementenum; routing info viagetRoutingInfo. - Process context — configurable tempo / time signature / transport (
playing,cycleActive,recording) /systemTime/continuousTimeSamplesviasetProcessContext/getProcessContext;IProcessContextRequirementsgating so the host skips recomputation of unneeded fields each block. - Information interfaces —
IAudioPresentationLatency(setAudioPresentationLatency);IInfoListener(setChannelContextInfo);IPrefetchableSupport(isPrefetchable);IEditController2(setKnobMode). - Restart auto-react — when the plugin calls
restartComponent, the host automatically re-queries the affected SDK state (latency, bus info, etc.) BEFORE the JSrestartevent fires;applyRestartFlags(flags)is exposed for manual re-query. - Mutable ProcessSetup —
setProcessSetup({ sampleRate?, maxBlockSize?, processMode?, sampleSize? })re-negotiates the setup for the nextsetActive(true)(requiressetActive(false)first). - Accurate PlugInterfaceSupport — the host advertises exactly the 13 interfaces it implements via a custom
IPlugInterfaceSupport(no GUI-only interfaces such asIPlugView/IPlugFrame). - Plugin→host events —
on('restart'),on('dirty'),on('beginGesture'),on('endGesture'),on('startGroup'),on('finishGroup'), all delivered safely across threads via aNapi::ThreadSafeFunction. - Error handling — typed
NstErrorwithcode,cause,runtimeTriple, andsupportedTriplesfields; 14 stable error codes covering load, activation, processing, state, MIDI, platform, and unexpected-fault conditions. - Cross-platform prebuilt binaries —
npm install nvst3-hostships native.nodefiles viaprebuildifyforwin32-x64,darwin-arm64,linux-x64, andlinux-arm64. No toolchain needed for end users. - MIT-licensed end-to-end — both
nvst3-hostand the bundled VST3 SDK are MIT-licensed (since SDK v3.7.7), so there are no licensing concerns for commercial or closed-source use. - Strong TypeScript types — a hand-written
index.d.tsmirrors the native surface 1:1, including all 14 enums and full JSDoc, for editor IntelliSense.
Installation
npm install nvst3-host
No compiler toolchain required — prebuilt binaries are shipped for:
| Platform | Triple | Build Method |
|---|---|---|
| Windows x64 | win32-x64 |
GitHub Actions (windows-latest) |
| macOS Apple Silicon | darwin-arm64 |
GitHub Actions (macos-14) |
| Linux x64 | linux-x64 |
GitHub Actions (ubuntu-latest) |
| Linux ARM64 | linux-arm64 |
GitHub Actions (ubuntu-24.04-arm) |
Note on Intel Macs and Windows x86: GitHub Actions no longer provides
darwin-x64(Intel macOS) runners as of late 2025 — those runners were deprecated and removed.win32-ia32(32-bit Windows) was never supported in CI and modern VST3 plugins are universally 64-bit. Users on Intel Macs can build from source vianpm install(which falls back tonode-gyp rebuild) — see Building from Source.
If no prebuilt binary matches your runtime, node-gyp-build automatically falls back to a source build (node-gyp rebuild) — this requires Node.js headers and a C++17 compiler.
Requirements
- Node.js
>= 16.17 - One of the supported platform triples above
- (For source builds only) a C++17 compiler and Python 3 — see Building from Source
Quick Start
const { Host } = require('nvst3-host');
// 1. Create a host with the audio format you want to process at.
const host = new Host({
sampleRate: 48000,
maxBlockSize: 512,
audioInputs: 2, // stereo in
audioOutputs: 2, // stereo out
});
// 2. Load a VST3 plugin (path to the .vst3 bundle/module).
const plugin = host.load('/path/to/SomePlugin.vst3');
console.log(plugin.getInfo());
// 3. Activate and start processing.
plugin.setActive(true);
plugin.setProcessing(true);
// 4. Tweak a parameter by ID (normalized 0..1).
const paramInfo = plugin.getParameterInfo(0);
plugin.setParameter(paramInfo.id, 0.75);
// 5. Process a block of audio — buffers are passed zero-copy.
const numSamples = 512;
const inputs = [
new Float32Array(numSamples), // left (silence in)
new Float32Array(numSamples), // right
];
const outputs = [
new Float32Array(numSamples), // left (filled by plugin)
new Float32Array(numSamples), // right
];
plugin.process({ inputs, outputs, numSamples });
// 6. Dispose when done (idempotent — safe to call twice).
plugin.dispose();
If you are on Node.js ≥ 20 with explicit resource management enabled, you can use the using keyword for automatic cleanup:
{
using plugin = host.load('/path/to/SomePlugin.vst3');
// ... use plugin ...
// plugin[Symbol.dispose]() is called automatically at end of scope.
}
API
The full surface is documented in docs/API.md. The two main classes are:
Host
const { Host } = require('nvst3-host');
new Host(opts?)— construct a host withsampleRate,maxBlockSize,audioInputs,audioOutputs(all optional, with sensible defaults).host.load(path, opts?)→PluginInstance— load and instantiate a VST3 plugin.host.getOptions()→Required<HostOptions>— snapshot of the host's audio format.Host.scanDefaultLocations()→PluginInfo[]— scan platform-default VST3 directories.Host.scanDirectory(path)→PluginInfo[]— recursively scan an arbitrary directory.Host.inspectPlugin(path)→PluginInfo | PluginInfo[]— read metadata from a single.vst3without instantiating it.
PluginInstance
Obtained from host.load(...). Never call new PluginInstance(...) directly.
- Lifecycle:
dispose(),[Symbol.dispose](),on('restart', cb) - Metadata:
getInfo(),getPluginInfo()(debug snapshot with full state + buses + interfaces),getParameterTree()(all parameters grouped by unit with current values),getLatency() - Processing:
setActive(bool),setProcessing(bool),process({ inputs, outputs, numSamples }) - Parameters:
getParameterCount(),getParameterInfo(index),getParameter(id),setParameter(id, value),setParameters(changes),formatParameter(id, value) - MIDI:
addMidiEvent(event),addMidiBytes(sampleOffset, bytes),takeOutputEvents(),clearEvents() - State:
saveState()→Buffer,loadState(buffer)
MIDI Events
addMidiEvent accepts a discriminated union tagged by type. Use the MidiEventType enum to construct events:
const { Host, MidiEventType } = require('nvst3-host');
const host = new Host({ sampleRate: 48000, maxBlockSize: 512 });
const synth = host.load('/path/to/Synth.vst3');
synth.setActive(true);
synth.setProcessing(true);
// Schedule a C4 note on at sample offset 0, note off at offset 256.
synth.addMidiEvent({
type: MidiEventType.NoteOn,
channel: 0,
note: 60, // C4
velocity: 0.8,
sampleOffset: 0,
});
synth.addMidiEvent({
type: MidiEventType.NoteOff,
channel: 0,
note: 60,
velocity: 0.0,
sampleOffset: 256,
});
const outputs = [new Float32Array(512), new Float32Array(512)];
synth.process({ inputs: [], outputs, numSamples: 512 });
// Capture any output events (note-offs from the synth, SysEx, etc.).
const outEvents = synth.takeOutputEvents();
You can also push raw MIDI bytes with addMidiBytes:
// Status 0x90 (note on, channel 0), note 60, velocity 100
synth.addMidiBytes(0, Uint8Array.from([0x90, 60, 100]));
Error Handling
All errors thrown by nvst3-host carry a code property with one of the VST3_* error codes documented in docs/API.md.
const { Host } = require('nvst3-host');
const host = new Host();
try {
const plugin = host.load('/nonexistent/path.vst3');
} catch (err) {
if (err.code === 'VST3_LOAD_FAILED') {
console.error('Plugin could not be loaded:', err.message);
} else if (err.code === 'VST3_FACTORY_MISSING') {
console.error('Module loaded but no VST3 factory was found.');
} else {
throw err; // rethrow unknown errors
}
}
After any process() failure, the plugin enters a faulted state and all subsequent method calls (other than dispose()) reject with VST3_FAULTED until you dispose() the instance and create a new one via host.load().
State Save / Load
const fs = require('fs');
const { Host } = require('nvst3-host');
const host = new Host();
const plugin = host.load('/path/to/SomePlugin.vst3');
plugin.setActive(true);
// Mutate some parameters.
plugin.setParameter(0, 0.42);
plugin.setParameter(1, 0.97);
// Serialize current state to a Buffer.
const state = plugin.saveState();
fs.writeFileSync('plugin-state.bin', state);
// ... later, restore it ...
plugin.loadState(state);
console.log(plugin.getParameter(0)); // 0.42
State round-trips through IComponent::getState / setState and (if present) IEditController::setComponentState, so all parameter values are preserved.
Examples
The examples/ directory contains runnable sample scripts:
examples/scan-plugins.js— list installed VST3 plugins with metadata.examples/process-file.js— read a WAV, process it through a plugin, write a WAV.examples/midi-synth.js— schedule MIDI notes through an instrument, write output WAV.examples/parameter-sweep.js— automate a parameter across blocks, write output WAV.
Building from Source
End users should never need this — prebuilt binaries cover all supported platforms. For contributors and unsupported platforms:
# 1. Clone with the VST3 SDK submodule.
git clone --recursive https://github.com/Henley04/nvst3-host.git
cd nvst3-host
# 2. If you cloned without --recursive:
git submodule update --init --recursive
# 3. Install dev dependencies.
npm ci
# 4. Build the native addon (uses node-gyp under the hood).
npm run build
# 5. Verify it loads.
node -e "console.log(require('./').version())"
# { native: '0.1.0', vst3sdk: 'VST 3.8.0', napi: 8 }
Prerequisites for source builds:
- Node.js 20+ (for development; the runtime supports 16.17+)
- Python 3 (required by
node-gyp) - C++17 compiler:
- macOS: Xcode Command Line Tools (
xcode-select --install) - Linux:
g++≥ 11 orclang++≥ 13, pluslibasound2-devandlibstdc++-12-dev(or equivalent) - Windows: Visual Studio 2022 with the "Desktop development with C++" workload
- macOS: Xcode Command Line Tools (
- CMake ≥ 3.22 (only required to build the test plugin; see CONTRIBUTING.md)
To create prebuilds for the current platform:
npm run prebuild
This invokes prebuildify --napi-version 8 --tag-armv -t 20.0.0 and writes .node files into prebuilds/<triple>/. See CONTRIBUTING.md for the full CI-driven cross-platform build flow.
Performance
nvst3-host is designed for real-time, block-based audio processing:
- Zero-copy buffers —
Float32Arraychannel data is handed directly to the plugin viaAudioBusBufferschannel pointers. There is no copy on input or output. - No allocations on the hot path —
ProcessData,AudioBusBuffers,ParameterChangesContainer, andEventListContainerare reused acrossprocess()calls;setParameterqueues changes into pre-allocated queues. Steady-stateprocess()does not allocate. - No locks during
IAudioProcessor::process— restart notifications are posted through aNapi::ThreadSafeFunctiondriven by an atomic flag, so the audio thread never blocks on JavaScript-side state. - Block-based processing —
numSamplesper call is bounded bymaxBlockSize(default 512). Process the file in chunks and you will get DAW-class throughput on commodity hardware.
Limitations
- No GUI/editor support —
nvst3-hostis a headless host. Plugins that ship only an editor still process audio correctly; theirIPlugViewis never opened. - 64-bit audio is opt-in —
kSample32is the default; 64-bit double precision (kSample64) is opt-in viasampleSize: 64inHostOptions/LoadOptions. The host silently falls back to 32 if the plugin refuses 64. - Single-process context — each
PluginInstanceowns its own component/controller pair; there is no built-in signal graph or routing layer. Compose plugins in JavaScript by chainingprocess()calls. - Process mode is configurable —
realtimeis the default;offlineandprefetchmodes are opt-in viaprocessModeinHostOptions/LoadOptions. - macOS target — binaries are built with
MACOSX_DEPLOYMENT_TARGET=10.13(High Sierra and later). - Linux target — prebuilt binaries require
glibc ≥ 2.28(Ubuntu 18.04+ / Debian 10+).
License
MIT — see LICENSE.
The bundled VST3 SDK in third_party/vst3sdk/ is also MIT-licensed (since SDK v3.7.7), so there are no licensing concerns for commercial or closed-source use. See third_party/vst3sdk/LICENSE.txt for the SDK's license.