Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interface for custom event handling #88

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/GainPlugin/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down
137 changes: 19 additions & 118 deletions examples/GainPlugin/GainPlugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,133 +59,34 @@ void GainPlugin::processBlock(juce::AudioBuffer<float> &buffer, juce::MidiBuffer
gain.process(juce::dsp::ProcessContextReplacing<float>{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<float *, maxBuses> 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<float> 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<const clap_event_param_value *>(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<const clap_event_param_mod *>(event);
auto *modulatableParam = static_cast<ModulatableFloatParameter *>(paramModEvent->cookie);
if (paramModEvent->note_id >= 0)
{
auto paramModEvent = reinterpret_cast<const clap_event_param_mod *>(event);
auto *modulatableParam = static_cast<ModulatableFloatParameter *>(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);
}
}

Expand Down
6 changes: 2 additions & 4 deletions examples/GainPlugin/GainPlugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ class GainPlugin : public juce::AudioProcessor,
void processBlock(juce::AudioBuffer<float> &, juce::MidiBuffer &) override;
void processBlock(juce::AudioBuffer<double> &, 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;
Expand All @@ -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;
Expand Down
37 changes: 34 additions & 3 deletions include/clap-juce-extensions/clap-juce-extensions.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions src/wrapper/clap-juce-wrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down