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 (parameters.size())) - return false; - - *value = static_cast (parameters[parameterId]->convertFromString (display)); - - return true; - }; - - extensionParams.flush = [] (const clap_plugin_t* plugin, const clap_input_events_t* in, const clap_output_events_t* out) - { - /* // TODO - auto wrapper = getWrapper (plugin); - - MyPlugin *plugin = (MyPlugin *) _plugin->plugin_data; - const uint32_t eventCount = in->size(in); - - // For parameters that have been modified by the main thread, send CLAP_EVENT_PARAM_VALUE events to the host. - PluginSyncMainToAudio(plugin, out); - - // Process events sent to our plugin from the host. - for (uint32_t eventIndex = 0; eventIndex < eventCount; eventIndex++) - { - PluginProcessEvent(plugin, in->get(in, eventIndex)); - } - */ - }; - - // ==== Setup extensions: note ports - extensionNotePorts.count = [] (const clap_plugin_t* plugin, bool isInput) -> uint32_t - { - // TODO - this depends on the YupPlugin_IsSynth, but we might want to probe for midi input buses - return isInput ? 1 : 0; - }; - - extensionNotePorts.get = [] (const clap_plugin_t* plugin, uint32_t index, bool isInput, clap_note_port_info_t* info) -> bool - { - if (! isInput || index) - return false; - - info->id = 0; - info->supported_dialects = CLAP_NOTE_DIALECT_CLAP; // TODO Also support the MIDI dialect. - info->preferred_dialect = CLAP_NOTE_DIALECT_CLAP; - - std::snprintf (info->name, sizeof (info->name), "%s", "Note Port"); - - return true; - }; - - // ==== Setup extensions: audio ports - extensionAudioPorts.count = [] (const clap_plugin_t* plugin, bool isInput) -> uint32_t - { - auto wrapper = getWrapper (plugin); - auto* audioProcessor = wrapper->audioProcessor.get(); - - Span busses = isInput - ? audioProcessor->getBusLayout().getInputBuses() - : audioProcessor->getBusLayout().getOutputBuses(); - - uint32_t count = 0; - for (const auto& bus : busses) - if (bus.getType() == AudioBus::Type::Audio) - ++count; - - return count; - }; - - extensionAudioPorts.get = [] (const clap_plugin_t* plugin, uint32_t index, bool isInput, clap_audio_port_info_t* info) -> bool - { - auto wrapper = getWrapper (plugin); - auto* audioProcessor = wrapper->audioProcessor.get(); - - Span busses = isInput - ? audioProcessor->getBusLayout().getInputBuses() - : audioProcessor->getBusLayout().getOutputBuses(); - - const AudioBus* audioBus = nullptr; - uint32_t audioBusIndex = 0; - - for (const auto& bus : busses) - { - if (bus.getType() != AudioBus::Type::Audio) - continue; - - if (audioBusIndex == index) - { - audioBus = &bus; - break; - } - - ++audioBusIndex; - } - - if (audioBus == nullptr) - return false; - - info->id = index; - info->channel_count = audioBus->getNumChannels(); - info->flags = (index == 0) ? CLAP_AUDIO_PORT_IS_MAIN : 0; - info->port_type = audioBus->isStereo() ? CLAP_PORT_STEREO : CLAP_PORT_MONO; - info->in_place_pair = CLAP_INVALID_ID; - audioBus->getName().copyToUTF8 (info->name, sizeof (info->name)); - - return true; - }; - - // ==== Setup extensions: state - extensionState.save = [] (const clap_plugin_t* plugin, const clap_ostream_t* stream) -> bool - { - auto wrapper = getWrapper (plugin); - - MemoryBlock data; - - // TODO - should we suspend ? - - if (auto result = wrapper->audioProcessor->saveStateIntoMemory (data); result.failed()) - return false; - - // TODO - should we resume ? - - return stream->write (stream, data.getData(), data.getSize()) == data.getSize(); - }; - - extensionState.load = [] (const clap_plugin_t* plugin, const clap_istream_t* stream) -> bool - { - auto wrapper = getWrapper (plugin); - auto parameters = wrapper->audioProcessor->getParameters(); - - MemoryBlock data; - if (auto result = stream->read (stream, data.getData(), data.getSize()); result <= 0) - return false; - - // TODO - should we suspend ? - - auto result = wrapper->audioProcessor->loadStateFromMemory (data); - - // TODO - should we resume ? - - return result.wasOk(); - }; - - // ==== Setup extensions: tail - extensionTail.get = [] (const clap_plugin_t* plugin) -> uint32_t - { - auto wrapper = getWrapper (plugin); - return static_cast (wrapper->audioProcessor->getTailSamples()); - }; - - // ==== Setup extensions: latency - extensionLatency.get = [] (const clap_plugin_t* plugin) -> uint32_t - { - auto wrapper = getWrapper (plugin); - return static_cast (wrapper->audioProcessor->getLatencySamples()); - }; - - // ==== Setup extensions: timer support - extensionTimerSupport.on_timer = [] (const clap_plugin_t* plugin, clap_id timerId) - { -#if YUP_LINUX - if (auto wrapper = getWrapper (plugin); wrapper->guiTimerId == timerId) - MessageManager::getInstance()->runDispatchLoopUntil (10); -#endif - }; - - // ==== Setup extensions: gui - extensionGUI.is_api_supported = [] (const clap_plugin_t* plugin, const char* api, bool isFloating) -> bool - { - auto wrapper = getWrapper (plugin); - if (wrapper->audioProcessor == nullptr || ! wrapper->audioProcessor->hasEditor()) - return false; - - return std::string_view (api) == preferredApi && ! isFloating; - }; - - extensionGUI.get_preferred_api = [] (const clap_plugin_t* plugin, const char** api, bool* isFloating) -> bool - { - *api = preferredApi; - *isFloating = false; - return true; - }; - - extensionGUI.create = [] (const clap_plugin_t* plugin, const char* api, bool isFloating) -> bool - { - if (api == nullptr || std::string_view (api) != preferredApi || isFloating) - return false; - - auto wrapper = getWrapper (plugin); - - auto processorEditor = wrapper->audioProcessor->createEditor(); - if (processorEditor == nullptr) - return false; - - wrapper->audioPluginEditor = std::make_unique (wrapper, processorEditor); - - if (isFloating) - { - auto audioProcessorEditor = wrapper->audioPluginEditor->getAudioProcessorEditor(); - if (audioProcessorEditor == nullptr) - return false; - - ComponentNative::Flags flags = ComponentNative::defaultFlags; - - if (audioProcessorEditor->shouldRenderContinuous()) - flags.set (ComponentNative::renderContinuous); - - auto options = ComponentNative::Options() - .withFlags (flags) - .withResizableWindow (audioProcessorEditor->isResizable()); - - wrapper->audioPluginEditor->addToDesktop (options); - wrapper->audioPluginEditor->setVisible (true); - - audioProcessorEditor->attachedToNative(); - } - - return true; - }; - - extensionGUI.destroy = [] (const clap_plugin_t* plugin) - { - auto wrapper = getWrapper (plugin); - wrapper->audioPluginEditor.reset(); - }; - - extensionGUI.set_scale = [] (const clap_plugin_t* plugin, double scale) -> bool - { - return false; - }; - - extensionGUI.get_size = [] (const clap_plugin_t* plugin, uint32_t* width, uint32_t* height) -> bool - { - auto wrapper = getWrapper (plugin); - if (wrapper->audioPluginEditor == nullptr) - return false; - - auto audioProcessorEditor = wrapper->audioPluginEditor->getAudioProcessorEditor(); - - if (audioProcessorEditor->isResizable() && audioProcessorEditor->getWidth() != 0) - { - *width = static_cast (audioProcessorEditor->getWidth()); - *height = static_cast (audioProcessorEditor->getHeight()); - } - else - { - *width = static_cast (audioProcessorEditor->getPreferredSize().getWidth()); - *height = static_cast (audioProcessorEditor->getPreferredSize().getHeight()); - } - - return true; - }; - - extensionGUI.can_resize = [] (const clap_plugin_t* plugin) -> bool - { - auto wrapper = getWrapper (plugin); - if (wrapper->audioPluginEditor == nullptr) - return false; - - return wrapper->audioPluginEditor->getAudioProcessorEditor()->isResizable(); - }; - - extensionGUI.get_resize_hints = [] (const clap_plugin_t* plugin, clap_gui_resize_hints_t* hints) -> bool - { - auto wrapper = getWrapper (plugin); - if (wrapper->audioPluginEditor == nullptr) - return false; - - auto audioProcessorEditor = wrapper->audioPluginEditor->getAudioProcessorEditor(); - - hints->can_resize_horizontally = audioProcessorEditor->isResizable(); - hints->can_resize_vertically = audioProcessorEditor->isResizable(); - hints->preserve_aspect_ratio = audioProcessorEditor->shouldPreserveAspectRatio(); - hints->aspect_ratio_width = audioProcessorEditor->getPreferredSize().getWidth(); - hints->aspect_ratio_height = audioProcessorEditor->getPreferredSize().getHeight(); - - return true; - }; - - extensionGUI.adjust_size = [] (const clap_plugin_t* plugin, uint32_t* width, uint32_t* height) -> bool - { - auto wrapper = getWrapper (plugin); - if (wrapper->audioPluginEditor == nullptr) - return false; - - auto audioProcessorEditor = wrapper->audioPluginEditor->getAudioProcessorEditor(); - - const auto preferredSize = audioProcessorEditor->getPreferredSize(); - - if (! audioProcessorEditor->isResizable()) - { - *width = static_cast (preferredSize.getWidth()); - *height = static_cast (preferredSize.getHeight()); - } - else if (audioProcessorEditor->shouldPreserveAspectRatio()) - { - if (preferredSize.getWidth() > preferredSize.getHeight()) - *height = static_cast (*width * (preferredSize.getWidth() / static_cast (preferredSize.getHeight()))); - else - *width = static_cast (*height * (preferredSize.getHeight() / static_cast (preferredSize.getWidth()))); - } - - return true; - }; - - extensionGUI.set_size = [] (const clap_plugin_t* plugin, uint32_t width, uint32_t height) -> bool - { - auto wrapper = getWrapper (plugin); - if (wrapper->audioPluginEditor == nullptr) - return false; - - auto audioProcessorEditor = wrapper->audioPluginEditor->getAudioProcessorEditor(); - - if (! audioProcessorEditor->isResizable()) - { - const auto preferredSize = audioProcessorEditor->getPreferredSize(); - - width = static_cast (preferredSize.getWidth()); - height = static_cast (preferredSize.getHeight()); - } - - const auto scoped = wrapper->scopedHostEditorResizing(); - - wrapper->audioPluginEditor->setSize ({ static_cast (width), static_cast (height) }); - - return true; - }; - - extensionGUI.set_parent = [] (const clap_plugin_t* plugin, const clap_window_t* window) -> bool - { - jassert (std::string_view (window->api) == preferredApi); - - auto wrapper = getWrapper (plugin); - if (wrapper->audioPluginEditor == nullptr) - return false; - - auto audioProcessorEditor = wrapper->audioPluginEditor->getAudioProcessorEditor(); - if (audioProcessorEditor == nullptr) - return false; - - ComponentNative::Flags flags = ComponentNative::defaultFlags & ~ComponentNative::decoratedWindow; - - if (audioProcessorEditor->shouldRenderContinuous()) - flags.set (ComponentNative::renderContinuous); - - auto options = ComponentNative::Options() - .withFlags (flags) - .withResizableWindow (audioProcessorEditor->isResizable()); - - wrapper->audioPluginEditor->addToDesktop ( - options, -#if YUP_MAC - window->cocoa); -#elif YUP_WINDOWS - window->win32); -#elif YUP_LINUX - reinterpret_cast (window->x11)); -#else - nullptr); -#endif - - wrapper->audioPluginEditor->setVisible (true); - - audioProcessorEditor->attachedToNative(); - - return true; - }; - - extensionGUI.set_transient = [] (const clap_plugin_t* plugin, const clap_window_t* window) -> bool - { - return false; - }; - - extensionGUI.suggest_title = [] (const clap_plugin_t* plugin, const char* title) {}; - - extensionGUI.show = [] (const clap_plugin_t* plugin) -> bool - { - auto wrapper = getWrapper (plugin); - if (wrapper->audioPluginEditor == nullptr) - return false; - - wrapper->audioPluginEditor->setVisible (true); - return true; - }; - - extensionGUI.hide = [] (const clap_plugin_t* plugin) -> bool - { - auto wrapper = getWrapper (plugin); - if (wrapper->audioPluginEditor == nullptr) - return false; - - wrapper->audioPluginEditor->setVisible (false); - return true; - }; - - // ==== Setup extensions: host - hostParams = reinterpret_cast (host->get_extension (host, CLAP_EXT_PARAMS)); - hostState = reinterpret_cast (host->get_extension (host, CLAP_EXT_STATE)); - hostTail = reinterpret_cast (host->get_extension (host, CLAP_EXT_TAIL)); - hostLatency = reinterpret_cast (host->get_extension (host, CLAP_EXT_LATENCY)); - hostTimerSupport = reinterpret_cast (host->get_extension (host, CLAP_EXT_TIMER_SUPPORT)); - hostGUI = reinterpret_cast (host->get_extension (host, CLAP_EXT_GUI)); - - return true; -} - -//============================================================================== - -void AudioPluginProcessorCLAP::destroy() -{ - plugin.plugin_data = nullptr; - delete this; -} - -//============================================================================== - -bool AudioPluginProcessorCLAP::activate (float sampleRate, int samplesPerBlock) -{ -#if YUP_LINUX - if (instancesCount.fetch_add (1) == 0) - registerTimer (16, &guiTimerId); -#endif - - audioProcessor->setPlaybackConfiguration (sampleRate, samplesPerBlock); - return true; -} - -//============================================================================== - -void AudioPluginProcessorCLAP::deactivate() -{ - audioProcessor->releaseResources(); - -#if YUP_LINUX - if (instancesCount.fetch_sub (1) == 1) - unregisterTimer (guiTimerId); -#endif -} - -//============================================================================== - -bool AudioPluginProcessorCLAP::startProcessing() -{ - audioProcessor->suspendProcessing (false); - return true; -} - -//============================================================================== - -void AudioPluginProcessorCLAP::stopProcessing() -{ - audioProcessor->suspendProcessing (true); -} - -//============================================================================== - -void AudioPluginProcessorCLAP::reset() -{ - audioProcessor->flush(); // TODO - should we just call releaseResources()? -} - -//============================================================================== - -void AudioPluginProcessorCLAP::registerTimer (uint32_t periodMs, clap_id* timerId) -{ - if (hostTimerSupport != nullptr && hostTimerSupport->register_timer) - hostTimerSupport->register_timer (host, periodMs, timerId); -} - -void AudioPluginProcessorCLAP::unregisterTimer (clap_id timerId) -{ - if (hostTimerSupport != nullptr && hostTimerSupport->register_timer) - hostTimerSupport->unregister_timer (host, timerId); -} - -//============================================================================== - -const void* AudioPluginProcessorCLAP::getExtension (std::string_view id) -{ - if (id == CLAP_EXT_NOTE_PORTS) - return std::addressof (extensionNotePorts); - if (id == CLAP_EXT_AUDIO_PORTS) - return std::addressof (extensionAudioPorts); - if (id == CLAP_EXT_PARAMS) - return std::addressof (extensionParams); - if (id == CLAP_EXT_STATE) - return std::addressof (extensionState); - if (id == CLAP_EXT_TAIL) - return std::addressof (extensionTail); - if (id == CLAP_EXT_LATENCY) - return std::addressof (extensionLatency); - if (id == CLAP_EXT_TIMER_SUPPORT) - return std::addressof (extensionTimerSupport); - if (id == CLAP_EXT_GUI) - return std::addressof (extensionGUI); - - return nullptr; -} - -//============================================================================== - -const clap_plugin_t* AudioPluginProcessorCLAP::getPlugin() const -{ - return std::addressof (plugin); -} - -//============================================================================== - -void AudioPluginProcessorCLAP::editorResized() -{ - if (audioPluginEditor == nullptr || hostTriggeredResizing) - return; - - if (hostGUI != nullptr && hostGUI->request_resize != nullptr) - hostGUI->request_resize (host, audioPluginEditor->getWidth(), audioPluginEditor->getHeight()); -} - -ScopedValueSetter AudioPluginProcessorCLAP::scopedHostEditorResizing() -{ - return { hostTriggeredResizing, true }; -} - -//============================================================================== - -void AudioPluginEditorCLAP::resized() -{ - if (processorEditor == nullptr) - return; - - processorEditor->setBounds (getLocalBounds()); - - wrapper->editorResized(); -} - -} // namespace yup - -//============================================================================== - -static const clap_plugin_factory_t plugin_factory = [] -{ - clap_plugin_factory_t factory; - - factory.get_plugin_count = [] (const clap_plugin_factory* factory) -> uint32_t - { - return 1; - }; - - factory.get_plugin_descriptor = [] (const clap_plugin_factory* factory, uint32_t index) -> const clap_plugin_descriptor_t* - { - return index == 0 ? &yup::pluginDescriptor : nullptr; - }; - - factory.create_plugin = [] (const clap_plugin_factory* factory, const clap_host_t* host, const char* pluginId) -> const clap_plugin_t* - { - if (! clap_version_is_compatible (host->clap_version) || std::string_view (pluginId) != yup::pluginDescriptor.id) - return nullptr; - - auto wrapper = new yup::AudioPluginProcessorCLAP (host); - return wrapper->getPlugin(); - }; - - return factory; -}(); - -//============================================================================== - -extern "C" const CLAP_EXPORT clap_plugin_entry_t clap_entry = [] -{ - clap_plugin_entry_t plugin; - - plugin.clap_version = CLAP_VERSION_INIT; - - plugin.init = [] (const char* path) -> bool - { - yup::initialiseYup_GUI(); - yup::initialiseYup_Windowing(); - - return true; - }; - - plugin.deinit = [] - { - yup::shutdownYup_Windowing(); - yup::shutdownYup_GUI(); - }; - - plugin.get_factory = [] (const char* factoryId) -> const void* - { - if (std::string_view (factoryId) == CLAP_PLUGIN_FACTORY_ID) - return std::addressof (plugin_factory); - - return nullptr; - }; - - return plugin; -}(); +/* + ============================================================================== + + 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" + +#include "../common/yup_AudioPluginUtilities.h" + +#if ! defined(YUP_AUDIO_PLUGIN_ENABLE_CLAP) +#error "YUP_AUDIO_PLUGIN_ENABLE_CLAP must be defined" +#endif + +#include +#include +#include + +#include + +extern "C" yup::AudioProcessor* createPluginProcessor(); + +namespace yup +{ + +//============================================================================== + +std::optional clapEventToMidiMessage (const clap_event_header_t* event) +{ + const auto clampMidiChannel = [] (int channel) noexcept + { + return jlimit (1, 16, channel < 0 ? 1 : channel + 1); + }; + + const auto clampMidiValue = [] (double value) noexcept + { + if (! (value >= 0.0)) + return 0; + + if (value >= 127.0) + return 127; + + return static_cast (value); + }; + + const auto clampPitchBendValue = [] (double value) noexcept + { + if (! (value >= 0.0)) + return 0; + + if (value >= 16383.0) + return 16383; + + return static_cast (value); + }; + + switch (event->type) + { + case CLAP_EVENT_NOTE_ON: + { + const auto* noteEvent = reinterpret_cast (event); + if (! isPositiveAndBelow (noteEvent->key, 128)) + return std::nullopt; + + return MidiMessage::noteOn (clampMidiChannel (noteEvent->channel), + noteEvent->key, + static_cast (clampMidiValue (noteEvent->velocity * 127.0))); + } + + case CLAP_EVENT_NOTE_OFF: + { + const auto* noteEvent = reinterpret_cast (event); + if (! isPositiveAndBelow (noteEvent->key, 128)) + return std::nullopt; + + return MidiMessage::noteOff (clampMidiChannel (noteEvent->channel), + noteEvent->key, + static_cast (clampMidiValue (noteEvent->velocity * 127.0))); + } + + case CLAP_EVENT_NOTE_CHOKE: + { + const auto* noteEvent = reinterpret_cast (event); + if (! isPositiveAndBelow (noteEvent->key, 128)) + return std::nullopt; + + return MidiMessage::noteOff (clampMidiChannel (noteEvent->channel), noteEvent->key); + } + + case CLAP_EVENT_MIDI: + { + const auto* midiEvent = reinterpret_cast (event); + if (midiEvent->data[0] < 0x80) + return std::nullopt; + + const int messageLength = MidiMessage::getMessageLengthFromFirstByte (midiEvent->data[0]); + if (messageLength <= 0 || messageLength > 3) + return std::nullopt; + + for (int byteIndex = 1; byteIndex < messageLength; ++byteIndex) + if (midiEvent->data[byteIndex] >= 0x80) + return std::nullopt; + + return MidiMessage (midiEvent->data, messageLength); + } + + case CLAP_EVENT_NOTE_EXPRESSION: + { + const auto* ev = reinterpret_cast (event); + const int channel = clampMidiChannel (ev->channel); + + if (ev->expression_id == CLAP_NOTE_EXPRESSION_TUNING) + { + const int pitchBendValue = clampPitchBendValue (ev->value * 8192.0 + 8192.0); + return MidiMessage::pitchWheel (channel, pitchBendValue); + } + + if (ev->expression_id == CLAP_NOTE_EXPRESSION_PRESSURE) + return MidiMessage::channelPressureChange (channel, clampMidiValue (ev->value * 127.0)); + + if (ev->expression_id == CLAP_NOTE_EXPRESSION_BRIGHTNESS) + return MidiMessage::controllerEvent (channel, 74, clampMidiValue (ev->value * 127.0)); + + break; + } + + case CLAP_EVENT_MIDI_SYSEX: + { + const auto* sysexEvent = reinterpret_cast (event); + if (sysexEvent->buffer == nullptr || sysexEvent->size == 0) + return std::nullopt; + + return MidiMessage (sysexEvent->buffer, static_cast (sysexEvent->size)); + } + + 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 parameterIndex = audioProcessor.getParameterIndexByHostID (paramEvent->param_id); + auto parameters = audioProcessor.getParameters(); + if (! isPositiveAndBelow (parameterIndex, static_cast (parameters.size()))) + return; + + if (parameters[parameterIndex]->isReadOnly() + || parameters[parameterIndex]->isPerformingChangeGesture()) + { + return; + } + + parameters[parameterIndex]->setValue (static_cast (paramEvent->value)); +} + +bool addParameterModByCLAPEvent (AudioProcessor& processor, + ParameterChangeBuffer& changes, + const clap_event_param_mod_t* modEvent) +{ + if (modEvent->note_id != -1 || modEvent->key != -1) + return false; // per-voice modulation needs voice infrastructure + + const auto parameters = processor.getParameters(); + const auto parameterIndex = processor.getParameterIndexByHostID (modEvent->param_id); + + if (! isPositiveAndBelow (parameterIndex, static_cast (parameters.size()))) + return false; + + const auto& param = parameters[parameterIndex]; + + if (! param->isModulatable() || param->isReadOnly()) + return false; + + const auto modulatedValue = jlimit (param->getMinimumValue(), + param->getMaximumValue(), + param->getValue() + static_cast (modEvent->amount)); + + return changes.addChange (parameterIndex, + param->convertToNormalizedValue (modulatedValue), + static_cast (modEvent->header.time)); +} + +bool addParameterChangeByCLAPValue (AudioProcessor& processor, + ParameterChangeBuffer& changes, + uint32 hostParameterID, + float value, + int sampleOffset) +{ + const auto parameters = processor.getParameters(); + const auto parameterIndex = processor.getParameterIndexByHostID (hostParameterID); + + if (! isPositiveAndBelow (parameterIndex, static_cast (parameters.size()))) + return false; + + if (parameters[parameterIndex]->isReadOnly() + || parameters[parameterIndex]->isPerformingChangeGesture()) + { + return false; + } + + return changes.addChange (parameterIndex, + parameters[parameterIndex]->convertToNormalizedValue (value), + sampleOffset); +} + +clap_param_info_flags getCLAPParameterFlags (const AudioParameter& parameter) noexcept +{ + clap_param_info_flags flags = 0; + + if (parameter.isStepped() || parameter.isEnum()) + flags |= CLAP_PARAM_IS_STEPPED; + + if (parameter.isEnum()) + flags |= CLAP_PARAM_IS_ENUM; + + if (parameter.isReadOnly()) + flags |= CLAP_PARAM_IS_READONLY; + + if (parameter.isAutomatable() && ! parameter.isReadOnly()) + flags |= CLAP_PARAM_IS_AUTOMATABLE; + + if (parameter.isModulatable()) + flags |= CLAP_PARAM_IS_MODULATABLE; + + if (parameter.isPerNoteModulatable()) + flags |= CLAP_PARAM_IS_MODULATABLE | CLAP_PARAM_IS_MODULATABLE_PER_NOTE_ID; + + return flags; +} + +constexpr int clapWrapperStateMagic = 0x504c4359; // "YCLP" +constexpr int clapWrapperStateVersion = 1; + +static bool writeAllToCLAPStream (const clap_ostream_t* stream, const void* data, size_t dataSize) +{ + const auto* bytes = static_cast (data); + size_t bytesWritten = 0; + + while (bytesWritten < dataSize) + { + const auto remaining = dataSize - bytesWritten; + const auto written = stream->write (stream, bytes + bytesWritten, static_cast (remaining)); + + if (written <= 0 || static_cast (written) > remaining) + return false; + + bytesWritten += static_cast (written); + } + + return true; +} + +//============================================================================== + +/* +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 == nullptr || process->transport == nullptr) + return {}; + + const auto& transport = *process->transport; + PositionInfo result; + + if ((transport.flags & CLAP_TRANSPORT_HAS_SECONDS_TIMELINE) != 0) + { + const auto timeInSeconds = transport.song_pos_seconds / (double) CLAP_SECTIME_FACTOR; + result.setTimeInSeconds (timeInSeconds); + result.setTimeInSamples ((int64) (sampleRate * timeInSeconds)); + } + + if ((transport.flags & CLAP_TRANSPORT_HAS_BEATS_TIMELINE) != 0) + { + result.setPpqPosition (transport.song_pos_beats / (double) CLAP_BEATTIME_FACTOR); + result.setPpqPositionOfLastBarStart (transport.bar_start / (double) CLAP_BEATTIME_FACTOR); + result.setBarCount (transport.bar_number); + } + + if ((transport.flags & CLAP_TRANSPORT_HAS_TIME_SIGNATURE) != 0) + result.setTimeSignature (TimeSignature { transport.tsig_num, transport.tsig_denom }); + + if ((transport.flags & CLAP_TRANSPORT_HAS_TEMPO) != 0) + result.setBpm (transport.tempo); + + result.setIsPlaying ((transport.flags & CLAP_TRANSPORT_IS_PLAYING) != 0); + result.setIsRecording ((transport.flags & CLAP_TRANSPORT_IS_RECORDING) != 0); + result.setIsLooping ((transport.flags & CLAP_TRANSPORT_IS_LOOP_ACTIVE) != 0); + + if ((transport.flags & CLAP_TRANSPORT_IS_LOOP_ACTIVE) != 0) + result.setLoopPoints (LoopPoints { + transport.loop_start_beats / (double) CLAP_BEATTIME_FACTOR, + transport.loop_end_beats / (double) CLAP_BEATTIME_FACTOR }); + + result.setFrameRate (AudioPlayHead::fpsUnknown); + + return result; + } + +private: + const clap_process_t* process = nullptr; + float sampleRate = 44100.0f; +}; + +//============================================================================== + +class AudioPluginEditorCLAP final : public Component +{ +public: + AudioPluginEditorCLAP (AudioPluginProcessorCLAP* wrapper, AudioProcessorEditor* editor) + : wrapper (wrapper) + , processorEditor (editor) + { + addAndMakeVisible (*processorEditor); + } + + ~AudioPluginEditorCLAP() override + { + if (processorEditor != nullptr) + { + setVisible (false); + removeFromDesktop(); + + removeChildComponent (processorEditor.get()); + processorEditor.reset(); + } + } + + AudioProcessorEditor* getAudioProcessorEditor() { return processorEditor.get(); } + + void contentScaleChanged (float dpiScale) override; + + void resized() override; + +private: + ScopedYupInitialiser_Windowing scopeInitialiser; + AudioPluginProcessorCLAP* wrapper = nullptr; + std::unique_ptr processorEditor; +}; + +//============================================================================== + +class AudioPluginProcessorCLAP final + : private AudioParameter::Listener + , private AudioProcessor::Listener +{ +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: + void addParameterListeners(); + void removeParameterListeners(); + bool isValidProcessorParameterIndex (int indexInContainer) const; + + void parameterValueChanged (const AudioParameter::Ptr& parameter, int indexInContainer) override; + void parameterGestureBegin (const AudioParameter::Ptr& parameter, int indexInContainer) override; + void parameterGestureEnd (const AudioParameter::Ptr& parameter, int indexInContainer) override; + + void audioProcessorChanged (AudioProcessor* processor, const AudioProcessor::ChangeDetails& details) override; + + void enqueueParameterEvent (uint16 eventType, clap_id parameterId, double value = 0.0) noexcept; + void drainParameterEvents (const clap_output_events_t* out) noexcept; + void requestParameterFlush() const noexcept; + void requestMainThreadCallback() const noexcept; + void handleMainThreadNotifications() noexcept; + void handleAudioThreadNotifications() noexcept; + + ScopedYupInitialiser_GUI scopeInitialiser; + + 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; + clap_plugin_render_t extensionRender; + clap_plugin_voice_info_t extensionVoiceInfo; + + 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; + ParameterChangeBuffer paramChangeBuffer; + ParameterChangeBuffer hostParameterChangeBuffer; + std::vector listenedParameters; + std::vector outputChannelsFloat; + std::vector outputChannelsDouble; + bool isBypassed = false; + std::atomic isActive { false }; + std::atomic isInsideProcessBlock { false }; + std::atomic callLatencyChangeOnNextActivate { false }; + std::atomic tailChangedPending { false }; + std::atomic stateDirtyPending { false }; + std::atomic parameterRescanFlagsPending { 0 }; + + struct QueuedParameterEvent + { + uint16 eventType = 0; + clap_id parameterId = CLAP_INVALID_ID; + double value = 0.0; + }; + + static constexpr int parameterEventQueueSize = 4096; + AbstractFifo parameterEventFifo { parameterEventQueueSize }; + std::array parameterEvents {}; + + 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; + + wrapper->handleAudioThreadNotifications(); + wrapper->drainParameterEvents (process->out_events); + + 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()); + + // Process incoming parameter and MIDI events (CLAP guarantees time-sorted order) + midiBuffer.clear(); + wrapper->paramChangeBuffer.clear(); + wrapper->hostParameterChangeBuffer.clear(); + + bool bypassed = wrapper->isBypassed; + const auto bypassParameterID = getBypassHostParameterID (audioProcessor); + + 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 (event->type == CLAP_EVENT_PARAM_VALUE) + { + const auto* paramEvent = reinterpret_cast (event); + + if (paramEvent->param_id == bypassParameterID) + { + bypassed = paramEvent->value >= 0.5; + wrapper->isBypassed = bypassed; + continue; + } + + addParameterChangeByCLAPValue (audioProcessor, + wrapper->paramChangeBuffer, + paramEvent->param_id, + static_cast (paramEvent->value), + static_cast (event->time)); + + addParameterChangeByCLAPValue (audioProcessor, + wrapper->hostParameterChangeBuffer, + paramEvent->param_id, + static_cast (paramEvent->value), + static_cast (event->time)); + } + else if (event->type == CLAP_EVENT_PARAM_MOD) + { + const auto* modEvent = reinterpret_cast (event); + addParameterModByCLAPEvent (audioProcessor, wrapper->paramChangeBuffer, modEvent); + } + else if (auto convertedEvent = clapEventToMidiMessage (event)) + { + midiBuffer.addEvent (*convertedEvent, static_cast (event->time)); + } + } + + // CLAP events arrive sorted — no sort needed; apply final values for backward compat + applyParameterChangesToProcessor (audioProcessor, wrapper->hostParameterChangeBuffer); + + AudioPluginPlayHeadCLAP playHead (audioProcessor.getSampleRate(), process); + auto* const playHeadPtr = process->transport != nullptr ? &playHead : nullptr; + + const bool useDoublePrecision = audioProcessor.supportsDoublePrecisionProcessing() + && process->audio_outputs_count > 0 + && process->audio_outputs[0].data64 != nullptr; + + if (useDoublePrecision) + { + // Copy input audio into output buffers for effect processors (double) + for (uint32_t busIdx = 0; busIdx < std::min (process->audio_inputs_count, process->audio_outputs_count); ++busIdx) + { + const auto& inBus = process->audio_inputs[busIdx]; + const auto& outBus = process->audio_outputs[busIdx]; + const uint32_t chCount = std::min (inBus.channel_count, outBus.channel_count); + + for (uint32_t ch = 0; ch < chCount; ++ch) + { + const auto* in = inBus.data64[ch]; + auto* out = outBus.data64[ch]; + if (in != out) + std::memcpy (out, in, process->frames_count * sizeof (double)); + } + } + + wrapper->outputChannelsDouble.clear(); + for (uint32_t busIdx = 0; busIdx < process->audio_outputs_count; ++busIdx) + for (uint32_t ch = 0; ch < process->audio_outputs[busIdx].channel_count; ++ch) + wrapper->outputChannelsDouble.push_back (process->audio_outputs[busIdx].data64[ch]); + + AudioBuffer audioBuffer (wrapper->outputChannelsDouble.data(), + static_cast (wrapper->outputChannelsDouble.size()), + 0, + static_cast (process->frames_count)); + + AudioProcessContext context { audioBuffer, midiBuffer, wrapper->paramChangeBuffer, playHeadPtr }; + + wrapper->isInsideProcessBlock.store (true); + processAudioBlock (audioProcessor, context, bypassed); + wrapper->isInsideProcessBlock.store (false); + } + else + { + // Copy input audio into output buffers for effect processors (float) + for (uint32_t busIdx = 0; busIdx < std::min (process->audio_inputs_count, process->audio_outputs_count); ++busIdx) + { + const auto& inBus = process->audio_inputs[busIdx]; + const auto& outBus = process->audio_outputs[busIdx]; + const uint32_t chCount = std::min (inBus.channel_count, outBus.channel_count); + + for (uint32_t ch = 0; ch < chCount; ++ch) + { + const auto* in = inBus.data32[ch]; + auto* out = outBus.data32[ch]; + if (in != out) + std::memcpy (out, in, process->frames_count * sizeof (float)); + } + } + + wrapper->outputChannelsFloat.clear(); + for (uint32_t busIdx = 0; busIdx < process->audio_outputs_count; ++busIdx) + for (uint32_t ch = 0; ch < process->audio_outputs[busIdx].channel_count; ++ch) + wrapper->outputChannelsFloat.push_back (process->audio_outputs[busIdx].data32[ch]); + + AudioSampleBuffer audioBuffer (wrapper->outputChannelsFloat.data(), + static_cast (wrapper->outputChannelsFloat.size()), + 0, + static_cast (process->frames_count)); + + AudioProcessContext context { audioBuffer, midiBuffer, wrapper->paramChangeBuffer, playHeadPtr }; + + wrapper->isInsideProcessBlock.store (true); + processAudioBlock (audioProcessor, context, bypassed); + wrapper->isInsideProcessBlock.store (false); + } + + wrapper->handleAudioThreadNotifications(); + wrapper->drainParameterEvents (process->out_events); + + // Send output events back to host + if (process->out_events != nullptr) + { + for (const MidiMessageMetadata metadata : midiBuffer) + { + const auto& message = metadata.getMessage(); + + if (message.isNoteOff()) + { + clap_event_note_t ev = {}; + ev.header.size = sizeof (ev); + ev.header.time = static_cast (metadata.samplePosition); + ev.header.space_id = CLAP_CORE_EVENT_SPACE_ID; + ev.header.type = CLAP_EVENT_NOTE_END; + ev.header.flags = 0; + ev.note_id = -1; + ev.key = message.getNoteNumber(); + ev.channel = message.getChannel() - 1; + ev.port_index = 0; + process->out_events->try_push (process->out_events, &ev.header); + } + else if (message.getRawDataSize() > 0 && message.getRawDataSize() <= 3) + { + clap_event_midi_t ev = {}; + ev.header.size = sizeof (ev); + ev.header.time = static_cast (metadata.samplePosition); + ev.header.space_id = CLAP_CORE_EVENT_SPACE_ID; + ev.header.type = CLAP_EVENT_MIDI; + ev.header.flags = 0; + ev.port_index = 0; + std::memcpy (ev.data, message.getRawData(), static_cast (message.getRawDataSize())); + process->out_events->try_push (process->out_events, &ev.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) + { + getWrapper (plugin)->handleMainThreadNotifications(); + }; +} + +//============================================================================== + +AudioPluginProcessorCLAP::~AudioPluginProcessorCLAP() +{ + endActiveParameterGestures (audioProcessor.get()); +} + +//============================================================================== + +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() + 1); + }; + + 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; + + if (index == static_cast (parameters.size())) + { + information->id = getBypassHostParameterID (*wrapper->audioProcessor); + information->flags = CLAP_PARAM_IS_AUTOMATABLE | CLAP_PARAM_IS_STEPPED | CLAP_PARAM_IS_BYPASS; + information->min_value = 0.0; + information->max_value = 1.0; + information->default_value = 0.0; + std::snprintf (information->name, sizeof (information->name), "%s", "Bypass"); + return true; + } + + auto& parameter = parameters[index]; + + information->id = parameter->getHostParameterID(); + information->cookie = parameter.get(); + information->flags = getCLAPParameterFlags (*parameter); + information->min_value = parameter->getMinimumValue(); + information->max_value = parameter->getMaximumValue(); + information->default_value = parameter->getDefaultValue(); + parameter->getName().copyToUTF8 (information->name, CLAP_NAME_SIZE); + parameter->getModulePath().copyToUTF8 (information->module, CLAP_PATH_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(); + + const auto parameterIndex = wrapper->audioProcessor->getParameterIndexByHostID (parameterId); + if (! isPositiveAndBelow (parameterIndex, static_cast (parameters.size()))) + { + if (parameterId == getBypassHostParameterID (*wrapper->audioProcessor)) + { + *value = wrapper->isBypassed ? 1.0 : 0.0; + return true; + } + + return false; + } + + *value = parameters[parameterIndex]->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(); + + const auto parameterIndex = wrapper->audioProcessor->getParameterIndexByHostID (parameterId); + if (! isPositiveAndBelow (parameterIndex, static_cast (parameters.size()))) + { + if (parameterId == getBypassHostParameterID (*wrapper->audioProcessor)) + { + const auto text = value >= 0.5 ? String ("On") : String ("Off"); + text.copyToUTF8 (display, size); + return true; + } + + return false; + } + + const auto text = parameters[parameterIndex]->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(); + + const auto parameterIndex = wrapper->audioProcessor->getParameterIndexByHostID (parameterId); + if (! isPositiveAndBelow (parameterIndex, static_cast (parameters.size()))) + { + if (parameterId == getBypassHostParameterID (*wrapper->audioProcessor)) + { + const String text (display); + *value = (text == "On" || text == "1") ? 1.0 : 0.0; + return true; + } + + return false; + } + + *value = static_cast (parameters[parameterIndex]->convertFromString (display)); + + return true; + }; + + extensionParams.flush = [] (const clap_plugin_t* plugin, const clap_input_events_t* in, const clap_output_events_t* out) + { + auto wrapper = getWrapper (plugin); + + wrapper->drainParameterEvents (out); + + if (in == nullptr) + return; + + const uint32_t count = in->size (in); + const auto bypassParameterID = getBypassHostParameterID (*wrapper->audioProcessor); + + for (uint32_t i = 0; i < count; ++i) + { + const clap_event_header_t* event = in->get (in, i); + + if (event->space_id != CLAP_CORE_EVENT_SPACE_ID) + continue; + + if (event->type == CLAP_EVENT_PARAM_VALUE) + { + const auto* paramEvent = reinterpret_cast (event); + + if (paramEvent->param_id == bypassParameterID) + { + wrapper->isBypassed = paramEvent->value >= 0.5; + continue; + } + + clapEventToParameterChange (event, *wrapper->audioProcessor); + } + else if (event->type == CLAP_EVENT_PARAM_MOD) + { + // Modulation is transient audio-block data; there is no processor context during flush(). + } + } + }; + + // ==== Setup extensions: note ports + extensionNotePorts.count = [] (const clap_plugin_t* plugin, bool isInput) -> uint32_t + { + auto wrapper = getWrapper (plugin); + const auto& busses = isInput + ? wrapper->audioProcessor->getBusLayout().getInputBuses() + : wrapper->audioProcessor->getBusLayout().getOutputBuses(); + + uint32_t count = 0; + for (const auto& bus : busses) + if (bus.getType() == AudioBus::Type::MIDI) + ++count; + + // Fallback: synths with no declared MIDI input bus always get one + if (isInput && count == 0 && YupPlugin_IsSynth) + return 1; + + return count; + }; + + extensionNotePorts.get = [] (const clap_plugin_t* plugin, uint32_t index, bool isInput, clap_note_port_info_t* info) -> bool + { + auto wrapper = getWrapper (plugin); + const auto& busses = isInput + ? wrapper->audioProcessor->getBusLayout().getInputBuses() + : wrapper->audioProcessor->getBusLayout().getOutputBuses(); + + uint32_t midiIndex = 0; + for (const auto& bus : busses) + { + if (bus.getType() != AudioBus::Type::MIDI) + continue; + + if (midiIndex == index) + { + info->id = index; + info->supported_dialects = CLAP_NOTE_DIALECT_CLAP | CLAP_NOTE_DIALECT_MIDI; + info->preferred_dialect = CLAP_NOTE_DIALECT_CLAP; + bus.getName().copyToUTF8 (info->name, sizeof (info->name)); + return true; + } + + ++midiIndex; + } + + // Fallback port for synths without declared MIDI buses + if (isInput && index == 0 && midiIndex == 0 && YupPlugin_IsSynth) + { + info->id = 0; + info->supported_dialects = CLAP_NOTE_DIALECT_CLAP | CLAP_NOTE_DIALECT_MIDI; + info->preferred_dialect = CLAP_NOTE_DIALECT_CLAP; + std::snprintf (info->name, sizeof (info->name), "%s", "Midi In"); + return true; + } + + return false; + }; + + // ==== Setup extensions: audio ports + extensionAudioPorts.count = [] (const clap_plugin_t* plugin, bool isInput) -> uint32_t + { + auto wrapper = getWrapper (plugin); + auto* audioProcessor = wrapper->audioProcessor.get(); + + Span busses = isInput + ? audioProcessor->getBusLayout().getInputBuses() + : audioProcessor->getBusLayout().getOutputBuses(); + + uint32_t count = 0; + for (const auto& bus : busses) + if (bus.getType() == AudioBus::Type::Audio) + ++count; + + return count; + }; + + extensionAudioPorts.get = [] (const clap_plugin_t* plugin, uint32_t index, bool isInput, clap_audio_port_info_t* info) -> bool + { + auto wrapper = getWrapper (plugin); + auto* audioProcessor = wrapper->audioProcessor.get(); + + Span busses = isInput + ? audioProcessor->getBusLayout().getInputBuses() + : audioProcessor->getBusLayout().getOutputBuses(); + + const AudioBus* audioBus = nullptr; + uint32_t audioBusIndex = 0; + + for (const auto& bus : busses) + { + if (bus.getType() != AudioBus::Type::Audio) + continue; + + if (audioBusIndex == index) + { + audioBus = &bus; + break; + } + + ++audioBusIndex; + } + + if (audioBus == nullptr) + return false; + + info->id = index; + info->channel_count = audioBus->getNumChannels(); + + uint32_t flags = (index == 0) ? CLAP_AUDIO_PORT_IS_MAIN : 0; + if (audioProcessor->supportsDoublePrecisionProcessing()) + flags |= CLAP_AUDIO_PORT_SUPPORTS_64BITS | CLAP_AUDIO_PORT_PREFERS_64BITS | CLAP_AUDIO_PORT_REQUIRES_COMMON_SAMPLE_SIZE; + info->flags = flags; + + info->port_type = audioBus->isStereo() ? CLAP_PORT_STEREO : CLAP_PORT_MONO; + + // For output ports, advertise in-place processing when a corresponding input bus exists + if (! isInput) + { + uint32_t inputAudioCount = 0; + for (const auto& bus : audioProcessor->getBusLayout().getInputBuses()) + if (bus.getType() == AudioBus::Type::Audio) + ++inputAudioCount; + info->in_place_pair = (inputAudioCount > index) ? static_cast (index) : CLAP_INVALID_ID; + } + else + { + info->in_place_pair = CLAP_INVALID_ID; + } + + audioBus->getName().copyToUTF8 (info->name, sizeof (info->name)); + + return true; + }; + + // ==== Setup extensions: state + extensionState.save = [] (const clap_plugin_t* plugin, const clap_ostream_t* stream) -> bool + { + auto wrapper = getWrapper (plugin); + MemoryBlock data; + + wrapper->audioProcessor->suspendProcessing (true); + const bool saved = wrapper->audioProcessor->saveStateIntoMemory (data).wasOk(); + wrapper->audioProcessor->suspendProcessing (false); + + const auto wrapperState = writeWrapperBypassState (clapWrapperStateMagic, + clapWrapperStateVersion, + wrapper->isBypassed, + data, + saved); + + return writeAllToCLAPStream (stream, wrapperState.getData(), wrapperState.getSize()); + }; + + extensionState.load = [] (const clap_plugin_t* plugin, const clap_istream_t* stream) -> bool + { + auto wrapper = getWrapper (plugin); + MemoryBlock data; + + char buf[4096]; + for (;;) + { + const int64_t n = stream->read (stream, buf, sizeof (buf)); + if (n < 0) + return false; + + if (n == 0) + break; + + data.append (buf, static_cast (n)); + } + + if (data.isEmpty()) + return false; + + const auto wrapperState = readWrapperBypassState (data, clapWrapperStateMagic, clapWrapperStateVersion); + if (wrapperState.hasWrapperState) + { + wrapper->isBypassed = wrapperState.isBypassed; + + if (! wrapperState.hasProcessorState) + return true; + } + + const auto& processorState = wrapperState.hasWrapperState ? wrapperState.processorState : data; + + wrapper->audioProcessor->suspendProcessing (true); + const bool ok = wrapper->audioProcessor->loadStateFromMemory (processorState).wasOk(); + wrapper->audioProcessor->suspendProcessing (false); + + return ok; + }; + + // ==== Setup extensions: tail + extensionTail.get = [] (const clap_plugin_t* plugin) -> uint32_t + { + auto wrapper = getWrapper (plugin); + return static_cast (wrapper->audioProcessor->getTailSamples()); + }; + + // ==== Setup extensions: latency + extensionLatency.get = [] (const clap_plugin_t* plugin) -> uint32_t + { + auto wrapper = getWrapper (plugin); + return static_cast (wrapper->audioProcessor->getLatencySamples()); + }; + + // ==== Setup extensions: timer support + extensionTimerSupport.on_timer = [] (const clap_plugin_t* plugin, clap_id timerId) + { +#if YUP_LINUX + if (auto wrapper = getWrapper (plugin); wrapper->guiTimerId == timerId) + MessageManager::getInstance()->runDispatchLoopUntil (10); +#endif + }; + + // ==== Setup extensions: render + extensionRender.has_hard_realtime_requirement = [] (const clap_plugin_t*) -> bool + { + return false; + }; + + extensionRender.set = [] (const clap_plugin_t* plugin, clap_plugin_render_mode mode) -> bool + { + getWrapper (plugin)->audioProcessor->setOfflineProcessing (mode == CLAP_RENDER_OFFLINE); + return true; + }; + + // ==== Setup extensions: voice info + extensionVoiceInfo.get = [] (const clap_plugin_t* plugin, clap_voice_info_t* info) -> bool + { + const int voices = getWrapper (plugin)->audioProcessor->getNumVoices(); + if (voices <= 0) + return false; + + info->voice_count = static_cast (voices); + info->voice_capacity = static_cast (voices); + info->flags = CLAP_VOICE_INFO_SUPPORTS_OVERLAPPING_NOTES; + return true; + }; + + // ==== Setup extensions: gui + extensionGUI.is_api_supported = [] (const clap_plugin_t* plugin, const char* api, bool isFloating) -> bool + { + auto wrapper = getWrapper (plugin); + if (wrapper->audioProcessor == nullptr || ! wrapper->audioProcessor->hasEditor()) + return false; + + return std::string_view (api) == preferredApi && ! isFloating; + }; + + extensionGUI.get_preferred_api = [] (const clap_plugin_t* plugin, const char** api, bool* isFloating) -> bool + { + *api = preferredApi; + *isFloating = false; + return true; + }; + + extensionGUI.create = [] (const clap_plugin_t* plugin, const char* api, bool isFloating) -> bool + { + if (api == nullptr || std::string_view (api) != preferredApi || isFloating) + return false; + + auto wrapper = getWrapper (plugin); + + auto processorEditor = wrapper->audioProcessor->createEditor(); + if (processorEditor == nullptr) + return false; + + wrapper->audioPluginEditor = std::make_unique (wrapper, processorEditor); + + if (isFloating) + { + auto audioProcessorEditor = wrapper->audioPluginEditor->getAudioProcessorEditor(); + if (audioProcessorEditor == nullptr) + return false; + + ComponentNative::Flags flags = ComponentNative::defaultFlags; + + if (audioProcessorEditor->shouldRenderContinuous()) + flags.set (ComponentNative::renderContinuous); + + auto options = ComponentNative::Options() + .withFlags (flags) + .withResizableWindow (audioProcessorEditor->isResizable()); + + wrapper->audioPluginEditor->addToDesktop (options); + wrapper->audioPluginEditor->setVisible (true); + + audioProcessorEditor->attachedToNative(); + } + + return true; + }; + + extensionGUI.destroy = [] (const clap_plugin_t* plugin) + { + auto wrapper = getWrapper (plugin); + endActiveParameterGestures (wrapper->audioProcessor.get()); + wrapper->audioPluginEditor.reset(); + }; + + extensionGUI.set_scale = [] (const clap_plugin_t* plugin, double scale) -> bool + { + auto wrapper = getWrapper (plugin); + if (wrapper->audioPluginEditor == nullptr) + return false; + + wrapper->audioPluginEditor->contentScaleChanged (static_cast (scale)); + return true; + }; + + extensionGUI.get_size = [] (const clap_plugin_t* plugin, uint32_t* width, uint32_t* height) -> bool + { + auto wrapper = getWrapper (plugin); + if (wrapper->audioPluginEditor == nullptr) + return false; + + auto audioProcessorEditor = wrapper->audioPluginEditor->getAudioProcessorEditor(); + + if (audioProcessorEditor->isResizable() && audioProcessorEditor->getWidth() != 0) + { + *width = static_cast (audioProcessorEditor->getWidth()); + *height = static_cast (audioProcessorEditor->getHeight()); + } + else + { + *width = static_cast (audioProcessorEditor->getPreferredSize().getWidth()); + *height = static_cast (audioProcessorEditor->getPreferredSize().getHeight()); + } + + return true; + }; + + extensionGUI.can_resize = [] (const clap_plugin_t* plugin) -> bool + { + auto wrapper = getWrapper (plugin); + if (wrapper->audioPluginEditor == nullptr) + return false; + + return wrapper->audioPluginEditor->getAudioProcessorEditor()->isResizable(); + }; + + extensionGUI.get_resize_hints = [] (const clap_plugin_t* plugin, clap_gui_resize_hints_t* hints) -> bool + { + auto wrapper = getWrapper (plugin); + if (wrapper->audioPluginEditor == nullptr) + return false; + + auto audioProcessorEditor = wrapper->audioPluginEditor->getAudioProcessorEditor(); + + hints->can_resize_horizontally = audioProcessorEditor->isResizable(); + hints->can_resize_vertically = audioProcessorEditor->isResizable(); + hints->preserve_aspect_ratio = audioProcessorEditor->shouldPreserveAspectRatio(); + hints->aspect_ratio_width = audioProcessorEditor->getPreferredSize().getWidth(); + hints->aspect_ratio_height = audioProcessorEditor->getPreferredSize().getHeight(); + + return true; + }; + + extensionGUI.adjust_size = [] (const clap_plugin_t* plugin, uint32_t* width, uint32_t* height) -> bool + { + auto wrapper = getWrapper (plugin); + if (wrapper->audioPluginEditor == nullptr) + return false; + + auto audioProcessorEditor = wrapper->audioPluginEditor->getAudioProcessorEditor(); + + const auto preferredSize = audioProcessorEditor->getPreferredSize(); + + if (! audioProcessorEditor->isResizable()) + { + *width = static_cast (preferredSize.getWidth()); + *height = static_cast (preferredSize.getHeight()); + } + else if (audioProcessorEditor->shouldPreserveAspectRatio()) + { + if (preferredSize.getWidth() > preferredSize.getHeight()) + *height = static_cast (*width * (preferredSize.getWidth() / static_cast (preferredSize.getHeight()))); + else + *width = static_cast (*height * (preferredSize.getHeight() / static_cast (preferredSize.getWidth()))); + } + + return true; + }; + + extensionGUI.set_size = [] (const clap_plugin_t* plugin, uint32_t width, uint32_t height) -> bool + { + auto wrapper = getWrapper (plugin); + if (wrapper->audioPluginEditor == nullptr) + return false; + + auto audioProcessorEditor = wrapper->audioPluginEditor->getAudioProcessorEditor(); + + if (! audioProcessorEditor->isResizable()) + { + const auto preferredSize = audioProcessorEditor->getPreferredSize(); + + width = static_cast (preferredSize.getWidth()); + height = static_cast (preferredSize.getHeight()); + } + + const auto scoped = wrapper->scopedHostEditorResizing(); + + wrapper->audioPluginEditor->setSize ({ static_cast (width), static_cast (height) }); + + return true; + }; + + extensionGUI.set_parent = [] (const clap_plugin_t* plugin, const clap_window_t* window) -> bool + { + jassert (std::string_view (window->api) == preferredApi); + + auto wrapper = getWrapper (plugin); + if (wrapper->audioPluginEditor == nullptr) + return false; + + auto audioProcessorEditor = wrapper->audioPluginEditor->getAudioProcessorEditor(); + if (audioProcessorEditor == nullptr) + return false; + + ComponentNative::Flags flags = ComponentNative::defaultFlags & ~ComponentNative::decoratedWindow; + + if (audioProcessorEditor->shouldRenderContinuous()) + flags.set (ComponentNative::renderContinuous); + + auto options = ComponentNative::Options() + .withFlags (flags) + .withResizableWindow (audioProcessorEditor->isResizable()); + + wrapper->audioPluginEditor->addToDesktop ( + options, +#if YUP_MAC + window->cocoa); +#elif YUP_WINDOWS + window->win32); +#elif YUP_LINUX + reinterpret_cast (window->x11)); +#else + nullptr); +#endif + + wrapper->audioPluginEditor->setVisible (true); + + audioProcessorEditor->attachedToNative(); + + return true; + }; + + extensionGUI.set_transient = [] (const clap_plugin_t* plugin, const clap_window_t* window) -> bool + { + return false; + }; + + extensionGUI.suggest_title = [] (const clap_plugin_t* plugin, const char* title) {}; + + extensionGUI.show = [] (const clap_plugin_t* plugin) -> bool + { + auto wrapper = getWrapper (plugin); + if (wrapper->audioPluginEditor == nullptr) + return false; + + wrapper->audioPluginEditor->setVisible (true); + return true; + }; + + extensionGUI.hide = [] (const clap_plugin_t* plugin) -> bool + { + auto wrapper = getWrapper (plugin); + if (wrapper->audioPluginEditor == nullptr) + return false; + + wrapper->audioPluginEditor->setVisible (false); + return true; + }; + + // ==== Setup extensions: host + hostParams = reinterpret_cast (host->get_extension (host, CLAP_EXT_PARAMS)); + hostState = reinterpret_cast (host->get_extension (host, CLAP_EXT_STATE)); + hostTail = reinterpret_cast (host->get_extension (host, CLAP_EXT_TAIL)); + hostLatency = reinterpret_cast (host->get_extension (host, CLAP_EXT_LATENCY)); + hostTimerSupport = reinterpret_cast (host->get_extension (host, CLAP_EXT_TIMER_SUPPORT)); + hostGUI = reinterpret_cast (host->get_extension (host, CLAP_EXT_GUI)); + + audioProcessor->addListener (this); + addParameterListeners(); + +#if YUP_LINUX + if (instancesCount.fetch_add (1) == 0) + registerTimer (16, &guiTimerId); +#endif + + return true; +} + +//============================================================================== + +void AudioPluginProcessorCLAP::destroy() +{ + removeParameterListeners(); + + if (audioProcessor != nullptr) + audioProcessor->removeListener (this); + +#if YUP_LINUX + if (instancesCount.fetch_sub (1) == 1) + unregisterTimer (guiTimerId); +#endif + + plugin.plugin_data = nullptr; + delete this; +} + +//============================================================================== + +bool AudioPluginProcessorCLAP::activate (float sampleRate, int samplesPerBlock) +{ + audioProcessor->setPlaybackConfiguration (sampleRate, samplesPerBlock); + + if (callLatencyChangeOnNextActivate.exchange (false) + && hostLatency != nullptr + && hostLatency->changed != nullptr) + { + hostLatency->changed (host); + } + + midiEvents.ensureSize (4096); + paramChangeBuffer.reserve (getDefaultParameterChangeCapacity (*audioProcessor)); + hostParameterChangeBuffer.reserve (getDefaultParameterChangeCapacity (*audioProcessor)); + + const int totalOutputChannels = getTotalAudioOutputChannels (*audioProcessor); + outputChannelsFloat.reserve (static_cast (totalOutputChannels)); + outputChannelsDouble.reserve (static_cast (totalOutputChannels)); + + isActive.store (true); + + return true; +} + +//============================================================================== + +void AudioPluginProcessorCLAP::deactivate() +{ + isActive.store (false); + audioProcessor->releaseResources(); +} + +//============================================================================== + +bool AudioPluginProcessorCLAP::startProcessing() +{ + audioProcessor->suspendProcessing (false); + return true; +} + +//============================================================================== + +void AudioPluginProcessorCLAP::stopProcessing() +{ + audioProcessor->suspendProcessing (true); +} + +//============================================================================== + +void AudioPluginProcessorCLAP::reset() +{ + audioProcessor->flush(); // TODO - should we just call releaseResources()? +} + +//============================================================================== + +void AudioPluginProcessorCLAP::registerTimer (uint32_t periodMs, clap_id* timerId) +{ + if (hostTimerSupport != nullptr && hostTimerSupport->register_timer) + hostTimerSupport->register_timer (host, periodMs, timerId); +} + +void AudioPluginProcessorCLAP::unregisterTimer (clap_id timerId) +{ + if (hostTimerSupport != nullptr && hostTimerSupport->register_timer) + hostTimerSupport->unregister_timer (host, timerId); +} + +//============================================================================== + +const void* AudioPluginProcessorCLAP::getExtension (std::string_view id) +{ + if (id == CLAP_EXT_NOTE_PORTS) + return std::addressof (extensionNotePorts); + if (id == CLAP_EXT_AUDIO_PORTS) + return std::addressof (extensionAudioPorts); + if (id == CLAP_EXT_PARAMS) + return std::addressof (extensionParams); + if (id == CLAP_EXT_STATE) + return std::addressof (extensionState); + if (id == CLAP_EXT_TAIL) + return std::addressof (extensionTail); + if (id == CLAP_EXT_LATENCY) + return std::addressof (extensionLatency); + if (id == CLAP_EXT_TIMER_SUPPORT) + return std::addressof (extensionTimerSupport); + if (id == CLAP_EXT_GUI) + return std::addressof (extensionGUI); + if (id == CLAP_EXT_RENDER) + return std::addressof (extensionRender); + if (id == CLAP_EXT_VOICE_INFO) + return audioProcessor->getNumVoices() > 0 ? std::addressof (extensionVoiceInfo) : nullptr; + + return nullptr; +} + +//============================================================================== + +const clap_plugin_t* AudioPluginProcessorCLAP::getPlugin() const +{ + return std::addressof (plugin); +} + +//============================================================================== + +void AudioPluginProcessorCLAP::addParameterListeners() +{ + removeParameterListeners(); + + if (audioProcessor == nullptr) + return; + + for (const auto& parameter : audioProcessor->getParameters()) + { + parameter->addListener (this); + listenedParameters.push_back (parameter); + } +} + +void AudioPluginProcessorCLAP::removeParameterListeners() +{ + for (auto& parameter : listenedParameters) + parameter->removeListener (this); + + listenedParameters.clear(); +} + +bool AudioPluginProcessorCLAP::isValidProcessorParameterIndex (int indexInContainer) const +{ + return audioProcessor != nullptr + && isPositiveAndBelow (indexInContainer, static_cast (audioProcessor->getParameters().size())); +} + +void AudioPluginProcessorCLAP::parameterValueChanged (const AudioParameter::Ptr& parameter, int indexInContainer) +{ + if (! isValidProcessorParameterIndex (indexInContainer)) + return; + + enqueueParameterEvent (CLAP_EVENT_PARAM_VALUE, + parameter->getHostParameterID(), + static_cast (parameter->getValue())); + requestParameterFlush(); +} + +void AudioPluginProcessorCLAP::parameterGestureBegin (const AudioParameter::Ptr& parameter, int indexInContainer) +{ + if (! isValidProcessorParameterIndex (indexInContainer)) + return; + + enqueueParameterEvent (CLAP_EVENT_PARAM_GESTURE_BEGIN, parameter->getHostParameterID()); + requestParameterFlush(); +} + +void AudioPluginProcessorCLAP::parameterGestureEnd (const AudioParameter::Ptr& parameter, int indexInContainer) +{ + if (! isValidProcessorParameterIndex (indexInContainer)) + return; + + enqueueParameterEvent (CLAP_EVENT_PARAM_GESTURE_END, parameter->getHostParameterID()); + requestParameterFlush(); +} + +void AudioPluginProcessorCLAP::audioProcessorChanged (AudioProcessor* processor, const AudioProcessor::ChangeDetails& details) +{ + ignoreUnused (processor); + + if (details.latencyChanged) + { + callLatencyChangeOnNextActivate.store (true); + + if (isActive.load() && host != nullptr && host->request_restart != nullptr) + host->request_restart (host); + } + + if (details.tailChanged) + { + tailChangedPending.store (true); + + if (host != nullptr && host->request_process != nullptr) + host->request_process (host); + } + + uint32 parameterRescanFlags = 0; + + if (details.parameterValuesChanged) + parameterRescanFlags |= CLAP_PARAM_RESCAN_VALUES; + + if (details.parameterInfoChanged) + parameterRescanFlags |= CLAP_PARAM_RESCAN_VALUES + | CLAP_PARAM_RESCAN_TEXT + | CLAP_PARAM_RESCAN_INFO; + + if (parameterRescanFlags != 0) + { + parameterRescanFlagsPending.fetch_or (parameterRescanFlags); + requestMainThreadCallback(); + } + + if (details.nonParameterStateChanged) + { + stateDirtyPending.store (true); + requestMainThreadCallback(); + } +} + +void AudioPluginProcessorCLAP::enqueueParameterEvent (uint16 eventType, clap_id parameterId, double value) noexcept +{ + int start1, size1, start2, size2; + parameterEventFifo.prepareToWrite (1, start1, size1, start2, size2); + + ignoreUnused (start2, size2); + + if (size1 <= 0) + { + jassertfalse; // Increase parameterEventQueueSize if hosts drop CLAP parameter feedback. + parameterEventFifo.finishedWrite (0); + return; + } + + parameterEvents[static_cast (start1)] = { eventType, parameterId, value }; + parameterEventFifo.finishedWrite (1); +} + +void AudioPluginProcessorCLAP::drainParameterEvents (const clap_output_events_t* out) noexcept +{ + if (out == nullptr) + return; + + for (;;) + { + int start1, size1, start2, size2; + parameterEventFifo.prepareToRead (1, start1, size1, start2, size2); + + ignoreUnused (start2, size2); + + if (size1 <= 0) + { + parameterEventFifo.finishedRead (0); + return; + } + + const auto event = parameterEvents[static_cast (start1)]; + parameterEventFifo.finishedRead (1); + + if (event.eventType == CLAP_EVENT_PARAM_VALUE) + { + clap_event_param_value_t paramEvent {}; + paramEvent.header.size = sizeof (paramEvent); + paramEvent.header.time = 0; + paramEvent.header.space_id = CLAP_CORE_EVENT_SPACE_ID; + paramEvent.header.type = CLAP_EVENT_PARAM_VALUE; + paramEvent.header.flags = 0; + paramEvent.param_id = event.parameterId; + paramEvent.cookie = nullptr; + paramEvent.note_id = -1; + paramEvent.port_index = -1; + paramEvent.channel = -1; + paramEvent.key = -1; + paramEvent.value = event.value; + out->try_push (out, ¶mEvent.header); + } + else if (event.eventType == CLAP_EVENT_PARAM_GESTURE_BEGIN + || event.eventType == CLAP_EVENT_PARAM_GESTURE_END) + { + clap_event_param_gesture_t gestureEvent {}; + gestureEvent.header.size = sizeof (gestureEvent); + gestureEvent.header.time = 0; + gestureEvent.header.space_id = CLAP_CORE_EVENT_SPACE_ID; + gestureEvent.header.type = event.eventType; + gestureEvent.header.flags = 0; + gestureEvent.param_id = event.parameterId; + out->try_push (out, &gestureEvent.header); + } + } +} + +void AudioPluginProcessorCLAP::requestParameterFlush() const noexcept +{ + if (! isInsideProcessBlock.load() + && hostParams != nullptr + && hostParams->request_flush != nullptr) + { + hostParams->request_flush (host); + } +} + +void AudioPluginProcessorCLAP::requestMainThreadCallback() const noexcept +{ + if (host != nullptr && host->request_callback != nullptr) + host->request_callback (host); +} + +void AudioPluginProcessorCLAP::handleMainThreadNotifications() noexcept +{ + const auto parameterRescanFlags = parameterRescanFlagsPending.exchange (0); + if (parameterRescanFlags != 0 + && hostParams != nullptr + && hostParams->rescan != nullptr) + { + hostParams->rescan (host, parameterRescanFlags); + } + + if (stateDirtyPending.exchange (false) + && hostState != nullptr + && hostState->mark_dirty != nullptr) + { + hostState->mark_dirty (host); + } +} + +void AudioPluginProcessorCLAP::handleAudioThreadNotifications() noexcept +{ + if (tailChangedPending.exchange (false) + && hostTail != nullptr + && hostTail->changed != nullptr) + { + hostTail->changed (host); + } +} + +//============================================================================== + +void AudioPluginProcessorCLAP::editorResized() +{ + if (audioPluginEditor == nullptr || hostTriggeredResizing) + return; + + if (hostGUI != nullptr && hostGUI->request_resize != nullptr) + hostGUI->request_resize (host, audioPluginEditor->getWidth(), audioPluginEditor->getHeight()); +} + +ScopedValueSetter AudioPluginProcessorCLAP::scopedHostEditorResizing() +{ + return { hostTriggeredResizing, true }; +} + +//============================================================================== + +void AudioPluginEditorCLAP::contentScaleChanged (float dpiScale) +{ + if (processorEditor == nullptr) + return; + + processorEditor->contentScaleChanged (dpiScale); +} + +void AudioPluginEditorCLAP::resized() +{ + if (processorEditor == nullptr) + return; + + processorEditor->setBounds (getLocalBounds()); + + wrapper->editorResized(); +} + +} // namespace yup + +//============================================================================== + +static const clap_plugin_factory_t plugin_factory = [] +{ + clap_plugin_factory_t factory; + + factory.get_plugin_count = [] (const clap_plugin_factory* factory) -> uint32_t + { + return 1; + }; + + factory.get_plugin_descriptor = [] (const clap_plugin_factory* factory, uint32_t index) -> const clap_plugin_descriptor_t* + { + return index == 0 ? &yup::pluginDescriptor : nullptr; + }; + + factory.create_plugin = [] (const clap_plugin_factory* factory, const clap_host_t* host, const char* pluginId) -> const clap_plugin_t* + { + if (! clap_version_is_compatible (host->clap_version) || std::string_view (pluginId) != yup::pluginDescriptor.id) + return nullptr; + + auto wrapper = new yup::AudioPluginProcessorCLAP (host); + return wrapper->getPlugin(); + }; + + return factory; +}(); + +//============================================================================== + +static bool clapInit (const char*) noexcept +{ + return true; +} + +static void clapDeinit() noexcept +{ +} + +static const void* clapGetFactory (const char* factoryId) noexcept +{ + if (std::string_view (factoryId) == CLAP_PLUGIN_FACTORY_ID) + return std::addressof (plugin_factory); + + return nullptr; +} + +extern "C" const CLAP_EXPORT clap_plugin_entry_t clap_entry = { + CLAP_VERSION_INIT, + clapInit, + clapDeinit, + clapGetFactory +}; diff --git a/modules/yup_audio_plugin_client/clap/yup_audio_plugin_client_CLAP.mm b/modules/yup_audio_plugin_client/clap/yup_audio_plugin_client_CLAP.mm index 0c888aa5f..c081bd911 100644 --- a/modules/yup_audio_plugin_client/clap/yup_audio_plugin_client_CLAP.mm +++ b/modules/yup_audio_plugin_client/clap/yup_audio_plugin_client_CLAP.mm @@ -1,22 +1,22 @@ -/* - ============================================================================== - - 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_CLAP.cpp" +/* + ============================================================================== + + 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_CLAP.cpp" diff --git a/modules/yup_audio_plugin_client/common/yup_AudioPluginUtilities.h b/modules/yup_audio_plugin_client/common/yup_AudioPluginUtilities.h new file mode 100644 index 000000000..7c453f935 --- /dev/null +++ b/modules/yup_audio_plugin_client/common/yup_AudioPluginUtilities.h @@ -0,0 +1,208 @@ +/* + ============================================================================== + + 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. + + ============================================================================== +*/ + +#pragma once + +#include + +//============================================================================== +namespace yup +{ + +/** Returns the first host-facing parameter ID not used by the processor. + + This is useful for wrapper-owned synthetic parameters, such as bypass, that + must not collide with plugin-authored stable automation IDs. +*/ +inline uint32 findFirstUnusedHostParameterID (const AudioProcessor& processor, uint32 preferredParameterID) +{ + auto parameterID = preferredParameterID; + + while (processor.getParameterByHostID (parameterID) != nullptr + && parameterID < AudioParameter::maximumHostParameterID) + { + ++parameterID; + } + + jassert (parameterID <= AudioParameter::maximumHostParameterID); + jassert (processor.getParameterByHostID (parameterID) == nullptr); + + return parameterID; +} + +/** Returns a host-facing parameter ID suitable for a wrapper-owned bypass parameter. */ +inline uint32 getBypassHostParameterID (const AudioProcessor& processor) +{ + return findFirstUnusedHostParameterID (processor, static_cast (processor.getParameters().size())); +} + +/** Returns the total number of output audio channels across all output audio buses. */ +inline int getTotalAudioOutputChannels (const AudioProcessor& processor) +{ + int count = 0; + + for (const auto& bus : processor.getBusLayout().getOutputBuses()) + if (bus.getType() == AudioBus::Type::Audio) + count += bus.getNumChannels(); + + return count; +} + +/** Returns the default automation-event capacity used by plugin wrappers. */ +inline int getDefaultParameterChangeCapacity (const AudioProcessor& processor) +{ + return static_cast (processor.getParameters().size()) * 4 + 32; +} + +/** Calls the processor's normal or bypass processing path for a prepared context. */ +template +void processAudioBlock (AudioProcessor& processor, Context& context, bool isBypassed) +{ + if (isBypassed) + processor.processBlockBypassed (context); + else + processor.processBlock (context); +} + +/** Wrapper-owned state that can be stored alongside processor state. */ +struct WrapperBypassState +{ + bool hasWrapperState = false; + bool isBypassed = false; + bool hasProcessorState = false; + MemoryBlock processorState; +}; + +/** Writes wrapper bypass state and optional processor state using a format-specific magic. */ +inline MemoryBlock writeWrapperBypassState (int magic, + int version, + bool isBypassed, + const MemoryBlock& processorState, + bool hasProcessorState) +{ + MemoryBlock data; + MemoryOutputStream output (data, false); + output.writeInt (magic); + output.writeInt (version); + output.writeBool (isBypassed); + output.writeBool (hasProcessorState); + output.writeInt64 (hasProcessorState ? static_cast (processorState.getSize()) : 0); + + if (hasProcessorState && ! processorState.isEmpty()) + output.write (processorState.getData(), processorState.getSize()); + + output.flush(); + return data; +} + +/** Reads wrapper bypass state, falling back to legacy raw processor state. */ +inline WrapperBypassState readWrapperBypassState (const MemoryBlock& data, int magic, int version) +{ + WrapperBypassState result; + + MemoryInputStream input (data, false); + + if (input.readInt() != magic + || input.readInt() != version) + { + result.processorState = data; + return result; + } + + result.hasWrapperState = true; + result.isBypassed = input.readBool(); + result.hasProcessorState = input.readBool(); + + const auto processorStateSize = input.readInt64(); + if (processorStateSize < 0 + || processorStateSize > input.getNumBytesRemaining() + || processorStateSize > static_cast (std::numeric_limits::max())) + { + result.hasWrapperState = false; + result.hasProcessorState = false; + result.processorState = data; + return result; + } + + result.processorState.setSize (static_cast (processorStateSize)); + + const auto bytesToRead = static_cast (processorStateSize); + if (bytesToRead > 0 + && input.read (result.processorState.getData(), bytesToRead) != bytesToRead) + { + result.hasWrapperState = false; + result.hasProcessorState = false; + result.processorState = data; + } + + return result; +} + +/** Adds a normalized automation change for a host-facing parameter ID. + + @returns true if the host parameter ID mapped to a processor parameter and + the change was added to the buffer. +*/ +inline bool addParameterChangeByHostParameterID (AudioProcessor& processor, + ParameterChangeBuffer& changes, + uint32 hostParameterID, + float normalizedValue, + int sampleOffset) +{ + const auto parameters = processor.getParameters(); + const auto parameterIndex = processor.getParameterIndexByHostID (hostParameterID); + + if (! isPositiveAndBelow (parameterIndex, static_cast (parameters.size()))) + return false; + + return changes.addChange (parameterIndex, normalizedValue, sampleOffset); +} + +/** Applies the last known normalized values in a parameter change buffer. + + Wrappers use this after collecting sample-accurate changes so processors that + still read AudioParameter atomics directly see the latest host value. +*/ +inline void applyParameterChangesToProcessor (AudioProcessor& processor, const ParameterChangeBuffer& changes) +{ + const auto parameters = processor.getParameters(); + + for (const auto& change : changes) + { + if (isPositiveAndBelow (change.parameterIndex, static_cast (parameters.size()))) + parameters[change.parameterIndex]->setNormalizedValue (change.normalizedValue); + } +} + +/** Ends any active parameter gestures before a plugin wrapper tears down its processor. */ +inline void endActiveParameterGestures (AudioProcessor* processor) +{ + if (processor == nullptr) + return; + + for (auto& parameter : processor->getParameters()) + { + while (parameter->isPerformingChangeGesture()) + parameter->endChangeGesture(); + } +} + +} // namespace yup diff --git a/modules/yup_audio_plugin_client/standalone/yup_audio_plugin_client_Standalone.cpp b/modules/yup_audio_plugin_client/standalone/yup_audio_plugin_client_Standalone.cpp index 7301c854c..8e2687ce0 100644 --- a/modules/yup_audio_plugin_client/standalone/yup_audio_plugin_client_Standalone.cpp +++ b/modules/yup_audio_plugin_client/standalone/yup_audio_plugin_client_Standalone.cpp @@ -21,6 +21,8 @@ #include "../yup_audio_plugin_client.h" +#include "../common/yup_AudioPluginUtilities.h" + #include #if ! defined(YUP_AUDIO_PLUGIN_ENABLE_STANDALONE) @@ -131,7 +133,9 @@ class AudioProcessorApplication } MidiBuffer midiBuffer; - processor->processBlock (audioBuffer, midiBuffer); + ParameterChangeBuffer emptyParams; + AudioProcessContext ctx { audioBuffer, midiBuffer, emptyParams }; + processor->processBlock (ctx); AudioBuffer outputBuffer { outputChannelData, numOutputChannels, numSamples }; for (int outputIndex = 0; outputIndex < numOutputChannels; ++outputIndex) diff --git a/modules/yup_audio_plugin_client/vst3/yup_audio_plugin_client_VST3.cpp b/modules/yup_audio_plugin_client/vst3/yup_audio_plugin_client_VST3.cpp index 9a6e5048f..401797189 100644 --- a/modules/yup_audio_plugin_client/vst3/yup_audio_plugin_client_VST3.cpp +++ b/modules/yup_audio_plugin_client/vst3/yup_audio_plugin_client_VST3.cpp @@ -21,6 +21,8 @@ #include "../yup_audio_plugin_client.h" +#include "../common/yup_AudioPluginUtilities.h" + #if ! defined(YUP_AUDIO_PLUGIN_ENABLE_VST3) #error "YUP_AUDIO_PLUGIN_ENABLE_VST3 must be defined" #endif @@ -28,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -43,7 +47,9 @@ #include #include +#include #include +#include //============================================================================== @@ -57,8 +63,6 @@ using namespace Steinberg; namespace { -//============================================================================== - FUID toFUID (const String& source) { const auto uid = Uuid::fromSHA1 (SHA1 (source.toUTF8())); @@ -94,29 +98,80 @@ void toString128 (const String& source, Vst::String128 destination) destination[length] = 0; } -//============================================================================== +Vst::ParamID getVST3ParameterID (const AudioParameter::Ptr& parameter) +{ + return static_cast (parameter->getHostParameterID()); +} + +Vst::ParamID getVST3BypassParameterID (const AudioProcessor& processor) +{ + return static_cast (getBypassHostParameterID (processor)); +} -static std::atomic_int numScopedInitInstancesGui = 0; +constexpr int vst3WrapperStateMagic = 0x33535659; // "YVS3" +constexpr int vst3WrapperStateVersion = 1; -struct VST3ScopedYupInitialiser +MemoryBlock readVST3StreamData (IBStream& stream) { - VST3ScopedYupInitialiser() + MemoryBlock data; + char buffer[4096]; + int32 bytesRead = 0; + + while (stream.read (buffer, sizeof (buffer), &bytesRead) == kResultOk && bytesRead > 0) + data.append (buffer, static_cast (bytesRead)); + + return data; +} + +class AudioPluginPlayHeadVST3 final : public AudioPlayHead +{ +public: + explicit AudioPluginPlayHeadVST3 (const Vst::ProcessContext* processContext) + : processContext (processContext) { - if (numScopedInitInstancesGui.fetch_add (1) == 0) - { - initialiseYup_GUI(); - initialiseYup_Windowing(); - } } - ~VST3ScopedYupInitialiser() + bool canControlTransport() override { - if (numScopedInitInstancesGui.fetch_add (-1) == 1) - { - shutdownYup_Windowing(); - shutdownYup_GUI(); - } + return false; } + + std::optional getPosition() const override + { + if (processContext == nullptr) + return {}; + + PositionInfo result; + + result.setTimeInSamples (static_cast (processContext->projectTimeSamples)); + + if (processContext->sampleRate > 0.0) + result.setTimeInSeconds (static_cast (processContext->projectTimeSamples) / processContext->sampleRate); + + if ((processContext->state & Vst::ProcessContext::kTempoValid) != 0) + result.setBpm (processContext->tempo); + + if ((processContext->state & Vst::ProcessContext::kTimeSigValid) != 0) + result.setTimeSignature (TimeSignature { processContext->timeSigNumerator, processContext->timeSigDenominator }); + + if ((processContext->state & Vst::ProcessContext::kProjectTimeMusicValid) != 0) + result.setPpqPosition (processContext->projectTimeMusic); + + if ((processContext->state & Vst::ProcessContext::kBarPositionValid) != 0) + result.setPpqPositionOfLastBarStart (processContext->barPositionMusic); + + if ((processContext->state & Vst::ProcessContext::kCycleValid) != 0) + result.setLoopPoints (LoopPoints { processContext->cycleStartMusic, processContext->cycleEndMusic }); + + result.setIsPlaying ((processContext->state & Vst::ProcessContext::kPlaying) != 0); + result.setIsRecording ((processContext->state & Vst::ProcessContext::kRecording) != 0); + result.setIsLooping ((processContext->state & Vst::ProcessContext::kCycleActive) != 0); + + return result; + } + +private: + const Vst::ProcessContext* processContext = nullptr; }; } // namespace @@ -128,6 +183,113 @@ static const auto YupPlugin_Controller_UID = toFUID (YupPlugin_Id ".controller") //============================================================================== +static Vst::SpeakerArrangement speakerArrForChannels (int channels) +{ + switch (channels) + { + case 1: + return Vst::SpeakerArr::kMono; + case 6: + return Vst::SpeakerArr::k51; + case 8: + return Vst::SpeakerArr::k71CineFullFront; + default: + return Vst::SpeakerArr::kStereo; + } +} + +static void writeMidiBufferToVST3EventList (const MidiBuffer& midiBuffer, + Vst::IEventList& outputEvents, + int numSamples) +{ + for (const MidiMessageMetadata metadata : midiBuffer) + { + const auto& message = metadata.getMessage(); + + Vst::Event event {}; + event.busIndex = 0; + event.sampleOffset = jlimit (0, jmax (0, numSamples - 1), metadata.samplePosition); + event.ppqPosition = 0.0; + event.flags = 0; + + if (message.isNoteOn()) + { + event.type = Vst::Event::kNoteOnEvent; + event.noteOn.channel = static_cast (message.getChannel() - 1); + event.noteOn.pitch = static_cast (message.getNoteNumber()); + event.noteOn.tuning = 0.0f; + event.noteOn.velocity = message.getFloatVelocity(); + event.noteOn.length = 0; + event.noteOn.noteId = -1; + } + else if (message.isNoteOff()) + { + event.type = Vst::Event::kNoteOffEvent; + event.noteOff.channel = static_cast (message.getChannel() - 1); + event.noteOff.pitch = static_cast (message.getNoteNumber()); + event.noteOff.velocity = message.getFloatVelocity(); + event.noteOff.noteId = -1; + event.noteOff.tuning = 0.0f; + } + else if (message.isAftertouch()) + { + event.type = Vst::Event::kPolyPressureEvent; + event.polyPressure.channel = static_cast (message.getChannel() - 1); + event.polyPressure.pitch = static_cast (message.getNoteNumber()); + event.polyPressure.pressure = static_cast (message.getAfterTouchValue()) / 127.0f; + event.polyPressure.noteId = -1; + } + else if (message.isController()) + { + event.type = Vst::Event::kLegacyMIDICCOutEvent; + event.midiCCOut.channel = static_cast (jlimit (0, 15, message.getChannel() - 1)); + event.midiCCOut.controlNumber = static_cast (message.getControllerNumber()); + event.midiCCOut.value = static_cast (message.getControllerValue()); + event.midiCCOut.value2 = 0; + } + else if (message.isProgramChange()) + { + event.type = Vst::Event::kLegacyMIDICCOutEvent; + event.midiCCOut.channel = static_cast (jlimit (0, 15, message.getChannel() - 1)); + event.midiCCOut.controlNumber = static_cast (Vst::kCtrlProgramChange); + event.midiCCOut.value = static_cast (message.getProgramChangeNumber()); + event.midiCCOut.value2 = 0; + } + else if (message.isPitchWheel()) + { + const auto value = jlimit (0, 0x3fff, message.getPitchWheelValue()); + event.type = Vst::Event::kLegacyMIDICCOutEvent; + event.midiCCOut.channel = static_cast (jlimit (0, 15, message.getChannel() - 1)); + event.midiCCOut.controlNumber = static_cast (Vst::kPitchBend); + event.midiCCOut.value = static_cast (value & 0x7f); + event.midiCCOut.value2 = static_cast ((value >> 7) & 0x7f); + } + else if (message.isChannelPressure()) + { + event.type = Vst::Event::kLegacyMIDICCOutEvent; + event.midiCCOut.channel = static_cast (jlimit (0, 15, message.getChannel() - 1)); + event.midiCCOut.controlNumber = static_cast (Vst::kAfterTouch); + event.midiCCOut.value = static_cast (message.getChannelPressureValue()); + event.midiCCOut.value2 = 0; + } + else if (message.isSysEx()) + { + event.type = Vst::Event::kDataEvent; + event.data.size = static_cast (message.getSysExDataSize()); + event.data.type = Vst::DataEvent::kMidiSysEx; + event.data.bytes = message.getSysExData(); + } + else + { + continue; + } + + outputEvents.addEvent (event); + } +} + +//============================================================================== + class AudioPluginEditorViewVST3 : public Component , public Vst::EditorView @@ -149,10 +311,8 @@ class AudioPluginEditorViewVST3 if (size != nullptr) { - setBounds ({ static_cast (size->left), - static_cast (size->top), - static_cast (size->getWidth()), - static_cast (size->getHeight()) }); + setSize ({ static_cast (size->getWidth()), + static_cast (size->getHeight()) }); } else { @@ -166,6 +326,7 @@ class AudioPluginEditorViewVST3 { if (editor != nullptr) { + endActiveParameterGestures (processor); setVisible (false); removeFromDesktop(); @@ -181,10 +342,10 @@ class AudioPluginEditorViewVST3 if (plugFrame != nullptr && ! hostTriggeredResizing) { ViewRect viewRect; - viewRect.left = getX(); - viewRect.top = getY(); - viewRect.right = viewRect.left + getWidth(); - viewRect.bottom = viewRect.top + getHeight(); + viewRect.left = 0; + viewRect.top = 0; + viewRect.right = getWidth(); + viewRect.bottom = getHeight(); plugFrame->resizeView (this, std::addressof (viewRect)); } @@ -220,6 +381,7 @@ class AudioPluginEditorViewVST3 { if (editor != nullptr) { + endActiveParameterGestures (processor); setVisible (false); removeFromDesktop(); } @@ -274,10 +436,8 @@ class AudioPluginEditorViewVST3 const auto scoped = ScopedValueSetter (hostTriggeredResizing, true); - setBounds ({ static_cast (rect.left), - static_cast (rect.top), - static_cast (rect.getWidth()), - static_cast (rect.getHeight()) }); + setSize ({ static_cast (rect.getWidth()), + static_cast (rect.getHeight()) }); } return kResultTrue; @@ -293,18 +453,18 @@ class AudioPluginEditorViewVST3 if (editor->isResizable() && editor->getWidth() != 0 && editor->getHeight() != 0) { - size->left = getX(); - size->top = getY(); - size->right = size->left + getWidth(); - size->bottom = size->top + getHeight(); + size->left = 0; + size->top = 0; + size->right = getWidth(); + size->bottom = getHeight(); } else { const auto preferredSize = editor->getPreferredSize(); - size->left = getX(); - size->top = getY(); - size->right = size->left + preferredSize.getWidth(); - size->bottom = size->top + preferredSize.getHeight(); + size->left = 0; + size->top = 0; + size->right = preferredSize.getWidth(); + size->bottom = preferredSize.getHeight(); } return kResultTrue; @@ -342,6 +502,7 @@ class AudioPluginEditorViewVST3 } private: + ScopedYupInitialiser_Windowing scopeInitialiser; AudioProcessor* processor = nullptr; std::unique_ptr editor; bool hostTriggeredResizing = false; @@ -355,6 +516,8 @@ class AudioPluginControllerVST3 , public Vst::IUnitInfo , public Vst::IRemapParamID , public Vst::ChannelContext::IInfoListener + , private AudioParameter::Listener + , private AudioProcessor::Listener { public: //============================================================================== @@ -387,6 +550,8 @@ class AudioPluginControllerVST3 ~AudioPluginControllerVST3() { + removeParameterListeners(); + removeProcessorListener(); } //============================================================================== @@ -402,6 +567,10 @@ class AudioPluginControllerVST3 tresult PLUGIN_API terminate() override { + removeParameterListeners(); + removeProcessorListener(); + processor = nullptr; + return Vst::EditController::terminate(); } @@ -409,13 +578,15 @@ class AudioPluginControllerVST3 tresult PLUGIN_API connect (Vst::IConnectionPoint* other) override { - return kResultTrue; + return Vst::EditController::connect (other); } tresult PLUGIN_API disconnect (Vst::IConnectionPoint* other) override { + removeParameterListeners(); + removeProcessorListener(); processor = nullptr; - return kResultTrue; + return Vst::EditController::disconnect (other); } tresult PLUGIN_API notify (Vst::IMessage* message) override @@ -435,9 +606,11 @@ class AudioPluginControllerVST3 auto result = attributes->getBinary ("data", msgData, msgSize); if (result == kResultTrue && msgSize == sizeof (void*)) { + removeProcessorListener(); processor = static_cast (*reinterpret_cast (msgData)); setupParameters(); + addProcessorListener(); return result; } @@ -450,32 +623,49 @@ class AudioPluginControllerVST3 tresult PLUGIN_API setState (IBStream* state) override { - return kResultFalse; + if (state == nullptr) + return kInvalidArgument; + + return kResultOk; } tresult PLUGIN_API getState (IBStream* state) override { - return kResultFalse; - } + if (state == nullptr) + return kInvalidArgument; - //============================================================================== + return kResultOk; + } - int32 PLUGIN_API getParameterCount() override + tresult PLUGIN_API setComponentState (IBStream* state) override { - if (processor == nullptr) - return 0; + if (processor == nullptr || state == nullptr) + return kResultFalse; - return static_cast (processor->getParameters().size()); + const auto wrapperState = readWrapperBypassState (readVST3StreamData (*state), + vst3WrapperStateMagic, + vst3WrapperStateVersion); + if (! wrapperState.hasWrapperState) + { + syncProcessorParametersToController(); + return kResultOk; + } + + Vst::EditController::setParamNormalized (getVST3BypassParameterID (*processor), + wrapperState.isBypassed ? 1.0 : 0.0); + + syncProcessorParametersToController(); + + return kResultOk; } + //============================================================================== + tresult PLUGIN_API getParameterInfo (int32 paramIndex, Vst::ParameterInfo& info) override { if (processor == nullptr) return kInternalError; - if (! isPositiveAndBelow (paramIndex, getParameterCount())) - return kInvalidArgument; - if (auto parameter = parameters.getParameterByIndex (paramIndex)) { info = parameter->getInfo(); @@ -490,13 +680,18 @@ class AudioPluginControllerVST3 if (processor == nullptr) return kInternalError; - if (! isPositiveAndBelow (static_cast (tag), getParameterCount())) - return kInvalidArgument; + const auto bypassParameterID = getVST3BypassParameterID (*processor); - if (auto parameter = processor->getParameters()[tag]) + if (tag == bypassParameterID) { - toString128 (parameter->convertToString (parameter->convertToDenormalizedValue (valueNormalized)), string); + // Bypass parameter + toString128 (valueNormalized >= 0.5 ? "On" : "Off", string); + return kResultOk; + } + if (auto parameter = processor->getParameterByHostID (static_cast (tag))) + { + toString128 (parameter->convertToString (parameter->convertToDenormalizedValue (valueNormalized)), string); return kResultOk; } @@ -508,13 +703,19 @@ class AudioPluginControllerVST3 if (processor == nullptr) return kInternalError; - if (! isPositiveAndBelow (static_cast (tag), getParameterCount())) - return kInvalidArgument; + const auto bypassParameterID = getVST3BypassParameterID (*processor); - if (auto parameter = processor->getParameters()[tag]) + if (tag == bypassParameterID) { - valueNormalized = parameter->convertToNormalizedValue (parameter->convertFromString (toString (string))); + // Bypass parameter + const auto str = toString (string); + valueNormalized = (str == "On" || str == "1") ? 1.0 : 0.0; + return kResultOk; + } + if (auto parameter = processor->getParameterByHostID (static_cast (tag))) + { + valueNormalized = parameter->convertToNormalizedValue (parameter->convertFromString (toString (string))); return kResultOk; } @@ -526,10 +727,10 @@ class AudioPluginControllerVST3 if (processor == nullptr) return valueNormalized; - if (! isPositiveAndBelow (static_cast (tag), getParameterCount())) + if (tag == getVST3BypassParameterID (*processor)) return valueNormalized; - if (auto parameter = processor->getParameters()[tag]) + if (auto parameter = processor->getParameterByHostID (static_cast (tag))) return parameter->convertToDenormalizedValue (valueNormalized); return valueNormalized; @@ -540,10 +741,10 @@ class AudioPluginControllerVST3 if (processor == nullptr) return plainValue; - if (! isPositiveAndBelow (static_cast (tag), getParameterCount())) + if (tag == getVST3BypassParameterID (*processor)) return plainValue; - if (auto parameter = processor->getParameters()[tag]) + if (auto parameter = processor->getParameterByHostID (static_cast (tag))) return parameter->convertToNormalizedValue (plainValue); return plainValue; @@ -554,10 +755,10 @@ class AudioPluginControllerVST3 if (processor == nullptr) return 0.0; - if (! isPositiveAndBelow (static_cast (tag), getParameterCount())) - return 0.0; + if (tag == getVST3BypassParameterID (*processor)) + return Vst::EditController::getParamNormalized (tag); - if (auto parameter = processor->getParameters()[tag]) + if (auto parameter = processor->getParameterByHostID (static_cast (tag))) return parameter->getNormalizedValue(); return 0.0; @@ -568,12 +769,13 @@ class AudioPluginControllerVST3 if (processor == nullptr) return kInternalError; - if (! isPositiveAndBelow (static_cast (tag), getParameterCount())) - return kInvalidArgument; + if (tag == getVST3BypassParameterID (*processor)) + return Vst::EditController::setParamNormalized (tag, value); - if (auto parameter = processor->getParameters()[tag]) + if (auto parameter = processor->getParameterByHostID (static_cast (tag))) { - parameter->setNormalizedValue (value); + parameter->setNormalizedValue (static_cast (value)); + Vst::EditController::setParamNormalized (tag, value); return kResultOk; } @@ -587,8 +789,8 @@ class AudioPluginControllerVST3 if (processor == nullptr) return kInternalError; - const auto numParams = static_cast (processor->getParameters().size()); - if (oldParamID >= 0 && oldParamID < numParams) + if (processor->getParameterByHostID (static_cast (oldParamID)) != nullptr + || oldParamID == getVST3BypassParameterID (*processor)) { newParamID = oldParamID; return kResultOk; @@ -604,7 +806,17 @@ class AudioPluginControllerVST3 Vst::CtrlNumber midiControllerNumber, Vst::ParamID& id) override { - return kNotImplemented; + if (processor == nullptr) + return kResultFalse; + + const auto parameters = processor->getParameters(); + if (midiControllerNumber < static_cast (parameters.size())) + { + id = getVST3ParameterID (parameters[static_cast (midiControllerNumber)]); + return kResultOk; + } + + return kResultFalse; } //============================================================================== @@ -697,7 +909,7 @@ class AudioPluginControllerVST3 if (processor == nullptr) return kInternalError; - if (listId != Vst::kNoProgramListId) + if (listId != 0) return kResultFalse; if (isPositiveAndBelow (programIndex, processor->getNumPresets())) @@ -717,7 +929,7 @@ class AudioPluginControllerVST3 if (processor == nullptr) return kInternalError; - if (listId != Vst::kNoProgramListId) + if (listId != 0) return kResultFalse; if (std::string_view (attributeId) != Vst::PresetAttributes::kName) @@ -765,26 +977,148 @@ class AudioPluginControllerVST3 private: void setupParameters() { + removeParameterListeners(); + parameters.removeAll(); + if (processor == nullptr) return; for (size_t parameterIndex = 0; parameterIndex < processor->getParameters().size(); ++parameterIndex) { const auto parameter = processor->getParameters()[parameterIndex]; + const auto flags = (parameter->isAutomatable() && ! parameter->isReadOnly()) + ? Vst::ParameterInfo::kCanAutomate + : 0; parameters.addParameter ( reinterpret_cast (parameter->getName().toUTF16().getAddress()), - nullptr, // units - 0, // step count - parameter->getNormalizedValue(), // normalized value - Vst::ParameterInfo::kCanAutomate, // flags (Vst::ParameterInfo::kNoFlags) - static_cast (parameterIndex), // tag - Vst::kRootUnitId, // unit - nullptr); // short title + nullptr, // units + parameter->getNumSteps(), // step count + parameter->getNormalizedValue(), // normalized value + flags, // flags + getVST3ParameterID (parameter), // tag + Vst::kRootUnitId, // unit + nullptr); // short title + + parameter->addListener (this); + listenedParameters.push_back (parameter); } + + // VST3 bypass parameter (always the last parameter) + parameters.addParameter ( + STR16 ("Bypass"), + nullptr, + 1, // step count 1 = toggle + 0, // default: not bypassed + Vst::ParameterInfo::kCanAutomate | Vst::ParameterInfo::kIsBypass, + getVST3BypassParameterID (*processor), + Vst::kRootUnitId, + nullptr); + } + + void removeParameterListeners() + { + for (auto& parameter : listenedParameters) + parameter->removeListener (this); + + listenedParameters.clear(); + } + + void addProcessorListener() + { + if (processor != nullptr) + processor->addListener (this); + } + + void removeProcessorListener() + { + if (processor != nullptr) + processor->removeListener (this); + } + + void parameterValueChanged (const AudioParameter::Ptr& parameter, int indexInContainer) override + { + if (! isValidProcessorParameterIndex (indexInContainer)) + return; + + const auto tag = getVST3ParameterID (parameter); + const auto normalizedValue = static_cast (parameter->getNormalizedValue()); + + if (parameter->isReadOnly()) + return; + + Vst::EditController::setParamNormalized (tag, normalizedValue); + + if (parameter->isAutomatable()) + Vst::EditController::performEdit (tag, normalizedValue); + } + + void parameterGestureBegin (const AudioParameter::Ptr& parameter, int indexInContainer) override + { + if (isValidProcessorParameterIndex (indexInContainer) + && parameter->isAutomatable() + && ! parameter->isReadOnly()) + { + Vst::EditController::beginEdit (getVST3ParameterID (processor->getParameters()[indexInContainer])); + } + } + + void parameterGestureEnd (const AudioParameter::Ptr& parameter, int indexInContainer) override + { + if (isValidProcessorParameterIndex (indexInContainer) + && parameter->isAutomatable() + && ! parameter->isReadOnly()) + { + Vst::EditController::endEdit (getVST3ParameterID (processor->getParameters()[indexInContainer])); + } + } + + void syncProcessorParametersToController() + { + if (processor == nullptr) + return; + + for (const auto& parameter : processor->getParameters()) + { + Vst::EditController::setParamNormalized ( + getVST3ParameterID (parameter), + static_cast (parameter->getNormalizedValue())); + } + } + + void audioProcessorChanged (AudioProcessor* processor, const AudioProcessor::ChangeDetails& details) override + { + ignoreUnused (processor); + + int32 restartFlags = 0; + + if (details.latencyChanged) + restartFlags |= Vst::kLatencyChanged; + + if (details.parameterValuesChanged) + restartFlags |= Vst::kParamValuesChanged; + + if (details.parameterInfoChanged) + restartFlags |= Vst::kParamValuesChanged + | Vst::kParamTitlesChanged + | Vst::kMidiCCAssignmentChanged; + + if (restartFlags != 0) + if (auto* handler = getComponentHandler()) + handler->restartComponent (restartFlags); + + if (details.nonParameterStateChanged) + setDirty (true); + } + + bool isValidProcessorParameterIndex (int indexInContainer) const + { + return processor != nullptr + && isPositiveAndBelow (indexInContainer, static_cast (processor->getParameters().size())); } AudioProcessor* processor = nullptr; + std::vector listenedParameters; }; //============================================================================== @@ -803,6 +1137,7 @@ class AudioPluginProcessorVST3 : public Vst::AudioEffect virtual ~AudioPluginProcessorVST3() { + endActiveParameterGestures (processor.get()); processor.reset(); } @@ -824,17 +1159,27 @@ class AudioPluginProcessorVST3 : public Vst::AudioEffect for (const auto& inputBus : processor->getBusLayout().getInputBuses()) { const auto nameUTF16 = inputBus.getName().toUTF16(); - addAudioInput (toTChar (nameUTF16), Vst::SpeakerArr::kStereo); + + if (inputBus.getType() == AudioBus::Type::Audio) + addAudioInput (toTChar (nameUTF16), speakerArrForChannels (inputBus.getNumChannels())); + else if (inputBus.getType() == AudioBus::Type::MIDI) + addEventInput (toTChar (nameUTF16)); } for (const auto& outputBus : processor->getBusLayout().getOutputBuses()) { const auto nameUTF16 = outputBus.getName().toUTF16(); - addAudioOutput (toTChar (nameUTF16), Vst::SpeakerArr::kStereo); + + if (outputBus.getType() == AudioBus::Type::Audio) + addAudioOutput (toTChar (nameUTF16), speakerArrForChannels (outputBus.getNumChannels())); + else if (outputBus.getType() == AudioBus::Type::MIDI) + addEventOutput (toTChar (nameUTF16)); } + // Fallback: synths without an explicit MIDI input bus always get one #if YupPlugin_IsSynth - addEventInput (STR16 ("Midi In")); + if (getBusCount (Vst::kEvent, Vst::kInput) == 0) + addEventInput (STR16 ("Midi In")); #endif return kResultOk; @@ -843,7 +1188,10 @@ class AudioPluginProcessorVST3 : public Vst::AudioEffect tresult PLUGIN_API terminate() override { if (processor != nullptr) + { + endActiveParameterGestures (processor.get()); processor->releaseResources(); + } return AudioEffect::terminate(); } @@ -891,17 +1239,43 @@ class AudioPluginProcessorVST3 : public Vst::AudioEffect if (processor == nullptr) return kResultFalse; - // TODO - check compatibility of bus arrangement + const auto& busLayout = processor->getBusLayout(); + + int32 audioInputCount = 0; + int32 audioOutputCount = 0; + + for (const auto& bus : busLayout.getInputBuses()) + if (bus.getType() == AudioBus::Type::Audio) + ++audioInputCount; + + for (const auto& bus : busLayout.getOutputBuses()) + if (bus.getType() == AudioBus::Type::Audio) + ++audioOutputCount; - if (numIns == 1 - && numOuts == 1 - && inputs[0] == Vst::SpeakerArr::kStereo - && outputs[0] == Vst::SpeakerArr::kStereo) + if (numIns != audioInputCount || numOuts != audioOutputCount) + return kResultFalse; + + int32 idx = 0; + for (const auto& bus : busLayout.getInputBuses()) { - return kResultOk; + if (bus.getType() != AudioBus::Type::Audio) + continue; + if (Vst::SpeakerArr::getChannelCount (inputs[idx]) != bus.getNumChannels()) + return kResultFalse; + ++idx; } - return kResultFalse; + idx = 0; + for (const auto& bus : busLayout.getOutputBuses()) + { + if (bus.getType() != AudioBus::Type::Audio) + continue; + if (Vst::SpeakerArr::getChannelCount (outputs[idx]) != bus.getNumChannels()) + return kResultFalse; + ++idx; + } + + return kResultOk; } //============================================================================== @@ -921,6 +1295,95 @@ class AudioPluginProcessorVST3 : public Vst::AudioEffect //============================================================================== + tresult PLUGIN_API canProcessSampleSize (int32 symbolicSampleSize) override + { + if (symbolicSampleSize == Vst::kSample32) + return kResultTrue; + + if (symbolicSampleSize == Vst::kSample64 + && processor != nullptr + && processor->supportsDoublePrecisionProcessing()) + { + return kResultTrue; + } + + return kResultFalse; + } + + uint32 PLUGIN_API getLatencySamples() override + { + return processor != nullptr ? static_cast (jmax (0, processor->getLatencySamples())) + : 0u; + } + + uint32 PLUGIN_API getTailSamples() override + { + if (processor == nullptr) + return Vst::kNoTail; + + const auto tailSamples = processor->getTailSamples(); + return tailSamples > 0 ? static_cast (tailSamples) : Vst::kNoTail; + } + + tresult PLUGIN_API setProcessing (TBool state) override + { + if (processor == nullptr) + return kResultFalse; + + processor->suspendProcessing (! state); + + if (! state) + processor->flush(); + + return kResultOk; + } + + //============================================================================== + + tresult PLUGIN_API getState (IBStream* stream) override + { + if (processor == nullptr || stream == nullptr) + return kResultFalse; + + MemoryBlock processorState; + const auto hasProcessorState = processor->saveStateIntoMemory (processorState).wasOk(); + + auto data = writeWrapperBypassState (vst3WrapperStateMagic, + vst3WrapperStateVersion, + isBypassed, + processorState, + hasProcessorState); + + int32 written = 0; + return stream->write (data.getData(), static_cast (data.getSize()), &written) == kResultOk + && written == static_cast (data.getSize()) + ? kResultOk + : kResultFalse; + } + + tresult PLUGIN_API setState (IBStream* stream) override + { + if (processor == nullptr || stream == nullptr) + return kResultFalse; + + const auto data = readVST3StreamData (*stream); + if (data.isEmpty()) + return kResultFalse; + + const auto wrapperState = readWrapperBypassState (data, vst3WrapperStateMagic, vst3WrapperStateVersion); + if (! wrapperState.hasWrapperState) + return processor->loadStateFromMemory (wrapperState.processorState).wasOk() ? kResultOk : kResultFalse; + + isBypassed = wrapperState.isBypassed; + + if (! wrapperState.hasProcessorState) + return kResultOk; + + return processor->loadStateFromMemory (wrapperState.processorState).wasOk() ? kResultOk : kResultFalse; + } + + //============================================================================== + tresult PLUGIN_API setupProcessing (Vst::ProcessSetup& setup) override { if (processor == nullptr) @@ -929,17 +1392,16 @@ class AudioPluginProcessorVST3 : public Vst::AudioEffect processSetup = setup; processor->setPlaybackConfiguration (setup.sampleRate, setup.maxSamplesPerBlock); - - /* - if (setup.processMode != Vst::kOffline) - processor->setIsRealtime (true); - else - processor->setIsRealtime (false); - */ + processor->setOfflineProcessing (setup.processMode == Vst::kOffline); midiBuffer.ensureSize (4096); midiBuffer.clear(); + paramChangeBuffer.reserve (getDefaultParameterChangeCapacity (*processor)); + const auto totalOutputChannels = getTotalAudioOutputChannels (*processor); + outputChannelsFloat.reserve (static_cast (totalOutputChannels)); + outputChannelsDouble.reserve (static_cast (totalOutputChannels)); + return kResultOk; } @@ -948,27 +1410,68 @@ class AudioPluginProcessorVST3 : public Vst::AudioEffect tresult PLUGIN_API process (Vst::ProcessData& data) override { if (data.processContext != nullptr) + { processContext = *data.processContext; + processor->setOfflineProcessing ((processContext.state & Vst::kOfflineProcessing) != 0); + } // --- Process Parameters --- + bool bypassed = isBypassed; + paramChangeBuffer.clear(); + if (data.inputParameterChanges) { - int32 numParams = data.inputParameterChanges->getParameterCount(); - for (int32 i = 0; i < numParams; i++) + const auto bypassTag = getVST3BypassParameterID (*processor); + + const int32 numParams = data.inputParameterChanges->getParameterCount(); + for (int32 i = 0; i < numParams; ++i) { Vst::IParamValueQueue* queue = data.inputParameterChanges->getParameterData (i); if (queue == nullptr) continue; - int32 numPoints = queue->getPointCount(); + const int32 numPoints = queue->getPointCount(); if (numPoints <= 0) continue; - int32 sampleOffset; - Vst::ParamValue value; - if (queue->getPoint (numPoints - 1, sampleOffset, value) == kResultOk) - processor->getParameters()[i]->setNormalizedValue (static_cast (value)); + const auto tag = queue->getParameterId(); + + if (tag == bypassTag) + { + int32 sampleOffset; + Vst::ParamValue value; + if (queue->getPoint (numPoints - 1, sampleOffset, value) == kResultOk) + { + bypassed = (value >= 0.5); + isBypassed = bypassed; + } + } + else + { + const auto parameter = processor->getParameterByHostID (static_cast (tag)); + if (parameter == nullptr + || parameter->isReadOnly() + || parameter->isPerformingChangeGesture()) + { + continue; + } + + for (int32 p = 0; p < numPoints; ++p) + { + int32 sampleOffset; + Vst::ParamValue value; + if (queue->getPoint (p, sampleOffset, value) == kResultOk) + addParameterChangeByHostParameterID (*processor, + paramChangeBuffer, + static_cast (tag), + static_cast (value), + sampleOffset); + } + } } + + paramChangeBuffer.sort(); + applyParameterChangesToProcessor (*processor, paramChangeBuffer); } // --- Process Events --- @@ -986,23 +1489,36 @@ class AudioPluginProcessorVST3 : public Vst::AudioEffect switch (e.type) { case Vst::Event::kNoteOnEvent: - midiBuffer.addEvent (MidiMessage::noteOn (e.noteOn.channel + 1, e.noteOn.pitch, e.noteOn.velocity), e.sampleOffset); + midiBuffer.addEvent (MidiMessage::noteOn (e.noteOn.channel + 1, + e.noteOn.pitch, + static_cast (e.noteOn.velocity * 127.0f)), + e.sampleOffset); break; case Vst::Event::kNoteOffEvent: - midiBuffer.addEvent (MidiMessage::noteOff (e.noteOff.channel + 1, e.noteOff.pitch, e.noteOff.velocity), e.sampleOffset); + midiBuffer.addEvent (MidiMessage::noteOff (e.noteOff.channel + 1, + e.noteOff.pitch, + static_cast (e.noteOff.velocity * 127.0f)), + e.sampleOffset); break; case Vst::Event::kPolyPressureEvent: - // handle poly pressure if needed + midiBuffer.addEvent (MidiMessage::aftertouchChange (e.polyPressure.channel + 1, + e.polyPressure.pitch, + static_cast (e.polyPressure.pressure * 127.0f)), + e.sampleOffset); break; - case Vst::Event::kDataEvent: - // optional: handle MIDI SysEx or other custom events + case Vst::Event::kLegacyMIDICCOutEvent: + midiBuffer.addEvent (MidiMessage::controllerEvent (e.midiCCOut.channel + 1, + e.midiCCOut.controlNumber, + e.midiCCOut.value), + e.sampleOffset); break; - case Vst::Event::kLegacyMIDICCOutEvent: - // handle legacy CC output + case Vst::Event::kDataEvent: + if (e.data.type == Vst::DataEvent::kMidiSysEx) + midiBuffer.addEvent (e.data.bytes, static_cast (e.data.size), e.sampleOffset); break; default: @@ -1014,21 +1530,82 @@ class AudioPluginProcessorVST3 : public Vst::AudioEffect // --- Process Audio --- if (data.numSamples > 0 && data.outputs != nullptr) { - Vst::AudioBusBuffers& outBus = data.outputs[0]; + const bool useDoublePrecision = processSetup.symbolicSampleSize == Vst::kSample64 + && processor->supportsDoublePrecisionProcessing(); - AudioSampleBuffer audioBuffer ( - reinterpret_cast (outBus.channelBuffers32), - outBus.numChannels, - data.numSamples); + // Copy input audio into output buffers for effects + if (data.inputs != nullptr) + { + for (int32 busIdx = 0; busIdx < std::min (data.numInputs, data.numOutputs); ++busIdx) + { + auto& inBus = data.inputs[busIdx]; + auto& outBus = data.outputs[busIdx]; + + for (int32 ch = 0; ch < std::min (inBus.numChannels, outBus.numChannels); ++ch) + { + if (useDoublePrecision) + { + auto* in = reinterpret_cast (inBus.channelBuffers64[ch]); + auto* out = reinterpret_cast (outBus.channelBuffers64[ch]); + if (in != out) + std::memcpy (out, in, static_cast (data.numSamples) * sizeof (double)); + } + else + { + auto* in = reinterpret_cast (inBus.channelBuffers32[ch]); + auto* out = reinterpret_cast (outBus.channelBuffers32[ch]); + if (in != out) + std::memcpy (out, in, static_cast (data.numSamples) * sizeof (float)); + } + } + } + } + + AudioPluginPlayHeadVST3 playHead (data.processContext); + auto* const playHeadPtr = data.processContext != nullptr ? &playHead : nullptr; + + if (useDoublePrecision) + { + outputChannelsDouble.clear(); + for (int32 busIdx = 0; busIdx < data.numOutputs; ++busIdx) + for (int32 ch = 0; ch < data.outputs[busIdx].numChannels; ++ch) + outputChannelsDouble.push_back (reinterpret_cast (data.outputs[busIdx].channelBuffers64[ch])); + + AudioBuffer audioBuffer (outputChannelsDouble.data(), + static_cast (outputChannelsDouble.size()), + 0, + data.numSamples); + + AudioProcessContext doubleCtx { audioBuffer, midiBuffer, paramChangeBuffer, playHeadPtr }; - processor->processBlock (audioBuffer, midiBuffer); + processAudioBlock (*processor, doubleCtx, bypassed); + } + else + { + outputChannelsFloat.clear(); + for (int32 busIdx = 0; busIdx < data.numOutputs; ++busIdx) + for (int32 ch = 0; ch < data.outputs[busIdx].numChannels; ++ch) + outputChannelsFloat.push_back (reinterpret_cast (data.outputs[busIdx].channelBuffers32[ch])); + + AudioSampleBuffer audioBuffer (outputChannelsFloat.data(), + static_cast (outputChannelsFloat.size()), + 0, + data.numSamples); + + AudioProcessContext context { audioBuffer, midiBuffer, paramChangeBuffer, playHeadPtr }; + + processAudioBlock (*processor, context, bypassed); + } } + if (data.outputEvents != nullptr) + writeMidiBufferToVST3EventList (midiBuffer, *data.outputEvents, data.numSamples); + return kResultOk; } private: - VST3ScopedYupInitialiser scopeInitialiser; + ScopedYupInitialiser_GUI scopeInitialiser; std::unique_ptr processor; @@ -1036,10 +1613,14 @@ class AudioPluginProcessorVST3 : public Vst::AudioEffect Vst::ProcessSetup processSetup; MidiBuffer midiBuffer; + ParameterChangeBuffer paramChangeBuffer; + std::vector outputChannelsFloat; + std::vector outputChannelsDouble; + bool isBypassed = false; }; #if YupPlugin_IsSynth -const auto YupPlugin_Category = Vst::PlugType::kInstrument; +const auto YupPlugin_Category = Vst::PlugType::kInstrumentSynth; #else const auto YupPlugin_Category = Vst::PlugType::kFx; #endif @@ -1059,7 +1640,7 @@ DEF_CLASS2 ( kVstAudioEffectClass, // Component category (do not change this) YupPlugin_Name, // Plugin name Vst::kDistributable, // Distribution status - yup::YupPlugin_Category, // Subcategory (effect) + yup::YupPlugin_Category, // Subcategory YupPlugin_Version, // Plugin version kVstVersionString, // The VST 3 SDK version (do not change this, always use this define) yup::AudioPluginProcessorVST3::createInstance) diff --git a/modules/yup_audio_plugin_client/yup_audio_plugin_client.h b/modules/yup_audio_plugin_client/yup_audio_plugin_client.h index 3d544d631..ae7e7ffe0 100644 --- a/modules/yup_audio_plugin_client/yup_audio_plugin_client.h +++ b/modules/yup_audio_plugin_client/yup_audio_plugin_client.h @@ -40,8 +40,33 @@ */ #pragma once -#define YUP_AUDIO_PPLUGIN_CLIENT_H_INCLUDED +#define YUP_AUDIO_PLUGIN_CLIENT_H_INCLUDED -#include +//============================================================================== +/** Config: YUP_ENABLE_PLUGIN_CLIENT_AU_LOGGING + + Enable debug logging for AUv2 plugin client. +*/ +#ifndef YUP_ENABLE_PLUGIN_CLIENT_AU_LOGGING +#define YUP_ENABLE_PLUGIN_CLIENT_AU_LOGGING 0 +#endif + +/** Config: YUP_ENABLE_PLUGIN_CLIENT_CLAP_LOGGING + + Enable debug logging for CLAP plugin client. +*/ +#ifndef YUP_ENABLE_PLUGIN_CLIENT_CLAP_LOGGING +#define YUP_ENABLE_PLUGIN_CLIENT_CLAP_LOGGING 0 +#endif + +/** Config: YUP_ENABLE_PLUGIN_CLIENT_VST3_LOGGING + + Enable debug logging for VST3 plugin client. +*/ +#ifndef YUP_ENABLE_PLUGIN_CLIENT_VST3_LOGGING +#define YUP_ENABLE_PLUGIN_CLIENT_VST3_LOGGING 0 +#endif //============================================================================== + +#include diff --git a/modules/yup_audio_plugin_host/host/yup_AudioPluginHostContext.h b/modules/yup_audio_plugin_host/host/yup_AudioPluginHostContext.h index 9edcd76b7..02096fbbe 100644 --- a/modules/yup_audio_plugin_host/host/yup_AudioPluginHostContext.h +++ b/modules/yup_audio_plugin_host/host/yup_AudioPluginHostContext.h @@ -43,9 +43,6 @@ struct AudioPluginHostContext /** True when the host is preparing the plugin for offline/non-realtime rendering. */ bool isNonRealtime = false; - /** Optional playhead — the host sets this before processing. */ - AudioPlayHead* playHead = nullptr; - /** Host application name reported to the plugin. */ String hostName = "YUP Audio Plugin Host"; diff --git a/modules/yup_audio_plugin_host/host/yup_AudioPluginInstance.cpp b/modules/yup_audio_plugin_host/host/yup_AudioPluginInstance.cpp index fb4cb596c..15971fb86 100644 --- a/modules/yup_audio_plugin_host/host/yup_AudioPluginInstance.cpp +++ b/modules/yup_audio_plugin_host/host/yup_AudioPluginInstance.cpp @@ -97,16 +97,14 @@ bool AudioPluginInstance::isBypassed() const noexcept return bypassed; } -void AudioPluginInstance::processBlockBypassed (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) +void AudioPluginInstance::processBlockBypassed (AudioProcessContext& context) { - ignoreUnused (midiBuffer); - processPluginBypassedBlock (pluginDescription, audioBuffer); + processPluginBypassedBlock (pluginDescription, context.audio); } -void AudioPluginInstance::processBlockBypassed (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) +void AudioPluginInstance::processBlockBypassed (AudioProcessContext& context) { - ignoreUnused (midiBuffer); - processPluginBypassedBlock (pluginDescription, audioBuffer); + processPluginBypassedBlock (pluginDescription, context.audio); } } // namespace yup diff --git a/modules/yup_audio_plugin_host/host/yup_AudioPluginInstance.h b/modules/yup_audio_plugin_host/host/yup_AudioPluginInstance.h index e696344b6..3067f51f9 100644 --- a/modules/yup_audio_plugin_host/host/yup_AudioPluginInstance.h +++ b/modules/yup_audio_plugin_host/host/yup_AudioPluginInstance.h @@ -69,12 +69,12 @@ class AudioPluginInstance : public AudioProcessor /** Processes a bypassed single-precision block by copying matching inputs to outputs and clearing outputs without matching inputs. */ - void processBlockBypassed (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) override; + void processBlockBypassed (AudioProcessContext& context) override; /** Processes a bypassed double-precision block by copying matching inputs to outputs and clearing outputs without matching inputs. */ - void processBlockBypassed (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) override; + void processBlockBypassed (AudioProcessContext& context) override; protected: AudioPluginDescription pluginDescription; diff --git a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.mm b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.mm index d58a7cb5e..8afa3d4a4 100644 --- a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.mm +++ b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.mm @@ -381,6 +381,7 @@ void prepareToPlay(float sampleRate, int maxBlockSize) override releaseResources(); renderSampleTime = 0.0; + updateOfflineRenderMode(); const auto numHostedChannels = jmax(2, pluginDescription.numInputChannels, @@ -401,6 +402,7 @@ void prepareToPlay(float sampleRate, int maxBlockSize) override configureStreamFormat(sampleRateValue, numHostedChannels); installInputCallback(); + installHostCallbacks(); installMIDIOutputCallback(); if (![NSThread isMainThread]) @@ -435,13 +437,21 @@ void releaseResources() override AudioUnitUninitialize(audioUnit); } - void processBlock(AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) override + void nonRealtimeStateChanged() override { + updateOfflineRenderMode(); + } + + void processBlock (AudioProcessContext& context) override + { + auto& audioBuffer = context.audio; + auto& midiBuffer = context.midi; + ScopedNoDenormals noDenormals; if (isBypassed()) { - processBlockBypassed(audioBuffer, midiBuffer); + processBlockBypassed (context); return; } @@ -479,6 +489,9 @@ void processBlock(AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) overr currentInputBuffer = &inputBuffer; currentInputNumChannels = numChannels; currentInputNumSamples = numSamples; + currentPlayHead = context.playHead; + currentPlayHeadPositionQueried = false; + currentPlayHeadPosition = std::nullopt; AudioUnitRenderActionFlags flags = 0; AudioTimeStamp timeStamp{}; @@ -499,6 +512,9 @@ void processBlock(AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) overr currentInputNumChannels = 0; currentInputNumSamples = 0; currentMidiOutputBuffer = nullptr; + currentPlayHead = nullptr; + currentPlayHeadPositionQueried = false; + currentPlayHeadPosition = std::nullopt; if (status != noErr) { @@ -962,6 +978,21 @@ void installInputCallback() &callback, sizeof(callback)); } + void installHostCallbacks() + { + HostCallbackInfo callbacks {}; + callbacks.hostUserData = this; + callbacks.beatAndTempoProc = hostBeatAndTempoCallback; + callbacks.musicalTimeLocationProc = hostMusicalTimeLocationCallback; + callbacks.transportStateProc = hostTransportStateCallback; + callbacks.transportStateProc2 = hostTransportState2Callback; + + AudioUnitSetProperty(audioUnit, + kAudioUnitProperty_HostCallbacks, + kAudioUnitScope_Global, 0, + &callbacks, sizeof(callbacks)); + } + void installMIDIOutputCallback() { AUMIDIOutputCallbackStruct callback{}; @@ -974,6 +1005,183 @@ void installMIDIOutputCallback() &callback, sizeof(callback)); } + static OSStatus hostBeatAndTempoCallback(void* userData, + Float64* outCurrentBeat, + Float64* outCurrentTempo) + { + auto* instance = static_cast(userData); + if (instance == nullptr) + return kAudioUnitErr_CannotDoInCurrentContext; + + const auto* position = instance->getCurrentPlayHeadPosition(); + if (position == nullptr) + return kAudioUnitErr_CannotDoInCurrentContext; + + if (outCurrentBeat != nullptr) + { + const auto ppq = position->getPpqPosition(); + if (! ppq.has_value()) + return kAudioUnitErr_CannotDoInCurrentContext; + + *outCurrentBeat = *ppq; + } + + if (outCurrentTempo != nullptr) + { + const auto bpm = position->getBpm(); + if (! bpm.has_value()) + return kAudioUnitErr_CannotDoInCurrentContext; + + *outCurrentTempo = *bpm; + } + + return noErr; + } + + static OSStatus hostMusicalTimeLocationCallback(void* userData, + UInt32* outDeltaSampleOffsetToNextBeat, + Float32* outTimeSigNumerator, + UInt32* outTimeSigDenominator, + Float64* outCurrentMeasureDownBeat) + { + auto* instance = static_cast(userData); + if (instance == nullptr) + return kAudioUnitErr_CannotDoInCurrentContext; + + const auto* position = instance->getCurrentPlayHeadPosition(); + if (position == nullptr) + return kAudioUnitErr_CannotDoInCurrentContext; + + if (outDeltaSampleOffsetToNextBeat != nullptr) + { + const auto ppq = position->getPpqPosition(); + const auto bpm = position->getBpm(); + const auto sampleRate = instance->getSampleRate(); + + if (! ppq.has_value() || ! bpm.has_value() || sampleRate <= 0.0) + return kAudioUnitErr_CannotDoInCurrentContext; + + const auto nextBeat = std::ceil (*ppq); + const auto beatsToNext = jmax (0.0, nextBeat - *ppq); + *outDeltaSampleOffsetToNextBeat = static_cast (beatsToNext * (60.0 / *bpm) * sampleRate); + } + + if (outTimeSigNumerator != nullptr || outTimeSigDenominator != nullptr) + { + const auto timeSignature = position->getTimeSignature(); + if (! timeSignature.has_value()) + return kAudioUnitErr_CannotDoInCurrentContext; + + if (outTimeSigNumerator != nullptr) + *outTimeSigNumerator = static_cast (timeSignature->numerator); + + if (outTimeSigDenominator != nullptr) + *outTimeSigDenominator = static_cast (timeSignature->denominator); + } + + if (outCurrentMeasureDownBeat != nullptr) + { + const auto barStart = position->getPpqPositionOfLastBarStart(); + if (! barStart.has_value()) + return kAudioUnitErr_CannotDoInCurrentContext; + + *outCurrentMeasureDownBeat = *barStart; + } + + return noErr; + } + + static OSStatus hostTransportStateCallback(void* userData, + Boolean* outIsPlaying, + Boolean* outTransportStateChanged, + Float64* outCurrentSampleInTimeLine, + Boolean* outIsCycling, + Float64* outCycleStartBeat, + Float64* outCycleEndBeat) + { + return fillHostTransportState(userData, + outIsPlaying, + nullptr, + outTransportStateChanged, + outCurrentSampleInTimeLine, + outIsCycling, + outCycleStartBeat, + outCycleEndBeat); + } + + static OSStatus hostTransportState2Callback(void* userData, + Boolean* outIsPlaying, + Boolean* outIsRecording, + Boolean* outTransportStateChanged, + Float64* outCurrentSampleInTimeLine, + Boolean* outIsCycling, + Float64* outCycleStartBeat, + Float64* outCycleEndBeat) + { + return fillHostTransportState(userData, + outIsPlaying, + outIsRecording, + outTransportStateChanged, + outCurrentSampleInTimeLine, + outIsCycling, + outCycleStartBeat, + outCycleEndBeat); + } + + static OSStatus fillHostTransportState(void* userData, + Boolean* outIsPlaying, + Boolean* outIsRecording, + Boolean* outTransportStateChanged, + Float64* outCurrentSampleInTimeLine, + Boolean* outIsCycling, + Float64* outCycleStartBeat, + Float64* outCycleEndBeat) + { + auto* instance = static_cast(userData); + if (instance == nullptr) + return kAudioUnitErr_CannotDoInCurrentContext; + + const auto* position = instance->getCurrentPlayHeadPosition(); + if (position == nullptr) + return kAudioUnitErr_CannotDoInCurrentContext; + + if (outIsPlaying != nullptr) + *outIsPlaying = position->getIsPlaying(); + + if (outIsRecording != nullptr) + *outIsRecording = position->getIsRecording(); + + if (outTransportStateChanged != nullptr) + *outTransportStateChanged = false; + + if (outCurrentSampleInTimeLine != nullptr) + { + const auto timeInSamples = position->getTimeInSamples(); + if (! timeInSamples.has_value()) + return kAudioUnitErr_CannotDoInCurrentContext; + + *outCurrentSampleInTimeLine = static_cast (*timeInSamples); + } + + if (outIsCycling != nullptr) + *outIsCycling = position->getIsLooping(); + + if (outCycleStartBeat != nullptr || outCycleEndBeat != nullptr) + { + const auto loopPoints = position->getLoopPoints(); + if (! loopPoints.has_value()) + return kAudioUnitErr_CannotDoInCurrentContext; + + if (outCycleStartBeat != nullptr) + *outCycleStartBeat = loopPoints->ppqStart; + + if (outCycleEndBeat != nullptr) + *outCycleEndBeat = loopPoints->ppqEnd; + } + + return noErr; + } + void sendMidiInputEvents(const MidiBuffer& midiBuffer) { for (const auto& metadata : midiBuffer) @@ -1194,6 +1402,20 @@ void removeLatencyListener() AudioUnitRemovePropertyListenerWithUserData(audioUnit, kAudioUnitProperty_Latency, latencyPropertyChanged, this); } + void updateOfflineRenderMode() + { + if (audioUnit == nullptr) + return; + + UInt32 offline = isNonRealtime() ? 1u : 0u; + AudioUnitSetProperty(audioUnit, + kAudioUnitProperty_OfflineRender, + kAudioUnitScope_Global, + 0, + &offline, + sizeof(offline)); + } + static void latencyPropertyChanged(void* userData, AudioUnit, AudioUnitPropertyID propertyID, @@ -1208,6 +1430,20 @@ static void latencyPropertyChanged(void* userData, instance->setLatencySamples(instance->getLatencySamples()); } + const AudioPlayHead::PositionInfo* getCurrentPlayHeadPosition() + { + if (currentPlayHead == nullptr) + return nullptr; + + if (! currentPlayHeadPositionQueried) + { + currentPlayHeadPosition = currentPlayHead->getPosition(); + currentPlayHeadPositionQueried = true; + } + + return currentPlayHeadPosition.has_value() ? &*currentPlayHeadPosition : nullptr; + } + AudioUnit audioUnit = nullptr; AUEventListenerRef eventListener = nullptr; int currentPreset = 0; @@ -1217,6 +1453,9 @@ static void latencyPropertyChanged(void* userData, MidiBuffer outputMidiBuffer; AudioBuffer* currentInputBuffer = nullptr; MidiBuffer* currentMidiOutputBuffer = nullptr; + AudioPlayHead* currentPlayHead = nullptr; + bool currentPlayHeadPositionQueried = false; + std::optional currentPlayHeadPosition; int currentInputNumChannels = 0; int currentInputNumSamples = 0; int preparedNumChannels = 0; @@ -1272,10 +1511,13 @@ static void latencyPropertyChanged(void* userData, AudioComponentDescription acd{}; AudioComponentGetDescription(comp, &acd); auto desc = descriptionFromComponent(comp, acd); + YUP_MODULE_DBG (PLUGIN_HOST_AU, "scan found: " << desc.name << " [" << desc.identifier << "]"); results.push_back(std::move(desc)); } } + YUP_MODULE_DBG (PLUGIN_HOST_AU, "scan complete: " << results.size() << " AudioComponents found"); + if (results.empty()) return makeResultValueFail("No AudioComponents found in registry"); @@ -1286,11 +1528,17 @@ static void latencyPropertyChanged(void* userData, const AudioPluginDescription& description, const AudioPluginHostContext& context) { + YUP_MODULE_DBG (PLUGIN_HOST_AU, "loading: " << description.name << " [" << description.identifier << "]"); + auto instance = AUv2Instance::create(description, context); if (instance == nullptr) + { + YUP_MODULE_DBG (PLUGIN_HOST_AU, "load failed: " << description.name); return makeResultValueFail("Failed to instantiate AUv2 plugin: " + description.name); + } + YUP_MODULE_DBG (PLUGIN_HOST_AU, "loaded: " << description.name); return makeResultValueOk(std::move(instance)); } diff --git a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.cpp b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.cpp index d888c664b..784107a6f 100644 --- a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.cpp +++ b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.cpp @@ -33,10 +33,68 @@ struct CLAPModule CLAPModuleHandle handle = nullptr; const clap_plugin_entry_t* entry = nullptr; +#if YUP_MAC + template + struct CFObjectDeleter + { + void operator() (CFType object) const noexcept + { + if (object != nullptr) + CFRelease (object); + } + }; + + template + using CFUniquePtr = std::unique_ptr, CFObjectDeleter>; + + static CFUniquePtr createBundle (const File& file) + { + const auto path = file.getFullPathName(); + CFUniquePtr url (CFURLCreateFromFileSystemRepresentation (kCFAllocatorDefault, + reinterpret_cast (path.toRawUTF8()), + static_cast (path.getNumBytesAsUTF8()), + true)); + + if (url == nullptr) + return {}; + + return CFUniquePtr (CFBundleCreate (kCFAllocatorDefault, url.get())); + } + + static File getBundleExecutableFile (CFBundleRef bundleToUse, const File& bundleFile) + { + const auto macOSFolder = bundleFile.getChildFile ("Contents/MacOS"); + + if (bundleToUse != nullptr) + { + auto* executableValue = CFBundleGetValueForInfoDictionaryKey (bundleToUse, kCFBundleExecutableKey); + + if (executableValue != nullptr && CFGetTypeID (executableValue) == CFStringGetTypeID()) + return macOSFolder.getChildFile (String::fromCFString (static_cast (executableValue))); + } + + return macOSFolder.getChildFile (bundleFile.getFileNameWithoutExtension()); + } +#endif + static std::unique_ptr load (const File& file) { auto m = std::make_unique(); - m->handle = clapLoadModule (file.getFullPathName().toRawUTF8()); + auto libraryFile = file; + +#if YUP_MAC + if (file.isDirectory()) + { + auto bundle = createBundle (file); + + if (bundle == nullptr) + return nullptr; + + libraryFile = getBundleExecutableFile (bundle.get(), file); + } +#endif + + m->handle = clapLoadModule (libraryFile.getFullPathName().toRawUTF8()); if (m->handle == nullptr) return nullptr; @@ -76,7 +134,12 @@ struct YUPCLAPHost clap_host_t host {}; clap_host_note_ports_t notePorts {}; clap_host_latency_t latency {}; + clap_host_gui_t gui {}; std::function latencyChanged; + std::function guiResizeRequested; + std::function guiShowRequested; + std::function guiHideRequested; + std::function guiClosed; String hostName; String hostVendor; String hostVersion; @@ -103,6 +166,40 @@ struct YUPCLAPHost if (self->latencyChanged != nullptr) self->latencyChanged(); }; + gui.resize_hints_changed = [] (const clap_host_t*) {}; + gui.request_resize = [] (const clap_host_t* host, uint32_t width, uint32_t height) -> bool + { + if (! MessageManager::existsAndIsCurrentThread()) + return false; + + auto* self = static_cast (host->host_data); + return self->guiResizeRequested != nullptr && self->guiResizeRequested (width, height); + }; + gui.request_show = [] (const clap_host_t* host) -> bool + { + if (! MessageManager::existsAndIsCurrentThread()) + return false; + + auto* self = static_cast (host->host_data); + return self->guiShowRequested != nullptr && self->guiShowRequested(); + }; + gui.request_hide = [] (const clap_host_t* host) -> bool + { + if (! MessageManager::existsAndIsCurrentThread()) + return false; + + auto* self = static_cast (host->host_data); + return self->guiHideRequested != nullptr && self->guiHideRequested(); + }; + gui.closed = [] (const clap_host_t* host, bool wasDestroyed) + { + if (! MessageManager::existsAndIsCurrentThread()) + return; + + auto* self = static_cast (host->host_data); + if (self->guiClosed != nullptr) + self->guiClosed (wasDestroyed); + }; host.get_extension = [] (const clap_host_t* host, const char* extensionId) -> const void* { @@ -113,6 +210,9 @@ struct YUPCLAPHost if (std::strcmp (extensionId, CLAP_EXT_LATENCY) == 0) return &self->latency; + if (std::strcmp (extensionId, CLAP_EXT_GUI) == 0) + return &self->gui; + return nullptr; }; host.request_restart = [] (const clap_host_t*) {}; @@ -121,6 +221,277 @@ struct YUPCLAPHost } }; +//============================================================================== +#if YUP_MAC +void* getCLAPParentViewFromNativeHandle (void* nativeHandle) +{ + if (nativeHandle == nullptr) + return nullptr; + + id nativeObject = (__bridge id) nativeHandle; + if ([nativeObject isKindOfClass:[NSWindow class]]) + return (__bridge void*) [(NSWindow*) nativeObject contentView]; + + if ([nativeObject isKindOfClass:[NSView class]]) + return nativeHandle; + + return nullptr; +} +#endif + +const char* getCLAPWindowApi() +{ +#if YUP_MAC + return CLAP_WINDOW_API_COCOA; +#elif YUP_WINDOWS + return CLAP_WINDOW_API_WIN32; +#elif YUP_LINUX + return CLAP_WINDOW_API_X11; +#else + return nullptr; +#endif +} + +bool initialiseCLAPWindow (clap_window_t& window, void* nativeHandle) +{ + if (nativeHandle == nullptr) + return false; + + window.api = getCLAPWindowApi(); + if (window.api == nullptr) + return false; + +#if YUP_MAC + window.cocoa = getCLAPParentViewFromNativeHandle (nativeHandle); + return window.cocoa != nullptr; +#elif YUP_WINDOWS + window.win32 = nativeHandle; + return true; +#elif YUP_LINUX + window.x11 = static_cast (reinterpret_cast (nativeHandle)); + return window.x11 != 0; +#else + ignoreUnused (window); + return false; +#endif +} + +bool canCreateCLAPEditor (const clap_plugin_t* plugin) +{ + if (plugin == nullptr) + return false; + + const auto* gui = reinterpret_cast ( + plugin->get_extension (plugin, CLAP_EXT_GUI)); + + const auto* windowApi = getCLAPWindowApi(); + return gui != nullptr + && windowApi != nullptr + && gui->is_api_supported != nullptr + && gui->is_api_supported (plugin, windowApi, false); +} + +class CLAPEditor final : public AudioProcessorEditor +{ +public: + static std::unique_ptr create (const clap_plugin_t* plugin, YUPCLAPHost& host) + { + if (! canCreateCLAPEditor (plugin)) + return nullptr; + + const auto* gui = reinterpret_cast ( + plugin->get_extension (plugin, CLAP_EXT_GUI)); + + if (gui == nullptr || gui->create == nullptr || ! gui->create (plugin, getCLAPWindowApi(), false)) + return nullptr; + + return std::unique_ptr (new CLAPEditor (plugin, gui, host)); + } + + ~CLAPEditor() override + { + detachPlugView(); + + host.guiResizeRequested = nullptr; + host.guiShowRequested = nullptr; + host.guiHideRequested = nullptr; + host.guiClosed = nullptr; + + if (gui != nullptr && gui->destroy != nullptr) + gui->destroy (clapPlugin); + } + + bool isResizable() const override + { + return gui != nullptr && gui->can_resize != nullptr && gui->can_resize (clapPlugin); + } + + Size getPreferredSize() const override { return preferredSize; } + + void paint (Graphics& g) override + { + g.setFillColor (Color (0xff101417)); + g.fillAll(); + } + + void resized() override + { + resizePlugViewToBounds(); + } + + void attachedToNative() override + { + attachPlugView(); + } + + void detachedFromNative() override + { + detachPlugView(); + } + +private: + CLAPEditor (const clap_plugin_t* plugin, const clap_plugin_gui_t* guiExtension, YUPCLAPHost& hostToUse) + : clapPlugin (plugin) + , gui (guiExtension) + , host (hostToUse) + { + uint32_t width = 0; + uint32_t height = 0; + + if (gui != nullptr + && gui->get_size != nullptr + && gui->get_size (clapPlugin, &width, &height) + && width > 0 + && height > 0) + { + preferredSize = { + jmax (320, static_cast (width)), + jmax (240, static_cast (height)) + }; + } + + setSize (preferredSize.to()); + + host.guiResizeRequested = [this] (uint32_t widthToUse, uint32_t heightToUse) + { + return handleResizeRequest (widthToUse, heightToUse); + }; + host.guiShowRequested = [this] + { + return handleShowRequest(); + }; + host.guiHideRequested = [this] + { + return handleHideRequest(); + }; + host.guiClosed = [this] (bool) + { + shown = false; + attached = false; + }; + } + + bool handleResizeRequest (uint32_t width, uint32_t height) + { + if (width == 0 || height == 0) + return false; + + preferredSize = { + jmax (1, static_cast (width)), + jmax (1, static_cast (height)) + }; + + if (auto* topLevel = getTopLevelComponent()) + topLevel->setSize (preferredSize.to()); + else + setSize (preferredSize.to()); + + return true; + } + + bool handleShowRequest() + { + if (auto* topLevel = getTopLevelComponent()) + { + topLevel->setVisible (true); + return true; + } + + return false; + } + + bool handleHideRequest() + { + if (auto* topLevel = getTopLevelComponent()) + { + topLevel->setVisible (false); + return true; + } + + return false; + } + + void attachPlugView() + { + if (gui == nullptr || clapPlugin == nullptr || attached) + return; + + auto* nativeComponent = getNativeComponent(); + if (nativeComponent == nullptr) + return; + + clap_window_t parentWindow {}; + if (! initialiseCLAPWindow (parentWindow, nativeComponent->getNativeHandle())) + return; + + if (gui->set_parent == nullptr || ! gui->set_parent (clapPlugin, &parentWindow)) + return; + + attached = true; + resizePlugViewToBounds(); + + if (! shown && gui->show != nullptr) + shown = gui->show (clapPlugin); + } + + void detachPlugView() + { + if (gui == nullptr || clapPlugin == nullptr) + return; + + if (shown && gui->hide != nullptr) + gui->hide (clapPlugin); + + shown = false; + attached = false; + } + + void resizePlugViewToBounds() + { + if (gui == nullptr || clapPlugin == nullptr || ! attached || gui->set_size == nullptr) + return; + + const auto bounds = getBoundsRelativeToTopLevelComponent(); + uint32_t width = static_cast (jmax (1.0f, bounds.getWidth())); + uint32_t height = static_cast (jmax (1.0f, bounds.getHeight())); + + if (gui->adjust_size != nullptr) + gui->adjust_size (clapPlugin, &width, &height); + + if (width > 0 && height > 0) + gui->set_size (clapPlugin, width, height); + } + + const clap_plugin_t* clapPlugin = nullptr; + const clap_plugin_gui_t* gui = nullptr; + YUPCLAPHost& host; + Size preferredSize { 640, 480 }; + bool attached = false; + bool shown = false; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CLAPEditor) +}; + struct CLAPInputEvents { std::deque parameterEvents; @@ -285,6 +656,76 @@ struct CLAPOutputEvents } }; +std::optional createCLAPTransport (AudioPlayHead* playHead, double sampleRate) +{ + if (playHead == nullptr) + return std::nullopt; + + const auto optPosition = playHead->getPosition(); + if (! optPosition.has_value()) + return std::nullopt; + + const auto& position = optPosition.value(); + + clap_event_transport_t transport {}; + transport.header.size = sizeof (transport); + transport.header.space_id = CLAP_CORE_EVENT_SPACE_ID; + transport.header.type = CLAP_EVENT_TRANSPORT; + + if (auto timeInSeconds = position.getTimeInSeconds()) + { + transport.flags |= CLAP_TRANSPORT_HAS_SECONDS_TIMELINE; + transport.song_pos_seconds = static_cast (*timeInSeconds * CLAP_SECTIME_FACTOR); + } + else if (auto timeInSamples = position.getTimeInSamples(); timeInSamples && sampleRate > 0.0) + { + transport.flags |= CLAP_TRANSPORT_HAS_SECONDS_TIMELINE; + transport.song_pos_seconds = static_cast ((*timeInSamples / sampleRate) * CLAP_SECTIME_FACTOR); + } + + if (auto ppq = position.getPpqPosition()) + { + transport.flags |= CLAP_TRANSPORT_HAS_BEATS_TIMELINE; + transport.song_pos_beats = static_cast (*ppq * CLAP_BEATTIME_FACTOR); + } + + if (auto tempo = position.getBpm()) + { + transport.flags |= CLAP_TRANSPORT_HAS_TEMPO; + transport.tempo = *tempo; + } + + if (auto timeSignature = position.getTimeSignature()) + { + transport.flags |= CLAP_TRANSPORT_HAS_TIME_SIGNATURE; + transport.tsig_num = static_cast (timeSignature->numerator); + transport.tsig_denom = static_cast (timeSignature->denominator); + } + + if (auto loopPoints = position.getLoopPoints()) + { + transport.loop_start_beats = static_cast (loopPoints->ppqStart * CLAP_BEATTIME_FACTOR); + transport.loop_end_beats = static_cast (loopPoints->ppqEnd * CLAP_BEATTIME_FACTOR); + } + + if (auto barStart = position.getPpqPositionOfLastBarStart()) + transport.bar_start = static_cast (*barStart * CLAP_BEATTIME_FACTOR); + + if (auto barCount = position.getBarCount()) + transport.bar_number = static_cast (*barCount); + + if (position.getIsPlaying()) + transport.flags |= CLAP_TRANSPORT_IS_PLAYING; + + if (position.getIsRecording()) + transport.flags |= CLAP_TRANSPORT_IS_RECORDING; + + if (position.getIsLooping()) + transport.flags |= CLAP_TRANSPORT_IS_LOOP_ACTIVE; + + return transport; +} + } // namespace //============================================================================== @@ -332,6 +773,7 @@ class CLAPInstance : public AudioPluginInstance preparedInPtrs.resize (static_cast (numChannels)); preparedOutPtrs.resize (static_cast (numChannels)); + updateRenderMode(); clapPlugin->activate (clapPlugin, sampleRate, 1, static_cast (jmax (1, maxBlockSize))); updateRenderMode(); @@ -344,19 +786,23 @@ class CLAPInstance : public AudioPluginInstance { clapPlugin->stop_processing (clapPlugin); clapPlugin->deactivate (clapPlugin); + currentRenderMode = -1; } } - void processBlock (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) override + void processBlock (AudioProcessContext& context) override { ScopedNoDenormals noDenormals; if (isBypassed()) { - processBlockBypassed (audioBuffer, midiBuffer); + processBlockBypassed (context); return; } + auto& audioBuffer = context.audio; + auto& midiBuffer = context.midi; + const int numSamples = audioBuffer.getNumSamples(); const int numChannels = audioBuffer.getNumChannels(); @@ -396,6 +842,9 @@ class CLAPInstance : public AudioPluginInstance process.audio_outputs = outputBuf.channel_count > 0 ? &outputBuf : nullptr; process.audio_outputs_count = outputBuf.channel_count > 0 ? 1 : 0; + const auto transport = createCLAPTransport (context.playHead, getSampleRate()); + process.transport = transport.has_value() ? &*transport : nullptr; + clapInputEvents.clear(); const auto params = getParameters(); const auto numParams = yup::jmin (params.size(), clapParameterIds.size()); @@ -498,7 +947,18 @@ class CLAPInstance : public AudioPluginInstance //============================================================================== - bool hasEditor() const override { return false; } + bool hasEditor() const override + { + return canCreateCLAPEditor (clapPlugin); + } + + AudioProcessorEditor* createEditor() override + { + if (auto editor = CLAPEditor::create (clapPlugin, *yupHost)) + return editor.release(); + + return nullptr; + } int getLatencySamples() override { @@ -638,11 +1098,25 @@ class CLAPInstance : public AudioPluginInstance if (clapPlugin == nullptr) return; + const auto mode = isNonRealtime() ? CLAP_RENDER_OFFLINE : CLAP_RENDER_REALTIME; + if (currentRenderMode == mode) + return; + auto* renderExt = reinterpret_cast ( clapPlugin->get_extension (clapPlugin, CLAP_EXT_RENDER)); if (renderExt != nullptr && renderExt->set != nullptr) - renderExt->set (clapPlugin, isNonRealtime() ? CLAP_RENDER_OFFLINE : CLAP_RENDER_REALTIME); + { + if (mode == CLAP_RENDER_OFFLINE + && renderExt->has_hard_realtime_requirement != nullptr + && renderExt->has_hard_realtime_requirement (clapPlugin)) + { + return; + } + + if (renderExt->set (clapPlugin, mode)) + currentRenderMode = mode; + } } void nonRealtimeStateChanged() override @@ -660,6 +1134,7 @@ class CLAPInstance : public AudioPluginInstance std::vector preparedOutPtrs; MidiBuffer outputMidiBuffer; std::vector clapParameterIds; + clap_plugin_render_mode currentRenderMode = -1; int currentPreset = 0; }; @@ -709,15 +1184,23 @@ ResultValue> CLAPFormat::scanFile (const Fil if (file.getFileExtension().toLowerCase() != ".clap") return makeResultValueFail ("Not a CLAP file"); + YUP_MODULE_DBG (PLUGIN_HOST_CLAP, "scanning: " << file.getFullPathName()); + auto mod = CLAPModule::load (file); if (mod == nullptr) + { + YUP_MODULE_DBG (PLUGIN_HOST_CLAP, "failed to load module: " << file.getFullPathName()); return makeResultValueFail ("Failed to load CLAP module: " + file.getFullPathName()); + } const clap_plugin_factory_t* factory = reinterpret_cast ( mod->entry->get_factory (CLAP_PLUGIN_FACTORY_ID)); if (factory == nullptr) + { + YUP_MODULE_DBG (PLUGIN_HOST_CLAP, "no plugin factory in: " << file.getFullPathName()); return makeResultValueFail ("No plugin factory in: " + file.getFullPathName()); + } std::vector results; const uint32_t count = factory->get_plugin_count (factory); @@ -803,9 +1286,12 @@ ResultValue> CLAPFormat::scanFile (const Fil } } + YUP_MODULE_DBG (PLUGIN_HOST_CLAP, "scan found: " << desc.name << " [" << desc.identifier << "]"); results.push_back (std::move (desc)); } + YUP_MODULE_DBG (PLUGIN_HOST_CLAP, "scan complete: " << results.size() << " plugins in " << file.getFileName()); + if (results.empty()) return makeResultValueFail ("No plugins found in: " + file.getFullPathName()); @@ -816,11 +1302,17 @@ ResultValue> CLAPFormat::loadPlugin ( const AudioPluginDescription& description, const AudioPluginHostContext& context) { + YUP_MODULE_DBG (PLUGIN_HOST_CLAP, "loading: " << description.name << " [" << description.identifier << "]"); + auto instance = CLAPInstance::create (description, context); if (instance == nullptr) + { + YUP_MODULE_DBG (PLUGIN_HOST_CLAP, "load failed: " << description.name); return makeResultValueFail ("Failed to load CLAP plugin: " + description.name); + } + YUP_MODULE_DBG (PLUGIN_HOST_CLAP, "loaded: " << description.name); return makeResultValueOk (std::move (instance)); } diff --git a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.cpp b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.cpp index a686776e1..349517c7c 100644 --- a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.cpp +++ b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.cpp @@ -532,10 +532,40 @@ struct VST3Module } #endif + static File findFirstPackageBinary (const File& package, const String& wildcardPattern) + { + Array files; + package.getChildFile ("Contents").findChildFiles (files, File::findFiles, true, wildcardPattern, File::FollowSymlinks::noCycles); + + const auto expectedName = package.getFileNameWithoutExtension(); + + for (const auto& file : files) + if (file.getFileNameWithoutExtension() == expectedName) + return file; + + return files.isEmpty() ? File() : files.getFirst(); + } + + static File getPackageBinaryFile (const File& file) + { + if (! file.isDirectory()) + return file; + +#if YUP_MAC + return {}; +#elif YUP_WINDOWS + return findFirstPackageBinary (file, "*.vst3"); +#elif YUP_LINUX + return findFirstPackageBinary (file, "*.so"); +#else + return file; +#endif + } + static std::unique_ptr load (const File& file) { auto m = std::make_unique(); - auto libraryFile = file; + auto libraryFile = getPackageBinaryFile (file); #if YUP_MAC if (file.isDirectory()) @@ -592,6 +622,8 @@ class HostComponentHandler : public Vst::IComponentHandler { public: using RestartCallback = std::function; + using ParameterGestureCallback = std::function; + using ParameterEditCallback = std::function; HostComponentHandler() { @@ -602,14 +634,29 @@ class HostComponentHandler : public Vst::IComponentHandler FUNKNOWN_DTOR } - tresult PLUGIN_API beginEdit (Vst::ParamID) override + tresult PLUGIN_API beginEdit (Vst::ParamID tag) override + { + if (beginEditCallback != nullptr) + beginEditCallback (tag); + + return kResultOk; + } + + tresult PLUGIN_API performEdit (Vst::ParamID tag, Vst::ParamValue value) override { + if (performEditCallback != nullptr) + performEditCallback (tag, value); + return kResultOk; } - tresult PLUGIN_API performEdit (Vst::ParamID, Vst::ParamValue) override { return kResultOk; } + tresult PLUGIN_API endEdit (Vst::ParamID tag) override + { + if (endEditCallback != nullptr) + endEditCallback (tag); - tresult PLUGIN_API endEdit (Vst::ParamID) override { return kResultOk; } + return kResultOk; + } tresult PLUGIN_API restartComponent (int32 flags) override { @@ -624,10 +671,22 @@ class HostComponentHandler : public Vst::IComponentHandler restartCallback = std::move (callback); } + void setParameterEditCallbacks (ParameterGestureCallback beginCallback, + ParameterEditCallback performCallback, + ParameterGestureCallback endCallback) + { + beginEditCallback = std::move (beginCallback); + performEditCallback = std::move (performCallback); + endEditCallback = std::move (endCallback); + } + DECLARE_FUNKNOWN_METHODS private: RestartCallback restartCallback; + ParameterGestureCallback beginEditCallback; + ParameterEditCallback performEditCallback; + ParameterGestureCallback endEditCallback; }; IMPLEMENT_FUNKNOWN_METHODS (HostComponentHandler, Vst::IComponentHandler, Vst::IComponentHandler::iid) @@ -886,8 +945,7 @@ class VST3Instance : public AudioPluginInstance IPtr processor, IPtr controller, bool controllerWasInitialized) - : AudioPluginInstance (desc, - buildBusLayout (component.get())) + : AudioPluginInstance (desc, buildBusLayout (component.get())) , hostContext (context) , vst3Module (std::move (module)) , vst3HostApplication (std::move (hostApplication)) @@ -898,13 +956,33 @@ class VST3Instance : public AudioPluginInstance , vst3ControllerInitialized (controllerWasInitialized) { if (auto* handler = static_cast (vst3ComponentHandler.get())) + { handler->setRestartCallback ([this] (int32 flags) { handleRestartComponent (flags); }); + } connectComponentAndController(); buildParameterList(); + + if (auto* handler = static_cast (vst3ComponentHandler.get())) + { + handler->setParameterEditCallbacks ( + [this] (Vst::ParamID id) + { + handleParameterGestureBegin (id); + }, + [this] (Vst::ParamID id, Vst::ParamValue value) + { + handleParameterEdit (id, value); + }, + [this] (Vst::ParamID id) + { + handleParameterGestureEnd (id); + }); + } + setNonRealtime (context.isNonRealtime); } @@ -914,7 +992,10 @@ class VST3Instance : public AudioPluginInstance releaseResources(); if (auto* handler = static_cast (vst3ComponentHandler.get())) + { handler->setRestartCallback (nullptr); + handler->setParameterEditCallbacks (nullptr, nullptr, nullptr); + } if (vst3Controller != nullptr) { @@ -987,20 +1068,24 @@ class VST3Instance : public AudioPluginInstance processingPrepared = false; } - void processBlock (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) override + void processBlock (AudioProcessContext& context) override { ScopedNoDenormals noDenormals; if (isBypassed()) { - processBlockBypassed (audioBuffer, midiBuffer); + processBlockBypassed (context); return; } + auto& audioBuffer = context.audio; + auto& midiBuffer = context.midi; + if (isUsingDoublePrecision()) { doublePrecisionBuffer.makeCopyOf (audioBuffer, true); - processBlock (doublePrecisionBuffer, midiBuffer); + AudioProcessContext doubleCtx { doublePrecisionBuffer, midiBuffer, context.params, context.playHead }; + processBlock (doubleCtx); const int numChannels = jmin (audioBuffer.getNumChannels(), doublePrecisionBuffer.getNumChannels()); const int numSamples = jmin (audioBuffer.getNumSamples(), doublePrecisionBuffer.getNumSamples()); @@ -1010,15 +1095,14 @@ class VST3Instance : public AudioPluginInstance auto* destination = audioBuffer.getWritePointer (channel); const auto* source = doublePrecisionBuffer.getReadPointer (channel); - for (int sample = 0; sample < numSamples; ++sample) - destination[sample] = static_cast (source[sample]); + FloatVectorOperations::convertDoubleToFloat (destination, source, numSamples); } return; } Vst::ProcessData data {}; - prepareProcessData (data, audioBuffer.getNumSamples(), Vst::kSample32); + prepareProcessData (data, audioBuffer.getNumSamples(), Vst::kSample32, context.params, context.playHead); prepareMidiInputEvents (midiBuffer); // Input busses @@ -1046,21 +1130,19 @@ class VST3Instance : public AudioPluginInstance collectOutputEvents (midiBuffer); } - int getLatencySamples() override - { - return vst3Processor != nullptr ? static_cast (vst3Processor->getLatencySamples()) : 0; - } - - void processBlock (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) override + void processBlock (AudioProcessContext& context) override { ScopedNoDenormals noDenormals; if (isBypassed()) { - processBlockBypassed (audioBuffer, midiBuffer); + processBlockBypassed (context); return; } + auto& audioBuffer = context.audio; + auto& midiBuffer = context.midi; + if (! isUsingDoublePrecision()) { jassertfalse; @@ -1070,7 +1152,7 @@ class VST3Instance : public AudioPluginInstance } Vst::ProcessData data {}; - prepareProcessData (data, audioBuffer.getNumSamples(), Vst::kSample64); + prepareProcessData (data, audioBuffer.getNumSamples(), Vst::kSample64, context.params, context.playHead); prepareMidiInputEvents (midiBuffer); Vst::AudioBusBuffers inputBus {}; @@ -1103,6 +1185,13 @@ class VST3Instance : public AudioPluginInstance //============================================================================== + int getLatencySamples() override + { + return vst3Processor != nullptr ? static_cast (vst3Processor->getLatencySamples()) : 0; + } + + //============================================================================== + int getCurrentPreset() const noexcept override { return currentPreset; } void setCurrentPreset (int index) noexcept override @@ -1354,24 +1443,41 @@ class VST3Instance : public AudioPluginInstance inputParameterChanges.setMaxParameters (static_cast (vst3ParameterIds.size())); } - AudioPluginHostContext hostContext; - std::unique_ptr vst3Module; - IPtr vst3HostApplication; - IPtr vst3ComponentHandler; - IPtr vst3Component; - IPtr vst3Processor; - IPtr vst3Controller; - Vst::ProcessContext vst3ProcessContext {}; - Vst::ParameterChanges inputParameterChanges; - Vst::EventList inputEvents; - Vst::EventList outputEvents; - AudioBuffer doublePrecisionBuffer; - std::vector vst3ParameterIds; - int currentPreset = 0; - int numPresets = 0; - bool processingPrepared = false; - bool vst3ControllerInitialized = false; - bool vst3ComponentsConnected = false; + int findParameterIndexForVST3Id (Vst::ParamID id) const + { + const auto iter = std::find (vst3ParameterIds.begin(), vst3ParameterIds.end(), id); + if (iter == vst3ParameterIds.end()) + return -1; + + return static_cast (std::distance (vst3ParameterIds.begin(), iter)); + } + + void handleParameterGestureBegin (Vst::ParamID id) + { + const auto index = findParameterIndexForVST3Id (id); + const auto params = getParameters(); + + if (isPositiveAndBelow (index, static_cast (params.size()))) + params[static_cast (index)]->beginChangeGesture(); + } + + void handleParameterEdit (Vst::ParamID id, Vst::ParamValue value) + { + const auto index = findParameterIndexForVST3Id (id); + const auto params = getParameters(); + + if (isPositiveAndBelow (index, static_cast (params.size()))) + params[static_cast (index)]->setNormalizedValue (static_cast (value)); + } + + void handleParameterGestureEnd (Vst::ParamID id) + { + const auto index = findParameterIndexForVST3Id (id); + const auto params = getParameters(); + + if (isPositiveAndBelow (index, static_cast (params.size()))) + params[static_cast (index)]->endChangeGesture(); + } bool connectComponentAndController() { @@ -1442,24 +1548,29 @@ class VST3Instance : public AudioPluginInstance vst3ComponentsConnected = false; } - void prepareProcessData (Vst::ProcessData& data, int numSamples, int32 symbolicSampleSize) + void prepareProcessData (Vst::ProcessData& data, + int numSamples, + int32 symbolicSampleSize, + const ParameterChangeBuffer& parameterChanges, + AudioPlayHead* playHead) { data.processMode = isNonRealtime() ? Vst::kOffline : Vst::kRealtime; data.symbolicSampleSize = symbolicSampleSize; data.numSamples = numSamples; inputParameterChanges.clearQueue(); - const auto params = getParameters(); - const auto numParams = yup::jmin (params.size(), vst3ParameterIds.size()); - for (std::size_t i = 0; i < numParams; ++i) + for (const auto& change : parameterChanges) { + if (! isPositiveAndBelow (change.parameterIndex, static_cast (vst3ParameterIds.size()))) + continue; + int32 queueIndex = 0; - if (auto* queue = inputParameterChanges.addParameterData (vst3ParameterIds[i], queueIndex)) + if (auto* queue = inputParameterChanges.addParameterData (vst3ParameterIds[static_cast (change.parameterIndex)], queueIndex)) { int32 pointIndex = 0; - queue->addPoint (0, - static_cast (params[i]->getValue()), + queue->addPoint (change.sampleOffset, + static_cast (change.normalizedValue), pointIndex); } } @@ -1471,23 +1582,66 @@ class VST3Instance : public AudioPluginInstance inputEvents.clear(); outputEvents.clear(); - if (hostContext.playHead == nullptr) + if (playHead == nullptr) return; - const auto optPos = hostContext.playHead->getPosition(); + const auto optPos = playHead->getPosition(); if (! optPos.has_value()) return; const auto& posInfo = optPos.value(); vst3ProcessContext = {}; - vst3ProcessContext.state = Vst::ProcessContext::kPlaying; vst3ProcessContext.sampleRate = getSampleRate(); + if (posInfo.getIsPlaying()) + vst3ProcessContext.state |= Vst::ProcessContext::kPlaying; + + if (posInfo.getIsRecording()) + vst3ProcessContext.state |= Vst::ProcessContext::kRecording; + + if (posInfo.getIsLooping()) + vst3ProcessContext.state |= Vst::ProcessContext::kCycleActive; + if (auto timeSamples = posInfo.getTimeInSamples()) vst3ProcessContext.projectTimeSamples = *timeSamples; if (auto tempo = posInfo.getBpm()) + { + vst3ProcessContext.state |= Vst::ProcessContext::kTempoValid; vst3ProcessContext.tempo = *tempo; + } + + if (auto ppq = posInfo.getPpqPosition()) + { + vst3ProcessContext.state |= Vst::ProcessContext::kProjectTimeMusicValid; + vst3ProcessContext.projectTimeMusic = *ppq; + } + + if (auto barPosition = posInfo.getPpqPositionOfLastBarStart()) + { + vst3ProcessContext.state |= Vst::ProcessContext::kBarPositionValid; + vst3ProcessContext.barPositionMusic = *barPosition; + } + + if (auto loopPoints = posInfo.getLoopPoints()) + { + vst3ProcessContext.state |= Vst::ProcessContext::kCycleValid; + vst3ProcessContext.cycleStartMusic = loopPoints->ppqStart; + vst3ProcessContext.cycleEndMusic = loopPoints->ppqEnd; + } + + if (auto continuousTime = posInfo.getContinuousTimeInSamples()) + { + vst3ProcessContext.state |= Vst::ProcessContext::kContTimeValid; + vst3ProcessContext.continousTimeSamples = *continuousTime; + } + + if (auto timeSignature = posInfo.getTimeSignature()) + { + vst3ProcessContext.state |= Vst::ProcessContext::kTimeSigValid; + vst3ProcessContext.timeSigNumerator = timeSignature->numerator; + vst3ProcessContext.timeSigDenominator = timeSignature->denominator; + } data.processContext = &vst3ProcessContext; } @@ -1536,6 +1690,25 @@ class VST3Instance : public AudioPluginInstance setup.sampleRate = getSampleRate(); vst3Processor->setupProcessing (setup); } + + AudioPluginHostContext hostContext; + std::unique_ptr vst3Module; + IPtr vst3HostApplication; + IPtr vst3ComponentHandler; + IPtr vst3Component; + IPtr vst3Processor; + IPtr vst3Controller; + Vst::ProcessContext vst3ProcessContext {}; + Vst::ParameterChanges inputParameterChanges; + Vst::EventList inputEvents; + Vst::EventList outputEvents; + AudioBuffer doublePrecisionBuffer; + std::vector vst3ParameterIds; + int currentPreset = 0; + int numPresets = 0; + bool processingPrepared = false; + bool vst3ControllerInitialized = false; + bool vst3ComponentsConnected = false; }; //============================================================================== @@ -1584,17 +1757,24 @@ FileSearchPath VST3Format::getDefaultSearchPaths() const ResultValue> VST3Format::scanFile (const File& file) { - if (file.getFileExtension().toLowerCase() != ".vst3" - && ! file.isDirectory()) + if (file.getFileExtension().toLowerCase() != ".vst3") return makeResultValueFail ("Not a VST3 file"); + YUP_MODULE_DBG (PLUGIN_HOST_VST3, "scanning: " << file.getFullPathName()); + auto mod = VST3Module::load (file); if (mod == nullptr) + { + YUP_MODULE_DBG (PLUGIN_HOST_VST3, "failed to load module: " << file.getFullPathName()); return makeResultValueFail ("Failed to load VST3 module: " + file.getFullPathName()); + } IPluginFactory* rawFactory = mod->getFactory(); if (rawFactory == nullptr) + { + YUP_MODULE_DBG (PLUGIN_HOST_VST3, "no factory in: " << file.getFullPathName()); return makeResultValueFail ("No factory in " + file.getFullPathName()); + } IPtr factory (rawFactory); @@ -1624,7 +1804,10 @@ ResultValue> VST3Format::scanFile (const Fil } if (String (info2.category) != "Audio Module Class") + { + YUP_MODULE_DBG (PLUGIN_HOST_VST3, "skipped class " << info2.name << " (category: " << info2.category << ")"); continue; + } AudioPluginDescription desc; desc.formatType = AudioPluginFormatType::vst3; @@ -1650,9 +1833,12 @@ ResultValue> VST3Format::scanFile (const Fil desc.numOutputChannels = 2; } + YUP_MODULE_DBG (PLUGIN_HOST_VST3, "scan found: " << desc.name << " [" << desc.identifier << "]"); results.push_back (std::move (desc)); } + YUP_MODULE_DBG (PLUGIN_HOST_VST3, "scan complete: " << results.size() << " plugins in " << file.getFileName()); + if (results.empty()) return makeResultValueFail ("No Audio Module Class entries in " + file.getFullPathName()); @@ -1663,11 +1849,17 @@ ResultValue> VST3Format::loadPlugin ( const AudioPluginDescription& description, const AudioPluginHostContext& context) { + YUP_MODULE_DBG (PLUGIN_HOST_VST3, "loading: " << description.name << " [" << description.identifier << "]"); + auto instance = VST3Instance::create (description, context); if (instance == nullptr) + { + YUP_MODULE_DBG (PLUGIN_HOST_VST3, "load failed: " << description.name); return makeResultValueFail ("Failed to load VST3 plugin: " + description.name); + } + YUP_MODULE_DBG (PLUGIN_HOST_VST3, "loaded: " << description.name); return makeResultValueOk (std::move (instance)); } diff --git a/modules/yup_audio_plugin_host/yup_audio_plugin_host.cpp b/modules/yup_audio_plugin_host/yup_audio_plugin_host.cpp index 7c00b9eb8..018fb2629 100644 --- a/modules/yup_audio_plugin_host/yup_audio_plugin_host.cpp +++ b/modules/yup_audio_plugin_host/yup_audio_plugin_host.cpp @@ -25,6 +25,7 @@ #include "yup_audio_plugin_host.h" +#include #include #include #include @@ -65,6 +66,11 @@ #if YUP_AUDIO_PLUGIN_HOST_ENABLE_CLAP #include +#if YUP_MAC +#import +#include +#endif + #if YUP_WINDOWS #include using CLAPModuleHandle = HMODULE; diff --git a/modules/yup_audio_plugin_host/yup_audio_plugin_host.h b/modules/yup_audio_plugin_host/yup_audio_plugin_host.h index 9826cf05f..d4f68f1cc 100644 --- a/modules/yup_audio_plugin_host/yup_audio_plugin_host.h +++ b/modules/yup_audio_plugin_host/yup_audio_plugin_host.h @@ -43,6 +43,32 @@ #pragma once #define YUP_AUDIO_PLUGIN_HOST_H_INCLUDED +//============================================================================== +/** Config: YUP_ENABLE_PLUGIN_HOST_AU_LOGGING + + Enable debug logging for AUv2 plugin scanning and loading. +*/ +#ifndef YUP_ENABLE_PLUGIN_HOST_AU_LOGGING +#define YUP_ENABLE_PLUGIN_HOST_AU_LOGGING 0 +#endif + +/** Config: YUP_ENABLE_PLUGIN_HOST_CLAP_LOGGING + + Enable debug logging for CLAP plugin scanning and loading. +*/ +#ifndef YUP_ENABLE_PLUGIN_HOST_CLAP_LOGGING +#define YUP_ENABLE_PLUGIN_HOST_CLAP_LOGGING 0 +#endif + +/** Config: YUP_ENABLE_PLUGIN_HOST_VST3_LOGGING + + Enable debug logging for VST3 plugin scanning and loading. +*/ +#ifndef YUP_ENABLE_PLUGIN_HOST_VST3_LOGGING +#define YUP_ENABLE_PLUGIN_HOST_VST3_LOGGING 0 +#endif + +//============================================================================== #include //============================================================================== diff --git a/modules/yup_audio_processors/processors/yup_AudioParameter.cpp b/modules/yup_audio_processors/processors/yup_AudioParameter.cpp index 56bacab10..df54ac6d8 100644 --- a/modules/yup_audio_processors/processors/yup_AudioParameter.cpp +++ b/modules/yup_audio_processors/processors/yup_AudioParameter.cpp @@ -42,44 +42,17 @@ float defaultFromString (const String& string) //============================================================================== AudioParameter::AudioParameter (const String& id, - const String& name, - float minValue, - float maxValue, - float defaultValue, + Metadata metadata, ValueToString valueToString, - StringToValue stringToValue, - bool smoothingEnabled, - float smoothingTimeMs) + StringToValue stringToValue) : paramID (id) - , paramName (name) - , valueRange (minValue, maxValue) - , defaultValue (defaultValue) + , metadata (std::move (metadata)) , valueToString (valueToString ? valueToString : defaultToString) , stringToValue (stringToValue ? stringToValue : defaultFromString) - , smoothingEnabled (smoothingEnabled) - , smoothingTimeMs (smoothingTimeMs) { - setValue (defaultValue); -} + jassert (this->metadata.hostParameterID == invalidHostParameterID || this->metadata.hostParameterID <= maximumHostParameterID); -AudioParameter::AudioParameter (const String& id, - const String& name, - NormalisableRange valueRange, - float defaultValue, - ValueToString valueToString, - StringToValue stringToValue, - bool smoothingEnabled, - float smoothingTimeMs) - : paramID (id) - , paramName (name) - , valueRange (std::move (valueRange)) - , defaultValue (defaultValue) - , valueToString (valueToString ? valueToString : defaultToString) - , stringToValue (stringToValue ? stringToValue : defaultFromString) - , smoothingEnabled (smoothingEnabled) - , smoothingTimeMs (smoothingTimeMs) -{ - setValue (defaultValue); + setValue (this->metadata.defaultValue); } AudioParameter::~AudioParameter() @@ -91,19 +64,23 @@ AudioParameter::~AudioParameter() void AudioParameter::beginChangeGesture() { - ++isInsideGesture; + const auto newGestureDepth = isInsideGesture.fetch_add (1) + 1; - if (isInsideGesture == 1) + if (newGestureDepth == 1) listeners.call (&Listener::parameterGestureBegin, this, paramIndex); } void AudioParameter::endChangeGesture() { - jassert (isInsideGesture > 0); // Unbalanced calls to begin and end change gesture found! + const auto currentGestureDepth = isInsideGesture.load(); + + jassert (currentGestureDepth > 0); // Unbalanced calls to begin and end change gesture found! + if (currentGestureDepth <= 0) + return; - --isInsideGesture; + const auto newGestureDepth = isInsideGesture.fetch_sub (1) - 1; - if (isInsideGesture == 0) + if (newGestureDepth == 0) listeners.call (&Listener::parameterGestureEnd, this, paramIndex); } diff --git a/modules/yup_audio_processors/processors/yup_AudioParameter.h b/modules/yup_audio_processors/processors/yup_AudioParameter.h index f571eae4e..e1689fa17 100644 --- a/modules/yup_audio_processors/processors/yup_AudioParameter.h +++ b/modules/yup_audio_processors/processors/yup_AudioParameter.h @@ -48,47 +48,125 @@ class AudioParameter : public ReferenceCountedObject /** A function that converts a string to a real value. */ using StringToValue = std::function; - //============================================================================== + /** Sentinel used when a parameter does not provide an explicit host-facing ID. */ + static constexpr uint32 invalidHostParameterID = 0xffffffffu; /** - Constructs an AudioParameter instance. + Highest host-facing parameter ID that is portable across VST3, AUv2, and CLAP. - @param id The parameter ID (used in the state tree). - @param name The display name. - @param minValue The minimum real value. - @param maxValue The maximum real value. - @param defaultValue The default real value. - @param valueToString Converts real value to display string (optional). - @param stringToValue Parses display string to real value (optional). + VST3 reserves the upper half of the 32-bit parameter ID range for hosts, so + explicit IDs used by YUP plugins must stay in the lower half. */ - AudioParameter (const String& id, - const String& name, - float minValue, - float maxValue, - float defaultValue, - ValueToString valueToString = nullptr, - StringToValue stringToValue = nullptr, - bool smoothingEnabled = false, - float smoothingTimeMs = 0.0f); + static constexpr uint32 maximumHostParameterID = 0x7fffffffu; + + /** Metadata used by processors and plugin wrappers when describing this parameter. */ + struct Metadata + { + private: + /** Bit flags used to store optional parameter capabilities compactly. */ + enum Flag : uint8 + { + automatableFlag = 1 << 0, + readOnlyFlag = 1 << 1, + steppedFlag = 1 << 2, + enumeratedFlag = 1 << 3, + modulatableFlag = 1 << 4, + perNoteModulatableFlag = 1 << 5, + smoothingEnabledFlag = 1 << 6 + }; + + /** Returns true if the supplied flag is set. */ + bool isFlagSet (Flag flag) const noexcept { return (flags & flag) != 0; } + + /** Enables or disables the supplied flag. */ + void setFlag (Flag flag, bool shouldBeSet) noexcept + { + if (shouldBeSet) + flags = static_cast (flags | flag); + else + flags = static_cast (flags & ~static_cast (flag)); + } + + public: + /** Returns true when hosts may automate this parameter. */ + bool isAutomatable() const noexcept { return isFlagSet (automatableFlag); } + + /** Sets whether hosts may automate this parameter. */ + void setAutomatable (bool shouldBeAutomatable) noexcept { setFlag (automatableFlag, shouldBeAutomatable); } + + /** Returns true when hosts may display but not change this parameter. */ + bool isReadOnly() const noexcept { return isFlagSet (readOnlyFlag); } + + /** Sets whether hosts may display but not change this parameter. */ + void setReadOnly (bool shouldBeReadOnly) noexcept { setFlag (readOnlyFlag, shouldBeReadOnly); } + + /** Returns true when this parameter accepts only discrete step values. */ + bool isStepped() const noexcept { return isFlagSet (steppedFlag); } + + /** Sets whether this parameter accepts only discrete step values. */ + void setStepped (bool shouldBeStepped) noexcept { setFlag (steppedFlag, shouldBeStepped); } + + /** Returns true when this stepped parameter represents an enumerated value. */ + bool isEnum() const noexcept { return isFlagSet (enumeratedFlag); } + + /** Sets whether this stepped parameter represents an enumerated value. */ + void setEnum (bool shouldBeEnum) noexcept { setFlag (enumeratedFlag, shouldBeEnum); } + + /** Returns true when this parameter supports CLAP modulation events. */ + bool isModulatable() const noexcept { return isFlagSet (modulatableFlag); } + + /** Sets whether this parameter supports CLAP modulation events. */ + void setModulatable (bool shouldBeModulatable) noexcept { setFlag (modulatableFlag, shouldBeModulatable); } + + /** Returns true when this parameter supports CLAP per-note modulation events. */ + bool isPerNoteModulatable() const noexcept { return isFlagSet (perNoteModulatableFlag); } + + /** Sets whether this parameter supports CLAP per-note modulation events. */ + void setPerNoteModulatable (bool shouldBePerNoteModulatable) noexcept { setFlag (perNoteModulatableFlag, shouldBePerNoteModulatable); } + + /** Returns true if smoothing is enabled. */ + bool isSmoothingEnabled() const noexcept { return isFlagSet (smoothingEnabledFlag); } + + /** Sets whether smoothing is enabled. */ + void setSmoothingEnabled (bool shouldBeEnabled) noexcept { setFlag (smoothingEnabledFlag, shouldBeEnabled); } + + /** The parameter display name. */ + String name; + + /** Optional stable host-facing automation ID. */ + uint32 hostParameterID = invalidHostParameterID; + + /** The parameter value range. */ + NormalisableRange valueRange = { 0.0f, 1.0f }; + + /** The default real value. */ + float defaultValue = 0.0f; + + /** The smoothing time in milliseconds. */ + float smoothingTimeMs = 0.0f; + + /** Optional host-facing module path, using "/" as a separator. */ + String modulePath; + + private: + uint8 flags = automatableFlag; + }; + + //============================================================================== /** Constructs an AudioParameter instance. - @param id The parameter ID (used in the state tree). - @param name The display name. - @param valueRange The value range. - @param defaultValue The default real value. - @param valueToString Converts real value to display string (optional). - @param stringToValue Parses display string to real value (optional). + @param id The parameter ID used in processor state. + @param metadata The parameter display, range, default, smoothing, + and host-facing metadata. + @param valueToString Converts real values to display strings. + @param stringToValue Parses display strings back to real values. */ AudioParameter (const String& id, - const String& name, - NormalisableRange valueRange, - float defaultValue, + Metadata metadata, ValueToString valueToString = nullptr, - StringToValue stringToValue = nullptr, - bool smoothingEnabled = false, - float smoothingTimeMs = 0.0f); + StringToValue stringToValue = nullptr); /** Destructor. */ ~AudioParameter(); @@ -99,32 +177,105 @@ class AudioParameter : public ReferenceCountedObject const String& getID() const { return paramID; } /** Returns the parameter name. */ - const String& getName() const { return paramName; } + const String& getName() const { return metadata.name; } + + /** + Returns true when this parameter has an explicit host-facing automation ID. + + Explicit IDs should be stable forever once a plugin version ships. Do not + reuse an old ID for a different parameter, even if the original parameter is + removed from the plugin UI. + */ + bool hasExplicitHostParameterID() const noexcept + { + return metadata.hostParameterID != invalidHostParameterID; + } + + /** + Returns the host-facing automation ID for this parameter. + + If no explicit ID was provided, this returns the parameter's index assigned + by AudioProcessor::addParameter(), preserving the legacy index-based mapping. + */ + uint32 getHostParameterID() const noexcept + { + return hasExplicitHostParameterID() + ? metadata.hostParameterID + : (paramIndex >= 0 ? static_cast (paramIndex) : invalidHostParameterID); + } //============================================================================== + /** Returns the index of this parameter in its container. */ int getIndexInContainer() const { return paramIndex; } + /** Sets the index of this parameter in its container. */ void setIndexInContainer (int newIndex) { paramIndex = newIndex; } //============================================================================== /** Returns the minimum value. */ - float getMinimumValue() const { return valueRange.start; } + float getMinimumValue() const { return metadata.valueRange.start; } /** Returns the maximum value. */ - float getMaximumValue() const { return valueRange.end; } + float getMaximumValue() const { return metadata.valueRange.end; } /** Returns the default value. */ - float getDefaultValue() const { return defaultValue; } + float getDefaultValue() const { return metadata.defaultValue; } + + /** Returns true when hosts may automate this parameter. */ + bool isAutomatable() const noexcept { return metadata.isAutomatable(); } + + /** Returns true when hosts may display but not change this parameter. */ + bool isReadOnly() const noexcept { return metadata.isReadOnly(); } + + /** Returns true when this parameter accepts only discrete step values. */ + bool isStepped() const noexcept { return metadata.isStepped() || metadata.valueRange.interval > 0.0f; } + + /** Returns the number of discrete steps, or 0 for continuous parameters. */ + int getNumSteps() const noexcept + { + if (! isStepped()) + return 0; + + if (metadata.valueRange.interval <= 0.0f || metadata.valueRange.end <= metadata.valueRange.start) + return 1; + + return jmax (1, static_cast (std::floor (((metadata.valueRange.end - metadata.valueRange.start) / metadata.valueRange.interval) + 0.5f))); + } + + /** Returns true when this stepped parameter represents an enumerated value. */ + bool isEnum() const noexcept { return metadata.isEnum(); } + + /** Returns true when this parameter supports CLAP modulation events. */ + bool isModulatable() const noexcept { return metadata.isModulatable(); } + + /** Returns true when this parameter supports CLAP per-note modulation events. */ + bool isPerNoteModulatable() const noexcept { return metadata.isPerNoteModulatable(); } + + /** Returns the module path of this parameter. */ + String getModulePath() const { return metadata.modulePath; } //============================================================================== + /** Begins a change gesture for this parameter. + + Gestures can be nested, but each beginChangeGesture() call must be balanced with a corresponding endChangeGesture() call. + + Hosts typically use change gestures to group multiple parameter changes into a single undo step and to indicate when to update automation envelopes. + */ void beginChangeGesture(); + /** Ends a change gesture for this parameter. + + Gestures can be nested, but each endChangeGesture() call must be balanced with a corresponding beginChangeGesture() call. + + Hosts typically use change gestures to group multiple parameter changes into a single undo step and to indicate when to update automation envelopes. + */ void endChangeGesture(); - bool isPerformingChangeGesture() const { return isInsideGesture != 0; } + /** Returns true if a change gesture is currently being performed. */ + bool isPerformingChangeGesture() const { return isInsideGesture.load() != 0; } //============================================================================== @@ -142,7 +293,7 @@ class AudioParameter : public ReferenceCountedObject */ void setValue (float newValue) { - currentValue.store (valueRange.snapToLegalValue (newValue)); + currentValue.store (metadata.valueRange.snapToLegalValue (newValue)); } /** Gets the real (un-normalized) parameter value. */ @@ -166,16 +317,22 @@ class AudioParameter : public ReferenceCountedObject //============================================================================== - /** */ + /** Converts a real value to a normalized [0..1] value. + + @param denormalizedValue The real value to convert. + */ float convertToNormalizedValue (float denormalizedValue) const { - return valueRange.convertTo0to1 (denormalizedValue); + return metadata.valueRange.convertTo0to1 (denormalizedValue); } - /** */ + /** Converts a normalized [0..1] value to a real value. + + @param normalizedValue The normalized value to convert. + */ float convertToDenormalizedValue (float normalizedValue) const { - return valueRange.convertFrom0to1 (normalizedValue); + return metadata.valueRange.convertFrom0to1 (normalizedValue); } //============================================================================== @@ -188,19 +345,19 @@ class AudioParameter : public ReferenceCountedObject //============================================================================== - /** */ + /** Converts a real value to its display string. */ String convertToString (float value) const { return valueToString (value); } - /** */ + /** Parses a string into a real parameter value. */ float convertFromString (const String& string) const { return stringToValue (string); } //============================================================================== /** Returns true if smoothing is enabled. */ - bool isSmoothingEnabled() const { return smoothingEnabled; } + bool isSmoothingEnabled() const { return metadata.isSmoothingEnabled(); } /** Returns the smoothing time in milliseconds. */ - float getSmoothingTimeMs() const { return smoothingTimeMs; } + float getSmoothingTimeMs() const { return metadata.smoothingTimeMs; } //============================================================================== @@ -230,18 +387,13 @@ class AudioParameter : public ReferenceCountedObject using ListenersType = ListenerList>; String paramID; - String paramName; - int paramVersion = 0; int paramIndex = -1; std::atomic currentValue = 0.0f; - NormalisableRange valueRange = { 0.0f, 1.0f }; - float defaultValue = 0.0f; + Metadata metadata; + ListenersType listeners; ValueToString valueToString = nullptr; StringToValue stringToValue = nullptr; - ListenersType listeners; - float smoothingTimeMs = 0.0f; - bool smoothingEnabled = false; - int isInsideGesture = 0; + std::atomic isInsideGesture = 0; }; } // namespace yup diff --git a/modules/yup_audio_processors/processors/yup_AudioParameterBuilder.cpp b/modules/yup_audio_processors/processors/yup_AudioParameterBuilder.cpp index a4f957d2a..efe50662e 100644 --- a/modules/yup_audio_processors/processors/yup_AudioParameterBuilder.cpp +++ b/modules/yup_audio_processors/processors/yup_AudioParameterBuilder.cpp @@ -24,33 +24,49 @@ namespace yup //============================================================================== +AudioParameterBuilder::AudioParameterBuilder() = default; + +//============================================================================== + AudioParameterBuilder& AudioParameterBuilder::withID (const String& paramID) { + jassert (paramID.isNotEmpty()); + id = paramID; return *this; } AudioParameterBuilder& AudioParameterBuilder::withName (const String& paramName) { - name = paramName; + jassert (paramName.isNotEmpty()); + + metadata.name = paramName; + return *this; +} + +AudioParameterBuilder& AudioParameterBuilder::withHostID (uint32 hostParameterID) +{ + jassert (hostParameterID <= AudioParameter::maximumHostParameterID); + + metadata.hostParameterID = hostParameterID; return *this; } AudioParameterBuilder& AudioParameterBuilder::withRange (float minValue, float maxValue) { - valueRange = { minValue, maxValue }; + metadata.valueRange = { minValue, maxValue }; return *this; } AudioParameterBuilder& AudioParameterBuilder::withRange (NormalisableRange valueRange) { - this->valueRange = std::move (valueRange); + metadata.valueRange = std::move (valueRange); return *this; } AudioParameterBuilder& AudioParameterBuilder::withDefault (float defaultValue) { - this->defaultValue = defaultValue; + metadata.defaultValue = defaultValue; return *this; } @@ -68,8 +84,79 @@ AudioParameterBuilder& AudioParameterBuilder::withStringToValue (AudioParameter: AudioParameterBuilder& AudioParameterBuilder::withSmoothing (float smoothingTimeMs) { - smoothingEnabled = true; - this->smoothingTimeMs = smoothingTimeMs; + if (smoothingTimeMs > 0.0f) + { + metadata.setSmoothingEnabled (true); + metadata.smoothingTimeMs = smoothingTimeMs; + } + else + { + metadata.setSmoothingEnabled (false); + metadata.smoothingTimeMs = 0.0f; + } + + return *this; +} + +AudioParameterBuilder& AudioParameterBuilder::withAutomatable (bool shouldBeAutomatable) +{ + if (shouldBeAutomatable && metadata.isReadOnly()) + metadata.setReadOnly (false); + + metadata.setAutomatable (shouldBeAutomatable); + + return *this; +} + +AudioParameterBuilder& AudioParameterBuilder::withReadOnly (bool shouldBeReadOnly) +{ + if (shouldBeReadOnly && metadata.isAutomatable()) + metadata.setAutomatable (false); + + metadata.setReadOnly (shouldBeReadOnly); + + return *this; +} + +AudioParameterBuilder& AudioParameterBuilder::withStepped (bool shouldBeStepped) +{ + if (! shouldBeStepped && metadata.isEnum()) + metadata.setEnum (false); + + metadata.setStepped (shouldBeStepped); + + return *this; +} + +AudioParameterBuilder& AudioParameterBuilder::withEnum (bool shouldBeEnum) +{ + if (shouldBeEnum) + metadata.setStepped (true); + + metadata.setEnum (shouldBeEnum); + + return *this; +} + +AudioParameterBuilder& AudioParameterBuilder::withModulatable (bool shouldBeModulatable) +{ + metadata.setModulatable (shouldBeModulatable); + return *this; +} + +AudioParameterBuilder& AudioParameterBuilder::withPerNoteModulatable (bool shouldBePerNoteModulatable) +{ + if (shouldBePerNoteModulatable) + metadata.setModulatable (true); + + metadata.setPerNoteModulatable (shouldBePerNoteModulatable); + + return *this; +} + +AudioParameterBuilder& AudioParameterBuilder::withModulePath (const String& modulePath) +{ + metadata.modulePath = modulePath; return *this; } @@ -77,17 +164,15 @@ AudioParameterBuilder& AudioParameterBuilder::withSmoothing (float smoothingTime AudioParameter::Ptr AudioParameterBuilder::build() const { - jassert (! id.isEmpty() && ! name.isEmpty()); - - return AudioParameter::Ptr (new AudioParameter ( - id, - name, - valueRange, - valueRange.snapToLegalValue (defaultValue), - std::move (valueToString), - std::move (stringToValue), - smoothingEnabled, - smoothingTimeMs)); + jassert (! id.isEmpty() && ! metadata.name.isEmpty()); + + auto parameterMetadata = metadata; + parameterMetadata.defaultValue = parameterMetadata.valueRange.snapToLegalValue (parameterMetadata.defaultValue); + + return AudioParameter::Ptr (new AudioParameter (id, + std::move (parameterMetadata), + valueToString, + stringToValue)); } } // namespace yup diff --git a/modules/yup_audio_processors/processors/yup_AudioParameterBuilder.h b/modules/yup_audio_processors/processors/yup_AudioParameterBuilder.h index bae7e0946..f5d313a29 100644 --- a/modules/yup_audio_processors/processors/yup_AudioParameterBuilder.h +++ b/modules/yup_audio_processors/processors/yup_AudioParameterBuilder.h @@ -49,7 +49,7 @@ class AudioParameterBuilder { public: /** Constructs a new AudioParameterBuilder. */ - AudioParameterBuilder() = default; + AudioParameterBuilder(); /** Sets the parameter ID (used in the state tree and automation). */ AudioParameterBuilder& withID (const String& paramID); @@ -57,6 +57,16 @@ class AudioParameterBuilder /** Sets the parameter display name. */ AudioParameterBuilder& withName (const String& paramName); + /** + Sets the stable host-facing automation ID. + + Use this for plugins that need automation compatibility when parameters are + reordered or when reserved parameter slots are kept for future expansion. + Once released, an ID should never be reused for a different parameter. + Values must be less than or equal to AudioParameter::maximumHostParameterID. + */ + AudioParameterBuilder& withHostID (uint32 hostParameterID); + /** Sets the parameter's value range. @@ -88,6 +98,27 @@ class AudioParameterBuilder /** Sets the smoothing time for the parameter. */ AudioParameterBuilder& withSmoothing (float smoothingTimeMs); + /** Sets whether hosts may automate this parameter. */ + AudioParameterBuilder& withAutomatable (bool shouldBeAutomatable); + + /** Sets whether hosts may display but not change this parameter. */ + AudioParameterBuilder& withReadOnly (bool shouldBeReadOnly); + + /** Sets whether this parameter accepts only discrete step values. */ + AudioParameterBuilder& withStepped (bool shouldBeStepped); + + /** Sets whether this stepped parameter represents an enumerated value. */ + AudioParameterBuilder& withEnum (bool shouldBeEnum); + + /** Sets whether this parameter supports CLAP modulation events. */ + AudioParameterBuilder& withModulatable (bool shouldBeModulatable); + + /** Sets whether this parameter supports CLAP per-note modulation events. */ + AudioParameterBuilder& withPerNoteModulatable (bool shouldBePerNoteModulatable); + + /** Sets the optional host-facing module path, using "/" as a separator. */ + AudioParameterBuilder& withModulePath (const String& modulePath); + /** Finalizes the builder and returns a fully constructed AudioProcessorParameter instance. @@ -97,11 +128,7 @@ class AudioParameterBuilder private: String id; - String name; - NormalisableRange valueRange = { 0.0f, 1.0f }; - float defaultValue = 0.5f; - bool smoothingEnabled = false; - float smoothingTimeMs = 0.0f; + AudioParameter::Metadata metadata; AudioParameter::ValueToString valueToString = nullptr; AudioParameter::StringToValue stringToValue = nullptr; }; diff --git a/modules/yup_audio_processors/processors/yup_AudioParameterHandle.h b/modules/yup_audio_processors/processors/yup_AudioParameterHandle.h index 06afac926..9960501fd 100644 --- a/modules/yup_audio_processors/processors/yup_AudioParameterHandle.h +++ b/modules/yup_audio_processors/processors/yup_AudioParameterHandle.h @@ -64,9 +64,11 @@ class AudioParameterHandle ~AudioParameterHandle() = default; /** - Updates the smoothed value of the parameter. + Updates the smoothed value of the parameter from its atomic value. - This must be called on the audio thread once per audio block. + Call once at the start of each audio block when not using sample-accurate + automation. For sample-accurate automation use prepareBlock() and + advanceToSample() instead. @returns true if the parameter is currently being smoothed, false otherwise. */ @@ -79,13 +81,71 @@ class AudioParameterHandle return smoothed.isSmoothing(); } - /** Returns the next value of the parameter. */ + /** + Prepares this handle for sample-accurate automation in a processing block. + + Call once at the start of processBlock() in place of updateNextAudioBlock() + when you intend to use advanceToSample(). Syncs the smoother from the + parameter's current atomic value and stores a reference to the automation + buffer so advanceToSample() can apply changes at exact sample positions. + + @param changes The per-block automation buffer from AudioProcessContext::params. + @param paramIdx Index of this parameter — use AudioParameter::getIndexInContainer(). + */ + forcedinline void prepareBlock (const ParameterChangeBuffer& changes, int paramIdx) noexcept + { + jassert (parameter != nullptr); + + blockChanges = std::addressof (changes); + myParamIndex = paramIdx; + nextChangePtr = changes.begin(); + + smoothed.setTargetValue (parameter->getValue()); + } + + /** + Applies pending automation events up to and including @p samplePosition. + + Call at each sub-block boundary in your event-driven processing loop alongside + MIDI event iteration. Returns true if at least one automation change was applied + so the processing loop can react immediately (e.g. re-compute a coefficient). + + The smoother is retargeted to the new parameter value at each change point, so + getNextValue() continues to produce a smooth ramp even under automation. + + @param samplePosition Current sample offset within the block. + @returns true if at least one change was applied. + */ + forcedinline bool advanceToSample (int samplePosition) noexcept + { + if (blockChanges == nullptr || parameter == nullptr) + return false; + + bool changed = false; + + while (nextChangePtr != blockChanges->end() + && nextChangePtr->sampleOffset <= samplePosition) + { + if (nextChangePtr->parameterIndex == myParamIndex) + { + parameter->setNormalizedValue (nextChangePtr->normalizedValue); + smoothed.setTargetValue (parameter->getValue()); + changed = true; + } + + ++nextChangePtr; + } + + return changed; + } + + /** Returns the next smoothed value of the parameter. */ forcedinline float getNextValue() noexcept { return smoothed.getNextValue(); } - /** Returns the current value of the parameter. */ + /** Returns the current smoothed value of the parameter without advancing. */ forcedinline float getCurrentValue() const noexcept { return smoothed.getCurrentValue(); @@ -94,11 +154,10 @@ class AudioParameterHandle /** Skips the next numSamples samples of the parameter. - This is identical to calling getNextValue numSamples times. + Equivalent to calling getNextValue() numSamples times. @param numSamples The number of samples to skip. - - @returns The current value of the parameter after skipping the samples. + @returns The current value after skipping. */ forcedinline float skip (int numSamples) noexcept { @@ -108,6 +167,10 @@ class AudioParameterHandle private: AudioParameter* parameter = nullptr; SmoothedValue smoothed; + + const ParameterChangeBuffer* blockChanges = nullptr; + const ParameterChange* nextChangePtr = nullptr; + int myParamIndex = -1; }; } // namespace yup diff --git a/modules/yup_audio_processors/processors/yup_AudioProcessContext.h b/modules/yup_audio_processors/processors/yup_AudioProcessContext.h new file mode 100644 index 000000000..a8c8c1c9b --- /dev/null +++ b/modules/yup_audio_processors/processors/yup_AudioProcessContext.h @@ -0,0 +1,67 @@ +/* + ============================================================================== + + 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. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== + +/** + All inputs available to an AudioProcessor for a single processing block. + + AudioProcessContext is passed to AudioProcessor::processBlock() and bundles: + - the audio I/O buffer (in-place processing model, single or double precision), + - sample-accurate MIDI events, + - sample-accurate parameter automation events, + - host play-head information, when available. + + Use AudioProcessContext for the primary single-precision processing path. + Use AudioProcessContext for double-precision processing in processors that + override processBlock(AudioProcessContext&) and return true from + supportsDoublePrecisionProcessing(). + + Processors that only need audio and MIDI can ignore the @c params, + and @c playHead fields. Processors that implement sample-accurate + automation should use AudioParameterHandle::prepareBlock() and + AudioParameterHandle::advanceToSample() together with the @c params buffer. + Processors that need tempo, transport, or timeline information should use + @c playHead; a null pointer means no audio position information is available + for this block. + + @see AudioProcessor, AudioPlayHead, ParameterChangeBuffer, AudioParameterHandle, MidiBuffer +*/ +template +struct AudioProcessContext +{ + /** Audio I/O buffer. Process in-place: read and write the same channels. */ + AudioBuffer& audio; + + /** MIDI events for this block, sorted by samplePosition in [0, blockSize). */ + MidiBuffer& midi; + + /** Parameter automation events for this block, sorted by sampleOffset in [0, blockSize). */ + ParameterChangeBuffer& params; + + /** Optional play-head for this block. A null pointer means position information is unavailable. */ + AudioPlayHead* playHead = nullptr; +}; + +} // namespace yup diff --git a/modules/yup_audio_processors/processors/yup_AudioProcessor.cpp b/modules/yup_audio_processors/processors/yup_AudioProcessor.cpp index 05f89f78f..b510e37c9 100644 --- a/modules/yup_audio_processors/processors/yup_AudioProcessor.cpp +++ b/modules/yup_audio_processors/processors/yup_AudioProcessor.cpp @@ -32,9 +32,7 @@ AudioProcessor::AudioProcessor (StringRef name, AudioBusLayout busLayout) //============================================================================== -AudioProcessor::~AudioProcessor() -{ -} +AudioProcessor::~AudioProcessor() = default; //============================================================================== @@ -47,9 +45,22 @@ void AudioProcessor::addParameter (AudioParameter::Ptr parameter) if (parameterMap.find (parameter->getID()) != parameterMap.end()) return; + const auto hostParameterID = parameter->hasExplicitHostParameterID() + ? parameter->getHostParameterID() + : static_cast (parameters.size()); + jassert (hostParameterID != AudioParameter::invalidHostParameterID); + jassert (hostParameterID <= AudioParameter::maximumHostParameterID); + + if (parameterHostIDMap.find (hostParameterID) != parameterHostIDMap.end()) + { + jassertfalse; + return; + } + parameter->setIndexInContainer (static_cast (parameters.size())); parameterMap.emplace (parameter->getID(), parameter); + parameterHostIDMap.emplace (hostParameterID, parameter); parameters.emplace_back (std::move (parameter)); } @@ -59,6 +70,20 @@ AudioParameter::Ptr AudioProcessor::getParameterByID (StringRef parameterID) con return iterator != parameterMap.end() ? iterator->second : nullptr; } +AudioParameter::Ptr AudioProcessor::getParameterByHostID (uint32 hostParameterID) const +{ + const auto iterator = parameterHostIDMap.find (hostParameterID); + return iterator != parameterHostIDMap.end() ? iterator->second : nullptr; +} + +int AudioProcessor::getParameterIndexByHostID (uint32 hostParameterID) const +{ + if (auto parameter = getParameterByHostID (hostParameterID)) + return parameter->getIndexInContainer(); + + return -1; +} + void AudioProcessor::addListener (Listener* listener) { listeners.add (listener); @@ -100,13 +125,6 @@ int AudioProcessor::getNumAudioInputs() const //============================================================================== -void AudioProcessor::setPlayHead (AudioPlayHead* playHead) -{ - this->playHead = playHead; -} - -//============================================================================== - void AudioProcessor::suspendProcessing (bool shouldSuspend) { auto lock = CriticalSection::ScopedLockType (processLock); @@ -146,18 +164,6 @@ void AudioProcessor::setProcessingPrecision (ProcessingPrecision precision) //============================================================================== -void AudioProcessor::processBlockBypassed (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) -{ - ignoreUnused (audioBuffer, midiBuffer); -} - -void AudioProcessor::processBlockBypassed (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) -{ - ignoreUnused (audioBuffer, midiBuffer); -} - -//============================================================================== - void AudioProcessor::setPlaybackConfiguration (float sampleRate, int samplesPerBlock) { releaseResources(); diff --git a/modules/yup_audio_processors/processors/yup_AudioProcessor.h b/modules/yup_audio_processors/processors/yup_AudioProcessor.h index fe8fe8761..edd064767 100644 --- a/modules/yup_audio_processors/processors/yup_AudioProcessor.h +++ b/modules/yup_audio_processors/processors/yup_AudioProcessor.h @@ -44,8 +44,52 @@ class YUP_API AudioProcessor return copy; } + /** Returns a copy of these details with the tail length change flag set. */ + ChangeDetails withTailChanged (bool shouldBeTailChanged) const noexcept + { + auto copy = *this; + copy.tailChanged = shouldBeTailChanged; + return copy; + } + + /** Returns a copy of these details with the parameter value change flag set. */ + ChangeDetails withParameterValuesChanged (bool shouldBeParameterValuesChanged) const noexcept + { + auto copy = *this; + copy.parameterValuesChanged = shouldBeParameterValuesChanged; + return copy; + } + + /** Returns a copy of these details with the parameter metadata change flag set. */ + ChangeDetails withParameterInfoChanged (bool shouldBeParameterInfoChanged) const noexcept + { + auto copy = *this; + copy.parameterInfoChanged = shouldBeParameterInfoChanged; + return copy; + } + + /** Returns a copy of these details with the non-parameter state change flag set. */ + ChangeDetails withNonParameterStateChanged (bool shouldBeNonParameterStateChanged) const noexcept + { + auto copy = *this; + copy.nonParameterStateChanged = shouldBeNonParameterStateChanged; + return copy; + } + /** True when the processor latency may have changed. */ bool latencyChanged = false; + + /** True when the processor tail length may have changed. */ + bool tailChanged = false; + + /** True when one or more parameter values may have changed without a host automation event. */ + bool parameterValuesChanged = false; + + /** True when one or more parameter names, ranges, or display conversions may have changed. */ + bool parameterInfoChanged = false; + + /** True when non-parameter processor state may have changed. */ + bool nonParameterStateChanged = false; }; /** Receives processor-level change notifications. */ @@ -87,6 +131,12 @@ class YUP_API AudioProcessor /** Returns a parameter by stable ID, or nullptr when no such parameter exists. */ AudioParameter::Ptr getParameterByID (StringRef parameterID) const; + /** Returns a parameter by host-facing automation ID, or nullptr when no such parameter exists. */ + AudioParameter::Ptr getParameterByHostID (uint32 hostParameterID) const; + + /** Returns a parameter index by host-facing automation ID, or -1 when no such parameter exists. */ + int getParameterIndexByHostID (uint32 hostParameterID) const; + /** Adds a parameter. */ void addParameter (AudioParameter::Ptr parameter); @@ -123,40 +173,46 @@ class YUP_API AudioProcessor virtual void releaseResources() = 0; /** - Processes a block of audio. + Primary single-precision processing entry point. + + Override this to process a block of audio and MIDI. The context provides + sample-accurate parameter automation via @c context.params and the transport + state via @c context.playHead when available. + + The base-class implementation asserts false so unoverridden processors are + caught at runtime in debug builds. - @param audioBuffer The audio buffer to process. - @param midiBuffer The MIDI buffer to process. + @param context All per-block inputs: audio, MIDI, parameter changes, and position. */ - virtual void processBlock (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) = 0; + virtual void processBlock (AudioProcessContext& context) = 0; /** - Processes a block of audio. + Double-precision processing entry point. - @param audioBuffer The audio buffer to process. - @param midiBuffer The MIDI buffer to process. + Override this and return true from supportsDoublePrecisionProcessing() to + support 64-bit audio. The default implementation does nothing. + + @param context All per-block inputs with double-precision audio. */ - virtual void processBlock (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) {} + virtual void processBlock (AudioProcessContext& context) { ignoreUnused (context); } /** - Processes a block while the processor is bypassed. + Called by plugin wrappers when the processor is bypassed (single-precision). - The default implementation leaves audio and MIDI unchanged. + The default implementation routes inputs to outputs, or clears extra outputs. - @param audioBuffer The audio buffer to process. - @param midiBuffer The MIDI buffer to process. + @param context All per-block inputs. */ - virtual void processBlockBypassed (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer); + virtual void processBlockBypassed (AudioProcessContext& context) { ignoreUnused (context); } /** - Processes a block while the processor is bypassed. + Called by plugin wrappers when the processor is bypassed (double-precision). - The default implementation leaves audio and MIDI unchanged. + The default implementation routes inputs to outputs, or clears extra outputs. - @param audioBuffer The audio buffer to process. - @param midiBuffer The MIDI buffer to process. + @param context All per-block inputs. */ - virtual void processBlockBypassed (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer); + virtual void processBlockBypassed (AudioProcessContext& context) { ignoreUnused (context); } /** Flushes the processor. */ virtual void flush() {} @@ -198,11 +254,17 @@ class YUP_API AudioProcessor /** Sets the processor latency in samples and notifies listeners when it changes. */ void setLatencySamples (int newLatencySamples); + /** Returns the number of simultaneous voices this processor can produce. + Returns 0 for effects and MIDI-only processors. Override in instruments. */ + virtual int getNumVoices() const { return 0; } + //============================================================================== - void setPlayHead (AudioPlayHead* playHead); + /** Returns true when the processor is running in offline (non-realtime) mode. */ + bool isOfflineProcessing() const noexcept { return offlineProcessing.load(); } - AudioPlayHead* getPlayHead() { return playHead; } + /** Called by the plugin wrapper to indicate offline vs. realtime rendering. */ + void setOfflineProcessing (bool offline) { offlineProcessing.store (offline); } //============================================================================== @@ -267,6 +329,7 @@ class YUP_API AudioProcessor std::vector parameters; std::unordered_map parameterMap; + std::unordered_map parameterHostIDMap; ListenerList> listeners; AudioBusLayout busLayout; @@ -276,10 +339,9 @@ class YUP_API AudioProcessor std::atomic latencySamples { 0 }; ProcessingPrecision processingPrecision = ProcessingPrecision::singlePrecision; - AudioPlayHead* playHead = nullptr; - CriticalSection processLock; std::atomic processIsSuspended { false }; + std::atomic offlineProcessing { false }; }; } // namespace yup diff --git a/modules/yup_audio_processors/processors/yup_ParameterChangeBuffer.h b/modules/yup_audio_processors/processors/yup_ParameterChangeBuffer.h new file mode 100644 index 000000000..6a06b93ad --- /dev/null +++ b/modules/yup_audio_processors/processors/yup_ParameterChangeBuffer.h @@ -0,0 +1,175 @@ +/* + ============================================================================== + + 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. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== + +/** + A single parameter automation event with a sample-accurate position within a block. + + @see ParameterChangeBuffer +*/ +struct ParameterChange +{ + /** Index into AudioProcessor::getParameters(). */ + int parameterIndex = 0; + + /** Normalized value in [0, 1]. */ + float normalizedValue = 0.0f; + + /** Sample position within the current processing block, in [0, blockSize). */ + int sampleOffset = 0; +}; + +//============================================================================== + +/** + A pre-allocated, sorted buffer of intra-block parameter automation events. + + All memory is reserved at prepare time so that addChange() and clear() never + allocate on the audio thread. Exceeding the reserved capacity fires an assertion + in debug builds and silently drops the excess event in release builds, preserving + realtime safety. + + Typical usage pattern: + @code + // At prepare time (not on audio thread): + paramBuf.reserve (processor.getParameters().size() * 4 + 32); + + // Per block (audio thread): + paramBuf.clear(); + for (auto& automationPoint : hostAutomationPoints) + paramBuf.addChange (automationPoint.paramIdx, + automationPoint.normalizedValue, + automationPoint.sampleOffset); + paramBuf.sort(); + + AudioProcessContext ctx { audioBuffer, midiBuffer, paramBuf, transportPosition, playHead }; + processor.processBlock (ctx); + @endcode + + @see ParameterChange, AudioProcessContext, AudioParameterHandle +*/ +class ParameterChangeBuffer +{ +public: + //============================================================================== + /** Default constructor. */ + ParameterChangeBuffer() = default; + + //============================================================================== + /** Reserves capacity for automation events. + + Call at prepare time (not on the audio thread). A good default is + @code numParams * 4 + 32 @endcode for manual automation, or + @code numParams * blockSize @endcode for fully sample-accurate automation. + + @param maxChanges Maximum number of events per processing block. + */ + void reserve (int maxChanges) + { + changes.reserve (static_cast (maxChanges)); + } + + //============================================================================== + /** Clears all events without releasing memory. Safe to call on the audio thread. */ + void clear() noexcept + { + changes.clear(); + } + + /** Returns true when the buffer contains no events. */ + bool isEmpty() const noexcept + { + return changes.empty(); + } + + /** Returns the number of events currently held. */ + int getNumChanges() const noexcept + { + return static_cast (changes.size()); + } + + //============================================================================== + /** Adds a parameter automation event. + + Safe on the audio thread when the buffer was reserved with sufficient capacity. + If the capacity is exceeded the event is dropped and a debug assertion fires. + + @param parameterIndex Index into AudioProcessor::getParameters(). + @param normalizedValue Value in [0, 1]. + @param sampleOffset Sample position within the current block. + @return true if the event was added, false if it was dropped. + */ + bool addChange (int parameterIndex, float normalizedValue, int sampleOffset) noexcept + { + if (changes.size() >= changes.capacity()) + { + jassertfalse; // Increase reserved capacity at prepare time + return false; + } + + changes.push_back ({ parameterIndex, normalizedValue, sampleOffset }); + return true; + } + + /** Sorts events by sampleOffset in ascending order. + + Call once after filling the buffer for a block, before passing the buffer to + processBlock(). Uses std::sort which is in-place and allocation-free. + */ + void sort() noexcept + { + std::sort (changes.begin(), changes.end(), [] (const ParameterChange& a, const ParameterChange& b) noexcept + { + return a.sampleOffset < b.sampleOffset; + }); + } + + //============================================================================== + /** Returns a pointer to the first event (sorted by sampleOffset). */ + const ParameterChange* begin() const noexcept + { + return changes.data(); + } + + /** Returns a pointer one past the last event. */ + const ParameterChange* end() const noexcept + { + return changes.data() + changes.size(); + } + + /** Returns a pointer to the first event whose sampleOffset >= samplePosition. */ + const ParameterChange* findNextSamplePosition (int samplePosition) const noexcept + { + return std::lower_bound (begin(), end(), samplePosition, [] (const ParameterChange& change, int sample) noexcept + { + return change.sampleOffset < sample; + }); + } + +private: + std::vector changes; +}; + +} // namespace yup diff --git a/modules/yup_audio_processors/yup_audio_processors.h b/modules/yup_audio_processors/yup_audio_processors.h index c279dd92a..214195dcb 100644 --- a/modules/yup_audio_processors/yup_audio_processors.h +++ b/modules/yup_audio_processors/yup_audio_processors.h @@ -55,6 +55,8 @@ #include "processors/yup_AudioBusLayout.h" #include "processors/yup_AudioParameter.h" #include "processors/yup_AudioParameterBuilder.h" +#include "processors/yup_ParameterChangeBuffer.h" +#include "processors/yup_AudioProcessContext.h" #include "processors/yup_AudioParameterHandle.h" #include "processors/yup_AudioProcessor.h" diff --git a/modules/yup_core/system/yup_PlatformDefs.h b/modules/yup_core/system/yup_PlatformDefs.h index b9c8ef125..ca18d5d6d 100644 --- a/modules/yup_core/system/yup_PlatformDefs.h +++ b/modules/yup_core/system/yup_PlatformDefs.h @@ -200,10 +200,10 @@ constexpr bool isConstantEvaluated() noexcept // clang-format off #if YUP_MSVC && ! defined(DOXYGEN) #define YUP_BLOCK_WITH_FORCED_SEMICOLON(x) \ - __pragma (warning (push)) \ - __pragma (warning (disable : 4127)) \ - __pragma (warning (disable : 4390)) \ - do { x } while (false) \ + __pragma (warning (push)) \ + __pragma (warning (disable : 4127)) \ + __pragma (warning (disable : 4390)) \ + do { x } while (false) \ __pragma (warning (pop)) #else /** This is the good old C++ trick for creating a macro that forces the user to put @@ -220,7 +220,7 @@ constexpr bool isConstantEvaluated() noexcept /** Assertion are enabled in debug unless explicitly disabled. */ #define YUP_ASSERTIONS_ENABLED 1 -/** Writes a string to the standard error stream. +/** Writes a string to the current logger, eventually going to the error stream if not set. Note that as well as a single string, you can use this to write multiple items as a stream, e.g. @@ -231,31 +231,61 @@ constexpr bool isConstantEvaluated() noexcept The macro is only enabled in a debug build, so be careful not to use it with expressions that have important side-effects! - @see Logger::outputDebugString + @see Logger::outputDebugString, Logger::writeToLog */ -#define YUP_DBG(textToWrite) YUP_BLOCK_WITH_FORCED_SEMICOLON (\ +#define YUP_DBG(textToWrite) YUP_BLOCK_WITH_FORCED_SEMICOLON ( \ yup::String tempDbgBuf; \ - tempDbgBuf << textToWrite; \ + tempDbgBuf << textToWrite; \ yup::Logger::outputDebugString (tempDbgBuf);) +/** Module-specific debug logging macro. + + This is a convenient way to write debug messages that are tagged with the name of the + module they came from, and which can be enabled or disabled on a per-module basis. + + To enable logging for a module, define YUP_ENABLE__LOGGING to 1 in your + project settings. For example, if you have a module called "Audio", you would define + YUP_ENABLE_AUDIO_LOGGING to 1 to enable logging for that module. + + The macro is only enabled in a debug build, so be careful not to use it with expressions + that have important side-effects! + + @see YUP_DBG +*/ +#define YUP_MODULE_DBG(module, textToWrite) \ + YUP_MODULE_DBG_RESOLVE_ (YUP_ENABLE_##module##_LOGGING, module, textToWrite) + +#define YUP_MODULE_DBG_RESOLVE_(flag, module, textToWrite) \ + YUP_MODULE_DBG_RESOLVE__ (flag, module, textToWrite) + +#define YUP_MODULE_DBG_RESOLVE__(flag, module, textToWrite) \ + YUP_CONCAT (YUP_MODULE_DBG_EMIT_, flag) (module, textToWrite) + +#define YUP_MODULE_DBG_EMIT_1(module, textToWrite) YUP_BLOCK_WITH_FORCED_SEMICOLON ( \ + yup::String tempDbgBuf; \ + tempDbgBuf << "[" #module "] " << textToWrite; \ + yup::Logger::writeToLog (tempDbgBuf);) + +#define YUP_MODULE_DBG_EMIT_0(module, textToWrite) YUP_BLOCK_WITH_FORCED_SEMICOLON ({}) + //============================================================================== /** This will always cause an assertion failure. It is only compiled in a debug build, (unless YUP_LOG_ASSERTIONS is enabled for your build). @see jassert */ -#define jassertfalse YUP_BLOCK_WITH_FORCED_SEMICOLON (\ - if (! yup::isConstantEvaluated()) \ +#define jassertfalse YUP_BLOCK_WITH_FORCED_SEMICOLON ( \ + if (! yup::isConstantEvaluated()) \ { \ - YUP_LOG_CURRENT_ASSERTION; \ - if (yup::yup_isRunningUnderDebugger()) \ - { YUP_BREAK_IN_DEBUGGER } \ + YUP_LOG_CURRENT_ASSERTION; \ + if (yup::yup_isRunningUnderDebugger()) \ + { YUP_BREAK_IN_DEBUGGER } \ else \ - { YUP_ANALYZER_NORETURN } \ + { YUP_ANALYZER_NORETURN } \ } \ else \ { \ - YUP_ANALYZER_NORETURN \ + YUP_ANALYZER_NORETURN \ }) //============================================================================== @@ -281,6 +311,8 @@ constexpr bool isConstantEvaluated() noexcept #define YUP_ASSERTIONS_ENABLED 0 #define YUP_DBG(textToWrite) +#define YUP_MODULE_DBG(module, text) + #define jassertfalse YUP_BLOCK_WITH_FORCED_SEMICOLON (if (! yup::isConstantEvaluated()) YUP_LOG_CURRENT_ASSERTION;) #if YUP_LOG_ASSERTIONS diff --git a/modules/yup_core/threads/yup_WaitableEvent.cpp b/modules/yup_core/threads/yup_WaitableEvent.cpp index 63299e4a9..94b9d4b91 100644 --- a/modules/yup_core/threads/yup_WaitableEvent.cpp +++ b/modules/yup_core/threads/yup_WaitableEvent.cpp @@ -47,45 +47,44 @@ WaitableEvent::WaitableEvent (bool manualReset) noexcept bool WaitableEvent::wait (double timeOutMilliseconds) const { - std::unique_lock lock (mutex); + const auto pred = [this] + { + return triggered.load(); + }; + + std::unique_lock lock (mutex); if (! triggered) { if (timeOutMilliseconds < 0.0) { - condition.wait (lock, [this] - { - return triggered == true; - }); + condition.wait (lock, pred); } - else + else if (! condition.wait_for (lock, std::chrono::duration { timeOutMilliseconds }, pred)) { - if (! condition.wait_for (lock, std::chrono::duration { timeOutMilliseconds }, [this] - { - return triggered == true; - })) - { - return false; - } + return false; } } if (! useManualReset) - reset(); + triggered = false; return true; } void WaitableEvent::signal() const { - std::lock_guard lock (mutex); + { + std::lock_guard lock (mutex); + triggered = true; + } - triggered = true; condition.notify_all(); } void WaitableEvent::reset() const { + std::lock_guard lock (mutex); triggered = false; } diff --git a/modules/yup_events/messages/yup_Initialisation.h b/modules/yup_events/messages/yup_Initialisation.h index af9e4ee2e..3765b969f 100644 --- a/modules/yup_events/messages/yup_Initialisation.h +++ b/modules/yup_events/messages/yup_Initialisation.h @@ -93,6 +93,9 @@ class YUP_API ScopedYupInitialiser_GUI final YUP_DECLARE_NON_COPYABLE(ScopedYupInitialiser_GUI) YUP_DECLARE_NON_MOVEABLE(ScopedYupInitialiser_GUI) + + private: + static std::atomic_int numScopedInitInstances; }; //============================================================================== diff --git a/modules/yup_events/messages/yup_MessageManager.cpp b/modules/yup_events/messages/yup_MessageManager.cpp index bc4f58f1a..8027f14fa 100644 --- a/modules/yup_events/messages/yup_MessageManager.cpp +++ b/modules/yup_events/messages/yup_MessageManager.cpp @@ -577,7 +577,8 @@ YUP_API void YUP_CALLTYPE shutdownYup_GUI() } } -static std::atomic_int numScopedInitInstances = 0; +//============================================================================== +std::atomic_int ScopedYupInitialiser_GUI::numScopedInitInstances = 0; ScopedYupInitialiser_GUI::ScopedYupInitialiser_GUI() { diff --git a/modules/yup_events/timers/yup_Timer.cpp b/modules/yup_events/timers/yup_Timer.cpp index e179c83a9..996044d0f 100644 --- a/modules/yup_events/timers/yup_Timer.cpp +++ b/modules/yup_events/timers/yup_Timer.cpp @@ -48,7 +48,8 @@ class Timer::TimerThread final : private Thread public: using LockType = CriticalSection; // (mysteriously, using a SpinLock here causes problems on some XP machines..) - YUP_DECLARE_SINGLETON (TimerThread, true) + // Plugin hosts can tear down and recreate YUP inside the same process. + YUP_DECLARE_SINGLETON (TimerThread, false) TimerThread() : Thread ("YUP Timer") diff --git a/modules/yup_gui/application/yup_Application.h b/modules/yup_gui/application/yup_Application.h index f3a50cb45..3c702b2d2 100644 --- a/modules/yup_gui/application/yup_Application.h +++ b/modules/yup_gui/application/yup_Application.h @@ -99,8 +99,40 @@ class YUP_API YUPApplication : public YUPApplicationBase YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (YUPApplication) }; -/** These are called automatically by the YUPApplication class but must be called by plugins. */ -void initialiseYup_Windowing(); -void shutdownYup_Windowing(); +//============================================================================== + +/** Initialises YUP's Windowing classes. + + @see shutdownYup_Windowing() +*/ +YUP_API void YUP_CALLTYPE initialiseYup_Windowing(); + +/** Clears up any static data being used by YUP's Windowing classes. + + @see initialiseYup_Windowing() +*/ +YUP_API void YUP_CALLTYPE shutdownYup_Windowing(); + +//============================================================================== +/** A utility object that helps you initialise and shutdown YUP Windowing correctly + using an RAII pattern. + + @note initialiseYup_GUI() or ScopedYupInitialiser_GUI must be called before this. +*/ +class YUP_API ScopedYupInitialiser_Windowing final +{ +public: + /** The constructor simply calls initialiseYup_Windowing(). */ + ScopedYupInitialiser_Windowing(); + + /** The destructor simply calls shutdownYup_Windowing(). */ + ~ScopedYupInitialiser_Windowing(); + + YUP_DECLARE_NON_COPYABLE (ScopedYupInitialiser_Windowing) + YUP_DECLARE_NON_MOVEABLE (ScopedYupInitialiser_Windowing) + +private: + static std::atomic_int numScopedInitInstances; +}; } // namespace yup diff --git a/modules/yup_gui/buttons/yup_Button.h b/modules/yup_gui/buttons/yup_Button.h index 41533cb02..55e669f03 100644 --- a/modules/yup_gui/buttons/yup_Button.h +++ b/modules/yup_gui/buttons/yup_Button.h @@ -23,28 +23,54 @@ namespace yup { //============================================================================== +/** A base class for buttons. + + To create a custom button, inherit from this class and implement the paintButton() method. + + The button will automatically track mouse events to determine when it is being hovered over + or clicked, and will call the onClick callback when it is clicked. + + @see Component, DrawableButton +*/ class YUP_API Button : public Component { public: //============================================================================== + /** Creates a button with the given component ID. */ Button (StringRef componentID); //============================================================================== + /** Returns true if the button is currently being hovered over. */ bool isButtonOver() const { return isButtonCurrentlyOver; } + /** Returns true if the button is currently being clicked. */ bool isButtonDown() const { return isButtonCurrentlyDown; } //============================================================================== + /** Paints the button. + + This method must be implemented by subclasses to define how the button should be drawn. + The button's current state (hovered, clicked) can be determined using the isButtonOver() and + isButtonDown() methods. + + @param g The graphics context to use for drawing. + */ virtual void paintButton (Graphics& g) = 0; //============================================================================== + /** A callback that is called when the button is clicked. */ std::function onClick; //============================================================================== + /** @internal */ void paint (Graphics& g) override; + /** @internal */ void mouseEnter (const MouseEvent& event) override; + /** @internal */ void mouseExit (const MouseEvent& event) override; + /** @internal */ void mouseDown (const MouseEvent& event) override; + /** @internal */ void mouseUp (const MouseEvent& event) override; private: diff --git a/modules/yup_gui/buttons/yup_SwitchButton.h b/modules/yup_gui/buttons/yup_SwitchButton.h index be25172f3..46ea09156 100644 --- a/modules/yup_gui/buttons/yup_SwitchButton.h +++ b/modules/yup_gui/buttons/yup_SwitchButton.h @@ -79,6 +79,7 @@ class YUP_API SwitchButton : public Button //============================================================================== /** Called when the toggle state changes. + Override this to perform custom actions when the switch is toggled. */ virtual void toggleStateChanged() {} @@ -93,6 +94,9 @@ class YUP_API SwitchButton : public Button }; //============================================================================== + /** @internal */ + Rectangle getSwitchCircleBounds() const { return switchCircleBounds; } + /** @internal */ void refreshDisplay (double lastFrameTimeSeconds) override; /** @internal */ @@ -102,9 +106,6 @@ class YUP_API SwitchButton : public Button /** @internal */ void mouseUp (const MouseEvent& event) override; - /** @internal */ - Rectangle getSwitchCircleBounds() const { return switchCircleBounds; } - private: //============================================================================== void updateSwitchCirclePosition(); diff --git a/modules/yup_gui/buttons/yup_TextButton.h b/modules/yup_gui/buttons/yup_TextButton.h index 41341d941..7e6101661 100644 --- a/modules/yup_gui/buttons/yup_TextButton.h +++ b/modules/yup_gui/buttons/yup_TextButton.h @@ -23,16 +23,30 @@ namespace yup { //============================================================================== +/** A button that displays text. + + To create a custom text button, inherit from this class and implement the paintButton() method. + + The button will automatically track mouse events to determine when it is being hovered over + or clicked, and will call the onClick callback when it is clicked. + + @see Button, Component +*/ class YUP_API TextButton : public Button { public: //============================================================================== + /** Creates a text button with the given component ID. */ TextButton (StringRef componentID = {}); //============================================================================== - + /** Returns the text displayed on the button. */ String getButtonText() const { return buttonText; } + /** Sets the text displayed on the button + + @param newButtonText The new text to display on the button. + */ void setButtonText (StringRef newButtonText); //============================================================================== @@ -47,17 +61,19 @@ class YUP_API TextButton : public Button static const Identifier outlineFocusedColorId; }; + //============================================================================== + /** Returns the bounds of the text within the button. */ Rectangle getTextBounds() const; //============================================================================== + /** @internal */ + StyledText& getStyledText() const noexcept { return const_cast (styledText); } + /** @internal */ void paintButton (Graphics& g) override; /** @internal */ void resized() override; - /** @internal */ - StyledText& getStyledText() const noexcept { return const_cast (styledText); } - private: void updateTextLayout(); diff --git a/modules/yup_gui/buttons/yup_ToggleButton.h b/modules/yup_gui/buttons/yup_ToggleButton.h index 6197b0f61..d8520a1d9 100644 --- a/modules/yup_gui/buttons/yup_ToggleButton.h +++ b/modules/yup_gui/buttons/yup_ToggleButton.h @@ -70,6 +70,7 @@ class YUP_API ToggleButton : public Button //============================================================================== /** Called when the toggle state changes. + Override this to perform custom actions when the button is toggled. */ virtual void toggleStateChanged() {} @@ -86,6 +87,9 @@ class YUP_API ToggleButton : public Button }; //============================================================================== + /** @internal */ + StyledText& getStyledText() const noexcept { return const_cast (styledText); } + /** @internal */ void paintButton (Graphics& g) override; /** @internal */ @@ -97,9 +101,6 @@ class YUP_API ToggleButton : public Button /** @internal */ void focusLost() override; - /** @internal */ - StyledText& getStyledText() const noexcept { return const_cast (styledText); } - private: String buttonText; StyledText styledText; diff --git a/modules/yup_gui/component/yup_Component.cpp b/modules/yup_gui/component/yup_Component.cpp index 038a2896a..f5f9be840 100644 --- a/modules/yup_gui/component/yup_Component.cpp +++ b/modules/yup_gui/component/yup_Component.cpp @@ -1144,31 +1144,31 @@ std::optional Component::findColor (const Identifier& colorId) const //============================================================================== -void Component::setStyleProperty (const Identifier& propertyId, const std::optional& property) +void Component::setMetric (const Identifier& metricId, const std::optional& metric) { - if (property) - properties.set (propertyId, *property); + if (metric) + properties.set (metricId, static_cast (*metric)); else - properties.remove (propertyId); + properties.remove (metricId); styleChanged(); } -std::optional Component::getStyleProperty (const Identifier& propertyId) const +std::optional Component::getMetric (const Identifier& metricId) const { - if (auto property = properties.getVarPointer (propertyId); property != nullptr && ! property->isVoid()) - return *property; + if (auto value = properties.getVarPointer (metricId); value != nullptr && value->isDouble()) + return static_cast (static_cast (*value)); return std::nullopt; } -std::optional Component::findStyleProperty (const Identifier& propertyId) const +std::optional Component::findMetric (const Identifier& metricId) const { - if (auto property = getStyleProperty (propertyId)) - return property; + if (auto metric = getMetric (metricId)) + return metric; if (parentComponent != nullptr) - return parentComponent->findStyleProperty (propertyId); + return parentComponent->findMetric (metricId); return std::nullopt; } diff --git a/modules/yup_gui/component/yup_Component.h b/modules/yup_gui/component/yup_Component.h index e421741a8..817127d20 100644 --- a/modules/yup_gui/component/yup_Component.h +++ b/modules/yup_gui/component/yup_Component.h @@ -1185,28 +1185,35 @@ class YUP_API Component : public MouseListener //============================================================================== - /** Set a style property for the component. + /** Set a metric value for the component. - @param propertyId The identifier of the property to set. - @param property The property to set. - */ - void setStyleProperty (const Identifier& propertyId, const std::optional& property); + Metrics are numeric values like corner radius, padding, or spacing that + can be themed globally and overridden per-component, following the same + pattern as component colors. + + @param metricId The identifier of the metric to set. + @param metric The metric value to set. Pass std::nullopt to remove the override. + */ + void setMetric (const Identifier& metricId, const std::optional& metric); - /** Get a style property for the component. + /** Get the metric override for this component (does not walk parents). - @param propertyId The identifier of the property to get. + @param metricId The identifier of the metric to get. - @return The property of the component. - */ - std::optional getStyleProperty (const Identifier& propertyId) const; + @return The metric value, or std::nullopt if not set on this specific component. + */ + std::optional getMetric (const Identifier& metricId) const; - /** Find a style property for the component. + /** Find the metric value, walking up the parent hierarchy. - @param propertyId The identifier of the property to find. + Checks this component first, then walks up the parent chain. If no override + is found, falls back to the value registered in the global ApplicationTheme. - @return The property of the component. - */ - std::optional findStyleProperty (const Identifier& propertyId) const; + @param metricId The identifier of the metric to find. + + @return The metric value, or std::nullopt if not found anywhere. + */ + std::optional findMetric (const Identifier& metricId) const; //============================================================================== /** A bail out checker for the component. */ diff --git a/modules/yup_gui/component/yup_ComponentNative.cpp b/modules/yup_gui/component/yup_ComponentNative.cpp index 815911174..a3d93f6b2 100644 --- a/modules/yup_gui/component/yup_ComponentNative.cpp +++ b/modules/yup_gui/component/yup_ComponentNative.cpp @@ -66,6 +66,15 @@ ComponentNative::Options& ComponentNative::Options::withAllowedHighDensityDispla return *this; } +ComponentNative::Options& ComponentNative::Options::withMouseCapture (bool shouldCaptureMouse) noexcept +{ + if (shouldCaptureMouse) + flags |= captureMouse; + else + flags &= ~captureMouse; + return *this; +} + ComponentNative::Options& ComponentNative::Options::withTemporaryWindow (bool shouldBeTemporary) noexcept { if (shouldBeTemporary) diff --git a/modules/yup_gui/component/yup_ComponentNative.h b/modules/yup_gui/component/yup_ComponentNative.h index 3b18516d5..36d1bd760 100644 --- a/modules/yup_gui/component/yup_ComponentNative.h +++ b/modules/yup_gui/component/yup_ComponentNative.h @@ -44,6 +44,7 @@ class YUP_API ComponentNative : public ReferenceCountedObject struct temporaryWindowTag; struct renderContinuousTag; struct allowHighDensityDisplayTag; + struct captureMouseTag; public: //============================================================================== @@ -57,7 +58,8 @@ class YUP_API ComponentNative : public ReferenceCountedObject resizableWindowTag, temporaryWindowTag, renderContinuousTag, - allowHighDensityDisplayTag>; + allowHighDensityDisplayTag, + captureMouseTag>; /** No flags set. */ static inline constexpr Flags noFlags = Flags(); @@ -71,6 +73,8 @@ class YUP_API ComponentNative : public ReferenceCountedObject static inline constexpr Flags renderContinuous = Flags::declareValue(); /** Flag to enable high-density display support. */ static inline constexpr Flags allowHighDensityDisplay = Flags::declareValue(); + /** Flag to capture mouse input outside the native window while the component is on the desktop. */ + static inline constexpr Flags captureMouse = Flags::declareValue(); /** Default flags combining decoratedWindow, resizableWindow, and allowHighDensityDisplay. */ static inline constexpr Flags defaultFlags = decoratedWindow | resizableWindow | allowHighDensityDisplay; @@ -127,6 +131,14 @@ class YUP_API ComponentNative : public ReferenceCountedObject */ Options& withAllowedHighDensityDisplay (bool shouldAllowHighDensity) noexcept; + /** Sets whether the native window should capture mouse input outside its bounds. + + @param shouldCaptureMouse True to capture mouse input, false to use normal window-local input. + + @return Reference to this Options object for method chaining. + */ + Options& withMouseCapture (bool shouldCaptureMouse) noexcept; + /** Sets whether the window should be treated as a temporary popup/menu window. @param shouldBeTemporary True for popup/menu-style windows, false for regular windows. diff --git a/modules/yup_gui/desktop/yup_Desktop.h b/modules/yup_gui/desktop/yup_Desktop.h index 221e25972..b8f51aa35 100644 --- a/modules/yup_gui/desktop/yup_Desktop.h +++ b/modules/yup_gui/desktop/yup_Desktop.h @@ -31,7 +31,7 @@ class ComponentNative; access to multiple screens connected to the system. It allows querying and management of different screen properties through the `Screen` objects. */ -class YUP_API Desktop +class YUP_API Desktop : private DeletedAtShutdown { public: //============================================================================== diff --git a/modules/yup_gui/dialogs/yup_FileChooser.cpp b/modules/yup_gui/dialogs/yup_FileChooser.cpp index 183697a75..e3d197ec6 100644 --- a/modules/yup_gui/dialogs/yup_FileChooser.cpp +++ b/modules/yup_gui/dialogs/yup_FileChooser.cpp @@ -22,6 +22,23 @@ namespace yup { +namespace +{ + +CriticalSection& getActiveFileChoosersLock() +{ + static CriticalSection lock; + return lock; +} + +std::vector& getActiveFileChoosers() +{ + static std::vector activeChoosers; + return activeChoosers; +} + +} // namespace + //============================================================================== #if ! YUP_LINUX && ! YUP_WINDOWS && ! YUP_ANDROID class FileChooser::FileChooserImpl @@ -107,18 +124,27 @@ void FileChooser::showDialog (CompletionCallback callback, int flags) if (packageDirsAsFiles) flags |= treatFilePackagesAsDirs; + addToActiveFileChoosers(); + auto capturedCallback = createCapturingCallback (std::move (callback)); - auto showOnMessageThread = [self = Ptr { this }, flags, callback = std::move (capturedCallback)]() mutable + WeakReference weakThis (this); + auto showOnMessageThread = [weakThis, flags, callback = std::move (capturedCallback)]() mutable { - self->showPlatformDialog (std::move (callback), flags); + if (auto* self = weakThis.get()) + { + Ptr retainedSelf (self); + retainedSelf->showPlatformDialog (std::move (callback), flags); + } }; if (! MessageManager::existsAndIsCurrentThread()) { - MessageManager::callAsync ([show = std::move (showOnMessageThread)]() mutable + if (! MessageManager::callAsync ([show = std::move (showOnMessageThread)]() mutable { show(); - }); + })) + removeFromActiveFileChoosers(); + return; } @@ -145,10 +171,63 @@ void FileChooser::invokeCallback (CompletionCallback callback, bool success, con FileChooser::CompletionCallback FileChooser::createCapturingCallback (CompletionCallback callback) { - return [self = Ptr { this }, callback = std::move (callback)] (bool success, const Array& results) + WeakReference weakThis (this); + + return [weakThis, callback = std::move (callback)] (bool success, const Array& results) mutable { - callback (success, results); + auto* chooser = weakThis.get(); + if (chooser == nullptr) + return; + + chooser->removeFromActiveFileChoosers(); + + if (callback) + callback (success, results); }; } +void FileChooser::addToActiveFileChoosers() +{ + static bool installShutdownCallback = [] + { + MessageManager::getInstance()->registerShutdownCallback ([] + { + FileChooser::releaseAllActiveFileChoosers(); + }); + return true; + }(); + + ignoreUnused (installShutdownCallback); + + const ScopedLock lock (getActiveFileChoosersLock()); + getActiveFileChoosers().push_back (this); +} + +void FileChooser::removeFromActiveFileChoosers() +{ + const ScopedLock lock (getActiveFileChoosersLock()); + auto& activeChoosers = getActiveFileChoosers(); + + for (auto it = activeChoosers.begin(); it != activeChoosers.end();) + { + if (it->get() == this) + it = activeChoosers.erase (it); + else + ++it; + } +} + +void FileChooser::releaseAllActiveFileChoosers() +{ + std::vector activeChoosers; + + { + const ScopedLock lock (getActiveFileChoosersLock()); + activeChoosers.swap (getActiveFileChoosers()); + } + + for (auto& chooser : activeChoosers) + chooser->impl.reset(); +} + } // namespace yup diff --git a/modules/yup_gui/dialogs/yup_FileChooser.h b/modules/yup_gui/dialogs/yup_FileChooser.h index cfa98ab0b..5821b3b69 100644 --- a/modules/yup_gui/dialogs/yup_FileChooser.h +++ b/modules/yup_gui/dialogs/yup_FileChooser.h @@ -200,6 +200,9 @@ class YUP_API FileChooser : public ReferenceCountedObject String getFilePatternsForPlatform() const; void invokeCallback (CompletionCallback callback, bool success, const Array& results); CompletionCallback createCapturingCallback (CompletionCallback callback); + void addToActiveFileChoosers(); + void removeFromActiveFileChoosers(); + static void releaseAllActiveFileChoosers(); String title, filters; File startingFile; @@ -210,6 +213,7 @@ class YUP_API FileChooser : public ReferenceCountedObject std::unique_ptr impl; //============================================================================== + YUP_DECLARE_WEAK_REFERENCEABLE (FileChooser) YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FileChooser) }; diff --git a/modules/yup_gui/menus/yup_PopupMenu.cpp b/modules/yup_gui/menus/yup_PopupMenu.cpp index 1bf617a7c..8c498da7f 100644 --- a/modules/yup_gui/menus/yup_PopupMenu.cpp +++ b/modules/yup_gui/menus/yup_PopupMenu.cpp @@ -29,14 +29,30 @@ namespace static std::vector activePopups; +constexpr float separatorHeight = 8.0f; // TODO: move to Options +constexpr float verticalPadding = 4.0f; // TODO: move to Style +constexpr float itemHeight = 22.0f; // TODO: move to Options +constexpr float defaultMenuWidth = 200.0f; // TODO: move to Options +constexpr float horizontalTextPadding = 12.0f; +constexpr float tickedTextIndent = 8.0f; +constexpr float submenuArrowWidth = 24.0f; +constexpr float shortcutTextWidth = 80.0f; +constexpr float itemTextHeight = 14.0f; +constexpr float shortcutTextHeight = 13.0f; +constexpr float screenEdgePadding = 5.0f; + void removeActivePopup (PopupMenu* popupMenu) { for (auto it = activePopups.begin(); it != activePopups.end();) { if (it->get() == popupMenu) + { it = activePopups.erase (it); + } else + { ++it; + } } } @@ -52,6 +68,11 @@ PopupMenu* findActivePopupAt (Point globalPos) return nullptr; } +bool isInsideAnyActivePopup (Point globalPos) +{ + return findActivePopupAt (globalPos) != nullptr; +} + MouseEvent makePopupMouseEvent (const MouseEvent& event, PopupMenu& popupMenu, Point globalPos) { return event.withPosition (popupMenu.screenToLocal (globalPos)) @@ -68,6 +89,14 @@ void installGlobalMouseListener() { const auto globalPos = event.getScreenPosition(); + if (! isInsideAnyActivePopup (globalPos)) + { + if (! activePopups.empty()) + PopupMenu::dismissAllPopups(); + + return; + } + // Walk the component hierarchy from the event source. // If any ancestor is a PopupMenu the click is inside a popup — don't dismiss. auto* comp = event.getSourceComponent(); @@ -230,8 +259,7 @@ Point constrainPositionToAvailableArea (Point desiredPosition, const Rectangle& targetArea) { // Add padding to keep menu slightly away from screen edges - const int padding = 5; - auto constrainedArea = availableArea.reduced (padding); + auto constrainedArea = availableArea.reduced (static_cast (screenEdgePadding)); Point position = desiredPosition; @@ -266,6 +294,29 @@ Point constrainPositionToAvailableArea (Point desiredPosition, return position; } +float measureMenuTextWidth (const String& text, const Font& font) +{ + if (text.isEmpty()) + return 0.0f; + + auto styledText = StyledText(); + { + auto modifier = styledText.startUpdate(); + modifier.setWrap (StyledText::noWrap); + modifier.appendText (text, font); + } + + return styledText.getComputedTextBounds().getWidth(); +} + +float getItemHeight (const PopupMenu::Item& item) +{ + if (item.isCustomComponent()) + return item.customComponent->getHeight(); + + return item.isSeparator() ? separatorHeight : itemHeight; +} + } // namespace //============================================================================== @@ -457,11 +508,26 @@ void PopupMenu::clear() void PopupMenu::setupMenuItems() { - constexpr float separatorHeight = 8.0f; // TODO: move to Options - constexpr float verticalPadding = 4.0f; // TODO: move to Style ? + const auto globalTheme = ApplicationTheme::getGlobalTheme(); + const auto defaultFont = globalTheme != nullptr ? globalTheme->getDefaultFont() + : Font(); + const auto itemFont = defaultFont.withHeight (itemTextHeight); + const auto shortcutFont = defaultFont.withHeight (shortcutTextHeight); + bool anyItemIsTicked = false; + for (const auto& item : items) + { + if (item->isTicked) + { + anyItemIsTicked = true; + break; + } + } - float itemHeight = static_cast (22); // TODO: move to Options - float width = options.minWidth.value_or (200); // TODO: move to magic + const auto minimumWidth = static_cast (jmax (0, options.minWidth.value_or (static_cast (defaultMenuWidth)))); + const auto maximumWidth = static_cast (jmax (static_cast (minimumWidth), + options.maxWidth.value_or (std::numeric_limits::max()))); + + float width = minimumWidth; // First pass: calculate total content height and determine width totalContentHeight = verticalPadding; // Top padding @@ -469,16 +535,34 @@ void PopupMenu::setupMenuItems() { if (item->isCustomComponent()) { - width = jmax (width, item->customComponent->getWidth()); + width = jmax (width, static_cast (item->customComponent->getWidth())); totalContentHeight += item->customComponent->getHeight(); } else { - const auto height = item->isSeparator() ? separatorHeight : itemHeight; - totalContentHeight += height; + totalContentHeight += item->isSeparator() ? separatorHeight : itemHeight; + + if (! item->isSeparator()) + { + auto itemWidth = horizontalTextPadding * 2.0f + + measureMenuTextWidth (item->text, itemFont); + + if (anyItemIsTicked) + itemWidth += tickedTextIndent; + + if (item->shortcutKeyText.isNotEmpty()) + itemWidth += jmax (shortcutTextWidth, + measureMenuTextWidth (item->shortcutKeyText, shortcutFont) + horizontalTextPadding); + + if (item->isSubMenu()) + itemWidth += submenuArrowWidth; + + width = jmax (width, itemWidth); + } } } totalContentHeight += verticalPadding; // Bottom padding + width = jlimit (minimumWidth, maximumWidth, width); // Calculate available content height properly (without depending on current position) calculateAvailableHeight(); @@ -492,15 +576,11 @@ void PopupMenu::setupMenuItems() updateVisibleItemRange(); - // Set menu bounds based on available space - do this only once - if (getWidth() == 0 || getHeight() == 0) // Only set size if not already set - { - float menuHeight = jmin (totalContentHeight, availableContentHeight); - if (showScrollIndicators) - menuHeight -= scrollIndicatorHeight * 2.0f; // Reserve space for indicators - - setSize (static_cast (width), static_cast (menuHeight)); - } + const auto menuHeight = verticalPadding * 2.0f + + getVisibleItemsHeight() + + (showScrollIndicators ? scrollIndicatorHeight * 2.0f : 0.0f); + setSize (static_cast (std::ceil (width)), + static_cast (std::ceil (jmax (itemHeight, menuHeight)))); // Remove all child components first for (auto& item : items) @@ -671,7 +751,8 @@ void PopupMenu::showCustom (const Options& options, bool isSubmenu, std::functio auto nativeOptions = ComponentNative::Options {} .withDecoration (false) .withResizableWindow (false) - .withTemporaryWindow (true); + .withTemporaryWindow (true) + .withMouseCapture (true); if (! isOnDesktop()) addToDesktop (nativeOptions); @@ -877,7 +958,7 @@ void PopupMenu::keyDown (const KeyPress& key, const Point& position) auto keyCode = key.getKey(); if (keyCode == KeyPress::escapeKey) - dismiss(); + dismissAllPopups(); else if (keyCode == KeyPress::upKey) navigateUp(); @@ -920,8 +1001,11 @@ void PopupMenu::showSubmenu (int itemIndex) if (! currentSubmenu) return; + currentSubmenu->parentMenu = this; + // Reset the submenu's state before showing to ensure clean positioning currentSubmenu->resetInternalState(); + currentSubmenu->parentMenu = this; // Configure submenu options auto submenuOptions = prepareSubmenuOptions (currentSubmenu); @@ -1312,100 +1396,43 @@ int PopupMenu::getPreviousSelectableItemIndex (int currentIndex) const void PopupMenu::calculateAvailableHeight() { + const auto minimumMenuHeight = itemHeight + (verticalPadding * 2.0f); + if (options.parentComponent) { - // Calculate available height within parent component bounds - auto parentBounds = options.parentComponent->getLocalBounds().to(); - - // Use the target position/area to determine where the menu will be positioned - float menuY = 0.0f; - - switch (options.positioningMode) - { - case PositioningMode::atPoint: - menuY = options.targetPosition.getY(); - break; - - case PositioningMode::relativeToArea: - menuY = options.targetArea.getY(); - if (options.placement.side == Side::below) - menuY = options.targetArea.getBottom(); - else if (options.placement.side == Side::above) - menuY = options.targetArea.getY(); // Will be adjusted later - break; - - case PositioningMode::relativeToComponent: - if (options.targetComponent) - { - Rectangle targetArea; - if (options.targetComponent->getParentComponent() == options.parentComponent) - targetArea = options.targetComponent->getBounds().to(); - else - targetArea = options.parentComponent->getLocalArea (options.targetComponent, options.targetComponent->getLocalBounds()).to(); - - menuY = targetArea.getY(); - if (options.placement.side == Side::below) - menuY = targetArea.getBottom(); - } - break; - } - - // Calculate available space from anticipated position to parent bottom - availableContentHeight = parentBounds.getBottom() - menuY; - availableContentHeight = jmax (100.0f, availableContentHeight); // Minimum height + const auto parentBounds = options.parentComponent->getLocalBounds().to(); + availableContentHeight = jmax (minimumMenuHeight, parentBounds.getHeight() - (screenEdgePadding * 2.0f)); + return; } - else - { - // Use screen bounds - if (auto* desktop = Desktop::getInstance()) - { - Screen::Ptr screen; - float menuY = 0.0f; - if (options.positioningMode == PositioningMode::atPoint) - { - menuY = options.targetPosition.getY(); - screen = desktop->getScreenContaining (options.targetPosition.to()); - } - else if (options.positioningMode == PositioningMode::relativeToArea) - { - menuY = options.targetArea.getY(); - screen = desktop->getScreenContaining (options.targetArea.to()); - } - else if (options.positioningMode == PositioningMode::relativeToComponent && options.targetComponent) - { - menuY = options.targetComponent->getScreenBounds().getY(); - screen = desktop->getScreenContaining (options.targetComponent); - } + // Use screen bounds + if (auto* desktop = Desktop::getInstance()) + { + Screen::Ptr screen; - if (screen == nullptr) - screen = desktop->getPrimaryScreen(); + if (options.positioningMode == PositioningMode::atPoint) + screen = desktop->getScreenContaining (options.targetPosition.to()); + else if (options.positioningMode == PositioningMode::relativeToArea) + screen = desktop->getScreenContaining (options.targetArea.to()); + else if (options.positioningMode == PositioningMode::relativeToComponent && options.targetComponent) + screen = desktop->getScreenContaining (options.targetComponent); - if (screen != nullptr) - { - auto screenBounds = screen->workArea.to(); + if (screen == nullptr) + screen = desktop->getPrimaryScreen(); - availableContentHeight = screenBounds.getBottom() - menuY; - availableContentHeight = jmax (100.0f, availableContentHeight); - } - else - { - availableContentHeight = 800.0f; // Fallback - } - } + if (screen != nullptr) + availableContentHeight = jmax (minimumMenuHeight, screen->workArea.getHeight() - (screenEdgePadding * 2.0f)); else - { availableContentHeight = 800.0f; // Fallback - } + + return; } + + availableContentHeight = 800.0f; // Fallback } void PopupMenu::layoutVisibleItems (float width) { - constexpr float separatorHeight = 8.0f; // TODO: move to Options - constexpr float verticalPadding = 4.0f; // TODO: move to Style - const float itemHeight = 22.0f; // TODO: move to Options - // Clear all item areas first to prevent rendering artifacts for (auto& item : items) { @@ -1456,11 +1483,6 @@ void PopupMenu::updateVisibleItemRange() return; } - // Calculate how many items can fit in the available space - constexpr float separatorHeight = 8.0f; // TODO: move to Options - constexpr float verticalPadding = 4.0f; // TODO: move to Style - const float itemHeight = 22.0f; // TODO: move to Options - float availableHeight = availableContentHeight; if (showScrollIndicators) availableHeight -= 2 * scrollIndicatorHeight; @@ -1479,13 +1501,7 @@ void PopupMenu::updateVisibleItemRange() for (int i = startIndex; i < static_cast (items.size()); ++i) { - const auto& item = *items[i]; - float itemHeightToAdd; - - if (item.isCustomComponent()) - itemHeightToAdd = item.customComponent->getHeight(); - else - itemHeightToAdd = item.isSeparator() ? separatorHeight : itemHeight; + const auto itemHeightToAdd = getItemHeight (*items[i]); if (usedHeight + itemHeightToAdd > availableHeight) break; @@ -1498,9 +1514,30 @@ void PopupMenu::updateVisibleItemRange() if (visibleCount == 0 && startIndex < static_cast (items.size())) visibleCount = 1; + while (startIndex > 0) + { + const auto previousItemHeight = getItemHeight (*items[startIndex - 1]); + if (usedHeight + previousItemHeight > availableHeight) + break; + + --startIndex; + ++visibleCount; + usedHeight += previousItemHeight; + } + visibleItemRange = Range (startIndex, startIndex + visibleCount); } +float PopupMenu::getVisibleItemsHeight() const +{ + float height = 0.0f; + + for (int i = visibleItemRange.getStart(); i < visibleItemRange.getEnd() && i < static_cast (items.size()); ++i) + height += getItemHeight (*items[i]); + + return height; +} + void PopupMenu::scrollUp() { if (canScrollUp()) @@ -1512,7 +1549,11 @@ void PopupMenu::scrollUp() // Recalculate the end based on available space updateVisibleItemRange(); - // Re-layout visible items without changing menu size + // Re-layout visible items using the exact height of the visible rows. + const auto menuHeight = verticalPadding * 2.0f + + getVisibleItemsHeight() + + (showScrollIndicators ? scrollIndicatorHeight * 2.0f : 0.0f); + setSize (getWidth(), static_cast (std::ceil (jmax (itemHeight, menuHeight)))); layoutVisibleItems (getWidth()); // Repaint to update the display @@ -1531,7 +1572,11 @@ void PopupMenu::scrollDown() // Recalculate the end based on available space updateVisibleItemRange(); - // Re-layout visible items without changing menu size + // Re-layout visible items using the exact height of the visible rows. + const auto menuHeight = verticalPadding * 2.0f + + getVisibleItemsHeight() + + (showScrollIndicators ? scrollIndicatorHeight * 2.0f : 0.0f); + setSize (getWidth(), static_cast (std::ceil (jmax (itemHeight, menuHeight)))); layoutVisibleItems (getWidth()); // Repaint to update the display diff --git a/modules/yup_gui/menus/yup_PopupMenu.h b/modules/yup_gui/menus/yup_PopupMenu.h index 3090bcd0f..3bae12bb3 100644 --- a/modules/yup_gui/menus/yup_PopupMenu.h +++ b/modules/yup_gui/menus/yup_PopupMenu.h @@ -380,6 +380,7 @@ class YUP_API PopupMenu void layoutVisibleItems (float width); Rectangle getMenuContentBounds() const; void updateVisibleItemRange(); + float getVisibleItemsHeight() const; void scrollUp(); void scrollDown(); int getVisibleItemCount() const; diff --git a/modules/yup_gui/native/yup_FileChooser_android.cpp b/modules/yup_gui/native/yup_FileChooser_android.cpp index b08bb45cb..a048f1323 100644 --- a/modules/yup_gui/native/yup_FileChooser_android.cpp +++ b/modules/yup_gui/native/yup_FileChooser_android.cpp @@ -121,9 +121,8 @@ static StringArray createMimeTypes (const String& filters) class FileChooser::FileChooserImpl { public: - FileChooserImpl (FileChooser& owner, CompletionCallback cb) - : fileChooser (owner) - , callback (std::move (cb)) + FileChooserImpl (CompletionCallback cb) + : callback (std::move (cb)) { } @@ -183,9 +182,6 @@ class FileChooser::FileChooserImpl // Invoke callback with results invokeCallback (resultCode == -1, results); - - // Clean up - remove this impl from the FileChooser - fileChooser.impl.reset(); } void invokeCallback (bool result, const Array& results) @@ -195,7 +191,6 @@ class FileChooser::FileChooserImpl } private: - FileChooser& fileChooser; CompletionCallback callback; }; @@ -214,7 +209,7 @@ void FileChooser::showPlatformDialog (CompletionCallback callback, int flags) } // Create the implementation that will stay alive until the result comes back - impl = std::make_unique (*this, std::move (callback)); + impl = std::make_unique (std::move (callback)); LocalRef intent; @@ -279,19 +274,30 @@ void FileChooser::showPlatformDialog (CompletionCallback callback, int flags) // Use YUP's non-blocking activity result handler const int requestCode = 12345; - startAndroidActivityForResult (intent, requestCode, [this] (int activityRequestCode, int resultCode, LocalRef data) + WeakReference weakThis (this); + + startAndroidActivityForResult (intent, requestCode, [weakThis] (int activityRequestCode, int resultCode, LocalRef data) { - if (impl != nullptr) - impl->processActivityResult (activityRequestCode, resultCode, data); + if (auto* chooser = weakThis.get()) + { + FileChooser::Ptr retainedChooser (chooser); + + if (chooser->impl != nullptr) + { + chooser->impl->processActivityResult (activityRequestCode, resultCode, data); + chooser->impl.reset(); + } + } }); } else { // Failed to create intent, cleanup and call callback if (impl != nullptr) + { impl->invokeCallback (false, {}); - - impl.reset(); + impl.reset(); + } } } diff --git a/modules/yup_gui/native/yup_FileChooser_mac.mm b/modules/yup_gui/native/yup_FileChooser_mac.mm index bb318bb77..9927610a0 100644 --- a/modules/yup_gui/native/yup_FileChooser_mac.mm +++ b/modules/yup_gui/native/yup_FileChooser_mac.mm @@ -122,8 +122,8 @@ } } - MessageManager::callAsync([this, callback = std::move(callback), result, results] - { invokeCallback(std::move(callback), result == NSModalResponseOK, results); }); + MessageManager::callAsync([callback = std::move(callback), result, results]() mutable + { callback(result == NSModalResponseOK, results); }); }]; } else @@ -161,8 +161,8 @@ } } - MessageManager::callAsync([this, callback = std::move(callback), result, results] - { invokeCallback(std::move(callback), result == NSModalResponseOK, results); }); + MessageManager::callAsync([callback = std::move(callback), result, results]() mutable + { callback(result == NSModalResponseOK, results); }); }]; } } diff --git a/modules/yup_gui/native/yup_Windowing_sdl2.cpp b/modules/yup_gui/native/yup_Windowing_sdl2.cpp index ea04a68d5..72e0bfe10 100644 --- a/modules/yup_gui/native/yup_Windowing_sdl2.cpp +++ b/modules/yup_gui/native/yup_Windowing_sdl2.cpp @@ -23,20 +23,25 @@ namespace yup { //============================================================================== -#if YUP_ENABLE_WINDOWING_EVENT_LOGGING -#define YUP_WINDOWING_LOG(textToWrite) YUP_DBG (textToWrite) -#else -#define YUP_WINDOWING_LOG(textToWrite) \ - { \ - } -#endif + +std::atomic_flag SDL2ComponentNative::isInitialised = ATOMIC_FLAG_INIT; +int SDL2ComponentNative::mouseCaptureRequestCount = 0; +uint32_t SDL2ComponentNative::lastCapturedMouseButtonState = 0; +bool SDL2ComponentNative::popupDismissalCheckPending = false; //============================================================================== -std::atomic_flag SDL2ComponentNative::isInitialised = ATOMIC_FLAG_INIT; +static constexpr uint32 sdlDefaultSubsystems = SDL_INIT_VIDEO | SDL_INIT_EVENTS; //============================================================================== +static String getSDLVersionString (const SDL_version& version) +{ + return String (static_cast (version.major)) + "." + + String (static_cast (version.minor)) + "." + + String (static_cast (version.patch)); +} + SDL2ComponentNative::SDL2ComponentNative (Component& component, const Options& options, void* parent) @@ -50,12 +55,17 @@ SDL2ComponentNative::SDL2ComponentNative (Component& component, , desiredFrameRate (options.framerateRedraw.value_or (60.0f)) , shouldRenderContinuous (options.flags.test (renderContinuous)) , updateOnlyWhenFocused (options.updateOnlyWhenFocused) + , shouldCaptureMouse (options.flags.test (captureMouse)) { incReferenceCount(); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: constructing native component: component=" << String::toHexString (static_cast (reinterpret_cast (&component))) << ", parent=" << String::toHexString (static_cast (reinterpret_cast (parent))) << ", bounds=" << component.getBounds().toString() << ", visible=" << String (component.isVisible() ? "true" : "false") << ", renderContinuous=" << String (shouldRenderContinuous ? "true" : "false") << ", updateOnlyWhenFocused=" << String (updateOnlyWhenFocused ? "true" : "false") << ", desiredFrameRate=" << String (desiredFrameRate)); + Desktop::getInstance()->registerNativeComponent (this); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: registered native component"); SDL_AddEventWatch (eventDispatcher, this); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: registered window event watch"); // Setup window hints and get flags windowFlags = setContextWindowHints (currentGraphicsApi); @@ -82,6 +92,8 @@ SDL2ComponentNative::SDL2ComponentNative (Component& component, SDL_SetHint (SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1"); // Create the window, renderer and parent it + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: creating window: title=" << component.getTitle() << ", flags=" << String::toHexString (static_cast (windowFlags)) << ", parent=" << String::toHexString (static_cast (reinterpret_cast (parent)))); + window = SDL_CreateWindow (component.getTitle().toRawUTF8(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, @@ -89,20 +101,32 @@ SDL2ComponentNative::SDL2ComponentNative (Component& component, 1, windowFlags); if (window == nullptr) + { + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: unable to create heavyweight window: " << SDL_GetError()); return; // TODO - raise something ? + } + + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: created window: id=" << static_cast (SDL_GetWindowID (window)) << ", window=" << String::toHexString (static_cast (reinterpret_cast (window)))); SDL_SetWindowData (window, "self", this); if (parent != nullptr) + { setNativeParent (parent, window); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: set native parent"); + } if (currentGraphicsApi == GraphicsContext::OpenGL) { windowContext = SDL_GL_CreateContext (window); if (windowContext == nullptr) + { + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: unable to create GL context: " << SDL_GetError()); return; // TODO - raise something ? + } SDL_GL_MakeCurrent (window, windowContext); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: created GL context"); } // Create the rendering context @@ -111,7 +135,12 @@ SDL2ComponentNative::SDL2ComponentNative (Component& component, graphicsOptions.loaderFunction = SDL_GL_GetProcAddress; context = GraphicsContext::createContext (currentGraphicsApi, graphicsOptions); if (context == nullptr) + { + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: unable to create YUP GraphicsContext"); return; // TODO - raise something ? + } + + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: created YUP GraphicsContext"); // Resize after callbacks are in place setBounds ( @@ -120,17 +149,29 @@ SDL2ComponentNative::SDL2ComponentNative (Component& component, jmax (1, screenBounds.getWidth()), jmax (1, screenBounds.getHeight()) }); + // Check mouse capture + if (shouldCaptureMouse && isVisible()) + updateMouseCapture (true); + // Start the rendering startRendering(); + + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: native component constructed: window=" << String::toHexString (static_cast (reinterpret_cast (window))) << ", context=" << String::toHexString (static_cast (reinterpret_cast (context.get()))) << ", rendering=" << String (isRendering() ? "true" : "false")); } SDL2ComponentNative::~SDL2ComponentNative() { + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: destroying native component: window=" << String::toHexString (static_cast (reinterpret_cast (window))) << ", context=" << String::toHexString (static_cast (reinterpret_cast (context.get()))) << ", rendering=" << String (isRendering() ? "true" : "false")); + + updateMouseCapture (false); + // Remove event watch SDL_DelEventWatch (eventDispatcher, this); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: unregistered window event watch"); // Unregister this component from the desktop Desktop::getInstance()->unregisterNativeComponent (this); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: unregistered native component"); // Stop the rendering stopRendering(); @@ -140,8 +181,11 @@ SDL2ComponentNative::~SDL2ComponentNative() { SDL_SetWindowData (window, "self", nullptr); SDL_DestroyWindow (window); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: destroyed window"); window = nullptr; } + + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: native component destroyed"); } //============================================================================== @@ -175,12 +219,24 @@ String SDL2ComponentNative::getTitle() const void SDL2ComponentNative::setVisible (bool shouldBeVisible) { if (window == nullptr) + { + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: setVisible skipped: window is null, visible=" << String (shouldBeVisible ? "true" : "false")); return; + } + + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: setVisible " << String (shouldBeVisible ? "true" : "false") << ", currentFlags=" << String::toHexString (static_cast (SDL_GetWindowFlags (window)))); if (shouldBeVisible) + { SDL_ShowWindow (window); + repaint(); + updateMouseCapture (true); + } else + { + updateMouseCapture (false); SDL_HideWindow (window); + } } bool SDL2ComponentNative::isVisible() const @@ -193,7 +249,10 @@ bool SDL2ComponentNative::isVisible() const void SDL2ComponentNative::toFront() { if (window != nullptr && isVisible()) + { + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: raise window"); SDL_RaiseWindow (window); + } } //============================================================================== @@ -213,12 +272,18 @@ Size SDL2ComponentNative::getContentSize() const void SDL2ComponentNative::setSize (const Size& newSize) { if (window == nullptr) + { + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: setSize skipped: window is null, size=" << newSize.toString()); return; + } screenBounds = screenBounds.withSize (newSize); if (auto currentSize = getSize(); currentSize != newSize) + { + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: setSize " << currentSize.toString() << " -> " << newSize.toString()); SDL_SetWindowSize (window, jmax (1, newSize.getWidth()), jmax (1, newSize.getHeight())); + } } Size SDL2ComponentNative::getSize() const @@ -234,12 +299,18 @@ Size SDL2ComponentNative::getSize() const void SDL2ComponentNative::setPosition (const Point& newPosition) { if (window == nullptr) + { + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: setPosition skipped: window is null, position=" << newPosition.toString()); return; + } screenBounds = screenBounds.withPosition (newPosition); if (auto currentPosition = getPosition(); currentPosition != newPosition) + { + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: setPosition " << currentPosition.toString() << " -> " << newPosition.toString()); SDL_SetWindowPosition (window, newPosition.getX(), newPosition.getY()); + } } Point SDL2ComponentNative::getPosition() const @@ -259,7 +330,10 @@ void SDL2ComponentNative::setBounds (const Rectangle& newBounds) #else if (window == nullptr) + { + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: setBounds skipped: window is null, bounds=" << newBounds.toString()); return; + } auto adjustedBounds = newBounds; int leftMargin = 0, topMargin = 0, rightMargin = 0, bottomMargin = 0; @@ -287,12 +361,18 @@ void SDL2ComponentNative::setBounds (const Rectangle& newBounds) jmax (1, adjustedBounds.getHeight() - topMargin - bottomMargin) }); if (auto currentSize = getSize(); currentSize != adjustedBounds.getSize()) + { + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: setBounds size " << currentSize.toString() << " -> " << adjustedBounds.getSize().toString() << ", requested=" << newBounds.toString() << ", margins=" << leftMargin << "," << topMargin << "," << rightMargin << "," << bottomMargin); SDL_SetWindowSize (window, adjustedBounds.getWidth(), adjustedBounds.getHeight()); + } #endif if (auto currentPosition = getPosition(); currentPosition != adjustedBounds.getPosition()) + { + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: setBounds position " << currentPosition.toString() << " -> " << adjustedBounds.getPosition().toString() << ", requested=" << newBounds.toString()); SDL_SetWindowPosition (window, adjustedBounds.getX(), adjustedBounds.getY()); + } screenBounds = newBounds; @@ -309,7 +389,12 @@ Rectangle SDL2ComponentNative::getBounds() const void SDL2ComponentNative::setFullScreen (bool shouldBeFullScreen) { if (window == nullptr) + { + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: setFullScreen skipped: window is null, fullScreen=" << String (shouldBeFullScreen ? "true" : "false")); return; + } + + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: setFullScreen " << String (shouldBeFullScreen ? "true" : "false") << ", current=" << String (isFullScreen() ? "true" : "false")); if (shouldBeFullScreen) { @@ -656,6 +741,8 @@ void SDL2ComponentNative::renderContext() { YUP_PROFILE_NAMED_INTERNAL_TRACE (RenderContext); + pollCapturedMouseState(); + if (context == nullptr) return; @@ -666,15 +753,21 @@ void SDL2ComponentNative::renderContext() if (contentWidth == 0 || contentHeight == 0) return; + if (! isVisible()) + return; + if (currentContentWidth != contentWidth || currentContentHeight != contentHeight) { YUP_PROFILE_NAMED_INTERNAL_TRACE (ResizeRenderer); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: resize render target " << currentContentWidth << "x" << currentContentHeight << " -> " << contentWidth << "x" << contentHeight << ", dpiScale=" << getScaleDpi()); + currentContentWidth = contentWidth; currentContentHeight = contentHeight; context->onSizeChanged (getNativeHandle(), contentWidth, contentHeight, 0); renderer = context->makeRenderer (contentWidth, contentHeight); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: renderer " << String (renderer != nullptr ? "created" : "creation failed")); repaint(); } @@ -793,6 +886,8 @@ void SDL2ComponentNative::renderContext() void SDL2ComponentNative::startRendering() { + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: startRendering requested: timerDriven=" << String (renderDrivenByTimer ? "true" : "false") << ", alreadyRendering=" << String (isRendering() ? "true" : "false") << ", desiredFrameRate=" << String (desiredFrameRate)); + lastRenderTimeSeconds = yup::Time::getMillisecondCounterHiRes() / 1000.0; frameRateStartTimeSeconds = lastRenderTimeSeconds; frameRateCounter = 0; @@ -809,14 +904,21 @@ void SDL2ComponentNative::startRendering() } repaint(); + + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: startRendering completed: rendering=" << String (isRendering() ? "true" : "false")); } void SDL2ComponentNative::stopRendering() { + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: stopRendering requested: rendering=" << String (isRendering() ? "true" : "false")); + if constexpr (renderDrivenByTimer) { if (isTimerRunning()) + { stopTimer(); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: stopped render timer"); + } } else { @@ -826,8 +928,11 @@ void SDL2ComponentNative::stopRendering() notify(); renderEvent.signal(); stopThread (-1); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: stopped render thread"); } } + + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: stopRendering completed: rendering=" << String (isRendering() ? "true" : "false")); } bool SDL2ComponentNative::isRendering() const @@ -1060,7 +1165,12 @@ void SDL2ComponentNative::handleMoved (int xpos, int ypos) YUP_PROFILE_INTERNAL_TRACE(); if (internalBoundsChange) + { + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: handleMoved ignored during internal bounds change: " << xpos << " " << ypos); return; + } + + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: handleMoved " << screenBounds.getX() << " " << screenBounds.getY() << " -> " << xpos << " " << ypos << ", parent=" << String::toHexString (static_cast (reinterpret_cast (parentWindow)))); component.internalMoved (xpos, ypos); @@ -1071,6 +1181,7 @@ void SDL2ComponentNative::handleMoved (int xpos, int ypos) auto preventBoundsChange = ScopedValueSetter (internalBoundsChange, true); auto nativeWindowPos = getNativeWindowPosition (parentWindow); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: parent window position sync after move: " << nativeWindowPos.toString()); setPosition (nativeWindowPos.getTopLeft()); } } @@ -1079,6 +1190,8 @@ void SDL2ComponentNative::handleResized (int width, int height) { YUP_PROFILE_INTERNAL_TRACE(); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: handleResized " << screenBounds.getWidth() << "x" << screenBounds.getHeight() << " -> " << width << "x" << height << ", parent=" << String::toHexString (static_cast (reinterpret_cast (parentWindow)))); + component.internalResized (width, height); screenBounds = screenBounds.withSize (width, height); @@ -1088,6 +1201,7 @@ void SDL2ComponentNative::handleResized (int width, int height) auto preventBoundsChange = ScopedValueSetter (internalBoundsChange, true); auto nativeWindowPos = getNativeWindowPosition (parentWindow); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: parent window position sync after resize: " << nativeWindowPos.toString()); setPosition (nativeWindowPos.getTopLeft()); } @@ -1101,6 +1215,8 @@ void SDL2ComponentNative::handleFocusChanged (bool gotFocus) { YUP_PROFILE_INTERNAL_TRACE(); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: handleFocusChanged " << String (gotFocus ? "true" : "false") << ", rendering=" << String (isRendering() ? "true" : "false")); + if (gotFocus) { if (! isRendering()) @@ -1150,6 +1266,8 @@ void SDL2ComponentNative::handleFocusChanged (bool gotFocus) if (isRendering()) stopRendering(); } + + triggerPopupDismissalCheck(); } } @@ -1160,6 +1278,7 @@ bool SDL2ComponentNative::hasNativeKeyboardFocus() const void SDL2ComponentNative::handleMinimized() { + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: handleMinimized"); PopupMenu::dismissAllPopups(); stopRendering(); @@ -1167,16 +1286,19 @@ void SDL2ComponentNative::handleMinimized() void SDL2ComponentNative::handleMaximized() { + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: handleMaximized"); repaint(); } void SDL2ComponentNative::handleRestored() { + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: handleRestored"); repaint(); } void SDL2ComponentNative::handleExposed() { + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: handleExposed"); repaint(); } @@ -1184,6 +1306,8 @@ void SDL2ComponentNative::handleContentScaleChanged() { YUP_PROFILE_INTERNAL_TRACE(); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: handleContentScaleChanged dpiScale=" << getScaleDpi()); + component.internalContentScaleChanged (getScaleDpi()); handleResized (screenBounds.getWidth(), screenBounds.getHeight()); @@ -1193,6 +1317,8 @@ void SDL2ComponentNative::handleDisplayChanged() { YUP_PROFILE_INTERNAL_TRACE(); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: handleDisplayChanged"); + component.internalDisplayChanged(); } @@ -1268,27 +1394,27 @@ void SDL2ComponentNative::handleWindowEvent (const SDL_WindowEvent& windowEvent) switch (windowEvent.event) { case SDL_WINDOWEVENT_CLOSE: - YUP_WINDOWING_LOG ("SDL_WINDOWEVENT_CLOSE"); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL_WINDOWEVENT_CLOSE"); component.internalUserTriedToCloseWindow(); break; case SDL_WINDOWEVENT_RESIZED: - YUP_WINDOWING_LOG ("SDL_WINDOWEVENT_RESIZED " << windowEvent.data1 << " " << windowEvent.data2); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL_WINDOWEVENT_RESIZED " << windowEvent.data1 << " " << windowEvent.data2); break; case SDL_WINDOWEVENT_SIZE_CHANGED: - YUP_WINDOWING_LOG ("SDL_WINDOWEVENT_SIZE_CHANGED " << windowEvent.data1 << " " << windowEvent.data2); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL_WINDOWEVENT_SIZE_CHANGED " << windowEvent.data1 << " " << windowEvent.data2); handleResized (windowEvent.data1, windowEvent.data2); break; case SDL_WINDOWEVENT_MOVED: - YUP_WINDOWING_LOG ("SDL_WINDOWEVENT_MOVED " << windowEvent.data1 << " " << windowEvent.data2); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL_WINDOWEVENT_MOVED " << windowEvent.data1 << " " << windowEvent.data2); handleMoved (windowEvent.data1, windowEvent.data2); break; case SDL_WINDOWEVENT_ENTER: { - YUP_WINDOWING_LOG ("SDL_WINDOWEVENT_ENTER"); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL_WINDOWEVENT_ENTER"); int x = 0, y = 0; SDL_GetMouseState (&x, &y); handleMouseEnter ({ x, y }); @@ -1297,7 +1423,7 @@ void SDL2ComponentNative::handleWindowEvent (const SDL_WindowEvent& windowEvent) case SDL_WINDOWEVENT_LEAVE: { - YUP_WINDOWING_LOG ("SDL_WINDOWEVENT_LEAVE"); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL_WINDOWEVENT_LEAVE"); int x = 0, y = 0; SDL_GetMouseState (&x, &y); handleMouseLeave ({ x, y }); @@ -1305,7 +1431,7 @@ void SDL2ComponentNative::handleWindowEvent (const SDL_WindowEvent& windowEvent) } case SDL_WINDOWEVENT_SHOWN: - YUP_WINDOWING_LOG ("SDL_WINDOWEVENT_SHOWN"); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL_WINDOWEVENT_SHOWN"); if (firstDisplay) { firstDisplay = false; @@ -1316,45 +1442,45 @@ void SDL2ComponentNative::handleWindowEvent (const SDL_WindowEvent& windowEvent) break; case SDL_WINDOWEVENT_HIDDEN: - YUP_WINDOWING_LOG ("SDL_WINDOWEVENT_HIDDEN"); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL_WINDOWEVENT_HIDDEN"); break; case SDL_WINDOWEVENT_MINIMIZED: - YUP_WINDOWING_LOG ("SDL_WINDOWEVENT_MINIMIZED"); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL_WINDOWEVENT_MINIMIZED"); handleMinimized(); break; case SDL_WINDOWEVENT_MAXIMIZED: - YUP_WINDOWING_LOG ("SDL_WINDOWEVENT_MAXIMIZED"); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL_WINDOWEVENT_MAXIMIZED"); handleMaximized(); break; case SDL_WINDOWEVENT_RESTORED: - YUP_WINDOWING_LOG ("SDL_WINDOWEVENT_RESTORED"); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL_WINDOWEVENT_RESTORED"); handleRestored(); break; case SDL_WINDOWEVENT_EXPOSED: - YUP_WINDOWING_LOG ("SDL_WINDOWEVENT_EXPOSED"); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL_WINDOWEVENT_EXPOSED"); repaint(); break; case SDL_WINDOWEVENT_FOCUS_GAINED: - YUP_WINDOWING_LOG ("SDL_WINDOWEVENT_FOCUS_GAINED"); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL_WINDOWEVENT_FOCUS_GAINED"); handleFocusChanged (true); break; case SDL_WINDOWEVENT_FOCUS_LOST: - YUP_WINDOWING_LOG ("SDL_WINDOWEVENT_FOCUS_LOST"); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL_WINDOWEVENT_FOCUS_LOST"); handleFocusChanged (false); break; case SDL_WINDOWEVENT_TAKE_FOCUS: - YUP_WINDOWING_LOG ("SDL_WINDOWEVENT_TAKE_FOCUS"); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL_WINDOWEVENT_TAKE_FOCUS"); break; case SDL_WINDOWEVENT_DISPLAY_CHANGED: - YUP_WINDOWING_LOG ("SDL_WINDOWEVENT_DISPLAY_CHANGED"); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL_WINDOWEVENT_DISPLAY_CHANGED"); handleContentScaleChanged(); break; } @@ -1378,23 +1504,23 @@ void SDL2ComponentNative::handleEvent (SDL_Event* event) case SDL_RENDER_TARGETS_RESET: { - YUP_WINDOWING_LOG ("SDL_RENDER_TARGETS_RESET"); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL_RENDER_TARGETS_RESET"); break; } case SDL_RENDER_DEVICE_RESET: { - YUP_WINDOWING_LOG ("SDL_RENDER_DEVICE_RESET"); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL_RENDER_DEVICE_RESET"); break; } case SDL_MOUSEMOTION: { - //YUP_WINDOWING_LOG ("SDL_MOUSEMOTION " << event->motion.x << " " << event->motion.y); + //YUP_MODULE_DBG (GUI_WINDOWING, "SDL_MOUSEMOTION " << event->motion.x << " " << event->motion.y); auto cursorPosition = Point { static_cast (event->motion.x), static_cast (event->motion.y) }; - if (event->window.windowID == SDL_GetWindowID (window)) + if (event->motion.windowID == SDL_GetWindowID (window)) handleMouseMoveOrDrag (cursorPosition); break; @@ -1402,35 +1528,34 @@ void SDL2ComponentNative::handleEvent (SDL_Event* event) case SDL_MOUSEBUTTONDOWN: { - YUP_WINDOWING_LOG ("SDL_MOUSEBUTTONDOWN " << event->button.x << " " << event->button.y); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL_MOUSEBUTTONDOWN " << event->button.x << " " << event->button.y); auto cursorPosition = Point { static_cast (event->button.x), static_cast (event->button.y) }; if (event->button.windowID == SDL_GetWindowID (window)) handleMouseDown (cursorPosition, toMouseButton (event->button.button), KeyModifiers (SDL_GetModState())); - else - ; // TODO - when opening a window in mouse down, mouse up is sent to the other window break; } case SDL_MOUSEBUTTONUP: { - YUP_WINDOWING_LOG ("SDL_MOUSEBUTTONUP " << event->button.x << " " << event->button.y); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL_MOUSEBUTTONUP " << event->button.x << " " << event->button.y); auto cursorPosition = Point { static_cast (event->button.x), static_cast (event->button.y) }; if (event->button.windowID == SDL_GetWindowID (window)) handleMouseUp (cursorPosition, toMouseButton (event->button.button), KeyModifiers (SDL_GetModState())); - else - ; // TODO - when opening a window in mouse down, mouse up is sent to the other window + + else if (lastComponentClicked != nullptr) + handleMouseUp (cursorPosition, toMouseButton (event->button.button), KeyModifiers (SDL_GetModState())); break; } case SDL_MOUSEWHEEL: { - YUP_WINDOWING_LOG ("SDL_MOUSEWHEEL " << event->wheel.x << " " << event->wheel.y); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL_MOUSEWHEEL " << event->wheel.x << " " << event->wheel.y); auto cursorPosition = getCursorPosition(); @@ -1442,7 +1567,7 @@ void SDL2ComponentNative::handleEvent (SDL_Event* event) case SDL_KEYDOWN: { - YUP_WINDOWING_LOG ("SDL_KEYDOWN " << event->key.keysym.sym << " " << event->key.keysym.scancode); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL_KEYDOWN " << event->key.keysym.sym << " " << event->key.keysym.scancode); auto cursorPosition = getCursorPosition(); auto modifiers = toKeyModifiers (event->key.keysym.mod); @@ -1455,7 +1580,7 @@ void SDL2ComponentNative::handleEvent (SDL_Event* event) case SDL_KEYUP: { - YUP_WINDOWING_LOG ("SDL_KEYUP " << event->key.keysym.sym << " " << event->key.keysym.scancode); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL_KEYUP " << event->key.keysym.sym << " " << event->key.keysym.scancode); auto cursorPosition = getCursorPosition(); auto modifiers = toKeyModifiers (event->key.keysym.mod); @@ -1468,7 +1593,7 @@ void SDL2ComponentNative::handleEvent (SDL_Event* event) case SDL_TEXTINPUT: { - YUP_WINDOWING_LOG ("SDL_TEXTINPUT " << String::fromUTF8 (event->text.text)); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL_TEXTINPUT " << String::fromUTF8 (event->text.text)); // auto cursorPosition = getCursorPosition(); // auto modifiers = toKeyModifiers (getKeyModifiers()); @@ -1481,7 +1606,7 @@ void SDL2ComponentNative::handleEvent (SDL_Event* event) case SDL_TEXTEDITING: { - YUP_WINDOWING_LOG ("SDL_TEXTEDITING"); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL_TEXTEDITING"); // auto cursorPosition = getCursorPosition(); // auto modifiers = toKeyModifiers (getKeyModifiers()); @@ -1505,7 +1630,7 @@ int SDL2ComponentNative::eventDispatcher (void* userdata, SDL_Event* event) { case SDL_QUIT: { - YUP_WINDOWING_LOG ("SDL_QUIT"); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL_QUIT"); break; } @@ -1515,6 +1640,8 @@ int SDL2ComponentNative::eventDispatcher (void* userdata, SDL_Event* event) { if (auto nativeComponent = dynamic_cast (component.get())) nativeComponent->handleEvent (event); + else + YUP_MODULE_DBG (GUI_WINDOWING, "Received event for unknown component"); } break; @@ -1526,6 +1653,157 @@ int SDL2ComponentNative::eventDispatcher (void* userdata, SDL_Event* event) //============================================================================== +void SDL2ComponentNative::triggerPopupDismissalCheck() +{ + if (popupDismissalCheckPending) + return; + + popupDismissalCheckPending = true; + + if (! MessageManager::callAsync ([] + { + dismissPopupsIfNoNativeWindowHasFocus(); + })) + { + popupDismissalCheckPending = false; + } +} + +void SDL2ComponentNative::dismissPopupsIfNoNativeWindowHasFocus() +{ + popupDismissalCheckPending = false; + + if (anyNativeWindowHasKeyboardFocus()) + return; + + PopupMenu::dismissAllPopups(); +} + +bool SDL2ComponentNative::anyNativeWindowHasKeyboardFocus() +{ + auto* desktop = Desktop::getInstanceWithoutCreating(); + + if (desktop == nullptr) + return false; + + for (const auto& [userdata, nativeComponent] : desktop->nativeComponents) + { + ignoreUnused (userdata); + + if (auto* sdlNativeComponent = dynamic_cast (nativeComponent)) + { + if (sdlNativeComponent->hasNativeKeyboardFocus()) + return true; + } + } + + return false; +} + +bool SDL2ComponentNative::anyNativeWindowContains (Point screenPosition) +{ + auto* desktop = Desktop::getInstanceWithoutCreating(); + + if (desktop == nullptr) + return false; + + for (const auto& [userdata, nativeComponent] : desktop->nativeComponents) + { + ignoreUnused (userdata); + + if (nativeComponent != nullptr + && nativeComponent->isVisible() + && nativeComponent->getBounds().to().contains (screenPosition)) + { + return true; + } + } + + return false; +} + +//============================================================================== + +void SDL2ComponentNative::updateMouseCapture (bool shouldBeActive) +{ + if (! shouldCaptureMouse) + shouldBeActive = false; + + if (shouldBeActive == mouseCaptureActive) + return; + + if (shouldBeActive) + { + mouseCaptureActive = requestMouseCapture(); + return; + } + + releaseMouseCapture(); + mouseCaptureActive = false; +} + +void SDL2ComponentNative::pollCapturedMouseState() +{ + if (mouseCaptureRequestCount <= 0 || SDL_WasInit (SDL_INIT_VIDEO) == 0) + return; + + int x = 0; + int y = 0; + const auto currentButtons = SDL_GetGlobalMouseState (&x, &y); + const auto hadButtonsDown = lastCapturedMouseButtonState != 0; + const auto hasButtonsDown = currentButtons != 0; + + lastCapturedMouseButtonState = currentButtons; + + if (hadButtonsDown || ! hasButtonsDown) + return; + + if (anyNativeWindowContains ({ static_cast (x), static_cast (y) })) + return; + + MessageManager::callAsync ([] + { + PopupMenu::dismissAllPopups(); + }); +} + +bool SDL2ComponentNative::requestMouseCapture() +{ + if (SDL_WasInit (SDL_INIT_VIDEO) == 0) + return false; + + const bool shouldEnableCapture = mouseCaptureRequestCount == 0; + + if (shouldEnableCapture && SDL_CaptureMouse (SDL_TRUE) != 0) + return false; + + ++mouseCaptureRequestCount; + lastCapturedMouseButtonState = SDL_GetGlobalMouseState (nullptr, nullptr); + + if (shouldEnableCapture) + YUP_MODULE_DBG (GUI_WINDOWING, "Enabled SDL Mouse Capture"); + + return true; +} + +void SDL2ComponentNative::releaseMouseCapture() +{ + if (mouseCaptureRequestCount <= 0) + return; + + --mouseCaptureRequestCount; + + if (mouseCaptureRequestCount == 0 && SDL_WasInit (SDL_INIT_VIDEO) != 0) + { + SDL_CaptureMouse (SDL_FALSE); + lastCapturedMouseButtonState = 0; + + YUP_MODULE_DBG (GUI_WINDOWING, "Disabled SDL Mouse Capture"); + } +} + +//============================================================================== + ComponentNative::Ptr ComponentNative::createFor (Component& component, const Options& options, void* parent) @@ -1738,30 +2016,59 @@ void Desktop::setCurrentMouseLocation (const Point& location) //============================================================================== -void initialiseYup_Windowing() +YUP_API void YUP_CALLTYPE initialiseYup_Windowing() { + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: initialising windowing"); + + SDL_version compiledVersion; + SDL_VERSION (&compiledVersion); + + SDL_version linkedVersion; + SDL_GetVersion (&linkedVersion); + + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: compiled version=" << getSDLVersionString (compiledVersion) << ", linked version=" << getSDLVersionString (linkedVersion)); + // Do not install signal handlers SDL_SetHint (SDL_HINT_NO_SIGNAL_HANDLERS, "1"); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: disabled SDL signal handlers"); // Initialise SDL SDL_SetMainReady(); - if (SDL_Init (SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: SDL main marked ready"); + + const auto alreadyInitialised = SDL_WasInit (sdlDefaultSubsystems); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: requested subsystems=" << String::toHexString (static_cast (sdlDefaultSubsystems)) << ", already initialised=" << String::toHexString (static_cast (alreadyInitialised))); + + if ((alreadyInitialised & sdlDefaultSubsystems) != sdlDefaultSubsystems) { - YUP_DBG ("Error initialising SDL: " << SDL_GetError()); + if (SDL_InitSubSystem (sdlDefaultSubsystems) != 0) + { + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: error initialising SDL: " << SDL_GetError()); - jassertfalse; - YUPApplicationBase::quit(); + jassertfalse; + YUPApplicationBase::quit(); - return; + return; + } + + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: SDL subsystems initialised"); + } + else + { + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: SDL subsystems were already initialised"); } // Update available displays Desktop::getInstance()->updateScreens(); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: updated screens: displays=" << SDL_GetNumVideoDisplays()); + SDL_AddEventWatch (displayEventDispatcher, Desktop::getInstance()); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: registered display event watch"); // Set the default theme now in all platforms except ios #if ! YUP_IOS ApplicationTheme::setGlobalTheme (createThemeVersion1()); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: registered default theme"); #endif // Inject the event loop @@ -1793,33 +2100,75 @@ void initialiseYup_Windowing() { const MessageManagerLock mmLock; ApplicationTheme::setGlobalTheme (createThemeVersion1()); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: registered default theme"); } #endif SDL2ComponentNative::isInitialised.test_and_set(); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: windowing initialised"); } -void shutdownYup_Windowing() +YUP_API void YUP_CALLTYPE shutdownYup_Windowing() { + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: shutting down windowing"); + SDL2ComponentNative::isInitialised.clear(); // Shutdown desktop - SDL_DelEventWatch (displayEventDispatcher, Desktop::getInstance()); if (auto desktop = Desktop::getInstanceWithoutCreating()) + { + SDL_DelEventWatch (displayEventDispatcher, desktop); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: unregistered display event watch"); + desktop->deleteInstance(); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: deleted desktop instance"); + } + + auto messageManager = MessageManager::getInstanceWithoutCreating(); // Unregister theme + if (messageManager == nullptr) + { + ApplicationTheme::setGlobalTheme (nullptr); + } + else { const MessageManagerLock mmLock; ApplicationTheme::setGlobalTheme (nullptr); } + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: unregistered default theme"); + // Unregister event loop - if (auto messageManager = MessageManager::getInstanceWithoutCreating()) + if (messageManager != nullptr) + { messageManager->registerEventLoopCallback (nullptr); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: unregistered event loop callback"); + } + + // Quit only the subsystems YUP initialised. + SDL_QuitSubSystem (sdlDefaultSubsystems); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: SDL subsystems quit"); + +#if YUP_STANDALONE_APPLICATION + std::atexit (&SDL_Quit); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: registered SDL_Quit at exit"); +#endif +} - // Quit SDL - SDL_Quit(); +//============================================================================== +std::atomic_int ScopedYupInitialiser_Windowing::numScopedInitInstances = 0; + +ScopedYupInitialiser_Windowing::ScopedYupInitialiser_Windowing() +{ + if (numScopedInitInstances.fetch_add (1) == 0) + initialiseYup_Windowing(); +} + +ScopedYupInitialiser_Windowing::~ScopedYupInitialiser_Windowing() +{ + if (numScopedInitInstances.fetch_add (-1) == 1) + shutdownYup_Windowing(); } } // namespace yup diff --git a/modules/yup_gui/native/yup_Windowing_sdl2.h b/modules/yup_gui/native/yup_Windowing_sdl2.h index f0434ed25..7094b998e 100644 --- a/modules/yup_gui/native/yup_Windowing_sdl2.h +++ b/modules/yup_gui/native/yup_Windowing_sdl2.h @@ -152,6 +152,19 @@ class SDL2ComponentNative final static std::atomic_flag isInitialised; private: + static bool requestMouseCapture(); + static void releaseMouseCapture(); + static int mouseCaptureRequestCount; + static uint32_t lastCapturedMouseButtonState; + static bool popupDismissalCheckPending; + + void updateMouseCapture (bool shouldBeActive); + static void pollCapturedMouseState(); + static void triggerPopupDismissalCheck(); + static void dismissPopupsIfNoNativeWindowHasFocus(); + static bool anyNativeWindowHasKeyboardFocus(); + static bool anyNativeWindowContains (Point screenPosition); + Component* findComponentForMouseEvent (const Point& position); void updateComponentUnderMouse (const MouseEvent& event); void renderContext(); @@ -184,6 +197,7 @@ class SDL2ComponentNative final WeakReference lastComponentClicked; WeakReference lastComponentFocused; WeakReference lastComponentUnderMouse; + WeakReference currentTextInputComponent; HashMap keyState; MouseEvent::Buttons currentMouseButtons = MouseEvent::noButtons; @@ -209,8 +223,8 @@ class SDL2ComponentNative final bool renderAtomicMode = false; bool renderWireframe = false; bool updateOnlyWhenFocused = false; - - WeakReference currentTextInputComponent; + bool shouldCaptureMouse = false; + bool mouseCaptureActive = false; }; } // namespace yup diff --git a/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp b/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp index 09c279ac6..a33618383 100644 --- a/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp +++ b/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp @@ -48,12 +48,12 @@ struct SliderColors SliderColors getSliderColors (const ApplicationTheme& theme, const Slider& slider) { SliderColors colors; - colors.background = slider.findColor (Slider::Style::backgroundColorId).value_or (Color (0xff3d3d3d)); - colors.track = slider.findColor (Slider::Style::trackColorId).value_or (Color (0xff636363)); - colors.thumb = slider.findColor (Slider::Style::thumbColorId).value_or (Color (0xff4ebfff)); - colors.thumbOver = slider.findColor (Slider::Style::thumbOverColorId).value_or (colors.thumb.brighter (0.3f)); - colors.thumbDown = slider.findColor (Slider::Style::thumbDownColorId).value_or (colors.thumb.darker (0.2f)); - colors.text = slider.findColor (Slider::Style::textColorId).value_or (Colors::white); + colors.background = theme.findColor (slider, Slider::Style::backgroundColorId).value_or (Color (0xff3d3d3d)); + colors.track = theme.findColor (slider, Slider::Style::trackColorId).value_or (Color (0xff636363)); + colors.thumb = theme.findColor (slider, Slider::Style::thumbColorId).value_or (Color (0xff4ebfff)); + colors.thumbOver = theme.findColor (slider, Slider::Style::thumbOverColorId).value_or (colors.thumb.brighter (0.3f)); + colors.thumbDown = theme.findColor (slider, Slider::Style::thumbDownColorId).value_or (colors.thumb.darker (0.2f)); + colors.text = theme.findColor (slider, Slider::Style::textColorId).value_or (Colors::white); return colors; } @@ -630,6 +630,9 @@ void paintPopupMenu (Graphics& g, const ApplicationTheme& theme, const PopupMenu ++itemIndex; const auto rect = item->area; + if (rect.isEmpty()) + continue; + // Skip custom components as they render themselves if (item->isCustomComponent()) continue; @@ -685,12 +688,21 @@ void paintPopupMenu (Graphics& g, const ApplicationTheme& theme, const PopupMenu auto textRect = rect.reduced (12.0f, 2.0f); if (anyItemIsTicked) - textRect.setX (textRect.getX() + 8.0f); + textRect = textRect.withTrimmedLeft (8.0f); + + if (item->shortcutKeyText.isNotEmpty()) + textRect = textRect.withTrimmedRight (80.0f); + + if (item->isSubMenu()) + textRect = textRect.withTrimmedRight (24.0f); { auto styledText = yup::StyledText(); { auto modifier = styledText.startUpdate(); + modifier.setMaxSize (textRect.getSize()); + modifier.setOverflow (yup::StyledText::ellipsis); + modifier.setWrap (yup::StyledText::noWrap); modifier.appendText (item->text, itemFont.withHeight (14.0f)); } @@ -715,6 +727,9 @@ void paintPopupMenu (Graphics& g, const ApplicationTheme& theme, const PopupMenu auto styledText = yup::StyledText(); { auto modifier = styledText.startUpdate(); + modifier.setMaxSize (shortcutRect.getSize()); + modifier.setOverflow (yup::StyledText::ellipsis); + modifier.setWrap (yup::StyledText::noWrap); modifier.setHorizontalAlign (yup::StyledText::right); modifier.appendText (item->shortcutKeyText, itemFont.withHeight (13.0f)); } @@ -1210,7 +1225,7 @@ void paintMidiKeyboard (Graphics& g, const ApplicationTheme& theme, const MidiKe // Draw keyboard background with subtle gradient shadow auto keyboardWidth = keyboard.getKeyStartRange().getEnd(); - auto shadowColor = ApplicationTheme::findColor (MidiKeyboardComponent::Style::whiteKeyShadowColorId).value_or (Color()); + auto shadowColor = theme.findColor (keyboard, MidiKeyboardComponent::Style::whiteKeyShadowColorId).value_or (Color()); if (! shadowColor.isTransparent()) { @@ -1224,7 +1239,7 @@ void paintMidiKeyboard (Graphics& g, const ApplicationTheme& theme, const MidiKe } // Draw separator line at bottom - auto lineColor = ApplicationTheme::findColor (MidiKeyboardComponent::Style::keyOutlineColorId).value_or (Color()); + auto lineColor = theme.findColor (keyboard, MidiKeyboardComponent::Style::keyOutlineColorId).value_or (Color()); if (! lineColor.isTransparent()) { g.setFillColor (lineColor); @@ -1245,9 +1260,9 @@ void paintMidiKeyboard (Graphics& g, const ApplicationTheme& theme, const MidiKe auto isOver = keyboard.isMouseOverNote (note); // Base colors from theme - auto whiteKeyColor = ApplicationTheme::findColor (MidiKeyboardComponent::Style::whiteKeyColorId).value_or (Color()); - auto pressedColor = ApplicationTheme::findColor (MidiKeyboardComponent::Style::whiteKeyPressedColorId).value_or (Color()); - auto outlineColor = ApplicationTheme::findColor (MidiKeyboardComponent::Style::keyOutlineColorId).value_or (Color()); + auto whiteKeyColor = theme.findColor (keyboard, MidiKeyboardComponent::Style::whiteKeyColorId).value_or (Color()); + auto pressedColor = theme.findColor (keyboard, MidiKeyboardComponent::Style::whiteKeyPressedColorId).value_or (Color()); + auto outlineColor = theme.findColor (keyboard, MidiKeyboardComponent::Style::keyOutlineColorId).value_or (Color()); // Determine fill color based on state Color fillColor = whiteKeyColor; @@ -1338,8 +1353,8 @@ void paintMidiKeyboard (Graphics& g, const ApplicationTheme& theme, const MidiKe auto isOver = keyboard.isMouseOverNote (note); // Base colors from theme - auto blackKeyColor = ApplicationTheme::findColor (MidiKeyboardComponent::Style::blackKeyColorId).value_or (Color()); - auto blackPressedColor = ApplicationTheme::findColor (MidiKeyboardComponent::Style::blackKeyPressedColorId).value_or (Color()); + auto blackKeyColor = theme.findColor (keyboard, MidiKeyboardComponent::Style::blackKeyColorId).value_or (Color()); + auto blackPressedColor = theme.findColor (keyboard, MidiKeyboardComponent::Style::blackKeyPressedColorId).value_or (Color()); // Determine fill color based on state Color fillColor = blackKeyColor; @@ -1385,14 +1400,14 @@ void paintKMeter (Graphics& g, const ApplicationTheme& theme, const KMeterCompon return; // Get colors from theme - const auto backgroundColor = meter.findColor (KMeterComponent::Style::backgroundColorId).value_or (Color (0xff1a1a1a)); - const auto greenColor = meter.findColor (KMeterComponent::Style::greenZoneColorId).value_or (Color (0xff00cc00)); - const auto amberColor = meter.findColor (KMeterComponent::Style::amberZoneColorId).value_or (Color (0xffffaa00)); - const auto redColor = meter.findColor (KMeterComponent::Style::redZoneColorId).value_or (Color (0xffcc0000)); - const auto averageColor = meter.findColor (KMeterComponent::Style::averageLevelColorId).value_or (Color (0xccffffff)); - const auto peakColor = meter.findColor (KMeterComponent::Style::peakLevelColorId).value_or (Color (0xffffffff)); - const auto peakClipColor = meter.findColor (KMeterComponent::Style::peakLevelClipColorId).value_or (Color (0xffff0000)); - const auto peakHoldColor = meter.findColor (KMeterComponent::Style::peakHoldColorId).value_or (Color (0xffffff00)); + const auto backgroundColor = theme.findColor (meter, KMeterComponent::Style::backgroundColorId).value_or (Color (0xff1a1a1a)); + const auto greenColor = theme.findColor (meter, KMeterComponent::Style::greenZoneColorId).value_or (Color (0xff00cc00)); + const auto amberColor = theme.findColor (meter, KMeterComponent::Style::amberZoneColorId).value_or (Color (0xffffaa00)); + const auto redColor = theme.findColor (meter, KMeterComponent::Style::redZoneColorId).value_or (Color (0xffcc0000)); + const auto averageColor = theme.findColor (meter, KMeterComponent::Style::averageLevelColorId).value_or (Color (0xccffffff)); + const auto peakColor = theme.findColor (meter, KMeterComponent::Style::peakLevelColorId).value_or (Color (0xffffffff)); + const auto peakClipColor = theme.findColor (meter, KMeterComponent::Style::peakLevelClipColorId).value_or (Color (0xffff0000)); + const auto peakHoldColor = theme.findColor (meter, KMeterComponent::Style::peakHoldColorId).value_or (Color (0xffffff00)); // Draw background with subtle depth { diff --git a/modules/yup_gui/themes/yup_ApplicationTheme.cpp b/modules/yup_gui/themes/yup_ApplicationTheme.cpp index 27519dd5a..df2b307a6 100644 --- a/modules/yup_gui/themes/yup_ApplicationTheme.cpp +++ b/modules/yup_gui/themes/yup_ApplicationTheme.cpp @@ -48,13 +48,19 @@ ApplicationTheme::Ptr& ApplicationTheme::getGlobalThemeInstance() //============================================================================== -std::optional ApplicationTheme::findColor (const Identifier& colorId) +std::optional ApplicationTheme::findComponentColor (const Component& component, const Identifier& colorId) { jassert (getGlobalThemeInstance() != nullptr); - const auto& colors = getGlobalThemeInstance()->defaultColors; + return getGlobalThemeInstance()->findColor (component, colorId); +} + +std::optional ApplicationTheme::findColor (const Component& component, const Identifier& colorId) const +{ + if (auto color = component.findColor (colorId)) + return color; - if (auto it = colors.find (colorId); it != colors.end()) + if (auto it = defaultColors.find (colorId); it != defaultColors.end()) return it->second; return std::nullopt; @@ -73,13 +79,19 @@ void ApplicationTheme::setColors (std::initializer_list ApplicationTheme::findMetric (const Identifier& metricId) +std::optional ApplicationTheme::findComponentMetric (const Component& component, const Identifier& metricId) { jassert (getGlobalThemeInstance() != nullptr); - const auto& metrics = getGlobalThemeInstance()->defaultMetrics; + return getGlobalThemeInstance()->findMetric (component, metricId); +} - if (auto it = metrics.find (metricId); it != metrics.end()) +std::optional ApplicationTheme::findMetric (const Component& component, const Identifier& metricId) const +{ + if (auto metric = component.findMetric (metricId)) + return metric; + + if (auto it = defaultMetrics.find (metricId); it != defaultMetrics.end()) return it->second; return std::nullopt; @@ -90,6 +102,12 @@ void ApplicationTheme::setMetric (const Identifier& metricId, float value) defaultMetrics.insert_or_assign (metricId, value); } +void ApplicationTheme::setMetrics (std::initializer_list> metrics) +{ + for (const auto& entry : metrics) + defaultMetrics.insert_or_assign (entry.first, entry.second); +} + //============================================================================== void ApplicationTheme::setDefaultFont (Font font) diff --git a/modules/yup_gui/themes/yup_ApplicationTheme.h b/modules/yup_gui/themes/yup_ApplicationTheme.h index 75605c6c7..3d22c1d29 100644 --- a/modules/yup_gui/themes/yup_ApplicationTheme.h +++ b/modules/yup_gui/themes/yup_ApplicationTheme.h @@ -45,6 +45,7 @@ class YUP_API ApplicationTheme final : public ReferenceCountedObject /** Constructs an ApplicationTheme object. */ ApplicationTheme(); + /** Destructor for the ApplicationTheme object. */ ~ApplicationTheme(); //============================================================================== @@ -120,19 +121,37 @@ class YUP_API ApplicationTheme final : public ReferenceCountedObject //============================================================================== /** Returns a color from the global theme. - + + This method looks for the color in the component's properties first, then in the global theme. If no color + is found, it returns std::nullopt. + + @param component The component for which to find the color. + @param colorId The identifier for the color to retrieve. + + @return The color associated with the given identifier, or std::nullopt if not found. + */ + static std::optional findComponentColor (const Component& component, const Identifier& colorId); + + /** Returns a color from this theme. + + This method looks for the color in the component's properties first, then in this theme. If no color + is found, it returns std::nullopt. + + @param component The component for which to find the color. @param colorId The identifier for the color to retrieve. + + @return The color associated with the given identifier, or std::nullopt if not found. */ - static std::optional findColor (const Identifier& colorId); + std::optional findColor (const Component& component, const Identifier& colorId) const; - /** Sets a color in the global theme. + /** Sets a color in this theme. @param colorId The identifier for the color to set. @param color The color to set. */ void setColor (const Identifier& colorId, const Color& color); - /** Sets multiple colors in the global theme. + /** Sets multiple colors in this theme. @param colors An initializer list of color identifier and color pairs. */ @@ -141,9 +160,27 @@ class YUP_API ApplicationTheme final : public ReferenceCountedObject //============================================================================== /** Returns a named float metric from the global theme. - Returns @p defaultValue when the metric has not been registered. + This method looks for the metric in the component's properties first, then in the global theme. If no metric + is found, it returns std::nullopt. + + @param component The component for which to find the metric. + @param metricId The identifier for the metric to retrieve. + + @return The metric associated with the given identifier, or std::nullopt if not found. + */ + static std::optional findComponentMetric (const Component& component, const Identifier& metricId); + + /** Returns a named float metric from this theme. + + This method looks for the metric in the component's properties first, then in this theme. If no metric + is found, it returns std::nullopt. + + @param component The component for which to find the metric. + @param metricId The identifier for the metric to retrieve. + + @return The metric associated with the given identifier, or std::nullopt if not found. */ - static std::optional findMetric (const Identifier& metricId); + std::optional findMetric (const Component& component, const Identifier& metricId) const; /** Registers a named float metric in this theme. @@ -152,6 +189,12 @@ class YUP_API ApplicationTheme final : public ReferenceCountedObject */ void setMetric (const Identifier& metricId, float value); + /** Sets multiple metrics in the global theme. + + @param metrics An initializer list of metric identifier and value pairs. + */ + void setMetrics (std::initializer_list> metrics); + //============================================================================== /** Sets the default text font for the application theme. diff --git a/modules/yup_gui/widgets/yup_Label.cpp b/modules/yup_gui/widgets/yup_Label.cpp index 046343567..1dce35598 100644 --- a/modules/yup_gui/widgets/yup_Label.cpp +++ b/modules/yup_gui/widgets/yup_Label.cpp @@ -28,6 +28,7 @@ const Identifier Label::Style::textFillColorId { "Label_textFillColorId" }; const Identifier Label::Style::textStrokeColorId { "Label_textStrokeColorId" }; const Identifier Label::Style::backgroundColorId { "Label_backgroundColorId" }; const Identifier Label::Style::outlineColorId { "Label_outlineColorId" }; +const Identifier Label::Style::textHeightProportionMetricId { "Label_textHeightProportionMetricId" }; //============================================================================== diff --git a/modules/yup_gui/widgets/yup_Label.h b/modules/yup_gui/widgets/yup_Label.h index 2bd327565..e17f6260c 100644 --- a/modules/yup_gui/widgets/yup_Label.h +++ b/modules/yup_gui/widgets/yup_Label.h @@ -93,10 +93,14 @@ class YUP_API Label : public Component struct Style { + //! Colors static const Identifier textFillColorId; static const Identifier textStrokeColorId; static const Identifier backgroundColorId; static const Identifier outlineColorId; + + //! Metrics + static const Identifier textHeightProportionMetricId; }; //============================================================================== diff --git a/modules/yup_gui/widgets/yup_Slider.cpp b/modules/yup_gui/widgets/yup_Slider.cpp index 0db39fae4..5aed078cd 100644 --- a/modules/yup_gui/widgets/yup_Slider.cpp +++ b/modules/yup_gui/widgets/yup_Slider.cpp @@ -373,6 +373,9 @@ void Slider::mouseDown (const MouseEvent& event) if (dragMode != notDragging) { + if (onDragStart) + onDragStart (event); + // For linear sliders, implement improved click behavior bool shouldJumpToClickPosition = false; @@ -427,9 +430,6 @@ void Slider::mouseDown (const MouseEvent& event) valueOnMouseDown = currentValue; minValueOnMouseDown = minValue; maxValueOnMouseDown = maxValue; - - if (onDragStart) - onDragStart (event); } takeKeyboardFocus(); diff --git a/modules/yup_gui/yup_gui.h b/modules/yup_gui/yup_gui.h index f5ff40e61..df59d7964 100644 --- a/modules/yup_gui/yup_gui.h +++ b/modules/yup_gui/yup_gui.h @@ -61,12 +61,12 @@ #endif //============================================================================== -/** Config: YUP_ENABLE_WINDOWING_EVENT_LOGGING +/** Config: YUP_ENABLE_GUI_WINDOWING_LOGGING Enable logging of windowing events like movement, resizes, mouse interactions. */ -#ifndef YUP_ENABLE_WINDOWING_EVENT_LOGGING -#define YUP_ENABLE_WINDOWING_EVENT_LOGGING 0 +#ifndef YUP_ENABLE_GUI_WINDOWING_LOGGING +#define YUP_ENABLE_GUI_WINDOWING_LOGGING 1 #endif //============================================================================== diff --git a/modules/yup_python/bindings/yup_YupGui_bindings.cpp b/modules/yup_python/bindings/yup_YupGui_bindings.cpp index 87d03bd0c..5120a1d71 100644 --- a/modules/yup_python/bindings/yup_YupGui_bindings.cpp +++ b/modules/yup_python/bindings/yup_YupGui_bindings.cpp @@ -193,6 +193,7 @@ void registerYupGuiBindings (py::module_& m) .def ("withResizableWindow", &ComponentNative::Options::withResizableWindow) .def ("withRenderContinuous", &ComponentNative::Options::withRenderContinuous) .def ("withAllowedHighDensityDisplay", &ComponentNative::Options::withAllowedHighDensityDisplay) + .def ("withMouseCapture", &ComponentNative::Options::withMouseCapture) //.def ("withGraphicsApi", &ComponentNative::Options::withGraphicsApi) .def ("withFramerateRedraw", &ComponentNative::Options::withFramerateRedraw) .def ("withClearColor", &ComponentNative::Options::withClearColor) @@ -393,9 +394,6 @@ void registerYupGuiBindings (py::module_& m) .def ("setColor", &Component::setColor) .def ("getColor", &Component::getColor) .def ("findColor", &Component::findColor) - .def ("setStyleProperty", &Component::setStyleProperty) - .def ("getStyleProperty", &Component::getStyleProperty) - .def ("findStyleProperty", &Component::findStyleProperty) ; // ============================================================================================ yup::DocumentWindow diff --git a/tests/yup_audio_graph/yup_AudioGraphProcessor.cpp b/tests/yup_audio_graph/yup_AudioGraphProcessor.cpp index 3f2d83620..2a042744b 100644 --- a/tests/yup_audio_graph/yup_AudioGraphProcessor.cpp +++ b/tests/yup_audio_graph/yup_AudioGraphProcessor.cpp @@ -31,6 +31,7 @@ using namespace yup; namespace { + AudioBusLayout stereoLayout() { return AudioBusLayout ({ AudioBus ("Input", AudioBus::Type::Audio, AudioBus::Direction::Input, 2) }, @@ -68,8 +69,9 @@ class TestProcessor : public AudioProcessor void releaseResources() override { prepared = false; } - void processBlock (AudioBuffer& audioBuffer, MidiBuffer&) override + void processBlock (AudioProcessContext& context) override { + auto& audioBuffer = context.audio; for (int channel = 0; channel < audioBuffer.getNumChannels(); ++channel) audioBuffer.applyGain (channel, 0, audioBuffer.getNumSamples(), gain); } @@ -114,7 +116,7 @@ class MidiPassthroughProcessor : public AudioProcessor void releaseResources() override {} - void processBlock (AudioBuffer&, MidiBuffer&) override {} + void processBlock (AudioProcessContext&) override {} int getLatencySamples() override { return latency; } @@ -167,8 +169,10 @@ class MidiDelayingProcessor : public AudioProcessor nextPendingMidi.clear(); } - void processBlock (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) override + void processBlock (AudioProcessContext& context) override { + auto& audioBuffer = context.audio; + auto& midiBuffer = context.midi; const int numSamples = audioBuffer.getNumSamples(); const MidiBuffer inputMidi = midiBuffer; @@ -245,7 +249,7 @@ class MonoLayoutProcessor : public AudioProcessor void releaseResources() override {} - void processBlock (AudioBuffer&, MidiBuffer&) override {} + void processBlock (AudioProcessContext&) override {} int getCurrentPreset() const noexcept override { return 0; } @@ -275,8 +279,9 @@ class DelayingProcessor : public TestProcessor history.clear(); } - void processBlock (AudioBuffer& audioBuffer, MidiBuffer&) override + void processBlock (AudioProcessContext& context) override { + auto& audioBuffer = context.audio; const int ringSize = history.getNumSamples(); for (int sample = 0; sample < audioBuffer.getNumSamples(); ++sample) @@ -364,8 +369,9 @@ class StatefulGainProcessor : public AudioProcessor void releaseResources() override {} - void processBlock (AudioBuffer& audioBuffer, MidiBuffer&) override + void processBlock (AudioProcessContext& context) override { + auto& audioBuffer = context.audio; for (int channel = 0; channel < audioBuffer.getNumChannels(); ++channel) audioBuffer.applyGain (channel, 0, audioBuffer.getNumSamples(), gain); } @@ -440,8 +446,9 @@ class StatefulExternalProcessor : public AudioProcessor void releaseResources() override {} - void processBlock (AudioBuffer& audioBuffer, MidiBuffer&) override + void processBlock (AudioProcessContext& context) override { + auto& audioBuffer = context.audio; for (int channel = 0; channel < audioBuffer.getNumChannels(); ++channel) audioBuffer.applyGain (channel, 0, audioBuffer.getNumSamples(), gain); } @@ -492,7 +499,7 @@ class SaveFailingProcessor : public AudioProcessor void releaseResources() override {} - void processBlock (AudioBuffer&, MidiBuffer&) override {} + void processBlock (AudioProcessContext&) override {} int getCurrentPreset() const noexcept override { return 0; } @@ -663,8 +670,9 @@ class DenormalCheckProcessor : public AudioProcessor void releaseResources() override {} - void processBlock (AudioBuffer& audioBuffer, MidiBuffer&) override + void processBlock (AudioProcessContext& context) override { + auto& audioBuffer = context.audio; for (int channel = 0; channel < audioBuffer.getNumChannels(); ++channel) audioBuffer.applyGain (channel, 0, audioBuffer.getNumSamples(), 1.0f); @@ -712,8 +720,9 @@ class MixedProcessor : public AudioProcessor void releaseResources() override {} - void processBlock (AudioBuffer& audioBuffer, MidiBuffer&) override + void processBlock (AudioProcessContext& context) override { + auto& audioBuffer = context.audio; for (int channel = 0; channel < audioBuffer.getNumChannels(); ++channel) audioBuffer.applyGain (channel, 0, audioBuffer.getNumSamples(), gain); } @@ -766,8 +775,9 @@ class MixedDelayingProcessor : public AudioProcessor writePosition = 0; } - void processBlock (AudioBuffer& audioBuffer, MidiBuffer&) override + void processBlock (AudioProcessContext& context) override { + auto& audioBuffer = context.audio; const int ringSize = history.getNumSamples(); for (int sample = 0; sample < audioBuffer.getNumSamples(); ++sample) @@ -871,9 +881,12 @@ TEST (AudioGraphProcessorTests, ProcessesSerialAudioChain) AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; + fillImpulse (audio); - graph.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); EXPECT_FLOAT_EQ (1.0f, audio.getReadPointer (0)[0]); EXPECT_FLOAT_EQ (1.0f, audio.getReadPointer (1)[0]); @@ -893,11 +906,14 @@ TEST (AudioGraphProcessorTests, ProcessesBlocksLargerThanPreparedMaximumInChunks AudioBuffer audio (2, 40); MidiBuffer midi; + ParameterChangeBuffer params; + for (int channel = 0; channel < audio.getNumChannels(); ++channel) for (int sample = 0; sample < audio.getNumSamples(); ++sample) audio.getWritePointer (channel)[sample] = 1.0f; - graph.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); for (int channel = 0; channel < audio.getNumChannels(); ++channel) for (int sample = 0; sample < audio.getNumSamples(); ++sample) @@ -917,13 +933,15 @@ TEST (AudioGraphProcessorTests, PreservesMidiEventsInBlocksLargerThanPreparedMax AudioBuffer audio (0, 40); MidiBuffer midi; - const uint8 noteOn[] = { 0x90, 60, 100 }; + ParameterChangeBuffer params; + const uint8 noteOn[] = { 0x90, 60, 100 }; midi.addEvent (noteOn, 3, 7); midi.addEvent (noteOn, 3, 17); midi.addEvent (noteOn, 3, 35); - graph.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); EXPECT_EQ (1, countMidiEventsAt (midi, 7)); EXPECT_EQ (1, countMidiEventsAt (midi, 17)); @@ -946,9 +964,12 @@ TEST (AudioGraphProcessorTests, MixesFanIn) AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; + fillImpulse (audio); - graph.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); EXPECT_FLOAT_EQ (1.0f, audio.getReadPointer (0)[0]); EXPECT_FLOAT_EQ (1.0f, audio.getReadPointer (1)[0]); @@ -968,10 +989,13 @@ TEST (AudioGraphProcessorTests, PreservesMidiTimestamps) AudioBuffer audio (0, 32); MidiBuffer midi; + ParameterChangeBuffer params; + const uint8 noteOn[] = { 0x90, 60, 100 }; midi.addEvent (noteOn, 3, 7); - graph.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); EXPECT_EQ (1, countMidiEventsAt (midi, 7)); } @@ -991,9 +1015,12 @@ TEST (AudioGraphProcessorTests, CompensatesShorterParallelPaths) AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; + fillImpulse (audio); - graph.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[0]); EXPECT_FLOAT_EQ (2.0f, audio.getReadPointer (0)[4]); @@ -1145,9 +1172,12 @@ TEST (AudioGraphProcessorTests, MissingCommitKeepsPreviousPlan) AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; + fillImpulse (audio); - graph.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (0)[0]); } @@ -1190,11 +1220,6 @@ TEST (AudioGraphProcessorTests, ExternalTopologyEditsDriveDirtyRevision) ASSERT_NE (nullptr, processor); processor->setLatencySamplesForTest (16); - EXPECT_TRUE (graph.hasUncommittedChanges()); - EXPECT_EQ (0, graph.getLatencySamples()); - EXPECT_EQ (latencyQueriesAfterCommit, latencyQueryCount.load()); - - EXPECT_TRUE (graph.commitChanges().wasOk()); EXPECT_FALSE (graph.hasUncommittedChanges()); EXPECT_EQ (16, graph.getLatencySamples()); EXPECT_GT (latencyQueryCount.load(), latencyQueriesAfterCommit); @@ -1302,9 +1327,12 @@ TEST (AudioGraphProcessorTests, SaveAndLoadRestoresConnectionsAndNodeState) AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; + fillImpulse (audio); - graph.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); EXPECT_FLOAT_EQ (0.25f, audio.getReadPointer (0)[0]); EXPECT_FLOAT_EQ (0.25f, audio.getReadPointer (1)[0]); @@ -1338,9 +1366,12 @@ TEST (AudioGraphProcessorTests, LoadStateRecreatesProcessorNodesWithFactory) AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; + fillImpulse (audio); - destination.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + destination.processBlock (ctx); EXPECT_FLOAT_EQ (0.25f, audio.getReadPointer (0)[0]); EXPECT_FLOAT_EQ (0.25f, audio.getReadPointer (1)[0]); @@ -1376,9 +1407,12 @@ TEST (AudioGraphProcessorTests, CreateXmlAndRestoreFromXmlCanBeUsedDirectly) AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; + fillImpulse (audio); - destination.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + destination.processBlock (ctx); EXPECT_FLOAT_EQ (0.25f, audio.getReadPointer (0)[0]); EXPECT_FLOAT_EQ (0.25f, audio.getReadPointer (1)[0]); @@ -1450,9 +1484,12 @@ TEST (AudioGraphProcessorTests, LoadStateRecreatesExternalNodesWithXmlCreationDa AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; + fillImpulse (audio); - destination.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + destination.processBlock (ctx); EXPECT_FLOAT_EQ (0.25f, audio.getReadPointer (0)[0]); EXPECT_FLOAT_EQ (0.25f, audio.getReadPointer (1)[0]); @@ -1482,9 +1519,12 @@ TEST (AudioGraphProcessorTests, LoadStateFailureRestoresPreviousGraphModel) AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; + fillImpulse (audio); - destination.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + destination.processBlock (ctx); EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (0)[0]); EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (1)[0]); @@ -1593,9 +1633,12 @@ TEST (AudioGraphProcessorTests, LoadStateRestoresMultiNodeXmlGraphAndNextNodeID) AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; + fillImpulse (audio); - destination.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + destination.processBlock (ctx); EXPECT_FLOAT_EQ (0.125f, audio.getReadPointer (0)[0]); EXPECT_FLOAT_EQ (0.125f, audio.getReadPointer (1)[0]); @@ -1792,9 +1835,12 @@ TEST (AudioGraphProcessorTests, LoadStateFailureFromNodeStateCallbackRestoresPre AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; + fillImpulse (audio); - destination.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + destination.processBlock (ctx); EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (0)[0]); EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (1)[0]); @@ -1818,9 +1864,12 @@ TEST (AudioGraphProcessorTests, RemoveConnectionStopsRoutingAfterCommit) AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; + fillImpulse (audio); - graph.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[0]); EXPECT_FALSE (model->removeConnection (outputConnection)); @@ -1840,9 +1889,12 @@ TEST (AudioGraphProcessorTests, RemoveNodePrunesConnections) AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; + fillImpulse (audio); - graph.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[0]); EXPECT_FALSE (model->removeNode (node)); @@ -1865,9 +1917,12 @@ TEST (AudioGraphProcessorTests, ClearRemovesAllRouting) AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; + fillImpulse (audio); - graph.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[0]); } @@ -1939,11 +1994,16 @@ TEST (AudioGraphProcessorTests, WorkerThreadOutputMatchesSingleThreadOutput) AudioBuffer threadedAudio (2, 16); MidiBuffer singleMidi; MidiBuffer threadedMidi; + ParameterChangeBuffer singleParams; + ParameterChangeBuffer threadedParams; fillImpulse (singleAudio); fillImpulse (threadedAudio); - singleThreaded.processBlock (singleAudio, singleMidi); - threaded.processBlock (threadedAudio, threadedMidi); + AudioProcessContext singleCtx { singleAudio, singleMidi, singleParams }; + singleThreaded.processBlock (singleCtx); + + AudioProcessContext threadedCtx { threadedAudio, threadedMidi, threadedParams }; + threaded.processBlock (threadedCtx); for (int channel = 0; channel < 2; ++channel) { @@ -1996,6 +2056,8 @@ TEST (AudioGraphProcessorTests, WorkerThreadsMatchSingleThreadOutputUnderLoad) AudioBuffer threadedAudio (2, numSamples); MidiBuffer singleMidi; MidiBuffer threadedMidi; + ParameterChangeBuffer singleParams; + ParameterChangeBuffer threadedParams; for (int block = 0; block < numBlocks; ++block) { @@ -2014,8 +2076,10 @@ TEST (AudioGraphProcessorTests, WorkerThreadsMatchSingleThreadOutputUnderLoad) } } - singleThreaded.processBlock (singleAudio, singleMidi); - threaded.processBlock (threadedAudio, threadedMidi); + AudioProcessContext singleCtx { singleAudio, singleMidi, singleParams }; + singleThreaded.processBlock (singleCtx); + AudioProcessContext threadedCtx { threadedAudio, threadedMidi, threadedParams }; + threaded.processBlock (threadedCtx); for (int channel = 0; channel < 2; ++channel) { @@ -2070,6 +2134,8 @@ TEST (AudioGraphProcessorTests, SwitchingWorkerThreadsBetweenBlocksPreservesOutp AudioBuffer threadedAudio (2, numSamples); MidiBuffer singleMidi; MidiBuffer threadedMidi; + ParameterChangeBuffer singleParams; + ParameterChangeBuffer threadedParams; for (int block = 0; block < numBlocks; ++block) { @@ -2090,8 +2156,10 @@ TEST (AudioGraphProcessorTests, SwitchingWorkerThreadsBetweenBlocksPreservesOutp } } - singleThreaded.processBlock (singleAudio, singleMidi); - threaded.processBlock (threadedAudio, threadedMidi); + AudioProcessContext singleCtx { singleAudio, singleMidi, singleParams }; + singleThreaded.processBlock (singleCtx); + AudioProcessContext threadedCtx { threadedAudio, threadedMidi, threadedParams }; + threaded.processBlock (threadedCtx); for (int channel = 0; channel < 2; ++channel) { @@ -2124,6 +2192,7 @@ TEST (AudioGraphProcessorTests, ConcurrentCommitsDoNotInvalidateAudioThreadPlan) { AudioBuffer audio (2, 32); MidiBuffer midi; + ParameterChangeBuffer params; while (! startProcessing.load()) std::this_thread::yield(); @@ -2137,7 +2206,8 @@ TEST (AudioGraphProcessorTests, ConcurrentCommitsDoNotInvalidateAudioThreadPlan) for (int sample = 0; sample < audio.getNumSamples(); ++sample) audio.getWritePointer (channel)[sample] = 1.0f; - graph.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); for (int channel = 0; channel < audio.getNumChannels(); ++channel) { @@ -2199,16 +2269,22 @@ TEST (AudioGraphProcessorTests, MidiCompensationCanSpillIntoNextBlock) AudioBuffer audio (0, 8); MidiBuffer midi; + ParameterChangeBuffer params; + const uint8 noteOn[] = { 0x90, 64, 100 }; midi.addEvent (noteOn, 3, 5); - graph.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); EXPECT_EQ (1, countMidiEventsAt (midi, 5)); EXPECT_EQ (1, countMidiEvents (midi)); midi.clear(); - graph.processBlock (audio, midi); + { + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); + } EXPECT_EQ (1, countMidiEventsAt (midi, 3)); } @@ -2230,9 +2306,12 @@ TEST (AudioGraphProcessorTests, PdcAccumulatesSerialPathLatencyAtGraphOutput) AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; + fillImpulse (audio); - graph.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[0]); EXPECT_FLOAT_EQ (2.0f, audio.getReadPointer (0)[7]); @@ -2258,9 +2337,12 @@ TEST (AudioGraphProcessorTests, PdcCompensatesFanInAtProcessorInput) AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; + fillImpulse (audio); - graph.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[0]); EXPECT_FLOAT_EQ (2.0f, audio.getReadPointer (0)[5]); @@ -2281,9 +2363,12 @@ TEST (AudioGraphProcessorTests, PdcDelaysDirectAudioOutputToMatchLatentAudioPath AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; + fillImpulseAt (audio, 3); - graph.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[3]); EXPECT_FLOAT_EQ (2.0f, audio.getReadPointer (0)[8]); @@ -2306,17 +2391,22 @@ TEST (AudioGraphProcessorTests, PdcAudioDelayLongerThanBlockSpillsAcrossBlocks) AudioBuffer audio (2, 4); MidiBuffer midi; + ParameterChangeBuffer params; + fillImpulse (audio); - graph.processBlock (audio, midi); + AudioProcessContext ctx1 { audio, midi, params }; + graph.processBlock (ctx1); EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[0]); audio.clear(); - graph.processBlock (audio, midi); + AudioProcessContext ctx2 { audio, midi, params }; + graph.processBlock (ctx2); EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[0]); audio.clear(); - graph.processBlock (audio, midi); + AudioProcessContext ctx3 { audio, midi, params }; + graph.processBlock (ctx3); EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[1]); EXPECT_FLOAT_EQ (2.0f, audio.getReadPointer (0)[2]); EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[3]); @@ -2335,13 +2425,17 @@ TEST (AudioGraphProcessorTests, PdcDirectAudioOutputCompensationSpillsAcrossBloc AudioBuffer audio (2, 4); MidiBuffer midi; + ParameterChangeBuffer params; + fillImpulseAt (audio, 1); - graph.processBlock (audio, midi); + AudioProcessContext ctx1 { audio, midi, params }; + graph.processBlock (ctx1); EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[1]); audio.clear(); - graph.processBlock (audio, midi); + AudioProcessContext ctx2 { audio, midi, params }; + graph.processBlock (ctx2); EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[2]); EXPECT_FLOAT_EQ (2.0f, audio.getReadPointer (0)[3]); } @@ -2359,17 +2453,22 @@ TEST (AudioGraphProcessorTests, PdcFlushClearsPendingAudioCompensation) AudioBuffer audio (2, 4); MidiBuffer midi; + ParameterChangeBuffer params; + fillImpulse (audio); - graph.processBlock (audio, midi); + AudioProcessContext ctx1 { audio, midi, params }; + graph.processBlock (ctx1); graph.flush(); audio.clear(); - graph.processBlock (audio, midi); + AudioProcessContext ctx2 { audio, midi, params }; + graph.processBlock (ctx2); EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[0]); audio.clear(); - graph.processBlock (audio, midi); + AudioProcessContext ctx3 { audio, midi, params }; + graph.processBlock (ctx3); EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[2]); } @@ -2386,22 +2485,28 @@ TEST (AudioGraphProcessorTests, PdcMidiDelayLongerThanOneBlockAlignsWithDelayedP AudioBuffer audio (0, 8); MidiBuffer midi; + ParameterChangeBuffer params; + const uint8 noteOn[] = { 0x90, 67, 100 }; midi.addEvent (noteOn, 3, 6); - graph.processBlock (audio, midi); + AudioProcessContext ctx1 { audio, midi, params }; + graph.processBlock (ctx1); EXPECT_EQ (0, countMidiEvents (midi)); midi.clear(); - graph.processBlock (audio, midi); + AudioProcessContext ctx2 { audio, midi, params }; + graph.processBlock (ctx2); EXPECT_EQ (0, countMidiEvents (midi)); midi.clear(); - graph.processBlock (audio, midi); + AudioProcessContext ctx3 { audio, midi, params }; + graph.processBlock (ctx3); EXPECT_EQ (0, countMidiEvents (midi)); midi.clear(); - graph.processBlock (audio, midi); + AudioProcessContext ctx4 { audio, midi, params }; + graph.processBlock (ctx4); EXPECT_EQ (2, countMidiEventsAt (midi, 0)); EXPECT_EQ (2, countMidiEvents (midi)); } @@ -2419,20 +2524,25 @@ TEST (AudioGraphProcessorTests, PdcFlushClearsPendingMidiCompensation) AudioBuffer audio (0, 8); MidiBuffer midi; + ParameterChangeBuffer params; + const uint8 noteOn[] = { 0x90, 69, 100 }; midi.addEvent (noteOn, 3, 7); - graph.processBlock (audio, midi); + AudioProcessContext ctx1 { audio, midi, params }; + graph.processBlock (ctx1); EXPECT_EQ (1, countMidiEventsAt (midi, 7)); graph.flush(); midi.clear(); - graph.processBlock (audio, midi); + AudioProcessContext ctx2 { audio, midi, params }; + graph.processBlock (ctx2); EXPECT_EQ (0, countMidiEvents (midi)); midi.clear(); - graph.processBlock (audio, midi); + AudioProcessContext ctx3 { audio, midi, params }; + graph.processBlock (ctx3); EXPECT_EQ (0, countMidiEvents (midi)); } @@ -2456,9 +2566,12 @@ TEST (AudioGraphProcessorTests, PdcRecompileAfterRemovingLatencyPathClearsCompen AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; + fillImpulse (audio); - graph.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); EXPECT_FLOAT_EQ (1.0f, audio.getReadPointer (0)[0]); } @@ -2480,11 +2593,15 @@ TEST (AudioGraphProcessorTests, WorkerThreadsProcessManyBlocksCorrectly) AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; for (int block = 0; block < 1000; ++block) { fillImpulse (audio); - graph.processBlock (audio, midi); + + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); + EXPECT_FLOAT_EQ (1.0f, audio.getReadPointer (0)[0]) << "block " << block; } } @@ -2501,20 +2618,25 @@ TEST (AudioGraphProcessorTests, WorkerThreadCountCanBeChangedAfterProcessing) AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; graph.setNumWorkerThreads (2); fillImpulse (audio); - graph.processBlock (audio, midi); + + AudioProcessContext ctx1 { audio, midi, params }; + graph.processBlock (ctx1); EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (0)[0]); graph.setNumWorkerThreads (0); fillImpulse (audio); - graph.processBlock (audio, midi); + AudioProcessContext ctx2 { audio, midi, params }; + graph.processBlock (ctx2); EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (0)[0]); graph.setNumWorkerThreads (4); fillImpulse (audio); - graph.processBlock (audio, midi); + AudioProcessContext ctx3 { audio, midi, params }; + graph.processBlock (ctx3); EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (0)[0]); graph.setNumWorkerThreads (0); @@ -2533,16 +2655,20 @@ TEST (AudioGraphProcessorTests, WorkerThreadsContinueProcessingAfterIdleReset) AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; fillImpulse (audio); - graph.processBlock (audio, midi); + + AudioProcessContext ctx1 { audio, midi, params }; + graph.processBlock (ctx1); EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (0)[0]); for (int i = 0; i < 100; ++i) std::this_thread::yield(); fillImpulse (audio); - graph.processBlock (audio, midi); + AudioProcessContext ctx2 { audio, midi, params }; + graph.processBlock (ctx2); EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (0)[0]); graph.setNumWorkerThreads (0); @@ -2561,11 +2687,15 @@ TEST (AudioGraphProcessorTests, ZeroWorkerThreadsProcessesCorrectly) AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; for (int block = 0; block < 10; ++block) { fillImpulse (audio); - graph.processBlock (audio, midi); + + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); + EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (0)[0]) << "block " << block; } } @@ -2589,12 +2719,16 @@ TEST (AudioGraphProcessorTests, WorkerThreadOutputMatchesSingleThreadOutputManyB AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; std::vector results; for (int block = 0; block < 100; ++block) { fillImpulse (audio); - graph.processBlock (audio, midi); + + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); + results.push_back (audio.getReadPointer (0)[0]); } @@ -2621,6 +2755,7 @@ TEST (AudioGraphProcessorTests, RapidWorkerThreadResizeDoesNotCrash) AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; const int threadCounts[] = { 0, 1, 2, 4, 2, 1, 0, 3, 0 }; @@ -2628,7 +2763,10 @@ TEST (AudioGraphProcessorTests, RapidWorkerThreadResizeDoesNotCrash) { graph.setNumWorkerThreads (count); fillImpulse (audio); - graph.processBlock (audio, midi); + + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); + EXPECT_FLOAT_EQ (1.0f, audio.getReadPointer (0)[0]); } } @@ -2649,9 +2787,12 @@ TEST (AudioGraphProcessorTests, ProcessBlockDisablesDenormals) AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; + fillImpulse (audio); - graph.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); EXPECT_TRUE (procPtr->denormalsWereDisabled); } @@ -2670,6 +2811,7 @@ TEST (AudioGraphProcessorTests, ManyMidiEventsPassThroughGraphCorrectly) AudioBuffer audio (0, 128); MidiBuffer midi; + ParameterChangeBuffer params; const int numEvents = 64; for (int i = 0; i < numEvents; ++i) @@ -2678,7 +2820,8 @@ TEST (AudioGraphProcessorTests, ManyMidiEventsPassThroughGraphCorrectly) midi.addEvent (noteOn, 3, i % 128); } - graph.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); EXPECT_EQ (numEvents, countMidiEvents (midi)); } @@ -2703,9 +2846,12 @@ TEST (AudioGraphProcessorTests, WorkerThreadsPdcCompensatesParallelPaths) AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; + fillImpulse (audio); - graph.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[0]); EXPECT_FLOAT_EQ (2.0f, audio.getReadPointer (0)[4]); @@ -2747,14 +2893,18 @@ TEST (AudioGraphProcessorTests, WorkerThreadsPdcMatchesSingleThreadOutput) AudioBuffer multiAudio (2, 16); MidiBuffer singleMidi; MidiBuffer multiMidi; + ParameterChangeBuffer singleParams; + ParameterChangeBuffer multiParams; for (int block = 0; block < 8; ++block) { fillImpulse (singleAudio); fillImpulse (multiAudio); - single->processBlock (singleAudio, singleMidi); - multi->processBlock (multiAudio, multiMidi); + AudioProcessContext singleCtx { singleAudio, singleMidi, singleParams }; + single->processBlock (singleCtx); + AudioProcessContext multiCtx { multiAudio, multiMidi, multiParams }; + multi->processBlock (multiCtx); for (int channel = 0; channel < 2; ++channel) { @@ -2785,11 +2935,15 @@ TEST (AudioGraphProcessorTests, MixedAudioMidiProcessorPassesThroughBothSignals) AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; + fillImpulse (audio); + const uint8 noteOn[] = { 0x90, 60, 100 }; midi.addEvent (noteOn, 3, 5); - graph.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (0)[0]); EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (1)[0]); @@ -2816,11 +2970,15 @@ TEST (AudioGraphProcessorTests, MixedAudioMidiSerialChainProcessesBothSignals) AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; + fillImpulse (audio); + const uint8 noteOn[] = { 0x90, 60, 100 }; midi.addEvent (noteOn, 3, 3); - graph.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); EXPECT_FLOAT_EQ (0.25f, audio.getReadPointer (0)[0]); EXPECT_FLOAT_EQ (0.25f, audio.getReadPointer (1)[0]); @@ -2859,11 +3017,15 @@ TEST (AudioGraphProcessorTests, MixedAudioMidiProcessorPdcCompensatesDirectPaths AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; + fillImpulseAt (audio, 2); + const uint8 noteOn[] = { 0x90, 60, 100 }; midi.addEvent (noteOn, 3, 2); - graph.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[2]); EXPECT_FLOAT_EQ (2.0f, audio.getReadPointer (0)[7]); @@ -2917,9 +3079,12 @@ TEST (AudioGraphProcessorTests, ReplaceNodeProcessorPreservesNodeIDAndCompatible AudioBuffer audio (2, 16); MidiBuffer midi; + ParameterChangeBuffer params; + fillImpulse (audio); - graph.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + graph.processBlock (ctx); EXPECT_FLOAT_EQ (0.25f, audio.getReadPointer (0)[0]); EXPECT_FLOAT_EQ (0.25f, audio.getReadPointer (1)[0]); diff --git a/tests/yup_audio_gui/yup_AudioGraphComponent.cpp b/tests/yup_audio_gui/yup_AudioGraphComponent.cpp index dc92e93ca..76160a651 100644 --- a/tests/yup_audio_gui/yup_AudioGraphComponent.cpp +++ b/tests/yup_audio_gui/yup_AudioGraphComponent.cpp @@ -71,7 +71,7 @@ class TestProcessor : public AudioProcessor void releaseResources() override {} - void processBlock (AudioBuffer&, MidiBuffer&) override {} + void processBlock (AudioProcessContext&) override {} int getLatencySamples() override { return 0; } @@ -104,7 +104,7 @@ class MonoProcessor : public AudioProcessor void releaseResources() override {} - void processBlock (AudioBuffer&, MidiBuffer&) override {} + void processBlock (AudioProcessContext&) override {} int getCurrentPreset() const noexcept override { return 0; } diff --git a/tests/yup_audio_plugin_host/yup_AudioPluginInstance.cpp b/tests/yup_audio_plugin_host/yup_AudioPluginInstance.cpp index 07799367d..d230070fb 100644 --- a/tests/yup_audio_plugin_host/yup_AudioPluginInstance.cpp +++ b/tests/yup_audio_plugin_host/yup_AudioPluginInstance.cpp @@ -22,15 +22,15 @@ class FakePluginInstance : public AudioPluginInstance void releaseResources() override { prepared = false; } - void processBlock (AudioBuffer& audio, MidiBuffer&) override + void processBlock (AudioProcessContext& context) override { - audio.clear(); + context.audio.clear(); processCallCount++; } - void processBlock (AudioBuffer& audio, MidiBuffer&) override + void processBlock (AudioProcessContext& context) override { - audio.clear(); + context.audio.clear(); doubleProcessCallCount++; } @@ -92,7 +92,7 @@ class CountOnlyPluginInstance : public AudioPluginInstance void releaseResources() override {} - void processBlock (AudioBuffer&, MidiBuffer&) override {} + void processBlock (AudioProcessContext&) override {} int getCurrentPreset() const noexcept override { return 0; } @@ -134,15 +134,15 @@ class BypassPluginInstance : public AudioPluginInstance void releaseResources() override {} - void processBlock (AudioBuffer& audio, MidiBuffer& midi) override + void processBlock (AudioProcessContext& context) override { if (isBypassed()) { - processBlockBypassed (audio, midi); + processBlockBypassed (context); return; } - audio.clear(); + context.audio.clear(); } int getCurrentPreset() const noexcept override { return 0; } @@ -221,9 +221,11 @@ TEST_F (AudioPluginInstanceTests, ProcessBlockIncrementsCounter) { AudioBuffer audio (2, 512); MidiBuffer midi; + ParameterChangeBuffer params; instance.prepareToPlay (44100.0f, 512); - instance.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + instance.processBlock (ctx); EXPECT_EQ (1, instance.processCallCount); } @@ -262,12 +264,14 @@ TEST_F (AudioPluginInstanceTests, DoublePrecisionProcessBlockUsesDoublePath) FakePluginInstance doublePrecisionInstance (true); AudioBuffer audio (2, 512); MidiBuffer midi; + ParameterChangeBuffer params; audio.setSample (0, 0, 1.0); doublePrecisionInstance.setProcessingPrecision (AudioProcessor::ProcessingPrecision::doublePrecision); doublePrecisionInstance.prepareToPlay (44100.0f, 512); - doublePrecisionInstance.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + doublePrecisionInstance.processBlock (ctx); EXPECT_EQ (0, doublePrecisionInstance.processCallCount); EXPECT_EQ (1, doublePrecisionInstance.doubleProcessCallCount); @@ -308,12 +312,14 @@ TEST (AudioPluginInstanceBypassTests, CopiesMatchingInputChannelsAndClearsExtraO BypassPluginInstance bypassInstance (1, 2); AudioBuffer audio (2, 8); MidiBuffer midi; + ParameterChangeBuffer params; audio.setSample (0, 0, 0.75f); audio.setSample (1, 0, 0.5f); bypassInstance.setBypassed (true); - bypassInstance.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + bypassInstance.processBlock (ctx); EXPECT_FLOAT_EQ (0.75f, audio.getSample (0, 0)); EXPECT_FLOAT_EQ (0.0f, audio.getSample (1, 0)); @@ -324,12 +330,14 @@ TEST (AudioPluginInstanceBypassTests, ClearsInstrumentOutputsWithoutInputs) BypassPluginInstance bypassInstance (0, 2); AudioBuffer audio (2, 8); MidiBuffer midi; + ParameterChangeBuffer params; audio.setSample (0, 0, 0.75f); audio.setSample (1, 0, 0.5f); bypassInstance.setBypassed (true); - bypassInstance.processBlock (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + bypassInstance.processBlock (ctx); EXPECT_FLOAT_EQ (0.0f, audio.getSample (0, 0)); EXPECT_FLOAT_EQ (0.0f, audio.getSample (1, 0)); @@ -340,11 +348,13 @@ TEST (AudioPluginInstanceBypassTests, SupportsDoublePrecisionBypass) BypassPluginInstance bypassInstance (1, 2); AudioBuffer audio (2, 8); MidiBuffer midi; + ParameterChangeBuffer params; audio.setSample (0, 0, 0.75); audio.setSample (1, 0, 0.5); - bypassInstance.processBlockBypassed (audio, midi); + AudioProcessContext ctx { audio, midi, params }; + bypassInstance.processBlockBypassed (ctx); EXPECT_DOUBLE_EQ (0.75, audio.getSample (0, 0)); EXPECT_DOUBLE_EQ (0.0, audio.getSample (1, 0)); diff --git a/tests/yup_audio_plugin_host/yup_AudioPluginState.cpp b/tests/yup_audio_plugin_host/yup_AudioPluginState.cpp index 551f5a3bf..1d79efafa 100644 --- a/tests/yup_audio_plugin_host/yup_AudioPluginState.cpp +++ b/tests/yup_audio_plugin_host/yup_AudioPluginState.cpp @@ -20,7 +20,7 @@ class StatefulFakeInstance : public AudioPluginInstance void releaseResources() override {} - void processBlock (AudioBuffer&, MidiBuffer&) override {} + void processBlock (AudioProcessContext&) override {} int getCurrentPreset() const noexcept override { return currentPreset; } diff --git a/tests/yup_audio_processors.cpp b/tests/yup_audio_processors.cpp new file mode 100644 index 000000000..28ddcf803 --- /dev/null +++ b/tests/yup_audio_processors.cpp @@ -0,0 +1,24 @@ +/* + ============================================================================== + + 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_processors/yup_AudioParameter.cpp" +#include "yup_audio_processors/yup_AudioProcessContext.cpp" +#include "yup_audio_processors/yup_ParameterChangeBuffer.cpp" diff --git a/tests/yup_audio_processors/yup_AudioParameter.cpp b/tests/yup_audio_processors/yup_AudioParameter.cpp new file mode 100644 index 000000000..fa53e973f --- /dev/null +++ b/tests/yup_audio_processors/yup_AudioParameter.cpp @@ -0,0 +1,504 @@ +/* + ============================================================================== + + 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 + +#include + +using namespace yup; + +namespace +{ + +class TestAudioProcessor final : public AudioProcessor +{ +public: + TestAudioProcessor (AudioBusLayout layout = AudioBusLayout ({}, {})) + : AudioProcessor ("Test", std::move (layout)) + { + } + + void prepareToPlay (float newSampleRate, int newSamplesPerBlock) override + { + ++prepareCallCount; + preparedSampleRate = newSampleRate; + preparedSamplesPerBlock = newSamplesPerBlock; + } + + void releaseResources() override + { + ++releaseCallCount; + } + + void processBlock (AudioProcessContext& context) override + { + ignoreUnused (context); + } + + int getCurrentPreset() const noexcept override { return 0; } + + void setCurrentPreset (int) noexcept override {} + + int getNumPresets() const override { return 0; } + + String getPresetName (int) const override { return {}; } + + void setPresetName (int, StringRef) override {} + + Result loadStateFromMemory (const MemoryBlock&) override { return Result::ok(); } + + Result saveStateIntoMemory (MemoryBlock&) override { return Result::ok(); } + + bool hasEditor() const override { return false; } + + int prepareCallCount = 0; + int releaseCallCount = 0; + float preparedSampleRate = 0.0f; + int preparedSamplesPerBlock = 0; +}; + +class ParameterListener final : public AudioParameter::Listener +{ +public: + void parameterValueChanged (const AudioParameter::Ptr& parameter, int indexInContainer) override + { + ++valueChangedCount; + lastParameter = parameter.get(); + lastIndex = indexInContainer; + } + + void parameterGestureBegin (const AudioParameter::Ptr& parameter, int indexInContainer) override + { + ++gestureBeginCount; + lastParameter = parameter.get(); + lastIndex = indexInContainer; + } + + void parameterGestureEnd (const AudioParameter::Ptr& parameter, int indexInContainer) override + { + ++gestureEndCount; + lastParameter = parameter.get(); + lastIndex = indexInContainer; + } + + int valueChangedCount = 0; + int gestureBeginCount = 0; + int gestureEndCount = 0; + AudioParameter* lastParameter = nullptr; + int lastIndex = -1; +}; + +class ProcessorListener final : public AudioProcessor::Listener +{ +public: + void audioProcessorChanged (AudioProcessor* processor, const AudioProcessor::ChangeDetails& details) override + { + ++changedCount; + lastProcessor = processor; + lastDetails = details; + } + + int changedCount = 0; + AudioProcessor* lastProcessor = nullptr; + AudioProcessor::ChangeDetails lastDetails; +}; + +AudioParameter::Ptr makeParameter (StringRef id, StringRef name) +{ + return AudioParameterBuilder() + .withID (id) + .withName (name) + .withRange (0.0f, 1.0f) + .withDefault (0.5f) + .build(); +} + +} // namespace + +TEST (AudioParameterTests, UsesIndexAsHostIDByDefault) +{ + TestAudioProcessor processor; + auto first = makeParameter ("first", "First"); + auto second = makeParameter ("second", "Second"); + + processor.addParameter (first); + processor.addParameter (second); + + EXPECT_FALSE (first->hasExplicitHostParameterID()); + EXPECT_FALSE (second->hasExplicitHostParameterID()); + EXPECT_EQ (0u, first->getHostParameterID()); + EXPECT_EQ (1u, second->getHostParameterID()); + EXPECT_EQ (first.get(), processor.getParameterByHostID (0u).get()); + EXPECT_EQ (second.get(), processor.getParameterByHostID (1u).get()); +} + +TEST (AudioParameterTests, UsesExplicitStableHostIDWhenProvided) +{ + TestAudioProcessor processor; + auto parameter = AudioParameterBuilder() + .withID ("gain") + .withName ("Gain") + .withHostID (1001u) + .withRange (0.0f, 1.0f) + .withDefault (0.5f) + .build(); + + processor.addParameter (parameter); + + EXPECT_TRUE (parameter->hasExplicitHostParameterID()); + EXPECT_EQ (1001u, parameter->getHostParameterID()); + EXPECT_EQ (0, processor.getParameterIndexByHostID (1001u)); + EXPECT_EQ (parameter.get(), processor.getParameterByHostID (1001u).get()); + EXPECT_EQ (nullptr, processor.getParameterByHostID (0u).get()); +} + +TEST (AudioParameterTests, IsNotModulatableByDefault) +{ + auto parameter = makeParameter ("gain", "Gain"); + + EXPECT_FALSE (parameter->isModulatable()); + EXPECT_FALSE (parameter->isPerNoteModulatable()); +} + +TEST (AudioParameterTests, WithModulatableSetsFlag) +{ + auto parameter = AudioParameterBuilder() + .withID ("gain") + .withName ("Gain") + .withRange (0.0f, 1.0f) + .withDefault (0.5f) + .withModulatable (true) + .build(); + + EXPECT_TRUE (parameter->isModulatable()); + EXPECT_FALSE (parameter->isPerNoteModulatable()); +} + +TEST (AudioParameterTests, WithPerNoteModulatableImpliesModulatable) +{ + auto parameter = AudioParameterBuilder() + .withID ("gain") + .withName ("Gain") + .withRange (0.0f, 1.0f) + .withDefault (0.5f) + .withPerNoteModulatable (true) + .build(); + + EXPECT_TRUE (parameter->isModulatable()); + EXPECT_TRUE (parameter->isPerNoteModulatable()); +} + +TEST (AudioParameterTests, ClearingPerNoteModulatablePreservesModulatable) +{ + auto parameter = AudioParameterBuilder() + .withID ("gain") + .withName ("Gain") + .withRange (0.0f, 1.0f) + .withDefault (0.5f) + .withModulatable (true) + .withPerNoteModulatable (true) + .withPerNoteModulatable (false) + .build(); + + EXPECT_TRUE (parameter->isModulatable()); + EXPECT_FALSE (parameter->isPerNoteModulatable()); +} + +TEST (AudioParameterTests, ReadOnlyParameterIsNotAutomatable) +{ + auto parameter = AudioParameterBuilder() + .withID ("gain") + .withName ("Gain") + .withRange (0.0f, 1.0f) + .withDefault (0.5f) + .withReadOnly (true) + .build(); + + EXPECT_TRUE (parameter->isReadOnly()); + EXPECT_FALSE (parameter->isAutomatable()); +} + +TEST (AudioParameterTests, AutomatableParameterIsNotReadOnly) +{ + auto parameter = AudioParameterBuilder() + .withID ("gain") + .withName ("Gain") + .withRange (0.0f, 1.0f) + .withDefault (0.5f) + .withReadOnly (true) + .withAutomatable (true) + .build(); + + EXPECT_FALSE (parameter->isReadOnly()); + EXPECT_TRUE (parameter->isAutomatable()); +} + +TEST (AudioParameterTests, SmoothingCanBeEnabledAndDisabled) +{ + auto smoothed = AudioParameterBuilder() + .withID ("gain") + .withName ("Gain") + .withRange (0.0f, 1.0f) + .withDefault (0.5f) + .withSmoothing (25.0f) + .build(); + + EXPECT_TRUE (smoothed->isSmoothingEnabled()); + EXPECT_FLOAT_EQ (25.0f, smoothed->getSmoothingTimeMs()); + + auto unsmoothed = AudioParameterBuilder() + .withID ("mix") + .withName ("Mix") + .withRange (0.0f, 1.0f) + .withDefault (0.5f) + .withSmoothing (25.0f) + .withSmoothing (0.0f) + .build(); + + EXPECT_FALSE (unsmoothed->isSmoothingEnabled()); + EXPECT_FLOAT_EQ (0.0f, unsmoothed->getSmoothingTimeMs()); +} + +TEST (AudioParameterTests, EnumImpliesSteppedAndClearingSteppedClearsEnum) +{ + auto enumParameter = AudioParameterBuilder() + .withID ("shape") + .withName ("Shape") + .withRange (0.0f, 3.0f) + .withDefault (1.0f) + .withEnum (true) + .build(); + + EXPECT_TRUE (enumParameter->isEnum()); + EXPECT_TRUE (enumParameter->isStepped()); + + auto continuousParameter = AudioParameterBuilder() + .withID ("shape") + .withName ("Shape") + .withRange (0.0f, 3.0f) + .withDefault (1.0f) + .withEnum (true) + .withStepped (false) + .build(); + + EXPECT_FALSE (continuousParameter->isEnum()); + EXPECT_FALSE (continuousParameter->isStepped()); +} + +TEST (AudioParameterTests, DefaultAndValueAreSnappedToLegalRange) +{ + auto parameter = AudioParameterBuilder() + .withID ("steps") + .withName ("Steps") + .withRange (NormalisableRange (0.0f, 10.0f, 2.0f)) + .withDefault (5.1f) + .build(); + + EXPECT_FLOAT_EQ (6.0f, parameter->getDefaultValue()); + EXPECT_FLOAT_EQ (6.0f, parameter->getValue()); + EXPECT_TRUE (parameter->isStepped()); + EXPECT_EQ (5, parameter->getNumSteps()); + + parameter->setValue (11.0f); + EXPECT_FLOAT_EQ (10.0f, parameter->getValue()); +} + +TEST (AudioParameterTests, NumStepsReportsContinuousAndFlagOnlySteppedParameters) +{ + auto continuous = makeParameter ("gain", "Gain"); + EXPECT_FALSE (continuous->isStepped()); + EXPECT_EQ (0, continuous->getNumSteps()); + + auto stepped = AudioParameterBuilder() + .withID ("mode") + .withName ("Mode") + .withRange (0.0f, 3.0f) + .withDefault (0.0f) + .withStepped (true) + .build(); + + EXPECT_TRUE (stepped->isStepped()); + EXPECT_EQ (1, stepped->getNumSteps()); +} + +TEST (AudioParameterTests, CustomStringConvertersAreUsed) +{ + auto parameter = AudioParameterBuilder() + .withID ("mode") + .withName ("Mode") + .withRange (0.0f, 1.0f) + .withDefault (0.25f) + .withValueToString ([] (float value) + { + return value >= 0.5f ? String ("high") : String ("low"); + }).withStringToValue ([] (const String& text) + { + return text == "high" ? 0.75f : 0.25f; + }).build(); + + EXPECT_EQ ("low", parameter->toString()); + EXPECT_EQ ("high", parameter->convertToString (0.75f)); + EXPECT_FLOAT_EQ (0.75f, parameter->convertFromString ("high")); + + parameter->fromString ("high"); + EXPECT_FLOAT_EQ (0.75f, parameter->getValue()); + EXPECT_EQ ("high", parameter->toString()); +} + +TEST (AudioParameterTests, ListenerReceivesValueAndGestureChanges) +{ + auto parameter = makeParameter ("gain", "Gain"); + parameter->setIndexInContainer (7); + + ParameterListener listener; + parameter->addListener (&listener); + + parameter->setValueNotifyingHost (0.75f); + EXPECT_EQ (1, listener.valueChangedCount); + EXPECT_EQ (parameter.get(), listener.lastParameter); + EXPECT_EQ (7, listener.lastIndex); + + parameter->beginChangeGesture(); + parameter->beginChangeGesture(); + EXPECT_TRUE (parameter->isPerformingChangeGesture()); + EXPECT_EQ (1, listener.gestureBeginCount); + + parameter->endChangeGesture(); + EXPECT_TRUE (parameter->isPerformingChangeGesture()); + EXPECT_EQ (0, listener.gestureEndCount); + + parameter->endChangeGesture(); + EXPECT_FALSE (parameter->isPerformingChangeGesture()); + EXPECT_EQ (1, listener.gestureEndCount); +} + +TEST (AudioParameterTests, RemovedListenerDoesNotReceiveFurtherChanges) +{ + auto parameter = makeParameter ("gain", "Gain"); + + ParameterListener listener; + parameter->addListener (&listener); + parameter->removeListener (&listener); + + parameter->setValueNotifyingHost (0.75f); + EXPECT_EQ (0, listener.valueChangedCount); +} + +TEST (AudioProcessorTests, DuplicateParameterIDsAreIgnored) +{ + TestAudioProcessor processor; + auto first = makeParameter ("gain", "Gain"); + auto duplicateID = makeParameter ("gain", "Duplicate Gain"); + auto hostID = AudioParameterBuilder() + .withID ("mix") + .withName ("Mix") + .withHostID (10u) + .withRange (0.0f, 1.0f) + .withDefault (0.5f) + .build(); + + processor.addParameter (first); + processor.addParameter (duplicateID); + processor.addParameter (hostID); + + EXPECT_EQ (2, static_cast (processor.getParameters().size())); + EXPECT_EQ (first.get(), processor.getParameterByID ("gain").get()); + EXPECT_EQ (hostID.get(), processor.getParameterByHostID (10u).get()); + EXPECT_EQ (-1, processor.getParameterIndexByHostID (11u)); +} + +TEST (AudioProcessorTests, CountsOnlyAudioBuses) +{ + TestAudioProcessor processor (AudioBusLayout ( + { AudioBus ("Audio In", AudioBus::Type::Audio, AudioBus::Direction::Input, 2), + AudioBus ("MIDI In", AudioBus::Type::MIDI, AudioBus::Direction::Input, 0) }, + { AudioBus ("Audio Out", AudioBus::Type::Audio, AudioBus::Direction::Output, 2), + AudioBus ("MIDI Out", AudioBus::Type::MIDI, AudioBus::Direction::Output, 0) })); + + EXPECT_EQ (1, processor.getNumAudioInputs()); + EXPECT_EQ (1, processor.getNumAudioOutputs()); +} + +TEST (AudioProcessorTests, LatencyChangeNotifiesOnlyWhenValueChanges) +{ + TestAudioProcessor processor; + ProcessorListener listener; + processor.addListener (&listener); + + processor.setLatencySamples (-12); + EXPECT_EQ (0, processor.getLatencySamples()); + EXPECT_EQ (0, listener.changedCount); + + processor.setLatencySamples (64); + EXPECT_EQ (64, processor.getLatencySamples()); + EXPECT_EQ (1, listener.changedCount); + EXPECT_EQ (&processor, listener.lastProcessor); + EXPECT_TRUE (listener.lastDetails.latencyChanged); + + processor.setLatencySamples (64); + EXPECT_EQ (1, listener.changedCount); + + processor.removeListener (&listener); + processor.setLatencySamples (128); + EXPECT_EQ (1, listener.changedCount); +} + +TEST (AudioProcessorTests, ChangeDetailsSetRequestedFlags) +{ + const auto details = AudioProcessor::ChangeDetails() + .withLatencyChanged (true) + .withTailChanged (true) + .withParameterValuesChanged (true) + .withParameterInfoChanged (true) + .withNonParameterStateChanged (true); + + EXPECT_TRUE (details.latencyChanged); + EXPECT_TRUE (details.tailChanged); + EXPECT_TRUE (details.parameterValuesChanged); + EXPECT_TRUE (details.parameterInfoChanged); + EXPECT_TRUE (details.nonParameterStateChanged); + + const auto cleared = details.withLatencyChanged (false) + .withTailChanged (false) + .withParameterValuesChanged (false) + .withParameterInfoChanged (false) + .withNonParameterStateChanged (false); + + EXPECT_FALSE (cleared.latencyChanged); + EXPECT_FALSE (cleared.tailChanged); + EXPECT_FALSE (cleared.parameterValuesChanged); + EXPECT_FALSE (cleared.parameterInfoChanged); + EXPECT_FALSE (cleared.nonParameterStateChanged); +} + +TEST (AudioProcessorTests, PlaybackConfigurationReleasesBeforePreparing) +{ + TestAudioProcessor processor; + + processor.setPlaybackConfiguration (48000.0f, 256); + + EXPECT_EQ (1, processor.releaseCallCount); + EXPECT_EQ (1, processor.prepareCallCount); + EXPECT_FLOAT_EQ (48000.0f, processor.getSampleRate()); + EXPECT_EQ (256, processor.getSamplesPerBlock()); + EXPECT_FLOAT_EQ (48000.0f, processor.preparedSampleRate); + EXPECT_EQ (256, processor.preparedSamplesPerBlock); +} diff --git a/tests/yup_audio_processors/yup_AudioProcessContext.cpp b/tests/yup_audio_processors/yup_AudioProcessContext.cpp new file mode 100644 index 000000000..5843c5219 --- /dev/null +++ b/tests/yup_audio_processors/yup_AudioProcessContext.cpp @@ -0,0 +1,67 @@ +/* + ============================================================================== + + 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 + +#include + +using namespace yup; + +namespace +{ + +class TestPlayHead final : public AudioPlayHead +{ +public: + std::optional getPosition() const override + { + PositionInfo position; + position.setTimeInSamples (128); + return position; + } +}; + +} // namespace + +TEST (AudioProcessContextTests, DefaultsToNoPlayHead) +{ + AudioBuffer audio (2, 16); + MidiBuffer midi; + ParameterChangeBuffer params; + + AudioProcessContext context { audio, midi, params }; + + EXPECT_EQ (nullptr, context.playHead); +} + +TEST (AudioProcessContextTests, CarriesPlayHeadPointer) +{ + AudioBuffer audio (2, 16); + MidiBuffer midi; + ParameterChangeBuffer params; + TestPlayHead playHead; + + AudioProcessContext context { audio, midi, params, &playHead }; + + EXPECT_EQ (&playHead, context.playHead); + ASSERT_TRUE (context.playHead->getPosition().has_value()); + EXPECT_EQ (128, *context.playHead->getPosition()->getTimeInSamples()); +} diff --git a/tests/yup_audio_processors/yup_ParameterChangeBuffer.cpp b/tests/yup_audio_processors/yup_ParameterChangeBuffer.cpp new file mode 100644 index 000000000..ee87fbc24 --- /dev/null +++ b/tests/yup_audio_processors/yup_ParameterChangeBuffer.cpp @@ -0,0 +1,155 @@ +/* + ============================================================================== + + 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 + +#include + +using namespace yup; + +TEST (ParameterChangeBufferTests, AddsAndClearsReservedChanges) +{ + ParameterChangeBuffer changes; + + EXPECT_TRUE (changes.isEmpty()); + EXPECT_EQ (0, changes.getNumChanges()); + + changes.reserve (2); + + EXPECT_TRUE (changes.addChange (3, 0.25f, 12)); + EXPECT_TRUE (changes.addChange (4, 0.5f, 24)); + + EXPECT_FALSE (changes.isEmpty()); + EXPECT_EQ (2, changes.getNumChanges()); + + ASSERT_NE (changes.begin(), changes.end()); + EXPECT_EQ (3, changes.begin()->parameterIndex); + EXPECT_FLOAT_EQ (0.25f, changes.begin()->normalizedValue); + EXPECT_EQ (12, changes.begin()->sampleOffset); + + changes.clear(); + + EXPECT_TRUE (changes.isEmpty()); + EXPECT_EQ (0, changes.getNumChanges()); +} + +TEST (ParameterChangeBufferTests, SortOrdersChangesBySampleOffset) +{ + ParameterChangeBuffer changes; + changes.reserve (4); + + EXPECT_TRUE (changes.addChange (1, 0.3f, 24)); + EXPECT_TRUE (changes.addChange (2, 0.4f, 4)); + EXPECT_TRUE (changes.addChange (1, 0.2f, 12)); + + changes.sort(); + + ASSERT_EQ (3, changes.getNumChanges()); + + const auto* change = changes.begin(); + EXPECT_EQ (4, change[0].sampleOffset); + EXPECT_EQ (12, change[1].sampleOffset); + EXPECT_EQ (24, change[2].sampleOffset); +} + +TEST (ParameterChangeBufferTests, FindsNextChangeAtOrAfterSamplePosition) +{ + ParameterChangeBuffer changes; + changes.reserve (3); + + EXPECT_TRUE (changes.addChange (1, 0.1f, 3)); + EXPECT_TRUE (changes.addChange (1, 0.2f, 9)); + EXPECT_TRUE (changes.addChange (1, 0.3f, 14)); + changes.sort(); + + EXPECT_EQ (3, changes.findNextSamplePosition (0)->sampleOffset); + EXPECT_EQ (3, changes.findNextSamplePosition (3)->sampleOffset); + EXPECT_EQ (9, changes.findNextSamplePosition (4)->sampleOffset); + EXPECT_EQ (14, changes.findNextSamplePosition (14)->sampleOffset); + EXPECT_EQ (changes.end(), changes.findNextSamplePosition (15)); +} + +TEST (AudioParameterHandleTests, AdvanceToSampleAppliesOnlyMatchingParameterChanges) +{ + auto parameter = AudioParameterBuilder() + .withID ("gain") + .withName ("Gain") + .withRange (0.0f, 100.0f) + .withDefault (10.0f) + .build(); + + parameter->setIndexInContainer (2); + + AudioParameterHandle handle (*parameter, 48000.0); + + ParameterChangeBuffer changes; + changes.reserve (4); + EXPECT_TRUE (changes.addChange (1, 0.5f, 0)); + EXPECT_TRUE (changes.addChange (2, 0.25f, 4)); + EXPECT_TRUE (changes.addChange (2, 0.75f, 8)); + changes.sort(); + + handle.prepareBlock (changes, parameter->getIndexInContainer()); + + EXPECT_FALSE (handle.advanceToSample (3)); + EXPECT_FLOAT_EQ (10.0f, parameter->getValue()); + + EXPECT_TRUE (handle.advanceToSample (4)); + EXPECT_FLOAT_EQ (25.0f, parameter->getValue()); + EXPECT_FLOAT_EQ (25.0f, handle.getCurrentValue()); + + EXPECT_FALSE (handle.advanceToSample (7)); + EXPECT_FLOAT_EQ (25.0f, parameter->getValue()); + + EXPECT_TRUE (handle.advanceToSample (8)); + EXPECT_FLOAT_EQ (75.0f, parameter->getValue()); + EXPECT_FLOAT_EQ (75.0f, handle.getCurrentValue()); + + EXPECT_FALSE (handle.advanceToSample (8)); +} + +TEST (AudioParameterHandleTests, PrepareBlockRestartsAutomationIteration) +{ + auto parameter = AudioParameterBuilder() + .withID ("mix") + .withName ("Mix") + .withRange (0.0f, 1.0f) + .withDefault (0.0f) + .build(); + + parameter->setIndexInContainer (0); + + AudioParameterHandle handle (*parameter, 48000.0); + + ParameterChangeBuffer changes; + changes.reserve (1); + EXPECT_TRUE (changes.addChange (0, 1.0f, 2)); + + handle.prepareBlock (changes, parameter->getIndexInContainer()); + EXPECT_TRUE (handle.advanceToSample (2)); + EXPECT_FLOAT_EQ (1.0f, parameter->getValue()); + + parameter->setValue (0.0f); + + handle.prepareBlock (changes, parameter->getIndexInContainer()); + EXPECT_TRUE (handle.advanceToSample (2)); + EXPECT_FLOAT_EQ (1.0f, parameter->getValue()); +} diff --git a/tests/yup_gui/yup_ApplicationTheme.cpp b/tests/yup_gui/yup_ApplicationTheme.cpp index 7f4a57805..4608feccf 100644 --- a/tests/yup_gui/yup_ApplicationTheme.cpp +++ b/tests/yup_gui/yup_ApplicationTheme.cpp @@ -53,22 +53,28 @@ class ApplicationThemeTest : public ::testing::Test TEST_F (ApplicationThemeTest, FindColorReturnsNulloptWhenColorNotRegistered) { - auto result = ApplicationTheme::findColor (Identifier ("unknownColor")); + auto c = Component ("testComponent"); + + auto result = ApplicationTheme::findComponentColor (c, Identifier ("unknownColor")); EXPECT_FALSE (result.has_value()); } TEST_F (ApplicationThemeTest, FindColorReturnsRegisteredColor) { + auto c = Component ("testComponent"); + const Color expected = Color::fromRGBA (255, 128, 0, 255); theme->setColor (Identifier ("testColor"), expected); - auto result = ApplicationTheme::findColor (Identifier ("testColor")); + auto result = ApplicationTheme::findComponentColor (c, Identifier ("testColor")); ASSERT_TRUE (result.has_value()); EXPECT_EQ (result.value(), expected); } TEST_F (ApplicationThemeTest, SetColorsRegistersMultipleColors) { + auto c = Component ("testComponent"); + const Identifier idA ("colorA"); const Identifier idB ("colorB"); const Color colorA = Color::fromRGBA (10, 20, 30, 255); @@ -76,8 +82,8 @@ TEST_F (ApplicationThemeTest, SetColorsRegistersMultipleColors) theme->setColors ({ { idA, colorA }, { idB, colorB } }); - auto resultA = ApplicationTheme::findColor (idA); - auto resultB = ApplicationTheme::findColor (idB); + auto resultA = ApplicationTheme::findComponentColor (c, idA); + auto resultB = ApplicationTheme::findComponentColor (c, idB); ASSERT_TRUE (resultA.has_value()); EXPECT_EQ (resultA.value(), colorA); @@ -87,6 +93,8 @@ TEST_F (ApplicationThemeTest, SetColorsRegistersMultipleColors) TEST_F (ApplicationThemeTest, SetColorOverwritesExistingColor) { + auto c = Component ("testComponent"); + const Identifier id ("myColor"); const Color first = Color::fromRGBA (1, 2, 3, 255); const Color second = Color::fromRGBA (4, 5, 6, 255); @@ -94,70 +102,142 @@ TEST_F (ApplicationThemeTest, SetColorOverwritesExistingColor) theme->setColor (id, first); theme->setColor (id, second); - auto result = ApplicationTheme::findColor (id); + auto result = ApplicationTheme::findComponentColor (c, id); ASSERT_TRUE (result.has_value()); EXPECT_EQ (result.value(), second); } +TEST_F (ApplicationThemeTest, ComponentColorOverridesRegisteredColor) +{ + auto c = Component ("testComponent"); + + const Identifier id ("accentColor"); + const Color themeColor = Color::fromRGBA (1, 2, 3, 255); + const Color componentColor = Color::fromRGBA (4, 5, 6, 255); + + theme->setColor (id, themeColor); + c.setColor (id, componentColor); + + auto result = ApplicationTheme::findComponentColor (c, id); + ASSERT_TRUE (result.has_value()); + EXPECT_EQ (result.value(), componentColor); + + c.setColor (id, std::nullopt); + + result = ApplicationTheme::findComponentColor (c, id); + ASSERT_TRUE (result.has_value()); + EXPECT_EQ (result.value(), themeColor); +} + TEST_F (ApplicationThemeTest, FindColorUnregisteredIdDoesNotAffectOtherIds) { + auto c = Component ("testComponent"); + const Identifier registered ("registered"); const Identifier unregistered ("unregistered"); const Color color = Color::fromRGBA (0, 0, 255, 255); theme->setColor (registered, color); - EXPECT_TRUE (ApplicationTheme::findColor (registered).has_value()); - EXPECT_FALSE (ApplicationTheme::findColor (unregistered).has_value()); + EXPECT_TRUE (ApplicationTheme::findComponentColor (c, registered).has_value()); + EXPECT_FALSE (ApplicationTheme::findComponentColor (c, unregistered).has_value()); } // ============================================================================= TEST_F (ApplicationThemeTest, FindMetricReturnsNulloptWhenMetricNotRegistered) { - auto result = ApplicationTheme::findMetric (Identifier ("unknownMetric")); + auto c = Component ("testComponent"); + + auto result = ApplicationTheme::findComponentMetric (c, Identifier ("unknownMetric")); EXPECT_FALSE (result.has_value()); } TEST_F (ApplicationThemeTest, FindMetricReturnsRegisteredValue) { + auto c = Component ("testComponent"); + theme->setMetric (Identifier ("cornerRadius"), 8.0f); - auto result = ApplicationTheme::findMetric (Identifier ("cornerRadius")); + auto result = ApplicationTheme::findComponentMetric (c, Identifier ("cornerRadius")); ASSERT_TRUE (result.has_value()); EXPECT_FLOAT_EQ (result.value(), 8.0f); } TEST_F (ApplicationThemeTest, SetMetricOverwritesExistingValue) { + auto c = Component ("testComponent"); + const Identifier id ("borderWidth"); theme->setMetric (id, 1.0f); theme->setMetric (id, 3.5f); - auto result = ApplicationTheme::findMetric (id); + auto result = ApplicationTheme::findComponentMetric (c, id); ASSERT_TRUE (result.has_value()); EXPECT_FLOAT_EQ (result.value(), 3.5f); } +TEST_F (ApplicationThemeTest, SetMetricsRegistersMultipleValues) +{ + auto c = Component ("testComponent"); + + const Identifier idA ("smallSpacing"); + const Identifier idB ("largeSpacing"); + + theme->setMetrics ({ { idA, 4.0f }, { idB, 12.0f } }); + + auto resultA = ApplicationTheme::findComponentMetric (c, idA); + auto resultB = ApplicationTheme::findComponentMetric (c, idB); + + ASSERT_TRUE (resultA.has_value()); + EXPECT_FLOAT_EQ (resultA.value(), 4.0f); + ASSERT_TRUE (resultB.has_value()); + EXPECT_FLOAT_EQ (resultB.value(), 12.0f); +} + +TEST_F (ApplicationThemeTest, ComponentMetricOverridesRegisteredMetric) +{ + auto c = Component ("testComponent"); + + const Identifier id ("cornerRadius"); + + theme->setMetric (id, 8.0f); + c.setMetric (id, 12.0f); + + auto result = ApplicationTheme::findComponentMetric (c, id); + ASSERT_TRUE (result.has_value()); + EXPECT_FLOAT_EQ (result.value(), 12.0f); + + c.setMetric (id, std::nullopt); + + result = ApplicationTheme::findComponentMetric (c, id); + ASSERT_TRUE (result.has_value()); + EXPECT_FLOAT_EQ (result.value(), 8.0f); +} + TEST_F (ApplicationThemeTest, FindMetricUnregisteredIdDoesNotAffectOtherIds) { + auto c = Component ("testComponent"); + const Identifier registered ("spacing"); const Identifier unregistered ("padding"); theme->setMetric (registered, 4.0f); - EXPECT_TRUE (ApplicationTheme::findMetric (registered).has_value()); - EXPECT_FALSE (ApplicationTheme::findMetric (unregistered).has_value()); + EXPECT_TRUE (ApplicationTheme::findComponentMetric (c, registered).has_value()); + EXPECT_FALSE (ApplicationTheme::findComponentMetric (c, unregistered).has_value()); } TEST_F (ApplicationThemeTest, SetMetricAcceptsZeroAndNegativeValues) { + auto c = Component ("testComponent"); + theme->setMetric (Identifier ("zeroMetric"), 0.0f); theme->setMetric (Identifier ("negativeMetric"), -2.5f); - auto zero = ApplicationTheme::findMetric (Identifier ("zeroMetric")); - auto negative = ApplicationTheme::findMetric (Identifier ("negativeMetric")); + auto zero = ApplicationTheme::findComponentMetric (c, Identifier ("zeroMetric")); + auto negative = ApplicationTheme::findComponentMetric (c, Identifier ("negativeMetric")); ASSERT_TRUE (zero.has_value()); EXPECT_FLOAT_EQ (zero.value(), 0.0f); diff --git a/tests/yup_gui/yup_Component.cpp b/tests/yup_gui/yup_Component.cpp index 9b7d6ccfd..254a99b8b 100644 --- a/tests/yup_gui/yup_Component.cpp +++ b/tests/yup_gui/yup_Component.cpp @@ -1039,11 +1039,25 @@ class ComponentMockTest : public ::testing::Test protected: void SetUp() override { + oldTheme = ApplicationTheme::getGlobalTheme(); + theme = new ApplicationTheme(); + ApplicationTheme::setGlobalTheme (theme); + mockComponent = std::make_unique ("mockComponent"); mockComponent->resetCallTracking(); } + void TearDown() override + { + mockComponent.reset(); + ApplicationTheme::setGlobalTheme (oldTheme.get()); + theme = nullptr; + oldTheme = nullptr; + } + std::unique_ptr mockComponent; + ApplicationTheme::Ptr theme; + ApplicationTheme::Ptr oldTheme; }; // ============================================================================= @@ -1537,37 +1551,6 @@ TEST_F (ComponentMockTest, ColorMethods) EXPECT_FALSE (notFoundColor.has_value()); } -TEST_F (ComponentMockTest, StylePropertyMethods) -{ - Identifier propertyId ("testProperty"); - var testProperty = var (42); - - // Test setting style property - mockComponent->setStyleProperty (propertyId, testProperty); - - // Test getting style property - auto retrievedProperty = mockComponent->getStyleProperty (propertyId); - EXPECT_TRUE (retrievedProperty.has_value()); - if (retrievedProperty.has_value()) - { - EXPECT_EQ (static_cast (retrievedProperty.value()), 42); - } - - // Test finding style property - auto foundProperty = mockComponent->findStyleProperty (propertyId); - EXPECT_TRUE (foundProperty.has_value()); - - // Test setting null style property - mockComponent->setStyleProperty (propertyId, std::nullopt); - auto nullProperty = mockComponent->getStyleProperty (propertyId); - EXPECT_FALSE (nullProperty.has_value()); - - // Test finding non-existent property - Identifier nonExistentId ("nonExistent"); - auto notFoundProperty = mockComponent->findStyleProperty (nonExistentId); - EXPECT_FALSE (notFoundProperty.has_value()); -} - TEST_F (ComponentMockTest, UnclippedRenderingMethods) { // Test default unclipped rendering state @@ -1878,3 +1861,110 @@ TEST_F (ComponentMockTest, CoordinateTransformationMethods) EXPECT_TRUE (true); // Methods completed without crashing } + +TEST_F (ComponentMockTest, MetricMethods) +{ + Identifier metricId ("cornerRadius"); + + // Test setting metric + mockComponent->setMetric (metricId, 8.0f); + + // Test getting metric + auto retrievedMetric = mockComponent->getMetric (metricId); + ASSERT_TRUE (retrievedMetric.has_value()); + EXPECT_FLOAT_EQ (retrievedMetric.value(), 8.0f); + + // Test finding metric + auto foundMetric = mockComponent->findMetric (metricId); + ASSERT_TRUE (foundMetric.has_value()); + EXPECT_FLOAT_EQ (foundMetric.value(), 8.0f); + + // Test setting null metric (removing override) + mockComponent->setMetric (metricId, std::nullopt); + auto nullMetric = mockComponent->getMetric (metricId); + EXPECT_FALSE (nullMetric.has_value()); + + // Test finding non-existent metric (not in theme either) + Identifier nonExistentId ("nonExistentMetric"); + auto notFoundMetric = mockComponent->findMetric (nonExistentId); + EXPECT_FALSE (notFoundMetric.has_value()); +} + +TEST_F (ComponentMockTest, MetricParentFallback) +{ + auto parent = std::make_unique ("parent"); + auto child = std::make_unique ("child"); + + parent->addAndMakeVisible (*child); + + Identifier metricId ("padding"); + + // Set metric on parent + parent->setMetric (metricId, 12.0f); + + // Child should find parent's metric via parent chain fallback + auto childMetric = child->findMetric (metricId); + ASSERT_TRUE (childMetric.has_value()); + EXPECT_FLOAT_EQ (childMetric.value(), 12.0f); + + // Child override should take precedence + child->setMetric (metricId, 16.0f); + auto overriddenMetric = child->findMetric (metricId); + ASSERT_TRUE (overriddenMetric.has_value()); + EXPECT_FLOAT_EQ (overriddenMetric.value(), 16.0f); + + // Parent's metric should be unchanged + auto parentMetric = parent->getMetric (metricId); + ASSERT_TRUE (parentMetric.has_value()); + EXPECT_FLOAT_EQ (parentMetric.value(), 12.0f); + + // Clearing child override falls back to parent + child->setMetric (metricId, std::nullopt); + auto fallbackMetric = child->findMetric (metricId); + ASSERT_TRUE (fallbackMetric.has_value()); + EXPECT_FLOAT_EQ (fallbackMetric.value(), 12.0f); +} + +TEST_F (ComponentMockTest, DISABLED_MetricThemeFallback) +{ + // TODO - rewrite this with the new structure in mind, Component should not access to the global theme directly + Identifier metricId ("globalSpacing"); + + // Set a metric in the global theme + theme->setMetric (metricId, 20.0f); + + // Component should find it via findMetric -> theme fallback + auto metric = mockComponent->findMetric (metricId); + ASSERT_TRUE (metric.has_value()); + EXPECT_FLOAT_EQ (metric.value(), 20.0f); + + // Component override should take precedence over theme + mockComponent->setMetric (metricId, 24.0f); + auto overriddenMetric = mockComponent->findMetric (metricId); + ASSERT_TRUE (overriddenMetric.has_value()); + EXPECT_FLOAT_EQ (overriddenMetric.value(), 24.0f); + + // Clearing override falls back to theme + mockComponent->setMetric (metricId, std::nullopt); + auto themeFallback = mockComponent->findMetric (metricId); + ASSERT_TRUE (themeFallback.has_value()); + EXPECT_FLOAT_EQ (themeFallback.value(), 20.0f); +} + +TEST_F (ComponentMockTest, MetricAcceptsZeroAndNegative) +{ + Identifier zeroId ("zeroMetric"); + Identifier negativeId ("negativeMetric"); + + mockComponent->setMetric (zeroId, 0.0f); + mockComponent->setMetric (negativeId, -2.5f); + + auto zero = mockComponent->getMetric (zeroId); + auto negative = mockComponent->getMetric (negativeId); + + ASSERT_TRUE (zero.has_value()); + EXPECT_FLOAT_EQ (zero.value(), 0.0f); + + ASSERT_TRUE (negative.has_value()); + EXPECT_FLOAT_EQ (negative.value(), -2.5f); +} diff --git a/tests/yup_gui/yup_FileChooser.cpp b/tests/yup_gui/yup_FileChooser.cpp index 8b697d9f5..89a1e061e 100644 --- a/tests/yup_gui/yup_FileChooser.cpp +++ b/tests/yup_gui/yup_FileChooser.cpp @@ -19,8 +19,6 @@ ============================================================================== */ -#if 0 - #include #include @@ -29,174 +27,89 @@ using namespace yup; namespace { - // Mock component for testing - class MockComponent : public Component - { - public: - MockComponent() = default; - ~MockComponent() override = default; - }; +struct CallbackTracker +{ + bool called = false; + bool success = false; + Array results; - // Test helper to track callback invocations - struct CallbackTracker + void reset() { - bool called = false; - bool success = false; - Array results; - - void reset() - { - called = false; - success = false; - results.clear(); - } - - FileChooser::CompletionCallback makeCallback() - { - return [this] (bool callbackSuccess, const Array& callbackResults) - { - called = true; - success = callbackSuccess; - results = callbackResults; - }; - } - }; -} + called = false; + success = false; + results.clear(); + } -class FileChooserTests : public ::testing::Test -{ -protected: - void SetUp() override + FileChooser::CompletionCallback makeCallback() { - tracker.reset(); + return [this] (bool callbackSuccess, const Array& callbackResults) + { + called = true; + success = callbackSuccess; + results = callbackResults; + }; } - - CallbackTracker tracker; - MockComponent component; }; -TEST_F (FileChooserTests, ConstructorInitializesCorrectly) +File makeTemporaryFile() { - FileChooser chooser ("Test Dialog", File::getSpecialLocation (File::userHomeDirectory), "*.txt"); + auto file = File::getSpecialLocation (File::tempDirectory) + .getNonexistentChildFile ("yup_file_chooser_test", ".txt"); - // Constructor should complete without issues - EXPECT_TRUE (true); + EXPECT_TRUE (file.create().wasOk()); + return file; } +} // namespace -TEST_F (FileChooserTests, ConstructorWithEmptyFileUsesHomeDirectory) +TEST (FileChooserTests, CreateUsesHomeDirectoryWhenInitialFileIsDefault) { - FileChooser chooser ("Test Dialog"); - - // Should default to home directory when no file is specified - EXPECT_TRUE (true); + auto chooser = FileChooser::create ("Test Dialog"); + EXPECT_NE (nullptr, chooser.get()); } -TEST_F (FileChooserTests, ConstructorWithFileUsesParentDirectory) +TEST (FileChooserTests, CreateAcceptsInitialDirectoryAndFilters) { - File testFile = File::getSpecialLocation (File::userHomeDirectory).getChildFile ("test.txt"); - FileChooser chooser ("Test Dialog", testFile); - - // Should use parent directory when file is specified - EXPECT_TRUE (true); -} - -TEST_F (FileChooserTests, BrowseForFileToOpenHasCorrectSignature) -{ - FileChooser chooser ("Test Dialog"); - - // Test that the method exists and has correct signature - auto callback = tracker.makeCallback(); - - // This should compile without errors - // Note: We don't actually call it in tests since it would show a dialog - EXPECT_TRUE (true); -} - -TEST_F (FileChooserTests, BrowseForMultipleFilesToOpenHasCorrectSignature) -{ - FileChooser chooser ("Test Dialog"); - - // Test that the method exists and has correct signature - auto callback = tracker.makeCallback(); + auto chooser = FileChooser::create ("Test Dialog", + File::getSpecialLocation (File::userHomeDirectory), + "*.txt;*.doc"); - // This should compile without errors - EXPECT_TRUE (true); + EXPECT_NE (nullptr, chooser.get()); } -TEST_F (FileChooserTests, BrowseForFileToSaveHasCorrectSignature) +TEST (FileChooserTests, CreateAcceptsInitialFile) { - FileChooser chooser ("Test Dialog"); - - // Test that the method exists and has correct signature - auto callback = tracker.makeCallback(); + const auto file = makeTemporaryFile(); + const ScopeGuard deleteFile { [&file] + { + file.deleteFile(); + } }; - // This should compile without errors - EXPECT_TRUE (true); + auto chooser = FileChooser::create ("Test Dialog", file, "*.txt"); + EXPECT_NE (nullptr, chooser.get()); } -TEST_F (FileChooserTests, BrowseForDirectoryHasCorrectSignature) +TEST (FileChooserTests, CreateAcceptsPackageDirectoryOption) { - FileChooser chooser ("Test Dialog"); + auto chooser = FileChooser::create ("Test Dialog", + File::getSpecialLocation (File::userHomeDirectory), + "*", + true, + true); - // Test that the method exists and has correct signature - auto callback = tracker.makeCallback(); - - // This should compile without errors - EXPECT_TRUE (true); + EXPECT_NE (nullptr, chooser.get()); } -TEST_F (FileChooserTests, InvokeCallbackWorksCorrectly) +TEST (FileChooserTests, CompletionCallbackReceivesResults) { - FileChooser chooser ("Test Dialog"); - - Array testResults; - testResults.add (File::getSpecialLocation (File::userHomeDirectory)); + CallbackTracker tracker; + Array results; + results.add (File::getSpecialLocation (File::userHomeDirectory)); auto callback = tracker.makeCallback(); + callback (true, results); - // Test invokeCallback method - chooser.invokeCallback (std::move (callback), true, testResults); - - // Note: The callback is invoked asynchronously on the message thread - // In a real test environment, we would need to wait for message processing - // For now, we just verify the method exists and can be called - EXPECT_TRUE (true); -} - -TEST_F (FileChooserTests, GetFilePatternsForPlatformReturnsFilters) -{ - FileChooser chooser ("Test Dialog", File(), "*.txt;*.doc"); - - String patterns = chooser.getFilePatternsForPlatform(); - EXPECT_EQ (patterns, "*.txt;*.doc"); -} - -TEST_F (FileChooserTests, GetFilePatternsForPlatformReturnsEmptyWhenNoFilters) -{ - FileChooser chooser ("Test Dialog"); - - String patterns = chooser.getFilePatternsForPlatform(); - EXPECT_TRUE (patterns.isEmpty()); -} - -TEST_F (FileChooserTests, MultipleFileExtensionsAreSupported) -{ - FileChooser chooser ("Test Dialog", File(), "*.txt,*.doc;*.pdf"); - - String patterns = chooser.getFilePatternsForPlatform(); - EXPECT_EQ (patterns, "*.txt,*.doc;*.pdf"); + EXPECT_TRUE (tracker.called); + EXPECT_TRUE (tracker.success); + ASSERT_EQ (1, tracker.results.size()); + EXPECT_EQ (results[0], tracker.results[0]); } - -TEST_F (FileChooserTests, CallbackTypesAreCorrect) -{ - // Test that callback types are properly defined - FileChooser::CompletionCallback callback = [] (bool success, const Array& results) - { - // This should compile correctly - EXPECT_TRUE (true); - }; - - EXPECT_TRUE (callback != nullptr); -} - -#endif diff --git a/tests/yup_gui/yup_PopupMenu.cpp b/tests/yup_gui/yup_PopupMenu.cpp index 7e9be8431..7ec0edbcc 100644 --- a/tests/yup_gui/yup_PopupMenu.cpp +++ b/tests/yup_gui/yup_PopupMenu.cpp @@ -554,6 +554,33 @@ TEST_F (PopupMenuTest, VeryLongItemText) EXPECT_EQ (1, menu->getNumItems()); } +TEST_F (PopupMenuTest, WidthUsesTextSizeAndRespectsLimits) +{ + auto oldTheme = ApplicationTheme::getGlobalTheme(); + auto theme = createThemeVersion1(); + ApplicationTheme::setGlobalTheme (theme); + const ScopeGuard restoreTheme { [&] + { + ApplicationTheme::setGlobalTheme (oldTheme.get()); + } }; + + PopupMenu::Options options; + options.withParentComponent (parentComponent.get()) + .withPosition (Point (10, 10)) + .withMinimumWidth (120) + .withMaximumWidth (260); + + auto menu = PopupMenu::create (options); + menu->addItem ("A very long popup menu item that should be measured before the menu is shown", kPopupTestId1); + menu->show(); + + EXPECT_GE (menu->getWidth(), 120); + EXPECT_LE (menu->getWidth(), 260); + EXPECT_EQ (260, menu->getWidth()); + + menu->dismiss(); +} + TEST_F (PopupMenuTest, SpecialCharactersInText) { auto menu = PopupMenu::create(); @@ -678,6 +705,20 @@ TEST_F (PopupMenuTest, MenuWithManyItemsInSmallSpace) // The menu height should be constrained by the parent EXPECT_LE (menu->getHeight(), smallParent->getHeight()); + int laidOutItems = 0; + int hiddenItems = 0; + for (const auto& item : *menu) + { + if (item->area.isEmpty()) + ++hiddenItems; + else + ++laidOutItems; + } + + EXPECT_GT (laidOutItems, 0); + EXPECT_GT (hiddenItems, 0); + EXPECT_LT (laidOutItems, menu->getNumItems()); + // With the new approach, the menu should show even with limited space EXPECT_TRUE (menu->isVisible()); } @@ -714,12 +755,49 @@ TEST_F (PopupMenuTest, MouseWheelEventHandling) for (int i = 0; i < 5; ++i) EXPECT_NO_THROW (menu->mouseWheel (mouseEvent, wheelDown)); + EXPECT_TRUE (menu->canScrollUp()); + for (int i = 0; i < 3; ++i) EXPECT_NO_THROW (menu->mouseWheel (mouseEvent, wheelUp)); EXPECT_TRUE (menu->isVisible()); } +TEST_F (PopupMenuTest, ScrollingKeepsRowsAdjacentToScrollIndicators) +{ + auto smallParent = std::make_unique ("smallParent"); + smallParent->setBounds (0, 0, 220, 130); + + PopupMenu::Options options; + options.withParentComponent (smallParent.get()) + .withPosition (Point (10, 10)); + + auto menu = PopupMenu::create (options); + for (int i = 1; i <= 40; ++i) + menu->addItem (String ("Item ") + String (i), i); + + menu->show(); + + MouseEvent mouseEvent (MouseEvent::leftButton, KeyModifiers(), Point (50, 50)); + MouseWheelData wheelDown (0.0f, -1.0f); + for (int i = 0; i < 100; ++i) + menu->mouseWheel (mouseEvent, wheelDown); + + EXPECT_TRUE (menu->canScrollUp()); + EXPECT_FALSE (menu->canScrollDown()); + + float lastVisibleItemBottom = 0.0f; + for (const auto& item : *menu) + { + if (! item->area.isEmpty()) + lastVisibleItemBottom = jmax (lastVisibleItemBottom, item->area.getBottom()); + } + + const auto downIndicatorTop = menu->getScrollDownIndicatorBounds().getY(); + EXPECT_LE (lastVisibleItemBottom, downIndicatorTop); + EXPECT_LE (downIndicatorTop - lastVisibleItemBottom, 4.0f); +} + TEST_F (PopupMenuTest, ScrollingWithCustomComponents) { // Use small parent to trigger scrolling behavior