From bd701cb5f260de5f2d03632606da31472c91d07b Mon Sep 17 00:00:00 2001 From: essej Date: Tue, 14 Jun 2022 19:24:15 -0400 Subject: [PATCH] git subrepo clone https://github.com/free-audio/clap-juce-extensions deps/clap-juce-extensions subrepo: subdir: "deps/clap-juce-extensions" merged: "6489846d0" upstream: origin: "https://github.com/free-audio/clap-juce-extensions" branch: "main" commit: "6489846d0" git-subrepo: version: "0.4.3" origin: "https://github.com/ingydotnet/git-subrepo.git" commit: "2f68596" --- deps/clap-juce-extensions/.clang-format | 59 + deps/clap-juce-extensions/.gitmodules | 6 + deps/clap-juce-extensions/.gitrepo | 12 + deps/clap-juce-extensions/CMakeLists.txt | 138 ++ deps/clap-juce-extensions/LICENSE.md | 9 + deps/clap-juce-extensions/README.md | 171 +++ deps/clap-juce-extensions/clap-libs/clap | 1 + .../clap-libs/clap-helpers | 1 + .../clap-juce-extensions.h | 133 ++ .../src/extensions/clap-juce-extensions.cpp | 15 + .../src/wrapper/clap-juce-mac.mm | 13 + .../src/wrapper/clap-juce-wrapper.cpp | 1109 +++++++++++++++++ 12 files changed, 1667 insertions(+) create mode 100644 deps/clap-juce-extensions/.clang-format create mode 100644 deps/clap-juce-extensions/.gitmodules create mode 100644 deps/clap-juce-extensions/.gitrepo create mode 100644 deps/clap-juce-extensions/CMakeLists.txt create mode 100644 deps/clap-juce-extensions/LICENSE.md create mode 100644 deps/clap-juce-extensions/README.md create mode 160000 deps/clap-juce-extensions/clap-libs/clap create mode 160000 deps/clap-juce-extensions/clap-libs/clap-helpers create mode 100644 deps/clap-juce-extensions/include/clap-juce-extensions/clap-juce-extensions.h create mode 100644 deps/clap-juce-extensions/src/extensions/clap-juce-extensions.cpp create mode 100644 deps/clap-juce-extensions/src/wrapper/clap-juce-mac.mm create mode 100644 deps/clap-juce-extensions/src/wrapper/clap-juce-wrapper.cpp diff --git a/deps/clap-juce-extensions/.clang-format b/deps/clap-juce-extensions/.clang-format new file mode 100644 index 0000000..a7026a8 --- /dev/null +++ b/deps/clap-juce-extensions/.clang-format @@ -0,0 +1,59 @@ +--- +BasedOnStyle: LLVM +IndentWidth: 4 +--- +Language: Cpp +BasedOnStyle: LLVM +IndentWidth: 4 +AlignAfterOpenBracket: Align +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: true + AfterClass: true + AfterControlStatement: Always + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: true + BeforeElse: true + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +ColumnLimit: 100 +SortIncludes: false +--- +Language: ObjC +BasedOnStyle: LLVM +IndentWidth: 4 +AlignAfterOpenBracket: Align +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: true + AfterClass: true + AfterControlStatement: Always + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: true + BeforeElse: true + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +ColumnLimit: 100 +SortIncludes: false +--- + diff --git a/deps/clap-juce-extensions/.gitmodules b/deps/clap-juce-extensions/.gitmodules new file mode 100644 index 0000000..d3a5b72 --- /dev/null +++ b/deps/clap-juce-extensions/.gitmodules @@ -0,0 +1,6 @@ +[submodule "clap-libs/clap"] + path = clap-libs/clap + url = https://github.com/free-audio/clap.git +[submodule "clap-libs/clap-helpers"] + path = clap-libs/clap-helpers + url = https://github.com/free-audio/clap-helpers.git diff --git a/deps/clap-juce-extensions/.gitrepo b/deps/clap-juce-extensions/.gitrepo new file mode 100644 index 0000000..585bbe8 --- /dev/null +++ b/deps/clap-juce-extensions/.gitrepo @@ -0,0 +1,12 @@ +; DO NOT EDIT (unless you know what you are doing) +; +; This subdirectory is a git "subrepo", and this file is maintained by the +; git-subrepo command. See https://github.com/git-commands/git-subrepo#readme +; +[subrepo] + remote = https://github.com/free-audio/clap-juce-extensions + branch = main + commit = 6489846d05a3bc0b743c5c0f301ad1858b4dc919 + parent = 345d5a1d9f1c12303202574c23520c8d1780dd6f + method = merge + cmdver = 0.4.3 diff --git a/deps/clap-juce-extensions/CMakeLists.txt b/deps/clap-juce-extensions/CMakeLists.txt new file mode 100644 index 0000000..67eee8a --- /dev/null +++ b/deps/clap-juce-extensions/CMakeLists.txt @@ -0,0 +1,138 @@ +# CMAKE Support for out of tree clap plugin extensions to Juce 6 +# +# To use these in your juce6 cmake project +# 1. Include this cmake file in your build path +# 2. Create your juce plugin as normal with formats VST3 etc... +# 3. After that, add the following lines (or similar) to your cmake +# clap_juce_extensions_plugin(TARGET my-target +# CLAP_ID "com.my-cool-plugs.my-target") +# 4. Reload your CMAKe file and my-target_CLAP will be a buildable target + +cmake_minimum_required (VERSION 3.15 FATAL_ERROR) + +project(clap-juce-extensions VERSION 0.1 LANGUAGES C CXX) + +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_CXX_STANDARD 14) + + +add_subdirectory(clap-libs/clap clapjuceext_clap EXCLUDE_FROM_ALL) +add_subdirectory(clap-libs/clap-helpers clapjuceext_claphelpers EXCLUDE_FROM_ALL) + +add_library(clap_juce_extensions STATIC src/extensions/clap-juce-extensions.cpp) +set_property(TARGET clap_juce_extensions PROPERTY CXX_STANDARD 14) +target_include_directories(clap_juce_extensions PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_compile_definitions(clap_juce_extensions PUBLIC + HAS_CLAP_JUCE_EXTENSIONS=1) +target_link_libraries(clap_juce_extensions PUBLIC clap-core clap-helpers) + +set_target_properties(clap_juce_extensions PROPERTIES + POSITION_INDEPENDENT_CODE TRUE + VISIBILITY_INLINES_HIDDEN TRUE + C_VISBILITY_PRESET hidden + CXX_VISIBILITY_PRESET hidden +) + +add_library(clap_juce_sources INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/src/wrapper/clap-juce-wrapper.cpp) +set_property(TARGET clap_juce_sources PROPERTY CXX_STANDARD 14) +if (APPLE) + target_sources(clap_juce_sources INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/src/wrapper/clap-juce-mac.mm) +endif() +target_include_directories(clap_juce_sources INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include) + +function(clap_juce_extensions_plugin) + set(oneValueArgs TARGET) + set(multiValueArgs CLAP_ID CLAP_FEATURES CLAP_MANUAL_URL CLAP_SUPPORT_URL) + + cmake_parse_arguments(CJA "" "${oneValueArgs}" + "${multiValueArgs}" ${ARGN} ) + set(target ${CJA_TARGET}) + set(claptarget ${target}_CLAP) + + message(STATUS "Creating CLAP ${claptarget} from ${target}") + + if ("${CJA_CLAP_ID}" STREQUAL "") + message(FATAL_ERROR "You must specify CLAP_ID to add a clap" ) + endif() + + if ("${CJA_CLAP_FEATURES}" STREQUAL "") + message(WARNING "No CLAP_FEATURES were specified! Using \"instrument\" by default.") + set(CJA_CLAP_FEATURES instrument) + endif() + + # we need the list of features as comma separated quoted strings + foreach(feature IN LISTS CJA_CLAP_FEATURES) + list (APPEND CJA_CLAP_FEATURES_PARSED "\"${feature}\"") + endforeach() + list (JOIN CJA_CLAP_FEATURES_PARSED ", " CJA_CLAP_FEATURES_PARSED) + + get_property(SRC TARGET clap_juce_sources PROPERTY SOURCES) + add_library(${claptarget} MODULE ${SRC}) + + set_target_properties(${claptarget} PROPERTIES + CXX_STANDARD 14 + ARCHIVE_OUTPUT_DIRECTORY "$>/CLAP" + LIBRARY_OUTPUT_DIRECTORY "$>/CLAP" + RUNTIME_OUTPUT_DIRECTORY "$>/CLAP") + + get_target_property(products_folder ${claptarget} LIBRARY_OUTPUT_DIRECTORY) + set(product_name $) + + if (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") + set_target_properties(${claptarget} PROPERTIES + BUNDLE True + BUNDLE_EXTENSION clap + PREFIX "" + OUTPUT_NAME "${product_name}" + MACOSX_BUNDLE TRUE + ) + else() + set_target_properties(${claptarget} PROPERTIES + PREFIX "" + SUFFIX ".clap" + OUTPUT_NAME "${product_name}" + ) + endif() + + target_include_directories(${claptarget} PRIVATE + $) + + target_compile_definitions(${claptarget} PRIVATE + CLAP_ID="${CJA_CLAP_ID}" + CLAP_FEATURES=${CJA_CLAP_FEATURES_PARSED} + CLAP_MANUAL_URL="${CJA_CLAP_MANUAL_URL}" + CLAP_SUPPORT_URL="${CJA_CLAP_SUPPORT_URL}") + + target_link_libraries(${target} PUBLIC clap_juce_extensions) + + target_link_libraries(${claptarget} PUBLIC clap-core clap-helpers clap_juce_sources ${target}) + set_property(TARGET ${claptarget} PROPERTY C_VISIBILITY_PRESET hidden) + set_property(TARGET ${claptarget} PROPERTY VISIBILITY_INLINES_HIDDEN ON) + + set_target_properties(${claptarget} PROPERTIES + POSITION_INDEPENDENT_CODE TRUE + VISIBILITY_INLINES_HIDDEN TRUE + C_VISBILITY_PRESET hidden + CXX_VISIBILITY_PRESET hidden + ) + + get_target_property(docopy "${target}" JUCE_COPY_PLUGIN_AFTER_BUILD) + + if(docopy) + message(STATUS "Copy After Build" ) + if (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") + add_custom_command(TARGET ${claptarget} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E echo "Installing ${products_folder}/${product_name}.clap to ~/Library/Audio/Plug-Ins/CLAP/" + COMMAND ${CMAKE_COMMAND} -E make_directory "~/Library/Audio/Plug-Ins/CLAP" + COMMAND ${CMAKE_COMMAND} -E copy_directory "${products_folder}/${product_name}.clap" "~/Library/Audio/Plug-Ins/CLAP/${product_name}.clap" + ) + endif() + if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + add_custom_command(TARGET ${claptarget} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E echo "Installing ${products_folder}/${product_name}.clap" + COMMAND ${CMAKE_COMMAND} -E make_directory "~/.clap" + COMMAND ${CMAKE_COMMAND} -E copy "${products_folder}/${product_name}.clap" "~/.clap/" + ) + endif() + endif() +endfunction() diff --git a/deps/clap-juce-extensions/LICENSE.md b/deps/clap-juce-extensions/LICENSE.md new file mode 100644 index 0000000..64d1d07 --- /dev/null +++ b/deps/clap-juce-extensions/LICENSE.md @@ -0,0 +1,9 @@ +Copyright 2019-2020, Paul Walker + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + diff --git a/deps/clap-juce-extensions/README.md b/deps/clap-juce-extensions/README.md new file mode 100644 index 0000000..af3d734 --- /dev/null +++ b/deps/clap-juce-extensions/README.md @@ -0,0 +1,171 @@ +# JUCE6 and 7 Unofficial CMake Clap Plugin Support + +This is a set of code which, combined with a JUCE6 or JUCE 7 CMake plugin project, allows you to build a CLAP plugin. It +is licensed under the MIT license, and can be used for both open and closed source projects. + +We are labeling it 'unofficial' for four reasons + +1. It is not supported by the JUCE team, +2. There are some JUCE features which we have not translated to CLAP yet, +3. It presents a set of completely optional extensions which break JUCE abstractions to allow extended CLAP feature + support and +4. It does not support JUCE-based CLAP hosting + +Despite those caveats, the basic use of this library has allowed a wide variety +of synths and effects to generate a CLAP from their JUCE program, including Surge, B-Step, +Monique, several ChowDSP plugins, Dexed and more. + +By far the best solution for CLAP in JUCE would be full native support by the JUCE team. Until such a time as that +happens (and it may never happen), this code may help you if you have a JUCE plugin and want to generate a CLAP. We are +happy to merge changes and answer questions as you try to use it. Please feel free to raise github issues in this repo. + +This version is based off of CLAP 1.0 and generates plugins which work in BWS 4.3beta5 and later, as well as +other CLAP 1.0 DAWs such as MultitrackStudio. + +## Basics: Using these extensions to build a CLAP + +Given a starting point of a JUCE plugin using CMake which can build a VST3, AU, Standalone and so forth with +`juce_plugin`, building a CLAP is a simple exercise of checking out this CLAP extension code +somewhere in your dev environment, setting a few CMake variables, and adding a couple of lines to your CMake file. + +The instructions are as follows: + +1. Add `https://github.com/free-audio/clap-juce-extensions.git` as a submodule of your project, or otherwise make the + source available to your cmake (CPM, side by side check out in CI, etc...). +2. Load the `clap-juce-extension` in your CMake after you have loaded JUCE. For instance, you could do + +```cmake +add_subdirectory(libs/JUCE) # this is however you load juce +add_subdirectory(libs/clap-juce-extensions EXCLUDE_FROM_ALL) +``` + +3. Create your JUCE plugin as normal with flags and formats using the `juce_plugin` CMake function +4. After your `juce_plugin` code, add the following lines (or similar) + to your CMake (a list of pre-defined CLAP + features can be found [here](https://github.com/free-audio/clap/blob/main/include/clap/plugin-features.h)): + +```cmake + clap_juce_extensions_plugin(TARGET my-target + CLAP_ID "com.my-cool-plugs.my-target" + CLAP_FEATURES instrument "virtual analog" gritty basses leads pads) +``` + +5. Reload your CMake file and you will have a new target `my-target_CLAP` which will build a CLAP and leave + it side-by-side with your AU, Standalone, VST3, and so forth. Load that CLAP into a DAW and give it a whirl! + +## Risks of using this library + +Using this library is, of course, not without risks. There could easily be bugs we haven't found and there are +APIs we don't cover. We are happy to discuss, investigate, and work to fix any of those. + +The biggest risk, though, involves JUCE team providing official support in a way which is fundamentally incompatible +with these wrappers. As of this writing, the JUCE team has not committed to supporting CLAP in JUCE 7 or any future +version, although they are aware of the project. But if the JUCE team did provide official future support, it is not +clear that your CLAP plugin which resulted from their official support would work in the same way as the plugin +generated by this library would. + +There are a couple of mitigants to that risk. + +Most importantly, in the three critical places a DAW interacts with a plugin - CLAP ID, parameter IDs, and state streaming - +we have endeavoured to write in as JUCE-natural a way as possible. + +1. The CLAP ID is just a CMake parameter, as we expect it would be in an official build. +2. The parameter IDs we use uses the [internal JUCE hashing mechanism to generate + our `uint32_t`](https://github.com/free-audio/clap-juce-extensions/blob/85bc0d56dc784a5f1271602db46f0748954b180e/src/wrapper/clap-juce-wrapper.cpp#L198) + just like + the [current VST3 wrapper does](https://github.com/juce-framework/JUCE/blob/2f980209cc4091a4490bb1bafc5d530f16834e58/modules/juce_audio_plugin_client/VST3/juce_VST3_Wrapper.cpp#L585). +3. Our stream implementation + transparently [calls `AudioProcessor::setStateInformation` and `AudioProcessor::getStateInformation`](https://github.com/free-audio/clap-juce-extensions/blob/85bc0d56dc784a5f1271602db46f0748954b180e/src/wrapper/clap-juce-wrapper.cpp#L930) + with no intervening + modification of the stream. + +While there is no guarantee that an official JUCE implementation, if it were to exist, would make these choices, +it seems quite natural that it would, and in that case, your plugin would continue to work. + +If, however, you use the extensions detailed below - which allows features outside of JUCE like note expressions, +sample accurate automation, and polyphonic and non-destructive modulation - there is very little assurance we can +give you that an official JUCE implementation, if it were to exist, would work with your code without modification, +or even that it would support those features at all. That would leave such a synth (of which Surge is the primary +example today) relying on these wrappers still. + +## Major Missing API points + +1. We have not tested any JUCE version earlier than 6.0.7, and plugins which use deprecated APIs may not work +2. The [`AudioProcessor::WrapperType`](https://docs.juce.com/master/classAudioProcessor.html#a2e1b21b8831ac529965abffc96223dcf) + API doesn't support CLAP. All CLAP plugins will define a `wrapperType` of `wrapperType_Undefined`. We do provide + a workaround for using our extensions mechanism, below. +3. Several parameter features - including discrete (stepped) parameters - don't translate from JUCE to CLAP + in our adapter (although they are supported in the CLAP API of course). We would love a test plugin to help us + resolve this. + +## The Extensions API + +There are a set of things which JUCE doesn't support which CLAP does. Rather than not support them in our +plugins, we've decided to create an extensions API. These are a set of classes which your AudioProcessor can +implement and, if it does, then the CLAP JUCE wrapper will call the associated functions. + +The extension are in "include/clap-juce-extensions.h" and are documented there, but currently have +three classes + +- `clap_juce_extensions::clap_properties` + - if you subclass this your AudioProcessor will have a collection of members which give you extra CLAP info + - Most usefully, you get an `is_clap` member which is false if not a CLAP and true if it is, which works around + the fact that our 'forkless' approach doesn't let us add a `AudioProcessor::WrapperType` to the JUCE API +- `clap_juce_extensions::clap_extensions` + - these are a set of advanced extensions which let you optionally interact more directly with the CLAP API + and are mostly useful for advanced features like non-destructive modulation and note expression support +- `clap_juce_extensions::clap_param_extensions` + - If your AudioProcessorParameter subclass implements this API, you can share extended CLAP information on + a parameter by parameter basis + +As an example, here's how to use `clap_properties` to work around `AudioProcessor::WrapperType` being `Undefined` in the forkless +CLAP approach + +- `#include "clap-juce-extensions/clap-juce-extensions.h"` +- Make your main plugin `juce::AudioProcessor` derive from `clap_juce_extensions::clap_properties` +- Use the `is_clap` member variable to figure out the correct wrapper type. + +Here's a minimal example: + +```cpp +#include +#include "clap-juce-extensions/clap-juce-extensions.h" + +class MyCoolPlugin : public juce::AudioProcessor, + public clap_juce_extensions::clap_properties +{ + String getWrapperTypeString() + { + if (wrapperType == wrapperType_Undefined && is_clap) + return "CLAP"; + + return juce::AudioProcessor::getWrapperTypeDescription (wrapperType); + } + + ... +}; +``` + +If you are interested in using these extensions, please consult the documentation in the +[clap-juce-extensions header.](https://github.com/free-audio/clap-juce-extensions/blob/main/include/clap-juce-extensions/clap-juce-extensions.h) +The [Surge XT Synthesizer](https://github.com/surge-synthesizer/surge) is a worked example of using many of these. +We are also happy to discuss them - reach out in the issues here or in a shared discord server. + +## Technical Detail: The "Forkless" approach + +There's a couple of ways we could have gone adding experimental JUCE support. The way the LV2 extensions to JUCE work +requires a forked JUCE which places LV2 support fully inside the JUCE ecosystem at the cost of maintaining a fork (and +not allowing folks with their own forks to easily use LV2). We instead chose an 'out-of-JUCE' approach which has the +following pros and cons + +Pros: + +* You can use any JUCE 6 or 7 / CMake method you want and don't need to use our branch. +* We don't have to update our fork to pull latest JUCE features; you don't have to use our fork and choices to build + your plugin. + +Cons: + +* The CMake API is not consistent. Rather than add "CLAP" as a plugin type, you need a couple of extra lines of CMake to + activate your CLAP. +* We cannot support the `AudioProcessor::WrapperType` API, as discussed above. diff --git a/deps/clap-juce-extensions/clap-libs/clap b/deps/clap-juce-extensions/clap-libs/clap new file mode 160000 index 0000000..84ad6fe --- /dev/null +++ b/deps/clap-juce-extensions/clap-libs/clap @@ -0,0 +1 @@ +Subproject commit 84ad6fe9739d3070ed0eb05c24be303a899ab813 diff --git a/deps/clap-juce-extensions/clap-libs/clap-helpers b/deps/clap-juce-extensions/clap-libs/clap-helpers new file mode 160000 index 0000000..2bb43c1 --- /dev/null +++ b/deps/clap-juce-extensions/clap-libs/clap-helpers @@ -0,0 +1 @@ +Subproject commit 2bb43c18788c689708ead6f127a2d75e772ab389 diff --git a/deps/clap-juce-extensions/include/clap-juce-extensions/clap-juce-extensions.h b/deps/clap-juce-extensions/include/clap-juce-extensions/clap-juce-extensions.h new file mode 100644 index 0000000..1912d44 --- /dev/null +++ b/deps/clap-juce-extensions/include/clap-juce-extensions/clap-juce-extensions.h @@ -0,0 +1,133 @@ +/* + * This file contains interface extensions which allow your AudioProcessor or + * AudioProcessorParameter to implement additional clap-specific API points, and then allows the + * CLAP wrapper to detect those implementation points and activate advanced features beyond the base + * JUCE model. + */ + +#ifndef SURGE_CLAP_JUCE_EXTENSIONS_H +#define SURGE_CLAP_JUCE_EXTENSIONS_H + +#include +#include +#include + +namespace clap_juce_extensions +{ +/* + * clap_properties contains simple properties about clap which you may want to use. + */ +struct clap_properties +{ + clap_properties(); + virtual ~clap_properties() = default; + + // The three part clap version + static uint32_t clap_version_major, clap_version_minor, clap_version_revision; + + // this will be true for the clap instance and false for all other flavors + bool is_clap{false}; + + // this will be non-null in the process block of a clap where the DAW provides transport + const clap_event_transport *clap_transport{nullptr}; + + // Internal implementation detail. Please disregard (and FIXME) + static bool building_clap; +}; + +/* + * clap_extensions allows you to interact with advanced properties of the CLAP api. + * The default implementations here mean if you implement clap_extensions and override + * nothing, you get the same behaviour as if you hadn't implemented it. + */ +struct clap_extensions +{ + /* + * In some cases, there is no main input, and input 0 is not main. Allow your plugin + * to advertise that. (This case is usually for synths with sidechains). + */ + virtual bool isInputMain(int input) + { + if (input == 0) + return true; + else + return false; + } + + /* + * If you want to provide information about voice structure, as documented + * in the voice-info clap extension. + */ + virtual bool supportsVoiceInfo() { return false; } + virtual bool voiceInfoGet(clap_voice_info * /*info*/) { return false; } + + /* + * Do you want to receive note expression messages? Note that if you return true + * here and don't implement supportsDirectProcess, the note expression messages will + * be received and ignored. + */ + virtual bool supportsNoteExpressions() { return false; } + + /* + * The JUCE process loop makes it difficult to do things like note expressions, + * sample accurate parameter automation, and other CLAP features. In most cases that + * is fine, but for some use cases, a synth may want the entirety of the JUCE infrastructure + * *except* the process loop. (Surge is one such synth). + * + * In this case, you can implement supportsDirectProcess to return true and then the clap + * juce wrapper will skip most parts of the process loop (it will still set up transport + * and deal with UI thread -> audio thread change events), and then call clap_direct_process. + * + * In this mode, it is the synth designer responsibility to implement clap_direct_process + * side by side with AudioProcessor::processBlock to use the CLAP api and synth internals + * directly. + */ + virtual bool supportsDirectProcess() { return false; } + virtual clap_process_status clap_direct_process(const clap_process * /*process*/) noexcept + { + return CLAP_PROCESS_CONTINUE; + } + + /* + * Do I support the CLAP_NOTE_DIALECT_CLAP? And prefer it if so? By default this + * is true if I support either note expressions, direct processing, or voice info, + * but you can override it for other reasons also, including not liking that default. + * + * The strictest hosts will not send note expression without this dialect, and so + * if you override this to return false, hosts may not give you NE or Voice level + * modulators in clap_direct_process. + */ + virtual bool supportsNoteDialectClap(bool /* isInput */) + { + return supportsNoteExpressions() || supportsVoiceInfo() || supportsDirectProcess(); + } + + virtual bool prefersNoteDialectClap(bool isInput) { return supportsNoteDialectClap(isInput); } +}; + +/* + * clap_param_extensions is intended to be applied to AudioParameter subclasses. When + * asking your JUCE plugin for parameters, the clap wrapper will check if your parameter + * implements the extensions and call the associated functions. + */ +struct clap_param_extensions +{ + /* + * Return true if this parameter should receive non-destructive + * monophonic modulation rather than simple setValue when a DAW + * initiated modulation changes. Requires you to implement + * clap_direct_process + */ + virtual bool supportsMonophonicModulation() { return false; }; + + /* + * Return true if this parameter should receive non-destructive + * polyphonic modulation. As well as supporting the monophonic case + * this also requires your process to return note end events when + * voices are terminated. + */ + virtual bool supportsPolyphonicModulation() { return false; }; +}; +} // namespace clap_juce_extensions + +#endif // SURGE_CLAP_JUCE_EXTENSIONS_H diff --git a/deps/clap-juce-extensions/src/extensions/clap-juce-extensions.cpp b/deps/clap-juce-extensions/src/extensions/clap-juce-extensions.cpp new file mode 100644 index 0000000..4268802 --- /dev/null +++ b/deps/clap-juce-extensions/src/extensions/clap-juce-extensions.cpp @@ -0,0 +1,15 @@ +// +// Created by Paul Walker on 12/19/21. +// + +#include "clap-juce-extensions/clap-juce-extensions.h" + +namespace clap_juce_extensions +{ +bool clap_properties::building_clap{false}; +uint32_t clap_properties::clap_version_major{0}, clap_properties::clap_version_minor{0}, + clap_properties::clap_version_revision{0}; + +clap_properties::clap_properties() : is_clap{building_clap} {} + +} // namespace clap_juce_extensions \ No newline at end of file diff --git a/deps/clap-juce-extensions/src/wrapper/clap-juce-mac.mm b/deps/clap-juce-extensions/src/wrapper/clap-juce-mac.mm new file mode 100644 index 0000000..fd5958f --- /dev/null +++ b/deps/clap-juce-extensions/src/wrapper/clap-juce-mac.mm @@ -0,0 +1,13 @@ +/* + * This file allows our CLAP extension to load the objective + * C extensions that JUCE uses to create a UI on mac in the + * VST3 and VST implementations. Basically it provides the pair + * of functions to attach our NSView to the parent window properly + * from the juce editor. This code is maintained by the JUCE team + * but we need to link it here also, so create this little stub + * which (for this one file only) tells JUCE I'm a VST3 and makes + * the objective C symbols available. + */ + +#define JucePlugin_Build_VST3 1 +#include "juce_audio_plugin_client/VST/juce_VST_Wrapper.mm" diff --git a/deps/clap-juce-extensions/src/wrapper/clap-juce-wrapper.cpp b/deps/clap-juce-extensions/src/wrapper/clap-juce-wrapper.cpp new file mode 100644 index 0000000..e29b921 --- /dev/null +++ b/deps/clap-juce-extensions/src/wrapper/clap-juce-wrapper.cpp @@ -0,0 +1,1109 @@ +/* + * BaconPaul's running todo + * + * - We always say we are an instrument.... + * - midi out (try stochas perhaps?) + * - why does dexed not work? + * - Finish populating the desc + * - Cleanup and comment of course (including the CMake) including what's skipped + */ + +#include +#include +#include + +#define JUCE_GUI_BASICS_INCLUDE_XHEADERS 1 +#include +#include +#include + +JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE("-Wunused-parameter", "-Wsign-conversion") +JUCE_BEGIN_IGNORE_WARNINGS_MSVC(4100 4127 4244) +// Sigh - X11.h eventually does a #define None 0L which doesn't work +// with an enum in clap land being called None, so just undef it +// post the JUCE installs +#ifdef None +#undef None +#endif +#include +#include +#include +#include +#include +JUCE_END_IGNORE_WARNINGS_MSVC +JUCE_END_IGNORE_WARNINGS_GCC_LIKE + +#include + +#if JUCE_LINUX +#if JUCE_VERSION > 0x060008 +#include +#endif +#endif + +#define FIXME(x) \ + { \ + static bool onetime_ = false; \ + if (!onetime_) \ + { \ + std::ostringstream oss; \ + oss << "FIXME: " << x << " @" << __LINE__; \ + DBG(oss.str()); \ + } \ + jassert(onetime_); \ + onetime_ = true; \ + } + +/* + * This is a utility lock free queue based on the JUCE abstract fifo + */ +template class PushPopQ +{ + public: + PushPopQ() : af(qSize) {} + bool push(const T &ad) + { + auto ret = false; + int start1, size1, start2, size2; + af.prepareToWrite(1, start1, size1, start2, size2); + if (size1 > 0) + { + dq[start1] = ad; + ret = true; + } + af.finishedWrite(size1 + size2); + return ret; + } + bool pop(T &ad) + { + bool ret = false; + int start1, size1, start2, size2; + af.prepareToRead(1, start1, size1, start2, size2); + if (size1 > 0) + { + ad = dq[start1]; + ret = true; + } + af.finishedRead(size1 + size2); + return ret; + } + juce::AbstractFifo af; + T dq[qSize]; +}; + +/* + * These functions are the JUCE VST2/3 NSView attachment functions. We compile them into + * our clap dll by, on macos, also linking clap_juce_mac.mm + */ +namespace juce +{ +extern JUCE_API void initialiseMacVST(); +extern JUCE_API void *attachComponentToWindowRefVST(Component *, void *parentWindowOrView, + bool isNSView); +} // namespace juce + +JUCE_BEGIN_IGNORE_WARNINGS_MSVC(4996) // allow strncpy + +/* + * The ClapJuceWrapper is a class which immplements a collection + * of CLAP and JUCE APIs + */ +class ClapJuceWrapper : public clap::helpers::Plugin, + public juce::AudioProcessorListener, + public juce::AudioPlayHead, + public juce::AudioProcessorParameter::Listener, + public juce::ComponentListener +{ + public: + // this needs to be the very last thing to get deleted! + juce::ScopedJuceInitialiser_GUI libraryInitializer; + + static clap_plugin_descriptor desc; + std::unique_ptr processor; + clap_juce_extensions::clap_properties *processorAsClapProperties{nullptr}; + clap_juce_extensions::clap_extensions *processorAsClapExtensions{nullptr}; + + ClapJuceWrapper(const clap_host *host, juce::AudioProcessor *p) + : clap::helpers::Plugin(&desc, host), + processor(p) + { + processor->setRateAndBufferSizeDetails(0, 0); + processor->setPlayHead(this); + processor->addListener(this); + + processorAsClapProperties = dynamic_cast(p); + processorAsClapExtensions = dynamic_cast(p); + + const bool forceLegacyParamIDs = false; + + juceParameters.update(*processor, forceLegacyParamIDs); + + for (auto *juceParam : +#if JUCE_VERSION >= 0x060103 + juceParameters +#else + juceParameters.params +#endif + + ) + { + uint32_t clap_id = generateClapIDForJuceParam(juceParam); + + allClapIDs.insert(clap_id); + paramPtrByClapID[clap_id] = juceParam; + clapIDByParamPtr[juceParam] = clap_id; + } + } + + ~ClapJuceWrapper() override + { + processor->editorBeingDeleted(editor.get()); + +#if JUCE_LINUX + if (_host.canUseTimerSupport()) + { + _host.timerSupportUnregister(idleTimer); + } +#endif + } + + bool init() noexcept override + { +#if JUCE_LINUX + if (_host.canUseTimerSupport()) + { + _host.timerSupportRegister(1000 / 50, &idleTimer); + } +#endif + return true; + } + + public: + bool implementsTimerSupport() const noexcept override { return true; } + void onTimer(clap_id timerId) noexcept override + { + juce::ignoreUnused(timerId); +#if LINUX + juce::ScopedJuceInitialiser_GUI libraryInitialiser; + const juce::MessageManagerLock mmLock; + +#if JUCE_VERSION > 0x060008 + while (juce::dispatchNextMessageOnSystemQueue(true)) + { + } +#else + auto mm = juce::MessageManager::getInstance(); + mm->runDispatchLoopUntil(0); +#endif +#endif + } + + clap_id idleTimer{0}; + + uint32_t generateClapIDForJuceParam(juce::AudioProcessorParameter *param) const + { + auto juceParamID = juce::LegacyAudioParameter::getParamID(param, false); + auto clap_id = static_cast(juceParamID.hashCode()); + return clap_id; + } + +#if JUCE_VERSION >= 0x060008 + void audioProcessorChanged(juce::AudioProcessor *proc, const ChangeDetails &details) override + { + juce::ignoreUnused(proc); + if (details.latencyChanged) + { + runOnMainThread([this] { + if (isBeingDestroyed()) + return; + + _host.latencyChanged(); + }); + } + if (details.programChanged) + { + // At the moment, CLAP doesn't have a sense of programs (to my knowledge). + // (I think) what makes most sense is to tell the host to update the parameters + // as though a preset has been loaded. + runOnMainThread([this] { + if (isBeingDestroyed()) + return; + + _host.paramsRescan(CLAP_PARAM_RESCAN_VALUES); + }); + } +#if JUCE_VERSION >= 0x060103 + if (details.nonParameterStateChanged) + { + runOnMainThread([this] { + if (isBeingDestroyed()) + return; + + _host.stateMarkDirty(); + }); + } +#endif + if (details.parameterInfoChanged) + { + // JUCE documentations states that, `parameterInfoChanged` means + // "Indicates that some attributes of the AudioProcessor's parameters have changed." + // For now, I'm going to assume this means the parameter's name or value->text + // conversion has changed, and tell the clap host to rescan those. + // + // We could do CLAP_PARAM_RESCAN_ALL, but then the plugin would have to be deactivated. + runOnMainThread([this] { + if (isBeingDestroyed()) + return; + + _host.paramsRescan(CLAP_PARAM_RESCAN_VALUES | CLAP_PARAM_RESCAN_TEXT); + }); + } + } +#else + void audioProcessorChanged(juce::AudioProcessor *proc) override { + /* + * Before 6.0.8 it was unclear what changed. For now make the approximating decision to just + * rescan values and text. + */ + runOnMainThread([this] { + if (isBeingDestroyed()) + return; + + _host.paramsRescan(CLAP_PARAM_RESCAN_VALUES | CLAP_PARAM_RESCAN_TEXT); + }); + } +#endif + + + clap_id clapIdFromParameterIndex(int index) + { + auto pbi = juceParameters.getParamForIndex(index); + auto pf = clapIDByParamPtr.find(pbi); + if (pf != clapIDByParamPtr.end()) + return pf->second; + + auto id = generateClapIDForJuceParam(pbi); // a lookup obviously + return id; + } + + void audioProcessorParameterChanged(juce::AudioProcessor *, int index, float newValue) override + { + auto id = clapIdFromParameterIndex(index); + uiParamChangeQ.push({CLAP_EVENT_PARAM_VALUE, 0, id, newValue}); + } + + void audioProcessorParameterChangeGestureBegin(juce::AudioProcessor *, int index) override + { + auto id = clapIdFromParameterIndex(index); + auto p = paramPtrByClapID[id]; + uiParamChangeQ.push({CLAP_EVENT_PARAM_GESTURE_BEGIN, 0, id, p->getValue()}); + } + + void audioProcessorParameterChangeGestureEnd(juce::AudioProcessor *, int index) override + { + auto id = clapIdFromParameterIndex(index); + auto p = paramPtrByClapID[id]; + uiParamChangeQ.push({CLAP_EVENT_PARAM_GESTURE_END, 0, id, p->getValue()}); + } + + /* + * According to the JUCE docs this is *only* called on the processing thread + */ + bool getCurrentPosition(juce::AudioPlayHead::CurrentPositionInfo &info) override + { + if (hasTransportInfo && transportInfo) + { + auto flags = transportInfo->flags; + + if (flags & CLAP_TRANSPORT_HAS_TEMPO) + info.bpm = transportInfo->tempo; + if (flags & CLAP_TRANSPORT_HAS_TIME_SIGNATURE) + { + info.timeSigNumerator = transportInfo->tsig_num; + info.timeSigDenominator = transportInfo->tsig_denom; + } + + if (flags & CLAP_TRANSPORT_HAS_BEATS_TIMELINE) + { + info.ppqPosition = 1.0 * transportInfo->song_pos_beats / CLAP_BEATTIME_FACTOR; + info.ppqPositionOfLastBarStart = + 1.0 * transportInfo->bar_start / CLAP_BEATTIME_FACTOR; + } + if (flags & CLAP_TRANSPORT_HAS_SECONDS_TIMELINE) + { + info.timeInSeconds = 1.0 * (double)transportInfo->song_pos_seconds / CLAP_SECTIME_FACTOR; + info.timeInSamples = (int64_t)(info.timeInSeconds * sampleRate()); + } + info.isPlaying = flags & CLAP_TRANSPORT_IS_PLAYING; + info.isRecording = flags & CLAP_TRANSPORT_IS_RECORDING; + info.isLooping = flags & CLAP_TRANSPORT_IS_LOOP_ACTIVE; + } + return hasTransportInfo; + } + + void parameterValueChanged(int, float newValue) override + { + juce::ignoreUnused(newValue); + FIXME("parameter value changed"); + // this can only come from the bypass parameter + } + + void parameterGestureChanged(int, bool) override { FIXME("parameter gesture changed"); } + + bool activate(double sampleRate, uint32_t minFrameCount, + uint32_t maxFrameCount) noexcept override + { + juce::ignoreUnused(minFrameCount); + processor->setRateAndBufferSizeDetails(sampleRate, maxFrameCount); + processor->prepareToPlay(sampleRate, (int)maxFrameCount); + midiBuffer.ensureSize(2048); + midiBuffer.clear(); + return true; + } + + /* CLAP API */ + + bool implementsAudioPorts() const noexcept override { return true; } + uint32_t audioPortsCount(bool isInput) const noexcept override + { + return (uint32_t)processor->getBusCount(isInput); + } + + bool audioPortsInfo(uint32_t index, bool isInput, + clap_audio_port_info *info) const noexcept override + { + // For now hardcode to stereo out. Fix this obviously. + auto bus = processor->getBus(isInput, (int)index); + auto clob = bus->getDefaultLayout(); + + // For now we only support stereo channels + jassert(clob.size() == 1 || clob.size() == 2); + jassert(clob.size() == 1 || (clob.getTypeOfChannel(0) == juce::AudioChannelSet::left && + clob.getTypeOfChannel(1) == juce::AudioChannelSet::right)); + // if (isInput || index != 0) return false; + info->id = (isInput ? 1 << 15 : 1) + index; + strncpy(info->name, bus->getName().toRawUTF8(), sizeof(info->name)); + + bool couldBeMain = true; + if (isInput && processorAsClapExtensions) + couldBeMain = processorAsClapExtensions->isInputMain(index); + if (index == 0 && couldBeMain) + { + info->flags = CLAP_AUDIO_PORT_IS_MAIN; + } + else + { + info->flags = 0; + } + + info->in_place_pair = info->id; + info->channel_count = (uint32_t)clob.size(); + + if (clob.size() == 1) + info->port_type = CLAP_PORT_MONO; + else + info->port_type = CLAP_PORT_STEREO; + + auto requested = processor->getBusesLayout(); + if (clob.size() == 1) + requested.getChannelSet(isInput, (int)index) = juce::AudioChannelSet::mono(); + if (clob.size() == 2) + requested.getChannelSet(isInput, (int)index) = juce::AudioChannelSet::stereo(); + + processor->setBusesLayoutWithoutEnabling(requested); + + DBG("audioPortsInfo " << (isInput ? "INPUT " : "OUTPUT ") << (int)index << " '" + << bus->getName() + << "' isMain=" << ((index == 0 && couldBeMain) ? "T" : "F") + << " sz=" << clob.size() << " enabled=" << (int)(bus->isEnabled())); + + return true; + } + uint32_t audioPortsConfigCount() const noexcept override + { + DBG("audioPortsConfigCount CALLED - returning 0"); + return 0; + } + bool audioPortsGetConfig(uint32_t /*index*/, + clap_audio_ports_config * /*config*/) const noexcept override + { + return false; + } + bool audioPortsSetConfig(clap_id /*configId*/) noexcept override { return false; } + + bool implementsNotePorts() const noexcept override { return true; } + uint32_t notePortsCount(bool is_input) const noexcept override + { + if (is_input) + { + if (processor->acceptsMidi()) + return 1; + } + else + { + if (processor->producesMidi()) + return 1; + } + return 0; + } + bool notePortsInfo(uint32_t index, bool is_input, + clap_note_port_info *info) const noexcept override + { + juce::ignoreUnused(index); + + if (is_input) + { + info->id = 1 << 5U; + info->supported_dialects = CLAP_NOTE_DIALECT_MIDI; + if (processor->supportsMPE()) + info->supported_dialects |= CLAP_NOTE_DIALECT_MIDI_MPE; + + info->preferred_dialect = CLAP_NOTE_DIALECT_MIDI; + + if (processorAsClapExtensions) + { + if (processorAsClapExtensions->supportsNoteDialectClap(true)) + { + info->supported_dialects |= CLAP_NOTE_DIALECT_CLAP; + } + if (processorAsClapExtensions->prefersNoteDialectClap(true)) + { + info->preferred_dialect = CLAP_NOTE_DIALECT_CLAP; + } + } + strncpy(info->name, "JUCE Note Input", CLAP_NAME_SIZE); + } + else + { + info->id = 1 << 2U; + info->supported_dialects = CLAP_NOTE_DIALECT_MIDI; + if (processor->supportsMPE()) + info->supported_dialects |= CLAP_NOTE_DIALECT_MIDI_MPE; + info->preferred_dialect = CLAP_NOTE_DIALECT_MIDI; + + if (processorAsClapExtensions) + { + if (processorAsClapExtensions->supportsNoteDialectClap(false)) + { + info->supported_dialects |= CLAP_NOTE_DIALECT_CLAP; + } + if (processorAsClapExtensions->prefersNoteDialectClap(false)) + { + info->preferred_dialect = CLAP_NOTE_DIALECT_CLAP; + } + } + + strncpy(info->name, "JUCE Note Output", CLAP_NAME_SIZE); + } + return true; + } + + bool implementsVoiceInfo() const noexcept override + { + if (processorAsClapExtensions) + return processorAsClapExtensions->supportsVoiceInfo(); + return false; + } + + bool voiceInfoGet(clap_voice_info *info) noexcept override + { + if (processorAsClapExtensions) + return processorAsClapExtensions->voiceInfoGet(info); + return Plugin::voiceInfoGet(info); + } + + public: + bool implementsParams() const noexcept override { return true; } + bool isValidParamId(clap_id paramId) const noexcept override + { + return allClapIDs.find(paramId) != allClapIDs.end(); + } + uint32_t paramsCount() const noexcept override { return (uint32_t)allClapIDs.size(); } + bool paramsInfo(uint32_t paramIndex, clap_param_info *info) const noexcept override + { + auto pbi = juceParameters.getParamForIndex((int)paramIndex); + + auto *parameterGroup = processor->getParameterTree().getGroupsForParameter(pbi).getLast(); + juce::String group = ""; + while (parameterGroup && parameterGroup->getParent() && + parameterGroup->getParent()->getName().isNotEmpty()) + { + group = parameterGroup->getName() + "/" + group; + parameterGroup = parameterGroup->getParent(); + } + + if (group.isNotEmpty()) + group = "/" + group; + + // Fixme - using parameter groups here would be lovely but until then + info->id = generateClapIDForJuceParam(pbi); + strncpy(info->name, (pbi->getName(CLAP_NAME_SIZE)).toRawUTF8(), CLAP_NAME_SIZE); + strncpy(info->module, group.toRawUTF8(), CLAP_NAME_SIZE); + + info->min_value = 0; // FIXME + info->max_value = 1; + info->default_value = pbi->getDefaultValue(); + info->cookie = pbi; + info->flags = 0; + + if (pbi->isAutomatable()) + info->flags = info->flags | CLAP_PARAM_IS_AUTOMATABLE; + + if (pbi->isBoolean() || pbi->isDiscrete()) + { + info->flags = info->flags | CLAP_PARAM_IS_STEPPED; + } + + auto cpe = dynamic_cast(pbi); + if (cpe) + { + if (cpe->supportsMonophonicModulation()) + { + info->flags = info->flags | CLAP_PARAM_IS_MODULATABLE; + } + if (cpe->supportsPolyphonicModulation()) + { + info->flags = + info->flags | CLAP_PARAM_IS_MODULATABLE | + CLAP_PARAM_IS_MODULATABLE_PER_CHANNEL | CLAP_PARAM_IS_MODULATABLE_PER_KEY | + CLAP_PARAM_IS_AUTOMATABLE_PER_NOTE_ID | CLAP_PARAM_IS_AUTOMATABLE_PER_PORT; + } + } + + return true; + } + + bool paramsValue(clap_id paramId, double *value) noexcept override + { + auto pbi = paramPtrByClapID[paramId]; + *value = pbi->getValue(); + return true; + } + + bool paramsValueToText(clap_id paramId, double value, char *display, + uint32_t size) noexcept override + { + auto pbi = paramPtrByClapID[paramId]; + auto res = pbi->getText((float)value, (int)size); + strncpy(display, res.toStdString().c_str(), size); + return true; + } + + bool paramsTextToValue(clap_id paramId, const char *display, double *value) noexcept override + { + auto pbi = paramPtrByClapID[paramId]; + *value = pbi->getValueForText(display); + return true; + } + + void paramSetValueAndNotifyIfChanged(juce::AudioProcessorParameter ¶m, float newValue) + { + if (param.getValue() == newValue) + return; + + param.setValueNotifyingHost(newValue); + } + + bool implementsLatency() const noexcept override { return true; } + uint32_t latencyGet() const noexcept override + { + return (uint32_t)processor->getLatencySamples(); + } + + bool implementsTail() const noexcept override { return true; } + uint32_t tailGet(const clap_plugin_t *) const noexcept override + { + return uint32_t( + juce::roundToIntAccurate((double)sampleRate() * processor->getTailLengthSeconds())); + } + + juce::MidiBuffer midiBuffer; + + clap_process_status process(const clap_process *process) noexcept override + { + // Since the playhead is *only* good inside juce audio processor process, + // we can just keep this little transient pointer here + if (process->transport) + { + hasTransportInfo = true; + transportInfo = process->transport; + } + else + { + hasTransportInfo = false; + transportInfo = nullptr; + } + + if (processorAsClapProperties) + processorAsClapProperties->clap_transport = process->transport; + + auto pc = ParamChange(); + auto ov = process->out_events; + + while (uiParamChangeQ.pop(pc)) + { + if (pc.type == CLAP_EVENT_PARAM_VALUE) + { + auto evt = clap_event_param_value(); + evt.header.size = sizeof(clap_event_param_value); + evt.header.type = (uint16_t)CLAP_EVENT_PARAM_VALUE; + evt.header.time = 0; // for now + evt.header.space_id = CLAP_CORE_EVENT_SPACE_ID; + evt.header.flags = pc.flag; + evt.param_id = pc.id; + evt.value = pc.newval; + ov->try_push(ov, reinterpret_cast(&evt)); + } + + if (pc.type == CLAP_EVENT_PARAM_GESTURE_END || + pc.type == CLAP_EVENT_PARAM_GESTURE_BEGIN) + { + auto evt = clap_event_param_gesture(); + evt.header.size = sizeof(clap_event_param_gesture); + evt.header.type = (uint16_t)pc.type; + evt.header.time = 0; // for now + evt.header.space_id = CLAP_CORE_EVENT_SPACE_ID; + evt.header.flags = pc.flag; + evt.param_id = pc.id; + ov->try_push(ov, reinterpret_cast(&evt)); + } + } + + if (processorAsClapExtensions && processorAsClapExtensions->supportsDirectProcess()) + return processorAsClapExtensions->clap_direct_process(process); + + auto ev = process->in_events; + auto sz = ev->size(ev); + + if (sz != 0) + { + for (uint32_t i = 0; i < sz; ++i) + { + auto evt = ev->get(ev, i); + + if (evt->space_id != CLAP_CORE_EVENT_SPACE_ID) + continue; + + switch (evt->type) + { + case CLAP_EVENT_NOTE_ON: + { + auto nevt = reinterpret_cast(evt); + + midiBuffer.addEvent(juce::MidiMessage::noteOn(nevt->channel + 1, nevt->key, + (float)nevt->velocity), + (int)nevt->header.time); + } + break; + case CLAP_EVENT_NOTE_OFF: + { + auto nevt = reinterpret_cast(evt); + midiBuffer.addEvent(juce::MidiMessage::noteOff(nevt->channel + 1, nevt->key, + (float)nevt->velocity), + (int)nevt->header.time); + } + break; + case CLAP_EVENT_MIDI: + { + auto mevt = reinterpret_cast(evt); + midiBuffer.addEvent(juce::MidiMessage(mevt->data[0], mevt->data[1], + mevt->data[2], mevt->header.time), + (int)mevt->header.time); + } + break; + case CLAP_EVENT_TRANSPORT: + { + // handle this case + } + break; + case CLAP_EVENT_PARAM_VALUE: + { + auto pevt = reinterpret_cast(evt); + + auto id = pevt->param_id; + auto nf = pevt->value; + jassert(pevt->cookie == paramPtrByClapID[id]); + auto jp = static_cast(pevt->cookie); + paramSetValueAndNotifyIfChanged(*jp, (float)nf); + } + break; + case CLAP_EVENT_PARAM_MOD: + { + } + break; + case CLAP_EVENT_NOTE_END: + { + // Why do you send me this, Alex? + } + break; + default: + { + DBG("Unknown message type " << (int)evt->type); + // In theory I should never get this. + // jassertfalse + } + break; + } + } + } + + // We process in place so + static constexpr uint32_t maxBuses = 128; + std::array busses{}; + busses.fill(nullptr); + + /*DBG("IO Configuration: I=" << (int)process->audio_inputs_count << " O=" + << (int)process->audio_outputs_count << " MX=" << (int)mx); + DBG("Plugin Configuration: IC=" << processor->getTotalNumInputChannels() + << " OC=" << processor->getTotalNumOutputChannels()); + */ + + /* + * OK so here is what JUCE expects in its audio buffer. It *always* uses input as output + * buffer so we need to create a buffer where each channel is the channel of the associated + * output pointer (fine) and then the inputs need to either check they are the same or copy. + */ + + /* + * So first lets load up with our outputs + */ + uint32_t ochans = 0; + for (uint32_t idx = 0; idx < process->audio_outputs_count && ochans < maxBuses; ++idx) + { + for (uint32_t ch = 0; ch < process->audio_outputs[idx].channel_count; ++ch) + { + busses[ochans] = process->audio_outputs[idx].data32[ch]; + ochans++; + } + } + + uint32_t ichans = 0; + for (uint32_t idx = 0; idx < process->audio_inputs_count && ichans < maxBuses; ++idx) + { + for (uint32_t ch = 0; ch < process->audio_inputs[idx].channel_count; ++ch) + { + auto *ic = process->audio_inputs[idx].data32[ch]; + if (ichans < ochans) + { + if (ic == busses[ichans]) + { + // The buffers overlap - no need to do anything + } + else + { + juce::FloatVectorOperations::copy(busses[ichans], ic, + (int)process->frames_count); + } + } + else + { + busses[ichans] = ic; + } + ichans++; + } + } + + auto totalChans = std::max(ichans, ochans); + juce::AudioBuffer buf(busses.data(), (int)totalChans, (int)process->frames_count); + + FIXME("Handle bypass and deactivated states"); + processor->processBlock(buf, midiBuffer); + + if (processor->producesMidi()) + { + for (auto meta : midiBuffer) + { + auto msg = meta.getMessage(); + if (msg.getRawDataSize() == 3) + { + auto evt = clap_event_midi(); + evt.header.size = sizeof(clap_event_midi); + evt.header.type = (uint16_t)CLAP_EVENT_MIDI; + evt.header.time = meta.samplePosition; // for now + evt.header.space_id = CLAP_CORE_EVENT_SPACE_ID; + evt.header.flags = 0; + evt.port_index = 0; + memcpy(&evt.data, msg.getRawData(), 3 * sizeof(uint8_t)); + ov->try_push(ov, reinterpret_cast(&evt)); + } + } + } + + if (!midiBuffer.isEmpty()) + midiBuffer.clear(); + + return CLAP_PROCESS_CONTINUE; + } + + void componentMovedOrResized(juce::Component &component, bool wasMoved, + bool wasResized) override + { + juce::ignoreUnused(wasMoved); + if (wasResized) + _host.guiRequestResize((uint32_t)component.getWidth(), (uint32_t)component.getHeight()); + } + + std::unique_ptr editor; + bool implementsGui() const noexcept override { return processor->hasEditor(); } + bool guiCanResize() const noexcept override + { + if (editor) + return editor->isResizable(); + return true; + } + bool guiAdjustSize(uint32_t *w, uint32_t *h) noexcept override + { + if (!editor) + return false; + + if (!editor->isResizable()) + return false; + + editor->setSize(*w, *h); + return true; + } + + bool guiIsApiSupported(const char *api, bool isFloating) noexcept override + { + if (isFloating) + return false; + + if (strcmp(api, CLAP_WINDOW_API_WIN32) == 0 || strcmp(api, CLAP_WINDOW_API_COCOA) == 0 || + strcmp(api, CLAP_WINDOW_API_X11) == 0) + return true; + + return false; + } + + bool guiCreate(const char *api, bool isFloating) noexcept override + { + juce::ignoreUnused(api); + + // Should never happen + if (isFloating) + return false; + + const juce::MessageManagerLock mmLock; + editor.reset(processor->createEditorIfNeeded()); + editor->addComponentListener(this); + return editor != nullptr; + } + + void guiDestroy() noexcept override + { + processor->editorBeingDeleted(editor.get()); + editor.reset(nullptr); + } + + bool guiSetParent(const clap_window *window) noexcept override + { +#if JUCE_MAC + return guiCocoaAttach(window->cocoa); +#elif JUCE_LINUX + return guiX11Attach(NULL, window->x11); +#elif JUCE_WINDOWS + return guiWin32Attach(window->win32); +#else + return false; +#endif + } + + bool guiGetSize(uint32_t *width, uint32_t *height) noexcept override + { + const juce::MessageManagerLock mmLock; + if (editor) + { + auto b = editor->getBounds(); + *width = (uint32_t)b.getWidth(); + *height = (uint32_t)b.getHeight(); + return true; + } + else + { + *width = 1000; + *height = 800; + } + return false; + } + + protected: + juce::CriticalSection stateInformationLock; + juce::MemoryBlock chunkMemory; + + public: + bool implementsState() const noexcept override { return true; } + bool stateSave(const clap_ostream *stream) noexcept override + { + if (processor == nullptr) + return false; + + juce::ScopedLock lock(stateInformationLock); + chunkMemory.reset(); + + processor->getStateInformation(chunkMemory); + + auto written = stream->write(stream, chunkMemory.getData(), chunkMemory.getSize()); + return written == (int64_t)chunkMemory.getSize(); + } + bool stateLoad(const clap_istream *stream) noexcept override + { + if (processor == nullptr) + return false; + + juce::ScopedLock lock(stateInformationLock); + chunkMemory.reset(); + // There must be a better way + char block[256]; + int64_t rd; + while ((rd = stream->read(stream, block, 256)) > 0) + chunkMemory.append(block, (size_t)rd); + + processor->setStateInformation(chunkMemory.getData(), (int)chunkMemory.getSize()); + chunkMemory.reset(); + return true; + } + + public: +#if JUCE_MAC + bool guiCocoaAttach(void *nsView) noexcept + { + juce::initialiseMacVST(); + auto hostWindow = juce::attachComponentToWindowRefVST(editor.get(), nsView, true); + juce::ignoreUnused(hostWindow); + return true; + } +#endif + +#if JUCE_LINUX + bool guiX11Attach(const char *displayName, unsigned long window) noexcept + { + const juce::MessageManagerLock mmLock; + editor->setVisible(false); + editor->addToDesktop(0, (void *)window); + auto *display = juce::XWindowSystem::getInstance()->getDisplay(); + juce::X11Symbols::getInstance()->xReparentWindow(display, (Window)editor->getWindowHandle(), + window, 0, 0); + editor->setVisible(true); + return true; + } +#endif + +#if JUCE_WINDOWS + bool guiWin32Attach(clap_hwnd window) noexcept + { + editor->setVisible(false); + editor->setOpaque(true); + editor->setTopLeftPosition(0, 0); + editor->addToDesktop(0, (void *)window); + editor->setVisible(true); + return true; + } +#endif + + private: + struct ParamChange + { + int type; + int flag; + uint32_t id; + float newval{0}; + }; + PushPopQ uiParamChangeQ; + + /* + * Various maps for ID lookups + */ + // clap_id to param * + std::unordered_map paramPtrByClapID; + // param * to clap_id + std::unordered_map clapIDByParamPtr; + // Every id we have issued + std::unordered_set allClapIDs; + + juce::LegacyAudioParametersWrapper juceParameters; + + const clap_event_transport *transportInfo{nullptr}; + bool hasTransportInfo{false}; +}; + +JUCE_END_IGNORE_WARNINGS_MSVC + +const char *features[] = {CLAP_FEATURES, nullptr}; +clap_plugin_descriptor ClapJuceWrapper::desc = {CLAP_VERSION, + CLAP_ID, + JucePlugin_Name, + JucePlugin_Manufacturer, + JucePlugin_ManufacturerWebsite, + CLAP_MANUAL_URL, + CLAP_SUPPORT_URL, + JucePlugin_VersionString, + JucePlugin_Desc, + features}; + +juce::AudioProcessor *JUCE_CALLTYPE createPluginFilter(); + +namespace ClapAdapter +{ +bool clap_init(const char *) { return true; } + +void clap_deinit(void) {} + +uint32_t clap_get_plugin_count(const struct clap_plugin_factory *) { return 1; } + +const clap_plugin_descriptor *clap_get_plugin_descriptor(const struct clap_plugin_factory *, + uint32_t) +{ + return &ClapJuceWrapper::desc; +} + +static const clap_plugin *clap_create_plugin(const struct clap_plugin_factory *, + const clap_host *host, const char *plugin_id) +{ + juce::ScopedJuceInitialiser_GUI libraryInitialiser; + + if (strcmp(plugin_id, ClapJuceWrapper::desc.id)) + { + std::cout << "Warning: CLAP asked for plugin_id '" << plugin_id + << "' and JuceCLAPWrapper ID is '" << ClapJuceWrapper::desc.id << "'" + << std::endl; + return nullptr; + } + clap_juce_extensions::clap_properties::building_clap = true; + clap_juce_extensions::clap_properties::clap_version_major = CLAP_VERSION_MAJOR; + clap_juce_extensions::clap_properties::clap_version_minor = CLAP_VERSION_MINOR; + clap_juce_extensions::clap_properties::clap_version_revision = CLAP_VERSION_REVISION; + auto *const pluginInstance = ::createPluginFilter(); + clap_juce_extensions::clap_properties::building_clap = false; + auto *wrapper = new ClapJuceWrapper(host, pluginInstance); + return wrapper->clapPlugin(); +} + +const struct clap_plugin_factory juce_clap_plugin_factory = { + ClapAdapter::clap_get_plugin_count, + ClapAdapter::clap_get_plugin_descriptor, + ClapAdapter::clap_create_plugin, +}; + +const void *clap_get_factory(const char *factory_id) +{ + if (strcmp(factory_id, CLAP_PLUGIN_FACTORY_ID) == 0) + { + return &juce_clap_plugin_factory; + } + + return nullptr; +} + +} // namespace ClapAdapter + +extern "C" +{ +#if JUCE_LINUX +#pragma GCC diagnostic ignored "-Wattributes" +#endif + const CLAP_EXPORT struct clap_plugin_entry clap_entry = {CLAP_VERSION, ClapAdapter::clap_init, + ClapAdapter::clap_deinit, + ClapAdapter::clap_get_factory}; +}