Checkpoint
@@ -103,7 +103,7 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
// Resolved by CMake Tools:
|
// Resolved by CMake Tools:
|
||||||
"program": "${command:cmake.launchTargetPath}",
|
"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,
|
"stopAtEntry": false,
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
"environment": [
|
"environment": [
|
||||||
@@ -142,7 +142,8 @@
|
|||||||
//"[pipedal_alsa_test]"
|
//"[pipedal_alsa_test]"
|
||||||
// "[wifi_channels_test]"
|
// "[wifi_channels_test]"
|
||||||
// "[locale]"
|
// "[locale]"
|
||||||
"[pipewire_input_stream]"
|
//"[mod_gui_init]"
|
||||||
|
"[mod_gui_templates]"
|
||||||
],
|
],
|
||||||
|
|
||||||
"stopAtEntry": false,
|
"stopAtEntry": false,
|
||||||
|
|||||||
@@ -161,7 +161,7 @@
|
|||||||
"cSpell.ignoreWords": [
|
"cSpell.ignoreWords": [
|
||||||
"nammodel"
|
"nammodel"
|
||||||
],
|
],
|
||||||
"cSpell.enabled": true,
|
"cSpell.enabled": false,
|
||||||
|
|
||||||
// Disable all automatic completion suggestions - only show when Ctrl+Space is pressed
|
// Disable all automatic completion suggestions - only show when Ctrl+Space is pressed
|
||||||
"editor.quickSuggestions": {
|
"editor.quickSuggestions": {
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ DBusDispatcher::PostHandle DBusDispatcher::PostDelayed(const clock::duration &de
|
|||||||
void DBusDispatcher::Run()
|
void DBusDispatcher::Run()
|
||||||
{
|
{
|
||||||
this->eventFd = eventfd(0, 0);
|
this->eventFd = eventfd(0, 0);
|
||||||
|
threadStarted = true;
|
||||||
this->serviceThread = std::thread(
|
this->serviceThread = std::thread(
|
||||||
[this]()
|
[this]()
|
||||||
{ this->ThreadProc(); });
|
{ this->ThreadProc(); });
|
||||||
@@ -169,7 +170,7 @@ void DBusDispatcher::ThreadProc()
|
|||||||
int eventTimeout = GetEventTimeoutMs();
|
int eventTimeout = GetEventTimeoutMs();
|
||||||
if (eventTimeout != -1 && eventTimeout < pollTimeout)
|
if (eventTimeout != -1 && eventTimeout < pollTimeout)
|
||||||
{
|
{
|
||||||
pollTimeout = eventTimeout;/*+-*/
|
pollTimeout = eventTimeout; /*+-*/
|
||||||
}
|
}
|
||||||
if (pollTimeout > 250 || pollTimeout == -1)
|
if (pollTimeout > 250 || pollTimeout == -1)
|
||||||
{
|
{
|
||||||
@@ -197,13 +198,13 @@ void DBusDispatcher::ThreadProc()
|
|||||||
LogTrace("DBusDispatcher", "ThreadProc", "Service thread terminated.");
|
LogTrace("DBusDispatcher", "ThreadProc", "Service thread terminated.");
|
||||||
}
|
}
|
||||||
|
|
||||||
void DBusDispatcher::Wait()
|
void DBusDispatcher::WaitForClose()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!threadJoined)
|
if (threadStarted)
|
||||||
{
|
{
|
||||||
threadJoined = true;
|
threadStarted = false;
|
||||||
serviceThread.join();
|
serviceThread.join();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,7 +230,7 @@ void DBusDispatcher::Stop()
|
|||||||
|
|
||||||
this->stopping = true;
|
this->stopping = true;
|
||||||
WakeThread();
|
WakeThread();
|
||||||
Wait();
|
WaitForClose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public:
|
|||||||
void Run();
|
void Run();
|
||||||
// Stop everything, shutting down gracefully.
|
// Stop everything, shutting down gracefully.
|
||||||
void Stop();
|
void Stop();
|
||||||
void Wait();
|
void WaitForClose();
|
||||||
bool IsFinished();
|
bool IsFinished();
|
||||||
|
|
||||||
// Stop, but don't wait. Suitable for use in a signal handler.
|
// Stop, but don't wait. Suitable for use in a signal handler.
|
||||||
@@ -78,7 +78,7 @@ private:
|
|||||||
std::atomic<bool> stopping;
|
std::atomic<bool> stopping;
|
||||||
|
|
||||||
std::vector<CallbackEntry> postedEvents;
|
std::vector<CallbackEntry> postedEvents;
|
||||||
bool threadJoined;
|
bool threadStarted;
|
||||||
std::thread serviceThread;
|
std::thread serviceThread;
|
||||||
std::mutex postMutex;
|
std::mutex postMutex;
|
||||||
};
|
};
|
||||||
@@ -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 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 char*pStart, const char *pEnd, bool isQuerySegment = false);
|
||||||
static void encode_url_segment(std::ostream&os, const std::string&segment, bool isQuerySegment = false)
|
static void encode_url_segment(std::ostream&os, const std::string&segment, bool isQuerySegment = false)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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 |
@@ -206,6 +206,9 @@ else()
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
set (PIPEDAL_SOURCES
|
set (PIPEDAL_SOURCES
|
||||||
|
ModTemplateGenerator.cpp ModTemplateGenerator.hpp
|
||||||
|
WebServerMod.cpp WebServerMod.hpp
|
||||||
|
ModGui.cpp ModGui.hpp
|
||||||
PipewireInputStream.cpp PipewireInputStream.hpp
|
PipewireInputStream.cpp PipewireInputStream.hpp
|
||||||
AudioFiles.cpp AudioFiles.hpp
|
AudioFiles.cpp AudioFiles.hpp
|
||||||
AudioFileMetadataReader.cpp AudioFileMetadataReader.hpp
|
AudioFileMetadataReader.cpp AudioFileMetadataReader.hpp
|
||||||
@@ -414,6 +417,8 @@ add_executable(AuxInTest
|
|||||||
|
|
||||||
add_executable(pipedaltest
|
add_executable(pipedaltest
|
||||||
testMain.cpp
|
testMain.cpp
|
||||||
|
|
||||||
|
ModGuiTest.cpp
|
||||||
PipewireInputStreamTest.cpp
|
PipewireInputStreamTest.cpp
|
||||||
|
|
||||||
AudioFilesTest.cpp
|
AudioFilesTest.cpp
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ TEST_CASE( "PluginHost memory leak", "[lv2host_leak][Build][Dev]" ) {
|
|||||||
{
|
{
|
||||||
PluginHost host;
|
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();
|
MemStats finalMemory = GetMemStats();
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#include "ss.hpp"
|
#include "ss.hpp"
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include "util.hpp"
|
||||||
|
|
||||||
using namespace pipedal;
|
using namespace pipedal;
|
||||||
|
|
||||||
@@ -30,7 +31,16 @@ static std::string empty;
|
|||||||
const std::string& MimeTypes::MimeTypeFromExtension(const std::string &extension) const
|
const std::string& MimeTypes::MimeTypeFromExtension(const std::string &extension) const
|
||||||
{
|
{
|
||||||
auto iter = extensionToMimeType.find(extension);
|
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;
|
return iter->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ ModFileTypes::ModFileTypes(const std::string &fileTypes)
|
|||||||
if (wellKnownType)
|
if (wellKnownType)
|
||||||
{
|
{
|
||||||
rootDirectories_.push_back(type);
|
rootDirectories_.push_back(type);
|
||||||
|
modFileTypes_.push_back(type);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -32,12 +32,14 @@ namespace pipedal
|
|||||||
ModFileTypes(const std::string &fileTypes);
|
ModFileTypes(const std::string &fileTypes);
|
||||||
const std::vector<std::string> &rootDirectories() const { return rootDirectories_; }
|
const std::vector<std::string> &rootDirectories() const { return rootDirectories_; }
|
||||||
std::vector<std::string> &rootDirectories() { 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_; }
|
const std::vector<std::string> &fileTypes() const { return fileTypes_; }
|
||||||
|
|
||||||
static constexpr const char *DEFAULT_FILE_TYPES = "audio,nammodel,mlmodel,sf2,sfz,midisong,midiclip,*"; // (everything)
|
static constexpr const char *DEFAULT_FILE_TYPES = "audio,nammodel,mlmodel,sf2,sfz,midisong,midiclip,*"; // (everything)
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::vector<std::string> rootDirectories_;
|
std::vector<std::string> rootDirectories_;
|
||||||
|
std::vector<std::string> modFileTypes_; // e.g. "audio", "nammodel", etc.
|
||||||
std::vector<std::string> fileTypes_;
|
std::vector<std::string> fileTypes_;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
@@ -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
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -242,7 +242,7 @@ void PiPedalModel::LoadLv2PluginInfo()
|
|||||||
}
|
}
|
||||||
|
|
||||||
pluginChangeMonitor = std::make_unique<Lv2PluginChangeMonitor>(*this);
|
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
|
// Copy all presets out of Lilv data to json files
|
||||||
// so that we can close lilv while we're actually
|
// so that we can close lilv while we're actually
|
||||||
|
|||||||
@@ -331,7 +331,9 @@ namespace pipedal
|
|||||||
void LoadLv2PluginInfo();
|
void LoadLv2PluginInfo();
|
||||||
void Load();
|
void Load();
|
||||||
|
|
||||||
const PluginHost &GetLv2Host() const { return pluginHost; }
|
const PluginHost &GetPluginHost() const { return pluginHost; }
|
||||||
|
PluginHost &GetPluginHost() { return pluginHost; }
|
||||||
|
|
||||||
Pedalboard GetCurrentPedalboardCopy()
|
Pedalboard GetCurrentPedalboardCopy()
|
||||||
{
|
{
|
||||||
std::lock_guard<std::recursive_mutex> guard(mutex);
|
std::lock_guard<std::recursive_mutex> guard(mutex);
|
||||||
|
|||||||
@@ -1331,12 +1331,12 @@ public:
|
|||||||
}
|
}
|
||||||
else if (message == "plugins")
|
else if (message == "plugins")
|
||||||
{
|
{
|
||||||
auto ui_plugins = model.GetLv2Host().GetUiPlugins();
|
auto ui_plugins = model.GetPluginHost().GetUiPlugins();
|
||||||
Reply(replyTo, "plugins", ui_plugins);
|
Reply(replyTo, "plugins", ui_plugins);
|
||||||
}
|
}
|
||||||
else if (message == "pluginClasses")
|
else if (message == "pluginClasses")
|
||||||
{
|
{
|
||||||
auto classes = model.GetLv2Host().GetLv2PluginClass();
|
auto classes = model.GetPluginHost().GetLv2PluginClass();
|
||||||
Reply(replyTo, "pluginClasses", classes);
|
Reply(replyTo, "pluginClasses", classes);
|
||||||
}
|
}
|
||||||
else if (message == "hello")
|
else if (message == "hello")
|
||||||
|
|||||||
@@ -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
|
// 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
|
// this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -136,8 +136,11 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld)
|
|||||||
|
|
||||||
lv2core__symbol = lilv_new_uri(pWorld, LV2_CORE__symbol);
|
lv2core__symbol = lilv_new_uri(pWorld, LV2_CORE__symbol);
|
||||||
lv2core__name = lilv_new_uri(pWorld, LV2_CORE__name);
|
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__index = lilv_new_uri(pWorld, LV2_CORE__index);
|
||||||
lv2core__Parameter = lilv_new_uri(pWorld, LV2_CORE_PREFIX "Parameter");
|
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__ui = lilv_new_uri(pWorld, PIPEDAL_UI__ui);
|
||||||
pipedalUI__fileProperties = lilv_new_uri(pWorld, PIPEDAL_UI__fileProperties);
|
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__mimeType = lilv_new_uri(pWorld, PIPEDAL_UI__mimeType);
|
||||||
pipedalUI__outputPorts = lilv_new_uri(pWorld, PIPEDAL_UI__outputPorts);
|
pipedalUI__outputPorts = lilv_new_uri(pWorld, PIPEDAL_UI__outputPorts);
|
||||||
pipedalUI__text = lilv_new_uri(pWorld, PIPEDAL_UI__text);
|
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__frequencyPlot = lilv_new_uri(pWorld, PIPEDAL_UI__frequencyPlot);
|
||||||
pipedalUI__xLeft = lilv_new_uri(pWorld, PIPEDAL_UI__xLeft);
|
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__yBottom = lilv_new_uri(pWorld, PIPEDAL_UI__yBottom);
|
||||||
pipedalUI__xLog = lilv_new_uri(pWorld, PIPEDAL_UI__xLog);
|
pipedalUI__xLog = lilv_new_uri(pWorld, PIPEDAL_UI__xLog);
|
||||||
pipedalUI__width = lilv_new_uri(pWorld, PIPEDAL_UI__width);
|
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__portNotification = lilv_new_uri(pWorld, LV2_UI__portNotification);
|
||||||
ui__plugin = lilv_new_uri(pWorld, LV2_UI__plugin);
|
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);
|
lv2__port = lilv_new_uri(pWorld, LV2_CORE__port);
|
||||||
|
|
||||||
#define MOD_PREFIX "http://moddevices.com/ns/mod#"
|
#define MOD_PREFIX "http://moddevices.com/ns/mod#"
|
||||||
|
|
||||||
mod__label = lilv_new_uri(pWorld, MOD_PREFIX "label");
|
mod__label = lilv_new_uri(pWorld, MOD_PREFIX "label");
|
||||||
mod__brand = lilv_new_uri(pWorld, MOD_PREFIX "brand");
|
mod__brand = lilv_new_uri(pWorld, MOD_PREFIX "brand");
|
||||||
mod__preferMomentaryOffByDefault = lilv_new_uri(pWorld, MOD_PREFIX "preferMomentaryOffByDefault");
|
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__writable = lilv_new_uri(pWorld, LV2_PATCH__writable);
|
||||||
patch__readable = lilv_new_uri(pWorld, LV2_PATCH__readable);
|
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");
|
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__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()
|
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)
|
static std::string nodeAsString(const LilvNode *node)
|
||||||
{
|
{
|
||||||
@@ -270,7 +297,6 @@ PluginHost::PluginHost()
|
|||||||
fileMetadataFeature.Prepare(mapFeature);
|
fileMetadataFeature.Prepare(mapFeature);
|
||||||
lv2Features.push_back(fileMetadataFeature.GetFeature());
|
lv2Features.push_back(fileMetadataFeature.GetFeature());
|
||||||
|
|
||||||
|
|
||||||
lv2Features.push_back(nullptr);
|
lv2Features.push_back(nullptr);
|
||||||
|
|
||||||
this->urids = new Urids(mapFeature);
|
this->urids = new Urids(mapFeature);
|
||||||
@@ -294,8 +320,11 @@ PluginHost::~PluginHost()
|
|||||||
{
|
{
|
||||||
delete lilvUris;
|
delete lilvUris;
|
||||||
lilvUris = nullptr;
|
lilvUris = nullptr;
|
||||||
|
|
||||||
delete urids;
|
delete urids;
|
||||||
urids = nullptr;
|
urids = nullptr;
|
||||||
|
delete mod_gui_uris;
|
||||||
|
mod_gui_uris = nullptr;
|
||||||
free_world();
|
free_world();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,7 +426,7 @@ void PluginHost::LoadPluginClassesFromLilv()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginHost::Load(const char *lv2Path)
|
void PluginHost::LoadLilv(const char *lv2Path)
|
||||||
{
|
{
|
||||||
|
|
||||||
this->plugins_.clear();
|
this->plugins_.clear();
|
||||||
@@ -418,6 +447,9 @@ void PluginHost::Load(const char *lv2Path)
|
|||||||
|
|
||||||
lilv_world_load_all(pWorld);
|
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);
|
// LilvNode*lv2_path = lilv_new_file_uri(pWorld,NULL,lv2Path);
|
||||||
// lilv_world_set_option(world,LILV_OPTION_LV2_PATH,lv)
|
// lilv_world_set_option(world,LILV_OPTION_LV2_PATH,lv)
|
||||||
|
|
||||||
@@ -439,7 +471,8 @@ void PluginHost::Load(const char *lv2Path)
|
|||||||
if (pluginInfo->hasCvPorts())
|
if (pluginInfo->hasCvPorts())
|
||||||
{
|
{
|
||||||
Lv2Log::debug("Plugin %s (%s) skipped. (Has CV ports).", pluginInfo->name().c_str(), pluginInfo->uri().c_str());
|
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());
|
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 (plugin->is_valid())
|
||||||
{
|
{
|
||||||
#if 1
|
#if 1
|
||||||
// no plugins with more than 2 inputs or outputs.
|
// no plugins with more than 2 inputs or outputs.
|
||||||
// no zero-input or zero-output plugins (temporarily disables midi plugins)
|
// no zero-input or zero-output plugins (temporarily disables midi plugins)
|
||||||
// no zero output devices (permanent, I think)
|
// no zero output devices (permanent, I think)
|
||||||
if (info.audio_inputs() > 2 || info.audio_outputs() > 2) {
|
if (info.audio_inputs() > 2 || info.audio_outputs() > 2)
|
||||||
|
{
|
||||||
Lv2Log::debug(
|
Lv2Log::debug(
|
||||||
"Plugin %s (%s) skipped. %d inputs, %d outputs.", plugin->name().c_str(), plugin->uri().c_str(),
|
"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());
|
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.
|
// temporarily disable this feature.
|
||||||
Lv2Log::debug("Plugin %s (%s) skipped. No inputs.", plugin->name().c_str(), plugin->uri().c_str());
|
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
|
#endif
|
||||||
else
|
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());
|
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));
|
ui_plugins_.push_back(std::move(info));
|
||||||
@@ -619,7 +653,7 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
|
|||||||
// example:
|
// example:
|
||||||
|
|
||||||
// <http://github.com/mikeoliphant/neural-amp-modeler-lv2#model>
|
// <http://github.com/mikeoliphant/neural-amp-modeler-lv2#model>
|
||||||
// a lv2:Parameter;
|
// a lv2:Parameter;
|
||||||
// rdfs:label "Model";
|
// rdfs:label "Model";
|
||||||
// rdfs:range atom:Path.
|
// 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))
|
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);
|
AutoLilvNode label = lilv_world_get(pWorld, propertyUri, lv2Host->lilvUris->rdfs__label, nullptr);
|
||||||
std::string strLabel = label.AsString();
|
std::string strLabel = label.AsString();
|
||||||
|
|
||||||
@@ -662,19 +695,16 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
|
|||||||
std::make_shared<UiFileProperty>(
|
std::make_shared<UiFileProperty>(
|
||||||
strLabel, propertyUri.AsUri(), lv2DirectoryName);
|
strLabel, propertyUri.AsUri(), lv2DirectoryName);
|
||||||
|
|
||||||
|
|
||||||
AutoLilvNode indexNode = lilv_world_get(pWorld, propertyUri, lv2Host->lilvUris->lv2core__index, nullptr);
|
AutoLilvNode indexNode = lilv_world_get(pWorld, propertyUri, lv2Host->lilvUris->lv2core__index, nullptr);
|
||||||
int32_t index = indexNode.AsInt(-1);
|
int32_t index = indexNode.AsInt(-1);
|
||||||
fileProperty->index(index);
|
fileProperty->index(index);
|
||||||
|
|
||||||
|
|
||||||
// if there's a pipedalui_fileTypes node, use that instead.
|
// if there's a pipedalui_fileTypes node, use that instead.
|
||||||
|
|
||||||
|
|
||||||
AutoLilvNode mod__fileTypes = lilv_world_get(pWorld, propertyUri, lv2Host->lilvUris->mod__fileTypes, nullptr);
|
AutoLilvNode mod__fileTypes = lilv_world_get(pWorld, propertyUri, lv2Host->lilvUris->mod__fileTypes, nullptr);
|
||||||
|
|
||||||
// default: everything.
|
// default: everything.
|
||||||
std::string fileTypes = ModFileTypes::DEFAULT_FILE_TYPES;
|
std::string fileTypes = ModFileTypes::DEFAULT_FILE_TYPES;
|
||||||
if (mod__fileTypes)
|
if (mod__fileTypes)
|
||||||
{
|
{
|
||||||
// "nam,nammodel"
|
// "nam,nammodel"
|
||||||
@@ -682,8 +712,6 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
|
|||||||
}
|
}
|
||||||
ModFileTypes modFileTypes(fileTypes);
|
ModFileTypes modFileTypes(fileTypes);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Legacy case: audio_uploads/<plugin_directory> exists.
|
// Legacy case: audio_uploads/<plugin_directory> exists.
|
||||||
|
|
||||||
std::filesystem::path bundleDirectoryName = std::filesystem::path(bundle_path()).parent_path().filename();
|
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);
|
fileProperty->directory(bundleDirectoryName);
|
||||||
|
|
||||||
if (std::filesystem::exists(legacyUploadPath)
|
if (std::filesystem::exists(legacyUploadPath) && !std::filesystem::exists(legacyUploadPath / ".migrated"))
|
||||||
&& !std::filesystem::exists(legacyUploadPath / ".migrated"))
|
|
||||||
{
|
{
|
||||||
fileProperty->useLegacyModDirectory(true);
|
fileProperty->useLegacyModDirectory(true);
|
||||||
fileProperty->directory(bundleDirectoryName);
|
fileProperty->directory(bundleDirectoryName);
|
||||||
modFileTypes.rootDirectories().push_back(bundleDirectoryName.filename()); // push the private directory!
|
modFileTypes.rootDirectories().push_back(bundleDirectoryName.filename()); // push the private directory!
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
if (modFileTypes.rootDirectories().size() == 1)
|
if (modFileTypes.rootDirectories().size() == 1)
|
||||||
{
|
{
|
||||||
std::string modName = modFileTypes.rootDirectories()[0];
|
std::string modName = modFileTypes.rootDirectories()[0];
|
||||||
@@ -708,9 +737,10 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fileProperties.push_back(fileProperty);
|
fileProperties.push_back(fileProperty);
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
unsupportedPatchProperty = true;
|
unsupportedPatchProperty = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -718,24 +748,25 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
|
|||||||
}
|
}
|
||||||
FindWritablePathPropertiesResult result;
|
FindWritablePathPropertiesResult result;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (fileProperties.size() != 0)
|
if (fileProperties.size() != 0)
|
||||||
{
|
{
|
||||||
std::sort(fileProperties.begin(),fileProperties.end(),[](const UiFileProperty::ptr& left,const UiFileProperty::ptr&right) {
|
std::sort(fileProperties.begin(), fileProperties.end(), [](const UiFileProperty::ptr &left, const UiFileProperty::ptr &right)
|
||||||
// properies with indexes first.
|
{
|
||||||
int32_t indexL = left->index();
|
// properies with indexes first.
|
||||||
if (indexL < 0) indexL = std::numeric_limits<int32_t>::max();
|
int32_t indexL = left->index();
|
||||||
int32_t indexR = right->index();
|
if (indexL < 0)
|
||||||
if (indexR < 0) indexR = std::numeric_limits<int32_t>::max();
|
indexL = std::numeric_limits<int32_t>::max();
|
||||||
if (indexL < indexR) return true;
|
int32_t indexR = right->index();
|
||||||
if (indexL > indexR) return false;
|
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.
|
// 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.
|
// We can't order them sensibly. Let's at least order them consistently.
|
||||||
return left->label() < right->label();
|
return left->label() < right->label(); });
|
||||||
|
|
||||||
});
|
|
||||||
result.pipedalUi = std::make_shared<PiPedalUI>(std::move(fileProperties));
|
result.pipedalUi = std::make_shared<PiPedalUI>(std::move(fileProperties));
|
||||||
}
|
}
|
||||||
result.hasUnsupportedPatchProperties = unsupportedPatchProperty;
|
result.hasUnsupportedPatchProperties = unsupportedPatchProperty;
|
||||||
@@ -764,6 +795,12 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP
|
|||||||
AutoLilvNode name = (lilv_plugin_get_name(pPlugin));
|
AutoLilvNode name = (lilv_plugin_get_name(pPlugin));
|
||||||
this->name_ = nodeAsString(name);
|
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);
|
AutoLilvNode brand = lilv_world_get(pWorld, plugUri, lv2Host->lilvUris->mod__brand, nullptr);
|
||||||
this->brand_ = nodeAsString(brand);
|
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);
|
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
|
// Fetch pipedal plugin UI
|
||||||
|
|
||||||
@@ -886,7 +976,7 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP
|
|||||||
++nOutputs;
|
++nOutputs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this->modGui_ = ModGui::Create(lv2Host, pPlugin);
|
||||||
this->is_valid_ = isValid;
|
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->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_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->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->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->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->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->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);
|
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)
|
if (port_ledColor)
|
||||||
{
|
{
|
||||||
auto value = lilv_node_as_string(port_ledColor);
|
auto value = lilv_node_as_string(port_ledColor);
|
||||||
if (value) {
|
if (value)
|
||||||
|
{
|
||||||
this->pipedal_ledColor_ = 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)
|
Lv2PluginUiInfo::Lv2PluginUiInfo(PluginHost *pHost, const Lv2PluginInfo *plugin)
|
||||||
: uri_(plugin->uri()),
|
: uri_(plugin->uri()),
|
||||||
name_(plugin->name()),
|
name_(plugin->name()),
|
||||||
|
minorVersion_(plugin->minorVersion()),
|
||||||
|
microVersion_(plugin->microVersion()),
|
||||||
brand_(plugin->brand()),
|
brand_(plugin->brand()),
|
||||||
label_(plugin->label()),
|
label_(plugin->label()),
|
||||||
author_name_(plugin->author_name()),
|
author_name_(plugin->author_name()),
|
||||||
@@ -1173,7 +1264,9 @@ Lv2PluginUiInfo::Lv2PluginUiInfo(PluginHost *pHost, const Lv2PluginInfo *plugin)
|
|||||||
audio_outputs_(0),
|
audio_outputs_(0),
|
||||||
description_(plugin->comment()),
|
description_(plugin->comment()),
|
||||||
has_midi_input_(false),
|
has_midi_input_(false),
|
||||||
has_midi_output_(false)
|
has_midi_output_(false),
|
||||||
|
modGui_(plugin->modGui()),
|
||||||
|
patchProperties_(plugin->patchProperties())
|
||||||
{
|
{
|
||||||
PLUGIN_MAP_CHECK();
|
PLUGIN_MAP_CHECK();
|
||||||
auto pluginClass = pHost->GetPluginClass(plugin->plugin_class());
|
auto pluginClass = pHost->GetPluginClass(plugin->plugin_class());
|
||||||
@@ -1253,7 +1346,7 @@ std::shared_ptr<Lv2PluginInfo> PluginHost::GetPluginInfo(const std::string &uri)
|
|||||||
return ff->second;
|
return ff->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
Lv2Pedalboard *PluginHost::UpdateLv2PedalboardStructure(Pedalboard &pedalboard,Lv2Pedalboard *existingPedalboard,Lv2PedalboardErrorList &errorList)
|
Lv2Pedalboard *PluginHost::UpdateLv2PedalboardStructure(Pedalboard &pedalboard, Lv2Pedalboard *existingPedalboard, Lv2PedalboardErrorList &errorList)
|
||||||
{
|
{
|
||||||
ExistingEffectMap existingEffects;
|
ExistingEffectMap existingEffects;
|
||||||
|
|
||||||
@@ -1261,7 +1354,8 @@ Lv2Pedalboard *PluginHost::UpdateLv2PedalboardStructure(Pedalboard &pedalboard,L
|
|||||||
{
|
{
|
||||||
for (auto &effect : existingPedalboard->GetSharedEffectList())
|
for (auto &effect : existingPedalboard->GetSharedEffectList())
|
||||||
{
|
{
|
||||||
if (effect->IsLv2Effect()) {
|
if (effect->IsLv2Effect())
|
||||||
|
{
|
||||||
existingEffects[effect->GetInstanceId()] = effect;
|
existingEffects[effect->GetInstanceId()] = effect;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1269,7 +1363,7 @@ Lv2Pedalboard *PluginHost::UpdateLv2PedalboardStructure(Pedalboard &pedalboard,L
|
|||||||
Lv2Pedalboard *pPedalboard = new Lv2Pedalboard();
|
Lv2Pedalboard *pPedalboard = new Lv2Pedalboard();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
pPedalboard->Prepare(this, pedalboard, errorList,&existingEffects);
|
pPedalboard->Prepare(this, pedalboard, errorList, &existingEffects);
|
||||||
return pPedalboard;
|
return pPedalboard;
|
||||||
}
|
}
|
||||||
catch (const std::exception &e)
|
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);
|
auto plugin = GetPluginInfo(pluginUri);
|
||||||
if (plugin) {
|
if (plugin)
|
||||||
|
{
|
||||||
|
|
||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return filePath;
|
return filePath;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginHost::CheckForResourceInitialization(const std::string &pluginUri, const std::filesystem::path &pluginUploadDirectory)
|
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;
|
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)
|
// 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::storage_type<Lv2PluginInfo> Lv2PluginInfo::jmap{{
|
||||||
json_map::reference("bundle_path", &Lv2PluginInfo::bundle_path_),
|
json_map::reference("bundle_path", &Lv2PluginInfo::bundle_path_),
|
||||||
json_map::reference("uri", &Lv2PluginInfo::uri_),
|
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("name", &Lv2PluginInfo::name_),
|
||||||
json_map::reference("brand", &Lv2PluginInfo::brand_),
|
json_map::reference("brand", &Lv2PluginInfo::brand_),
|
||||||
json_map::reference("label", &Lv2PluginInfo::label_),
|
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("port_groups", &Lv2PluginInfo::port_groups_),
|
||||||
json_map::reference("is_valid", &Lv2PluginInfo::is_valid_),
|
json_map::reference("is_valid", &Lv2PluginInfo::is_valid_),
|
||||||
json_map::reference("has_factory_presets", &Lv2PluginInfo::has_factory_presets_),
|
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{{
|
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("uri", &Lv2PluginUiInfo::uri_),
|
||||||
json_map::reference("name", &Lv2PluginUiInfo::name_),
|
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("brand", &Lv2PluginUiInfo::brand_),
|
||||||
json_map::reference("label", &Lv2PluginUiInfo::label_),
|
json_map::reference("label", &Lv2PluginUiInfo::label_),
|
||||||
json_map::enum_reference("plugin_type", &Lv2PluginUiInfo::plugin_type_, get_plugin_type_enum_converter()),
|
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("fileProperties", &Lv2PluginUiInfo::fileProperties_),
|
||||||
json_map::reference("frequencyPlots", &Lv2PluginUiInfo::frequencyPlots_),
|
json_map::reference("frequencyPlots", &Lv2PluginUiInfo::frequencyPlots_),
|
||||||
json_map::reference("uiPortNotifications", &Lv2PluginUiInfo::uiPortNotifications_),
|
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()
|
||||||
@@ -32,6 +32,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include "IHost.hpp"
|
#include "IHost.hpp"
|
||||||
#include <set>
|
#include <set>
|
||||||
|
#include "ModGui.hpp"
|
||||||
|
|
||||||
//#include "lv2.h"
|
//#include "lv2.h"
|
||||||
#include "Units.hpp"
|
#include "Units.hpp"
|
||||||
@@ -42,6 +43,7 @@
|
|||||||
#include "AutoLilvNode.hpp"
|
#include "AutoLilvNode.hpp"
|
||||||
#include "PiPedalUI.hpp"
|
#include "PiPedalUI.hpp"
|
||||||
#include "MapPathFeature.hpp"
|
#include "MapPathFeature.hpp"
|
||||||
|
#include "ModGui.hpp"
|
||||||
|
|
||||||
namespace pipedal
|
namespace pipedal
|
||||||
{
|
{
|
||||||
@@ -325,6 +327,38 @@ namespace pipedal
|
|||||||
static json_map::storage_type<Lv2PortInfo> jmap;
|
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
|
class Lv2PortGroup
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
@@ -364,6 +398,8 @@ namespace pipedal
|
|||||||
bool HasFactoryPresets(PluginHost *lv2Host, const LilvPlugin *plugin);
|
bool HasFactoryPresets(PluginHost *lv2Host, const LilvPlugin *plugin);
|
||||||
std::string bundle_path_;
|
std::string bundle_path_;
|
||||||
std::string uri_;
|
std::string uri_;
|
||||||
|
uint32_t minorVersion_ = 0;
|
||||||
|
uint32_t microVersion_ = 0;
|
||||||
std::string name_;
|
std::string name_;
|
||||||
std::string plugin_class_;
|
std::string plugin_class_;
|
||||||
std::string brand_;
|
std::string brand_;
|
||||||
@@ -380,8 +416,12 @@ namespace pipedal
|
|||||||
std::string comment_;
|
std::string comment_;
|
||||||
std::vector<std::shared_ptr<Lv2PortInfo>> ports_;
|
std::vector<std::shared_ptr<Lv2PortInfo>> ports_;
|
||||||
std::vector<std::shared_ptr<Lv2PortGroup>> port_groups_;
|
std::vector<std::shared_ptr<Lv2PortGroup>> port_groups_;
|
||||||
|
std::vector<Lv2PatchPropertyInfo> patchProperties_;
|
||||||
|
|
||||||
bool is_valid_ = false;
|
bool is_valid_ = false;
|
||||||
PiPedalUI::ptr piPedalUI_;
|
PiPedalUI::ptr piPedalUI_;
|
||||||
|
ModGui::ptr modGui_;
|
||||||
|
|
||||||
bool hasUnsupportedPatchProperties_ = false;
|
bool hasUnsupportedPatchProperties_ = false;
|
||||||
|
|
||||||
bool IsSupportedFeature(const std::string &feature) const;
|
bool IsSupportedFeature(const std::string &feature) const;
|
||||||
@@ -390,6 +430,8 @@ namespace pipedal
|
|||||||
LV2_PROPERTY_GETSET(bundle_path)
|
LV2_PROPERTY_GETSET(bundle_path)
|
||||||
LV2_PROPERTY_GETSET(uri)
|
LV2_PROPERTY_GETSET(uri)
|
||||||
LV2_PROPERTY_GETSET(name)
|
LV2_PROPERTY_GETSET(name)
|
||||||
|
LV2_PROPERTY_GETSET(minorVersion)
|
||||||
|
LV2_PROPERTY_GETSET(microVersion)
|
||||||
LV2_PROPERTY_GETSET(brand)
|
LV2_PROPERTY_GETSET(brand)
|
||||||
LV2_PROPERTY_GETSET(label)
|
LV2_PROPERTY_GETSET(label)
|
||||||
LV2_PROPERTY_GETSET(plugin_class)
|
LV2_PROPERTY_GETSET(plugin_class)
|
||||||
@@ -406,6 +448,8 @@ namespace pipedal
|
|||||||
LV2_PROPERTY_GETSET(has_factory_presets)
|
LV2_PROPERTY_GETSET(has_factory_presets)
|
||||||
LV2_PROPERTY_GETSET(piPedalUI)
|
LV2_PROPERTY_GETSET(piPedalUI)
|
||||||
LV2_PROPERTY_GETSET(hasUnsupportedPatchProperties)
|
LV2_PROPERTY_GETSET(hasUnsupportedPatchProperties)
|
||||||
|
LV2_PROPERTY_GETSET(modGui)
|
||||||
|
LV2_PROPERTY_GETSET(patchProperties)
|
||||||
|
|
||||||
const Lv2PortInfo &getPort(const std::string &symbol)
|
const Lv2PortInfo &getPort(const std::string &symbol)
|
||||||
{
|
{
|
||||||
@@ -618,6 +662,8 @@ namespace pipedal
|
|||||||
private:
|
private:
|
||||||
std::string uri_;
|
std::string uri_;
|
||||||
std::string name_;
|
std::string name_;
|
||||||
|
uint32_t minorVersion_ = 0;
|
||||||
|
uint32_t microVersion_ = 0;;
|
||||||
std::string brand_;
|
std::string brand_;
|
||||||
std::string label_;
|
std::string label_;
|
||||||
std::string author_name_;
|
std::string author_name_;
|
||||||
@@ -636,10 +682,15 @@ namespace pipedal
|
|||||||
std::vector<UiFileProperty::ptr> fileProperties_;
|
std::vector<UiFileProperty::ptr> fileProperties_;
|
||||||
std::vector<UiFrequencyPlot::ptr> frequencyPlots_;
|
std::vector<UiFrequencyPlot::ptr> frequencyPlots_;
|
||||||
std::vector<UiPortNotification::ptr> uiPortNotifications_;
|
std::vector<UiPortNotification::ptr> uiPortNotifications_;
|
||||||
|
ModGui::ptr modGui_;
|
||||||
|
std::vector<Lv2PatchPropertyInfo> patchProperties_;
|
||||||
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
LV2_PROPERTY_GETSET(uri)
|
LV2_PROPERTY_GETSET(uri)
|
||||||
LV2_PROPERTY_GETSET(name)
|
LV2_PROPERTY_GETSET(name)
|
||||||
|
LV2_PROPERTY_GETSET(minorVersion)
|
||||||
|
LV2_PROPERTY_GETSET(microVersion)
|
||||||
LV2_PROPERTY_GETSET(brand)
|
LV2_PROPERTY_GETSET(brand)
|
||||||
LV2_PROPERTY_GETSET(label)
|
LV2_PROPERTY_GETSET(label)
|
||||||
LV2_PROPERTY_GETSET(author_name)
|
LV2_PROPERTY_GETSET(author_name)
|
||||||
@@ -657,6 +708,8 @@ namespace pipedal
|
|||||||
LV2_PROPERTY_GETSET(fileProperties)
|
LV2_PROPERTY_GETSET(fileProperties)
|
||||||
LV2_PROPERTY_GETSET(frequencyPlots)
|
LV2_PROPERTY_GETSET(frequencyPlots)
|
||||||
LV2_PROPERTY_GETSET(uiPortNotifications)
|
LV2_PROPERTY_GETSET(uiPortNotifications)
|
||||||
|
LV2_PROPERTY_GETSET(modGui)
|
||||||
|
LV2_PROPERTY_GETSET(patchProperties)
|
||||||
|
|
||||||
static json_map::storage_type<Lv2PluginUiInfo> jmap;
|
static json_map::storage_type<Lv2PluginUiInfo> jmap;
|
||||||
};
|
};
|
||||||
@@ -679,6 +732,8 @@ namespace pipedal
|
|||||||
#endif
|
#endif
|
||||||
friend class pipedal::AutoLilvNode;
|
friend class pipedal::AutoLilvNode;
|
||||||
friend class pipedal::PiPedalUI;
|
friend class pipedal::PiPedalUI;
|
||||||
|
friend class PluginHostTest;
|
||||||
|
|
||||||
static const char *RDFS__comment;
|
static const char *RDFS__comment;
|
||||||
static const char *RDFS__range;
|
static const char *RDFS__range;
|
||||||
|
|
||||||
@@ -707,6 +762,7 @@ namespace pipedal
|
|||||||
AutoLilvNode invada_units__unit; // typo in invada plugins.
|
AutoLilvNode invada_units__unit; // typo in invada plugins.
|
||||||
AutoLilvNode invada_portprops__logarithmic; // typo in invada plugins.
|
AutoLilvNode invada_portprops__logarithmic; // typo in invada plugins.
|
||||||
|
|
||||||
|
|
||||||
AutoLilvNode atom__bufferType;
|
AutoLilvNode atom__bufferType;
|
||||||
AutoLilvNode atom__Path;
|
AutoLilvNode atom__Path;
|
||||||
AutoLilvNode presets__preset;
|
AutoLilvNode presets__preset;
|
||||||
@@ -714,8 +770,11 @@ namespace pipedal
|
|||||||
AutoLilvNode rdfs__label;
|
AutoLilvNode rdfs__label;
|
||||||
AutoLilvNode lv2core__symbol;
|
AutoLilvNode lv2core__symbol;
|
||||||
AutoLilvNode lv2core__name;
|
AutoLilvNode lv2core__name;
|
||||||
|
AutoLilvNode lv2core__shortName;
|
||||||
AutoLilvNode lv2core__index;
|
AutoLilvNode lv2core__index;
|
||||||
AutoLilvNode lv2core__Parameter;
|
AutoLilvNode lv2core__Parameter;
|
||||||
|
AutoLilvNode lv2core__minorVersion;
|
||||||
|
AutoLilvNode lv2core__microVersion;
|
||||||
AutoLilvNode pipedalUI__ui;
|
AutoLilvNode pipedalUI__ui;
|
||||||
AutoLilvNode pipedalUI__fileProperties;
|
AutoLilvNode pipedalUI__fileProperties;
|
||||||
AutoLilvNode pipedalUI__directory;
|
AutoLilvNode pipedalUI__directory;
|
||||||
@@ -766,6 +825,7 @@ namespace pipedal
|
|||||||
AutoLilvNode patch__readable;
|
AutoLilvNode patch__readable;
|
||||||
AutoLilvNode pipedal_patch__readable;
|
AutoLilvNode pipedal_patch__readable;
|
||||||
|
|
||||||
|
|
||||||
AutoLilvNode mod__brand;
|
AutoLilvNode mod__brand;
|
||||||
AutoLilvNode mod__label;
|
AutoLilvNode mod__label;
|
||||||
AutoLilvNode mod__preferMomentaryOffByDefault;
|
AutoLilvNode mod__preferMomentaryOffByDefault;
|
||||||
@@ -773,6 +833,7 @@ namespace pipedal
|
|||||||
AutoLilvNode dc__format;
|
AutoLilvNode dc__format;
|
||||||
|
|
||||||
AutoLilvNode mod__fileTypes;
|
AutoLilvNode mod__fileTypes;
|
||||||
|
AutoLilvNode mod__supportedExtensions;
|
||||||
AutoLilvNode pipedalui__fileTypes;
|
AutoLilvNode pipedalui__fileTypes;
|
||||||
|
|
||||||
|
|
||||||
@@ -817,6 +878,7 @@ namespace pipedal
|
|||||||
friend class Lv2PluginInfo;
|
friend class Lv2PluginInfo;
|
||||||
friend class Lv2PortInfo;
|
friend class Lv2PortInfo;
|
||||||
friend class Lv2PortGroup;
|
friend class Lv2PortGroup;
|
||||||
|
friend class ModGui;
|
||||||
|
|
||||||
std::shared_ptr<Lv2PluginClass> GetPluginClass(const LilvPluginClass *pClass);
|
std::shared_ptr<Lv2PluginClass> GetPluginClass(const LilvPluginClass *pClass);
|
||||||
std::shared_ptr<Lv2PluginClass> MakePluginClass(const LilvPluginClass *pClass);
|
std::shared_ptr<Lv2PluginClass> MakePluginClass(const LilvPluginClass *pClass);
|
||||||
@@ -924,7 +986,9 @@ namespace pipedal
|
|||||||
|
|
||||||
class Urids;
|
class Urids;
|
||||||
|
|
||||||
Urids *urids;
|
Urids *urids = nullptr;
|
||||||
|
ModGuiUris* mod_gui_uris = nullptr;
|
||||||
|
|
||||||
|
|
||||||
void OnConfigurationChanged(const JackConfiguration &configuration, const JackChannelSelection &settings);
|
void OnConfigurationChanged(const JackConfiguration &configuration, const JackChannelSelection &settings);
|
||||||
|
|
||||||
@@ -933,7 +997,7 @@ namespace pipedal
|
|||||||
|
|
||||||
std::shared_ptr<Lv2PluginClass> GetLv2PluginClass() const;
|
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_; }
|
const std::vector<Lv2PluginUiInfo> &GetUiPlugins() const { return ui_plugins_; }
|
||||||
|
|
||||||
virtual std::shared_ptr<Lv2PluginInfo> GetPluginInfo(const std::string &uri) const;
|
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";
|
static constexpr const char *DEFAULT_LV2_PATH = "/usr/lib/lv2";
|
||||||
|
|
||||||
void LoadPluginClassesFromJson(std::filesystem::path jsonFile);
|
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)
|
virtual LV2_URID GetLv2Urid(const char *uri)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -68,3 +68,4 @@ json_enum_converter<Units> *get_units_enum_converter();
|
|||||||
|
|
||||||
} // namespace.
|
} // namespace.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
#include "Ipv6Helpers.hpp"
|
#include "Ipv6Helpers.hpp"
|
||||||
#include "util.hpp"
|
#include "util.hpp"
|
||||||
#include "ofstream_synced.hpp"
|
#include "ofstream_synced.hpp"
|
||||||
|
#include "ModTemplateGenerator.hpp"
|
||||||
|
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include "WebServer.hpp"
|
#include "WebServer.hpp"
|
||||||
@@ -741,6 +742,14 @@ namespace pipedal
|
|||||||
request.set_body_file(bodyFile->Path(),bodyFile->DeleteFile());
|
request.set_body_file(bodyFile->Path(),bodyFile->DeleteFile());
|
||||||
bodyFile->Detach();
|
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 {
|
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.
|
// cast away const to do what ther request would do if it had that method.
|
||||||
request.set_body_file(path,deleteWhenDone);
|
request.set_body_file(path,deleteWhenDone);
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ public:
|
|||||||
virtual void setBody(const std::string&body) = 0;
|
virtual void setBody(const std::string&body) = 0;
|
||||||
virtual void setBodyFile(std::shared_ptr<TemporaryFile>&temporaryFile) = 0;
|
virtual void setBodyFile(std::shared_ptr<TemporaryFile>&temporaryFile) = 0;
|
||||||
virtual void setBodyFile(std::filesystem::path&path, bool deleteWhenDone) = 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;
|
virtual void keepAlive(bool value) = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
#include "AudioFiles.hpp"
|
#include "AudioFiles.hpp"
|
||||||
#include "util.hpp"
|
#include "util.hpp"
|
||||||
#include "HtmlHelper.hpp"
|
#include "HtmlHelper.hpp"
|
||||||
|
#include "WebServerMod.hpp"
|
||||||
|
|
||||||
#define OLD_PRESET_EXTENSION ".piPreset"
|
#define OLD_PRESET_EXTENSION ".piPreset"
|
||||||
#define PRESET_EXTENSION ".piPreset"
|
#define PRESET_EXTENSION ".piPreset"
|
||||||
@@ -249,7 +250,7 @@ public:
|
|||||||
void GetPluginPresets(const uri &request_uri, std::string *pName, std::string *pContent)
|
void GetPluginPresets(const uri &request_uri, std::string *pName, std::string *pContent)
|
||||||
{
|
{
|
||||||
std::string pluginUri = request_uri.query("id");
|
std::string pluginUri = request_uri.query("id");
|
||||||
auto plugin = model->GetLv2Host().GetPluginInfo(pluginUri);
|
auto plugin = model->GetPluginHost().GetPluginInfo(pluginUri);
|
||||||
*pName = plugin->name();
|
*pName = plugin->name();
|
||||||
|
|
||||||
PluginPresets pluginPresets = model->GetPluginPresets(pluginUri);
|
PluginPresets pluginPresets = model->GetPluginPresets(pluginUri);
|
||||||
@@ -1102,4 +1103,7 @@ void pipedal::ConfigureWebServer(
|
|||||||
|
|
||||||
std::shared_ptr<DownloadIntercept> downloadIntercept = std::make_shared<DownloadIntercept>(&model);
|
std::shared_ptr<DownloadIntercept> downloadIntercept = std::make_shared<DownloadIntercept>(&model);
|
||||||
server.AddRequestHandler(downloadIntercept);
|
server.AddRequestHandler(downloadIntercept);
|
||||||
|
|
||||||
|
std::shared_ptr<ModWebIntercept> modWebIntercept = ModWebIntercept::Create(&model);
|
||||||
|
server.AddRequestHandler(modWebIntercept);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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.
|
Test autohotspot detection.
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ add_custom_command(
|
|||||||
public/iso_codes.json
|
public/iso_codes.json
|
||||||
public/logo512.png
|
public/logo512.png
|
||||||
public/sample_lv2_plugins.json
|
public/sample_lv2_plugins.json
|
||||||
public/vite.svg
|
|
||||||
src/pipedal/SplitUiControls.tsx
|
src/pipedal/SplitUiControls.tsx
|
||||||
src/pipedal/JackHostStatus.tsx
|
src/pipedal/JackHostStatus.tsx
|
||||||
src/pipedal/VirtualKeyboardHandler.tsx
|
src/pipedal/VirtualKeyboardHandler.tsx
|
||||||
@@ -291,8 +290,6 @@ add_custom_command(
|
|||||||
src/pipedal/MidiChannelBinding.tsx
|
src/pipedal/MidiChannelBinding.tsx
|
||||||
src/pipedal/ModFileTypes.tsx
|
src/pipedal/ModFileTypes.tsx
|
||||||
src/assets/react.svg
|
src/assets/react.svg
|
||||||
src/App.css
|
|
||||||
src/App.tsx
|
|
||||||
src/vite-env.d.ts
|
src/vite-env.d.ts
|
||||||
src/index.css
|
src/index.css
|
||||||
src/main.tsx
|
src/main.tsx
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<link rel="stylesheet" href="/css/roboto.css" />
|
<link rel="stylesheet" href="/css/roboto.css" />
|
||||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
<link rel="stylesheet" href="/css/modGui.css" />
|
||||||
<style>
|
<style>
|
||||||
BODY {
|
BODY {
|
||||||
background: #D0D0D0;
|
background: #D0D0D0;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -2,28 +2,28 @@
|
|||||||
font-family: 'Roboto';
|
font-family: 'Roboto';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(/fonts/Roboto-Light.woff2) format('woff2');
|
src: url(/fonts/Roboto-Light.woff2) format('woff2');
|
||||||
}
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Roboto';
|
font-family: 'Roboto';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(/fonts/Roboto-Regular.woff2) format('woff2');
|
src: url(/fonts/Roboto-Regular.woff2) format('woff2');
|
||||||
}
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Roboto';
|
font-family: 'Roboto';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(/fonts/Roboto-Medium.woff2) format('woff2');
|
src: url(/fonts/Roboto-Medium.woff2) format('woff2');
|
||||||
}
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Roboto';
|
font-family: 'Roboto';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-display: swap;
|
font-display: block;
|
||||||
src: url(/fonts/Roboto-Bold.woff2) format('woff2');
|
src: url(/fonts/Roboto-Bold.woff2) format('woff2');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'EnglandHand';
|
||||||
|
src: url('england-webfont.woff');
|
||||||
|
font-weight: normal;
|
||||||
|
font-display: swap;
|
||||||
|
font-style: normal;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'epflul';
|
||||||
|
src: url('epf_lul-webfont.woff') format('woff');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 766 B |
|
After Width: | Height: | Size: 1.7 KiB |
@@ -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 |
@@ -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
|
|
||||||
@@ -152,7 +152,7 @@ const AboutDialog = class extends Component<AboutDialogProps, AboutDialogState>
|
|||||||
<ArrowBackIcon />
|
<ArrowBackIcon />
|
||||||
</IconButtonEx>
|
</IconButtonEx>
|
||||||
<Typography noWrap variant="h6"
|
<Typography noWrap variant="h6"
|
||||||
sx={{ maringLeft: 2, flex: 1 }}
|
sx={{ marginLeft: 2, flex: 1 }}
|
||||||
>
|
>
|
||||||
About
|
About
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -18,13 +18,15 @@
|
|||||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { ThemeProvider, createTheme, StyledEngineProvider } from '@mui/material/styles';
|
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 VirtualKeyboardHandler from './VirtualKeyboardHandler';
|
||||||
import AppThemed from "./AppThemed";
|
import AppThemed from "./AppThemed";
|
||||||
import { isDarkMode } from './DarkMode';
|
import { isDarkMode } from './DarkMode';
|
||||||
import Tone3000AuthComplete from './Tone3000AuthComplete';
|
import Tone3000AuthComplete from './Tone3000AuthComplete';
|
||||||
|
import FontTest from './FontTest';
|
||||||
|
|
||||||
|
|
||||||
declare module '@mui/material/styles' {
|
declare module '@mui/material/styles' {
|
||||||
@@ -225,8 +227,13 @@ type AppThemeProps = {
|
|||||||
|
|
||||||
function isTone3000Auth() {
|
function isTone3000Auth() {
|
||||||
let url = new URL(window.location.href);
|
let url = new URL(window.location.href);
|
||||||
let param = url.searchParams.get("api_key");
|
let param = url.searchParams.get("api_key");
|
||||||
return (param !== null && param !== "")
|
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 {
|
const App = (class extends React.Component {
|
||||||
@@ -248,11 +255,11 @@ const App = (class extends React.Component {
|
|||||||
<StyledEngineProvider injectFirst>
|
<StyledEngineProvider injectFirst>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
{isTone3000Auth() ? (
|
{
|
||||||
<Tone3000AuthComplete />
|
isTone3000Auth() && (<Tone3000AuthComplete />)
|
||||||
) : (
|
|| isFontTest() && (<FontTest />)
|
||||||
<AppThemed />
|
|| (<AppThemed />)
|
||||||
)}
|
}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</StyledEngineProvider>
|
</StyledEngineProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import './AppThemed.css';
|
import './AppThemed.css';
|
||||||
|
|
||||||
//import {alpha} from '@mui/material/styles';
|
//import {alpha} from '@mui/material/styles';
|
||||||
import AppBar from '@mui/material/AppBar';
|
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 FxAmplifierIcon from './svg/fx_amplifier.svg?react';
|
||||||
import { PerformanceView } from './PerformanceView';
|
import { PerformanceView } from './PerformanceView';
|
||||||
|
|
||||||
|
|
||||||
import DialogEx from './DialogEx';
|
import DialogEx from './DialogEx';
|
||||||
|
|
||||||
import { IDialogStackable, popDialogStack, pushDialogStack } from './DialogStack';
|
import { IDialogStackable, popDialogStack, pushDialogStack } from './DialogStack';
|
||||||
|
|||||||
@@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
@@ -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();
|
let model: PiPedalModel = PiPedalModelFactory.getInstance();
|
||||||
|
|
||||||
if (!pedalboardItem) {
|
if (!pedalboardItem) {
|
||||||
@@ -50,7 +54,13 @@ export function GetControlView(pedalboardItem?: PedalboardItem | null): React.Re
|
|||||||
}
|
}
|
||||||
if (pedalboardItem.isStart() || pedalboardItem.isEnd()) {
|
if (pedalboardItem.isStart() || pedalboardItem.isEnd()) {
|
||||||
return (
|
return (
|
||||||
<PluginControlView instanceId={pedalboardItem.instanceId} item={pedalboardItem} />
|
<PluginControlView instanceId={pedalboardItem.instanceId}
|
||||||
|
item={pedalboardItem}
|
||||||
|
showModGui={showModUi}
|
||||||
|
onSetShowModGui={(instanceId, showModGui) => {
|
||||||
|
onSetShowModGui?.(instanceId, showModGui);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (pedalboardItem.isSplit()) {
|
if (pedalboardItem.isSplit()) {
|
||||||
@@ -73,7 +83,11 @@ export function GetControlView(pedalboardItem?: PedalboardItem | null): React.Re
|
|||||||
</div>
|
</div>
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<PluginControlView instanceId={pedalboardItem.instanceId} item={pedalboardItem} />
|
<PluginControlView instanceId={pedalboardItem.instanceId} item={pedalboardItem} showModGui={showModUi}
|
||||||
|
onSetShowModGui={(instanceId, showModGui) => {
|
||||||
|
onSetShowModGui?.(instanceId, showModGui);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -21,12 +21,13 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
import IconButton, {IconButtonProps} from '@mui/material/IconButton';
|
import IconButton, {IconButtonProps} from '@mui/material/IconButton';
|
||||||
import ToolTipEx from './ToolTipEx';
|
import ToolTipEx from './ToolTipEx';
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
interface IconButtonExProps extends IconButtonProps {
|
interface IconButtonExProps extends IconButtonProps {
|
||||||
tooltip: string;
|
tooltip: React.ReactElement | string;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -35,9 +36,10 @@ function IconButtonEx(props: IconButtonExProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolTipEx title={
|
<ToolTipEx title={
|
||||||
|
(typeof props.tooltip === 'string') ?
|
||||||
(
|
(
|
||||||
<Typography variant="caption">{tooltip || extra['aria-label'] }</Typography>
|
<Typography variant="caption">{tooltip || extra['aria-label'] }</Typography>
|
||||||
)
|
): (props.tooltip as React.ReactElement)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<IconButton {...extra} style={style} />
|
<IconButton {...extra} style={style} />
|
||||||
|
|||||||
@@ -164,7 +164,8 @@ interface PluginGridProps extends WithStyles<typeof pluginGridStyles> {
|
|||||||
uri?: string;
|
uri?: string;
|
||||||
minimumItemWidth?: number;
|
minimumItemWidth?: number;
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
open: boolean
|
open: boolean;
|
||||||
|
modGuiOnly?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PluginGridState = {
|
type PluginGridState = {
|
||||||
@@ -526,6 +527,8 @@ export const LoadPluginDialog =
|
|||||||
|
|
||||||
for (let i = 0; i < plugins.length; ++i) {
|
for (let i = 0; i < plugins.length; ++i) {
|
||||||
let plugin = plugins[i];
|
let plugin = plugins[i];
|
||||||
|
if (this.props.modGuiOnly == true && !plugin.modGui)
|
||||||
|
continue;
|
||||||
try {
|
try {
|
||||||
if (filterType === PluginType.Plugin || rootClass.is_type_of(filterType, plugin.plugin_type)) {
|
if (filterType === PluginType.Plugin || rootClass.is_type_of(filterType, plugin.plugin_type)) {
|
||||||
let score: number = 0;
|
let score: number = 0;
|
||||||
@@ -720,9 +723,15 @@ export const LoadPluginDialog =
|
|||||||
style={{ overflowX: "hidden", overflowY: "hidden", display: "flex", flexDirection: "column", flexWrap: "nowrap" }}
|
style={{ overflowX: "hidden", overflowY: "hidden", display: "flex", flexDirection: "column", flexWrap: "nowrap" }}
|
||||||
aria-labelledby="select-plugin-dialog-title">
|
aria-labelledby="select-plugin-dialog-title">
|
||||||
<div style={{ display: "flex", flexDirection: "column", flexWrap: "nowrap", height: "100%" }}>
|
<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 }}>
|
<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" }}>
|
marginRight: 16
|
||||||
<IconButtonEx tooltip="Back" onClick={() => { this.cancel(); }} style={{ flex: "0 0 auto" }} >
|
}}>
|
||||||
|
<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 />
|
<ArrowBackIcon />
|
||||||
</IconButtonEx>
|
</IconButtonEx>
|
||||||
|
|
||||||
@@ -731,9 +740,9 @@ export const LoadPluginDialog =
|
|||||||
visibility: (this.state.search_collapsed ? "visible" : "collapse"),
|
visibility: (this.state.search_collapsed ? "visible" : "collapse"),
|
||||||
width: (this.state.search_collapsed ? undefined : "0px")
|
width: (this.state.search_collapsed ? undefined : "0px")
|
||||||
}}>
|
}}>
|
||||||
{this.state.client_width > 520 ? "Select Plugin" : ""}
|
{this.state.client_width > 645 ? "Select Plugin" : ""}
|
||||||
</Typography>
|
</Typography>
|
||||||
<div style={{ flex: "1 1 auto" }} >
|
<div style={{ flex: "2 2 auto" }} >
|
||||||
<SearchControl collapsed={this.state.search_collapsed} searchText={this.state.search_string}
|
<SearchControl collapsed={this.state.search_collapsed} searchText={this.state.search_string}
|
||||||
inputRef={this.searchInputRef}
|
inputRef={this.searchInputRef}
|
||||||
showSearchIcon={showSearchIcon}
|
showSearchIcon={showSearchIcon}
|
||||||
@@ -784,13 +793,13 @@ export const LoadPluginDialog =
|
|||||||
)}
|
)}
|
||||||
</IconButtonEx>
|
</IconButtonEx>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: "0 0 160px", marginRight: 24 }} >
|
<div style={{ flex: "0 1 160px", position: "relative", marginRight: 24 }} >
|
||||||
|
|
||||||
<TextField select variant="standard"
|
<TextField select variant="standard"
|
||||||
defaultValue={this.state.filterType}
|
defaultValue={this.state.filterType}
|
||||||
key={this.state.filterType}
|
key={this.state.filterType}
|
||||||
onChange={(e) => { this.onFilterChange(e); }}
|
onChange={(e) => { this.onFilterChange(e); }}
|
||||||
sx={{ minWidth: 160 }}
|
sx={{ width: "100%",minWidth: "100%" }}
|
||||||
>
|
>
|
||||||
{this.createFilterOptions()}
|
{this.createFilterOptions()}
|
||||||
</TextField>
|
</TextField>
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ interface Deserializable<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const noteNames: string[] = [
|
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;
|
let iValue = Math.round(value) % 12;
|
||||||
return noteNames[iValue];
|
return noteNames[iValue];
|
||||||
|
|
||||||
@@ -313,10 +313,85 @@ export class UiFileProperty {
|
|||||||
useLegacyModDirectory: boolean = false;
|
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> {
|
export class Lv2Plugin implements Deserializable<Lv2Plugin> {
|
||||||
deserialize(input: any): Lv2Plugin {
|
deserialize(input: any): Lv2Plugin {
|
||||||
this.uri = input.uri;
|
this.uri = input.uri;
|
||||||
this.name = input.name;
|
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.brand = input.name ? input.name : "";
|
||||||
this.label = input.label ? input.label : this.name;
|
this.label = input.label ? input.label : this.name;
|
||||||
this.plugin_class = input.plugin_class;
|
this.plugin_class = input.plugin_class;
|
||||||
@@ -344,12 +419,18 @@ export class Lv2Plugin implements Deserializable<Lv2Plugin> {
|
|||||||
} else {
|
} else {
|
||||||
this.uiPortNotifications = [];
|
this.uiPortNotifications = [];
|
||||||
}
|
}
|
||||||
|
this.modGui = null;
|
||||||
|
if (input.modGui) {
|
||||||
|
this.modGui = new ModGui().deserialize(input.modGui);
|
||||||
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
static EmptyFeatures: string[] = [];
|
static EmptyFeatures: string[] = [];
|
||||||
|
|
||||||
uri: string = "";
|
uri: string = "";
|
||||||
name: string = "";
|
name: string = "";
|
||||||
|
minorVersion: number = 0;
|
||||||
|
microVersion: number = 0;
|
||||||
brand: string = "";
|
brand: string = "";
|
||||||
label: string = "";
|
label: string = "";
|
||||||
plugin_class: string = "";
|
plugin_class: string = "";
|
||||||
@@ -364,6 +445,7 @@ export class Lv2Plugin implements Deserializable<Lv2Plugin> {
|
|||||||
fileProperties: UiFileProperty[] = [];
|
fileProperties: UiFileProperty[] = [];
|
||||||
frequencyPlots: UiFrequencyPlot[] = [];
|
frequencyPlots: UiFrequencyPlot[] = [];
|
||||||
uiPortNotifications: UiPropertyNotification[] = [];
|
uiPortNotifications: UiPropertyNotification[] = [];
|
||||||
|
modGui: ModGui | null = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -459,10 +541,12 @@ export enum ControlType {
|
|||||||
Vu,
|
Vu,
|
||||||
Progress,
|
Progress,
|
||||||
DbVu,
|
DbVu,
|
||||||
OutputText
|
OutputText,
|
||||||
|
|
||||||
|
BypassLight
|
||||||
}
|
}
|
||||||
|
|
||||||
const textUnits : Set<Units> = new Set<Units>([
|
const textUnits: Set<Units> = new Set<Units>([
|
||||||
Units.bar,
|
Units.bar,
|
||||||
Units.beat,
|
Units.beat,
|
||||||
Units.bpm,
|
Units.bpm,
|
||||||
@@ -478,13 +562,12 @@ const textUnits : Set<Units> = new Set<Units>([
|
|||||||
Units.semitone12TET
|
Units.semitone12TET
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function displayUnitAsText(unit: Units): boolean
|
function displayUnitAsText(unit: Units): boolean {
|
||||||
{
|
|
||||||
return textUnits.has(unit);
|
return textUnits.has(unit);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UiControl implements Deserializable<UiControl> {
|
export class UiControl implements Deserializable<UiControl> {
|
||||||
deserialize(input: any): UiControl {
|
deserialize(input: any): UiControl {
|
||||||
this.symbol = input.symbol;
|
this.symbol = input.symbol;
|
||||||
this.name = input.name;
|
this.name = input.name;
|
||||||
this.index = input.index;
|
this.index = input.index;
|
||||||
@@ -531,12 +614,11 @@ deserialize(input: any): UiControl {
|
|||||||
this.controlType = ControlType.Progress;
|
this.controlType = ControlType.Progress;
|
||||||
} else if (this.enumeration_property) {
|
} else if (this.enumeration_property) {
|
||||||
this.controlType = ControlType.OutputText;
|
this.controlType = ControlType.OutputText;
|
||||||
} else if (displayUnitAsText(this.units))
|
} else if (displayUnitAsText(this.units)) {
|
||||||
{
|
|
||||||
this.controlType = ControlType.OutputText
|
this.controlType = ControlType.OutputText
|
||||||
} else {
|
} else {
|
||||||
this.controlType = ControlType.Vu;
|
this.controlType = ControlType.Vu;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.isValidEnumeration()) {
|
if (this.isValidEnumeration()) {
|
||||||
this.controlType = ControlType.Select;
|
this.controlType = ControlType.Select;
|
||||||
@@ -548,10 +630,8 @@ deserialize(input: any): UiControl {
|
|||||||
this.controlType = ControlType.OnOffSwitch;
|
this.controlType = ControlType.OnOffSwitch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.is_input)
|
if (this.is_input) {
|
||||||
{
|
if (this.pipedal_graphicEq) {
|
||||||
if (this.pipedal_graphicEq)
|
|
||||||
{
|
|
||||||
this.controlType = ControlType.GraphicEq;
|
this.controlType = ControlType.GraphicEq;
|
||||||
}
|
}
|
||||||
else if (this.mod_momentaryOnByDefault) {
|
else if (this.mod_momentaryOnByDefault) {
|
||||||
@@ -560,7 +640,7 @@ deserialize(input: any): UiControl {
|
|||||||
this.controlType = ControlType.Momentary;
|
this.controlType = ControlType.Momentary;
|
||||||
} else if (this.trigger_property) {
|
} else if (this.trigger_property) {
|
||||||
this.controlType = ControlType.Trigger;
|
this.controlType = ControlType.Trigger;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -654,7 +734,7 @@ deserialize(input: any): UiControl {
|
|||||||
isAbToggle(): boolean {
|
isAbToggle(): boolean {
|
||||||
return this.controlType === ControlType.ABSwitch;
|
return this.controlType === ControlType.ABSwitch;
|
||||||
}
|
}
|
||||||
isGraphicEq() : boolean {
|
isGraphicEq(): boolean {
|
||||||
return this.controlType === ControlType.GraphicEq;
|
return this.controlType === ControlType.GraphicEq;
|
||||||
}
|
}
|
||||||
isSelect(): boolean {
|
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);
|
let iValue = Math.round(value);
|
||||||
if (iValue >= 60)
|
if (iValue >= 60) {
|
||||||
{
|
let minutes = Math.floor(iValue / 60);
|
||||||
let minutes = Math.floor(iValue/60);
|
|
||||||
let seconds = iValue % 60;
|
let seconds = iValue % 60;
|
||||||
if (seconds < 10)
|
if (seconds < 10) {
|
||||||
{
|
|
||||||
return minutes + ":0" + seconds;
|
return minutes + ":0" + seconds;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -756,11 +833,10 @@ deserialize(input: any): UiControl {
|
|||||||
return this.formatShortValue(value);
|
return this.formatShortValue(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.units === Units.semitone12TET)
|
if (this.units === Units.semitone12TET) {
|
||||||
{
|
|
||||||
return semitone12TETValue(value);
|
return semitone12TETValue(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
let text = this.formatShortValue(value);
|
let text = this.formatShortValue(value);
|
||||||
|
|
||||||
switch (this.units) {
|
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> {
|
export class UiPlugin implements Deserializable<UiPlugin> {
|
||||||
deserialize(input: any): UiPlugin {
|
deserialize(input: any): UiPlugin {
|
||||||
this.uri = input.uri;
|
this.uri = input.uri;
|
||||||
this.name = input.name;
|
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.brand = input.brand ? input.brand : "";
|
||||||
this.label = input.label ? input.label : this.name;
|
this.label = input.label ? input.label : this.name;
|
||||||
this.plugin_type = input.plugin_type as PluginType;
|
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.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;
|
return this;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -908,6 +1023,8 @@ export class UiPlugin implements Deserializable<UiPlugin> {
|
|||||||
|
|
||||||
uri: string = "";
|
uri: string = "";
|
||||||
name: string = "";
|
name: string = "";
|
||||||
|
minorVersion: number = 0;
|
||||||
|
microVersion: number = 0;
|
||||||
brand: string = "";
|
brand: string = "";
|
||||||
label: string = "";
|
label: string = "";
|
||||||
plugin_type: PluginType = PluginType.InvalidPlugin;
|
plugin_type: PluginType = PluginType.InvalidPlugin;
|
||||||
@@ -924,6 +1041,8 @@ export class UiPlugin implements Deserializable<UiPlugin> {
|
|||||||
fileProperties: UiFileProperty[] = [];
|
fileProperties: UiFileProperty[] = [];
|
||||||
frequencyPlots: UiFrequencyPlot[] = [];
|
frequencyPlots: UiFrequencyPlot[] = [];
|
||||||
is_vst3: boolean = false;
|
is_vst3: boolean = false;
|
||||||
|
modGui: ModGui | null = null; // null if no mod gui.
|
||||||
|
patchProperties: Lv2PatchPropertyInfo[] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ import Snapshot3Icon from "./svg/snapshot_3.svg?react";
|
|||||||
import Snapshot4Icon from "./svg/snapshot_4.svg?react";
|
import Snapshot4Icon from "./svg/snapshot_4.svg?react";
|
||||||
import Snapshot5Icon from "./svg/snapshot_5.svg?react";
|
import Snapshot5Icon from "./svg/snapshot_5.svg?react";
|
||||||
import Snapshot6Icon from "./svg/snapshot_6.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 SnapshotDialog from './SnapshotDialog';
|
||||||
import { css } from '@emotion/react';
|
import { css } from '@emotion/react';
|
||||||
@@ -128,6 +130,8 @@ interface MainState {
|
|||||||
showMidiBindingsDialog: boolean;
|
showMidiBindingsDialog: boolean;
|
||||||
screenHeight: number;
|
screenHeight: number;
|
||||||
displayNameDialogOpen: boolean;
|
displayNameDialogOpen: boolean;
|
||||||
|
showModUi: boolean;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -167,7 +171,8 @@ export const MainPage =
|
|||||||
displayAuthor: this.getDisplayAuthor(),
|
displayAuthor: this.getDisplayAuthor(),
|
||||||
horizontalScrollLayout: this.windowSize.height < HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK,
|
horizontalScrollLayout: this.windowSize.height < HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK,
|
||||||
showMidiBindingsDialog: false,
|
showMidiBindingsDialog: false,
|
||||||
screenHeight: this.windowSize.height
|
screenHeight: this.windowSize.height,
|
||||||
|
showModUi: false
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
@@ -375,7 +380,7 @@ export const MainPage =
|
|||||||
if (pedalboardItem === null) return "";
|
if (pedalboardItem === null) return "";
|
||||||
return pedalboardItem.uri;
|
return pedalboardItem.uri;
|
||||||
}
|
}
|
||||||
titleBar(pedalboardItem: PedalboardItem | null): React.ReactNode {
|
titleBar(pedalboardItem: PedalboardItem | null,canShowModUi: boolean): React.ReactNode {
|
||||||
let title = "";
|
let title = "";
|
||||||
let author = "";
|
let author = "";
|
||||||
let infoPluginUri = "";
|
let infoPluginUri = "";
|
||||||
@@ -441,7 +446,7 @@ export const MainPage =
|
|||||||
{canEditTitle ? (
|
{canEditTitle ? (
|
||||||
<ButtonBase
|
<ButtonBase
|
||||||
style={{ borderRadius: "6px", width: "100%", paddingLeft: 8, paddingRight: 8, height: 48, textTransform: "none" }}
|
style={{ borderRadius: "6px", width: "100%", paddingLeft: 8, paddingRight: 8, height: 48, textTransform: "none" }}
|
||||||
onClick={()=> {
|
onClick={() => {
|
||||||
this.handleEditPluginDisplayName();
|
this.handleEditPluginDisplayName();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -453,7 +458,7 @@ export const MainPage =
|
|||||||
</span>
|
</span>
|
||||||
</ButtonBase>)
|
</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>
|
<span className={classes.title}>{title}</span>
|
||||||
{this.state.displayAuthor && (
|
{this.state.displayAuthor && (
|
||||||
<span className={classes.author}>{author}</span>
|
<span className={classes.author}>{author}</span>
|
||||||
@@ -469,6 +474,32 @@ export const MainPage =
|
|||||||
<PluginPresetSelector pluginUri={presetsUri} instanceId={pedalboardItem?.instanceId ?? 0}
|
<PluginPresetSelector pluginUri={presetsUri} instanceId={pedalboardItem?.instanceId ?? 0}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</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() {
|
render() {
|
||||||
const classes = withStyles.getClasses(this.props);
|
const classes = withStyles.getClasses(this.props);
|
||||||
let pedalboard = this.model.pedalboard.get();
|
let pedalboard = this.model.pedalboard.get();
|
||||||
@@ -508,8 +548,9 @@ export const MainPage =
|
|||||||
let instanceId = -1;
|
let instanceId = -1;
|
||||||
let missing = false;
|
let missing = false;
|
||||||
let pluginUri = "#error";
|
let pluginUri = "#error";
|
||||||
|
let canShowModUi = false;
|
||||||
if (pedalboardItem) {
|
if (pedalboardItem) {
|
||||||
|
canShowModUi = this.canShowModUi(pedalboardItem);
|
||||||
canDelete = pedalboard.canDeleteItem(pedalboardItem.instanceId);
|
canDelete = pedalboard.canDeleteItem(pedalboardItem.instanceId);
|
||||||
instanceId = pedalboardItem.instanceId;
|
instanceId = pedalboardItem.instanceId;
|
||||||
if (pedalboardItem.isEmpty()) {
|
if (pedalboardItem.isEmpty()) {
|
||||||
@@ -570,7 +611,7 @@ export const MainPage =
|
|||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
(!this.state.splitControlBar || !this.props.enableStructureEditing)
|
(!this.state.splitControlBar || !this.props.enableStructureEditing)
|
||||||
&& this.titleBar(pedalboardItem)
|
&& this.titleBar(pedalboardItem, canShowModUi)
|
||||||
}
|
}
|
||||||
<div style={{ flex: "1 1 1px" }}>
|
<div style={{ flex: "1 1 1px" }}>
|
||||||
|
|
||||||
@@ -642,7 +683,7 @@ export const MainPage =
|
|||||||
this.state.splitControlBar && this.props.enableStructureEditing && (
|
this.state.splitControlBar && this.props.enableStructureEditing && (
|
||||||
<div className={classes.splitControlBar}>
|
<div className={classes.splitControlBar}>
|
||||||
{
|
{
|
||||||
this.titleBar(pedalboardItem)
|
this.titleBar(pedalboardItem, canShowModUi)
|
||||||
}
|
}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
@@ -689,7 +734,7 @@ export const MainPage =
|
|||||||
this.setState({ displayNameDialogOpen: false });
|
this.setState({ displayNameDialogOpen: false });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -416,6 +416,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
|
|
||||||
socketServerUrl: string = "";
|
socketServerUrl: string = "";
|
||||||
varServerUrl: string = "";
|
varServerUrl: string = "";
|
||||||
|
modResourcesUrl: string = "";
|
||||||
lv2Path: string = "";
|
lv2Path: string = "";
|
||||||
webSocket?: PiPedalSocket;
|
webSocket?: PiPedalSocket;
|
||||||
|
|
||||||
@@ -972,13 +973,17 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
// anything could have changed while we were disconnected.
|
// anything could have changed while we were disconnected.
|
||||||
await this.loadServerState();
|
await this.loadServerState();
|
||||||
}
|
}
|
||||||
makeSocketServerUrl(hostName: string, port: number): string {
|
private makeSocketServerUrl(hostName: string, port: number): string {
|
||||||
return "ws://" + hostName + ":" + port + "/pipedal";
|
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/";
|
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> {
|
async getNextAudioFile(filePath: string): Promise<string> {
|
||||||
@@ -1053,6 +1058,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
if (!socket_server_port) socket_server_port = 8080;
|
if (!socket_server_port) socket_server_port = 8080;
|
||||||
let socket_server = this.makeSocketServerUrl(socket_server_address, socket_server_port);
|
let socket_server = this.makeSocketServerUrl(socket_server_address, socket_server_port);
|
||||||
let var_server_url = this.makeVarServerUrl("http", 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.socketServerUrl = socket_server;
|
||||||
this.varServerUrl = var_server_url;
|
this.varServerUrl = var_server_url;
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import WithStyles, { withTheme } from './WithStyles';
|
|||||||
import { createStyles } from './WithStyles';
|
import { createStyles } from './WithStyles';
|
||||||
import { css } from '@emotion/react';
|
import { css } from '@emotion/react';
|
||||||
|
|
||||||
|
import ModGuiHost, { IModGuiHostSite } from './ModGuiHost';
|
||||||
|
|
||||||
import { withStyles } from "tss-react/mui";
|
import { withStyles } from "tss-react/mui";
|
||||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||||
import { UiPlugin, UiControl, UiFileProperty, UiFrequencyPlot, ScalePoint } from './Lv2Plugin';
|
import { UiPlugin, UiControl, UiFileProperty, UiFrequencyPlot, ScalePoint } from './Lv2Plugin';
|
||||||
@@ -340,6 +342,8 @@ export interface PluginControlViewProps extends WithStyles<typeof styles> {
|
|||||||
item: PedalboardItem;
|
item: PedalboardItem;
|
||||||
customization?: ControlViewCustomization;
|
customization?: ControlViewCustomization;
|
||||||
customizationId?: number;
|
customizationId?: number;
|
||||||
|
showModGui: boolean;
|
||||||
|
onSetShowModGui: (instanceId: number, showModGui: boolean) => void;
|
||||||
}
|
}
|
||||||
type PluginControlViewState = {
|
type PluginControlViewState = {
|
||||||
landscapeGrid: boolean;
|
landscapeGrid: boolean;
|
||||||
@@ -360,7 +364,6 @@ const PluginControlView =
|
|||||||
constructor(props: PluginControlViewProps) {
|
constructor(props: PluginControlViewProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.model = PiPedalModelFactory.getInstance();
|
this.model = PiPedalModelFactory.getInstance();
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
landscapeGrid: false,
|
landscapeGrid: false,
|
||||||
imeUiControl: undefined,
|
imeUiControl: undefined,
|
||||||
@@ -376,6 +379,19 @@ const PluginControlView =
|
|||||||
this.onControlValueChanged = this.onControlValueChanged.bind(this);
|
this.onControlValueChanged = this.onControlValueChanged.bind(this);
|
||||||
this.onPreviewChange = this.onPreviewChange.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 {
|
onPreviewChange(key: string, value: number): void {
|
||||||
@@ -758,19 +774,45 @@ const PluginControlView =
|
|||||||
}
|
}
|
||||||
return false;
|
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 {
|
renderModGui(): ReactNode {
|
||||||
this.controlKeyIndex = 0;
|
let uiPlugin = this.model.getUiPlugin(this.props.item.uri);
|
||||||
const classes = withStyles.getClasses(this.props);
|
if (!uiPlugin) {
|
||||||
let pedalboardItem: PedalboardItem;
|
return (<div />);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
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)
|
if (!pedalboardItem)
|
||||||
return (<div className={classes.frame} ></div>);
|
return (<div className={classes.frame} ></div>);
|
||||||
@@ -792,19 +834,13 @@ const PluginControlView =
|
|||||||
|
|
||||||
controlValues = this.filterNotOnGui(controlValues, plugin);
|
controlValues = this.filterNotOnGui(controlValues, plugin);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let gridClass = this.state.landscapeGrid ? classes.landscapeGrid : classes.normalGrid;
|
let gridClass = this.state.landscapeGrid ? classes.landscapeGrid : classes.normalGrid;
|
||||||
let scrollClass = this.state.landscapeGrid ? classes.frameScrollLandscape : classes.frameScrollPortrait;
|
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()) {
|
if (this.fullScreen()) {
|
||||||
gridClass = classes.noScrollGrid;
|
gridClass = classes.noScrollGrid;
|
||||||
scrollClass = classes.frameScrollNone;
|
scrollClass = classes.frameScrollNone;
|
||||||
frameClass = classes.noScrollFrame;
|
|
||||||
}
|
}
|
||||||
|
let controlNodes: ControlNodes;
|
||||||
|
|
||||||
controlNodes = this.getStandardControlNodes(plugin, controlValues);
|
controlNodes = this.getStandardControlNodes(plugin, controlValues);
|
||||||
|
|
||||||
@@ -822,16 +858,7 @@ const PluginControlView =
|
|||||||
if (pedalboardItem.midiChannelBinding) {
|
if (pedalboardItem.midiChannelBinding) {
|
||||||
nodes.push(this.midiBindingControl(pedalboardItem));
|
nodes.push(this.midiBindingControl(pedalboardItem));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
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={scrollClass}>
|
||||||
<div className={gridClass} >
|
<div className={gridClass} >
|
||||||
{
|
{
|
||||||
@@ -848,6 +875,44 @@ const PluginControlView =
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</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 && (
|
{this.state.showFileDialog && (
|
||||||
|
|
||||||
<FilePropertyDialog open={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}
|
<FullScreenIME uiControl={this.state.imeUiControl} value={this.state.imeValue}
|
||||||
|
|
||||||
onChange={(key, value) => this.onImeValueChange(key, value)}
|
onChange={(key, value) => this.onImeValueChange(key, value)}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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 |
@@ -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,4 +8,12 @@ export default defineConfig({
|
|||||||
chunkSizeWarningLimit: 2000
|
chunkSizeWarningLimit: 2000
|
||||||
},
|
},
|
||||||
plugins: [react(),svgr()],
|
plugins: [react(),svgr()],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/resources': {
|
||||||
|
target: 'http://localhost:8080',
|
||||||
|
changeOrigin: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||