1
0

Send events on parameter gesture/change

This commit is contained in:
Geraint 2025-06-26 15:51:43 +01:00
parent 4e646da32b
commit 124fd4592a
4 changed files with 103 additions and 69 deletions

View File

@ -133,6 +133,7 @@ template<template<class> class EffectSTFX>
struct Plugin : public clap_plugin { struct Plugin : public clap_plugin {
const Plugins &plugins; const Plugins &plugins;
const clap_host *host; const clap_host *host;
const clap_host_params *hostParams = nullptr;
const clap_host_webview1 *hostWebview1 = nullptr; const clap_host_webview1 *hostWebview1 = nullptr;
using Effect = stfx::web::WebUILibraryEffect<float, EffectSTFX>; using Effect = stfx::web::WebUILibraryEffect<float, EffectSTFX>;
Effect effect; Effect effect;
@ -152,16 +153,6 @@ struct Plugin : public clap_plugin {
this->get_extension = plugin_get_extension; this->get_extension = plugin_get_extension;
this->on_main_thread = plugin_on_main_thread; this->on_main_thread = plugin_on_main_thread;
effect.paramListenerRange = [&](stfx::web::ParamContext context, double v){
std::cout << "param #" << context.index << " change: " << v << "\n";
};
effect.paramListenerGesture = [&](stfx::web::ParamContext context, bool gesture){
std::cout << "param #" << context.index << " gesture: " << gesture << "\n";
};
effect.paramListenerStepped = [&](stfx::web::ParamContext context, int v){
std::cout << "param #" << context.index << " change: " << v << "\n";
};
scanParams(); scanParams();
} }
@ -207,6 +198,7 @@ struct Plugin : public clap_plugin {
auto &plugin = *(Plugin *)obj; auto &plugin = *(Plugin *)obj;
#define STFX_GET_EXT(field, extId) \ #define STFX_GET_EXT(field, extId) \
plugin.field = (decltype(plugin.field))plugin.host->get_extension(plugin.host, extId); plugin.field = (decltype(plugin.field))plugin.host->get_extension(plugin.host, extId);
STFX_GET_EXT(hostParams, CLAP_EXT_PARAMS)
STFX_GET_EXT(hostWebview1, CLAP_EXT_WEBVIEW1) STFX_GET_EXT(hostWebview1, CLAP_EXT_WEBVIEW1)
#undef STFX_GET_EXT #undef STFX_GET_EXT
return true; return true;
@ -319,11 +311,10 @@ struct Plugin : public clap_plugin {
inputBuffers.resize(0); inputBuffers.resize(0);
outputBuffers.resize(0); outputBuffers.resize(0);
plugin.host->request_callback(plugin.host);
if (plugin.effect.hasPendingWebMessage()) { if (plugin.effect.hasPendingWebMessage()) {
plugin.host->request_callback(plugin.host); plugin.host->request_callback(plugin.host);
} }
plugin.sendParamEvents(process->out_events);
return CLAP_PROCESS_CONTINUE; return CLAP_PROCESS_CONTINUE;
} }
static void plugin_on_main_thread(const clap_plugin *obj) { static void plugin_on_main_thread(const clap_plugin *obj) {
@ -338,6 +329,13 @@ plugin.host->request_callback(plugin.host);
typename Effect::ParamStepped *steppedParam = nullptr; typename Effect::ParamStepped *steppedParam = nullptr;
std::optional<SteppedParamInfo> steppedInfo; std::optional<SteppedParamInfo> steppedInfo;
std::atomic_flag hostValueSent = ATOMIC_FLAG_INIT;
std::atomic_flag hostStartGestureSent = ATOMIC_FLAG_INIT;
std::atomic_flag hostStopGestureSent = ATOMIC_FLAG_INIT;
Param() {}
Param(Param &&other) : clap_param_info(other), rangeParam(other.rangeParam), rangeInfo(std::move(other.rangeInfo)), steppedParam(other.steppedParam), steppedInfo(std::move(other.steppedInfo)) {}
void setClapInfo() { void setClapInfo() {
flags |= CLAP_PARAM_IS_AUTOMATABLE; flags |= CLAP_PARAM_IS_AUTOMATABLE;
cookie = this; cookie = this;
@ -354,6 +352,10 @@ plugin.host->request_callback(plugin.host);
max_value = steppedInfo->high; max_value = steppedInfo->high;
default_value = steppedInfo->defaultValue; default_value = steppedInfo->defaultValue;
} }
hostValueSent.test_and_set();
hostStartGestureSent.test_and_set();
hostStopGestureSent.test_and_set();
} }
}; };
std::vector<Param> params; std::vector<Param> params;
@ -365,7 +367,7 @@ plugin.host->request_callback(plugin.host);
template<class PR> template<class PR>
RangeParamInfo & range(const char *key, PR &param) { RangeParamInfo & range(const char *key, PR &param) {
param.context.index = params.size(); param.context.index = params.size(); // so we can find it in the listeners below
params.emplace_back(); params.emplace_back();
auto &entry = params.back(); auto &entry = params.back();
entry.id = crc.copy().addString(key, true).done(); entry.id = crc.copy().addString(key, true).done();
@ -374,7 +376,8 @@ plugin.host->request_callback(plugin.host);
} }
template<class PS> template<class PS>
SteppedParamInfo & stepped(const char *key, PS &param) { SteppedParamInfo & stepped(const char *key, PS &param) {
param.context.index = params.size(); param.context.index = params.size(); // so we can find it in the listeners below
params.emplace_back(); params.emplace_back();
auto &entry = params.back(); auto &entry = params.back();
entry.id = crc.copy().addString(key, true).done(); entry.id = crc.copy().addString(key, true).done();
@ -383,10 +386,80 @@ plugin.host->request_callback(plugin.host);
} }
}; };
void scanParams() { void scanParams() {
params.resize(0); params.clear();
ParamScanner scanner{params}; ParamScanner scanner{params};
effect.state(scanner); effect.state(scanner);
for (auto &entry : params) entry.setClapInfo(); for (auto &entry : params) entry.setClapInfo();
// Listen for changes from the effect UI
effect.paramListenerRange = [&](stfx::web::ParamContext context, double value){
params[context.index].hostValueSent.clear();
if (hostParams) hostParams->request_flush(host);
};
effect.paramListenerGesture = [&](stfx::web::ParamContext context, bool gesture){
if (gesture) {
params[context.index].hostStartGestureSent.clear();
} else {
params[context.index].hostStopGestureSent.clear();
}
if (hostParams) hostParams->request_flush(host);
};
effect.paramListenerStepped = [&](stfx::web::ParamContext context, int value){
params[context.index].hostValueSent.clear();
if (hostParams) hostParams->request_flush(host);
};
}
void sendParamEvents(const clap_output_events *events) {
for (auto &param : params) {
auto sendGesture = [&](uint16_t eventType){
clap_event_param_gesture event{
.header={
.size=sizeof(event),
.time=0,
.space_id=CLAP_CORE_EVENT_SPACE_ID,
.type=eventType
},
.param_id=param.id,
};
LOG_EXPR(event.header.type);
// Not *super* bothered if this fails
events->try_push(events, &event.header);
};
if (!param.hostStartGestureSent.test_and_set()) {
sendGesture(CLAP_EVENT_PARAM_GESTURE_BEGIN);
}
if (!param.hostValueSent.test_and_set()) {
clap_event_param_value event{
.header={
.size=sizeof(event),
.time=0,
.space_id=CLAP_CORE_EVENT_SPACE_ID,
.type=CLAP_EVENT_PARAM_VALUE
},
.param_id=param.id,
.cookie=param.cookie,
.note_id=-1,
.port_index=-1,
.channel=-1,
.key=-1
};
LOG_EXPR(event.header.type);
if (param.rangeParam) {
event.value = param.rangeInfo->toUnit((double)*param.rangeParam);
} else {
event.value = (int)*param.steppedParam;
}
if (!events->try_push(events, &event.header)) {
// failed, try again later
param.hostValueSent.clear();
continue;
}
}
if (!param.hostStopGestureSent.test_and_set()) {
sendGesture(CLAP_EVENT_PARAM_GESTURE_END);
}
}
} }
// CLAP parameter methods // CLAP parameter methods
@ -409,6 +482,7 @@ plugin.host->request_callback(plugin.host);
} else { } else {
*value = (int)*param.steppedParam; *value = (int)*param.steppedParam;
} }
param.hostValueSent.test_and_set();
return true; return true;
} }
} }
@ -452,6 +526,7 @@ plugin.host->request_callback(plugin.host);
auto *header = inEvents->get(inEvents, i); auto *header = inEvents->get(inEvents, i);
plugin.processEvent(header); plugin.processEvent(header);
} }
plugin.sendParamEvents(outEvents);
} }
// CLAP audio port methods // CLAP audio port methods
@ -537,59 +612,18 @@ plugin.host->request_callback(plugin.host);
} }
} }
struct StateWriter : public stfx::storage::STFXStorageWriter<StateWriter> {
using stfx::storage::STFXStorageWriter<StateWriter>::STFXStorageWriter;
template<class RP>
RangeParamIgnore range(const char *key, RP &param) {
double v = param;
(*this)(key, v);
return {};
}
template<class SP>
SteppedParamIgnore stepped(const char *key, SP &param) {
int v = param;
(*this)(key, v);
return {};
}
};
struct StateReader : public stfx::storage::STFXStorageReader<StateReader> {
using stfx::storage::STFXStorageReader<StateReader>::STFXStorageReader;
template<class RP>
RangeParamIgnore range(const char *key, RP &param) {
double v = param;
(*this)(key, v);
param = v;
return {};
}
template<class SP>
SteppedParamIgnore stepped(const char *key, SP &param) {
int v = param;
(*this)(key, v);
param = v;
return {};
}
void invalidate(const char *) {
LOG_EXPR("invalidate() called on state load - this should mark the entire state as dirty for reload");
}
};
std::vector<unsigned char> stateBuffer; std::vector<unsigned char> stateBuffer;
static bool state_save(const clap_plugin *obj, const clap_ostream *stream) { static bool state_save(const clap_plugin *obj, const clap_ostream *stream) {
auto &plugin = *(Plugin *)obj; auto &plugin = *(Plugin *)obj;
auto &buffer = plugin.stateBuffer; auto &buffer = plugin.stateBuffer;
StateWriter storage{buffer}; plugin.effect.saveState(buffer);
plugin.effect.state(storage);
return writeToStream(buffer.data(), buffer.size(), stream); return writeToStream(buffer.data(), buffer.size(), stream);
} }
static bool state_load(const clap_plugin *obj, const clap_istream *stream) { static bool state_load(const clap_plugin *obj, const clap_istream *stream) {
auto &plugin = *(Plugin *)obj; auto &plugin = *(Plugin *)obj;
auto &buffer = plugin.stateBuffer; auto &buffer = plugin.stateBuffer;
if (!fillFromStream(stream, buffer)) return false; if (!fillFromStream(stream, buffer)) return false;
StateReader storage{buffer}; plugin.effect.loadState(buffer);
plugin.effect.state(storage);
plugin.sendWebMessages(); // should already be on the main thread plugin.sendWebMessages(); // should already be on the main thread
return true; return true;
} }

