SUPERB
Examples

clap_host_scan

examples/primitives/clap_host_scan.rs, the whole program

Run it with cargo run --example clap_host_scan (see the examples for the features some need).

//! Scans the default CLAP directories, plus `CLAP_PATH` and any directories
//! on the command line. With no audio effect it returns. Otherwise it lists
//! that plugin's parameters and renders one second of a 440 Hz sine through it.
//!
//! ```text
//! cargo run --example clap_host_scan -- [dir ...] [--id com.vendor.plugin]
//! ```

use std::path::PathBuf;

use superb::primitives::graph::nodes::SineNode;
use superb::primitives::graph::AudioGraph;
use superb::primitives::io::{offline, Engine};
use superb::primitives::plugins::clap::ClapBundle;
use superb::primitives::plugins::scan::{
    default_search_paths, scan_default, scan_directory, ScanReport,
};
use superb::primitives::plugins::{PluginFormat, PluginInstance, PluginNode};
use superb::primitives::transport::Transport;
use superb::primitives::{Result, SampleRate};

fn main() -> Result<()> {
    // Extra directories, and an optional `--id` that consumes the next token.
    let mut dirs: Vec<PathBuf> = Vec::new();
    let mut wanted_id: Option<String> = None;
    let mut args = std::env::args().skip(1);
    while let Some(a) = args.next() {
        if a == "--id" {
            wanted_id = args.next();
        } else {
            dirs.push(PathBuf::from(a));
        }
    }

    // `+` means the directory exists. Defaults that are missing still print.
    println!("default CLAP search paths:");
    for p in default_search_paths(PluginFormat::Clap) {
        println!("  {} {}", if p.is_dir() { "+" } else { "-" }, p.display());
    }

    // OS defaults and `CLAP_PATH`, then each extra directory. A directory
    // that cannot be scanned is skipped.
    let mut report: ScanReport = scan_default(PluginFormat::Clap)?;
    for dir in &dirs {
        match scan_directory(dir, PluginFormat::Clap) {
            Ok(part) => {
                report.plugins.extend(part.plugins);
                report.failures.extend(part.failures);
            }
            Err(e) => println!("skipping {}: {e}", dir.display()),
        }
    }
    println!(
        "\nfound {} plugin(s), {} bundle(s) failed to load",
        report.plugins.len(),
        report.failures.len()
    );
    for d in &report.plugins {
        println!(
            "  {:<40} {:<24} v{:<8} [{}]  {}",
            d.id,
            d.name,
            d.version,
            d.features.join(", "),
            d.path.display()
        );
    }
    for (path, why) in &report.failures {
        println!("  ! {}: {why}", path.display());
    }

    // `--id` if one was given, otherwise the first audio effect. Neither
    // is a failed run: an empty scan returns here.
    let chosen = match &wanted_id {
        Some(id) => report.plugins.iter().find(|d| &d.id == id),
        None => report.plugins.iter().find(|d| d.is_audio_effect()),
    };
    let Some(desc) = chosen else {
        println!("\nnothing to instantiate (no audio effect found); pass a directory or --id to pick one");
        return Ok(());
    };

    println!("\ninstantiating {} ({})", desc.name, desc.id);
    let bundle = ClapBundle::load(&desc.path)?;
    let plugin = bundle.instantiate(&desc.id)?;
    println!("ports: {:?}", plugin.ports());
    println!("{} parameter(s):", plugin.params().len());
    for p in plugin.params() {
        let value = plugin.param_value(p.id).unwrap_or(f64::NAN);
        let flags = [
            (p.automatable, "auto"),
            (p.stepped, "stepped"),
            (p.hidden, "hidden"),
            (p.readonly, "ro"),
            (p.bypass, "bypass"),
        ]
        .iter()
        .filter(|(on, _)| *on)
        .map(|(_, n)| *n)
        .collect::<Vec<_>>()
        .join(",");
        println!(
            "  #{:<6} {:<32} {:>10.4} in [{}, {}] default {} {}",
            p.id, p.name, value, p.min, p.max, p.default, flags
        );
    }

    // One second at 48 kHz is 48000 frames, in blocks of 256. The sine is
    // 440 Hz at amplitude 0.5, with one channel per plugin input (at least
    // one). A plugin with no input is rendered on its own.
    let rate = SampleRate::HZ_48K;
    assert_eq!(rate.seconds_to_samples(1.0), 48_000);
    let mut graph = AudioGraph::new();
    let sine = SineNode::new(440.0, 0.5, plugin.ports().inputs.max(1));
    assert!((sine.frequency().get() - 440.0).abs() < 1e-6);
    assert!((sine.amplitude().get() - 0.5).abs() < 1e-6);
    let osc = graph.add_node(Box::new(sine));
    let fx = graph.add_node(Box::new(PluginNode::new(Box::new(plugin))));
    if graph.connect(osc, fx).is_err() {
        println!("plugin has no audio input (instrument?) — rendering it unconnected");
    }
    graph.set_output(fx)?;
    let mut engine = Engine::new(graph, Transport::new(rate, 120.0));
    engine.transport_mut().play();
    let frames = rate.hz() as usize;
    let audio = offline::render(&mut engine, rate, 256, frames)?;
    assert_eq!(frames, 48_000);
    assert_eq!(audio.frames(), 48_000);
    let latency = engine.processor().latency_frames();
    println!(
        "\nrendered {} frames x {} ch through the plugin: peak {:.4}, reported latency {} frames",
        audio.frames(),
        audio.channels(),
        audio.peak(),
        latency
    );
    Ok(())
}