diff --git a/examples/custom_nodes/Cargo.toml b/examples/custom_nodes/Cargo.toml index 56a045a1..5f077ecb 100644 --- a/examples/custom_nodes/Cargo.toml +++ b/examples/custom_nodes/Cargo.toml @@ -21,3 +21,10 @@ eframe = { version = "0.32", default-features = false, features = [ "x11", "wayland", ] } +clack_host = { git = "https://github.com/prokopyl/clack", package = "clack-host", features = ["clack-plugin"] } +clack_extensions = { git = "https://github.com/prokopyl/clack", package = "clack-extensions", features = ["params", "note-ports"] } +clack_plugin = { git = "https://github.com/prokopyl/clack", package = "clack-plugin" } +clack_common = { git = "https://github.com/prokopyl/clack", package = "clack-common" } +cpal = "0.16.0" +clap-sys = "0.4.0" +windows-sys = { version = "0.52", features = ["Win32_System_LibraryLoader"] } \ No newline at end of file diff --git a/examples/custom_nodes/src/nodes/assets/polly.clap b/examples/custom_nodes/src/nodes/assets/polly.clap new file mode 100644 index 00000000..bea2cd69 Binary files /dev/null and b/examples/custom_nodes/src/nodes/assets/polly.clap differ diff --git a/examples/custom_nodes/src/nodes/clap_plugin.rs b/examples/custom_nodes/src/nodes/clap_plugin.rs new file mode 100644 index 00000000..99e4ed18 --- /dev/null +++ b/examples/custom_nodes/src/nodes/clap_plugin.rs @@ -0,0 +1,336 @@ + +#![cfg(not(target_arch = "wasm32"))] + +use std::ffi::CString; +use clack_host::events::event_types::NoteOnEvent; +use clack_host::prelude::*; +use clack_host::events::io::{InputEvents, OutputEvents}; +use clack_host::process::StartedPluginAudioProcessor; + +use firewheel::{ + channel_config::{ChannelConfig, ChannelCount}, + diff::PatchError, + event::{ParamData, ProcEvents}, + node::{AudioNode, AudioNodeInfo, AudioNodeProcessor, ConstructProcessorContext, ProcBuffers, ProcExtra, ProcInfo, ProcessStatus}, +}; +use firewheel::diff::EventQueue; +use clack_host::host::{SharedHandler, MainThreadHandler, AudioProcessorHandler}; + +use std::path::Path; +use windows_sys::Win32::System::LibraryLoader::SetDllDirectoryW; +use std::ffi::OsStr; +use std::os::windows::ffi::OsStrExt; + +fn set_dll_directory>(path: P) { + let wide: Vec = OsStr::new(path.as_ref()) + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + unsafe { + SetDllDirectoryW(wide.as_ptr()); + } +} + + +// ---- Define your node type ---- + +#[derive(Clone, Default)] +pub struct ClapPluginNode { + pub path: String, + pub enabled: bool, +} + +// You need to inspect AudioNode trait in your version and implement its expected items: +impl AudioNode for ClapPluginNode { + type Configuration = ClapPluginConfig; + + fn info(&self, _cfg: &Self::Configuration) -> AudioNodeInfo { + AudioNodeInfo::new() + .debug_name("clap_plugin_node") + .channel_config(ChannelConfig { + num_inputs: ChannelCount::STEREO, + num_outputs: ChannelCount::STEREO, + }) + } + + fn construct_processor( + &self, + _cfg: &Self::Configuration, + _ctx: ConstructProcessorContext<'_>, + ) -> impl AudioNodeProcessor { + let hardcoded_node = ClapPluginNode { + // insert your own path: std::path::PathBuf::from(r"C:\Program Files\Common Files\CLAP\your_plugin_name.clap") + path: std::path::PathBuf::from("assets/polly.clap") + .to_string_lossy() + .to_string(), + ..self.clone() + }; + + let cfg = ClapPluginConfig { + sample_rate: 48000.0, + block_size: 1024, + }; + + let processor_result = ClapPluginProcessor::new(hardcoded_node, cfg); + + match processor_result { + Ok(ref processor) => { + println!("✅ ClapPluginProcessor successfully created:"); + println!( + " - plugin: {}", + if processor.plugin.is_some() { "Some" } else { "None" } + ); + println!( + " - audio_proc: {}", + if processor.audio_proc.is_some() { "Some" } else { "None" } + ); + println!(" - enabled: {}", processor.enabled); + } + Err(ref err) => { + eprintln!("❌ Failed to construct CLAP plugin processor: {err} \n + In order to construct the plugin, \n + both plugin location and ID must match to be registered"); + } + } + + // Unwrap or panic after printing debug info + processor_result.expect("❌ Failed to construct CLAP plugin processor \n + In order to construct the plugin, \n + both plugin location and ID must match to be registered") + } + + +} + +// Patch-type and Diff-type definitions +#[derive(Clone)] +pub enum ClapPluginPatch { + Enabled(bool), +} + +impl firewheel::diff::Patch for ClapPluginNode { + type Patch = ClapPluginPatch; + + fn patch(data: &ParamData, _path: &[u32]) -> Result { + if let Some(b) = data.downcast_ref::() { + Ok(ClapPluginPatch::Enabled(*b)) + } else { + Err(PatchError::InvalidData) + } + } + + fn apply(&mut self, patch: ClapPluginPatch) { + match patch { + ClapPluginPatch::Enabled(value) => { + self.enabled = value; + } + } + } +} + +impl firewheel::diff::Diff for ClapPluginNode { + fn diff( + &self, + other: &Self, + path: firewheel::diff::PathBuilder, + queue: &mut E, + ) { + if self.enabled != other.enabled { + // ✅ Correct: param-based diff + queue.push_param(self.enabled, path); + + + } + } +} + + +// ----- Configuration ----- + +#[derive(Clone, Default)] +pub struct ClapPluginConfig { + pub sample_rate: f32, + pub block_size: usize, +} + +// ----- Processor ----- + +pub struct ClapPluginProcessor { + plugin: Option>, + audio_proc: Option>, + enabled: bool, +} + +unsafe impl Send for ClapPluginProcessor {} + + +impl AudioNodeProcessor for ClapPluginProcessor { + fn process( + &mut self, + _info: &ProcInfo, + _buffers: ProcBuffers, + events: &mut ProcEvents, + _extra: &mut ProcExtra, + ) -> ProcessStatus { + if !self.enabled { + return ProcessStatus::ClearAllOutputs; + } + let _proc = match &mut self.audio_proc { + Some(p) => p, + None => return ProcessStatus::ClearAllOutputs, + }; + + // Handle patches + for patch in events.drain_patches::() { + match patch { + ClapPluginPatch::Enabled(v) => self.enabled = v, + } + } + + + // TODO: convert MIDI events properly for your clack_host version + + let clap_buf: Vec = Vec::new(); + for _midi in events.drain() { + // Example pseudo: get raw bytes + // let bytes = midi.as_midi_bytes(); // depending on your version + // match bytes[0] & 0xF0 { ... produce NoteOnEvent / NoteOffEvent } + } + + // TODO: prepare audio buffer types accepted by proc.process (not AudioPorts directly) + // e.g. InputAudioBuffers, OutputAudioBuffers + // then call proc.process(&in_bufs, &mut out_bufs, &input_events, &mut output_events, None, None) + + let _input_events = InputEvents::from_buffer(&clap_buf); + let _output_events = OutputEvents::void(); + + // Pseudo: + // let status = proc.process(&in_bufs, &mut out_bufs, &input_events, &mut output_events, None, None); + + // match status { + // Ok(_) => ProcessStatus::OutputsModified, + // Err(_) => ProcessStatus::ClearAllOutputs, + // } + + ProcessStatus::ClearAllOutputs // placeholder + } +} + +impl ClapPluginProcessor { + pub fn new( + node: ClapPluginNode, + cfg: ClapPluginConfig, + ) -> Result> { + let path = std::fs::canonicalize(&node.path) + .expect("Failed to resolve full path to plugin"); + println!("ClapPluginProcessor::new: loading from resolved path = {:?}", path.display()); + + if let Some(parent) = path.parent() { + set_dll_directory(parent); + println!("Set CLAP directory to {:?}", parent); + } + + // Load the plugin bundle + let bundle = unsafe { PluginBundle::load(path.clone())? }; + + // Get the factory from the bundle + let factory = bundle.get_plugin_factory() + .ok_or("Failed to get plugin factory")?; + + // List all plugins inside the bundle with their IDs and names + println!("Available plugins in bundle:"); + for descriptor in factory.plugin_descriptors() { + let id_str = descriptor.id() + .and_then(|cstr| cstr.to_str().ok()) + .unwrap_or("Unknown ID"); + + let name_str = descriptor.name() + .and_then(|cstr| cstr.to_str().ok()) + .unwrap_or("Unknown Name"); + + println!(" - ID: {}, Name: {}", id_str, name_str); + } + + // TODO: Replace this with the exact plugin ID you want to instantiate! + // For example, after running, pick an ID from the printed list + // let plugin_id_str = "polly"; // <-- Replace "polly" with your exact plugin ID string + let plugin_id = CString::new("hqsoundz.polly")?; + +// let plugin_id = CString::new(plugin_id_str)?; + + // Create host info for the plugin + let host_info = HostInfo::new("MyHost", "MyVendor", "http://localhost", "1.0")?; + + // Try to create plugin instance + let mut instance = PluginInstance::::new( + |_| MinimalShared::default(), + |_| MinimalMainThread::default(), + &bundle, + &plugin_id, + &host_info, + )?; + + let audio_config = PluginAudioConfiguration { + sample_rate: cfg.sample_rate as f64, + min_frames_count: cfg.block_size as u32, + max_frames_count: cfg.block_size as u32, + }; + + let started = { + instance.activate( + |_shared, _main| ClapPluginProcessor { + plugin: None, + audio_proc: None, + enabled: node.enabled, + }, + audio_config, + )? + } + .start_processing()?; + + Ok(ClapPluginProcessor { + plugin: Some(instance), + audio_proc: Some(started), + enabled: node.enabled, + }) + } +} + + +impl<'a> AudioProcessorHandler<'a> for ClapPluginProcessor {} +// ----- Minimal Host ----- + +#[derive(Default)] +pub struct MinimalShared; + +// impl<'a> SharedHandler<'a> for MinimalShared {} +impl<'a> SharedHandler<'a> for MinimalShared { + fn request_restart(&self) { + println!("Host requested plugin restart."); + } + + fn request_process(&self) { + println!("Host requested process."); + } + + fn request_callback(&self) { + println!("Host requested callback."); + } +} + +#[derive(Default)] +pub struct MinimalMainThread; + +impl<'a> MainThreadHandler<'a> for MinimalMainThread {} + +pub struct MinimalHost; + +impl HostHandlers for MinimalHost { + type Shared<'a> = MinimalShared; + type MainThread<'a> = MinimalMainThread; + type AudioProcessor<'a> = ClapPluginProcessor; + + fn declare_extensions(_builder: &mut HostExtensions, _shared: &Self::Shared<'_>) { + // Only register extensions that implement ExtensionImplementation + } +} diff --git a/examples/custom_nodes/src/nodes/mod.rs b/examples/custom_nodes/src/nodes/mod.rs index 4c2301e0..08c47ffe 100644 --- a/examples/custom_nodes/src/nodes/mod.rs +++ b/examples/custom_nodes/src/nodes/mod.rs @@ -1,3 +1,5 @@ pub mod filter; pub mod noise_gen; pub mod rms; +pub mod clap_plugin; + diff --git a/examples/custom_nodes/src/system.rs b/examples/custom_nodes/src/system.rs index 2687e728..9e6ab687 100644 --- a/examples/custom_nodes/src/system.rs +++ b/examples/custom_nodes/src/system.rs @@ -1,6 +1,6 @@ use firewheel::{diff::Memo, error::UpdateError, node::NodeID, FirewheelContext}; -use crate::nodes::{filter::FilterNode, noise_gen::NoiseGenNode, rms::RmsNode}; +use crate::nodes::{filter::FilterNode, noise_gen::NoiseGenNode, rms::RmsNode, clap_plugin::ClapPluginNode}; pub struct AudioSystem { pub cx: FirewheelContext, @@ -8,10 +8,12 @@ pub struct AudioSystem { pub noise_gen_node: Memo, pub filter_node: Memo, pub rms_node: Memo, + pub clap_node: Memo, pub noise_gen_node_id: NodeID, pub filter_node_id: NodeID, - pub rms_node_id: NodeID, + pub rms_node_id: NodeID, + pub clap_node_id: NodeID, } impl AudioSystem { @@ -23,9 +25,17 @@ impl AudioSystem { let filter_node = FilterNode::default(); let rms_node = RmsNode::default(); + let clap_node = ClapPluginNode { + // Change this to your actual path + path: "C:/Program Files/CLAP/MyPlugin.clap".into(), + enabled: true, + // TODO: add parameters to talk to the plugin + }; + let noise_gen_node_id = cx.add_node(noise_gen_node, None); let filter_node_id = cx.add_node(filter_node, None); let rms_node_id = cx.add_node(rms_node.clone(), None); + let clap_node_id = cx.add_node(clap_node.clone(), None); let graph_out_node_id = cx.graph_out_node_id(); @@ -35,15 +45,20 @@ impl AudioSystem { .unwrap(); cx.connect(filter_node_id, graph_out_node_id, &[(0, 0), (0, 1)], false) .unwrap(); + cx.connect(clap_node_id, graph_out_node_id, &[(0, 0), (1, 1)], false) + .unwrap(); Self { cx, noise_gen_node: Memo::new(noise_gen_node), filter_node: Memo::new(filter_node), rms_node: Memo::new(rms_node), + clap_node: Memo::new(clap_node), + noise_gen_node_id, filter_node_id, rms_node_id, + clap_node_id, } } diff --git a/examples/custom_nodes/src/ui.rs b/examples/custom_nodes/src/ui.rs index 984ddb85..3b40f43e 100644 --- a/examples/custom_nodes/src/ui.rs +++ b/examples/custom_nodes/src/ui.rs @@ -107,6 +107,23 @@ impl App for DemoApp { // The rms value is quite low, so scale it up to register on the meter better. ui.add(ProgressBar::new(rms_value * 2.0).fill(Color32::DARK_GREEN)); + + ui.separator(); + ui.label("CLAP plugin"); + + if ui + .checkbox(&mut self.audio_system.clap_node.enabled, "enabled") + .changed() + { + self.audio_system.clap_node.update_memo( + &mut self + .audio_system + .cx + .event_queue(self.audio_system.clap_node_id), + ); + } + + }); self.audio_system.update();