diff --git a/cmake/platforms/mac/AUInfo.plist b/cmake/platforms/mac/AUInfo.plist new file mode 100644 index 000000000..aa796a91f --- /dev/null +++ b/cmake/platforms/mac/AUInfo.plist @@ -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_audio_plugin.cmake b/cmake/yup_audio_plugin.cmake index d5f7ce766..6cfcc5cc9 100644 --- a/cmake/yup_audio_plugin.cmake +++ b/cmake/yup_audio_plugin.cmake @@ -27,7 +27,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 +44,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 +60,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() @@ -140,11 +145,16 @@ function (yup_audio_plugin) ${YUP_ARG_MODULES}) set_target_properties (${target_name}_clap_plugin PROPERTIES + C_VISIBILITY_PRESET hidden + CXX_VISIBILITY_PRESET hidden + OBJC_VISIBILITY_PRESET hidden + OBJCXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN ON SUFFIX ".clap" FOLDER "${YUP_ARG_TARGET_IDE_GROUP}" XCODE_GENERATE_SCHEME ON) - #yup_audio_plugin_copy_bundle (${target_name} clap) + yup_audio_plugin_copy_bundle (${target_name} clap) endif() # ==== Fetch vst3 SDK and build vst3 target @@ -152,7 +162,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} @@ -191,7 +203,7 @@ function (yup_audio_plugin) if (YUP_PLATFORM_MAC) smtg_target_set_bundle (${target_name}_vst3_plugin - BUNDLE_IDENTIFIER org.kunitoki.yup.${target_name} + BUNDLE_IDENTIFIER "${target_bundle_id}" COMPANY_NAME "kunitoki") #smtg_target_set_debug_executable(MyPlugin @@ -211,6 +223,11 @@ function (yup_audio_plugin) endif() 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) @@ -233,7 +250,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 +264,141 @@ 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/AUInfo.plist") + 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_audio_plugin_copy_bundle (${target_name} au) + 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 +410,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 +430,28 @@ 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 -f "${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") + get_target_property (source_plugin_path ${dependency_target} SMTG_PLUGIN_PACKAGE_PATH) + if (NOT source_plugin_path) + set (source_plugin_path "$") + 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}" + COMMAND codesign --force --sign - "${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_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_standalone.cmake b/cmake/yup_standalone.cmake index 053928a7b..f0e18d278 100644 --- a/cmake/yup_standalone.cmake +++ b/cmake/yup_standalone.cmake @@ -128,6 +128,13 @@ 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}) 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..43976d539 100644 --- a/examples/audiograph/source/nodes/SubgraphNode.h +++ b/examples/audiograph/source/nodes/SubgraphNode.h @@ -145,9 +145,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/ExamplePlugin.cpp b/examples/plugin/source/ExamplePlugin.cpp index 41b06aa25..acb8f44e2 100644 --- a/examples/plugin/source/ExamplePlugin.cpp +++ b/examples/plugin/source/ExamplePlugin.cpp @@ -22,12 +22,72 @@ #include "ExamplePlugin.h" #include "ExampleEditor.h" +#include + +//============================================================================== + +namespace +{ + +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") @@ -56,8 +116,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 +128,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; 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_graph/graph/yup_AudioGraphProcessor.cpp b/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.cpp index 9b79604aa..664c52b6a 100644 --- a/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.cpp +++ b/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.cpp @@ -410,11 +410,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(); @@ -886,7 +894,9 @@ 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 }; + node.processor->processBlock (nodeCtx); } void processLevels (CompiledGraph& graph, int numSamples) @@ -1267,8 +1277,10 @@ void AudioGraphProcessor::Pimpl::WorkerThread::run() //============================================================================== 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 +1358,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..848980814 --- /dev/null +++ b/modules/yup_audio_plugin_client/au/yup_audio_plugin_client_AU.cpp @@ -0,0 +1,1100 @@ +/* + ============================================================================== + + 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(); + +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 +{ + +//============================================================================== + +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 +{ +public: + //============================================================================== + + 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"); + + 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())); + + 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(); + + 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_IsWritable | kAudioUnitParameterFlag_HasCFNameString; + + 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]->isPerformingChangeGesture()) + return noErr; + + parameters[parameterIndex]->setValue (static_cast (inValue)); + 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; + } + + //============================================================================== + +#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(); + + std::vector channels; + for (UInt32 ch = 0; ch < outBufList.mNumberBuffers; ++ch) + channels.push_back (static_cast (outBufList.mBuffers[ch].mData)); + + AudioSampleBuffer audioBuffer (channels.data(), + static_cast (channels.size()), + 0, + static_cast (inNumberFrames)); + + { + std::lock_guard lock (midiMutex); + AudioProcessContext context { audioBuffer, midiBuffer, emptyParamChanges }; + processor->processBlock (context); + midiBuffer.clear(); + } + + return noErr; + } + + //============================================================================== + + OSStatus HandleMIDIEvent (UInt8 status, UInt8 channel, UInt8 data1, UInt8 data2, UInt32 offsetSampleFrame) override + { + std::lock_guard lock (midiMutex); + + 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::lock_guard lock (midiMutex); + + 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); + + std::vector channels; + 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)); + + channels.push_back (out); + } + + AudioSampleBuffer audioBuffer (channels.data(), + static_cast (channels.size()), + 0, + static_cast (inFramesToProcess)); + + AudioProcessContext context { audioBuffer, midiBuffer, emptyParamChanges }; + processor->processBlock (context); + midiBuffer.clear(); + + return noErr; + } +#endif + + //============================================================================== + + OSStatus SaveState (CFPropertyListRef* outData) override + { + YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "SaveState requested"); + + if (processor == nullptr || outData == nullptr) + { + YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "SaveState failed: processor=" << describePointer (processor.get()) << ", outData=" << describePointer (outData)); + return kAudioUnitErr_InvalidPropertyValue; + } + + MemoryBlock data; + const auto result = processor->saveStateIntoMemory (data); + if (result.failed()) + { + YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "SaveState failed: " << result.getErrorMessage()); + return kAudioUnitErr_InvalidPropertyValue; + } + + NSData* nsData = [NSData dataWithBytes:data.getData() + length:data.getSize()]; + *outData = (__bridge_retained CFPropertyListRef) nsData; + + YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "SaveState completed: bytes=" << String (static_cast (data.getSize()))); + + return noErr; + } + + OSStatus RestoreState (CFPropertyListRef inData) override + { + YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "RestoreState requested"); + + if (processor == nullptr || inData == nullptr) + { + YUP_MODULE_DBG (PLUGIN_CLIENT_AU, "RestoreState failed: processor=" << describePointer (processor.get()) << ", inData=" << describePointer (inData)); + return kAudioUnitErr_InvalidPropertyValue; + } + + NSData* nsData = (__bridge NSData*) inData; + + MemoryBlock data ([nsData bytes], [nsData length]); + + 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_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; + } + + 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(); } + + 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()))); + } + + Float64 getCurrentSampleRate() + { + return Output (0).GetStreamFormat().mSampleRate; + } + + AUScopedYupInitialiser scopeInitialiser; + std::unique_ptr processor; + + MidiBuffer midiBuffer; + ParameterChangeBuffer emptyParamChanges; // AU delivers param changes via SetParameter, not in the audio stream + std::mutex midiMutex; + std::vector channelInfoCache; + AudioUnit componentInstance = nullptr; + bool renderingOffline = false; +}; + +} // namespace yup + +//============================================================================== +// Objective-C editor view + +@interface AudioPluginEditorViewAU : NSView +{ + yup::AUScopedYupWindowingInitialiser _scopeInitialiser; + yup::AudioProcessor* _processor; + std::unique_ptr _processorEditor; +} +- (instancetype)initWithProcessor:(yup::AudioProcessor*)processor + preferredSize:(NSSize)size; +- (void)attachEditorIfNeeded; +- (void)detachEditorIfNeeded; +- (void)resizeEditorToBounds; +@end + +@implementation AudioPluginEditorViewAU + +- (instancetype)initWithProcessor:(yup::AudioProcessor*)processor + 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)) << ", processor=" << yup::describePointer (processor) << ", 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)); + _processor = processor; + + 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())); + + [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)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())); + + _processorEditor->setBounds ({ 0.0f, + 0.0f, + yup::jmax (1.0f, static_cast (NSWidth (bounds))), + yup::jmax (1.0f, static_cast (NSHeight (bounds))) }); +} + +- (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 detachEditorIfNeeded]; + + yup::endActiveParameterGestures (_processor); + + _processorEditor.reset(); + _processor = nullptr; +} + +@end + +//============================================================================== +// 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] initWithProcessor:proc->getProcessor() + 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_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..292d91f6f 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,1337 @@ -/* - ============================================================================== - - 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 + +extern "C" yup::AudioProcessor* createPluginProcessor(); + +namespace yup +{ + +//============================================================================== + +std::optional clapEventToMidiMessage (const clap_event_header_t* event) +{ + switch (event->type) + { + case CLAP_EVENT_NOTE_ON: + { + const auto* 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 auto* noteEvent = reinterpret_cast (event); + const int channel = noteEvent->channel < 0 ? 1 : noteEvent->channel + 1; + return MidiMessage::noteOff (channel, noteEvent->key, static_cast (noteEvent->velocity * 127.0f)); + } + + case CLAP_EVENT_NOTE_CHOKE: + { + const auto* noteEvent = reinterpret_cast (event); + const int channel = noteEvent->channel < 0 ? 1 : noteEvent->channel + 1; + return MidiMessage::noteOff (channel, noteEvent->key); + } + + case CLAP_EVENT_MIDI: + { + const auto* midiEvent = reinterpret_cast (event); + return MidiMessage (midiEvent->data, 3); + } + + case CLAP_EVENT_NOTE_EXPRESSION: + { + const auto* ev = reinterpret_cast (event); + const int channel = ev->channel < 0 ? 1 : ev->channel + 1; + + if (ev->expression_id == CLAP_NOTE_EXPRESSION_TUNING) + { + const int pitchBendValue = jlimit (0, 16383, static_cast (ev->value * 8192.0 + 8192.0)); + return MidiMessage::pitchWheel (channel, pitchBendValue); + } + + if (ev->expression_id == CLAP_NOTE_EXPRESSION_PRESSURE) + return MidiMessage::channelPressureChange (channel, static_cast (ev->value * 127.0)); + + if (ev->expression_id == CLAP_NOTE_EXPRESSION_BRIGHTNESS) + return MidiMessage::controllerEvent (channel, 74, static_cast (ev->value * 127.0)); + + break; + } + + case CLAP_EVENT_MIDI_SYSEX: + { + const auto* sysexEvent = reinterpret_cast (event); + 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; + + 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 + +struct CLAPScopedGuiInitialiser +{ + CLAPScopedGuiInitialiser() + { + if (numCLAPScopedGuiInitInstances.fetch_add (1) == 0) + { + initialiseYup_GUI(); + } + } + + ~CLAPScopedGuiInitialiser() + { + if (numCLAPScopedGuiInitInstances.fetch_sub (1) == 1) + { + shutdownYup_GUI(); + } + } + +private: + static std::atomic_int numCLAPScopedGuiInitInstances; +}; + +std::atomic_int CLAPScopedGuiInitialiser::numCLAPScopedGuiInitInstances = 0; + +struct CLAPScopedWindowingInitialiser +{ + CLAPScopedWindowingInitialiser() + { + if (numCLAPScopedGuiInitInstances.fetch_add (1) == 0) + { + initialiseYup_Windowing(); + } + } + + ~CLAPScopedWindowingInitialiser() + { + if (numCLAPScopedGuiInitInstances.fetch_sub (1) == 1) + { + shutdownYup_Windowing(); + } + } + +private: + static std::atomic_int numCLAPScopedGuiInitInstances; +}; + +std::atomic_int CLAPScopedWindowingInitialiser::numCLAPScopedGuiInitInstances = 0; + +//============================================================================== + +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: + CLAPScopedWindowingInitialiser scopedWindowingInitialiser; + + 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: + CLAPScopedGuiInitialiser scopedGuiInitialiser; + + 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; + + 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()); + + // Process incoming parameter and MIDI events (CLAP guarantees time-sorted order) + midiBuffer.clear(); + wrapper->paramChangeBuffer.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 (event->type == CLAP_EVENT_PARAM_VALUE) + { + const auto* paramEvent = reinterpret_cast (event); + const auto paramIndex = audioProcessor.getParameterIndexByHostID (paramEvent->param_id); + + if (isPositiveAndBelow (paramIndex, static_cast (audioProcessor.getParameters().size()))) + { + wrapper->paramChangeBuffer.addChange (paramIndex, + static_cast (paramEvent->value), + static_cast (event->time)); + } + } + 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 + for (const auto& change : wrapper->paramChangeBuffer) + audioProcessor.getParameters()[change.parameterIndex]->setNormalizedValue (change.normalizedValue); + + // Copy input audio into output buffers for effect processors + 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)); + } + } + + // Build flat channel pointer array across all output buses + std::vector outputChannels; + for (uint32_t busIdx = 0; busIdx < process->audio_outputs_count; ++busIdx) + for (uint32_t ch = 0; ch < process->audio_outputs[busIdx].channel_count; ++ch) + outputChannels.push_back (process->audio_outputs[busIdx].data32[ch]); + + AudioSampleBuffer audioBuffer (outputChannels.data(), + static_cast (outputChannels.size()), + 0, + static_cast (process->frames_count)); + + AudioPluginPlayHeadCLAP playHead (audioProcessor.getSampleRate(), process); + audioProcessor.setPlayHead (&playHead); + + const int64_t samplePosition = (process->transport != nullptr) + ? static_cast (process->transport->song_pos_seconds + * audioProcessor.getSampleRate()) + : 0; + + AudioProcessContext context { audioBuffer, midiBuffer, wrapper->paramChangeBuffer, samplePosition }; + audioProcessor.processBlock (context); + + audioProcessor.setPlayHead (nullptr); + + // Send output events back to host + 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) {}; +} + +//============================================================================== + +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()); + }; + + 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 = parameter->getHostParameterID(); + 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(); + + const auto parameterIndex = wrapper->audioProcessor->getParameterIndexByHostID (parameterId); + if (! isPositiveAndBelow (parameterIndex, static_cast (parameters.size()))) + 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()))) + 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()))) + 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); + const uint32_t count = in->size (in); + + for (uint32_t i = 0; i < count; ++i) + clapEventToParameterChange (in->get (in, i), *wrapper->audioProcessor); + }; + + // ==== 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(); + 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; + + wrapper->audioProcessor->suspendProcessing (true); + const bool saved = wrapper->audioProcessor->saveStateIntoMemory (data).wasOk(); + wrapper->audioProcessor->suspendProcessing (false); + + if (! saved) + return false; + + return stream->write (stream, data.getData(), static_cast (data.getSize())) + == static_cast (data.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) + break; + data.append (buf, static_cast (n)); + } + + if (data.isEmpty()) + return false; + + wrapper->audioProcessor->suspendProcessing (true); + const bool ok = wrapper->audioProcessor->loadStateFromMemory (data).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); + 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); + + midiEvents.ensureSize (4096); + paramChangeBuffer.reserve (static_cast (audioProcessor->getParameters().size()) * 4 + 32); + + 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); + 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::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*) -> bool + { + return true; + }; + + plugin.deinit = [] {}; + + 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; +}(); 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..616ad4327 --- /dev/null +++ b/modules/yup_audio_plugin_client/common/yup_AudioPluginUtilities.h @@ -0,0 +1,41 @@ +/* + ============================================================================== + + 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 + +//============================================================================== +namespace yup +{ + +/** 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..8e883a548 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 @@ -43,7 +46,9 @@ #include #include +#include #include +#include //============================================================================== @@ -57,8 +62,6 @@ using namespace Steinberg; namespace { -//============================================================================== - FUID toFUID (const String& source) { const auto uid = Uuid::fromSHA1 (SHA1 (source.toUTF8())); @@ -94,6 +97,26 @@ 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) +{ + auto parameterID = static_cast (processor.getParameters().size()); + + while (processor.getParameterByHostID (parameterID) != nullptr + && parameterID < AudioParameter::maximumHostParameterID) + { + ++parameterID; + } + + jassert (parameterID <= AudioParameter::maximumHostParameterID); + jassert (processor.getParameterByHostID (parameterID) == nullptr); + return static_cast (parameterID); +} + //============================================================================== static std::atomic_int numScopedInitInstancesGui = 0; @@ -128,6 +151,23 @@ 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; + } +} + +//============================================================================== + class AudioPluginEditorViewVST3 : public Component , public Vst::EditorView @@ -149,10 +189,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 +204,7 @@ class AudioPluginEditorViewVST3 { if (editor != nullptr) { + endActiveParameterGestures (processor); setVisible (false); removeFromDesktop(); @@ -181,10 +220,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 +259,7 @@ class AudioPluginEditorViewVST3 { if (editor != nullptr) { + endActiveParameterGestures (processor); setVisible (false); removeFromDesktop(); } @@ -274,10 +314,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 +331,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; @@ -355,6 +393,7 @@ class AudioPluginControllerVST3 , public Vst::IUnitInfo , public Vst::IRemapParamID , public Vst::ChannelContext::IInfoListener + , private AudioParameter::Listener { public: //============================================================================== @@ -387,6 +426,7 @@ class AudioPluginControllerVST3 ~AudioPluginControllerVST3() { + removeParameterListeners(); } //============================================================================== @@ -402,6 +442,9 @@ class AudioPluginControllerVST3 tresult PLUGIN_API terminate() override { + removeParameterListeners(); + processor = nullptr; + return Vst::EditController::terminate(); } @@ -409,13 +452,14 @@ 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(); processor = nullptr; - return kResultTrue; + return Vst::EditController::disconnect (other); } tresult PLUGIN_API notify (Vst::IMessage* message) override @@ -460,22 +504,11 @@ class AudioPluginControllerVST3 //============================================================================== - int32 PLUGIN_API getParameterCount() override - { - if (processor == nullptr) - return 0; - - return static_cast (processor->getParameters().size()); - } - 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 +523,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 +546,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 +570,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 +584,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 +598,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 +612,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 +632,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 +649,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; } //============================================================================== @@ -765,6 +820,9 @@ class AudioPluginControllerVST3 private: void setupParameters() { + removeParameterListeners(); + parameters.removeAll(); + if (processor == nullptr) return; @@ -774,17 +832,70 @@ class AudioPluginControllerVST3 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 + 0, // step count + parameter->getNormalizedValue(), // normalized value + Vst::ParameterInfo::kCanAutomate, // 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 parameterValueChanged (const AudioParameter::Ptr& parameter, int indexInContainer) override + { + if (! isValidProcessorParameterIndex (indexInContainer)) + return; + + const auto tag = getVST3ParameterID (parameter); + const auto normalizedValue = static_cast (parameter->getNormalizedValue()); + + Vst::EditController::setParamNormalized (tag, normalizedValue); + Vst::EditController::performEdit (tag, normalizedValue); + } + + void parameterGestureBegin (const AudioParameter::Ptr&, int indexInContainer) override + { + if (isValidProcessorParameterIndex (indexInContainer)) + Vst::EditController::beginEdit (getVST3ParameterID (processor->getParameters()[indexInContainer])); + } + + void parameterGestureEnd (const AudioParameter::Ptr&, int indexInContainer) override + { + if (isValidProcessorParameterIndex (indexInContainer)) + Vst::EditController::endEdit (getVST3ParameterID (processor->getParameters()[indexInContainer])); + } + + bool isValidProcessorParameterIndex (int indexInContainer) const + { + return processor != nullptr + && isPositiveAndBelow (indexInContainer, static_cast (processor->getParameters().size())); } AudioProcessor* processor = nullptr; + std::vector listenedParameters; }; //============================================================================== @@ -803,6 +914,7 @@ class AudioPluginProcessorVST3 : public Vst::AudioEffect virtual ~AudioPluginProcessorVST3() { + endActiveParameterGestures (processor.get()); processor.reset(); } @@ -824,17 +936,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 +965,10 @@ class AudioPluginProcessorVST3 : public Vst::AudioEffect tresult PLUGIN_API terminate() override { if (processor != nullptr) + { + endActiveParameterGestures (processor.get()); processor->releaseResources(); + } return AudioEffect::terminate(); } @@ -891,17 +1016,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 != audioInputCount || numOuts != audioOutputCount) + return kResultFalse; + + int32 idx = 0; + for (const auto& bus : busLayout.getInputBuses()) + { + if (bus.getType() != AudioBus::Type::Audio) + continue; + if (Vst::SpeakerArr::getChannelCount (inputs[idx]) != bus.getNumChannels()) + return kResultFalse; + ++idx; + } - if (numIns == 1 - && numOuts == 1 - && inputs[0] == Vst::SpeakerArr::kStereo - && outputs[0] == Vst::SpeakerArr::kStereo) + idx = 0; + for (const auto& bus : busLayout.getOutputBuses()) { - return kResultOk; + if (bus.getType() != AudioBus::Type::Audio) + continue; + if (Vst::SpeakerArr::getChannelCount (outputs[idx]) != bus.getNumChannels()) + return kResultFalse; + ++idx; } - return kResultFalse; + return kResultOk; } //============================================================================== @@ -921,6 +1072,39 @@ class AudioPluginProcessorVST3 : public Vst::AudioEffect //============================================================================== + tresult PLUGIN_API getState (IBStream* stream) override + { + if (processor == nullptr || stream == nullptr) + return kResultFalse; + + MemoryBlock data; + if (processor->saveStateIntoMemory (data).failed()) + return kResultFalse; + + int32 written = 0; + return stream->write (data.getData(), static_cast (data.getSize()), &written); + } + + tresult PLUGIN_API setState (IBStream* stream) override + { + if (processor == nullptr || stream == nullptr) + return kResultFalse; + + MemoryBlock data; + char buf[4096]; + int32 bytesRead = 0; + + while (stream->read (buf, sizeof (buf), &bytesRead) == kResultOk && bytesRead > 0) + data.append (buf, static_cast (bytesRead)); + + if (data.isEmpty()) + return kResultFalse; + + return processor->loadStateFromMemory (data).wasOk() ? kResultOk : kResultFalse; + } + + //============================================================================== + tresult PLUGIN_API setupProcessing (Vst::ProcessSetup& setup) override { if (processor == nullptr) @@ -929,17 +1113,13 @@ 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 (static_cast (processor->getParameters().size()) * 4 + 32); + return kResultOk; } @@ -948,27 +1128,65 @@ 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 parameters = processor->getParameters(); + 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 parameterIndex = processor->getParameterIndexByHostID (static_cast (tag)); + if (! isPositiveAndBelow (parameterIndex, static_cast (parameters.size()))) + continue; + + if (parameters[parameterIndex]->isPerformingChangeGesture()) + continue; + + for (int32 p = 0; p < numPoints; ++p) + { + int32 sampleOffset; + Vst::ParamValue value; + if (queue->getPoint (p, sampleOffset, value) == kResultOk) + paramChangeBuffer.addChange (parameterIndex, static_cast (value), sampleOffset); + } + } } + + paramChangeBuffer.sort(); + for (const auto& change : paramChangeBuffer) + parameters[change.parameterIndex]->setNormalizedValue (change.normalizedValue); } // --- Process Events --- @@ -986,23 +1204,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,14 +1245,67 @@ class AudioPluginProcessorVST3 : public Vst::AudioEffect // --- Process Audio --- if (data.numSamples > 0 && data.outputs != nullptr) { - Vst::AudioBusBuffers& outBus = data.outputs[0]; + // 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) + { + 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)); + } + } + } + + // Build a flat channel pointer array across all output buses + std::vector outputChannels; + for (int32 busIdx = 0; busIdx < data.numOutputs; ++busIdx) + for (int32 ch = 0; ch < data.outputs[busIdx].numChannels; ++ch) + outputChannels.push_back (reinterpret_cast (data.outputs[busIdx].channelBuffers32[ch])); + + const int64_t samplePosition = (data.processContext != nullptr) + ? data.processContext->projectTimeSamples + : 0; + + if (processSetup.symbolicSampleSize == Vst::kSample64 && processor->supportsDoublePrecisionProcessing()) + { + std::vector outputChannels64; + for (int32 busIdx = 0; busIdx < data.numOutputs; ++busIdx) + for (int32 ch = 0; ch < data.outputs[busIdx].numChannels; ++ch) + outputChannels64.push_back (reinterpret_cast (data.outputs[busIdx].channelBuffers64[ch])); + + AudioBuffer audioBuffer (outputChannels64.data(), + static_cast (outputChannels64.size()), + 0, + data.numSamples); - AudioSampleBuffer audioBuffer ( - reinterpret_cast (outBus.channelBuffers32), - outBus.numChannels, - data.numSamples); + AudioProcessContext doubleCtx { audioBuffer, midiBuffer, paramChangeBuffer, samplePosition }; - processor->processBlock (audioBuffer, midiBuffer); + if (bypassed) + processor->processBlockBypassed (doubleCtx); + else + processor->processBlock (doubleCtx); + } + else + { + AudioSampleBuffer audioBuffer (outputChannels.data(), + static_cast (outputChannels.size()), + 0, + data.numSamples); + + AudioProcessContext context { audioBuffer, midiBuffer, paramChangeBuffer, samplePosition }; + + if (bypassed) + processor->processBlockBypassed (context); + else + processor->processBlock (context); + } } return kResultOk; @@ -1036,10 +1320,12 @@ class AudioPluginProcessorVST3 : public Vst::AudioEffect Vst::ProcessSetup processSetup; MidiBuffer midiBuffer; + ParameterChangeBuffer paramChangeBuffer; + 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 +1345,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_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..5d9498ee0 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, @@ -435,13 +436,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; } @@ -1194,6 +1203,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, @@ -1272,10 +1295,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 +1312,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..9d561e04a 100644 --- a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.cpp +++ b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.cpp @@ -76,7 +76,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 +108,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 +152,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 +163,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; @@ -332,6 +645,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 +658,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(); @@ -498,7 +816,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 +967,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 +1003,7 @@ class CLAPInstance : public AudioPluginInstance std::vector preparedOutPtrs; MidiBuffer outputMidiBuffer; std::vector clapParameterIds; + clap_plugin_render_mode currentRenderMode = -1; int currentPreset = 0; }; @@ -709,15 +1053,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 +1155,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 +1171,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..ce383c9b5 100644 --- a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.cpp +++ b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.cpp @@ -592,6 +592,8 @@ class HostComponentHandler : public Vst::IComponentHandler { public: using RestartCallback = std::function; + using ParameterGestureCallback = std::function; + using ParameterEditCallback = std::function; HostComponentHandler() { @@ -602,14 +604,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, Vst::ParamValue) override { return kResultOk; } + tresult PLUGIN_API performEdit (Vst::ParamID tag, Vst::ParamValue value) override + { + if (performEditCallback != nullptr) + performEditCallback (tag, value); + + 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 +641,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 +915,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 +926,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 +962,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 +1038,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.samplePosition }; + processBlock (doubleCtx); const int numChannels = jmin (audioBuffer.getNumChannels(), doublePrecisionBuffer.getNumChannels()); const int numSamples = jmin (audioBuffer.getNumSamples(), doublePrecisionBuffer.getNumSamples()); @@ -1010,15 +1065,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); prepareMidiInputEvents (midiBuffer); // Input busses @@ -1046,21 +1100,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 +1122,7 @@ class VST3Instance : public AudioPluginInstance } Vst::ProcessData data {}; - prepareProcessData (data, audioBuffer.getNumSamples(), Vst::kSample64); + prepareProcessData (data, audioBuffer.getNumSamples(), Vst::kSample64, context.params); prepareMidiInputEvents (midiBuffer); Vst::AudioBusBuffers inputBus {}; @@ -1103,6 +1155,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 +1413,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 +1518,28 @@ 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) { 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); } } @@ -1536,6 +1616,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; }; //============================================================================== @@ -1588,13 +1687,21 @@ ResultValue> VST3Format::scanFile (const Fil && ! file.isDirectory()) 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 +1731,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 +1760,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 +1776,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..8e3d6e0b7 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,10 @@ #if YUP_AUDIO_PLUGIN_HOST_ENABLE_CLAP #include +#if YUP_MAC +#import +#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..8dfa29e3b 100644 --- a/modules/yup_audio_processors/processors/yup_AudioParameter.cpp +++ b/modules/yup_audio_processors/processors/yup_AudioParameter.cpp @@ -49,9 +49,11 @@ AudioParameter::AudioParameter (const String& id, ValueToString valueToString, StringToValue stringToValue, bool smoothingEnabled, - float smoothingTimeMs) + float smoothingTimeMs, + uint32 hostParameterID) : paramID (id) , paramName (name) + , hostParameterID (hostParameterID) , valueRange (minValue, maxValue) , defaultValue (defaultValue) , valueToString (valueToString ? valueToString : defaultToString) @@ -59,6 +61,8 @@ AudioParameter::AudioParameter (const String& id, , smoothingEnabled (smoothingEnabled) , smoothingTimeMs (smoothingTimeMs) { + jassert (hostParameterID == invalidHostParameterID || hostParameterID <= maximumHostParameterID); + setValue (defaultValue); } @@ -69,9 +73,11 @@ AudioParameter::AudioParameter (const String& id, ValueToString valueToString, StringToValue stringToValue, bool smoothingEnabled, - float smoothingTimeMs) + float smoothingTimeMs, + uint32 hostParameterID) : paramID (id) , paramName (name) + , hostParameterID (hostParameterID) , valueRange (std::move (valueRange)) , defaultValue (defaultValue) , valueToString (valueToString ? valueToString : defaultToString) @@ -79,6 +85,8 @@ AudioParameter::AudioParameter (const String& id, , smoothingEnabled (smoothingEnabled) , smoothingTimeMs (smoothingTimeMs) { + jassert (hostParameterID == invalidHostParameterID || hostParameterID <= maximumHostParameterID); + setValue (defaultValue); } @@ -91,19 +99,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..f89098e8d 100644 --- a/modules/yup_audio_processors/processors/yup_AudioParameter.h +++ b/modules/yup_audio_processors/processors/yup_AudioParameter.h @@ -48,6 +48,17 @@ 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; + + /** + Highest host-facing parameter ID that is portable across VST3, AUv2, and CLAP. + + 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. + */ + static constexpr uint32 maximumHostParameterID = 0x7fffffffu; + //============================================================================== /** @@ -60,6 +71,9 @@ class AudioParameter : public ReferenceCountedObject @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 hostParameterID Optional stable host-facing automation ID. Leave this + as invalidHostParameterID to use the parameter's + addParameter() index for backward compatibility. */ AudioParameter (const String& id, const String& name, @@ -69,7 +83,8 @@ class AudioParameter : public ReferenceCountedObject ValueToString valueToString = nullptr, StringToValue stringToValue = nullptr, bool smoothingEnabled = false, - float smoothingTimeMs = 0.0f); + float smoothingTimeMs = 0.0f, + uint32 hostParameterID = invalidHostParameterID); /** Constructs an AudioParameter instance. @@ -80,6 +95,9 @@ class AudioParameter : public ReferenceCountedObject @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 hostParameterID Optional stable host-facing automation ID. Leave this + as invalidHostParameterID to use the parameter's + addParameter() index for backward compatibility. */ AudioParameter (const String& id, const String& name, @@ -88,7 +106,8 @@ class AudioParameter : public ReferenceCountedObject ValueToString valueToString = nullptr, StringToValue stringToValue = nullptr, bool smoothingEnabled = false, - float smoothingTimeMs = 0.0f); + float smoothingTimeMs = 0.0f, + uint32 hostParameterID = invalidHostParameterID); /** Destructor. */ ~AudioParameter(); @@ -101,6 +120,28 @@ class AudioParameter : public ReferenceCountedObject /** Returns the parameter name. */ const String& getName() const { return paramName; } + /** + 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 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() + ? hostParameterID + : (paramIndex >= 0 ? static_cast (paramIndex) : invalidHostParameterID); + } + //============================================================================== int getIndexInContainer() const { return paramIndex; } @@ -124,7 +165,7 @@ class AudioParameter : public ReferenceCountedObject void endChangeGesture(); - bool isPerformingChangeGesture() const { return isInsideGesture != 0; } + bool isPerformingChangeGesture() const { return isInsideGesture.load() != 0; } //============================================================================== @@ -231,6 +272,7 @@ class AudioParameter : public ReferenceCountedObject String paramID; String paramName; + uint32 hostParameterID = invalidHostParameterID; int paramVersion = 0; int paramIndex = -1; std::atomic currentValue = 0.0f; @@ -241,7 +283,7 @@ class AudioParameter : public ReferenceCountedObject 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..959ee4c8a 100644 --- a/modules/yup_audio_processors/processors/yup_AudioParameterBuilder.cpp +++ b/modules/yup_audio_processors/processors/yup_AudioParameterBuilder.cpp @@ -36,6 +36,14 @@ AudioParameterBuilder& AudioParameterBuilder::withName (const String& paramName) return *this; } +AudioParameterBuilder& AudioParameterBuilder::withHostID (uint32 hostParameterID) +{ + jassert (hostParameterID <= AudioParameter::maximumHostParameterID); + + this->hostParameterID = hostParameterID; + return *this; +} + AudioParameterBuilder& AudioParameterBuilder::withRange (float minValue, float maxValue) { valueRange = { minValue, maxValue }; @@ -87,7 +95,8 @@ AudioParameter::Ptr AudioParameterBuilder::build() const std::move (valueToString), std::move (stringToValue), smoothingEnabled, - smoothingTimeMs)); + smoothingTimeMs, + hostParameterID)); } } // namespace yup diff --git a/modules/yup_audio_processors/processors/yup_AudioParameterBuilder.h b/modules/yup_audio_processors/processors/yup_AudioParameterBuilder.h index bae7e0946..e612de95d 100644 --- a/modules/yup_audio_processors/processors/yup_AudioParameterBuilder.h +++ b/modules/yup_audio_processors/processors/yup_AudioParameterBuilder.h @@ -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. @@ -98,6 +108,7 @@ class AudioParameterBuilder private: String id; String name; + uint32 hostParameterID = AudioParameter::invalidHostParameterID; NormalisableRange valueRange = { 0.0f, 1.0f }; float defaultValue = 0.5f; bool smoothingEnabled = false; 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..fb7f27bed --- /dev/null +++ b/modules/yup_audio_processors/processors/yup_AudioProcessContext.h @@ -0,0 +1,64 @@ +/* + ============================================================================== + + 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, and + - the global transport sample position. + + 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 samplePosition fields. Processors that implement sample-accurate + automation should use AudioParameterHandle::prepareBlock() and + AudioParameterHandle::advanceToSample() together with the @c params buffer. + + @see AudioProcessor, 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; + + /** Global sample position at the start of this block (from the transport). */ + int64_t samplePosition = 0; +}; + +} // namespace yup diff --git a/modules/yup_audio_processors/processors/yup_AudioProcessor.cpp b/modules/yup_audio_processors/processors/yup_AudioProcessor.cpp index 05f89f78f..1e25b4e3d 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); @@ -146,18 +171,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..2f1f8b46d 100644 --- a/modules/yup_audio_processors/processors/yup_AudioProcessor.h +++ b/modules/yup_audio_processors/processors/yup_AudioProcessor.h @@ -87,6 +87,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 +129,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 + position via @c context.samplePosition. - @param audioBuffer The audio buffer to process. - @param midiBuffer The MIDI buffer to process. + The base-class implementation asserts false so unoverridden processors are + caught at runtime in debug builds. + + @param context All per-block inputs: audio, MIDI, parameter changes, 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. + + Override this and return true from supportsDoublePrecisionProcessing() to + support 64-bit audio. The default implementation does nothing. - @param audioBuffer The audio buffer to process. - @param midiBuffer The MIDI buffer to process. + @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,6 +210,18 @@ 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; } + + //============================================================================== + + /** Returns true when the processor is running in offline (non-realtime) mode. */ + bool isOfflineProcessing() const noexcept { return offlineProcessing.load(); } + + /** Called by the plugin wrapper to indicate offline vs. realtime rendering. */ + void setOfflineProcessing (bool offline) { offlineProcessing.store (offline); } + //============================================================================== void setPlayHead (AudioPlayHead* playHead); @@ -267,6 +291,7 @@ class YUP_API AudioProcessor std::vector parameters; std::unordered_map parameterMap; + std::unordered_map parameterHostIDMap; ListenerList> listeners; AudioBusLayout busLayout; @@ -280,6 +305,7 @@ class YUP_API AudioProcessor 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..996fa3195 --- /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 }; + 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_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/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..96ee2af39 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) @@ -1740,28 +2018,57 @@ void Desktop::setCurrentMouseLocation (const Point& location) void 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,51 @@ 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_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"); + } // Unregister theme { const MessageManagerLock mmLock; ApplicationTheme::setGlobalTheme (nullptr); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: unregistered default theme"); } // Unregister event loop if (auto messageManager = MessageManager::getInstanceWithoutCreating()) + { messageManager->registerEventLoopCallback (nullptr); + YUP_MODULE_DBG (GUI_WINDOWING, "SDL2: unregistered event loop callback"); + } - // Quit SDL - SDL_Quit(); + // 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 } } // 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..6e181d028 --- /dev/null +++ b/tests/yup_audio_processors.cpp @@ -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_processors/yup_AudioParameter.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..9415656ef --- /dev/null +++ b/tests/yup_audio_processors/yup_AudioParameter.cpp @@ -0,0 +1,112 @@ +/* + ============================================================================== + + 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() + : AudioProcessor ("Test", AudioBusLayout ({}, {})) + { + } + + void prepareToPlay (float, int) override {} + + void releaseResources() override {} + + 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; } +}; + +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()); +} diff --git a/tests/yup_gui/yup_ApplicationTheme.cpp b/tests/yup_gui/yup_ApplicationTheme.cpp index 7f4a57805..6b977481d 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,82 @@ 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, 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, 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_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