From 23f932f295aae79ebe79c4d07df770e238b39208 Mon Sep 17 00:00:00 2001 From: Jatin Chowdhury Date: Fri, 24 Jun 2022 11:57:31 +0100 Subject: [PATCH] Implement optional sample-accurate event processing --- README.md | 5 + cmake/ClapTargetHelpers.cmake | 9 +- cmake/JucerClap.cmake | 13 +- src/wrapper/clap-juce-wrapper.cpp | 322 +++++++++++++++++------------- 4 files changed, 198 insertions(+), 151 deletions(-) diff --git a/README.md b/README.md index db59c07..dc9ae3c 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,11 @@ 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 error checking level for the plugin. +* `CLAP_SMALLEST_ALLOWED_BLOCK_SIZE` can be set to any integer value to choose the + smallest allowed block size for doing sample-accurate event processing. Setting the + value to `0` (the default value) will turn off sample-accurate event processing. ## Risks of using this library diff --git a/cmake/ClapTargetHelpers.cmake b/cmake/ClapTargetHelpers.cmake index e25177f..284d19c 100644 --- a/cmake/ClapTargetHelpers.cmake +++ b/cmake/ClapTargetHelpers.cmake @@ -1,5 +1,5 @@ function(clap_juce_extensions_plugin_internal) - set(oneValueArgs TARGET TARGET_PATH PLUGIN_NAME IS_JUCER DO_COPY CLAP_MANUAL_URL CLAP_SUPPORT_URL CLAP_MISBEHAVIOUR_HANDLER_LEVEL CLAP_CHECKING_LEVEL) + set(oneValueArgs TARGET TARGET_PATH PLUGIN_NAME IS_JUCER DO_COPY CLAP_MANUAL_URL CLAP_SUPPORT_URL CLAP_MISBEHAVIOUR_HANDLER_LEVEL CLAP_CHECKING_LEVEL CLAP_SMALLEST_ALLOWED_BLOCK_SIZE) set(multiValueArgs CLAP_ID CLAP_FEATURES) cmake_parse_arguments(CJA "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) @@ -31,6 +31,13 @@ function(clap_juce_extensions_plugin_internal) message( STATUS "Setting Checking handler level to '${CJA_CLAP_CHECKING_LEVEL}'") endif() + if ("${CJA_CLAP_SMALLEST_ALLOWED_BLOCK_SIZE}" STREQUAL "") + message( STATUS "Setting smallest allowed block size to 0 (no sample-accurate automation)") + set(CJA_CLAP_SMALLEST_ALLOWED_BLOCK_SIZE 0) + else() + message( STATUS "Setting smallest allowed block size to ${CJA_CLAP_SMALLEST_ALLOWED_BLOCK_SIZE}") + 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}\"") diff --git a/cmake/JucerClap.cmake b/cmake/JucerClap.cmake index b395512..496c18a 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 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 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}) @@ -37,15 +37,8 @@ function(create_jucer_clap_target) endif() clap_juce_extensions_plugin_jucer( - TARGET ${CJA_TARGET} TARGET_PATH "${PLUGIN_LIBRARY_PATH}" - PLUGIN_NAME "${CJA_PLUGIN_NAME}" - 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 d00e08f..899d7f3 100644 --- a/src/wrapper/clap-juce-wrapper.cpp +++ b/src/wrapper/clap-juce-wrapper.cpp @@ -109,18 +109,20 @@ JUCE_BEGIN_IGNORE_WARNINGS_MSVC(4996) // allow strncpy #define CLAP_MISBEHAVIOUR_HANDLER_LEVEL "Ignore" #endif - #if !defined(CLAP_CHECKING_LEVEL) #define CLAP_CHECKING_LEVEL "Minimal" #endif +#if !defined(CLAP_SMALLEST_ALLOWED_BLOCK_SIZE) +#define CLAP_SMALLEST_ALLOWED_BLOCK_SIZE 0 // sample-accurate events are off by default +#endif + // This is useful for debugging overrides // #undef CLAP_MISBEHAVIOUR_HANDLER_LEVEL // #define CLAP_MISBEHAVIOUR_HANDLER_LEVEL Terminate // #undef CLAP_CHECKING_LEVEL // #define CLAP_CHECKING_LEVEL Maximal - /* * A little class that sets an atomic bool to a value across its lifetime and * restores it on exit. @@ -242,7 +244,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()); @@ -743,7 +745,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; @@ -819,176 +822,215 @@ 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) { - 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); - - /*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. */ + 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) + for (int n = 0; n < numSamples;) { - for (uint32_t ch = 0; ch < process->audio_outputs[idx].channel_count; ++ch) + auto getSamplesToProcess = [&]() { + if (CLAP_SMALLEST_ALLOWED_BLOCK_SIZE <= 0) + { + // Sample-accurate events are turned off, so just process the whole block! + return numSamples; + } + else + { + // How many samples should we process at a time? + // In the spirit of sample-accurate events, we want to process a batch of + // samples until we hit the next event, but we don't want to have a batch + // smaller than the `CLAP_SMALLEST_ALLOWED_BLOCK_SIZE`. If there's no more + // events, just process the rest of the block! + return (numSamples - n >= CLAP_SMALLEST_ALLOWED_BLOCK_SIZE) + ? juce::jmax(nextEventTime - n, CLAP_SMALLEST_ALLOWED_BLOCK_SIZE) + : (numSamples - n); + } + }; + + const auto numSamplesToProcess = getSamplesToProcess(); + while (nextEventTime < n + numSamplesToProcess && currentEvent < numEvents) { - busses[ochans] = process->audio_outputs[idx].data32[ch]; - ochans++; + auto event = events->get(events, (uint32_t)currentEvent); + process_clap_event(event, n); + + currentEvent++; + nextEventTime = (currentEvent < numEvents) + ? (int)events->get(events, (uint32_t)currentEvent)->time + : numSamples; } - } - 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 outputChannels = 0; + for (uint32_t idx = 0; idx < process->audio_outputs_count && outputChannels < maxBuses; + ++idx) { - auto *ic = process->audio_inputs[idx].data32[ch]; - if (ichans < ochans) + for (uint32_t ch = 0; ch < process->audio_outputs[idx].channel_count; ++ch) { - if (ic == busses[ichans]) + busses[outputChannels] = process->audio_outputs[idx].data32[ch] + n; + outputChannels++; + } + } + + uint32_t inputChannels = 0; + for (uint32_t idx = 0; idx < process->audio_inputs_count && inputChannels < maxBuses; + ++idx) + { + for (uint32_t ch = 0; ch < process->audio_inputs[idx].channel_count; ++ch) + { + 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 + for (; currentEvent < numEvents; ++currentEvent) + { + auto event = events->get(events, (uint32_t)currentEvent); + process_clap_event(event, 0); + } 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 { @@ -1035,7 +1077,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 @@ -1050,7 +1092,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;