├── .gitignore ├── .cargo └── config.toml ├── clap-wrapper-extensions ├── src │ ├── lib.rs │ ├── auv2.rs │ └── vst3.rs ├── README.md └── Cargo.toml ├── xtask ├── Cargo.toml ├── cmake │ ├── clap_entry.h │ ├── clap_entry.cpp │ └── CMakeLists.txt ├── README.md └── src │ └── main.rs ├── Cargo.toml ├── plugins └── gain-example │ ├── Cargo.toml │ └── src │ ├── audio_thread.rs │ ├── main_thread.rs │ └── lib.rs ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | .idea 4 | 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | xtask = "run --package xtask --release --" 3 | -------------------------------------------------------------------------------- /clap-wrapper-extensions/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This module contains definitions for CLAP extensions 2 | //! that are not included in clack. 3 | 4 | pub mod auv2; 5 | pub mod vst3; 6 | -------------------------------------------------------------------------------- /xtask/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xtask" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | # this is clap the command line parser, not the CLAP plugin API! 9 | clap = { version = "4.5.31", features = ["derive"] } 10 | -------------------------------------------------------------------------------- /clap-wrapper-extensions/README.md: -------------------------------------------------------------------------------- 1 | # clap-wrapper-extensions 2 | 3 | Rust bindings for the CLAP extensions that clap-wrapper [provides](https://github.com/free-audio/clap-wrapper/tree/main/include/clapwrapper). 4 | Written by [@Prokopyl](https://github.com/prokopyl). 5 | 6 | These should hopefully go into the main clack project. -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["xtask", "clap-wrapper-extensions", "plugins/*"] 3 | resolver = "2" 4 | 5 | [workspace.package] 6 | edition = "2021" 7 | license = "MIT OR Apache-2.0" 8 | authors = ["free-audio"] 9 | 10 | [profile.release] 11 | lto = "thin" 12 | strip = "symbols" 13 | 14 | [profile.profiling] 15 | inherits = "release" 16 | debug = true 17 | strip = "none" 18 | -------------------------------------------------------------------------------- /xtask/cmake/clap_entry.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Define CLAP_EXPORT 4 | #if !defined(CLAP_EXPORT) 5 | # if defined _WIN32 || defined __CYGWIN__ 6 | # ifdef __GNUC__ 7 | # define CLAP_EXPORT __attribute__((dllexport)) 8 | # else 9 | # define CLAP_EXPORT __declspec(dllexport) 10 | # endif 11 | # else 12 | # if __GNUC__ >= 4 || defined(__clang__) 13 | # define CLAP_EXPORT __attribute__((visibility("default"))) 14 | # else 15 | # define CLAP_EXPORT 16 | # endif 17 | # endif 18 | #endif 19 | 20 | -------------------------------------------------------------------------------- /clap-wrapper-extensions/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "clap-wrapper-extensions" 3 | version = "0.1.0" 4 | edition = "2021" # required for c-string literals 5 | 6 | [dependencies] 7 | clack-plugin = { git = "https://github.com/prokopyl/clack.git", rev = "5deaa1b" } 8 | 9 | # The following are needed for the VST3 clap-wrapper support - 10 | # once that is added to clack, these dependency can go 11 | clack-common = { git = "https://github.com/prokopyl/clack.git", rev = "5deaa1b" } 12 | 13 | # This is the version used by the clack version we're using. 14 | # Must be compatible to compile correctly. 15 | clap-sys = "0.4.0" -------------------------------------------------------------------------------- /plugins/gain-example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gain-example" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [lib] 8 | # only a static library is built for this clap-wrapper based approach. 9 | # the dylib will be generated on the fly by the CMake script. 10 | crate-type = ["staticlib"] 11 | 12 | [dependencies] 13 | clack-plugin = { git = "https://github.com/prokopyl/clack.git", rev = "5deaa1b" } 14 | 15 | # add any additional extensions that you need 16 | # (params, state, gui, note-ports, ...) 17 | # by enabling the respective features on clack-extensions 18 | clack-extensions = { git = "https://github.com/prokopyl/clack.git", rev = "5deaa1b", features = ["audio-ports", "clack-plugin"] } 19 | 20 | # This will hopefully be included in clack soon! 21 | clap-wrapper-extensions = { path = "../../clap-wrapper-extensions" } -------------------------------------------------------------------------------- /xtask/cmake/clap_entry.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * This file re-exports the rust_clap_entry symbol from the Rust static library 3 | * as a standard clap_entry which clap-wrapper uses to build the base CLAP. 4 | */ 5 | 6 | #include "clap_entry.h" 7 | #include 8 | 9 | struct clap_version { 10 | uint32_t major; 11 | uint32_t minor; 12 | uint32_t revision; 13 | }; 14 | 15 | struct clap_plugin_entry { 16 | clap_version version; 17 | bool (*init)(const char *plugin_path); 18 | void (*deinit)(); 19 | const void *(*get_factory)(const char *factory_id); 20 | }; 21 | 22 | extern "C" { 23 | 24 | #ifdef __GNUC__ 25 | #pragma GCC diagnostic push 26 | #pragma GCC diagnostic ignored "-Wattributes" 27 | #endif 28 | 29 | // The Rust library's exported symbol... 30 | extern const clap_plugin_entry rust_clap_entry; 31 | // ... is re-exported under the expected CLAP entry name. 32 | CLAP_EXPORT extern const clap_plugin_entry clap_entry; 33 | const CLAP_EXPORT struct clap_plugin_entry clap_entry = rust_clap_entry; 34 | 35 | #ifdef __GNUC__ 36 | #pragma GCC diagnostic pop 37 | #endif 38 | } 39 | -------------------------------------------------------------------------------- /xtask/README.md: -------------------------------------------------------------------------------- 1 | # CLAP Plugin Build System (xtask) 2 | 3 | This module follows the [xtask pattern](https://github.com/matklad/cargo-xtask) to provide a custom build system for 4 | CLAP-first Rust plugins. 5 | It handles the integration between Rust code and the C++ clap-wrapper project to build plugins in multiple formats. 6 | 7 | ## How It Works 8 | 9 | 1. The Rust code is compiled into a static library 10 | 2. The `xtask` tool creates a CMake build environment with clap-wrapper and builds the final plugin formats (CLAP, VST3, 11 | AU). 12 | 13 | ## Command Reference 14 | 15 | The build system is invoked via the cargo alias defined in `.cargo/config.toml`: 16 | 17 | ```bash 18 | cargo xtask build [OPTIONS] 19 | ``` 20 | 21 | ### Options 22 | 23 | | Option | Description | 24 | |-----------------------|---------------------------------------------------------------------------------| 25 | | `--release` | Build using the release profile. Default is debug. | 26 | | `--bundle-id ` | Set bundle identifier (default: "org.free-audio.rust-gain-example") | 27 | | `--clean` | Clean build directories before building | 28 | | `--install` | Install plugins to system directories after building (not supported on Windows) | 29 | 30 | ### Examples 31 | 32 | ```bash 33 | # Basic build 34 | cargo xtask build gain-example 35 | 36 | # Release build with custom bundle ID 37 | cargo xtask build gain-example --release --bundle-id "com.mycompany.myplugin" 38 | 39 | cargo xtask build gain-example --clean --install 40 | ``` 41 | 42 | ## Adding New Plugins 43 | 44 | To add a new plugin: 45 | 46 | 1. Create a new crate in the `plugins/` directory 47 | 2. Ensure it has a `staticlib` crate type in `Cargo.toml` 48 | 3. Implement the necessary CLAP interfaces 49 | 4. Run `cargo xtask build your-new-plugin` 50 | -------------------------------------------------------------------------------- /plugins/gain-example/src/audio_thread.rs: -------------------------------------------------------------------------------- 1 | //! This module handles all CLAP callbacks that run on the audio thread. 2 | 3 | use crate::main_thread::GainPluginMainThread; 4 | use clack_plugin::prelude::*; 5 | 6 | pub struct GainPluginProcessor<'a> { 7 | #[allow(dead_code)] // unused in example 8 | host: HostAudioProcessorHandle<'a>, 9 | 10 | /// The constant factor to multiply incoming samples with. 11 | factor: f32, 12 | } 13 | 14 | impl<'a> PluginAudioProcessor<'a, (), GainPluginMainThread<'a>> for GainPluginProcessor<'a> { 15 | fn activate( 16 | host: HostAudioProcessorHandle<'a>, 17 | main_thread: &mut GainPluginMainThread<'a>, 18 | _shared: &'a (), 19 | _audio_config: PluginAudioConfiguration, 20 | ) -> Result { 21 | // in a real plugin, you might set up 22 | // communication lines with the main thread here. 23 | Ok(Self { 24 | host, 25 | factor: main_thread.factor, 26 | }) 27 | } 28 | 29 | fn deactivate(self, _main_thread: &mut GainPluginMainThread<'a>) { 30 | // here's where you tear down communications with the main thread. 31 | } 32 | 33 | /// This is where the DSP happens! 34 | /// This example plugin simply multiplies 35 | /// the amplitude of the incoming signal with a constant factor. 36 | fn process( 37 | &mut self, 38 | _process: Process, 39 | mut audio: Audio, 40 | _events: Events, 41 | ) -> Result { 42 | for mut port_pair in &mut audio { 43 | let Some(channel_pairs) = port_pair.channels()?.into_f32() else { 44 | continue; 45 | }; 46 | 47 | for pair in channel_pairs { 48 | if let ChannelPair::InputOutput(input, output) = pair { 49 | for i in 0..input.len() { 50 | output[i] = input[i] * self.factor; 51 | } 52 | } 53 | } 54 | } 55 | 56 | Ok(ProcessStatus::ContinueIfNotQuiet) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /plugins/gain-example/src/main_thread.rs: -------------------------------------------------------------------------------- 1 | //! This module handles all CLAP callbacks that run on the main thread. 2 | 3 | use clack_extensions::audio_ports::{AudioPortFlags, AudioPortInfo, AudioPortInfoWriter, AudioPortType, PluginAudioPortsImpl}; 4 | use clack_plugin::prelude::*; 5 | 6 | pub struct GainPluginMainThread<'a> { 7 | #[allow(dead_code)] // unused in example 8 | host: HostMainThreadHandle<'a>, 9 | 10 | /// The constant factor to multiply incoming samples with. 11 | pub factor: f32, 12 | } 13 | 14 | impl<'a> GainPluginMainThread<'a> { 15 | /// Creates an instance of the plugin's main thread. 16 | /// This plugin will multiply the incoming signal with gain_factor. 17 | pub fn create(host: HostMainThreadHandle<'a>, gain_factor: f32) -> Result { 18 | // this example main thread doesn't 19 | // do anything or hold any data 20 | Ok(Self { host, factor: gain_factor }) 21 | } 22 | } 23 | 24 | impl<'a> PluginMainThread<'a, ()> for GainPluginMainThread<'a> { 25 | fn on_main_thread(&mut self) { 26 | // in a real plugin, you might exchange information 27 | // with your GUI or audio thread in this callback. 28 | } 29 | } 30 | 31 | /// This example plugin has a single input and output audio port. 32 | /// additional ports, e.g. for sidechain inputs, would be configured here. 33 | impl<'a> PluginAudioPortsImpl for GainPluginMainThread<'a> { 34 | fn count(&mut self, is_input: bool) -> u32 { 35 | match is_input { 36 | true => { 1 } 37 | false => { 1 } 38 | } 39 | } 40 | 41 | fn get(&mut self, index: u32, is_input: bool, writer: &mut AudioPortInfoWriter) { 42 | if index != 0 { 43 | return; 44 | } 45 | 46 | // input and output ports are both stereo (2 channels) 47 | // and 32-bit only. 48 | writer.set(&AudioPortInfo { 49 | id: ClapId::new(if is_input { 0 } else { 1 }), 50 | name: b"Audio port", 51 | channel_count: 2, 52 | flags: AudioPortFlags::IS_MAIN, 53 | port_type: Some(AudioPortType::STEREO), 54 | in_place_pair: None, 55 | }); 56 | } 57 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust CLAP-First Plugin Example 2 | 3 | > Write a CLAP plugin in Rust, get self-contained AU and VST3 plugins for free! 4 | > Supports Windows, Linux, and macOS (universal binary x86_64 + arm64) 5 | 6 | This repository demonstrates a Rust-based approach to audio plugin development that starts with 7 | the [CLAP](https://cleveraudio.org/) plugin format and extends to VST3 and AU formats using 8 | the [clap-wrapper](https://github.com/free-audio/clap-wrapper/) project. 9 | The resulting plugins are **self-contained** thanks to a static linking approach. 10 | 11 | The [clack](https://github.com/prokopyl/clack) crate is used to provide safe Rust wrappers 12 | for the CLAP API without adding an opinionated plugin framework. 13 | 14 | Uniquely, this example demonstrates exporting **multiple plug-ins from a single binary**! 15 | To support AU, clap-wrapper's AUv2 plugin factory extension is used. 16 | 17 | Bindings for the clap-wrapper extensions are provided in [clap-wrapper-extensions](./clap-wrapper-extensions). 18 | I hope for them to be included in clack directly to reduce the required boilerplate for plugins. 19 | 20 | ## Example Gain Plugins 21 | 22 | This example exposes two variations of a simple gain plugin: 23 | 24 | - **Gain Halver**: Multiplies signal with 0.5 25 | - **Gain Doubler**: Multiplies signal with 2.0 26 | 27 | ## Requirements 28 | 29 | - Rust toolchain (2021 edition or later) 30 | - CMake (3.15 or later) 31 | - C++ compiler with C++17 support 32 | 33 | ## Building the Plugins 34 | 35 | The project uses the [xtask](https://github.com/matklad/cargo-xtask) pattern to simplify building: 36 | 37 | ```bash 38 | # Build debug version 39 | cargo xtask build gain-example 40 | 41 | # Build release version 42 | cargo xtask build gain-example --release 43 | 44 | # Build and install release version to user's plugin directories (macOS/Linux only) 45 | cargo xtask build gain-example --release --install 46 | ``` 47 | 48 | See the [xtask README](./xtask/README.md) for more detailed commands and options. 49 | 50 | ## How It Works 51 | 52 | 1. The Rust code is compiled into a static library that exports a non-standard `rust_clap_entry` symbol 53 | 2. A small C++ shim re-exports this symbol as the standard CLAP entry point 54 | 3. clap-wrapper builds self-contained plugins for CLAP, VST3, and AU formats 55 | 56 | ## Customizing 57 | 58 | To adapt this example for your own plugin: 59 | 60 | 1. Rename/duplicate the `gain-example` plugin directory 61 | 2. Modify the implementations in `audio_thread.rs` and `main_thread.rs` 62 | 3. Update the plugin descriptors in `lib.rs` 63 | 4. Update bundle IDs and other metadata in build commands 64 | 65 | ## Acknowledgements 66 | 67 | - [@Prokopyl](https://github.com/prokopyl) for providing Rust bindings for the clap-wrapper's extensions 68 | - [SG60/rust-clap-wrapper-thick](https://github.com/SG60/rust-clap-wrapper-thick) for their pioneering work 69 | 70 | ## License 71 | 72 | MIT OR Apache-2.0 73 | -------------------------------------------------------------------------------- /clap-wrapper-extensions/src/auv2.rs: -------------------------------------------------------------------------------- 1 | //! This module implements the AUv2 plugin info extension of the clap-wrapper project. 2 | //! Using these extensions, we can tell the wrapper how to advertise our CLAP plugins as AUv2. 3 | 4 | #![allow(non_camel_case_types)] 5 | 6 | use clack_plugin::factory::Factory; 7 | use std::ffi::{c_char, CStr}; 8 | use std::panic::{catch_unwind, AssertUnwindSafe}; 9 | 10 | const CLAP_PLUGIN_FACTORY_INFO_AUV2: &CStr = c"clap.plugin-factory-info-as-auv2.draft0"; 11 | 12 | #[repr(C)] 13 | #[derive(Debug, Copy, Clone)] 14 | struct clap_plugin_info_as_auv2 { 15 | au_type: [u8; 5], 16 | au_subt: [u8; 5], 17 | } 18 | 19 | #[repr(C)] 20 | #[derive(Debug, Copy, Clone)] 21 | struct clap_plugin_factory_as_auv2 { 22 | pub manufacturer_code: *const c_char, 23 | pub manufacturer_name: *const c_char, 24 | 25 | pub get_auv2_info: Option< 26 | unsafe extern "C" fn( 27 | factory: *mut clap_plugin_factory_as_auv2, 28 | index: u32, 29 | info: *mut clap_plugin_info_as_auv2, 30 | ) -> bool, 31 | >, 32 | } 33 | 34 | // SAFETY: everything here is read-only 35 | unsafe impl Send for clap_plugin_factory_as_auv2 {} 36 | // SAFETY: everything here is read-only 37 | unsafe impl Sync for clap_plugin_factory_as_auv2 {} 38 | 39 | #[derive(Debug, Copy, Clone)] 40 | pub struct PluginInfoAsAUv2 { 41 | inner: clap_plugin_info_as_auv2, 42 | } 43 | 44 | impl PluginInfoAsAUv2 { 45 | #[inline] 46 | pub fn new(au_type: &str, au_subt: &str) -> Self { 47 | assert_eq!(au_type.len(), 4, "au_type must be exactly 4 characters long"); 48 | assert_eq!(au_subt.len(), 4, "au_subt must be exactly 4 characters long"); 49 | 50 | let mut inner = clap_plugin_info_as_auv2 { 51 | au_type: [0; 5], 52 | au_subt: [0; 5], 53 | }; 54 | 55 | inner.au_type[..4].copy_from_slice(au_type.as_bytes()); 56 | inner.au_subt[..4].copy_from_slice(au_subt.as_bytes()); 57 | 58 | // Byte 4 is already zero due to array init: [0; 5] 59 | 60 | Self { inner } 61 | } 62 | } 63 | 64 | pub trait PluginFactoryAsAUv2 { 65 | fn get_auv2_info(&self, index: u32) -> Option; 66 | } 67 | 68 | #[repr(C)] 69 | pub struct PluginFactoryAsAUv2Wrapper { 70 | raw: clap_plugin_factory_as_auv2, 71 | factory: F, 72 | } 73 | 74 | // SAFETY: PluginFactoryWrapper is #[repr(C)] with clap_plugin_factory_as_auv2 as its first field, and matches 75 | // CLAP_PLUGIN_FACTORY_INFO_AUV2. 76 | unsafe impl Factory for PluginFactoryAsAUv2Wrapper { 77 | const IDENTIFIER: &'static CStr = CLAP_PLUGIN_FACTORY_INFO_AUV2; 78 | } 79 | 80 | impl PluginFactoryAsAUv2Wrapper { 81 | pub const fn new( 82 | manufacturer_code: &'static CStr, 83 | manufacturer_name: &'static CStr, 84 | factory: F, 85 | ) -> Self { 86 | Self { 87 | factory, 88 | raw: clap_plugin_factory_as_auv2 { 89 | get_auv2_info: Some(Self::get_auv2_info), 90 | manufacturer_code: manufacturer_code.as_ptr(), 91 | manufacturer_name: manufacturer_name.as_ptr(), 92 | }, 93 | } 94 | } 95 | 96 | #[allow(clippy::missing_safety_doc)] 97 | unsafe extern "C" fn get_auv2_info( 98 | factory: *mut clap_plugin_factory_as_auv2, 99 | index: u32, 100 | info: *mut clap_plugin_info_as_auv2, 101 | ) -> bool { 102 | let Some(factory) = (factory as *const Self).as_ref() else { 103 | return false; // HOST_MISBEHAVING 104 | }; 105 | 106 | let Ok(Some(info_data)) = 107 | catch_unwind(AssertUnwindSafe(|| factory.factory.get_auv2_info(index))) 108 | else { 109 | return false; // Either panicked or returned None. 110 | }; 111 | 112 | info.write(info_data.inner); 113 | 114 | true 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /xtask/cmake/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # ============================================================================== 2 | # Project Configuration 3 | # ============================================================================== 4 | cmake_minimum_required(VERSION 3.15) # TODO: can we lower this? 5 | 6 | # Set project name and initialize project 7 | set(PROJECT_NAME "ClapFirstRustPlugin" CACHE STRING "Say my name") 8 | project(${PROJECT_NAME}) 9 | 10 | # ============================================================================== 11 | # Platform and Compiler Settings 12 | # ============================================================================== 13 | set(CMAKE_CXX_STANDARD 17) 14 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 15 | set(CMAKE_POSITION_INDEPENDENT_CODE ON) 16 | 17 | # macOS Audio Unit SDK requirements 18 | if (APPLE) 19 | enable_language(OBJC) 20 | enable_language(OBJCXX) 21 | 22 | set(CMAKE_OSX_DEPLOYMENT_TARGET 15.4) # TODO: can we lower this? 23 | # Build universal binary (Intel + Apple Silicon) 24 | set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64") 25 | 26 | set(CMAKE_OBJC_VISIBILITY_PRESET hidden) 27 | set(CMAKE_OBJCXX_VISIBILITY_PRESET hidden) 28 | set(CMAKE_VISIBILITY_INLINES_HIDDEN ON) 29 | endif () 30 | 31 | # Windows specific settings 32 | if (WIN32) 33 | add_compile_definitions(_SILENCE_ALL_CXX17_DEPRECATION_WARNINGS) 34 | 35 | # Set MSVC runtime library to static linking 36 | if(MSVC) 37 | set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") 38 | add_compile_options(/MP) # Enable multi-processor compilation 39 | endif() 40 | 41 | # Add required Windows libraries 42 | set(WINDOWS_LIBS 43 | ws2_32 # For network functionality 44 | userenv # For user profile functions 45 | ntdll # For NT API functions 46 | ) 47 | endif() 48 | 49 | # Always build static libraries unless explicitly specified otherwise 50 | set(BUILD_SHARED_LIBS OFF CACHE BOOL "Never want shared if not specified") 51 | 52 | # ============================================================================== 53 | # 📦 Dependencies: CPM and CLAP Wrapper 54 | # ============================================================================== 55 | # Download CPM.cmake for dependency management 56 | file( 57 | DOWNLOAD 58 | https://github.com/cpm-cmake/CPM.cmake/releases/download/v0.40.2/CPM.cmake 59 | ${CMAKE_CURRENT_BINARY_DIR}/cmake/CPM.cmake 60 | EXPECTED_HASH SHA256=C8CDC32C03816538CE22781ED72964DC864B2A34A310D3B7104812A5CA2D835D 61 | ) 62 | include(${CMAKE_CURRENT_BINARY_DIR}/cmake/CPM.cmake) 63 | 64 | # Configure CLAP wrapper options 65 | set(BUILD_SHARED_LIBS OFF CACHE BOOL "Never want shared if not specified") 66 | set(CLAP_WRAPPER_DOWNLOAD_DEPENDENCIES ON) 67 | set(CLAP_WRAPPER_DONT_ADD_TARGETS ON CACHE BOOL "I'll targetize") 68 | 69 | # Add CLAP wrapper package 70 | CPMAddPackage( 71 | NAME clap-wrapper 72 | GITHUB_REPOSITORY "free-audio/clap-wrapper" 73 | GIT_TAG "main" 74 | ) 75 | 76 | # ============================================================================== 77 | # Static Library Configuration 78 | # ============================================================================== 79 | # Setup for pre-built static library with rust_clap_entry symbol 80 | # These variables can be overridden via command line or parent scope 81 | set(STATIC_LIB_FILE "" CACHE FILEPATH "Full path to the static library file") 82 | set(STATIC_LIB_INCLUDE_DIR "" CACHE PATH "Optional path to the include directory for the static library") 83 | set(BUNDLE_ID "org.free-audio.clap-plugin" CACHE STRING "Bundle identifier for the plugin") 84 | set(PLUGIN_OUTPUT_DIR "${CMAKE_BINARY_DIR}/plugins" CACHE PATH "Output directory to place the built plugins in") 85 | set(INSTALL_PLUGINS_AFTER_BUILD OFF CACHE BOOL "Whether to install resulting plugins") 86 | 87 | # Validate required variables 88 | if (NOT STATIC_LIB_FILE) 89 | message(FATAL_ERROR "STATIC_LIB_FILE must be specified") 90 | endif () 91 | 92 | if (NOT EXISTS "${STATIC_LIB_FILE}") 93 | message(FATAL_ERROR "Static library file does not exist: ${STATIC_LIB_FILE}") 94 | endif () 95 | 96 | # Create an imported target for the pre-built static library 97 | add_library(rust_static_lib STATIC IMPORTED) 98 | set_target_properties(rust_static_lib PROPERTIES 99 | IMPORTED_LOCATION "${STATIC_LIB_FILE}" 100 | ) 101 | 102 | # ============================================================================== 103 | # Plugin Target Configuration 104 | # ============================================================================== 105 | # Create a CLAP library target that re-exposes the Rust static library's CLAP entry 106 | add_library(clap_entry STATIC "clap_entry.cpp") 107 | target_link_libraries(clap_entry PRIVATE rust_static_lib) 108 | 109 | # Link required platform-specific libraries 110 | if (APPLE) 111 | target_link_libraries(clap_entry PUBLIC 112 | "-framework Foundation" 113 | "-framework CoreFoundation" 114 | "-framework AppKit" 115 | "-framework AudioToolbox" 116 | "-framework Quartz" 117 | "-framework CoreAudio" 118 | "-framework AudioUnit" 119 | "-framework CoreMIDI" 120 | ) 121 | elseif (WIN32) 122 | target_link_libraries(clap_entry PUBLIC ${WINDOWS_LIBS}) 123 | endif () 124 | 125 | # ============================================================================== 126 | # Plugin Build Configuration 127 | # ============================================================================== 128 | # Use clap-wrapper's make_clapfirst_plugin to build all plugin formats, 129 | # including the CLAP, from the provided clap_entry implementation 130 | 131 | make_clapfirst_plugins( 132 | TARGET_NAME ${PROJECT_NAME} 133 | IMPL_TARGET clap_entry 134 | 135 | OUTPUT_NAME "${PROJECT_NAME}" 136 | 137 | ENTRY_SOURCE "clap_entry.cpp" 138 | 139 | BUNDLE_IDENTIFER "${BUNDLE_ID}" 140 | BUNDLE_VERSION ${PROJECT_VERSION} 141 | 142 | COPY_AFTER_BUILD ${INSTALL_PLUGINS_AFTER_BUILD} 143 | 144 | PLUGIN_FORMATS CLAP VST3 AUV2 145 | 146 | ASSET_OUTPUT_DIRECTORY ${PLUGIN_OUTPUT_DIR} 147 | ) -------------------------------------------------------------------------------- /plugins/gain-example/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This module declares a plugin factory 2 | //! that is exposed behind the CLAP entry points. 3 | 4 | mod audio_thread; 5 | mod main_thread; 6 | 7 | use crate::audio_thread::GainPluginProcessor; 8 | use crate::main_thread::GainPluginMainThread; 9 | use clack_extensions::audio_ports::PluginAudioPorts; 10 | use clack_plugin::clack_entry; 11 | use clack_plugin::entry::prelude::*; 12 | use clack_plugin::plugin::features::AUDIO_EFFECT; 13 | use clack_plugin::prelude::*; 14 | use clap_wrapper_extensions::auv2::{ 15 | PluginFactoryAsAUv2, PluginFactoryAsAUv2Wrapper, PluginInfoAsAUv2, 16 | }; 17 | use clap_wrapper_extensions::vst3::{PluginFactoryAsVST3, PluginInfoAsVST3}; 18 | use std::ffi::CStr; 19 | 20 | pub struct GainPlugin; 21 | 22 | impl Plugin for GainPlugin { 23 | type AudioProcessor<'a> = GainPluginProcessor<'a>; 24 | type MainThread<'a> = GainPluginMainThread<'a>; 25 | 26 | /// We don't use any shared state in this example. 27 | /// 28 | /// Generally, it is preferred in Rust to communicate data between threads 29 | /// by passing messages through queues instead of sharing state. 30 | /// You can use the ringbuf crate or any other lock-free realtime-safe 31 | /// queue to achieve this in practice. 32 | type Shared<'a> = (); 33 | 34 | fn declare_extensions( 35 | builder: &mut PluginExtensions, 36 | _shared: Option<&Self::Shared<'_>>, 37 | ) { 38 | builder.register::(); 39 | } 40 | } 41 | 42 | /// Contains the CLAP, VST3 and AUv2 descriptors for a single plugin. 43 | struct PluginInfo( 44 | PluginDescriptor, 45 | PluginInfoAsVST3<'static>, 46 | PluginInfoAsAUv2, 47 | ); 48 | 49 | /// The factory exposes the plugins that can be instantiated from this binary. 50 | pub struct GainPluginFactory { 51 | info_halver: PluginInfo, 52 | info_doubler: PluginInfo, 53 | } 54 | 55 | const VST3_VENDOR: &CStr = c"free-audio"; 56 | const AU_MANUFACTURER_CODE: &CStr = c"Frau"; 57 | const AU_MANUFACTURER_NAME: &CStr = c"free-audio"; 58 | 59 | // 4-char IDs for the AU descriptors 60 | const AU_ID_HALVER: &str = "Ghlv"; 61 | const AU_ID_DOUBLER: &str = "Gdbl"; 62 | 63 | impl GainPluginFactory { 64 | fn new() -> Self { 65 | Self { 66 | info_halver: PluginInfo( 67 | PluginDescriptor::new("free-audio.clap.rust-gain-example.halver", "Gain Halver") 68 | .with_features([AUDIO_EFFECT]), 69 | PluginInfoAsVST3::new(Some(&VST3_VENDOR), None, None), 70 | PluginInfoAsAUv2::new("aufx", AU_ID_HALVER), 71 | ), 72 | info_doubler: PluginInfo( 73 | PluginDescriptor::new("free-audio.clap.rust-gain-example.doubler", "Gain Doubler") 74 | .with_features([AUDIO_EFFECT]), 75 | PluginInfoAsVST3::new(Some(&VST3_VENDOR), None, None), 76 | PluginInfoAsAUv2::new("aufx", AU_ID_DOUBLER), 77 | ), 78 | } 79 | } 80 | } 81 | 82 | /// Implements a plugin factory that exposes 2 plugins. 83 | /// For this gain example, one plugin halves the incoming audio, 84 | /// and the other doubles incoming audio. 85 | impl PluginFactory for GainPluginFactory { 86 | fn plugin_count(&self) -> u32 { 87 | 2 88 | } 89 | 90 | fn plugin_descriptor(&self, index: u32) -> Option<&PluginDescriptor> { 91 | match index { 92 | 0 => Some(&self.info_halver.0), 93 | 1 => Some(&self.info_doubler.0), 94 | _ => None, 95 | } 96 | } 97 | 98 | fn create_plugin<'b>( 99 | &'b self, 100 | host_info: HostInfo<'b>, 101 | plugin_id: &CStr, 102 | ) -> Option> { 103 | // the only way in which the two exposed plugins differ 104 | // is the gain factor that is passed to the main thread upon creation. 105 | 106 | if plugin_id == self.info_halver.0.id() { 107 | Some(PluginInstance::new::( 108 | host_info, 109 | &self.info_halver.0, 110 | |_host| Ok(()), 111 | |host, _| GainPluginMainThread::create(host, 0.5), 112 | )) 113 | } else if plugin_id == self.info_doubler.0.id() { 114 | Some(PluginInstance::new::( 115 | host_info, 116 | &self.info_doubler.0, 117 | |_host| Ok(()), 118 | |host, _| GainPluginMainThread::create(host, 2.0), 119 | )) 120 | } else { 121 | None 122 | } 123 | } 124 | } 125 | 126 | impl PluginFactoryAsVST3 for GainPluginFactory { 127 | fn get_vst3_info(&self, index: u32) -> Option<&PluginInfoAsVST3> { 128 | match index { 129 | 0 => Some(&self.info_halver.1), 130 | 1 => Some(&self.info_doubler.1), 131 | _ => None, 132 | } 133 | } 134 | } 135 | 136 | impl PluginFactoryAsAUv2 for GainPluginFactory { 137 | fn get_auv2_info(&self, index: u32) -> Option { 138 | match index { 139 | 0 => Some(self.info_halver.2), 140 | 1 => Some(self.info_doubler.2), 141 | _ => None, 142 | } 143 | } 144 | } 145 | 146 | /// Provides the CLAP entry points by deferring to our factory. 147 | pub struct GainPluginEntry { 148 | factory: PluginFactoryWrapper, 149 | factory_auv2: PluginFactoryAsAUv2Wrapper, 150 | } 151 | 152 | impl Entry for GainPluginEntry { 153 | fn new(_bundle_path: &CStr) -> Result { 154 | Ok(Self { 155 | factory: PluginFactoryWrapper::new(GainPluginFactory::new()), 156 | factory_auv2: PluginFactoryAsAUv2Wrapper::new( 157 | AU_MANUFACTURER_CODE, 158 | AU_MANUFACTURER_NAME, 159 | GainPluginFactory::new(), 160 | ), 161 | }) 162 | } 163 | 164 | fn declare_factories<'a>(&'a self, builder: &mut EntryFactories<'a>) { 165 | builder 166 | .register_factory(&self.factory) 167 | .register_factory(&self.factory_auv2); 168 | } 169 | } 170 | 171 | /// Expose the CLAP entry point, 172 | /// but notably under a non-standard symbol name, 173 | /// i.e. "rust_clap_entry" instead of "clap_entry"! 174 | /// 175 | /// When building the final plug-ins with clap-wrapper, 176 | /// the C++ rust_clap_entry.cpp file links against the static library built from this crate. 177 | /// and re-exports this entry under the expected "clap_entry" symbol name. 178 | #[allow(non_upper_case_globals, missing_docs)] 179 | #[allow(unsafe_code)] 180 | #[allow(warnings, unused)] 181 | #[unsafe(no_mangle)] 182 | pub static rust_clap_entry: EntryDescriptor = clack_entry!(GainPluginEntry); 183 | -------------------------------------------------------------------------------- /clap-wrapper-extensions/src/vst3.rs: -------------------------------------------------------------------------------- 1 | //! This module implements the VST3 plugin info extension of the clap-wrapper project. 2 | //! Using these extensions, we can tell the wrapper how to advertise our CLAP plugins as VST3. 3 | 4 | #![allow(non_camel_case_types)] 5 | 6 | use clack_common::extensions::{ 7 | Extension, ExtensionImplementation, PluginExtensionSide, RawExtension, 8 | RawExtensionImplementation, 9 | }; 10 | use clack_plugin::extensions::prelude::PluginWrapper; 11 | use clack_plugin::factory::Factory; 12 | use clack_plugin::prelude::Plugin; 13 | use clap_sys::plugin::clap_plugin; 14 | use core::ffi::{c_char, CStr}; 15 | use core::marker::PhantomData; 16 | use std::panic::{catch_unwind, AssertUnwindSafe}; 17 | // ===== Factory 18 | 19 | const CLAP_PLUGIN_FACTORY_INFO_VST3: &CStr = c"clap.plugin-factory-info-as-vst3/0"; 20 | 21 | #[repr(C)] 22 | #[derive(Debug, Copy, Clone)] 23 | struct clap_plugin_info_as_vst3 { 24 | pub vendor: *const c_char, 25 | pub component_id: *const [u8; 16], 26 | pub features: *const c_char, 27 | } 28 | 29 | #[repr(C)] 30 | #[derive(Debug, Copy, Clone)] 31 | struct clap_plugin_factory_as_vst3 { 32 | pub vendor: *const c_char, 33 | pub vendor_url: *const c_char, 34 | pub email_contact: *const c_char, 35 | 36 | pub get_vst3_info: Option< 37 | unsafe extern "C" fn( 38 | factory: *mut clap_plugin_factory_as_vst3, 39 | index: u32, 40 | ) -> *const clap_plugin_info_as_vst3, 41 | >, 42 | } 43 | 44 | // SAFETY: everything here is read-only 45 | unsafe impl Send for clap_plugin_factory_as_vst3 {} 46 | // SAFETY: everything here is read-only 47 | unsafe impl Sync for clap_plugin_factory_as_vst3 {} 48 | 49 | #[derive(Debug, Copy, Clone)] 50 | pub struct PluginInfoAsVST3<'a> { 51 | inner: clap_plugin_info_as_vst3, 52 | _lifetime: PhantomData<&'a CStr>, 53 | } 54 | 55 | // SAFETY: everything here is read-only 56 | unsafe impl Send for clap_plugin_info_as_vst3 {} 57 | // SAFETY: everything here is read-only 58 | unsafe impl Sync for clap_plugin_info_as_vst3 {} 59 | 60 | impl<'a> PluginInfoAsVST3<'a> { 61 | #[inline] 62 | pub const fn new( 63 | vendor: Option<&'a CStr>, 64 | component_id: Option<&'a [u8; 16]>, 65 | features: Option<&'a CStr>, 66 | ) -> Self { 67 | Self { 68 | _lifetime: PhantomData, 69 | inner: clap_plugin_info_as_vst3 { 70 | vendor: match vendor { 71 | Some(v) => v.as_ptr(), 72 | None => core::ptr::null(), 73 | }, 74 | features: match features { 75 | Some(v) => v.as_ptr(), 76 | None => core::ptr::null(), 77 | }, 78 | component_id: match component_id { 79 | Some(v) => v, 80 | None => core::ptr::null(), 81 | }, 82 | }, 83 | } 84 | } 85 | } 86 | 87 | pub trait PluginFactoryAsVST3 { 88 | fn get_vst3_info(&self, index: u32) -> Option<&PluginInfoAsVST3>; 89 | } 90 | 91 | #[repr(C)] 92 | pub struct PluginFactoryAsVST3Wrapper { 93 | raw: clap_plugin_factory_as_vst3, 94 | factory: F, 95 | } 96 | 97 | // SAFETY: PluginFactoryWrapper is #[repr(C)] with clap_plugin_factory_as_vst3 as its first field, and matches 98 | // CLAP_PLUGIN_FACTORY_INFO_VST3. 99 | unsafe impl Factory for PluginFactoryAsVST3Wrapper { 100 | const IDENTIFIER: &'static CStr = CLAP_PLUGIN_FACTORY_INFO_VST3; 101 | } 102 | 103 | impl PluginFactoryAsVST3Wrapper { 104 | #[inline] 105 | pub const fn new( 106 | vendor: Option<&'static CStr>, 107 | vendor_url: Option<&'static CStr>, 108 | email_contact: Option<&'static CStr>, 109 | factory: F, 110 | ) -> Self { 111 | Self { 112 | factory, 113 | raw: clap_plugin_factory_as_vst3 { 114 | vendor: match vendor { 115 | Some(v) => v.as_ptr(), 116 | None => core::ptr::null(), 117 | }, 118 | vendor_url: match vendor_url { 119 | Some(v) => v.as_ptr(), 120 | None => core::ptr::null(), 121 | }, 122 | email_contact: match email_contact { 123 | Some(v) => v.as_ptr(), 124 | None => core::ptr::null(), 125 | }, 126 | get_vst3_info: Some(Self::get_vst3_info), 127 | }, 128 | } 129 | } 130 | 131 | #[allow(clippy::missing_safety_doc)] 132 | unsafe extern "C" fn get_vst3_info( 133 | factory: *mut clap_plugin_factory_as_vst3, 134 | index: u32, 135 | ) -> *const clap_plugin_info_as_vst3 { 136 | let Some(factory) = (factory as *const Self).as_ref() else { 137 | return core::ptr::null(); // HOST_MISBEHAVING 138 | }; 139 | 140 | let Ok(Some(info)) = 141 | catch_unwind(AssertUnwindSafe(|| factory.factory.get_vst3_info(index))) 142 | else { 143 | return core::ptr::null(); // Either panicked or returned None. 144 | }; 145 | 146 | &info.inner 147 | } 148 | } 149 | 150 | // ===== Extension 151 | 152 | const CLAP_PLUGIN_AS_VST3: &CStr = c"clap.plugin-info-as-vst3/0"; 153 | #[repr(C)] 154 | #[derive(Debug, Copy, Clone)] 155 | struct clap_plugin_as_vst3 { 156 | pub get_num_midi_channels: 157 | Option u32>, 158 | pub supported_note_expressions: Option u32>, 159 | } 160 | 161 | #[derive(Copy, Clone)] 162 | #[allow(dead_code)] 163 | pub struct PluginAsVST3(RawExtension); 164 | 165 | // SAFETY: This type is repr(C) and ABI-compatible with the matching extension type. 166 | unsafe impl Extension for PluginAsVST3 { 167 | const IDENTIFIER: &'static CStr = CLAP_PLUGIN_AS_VST3; 168 | type ExtensionSide = PluginExtensionSide; 169 | 170 | #[inline] 171 | unsafe fn from_raw(raw: RawExtension) -> Self { 172 | Self(raw.cast()) 173 | } 174 | } 175 | 176 | pub trait PluginAsVST3Impl { 177 | fn num_midi_channels(&self, note_port: u32) -> u32; 178 | fn supported_note_expressions(&self) -> u32; 179 | } 180 | 181 | // SAFETY: The given struct is the CLAP extension struct for the matching side of this extension. 182 | unsafe impl ExtensionImplementation

