diff --git a/examples/GainPlugin/CMakeLists.txt b/examples/GainPlugin/CMakeLists.txt index 6de36b2..4f1a7dc 100644 --- a/examples/GainPlugin/CMakeLists.txt +++ b/examples/GainPlugin/CMakeLists.txt @@ -20,6 +20,7 @@ if(CLAP_WRAP_PROJUCER_PLUGIN) VERSION_STRING "${CMAKE_PROJECT_VERSION}" CLAP_ID "org.free-audio.GainPlugin" CLAP_FEATURES audio-effect utility + CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES 64 ) return() @@ -37,6 +38,7 @@ clap_juce_extensions_plugin( TARGET GainPlugin CLAP_ID "org.free-audio.GainPlugin" CLAP_FEATURES audio-effect utility + CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES 64 ) target_sources(GainPlugin PRIVATE diff --git a/examples/GainPlugin/GainPlugin.cpp b/examples/GainPlugin/GainPlugin.cpp index 662ddf6..9cadc37 100644 --- a/examples/GainPlugin/GainPlugin.cpp +++ b/examples/GainPlugin/GainPlugin.cpp @@ -59,133 +59,34 @@ void GainPlugin::processBlock(juce::AudioBuffer &buffer, juce::MidiBuffer gain.process(juce::dsp::ProcessContextReplacing{block}); } -clap_process_status GainPlugin::clap_direct_process(const clap_process *process) noexcept +bool GainPlugin::supportsDirectEvent(uint16_t space_id, uint16_t type) { - 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 (numEvents > 0) - { - 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); - juce::MidiBuffer midiBuffer; - - static constexpr int smallestBlockSize = 64; - for (int n = 0; n < numSamples;) - { - const auto numSamplesToProcess = - (numSamples - n >= smallestBlockSize) - ? juce::jmax(nextEventTime - n, - smallestBlockSize) // process until next event, but no smaller than - // smallest block size - : (numSamples - n); // process a few leftover samples - - while (nextEventTime < n + numSamplesToProcess && currentEvent < numEvents) - { - auto event = events->get(events, (uint32_t)currentEvent); - process_clap_event(event); - - currentEvent++; - nextEventTime = (currentEvent < numEvents) - ? (int)events->get(events, (uint32_t)currentEvent)->time - : numSamples; - } - - uint32_t outputChannels = 0; - for (uint32_t idx = 0; idx < process->audio_outputs_count && outputChannels < maxBuses; - ++idx) - { - 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 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) - { - if (ic == busses[inputChannels]) - { - // The buffers overlap - no need to do anything - } - else - { - juce::FloatVectorOperations::copy(busses[inputChannels], ic, - numSamplesToProcess); - } - } - else - { - busses[inputChannels] = ic; - } - inputChannels++; - } - } - - auto totalChans = juce::jmax(inputChannels, outputChannels); - juce::AudioBuffer buffer(busses.data(), (int)totalChans, numSamplesToProcess); - - processBlock(buffer, midiBuffer); - - midiBuffer.clear(); - n += numSamplesToProcess; - } - - // process any leftover events - for (; currentEvent < numEvents; ++currentEvent) - { - auto event = events->get(events, (uint32_t)currentEvent); - process_clap_event(event); - } + if (space_id != CLAP_CORE_EVENT_SPACE_ID) + return false; - return CLAP_PROCESS_CONTINUE; + return type == CLAP_EVENT_PARAM_MOD; // custom handling for parameter modulation events only } -void GainPlugin::process_clap_event(const clap_event_header_t *event) +void GainPlugin::handleDirectEvent(const clap_event_header_t *event, int /*sampleOffset*/) { - if (event->space_id != CLAP_CORE_EVENT_SPACE_ID) - return; - - switch (event->type) - { - case CLAP_EVENT_PARAM_VALUE: + if (event->space_id != CLAP_CORE_EVENT_SPACE_ID || event->type != CLAP_EVENT_PARAM_MOD) { - auto paramEvent = reinterpret_cast(event); - handleParameterChange(paramEvent); + // we should not be receiving events of this type! + jassertfalse; + return; } - break; - case CLAP_EVENT_PARAM_MOD: + + // custom handling for parameter modulation events: + auto paramModEvent = reinterpret_cast(event); + auto *modulatableParam = static_cast(paramModEvent->cookie); + if (paramModEvent->note_id >= 0) { - auto paramModEvent = reinterpret_cast(event); - auto *modulatableParam = static_cast(paramModEvent->cookie); - if (paramModEvent->note_id >= 0) - { - // no polyphonic modulation - } - else - { - if (modulatableParam->supportsMonophonicModulation()) - modulatableParam->applyMonophonicModulation(paramModEvent->amount); - } + // no polyphonic modulation } - break; - default: - break; + else + { + if (modulatableParam->supportsMonophonicModulation()) + modulatableParam->applyMonophonicModulation(paramModEvent->amount); } } diff --git a/examples/GainPlugin/GainPlugin.h b/examples/GainPlugin/GainPlugin.h index 5a55cd4..da56ded 100644 --- a/examples/GainPlugin/GainPlugin.h +++ b/examples/GainPlugin/GainPlugin.h @@ -32,8 +32,8 @@ class GainPlugin : public juce::AudioProcessor, void processBlock(juce::AudioBuffer &, juce::MidiBuffer &) override; void processBlock(juce::AudioBuffer &, juce::MidiBuffer &) override {} - bool supportsDirectProcess() override { return true; } - clap_process_status clap_direct_process(const clap_process *process) noexcept override; + bool supportsDirectEvent(uint16_t space_id, uint16_t type) override; + void handleDirectEvent(const clap_event_header_t *event, int sampleOffset) override; bool hasEditor() const override { return true; } juce::AudioProcessorEditor *createEditor() override; @@ -46,8 +46,6 @@ class GainPlugin : public juce::AudioProcessor, auto &getValueTreeState() { return vts; } private: - void process_clap_event(const clap_event_header_t *event); - ModulatableFloatParameter *gainDBParameter = nullptr; juce::AudioProcessorValueTreeState vts; diff --git a/include/clap-juce-extensions/clap-juce-extensions.h b/include/clap-juce-extensions/clap-juce-extensions.h index ef55991..464a748 100644 --- a/include/clap-juce-extensions/clap-juce-extensions.h +++ b/include/clap-juce-extensions/clap-juce-extensions.h @@ -72,11 +72,42 @@ struct clap_juce_audio_processor_capabilities */ virtual bool supportsNoteExpressions() { return false; } + /** + * The regular CLAP/JUCE wrapper handles the following CLAP events: + * - MIDI note on/off events + * - MIDI CC events + * - Parameter change events + * + * If you would like to handle these events using some custom behaviour, or + * if you would like to handle other CLAP events (e.g. parameter modulation or + * note expression), or events from another namespace, you should override + * this method to return true for those event types. + * + * @param space_id The namespace ID for the given event. + * @param type The event type. + */ + virtual bool supportsDirectEvent(uint16_t /*space_id*/, uint16_t /*type*/) { return false; } + + /** + * If your plugin returns true for supportsDirectEvent, then you'll need to + * implement this method to actually handle that event when it comes along. + * + * @param event The header for the incoming event. + * @param sampleOffset If the CLAP wrapper has split up the incoming buffer (e.g. to + * apply sample-accurate automation), then you'll need to apply + * this sample offset to the timestamp of the incoming event + * to get the actual event time relative to the start of the + * next incoming buffer to your processBlock method. For example: + * `const auto actualNoteTime = noteEvent->header.time - sampleOffset;` + */ + virtual void handleDirectEvent(const clap_event_header_t * /*event*/, int /*sampleOffset*/) {} + /* * 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). + * sample accurate parameter automation, and other CLAP features. The custom event handlers + * (above) help make some of these features possible, 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 diff --git a/src/wrapper/clap-juce-wrapper.cpp b/src/wrapper/clap-juce-wrapper.cpp index 8241121..0e90be0 100644 --- a/src/wrapper/clap-juce-wrapper.cpp +++ b/src/wrapper/clap-juce-wrapper.cpp @@ -1050,6 +1050,14 @@ class ClapJuceWrapper : public clap::helpers::Plugin< void process_clap_event(const clap_event_header_t *event, int sampleOffset) { + if (processorAsClapExtensions && + processorAsClapExtensions->supportsDirectEvent(event->space_id, event->type)) + { + // the plugin wants to handle this event with some custom logic + processorAsClapExtensions->handleDirectEvent(event, sampleOffset); + return; + } + if (event->space_id != CLAP_CORE_EVENT_SPACE_ID) return;