View File

@ -142,23 +142,16 @@ struct StorageCborReader {
template<class V> template<class V>
void operator()(const char *key, V &v) { void operator()(const char *key, V &v) {
LOG_EXPR(key);
LOG_EXPR(filterKeyLength);
if (filterKeyBytes != nullptr) { if (filterKeyBytes != nullptr) {
if (!keyMatch(key, filterKeyBytes, filterKeyLength)) return; if (!keyMatch(key, filterKeyBytes, filterKeyLength)) return;
} }
LOG_EXPR(cbor.isUtf8());
if (!cbor.isUtf8()) return; // We expect a string key if (!cbor.isUtf8()) return; // We expect a string key
LOG_EXPR(cbor.utf8());
// If we have a filter, we *should* be just in front of the appropriate key, but check anyway // If we have a filter, we *should* be just in front of the appropriate key, but check anyway
if (!keyMatch(key, (const char *)cbor.bytes(), cbor.length())) { if (!keyMatch(key, (const char *)cbor.bytes(), cbor.length())) {
LOG_EXPR(key);
LOG_EXPR(cbor.utf8());
return; // key doesn't match return; // key doesn't match
} }
cbor++; cbor++;
readValue(v); readValue(v);
LOG_EXPR(v);
} }
private: private:

View File

@ -9,7 +9,7 @@
body { body {
display: grid; display: grid;
grid-template-areas: "header" "params"; grid-template-areas: "header" "params";
grid-template-rows: 3em 1fr; grid-template-rows: min-content 1fr;
margin: 0; margin: 0;
padding: 0; padding: 0;

View File

@ -159,7 +159,6 @@ struct WebUIHelper {
SuperRange::state(storage); SuperRange::state(storage);
constexpr bool isWeb = storageIsWeb<Storage>(); constexpr bool isWeb = storageIsWeb<Storage>();
LOG_EXPR(isWeb);
if (isWeb) { // a bunch of extra state if (isWeb) { // a bunch of extra state
storage.extra("$type", "ParamRange"); storage.extra("$type", "ParamRange");
storage.extra("name", info->name); storage.extra("name", info->name);
@ -171,9 +170,7 @@ struct WebUIHelper {
storage("gesture", gesture); storage("gesture", gesture);
double unit = info->toUnit(*this); double unit = info->toUnit(*this);
LOG_EXPR(unit);
if (storage.changed("rangeUnit", unit)) { if (storage.changed("rangeUnit", unit)) {
LOG_EXPR(unit);
SuperRange::operator=(info->fromUnit(unit)); SuperRange::operator=(info->fromUnit(unit));
storage.invalidate("value"); storage.invalidate("value");
} }
@ -298,6 +295,16 @@ struct WebUIHelper {
std::function<void(ParamContext, bool)> paramListenerGesture; std::function<void(ParamContext, bool)> paramListenerGesture;
std::function<void(ParamContext, int)> paramListenerStepped; std::function<void(ParamContext, int)> paramListenerStepped;
void saveState(std::vector<unsigned char> &bytes) {
stfx::storage::STFXStorageWriter<> storage(bytes);
this->state(storage);
}
void loadState(const std::vector<unsigned char> &bytes) {
stfx::storage::STFXStorageReader<> storage(bytes);
this->state(storage);
requestEntireState();
}
struct WebMessage { struct WebMessage {
std::vector<unsigned char> bytes; std::vector<unsigned char> bytes;