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
+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;
};
}