maolan-engine 0.2.23

Audio engine for the Maolan DAW with audio/MIDI tracks, routing, export, and out-of-process CLAP/VST3/LV2 plugin support
Documentation
pub mod clap_proc;
pub mod ipc;
#[cfg(all(unix, not(target_os = "macos")))]
pub mod lv2_proc;
pub mod types;
pub mod vst3_proc;

pub use types::*;

use serde::de::DeserializeOwned;

#[derive(serde::Deserialize)]
struct ScanDiagnostic {
    message: String,
    plugin_uri: Option<String>,
    plugin_name: Option<String>,
    bundle_uri: Option<String>,
}

#[derive(serde::Deserialize)]
struct ScanOutput<T> {
    data: T,
    errors: Vec<ScanDiagnostic>,
    warnings: Vec<ScanDiagnostic>,
}

pub fn scan_plugins<T: DeserializeOwned>(format: &str) -> Result<Vec<T>, String> {
    let host_bin = ipc::find_plugin_host_binary().ok_or("maolan-plugin-host binary not found")?;

    let mut cmd = std::process::Command::new(&host_bin);
    cmd.arg("--scan")
        .arg("--format")
        .arg(format)
        .arg("--path")
        .arg("--system");
    ipc::append_parent_log_level(&mut cmd);

    let output = cmd
        .output()
        .map_err(|e| format!("failed to spawn plugin-host scanner: {e}"))?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(format!(
            "plugin-host scanner exited with code {:?}: {stderr}",
            output.status.code()
        ));
    }

    let json = String::from_utf8_lossy(&output.stdout);
    let parsed: ScanOutput<Vec<T>> =
        serde_json::from_str(&json).map_err(|e| format!("failed to parse scan JSON: {e}"))?;

    for error in &parsed.errors {
        tracing::error!(
            message = %error.message,
            plugin_uri = ?error.plugin_uri,
            plugin_name = ?error.plugin_name,
            bundle_uri = ?error.bundle_uri,
            "plugin scan error"
        );
    }
    for warning in &parsed.warnings {
        tracing::warn!(
            message = %warning.message,
            plugin_uri = ?warning.plugin_uri,
            plugin_name = ?warning.plugin_name,
            bundle_uri = ?warning.bundle_uri,
            "plugin scan warning"
        );
    }

    Ok(parsed.data)
}

#[cfg(test)]
mod tests {
    use super::ScanOutput;

    #[test]
    fn scan_output_parses_wrapper() {
        let json = r#"{
            "errors": [
                {
                    "message": "error: failed to open manifest.ttl",
                    "bundle_uri": "file:///tmp/broken.lv2/"
                }
            ],
            "warnings": [
                {
                    "message": "warning: duplicate version",
                    "plugin_uri": "http://example.com/plugin"
                }
            ],
            "data": [{"name": "Test", "path": "/tmp/test.clap", "capabilities": null}]
        }"#;
        let output: ScanOutput<Vec<serde_json::Value>> = serde_json::from_str(json).unwrap();
        assert_eq!(output.errors.len(), 1);
        assert_eq!(
            output.errors[0].message,
            "error: failed to open manifest.ttl"
        );
        assert_eq!(
            output.errors[0].bundle_uri,
            Some("file:///tmp/broken.lv2/".to_string())
        );
        assert_eq!(output.warnings.len(), 1);
        assert_eq!(
            output.warnings[0].plugin_uri,
            Some("http://example.com/plugin".to_string())
        );
        assert_eq!(output.data.len(), 1);
        assert_eq!(output.data[0]["name"], "Test");
    }
}