migrating to the latest JUCE version
This commit is contained in:
250
deps/clap-juce-extensions/docs/HowToParameterModulation.md
vendored
Normal file
250
deps/clap-juce-extensions/docs/HowToParameterModulation.md
vendored
Normal file
@ -0,0 +1,250 @@
|
||||
# How To: Parameter Modulation
|
||||
|
||||
One of the most exciting features available to CLAP plugins is
|
||||
[non-destructive parameter modulation](https://www.youtube.com/embed/B2mywWyI9es).
|
||||
|
||||
If you're using this repository to build your JUCE-based plugin as a CLAP,
|
||||
you may be wondering how to get parameter modulation working with your plugin.
|
||||
This document should be able to help you get started.
|
||||
|
||||
It is worth repeating that if JUCE implements CLAP support "natively"
|
||||
in the future, it is unlikely that the approach outlined here would be
|
||||
compatible with that implementation.
|
||||
|
||||
Note that the workflow below pre-supposes that your plugin uses the modern
|
||||
JUCE parameter classes. If your plugin is using JUCE's "legacy" parameter
|
||||
mechanisms, then the CLAP JUCE wrapper cannot support parameter modulation
|
||||
(although regular parameter functionality will still work).
|
||||
|
||||
## Monophonic Modulation
|
||||
|
||||
For audio effects, monophonic synthesizers, and global parameters on
|
||||
polyphonic synthesizers, monophonic parameter modulation can be enabled
|
||||
as follows.
|
||||
|
||||
1. Link your plugin to the `clap_juce_extensions` header.
|
||||
- For a CMake project, this can be done by adding
|
||||
`target_link_libraries(MyPlugin PUBLIC clap_juce_extensions)`
|
||||
to your CMake configuration.
|
||||
- For a Projucer project, the user will need to add the
|
||||
following include paths to your Projucer configuration:
|
||||
- `path/to/clap-juce-extensions/include`
|
||||
- `path/to/clap-juce-extensions/clap-libs/clap/include`
|
||||
- `path/to/clap-juce-extensions/clap-libs/clap-helpers/include`
|
||||
|
||||
You'll also need to add the following file to your Projucer
|
||||
source files:
|
||||
- `path/to/clap-juce-extensions/src/extensions/clap-juce-extensions.cpp`
|
||||
|
||||
2. Implement a custom parameter type, derived from
|
||||
`clap_juce_extensions::clap_juce_parameter_capabilities`.
|
||||
An example can be seen [here](../examples/GainPlugin/ModulatableFloatParameter.h).
|
||||
|
||||
3. Implement `supportsMonophonicModulation()` and
|
||||
`applyMonophonicModulation()` for your custom parameter.
|
||||
The final parameter class should look something like this:
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
|
||||
#include <juce_audio_utils/juce_audio_utils.h>
|
||||
#include <clap-juce-extensions/clap-juce-extensions.h>
|
||||
|
||||
class ModulatableFloatParameter : public juce::AudioParameterFloat,
|
||||
public clap_juce_extensions::clap_juce_parameter_capabilities
|
||||
{
|
||||
public:
|
||||
ModulatableFloatParameter (/* args */) {} // implement your constructor
|
||||
|
||||
bool supportsMonophonicModulation() override { return true; }
|
||||
|
||||
void applyMonophonicModulation(double modulationValue) override
|
||||
{
|
||||
// do something with the modulation value here...
|
||||
}
|
||||
|
||||
float getCurrentValue() const noexcept
|
||||
{
|
||||
// return parameter value with modulation applied...
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Any parameters in your plugin that should support non-destructuve modulation
|
||||
should be derived from your custom parameter class. It's also important to
|
||||
make sure that when accessing the parameter's value (for example, in your
|
||||
`processBlock()` method), that you are calling `getCurrentValue()` (or the
|
||||
equivalent in your code), rather than using the standard JUCE parameter APIs
|
||||
for getting the parameter value, otherwise the processor will be using the
|
||||
un-modulated value of the parameter. Notably, this constraint includes
|
||||
`juce::AudioProcessorValueTreeState::getRawParameterValue()`.
|
||||
|
||||
4. Add `CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES` to your CLAP CMake arguments.
|
||||
```cmake
|
||||
# For CMake plugins:
|
||||
clap_juce_extensions_plugin(TARGET my-target
|
||||
CLAP_ID "com.my-cool-plugs.my-target"
|
||||
CLAP_FEATURES instrument "virtual analog" gritty basses leads pads
|
||||
CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES 64)
|
||||
|
||||
# For Projucer plugins:
|
||||
create_jucer_clap_target(
|
||||
TARGET MyPlugin
|
||||
PLUGIN_NAME "My Plugin"
|
||||
BINARY_NAME "MyPlugin"
|
||||
MANUFACTURER_NAME "My Company"
|
||||
MANUFACTURER_CODE Manu
|
||||
PLUGIN_CODE Plg1
|
||||
VERSION_STRING "1.0.0"
|
||||
CLAP_ID "org.mycompany.myplugin"
|
||||
CLAP_FEATURES instrument synthesizer
|
||||
CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES 64
|
||||
)
|
||||
```
|
||||
While not strictly necessary, defining `CLAP_PROCESS_EVENTS_RESOLUTION_SAMPLES`
|
||||
will allow the plugin to respond to modulation events sent from the host at a
|
||||
finer resolution than the host's block size. Bitwig Studio sends modulation
|
||||
events at a resolution of 64 samples, but you may want to use a different resolution
|
||||
size, for example, if your plugin already has a modulation system which uses a
|
||||
different sample resolution for modulation.
|
||||
|
||||
And that's it! Now your plugin should support monophonic modulation!
|
||||
|
||||
## Polyphonic Modulation
|
||||
|
||||
If your plugin is a polyphonic synthesizer or MIDI effect, you may also want to
|
||||
implement polyphonic modulation for some parameters. It should be noted that
|
||||
any parameter which supports polyphonic modulation is also expected to support
|
||||
monophonic modulation, so make sure to complete the steps from the previous
|
||||
section before following along here.
|
||||
|
||||
1. Implement `supportsPolyphonicModulation()` and
|
||||
`applyPolyphonicModulation()` for your custom parameter type.
|
||||
```cpp
|
||||
class PolyModulatableFloatParameter : public ModulatableFloatParameter
|
||||
{
|
||||
public:
|
||||
PolyModulatableFloatParameter (/* args */) {} // implement your constructor
|
||||
|
||||
bool supportsPolyphonicModulation() override { return true; }
|
||||
|
||||
void applyPolyphonicModulation(int32_t note_id, int16_t port_index,
|
||||
int16_t channel, int16_t key,
|
||||
double amount) override
|
||||
{
|
||||
// apply the modulation here
|
||||
}
|
||||
|
||||
float getCurrentValuePoly(int32_t note_id, int16_t port_index,
|
||||
int16_t channel, int16_t key) const noexcept
|
||||
{
|
||||
// return parameter value with modulation applied
|
||||
// for this note, channel, etc ...
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
2. Next we need our plugin to keep track of note IDs so we can know
|
||||
which modulation events should apply to which notes. This can be
|
||||
done by implementing custom event handlers for incoming note events.
|
||||
```cpp
|
||||
class MyPlugin : public juce::AudioProcessor,
|
||||
public clap_juce_extensions::clap_juce_audio_processor_capabilities
|
||||
{
|
||||
public:
|
||||
// All your other code here...
|
||||
|
||||
bool supportsDirectEvent(uint16_t space_id, uint16_t type) override
|
||||
{
|
||||
if (space_id != CLAP_CORE_EVENT_SPACE_ID)
|
||||
return false; // only handle events in the core namespace
|
||||
|
||||
// do custom handling for note events
|
||||
return type == CLAP_EVENT_NOTE_ON
|
||||
&& type == CLAP_EVENT_NOTE_OFF;
|
||||
}
|
||||
|
||||
void handleDirectEvent(const clap_event_header_t *evt, int sampleOffset) override
|
||||
{
|
||||
if (evt->space_id != CLAP_CORE_EVENT_SPACE_ID)
|
||||
return;
|
||||
|
||||
switch (evt->type)
|
||||
{
|
||||
case CLAP_EVENT_NOTE_ON:
|
||||
{
|
||||
auto nevt = reinterpret_cast<const clap_event_note *>(evt);
|
||||
const auto note_id = nevt->note_id;
|
||||
// start the note here...
|
||||
}
|
||||
break;
|
||||
case CLAP_EVENT_NOTE_OFF:
|
||||
{
|
||||
auto nevt = reinterpret_cast<const clap_event_note *>(evt);
|
||||
const auto note_id = nevt->note_id;
|
||||
// end the note here...
|
||||
}
|
||||
break;
|
||||
};
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
3. Finally, we need to tell the host when each note is ending, by adding note
|
||||
end events to the output event queue:
|
||||
```cpp
|
||||
class MyPlugin : public juce::AudioProcessor,
|
||||
public clap_juce_extensions::clap_juce_audio_processor_capabilities
|
||||
{
|
||||
public:
|
||||
// All your other code here...
|
||||
|
||||
bool supportsOutboundEvents() override { return true; }
|
||||
void addOutboundEventsToQueue(const clap_output_events *out_events,
|
||||
const juce::MidiBuffer &midiBuffer, int sampleOffset) override
|
||||
{
|
||||
// Assuming the plugin has implemented some container `notesThatEndedDuringLastBlock`
|
||||
// to hold information for all the notes that ended during the previous `processBlock()`
|
||||
for (auto& noteEndEvent : notesThatEndedDuringLastBlock)
|
||||
{
|
||||
auto evt = clap_event_note();
|
||||
evt.header.size = sizeof(clap_event_note);
|
||||
evt.header.type = (uint16_t)CLAP_EVENT_NOTE_END;
|
||||
evt.header.space_id = CLAP_CORE_EVENT_SPACE_ID;
|
||||
evt.header.flags = 0;
|
||||
|
||||
// The way the CLAP/JUCE wrapper is able to accomplish sample-accurate
|
||||
// parameter automation/modulation is by splitting up the incoming CLAP
|
||||
// audio block into multiple JUCE `processBlock()` calls. So when adding
|
||||
// events to the output event queue, we need to take the note end time
|
||||
// relative to the last `processBlock()` call and add the sample offset
|
||||
// to get the correct event time relative to the start of the CLAP block.
|
||||
evt.header.time = uint32_t(noteEndEvent.noteEndTime + sampleOffset);
|
||||
|
||||
// some of these values may be zero, for example
|
||||
// your synth might not have a concept of "note ports".
|
||||
evt.port_index = noteEndEvent.notePort;
|
||||
evt.channel = noteEndEvent.channel;
|
||||
evt.key = noteEndEvent.key;
|
||||
evt.note_id = noteEndEvent.noteID;
|
||||
evt.velocity = noteEndEvent.velocity;
|
||||
|
||||
out_events->try_push(out_events, reinterpret_cast<const clap_event_header *>(&evt));
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
For plugins that produce MIDI, extra care needs to be taken during this step.
|
||||
Any MIDI events in the `juce::MidiBuffer` which is passed to
|
||||
`addOutboundEventsToQueue()` will also need to be added to the output event
|
||||
queue, however, since all the events in the queue need to be ordered sequentially,
|
||||
the implementer may need to "interleave" the note end events with the MIDI events
|
||||
in order to make sure all the events are in the correct order.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you run into difficulties when trying to implement parameter modulation in your
|
||||
plugin, please create a GitHub Issue in this repo. If the wrapper API for supporting
|
||||
parameter modulation appears to be incomplete, Pull Requests for improving the API
|
||||
are welcome!
|
Reference in New Issue
Block a user