diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index c28654770..7a610c47a 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -13,9 +13,11 @@ on:
- "!**/native/java*/**"
- "!**/native/*_android.*"
- "!**/native/*_apple.*"
+ - "!**/native/*_curl.*"
- "!**/native/*_emscripten.*"
- "!**/native/*_ios.*"
- "!**/native/*_mac.*"
+ - "!**/native/*_sdl2.*"
- "!**/native/*_wasm.*"
- "!**/native/*_windows.*"
branches:
@@ -32,9 +34,11 @@ on:
- "!**/native/java*/**"
- "!**/native/*_android.*"
- "!**/native/*_apple.*"
+ - "!**/native/*_curl.*"
- "!**/native/*_emscripten.*"
- "!**/native/*_ios.*"
- "!**/native/*_mac.*"
+ - "!**/native/*_sdl2.*"
- "!**/native/*_wasm.*"
- "!**/native/*_windows.*"
branches:
@@ -93,6 +97,15 @@ jobs:
"*/usr/*" \
"*/opt/*" \
"*/CMakeFiles/*" \
+ "*/native/*_android.*" \
+ "*/native/*_apple.*" \
+ "*/native/*_curl.*" \
+ "*/native/*_emscripten.*" \
+ "*/native/*_ios.*" \
+ "*/native/*_mac.*" \
+ "*/native/*_sdl2.*" \
+ "*/native/*_wasm.*" \
+ "*/native/*_windows.*" \
--output-file coverage/coverage_final.info --ignore-errors ${IGNORE_ERRORS}
lcov --list coverage/coverage_final.info
- name: Upload Coverage to Codecov
diff --git a/CMakeLists.txt b/CMakeLists.txt
index bedc04bab..86b96c279 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -36,9 +36,13 @@ set_property (GLOBAL PROPERTY USE_FOLDERS ON)
# Options
option (YUP_TARGET_ANDROID "Target Android project" OFF)
option (YUP_TARGET_ANDROID_BUILD_GRADLE "When building for Android, build the gradle infrastructure" OFF)
+option (YUP_EXPORT_MODULES "Export the modules to the parent project" ON)
option (YUP_ENABLE_PROFILING "Enable the profiling code using Perfetto SDK" OFF)
option (YUP_ENABLE_COVERAGE "Enable code coverage collection for tests" OFF)
-option (YUP_EXPORT_MODULES "Export the modules to the parent project" ON)
+option (YUP_ENABLE_VST3_VALIDATOR "Enable the Steinberg VST3 validator for VST3 plugins" ${PROJECT_IS_TOP_LEVEL})
+option (YUP_ENABLE_CLAP_VALIDATOR "Enable clap-validator validation for CLAP plugins" ${PROJECT_IS_TOP_LEVEL})
+option (YUP_ENABLE_AUVAL_VALIDATOR "Enable auval validation for AU plugins" ${PROJECT_IS_TOP_LEVEL})
+option (YUP_ENABLE_PLUGINVAL "Enable pluginval validation for VST3/AU plugins" ${PROJECT_IS_TOP_LEVEL})
option (YUP_ENABLE_STATIC_PYTHON_LIBS "Use static Python libraries" OFF)
option (YUP_BUILD_JAVA_SUPPORT "Build the Java support" OFF)
option (YUP_BUILD_EXAMPLES "Build the examples" ${PROJECT_IS_TOP_LEVEL})
@@ -70,6 +74,17 @@ if (YUP_ENABLE_PROFILING)
_yup_fetch_perfetto()
endif()
+# Setup validation tools
+if (YUP_ENABLE_PLUGINVAL)
+ _yup_message (STATUS "Setting up pluginval")
+ yup_setup_pluginval()
+endif()
+
+if (YUP_ENABLE_CLAP_VALIDATOR)
+ _yup_message (STATUS "Setting up clap-validator")
+ yup_setup_clap_validator()
+endif()
+
# Targets
if (YUP_BUILD_EXAMPLES)
_yup_message (STATUS "Building examples")
diff --git a/cmake/platforms/ios/Info.plist b/cmake/platforms/ios/ApplicationInfo.plist
similarity index 100%
rename from cmake/platforms/ios/Info.plist
rename to cmake/platforms/ios/ApplicationInfo.plist
diff --git a/cmake/platforms/mac/Info.plist b/cmake/platforms/mac/ApplicationInfo.plist
similarity index 100%
rename from cmake/platforms/mac/Info.plist
rename to cmake/platforms/mac/ApplicationInfo.plist
diff --git a/cmake/platforms/mac/AudioPluginInfo.plist.in b/cmake/platforms/mac/AudioPluginInfo.plist.in
new file mode 100644
index 000000000..99c05fbef
--- /dev/null
+++ b/cmake/platforms/mac/AudioPluginInfo.plist.in
@@ -0,0 +1,26 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ English
+ CFBundleExecutable
+ ${MACOSX_BUNDLE_EXECUTABLE_NAME}
+ CFBundleIdentifier
+ ${MACOSX_BUNDLE_GUI_IDENTIFIER}
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ ${MACOSX_BUNDLE_BUNDLE_NAME}
+ CFBundlePackageType
+ @YUP_AUDIO_PLUGIN_BUNDLE_PACKAGE_TYPE@
+ CFBundleShortVersionString
+ ${MACOSX_BUNDLE_SHORT_VERSION_STRING}
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ ${MACOSX_BUNDLE_BUNDLE_VERSION}
+ CSResourcesFileMapped
+
+
+
diff --git a/cmake/platforms/mac/AudioUnitInfo.plist.in b/cmake/platforms/mac/AudioUnitInfo.plist.in
new file mode 100644
index 000000000..aa796a91f
--- /dev/null
+++ b/cmake/platforms/mac/AudioUnitInfo.plist.in
@@ -0,0 +1,47 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ English
+ CFBundleExecutable
+ ${MACOSX_BUNDLE_EXECUTABLE_NAME}
+ CFBundleIdentifier
+ ${MACOSX_BUNDLE_GUI_IDENTIFIER}
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ @PLUGIN_AU_NAME@
+ CFBundlePackageType
+ BNDL
+ CFBundleShortVersionString
+ @PLUGIN_AU_VERSION@
+ CFBundleVersion
+ @PLUGIN_AU_VERSION@
+ CFBundleSignature
+ @PLUGIN_AU_MANUFACTURER@
+ NSHumanReadableCopyright
+ Copyright (c) 2026 - kunitoki@gmail.com
+ NSHighResolutionCapable
+
+ AudioComponents
+
+
+ type
+ @PLUGIN_AU_TYPE@
+ subtype
+ @PLUGIN_AU_SUBTYPE@
+ manufacturer
+ @PLUGIN_AU_MANUFACTURER@
+ name
+ @PLUGIN_AU_NAME@
+ version
+ 1
+ factoryFunction
+ AudioPluginProcessorAUFactory
+ sandboxSafe
+
+
+
+
+
diff --git a/cmake/yup.cmake b/cmake/yup.cmake
index f931b9bbd..4df50e442 100644
--- a/cmake/yup.cmake
+++ b/cmake/yup.cmake
@@ -95,6 +95,8 @@ include (${CMAKE_CURRENT_LIST_DIR}/yup_utilities.cmake)
include (${CMAKE_CURRENT_LIST_DIR}/yup_dependencies.cmake)
include (${CMAKE_CURRENT_LIST_DIR}/yup_modules.cmake)
include (${CMAKE_CURRENT_LIST_DIR}/yup_standalone.cmake)
+include (${CMAKE_CURRENT_LIST_DIR}/yup_pluginval.cmake)
+include (${CMAKE_CURRENT_LIST_DIR}/yup_codesign.cmake)
include (${CMAKE_CURRENT_LIST_DIR}/yup_audio_plugin.cmake)
include (${CMAKE_CURRENT_LIST_DIR}/yup_embed_binary.cmake)
include (${CMAKE_CURRENT_LIST_DIR}/yup_python.cmake)
diff --git a/cmake/yup_audio_plugin.cmake b/cmake/yup_audio_plugin.cmake
index d5f7ce766..d9acaf947 100644
--- a/cmake/yup_audio_plugin.cmake
+++ b/cmake/yup_audio_plugin.cmake
@@ -19,6 +19,13 @@
#==============================================================================
+function (_yup_configure_audio_plugin_bundle_info_plist output_file package_type)
+ set (YUP_AUDIO_PLUGIN_BUNDLE_PACKAGE_TYPE "${package_type}")
+ configure_file ("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/platforms/mac/AudioPluginInfo.plist.in" "${output_file}" @ONLY)
+endfunction()
+
+#==============================================================================
+
function (yup_audio_plugin)
# ==== Fetch options
set (options CONSOLE)
@@ -27,7 +34,7 @@ function (yup_audio_plugin)
# Globals
TARGET_NAME TARGET_VERSION TARGET_IDE_GROUP TARGET_APP_ID TARGET_APP_NAMESPACE TARGET_CXX_STANDARD
# Plugin types
- PLUGIN_CREATE_CLAP PLUGIN_CREATE_VST3 PLUGIN_CREATE_STANDALONE)
+ PLUGIN_CREATE_CLAP PLUGIN_CREATE_VST3 PLUGIN_CREATE_STANDALONE PLUGIN_CREATE_AU)
set (multi_value_args
DEFINITIONS
@@ -44,6 +51,11 @@ function (yup_audio_plugin)
set (target_app_id "${YUP_ARG_TARGET_APP_ID}")
set (target_app_namespace "${YUP_ARG_TARGET_APP_NAMESPACE}")
set (target_cxx_standard "${YUP_ARG_TARGET_CXX_STANDARD}")
+ set (target_bundle_id "${target_app_id}")
+ if (NOT target_bundle_id)
+ set (target_bundle_id "org.kunitoki.yup.${target_name}")
+ endif()
+ string (REGEX REPLACE "[^A-Za-z0-9.-]" "-" target_bundle_id "${target_bundle_id}")
set (additional_definitions "")
set (additional_options "")
set (additional_libraries "")
@@ -55,8 +67,8 @@ function (yup_audio_plugin)
return()
endif()
- if (NOT YUP_ARG_PLUGIN_CREATE_CLAP AND NOT YUP_ARG_PLUGIN_CREATE_VST3 AND NOT YUP_ARG_PLUGIN_CREATE_STANDALONE)
- _yup_message (FATAL_ERROR "At least one plugin type must be enabled (CLAP, VST3, or Standalone).")
+ if (NOT YUP_ARG_PLUGIN_CREATE_CLAP AND NOT YUP_ARG_PLUGIN_CREATE_VST3 AND NOT YUP_ARG_PLUGIN_CREATE_STANDALONE AND NOT YUP_ARG_PLUGIN_CREATE_AU)
+ _yup_message (FATAL_ERROR "At least one plugin type must be enabled (CLAP, VST3, AU, or Standalone).")
return()
endif()
@@ -115,7 +127,11 @@ function (yup_audio_plugin)
# Create CLAP plugin target
_yup_message (STATUS "Creating CLAP plugin target")
- add_library (${target_name}_clap_plugin SHARED)
+ if (YUP_PLATFORM_MAC)
+ add_library (${target_name}_clap_plugin MODULE)
+ else()
+ add_library (${target_name}_clap_plugin SHARED)
+ endif()
target_compile_features (${target_name}_clap_plugin PRIVATE cxx_std_20)
@@ -140,11 +156,46 @@ function (yup_audio_plugin)
${YUP_ARG_MODULES})
set_target_properties (${target_name}_clap_plugin PROPERTIES
- SUFFIX ".clap"
+ C_VISIBILITY_PRESET hidden
+ CXX_VISIBILITY_PRESET hidden
+ OBJC_VISIBILITY_PRESET hidden
+ OBJCXX_VISIBILITY_PRESET hidden
+ VISIBILITY_INLINES_HIDDEN ON
FOLDER "${YUP_ARG_TARGET_IDE_GROUP}"
XCODE_GENERATE_SCHEME ON)
- #yup_audio_plugin_copy_bundle (${target_name} clap)
+ if (YUP_PLATFORM_MAC)
+ set (clap_plist_output "${CMAKE_CURRENT_BINARY_DIR}/${target_name}_clap_plugin.plist")
+ _yup_configure_audio_plugin_bundle_info_plist ("${clap_plist_output}" "BNDL")
+
+ set_target_properties (${target_name}_clap_plugin PROPERTIES
+ BUNDLE TRUE
+ BUNDLE_EXTENSION "clap"
+ MACOSX_BUNDLE TRUE
+ MACOSX_BUNDLE_INFO_PLIST "${clap_plist_output}"
+ MACOSX_BUNDLE_BUNDLE_NAME "${target_name}_clap_plugin"
+ MACOSX_BUNDLE_BUNDLE_VERSION "${target_version}"
+ MACOSX_BUNDLE_SHORT_VERSION_STRING "${target_version}"
+ MACOSX_BUNDLE_GUI_IDENTIFIER "${target_bundle_id}.clap"
+ XCODE_ATTRIBUTE_GENERATE_PKGINFO_FILE YES
+ XCODE_ATTRIBUTE_PRODUCT_BUNDLE_PACKAGE_TYPE BNDL
+ XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${target_bundle_id}.clap"
+ PREFIX "")
+
+ set (clap_plugin_path "$")
+ else()
+ set_target_properties (${target_name}_clap_plugin PROPERTIES
+ PREFIX ""
+ SUFFIX ".clap")
+
+ set (clap_plugin_path "$")
+ endif()
+
+ yup_codesign_target (${target_name}_clap_plugin "${clap_plugin_path}")
+
+ yup_validate_clap_plugin (${target_name}_clap_plugin "${clap_plugin_path}")
+
+ yup_audio_plugin_copy_bundle (${target_name} clap)
endif()
# ==== Fetch vst3 SDK and build vst3 target
@@ -152,7 +203,9 @@ function (yup_audio_plugin)
_yup_fetch_vst3sdk()
_yup_message (STATUS "Setting up VST3 plugin client")
+ get_directory_property (_yup_vst3_saved_compile_options COMPILE_OPTIONS)
smtg_enable_vst3_sdk()
+ set_directory_properties (PROPERTIES COMPILE_OPTIONS "${_yup_vst3_saved_compile_options}")
_yup_module_setup_plugin_client (
${target_name}
@@ -189,31 +242,52 @@ function (yup_audio_plugin)
${additional_libraries}
${YUP_ARG_MODULES})
+ set_target_properties (${target_name}_vst3_plugin PROPERTIES
+ C_VISIBILITY_PRESET hidden
+ CXX_VISIBILITY_PRESET hidden
+ OBJC_VISIBILITY_PRESET hidden
+ OBJCXX_VISIBILITY_PRESET hidden
+ VISIBILITY_INLINES_HIDDEN ON
+ SUFFIX ".vst3"
+ FOLDER "${YUP_ARG_TARGET_IDE_GROUP}"
+ XCODE_GENERATE_SCHEME ON)
+
+ set (vst3_plugin_binary_path "$")
+ set (vst3_pluginval_path "${vst3_plugin_binary_path}")
+ get_target_property (vst3_plugin_package_path ${target_name}_vst3_plugin SMTG_PLUGIN_PACKAGE_PATH)
+ if (vst3_plugin_package_path)
+ set (vst3_pluginval_path "${vst3_plugin_package_path}")
+ else()
+ set (vst3_plugin_package_path "${vst3_plugin_binary_path}")
+ endif()
if (YUP_PLATFORM_MAC)
smtg_target_set_bundle (${target_name}_vst3_plugin
- BUNDLE_IDENTIFIER org.kunitoki.yup.${target_name}
- COMPANY_NAME "kunitoki")
-
- #smtg_target_set_debug_executable(MyPlugin
- # "/Applications/VST3PluginTestHost.app"
- # "--pluginfolder;$(BUILT_PRODUCTS_DIR)")
-
- if (NOT XCODE)
- add_custom_command(
- TARGET ${target_name}_vst3_plugin POST_BUILD
- COMMAND ${CMAKE_COMMAND} -E echo [SMTG] Validator started...
- COMMAND
- $
- "${CMAKE_BINARY_DIR}/VST3/${CMAKE_BUILD_TYPE}/${CMAKE_BUILD_TYPE}/${target_name}_vst3_plugin.vst3"
- WORKING_DIRECTORY "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}"
- COMMAND ${CMAKE_COMMAND} -E echo [SMTG] Validator finished.)
+ BUNDLE_IDENTIFIER "${target_bundle_id}"
+ COMPANY_NAME "kunitoki") # TODO - make company name configurable
+
+ set (vst3_plist_output "${CMAKE_CURRENT_BINARY_DIR}/${target_name}_vst3_plugin.plist")
+ _yup_configure_audio_plugin_bundle_info_plist ("${vst3_plist_output}" "BNDL")
+
+ set_target_properties (${target_name}_vst3_plugin PROPERTIES
+ MACOSX_BUNDLE_INFO_PLIST "${vst3_plist_output}"
+ MACOSX_BUNDLE_BUNDLE_NAME "${target_name}_vst3_plugin"
+ MACOSX_BUNDLE_BUNDLE_VERSION "${target_version}"
+ MACOSX_BUNDLE_SHORT_VERSION_STRING "${target_version}"
+ MACOSX_BUNDLE_GUI_IDENTIFIER "${target_bundle_id}"
+ XCODE_ATTRIBUTE_INFOPLIST_FILE "${vst3_plist_output}"
+ XCODE_ATTRIBUTE_PRODUCT_BUNDLE_PACKAGE_TYPE BNDL)
+
+ if (XCODE)
+ get_target_property (vst3_plugin_package_path ${target_name}_vst3_plugin SMTG_PLUGIN_PACKAGE_PATH)
+ else()
+ set (vst3_plugin_package_path "$")
endif()
+
+ set (vst3_pluginval_path "${vst3_plugin_package_path}")
endif()
+ yup_validate_smtg_vst3_plugin (${target_name}_vst3_plugin "${vst3_plugin_package_path}")
- set_target_properties (${target_name}_vst3_plugin PROPERTIES
- SUFFIX ".vst3"
- FOLDER "${YUP_ARG_TARGET_IDE_GROUP}"
- XCODE_GENERATE_SCHEME ON)
+ yup_validate_pluginval (${target_name}_vst3_plugin "${vst3_pluginval_path}")
yup_audio_plugin_copy_bundle (${target_name} vst3)
endif()
@@ -233,7 +307,7 @@ function (yup_audio_plugin)
TARGET_NAME ${target_name}_standalone_plugin
TARGET_VERSION ${target_version}
TARGET_IDE_GROUP ${target_ide_group}
- TARGET_APP_ID ${target_app_id}
+ TARGET_APP_ID ${target_bundle_id}
TARGET_APP_NAMESPACE ${target_app_namespace}
TARGET_CXX_STANDARD ${target_cxx_standard}
DEFINITIONS
@@ -247,6 +321,156 @@ function (yup_audio_plugin)
${YUP_ARG_MODULES})
endif()
+ # ==== Build AUv2 plugin target (macOS only)
+ if (YUP_ARG_PLUGIN_CREATE_AU)
+ if (NOT YUP_PLATFORM_MAC)
+ _yup_message (WARNING "AUv2 plugins are only supported on macOS. Skipping AU target.")
+ else()
+ _yup_fetch_apple_ausdk()
+
+ _yup_message (STATUS "Setting up AUv2 plugin client")
+ _yup_module_setup_plugin_client (
+ ${target_name}
+ yup_audio_plugin_client
+ ${YUP_ARG_TARGET_IDE_GROUP}
+ au
+ ${YUP_ARG_UNPARSED_ARGUMENTS})
+
+ # Determine AU type (aumu for instruments, aufx for effects)
+ cmake_parse_arguments (AU_ARGS ""
+ "PLUGIN_IS_SYNTH;PLUGIN_AU_SUBTYPE;PLUGIN_AU_MANUFACTURER;PLUGIN_NAME;PLUGIN_VERSION;PLUGIN_ID;PLUGIN_VENDOR;PLUGIN_DESCRIPTION;PLUGIN_URL;PLUGIN_EMAIL;PLUGIN_IS_MONO"
+ "" ${YUP_ARG_UNPARSED_ARGUMENTS})
+ if (AU_ARGS_PLUGIN_IS_SYNTH)
+ set (au_bundle_type "aumu")
+ else()
+ set (au_bundle_type "aufx")
+ endif()
+
+ if (NOT AU_ARGS_PLUGIN_AU_SUBTYPE)
+ set (AU_ARGS_PLUGIN_AU_SUBTYPE "Dflt")
+ endif()
+ if (NOT AU_ARGS_PLUGIN_AU_MANUFACTURER)
+ set (AU_ARGS_PLUGIN_AU_MANUFACTURER "Yup!")
+ endif()
+ if (NOT AU_ARGS_PLUGIN_NAME)
+ set (AU_ARGS_PLUGIN_NAME "${target_name}")
+ endif()
+ if (NOT AU_ARGS_PLUGIN_VERSION)
+ set (AU_ARGS_PLUGIN_VERSION "1")
+ endif()
+
+ _yup_message (STATUS "Creating AUv2 plugin target")
+ add_library (${target_name}_au_plugin MODULE)
+
+ target_compile_features (${target_name}_au_plugin PRIVATE cxx_std_${target_cxx_standard})
+
+ target_compile_definitions (${target_name}_au_plugin PRIVATE
+ YUP_AUDIO_PLUGIN_ENABLE_AU=1
+ YUP_STANDALONE_APPLICATION=0)
+
+ target_link_libraries (${target_name}_au_plugin PRIVATE
+ ${target_name}_shared
+ yup_audio_plugin_client
+ base-sdk-auv2
+ ${target_name}_au
+ ${additional_libraries}
+ ${YUP_ARG_MODULES}
+ "-framework AudioUnit"
+ "-framework AudioToolbox"
+ "-framework CoreAudio"
+ "-framework CoreFoundation"
+ "-framework AppKit")
+
+ _yup_module_apply_arc_to_target_sources (${target_name}_au_plugin
+ ${target_name}_shared
+ yup_audio_plugin_client
+ base-sdk-auv2
+ ${target_name}_au
+ ${additional_libraries}
+ ${YUP_ARG_MODULES})
+
+ # Generate the AU Info.plist from our template
+ set (au_plist_template "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/platforms/mac/AudioUnitInfo.plist.in")
+ set (au_plist_output "${CMAKE_CURRENT_BINARY_DIR}/${target_name}_au_plugin.plist")
+
+ set (PLUGIN_AU_TYPE "${au_bundle_type}")
+ set (PLUGIN_AU_SUBTYPE "${AU_ARGS_PLUGIN_AU_SUBTYPE}")
+ set (PLUGIN_AU_MANUFACTURER "${AU_ARGS_PLUGIN_AU_MANUFACTURER}")
+ set (PLUGIN_AU_NAME "${AU_ARGS_PLUGIN_NAME}")
+ set (PLUGIN_AU_VERSION "${AU_ARGS_PLUGIN_VERSION}")
+
+ set (au_bundle_identifier "${target_bundle_id}.au")
+ string (REGEX REPLACE "[^A-Za-z0-9.-]" "-" au_bundle_identifier "${au_bundle_identifier}")
+
+ set (au_pkginfo_file "${CMAKE_CURRENT_BINARY_DIR}/${target_name}_au_plugin.PkgInfo")
+ file (WRITE "${au_pkginfo_file}" "BNDL${AU_ARGS_PLUGIN_AU_MANUFACTURER}")
+
+ configure_file ("${au_plist_template}" "${au_plist_output}" @ONLY)
+
+ set_target_properties (${target_name}_au_plugin PROPERTIES
+ C_VISIBILITY_PRESET hidden
+ CXX_VISIBILITY_PRESET hidden
+ OBJC_VISIBILITY_PRESET hidden
+ OBJCXX_VISIBILITY_PRESET hidden
+ VISIBILITY_INLINES_HIDDEN ON
+ BUNDLE TRUE
+ BUNDLE_EXTENSION "component"
+ MACOSX_BUNDLE TRUE
+ MACOSX_BUNDLE_INFO_PLIST "${au_plist_output}"
+ MACOSX_BUNDLE_BUNDLE_NAME "${AU_ARGS_PLUGIN_NAME}"
+ MACOSX_BUNDLE_BUNDLE_VERSION "${AU_ARGS_PLUGIN_VERSION}"
+ MACOSX_BUNDLE_SHORT_VERSION_STRING "${AU_ARGS_PLUGIN_VERSION}"
+ MACOSX_BUNDLE_GUI_IDENTIFIER "${au_bundle_identifier}"
+ FOLDER "${YUP_ARG_TARGET_IDE_GROUP}"
+ XCODE_ATTRIBUTE_GENERATE_PKGINFO_FILE YES
+ XCODE_ATTRIBUTE_PRODUCT_BUNDLE_PACKAGE_TYPE BNDL
+ XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${au_bundle_identifier}"
+ XCODE_GENERATE_SCHEME ON)
+
+ add_custom_command (TARGET ${target_name}_au_plugin POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different "${au_pkginfo_file}" "$/PkgInfo"
+ COMMENT "Generating AU PkgInfo"
+ VERBATIM)
+
+ yup_codesign_target (${target_name}_au_plugin "$")
+
+ yup_audio_plugin_copy_bundle (${target_name} au)
+
+ set (au_pluginval_path "$ENV{HOME}/Library/Audio/Plug-Ins/Components/${target_name}_au_plugin.component")
+
+ yup_validate_au_plugin (
+ ${target_name}_au_plugin
+ "${AU_ARGS_PLUGIN_NAME}"
+ "${au_bundle_type}"
+ "${AU_ARGS_PLUGIN_AU_SUBTYPE}"
+ "${AU_ARGS_PLUGIN_AU_MANUFACTURER}")
+
+ yup_validate_pluginval (
+ ${target_name}_au_plugin
+ "${au_pluginval_path}")
+ endif()
+ endif()
+
+ # ==== Create composite target for all enabled plugin formats
+ set (_all_plugin_targets "")
+ if (YUP_ARG_PLUGIN_CREATE_CLAP)
+ list (APPEND _all_plugin_targets ${target_name}_clap_plugin)
+ endif()
+ if (YUP_ARG_PLUGIN_CREATE_VST3)
+ list (APPEND _all_plugin_targets ${target_name}_vst3_plugin)
+ endif()
+ if (YUP_ARG_PLUGIN_CREATE_STANDALONE)
+ list (APPEND _all_plugin_targets ${target_name}_standalone_plugin)
+ endif()
+ if (YUP_ARG_PLUGIN_CREATE_AU AND YUP_PLATFORM_MAC)
+ list (APPEND _all_plugin_targets ${target_name}_au_plugin)
+ endif()
+
+ add_custom_target (${target_name} DEPENDS ${_all_plugin_targets})
+ set_target_properties (${target_name} PROPERTIES
+ FOLDER "${YUP_ARG_TARGET_IDE_GROUP}"
+ XCODE_GENERATE_SCHEME ON)
+
endfunction()
#==============================================================================
@@ -258,11 +482,18 @@ function (yup_audio_plugin_copy_bundle target_name plugin_type)
string (TOUPPER "${plugin_type}" plugin_type_upper)
set (dependency_target ${target_name}_${plugin_type}_plugin)
- set (target_file_name "${target_name}_${plugin_type}_plugin.${plugin_type}")
- set (plugin_target_path "$ENV{HOME}/Library/Audio/Plug-Ins/${plugin_type_upper}")
+
+ if ("${plugin_type}" STREQUAL "au")
+ set (target_file_name "${target_name}_${plugin_type}_plugin.component")
+ set (plugin_target_path "$ENV{HOME}/Library/Audio/Plug-Ins/Components")
+ else()
+ set (target_file_name "${target_name}_${plugin_type}_plugin.${plugin_type}")
+ set (plugin_target_path "$ENV{HOME}/Library/Audio/Plug-Ins/${plugin_type_upper}")
+ endif()
+
set (plugin_path "${plugin_target_path}/${target_file_name}")
- if (NOT EXISTS ${plugin_target_path})
+ if (NOT EXISTS ${plugin_target_path} AND NOT "${plugin_type}" STREQUAL "clap")
_yup_message (STATUS "Plugin path ${plugin_target_path} does not exist, skipping copy")
return()
endif()
@@ -271,15 +502,31 @@ function (yup_audio_plugin_copy_bundle target_name plugin_type)
if ("${plugin_type}" STREQUAL "clap")
add_custom_command(TARGET ${dependency_target} POST_BUILD
- COMMAND ${CMAKE_COMMAND} -E rm -f ${plugin_path}
- COMMAND ${CMAKE_COMMAND} -E create_symlink "$" ${plugin_path}
- COMMENT "Copying ${plugin_type_upper} plugin to ${plugin_path}"
+ COMMAND ${CMAKE_COMMAND} -E make_directory "${plugin_target_path}"
+ COMMAND ${CMAKE_COMMAND} -E rm -rf "${plugin_path}"
+ COMMAND ${CMAKE_COMMAND} -E create_symlink "$" "${plugin_path}"
+ COMMENT "Symlinking CLAP plugin ${plugin_type_upper} plugin to ${plugin_path}"
VERBATIM)
elseif ("${plugin_type}" STREQUAL "vst3")
+ if (YUP_PLATFORM_MAC AND NOT XCODE)
+ set (source_plugin_path "$")
+ else()
+ get_target_property (source_plugin_path ${dependency_target} SMTG_PLUGIN_PACKAGE_PATH)
+ if (NOT source_plugin_path)
+ set (source_plugin_path "$")
+ endif()
+ endif()
+
+ add_custom_command(TARGET ${dependency_target} POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E rm -rf "${plugin_path}"
+ COMMAND ${CMAKE_COMMAND} -E create_symlink "${source_plugin_path}" "${plugin_path}"
+ COMMENT "Symlinking VST3 plugin ${plugin_type_upper} plugin to ${plugin_path}"
+ VERBATIM)
+ elseif ("${plugin_type}" STREQUAL "au")
add_custom_command(TARGET ${dependency_target} POST_BUILD
- COMMAND ${CMAKE_COMMAND} -E rm -f ${plugin_path}
- COMMAND ${CMAKE_COMMAND} -E create_symlink "$/../../../${target_file_name}" ${plugin_path}
- COMMENT "Copying ${plugin_type_upper} plugin to ${plugin_path}"
+ COMMAND ${CMAKE_COMMAND} -E rm -rf "${plugin_path}"
+ COMMAND ${CMAKE_COMMAND} -E copy_directory "$" "${plugin_path}"
+ COMMENT "Copying AU plugin ${plugin_type_upper} to ${plugin_path}"
VERBATIM)
else()
_yup_message (FATAL_ERROR "Unsupported plugin type ${plugin_type} for copying bundle")
diff --git a/cmake/yup_codesign.cmake b/cmake/yup_codesign.cmake
new file mode 100644
index 000000000..efb509e3d
--- /dev/null
+++ b/cmake/yup_codesign.cmake
@@ -0,0 +1,31 @@
+# ==============================================================================
+#
+# This file is part of the YUP library.
+# Copyright (c) 2026 - kunitoki@gmail.com
+#
+# YUP is an open source library subject to open-source licensing.
+#
+# The code included in this file is provided under the terms of the ISC license
+# http://www.isc.org/downloads/software-support-policy/isc-license. Permission
+# To use, copy, modify, and/or distribute this software for any purpose with or
+# without fee is hereby granted provided that the above copyright notice and
+# this permission notice appear in all copies.
+#
+# YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
+# EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
+# DISCLAIMED.
+#
+# ==============================================================================
+
+# ==============================================================================
+
+function (yup_codesign_target target_name bundle_path)
+ if (NOT YUP_PLATFORM_MAC)
+ return()
+ endif()
+
+ add_custom_command (TARGET ${target_name} POST_BUILD
+ COMMAND codesign --force --sign - "${bundle_path}"
+ COMMENT "Codesigning ${target_name}"
+ VERBATIM)
+endfunction()
diff --git a/cmake/yup_dependencies.cmake b/cmake/yup_dependencies.cmake
index c2e74f493..14e4a2c80 100644
--- a/cmake/yup_dependencies.cmake
+++ b/cmake/yup_dependencies.cmake
@@ -77,6 +77,45 @@ endfunction()
#==============================================================================
+function (_yup_fetch_apple_ausdk)
+ if (NOT TARGET base-sdk-auv2)
+ if (NOT AUDIOUNIT_SDK_ROOT)
+ _yup_message (STATUS "Fetching Apple AudioUnitSDK")
+ _yup_fetchcontent_declare (AudioUnitSDK
+ GIT_REPOSITORY https://github.com/apple/AudioUnitSDK.git
+ GIT_TAG AudioUnitSDK-1.1.0)
+ FetchContent_MakeAvailable (AudioUnitSDK)
+ set (AUDIOUNIT_SDK_ROOT "${audiounitsdk_SOURCE_DIR}")
+ endif()
+
+ set (AUSDK_SRC "${AUDIOUNIT_SDK_ROOT}/src/AudioUnitSDK")
+
+ add_library (base-sdk-auv2 STATIC
+ "${AUSDK_SRC}/AUBase.cpp"
+ "${AUSDK_SRC}/AUBuffer.cpp"
+ "${AUSDK_SRC}/AUBufferAllocator.cpp"
+ "${AUSDK_SRC}/AUEffectBase.cpp"
+ "${AUSDK_SRC}/AUInputElement.cpp"
+ "${AUSDK_SRC}/AUMIDIBase.cpp"
+ "${AUSDK_SRC}/AUMIDIEffectBase.cpp"
+ "${AUSDK_SRC}/AUOutputElement.cpp"
+ "${AUSDK_SRC}/AUPlugInDispatch.cpp"
+ "${AUSDK_SRC}/AUScopeElement.cpp"
+ "${AUSDK_SRC}/ComponentBase.cpp"
+ "${AUSDK_SRC}/MusicDeviceBase.cpp")
+
+ target_include_directories (base-sdk-auv2 PUBLIC "${AUDIOUNIT_SDK_ROOT}/include")
+ target_compile_features (base-sdk-auv2 PUBLIC cxx_std_17)
+ target_compile_options (base-sdk-auv2 PRIVATE -Wno-deprecated-declarations)
+
+ set_target_properties (base-sdk-auv2 PROPERTIES
+ POSITION_INDEPENDENT_CODE ON
+ FOLDER "Thirdparty")
+ endif()
+endfunction()
+
+#==============================================================================
+
function (_yup_fetch_clap)
if (NOT TARGET clap)
_yup_message (STATUS "Fetching CLAP SDK")
diff --git a/cmake/yup_modules.cmake b/cmake/yup_modules.cmake
index c9be8ac3f..737d74629 100644
--- a/cmake/yup_modules.cmake
+++ b/cmake/yup_modules.cmake
@@ -509,7 +509,7 @@ function (_yup_module_setup_plugin_client target_name plugin_client_target folde
endif()
set (options "")
- set (one_value_args PLUGIN_ID PLUGIN_NAME PLUGIN_VENDOR PLUGIN_VERSION PLUGIN_DESCRIPTION PLUGIN_URL PLUGIN_EMAIL PLUGIN_IS_SYNTH PLUGIN_IS_MONO)
+ set (one_value_args PLUGIN_ID PLUGIN_NAME PLUGIN_VENDOR PLUGIN_VERSION PLUGIN_DESCRIPTION PLUGIN_URL PLUGIN_EMAIL PLUGIN_IS_SYNTH PLUGIN_IS_MONO PLUGIN_AU_SUBTYPE PLUGIN_AU_MANUFACTURER)
set (multi_value_args "")
cmake_parse_arguments (YUP_ARG "${options}" "${one_value_args}" "${multi_value_args}" ${ARGN})
@@ -523,8 +523,11 @@ function (_yup_module_setup_plugin_client target_name plugin_client_target folde
elseif (plugin_type STREQUAL "standalone")
set (custom_target_name "${target_name}_standalone")
set (plugin_define "YUP_AUDIO_PLUGIN_ENABLE_STANDALONE=1")
+ elseif (plugin_type STREQUAL "au")
+ set (custom_target_name "${target_name}_au")
+ set (plugin_define "YUP_AUDIO_PLUGIN_ENABLE_AU=1")
else()
- _yup_message (FATAL_ERROR "Invalid plugin type: ${plugin_type}. Must be either 'vst3', 'clap' or 'standalone'")
+ _yup_message (FATAL_ERROR "Invalid plugin type: ${plugin_type}. Must be either 'vst3', 'clap', 'au' or 'standalone'")
endif()
add_library (${custom_target_name} INTERFACE)
@@ -562,6 +565,14 @@ function (_yup_module_setup_plugin_client target_name plugin_client_target folde
list (APPEND module_defines YupPlugin_IsMono=0)
endif()
+ if (YUP_ARG_PLUGIN_AU_SUBTYPE)
+ list (APPEND module_defines "YupPlugin_AUSubType=\"${YUP_ARG_PLUGIN_AU_SUBTYPE}\"")
+ endif()
+
+ if (YUP_ARG_PLUGIN_AU_MANUFACTURER)
+ list (APPEND module_defines "YupPlugin_AUManufacturer=\"${YUP_ARG_PLUGIN_AU_MANUFACTURER}\"")
+ endif()
+
if (YUP_PLATFORM_APPLE)
_yup_glob_recurse ("${module_path}/${plugin_type}/*.mm" module_sources)
else()
diff --git a/cmake/yup_pluginval.cmake b/cmake/yup_pluginval.cmake
new file mode 100644
index 000000000..6657909ab
--- /dev/null
+++ b/cmake/yup_pluginval.cmake
@@ -0,0 +1,246 @@
+# ==============================================================================
+#
+# This file is part of the YUP library.
+# Copyright (c) 2025 - kunitoki@gmail.com
+#
+# YUP is an open source library subject to open-source licensing.
+#
+# The code included in this file is provided under the terms of the ISC license
+# http://www.isc.org/downloads/software-support-policy/isc-license. Permission
+# To use, copy, modify, and/or distribute this software for any purpose with or
+# without fee is hereby granted provided that the above copyright notice and
+# this permission notice appear in all copies.
+#
+# YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
+# EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
+# DISCLAIMED.
+#
+# ==============================================================================
+
+# ==============================================================================
+
+set (PLUGINVAL_VERSION "v1.0.4")
+set (CLAP_VALIDATOR_VERSION "0.3.2")
+
+# ==============================================================================
+
+function (_yup_setup_validation_tool tool_name tool_version tool_platform tool_archive tool_executable tool_url executable_cache_variable)
+ if (NOT YUP_PLATFORM_DESKTOP)
+ _yup_message (WARNING "${tool_name} is only supported on desktop platforms")
+ return()
+ endif()
+
+ set (tool_dir "${CMAKE_BINARY_DIR}/${tool_name}")
+ set (tool_archive_path "${tool_dir}/${tool_archive}")
+ set (tool_executable_path "${tool_dir}/${tool_executable}")
+
+ file (MAKE_DIRECTORY "${tool_dir}")
+
+ if (NOT EXISTS "${tool_executable_path}")
+ _yup_message (STATUS "Downloading ${tool_name} ${tool_version} for ${tool_platform}")
+
+ file (DOWNLOAD "${tool_url}" "${tool_archive_path}"
+ SHOW_PROGRESS
+ STATUS download_status)
+
+ list (GET download_status 0 download_error)
+ if (NOT download_error EQUAL 0)
+ list (GET download_status 1 download_error_message)
+ _yup_message (FATAL_ERROR "Failed to download ${tool_name}: ${download_error_message}")
+ endif()
+
+ _yup_message (STATUS "Extracting ${tool_name} archive")
+ execute_process(
+ COMMAND ${CMAKE_COMMAND} -E tar xzf "${tool_archive_path}"
+ WORKING_DIRECTORY "${tool_dir}"
+ RESULT_VARIABLE extract_result)
+
+ if (NOT extract_result EQUAL 0)
+ _yup_message (FATAL_ERROR "Failed to extract ${tool_name} archive")
+ endif()
+
+ if (YUP_PLATFORM_POSIX)
+ execute_process(
+ COMMAND chmod +x "${tool_executable_path}"
+ RESULT_VARIABLE chmod_result)
+
+ if (NOT chmod_result EQUAL 0)
+ _yup_message (WARNING "Failed to make ${tool_name} executable")
+ endif()
+ endif()
+
+ file (REMOVE "${tool_archive_path}")
+ endif()
+
+ if (NOT EXISTS "${tool_executable_path}")
+ _yup_message (FATAL_ERROR "${tool_name} executable not found at: ${tool_executable_path}")
+ endif()
+
+ set (${executable_cache_variable} "${tool_executable_path}" CACHE INTERNAL "Path to ${tool_name} executable")
+
+ _yup_message (STATUS "${tool_name} is available at: ${tool_executable_path}")
+endfunction()
+
+# ==============================================================================
+
+function (yup_setup_pluginval)
+ if (NOT YUP_ENABLE_PLUGINVAL)
+ return()
+ endif()
+
+ # Determine platform-specific download URL and executable name
+ if (YUP_PLATFORM_WINDOWS)
+ if (CMAKE_SIZEOF_VOID_P EQUAL 8)
+ set (PLUGINVAL_PLATFORM "Windows")
+ set (PLUGINVAL_ARCHIVE "pluginval_Windows.zip")
+ else()
+ _yup_message (WARNING "pluginval does not support 32-bit Windows")
+ return()
+ endif()
+ set (PLUGINVAL_EXECUTABLE "pluginval.exe")
+ elseif (YUP_PLATFORM_MAC)
+ set (PLUGINVAL_PLATFORM "macOS")
+ set (PLUGINVAL_ARCHIVE "pluginval_macOS.zip")
+ set (PLUGINVAL_EXECUTABLE "pluginval.app/Contents/MacOS/pluginval")
+ elseif (YUP_PLATFORM_LINUX)
+ set (PLUGINVAL_PLATFORM "Linux")
+ set (PLUGINVAL_ARCHIVE "pluginval_Linux.zip")
+ set (PLUGINVAL_EXECUTABLE "pluginval")
+ else()
+ _yup_message (WARNING "Unsupported platform for pluginval")
+ return()
+ endif()
+
+ # Set up download URL
+ set (PLUGINVAL_URL "https://github.com/Tracktion/pluginval/releases/download/${PLUGINVAL_VERSION}/${PLUGINVAL_ARCHIVE}")
+
+ _yup_setup_validation_tool (
+ pluginval
+ ${PLUGINVAL_VERSION}
+ ${PLUGINVAL_PLATFORM}
+ ${PLUGINVAL_ARCHIVE}
+ ${PLUGINVAL_EXECUTABLE}
+ ${PLUGINVAL_URL}
+ PLUGINVAL_EXECUTABLE)
+endfunction()
+
+# ==============================================================================
+
+function (yup_setup_clap_validator)
+ if (NOT YUP_ENABLE_CLAP_VALIDATOR)
+ return()
+ endif()
+
+ if (YUP_PLATFORM_WINDOWS)
+ if (CMAKE_SIZEOF_VOID_P EQUAL 8)
+ set (CLAP_VALIDATOR_PLATFORM "Windows")
+ set (CLAP_VALIDATOR_ARCHIVE "clap-validator-${CLAP_VALIDATOR_VERSION}-windows.zip")
+ else()
+ _yup_message (WARNING "clap-validator does not support 32-bit Windows")
+ return()
+ endif()
+ set (CLAP_VALIDATOR_EXECUTABLE "clap-validator.exe")
+ elseif (YUP_PLATFORM_MAC)
+ set (CLAP_VALIDATOR_PLATFORM "macOS")
+ set (CLAP_VALIDATOR_ARCHIVE "clap-validator-${CLAP_VALIDATOR_VERSION}-macos-universal.tar.gz")
+ set (CLAP_VALIDATOR_EXECUTABLE "binaries/clap-validator")
+ elseif (YUP_PLATFORM_LINUX)
+ set (CLAP_VALIDATOR_PLATFORM "Linux")
+ set (CLAP_VALIDATOR_ARCHIVE "clap-validator-${CLAP_VALIDATOR_VERSION}-ubuntu-18.04.tar.gz")
+ set (CLAP_VALIDATOR_EXECUTABLE "clap-validator")
+ else()
+ _yup_message (WARNING "Unsupported platform for clap-validator")
+ return()
+ endif()
+
+ set (CLAP_VALIDATOR_URL "https://github.com/free-audio/clap-validator/releases/download/${CLAP_VALIDATOR_VERSION}/${CLAP_VALIDATOR_ARCHIVE}")
+
+ _yup_setup_validation_tool (
+ clap-validator
+ ${CLAP_VALIDATOR_VERSION}
+ ${CLAP_VALIDATOR_PLATFORM}
+ ${CLAP_VALIDATOR_ARCHIVE}
+ ${CLAP_VALIDATOR_EXECUTABLE}
+ ${CLAP_VALIDATOR_URL}
+ CLAP_VALIDATOR_EXECUTABLE)
+endfunction()
+
+# ==============================================================================
+
+function (yup_validate_pluginval target_name plugin_path)
+ if (NOT YUP_ENABLE_PLUGINVAL OR NOT PLUGINVAL_EXECUTABLE)
+ return()
+ endif()
+
+ add_custom_command(
+ TARGET ${target_name} POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E echo "[PLUGINVAL] Starting validation of ${target_name}..."
+ COMMAND
+ "${PLUGINVAL_EXECUTABLE}"
+ --strictness-level 5
+ --validate-in-process
+ --skip-gui-tests
+ --validate "${plugin_path}"
+ COMMAND ${CMAKE_COMMAND} -E echo "[PLUGINVAL] Validation of ${target_name} completed"
+ COMMENT "Running pluginval validation on ${target_name}"
+ VERBATIM)
+endfunction()
+
+# ==============================================================================
+
+function (yup_validate_smtg_vst3_plugin target_name plugin_path)
+ if (NOT YUP_ENABLE_VST3_VALIDATOR)
+ return()
+ endif()
+
+ add_custom_command(
+ TARGET ${target_name} POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E echo "[SMTG] Validator started..."
+ COMMAND
+ $
+ "${plugin_path}"
+ WORKING_DIRECTORY "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}"
+ COMMAND ${CMAKE_COMMAND} -E echo "[SMTG] Validator finished."
+ COMMENT "Running SMTG VST3 validation on ${target_name}"
+ VERBATIM)
+endfunction()
+
+# ==============================================================================
+
+function (yup_validate_clap_plugin target_name plugin_path)
+ if (NOT YUP_ENABLE_CLAP_VALIDATOR OR NOT CLAP_VALIDATOR_EXECUTABLE)
+ return()
+ endif()
+
+ add_custom_command(
+ TARGET ${target_name} POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E echo "[CLAP-VALIDATOR] Starting validation of ${target_name}..."
+ COMMAND "${CLAP_VALIDATOR_EXECUTABLE}" validate "${plugin_path}"
+ COMMAND ${CMAKE_COMMAND} -E echo "[CLAP-VALIDATOR] Validation of ${target_name} completed"
+ COMMENT "Running clap-validator validation on ${target_name}"
+ VERBATIM)
+endfunction()
+
+# ==============================================================================
+
+function (yup_validate_au_plugin target_name plugin_name au_type au_subtype au_manufacturer)
+ if (NOT YUP_ENABLE_AUVAL_VALIDATOR OR NOT YUP_PLATFORM_MAC)
+ return()
+ endif()
+
+ find_program (AUVAL_EXECUTABLE auval)
+
+ if (AUVAL_EXECUTABLE)
+ add_custom_command (TARGET ${target_name} POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E echo "[AUVAL] Validating ${plugin_name}..."
+ COMMAND "${AUVAL_EXECUTABLE}" -strict -v
+ "${au_type}"
+ "${au_subtype}"
+ "${au_manufacturer}"
+ COMMAND ${CMAKE_COMMAND} -E echo "[AUVAL] Validation of ${plugin_name} completed"
+ COMMENT "Running auval validation on ${target_name}"
+ VERBATIM)
+ else()
+ _yup_message (WARNING "auval not found; skipping AU validation")
+ endif()
+endfunction()
diff --git a/cmake/yup_standalone.cmake b/cmake/yup_standalone.cmake
index 053928a7b..f43f8df83 100644
--- a/cmake/yup_standalone.cmake
+++ b/cmake/yup_standalone.cmake
@@ -128,13 +128,20 @@ function (yup_standalone_app)
add_executable (${target_name} ${executable_options})
endif()
+ set_target_properties (${target_name} PROPERTIES
+ C_VISIBILITY_PRESET hidden
+ CXX_VISIBILITY_PRESET hidden
+ OBJC_VISIBILITY_PRESET hidden
+ OBJCXX_VISIBILITY_PRESET hidden
+ VISIBILITY_INLINES_HIDDEN ON)
+
target_compile_features (${target_name} PRIVATE cxx_std_${target_cxx_standard})
target_include_directories (${target_name} PRIVATE ${module_include_dirs})
# ==== Per platform configuration
if (YUP_PLATFORM_APPLE)
if (NOT "${target_console}" AND NOT "${target_wheel}")
- _yup_set_default (YUP_ARG_CUSTOM_PLIST "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/platforms/${YUP_PLATFORM}/Info.plist")
+ _yup_set_default (YUP_ARG_CUSTOM_PLIST "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/platforms/${YUP_PLATFORM}/ApplicationInfo.plist")
_yup_valid_identifier_string ("${target_app_identifier}" target_app_identifier)
_yup_message (STATUS "${target_name} - Converting application input icon to apple .icns format")
diff --git a/examples/audiograph/source/AudioGraphApp.cpp b/examples/audiograph/source/AudioGraphApp.cpp
index 14f0efdae..35f31717f 100644
--- a/examples/audiograph/source/AudioGraphApp.cpp
+++ b/examples/audiograph/source/AudioGraphApp.cpp
@@ -50,6 +50,13 @@ AudioGraphApp::AudioGraphApp()
{
deviceManager.initialiseWithDefaultDevices (2, 2);
+ if (auto defaultMidiIn = yup::MidiInput::getDefaultDevice();
+ defaultMidiIn != yup::MidiDeviceInfo())
+ {
+ deviceManager.setMidiInputDeviceEnabled (defaultMidiIn.identifier, true);
+ deviceManager.addMidiInputDeviceCallback (defaultMidiIn.identifier, &midiCollector);
+ }
+
model = std::make_shared();
graph = std::make_shared (model);
nodeRegistry.registerInternalNodes();
@@ -89,6 +96,13 @@ AudioGraphApp::~AudioGraphApp()
scanLifetime->store (false);
#endif
+ if (auto defaultMidiIn = yup::MidiInput::getDefaultDevice();
+ defaultMidiIn != yup::MidiDeviceInfo())
+ {
+ deviceManager.removeMidiInputDeviceCallback (defaultMidiIn.identifier, &midiCollector);
+ deviceManager.setMidiInputDeviceEnabled (defaultMidiIn.identifier, false);
+ }
+
closePluginEditor();
closeAllSubgraphEditors();
@@ -164,8 +178,11 @@ void AudioGraphApp::audioDeviceIOCallbackWithContext (const float* const* inputC
yup::FloatVectorOperations::copy (outputChannelData[ch], inputChannelData[ch], numSamples);
}
- yup::MidiBuffer midi;
- graph->processBlock (outputBuffer, midi);
+ midiCollector.removeNextBlockOfMessages (midiBuffer, numSamples);
+
+ yup::ParameterChangeBuffer emptyParams;
+ yup::AudioProcessContext ctx { outputBuffer, midiBuffer, emptyParams };
+ graph->processBlock (ctx);
}
void AudioGraphApp::audioDeviceAboutToStart (yup::AudioIODevice* device)
@@ -173,6 +190,11 @@ void AudioGraphApp::audioDeviceAboutToStart (yup::AudioIODevice* device)
if (graph == nullptr || device == nullptr)
return;
+ const auto sampleRate = device->getCurrentSampleRate();
+ midiCollector.reset (sampleRate);
+
+ midiBuffer.ensureSize (4096);
+
#if YUP_DESKTOP
yup::AudioPluginHostContext ctx;
ctx.sampleRate = static_cast (device->getCurrentSampleRate());
@@ -424,13 +446,13 @@ struct SubgraphEditorRecord
std::unique_ptr AudioGraphApp::createMainPanel()
{
AudioGraphEditorPanel::EndpointViews endpointViews;
- endpointViews.createInputView = []
+ endpointViews.createInputView = [this]
{
- return std::make_unique();
+ return std::make_unique (graph, "sound card");
};
- endpointViews.createOutputView = []
+ endpointViews.createOutputView = [this]
{
- return std::make_unique();
+ return std::make_unique (graph, "sound card");
};
auto panel = std::make_unique (graph, nodeRegistry, std::move (endpointViews));
diff --git a/examples/audiograph/source/AudioGraphApp.h b/examples/audiograph/source/AudioGraphApp.h
index 8878c4a60..e96f194b0 100644
--- a/examples/audiograph/source/AudioGraphApp.h
+++ b/examples/audiograph/source/AudioGraphApp.h
@@ -132,6 +132,9 @@ class AudioGraphApp final
NodeRegistry nodeRegistry;
std::unique_ptr editorPanel;
+ yup::MidiMessageCollector midiCollector;
+ yup::MidiBuffer midiBuffer;
+
yup::File currentFilePath;
yup::FileChooser::Ptr fileChooser;
diff --git a/examples/audiograph/source/nodes/GainNode.h b/examples/audiograph/source/nodes/GainNode.h
index f292456f0..e62982277 100644
--- a/examples/audiograph/source/nodes/GainNode.h
+++ b/examples/audiograph/source/nodes/GainNode.h
@@ -40,9 +40,9 @@ class GainProcessor final : public yup::AudioProcessor
void releaseResources() override {}
- void processBlock (yup::AudioBuffer& audioBuffer, yup::MidiBuffer&) override
+ void processBlock (yup::AudioProcessContext& context) override
{
- audioBuffer.applyGain (gain.load (std::memory_order_relaxed));
+ context.audio.applyGain (gain.load (std::memory_order_relaxed));
}
int getCurrentPreset() const noexcept override { return 0; }
diff --git a/examples/audiograph/source/nodes/LatencyNode.h b/examples/audiograph/source/nodes/LatencyNode.h
index 1b25d20b8..4b9b58b86 100644
--- a/examples/audiograph/source/nodes/LatencyNode.h
+++ b/examples/audiograph/source/nodes/LatencyNode.h
@@ -57,8 +57,9 @@ class LatencyProcessor final : public yup::AudioProcessor
writePosition = 0;
}
- void processBlock (yup::AudioBuffer& audioBuffer, yup::MidiBuffer&) override
+ void processBlock (yup::AudioProcessContext& context) override
{
+ auto& audioBuffer = context.audio;
const int currentDelaySamples = getLatencySamples();
if (currentDelaySamples <= 0)
return;
diff --git a/examples/audiograph/source/nodes/LowPassFilterNode.h b/examples/audiograph/source/nodes/LowPassFilterNode.h
index 6018acbed..14e0d9331 100644
--- a/examples/audiograph/source/nodes/LowPassFilterNode.h
+++ b/examples/audiograph/source/nodes/LowPassFilterNode.h
@@ -46,8 +46,9 @@ class LowPassFilterProcessor final : public yup::AudioProcessor
void releaseResources() override {}
- void processBlock (yup::AudioBuffer& audioBuffer, yup::MidiBuffer&) override
+ void processBlock (yup::AudioProcessContext& context) override
{
+ auto& audioBuffer = context.audio;
const auto currentCutoff = static_cast (cutoff.load (std::memory_order_relaxed));
const auto alpha = static_cast (1.0 - std::exp (-yup::MathConstants::twoPi * currentCutoff / static_cast (sampleRate)));
diff --git a/examples/audiograph/source/nodes/OscillatorNode.h b/examples/audiograph/source/nodes/OscillatorNode.h
index e494c34ad..d48c2b2fe 100644
--- a/examples/audiograph/source/nodes/OscillatorNode.h
+++ b/examples/audiograph/source/nodes/OscillatorNode.h
@@ -45,8 +45,9 @@ class OscillatorProcessor final : public yup::AudioProcessor
void releaseResources() override {}
- void processBlock (yup::AudioBuffer& audioBuffer, yup::MidiBuffer&) override
+ void processBlock (yup::AudioProcessContext& context) override
{
+ auto& audioBuffer = context.audio;
const auto currentFrequency = static_cast (frequency.load (std::memory_order_relaxed));
const auto increment = yup::MathConstants::twoPi * currentFrequency / static_cast (sampleRate);
diff --git a/examples/audiograph/source/nodes/PluginNodeView.h b/examples/audiograph/source/nodes/PluginNodeView.h
index 3ce9ea68d..2250d7f11 100644
--- a/examples/audiograph/source/nodes/PluginNodeView.h
+++ b/examples/audiograph/source/nodes/PluginNodeView.h
@@ -43,12 +43,22 @@ class PluginNodeView final : public yup::AudioGraphNodeView
int getNumInputPorts() const override
{
- return desc.numInputChannels > 0 ? 1 : 0;
+ int count = 0;
+ if (desc.numInputChannels > 0)
+ ++count;
+ if (desc.numMidiInputPorts > 0)
+ ++count;
+ return count;
}
int getNumOutputPorts() const override
{
- return desc.numOutputChannels > 0 ? 1 : 0;
+ int count = 0;
+ if (desc.numOutputChannels > 0)
+ ++count;
+ if (desc.numMidiOutputPorts > 0)
+ ++count;
+ return count;
}
int getPreferredWidth() const override
@@ -61,14 +71,20 @@ class PluginNodeView final : public yup::AudioGraphNodeView
return desc.isInstrument ? yup::Color (0xffe11d48) : yup::Color (0xff0891b2);
}
- PortInfo getInputPortInfo (int) const override
+ PortInfo getInputPortInfo (int portIndex) const override
{
- return { "audio", getPortKindColor (PortKind::audio), PortKind::audio };
+ if (desc.numInputChannels > 0 && portIndex == 0)
+ return { "audio", getPortKindColor (PortKind::audio), PortKind::audio };
+
+ return { "MIDI", getPortKindColor (PortKind::midi), PortKind::midi };
}
- PortInfo getOutputPortInfo (int) const override
+ PortInfo getOutputPortInfo (int portIndex) const override
{
- return { "audio", getPortKindColor (PortKind::audio), PortKind::audio };
+ if (desc.numOutputChannels > 0 && portIndex == 0)
+ return { "audio", getPortKindColor (PortKind::audio), PortKind::audio };
+
+ return { "MIDI", getPortKindColor (PortKind::midi), PortKind::midi };
}
int getNumParameterRows() const override { return 0; }
diff --git a/examples/audiograph/source/nodes/SamplePlayerNode.h b/examples/audiograph/source/nodes/SamplePlayerNode.h
index aa104993f..0ee28fa44 100644
--- a/examples/audiograph/source/nodes/SamplePlayerNode.h
+++ b/examples/audiograph/source/nodes/SamplePlayerNode.h
@@ -53,8 +53,9 @@ class SamplePlayerProcessor final : public yup::AudioProcessor
void releaseResources() override {}
- void processBlock (yup::AudioBuffer& audioBuffer, yup::MidiBuffer&) override
+ void processBlock (yup::AudioProcessContext& context) override
{
+ auto& audioBuffer = context.audio;
audioBuffer.clear();
const auto* sample = currentSample.load (std::memory_order_acquire);
diff --git a/examples/audiograph/source/nodes/SoundCardInputNodeView.h b/examples/audiograph/source/nodes/SoundCardInputNodeView.h
index 28780af97..959a6a97b 100644
--- a/examples/audiograph/source/nodes/SoundCardInputNodeView.h
+++ b/examples/audiograph/source/nodes/SoundCardInputNodeView.h
@@ -25,8 +25,10 @@
class SoundCardInputNodeView final : public yup::AudioGraphNodeView
{
public:
- explicit SoundCardInputNodeView (yup::StringRef subtitleIn = "sound card")
+ explicit SoundCardInputNodeView (std::shared_ptr graphIn,
+ yup::StringRef subtitleIn = "sound card")
: AudioGraphNodeView (yup::AudioGraphModel::getGraphInputNodeID())
+ , graph (std::move (graphIn))
, subtitle (subtitleIn)
{
}
@@ -37,17 +39,34 @@ class SoundCardInputNodeView final : public yup::AudioGraphNodeView
int getNumInputPorts() const override { return 0; }
- int getNumOutputPorts() const override { return 1; }
+ int getNumOutputPorts() const override
+ {
+ return graph != nullptr ? static_cast (graph->getBusLayout().getInputBuses().size()) : 1;
+ }
int getPreferredWidth() const override { return 150; }
yup::Color getNodeColor() const override { return yup::Color (0xfff97316); }
- PortInfo getOutputPortInfo (int) const override
+ PortInfo getOutputPortInfo (int busIndex) const override
{
- return { "audio", getPortKindColor (PortKind::audio), PortKind::audio };
+ if (graph == nullptr)
+ return { "audio", getPortKindColor (PortKind::audio), PortKind::audio };
+
+ return getPortInfo (graph->getBusLayout().getInputBuses(), busIndex);
}
private:
+ static PortInfo getPortInfo (yup::Span buses, int busIndex)
+ {
+ if (busIndex < 0 || busIndex >= static_cast (buses.size()))
+ return { "?", getPortKindColor (PortKind::audio), PortKind::audio };
+
+ const auto& bus = buses[static_cast (busIndex)];
+ const auto kind = bus.getType() == yup::AudioBus::Type::Audio ? PortKind::audio : PortKind::midi;
+ return { bus.getName(), getPortKindColor (kind), kind };
+ }
+
+ std::shared_ptr graph;
yup::String subtitle;
};
diff --git a/examples/audiograph/source/nodes/SoundCardOutputNodeView.h b/examples/audiograph/source/nodes/SoundCardOutputNodeView.h
index 48ec7c153..b288d3bf7 100644
--- a/examples/audiograph/source/nodes/SoundCardOutputNodeView.h
+++ b/examples/audiograph/source/nodes/SoundCardOutputNodeView.h
@@ -25,8 +25,10 @@
class SoundCardOutputNodeView final : public yup::AudioGraphNodeView
{
public:
- explicit SoundCardOutputNodeView (yup::StringRef subtitleIn = "sound card")
+ explicit SoundCardOutputNodeView (std::shared_ptr graphIn,
+ yup::StringRef subtitleIn = "sound card")
: AudioGraphNodeView (yup::AudioGraphModel::getGraphOutputNodeID())
+ , graph (std::move (graphIn))
, subtitle (subtitleIn)
{
}
@@ -35,7 +37,10 @@ class SoundCardOutputNodeView final : public yup::AudioGraphNodeView
yup::String getNodeSubtitle() const override { return subtitle; }
- int getNumInputPorts() const override { return 1; }
+ int getNumInputPorts() const override
+ {
+ return graph != nullptr ? static_cast (graph->getBusLayout().getOutputBuses().size()) : 1;
+ }
int getNumOutputPorts() const override { return 0; }
@@ -43,11 +48,25 @@ class SoundCardOutputNodeView final : public yup::AudioGraphNodeView
yup::Color getNodeColor() const override { return yup::Color (0xff06b6d4); }
- PortInfo getInputPortInfo (int) const override
+ PortInfo getInputPortInfo (int busIndex) const override
{
- return { "audio", getPortKindColor (PortKind::audio), PortKind::audio };
+ if (graph == nullptr)
+ return { "audio", getPortKindColor (PortKind::audio), PortKind::audio };
+
+ return getPortInfo (graph->getBusLayout().getOutputBuses(), busIndex);
}
private:
+ static PortInfo getPortInfo (yup::Span buses, int busIndex)
+ {
+ if (busIndex < 0 || busIndex >= static_cast (buses.size()))
+ return { "?", getPortKindColor (PortKind::audio), PortKind::audio };
+
+ const auto& bus = buses[static_cast (busIndex)];
+ const auto kind = bus.getType() == yup::AudioBus::Type::Audio ? PortKind::audio : PortKind::midi;
+ return { bus.getName(), getPortKindColor (kind), kind };
+ }
+
+ std::shared_ptr graph;
yup::String subtitle;
};
diff --git a/examples/audiograph/source/nodes/SubgraphNode.h b/examples/audiograph/source/nodes/SubgraphNode.h
index 1b60a7c0b..9c86ce67f 100644
--- a/examples/audiograph/source/nodes/SubgraphNode.h
+++ b/examples/audiograph/source/nodes/SubgraphNode.h
@@ -136,7 +136,6 @@ class SubgraphProcessor final : public yup::AudioProcessor
void prepareToPlay (float sampleRate, int maxBlockSize) override
{
- graph->setPlayHead (getPlayHead());
graph->prepareToPlay (sampleRate, maxBlockSize);
}
@@ -145,9 +144,9 @@ class SubgraphProcessor final : public yup::AudioProcessor
graph->releaseResources();
}
- void processBlock (yup::AudioBuffer& audioBuffer, yup::MidiBuffer& midiBuffer) override
+ void processBlock (yup::AudioProcessContext& context) override
{
- graph->processBlock (audioBuffer, midiBuffer);
+ graph->processBlock (context);
}
void flush() override
diff --git a/examples/plugin/CMakeLists.txt b/examples/plugin/CMakeLists.txt
index ba1eee55c..aafba5afd 100644
--- a/examples/plugin/CMakeLists.txt
+++ b/examples/plugin/CMakeLists.txt
@@ -32,8 +32,8 @@ yup_audio_plugin (
TARGET_APP_ID "org.yup.${target_name}"
TARGET_APP_NAMESPACE "org.yup"
TARGET_CXX_STANDARD 20
- PLUGIN_ID "org.yup.YupCLAP"
- PLUGIN_NAME "YupCLAPPZ"
+ PLUGIN_ID "org.yup.YupSynth"
+ PLUGIN_NAME "YupSynth"
PLUGIN_VENDOR "org.yup"
PLUGIN_EMAIL "kunitoki@gmail.com"
PLUGIN_VERSION "${target_version}"
@@ -43,6 +43,7 @@ yup_audio_plugin (
PLUGIN_IS_MONO OFF
PLUGIN_CREATE_CLAP ON
PLUGIN_CREATE_VST3 ON
+ PLUGIN_CREATE_AU ON
PLUGIN_CREATE_STANDALONE ON
MODULES
yup::yup_gui
diff --git a/examples/plugin/source/ExampleEditor.cpp b/examples/plugin/source/ExampleEditor.cpp
index 51f5238f0..406fa9380 100644
--- a/examples/plugin/source/ExampleEditor.cpp
+++ b/examples/plugin/source/ExampleEditor.cpp
@@ -45,9 +45,9 @@ ExampleEditor::ExampleEditor (ExamplePlugin& processor)
{
gainParameter->beginChangeGesture();
};
- x->onValueChanged = [this] (float value)
+ x->onValueChanged = [this] (double value)
{
- gainParameter->setValueNotifyingHost (value);
+ gainParameter->setValueNotifyingHost (static_cast (value));
};
x->onDragEnd = [this] (const yup::MouseEvent&)
{
diff --git a/examples/plugin/source/ExamplePlugin.cpp b/examples/plugin/source/ExamplePlugin.cpp
index 41b06aa25..8c691d15e 100644
--- a/examples/plugin/source/ExamplePlugin.cpp
+++ b/examples/plugin/source/ExamplePlugin.cpp
@@ -22,18 +22,82 @@
#include "ExamplePlugin.h"
#include "ExampleEditor.h"
+#include
+
+//==============================================================================
+
+namespace
+{
+
+constexpr char examplePluginStateMagic[] = { 'Y', 'U', 'P', 'S' };
+constexpr int examplePluginStateVersion = 1;
+
+const char* getPluginFormatName()
+{
+#if YUP_AUDIO_PLUGIN_ENABLE_AU
+ return "au";
+#elif YUP_AUDIO_PLUGIN_ENABLE_CLAP
+ return "clap";
+#elif YUP_AUDIO_PLUGIN_ENABLE_VST3
+ return "vst3";
+#elif YUP_AUDIO_PLUGIN_ENABLE_STANDALONE
+ return "standalone";
+#else
+ return "unknown";
+#endif
+}
+
+class ExamplePluginLogger final
+{
+public:
+ ExamplePluginLogger()
+ {
+ const auto logFileName = yup::String (YupPlugin_Name) + "_" + getPluginFormatName() + ".log";
+ logger.reset (new yup::FileLogger (yup::FileLogger::getSystemLogFileFolder().getChildFile (logFileName),
+ yup::String (YupPlugin_Name) + " " + getPluginFormatName() + " log"));
+
+ yup::Logger::setCurrentLogger (logger.get());
+ yup::Logger::writeToLog ("Logger initialised: " + logger->getLogFile().getFullPathName());
+ }
+
+ void setAsCurrentLogger()
+ {
+ yup::Logger::setCurrentLogger (logger.get());
+ }
+
+ ~ExamplePluginLogger()
+ {
+ if (yup::Logger::getCurrentLogger() == logger.get())
+ yup::Logger::setCurrentLogger (nullptr);
+ }
+
+private:
+ std::unique_ptr logger;
+};
+
+void initialiseExamplePluginLogger()
+{
+ static ExamplePluginLogger logger;
+ logger.setAsCurrentLogger();
+}
+
+} // namespace
+
//==============================================================================
ExamplePlugin::ExamplePlugin()
: yup::AudioProcessor ("MyPlugin",
yup::AudioBusLayout ({}, { yup::AudioBus ("main", yup::AudioBus::Audio, yup::AudioBus::Output, 2) }))
{
+ initialiseExamplePluginLogger();
+
addParameter (gainParameter = yup::AudioParameterBuilder()
.withID ("volume")
.withName ("Volume")
.withRange (0.0f, 1.0f)
.withDefault (0.5f)
.withSmoothing (20.0f)
+ .withModulatable (true)
.build());
}
@@ -56,8 +120,11 @@ void ExamplePlugin::releaseResources()
voices.free();
}
-void ExamplePlugin::processBlock (yup::AudioSampleBuffer& audioBuffer, yup::MidiBuffer& midiBuffer)
+void ExamplePlugin::processBlock (yup::AudioProcessContext& context)
{
+ auto& audioBuffer = context.audio;
+ auto& midiBuffer = context.midi;
+
int numSamples = audioBuffer.getNumSamples();
float* outputL = audioBuffer.getWritePointer (0);
float* outputR = audioBuffer.getWritePointer (1);
@@ -65,10 +132,12 @@ void ExamplePlugin::processBlock (yup::AudioSampleBuffer& audioBuffer, yup::Midi
int nextEventSample = midiBuffer.getNumEvents() ? 0 : numSamples;
auto midiIterator = midiBuffer.begin();
- gainHandle.updateNextAudioBlock();
+ gainHandle.prepareBlock (context.params, gainParameter->getIndexInContainer());
for (int currentSample = 0; currentSample < numSamples;)
{
+ gainHandle.advanceToSample (currentSample);
+
while (midiIterator != midiBuffer.end() && nextEventSample == currentSample)
{
const auto& event = *midiIterator;
@@ -122,7 +191,9 @@ void ExamplePlugin::processBlock (yup::AudioSampleBuffer& audioBuffer, yup::Midi
}
}
- // TODO - clap supports per voice modulations: clap_event_param_mod_t
+ // Per-voice (polyphonic) modulation is not yet supported; global modulation
+ // via CLAP_EVENT_PARAM_MOD is handled by the plugin wrapper and already
+ // reflected in the value returned by gainHandle.getNextValue().
}
if (midiIterator == midiBuffer.end())
@@ -188,7 +259,10 @@ void ExamplePlugin::setCurrentPreset (int index) noexcept
return;
if (yup::isPositiveAndBelow (index, yup::numElementsInArray (presets)))
+ {
+ currentPreset = index;
gainParameter->setValue (presets[index].gainValue);
+ }
}
int ExamplePlugin::getNumPresets() const
@@ -214,12 +288,51 @@ void ExamplePlugin::setPresetName (int index, yup::StringRef newName)
yup::Result ExamplePlugin::loadStateFromMemory (const yup::MemoryBlock& memoryBlock)
{
- return yup::Result::fail ("Not implemented");
+ constexpr size_t expectedSize = sizeof (examplePluginStateMagic) + (sizeof (int) * 2) + sizeof (float);
+
+ if (memoryBlock.getSize() != expectedSize)
+ return yup::Result::fail ("Invalid example plugin state size");
+
+ yup::MemoryInputStream stream (memoryBlock, false);
+
+ char magic[sizeof (examplePluginStateMagic)] {};
+ if (stream.read (magic, sizeof (magic)) != static_cast (sizeof (magic)))
+ return yup::Result::fail ("Invalid example plugin state header");
+
+ for (size_t i = 0; i < sizeof (examplePluginStateMagic); ++i)
+ if (magic[i] != examplePluginStateMagic[i])
+ return yup::Result::fail ("Invalid example plugin state header");
+
+ const auto version = stream.readInt();
+ if (version != examplePluginStateVersion)
+ return yup::Result::fail ("Unsupported example plugin state version");
+
+ const auto presetIndex = stream.readInt();
+ if (! yup::isPositiveAndBelow (presetIndex, yup::numElementsInArray (presets)))
+ return yup::Result::fail ("Invalid example plugin preset index");
+
+ const auto gainValue = stream.readFloat();
+ if (! (gainValue >= gainParameter->getMinimumValue() && gainValue <= gainParameter->getMaximumValue()))
+ return yup::Result::fail ("Invalid example plugin gain value");
+
+ currentPreset = presetIndex;
+ gainParameter->setValue (gainValue);
+
+ return yup::Result::ok();
}
yup::Result ExamplePlugin::saveStateIntoMemory (yup::MemoryBlock& memoryBlock)
{
- return yup::Result::fail ("Not implemented");
+ yup::MemoryOutputStream stream (memoryBlock, false);
+
+ if (! stream.write (examplePluginStateMagic, sizeof (examplePluginStateMagic))
+ || ! stream.writeInt (examplePluginStateVersion)
+ || ! stream.writeInt (currentPreset)
+ || ! stream.writeFloat (gainParameter->getValue()))
+ return yup::Result::fail ("Failed to write example plugin state");
+
+ stream.flush();
+ return yup::Result::ok();
}
//==============================================================================
diff --git a/examples/plugin/source/ExamplePlugin.h b/examples/plugin/source/ExamplePlugin.h
index 5463edd03..4c1887bcf 100644
--- a/examples/plugin/source/ExamplePlugin.h
+++ b/examples/plugin/source/ExamplePlugin.h
@@ -114,7 +114,7 @@ class ExamplePlugin : public yup::AudioProcessor
void prepareToPlay (float sampleRate, int maxBlockSize) override;
void releaseResources() override;
- void processBlock (yup::AudioSampleBuffer& audioBuffer, yup::MidiBuffer& midiBuffer) override;
+ void processBlock (yup::AudioProcessContext& context) override;
void flush() override;
int getCurrentPreset() const noexcept override;
diff --git a/modules/yup_audio_basics/audio_play_head/yup_AudioPlayHead.h b/modules/yup_audio_basics/audio_play_head/yup_AudioPlayHead.h
index 87e1d3c3a..6e8b8282e 100644
--- a/modules/yup_audio_basics/audio_play_head/yup_AudioPlayHead.h
+++ b/modules/yup_audio_basics/audio_play_head/yup_AudioPlayHead.h
@@ -45,10 +45,11 @@ namespace yup
A subclass of AudioPlayHead can supply information about the position and
status of a moving play head during audio playback.
- One of these can be supplied to an AudioProcessor object so that it can find
- out about the position of the audio that it is rendering.
-
- @see AudioProcessor::setPlayHead, AudioProcessor::getPlayHead
+ One of these can be supplied in an AudioProcessContext so that an
+ AudioProcessor can find out about the position of the audio that it is
+ rendering.
+
+ @see AudioProcessContext
@tags{Audio}
*/
diff --git a/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.cpp b/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.cpp
index 9b79604aa..aaafbb3c5 100644
--- a/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.cpp
+++ b/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.cpp
@@ -122,6 +122,26 @@ class AudioGraphProcessor::Pimpl final : private AudioProcessor::Listener
std::atomic& flag;
};
+ struct ScopedWorkerDrain
+ {
+ ScopedWorkerDrain (std::atomic& counterIn, bool enabledIn) noexcept
+ : counter (counterIn)
+ , enabled (enabledIn)
+ {
+ if (enabled)
+ counter.fetch_add (1, std::memory_order_acq_rel);
+ }
+
+ ~ScopedWorkerDrain()
+ {
+ if (enabled)
+ counter.fetch_sub (1, std::memory_order_acq_rel);
+ }
+
+ std::atomic& counter;
+ bool enabled;
+ };
+
struct ScopedProcessBlock
{
explicit ScopedProcessBlock (std::atomic& counterIn) noexcept
@@ -285,7 +305,6 @@ class AudioGraphProcessor::Pimpl final : private AudioProcessor::Listener
if (! isPrepared)
{
- node.processor->setPlayHead (owner.getPlayHead());
node.processor->setPlaybackConfiguration (sampleRate, maxBlockSize);
newlyPreparedNodes.push_back (node.processor);
}
@@ -410,11 +429,19 @@ class AudioGraphProcessor::Pimpl final : private AudioProcessor::Listener
if (details.latencyChanged)
{
latencyChangeCounter.fetch_add (1);
+
+ if (! commitInProgress.load())
+ {
+ ignoreUnused (commitChanges());
+ }
}
}
- void processBlock (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer)
+ void processBlock (AudioProcessContext& context)
{
+ auto& audioBuffer = context.audio;
+ auto& midiBuffer = context.midi;
+
ScopedNoDenormals noDenormals;
const ScopedProcessBlock scopedProcessBlock (activeProcessBlocks);
swapPendingPlan();
@@ -446,15 +473,16 @@ class AudioGraphProcessor::Pimpl final : private AudioProcessor::Listener
if (desiredWorkerThreads.load() > 0)
{
- processLevels (*graph, numSamples);
+ processLevels (*graph, numSamples, context.playHead);
}
else
{
for (const auto nodeIndex : graph->topologicalOrder)
- processNode (*graph, nodeIndex, numSamples);
+ processNode (*graph, nodeIndex, numSamples, context.playHead);
}
for (const auto connectionIndex : graph->graphOutputConnections)
+ {
routeConnection (*graph,
graph->connections[static_cast (connectionIndex)],
audioBuffer,
@@ -462,6 +490,7 @@ class AudioGraphProcessor::Pimpl final : private AudioProcessor::Listener
numSamples,
startSample,
startSample);
+ }
}
midiBuffer.clear();
@@ -875,7 +904,10 @@ class AudioGraphProcessor::Pimpl final : private AudioProcessor::Listener
graph.graphInputMidi.addEvents (midiBuffer, startSample, numSamples, -startSample);
}
- void processNode (CompiledGraph& graph, int nodeIndex, int numSamples)
+ void processNode (CompiledGraph& graph,
+ int nodeIndex,
+ int numSamples,
+ AudioPlayHead* playHead)
{
auto& node = graph.nodes[static_cast (nodeIndex)];
@@ -886,10 +918,12 @@ class AudioGraphProcessor::Pimpl final : private AudioProcessor::Listener
for (const auto connectionIndex : node.incomingConnections)
routeConnection (graph, graph.connections[static_cast (connectionIndex)], node.audioBuffer, node.midiBuffer, numSamples);
- node.processor->processBlock (node.audioBuffer, node.midiBuffer);
+ ParameterChangeBuffer emptyParams;
+ AudioProcessContext nodeCtx { node.audioBuffer, node.midiBuffer, emptyParams, playHead };
+ node.processor->processBlock (nodeCtx);
}
- void processLevels (CompiledGraph& graph, int numSamples)
+ void processLevels (CompiledGraph& graph, int numSamples, AudioPlayHead* playHead)
{
for (auto& level : graph.executionLevels)
{
@@ -901,6 +935,7 @@ class AudioGraphProcessor::Pimpl final : private AudioProcessor::Listener
activeGraph.store (&graph, std::memory_order_relaxed);
activeLevel.store (&level, std::memory_order_relaxed);
activeNumSamples.store (numSamples, std::memory_order_relaxed);
+ activePlayHead.store (playHead, std::memory_order_relaxed);
nextJobIndex.store (0, std::memory_order_relaxed);
remainingJobs.store (static_cast (level.size()), std::memory_order_release);
activeGeneration.store (generation, std::memory_order_release);
@@ -908,22 +943,30 @@ class AudioGraphProcessor::Pimpl final : private AudioProcessor::Listener
workerReadyEvent.reset();
workerReadyEvent.signal();
- drainActiveJobs (generation);
+ drainActiveJobs (generation, false);
while (remainingJobs.load (std::memory_order_acquire) > 0)
;
+ activeGeneration.store (0, std::memory_order_release);
+
+ while (activeWorkerDrains.load (std::memory_order_acquire) > 0)
+ ;
+
workerReadyEvent.reset();
}
activeGraph.store (nullptr, std::memory_order_relaxed);
activeLevel.store (nullptr, std::memory_order_relaxed);
activeNumSamples.store (0, std::memory_order_relaxed);
+ activePlayHead.store (nullptr, std::memory_order_relaxed);
activeGeneration.store (0, std::memory_order_release);
}
- void drainActiveJobs (int generation)
+ void drainActiveJobs (int generation, bool workerThread)
{
+ const ScopedWorkerDrain scopedWorkerDrain (activeWorkerDrains, workerThread);
+
if (activeGeneration.load (std::memory_order_acquire) != generation)
return;
@@ -934,15 +977,19 @@ class AudioGraphProcessor::Pimpl final : private AudioProcessor::Listener
return;
const int numSamples = activeNumSamples.load (std::memory_order_relaxed);
+ auto* const playHead = activePlayHead.load (std::memory_order_relaxed);
for (;;)
{
+ if (activeGeneration.load (std::memory_order_acquire) != generation)
+ break;
+
const int jobIndex = nextJobIndex.fetch_add (1);
if (jobIndex >= static_cast (level->size()))
break;
- processNode (*graph, (*level)[static_cast (jobIndex)], numSamples);
+ processNode (*graph, (*level)[static_cast (jobIndex)], numSamples, playHead);
remainingJobs.fetch_sub (1, std::memory_order_acq_rel);
}
@@ -1225,7 +1272,9 @@ class AudioGraphProcessor::Pimpl final : private AudioProcessor::Listener
std::atomic activeGraph { nullptr };
std::atomic*> activeLevel { nullptr };
std::atomic activeNumSamples { 0 };
+ std::atomic activePlayHead { nullptr };
std::atomic activeGeneration { 0 };
+ std::atomic activeWorkerDrains { 0 };
};
//==============================================================================
@@ -1260,15 +1309,17 @@ void AudioGraphProcessor::Pimpl::WorkerThread::run()
ScopedNoDenormals noDenormals;
owner.joinWorkgroup (workgroupToken);
- owner.drainActiveJobs (generation);
+ owner.drainActiveJobs (generation, true);
}
}
//==============================================================================
AudioBusLayout AudioGraphProcessor::createDefaultBusLayout()
{
- return AudioBusLayout ({ AudioBus ("Input", AudioBus::Type::Audio, AudioBus::Direction::Input, 2) },
- { AudioBus ("Output", AudioBus::Type::Audio, AudioBus::Direction::Output, 2) });
+ return AudioBusLayout ({ AudioBus ("Input", AudioBus::Type::Audio, AudioBus::Direction::Input, 2),
+ AudioBus ("MIDI Input", AudioBus::Type::MIDI, AudioBus::Direction::Input, 1) },
+ { AudioBus ("Output", AudioBus::Type::Audio, AudioBus::Direction::Output, 2),
+ AudioBus ("MIDI Output", AudioBus::Type::MIDI, AudioBus::Direction::Output, 1) });
}
AudioGraphProcessor::AudioGraphProcessor (std::shared_ptr model,
@@ -1346,9 +1397,9 @@ void AudioGraphProcessor::releaseResources()
pimpl->releaseResources();
}
-void AudioGraphProcessor::processBlock (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer)
+void AudioGraphProcessor::processBlock (AudioProcessContext& context)
{
- pimpl->processBlock (audioBuffer, midiBuffer);
+ pimpl->processBlock (context);
}
void AudioGraphProcessor::flush()
diff --git a/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.h b/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.h
index 5417b8ef1..ec812865d 100644
--- a/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.h
+++ b/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.h
@@ -29,11 +29,11 @@ namespace yup
Topology edits are made to a control-thread graph model. commitChanges()
validates the model, prepares newly compiled nodes for the current playback
configuration, and publishes an immutable processing plan. Child processor
- latency notifications mark the graph dirty; call commitChanges() from the
- control thread to rebuild delay compensation. Metadata edits such as node
- positions and properties are saved by the model without invalidating the
- compiled plan. processBlock() only swaps pending plans at block boundaries
- and keeps retired plans alive until a later control-thread commit or destruction.
+ latency notifications are handled by the graph as host notifications and
+ rebuild delay compensation. Metadata edits such as node positions and
+ properties are saved by the model without invalidating the compiled plan.
+ processBlock() only swaps pending plans at block boundaries and keeps retired
+ plans alive until a later control-thread commit or destruction.
*/
class YUP_API AudioGraphProcessor final : public AudioProcessor
{
@@ -90,7 +90,7 @@ class YUP_API AudioGraphProcessor final : public AudioProcessor
// AudioProcessor
void prepareToPlay (float sampleRate, int maxBlockSize) override;
void releaseResources() override;
- void processBlock (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) override;
+ void processBlock (AudioProcessContext& context) override;
void flush() override;
int getLatencySamples() override;
diff --git a/modules/yup_audio_plugin_client/au/yup_audio_plugin_client_AU.cpp b/modules/yup_audio_plugin_client/au/yup_audio_plugin_client_AU.cpp
new file mode 100644
index 000000000..5a314d242
--- /dev/null
+++ b/modules/yup_audio_plugin_client/au/yup_audio_plugin_client_AU.cpp
@@ -0,0 +1,1549 @@
+/*
+ ==============================================================================
+
+ This file is part of the YUP library.
+ Copyright (c) 2026 - kunitoki@gmail.com
+
+ YUP is an open source library subject to open-source licensing.
+
+ The code included in this file is provided under the terms of the ISC license
+ http://www.isc.org/downloads/software-support-policy/isc-license. Permission
+ to use, copy, modify, and/or distribute this software for any purpose with or
+ without fee is hereby granted provided that the above copyright notice and
+ this permission notice appear in all copies.
+
+ YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
+ EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
+ DISCLAIMED.
+
+ ==============================================================================
+*/
+
+#include "../yup_audio_plugin_client.h"
+
+#include "../common/yup_AudioPluginUtilities.h"
+
+#if ! defined(YUP_AUDIO_PLUGIN_ENABLE_AU)
+#error "YUP_AUDIO_PLUGIN_ENABLE_AU must be defined"
+#endif
+
+#if YUP_MAC
+#include
+#include
+#include
+#include
+#include
+
+#import
+#import
+#import
+#import
+#import
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+//==============================================================================
+
+extern "C" yup::AudioProcessor* createPluginProcessor();
+
+@class AudioPluginEditorViewAU;
+
+namespace yup
+{
+
+static String describeScopeAndElement (AudioUnitScope scope, AudioUnitElement element)
+{
+ return "scope=" + String (static_cast (scope)) + ", element=" + String (static_cast (element));
+}
+
+static String describePointer (const void* value)
+{
+ return "0x" + String::toHexString (static_cast (reinterpret_cast (value)));
+}
+
+static String describeStatus (OSStatus status)
+{
+ return String (static_cast (status));
+}
+
+//==============================================================================
+
+namespace
+{
+
+//==============================================================================
+
+static CFStringRef getProcessorStateKey()
+{
+ return CFSTR ("YUPProcessorState");
+}
+
+//==============================================================================
+
+struct AUScopedYupInitialiser
+{
+ AUScopedYupInitialiser()
+ {
+ if (numAUScopedInitInstances.fetch_add (1) == 0)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "initialising YUP GUI");
+ initialiseYup_GUI();
+ }
+ }
+
+ ~AUScopedYupInitialiser()
+ {
+ if (numAUScopedInitInstances.fetch_sub (1) == 1)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "shutting down YUP GUI");
+ shutdownYup_GUI();
+ }
+ }
+
+private:
+ static std::atomic_int numAUScopedInitInstances;
+};
+
+std::atomic_int AUScopedYupInitialiser::numAUScopedInitInstances = 0;
+
+struct AUScopedYupWindowingInitialiser
+{
+ AUScopedYupWindowingInitialiser()
+ {
+ if (numAUScopedInitInstances.fetch_add (1) == 0)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "initialising YUP windowing for editor");
+ initialiseYup_Windowing();
+ }
+ }
+
+ ~AUScopedYupWindowingInitialiser()
+ {
+ if (numAUScopedInitInstances.fetch_sub (1) == 1)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "shutting down YUP windowing for editor");
+ shutdownYup_Windowing();
+ }
+ }
+
+private:
+ static std::atomic_int numAUScopedInitInstances;
+};
+
+std::atomic_int AUScopedYupWindowingInitialiser::numAUScopedInitInstances = 0;
+
+//==============================================================================
+
+static OSType osTypeFromString (const char* s)
+{
+ if (s == nullptr || std::strlen (s) < 4)
+ return 0;
+
+ return static_cast (
+ (static_cast (static_cast (s[0])) << 24) | (static_cast (static_cast (s[1])) << 16) | (static_cast (static_cast (s[2])) << 8) | static_cast (static_cast (s[3])));
+}
+
+} // namespace
+
+//==============================================================================
+
+#if YupPlugin_IsSynth
+using AudioPluginAUBase = ausdk::MusicDeviceBase;
+#else
+using AudioPluginAUBase = ausdk::AUEffectBase;
+#endif
+
+//==============================================================================
+
+/**
+ AUv2 wrapper for a YUP AudioProcessor.
+
+ Supports both effects (AUEffectBase) and instruments (MusicDeviceBase)
+ depending on the YupPlugin_IsSynth compile-time setting.
+*/
+class AudioPluginProcessorAU final
+ : public AudioPluginAUBase
+ , private AudioParameter::Listener
+{
+public:
+ class AudioPluginPlayHeadAU final : public AudioPlayHead
+ {
+ public:
+ AudioPluginPlayHeadAU (AudioPluginProcessorAU& owner, const AudioTimeStamp* timeStamp)
+ : owner (owner)
+ , timeStamp (timeStamp)
+ {
+ }
+
+ bool canControlTransport() override
+ {
+ return false;
+ }
+
+ std::optional getPosition() const override
+ {
+ return owner.createPositionInfo (timeStamp);
+ }
+
+ private:
+ AudioPluginProcessorAU& owner;
+ const AudioTimeStamp* timeStamp = nullptr;
+ };
+
+ //==============================================================================
+
+ AudioPluginProcessorAU (AudioComponentInstance component)
+#if YupPlugin_IsSynth
+ : AudioPluginAUBase (component, 0, 1)
+ ,
+#else
+ : AudioPluginAUBase (component)
+ ,
+#endif
+ componentInstance (component)
+ {
+ processor.reset (::createPluginProcessor());
+
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "created processor instance: wrapper=" << yup::describePointer (this) << ", component=" << yup::describePointer (componentInstance) << ", processor=" << yup::describePointer (processor.get()));
+
+ if (processor == nullptr)
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "createPluginProcessor returned null");
+
+ addParameterListeners();
+ registerInstance (componentInstance, this);
+ }
+
+ ~AudioPluginProcessorAU() override
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "destroying processor instance: wrapper=" << yup::describePointer (this) << ", component=" << yup::describePointer (componentInstance) << ", processor=" << yup::describePointer (processor.get()));
+
+ closeEditorViews();
+ removeParameterListeners();
+ yup::endActiveParameterGestures (processor.get());
+
+ unregisterInstance (componentInstance);
+
+ processor.reset();
+ }
+
+ //==============================================================================
+
+ OSStatus Initialize() override
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "Initialize requested");
+
+ const auto result = AudioPluginAUBase::Initialize();
+ if (result != noErr)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "base Initialize failed: status=" << describeStatus (result));
+ return result;
+ }
+
+ if (processor == nullptr)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "Initialize failed: processor is null");
+ return kAudioUnitErr_FailedInitialization;
+ }
+
+ processor->setOfflineProcessing (renderingOffline);
+ processor->setPlaybackConfiguration (static_cast (getCurrentSampleRate()),
+ static_cast (GetMaxFramesPerSlice()));
+
+ midiBuffer.ensureSize (4096);
+ midiBuffer.clear();
+ emptyMidiBuffer.ensureSize (4096);
+ emptyMidiBuffer.clear();
+ paramChangeBuffer.reserve (getDefaultParameterChangeCapacity (*processor));
+ emptyParamChangeBuffer.reserve (getDefaultParameterChangeCapacity (*processor));
+ audioChannels.reserve (static_cast (getTotalAudioOutputChannels (*processor)));
+
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "Initialize completed: sampleRate=" << String (getCurrentSampleRate()) << ", maxFramesPerSlice=" << String (static_cast (GetMaxFramesPerSlice())));
+
+ return noErr;
+ }
+
+ void Cleanup() override
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "Cleanup requested: wrapper=" << yup::describePointer (this) << ", processor=" << yup::describePointer (processor.get()));
+
+ if (processor != nullptr)
+ processor->releaseResources();
+
+ AudioPluginAUBase::Cleanup();
+ }
+
+ //==============================================================================
+
+ OSStatus GetParameterList (AudioUnitScope inScope,
+ AudioUnitParameterID* outParameterList,
+ UInt32& outNumParameters) override
+ {
+ if (inScope != kAudioUnitScope_Global || processor == nullptr)
+ {
+ outNumParameters = 0;
+ return kAudioUnitErr_InvalidParameter;
+ }
+
+ const auto parameters = processor->getParameters();
+
+ if (outParameterList != nullptr)
+ {
+ for (size_t i = 0; i < parameters.size(); ++i)
+ outParameterList[i] = static_cast (parameters[i]->getHostParameterID());
+ }
+
+ outNumParameters = static_cast (parameters.size());
+ return noErr;
+ }
+
+ OSStatus GetParameterInfo (AudioUnitScope inScope,
+ AudioUnitParameterID inParameterID,
+ AudioUnitParameterInfo& outParameterInfo) override
+ {
+ if (inScope != kAudioUnitScope_Global || processor == nullptr)
+ return kAudioUnitErr_InvalidParameter;
+
+ const auto parameters = processor->getParameters();
+ const auto parameterIndex = processor->getParameterIndexByHostID (static_cast (inParameterID));
+ if (! isPositiveAndBelow (parameterIndex, static_cast (parameters.size())))
+ return kAudioUnitErr_InvalidParameter;
+
+ const auto& param = parameters[parameterIndex];
+
+ outParameterInfo.flags = kAudioUnitParameterFlag_IsReadable | kAudioUnitParameterFlag_HasCFNameString;
+
+ if (! param->isReadOnly())
+ outParameterInfo.flags |= kAudioUnitParameterFlag_IsWritable;
+
+ outParameterInfo.cfNameString = param->getName().toCFString();
+ param->getName().copyToUTF8 (outParameterInfo.name, sizeof (outParameterInfo.name));
+
+ outParameterInfo.unit = kAudioUnitParameterUnit_Generic;
+ outParameterInfo.minValue = param->getMinimumValue();
+ outParameterInfo.maxValue = param->getMaximumValue();
+ outParameterInfo.defaultValue = param->getDefaultValue();
+ outParameterInfo.clumpID = 0;
+
+ return noErr;
+ }
+
+ OSStatus GetParameter (AudioUnitParameterID inID,
+ AudioUnitScope inScope,
+ AudioUnitElement inElement,
+ AudioUnitParameterValue& outValue) override
+ {
+ if (inScope != kAudioUnitScope_Global || processor == nullptr)
+ return kAudioUnitErr_InvalidParameter;
+
+ const auto parameters = processor->getParameters();
+ const auto parameterIndex = processor->getParameterIndexByHostID (static_cast (inID));
+ if (! isPositiveAndBelow (parameterIndex, static_cast (parameters.size())))
+ return kAudioUnitErr_InvalidParameter;
+
+ outValue = static_cast (parameters[parameterIndex]->getValue());
+ return noErr;
+ }
+
+ OSStatus SetParameter (AudioUnitParameterID inID,
+ AudioUnitScope inScope,
+ AudioUnitElement inElement,
+ AudioUnitParameterValue inValue,
+ UInt32 inBufferOffsetInFrames) override
+ {
+ if (inScope != kAudioUnitScope_Global || processor == nullptr)
+ return kAudioUnitErr_InvalidParameter;
+
+ const auto parameters = processor->getParameters();
+ const auto parameterIndex = processor->getParameterIndexByHostID (static_cast (inID));
+ if (! isPositiveAndBelow (parameterIndex, static_cast (parameters.size())))
+ return kAudioUnitErr_InvalidParameter;
+
+ if (parameters[parameterIndex]->isReadOnly()
+ || parameters[parameterIndex]->isPerformingChangeGesture())
+ {
+ return noErr;
+ }
+
+ parameters[parameterIndex]->setValue (static_cast (inValue));
+
+ std::unique_lock lock (parameterChangeMutex, std::try_to_lock);
+ if (lock.owns_lock())
+ {
+ addParameterChangeByHostParameterID (*processor,
+ paramChangeBuffer,
+ static_cast (inID),
+ parameters[parameterIndex]->convertToNormalizedValue (static_cast (inValue)),
+ static_cast (inBufferOffsetInFrames));
+ }
+
+ return noErr;
+ }
+
+ //==============================================================================
+
+ UInt32 SupportedNumChannels (const AUChannelInfo** outInfo) override
+ {
+ if (processor == nullptr)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "SupportedNumChannels requested without processor");
+ return 0;
+ }
+
+ channelInfoCache.clear();
+
+ const auto& busLayout = processor->getBusLayout();
+
+ int inputChannels = 0;
+ for (const auto& bus : busLayout.getInputBuses())
+ if (bus.getType() == AudioBus::Type::Audio)
+ inputChannels = std::max (inputChannels, bus.getNumChannels());
+
+ int outputChannels = 0;
+ for (const auto& bus : busLayout.getOutputBuses())
+ if (bus.getType() == AudioBus::Type::Audio)
+ outputChannels = std::max (outputChannels, bus.getNumChannels());
+
+ if (inputChannels > 0 || outputChannels > 0)
+ {
+ AUChannelInfo info;
+ info.inChannels = static_cast (inputChannels);
+ info.outChannels = static_cast (outputChannels);
+ channelInfoCache.push_back (info);
+ }
+
+ if (outInfo != nullptr)
+ *outInfo = channelInfoCache.data();
+
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "SupportedNumChannels returned " << String (static_cast (channelInfoCache.size())) << " layouts");
+
+ return static_cast (channelInfoCache.size());
+ }
+
+ //==============================================================================
+
+ bool SupportsTail() override
+ {
+ return processor != nullptr && processor->getTailSamples() > 0;
+ }
+
+ Float64 GetTailTime() override
+ {
+ const auto sampleRate = getCurrentSampleRate();
+ if (processor == nullptr || sampleRate <= 0.0)
+ return 0.0;
+
+ return static_cast (processor->getTailSamples()) / sampleRate;
+ }
+
+ Float64 GetLatency() override
+ {
+ const auto sampleRate = getCurrentSampleRate();
+ if (processor == nullptr || sampleRate <= 0.0)
+ return 0.0;
+
+ return static_cast (processor->getLatencySamples()) / sampleRate;
+ }
+
+ //==============================================================================
+
+ std::optional createPositionInfo (const AudioTimeStamp* timeStamp)
+ {
+ AudioPlayHead::PositionInfo result;
+ bool hasPosition = false;
+
+ if (timeStamp != nullptr && (timeStamp->mFlags & kAudioTimeStampSampleTimeValid) != 0)
+ {
+ const auto timeInSamples = static_cast (timeStamp->mSampleTime);
+ result.setTimeInSamples (timeInSamples);
+
+ const auto sampleRate = getCurrentSampleRate();
+ if (sampleRate > 0.0)
+ result.setTimeInSeconds (static_cast (timeInSamples) / sampleRate);
+
+ hasPosition = true;
+ }
+
+ Float64 currentBeat = 0.0;
+ Float64 currentTempo = 0.0;
+ if (CallHostBeatAndTempo (¤tBeat, ¤tTempo) == noErr)
+ {
+ result.setPpqPosition (currentBeat);
+ result.setBpm (currentTempo);
+ hasPosition = true;
+ }
+
+ UInt32 deltaSamplesToNextBeat = 0;
+ Float32 timeSignatureNumerator = 0.0f;
+ UInt32 timeSignatureDenominator = 0;
+ Float64 currentMeasureDownBeat = 0.0;
+ if (CallHostMusicalTimeLocation (&deltaSamplesToNextBeat,
+ &timeSignatureNumerator,
+ &timeSignatureDenominator,
+ ¤tMeasureDownBeat)
+ == noErr)
+ {
+ ignoreUnused (deltaSamplesToNextBeat);
+ result.setTimeSignature (AudioPlayHead::TimeSignature {
+ static_cast (timeSignatureNumerator),
+ static_cast (timeSignatureDenominator) });
+ result.setPpqPositionOfLastBarStart (currentMeasureDownBeat);
+ hasPosition = true;
+ }
+
+ Boolean isPlaying = false;
+ Boolean transportStateChanged = false;
+ Float64 currentSampleInTimeline = 0.0;
+ Boolean isCycling = false;
+ Float64 cycleStartBeat = 0.0;
+ Float64 cycleEndBeat = 0.0;
+ if (CallHostTransportState (&isPlaying,
+ &transportStateChanged,
+ ¤tSampleInTimeline,
+ &isCycling,
+ &cycleStartBeat,
+ &cycleEndBeat)
+ == noErr)
+ {
+ ignoreUnused (transportStateChanged);
+ result.setIsPlaying (isPlaying);
+ result.setIsLooping (isCycling);
+
+ if (isCycling)
+ result.setLoopPoints (AudioPlayHead::LoopPoints { cycleStartBeat, cycleEndBeat });
+
+ result.setTimeInSamples (static_cast (currentSampleInTimeline));
+
+ const auto sampleRate = getCurrentSampleRate();
+ if (sampleRate > 0.0)
+ result.setTimeInSeconds (currentSampleInTimeline / sampleRate);
+
+ hasPosition = true;
+ }
+
+ return hasPosition ? std::make_optional (result) : std::nullopt;
+ }
+
+ //==============================================================================
+
+#if YupPlugin_IsSynth
+ // Instrument: render audio and drain the MIDI buffer
+ OSStatus RenderBus (AudioUnitRenderActionFlags& ioActionFlags,
+ const AudioTimeStamp& inTimeStamp,
+ UInt32 inBusNumber,
+ UInt32 inNumberFrames) override
+ {
+ if (processor == nullptr)
+ return kAudioUnitErr_NoConnection;
+
+ auto& outputBus = Output (inBusNumber);
+
+ outputBus.PrepareBuffer (inNumberFrames);
+ AudioBufferList& outBufList = outputBus.GetBufferList();
+
+ audioChannels.clear();
+ for (UInt32 ch = 0; ch < outBufList.mNumberBuffers; ++ch)
+ audioChannels.push_back (static_cast (outBufList.mBuffers[ch].mData));
+
+ AudioSampleBuffer audioBuffer (audioChannels.data(),
+ static_cast (audioChannels.size()),
+ 0,
+ static_cast (inNumberFrames));
+
+ {
+ AudioPluginPlayHeadAU playHead (*this, &inTimeStamp);
+ std::unique_lock lock (midiMutex, std::try_to_lock);
+ auto& processMidiBuffer = lock.owns_lock() ? midiBuffer : emptyMidiBuffer;
+ std::unique_lock parameterLock (parameterChangeMutex, std::try_to_lock);
+ auto& processParamChangeBuffer = parameterLock.owns_lock() ? paramChangeBuffer : emptyParamChangeBuffer;
+
+ AudioProcessContext context { audioBuffer,
+ processMidiBuffer,
+ processParamChangeBuffer,
+ &playHead };
+ processAudioBlock (*processor, context, isBypassed);
+
+ processMidiBuffer.clear();
+ processParamChangeBuffer.clear();
+ }
+
+ return noErr;
+ }
+
+ //==============================================================================
+
+ OSStatus HandleMIDIEvent (UInt8 status, UInt8 channel, UInt8 data1, UInt8 data2, UInt32 offsetSampleFrame) override
+ {
+ std::unique_lock lock (midiMutex, std::try_to_lock);
+ if (! lock.owns_lock())
+ return noErr;
+
+ const uint8_t rawData[3] = {
+ static_cast (status | channel),
+ data1,
+ data2
+ };
+
+ const int numBytes = MidiMessage::getMessageLengthFromFirstByte (rawData[0]);
+ midiBuffer.addEvent (rawData, numBytes, static_cast (offsetSampleFrame));
+
+ return noErr;
+ }
+
+ [[nodiscard]] bool CanScheduleParameters() const override
+ {
+ return false;
+ }
+
+ bool StreamFormatWritable (AudioUnitScope inScope, AudioUnitElement inElement) override
+ {
+ return inScope == kAudioUnitScope_Output && inElement == 0;
+ }
+
+ OSStatus HandleSysEx (const UInt8* inData, UInt32 inLength) override
+ {
+ std::unique_lock lock (midiMutex, std::try_to_lock);
+ if (! lock.owns_lock())
+ return noErr;
+
+ if (inData != nullptr && inLength > 0)
+ midiBuffer.addEvent (inData, static_cast (inLength), 0);
+
+ return noErr;
+ }
+
+#else
+ // Effect: copy input to output and call processBlock
+ OSStatus ProcessBufferLists (AudioUnitRenderActionFlags& ioActionFlags,
+ const AudioBufferList& inBuffer,
+ AudioBufferList& outBuffer,
+ UInt32 inFramesToProcess) override
+ {
+ if (processor == nullptr)
+ return kAudioUnitErr_NoConnection;
+
+ const UInt32 numBuffers = std::min (inBuffer.mNumberBuffers, outBuffer.mNumberBuffers);
+
+ audioChannels.clear();
+ for (UInt32 ch = 0; ch < numBuffers; ++ch)
+ {
+ const auto* in = static_cast (inBuffer.mBuffers[ch].mData);
+ auto* out = static_cast (outBuffer.mBuffers[ch].mData);
+
+ if (in != out)
+ std::memcpy (out, in, inFramesToProcess * sizeof (float));
+
+ audioChannels.push_back (out);
+ }
+
+ AudioSampleBuffer audioBuffer (audioChannels.data(),
+ static_cast (audioChannels.size()),
+ 0,
+ static_cast (inFramesToProcess));
+
+ AudioPluginPlayHeadAU playHead (*this, nullptr);
+ std::unique_lock parameterLock (parameterChangeMutex, std::try_to_lock);
+ auto& processParamChangeBuffer = parameterLock.owns_lock() ? paramChangeBuffer : emptyParamChangeBuffer;
+
+ AudioProcessContext context { audioBuffer,
+ midiBuffer,
+ processParamChangeBuffer,
+ &playHead };
+ processAudioBlock (*processor, context, isBypassed);
+ midiBuffer.clear();
+ processParamChangeBuffer.clear();
+
+ return noErr;
+ }
+#endif
+
+ //==============================================================================
+
+ OSStatus SaveState (CFPropertyListRef* outData) override
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "SaveState requested");
+
+ if (outData == nullptr)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "SaveState failed: outData is null");
+ return kAudioUnitErr_InvalidPropertyValue;
+ }
+
+ const auto baseResult = AudioPluginAUBase::SaveState (outData);
+ if (baseResult != noErr)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "SaveState failed: base status=" << describeStatus (baseResult));
+ return baseResult;
+ }
+
+ if (processor == nullptr)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "SaveState completed without processor state: processor is null");
+ return noErr;
+ }
+
+ MemoryBlock data;
+ const auto result = processor->saveStateIntoMemory (data);
+ if (result.failed())
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "SaveState completed without processor state: " << result.getErrorMessage());
+ return noErr;
+ }
+
+ if (*outData != nullptr && CFGetTypeID (*outData) == CFDictionaryGetTypeID())
+ {
+ NSData* nsData = data.getSize() > 0
+ ? [NSData dataWithBytes:data.getData() length:data.getSize()]
+ : [NSData data];
+
+ auto* stateDictionary = const_cast (static_cast (*outData));
+ CFDictionarySetValue (stateDictionary,
+ getProcessorStateKey(),
+ (__bridge CFDataRef) nsData);
+ }
+
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "SaveState completed with processor state: bytes=" << String (static_cast (data.getSize())));
+
+ return noErr;
+ }
+
+ OSStatus RestoreState (CFPropertyListRef inData) override
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "RestoreState requested");
+
+ if (inData == nullptr)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "RestoreState failed: inData is null");
+ return kAudioUnitErr_InvalidPropertyValue;
+ }
+
+ CFDataRef processorState = nullptr;
+ OSStatus baseResult = noErr;
+
+ if (CFGetTypeID (inData) == CFDictionaryGetTypeID())
+ {
+ processorState = static_cast (CFDictionaryGetValue (static_cast (inData),
+ getProcessorStateKey()));
+
+ if (processorState != nullptr && CFGetTypeID (processorState) != CFDataGetTypeID())
+ return kAudioUnitErr_InvalidPropertyValue;
+
+ baseResult = AudioPluginAUBase::RestoreState (inData);
+ if (baseResult != noErr)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "RestoreState failed: base status=" << describeStatus (baseResult));
+ return baseResult;
+ }
+ }
+ else if (CFGetTypeID (inData) == CFDataGetTypeID())
+ {
+ processorState = static_cast (inData);
+ }
+ else
+ {
+ return kAudioUnitErr_InvalidPropertyValue;
+ }
+
+ if (processorState == nullptr)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "RestoreState completed without processor state");
+ return baseResult;
+ }
+
+ if (processor == nullptr)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "RestoreState failed: processor is null");
+ return kAudioUnitErr_InvalidPropertyValue;
+ }
+
+ MemoryBlock data (CFDataGetBytePtr (processorState),
+ static_cast (CFDataGetLength (processorState)));
+
+ processor->suspendProcessing (true);
+ const auto result = processor->loadStateFromMemory (data);
+ const bool ok = result.wasOk();
+ processor->suspendProcessing (false);
+
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "RestoreState " << String (ok ? "completed" : "failed") << ": bytes=" << String (static_cast (data.getSize())) << (ok ? String() : ", error=" + result.getErrorMessage()));
+
+ return ok ? static_cast (noErr)
+ : static_cast (kAudioUnitErr_InvalidPropertyValue);
+ }
+
+ //==============================================================================
+
+ OSStatus GetPresets (CFArrayRef* outData) const override
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "GetPresets requested");
+
+ if (processor == nullptr || outData == nullptr)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "GetPresets failed: processor=" << describePointer (processor.get()) << ", outData=" << describePointer (outData));
+ return kAudioUnitErr_InvalidPropertyValue;
+ }
+
+ const int numPresets = processor->getNumPresets();
+ NSMutableArray* presetsArray = [[NSMutableArray alloc] initWithCapacity:numPresets];
+
+ for (int i = 0; i < numPresets; ++i)
+ {
+ AUPreset preset;
+ preset.presetNumber = i;
+ preset.presetName = processor->getPresetName (i).toCFString();
+
+ [presetsArray addObject:[NSValue valueWithBytes:&preset objCType:@encode (AUPreset)]];
+ CFRelease (preset.presetName);
+ }
+
+ *outData = (__bridge_retained CFArrayRef) presetsArray;
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "GetPresets returned " << String (numPresets) << " presets");
+ return noErr;
+ }
+
+ OSStatus NewFactoryPresetSet (const AUPreset& inNewFactoryPreset) override
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "NewFactoryPresetSet requested: preset=" << String (static_cast (inNewFactoryPreset.presetNumber)));
+
+ if (processor == nullptr)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "NewFactoryPresetSet failed: processor is null");
+ return kAudioUnitErr_InvalidPropertyValue;
+ }
+
+ if (! isPositiveAndBelow (static_cast (inNewFactoryPreset.presetNumber), processor->getNumPresets()))
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "NewFactoryPresetSet failed: preset out of range");
+ return kAudioUnitErr_InvalidPropertyValue;
+ }
+
+ processor->setCurrentPreset (static_cast (inNewFactoryPreset.presetNumber));
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "NewFactoryPresetSet completed");
+ return noErr;
+ }
+
+ //==============================================================================
+
+ OSStatus GetPropertyInfo (AudioUnitPropertyID inID,
+ AudioUnitScope inScope,
+ AudioUnitElement inElement,
+ UInt32& outDataSize,
+ bool& outWritable) override
+ {
+ if (inID == kAudioUnitProperty_OfflineRender)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "GetPropertyInfo OfflineRender requested: " << describeScopeAndElement (inScope, inElement));
+
+ if (inScope != kAudioUnitScope_Global)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "GetPropertyInfo OfflineRender failed: invalid scope");
+ return kAudioUnitErr_InvalidScope;
+ }
+
+ outDataSize = sizeof (UInt32);
+ outWritable = true;
+ return noErr;
+ }
+
+ if (inID == kAudioUnitProperty_BypassEffect)
+ {
+ if (inScope != kAudioUnitScope_Global)
+ return kAudioUnitErr_InvalidScope;
+
+ outDataSize = sizeof (UInt32);
+ outWritable = true;
+ return noErr;
+ }
+
+ if (inID == kAudioUnitProperty_CocoaUI)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "GetPropertyInfo CocoaUI requested: " << describeScopeAndElement (inScope, inElement));
+
+ if (inScope != kAudioUnitScope_Global)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "GetPropertyInfo CocoaUI failed: invalid scope");
+ return kAudioUnitErr_InvalidScope;
+ }
+
+ if (processor != nullptr && processor->hasEditor())
+ {
+ outDataSize = sizeof (AudioUnitCocoaViewInfo);
+ outWritable = false;
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "GetPropertyInfo CocoaUI available");
+ return noErr;
+ }
+
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "GetPropertyInfo CocoaUI not available: processor=" << describePointer (processor.get()) << ", hasEditor=" << String (processor != nullptr && processor->hasEditor() ? "true" : "false"));
+ return kAudioUnitErr_PropertyNotInUse;
+ }
+
+ const auto result = AudioPluginAUBase::GetPropertyInfo (inID, inScope, inElement, outDataSize, outWritable);
+ if (result != noErr)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "GetPropertyInfo delegated failed: property=" << String (static_cast (inID)) << ", " << describeScopeAndElement (inScope, inElement) << ", status=" << describeStatus (result));
+ }
+
+ return result;
+ }
+
+ OSStatus GetProperty (AudioUnitPropertyID inID,
+ AudioUnitScope inScope,
+ AudioUnitElement inElement,
+ void* outData) override; // Implemented below (needs ObjC)
+
+ OSStatus SetProperty (AudioUnitPropertyID inID,
+ AudioUnitScope inScope,
+ AudioUnitElement inElement,
+ const void* inData,
+ UInt32 inDataSize) override
+ {
+ if (inID == kAudioUnitProperty_OfflineRender)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "SetProperty OfflineRender requested: " << describeScopeAndElement (inScope, inElement));
+
+ if (inScope != kAudioUnitScope_Global)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "SetProperty OfflineRender failed: invalid scope");
+ return kAudioUnitErr_InvalidScope;
+ }
+
+ if (inData == nullptr || inDataSize < sizeof (UInt32))
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "SetProperty OfflineRender failed: inData=" << describePointer (inData) << ", inDataSize=" << String (static_cast (inDataSize)));
+ return kAudioUnitErr_InvalidPropertyValue;
+ }
+
+ renderingOffline = *static_cast (inData) != 0;
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "Offline rendering set to " << String (renderingOffline ? "true" : "false"));
+
+ if (processor != nullptr)
+ processor->setOfflineProcessing (renderingOffline);
+
+ return noErr;
+ }
+
+ if (inID == kAudioUnitProperty_BypassEffect)
+ {
+ if (inScope != kAudioUnitScope_Global)
+ return kAudioUnitErr_InvalidScope;
+
+ if (inData == nullptr || inDataSize < sizeof (UInt32))
+ return kAudioUnitErr_InvalidPropertyValue;
+
+ isBypassed = *static_cast (inData) != 0;
+ return noErr;
+ }
+
+ const auto result = AudioPluginAUBase::SetProperty (inID, inScope, inElement, inData, inDataSize);
+ if (result != noErr)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "SetProperty delegated failed: property=" << String (static_cast (inID)) << ", " << describeScopeAndElement (inScope, inElement) << ", status=" << describeStatus (result));
+ }
+
+ return result;
+ }
+
+ //==============================================================================
+
+ AudioProcessor* getProcessor() const { return processor.get(); }
+
+ void registerEditorView (AudioPluginEditorViewAU* view)
+ {
+ if (view == nil)
+ return;
+
+ std::lock_guard lock (editorViewsMutex);
+ editorViews.push_back ((__bridge void*) view);
+ }
+
+ void unregisterEditorView (AudioPluginEditorViewAU* view)
+ {
+ if (view == nil)
+ return;
+
+ std::lock_guard lock (editorViewsMutex);
+ editorViews.erase (std::remove (editorViews.begin(), editorViews.end(), (__bridge void*) view), editorViews.end());
+ }
+
+ void closeEditorViews();
+
+ static AudioPluginProcessorAU* findInstance (AudioUnit component)
+ {
+ std::lock_guard lock (getInstanceRegistryMutex());
+
+ const auto iter = getInstanceRegistry().find (component);
+ auto* instance = iter != getInstanceRegistry().end() ? iter->second : nullptr;
+
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "lookup instance: component=" << describePointer (component) << ", instance=" << describePointer (instance) << ", registeredInstances=" << String (static_cast (getInstanceRegistry().size())));
+
+ return instance;
+ }
+
+private:
+ static std::mutex& getInstanceRegistryMutex()
+ {
+ static std::mutex mutex;
+ return mutex;
+ }
+
+ static std::unordered_map& getInstanceRegistry()
+ {
+ static std::unordered_map instances;
+ return instances;
+ }
+
+ static void registerInstance (AudioUnit component, AudioPluginProcessorAU* instance)
+ {
+ std::lock_guard lock (getInstanceRegistryMutex());
+
+ getInstanceRegistry()[component] = instance;
+
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "registered instance: component=" << describePointer (component) << ", instance=" << describePointer (instance) << ", registeredInstances=" << String (static_cast (getInstanceRegistry().size())));
+ }
+
+ static void unregisterInstance (AudioUnit component)
+ {
+ std::lock_guard lock (getInstanceRegistryMutex());
+
+ const auto numRemoved = getInstanceRegistry().erase (component);
+
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "unregistered instance: component=" << describePointer (component) << ", removed=" << String (static_cast (numRemoved)) << ", registeredInstances=" << String (static_cast (getInstanceRegistry().size())));
+ }
+
+ void addParameterListeners()
+ {
+ removeParameterListeners();
+
+ if (processor == nullptr)
+ return;
+
+ for (const auto& parameter : processor->getParameters())
+ {
+ parameter->addListener (this);
+ listenedParameters.push_back (parameter);
+ }
+ }
+
+ void removeParameterListeners()
+ {
+ for (auto& parameter : listenedParameters)
+ parameter->removeListener (this);
+
+ listenedParameters.clear();
+ }
+
+ bool isValidProcessorParameterIndex (int indexInContainer) const
+ {
+ return processor != nullptr
+ && isPositiveAndBelow (indexInContainer, static_cast (processor->getParameters().size()));
+ }
+
+ void parameterValueChanged (const AudioParameter::Ptr& parameter, int indexInContainer) override
+ {
+ if (! isValidProcessorParameterIndex (indexInContainer) || parameter->isReadOnly())
+ return;
+
+ AudioPluginAUBase::SetParameter (static_cast (parameter->getHostParameterID()),
+ kAudioUnitScope_Global,
+ 0,
+ static_cast (parameter->getValue()),
+ 0);
+ }
+
+ void parameterGestureBegin (const AudioParameter::Ptr& parameter, int indexInContainer) override
+ {
+ ignoreUnused (parameter, indexInContainer);
+ }
+
+ void parameterGestureEnd (const AudioParameter::Ptr& parameter, int indexInContainer) override
+ {
+ ignoreUnused (parameter, indexInContainer);
+ }
+
+ Float64 getCurrentSampleRate()
+ {
+ return Output (0).GetStreamFormat().mSampleRate;
+ }
+
+ ScopedYupInitialiser_GUI scopeInitialiser;
+ ScopedYupInitialiser_Windowing scopeWindowingInitialiser;
+ std::unique_ptr processor;
+
+ MidiBuffer midiBuffer;
+ MidiBuffer emptyMidiBuffer;
+ ParameterChangeBuffer paramChangeBuffer;
+ ParameterChangeBuffer emptyParamChangeBuffer;
+ std::mutex midiMutex;
+ std::mutex parameterChangeMutex;
+ std::mutex editorViewsMutex;
+ std::vector channelInfoCache;
+ std::vector listenedParameters;
+ std::vector audioChannels;
+ std::vector editorViews;
+ AudioUnit componentInstance = nullptr;
+ bool renderingOffline = false;
+ bool isBypassed = false;
+};
+
+} // namespace yup
+
+//==============================================================================
+// Objective-C editor view
+
+namespace yup
+{
+
+class AudioPluginEditorViewAUListener final : public ComponentListener
+{
+public:
+ explicit AudioPluginEditorViewAUListener (AudioPluginEditorViewAU* owner)
+ : owner (owner)
+ {
+ }
+
+ void componentResized (Component& component) override;
+
+private:
+ AudioPluginEditorViewAU* owner = nil;
+
+ YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioPluginEditorViewAUListener)
+};
+
+} // namespace yup
+
+@interface AudioPluginEditorViewAU : NSView
+{
+ yup::AudioPluginProcessorAU* _processorWrapper;
+ yup::AudioProcessor* _processor;
+ std::unique_ptr _processorEditor;
+ std::unique_ptr _processorEditorListener;
+ bool _resizingEditorToBounds;
+}
+- (instancetype)initWithAudioUnitWrapper:(yup::AudioPluginProcessorAU*)processorWrapper
+ preferredSize:(NSSize)size;
+- (void)attachEditorIfNeeded;
+- (void)detachEditorIfNeeded;
+- (void)closeEditorIfNeeded;
+- (void)closeEditorForProcessorDestruction;
+- (void)resizeEditorToBounds;
+- (void)resizeViewToEditorSize;
+- (void)processorEditorResized;
+@end
+
+@implementation AudioPluginEditorViewAU
+
+- (instancetype)initWithAudioUnitWrapper:(yup::AudioPluginProcessorAU*)processorWrapper
+ preferredSize:(NSSize)size
+{
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "creating editor view: requestedWidth=" << yup::String (static_cast (size.width)) << ", requestedHeight=" << yup::String (static_cast (size.height)) << ", wrapper=" << yup::describePointer (processorWrapper) << ", view=" << yup::describePointer ((__bridge void*) self));
+
+ if ((self = [super initWithFrame:NSMakeRect (0, 0, size.width, size.height)]))
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "editor view initialised: view=" << yup::describePointer ((__bridge void*) self));
+ _processorWrapper = processorWrapper;
+ _processor = processorWrapper != nullptr ? processorWrapper->getProcessor() : nullptr;
+ _resizingEditorToBounds = false;
+ [self setPostsFrameChangedNotifications:YES];
+
+ if (_processorWrapper != nullptr)
+ _processorWrapper->registerEditorView (self);
+
+ if (_processor != nullptr && _processor->hasEditor())
+ {
+ _processorEditor.reset (_processor->createEditor());
+
+ if (_processorEditor != nullptr)
+ {
+ const auto preferredSize = _processorEditor->getPreferredSize();
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "created editor: preferredWidth=" << yup::String (preferredSize.getWidth()) << ", preferredHeight=" << yup::String (preferredSize.getHeight()) << ", editor=" << yup::describePointer (_processorEditor.get()));
+
+ if (_processorEditor->isResizable())
+ [self setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
+ else
+ [self setAutoresizingMask:NSViewNotSizable];
+
+ _processorEditorListener = std::make_unique (self);
+ _processorEditor->addComponentListener (_processorEditorListener.get());
+
+ [self setFrameSize:NSMakeSize (preferredSize.getWidth(), preferredSize.getHeight())];
+ [self resizeEditorToBounds];
+ }
+ else
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "processor returned null editor");
+ }
+ }
+ else
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "processor has no editor");
+ }
+ }
+ else
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "editor view initialisation failed");
+ }
+
+ return self;
+}
+
+- (void)viewDidMoveToWindow
+{
+ [super viewDidMoveToWindow];
+
+ if ([self window] != nil)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "editor view moved to window: view=" << yup::describePointer ((__bridge void*) self) << ", window=" << yup::describePointer ((__bridge void*) [self window]) << ", contentView=" << yup::describePointer ((__bridge void*) [[self window] contentView]));
+
+ [self attachEditorIfNeeded];
+ }
+ else
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "editor view removed from window: view=" << yup::describePointer ((__bridge void*) self));
+
+ [self detachEditorIfNeeded];
+ }
+}
+
+- (void)setFrameSize:(NSSize)newSize
+{
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "editor view frame size changed: width=" << yup::String (static_cast (newSize.width)) << ", height=" << yup::String (static_cast (newSize.height)));
+
+ [super setFrameSize:newSize];
+ [self resizeEditorToBounds];
+}
+
+- (void)attachEditorIfNeeded
+{
+ if (_processorEditor == nullptr)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "attachEditorIfNeeded skipped: editor is null");
+ return;
+ }
+
+ if (_processorEditor->isOnDesktop())
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "attachEditorIfNeeded skipped: editor is already on desktop");
+ return;
+ }
+
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "attaching editor to native view: editor=" << yup::describePointer (_processorEditor.get()) << ", view=" << yup::describePointer ((__bridge void*) self) << ", window=" << yup::describePointer ((__bridge void*) [self window]));
+
+ [self resizeEditorToBounds];
+
+ yup::ComponentNative::Flags flags = yup::ComponentNative::defaultFlags & ~yup::ComponentNative::decoratedWindow;
+
+ if (_processorEditor->shouldRenderContinuous())
+ flags.set (yup::ComponentNative::renderContinuous);
+
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "editor native options: renderContinuous=" << yup::String (_processorEditor->shouldRenderContinuous() ? "true" : "false") << ", resizable=" << yup::String (_processorEditor->isResizable() ? "true" : "false"));
+
+ auto options = yup::ComponentNative::Options()
+ .withFlags (flags)
+ .withResizableWindow (_processorEditor->isResizable());
+
+ _processorEditor->addToDesktop (options, (__bridge void*) self);
+ _processorEditor->setVisible (true);
+ _processorEditor->attachedToNative();
+
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "editor attached to native view: isOnDesktop=" << yup::String (_processorEditor->isOnDesktop() ? "true" : "false"));
+}
+
+- (void)detachEditorIfNeeded
+{
+ if (_processorEditor == nullptr)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "detachEditorIfNeeded skipped: editor is null");
+ return;
+ }
+
+ if (! _processorEditor->isOnDesktop())
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "detachEditorIfNeeded skipped: editor is not on desktop");
+ return;
+ }
+
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "detaching editor from native view: editor=" << yup::describePointer (_processorEditor.get()) << ", view=" << yup::describePointer ((__bridge void*) self) << ", window=" << yup::describePointer ((__bridge void*) [self window]));
+
+ yup::endActiveParameterGestures (_processor);
+ _processorEditor->setVisible (false);
+ _processorEditor->removeFromDesktop();
+
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "editor detached from native view: isOnDesktop=" << yup::String (_processorEditor->isOnDesktop() ? "true" : "false"));
+}
+
+- (void)closeEditorIfNeeded
+{
+ [self detachEditorIfNeeded];
+
+ yup::endActiveParameterGestures (_processor);
+
+ if (_processorEditor != nullptr && _processorEditorListener != nullptr)
+ _processorEditor->removeComponentListener (_processorEditorListener.get());
+
+ _processorEditorListener.reset();
+ _processorEditor.reset();
+}
+
+- (void)closeEditorForProcessorDestruction
+{
+ [self closeEditorIfNeeded];
+
+ _processorWrapper = nullptr;
+ _processor = nullptr;
+}
+
+- (void)resizeEditorToBounds
+{
+ if (_processorEditor == nullptr)
+ return;
+
+ const auto bounds = [self bounds];
+
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "resizing editor to bounds: width=" << yup::String (static_cast (NSWidth (bounds))) << ", height=" << yup::String (static_cast (NSHeight (bounds))) << ", editor=" << yup::describePointer (_processorEditor.get()));
+
+ const auto scoped = yup::ScopedValueSetter (_resizingEditorToBounds, true);
+
+ _processorEditor->setBounds ({ 0.0f,
+ 0.0f,
+ yup::jmax (1.0f, static_cast (NSWidth (bounds))),
+ yup::jmax (1.0f, static_cast (NSHeight (bounds))) });
+}
+
+- (void)resizeViewToEditorSize
+{
+ if (_processorEditor == nullptr || ! _processorEditor->isResizable())
+ return;
+
+ const auto newSize = NSMakeSize (yup::jmax (1.0f, _processorEditor->getWidth()),
+ yup::jmax (1.0f, _processorEditor->getHeight()));
+
+ if (NSEqualSizes ([self frame].size, newSize))
+ return;
+
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "resizing editor view to editor: width=" << yup::String (static_cast (newSize.width)) << ", height=" << yup::String (static_cast (newSize.height)) << ", editor=" << yup::describePointer (_processorEditor.get()));
+
+ [super setFrameSize:newSize];
+}
+
+- (void)processorEditorResized
+{
+ if (_resizingEditorToBounds)
+ return;
+
+ [self resizeViewToEditorSize];
+}
+
+- (void)dealloc
+{
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "destroying editor view: view=" << yup::describePointer ((__bridge void*) self) << ", editor=" << yup::describePointer (_processorEditor.get()) << ", processor=" << yup::describePointer (_processor));
+
+ [self closeEditorIfNeeded];
+
+ if (_processorWrapper != nullptr)
+ _processorWrapper->unregisterEditorView (self);
+
+ _processorWrapper = nullptr;
+ _processor = nullptr;
+}
+
+@end
+
+namespace yup
+{
+
+void AudioPluginEditorViewAUListener::componentResized (Component& component)
+{
+ ignoreUnused (component);
+
+ if (owner != nil)
+ [owner processorEditorResized];
+}
+
+void AudioPluginProcessorAU::closeEditorViews()
+{
+ std::vector viewsToClose;
+
+ {
+ std::lock_guard lock (editorViewsMutex);
+ viewsToClose.swap (editorViews);
+ }
+
+ for (auto* view : viewsToClose)
+ if (view != nullptr)
+ [(__bridge AudioPluginEditorViewAU*) view closeEditorForProcessorDestruction];
+}
+
+} // namespace yup
+
+//==============================================================================
+// Cocoa view factory
+
+@interface AudioPluginProcessorAUViewFactory : NSObject
+@end
+
+@implementation AudioPluginProcessorAUViewFactory
+
+- (unsigned)interfaceVersion
+{
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "view factory interfaceVersion requested");
+ return 0;
+}
+
+- (NSString*)description
+{
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "view factory description requested");
+ return @YupPlugin_Name;
+}
+
+- (NSView*)uiViewForAudioUnit:(AudioUnit)inAudioUnit withSize:(NSSize)inPreferredSize
+{
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "view factory requested editor view: audioUnit=" << yup::describePointer (inAudioUnit) << ", preferredWidth=" << yup::String (static_cast (inPreferredSize.width)) << ", preferredHeight=" << yup::String (static_cast (inPreferredSize.height)));
+
+ auto* proc = yup::AudioPluginProcessorAU::findInstance (inAudioUnit);
+ if (proc == nullptr)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "view factory failed: AU instance not found");
+ return nil;
+ }
+
+ if (proc->getProcessor() == nullptr)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "view factory failed: processor is null");
+ return nil;
+ }
+
+ return [[AudioPluginEditorViewAU alloc] initWithAudioUnitWrapper:proc
+ preferredSize:inPreferredSize];
+}
+
+@end
+
+//==============================================================================
+// GetProperty implementation (needs ObjC)
+
+namespace yup
+{
+
+OSStatus AudioPluginProcessorAU::GetProperty (AudioUnitPropertyID inID,
+ AudioUnitScope inScope,
+ AudioUnitElement inElement,
+ void* outData)
+{
+ if (inID == kAudioUnitProperty_OfflineRender)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "GetProperty OfflineRender requested: " << describeScopeAndElement (inScope, inElement));
+
+ if (inScope != kAudioUnitScope_Global)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "GetProperty OfflineRender failed: invalid scope");
+ return kAudioUnitErr_InvalidScope;
+ }
+
+ if (outData == nullptr)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "GetProperty OfflineRender failed: outData is null");
+ return kAudioUnitErr_InvalidPropertyValue;
+ }
+
+ *static_cast (outData) = renderingOffline ? 1u : 0u;
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "GetProperty OfflineRender returned " << String (renderingOffline ? "true" : "false"));
+ return noErr;
+ }
+
+ if (inID == kAudioUnitProperty_BypassEffect)
+ {
+ if (inScope != kAudioUnitScope_Global)
+ return kAudioUnitErr_InvalidScope;
+
+ if (outData == nullptr)
+ return kAudioUnitErr_InvalidPropertyValue;
+
+ *static_cast (outData) = isBypassed ? 1u : 0u;
+ return noErr;
+ }
+
+ if (inID == kAudioUnitProperty_CocoaUI)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "GetProperty CocoaUI requested: " << describeScopeAndElement (inScope, inElement));
+
+ if (inScope != kAudioUnitScope_Global)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "GetProperty CocoaUI failed: invalid scope");
+ return kAudioUnitErr_InvalidScope;
+ }
+
+ if (processor == nullptr || ! processor->hasEditor())
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "GetProperty CocoaUI failed: editor not available");
+ return kAudioUnitErr_PropertyNotInUse;
+ }
+
+ if (outData == nullptr)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "GetProperty CocoaUI failed: outData is null");
+ return kAudioUnitErr_InvalidPropertyValue;
+ }
+
+ auto* info = static_cast (outData);
+
+ // The bundle location is this plugin's own bundle
+ NSBundle* bundle = [NSBundle bundleForClass:[AudioPluginProcessorAUViewFactory class]];
+ if (bundle == nil)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "GetProperty CocoaUI failed: bundle is nil");
+ return kAudioUnitErr_InvalidPropertyValue;
+ }
+
+ auto* bundleLocation = (__bridge_retained CFURLRef)[bundle bundleURL];
+ auto* viewClass = CFStringCreateWithCString (kCFAllocatorDefault,
+ "AudioPluginProcessorAUViewFactory",
+ kCFStringEncodingUTF8);
+
+ if (bundleLocation == nullptr || viewClass == nullptr)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "GetProperty CocoaUI failed: bundleURL=" << describePointer (bundleLocation) << ", viewClass=" << describePointer (viewClass));
+
+ if (bundleLocation != nullptr)
+ CFRelease (bundleLocation);
+
+ if (viewClass != nullptr)
+ CFRelease (viewClass);
+
+ return kAudioUnitErr_InvalidPropertyValue;
+ }
+
+ info->mCocoaAUViewBundleLocation = bundleLocation;
+ info->mCocoaAUViewClass[0] = viewClass;
+
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "GetProperty CocoaUI returned view factory: bundle=" << String::fromCFString ((__bridge CFStringRef)[[bundle bundleURL] absoluteString]));
+
+ return noErr;
+ }
+
+ const auto result = AudioPluginAUBase::GetProperty (inID, inScope, inElement, outData);
+ if (result != noErr)
+ {
+ YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "GetProperty delegated failed: property=" << String (static_cast (inID)) << ", " << describeScopeAndElement (inScope, inElement) << ", status=" << describeStatus (result));
+ }
+
+ return result;
+}
+
+} // namespace yup
+
+//==============================================================================
+// Factory entry point
+
+#if YupPlugin_IsSynth
+using AudioPluginProcessorAU = yup::AudioPluginProcessorAU;
+AUSDK_COMPONENT_ENTRY (ausdk::AUMusicDeviceFactory, AudioPluginProcessorAU)
+#else
+using AudioPluginProcessorAU = yup::AudioPluginProcessorAU;
+AUSDK_COMPONENT_ENTRY (ausdk::AUBaseProcessFactory, AudioPluginProcessorAU)
+#endif
+
+#endif // YUP_MAC
diff --git a/modules/yup_audio_plugin_client/au/yup_audio_plugin_client_AU.mm b/modules/yup_audio_plugin_client/au/yup_audio_plugin_client_AU.mm
new file mode 100644
index 000000000..e9bc75ea8
--- /dev/null
+++ b/modules/yup_audio_plugin_client/au/yup_audio_plugin_client_AU.mm
@@ -0,0 +1,22 @@
+/*
+ ==============================================================================
+
+ This file is part of the YUP library.
+ Copyright (c) 2026 - kunitoki@gmail.com
+
+ YUP is an open source library subject to open-source licensing.
+
+ The code included in this file is provided under the terms of the ISC license
+ http://www.isc.org/downloads/software-support-policy/isc-license. Permission
+ to use, copy, modify, and/or distribute this software for any purpose with or
+ without fee is hereby granted provided that the above copyright notice and
+ this permission notice appear in all copies.
+
+ YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
+ EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
+ DISCLAIMED.
+
+ ==============================================================================
+*/
+
+#include "yup_audio_plugin_client_AU.cpp"
diff --git a/modules/yup_audio_plugin_client/clap/yup_audio_plugin_client_CLAP.cpp b/modules/yup_audio_plugin_client/clap/yup_audio_plugin_client_CLAP.cpp
index beb01079d..ab7041699 100644
--- a/modules/yup_audio_plugin_client/clap/yup_audio_plugin_client_CLAP.cpp
+++ b/modules/yup_audio_plugin_client/clap/yup_audio_plugin_client_CLAP.cpp
@@ -1,1144 +1,1940 @@
-/*
- ==============================================================================
-
- This file is part of the YUP library.
- Copyright (c) 2024 - kunitoki@gmail.com
-
- YUP is an open source library subject to open-source licensing.
-
- The code included in this file is provided under the terms of the ISC license
- http://www.isc.org/downloads/software-support-policy/isc-license. Permission
- to use, copy, modify, and/or distribute this software for any purpose with or
- without fee is hereby granted provided that the above copyright notice and
- this permission notice appear in all copies.
-
- YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
- EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
- DISCLAIMED.
-
- ==============================================================================
-*/
-
-#include "../yup_audio_plugin_client.h"
-
-#if ! defined(YUP_AUDIO_PLUGIN_ENABLE_CLAP)
-#error "YUP_AUDIO_PLUGIN_ENABLE_CLAP must be defined"
-#endif
-
-#include
-#include
-
-#include
-
-extern "C" yup::AudioProcessor* createPluginProcessor();
-
-namespace yup
-{
-
-//==============================================================================
-
-std::optional clapEventToMidiNoteMessage (const clap_event_header_t* event)
-{
- switch (event->type)
- {
- case CLAP_EVENT_NOTE_ON:
- {
- const clap_event_note_t* noteEvent = reinterpret_cast (event);
- const int channel = noteEvent->channel < 0 ? 1 : noteEvent->channel + 1;
-
- return MidiMessage::noteOn (channel, noteEvent->key, static_cast (noteEvent->velocity * 127.0f));
- }
-
- case CLAP_EVENT_NOTE_OFF:
- {
- const clap_event_note_t* noteEvent = reinterpret_cast (event);
- const int channel = noteEvent->channel < 0 ? 1 : noteEvent->channel + 1;
-
- return MidiMessage::noteOff (channel, noteEvent->key, static_cast (noteEvent->velocity));
- }
-
- case CLAP_EVENT_NOTE_CHOKE:
- {
- const clap_event_note_t* noteEvent = reinterpret_cast (event);
- const int channel = noteEvent->channel < 0 ? 1 : noteEvent->channel + 1;
-
- return MidiMessage::noteOff (channel, noteEvent->key);
- }
-
- case CLAP_EVENT_NOTE_END:
- case CLAP_EVENT_NOTE_EXPRESSION:
- case CLAP_EVENT_PARAM_VALUE:
- case CLAP_EVENT_PARAM_MOD:
- case CLAP_EVENT_MIDI:
- case CLAP_EVENT_MIDI_SYSEX:
- default:
- break;
- }
-
- return std::nullopt;
-}
-
-//==============================================================================
-
-void clapEventToParameterChange (const clap_event_header_t* event, AudioProcessor& audioProcessor)
-{
- if (event->type != CLAP_EVENT_PARAM_VALUE)
- return;
-
- const clap_event_param_value_t* paramEvent = reinterpret_cast (event);
-
- auto parameters = audioProcessor.getParameters();
-
- auto parameterIndex = static_cast (paramEvent->param_id);
- if (! isPositiveAndBelow (parameterIndex, static_cast (parameters.size())))
- return;
-
- parameters[parameterIndex]->setValue (static_cast (paramEvent->value));
-}
-
-//==============================================================================
-
-/*
-void pluginSyncMainToAudio (AudioProcessor& audioProcessor, const clap_output_events_t* out)
-{
- auto sl = CriticalSection::ScopedLockType (plugin->syncParameters);
-
- for (uint32_t i = 0; i < P_COUNT; i++)
- {
- if (plugin->mainChanged[i])
- {
- plugin->parameters[i] = plugin->mainParameters[i];
- plugin->mainChanged[i] = false;
-
- clap_event_param_value_t event = {};
- event.header.size = sizeof(event);
- event.header.time = 0;
- event.header.space_id = CLAP_CORE_EVENT_SPACE_ID;
- event.header.type = CLAP_EVENT_PARAM_VALUE;
- event.header.flags = 0;
- event.param_id = i;
- event.cookie = NULL;
- event.note_id = -1;
- event.port_index = -1;
- event.channel = -1;
- event.key = -1;
- event.value = plugin->parameters[i];
- out->try_push(out, &event.header);
- }
- }
-}
-
-bool pluginSyncAudioToMain (AudioProcessor& audioProcessor)
-{
- bool anyChanged = false;
- auto sl = CriticalSection::ScopedLockType (plugin->syncParameters);
-
- for (uint32_t i = 0; i < P_COUNT; i++)
- {
- if (plugin->changed[i])
- {
- plugin->mainParameters[i] = plugin->parameters[i];
- plugin->changed[i] = false;
- anyChanged = true;
- }
- }
-
- return anyChanged;
-
- return false;
-}
-*/
-
-//==============================================================================
-
-static const char* pluginFeatures[] = {
-#if YupPlugin_IsSynth
- CLAP_PLUGIN_FEATURE_INSTRUMENT,
- CLAP_PLUGIN_FEATURE_SYNTHESIZER,
-#else
- CLAP_PLUGIN_FEATURE_AUDIO_EFFECT,
-#endif
-
-#if YupPlugin_IsMono
- CLAP_PLUGIN_FEATURE_MONO,
-#else
- CLAP_PLUGIN_FEATURE_STEREO,
-#endif
-
- nullptr
-};
-
-static const clap_plugin_descriptor_t pluginDescriptor = {
- .clap_version = CLAP_VERSION_INIT,
- .id = YupPlugin_Id,
- .name = YupPlugin_Name,
- .vendor = YupPlugin_Vendor,
- .url = YupPlugin_URL,
- .manual_url = YupPlugin_URL,
- .support_url = YupPlugin_URL,
- .version = YupPlugin_Version,
- .description = YupPlugin_Description,
- .features = pluginFeatures,
-};
-
-#if YUP_MAC
-static const char* const preferredApi = CLAP_WINDOW_API_COCOA;
-#elif YUP_WINDOWS
-static const char* const preferredApi = CLAP_WINDOW_API_WIN32;
-#elif YUP_LINUX
-static const char* const preferredApi = CLAP_WINDOW_API_X11;
-#endif
-
-//==============================================================================
-
-class AudioPluginProcessorCLAP;
-
-//==============================================================================
-
-class AudioPluginPlayHeadCLAP final : public AudioPlayHead
-{
-public:
- explicit AudioPluginPlayHeadCLAP (float sampleRate, const clap_process_t* process)
- : process (*process)
- , sampleRate (sampleRate)
- {
- }
-
- bool canControlTransport() override
- {
- return false;
- }
-
- void transportPlay (bool shouldSartPlaying) override
- {
- if (! canControlTransport())
- return;
- }
-
- void transportRecord (bool shouldStartRecording) override
- {
- if (! canControlTransport())
- return;
- }
-
- void transportRewind() override
- {
- if (! canControlTransport())
- return;
- }
-
- std::optional getPosition() const override
- {
- if (process.transport == nullptr)
- return {};
-
- PositionInfo result;
-
- result.setTimeInSeconds (process.transport->song_pos_seconds / (double) CLAP_SECTIME_FACTOR);
- result.setTimeInSamples ((int64) (sampleRate * (process.transport->song_pos_seconds / (double) CLAP_SECTIME_FACTOR)));
- result.setTimeSignature (TimeSignature { process.transport->tsig_num, process.transport->tsig_denom });
- result.setBpm (process.transport->tempo);
- result.setBarCount (process.transport->bar_number);
- result.setPpqPositionOfLastBarStart (process.transport->bar_start / (double) CLAP_BEATTIME_FACTOR);
- result.setIsPlaying (process.transport->flags & CLAP_TRANSPORT_IS_PLAYING);
- result.setIsRecording (process.transport->flags & CLAP_TRANSPORT_IS_RECORDING);
- result.setIsLooping (process.transport->flags & CLAP_TRANSPORT_IS_LOOP_ACTIVE);
- result.setLoopPoints (LoopPoints {
- process.transport->loop_start_beats / (double) CLAP_BEATTIME_FACTOR,
- process.transport->loop_end_beats / (double) CLAP_BEATTIME_FACTOR });
- result.setFrameRate (AudioPlayHead::fpsUnknown);
-
- return result;
- }
-
-private:
- clap_process_t process;
- float sampleRate = 44100.0f;
-};
-
-//==============================================================================
-
-class AudioPluginEditorCLAP final : public Component
-{
-public:
- AudioPluginEditorCLAP (AudioPluginProcessorCLAP* wrapper, AudioProcessorEditor* editor)
- : wrapper (wrapper)
- , processorEditor (editor)
- {
- addAndMakeVisible (*processorEditor);
- }
-
- AudioProcessorEditor* getAudioProcessorEditor() { return processorEditor.get(); }
-
- void resized() override;
-
-private:
- AudioPluginProcessorCLAP* wrapper = nullptr;
- std::unique_ptr processorEditor;
-};
-
-//==============================================================================
-
-class AudioPluginProcessorCLAP final
-{
-public:
- AudioPluginProcessorCLAP (const clap_host_t* host);
- ~AudioPluginProcessorCLAP();
-
- bool initialise();
- void destroy();
-
- bool activate (float sampleRate, int samplesPerBlock);
- void deactivate();
-
- bool startProcessing();
- void stopProcessing();
-
- void reset();
-
- void registerTimer (uint32_t periodMs, clap_id* timerId);
- void unregisterTimer (clap_id timerId);
-
- const void* getExtension (std::string_view id);
- const clap_plugin_t* getPlugin() const;
-
- void editorResized();
- ScopedValueSetter scopedHostEditorResizing();
-
-private:
- std::unique_ptr audioProcessor;
- std::unique_ptr audioPluginEditor;
-
- const clap_host_t* host = nullptr;
-
- clap_plugin_t plugin;
-
- clap_plugin_note_ports_t extensionNotePorts;
- clap_plugin_audio_ports_t extensionAudioPorts;
- clap_plugin_params_t extensionParams;
- clap_plugin_state_t extensionState;
- clap_plugin_tail_t extensionTail;
- clap_plugin_latency_t extensionLatency;
- clap_plugin_timer_support_t extensionTimerSupport;
- clap_plugin_gui_t extensionGUI;
-
- const clap_host_params_t* hostParams = nullptr;
- const clap_host_state_t* hostState = nullptr;
- const clap_host_tail_t* hostTail = nullptr;
- const clap_host_latency_t* hostLatency = nullptr;
- const clap_host_timer_support_t* hostTimerSupport = nullptr;
- const clap_host_gui_t* hostGUI = nullptr;
-
- clap_id guiTimerId;
- bool hostTriggeredResizing = false;
-
- MidiBuffer midiEvents;
-
- static std::atomic_int instancesCount;
-};
-
-//==============================================================================
-
-std::atomic_int AudioPluginProcessorCLAP::instancesCount = 0;
-
-//==============================================================================
-
-AudioPluginProcessorCLAP* getWrapper (const clap_plugin_t* plugin)
-{
- return reinterpret_cast (plugin->plugin_data);
-}
-
-//==============================================================================
-
-AudioPluginProcessorCLAP::AudioPluginProcessorCLAP (const clap_host_t* host)
- : host (host)
-{
- jassert (host != nullptr);
-
- plugin.desc = &pluginDescriptor;
- plugin.plugin_data = this;
-
- plugin.init = [] (const clap_plugin* plugin) -> bool
- {
- return getWrapper (plugin)->initialise();
- };
-
- plugin.destroy = [] (const clap_plugin* plugin)
- {
- getWrapper (plugin)->destroy();
- };
-
- plugin.activate = [] (const clap_plugin* plugin, double sampleRate, uint32_t minimumFramesCount, uint32_t maximumFramesCount) -> bool
- {
- return getWrapper (plugin)->activate (static_cast (sampleRate), static_cast (maximumFramesCount));
- };
-
- plugin.deactivate = [] (const clap_plugin* plugin)
- {
- getWrapper (plugin)->deactivate();
- };
-
- plugin.start_processing = [] (const clap_plugin* plugin) -> bool
- {
- return getWrapper (plugin)->startProcessing();
- };
-
- plugin.stop_processing = [] (const clap_plugin* plugin)
- {
- getWrapper (plugin)->stopProcessing();
- };
-
- plugin.reset = [] (const clap_plugin* plugin)
- {
- getWrapper (plugin)->reset();
- };
-
- plugin.process = [] (const clap_plugin* plugin, const clap_process_t* process) -> clap_process_status
- {
- auto wrapper = getWrapper (plugin);
-
- auto& audioProcessor = *wrapper->audioProcessor;
- auto& midiBuffer = wrapper->midiEvents;
-
- auto lock = CriticalSection::ScopedTryLockType (audioProcessor.getProcessLock());
- if (! lock.isLocked() || audioProcessor.isSuspended())
- return CLAP_PROCESS_CONTINUE;
-
- jassert (process->audio_outputs_count == audioProcessor.getNumAudioOutputs());
- jassert (process->audio_inputs_count == audioProcessor.getNumAudioInputs());
-
- // PluginSyncMainToAudio(plugin, process->out_events);
-
- // Prepare midi events
- midiBuffer.clear();
-
- const uint32_t inputEventCount = process->in_events->size (process->in_events);
- for (uint32_t eventIndex = 0; eventIndex < inputEventCount; ++eventIndex)
- {
- const clap_event_header_t* event = process->in_events->get (process->in_events, eventIndex);
-
- if (event->space_id != CLAP_CORE_EVENT_SPACE_ID)
- continue;
-
- if (auto convertedEvent = clapEventToMidiNoteMessage (event))
- midiBuffer.addEvent (*convertedEvent, static_cast (event->time));
- else
- clapEventToParameterChange (event, audioProcessor);
- }
-
- // Prepare audio buffers, play head and process block
- float* buffers[2] = {
- process->audio_outputs[0].data32[0],
- process->audio_outputs[0].data32[1]
- };
-
- AudioSampleBuffer audioBuffer (&buffers[0], 2, 0, static_cast (process->frames_count));
-
- AudioPluginPlayHeadCLAP playHead (audioProcessor.getSampleRate(), process);
- audioProcessor.setPlayHead (&playHead);
-
- audioProcessor.processBlock (audioBuffer, midiBuffer);
-
- audioProcessor.setPlayHead (nullptr);
-
- // Send back note end to host
- for (const MidiMessageMetadata metadata : midiBuffer)
- {
- if (const auto& message = metadata.getMessage(); message.isNoteOff())
- {
- clap_event_note_t event = {};
- event.header.size = sizeof (event);
- event.header.time = 0;
- event.header.space_id = CLAP_CORE_EVENT_SPACE_ID;
- event.header.type = CLAP_EVENT_NOTE_END;
- event.header.flags = 0;
- event.note_id = -1;
- event.key = message.getNoteNumber();
- event.channel = message.getChannel() - 1;
- event.port_index = 0;
-
- process->out_events->try_push (process->out_events, &event.header);
- }
- }
-
- return CLAP_PROCESS_CONTINUE;
- };
-
- plugin.get_extension = [] (const clap_plugin* plugin, const char* id) -> const void*
- {
- return getWrapper (plugin)->getExtension (id);
- };
-
- plugin.on_main_thread = [] (const clap_plugin* plugin) {};
-}
-
-//==============================================================================
-
-AudioPluginProcessorCLAP::~AudioPluginProcessorCLAP()
-{
-}
-
-//==============================================================================
-
-bool AudioPluginProcessorCLAP::initialise()
-{
- jassert (audioProcessor == nullptr);
-
- audioProcessor.reset (::createPluginProcessor());
- if (audioProcessor == nullptr)
- return false;
-
- // ==== Setup extensions: parameters
- extensionParams.count = [] (const clap_plugin_t* plugin) -> uint32_t
- {
- return static_cast (getWrapper (plugin)->audioProcessor->getParameters().size());
- };
-
- extensionParams.get_info = [] (const clap_plugin_t* plugin, uint32_t index, clap_param_info_t* information) -> bool
- {
- std::memset (information, 0, sizeof (clap_param_info_t));
-
- auto wrapper = getWrapper (plugin);
- auto parameters = wrapper->audioProcessor->getParameters();
-
- if (index >= static_cast (parameters.size()))
- return false;
-
- auto& parameter = parameters[index];
-
- information->id = index;
- information->cookie = parameter.get();
- information->flags = CLAP_PARAM_IS_AUTOMATABLE | CLAP_PARAM_IS_MODULATABLE | CLAP_PARAM_IS_MODULATABLE_PER_NOTE_ID;
- information->min_value = parameter->getMinimumValue();
- information->max_value = parameter->getMaximumValue();
- information->default_value = parameter->getDefaultValue();
- parameter->getName().copyToUTF8 (information->name, CLAP_NAME_SIZE);
-
- return true;
- };
-
- extensionParams.get_value = [] (const clap_plugin_t* plugin, clap_id parameterId, double* value) -> bool
- {
- auto wrapper = getWrapper (plugin);
- auto parameters = wrapper->audioProcessor->getParameters();
-
- if (parameterId >= static_cast (parameters.size()))
- return false;
-
- *value = parameters[parameterId]->getValue();
-
- return true;
- };
-
- extensionParams.value_to_text = [] (const clap_plugin_t* plugin, clap_id parameterId, double value, char* display, uint32_t size) -> bool
- {
- auto wrapper = getWrapper (plugin);
- auto parameters = wrapper->audioProcessor->getParameters();
-
- if (parameterId >= static_cast (parameters.size()))
- return false;
-
- const auto text = parameters[parameterId]->convertToString (static_cast (value));
- text.copyToUTF8 (display, size);
-
- return true;
- };
-
- extensionParams.text_to_value = [] (const clap_plugin_t* plugin, clap_id parameterId, const char* display, double* value) -> bool
- {
- auto wrapper = getWrapper (plugin);
- auto parameters = wrapper->audioProcessor->getParameters();
-
- if (parameterId >= static_cast