Checkpoint

This commit is contained in:
Robin E. R. Davies
2025-07-07 02:30:30 -04:00
parent fddfde9313
commit e7cdd38056
83 changed files with 4733 additions and 214 deletions
+3 -2
View File
@@ -103,7 +103,7 @@
"request": "launch",
// Resolved by CMake Tools:
"program": "${command:cmake.launchTargetPath}",
"args": [ "/etc/pipedal/config", "/etc/pipedal/react", "-port", "0.0.0.0:8080", "-log-level","debug" ],
"args": [ "/etc/pipedal/config", "${workspaceFolder}/vite/dist", "-port", "0.0.0.0:8080", "-log-level","debug" ],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [
@@ -142,7 +142,8 @@
//"[pipedal_alsa_test]"
// "[wifi_channels_test]"
// "[locale]"
"[pipewire_input_stream]"
//"[mod_gui_init]"
"[mod_gui_templates]"
],
"stopAtEntry": false,
+1 -1
View File
@@ -161,7 +161,7 @@
"cSpell.ignoreWords": [
"nammodel"
],
"cSpell.enabled": true,
"cSpell.enabled": false,
// Disable all automatic completion suggestions - only show when Ctrl+Space is pressed
"editor.quickSuggestions": {
+6 -5
View File
@@ -73,6 +73,7 @@ DBusDispatcher::PostHandle DBusDispatcher::PostDelayed(const clock::duration &de
void DBusDispatcher::Run()
{
this->eventFd = eventfd(0, 0);
threadStarted = true;
this->serviceThread = std::thread(
[this]()
{ this->ThreadProc(); });
@@ -169,7 +170,7 @@ void DBusDispatcher::ThreadProc()
int eventTimeout = GetEventTimeoutMs();
if (eventTimeout != -1 && eventTimeout < pollTimeout)
{
pollTimeout = eventTimeout;/*+-*/
pollTimeout = eventTimeout; /*+-*/
}
if (pollTimeout > 250 || pollTimeout == -1)
{
@@ -197,13 +198,13 @@ void DBusDispatcher::ThreadProc()
LogTrace("DBusDispatcher", "ThreadProc", "Service thread terminated.");
}
void DBusDispatcher::Wait()
void DBusDispatcher::WaitForClose()
{
try
{
if (!threadJoined)
if (threadStarted)
{
threadJoined = true;
threadStarted = false;
serviceThread.join();
}
}
@@ -229,7 +230,7 @@ void DBusDispatcher::Stop()
this->stopping = true;
WakeThread();
Wait();
WaitForClose();
}
}
+2 -2
View File
@@ -24,7 +24,7 @@ public:
void Run();
// Stop everything, shutting down gracefully.
void Stop();
void Wait();
void WaitForClose();
bool IsFinished();
// Stop, but don't wait. Suitable for use in a signal handler.
@@ -78,7 +78,7 @@ private:
std::atomic<bool> stopping;
std::vector<CallbackEntry> postedEvents;
bool threadJoined;
bool threadStarted;
std::thread serviceThread;
std::mutex postMutex;
};
+3
View File
@@ -35,6 +35,9 @@ public:
static std::string encode_url_segment(const char*pStart, const char*pEnd, bool isQuerySegment = false);
static std::string encode_url_segment(const std::string &segment, bool isQuerySegment = false) {
return encode_url_segment(segment.c_str(), segment.c_str() + segment.length(), isQuerySegment);
}
static void encode_url_segment(std::ostream&os, const char*pStart, const char *pEnd, bool isQuerySegment = false);
static void encode_url_segment(std::ostream&os, const std::string&segment, bool isQuerySegment = false)
{
Binary file not shown.
Binary file not shown.
+112
View File
@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg132"
width="32"
height="32"
viewBox="0 0 32 32"
sodipodi:docname="red-light-on.png"
xml:space="preserve"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs136"><linearGradient
inkscape:collect="always"
id="linearGradient1239"><stop
style="stop-color:#ffffff;stop-opacity:0.84431136;"
offset="0"
id="stop1235" /><stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop1237" /></linearGradient><linearGradient
inkscape:collect="always"
id="linearGradient1051"><stop
style="stop-color:#580b0b;stop-opacity:1;"
offset="0"
id="stop1047" /><stop
style="stop-color:#252525;stop-opacity:1;"
offset="1"
id="stop1049" /></linearGradient><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1051"
id="linearGradient1053"
x1="16"
y1="24"
x2="16"
y2="8"
gradientUnits="userSpaceOnUse" /><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient1239"
id="radialGradient1241"
cx="15.964222"
cy="13.292918"
fx="15.964222"
fy="13.292918"
r="4.8461695"
gradientTransform="matrix(1,0,0,0.74626396,0,3.3728925)"
gradientUnits="userSpaceOnUse" /></defs><sodipodi:namedview
id="namedview134"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="23.583986"
inkscape:cx="16.536645"
inkscape:cy="17.066666"
inkscape:window-width="1867"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="g138" /><g
inkscape:groupmode="layer"
inkscape:label="Image"
id="g138"><image
width="32"
height="32"
preserveAspectRatio="none"
xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlz
AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAQSSURB
VFiF7VZNaFxVFP7O/Xvv3Zk3P8/JhEnsKCNp0xJokFilSJCC0GUo6NKNSxeuBBcKwZWLUsRNUSJU
ihQyuAmYjYsORRChwUKpENJYaUpqtbGxfTN5/9dFJyo0b5pUXZkPDlw495zz3XPuuecC+9jH/x30
BPs927bLQghHSsmVUpmUMtRa94jo/vLysg/A/BcEvEKhMCqlFEqpjIgyy7IyIsps284YY5nWOhNC
hEmS/La0tNT7twiQ4zijlmUNKaUyR6kWB04x4CUDjPad3CLOv7G0vlBvNH6QUmaWZfkLCwsP/jGB
Wq02whirOY4jRJK8w4153QoCpuIYPE0BABnniKREYNspSfl58+DB9yuVSgBgq91ubz0xgVarVU6S
5GnOuRBJ8okVhi9Wt7bw7NgYRlstFMplAIC/uYn1Gzfw08oK7jkOQsu6NDE1dapYLAZBEATtdjvN
i8EHkRsaGhpRSsGE4Xs6DE82whBT09NoNJuQUgJpCqQplJR4ql5HfXgYvdVVhETP/Hz3rjt94sRi
vV5Hp9PJ9pyBQ4cOucVisR74/ljy4MFXzc1NNjYxgaEDBwYlDb/cvInr167hZqWSJpxPrq6tXeur
duwMludIa207jhNHQfCaFwTMNgalchlJrzdQypUKbGPgBQEn4M35+Xk2Pz+fG0fkKTzPY0KI2KTp
y9U4hgKQ9XogkWsCAMiSBAqAF8e4C7yKv8q84z3I9aaUyoQQKYDRYpqCAfDv3EGxWh1IwL93DxIA
S1MYY5rdbpcVCoXcUucSGB4ejrvdrgFgZP8Y3du3oQBI297RJg6CP/ckAGAMOOe83O+WPRFQSkVK
KcCYtYTzw3aSAFkGf30dqlCAtCxwKQEAaRwjDkNE3S6EeXjXQs7BOF+r1WpcKZXbBbkERkZGovX1
dTJEX/8u5WEvSR4qjIHxfUS+/4iN/Nt6U0pIIS42Gg3GGMt9B3IJzM7OpgDMeKs1dyvL3nouCPj2
6R6HmAi3HCc9eOTIF1EU8Y2NjdzXcNBDhNnZWTZx9OjG91eu1Hucv3AgisD7RnnCAFxxXYSl0tyn
5861lVI4fvx47kzI7c9teJ7HT87MvHvHcS4uuS6ICArYUUCEJdfFr8XipY/Onv1ASsnHx8cfrdVu
M9DpdMzMzAybnJykkWbzy2+vXq2uSjkJImYDsIwBEcEXAj9aFr5z3TSqVj+bO3/+7WazmZVKpZ7W
euBY3tV/YHFx0QJgSSnp4zNnDt+4fv2NOIpeybKsCQCMsTWtdef5Y8cufHj69IrWmrTWPhHdf5zv
XX9ILl++LAHoNE0F55zK5TIREbmuS9voB04B3CeigWN4zwS2sbKyYnmeZ4VhqEqlEu/7MIVCIQaw
BSAgol1/yfaxj338AXYNhqZlqq9VAAAAAElFTkSuQmCC
"
id="image412"
x="0"
y="0" /><circle
style="fill:url(#linearGradient1053);fill-opacity:1;stroke:#000000;stroke-width:1;stop-color:#000000;stroke-dasharray:none;stroke-opacity:1"
id="path468"
cx="16"
cy="16"
r="7.9999995" /><ellipse
style="fill:url(#radialGradient1241);fill-opacity:1;stroke:#000000;stroke-width:0;stroke-dasharray:none;stroke-opacity:1;stop-color:#000000"
id="path1231"
cx="15.964222"
cy="13.292918"
rx="4.3461695"
ry="3.1165216" /></g></svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.
+5
View File
@@ -206,6 +206,9 @@ else()
endif()
set (PIPEDAL_SOURCES
ModTemplateGenerator.cpp ModTemplateGenerator.hpp
WebServerMod.cpp WebServerMod.hpp
ModGui.cpp ModGui.hpp
PipewireInputStream.cpp PipewireInputStream.hpp
AudioFiles.cpp AudioFiles.hpp
AudioFileMetadataReader.cpp AudioFileMetadataReader.hpp
@@ -414,6 +417,8 @@ add_executable(AuxInTest
add_executable(pipedaltest
testMain.cpp
ModGuiTest.cpp
PipewireInputStreamTest.cpp
AudioFilesTest.cpp
+1 -1
View File
@@ -36,7 +36,7 @@ TEST_CASE( "PluginHost memory leak", "[lv2host_leak][Build][Dev]" ) {
{
PluginHost host;
host.Load("/usr/lib/lv2:/usr/local/lib/lv2:/usr/modep/lv2");
host.LoadLilv("/usr/lib/lv2:/usr/local/lib/lv2:/usr/modep/lv2");
}
MemStats finalMemory = GetMemStats();
+11 -1
View File
@@ -22,6 +22,7 @@
#include "ss.hpp"
#include <mutex>
#include <memory>
#include "util.hpp"
using namespace pipedal;
@@ -30,7 +31,16 @@ static std::string empty;
const std::string& MimeTypes::MimeTypeFromExtension(const std::string &extension) const
{
auto iter = extensionToMimeType.find(extension);
if (iter == extensionToMimeType.end()) return empty;
if (iter == extensionToMimeType.end())
{
std::string lowerExt = ToLower(extension);
auto iter2 = extensionToMimeType.find(lowerExt);
if (iter2 != extensionToMimeType.end())
{
return iter2->second;
}
return empty;
}
return iter->second;
}
+1
View File
@@ -95,6 +95,7 @@ ModFileTypes::ModFileTypes(const std::string &fileTypes)
if (wellKnownType)
{
rootDirectories_.push_back(type);
modFileTypes_.push_back(type);
}
else
{
+2
View File
@@ -32,12 +32,14 @@ namespace pipedal
ModFileTypes(const std::string &fileTypes);
const std::vector<std::string> &rootDirectories() const { return rootDirectories_; }
std::vector<std::string> &rootDirectories() { return rootDirectories_; }
const std::vector<std::string> &modFileTypes() const { return modFileTypes_; }
const std::vector<std::string> &fileTypes() const { return fileTypes_; }
static constexpr const char *DEFAULT_FILE_TYPES = "audio,nammodel,mlmodel,sf2,sfz,midisong,midiclip,*"; // (everything)
private:
std::vector<std::string> rootDirectories_;
std::vector<std::string> modFileTypes_; // e.g. "audio", "nammodel", etc.
std::vector<std::string> fileTypes_;
public:
+531
View File
@@ -0,0 +1,531 @@
/*
* Copyright (c) 2025 Robin E. R. Davies
* All rights reserved.
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include "ModGui.hpp"
#include "MapFeature.hpp"
#include "PluginHost.hpp"
#include "Finally.hpp"
#include "lv2/atom/atom.h"
#define MOD_GUI_PREFIX "http://moddevices.com/ns/modgui#"
#define MOD_GUI__gui (MOD_GUI_PREFIX "gui")
#define MOD_GUI__modgui (MOD_GUI_PREFIX "modgui")
#define MOD_GUI__resourcesDirectory (MOD_GUI_PREFIX "resourcesDirectory")
#define MOD_GUI__iconTemplate (MOD_GUI_PREFIX "iconTemplate")
#define MOD_GUI__settingsTemplate (MOD_GUI_PREFIX "settingsTemplate")
#define MOD_GUI__javascript (MOD_GUI_PREFIX "javascript")
#define MOD_GUI__stylesheet (MOD_GUI_PREFIX "stylesheet")
#define MOD_GUI__screenshot (MOD_GUI_PREFIX "screenshot")
#define MOD_GUI__thumbnail (MOD_GUI_PREFIX "thumbnail")
#define MOD_GUI__discussionURL (MOD_GUI_PREFIX "discussionURL")
#define MOD_GUI__documentation (MOD_GUI_PREFIX "documentation")
#define MOD_GUI__brand (MOD_GUI_PREFIX "brand")
#define MOD_GUI__label (MOD_GUI_PREFIX "label")
#define MOD_GUI__model (MOD_GUI_PREFIX "model")
#define MOD_GUI__panel (MOD_GUI_PREFIX "panel")
#define MOD_GUI__color (MOD_GUI_PREFIX "color")
#define MOD_GUI__knob (MOD_GUI_PREFIX "knob")
#define MOD_GUI__port (MOD_GUI_PREFIX "port")
#define MOD_GUI__monitoredOutputs (MOD_GUI_PREFIX "monitoredOutputs")
using namespace pipedal;
ModGuiUris::ModGuiUris(LilvWorld *pWorld, MapFeature &mapFeature)
{
mod_gui__gui = lilv_new_uri(pWorld, MOD_GUI__gui);
mod_gui__modgui = lilv_new_uri(pWorld, MOD_GUI__modgui);
mod_gui__resourceDirectory = lilv_new_uri(pWorld, MOD_GUI__resourcesDirectory);
mod_gui__iconTemplate = lilv_new_uri(pWorld, MOD_GUI__iconTemplate);
mod_gui__settingsTemplate = lilv_new_uri(pWorld, MOD_GUI__settingsTemplate);
mod_gui__javascript = lilv_new_uri(pWorld, MOD_GUI__javascript);
mod_gui__stylesheet = lilv_new_uri(pWorld, MOD_GUI__stylesheet);
mod_gui__screenshot = lilv_new_uri(pWorld, MOD_GUI__screenshot);
mod_gui__thumbnail = lilv_new_uri(pWorld, MOD_GUI__thumbnail);
mod_gui__discussionURL = lilv_new_uri(pWorld, MOD_GUI__discussionURL);
mod_gui__documentation = lilv_new_uri(pWorld, MOD_GUI__documentation);
mod_gui__brand = lilv_new_uri(pWorld, MOD_GUI__brand);
mod_gui__label = lilv_new_uri(pWorld, MOD_GUI__label);
mod_gui__model = lilv_new_uri(pWorld, MOD_GUI__model);
mod_gui__panel = lilv_new_uri(pWorld, MOD_GUI__panel);
mod_gui__color = lilv_new_uri(pWorld, MOD_GUI__color);
mod_gui__knob = lilv_new_uri(pWorld, MOD_GUI__knob);
mod_gui__port = lilv_new_uri(pWorld, MOD_GUI__port);
mod_gui__monitoredOutputs = lilv_new_uri(pWorld, MOD_GUI__monitoredOutputs);
}
static const char*NonNull(const char*string) {
return string ? string : "";
}
ModGui::ModGui(
PluginHost *lv2Host,
const LilvPlugin *lilvPlugin,
const std::string &resourceDirectory,
const LilvNode * modGuiUrl)
: pluginUri_(lilv_node_as_uri(lilv_plugin_get_uri(lilvPlugin))),
resourceDirectory_(resourceDirectory)
{
LilvWorld *pWorld = lv2Host->getWorld();
ModGuiUris &modGuiUrids = *lv2Host->mod_gui_uris;
PluginHost::LilvUris &lilvUris = *lv2Host->lilvUris;
AutoLilvNode modguiIcon = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__iconTemplate, nullptr);
if (modguiIcon) {
this->iconTemplate_ = NonNull(lilv_file_uri_parse(lilv_node_as_string(modguiIcon), nullptr));
}
AutoLilvNode modguiSettingsTemplate = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__settingsTemplate, nullptr);
if (modguiSettingsTemplate) {
this->settingsTemplate_ = NonNull(lilv_file_uri_parse(lilv_node_as_string(modguiSettingsTemplate), nullptr));
}
AutoLilvNode modguiJavascript = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__javascript, nullptr);
if (modguiJavascript) {
this->javascript_ = NonNull(lilv_file_uri_parse(lilv_node_as_string(modguiJavascript), nullptr));
}
AutoLilvNode modguiStylesheet = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__stylesheet, nullptr);
if (modguiStylesheet) {
this->stylesheet_ = NonNull(lilv_file_uri_parse(lilv_node_as_string(modguiStylesheet), nullptr));
}
AutoLilvNode modguiScreenshot = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__screenshot, nullptr);
if (modguiScreenshot) {
this->screenshot_ = NonNull(lilv_file_uri_parse(lilv_node_as_string(modguiScreenshot), nullptr));
}
AutoLilvNode modguiThumbnail = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__thumbnail, nullptr);
if (modguiThumbnail) {
this->thumbnail_ = NonNull(lilv_file_uri_parse(lilv_node_as_string(modguiThumbnail), nullptr));
}
AutoLilvNode discussionUrl = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__discussionURL, nullptr);
if (discussionUrl) {
this->discussionUrl_ = NonNull(lilv_file_uri_parse(lilv_node_as_string(discussionUrl), nullptr));
}
AutoLilvNode documentationUrl = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__documentation, nullptr);
if (documentationUrl) {
this->documentationUrl_ = NonNull(lilv_file_uri_parse(lilv_node_as_string(documentationUrl), nullptr));
}
AutoLilvNode brand = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__brand, nullptr);
if (brand) {
this->brand_ = NonNull(lilv_node_as_string(brand));
}
AutoLilvNode label = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__label, nullptr);
if (label) {
this->label_ = NonNull(lilv_node_as_string(label));
}
AutoLilvNode model = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__model, nullptr);
if (model) {
this->model_ = NonNull(lilv_node_as_string(model));
}
AutoLilvNode panel = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__panel, nullptr);
if (panel) {
this->panel_ = NonNull(lilv_node_as_string(panel));
}
AutoLilvNode color = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__color, nullptr);
if (color) {
this->color_ = NonNull(lilv_node_as_string(color));
}
AutoLilvNode knob = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__knob, nullptr);
if (knob) {
this->knob_ = NonNull(lilv_node_as_string(knob));
}
AutoLilvNodes ports = lilv_world_find_nodes(pWorld, modGuiUrl, modGuiUrids.mod_gui__port, nullptr);
if (ports) {
LILV_FOREACH(nodes, it, ports.Get()) {
AutoLilvNode port_node = lilv_nodes_get(ports, it);
this->ports_.push_back(ModGuiPort(lv2Host,port_node));
}
}
std::sort(this->ports_.begin(), this->ports_.end(),
[](const ModGuiPort &p1, const ModGuiPort &p2) {
return p1.index() < p2.index();
});
AutoLilvNodes monitoredOutputs = lilv_world_find_nodes(pWorld, modGuiUrl, modGuiUrids.mod_gui__monitoredOutputs, nullptr);
if (monitoredOutputs) {
LILV_FOREACH(nodes, it, monitoredOutputs.Get()) {
AutoLilvNode monitoredOutput = lilv_nodes_get(monitoredOutputs.Get(), it);
AutoLilvNode symbol = lilv_world_get(pWorld, monitoredOutput, lilvUris.lv2core__symbol, nullptr);
if (symbol) {
this->monitoredOutputs_.push_back(NonNull(lilv_node_as_string(symbol)));
}
}
}
}
ModGui::ptr ModGui::Create(PluginHost *lv2Host, const LilvPlugin *lilvPlugin)
{
LilvWorld *pWorld = lv2Host->getWorld();
ModGuiUris &modGuiUrids = *(lv2Host->mod_gui_uris);
AutoLilvNode modGuiUri;
std::string resourceDirectory;
AutoLilvNodes modGuiNodes = lilv_plugin_get_value(lilvPlugin,modGuiUrids.mod_gui__gui);
if (!modGuiNodes) {
return nullptr; // no mod gui found.
}
LILV_FOREACH(nodes, it, modGuiNodes.Get()) {
AutoLilvNode node = lilv_nodes_get(modGuiNodes.Get(), it);
AutoLilvNode resourceDir = lilv_world_get(pWorld, node, modGuiUrids.mod_gui__resourceDirectory,nullptr);
if (resourceDir) {
resourceDirectory = lilv_file_uri_parse(lilv_node_as_string(resourceDir),nullptr);
modGuiUri = lilv_node_duplicate(node);
} else {
resourceDirectory.clear();
modGuiUri.Free();
}
}
if (!modGuiUri) {
return nullptr; // no mod gui found.
}
return std::shared_ptr<ModGui>(new ModGui(lv2Host, lilvPlugin, resourceDirectory, modGuiUri.Get()));
}
ModGuiPort::ModGuiPort(PluginHost *lv2Host, const LilvNode *portNode)
{
LilvWorld *pWorld = lv2Host->getWorld();
PluginHost::LilvUris &lilvUris = *lv2Host->lilvUris;
this->index_ = lilv_node_as_int(lilv_world_get(pWorld, portNode, lilvUris.lv2core__index, nullptr));
this->symbol_ = NonNull(lilv_node_as_string(lilv_world_get(pWorld, portNode, lilvUris.lv2core__symbol, nullptr)));
this->name_ = NonNull(lilv_node_as_string(lilv_world_get(pWorld, portNode, lilvUris.lv2core__name, nullptr)));
}
static json_variant UnitsObj(const std::string &label, const std::string &render, const std::string &symbol)
{
json_variant obj = json_variant::make_object();
obj["label"] = label;
obj["render"] = render;
obj["symbol"] = symbol;
return obj;
}
static std::map<Units,json_variant> unitsMap
{
{Units::none, UnitsObj("","%f","")},
{Units::unknown, UnitsObj("","%f","")},
{Units::s, UnitsObj("seconds", "%f s", "s")},
{Units::ms, UnitsObj("milliseconds", "%f ms", "ms")},
{Units::min, UnitsObj("minutes", "%f mins", "min")},
{Units::bar, UnitsObj("bars", "%f bars", "bars")},
{Units::beat, UnitsObj("beats", "%f beats", "beats")},
{Units::frame, UnitsObj("audio frames", "%f frames", "frames")},
{Units::m, UnitsObj("metres", "%f m", "m")},
{Units::cm, UnitsObj("centimetres", "%f cm", "cm")},
{Units::mm, UnitsObj("millimetres", "%f mm", "mm")},
{Units::km, UnitsObj("kilometres", "%f km", "km")},
{Units::inch, UnitsObj("inches", "\"%f\"", "in")},
{Units::mile, UnitsObj("miles", "%f mi", "mi")},
{Units::db, UnitsObj("decibels", "%f dB", "dB")},
{Units::pc, UnitsObj("percent", "%f%%", "%")},
{Units::coef, UnitsObj("coefficient", "* %f", "*")},
{Units::hz, UnitsObj("hertz", "%f Hz", "Hz")},
{Units::khz, UnitsObj("kilohertz", "%f kHz", "kHz")},
{Units::mhz, UnitsObj("megahertz", "%f MHz", "MHz")},
{Units::bpm, UnitsObj("beats per minute", "%f BPM", "BPM")},
{Units::oct, UnitsObj("octaves", "%f octaves", "oct")},
{Units::cent, UnitsObj("cents", "%f ct", "ct")},
{Units::semitone12TET, UnitsObj("semitones", "%f semi", "semi")},
{Units::degree, UnitsObj("degrees", "%f°", "°")},
{Units::midiNote, UnitsObj("MIDI note", "MIDI note %d", "note")},
};
static json_variant UnitsToJsonVariant(Units units)
{
if (unitsMap.find(units) != unitsMap.end())
{
return unitsMap[units];
}
return unitsMap[Units::none];
}
template<typename T>
static inline json_variant MakeJsonArray(const std::vector<T> &vec)
{
json_variant array = json_variant::make_array();
auto &arrayRef = *array.as_array();
for (const auto &item : vec)
{
json_variant itemVariant = json_variant(item);
arrayRef.push_back(itemVariant);
}
return array;
}
static std::string AtomTypeToRangesType(const std::string& atomType)
{
if (atomType == LV2_ATOM__Path ||
atomType == LV2_ATOM__String ||
atomType == LV2_ATOM__URI ||
atomType == LV2_ATOM__URID)
{
return "s";
}
if (atomType == LV2_ATOM__Float)
{
return "f";
}
if (atomType == LV2_ATOM__Double)
{
return "d";
}
if (atomType == LV2_ATOM__Long)
{
return "l";
}
if (atomType == LV2_ATOM__Vector)
{
return "v";
}
return "?";
}
json_variant pipedal::MakeModGuiTemplateData(
std::shared_ptr<Lv2PluginInfo> pluginInfo)
{
json_variant context = json_variant::make_object();
auto &contextObj = *(context.as_object());
auto modGui = pluginInfo->modGui();
contextObj["brand"] = modGui->brand();
contextObj["label"] = modGui->label();
contextObj["model"] = modGui->model();
contextObj["panel"] = modGui->panel();
contextObj["color"] = modGui->color();
contextObj["knob"] = modGui->knob();
json_variant controls = json_variant::make_array();
auto &controlArray = *controls.as_array();
size_t ix = 0;
for (const auto& modGuiPort : modGui->ports())
{
const Lv2PortInfo &pluginPort = pluginInfo->getPort(modGuiPort.symbol());
if (!pluginPort.is_control_port() || !pluginPort.is_input() || pluginPort.not_on_gui())
{
continue; // only include control ports
}
json_variant control = json_variant::make_object();
auto &portObj = *control.as_object();
portObj["index"] = double(pluginPort.index());
portObj["symbol"] = pluginPort.symbol();
portObj["name"] = modGuiPort.name();
portObj["comment"] = pluginPort.comment();
portObj["units"] = UnitsToJsonVariant(pluginPort.units());
{
json_variant ranges = json_variant::make_object();
ranges["minimum"] = SS(pluginPort.min_value());
ranges["maximum"] = SS(pluginPort.max_value());
ranges["default"] = SS(pluginPort.default_value());
portObj["ranges"] = json_variant::make_array();
}
json_variant scalePoints = json_variant::make_array();
auto &scalePointsArray = *scalePoints.as_array();
for (const auto &scalePoint: pluginPort.scale_points())
{
json_variant scalePointObj = json_variant::make_object();
scalePointObj["valid"] = true;
scalePointObj["label"] = scalePoint.label();
scalePointObj["value"] = SS(scalePoint.value());
scalePointsArray.push_back(scalePointObj);
}
portObj["scalePoints"] = scalePoints;
controlArray.push_back(control);
}
contextObj["controls"] = controls;
json_variant effect = json_variant::make_object();
contextObj["effect"] = effect;
json_variant ports = json_variant::make_object();
(*effect.as_object())["ports"] = ports;
json_variant audio = json_variant::make_object();
(*ports.as_object())["audio"] = audio;
json_variant audio_input = json_variant::make_array();
(*audio.as_object())["input"] = audio_input;
json_variant audio_output = json_variant::make_array();
(*audio.as_object())["output"] = audio_output;
json_variant midi = json_variant::make_object();
(*ports.as_object())["midi"] = midi;
json_variant midi_input = json_variant::make_array();
(*midi.as_object())["input"] = midi_input;
json_variant midi_output = json_variant::make_array();
(*midi.as_object())["output"] = midi_output;
json_variant cv = json_variant::make_object();
(*ports.as_object())["cv"] = cv;
json_variant cv_input = json_variant::make_array();
(*cv.as_object())["input"] = cv_input;
json_variant cv_output = json_variant::make_array();
(*cv.as_object())["output"] = cv_output;
{
json_variant parametersObj = json_variant::make_object();
json_variant pathObj = json_variant::make_array();
parametersObj["path"] = pathObj;
for (const auto &patchProperty: pluginInfo->patchProperties())
{
json_variant parameterObj = json_variant::make_object();
parameterObj["valid"] = true;
parameterObj["readable"] = patchProperty.readable();
parameterObj["writable"] = patchProperty.writable();
parameterObj["uri"] = patchProperty.uri();
parameterObj["label"] = patchProperty.label();
parameterObj["type"] = patchProperty.type();
json_variant rangesObj = json_variant::make_object();
rangesObj["type"] = AtomTypeToRangesType(patchProperty.type());
rangesObj["atomType"] = patchProperty.type();
parameterObj["ranges"] = rangesObj;
parameterObj["comment"] = patchProperty.comment();
parameterObj["shortName"] = patchProperty.shortName();
parameterObj["fileTypes"] = MakeJsonArray(patchProperty.fileTypes());
parameterObj["supportedExtensions"] = MakeJsonArray(patchProperty.supportedExtensions());
if (patchProperty.type() == LV2_ATOM__Path && patchProperty.writable()) {
// Feed them no files; they will be added later.
json_variant filesObj = json_variant::make_array();
json_variant fileObj = json_variant::make_object();
fileObj["fileType"] = "{{filetype}}"; // magic tags that we will use to generate files at runtime.
fileObj["fullname"] = "{{fullname}}";
fileObj["basename"] = "{{basename}}";
filesObj.as_array()->push_back(fileObj);
parameterObj["files"] = filesObj;
pathObj.as_array()->push_back(parameterObj);
}
}
effect["parameters"] = parametersObj;
}
for (const auto &pluginPort : pluginInfo->ports())
{
if (pluginPort->is_control_port())
{
continue; // only include control ports
}
if (pluginPort->is_audio_port())
{
json_variant audioPort = json_variant::make_object();
auto &audioPortObj = *audioPort.as_object();
audioPortObj["index"] = double(pluginPort->index());
audioPortObj["symbol"] = pluginPort->symbol();
audioPortObj["name"] = pluginPort->name();
if (pluginPort->is_input())
{
audio_input.as_array()->push_back(audioPort);
}
else
{
audio_output.as_array()->push_back(audioPort);
}
}
else if (pluginPort->is_atom_port())
{
if (pluginPort->supports_midi())
{
json_variant midiPort = json_variant::make_object();
auto &midiPortObj = *midiPort.as_object();
midiPortObj["index"] = double(pluginPort->index());
midiPortObj["symbol"] = pluginPort->symbol();
midiPortObj["name"] = pluginPort->name();
if (pluginPort->is_input())
{
midi_input.as_array()->push_back(midiPort);
}
else
{
midi_output.as_array()->push_back(midiPort);
}
}
}
/* else if plugin_port->is_cv_port()... */
}
return context;
}
JSON_MAP_BEGIN(ModGuiPort)
JSON_MAP_REFERENCE(ModGuiPort,index)
JSON_MAP_REFERENCE(ModGuiPort,symbol)
JSON_MAP_REFERENCE(ModGuiPort,name)
JSON_MAP_END()
JSON_MAP_BEGIN(ModGui)
JSON_MAP_REFERENCE(ModGui,pluginUri)
JSON_MAP_REFERENCE(ModGui,resourceDirectory)
JSON_MAP_REFERENCE(ModGui,iconTemplate)
JSON_MAP_REFERENCE(ModGui,settingsTemplate)
JSON_MAP_REFERENCE(ModGui,screenshot)
JSON_MAP_REFERENCE(ModGui,javascript)
JSON_MAP_REFERENCE(ModGui,stylesheet)
JSON_MAP_REFERENCE(ModGui,thumbnail)
JSON_MAP_REFERENCE(ModGui,discussionUrl)
JSON_MAP_REFERENCE(ModGui,documentationUrl)
JSON_MAP_REFERENCE(ModGui,brand)
JSON_MAP_REFERENCE(ModGui,label)
JSON_MAP_REFERENCE(ModGui,model)
JSON_MAP_REFERENCE(ModGui,panel)
JSON_MAP_REFERENCE(ModGui,color)
JSON_MAP_REFERENCE(ModGui,knob)
JSON_MAP_REFERENCE(ModGui,ports)
JSON_MAP_REFERENCE(ModGui,monitoredOutputs)
JSON_MAP_END()
+180
View File
@@ -0,0 +1,180 @@
/*
* Copyright (c) 2025 Robin E. R. Davies
* All rights reserved.
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include "lv2/urid/urid.h"
#include <string>
#include <lilv/lilv.h>
#include <memory>
#include "AutoLilvNode.hpp"
#include "json.hpp"
#include "json_variant.hpp"
namespace pipedal
{
class MapFeature;
class Lv2PluginInfo;
class ModGuiUris
{
public:
ModGuiUris(LilvWorld *pWorld, MapFeature &mapFeature);
AutoLilvNode mod_gui__gui;
AutoLilvNode mod_gui__modgui;
AutoLilvNode mod_gui__resourceDirectory;
AutoLilvNode mod_gui__iconTemplate;
AutoLilvNode mod_gui__settingsTemplate;
AutoLilvNode mod_gui__javascript;
AutoLilvNode mod_gui__stylesheet;
AutoLilvNode mod_gui__screenshot;
AutoLilvNode mod_gui__thumbnail;
AutoLilvNode mod_gui__discussionURL;
AutoLilvNode mod_gui__documentation;
AutoLilvNode mod_gui__brand;
AutoLilvNode mod_gui__label;
AutoLilvNode mod_gui__model;
AutoLilvNode mod_gui__panel;
AutoLilvNode mod_gui__color;
AutoLilvNode mod_gui__knob;
AutoLilvNode mod_gui__port;
AutoLilvNode mod_gui__monitoredOutputs;
};
class PluginHost;
class ModGuiPort {
public:
ModGuiPort() = default;
ModGuiPort(PluginHost *lv2Host, const LilvNode *portNode);
private:
uint32_t index_ = 0;
std::string symbol_;
std::string name_;
public:
uint32_t index() const { return index_; }
void index(uint32_t value) { index_ = value; }
const std::string &symbol() const { return symbol_; }
void symbol(const std::string &value) { symbol_ = value; }
const std::string &name() const { return name_; }
void name(const std::string &value) { name_ = value; }
DECLARE_JSON_MAP(ModGuiPort);
};
class ModGui
{
private:
ModGui(PluginHost *lv2Host, const LilvPlugin *lilvPlugin, const std::string &resourceDirectory, const LilvNode *modGuiNode);
public:
using self = ModGui;
using ptr = std::shared_ptr<self>;
static ptr Create(PluginHost *lv2Host, const LilvPlugin *lilvPlugin);
ModGui() = default;
virtual ~ModGui() = default;
ModGui(const ModGui &) = default;
ModGui &operator=(const ModGui &) = default;
ModGui(ModGui &&) = default;
ModGui &operator=(ModGui &&) = default;
private:
std::string pluginUri_;
std::string resourceDirectory_;
std::string iconTemplate_;
std::string settingsTemplate_;
std::string javascript_;
std::string stylesheet_;
std::string screenshot_;
std::string thumbnail_;
std::string discussionUrl_;
std::string documentationUrl_;
std::string brand_;
std::string label_;
std::string model_;
std::string panel_;
std::string color_;
std::string knob_;
std::vector<ModGuiPort> ports_;
std::vector<std::string> monitoredOutputs_;
public:
const std::string &pluginUri() const { return pluginUri_; }
void pluginUri(const std::string &value) { pluginUri_ = value; }
const std::string &resourceDirectory() const { return resourceDirectory_; }
void resourceDirectory(const std::string &value) { resourceDirectory_ = value; }
const std::string &iconTemplate() const { return iconTemplate_; }
void iconTemplate(const std::string &value) { iconTemplate_ = value; }
const std::string &settingsTemplate() const { return settingsTemplate_; }
void settingsTemplate(const std::string &value) { settingsTemplate_ = value; }
const std::string &screenshot() const { return screenshot_; }
const std::string &javascript() const { return javascript_; }
void javascript(const std::string &value) { javascript_ = value; }
const std::string &stylesheet() const { return stylesheet_; }
void stylesheet(const std::string &value) { stylesheet_ = value; }
void screenshot(const std::string &value) { screenshot_ = value; }
const std::string &thumbnail() const { return thumbnail_; }
void thumbnail(const std::string &value) { thumbnail_ = value; }
const std::string &discussionUrl() const { return discussionUrl_; }
void discussionUrl(const std::string &value) { discussionUrl_ = value; }
const std::string &documentationUrl() const { return documentationUrl_; }
void documentationUrl(const std::string &value) { documentationUrl_ = value; }
const std::string &brand() const { return brand_; }
void brand(const std::string &value) { brand_ = value; }
const std::string &label() const { return label_; }
void label(const std::string &value) { label_ = value; }
const std::string &model() const { return model_; }
void model(const std::string &value) { model_ = value; }
const std::string &panel() const { return panel_; }
void panel(const std::string &value) { panel_ = value; }
const std::string &color() const { return color_; }
void color(const std::string &value) { color_ = value; }
const std::string &knob() const { return knob_; }
void knob(const std::string &value) { knob_ = value; }
const std::vector<ModGuiPort> &ports() const { return ports_; }
std::vector<ModGuiPort> &ports() { return ports_; }
void ports(const std::vector<ModGuiPort> &value) { ports_ = value; }
const std::vector<std::string> &monitoredOutputs() const { return monitoredOutputs_; }
std::vector<std::string> &monitoredOutputs() { return monitoredOutputs_; }
void monitoredOutputs(const std::vector<std::string> &value) { monitoredOutputs_ = value; }
DECLARE_JSON_MAP(ModGui);
};
json_variant MakeModGuiTemplateData(
std::shared_ptr<Lv2PluginInfo> pluginInfo
);
}
+117
View File
@@ -0,0 +1,117 @@
// Copyright (c) 2025 Robin Davies
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#include "pch.h"
#include "catch.hpp"
#include <sstream>
#include <cstdint>
#include <string>
#include <iostream>
#include <atomic>
#include <thread>
#include "ModTemplateGenerator.hpp"
#include "PiPedalModel.hpp"
using namespace pipedal;
using namespace std;
class PluginHostTest {
public:
static void TestInit()
{
PiPedalModel model;
model.GetPluginHost().LoadLilv();
const auto&plugins = model.GetPluginHost().GetPlugins();
REQUIRE(!plugins.empty());
size_t modGuicount = 0;
for (const auto&plugin : plugins)
{
REQUIRE(plugin != nullptr);
if (plugin->modGui())
{
modGuicount++;
const auto&modGui = plugin->modGui();
REQUIRE(!modGui->iconTemplate().empty());
REQUIRE(modGui->javascript().empty());
REQUIRE(!modGui->stylesheet().empty());
REQUIRE(!modGui->screenshot().empty());
REQUIRE(!modGui->thumbnail().empty());
}
}
}
};
TEST_CASE("ModGui Init Test", "[mod_gui_init]")
{
PluginHostTest::TestInit();
}
TEST_CASE("ModGui Templates", "[mod_gui_templates]")
{
json_variant data = json_variant::make_object();
data["name"] = "Test Plugin";
data["uri"] = "http://example.com/test-plugin";
data["version"] = "1.0.0";
data["author"] = "Test Author";
data["cn"] = "14323";
json_variant items = json_variant::make_array();
for (size_t i = 0; i < 3; ++i)
{
json_variant item = json_variant::make_object();
item["label"] = "Item " + std::to_string(i + 1);
items.as_array()->push_back(item);
}
data["items"] = items;
data["inputs"] = json_variant::make_object();
data["inputs"]["input1"] = "Input 1";
data["inputs"]["input2"] = "Input 2";
data["_cn"] = "?cn=1234";
data["_cns"] = "_toobamp.lv2_ToobAmp__1234";
std::string templateString = R"(
<div>
<h1>{{name}}</h1>
<p>URI: {{uri}}</p>
<p>Version: {{version}}</p>
<p>Author: {{author}}</p>
<ul>
{{#items}}
<li id="item-{{cn}}">{{label}}</li>
{{/items}}
</ul>
<h2>{{inputs.input1}}</h2>
<h2>{{inputs.input2}}</h2>
<h2>{{inputs.input3}}</h2>
<img src="img/helpIcon.png{{{cn}}}" class="{{{cns}}}_help_icon" alt="Help Icon" />
</div>
)";
auto result = GenerateFromTemplateString(templateString, data);
cout << result;
cout << endl;
}
+295
View File
@@ -0,0 +1,295 @@
/*
* Copyright (c) 2025 Robin E. R. Davies
* All rights reserved.
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include "ModTemplateGenerator.hpp"
#include <fstream>
#include <sstream>
#include "util.hpp"
namespace fs = std::filesystem;
namespace pipedal
{
namespace impl
{
static std::string trim(const std::string &str)
{
size_t first = str.find_first_not_of(" \t\n\r");
if (first == std::string::npos)
return "";
size_t last = str.find_last_not_of(" \t\n\r");
return str.substr(first, (last - first + 1));
}
static bool variableEndsWithNumber(const std::string &variableName)
{
size_t pos = variableName.find_last_of(".");
if (pos == std::string::npos)
{
return false;
}
for (size_t i = pos + 1; i < variableName.length(); ++i)
{
if (!std::isdigit(variableName[i]))
{
return false;
}
}
// Check if the last character is a digit
return true;
}
void splitIndexedArrayVariable(
const std::string &variableName,
std::string &arrayName,
int64_t &arrayIndex)
{
size_t pos = variableName.find_last_of(".");
if (pos == std::string::npos)
{
throw std::logic_error("Variable name does not contain an index: " + variableName);
}
else
{
arrayName = variableName.substr(0, pos);
arrayIndex = std::stoll(variableName.substr(pos + 1));
}
}
class VariableContext
{
public:
VariableContext(
const json_variant &context,
VariableContext *parent = nullptr)
: context(context), parent(parent)
{
}
json_variant getVariable(const std::string &name)
{
std::vector<std::string> parts = split(trim(name), '.');
json_variant *current = &context;
for (const auto &part : parts)
{
if (current->is_object())
{
auto ff = current->as_object()->find(part);
if (ff != current->as_object()->end())
{
current = &ff->second;
continue;
}
}
if (parent)
{
return parent->getVariable(name);
}
else
{
return json_variant(json_null());
}
}
return *current;
}
private:
json_variant context;
VariableContext *parent;
};
static std::string GenerateTemplateFromString(
const std::string &content,
VariableContext &context)
{
std::stringstream ss;
size_t ix = 0;
size_t end = content.length();
while (ix < end)
{
// copy content until we find a '{{'
char c = content[ix++];
if (c != '{')
{
ss << c;
continue;
}
if (ix >= end)
{
break;
}
c = content[ix++];
if (c != '{')
{
ss << '{';
ss << c;
continue;
}
if (ix < end && content[ix] == '{')
{
// this is a '{{{', so we just copy it.
++ix; // skip the third '{'
size_t endTagPos = content.find("}}}", ix);
if (endTagPos == std::string::npos)
{
throw std::runtime_error("Unmatched opening tag '{{{' in template.");
}
std::string variableString = content.substr(ix, endTagPos - ix);
ix = endTagPos + 3; // Move past the closing '}}}'
auto result = context.getVariable("_" + variableString);
if (result.is_null())
{
result = ""; // Default to empty string if variable not found
}
if (!result.is_string())
{
throw std::runtime_error("Variable '" + variableString + "' is not a string.");
}
ss << result.as_string();
continue;
}
else
{
// Now we have a '{{' at position ix-2.
size_t endTagPos = content.find("}}", ix);
if (endTagPos == std::string::npos)
{
throw std::runtime_error("Unmatched opening tag '{{' in template.");
}
std::string variableString = content.substr(ix, endTagPos - ix);
ix = endTagPos + 2; // Move past the closing '}}'
if (!variableString.starts_with("#"))
{
// a straightforward variable substitution.
json_variant value = context.getVariable(variableString);
if (value.is_null())
{
value = ""; // Default to empty string if variable not found
}
if (!value.is_string())
{
throw std::runtime_error("Variable '" + variableString + "' is not a string.");
}
ss << value.as_string();
}
else
{
// this is a looping construct.
std::string variableName = variableString.substr(1);
std::string endTag = SS("{{/" << variableName << "}}");
size_t endTagPos = content.find(endTag, ix);
if (endTagPos == std::string::npos)
{
throw std::runtime_error("Unmatched opening tag for '" + variableString + "' in template.");
}
std::string arrayContent = content.substr(ix, endTagPos - ix);
if (variableEndsWithNumber(variableName))
{
std::string arrayName;
int64_t arrayIndex = 0;
splitIndexedArrayVariable(variableName, arrayName, arrayIndex);
json_variant array = context.getVariable(arrayName);
auto &arrayValue = *array.as_array();
if (arrayIndex >= 0 && arrayIndex < static_cast<int64_t>(arrayValue.size()))
{
json_variant contextValue = arrayValue[(size_t)arrayIndex];
// variable frame.
VariableContext itemContext{contextValue, &context};
ss << GenerateTemplateFromString(
arrayContent,
itemContext);
}
}
else
{
json_variant variable = context.getVariable(variableName);
if (variable.is_array())
{
auto &arrayValue = *variable.as_array();
for (auto iter = arrayValue.begin(); iter != arrayValue.end(); ++iter)
{
// variable frame.
VariableContext itemContext{*iter, &context};
ss << GenerateTemplateFromString(
arrayContent,
itemContext);
}
} else if (variable.is_object())
{
VariableContext itemContext(variable,&context);
ss << GenerateTemplateFromString(
arrayContent,
itemContext);
}
}
ix = endTagPos + endTag.length(); // Move past the closing tag
}
}
}
return ss.str();
}
}
using namespace impl;
std::string GenerateFromTemplateString(
const std::string &templateString,
const json_variant &data)
{
VariableContext context{data, nullptr};
return impl::GenerateTemplateFromString(
templateString,
context);
}
std::string GenerateFromTemplateFile(
const std::filesystem::path &templateFilePath,
const json_variant &data)
{
std::string templateContent;
if (!fs::exists(templateFilePath))
{
throw std::runtime_error("Template file not found: " + templateFilePath.string());
}
std::ifstream file(templateFilePath);
if (!file.is_open())
{
throw std::runtime_error("Failed to open template file: " + templateFilePath.string());
}
templateContent.assign((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
file.close();
return GenerateFromTemplateString(
templateContent,
data);
}
}
+41
View File
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2025 Robin E. R. Davies
* All rights reserved.
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include <filesystem>
#include <string>
#include "json_variant.hpp"
namespace pipedal
{
std::string GenerateFromTemplateString(
const std::string& templateString,
const json_variant& data
);
std::string GenerateFromTemplateFile(
const std::filesystem::path& templateFilePath,
const json_variant& data
);
}
+1 -1
View File
@@ -242,7 +242,7 @@ void PiPedalModel::LoadLv2PluginInfo()
}
pluginChangeMonitor = std::make_unique<Lv2PluginChangeMonitor>(*this);
pluginHost.Load(configuration.GetLv2Path().c_str());
pluginHost.LoadLilv(configuration.GetLv2Path().c_str());
// Copy all presets out of Lilv data to json files
// so that we can close lilv while we're actually
+3 -1
View File
@@ -331,7 +331,9 @@ namespace pipedal
void LoadLv2PluginInfo();
void Load();
const PluginHost &GetLv2Host() const { return pluginHost; }
const PluginHost &GetPluginHost() const { return pluginHost; }
PluginHost &GetPluginHost() { return pluginHost; }
Pedalboard GetCurrentPedalboardCopy()
{
std::lock_guard<std::recursive_mutex> guard(mutex);
+2 -2
View File
@@ -1331,12 +1331,12 @@ public:
}
else if (message == "plugins")
{
auto ui_plugins = model.GetLv2Host().GetUiPlugins();
auto ui_plugins = model.GetPluginHost().GetUiPlugins();
Reply(replyTo, "plugins", ui_plugins);
}
else if (message == "pluginClasses")
{
auto classes = model.GetLv2Host().GetLv2PluginClass();
auto classes = model.GetPluginHost().GetLv2PluginClass();
Reply(replyTo, "pluginClasses", classes);
}
else if (message == "hello")
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) 2022 Robin Davies
// Copyright (c) 2025 Robin Davies
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
+207 -59
View File
@@ -136,8 +136,11 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld)
lv2core__symbol = lilv_new_uri(pWorld, LV2_CORE__symbol);
lv2core__name = lilv_new_uri(pWorld, LV2_CORE__name);
lv2core__shortName = lilv_new_uri(pWorld, LV2_CORE_PREFIX "shortName"); // ?? from mod sources
lv2core__index = lilv_new_uri(pWorld, LV2_CORE__index);
lv2core__Parameter = lilv_new_uri(pWorld, LV2_CORE_PREFIX "Parameter");
lv2core__minorVersion = lilv_new_uri(pWorld, LV2_CORE__minorVersion);
lv2core__microVersion = lilv_new_uri(pWorld, LV2_CORE__microVersion);
pipedalUI__ui = lilv_new_uri(pWorld, PIPEDAL_UI__ui);
pipedalUI__fileProperties = lilv_new_uri(pWorld, PIPEDAL_UI__fileProperties);
@@ -150,7 +153,7 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld)
pipedalUI__mimeType = lilv_new_uri(pWorld, PIPEDAL_UI__mimeType);
pipedalUI__outputPorts = lilv_new_uri(pWorld, PIPEDAL_UI__outputPorts);
pipedalUI__text = lilv_new_uri(pWorld, PIPEDAL_UI__text);
pipedalUI__ledColor = lilv_new_uri(pWorld,PIPEDAL_UI__ledColor);
pipedalUI__ledColor = lilv_new_uri(pWorld, PIPEDAL_UI__ledColor);
pipedalUI__frequencyPlot = lilv_new_uri(pWorld, PIPEDAL_UI__frequencyPlot);
pipedalUI__xLeft = lilv_new_uri(pWorld, PIPEDAL_UI__xLeft);
@@ -159,7 +162,7 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld)
pipedalUI__yBottom = lilv_new_uri(pWorld, PIPEDAL_UI__yBottom);
pipedalUI__xLog = lilv_new_uri(pWorld, PIPEDAL_UI__xLog);
pipedalUI__width = lilv_new_uri(pWorld, PIPEDAL_UI__width);
pipedalUI__graphicEq = lilv_new_uri(pWorld,PIPEDAL_UI__graphicEq);
pipedalUI__graphicEq = lilv_new_uri(pWorld, PIPEDAL_UI__graphicEq);
ui__portNotification = lilv_new_uri(pWorld, LV2_UI__portNotification);
ui__plugin = lilv_new_uri(pWorld, LV2_UI__plugin);
@@ -171,6 +174,7 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld)
lv2__port = lilv_new_uri(pWorld, LV2_CORE__port);
#define MOD_PREFIX "http://moddevices.com/ns/mod#"
mod__label = lilv_new_uri(pWorld, MOD_PREFIX "label");
mod__brand = lilv_new_uri(pWorld, MOD_PREFIX "brand");
mod__preferMomentaryOffByDefault = lilv_new_uri(pWorld, MOD_PREFIX "preferMomentaryOffByDefault");
@@ -194,16 +198,39 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld)
patch__writable = lilv_new_uri(pWorld, LV2_PATCH__writable);
patch__readable = lilv_new_uri(pWorld, LV2_PATCH__readable);
pipedal_patch__readable = lilv_new_uri(pWorld, PIPEDAL_PATCH__readable);
pipedal_patch__readable = lilv_new_uri(pWorld, PIPEDAL_PATCH__readable);
dc__format = lilv_new_uri(pWorld, "http://purl.org/dc/terms/format");
mod__fileTypes = lilv_new_uri(pWorld, "http://moddevices.com/ns/mod#fileTypes");
mod__supportedExtensions = lilv_new_uri(pWorld, "http://moddevices.com/ns/mod#supportedExtensions");
}
void PluginHost::LilvUris::Free()
{
}
static int32_t nodesAsInt(const LilvNodes *nodes)
{
if (!nodes)
return 0;
LILV_FOREACH(nodes, iNode, nodes)
{
const LilvNode *node = lilv_nodes_get(nodes, iNode);
if (!node)
return 0;
if (lilv_node_is_int(node))
{
return lilv_node_as_int(node);
}
if (lilv_node_is_float(node))
{
return static_cast<int32_t>(lilv_node_as_float(node));
}
break;
}
return 0;
}
static std::string nodeAsString(const LilvNode *node)
{
@@ -270,7 +297,6 @@ PluginHost::PluginHost()
fileMetadataFeature.Prepare(mapFeature);
lv2Features.push_back(fileMetadataFeature.GetFeature());
lv2Features.push_back(nullptr);
this->urids = new Urids(mapFeature);
@@ -294,8 +320,11 @@ PluginHost::~PluginHost()
{
delete lilvUris;
lilvUris = nullptr;
delete urids;
urids = nullptr;
delete mod_gui_uris;
mod_gui_uris = nullptr;
free_world();
}
@@ -397,7 +426,7 @@ void PluginHost::LoadPluginClassesFromLilv()
}
}
void PluginHost::Load(const char *lv2Path)
void PluginHost::LoadLilv(const char *lv2Path)
{
this->plugins_.clear();
@@ -418,6 +447,9 @@ void PluginHost::Load(const char *lv2Path)
lilv_world_load_all(pWorld);
}
// Not a safe pointer,because auto-deleting after freeWorld() would be death.
this->mod_gui_uris = new ModGuiUris(pWorld, mapFeature);
// LilvNode*lv2_path = lilv_new_file_uri(pWorld,NULL,lv2Path);
// lilv_world_set_option(world,LILV_OPTION_LV2_PATH,lv)
@@ -439,7 +471,8 @@ void PluginHost::Load(const char *lv2Path)
if (pluginInfo->hasCvPorts())
{
Lv2Log::debug("Plugin %s (%s) skipped. (Has CV ports).", pluginInfo->name().c_str(), pluginInfo->uri().c_str());
} else if (pluginInfo->hasUnsupportedPatchProperties())
}
else if (pluginInfo->hasUnsupportedPatchProperties())
{
Lv2Log::debug("Plugin %s (%s) skipped. (Has unsupported patch parameters).", pluginInfo->name().c_str(), pluginInfo->uri().c_str());
}
@@ -497,20 +530,20 @@ void PluginHost::Load(const char *lv2Path)
if (plugin->is_valid())
{
#if 1
// no plugins with more than 2 inputs or outputs.
// no zero-input or zero-output plugins (temporarily disables midi plugins)
// no zero output devices (permanent, I think)
if (info.audio_inputs() > 2 || info.audio_outputs() > 2) {
// no plugins with more than 2 inputs or outputs.
// no zero-input or zero-output plugins (temporarily disables midi plugins)
// no zero output devices (permanent, I think)
if (info.audio_inputs() > 2 || info.audio_outputs() > 2)
{
Lv2Log::debug(
"Plugin %s (%s) skipped. %d inputs, %d outputs.", plugin->name().c_str(), plugin->uri().c_str(),
(int)info.audio_inputs(),(int)info.audio_outputs());
(int)info.audio_inputs(), (int)info.audio_outputs());
}
else if (info.audio_inputs() == 0 && info.audio_outputs() == 0 )
else if (info.audio_inputs() == 0 && info.audio_outputs() == 0)
{
Lv2Log::debug("Plugin %s (%s) skipped. No audio i/o.", plugin->name().c_str(), plugin->uri().c_str());
} else if (info.audio_inputs() == 0)
}
else if (info.audio_inputs() == 0)
{
// temporarily disable this feature.
Lv2Log::debug("Plugin %s (%s) skipped. No inputs.", plugin->name().c_str(), plugin->uri().c_str());
@@ -536,7 +569,8 @@ void PluginHost::Load(const char *lv2Path)
#endif
else
{
if (info.audio_inputs() == 0) {
if (info.audio_inputs() == 0)
{
Lv2Log::debug("************* ZERO INPUTS: %s (%s) skipped. No audio outputs.", plugin->name().c_str(), plugin->uri().c_str());
}
ui_plugins_.push_back(std::move(info));
@@ -619,7 +653,7 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
// example:
// <http://github.com/mikeoliphant/neural-amp-modeler-lv2#model>
// a lv2:Parameter;
// a lv2:Parameter;
// rdfs:label "Model";
// rdfs:range atom:Path.
// ...
@@ -644,7 +678,6 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
if (lilv_world_ask(pWorld, propertyUri, lv2Host->lilvUris->rdfs__range, lv2Host->lilvUris->atom__Path))
{
AutoLilvNode label = lilv_world_get(pWorld, propertyUri, lv2Host->lilvUris->rdfs__label, nullptr);
std::string strLabel = label.AsString();
@@ -662,19 +695,16 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
std::make_shared<UiFileProperty>(
strLabel, propertyUri.AsUri(), lv2DirectoryName);
AutoLilvNode indexNode = lilv_world_get(pWorld, propertyUri, lv2Host->lilvUris->lv2core__index, nullptr);
int32_t index = indexNode.AsInt(-1);
fileProperty->index(index);
// if there's a pipedalui_fileTypes node, use that instead.
AutoLilvNode mod__fileTypes = lilv_world_get(pWorld, propertyUri, lv2Host->lilvUris->mod__fileTypes, nullptr);
// default: everything.
std::string fileTypes = ModFileTypes::DEFAULT_FILE_TYPES;
std::string fileTypes = ModFileTypes::DEFAULT_FILE_TYPES;
if (mod__fileTypes)
{
// "nam,nammodel"
@@ -682,8 +712,6 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
}
ModFileTypes modFileTypes(fileTypes);
// Legacy case: audio_uploads/<plugin_directory> exists.
std::filesystem::path bundleDirectoryName = std::filesystem::path(bundle_path()).parent_path().filename();
@@ -693,13 +721,14 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
fileProperty->directory(bundleDirectoryName);
if (std::filesystem::exists(legacyUploadPath)
&& !std::filesystem::exists(legacyUploadPath / ".migrated"))
if (std::filesystem::exists(legacyUploadPath) && !std::filesystem::exists(legacyUploadPath / ".migrated"))
{
fileProperty->useLegacyModDirectory(true);
fileProperty->directory(bundleDirectoryName);
modFileTypes.rootDirectories().push_back(bundleDirectoryName.filename()); // push the private directory!
} else {
}
else
{
if (modFileTypes.rootDirectories().size() == 1)
{
std::string modName = modFileTypes.rootDirectories()[0];
@@ -708,9 +737,10 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
}
}
fileProperties.push_back(fileProperty);
} else {
}
else
{
unsupportedPatchProperty = true;
}
}
@@ -718,24 +748,25 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
}
FindWritablePathPropertiesResult result;
if (fileProperties.size() != 0)
{
std::sort(fileProperties.begin(),fileProperties.end(),[](const UiFileProperty::ptr& left,const UiFileProperty::ptr&right) {
// properies with indexes first.
int32_t indexL = left->index();
if (indexL < 0) indexL = std::numeric_limits<int32_t>::max();
int32_t indexR = right->index();
if (indexR < 0) indexR = std::numeric_limits<int32_t>::max();
if (indexL < indexR) return true;
if (indexL > indexR) return false;
std::sort(fileProperties.begin(), fileProperties.end(), [](const UiFileProperty::ptr &left, const UiFileProperty::ptr &right)
{
// properies with indexes first.
int32_t indexL = left->index();
if (indexL < 0)
indexL = std::numeric_limits<int32_t>::max();
int32_t indexR = right->index();
if (indexR < 0)
indexR = std::numeric_limits<int32_t>::max();
if (indexL < indexR)
return true;
if (indexL > indexR)
return false;
// there is no natural order. TTL indexing means that the order we see them in is random.
// We can't order them sensibly. Let's at least order them consistently.
return left->label() < right->label();
});
// there is no natural order. TTL indexing means that the order we see them in is random.
// We can't order them sensibly. Let's at least order them consistently.
return left->label() < right->label(); });
result.pipedalUi = std::make_shared<PiPedalUI>(std::move(fileProperties));
}
result.hasUnsupportedPatchProperties = unsupportedPatchProperty;
@@ -764,6 +795,12 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP
AutoLilvNode name = (lilv_plugin_get_name(pPlugin));
this->name_ = nodeAsString(name);
AutoLilvNodes minorVersion = lilv_plugin_get_value(pPlugin, lv2Host->lilvUris->lv2core__minorVersion);
this->minorVersion_ = nodesAsInt(minorVersion);
AutoLilvNodes microVersion = lilv_plugin_get_value(pPlugin, lv2Host->lilvUris->lv2core__microVersion);
this->microVersion_ = nodesAsInt(microVersion);
AutoLilvNode brand = lilv_world_get(pWorld, plugUri, lv2Host->lilvUris->mod__brand, nullptr);
this->brand_ = nodeAsString(brand);
@@ -838,6 +875,59 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP
}
std::sort(ports_.begin(), ports_.end(), ports_sort_compare);
// Fetch patch properties.
{
AutoLilvNodes patchWritables = lilv_world_find_nodes(pWorld, plugUri, lv2Host->lilvUris->patch__writable, nullptr);
bool unsupportedPatchProperty = false;
LILV_FOREACH(nodes, iNode, patchWritables)
{
AutoLilvNode propertyUri = lilv_nodes_get(patchWritables, iNode);
if (propertyUri)
{
// a lv2:Parameter?
if (lilv_world_ask(pWorld, propertyUri, lv2Host->lilvUris->isA, lv2Host->lilvUris->lv2core__Parameter))
{
Lv2PatchPropertyInfo propertyInfo{lv2Host, propertyUri};
propertyInfo.writable(true);
patchProperties_.push_back(std::move(propertyInfo));
}
}
}
}
{
AutoLilvNodes patchReadables = lilv_world_find_nodes(pWorld, plugUri, lv2Host->lilvUris->patch__readable, nullptr);
bool unsupportedPatchProperty = false;
LILV_FOREACH(nodes, iNode, patchReadables)
{
AutoLilvNode propertyUri = lilv_nodes_get(patchReadables, iNode);
if (propertyUri)
{
std::string uri = propertyUri.AsUri();
bool found = false;
for (auto &property : patchProperties_)
{
if (property.uri() == uri)
{
property.readable(true);
found = true;
break;
}
}
if (!found)
{
if (lilv_world_ask(pWorld, propertyUri, lv2Host->lilvUris->isA, lv2Host->lilvUris->lv2core__Parameter))
{
Lv2PatchPropertyInfo propertyInfo{lv2Host, propertyUri};
propertyInfo.readable(true);
patchProperties_.push_back(std::move(propertyInfo));
}
}
}
}
}
// Fetch pipedal plugin UI
@@ -886,7 +976,7 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP
++nOutputs;
}
}
this->modGui_ = ModGui::Create(lv2Host, pPlugin);
this->is_valid_ = isValid;
}
@@ -1012,22 +1102,21 @@ Lv2PortInfo::Lv2PortInfo(PluginHost *host, const LilvPlugin *plugin, const LilvP
this->integer_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->integer_property_uri);
this->mod_momentaryOffByDefault_ = lilv_port_has_property(plugin, pPort, host->lilvUris->mod__preferMomentaryOffByDefault);
this->mod_momentaryOnByDefault_ = lilv_port_has_property(plugin, pPort, host->lilvUris->mod__preferMomentaryOnByDefault);
this->pipedal_graphicEq_ = lilv_port_has_property(plugin, pPort, host->lilvUris->pipedalUI__graphicEq);
this->pipedal_graphicEq_ = lilv_port_has_property(plugin, pPort, host->lilvUris->pipedalUI__graphicEq);
this->enumeration_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->enumeration_property_uri);
this->toggled_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->core__toggled);
this->not_on_gui_ = lilv_port_has_property(plugin, pPort, host->lilvUris->portprops__not_on_gui_property_uri);
this->connection_optional_ = lilv_port_has_property(plugin, pPort, host->lilvUris->core__connectionOptional);
this->trigger_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->portprops__trigger);
AutoLilvNode port_ledColor = lilv_port_get(plugin,pPort,host->lilvUris->pipedalUI__ledColor);
AutoLilvNode port_ledColor = lilv_port_get(plugin, pPort, host->lilvUris->pipedalUI__ledColor);
if (port_ledColor)
{
auto value = lilv_node_as_string(port_ledColor);
if (value) {
if (value)
{
this->pipedal_ledColor_ = value;
}
}
@@ -1164,6 +1253,8 @@ bool PluginHost::is_a(const std::string &class_, const std::string &target_class
Lv2PluginUiInfo::Lv2PluginUiInfo(PluginHost *pHost, const Lv2PluginInfo *plugin)
: uri_(plugin->uri()),
name_(plugin->name()),
minorVersion_(plugin->minorVersion()),
microVersion_(plugin->microVersion()),
brand_(plugin->brand()),
label_(plugin->label()),
author_name_(plugin->author_name()),
@@ -1173,7 +1264,9 @@ Lv2PluginUiInfo::Lv2PluginUiInfo(PluginHost *pHost, const Lv2PluginInfo *plugin)
audio_outputs_(0),
description_(plugin->comment()),
has_midi_input_(false),
has_midi_output_(false)
has_midi_output_(false),
modGui_(plugin->modGui()),
patchProperties_(plugin->patchProperties())
{
PLUGIN_MAP_CHECK();
auto pluginClass = pHost->GetPluginClass(plugin->plugin_class());
@@ -1253,7 +1346,7 @@ std::shared_ptr<Lv2PluginInfo> PluginHost::GetPluginInfo(const std::string &uri)
return ff->second;
}
Lv2Pedalboard *PluginHost::UpdateLv2PedalboardStructure(Pedalboard &pedalboard,Lv2Pedalboard *existingPedalboard,Lv2PedalboardErrorList &errorList)
Lv2Pedalboard *PluginHost::UpdateLv2PedalboardStructure(Pedalboard &pedalboard, Lv2Pedalboard *existingPedalboard, Lv2PedalboardErrorList &errorList)
{
ExistingEffectMap existingEffects;
@@ -1261,7 +1354,8 @@ Lv2Pedalboard *PluginHost::UpdateLv2PedalboardStructure(Pedalboard &pedalboard,L
{
for (auto &effect : existingPedalboard->GetSharedEffectList())
{
if (effect->IsLv2Effect()) {
if (effect->IsLv2Effect())
{
existingEffects[effect->GetInstanceId()] = effect;
}
}
@@ -1269,7 +1363,7 @@ Lv2Pedalboard *PluginHost::UpdateLv2PedalboardStructure(Pedalboard &pedalboard,L
Lv2Pedalboard *pPedalboard = new Lv2Pedalboard();
try
{
pPedalboard->Prepare(this, pedalboard, errorList,&existingEffects);
pPedalboard->Prepare(this, pedalboard, errorList, &existingEffects);
return pPedalboard;
}
catch (const std::exception &e)
@@ -1589,17 +1683,16 @@ static void createTargetLinks(const std::filesystem::path &sourceDirectory, cons
}
}
std::string PluginHost::MapResourcePath(const std::string&pluginUri, const std::string&filePath)
std::string PluginHost::MapResourcePath(const std::string &pluginUri, const std::string &filePath)
{
auto plugin = GetPluginInfo(pluginUri);
if (plugin) {
if (plugin)
{
return filePath;
}
return filePath;
}
void PluginHost::CheckForResourceInitialization(const std::string &pluginUri, const std::filesystem::path &pluginUploadDirectory)
@@ -1679,6 +1772,43 @@ std::string PluginHost::AbstractPath(const std::string &path)
}
return path;
}
Lv2PatchPropertyInfo::Lv2PatchPropertyInfo(PluginHost *pluginHost, const LilvNode *propertyUri)
{
LilvWorld *pWorld = pluginHost->getWorld();
this->uri_ = nodeAsString(propertyUri);
AutoLilvNode range = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->rdfs__range, nullptr);
if (range)
{
this->type_ = range.AsUri();
}
AutoLilvNode comment = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->rdfs__Comment, nullptr);
if (comment)
{
this->comment_ = comment.AsString();
}
AutoLilvNode shortName = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->lv2core__shortName, nullptr);
if (shortName)
{
this->shortName_ = shortName.AsString();
} else {
this->shortName_ = this->label_;
}
AutoLilvNode indexNode = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->lv2core__index, nullptr);
this->index_ = indexNode.AsInt(-1);
AutoLilvNode modFileTypesNode = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->mod__fileTypes, nullptr);
if (modFileTypesNode) {
std::string strFileTypes = modFileTypesNode.AsString();
this->fileTypes_ = split(strFileTypes,',');
}
AutoLilvNode modSupportedExtensionsNode = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->mod__supportedExtensions, nullptr);
if (modSupportedExtensionsNode) {
std::string strSupportedExtensions = modSupportedExtensionsNode.AsString();
this->supportedExtensions_ = split(strSupportedExtensions,',');
}
}
// void PiPedalHostLogError(const std::string &error)
// {
@@ -1757,6 +1887,8 @@ json_map::storage_type<Lv2PortGroup> Lv2PortGroup::jmap{{
json_map::storage_type<Lv2PluginInfo> Lv2PluginInfo::jmap{{
json_map::reference("bundle_path", &Lv2PluginInfo::bundle_path_),
json_map::reference("uri", &Lv2PluginInfo::uri_),
json_map::reference("minorVersion", &Lv2PluginInfo::minorVersion_),
json_map::reference("microVersion", &Lv2PluginInfo::microVersion_),
json_map::reference("name", &Lv2PluginInfo::name_),
json_map::reference("brand", &Lv2PluginInfo::brand_),
json_map::reference("label", &Lv2PluginInfo::label_),
@@ -1774,6 +1906,8 @@ json_map::storage_type<Lv2PluginInfo> Lv2PluginInfo::jmap{{
json_map::reference("port_groups", &Lv2PluginInfo::port_groups_),
json_map::reference("is_valid", &Lv2PluginInfo::is_valid_),
json_map::reference("has_factory_presets", &Lv2PluginInfo::has_factory_presets_),
json_map::reference("modGui", &Lv2PluginInfo::modGui_),
json_map::reference("patchProperties", &Lv2PluginInfo::patchProperties_),
}};
json_map::storage_type<Lv2PluginClass> Lv2PluginClass::jmap{{
@@ -1831,6 +1965,8 @@ json_map::storage_type<Lv2PluginUiInfo>
{
json_map::reference("uri", &Lv2PluginUiInfo::uri_),
json_map::reference("name", &Lv2PluginUiInfo::name_),
json_map::reference("minorVersion", &Lv2PluginUiInfo::minorVersion_),
json_map::reference("microVersion", &Lv2PluginUiInfo::microVersion_),
json_map::reference("brand", &Lv2PluginUiInfo::brand_),
json_map::reference("label", &Lv2PluginUiInfo::label_),
json_map::enum_reference("plugin_type", &Lv2PluginUiInfo::plugin_type_, get_plugin_type_enum_converter()),
@@ -1848,4 +1984,16 @@ json_map::storage_type<Lv2PluginUiInfo>
json_map::reference("fileProperties", &Lv2PluginUiInfo::fileProperties_),
json_map::reference("frequencyPlots", &Lv2PluginUiInfo::frequencyPlots_),
json_map::reference("uiPortNotifications", &Lv2PluginUiInfo::uiPortNotifications_),
json_map::reference("modGui", &Lv2PluginUiInfo::modGui_),
json_map::reference("patchProperties", &Lv2PluginUiInfo::patchProperties_),
}};
JSON_MAP_BEGIN(Lv2PatchPropertyInfo)
JSON_MAP_REFERENCE(Lv2PatchPropertyInfo, uri)
JSON_MAP_REFERENCE(Lv2PatchPropertyInfo, label)
JSON_MAP_REFERENCE(Lv2PatchPropertyInfo, type)
JSON_MAP_REFERENCE(Lv2PatchPropertyInfo, comment)
JSON_MAP_REFERENCE(Lv2PatchPropertyInfo, shortName)
JSON_MAP_REFERENCE(Lv2PatchPropertyInfo, fileTypes)
JSON_MAP_REFERENCE(Lv2PatchPropertyInfo, supportedExtensions)
JSON_MAP_END()
+67 -3
View File
@@ -32,6 +32,7 @@
#include <string>
#include "IHost.hpp"
#include <set>
#include "ModGui.hpp"
//#include "lv2.h"
#include "Units.hpp"
@@ -42,6 +43,7 @@
#include "AutoLilvNode.hpp"
#include "PiPedalUI.hpp"
#include "MapPathFeature.hpp"
#include "ModGui.hpp"
namespace pipedal
{
@@ -325,6 +327,38 @@ namespace pipedal
static json_map::storage_type<Lv2PortInfo> jmap;
};
class Lv2PatchPropertyInfo {
private:
std::string uri_;
bool writable_ = false;
bool readable_ = false;
std::string label_;
uint32_t index_ = -1;
std::string type_;
std::string comment_;
std::string shortName_;
std::vector<std::string> fileTypes_;
std::vector<std::string> supportedExtensions_;
public:
Lv2PatchPropertyInfo() {}
Lv2PatchPropertyInfo(PluginHost *pluginHost, const LilvNode *propertyUri);
LV2_PROPERTY_GETSET(uri);
LV2_PROPERTY_GETSET(writable);
LV2_PROPERTY_GETSET(readable);
LV2_PROPERTY_GETSET(label);
LV2_PROPERTY_GETSET_SCALAR(index);
LV2_PROPERTY_GETSET(type);
LV2_PROPERTY_GETSET(comment);
LV2_PROPERTY_GETSET(shortName);
LV2_PROPERTY_GETSET(fileTypes);
LV2_PROPERTY_GETSET(supportedExtensions);
DECLARE_JSON_MAP(Lv2PatchPropertyInfo);
};
class Lv2PortGroup
{
private:
@@ -364,6 +398,8 @@ namespace pipedal
bool HasFactoryPresets(PluginHost *lv2Host, const LilvPlugin *plugin);
std::string bundle_path_;
std::string uri_;
uint32_t minorVersion_ = 0;
uint32_t microVersion_ = 0;
std::string name_;
std::string plugin_class_;
std::string brand_;
@@ -380,8 +416,12 @@ namespace pipedal
std::string comment_;
std::vector<std::shared_ptr<Lv2PortInfo>> ports_;
std::vector<std::shared_ptr<Lv2PortGroup>> port_groups_;
std::vector<Lv2PatchPropertyInfo> patchProperties_;
bool is_valid_ = false;
PiPedalUI::ptr piPedalUI_;
ModGui::ptr modGui_;
bool hasUnsupportedPatchProperties_ = false;
bool IsSupportedFeature(const std::string &feature) const;
@@ -390,6 +430,8 @@ namespace pipedal
LV2_PROPERTY_GETSET(bundle_path)
LV2_PROPERTY_GETSET(uri)
LV2_PROPERTY_GETSET(name)
LV2_PROPERTY_GETSET(minorVersion)
LV2_PROPERTY_GETSET(microVersion)
LV2_PROPERTY_GETSET(brand)
LV2_PROPERTY_GETSET(label)
LV2_PROPERTY_GETSET(plugin_class)
@@ -406,6 +448,8 @@ namespace pipedal
LV2_PROPERTY_GETSET(has_factory_presets)
LV2_PROPERTY_GETSET(piPedalUI)
LV2_PROPERTY_GETSET(hasUnsupportedPatchProperties)
LV2_PROPERTY_GETSET(modGui)
LV2_PROPERTY_GETSET(patchProperties)
const Lv2PortInfo &getPort(const std::string &symbol)
{
@@ -618,6 +662,8 @@ namespace pipedal
private:
std::string uri_;
std::string name_;
uint32_t minorVersion_ = 0;
uint32_t microVersion_ = 0;;
std::string brand_;
std::string label_;
std::string author_name_;
@@ -636,10 +682,15 @@ namespace pipedal
std::vector<UiFileProperty::ptr> fileProperties_;
std::vector<UiFrequencyPlot::ptr> frequencyPlots_;
std::vector<UiPortNotification::ptr> uiPortNotifications_;
ModGui::ptr modGui_;
std::vector<Lv2PatchPropertyInfo> patchProperties_;
public:
LV2_PROPERTY_GETSET(uri)
LV2_PROPERTY_GETSET(name)
LV2_PROPERTY_GETSET(minorVersion)
LV2_PROPERTY_GETSET(microVersion)
LV2_PROPERTY_GETSET(brand)
LV2_PROPERTY_GETSET(label)
LV2_PROPERTY_GETSET(author_name)
@@ -657,6 +708,8 @@ namespace pipedal
LV2_PROPERTY_GETSET(fileProperties)
LV2_PROPERTY_GETSET(frequencyPlots)
LV2_PROPERTY_GETSET(uiPortNotifications)
LV2_PROPERTY_GETSET(modGui)
LV2_PROPERTY_GETSET(patchProperties)
static json_map::storage_type<Lv2PluginUiInfo> jmap;
};
@@ -679,6 +732,8 @@ namespace pipedal
#endif
friend class pipedal::AutoLilvNode;
friend class pipedal::PiPedalUI;
friend class PluginHostTest;
static const char *RDFS__comment;
static const char *RDFS__range;
@@ -707,6 +762,7 @@ namespace pipedal
AutoLilvNode invada_units__unit; // typo in invada plugins.
AutoLilvNode invada_portprops__logarithmic; // typo in invada plugins.
AutoLilvNode atom__bufferType;
AutoLilvNode atom__Path;
AutoLilvNode presets__preset;
@@ -714,8 +770,11 @@ namespace pipedal
AutoLilvNode rdfs__label;
AutoLilvNode lv2core__symbol;
AutoLilvNode lv2core__name;
AutoLilvNode lv2core__shortName;
AutoLilvNode lv2core__index;
AutoLilvNode lv2core__Parameter;
AutoLilvNode lv2core__minorVersion;
AutoLilvNode lv2core__microVersion;
AutoLilvNode pipedalUI__ui;
AutoLilvNode pipedalUI__fileProperties;
AutoLilvNode pipedalUI__directory;
@@ -766,6 +825,7 @@ namespace pipedal
AutoLilvNode patch__readable;
AutoLilvNode pipedal_patch__readable;
AutoLilvNode mod__brand;
AutoLilvNode mod__label;
AutoLilvNode mod__preferMomentaryOffByDefault;
@@ -773,6 +833,7 @@ namespace pipedal
AutoLilvNode dc__format;
AutoLilvNode mod__fileTypes;
AutoLilvNode mod__supportedExtensions;
AutoLilvNode pipedalui__fileTypes;
@@ -817,6 +878,7 @@ namespace pipedal
friend class Lv2PluginInfo;
friend class Lv2PortInfo;
friend class Lv2PortGroup;
friend class ModGui;
std::shared_ptr<Lv2PluginClass> GetPluginClass(const LilvPluginClass *pClass);
std::shared_ptr<Lv2PluginClass> MakePluginClass(const LilvPluginClass *pClass);
@@ -924,7 +986,9 @@ namespace pipedal
class Urids;
Urids *urids;
Urids *urids = nullptr;
ModGuiUris* mod_gui_uris = nullptr;
void OnConfigurationChanged(const JackConfiguration &configuration, const JackChannelSelection &settings);
@@ -933,7 +997,7 @@ namespace pipedal
std::shared_ptr<Lv2PluginClass> GetLv2PluginClass() const;
std::vector<std::shared_ptr<Lv2PluginInfo>> GetPlugins() const { return plugins_; }
const std::vector<std::shared_ptr<Lv2PluginInfo>>& GetPlugins() const { return plugins_; }
const std::vector<Lv2PluginUiInfo> &GetUiPlugins() const { return ui_plugins_; }
virtual std::shared_ptr<Lv2PluginInfo> GetPluginInfo(const std::string &uri) const;
@@ -941,7 +1005,7 @@ namespace pipedal
static constexpr const char *DEFAULT_LV2_PATH = "/usr/lib/lv2";
void LoadPluginClassesFromJson(std::filesystem::path jsonFile);
void Load(const char *lv2Path = PluginHost::DEFAULT_LV2_PATH);
void LoadLilv(const char *lv2Path = PluginHost::DEFAULT_LV2_PATH);
virtual LV2_URID GetLv2Urid(const char *uri)
{
+1
View File
@@ -68,3 +68,4 @@ json_enum_converter<Units> *get_units_enum_converter();
} // namespace.
+9
View File
@@ -37,6 +37,7 @@
#include "Ipv6Helpers.hpp"
#include "util.hpp"
#include "ofstream_synced.hpp"
#include "ModTemplateGenerator.hpp"
#include <mutex>
#include "WebServer.hpp"
@@ -741,6 +742,14 @@ namespace pipedal
request.set_body_file(bodyFile->Path(),bodyFile->DeleteFile());
bodyFile->Detach();
}
virtual void clearBody() override {
// clear the body, but leave the file size intact (e.g for a HEAD request).
request.set_body("");
// STUB: Don't know how to clear the body file!!
}
virtual void setBodyFile(std::filesystem::path&path, bool deleteWhenDone) override {
// cast away const to do what ther request would do if it had that method.
request.set_body_file(path,deleteWhenDone);
+1
View File
@@ -43,6 +43,7 @@ public:
virtual void setBody(const std::string&body) = 0;
virtual void setBodyFile(std::shared_ptr<TemporaryFile>&temporaryFile) = 0;
virtual void setBodyFile(std::filesystem::path&path, bool deleteWhenDone) = 0;
virtual void clearBody() = 0; // but leave the file size intact (e.g for a HEAD request).
virtual void keepAlive(bool value) = 0;
};
+5 -1
View File
@@ -39,6 +39,7 @@
#include "AudioFiles.hpp"
#include "util.hpp"
#include "HtmlHelper.hpp"
#include "WebServerMod.hpp"
#define OLD_PRESET_EXTENSION ".piPreset"
#define PRESET_EXTENSION ".piPreset"
@@ -249,7 +250,7 @@ public:
void GetPluginPresets(const uri &request_uri, std::string *pName, std::string *pContent)
{
std::string pluginUri = request_uri.query("id");
auto plugin = model->GetLv2Host().GetPluginInfo(pluginUri);
auto plugin = model->GetPluginHost().GetPluginInfo(pluginUri);
*pName = plugin->name();
PluginPresets pluginPresets = model->GetPluginPresets(pluginUri);
@@ -1102,4 +1103,7 @@ void pipedal::ConfigureWebServer(
std::shared_ptr<DownloadIntercept> downloadIntercept = std::make_shared<DownloadIntercept>(&model);
server.AddRequestHandler(downloadIntercept);
std::shared_ptr<ModWebIntercept> modWebIntercept = ModWebIntercept::Create(&model);
server.AddRequestHandler(modWebIntercept);
}
+308
View File
@@ -0,0 +1,308 @@
/*
* Copyright (c) 2025 Robin E. R. Davies
* All rights reserved.
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include "WebServerMod.hpp"
#include <filesystem>
#include "json_variant.hpp"
#include "ss.hpp"
#include "ModTemplateGenerator.hpp"
#include "HtmlHelper.hpp"
#include "MimeTypes.hpp"
using namespace pipedal;
namespace fs = std::filesystem;
namespace pipedal::impl
{
class ModWebInterceptImpl : public ModWebIntercept
{
private:
using super = ModWebIntercept;
using self = ModWebInterceptImpl;
PiPedalModel *model;
public:
ModWebInterceptImpl(PiPedalModel *model)
: super("/resources"),
model(model)
{
}
virtual bool wants(const std::string &method, const uri &request_uri) const override;
virtual void head_response(
const uri &request_uri,
HttpRequest &req,
HttpResponse &res,
std::error_code &ec) override;
virtual void get_response(
const uri &request_uri,
HttpRequest &req,
HttpResponse &res,
std::error_code &ec) override;
private:
std::string GenerateTemplate(
const fs::path &templateFile,
std::shared_ptr<Lv2PluginInfo> pluginInfo);
};
}
using namespace pipedal::impl;
ModWebIntercept::ptr ModWebIntercept::Create(PiPedalModel *model)
{
return std::make_shared<impl::ModWebInterceptImpl>(model);
}
bool ModWebInterceptImpl::wants(const std::string &method, const uri &request_uri) const
{
if (request_uri.segment_count() < 2 || request_uri.segment(0) != "resources")
{
return false;
}
return true;
}
void ModWebInterceptImpl::head_response(
const uri &request_uri,
HttpRequest &req,
HttpResponse &res,
std::error_code &ec)
{
get_response(request_uri, req, res, ec);
res.clearBody();
}
static void setCacheControl(HttpResponse &res, const fs::path &path)
{
if (fs::exists(path))
{
res.set("Cache-Control", "public, max-age=31536000"); // 1 month
auto lastModified = std::filesystem::last_write_time(path);
res.set(HttpField::LastModified, HtmlHelper::timeToHttpDate(lastModified));
}
else
{
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
}
}
void ModWebInterceptImpl::get_response(
const uri &request_uri,
HttpRequest &req,
HttpResponse &res,
std::error_code &ec)
{
try
{
std::string ns = request_uri.query("ns");
std::string strVersion = request_uri.query("v");
(void)strVersion; // this is just cache-busting, so we don't actually use it.
std::shared_ptr<Lv2PluginInfo> pluginInfo;
if (!ns.empty())
{
pluginInfo = model->GetPluginInfo(ns);
if (!pluginInfo)
{
throw std::runtime_error("Plugin not found.");
}
}
if (!pluginInfo->modGui())
{
throw std::runtime_error("Plugin does not have a ModGui.");
}
if (request_uri.segment_count() < 2)
{
throw std::runtime_error("Invalid request URI.");
}
if (request_uri.segment(0) != "resources")
{
throw std::runtime_error("Invalid request URI: expected 'resources'.");
}
std::string segment = request_uri.segment(1);
if (segment == "_")
{
segment = request_uri.segment(2);
if (segment == "iconTemplate")
{
res.set("Content-Type", "text/html");
res.setBody(GenerateTemplate(
pluginInfo->modGui()->iconTemplate(),
pluginInfo));
setCacheControl(res, pluginInfo->modGui()->iconTemplate());
}
else if (segment == "stylesheet")
{
res.set("Content-Type", "text/css");
res.setBody(GenerateTemplate(
pluginInfo->modGui()->stylesheet(),
pluginInfo));
setCacheControl(res, pluginInfo->modGui()->stylesheet());
}
else if (segment == "screenshot")
{
std::filesystem::path path = pluginInfo->modGui()->screenshot();
if (!fs::exists(path))
{
ec = std::make_error_code(std::errc::no_such_file_or_directory);
return;
}
if (!fs::is_regular_file(path))
{
throw std::runtime_error("Screenshot is not a regular file: " + path.string());
}
size_t size = fs::file_size(path);
std::string mimeType = MimeTypes::instance().MimeTypeFromExtension(path.extension().string());
if (mimeType.empty())
{
throw std::runtime_error("Unknown file type for screenshot: " + path.string());
}
res.setBodyFile(
path,
false); // delete when done
res.set("Content-Type", mimeType);
res.setContentLength(size);
setCacheControl(res, path);
}
else if (segment == "thumbnail")
{
std::filesystem::path path = pluginInfo->modGui()->thumbnail();
if (!fs::exists(path))
{
throw std::runtime_error("Thumbnail not found: " + path.string());
}
if (!fs::is_regular_file(path))
{
throw std::runtime_error("Thumbnail is not a regular file: " + path.string());
}
size_t size = fs::file_size(path);
std::string mimeType = MimeTypes::instance().MimeTypeFromExtension(path.extension().string());
if (mimeType.empty())
{
throw std::runtime_error("Unknown file type for thumbnail: " + path.string());
}
res.setBodyFile(
path,
false); // delete when done
res.set("Content-Type", mimeType);
setCacheControl(res, path);
res.setContentLength(size);
} else {
throw std::runtime_error("Unknown resource: _/" + segment);
}
}
else
{
// a request for a plugin resource file.
fs::path resourcefile = pluginInfo->modGui()->resourceDirectory();
for (size_t i = 1; i < request_uri.segment_count(); ++i)
{
resourcefile /= request_uri.segment(i);
}
if (!fs::exists(resourcefile) && !fs::is_regular_file(resourcefile))
{
ec = std::make_error_code(std::errc::no_such_file_or_directory);
return;
}
size_t size = fs::file_size(resourcefile);
std::string mimeType = MimeTypes::instance().MimeTypeFromExtension(resourcefile.extension().string());
if (mimeType.empty())
{
throw std::runtime_error("Unknown file type for resource: " + resourcefile.string());
}
res.setBodyFile(
resourcefile,
false); // delete when done
res.set("Content-Type", mimeType);
Lv2Log::debug(SS("ModWebIntercept: Serving resource file: " << resourcefile.string() << " (" << size << " bytes)"));
Lv2Log::debug(SS(" url: " << request_uri.str()));
setCacheControl(res, resourcefile);
res.setContentLength(size);
}
}
catch (const std::exception &e)
{
res.setBody("Error: " + std::string(e.what()));
ec = boost::system::errc::make_error_code(boost::system::errc::invalid_argument);
return;
}
}
static std::string makeCns(const std::string &encodedUri, int64_t instanceId)
{
std::stringstream ss;
ss << "_";
for (char c : encodedUri)
{
if (
(c >= 'a' && c <= 'z') || (c >= 'A' & c <= 'Z') || (c >= '0' && c <= '9'))
{
ss << c;
}
else
{
ss << '_'; // replace non-alphanumeric characters with '_'
}
}
ss << "_";
if (instanceId < 0)
{
ss << "_";
instanceId = -instanceId;
}
ss << instanceId;
return ss.str();
}
std::string ModWebInterceptImpl::GenerateTemplate(
const fs::path &templateFile,
std::shared_ptr<Lv2PluginInfo> pluginInfo)
{
if (!fs::exists(templateFile))
{
throw std::runtime_error("File not found. " + templateFile.string());
}
json_variant context = MakeModGuiTemplateData(pluginInfo);
int64_t version = pluginInfo->minorVersion() * 1000 +
pluginInfo->microVersion();
std::string encodedUri = HtmlHelper::encode_url_segment(pluginInfo->uri(), true);
std::string ns = SS("?ns=" << encodedUri << "&v=" << version);
context["_ns"] = ns;
std::string cns = makeCns(encodedUri, version);
context["_cns"] = cns;
return GenerateFromTemplateFile(
templateFile,
context);
}
+61
View File
@@ -0,0 +1,61 @@
/*
* Copyright (c) 2025 Robin E. R. Davies
* All rights reserved.
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include "WebServer.hpp"
#include "PiPedalModel.hpp"
#include <memory>
namespace pipedal {
class ModWebIntercept : public RequestHandler
{
PiPedalModel *model;
protected:
ModWebIntercept(const std::string &target_url)
: RequestHandler(target_url.c_str())
{
}
public:
using self = ModWebIntercept;
using super = RequestHandler;
using ptr = std::shared_ptr<self>;
static ptr Create(PiPedalModel *model);
ModWebIntercept(PiPedalModel *model)
: RequestHandler("/var"),
model(model)
{
}
virtual ~ModWebIntercept() = default;
};
}
+8 -1
View File
@@ -1,4 +1,11 @@
Exctly one underrun per seek in Toob Player
check reload after change of LV2 plugins.
Vu Meters, move rendering to background images, in order to reduce layout overhead.
Remove FullScreenIME in PluginControlView.tsx
Exactly one underrun per seek in Toob Player
Test autohotspot detection.
-3
View File
@@ -108,7 +108,6 @@ add_custom_command(
public/iso_codes.json
public/logo512.png
public/sample_lv2_plugins.json
public/vite.svg
src/pipedal/SplitUiControls.tsx
src/pipedal/JackHostStatus.tsx
src/pipedal/VirtualKeyboardHandler.tsx
@@ -291,8 +290,6 @@ add_custom_command(
src/pipedal/MidiChannelBinding.tsx
src/pipedal/ModFileTypes.tsx
src/assets/react.svg
src/App.css
src/App.tsx
src/vite-env.d.ts
src/index.css
src/main.tsx
+1 -1
View File
@@ -10,7 +10,7 @@
<link rel="stylesheet" href="/css/roboto.css" />
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
<link rel="stylesheet" href="/css/modGui.css" />
<style>
BODY {
background: #D0D0D0;
+64
View File
@@ -0,0 +1,64 @@
.mod-pedal {
}
.mod-pedal .mod-light {
}
.mod-pedal .mod-light.off { background-image:url(../img/red-light-off.png); }
.mod-pedal .mod-light.on { background-image:url(../img/red-light-on.png); }
.mod-pedal .mod-pedal-input {
position: absolute;
top: 70px;
left: -24px;
width: 24px;
}
.mod-pedal .mod-pedal-output {
position: absolute;
top: 70px;
right: -25px;
width: 25px;
}
.mod-pedal .mod-pedal-input .mod-audio-input,
.mod-pedal .mod-pedal-input .mod-midi-input,
.mod-pedal .mod-pedal-input .mod-cv-input {
position: relative;
margin-bottom: 20px;
}
.mod-audio-input .mod-pedal-input-image {
background-image:url(../img/audio-input.png) ;
width: 24px;
height: 56px;
margin-bottom: 20px;
}
.mod-audio-output .mod-pedal-output-image {
background-image:url(../img/audio-output.png);
width: 24px;
height: 56px;
margin-bottom: 20px;
}
.ppmod-dial-control-image {
width: 48px;
height: 48px;
object-fit: fill;
}
.ppmod-dial-control-frame
{
position: relative;
width: 48px;
height: 48px;
overflow: hidden;
}
.mod-knob-title {
white-space: nowrap;
overflow: hidden;
}
.mod-slider-title {
white-space: nowrap;
overflow: hidden;
}
+4 -4
View File
@@ -2,28 +2,28 @@
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
font-display: swap;
font-display: block;
src: url(/fonts/Roboto-Light.woff2) format('woff2');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-display: swap;
font-display: block;
src: url(/fonts/Roboto-Regular.woff2) format('woff2');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-display: swap;
font-display: block;
src: url(/fonts/Roboto-Medium.woff2) format('woff2');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
font-display: swap;
font-display: block;
src: url(/fonts/Roboto-Bold.woff2) format('woff2');
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+10
View File
@@ -0,0 +1,10 @@
@font-face {
font-family: 'EnglandHand';
src: url('england-webfont.woff');
font-weight: normal;
font-display: swap;
font-style: normal;
}
Binary file not shown.
+7
View File
@@ -0,0 +1,7 @@
@font-face {
font-family: 'epflul';
src: url('epf_lul-webfont.woff') format('woff');
font-weight: normal;
font-style: normal;
}
Binary file not shown.
+33
View File
@@ -0,0 +1,33 @@
/*
* Web Fonts from fontspring.com
*
* All OpenType features and all extended glyphs have been removed.
* Fully installable fonts can be purchased at http://www.fontspring.com
*
* The fonts included in this stylesheet are subject to the End User License you purchased
* from Fontspring. The fonts are protected under domestic and international trademark and
* copyright law. You are prohibited from modifying, reverse engineering, duplicating, or
* distributing this font software.
*
* (c) 2010-2012 Fontspring
*
*
*
*
* The fonts included are copyrighted by the vendor listed below.
*
* Vendor: Fontfabric
* License URL: http://www.fontspring.com/fflicense/fontfabric
*
*
*/
@font-face {
font-family: 'Nexa';
src: url('Nexa_Free_Bold-webfont.woff') format('woff');
font-display: block;
font-weight: normal;
font-style: normal;
}
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
/* Generated by Font Squirrel (http://www.fontsquirrel.com) on February 25, 2013 */
@font-face {
font-family: 'Pirulen';
src: url('pirulen_rg-webfont.woff') format('woff');
font-weight: normal;
font-style: normal;
}
Binary file not shown.
+10
View File
@@ -0,0 +1,10 @@
/* Generated by Font Squirrel (http://www.fontsquirrel.com) on February 27, 2013 */
@font-face {
font-family: 'Questrial';
src: url('questrial-regular-webfont.woff') format('woff');
font-display: block;
font-weight: normal;
font-style: normal;
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File
-36
View File
@@ -1,36 +0,0 @@
import { useState } from 'react'
import ReactLogo from './assets/react.svg?react'
//import ViteLogo from '/vite.svg?react'
import './App.css'
function App() {
const [count, setCount] = useState(0)
return (
<>
<div>
<a href="https://vite.dev" target="_blank">
{/* <ViteLogo/> */}
</a>
<a href="https://react.dev" target="_blank">
<ReactLogo style={{background: "blue", padding: 96}} />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
)
}
export default App
+1 -1
View File
@@ -152,7 +152,7 @@ const AboutDialog = class extends Component<AboutDialogProps, AboutDialogState>
<ArrowBackIcon />
</IconButtonEx>
<Typography noWrap variant="h6"
sx={{ maringLeft: 2, flex: 1 }}
sx={{ marginLeft: 2, flex: 1 }}
>
About
</Typography>
+15 -8
View File
@@ -18,13 +18,15 @@
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import React from 'react';
import { ThemeProvider, createTheme, StyledEngineProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import CssBaseline from '@mui/material/CssBaseline';
import VirtualKeyboardHandler from './VirtualKeyboardHandler';
import AppThemed from "./AppThemed";
import { isDarkMode } from './DarkMode';
import Tone3000AuthComplete from './Tone3000AuthComplete';
import FontTest from './FontTest';
declare module '@mui/material/styles' {
@@ -225,8 +227,13 @@ type AppThemeProps = {
function isTone3000Auth() {
let url = new URL(window.location.href);
let param = url.searchParams.get("api_key");
return (param !== null && param !== "")
let param = url.searchParams.get("api_key");
return (param !== null && param !== "")
}
function isFontTest() {
let url = new URL(window.location.href);
let param = url.searchParams.get("fontTest");
return (param !== null)
}
const App = (class extends React.Component {
@@ -248,11 +255,11 @@ const App = (class extends React.Component {
<StyledEngineProvider injectFirst>
<ThemeProvider theme={theme}>
<CssBaseline />
{isTone3000Auth() ? (
<Tone3000AuthComplete />
) : (
<AppThemed />
)}
{
isTone3000Auth() && (<Tone3000AuthComplete />)
|| isFontTest() && (<FontTest />)
|| (<AppThemed />)
}
</ThemeProvider>
</StyledEngineProvider>
);
+2
View File
@@ -19,6 +19,7 @@
import './AppThemed.css';
//import {alpha} from '@mui/material/styles';
import AppBar from '@mui/material/AppBar';
@@ -74,6 +75,7 @@ import HelpOutlineIcon from './svg/ic_help_outline.svg?react';
import FxAmplifierIcon from './svg/fx_amplifier.svg?react';
import { PerformanceView } from './PerformanceView';
import DialogEx from './DialogEx';
import { IDialogStackable, popDialogStack, pushDialogStack } from './DialogStack';
+23
View File
@@ -0,0 +1,23 @@
/*
* Copyright (c) 2025 Robin E. R. Davies
* All rights reserved.
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
+17 -3
View File
@@ -42,7 +42,11 @@ let pluginFactories: IControlViewFactory[] = [
];
export function GetControlView(pedalboardItem?: PedalboardItem | null): React.ReactNode {
export function GetControlView(
pedalboardItem: PedalboardItem | null,
showModUi: boolean,
onSetShowModGui: (instanceId: number, showModGui: boolean) => void
): React.ReactNode {
let model: PiPedalModel = PiPedalModelFactory.getInstance();
if (!pedalboardItem) {
@@ -50,7 +54,13 @@ export function GetControlView(pedalboardItem?: PedalboardItem | null): React.Re
}
if (pedalboardItem.isStart() || pedalboardItem.isEnd()) {
return (
<PluginControlView instanceId={pedalboardItem.instanceId} item={pedalboardItem} />
<PluginControlView instanceId={pedalboardItem.instanceId}
item={pedalboardItem}
showModGui={showModUi}
onSetShowModGui={(instanceId, showModGui) => {
onSetShowModGui?.(instanceId, showModGui);
}}
/>
);
}
if (pedalboardItem.isSplit()) {
@@ -73,7 +83,11 @@ export function GetControlView(pedalboardItem?: PedalboardItem | null): React.Re
</div>
} else {
return (
<PluginControlView instanceId={pedalboardItem.instanceId} item={pedalboardItem} />
<PluginControlView instanceId={pedalboardItem.instanceId} item={pedalboardItem} showModGui={showModUi}
onSetShowModGui={(instanceId, showModGui) => {
onSetShowModGui?.(instanceId, showModGui);
}}
/>
)
}
}
+75
View File
@@ -0,0 +1,75 @@
/*
* Copyright (c) 2025 Robin E. R. Davies
* All rights reserved.
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from 'react';
import Divider from '@mui/material/Divider';
function TypeSample(props: { fontFamily: string, cssRef: string, fontWeights: number[] }) {
let { fontFamily, cssRef, fontWeights } = props;
React.useEffect(() => {
let link = document.createElement('link');
link.rel = 'stylesheet';
link.href = cssRef;
document.head.appendChild(link);
return () => {
document.head.removeChild(link);
};
}, []);
return (
<div>
{fontWeights.map((weight) => (
<div key={`${fontFamily}-${weight}`} style={{ marginBottom: 24 }}>
<Divider />
<div style={{ marginLeft: 24, marginRight: 24 }}>
<h4 style={{marginBottom: 0}}>{fontFamily} {weight}</h4>
<div style={{ fontFamily: fontFamily, fontSize: 20, fontWeight: weight, marginLeft: 24 }}>
<div style={{ fontFamily: fontFamily }}>The quick brown fox jumped over the lazy dog.</div>
</div>
</div>
</div>
))}
</div>
);
}
function FontTest() {
return (
<div>
<h4 style={{ marginLeft: 24, marginRight: 24 }}>Font Test</h4>
{TypeSample({ fontFamily: "EnglandHand", cssRef: "/fonts/england-hand/stylesheet.css", fontWeights: [400] })}
{TypeSample({ fontFamily: "epflul", cssRef: "/fonts/epf/stylesheet.css", fontWeights: [400] })}
{TypeSample({ fontFamily: "Nexa", cssRef: "/fonts/nexa/stylesheet.css", fontWeights: [400] })}
{TypeSample({ fontFamily: "Pirulen", cssRef: "/fonts/pirulen/stylesheet.css", fontWeights: [400] })}
{TypeSample({ fontFamily: "Questrial", cssRef: "/fonts/questrial/stylesheet.css", fontWeights: [400] })}
{TypeSample({ fontFamily: "Pirulen", cssRef: "/fonts/pirulen/stylesheet.css", fontWeights: [400] })}
{TypeSample({ fontFamily: "Pirulen", cssRef: "/fonts/pirulen/stylesheet.css", fontWeights: [400] })}
{TypeSample({ fontFamily: "Pirulen", cssRef: "/fonts/pirulen/stylesheet.css", fontWeights: [400] })}
</div>
);
}
export default FontTest;
+4 -2
View File
@@ -21,12 +21,13 @@
* SOFTWARE.
*/
import React from 'react';
import IconButton, {IconButtonProps} from '@mui/material/IconButton';
import ToolTipEx from './ToolTipEx';
import Typography from "@mui/material/Typography";
interface IconButtonExProps extends IconButtonProps {
tooltip: string;
tooltip: React.ReactElement | string;
style?: React.CSSProperties;
};
@@ -35,9 +36,10 @@ function IconButtonEx(props: IconButtonExProps) {
return (
<ToolTipEx title={
(typeof props.tooltip === 'string') ?
(
<Typography variant="caption">{tooltip || extra['aria-label'] }</Typography>
)
): (props.tooltip as React.ReactElement)
}
>
<IconButton {...extra} style={style} />
+17 -8
View File
@@ -164,7 +164,8 @@ interface PluginGridProps extends WithStyles<typeof pluginGridStyles> {
uri?: string;
minimumItemWidth?: number;
theme: Theme;
open: boolean
open: boolean;
modGuiOnly?: boolean;
};
type PluginGridState = {
@@ -526,6 +527,8 @@ export const LoadPluginDialog =
for (let i = 0; i < plugins.length; ++i) {
let plugin = plugins[i];
if (this.props.modGuiOnly == true && !plugin.modGui)
continue;
try {
if (filterType === PluginType.Plugin || rootClass.is_type_of(filterType, plugin.plugin_type)) {
let score: number = 0;
@@ -720,9 +723,15 @@ export const LoadPluginDialog =
style={{ overflowX: "hidden", overflowY: "hidden", display: "flex", flexDirection: "column", flexWrap: "nowrap" }}
aria-labelledby="select-plugin-dialog-title">
<div style={{ display: "flex", flexDirection: "column", flexWrap: "nowrap", height: "100%" }}>
<DialogTitle id="select-plugin-dialog-title" style={{ flex: "0 0 auto", padding: "0px", height: 54 }}>
<div style={{ display: "flex", flexDirection: "row", paddingTop: 3, paddingBottom: 3, flexWrap: "nowrap", width: "100%", alignItems: "center" }}>
<IconButtonEx tooltip="Back" onClick={() => { this.cancel(); }} style={{ flex: "0 0 auto" }} >
<DialogTitle id="select-plugin-dialog-title" style={{ flex: "0 0 auto", padding: "0px", height: 54,
marginRight: 16
}}>
<div style={{ display: "flex", flexDirection: "row", paddingTop: 3, paddingBottom: 3, flexWrap: "nowrap",
height: 64,
width: "100%", alignItems: "center"
}}>
<IconButtonEx tooltip="Back" onClick={() => { this.cancel(); }}
style={{ flex: "0 0 auto", marginRight: 16 }} >
<ArrowBackIcon />
</IconButtonEx>
@@ -731,9 +740,9 @@ export const LoadPluginDialog =
visibility: (this.state.search_collapsed ? "visible" : "collapse"),
width: (this.state.search_collapsed ? undefined : "0px")
}}>
{this.state.client_width > 520 ? "Select Plugin" : ""}
{this.state.client_width > 645 ? "Select Plugin" : ""}
</Typography>
<div style={{ flex: "1 1 auto" }} >
<div style={{ flex: "2 2 auto" }} >
<SearchControl collapsed={this.state.search_collapsed} searchText={this.state.search_string}
inputRef={this.searchInputRef}
showSearchIcon={showSearchIcon}
@@ -784,13 +793,13 @@ export const LoadPluginDialog =
)}
</IconButtonEx>
</div>
<div style={{ flex: "0 0 160px", marginRight: 24 }} >
<div style={{ flex: "0 1 160px", position: "relative", marginRight: 24 }} >
<TextField select variant="standard"
defaultValue={this.state.filterType}
key={this.state.filterType}
onChange={(e) => { this.onFilterChange(e); }}
sx={{ minWidth: 160 }}
sx={{ width: "100%",minWidth: "100%" }}
>
{this.createFilterOptions()}
</TextField>
+145 -26
View File
@@ -26,10 +26,10 @@ interface Deserializable<T> {
}
const noteNames: string[] = [
"C","C#","D","Eb","E","F","F#","G","Ab","A","Bb","B","C"
"C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B", "C"
];
function semitone12TETValue(value: number) : string {
function semitone12TETValue(value: number): string {
let iValue = Math.round(value) % 12;
return noteNames[iValue];
@@ -313,10 +313,85 @@ export class UiFileProperty {
useLegacyModDirectory: boolean = false;
};
export class ModGuiPort {
deserialize(input: any): ModGuiPort {
this.index = input.index;
this.symbol = input.symbol;
this.name = input.name;
return this;
}
static deserialize_array(input: any): ModGuiPort[] {
let result: ModGuiPort[] = [];
for (let i = 0; i < input.length; ++i) {
result[i] = new ModGuiPort().deserialize(input[i]);
}
return result;
}
index: number = -1;
symbol: string = "";
name: string = "";
};
export class ModGui {
deserialize(input: any): ModGui {
this.pluginUri = input.pluginUri;
this.resourceDirectory = input.resourceDirectory;
this.iconTemplate = input.iconTemplate ?? "";
this.settingsTemplate = input.settingsTemplate;
this.javascript = input.javascript;
this.stylesheet = input.stylesheet ?? "";
this.screenshot = input.screenshot;
this.thumbnail = input.thumbnail;
this.discussionUrl = input.discussionUrl;
this.documentationUrl = input.documentationUrl;
this.brand = input.brand;
this.label = input.label;
this.model = input.model;
this.panel = input.panel;
this.color = input.color;
this.knob = input.knob;
if (input.ports) {
this.ports = ModGuiPort.deserialize_array(input.ports);
} else {
this.ports = [];
}
return this;
}
static serialize_array(input: any): ModGui[] {
let result: ModGui[] = [];
for (let i = 0; i < input.length; ++i) {
result[i] = new ModGui().deserialize(input[i]);
}
return result;
}
pluginUri = "";
resourceDirectory = "";
iconTemplate = "";
settingsTemplate = "";
javascript = "";
stylesheet = "";
screenshot = "";
thumbnail = "";
discussionUrl = "";
documentationUrl = "";
brand = "";
label = "";
model = "";
panel = "";
color = "";
knob = "";
ports: ModGuiPort[] = [];
};
export class Lv2Plugin implements Deserializable<Lv2Plugin> {
deserialize(input: any): Lv2Plugin {
this.uri = input.uri;
this.name = input.name;
this.minorVersion = input.minorVersion ? input.minorVersion : 0;
this.microVersion = input.microVersion ? input.microVersion : 0;
this.brand = input.name ? input.name : "";
this.label = input.label ? input.label : this.name;
this.plugin_class = input.plugin_class;
@@ -344,12 +419,18 @@ export class Lv2Plugin implements Deserializable<Lv2Plugin> {
} else {
this.uiPortNotifications = [];
}
this.modGui = null;
if (input.modGui) {
this.modGui = new ModGui().deserialize(input.modGui);
}
return this;
}
static EmptyFeatures: string[] = [];
uri: string = "";
name: string = "";
minorVersion: number = 0;
microVersion: number = 0;
brand: string = "";
label: string = "";
plugin_class: string = "";
@@ -364,6 +445,7 @@ export class Lv2Plugin implements Deserializable<Lv2Plugin> {
fileProperties: UiFileProperty[] = [];
frequencyPlots: UiFrequencyPlot[] = [];
uiPortNotifications: UiPropertyNotification[] = [];
modGui: ModGui | null = null;
}
@@ -459,10 +541,12 @@ export enum ControlType {
Vu,
Progress,
DbVu,
OutputText
OutputText,
BypassLight
}
const textUnits : Set<Units> = new Set<Units>([
const textUnits: Set<Units> = new Set<Units>([
Units.bar,
Units.beat,
Units.bpm,
@@ -478,13 +562,12 @@ const textUnits : Set<Units> = new Set<Units>([
Units.semitone12TET
]);
function displayUnitAsText(unit: Units): boolean
{
function displayUnitAsText(unit: Units): boolean {
return textUnits.has(unit);
}
export class UiControl implements Deserializable<UiControl> {
deserialize(input: any): UiControl {
deserialize(input: any): UiControl {
this.symbol = input.symbol;
this.name = input.name;
this.index = input.index;
@@ -531,12 +614,11 @@ deserialize(input: any): UiControl {
this.controlType = ControlType.Progress;
} else if (this.enumeration_property) {
this.controlType = ControlType.OutputText;
} else if (displayUnitAsText(this.units))
{
} else if (displayUnitAsText(this.units)) {
this.controlType = ControlType.OutputText
} else {
this.controlType = ControlType.Vu;
}
}
}
if (this.isValidEnumeration()) {
this.controlType = ControlType.Select;
@@ -548,10 +630,8 @@ deserialize(input: any): UiControl {
this.controlType = ControlType.OnOffSwitch;
}
}
if (this.is_input)
{
if (this.pipedal_graphicEq)
{
if (this.is_input) {
if (this.pipedal_graphicEq) {
this.controlType = ControlType.GraphicEq;
}
else if (this.mod_momentaryOnByDefault) {
@@ -560,7 +640,7 @@ deserialize(input: any): UiControl {
this.controlType = ControlType.Momentary;
} else if (this.trigger_property) {
this.controlType = ControlType.Trigger;
}
}
}
return this;
}
@@ -654,7 +734,7 @@ deserialize(input: any): UiControl {
isAbToggle(): boolean {
return this.controlType === ControlType.ABSwitch;
}
isGraphicEq() : boolean {
isGraphicEq(): boolean {
return this.controlType === ControlType.GraphicEq;
}
isSelect(): boolean {
@@ -739,15 +819,12 @@ deserialize(input: any): UiControl {
}
}
if (this.units === Units.s)
{
if (this.units === Units.s) {
let iValue = Math.round(value);
if (iValue >= 60)
{
let minutes = Math.floor(iValue/60);
if (iValue >= 60) {
let minutes = Math.floor(iValue / 60);
let seconds = iValue % 60;
if (seconds < 10)
{
if (seconds < 10) {
return minutes + ":0" + seconds;
}
@@ -756,11 +833,10 @@ deserialize(input: any): UiControl {
return this.formatShortValue(value);
}
}
if (this.units === Units.semitone12TET)
{
if (this.units === Units.semitone12TET) {
return semitone12TETValue(value);
}
let text = this.formatShortValue(value);
switch (this.units) {
@@ -831,11 +907,45 @@ deserialize(input: any): UiControl {
}
export class Lv2PatchPropertyInfo {
deserialize(input: any): Lv2PatchPropertyInfo {
this.uri = input.uri;
this.writable = input.writable;
this.readable = input.readable;
this.label = input.label;
this.index = input.index;
this.type = input.type;
this.comment = input.comment;
this.shortName = input.shortName;
this.fileTypes = input.fileTypes;
this.supportedExtensions = input.supportedExtensions;
return this;
}
static deserialize_array(input: any): Lv2PatchPropertyInfo[] {
let result: Lv2PatchPropertyInfo[] = [];
for (let i = 0; i < input.length; ++i) {
result[i] = new Lv2PatchPropertyInfo().deserialize(input[i]);
}
return result;
}
uri: string = "";
writable : boolean = false;
readable: boolean = false;
label: string = "";
index: number = -1;
type: string = "";
comment: string = "";
shortName: string = "";
fileTypes: string[] = [];
supportedExtensions: string[] = [];
};
export class UiPlugin implements Deserializable<UiPlugin> {
deserialize(input: any): UiPlugin {
this.uri = input.uri;
this.name = input.name;
this.minorVersion = input.minorVersion ? input.minorVersion : 0;
this.microVersion = input.microVersion ? input.microVersion : 0;
this.brand = input.brand ? input.brand : "";
this.label = input.label ? input.label : this.name;
this.plugin_type = input.plugin_type as PluginType;
@@ -861,6 +971,11 @@ export class UiPlugin implements Deserializable<UiPlugin> {
}
this.is_vst3 = input.is_vst3;
this.modGui = null;
if (input.modGui) {
this.modGui = new ModGui().deserialize(input.modGui);
}
this.patchProperties = Lv2PatchPropertyInfo.deserialize_array(input.patchProperties ?? []);
return this;
}
@@ -908,6 +1023,8 @@ export class UiPlugin implements Deserializable<UiPlugin> {
uri: string = "";
name: string = "";
minorVersion: number = 0;
microVersion: number = 0;
brand: string = "";
label: string = "";
plugin_type: PluginType = PluginType.InvalidPlugin;
@@ -924,6 +1041,8 @@ export class UiPlugin implements Deserializable<UiPlugin> {
fileProperties: UiFileProperty[] = [];
frequencyPlots: UiFrequencyPlot[] = [];
is_vst3: boolean = false;
modGui: ModGui | null = null; // null if no mod gui.
patchProperties: Lv2PatchPropertyInfo[] = [];
}
+54 -9
View File
@@ -58,6 +58,8 @@ import Snapshot3Icon from "./svg/snapshot_3.svg?react";
import Snapshot4Icon from "./svg/snapshot_4.svg?react";
import Snapshot5Icon from "./svg/snapshot_5.svg?react";
import Snapshot6Icon from "./svg/snapshot_6.svg?react";
import ModUiIcon from './svg/mod_ui.svg?react';
import PipedalUiIcon from './svg/pp_ui.svg?react';
import SnapshotDialog from './SnapshotDialog';
import { css } from '@emotion/react';
@@ -128,6 +130,8 @@ interface MainState {
showMidiBindingsDialog: boolean;
screenHeight: number;
displayNameDialogOpen: boolean;
showModUi: boolean;
}
@@ -167,7 +171,8 @@ export const MainPage =
displayAuthor: this.getDisplayAuthor(),
horizontalScrollLayout: this.windowSize.height < HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK,
showMidiBindingsDialog: false,
screenHeight: this.windowSize.height
screenHeight: this.windowSize.height,
showModUi: false
};
@@ -375,7 +380,7 @@ export const MainPage =
if (pedalboardItem === null) return "";
return pedalboardItem.uri;
}
titleBar(pedalboardItem: PedalboardItem | null): React.ReactNode {
titleBar(pedalboardItem: PedalboardItem | null,canShowModUi: boolean): React.ReactNode {
let title = "";
let author = "";
let infoPluginUri = "";
@@ -441,7 +446,7 @@ export const MainPage =
{canEditTitle ? (
<ButtonBase
style={{ borderRadius: "6px", width: "100%", paddingLeft: 8, paddingRight: 8, height: 48, textTransform: "none" }}
onClick={()=> {
onClick={() => {
this.handleEditPluginDisplayName();
}}
>
@@ -453,7 +458,7 @@ export const MainPage =
</span>
</ButtonBase>)
: (
<div style={{ flex: "0 1 auto", paddingLeft: 8, paddingRight: 8,minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }}>
<div style={{ flex: "0 1 auto", paddingLeft: 8, paddingRight: 8, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }}>
<span className={classes.title}>{title}</span>
{this.state.displayAuthor && (
<span className={classes.author}>{author}</span>
@@ -469,6 +474,32 @@ export const MainPage =
<PluginPresetSelector pluginUri={presetsUri} instanceId={pedalboardItem?.instanceId ?? 0}
/>
</div>
{canShowModUi && (
<div style={{ flex: "0 0 auot" }}>
<IconButtonEx tooltip={(
<div>
<Typography variant="body2" >
{this.state.showModUi ? "PiPedal UI" : "MOD UI"}</Typography>
<Divider />
<Typography variant="caption">Use MOD UI or PiPedal UI for plugin</Typography>
</div>
)}
onClick={() => {
this.setState({ showModUi: !this.state.showModUi });
}}
size="large">
{!this.state.showModUi ?
(
<ModUiIcon style={{ height: 24, width: 24, fill: this.props.theme.palette.text.primary, opacity: 0.6 }} />
) : (
<PipedalUiIcon style={{ height: 24, width: 24, fill: this.props.theme.palette.text.primary, opacity: 0.6 }} />
)
}
</IconButtonEx>
</div>
)}
</div>
);
}
@@ -494,6 +525,15 @@ export const MainPage =
}
}
canShowModUi(pedalboardItem: PedalboardItem): boolean {
let pluginInfo = this.model.getUiPlugin(pedalboardItem.uri);
if (pluginInfo === null) {
return false;
}
return pluginInfo.modGui !== null;
}
render() {
const classes = withStyles.getClasses(this.props);
let pedalboard = this.model.pedalboard.get();
@@ -508,8 +548,9 @@ export const MainPage =
let instanceId = -1;
let missing = false;
let pluginUri = "#error";
let canShowModUi = false;
if (pedalboardItem) {
canShowModUi = this.canShowModUi(pedalboardItem);
canDelete = pedalboard.canDeleteItem(pedalboardItem.instanceId);
instanceId = pedalboardItem.instanceId;
if (pedalboardItem.isEmpty()) {
@@ -570,7 +611,7 @@ export const MainPage =
</div>
{
(!this.state.splitControlBar || !this.props.enableStructureEditing)
&& this.titleBar(pedalboardItem)
&& this.titleBar(pedalboardItem, canShowModUi)
}
<div style={{ flex: "1 1 1px" }}>
@@ -642,7 +683,7 @@ export const MainPage =
this.state.splitControlBar && this.props.enableStructureEditing && (
<div className={classes.splitControlBar}>
{
this.titleBar(pedalboardItem)
this.titleBar(pedalboardItem, canShowModUi)
}
</div>
)
@@ -657,7 +698,11 @@ export const MainPage =
) :
(
GetControlView(pedalboardItem)
GetControlView(pedalboardItem,this.state.showModUi && canShowModUi,
(instanceId: number, showModGui: boolean) => {
this.setState({ showModUi: showModGui });
}
)
)
}
</div>
@@ -689,7 +734,7 @@ export const MainPage =
this.setState({ displayNameDialogOpen: false });
}}
/>
)}
)}
</div>
);
}
+104
View File
@@ -0,0 +1,104 @@
/*
* Copyright (c) 2025 Robin E. R. Davies
* All rights reserved.
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { Component, ErrorInfo, ReactNode } from "react";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import { UiPlugin } from "./Lv2Plugin";
interface Props {
plugin: UiPlugin | null;
children?: ReactNode;
onClose: () => void;
}
interface State {
hasError: boolean;
message: string;
}
class ModGuiErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false,
message: "",
};
public static getDerivedStateFromError(error: Error): State {
// Update state so the next render will show the fallback UI.
return { hasError: true, message: error.message };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error("ModGUI rendering error: ", error);
console.error("Stack trace: " , errorInfo.componentStack);
}
public render() {
if (this.state.hasError) {
return (
<div style={{
position: "absolute",left: 0, right: 0, top: 0, bottom: 0,
display: "flex",
flexDirection: "column",
justifyContent: "stretch",
}}>
<div style={{ flex: "1 1 auto" }} />
<div style={{display: "flex", flexDirection: "row", alignItems: "stretch", justifyContent: "stretch"}}>
<div style={{ flex: "1 1 auto" }} />
<div style={{maxWidth: 600, margin: 24}}>
<Typography variant="h6" style={{ marginBottom: 8 }} >
{ this.props.plugin ?
`Error Loading '${this.props.plugin.name}'` :
`Error Loading UI`
}
</Typography>
<Typography variant="body2" style={{marginBottom:16}} >
{this.state.message}
</Typography>
<div style={{alignSelf: "flex-end", marginBottom: 16, marginRight: 24, display: "flex", flexFlow: "row nowrap", justifyContent: "flex-end" }}>
<Button
style={{alignSelf: "flex-end"}}
variant="contained"
onClick={() => {
this.props.onClose();
}}
>Close
</Button>
</div>
</div>
<div style={{ flex: "1 1 auto" }} />
</div>
<div style={{ flex: "2 2 auto" }} />
</div>
);
}
return this.props.children;
}
}
export default ModGuiErrorBoundary;
File diff suppressed because it is too large Load Diff
+346
View File
@@ -0,0 +1,346 @@
/*
* Copyright (c) 2025 Robin E. R. Davies
* All rights reserved.
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from 'react';
import Divider from '@mui/material/Divider';
import { PiPedalModel, PiPedalModelFactory, MonitorPortHandle, State, ListenHandle, PatchPropertyListener } from './PiPedalModel';
import { useTheme } from '@mui/material/styles';
import CircularProgress from '@mui/material/CircularProgress';
import LoadPluginDialog from './LoadPluginDialog';
import ButtonEx from './ButtonEx';
import Typography from '@mui/material/Typography/Typography';
import ModGuiHost, { IModGuiHostSite } from './ModGuiHost';
import { UiPlugin } from './Lv2Plugin';
import ModGuiErrorBoundary from './ModGuiErrorBoundary';
class MyPortHandle implements MonitorPortHandle {
private static nextHandle: number = 1;
constructor() {
this.handle = MyPortHandle.nextHandle++;
}
handle: number;
};
type MonitorCallback = (value: number) => void;
class ValueEntry {
constructor(defaultValue: number) {
this.value = defaultValue;
}
private listeners: { [handle: number]: MonitorCallback } = {};
monitor(interval: number, callback: MonitorCallback): MyPortHandle {
let handle = new MyPortHandle();
this.listeners[handle.handle] = callback;
return handle;
}
unmonitor(handle: MonitorPortHandle) {
let h = (handle as MyPortHandle).handle;
// remove entry h from listeners
if (!(h in this.listeners)) {
throw new Error("Invalid handle: " + h);
}
// remove the callback from the listeners
delete this.listeners[h];
}
setValue(value: number) {
if (value !== this.value) {
this.value = value;
// notify all listeners
for (let handle in this.listeners) {
this.listeners[handle](value);
}
}
}
value: number;
};
class PatchListenerEntry {
constructor(instanceId: number, propertyUri: string) {
this.instanceId = instanceId;
this.propertyUri = propertyUri;
}
instanceId: number;
propertyUri: string;
value: any = "";
listeners: { handle: number, listener: PatchPropertyListener}[] = [];;
};
class ValueHandler {
private valueDictionary: { [symbol: string]: ValueEntry } = {};
private handleToValueEnry: { [handle: number]: ValueEntry } = {};
constructor(instanceId: number, plugin: UiPlugin) {
for (let control of plugin.controls) {
this.valueDictionary[control.symbol] = new ValueEntry(control.default_value);
}
this.valueDictionary["_bypass"] = new ValueEntry(0);
}
monitorPort(instanceId: number, symbol: string, interval: number, callback: MonitorCallback): MonitorPortHandle {
let valueEntry = this.valueDictionary[symbol];
let portHandle = valueEntry.monitor(interval, callback);
this.handleToValueEnry[(portHandle as MyPortHandle).handle] = valueEntry;
callback(valueEntry.value);
return portHandle;
}
unmonitorPort(handle: MonitorPortHandle): void {
let h = (handle as MyPortHandle).handle;
if (!(h in this.handleToValueEnry)) {
throw new Error("Invalid handle: " + h);
}
let valueEntry = this.handleToValueEnry[h];
valueEntry.unmonitor(handle);
delete this.handleToValueEnry[h];
}
setValue(instanceId: number, symbol: string, value: number) {
// add a delay just for funsies.
window.setTimeout(() => {
if (!(symbol in this.valueDictionary)) {
throw new Error("Invalid symbol: " + symbol);
}
let valueEntry = this.valueDictionary[symbol];
valueEntry.setValue(value);
}, 100);
}
getValue(instanceId: number, symbol: string): number {
if (!(symbol in this.valueDictionary)) {
throw new Error("Invalid symbol: " + symbol);
}
let valueEntry = this.valueDictionary[symbol];
return valueEntry.value;
}
private nextListenHandle: number = 1;
private patchListeners: PatchListenerEntry[] = [];
monitorPatchProperty(instanceId: number, propertyUri: string, onReceived: PatchPropertyListener): ListenHandle {
let h = this.nextListenHandle++;
for (let entry of this.patchListeners) {
if (entry.instanceId === instanceId && entry.propertyUri === propertyUri) {
// already listening to this property
entry.listeners.push({ handle: h, listener: onReceived });
onReceived(instanceId,propertyUri,entry.value);
return { _handle: h };
}
}
let entry = new PatchListenerEntry(instanceId, propertyUri);
this.patchListeners.push(entry);
entry.listeners.push({ handle: h, listener: onReceived });
onReceived(instanceId, propertyUri, entry.value);
return {_handle: h};
}
cancelMonitorPatchProperty(listenHandle: ListenHandle): void {
for (let i = 0; i < this.patchListeners.length; ++i) {
let entry = this.patchListeners[i];
for (let j = 0; j < entry.listeners.length; ++j) {
if (entry.listeners[j].handle === listenHandle._handle) {
// found the listener, remove it
entry.listeners.splice(j, 1);
if (entry.listeners.length === 0) {
// no more listeners, remove the entry
this.patchListeners.splice(i, 1);
}
return;
}
}
}
}
getPatchProperty(instanceId: number, uri: string): Promise<any> {
let promise = new Promise<any>((resolve, reject) => {
window.setTimeout(() => {
for (let entry of this.patchListeners) {
if (entry.instanceId === instanceId && entry.propertyUri === uri) {
window.setTimeout(() => {
// simulate a delay for getting the property
entry.listeners.forEach(l => l.listener(instanceId, uri, entry.value));
}, 100);
return resolve(entry.value);
}
}
reject(new Error(`Patch property not found: ${uri} for instance ${instanceId}`));
},50);
});
return promise;
}
setPatchProperty(instanceId: number, uri: string, value: any): Promise<boolean> {
// Implementation here
for (let entry of this.patchListeners) {
if (entry.instanceId === instanceId && entry.propertyUri === uri) {
entry.value = value;
// notify all listeners
entry.listeners.forEach(l => l.listener(instanceId, uri, value));
return Promise.resolve(true);
}
}
// if we reach here, the property was not found
return Promise.reject("Not found.");
}
};
function ModGuiTest() {
const [pluginUrl, setPluginUrl] = React.useState("");
const [uiPlugin, setUiPlugin] = React.useState<UiPlugin | null>(null);
const [pluginDialogOpen, setPluginDialogOpen] = React.useState(false);
const [loading, setLoading] = React.useState(true);
const [valueHandler, setValueHandler] = React.useState<ValueHandler | null>(null);
const [modGuiHostSite] = React.useState<IModGuiHostSite>({
monitorPort: (instanceId: number, symbol: string, interval: number, callback: (value: number) => void) => {
if (!valueHandler) {
throw new Error("ValueHandler is not set");
}
return valueHandler.monitorPort(instanceId, symbol, interval, callback);
},
unmonitorPort: (handle) => {
if (!valueHandler) {
throw new Error("ValueHandler is not set");
}
valueHandler.unmonitorPort(handle);
},
setPedalboardControl: (instanceId, symbol, value) => {
if (!valueHandler) {
throw new Error("ValueHandler is not set");
}
valueHandler.setValue(instanceId, symbol, value);
},
monitorPatchProperty(instanceId: number, propertyUri: string, onReceived: PatchPropertyListener): ListenHandle {
if (!valueHandler) {
throw new Error("ValueHandler is not set");
}
return valueHandler.monitorPatchProperty(instanceId, propertyUri, onReceived);
},
cancelMonitorPatchProperty: (listenHandle) => {
if (!valueHandler) {
throw new Error("ValueHandler is not set");
}
valueHandler.cancelMonitorPatchProperty(listenHandle);
},
getPatchProperty: async (instanceId, uri) => {
if (!valueHandler) {
throw new Error("ValueHandler is not set");
}
return valueHandler.getPatchProperty(instanceId, uri) as Promise<any>;
},
setPatchProperty: async (instanceId, uri, value) => {
if (!valueHandler) {
throw new Error("ValueHandler is not set");
}
return valueHandler.setPatchProperty(instanceId, uri, value) as Promise<boolean>;
}
});
let model: PiPedalModel = PiPedalModelFactory.getInstance();
React.useEffect(() => {
const onStateChange = (state: State) => {
if (state == State.Ready) {
setLoading(false);
}
};
model.state.addOnChangedHandler(onStateChange);
return () => {
model.state.removeOnChangedHandler(onStateChange);
};
}, [])
const theme = useTheme();
return (
<div style={{
display: "flex", flexFlow: "column nowrap", alignItems: "stretch", justifyContent: "stretch",
position: "absolute", top: 0, left: 0, width: "100%", height: "100%", backgroundColor: (theme.palette as any).mainBackground
}}>
<div style={{
position: "absolute", top: 0, left: 0, right: 0,
bottom: 0, zIndex: 1000,
display: loading ? "flex" : "none",
alignItems: "center", justifyContent: "center",
background: "rgba(0.5,0.5,0.5, 0.9)"
}}>
<CircularProgress style={{ color: theme.palette.text.secondary }} />
</div>
<div style={{
flex: "0 0 auto", display: "flex", flexFlow: "row nowrap",
alignItems: "center", gap: 16, paddingLeft: 32, paddingRight: 32, paddingTop: 16, paddingBottom: 16,
}}>
<Typography noWrap variant="h6" style={{ flex: "1 0 auto" }}>ModGUI Test</Typography>
<Typography noWrap variant="body2" style={{ flex: "0 1 auto", }}>
{pluginUrl ? pluginUrl : "No plugin loaded"}
</Typography>
<ButtonEx style={{ flex: "0 0 auto" }} variant="contained" tooltip="Select plugin" onClick={() => setPluginDialogOpen(true)} >
Load
</ButtonEx>
</div>
<Divider />
{uiPlugin && (
<div style={{ flex: "1 1 auto", position: "relative" }}>
<ModGuiErrorBoundary plugin={uiPlugin} onClose={() => {
setPluginUrl("");
setUiPlugin(null);
setValueHandler(null);
}} >
<ModGuiHost instanceId={-1} plugin={uiPlugin}
onClose={() => {
setPluginUrl("");
setUiPlugin(null);
}}
hostSite={modGuiHostSite}
/>
</ModGuiErrorBoundary>
</div>
)}
{pluginDialogOpen &&
<LoadPluginDialog
open={pluginDialogOpen}
onOk={(url) => {
setPluginUrl(url as string);
let uiPlugin = model.getUiPlugin(url);
setUiPlugin(uiPlugin);
if (uiPlugin) {
setValueHandler(new ValueHandler(-1, uiPlugin));
}
setPluginDialogOpen(false);
}}
onCancel={() => setPluginDialogOpen(false)}
uri={pluginUrl}
modGuiOnly={true}
/>
}
</div>
);
}
export default ModGuiTest;
+105
View File
@@ -0,0 +1,105 @@
/*
* MIT License
*
* Copyright (c) 2022 Robin E. R. Davies
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from 'react';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import IconButtonEx from './IconButtonEx';
import Button from '@mui/material/Button';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import DialogEx from './DialogEx';
import DialogTitle from '@mui/material/DialogTitle';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import ReportGmailerrorredIcon from '@mui/icons-material/ReportGmailerrorred';
export interface OkDialogProps {
open: boolean,
title?: string,
text: string,
okButtonText?: string,
onClose: () => void
};
export interface OkDialogState {
};
export default class OkDialog extends React.Component<OkDialogProps, OkDialogState> {
constructor(props: OkDialogProps) {
super(props);
this.state = {
};
}
render() {
let props = this.props;
let { open, okButtonText, text, onClose } = props;
const handleClose = () => {
onClose();
};
const handleOk = () => {
onClose();
}
return (
<DialogEx tag="alert" open={open} onClose={handleClose}
onEnterKey={handleOk}
maxWidth="sm"
style={{ userSelect: "none" }}
>
{props.title && (
<DialogTitle id="alert-dialog-title" style={{ userSelect: "none", padding: 0, margin: 0 }}>
<Toolbar style={{paddingLeft: 24, paddingRight: 24,margin: 0}} >
<IconButtonEx
tooltip="Back" edge="start" color="inherit" onClick={()=>{this.props.onClose();}} aria-label="back"
>
<ArrowBackIcon />
</IconButtonEx>
<Typography noWrap variant="body1" style={{ flex: "1 1 auto" }}>{props.title}</Typography>
</Toolbar>
</DialogTitle>
)}
<DialogContent >
<div style={{ display: "flex", flexFlow: "row nowrap", gap: 16, marginLeft: 32, marginRight: 32, alignItems: "start" }}>
<ReportGmailerrorredIcon style={{ color: "var(--color-error)", fontSize: 48, flexShrink: 0, opacity: 0.8 }} />
<Typography variant="body2" style={{ marginTop:16}}
>{text}</Typography>
</div>
</DialogContent>
<DialogActions>
<Button variant="dialogPrimary" onClick={handleOk} style={{marginRight: 24}} >
{okButtonText ? okButtonText : "OK"}
</Button>
</DialogActions>
</DialogEx>
);
}
}
+8 -2
View File
@@ -416,6 +416,7 @@ export class PiPedalModel //implements PiPedalModel
socketServerUrl: string = "";
varServerUrl: string = "";
modResourcesUrl: string = "";
lv2Path: string = "";
webSocket?: PiPedalSocket;
@@ -972,13 +973,17 @@ export class PiPedalModel //implements PiPedalModel
// anything could have changed while we were disconnected.
await this.loadServerState();
}
makeSocketServerUrl(hostName: string, port: number): string {
private makeSocketServerUrl(hostName: string, port: number): string {
return "ws://" + hostName + ":" + port + "/pipedal";
}
makeVarServerUrl(protocol: string, hostName: string, port: number): string {
private makeVarServerUrl(protocol: string, hostName: string, port: number): string {
return protocol + "://" + hostName + ":" + port + "/var/";
}
private makeModResourceUrl(protocol: string, hostName: string, port: number): string {
return protocol + "://" + hostName + ":" + port + "/resources/";
}
async getNextAudioFile(filePath: string): Promise<string> {
@@ -1053,6 +1058,7 @@ export class PiPedalModel //implements PiPedalModel
if (!socket_server_port) socket_server_port = 8080;
let socket_server = this.makeSocketServerUrl(socket_server_address, socket_server_port);
let var_server_url = this.makeVarServerUrl("http", socket_server_address, socket_server_port);
this.modResourcesUrl = this.makeModResourceUrl("http", socket_server_address, socket_server_port);
this.socketServerUrl = socket_server;
this.varServerUrl = var_server_url;
+94 -29
View File
@@ -23,6 +23,8 @@ import WithStyles, { withTheme } from './WithStyles';
import { createStyles } from './WithStyles';
import { css } from '@emotion/react';
import ModGuiHost, { IModGuiHostSite } from './ModGuiHost';
import { withStyles } from "tss-react/mui";
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
import { UiPlugin, UiControl, UiFileProperty, UiFrequencyPlot, ScalePoint } from './Lv2Plugin';
@@ -340,6 +342,8 @@ export interface PluginControlViewProps extends WithStyles<typeof styles> {
item: PedalboardItem;
customization?: ControlViewCustomization;
customizationId?: number;
showModGui: boolean;
onSetShowModGui: (instanceId: number, showModGui: boolean) => void;
}
type PluginControlViewState = {
landscapeGrid: boolean;
@@ -360,7 +364,6 @@ const PluginControlView =
constructor(props: PluginControlViewProps) {
super(props);
this.model = PiPedalModelFactory.getInstance();
this.state = {
landscapeGrid: false,
imeUiControl: undefined,
@@ -376,6 +379,19 @@ const PluginControlView =
this.onControlValueChanged = this.onControlValueChanged.bind(this);
this.onPreviewChange = this.onPreviewChange.bind(this);
}
// unmonitorPort: (handle: MonitorPortHandle) => void;
// onValueChanged: (instanceId: number, symbol: string, value: number) => void;
// monitorPatchProperty(
// instanceId: number,
// propertyUri: string,
// onReceived: PatchPropertyListener
// ): ListenHandle;
// cancelMonitorPatchProperty(listenHandle: ListenHandle): void;
// getPatchProperty(instanceId: number, uri: string): Promise<any>;
// setPatchProperty(instanceId: number, uri: string, value: any): Promise<boolean>
onPreviewChange(key: string, value: number): void {
@@ -758,19 +774,45 @@ const PluginControlView =
}
return false;
}
makeHostSite(): IModGuiHostSite {
// Yuck. :-( This worked out badly.
let model = this.model;
return {
monitorPort: this.model.monitorPort.bind(model),
unmonitorPort: this.model.unmonitorPort.bind(model),
setPedalboardControl: this.model.setPedalboardControl.bind(model),
monitorPatchProperty: this.model.monitorPatchProperty.bind(model),
cancelMonitorPatchProperty: this.model.cancelMonitorPatchProperty.bind(model),
getPatchProperty: this.model.getPatchProperty.bind(model),
setPatchProperty: this.model.setPatchProperty.bind(model),
};
}
render(): ReactNode {
this.controlKeyIndex = 0;
const classes = withStyles.getClasses(this.props);
let pedalboardItem: PedalboardItem;
let pedalboard = this.model.pedalboard.get();
if (this.props.instanceId === Pedalboard.START_CONTROL) {
pedalboardItem = pedalboard.makeStartItem();
} else if (this.props.instanceId === Pedalboard.END_CONTROL) {
pedalboardItem = pedalboard.makeEndItem();
} else {
pedalboardItem = pedalboard.getItem(this.props.instanceId);
renderModGui(): ReactNode {
let uiPlugin = this.model.getUiPlugin(this.props.item.uri);
if (!uiPlugin) {
return (<div />);
}
return (
<ModGuiHost
instanceId={this.props.instanceId}
plugin={uiPlugin}
onClose={() => {
if (this.props.onSetShowModGui) {
this.props.onSetShowModGui(this.props.instanceId, false);
}
}}
hostSite={this.makeHostSite()}
/>
)
}
renderPiPedalControl(pedalboardItem?: PedalboardItem): ReactNode {
this.controlKeyIndex = 0;
const classes = withStyles.getClasses(this.props);
let pedalboard = this.model.pedalboard.get();
if (!pedalboardItem)
return (<div className={classes.frame} ></div>);
@@ -792,19 +834,13 @@ const PluginControlView =
controlValues = this.filterNotOnGui(controlValues, plugin);
let gridClass = this.state.landscapeGrid ? classes.landscapeGrid : classes.normalGrid;
let scrollClass = this.state.landscapeGrid ? classes.frameScrollLandscape : classes.frameScrollPortrait;
let vuMeterRClass = this.state.landscapeGrid ? classes.vuMeterRLandscape : classes.vuMeterR;
let controlNodes: ControlNodes;
let frameClass = classes.frame;
if (this.fullScreen()) {
gridClass = classes.noScrollGrid;
scrollClass = classes.frameScrollNone;
frameClass = classes.noScrollFrame;
}
let controlNodes: ControlNodes;
controlNodes = this.getStandardControlNodes(plugin, controlValues);
@@ -822,16 +858,7 @@ const PluginControlView =
if (pedalboardItem.midiChannelBinding) {
nodes.push(this.midiBindingControl(pedalboardItem));
}
return (
<div className={frameClass}>
<div className={classes.vuMeterL}>
<VuMeter displayText={true} display="input" instanceId={pedalboardItem.instanceId} />
</div>
<div className={vuMeterRClass}>
<VuMeter displayText={true} display="output" instanceId={pedalboardItem.instanceId} />
</div>
<div className={scrollClass}>
<div className={gridClass} >
{
@@ -848,6 +875,44 @@ const PluginControlView =
}
</div>
</div>
);
}
render(): ReactNode {
const classes = withStyles.getClasses(this.props);
let pedalboard = this.model.pedalboard.get();
let pedalboardItem: PedalboardItem;
if (this.props.instanceId === Pedalboard.START_CONTROL) {
pedalboardItem = pedalboard.makeStartItem();
} else if (this.props.instanceId === Pedalboard.END_CONTROL) {
pedalboardItem = pedalboard.makeEndItem();
} else {
pedalboardItem = pedalboard.getItem(this.props.instanceId);
}
let vuMeterRClass = this.state.landscapeGrid ? classes.vuMeterRLandscape : classes.vuMeterR;
let frameClass = classes.frame;
if (this.fullScreen()) {
frameClass = classes.noScrollFrame;
}
return (
<div className={frameClass}>
<div className={classes.vuMeterL}>
<VuMeter displayText={true} display="input" instanceId={pedalboardItem.instanceId} />
</div>
<div className={vuMeterRClass}>
<VuMeter displayText={true} display="output" instanceId={pedalboardItem.instanceId} />
</div>
{
this.props.showModGui && this.renderModGui()
|| this.renderPiPedalControl(pedalboardItem)
}
{this.state.showFileDialog && (
<FilePropertyDialog open={this.state.showFileDialog}
@@ -889,7 +954,7 @@ const PluginControlView =
}
/>
)}
{/* xxx: I don't think we need this anyore. */}
<FullScreenIME uiControl={this.state.imeUiControl} value={this.state.imeValue}
onChange={(key, value) => this.onImeValueChange(key, value)}
+91
View File
@@ -0,0 +1,91 @@
// Copyright (c) 2022 Robin Davies
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
import { Theme } from '@mui/material/styles';
import WithStyles from './WithStyles';
import { withStyles } from "tss-react/mui";
import {createStyles} from './WithStyles';
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
const styles = (theme: Theme) => createStyles({
pgraph: {
paddingBottom: 16
}
});
export interface ScratchClassProps extends WithStyles<typeof styles> {
}
export interface ScratchClassState {
screenWidth: number,
screenHeight: number
}
const ScratchClass = withStyles(
class extends ResizeResponsiveComponent<ScratchClassProps, ScratchClassState> {
private model: PiPedalModel;
constructor(props: ScratchClassProps) {
super(props);
this.model = PiPedalModelFactory.getInstance();
void this.model; // suppress unused variable warning
this.state = {
screenWidth: this.windowSize.width,
screenHeight: this.windowSize.height
};
}
mounted: boolean = false;
onWindowSizeChanged(width: number, height: number): void {
this.setState({ screenWidth: width, screenHeight: height });
}
componentDidMount() {
super.componentDidMount();
this.mounted = true;
}
componentWillUnmount() {
this.mounted = false;
super.componentWillUnmount();
}
componentDidUpdate(prevProps: ScratchClassProps) {
}
render() {
const classes = withStyles.getClasses(this.props);
void classes; // suppress unused variable warning
return (<div/>);
}
},
styles);
export default ScratchClass;
+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="24"
height="24"
viewBox="0 0 6.3499999 6.35"
version="1.1"
id="svg5"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<g
id="layer1">
<path
style="fill-rule:evenodd;-inkscape-stroke:none"
d="m 2.2089844,0.26367188 c -0.564979,0 -1.0234375,0.46978477 -1.0234375,1.03710932 v 3.7578125 c 0,0.5673246 0.4584585,1.0371094 1.0234375,1.0371094 H 4.140625 c 0.564979,0 1.0234375,-0.4697848 1.0234375,-1.0371094 V 1.3007812 c 0,-0.56732455 -0.4584585,-1.03710932 -1.0234375,-1.03710932 z m 0,0.52929687 H 4.140625 c 0.276179,0 0.4941406,0.21977775 0.4941406,0.50781245 v 3.7578125 c 0,0.2880348 -0.2179616,0.5078125 -0.4941406,0.5078125 H 2.2089844 c -0.276179,0 -0.4941406,-0.2197777 -0.4941407,-0.5078125 V 1.3007812 c 0,-0.2880347 0.2179617,-0.50781245 0.4941407,-0.50781245 z"
id="rect234" />
<path
style="-inkscape-stroke:none"
d="m 3.1757813,3.0429688 c -0.4352418,0 -0.7949219,0.357727 -0.7949219,0.7929687 0,0.4352417 0.3596801,0.7949219 0.7949219,0.7949219 0.4352417,0 0.7929687,-0.3596802 0.7929687,-0.7949219 0,-0.4352417 -0.357727,-0.7929687 -0.7929687,-0.7929687 z m 0,0.5292968 c 0.1492592,0 0.2636718,0.1144129 0.2636718,0.2636719 0,0.149259 -0.1144126,0.265625 -0.2636718,0.265625 -0.1492593,0 -0.2656251,-0.116366 -0.265625,-0.265625 0,-0.149259 0.1163657,-0.2636719 0.265625,-0.2636719 z"
id="path2914" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="24"
height="24"
viewBox="0 0 6.3499999 6.35"
version="1.1"
id="svg5"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<g
id="layer1">
<path
id="path3692"
d="M 1.7218342,0.53148711 C 1.03731,0.54023357 0.48004958,1.102787 0.48004958,1.7892914 c 0,0.69191 0.56589442,1.2578044 1.25780432,1.2578044 0.69191,0 1.2598715,-0.5658944 1.2598715,-1.2578044 0,-0.5024751 -0.2996529,-0.9383787 -0.7286377,-1.13946526 L 2.1460978,1.1831269 c 0.1950392,0.1300488 0.3224609,0.3518001 0.3224609,0.6061645 0,0.4059278 -0.324777,0.7286377 -0.7307048,0.7286377 -0.4059278,0 -0.7286376,-0.3227099 -0.7286376,-0.7286377 0,-0.3691252 0.2670204,-0.669466 0.6206339,-0.7208862 L 1.4810221,1.7143606 1.7388875,1.7737885 1.8990844,1.0782237 2.0174234,0.56300973 c -0.0053,-0.001214 -0.010694,-0.002471 -0.01602,-0.003617 -0.00604,-0.001301 -0.012021,-0.002404 -0.018087,-0.003617 -0.00465,-9.2974e-4 -0.00928,-0.001706 -0.013953,-0.002584 -0.0066,-0.001242 -0.013523,-0.002479 -0.020154,-0.003617 -0.00411,-7.0267e-4 -0.00828,-0.001405 -0.012402,-0.002067 -0.00736,-0.001187 -0.01482,-0.00256 -0.022221,-0.003617 -8.711e-4,-1.2398e-4 -0.00171,-3.9459e-4 -0.00258,-5.1676e-4 -0.010646,-0.001498 -0.021313,-0.002391 -0.032039,-0.003617 -0.00292,-3.3196e-4 -0.00586,-7.218e-4 -0.00879,-0.001034 -0.00879,-9.4149e-4 -0.017517,-0.001827 -0.026355,-0.002584 -0.00221,-1.8873e-4 -0.0045,-3.3967e-4 -0.00672,-5.1677e-4 -0.00954,-7.6485e-4 -0.019336,-0.00152 -0.028939,-0.002067 -0.00717,-4.0808e-4 -0.014505,-7.4831e-4 -0.021704,-0.001034 -0.00474,-1.8774e-4 -0.0092,-3.827e-4 -0.013953,-5.1677e-4 -0.00657,-1.8642e-4 -0.013046,-4.3271e-4 -0.019637,-5.1676e-4 -0.00537,-6.855e-5 -0.010629,0 -0.01602,0 -0.00541,0 -0.01063,-6.887e-5 -0.01602,0 z" />
<path
id="path3861"
d="m 4.23951,2.741472 c -0.6845242,0.00875 -1.2417846,0.5712999 -1.2417846,1.2578043 0,0.69191 0.5658944,1.2578044 1.2578043,1.2578044 0.69191,0 1.2598715,-0.5658944 1.2598715,-1.2578044 0,-0.5024751 -0.2996529,-0.9383787 -0.7286377,-1.1394653 L 4.6637736,3.3931118 c 0.1950392,0.1300488 0.3224609,0.3518001 0.3224609,0.6061645 0,0.4059278 -0.324777,0.7286377 -0.7307048,0.7286377 -0.4059278,0 -0.7286377,-0.3227099 -0.7286377,-0.7286377 0,-0.3691252 0.2670204,-0.669466 0.620634,-0.7208862 l -0.1488281,0.6459554 0.2578654,0.059428 0.1601969,-0.6955648 0.118339,-0.515214 c -0.0053,-0.00121 -0.010694,-0.00247 -0.01602,-0.00362 -0.00604,-0.0013 -0.012021,-0.0024 -0.018087,-0.00362 -0.00465,-9.297e-4 -0.00928,-0.00171 -0.013953,-0.00258 -0.0066,-0.00124 -0.013523,-0.00248 -0.020154,-0.00362 -0.00411,-7.027e-4 -0.00828,-0.0014 -0.012402,-0.00207 -0.00736,-0.00119 -0.01482,-0.00256 -0.022221,-0.00362 -8.711e-4,-1.24e-4 -0.00171,-3.946e-4 -0.00258,-5.168e-4 -0.010646,-0.0015 -0.021313,-0.00239 -0.032039,-0.00362 -0.00292,-3.319e-4 -0.00586,-7.218e-4 -0.00878,-0.00103 -0.00879,-9.415e-4 -0.017517,-0.00183 -0.026355,-0.00258 -0.00221,-1.887e-4 -0.0045,-3.397e-4 -0.00672,-5.168e-4 -0.00954,-7.648e-4 -0.019336,-0.00152 -0.028939,-0.00207 -0.00717,-4.081e-4 -0.014505,-7.483e-4 -0.021704,-0.00103 -0.00474,-1.877e-4 -0.0092,-3.827e-4 -0.013953,-5.167e-4 -0.00657,-1.864e-4 -0.013046,-4.327e-4 -0.019637,-5.168e-4 -0.00537,-6.85e-5 -0.010629,0 -0.01602,0 -0.00541,0 -0.01063,-6.89e-5 -0.01602,0 z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

+8
View File
@@ -8,4 +8,12 @@ export default defineConfig({
chunkSizeWarningLimit: 2000
},
plugins: [react(),svgr()],
server: {
proxy: {
'/resources': {
target: 'http://localhost:8080',
changeOrigin: false,
},
}
}
})