From 045467348375a9b2d0d5ca9baaa930e12097c671 Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:26:48 +0200 Subject: [PATCH 01/23] WIP: AUv3 --- CMakeLists.txt | 2 +- cmake/make_clapfirst.cmake | 68 +- cmake/shared_prologue.cmake | 4 + cmake/top_level_default.cmake | 27 + cmake/wrap_auv3.cmake | 276 ++++++ cmake/wrap_auv3_standalone.cmake | 168 ++++ cmake/wrapper_functions.cmake | 2 + src/detail/auv3/auv3.entitlements | 16 + src/detail/auv3/auv3_audiounit.h | 58 ++ src/detail/auv3/auv3_audiounit.mm | 862 ++++++++++++++++++ src/detail/auv3/auv3_factory.h | 17 + src/detail/auv3/auv3_parameters.h | 29 + src/detail/auv3/auv3_parameters.mm | 205 +++++ .../auv3/build-helper/auv3_infoplist_top.in | 30 + src/detail/auv3/build-helper/build-helper.cpp | 441 +++++++++ src/detail/auv3/process.h | 115 +++ src/detail/auv3/process.mm | 524 +++++++++++ .../macos/auv3/AUv3HostAppDelegate.h | 9 + .../macos/auv3/AUv3HostAppDelegate.mm | 627 +++++++++++++ .../standalone/macos/auv3/Info.plist.in | 33 + src/detail/standalone/macos/auv3/MainMenu.xib | 100 ++ src/wrapasauv3.mm | 19 + src/wrapasauv3standalone.mm | 17 + tests/clap-first-example/CMakeLists.txt | 2 +- 24 files changed, 3648 insertions(+), 3 deletions(-) create mode 100644 cmake/wrap_auv3.cmake create mode 100644 cmake/wrap_auv3_standalone.cmake create mode 100644 src/detail/auv3/auv3.entitlements create mode 100644 src/detail/auv3/auv3_audiounit.h create mode 100644 src/detail/auv3/auv3_audiounit.mm create mode 100644 src/detail/auv3/auv3_factory.h create mode 100644 src/detail/auv3/auv3_parameters.h create mode 100644 src/detail/auv3/auv3_parameters.mm create mode 100644 src/detail/auv3/build-helper/auv3_infoplist_top.in create mode 100644 src/detail/auv3/build-helper/build-helper.cpp create mode 100644 src/detail/auv3/process.h create mode 100644 src/detail/auv3/process.mm create mode 100644 src/detail/standalone/macos/auv3/AUv3HostAppDelegate.h create mode 100644 src/detail/standalone/macos/auv3/AUv3HostAppDelegate.mm create mode 100644 src/detail/standalone/macos/auv3/Info.plist.in create mode 100644 src/detail/standalone/macos/auv3/MainMenu.xib create mode 100644 src/wrapasauv3.mm create mode 100644 src/wrapasauv3standalone.mm diff --git a/CMakeLists.txt b/CMakeLists.txt index 533981e7..016c62ba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,7 +26,7 @@ cmake_minimum_required(VERSION 3.21) cmake_policy(SET CMP0091 NEW) cmake_policy(SET CMP0149 NEW) if (NOT DEFINED CMAKE_OSX_DEPLOYMENT_TARGET) - message(STATUS "[clap-wrapper]: OSX_DEPLOYEMNT_TARGET is undefined. Setting to 10.13") + message(STATUS "[clap-wrapper]: OSX_DEPLOYMENT_TARGET is undefined. Setting to 10.13") set(CMAKE_OSX_DEPLOYMENT_TARGET 10.13 CACHE STRING "Minimum macOS version") endif() if (NOT DEFINED CMAKE_OSX_ARCHITECTURES) diff --git a/cmake/make_clapfirst.cmake b/cmake/make_clapfirst.cmake index 14c97248..c03e06db 100644 --- a/cmake/make_clapfirst.cmake +++ b/cmake/make_clapfirst.cmake @@ -76,7 +76,7 @@ function(make_clapfirst_plugins) if (ANY_WASM_TOOLCHAIN) set(C1ST_PLUGIN_FORMATS WCLAP) else() - set(C1ST_PLUGIN_FORMATS CLAP VST3 AUV2 AAX) + set(C1ST_PLUGIN_FORMATS CLAP VST3 AUV2 AUV3 AAX) endif() endif() @@ -84,12 +84,14 @@ function(make_clapfirst_plugins) set(BUILD_CLAP -1) set(BUILD_VST3 -1) set(BUILD_AUV2 -1) + set(BUILD_AUV3 -1) set(BUILD_AAX -1) list(FIND C1ST_PLUGIN_FORMATS "WCLAP" BUILD_WCLAP) else() list(FIND C1ST_PLUGIN_FORMATS "CLAP" BUILD_CLAP) list(FIND C1ST_PLUGIN_FORMATS "VST3" BUILD_VST3) list(FIND C1ST_PLUGIN_FORMATS "AUV2" BUILD_AUV2) + list(FIND C1ST_PLUGIN_FORMATS "AUV3" BUILD_AUV3) list(FIND C1ST_PLUGIN_FORMATS "AAX" BUILD_AAX) set(BUILD_WCLAP -1) @@ -215,6 +217,70 @@ function(make_clapfirst_plugins) add_dependencies(${ALL_TARGET} ${AUV2_TARGET}) endif() + if (APPLE AND ${BUILD_AUV3} GREATER -1) + message(STATUS "clap-wrapper: ClapFirst is making an AUv3") + set(AUV3_TARGET ${C1ST_TARGET_NAME}_auv3) + add_library(${AUV3_TARGET} MODULE) + target_sources(${AUV3_TARGET} PRIVATE ${C1ST_ENTRY_SOURCE}) + target_link_libraries(${AUV3_TARGET} PRIVATE ${C1ST_IMPL_TARGET}) + if (DEFINED C1ST_AUV2_MANUFACTURER_CODE) + target_add_auv3_wrapper( + TARGET ${AUV3_TARGET} + OUTPUT_NAME "${C1ST_OUTPUT_NAME}" + BUNDLE_IDENTIFIER "${C1ST_BUNDLE_IDENTIFER}.auv3" + BUNDLE_VERSION "${C1ST_BUNDLE_VERSION}" + RESOURCE_DIRECTORY "${C1ST_RESOURCE_DIRECTORY}" + + MANUFACTURER_NAME "${C1ST_AUV2_MANUFACTURER_NAME}" + MANUFACTURER_CODE "${C1ST_AUV2_MANUFACTURER_CODE}" + SUBTYPE_CODE "${C1ST_AUV2_SUBTYPE_CODE}" + INSTRUMENT_TYPE "${C1ST_AUV2_INSTRUMENT_TYPE}" + ) + else() + target_add_auv3_wrapper( + TARGET ${AUV3_TARGET} + OUTPUT_NAME "${C1ST_OUTPUT_NAME}" + BUNDLE_IDENTIFIER "${C1ST_BUNDLE_IDENTIFER}.auv3" + BUNDLE_VERSION "${C1ST_BUNDLE_VERSION}" + RESOURCE_DIRECTORY "${C1ST_RESOURCE_DIRECTORY}" + + CLAP_TARGET_FOR_CONFIG "${CLAP_TARGET}" + ) + endif() + + if (DEFINED C1ST_ASSET_OUTPUT_DIRECTORY) + set_target_properties(${AUV3_TARGET} PROPERTIES + LIBRARY_OUTPUT_DIRECTORY ${C1ST_ASSET_OUTPUT_DIRECTORY}) + endif() + + add_dependencies(${ALL_TARGET} ${AUV3_TARGET}) + + # AUv3 Standalone host app (embeds the .appex) + if (DEFINED C1ST_AUV2_MANUFACTURER_CODE) + set(AUV3SA_TARGET ${C1ST_TARGET_NAME}_auv3_standalone) + message(STATUS "clap-wrapper: ClapFirst is making an AUv3 Standalone") + add_executable(${AUV3SA_TARGET}) + target_add_auv3_standalone_wrapper( + TARGET ${AUV3SA_TARGET} + OUTPUT_NAME "${C1ST_OUTPUT_NAME} AUv3" + BUNDLE_IDENTIFIER "${C1ST_BUNDLE_IDENTIFER}.auv3standalone" + BUNDLE_VERSION "${C1ST_BUNDLE_VERSION}" + AUV3_TARGET ${AUV3_TARGET} + AU_TYPE "${C1ST_AUV2_INSTRUMENT_TYPE}" + AU_SUBTYPE "${C1ST_AUV2_SUBTYPE_CODE}" + AU_MANUFACTURER "${C1ST_AUV2_MANUFACTURER_CODE}" + MACOS_ICON "${C1ST_STANDALONE_MACOS_ICON}" + ) + + if (DEFINED C1ST_ASSET_OUTPUT_DIRECTORY) + set_target_properties(${AUV3SA_TARGET} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${C1ST_ASSET_OUTPUT_DIRECTORY}) + endif() + + add_dependencies(${ALL_TARGET} ${AUV3SA_TARGET}) + endif() + endif() + ## ---------------------- if (CLAP_WRAPPER_CAN_BUILD_AAX AND ${BUILD_AAX} GREATER -1) message(STATUS "clap-wrapper: ClapFirst is making an AAX") diff --git a/cmake/shared_prologue.cmake b/cmake/shared_prologue.cmake index 942bae20..aea7028f 100644 --- a/cmake/shared_prologue.cmake +++ b/cmake/shared_prologue.cmake @@ -172,6 +172,10 @@ function(target_copy_after_build) set(postfix "component") set(macdir "Components") set(lindir ".component") + elseif (${CAB_FLAVOR} STREQUAL "auv3") + set(postfix "appex") + set(macdir "Components") + set(lindir ".appex") elseif (${CAB_FLAVOR} STREQUAL "clap") set(postfix "clap") set(macdir "CLAP") diff --git a/cmake/top_level_default.cmake b/cmake/top_level_default.cmake index aeebf73f..eed9226e 100644 --- a/cmake/top_level_default.cmake +++ b/cmake/top_level_default.cmake @@ -50,6 +50,33 @@ if (PROJECT_IS_TOP_LEVEL) endif() endif() + if (${CLAP_WRAPPER_BUILD_AUV3}) + add_library(${pluginname}_as_auv3 MODULE) + target_add_auv3_wrapper( + TARGET ${pluginname}_as_auv3 + OUTPUT_NAME "${CLAP_WRAPPER_OUTPUT_NAME}" + BUNDLE_IDENTIFIER "${CLAP_WRAPPER_BUNDLE_IDENTIFIER}" + BUNDLE_VERSION "${CLAP_WRAPPER_BUNDLE_VERSION}" + + INSTRUMENT_TYPE "aumu" + MANUFACTURER_NAME "schnuf.org" + MANUFACTURER_CODE "clAA" + SUBTYPE_CODE "gWwp" + ) + + add_executable(${pluginname}_as_auv3_standalone) + target_add_auv3_standalone_wrapper( + TARGET ${pluginname}_as_auv3_standalone + OUTPUT_NAME "${CLAP_WRAPPER_OUTPUT_NAME} AUv3" + BUNDLE_IDENTIFIER "${CLAP_WRAPPER_BUNDLE_IDENTIFIER}" + BUNDLE_VERSION "${CLAP_WRAPPER_BUNDLE_VERSION}" + AUV3_TARGET ${pluginname}_as_auv3 + AU_TYPE "aumu" + AU_SUBTYPE "gWwp" + AU_MANUFACTURER "clAA" + ) + endif() + if (${CLAP_WRAPPER_BUILD_STANDALONE}) add_executable(${pluginname}_as_standalone) target_add_standalone_wrapper(TARGET ${pluginname}_as_standalone diff --git a/cmake/wrap_auv3.cmake b/cmake/wrap_auv3.cmake new file mode 100644 index 00000000..3383a8fc --- /dev/null +++ b/cmake/wrap_auv3.cmake @@ -0,0 +1,276 @@ + +function(target_add_auv3_wrapper) + set(oneValueArgs + TARGET + OUTPUT_NAME + BUNDLE_IDENTIFIER + BUNDLE_VERSION + RESOURCE_DIRECTORY + + MANUFACTURER_NAME + MANUFACTURER_CODE + SUBTYPE_CODE + INSTRUMENT_TYPE + + CLAP_TARGET_FOR_CONFIG + + MACOS_EMBEDDED_CLAP_LOCATION + MACOSX_EMBEDDED_CLAP_LOCATION + ) + cmake_parse_arguments(AUV3 "" "${oneValueArgs}" "" ${ARGN}) + + if (NOT DEFINED AUV3_MACOS_EMBEDDED_CLAP_LOCATION AND DEFINED AUV3_MACOSX_EMBEDDED_CLAP_LOCATION) + set(AUV3_MACOS_EMBEDDED_CLAP_LOCATION ${AUV3_MACOSX_EMBEDDED_CLAP_LOCATION}) + endif() + + if (NOT DEFINED AUV3_MACOSX_EMBEDDED_CLAP_LOCATION AND DEFINED AUV3_MACOS_EMBEDDED_CLAP_LOCATION) + set(AUV3_MACOSX_EMBEDDED_CLAP_LOCATION ${AUV3_MACOS_EMBEDDED_CLAP_LOCATION}) + endif() + + if (NOT APPLE) + message(STATUS "clap-wrapper: auv3 is only available on macOS/iOS") + return() + endif() + + # AUv3 does NOT require the AudioUnit SDK (ausdk) - it uses AudioToolbox.framework directly + + if (NOT DEFINED AUV3_TARGET) + message(FATAL_ERROR "clap-wrapper: target_add_auv3_wrapper requires a target") + endif () + + if (NOT TARGET ${AUV3_TARGET}) + message(FATAL_ERROR "clap-wrapper: auv3-target must be a target") + endif () + + if (NOT DEFINED AUV3_BUNDLE_VERSION) + message(WARNING "clap-wrapper: bundle version not defined. Choosing ${PROJECT_VERSION}") + set(AUV3_BUNDLE_VERSION ${PROJECT_VERSION}) + endif () + + if (NOT DEFINED AUV3_RESOURCE_DIRECTORY) + set(AUV3_RESOURCE_DIRECTORY "") + endif() + + # Build helper to generate Info.plist and entry points + set(bhtg ${AUV3_TARGET}-auv3-build-helper) + set(bhsc "${CLAP_WRAPPER_CMAKE_CURRENT_SOURCE_DIR}/src/detail/auv3/build-helper/") + add_executable(${bhtg} ${bhsc}/build-helper.cpp) + target_link_libraries(${bhtg} PRIVATE + clap-wrapper-compile-options + clap-wrapper-shared-detail + macos_filesystem_support + "-framework Foundation" + "-framework CoreFoundation" + ) + set(bhtgoutdir "${CMAKE_CURRENT_BINARY_DIR}/${AUV3_TARGET}-auv3-build-helper-output") + + add_custom_command(TARGET ${bhtg} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E echo "clap-wrapper: auv3 configuration output dir is ${bhtgoutdir}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${bhtgoutdir}" + ) + + add_dependencies(${AUV3_TARGET} ${bhtg}) + + if (DEFINED AUV3_CLAP_TARGET_FOR_CONFIG) + set(clpt ${AUV3_CLAP_TARGET_FOR_CONFIG}) + message(STATUS "clap-wrapper: building auv3 based on target ${AUV3_CLAP_TARGET_FOR_CONFIG}") + get_property(ton TARGET ${clpt} PROPERTY LIBRARY_OUTPUT_NAME) + set(AUV3_OUTPUT_NAME "${ton}") + + if (NOT DEFINED AUV3_MANUFACTURER_CODE) + set(AUV3_MANUFACTURER_CODE "errr") + endif() + if (NOT DEFINED AUV3_MANUFACTURER_NAME) + set(AUV3_MANUFACTURER_NAME "errr") + endif() + if (NOT DEFINED AUV3_SUBTYPE_CODE) + set(AUV3_SUBTYPE_CODE "errr") + endif() + if (NOT DEFINED AUV3_INSTRUMENT_TYPE) + set(AUV3_INSTRUMENT_TYPE "errr") + endif() + + add_dependencies(${AUV3_TARGET} ${clpt}) + add_dependencies(${bhtg} ${clpt}) + + add_custom_command( + TARGET ${bhtg} + POST_BUILD + WORKING_DIRECTORY ${bhtgoutdir} + BYPRODUCTS ${bhtgoutdir}/auv3_Info.plist ${bhtgoutdir}/generated_auv3_entrypoints.hxx + COMMAND codesign -s - -f "$" + COMMAND $ --fromclap + "${AUV3_OUTPUT_NAME}" + "$" "${AUV3_BUNDLE_VERSION}" + "${AUV3_MANUFACTURER_CODE}" "${AUV3_MANUFACTURER_NAME}" + "${AUV3_INSTRUMENT_TYPE}" "${AUV3_SUBTYPE_CODE}" + ) + elseif (DEFINED AUV3_MACOSX_EMBEDDED_CLAP_LOCATION) + message(STATUS "clap-wrapper: building auv3 based on clap ${AUV3_MACOSX_EMBEDDED_CLAP_LOCATION}") + + if (NOT DEFINED AUV3_MANUFACTURER_CODE) + set(AUV3_MANUFACTURER_CODE "errr") + endif() + if (NOT DEFINED AUV3_MANUFACTURER_NAME) + set(AUV3_MANUFACTURER_NAME "errr") + endif() + if (NOT DEFINED AUV3_SUBTYPE_CODE) + set(AUV3_SUBTYPE_CODE "errr") + endif() + if (NOT DEFINED AUV3_INSTRUMENT_TYPE) + set(AUV3_INSTRUMENT_TYPE "errr") + endif() + + add_custom_command( + TARGET ${bhtg} + POST_BUILD + WORKING_DIRECTORY ${bhtgoutdir} + BYPRODUCTS ${bhtgoutdir}/auv3_Info.plist ${bhtgoutdir}/generated_auv3_entrypoints.hxx + COMMAND codesign -s - -f "$" + COMMAND $ --fromclap + "${AUV3_OUTPUT_NAME}" + "${AUV3_MACOSX_EMBEDDED_CLAP_LOCATION}" "${AUV3_BUNDLE_VERSION}" + "${AUV3_MANUFACTURER_CODE}" "${AUV3_MANUFACTURER_NAME}" + "${AUV3_INSTRUMENT_TYPE}" "${AUV3_SUBTYPE_CODE}" + ) + else () + message(STATUS "clap-wrapper: using cmake configuration for auv3") + if (NOT DEFINED AUV3_OUTPUT_NAME) + message(FATAL_ERROR "clap-wrapper: target_add_auv3_wrapper requires an output name") + endif () + + if (NOT DEFINED AUV3_SUBTYPE_CODE) + message(FATAL_ERROR "clap-wrapper: For nontarget build specify AUV3 subtype code (4 chars)") + endif () + + if (NOT DEFINED AUV3_MANUFACTURER_NAME) + message(FATAL_ERROR "clap-wrapper: For nontarget build specify AUV3 manufacturer name") + endif () + + if (NOT DEFINED AUV3_MANUFACTURER_CODE) + message(FATAL_ERROR "clap-wrapper: For nontarget build specify AUV3 manufacturer code (4 chars)") + endif () + + if (NOT DEFINED AUV3_INSTRUMENT_TYPE) + message(WARNING "clap-wrapper: auv3 instrument type not specified. Using aumu") + set(AUV3_INSTRUMENT_TYPE "aumu") + endif () + + add_custom_command( + TARGET ${bhtg} + POST_BUILD + WORKING_DIRECTORY ${bhtgoutdir} + BYPRODUCTS ${bhtgoutdir}/auv3_Info.plist ${bhtgoutdir}/generated_auv3_entrypoints.hxx + COMMAND codesign -s - -f "$" + COMMAND $ --explicit + "${AUV3_OUTPUT_NAME}" "${AUV3_BUNDLE_VERSION}" + "${AUV3_INSTRUMENT_TYPE}" "${AUV3_SUBTYPE_CODE}" + "${AUV3_MANUFACTURER_CODE}" "${AUV3_MANUFACTURER_NAME}" + ) + endif () + + string(MAKE_C_IDENTIFIER ${AUV3_OUTPUT_NAME} outidentifier) + + if ("${AUV3_BUNDLE_IDENTIFIER}" STREQUAL "") + string(REPLACE "_" "-" repout ${outidentifier}) + set(AUV3_BUNDLE_IDENTIFIER "org.cleveraudio.wrapper.${repout}.auv3") + endif () + + set(AUV3_MANUFACTURER_NAME ${AUV3_MANUFACTURER_NAME} PARENT_SCOPE) + set(AUV3_MANUFACTURER_CODE ${AUV3_MANUFACTURER_CODE} PARENT_SCOPE) + configure_file(${bhsc}/auv3_infoplist_top.in + ${bhtgoutdir}/auv3_infoplist_top) + + set(AUV3_INSTRUMENT_TYPE ${AUV3_INSTRUMENT_TYPE} PARENT_SCOPE) + set(AUV3_SUBTYPE_CODE ${AUV3_SUBTYPE_CODE} PARENT_SCOPE) + + message(STATUS "clap-wrapper: Adding AUv3 Wrapper to target ${AUV3_TARGET} generating '${AUV3_OUTPUT_NAME}.appex'") + + target_sources(${AUV3_TARGET} PRIVATE ${CLAP_WRAPPER_CMAKE_CURRENT_SOURCE_DIR}/src/detail/os/macos.mm) + + target_sources(${AUV3_TARGET} PRIVATE + ${CLAP_WRAPPER_CMAKE_CURRENT_SOURCE_DIR}/src/wrapasauv3.mm + ${CLAP_WRAPPER_CMAKE_CURRENT_SOURCE_DIR}/src/detail/auv3/auv3_audiounit.mm + ${CLAP_WRAPPER_CMAKE_CURRENT_SOURCE_DIR}/src/detail/auv3/auv3_parameters.mm + ${CLAP_WRAPPER_CMAKE_CURRENT_SOURCE_DIR}/src/detail/auv3/process.mm + ${bhtgoutdir}/generated_auv3_entrypoints.hxx) + target_compile_options(${AUV3_TARGET} PRIVATE -fno-char8_t -fobjc-arc) + + if (NOT TARGET ${AUV3_TARGET}-clap-wrapper-auv3-lib) + add_library(${AUV3_TARGET}-clap-wrapper-auv3-lib INTERFACE) + target_include_directories(${AUV3_TARGET}-clap-wrapper-auv3-lib INTERFACE "${bhtgoutdir}" "${CLAP_WRAPPER_CMAKE_CURRENT_SOURCE_DIR}/src") + target_link_libraries(${AUV3_TARGET}-clap-wrapper-auv3-lib INTERFACE clap) + target_link_libraries(${AUV3_TARGET}-clap-wrapper-auv3-lib INTERFACE clap-wrapper-extensions clap-wrapper-shared-detail clap-wrapper-compile-options) + endif () + + set_target_properties(${AUV3_TARGET} PROPERTIES LIBRARY_OUTPUT_NAME "${AUV3_OUTPUT_NAME}") + target_link_libraries(${AUV3_TARGET} PUBLIC ${AUV3_TARGET}-clap-wrapper-auv3-lib) + + if ("${CLAP_WRAPPER_BUNDLE_VERSION}" STREQUAL "") + set(CLAP_WRAPPER_BUNDLE_VERSION "1.0") + endif () + + target_link_libraries(${AUV3_TARGET} PUBLIC + "-framework Foundation" + "-framework CoreFoundation" + "-framework AppKit" + "-framework AudioToolbox" + "-framework AVFoundation" + "-framework CoreAudio" + "-framework CoreAudioKit" + "-framework CoreMIDI") + + set_target_properties(${AUV3_TARGET} PROPERTIES + BUNDLE True + BUNDLE_EXTENSION appex + LIBRARY_OUTPUT_NAME ${AUV3_OUTPUT_NAME} + MACOSX_BUNDLE_GUI_IDENTIFIER "${AUV3_BUNDLE_IDENTIFIER}" + MACOSX_BUNDLE_BUNDLE_NAME ${AUV3_OUTPUT_NAME} + MACOSX_BUNDLE_BUNDLE_VERSION ${AUV3_BUNDLE_VERSION} + MACOSX_BUNDLE_SHORT_VERSION_STRING ${AUV3_BUNDLE_VERSION} + ) + + # For Xcode: tell it to use the build-helper's plist as INFOPLIST_FILE so + # Xcode's own "Process Info.plist" phase preserves our NSExtension block. + # For non-Xcode generators: POST_BUILD copy works because there's no + # implicit plist processing after POST_BUILD. + if (CMAKE_GENERATOR STREQUAL "Xcode") + set_target_properties(${AUV3_TARGET} PROPERTIES + MACOSX_BUNDLE_INFO_PLIST "${bhtgoutdir}/auv3_Info.plist" + ) + else() + add_custom_command(TARGET ${AUV3_TARGET} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy ${bhtgoutdir}/auv3_Info.plist $/../Info.plist + COMMENT "Replacing Info.plist with build-helper generated version (contains NSExtension)") + endif() + + set_target_properties(${AUV3_TARGET} PROPERTIES XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${AUV3_BUNDLE_IDENTIFIER}") + + macos_include_clap_in_bundle(TARGET ${AUV3_TARGET} + MACOS_EMBEDDED_CLAP_LOCATION ${AUV3_MACOSX_EMBEDDED_CLAP_LOCATION}) + macos_bundle_flag(TARGET ${AUV3_TARGET}) + + if(NOT AUV3_RESOURCE_DIRECTORY STREQUAL "") + message(WARNING "RESOURCE_DIRECTORY defined, but not (yet) supported for AUV3") + endif() + + # Set entitlements for sandboxing (required for AUv3 registration) + set(AUV3_ENTITLEMENTS "${CLAP_WRAPPER_CMAKE_CURRENT_SOURCE_DIR}/src/detail/auv3/auv3.entitlements") + + # For Xcode, set the entitlements via build settings + set_target_properties(${AUV3_TARGET} PROPERTIES + XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${AUV3_ENTITLEMENTS}" + XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "-" + XCODE_ATTRIBUTE_ENABLE_APP_SANDBOX "YES" + ) + + # Ad-hoc sign the appex with entitlements so macOS will register it as an Audio Unit + add_custom_command(TARGET ${AUV3_TARGET} POST_BUILD + COMMAND codesign -s - -f --entitlements "${AUV3_ENTITLEMENTS}" "$" + COMMENT "Ad-hoc signing AUv3 appex with sandbox entitlements" + ) + + if (${CLAP_WRAPPER_COPY_AFTER_BUILD}) + target_copy_after_build(TARGET ${AUV3_TARGET} FLAVOR auv3) + endif () +endfunction(target_add_auv3_wrapper) diff --git a/cmake/wrap_auv3_standalone.cmake b/cmake/wrap_auv3_standalone.cmake new file mode 100644 index 00000000..56eb5bcf --- /dev/null +++ b/cmake/wrap_auv3_standalone.cmake @@ -0,0 +1,168 @@ + +function(target_add_auv3_standalone_wrapper) + set(oneValueArgs + TARGET + OUTPUT_NAME + BUNDLE_IDENTIFIER + BUNDLE_VERSION + + AUV3_TARGET + + AU_TYPE + AU_SUBTYPE + AU_MANUFACTURER + + MACOS_ICON + ) + cmake_parse_arguments(AUSA "" "${oneValueArgs}" "" ${ARGN}) + + if (NOT APPLE) + message(STATUS "clap-wrapper: auv3 standalone is only available on macOS") + return() + endif() + + if (NOT DEFINED AUSA_TARGET) + message(FATAL_ERROR "clap-wrapper: target_add_auv3_standalone_wrapper requires a TARGET") + endif() + + if (NOT TARGET ${AUSA_TARGET}) + message(FATAL_ERROR "clap-wrapper: auv3-standalone target must be a target") + endif() + + if (NOT DEFINED AUSA_AUV3_TARGET) + message(FATAL_ERROR "clap-wrapper: target_add_auv3_standalone_wrapper requires AUV3_TARGET") + endif() + + if (NOT DEFINED AUSA_OUTPUT_NAME) + set(AUSA_OUTPUT_NAME "${AUSA_TARGET}") + endif() + + if (NOT DEFINED AUSA_BUNDLE_VERSION) + set(AUSA_BUNDLE_VERSION "1.0") + endif() + + if (NOT DEFINED AUSA_BUNDLE_IDENTIFIER) + string(MAKE_C_IDENTIFIER ${AUSA_OUTPUT_NAME} outidentifier) + string(REPLACE "_" "-" repout ${outidentifier}) + set(AUSA_BUNDLE_IDENTIFIER "org.cleveraudio.wrapper.${repout}.auv3standalone") + endif() + + if (NOT DEFINED AUSA_AU_TYPE) + set(AUSA_AU_TYPE "aufx") + endif() + + if (NOT DEFINED AUSA_AU_SUBTYPE) + set(AUSA_AU_SUBTYPE "none") + endif() + + if (NOT DEFINED AUSA_AU_MANUFACTURER) + set(AUSA_AU_MANUFACTURER "none") + endif() + + if (NOT DEFINED AUSA_MACOS_ICON) + set(AUSA_MACOS_ICON "") + endif() + + message(STATUS "clap-wrapper: Adding AUv3 Standalone to target ${AUSA_TARGET} for '${AUSA_OUTPUT_NAME}'") + + # --- XIB --- + set(MAIN_XIB "${CLAP_WRAPPER_CMAKE_CURRENT_SOURCE_DIR}/src/detail/standalone/macos/auv3/MainMenu.xib") + set(SA_OUTPUT_NAME "${AUSA_OUTPUT_NAME}") + set(GEN_XIB "${CMAKE_BINARY_DIR}/generated_xib/${AUSA_TARGET}/MainMenu.xib") + configure_file(${MAIN_XIB} ${GEN_XIB}) + + # --- Sources --- + target_sources(${AUSA_TARGET} PRIVATE + "${CLAP_WRAPPER_CMAKE_CURRENT_SOURCE_DIR}/src/wrapasauv3standalone.mm" + "${CLAP_WRAPPER_CMAKE_CURRENT_SOURCE_DIR}/src/detail/standalone/macos/auv3/AUv3HostAppDelegate.mm" + ${GEN_XIB} + ) + + target_include_directories(${AUSA_TARGET} PRIVATE + "${CLAP_WRAPPER_CMAKE_CURRENT_SOURCE_DIR}/src/detail/standalone/macos/auv3" + ) + + target_compile_options(${AUSA_TARGET} PRIVATE -fobjc-arc -fno-char8_t) + + # --- Suppress ObjC nullability warnings bleeding into C++ --- + target_compile_options(${AUSA_TARGET} PRIVATE -Wno-nullability-completeness) + + # --- Frameworks (no RtAudio/RtMidi, no CLAP SDK) --- + target_link_libraries(${AUSA_TARGET} PRIVATE + "-framework AVFoundation" + "-framework AudioToolbox" + "-framework CoreAudio" + "-framework CoreMIDI" + "-framework Foundation" + "-framework CoreFoundation" + "-framework AppKit" + ) + + # --- FourCC conversion: turn 4-char string into a uint32 compile definition --- + # We pass the raw strings and do the conversion in the source code + target_compile_definitions(${AUSA_TARGET} PRIVATE + AU_TYPE_STR="${AUSA_AU_TYPE}" + AU_SUBTYPE_STR="${AUSA_AU_SUBTYPE}" + AU_MANUFACTURER_STR="${AUSA_AU_MANUFACTURER}" + AUV3_STANDALONE_OUTPUT_NAME="${AUSA_OUTPUT_NAME}" + ) + + # --- App bundle properties --- + set_target_properties(${AUSA_TARGET} PROPERTIES + BUNDLE TRUE + BUNDLE_NAME "${AUSA_OUTPUT_NAME}" + BUNDLE_EXTENSION app + OUTPUT_NAME "${AUSA_OUTPUT_NAME}" + MACOSX_BUNDLE_BUNDLE_NAME "${AUSA_OUTPUT_NAME}" + MACOSX_BUNDLE_GUI_IDENTIFIER "${AUSA_BUNDLE_IDENTIFIER}" + MACOSX_BUNDLE TRUE + MACOSX_BUNDLE_INFO_PLIST "${CLAP_WRAPPER_CMAKE_CURRENT_SOURCE_DIR}/src/detail/standalone/macos/auv3/Info.plist.in" + XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${AUSA_BUNDLE_IDENTIFIER}" + RESOURCE "${GEN_XIB}" + ) + + # --- XIB -> NIB for non-Xcode generators --- + if (NOT ${CMAKE_GENERATOR} STREQUAL "Xcode") + message(STATUS "clap-wrapper: ejecting xib->nib rules manually for ${CMAKE_GENERATOR} on ${AUSA_TARGET}") + find_program(IBTOOL ibtool REQUIRED) + add_custom_command(TARGET ${AUSA_TARGET} PRE_BUILD + COMMAND ${CMAKE_COMMAND} -E echo ${IBTOOL} --compile "$/../Resources/MainMenu.nib" ${GEN_XIB} + COMMAND ${IBTOOL} --compile "$/../Resources/MainMenu.nib" ${GEN_XIB} + ) + endif() + + # --- Icon --- + if(NOT "${AUSA_MACOS_ICON}" STREQUAL "") + add_custom_command(TARGET ${AUSA_TARGET} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy ${AUSA_MACOS_ICON} "$/../Resources/Icon.icns" + ) + endif() + + # --- Embed the .appex in Contents/PlugIns/ --- + add_dependencies(${AUSA_TARGET} ${AUSA_AUV3_TARGET}) + + # Get the appex bundle path. For MODULE libraries built as bundles, the output + # is at LIBRARY_OUTPUT_DIRECTORY or the default build dir. + # The appex bundle is a directory at: /../../.. + # which is the same as the .appex top-level directory. + # We use TARGET_BUNDLE_DIR for BUNDLE targets to get the correct path. + set(_auv3_appex_src "$") + set(_auv3_appex_dst "$/Contents/PlugIns/$.appex") + + add_custom_command(TARGET ${AUSA_TARGET} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "$/Contents/PlugIns" + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${_auv3_appex_src}" + "${_auv3_appex_dst}" + ) + + # Ad-hoc sign the embedded appex first, then the host app + if (NOT ${CMAKE_GENERATOR} STREQUAL "Xcode") + add_custom_command(TARGET ${AUSA_TARGET} POST_BUILD + COMMAND codesign -s - -f "${_auv3_appex_dst}" + COMMAND codesign -s - -f "$" + COMMENT "Ad-hoc signing AUv3 standalone and embedded appex" + ) + endif() + +endfunction(target_add_auv3_standalone_wrapper) diff --git a/cmake/wrapper_functions.cmake b/cmake/wrapper_functions.cmake index 6cc9939d..4f231269 100644 --- a/cmake/wrapper_functions.cmake +++ b/cmake/wrapper_functions.cmake @@ -25,6 +25,8 @@ guarantee_clap_wrapper_shared() include(cmake/wrap_auv2.cmake) +include(cmake/wrap_auv3.cmake) +include(cmake/wrap_auv3_standalone.cmake) include(cmake/wrap_vst3.cmake) include(cmake/wrap_aax.cmake) include(cmake/wrap_standalone.cmake) diff --git a/src/detail/auv3/auv3.entitlements b/src/detail/auv3/auv3.entitlements new file mode 100644 index 00000000..ae79293e --- /dev/null +++ b/src/detail/auv3/auv3.entitlements @@ -0,0 +1,16 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + com.apple.security.files.user-selected.read-write + + com.apple.security.temporary-exception.files.absolute-path.read-write + + / + + + diff --git a/src/detail/auv3/auv3_audiounit.h b/src/detail/auv3/auv3_audiounit.h new file mode 100644 index 00000000..724df15b --- /dev/null +++ b/src/detail/auv3/auv3_audiounit.h @@ -0,0 +1,58 @@ +#pragma once + +/* + AUv3 Audio Unit Wrapper + + Copyright (c) 2024 Timo Kaluza (defiantnerd) + + This file is part of the clap-wrappers project which is released under MIT License. + See file LICENSE or go to https://github.com/free-audio/clap-wrapper for full license details. + + ClapAUv3AudioUnit is the AUAudioUnit subclass that wraps a CLAP plugin as an AUv3 Audio Unit. +*/ + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnullability-completeness" + +#import +#import +#import +#import + +#pragma clang diagnostic pop + +@class ClapAUv3AudioUnit; + +// The view controller is also the AUAudioUnitFactory. +// Apple's AUv3 model requires the NSExtensionPrincipalClass to be the +// AUViewController subclass which also conforms to AUAudioUnitFactory. +@interface ClapAUv3ViewController : AUViewController +@property (nonatomic, weak) ClapAUv3AudioUnit *audioUnit; +// Subclasses (generated by build-helper) override this to provide plugin-specific info +- (AUAudioUnit *)createAudioUnitWithComponentDescription:(AudioComponentDescription)desc + error:(NSError **)error; +@end + +@interface ClapAUv3AudioUnit : AUAudioUnit + +- (instancetype)initWithComponentDescription:(AudioComponentDescription)componentDescription + options:(AudioComponentInstantiationOptions)options + error:(NSError **)outError + clapName:(NSString *)clapName + clapId:(NSString *)clapId + clapIndex:(int)clapIndex; + +// Called by the view controller to create/attach the CLAP GUI to a parent view. +// Returns YES if the CLAP plugin has a GUI and it was successfully created. +- (BOOL)createGUIInView:(NSView *)parentView width:(uint32_t *)outWidth height:(uint32_t *)outHeight; + +// Called by the view controller when the view is being torn down. +- (void)destroyGUI; + +// Called by the view controller to check if the CLAP GUI supports resizing. +- (BOOL)canResizeGUI; + +// Called by the view controller when the host resizes the view. +- (BOOL)setGUISize:(uint32_t)width height:(uint32_t)height; + +@end diff --git a/src/detail/auv3/auv3_audiounit.mm b/src/detail/auv3/auv3_audiounit.mm new file mode 100644 index 00000000..e51ea459 --- /dev/null +++ b/src/detail/auv3/auv3_audiounit.mm @@ -0,0 +1,862 @@ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnullability-completeness" +#pragma clang diagnostic ignored "-Wlanguage-extension-token" + +#import "auv3_audiounit.h" +#include "auv3_parameters.h" +#include "process.h" + +#include "clap_proxy.h" +#include "detail/clap/fsutil.h" +#include "detail/os/osutil.h" +#include "detail/shared/fixedqueue.h" +#include "detail/clap/automation.h" + +#include +#include +#include +#include +#include +#include + +// ----------------------------------------------------------------------- +// C++ implementation detail bridging IHost, IAutomation, and IPlugObject +// ----------------------------------------------------------------------- + +namespace free_audio::auv3_wrapper +{ + +class queueEvent +{ + public: + typedef enum class type + { + editstart, + editvalue, + editend, + } type_t; + type_t _type; + union + { + clap_id _id; + clap_event_param_value_t _value; + } _data; +}; + +class AUv3ImplDetail : public Clap::IHost, + public Clap::IAutomation, + public os::IPlugObject +{ + public: + AUv3ImplDetail() : _os_attached([this] { os::attach(this); }, [this] { os::detach(this); }) + { + } + + ~AUv3ImplDetail() override + { + if (_plugin) + { + _os_attached.off(); + _plugin->terminate(); + _plugin.reset(); + } + } + + // CLAP plugin state + std::shared_ptr _plugin; + std::unique_ptr _processAdapter; + const clap_plugin_descriptor_t *_desc = nullptr; + + // Audio bus info + struct BusInfo + { + uint32_t channelCount; + std::string name; + }; + std::vector _inputBusInfos; + std::vector _outputBusInfos; + + // MIDI + uint32_t _midi_preferred_dialect = CLAP_NOTE_DIALECT_CLAP; + bool _midi_wants_midi_input = false; + std::vector _midiOutputNames; + + // Parameters + AUParameterTree *_parameterTree = nil; + + // Hosting + std::string _clapname; + std::string _clapid; + int _idx = 0; + os::State _os_attached; + std::string _hostname = "CLAP-as-AUv3"; + std::atomic _initialized{false}; + std::atomic_bool _requestUICallback{false}; + + // Back-reference to the ObjC audio unit (weak to avoid retain cycle) + __weak ClapAUv3AudioUnit *_audioUnit = nil; + + // The NSView that the CLAP GUI is parented to (set by createGUIInView:) + __weak NSView *_guiParentView = nil; + + // Queue for audio -> UI thread parameter notifications + ClapWrapper::detail::shared::fixedqueue _queueToUI; + + // --- IHost --- + void mark_dirty() override {} + void restartPlugin() override {} + + void request_callback() override { _requestUICallback = true; } + + void setupWrapperSpecifics(const clap_plugin_t *plugin) override + { + // AUv3-specific extensions could be queried here + } + + void setupAudioBusses(const clap_plugin_t *plugin, + const clap_plugin_audio_ports_t *audioports) override + { + _inputBusInfos.clear(); + _outputBusInfos.clear(); + + auto numIn = audioports->count(plugin, true); + auto numOut = audioports->count(plugin, false); + + for (decltype(numIn) i = 0; i < numIn; ++i) + { + clap_audio_port_info_t info; + if (audioports->get(plugin, i, true, &info)) + { + _inputBusInfos.push_back({info.channel_count, info.name}); + } + } + + for (decltype(numOut) i = 0; i < numOut; ++i) + { + clap_audio_port_info_t info; + if (audioports->get(plugin, i, false, &info)) + { + _outputBusInfos.push_back({info.channel_count, info.name}); + } + } + } + + void setupMIDIBusses(const clap_plugin_t *plugin, + const clap_plugin_note_ports_t *noteports) override + { + if (!noteports) return; + + auto numMIDIIn = noteports->count(plugin, true); + auto numMIDIOut = noteports->count(plugin, false); + + _midi_wants_midi_input = (numMIDIIn > 0); + if (numMIDIIn > 0) + { + clap_note_port_info_t info; + if (noteports->get(plugin, 0, true, &info)) + { + _midi_preferred_dialect = info.preferred_dialect; + } + } + + _midiOutputNames.clear(); + for (decltype(numMIDIOut) i = 0; i < numMIDIOut; ++i) + { + clap_note_port_info_t info; + if (noteports->get(plugin, i, false, &info)) + { + _midiOutputNames.push_back([NSString stringWithUTF8String:info.name]); + } + } + } + + void setupParameters(const clap_plugin_t *plugin, + const clap_plugin_params_t *params) override + { + _parameterTree = Clap::AUv3::createParameterTree(plugin, params); + } + + void param_rescan(clap_param_rescan_flags flags) override + { + // TODO: Rebuild parameter tree when plugin requests rescan + std::cout << "[clap-wrapper] auv3: param_rescan requested (not yet fully implemented)" << std::endl; + } + + void param_clear(clap_id param, clap_param_clear_flags flags) override {} + void param_request_flush() override {} + + void latency_changed() override + { + // AUv3 handles latency via the latency property - hosts observe it via KVO + } + + void tail_changed() override + { + // AUv3 handles tail time via the tailTime property + } + + bool gui_can_resize() override + { + if (_plugin && _plugin->_ext._gui) + return _plugin->_ext._gui->can_resize(_plugin->_plugin); + return false; + } + + bool gui_request_resize(uint32_t width, uint32_t height) override + { + // Notify the host that the plugin wants to resize + if (_guiParentView) + { + dispatch_async(dispatch_get_main_queue(), ^{ + NSView *view = _guiParentView; + if (view) + { + NSWindow *window = view.window; + if (window) + { + [window setContentSize:NSMakeSize(width, height)]; + } + } + }); + return true; + } + return false; + } + + bool gui_request_show() override { return false; } + bool gui_request_hide() override { return false; } + + bool register_timer(uint32_t period_ms, clap_id *timer_id) override { return false; } + bool unregister_timer(clap_id timer_id) override { return false; } + + const char *host_get_name() override + { + NSBundle *mainBundle = [NSBundle mainBundle]; + if (mainBundle) + { + NSString *name = [mainBundle objectForInfoDictionaryKey:@"CFBundleName"]; + NSString *version = [mainBundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; + if (name) + { + _hostname = [name UTF8String]; + if (version) + { + _hostname += " "; + _hostname += [version UTF8String]; + } + _hostname += " (CLAP-as-AUv3)"; + } + } + return _hostname.c_str(); + } + + bool track_info_get(clap_track_info_t *info) override { return false; } + + bool supportsContextMenu() const override { return false; } + bool context_menu_populate(const clap_context_menu_target_t *target, + const clap_context_menu_builder_t *builder) override + { + return false; + } + bool context_menu_perform(const clap_context_menu_target_t *target, clap_id action_id) override + { + return false; + } + bool context_menu_can_popup() override { return false; } + bool context_menu_popup(const clap_context_menu_target_t *target, int32_t screen_index, + int32_t x, int32_t y) override + { + return false; + } + + // --- IAutomation --- + void onBeginEdit(clap_id id) override + { + queueEvent evt; + evt._type = queueEvent::type::editstart; + evt._data._id = id; + _queueToUI.push(evt); + } + + void onPerformEdit(const clap_event_param_value_t *value) override + { + queueEvent evt; + evt._type = queueEvent::type::editvalue; + evt._data._value = *value; + _queueToUI.push(evt); + } + + void onEndEdit(clap_id id) override + { + queueEvent evt; + evt._type = queueEvent::type::editend; + evt._data._id = id; + _queueToUI.push(evt); + } + + // --- IPlugObject --- + void onIdle() override + { + if (!_plugin) return; + + if (_requestUICallback.exchange(false)) + { + auto guard = _plugin->AlwaysMainThread(); + _plugin->_plugin->on_main_thread(_plugin->_plugin); + } + + // Process queued parameter changes from audio thread + queueEvent evt; + while (_queueToUI.pop(evt)) + { + switch (evt._type) + { + case queueEvent::type::editvalue: + { + if (_parameterTree) + { + AUParameter *param = [_parameterTree parameterWithAddress:(AUParameterAddress)evt._data._value.param_id]; + if (param) + { + param.value = (AUValue)evt._data._value.value; + } + } + break; + } + default: + break; + } + } + } +}; + +} // namespace free_audio::auv3_wrapper + +// ----------------------------------------------------------------------- +// Static CLAP library holder +// ----------------------------------------------------------------------- + +static Clap::Library _library; + +// ----------------------------------------------------------------------- +// ClapAUv3AudioUnit implementation +// ----------------------------------------------------------------------- + +@implementation ClapAUv3AudioUnit +{ + std::unique_ptr _impl; + AUAudioUnitBusArray *_inputBusArray; + AUAudioUnitBusArray *_outputBusArray; + BOOL _renderResourcesAllocated; +} + +- (instancetype)initWithComponentDescription:(AudioComponentDescription)componentDescription + options:(AudioComponentInstantiationOptions)options + error:(NSError **)outError + clapName:(NSString *)clapName + clapId:(NSString *)clapId + clapIndex:(int)clapIndex +{ + self = [super initWithComponentDescription:componentDescription options:options error:outError]; + if (!self) return nil; + + _impl = std::make_unique(); + _impl->_audioUnit = self; + _impl->_clapname = [clapName UTF8String]; + _impl->_clapid = clapId ? [clapId UTF8String] : ""; + _impl->_idx = clapIndex; + + // Load CLAP library + if (!_library.hasEntryPoint()) + { + if (_impl->_clapname.empty()) + { + std::cout << "[ERROR] auv3: _clapname empty and no internal entry point" << std::endl; + if (outError) + *outError = [NSError errorWithDomain:@"ClapAUv3" code:-1 + userInfo:@{NSLocalizedDescriptionKey : @"CLAP name is empty"}]; + return nil; + } + + auto csp = Clap::getValidCLAPSearchPaths(); + auto it = std::find_if(csp.begin(), csp.end(), + [&](const auto &cs) + { + auto fp = cs / (_impl->_clapname + ".clap"); + return fs::is_directory(fp) && _library.load(fp); + }); + + if (it != csp.end()) + { + std::cout << "[clap-wrapper] auv3 loaded clap from " << it->u8string() << std::endl; + } + else + { + std::cout << "[ERROR] auv3: cannot load clap" << std::endl; + if (outError) + *outError = [NSError errorWithDomain:@"ClapAUv3" code:-2 + userInfo:@{NSLocalizedDescriptionKey : @"Cannot load CLAP plugin"}]; + return nil; + } + } + + // Find the plugin descriptor + if (!_impl->_clapid.empty()) + { + for (auto *d : _library.plugins) + { + if (strcmp(d->id, _impl->_clapid.c_str()) == 0) + { + _impl->_desc = d; + } + } + } + else if (_impl->_idx >= 0 && _impl->_idx < (int)_library.plugins.size()) + { + _impl->_desc = _library.plugins[_impl->_idx]; + } + + if (!_impl->_desc) + { + std::cout << "[ERROR] auv3: cannot determine plugin description" << std::endl; + if (outError) + *outError = [NSError errorWithDomain:@"ClapAUv3" code:-3 + userInfo:@{NSLocalizedDescriptionKey : @"Cannot find CLAP plugin descriptor"}]; + return nil; + } + + std::cout << "[clap-wrapper] auv3: Initialized '" << _impl->_desc->id << "' / '" + << _impl->_desc->name << "' / '" << _impl->_desc->version << "'" << std::endl; + + // Create the plugin instance + _impl->_plugin = Clap::Plugin::createInstance(_library._pluginFactory, _impl->_desc->id, _impl.get()); + if (!_impl->_plugin) + { + std::cout << "[ERROR] auv3: the clap did not create an instance" << std::endl; + if (outError) + *outError = [NSError errorWithDomain:@"ClapAUv3" code:-4 + userInfo:@{NSLocalizedDescriptionKey : @"CLAP plugin instance creation failed"}]; + return nil; + } + + _impl->_plugin->initialize(); + _impl->_os_attached.on(); + + // Build audio bus arrays from the CLAP audio port info + [self _buildBusArrays]; + + _renderResourcesAllocated = NO; + + return self; +} + +- (void)dealloc +{ + if (_impl && _impl->_plugin) + { + _impl->_os_attached.off(); + _impl->_plugin->terminate(); + _impl->_plugin.reset(); + } + _impl.reset(); +} + +- (void)_buildBusArrays +{ + // Build input bus array + NSMutableArray *inputs = [NSMutableArray new]; + for (auto &busInfo : _impl->_inputBusInfos) + { + AVAudioFormat *format = [[AVAudioFormat alloc] initStandardFormatWithSampleRate:self.outputBusses.count > 0 ? 44100.0 : 44100.0 + channels:busInfo.channelCount]; + if (format) + { + NSError *error = nil; + AUAudioUnitBus *bus = [[AUAudioUnitBus alloc] initWithFormat:format error:&error]; + if (bus) + { + bus.name = [NSString stringWithUTF8String:busInfo.name.c_str()]; + [inputs addObject:bus]; + } + } + } + _inputBusArray = [[AUAudioUnitBusArray alloc] initWithAudioUnit:self busType:AUAudioUnitBusTypeInput busses:inputs]; + + // Build output bus array + NSMutableArray *outputs = [NSMutableArray new]; + for (auto &busInfo : _impl->_outputBusInfos) + { + AVAudioFormat *format = [[AVAudioFormat alloc] initStandardFormatWithSampleRate:44100.0 + channels:busInfo.channelCount]; + if (format) + { + NSError *error = nil; + AUAudioUnitBus *bus = [[AUAudioUnitBus alloc] initWithFormat:format error:&error]; + if (bus) + { + bus.name = [NSString stringWithUTF8String:busInfo.name.c_str()]; + [outputs addObject:bus]; + } + } + } + _outputBusArray = [[AUAudioUnitBusArray alloc] initWithAudioUnit:self busType:AUAudioUnitBusTypeOutput busses:outputs]; +} + +// --- AUAudioUnit property overrides --- + +- (AUAudioUnitBusArray *)inputBusses +{ + return _inputBusArray; +} + +- (AUAudioUnitBusArray *)outputBusses +{ + return _outputBusArray; +} + +- (AUParameterTree *)parameterTree +{ + if (_impl && _impl->_parameterTree) + { + return _impl->_parameterTree; + } + return [AUParameterTree createTreeWithChildren:@[]]; +} + +- (NSArray *)MIDIOutputNames +{ + if (_impl && !_impl->_midiOutputNames.empty()) + { + NSMutableArray *names = [NSMutableArray new]; + for (auto &name : _impl->_midiOutputNames) + { + [names addObject:name]; + } + return names; + } + return @[]; +} + +- (NSTimeInterval)latency +{ + if (_impl && _impl->_plugin && _impl->_plugin->_ext._latency) + { + uint32_t samples = _impl->_plugin->_ext._latency->get(_impl->_plugin->_plugin); + return (double)samples / self.outputBusses[0].format.sampleRate; + } + return 0; +} + +- (NSTimeInterval)tailTime +{ + if (_impl && _impl->_plugin && _impl->_plugin->_ext._tail) + { + uint32_t samples = _impl->_plugin->_ext._tail->get(_impl->_plugin->_plugin); + if (samples == UINT32_MAX) return INFINITY; + return (double)samples / self.outputBusses[0].format.sampleRate; + } + return 0; +} + +- (BOOL)shouldChangeToFormat:(AVAudioFormat *)format forBus:(AUAudioUnitBus *)bus +{ + // Accept format changes + return YES; +} + +// --- State save/restore --- + +- (NSDictionary *)fullState +{ + NSMutableDictionary *state = [[super fullState] mutableCopy]; + if (!state) state = [NSMutableDictionary new]; + + if (_impl && _impl->_plugin && _impl->_plugin->_ext._state) + { + Clap::StateMemento chunk; + if (_impl->_plugin->_ext._state->save(_impl->_plugin->_plugin, chunk)) + { + NSData *clapState = [NSData dataWithBytes:chunk.data() length:chunk.size()]; + state[@"clapState"] = clapState; + } + } + + return state; +} + +- (void)setFullState:(NSDictionary *)fullState +{ + [super setFullState:fullState]; + + if (_impl && _impl->_plugin && _impl->_plugin->_ext._state) + { + NSData *clapState = fullState[@"clapState"]; + if (clapState) + { + Clap::StateMemento chunk; + chunk.setData((const uint8_t *)[clapState bytes], [clapState length]); + _impl->_plugin->_ext._state->load(_impl->_plugin->_plugin, chunk); + } + } +} + +// --- Render resources --- + +- (BOOL)allocateRenderResourcesAndReturnError:(NSError **)outError +{ + if (![super allocateRenderResourcesAndReturnError:outError]) + { + return NO; + } + + if (!_impl || !_impl->_plugin) + { + if (outError) + *outError = [NSError errorWithDomain:@"ClapAUv3" code:-10 + userInfo:@{NSLocalizedDescriptionKey : @"Plugin not initialized"}]; + return NO; + } + + // Get sample rate from output bus format + double sampleRate = 44100.0; + if (self.outputBusses.count > 0) + { + sampleRate = self.outputBusses[0].format.sampleRate; + } + else if (self.inputBusses.count > 0) + { + sampleRate = self.inputBusses[0].format.sampleRate; + } + + auto guarantee_mainthread = _impl->_plugin->AlwaysMainThread(); + + _impl->_plugin->setSampleRate(sampleRate); + _impl->_plugin->setBlockSizes(1, self.maximumFramesToRender); + + // Collect channel counts + std::vector inputChs, outputChs; + for (NSUInteger i = 0; i < self.inputBusses.count; ++i) + { + inputChs.push_back((uint32_t)self.inputBusses[i].format.channelCount); + } + for (NSUInteger i = 0; i < self.outputBusses.count; ++i) + { + outputChs.push_back((uint32_t)self.outputBusses[i].format.channelCount); + } + + // Create and set up the process adapter + _impl->_processAdapter = std::make_unique(); + _impl->_processAdapter->setupProcessing( + (uint32_t)inputChs.size(), inputChs.empty() ? nullptr : inputChs.data(), + (uint32_t)outputChs.size(), outputChs.empty() ? nullptr : outputChs.data(), + _impl->_plugin->_plugin, _impl->_plugin->_ext._params, _impl.get(), + self.maximumFramesToRender, _impl->_midi_preferred_dialect); + + // Set transport state block + _impl->_processAdapter->setTransportStateBlock(self.transportStateBlock); + + // Set MIDI output block + _impl->_processAdapter->midiOutputEventBlock = self.MIDIOutputEventBlock; + + // Activate the CLAP plugin + _impl->_plugin->activate(); + _impl->_plugin->start_processing(); + _impl->_initialized = true; + + // Wire up the parameter value observer + if (_impl->_parameterTree) + { + __weak typeof(self) weakSelf = self; + _impl->_parameterTree.implementorValueObserver = ^(AUParameter *param, AUValue value) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (strongSelf && strongSelf->_impl && strongSelf->_impl->_processAdapter) + { + strongSelf->_impl->_processAdapter->addParameterEvent((clap_id)param.address, (double)value, 0); + } + }; + } + + _renderResourcesAllocated = YES; + return YES; +} + +- (void)deallocateRenderResources +{ + if (_impl && _impl->_plugin && _impl->_initialized) + { + auto guarantee_mainthread = _impl->_plugin->AlwaysMainThread(); + _impl->_plugin->stop_processing(); + _impl->_plugin->deactivate(); + _impl->_initialized = false; + } + + _impl->_processAdapter.reset(); + _renderResourcesAllocated = NO; + + [super deallocateRenderResources]; +} + +// --- Render block --- + +- (AUInternalRenderBlock)internalRenderBlock +{ + // Capture a raw pointer to the C++ impl for use in the render block. + // This is safe because the render block's lifetime is bounded by + // allocateRenderResources / deallocateRenderResources. + auto *adapter = _impl->_processAdapter.get(); + + return ^AUAudioUnitStatus(AudioUnitRenderActionFlags *actionFlags, + const AudioTimeStamp *timestamp, + AUAudioFrameCount frameCount, + NSInteger outputBusNumber, + AudioBufferList *outputData, + const AURenderEvent *realtimeEventListHead, + AURenderPullInputBlock __unsafe_unretained pullInputBlock) { + if (!adapter) return kAudioUnitErr_Uninitialized; + + return adapter->process(actionFlags, timestamp, frameCount, outputBusNumber, outputData, + realtimeEventListHead, pullInputBlock); + }; +} + +// --- GUI methods for the view controller --- + +- (BOOL)createGUIInView:(NSView *)parentView width:(uint32_t *)outWidth height:(uint32_t *)outHeight +{ + if (!_impl || !_impl->_plugin || !_impl->_plugin->_ext._gui) return NO; + + auto *gui = _impl->_plugin->_ext._gui; + auto *plugin = _impl->_plugin->_plugin; + + if (!gui->is_api_supported(plugin, CLAP_WINDOW_API_COCOA, false)) return NO; + + if (!gui->create(plugin, CLAP_WINDOW_API_COCOA, false)) return NO; + + gui->set_scale(plugin, 1.0); + + uint32_t w = 0, h = 0; + gui->get_size(plugin, &w, &h); + + if (gui->can_resize(plugin)) + { + gui->adjust_size(plugin, &w, &h); + } + + clap_window_t window; + window.api = CLAP_WINDOW_API_COCOA; + window.cocoa = (__bridge void *)parentView; + gui->set_parent(plugin, &window); + gui->show(plugin); + + if (outWidth) *outWidth = w; + if (outHeight) *outHeight = h; + + // Update the IHost gui_request_resize to notify the view controller + _impl->_guiParentView = parentView; + + return YES; +} + +- (void)destroyGUI +{ + if (!_impl || !_impl->_plugin || !_impl->_plugin->_ext._gui) return; + + _impl->_plugin->_ext._gui->hide(_impl->_plugin->_plugin); + _impl->_plugin->_ext._gui->destroy(_impl->_plugin->_plugin); + _impl->_guiParentView = nil; +} + +- (BOOL)canResizeGUI +{ + if (!_impl || !_impl->_plugin || !_impl->_plugin->_ext._gui) return NO; + return _impl->_plugin->_ext._gui->can_resize(_impl->_plugin->_plugin) ? YES : NO; +} + +- (BOOL)setGUISize:(uint32_t)width height:(uint32_t)height +{ + if (!_impl || !_impl->_plugin || !_impl->_plugin->_ext._gui) return NO; + return _impl->_plugin->_ext._gui->set_size(_impl->_plugin->_plugin, width, height) ? YES : NO; +} + +// --- View controller --- + +- (void)requestViewControllerWithCompletionHandler:(void (^)(AUViewControllerBase *_Nullable))completionHandler +{ + if (!_impl || !_impl->_plugin || !_impl->_plugin->_ext._gui) + { + completionHandler(nil); + return; + } + + // Check if the CLAP plugin supports Cocoa GUI + if (!_impl->_plugin->_ext._gui->is_api_supported(_impl->_plugin->_plugin, CLAP_WINDOW_API_COCOA, false)) + { + completionHandler(nil); + return; + } + + // Create the view controller on the main thread + dispatch_async(dispatch_get_main_queue(), ^{ + ClapAUv3ViewController *vc = [[ClapAUv3ViewController alloc] init]; + vc.audioUnit = self; + completionHandler(vc); + }); +} + +@end + +// ----------------------------------------------------------------------- +// ClapAUv3ViewController implementation (also serves as AUAudioUnitFactory) +// ----------------------------------------------------------------------- + +@implementation ClapAUv3ViewController + +- (void)loadView +{ + // Create a plain NSView as the container + self.view = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 480, 360)]; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + if (!self.audioUnit) return; + + uint32_t w = 0, h = 0; + if ([self.audioUnit createGUIInView:self.view width:&w height:&h]) + { + if (w > 0 && h > 0) + { + self.preferredContentSize = NSMakeSize(w, h); + self.view.frame = NSMakeRect(0, 0, w, h); + } + } +} + +- (void)viewDidDisappear +{ + [self.audioUnit destroyGUI]; + [super viewDidDisappear]; +} + +// --- AUAudioUnitFactory --- +// Base implementation -- subclasses generated by build-helper override this. + +- (AUAudioUnit *)createAudioUnitWithComponentDescription:(AudioComponentDescription)desc + error:(NSError **)error +{ + if (error) + *error = [NSError errorWithDomain:@"ClapAUv3" code:-100 + userInfo:@{NSLocalizedDescriptionKey : @"Base factory should not be called directly"}]; + return nil; +} + +- (void)beginRequestWithExtensionContext:(NSExtensionContext *)context +{ + // Required by NSExtensionRequestHandling protocol. +} + +@end + +#pragma clang diagnostic pop diff --git a/src/detail/auv3/auv3_factory.h b/src/detail/auv3/auv3_factory.h new file mode 100644 index 00000000..8bfdf80d --- /dev/null +++ b/src/detail/auv3/auv3_factory.h @@ -0,0 +1,17 @@ +#pragma once + +/* + AUv3 Factory + + Copyright (c) 2024 Timo Kaluza (defiantnerd) + + This file is part of the clap-wrappers project which is released under MIT License. + See file LICENSE or go to https://github.com/free-audio/clap-wrapper for full license details. +*/ + +#import + +@interface ClapAUv3Factory : NSObject +- (AUAudioUnit *)createAudioUnitWithComponentDescription:(AudioComponentDescription)desc + error:(NSError **)error; +@end diff --git a/src/detail/auv3/auv3_parameters.h b/src/detail/auv3/auv3_parameters.h new file mode 100644 index 00000000..fed1490d --- /dev/null +++ b/src/detail/auv3/auv3_parameters.h @@ -0,0 +1,29 @@ +#pragma once + +/* + AUv3 Parameter Bridge + + Copyright (c) 2024 Timo Kaluza (defiantnerd) + + This file is part of the clap-wrappers project which is released under MIT License. + See file LICENSE or go to https://github.com/free-audio/clap-wrapper for full license details. + + Builds an AUParameterTree from CLAP parameters with proper grouping, + value observation, and string conversion callbacks. +*/ + +#import +#include +#include + +namespace Clap::AUv3 +{ + +// Build an AUParameterTree from the CLAP plugin's parameter extensions. +// The tree groups parameters by their module path (split on '/'). +// The callbacks (implementorValueObserver, implementorValueProvider, etc.) +// are wired to the provided plugin and params extension. +AUParameterTree *createParameterTree(const clap_plugin_t *plugin, + const clap_plugin_params_t *params); + +} // namespace Clap::AUv3 diff --git a/src/detail/auv3/auv3_parameters.mm b/src/detail/auv3/auv3_parameters.mm new file mode 100644 index 00000000..530bbede --- /dev/null +++ b/src/detail/auv3/auv3_parameters.mm @@ -0,0 +1,205 @@ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnullability-completeness" + +#include "auv3_parameters.h" + +#import +#import + +#include +#include +#include + +namespace Clap::AUv3 +{ + +// Split a module path like "Filter/Cutoff" into ["Filter", "Cutoff"] +static std::vector splitModulePath(const char *module) +{ + std::vector parts; + if (!module || !module[0]) return parts; + + std::string path(module); + size_t pos = 0; + while ((pos = path.find('/')) != std::string::npos) + { + auto part = path.substr(0, pos); + if (!part.empty()) parts.push_back(part); + path = path.substr(pos + 1); + } + if (!path.empty()) parts.push_back(path); + return parts; +} + +// Recursive tree node for building parameter groups +struct GroupNode +{ + std::string name; + std::map> children; + NSMutableArray *parameters = nil; + + GroupNode() : parameters([NSMutableArray new]) + { + } + + AUParameterGroup *toGroup() + { + NSMutableArray *groupChildren = [NSMutableArray new]; + + // Add sub-groups first + for (auto &[childName, child] : children) + { + [groupChildren addObject:child->toGroup()]; + } + // Then parameters + for (AUParameter *p in parameters) + { + [groupChildren addObject:p]; + } + + return [AUParameterTree createGroupWithIdentifier:[NSString stringWithUTF8String:name.c_str()] + name:[NSString stringWithUTF8String:name.c_str()] + children:groupChildren]; + } +}; + +AUParameterTree *createParameterTree(const clap_plugin_t *plugin, + const clap_plugin_params_t *params) +{ + if (!params) return [AUParameterTree createTreeWithChildren:@[]]; + + uint32_t numParams = params->count(plugin); + if (numParams == 0) return [AUParameterTree createTreeWithChildren:@[]]; + + // Root group node for building hierarchy + GroupNode root; + root.name = "Root"; + + // Top-level parameters (no module path) + NSMutableArray *topLevelChildren = [NSMutableArray new]; + + for (uint32_t i = 0; i < numParams; ++i) + { + clap_param_info_t info; + if (!params->get_info(plugin, i, &info)) continue; + + // Determine AU parameter unit + AudioUnitParameterUnit unit = kAudioUnitParameterUnit_Generic; + AudioUnitParameterOptions flags = + kAudioUnitParameterFlag_IsReadable | kAudioUnitParameterFlag_IsHighResolution | + kAudioUnitParameterFlag_HasCFNameString | kAudioUnitParameterFlag_CFNameRelease; + + bool isStepped = (info.flags & CLAP_PARAM_IS_STEPPED) != 0; + bool isHidden = (info.flags & CLAP_PARAM_IS_HIDDEN) != 0; + bool isReadonly = (info.flags & CLAP_PARAM_IS_READONLY) != 0; + bool isAutomatable = (info.flags & CLAP_PARAM_IS_AUTOMATABLE) != 0; + + if (isHidden) continue; // skip hidden parameters + + if (!isReadonly && isAutomatable) + { + flags |= kAudioUnitParameterFlag_IsWritable; + } + + if (isStepped) + { + if (info.min_value == 0.0 && info.max_value == 1.0) + { + unit = kAudioUnitParameterUnit_Boolean; + } + else + { + unit = kAudioUnitParameterUnit_Indexed; + } + } + + NSString *identifier = [NSString stringWithFormat:@"clap_%llu", (unsigned long long)info.id]; + NSString *displayName = [NSString stringWithUTF8String:info.name]; + + AUParameter *param = [AUParameterTree createParameterWithIdentifier:identifier + name:displayName + address:(AUParameterAddress)info.id + min:(AUValue)info.min_value + max:(AUValue)info.max_value + unit:unit + unitName:nil + flags:flags + valueStrings:nil + dependentParameters:nil]; + param.value = (AUValue)info.default_value; + + // Place in group hierarchy based on module path + auto moduleParts = splitModulePath(info.module); + if (moduleParts.empty()) + { + [topLevelChildren addObject:param]; + } + else + { + // Navigate/create the group hierarchy + GroupNode *current = &root; + for (const auto &part : moduleParts) + { + auto it = current->children.find(part); + if (it == current->children.end()) + { + auto node = std::make_unique(); + node->name = part; + current->children[part] = std::move(node); + current = current->children[part].get(); + } + else + { + current = it->second.get(); + } + } + [current->parameters addObject:param]; + } + } + + // Build the final tree: top-level groups + ungrouped params + for (auto &[childName, child] : root.children) + { + [topLevelChildren addObject:child->toGroup()]; + } + + AUParameterTree *tree = [AUParameterTree createTreeWithChildren:topLevelChildren]; + + // Wire up the callbacks using the plugin/params pointers. + // These blocks capture the raw pointers - they must remain valid for the lifetime of the tree. + const clap_plugin_t *capturedPlugin = plugin; + const clap_plugin_params_t *capturedParams = params; + + tree.implementorValueProvider = ^AUValue(AUParameter *param) { + double value = 0; + capturedParams->get_value(capturedPlugin, (clap_id)param.address, &value); + return (AUValue)value; + }; + + tree.implementorStringFromValueCallback = ^NSString *(AUParameter *param, const AUValue *value) { + char buf[256]; + AUValue v = value ? *value : param.value; + if (capturedParams->value_to_text(capturedPlugin, (clap_id)param.address, (double)v, buf, + sizeof(buf))) + { + return [NSString stringWithUTF8String:buf]; + } + return [NSString stringWithFormat:@"%.3f", v]; + }; + + tree.implementorValueFromStringCallback = ^AUValue(AUParameter *param, NSString *string) { + double value = 0; + if (capturedParams->text_to_value(capturedPlugin, (clap_id)param.address, + [string UTF8String], &value)) + { + return (AUValue)value; + } + return (AUValue)[string doubleValue]; + }; + + return tree; +} + +} // namespace Clap::AUv3 + +#pragma clang diagnostic pop diff --git a/src/detail/auv3/build-helper/auv3_infoplist_top.in b/src/detail/auv3/build-helper/auv3_infoplist_top.in new file mode 100644 index 00000000..6de4fea4 --- /dev/null +++ b/src/detail/auv3/build-helper/auv3_infoplist_top.in @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${AUV3_OUTPUT_NAME} + CFBundleIdentifier + ${AUV3_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${AUV3_OUTPUT_NAME} + CFBundlePackageType + XPC! + CFBundleShortVersionString + ${AUV3_BUNDLE_VERSION} + CFBundleSignature + ???? + CFBundleVersion + ${AUV3_BUNDLE_VERSION} + CFBundleSupportedPlatforms + + MacOSX + + LSMinimumSystemVersion + 10.13 + NSHighResolutionCapable + diff --git a/src/detail/auv3/build-helper/build-helper.cpp b/src/detail/auv3/build-helper/build-helper.cpp new file mode 100644 index 00000000..29b08ef1 --- /dev/null +++ b/src/detail/auv3/build-helper/build-helper.cpp @@ -0,0 +1,441 @@ +#include +#include +#include +#include +#include + +#include "detail/clap/fsutil.h" +#include "detail/os/fs.h" + +// Fowler-Noll-Vo hash function (same as in detail/aax/util.cpp) +// see https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function +static uint32_t fnv1a_keogh(const char *input) +{ + uint32_t hash = 0x811c9dc5; + while (*input) + { + hash ^= *input++; + hash *= 0x01000193; + hash = (0x19660d * hash) + 0x3c6ef35f; // LCG + } + return hash; +} + +// Generate a 4-character FourCC string from an arbitrary input string +// using a deterministic hash. Same approach as AAXIDfromString. +static const char _fourcc_map[65] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz$_"; +static std::string fourCCFromString(const std::string &input) +{ + uint32_t p = fnv1a_keogh(input.c_str()); + char result[5]; + result[0] = _fourcc_map[(p >> 0) & 0x3f]; + result[1] = _fourcc_map[(p >> 6) & 0x3f]; + result[2] = _fourcc_map[(p >> 12) & 0x3f]; + result[3] = _fourcc_map[(p >> 18) & 0x3f]; + result[4] = 0; + return result; +} + +struct auInfo +{ + std::string name, vers, type, subt, manu, manunm, clapid, desc, clapname, bundlevers; + bool explicitMode{false}; + std::vector tags; + + const std::string factoryBase{"ClapAUv3ViewController_inst"}; + + uint32_t bundleversToVersion() const + { + uint16_t rev[3]{0, 0, 0}; + auto sum = [&]() + { + auto res = std::max((rev[0] << 16) + (rev[1] << 8) + rev[2], 1); + return res; + }; + auto uv = bundlevers; + for (int i = 0; i < 3; ++i) + { + auto p = uv.find('.'); + if (p == std::string::npos) + { + return sum(); + } + auto sub = uv.substr(0, p); + rev[i] = std::atoi(sub.c_str()); + uv = uv.substr(p + 1); + } + return sum(); + } + + void writePListFragment(std::ostream &of, int idx) const + { + if (!clapid.empty()) + { + of << " \n"; + } + else + { + of << " \n"; + } + of << " \n" + << " name\n" + << " " << manunm << ": " << name << "\n" + << " description\n" + << " " << desc << "\n" + << " factoryFunction\n" + << " " << factoryBase << idx << "\n" + << " manufacturer\n" + << " " << manu << "\n" + << " subtype\n" + << " " << subt << "\n" + << " type\n" + << " " << type << "\n" + << " version\n" + << " " << bundleversToVersion() << "\n" + << " sandboxSafe\n" + << " \n" + << " resourceUsage\n" + << " \n" + << " network.client\n" + << " \n" + << " temporary-exception.files.all.read-write\n" + << " \n" + << " \n"; + + if (!tags.empty()) + { + of << " tags\n" + << " \n"; + for (auto tag : tags) + { + if (tag[0] >= 'a' && tag[0] <= 'z') + { + tag[0] = std::toupper(tag[0]); + } + of << " " << tag << "\n"; + } + of << " \n"; + } + of << " \n"; + } +}; + +bool buildUnitsFromClap(const std::string &clapfile, const std::string &clapname, std::string manu, + std::string manuName, std::string itype, std::string subt, + std::vector &units) +{ + Clap::Library loader; + if (!loader.load(clapfile)) + { + std::cout << "[ERROR] library.load of clapfile failed" << std::endl; + return false; + } + + int idx{0}; + + if (manu.empty() && loader._pluginFactoryAUv2Info == nullptr) + { + std::cout << "[ERROR] No manufacturer provider and no auv2 info available" << std::endl; + return false; + } + + if (manu.empty()) + { + manu = loader._pluginFactoryAUv2Info->manufacturer_code; + manuName = loader._pluginFactoryAUv2Info->manufacturer_name; + std::cout << " - using factory manufacturer '" << manuName << "' (" << manu << ")" << std::endl; + } + + if (!itype.empty() && !subt.empty() && loader.plugins.size() > 1) + { + std::cout << "[ERROR] Multi-plugin claps must specify itype and subtype via extension" << std::endl; + } + + for (const auto *clapPlug : loader.plugins) + { + auto u = auInfo(); + bool doExport = true; + + u.name = clapPlug->name; + u.clapname = clapname; + u.clapid = clapPlug->id; + u.vers = clapPlug->version; + u.desc = clapPlug->description; + + // Generate a deterministic FourCC subtype from manufacturer + plugin id + // using the fnv1a hash (same approach as AAX wrapper) + if (subt.empty()) + { + std::string hashInput = manu + ":" + std::string(clapPlug->id); + u.subt = fourCCFromString(hashInput); + std::cout << " - generated subtype '" << u.subt << "' from '" << hashInput << "'" << std::endl; + } + else + { + u.subt = subt; + } + u.manu = manu; + u.manunm = manuName; + + auto f = clapPlug->features[0]; + if (!itype.empty()) + { + u.type = itype; + } + else if (f == nullptr || strcmp(f, CLAP_PLUGIN_FEATURE_INSTRUMENT) == 0) + { + u.type = "aumu"; + } + else if (strcmp(f, CLAP_PLUGIN_FEATURE_AUDIO_EFFECT) == 0) + { + u.type = "aufx"; + } + else if (strcmp(f, CLAP_PLUGIN_FEATURE_NOTE_EFFECT) == 0) + { + u.type = "aumi"; + } + else + { + std::cout << "[WARNING] can't determine instrument type. Using aumu" << std::endl; + u.type = "aumu"; + } + + auto fp = clapPlug->features; + while (*fp) + { + u.tags.push_back(*fp); + ++fp; + } + + if (loader._pluginFactoryAUv2Info) + { + clap_plugin_info_as_auv2_t v2inf; + auto res = + loader._pluginFactoryAUv2Info->get_auv2_info(loader._pluginFactoryAUv2Info, idx, &v2inf); + if (res) + { + if (v2inf.au_type[0] != 0) + { + u.type = v2inf.au_type; + } + if (v2inf.au_subt[0] != 0) + { + u.subt = v2inf.au_subt; + } + } + else + { + doExport = false; + std::cout << " - Skipping Audio Unit Export for index " << idx << "/" << u.clapid << std::endl; + } + } + + if (doExport) + { + units.push_back(u); + } + idx++; + } + return true; +} + +int main(int argc, char **argv) +{ + if (argc < 2) return 1; + + std::cout << "clap-wrapper: auv3 configuration tool starting\n"; + + std::vector units; + if (std::string(argv[1]) == "--explicit") + { + if (argc != 8) + { + std::cout << "[ERROR] Configuration incorrect. Got " << argc << " arguments in explicit" + << std::endl; + return 5; + } + int idx = 2; + auInfo u; + u.explicitMode = true; + u.name = std::string(argv[idx++]); + u.clapname = u.name; + u.vers = std::string(argv[idx++]); + u.bundlevers = u.vers; + u.type = std::string(argv[idx++]); + u.subt = std::string(argv[idx++]); + u.manu = std::string(argv[idx++]); + u.manunm = std::string(argv[idx++]); + u.desc = u.name + " CLAP to AUv3 Wrapper"; + + std::cout << " - single plugin explicit mode: " << u.name << " (" << u.type << "/" << u.subt << ")" + << std::endl; + units.push_back(u); + } + else if (std::string(argv[1]) == "--fromclap") + { + if (argc < 4) + { + std::cout << "[ERROR] Configuration incorrect. Got " << argc << " arguments in fromclap" + << std::endl; + return 6; + } + int idx = 2; + auto clapname = std::string(argv[idx++]); + auto clapfile = std::string(argv[idx++]); + auto bundlev = std::string(argv[idx++]); + + auto nerr = [](const auto &a) + { + if (a == "errr") + return std::string(); + else + return a; + }; + + auto mcode = nerr((idx < argc) ? std::string(argv[idx++]) : std::string()); + auto mname = nerr((idx < argc) ? std::string(argv[idx++]) : std::string()); + + auto itype = nerr((idx < argc) ? std::string(argv[idx++]) : std::string()); + auto isubt = nerr((idx < argc) ? std::string(argv[idx++]) : std::string()); + + try + { + auto p = fs::path{clapfile}; + if (fs::is_directory(p)) + { + std::cout << " - CLAP is a directory. Assuming bundle\n"; + } + else + { + std::cout << " - CLAP is a regular file. Assuming dll in bundle\n"; + p = p.parent_path().parent_path().parent_path(); + clapfile = p.u8string(); + } + } + catch (const fs::filesystem_error &e) + { + std::cout << "[ERROR] cant get path " << e.what() << std::endl; + return 3; + } + + std::cout << " - building information from CLAP directly\n" + << " - source clap: '" << clapfile << "'" << std::endl; + + if (!buildUnitsFromClap(clapfile, clapname, mcode, mname, itype, isubt, units)) + { + std::cout << "[ERROR] Can't build units from CLAP" << std::endl; + return 4; + } + + if (units.empty()) + { + std::cout << "[ERROR] No units from clap file\n"; + return 5; + } + + for (auto &u : units) + { + u.bundlevers = bundlev; + } + + std::cout << " - clap file produced " << units.size() << " units" << std::endl; + } + else + { + std::cout << "[ERROR] Unknown Mode : " << argv[1] << std::endl; + return 2; + } + + // --- Generate auv3_Info.plist --- + std::cout << " - generating auv3_Info.plist from auv3_infoplist_top" << std::endl; + std::ifstream intop("auv3_infoplist_top"); + if (!intop.is_open()) + { + std::cerr << "[ERROR] Unable to open pre-generated file auv3_infoplist_top" << std::endl; + return 1; + } + + std::ofstream of("auv3_Info.plist"); + if (!of.is_open()) + { + std::cerr << "[ERROR] Unable to open output file auv3_Info.plist" << std::endl; + return 1; + } + of << intop.rdbuf(); + + // The principal class is the first factory subclass + std::string principalClass = units[0].factoryBase + "0"; + + of << " NSExtension\n" + << " \n" + << " NSExtensionPointIdentifier\n" + << " com.apple.AudioUnit-UI\n" + << " NSExtensionPrincipalClass\n" + << " " << principalClass << "\n" + << " NSExtensionAttributes\n" + << " \n"; + + of << " AudioComponents\n \n"; + int idx{0}; + for (const auto &u : units) + { + std::cout << " + " << u.name << " (" << u.type << "/" << u.subt << ") by " << u.manunm << " (" + << u.manu << ")" << std::endl; + u.writePListFragment(of, idx++); + } + of << " \n"; + of << " \n"; // close NSExtensionAttributes + of << " \n"; // close NSExtension + of << " \n\n"; + of.close(); + std::cout << " - auv3_Info.plist generated" << std::endl; + + // --- Generate generated_auv3_entrypoints.hxx --- + { + std::cout << " - generating generated_auv3_entrypoints.hxx" << std::endl; + std::ofstream cppf("generated_auv3_entrypoints.hxx"); + if (!cppf.is_open()) + { + std::cout << "[ERROR] Unable to open generated_auv3_entrypoints.hxx" << std::endl; + return 1; + } + + cppf << "#pragma once\n\n"; + cppf << "// Generated by AUv3 build helper - do not edit\n\n"; + cppf << "#import \"detail/auv3/auv3_audiounit.h\"\n\n"; + + idx = 0; + for (const auto &u : units) + { + auto vcName = u.factoryBase + std::to_string(idx); + + std::cout << " + " << u.name << " view controller " << vcName << std::endl; + + // Generate a unique AUViewController subclass per plugin + // This class serves as both the view controller AND the AUAudioUnitFactory + cppf << "// ViewController/Factory for '" << u.name << "' (" << u.type << "/" << u.subt << ")\n"; + cppf << "@interface " << vcName + << " : ClapAUv3ViewController\n" + << "@end\n\n"; + cppf << "@implementation " << vcName << "\n"; + cppf << "- (AUAudioUnit *)createAudioUnitWithComponentDescription:(AudioComponentDescription)desc\n" + << " error:(NSError **)error {\n" + << " ClapAUv3AudioUnit *au = [[ClapAUv3AudioUnit alloc] initWithComponentDescription:desc\n" + << " options:0\n" + << " error:error\n" + << " clapName:@\"" << u.clapname << "\"\n" + << " clapId:@\"" << u.clapid << "\"\n" + << " clapIndex:" << idx << "];\n" + << " self.audioUnit = au;\n" + << " return au;\n" + << "}\n" + << "@end\n\n"; + + idx++; + } + cppf.close(); + std::cout << " - generated_auv3_entrypoints.hxx generated" << std::endl; + } + + return 0; +} diff --git a/src/detail/auv3/process.h b/src/detail/auv3/process.h new file mode 100644 index 00000000..f074857f --- /dev/null +++ b/src/detail/auv3/process.h @@ -0,0 +1,115 @@ +#pragma once + +/* + AUv3 Process Adapter + + Copyright (c) 2024 Timo Kaluza (defiantnerd) + + This file is part of the clap-wrappers project which is released under MIT License. + See file LICENSE or go to https://github.com/free-audio/clap-wrapper for full license details. + + The AUv3 process adapter translates between the AUv3 render block model + (AURenderEvent linked list, AURenderPullInputBlock) and the CLAP process model + (clap_process_t with event lists and audio buffers). +*/ + +#include + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnullability-completeness" + +#import +#import +#include +#include +#include "../clap/automation.h" + +namespace Clap::AUv3 +{ + +typedef union clap_multi_event +{ + clap_event_header_t header; + clap_event_note_t note; + clap_event_midi_t midi; + clap_event_midi_sysex_t sysex; + clap_event_param_value_t param; + clap_event_note_expression_t noteexpression; +} clap_multi_event_t; + +class ProcessAdapter +{ + public: + ProcessAdapter() = default; + ~ProcessAdapter(); + + // Set up the processing state for the given bus configuration. + // Called once when render resources are allocated. + void setupProcessing(uint32_t numInputBusses, const uint32_t *inputChannelCounts, + uint32_t numOutputBusses, const uint32_t *outputChannelCounts, + const clap_plugin_t *plugin, const clap_plugin_params_t *ext_params, + Clap::IAutomation *automation, uint32_t numMaxSamples, + uint32_t preferredMIDIDialect); + + // Main render call - invoked from the AUv3 internalRenderBlock. + // Translates AUv3 events, pulls input, calls CLAP process, and writes output. + AUAudioUnitStatus process(AudioUnitRenderActionFlags *actionFlags, + const AudioTimeStamp *timestamp, AVAudioFrameCount frameCount, + NSInteger outputBusNumber, AudioBufferList *outputData, + const AURenderEvent *realtimeEventListHead, + AURenderPullInputBlock __unsafe_unretained pullInputBlock); + + // Provide transport state from the host + void setTransportStateBlock(AUHostTransportStateBlock __nullable block); + + // Queue a parameter change from the host (outside render block) + void addParameterEvent(clap_id paramId, double value, uint32_t sampleOffset); + + // MIDI output event block (set by the AU host) + AUMIDIOutputEventBlock __nullable midiOutputEventBlock; + + private: + static uint32_t input_events_size(const struct clap_input_events *list); + static const clap_event_header_t *input_events_get(const struct clap_input_events *list, + uint32_t index); + static bool output_events_try_push(const struct clap_output_events *list, + const clap_event_header_t *event); + + void sortEventIndices(); + bool enqueueOutputEvent(const clap_event_header_t *event); + void translateAUv3Events(const AURenderEvent *head); + + const clap_plugin_t *_plugin = nullptr; + const clap_plugin_params_t *_ext_params = nullptr; + Clap::IAutomation *_automation = nullptr; + + uint32_t _numInputs = 0; + uint32_t _numOutputs = 0; + + clap_audio_buffer_t *_input_ports = nullptr; + clap_audio_buffer_t *_output_ports = nullptr; + clap_event_transport_t _transport = {}; + clap_input_events_t _in_events = {}; + clap_output_events_t _out_events = {}; + + float *_silent_input = nullptr; + float *_silent_output = nullptr; + + clap_process_t _processData = {-1, 0, &_transport, nullptr, nullptr, 0, 0, &_in_events, &_out_events}; + + std::vector _events; + std::vector _eventindices; + std::vector _outevents; + + uint32_t _preferred_midi_dialect = CLAP_NOTE_DIALECT_CLAP; + + AUHostTransportStateBlock __nullable _transportStateBlock = nil; + + // Temporary storage for input pulling + AudioBufferList *_inputBufferList = nullptr; + uint32_t _inputBufferListChannels = 0; +}; + +} // namespace Clap::AUv3 + +#pragma clang diagnostic pop diff --git a/src/detail/auv3/process.mm b/src/detail/auv3/process.mm new file mode 100644 index 00000000..d856c1a4 --- /dev/null +++ b/src/detail/auv3/process.mm @@ -0,0 +1,524 @@ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnullability-completeness" + +#include "process.h" + +#include +#include +#include + +namespace Clap::AUv3 +{ + +inline clap_beattime doubleToBeatTime(double t) +{ + return std::round(t * CLAP_BEATTIME_FACTOR); +} + +inline clap_sectime doubleToSecTime(double t) +{ + return std::round(t * CLAP_SECTIME_FACTOR); +} + +ProcessAdapter::~ProcessAdapter() +{ + if (_input_ports) + { + for (uint32_t i = 0; i < _numInputs; ++i) + { + delete[] _input_ports[i].data32; + } + delete[] _input_ports; + _input_ports = nullptr; + } + if (_output_ports) + { + for (uint32_t i = 0; i < _numOutputs; ++i) + { + delete[] _output_ports[i].data32; + } + delete[] _output_ports; + _output_ports = nullptr; + } + delete[] _silent_input; + _silent_input = nullptr; + delete[] _silent_output; + _silent_output = nullptr; + + if (_inputBufferList) + { + free(_inputBufferList); + _inputBufferList = nullptr; + } +} + +void ProcessAdapter::setupProcessing(uint32_t numInputBusses, const uint32_t *inputChannelCounts, + uint32_t numOutputBusses, const uint32_t *outputChannelCounts, + const clap_plugin_t *plugin, + const clap_plugin_params_t *ext_params, + Clap::IAutomation *automation, uint32_t numMaxSamples, + uint32_t preferredMIDIDialect) +{ + _plugin = plugin; + _ext_params = ext_params; + _automation = automation; + _preferred_midi_dialect = preferredMIDIDialect; + + // Setup silent buffers + if (numMaxSamples > 0) + { + delete[] _silent_input; + _silent_input = new float[numMaxSamples]; + memset(_silent_input, 0, numMaxSamples * sizeof(float)); + + delete[] _silent_output; + _silent_output = new float[numMaxSamples]; + memset(_silent_output, 0, numMaxSamples * sizeof(float)); + } + + // Setup input ports + _numInputs = numInputBusses; + delete[] _input_ports; + _input_ports = nullptr; + + if (_numInputs > 0) + { + _input_ports = new clap_audio_buffer_t[_numInputs]; + for (uint32_t i = 0; i < _numInputs; ++i) + { + auto &bus = _input_ports[i]; + bus.channel_count = inputChannelCounts[i]; + bus.constant_mask = 0; + bus.latency = 0; + bus.data64 = nullptr; + bus.data32 = new float *[bus.channel_count]; + for (uint32_t j = 0; j < bus.channel_count; ++j) + { + bus.data32[j] = _silent_input; + } + } + } + + // Setup output ports + _numOutputs = numOutputBusses; + delete[] _output_ports; + _output_ports = nullptr; + + if (_numOutputs > 0) + { + _output_ports = new clap_audio_buffer_t[_numOutputs]; + for (uint32_t i = 0; i < _numOutputs; ++i) + { + auto &bus = _output_ports[i]; + bus.channel_count = outputChannelCounts[i]; + bus.constant_mask = 0; + bus.latency = 0; + bus.data64 = nullptr; + bus.data32 = new float *[bus.channel_count]; + for (uint32_t j = 0; j < bus.channel_count; ++j) + { + bus.data32[j] = _silent_output; + } + } + } + + // Allocate input buffer list for pulling input + if (_numInputs > 0) + { + uint32_t maxCh = 0; + for (uint32_t i = 0; i < _numInputs; ++i) + { + if (inputChannelCounts[i] > maxCh) maxCh = inputChannelCounts[i]; + } + if (_inputBufferList) free(_inputBufferList); + size_t ablSize = sizeof(AudioBufferList) + (maxCh > 1 ? (maxCh - 1) * sizeof(AudioBuffer) : 0); + _inputBufferList = (AudioBufferList *)calloc(1, ablSize); + _inputBufferListChannels = maxCh; + } + + // Wire up CLAP process data + _processData.audio_inputs = _input_ports; + _processData.audio_inputs_count = _numInputs; + _processData.audio_outputs = _output_ports; + _processData.audio_outputs_count = _numOutputs; + + _processData.in_events = &_in_events; + _processData.out_events = &_out_events; + + _transport.header.space_id = CLAP_CORE_EVENT_SPACE_ID; + _transport.header.type = CLAP_EVENT_TRANSPORT; + _transport.header.time = 0; + _transport.header.size = sizeof(clap_event_transport_t); + _processData.transport = &_transport; + + _in_events.ctx = this; + _in_events.size = input_events_size; + _in_events.get = input_events_get; + + _out_events.ctx = this; + _out_events.try_push = output_events_try_push; + + _events.clear(); + _events.reserve(8192); + _eventindices.clear(); + _eventindices.reserve(8192); +} + +void ProcessAdapter::setTransportStateBlock(AUHostTransportStateBlock __nullable block) +{ + _transportStateBlock = block; +} + +void ProcessAdapter::sortEventIndices() +{ + std::sort(_eventindices.begin(), _eventindices.end(), + [&](size_t const &a, size_t const &b) + { + auto t1 = _events[a].header.time; + auto t2 = _events[b].header.time; + return (t1 == t2) ? (a < b) : (t1 < t2); + }); +} + +void ProcessAdapter::translateAUv3Events(const AURenderEvent *head) +{ + for (const AURenderEvent *event = head; event != nullptr; event = event->head.next) + { + clap_multi_event_t n; + memset(&n, 0, sizeof(n)); + + switch (event->head.eventType) + { + case AURenderEventParameter: + case AURenderEventParameterRamp: + { + auto &pe = event->parameter; + n.header.size = sizeof(clap_event_param_value_t); + n.header.type = CLAP_EVENT_PARAM_VALUE; + n.header.space_id = CLAP_CORE_EVENT_SPACE_ID; + n.header.time = (uint32_t)pe.eventSampleTime; + n.header.flags = 0; + + n.param.param_id = (clap_id)pe.parameterAddress; + n.param.value = (double)pe.value; + n.param.port_index = -1; + n.param.key = -1; + n.param.channel = -1; + n.param.note_id = -1; + n.param.cookie = nullptr; + + _eventindices.emplace_back(_events.size()); + _events.emplace_back(n); + break; + } + + case AURenderEventMIDI: + { + auto &me = event->MIDI; + uint8_t status = me.data[0]; + uint8_t strippedStatus = (status >> 4) & 0x0F; + uint8_t channel = status & 0x0F; + + n.header.time = (uint32_t)me.eventSampleTime; + n.header.flags = 0; + n.header.space_id = CLAP_CORE_EVENT_SPACE_ID; + + if (_preferred_midi_dialect == CLAP_NOTE_DIALECT_CLAP) + { + if (strippedStatus == 0x09 && me.data[2] > 0) // Note On + { + n.header.type = CLAP_EVENT_NOTE_ON; + n.header.size = sizeof(clap_event_note_t); + n.note.port_index = 0; + n.note.note_id = -1; + n.note.key = me.data[1] & 0x7F; + n.note.velocity = (float)(me.data[2] & 0x7F) / 127.0f; + n.note.channel = channel; + + _eventindices.emplace_back(_events.size()); + _events.emplace_back(n); + break; + } + else if (strippedStatus == 0x08 || (strippedStatus == 0x09 && me.data[2] == 0)) // Note Off + { + n.header.type = CLAP_EVENT_NOTE_OFF; + n.header.size = sizeof(clap_event_note_t); + n.note.port_index = 0; + n.note.note_id = -1; + n.note.key = me.data[1] & 0x7F; + n.note.velocity = (strippedStatus == 0x08) ? (float)(me.data[2] & 0x7F) / 127.0f : 0.0f; + n.note.channel = channel; + + _eventindices.emplace_back(_events.size()); + _events.emplace_back(n); + break; + } + } + + // Fall through for non-note MIDI or MIDI dialect preference + n.header.type = CLAP_EVENT_MIDI; + n.header.size = sizeof(clap_event_midi_t); + n.midi.port_index = 0; + n.midi.data[0] = me.data[0]; + n.midi.data[1] = me.data[1]; + n.midi.data[2] = me.data[2]; + + _eventindices.emplace_back(_events.size()); + _events.emplace_back(n); + break; + } + + case AURenderEventMIDISysEx: + { + // SysEx uses the same AUMIDIEvent struct with extended data + auto &se = event->MIDI; + n.header.type = CLAP_EVENT_MIDI_SYSEX; + n.header.size = sizeof(clap_event_midi_sysex_t); + n.header.space_id = CLAP_CORE_EVENT_SPACE_ID; + n.header.time = (uint32_t)se.eventSampleTime; + n.header.flags = 0; + n.sysex.port_index = 0; + n.sysex.buffer = se.data; + n.sysex.size = se.length; + + _eventindices.emplace_back(_events.size()); + _events.emplace_back(n); + break; + } + + default: + // AURenderEventMIDIEventList (MIDI 2.0) - not yet supported + break; + } + } +} + +AUAudioUnitStatus ProcessAdapter::process(AudioUnitRenderActionFlags *actionFlags, + const AudioTimeStamp *timestamp, + AVAudioFrameCount frameCount, + NSInteger outputBusNumber, + AudioBufferList *outputData, + const AURenderEvent *realtimeEventListHead, + AURenderPullInputBlock __unsafe_unretained pullInputBlock) +{ + // Clear events from previous cycle + _events.clear(); + _eventindices.clear(); + + // Translate AUv3 events to CLAP events + if (realtimeEventListHead) + { + translateAUv3Events(realtimeEventListHead); + } + + // Sort events by timestamp + sortEventIndices(); + + _processData.frames_count = frameCount; + + // Setup transport + _transport.flags = 0; + if (_transportStateBlock) + { + AUHostTransportStateFlags transportFlags = 0; + double currentSamplePosition = 0; + double cycleStartBeatPosition = 0; + double cycleEndBeatPosition = 0; + + // Query transport state + if (_transportStateBlock(&transportFlags, ¤tSamplePosition, &cycleStartBeatPosition, + &cycleEndBeatPosition)) + { + if (transportFlags & AUHostTransportStateMoving) + { + _transport.flags |= CLAP_TRANSPORT_IS_PLAYING; + } + if (transportFlags & AUHostTransportStateRecording) + { + _transport.flags |= CLAP_TRANSPORT_IS_RECORDING; + } + if (transportFlags & AUHostTransportStateCycling) + { + _transport.flags |= CLAP_TRANSPORT_IS_LOOP_ACTIVE; + _transport.loop_start_beats = doubleToBeatTime(cycleStartBeatPosition); + _transport.loop_end_beats = doubleToBeatTime(cycleEndBeatPosition); + } + } + + // Note: AUv3 provides tempo via the musicalContextBlock property + // which can be queried separately if needed in the future. + } + + // Pull input audio + if (_numInputs > 0 && pullInputBlock) + { + for (uint32_t bus = 0; bus < _numInputs; ++bus) + { + uint32_t numCh = _input_ports[bus].channel_count; + + // Setup the input buffer list for this bus + _inputBufferList->mNumberBuffers = numCh; + for (uint32_t ch = 0; ch < numCh; ++ch) + { + _inputBufferList->mBuffers[ch].mNumberChannels = 1; + _inputBufferList->mBuffers[ch].mDataByteSize = frameCount * sizeof(float); + _inputBufferList->mBuffers[ch].mData = nullptr; // let the host provide the buffer + } + + AudioUnitRenderActionFlags pullFlags = 0; + AUAudioUnitStatus status = pullInputBlock(&pullFlags, timestamp, frameCount, bus, _inputBufferList); + if (status == noErr) + { + for (uint32_t ch = 0; ch < numCh && ch < _inputBufferList->mNumberBuffers; ++ch) + { + _input_ports[bus].data32[ch] = (float *)_inputBufferList->mBuffers[ch].mData; + } + } + else + { + // Fill with silence on pull failure + for (uint32_t ch = 0; ch < numCh; ++ch) + { + _input_ports[bus].data32[ch] = _silent_input; + } + } + } + } + + // Wire output buffers + if (outputData && outputBusNumber < _numOutputs) + { + uint32_t outBus = (uint32_t)outputBusNumber; + uint32_t numCh = std::min((uint32_t)outputData->mNumberBuffers, _output_ports[outBus].channel_count); + for (uint32_t ch = 0; ch < numCh; ++ch) + { + _output_ports[outBus].data32[ch] = (float *)outputData->mBuffers[ch].mData; + } + } + + // Process! + _plugin->process(_plugin, &_processData); + + // Process output events + for (auto &evt : _outevents) + { + switch (evt.header.type) + { + case CLAP_EVENT_PARAM_VALUE: + if (_automation) + { + _automation->onPerformEdit(&evt.param); + } + break; + case CLAP_EVENT_PARAM_GESTURE_BEGIN: + { + auto *ge = (clap_event_param_gesture *)&evt; + if (_automation) _automation->onBeginEdit(ge->param_id); + break; + } + case CLAP_EVENT_PARAM_GESTURE_END: + { + auto *ge = (clap_event_param_gesture *)&evt; + if (_automation) _automation->onEndEdit(ge->param_id); + break; + } + case CLAP_EVENT_NOTE_ON: + case CLAP_EVENT_NOTE_OFF: + case CLAP_EVENT_MIDI: + { + if (midiOutputEventBlock) + { + if (evt.header.type == CLAP_EVENT_MIDI) + { + midiOutputEventBlock(timestamp->mSampleTime + evt.header.time, 0, 3, evt.midi.data); + } + else if (evt.header.type == CLAP_EVENT_NOTE_ON) + { + uint8_t data[3] = {(uint8_t)(0x90 | (evt.note.channel & 0x0F)), + (uint8_t)(evt.note.key & 0x7F), + (uint8_t)(uint8_t)(evt.note.velocity * 127.0f)}; + midiOutputEventBlock(timestamp->mSampleTime + evt.header.time, 0, 3, data); + } + else if (evt.header.type == CLAP_EVENT_NOTE_OFF) + { + uint8_t data[3] = {(uint8_t)(0x80 | (evt.note.channel & 0x0F)), + (uint8_t)(evt.note.key & 0x7F), + (uint8_t)(uint8_t)(evt.note.velocity * 127.0f)}; + midiOutputEventBlock(timestamp->mSampleTime + evt.header.time, 0, 3, data); + } + } + break; + } + default: + break; + } + } + _outevents.clear(); + + return noErr; +} + +void ProcessAdapter::addParameterEvent(clap_id paramId, double value, uint32_t sampleOffset) +{ + clap_multi_event_t n; + memset(&n, 0, sizeof(n)); + n.header.size = sizeof(clap_event_param_value_t); + n.header.type = CLAP_EVENT_PARAM_VALUE; + n.header.space_id = CLAP_CORE_EVENT_SPACE_ID; + n.header.time = sampleOffset; + n.header.flags = 0; + + n.param.value = value; + n.param.param_id = paramId; + n.param.cookie = nullptr; + n.param.port_index = -1; + n.param.key = -1; + n.param.channel = -1; + n.param.note_id = -1; + + _eventindices.emplace_back(_events.size()); + _events.emplace_back(n); +} + +// --- CLAP event callbacks --- + +uint32_t ProcessAdapter::input_events_size(const struct clap_input_events *list) +{ + auto self = static_cast(list->ctx); + return (uint32_t)self->_events.size(); +} + +const clap_event_header_t *ProcessAdapter::input_events_get(const struct clap_input_events *list, + uint32_t index) +{ + auto self = static_cast(list->ctx); + if (index < self->_events.size()) + { + auto realindex = self->_eventindices[index]; + return &(self->_events[realindex].header); + } + return nullptr; +} + +bool ProcessAdapter::output_events_try_push(const struct clap_output_events *list, + const clap_event_header_t *event) +{ + auto self = static_cast(list->ctx); + return self->enqueueOutputEvent(event); +} + +bool ProcessAdapter::enqueueOutputEvent(const clap_event_header_t *event) +{ + if (event->size <= sizeof(clap_multi_event_t)) + { + clap_multi_event_t e; + memcpy(&e, event, event->size); + _outevents.emplace_back(e); + return true; + } + return false; +} + +} // namespace Clap::AUv3 + +#pragma clang diagnostic pop diff --git a/src/detail/standalone/macos/auv3/AUv3HostAppDelegate.h b/src/detail/standalone/macos/auv3/AUv3HostAppDelegate.h new file mode 100644 index 00000000..b80fff76 --- /dev/null +++ b/src/detail/standalone/macos/auv3/AUv3HostAppDelegate.h @@ -0,0 +1,9 @@ +#pragma once + +#import + +@interface AUv3HostAppDelegate : NSObject + +@property (nonatomic, weak) IBOutlet NSWindow *window; + +@end diff --git a/src/detail/standalone/macos/auv3/AUv3HostAppDelegate.mm b/src/detail/standalone/macos/auv3/AUv3HostAppDelegate.mm new file mode 100644 index 00000000..02085004 --- /dev/null +++ b/src/detail/standalone/macos/auv3/AUv3HostAppDelegate.mm @@ -0,0 +1,627 @@ +#import "AUv3HostAppDelegate.h" + +#import +#import +#import +#import + +#include + +// --- FourCC helper: convert a 4-char string like "aufx" to a uint32_t --- +static uint32_t fourCCFromString(const char *s) +{ + if (!s || strlen(s) < 4) return 0; + return ((uint32_t)s[0] << 24) | ((uint32_t)s[1] << 16) | ((uint32_t)s[2] << 8) | (uint32_t)s[3]; +} + +// --- MIDI input state --- +static MIDIClientRef sMIDIClient = 0; +static MIDIPortRef sMIDIInputPort = 0; + +@implementation AUv3HostAppDelegate +{ + AVAudioEngine *_engine; + AVAudioUnit *_avAudioUnit; + AUAudioUnit *_directAU; // used when loading appex directly (no system registration) + NSViewController *_auViewController; + NSString *_settingsPath; + + // MIDI + AUScheduleMIDIEventBlock _scheduleMIDIBlock; +} + +// --------------------------------------------------------------------------- +#pragma mark - Application lifecycle +// --------------------------------------------------------------------------- + +- (void)applicationDidFinishLaunching:(NSNotification *)notification +{ + [NSApp activateIgnoringOtherApps:YES]; + + // Defer setup slightly so the window is visible + [NSTimer scheduledTimerWithTimeInterval:0.001 + target:self + selector:@selector(doSetup) + userInfo:nil + repeats:NO]; +} + +- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender +{ + return YES; +} + +- (void)applicationWillTerminate:(NSNotification *)notification +{ + [self teardownMIDI]; + [self saveState]; + + if (_engine) + { + [_engine stop]; + + if (_avAudioUnit) + { + [_engine detachNode:_avAudioUnit]; + } + _engine = nil; + } + + if (_directAU) + { + [_directAU deallocateRenderResources]; + _directAU = nil; + } + + _avAudioUnit = nil; + _auViewController = nil; +} + +// --------------------------------------------------------------------------- +#pragma mark - Appex Registration +// --------------------------------------------------------------------------- + +- (NSBundle *)findEmbeddedAppexBundle +{ + NSString *plugInsPath = [[NSBundle mainBundle] builtInPlugInsPath]; + if (!plugInsPath) return nil; + + NSArray *contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:plugInsPath error:nil]; + for (NSString *item in contents) + { + if ([item hasSuffix:@".appex"]) + { + NSString *appexPath = [plugInsPath stringByAppendingPathComponent:item]; + NSBundle *bundle = [NSBundle bundleWithPath:appexPath]; + if (bundle) + { + std::cout << "[auv3-standalone] Found appex bundle: " << [appexPath UTF8String] << std::endl; + return bundle; + } + } + } + return nil; +} + +- (AUAudioUnit *)instantiateAUDirectlyFromAppex:(NSBundle *)appexBundle + componentDescription:(AudioComponentDescription)desc + error:(NSError **)outError +{ + // Load the appex bundle to get its Objective-C classes + if (![appexBundle isLoaded]) + { + NSError *loadError = nil; + if (![appexBundle loadAndReturnError:&loadError]) + { + std::cout << "[auv3-standalone] ERROR: Failed to load appex bundle: " + << [loadError.localizedDescription UTF8String] << std::endl; + if (outError) *outError = loadError; + return nil; + } + std::cout << "[auv3-standalone] Appex bundle loaded" << std::endl; + } + + // Get the principal class from the appex's Info.plist + NSDictionary *plist = appexBundle.infoDictionary; + NSDictionary *nsExt = plist[@"NSExtension"]; + NSString *principalClassName = nsExt[@"NSExtensionPrincipalClass"]; + + if (!principalClassName) + { + std::cout << "[auv3-standalone] ERROR: No NSExtensionPrincipalClass in appex" << std::endl; + if (outError) + *outError = [NSError errorWithDomain:@"ClapAUv3" code:-1 + userInfo:@{NSLocalizedDescriptionKey: @"No principal class in appex"}]; + return nil; + } + + std::cout << "[auv3-standalone] Principal class: " << [principalClassName UTF8String] << std::endl; + + // Get the factory class + Class factoryClass = NSClassFromString(principalClassName); + if (!factoryClass) + { + // Try loading from the bundle explicitly + factoryClass = [appexBundle classNamed:principalClassName]; + } + + if (!factoryClass) + { + std::cout << "[auv3-standalone] ERROR: Cannot find class " << [principalClassName UTF8String] << std::endl; + if (outError) + *outError = [NSError errorWithDomain:@"ClapAUv3" code:-2 + userInfo:@{NSLocalizedDescriptionKey: + [NSString stringWithFormat:@"Cannot find class %@", principalClassName]}]; + return nil; + } + + // The factory class conforms to AUAudioUnitFactory + if (![factoryClass conformsToProtocol:@protocol(AUAudioUnitFactory)]) + { + std::cout << "[auv3-standalone] ERROR: Principal class does not conform to AUAudioUnitFactory" << std::endl; + if (outError) + *outError = [NSError errorWithDomain:@"ClapAUv3" code:-3 + userInfo:@{NSLocalizedDescriptionKey: @"Principal class is not an AUAudioUnitFactory"}]; + return nil; + } + + // Create the factory and ask it to create the AU + id factory = [[factoryClass alloc] init]; + AUAudioUnit *au = [factory createAudioUnitWithComponentDescription:desc error:outError]; + + if (!au) + { + std::cout << "[auv3-standalone] ERROR: Factory returned nil" << std::endl; + return nil; + } + + std::cout << "[auv3-standalone] AUAudioUnit created directly from appex" << std::endl; + return au; +} + +// --------------------------------------------------------------------------- +#pragma mark - Setup +// --------------------------------------------------------------------------- + +- (void)doSetup +{ +#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 101400 + // Request microphone permission + switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]) + { + case AVAuthorizationStatusNotDetermined: + [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio + completionHandler:^(BOOL granted) { + }]; + break; + default: + break; + } +#endif + + // Build the AudioComponentDescription from compile-time defines + AudioComponentDescription desc; + desc.componentType = fourCCFromString(AU_TYPE_STR); + desc.componentSubType = fourCCFromString(AU_SUBTYPE_STR); + desc.componentManufacturer = fourCCFromString(AU_MANUFACTURER_STR); + desc.componentFlags = 0; + desc.componentFlagsMask = 0; + + std::cout << "[auv3-standalone] Looking for AU: type='" << AU_TYPE_STR + << "' subtype='" << AU_SUBTYPE_STR + << "' manufacturer='" << AU_MANUFACTURER_STR << "'" << std::endl; + + // First try: check if the AU is already registered with the system + AudioComponent comp = AudioComponentFindNext(NULL, &desc); + if (comp) + { + CFStringRef compName = NULL; + AudioComponentCopyName(comp, &compName); + std::cout << "[auv3-standalone] Found registered AudioComponent: " + << (compName ? [(__bridge NSString *)compName UTF8String] : "?") << std::endl; + if (compName) CFRelease(compName); + + // Use normal AVAudioUnit instantiation path + __weak typeof(self) weakSelf = self; + [AVAudioUnit instantiateWithComponentDescription:desc + options:kAudioComponentInstantiation_LoadInProcess + completionHandler:^(AVAudioUnit *_Nullable audioUnit, NSError *_Nullable error) { + dispatch_async(dispatch_get_main_queue(), ^{ + __strong typeof(weakSelf) self = weakSelf; + if (self) [self finishSetupWithAudioUnit:audioUnit error:error]; + }); + }]; + return; + } + + // Second path: load the appex bundle directly and instantiate through the factory + std::cout << "[auv3-standalone] AudioComponent not registered, loading appex directly" << std::endl; + + NSBundle *appexBundle = [self findEmbeddedAppexBundle]; + if (!appexBundle) + { + std::cout << "[auv3-standalone] ERROR: No embedded appex found" << std::endl; + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:@"Failed to load Audio Unit"]; + [alert setInformativeText:@"No embedded .appex found in PlugIns directory"]; + [alert addButtonWithTitle:@"OK"]; + [alert runModal]; + return; + } + + NSError *error = nil; + AUAudioUnit *au = [self instantiateAUDirectlyFromAppex:appexBundle + componentDescription:desc + error:&error]; + if (!au) + { + NSString *msg = error ? error.localizedDescription : @"Unknown error creating AU from appex"; + std::cout << "[auv3-standalone] ERROR: " << [msg UTF8String] << std::endl; + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:@"Failed to load Audio Unit"]; + [alert setInformativeText:msg]; + [alert addButtonWithTitle:@"OK"]; + [alert runModal]; + return; + } + + // Wrap the raw AUAudioUnit in an AVAudioUnit for use with AVAudioEngine + // We need to use AVAudioUnit's instantiation since AVAudioEngine requires AVAudioUnit nodes. + // Since the AU isn't registered, we can't use AVAudioUnit directly. + // Instead, we'll work with the AUAudioUnit directly (without AVAudioEngine). + [self finishSetupWithAUAudioUnit:au]; +} + +// --------------------------------------------------------------------------- +#pragma mark - Finish Setup +// --------------------------------------------------------------------------- + +- (void)finishSetupWithAudioUnit:(AVAudioUnit *)audioUnit error:(NSError *)error +{ + if (error || !audioUnit) + { + NSString *msg = error ? error.localizedDescription : @"Unknown error"; + std::cout << "[auv3-standalone] ERROR: Failed to instantiate AU: " + << [msg UTF8String] << std::endl; + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:@"Failed to load Audio Unit"]; + [alert setInformativeText:msg]; + [alert addButtonWithTitle:@"OK"]; + [alert runModal]; + return; + } + + _avAudioUnit = audioUnit; + std::cout << "[auv3-standalone] AU instantiated via AVAudioUnit: " + << [audioUnit.name UTF8String] << std::endl; + + [self restoreState]; + [self setupEngine]; + [self setupGUI]; + [self setupMIDI]; +} + +- (void)finishSetupWithAUAudioUnit:(AUAudioUnit *)au +{ + // Direct appex loading path: we have an AUAudioUnit but no AVAudioUnit. + // We can still set up audio I/O using the AUAudioUnit's render block directly, + // and request the view controller for GUI. + + std::cout << "[auv3-standalone] Setting up with direct AUAudioUnit (no AVAudioEngine)" << std::endl; + + // Store the raw AU -- we need to keep it alive + _directAU = au; + + // Allocate render resources + NSError *error = nil; + if (![au allocateRenderResourcesAndReturnError:&error]) + { + std::cout << "[auv3-standalone] ERROR: allocateRenderResources failed: " + << [error.localizedDescription UTF8String] << std::endl; + } + else + { + std::cout << "[auv3-standalone] Render resources allocated" << std::endl; + } + + // Set up the GUI from the AUAudioUnit directly + [self setupGUIFromAUAudioUnit:au]; + [self setupMIDIForAUAudioUnit:au]; +} + +// --------------------------------------------------------------------------- +#pragma mark - AVAudioEngine +// --------------------------------------------------------------------------- + +- (void)setupEngine +{ + _engine = [[AVAudioEngine alloc] init]; + [_engine attachNode:_avAudioUnit]; + + AVAudioNode *output = _engine.outputNode; + AVAudioFormat *outputFormat = [output inputFormatForBus:0]; + + uint32_t auType = fourCCFromString(AU_TYPE_STR); + + if (auType == kAudioUnitType_Effect || auType == kAudioUnitType_MIDIProcessor) + { + // Effect: input -> AU -> output + AVAudioNode *input = _engine.inputNode; + AVAudioFormat *inputFormat = [input outputFormatForBus:0]; + + [_engine connect:input to:_avAudioUnit format:inputFormat]; + [_engine connect:_avAudioUnit to:output format:outputFormat]; + } + else + { + // Instrument/Generator: AU -> output (no audio input needed) + [_engine connect:_avAudioUnit to:output format:outputFormat]; + } + + NSError *error = nil; + if (![_engine startAndReturnError:&error]) + { + std::cout << "[auv3-standalone] ERROR: Failed to start engine: " + << [error.localizedDescription UTF8String] << std::endl; + + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:@"Failed to start audio engine"]; + [alert setInformativeText:error.localizedDescription]; + [alert addButtonWithTitle:@"OK"]; + [alert runModal]; + } + else + { + std::cout << "[auv3-standalone] Engine started. Sample rate: " + << outputFormat.sampleRate << " Hz" << std::endl; + } +} + +// --------------------------------------------------------------------------- +#pragma mark - GUI +// --------------------------------------------------------------------------- + +- (void)setupGUI +{ + AUAudioUnit *au = _avAudioUnit.AUAudioUnit; + + [au requestViewControllerWithCompletionHandler:^(AUViewControllerBase *vc) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (!vc) + { + std::cout << "[auv3-standalone] No view controller provided by AU" << std::endl; + // Show the window as-is (empty) -- the plugin has no GUI + [[self window] orderFrontRegardless]; + return; + } + + self->_auViewController = vc; + + NSSize preferredSize = vc.preferredContentSize; + if (preferredSize.width < 1 || preferredSize.height < 1) + { + preferredSize = NSMakeSize(480, 360); + } + + [[self window] setContentSize:preferredSize]; + [[self window] setDelegate:self]; + + NSView *contentView = [[self window] contentView]; + NSView *auView = vc.view; + auView.frame = contentView.bounds; + auView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; + [contentView addSubview:auView]; + + [[self window] orderFrontRegardless]; + + std::cout << "[auv3-standalone] GUI displayed (" + << (int)preferredSize.width << "x" << (int)preferredSize.height << ")" << std::endl; + }); + }]; +} + +- (void)setupGUIFromAUAudioUnit:(AUAudioUnit *)au +{ + [au requestViewControllerWithCompletionHandler:^(AUViewControllerBase *vc) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (!vc) + { + std::cout << "[auv3-standalone] No view controller provided by AU (direct)" << std::endl; + [[self window] orderFrontRegardless]; + return; + } + + self->_auViewController = vc; + + NSSize preferredSize = vc.preferredContentSize; + if (preferredSize.width < 1 || preferredSize.height < 1) + { + preferredSize = NSMakeSize(480, 360); + } + + [[self window] setContentSize:preferredSize]; + [[self window] setDelegate:self]; + + NSView *contentView = [[self window] contentView]; + NSView *auView = vc.view; + auView.frame = contentView.bounds; + auView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; + [contentView addSubview:auView]; + + [[self window] orderFrontRegardless]; + + std::cout << "[auv3-standalone] GUI displayed (direct) (" + << (int)preferredSize.width << "x" << (int)preferredSize.height << ")" << std::endl; + }); + }]; +} + +- (void)setupMIDIForAUAudioUnit:(AUAudioUnit *)au +{ + _scheduleMIDIBlock = au.scheduleMIDIEventBlock; + if (!_scheduleMIDIBlock) + { + std::cout << "[auv3-standalone] AU does not accept MIDI (direct)" << std::endl; + return; + } + + // Reuse the same MIDI setup logic + OSStatus status = MIDIClientCreate(CFSTR("ClapWrapperAUv3Standalone"), NULL, NULL, &sMIDIClient); + if (status != noErr) return; + + status = MIDIInputPortCreate(sMIDIClient, CFSTR("Input"), + midiInputCallback, + (__bridge void *)_scheduleMIDIBlock, + &sMIDIInputPort); + if (status != noErr) return; + + ItemCount numSources = MIDIGetNumberOfSources(); + for (ItemCount i = 0; i < numSources; ++i) + { + MIDIEndpointRef src = MIDIGetSource(i); + MIDIPortConnectSource(sMIDIInputPort, src, NULL); + } + std::cout << "[auv3-standalone] MIDI connected (direct) to " << numSources << " source(s)" << std::endl; +} + +// --------------------------------------------------------------------------- +#pragma mark - MIDI +// --------------------------------------------------------------------------- + +static void midiInputCallback(const MIDIPacketList *pktlist, void *readProcRefCon, + void *srcConnRefCon) +{ + AUScheduleMIDIEventBlock block = (__bridge AUScheduleMIDIEventBlock)readProcRefCon; + if (!block) return; + + const MIDIPacket *packet = &pktlist->packet[0]; + for (UInt32 i = 0; i < pktlist->numPackets; ++i) + { + if (packet->length > 0 && packet->length <= 3) + { + block(AUEventSampleTimeImmediate, 0, packet->length, packet->data); + } + packet = MIDIPacketNext(packet); + } +} + +- (void)setupMIDI +{ + _scheduleMIDIBlock = _avAudioUnit.AUAudioUnit.scheduleMIDIEventBlock; + if (!_scheduleMIDIBlock) + { + std::cout << "[auv3-standalone] AU does not accept MIDI" << std::endl; + return; + } + + OSStatus status = MIDIClientCreate(CFSTR("ClapWrapperAUv3Standalone"), NULL, NULL, &sMIDIClient); + if (status != noErr) + { + std::cout << "[auv3-standalone] Failed to create MIDI client: " << status << std::endl; + return; + } + + status = MIDIInputPortCreate(sMIDIClient, CFSTR("Input"), + midiInputCallback, + (__bridge void *)_scheduleMIDIBlock, + &sMIDIInputPort); + if (status != noErr) + { + std::cout << "[auv3-standalone] Failed to create MIDI input port: " << status << std::endl; + return; + } + + // Connect to all available MIDI sources + ItemCount numSources = MIDIGetNumberOfSources(); + for (ItemCount i = 0; i < numSources; ++i) + { + MIDIEndpointRef src = MIDIGetSource(i); + MIDIPortConnectSource(sMIDIInputPort, src, NULL); + } + + std::cout << "[auv3-standalone] MIDI connected to " << numSources << " source(s)" << std::endl; +} + +- (void)teardownMIDI +{ + if (sMIDIInputPort) + { + MIDIPortDispose(sMIDIInputPort); + sMIDIInputPort = 0; + } + if (sMIDIClient) + { + MIDIClientDispose(sMIDIClient); + sMIDIClient = 0; + } + _scheduleMIDIBlock = nil; +} + +// --------------------------------------------------------------------------- +#pragma mark - State persistence +// --------------------------------------------------------------------------- + +- (NSString *)settingsDirectory +{ + if (!_settingsPath) + { + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); + NSString *appSupport = [paths firstObject]; + _settingsPath = [appSupport stringByAppendingPathComponent:@"clap-wrapper-auv3-standalone"]; + + NSFileManager *fm = [NSFileManager defaultManager]; + if (![fm fileExistsAtPath:_settingsPath]) + { + [fm createDirectoryAtPath:_settingsPath withIntermediateDirectories:YES attributes:nil error:nil]; + } + } + return _settingsPath; +} + +- (NSString *)settingsFilePath +{ + NSString *auName = _avAudioUnit.name ?: @"unknown"; + // Sanitize name for filesystem + NSCharacterSet *illegal = [NSCharacterSet characterSetWithCharactersInString:@"/\\:"]; + auName = [[auName componentsSeparatedByCharactersInSet:illegal] componentsJoinedByString:@"_"]; + return [[self settingsDirectory] stringByAppendingPathComponent: + [NSString stringWithFormat:@"%@.plist", auName]]; +} + +- (void)saveState +{ + if (!_avAudioUnit) return; + + NSDictionary *state = _avAudioUnit.AUAudioUnit.fullState; + if (state) + { + NSString *path = [self settingsFilePath]; + [state writeToFile:path atomically:YES]; + std::cout << "[auv3-standalone] State saved to " << [path UTF8String] << std::endl; + } +} + +- (void)restoreState +{ + if (!_avAudioUnit) return; + + NSString *path = [self settingsFilePath]; + NSDictionary *state = [NSDictionary dictionaryWithContentsOfFile:path]; + if (state) + { + _avAudioUnit.AUAudioUnit.fullState = state; + std::cout << "[auv3-standalone] State restored from " << [path UTF8String] << std::endl; + } +} + +// --------------------------------------------------------------------------- +#pragma mark - NSWindowDelegate +// --------------------------------------------------------------------------- + +- (NSSize)windowWillResize:(NSWindow *)sender toSize:(NSSize)frameSize +{ + // Let the window resize freely; the AU view uses autoresizing + return frameSize; +} + +@end diff --git a/src/detail/standalone/macos/auv3/Info.plist.in b/src/detail/standalone/macos/auv3/Info.plist.in new file mode 100644 index 00000000..b42d3166 --- /dev/null +++ b/src/detail/standalone/macos/auv3/Info.plist.in @@ -0,0 +1,33 @@ + + + + + NSMicrophoneUsageDescription + ${MACOSX_BUNDLE_BUNDLE_NAME} would like microphone access. + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleIconFile + + CFBundleIdentifier + ${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/src/detail/standalone/macos/auv3/MainMenu.xib b/src/detail/standalone/macos/auv3/MainMenu.xib new file mode 100644 index 00000000..8cddc13d --- /dev/null +++ b/src/detail/standalone/macos/auv3/MainMenu.xib @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/wrapasauv3.mm b/src/wrapasauv3.mm new file mode 100644 index 00000000..9d2eafc2 --- /dev/null +++ b/src/wrapasauv3.mm @@ -0,0 +1,19 @@ +/* + wrapasauv3.mm + + Copyright (c) 2024 Timo Kaluza (defiantnerd) + + This file is part of the clap-wrappers project which is released under MIT License. + See file LICENSE or go to https://github.com/free-audio/clap-wrapper for full license details. + + This file includes the generated entry points which create per-plugin factory subclasses. + It serves as the compilation unit that ties together the generated code with the AUv3 wrapper. +*/ + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnullability-completeness" +#pragma clang diagnostic ignored "-Wlanguage-extension-token" + +#include "generated_auv3_entrypoints.hxx" + +#pragma clang diagnostic pop diff --git a/src/wrapasauv3standalone.mm b/src/wrapasauv3standalone.mm new file mode 100644 index 00000000..1874e007 --- /dev/null +++ b/src/wrapasauv3standalone.mm @@ -0,0 +1,17 @@ +/* + wrapasauv3standalone.mm + + Copyright (c) 2024 Timo Kaluza (defiantnerd) + + This file is part of the clap-wrappers project which is released under MIT License. + See file LICENSE or go to https://github.com/free-audio/clap-wrapper for full license details. + + Entry point for the AUv3 standalone host application. +*/ + +#import + +int main(int argc, const char *argv[]) +{ + return NSApplicationMain(argc, argv); +} diff --git a/tests/clap-first-example/CMakeLists.txt b/tests/clap-first-example/CMakeLists.txt index 461ac6bb..4ab5859f 100644 --- a/tests/clap-first-example/CMakeLists.txt +++ b/tests/clap-first-example/CMakeLists.txt @@ -36,7 +36,7 @@ make_clapfirst_plugins( # vst3 validator on windows needs this to be true to pass WINDOWS_FOLDER_VST3 ${EXAMPLE_USE_WINDOWS_FOLDER} - PLUGIN_FORMATS CLAP VST3 AUV2 WCLAP + PLUGIN_FORMATS CLAP VST3 AUV2 AUV3 WCLAP ASSET_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${PROJECT_NAME}_assets From f61cef85003211699898648b50cd15ec853b52e3 Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Fri, 10 Apr 2026 23:59:11 +0200 Subject: [PATCH 02/23] WIP: intermediate state --- cmake/embed_clap.cmake | 29 ++ cmake/make_clapfirst.cmake | 2 +- cmake/top_level_default.cmake | 11 +- cmake/wrap_auv3.cmake | 43 ++- cmake/wrap_auv3_standalone.cmake | 2 +- src/detail/auv3/auv3_audiounit.h | 2 +- src/detail/auv3/auv3_audiounit.mm | 350 +++++++++++++----- .../macos/auv3/AUv3HostAppDelegate.mm | 97 +++-- src/wrapasauv3.mm | 13 + 9 files changed, 403 insertions(+), 146 deletions(-) create mode 100644 cmake/embed_clap.cmake diff --git a/cmake/embed_clap.cmake b/cmake/embed_clap.cmake new file mode 100644 index 00000000..8a17522e --- /dev/null +++ b/cmake/embed_clap.cmake @@ -0,0 +1,29 @@ +# embed_clap.cmake — find an installed .clap bundle and copy it into the target +# Called as a post-build script with -DCLAP_NAME=.clap -DDST= + +if(NOT DEFINED CLAP_NAME OR NOT DEFINED DST) + message(FATAL_ERROR "embed_clap.cmake requires -DCLAP_NAME and -DDST") +endif() + +# Search standard CLAP install locations +set(_search_paths + "$ENV{HOME}/Library/Audio/Plug-Ins/CLAP" + "/Library/Audio/Plug-Ins/CLAP" +) + +set(_found "") +foreach(_dir IN LISTS _search_paths) + set(_candidate "${_dir}/${CLAP_NAME}") + if(IS_DIRECTORY "${_candidate}") + set(_found "${_candidate}") + break() + endif() +endforeach() + +if(_found STREQUAL "") + message(WARNING "embed_clap: '${CLAP_NAME}' not found in standard CLAP paths, appex will search at runtime") + return() +endif() + +message(STATUS "embed_clap: copying ${_found} -> ${DST}") +file(COPY "${_found}" DESTINATION "${DST}/..") diff --git a/cmake/make_clapfirst.cmake b/cmake/make_clapfirst.cmake index c03e06db..44bd2029 100644 --- a/cmake/make_clapfirst.cmake +++ b/cmake/make_clapfirst.cmake @@ -220,7 +220,7 @@ function(make_clapfirst_plugins) if (APPLE AND ${BUILD_AUV3} GREATER -1) message(STATUS "clap-wrapper: ClapFirst is making an AUv3") set(AUV3_TARGET ${C1ST_TARGET_NAME}_auv3) - add_library(${AUV3_TARGET} MODULE) + add_executable(${AUV3_TARGET}) target_sources(${AUV3_TARGET} PRIVATE ${C1ST_ENTRY_SOURCE}) target_link_libraries(${AUV3_TARGET} PRIVATE ${C1ST_IMPL_TARGET}) if (DEFINED C1ST_AUV2_MANUFACTURER_CODE) diff --git a/cmake/top_level_default.cmake b/cmake/top_level_default.cmake index eed9226e..bbef82a7 100644 --- a/cmake/top_level_default.cmake +++ b/cmake/top_level_default.cmake @@ -51,7 +51,7 @@ if (PROJECT_IS_TOP_LEVEL) endif() if (${CLAP_WRAPPER_BUILD_AUV3}) - add_library(${pluginname}_as_auv3 MODULE) + add_executable(${pluginname}_as_auv3) target_add_auv3_wrapper( TARGET ${pluginname}_as_auv3 OUTPUT_NAME "${CLAP_WRAPPER_OUTPUT_NAME}" @@ -64,6 +64,15 @@ if (PROJECT_IS_TOP_LEVEL) SUBTYPE_CODE "gWwp" ) + # Embed the installed .clap into the appex so it can find the plugin at runtime + set(_clap_bundle_name "${CLAP_WRAPPER_OUTPUT_NAME}.clap") + set(_clap_embed_dst "$/Contents/PlugIns/${_clap_bundle_name}") + add_custom_command(TARGET ${pluginname}_as_auv3 POST_BUILD + COMMAND ${CMAKE_COMMAND} "-DCLAP_NAME=${_clap_bundle_name}" "-DDST=${_clap_embed_dst}" + -P "${CLAP_WRAPPER_CMAKE_CURRENT_SOURCE_DIR}/cmake/embed_clap.cmake" + COMMENT "Embedding ${_clap_bundle_name} in AUv3 appex" + ) + add_executable(${pluginname}_as_auv3_standalone) target_add_auv3_standalone_wrapper( TARGET ${pluginname}_as_auv3_standalone diff --git a/cmake/wrap_auv3.cmake b/cmake/wrap_auv3.cmake index 3383a8fc..b83cdf6e 100644 --- a/cmake/wrap_auv3.cmake +++ b/cmake/wrap_auv3.cmake @@ -64,9 +64,25 @@ function(target_add_auv3_wrapper) ) set(bhtgoutdir "${CMAKE_CURRENT_BINARY_DIR}/${AUV3_TARGET}-auv3-build-helper-output") + # Create output dir and a placeholder Info.plist at configure time. + # CMake's generate step needs the plist to exist (MACOSX_BUNDLE_INFO_PLIST), + # but the real one is produced by the build-helper at build time. + file(MAKE_DIRECTORY "${bhtgoutdir}") + if (NOT EXISTS "${bhtgoutdir}/auv3_Info.plist") + file(WRITE "${bhtgoutdir}/auv3_Info.plist" +" + + + + CFBundlePackageType + XPC! + + +") + endif() + add_custom_command(TARGET ${bhtg} POST_BUILD COMMAND ${CMAKE_COMMAND} -E echo "clap-wrapper: auv3 configuration output dir is ${bhtgoutdir}" - COMMAND ${CMAKE_COMMAND} -E make_directory "${bhtgoutdir}" ) add_dependencies(${AUV3_TARGET} ${bhtg}) @@ -203,7 +219,9 @@ function(target_add_auv3_wrapper) target_link_libraries(${AUV3_TARGET}-clap-wrapper-auv3-lib INTERFACE clap-wrapper-extensions clap-wrapper-shared-detail clap-wrapper-compile-options) endif () - set_target_properties(${AUV3_TARGET} PROPERTIES LIBRARY_OUTPUT_NAME "${AUV3_OUTPUT_NAME}") + set_target_properties(${AUV3_TARGET} PROPERTIES + OUTPUT_NAME "${AUV3_OUTPUT_NAME}" + LIBRARY_OUTPUT_NAME "${AUV3_OUTPUT_NAME}") # also set for target_copy_after_build compatibility target_link_libraries(${AUV3_TARGET} PUBLIC ${AUV3_TARGET}-clap-wrapper-auv3-lib) if ("${CLAP_WRAPPER_BUNDLE_VERSION}" STREQUAL "") @@ -221,28 +239,27 @@ function(target_add_auv3_wrapper) "-framework CoreMIDI") set_target_properties(${AUV3_TARGET} PROPERTIES - BUNDLE True + MACOSX_BUNDLE True BUNDLE_EXTENSION appex - LIBRARY_OUTPUT_NAME ${AUV3_OUTPUT_NAME} + OUTPUT_NAME ${AUV3_OUTPUT_NAME} MACOSX_BUNDLE_GUI_IDENTIFIER "${AUV3_BUNDLE_IDENTIFIER}" MACOSX_BUNDLE_BUNDLE_NAME ${AUV3_OUTPUT_NAME} MACOSX_BUNDLE_BUNDLE_VERSION ${AUV3_BUNDLE_VERSION} MACOSX_BUNDLE_SHORT_VERSION_STRING ${AUV3_BUNDLE_VERSION} ) - # For Xcode: tell it to use the build-helper's plist as INFOPLIST_FILE so - # Xcode's own "Process Info.plist" phase preserves our NSExtension block. - # For non-Xcode generators: POST_BUILD copy works because there's no - # implicit plist processing after POST_BUILD. + # The build-helper generates auv3_Info.plist at build time (POST_BUILD). + # We replace Xcode's/CMake's auto-generated plist with it after the build. + # Using MACOSX_BUNDLE_INFO_PLIST doesn't work reliably because Xcode may + # process the template before the build-helper has run. if (CMAKE_GENERATOR STREQUAL "Xcode") set_target_properties(${AUV3_TARGET} PROPERTIES - MACOSX_BUNDLE_INFO_PLIST "${bhtgoutdir}/auv3_Info.plist" + XCODE_PRODUCT_TYPE com.apple.product-type.app-extension ) - else() - add_custom_command(TARGET ${AUV3_TARGET} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy ${bhtgoutdir}/auv3_Info.plist $/../Info.plist - COMMENT "Replacing Info.plist with build-helper generated version (contains NSExtension)") endif() + add_custom_command(TARGET ${AUV3_TARGET} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy "${bhtgoutdir}/auv3_Info.plist" "$/Info.plist" + COMMENT "Replacing Info.plist with build-helper generated version (contains NSExtension)") set_target_properties(${AUV3_TARGET} PROPERTIES XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${AUV3_BUNDLE_IDENTIFIER}") diff --git a/cmake/wrap_auv3_standalone.cmake b/cmake/wrap_auv3_standalone.cmake index 56eb5bcf..195186ca 100644 --- a/cmake/wrap_auv3_standalone.cmake +++ b/cmake/wrap_auv3_standalone.cmake @@ -147,7 +147,7 @@ function(target_add_auv3_standalone_wrapper) # which is the same as the .appex top-level directory. # We use TARGET_BUNDLE_DIR for BUNDLE targets to get the correct path. set(_auv3_appex_src "$") - set(_auv3_appex_dst "$/Contents/PlugIns/$.appex") + set(_auv3_appex_dst "$/Contents/PlugIns/$.appex") add_custom_command(TARGET ${AUSA_TARGET} POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory "$/Contents/PlugIns" diff --git a/src/detail/auv3/auv3_audiounit.h b/src/detail/auv3/auv3_audiounit.h index 724df15b..efdcbade 100644 --- a/src/detail/auv3/auv3_audiounit.h +++ b/src/detail/auv3/auv3_audiounit.h @@ -27,7 +27,7 @@ // Apple's AUv3 model requires the NSExtensionPrincipalClass to be the // AUViewController subclass which also conforms to AUAudioUnitFactory. @interface ClapAUv3ViewController : AUViewController -@property (nonatomic, weak) ClapAUv3AudioUnit *audioUnit; +@property (nonatomic, strong) ClapAUv3AudioUnit *audioUnit; // Subclasses (generated by build-helper) override this to provide plugin-specific info - (AUAudioUnit *)createAudioUnitWithComponentDescription:(AudioComponentDescription)desc error:(NSError **)error; diff --git a/src/detail/auv3/auv3_audiounit.mm b/src/detail/auv3/auv3_audiounit.mm index e51ea459..a4c12f68 100644 --- a/src/detail/auv3/auv3_audiounit.mm +++ b/src/detail/auv3/auv3_audiounit.mm @@ -12,6 +12,7 @@ #include "detail/shared/fixedqueue.h" #include "detail/clap/automation.h" +#include #include #include #include @@ -19,6 +20,13 @@ #include #include +static os_log_t _auv3Log() { + static os_log_t log = os_log_create("org.clap-wrapper.auv3", "wrapper"); + return log; +} +#define AUV3LOG(...) os_log(_auv3Log(), __VA_ARGS__) +#define AUV3ERR(...) os_log_error(_auv3Log(), __VA_ARGS__) + // ----------------------------------------------------------------------- // C++ implementation detail bridging IHost, IAutomation, and IPlugObject // ----------------------------------------------------------------------- @@ -54,11 +62,16 @@ ~AUv3ImplDetail() override { + AUV3LOG("~AUv3ImplDetail: destructor entered (plugin=%{public}s)", _plugin ? "valid" : "null"); if (_plugin) { + AUV3LOG("~AUv3ImplDetail: calling _os_attached.off()"); _os_attached.off(); + AUV3LOG("~AUv3ImplDetail: calling _plugin->terminate()"); _plugin->terminate(); + AUV3LOG("~AUv3ImplDetail: calling _plugin.reset()"); _plugin.reset(); + AUV3LOG("~AUv3ImplDetail: plugin teardown complete"); } } @@ -357,108 +370,179 @@ - (instancetype)initWithComponentDescription:(AudioComponentDescription)componen clapId:(NSString *)clapId clapIndex:(int)clapIndex { - self = [super initWithComponentDescription:componentDescription options:options error:outError]; - if (!self) return nil; + AUV3LOG("initWithComponentDescription: entered (name=%{public}s id=%{public}s idx=%d)", + [clapName UTF8String], clapId ? [clapId UTF8String] : "(nil)", clapIndex); + AUV3LOG("initWithComponentDescription: thread=%{public}s", [NSThread.currentThread.name UTF8String] ?: "unnamed"); - _impl = std::make_unique(); - _impl->_audioUnit = self; - _impl->_clapname = [clapName UTF8String]; - _impl->_clapid = clapId ? [clapId UTF8String] : ""; - _impl->_idx = clapIndex; + self = [super initWithComponentDescription:componentDescription options:options error:outError]; + if (!self) + { + AUV3ERR("initWithComponentDescription: [super init] returned nil"); + return nil; + } + AUV3LOG("initWithComponentDescription: super init succeeded, self=%p", self); - // Load CLAP library - if (!_library.hasEntryPoint()) + try { - if (_impl->_clapname.empty()) - { - std::cout << "[ERROR] auv3: _clapname empty and no internal entry point" << std::endl; - if (outError) - *outError = [NSError errorWithDomain:@"ClapAUv3" code:-1 - userInfo:@{NSLocalizedDescriptionKey : @"CLAP name is empty"}]; - return nil; - } + _impl = std::make_unique(); + _impl->_audioUnit = self; + _impl->_clapname = [clapName UTF8String]; + _impl->_clapid = clapId ? [clapId UTF8String] : ""; + _impl->_idx = clapIndex; - auto csp = Clap::getValidCLAPSearchPaths(); - auto it = std::find_if(csp.begin(), csp.end(), - [&](const auto &cs) - { - auto fp = cs / (_impl->_clapname + ".clap"); - return fs::is_directory(fp) && _library.load(fp); - }); + AUV3LOG("init: name='%{public}s' id='%{public}s' idx=%d", + _impl->_clapname.c_str(), _impl->_clapid.c_str(), _impl->_idx); - if (it != csp.end()) + // Load CLAP library + if (!_library.hasEntryPoint()) { - std::cout << "[clap-wrapper] auv3 loaded clap from " << it->u8string() << std::endl; + AUV3LOG("init: library has no entry point, searching for CLAP"); + if (_impl->_clapname.empty()) + { + AUV3ERR("init: _clapname empty and no internal entry point"); + if (outError) + *outError = [NSError errorWithDomain:@"ClapAUv3" code:-1 + userInfo:@{NSLocalizedDescriptionKey : @"CLAP name is empty"}]; + return nil; + } + + auto csp = Clap::getValidCLAPSearchPaths(); + for (const auto &p : csp) + { + AUV3LOG("init: search path: %{public}s", p.u8string().c_str()); + } + + auto it = std::find_if(csp.begin(), csp.end(), + [&](const auto &cs) + { + auto fp = cs / (_impl->_clapname + ".clap"); + AUV3LOG("init: trying %{public}s", fp.u8string().c_str()); + return fs::is_directory(fp) && _library.load(fp); + }); + + if (it != csp.end()) + { + AUV3LOG("init: loaded CLAP from %{public}s", it->u8string().c_str()); + } + else + { + AUV3ERR("init: cannot load CLAP '%{public}s'", _impl->_clapname.c_str()); + if (outError) + *outError = [NSError errorWithDomain:@"ClapAUv3" code:-2 + userInfo:@{NSLocalizedDescriptionKey : @"Cannot load CLAP plugin"}]; + return nil; + } } else { - std::cout << "[ERROR] auv3: cannot load clap" << std::endl; - if (outError) - *outError = [NSError errorWithDomain:@"ClapAUv3" code:-2 - userInfo:@{NSLocalizedDescriptionKey : @"Cannot load CLAP plugin"}]; - return nil; + AUV3LOG("init: library already has entry point, skipping search"); } - } - // Find the plugin descriptor - if (!_impl->_clapid.empty()) - { - for (auto *d : _library.plugins) + // Find the plugin descriptor + AUV3LOG("init: finding plugin descriptor (clapid='%{public}s' idx=%d, library has %zu plugins)", + _impl->_clapid.c_str(), _impl->_idx, _library.plugins.size()); + if (!_impl->_clapid.empty()) { - if (strcmp(d->id, _impl->_clapid.c_str()) == 0) + for (auto *d : _library.plugins) { - _impl->_desc = d; + if (strcmp(d->id, _impl->_clapid.c_str()) == 0) + { + _impl->_desc = d; + } } } + else if (_impl->_idx >= 0 && _impl->_idx < (int)_library.plugins.size()) + { + _impl->_desc = _library.plugins[_impl->_idx]; + } + + if (!_impl->_desc) + { + AUV3ERR("init: cannot determine plugin description"); + if (outError) + *outError = [NSError errorWithDomain:@"ClapAUv3" code:-3 + userInfo:@{NSLocalizedDescriptionKey : @"Cannot find CLAP plugin descriptor"}]; + return nil; + } + + AUV3LOG("init: found descriptor id='%{public}s' name='%{public}s' version='%{public}s'", + _impl->_desc->id, _impl->_desc->name, _impl->_desc->version); + + // Create the plugin instance + AUV3LOG("init: creating plugin instance via factory"); + _impl->_plugin = Clap::Plugin::createInstance(_library._pluginFactory, _impl->_desc->id, _impl.get()); + if (!_impl->_plugin) + { + AUV3ERR("init: factory returned null plugin instance"); + if (outError) + *outError = [NSError errorWithDomain:@"ClapAUv3" code:-4 + userInfo:@{NSLocalizedDescriptionKey : @"CLAP plugin instance creation failed"}]; + return nil; + } + AUV3LOG("init: plugin instance created successfully"); + + AUV3LOG("init: calling plugin->initialize()"); + _impl->_plugin->initialize(); + AUV3LOG("init: calling _os_attached.on()"); + _impl->_os_attached.on(); + + // Build audio bus arrays from the CLAP audio port info + AUV3LOG("init: building bus arrays (inputs=%zu outputs=%zu)", + _impl->_inputBusInfos.size(), _impl->_outputBusInfos.size()); + [self _buildBusArrays]; + + _renderResourcesAllocated = NO; + AUV3LOG("init: completed successfully"); } - else if (_impl->_idx >= 0 && _impl->_idx < (int)_library.plugins.size()) + catch (int e) { - _impl->_desc = _library.plugins[_impl->_idx]; + AUV3ERR("init: caught exception of type int: %d", e); + if (outError) + *outError = [NSError errorWithDomain:@"ClapAUv3" code:e + userInfo:@{NSLocalizedDescriptionKey : @"C++ int exception during init"}]; + return nil; } - - if (!_impl->_desc) + catch (const std::exception &e) { - std::cout << "[ERROR] auv3: cannot determine plugin description" << std::endl; + AUV3ERR("init: caught std::exception: %{public}s", e.what()); if (outError) - *outError = [NSError errorWithDomain:@"ClapAUv3" code:-3 - userInfo:@{NSLocalizedDescriptionKey : @"Cannot find CLAP plugin descriptor"}]; + *outError = [NSError errorWithDomain:@"ClapAUv3" code:-99 + userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithUTF8String:e.what()]}]; return nil; } - - std::cout << "[clap-wrapper] auv3: Initialized '" << _impl->_desc->id << "' / '" - << _impl->_desc->name << "' / '" << _impl->_desc->version << "'" << std::endl; - - // Create the plugin instance - _impl->_plugin = Clap::Plugin::createInstance(_library._pluginFactory, _impl->_desc->id, _impl.get()); - if (!_impl->_plugin) + catch (...) { - std::cout << "[ERROR] auv3: the clap did not create an instance" << std::endl; + AUV3ERR("init: caught unknown C++ exception"); if (outError) - *outError = [NSError errorWithDomain:@"ClapAUv3" code:-4 - userInfo:@{NSLocalizedDescriptionKey : @"CLAP plugin instance creation failed"}]; + *outError = [NSError errorWithDomain:@"ClapAUv3" code:-98 + userInfo:@{NSLocalizedDescriptionKey : @"Unknown C++ exception during init"}]; return nil; } - _impl->_plugin->initialize(); - _impl->_os_attached.on(); - - // Build audio bus arrays from the CLAP audio port info - [self _buildBusArrays]; - - _renderResourcesAllocated = NO; - return self; } - (void)dealloc { + AUV3LOG("dealloc: entered (self=%p, thread=%{public}s)", self, + [NSThread.currentThread.name UTF8String] ?: "unnamed"); + AUV3LOG("dealloc: _impl=%{public}s, _plugin=%{public}s", + _impl ? "valid" : "null", + (_impl && _impl->_plugin) ? "valid" : "null"); + if (_impl && _impl->_plugin) { + AUV3LOG("dealloc: calling _os_attached.off()"); _impl->_os_attached.off(); + AUV3LOG("dealloc: calling _plugin->terminate()"); _impl->_plugin->terminate(); + AUV3LOG("dealloc: calling _plugin.reset()"); _impl->_plugin.reset(); + AUV3LOG("dealloc: plugin teardown complete"); } + AUV3LOG("dealloc: calling _impl.reset()"); _impl.reset(); + AUV3LOG("dealloc: finished"); } - (void)_buildBusArrays @@ -568,6 +652,7 @@ - (BOOL)shouldChangeToFormat:(AVAudioFormat *)format forBus:(AUAudioUnitBus *)bu - (NSDictionary *)fullState { + AUV3LOG("fullState (save): entered"); NSMutableDictionary *state = [[super fullState] mutableCopy]; if (!state) state = [NSMutableDictionary new]; @@ -578,6 +663,11 @@ - (BOOL)shouldChangeToFormat:(AVAudioFormat *)format forBus:(AUAudioUnitBus *)bu { NSData *clapState = [NSData dataWithBytes:chunk.data() length:chunk.size()]; state[@"clapState"] = clapState; + AUV3LOG("fullState (save): saved %zu bytes of CLAP state", (size_t)[clapState length]); + } + else + { + AUV3LOG("fullState (save): CLAP state save returned false"); } } @@ -586,6 +676,7 @@ - (BOOL)shouldChangeToFormat:(AVAudioFormat *)format forBus:(AUAudioUnitBus *)bu - (void)setFullState:(NSDictionary *)fullState { + AUV3LOG("setFullState (restore): entered"); [super setFullState:fullState]; if (_impl && _impl->_plugin && _impl->_plugin->_ext._state) @@ -593,9 +684,15 @@ - (void)setFullState:(NSDictionary *)fullState NSData *clapState = fullState[@"clapState"]; if (clapState) { + AUV3LOG("setFullState (restore): loading %zu bytes of CLAP state", (size_t)[clapState length]); Clap::StateMemento chunk; chunk.setData((const uint8_t *)[clapState bytes], [clapState length]); _impl->_plugin->_ext._state->load(_impl->_plugin->_plugin, chunk); + AUV3LOG("setFullState (restore): completed"); + } + else + { + AUV3LOG("setFullState (restore): no clapState key in dictionary"); } } } @@ -604,13 +701,19 @@ - (void)setFullState:(NSDictionary *)fullState - (BOOL)allocateRenderResourcesAndReturnError:(NSError **)outError { + AUV3LOG("allocateRenderResources: entered (thread=%{public}s)", + [NSThread.currentThread.name UTF8String] ?: "unnamed"); + if (![super allocateRenderResourcesAndReturnError:outError]) { + AUV3ERR("allocateRenderResources: [super] failed"); return NO; } if (!_impl || !_impl->_plugin) { + AUV3ERR("allocateRenderResources: plugin not initialized (_impl=%{public}s)", + _impl ? "valid" : "null"); if (outError) *outError = [NSError errorWithDomain:@"ClapAUv3" code:-10 userInfo:@{NSLocalizedDescriptionKey : @"Plugin not initialized"}]; @@ -627,9 +730,12 @@ - (BOOL)allocateRenderResourcesAndReturnError:(NSError **)outError { sampleRate = self.inputBusses[0].format.sampleRate; } + AUV3LOG("allocateRenderResources: sampleRate=%.0f maxFrames=%u", + sampleRate, (unsigned)self.maximumFramesToRender); auto guarantee_mainthread = _impl->_plugin->AlwaysMainThread(); + AUV3LOG("allocateRenderResources: setting sample rate and block sizes"); _impl->_plugin->setSampleRate(sampleRate); _impl->_plugin->setBlockSizes(1, self.maximumFramesToRender); @@ -643,8 +749,11 @@ - (BOOL)allocateRenderResourcesAndReturnError:(NSError **)outError { outputChs.push_back((uint32_t)self.outputBusses[i].format.channelCount); } + AUV3LOG("allocateRenderResources: input busses=%zu output busses=%zu", + inputChs.size(), outputChs.size()); // Create and set up the process adapter + AUV3LOG("allocateRenderResources: creating process adapter"); _impl->_processAdapter = std::make_unique(); _impl->_processAdapter->setupProcessing( (uint32_t)inputChs.size(), inputChs.empty() ? nullptr : inputChs.data(), @@ -659,13 +768,16 @@ - (BOOL)allocateRenderResourcesAndReturnError:(NSError **)outError _impl->_processAdapter->midiOutputEventBlock = self.MIDIOutputEventBlock; // Activate the CLAP plugin + AUV3LOG("allocateRenderResources: calling activate()"); _impl->_plugin->activate(); + AUV3LOG("allocateRenderResources: calling start_processing()"); _impl->_plugin->start_processing(); _impl->_initialized = true; // Wire up the parameter value observer if (_impl->_parameterTree) { + AUV3LOG("allocateRenderResources: wiring parameter observer"); __weak typeof(self) weakSelf = self; _impl->_parameterTree.implementorValueObserver = ^(AUParameter *param, AUValue value) { __strong typeof(weakSelf) strongSelf = weakSelf; @@ -677,23 +789,32 @@ - (BOOL)allocateRenderResourcesAndReturnError:(NSError **)outError } _renderResourcesAllocated = YES; + AUV3LOG("allocateRenderResources: completed successfully"); return YES; } - (void)deallocateRenderResources { + AUV3LOG("deallocateRenderResources: entered (thread=%{public}s)", + [NSThread.currentThread.name UTF8String] ?: "unnamed"); + if (_impl && _impl->_plugin && _impl->_initialized) { auto guarantee_mainthread = _impl->_plugin->AlwaysMainThread(); + AUV3LOG("deallocateRenderResources: calling stop_processing()"); _impl->_plugin->stop_processing(); + AUV3LOG("deallocateRenderResources: calling deactivate()"); _impl->_plugin->deactivate(); _impl->_initialized = false; } + AUV3LOG("deallocateRenderResources: resetting process adapter"); _impl->_processAdapter.reset(); _renderResourcesAllocated = NO; + AUV3LOG("deallocateRenderResources: calling [super deallocateRenderResources]"); [super deallocateRenderResources]; + AUV3LOG("deallocateRenderResources: completed"); } // --- Render block --- @@ -723,7 +844,12 @@ - (AUInternalRenderBlock)internalRenderBlock - (BOOL)createGUIInView:(NSView *)parentView width:(uint32_t *)outWidth height:(uint32_t *)outHeight { - if (!_impl || !_impl->_plugin || !_impl->_plugin->_ext._gui) return NO; + AUV3LOG("createGUIInView: entered (parentView=%p)", parentView); + if (!_impl || !_impl->_plugin || !_impl->_plugin->_ext._gui) + { + AUV3LOG("createGUIInView: no GUI extension available"); + return NO; + } auto *gui = _impl->_plugin->_ext._gui; auto *plugin = _impl->_plugin->_plugin; @@ -759,11 +885,18 @@ - (BOOL)createGUIInView:(NSView *)parentView width:(uint32_t *)outWidth height:( - (void)destroyGUI { - if (!_impl || !_impl->_plugin || !_impl->_plugin->_ext._gui) return; + AUV3LOG("destroyGUI: entered"); + if (!_impl || !_impl->_plugin || !_impl->_plugin->_ext._gui) + { + AUV3LOG("destroyGUI: no GUI extension, nothing to destroy"); + return; + } + AUV3LOG("destroyGUI: hiding and destroying GUI"); _impl->_plugin->_ext._gui->hide(_impl->_plugin->_plugin); _impl->_plugin->_ext._gui->destroy(_impl->_plugin->_plugin); _impl->_guiParentView = nil; + AUV3LOG("destroyGUI: completed"); } - (BOOL)canResizeGUI @@ -779,29 +912,10 @@ - (BOOL)setGUISize:(uint32_t)width height:(uint32_t)height } // --- View controller --- - -- (void)requestViewControllerWithCompletionHandler:(void (^)(AUViewControllerBase *_Nullable))completionHandler -{ - if (!_impl || !_impl->_plugin || !_impl->_plugin->_ext._gui) - { - completionHandler(nil); - return; - } - - // Check if the CLAP plugin supports Cocoa GUI - if (!_impl->_plugin->_ext._gui->is_api_supported(_impl->_plugin->_plugin, CLAP_WINDOW_API_COCOA, false)) - { - completionHandler(nil); - return; - } - - // Create the view controller on the main thread - dispatch_async(dispatch_get_main_queue(), ^{ - ClapAUv3ViewController *vc = [[ClapAUv3ViewController alloc] init]; - vc.audioUnit = self; - completionHandler(vc); - }); -} +// requestViewControllerWithCompletionHandler: is NOT overridden. +// The default AUAudioUnit implementation returns the NSExtensionPrincipalClass +// view controller (the factory VC that created this AU). This is the same +// pattern used by the VST3 SDK's AUv3 wrapper. @end @@ -813,31 +927,78 @@ @implementation ClapAUv3ViewController - (void)loadView { - // Create a plain NSView as the container - self.view = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 480, 360)]; + AUV3LOG("loadView: entered (thread=%{public}s)", + [NSThread.currentThread.name UTF8String] ?: "unnamed"); + NSView *view = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 0, 0)]; + view.autoresizingMask = NSViewNotSizable; + view.translatesAutoresizingMaskIntoConstraints = YES; + [self setView:view]; + AUV3LOG("loadView: completed"); } -- (void)viewDidLoad +// Custom setter: trigger GUI creation when audioUnit is set and view is already loaded. +// This matches the VST3 SDK's setAudioUnit: → makePlugView pattern. +- (void)setAudioUnit:(ClapAUv3AudioUnit *)audioUnit { - [super viewDidLoad]; + AUV3LOG("setAudioUnit: entered (audioUnit=%p, viewLoaded=%d, thread=%{public}s)", + audioUnit, [self isViewLoaded], + [NSThread.currentThread.name UTF8String] ?: "unnamed"); + _audioUnit = audioUnit; + // Do NOT create the GUI here. The GUI is created lazily when the host + // explicitly shows the view (viewDidAppear / viewDidLayout). Creating it + // eagerly blocks the main thread (JUCE MessageManager init), which prevents + // the appex from processing subsequent XPC messages — causing auval WARM + // timeout (-10863) and similar hangs in headless hosts. +} - if (!self.audioUnit) return; +- (void)_createPluginGUI +{ + AUV3LOG("_createPluginGUI: entered (audioUnit=%p)", self.audioUnit); + if (!self.audioUnit) + { + AUV3LOG("_createPluginGUI: no audioUnit set, skipping"); + return; + } uint32_t w = 0, h = 0; if ([self.audioUnit createGUIInView:self.view width:&w height:&h]) { + AUV3LOG("_createPluginGUI: GUI created, size=%ux%u", w, h); if (w > 0 && h > 0) { self.preferredContentSize = NSMakeSize(w, h); self.view.frame = NSMakeRect(0, 0, w, h); } } + else + { + AUV3LOG("_createPluginGUI: createGUIInView returned NO"); + } +} + +- (void)viewDidLoad +{ + AUV3LOG("viewDidLoad: entered"); + [super viewDidLoad]; + // Do NOT create the GUI here — defer to viewDidAppear so the CLAP GUI + // is only created when the host actually displays the view. + AUV3LOG("viewDidLoad: completed"); +} + +- (void)viewDidAppear +{ + AUV3LOG("viewDidAppear: entered (audioUnit=%p)", self.audioUnit); + [super viewDidAppear]; + [self _createPluginGUI]; + AUV3LOG("viewDidAppear: completed"); } - (void)viewDidDisappear { + AUV3LOG("viewDidDisappear: entered"); [self.audioUnit destroyGUI]; [super viewDidDisappear]; + AUV3LOG("viewDidDisappear: completed"); } // --- AUAudioUnitFactory --- @@ -846,6 +1007,7 @@ - (void)viewDidDisappear - (AUAudioUnit *)createAudioUnitWithComponentDescription:(AudioComponentDescription)desc error:(NSError **)error { + AUV3ERR("createAudioUnitWithComponentDescription: BASE class called — subclass should override"); if (error) *error = [NSError errorWithDomain:@"ClapAUv3" code:-100 userInfo:@{NSLocalizedDescriptionKey : @"Base factory should not be called directly"}]; @@ -854,7 +1016,7 @@ - (AUAudioUnit *)createAudioUnitWithComponentDescription:(AudioComponentDescript - (void)beginRequestWithExtensionContext:(NSExtensionContext *)context { - // Required by NSExtensionRequestHandling protocol. + AUV3LOG("beginRequestWithExtensionContext: entered (context=%p)", context); } @end diff --git a/src/detail/standalone/macos/auv3/AUv3HostAppDelegate.mm b/src/detail/standalone/macos/auv3/AUv3HostAppDelegate.mm index 02085004..5ea14a1d 100644 --- a/src/detail/standalone/macos/auv3/AUv3HostAppDelegate.mm +++ b/src/detail/standalone/macos/auv3/AUv3HostAppDelegate.mm @@ -6,6 +6,7 @@ #import #include +#include // --- FourCC helper: convert a 4-char string like "aufx" to a uint32_t --- static uint32_t fourCCFromString(const char *s) @@ -24,6 +25,7 @@ @implementation AUv3HostAppDelegate AVAudioUnit *_avAudioUnit; AUAudioUnit *_directAU; // used when loading appex directly (no system registration) NSViewController *_auViewController; + NSViewController *_factoryViewController; // the factory/principal class VC from direct appex loading NSString *_settingsPath; // MIDI @@ -107,18 +109,40 @@ - (AUAudioUnit *)instantiateAUDirectlyFromAppex:(NSBundle *)appexBundle componentDescription:(AudioComponentDescription)desc error:(NSError **)outError { - // Load the appex bundle to get its Objective-C classes + // Load the appex bundle to get its Objective-C classes. + // Note: MH_EXECUTE binaries can't be loaded via NSBundle's load method, + // so we use dlopen on the executable directly to register the ObjC classes. if (![appexBundle isLoaded]) { - NSError *loadError = nil; - if (![appexBundle loadAndReturnError:&loadError]) + NSString *execPath = [appexBundle executablePath]; + if (execPath) { - std::cout << "[auv3-standalone] ERROR: Failed to load appex bundle: " - << [loadError.localizedDescription UTF8String] << std::endl; - if (outError) *outError = loadError; - return nil; + void *handle = dlopen([execPath fileSystemRepresentation], RTLD_NOW | RTLD_LOCAL); + if (!handle) + { + std::cout << "[auv3-standalone] dlopen fallback failed: " << dlerror() << std::endl; + } + else + { + std::cout << "[auv3-standalone] Appex loaded via dlopen" << std::endl; + } + } + + // Also try NSBundle load (works for MH_BUNDLE/MH_DYLIB) + if (![appexBundle isLoaded]) + { + NSError *loadError = nil; + if (![appexBundle loadAndReturnError:&loadError]) + { + std::cout << "[auv3-standalone] WARNING: NSBundle load failed: " + << [loadError.localizedDescription UTF8String] + << " (may be expected for executable appex)" << std::endl; + } + else + { + std::cout << "[auv3-standalone] Appex bundle loaded via NSBundle" << std::endl; + } } - std::cout << "[auv3-standalone] Appex bundle loaded" << std::endl; } // Get the principal class from the appex's Info.plist @@ -165,8 +189,10 @@ - (AUAudioUnit *)instantiateAUDirectlyFromAppex:(NSBundle *)appexBundle return nil; } - // Create the factory and ask it to create the AU + // Create the factory and ask it to create the AU. + // The factory IS also the view controller (AUViewController subclass), so keep it alive. id factory = [[factoryClass alloc] init]; + _factoryViewController = (NSViewController *)factory; AUAudioUnit *au = [factory createAudioUnitWithComponentDescription:desc error:outError]; if (!au) @@ -422,38 +448,39 @@ - (void)setupGUI - (void)setupGUIFromAUAudioUnit:(AUAudioUnit *)au { - [au requestViewControllerWithCompletionHandler:^(AUViewControllerBase *vc) { - dispatch_async(dispatch_get_main_queue(), ^{ - if (!vc) - { - std::cout << "[auv3-standalone] No view controller provided by AU (direct)" << std::endl; - [[self window] orderFrontRegardless]; - return; - } + // Use the factory VC directly — it IS the view controller (AUViewController subclass) + // and already has audioUnit set from createAudioUnitWithComponentDescription:. + NSViewController *vc = _factoryViewController; + if (!vc) + { + std::cout << "[auv3-standalone] No factory view controller available (direct)" << std::endl; + [[self window] orderFrontRegardless]; + return; + } - self->_auViewController = vc; + dispatch_async(dispatch_get_main_queue(), ^{ + self->_auViewController = vc; - NSSize preferredSize = vc.preferredContentSize; - if (preferredSize.width < 1 || preferredSize.height < 1) - { - preferredSize = NSMakeSize(480, 360); - } + NSSize preferredSize = vc.preferredContentSize; + if (preferredSize.width < 1 || preferredSize.height < 1) + { + preferredSize = NSMakeSize(480, 360); + } - [[self window] setContentSize:preferredSize]; - [[self window] setDelegate:self]; + [[self window] setContentSize:preferredSize]; + [[self window] setDelegate:self]; - NSView *contentView = [[self window] contentView]; - NSView *auView = vc.view; - auView.frame = contentView.bounds; - auView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; - [contentView addSubview:auView]; + NSView *contentView = [[self window] contentView]; + NSView *auView = vc.view; + auView.frame = contentView.bounds; + auView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; + [contentView addSubview:auView]; - [[self window] orderFrontRegardless]; + [[self window] orderFrontRegardless]; - std::cout << "[auv3-standalone] GUI displayed (direct) (" - << (int)preferredSize.width << "x" << (int)preferredSize.height << ")" << std::endl; - }); - }]; + std::cout << "[auv3-standalone] GUI displayed (direct) (" + << (int)preferredSize.width << "x" << (int)preferredSize.height << ")" << std::endl; + }); } - (void)setupMIDIForAUAudioUnit:(AUAudioUnit *)au diff --git a/src/wrapasauv3.mm b/src/wrapasauv3.mm index 9d2eafc2..36523190 100644 --- a/src/wrapasauv3.mm +++ b/src/wrapasauv3.mm @@ -17,3 +17,16 @@ #include "generated_auv3_entrypoints.hxx" #pragma clang diagnostic pop + +#include + +// App extension entry point. +// For in-process hosting, macOS loads the appex bundle directly and +// instantiates NSExtensionPrincipalClass — main() is never reached. +// For out-of-process hosting, macOS launches this executable and +// dispatch_main() keeps the process alive for XPC message handling. +int mainXXX(int, const char *[]) +{ + // dispatch_main(); + return 0; +} From 71b68b7e5bac31675541b93452d9b8f472245552 Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Sat, 11 Apr 2026 00:22:33 +0200 Subject: [PATCH 03/23] 3 fixes --- src/detail/auv3/auv3_audiounit.mm | 133 ++++++++++++++++++++++++------ 1 file changed, 110 insertions(+), 23 deletions(-) diff --git a/src/detail/auv3/auv3_audiounit.mm b/src/detail/auv3/auv3_audiounit.mm index a4c12f68..85db9f85 100644 --- a/src/detail/auv3/auv3_audiounit.mm +++ b/src/detail/auv3/auv3_audiounit.mm @@ -492,6 +492,15 @@ - (instancetype)initWithComponentDescription:(AudioComponentDescription)componen [self _buildBusArrays]; _renderResourcesAllocated = NO; + + // Wire up parameter observer so parameter changes reach the CLAP plugin + // both during rendering (via process adapter) and outside rendering (via flush). + if (_impl->_parameterTree) + { + AUV3LOG("init: wiring parameter observer"); + [self _wireParameterObserver]; + } + AUV3LOG("init: completed successfully"); } catch (int e) @@ -586,6 +595,58 @@ - (void)_buildBusArrays _outputBusArray = [[AUAudioUnitBusArray alloc] initWithAudioUnit:self busType:AUAudioUnitBusTypeOutput busses:outputs]; } +- (void)_wireParameterObserver +{ + __weak typeof(self) weakSelf = self; + + _impl->_parameterTree.implementorValueObserver = ^(AUParameter *param, AUValue value) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf || !strongSelf->_impl) return; + if (!strongSelf->_impl->_plugin || !strongSelf->_impl->_plugin->_ext._params) return; + + // When render resources are allocated, parameter changes arrive via the + // render event list (AURenderEventParameter) — the thread-safe path. + // Do NOT call addParameterEvent here as it races with process() on + // the render thread (both touch _events/_eventindices without locking). + if (strongSelf->_renderResourcesAllocated) return; + + // Non-realtime path: push directly to the CLAP plugin via flush. + // This is safe because flush must only be called when not processing. + auto *plugin = strongSelf->_impl->_plugin->_plugin; + auto *ext_params = strongSelf->_impl->_plugin->_ext._params; + + clap_event_param_value_t ev = {}; + ev.header.size = sizeof(ev); + ev.header.type = CLAP_EVENT_PARAM_VALUE; + ev.header.space_id = CLAP_CORE_EVENT_SPACE_ID; + ev.header.time = 0; + ev.header.flags = 0; + ev.param_id = (clap_id)param.address; + ev.value = (double)value; + ev.port_index = -1; + ev.key = -1; + ev.channel = -1; + ev.note_id = -1; + + // Build a single-event input list + const clap_event_header_t *evPtr = &ev.header; + clap_input_events_t in_events = {}; + in_events.ctx = &evPtr; + in_events.size = [](const clap_input_events_t *) -> uint32_t { return 1; }; + in_events.get = [](const clap_input_events_t *list, uint32_t) -> const clap_event_header_t * { + return *static_cast(list->ctx); + }; + + clap_output_events_t out_events = {}; + out_events.ctx = nullptr; + out_events.try_push = [](const clap_output_events_t *, const clap_event_header_t *) -> bool { + return true; + }; + + ext_params->flush(plugin, &in_events, &out_events); + }; +} + // --- AUAudioUnit property overrides --- - (AUAudioUnitBusArray *)inputBusses @@ -644,8 +705,48 @@ - (NSTimeInterval)tailTime - (BOOL)shouldChangeToFormat:(AVAudioFormat *)format forBus:(AUAudioUnitBus *)bus { - // Accept format changes - return YES; + if (!_impl) return NO; + + uint32_t requestedChannels = format.channelCount; + + // Check input busses + for (NSUInteger i = 0; i < self.inputBusses.count; ++i) + { + if (self.inputBusses[i] == bus) + { + if (i < _impl->_inputBusInfos.size()) + { + BOOL ok = (requestedChannels == _impl->_inputBusInfos[i].channelCount); + AUV3LOG("shouldChangeToFormat: input bus %lu requested %u ch, supported %u -> %{public}s", + (unsigned long)i, requestedChannels, _impl->_inputBusInfos[i].channelCount, + ok ? "YES" : "NO"); + return ok; + } + AUV3LOG("shouldChangeToFormat: input bus %lu out of range", (unsigned long)i); + return NO; + } + } + + // Check output busses + for (NSUInteger i = 0; i < self.outputBusses.count; ++i) + { + if (self.outputBusses[i] == bus) + { + if (i < _impl->_outputBusInfos.size()) + { + BOOL ok = (requestedChannels == _impl->_outputBusInfos[i].channelCount); + AUV3LOG("shouldChangeToFormat: output bus %lu requested %u ch, supported %u -> %{public}s", + (unsigned long)i, requestedChannels, _impl->_outputBusInfos[i].channelCount, + ok ? "YES" : "NO"); + return ok; + } + AUV3LOG("shouldChangeToFormat: output bus %lu out of range", (unsigned long)i); + return NO; + } + } + + AUV3LOG("shouldChangeToFormat: bus not found, rejecting"); + return NO; } // --- State save/restore --- @@ -774,20 +875,6 @@ - (BOOL)allocateRenderResourcesAndReturnError:(NSError **)outError _impl->_plugin->start_processing(); _impl->_initialized = true; - // Wire up the parameter value observer - if (_impl->_parameterTree) - { - AUV3LOG("allocateRenderResources: wiring parameter observer"); - __weak typeof(self) weakSelf = self; - _impl->_parameterTree.implementorValueObserver = ^(AUParameter *param, AUValue value) { - __strong typeof(weakSelf) strongSelf = weakSelf; - if (strongSelf && strongSelf->_impl && strongSelf->_impl->_processAdapter) - { - strongSelf->_impl->_processAdapter->addParameterEvent((clap_id)param.address, (double)value, 0); - } - }; - } - _renderResourcesAllocated = YES; AUV3LOG("allocateRenderResources: completed successfully"); return YES; @@ -821,10 +908,10 @@ - (void)deallocateRenderResources - (AUInternalRenderBlock)internalRenderBlock { - // Capture a raw pointer to the C++ impl for use in the render block. - // This is safe because the render block's lifetime is bounded by - // allocateRenderResources / deallocateRenderResources. - auto *adapter = _impl->_processAdapter.get(); + // Capture the stable _impl pointer — the framework may cache this block before + // allocateRenderResources is called, so we must dereference _processAdapter at + // render time rather than at block-creation time. + auto *impl = _impl.get(); return ^AUAudioUnitStatus(AudioUnitRenderActionFlags *actionFlags, const AudioTimeStamp *timestamp, @@ -833,10 +920,10 @@ - (AUInternalRenderBlock)internalRenderBlock AudioBufferList *outputData, const AURenderEvent *realtimeEventListHead, AURenderPullInputBlock __unsafe_unretained pullInputBlock) { - if (!adapter) return kAudioUnitErr_Uninitialized; + if (!impl || !impl->_processAdapter) return kAudioUnitErr_Uninitialized; - return adapter->process(actionFlags, timestamp, frameCount, outputBusNumber, outputData, - realtimeEventListHead, pullInputBlock); + return impl->_processAdapter->process(actionFlags, timestamp, frameCount, outputBusNumber, + outputData, realtimeEventListHead, pullInputBlock); }; } From 844aa7c47615212eb3ec436ea612ad84ca4097f3 Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Sat, 11 Apr 2026 18:42:23 +0200 Subject: [PATCH 04/23] finally passing auval --- src/clap_proxy.cpp | 12 ++ src/detail/auv3/auv3_audiounit.mm | 321 +++++++++++++++++++++++++++--- src/detail/auv3/process.h | 19 +- src/detail/auv3/process.mm | 170 ++++++++++++++-- 4 files changed, 473 insertions(+), 49 deletions(-) diff --git a/src/clap_proxy.cpp b/src/clap_proxy.cpp index 06226d66..d17335bd 100644 --- a/src/clap_proxy.cpp +++ b/src/clap_proxy.cpp @@ -6,6 +6,9 @@ #include #define OutputDebugString(x) std::cout << __FILE__ << ":" << __LINE__ << " " << (x) << std::endl; #define OutputDebugStringA(x) std::cout << __FILE__ << ":" << __LINE__ << " " << (x) << std::endl; +#if __has_include() +#include +#endif #endif #if WIN #include @@ -474,6 +477,15 @@ void Plugin::log(clap_log_severity severity, const char *msg) #endif #if MAC fprintf(stderr, "%s\n", n.c_str()); +#if __has_include() + { + static os_log_t sPluginLog = os_log_create("org.clap-wrapper.auv3", "plugin"); + if (severity >= CLAP_LOG_ERROR) + os_log_error(sPluginLog, "%{public}s", n.c_str()); + else + os_log(sPluginLog, "%{public}s", n.c_str()); + } +#endif #endif } diff --git a/src/detail/auv3/auv3_audiounit.mm b/src/detail/auv3/auv3_audiounit.mm index 85db9f85..f01c672f 100644 --- a/src/detail/auv3/auv3_audiounit.mm +++ b/src/detail/auv3/auv3_audiounit.mm @@ -19,6 +19,7 @@ #include #include #include +#include static os_log_t _auv3Log() { static os_log_t log = os_log_create("org.clap-wrapper.auv3", "wrapper"); @@ -27,6 +28,13 @@ static os_log_t _auv3Log() { #define AUV3LOG(...) os_log(_auv3Log(), __VA_ARGS__) #define AUV3ERR(...) os_log_error(_auv3Log(), __VA_ARGS__) +// Forward-declare private methods used by C++ code before the @implementation +@interface ClapAUv3AudioUnit () +- (void)_replaceParameterTree; +- (void)_notifyParameterValuesChanged; +- (void)_wireParameterObserver; +@end + // ----------------------------------------------------------------------- // C++ implementation detail bridging IHost, IAutomation, and IPlugObject // ----------------------------------------------------------------------- @@ -65,8 +73,6 @@ static os_log_t _auv3Log() { AUV3LOG("~AUv3ImplDetail: destructor entered (plugin=%{public}s)", _plugin ? "valid" : "null"); if (_plugin) { - AUV3LOG("~AUv3ImplDetail: calling _os_attached.off()"); - _os_attached.off(); AUV3LOG("~AUv3ImplDetail: calling _plugin->terminate()"); _plugin->terminate(); AUV3LOG("~AUv3ImplDetail: calling _plugin.reset()"); @@ -105,6 +111,7 @@ static os_log_t _auv3Log() { std::string _hostname = "CLAP-as-AUv3"; std::atomic _initialized{false}; std::atomic_bool _requestUICallback{false}; + dispatch_source_t _idleTimer = nullptr; // Back-reference to the ObjC audio unit (weak to avoid retain cycle) __weak ClapAUv3AudioUnit *_audioUnit = nil; @@ -112,14 +119,64 @@ static os_log_t _auv3Log() { // The NSView that the CLAP GUI is parented to (set by createGUIInView:) __weak NSView *_guiParentView = nil; + // Cached parameter values — avoids calling params->get_value() on every + // provider callback (wrong thread, expensive via XPC). Updated on set/flush/process. + // Reads from XPC thread, writes from XPC + audio thread; aligned double is + // naturally atomic on arm64/x86_64 so benign race at worst (slightly stale value). + std::unordered_map _paramValueCache; + std::unordered_map _paramCookieCache; + // Queue for audio -> UI thread parameter notifications ClapWrapper::detail::shared::fixedqueue _queueToUI; // --- IHost --- - void mark_dirty() override {} - void restartPlugin() override {} + void mark_dirty() override + { + AUV3LOG("IHost::mark_dirty() called"); + } + void restartPlugin() override + { + AUV3LOG("IHost::restartPlugin() called"); + } - void request_callback() override { _requestUICallback = true; } + void request_callback() override + { + // Just set the flag. The main-queue idle timer will service it between + // render cycles. Never call on_main_thread() synchronously or from + // the render thread — JUCE holds locks in on_main_thread() that + // process() also needs, causing deadlock. + _requestUICallback = true; + } + + void startIdleTimer() + { + if (_idleTimer) return; + _idleTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); + dispatch_source_set_timer(_idleTimer, DISPATCH_TIME_NOW, 10 * NSEC_PER_MSEC, 1 * NSEC_PER_MSEC); + + auto plugin = _plugin; + auto *flag = &_requestUICallback; + auto *processing = &_initialized; // true between start_processing/stop_processing + dispatch_source_set_event_handler(_idleTimer, ^{ + // Do NOT call on_main_thread() while the plugin is processing. + // JUCE's on_main_thread() acquires locks that process() also needs — + // calling both concurrently (main thread vs render thread) deadlocks. + if (processing->load() || !flag->exchange(false)) return; + + auto guard = plugin->AlwaysMainThread(); + plugin->_plugin->on_main_thread(plugin->_plugin); + }); + dispatch_resume(_idleTimer); + } + + void stopIdleTimer() + { + if (_idleTimer) + { + dispatch_source_cancel(_idleTimer); + _idleTimer = nullptr; + } + } void setupWrapperSpecifics(const clap_plugin_t *plugin) override { @@ -187,25 +244,126 @@ void setupParameters(const clap_plugin_t *plugin, const clap_plugin_params_t *params) override { _parameterTree = Clap::AUv3::createParameterTree(plugin, params); + + // Populate the parameter value and cookie caches with initial values + if (params) + { + uint32_t numParams = params->count(plugin); + for (uint32_t i = 0; i < numParams; ++i) + { + clap_param_info_t info; + if (params->get_info(plugin, i, &info)) + { + double value = 0; + if (params->get_value(plugin, info.id, &value)) + _paramValueCache[info.id] = value; + else + _paramValueCache[info.id] = info.default_value; + _paramCookieCache[info.id] = info.cookie; + } + } + } } void param_rescan(clap_param_rescan_flags flags) override { - // TODO: Rebuild parameter tree when plugin requests rescan - std::cout << "[clap-wrapper] auv3: param_rescan requested (not yet fully implemented)" << std::endl; + AUV3LOG("IHost::param_rescan(flags=0x%x) called", (unsigned)flags); + if (!_plugin || !_plugin->_ext._params) return; + + auto mainGuard = _plugin->AlwaysMainThread(); + auto *params = _plugin->_ext._params; + auto *plug = _plugin->_plugin; + + if (flags & (CLAP_PARAM_RESCAN_ALL | CLAP_PARAM_RESCAN_INFO)) + { + // AUParameter properties (name, range, flags) are immutable — rebuild the entire tree. + _parameterTree = Clap::AUv3::createParameterTree(plug, params); + + // Immediately replace the value provider with the cached version — + // createParameterTree() wires a provider that calls get_value() directly, + // which fails the thread check if called from the render thread. + auto *cache = &_paramValueCache; + _parameterTree.implementorValueProvider = ^AUValue(AUParameter *param) { + auto it = cache->find((clap_id)param.address); + if (it != cache->end()) + return (AUValue)it->second; + return (AUValue)0.0; + }; + + // Refresh value and cookie caches + _paramValueCache.clear(); + _paramCookieCache.clear(); + uint32_t n = params->count(plug); + for (uint32_t i = 0; i < n; ++i) + { + clap_param_info_t info; + if (params->get_info(plug, i, &info)) + { + double value = 0; + if (params->get_value(plug, info.id, &value)) + _paramValueCache[info.id] = value; + else + _paramValueCache[info.id] = info.default_value; + _paramCookieCache[info.id] = info.cookie; + } + } + + // Notify AUv3 host via KVO — must be on main thread + __strong auto au = _audioUnit; + if (au) + { + dispatch_async(dispatch_get_main_queue(), ^{ + [au _replaceParameterTree]; + }); + } + } + else if (flags & CLAP_PARAM_RESCAN_VALUES) + { + // Just refresh cached values — tree structure is unchanged + uint32_t n = params->count(plug); + for (uint32_t i = 0; i < n; ++i) + { + clap_param_info_t info; + if (params->get_info(plug, i, &info)) + { + double value = 0; + if (params->get_value(plug, info.id, &value)) + _paramValueCache[info.id] = value; + } + } + + // Notify host that values changed + __strong auto au = _audioUnit; + if (au) + { + dispatch_async(dispatch_get_main_queue(), ^{ + [au _notifyParameterValuesChanged]; + }); + } + } + + // CLAP_PARAM_RESCAN_TEXT needs no action — implementorStringFromValueCallback + // already calls plugin->value_to_text() on each invocation. } - void param_clear(clap_id param, clap_param_clear_flags flags) override {} - void param_request_flush() override {} + void param_clear(clap_id param, clap_param_clear_flags flags) override + { + AUV3LOG("IHost::param_clear(param=%u, flags=0x%x) called", (unsigned)param, (unsigned)flags); + } + + void param_request_flush() override + { + AUV3LOG("IHost::param_request_flush() called"); + } void latency_changed() override { - // AUv3 handles latency via the latency property - hosts observe it via KVO + AUV3LOG("IHost::latency_changed() called"); } void tail_changed() override { - // AUv3 handles tail time via the tailTime property + AUV3LOG("IHost::tail_changed() called"); } bool gui_can_resize() override @@ -285,6 +443,7 @@ bool context_menu_popup(const clap_context_menu_target_t *target, int32_t screen // --- IAutomation --- void onBeginEdit(clap_id id) override { + AUV3LOG("IAutomation::onBeginEdit(id=%u)", (unsigned)id); queueEvent evt; evt._type = queueEvent::type::editstart; evt._data._id = id; @@ -293,6 +452,10 @@ void onBeginEdit(clap_id id) override void onPerformEdit(const clap_event_param_value_t *value) override { + AUV3LOG("IAutomation::onPerformEdit(id=%u, value=%.4f)", (unsigned)value->param_id, value->value); + // Update cache immediately (audio thread write, benign race with reader) + _paramValueCache[value->param_id] = value->value; + queueEvent evt; evt._type = queueEvent::type::editvalue; evt._data._value = *value; @@ -301,6 +464,7 @@ void onPerformEdit(const clap_event_param_value_t *value) override void onEndEdit(clap_id id) override { + AUV3LOG("IAutomation::onEndEdit(id=%u)", (unsigned)id); queueEvent evt; evt._type = queueEvent::type::editend; evt._data._id = id; @@ -483,8 +647,11 @@ - (instancetype)initWithComponentDescription:(AudioComponentDescription)componen AUV3LOG("init: calling plugin->initialize()"); _impl->_plugin->initialize(); - AUV3LOG("init: calling _os_attached.on()"); - _impl->_os_attached.on(); + // Start the idle timer on the main queue. This services request_callback() + // (on_main_thread) between render cycles. We don't use the global os::attach + // mechanism — its CFRunLoopTimer is unreliable in out-of-process AUv3. + AUV3LOG("init: starting idle timer on main queue"); + _impl->startIdleTimer(); // Build audio bus arrays from the CLAP audio port info AUV3LOG("init: building bus arrays (inputs=%zu outputs=%zu)", @@ -539,15 +706,19 @@ - (void)dealloc _impl ? "valid" : "null", (_impl && _impl->_plugin) ? "valid" : "null"); - if (_impl && _impl->_plugin) + if (_impl) { - AUV3LOG("dealloc: calling _os_attached.off()"); - _impl->_os_attached.off(); - AUV3LOG("dealloc: calling _plugin->terminate()"); - _impl->_plugin->terminate(); - AUV3LOG("dealloc: calling _plugin.reset()"); - _impl->_plugin.reset(); - AUV3LOG("dealloc: plugin teardown complete"); + AUV3LOG("dealloc: stopping idle timer"); + _impl->stopIdleTimer(); + + if (_impl->_plugin) + { + AUV3LOG("dealloc: calling _plugin->terminate()"); + _impl->_plugin->terminate(); + AUV3LOG("dealloc: calling _plugin.reset()"); + _impl->_plugin.reset(); + AUV3LOG("dealloc: plugin teardown complete"); + } } AUV3LOG("dealloc: calling _impl.reset()"); _impl.reset(); @@ -604,6 +775,9 @@ - (void)_wireParameterObserver if (!strongSelf || !strongSelf->_impl) return; if (!strongSelf->_impl->_plugin || !strongSelf->_impl->_plugin->_ext._params) return; + // Always update the cache + strongSelf->_impl->_paramValueCache[(clap_id)param.address] = (double)value; + // When render resources are allocated, parameter changes arrive via the // render event list (AURenderEventParameter) — the thread-safe path. // Do NOT call addParameterEvent here as it races with process() on @@ -615,18 +789,21 @@ - (void)_wireParameterObserver auto *plugin = strongSelf->_impl->_plugin->_plugin; auto *ext_params = strongSelf->_impl->_plugin->_ext._params; + clap_id pid = (clap_id)param.address; clap_event_param_value_t ev = {}; ev.header.size = sizeof(ev); ev.header.type = CLAP_EVENT_PARAM_VALUE; ev.header.space_id = CLAP_CORE_EVENT_SPACE_ID; ev.header.time = 0; ev.header.flags = 0; - ev.param_id = (clap_id)param.address; + ev.param_id = pid; ev.value = (double)value; ev.port_index = -1; ev.key = -1; ev.channel = -1; ev.note_id = -1; + auto cookieIt = strongSelf->_impl->_paramCookieCache.find(pid); + ev.cookie = (cookieIt != strongSelf->_impl->_paramCookieCache.end()) ? cookieIt->second : nullptr; // Build a single-event input list const clap_event_header_t *evPtr = &ev.header; @@ -643,8 +820,63 @@ - (void)_wireParameterObserver return true; }; + auto mainGuard = strongSelf->_impl->_plugin->AlwaysMainThread(); ext_params->flush(plugin, &in_events, &out_events); }; + + // Rewire the parameter tree callbacks. The provider uses the local cache + // instead of calling params->get_value() (which requires main thread and is + // expensive over XPC). String conversion still calls into the plugin with guards. + auto plugin = _impl->_plugin; // shared_ptr keeps it alive in the blocks + auto *cache = &_impl->_paramValueCache; + + _impl->_parameterTree.implementorValueProvider = ^AUValue(AUParameter *param) { + auto it = cache->find((clap_id)param.address); + if (it != cache->end()) + return (AUValue)it->second; + return (AUValue)0.0; + }; + + _impl->_parameterTree.implementorStringFromValueCallback = ^NSString *(AUParameter *param, const AUValue *value) { + auto guard = plugin->AlwaysMainThread(); + char buf[256]; + AUValue v = value ? *value : param.value; + if (plugin->_ext._params->value_to_text(plugin->_plugin, (clap_id)param.address, (double)v, buf, sizeof(buf))) + { + return [NSString stringWithUTF8String:buf]; + } + return [NSString stringWithFormat:@"%.3f", v]; + }; + + _impl->_parameterTree.implementorValueFromStringCallback = ^AUValue(AUParameter *param, NSString *string) { + auto guard = plugin->AlwaysMainThread(); + double value = 0; + if (plugin->_ext._params->text_to_value(plugin->_plugin, (clap_id)param.address, [string UTF8String], &value)) + { + return (AUValue)value; + } + return (AUValue)[string doubleValue]; + }; +} + +- (void)_replaceParameterTree +{ + AUV3LOG("_replaceParameterTree: firing KVO and re-wiring callbacks"); + // Fire KVO so the host picks up the new tree + [self willChangeValueForKey:@"parameterTree"]; + [self didChangeValueForKey:@"parameterTree"]; + + // Re-wire the provider, observer, and string conversion callbacks + [self _wireParameterObserver]; +} + +- (void)_notifyParameterValuesChanged +{ + AUV3LOG("_notifyParameterValuesChanged: firing KVO"); + // Pseudo-property documented in AUAudioUnit.h — hosts observe this + // to know when all parameter values have been invalidated + [self willChangeValueForKey:@"allParameterValues"]; + [self didChangeValueForKey:@"allParameterValues"]; } // --- AUAudioUnit property overrides --- @@ -788,7 +1020,26 @@ - (void)setFullState:(NSDictionary *)fullState AUV3LOG("setFullState (restore): loading %zu bytes of CLAP state", (size_t)[clapState length]); Clap::StateMemento chunk; chunk.setData((const uint8_t *)[clapState bytes], [clapState length]); + auto mainGuard = _impl->_plugin->AlwaysMainThread(); _impl->_plugin->_ext._state->load(_impl->_plugin->_plugin, chunk); + + // Refresh the parameter cache after state restore — all values may have changed + if (_impl->_plugin->_ext._params) + { + auto *params = _impl->_plugin->_ext._params; + auto *plug = _impl->_plugin->_plugin; + uint32_t numParams = params->count(plug); + for (uint32_t i = 0; i < numParams; ++i) + { + clap_param_info_t info; + if (params->get_info(plug, i, &info)) + { + double value = 0; + if (params->get_value(plug, info.id, &value)) + _impl->_paramValueCache[info.id] = value; + } + } + } AUV3LOG("setFullState (restore): completed"); } else @@ -862,8 +1113,12 @@ - (BOOL)allocateRenderResourcesAndReturnError:(NSError **)outError _impl->_plugin->_plugin, _impl->_plugin->_ext._params, _impl.get(), self.maximumFramesToRender, _impl->_midi_preferred_dialect); - // Set transport state block + // Set transport state and musical context blocks _impl->_processAdapter->setTransportStateBlock(self.transportStateBlock); + _impl->_processAdapter->setMusicalContextBlock(self.musicalContextBlock); + + // Wire cookie cache for parameter events + _impl->_processAdapter->_cookieCache = &_impl->_paramCookieCache; // Set MIDI output block _impl->_processAdapter->midiOutputEventBlock = self.MIDIOutputEventBlock; @@ -922,8 +1177,24 @@ - (AUInternalRenderBlock)internalRenderBlock AURenderPullInputBlock __unsafe_unretained pullInputBlock) { if (!impl || !impl->_processAdapter) return kAudioUnitErr_Uninitialized; - return impl->_processAdapter->process(actionFlags, timestamp, frameCount, outputBusNumber, - outputData, realtimeEventListHead, pullInputBlock); + // Force audio-thread identity for the duration of the render call. + // In out-of-process AUv3, _main_thread_id was captured on the XPC worker + // thread during init, so the default heuristic is wrong. + auto audioGuard = impl->_plugin->AlwaysAudioThread(); + + auto status = impl->_processAdapter->process(actionFlags, timestamp, frameCount, outputBusNumber, + outputData, realtimeEventListHead, pullInputBlock); + + // Do NOT dispatch on_main_thread() from the render block. Surge XT's + // on_main_thread() acquires JUCE locks that process() also needs — dispatching + // it asynchronously causes lock contention: on_main_thread() runs on main while + // process() runs on render thread, both needing the same lock → deadlock. + // + // The _requestUICallback flag is still set by request_callback(). It will be + // serviced when a GUI is active (via idle timer) or when the plugin is not + // processing (e.g., after deallocateRenderResources). + + return status; }; } diff --git a/src/detail/auv3/process.h b/src/detail/auv3/process.h index f074857f..fb3855b5 100644 --- a/src/detail/auv3/process.h +++ b/src/detail/auv3/process.h @@ -22,6 +22,7 @@ #import #include #include +#include #include "../clap/automation.h" namespace Clap::AUv3 @@ -59,8 +60,9 @@ class ProcessAdapter const AURenderEvent *realtimeEventListHead, AURenderPullInputBlock __unsafe_unretained pullInputBlock); - // Provide transport state from the host + // Provide transport/musical context from the host void setTransportStateBlock(AUHostTransportStateBlock __nullable block); + void setMusicalContextBlock(AUHostMusicalContextBlock __nullable block); // Queue a parameter change from the host (outside render block) void addParameterEvent(clap_id paramId, double value, uint32_t sampleOffset); @@ -77,8 +79,13 @@ class ProcessAdapter void sortEventIndices(); bool enqueueOutputEvent(const clap_event_header_t *event); - void translateAUv3Events(const AURenderEvent *head); + void translateAUv3Events(const AURenderEvent *head, AUEventSampleTime bufferStartTime, + AVAudioFrameCount frameCount); + public: + const std::unordered_map *_cookieCache = nullptr; + + private: const clap_plugin_t *_plugin = nullptr; const clap_plugin_params_t *_ext_params = nullptr; Clap::IAutomation *_automation = nullptr; @@ -104,10 +111,18 @@ class ProcessAdapter uint32_t _preferred_midi_dialect = CLAP_NOTE_DIALECT_CLAP; AUHostTransportStateBlock __nullable _transportStateBlock = nil; + AUHostMusicalContextBlock __nullable _musicalContextBlock = nil; // Temporary storage for input pulling AudioBufferList *_inputBufferList = nullptr; uint32_t _inputBufferListChannels = 0; + + // Multi-bus render tracking: AUv3 calls the render block once per output bus, + // but CLAP processes all buses in a single process() call. We process on the + // first bus and just copy output for subsequent buses in the same cycle. + uint64_t _lastProcessedSampleTime = UINT64_MAX; + uint32_t _numMaxSamples = 0; + std::vector> _outputStorage; // [bus * maxCh + ch][samples] }; } // namespace Clap::AUv3 diff --git a/src/detail/auv3/process.mm b/src/detail/auv3/process.mm index d856c1a4..e8365e6a 100644 --- a/src/detail/auv3/process.mm +++ b/src/detail/auv3/process.mm @@ -3,10 +3,18 @@ #include "process.h" +#include #include #include #include +static os_log_t _procLog() { + static os_log_t log = os_log_create("org.clap-wrapper.auv3", "process"); + return log; +} +#define PROCLOG(...) os_log(_procLog(), __VA_ARGS__) +#define PROCERR(...) os_log_error(_procLog(), __VA_ARGS__) + namespace Clap::AUv3 { @@ -136,6 +144,13 @@ inline clap_sectime doubleToSecTime(double t) _inputBufferListChannels = maxCh; } + // Allocate output storage for multi-bus rendering (max 8 channels per bus) + _numMaxSamples = numMaxSamples; + _outputStorage.resize(_numOutputs * 8); + for (auto &buf : _outputStorage) + buf.resize(numMaxSamples, 0.0f); + _lastProcessedSampleTime = UINT64_MAX; + // Wire up CLAP process data _processData.audio_inputs = _input_ports; _processData.audio_inputs_count = _numInputs; @@ -169,6 +184,11 @@ inline clap_sectime doubleToSecTime(double t) _transportStateBlock = block; } +void ProcessAdapter::setMusicalContextBlock(AUHostMusicalContextBlock __nullable block) +{ + _musicalContextBlock = block; +} + void ProcessAdapter::sortEventIndices() { std::sort(_eventindices.begin(), _eventindices.end(), @@ -180,32 +200,53 @@ inline clap_sectime doubleToSecTime(double t) }); } -void ProcessAdapter::translateAUv3Events(const AURenderEvent *head) +void ProcessAdapter::translateAUv3Events(const AURenderEvent *head, + AUEventSampleTime bufferStartTime, + AVAudioFrameCount frameCount) { for (const AURenderEvent *event = head; event != nullptr; event = event->head.next) { clap_multi_event_t n; memset(&n, 0, sizeof(n)); + // Convert AUv3 absolute sample time to CLAP buffer-relative offset. + // AUEventSampleTimeImmediate (0xffffffff00000000) means "now" — treat as offset 0. + auto absTime = event->head.eventSampleTime; + uint32_t sampleOffset = 0; + if (absTime >= bufferStartTime && absTime != AUEventSampleTimeImmediate) + { + int64_t rel = absTime - bufferStartTime; + sampleOffset = (rel >= 0 && rel < (int64_t)frameCount) ? (uint32_t)rel : 0; + } + switch (event->head.eventType) { case AURenderEventParameter: case AURenderEventParameterRamp: { auto &pe = event->parameter; + PROCLOG("translateEvent: param addr=%llu value=%.4f absTime=%lld offset=%u", + (unsigned long long)pe.parameterAddress, (float)pe.value, + (long long)pe.eventSampleTime, sampleOffset); n.header.size = sizeof(clap_event_param_value_t); n.header.type = CLAP_EVENT_PARAM_VALUE; n.header.space_id = CLAP_CORE_EVENT_SPACE_ID; - n.header.time = (uint32_t)pe.eventSampleTime; + n.header.time = sampleOffset; n.header.flags = 0; - n.param.param_id = (clap_id)pe.parameterAddress; + clap_id pid = (clap_id)pe.parameterAddress; + + // Skip unknown parameter IDs — auval sends bogus IDs to test robustness + if (_cookieCache && _cookieCache->find(pid) == _cookieCache->end()) + break; + + n.param.param_id = pid; n.param.value = (double)pe.value; n.param.port_index = -1; n.param.key = -1; n.param.channel = -1; n.param.note_id = -1; - n.param.cookie = nullptr; + n.param.cookie = _cookieCache ? _cookieCache->at(pid) : nullptr; _eventindices.emplace_back(_events.size()); _events.emplace_back(n); @@ -214,12 +255,14 @@ inline clap_sectime doubleToSecTime(double t) case AURenderEventMIDI: { + auto &me = event->MIDI; + PROCLOG("translateEvent: %02x %02x %02x",(int)me.data[0],(int)me.data[1],(int)me.data[2]); uint8_t status = me.data[0]; uint8_t strippedStatus = (status >> 4) & 0x0F; uint8_t channel = status & 0x0F; - n.header.time = (uint32_t)me.eventSampleTime; + n.header.time = sampleOffset; n.header.flags = 0; n.header.space_id = CLAP_CORE_EVENT_SPACE_ID; @@ -275,7 +318,7 @@ inline clap_sectime doubleToSecTime(double t) n.header.type = CLAP_EVENT_MIDI_SYSEX; n.header.size = sizeof(clap_event_midi_sysex_t); n.header.space_id = CLAP_CORE_EVENT_SPACE_ID; - n.header.time = (uint32_t)se.eventSampleTime; + n.header.time = sampleOffset; n.header.flags = 0; n.sysex.port_index = 0; n.sysex.buffer = se.data; @@ -301,16 +344,28 @@ inline clap_sectime doubleToSecTime(double t) const AURenderEvent *realtimeEventListHead, AURenderPullInputBlock __unsafe_unretained pullInputBlock) { + // AUv3 calls the render block once per output bus. CLAP processes all buses + // in a single process() call. We only run the full CLAP process on bus 0, + // storing all output. Subsequent buses just copy from storage. + + if (outputBusNumber != 0) + { + goto copyOutput; + } + // Clear events from previous cycle _events.clear(); _eventindices.clear(); +#if 1 // Translate AUv3 events to CLAP events if (realtimeEventListHead) { - translateAUv3Events(realtimeEventListHead); + AUEventSampleTime bufferStart = (AUEventSampleTime)timestamp->mSampleTime; + translateAUv3Events(realtimeEventListHead, bufferStart, frameCount); + PROCLOG("process: translated %zu events", _events.size()); } - +#endif // Sort events by timestamp sortEventIndices(); @@ -318,6 +373,20 @@ inline clap_sectime doubleToSecTime(double t) // Setup transport _transport.flags = 0; + _transport.song_pos_beats = 0; + _transport.song_pos_seconds = 0; + _transport.tempo = 120; + _transport.tempo_inc = 0; + _transport.loop_start_beats = 0; + _transport.loop_end_beats = 0; + _transport.loop_start_seconds = 0; + _transport.loop_end_seconds = 0; + _transport.bar_start = 0; + _transport.bar_number = 0; + _transport.tsig_num = 4; + _transport.tsig_denom = 4; + _processData.steady_time = (int64_t)timestamp->mSampleTime; + if (_transportStateBlock) { AUHostTransportStateFlags transportFlags = 0; @@ -325,18 +394,13 @@ inline clap_sectime doubleToSecTime(double t) double cycleStartBeatPosition = 0; double cycleEndBeatPosition = 0; - // Query transport state if (_transportStateBlock(&transportFlags, ¤tSamplePosition, &cycleStartBeatPosition, &cycleEndBeatPosition)) { if (transportFlags & AUHostTransportStateMoving) - { _transport.flags |= CLAP_TRANSPORT_IS_PLAYING; - } if (transportFlags & AUHostTransportStateRecording) - { _transport.flags |= CLAP_TRANSPORT_IS_RECORDING; - } if (transportFlags & AUHostTransportStateCycling) { _transport.flags |= CLAP_TRANSPORT_IS_LOOP_ACTIVE; @@ -344,12 +408,50 @@ inline clap_sectime doubleToSecTime(double t) _transport.loop_end_beats = doubleToBeatTime(cycleEndBeatPosition); } } + } - // Note: AUv3 provides tempo via the musicalContextBlock property - // which can be queried separately if needed in the future. + if (false && _musicalContextBlock) + { + double tempo = 0; + double tsigNum = 0; + NSInteger tsigDenom = 0; + double beatPos = 0; + NSInteger sampleOffsetToNextBeat = 0; + double downbeatPos = 0; + + if (_musicalContextBlock(&tempo, &tsigNum, &tsigDenom, &beatPos, + &sampleOffsetToNextBeat, &downbeatPos)) + { + if (tempo > 0) + { + _transport.tempo = tempo; + _transport.flags |= CLAP_TRANSPORT_HAS_TEMPO; + } + _transport.song_pos_beats = doubleToBeatTime(beatPos); + _transport.flags |= CLAP_TRANSPORT_HAS_BEATS_TIMELINE; + + if (tsigDenom > 0) + { + _transport.tsig_num = (uint16_t)tsigNum; + _transport.tsig_denom = (uint16_t)tsigDenom; + _transport.flags |= CLAP_TRANSPORT_HAS_TIME_SIGNATURE; + } + + _transport.bar_start = doubleToBeatTime(downbeatPos); + + // Derive seconds position from beat position and tempo + if (tempo > 0) + { + double seconds = beatPos * 60.0 / tempo; + _transport.song_pos_seconds = doubleToSecTime(seconds); + _transport.flags |= CLAP_TRANSPORT_HAS_SECONDS_TIMELINE; + } + } } // Pull input audio + PROCLOG("process: pulling input (_numInputs=%u, pullInputBlock=%{public}s)", + _numInputs, pullInputBlock ? "yes" : "nil"); if (_numInputs > 0 && pullInputBlock) { for (uint32_t bus = 0; bus < _numInputs; ++bus) @@ -366,7 +468,9 @@ inline clap_sectime doubleToSecTime(double t) } AudioUnitRenderActionFlags pullFlags = 0; + PROCLOG("process: pulling bus %u (%u ch)", bus, numCh); AUAudioUnitStatus status = pullInputBlock(&pullFlags, timestamp, frameCount, bus, _inputBufferList); + PROCLOG("process: pull bus %u status=%d", bus, (int)status); if (status == noErr) { for (uint32_t ch = 0; ch < numCh && ch < _inputBufferList->mNumberBuffers; ++ch) @@ -385,18 +489,17 @@ inline clap_sectime doubleToSecTime(double t) } } - // Wire output buffers - if (outputData && outputBusNumber < _numOutputs) + // Point all output ports to our internal storage so CLAP writes there + for (uint32_t bus = 0; bus < _numOutputs; ++bus) { - uint32_t outBus = (uint32_t)outputBusNumber; - uint32_t numCh = std::min((uint32_t)outputData->mNumberBuffers, _output_ports[outBus].channel_count); - for (uint32_t ch = 0; ch < numCh; ++ch) + uint32_t numCh = _output_ports[bus].channel_count; + for (uint32_t ch = 0; ch < numCh && (bus * 8 + ch) < _outputStorage.size(); ++ch) { - _output_ports[outBus].data32[ch] = (float *)outputData->mBuffers[ch].mData; + _output_ports[bus].data32[ch] = _outputStorage[bus * 8 + ch].data(); } } - // Process! + // Process once for all buses _plugin->process(_plugin, &_processData); // Process output events @@ -455,6 +558,23 @@ inline clap_sectime doubleToSecTime(double t) } _outevents.clear(); +copyOutput: + // Copy stored output to the host's output buffer for this bus + if (outputData && outputBusNumber >= 0 && outputBusNumber < (NSInteger)_numOutputs) + { + uint32_t outBus = (uint32_t)outputBusNumber; + uint32_t numCh = std::min((uint32_t)outputData->mNumberBuffers, _output_ports[outBus].channel_count); + for (uint32_t ch = 0; ch < numCh; ++ch) + { + uint32_t storageIdx = outBus * 8 + ch; + if (storageIdx < _outputStorage.size() && outputData->mBuffers[ch].mData) + { + memcpy(outputData->mBuffers[ch].mData, _outputStorage[storageIdx].data(), + frameCount * sizeof(float)); + } + } + } + return noErr; } @@ -471,6 +591,12 @@ inline clap_sectime doubleToSecTime(double t) n.param.value = value; n.param.param_id = paramId; n.param.cookie = nullptr; + if (_cookieCache) + { + auto it = _cookieCache->find(paramId); + if (it != _cookieCache->end()) + n.param.cookie = it->second; + } n.param.port_index = -1; n.param.key = -1; n.param.channel = -1; From 9aaf2b4648995b630dc31c9b81501ccf3984090a Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Sat, 11 Apr 2026 21:59:29 +0200 Subject: [PATCH 05/23] WIP: AUv3 --- src/detail/auv3/auv3_audiounit.mm | 177 ++++++++++++++++-- src/detail/auv3/process.mm | 12 +- .../macos/auv3/AUv3HostAppDelegate.mm | 49 +++++ 3 files changed, 212 insertions(+), 26 deletions(-) diff --git a/src/detail/auv3/auv3_audiounit.mm b/src/detail/auv3/auv3_audiounit.mm index f01c672f..99f0b2e0 100644 --- a/src/detail/auv3/auv3_audiounit.mm +++ b/src/detail/auv3/auv3_audiounit.mm @@ -369,7 +369,10 @@ void tail_changed() override bool gui_can_resize() override { if (_plugin && _plugin->_ext._gui) + { + auto mainGuard = _plugin->AlwaysMainThread(); return _plugin->_ext._gui->can_resize(_plugin->_plugin); + } return false; } @@ -992,6 +995,7 @@ - (BOOL)shouldChangeToFormat:(AVAudioFormat *)format forBus:(AUAudioUnitBus *)bu if (_impl && _impl->_plugin && _impl->_plugin->_ext._state) { Clap::StateMemento chunk; + auto mainGuard = _impl->_plugin->AlwaysMainThread(); if (_impl->_plugin->_ext._state->save(_impl->_plugin->_plugin, chunk)) { NSData *clapState = [NSData dataWithBytes:chunk.data() length:chunk.size()]; @@ -1209,28 +1213,51 @@ - (BOOL)createGUIInView:(NSView *)parentView width:(uint32_t *)outWidth height:( return NO; } + // In out-of-process AUv3, _main_thread_id was captured on the XPC worker + // thread during init, so the CLAP proxy doesn't recognize the actual main + // thread. Override the thread identity for all GUI calls. + auto mainGuard = _impl->_plugin->AlwaysMainThread(); + auto *gui = _impl->_plugin->_ext._gui; auto *plugin = _impl->_plugin->_plugin; - if (!gui->is_api_supported(plugin, CLAP_WINDOW_API_COCOA, false)) return NO; + if (!gui->is_api_supported(plugin, CLAP_WINDOW_API_COCOA, false)) + { + AUV3LOG("createGUIInView: COCOA API not supported"); + return NO; + } + AUV3LOG("createGUIInView: COCOA API supported"); - if (!gui->create(plugin, CLAP_WINDOW_API_COCOA, false)) return NO; + if (!gui->create(plugin, CLAP_WINDOW_API_COCOA, false)) + { + AUV3LOG("createGUIInView: gui->create() failed"); + return NO; + } + AUV3LOG("createGUIInView: gui->create() succeeded"); gui->set_scale(plugin, 1.0); + AUV3LOG("createGUIInView: set_scale done"); uint32_t w = 0, h = 0; gui->get_size(plugin, &w, &h); + AUV3LOG("createGUIInView: get_size returned %ux%u", w, h); if (gui->can_resize(plugin)) { gui->adjust_size(plugin, &w, &h); + AUV3LOG("createGUIInView: adjust_size returned %ux%u", w, h); } clap_window_t window; window.api = CLAP_WINDOW_API_COCOA; window.cocoa = (__bridge void *)parentView; + AUV3LOG("createGUIInView: calling set_parent (parentView=%p, parentView.window=%p)", + parentView, parentView.window); gui->set_parent(plugin, &window); + AUV3LOG("createGUIInView: set_parent done"); + gui->show(plugin); + AUV3LOG("createGUIInView: show done, returning YES (size=%ux%u)", w, h); if (outWidth) *outWidth = w; if (outHeight) *outHeight = h; @@ -1250,6 +1277,8 @@ - (void)destroyGUI return; } + auto mainGuard = _impl->_plugin->AlwaysMainThread(); + AUV3LOG("destroyGUI: hiding and destroying GUI"); _impl->_plugin->_ext._gui->hide(_impl->_plugin->_plugin); _impl->_plugin->_ext._gui->destroy(_impl->_plugin->_plugin); @@ -1260,12 +1289,14 @@ - (void)destroyGUI - (BOOL)canResizeGUI { if (!_impl || !_impl->_plugin || !_impl->_plugin->_ext._gui) return NO; + auto mainGuard = _impl->_plugin->AlwaysMainThread(); return _impl->_plugin->_ext._gui->can_resize(_impl->_plugin->_plugin) ? YES : NO; } - (BOOL)setGUISize:(uint32_t)width height:(uint32_t)height { if (!_impl || !_impl->_plugin || !_impl->_plugin->_ext._gui) return NO; + auto mainGuard = _impl->_plugin->AlwaysMainThread(); return _impl->_plugin->_ext._gui->set_size(_impl->_plugin->_plugin, width, height) ? YES : NO; } @@ -1277,41 +1308,89 @@ - (BOOL)setGUISize:(uint32_t)width height:(uint32_t)height @end +// Forward-declare private method used by ClapAUv3ContainerView +@interface ClapAUv3ViewController () +- (void)_viewDidMoveToWindow; +@end + +// ----------------------------------------------------------------------- +// ClapAUv3ContainerView — custom NSView that notifies the VC when +// it enters or leaves a window. NSViewController lifecycle methods +// (viewDidAppear etc.) are unreliable when the host doesn't manage +// the VC hierarchy properly. viewDidMoveToWindow always fires. +// ----------------------------------------------------------------------- + +@interface ClapAUv3ContainerView : NSView +@property (nonatomic, weak) ClapAUv3ViewController *viewController; +@end + +@implementation ClapAUv3ContainerView + +- (void)viewDidMoveToWindow +{ + [super viewDidMoveToWindow]; + [self.viewController _viewDidMoveToWindow]; +} + +- (void)viewDidMoveToSuperview +{ + [super viewDidMoveToSuperview]; + // viewDidMoveToWindow only fires when the window changes. For LoadInProcess, + // the system puts the view in the host's window during factory creation. + // When the host later calls addSubview:, the window is the SAME, so + // viewDidMoveToWindow doesn't fire. viewDidMoveToSuperview fires in both cases. + if (self.superview && self.window) + { + [self.viewController _viewDidMoveToWindow]; + } +} + +@end + // ----------------------------------------------------------------------- // ClapAUv3ViewController implementation (also serves as AUAudioUnitFactory) // ----------------------------------------------------------------------- @implementation ClapAUv3ViewController +{ + BOOL _guiCreated; +} - (void)loadView { AUV3LOG("loadView: entered (thread=%{public}s)", [NSThread.currentThread.name UTF8String] ?: "unnamed"); - NSView *view = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 0, 0)]; - view.autoresizingMask = NSViewNotSizable; + // Use a custom container view that notifies us via viewDidMoveToWindow. + // NSViewController lifecycle methods (viewDidAppear, viewDidLayout) only fire + // when the VC is in the view controller hierarchy — many hosts just do + // addSubview: without addChildViewController:, so those methods never fire. + // viewDidMoveToWindow on NSView fires unconditionally. + ClapAUv3ContainerView *view = [[ClapAUv3ContainerView alloc] initWithFrame:NSMakeRect(0, 0, 0, 0)]; + view.viewController = self; view.translatesAutoresizingMaskIntoConstraints = YES; [self setView:view]; AUV3LOG("loadView: completed"); } -// Custom setter: trigger GUI creation when audioUnit is set and view is already loaded. -// This matches the VST3 SDK's setAudioUnit: → makePlugView pattern. - (void)setAudioUnit:(ClapAUv3AudioUnit *)audioUnit { - AUV3LOG("setAudioUnit: entered (audioUnit=%p, viewLoaded=%d, thread=%{public}s)", + AUV3LOG("setAudioUnit: entered (audioUnit=%p, viewLoaded=%d, window=%p, thread=%{public}s)", audioUnit, [self isViewLoaded], + [self isViewLoaded] ? self.view.window : nil, [NSThread.currentThread.name UTF8String] ?: "unnamed"); _audioUnit = audioUnit; - // Do NOT create the GUI here. The GUI is created lazily when the host - // explicitly shows the view (viewDidAppear / viewDidLayout). Creating it - // eagerly blocks the main thread (JUCE MessageManager init), which prevents - // the appex from processing subsequent XPC messages — causing auval WARM - // timeout (-10863) and similar hangs in headless hosts. + // Do NOT dispatch GUI creation here. dispatch_async fires during JUCE's + // nested run loop pump (during factory init), causing gui->create() to + // deadlock. GUI creation is triggered by: + // - viewDidAppear (out-of-process: system manages VC lifecycle) + // - viewDidMoveToSuperview (in-process: host calls addSubview:) + // - viewDidMoveToWindow (in-process: view enters host window) } - (void)_createPluginGUI { - AUV3LOG("_createPluginGUI: entered (audioUnit=%p)", self.audioUnit); + AUV3LOG("_createPluginGUI: entered (audioUnit=%p, isMainThread=%d)", self.audioUnit, + [NSThread isMainThread]); if (!self.audioUnit) { AUV3LOG("_createPluginGUI: no audioUnit set, skipping"); @@ -1334,29 +1413,87 @@ - (void)_createPluginGUI } } +// Convergence point: called when the view enters a window or audioUnit is set. +// Creates the GUI once all preconditions are met. +- (void)_tryCreateGUI +{ + AUV3LOG("_tryCreateGUI: entered (guiCreated=%d, audioUnit=%p, viewLoaded=%d, window=%p, isMainThread=%d)", + _guiCreated, self.audioUnit, self.isViewLoaded, + self.isViewLoaded ? self.view.window : nil, + [NSThread isMainThread]); + if (_guiCreated) return; + if (!self.audioUnit) return; + if (!self.isViewLoaded || !self.view.window) return; + + AUV3LOG("_tryCreateGUI: preconditions met, creating GUI"); + _guiCreated = YES; + [self _createPluginGUI]; +} + +// Called by ClapAUv3ContainerView.viewDidMoveToWindow when the view enters a window. +- (void)_viewDidMoveToWindow +{ + AUV3LOG("_viewDidMoveToWindow: window=%p, audioUnit=%p, isMainThread=%d", + self.view.window, self.audioUnit, [NSThread isMainThread]); + if (self.view.window) + { + AUV3LOG("_viewDidMoveToWindow: view entered window, scheduling GUI creation"); + // Defer to next run loop iteration — viewDidMoveToWindow fires synchronously + // during addSubview:, and JUCE plugins need the run loop to be processing + // events before their GUI can be created. + dispatch_async(dispatch_get_main_queue(), ^{ + [self _tryCreateGUI]; + }); + } + else + { + AUV3LOG("_viewDidMoveToWindow: view removed from window, destroying GUI"); + if (_guiCreated) + { + _guiCreated = NO; + [self.audioUnit destroyGUI]; + } + } +} + - (void)viewDidLoad { AUV3LOG("viewDidLoad: entered"); [super viewDidLoad]; - // Do NOT create the GUI here — defer to viewDidAppear so the CLAP GUI - // is only created when the host actually displays the view. AUV3LOG("viewDidLoad: completed"); } +// Out-of-process: the system manages the VC lifecycle properly (the extension +// is hosted via XPC), so viewDidAppear fires when the host displays the view. +// In-process: viewDidAppear doesn't fire (host doesn't use addChildViewController:), +// but viewDidMoveToSuperview on the container view handles that case. - (void)viewDidAppear { - AUV3LOG("viewDidAppear: entered (audioUnit=%p)", self.audioUnit); + AUV3LOG("viewDidAppear: entered (audioUnit=%p, isMainThread=%d)", + self.audioUnit, [NSThread isMainThread]); [super viewDidAppear]; - [self _createPluginGUI]; - AUV3LOG("viewDidAppear: completed"); + [self _tryCreateGUI]; } - (void)viewDidDisappear { AUV3LOG("viewDidDisappear: entered"); - [self.audioUnit destroyGUI]; + if (_guiCreated) + { + _guiCreated = NO; + [self.audioUnit destroyGUI]; + } [super viewDidDisappear]; - AUV3LOG("viewDidDisappear: completed"); +} + +- (void)dealloc +{ + AUV3LOG("ClapAUv3ViewController dealloc (guiCreated=%d)", _guiCreated); + if (_guiCreated) + { + [self.audioUnit destroyGUI]; + _guiCreated = NO; + } } // --- AUAudioUnitFactory --- diff --git a/src/detail/auv3/process.mm b/src/detail/auv3/process.mm index e8365e6a..0b4ec235 100644 --- a/src/detail/auv3/process.mm +++ b/src/detail/auv3/process.mm @@ -8,12 +8,12 @@ #include #include -static os_log_t _procLog() { - static os_log_t log = os_log_create("org.clap-wrapper.auv3", "process"); - return log; -} -#define PROCLOG(...) os_log(_procLog(), __VA_ARGS__) -#define PROCERR(...) os_log_error(_procLog(), __VA_ARGS__) +// static os_log_t _procLog() { +// static os_log_t log = os_log_create("org.clap-wrapper.auv3", "process"); +// return log; +// } +#define PROCLOG(...) // os_log(_procLog(), __VA_ARGS__) +#define PROCERR(...) // os_log_error(_procLog(), __VA_ARGS__) namespace Clap::AUv3 { diff --git a/src/detail/standalone/macos/auv3/AUv3HostAppDelegate.mm b/src/detail/standalone/macos/auv3/AUv3HostAppDelegate.mm index 5ea14a1d..daa04303 100644 --- a/src/detail/standalone/macos/auv3/AUv3HostAppDelegate.mm +++ b/src/detail/standalone/macos/auv3/AUv3HostAppDelegate.mm @@ -58,6 +58,16 @@ - (void)applicationWillTerminate:(NSNotification *)notification [self teardownMIDI]; [self saveState]; + // Remove KVO observer before tearing down + if (_auViewController) + { + @try { + [_auViewController removeObserver:self forKeyPath:@"preferredContentSize"]; + } @catch (NSException *e) { + // Observer was never added (e.g., no GUI path) + } + } + if (_engine) { [_engine stop]; @@ -423,6 +433,13 @@ - (void)setupGUI self->_auViewController = vc; + // Observe preferredContentSize — the AU view controller sets this + // asynchronously when the CLAP GUI is created (after viewDidMoveToWindow). + [vc addObserver:self + forKeyPath:@"preferredContentSize" + options:NSKeyValueObservingOptionNew + context:NULL]; + NSSize preferredSize = vc.preferredContentSize; if (preferredSize.width < 1 || preferredSize.height < 1) { @@ -461,6 +478,13 @@ - (void)setupGUIFromAUAudioUnit:(AUAudioUnit *)au dispatch_async(dispatch_get_main_queue(), ^{ self->_auViewController = vc; + // Observe preferredContentSize — the AU view controller sets this + // asynchronously when the CLAP GUI is created (after viewDidMoveToWindow). + [vc addObserver:self + forKeyPath:@"preferredContentSize" + options:NSKeyValueObservingOptionNew + context:NULL]; + NSSize preferredSize = vc.preferredContentSize; if (preferredSize.width < 1 || preferredSize.height < 1) { @@ -651,4 +675,29 @@ - (NSSize)windowWillResize:(NSWindow *)sender toSize:(NSSize)frameSize return frameSize; } +// --------------------------------------------------------------------------- +#pragma mark - KVO +// --------------------------------------------------------------------------- + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context +{ + if ([keyPath isEqualToString:@"preferredContentSize"] && object == _auViewController) + { + NSSize size = [(NSViewController *)object preferredContentSize]; + std::cout << "[auv3-standalone] preferredContentSize changed to " + << (int)size.width << "x" << (int)size.height << std::endl; + if (size.width > 0 && size.height > 0) + { + dispatch_async(dispatch_get_main_queue(), ^{ + [[self window] setContentSize:size]; + NSView *auView = self->_auViewController.view; + auView.frame = [[self window] contentView].bounds; + }); + } + } +} + @end From df3d9a2ef2657bc81570e55cf011cd42282effd0 Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Sun, 12 Apr 2026 00:03:26 +0200 Subject: [PATCH 06/23] a few updates Things are mostly working with clap-saw-demo, still, something is odd in UI which is not resizable and does not boot up with Surge --- src/detail/auv3/auv3.entitlements | 2 + src/detail/auv3/auv3_audiounit.mm | 150 +++++++----------- .../macos/auv3/AUv3HostAppDelegate.mm | 120 ++++++++++++-- 3 files changed, 162 insertions(+), 110 deletions(-) diff --git a/src/detail/auv3/auv3.entitlements b/src/detail/auv3/auv3.entitlements index ae79293e..147d2421 100644 --- a/src/detail/auv3/auv3.entitlements +++ b/src/detail/auv3/auv3.entitlements @@ -6,6 +6,8 @@ com.apple.security.network.client + com.apple.security.assets.music.read-write + com.apple.security.files.user-selected.read-write com.apple.security.temporary-exception.files.absolute-path.read-write diff --git a/src/detail/auv3/auv3_audiounit.mm b/src/detail/auv3/auv3_audiounit.mm index 99f0b2e0..0db934ed 100644 --- a/src/detail/auv3/auv3_audiounit.mm +++ b/src/detail/auv3/auv3_audiounit.mm @@ -73,6 +73,7 @@ - (void)_wireParameterObserver; AUV3LOG("~AUv3ImplDetail: destructor entered (plugin=%{public}s)", _plugin ? "valid" : "null"); if (_plugin) { + auto mainGuard = _plugin->AlwaysMainThread(); AUV3LOG("~AUv3ImplDetail: calling _plugin->terminate()"); _plugin->terminate(); AUV3LOG("~AUv3ImplDetail: calling _plugin.reset()"); @@ -126,6 +127,11 @@ - (void)_wireParameterObserver; std::unordered_map _paramValueCache; std::unordered_map _paramCookieCache; + // Cached latency in samples — queried on init and when the plugin calls + // latency_changed(). The AUv3 host reads the latency property from any + // thread, so we cache it to avoid calling into the plugin on the wrong thread. + uint32_t _cachedLatencySamples = 0; + // Queue for audio -> UI thread parameter notifications ClapWrapper::detail::shared::fixedqueue _queueToUI; @@ -358,7 +364,12 @@ void param_request_flush() override void latency_changed() override { - AUV3LOG("IHost::latency_changed() called"); + if (_plugin && _plugin->_ext._latency) + { + auto mainGuard = _plugin->AlwaysMainThread(); + _cachedLatencySamples = _plugin->_ext._latency->get(_plugin->_plugin); + AUV3LOG("IHost::latency_changed() -> %u samples", _cachedLatencySamples); + } } void tail_changed() override @@ -650,6 +661,14 @@ - (instancetype)initWithComponentDescription:(AudioComponentDescription)componen AUV3LOG("init: calling plugin->initialize()"); _impl->_plugin->initialize(); + + // Cache the initial latency so the AUv3 host can read it from any thread. + if (_impl->_plugin->_ext._latency) + { + _impl->_cachedLatencySamples = _impl->_plugin->_ext._latency->get(_impl->_plugin->_plugin); + AUV3LOG("init: initial latency = %u samples", _impl->_cachedLatencySamples); + } + // Start the idle timer on the main queue. This services request_callback() // (on_main_thread) between render cycles. We don't use the global os::attach // mechanism — its CFRunLoopTimer is unreliable in out-of-process AUv3. @@ -716,6 +735,7 @@ - (void)dealloc if (_impl->_plugin) { + auto mainGuard = _impl->_plugin->AlwaysMainThread(); AUV3LOG("dealloc: calling _plugin->terminate()"); _impl->_plugin->terminate(); AUV3LOG("dealloc: calling _plugin.reset()"); @@ -919,10 +939,13 @@ - (AUParameterTree *)parameterTree - (NSTimeInterval)latency { - if (_impl && _impl->_plugin && _impl->_plugin->_ext._latency) + // Return the cached latency — queried on init and updated when the plugin + // calls latency_changed(). Avoids calling into the plugin on the wrong thread. + if (_impl && _impl->_cachedLatencySamples > 0) { - uint32_t samples = _impl->_plugin->_ext._latency->get(_impl->_plugin->_plugin); - return (double)samples / self.outputBusses[0].format.sampleRate; + double sr = self.outputBusses[0].format.sampleRate; + if (sr > 0) + return (double)_impl->_cachedLatencySamples / sr; } return 0; } @@ -1206,12 +1229,7 @@ - (AUInternalRenderBlock)internalRenderBlock - (BOOL)createGUIInView:(NSView *)parentView width:(uint32_t *)outWidth height:(uint32_t *)outHeight { - AUV3LOG("createGUIInView: entered (parentView=%p)", parentView); - if (!_impl || !_impl->_plugin || !_impl->_plugin->_ext._gui) - { - AUV3LOG("createGUIInView: no GUI extension available"); - return NO; - } + if (!_impl || !_impl->_plugin || !_impl->_plugin->_ext._gui) return NO; // In out-of-process AUv3, _main_thread_id was captured on the XPC worker // thread during init, so the CLAP proxy doesn't recognize the actual main @@ -1221,43 +1239,25 @@ - (BOOL)createGUIInView:(NSView *)parentView width:(uint32_t *)outWidth height:( auto *gui = _impl->_plugin->_ext._gui; auto *plugin = _impl->_plugin->_plugin; - if (!gui->is_api_supported(plugin, CLAP_WINDOW_API_COCOA, false)) - { - AUV3LOG("createGUIInView: COCOA API not supported"); - return NO; - } - AUV3LOG("createGUIInView: COCOA API supported"); + if (!gui->is_api_supported(plugin, CLAP_WINDOW_API_COCOA, false)) return NO; - if (!gui->create(plugin, CLAP_WINDOW_API_COCOA, false)) - { - AUV3LOG("createGUIInView: gui->create() failed"); - return NO; - } - AUV3LOG("createGUIInView: gui->create() succeeded"); + if (!gui->create(plugin, CLAP_WINDOW_API_COCOA, false)) return NO; gui->set_scale(plugin, 1.0); - AUV3LOG("createGUIInView: set_scale done"); uint32_t w = 0, h = 0; gui->get_size(plugin, &w, &h); - AUV3LOG("createGUIInView: get_size returned %ux%u", w, h); if (gui->can_resize(plugin)) { gui->adjust_size(plugin, &w, &h); - AUV3LOG("createGUIInView: adjust_size returned %ux%u", w, h); } clap_window_t window; window.api = CLAP_WINDOW_API_COCOA; window.cocoa = (__bridge void *)parentView; - AUV3LOG("createGUIInView: calling set_parent (parentView=%p, parentView.window=%p)", - parentView, parentView.window); gui->set_parent(plugin, &window); - AUV3LOG("createGUIInView: set_parent done"); - gui->show(plugin); - AUV3LOG("createGUIInView: show done, returning YES (size=%ux%u)", w, h); if (outWidth) *outWidth = w; if (outHeight) *outHeight = h; @@ -1270,20 +1270,12 @@ - (BOOL)createGUIInView:(NSView *)parentView width:(uint32_t *)outWidth height:( - (void)destroyGUI { - AUV3LOG("destroyGUI: entered"); - if (!_impl || !_impl->_plugin || !_impl->_plugin->_ext._gui) - { - AUV3LOG("destroyGUI: no GUI extension, nothing to destroy"); - return; - } + if (!_impl || !_impl->_plugin || !_impl->_plugin->_ext._gui) return; auto mainGuard = _impl->_plugin->AlwaysMainThread(); - - AUV3LOG("destroyGUI: hiding and destroying GUI"); _impl->_plugin->_ext._gui->hide(_impl->_plugin->_plugin); _impl->_plugin->_ext._gui->destroy(_impl->_plugin->_plugin); _impl->_guiParentView = nil; - AUV3LOG("destroyGUI: completed"); } - (BOOL)canResizeGUI @@ -1326,6 +1318,12 @@ @interface ClapAUv3ContainerView : NSView @implementation ClapAUv3ContainerView +- (BOOL)isFlipped +{ + // Plugin GUIs expect (0,0) at top-left (flipped coordinate system). + return YES; +} + - (void)viewDidMoveToWindow { [super viewDidMoveToWindow]; @@ -1358,96 +1356,67 @@ @implementation ClapAUv3ViewController - (void)loadView { - AUV3LOG("loadView: entered (thread=%{public}s)", - [NSThread.currentThread.name UTF8String] ?: "unnamed"); - // Use a custom container view that notifies us via viewDidMoveToWindow. - // NSViewController lifecycle methods (viewDidAppear, viewDidLayout) only fire - // when the VC is in the view controller hierarchy — many hosts just do - // addSubview: without addChildViewController:, so those methods never fire. - // viewDidMoveToWindow on NSView fires unconditionally. + // Custom container view that detects when the view enters a window + // via viewDidMoveToWindow / viewDidMoveToSuperview. NSViewController + // lifecycle methods (viewDidAppear etc.) only fire when the VC is in + // the view controller hierarchy — many hosts just call addSubview:. ClapAUv3ContainerView *view = [[ClapAUv3ContainerView alloc] initWithFrame:NSMakeRect(0, 0, 0, 0)]; view.viewController = self; view.translatesAutoresizingMaskIntoConstraints = YES; [self setView:view]; - AUV3LOG("loadView: completed"); } - (void)setAudioUnit:(ClapAUv3AudioUnit *)audioUnit { - AUV3LOG("setAudioUnit: entered (audioUnit=%p, viewLoaded=%d, window=%p, thread=%{public}s)", - audioUnit, [self isViewLoaded], - [self isViewLoaded] ? self.view.window : nil, - [NSThread.currentThread.name UTF8String] ?: "unnamed"); _audioUnit = audioUnit; - // Do NOT dispatch GUI creation here. dispatch_async fires during JUCE's - // nested run loop pump (during factory init), causing gui->create() to - // deadlock. GUI creation is triggered by: - // - viewDidAppear (out-of-process: system manages VC lifecycle) - // - viewDidMoveToSuperview (in-process: host calls addSubview:) - // - viewDidMoveToWindow (in-process: view enters host window) } - (void)_createPluginGUI { - AUV3LOG("_createPluginGUI: entered (audioUnit=%p, isMainThread=%d)", self.audioUnit, - [NSThread isMainThread]); - if (!self.audioUnit) - { - AUV3LOG("_createPluginGUI: no audioUnit set, skipping"); - return; - } + if (!self.audioUnit) return; uint32_t w = 0, h = 0; if ([self.audioUnit createGUIInView:self.view width:&w height:&h]) { - AUV3LOG("_createPluginGUI: GUI created, size=%ux%u", w, h); + AUV3LOG("GUI created, size=%ux%u", w, h); if (w > 0 && h > 0) { + // Explicit KVO notifications — required for the remote proxy to + // forward preferredContentSize changes across the XPC boundary + // to the host process. + [self willChangeValueForKey:@"preferredContentSize"]; self.preferredContentSize = NSMakeSize(w, h); + [self didChangeValueForKey:@"preferredContentSize"]; self.view.frame = NSMakeRect(0, 0, w, h); } } - else - { - AUV3LOG("_createPluginGUI: createGUIInView returned NO"); - } } -// Convergence point: called when the view enters a window or audioUnit is set. -// Creates the GUI once all preconditions are met. +// Convergence point for GUI creation. Called from multiple triggers: +// - viewDidMoveToWindow / viewDidMoveToSuperview (in-process) +// - viewDidAppear (out-of-process) +// Creates the GUI once all preconditions are met. Guarded by _guiCreated. - (void)_tryCreateGUI { - AUV3LOG("_tryCreateGUI: entered (guiCreated=%d, audioUnit=%p, viewLoaded=%d, window=%p, isMainThread=%d)", - _guiCreated, self.audioUnit, self.isViewLoaded, - self.isViewLoaded ? self.view.window : nil, - [NSThread isMainThread]); if (_guiCreated) return; if (!self.audioUnit) return; if (!self.isViewLoaded || !self.view.window) return; - AUV3LOG("_tryCreateGUI: preconditions met, creating GUI"); _guiCreated = YES; [self _createPluginGUI]; } -// Called by ClapAUv3ContainerView.viewDidMoveToWindow when the view enters a window. +// Called by ClapAUv3ContainerView when the view enters or leaves a window. - (void)_viewDidMoveToWindow { - AUV3LOG("_viewDidMoveToWindow: window=%p, audioUnit=%p, isMainThread=%d", - self.view.window, self.audioUnit, [NSThread isMainThread]); if (self.view.window) { - AUV3LOG("_viewDidMoveToWindow: view entered window, scheduling GUI creation"); - // Defer to next run loop iteration — viewDidMoveToWindow fires synchronously - // during addSubview:, and JUCE plugins need the run loop to be processing - // events before their GUI can be created. dispatch_async(dispatch_get_main_queue(), ^{ [self _tryCreateGUI]; }); } else { - AUV3LOG("_viewDidMoveToWindow: view removed from window, destroying GUI"); if (_guiCreated) { _guiCreated = NO; @@ -1458,26 +1427,20 @@ - (void)_viewDidMoveToWindow - (void)viewDidLoad { - AUV3LOG("viewDidLoad: entered"); [super viewDidLoad]; - AUV3LOG("viewDidLoad: completed"); } -// Out-of-process: the system manages the VC lifecycle properly (the extension -// is hosted via XPC), so viewDidAppear fires when the host displays the view. -// In-process: viewDidAppear doesn't fire (host doesn't use addChildViewController:), -// but viewDidMoveToSuperview on the container view handles that case. +// Out-of-process: the system manages the VC lifecycle properly, so +// viewDidAppear fires when the host displays the view. +// In-process: viewDidMoveToSuperview on the container view handles it. - (void)viewDidAppear { - AUV3LOG("viewDidAppear: entered (audioUnit=%p, isMainThread=%d)", - self.audioUnit, [NSThread isMainThread]); [super viewDidAppear]; [self _tryCreateGUI]; } - (void)viewDidDisappear { - AUV3LOG("viewDidDisappear: entered"); if (_guiCreated) { _guiCreated = NO; @@ -1488,7 +1451,6 @@ - (void)viewDidDisappear - (void)dealloc { - AUV3LOG("ClapAUv3ViewController dealloc (guiCreated=%d)", _guiCreated); if (_guiCreated) { [self.audioUnit destroyGUI]; diff --git a/src/detail/standalone/macos/auv3/AUv3HostAppDelegate.mm b/src/detail/standalone/macos/auv3/AUv3HostAppDelegate.mm index daa04303..2ad9eb9f 100644 --- a/src/detail/standalone/macos/auv3/AUv3HostAppDelegate.mm +++ b/src/detail/standalone/macos/auv3/AUv3HostAppDelegate.mm @@ -247,7 +247,7 @@ - (void)doSetup << "' subtype='" << AU_SUBTYPE_STR << "' manufacturer='" << AU_MANUFACTURER_STR << "'" << std::endl; - // First try: check if the AU is already registered with the system + // First try: use the system-registered AU (via AVAudioUnit + AVAudioEngine) AudioComponent comp = AudioComponentFindNext(NULL, &desc); if (comp) { @@ -257,11 +257,10 @@ - (void)doSetup << (compName ? [(__bridge NSString *)compName UTF8String] : "?") << std::endl; if (compName) CFRelease(compName); - // Use normal AVAudioUnit instantiation path __weak typeof(self) weakSelf = self; [AVAudioUnit instantiateWithComponentDescription:desc options:kAudioComponentInstantiation_LoadInProcess - completionHandler:^(AVAudioUnit *_Nullable audioUnit, NSError *_Nullable error) { + completionHandler:^(AVAudioUnit *_Nullable audioUnit, NSError *_Nullable error) { dispatch_async(dispatch_get_main_queue(), ^{ __strong typeof(weakSelf) self = weakSelf; if (self) [self finishSetupWithAudioUnit:audioUnit error:error]; @@ -270,7 +269,7 @@ - (void)doSetup return; } - // Second path: load the appex bundle directly and instantiate through the factory + // Fallback: load the appex bundle directly (in-process, no AVAudioEngine) std::cout << "[auv3-standalone] AudioComponent not registered, loading appex directly" << std::endl; NSBundle *appexBundle = [self findEmbeddedAppexBundle]; @@ -446,19 +445,33 @@ - (void)setupGUI preferredSize = NSMakeSize(480, 360); } - [[self window] setContentSize:preferredSize]; - [[self window] setDelegate:self]; + NSWindow *window = [self window]; + [window setContentSize:preferredSize]; + [window setDelegate:self]; + window.styleMask |= NSWindowStyleMaskResizable; + window.contentMinSize = NSMakeSize(100, 100); - NSView *contentView = [[self window] contentView]; + NSView *contentView = [window contentView]; NSView *auView = vc.view; auView.frame = contentView.bounds; auView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; [contentView addSubview:auView]; - [[self window] orderFrontRegardless]; + [window setMovableByWindowBackground:YES]; + [window orderFrontRegardless]; + + std::cout << "[auv3-standalone] window styleMask=0x" + << std::hex << (unsigned long)window.styleMask << std::dec + << " resizable=" << ((window.styleMask & NSWindowStyleMaskResizable) ? "YES" : "NO") + << std::endl; std::cout << "[auv3-standalone] GUI displayed (" << (int)preferredSize.width << "x" << (int)preferredSize.height << ")" << std::endl; + + // For out-of-process AUv3, KVO on preferredContentSize may not work + // across the XPC boundary. Poll after a delay to pick up the plugin's + // actual GUI size once it has been created (via viewDidAppear). + [self _pollPreferredContentSize:vc retries:10]; }); }]; } @@ -491,19 +504,27 @@ - (void)setupGUIFromAUAudioUnit:(AUAudioUnit *)au preferredSize = NSMakeSize(480, 360); } - [[self window] setContentSize:preferredSize]; - [[self window] setDelegate:self]; + NSWindow *window = [self window]; + [window setContentSize:preferredSize]; + [window setDelegate:self]; + // Ensure the window is resizable + window.styleMask |= NSWindowStyleMaskResizable; - NSView *contentView = [[self window] contentView]; + NSView *contentView = [window contentView]; NSView *auView = vc.view; auView.frame = contentView.bounds; - auView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; + // Do NOT set autoresizingMask — it fights with explicit frame changes + // from the plugin (which sets self.view.frame to the GUI size). + // Window sizing is managed explicitly via _resizeWindowToFitGUI. + auView.autoresizingMask = 0; [contentView addSubview:auView]; - [[self window] orderFrontRegardless]; + [window orderFrontRegardless]; std::cout << "[auv3-standalone] GUI displayed (direct) (" << (int)preferredSize.width << "x" << (int)preferredSize.height << ")" << std::endl; + + [self _pollPreferredContentSize:vc retries:10]; }); } @@ -675,6 +696,75 @@ - (NSSize)windowWillResize:(NSWindow *)sender toSize:(NSSize)frameSize return frameSize; } +// --------------------------------------------------------------------------- +#pragma mark - GUI size tracking +// --------------------------------------------------------------------------- + +- (void)_resizeWindowToFitGUI:(NSViewController *)vc +{ + NSSize size = vc.preferredContentSize; + NSWindow *window = [self window]; + if (size.width <= 0 || size.height <= 0 || !window) return; + + // Resize keeping top-left corner fixed + NSRect oldFrame = window.frame; + NSRect contentRect = NSMakeRect(0, 0, size.width, size.height); + NSRect newFrame = [window frameRectForContentRect:contentRect]; + newFrame.origin.x = oldFrame.origin.x; + newFrame.origin.y = oldFrame.origin.y + oldFrame.size.height - newFrame.size.height; + + [window setFrame:newFrame display:YES animate:NO]; + + vc.view.frame = NSMakeRect(0, 0, size.width, size.height); + [vc.view setNeedsDisplay:YES]; + [[window contentView] setNeedsDisplay:YES]; +} + +- (void)_pollPreferredContentSize:(NSViewController *)vc retries:(int)retries +{ + if (retries <= 0) + { + // Last resort: re-request the VC from the AU to get a fresh proxy + // with updated preferredContentSize. + if (_avAudioUnit) + { + [_avAudioUnit.AUAudioUnit requestViewControllerWithCompletionHandler:^(AUViewControllerBase *freshVC) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (freshVC) + { + NSSize size = freshVC.preferredContentSize; + std::cout << "[auv3-standalone] Re-requested VC preferredContentSize: " + << (int)size.width << "x" << (int)size.height << std::endl; + if (size.width > 0 && size.height > 0) + { + self->_auViewController.preferredContentSize = size; + [self _resizeWindowToFitGUI:self->_auViewController]; + } + } + }); + }]; + } + return; + } + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(500 * NSEC_PER_MSEC)), + dispatch_get_main_queue(), ^{ + NSSize size = vc.preferredContentSize; + NSSize windowContent = [[self window] contentView].frame.size; + + if (size.width > 0 && size.height > 0 && + ((int)size.width != (int)windowContent.width || + (int)size.height != (int)windowContent.height)) + { + [self _resizeWindowToFitGUI:vc]; + } + else + { + [self _pollPreferredContentSize:vc retries:retries - 1]; + } + }); +} + // --------------------------------------------------------------------------- #pragma mark - KVO // --------------------------------------------------------------------------- @@ -692,9 +782,7 @@ - (void)observeValueForKeyPath:(NSString *)keyPath if (size.width > 0 && size.height > 0) { dispatch_async(dispatch_get_main_queue(), ^{ - [[self window] setContentSize:size]; - NSView *auView = self->_auViewController.view; - auView.frame = [[self window] contentView].bounds; + [self _resizeWindowToFitGUI:self->_auViewController]; }); } } From 560b86229c41c931e64aba4b1f17580e6a8d47d5 Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:23:11 +0200 Subject: [PATCH 07/23] create distinct class --- src/detail/auv3/auv3_audiounit.mm | 9 ++++++- src/detail/auv3/build-helper/build-helper.cpp | 25 ++++++++++++++++--- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/detail/auv3/auv3_audiounit.mm b/src/detail/auv3/auv3_audiounit.mm index 0db934ed..292fe6a0 100644 --- a/src/detail/auv3/auv3_audiounit.mm +++ b/src/detail/auv3/auv3_audiounit.mm @@ -1384,10 +1384,11 @@ - (void)_createPluginGUI // Explicit KVO notifications — required for the remote proxy to // forward preferredContentSize changes across the XPC boundary // to the host process. + self.view.frame = NSMakeRect(0, 0, w, h); [self willChangeValueForKey:@"preferredContentSize"]; self.preferredContentSize = NSMakeSize(w, h); [self didChangeValueForKey:@"preferredContentSize"]; - self.view.frame = NSMakeRect(0, 0, w, h); + } } } @@ -1428,6 +1429,12 @@ - (void)_viewDidMoveToWindow - (void)viewDidLoad { [super viewDidLoad]; + // Try to create the GUI early so preferredContentSize is set BEFORE + // the host reads it via requestViewControllerWithCompletionHandler:. + // For out-of-process AUv3, the proxy doesn't forward property changes, + // so the host only sees the value that was set at VC creation time. + // If gui->create() blocks (JUCE plugins), viewDidAppear handles it later. + [self _tryCreateGUI]; } // Out-of-process: the system manages the VC lifecycle properly, so diff --git a/src/detail/auv3/build-helper/build-helper.cpp b/src/detail/auv3/build-helper/build-helper.cpp index 29b08ef1..9d381c58 100644 --- a/src/detail/auv3/build-helper/build-helper.cpp +++ b/src/detail/auv3/build-helper/build-helper.cpp @@ -36,13 +36,30 @@ static std::string fourCCFromString(const std::string &input) return result; } +// Generate a valid ObjC identifier suffix from an arbitrary string. +// Uses hex encoding of fnv1a hash to produce a unique, deterministic suffix. +static std::string objcIdentifierFromString(const std::string &input) +{ + uint32_t hash = fnv1a_keogh(input.c_str()); + char buf[16]; + snprintf(buf, sizeof(buf), "%08X", hash); + return buf; +} + struct auInfo { std::string name, vers, type, subt, manu, manunm, clapid, desc, clapname, bundlevers; bool explicitMode{false}; std::vector tags; - const std::string factoryBase{"ClapAUv3ViewController_inst"}; + // Each plugin needs a unique ObjC class name to avoid collisions when + // multiple AUv3 wrappers are loaded in the same process. + std::string factoryBase() const + { + std::string key = manu + subt; + if (!clapid.empty()) key = clapid; + return "ClapAUv3VC_" + objcIdentifierFromString(key); + } uint32_t bundleversToVersion() const { @@ -83,7 +100,7 @@ struct auInfo << " description\n" << " " << desc << "\n" << " factoryFunction\n" - << " " << factoryBase << idx << "\n" + << " " << factoryBase() << idx << "\n" << " manufacturer\n" << " " << manu << "\n" << " subtype\n" @@ -364,7 +381,7 @@ int main(int argc, char **argv) of << intop.rdbuf(); // The principal class is the first factory subclass - std::string principalClass = units[0].factoryBase + "0"; + std::string principalClass = units[0].factoryBase() + "0"; of << " NSExtension\n" << " \n" @@ -407,7 +424,7 @@ int main(int argc, char **argv) idx = 0; for (const auto &u : units) { - auto vcName = u.factoryBase + std::to_string(idx); + auto vcName = u.factoryBase() + std::to_string(idx); std::cout << " + " << u.name << " view controller " << vcName << std::endl; From 70ca321045cdfa365e3d71238f6e09928b7104c5 Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Sun, 12 Apr 2026 21:23:01 +0200 Subject: [PATCH 08/23] UI is showing up --- src/detail/auv3/auv3_audiounit.h | 12 ++++ src/detail/auv3/auv3_audiounit.mm | 113 ++++++++++++++++++++++++------ 2 files changed, 102 insertions(+), 23 deletions(-) diff --git a/src/detail/auv3/auv3_audiounit.h b/src/detail/auv3/auv3_audiounit.h index efdcbade..f0b052fd 100644 --- a/src/detail/auv3/auv3_audiounit.h +++ b/src/detail/auv3/auv3_audiounit.h @@ -34,6 +34,14 @@ @end @interface ClapAUv3AudioUnit : AUAudioUnit +{ + @package + // Weak back-reference to the factory VC that created this AU. + // Set by [ClapAUv3ViewController setAudioUnit:] during factory creation. + // Used by requestViewControllerWithCompletionHandler: so the host can + // obtain the VC for displaying the plugin's custom UI. + __weak ClapAUv3ViewController *_factoryViewController; +} - (instancetype)initWithComponentDescription:(AudioComponentDescription)componentDescription options:(AudioComponentInstantiationOptions)options @@ -55,4 +63,8 @@ // Called by the view controller when the host resizes the view. - (BOOL)setGUISize:(uint32_t)width height:(uint32_t)height; +// Called by the view controller to establish the back-reference needed +// for gui_request_resize to set preferredContentSize on the VC. +- (void)setViewController:(ClapAUv3ViewController *)vc; + @end diff --git a/src/detail/auv3/auv3_audiounit.mm b/src/detail/auv3/auv3_audiounit.mm index 292fe6a0..c63da01f 100644 --- a/src/detail/auv3/auv3_audiounit.mm +++ b/src/detail/auv3/auv3_audiounit.mm @@ -120,6 +120,10 @@ - (void)_wireParameterObserver; // The NSView that the CLAP GUI is parented to (set by createGUIInView:) __weak NSView *_guiParentView = nil; + // The view controller that owns the GUI — needed for gui_request_resize + // to set preferredContentSize (the only legal AUv3 host communication path). + __weak ClapAUv3ViewController *_viewController = nil; + // Cached parameter values — avoids calling params->get_value() on every // provider callback (wrong thread, expensive via XPC). Updated on set/flush/process. // Reads from XPC thread, writes from XPC + audio thread; aligned double is @@ -389,23 +393,20 @@ bool gui_can_resize() override bool gui_request_resize(uint32_t width, uint32_t height) override { - // Notify the host that the plugin wants to resize - if (_guiParentView) - { - dispatch_async(dispatch_get_main_queue(), ^{ - NSView *view = _guiParentView; - if (view) - { - NSWindow *window = view.window; - if (window) - { - [window setContentSize:NSMakeSize(width, height)]; - } - } - }); - return true; - } - return false; + // Communicate size changes through the AUv3 protocol: set preferredContentSize + // on the view controller. The host decides the final size. + dispatch_async(dispatch_get_main_queue(), ^{ + __strong ClapAUv3ViewController *vc = _viewController; + if (vc) + { + vc.view.frame = NSMakeRect(0, 0, width, height); + + [vc willChangeValueForKey:@"preferredContentSize"]; + vc.preferredContentSize = NSMakeSize(width, height); + [vc didChangeValueForKey:@"preferredContentSize"]; + } + }); + return true; } bool gui_request_show() override { return false; } @@ -1253,6 +1254,14 @@ - (BOOL)createGUIInView:(NSView *)parentView width:(uint32_t *)outWidth height:( gui->adjust_size(plugin, &w, &h); } + // Confirm the size to the plugin (matches VST3/AUv2 pattern). + gui->set_size(plugin, w, h); + + // Resize the parent view BEFORE set_parent() so the CLAP plugin's + // subview is created inside a properly-sized container. Without this + // the container is 0x0 and plugins that clip to parent bounds are invisible. + [parentView setFrame:NSMakeRect(0, 0, w, h)]; + clap_window_t window; window.api = CLAP_WINDOW_API_COCOA; window.cocoa = (__bridge void *)parentView; @@ -1276,6 +1285,7 @@ - (void)destroyGUI _impl->_plugin->_ext._gui->hide(_impl->_plugin->_plugin); _impl->_plugin->_ext._gui->destroy(_impl->_plugin->_plugin); _impl->_guiParentView = nil; + _impl->_viewController = nil; } - (BOOL)canResizeGUI @@ -1292,11 +1302,29 @@ - (BOOL)setGUISize:(uint32_t)width height:(uint32_t)height return _impl->_plugin->_ext._gui->set_size(_impl->_plugin->_plugin, width, height) ? YES : NO; } +- (void)setViewController:(ClapAUv3ViewController *)vc +{ + if (_impl) _impl->_viewController = vc; +} + // --- View controller --- -// requestViewControllerWithCompletionHandler: is NOT overridden. -// The default AUAudioUnit implementation returns the NSExtensionPrincipalClass -// view controller (the factory VC that created this AU). This is the same -// pattern used by the VST3 SDK's AUv3 wrapper. +// Override requestViewControllerWithCompletionHandler: to return the factory VC. +// The default AUAudioUnit implementation returns nil. The extension infrastructure +// may handle this automatically in some contexts, but explicitly returning the VC +// ensures the host can always obtain it (both in-process and out-of-process). + +- (void)requestViewControllerWithCompletionHandler:(void (^)(AUViewControllerBase * __nullable))completionHandler +{ + AUV3LOG("requestViewControllerWithCompletionHandler: called (factoryVC=%p)", _factoryViewController); + completionHandler(_factoryViewController); +} + +// Tell the host this AU has a custom view. Without this, some hosts +// (Logic Pro) may never offer the "Custom" view option. +- (BOOL)providesUserInterface +{ + return (_impl && _impl->_plugin && _impl->_plugin->_ext._gui) ? YES : NO; +} @end @@ -1360,21 +1388,32 @@ - (void)loadView // via viewDidMoveToWindow / viewDidMoveToSuperview. NSViewController // lifecycle methods (viewDidAppear etc.) only fire when the VC is in // the view controller hierarchy — many hosts just call addSubview:. - ClapAUv3ContainerView *view = [[ClapAUv3ContainerView alloc] initWithFrame:NSMakeRect(0, 0, 0, 0)]; + // Use a reasonable default size rather than 0x0. The viewbridge infrastructure + // may reject a zero-sized view, preventing the host from displaying custom UI. + // The actual size is updated once the CLAP GUI is created (_createPluginGUI). + ClapAUv3ContainerView *view = [[ClapAUv3ContainerView alloc] initWithFrame:NSMakeRect(0, 0, 400, 300)]; view.viewController = self; view.translatesAutoresizingMaskIntoConstraints = YES; [self setView:view]; + self.preferredContentSize = NSMakeSize(400, 300); } - (void)setAudioUnit:(ClapAUv3AudioUnit *)audioUnit { _audioUnit = audioUnit; + // Establish the back-reference so the AU can return us from + // requestViewControllerWithCompletionHandler: + if (audioUnit) + audioUnit->_factoryViewController = self; } - (void)_createPluginGUI { if (!self.audioUnit) return; + // Establish the back-reference so gui_request_resize can reach this VC + [self.audioUnit setViewController:self]; + uint32_t w = 0, h = 0; if ([self.audioUnit createGUIInView:self.view width:&w height:&h]) { @@ -1446,6 +1485,29 @@ - (void)viewDidAppear [self _tryCreateGUI]; } +- (void)viewDidLayout +{ + [super viewDidLayout]; + if (!_guiCreated) return; + + NSRect bounds = self.view.bounds; + if (bounds.size.width > 0 && bounds.size.height > 0) + { + // Propagate host-initiated container resize to the CLAP plugin + if ([self.audioUnit canResizeGUI]) + { + [self.audioUnit setGUISize:(uint32_t)bounds.size.width + height:(uint32_t)bounds.size.height]; + } + + // Ensure the CLAP plugin's subview fills the container + for (NSView *subview in self.view.subviews) + { + subview.frame = bounds; + } + } +} + - (void)viewDidDisappear { if (_guiCreated) @@ -1481,8 +1543,13 @@ - (AUAudioUnit *)createAudioUnitWithComponentDescription:(AudioComponentDescript - (void)beginRequestWithExtensionContext:(NSExtensionContext *)context { AUV3LOG("beginRequestWithExtensionContext: entered (context=%p)", context); + // MUST call super — AUViewController uses this to set up the view bridge + // service. Without it the host never receives the view controller and + // the plugin's custom UI cannot be displayed. + [super beginRequestWithExtensionContext:context]; + AUV3LOG("beginRequestWithExtensionContext: leaving (context=%p)", context); } @end -#pragma clang diagnostic pop +// #pragma clang diagnostic pop From 375865e2dfa12270ad1ea0533b7ca527d28b1156 Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Sun, 12 Apr 2026 21:44:46 +0200 Subject: [PATCH 09/23] having preferred size be initial size --- src/detail/auv3/auv3_audiounit.h | 4 ++++ src/detail/auv3/auv3_audiounit.mm | 40 +++++++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/detail/auv3/auv3_audiounit.h b/src/detail/auv3/auv3_audiounit.h index f0b052fd..8d10401d 100644 --- a/src/detail/auv3/auv3_audiounit.h +++ b/src/detail/auv3/auv3_audiounit.h @@ -67,4 +67,8 @@ // for gui_request_resize to set preferredContentSize on the VC. - (void)setViewController:(ClapAUv3ViewController *)vc; +// Queries the CLAP plugin for its preferred GUI size without attaching to a view. +// Returns YES and fills outWidth/outHeight if the plugin has a GUI, NO otherwise. +- (BOOL)queryPreferredGUISize:(uint32_t *)outWidth height:(uint32_t *)outHeight; + @end diff --git a/src/detail/auv3/auv3_audiounit.mm b/src/detail/auv3/auv3_audiounit.mm index c63da01f..8c74cef2 100644 --- a/src/detail/auv3/auv3_audiounit.mm +++ b/src/detail/auv3/auv3_audiounit.mm @@ -1307,6 +1307,26 @@ - (void)setViewController:(ClapAUv3ViewController *)vc if (_impl) _impl->_viewController = vc; } +- (BOOL)queryPreferredGUISize:(uint32_t *)outWidth height:(uint32_t *)outHeight +{ + if (!_impl || !_impl->_plugin || !_impl->_plugin->_ext._gui) return NO; + + auto mainGuard = _impl->_plugin->AlwaysMainThread(); + auto *gui = _impl->_plugin->_ext._gui; + auto *plugin = _impl->_plugin->_plugin; + + if (!gui->is_api_supported(plugin, CLAP_WINDOW_API_COCOA, false)) return NO; + if (!gui->create(plugin, CLAP_WINDOW_API_COCOA, false)) return NO; + + uint32_t w = 0, h = 0; + gui->get_size(plugin, &w, &h); + gui->destroy(plugin); + + if (outWidth) *outWidth = w; + if (outHeight) *outHeight = h; + return (w > 0 && h > 0) ? YES : NO; +} + // --- View controller --- // Override requestViewControllerWithCompletionHandler: to return the factory VC. // The default AUAudioUnit implementation returns nil. The extension infrastructure @@ -1388,14 +1408,14 @@ - (void)loadView // via viewDidMoveToWindow / viewDidMoveToSuperview. NSViewController // lifecycle methods (viewDidAppear etc.) only fire when the VC is in // the view controller hierarchy — many hosts just call addSubview:. - // Use a reasonable default size rather than 0x0. The viewbridge infrastructure - // may reject a zero-sized view, preventing the host from displaying custom UI. - // The actual size is updated once the CLAP GUI is created (_createPluginGUI). - ClapAUv3ContainerView *view = [[ClapAUv3ContainerView alloc] initWithFrame:NSMakeRect(0, 0, 400, 300)]; + // Start with a reasonable default size. The viewbridge rejects zero-sized views. + // The actual size is updated from the CLAP plugin in setAudioUnit: / _createPluginGUI. + NSSize initialSize = NSMakeSize(400, 300); + ClapAUv3ContainerView *view = [[ClapAUv3ContainerView alloc] initWithFrame:NSMakeRect(0, 0, initialSize.width, initialSize.height)]; view.viewController = self; view.translatesAutoresizingMaskIntoConstraints = YES; [self setView:view]; - self.preferredContentSize = NSMakeSize(400, 300); + self.preferredContentSize = initialSize; } - (void)setAudioUnit:(ClapAUv3AudioUnit *)audioUnit @@ -1405,6 +1425,16 @@ - (void)setAudioUnit:(ClapAUv3AudioUnit *)audioUnit // requestViewControllerWithCompletionHandler: if (audioUnit) audioUnit->_factoryViewController = self; + + // Query the CLAP plugin for its preferred GUI size so the host sees + // the correct dimensions before the GUI is actually created. + uint32_t w = 0, h = 0; + if (audioUnit && [audioUnit queryPreferredGUISize:&w height:&h]) + { + if (self.isViewLoaded) + self.view.frame = NSMakeRect(0, 0, w, h); + self.preferredContentSize = NSMakeSize(w, h); + } } - (void)_createPluginGUI From 9c9a58cae8b71dbcec437539baf191325074868c Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Sun, 12 Apr 2026 22:40:32 +0200 Subject: [PATCH 10/23] automation working, too --- src/detail/auv3/auv3_audiounit.h | 3 - src/detail/auv3/auv3_audiounit.mm | 131 +++++++++++++++++++----------- 2 files changed, 83 insertions(+), 51 deletions(-) diff --git a/src/detail/auv3/auv3_audiounit.h b/src/detail/auv3/auv3_audiounit.h index 8d10401d..f7f562b7 100644 --- a/src/detail/auv3/auv3_audiounit.h +++ b/src/detail/auv3/auv3_audiounit.h @@ -67,8 +67,5 @@ // for gui_request_resize to set preferredContentSize on the VC. - (void)setViewController:(ClapAUv3ViewController *)vc; -// Queries the CLAP plugin for its preferred GUI size without attaching to a view. -// Returns YES and fills outWidth/outHeight if the plugin has a GUI, NO otherwise. -- (BOOL)queryPreferredGUISize:(uint32_t *)outWidth height:(uint32_t *)outHeight; @end diff --git a/src/detail/auv3/auv3_audiounit.mm b/src/detail/auv3/auv3_audiounit.mm index 8c74cef2..6234957c 100644 --- a/src/detail/auv3/auv3_audiounit.mm +++ b/src/detail/auv3/auv3_audiounit.mm @@ -103,6 +103,9 @@ - (void)_wireParameterObserver; // Parameters AUParameterTree *_parameterTree = nil; + // Observer token used as 'originator' when pushing parameter changes to the host. + // This prevents the host from echoing the change back to our implementorValueObserver. + AUParameterObserverToken _parameterObserverToken = nullptr; // Hosting std::string _clapname; @@ -167,7 +170,13 @@ void startIdleTimer() auto plugin = _plugin; auto *flag = &_requestUICallback; auto *processing = &_initialized; // true between start_processing/stop_processing + auto *self = this; dispatch_source_set_event_handler(_idleTimer, ^{ + // Drain the parameter automation queue (Touch/Value/Release → host). + // This is safe even while processing — it only touches AUParameter + // objects on the main queue, no CLAP plugin calls. + self->drainParameterQueue(); + // Do NOT call on_main_thread() while the plugin is processing. // JUCE's on_main_thread() acquires locks that process() also needs — // calling both concurrently (main thread vs render thread) deadlocks. @@ -486,40 +495,70 @@ void onEndEdit(clap_id id) override _queueToUI.push(evt); } - // --- IPlugObject --- - void onIdle() override + // Drain the audio→UI parameter queue and forward automation events to the host. + // Safe to call while processing — only touches AUParameter objects, no CLAP calls. + void drainParameterQueue() { - if (!_plugin) return; - - if (_requestUICallback.exchange(false)) - { - auto guard = _plugin->AlwaysMainThread(); - _plugin->_plugin->on_main_thread(_plugin->_plugin); - } - - // Process queued parameter changes from audio thread queueEvent evt; while (_queueToUI.pop(evt)) { + if (!_parameterTree) continue; + switch (evt._type) { + case queueEvent::type::editstart: + { + AUParameter *param = [_parameterTree parameterWithAddress:(AUParameterAddress)evt._data._id]; + if (param) + { + [param setValue:param.value + originator:_parameterObserverToken + atHostTime:0 + eventType:AUParameterAutomationEventTypeTouch]; + } + break; + } case queueEvent::type::editvalue: { - if (_parameterTree) + AUParameter *param = [_parameterTree parameterWithAddress:(AUParameterAddress)evt._data._value.param_id]; + if (param) { - AUParameter *param = [_parameterTree parameterWithAddress:(AUParameterAddress)evt._data._value.param_id]; - if (param) - { - param.value = (AUValue)evt._data._value.value; - } + [param setValue:(AUValue)evt._data._value.value + originator:_parameterObserverToken + atHostTime:0 + eventType:AUParameterAutomationEventTypeValue]; } break; } - default: + case queueEvent::type::editend: + { + AUParameter *param = [_parameterTree parameterWithAddress:(AUParameterAddress)evt._data._id]; + if (param) + { + [param setValue:param.value + originator:_parameterObserverToken + atHostTime:0 + eventType:AUParameterAutomationEventTypeRelease]; + } break; + } } } } + + // --- IPlugObject --- + void onIdle() override + { + if (!_plugin) return; + + if (_requestUICallback.exchange(false)) + { + auto guard = _plugin->AlwaysMainThread(); + _plugin->_plugin->on_main_thread(_plugin->_plugin); + } + + drainParameterQueue(); + } }; } // namespace free_audio::auv3_wrapper @@ -734,6 +773,12 @@ - (void)dealloc AUV3LOG("dealloc: stopping idle timer"); _impl->stopIdleTimer(); + if (_impl->_parameterObserverToken && _impl->_parameterTree) + { + [_impl->_parameterTree removeParameterObserver:_impl->_parameterObserverToken]; + _impl->_parameterObserverToken = nullptr; + } + if (_impl->_plugin) { auto mainGuard = _impl->_plugin->AlwaysMainThread(); @@ -794,6 +839,17 @@ - (void)_wireParameterObserver { __weak typeof(self) weakSelf = self; + // Register a parameter observer to obtain a token. The token is used as + // 'originator' in setValue:originator:atHostTime:eventType: so that + // changes pushed from the CLAP plugin don't echo back through + // implementorValueObserver (which would re-flush them to the plugin). + // The observer block itself is intentionally empty — all host→plugin + // value changes arrive via implementorValueObserver below. + _impl->_parameterObserverToken = [_impl->_parameterTree + tokenByAddingParameterObserver:^(AUParameterAddress address, AUValue value) { + // Intentionally empty — see comment above. + }]; + _impl->_parameterTree.implementorValueObserver = ^(AUParameter *param, AUValue value) { __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf || !strongSelf->_impl) return; @@ -886,6 +942,14 @@ - (void)_wireParameterObserver - (void)_replaceParameterTree { AUV3LOG("_replaceParameterTree: firing KVO and re-wiring callbacks"); + + // Remove the old observer token before the tree is replaced + if (_impl->_parameterObserverToken && _impl->_parameterTree) + { + [_impl->_parameterTree removeParameterObserver:_impl->_parameterObserverToken]; + _impl->_parameterObserverToken = nullptr; + } + // Fire KVO so the host picks up the new tree [self willChangeValueForKey:@"parameterTree"]; [self didChangeValueForKey:@"parameterTree"]; @@ -1307,26 +1371,6 @@ - (void)setViewController:(ClapAUv3ViewController *)vc if (_impl) _impl->_viewController = vc; } -- (BOOL)queryPreferredGUISize:(uint32_t *)outWidth height:(uint32_t *)outHeight -{ - if (!_impl || !_impl->_plugin || !_impl->_plugin->_ext._gui) return NO; - - auto mainGuard = _impl->_plugin->AlwaysMainThread(); - auto *gui = _impl->_plugin->_ext._gui; - auto *plugin = _impl->_plugin->_plugin; - - if (!gui->is_api_supported(plugin, CLAP_WINDOW_API_COCOA, false)) return NO; - if (!gui->create(plugin, CLAP_WINDOW_API_COCOA, false)) return NO; - - uint32_t w = 0, h = 0; - gui->get_size(plugin, &w, &h); - gui->destroy(plugin); - - if (outWidth) *outWidth = w; - if (outHeight) *outHeight = h; - return (w > 0 && h > 0) ? YES : NO; -} - // --- View controller --- // Override requestViewControllerWithCompletionHandler: to return the factory VC. // The default AUAudioUnit implementation returns nil. The extension infrastructure @@ -1410,7 +1454,7 @@ - (void)loadView // the view controller hierarchy — many hosts just call addSubview:. // Start with a reasonable default size. The viewbridge rejects zero-sized views. // The actual size is updated from the CLAP plugin in setAudioUnit: / _createPluginGUI. - NSSize initialSize = NSMakeSize(400, 300); + NSSize initialSize = NSMakeSize(400, 500); ClapAUv3ContainerView *view = [[ClapAUv3ContainerView alloc] initWithFrame:NSMakeRect(0, 0, initialSize.width, initialSize.height)]; view.viewController = self; view.translatesAutoresizingMaskIntoConstraints = YES; @@ -1426,15 +1470,6 @@ - (void)setAudioUnit:(ClapAUv3AudioUnit *)audioUnit if (audioUnit) audioUnit->_factoryViewController = self; - // Query the CLAP plugin for its preferred GUI size so the host sees - // the correct dimensions before the GUI is actually created. - uint32_t w = 0, h = 0; - if (audioUnit && [audioUnit queryPreferredGUISize:&w height:&h]) - { - if (self.isViewLoaded) - self.view.frame = NSMakeRect(0, 0, w, h); - self.preferredContentSize = NSMakeSize(w, h); - } } - (void)_createPluginGUI From b2bc4d1a4eb367607338df1dbab1dc7802a0e48f Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:28:32 +0200 Subject: [PATCH 11/23] formatting according to clang-format --- src/detail/auv2/auv2_shared.mm | 4 +- src/detail/auv3/auv3_audiounit.mm | 267 ++++++++++-------- src/detail/auv3/auv3_parameters.mm | 7 +- src/detail/auv3/process.mm | 48 ++-- src/detail/os/macos.mm | 22 +- .../macos/auv3/AUv3HostAppDelegate.mm | 185 ++++++------ src/wrapasstandalone.mm | 2 +- 7 files changed, 285 insertions(+), 250 deletions(-) diff --git a/src/detail/auv2/auv2_shared.mm b/src/detail/auv2/auv2_shared.mm index 0d26d7cf..19147e6f 100644 --- a/src/detail/auv2/auv2_shared.mm +++ b/src/detail/auv2/auv2_shared.mm @@ -7,11 +7,11 @@ namespace free_audio::auv2_wrapper { -bool auv2shared_mm_request_resize(const clap_window_t* win, uint32_t w, uint32_t h) +bool auv2shared_mm_request_resize(const clap_window_t *win, uint32_t w, uint32_t h) { if (!win) return false; - auto* nsv = (NSView*)win; + auto *nsv = (NSView *)win; [nsv setFrame:NSMakeRect(0, 0, w, h)]; return false; diff --git a/src/detail/auv3/auv3_audiounit.mm b/src/detail/auv3/auv3_audiounit.mm index 6234957c..84c9a127 100644 --- a/src/detail/auv3/auv3_audiounit.mm +++ b/src/detail/auv3/auv3_audiounit.mm @@ -21,7 +21,8 @@ #include #include -static os_log_t _auv3Log() { +static os_log_t _auv3Log() +{ static os_log_t log = os_log_create("org.clap-wrapper.auv3", "wrapper"); return log; } @@ -59,9 +60,7 @@ - (void)_wireParameterObserver; } _data; }; -class AUv3ImplDetail : public Clap::IHost, - public Clap::IAutomation, - public os::IPlugObject +class AUv3ImplDetail : public Clap::IHost, public Clap::IAutomation, public os::IPlugObject { public: AUv3ImplDetail() : _os_attached([this] { os::attach(this); }, [this] { os::detach(this); }) @@ -230,8 +229,7 @@ void setupAudioBusses(const clap_plugin_t *plugin, } } - void setupMIDIBusses(const clap_plugin_t *plugin, - const clap_plugin_note_ports_t *noteports) override + void setupMIDIBusses(const clap_plugin_t *plugin, const clap_plugin_note_ports_t *noteports) override { if (!noteports) return; @@ -259,8 +257,7 @@ void setupMIDIBusses(const clap_plugin_t *plugin, } } - void setupParameters(const clap_plugin_t *plugin, - const clap_plugin_params_t *params) override + void setupParameters(const clap_plugin_t *plugin, const clap_plugin_params_t *params) override { _parameterTree = Clap::AUv3::createParameterTree(plugin, params); @@ -304,8 +301,7 @@ void param_rescan(clap_param_rescan_flags flags) override auto *cache = &_paramValueCache; _parameterTree.implementorValueProvider = ^AUValue(AUParameter *param) { auto it = cache->find((clap_id)param.address); - if (it != cache->end()) - return (AUValue)it->second; + if (it != cache->end()) return (AUValue)it->second; return (AUValue)0.0; }; @@ -346,8 +342,7 @@ void param_rescan(clap_param_rescan_flags flags) override if (params->get_info(plug, i, &info)) { double value = 0; - if (params->get_value(plug, info.id, &value)) - _paramValueCache[info.id] = value; + if (params->get_value(plug, info.id, &value)) _paramValueCache[info.id] = value; } } @@ -418,11 +413,23 @@ bool gui_request_resize(uint32_t width, uint32_t height) override return true; } - bool gui_request_show() override { return false; } - bool gui_request_hide() override { return false; } + bool gui_request_show() override + { + return false; + } + bool gui_request_hide() override + { + return false; + } - bool register_timer(uint32_t period_ms, clap_id *timer_id) override { return false; } - bool unregister_timer(clap_id timer_id) override { return false; } + bool register_timer(uint32_t period_ms, clap_id *timer_id) override + { + return false; + } + bool unregister_timer(clap_id timer_id) override + { + return false; + } const char *host_get_name() override { @@ -445,9 +452,15 @@ bool gui_request_resize(uint32_t width, uint32_t height) override return _hostname.c_str(); } - bool track_info_get(clap_track_info_t *info) override { return false; } + bool track_info_get(clap_track_info_t *info) override + { + return false; + } - bool supportsContextMenu() const override { return false; } + bool supportsContextMenu() const override + { + return false; + } bool context_menu_populate(const clap_context_menu_target_t *target, const clap_context_menu_builder_t *builder) override { @@ -457,9 +470,12 @@ bool context_menu_perform(const clap_context_menu_target_t *target, clap_id acti { return false; } - bool context_menu_can_popup() override { return false; } - bool context_menu_popup(const clap_context_menu_target_t *target, int32_t screen_index, - int32_t x, int32_t y) override + bool context_menu_can_popup() override + { + return false; + } + bool context_menu_popup(const clap_context_menu_target_t *target, int32_t screen_index, int32_t x, + int32_t y) override { return false; } @@ -512,21 +528,22 @@ void drainParameterQueue() if (param) { [param setValue:param.value - originator:_parameterObserverToken - atHostTime:0 - eventType:AUParameterAutomationEventTypeTouch]; + originator:_parameterObserverToken + atHostTime:0 + eventType:AUParameterAutomationEventTypeTouch]; } break; } case queueEvent::type::editvalue: { - AUParameter *param = [_parameterTree parameterWithAddress:(AUParameterAddress)evt._data._value.param_id]; + AUParameter *param = + [_parameterTree parameterWithAddress:(AUParameterAddress)evt._data._value.param_id]; if (param) { [param setValue:(AUValue)evt._data._value.value - originator:_parameterObserverToken - atHostTime:0 - eventType:AUParameterAutomationEventTypeValue]; + originator:_parameterObserverToken + atHostTime:0 + eventType:AUParameterAutomationEventTypeValue]; } break; } @@ -536,9 +553,9 @@ void drainParameterQueue() if (param) { [param setValue:param.value - originator:_parameterObserverToken - atHostTime:0 - eventType:AUParameterAutomationEventTypeRelease]; + originator:_parameterObserverToken + atHostTime:0 + eventType:AUParameterAutomationEventTypeRelease]; } break; } @@ -590,7 +607,8 @@ - (instancetype)initWithComponentDescription:(AudioComponentDescription)componen { AUV3LOG("initWithComponentDescription: entered (name=%{public}s id=%{public}s idx=%d)", [clapName UTF8String], clapId ? [clapId UTF8String] : "(nil)", clapIndex); - AUV3LOG("initWithComponentDescription: thread=%{public}s", [NSThread.currentThread.name UTF8String] ?: "unnamed"); + AUV3LOG("initWithComponentDescription: thread=%{public}s", + [NSThread.currentThread.name UTF8String] ?: "unnamed"); self = [super initWithComponentDescription:componentDescription options:options error:outError]; if (!self) @@ -608,8 +626,8 @@ - (instancetype)initWithComponentDescription:(AudioComponentDescription)componen _impl->_clapid = clapId ? [clapId UTF8String] : ""; _impl->_idx = clapIndex; - AUV3LOG("init: name='%{public}s' id='%{public}s' idx=%d", - _impl->_clapname.c_str(), _impl->_clapid.c_str(), _impl->_idx); + AUV3LOG("init: name='%{public}s' id='%{public}s' idx=%d", _impl->_clapname.c_str(), + _impl->_clapid.c_str(), _impl->_idx); // Load CLAP library if (!_library.hasEntryPoint()) @@ -619,7 +637,8 @@ - (instancetype)initWithComponentDescription:(AudioComponentDescription)componen { AUV3ERR("init: _clapname empty and no internal entry point"); if (outError) - *outError = [NSError errorWithDomain:@"ClapAUv3" code:-1 + *outError = [NSError errorWithDomain:@"ClapAUv3" + code:-1 userInfo:@{NSLocalizedDescriptionKey : @"CLAP name is empty"}]; return nil; } @@ -646,8 +665,10 @@ - (instancetype)initWithComponentDescription:(AudioComponentDescription)componen { AUV3ERR("init: cannot load CLAP '%{public}s'", _impl->_clapname.c_str()); if (outError) - *outError = [NSError errorWithDomain:@"ClapAUv3" code:-2 - userInfo:@{NSLocalizedDescriptionKey : @"Cannot load CLAP plugin"}]; + *outError = + [NSError errorWithDomain:@"ClapAUv3" + code:-2 + userInfo:@{NSLocalizedDescriptionKey : @"Cannot load CLAP plugin"}]; return nil; } } @@ -678,8 +699,10 @@ - (instancetype)initWithComponentDescription:(AudioComponentDescription)componen { AUV3ERR("init: cannot determine plugin description"); if (outError) - *outError = [NSError errorWithDomain:@"ClapAUv3" code:-3 - userInfo:@{NSLocalizedDescriptionKey : @"Cannot find CLAP plugin descriptor"}]; + *outError = [NSError + errorWithDomain:@"ClapAUv3" + code:-3 + userInfo:@{NSLocalizedDescriptionKey : @"Cannot find CLAP plugin descriptor"}]; return nil; } @@ -688,13 +711,16 @@ - (instancetype)initWithComponentDescription:(AudioComponentDescription)componen // Create the plugin instance AUV3LOG("init: creating plugin instance via factory"); - _impl->_plugin = Clap::Plugin::createInstance(_library._pluginFactory, _impl->_desc->id, _impl.get()); + _impl->_plugin = + Clap::Plugin::createInstance(_library._pluginFactory, _impl->_desc->id, _impl.get()); if (!_impl->_plugin) { AUV3ERR("init: factory returned null plugin instance"); if (outError) - *outError = [NSError errorWithDomain:@"ClapAUv3" code:-4 - userInfo:@{NSLocalizedDescriptionKey : @"CLAP plugin instance creation failed"}]; + *outError = [NSError + errorWithDomain:@"ClapAUv3" + code:-4 + userInfo:@{NSLocalizedDescriptionKey : @"CLAP plugin instance creation failed"}]; return nil; } AUV3LOG("init: plugin instance created successfully"); @@ -716,8 +742,8 @@ - (instancetype)initWithComponentDescription:(AudioComponentDescription)componen _impl->startIdleTimer(); // Build audio bus arrays from the CLAP audio port info - AUV3LOG("init: building bus arrays (inputs=%zu outputs=%zu)", - _impl->_inputBusInfos.size(), _impl->_outputBusInfos.size()); + AUV3LOG("init: building bus arrays (inputs=%zu outputs=%zu)", _impl->_inputBusInfos.size(), + _impl->_outputBusInfos.size()); [self _buildBusArrays]; _renderResourcesAllocated = NO; @@ -736,24 +762,30 @@ - (instancetype)initWithComponentDescription:(AudioComponentDescription)componen { AUV3ERR("init: caught exception of type int: %d", e); if (outError) - *outError = [NSError errorWithDomain:@"ClapAUv3" code:e - userInfo:@{NSLocalizedDescriptionKey : @"C++ int exception during init"}]; + *outError = + [NSError errorWithDomain:@"ClapAUv3" + code:e + userInfo:@{NSLocalizedDescriptionKey : @"C++ int exception during init"}]; return nil; } catch (const std::exception &e) { AUV3ERR("init: caught std::exception: %{public}s", e.what()); if (outError) - *outError = [NSError errorWithDomain:@"ClapAUv3" code:-99 - userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithUTF8String:e.what()]}]; + *outError = [NSError + errorWithDomain:@"ClapAUv3" + code:-99 + userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithUTF8String:e.what()]}]; return nil; } catch (...) { AUV3ERR("init: caught unknown C++ exception"); if (outError) - *outError = [NSError errorWithDomain:@"ClapAUv3" code:-98 - userInfo:@{NSLocalizedDescriptionKey : @"Unknown C++ exception during init"}]; + *outError = + [NSError errorWithDomain:@"ClapAUv3" + code:-98 + userInfo:@{NSLocalizedDescriptionKey : @"Unknown C++ exception during init"}]; return nil; } @@ -764,8 +796,7 @@ - (void)dealloc { AUV3LOG("dealloc: entered (self=%p, thread=%{public}s)", self, [NSThread.currentThread.name UTF8String] ?: "unnamed"); - AUV3LOG("dealloc: _impl=%{public}s, _plugin=%{public}s", - _impl ? "valid" : "null", + AUV3LOG("dealloc: _impl=%{public}s, _plugin=%{public}s", _impl ? "valid" : "null", (_impl && _impl->_plugin) ? "valid" : "null"); if (_impl) @@ -800,8 +831,9 @@ - (void)_buildBusArrays NSMutableArray *inputs = [NSMutableArray new]; for (auto &busInfo : _impl->_inputBusInfos) { - AVAudioFormat *format = [[AVAudioFormat alloc] initStandardFormatWithSampleRate:self.outputBusses.count > 0 ? 44100.0 : 44100.0 - channels:busInfo.channelCount]; + AVAudioFormat *format = [[AVAudioFormat alloc] + initStandardFormatWithSampleRate:self.outputBusses.count > 0 ? 44100.0 : 44100.0 + channels:busInfo.channelCount]; if (format) { NSError *error = nil; @@ -813,14 +845,16 @@ - (void)_buildBusArrays } } } - _inputBusArray = [[AUAudioUnitBusArray alloc] initWithAudioUnit:self busType:AUAudioUnitBusTypeInput busses:inputs]; + _inputBusArray = [[AUAudioUnitBusArray alloc] initWithAudioUnit:self + busType:AUAudioUnitBusTypeInput + busses:inputs]; // Build output bus array NSMutableArray *outputs = [NSMutableArray new]; for (auto &busInfo : _impl->_outputBusInfos) { - AVAudioFormat *format = [[AVAudioFormat alloc] initStandardFormatWithSampleRate:44100.0 - channels:busInfo.channelCount]; + AVAudioFormat *format = + [[AVAudioFormat alloc] initStandardFormatWithSampleRate:44100.0 channels:busInfo.channelCount]; if (format) { NSError *error = nil; @@ -832,7 +866,9 @@ - (void)_buildBusArrays } } } - _outputBusArray = [[AUAudioUnitBusArray alloc] initWithAudioUnit:self busType:AUAudioUnitBusTypeOutput busses:outputs]; + _outputBusArray = [[AUAudioUnitBusArray alloc] initWithAudioUnit:self + busType:AUAudioUnitBusTypeOutput + busses:outputs]; } - (void)_wireParameterObserver @@ -845,9 +881,9 @@ - (void)_wireParameterObserver // implementorValueObserver (which would re-flush them to the plugin). // The observer block itself is intentionally empty — all host→plugin // value changes arrive via implementorValueObserver below. - _impl->_parameterObserverToken = [_impl->_parameterTree - tokenByAddingParameterObserver:^(AUParameterAddress address, AUValue value) { - // Intentionally empty — see comment above. + _impl->_parameterObserverToken = + [_impl->_parameterTree tokenByAddingParameterObserver:^(AUParameterAddress address, AUValue value){ + // Intentionally empty — see comment above. }]; _impl->_parameterTree.implementorValueObserver = ^(AUParameter *param, AUValue value) { @@ -890,15 +926,13 @@ - (void)_wireParameterObserver clap_input_events_t in_events = {}; in_events.ctx = &evPtr; in_events.size = [](const clap_input_events_t *) -> uint32_t { return 1; }; - in_events.get = [](const clap_input_events_t *list, uint32_t) -> const clap_event_header_t * { - return *static_cast(list->ctx); - }; + in_events.get = [](const clap_input_events_t *list, uint32_t) -> const clap_event_header_t * + { return *static_cast(list->ctx); }; clap_output_events_t out_events = {}; out_events.ctx = nullptr; - out_events.try_push = [](const clap_output_events_t *, const clap_event_header_t *) -> bool { - return true; - }; + out_events.try_push = [](const clap_output_events_t *, const clap_event_header_t *) -> bool + { return true; }; auto mainGuard = strongSelf->_impl->_plugin->AlwaysMainThread(); ext_params->flush(plugin, &in_events, &out_events); @@ -912,31 +946,34 @@ - (void)_wireParameterObserver _impl->_parameterTree.implementorValueProvider = ^AUValue(AUParameter *param) { auto it = cache->find((clap_id)param.address); - if (it != cache->end()) - return (AUValue)it->second; + if (it != cache->end()) return (AUValue)it->second; return (AUValue)0.0; }; - _impl->_parameterTree.implementorStringFromValueCallback = ^NSString *(AUParameter *param, const AUValue *value) { - auto guard = plugin->AlwaysMainThread(); - char buf[256]; - AUValue v = value ? *value : param.value; - if (plugin->_ext._params->value_to_text(plugin->_plugin, (clap_id)param.address, (double)v, buf, sizeof(buf))) - { - return [NSString stringWithUTF8String:buf]; - } - return [NSString stringWithFormat:@"%.3f", v]; - }; + _impl->_parameterTree.implementorStringFromValueCallback = + ^NSString *(AUParameter *param, const AUValue *value) { + auto guard = plugin->AlwaysMainThread(); + char buf[256]; + AUValue v = value ? *value : param.value; + if (plugin->_ext._params->value_to_text(plugin->_plugin, (clap_id)param.address, (double)v, buf, + sizeof(buf))) + { + return [NSString stringWithUTF8String:buf]; + } + return [NSString stringWithFormat:@"%.3f", v]; + }; - _impl->_parameterTree.implementorValueFromStringCallback = ^AUValue(AUParameter *param, NSString *string) { - auto guard = plugin->AlwaysMainThread(); - double value = 0; - if (plugin->_ext._params->text_to_value(plugin->_plugin, (clap_id)param.address, [string UTF8String], &value)) - { - return (AUValue)value; - } - return (AUValue)[string doubleValue]; - }; + _impl->_parameterTree.implementorValueFromStringCallback = + ^AUValue(AUParameter *param, NSString *string) { + auto guard = plugin->AlwaysMainThread(); + double value = 0; + if (plugin->_ext._params->text_to_value(plugin->_plugin, (clap_id)param.address, + [string UTF8String], &value)) + { + return (AUValue)value; + } + return (AUValue)[string doubleValue]; + }; } - (void)_replaceParameterTree @@ -1009,8 +1046,7 @@ - (NSTimeInterval)latency if (_impl && _impl->_cachedLatencySamples > 0) { double sr = self.outputBusses[0].format.sampleRate; - if (sr > 0) - return (double)_impl->_cachedLatencySamples / sr; + if (sr > 0) return (double)_impl->_cachedLatencySamples / sr; } return 0; } @@ -1127,8 +1163,7 @@ - (void)setFullState:(NSDictionary *)fullState if (params->get_info(plug, i, &info)) { double value = 0; - if (params->get_value(plug, info.id, &value)) - _impl->_paramValueCache[info.id] = value; + if (params->get_value(plug, info.id, &value)) _impl->_paramValueCache[info.id] = value; } } } @@ -1159,7 +1194,8 @@ - (BOOL)allocateRenderResourcesAndReturnError:(NSError **)outError AUV3ERR("allocateRenderResources: plugin not initialized (_impl=%{public}s)", _impl ? "valid" : "null"); if (outError) - *outError = [NSError errorWithDomain:@"ClapAUv3" code:-10 + *outError = [NSError errorWithDomain:@"ClapAUv3" + code:-10 userInfo:@{NSLocalizedDescriptionKey : @"Plugin not initialized"}]; return NO; } @@ -1174,8 +1210,8 @@ - (BOOL)allocateRenderResourcesAndReturnError:(NSError **)outError { sampleRate = self.inputBusses[0].format.sampleRate; } - AUV3LOG("allocateRenderResources: sampleRate=%.0f maxFrames=%u", - sampleRate, (unsigned)self.maximumFramesToRender); + AUV3LOG("allocateRenderResources: sampleRate=%.0f maxFrames=%u", sampleRate, + (unsigned)self.maximumFramesToRender); auto guarantee_mainthread = _impl->_plugin->AlwaysMainThread(); @@ -1193,8 +1229,8 @@ - (BOOL)allocateRenderResourcesAndReturnError:(NSError **)outError { outputChs.push_back((uint32_t)self.outputBusses[i].format.channelCount); } - AUV3LOG("allocateRenderResources: input busses=%zu output busses=%zu", - inputChs.size(), outputChs.size()); + AUV3LOG("allocateRenderResources: input busses=%zu output busses=%zu", inputChs.size(), + outputChs.size()); // Create and set up the process adapter AUV3LOG("allocateRenderResources: creating process adapter"); @@ -1202,8 +1238,8 @@ - (BOOL)allocateRenderResourcesAndReturnError:(NSError **)outError _impl->_processAdapter->setupProcessing( (uint32_t)inputChs.size(), inputChs.empty() ? nullptr : inputChs.data(), (uint32_t)outputChs.size(), outputChs.empty() ? nullptr : outputChs.data(), - _impl->_plugin->_plugin, _impl->_plugin->_ext._params, _impl.get(), - self.maximumFramesToRender, _impl->_midi_preferred_dialect); + _impl->_plugin->_plugin, _impl->_plugin->_ext._params, _impl.get(), self.maximumFramesToRender, + _impl->_midi_preferred_dialect); // Set transport state and musical context blocks _impl->_processAdapter->setTransportStateBlock(self.transportStateBlock); @@ -1260,13 +1296,10 @@ - (AUInternalRenderBlock)internalRenderBlock // render time rather than at block-creation time. auto *impl = _impl.get(); - return ^AUAudioUnitStatus(AudioUnitRenderActionFlags *actionFlags, - const AudioTimeStamp *timestamp, - AUAudioFrameCount frameCount, - NSInteger outputBusNumber, - AudioBufferList *outputData, - const AURenderEvent *realtimeEventListHead, - AURenderPullInputBlock __unsafe_unretained pullInputBlock) { + return ^AUAudioUnitStatus(AudioUnitRenderActionFlags *actionFlags, const AudioTimeStamp *timestamp, + AUAudioFrameCount frameCount, NSInteger outputBusNumber, + AudioBufferList *outputData, const AURenderEvent *realtimeEventListHead, + AURenderPullInputBlock __unsafe_unretained pullInputBlock) { if (!impl || !impl->_processAdapter) return kAudioUnitErr_Uninitialized; // Force audio-thread identity for the duration of the render call. @@ -1377,7 +1410,8 @@ - (void)setViewController:(ClapAUv3ViewController *)vc // may handle this automatically in some contexts, but explicitly returning the VC // ensures the host can always obtain it (both in-process and out-of-process). -- (void)requestViewControllerWithCompletionHandler:(void (^)(AUViewControllerBase * __nullable))completionHandler +- (void)requestViewControllerWithCompletionHandler: + (void (^)(AUViewControllerBase *__nullable))completionHandler { AUV3LOG("requestViewControllerWithCompletionHandler: called (factoryVC=%p)", _factoryViewController); completionHandler(_factoryViewController); @@ -1405,7 +1439,7 @@ - (void)_viewDidMoveToWindow; // ----------------------------------------------------------------------- @interface ClapAUv3ContainerView : NSView -@property (nonatomic, weak) ClapAUv3ViewController *viewController; +@property(nonatomic, weak) ClapAUv3ViewController *viewController; @end @implementation ClapAUv3ContainerView @@ -1455,7 +1489,8 @@ - (void)loadView // Start with a reasonable default size. The viewbridge rejects zero-sized views. // The actual size is updated from the CLAP plugin in setAudioUnit: / _createPluginGUI. NSSize initialSize = NSMakeSize(400, 500); - ClapAUv3ContainerView *view = [[ClapAUv3ContainerView alloc] initWithFrame:NSMakeRect(0, 0, initialSize.width, initialSize.height)]; + ClapAUv3ContainerView *view = [[ClapAUv3ContainerView alloc] + initWithFrame:NSMakeRect(0, 0, initialSize.width, initialSize.height)]; view.viewController = self; view.translatesAutoresizingMaskIntoConstraints = YES; [self setView:view]; @@ -1467,9 +1502,7 @@ - (void)setAudioUnit:(ClapAUv3AudioUnit *)audioUnit _audioUnit = audioUnit; // Establish the back-reference so the AU can return us from // requestViewControllerWithCompletionHandler: - if (audioUnit) - audioUnit->_factoryViewController = self; - + if (audioUnit) audioUnit->_factoryViewController = self; } - (void)_createPluginGUI @@ -1492,7 +1525,6 @@ - (void)_createPluginGUI [self willChangeValueForKey:@"preferredContentSize"]; self.preferredContentSize = NSMakeSize(w, h); [self didChangeValueForKey:@"preferredContentSize"]; - } } } @@ -1561,8 +1593,7 @@ - (void)viewDidLayout // Propagate host-initiated container resize to the CLAP plugin if ([self.audioUnit canResizeGUI]) { - [self.audioUnit setGUISize:(uint32_t)bounds.size.width - height:(uint32_t)bounds.size.height]; + [self.audioUnit setGUISize:(uint32_t)bounds.size.width height:(uint32_t)bounds.size.height]; } // Ensure the CLAP plugin's subview fills the container @@ -1600,8 +1631,10 @@ - (AUAudioUnit *)createAudioUnitWithComponentDescription:(AudioComponentDescript { AUV3ERR("createAudioUnitWithComponentDescription: BASE class called — subclass should override"); if (error) - *error = [NSError errorWithDomain:@"ClapAUv3" code:-100 - userInfo:@{NSLocalizedDescriptionKey : @"Base factory should not be called directly"}]; + *error = [NSError + errorWithDomain:@"ClapAUv3" + code:-100 + userInfo:@{NSLocalizedDescriptionKey : @"Base factory should not be called directly"}]; return nil; } diff --git a/src/detail/auv3/auv3_parameters.mm b/src/detail/auv3/auv3_parameters.mm index 530bbede..ddb879ce 100644 --- a/src/detail/auv3/auv3_parameters.mm +++ b/src/detail/auv3/auv3_parameters.mm @@ -63,8 +63,7 @@ } }; -AUParameterTree *createParameterTree(const clap_plugin_t *plugin, - const clap_plugin_params_t *params) +AUParameterTree *createParameterTree(const clap_plugin_t *plugin, const clap_plugin_params_t *params) { if (!params) return [AUParameterTree createTreeWithChildren:@[]]; @@ -189,8 +188,8 @@ tree.implementorValueFromStringCallback = ^AUValue(AUParameter *param, NSString *string) { double value = 0; - if (capturedParams->text_to_value(capturedPlugin, (clap_id)param.address, - [string UTF8String], &value)) + if (capturedParams->text_to_value(capturedPlugin, (clap_id)param.address, [string UTF8String], + &value)) { return (AUValue)value; } diff --git a/src/detail/auv3/process.mm b/src/detail/auv3/process.mm index 0b4ec235..ed2b09ec 100644 --- a/src/detail/auv3/process.mm +++ b/src/detail/auv3/process.mm @@ -12,8 +12,8 @@ // static os_log_t log = os_log_create("org.clap-wrapper.auv3", "process"); // return log; // } -#define PROCLOG(...) // os_log(_procLog(), __VA_ARGS__) -#define PROCERR(...) // os_log_error(_procLog(), __VA_ARGS__) +#define PROCLOG(...) // os_log(_procLog(), __VA_ARGS__) +#define PROCERR(...) // os_log_error(_procLog(), __VA_ARGS__) namespace Clap::AUv3 { @@ -62,8 +62,7 @@ inline clap_sectime doubleToSecTime(double t) void ProcessAdapter::setupProcessing(uint32_t numInputBusses, const uint32_t *inputChannelCounts, uint32_t numOutputBusses, const uint32_t *outputChannelCounts, - const clap_plugin_t *plugin, - const clap_plugin_params_t *ext_params, + const clap_plugin_t *plugin, const clap_plugin_params_t *ext_params, Clap::IAutomation *automation, uint32_t numMaxSamples, uint32_t preferredMIDIDialect) { @@ -147,8 +146,7 @@ inline clap_sectime doubleToSecTime(double t) // Allocate output storage for multi-bus rendering (max 8 channels per bus) _numMaxSamples = numMaxSamples; _outputStorage.resize(_numOutputs * 8); - for (auto &buf : _outputStorage) - buf.resize(numMaxSamples, 0.0f); + for (auto &buf : _outputStorage) buf.resize(numMaxSamples, 0.0f); _lastProcessedSampleTime = UINT64_MAX; // Wire up CLAP process data @@ -200,9 +198,8 @@ inline clap_sectime doubleToSecTime(double t) }); } -void ProcessAdapter::translateAUv3Events(const AURenderEvent *head, - AUEventSampleTime bufferStartTime, - AVAudioFrameCount frameCount) +void ProcessAdapter::translateAUv3Events(const AURenderEvent *head, AUEventSampleTime bufferStartTime, + AVAudioFrameCount frameCount) { for (const AURenderEvent *event = head; event != nullptr; event = event->head.next) { @@ -226,8 +223,8 @@ inline clap_sectime doubleToSecTime(double t) { auto &pe = event->parameter; PROCLOG("translateEvent: param addr=%llu value=%.4f absTime=%lld offset=%u", - (unsigned long long)pe.parameterAddress, (float)pe.value, - (long long)pe.eventSampleTime, sampleOffset); + (unsigned long long)pe.parameterAddress, (float)pe.value, (long long)pe.eventSampleTime, + sampleOffset); n.header.size = sizeof(clap_event_param_value_t); n.header.type = CLAP_EVENT_PARAM_VALUE; n.header.space_id = CLAP_CORE_EVENT_SPACE_ID; @@ -237,8 +234,7 @@ inline clap_sectime doubleToSecTime(double t) clap_id pid = (clap_id)pe.parameterAddress; // Skip unknown parameter IDs — auval sends bogus IDs to test robustness - if (_cookieCache && _cookieCache->find(pid) == _cookieCache->end()) - break; + if (_cookieCache && _cookieCache->find(pid) == _cookieCache->end()) break; n.param.param_id = pid; n.param.value = (double)pe.value; @@ -255,9 +251,8 @@ inline clap_sectime doubleToSecTime(double t) case AURenderEventMIDI: { - auto &me = event->MIDI; - PROCLOG("translateEvent: %02x %02x %02x",(int)me.data[0],(int)me.data[1],(int)me.data[2]); + PROCLOG("translateEvent: %02x %02x %02x", (int)me.data[0], (int)me.data[1], (int)me.data[2]); uint8_t status = me.data[0]; uint8_t strippedStatus = (status >> 4) & 0x0F; uint8_t channel = status & 0x0F; @@ -337,10 +332,8 @@ inline clap_sectime doubleToSecTime(double t) } AUAudioUnitStatus ProcessAdapter::process(AudioUnitRenderActionFlags *actionFlags, - const AudioTimeStamp *timestamp, - AVAudioFrameCount frameCount, - NSInteger outputBusNumber, - AudioBufferList *outputData, + const AudioTimeStamp *timestamp, AVAudioFrameCount frameCount, + NSInteger outputBusNumber, AudioBufferList *outputData, const AURenderEvent *realtimeEventListHead, AURenderPullInputBlock __unsafe_unretained pullInputBlock) { @@ -397,8 +390,7 @@ inline clap_sectime doubleToSecTime(double t) if (_transportStateBlock(&transportFlags, ¤tSamplePosition, &cycleStartBeatPosition, &cycleEndBeatPosition)) { - if (transportFlags & AUHostTransportStateMoving) - _transport.flags |= CLAP_TRANSPORT_IS_PLAYING; + if (transportFlags & AUHostTransportStateMoving) _transport.flags |= CLAP_TRANSPORT_IS_PLAYING; if (transportFlags & AUHostTransportStateRecording) _transport.flags |= CLAP_TRANSPORT_IS_RECORDING; if (transportFlags & AUHostTransportStateCycling) @@ -419,8 +411,8 @@ inline clap_sectime doubleToSecTime(double t) NSInteger sampleOffsetToNextBeat = 0; double downbeatPos = 0; - if (_musicalContextBlock(&tempo, &tsigNum, &tsigDenom, &beatPos, - &sampleOffsetToNextBeat, &downbeatPos)) + if (_musicalContextBlock(&tempo, &tsigNum, &tsigDenom, &beatPos, &sampleOffsetToNextBeat, + &downbeatPos)) { if (tempo > 0) { @@ -450,8 +442,8 @@ inline clap_sectime doubleToSecTime(double t) } // Pull input audio - PROCLOG("process: pulling input (_numInputs=%u, pullInputBlock=%{public}s)", - _numInputs, pullInputBlock ? "yes" : "nil"); + PROCLOG("process: pulling input (_numInputs=%u, pullInputBlock=%{public}s)", _numInputs, + pullInputBlock ? "yes" : "nil"); if (_numInputs > 0 && pullInputBlock) { for (uint32_t bus = 0; bus < _numInputs; ++bus) @@ -469,7 +461,8 @@ inline clap_sectime doubleToSecTime(double t) AudioUnitRenderActionFlags pullFlags = 0; PROCLOG("process: pulling bus %u (%u ch)", bus, numCh); - AUAudioUnitStatus status = pullInputBlock(&pullFlags, timestamp, frameCount, bus, _inputBufferList); + AUAudioUnitStatus status = + pullInputBlock(&pullFlags, timestamp, frameCount, bus, _inputBufferList); PROCLOG("process: pull bus %u status=%d", bus, (int)status); if (status == noErr) { @@ -594,8 +587,7 @@ inline clap_sectime doubleToSecTime(double t) if (_cookieCache) { auto it = _cookieCache->find(paramId); - if (it != _cookieCache->end()) - n.param.cookie = it->second; + if (it != _cookieCache->end()) n.param.cookie = it->second; } n.param.port_index = -1; n.param.key = -1; diff --git a/src/detail/os/macos.mm b/src/detail/os/macos.mm index 05732559..5329fa08 100644 --- a/src/detail/os/macos.mm +++ b/src/detail/os/macos.mm @@ -29,14 +29,14 @@ public: void init(); void terminate(); - void attach(IPlugObject* plugobject); - void detach(IPlugObject* plugobject); + void attach(IPlugObject *plugobject); + void detach(IPlugObject *plugobject); private: - static void timerCallback(CFRunLoopTimerRef t, void* info); + static void timerCallback(CFRunLoopTimerRef t, void *info); void executeDefered(); CFRunLoopTimerRef _timer = nullptr; - std::vector _plugs; + std::vector _plugs; } gMacOSHelper; // standard specific extensions @@ -63,15 +63,15 @@ } } -void MacOSHelper::timerCallback(CFRunLoopTimerRef /*t*/, void* info) +void MacOSHelper::timerCallback(CFRunLoopTimerRef /*t*/, void *info) { - auto self = static_cast(info); + auto self = static_cast(info); self->executeDefered(); } static float kIntervall = 10.f; -void MacOSHelper::attach(IPlugObject* plugobject) +void MacOSHelper::attach(IPlugObject *plugobject) { if (_plugs.empty()) { @@ -85,7 +85,7 @@ _plugs.push_back(plugobject); } -void MacOSHelper::detach(IPlugObject* plugobject) +void MacOSHelper::detach(IPlugObject *plugobject) { _plugs.erase(std::remove(_plugs.begin(), _plugs.end(), plugobject), _plugs.end()); if (_plugs.empty()) @@ -104,13 +104,13 @@ namespace os { // [UI Thread] -void attach(IPlugObject* plugobject) +void attach(IPlugObject *plugobject) { gMacOSHelper.attach(plugobject); } // [UI Thread] -void detach(IPlugObject* plugobject) +void detach(IPlugObject *plugobject) { gMacOSHelper.detach(plugobject); } @@ -123,7 +123,7 @@ uint64_t getTickInMS() fs::path getPluginPath() { Dl_info info; - if (dladdr((void*)getPluginPath, &info)) + if (dladdr((void *)getPluginPath, &info)) { fs::path binaryPath = info.dli_fname; return binaryPath.parent_path().parent_path().parent_path(); diff --git a/src/detail/standalone/macos/auv3/AUv3HostAppDelegate.mm b/src/detail/standalone/macos/auv3/AUv3HostAppDelegate.mm index 2ad9eb9f..5ce7f5b3 100644 --- a/src/detail/standalone/macos/auv3/AUv3HostAppDelegate.mm +++ b/src/detail/standalone/macos/auv3/AUv3HostAppDelegate.mm @@ -61,9 +61,12 @@ - (void)applicationWillTerminate:(NSNotification *)notification // Remove KVO observer before tearing down if (_auViewController) { - @try { + @try + { [_auViewController removeObserver:self forKeyPath:@"preferredContentSize"]; - } @catch (NSException *e) { + } + @catch (NSException *e) + { // Observer was never added (e.g., no GUI path) } } @@ -116,8 +119,8 @@ - (NSBundle *)findEmbeddedAppexBundle } - (AUAudioUnit *)instantiateAUDirectlyFromAppex:(NSBundle *)appexBundle - componentDescription:(AudioComponentDescription)desc - error:(NSError **)outError + componentDescription:(AudioComponentDescription)desc + error:(NSError **)outError { // Load the appex bundle to get its Objective-C classes. // Note: MH_EXECUTE binaries can't be loaded via NSBundle's load method, @@ -144,9 +147,9 @@ - (AUAudioUnit *)instantiateAUDirectlyFromAppex:(NSBundle *)appexBundle NSError *loadError = nil; if (![appexBundle loadAndReturnError:&loadError]) { - std::cout << "[auv3-standalone] WARNING: NSBundle load failed: " - << [loadError.localizedDescription UTF8String] - << " (may be expected for executable appex)" << std::endl; + std::cout << "[auv3-standalone] WARNING: NSBundle load failed: " << + [loadError.localizedDescription UTF8String] << " (may be expected for executable appex)" + << std::endl; } else { @@ -164,8 +167,10 @@ - (AUAudioUnit *)instantiateAUDirectlyFromAppex:(NSBundle *)appexBundle { std::cout << "[auv3-standalone] ERROR: No NSExtensionPrincipalClass in appex" << std::endl; if (outError) - *outError = [NSError errorWithDomain:@"ClapAUv3" code:-1 - userInfo:@{NSLocalizedDescriptionKey: @"No principal class in appex"}]; + *outError = + [NSError errorWithDomain:@"ClapAUv3" + code:-1 + userInfo:@{NSLocalizedDescriptionKey : @"No principal class in appex"}]; return nil; } @@ -181,21 +186,30 @@ - (AUAudioUnit *)instantiateAUDirectlyFromAppex:(NSBundle *)appexBundle if (!factoryClass) { - std::cout << "[auv3-standalone] ERROR: Cannot find class " << [principalClassName UTF8String] << std::endl; + std::cout << "[auv3-standalone] ERROR: Cannot find class " << [principalClassName UTF8String] + << std::endl; if (outError) - *outError = [NSError errorWithDomain:@"ClapAUv3" code:-2 - userInfo:@{NSLocalizedDescriptionKey: - [NSString stringWithFormat:@"Cannot find class %@", principalClassName]}]; + *outError = [NSError errorWithDomain:@"ClapAUv3" + code:-2 + userInfo:@{ + NSLocalizedDescriptionKey : [NSString + stringWithFormat:@"Cannot find class %@", principalClassName] + }]; return nil; } // The factory class conforms to AUAudioUnitFactory if (![factoryClass conformsToProtocol:@protocol(AUAudioUnitFactory)]) { - std::cout << "[auv3-standalone] ERROR: Principal class does not conform to AUAudioUnitFactory" << std::endl; + std::cout << "[auv3-standalone] ERROR: Principal class does not conform to AUAudioUnitFactory" + << std::endl; if (outError) - *outError = [NSError errorWithDomain:@"ClapAUv3" code:-3 - userInfo:@{NSLocalizedDescriptionKey: @"Principal class is not an AUAudioUnitFactory"}]; + *outError = + [NSError errorWithDomain:@"ClapAUv3" + code:-3 + userInfo:@{ + NSLocalizedDescriptionKey : @"Principal class is not an AUAudioUnitFactory" + }]; return nil; } @@ -227,7 +241,7 @@ - (void)doSetup { case AVAuthorizationStatusNotDetermined: [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio - completionHandler:^(BOOL granted) { + completionHandler:^(BOOL granted){ }]; break; default: @@ -243,9 +257,8 @@ - (void)doSetup desc.componentFlags = 0; desc.componentFlagsMask = 0; - std::cout << "[auv3-standalone] Looking for AU: type='" << AU_TYPE_STR - << "' subtype='" << AU_SUBTYPE_STR - << "' manufacturer='" << AU_MANUFACTURER_STR << "'" << std::endl; + std::cout << "[auv3-standalone] Looking for AU: type='" << AU_TYPE_STR << "' subtype='" + << AU_SUBTYPE_STR << "' manufacturer='" << AU_MANUFACTURER_STR << "'" << std::endl; // First try: use the system-registered AU (via AVAudioUnit + AVAudioEngine) AudioComponent comp = AudioComponentFindNext(NULL, &desc); @@ -260,12 +273,13 @@ - (void)doSetup __weak typeof(self) weakSelf = self; [AVAudioUnit instantiateWithComponentDescription:desc options:kAudioComponentInstantiation_LoadInProcess - completionHandler:^(AVAudioUnit *_Nullable audioUnit, NSError *_Nullable error) { - dispatch_async(dispatch_get_main_queue(), ^{ - __strong typeof(weakSelf) self = weakSelf; - if (self) [self finishSetupWithAudioUnit:audioUnit error:error]; - }); - }]; + completionHandler:^(AVAudioUnit *_Nullable audioUnit, + NSError *_Nullable error) { + dispatch_async(dispatch_get_main_queue(), ^{ + __strong typeof(weakSelf) self = weakSelf; + if (self) [self finishSetupWithAudioUnit:audioUnit error:error]; + }); + }]; return; } @@ -316,8 +330,7 @@ - (void)finishSetupWithAudioUnit:(AVAudioUnit *)audioUnit error:(NSError *)error if (error || !audioUnit) { NSString *msg = error ? error.localizedDescription : @"Unknown error"; - std::cout << "[auv3-standalone] ERROR: Failed to instantiate AU: " - << [msg UTF8String] << std::endl; + std::cout << "[auv3-standalone] ERROR: Failed to instantiate AU: " << [msg UTF8String] << std::endl; NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:@"Failed to load Audio Unit"]; [alert setInformativeText:msg]; @@ -327,8 +340,8 @@ - (void)finishSetupWithAudioUnit:(AVAudioUnit *)audioUnit error:(NSError *)error } _avAudioUnit = audioUnit; - std::cout << "[auv3-standalone] AU instantiated via AVAudioUnit: " - << [audioUnit.name UTF8String] << std::endl; + std::cout << "[auv3-standalone] AU instantiated via AVAudioUnit: " << [audioUnit.name UTF8String] + << std::endl; [self restoreState]; [self setupEngine]; @@ -351,8 +364,8 @@ - (void)finishSetupWithAUAudioUnit:(AUAudioUnit *)au NSError *error = nil; if (![au allocateRenderResourcesAndReturnError:&error]) { - std::cout << "[auv3-standalone] ERROR: allocateRenderResources failed: " - << [error.localizedDescription UTF8String] << std::endl; + std::cout << "[auv3-standalone] ERROR: allocateRenderResources failed: " << + [error.localizedDescription UTF8String] << std::endl; } else { @@ -396,8 +409,8 @@ - (void)setupEngine NSError *error = nil; if (![_engine startAndReturnError:&error]) { - std::cout << "[auv3-standalone] ERROR: Failed to start engine: " - << [error.localizedDescription UTF8String] << std::endl; + std::cout << "[auv3-standalone] ERROR: Failed to start engine: " << + [error.localizedDescription UTF8String] << std::endl; NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:@"Failed to start audio engine"]; @@ -407,8 +420,8 @@ - (void)setupEngine } else { - std::cout << "[auv3-standalone] Engine started. Sample rate: " - << outputFormat.sampleRate << " Hz" << std::endl; + std::cout << "[auv3-standalone] Engine started. Sample rate: " << outputFormat.sampleRate << " Hz" + << std::endl; } } @@ -460,13 +473,13 @@ - (void)setupGUI [window setMovableByWindowBackground:YES]; [window orderFrontRegardless]; - std::cout << "[auv3-standalone] window styleMask=0x" - << std::hex << (unsigned long)window.styleMask << std::dec + std::cout << "[auv3-standalone] window styleMask=0x" << std::hex << (unsigned long)window.styleMask + << std::dec << " resizable=" << ((window.styleMask & NSWindowStyleMaskResizable) ? "YES" : "NO") << std::endl; - std::cout << "[auv3-standalone] GUI displayed (" - << (int)preferredSize.width << "x" << (int)preferredSize.height << ")" << std::endl; + std::cout << "[auv3-standalone] GUI displayed (" << (int)preferredSize.width << "x" + << (int)preferredSize.height << ")" << std::endl; // For out-of-process AUv3, KVO on preferredContentSize may not work // across the XPC boundary. Poll after a delay to pick up the plugin's @@ -521,8 +534,8 @@ - (void)setupGUIFromAUAudioUnit:(AUAudioUnit *)au [window orderFrontRegardless]; - std::cout << "[auv3-standalone] GUI displayed (direct) (" - << (int)preferredSize.width << "x" << (int)preferredSize.height << ")" << std::endl; + std::cout << "[auv3-standalone] GUI displayed (direct) (" << (int)preferredSize.width << "x" + << (int)preferredSize.height << ")" << std::endl; [self _pollPreferredContentSize:vc retries:10]; }); @@ -541,10 +554,8 @@ - (void)setupMIDIForAUAudioUnit:(AUAudioUnit *)au OSStatus status = MIDIClientCreate(CFSTR("ClapWrapperAUv3Standalone"), NULL, NULL, &sMIDIClient); if (status != noErr) return; - status = MIDIInputPortCreate(sMIDIClient, CFSTR("Input"), - midiInputCallback, - (__bridge void *)_scheduleMIDIBlock, - &sMIDIInputPort); + status = MIDIInputPortCreate(sMIDIClient, CFSTR("Input"), midiInputCallback, + (__bridge void *)_scheduleMIDIBlock, &sMIDIInputPort); if (status != noErr) return; ItemCount numSources = MIDIGetNumberOfSources(); @@ -553,15 +564,15 @@ - (void)setupMIDIForAUAudioUnit:(AUAudioUnit *)au MIDIEndpointRef src = MIDIGetSource(i); MIDIPortConnectSource(sMIDIInputPort, src, NULL); } - std::cout << "[auv3-standalone] MIDI connected (direct) to " << numSources << " source(s)" << std::endl; + std::cout << "[auv3-standalone] MIDI connected (direct) to " << numSources << " source(s)" + << std::endl; } // --------------------------------------------------------------------------- #pragma mark - MIDI // --------------------------------------------------------------------------- -static void midiInputCallback(const MIDIPacketList *pktlist, void *readProcRefCon, - void *srcConnRefCon) +static void midiInputCallback(const MIDIPacketList *pktlist, void *readProcRefCon, void *srcConnRefCon) { AUScheduleMIDIEventBlock block = (__bridge AUScheduleMIDIEventBlock)readProcRefCon; if (!block) return; @@ -593,10 +604,8 @@ - (void)setupMIDI return; } - status = MIDIInputPortCreate(sMIDIClient, CFSTR("Input"), - midiInputCallback, - (__bridge void *)_scheduleMIDIBlock, - &sMIDIInputPort); + status = MIDIInputPortCreate(sMIDIClient, CFSTR("Input"), midiInputCallback, + (__bridge void *)_scheduleMIDIBlock, &sMIDIInputPort); if (status != noErr) { std::cout << "[auv3-standalone] Failed to create MIDI input port: " << status << std::endl; @@ -637,7 +646,8 @@ - (NSString *)settingsDirectory { if (!_settingsPath) { - NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); + NSArray *paths = + NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); NSString *appSupport = [paths firstObject]; _settingsPath = [appSupport stringByAppendingPathComponent:@"clap-wrapper-auv3-standalone"]; @@ -656,8 +666,8 @@ - (NSString *)settingsFilePath // Sanitize name for filesystem NSCharacterSet *illegal = [NSCharacterSet characterSetWithCharactersInString:@"/\\:"]; auName = [[auName componentsSeparatedByCharactersInSet:illegal] componentsJoinedByString:@"_"]; - return [[self settingsDirectory] stringByAppendingPathComponent: - [NSString stringWithFormat:@"%@.plist", auName]]; + return [[self settingsDirectory] + stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.plist", auName]]; } - (void)saveState @@ -728,41 +738,42 @@ - (void)_pollPreferredContentSize:(NSViewController *)vc retries:(int)retries // with updated preferredContentSize. if (_avAudioUnit) { - [_avAudioUnit.AUAudioUnit requestViewControllerWithCompletionHandler:^(AUViewControllerBase *freshVC) { - dispatch_async(dispatch_get_main_queue(), ^{ - if (freshVC) - { - NSSize size = freshVC.preferredContentSize; - std::cout << "[auv3-standalone] Re-requested VC preferredContentSize: " - << (int)size.width << "x" << (int)size.height << std::endl; - if (size.width > 0 && size.height > 0) - { - self->_auViewController.preferredContentSize = size; - [self _resizeWindowToFitGUI:self->_auViewController]; - } - } - }); - }]; + [_avAudioUnit.AUAudioUnit + requestViewControllerWithCompletionHandler:^(AUViewControllerBase *freshVC) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (freshVC) + { + NSSize size = freshVC.preferredContentSize; + std::cout << "[auv3-standalone] Re-requested VC preferredContentSize: " + << (int)size.width << "x" << (int)size.height << std::endl; + if (size.width > 0 && size.height > 0) + { + self->_auViewController.preferredContentSize = size; + [self _resizeWindowToFitGUI:self->_auViewController]; + } + } + }); + }]; } return; } dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(500 * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{ - NSSize size = vc.preferredContentSize; - NSSize windowContent = [[self window] contentView].frame.size; - - if (size.width > 0 && size.height > 0 && - ((int)size.width != (int)windowContent.width || - (int)size.height != (int)windowContent.height)) - { - [self _resizeWindowToFitGUI:vc]; - } - else - { - [self _pollPreferredContentSize:vc retries:retries - 1]; - } - }); + NSSize size = vc.preferredContentSize; + NSSize windowContent = [[self window] contentView].frame.size; + + if (size.width > 0 && size.height > 0 && + ((int)size.width != (int)windowContent.width || + (int)size.height != (int)windowContent.height)) + { + [self _resizeWindowToFitGUI:vc]; + } + else + { + [self _pollPreferredContentSize:vc retries:retries - 1]; + } + }); } // --------------------------------------------------------------------------- @@ -777,8 +788,8 @@ - (void)observeValueForKeyPath:(NSString *)keyPath if ([keyPath isEqualToString:@"preferredContentSize"] && object == _auViewController) { NSSize size = [(NSViewController *)object preferredContentSize]; - std::cout << "[auv3-standalone] preferredContentSize changed to " - << (int)size.width << "x" << (int)size.height << std::endl; + std::cout << "[auv3-standalone] preferredContentSize changed to " << (int)size.width << "x" + << (int)size.height << std::endl; if (size.width > 0 && size.height > 0) { dispatch_async(dispatch_get_main_queue(), ^{ diff --git a/src/wrapasstandalone.mm b/src/wrapasstandalone.mm index fa97b297..79691d6b 100644 --- a/src/wrapasstandalone.mm +++ b/src/wrapasstandalone.mm @@ -1,6 +1,6 @@ #import -int main(int argc, const char* argv[]) +int main(int argc, const char *argv[]) { return NSApplicationMain(argc, argv); } From 548c6845f22c024b3e9e12a92dd28c646bf1816a Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Mon, 13 Apr 2026 16:02:41 +0200 Subject: [PATCH 12/23] musicalContext, Timer and Bypass Parameter - Musical Context back in - Added Timer implementation - added Bypass Parameter detection and wired it up. --- src/detail/auv3/auv3_audiounit.mm | 158 +++++++++++++++++++++++++++-- src/detail/auv3/auv3_parameters.h | 11 +- src/detail/auv3/auv3_parameters.mm | 14 ++- src/detail/auv3/process.mm | 2 +- 4 files changed, 169 insertions(+), 16 deletions(-) diff --git a/src/detail/auv3/auv3_audiounit.mm b/src/detail/auv3/auv3_audiounit.mm index 84c9a127..c1a1e4ae 100644 --- a/src/detail/auv3/auv3_audiounit.mm +++ b/src/detail/auv3/auv3_audiounit.mm @@ -102,6 +102,7 @@ - (void)_wireParameterObserver; // Parameters AUParameterTree *_parameterTree = nil; + clap_id _bypassParamId = CLAP_INVALID_ID; // set if CLAP plugin has a bypass parameter // Observer token used as 'originator' when pushing parameter changes to the host. // This prevents the host from echoing the change back to our implementorValueObserver. AUParameterObserverToken _parameterObserverToken = nullptr; @@ -141,6 +142,15 @@ - (void)_wireParameterObserver; // Queue for audio -> UI thread parameter notifications ClapWrapper::detail::shared::fixedqueue _queueToUI; + // CLAP timer extension support — mirrors VST3/AAX TimerObject pattern + struct TimerObject + { + uint32_t period = 0; // 0 = unused slot (available for reuse) + uint64_t nexttick = 0; + clap_id timer_id = 0; + }; + std::vector _timerObjects; + // --- IHost --- void mark_dirty() override { @@ -176,13 +186,19 @@ void startIdleTimer() // objects on the main queue, no CLAP plugin calls. self->drainParameterQueue(); - // Do NOT call on_main_thread() while the plugin is processing. - // JUCE's on_main_thread() acquires locks that process() also needs — - // calling both concurrently (main thread vs render thread) deadlocks. - if (processing->load() || !flag->exchange(false)) return; + // Do NOT call into the plugin while processing — risk of deadlock + // (JUCE holds locks in on_main_thread that process() also needs). + if (processing->load()) return; + + // Service request_callback + if (flag->exchange(false)) + { + auto guard = plugin->AlwaysMainThread(); + plugin->_plugin->on_main_thread(plugin->_plugin); + } - auto guard = plugin->AlwaysMainThread(); - plugin->_plugin->on_main_thread(plugin->_plugin); + // Fire CLAP timers + self->fireTimers(); }); dispatch_resume(_idleTimer); } @@ -194,6 +210,7 @@ void stopIdleTimer() dispatch_source_cancel(_idleTimer); _idleTimer = nullptr; } + _timerObjects.clear(); } void setupWrapperSpecifics(const clap_plugin_t *plugin) override @@ -259,7 +276,9 @@ void setupMIDIBusses(const clap_plugin_t *plugin, const clap_plugin_note_ports_t void setupParameters(const clap_plugin_t *plugin, const clap_plugin_params_t *params) override { - _parameterTree = Clap::AUv3::createParameterTree(plugin, params); + auto result = Clap::AUv3::createParameterTree(plugin, params); + _parameterTree = result.tree; + _bypassParamId = result.bypassParamId; // Populate the parameter value and cookie caches with initial values if (params) @@ -293,7 +312,9 @@ void param_rescan(clap_param_rescan_flags flags) override if (flags & (CLAP_PARAM_RESCAN_ALL | CLAP_PARAM_RESCAN_INFO)) { // AUParameter properties (name, range, flags) are immutable — rebuild the entire tree. - _parameterTree = Clap::AUv3::createParameterTree(plug, params); + auto rescanResult = Clap::AUv3::createParameterTree(plug, params); + _parameterTree = rescanResult.tree; + _bypassParamId = rescanResult.bypassParamId; // Immediately replace the value provider with the cached version — // createParameterTree() wires a provider that calls get_value() directly, @@ -424,13 +445,59 @@ bool gui_request_hide() override bool register_timer(uint32_t period_ms, clap_id *timer_id) override { - return false; + if (period_ms < 30) period_ms = 30; + + // Reuse an unused slot + for (size_t i = 0; i < _timerObjects.size(); ++i) + { + auto &to = _timerObjects[i]; + if (to.period == 0) + { + to.timer_id = static_cast(i + 1000); + to.period = period_ms; + to.nexttick = os::getTickInMS() + period_ms; + *timer_id = to.timer_id; + return true; + } + } + + // Create new slot + auto newid = static_cast(_timerObjects.size() + 1000); + _timerObjects.push_back({period_ms, os::getTickInMS() + period_ms, newid}); + *timer_id = newid; + return true; } + bool unregister_timer(clap_id timer_id) override { + for (auto &to : _timerObjects) + { + if (to.timer_id == timer_id) + { + to.period = 0; + to.nexttick = 0; + return true; + } + } return false; } + void fireTimers() + { + if (_timerObjects.empty() || !_plugin || !_plugin->_ext._timer) return; + + auto now = os::getTickInMS(); + for (auto &to : _timerObjects) + { + if (to.period > 0 && to.nexttick <= now) + { + to.nexttick = now + to.period; + auto guard = _plugin->AlwaysMainThread(); + _plugin->_ext._timer->on_timer(_plugin->_plugin, to.timer_id); + } + } + } + const char *host_get_name() override { NSBundle *mainBundle = [NSBundle mainBundle]; @@ -545,6 +612,12 @@ void drainParameterQueue() atHostTime:0 eventType:AUParameterAutomationEventTypeValue]; } + // If this was the bypass parameter, notify KVO observers of shouldBypassEffect + if (evt._data._value.param_id == _bypassParamId && _audioUnit) + { + [_audioUnit willChangeValueForKey:@"shouldBypassEffect"]; + [_audioUnit didChangeValueForKey:@"shouldBypassEffect"]; + } break; } case queueEvent::type::editend: @@ -1424,6 +1497,73 @@ - (BOOL)providesUserInterface return (_impl && _impl->_plugin && _impl->_plugin->_ext._gui) ? YES : NO; } +// --- Bypass --- + +- (BOOL)shouldBypassEffect +{ + if (!_impl || _impl->_bypassParamId == CLAP_INVALID_ID) return NO; + + auto it = _impl->_paramValueCache.find(_impl->_bypassParamId); + if (it != _impl->_paramValueCache.end()) return it->second >= 0.5; + return NO; +} + +- (void)setShouldBypassEffect:(BOOL)shouldBypassEffect +{ + if (!_impl || _impl->_bypassParamId == CLAP_INVALID_ID) return; + + double newValue = shouldBypassEffect ? 1.0 : 0.0; + + // Update cache + _impl->_paramValueCache[_impl->_bypassParamId] = newValue; + + // Push to the CLAP plugin via params->flush() + if (_impl->_plugin && _impl->_plugin->_ext._params) + { + auto guard = _impl->_plugin->AlwaysMainThread(); + + clap_event_param_value_t ev = {}; + ev.header.size = sizeof(ev); + ev.header.type = CLAP_EVENT_PARAM_VALUE; + ev.header.space_id = CLAP_CORE_EVENT_SPACE_ID; + ev.header.time = 0; + ev.header.flags = 0; + ev.param_id = _impl->_bypassParamId; + ev.cookie = _impl->_paramCookieCache.count(_impl->_bypassParamId) + ? _impl->_paramCookieCache[_impl->_bypassParamId] + : nullptr; + ev.port_index = -1; + ev.key = -1; + ev.channel = -1; + ev.note_id = -1; + ev.value = newValue; + + clap_input_events_t in_events; + in_events.ctx = &ev; + in_events.size = [](const clap_input_events_t *) -> uint32_t { return 1; }; + in_events.get = [](const clap_input_events_t *list, uint32_t) -> const clap_event_header_t * { + return &static_cast(list->ctx)->header; + }; + clap_output_events_t out_events; + out_events.ctx = nullptr; + out_events.try_push = [](const clap_output_events_t *, const clap_event_header_t *) -> bool { + return false; + }; + _impl->_plugin->_ext._params->flush(_impl->_plugin->_plugin, &in_events, &out_events); + } + + // Update the AUParameter in the tree so the UI stays in sync + if (_impl->_parameterTree) + { + AUParameter *param = + [_impl->_parameterTree parameterWithAddress:(AUParameterAddress)_impl->_bypassParamId]; + if (param) + { + [param setValue:(AUValue)newValue originator:_impl->_parameterObserverToken]; + } + } +} + @end // Forward-declare private method used by ClapAUv3ContainerView diff --git a/src/detail/auv3/auv3_parameters.h b/src/detail/auv3/auv3_parameters.h index fed1490d..be2eabaa 100644 --- a/src/detail/auv3/auv3_parameters.h +++ b/src/detail/auv3/auv3_parameters.h @@ -19,11 +19,18 @@ namespace Clap::AUv3 { +struct ParameterTreeResult +{ + AUParameterTree *tree; + clap_id bypassParamId; // CLAP_INVALID_ID if no bypass parameter found +}; + // Build an AUParameterTree from the CLAP plugin's parameter extensions. // The tree groups parameters by their module path (split on '/'). // The callbacks (implementorValueObserver, implementorValueProvider, etc.) // are wired to the provided plugin and params extension. -AUParameterTree *createParameterTree(const clap_plugin_t *plugin, - const clap_plugin_params_t *params); +// Also detects the CLAP_PARAM_IS_BYPASS parameter and returns its ID. +ParameterTreeResult createParameterTree(const clap_plugin_t *plugin, + const clap_plugin_params_t *params); } // namespace Clap::AUv3 diff --git a/src/detail/auv3/auv3_parameters.mm b/src/detail/auv3/auv3_parameters.mm index ddb879ce..ae70fbe6 100644 --- a/src/detail/auv3/auv3_parameters.mm +++ b/src/detail/auv3/auv3_parameters.mm @@ -63,12 +63,15 @@ } }; -AUParameterTree *createParameterTree(const clap_plugin_t *plugin, const clap_plugin_params_t *params) +ParameterTreeResult createParameterTree(const clap_plugin_t *plugin, + const clap_plugin_params_t *params) { - if (!params) return [AUParameterTree createTreeWithChildren:@[]]; + if (!params) return {[AUParameterTree createTreeWithChildren:@[]], CLAP_INVALID_ID}; uint32_t numParams = params->count(plugin); - if (numParams == 0) return [AUParameterTree createTreeWithChildren:@[]]; + if (numParams == 0) return {[AUParameterTree createTreeWithChildren:@[]], CLAP_INVALID_ID}; + + clap_id bypassParamId = CLAP_INVALID_ID; // Root group node for building hierarchy GroupNode root; @@ -92,6 +95,9 @@ bool isHidden = (info.flags & CLAP_PARAM_IS_HIDDEN) != 0; bool isReadonly = (info.flags & CLAP_PARAM_IS_READONLY) != 0; bool isAutomatable = (info.flags & CLAP_PARAM_IS_AUTOMATABLE) != 0; + bool isBypass = (info.flags & CLAP_PARAM_IS_BYPASS) != 0; + + if (isBypass) bypassParamId = info.id; if (isHidden) continue; // skip hidden parameters @@ -196,7 +202,7 @@ return (AUValue)[string doubleValue]; }; - return tree; + return {tree, bypassParamId}; } } // namespace Clap::AUv3 diff --git a/src/detail/auv3/process.mm b/src/detail/auv3/process.mm index ed2b09ec..fbe40a93 100644 --- a/src/detail/auv3/process.mm +++ b/src/detail/auv3/process.mm @@ -402,7 +402,7 @@ inline clap_sectime doubleToSecTime(double t) } } - if (false && _musicalContextBlock) + if (_musicalContextBlock) { double tempo = 0; double tsigNum = 0; From 46b8e23a1690e035e147a445d5085641e47c0297 Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Mon, 13 Apr 2026 16:25:03 +0200 Subject: [PATCH 13/23] Note Expression added for DIALECT_CLAP Note Expressions are being added when the prefered dialect is not MIDI, but CLAP. --- src/detail/auv3/process.h | 14 +++++ src/detail/auv3/process.mm | 116 +++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) diff --git a/src/detail/auv3/process.h b/src/detail/auv3/process.h index fb3855b5..1f581ea2 100644 --- a/src/detail/auv3/process.h +++ b/src/detail/auv3/process.h @@ -110,6 +110,20 @@ class ProcessAdapter uint32_t _preferred_midi_dialect = CLAP_NOTE_DIALECT_CLAP; + // Active note tracking for note expression targeting + struct ActiveNote + { + bool used = false; + int32_t note_id; + int16_t port_index; + int16_t channel; + int16_t key; + }; + std::vector _activeNotes; + + void addToActiveNotes(const clap_event_note_t *note); + void removeFromActiveNotes(const clap_event_note_t *note); + AUHostTransportStateBlock __nullable _transportStateBlock = nil; AUHostMusicalContextBlock __nullable _musicalContextBlock = nil; diff --git a/src/detail/auv3/process.mm b/src/detail/auv3/process.mm index fbe40a93..f1e069d5 100644 --- a/src/detail/auv3/process.mm +++ b/src/detail/auv3/process.mm @@ -175,6 +175,9 @@ inline clap_sectime doubleToSecTime(double t) _events.reserve(8192); _eventindices.clear(); _eventindices.reserve(8192); + + _activeNotes.clear(); + _activeNotes.reserve(32); } void ProcessAdapter::setTransportStateBlock(AUHostTransportStateBlock __nullable block) @@ -275,6 +278,7 @@ inline clap_sectime doubleToSecTime(double t) _eventindices.emplace_back(_events.size()); _events.emplace_back(n); + addToActiveNotes(&n.note); break; } else if (strippedStatus == 0x08 || (strippedStatus == 0x09 && me.data[2] == 0)) // Note Off @@ -287,6 +291,55 @@ inline clap_sectime doubleToSecTime(double t) n.note.velocity = (strippedStatus == 0x08) ? (float)(me.data[2] & 0x7F) / 127.0f : 0.0f; n.note.channel = channel; + _eventindices.emplace_back(_events.size()); + _events.emplace_back(n); + removeFromActiveNotes(&n.note); + break; + } + else if (strippedStatus == 0x0A) // Poly Aftertouch → per-note PRESSURE + { + n.header.type = CLAP_EVENT_NOTE_EXPRESSION; + n.header.size = sizeof(clap_event_note_expression_t); + n.noteexpression.expression_id = CLAP_NOTE_EXPRESSION_PRESSURE; + n.noteexpression.port_index = 0; + n.noteexpression.channel = channel; + n.noteexpression.key = me.data[1] & 0x7F; + n.noteexpression.note_id = -1; + n.noteexpression.value = (double)(me.data[2] & 0x7F) / 127.0; + + _eventindices.emplace_back(_events.size()); + _events.emplace_back(n); + break; + } + else if (strippedStatus == 0x0D) // Channel Pressure → channel-wide PRESSURE + { + n.header.type = CLAP_EVENT_NOTE_EXPRESSION; + n.header.size = sizeof(clap_event_note_expression_t); + n.noteexpression.expression_id = CLAP_NOTE_EXPRESSION_PRESSURE; + n.noteexpression.port_index = 0; + n.noteexpression.channel = channel; + n.noteexpression.key = -1; // wildcard: all keys on this channel + n.noteexpression.note_id = -1; + n.noteexpression.value = (double)(me.data[1] & 0x7F) / 127.0; + + _eventindices.emplace_back(_events.size()); + _events.emplace_back(n); + break; + } + else if (strippedStatus == 0x0E) // Pitch Bend → channel-wide TUNING + { + n.header.type = CLAP_EVENT_NOTE_EXPRESSION; + n.header.size = sizeof(clap_event_note_expression_t); + n.noteexpression.expression_id = CLAP_NOTE_EXPRESSION_TUNING; + n.noteexpression.port_index = 0; + n.noteexpression.channel = channel; + n.noteexpression.key = -1; // wildcard: all keys on this channel + n.noteexpression.note_id = -1; + // MIDI pitch bend: 14-bit value (0-16383), center at 8192 + // Convert to CLAP semitones: ±2 semitones (MIDI default range) + uint16_t bendValue = ((uint16_t)(me.data[2] & 0x7F) << 7) | (me.data[1] & 0x7F); + n.noteexpression.value = ((double)bendValue - 8192.0) / 8192.0 * 2.0; + _eventindices.emplace_back(_events.size()); _events.emplace_back(n); break; @@ -545,6 +598,40 @@ inline clap_sectime doubleToSecTime(double t) } break; } + case CLAP_EVENT_NOTE_EXPRESSION: + { + if (!midiOutputEventBlock) break; + + auto &ne = evt.noteexpression; + + if (ne.expression_id == CLAP_NOTE_EXPRESSION_PRESSURE && ne.key >= 0) + { + // Per-note pressure → Poly Aftertouch + uint8_t data[3] = {(uint8_t)(0xA0 | (ne.channel >= 0 ? ne.channel & 0x0F : 0)), + (uint8_t)(ne.key & 0x7F), + (uint8_t)(std::clamp(ne.value, 0.0, 1.0) * 127.0)}; + midiOutputEventBlock(timestamp->mSampleTime + evt.header.time, 0, 3, data); + } + else if (ne.expression_id == CLAP_NOTE_EXPRESSION_PRESSURE && ne.key < 0) + { + // Channel-wide pressure → Channel Pressure + uint8_t data[2] = {(uint8_t)(0xD0 | (ne.channel >= 0 ? ne.channel & 0x0F : 0)), + (uint8_t)(std::clamp(ne.value, 0.0, 1.0) * 127.0)}; + midiOutputEventBlock(timestamp->mSampleTime + evt.header.time, 0, 2, data); + } + else if (ne.expression_id == CLAP_NOTE_EXPRESSION_TUNING) + { + // Tuning → Pitch Bend (±2 semitone range) + double normalized = std::clamp(ne.value / 2.0, -1.0, 1.0); + uint16_t bendValue = (uint16_t)((normalized + 1.0) * 8192.0); + if (bendValue > 16383) bendValue = 16383; + uint8_t data[3] = {(uint8_t)(0xE0 | (ne.channel >= 0 ? ne.channel & 0x0F : 0)), + (uint8_t)(bendValue & 0x7F), (uint8_t)((bendValue >> 7) & 0x7F)}; + midiOutputEventBlock(timestamp->mSampleTime + evt.header.time, 0, 3, data); + } + // Other expression types (volume, pan, vibrato, brightness) have no MIDI 1.0 equivalent + break; + } default: break; } @@ -637,6 +724,35 @@ inline clap_sectime doubleToSecTime(double t) return false; } +void ProcessAdapter::addToActiveNotes(const clap_event_note_t *note) +{ + for (auto &i : _activeNotes) + { + if (!i.used) + { + i.note_id = note->note_id; + i.port_index = note->port_index; + i.channel = note->channel; + i.key = note->key; + i.used = true; + return; + } + } + _activeNotes.emplace_back( + ActiveNote{true, note->note_id, note->port_index, note->channel, note->key}); +} + +void ProcessAdapter::removeFromActiveNotes(const clap_event_note_t *note) +{ + for (auto &i : _activeNotes) + { + if (i.used && i.port_index == note->port_index && i.channel == note->channel && i.key == note->key) + { + i.used = false; + } + } +} + } // namespace Clap::AUv3 #pragma clang diagnostic pop From 3f6a01561692a419cf6dab3537173601f3f181d5 Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Mon, 13 Apr 2026 16:26:42 +0200 Subject: [PATCH 14/23] Formatting ..of course.. --- src/detail/auv3/auv3_audiounit.mm | 10 ++++------ src/detail/auv3/auv3_parameters.mm | 13 +++++++++---- src/detail/auv3/process.mm | 3 +-- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/detail/auv3/auv3_audiounit.mm b/src/detail/auv3/auv3_audiounit.mm index c1a1e4ae..3e5d2c2e 100644 --- a/src/detail/auv3/auv3_audiounit.mm +++ b/src/detail/auv3/auv3_audiounit.mm @@ -1541,14 +1541,12 @@ - (void)setShouldBypassEffect:(BOOL)shouldBypassEffect clap_input_events_t in_events; in_events.ctx = &ev; in_events.size = [](const clap_input_events_t *) -> uint32_t { return 1; }; - in_events.get = [](const clap_input_events_t *list, uint32_t) -> const clap_event_header_t * { - return &static_cast(list->ctx)->header; - }; + in_events.get = [](const clap_input_events_t *list, uint32_t) -> const clap_event_header_t * + { return &static_cast(list->ctx)->header; }; clap_output_events_t out_events; out_events.ctx = nullptr; - out_events.try_push = [](const clap_output_events_t *, const clap_event_header_t *) -> bool { - return false; - }; + out_events.try_push = [](const clap_output_events_t *, const clap_event_header_t *) -> bool + { return false; }; _impl->_plugin->_ext._params->flush(_impl->_plugin->_plugin, &in_events, &out_events); } diff --git a/src/detail/auv3/auv3_parameters.mm b/src/detail/auv3/auv3_parameters.mm index ae70fbe6..072737bc 100644 --- a/src/detail/auv3/auv3_parameters.mm +++ b/src/detail/auv3/auv3_parameters.mm @@ -63,13 +63,18 @@ } }; -ParameterTreeResult createParameterTree(const clap_plugin_t *plugin, - const clap_plugin_params_t *params) +ParameterTreeResult createParameterTree(const clap_plugin_t *plugin, const clap_plugin_params_t *params) { - if (!params) return {[AUParameterTree createTreeWithChildren:@[]], CLAP_INVALID_ID}; + if (!params) return + { + [AUParameterTree createTreeWithChildren:@[]], CLAP_INVALID_ID + }; uint32_t numParams = params->count(plugin); - if (numParams == 0) return {[AUParameterTree createTreeWithChildren:@[]], CLAP_INVALID_ID}; + if (numParams == 0) return + { + [AUParameterTree createTreeWithChildren:@[]], CLAP_INVALID_ID + }; clap_id bypassParamId = CLAP_INVALID_ID; diff --git a/src/detail/auv3/process.mm b/src/detail/auv3/process.mm index f1e069d5..6b1788da 100644 --- a/src/detail/auv3/process.mm +++ b/src/detail/auv3/process.mm @@ -738,8 +738,7 @@ inline clap_sectime doubleToSecTime(double t) return; } } - _activeNotes.emplace_back( - ActiveNote{true, note->note_id, note->port_index, note->channel, note->key}); + _activeNotes.emplace_back(ActiveNote{true, note->note_id, note->port_index, note->channel, note->key}); } void ProcessAdapter::removeFromActiveNotes(const clap_event_note_t *note) From 5c83bcba553dc70702f0ddeec419e9b6a5976c68 Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Mon, 13 Apr 2026 20:01:58 +0200 Subject: [PATCH 15/23] Update top_level_default.cmake --- cmake/top_level_default.cmake | 64 ++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/cmake/top_level_default.cmake b/cmake/top_level_default.cmake index bbef82a7..ae4933f5 100644 --- a/cmake/top_level_default.cmake +++ b/cmake/top_level_default.cmake @@ -50,40 +50,42 @@ if (PROJECT_IS_TOP_LEVEL) endif() endif() - if (${CLAP_WRAPPER_BUILD_AUV3}) - add_executable(${pluginname}_as_auv3) - target_add_auv3_wrapper( - TARGET ${pluginname}_as_auv3 - OUTPUT_NAME "${CLAP_WRAPPER_OUTPUT_NAME}" - BUNDLE_IDENTIFIER "${CLAP_WRAPPER_BUNDLE_IDENTIFIER}" - BUNDLE_VERSION "${CLAP_WRAPPER_BUNDLE_VERSION}" + if (APPLE) + if (${CLAP_WRAPPER_BUILD_AUV3}) + add_executable(${pluginname}_as_auv3) + target_add_auv3_wrapper( + TARGET ${pluginname}_as_auv3 + OUTPUT_NAME "${CLAP_WRAPPER_OUTPUT_NAME}" + BUNDLE_IDENTIFIER "${CLAP_WRAPPER_BUNDLE_IDENTIFIER}" + BUNDLE_VERSION "${CLAP_WRAPPER_BUNDLE_VERSION}" - INSTRUMENT_TYPE "aumu" - MANUFACTURER_NAME "schnuf.org" - MANUFACTURER_CODE "clAA" - SUBTYPE_CODE "gWwp" - ) + INSTRUMENT_TYPE "aumu" + MANUFACTURER_NAME "schnuf.org" + MANUFACTURER_CODE "clAA" + SUBTYPE_CODE "gWwp" + ) - # Embed the installed .clap into the appex so it can find the plugin at runtime - set(_clap_bundle_name "${CLAP_WRAPPER_OUTPUT_NAME}.clap") - set(_clap_embed_dst "$/Contents/PlugIns/${_clap_bundle_name}") - add_custom_command(TARGET ${pluginname}_as_auv3 POST_BUILD - COMMAND ${CMAKE_COMMAND} "-DCLAP_NAME=${_clap_bundle_name}" "-DDST=${_clap_embed_dst}" - -P "${CLAP_WRAPPER_CMAKE_CURRENT_SOURCE_DIR}/cmake/embed_clap.cmake" - COMMENT "Embedding ${_clap_bundle_name} in AUv3 appex" - ) + # Embed the installed .clap into the appex so it can find the plugin at runtime + set(_clap_bundle_name "${CLAP_WRAPPER_OUTPUT_NAME}.clap") + set(_clap_embed_dst "$/Contents/PlugIns/${_clap_bundle_name}") + add_custom_command(TARGET ${pluginname}_as_auv3 POST_BUILD + COMMAND ${CMAKE_COMMAND} "-DCLAP_NAME=${_clap_bundle_name}" "-DDST=${_clap_embed_dst}" + -P "${CLAP_WRAPPER_CMAKE_CURRENT_SOURCE_DIR}/cmake/embed_clap.cmake" + COMMENT "Embedding ${_clap_bundle_name} in AUv3 appex" + ) - add_executable(${pluginname}_as_auv3_standalone) - target_add_auv3_standalone_wrapper( - TARGET ${pluginname}_as_auv3_standalone - OUTPUT_NAME "${CLAP_WRAPPER_OUTPUT_NAME} AUv3" - BUNDLE_IDENTIFIER "${CLAP_WRAPPER_BUNDLE_IDENTIFIER}" - BUNDLE_VERSION "${CLAP_WRAPPER_BUNDLE_VERSION}" - AUV3_TARGET ${pluginname}_as_auv3 - AU_TYPE "aumu" - AU_SUBTYPE "gWwp" - AU_MANUFACTURER "clAA" - ) + add_executable(${pluginname}_as_auv3_standalone) + target_add_auv3_standalone_wrapper( + TARGET ${pluginname}_as_auv3_standalone + OUTPUT_NAME "${CLAP_WRAPPER_OUTPUT_NAME} AUv3" + BUNDLE_IDENTIFIER "${CLAP_WRAPPER_BUNDLE_IDENTIFIER}" + BUNDLE_VERSION "${CLAP_WRAPPER_BUNDLE_VERSION}" + AUV3_TARGET ${pluginname}_as_auv3 + AU_TYPE "aumu" + AU_SUBTYPE "gWwp" + AU_MANUFACTURER "clAA" + ) + endif() endif() if (${CLAP_WRAPPER_BUILD_STANDALONE}) From cae8ee4476bb361994794e381a278cdbefc06188 Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Mon, 13 Apr 2026 20:05:23 +0200 Subject: [PATCH 16/23] clang formatted formatted, although before it should have already happen --- src/detail/auv3/auv3_audiounit.h | 5 ++-- src/detail/auv3/auv3_parameters.h | 3 +- src/detail/auv3/build-helper/build-helper.cpp | 30 ++++++++++--------- src/detail/auv3/process.h | 7 ++--- .../macos/auv3/AUv3HostAppDelegate.h | 2 +- 5 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/detail/auv3/auv3_audiounit.h b/src/detail/auv3/auv3_audiounit.h index f7f562b7..4e9640fc 100644 --- a/src/detail/auv3/auv3_audiounit.h +++ b/src/detail/auv3/auv3_audiounit.h @@ -27,7 +27,7 @@ // Apple's AUv3 model requires the NSExtensionPrincipalClass to be the // AUViewController subclass which also conforms to AUAudioUnitFactory. @interface ClapAUv3ViewController : AUViewController -@property (nonatomic, strong) ClapAUv3AudioUnit *audioUnit; +@property(nonatomic, strong) ClapAUv3AudioUnit *audioUnit; // Subclasses (generated by build-helper) override this to provide plugin-specific info - (AUAudioUnit *)createAudioUnitWithComponentDescription:(AudioComponentDescription)desc error:(NSError **)error; @@ -35,7 +35,7 @@ @interface ClapAUv3AudioUnit : AUAudioUnit { - @package + @package // Weak back-reference to the factory VC that created this AU. // Set by [ClapAUv3ViewController setAudioUnit:] during factory creation. // Used by requestViewControllerWithCompletionHandler: so the host can @@ -67,5 +67,4 @@ // for gui_request_resize to set preferredContentSize on the VC. - (void)setViewController:(ClapAUv3ViewController *)vc; - @end diff --git a/src/detail/auv3/auv3_parameters.h b/src/detail/auv3/auv3_parameters.h index be2eabaa..5784dfdc 100644 --- a/src/detail/auv3/auv3_parameters.h +++ b/src/detail/auv3/auv3_parameters.h @@ -30,7 +30,6 @@ struct ParameterTreeResult // The callbacks (implementorValueObserver, implementorValueProvider, etc.) // are wired to the provided plugin and params extension. // Also detects the CLAP_PARAM_IS_BYPASS parameter and returns its ID. -ParameterTreeResult createParameterTree(const clap_plugin_t *plugin, - const clap_plugin_params_t *params); +ParameterTreeResult createParameterTree(const clap_plugin_t *plugin, const clap_plugin_params_t *params); } // namespace Clap::AUv3 diff --git a/src/detail/auv3/build-helper/build-helper.cpp b/src/detail/auv3/build-helper/build-helper.cpp index 9d381c58..e261a26f 100644 --- a/src/detail/auv3/build-helper/build-helper.cpp +++ b/src/detail/auv3/build-helper/build-helper.cpp @@ -431,22 +431,24 @@ int main(int argc, char **argv) // Generate a unique AUViewController subclass per plugin // This class serves as both the view controller AND the AUAudioUnitFactory cppf << "// ViewController/Factory for '" << u.name << "' (" << u.type << "/" << u.subt << ")\n"; - cppf << "@interface " << vcName - << " : ClapAUv3ViewController\n" + cppf << "@interface " << vcName << " : ClapAUv3ViewController\n" << "@end\n\n"; cppf << "@implementation " << vcName << "\n"; - cppf << "- (AUAudioUnit *)createAudioUnitWithComponentDescription:(AudioComponentDescription)desc\n" - << " error:(NSError **)error {\n" - << " ClapAUv3AudioUnit *au = [[ClapAUv3AudioUnit alloc] initWithComponentDescription:desc\n" - << " options:0\n" - << " error:error\n" - << " clapName:@\"" << u.clapname << "\"\n" - << " clapId:@\"" << u.clapid << "\"\n" - << " clapIndex:" << idx << "];\n" - << " self.audioUnit = au;\n" - << " return au;\n" - << "}\n" - << "@end\n\n"; + cppf + << "- (AUAudioUnit *)createAudioUnitWithComponentDescription:(AudioComponentDescription)desc\n" + << " error:(NSError **)error {\n" + << " ClapAUv3AudioUnit *au = [[ClapAUv3AudioUnit alloc] initWithComponentDescription:desc\n" + << " options:0\n" + << " error:error\n" + << " clapName:@\"" << u.clapname + << "\"\n" + << " clapId:@\"" << u.clapid + << "\"\n" + << " clapIndex:" << idx << "];\n" + << " self.audioUnit = au;\n" + << " return au;\n" + << "}\n" + << "@end\n\n"; idx++; } diff --git a/src/detail/auv3/process.h b/src/detail/auv3/process.h index 1f581ea2..05ff3ad0 100644 --- a/src/detail/auv3/process.h +++ b/src/detail/auv3/process.h @@ -54,10 +54,9 @@ class ProcessAdapter // Main render call - invoked from the AUv3 internalRenderBlock. // Translates AUv3 events, pulls input, calls CLAP process, and writes output. - AUAudioUnitStatus process(AudioUnitRenderActionFlags *actionFlags, - const AudioTimeStamp *timestamp, AVAudioFrameCount frameCount, - NSInteger outputBusNumber, AudioBufferList *outputData, - const AURenderEvent *realtimeEventListHead, + AUAudioUnitStatus process(AudioUnitRenderActionFlags *actionFlags, const AudioTimeStamp *timestamp, + AVAudioFrameCount frameCount, NSInteger outputBusNumber, + AudioBufferList *outputData, const AURenderEvent *realtimeEventListHead, AURenderPullInputBlock __unsafe_unretained pullInputBlock); // Provide transport/musical context from the host diff --git a/src/detail/standalone/macos/auv3/AUv3HostAppDelegate.h b/src/detail/standalone/macos/auv3/AUv3HostAppDelegate.h index b80fff76..8efcee98 100644 --- a/src/detail/standalone/macos/auv3/AUv3HostAppDelegate.h +++ b/src/detail/standalone/macos/auv3/AUv3HostAppDelegate.h @@ -4,6 +4,6 @@ @interface AUv3HostAppDelegate : NSObject -@property (nonatomic, weak) IBOutlet NSWindow *window; +@property(nonatomic, weak) IBOutlet NSWindow *window; @end From 98ee9736d751ea30e4ac43c64d2c1f6d1be4043b Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:11:17 +0200 Subject: [PATCH 17/23] added channelCapabilities matching the CLAP bus count. --- src/detail/auv3/auv3_audiounit.mm | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/detail/auv3/auv3_audiounit.mm b/src/detail/auv3/auv3_audiounit.mm index 3e5d2c2e..95fdbed3 100644 --- a/src/detail/auv3/auv3_audiounit.mm +++ b/src/detail/auv3/auv3_audiounit.mm @@ -1135,6 +1135,36 @@ - (NSTimeInterval)tailTime return 0; } +- (NSArray *)channelCapabilities +{ + if (!_impl) return nil; + + // Build the channel capability pairs from CLAP audio port info, matching AUv2's + // SupportedNumChannels() approach. Each pair is [inChannels, outChannels]. + // If there are no input ports, report 0 for input (generator/instrument). + NSMutableArray *caps = [NSMutableArray new]; + + std::vector inCounts, outCounts; + for (auto &bus : _impl->_inputBusInfos) + inCounts.push_back((int)bus.channelCount); + for (auto &bus : _impl->_outputBusInfos) + outCounts.push_back((int)bus.channelCount); + + if (inCounts.empty()) inCounts.push_back(0); + if (outCounts.empty()) outCounts.push_back(0); + + for (int ic : inCounts) + { + for (int oc : outCounts) + { + [caps addObject:@(ic)]; + [caps addObject:@(oc)]; + } + } + + return caps; +} + - (BOOL)shouldChangeToFormat:(AVAudioFormat *)format forBus:(AUAudioUnitBus *)bus { if (!_impl) return NO; From 53b1df54466914c9aadce38051293033c7da6b42 Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:13:40 +0200 Subject: [PATCH 18/23] old manufacturer resetting default manufacturer --- cmake/top_level_default.cmake | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmake/top_level_default.cmake b/cmake/top_level_default.cmake index ae4933f5..59af4c87 100644 --- a/cmake/top_level_default.cmake +++ b/cmake/top_level_default.cmake @@ -60,9 +60,9 @@ if (PROJECT_IS_TOP_LEVEL) BUNDLE_VERSION "${CLAP_WRAPPER_BUNDLE_VERSION}" INSTRUMENT_TYPE "aumu" - MANUFACTURER_NAME "schnuf.org" - MANUFACTURER_CODE "clAA" - SUBTYPE_CODE "gWwp" + MANUFACTURER_NAME "cleveraudio.org" + MANUFACTURER_CODE "clAd" + SUBTYPE_CODE "gWrq" ) # Embed the installed .clap into the appex so it can find the plugin at runtime From 22f07fcbd6b65cfec85eac31d6d0c646b758d2a1 Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Tue, 14 Apr 2026 10:34:49 +0200 Subject: [PATCH 19/23] Latency and Timers added missing latency communication --- src/detail/auv3/auv3_audiounit.mm | 33 ++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/src/detail/auv3/auv3_audiounit.mm b/src/detail/auv3/auv3_audiounit.mm index 95fdbed3..e38ac733 100644 --- a/src/detail/auv3/auv3_audiounit.mm +++ b/src/detail/auv3/auv3_audiounit.mm @@ -186,8 +186,13 @@ void startIdleTimer() // objects on the main queue, no CLAP plugin calls. self->drainParameterQueue(); - // Do NOT call into the plugin while processing — risk of deadlock - // (JUCE holds locks in on_main_thread that process() also needs). + // Fire CLAP timers — safe while processing since timer callbacks + // run on the main thread, not the audio thread. + self->fireTimers(); + + // Do NOT call on_main_thread() while the plugin is processing. + // JUCE's on_main_thread() acquires locks that process() also needs — + // calling both concurrently (main thread vs render thread) deadlocks. if (processing->load()) return; // Service request_callback @@ -196,9 +201,6 @@ void startIdleTimer() auto guard = plugin->AlwaysMainThread(); plugin->_plugin->on_main_thread(plugin->_plugin); } - - // Fire CLAP timers - self->fireTimers(); }); dispatch_resume(_idleTimer); } @@ -398,6 +400,13 @@ void latency_changed() override auto mainGuard = _plugin->AlwaysMainThread(); _cachedLatencySamples = _plugin->_ext._latency->get(_plugin->_plugin); AUV3LOG("IHost::latency_changed() -> %u samples", _cachedLatencySamples); + + // Notify the AUv3 host via KVO so it re-reads the latency property + if (_audioUnit) + { + [_audioUnit willChangeValueForKey:@"latency"]; + [_audioUnit didChangeValueForKey:@"latency"]; + } } } @@ -1357,6 +1366,20 @@ - (BOOL)allocateRenderResourcesAndReturnError:(NSError **)outError // Activate the CLAP plugin AUV3LOG("allocateRenderResources: calling activate()"); _impl->_plugin->activate(); + + // Re-cache latency — the plugin may have set it during activation + if (_impl->_plugin->_ext._latency) + { + uint32_t newLatency = _impl->_plugin->_ext._latency->get(_impl->_plugin->_plugin); + if (newLatency != _impl->_cachedLatencySamples) + { + _impl->_cachedLatencySamples = newLatency; + AUV3LOG("allocateRenderResources: latency updated to %u samples after activate", newLatency); + [self willChangeValueForKey:@"latency"]; + [self didChangeValueForKey:@"latency"]; + } + } + AUV3LOG("allocateRenderResources: calling start_processing()"); _impl->_plugin->start_processing(); _impl->_initialized = true; From 3ee86a570430277840db3fb3b676b7740a470360 Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Tue, 14 Apr 2026 10:36:21 +0200 Subject: [PATCH 20/23] clang formatting --- src/detail/aax/categories.cpp | 6 +++--- src/detail/auv2/auv2_base_classes.h | 2 +- src/detail/auv2/process.h | 2 +- src/detail/auv3/auv3_audiounit.mm | 6 ++---- src/detail/vst3/categories.cpp | 6 +++--- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/detail/aax/categories.cpp b/src/detail/aax/categories.cpp index 87cd7f97..f09aef39 100644 --- a/src/detail/aax/categories.cpp +++ b/src/detail/aax/categories.cpp @@ -81,9 +81,9 @@ uint32_t clapCategoriesToAAX(const char *const *clap_categories) LOGDETAIL("creating categories:"); for (auto f = clap_categories; f && *f; ++f) { - auto it = - std::find_if(std::begin(translationTable), std::end(translationTable), [&](const auto &entry) - { return entry.clapattribute && !strcmp(entry.clapattribute, *f); }); + auto it = std::find_if(std::begin(translationTable), std::end(translationTable), + [&](const auto &entry) + { return entry.clapattribute && !strcmp(entry.clapattribute, *f); }); if (it != std::end(translationTable)) { diff --git a/src/detail/auv2/auv2_base_classes.h b/src/detail/auv2/auv2_base_classes.h index c0d682c1..66349bf8 100644 --- a/src/detail/auv2/auv2_base_classes.h +++ b/src/detail/auv2/auv2_base_classes.h @@ -522,7 +522,7 @@ class WrapAsAUV2 : public ausdk::AUBase, { return false; } - void SetBypassEffect(bool bypass) {}; + void SetBypassEffect(bool bypass){}; // --------------- internals diff --git a/src/detail/auv2/process.h b/src/detail/auv2/process.h index 7a6c8bc3..71aedaee 100644 --- a/src/detail/auv2/process.h +++ b/src/detail/auv2/process.h @@ -83,7 +83,7 @@ typedef union clap_multi_event class IMIDIOutputs { public: - virtual ~IMIDIOutputs() {}; + virtual ~IMIDIOutputs(){}; virtual void send(const clap_multi_event_t &event) = 0; }; diff --git a/src/detail/auv3/auv3_audiounit.mm b/src/detail/auv3/auv3_audiounit.mm index e38ac733..92de9caa 100644 --- a/src/detail/auv3/auv3_audiounit.mm +++ b/src/detail/auv3/auv3_audiounit.mm @@ -1154,10 +1154,8 @@ - (NSTimeInterval)tailTime NSMutableArray *caps = [NSMutableArray new]; std::vector inCounts, outCounts; - for (auto &bus : _impl->_inputBusInfos) - inCounts.push_back((int)bus.channelCount); - for (auto &bus : _impl->_outputBusInfos) - outCounts.push_back((int)bus.channelCount); + for (auto &bus : _impl->_inputBusInfos) inCounts.push_back((int)bus.channelCount); + for (auto &bus : _impl->_outputBusInfos) outCounts.push_back((int)bus.channelCount); if (inCounts.empty()) inCounts.push_back(0); if (outCounts.empty()) outCounts.push_back(0); diff --git a/src/detail/vst3/categories.cpp b/src/detail/vst3/categories.cpp index 32d71af3..03e26a42 100644 --- a/src/detail/vst3/categories.cpp +++ b/src/detail/vst3/categories.cpp @@ -116,9 +116,9 @@ std::string clapCategoriesToVST3(const char *const *clap_categories) std::vector r; for (auto f = clap_categories; f && *f; ++f) { - auto it = - std::find_if(std::begin(translationTable), std::end(translationTable), [&](const auto &entry) - { return entry.clapattribute && !strcmp(entry.clapattribute, *f); }); + auto it = std::find_if(std::begin(translationTable), std::end(translationTable), + [&](const auto &entry) + { return entry.clapattribute && !strcmp(entry.clapattribute, *f); }); if (it != std::end(translationTable)) { From 431ebb75ac7f12ce909db0efad2b895b2ab132da Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Tue, 14 Apr 2026 11:41:35 +0200 Subject: [PATCH 21/23] formatting? --- src/detail/aax/categories.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/detail/aax/categories.cpp b/src/detail/aax/categories.cpp index f09aef39..23b7a625 100644 --- a/src/detail/aax/categories.cpp +++ b/src/detail/aax/categories.cpp @@ -81,9 +81,9 @@ uint32_t clapCategoriesToAAX(const char *const *clap_categories) LOGDETAIL("creating categories:"); for (auto f = clap_categories; f && *f; ++f) { - auto it = std::find_if(std::begin(translationTable), std::end(translationTable), - [&](const auto &entry) - { return entry.clapattribute && !strcmp(entry.clapattribute, *f); }); + auto it = + std::find_if(std::begin(translationTable), std::end(translationTable),[&](const auto &entry) + { return entry.clapattribute && !strcmp(entry.clapattribute, *f); }); if (it != std::end(translationTable)) { From 267c708c21c6b0694cf4105dd8e4e49dac67e180 Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Tue, 14 Apr 2026 11:45:51 +0200 Subject: [PATCH 22/23] clang-format sxs Let's see if that works out now. --- src/detail/aax/categories.cpp | 6 +++--- src/detail/auv2/auv2_base_classes.h | 2 +- src/detail/auv2/process.h | 2 +- src/detail/vst3/categories.cpp | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/detail/aax/categories.cpp b/src/detail/aax/categories.cpp index 23b7a625..768a1119 100644 --- a/src/detail/aax/categories.cpp +++ b/src/detail/aax/categories.cpp @@ -81,9 +81,9 @@ uint32_t clapCategoriesToAAX(const char *const *clap_categories) LOGDETAIL("creating categories:"); for (auto f = clap_categories; f && *f; ++f) { - auto it = - std::find_if(std::begin(translationTable), std::end(translationTable),[&](const auto &entry) - { return entry.clapattribute && !strcmp(entry.clapattribute, *f); }); + auto it = std::find_if( + std::begin(translationTable), std::end(translationTable), + [&](const auto &entry) { return entry.clapattribute && !strcmp(entry.clapattribute, *f); }); if (it != std::end(translationTable)) { diff --git a/src/detail/auv2/auv2_base_classes.h b/src/detail/auv2/auv2_base_classes.h index 66349bf8..c0d682c1 100644 --- a/src/detail/auv2/auv2_base_classes.h +++ b/src/detail/auv2/auv2_base_classes.h @@ -522,7 +522,7 @@ class WrapAsAUV2 : public ausdk::AUBase, { return false; } - void SetBypassEffect(bool bypass){}; + void SetBypassEffect(bool bypass) {}; // --------------- internals diff --git a/src/detail/auv2/process.h b/src/detail/auv2/process.h index 71aedaee..7a6c8bc3 100644 --- a/src/detail/auv2/process.h +++ b/src/detail/auv2/process.h @@ -83,7 +83,7 @@ typedef union clap_multi_event class IMIDIOutputs { public: - virtual ~IMIDIOutputs(){}; + virtual ~IMIDIOutputs() {}; virtual void send(const clap_multi_event_t &event) = 0; }; diff --git a/src/detail/vst3/categories.cpp b/src/detail/vst3/categories.cpp index 03e26a42..898f92b8 100644 --- a/src/detail/vst3/categories.cpp +++ b/src/detail/vst3/categories.cpp @@ -116,9 +116,9 @@ std::string clapCategoriesToVST3(const char *const *clap_categories) std::vector r; for (auto f = clap_categories; f && *f; ++f) { - auto it = std::find_if(std::begin(translationTable), std::end(translationTable), - [&](const auto &entry) - { return entry.clapattribute && !strcmp(entry.clapattribute, *f); }); + auto it = std::find_if( + std::begin(translationTable), std::end(translationTable), + [&](const auto &entry) { return entry.clapattribute && !strcmp(entry.clapattribute, *f); }); if (it != std::end(translationTable)) { From 0767529f0c6ea77bb4fc4005e1654f98a808fba9 Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Tue, 14 Apr 2026 11:56:46 +0200 Subject: [PATCH 23/23] clang format changing behavior good job, llvm... --- src/detail/aax/categories.cpp | 6 +++--- src/detail/vst3/categories.cpp | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/detail/aax/categories.cpp b/src/detail/aax/categories.cpp index 768a1119..87cd7f97 100644 --- a/src/detail/aax/categories.cpp +++ b/src/detail/aax/categories.cpp @@ -81,9 +81,9 @@ uint32_t clapCategoriesToAAX(const char *const *clap_categories) LOGDETAIL("creating categories:"); for (auto f = clap_categories; f && *f; ++f) { - auto it = std::find_if( - std::begin(translationTable), std::end(translationTable), - [&](const auto &entry) { return entry.clapattribute && !strcmp(entry.clapattribute, *f); }); + auto it = + std::find_if(std::begin(translationTable), std::end(translationTable), [&](const auto &entry) + { return entry.clapattribute && !strcmp(entry.clapattribute, *f); }); if (it != std::end(translationTable)) { diff --git a/src/detail/vst3/categories.cpp b/src/detail/vst3/categories.cpp index 898f92b8..32d71af3 100644 --- a/src/detail/vst3/categories.cpp +++ b/src/detail/vst3/categories.cpp @@ -116,9 +116,9 @@ std::string clapCategoriesToVST3(const char *const *clap_categories) std::vector r; for (auto f = clap_categories; f && *f; ++f) { - auto it = std::find_if( - std::begin(translationTable), std::end(translationTable), - [&](const auto &entry) { return entry.clapattribute && !strcmp(entry.clapattribute, *f); }); + auto it = + std::find_if(std::begin(translationTable), std::end(translationTable), [&](const auto &entry) + { return entry.clapattribute && !strcmp(entry.clapattribute, *f); }); if (it != std::end(translationTable)) {