use crate::{
audio::AudioBuffers,
error::{Error, Result},
midi::MidiEvent,
parameters::Parameter,
plugin::{PluginInfo, PluginInternal},
process_isolation::{HostCommand, HostResponse, PluginHostProcess},
};
use std::path::PathBuf;
use std::sync::Mutex;
use std::time::Duration;
pub struct IsolatedPluginImpl {
process: Mutex<PluginHostProcess>,
info: PluginInfo,
sample_rate: f64,
block_size: usize,
tempo: f64,
time_sig_numerator: i32,
time_sig_denominator: i32,
is_processing: bool,
has_open_editor: bool,
editor_size: Option<(i32, i32)>,
output_channels: usize,
output_midi: Mutex<Vec<MidiEvent>>,
helper_path: Option<PathBuf>,
response_timeout: Duration,
auto_recover: bool,
auto_recover_max_retries: u32,
recovery_count: std::sync::atomic::AtomicU64,
}
const MAX_OUTPUT_MIDI: usize = 4096;
impl IsolatedPluginImpl {
#[allow(clippy::too_many_arguments)]
pub fn new(
process: PluginHostProcess,
info: PluginInfo,
sample_rate: f64,
block_size: usize,
tempo: f64,
time_sig_numerator: i32,
time_sig_denominator: i32,
output_channels: usize,
helper_path: Option<PathBuf>,
response_timeout: Duration,
auto_recover: bool,
auto_recover_max_retries: u32,
) -> Self {
Self {
process: Mutex::new(process),
info,
sample_rate,
block_size,
tempo,
time_sig_numerator,
time_sig_denominator,
is_processing: false,
has_open_editor: false,
editor_size: None,
output_channels,
output_midi: Mutex::new(Vec::new()),
helper_path,
response_timeout,
auto_recover,
auto_recover_max_retries,
recovery_count: std::sync::atomic::AtomicU64::new(0),
}
}
fn send_command_once(&self, command: HostCommand) -> Result<HostResponse> {
let mut process = self
.process
.lock()
.map_err(|e| Error::Other(format!("Failed to lock process: {}", e)))?;
process
.send_command(command)
.map_err(|e| classify_ipc_error(&e))
}
fn send_command(&self, command: HostCommand) -> Result<HostResponse> {
if !self.auto_recover {
return self.send_command_once(command);
}
let mut attempt: u32 = 0;
loop {
match self.send_command_once(command.clone()) {
Ok(resp) => return Ok(resp),
Err(e) => {
let recoverable = matches!(e, Error::PluginCrashed | Error::PluginTimeout);
if !recoverable || attempt >= self.auto_recover_max_retries {
return Err(e);
}
attempt += 1;
log::warn!(
"isolated plugin crashed/hung ({e}); auto-recover attempt {attempt}/{}",
self.auto_recover_max_retries
);
if self.recover_locked().is_err() {
return Err(e);
}
}
}
}
}
}
fn classify_ipc_error(message: &str) -> Error {
let lo = message.to_lowercase();
if lo.contains("timed out") {
Error::PluginTimeout
} else if lo.contains("crash")
|| lo.contains("no longer running")
|| lo.contains("gone")
|| lo.contains("exited")
|| lo.contains("not running")
{
Error::PluginCrashed
} else {
Error::Other(format!("IPC error: {message}"))
}
}
impl IsolatedPluginImpl {
fn expect_success(&self, command: HostCommand, what: &str) -> Result<()> {
match self.send_command(command)? {
HostResponse::Success { .. } => Ok(()),
HostResponse::Error { message } => Err(Error::Other(format!("{what}: {message}"))),
_ => Err(Error::Other(format!("{what}: unexpected response"))),
}
}
}
impl PluginInternal for IsolatedPluginImpl {
fn set_parameter(&mut self, id: u32, value: f64) -> Result<()> {
self.expect_success(HostCommand::SetParameter { id, value }, "SetParameter")
}
fn set_parameter_at(&mut self, id: u32, value: f64, sample_offset: i32) -> Result<()> {
self.expect_success(
HostCommand::SetParameterAt {
id,
value,
offset: sample_offset,
},
"SetParameterAt",
)
}
fn get_parameter(&self, id: u32) -> Result<f64> {
match self.send_command(HostCommand::GetParameter { id })? {
HostResponse::ParameterValue { value } => Ok(value),
HostResponse::Error { message } => {
Err(Error::Other(format!("GetParameter: {message}")))
}
_ => Err(Error::Other(
"GetParameter: unexpected response".to_string(),
)),
}
}
fn get_all_parameters(&self) -> Result<Vec<Parameter>> {
match self.send_command(HostCommand::GetAllParameters)? {
HostResponse::Parameters { params } => Ok(params),
HostResponse::Error { message } => {
Err(Error::Other(format!("GetAllParameters: {message}")))
}
_ => Err(Error::Other(
"GetAllParameters: unexpected response".to_string(),
)),
}
}
fn format_parameter(&self, id: u32, normalized: f64) -> Result<String> {
match self.send_command(HostCommand::FormatParameter { id, normalized })? {
HostResponse::ParameterString { value } => Ok(value),
HostResponse::Error { message } => {
Err(Error::Other(format!("FormatParameter: {message}")))
}
_ => Err(Error::Other(
"FormatParameter: unexpected response".to_string(),
)),
}
}
fn process(&mut self, buffers: &mut AudioBuffers) -> Result<()> {
let frames = buffers
.outputs
.first()
.map(|c| c.len())
.unwrap_or(self.block_size);
let response = self.send_command_once(HostCommand::Process {
inputs: buffers.inputs.clone(),
frames: frames as u32,
})?;
match response {
HostResponse::AudioOutput {
outputs,
output_midi,
} => {
for (ch_idx, output_channel) in buffers.outputs.iter_mut().enumerate() {
if let Some(src) = outputs.get(ch_idx) {
let n = output_channel.len().min(src.len());
output_channel[..n].copy_from_slice(&src[..n]);
for s in &mut output_channel[n..] {
*s = 0.0;
}
} else {
output_channel.fill(0.0);
}
}
if !output_midi.is_empty() {
if let Ok(mut buf) = self.output_midi.lock() {
buf.extend(output_midi);
if buf.len() > MAX_OUTPUT_MIDI {
let drop = buf.len() - MAX_OUTPUT_MIDI;
buf.drain(0..drop);
}
}
}
Ok(())
}
HostResponse::Error { message } => {
Err(Error::ProcessError(format!("Process error: {}", message)))
}
_ => Err(Error::Other(
"Unexpected response from process command".to_string(),
)),
}
}
fn send_midi_event(&mut self, event: MidiEvent) -> Result<()> {
self.expect_success(HostCommand::SendMidi { event }, "SendMidi")
}
fn note_on(
&mut self,
channel: crate::midi::MidiChannel,
note: u8,
velocity: u8,
sample_offset: i32,
) -> Result<crate::midi::NoteId> {
match self.send_command(HostCommand::NoteOn {
channel: channel.as_index(),
note,
velocity,
sample_offset,
})? {
HostResponse::NoteStarted { note_id } => Ok(crate::midi::NoteId(note_id)),
HostResponse::Error { message } => Err(Error::Other(format!("NoteOn: {message}"))),
_ => Err(Error::Other("NoteOn: unexpected response".to_string())),
}
}
fn note_off(&mut self, id: crate::midi::NoteId, sample_offset: i32) -> Result<()> {
self.expect_success(
HostCommand::NoteOff {
note_id: id.raw(),
sample_offset,
},
"NoteOff",
)
}
fn send_note_expression(
&mut self,
id: crate::midi::NoteId,
kind: crate::midi::NoteExpressionType,
value: f64,
sample_offset: i32,
) -> Result<()> {
self.expect_success(
HostCommand::SendNoteExpression {
note_id: id.raw(),
kind,
value,
sample_offset,
},
"SendNoteExpression",
)
}
fn note_expressions(
&self,
bus: i32,
channel: i16,
) -> Result<Vec<crate::midi::NoteExpressionInfo>> {
match self.send_command(HostCommand::NoteExpressions { bus, channel })? {
HostResponse::NoteExpressions { expressions } => Ok(expressions),
HostResponse::Error { message } => {
Err(Error::Other(format!("NoteExpressions: {message}")))
}
_ => Err(Error::Other(
"NoteExpressions: unexpected response".to_string(),
)),
}
}
fn start_processing(&mut self) -> Result<()> {
self.expect_success(HostCommand::StartProcessing, "StartProcessing")?;
self.is_processing = true;
Ok(())
}
fn stop_processing(&mut self) -> Result<()> {
self.expect_success(HostCommand::StopProcessing, "StopProcessing")?;
self.is_processing = false;
Ok(())
}
fn has_editor(&self) -> bool {
self.info.has_gui
}
fn open_editor(&mut self, _parent: *mut std::ffi::c_void) -> Result<()> {
let response = self.send_command(HostCommand::CreateGui)?;
match response {
HostResponse::GuiCreated { width, height } => {
self.editor_size = Some((width, height));
self.has_open_editor = true;
Ok(())
}
HostResponse::Success { .. } => {
self.has_open_editor = true;
Ok(())
}
HostResponse::Error { message } => {
Err(Error::Other(format!("Failed to open editor: {}", message)))
}
_ => Err(Error::Other(
"Unexpected response from CreateGui command".to_string(),
)),
}
}
fn close_editor(&mut self) -> Result<()> {
if !self.has_open_editor {
return Ok(());
}
let response = self.send_command(HostCommand::CloseGui)?;
match response {
HostResponse::Success { .. } => {
self.has_open_editor = false;
Ok(())
}
HostResponse::Error { message } => {
Err(Error::Other(format!("Failed to close editor: {}", message)))
}
_ => Err(Error::Other(
"Unexpected response from CloseGui command".to_string(),
)),
}
}
fn get_editor_size(&self) -> Result<(i32, i32)> {
Ok(self.editor_size.unwrap_or((800, 600)))
}
fn get_parameter_changes(&self) -> Vec<(u32, f64)> {
Vec::new()
}
fn save_state(&self) -> Result<Vec<u8>> {
match self.send_command(HostCommand::SaveState)? {
HostResponse::State { data } => Ok(data),
HostResponse::Error { message } => Err(Error::Other(format!("SaveState: {message}"))),
_ => Err(Error::Other("SaveState: unexpected response".to_string())),
}
}
fn load_state(&mut self, data: &[u8]) -> Result<()> {
self.expect_success(
HostCommand::LoadState {
data: data.to_vec(),
},
"LoadState",
)
}
fn take_output_events(&self) -> Vec<MidiEvent> {
self.output_midi
.lock()
.map(|mut o| std::mem::take(&mut *o))
.unwrap_or_default()
}
fn output_channel_count(&self) -> usize {
self.output_channels
}
fn helper_pid(&self) -> Option<u32> {
self.process.lock().ok().and_then(|p| p.helper_pid())
}
fn recovery_count(&self) -> u64 {
self.recovery_count
.load(std::sync::atomic::Ordering::Relaxed)
}
fn recover(&mut self) -> Result<()> {
self.recover_locked()
}
}
impl IsolatedPluginImpl {
fn recover_locked(&self) -> Result<()> {
let mut process = self
.process
.lock()
.map_err(|e| Error::Other(format!("Failed to lock process: {}", e)))?;
let mut fresh = PluginHostProcess::new(self.helper_path.clone(), self.response_timeout)
.map_err(|e| Error::ProcessError(format!("Failed to respawn helper: {e}")))?;
match fresh.send_command(HostCommand::LoadPlugin {
path: self.info.path.display().to_string(),
sample_rate: self.sample_rate,
block_size: self.block_size as u32,
tempo: self.tempo,
time_sig_numerator: self.time_sig_numerator,
time_sig_denominator: self.time_sig_denominator,
}) {
Ok(HostResponse::PluginInfo { .. }) => {}
Ok(HostResponse::Error { message }) => {
return Err(Error::PluginLoadFailed(format!("reload failed: {message}")))
}
Ok(_) => return Err(Error::Other("unexpected response while reloading".into())),
Err(e) => return Err(classify_ipc_error(&e)),
}
if self.is_processing {
let _ = fresh.send_command(HostCommand::StartProcessing);
}
*process = fresh;
self.recovery_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
Ok(())
}
}
unsafe impl Send for IsolatedPluginImpl {}