#pragma once #include "../storage/cbor-walker.h" #include "../storage/stfx-storage.h" #include "../param-info.h" #include #include #include #include #include #include // we log to stderr if our queue gets full namespace stfx { namespace web { struct ParamContext { union { void *pointer; size_t index; }; ParamContext() : index(0) {} }; struct WebStateWriter : public storage::STFXStorageWriter { using Super = storage::STFXStorageWriter; using Super::Super; // There should never be a version mismatch, ignore this int version(int v) {return v;} // Include extra info bool extra() { return true; } template void extra(const char *key, V v) { (*this)(key, v); } }; struct WebStateReader : public storage::STFXStorageReader { using Super = storage::STFXStorageReader; using Super::Super; // There should never be a version mismatch, ignore this int version(int v) {return v;} // We're interested in an extended set of values being sent back bool extra() { return true; } // But anything read-only gets skipped template void extra(const char *key, V v) {} void invalidate(const char *invalidatedKey) { // TODO: right now this is hacked together, by the params re-sending themselves whenever they would otherwise invalidate. // Instead, this would let us reply with only the keys which changed } }; template constexpr bool storageIsWeb() {return false;} template<> constexpr bool storageIsWeb() {return true;} template<> constexpr bool storageIsWeb() {return true;} /* 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 class EffectSTFX, class SubClass> struct WebUIHelper { template class WebBase : public Effect { using SuperRange = typename Effect::ParamRange; using SuperStepped = typename Effect::ParamStepped; protected: struct WebParamScanner : public storage::STFXStorageScanner{ SubClass *effect; std::vector scope; WebParamScanner(SubClass *effect) : effect(effect) {} // Extra methods we add for STFX storage void info(const char *, const char *) {} int version(int v) { return v; } template RangeParamInfo & range(const char *key, PR ¶m) { param.effect = effect; param.scope = scope; param.scope.emplace_back(key); param.info = std::unique_ptr{ new RangeParamInfo(param) }; return *param.info; } template SteppedParamInfo & stepped(const char *key, PS ¶m) { param.effect = effect; param.scope = scope; param.scope.emplace_back(key); param.info = std::unique_ptr{ new SteppedParamInfo(param) }; return *param.info; } template bool changed(const char *key, V &v) { (*this)(key, v); return false; } void invalidate(const char *) {} }; public: using Effect::Effect; std::string webPage = "html/generic.html"; int webWidth = 640, webHeight = 480; struct ParamRange : public SuperRange { using SuperRange::SuperRange; ParamContext context; ParamRange & operator=(double v) { bool changed = (v != *this); SuperRange::operator=(v); if (changed) sendUpdateMessage(); return *this; } template void state(Storage &storage) { double prevV = *this; bool prevGesture = gesture; SuperRange::state(storage); constexpr bool isWeb = storageIsWeb(); if (isWeb) { // a bunch of extra state 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); storage("gesture", gesture); double unit = info->toUnit(*this); if (storage.changed("rangeUnit", unit)) { SuperRange::operator=(info->fromUnit(unit)); storage.invalidate("value"); } } if (prevV != *this) { storage.invalidate("rangeUnit"); storage.invalidate("text"); storage.invalidate("textUnits"); if (isWeb) { if (effect->paramListenerRange) { effect->paramListenerRange(context, *this); } sendUpdateMessage(); // TODO: shouldn't be necessary once we have `.invalidate()` working } else { sendUpdateMessage(); } } if (gesture != prevGesture) { if (isWeb && effect->paramListenerGesture) { effect->paramListenerGesture(context, gesture); } } } private: friend struct WebParamScanner; SubClass *effect = nullptr; std::vector scope; std::unique_ptr info; bool gesture = false; void sendUpdateMessage() { if (auto *m = effect->getEmptyMessage()) { signalsmith::cbor::CborWriter cbor{m->bytes}; for (auto &key : scope) { cbor.openMap(1); cbor.addUtf8(key); } WebStateWriter storage{cbor}; state(storage); m->markReady(); } } }; struct ParamStepped : public SuperStepped { using SuperStepped::SuperStepped; ParamContext context; ParamStepped & operator=(int v) { bool changed = (v != *this); SuperStepped::operator=(v); if (changed) sendUpdateMessage(); return *this; } template void state(Storage &storage) { int prevV = *this; SuperStepped::state(storage); constexpr bool isWeb = storageIsWeb(); if (isWeb) { storage.extra("$type", "ParamStepped"); storage.extra("low", info->low); storage.extra("high", info->high); storage.extra("name", info->name); std::string text = info->toString((int)*this); storage.extra("text", text); } if (prevV != *this) { storage.invalidate("text"); if (isWeb) { if (effect->paramListenerStepped) { effect->paramListenerStepped(context, *this); } sendUpdateMessage(); // TODO: shouldn't be necessary once we have `.invalidate()` working } else { sendUpdateMessage(); } } } private: friend struct WebParamScanner; SubClass *effect = nullptr; std::vector scope; std::unique_ptr info; void sendUpdateMessage() { if (auto *m = effect->getEmptyMessage()) { signalsmith::cbor::CborWriter cbor{m->bytes}; for (auto &key : scope) { cbor.openMap(1); cbor.addUtf8(key); } WebStateWriter storage{cbor}; state(storage); m->markReady(); } } }; template void meterState(Storage &storage) {} }; template struct WebSTFX : public EffectSTFX, ExtraArgs...> { using Super = EffectSTFX, ExtraArgs...>; }; }; // Use this instead of a plain LibraryEffect template class EffectSTFX, class... ExtraArgs> struct WebUILibraryEffect : public LibraryEffect>::template WebSTFX, ExtraArgs...> { using Super = LibraryEffect>::template WebSTFX, ExtraArgs...>; template WebUILibraryEffect(Args... args) : Super(args...) { typename Super::WebParamScanner scanner{this}; this->state(scanner); }; // These are called when the parameter is changed from the web UI std::function paramListenerRange; std::function paramListenerGesture; std::function paramListenerStepped; // This state includes all the parameters void saveState(std::vector &bytes) { stfx::storage::STFXStorageWriter<> storage(bytes); this->state(storage); } void loadState(const std::vector &bytes) { stfx::storage::STFXStorageReader<> storage(bytes); this->state(storage); requestEntireState(); } struct WebMessage { std::vector bytes; WebMessage() { bytes.reserve(256); } void sent() { readyToSend.clear(); } private: friend class WebUILibraryEffect; friend struct Super::ParamRange; friend struct Super::ParamStepped; 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") { requestEntireState(); return; } } else if (cbor.isArray()) { size_t length = cbor.length(); cbor = cbor.enter(); if (cbor.utf8View() == "meters" && length == 3) { double interval = ++cbor; double duration = ++cbor; metersInterval.store(interval*this->config.sampleRate); metersDuration.store(duration*this->config.sampleRate); } } else if (cbor.isMap()) { // Apply it as a merge WebStateReader reader(cbor); this->state(reader); } } void webClosed() { // stops new messages from being queued up until the reset message is sent requestEntireState(); } // Replace `.process()` to add meter messages if they exist template void process(Buffers &&buffers, int blockLength) { process(buffers, buffers, blockLength); } template void process(Inputs &&inputs, Outputs &&outputs, int blockLength) { Super::process(inputs, outputs, blockLength); if (this->hasMeters()) { auto *m = getEmptyMessage(); if (m) { signalsmith::cbor::CborWriter cbor{m->bytes}; WebStateWriter storage{cbor}; this->meterState(storage); m->markReady(); } this->wantsMeters(false); } if (metersDuration.load() > 0) { metersDuration -= blockLength; samplesSinceMeters += blockLength; if (samplesSinceMeters > metersInterval.load()) { this->wantsMeters(); samplesSinceMeters = 0; } } } private: friend struct Super::ParamRange; friend struct Super::ParamStepped; std::atomic metersInterval = 0, metersDuration = 0; int samplesSinceMeters = 0; // only used from `.process()` std::vector queue = std::vector(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 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; } void requestEntireState() { if (auto *m = getEmptyMessage()) { resetQueue.test_and_set(); m->markReady(); } // if this fails, then the queue is full and we're doing a reset anyway }}; }} // namespace