use std::io::{self, BufRead, Write};
use std::sync::{Arc, Mutex};
use vst3_host::{
audio::AudioBuffers,
process_isolation::{HostCommand, HostResponse, ProtocolChannel},
Plugin, Vst3Host,
};
type SharedPlugin = Arc<Mutex<Option<Plugin>>>;
fn main() {
let protocol = ProtocolChannel::claim();
eprintln!("VST3 Host Helper Process Started");
let plugin: SharedPlugin = Arc::new(Mutex::new(None));
#[cfg(target_os = "macos")]
{
macos::run(plugin, protocol);
}
#[cfg(not(target_os = "macos"))]
{
let mut protocol = protocol;
let stdin = io::stdin();
let mut sample_rate = 44100.0;
for line in stdin.lock().lines() {
let Some(command) = parse_line(line, &mut protocol) else {
continue;
};
if matches!(command, HostCommand::Shutdown) {
eprintln!("Shutting down helper process");
break;
}
let response = handle(command, &plugin, &mut sample_rate, None);
respond(&mut protocol, &response);
}
}
}
fn parse_line(line: io::Result<String>, protocol: &mut ProtocolChannel) -> Option<HostCommand> {
let line = match line {
Ok(l) => l,
Err(e) => {
eprintln!("Failed to read line: {}", e);
return None;
}
};
if line.trim().is_empty() {
return None;
}
match serde_json::from_str(&line) {
Ok(cmd) => Some(cmd),
Err(e) => {
respond(
protocol,
&HostResponse::Error {
message: format!("Invalid command: {}", e),
},
);
None
}
}
}
fn respond(protocol: &mut ProtocolChannel, response: &HostResponse) {
if let Ok(json) = serde_json::to_string(response) {
let _ = writeln!(protocol, "{}", json);
let _ = protocol.flush();
}
}
fn err<E: std::fmt::Display>(prefix: &str, e: E) -> HostResponse {
HostResponse::Error {
message: format!("{prefix}: {e}"),
}
}
#[cfg(target_os = "macos")]
struct GuiRequest {
open: bool,
reply: std::sync::mpsc::Sender<HostResponse>,
}
fn handle(
command: HostCommand,
plugin: &SharedPlugin,
sample_rate: &mut f64,
#[allow(unused_variables)] gui: Option<&GuiChannel>,
) -> HostResponse {
fn with<F: FnOnce(&mut Plugin) -> HostResponse>(p: &SharedPlugin, f: F) -> HostResponse {
let mut guard = match p.lock() {
Ok(g) => g,
Err(_) => {
return HostResponse::Error {
message: "plugin lock poisoned".to_string(),
}
}
};
match guard.as_mut() {
Some(pl) => f(pl),
None => HostResponse::Error {
message: "No plugin loaded".to_string(),
},
}
}
match command {
HostCommand::LoadPlugin {
path,
sample_rate: sr,
block_size,
tempo,
time_sig_numerator,
time_sig_denominator,
class_id,
} => {
*sample_rate = sr;
let mut host = match Vst3Host::builder()
.sample_rate(sr)
.block_size(block_size as usize)
.tempo(tempo)
.time_signature(time_sig_numerator, time_sig_denominator)
.build()
{
Ok(h) => h,
Err(e) => return err("Failed to build host", e),
};
let loaded = match class_id {
Some(class_id) => host.load_plugin_class(&path, &class_id),
None => host.load_plugin(&path),
};
match loaded {
Ok(p) => {
let info = p.info().clone();
let compatibility = p.class_compatibility().to_vec();
let output_channels = p.output_channel_count() as i32;
*plugin.lock().unwrap() = Some(p);
HostResponse::PluginInfo {
vendor: info.vendor,
name: info.name,
version: info.version,
category: info.category,
uid: info.uid,
has_gui: info.has_gui,
audio_inputs: info.audio_inputs as i32,
audio_outputs: info.audio_outputs as i32,
output_channels,
has_midi_input: info.has_midi_input,
has_midi_output: info.has_midi_output,
compatibility,
}
}
Err(e) => err("Failed to load plugin", e),
}
}
HostCommand::UnloadPlugin => {
*plugin.lock().unwrap() = None;
HostResponse::Success {
message: "Plugin unloaded".to_string(),
}
}
HostCommand::StartProcessing => with(plugin, |p| match p.start_processing() {
Ok(()) => HostResponse::Success {
message: "processing started".to_string(),
},
Err(e) => err("StartProcessing", e),
}),
HostCommand::StopProcessing => with(plugin, |p| match p.stop_processing() {
Ok(()) => HostResponse::Success {
message: "processing stopped".to_string(),
},
Err(e) => err("StopProcessing", e),
}),
HostCommand::Reconfigure {
sample_rate: sr,
block_size,
} => with(plugin, |p| match p.reconfigure(sr, block_size as usize) {
Ok(()) => {
*sample_rate = sr;
HostResponse::Success {
message: "reconfigured".to_string(),
}
}
Err(e) => err("Reconfigure", e),
}),
HostCommand::SetProcessMode { mode } => with(plugin, |p| match p.set_process_mode(mode) {
Ok(()) => HostResponse::Success {
message: "process mode set".to_string(),
},
Err(e) => err("SetProcessMode", e),
}),
HostCommand::SetParameter { id, value } => {
with(plugin, |p| match p.set_parameter(id, value) {
Ok(()) => HostResponse::Success {
message: "parameter set".to_string(),
},
Err(e) => err("SetParameter", e),
})
}
HostCommand::SetParameterAt { id, value, offset } => {
with(plugin, |p| match p.set_parameter_at(id, value, offset) {
Ok(()) => HostResponse::Success {
message: "parameter scheduled".to_string(),
},
Err(e) => err("SetParameterAt", e),
})
}
HostCommand::SetTempo { bpm } => with(plugin, |p| match p.set_tempo(bpm) {
Ok(()) => HostResponse::Success {
message: "tempo set".to_string(),
},
Err(e) => err("SetTempo", e),
}),
HostCommand::SetTimeSignature {
numerator,
denominator,
} => with(plugin, |p| {
match p.set_time_signature(numerator, denominator) {
Ok(()) => HostResponse::Success {
message: "time signature set".to_string(),
},
Err(e) => err("SetTimeSignature", e),
}
}),
HostCommand::SetPlaying { playing } => with(plugin, |p| match p.set_playing(playing) {
Ok(()) => HostResponse::Success {
message: "playing state set".to_string(),
},
Err(e) => err("SetPlaying", e),
}),
HostCommand::GetParameter { id } => with(plugin, |p| match p.get_parameter(id) {
Ok(value) => HostResponse::ParameterValue { value },
Err(e) => err("GetParameter", e),
}),
HostCommand::GetAllParameters => with(plugin, |p| match p.get_parameters() {
Ok(params) => HostResponse::Parameters { params },
Err(e) => err("GetAllParameters", e),
}),
HostCommand::FormatParameter { id, normalized } => {
with(plugin, |p| match p.format_parameter(id, normalized) {
Ok(value) => HostResponse::ParameterString { value },
Err(e) => err("FormatParameter", e),
})
}
HostCommand::SendMidi { event } => with(plugin, |p| match p.send_midi_event(event) {
Ok(()) => HostResponse::Success {
message: "midi sent".to_string(),
},
Err(e) => err("SendMidi", e),
}),
HostCommand::SendMidiAt {
event,
sample_offset,
} => with(plugin, |p| {
match p.send_midi_event_at(event, sample_offset) {
Ok(()) => HostResponse::Success {
message: "midi sent".to_string(),
},
Err(e) => err("SendMidiAt", e),
}
}),
HostCommand::SendPluginEvent { event } => {
with(plugin, |p| match p.send_plugin_event(event) {
Ok(()) => HostResponse::Success {
message: "plugin event sent".to_string(),
},
Err(e) => err("SendPluginEvent", e),
})
}
HostCommand::MidiPanic => with(plugin, |p| match p.midi_panic() {
Ok(()) => HostResponse::Success {
message: "MIDI panic queued".to_string(),
},
Err(e) => err("MidiPanic", e),
}),
HostCommand::SetBusActive {
media_type,
direction,
bus_index,
active,
} => with(plugin, |p| {
match p.set_bus_active(media_type, direction, bus_index, active) {
Ok(()) => HostResponse::Success {
message: "bus activation set".to_string(),
},
Err(e) => err("SetBusActive", e),
}
}),
HostCommand::BusArrangements => with(plugin, |p| match p.bus_arrangements() {
Ok(arrangements) => HostResponse::BusArrangements { arrangements },
Err(e) => err("BusArrangements", e),
}),
HostCommand::SetBusArrangements { inputs, outputs } => with(plugin, |p| {
match p.set_bus_arrangements(&inputs, &outputs) {
Ok(()) => HostResponse::Success {
message: "bus arrangements set".to_string(),
},
Err(e) => err("SetBusArrangements", e),
}
}),
HostCommand::GetUnits => with(plugin, |p| match p.get_units() {
Ok(units) => HostResponse::Units { units },
Err(e) => err("GetUnits", e),
}),
HostCommand::GetSelectedUnit => with(plugin, |p| match p.selected_unit() {
Ok(unit_id) => HostResponse::SelectedUnit { unit_id },
Err(e) => err("GetSelectedUnit", e),
}),
HostCommand::SelectUnit { unit_id } => with(plugin, |p| match p.select_unit(unit_id) {
Ok(()) => HostResponse::Success {
message: "unit selected".to_string(),
},
Err(e) => err("SelectUnit", e),
}),
HostCommand::ProgramPitchNames {
program_list_id,
program_index,
} => with(plugin, |p| {
match p.program_pitch_names(program_list_id, program_index) {
Ok(names) => HostResponse::ProgramPitchNames { names },
Err(e) => err("ProgramPitchNames", e),
}
}),
HostCommand::GetProgramData {
program_list_id,
program_index,
} => with(plugin, |p| {
match p.get_program_data(program_list_id, program_index) {
Ok(data) => HostResponse::OpaqueData {
supported: data.is_some(),
data: data.unwrap_or_default(),
},
Err(e) => err("GetProgramData", e),
}
}),
HostCommand::SetProgramData {
program_list_id,
program_index,
data,
} => with(plugin, |p| {
match p.set_program_data(program_list_id, program_index, &data) {
Ok(()) => HostResponse::Success {
message: "program data restored".to_string(),
},
Err(e) => err("SetProgramData", e),
}
}),
HostCommand::GetUnitData { unit_id } => with(plugin, |p| match p.get_unit_data(unit_id) {
Ok(data) => HostResponse::OpaqueData {
supported: data.is_some(),
data: data.unwrap_or_default(),
},
Err(e) => err("GetUnitData", e),
}),
HostCommand::SetUnitData { unit_id, data } => {
with(plugin, |p| match p.set_unit_data(unit_id, &data) {
Ok(()) => HostResponse::Success {
message: "unit data restored".to_string(),
},
Err(e) => err("SetUnitData", e),
})
}
HostCommand::BeginHostEdit { parameter_id } => {
with(plugin, |p| match p.begin_host_edit(parameter_id) {
Ok(()) => HostResponse::Success {
message: "host edit begun".to_string(),
},
Err(e) => err("BeginHostEdit", e),
})
}
HostCommand::EndHostEdit { parameter_id } => {
with(plugin, |p| match p.end_host_edit(parameter_id) {
Ok(()) => HostResponse::Success {
message: "host edit ended".to_string(),
},
Err(e) => err("EndHostEdit", e),
})
}
HostCommand::SendMidiLearn {
bus,
channel,
controller,
} => with(plugin, |p| {
match p.send_midi_learn(bus, channel, controller) {
Ok(()) => HostResponse::Success {
message: "MIDI learn notified".to_string(),
},
Err(e) => err("SendMidiLearn", e),
}
}),
HostCommand::SetAutomationState { state } => {
with(plugin, |p| match p.set_automation_state(state) {
Ok(()) => HostResponse::Success {
message: "automation state set".to_string(),
},
Err(e) => err("SetAutomationState", e),
})
}
HostCommand::RemapParameterId {
old_plugin_uid,
old_param_id,
} => with(plugin, |p| {
match p.remap_parameter_id(&old_plugin_uid, old_param_id) {
Ok(id) => HostResponse::RemappedParameter { id },
Err(e) => err("RemapParameterId", e),
}
}),
HostCommand::LatencySamples => with(plugin, |p| HostResponse::LatencySamples {
samples: p.latency_samples(),
}),
HostCommand::TailSamples => with(plugin, |p| HostResponse::TailSamples {
samples: p.tail_samples(),
}),
HostCommand::MidiCcToParameter { bus, channel, cc } => {
with(plugin, |p| HostResponse::MidiParameterMapping {
id: p.midi_cc_to_parameter(bus, channel, cc),
})
}
HostCommand::Process { inputs, frames } => {
let sr = *sample_rate;
with(plugin, |p| {
let out_channels = p.output_channel_count().max(1);
let mut buffers = AudioBuffers {
inputs,
outputs: vec![vec![0.0; frames as usize]; out_channels],
sample_rate: sr,
block_size: frames as usize,
};
match p.process_audio(&mut buffers) {
Ok(()) => HostResponse::AudioOutput {
outputs: buffers.outputs,
output_events: p.take_output_events(),
},
Err(e) => err("Process", e),
}
})
}
HostCommand::ProcessBuses {
inputs,
outputs,
frames,
} => {
if inputs.len() > 256
|| outputs.len() > 256
|| frames as usize > (1 << 20)
|| outputs.iter().any(|bus| bus.channel_count > 256)
{
return HostResponse::Error {
message: "ProcessBuses: bus shape exceeds wire limits".to_string(),
};
}
let sr = *sample_rate;
with(plugin, |p| {
let mut buffers = vst3_host::BusAudioBuffers {
inputs,
outputs: outputs
.iter()
.map(|config| {
vst3_host::AudioBusBuffer::new(
config.channel_count,
frames as usize,
config.active,
)
})
.collect(),
sample_rate: sr,
block_size: frames as usize,
};
match p.process_bus_audio(&mut buffers) {
Ok(()) => HostResponse::BusAudioOutput {
outputs: buffers.outputs,
output_events: p.take_output_events(),
},
Err(e) => err("ProcessBuses", e),
}
})
}
HostCommand::AudioBusLayout => with(plugin, |p| match p.audio_bus_layout() {
Ok(layout) => HostResponse::AudioBusLayout { layout },
Err(e) => err("AudioBusLayout", e),
}),
HostCommand::SaveState => with(plugin, |p| match p.save_state() {
Ok(data) => HostResponse::State { data },
Err(e) => err("SaveState", e),
}),
HostCommand::LoadState { data, context } => with(plugin, |p| {
match p.load_state_with_context(&data, &context) {
Ok(()) => HostResponse::Success {
message: "state restored".to_string(),
},
Err(e) => err("LoadState", e),
}
}),
HostCommand::NoteOn {
channel,
note,
velocity,
sample_offset,
} => with(plugin, |p| {
let Some(ch) = vst3_host::MidiChannel::from_index(channel) else {
return HostResponse::Error {
message: format!("NoteOn: invalid channel index {channel}"),
};
};
match p.note_on_at(ch, note, velocity, sample_offset) {
Ok(id) => HostResponse::NoteStarted { note_id: id.raw() },
Err(e) => err("NoteOn", e),
}
}),
HostCommand::NoteOff {
note_id,
sample_offset,
} => with(plugin, |p| {
match p.note_off_at(vst3_host::NoteId::from_raw(note_id), sample_offset) {
Ok(()) => HostResponse::Success {
message: "note off".to_string(),
},
Err(e) => err("NoteOff", e),
}
}),
HostCommand::SendNoteExpression {
note_id,
kind,
value,
sample_offset,
} => with(plugin, |p| {
match p.send_note_expression_at(
vst3_host::NoteId::from_raw(note_id),
kind,
value,
sample_offset,
) {
Ok(()) => HostResponse::Success {
message: "note expression sent".to_string(),
},
Err(e) => err("SendNoteExpression", e),
}
}),
HostCommand::NoteExpressions { bus: _, channel: _ } => {
with(plugin, |p| match p.note_expressions() {
Ok(expressions) => HostResponse::NoteExpressions { expressions },
Err(e) => err("NoteExpressions", e),
})
}
HostCommand::SelectProgram {
unit_id,
program_index,
} => with(plugin, |p| match p.select_program(unit_id, program_index) {
Ok(()) => HostResponse::Success {
message: "program selected".to_string(),
},
Err(e) => err("SelectProgram", e),
}),
HostCommand::TakeParameterEdits => with(plugin, |p| HostResponse::ParameterEdits {
edits: p.take_parameter_edits(),
}),
HostCommand::TakeParameterChanges => with(plugin, |p| HostResponse::ParameterChanges {
changes: p.get_parameter_changes(),
}),
HostCommand::TakeHostNotifications => with(plugin, |p| HostResponse::HostNotifications {
notifications: p.take_host_notifications(),
}),
HostCommand::TakeDataExchangeBlocks => with(plugin, |p| HostResponse::DataExchangeBlocks {
blocks: p.take_data_exchange_blocks(),
}),
HostCommand::ExecuteContextMenuItem { menu_id, item_id } => with(plugin, |p| {
match p.execute_context_menu_item(menu_id, item_id) {
Ok(()) => HostResponse::Success {
message: "context-menu item executed".to_string(),
},
Err(error) => err("ExecuteContextMenuItem", error),
}
}),
HostCommand::DismissContextMenu { menu_id } => {
with(plugin, |p| match p.dismiss_context_menu(menu_id) {
Ok(()) => HostResponse::Success {
message: "context menu dismissed".to_string(),
},
Err(error) => err("DismissContextMenu", error),
})
}
HostCommand::TakeRestartFlags => with(plugin, |p| HostResponse::RestartFlags {
bits: p.take_restart_flags().bits(),
}),
HostCommand::ServiceHostRequests => with(plugin, |p| match p.service_host_requests() {
Ok(flags) => HostResponse::RestartFlags { bits: flags.bits() },
Err(error) => err("ServiceHostRequests", error),
}),
HostCommand::CreateGui => gui_request(gui, true),
HostCommand::CloseGui => gui_request(gui, false),
HostCommand::Shutdown => HostResponse::Success {
message: "shutting down".to_string(),
},
}
}
#[cfg(target_os = "macos")]
struct GuiChannel(std::sync::mpsc::Sender<GuiRequest>);
#[cfg(not(target_os = "macos"))]
struct GuiChannel;
fn gui_request(gui: Option<&GuiChannel>, open: bool) -> HostResponse {
#[cfg(target_os = "macos")]
{
let Some(GuiChannel(tx)) = gui else {
return HostResponse::Error {
message: "GUI loop unavailable".to_string(),
};
};
let (reply_tx, reply_rx) = std::sync::mpsc::channel();
if tx
.send(GuiRequest {
open,
reply: reply_tx,
})
.is_err()
{
return HostResponse::Error {
message: "GUI loop is gone".to_string(),
};
}
reply_rx.recv().unwrap_or(HostResponse::Error {
message: "GUI loop did not reply".to_string(),
})
}
#[cfg(not(target_os = "macos"))]
{
let _ = (gui, open);
HostResponse::Error {
message: "Plugin GUI is not supported across process isolation on this platform"
.to_string(),
}
}
}
#[cfg(target_os = "macos")]
mod macos {
use super::*;
use objc2::rc::Retained;
use objc2::{MainThreadMarker, MainThreadOnly};
use objc2_app_kit::{
NSApplication, NSApplicationActivationPolicy, NSBackingStoreType, NSEventMask, NSView,
NSWindow, NSWindowStyleMask,
};
use objc2_foundation::{NSDate, NSDefaultRunLoopMode, NSPoint, NSRect, NSSize, NSString};
use std::sync::mpsc;
pub fn run(plugin: SharedPlugin, mut protocol: ProtocolChannel) {
let (gui_tx, gui_rx) = mpsc::channel::<GuiRequest>();
let (shutdown_tx, shutdown_rx) = mpsc::channel::<()>();
{
let plugin = plugin.clone();
std::thread::spawn(move || {
let stdin = io::stdin();
let mut sample_rate = 44100.0;
let gui = GuiChannel(gui_tx);
for line in stdin.lock().lines() {
let Some(command) = parse_line(line, &mut protocol) else {
continue;
};
if matches!(command, HostCommand::Shutdown) {
eprintln!("Shutting down helper process");
let _ = shutdown_tx.send(());
break;
}
let response = handle(command, &plugin, &mut sample_rate, Some(&gui));
respond(&mut protocol, &response);
}
let _ = shutdown_tx.send(());
});
}
run_event_loop(&plugin, &gui_rx, &shutdown_rx);
}
fn run_event_loop(
plugin: &SharedPlugin,
gui_rx: &mpsc::Receiver<GuiRequest>,
shutdown_rx: &mpsc::Receiver<()>,
) {
let mtm = MainThreadMarker::new().expect("helper UI loop must run on the main thread");
let app = NSApplication::sharedApplication(mtm);
app.setActivationPolicy(NSApplicationActivationPolicy::Accessory);
app.finishLaunching();
let mut window: Option<Retained<NSWindow>> = None;
loop {
if shutdown_rx.try_recv().is_ok() {
break;
}
while let Ok(req) = gui_rx.try_recv() {
let response = if req.open {
match open_editor_window(plugin, mtm, &app) {
Ok((w, width, height)) => {
window = Some(w);
HostResponse::GuiCreated { width, height }
}
Err(e) => HostResponse::Error { message: e },
}
} else {
close_editor_window(plugin, window.take());
HostResponse::Success {
message: "editor closed".to_string(),
}
};
let _ = req.reply.send(response);
}
let until = NSDate::dateWithTimeIntervalSinceNow(0.02);
while let Some(event) = unsafe {
app.nextEventMatchingMask_untilDate_inMode_dequeue(
NSEventMask::Any,
Some(&until),
NSDefaultRunLoopMode,
true,
)
} {
app.sendEvent(&event);
}
}
close_editor_window(plugin, window.take());
}
fn open_editor_window(
plugin: &SharedPlugin,
mtm: MainThreadMarker,
app: &NSApplication,
) -> std::result::Result<(Retained<NSWindow>, i32, i32), String> {
let mut guard = plugin
.lock()
.map_err(|_| "plugin lock poisoned".to_string())?;
let p = guard
.as_mut()
.ok_or_else(|| "No plugin loaded".to_string())?;
if !p.has_editor() {
return Err("Plugin does not have a GUI editor".to_string());
}
let (width, height) = p.get_editor_size().unwrap_or((800, 600));
let title = format!("{} - VST3", p.info().name);
let frame = NSRect::new(
NSPoint::new(120.0, 120.0),
NSSize::new(width as f64, height as f64),
);
let style = NSWindowStyleMask::Titled
| NSWindowStyleMask::Closable
| NSWindowStyleMask::Miniaturizable;
let window = unsafe {
NSWindow::initWithContentRect_styleMask_backing_defer(
NSWindow::alloc(mtm),
frame,
style,
NSBackingStoreType::Buffered,
false,
)
};
unsafe { window.setReleasedWhenClosed(false) };
window.setTitle(&NSString::from_str(&title));
let container = NSView::initWithFrame(
NSView::alloc(mtm),
NSRect::new(NSPoint::new(0.0, 0.0), frame.size),
);
if let Some(content) = window.contentView() {
content.addSubview(&container);
}
let handle = unsafe {
vst3_host::WindowHandle::from_nsview(
Retained::as_ptr(&container) as *mut std::ffi::c_void
)
};
p.open_editor(handle).map_err(|e| e.to_string())?;
window.setContentSize(frame.size);
window.center();
window.makeKeyAndOrderFront(None);
app.activate();
Ok((window, width, height))
}
fn close_editor_window(plugin: &SharedPlugin, window: Option<Retained<NSWindow>>) {
if let Ok(mut guard) = plugin.lock() {
if let Some(p) = guard.as_mut() {
let _ = p.close_editor();
}
}
if let Some(w) = window {
w.close();
}
}
}