From eae6d5eafb7960434dd10e3768b206981c517233 Mon Sep 17 00:00:00 2001 From: jatinchowdhury18 Date: Sat, 2 Jul 2022 12:04:01 +0100 Subject: [PATCH] Implement optional sample-accurate event processing (#78) * Implement optional sample-accurate event processing * Updates to sample-accurte event processing * Better approach for handling events that are at the start of a sub-block * Fix typo * Undo silly submodule mistake * Adjust sample-accurate events to always be a multiple of the chosen resolution, as long as the block size provided by the host cooperates * Undo testing changes to example plugin * Add option to always split block, otherwise only split block for parameter events * Split block on transport events, don't split block for events outside the core namespace --- README.md | 11 + cmake/ClapTargetHelpers.cmake | 20 +- cmake/JucerClap.cmake | 12 +- src/wrapper/clap-juce-wrapper.cpp | 357 ++++++++++++++++++------------ 4 files changed, 254 insertions(+), 146 deletions(-) diff --git a/README.md b/README.md index bc57400..b4f2583 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,17 @@ are available * `CLAP_MANUAL_URL` and `CLAP_SUPPORT_URL` generate the urls in your description * `CLAP_MISBHEAVIOUR_HANDLER_LEVEL` can be set to `Terminate` or `Ignore` (default is `Ignore`) to choose your behaviour for a misbehaving host. +* `CLAP_CHECKING_LEVEL` can be set to `None`, `Minimal`, or `Maximal` (default is + `Minimal`) to choose the level of sanity checks enabled for the plugin. +* `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 2601e14..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) + 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}) @@ -31,6 +33,20 @@ function(clap_juce_extensions_plugin_internal) message( STATUS "Setting Checking handler level to '${CJA_CLAP_CHECKING_LEVEL}'") endif() + if ("${CJA_CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES}" STREQUAL "") + message( STATUS "Setting event resolution to 0 samples (no sample-accurate automation)") + set(CJA_CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES 0) + else() + 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}\"") @@ -96,6 +112,8 @@ function(clap_juce_extensions_plugin_internal) CLAP_SUPPORT_URL="${CJA_CLAP_SUPPORT_URL}" 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/cmake/JucerClap.cmake b/cmake/JucerClap.cmake index 17d86b5..6b73f3f 100644 --- a/cmake/JucerClap.cmake +++ b/cmake/JucerClap.cmake @@ -1,7 +1,7 @@ # use this function to create a CLAP from a jucer project function(create_jucer_clap_target) - set(oneValueArgs TARGET PLUGIN_NAME BINARY_NAME MANUFACTURER_NAME MANUFACTURER_URL VERSION_STRING MANUFACTURER_CODE PLUGIN_CODE EDITOR_NEEDS_KEYBOARD_FOCUS MISBEHAVIOUR_HANDLER_LEVEL CHECKING_LEVEL) - set(multiValueArgs CLAP_ID CLAP_FEATURES CLAP_MANUAL_URL CLAP_SUPPORT_URL) + set(oneValueArgs TARGET PLUGIN_NAME BINARY_NAME MANUFACTURER_NAME MANUFACTURER_URL VERSION_STRING MANUFACTURER_CODE PLUGIN_CODE EDITOR_NEEDS_KEYBOARD_FOCUS) + set(multiValueArgs CLAP_FEATURES) cmake_parse_arguments(CJA "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) @@ -41,16 +41,10 @@ function(create_jucer_clap_target) endif() clap_juce_extensions_plugin_jucer( - TARGET ${CJA_TARGET} TARGET_PATH "${PLUGIN_LIBRARY_PATH}" PLUGIN_NAME "${CJA_BINARY_NAME}" PLUGIN_VERSION "${CJA_VERSION_STRING}" - CLAP_ID "${CJA_CLAP_ID}" - CLAP_FEATURES "${CJA_CLAP_FEATURES}" - CLAP_MANUAL_URL "${CJA_CLAP_MANUAL_URL}" - CLAP_SUPPORT_URL "${CJA_CLAP_SUPPORT_URL}" - CLAP_MISBEHAVIOUR_HANDLER_LEVEL "${CJA_MISBEHAVIOUR_HANDLER_LEVEL}" - CLAP_CHECKING_LEVEL "${CJA_CHECKING_LEVEL}" + ${ARGV} ) string(REPLACE " " "_" clap_target "${CJA_TARGET}_CLAP") diff --git a/src/wrapper/clap-juce-wrapper.cpp b/src/wrapper/clap-juce-wrapper.cpp index db2e509..edf30d1 100644 --- a/src/wrapper/clap-juce-wrapper.cpp +++ b/src/wrapper/clap-juce-wrapper.cpp @@ -117,6 +117,14 @@ JUCE_BEGIN_IGNORE_WARNINGS_MSVC(4996) // allow strncpy #define CLAP_CHECKING_LEVEL "Minimal" #endif +#if !defined(CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES) +#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 @@ -246,7 +254,7 @@ class ClapJuceWrapper : public clap::helpers::Plugin< clap_id idleTimer{0}; - uint32_t generateClapIDForJuceParam(juce::AudioProcessorParameter *param) const + static uint32_t generateClapIDForJuceParam(juce::AudioProcessorParameter *param) { auto juceParamID = juce::LegacyAudioParameter::getParamID(param, false); auto clapID = static_cast(juceParamID.hashCode()); @@ -761,7 +769,8 @@ class ClapJuceWrapper : public clap::helpers::Plugin< return true; } - void paramSetValueAndNotifyIfChanged(juce::AudioProcessorParameter ¶m, float newValue) + static void paramSetValueAndNotifyIfChanged(juce::AudioProcessorParameter ¶m, + float newValue) { if (param.getValue() == newValue) return; @@ -837,176 +846,252 @@ class ClapJuceWrapper : public clap::helpers::Plugin< if (processorAsClapExtensions && processorAsClapExtensions->supportsDirectProcess()) return processorAsClapExtensions->clap_direct_process(process); - auto ev = process->in_events; - auto sz = ev->size(ev); + const auto numSamples = (int)process->frames_count; + auto events = process->in_events; + auto numEvents = (int)events->size(events); + int currentEvent = 0; + int nextEventTime = numSamples; - if (sz != 0) + if (numEvents > 0) // get timestamp for first event { - 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 nf = pevt->value; - jassert(pevt->cookie == paramPtrByClapID[pevt->param_id]); - auto jp = static_cast(pevt->cookie); - - /* - * In the event that a param value comes in from the host, we don't want - * to send it back out as a UI message but we do want to trigger any *other* - * listeners which may be attached. So suppress my listeners while we send this - * event. - */ - auto g = AtomicTGuard(supressParameterChangeMessages, true); - 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; - } - } + auto event = events->get(events, 0); + nextEventTime = (int)event->time; } - // We process in place so - static constexpr uint32_t maxBuses = 128; - std::array busses{}; - busses.fill(nullptr); + auto processEvent = [&](int sampleOffset) { + auto event = events->get(events, (uint32_t)currentEvent); + process_clap_event(event, sampleOffset); - /*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()); - */ + currentEvent++; + nextEventTime = (currentEvent < numEvents) + ? (int)events->get(events, (uint32_t)currentEvent)->time + : numSamples; + }; /* * 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. */ + static constexpr uint32_t maxBuses = 128; + std::array busses{}; + busses.fill(nullptr); - /* - * 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) + // we can't advance `n` until we know how many samples we're processing, + // so we'll increment it inside the loop. + for (int n = 0; n < numSamples;) { - for (uint32_t ch = 0; ch < process->audio_outputs[idx].channel_count; ++ch) +#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 = [&]() { + 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; + + if (event->space_id != CLAP_CORE_EVENT_SPACE_ID) + continue; // never split for events that are not in the core namespace + + // For now we're only splitting the block on parameter events + // so we can get sample-accurate automation, and transport events. + if (event->type == CLAP_EVENT_PARAM_VALUE || + event->type == CLAP_EVENT_PARAM_MOD || + event->type == CLAP_EVENT_TRANSPORT) + { + 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 events in this sub-block + while (nextEventTime < n + numSamplesToProcess && currentEvent < numEvents) + processEvent(n); + + uint32_t outputChannels = 0; + for (uint32_t idx = 0; idx < process->audio_outputs_count && outputChannels < maxBuses; + ++idx) { - busses[ochans] = process->audio_outputs[idx].data32[ch]; - ochans++; + for (uint32_t ch = 0; ch < process->audio_outputs[idx].channel_count; ++ch) + { + busses[outputChannels] = process->audio_outputs[idx].data32[ch] + n; + outputChannels++; + } } - } - 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) + uint32_t inputChannels = 0; + for (uint32_t idx = 0; idx < process->audio_inputs_count && inputChannels < maxBuses; + ++idx) { - auto *ic = process->audio_inputs[idx].data32[ch]; - if (ichans < ochans) + for (uint32_t ch = 0; ch < process->audio_inputs[idx].channel_count; ++ch) { - if (ic == busses[ichans]) + auto *ic = process->audio_inputs[idx].data32[ch] + n; + if (inputChannels < outputChannels) { - // The buffers overlap - no need to do anything + if (ic == busses[inputChannels]) + { + // The buffers overlap - no need to do anything + } + else + { + juce::FloatVectorOperations::copy(busses[inputChannels], ic, + numSamplesToProcess); + } } else { - juce::FloatVectorOperations::copy(busses[ichans], ic, - (int)process->frames_count); + busses[inputChannels] = ic; } + inputChannels++; } - else - { - busses[ichans] = ic; - } - ichans++; } - } - auto totalChans = std::max(ichans, ochans); - juce::AudioBuffer buf(busses.data(), (int)totalChans, (int)process->frames_count); + auto totalChans = juce::jmax(inputChannels, outputChannels); + juce::AudioBuffer buffer(busses.data(), (int)totalChans, numSamplesToProcess); - FIXME("Handle bypass and deactivated states"); - processor->processBlock(buf, midiBuffer); + FIXME("Handle bypass and deactivated states") + processor->processBlock(buffer, midiBuffer); - if (processor->producesMidi()) - { - for (auto meta : midiBuffer) + if (processor->producesMidi()) { - auto msg = meta.getMessage(); - if (msg.getRawDataSize() == 3) + for (auto meta : midiBuffer) { - auto evt = clap_event_midi(); - evt.header.size = sizeof(clap_event_midi); - evt.header.type = (uint16_t)CLAP_EVENT_MIDI; - evt.header.time = (uint32_t)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)); + 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 = uint32_t(meta.samplePosition + n); + 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(); + + n += numSamplesToProcess; } - if (!midiBuffer.isEmpty()) - midiBuffer.clear(); + // process any leftover events + while (currentEvent < numEvents) + processEvent(numSamples); return CLAP_PROCESS_CONTINUE; } + void process_clap_event(const clap_event_header_t *event, int sampleOffset) + { + if (event->space_id != CLAP_CORE_EVENT_SPACE_ID) + return; + + switch (event->type) + { + case CLAP_EVENT_NOTE_ON: + { + auto noteEvent = reinterpret_cast(event); + + midiBuffer.addEvent(juce::MidiMessage::noteOn(noteEvent->channel + 1, noteEvent->key, + (float)noteEvent->velocity), + (int)noteEvent->header.time - sampleOffset); + } + break; + case CLAP_EVENT_NOTE_OFF: + { + auto noteEvent = reinterpret_cast(event); + midiBuffer.addEvent(juce::MidiMessage::noteOff(noteEvent->channel + 1, noteEvent->key, + (float)noteEvent->velocity), + (int)noteEvent->header.time - sampleOffset); + } + break; + case CLAP_EVENT_MIDI: + { + auto midiEvent = reinterpret_cast(event); + midiBuffer.addEvent(juce::MidiMessage(midiEvent->data[0], midiEvent->data[1], + midiEvent->data[2], midiEvent->header.time), + (int)midiEvent->header.time - sampleOffset); + } + break; + case CLAP_EVENT_TRANSPORT: + { + // handle this case + } + break; + case CLAP_EVENT_PARAM_VALUE: + { + auto paramEvent = reinterpret_cast(event); + + auto nf = paramEvent->value; + jassert(paramEvent->cookie == paramPtrByClapID[paramEvent->param_id]); + auto jp = static_cast(paramEvent->cookie); + + /* + * In the event that a param value comes in from the host, we don't want + * to send it back out as a UI message but we do want to trigger any *other* + * listeners which may be attached. So suppress my listeners while we send this + * event. + */ + auto g = AtomicTGuard(supressParameterChangeMessages, true); + paramSetValueAndNotifyIfChanged(*jp, (float)nf); + } + break; + case CLAP_EVENT_PARAM_MOD: + { + // no way to handle this with built-in JUCE parameter mechanisms + } + break; + case CLAP_EVENT_NOTE_END: + { + // Why do you send me this, Alex? + } + break; + default: + { + DBG("Unknown message type " << (int)event->type); + // In theory I should never get this. + // jassertfalse + } + break; + } + } + void componentMovedOrResized(juce::Component &component, bool wasMoved, bool wasResized) override { @@ -1053,7 +1138,7 @@ class ClapJuceWrapper : public clap::helpers::Plugin< auto aspectRatio = (float)cst->getFixedAspectRatio(); - if (aspectRatio != 0.0) + if (aspectRatio != 0.0f) { /* * This is obviously an unsatisfactory algorithm, but we wanted to have @@ -1068,7 +1153,7 @@ class ClapJuceWrapper : public clap::helpers::Plugin< * So for now here's this approach. See the discussion in CJE PR #67 * and interop-tracker issue #30. */ - width = std::round(aspectRatio * height); + width = (uint32_t)std::round(aspectRatio * (float)height); } *w = width;