DPF

DISTRHO Plugin Framework
Log | Files | Refs | Submodules | README | LICENSE

commit e735e7efdcd2ee1d1c3e91db845c56581cc9d241
parent 12bf589be943cc0a3eecdc4ab40bb5fee6499e6f
Author: falkTX <falktx@falktx.com>
Date:   Sat, 10 Sep 2022 15:43:34 +0100

Implement clap state extension

Diffstat:
Mdistrho/src/DistrhoPluginCLAP.cpp | 455++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mdistrho/src/DistrhoPluginVST2.cpp | 18++++++++++++------
Mdistrho/src/DistrhoUIInternal.hpp | 12++++++------
Mexamples/States/Makefile | 1+
4 files changed, 458 insertions(+), 28 deletions(-)

diff --git a/distrho/src/DistrhoPluginCLAP.cpp b/distrho/src/DistrhoPluginCLAP.cpp @@ -31,15 +31,7 @@ # include "../extra/RingBuffer.hpp" #endif -#if (defined(DISTRHO_OS_MAC) || defined(DISTRHO_OS_WINDOWS)) && ! DISTRHO_PLUGIN_HAS_EXTERNAL_UI -# define DPF_CLAP_USING_HOST_TIMER 0 -#else -# define DPF_CLAP_USING_HOST_TIMER 1 -#endif - -#ifndef DPF_CLAP_TIMER_INTERVAL -# define DPF_CLAP_TIMER_INTERVAL 16 /* ~60 fps */ -#endif +#include <map> #include "clap/entry.h" #include "clap/plugin-factory.h" @@ -50,13 +42,25 @@ #include "clap/ext/state.h" #include "clap/ext/timer-support.h" +#if (defined(DISTRHO_OS_MAC) || defined(DISTRHO_OS_WINDOWS)) && ! DISTRHO_PLUGIN_HAS_EXTERNAL_UI +# define DPF_CLAP_USING_HOST_TIMER 0 +#else +# define DPF_CLAP_USING_HOST_TIMER 1 +#endif + +#ifndef DPF_CLAP_TIMER_INTERVAL +# define DPF_CLAP_TIMER_INTERVAL 16 /* ~60 fps */ +#endif + START_NAMESPACE_DISTRHO // -------------------------------------------------------------------------------------------------------------------- +typedef std::map<const String, String> StringMap; + struct ClapEventQueue { - #if DISTRHO_PLUGIN_HAS_UI + #if DISTRHO_PLUGIN_HAS_UI enum EventType { kEventGestureBegin, kEventGestureEnd, @@ -103,15 +107,22 @@ struct ClapEventQueue std::memcpy(&events[used++], &event, sizeof(Event)); } } fEventQueue; - #endif - #if DISTRHO_PLUGIN_HAS_UI && DISTRHO_PLUGIN_WANT_MIDI_INPUT + #if DISTRHO_PLUGIN_WANT_MIDI_INPUT SmallStackBuffer fNotesBuffer; #endif + #endif - #if DISTRHO_PLUGIN_HAS_UI && DISTRHO_PLUGIN_WANT_STATE + #if DISTRHO_PLUGIN_WANT_PROGRAMS + uint32_t fCurrentProgram; + #endif + + #if DISTRHO_PLUGIN_WANT_STATE + StringMap fStateMap; + #if DISTRHO_PLUGIN_HAS_UI virtual void setStateFromUI(const char* key, const char* value) = 0; #endif + #endif struct CachedParameters { uint numParams; @@ -150,6 +161,9 @@ struct ClapEventQueue #if DISTRHO_PLUGIN_HAS_UI && DISTRHO_PLUGIN_WANT_MIDI_INPUT && ! defined(DISTRHO_PROPER_CPP11_SUPPORT) std::memset(&fNotesBuffer, 0, sizeof(fNotesBuffer)); #endif + #if DISTRHO_PLUGIN_WANT_PROGRAMS + fCurrentProgram = 0; + #endif } virtual ~ClapEventQueue() {} @@ -184,6 +198,12 @@ public: fPluinEventQueue(eventQueue), fEventQueue(eventQueue->fEventQueue), fCachedParameters(eventQueue->fCachedParameters), + #if DISTRHO_PLUGIN_WANT_PROGRAMS + fCurrentProgram(eventQueue->fCurrentProgram), + #endif + #if DISTRHO_PLUGIN_WANT_STATE + fStateMap(eventQueue->fStateMap), + #endif fHost(host), fHostGui(hostGui), #if DPF_CLAP_USING_HOST_TIMER @@ -437,12 +457,42 @@ public: // ---------------------------------------------------------------------------------------------------------------- + void setParameterValueFromPlugin(const uint index, const float value) + { + if (UIExporter* const ui = fUI.get()) + ui->parameterChanged(index, value); + } + + #if DISTRHO_PLUGIN_WANT_PROGRAMS + void setProgramFromPlugin(const uint index) + { + if (UIExporter* const ui = fUI.get()) + ui->programLoaded(index); + } + #endif + + #if DISTRHO_PLUGIN_WANT_STATE + void setStateFromPlugin(const char* const key, const char* const value) + { + if (UIExporter* const ui = fUI.get()) + ui->stateChanged(key, value); + } + #endif + + // ---------------------------------------------------------------------------------------------------------------- + private: // Plugin and UI PluginExporter& fPlugin; ClapEventQueue* const fPluinEventQueue; ClapEventQueue::Queue& fEventQueue; ClapEventQueue::CachedParameters& fCachedParameters; + #if DISTRHO_PLUGIN_WANT_PROGRAMS + uint32_t& fCurrentProgram; + #endif + #if DISTRHO_PLUGIN_WANT_STATE + StringMap& fStateMap; + #endif const clap_host_t* const fHost; const clap_host_gui_t* const fHostGui; #if DPF_CLAP_USING_HOST_TIMER @@ -483,7 +533,31 @@ private: fPlugin.getInstancePointer(), fScaleFactor); - // TODO fetch and set state too + #if DISTRHO_PLUGIN_WANT_PROGRAMS + fUI->programLoaded(fCurrentProgram); + #endif + + #if DISTRHO_PLUGIN_WANT_FULL_STATE + // Update current state from plugin side + for (StringMap::const_iterator cit=fStateMap.begin(), cite=fStateMap.end(); cit != cite; ++cit) + { + const String& key = cit->first; + fStateMap[key] = fPlugin.getStateValue(key); + } + #endif + + #if DISTRHO_PLUGIN_WANT_STATE + // Set state + for (StringMap::const_iterator cit=fStateMap.begin(), cite=fStateMap.end(); cit != cite; ++cit) + { + const String& key = cit->first; + const String& value = cit->second; + + // TODO skip DSP only states + + fUI->stateChanged(key, value); + } + #endif for (uint32_t i=0; i<fCachedParameters.numParams; ++i) { @@ -621,6 +695,13 @@ public: #if DISTRHO_PLUGIN_HAS_UI && DISTRHO_PLUGIN_WANT_MIDI_INPUT fNotesRingBuffer.setRingBuffer(&fNotesBuffer, true); #endif + #if DISTRHO_PLUGIN_WANT_STATE + for (uint32_t i=0, count=fPlugin.getStateCount(); i<count; ++i) + { + const String& dkey(fPlugin.getStateKey(i)); + fStateMap[dkey] = fPlugin.getStateDefaultValue(i); + } + #endif } // ---------------------------------------------------------------------------------------------------------------- @@ -1063,6 +1144,309 @@ public: } // ---------------------------------------------------------------------------------------------------------------- + // state + + bool stateSave(const clap_ostream_t* const stream) + { + const uint32_t paramCount = fPlugin.getParameterCount(); + #if DISTRHO_PLUGIN_WANT_STATE + const uint32_t stateCount = fPlugin.getStateCount(); + #else + const uint32_t stateCount = 0; + #endif + + if (stateCount == 0 && paramCount == 0) + { + char buffer = '\0'; + return stream->write(stream, &buffer, 1) == 1; + } + + #if DISTRHO_PLUGIN_WANT_FULL_STATE + // Update current state + for (StringMap::const_iterator cit=fStateMap.begin(), cite=fStateMap.end(); cit != cite; ++cit) + { + const String& key = cit->first; + fStateMap[key] = fPlugin.getStateValue(key); + } + #endif + + String state; + + #if DISTRHO_PLUGIN_WANT_PROGRAMS + { + String tmpStr("__dpf_program__\xff"); + tmpStr += String(fCurrentProgram); + tmpStr += "\xff"; + + state += tmpStr; + } + #endif + + #if DISTRHO_PLUGIN_WANT_STATE + if (stateCount != 0) + { + state += "__dpf_state_begin__\xff"; + + for (StringMap::const_iterator cit=fStateMap.begin(), cite=fStateMap.end(); cit != cite; ++cit) + { + const String& key = cit->first; + const String& value = cit->second; + + // join key and value + String tmpStr; + tmpStr = key; + tmpStr += "\xff"; + tmpStr += value; + tmpStr += "\xff"; + + state += tmpStr; + } + + state += "__dpf_state_end__\xff"; + } + #endif + + if (paramCount != 0) + { + state += "__dpf_parameters_begin__\xff"; + + for (uint32_t i=0; i<paramCount; ++i) + { + if (fPlugin.isParameterOutputOrTrigger(i)) + continue; + + // join key and value + String tmpStr; + tmpStr = fPlugin.getParameterSymbol(i); + tmpStr += "\xff"; + if (fPlugin.getParameterHints(i) & kParameterIsInteger) + tmpStr += String(static_cast<int>(std::round(fPlugin.getParameterValue(i)))); + else + tmpStr += String(fPlugin.getParameterValue(i)); + tmpStr += "\xff"; + + state += tmpStr; + } + + state += "__dpf_parameters_end__\xff"; + } + + // terminator + state += "\xfe"; + + state.replace('\xff', '\0'); + + // now saving state, carefully until host written bytes matches full state size + const char* buffer = state.buffer(); + const int32_t size = static_cast<int32_t>(state.length())+1; + + for (int32_t wrtntotal = 0, wrtn; wrtntotal < size; wrtntotal += wrtn) + { + wrtn = stream->write(stream, buffer, size - wrtntotal); + DISTRHO_SAFE_ASSERT_INT_RETURN(wrtn > 0, wrtn, false); + } + + return true; + } + + bool stateLoad(const clap_istream_t* const stream) + { + #if DISTRHO_PLUGIN_HAS_UI + ClapUI* const ui = fUI.get(); + #endif + String key, value; + bool hasValue = false; + bool fillingKey = true; // if filling key or value + char queryingType = 'i'; // can be 'n', 's' or 'p' (none, states, parameters) + + char buffer[512], orig; + buffer[sizeof(buffer)-1] = '\xff'; + + for (int32_t terminated = 0, read; terminated == 0;) + { + read = stream->read(stream, buffer, sizeof(buffer)-1); + DISTRHO_SAFE_ASSERT_INT_RETURN(read >= 0, read, false); + + if (read == 0) + return true; + + for (int32_t i = 0; i < read; ++i) + { + // found terminator, stop here + if (buffer[i] == '\xfe') + { + terminated = 1; + break; + } + + // store character at read position + orig = buffer[read]; + + // place null character to create valid string + buffer[read] = '\0'; + + // append to temporary vars + if (fillingKey) + { + key += buffer + i; + } + else + { + value += buffer + i; + hasValue = true; + } + + // increase buffer offset by length of string + i += std::strlen(buffer + i); + + // restore read character + buffer[read] = orig; + + // if buffer offset points to null, we found the end of a string, lets check + if (buffer[i] == '\0') + { + // special keys + if (key == "__dpf_state_begin__") + { + DISTRHO_SAFE_ASSERT_INT_RETURN(queryingType == 'i' || queryingType == 'n', + queryingType, false); + queryingType = 's'; + key.clear(); + value.clear(); + hasValue = false; + continue; + } + if (key == "__dpf_state_end__") + { + DISTRHO_SAFE_ASSERT_INT_RETURN(queryingType == 's', queryingType, false); + queryingType = 'n'; + key.clear(); + value.clear(); + hasValue = false; + continue; + } + if (key == "__dpf_parameters_begin__") + { + DISTRHO_SAFE_ASSERT_INT_RETURN(queryingType == 'i' || queryingType == 'n', + queryingType, false); + queryingType = 'p'; + key.clear(); + value.clear(); + hasValue = false; + continue; + } + if (key == "__dpf_parameters_end__") + { + DISTRHO_SAFE_ASSERT_INT_RETURN(queryingType == 'p', queryingType, false); + queryingType = 'x'; + key.clear(); + value.clear(); + hasValue = false; + continue; + } + + // no special key, swap between reading real key and value + fillingKey = !fillingKey; + + // if there is no value yet keep reading until we have one + if (! hasValue) + continue; + + if (key == "__dpf_program__") + { + DISTRHO_SAFE_ASSERT_INT_RETURN(queryingType == 'i', queryingType, false); + queryingType = 'n'; + + d_debug("found program '%s'", value.buffer()); + + #if DISTRHO_PLUGIN_WANT_PROGRAMS + const int program = std::atoi(value.buffer()); + DISTRHO_SAFE_ASSERT_CONTINUE(program >= 0); + + fCurrentProgram = static_cast<uint32_t>(program); + fPlugin.loadProgram(fCurrentProgram); + + #if DISTRHO_PLUGIN_HAS_UI + if (ui != nullptr) + ui->setProgramFromPlugin(fCurrentProgram); + #endif + #endif + } + else if (queryingType == 's') + { + d_debug("found state '%s' '%s'", key.buffer(), value.buffer()); + + #if DISTRHO_PLUGIN_WANT_STATE + if (fPlugin.wantStateKey(key)) + { + fStateMap[key] = value; + fPlugin.setState(key, value); + + #if DISTRHO_PLUGIN_HAS_UI + if (ui != nullptr) + ui->setStateFromPlugin(key, value); + #endif + } + #endif + } + else if (queryingType == 'p') + { + d_debug("found parameter '%s' '%s'", key.buffer(), value.buffer()); + float fvalue; + + // find parameter with this symbol, and set its value + for (uint32_t j=0; j<fCachedParameters.numParams; ++j) + { + if (fPlugin.isParameterOutputOrTrigger(j)) + continue; + if (fPlugin.getParameterSymbol(j) != key) + continue; + + if (fPlugin.getParameterHints(j) & kParameterIsInteger) + fvalue = std::atoi(value.buffer()); + else + fvalue = std::atof(value.buffer()); + + fCachedParameters.values[j] = fvalue; + #if DISTRHO_PLUGIN_HAS_UI + if (ui != nullptr) + { + // UI parameter updates are handled outside the read loop (after host param restart) + fCachedParameters.changed[j] = true; + } + #endif + fPlugin.setParameterValue(j, fvalue); + break; + } + } + + key.clear(); + value.clear(); + hasValue = false; + } + } + } + + if (const clap_host_params_t* const hostParams = getHostExtension<clap_host_params_t>(CLAP_EXT_PARAMS)) + hostParams->rescan(fHost, CLAP_PARAM_RESCAN_VALUES|CLAP_PARAM_RESCAN_TEXT); + + #if DISTRHO_PLUGIN_HAS_UI + if (ui != nullptr) + { + for (uint32_t i=0; i<fCachedParameters.numParams; ++i) + { + if (fPlugin.isParameterOutputOrTrigger(i)) + continue; + fCachedParameters.changed[i] = false; + ui->setParameterValueFromPlugin(i, fCachedParameters.values[i]); + } + } + #endif + + return true; + } + + // ---------------------------------------------------------------------------------------------------------------- // gui #if DISTRHO_PLUGIN_HAS_UI @@ -1100,7 +1484,23 @@ public: { fPlugin.setState(key, value); - // TODO check if we want to save this key, and save it + // check if we want to save this key + if (! fPlugin.wantStateKey(key)) + return; + + // check if key already exists + for (StringMap::iterator it=fStateMap.begin(), ite=fStateMap.end(); it != ite; ++it) + { + const String& dkey(it->first); + + if (dkey == key) + { + it->second = value; + return; + } + } + + d_stderr("Failed to find plugin state with key \"%s\"", key); } #endif @@ -1124,7 +1524,6 @@ private: RingBufferControl<SmallStackBuffer> fNotesRingBuffer; #endif #endif - #if DISTRHO_PLUGIN_WANT_TIMEPOS TimePosition fTimePosition; #endif @@ -1507,6 +1906,26 @@ static const clap_plugin_params_t clap_plugin_params = { }; // -------------------------------------------------------------------------------------------------------------------- +// plugin state + +static bool clap_plugin_state_save(const clap_plugin_t* const plugin, const clap_ostream_t* const stream) +{ + PluginCLAP* const instance = static_cast<PluginCLAP*>(plugin->plugin_data); + return instance->stateSave(stream); +} + +static bool clap_plugin_state_load(const clap_plugin_t* const plugin, const clap_istream_t* const stream) +{ + PluginCLAP* const instance = static_cast<PluginCLAP*>(plugin->plugin_data); + return instance->stateLoad(stream); +} + +static const clap_plugin_state_t clap_plugin_state = { + clap_plugin_state_save, + clap_plugin_state_load +}; + +// -------------------------------------------------------------------------------------------------------------------- // plugin static bool clap_plugin_init(const clap_plugin_t* const plugin) @@ -1570,6 +1989,10 @@ static const void* clap_plugin_get_extension(const clap_plugin_t*, const char* c return &clap_plugin_note_ports; if (std::strcmp(id, CLAP_EXT_PARAMS) == 0) return &clap_plugin_params; + #if DISTRHO_PLUGIN_WANT_STATE + if (std::strcmp(id, CLAP_EXT_STATE) == 0) + return &clap_plugin_state; + #endif #if DISTRHO_PLUGIN_HAS_UI if (std::strcmp(id, CLAP_EXT_GUI) == 0) return &clap_plugin_gui; diff --git a/distrho/src/DistrhoPluginVST2.cpp b/distrho/src/DistrhoPluginVST2.cpp @@ -209,12 +209,12 @@ public: // ------------------------------------------------------------------- // functions called from the plugin side, may block -# if DISTRHO_PLUGIN_WANT_STATE + #if DISTRHO_PLUGIN_WANT_STATE void setStateFromPlugin(const char* const key, const char* const value) { fUI.stateChanged(key, value); } -# endif + #endif # if !DISTRHO_PLUGIN_HAS_EXTERNAL_UI int handlePluginKeyEvent(const bool down, const int32_t index, const intptr_t value) @@ -611,25 +611,28 @@ public: # endif fVstUI = new UIVst(fAudioMaster, fEffect, this, &fPlugin, (intptr_t)ptr, fLastScaleFactor); -# if DISTRHO_PLUGIN_WANT_FULL_STATE + #if DISTRHO_PLUGIN_WANT_FULL_STATE // Update current state from plugin side for (StringMap::const_iterator cit=fStateMap.begin(), cite=fStateMap.end(); cit != cite; ++cit) { const String& key = cit->first; fStateMap[key] = fPlugin.getStateValue(key); } -# endif + #endif -# if DISTRHO_PLUGIN_WANT_STATE + #if DISTRHO_PLUGIN_WANT_STATE // Set state for (StringMap::const_iterator cit=fStateMap.begin(), cite=fStateMap.end(); cit != cite; ++cit) { const String& key = cit->first; const String& value = cit->second; + // TODO skip DSP only states + fVstUI->setStateFromPlugin(key, value); } -# endif + #endif + for (uint32_t i=0, count=fPlugin.getParameterCount(); i < count; ++i) setParameterValueFromPlugin(i, fPlugin.getParameterValue(i)); @@ -777,7 +780,10 @@ public: # if DISTRHO_PLUGIN_HAS_UI if (fVstUI != nullptr) + { + // TODO skip DSP only states fVstUI->setStateFromPlugin(key, value); + } # endif // get next key diff --git a/distrho/src/DistrhoUIInternal.hpp b/distrho/src/DistrhoUIInternal.hpp @@ -193,16 +193,16 @@ public: ui->parameterChanged(index, value); } -#if DISTRHO_PLUGIN_WANT_PROGRAMS + #if DISTRHO_PLUGIN_WANT_PROGRAMS void programLoaded(const uint32_t index) { DISTRHO_SAFE_ASSERT_RETURN(ui != nullptr,); ui->programLoaded(index); } -#endif + #endif -#if DISTRHO_PLUGIN_WANT_STATE + #if DISTRHO_PLUGIN_WANT_STATE void stateChanged(const char* const key, const char* const value) { DISTRHO_SAFE_ASSERT_RETURN(ui != nullptr,); @@ -211,11 +211,11 @@ public: ui->stateChanged(key, value); } -#endif + #endif // ------------------------------------------------------------------- -#if DISTRHO_UI_IS_STANDALONE + #if DISTRHO_UI_IS_STANDALONE void exec(DGL_NAMESPACE::IdleCallback* const cb) { DISTRHO_SAFE_ASSERT_RETURN(cb != nullptr,); @@ -238,7 +238,7 @@ public: uiData->window->show(); uiData->window->focus(); } -#endif + #endif bool plugin_idle() { diff --git a/examples/States/Makefile b/examples/States/Makefile @@ -30,6 +30,7 @@ TARGETS += jack TARGETS += lv2_sep TARGETS += vst2 TARGETS += vst3 +TARGETS += clap all: $(TARGETS)