1
0

Improve web state transfer

This commit is contained in:
Geraint 2025-06-25 16:20:14 +01:00
parent 2efd1de4e9
commit bbad24674a
9 changed files with 419 additions and 83 deletions

View File

@ -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

View File

@ -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>

View File

@ -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:

View File

@ -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

File diff suppressed because one or more lines are too long

View File

@ -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>

File diff suppressed because one or more lines are too long

View File

@ -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 &param) {
param.effect = effect;
param.info = std::unique_ptr<RangeParamInfo>{
new RangeParamInfo(param)
};
return *param.info;
}
template<class PS>
SteppedParamInfo & stepped(const char *key, PS &param) {
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 &param) {
(*this)(key, param);
return {};
}
template<class PS>
SteppedParamIgnore stepped(const char *key, PS &param) {
(*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