357 lines
9.0 KiB
C++
357 lines
9.0 KiB
C++
#pragma once
|
|
|
|
#include "../storage/cbor-walker.h"
|
|
#include "../storage/storage.h"
|
|
#include "../param-info.h"
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
#include <functional>
|
|
#include <atomic>
|
|
#include <memory>
|
|
|
|
namespace stfx { namespace web {
|
|
|
|
/* Given an STFX template which (when insantiated) has inheritance like:
|
|
|
|
EffectSTFX : Effect
|
|
|
|
This produces a template with inheritance like:
|
|
|
|
WebSTFX : EffectSTFX : WebBase : Effect
|
|
|
|
The WebBase template replaces the ParamRange/ParamStepped classes, so that the UI can be updated when they change. The outer WebSTFX class adds methods for forwarding messages between the effect and its webview.
|
|
*/
|
|
|
|
template<template<class, class...> class EffectSTFX>
|
|
struct WebUIHelper {
|
|
|
|
struct MaybeChanged {
|
|
virtual void maybeChanged() = 0;
|
|
};
|
|
|
|
struct WebParamScanner {
|
|
MaybeChanged *effect;
|
|
|
|
// Do nothing for most types
|
|
template<class V>
|
|
void operator()(const char *key, V &v) {
|
|
if constexpr (std::is_void_v<decltype(v.state(*this))>) {
|
|
v.state(*this);
|
|
}
|
|
}
|
|
|
|
// Extra methods we add for STFX storage
|
|
void info(const char *, const char *) {}
|
|
int version(int v) {
|
|
return v;
|
|
}
|
|
template<class PR>
|
|
RangeParamInfo & range(const char *key, PR ¶m) {
|
|
param.effect = effect;
|
|
param.info = std::unique_ptr<RangeParamInfo>{
|
|
new RangeParamInfo(param)
|
|
};
|
|
return *param.info;
|
|
}
|
|
template<class PS>
|
|
SteppedParamInfo & stepped(const char *key, PS ¶m) {
|
|
param.effect = effect;
|
|
param.info = std::unique_ptr<SteppedParamInfo>{
|
|
new SteppedParamInfo(param)
|
|
};
|
|
return *param.info;
|
|
}
|
|
template<class V>
|
|
bool changed(const char *key, V &v) {
|
|
(*this)(key, v);
|
|
return false;
|
|
}
|
|
void invalidate(const char *) {}
|
|
};
|
|
|
|
template<class Effect>
|
|
class WebBase : public Effect {
|
|
using SuperRange = typename Effect::ParamRange;
|
|
using SuperStepped = typename Effect::ParamStepped;
|
|
public:
|
|
|
|
using Effect::Effect;
|
|
|
|
struct ParamRange : public SuperRange {
|
|
using SuperRange::SuperRange;
|
|
|
|
ParamRange & operator=(double v) {
|
|
SuperRange::operator=(v);
|
|
effect->maybeChanged();
|
|
return *this;
|
|
}
|
|
|
|
template<class Storage>
|
|
void state(Storage &storage) {
|
|
double prevV = *this;
|
|
SuperRange::state(storage);
|
|
|
|
if (storage.extra()) {
|
|
storage.extra("$type", "ParamRange");
|
|
storage.extra("name", info->name);
|
|
storage.extra("defaultValue", info->defaultValue);
|
|
std::string text, units;
|
|
info->toString((double)*this, text, units);
|
|
storage.extra("text", text);
|
|
storage.extra("textUnits", units);
|
|
|
|
double unit = info->toUnit(*this);
|
|
if (storage.changed("rangeUnit", unit)) {
|
|
*this = info->fromUnit(unit);
|
|
storage.invalidate("value");
|
|
}
|
|
}
|
|
|
|
if (prevV != *this) {
|
|
effect->maybeChanged();
|
|
storage.invalidate("rangeUnit");
|
|
storage.invalidate("text");
|
|
storage.invalidate("textUnits");
|
|
}
|
|
}
|
|
|
|
private:
|
|
friend struct WebParamScanner;
|
|
MaybeChanged *effect = nullptr;
|
|
std::unique_ptr<RangeParamInfo> info;
|
|
};
|
|
|
|
struct ParamStepped : public SuperStepped {
|
|
using SuperStepped::SuperStepped;
|
|
|
|
ParamStepped & operator=(double v) {
|
|
SuperStepped::operator=(v);
|
|
effect->maybeChanged();
|
|
return *this;
|
|
}
|
|
|
|
template<class Storage>
|
|
void state(Storage &storage) {
|
|
int prevV = *this;
|
|
SuperStepped::state(storage);
|
|
|
|
if (storage.extra()) {
|
|
storage.extra("name", info->name);
|
|
storage.extra("$type", "ParamStepped");
|
|
std::string text = info->toString((int)*this);
|
|
storage.extra("text", text);
|
|
}
|
|
|
|
if (prevV != *this) {
|
|
effect->maybeChanged();
|
|
storage.invalidate("text");
|
|
}
|
|
}
|
|
|
|
private:
|
|
friend struct WebParamScanner;
|
|
MaybeChanged *effect = nullptr;
|
|
std::unique_ptr<SteppedParamInfo> info;
|
|
};
|
|
};
|
|
|
|
struct WebStateWriter : public storage::StorageCborWriter<false, WebStateWriter> {
|
|
using Super = storage::StorageCborWriter<false, WebStateWriter>;
|
|
|
|
using Super::Super;
|
|
|
|
void info(const char *name, const char *desc) {
|
|
extra("name", name);
|
|
extra("desc", desc);
|
|
}
|
|
|
|
int version(int v) {
|
|
return v; // we don't use old states, so not worth tracking
|
|
}
|
|
|
|
bool extra() {
|
|
return true;
|
|
}
|
|
|
|
template<class V>
|
|
void extra(const char *key, V v) {
|
|
(*this)(key, v);
|
|
}
|
|
|
|
template<class PR>
|
|
RangeParamIgnore range(const char *key, PR ¶m) {
|
|
(*this)(key, param);
|
|
return {};
|
|
}
|
|
template<class PS>
|
|
SteppedParamIgnore stepped(const char *key, PS ¶m) {
|
|
(*this)(key, param);
|
|
return {};
|
|
}
|
|
template<class V>
|
|
bool changed(const char *key, V &v) {
|
|
(*this)(key, v);
|
|
return false;
|
|
}
|
|
void invalidate(const char *) {}
|
|
};
|
|
|
|
struct WebStateReader : public storage::StorageCborReader<false, WebStateReader> {
|
|
using Super = storage::StorageCborReader<false, WebStateReader>;
|
|
|
|
using Super::Super;
|
|
|
|
void info(const char *, const char *) {}
|
|
|
|
int version(int v) {return v;}
|
|
|
|
bool extra() {
|
|
// We're interested in an extended set of values being sent back, even if we ignore actual "extra" things
|
|
return true;
|
|
}
|
|
|
|
template<class V>
|
|
void extra(const char *key, V v) {}
|
|
|
|
template<class PR>
|
|
RangeParamIgnore range(const char *key, PR ¶m) {
|
|
(*this)(key, param);
|
|
return {};
|
|
}
|
|
template<class PS>
|
|
SteppedParamIgnore stepped(const char *key, PS ¶m) {
|
|
(*this)(key, param);
|
|
return {};
|
|
}
|
|
template<class V>
|
|
bool changed(const char *key, V &v) {
|
|
V prev = v;
|
|
(*this)(key, v);
|
|
return v != prev;
|
|
}
|
|
void invalidate(const char *invalidatedKey) {
|
|
LOG_EXPR(invalidatedKey);
|
|
}
|
|
};
|
|
|
|
template<class Effect, class... ExtraArgs>
|
|
struct WebSTFX : public EffectSTFX<WebBase<Effect>, ExtraArgs...>, MaybeChanged {
|
|
// Inherit constructor(s)
|
|
using Super = EffectSTFX<WebBase<Effect>, ExtraArgs...>;
|
|
|
|
template<class... Args>
|
|
WebSTFX(Args... args) : Super(args...) {
|
|
WebParamScanner scanner{this};
|
|
this->state(scanner);
|
|
};
|
|
|
|
std::string webPage = "html/generic.html";
|
|
int webWidth = 640, webHeight = 480;
|
|
|
|
struct WebMessage {
|
|
std::vector<unsigned char> bytes;
|
|
|
|
WebMessage() {
|
|
bytes.reserve(256);
|
|
}
|
|
|
|
void sent() {
|
|
readyToSend.clear();
|
|
}
|
|
private:
|
|
friend struct WebSTFX;
|
|
std::atomic_flag readyToSend = ATOMIC_FLAG_INIT;
|
|
|
|
void markReady() {
|
|
readyToSend.test_and_set();
|
|
}
|
|
};
|
|
|
|
bool hasPendingWebMessage() const {
|
|
auto &message = queue[readIndex];
|
|
return message.readyToSend.test();
|
|
}
|
|
|
|
// Poll on the main thread (and directly after any `.webReceive()`), calling `.sent()` after you've sent it, repeat until you get `nullptr`
|
|
WebMessage * getPendingWebMessage() {
|
|
auto &message = queue[readIndex];
|
|
if (!message.readyToSend.test()) return nullptr;
|
|
if (++readIndex == queue.size()) readIndex = 0;
|
|
|
|
if (resetQueue.test()) {
|
|
// Clear ("send") every message aside from this one
|
|
for (auto &m : queue) {
|
|
if (&m == &message) continue;
|
|
m.sent();
|
|
}
|
|
// Ready to start sending messages again, starting from the next index
|
|
writeIndex.store(readIndex);
|
|
resetQueue.clear();
|
|
// any messages (state updates) sent after this won't assume a *newer* state than what we serialise below
|
|
|
|
// Replace this one's message with the complete plugin state
|
|
WebStateWriter storage(message.bytes);
|
|
this->state(storage);
|
|
}
|
|
return &message;
|
|
}
|
|
|
|
// Call when the webview posts any message back
|
|
void webReceive(const void *message, size_t size) {
|
|
auto *bytes = (const unsigned char *)message;
|
|
signalsmith::cbor::TaggedCborWalker cbor(bytes, bytes + size);
|
|
|
|
if (cbor.isUtf8()) {
|
|
if (cbor.utf8View() == "ready") {
|
|
if (auto *m = getEmptyMessage()) {
|
|
resetQueue.test_and_set();
|
|
m->markReady();
|
|
} // if this fails we're doing a reset anyway, so no worrie
|
|
return;
|
|
}
|
|
} else if (cbor.isMap()) {
|
|
// Apply it as a merge
|
|
WebStateReader reader(cbor);
|
|
this->state(reader);
|
|
}
|
|
}
|
|
|
|
void maybeChanged() override {
|
|
std::cout << "web effect maybe changed\n";
|
|
// Resend entire state again
|
|
if (auto *m = getEmptyMessage()) {
|
|
resetQueue.test_and_set();
|
|
m->markReady();
|
|
}
|
|
}
|
|
|
|
private:
|
|
std::vector<WebMessage> queue = std::vector<WebMessage>(64); // power of 2 so that overflow doesn't mess with the modulo
|
|
size_t readIndex = 0;
|
|
std::atomic_flag resetQueue = ATOMIC_FLAG_INIT;
|
|
|
|
// Atomic because multiple threads might write
|
|
std::atomic<size_t> writeIndex = 0;
|
|
WebMessage * getEmptyMessage() {
|
|
if (resetQueue.test()) return nullptr;
|
|
auto reservedIndex = writeIndex.fetch_add(1);
|
|
auto &message = queue[reservedIndex%queue.size()];
|
|
if (message.readyToSend.test()) {
|
|
std::cerr << "Web message queue full - sending entire state instead\n";
|
|
// When our queue is full, we drop the entire thing and re-send the entire state
|
|
resetQueue.test_and_set();
|
|
return nullptr;
|
|
}
|
|
message.bytes.resize(0);
|
|
return &message;
|
|
}
|
|
};
|
|
};
|
|
|
|
// Use this instead of a plain LibraryEffect
|
|
template<typename Sample, template<class, class...> class EffectSTFX, class... ExtraArgs>
|
|
using WebUILibraryEffect = LibraryEffect<Sample, WebUIHelper<EffectSTFX>::template WebSTFX, ExtraArgs...>;
|
|
|
|
}} // namespace
|