diff --git a/README.md b/README.md index 4be69f9..b4f2583 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,12 @@ are available * `CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES` can be set to any integer value to choose the resolution (in samples) used by the wrapper for doing sample-accurate event processing. Setting the value to `0` (the default value) will turn off sample-accurate event processing. +* `CLAP_ALWAYS_SPLIT_BLOCK` can be set to `1` (on), or `0` (off, default), to tell the + wrapper to _always_ attempt to split incoming audio buffers into chunks of size + `CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES`, regardless of any input events being + sent from the host. Note that if the block size provided by the host is not an + even multiple of `CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES`, the plugin may be + required to process a chunk smaller than the chosen resolution. ## Risks of using this library diff --git a/cmake/ClapTargetHelpers.cmake b/cmake/ClapTargetHelpers.cmake index 5a11948..742215b 100644 --- a/cmake/ClapTargetHelpers.cmake +++ b/cmake/ClapTargetHelpers.cmake @@ -1,5 +1,7 @@ function(clap_juce_extensions_plugin_internal) - set(oneValueArgs TARGET TARGET_PATH PLUGIN_NAME IS_JUCER PLUGIN_VERSION DO_COPY CLAP_MANUAL_URL CLAP_SUPPORT_URL CLAP_MISBEHAVIOUR_HANDLER_LEVEL CLAP_CHECKING_LEVEL CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES) + set(oneValueArgs TARGET TARGET_PATH PLUGIN_NAME IS_JUCER PLUGIN_VERSION DO_COPY CLAP_MANUAL_URL CLAP_SUPPORT_URL + CLAP_MISBEHAVIOUR_HANDLER_LEVEL CLAP_CHECKING_LEVEL CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES + CLAP_ALWAYS_SPLIT_BLOCK) set(multiValueArgs CLAP_ID CLAP_FEATURES) cmake_parse_arguments(CJA "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) @@ -38,6 +40,13 @@ function(clap_juce_extensions_plugin_internal) message( STATUS "Setting event resolution to ${CJA_CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES} samples") endif() + if ("${CJA_CLAP_ALWAYS_SPLIT_BLOCK}" STREQUAL "") + message( STATUS "Setting \"Always split block\" to OFF") + set(CJA_CLAP_ALWAYS_SPLIT_BLOCK 0) + else() + message( STATUS "Setting \"Always split block\" to ${CJA_CLAP_ALWAYS_SPLIT_BLOCK}") + 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}\"") @@ -104,6 +113,7 @@ function(clap_juce_extensions_plugin_internal) CLAP_MISBEHAVIOUR_HANDLER_LEVEL=${CJA_CLAP_MISBEHAVIOUR_HANDLER_LEVEL} CLAP_CHECKING_LEVEL=${CJA_CLAP_CHECKING_LEVEL} CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES=${CJA_CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES} + CLAP_ALWAYS_SPLIT_BLOCK=${CJA_CLAP_ALWAYS_SPLIT_BLOCK} ) if(${CJA_IS_JUCER}) diff --git a/src/wrapper/clap-juce-wrapper.cpp b/src/wrapper/clap-juce-wrapper.cpp index 7ac641e..45ef286 100644 --- a/src/wrapper/clap-juce-wrapper.cpp +++ b/src/wrapper/clap-juce-wrapper.cpp @@ -121,6 +121,10 @@ JUCE_BEGIN_IGNORE_WARNINGS_MSVC(4996) // allow strncpy #define CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES 0 // sample-accurate events are off by default #endif +#if !defined(CLAP_ALWAYS_SPLIT_BLOCK) +#define CLAP_ALWAYS_SPLIT_BLOCK 0 +#endif + // This is useful for debugging overrides // #undef CLAP_MISBEHAVIOUR_HANDLER_LEVEL // #define CLAP_MISBEHAVIOUR_HANDLER_LEVEL Terminate @@ -834,7 +838,7 @@ class ClapJuceWrapper : public clap::helpers::Plugin< int currentEvent = 0; int nextEventTime = numSamples; - if (numEvents > 0) // load first event + if (numEvents > 0) // get timestamp for first event { auto event = events->get(events, 0); nextEventTime = (int)event->time; @@ -863,41 +867,62 @@ class ClapJuceWrapper : public clap::helpers::Plugin< // so we'll increment it inside the loop. for (int n = 0; n < numSamples;) { - // in order to know how many samples to process, we need to know when - // is the next timestamp that has an event. In order to know that next - // timestamp, we first need to process all of the events at the current timestamp. - while (nextEventTime == n) - processEvent(n); +#if CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES <= 0 + // Sample-accurate events are turned off, so just process the + // whole block. + const auto numSamplesToProcess = numSamples; +#endif +#if CLAP_ALWAYS_SPLIT_BLOCK && CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES > 0 + // process a block of the given resolution size, or a smaller block + // if there's not enough samples available + const auto numSamplesToProcess = + juce::jmin(CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES, numSamples - n); +#endif + +#if !CLAP_ALWAYS_SPLIT_BLOCK && CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES > 0 const auto numSamplesToProcess = [&]() { - if (CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES <= 0) - { - // Sample-accurate events are turned off, so just process the - // whole block. - return numSamples; - } - else - { - const auto samplesUntilEndOfBlock = numSamples - n; - const auto samplesUntilNextEvent = nextEventTime - n; - - // the number of samples left is less than - // CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES so let's just - // process the rest of the block - if (samplesUntilEndOfBlock <= CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES) - return samplesUntilEndOfBlock; - - // process up until the next event, rounding up to the nearest multiple - // of CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES - const auto numSmallBlocks = - (samplesUntilNextEvent + CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES - 1) / - CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES; - return juce::jmin(numSmallBlocks * CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES, - samplesUntilEndOfBlock); - } + const auto samplesUntilEndOfBlock = numSamples - n; + const auto samplesUntilNextEvent = [&]() { + for (int eventIndex = currentEvent; eventIndex < numEvents; ++eventIndex) + { + auto event = events->get(events, (uint32_t)eventIndex); + if ((int)event->time < n + CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES) + // this event is within the resolution size, so we don't need to split + continue; + + // For now we're only splitting the block on parameter events + // so we can get sample-accurate automation. + // @TODO: are there other events that will require us to split the block? + // like maybe transport events? + if (event->type == CLAP_EVENT_PARAM_VALUE || + event->type == CLAP_EVENT_PARAM_MOD || + event->type == CLAP_EVENT_PARAM_GESTURE_BEGIN || + event->type == CLAP_EVENT_PARAM_GESTURE_END) + { + return (int)event->time - n; + } + } + return samplesUntilEndOfBlock; + }(); + + // the number of samples left is less than + // CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES so let's just + // process the rest of the block + if (samplesUntilEndOfBlock <= CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES) + return samplesUntilEndOfBlock; + + // process up until the next event, rounding up to the nearest multiple + // of CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES + const auto numSmallBlocks = + (samplesUntilNextEvent + CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES - 1) / + CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES; + return juce::jmin(numSmallBlocks * CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES, + samplesUntilEndOfBlock); }(); +#endif - // process the rest of the events in this sub-block + // process the events in this sub-block while (nextEventTime < n + numSamplesToProcess && currentEvent < numEvents) processEvent(n);