for PluginAsVST3 183 | where 184 | for<'a> P::Shared<'a>: PluginAsVST3Impl, 185 | { 186 | const IMPLEMENTATION: RawExtensionImplementation = 187 | RawExtensionImplementation::new(&clap_plugin_as_vst3 { 188 | get_num_midi_channels: Some(get_num_midi_channels::

), 189 | supported_note_expressions: Some(supported_note_expressions::

), 190 | }); 191 | } 192 | 193 | #[allow(clippy::missing_safety_doc)] 194 | unsafe extern "C" fn get_num_midi_channels( 195 | plugin: *const clap_plugin, 196 | note_port: u32, 197 | ) -> u32 198 | where 199 | for<'a> P::Shared<'a>: PluginAsVST3Impl, 200 | { 201 | PluginWrapper::

::handle(plugin, |plugin| { 202 | Ok(plugin.shared().num_midi_channels(note_port)) 203 | }) 204 | .unwrap_or(0) 205 | } 206 | 207 | #[allow(clippy::missing_safety_doc)] 208 | unsafe extern "C" fn supported_note_expressions(plugin: *const clap_plugin) -> u32 209 | where 210 | for<'a> P::Shared<'a>: PluginAsVST3Impl, 211 | { 212 | PluginWrapper::

::handle(plugin, |plugin| { 213 | Ok(plugin.shared().supported_note_expressions()) 214 | }) 215 | .unwrap_or(0) 216 | } 217 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "anstream" 7 | version = "0.6.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 10 | dependencies = [ 11 | "anstyle", 12 | "anstyle-parse", 13 | "anstyle-query", 14 | "anstyle-wincon", 15 | "colorchoice", 16 | "is_terminal_polyfill", 17 | "utf8parse", 18 | ] 19 | 20 | [[package]] 21 | name = "anstyle" 22 | version = "1.0.10" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 25 | 26 | [[package]] 27 | name = "anstyle-parse" 28 | version = "0.2.6" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 31 | dependencies = [ 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle-query" 37 | version = "1.1.2" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 40 | dependencies = [ 41 | "windows-sys", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-wincon" 46 | version = "3.0.7" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 49 | dependencies = [ 50 | "anstyle", 51 | "once_cell", 52 | "windows-sys", 53 | ] 54 | 55 | [[package]] 56 | name = "bitflags" 57 | version = "2.9.0" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 60 | 61 | [[package]] 62 | name = "clack-common" 63 | version = "0.1.0" 64 | source = "git+https://github.com/prokopyl/clack.git?rev=5deaa1b#5deaa1be9b5af7078d75cbe54abefee12ed40f63" 65 | dependencies = [ 66 | "bitflags", 67 | "clap-sys", 68 | ] 69 | 70 | [[package]] 71 | name = "clack-extensions" 72 | version = "0.1.0" 73 | source = "git+https://github.com/prokopyl/clack.git?rev=5deaa1b#5deaa1be9b5af7078d75cbe54abefee12ed40f63" 74 | dependencies = [ 75 | "bitflags", 76 | "clack-common", 77 | "clack-plugin", 78 | "clap-sys", 79 | ] 80 | 81 | [[package]] 82 | name = "clack-plugin" 83 | version = "0.1.0" 84 | source = "git+https://github.com/prokopyl/clack.git?rev=5deaa1b#5deaa1be9b5af7078d75cbe54abefee12ed40f63" 85 | dependencies = [ 86 | "clack-common", 87 | "clap-sys", 88 | ] 89 | 90 | [[package]] 91 | name = "clap" 92 | version = "4.5.37" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" 95 | dependencies = [ 96 | "clap_builder", 97 | "clap_derive", 98 | ] 99 | 100 | [[package]] 101 | name = "clap-sys" 102 | version = "0.4.0" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "61b04354981cd059bf376007c9361ded38b15de87972e634670799685b042197" 105 | 106 | [[package]] 107 | name = "clap-wrapper-extensions" 108 | version = "0.1.0" 109 | dependencies = [ 110 | "clack-common", 111 | "clack-plugin", 112 | "clap-sys", 113 | ] 114 | 115 | [[package]] 116 | name = "clap_builder" 117 | version = "4.5.37" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" 120 | dependencies = [ 121 | "anstream", 122 | "anstyle", 123 | "clap_lex", 124 | "strsim", 125 | ] 126 | 127 | [[package]] 128 | name = "clap_derive" 129 | version = "4.5.32" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 132 | dependencies = [ 133 | "heck", 134 | "proc-macro2", 135 | "quote", 136 | "syn", 137 | ] 138 | 139 | [[package]] 140 | name = "clap_lex" 141 | version = "0.7.4" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 144 | 145 | [[package]] 146 | name = "colorchoice" 147 | version = "1.0.3" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 150 | 151 | [[package]] 152 | name = "gain-example" 153 | version = "0.1.0" 154 | dependencies = [ 155 | "clack-extensions", 156 | "clack-plugin", 157 | "clap-wrapper-extensions", 158 | ] 159 | 160 | [[package]] 161 | name = "heck" 162 | version = "0.5.0" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 165 | 166 | [[package]] 167 | name = "is_terminal_polyfill" 168 | version = "1.70.1" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 171 | 172 | [[package]] 173 | name = "once_cell" 174 | version = "1.21.3" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 177 | 178 | [[package]] 179 | name = "proc-macro2" 180 | version = "1.0.95" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 183 | dependencies = [ 184 | "unicode-ident", 185 | ] 186 | 187 | [[package]] 188 | name = "quote" 189 | version = "1.0.40" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 192 | dependencies = [ 193 | "proc-macro2", 194 | ] 195 | 196 | [[package]] 197 | name = "strsim" 198 | version = "0.11.1" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 201 | 202 | [[package]] 203 | name = "syn" 204 | version = "2.0.101" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 207 | dependencies = [ 208 | "proc-macro2", 209 | "quote", 210 | "unicode-ident", 211 | ] 212 | 213 | [[package]] 214 | name = "unicode-ident" 215 | version = "1.0.18" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 218 | 219 | [[package]] 220 | name = "utf8parse" 221 | version = "0.2.2" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 224 | 225 | [[package]] 226 | name = "windows-sys" 227 | version = "0.59.0" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 230 | dependencies = [ 231 | "windows-targets", 232 | ] 233 | 234 | [[package]] 235 | name = "windows-targets" 236 | version = "0.52.6" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 239 | dependencies = [ 240 | "windows_aarch64_gnullvm", 241 | "windows_aarch64_msvc", 242 | "windows_i686_gnu", 243 | "windows_i686_gnullvm", 244 | "windows_i686_msvc", 245 | "windows_x86_64_gnu", 246 | "windows_x86_64_gnullvm", 247 | "windows_x86_64_msvc", 248 | ] 249 | 250 | [[package]] 251 | name = "windows_aarch64_gnullvm" 252 | version = "0.52.6" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 255 | 256 | [[package]] 257 | name = "windows_aarch64_msvc" 258 | version = "0.52.6" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 261 | 262 | [[package]] 263 | name = "windows_i686_gnu" 264 | version = "0.52.6" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 267 | 268 | [[package]] 269 | name = "windows_i686_gnullvm" 270 | version = "0.52.6" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 273 | 274 | [[package]] 275 | name = "windows_i686_msvc" 276 | version = "0.52.6" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 279 | 280 | [[package]] 281 | name = "windows_x86_64_gnu" 282 | version = "0.52.6" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 285 | 286 | [[package]] 287 | name = "windows_x86_64_gnullvm" 288 | version = "0.52.6" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 291 | 292 | [[package]] 293 | name = "windows_x86_64_msvc" 294 | version = "0.52.6" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 297 | 298 | [[package]] 299 | name = "xtask" 300 | version = "0.1.0" 301 | dependencies = [ 302 | "clap", 303 | ] 304 | -------------------------------------------------------------------------------- /xtask/src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | use std::fs; 3 | use std::path::{Path, PathBuf}; 4 | use std::process::Command; 5 | 6 | #[derive(Debug, Parser)] 7 | #[command( 8 | name = "xtask", 9 | about = "Build CLAP-first audio plugins from a Rust crate" 10 | )] 11 | struct Cli { 12 | #[command(subcommand)] 13 | command: Commands, 14 | } 15 | 16 | #[derive(Debug, Subcommand)] 17 | enum Commands { 18 | /// Build a crate as a CLAP plugin 19 | Build { 20 | /// The crate to build as a static library 21 | crate_name: String, 22 | 23 | /// Release mode (default is debug) 24 | #[arg(long)] 25 | release: bool, 26 | 27 | /// Plugin bundle identifier 28 | #[arg(long, default_value = "org.free-audio.rust-gain-example")] 29 | bundle_id: String, 30 | 31 | /// Clean build directories first 32 | #[arg(long)] 33 | clean: bool, 34 | 35 | /// Install the resulting plugins to the local drive. 36 | /// Not supported on Windows. 37 | #[arg(long)] 38 | install: bool, 39 | }, 40 | } 41 | 42 | fn main() -> Result<(), Box> { 43 | let cli = Cli::parse(); 44 | 45 | match cli.command { 46 | Commands::Build { 47 | crate_name, 48 | release, 49 | bundle_id, 50 | clean, 51 | install, 52 | } => build_plugin(crate_name, release, bundle_id, clean, install)?, 53 | } 54 | 55 | Ok(()) 56 | } 57 | 58 | /// Build a plugin from a Rust crate 59 | fn build_plugin( 60 | crate_name: String, 61 | release: bool, 62 | bundle_id: String, 63 | clean: bool, 64 | install: bool, 65 | ) -> Result<(), Box> { 66 | // Get the project root directory 67 | let project_root = project_root(); 68 | 69 | // Clean if requested 70 | if clean { 71 | println!("Cleaning build directories..."); 72 | let _ = fs::remove_dir_all(project_root.join("target/cmake-build")); 73 | let _ = fs::remove_dir_all(project_root.join("target/cmake-assets")); 74 | let _ = fs::remove_dir_all(project_root.join("target/plugins")); 75 | } 76 | 77 | // Normalize crate name for file naming 78 | let normalized_crate_name = crate_name.replace('-', "_"); 79 | 80 | // Determine the output directory based on build profile 81 | let profile = if release { "release" } else { "debug" }; 82 | 83 | let static_lib_file = if cfg!(target_os = "macos") { 84 | // on macOS, build for both architectures 85 | // and create a universal binary using lipo 86 | build_universal_macos_binary(&project_root, &crate_name, &normalized_crate_name, release)? 87 | } else { 88 | // Regular build for the current architecture 89 | println!("Building static library for crate '{}'...", crate_name); 90 | 91 | let mut cargo_args = vec!["build"]; 92 | 93 | // Configure build profile 94 | if release { 95 | cargo_args.push("--release"); 96 | } 97 | 98 | // Add the crate to build 99 | cargo_args.push("-p"); 100 | cargo_args.push(&crate_name); 101 | 102 | let status = Command::new("cargo") 103 | .args(&cargo_args) 104 | .current_dir(&project_root) 105 | .status()?; 106 | 107 | if !status.success() { 108 | return Err("Failed to build static library".into()); 109 | } 110 | 111 | let target_dir = project_root.join("target").join(profile); 112 | 113 | // Determine the static library name based on the platform 114 | if cfg!(windows) { 115 | // On Windows, the static library is named: crate_name.lib 116 | target_dir.join(format!("{}.lib", normalized_crate_name)) 117 | } else { 118 | // On Unix-like systems (Linux, macOS), the static library is named: libcrate_name.a 119 | target_dir.join(format!("lib{}.a", normalized_crate_name)) 120 | } 121 | }; 122 | 123 | if !static_lib_file.exists() { 124 | return Err(format!( 125 | "Static library file not found: {}", 126 | static_lib_file.display() 127 | ) 128 | .into()); 129 | } 130 | 131 | println!("Found static library: {}", static_lib_file.display()); 132 | 133 | // Create the CMake build directory 134 | let cmake_build_dir = project_root.join("target/cmake-build"); 135 | fs::create_dir_all(&cmake_build_dir)?; 136 | 137 | // Path to the CMake script and source files 138 | let cmake_dir = project_root.join("xtask/cmake"); 139 | let build_cmake = cmake_dir.join("CMakeLists.txt"); 140 | let clap_entry_cpp = cmake_dir.join("clap_entry.cpp"); 141 | let clap_entry_h = cmake_dir.join("clap_entry.h"); 142 | 143 | // Check if the required files exist 144 | if !build_cmake.exists() || !clap_entry_cpp.exists() || !clap_entry_h.exists() { 145 | return Err("Required CMake files not found in xtask/cmake directory".into()); 146 | } 147 | 148 | 149 | // Create a temporary assets directory for CMake output 150 | let cmake_assets_dir = project_root.join("target/cmake-assets"); 151 | fs::create_dir_all(&cmake_assets_dir)?; 152 | 153 | // Final plugin output directory 154 | let plugin_output_dir = project_root.join("target").join(profile).join("plugins"); 155 | fs::create_dir_all(&plugin_output_dir)?; 156 | 157 | // Run CMake to configure the build 158 | println!("Configuring CMake build..."); 159 | 160 | let mut cmake_args = vec![ 161 | "-S".to_string(), 162 | cmake_dir.display().to_string(), 163 | "-B".to_string(), 164 | cmake_build_dir.display().to_string(), 165 | format!("-DPROJECT_NAME={}", crate_name), 166 | format!("-DSTATIC_LIB_FILE={}", static_lib_file.display()), 167 | format!("-DBUNDLE_ID={}", bundle_id), 168 | format!("-DPLUGIN_OUTPUT_DIR={}", cmake_assets_dir.display()), 169 | format!( 170 | "-DINSTALL_PLUGINS_AFTER_BUILD={}", 171 | if install { "ON" } else { "OFF" } 172 | ), 173 | ]; 174 | 175 | let status = Command::new("cmake") 176 | .args(&cmake_args) 177 | .status()?; 178 | 179 | if !status.success() { 180 | return Err("CMake configuration failed".into()); 181 | } 182 | 183 | // Build the plugins 184 | println!("Building plugins..."); 185 | let status = Command::new("cmake") 186 | .arg("--build") 187 | .arg(cmake_build_dir.to_str().unwrap()) 188 | .arg("--config") 189 | .arg(if release { "Release" } else { "Debug" }) 190 | .status()?; 191 | 192 | if !status.success() { 193 | return Err("Plugin build failed".into()); 194 | } 195 | 196 | // Copy the plugin files from the CMake output directory to the final plugin directory 197 | println!("Copying plugin files to final destination..."); 198 | copy_plugin_files(&cmake_assets_dir, &plugin_output_dir, &profile)?; 199 | 200 | println!("Build completed successfully!"); 201 | println!("Plugins are available in: {}", plugin_output_dir.display()); 202 | 203 | Ok(()) 204 | } 205 | 206 | /// Build a universal binary for macOS by building for both architectures and combining with lipo 207 | fn build_universal_macos_binary( 208 | project_root: &Path, 209 | crate_name: &str, 210 | normalized_crate_name: &str, 211 | release: bool, 212 | ) -> Result> { 213 | // Ensure both targets are available 214 | let status = Command::new("rustup") 215 | .args(&[ 216 | "target", 217 | "add", 218 | "x86_64-apple-darwin", 219 | "aarch64-apple-darwin", 220 | ]) 221 | .status()?; 222 | 223 | if !status.success() { 224 | return Err("Failed to add required targets".into()); 225 | } 226 | 227 | // Build profile 228 | let profile = if release { "release" } else { "debug" }; 229 | 230 | // Build for x86_64 (Intel) 231 | println!("Building for x86_64-apple-darwin..."); 232 | let mut cargo_args = vec!["build"]; 233 | 234 | if release { 235 | cargo_args.push("--release"); 236 | } 237 | 238 | cargo_args.extend(&["--target", "x86_64-apple-darwin", "-p", crate_name]); 239 | 240 | let status = Command::new("cargo") 241 | .args(&cargo_args) 242 | .current_dir(project_root) 243 | .status()?; 244 | 245 | if !status.success() { 246 | return Err("Failed to build for x86_64-apple-darwin".into()); 247 | } 248 | 249 | // Build for arm64 (Apple Silicon) 250 | println!("Building for aarch64-apple-darwin..."); 251 | let mut cargo_args = vec!["build"]; 252 | 253 | if release { 254 | cargo_args.push("--release"); 255 | } 256 | 257 | cargo_args.extend(&["--target", "aarch64-apple-darwin", "-p", crate_name]); 258 | 259 | let status = Command::new("cargo") 260 | .args(&cargo_args) 261 | .current_dir(project_root) 262 | .status()?; 263 | 264 | if !status.success() { 265 | return Err("Failed to build for aarch64-apple-darwin".into()); 266 | } 267 | 268 | // Path to the x86_64 and arm64 libraries 269 | let x86_64_lib = project_root 270 | .join("target") 271 | .join("x86_64-apple-darwin") 272 | .join(profile) 273 | .join(format!("lib{}.a", normalized_crate_name)); 274 | 275 | let arm64_lib = project_root 276 | .join("target") 277 | .join("aarch64-apple-darwin") 278 | .join(profile) 279 | .join(format!("lib{}.a", normalized_crate_name)); 280 | 281 | // Create output directory for universal binary 282 | let universal_dir = project_root.join("target").join("universal"); 283 | fs::create_dir_all(&universal_dir)?; 284 | 285 | // Path for the universal library 286 | let universal_lib = universal_dir.join(format!("lib{}.a", normalized_crate_name)); 287 | 288 | // Use lipo to create universal binary 289 | println!( 290 | "Creating universal binary with lipo: {}", 291 | universal_lib.display() 292 | ); 293 | let status = Command::new("lipo") 294 | .args(&[ 295 | "-create", 296 | &x86_64_lib.to_string_lossy(), 297 | &arm64_lib.to_string_lossy(), 298 | "-output", 299 | &universal_lib.to_string_lossy(), 300 | ]) 301 | .status()?; 302 | 303 | if !status.success() { 304 | return Err("Failed to create universal binary with lipo".into()); 305 | } 306 | 307 | // Verify the universal binary 308 | let output = Command::new("lipo") 309 | .args(&["-info", &universal_lib.to_string_lossy()]) 310 | .output()?; 311 | 312 | if output.status.success() { 313 | let info = String::from_utf8_lossy(&output.stdout); 314 | println!("Universal binary info: {}", info.trim()); 315 | } 316 | 317 | Ok(universal_lib) 318 | } 319 | 320 | /// Copy plugin files from CMake output to final destination 321 | fn copy_plugin_files( 322 | source_dir: &Path, 323 | dest_dir: &Path, 324 | profile: &str, 325 | ) -> Result<(), Box> { 326 | // Create destination directory if it doesn't exist 327 | fs::create_dir_all(dest_dir)?; 328 | 329 | // Handle platform-specific differences 330 | if cfg!(target_os = "windows") { 331 | // On Windows, we need to handle the nested file structure 332 | for format in ["VST3", "CLAP"] { 333 | let format_source_dir = source_dir.join(format).join(profile); 334 | if format_source_dir.exists() { 335 | for entry in fs::read_dir(&format_source_dir)? { 336 | let entry = entry?; 337 | let source_path = entry.path(); 338 | if source_path.is_file() { 339 | let dest_path = dest_dir.join(source_path.file_name().unwrap()); 340 | fs::copy(&source_path, &dest_path)?; 341 | } else if source_path.is_dir() { 342 | let dest_subdir = dest_dir.join(source_path.file_name().unwrap()); 343 | copy_dir_recursive(&source_path, &dest_subdir)?; 344 | } 345 | } 346 | } 347 | } 348 | } else { 349 | // On macOS, files are output directly in the asset output directory. 350 | // it's a sensible default for Linux as well 351 | copy_dir_recursive(source_dir, dest_dir)?; 352 | } 353 | 354 | Ok(()) 355 | } 356 | 357 | /// Copy all files and directories recursively 358 | fn copy_dir_recursive(source: &Path, dest: &Path) -> Result<(), Box> { 359 | if !dest.exists() { 360 | fs::create_dir_all(dest)?; 361 | } 362 | 363 | for entry in fs::read_dir(source)? { 364 | let entry = entry?; 365 | let path = entry.path(); 366 | let dest_path = dest.join(path.file_name().unwrap()); 367 | 368 | if path.is_dir() { 369 | copy_dir_recursive(&path, &dest_path)?; 370 | } else { 371 | fs::copy(path, dest_path)?; 372 | } 373 | } 374 | 375 | Ok(()) 376 | } 377 | 378 | /// Get the project root directory 379 | fn project_root() -> PathBuf { 380 | Path::new(&env!("CARGO_MANIFEST_DIR")) 381 | .ancestors() 382 | .nth(1) 383 | .unwrap() 384 | .to_path_buf() 385 | } 386 | --------------------------------------------------------------------------------