diff --git a/.gitmodules b/.gitmodules index c33efaad2f..755cb6c0dc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -25,3 +25,6 @@ [submodule "external/clap-sdk"] path = external/clap-sdk url = https://github.com/free-audio/clap.git +[submodule "external/efsw"] + path = external/efsw + url = https://github.com/SpartanJ/efsw diff --git a/architecture/clap/Makefile.simple b/architecture/clap/Makefile.simple index b0885bf837..e826edc84a 100644 --- a/architecture/clap/Makefile.simple +++ b/architecture/clap/Makefile.simple @@ -13,75 +13,95 @@ # - CLAP SDK and helpers in ../../external/ # - C++17 compatible compiler -CXX = g++ -CXXFLAGS = -std=c++17 -fPIC -O2 -Wall -Wextra -INCLUDES = -I../../architecture \ - -I../../external/clap-sdk/include \ - -I../../external/clap-helpers/include \ - -I/usr/local/include \ - -I/opt/homebrew/include \ - -Iefsw/include \ - -Iefsw/src - -LIBS = -L/usr/local/lib -L/opt/homebrew/lib -lfaust -ldl -framework CoreServices -framework CoreFoundation - -TARGET = FaustDynamic.clap -SOURCE = simple-faust.cpp -HELPER_OBJS = clap-helpers-impl.o interpreter-clap.o -MAIN_OBJ = faust-dynamic.o -EFSW_LIB = efsw/build/libefsw-static.a +CXX := /usr/bin/clang++ +CXXFLAGS := -std=c++17 -fPIC -O2 -Wall -Wextra -fvisibility=default + +# Detect whether we're inside an installed faust2clap tree or building in-repo +SCRIPT_DIR := $(realpath $(dir $(lastword $(MAKEFILE_LIST)))) +TOP_DIR := $(abspath $(SCRIPT_DIR)/../..) + +# Allow override via env, useful for installed tree +FAUST2CLAP_ROOT ?= $(TOP_DIR) + +# External dependencies +CLAP_SDK_INC := $(FAUST2CLAP_ROOT)/external/clap-sdk/include +CLAP_HELPERS_INC := $(FAUST2CLAP_ROOT)/external/clap-helpers/include +EFSW_INC := $(FAUST2CLAP_ROOT)/external/efsw/include +EFSW_SRC := $(FAUST2CLAP_ROOT)/external/efsw/src +EFSW_LIB := $(FAUST2CLAP_ROOT)/external/efsw/build/libefsw-static.a + +INCLUDES = \ + -I$(FAUST2CLAP_ROOT)/architecture \ + -I$(FAUST2CLAP_ROOT)/external/clap-sdk/include \ + -I$(FAUST2CLAP_ROOT)/external/clap-helpers/include \ + -I$(FAUST2CLAP_ROOT)/external/efsw/include \ + -I$(FAUST2CLAP_ROOT)/external/efsw/src \ + -I/usr/local/include \ + -I/opt/homebrew/include + +LIBS := -L$(FAUST2CLAP_ROOT)/external/efsw/build \ + -lfaust -ldl \ + -framework CoreServices -framework CoreFoundation + +# Output target +TARGET := FaustDynamic.clap + +# Source files +SOURCE := simple-faust.cpp +MAIN_OBJ := faust-dynamic.o +HELPER_OBJS := clap-helpers-impl.o interpreter-clap.o all: $(TARGET) $(TARGET): $(MAIN_OBJ) $(HELPER_OBJS) $(EFSW_LIB) @echo "πŸ”— Linking $(TARGET)..." - $(CXX) -shared $(MAIN_OBJ) $(HELPER_OBJS) $(EFSW_LIB) $(LIBS) -o $@ + $(CXX) -bundle -undefined dynamic_lookup \ + $(MAIN_OBJ) $(HELPER_OBJS) $(EFSW_LIB) $(LIBS) \ + -Wl,-headerpad_max_install_names \ + -Wl,-exported_symbol,_clap_entry \ + -Wl,-rpath,/usr/local/lib \ + -o $@ @echo "βœ… Built $(TARGET) successfully" $(MAIN_OBJ): $(SOURCE) @echo "πŸ”¨ Compiling main plugin source..." - $(CXX) $(CXXFLAGS) $(INCLUDES) -c $(SOURCE) -o $@ + $(CXX) $(CXXFLAGS) $(INCLUDES) -c $< -o $@ clap-helpers-impl.o: clap-helpers-impl.cpp @echo "πŸ”¨ Compiling CLAP helpers..." - $(CXX) $(CXXFLAGS) $(INCLUDES) -c clap-helpers-impl.cpp -o $@ + $(CXX) $(CXXFLAGS) $(INCLUDES) -c $< -o $@ interpreter-clap.o: interpreter-clap.cpp interpreter-clap.h @echo "πŸ”¨ Compiling Faust interpreter interface..." $(CXX) $(CXXFLAGS) $(INCLUDES) -c interpreter-clap.cpp -o $@ + +# Build efsw automatically if missing +ifeq ("$(wildcard $(EFSW_LIB))","") +$(info ⚠️ efsw not built yet – will build it now) +NEED_EFSW := 1 +endif + + $(EFSW_LIB): - @echo "πŸ”¨ Building efsw library..." - @cd efsw && mkdir -p build && cd build && cmake .. && make efsw-static + @echo "πŸ”¨ Building efsw..." + cd $(FAUST2CLAP_ROOT)/external/efsw && \ + mkdir -p build && \ + cd build && \ + cmake .. && \ + make efsw-static + +install: $(TARGET) + @echo "πŸ“¦ Installing $(TARGET)..." + mkdir -p $(HOME)/Library/Audio/Plug-Ins/CLAP/ + mkdir -p $(HOME)/.clap/plugins/ + cp -f $(TARGET) $(HOME)/Library/Audio/Plug-Ins/CLAP/ + cp -f $(TARGET) $(HOME)/.clap/plugins/ + @echo "βœ… Plugin installed to user plugin directories" clean: @echo "🧹 Cleaning build artifacts..." - rm -f $(MAIN_OBJ) $(HELPER_OBJS) $(TARGET) + rm -f *.o $(TARGET) @echo "βœ… Clean complete" -install: $(TARGET) - @echo "πŸ“¦ Installing $(TARGET)..." - @mkdir -p $(HOME)/Library/Audio/Plug-Ins/CLAP/ - @mkdir -p $(HOME)/.clap/plugins/ - cp $(TARGET) $(HOME)/Library/Audio/Plug-Ins/CLAP/ - cp $(TARGET) $(HOME)/.clap/plugins/ - @echo "βœ… Installed $(TARGET) to system plugin directories" - -# Development targets -test: $(TARGET) - @echo "πŸ§ͺ Testing plugin load..." - @if [ -n "$(CLAP_VALIDATOR)" ]; then \ - $(CLAP_VALIDATOR) $(TARGET); \ - else \ - echo "⚠️ CLAP_VALIDATOR not set, skipping validation"; \ - fi - -# Show build info -info: - @echo "πŸ› οΈ Faust Dynamic CLAP Plugin Build Configuration:" - @echo " Compiler: $(CXX)" - @echo " Flags: $(CXXFLAGS)" - @echo " Target: $(TARGET)" - @echo " Dependencies: libfaust (with interpreter), CLAP SDK" - -.PHONY: all clean install test info \ No newline at end of file +.PHONY: all install clean \ No newline at end of file diff --git a/tools/faust2clap/lib/faust_gui_glue.cpp b/architecture/clap/faust_gui_glue.cpp similarity index 100% rename from tools/faust2clap/lib/faust_gui_glue.cpp rename to architecture/clap/faust_gui_glue.cpp diff --git a/architecture/clap/gui_stuff/grame.icns b/architecture/clap/gui_stuff/grame.icns new file mode 100644 index 0000000000..b92d801f83 Binary files /dev/null and b/architecture/clap/gui_stuff/grame.icns differ diff --git a/tools/faust2clap/lib/plugin.cc b/architecture/clap/plugin.cc similarity index 100% rename from tools/faust2clap/lib/plugin.cc rename to architecture/clap/plugin.cc diff --git a/build/CMakeLists.txt b/build/CMakeLists.txt index 6e133898d0..6b27a5f520 100644 --- a/build/CMakeLists.txt +++ b/build/CMakeLists.txt @@ -416,7 +416,7 @@ install ( #################################### # install faust2xxx tools -file (GLOB FAUST2XXX ${ROOT}/tools/faust2appls/faust2* ${ROOT}/tools/faust2appls/faustremote ${ROOT}/tools/faust2appls/encoderunitypackage ${ROOT}/tools/faust2appls/usage.sh ${ROOT}/tools/faust2appls/filename2ident ${ROOT}/tools/faust2sc-1.0.0/faust2sc ${ROOT}/tools/faust2clap/faust2clap.py ) +file (GLOB FAUST2XXX ${ROOT}/tools/faust2appls/faust2* ${ROOT}/tools/faust2appls/faustremote ${ROOT}/tools/faust2appls/encoderunitypackage ${ROOT}/tools/faust2appls/usage.sh ${ROOT}/tools/faust2appls/filename2ident ${ROOT}/tools/faust2sc-1.0.0/faust2sc ${ROOT}/tools/faust2clap/faust2clap.py ${ROOT}/tools/faust2clap/faust2clap.sh ${ROOT}/tools/faust2clap/install-faust2clap.sh) install ( FILES ${FAUST2XXX} DESTINATION ${CMAKE_INSTALL_PREFIX}/bin PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_EXECUTE WORLD_READ diff --git a/external/efsw b/external/efsw new file mode 160000 index 0000000000..f94a6616ab --- /dev/null +++ b/external/efsw @@ -0,0 +1 @@ +Subproject commit f94a6616aba85fc9375fdff7ee69609d223a0672 diff --git a/tools/faust2clap/CMakeLists.txt b/tools/faust2clap/CMakeLists.txt index 8b21b0e10d..9d873f7607 100644 --- a/tools/faust2clap/CMakeLists.txt +++ b/tools/faust2clap/CMakeLists.txt @@ -1,88 +1,94 @@ -#min required version of cmake for compatibility cmake_minimum_required(VERSION 3.15) -#declaration of project and languages in use project(faust2clap LANGUAGES C CXX) -#specify c++17 and macos deployment target set(CMAKE_CXX_STANDARD 17) set(CMAKE_OSX_DEPLOYMENT_TARGET "10.11" CACHE STRING "macOS target") set(CMAKE_POSITION_INDEPENDENT_CODE ON) -#avoid installing clap sdk during build set(CLAP_NO_INSTALL ON CACHE BOOL "" FORCE) - -#enable post-build copying of plugin to standard location set(COPY_AFTER_BUILD ON CACHE BOOL "Copy the CLAP plugin to system folder after build") -#include required external libraries -add_subdirectory(${CMAKE_SOURCE_DIR}/../../external/clap-sdk clap-sdk) -add_subdirectory(${CMAKE_SOURCE_DIR}/../../external/clap-helpers clap-helpers) +# Define root for local install +set(FAUST2CLAP_ROOT ${CMAKE_CURRENT_SOURCE_DIR}) -#build a static lib to hold faust GUI glue (global symbols) -add_library(faust_gui_glue STATIC lib/faust_gui_glue.cpp) -#make sure it sees FAUST GUI headers (like faust/gui/GUI.h) -target_include_directories(faust_gui_glue PRIVATE - ${CMAKE_SOURCE_DIR}/../../architecture - ${CMAKE_SOURCE_DIR}/architecture/faust/gui -) +# -------------------------------------------------- +# Locate Faust headers for global builds +# -------------------------------------------------- +execute_process(COMMAND faust --includedir + OUTPUT_VARIABLE FAUST_INCLUDE_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE) +message(STATUS "Faust include dir: ${FAUST_INCLUDE_DIR}") + -#hardcode RtMidi paths for Homebrew on Apple Silicon -set(RTMIDI_LIBRARY /opt/homebrew/Cellar/rtmidi/6.0.0/lib/librtmidi.dylib CACHE FILEPATH "Hardcoded RtMidi lib path") -set(RTMIDI_INCLUDE_DIR /opt/homebrew/Cellar/rtmidi/6.0.0/include CACHE PATH "Hardcoded RtMidi include path") -message(STATUS "Final RtMidi include: ${RTMIDI_INCLUDE_DIR}") +# ------------------------------- +# External Dependencies +# ------------------------------- +# Clap SDK +add_subdirectory(${FAUST2CLAP_ROOT}/external/clap-sdk clap-sdk) +add_subdirectory(${FAUST2CLAP_ROOT}/external/clap-helpers clap-helpers) + +# GUI glue lib +add_library(faust_gui_glue STATIC ${FAUST2CLAP_ROOT}/architecture/clap/faust_gui_glue.cpp) + +target_include_directories(faust_gui_glue PRIVATE + ${FAUST2CLAP_ROOT}/architecture + ${FAUST2CLAP_ROOT}/architecture/faust/gui + ${FAUST_INCLUDE_DIR} +) +# RtMidi (hardcoded Homebrew path) +set(RTMIDI_LIBRARY /opt/homebrew/Cellar/rtmidi/6.0.0/lib/librtmidi.dylib CACHE FILEPATH "RtMidi lib") +set(RTMIDI_INCLUDE_DIR /opt/homebrew/Cellar/rtmidi/6.0.0/include CACHE PATH "RtMidi include") message(STATUS "Using hardcoded RtMidi:") message(STATUS " Library: ${RTMIDI_LIBRARY}") message(STATUS " Include: ${RTMIDI_INCLUDE_DIR}") -#search for all *_clap.cpp sources generated by Faust -file(GLOB_RECURSE FAUST_SOURCES "${CMAKE_SOURCE_DIR}/../../build/*_clap.cpp") -#loop through each generated file and add a CLAP plugin target + + +# ------------------------------- +# Discover and Build Plugins +# ------------------------------- + +# This assumes that faust2clap.py puts sources in: /build// +file(GLOB_RECURSE FAUST_SOURCES "${CMAKE_BINARY_DIR}/*_clap.cpp") + foreach(FAUST_CPP ${FAUST_SOURCES}) - get_filename_component(FILENAME ${FAUST_CPP} NAME_WE) # e.g. freeverb_clap - string(REPLACE "_clap" "" PLUGIN_NAME ${FILENAME}) # strip _clap + get_filename_component(FILENAME ${FAUST_CPP} NAME_WE) + string(REPLACE "_clap" "" PLUGIN_NAME ${FILENAME}) - #declare the plugin module add_library(${PLUGIN_NAME} MODULE ${FAUST_CPP}) - #add plugin.cc to get full implementation of clap::helpers::Plugin<> - target_sources(${PLUGIN_NAME} - PRIVATE - ${CMAKE_SOURCE_DIR}/lib/plugin.cc - ) + target_sources(${PLUGIN_NAME} PRIVATE ${FAUST2CLAP_ROOT}/architecture/clap/plugin.cc) - #include necessary directories for compilation target_include_directories(${PLUGIN_NAME} PRIVATE - ${CMAKE_SOURCE_DIR}/../../build - ${CMAKE_SOURCE_DIR}/../../architecture - ${CMAKE_SOURCE_DIR}/architecture/faust/gui - ${CMAKE_SOURCE_DIR}/architecture/faust/midi - ${CMAKE_SOURCE_DIR}/../../architecture/faust/dsp - ${CMAKE_SOURCE_DIR}/../../external/clap-helpers/include - ${CMAKE_SOURCE_DIR}/../../external/clap-sdk/include - ${RTMIDI_INCLUDE_DIR} # RtMidi include path - ${CMAKE_SOURCE_DIR}/../../build/${PLUGIN_NAME} + ${CMAKE_BINARY_DIR} + ${FAUST2CLAP_ROOT}/architecture + ${FAUST2CLAP_ROOT}/architecture/faust/gui + ${FAUST_INCLUDE_DIR} + ${FAUST2CLAP_ROOT}/architecture/faust/midi + ${FAUST2CLAP_ROOT}/architecture/faust/dsp + ${FAUST2CLAP_ROOT}/external/clap-helpers/include + ${FAUST2CLAP_ROOT}/external/clap-sdk/include + ${RTMIDI_INCLUDE_DIR} + ${CMAKE_BINARY_DIR}/${PLUGIN_NAME} ) - #link against the clap core library and clap helpers and GUI glue target_link_libraries(${PLUGIN_NAME} PRIVATE clap clap-helpers faust_gui_glue - ${RTMIDI_LIBRARY} # RtMidi dynamic library + ${RTMIDI_LIBRARY} ) - #use one shared plist template for all configure_file( - ${CMAKE_SOURCE_DIR}/cmake/generic.plist.in + ${FAUST2CLAP_ROOT}/cmake/generic.plist.in ${CMAKE_CURRENT_BINARY_DIR}/${PLUGIN_NAME}.plist @ONLY ) - #set bundle properties for macOS set_target_properties(${PLUGIN_NAME} PROPERTIES OUTPUT_NAME ${PLUGIN_NAME} BUNDLE TRUE @@ -94,7 +100,6 @@ foreach(FAUST_CPP ${FAUST_SOURCES}) MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_BINARY_DIR}/${PLUGIN_NAME}.plist ) - #copy plugin to user's system folder after build if (APPLE AND COPY_AFTER_BUILD) message(STATUS "Will copy plugin '${PLUGIN_NAME}' after build") set(products_folder ${CMAKE_BINARY_DIR}) @@ -108,3 +113,8 @@ foreach(FAUST_CPP ${FAUST_SOURCES}) ) endif() endforeach() + +# Optional: install the faust2clap CLI +install(PROGRAMS ${CMAKE_CURRENT_SOURCE_DIR}/faust2clap.sh + DESTINATION bin + RENAME faust2clap) \ No newline at end of file diff --git a/tools/faust2clap/README.md b/tools/faust2clap/README.md index 77b3f85c7f..34ed112096 100644 --- a/tools/faust2clap/README.md +++ b/tools/faust2clap/README.md @@ -12,6 +12,7 @@ In addition to this static mode, a dynamic implementation has been added. In thi ## Features - βœ… Generate CLAP plugins directly from Faust (.dsp file) code +- βœ… Modern CLI tool with global install support - βœ… Hot-reloading of DSP: update `.dsp` files without restarting the host or closing the plugin - βœ… Parameter discovery and synchronisation with host - βœ… MIDI and polyphonic support via Faust’s `mydsp_poly` @@ -22,51 +23,55 @@ In addition to this static mode, a dynamic implementation has been added. In thi ## πŸ’» Usage -### πŸ› οΈ Static build -Generate and build a static CLAP plugin from your DSP file -```python -python tools/faust2clap/faust2clap.py your_file.dsp +### 🌍 Global Installation + +Install `faust2clap` globally (CLI + dependencies): + +```bash +cd tools/faust2clap +chmod +x install-faust2clap.sh +sudo ./install-faust2clap.sh ``` -```shell -optional flags: - -mono generate monophonic plugin - -poly generate polyphonic plugin (default) - -nvoices N set number of polyphonic voices (default: 16) + +### πŸ› οΈ Static Plugin Generation +Generate and build a static CLAP plugin from your DSP file's directory + +```bash +faust2clap your_file.dsp ``` -The plugin will be automatically built and installed to: ```shell -~/Library/Audio/Plug-Ins/CLAP/your_file.clap +optional arguments: + --mono generate monophonic plugin + --poly generate polyphonic plugin (default) + -nvoices N number of polyphonic voices (default: 16) + --dry-run simulate build without compiling ``` -### Dynamic (Interpreter) mode -Build the dynamic hot-reload plugin ```bash -cd architecture/clap -make -f Makefile.simple +faust2clap myplugin.dsp +faust2clap --dynamic --install ``` -Install to system plugin directories +### Dynamic Plugin (Hot Reload) +Build the dynamic hot-reload plugin ```bash -make -f Makefile.simple install +faust2clap --dynamic --install ``` + +Install to system plugin directories + The dynamic plugin (FaustDynamic.clap) will be installed to: ```bash ~/Library/Audio/Plug-Ins/CLAP/ ~/.clap/plugins/ ``` -Clean build artifacts -```bash -make -f Makefile.simple clean -``` - Once installed, use the GUI to load DSP files for hot-reloading: Run the hot-reload GUI controller ```bash -cd architecture/clap -python faust-hot-reload.py +faust2clap --gui ``` The GUI allows you to: - Browse and load .dsp files diff --git a/tools/faust2clap/faust2clap.py b/tools/faust2clap/faust2clap.py index 7fa43aa65f..05f8145826 100755 --- a/tools/faust2clap/faust2clap.py +++ b/tools/faust2clap/faust2clap.py @@ -1,89 +1,258 @@ #!/usr/bin/env python3 + +##################################################################### +# # +# faust2clap # +# (c) Grame & Facundo Franchino, 2025 # +# # +##################################################################### + import sys #cli arguments import os #file path manipulation import subprocess #run shell commands import json import re -import argparse +import argparse +import time +from pathlib import Path +import shutil + +def generate_info_plist(app_name, identifier, version, executable_name): + return f""" + + + + CFBundlePackageType + BNDL + CFBundleExecutable + {executable_name} + CFBundleIdentifier + {identifier} + CFBundleName + {app_name} + CFBundleVersion + {version} + + +""" + +def get_dynamic_plugin_metadata(): + return { + "app_name": "FaustDynamic", + "identifier": "org.faust.dynamic", + "version": "1.0.0", + "executable_name": "FaustDynamic" + } + +def symlink_external(name, install_root): + """Symlink an external/ dependency (clap-helpers or clap-sdk) into ~/.faust2clap.""" + source = (install_root / "external" / name).resolve() + dest = Path.home() / ".faust2clap" / "external" / name + + if dest.exists(): + print(f"{CYAN}[*] {name} already exists at: {dest}{RESET}") + else: + try: + print(f"{CYAN}[*] Symlinking {name} β†’ {dest}{RESET}") + dest.parent.mkdir(parents=True, exist_ok=True) + dest.symlink_to(source) + except Exception as e: + print(f"{RED}[!] Failed to symlink {name}: {e}{RESET}") + sys.exit(1) + +# colours for terminal output +GREEN = "\033[92m" +RED = "\033[91m" +CYAN = "\033[96m" +YELLOW = "\033[93m" +RESET = "\033[0m" + +def locate_clap_architecture_file(): + """Locate the clap-arch.cpp file used for generating plugins.""" + + # try using `faust --archdir` + try: + archdir = subprocess.check_output(["faust", "--archdir"], universal_newlines=True).strip() + candidate = Path(archdir) / "clap" / "clap-arch.cpp" + if candidate.exists(): + return candidate.resolve() + except Exception: + pass + + # try FAUST_LIB env variable + faust_lib = os.environ.get("FAUST_LIB") + if faust_lib: + candidate = Path(faust_lib) / "clap" / "clap-arch.cpp" + if candidate.exists(): + return candidate.resolve() + + # fallback to bundled path relative to this script + script_dir = Path(__file__).resolve().parent + bundled = (script_dir / ".." / ".." / "architecture" / "clap" / "clap-arch.cpp").resolve() + if bundled.exists(): + return bundled + + # try standard install locations + fallback_paths = [ + "/usr/local/share/faust/architecture/clap/clap-arch.cpp", + "/opt/homebrew/share/faust/architecture/clap/clap-arch.cpp", # apple silicon + "/usr/share/faust/architecture/clap/clap-arch.cpp", + ] + for path in fallback_paths: + if Path(path).exists(): + return Path(path).resolve() + # nothing worked β€” fail + print(f"{RED}[!] Could not locate 'clap-arch.cpp' file.{RESET}") + print(f"{RED}[!] Tried:{RESET}") + print(f" - faust --archdir") + print(f" - $FAUST_LIB/clap/clap-arch.cpp") + print(f" - bundled relative path near this script: {bundled}") + for path in fallback_paths: + print(f" - {path}") + sys.exit(1) + +def locate_clap_dir(): + """Try to locate the architecture/clap directory.""" + + # prefer FAUST_SRC if set + faust_src = os.environ.get("FAUST_SRC") + if faust_src: + candidate = Path(faust_src) / "architecture" / "clap" + if candidate.is_dir(): + return candidate.resolve() + + # try walking from the script location + script_path = Path(__file__).resolve() + for parent in script_path.parents: + candidate = parent / "architecture" / "clap" + if candidate.is_dir(): + return candidate.resolve() + print(f"{RED}[!] Could not locate architecture/clap directory.{RESET}") + print(f"{RED}[!] Tried:{RESET}") + print(f" - $FAUST_SRC") + for path in fallback_paths: + print(f" {path}") + print(f"{YELLOW}[i] If you're running from a global install, try setting FAUST_SRC to your Faust repo path.{RESET}") + print(f"{CYAN}[*] Using clap_dir from: {clap_dir}{RESET}") + sys.exit(1) + +def build_dynamic_plugin(install=False): + global clap_dir + + clap_dir = ( + Path(os.environ["FAUST_SRC"]) / "architecture" / "clap" + if "FAUST_SRC" in os.environ + else Path(os.path.dirname(__file__)) / "architecture" / "clap" + ) + + # explicitly use the fallback location for Makefile.simple + makefile_path = "/usr/local/share/faust2clap/Makefile.simple" + if not Path(makefile_path).exists(): + print(f"{RED}[!] Missing Makefile.simple at: {makefile_path}{RESET}") + sys.exit(1) + + make_cmd = ["make", "-f", makefile_path] + + try: + print(f"{CYAN}[*] Building dynamic plugin in: {clap_dir}{RESET}") + subprocess.run(make_cmd, cwd=clap_dir, check=True) + print(f"{GREEN}[βœ“] Built dynamic plugin{RESET}") + + if install: + # create proper macOS bundle layout + plugin_binary = clap_dir / "FaustDynamic.clap" + bundle_dir = Path.home() / "Library" / "Audio" / "Plug-Ins" / "CLAP" / "FaustDynamic.clap" + contents_dir = bundle_dir / "Contents" / "MacOS" + plist_path = bundle_dir / "Contents" / "Info.plist" + + shutil.rmtree(bundle_dir, ignore_errors=True) + contents_dir.mkdir(parents=True, exist_ok=True) + shutil.copy2(plugin_binary, contents_dir / "FaustDynamic") + + info = get_dynamic_plugin_metadata() + plist_contents = generate_info_plist( + app_name=info["app_name"], + identifier=info["identifier"], + version=info["version"], + executable_name=info["executable_name"] + ) + with open(plist_path, "w") as f: + f.write(plist_contents) -#config -ARCH_REL_PATH="architecture/clap/clap-arch.cpp" -OUTPUT_ROOT="build" + print(f"{GREEN}[βœ“] Info.plist written to: {plist_path}{RESET}") + print(f"{GREEN}[βœ“] Bundled and installed dynamic plugin as .clap bundle{RESET}") + print(f"{GREEN}[βœ“] Installed dynamic plugin{RESET}") + except subprocess.CalledProcessError as e: + print(f"{RED}[!] Error building dynamic plugin: {e}{RESET}") + sys.exit(1) -#parse command line arguments properly -parser = argparse.ArgumentParser(description='faust2clap - Generate CLAP plugins from Faust DSP code') -parser.add_argument('dsp_file', help='Input .dsp file') +def launch_hot_reload_gui(): + clap_dir = locate_clap_dir() + gui_path = clap_dir / "faust-hot-reload.py" + + if not gui_path.exists(): + print(f"{RED}[!] GUI not found at: {gui_path}{RESET}") + return + try: + subprocess.run(["python3", str(gui_path)]) + except Exception as e: + print(f"{RED}[!] Error launching GUI: {e}{RESET}") + +parser = argparse.ArgumentParser( + description='faust2clap: Generate CLAP plugins from Faust DSP code - by Facundo Franchino', + formatter_class=argparse.ArgumentDefaultsHelpFormatter +) +parser.add_argument('--version', action='version', version='faust2clap 1.0.0') +parser.add_argument('dsp_file', nargs='?', help='Input .dsp file') parser.add_argument('-nvoices', type=int, default=16, help='Number of polyphonic voices (default: 16)') parser.add_argument('-mono', action='store_true', help='Generate monophonic plugin instead of polyphonic') parser.add_argument('-poly', action='store_true', help='Generate polyphonic plugin (default behaviour)') - +parser.add_argument('--dry-run', action='store_true', help='Run without generating or building anything') +parser.add_argument('--dynamic', action='store_true', help='Build the dynamic plugin to load any .dsp file at run-time (interpreter-based)') +parser.add_argument('--install', action='store_true', help='Install the dynamic plugin after building it (requires --dynamic)') +parser.add_argument('--gui', action='store_true', help='Launch the hot-reload GUI (faust-hot-reload.py)') args = parser.parse_args() -dsp_path = args.dsp_file #full path to the provided .dsp file +# dynamic mode +if args.dynamic: + if args.dsp_file: + print("[i] Ignoring DSP file, since --dynamic mode doesn't compile a specific file.") + build_dynamic_plugin(install=args.install) + if args.gui: + launch_hot_reload_gui() + sys.exit(0) + +#GUI only +if args.gui: + launch_hot_reload_gui() + sys.exit(0) + +#check for .dsp file +if not args.dsp_file: + print(f"{RED}[!] Error: No .dsp file provided.{RESET}") + sys.exit(1) + +dsp_path = os.path.abspath(args.dsp_file) #full path to the provided .dsp file nvoices = args.nvoices # Note: is_polyphonic will be set later based on auto-detection or user override if not os.path.isfile(dsp_path): - print(f"[!] dsp file not found: {dsp_path}") + print(f"{RED}[!] dsp file not found: {dsp_path}{RESET}") sys.exit(1) base=os.path.splitext(os.path.basename(dsp_path))[0] #name minus extension (.dsp) out_cpp=f"{base}_clap.cpp" #target outpu filename -#locate arch file using faust command -this_dir=os.path.dirname(os.path.abspath(__file__)) - # try to get architecture directory from faust itself -arch_path = None -faust_root = None +arch_path = str(locate_clap_architecture_file()) -try: - # use faust --archdir to get the architecture directory - arch_dir = subprocess.check_output(["faust", "--archdir"], universal_newlines=True).strip() - arch_path_from_faust = os.path.join(arch_dir, "clap/clap-arch.cpp") - if os.path.isfile(arch_path_from_faust): - arch_path = arch_path_from_faust - # get faust root from libdir - faust_root = subprocess.check_output(["faust", "--libdir"], universal_newlines=True).strip() -except (subprocess.CalledProcessError, FileNotFoundError): - # faust command not available or failed - pass - -# fallback: try other possible locations if faust command didn't work -if not arch_path: - possible_paths = [ - # relative to script location (development setup) - os.path.join(this_dir, "../..", ARCH_REL_PATH), - # check FAUST_LIB environment variable - os.path.join(os.environ.get("FAUST_LIB", ""), ARCH_REL_PATH) if os.environ.get("FAUST_LIB") else "", - # system-wide fallbacks (last resort) - f"/usr/local/share/faust/{ARCH_REL_PATH}", - f"/usr/share/faust/{ARCH_REL_PATH}", - f"/opt/homebrew/share/faust/{ARCH_REL_PATH}", - ] - - for path in possible_paths: - if path and os.path.isfile(path): - arch_path = path - # determine faust_root based on which path worked - if "share/faust" in path: - faust_root = path.split("share/faust")[0] + "share/faust" - elif os.environ.get("FAUST_LIB"): - faust_root = os.environ.get("FAUST_LIB") - else: - faust_root = os.path.abspath(os.path.join(os.path.dirname(path), "../..")) - break - -if not arch_path: - print(f"[!]missing architecture file: {ARCH_REL_PATH}") - print("[!]Try running: faust --archdir") - print("[!]Or set FAUST_LIB environment variable") - print("[!]Make sure Faust is properly installed and in PATH") - sys.exit(1) +# we'll build everything in ./build/ +plugin_name = os.path.splitext(os.path.basename(args.dsp_file))[0] +output_dir = Path.cwd() / "build" / plugin_name -#create output directory -output_dir=os.path.join(faust_root, OUTPUT_ROOT,base) +# make sure the output directory exists os.makedirs(output_dir, exist_ok=True) -out_cpp_path=os.path.join(output_dir,out_cpp) +out_cpp_path = os.path.join(output_dir, out_cpp) #extract metadata from the .dsp using faust -json def extract_metadata(dsp_path): @@ -96,7 +265,7 @@ def extract_metadata(dsp_path): metadata.update(entry) return metadata except (subprocess.CalledProcessError, json.JSONDecodeError, Exception) as e: - print(f"[i] JSON metadata not available, falling back to C++ parsing.") + print(f"{YELLOW}[i] JSON metadata not available, falling back to C++ parsing.{RESET}") #final fallback: parse -lang cpp output for m->declare("...", "...") try: @@ -110,7 +279,7 @@ def extract_metadata(dsp_path): metadata[key] = value return metadata except Exception as e: - print(f"[!] failed to fallback-parse cpp metadata: {e}") + print(f"{YELLOW}[!] failed to fallback-parse cpp metadata: {e}{RESET}") return {} metadata = extract_metadata(dsp_path) @@ -164,8 +333,6 @@ def detect_dsp_type(dsp_path, metadata): except: pass - #default to effect for safety (effects work for both cases) - print(f"[i] Defaulting to EFFECT mode") return 'effect' dsp_type = detect_dsp_type(dsp_path, metadata) @@ -190,7 +357,7 @@ def detect_dsp_type(dsp_path, metadata): #generate plugin_metadata.h metadata_header_path = os.path.join(output_dir, "plugin_metadata.h") -print(f"[i] Add this to your CMakeLists.txt:") +print(f"{CYAN}[i] Add this to your CMakeLists.txt:{RESET}") print(f' include_directories("{output_dir}")') with open(metadata_header_path, "w") as f: f.write(f'#define FAUST_PLUGIN_ID "{plugin_id}"\n') @@ -201,7 +368,7 @@ def detect_dsp_type(dsp_path, metadata): f.write(f'#define FAUST_NVOICES {nvoices}\n') f.write(f'#define FAUST_IS_POLYPHONIC {"1" if is_polyphonic else "0"}\n') -print("[*] extracted metadata:") +print(f"{CYAN}[*] extracted metadata:{RESET}") print(f" id: {plugin_id}") print(f" name: {plugin_name}") print(f" vendor: {plugin_vendor}") @@ -210,41 +377,55 @@ def detect_dsp_type(dsp_path, metadata): print(f" polyphonic: {is_polyphonic}") print(f" voices: {nvoices}") - #run faust to generate the plugin source file -try: - subprocess.run(["faust", "-a", arch_path, dsp_path, "-o", out_cpp_path], check=True) - print(f"[βœ“] generated: {out_cpp_path}") -except subprocess.CalledProcessError: - print("[!] faust compilation failed.") - sys.exit(1) +if args.dry_run: + print(f"{GREEN}[βœ“] Dry run: skipping Faust C++ generation.{RESET}") +else: + try: + subprocess.run(["faust", "-a", arch_path, dsp_path, "-o", out_cpp_path], check=True) + print(f"{GREEN}[βœ“] generated: {out_cpp_path}{RESET}") + except subprocess.CalledProcessError: + print(f"{RED}[!] faust compilation failed.{RESET}") + + sys.exit(1) + +#dry run +if args.dry_run: + print(f"{GREEN}[βœ“] Dry run: Skipping CMake configuration and build.{RESET}") + sys.exit(0) +start_time = time.time() #run cMake with macOS sdk path to find stdlib headers try: - print("[*] running cmake build") sdk_path = subprocess.check_output(["xcrun", "--sdk", "macosx", "--show-sdk-path"], universal_newlines=True).strip() cxx_include_path = "/Library/Developer/CommandLineTools/usr/include/c++/v1" + faust2clap_cmake_dir = Path(__file__).resolve().parent + build_dir = os.path.abspath(output_dir) # reusing generated output dir + +# decided to hardcode faust2clap_cmake_dir so a little safeguard can help a fail to be early and clear +# so add a defensive check to make sure CMakeLists.txt exists + if not os.path.isfile(os.path.join(faust2clap_cmake_dir, "CMakeLists.txt")): + print(f"{RED}[!] CMakeLists.txt not found in {faust2clap_cmake_dir}{RESET}") + + sys.exit(1) + subprocess.run([ - "cmake", "-S", ".", "-B", "build", + "cmake", "-S", faust2clap_cmake_dir, "-B", build_dir, f"-DCMAKE_CXX_FLAGS=-isysroot {sdk_path} -I{cxx_include_path}" - ], cwd=this_dir, check=True, capture_output=True) + ], check=True, capture_output=True) - subprocess.run(["cmake", "--build", "build"], cwd=this_dir, check=True, capture_output=True) - - print("[βœ“] build completed successfully.") + subprocess.run(["cmake", "--build", build_dir], check=True, capture_output=True) + +# wee change here, let's also print the build directory, if succesful + print(f"{GREEN}[βœ“] build completed successfully. Plugin is in: {build_dir}{RESET}") + duration = time.time() - start_time + print(f"{GREEN}βœ“ Done in {duration:.2f}s{RESET}") except subprocess.CalledProcessError as e: - print("[!] cmake build failed.") - print(f"[stderr]\n{e.stderr.decode() if e.stderr else 'No stderr'}") - print(f"[stdout]\n{e.stdout.decode() if e.stdout else 'No stdout'}") - sys.exit(1) + print(f"{RED}[!] cmake build failed.{RESET}") + print(f"{RED}[stderr]\n{e.stderr.decode() if e.stderr else 'No stderr'}{RESET}") + print(f"{RED}[stdout]\n{e.stdout.decode() if e.stdout else 'No stdout'}{RESET}") - -except subprocess.CalledProcessError as e: - print("[!] cmake build failed.") - print(f"[stderr]\n{e.stderr.decode() if e.stderr else 'No stderr'}") - print(f"[stdout]\n{e.stdout.decode() if e.stdout else 'No stdout'}") - sys.exit(1) diff --git a/tools/faust2clap/faust2clap.sh b/tools/faust2clap/faust2clap.sh new file mode 100755 index 0000000000..6a7a737612 --- /dev/null +++ b/tools/faust2clap/faust2clap.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# created by Facundo Franchino + +##################################################################### +# # +# faust2clap generator # +# (c) Grame & Facundo Franchino, 2025 # +# # +##################################################################### + +# wrapper for faust2clap.py +# runs the script from its actual location (not from /usr/local/bin) + +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do + DIR="$(cd -P "$(dirname "$SOURCE")" >/dev/null 2>&1 && pwd)" + SOURCE="$(readlink "$SOURCE")" + [[ "$SOURCE" != /* ]] && SOURCE="$DIR/$SOURCE" +done +SCRIPT_DIR="$(cd -P "$(dirname "$SOURCE")" >/dev/null 2>&1 && pwd)" +if [[ "$1" == "--gui" ]]; then + python3 "${SCRIPT_DIR}/faust-hot-reload.py" + exit 0 +fi +python3 "${SCRIPT_DIR}/faust2clap.py" "$@" diff --git a/tools/faust2clap/install-faust2clap.sh b/tools/faust2clap/install-faust2clap.sh new file mode 100755 index 0000000000..6cab3c6843 --- /dev/null +++ b/tools/faust2clap/install-faust2clap.sh @@ -0,0 +1,112 @@ +#!/bin/bash +set -euo pipefail + +##################################################################### +# # +# install-faust2clap.sh - setup for global faust2clap # +# (c) Grame & Facundo Franchino # +# # +##################################################################### + +INSTALL_ROOT="/usr/local/share/faust2clap" +BIN_LINK="/usr/local/bin/faust2clap" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +# create install location +sudo mkdir -p "$INSTALL_ROOT" + +# copy full required folders from REPO_ROOT +for dir in architecture external; do + if [ -d "$REPO_ROOT/$dir" ]; then + sudo cp -R "$REPO_ROOT/$dir" "$INSTALL_ROOT/" + else + echo "⚠️ Skipping missing directory: $dir" + fi +done + +# copy gui_stuff contents into architecture/clap/gui_stuff +GUI_ASSETS_SRC="$REPO_ROOT/architecture/clap/gui_stuff" +GUI_ASSETS_DEST="$INSTALL_ROOT/architecture/clap/gui_stuff" +if [ -d "$GUI_ASSETS_SRC" ]; then + sudo mkdir -p "$GUI_ASSETS_DEST" + sudo cp -R "$GUI_ASSETS_SRC/"* "$GUI_ASSETS_DEST/" +else + echo "⚠️ gui_stuff folder not found at: $GUI_ASSETS_SRC" +fi + +# copy core files from SCRIPT_DIR (tools/faust2clap) +for file in faust2clap.py faust2clap.sh CMakeLists.txt; do + if [ -f "$SCRIPT_DIR/$file" ]; then + sudo cp "$SCRIPT_DIR/$file" "$INSTALL_ROOT/" + else + echo "⚠️ Missing expected file: $file" + fi +done + +# copy Makefile.simple from architecture/clap instead +MAKEFILE_SRC="$REPO_ROOT/architecture/clap/Makefile.simple" +if [ -f "$MAKEFILE_SRC" ]; then + sudo cp "$MAKEFILE_SRC" "$INSTALL_ROOT/" +else + echo "⚠️ Missing Makefile.simple in architecture/clap" +fi + +# patch Makefile.simple include paths to point to install location +MAKEFILE_INSTALLED="$INSTALL_ROOT/Makefile.simple" +if [ -f "$MAKEFILE_INSTALLED" ]; then + sudo sed -i '' "s|../../architecture|$INSTALL_ROOT/architecture|g" "$MAKEFILE_INSTALLED" + sudo sed -i '' "s|\$(FAUST2CLAP_ROOT)/external|$INSTALL_ROOT/external|g" "$MAKEFILE_INSTALLED" +fi + +# copy GUI script from gui_stuff +GUI_SCRIPT_SRC="$REPO_ROOT/architecture/clap/faust-hot-reload.py" +if [ -f "$GUI_SCRIPT_SRC" ]; then + sudo cp "$GUI_SCRIPT_SRC" "$INSTALL_ROOT/" +else + echo "⚠️ GUI script not found at: $GUI_SCRIPT_SRC" +fi + +# copy icon asset for desktop gui +ICON_SRC="$REPO_ROOT/architecture/clap/gui_stuff/grame.icns" +ICON_DEST="$INSTALL_ROOT/architecture/clap/gui_stuff/grame.icns" + +if [ -f "$ICON_SRC" ]; then + sudo mkdir -p "$(dirname "$ICON_DEST")" + sudo cp "$ICON_SRC" "$ICON_DEST" +else + echo "⚠️ grame.icns not found at: $ICON_SRC" +fi + +# create CLI symlink +sudo ln -sf "$INSTALL_ROOT/faust2clap.sh" "$BIN_LINK" +sudo chmod +x "$INSTALL_ROOT/faust2clap.sh" + +# optional: install libfaust.dylib +FAUST_LIB_SRC="$REPO_ROOT/build/lib/libfaust.dylib" +FAUST_LIB_DEST="/usr/local/lib/libfaust.dylib" + +if [ -f "$FAUST_LIB_SRC" ]; then + if [ ! -f "$FAUST_LIB_DEST" ]; then + sudo cp "$FAUST_LIB_SRC" "$FAUST_LIB_DEST" + echo "βœ… libfaust.dylib installed." + else + echo "ℹ️ libfaust.dylib already exists in /usr/local/lib" + fi +else + echo "⚠️ libfaust.dylib not found in expected build location." + echo "πŸ”¨ Attempting to build libfaust..." + cd "$REPO_ROOT" + make -j$(sysctl -n hw.ncpu || echo 4) + + if [ -f "$FAUST_LIB_SRC" ]; then + sudo cp "$FAUST_LIB_SRC" "$FAUST_LIB_DEST" + echo "βœ… libfaust.dylib installed after build." + else + echo "❌ Still missing libfaust.dylib after build. Please verify Faust is built." + fi +fi + +echo "βœ… faust2clap installed to: $INSTALL_ROOT" +echo "πŸ‘‰ Try: faust2clap --dynamic --install" +echo "πŸ‘‰ Try: faust2clap myeffect.dsp" \ No newline at end of file