Improve web state transfer
This commit is contained in:
parent
2efd1de4e9
commit
bbad24674a
@ -7,7 +7,7 @@
|
||||
#include <cstdio>
|
||||
#include <optional>
|
||||
|
||||
#include "./param-info.h"
|
||||
#include "../param-info.h"
|
||||
#include "../storage/storage.h"
|
||||
#include "../ui/web-ui.h"
|
||||
|
||||
@ -118,11 +118,23 @@ private:
|
||||
uint32_t crc = 0xFFFFFFFFu;
|
||||
};
|
||||
|
||||
// Just include the proposed draft structs here
|
||||
static constexpr const char *CLAP_EXT_WEBVIEW1 = "clap.webview/1";
|
||||
struct clap_plugin_webview1 {
|
||||
bool(CLAP_ABI *provide_starting_uri)(const clap_plugin_t *plugin, char *out_buffer, uint32_t out_buffer_capacity);
|
||||
bool(CLAP_ABI *receive)(const clap_plugin_t *plugin, const void *buffer, uint32_t size);
|
||||
};
|
||||
struct clap_host_webview1 {
|
||||
bool(CLAP_ABI *is_open)(const clap_host_t *host);
|
||||
bool(CLAP_ABI *send)(const clap_host_t *host, const void *buffer, uint32_t size);
|
||||
};
|
||||
|
||||
template<template<class> class EffectSTFX>
|
||||
struct Plugin : public clap_plugin {
|
||||
const Plugins &plugins;
|
||||
const clap_host *host;
|
||||
using Effect = stfx::WebUILibraryEffect<float, EffectSTFX>;
|
||||
const clap_host_webview1 *hostWebview1 = nullptr;
|
||||
using Effect = stfx::web::WebUILibraryEffect<float, EffectSTFX>;
|
||||
Effect effect;
|
||||
|
||||
template<class ...Args>
|
||||
@ -183,6 +195,10 @@ struct Plugin : public clap_plugin {
|
||||
// CLAP plugin methods
|
||||
static bool plugin_init(const clap_plugin *obj) {
|
||||
auto &plugin = *(Plugin *)obj;
|
||||
#define STFX_GET_EXT(field, extId) \
|
||||
plugin.field = (decltype(plugin.field))plugin.host->get_extension(plugin.host, extId);
|
||||
STFX_GET_EXT(hostWebview1, CLAP_EXT_WEBVIEW1)
|
||||
#undef STFX_GET_EXT
|
||||
return true;
|
||||
}
|
||||
static void plugin_destroy(const clap_plugin *obj) {
|
||||
@ -240,10 +256,10 @@ struct Plugin : public clap_plugin {
|
||||
state_load
|
||||
};
|
||||
return &ext;
|
||||
} else if (!std::strcmp(extId, CLAP_EXT_WEBVIEW)) {
|
||||
static struct clap_plugin_webview ext{
|
||||
webview_provide_starting_uri,
|
||||
webview_receive
|
||||
} else if (!std::strcmp(extId, CLAP_EXT_WEBVIEW1)) {
|
||||
static struct clap_plugin_webview1 ext{
|
||||
webview1_provide_starting_uri,
|
||||
webview1_receive
|
||||
};
|
||||
return &ext;
|
||||
}
|
||||
@ -521,8 +537,8 @@ struct Plugin : public clap_plugin {
|
||||
}
|
||||
}
|
||||
|
||||
struct StateWriter : public StorageCborWriter<true, StateWriter> {
|
||||
using StorageCborWriter<true, StateWriter>::StorageCborWriter;
|
||||
struct StateWriter : public stfx::storage::StorageCborWriter<true, StateWriter> {
|
||||
using stfx::storage::StorageCborWriter<true, StateWriter>::StorageCborWriter;
|
||||
|
||||
void info(const char *, const char *) {}
|
||||
int version(int v) {
|
||||
@ -548,8 +564,8 @@ struct Plugin : public clap_plugin {
|
||||
}
|
||||
void invalidate(const char *) {}
|
||||
};
|
||||
struct StateReader : public StorageCborReader<true, StateReader> {
|
||||
using StorageCborReader<true, StateReader>::StorageCborReader;
|
||||
struct StateReader : public stfx::storage::StorageCborReader<true, StateReader> {
|
||||
using stfx::storage::StorageCborReader<true, StateReader>::StorageCborReader;
|
||||
|
||||
void info(const char *, const char *) {}
|
||||
int version(int v) {
|
||||
@ -596,26 +612,30 @@ struct Plugin : public clap_plugin {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Just include the proposed draft structs here
|
||||
static constexpr const char *CLAP_EXT_WEBVIEW = "clap.webview/1";
|
||||
struct clap_plugin_webview {
|
||||
bool(CLAP_ABI *provide_starting_uri)(const clap_plugin_t *plugin, char *out_buffer, uint32_t out_buffer_capacity);
|
||||
bool(CLAP_ABI *receive)(const clap_plugin_t *plugin, const void *buffer, uint32_t size);
|
||||
};
|
||||
struct clap_host_webview {
|
||||
bool(CLAP_ABI *send)(const clap_host_t *host, const void *buffer, uint32_t size);
|
||||
};
|
||||
static bool webview_provide_starting_uri(const clap_plugin_t *obj, char *startingUri, uint32_t capacity) {
|
||||
static bool webview1_provide_starting_uri(const clap_plugin_t *obj, char *startingUri, uint32_t capacity) {
|
||||
auto &plugin = *(Plugin *)obj;
|
||||
if (!plugin.hostWebview1) return false;
|
||||
if (!plugin.effect.webPage.size() + 1 > capacity) return false;
|
||||
std::strcpy(startingUri, plugin.effect.webPage.c_str());
|
||||
return true;
|
||||
}
|
||||
static bool webview_receive(const clap_plugin_t *obj, const void *buffer, uint32_t size) {
|
||||
static bool webview1_receive(const clap_plugin_t *obj, const void *buffer, uint32_t size) {
|
||||
auto &plugin = *(Plugin *)obj;
|
||||
plugin.effect.webReceive(buffer, size);
|
||||
// TODO: *double* check we're on the main thread, for safety
|
||||
plugin.sendWebMessages();
|
||||
return true;
|
||||
}
|
||||
|
||||
void sendWebMessages() {
|
||||
if (!hostWebview1) return;
|
||||
while (auto *m = effect.getPendingWebMessage()) {
|
||||
LOG_EXPR("pending web message");
|
||||
hostWebview1->send(host, m->bytes.data(), m->bytes.size());
|
||||
LOG_EXPR(m->bytes.size());
|
||||
m->sent();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}} // namespace
|
||||
|
||||
@ -488,6 +488,11 @@ namespace stfx {
|
||||
justHadReset = true;
|
||||
}
|
||||
|
||||
template<class Buffers>
|
||||
void process(Buffers &&buffers, int blockLength) {
|
||||
process(buffers, buffers, blockLength);
|
||||
}
|
||||
|
||||
/// Wraps the common `process(float** inputs, float** outputs, int length)` call into the `.process(io, config, block)`.
|
||||
/// It actually accepts any objects which support `inputs[channel][index]`, so you could write adapters for interleaved buffers etc.
|
||||
template<class Inputs, class Outputs>
|
||||
|
||||
@ -724,76 +724,96 @@ private:
|
||||
struct CborWriter {
|
||||
CborWriter(std::vector<unsigned char> &bytes) : bytes(bytes) {}
|
||||
|
||||
void addUInt(uint64_t u) {
|
||||
CborWriter & addUInt(uint64_t u) {
|
||||
writeHead(0, u);
|
||||
return *this;
|
||||
}
|
||||
void addInt(int64_t u) {
|
||||
CborWriter & addInt(int64_t u) {
|
||||
if (u >= 0) {
|
||||
writeHead(0, u);
|
||||
} else {
|
||||
writeHead(1, -1 - u);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
void addTag(uint64_t u) {
|
||||
CborWriter & addTag(uint64_t u) {
|
||||
writeHead(6, u);
|
||||
return *this;
|
||||
}
|
||||
void addBool(bool b) {
|
||||
CborWriter & addBool(bool b) {
|
||||
writeHead(7, 20 + b);
|
||||
return *this;
|
||||
}
|
||||
void openArray() {
|
||||
CborWriter & openArray() {
|
||||
bytes.push_back(0x9F);
|
||||
return *this;
|
||||
}
|
||||
void openArray(size_t items) {
|
||||
CborWriter & openArray(size_t items) {
|
||||
writeHead(4, items);
|
||||
return *this;
|
||||
}
|
||||
void openMap() {
|
||||
CborWriter & openMap() {
|
||||
bytes.push_back(0xBF);
|
||||
return *this;
|
||||
}
|
||||
void openMap(size_t pairs) {
|
||||
CborWriter & openMap(size_t pairs) {
|
||||
writeHead(5, pairs);
|
||||
return *this;
|
||||
}
|
||||
void close() {
|
||||
CborWriter & close() {
|
||||
bytes.push_back(0xFF);
|
||||
return *this;
|
||||
}
|
||||
void addBytes(const void *ptr, size_t length) {
|
||||
CborWriter & addBytes(const void *ptr, size_t length) {
|
||||
addBytes((const unsigned char *)ptr, length);
|
||||
return *this;
|
||||
}
|
||||
void addBytes(const unsigned char *ptr, size_t length) {
|
||||
CborWriter & addBytes(const unsigned char *ptr, size_t length) {
|
||||
writeHead(2, length);
|
||||
bytes.insert(bytes.end(), ptr, ptr + length);
|
||||
return *this;
|
||||
}
|
||||
void openBytes() {
|
||||
CborWriter & openBytes() {
|
||||
bytes.push_back(0x5F);
|
||||
return *this;
|
||||
}
|
||||
void addUtf8(const char *ptr, size_t length) {
|
||||
CborWriter & addUtf8(const char *ptr, size_t length) {
|
||||
writeHead(3, length);
|
||||
bytes.insert(bytes.end(), ptr, ptr + length);
|
||||
return *this;
|
||||
}
|
||||
void addUtf8(const char *str) {
|
||||
CborWriter & addUtf8(const char *str) {
|
||||
addUtf8(str, std::strlen(str));
|
||||
return *this;
|
||||
}
|
||||
void addUtf8(const std::string &str) {
|
||||
CborWriter & addUtf8(const std::string &str) {
|
||||
addUtf8(str.c_str());
|
||||
return *this;
|
||||
}
|
||||
#ifdef CBOR_WALKER_USE_STRING_VIEW
|
||||
void addUtf8(const std::string_view &str) {
|
||||
CborWriter & addUtf8(const std::string_view &str) {
|
||||
addUtf8(str.data(), str.size());
|
||||
return *this;
|
||||
}
|
||||
#endif
|
||||
void openUtf8() {
|
||||
CborWriter & openUtf8() {
|
||||
bytes.push_back(0x7F);
|
||||
return *this;
|
||||
}
|
||||
void addNull() {
|
||||
CborWriter & addNull() {
|
||||
bytes.push_back(0xF6);
|
||||
return *this;
|
||||
}
|
||||
void addUndefined() {
|
||||
CborWriter & addUndefined() {
|
||||
bytes.push_back(0xF7);
|
||||
return *this;
|
||||
}
|
||||
void addSimple(unsigned char k) {
|
||||
CborWriter & addSimple(unsigned char k) {
|
||||
writeHead(7, k);
|
||||
return *this;
|
||||
}
|
||||
|
||||
void addFloat(float v) {
|
||||
CborWriter & addFloat(float v) {
|
||||
bytes.push_back(0xFA);
|
||||
#ifdef CBOR_WALKER_USE_BIT_CAST
|
||||
uint32_t vi = std::bit_cast<uint32_t>(v);
|
||||
@ -805,8 +825,9 @@ struct CborWriter {
|
||||
auto shift = (3 - i)*8;
|
||||
bytes.push_back((vi>>shift)&0xFF);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
void addFloat(double v) {
|
||||
CborWriter & addFloat(double v) {
|
||||
bytes.push_back(0xFB);
|
||||
#ifdef CBOR_WALKER_USE_BIT_CAST
|
||||
uint64_t vi = std::bit_cast<uint64_t>(v);
|
||||
@ -818,32 +839,38 @@ struct CborWriter {
|
||||
auto shift = (7 - i)*8;
|
||||
bytes.push_back((vi>>shift)&0xFF);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
// RFC-8746 tags for typed arrays
|
||||
// bits: [1, 0] = log2(elementBytes), [2] = isLittleEndian, [3, 4] = [unsigned, signed, float]
|
||||
void addTypedArray(const uint8_t *arr, size_t length) {
|
||||
CborWriter & addTypedArray(const uint8_t *arr, size_t length) {
|
||||
addTag(64);
|
||||
addBytes((const void *)arr, length);
|
||||
return *this;
|
||||
}
|
||||
void addTypedArray(const int8_t *arr, size_t length) {
|
||||
CborWriter & addTypedArray(const int8_t *arr, size_t length) {
|
||||
addTag(72);
|
||||
addBytes((const void *)arr, length);
|
||||
return *this;
|
||||
}
|
||||
void addTypedArray(const uint16_t *arr, size_t length, bool bigEndian=false) {
|
||||
CborWriter & addTypedArray(const uint16_t *arr, size_t length, bool bigEndian=false) {
|
||||
addTag(bigEndian ? 65 : 69);
|
||||
writeTypedBlock<uint16_t>(arr, length, bigEndian);
|
||||
return *this;
|
||||
}
|
||||
void addTypedArray(const uint32_t *arr, size_t length, bool bigEndian=false) {
|
||||
CborWriter & addTypedArray(const uint32_t *arr, size_t length, bool bigEndian=false) {
|
||||
addTag(bigEndian ? 66 : 70);
|
||||
writeTypedBlock<uint32_t>(arr, length, bigEndian);
|
||||
return *this;
|
||||
}
|
||||
void addTypedArray(const uint64_t *arr, size_t length, bool bigEndian=false) {
|
||||
CborWriter & addTypedArray(const uint64_t *arr, size_t length, bool bigEndian=false) {
|
||||
addTag(bigEndian ? 67 : 71);
|
||||
writeTypedBlock<uint64_t>(arr, length, bigEndian);
|
||||
return *this;
|
||||
}
|
||||
// For signed ints, we make a proxy struct which casts them on-the-fly
|
||||
void addTypedArray(const int16_t *arr, size_t length, bool bigEndian=false) {
|
||||
CborWriter & addTypedArray(const int16_t *arr, size_t length, bool bigEndian=false) {
|
||||
addTag(bigEndian ? 73 : 77);
|
||||
struct {
|
||||
const int16_t *arr;
|
||||
@ -852,8 +879,9 @@ struct CborWriter {
|
||||
}
|
||||
} unsignedArray{arr};
|
||||
writeTypedBlock<uint16_t>(unsignedArray, length, bigEndian);
|
||||
return *this;
|
||||
}
|
||||
void addTypedArray(const int32_t *arr, size_t length, bool bigEndian=false) {
|
||||
CborWriter & addTypedArray(const int32_t *arr, size_t length, bool bigEndian=false) {
|
||||
addTag(bigEndian ? 74 : 78);
|
||||
struct {
|
||||
const int32_t *arr;
|
||||
@ -862,8 +890,9 @@ struct CborWriter {
|
||||
}
|
||||
} unsignedArray{arr};
|
||||
writeTypedBlock<uint32_t>(unsignedArray, length, bigEndian);
|
||||
return *this;
|
||||
}
|
||||
void addTypedArray(const int64_t *arr, size_t length, bool bigEndian=false) {
|
||||
CborWriter & addTypedArray(const int64_t *arr, size_t length, bool bigEndian=false) {
|
||||
addTag(bigEndian ? 75 : 79);
|
||||
struct {
|
||||
const int64_t *arr;
|
||||
@ -872,9 +901,9 @@ struct CborWriter {
|
||||
}
|
||||
} unsignedArray{arr};
|
||||
writeTypedBlock<uint64_t>(unsignedArray, length, bigEndian);
|
||||
return *this;
|
||||
}
|
||||
// Look, I'm not any happier about this than you are
|
||||
void addTypedArray(const float *arr, size_t length, bool bigEndian=false) {
|
||||
CborWriter & addTypedArray(const float *arr, size_t length, bool bigEndian=false) {
|
||||
addTag(bigEndian ? 81 : 85);
|
||||
struct {
|
||||
const float *arr;
|
||||
@ -890,8 +919,9 @@ struct CborWriter {
|
||||
}
|
||||
} unsignedArray{arr};
|
||||
writeTypedBlock<uint32_t>(unsignedArray, length, bigEndian);
|
||||
return *this;
|
||||
}
|
||||
void addTypedArray(const double *arr, size_t length, bool bigEndian=false) {
|
||||
CborWriter & addTypedArray(const double *arr, size_t length, bool bigEndian=false) {
|
||||
addTag(bigEndian ? 82 : 86);
|
||||
struct {
|
||||
const double *arr;
|
||||
@ -907,6 +937,7 @@ struct CborWriter {
|
||||
}
|
||||
} unsignedArray{arr};
|
||||
writeTypedBlock<uint64_t>(unsignedArray, length, bigEndian);
|
||||
return *this;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
#include "./cbor-walker.h"
|
||||
|
||||
namespace stfx { namespace storage {
|
||||
|
||||
struct StorageDummy {
|
||||
template<class V>
|
||||
void operator()(const char *, V &) {}
|
||||
@ -104,6 +106,10 @@ private:
|
||||
cbor.addUtf8(cStr);
|
||||
}
|
||||
|
||||
void writeValue(std::string &str) {
|
||||
writeValue(str.c_str());
|
||||
}
|
||||
|
||||
template<class Item>
|
||||
void writeValue(std::vector<Item> &array) {
|
||||
cbor.openArray(array.size());
|
||||
@ -260,3 +266,5 @@ private:
|
||||
|
||||
using SubClass = typename _impl::VoidFallback<StorageCborReader, SubClassCRTP>::T;
|
||||
};
|
||||
|
||||
}} // namespace
|
||||
|
||||
2
stfx/ui/html/cbor.min.js
vendored
2
stfx/ui/html/cbor.min.js
vendored
File diff suppressed because one or more lines are too long
@ -11,7 +11,7 @@
|
||||
background: linear-gradient(#FFF, #EEE, #CCC);
|
||||
color: #000;
|
||||
|
||||
font-family: Bahnschrift, 'DIN Alternate', 'Franklin Gothic Medium', 'Nimbus Sans Narrow', sans-serif-condensed, system-ui, sans-serif;
|
||||
font-family: Bahnschrift, 'DIN Alternate', 'Alte DIN 1451 Mittelschrift', 'D-DIN', 'OpenDin', 'Clear Sans', 'Barlow', 'Abel', 'Franklin Gothic Medium', system-ui, sans-serif;
|
||||
}
|
||||
output {
|
||||
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
|
||||
@ -20,26 +20,23 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{=}
|
||||
<h1>{name}</h1>
|
||||
<ul>
|
||||
<template @foreach>
|
||||
<li @if="${d => d.$type == 'ParamRange'}">{=}</li>
|
||||
</template>
|
||||
</ul>
|
||||
<script src="cbor.min.js"></script>
|
||||
<script src="matsui-bundle.min.js"></script>
|
||||
<script>
|
||||
let state = Matsui.replace(document.body, {name: "..."});
|
||||
state.trackMerges(merge => {
|
||||
window.parent.postMessage(CBOR.encode(merge), '*');
|
||||
});
|
||||
addEventListener('message', e => {
|
||||
let state = window.state = Matsui.replace(document.body, CBOR.decode(e.data));
|
||||
state.trackMerges(merge => {
|
||||
window.parent.postMessage(CBOR.encode(merge), '*');
|
||||
}, true/*async*/, false/*include direct merges*/);
|
||||
|
||||
addEventListener('message', e => {
|
||||
state.merge(CBOR.decode(e.data));
|
||||
});
|
||||
}, {once: true});
|
||||
state.merge(CBOR.decode(e.data));
|
||||
});
|
||||
|
||||
//*
|
||||
window.dispatchEvent(new MessageEvent('message', {
|
||||
data: CBOR.encode({name: "Hello 😀"})
|
||||
}));//*/
|
||||
window.parent.postMessage(CBOR.encode("ready"), '*');
|
||||
</script>
|
||||
</body>
|
||||
|
||||
2
stfx/ui/html/matsui-bundle.min.js
vendored
2
stfx/ui/html/matsui-bundle.min.js
vendored
File diff suppressed because one or more lines are too long
299
stfx/ui/web-ui.h
299
stfx/ui/web-ui.h
@ -1,21 +1,296 @@
|
||||
#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 stfx { namespace web {
|
||||
|
||||
template<typename Sample, template<class, class...> class EffectSTFX, class... ExtraArgs>
|
||||
struct WebUILibraryEffect : public LibraryEffect<Sample, EffectSTFX, ExtraArgs...> {
|
||||
std::string webPage = "generic.html";
|
||||
int webWidth = 640, webHeight = 480;
|
||||
/* Given an STFX template which (when insantiated) has inheritance like:
|
||||
|
||||
EffectSTFX : Effect
|
||||
|
||||
using LibraryEffect<Sample, EffectSTFX, ExtraArgs...>::LibraryEffect;
|
||||
This produces a template with inheritance like:
|
||||
|
||||
void webReceive(const void *message, size_t size) {
|
||||
std::cout << "received " << size << " bytes from webview\n";
|
||||
}
|
||||
private:
|
||||
using Super = LibraryEffect<Sample, EffectSTFX, ExtraArgs...>;
|
||||
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");
|
||||
std::string text, units;
|
||||
info->toString((double)*this, text, units);
|
||||
storage.extra("text", text);
|
||||
storage.extra("textUnits", units);
|
||||
}
|
||||
|
||||
if (prevV != *this) {
|
||||
effect->maybeChanged();
|
||||
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("$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 *) {}
|
||||
};
|
||||
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
// 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") {
|
||||
LOG_EXPR(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;
|
||||
}
|
||||
}
|
||||
std::cout << "received " << size << " bytes from webview\n";
|
||||
}
|
||||
|
||||
void maybeChanged() override {
|
||||
std::cout << "web effect maybe changed\n";
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
} // namespace
|
||||
// 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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user