Merge pull request #344 from rerdavies/dev-modui

v1.4.68
This commit is contained in:
Robin Davies
2025-07-21 12:22:16 -04:00
committed by GitHub
196 changed files with 7936 additions and 866 deletions
+3 -2
View File
@@ -103,7 +103,7 @@
"request": "launch",
// Resolved by CMake Tools:
"program": "${command:cmake.launchTargetPath}",
"args": [ "/etc/pipedal/config", "/etc/pipedal/react", "-port", "0.0.0.0:8080", "-log-level","debug" ],
"args": [ "/etc/pipedal/config", "${workspaceFolder}/vite/dist", "-port", "0.0.0.0:8080", "-log-level","debug" ],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [
@@ -142,7 +142,8 @@
//"[pipedal_alsa_test]"
// "[wifi_channels_test]"
// "[locale]"
"[pipewire_input_stream]"
//"[mod_gui_init]"
"[mod_gui_templates]"
],
"stopAtEntry": false,
+3 -1
View File
@@ -115,6 +115,7 @@
"Chowdhury",
"Chowdhury's",
"discursus",
"distro",
"distros",
"dpkg",
"flac",
@@ -134,6 +135,7 @@
"midiclip",
"midisong",
"mlmodel",
"modui",
"MOTU",
"Netplan",
"Pedalboard",
@@ -161,7 +163,7 @@
"cSpell.ignoreWords": [
"nammodel"
],
"cSpell.enabled": true,
"cSpell.enabled": false,
// Disable all automatic completion suggestions - only show when Ctrl+Space is pressed
"editor.quickSuggestions": {
View File
+8 -4
View File
@@ -1,13 +1,13 @@
cmake_minimum_required(VERSION 3.16.0)
project(pipedal
VERSION 1.4.77
VERSION 1.4.79
DESCRIPTION "PiPedal Guitar Effect Pedal For Raspberry Pi"
HOMEPAGE_URL "https://rerdavies.github.io/pipedal"
)
EXECUTE_PROCESS( COMMAND dpkg --print-architecture COMMAND tr -d '\n' OUTPUT_VARIABLE DEBIAN_ARCHITECTURE )
set (DISPLAY_VERSION "PiPedal v1.4.77-Beta")
set (DISPLAY_VERSION "PiPedal v1.4.79-Beta")
set (PACKAGE_ARCHITECTURE ${DEBIAN_ARCHITECTURE})
set (CMAKE_INSTALL_PREFIX "/usr/")
@@ -96,13 +96,17 @@ set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "IoT guitar effect pedal for Raspberry Pi"
set(CPACK_DEBIAN_PACKAGE_SECTION sound)
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
set(CPACK_DEBIAN_PACKAGE_CONTROL_STRICT_PERMISSION TRUE)
set(CPACK_DEBIAN_PACKAGE_DEPENDS "lv2-dev, authbind, curl, gpg, alsa-base| pipewire, alsa-utils" )
set(CPACK_DEBIAN_PACKAGE_DEPENDS "lv2-dev, ffmpeg, authbind, curl, gpg, alsa-base| pipewire, alsa-utils" )
set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE ${DEBIAN_ARCHITECTURE})
set(CPACK_PACKAGING_INSTALL_PREFIX /usr)
set(CPACK_PROJECT_NAME ${PROJECT_NAME})
set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})
set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT)
set (CPACK_STRIP_FILES true)
if (CMAKE_BUILD_TYPE MATCHES Debug)
set (CPACK_STRIP_FILES false)
else()
set (CPACK_STRIP_FILES true)
endif()
set(CPACK_DEBIAN_PACKAGE_SIGN_ALGORITHM "detached")
set(CPACK_DEBIAN_PACKAGE_SIGN_TYPE "origin")
+2 -1
View File
@@ -319,6 +319,7 @@ namespace pipedal
}
if (seqHandle)
{
Lv2Log::debug("Closing ALSA Sequencer");
snd_seq_close(seqHandle);
seqHandle = nullptr;
}
@@ -608,7 +609,7 @@ namespace pipedal
#ifndef NDEBUG
#define MSG_DEBUG_LOG(x) \
case x: \
Lv2Log::debug("ALSA Sequencer Message" #x); \
Lv2Log::debug("ALSA Sequencer Message " #x); \
break;
#else
#define MSG_DEBUG_LOG(x)
+6 -5
View File
@@ -73,6 +73,7 @@ DBusDispatcher::PostHandle DBusDispatcher::PostDelayed(const clock::duration &de
void DBusDispatcher::Run()
{
this->eventFd = eventfd(0, 0);
threadStarted = true;
this->serviceThread = std::thread(
[this]()
{ this->ThreadProc(); });
@@ -169,7 +170,7 @@ void DBusDispatcher::ThreadProc()
int eventTimeout = GetEventTimeoutMs();
if (eventTimeout != -1 && eventTimeout < pollTimeout)
{
pollTimeout = eventTimeout;/*+-*/
pollTimeout = eventTimeout; /*+-*/
}
if (pollTimeout > 250 || pollTimeout == -1)
{
@@ -197,13 +198,13 @@ void DBusDispatcher::ThreadProc()
LogTrace("DBusDispatcher", "ThreadProc", "Service thread terminated.");
}
void DBusDispatcher::Wait()
void DBusDispatcher::WaitForClose()
{
try
{
if (!threadJoined)
if (threadStarted)
{
threadJoined = true;
threadStarted = false;
serviceThread.join();
}
}
@@ -229,7 +230,7 @@ void DBusDispatcher::Stop()
this->stopping = true;
WakeThread();
Wait();
WaitForClose();
}
}
+182 -63
View File
@@ -24,22 +24,20 @@
using namespace pipedal;
// non-localizable. RFC 7231 Date Format
;
static const char*INVARIANT_WEEK_DAYS[] =
{ "Sun","Mon", "Tue", "Wed","Thu", "Fri","Sat"};
static const char *INVARIANT_WEEK_DAYS[] =
{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
// non-localizable. RFC 7231 Date Format
static const char*INVARIANT_MONTHS[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep","Oct","Nov", "Dec"};
static const char *INVARIANT_MONTHS[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
// non-localizable. RFC 7231 Date Format
static const char*FORMAT = "%s, %02d %s %04d %02d:%02d:%02d GMT";
static const char *FORMAT = "%s, %02d %s %04d %02d:%02d:%02d GMT";
std::string HtmlHelper::timeToHttpDate()
{
return timeToHttpDate(time(nullptr));
}
std::string HtmlHelper::timeToHttpDate(std::filesystem::file_time_type time)
{
@@ -56,29 +54,30 @@ std::string HtmlHelper::timeToHttpDate(time_t time)
struct tm gmTm;
if (gmtime_r(&time,&gmTm) == nullptr)
if (gmtime_r(&time, &gmTm) == nullptr)
{
return "Mon, 01 Jan 1970 00:00:00 GMT";
}
char szBuffer[sizeof("Mon, 01 Jan 1970 00:00:00 GMT+")];
snprintf(szBuffer,sizeof(szBuffer), FORMAT,
INVARIANT_WEEK_DAYS[gmTm.tm_wday],
gmTm.tm_mday,
INVARIANT_MONTHS[gmTm.tm_mon],
gmTm.tm_year+1900,
gmTm.tm_hour,
gmTm.tm_min,
gmTm.tm_sec);
snprintf(szBuffer, sizeof(szBuffer), FORMAT,
INVARIANT_WEEK_DAYS[gmTm.tm_wday],
gmTm.tm_mday,
INVARIANT_MONTHS[gmTm.tm_mon],
gmTm.tm_year + 1900,
gmTm.tm_hour,
gmTm.tm_min,
gmTm.tm_sec);
return szBuffer;
}
static int hexN(char c)
{
if (c >= '0' && c <= '9') return c-'0';
if (c >= 'a' && c <= 'f') return 10+c-'a';
if (c >= 'A' && c <= 'F') return 10+c-'A';
if (c >= '0' && c <= '9')
return c - '0';
if (c >= 'a' && c <= 'f')
return 10 + c - 'a';
if (c >= 'A' && c <= 'F')
return 10 + c - 'A';
throw std::invalid_argument("Malformed URL.");
}
static char hexChar(char c1, char c2)
@@ -86,15 +85,19 @@ static char hexChar(char c1, char c2)
return (char)((hexN(c1) << 4) + hexN(c2));
}
class SafeCharacterMap {
class SafeCharacterMap
{
bool safeCharacter[256];
public:
SafeCharacterMap()
SafeCharacterMap()
{
for (int i = 0; i < 0x20; ++i) {
for (int i = 0; i < 0x20; ++i)
{
safeCharacter[i] = false;
}
for (int i = 0x20; i < 0x80; ++i) {
for (int i = 0x20; i < 0x80; ++i)
{
safeCharacter[i] = true;
}
for (int i = 0x80; i < 256; ++i)
@@ -102,27 +105,25 @@ public:
safeCharacter[i] = false;
}
const char *unsafeCharacters = ":/+?=#%@ ";
for (const char*p = unsafeCharacters; *p != 0; ++p)
for (const char *p = unsafeCharacters; *p != 0; ++p)
{
safeCharacter[(uint8_t)*p] = false;
}
}
bool isSafeCharacter(char c) { return safeCharacter[(uint8_t)c];}
bool isSafeCharacter(char c) { return safeCharacter[(uint8_t)c]; }
} g_safeCharacterMap;
std::string HtmlHelper::encode_url_segment(const char*pStart, const char*pEnd, bool isQuerySegment )
std::string HtmlHelper::encode_url_segment(const char *pStart, const char *pEnd, bool isQuerySegment)
{
std::stringstream s;
encode_url_segment(s,pStart,pEnd,isQuerySegment);
encode_url_segment(s, pStart, pEnd, isQuerySegment);
return s.str();
}
static char hexChars[] = "0123456789ABCDEF";
void HtmlHelper::encode_url_segment(std::ostream&os, const char*pStart, const char *pEnd, bool isQuerySegment)
void HtmlHelper::encode_url_segment(std::ostream &os, const char *pStart, const char *pEnd, bool isQuerySegment)
{
while (pStart != pEnd)
{
@@ -130,21 +131,21 @@ void HtmlHelper::encode_url_segment(std::ostream&os, const char*pStart, const ch
if (g_safeCharacterMap.isSafeCharacter(c))
{
os << c;
} else if (c == ' ' && isQuerySegment)
}
else if (c == ' ' && isQuerySegment)
{
os << '+';
os << c;
} else {
}
else
{
os << '%' << hexChars[(c >> 4) & 0x0f] << hexChars[c & 0x0F];
}
}
}
std::string HtmlHelper::decode_url_segment(const char*pStart, const char*pEnd, bool isQuery)
std::string HtmlHelper::decode_url_segment(const char *pStart, const char *pEnd, bool isQuery)
{
std::stringstream s;
auto p = pStart;
@@ -155,16 +156,23 @@ std::string HtmlHelper::decode_url_segment(const char*pStart, const char*pEnd, b
if (c == '+')
{
s << ' ';
} else if (c != '%')
}
else if (c != '%')
{
s << c;
} else {
if (pStart+2 < pEnd)
}
else
{
if (pStart + 2 < pEnd)
{
char c1 = *p;
++p;
char c2 = *p;
++p;
s << hexChar(c1, c2);
}
else
{
char c1 = *p; ++p;
char c2 = *p; ++p;
s << hexChar(c1,c2);
} else {
throw std::invalid_argument("Malformed URL.");
}
}
@@ -172,9 +180,9 @@ std::string HtmlHelper::decode_url_segment(const char*pStart, const char*pEnd, b
return s.str();
}
std::string HtmlHelper::decode_url_segment(const char*text, bool isQuery)
std::string HtmlHelper::decode_url_segment(const char *text, bool isQuery)
{
return decode_url_segment(text, text + strlen(text),isQuery);
return decode_url_segment(text, text + strlen(text), isQuery);
}
void HtmlHelper::utf32_to_utf8_stream(std::ostream &s, uint32_t uc)
@@ -182,20 +190,27 @@ void HtmlHelper::utf32_to_utf8_stream(std::ostream &s, uint32_t uc)
if (uc < 0x80u)
{
s << (char)uc;
} else if (uc < 0x800u) {
}
else if (uc < 0x800u)
{
s << (char)(0xC0 + (uc >> 6));
s << (char)(0x80 + (uc & 0x3F));
} else if (uc < 0x10000u) {
}
else if (uc < 0x10000u)
{
s << (char)(0xE0 + (uc >> 12));
s << (char)(0x80 + ((uc >> 6) & 0x3F));
s << (char)(0x80 + (uc & 0x3F));
} else if (uc < 0x0110000) {
}
else if (uc < 0x0110000)
{
s << (char)(0xF0 + (uc >> 18));
s << (char)(0x80 + ((uc >> 12) & 0x3F));
s << (char)(0x80 + ((uc >> 6) & 0x3F));
s << (char)(0x80 + (uc & 0x3F));
} else {
}
else
{
throw std::range_error("Illegal UTF-32 character.");
}
}
@@ -205,20 +220,22 @@ const std::string ESPECIALS = "()<>@,;:\"/[]?.=";
#define MAX_FILENAME_LENGTH 96
#define MAX_SEGMENT_LENGTH 74 // 75 per RFC2047.2. But leave space for a ';' to allow easier breaking of lines.
std::string HtmlHelper::Rfc5987EncodeFileName(const std::string&name)
std::string HtmlHelper::Rfc5987EncodeFileName(const std::string &name)
{
// Encode all characters. Let the browser handle safe-ifying the actual name, since that's os dependent.
std::stringstream s;
std::string prefix = "UTF-8''";
std::string postfix = "";
s << prefix;
for (char c: name)
for (char c : name)
{
uint8_t uc = (uint8_t)c;
if (uc < 0x20 || uc >= 0x80 || ESPECIALS.find(c) != std::string::npos)
{
s << '%' << hexChars[uc >> 4] << hexChars[uc & 0x0F];
} else {
}
else
{
s << c;
}
}
@@ -226,7 +243,6 @@ std::string HtmlHelper::Rfc5987EncodeFileName(const std::string&name)
return s.str();
}
#define MAX_FILE_NAME_LENGHT 96
const std::string SF_SPECIALS = " <>@;:\"\'/[]?=";
@@ -249,23 +265,126 @@ std::string HtmlHelper::SafeFileName(const std::string &name)
return s.str();
}
std::string HtmlHelper::HtmlEncode(const std::string& text)
std::string HtmlHelper::HtmlEncode(const std::string &text)
{
std::stringstream os;
for (char c: text)
for (char c : text)
{
switch(c)
switch (c)
{
case '<': os << "&lt;"; break;
case '>': os << "&gt;"; break;
case '&': os << "&amp;"; break;
default: os << c; break;
case '<':
os << "&lt;";
break;
case '>':
os << "&gt;";
break;
case '&':
os << "&amp;";
break;
default:
os << c;
break;
}
}
return os.str();
}
static const uint64_t crc64_table[256] = {
0x0000000000000000ULL, 0x42F0E1EBA9EA3693ULL, 0x85E1C3D753D46D26ULL, 0xC711223CFA3E5BB5ULL,
0x493366450E42ECDFULL, 0x0BC387AEA7A8DA4CULL, 0xCCD2A5925D9681F9ULL, 0x8E224479F47CB76AULL,
0x9266CC8A1C85D9BEULL, 0xD0962D61B56FEF2DULL, 0x17870F5D4F51B498ULL, 0x5577EEB6E6BB820BULL,
0xDB55AACF12C73561ULL, 0x99A54B24BB2D03F2ULL, 0x5EB4691841135847ULL, 0x1C4488F3E8F96ED4ULL,
0x663D78FF90E185EFULL, 0x24CD9914390BB37CULL, 0xE3DCBB28C335E8C9ULL, 0xA12C5AC36ADFDE5AULL,
0x2F0E1EBA9EA36930ULL, 0x6DFEFF5137495FA3ULL, 0xAAEFDD6DCD770416ULL, 0xE81F3C86649D3285ULL,
0xF45BB4758C645C51ULL, 0xB6AB559E258E6AC2ULL, 0x71BA77A2DFB03177ULL, 0x334A9649765A07E4ULL,
0xBD68D2308226B08EULL, 0xFF9833DB2BCC861DULL, 0x388911E7D1F2DDA8ULL, 0x7A79F00C7818EB3BULL,
0xCC7AF1FF21C30BDEULL, 0x8E8A101488293D4DULL, 0x499B3228721766F8ULL, 0x0B6BD3C3DBFD506BULL,
0x854997BA2F81E701ULL, 0xC7B97651866BD192ULL, 0x00A8546D7C558A27ULL, 0x4258B586D5BFBCB4ULL,
0x5E1C3D753D46D260ULL, 0x1CECDC9E94ACE4F3ULL, 0xDBFDFEA26E92BF46ULL, 0x990D1F49C77889D5ULL,
0x172F5B3033043EBFULL, 0x55DFBADB9AEE082CULL, 0x92CE98E760D05399ULL, 0xD03E790CC93A650AULL,
0xAA478900B1228E31ULL, 0xE8B768EB18C8B8A2ULL, 0x2FA64AD7E2F6E317ULL, 0x6D56AB3C4B1CD584ULL,
0xE374EF45BF6062EEULL, 0xA1840EAE168A547DULL, 0x66952C92ECB40FC8ULL, 0x2465CD79455E395BULL,
0x3821458AADA7578FULL, 0x7AD1A461044D611CULL, 0xBDC0865DFE733AA9ULL, 0xFF3067B657990C3AULL,
0x711223CFA3E5BB50ULL, 0x33E2C2240A0F8DC3ULL, 0xF4F3E018F031D676ULL, 0xB60301F359DBE0E5ULL,
0xDA050215EA6C212FULL, 0x98F5E3FE438617BCULL, 0x5FE4C1C2B9B84C09ULL, 0x1D14202910527A9AULL,
0x93366450E42ECDF0ULL, 0xD1C685BB4DC4FB63ULL, 0x16D7A787B7FAA0D6ULL, 0x5427466C1E109645ULL,
0x4863CE9FF6E9F891ULL, 0x0A932F745F03CE02ULL, 0xCD820D48A53D95B7ULL, 0x8F72ECA30CD7A324ULL,
0x0150A8DAF8AB144EULL, 0x43A04931514122DDULL, 0x84B16B0DAB7F7968ULL, 0xC6418AE602954FFBULL,
0xBC387AEA7A8DA4C0ULL, 0xFEC89B01D3679253ULL, 0x39D9B93D2959C9E6ULL, 0x7B2958D680B3FF75ULL,
0xF50B1CAF74CF481FULL, 0xB7FBFD44DD257E8CULL, 0x70EADF78271B2539ULL, 0x321A3E938EF113AAULL,
0x2E5EB66066087D7EULL, 0x6CAE578BCFE24BEDULL, 0xABBF75B735DC1058ULL, 0xE94F945C9C3626CBULL,
0x676DD025684A91A1ULL, 0x259D31CEC1A0A732ULL, 0xE28C13F23B9EFC87ULL, 0xA07CF2199274CA14ULL,
0x167FF3EACBAF2AF1ULL, 0x548F120162451C62ULL, 0x939E303D987B47D7ULL, 0xD16ED1D631917144ULL,
0x5F4C95AFC5EDC62EULL, 0x1DBC74446C07F0BDULL, 0xDAAD56789639AB08ULL, 0x985DB7933FD39D9BULL,
0x84193F60D72AF34FULL, 0xC6E9DE8B7EC0C5DCULL, 0x01F8FCB784FE9E69ULL, 0x43081D5C2D14A8FAULL,
0xCD2A5925D9681F90ULL, 0x8FDAB8CE70822903ULL, 0x48CB9AF28ABC72B6ULL, 0x0A3B7B1923564425ULL,
0x70428B155B4EAF1EULL, 0x32B26AFEF2A4998DULL, 0xF5A348C2089AC238ULL, 0xB753A929A170F4ABULL,
0x3971ED50550C43C1ULL, 0x7B810CBBFCE67552ULL, 0xBC902E8706D82EE7ULL, 0xFE60CF6CAF321874ULL,
0xE224479F47CB76A0ULL, 0xA0D4A674EE214033ULL, 0x67C58448141F1B86ULL, 0x253565A3BDF52D15ULL,
0xAB1721DA49899A7FULL, 0xE9E7C031E063ACECULL, 0x2EF6E20D1A5DF759ULL, 0x6C0603E6B3B7C1CAULL,
0xF6FAE5C07D3274CDULL, 0xB40A042BD4D8425EULL, 0x731B26172EE619EBULL, 0x31EBC7FC870C2F78ULL,
0xBFC9838573709812ULL, 0xFD39626EDA9AAE81ULL, 0x3A28405220A4F534ULL, 0x78D8A1B9894EC3A7ULL,
0x649C294A61B7AD73ULL, 0x266CC8A1C85D9BE0ULL, 0xE17DEA9D3263C055ULL, 0xA38D0B769B89F6C6ULL,
0x2DAF4F0F6FF541ACULL, 0x6F5FAEE4C61F773FULL, 0xA84E8CD83C212C8AULL, 0xEABE6D3395CB1A19ULL,
0x90C79D3FEDD3F122ULL, 0xD2377CD44439C7B1ULL, 0x15265EE8BE079C04ULL, 0x57D6BF0317EDAA97ULL,
0xD9F4FB7AE3911DFDULL, 0x9B041A914A7B2B6EULL, 0x5C1538ADB04570DBULL, 0x1EE5D94619AF4648ULL,
0x02A151B5F156289CULL, 0x4051B05E58BC1E0FULL, 0x87409262A28245BAULL, 0xC5B073890B687329ULL,
0x4B9237F0FF14C443ULL, 0x0962D61B56FEF2D0ULL, 0xCE73F427ACC0A965ULL, 0x8C8315CC052A9FF6ULL,
0x3A80143F5CF17F13ULL, 0x7870F5D4F51B4980ULL, 0xBF61D7E80F251235ULL, 0xFD913603A6CF24A6ULL,
0x73B3727A52B393CCULL, 0x31439391FB59A55FULL, 0xF652B1AD0167FEEAULL, 0xB4A25046A88DC879ULL,
0xA8E6D8B54074A6ADULL, 0xEA16395EE99E903EULL, 0x2D071B6213A0CB8BULL, 0x6FF7FA89BA4AFD18ULL,
0xE1D5BEF04E364A72ULL, 0xA3255F1BE7DC7CE1ULL, 0x64347D271DE22754ULL, 0x26C49CCCB40811C7ULL,
0x5CBD6CC0CC10FAFCULL, 0x1E4D8D2B65FACC6FULL, 0xD95CAF179FC497DAULL, 0x9BAC4EFC362EA149ULL,
0x158E0A85C2521623ULL, 0x577EEB6E6BB820B0ULL, 0x906FC95291867B05ULL, 0xD29F28B9386C4D96ULL,
0xCEDBA04AD0952342ULL, 0x8C2B41A1797F15D1ULL, 0x4B3A639D83414E64ULL, 0x09CA82762AAB78F7ULL,
0x87E8C60FDED7CF9DULL, 0xC51827E4773DF90EULL, 0x020905D88D03A2BBULL, 0x40F9E43324E99428ULL,
0x2CFFE7D5975E55E2ULL, 0x6E0F063E3EB46371ULL, 0xA91E2402C48A38C4ULL, 0xEBEEC5E96D600E57ULL,
0x65CC8190991CB93DULL, 0x273C607B30F68FAEULL, 0xE02D4247CAC8D41BULL, 0xA2DDA3AC6322E288ULL,
0xBE992B5F8BDB8C5CULL, 0xFC69CAB42231BACFULL, 0x3B78E888D80FE17AULL, 0x7988096371E5D7E9ULL,
0xF7AA4D1A85996083ULL, 0xB55AACF12C735610ULL, 0x724B8ECDD64D0DA5ULL, 0x30BB6F267FA73B36ULL,
0x4AC29F2A07BFD00DULL, 0x08327EC1AE55E69EULL, 0xCF235CFD546BBD2BULL, 0x8DD3BD16FD818BB8ULL,
0x03F1F96F09FD3CD2ULL, 0x41011884A0170A41ULL, 0x86103AB85A2951F4ULL, 0xC4E0DB53F3C36767ULL,
0xD8A453A01B3A09B3ULL, 0x9A54B24BB2D03F20ULL, 0x5D45907748EE6495ULL, 0x1FB5719CE1045206ULL,
0x919735E51578E56CULL, 0xD367D40EBC92D3FFULL, 0x1476F63246AC884AULL, 0x568617D9EF46BED9ULL,
0xE085162AB69D5E3CULL, 0xA275F7C11F7768AFULL, 0x6564D5FDE549331AULL, 0x279434164CA30589ULL,
0xA9B6706FB8DFB2E3ULL, 0xEB46918411358470ULL, 0x2C57B3B8EB0BDFC5ULL, 0x6EA7525342E1E956ULL,
0x72E3DAA0AA188782ULL, 0x30133B4B03F2B111ULL, 0xF7021977F9CCEAA4ULL, 0xB5F2F89C5026DC37ULL,
0x3BD0BCE5A45A6B5DULL, 0x79205D0E0DB05DCEULL, 0xBE317F32F78E067BULL, 0xFCC19ED95E6430E8ULL,
0x86B86ED5267CDBD3ULL, 0xC4488F3E8F96ED40ULL, 0x0359AD0275A8B6F5ULL, 0x41A94CE9DC428066ULL,
0xCF8B0890283E370CULL, 0x8D7BE97B81D4019FULL, 0x4A6ACB477BEA5A2AULL, 0x089A2AACD2006CB9ULL,
0x14DEA25F3AF9026DULL, 0x562E43B4931334FEULL, 0x913F6188692D6F4BULL, 0xD3CF8063C0C759D8ULL,
0x5DEDC41A34BBEEB2ULL, 0x1F1D25F19D51D821ULL, 0xD80C07CD676F8394ULL, 0x9AFCE626CE85B507ULL};
uint64_t HtmlHelper::crc64(uint8_t *data, size_t length, uint64_t crc)
{
crc = crc ^ uint64_t(-1);
for (size_t i = 0; i < length; ++i)
{
uint8_t c = data[i];
crc = crc64_table[(crc ^ (uint8_t)c) & 0xFF] ^ (crc >> 8);
}
return crc ^ uint64_t(-1);
}
uint64_t HtmlHelper::crc64(const std::string&value, uint64_t crc)
{
crc = crc ^ uint64_t(-1);
for (auto c: value)
{
crc = crc64_table[(crc ^ (uint8_t)c) & 0xFF] ^ (crc >> 8);
}
return crc ^ uint64_t(-1);
}
std::string HtmlHelper::generateEtag(const std::filesystem::path &path)
{
uint64_t crc = 0;
crc = crc64(path.string());
auto fTime = std::filesystem::last_write_time(path);
crc = crc64((uint8_t*)&fTime, sizeof(fTime));
return std::to_string(crc);
}
+2 -2
View File
@@ -24,7 +24,7 @@ public:
void Run();
// Stop everything, shutting down gracefully.
void Stop();
void Wait();
void WaitForClose();
bool IsFinished();
// Stop, but don't wait. Suitable for use in a signal handler.
@@ -78,7 +78,7 @@ private:
std::atomic<bool> stopping;
std::vector<CallbackEntry> postedEvents;
bool threadJoined;
bool threadStarted;
std::thread serviceThread;
std::mutex postMutex;
};
+9
View File
@@ -29,12 +29,16 @@ namespace pipedal {
class HtmlHelper {
public:
static std::string timeToHttpDate();
static std::string timeToHttpDate(time_t time);
static std::string timeToHttpDate(std::filesystem::file_time_type time);
static std::string encode_url_segment(const char*pStart, const char*pEnd, bool isQuerySegment = false);
static std::string encode_url_segment(const std::string &segment, bool isQuerySegment = false) {
return encode_url_segment(segment.c_str(), segment.c_str() + segment.length(), isQuerySegment);
}
static void encode_url_segment(std::ostream&os, const char*pStart, const char *pEnd, bool isQuerySegment = false);
static void encode_url_segment(std::ostream&os, const std::string&segment, bool isQuerySegment = false)
{
@@ -54,6 +58,11 @@ public:
static std::string SafeFileName(const std::string &name);
static std::string HtmlEncode(const std::string& text);
static uint64_t crc64(uint8_t *data, size_t length,uint64_t crc = 0);
static uint64_t crc64(const std::string&value, uint64_t crc = 0);
static std::string generateEtag(const std::filesystem::path &path);
};
} // namespace.
+1 -1
View File
@@ -607,7 +607,7 @@ namespace pipedal
if (!obj) {
write_raw("null");
} else {
write(obj.get());
write(obj.value());
}
}
+4
View File
@@ -218,6 +218,10 @@ bool pipedal::IsSubdirectory(const std::filesystem::path &path, const std::files
if ((*i) != (*iPath))
{
if (*i == "" && ++i == basePath.end()) // match xyz/ (trailing /)
{
break;
}
return false;
}
++iPath;
+6 -2
View File
@@ -9,11 +9,11 @@
<img src="https://img.shields.io/github/downloads/rerdavies/pipedal/total?color=%23808080&link=https%3A%2F%2Frerdavies.github.io%2Fpipedal%2Fdownload.html"/>
Download:&nbsp;<a href='https://rerdavies.github.io/pipedal/download.html'>v1.4.77</a>
Download:&nbsp;<a href='https://rerdavies.github.io/pipedal/download.html'>v1.4.79</a>
Website:&nbsp;[https://rerdavies.github.io/pipedal](https://rerdavies.github.io/pipedal).
Documentation:&nbsp;[https://rerdavies.github.io/pipedal/Documentation.html](https://rerdavies.github.io/pipedal/Documentation.html).
#### NEW version 1.4.77 Release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details. New Phaser and Graphic Equalizer plugins.
#### NEW version 1.4.79 Release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details. New Phaser and Graphic Equalizer plugins.
&nbsp;
@@ -56,8 +56,12 @@ https://github.com/user-attachments/assets/9a9fd0c6-78fc-4284-8b44-6a1929c00cc6
### [Optimizing Audio Latency](https://rerdavies.github.io/pipedal/AudioLatency.html)
### [Command-Line Configuration of PiPedal](https://rerdavies.github.io/pipedal/CommandLine.html)
### [Changing the Web Server Port](https://rerdavies.github.io/pipedal/ChangingTheWebServerPort.html)
%nbsp;
### [Using LV2 Audio Plugins](https://rerdavies.github.io/pipedal/UsingLv2Plugins.md)
### [Which LV2 Plugins does PiPedal support?](https://rerdavies.github.io/pipedal/WhichLv2PluginsAreSupported.html)
### [Support for LV2 Plugins with MOD User Interfaces](https://rerdavies.github.io/pipedal/ModUiSupport.html)
 &nbsp;
+1 -1
View File
@@ -29,7 +29,7 @@
let galleryFrame = 0;
let calculateWidth = function () {
width = Math.min(maxWidth, window.innerWidth * 0.8);
width = Math.min(maxWidth, document.documentElement.clientWidth * 0.8);
height = width * aspectY / aspectX;
frameWidth = width + borderWidth * 2;
frameHeight = height + borderWidth * 2;
Binary file not shown.
Binary file not shown.
Binary file not shown.
+112
View File
@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg132"
width="32"
height="32"
viewBox="0 0 32 32"
sodipodi:docname="red-light-on.png"
xml:space="preserve"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs136"><linearGradient
inkscape:collect="always"
id="linearGradient1239"><stop
style="stop-color:#ffffff;stop-opacity:0.84431136;"
offset="0"
id="stop1235" /><stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop1237" /></linearGradient><linearGradient
inkscape:collect="always"
id="linearGradient1051"><stop
style="stop-color:#580b0b;stop-opacity:1;"
offset="0"
id="stop1047" /><stop
style="stop-color:#252525;stop-opacity:1;"
offset="1"
id="stop1049" /></linearGradient><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1051"
id="linearGradient1053"
x1="16"
y1="24"
x2="16"
y2="8"
gradientUnits="userSpaceOnUse" /><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient1239"
id="radialGradient1241"
cx="15.964222"
cy="13.292918"
fx="15.964222"
fy="13.292918"
r="4.8461695"
gradientTransform="matrix(1,0,0,0.74626396,0,3.3728925)"
gradientUnits="userSpaceOnUse" /></defs><sodipodi:namedview
id="namedview134"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="23.583986"
inkscape:cx="16.536645"
inkscape:cy="17.066666"
inkscape:window-width="1867"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="g138" /><g
inkscape:groupmode="layer"
inkscape:label="Image"
id="g138"><image
width="32"
height="32"
preserveAspectRatio="none"
xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlz
AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAQSSURB
VFiF7VZNaFxVFP7O/Xvv3Zk3P8/JhEnsKCNp0xJokFilSJCC0GUo6NKNSxeuBBcKwZWLUsRNUSJU
ihQyuAmYjYsORRChwUKpENJYaUpqtbGxfTN5/9dFJyo0b5pUXZkPDlw495zz3XPuuecC+9jH/x30
BPs927bLQghHSsmVUpmUMtRa94jo/vLysg/A/BcEvEKhMCqlFEqpjIgyy7IyIsps284YY5nWOhNC
hEmS/La0tNT7twiQ4zijlmUNKaUyR6kWB04x4CUDjPad3CLOv7G0vlBvNH6QUmaWZfkLCwsP/jGB
Wq02whirOY4jRJK8w4153QoCpuIYPE0BABnniKREYNspSfl58+DB9yuVSgBgq91ubz0xgVarVU6S
5GnOuRBJ8okVhi9Wt7bw7NgYRlstFMplAIC/uYn1Gzfw08oK7jkOQsu6NDE1dapYLAZBEATtdjvN
i8EHkRsaGhpRSsGE4Xs6DE82whBT09NoNJuQUgJpCqQplJR4ql5HfXgYvdVVhETP/Hz3rjt94sRi
vV5Hp9PJ9pyBQ4cOucVisR74/ljy4MFXzc1NNjYxgaEDBwYlDb/cvInr167hZqWSJpxPrq6tXeur
duwMludIa207jhNHQfCaFwTMNgalchlJrzdQypUKbGPgBQEn4M35+Xk2Pz+fG0fkKTzPY0KI2KTp
y9U4hgKQ9XogkWsCAMiSBAqAF8e4C7yKv8q84z3I9aaUyoQQKYDRYpqCAfDv3EGxWh1IwL93DxIA
S1MYY5rdbpcVCoXcUucSGB4ejrvdrgFgZP8Y3du3oQBI297RJg6CP/ckAGAMOOe83O+WPRFQSkVK
KcCYtYTzw3aSAFkGf30dqlCAtCxwKQEAaRwjDkNE3S6EeXjXQs7BOF+r1WpcKZXbBbkERkZGovX1
dTJEX/8u5WEvSR4qjIHxfUS+/4iN/Nt6U0pIIS42Gg3GGMt9B3IJzM7OpgDMeKs1dyvL3nouCPj2
6R6HmAi3HCc9eOTIF1EU8Y2NjdzXcNBDhNnZWTZx9OjG91eu1Hucv3AgisD7RnnCAFxxXYSl0tyn
5861lVI4fvx47kzI7c9teJ7HT87MvHvHcS4uuS6ICArYUUCEJdfFr8XipY/Onv1ASsnHx8cfrdVu
M9DpdMzMzAybnJykkWbzy2+vXq2uSjkJImYDsIwBEcEXAj9aFr5z3TSqVj+bO3/+7WazmZVKpZ7W
euBY3tV/YHFx0QJgSSnp4zNnDt+4fv2NOIpeybKsCQCMsTWtdef5Y8cufHj69IrWmrTWPhHdf5zv
XX9ILl++LAHoNE0F55zK5TIREbmuS9voB04B3CeigWN4zwS2sbKyYnmeZ4VhqEqlEu/7MIVCIQaw
BSAgol1/yfaxj338AXYNhqZlqq9VAAAAAElFTkSuQmCC
"
id="image412"
x="0"
y="0" /><circle
style="fill:url(#linearGradient1053);fill-opacity:1;stroke:#000000;stroke-width:1;stop-color:#000000;stroke-dasharray:none;stroke-opacity:1"
id="path468"
cx="16"
cy="16"
r="7.9999995" /><ellipse
style="fill:url(#radialGradient1241);fill-opacity:1;stroke:#000000;stroke-width:0;stroke-dasharray:none;stroke-opacity:1;stop-color:#000000"
id="path1231"
cx="15.964222"
cy="13.292918"
rx="4.3461695"
ry="3.1165216" /></g></svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.
Executable
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
# Run local jeckyl-hosted sevever for GitHub documentation page.
# See https://jekyllrb.com/docs/installation/ for instructions on how to isntall jeckyll locally.
# Then
# cd docs
# bundle install
#
cd docs
bundle exec jekyll serve --host
+3
View File
@@ -16,8 +16,11 @@
#### [Optimizing Audio Latency](AudioLatency.md)
#### [Command-Line Configuration of PiPedal](CommandLine.md)
#### [Changing the Web Server Port](ChangingTheWebServerPort.md)
&nbsp;
#### [Using LV2 Audio Plugins](UsingLv2Plugins.md)
#### [Which LV2 Plugins does PiPedal support?](WhichLv2PluginsAreSupported.md)
#### [LV2 Plugins with MOD User Interfaces](ModUiSupport.md)
&nbsp;
+2
View File
@@ -31,3 +31,5 @@ gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin]
# Lock `http_parser.rb` gem to `v0.6.x` on JRuby builds since newer versions of the gem
# do not have a Java counterpart.
gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby]
gem "webrick", "~> 1.9"
+5 -3
View File
@@ -205,14 +205,14 @@ GEM
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
mercenary (0.3.6)
mini_portile2 (2.8.9)
minima (2.5.1)
jekyll (>= 3.5, < 5.0)
jekyll-feed (~> 0.9)
jekyll-seo-tag (~> 2.1)
minitest (5.19.0)
nokogiri (1.18.8-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.8-x86_64-linux-gnu)
nokogiri (1.18.8)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
octokit (4.25.1)
faraday (>= 1, < 3)
@@ -249,6 +249,7 @@ GEM
unf_ext
unf_ext (0.0.8.2)
unicode-display_width (1.8.0)
webrick (1.9.1)
PLATFORMS
aarch64-linux
@@ -262,6 +263,7 @@ DEPENDENCIES
tzinfo (~> 1.2)
tzinfo-data
wdm (~> 0.1.1)
webrick (~> 1.9)
BUNDLED WITH
2.4.19
+5 -5
View File
@@ -13,18 +13,18 @@ page_icon: img/Install4.jpg
Download the most recent Debian (.deb) package for your platform:
- [Raspberry Pi OS bookworm (aarch64) v1.4.77](https://github.com/rerdavies/pipedal/releases/download/v1.4.77/pipedal_1.4.77_arm64.deb)
- [Ubuntu 24.x (aarch64) v1.4.77](https://github.com/rerdavies/pipedal/releases/download/v1.4.77/pipedal_1.4.77_arm64.deb)
- [Ubuntu 24.x (amd64) v1.4.77](https://github.com/rerdavies/pipedal/releases/download/v1.4.77/pipedal_1.4.77_amd64.deb)
- [Raspberry Pi OS bookworm (aarch64) v1.4.79](https://github.com/rerdavies/pipedal/releases/download/v1.4.79/pipedal_1.4.79_arm64.deb)
- [Ubuntu 24.x (aarch64) v1.4.79](https://github.com/rerdavies/pipedal/releases/download/v1.4.79/pipedal_1.4.79_arm64.deb)
- [Ubuntu 24.x (amd64) v1.4.79](https://github.com/rerdavies/pipedal/releases/download/v1.4.79/pipedal_1.4.79_amd64.deb)
Version 1.4.77 has been tested on Raspberry Pi OS bookworm, Ubuntu 24.04 (amd64), and Ubuntu 24.10 (aarch64). Download the appropriate package for your platform, and install using the following procedure:
Version 1.4.79 has been tested on Raspberry Pi OS bookworm, Ubuntu 24.04 (amd64), and Ubuntu 24.10 (aarch64). Download the appropriate package for your platform, and install using the following procedure:
```
sudo apt update
sudo apt upgrade
cd ~/Downloads
sudo apt-get install ./pipedal_1.4.77_arm64.deb
sudo apt-get install ./pipedal_1.4.79_arm64.deb
```
You MUST use `apt-get`. `apt` will not install downloaded packages; and `dpkg -i` will not install dependencies.
+35
View File
@@ -0,0 +1,35 @@
---
page_icon: img/moduiThumb.jpg
icon_width: 120px
---
## Support for LV2 Plugins with MOD User Interfaces
{% include pageIconL.html %}
Some LV2 plugins (currently a minority of plugins) provide custom plugin user interfaces based on [MOD Audio's](https://mod.audio/desktop/)
[ModUi framework](https://wiki.mod.audio/wiki/MOD_Web_GUI_Framework). When using such a plugin, you can choose whether to use the MOD Web GUI Framework user interface (the MOD UI) or the default PiPedal user interface. For the most part, the same functionality is available in both interfaces.
Plugins that are distributed via Linux distributions usually do not implement MOD user interfaces; but all (or almost all) of the plugins that are available from the [PatchStorage website](https://patchstorage.com/platform/lv2-plugins/) do implement MOD user interfaces. Many plugins that are available from GitHub or other sources also implement MOD user interfaces. Often you can find updated versions of a plugin that is published by a distro on PatchStorage which does provide a MOD UI.
Here's an example of a MOD UI for the ZAM Eq2 plugin:
<img src="img/zam-modui.jpg" alt="ZAM Eq2 MOD UI" style="width: 100%; max-width: 400px;box-sizing: border-box;padding-left: 24px; padding-right: 24px; "/>
And the PiPedal UI for the same plugin:
<img src="img/zam-pp-ui.png" alt="ZAM Eq2 PiPedal UI" style="display: block; box-sizing: border-box; padding-left: 24px; padding-right: 24px; width: 100%; max-width: 400px; margin-left: auto; margin-right: auto"/>
Which UI you use is entirely up to you. MOD UIs tend to be a bit unfriendly for small-format devices like phones, but they do look great in a desktop browser. Sometimes the MOD UI will make better use of available screen space; sometimes the Pipedal UI will look better. It really depends on the plugin and your personal preferences. In this particular case, the MOD UI is more space efficient, but that is not always true.
To select which user interface to use, tap on the UI selection button in the toolbar for the plugin. If the plugin does not provide a MOD UI, the button will be disabled.
<img src="img/selectUi.png" alt="MOD UI selection button" style="display: block; width: 100%; padding-left: 20px; padding-right: 20px; max-width: 300px; margin-left: auto; margin-right: auto"/>
Support for MOD UIs in PiPedal is still experimental, so you may encounter some issues. PiPedal does not yet support all of the features of the MOD UI framework, not because it can't, but because we haven't been able to locate examples of plugins that use those features, which makes it difficult to test. If you encounter any issues, please report them on PiPedal's [GitHub Issues page](https://github.com/rerdavies/pipedal/issues), and we will do our best to provide quick fixes.
&nbsp;
--------
[<< Which Plugins are Supported?](WhichLv2PluginsAreSupported.md) | [Up](Documentation.md) | [BuildingPiPedal from Source >>](BuildingPiPedalFromSource.md)
+34
View File
@@ -1,4 +1,38 @@
# Release Notes
## Pipedal 1.4.79 Beta
The Big Feature:
- Support for custom MOD User Interfaces for plugins that prove a MOD UI. See the [MOD UI documentation page](https://rerdavies.github.io/pipedal/ModUiSupport.html) for details on how to use MOD User Interfaces with PiPedal. This feature is experimental. If you experience problems with plugins that have MOD user interfaces, please report them on the [PiPedal GitHub Issues page](https://github.com/rerdavies/pipedal/issues).
New in this relase:
- PiPedal remembers which plugin you were viewing when you reload a preset.
- Keep Screen On setting in the PiPedal Remote Client (requires updated Android client).
- Lock Screen Orientation setting in the PiPedal Remote Client (requires updated Android client).
- Disable the default startup preset if it has previously caused a crash of the PiPedal process.
If you are using the Android PiPedal remote app, you should make sure it has upgraded to the latest version. The new Remote app has been fully rolled out and is now available in the Google Play Store. It should update automatically, but you can check by opening the App Info for PiPedal Remote. If an "Update" button is displayed, click it to upgrade to the current version right away. If you have received the update already, you should notice differences in the App startup procedure (notably an animated splash screen).
The PiPedal Remote Android app has been updated to provide the following new features:
- new Keep Screen On settings.
- new Lock Screen Orientation setting.
- Edit controls are automatically repositioned above the Keyboard IME when it opens.
- The Zoomed Control feature (large controls in landscape mode on phones) has been removed. Improved scrolling provides a better solution to the problem of controls being obscured by the keyboard IME.
- Support for Android 16 Edge-to-Edge display.
- Reduced display flickering while connecting.
- Complete support for Night Mode (the Android UI now follows the PiPedal Theme setting).
- Shiny new graphics on the opening screen in support of Android 16's Edge-to-Edge display feature (but you can see it in other versions of Android as well).
Bug Fixes:
- The "Listen for Message" buttons in the MIDI Bindings dialog now ignore CC0 (Bank Select), and CC32-63 (CC LSB) MIDI message, since CC0 is used to control preset selection.
- Fixed errors loading plugins with MIDI Event ports.
- Changing volume control on low-resolution displays breaks web page.
- MIDI binding for bypass controls have been restored after having been accidentally disabled in v1.4.77.
## PiPedal 1.4.77 Beta
New TooB Player plugin; improvements to MIDI input handling; and a slew of fit-and finish
+2 -3
View File
@@ -9,9 +9,8 @@ following conditions:
- Must be remotely controllable (no hard dependency on GUI-only controls), which is true of all but a tiny minority of LV2 plugins.
If you install a new LV2 plugin, PiPedal will detect the change and make it available immediately. Wait a few seconds, and the newly-installed plugin should show up in the list of available plugins.
PiPedal does not currently allow plugins to provide custom user interfaces; it displays controls that are declared by the LV2 plugin without customization. In practice, this means that LV2 plugins with simple sets of controls work well, but those with complex sets of controls can be difficult to use. However, we would be pleased to collaborate with developers who need more than the basic set of controls. This is a feature set that Pipedal needs to address, so we would be happy to collaborate with other developers on this. Please contact rerdavies at gmail.com for further details.
--------
[<< Using LV2 Audio Plugins](UsingLv2Plugins.md) | [Up](Documentation.md) | [BuildingPiPedal from Source >>](BuildingPiPedalFromSource.md)
[<< Using LV2 Audio Plugins](UsingLv2Plugins.md) | [Up](Documentation.md) | [MOD UI Support >>](ModUiSupport.md)
+1
View File
@@ -36,6 +36,7 @@ header_pages:
theme: minima
plugins:
- jekyll-feed
- jekyll-seo-tag
include:
- .well-known
+1 -1
View File
@@ -34,7 +34,7 @@
let galleryFrame = 0;
let calculateWidth = function () {
width = Math.min(maxWidth, window.innerWidth * 0.8);
width = Math.min(maxWidth, document.documentElement.clientWidth * 0.8);
height = width * aspectY / aspectX;
frameWidth = width + borderWidth * 2;
frameHeight = height + borderWidth * 2;
-3
View File
@@ -1,7 +1,4 @@
<div style="max-width: 30%; width: {{page.icon_width}}; background: #FFF; z-index: -1; margin-right: 18px; float: left">
<img src="{{page.page_icon}}" style="width: 100%;background: #FFF; z-index: -1;
float: right" />
<div>
<p style="font-size: 10px; text-align: right; color: #888">Generated by DALL-E 2</p>
</div>
</div>
+4 -4
View File
@@ -4,9 +4,9 @@
Download the most recent Debian (.deb) package for your platform:
- [Raspberry Pi OS bookworm (aarch64) v1.4.77 Beta](https://github.com/rerdavies/pipedal/releases/download/v1.4.77/pipedal_1.4.77_arm64.deb)
- [Ubuntu 24.x (aarch64) v1.4.77 Alpha](https://github.com/rerdavies/pipedal/releases/download/v1.4.77/pipedal_1.4.77_arm64.deb)
- [Ubuntu 24.x (amd64) v1.4.77 Alpha](https://github.com/rerdavies/pipedal/releases/download/v1.4.77/pipedal_1.4.77_amd64.deb)
- [Raspberry Pi OS bookworm (aarch64) v1.4.79 Beta](https://github.com/rerdavies/pipedal/releases/download/v1.4.79/pipedal_1.4.79_arm64.deb)
- [Ubuntu 24.x (aarch64) v1.4.79 Alpha](https://github.com/rerdavies/pipedal/releases/download/v1.4.79/pipedal_1.4.79_arm64.deb)
- [Ubuntu 24.x (amd64) v1.4.79 Alpha](https://github.com/rerdavies/pipedal/releases/download/v1.4.79/pipedal_1.4.79_amd64.deb)
Install the package by running
@@ -14,7 +14,7 @@ Install the package by running
```
sudo apt update
cd ~/Downloads
sudo apt-get install ./pipedal_1.4.77_arm64.deb
sudo apt-get install ./pipedal_1.4.79_arm64.deb
```
You MUST use `apt-get` to install the package. `apt install` will NOT install the package correctly. The message about missing permissions given by `apt-get` is
expected, and can be safely ignored.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 401 KiB

+264
View File
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 514 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

+1 -1
View File
@@ -93,7 +93,7 @@ cabir:impulseFile3
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 63 ;
lv2:microVersion 64 ;
rdfs:comment """
TooB Cab IR is a convolution-based guitar cabinet impulse response simulator.
+1 -1
View File
@@ -50,7 +50,7 @@ toob:frequencyResponseVector
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 63 ;
lv2:microVersion 64 ;
mod:brand "TooB";
mod:label "TooB CabSim";
@@ -53,7 +53,7 @@ toobimpulse:impulseFile
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 63 ;
lv2:microVersion 64 ;
rdfs:comment """
Convolution reverb is a notoriously compute-intensive effect. If you are having performance issues, use the Max T control to constrain the length of the impulse file to
@@ -51,7 +51,7 @@ toobimpulse:impulseFile
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 63 ;
lv2:microVersion 64 ;
rdfs:comment """
+1 -1
View File
@@ -66,7 +66,7 @@ inputStage:filterGroup
doap:license <https://two-play.com/TooB/licenses/isc> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 63 ;
lv2:microVersion 64 ;
mod:brand "TooB";
mod:label "TooB Input";
+1 -1
View File
@@ -68,7 +68,7 @@ pstage:stage3
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 63 ;
lv2:microVersion 64 ;
mod:brand "TooB";
mod:label "Power Stage";
+1 -1
View File
@@ -57,7 +57,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 63 ;
lv2:microVersion 64 ;
rdfs:comment "TooB spectrum analyzer" ;
mod:brand "TooB";
+1 -1
View File
@@ -57,7 +57,7 @@ tonestack:eqGroup
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 63 ;
lv2:microVersion 64 ;
uiext:ui <http://two-play.com/plugins/toob-tone-stack-ui>;
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -40,7 +40,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 63 ;
lv2:microVersion 64 ;
rdfs:comment """
Emulation of a Boss CE-2 Chorus.
+1 -1
View File
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 63 ;
lv2:microVersion 64 ;
rdfs:comment """
A straightforward no-frills digital delay.
+1 -1
View File
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 63 ;
lv2:microVersion 64 ;
rdfs:comment """
Emulation of a Boss BF-2 Flanger, based on circuit analysis and simulation of the original.
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 63 ;
lv2:microVersion 64 ;
rdfs:comment """
Digital emulation of a Boss BF-2 Flanger.
+1 -1
View File
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 63 ;
lv2:microVersion 64 ;
rdfs:comment """
Toob Freeverb is an Lv2 implementation of the famous Freeverb reverb effect. FreeVerb delivers a well-balanced reverb with very little tonal coloration.
+1 -1
View File
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 63 ;
lv2:microVersion 64 ;
rdfs:comment """
A 7 octave graphic equalizer, with frequencies chosen to be useful for EQ-ing guitar signals.
""" ;
+1 -1
View File
@@ -92,7 +92,7 @@ myprefix:output_group
doap:license <https://opensource.org/license/mit/> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 1 ;
lv2:microVersion 63 ;
lv2:microVersion 64 ;
ui:ui <http://two-play.com/plugins/toob-looper-four-ui>;
+1 -1
View File
@@ -78,7 +78,7 @@ myprefix:output_group
doap:license <https://opensource.org/license/mit/> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 1 ;
lv2:microVersion 63 ;
lv2:microVersion 64 ;
ui:ui <http://two-play.com/plugins/toob-looper-one-ui>;
+1 -1
View File
@@ -67,7 +67,7 @@ toobml:sagGroup
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 63 ;
lv2:microVersion 64 ;
rdfs:comment """
The TooB ML Amplifier plugin provides emulation of a variety of amplifiers and overdrive pedals that are implemented
using neural-network-based machine learning models of real amplifiers.
+1 -1
View File
@@ -40,7 +40,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 63 ;
lv2:microVersion 64 ;
rdfs:comment """
Remix a stereo input signal.
@@ -61,7 +61,7 @@ toobNam:eqGroup
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 63 ;
lv2:microVersion 64 ;
rdfs:comment """
A port of Steven Atkinson's Neural Amp Modeler to LV2.
+1 -1
View File
@@ -54,7 +54,7 @@ noisegate:envelope_group
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 63 ;
lv2:microVersion 64 ;
rdfs:comment """
A noise gate is an audio processing tool that controls the volume of an audio signal
by allowing it to pass through only when it exceeds a set threshold.
+1 -1
View File
@@ -40,7 +40,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 63 ;
lv2:microVersion 64 ;
rdfs:comment """
A loose emulation of an MXR® Phase 90 Phaser.
+1 -1
View File
@@ -60,7 +60,7 @@ toobPlayer:seek
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 63 ;
lv2:microVersion 64 ;
rdfs:comment """
Play audio files. Audio files can optionally be loooped by pressing the "Set loop" button.
+1 -1
View File
@@ -51,7 +51,7 @@ recordPrefix:audioFile
doap:license <https://opensource.org/license/mit/> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 1 ;
lv2:microVersion 63 ;
lv2:microVersion 64 ;
ui:ui <http://two-play.com/plugins/toob-record-mono-ui>;
+1 -1
View File
@@ -88,7 +88,7 @@ myprefix:loop3_group
doap:license <https://opensource.org/license/mit/> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 1 ;
lv2:microVersion 63 ;
lv2:microVersion 64 ;
ui:ui <http://two-play.com/plugins/toob-record-stereo-ui>;
+1 -1
View File
@@ -40,7 +40,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 63 ;
lv2:microVersion 64 ;
rdfs:comment """
TooB Tuner is a chromatic guitar tuner.
""" ;
+1 -1
View File
@@ -39,7 +39,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 63 ;
lv2:microVersion 64 ;
rdfs:comment """
Volume control.
+70 -40
View File
@@ -35,6 +35,7 @@
#include "PiPedalException.hpp"
#include "DummyAudioDriver.hpp"
#include "SchedulerPriority.hpp"
#include "CrashGuard.hpp"
#include "CpuUse.hpp"
@@ -1484,7 +1485,7 @@ namespace pipedal
#endif
}
std::jthread *audioThread = nullptr;
std::unique_ptr<std::jthread> audioThread;
bool audioRunning;
bool block = false;
@@ -1518,32 +1519,36 @@ namespace pipedal
} while (frames > 0);
return framesRead;
}
protected:
void ReadMidiData(uint32_t audioFrame)
{
AlsaMidiMessage message;
midiEventCount = 0;
while(alsaSequencer->ReadMessage(message,0))
while (alsaSequencer->ReadMessage(message, 0))
{
size_t messageSize = message.size;
if (messageSize == 0)
if (messageSize == 0)
{
continue;
}
if (midiEventMemoryIndex + messageSize >= this->midiEventMemory.size()) {
if (midiEventMemoryIndex + messageSize >= this->midiEventMemory.size())
{
continue;
}
if (midiEventCount >= this->midiEvents.size()) {
midiEvents.resize(midiEventCount*2);
if (midiEventCount >= this->midiEvents.size())
{
midiEvents.resize(midiEventCount * 2);
}
// for now, prevent META event messages from propagating.
if (message.data[0] == 0xFF && message.size > 1) {
// for now, prevent META event messages from propagating.
if (message.data[0] == 0xFF && message.size > 1)
{
continue;
}
MidiEvent *pEvent = midiEvents.data() + midiEventCount++;
pEvent->time = audioFrame;
pEvent->size = messageSize;
pEvent->size = messageSize;
pEvent->buffer = midiEventMemory.data() + midiEventMemoryIndex;
memcpy(
@@ -1551,11 +1556,10 @@ namespace pipedal
message.data,
message.size);
midiEventMemoryIndex += messageSize;
}
}
private:
private:
long WriteBuffer(snd_pcm_t *handle, uint8_t *buf, size_t frames)
{
long framesRead;
@@ -1592,6 +1596,8 @@ namespace pipedal
throw PiPedalStateException("Unable to start ALSA capture.");
}
CrashGuardLock crashGuardLock;
cpuUse.SetStartTime(cpuUse.Now());
while (true)
{
@@ -1759,7 +1765,7 @@ namespace pipedal
}
}
audioThread = new std::jthread([this]()
audioThread = std::make_unique<std::jthread>([this]()
{ AudioThread(); });
}
@@ -1773,8 +1779,7 @@ namespace pipedal
terminateAudio(true);
if (audioThread)
{
this->audioThread->join();
this->audioThread = 0;
this->audioThread = 0; // jthread joins.
}
Lv2Log::debug("Audio thread joined.");
}
@@ -1789,9 +1794,6 @@ namespace pipedal
AlsaSequencer::ptr alsaSequencer;
public:
virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) override
{
this->alsaSequencer = alsaSequencer;
@@ -1822,7 +1824,7 @@ namespace pipedal
{
for (size_t i = 0; i < buffer.size(); ++i)
{
// delete[] buffer[i];
delete[] buffer[i];
buffer[i] = 0;
}
buffer.clear();
@@ -1887,6 +1889,51 @@ namespace pipedal
snd_pcm_t *captureHandle = nullptr;
snd_pcm_hw_params_t *playbackHwParams = nullptr;
snd_pcm_hw_params_t *captureHwParams = nullptr;
Finally ff_playbackHandle{
[&playbackHandle]()
{
if (playbackHandle)
{
int rc = snd_pcm_close(playbackHandle);
if (rc < 0)
{
throw std::runtime_error("snd_pcm_close failed.");
}
playbackHandle = nullptr;
}
}};
Finally ff_captureHandle{
[&captureHandle]()
{
if (captureHandle)
{
int rc = snd_pcm_close(captureHandle);
if (rc < 0)
{
throw std::runtime_error("snd_pcm_close failed.");
}
captureHandle = nullptr;
}
}};
Finally ff_playbackHwParams{
[&playbackHwParams]()
{
if (playbackHwParams)
{
snd_pcm_hw_params_free(playbackHwParams);
}
}};
Finally ff_captureHwParams{
[&captureHwParams]()
{
if (captureHwParams)
{
snd_pcm_hw_params_free(captureHwParams);
}
}};
std::string alsaDeviceName = jackServerSettings.GetAlsaInputDevice();
bool result = false;
@@ -2001,28 +2048,6 @@ namespace pipedal
result = false;
throw;
}
if (playbackHwParams)
{
snd_pcm_hw_params_free(playbackHwParams);
playbackHwParams = nullptr;
}
if (captureHwParams)
{
snd_pcm_hw_params_free(captureHwParams);
captureHwParams = nullptr;
}
if (playbackHandle)
{
snd_pcm_close(playbackHandle);
playbackHandle = nullptr;
}
if (captureHandle)
{
snd_pcm_close(captureHandle);
captureHandle = nullptr;
}
return result;
}
@@ -2176,4 +2201,9 @@ namespace pipedal
}
#endif
}
void FreeAlsaGlobals()
{
snd_config_update_free_global(); // to get a clean Valgrind report.
}
} // namespace
+2
View File
@@ -40,5 +40,7 @@ namespace pipedal {
// test only.
void AlsaFormatEncodeDecodeTest(AudioDriverHost*driverHost);
void MidiDecoderTest();
void FreeAlsaGlobals(); // for valgrind. Free the Alsa configuration cache.
}
+13 -2
View File
@@ -931,6 +931,15 @@ private:
uint8_t cmd = (uint8_t)(event.buffer[0] & 0xF0);
bool isNote = cmd == 0x90 && event.buffer[2] != 0; // note on with velocity > 0.
bool isControl = cmd == 0xB0;
if (isControl)
{
// ignore bank select, and CC LSB values.
uint8_t cc1 = (uint8_t)(event.buffer[1]);
if (cc1 == 0 || (cc1 >= 32 && cc1 < 64))
{
isControl = false;
}
}
if (isNote || isControl)
{
MidiNotifyBody notifyBody (event.buffer[0],event.buffer[1], event.buffer[2]);
@@ -1502,8 +1511,10 @@ public:
if (property != nullptr && value != nullptr && property->type == uris.atom_URID)
{
LV2_URID propertyUrid = ((LV2_Atom_URID *)property)->body;
this->pNotifyCallbacks->OnPatchSetReply(instanceId, propertyUrid, value);
// this->pNotifyCallbacks->OnNotifyMaybeLv2StateChanged(instanceId);
if (this->pNotifyCallbacks)
{
this->pNotifyCallbacks->OnPatchSetReply(instanceId, propertyUrid, value);
}
}
}
}
+7
View File
@@ -206,6 +206,10 @@ else()
endif()
set (PIPEDAL_SOURCES
CrashGuard.cpp CrashGuard.hpp
ModTemplateGenerator.cpp ModTemplateGenerator.hpp
WebServerMod.cpp WebServerMod.hpp
ModGui.cpp ModGui.hpp
PipewireInputStream.cpp PipewireInputStream.hpp
AudioFiles.cpp AudioFiles.hpp
AudioFileMetadataReader.cpp AudioFileMetadataReader.hpp
@@ -414,6 +418,8 @@ add_executable(AuxInTest
add_executable(pipedaltest
testMain.cpp
ModGuiTest.cpp
PipewireInputStreamTest.cpp
AudioFilesTest.cpp
@@ -762,6 +768,7 @@ add_executable(pipedal_latency_test
DummyAudioDriver.cpp DummyAudioDriver.hpp
JackConfiguration.hpp JackConfiguration.cpp
JackServerSettings.hpp JackServerSettings.cpp
CrashGuard.cpp CrashGuard.hpp
CpuUse.hpp
CpuUse.cpp
)
+99
View File
@@ -0,0 +1,99 @@
/*
* 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 "CrashGuard.hpp"
#include "ofstream_synced.hpp"
#include <fstream>
#include <Lv2Log.hpp>
using namespace pipedal;
namespace fs = std::filesystem;
int CrashGuard::crashCount = 0;
std::mutex CrashGuard::mutex;
std::filesystem::path CrashGuard::fileName;
int64_t CrashGuard::depth = 0;
void CrashGuard::SetCrashGuardFileName(const std::filesystem::path &path)
{
CrashGuard::fileName = path;
{
std::ifstream f{path};
if (f.is_open())
{
f >> crashCount;
}
if (crashCount > 0) {
Lv2Log::info("CrashGuard: Detected previous crash, count = %d", (int)crashCount);
}
}
}
bool CrashGuard::HasCrashed()
{
return crashCount > 4;
}
void CrashGuard::ClearCrashFlag()
{
crashCount = 0;
try
{
if (fs::exists(fileName))
{
fs::remove(fileName);
}
}
catch (const std::exception &ignored)
{
}
}
void CrashGuard::EnterCrashGuardZone()
{
std::lock_guard lock{mutex};
if (depth++ == 0)
{
if (!fileName.empty()) {
ofstream_synced f{fileName};
f << (crashCount + 1) << std::endl;
}
}
}
void CrashGuard::LeaveCrashGuardZone()
{
std::lock_guard lock{mutex};
if (--depth == 0)
{
try
{
if (!fileName.empty() && fs::exists(fileName))
{
fs::remove(fileName);
}
}
catch (const std::exception &ignored)
{
}
}
}
+76
View File
@@ -0,0 +1,76 @@
/*
* 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 <mutex>
#include <atomic>
#include <filesystem>
namespace pipedal
{
/**
* @brief Crash protection.
*
* Detect the situation where PiPedal has crashed multiple times due to SIG_SEGVS caused by a plugin.
*
* The basic premise: mark segments of code as "crash protected". If Pipedal shuts down without exiting the
* crash guard zone for some fixed number of occasions (3?), then set a flag that will prevent
* loading of the default plugin when PiPedal next loads. Since PiPedal restarts indefinitely while
* running as a service, this allows users to recover after they have committed a plugin that crashes
* into their current preset.
*
*/
class CrashGuard
{
public:
static void SetCrashGuardFileName(const std::filesystem::path &path);
static bool HasCrashed();
static void ClearCrashFlag();
static void EnterCrashGuardZone();
static void LeaveCrashGuardZone();
private:
static int crashCount;
static std::mutex mutex;
static std::filesystem::path fileName;
static int64_t depth;
};
class CrashGuardLock {
public:
CrashGuardLock() {
CrashGuard::EnterCrashGuardZone();
}
~CrashGuardLock() {
CrashGuard::LeaveCrashGuardZone();
}
CrashGuardLock(const CrashGuardLock &) = delete;
CrashGuardLock &operator=(const CrashGuardLock &) = delete;
CrashGuardLock(CrashGuardLock &&) = delete;
CrashGuardLock &operator=(CrashGuardLock &&) = delete;
};
};
+2
View File
@@ -38,6 +38,7 @@
#include <stdexcept>
#include "ss.hpp"
#include "SchedulerPriority.hpp"
#include "CrashGuard.hpp"
#include "CpuUse.hpp"
@@ -287,6 +288,7 @@ namespace pipedal
bool ok = true;
CrashGuardLock crashGuardLock;
while (true)
{
+41 -16
View File
@@ -100,7 +100,7 @@ Lv2Effect::Lv2Effect(
lilv_node_free(uriNode);
{
AutoLilvNode bundleUri = lilv_plugin_get_bundle_uri(pPlugin);
char *bundleUriString = lilv_file_uri_parse(bundleUri.AsUri().c_str(), nullptr);
char* bundleUriString = lilv_file_uri_parse(bundleUri.AsUri().c_str(), nullptr);
std::string storagePath = pHost_->GetPluginStoragePath();
@@ -108,7 +108,7 @@ Lv2Effect::Lv2Effect(
pHost_->GetMapFeature().GetMap(),
logFeature.GetLog(),
bundleUriString,
storagePath);
storagePath);
mapPathFeature.Prepare(&(pHost_->GetMapFeature()));
mapPathFeature.SetPluginStoragePath(pHost_->GetPluginStoragePath());
@@ -117,11 +117,12 @@ Lv2Effect::Lv2Effect(
const auto &fileProperties = info_->piPedalUI()->fileProperties();
for (const auto &fileProperty : fileProperties)
{
if (!fileProperty->resourceDirectory().empty())
{
mapPathFeature.AddResourceFileMapping({makeAbsolutePath(fileProperty->resourceDirectory(), bundleUriString),
makeAbsolutePath(fileProperty->directory(), pHost_->GetPluginStoragePath())});
}
fs::path targetPath = fileProperty->directory() / std::filesystem::path(bundleUriString).parent_path().filename();
mapPathFeature.AddResourceFileMapping({
bundleUriString,
storagePath / targetPath,
fileProperty->fileTypes()
});
}
}
@@ -243,14 +244,41 @@ Lv2Effect::Lv2Effect(
// now that we've loaded the preset, clear the uri, and save new state
// Why? because lilv doesn't provide facilities for reading state.
pedalboardItem.lv2State(this->stateInterface->Save());
RestoreState(pedalboardItem); // reload it with OUR map/unmap handling.
pedalboardItem.lilvPresetUri("");
}
else
{
RestoreState(pedalboardItem);
if (!RestoreState(pedalboardItem))
{
if (info->hasDefaultState())
{
// restore the default state.
try
{
// REsTORE from LV2_STATE__state default state.
AutoLilvNode pluginNode = lilv_new_uri(pWorld, info->uri().c_str());
LilvState *pState = lilv_state_new_from_world(pWorld, pHost->GetMapFeature().GetMap(), pluginNode);
if (pState)
{
if (this->stateInterface)
{
this->stateInterface->RestoreState(pState);
}
lilv_state_free(pState);
}
pedalboardItem.lv2State(this->stateInterface->Save());
RestoreState(pedalboardItem); // do it with OUR map/unmap file handling.
}
catch (const std::exception &e)
{
Lv2Log::warning(SS("Failed to restore default state for " << info->name() << ": " << e.what()));
}
}
}
}
}
void Lv2Effect::RestoreState(PedalboardItem &pedalboardItem)
bool Lv2Effect::RestoreState(PedalboardItem &pedalboardItem)
{
// Restore state if present.
if (this->stateInterface)
@@ -260,20 +288,18 @@ void Lv2Effect::RestoreState(PedalboardItem &pedalboardItem)
if (pedalboardItem.lv2State().isValid_)
{
this->stateInterface->Restore(pedalboardItem.lv2State());
return true;
}
else
{
// set the state to default state.
auto savedState = this->stateInterface->Save();
pedalboardItem.lv2State(savedState);
}
return false;
}
catch (const std::exception &e)
{
std::string name = pedalboardItem.pluginName();
Lv2Log::warning(SS(name << ": " << e.what()));
return false;
}
}
return true;
}
void Lv2Effect::ConnectControlPorts()
@@ -1116,4 +1142,3 @@ void Lv2Effect::SetPathPatchProperty(const std::string &propertyUri, const std::
{
mainThreadPathProperties[propertyUri] = jsonAtom;
}
+1 -1
View File
@@ -61,7 +61,7 @@ namespace pipedal
FileBrowserFilesFeature fileBrowserFilesFeature;
std::unique_ptr<StateInterface> stateInterface;
void RestoreState(PedalboardItem&pedalboardItem);
bool RestoreState(PedalboardItem&pedalboardItem);
LogFeature logFeature;
std::map<std::string,AtomBuffer> patchPropertyPrototypes;
+1 -1
View File
@@ -36,7 +36,7 @@ TEST_CASE( "PluginHost memory leak", "[lv2host_leak][Build][Dev]" ) {
{
PluginHost host;
host.Load("/usr/lib/lv2:/usr/local/lib/lv2:/usr/modep/lv2");
host.LoadLilv("/usr/lib/lv2:/usr/local/lib/lv2:/usr/modep/lv2");
}
MemStats finalMemory = GetMemStats();
+3
View File
@@ -27,6 +27,7 @@
#include "AudioHost.hpp"
#include "Lv2EventBufferWriter.hpp"
#include "Lv2Log.hpp"
#include "CrashGuard.hpp"
using namespace pipedal;
@@ -401,6 +402,8 @@ void Lv2Pedalboard::UpdateAudioPorts()
void Lv2Pedalboard::Activate()
{
CrashGuardLock crashGuardLock;
for (int i = 0; i < this->effects.size(); ++i)
{
this->realtimeEffects[i]->Activate();
+3
View File
@@ -65,5 +65,8 @@ namespace pipedal {
const LV2_URID_Map *GetMap() const { return &map;}
LV2_URID_Map *GetMap() { return &map;}
const LV2_URID_Unmap *GetUnmap() const { return &unmap;}
LV2_URID_Unmap *GetUnmap() { return &unmap;}
};
}
+44
View File
@@ -22,9 +22,12 @@
#include <algorithm>
#include "json.hpp"
#include <sstream>
#include "PluginHost.hpp"
#include "util.hpp"
using namespace pipedal;
namespace fs = std::filesystem;
MapPathFeature::MapPathFeature()
{
@@ -69,6 +72,27 @@ void MapPathFeature::FnFreePath(LV2_State_Free_Path_Handle handle, char* path)
return ((MapPathFeature *)handle)->AbsolutePath(abstract_path);
}
static std::string CreateMappathLink(const fs::path&sourceFile, const fs::path &resourceDirectory, const fs::path &targetDirectory)
{
try {
fs::path targetFile = targetDirectory / sourceFile.filename();
if (fs::exists(targetFile)) {
return targetFile;
}
fs::create_directories(targetDirectory);
fs::create_symlink(sourceFile,targetFile);
return targetFile;
}catch (const std::exception &e) {
Lv2Log::error(SS("Failed to convert resource file to media file. "
<< sourceFile
<< " -> "
<< targetDirectory
<< " (" << e.what() << ")"
));
return sourceFile;
}
}
char *MapPathFeature::AbsolutePath(const char *abstract_path)
{
if (strlen(abstract_path) == 0)
@@ -76,7 +100,27 @@ char *MapPathFeature::AbsolutePath(const char *abstract_path)
return strdup("");
}
std::filesystem::path t (abstract_path);
std::string extension = t.extension();
if (t.is_absolute()) {
// Handle bundle files, mapping them to the storage path for this plugin.
for (const auto&mapping: this->resourceFileMappings)
{
if (IsSubdirectory(t,mapping.resourcePath))
{
bool allowed = false;
for (const auto&fileType: mapping.fileTypes) {
if (fileType.IsValidExtension(extension)) {
allowed = true;
break;
}
}
if (allowed) {
std::string result = CreateMappathLink(t,mapping.resourcePath, mapping.storagePath);
return strdup(result.c_str());
}
}
}
return strdup(abstract_path);
}
std::filesystem::path result = storagePath / t;
+11 -2
View File
@@ -21,17 +21,24 @@
#include "lv2/state/state.h"
#include <filesystem>
#include "MapFeature.hpp"
#include <memory>
#include <vector>
#include <filesystem>
#include "PiPedalUI.hpp"
namespace pipedal
{
class Lv2PluginInfo;
struct ResourceFileMapping {
std::string resourcePath; // absolute path of the resource directory.
std::string storagePath; // absolute path of where resource directory files get placed.
std::filesystem::path resourcePath; // absolute path of the resource directory.
std::filesystem::path storagePath; // absolute path of where resource directory files get placed.
std::vector<UiFileType> fileTypes;
};
class MapPathFeature
{
public:
MapPathFeature();
void Prepare(MapFeature* map);
void SetPluginStoragePath(const std::string&path) { storagePath = path;}
@@ -46,6 +53,8 @@ namespace pipedal
const LV2_Feature*GetMakePathFeature() { return &makePathFeature;}
const LV2_Feature*GetFreePathFeature() { return &freePathFeature;}
private:
std::shared_ptr<Lv2PluginInfo> pluginInfo;
char *AbsolutePath(const char *abstract_path);
static char *FnAbsolutePath(LV2_State_Map_Path_Handle handle,
const char *abstract_path);
+11 -1
View File
@@ -22,6 +22,7 @@
#include "ss.hpp"
#include <mutex>
#include <memory>
#include "util.hpp"
using namespace pipedal;
@@ -30,7 +31,16 @@ static std::string empty;
const std::string& MimeTypes::MimeTypeFromExtension(const std::string &extension) const
{
auto iter = extensionToMimeType.find(extension);
if (iter == extensionToMimeType.end()) return empty;
if (iter == extensionToMimeType.end())
{
std::string lowerExt = ToLower(extension);
auto iter2 = extensionToMimeType.find(lowerExt);
if (iter2 != extensionToMimeType.end())
{
return iter2->second;
}
return empty;
}
return iter->second;
}
+1
View File
@@ -95,6 +95,7 @@ ModFileTypes::ModFileTypes(const std::string &fileTypes)
if (wellKnownType)
{
rootDirectories_.push_back(type);
modFileTypes_.push_back(type);
}
else
{
+3
View File
@@ -32,12 +32,14 @@ namespace pipedal
ModFileTypes(const std::string &fileTypes);
const std::vector<std::string> &rootDirectories() const { return rootDirectories_; }
std::vector<std::string> &rootDirectories() { return rootDirectories_; }
const std::vector<std::string> &modFileTypes() const { return modFileTypes_; }
const std::vector<std::string> &fileTypes() const { return fileTypes_; }
static constexpr const char *DEFAULT_FILE_TYPES = "audio,nammodel,mlmodel,sf2,sfz,midisong,midiclip,*"; // (everything)
private:
std::vector<std::string> rootDirectories_;
std::vector<std::string> modFileTypes_; // e.g. "audio", "nammodel", etc.
std::vector<std::string> fileTypes_;
public:
@@ -55,6 +57,7 @@ namespace pipedal
const std::vector<std::string> fileTypesX; // mixture of .ext and mime types. Use fileExtensions instead.
const std::set<std::string> fileExtensions;
};
static const std::vector<ModDirectory> &ModDirectories();
+531
View File
@@ -0,0 +1,531 @@
/*
* Copyright (c) 2025 Robin E. R. Davies
* All rights reserved.
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include "ModGui.hpp"
#include "MapFeature.hpp"
#include "PluginHost.hpp"
#include "Finally.hpp"
#include "lv2/atom/atom.h"
#define MOD_GUI_PREFIX "http://moddevices.com/ns/modgui#"
#define MOD_GUI__gui (MOD_GUI_PREFIX "gui")
#define MOD_GUI__modgui (MOD_GUI_PREFIX "modgui")
#define MOD_GUI__resourcesDirectory (MOD_GUI_PREFIX "resourcesDirectory")
#define MOD_GUI__iconTemplate (MOD_GUI_PREFIX "iconTemplate")
#define MOD_GUI__settingsTemplate (MOD_GUI_PREFIX "settingsTemplate")
#define MOD_GUI__javascript (MOD_GUI_PREFIX "javascript")
#define MOD_GUI__stylesheet (MOD_GUI_PREFIX "stylesheet")
#define MOD_GUI__screenshot (MOD_GUI_PREFIX "screenshot")
#define MOD_GUI__thumbnail (MOD_GUI_PREFIX "thumbnail")
#define MOD_GUI__discussionURL (MOD_GUI_PREFIX "discussionURL")
#define MOD_GUI__documentation (MOD_GUI_PREFIX "documentation")
#define MOD_GUI__brand (MOD_GUI_PREFIX "brand")
#define MOD_GUI__label (MOD_GUI_PREFIX "label")
#define MOD_GUI__model (MOD_GUI_PREFIX "model")
#define MOD_GUI__panel (MOD_GUI_PREFIX "panel")
#define MOD_GUI__color (MOD_GUI_PREFIX "color")
#define MOD_GUI__knob (MOD_GUI_PREFIX "knob")
#define MOD_GUI__port (MOD_GUI_PREFIX "port")
#define MOD_GUI__monitoredOutputs (MOD_GUI_PREFIX "monitoredOutputs")
using namespace pipedal;
ModGuiUris::ModGuiUris(LilvWorld *pWorld, MapFeature &mapFeature)
{
mod_gui__gui = lilv_new_uri(pWorld, MOD_GUI__gui);
mod_gui__modgui = lilv_new_uri(pWorld, MOD_GUI__modgui);
mod_gui__resourceDirectory = lilv_new_uri(pWorld, MOD_GUI__resourcesDirectory);
mod_gui__iconTemplate = lilv_new_uri(pWorld, MOD_GUI__iconTemplate);
mod_gui__settingsTemplate = lilv_new_uri(pWorld, MOD_GUI__settingsTemplate);
mod_gui__javascript = lilv_new_uri(pWorld, MOD_GUI__javascript);
mod_gui__stylesheet = lilv_new_uri(pWorld, MOD_GUI__stylesheet);
mod_gui__screenshot = lilv_new_uri(pWorld, MOD_GUI__screenshot);
mod_gui__thumbnail = lilv_new_uri(pWorld, MOD_GUI__thumbnail);
mod_gui__discussionURL = lilv_new_uri(pWorld, MOD_GUI__discussionURL);
mod_gui__documentation = lilv_new_uri(pWorld, MOD_GUI__documentation);
mod_gui__brand = lilv_new_uri(pWorld, MOD_GUI__brand);
mod_gui__label = lilv_new_uri(pWorld, MOD_GUI__label);
mod_gui__model = lilv_new_uri(pWorld, MOD_GUI__model);
mod_gui__panel = lilv_new_uri(pWorld, MOD_GUI__panel);
mod_gui__color = lilv_new_uri(pWorld, MOD_GUI__color);
mod_gui__knob = lilv_new_uri(pWorld, MOD_GUI__knob);
mod_gui__port = lilv_new_uri(pWorld, MOD_GUI__port);
mod_gui__monitoredOutputs = lilv_new_uri(pWorld, MOD_GUI__monitoredOutputs);
}
static const char*NonNull(const char*string) {
return string ? string : "";
}
ModGui::ModGui(
PluginHost *lv2Host,
const LilvPlugin *lilvPlugin,
const std::string &resourceDirectory,
const LilvNode * modGuiUrl)
: pluginUri_(lilv_node_as_uri(lilv_plugin_get_uri(lilvPlugin))),
resourceDirectory_(resourceDirectory)
{
LilvWorld *pWorld = lv2Host->getWorld();
ModGuiUris &modGuiUrids = *lv2Host->mod_gui_uris;
PluginHost::LilvUris &lilvUris = *lv2Host->lilvUris;
AutoLilvNode modguiIcon = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__iconTemplate, nullptr);
if (modguiIcon) {
this->iconTemplate_ = NonNull(lilv_file_uri_parse(lilv_node_as_string(modguiIcon), nullptr));
}
AutoLilvNode modguiSettingsTemplate = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__settingsTemplate, nullptr);
if (modguiSettingsTemplate) {
this->settingsTemplate_ = NonNull(lilv_file_uri_parse(lilv_node_as_string(modguiSettingsTemplate), nullptr));
}
AutoLilvNode modguiJavascript = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__javascript, nullptr);
if (modguiJavascript) {
this->javascript_ = NonNull(lilv_file_uri_parse(lilv_node_as_string(modguiJavascript), nullptr));
}
AutoLilvNode modguiStylesheet = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__stylesheet, nullptr);
if (modguiStylesheet) {
this->stylesheet_ = NonNull(lilv_file_uri_parse(lilv_node_as_string(modguiStylesheet), nullptr));
}
AutoLilvNode modguiScreenshot = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__screenshot, nullptr);
if (modguiScreenshot) {
this->screenshot_ = NonNull(lilv_file_uri_parse(lilv_node_as_string(modguiScreenshot), nullptr));
}
AutoLilvNode modguiThumbnail = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__thumbnail, nullptr);
if (modguiThumbnail) {
this->thumbnail_ = NonNull(lilv_file_uri_parse(lilv_node_as_string(modguiThumbnail), nullptr));
}
AutoLilvNode discussionUrl = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__discussionURL, nullptr);
if (discussionUrl) {
this->discussionUrl_ = NonNull(lilv_file_uri_parse(lilv_node_as_string(discussionUrl), nullptr));
}
AutoLilvNode documentationUrl = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__documentation, nullptr);
if (documentationUrl) {
this->documentationUrl_ = NonNull(lilv_file_uri_parse(lilv_node_as_string(documentationUrl), nullptr));
}
AutoLilvNode brand = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__brand, nullptr);
if (brand) {
this->brand_ = NonNull(lilv_node_as_string(brand));
}
AutoLilvNode label = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__label, nullptr);
if (label) {
this->label_ = NonNull(lilv_node_as_string(label));
}
AutoLilvNode model = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__model, nullptr);
if (model) {
this->model_ = NonNull(lilv_node_as_string(model));
}
AutoLilvNode panel = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__panel, nullptr);
if (panel) {
this->panel_ = NonNull(lilv_node_as_string(panel));
}
AutoLilvNode color = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__color, nullptr);
if (color) {
this->color_ = NonNull(lilv_node_as_string(color));
}
AutoLilvNode knob = lilv_world_get(pWorld, modGuiUrl, modGuiUrids.mod_gui__knob, nullptr);
if (knob) {
this->knob_ = NonNull(lilv_node_as_string(knob));
}
AutoLilvNodes ports = lilv_world_find_nodes(pWorld, modGuiUrl, modGuiUrids.mod_gui__port, nullptr);
if (ports) {
LILV_FOREACH(nodes, it, ports.Get()) {
AutoLilvNode port_node = lilv_nodes_get(ports, it);
this->ports_.push_back(ModGuiPort(lv2Host,port_node));
}
}
std::sort(this->ports_.begin(), this->ports_.end(),
[](const ModGuiPort &p1, const ModGuiPort &p2) {
return p1.index() < p2.index();
});
AutoLilvNodes monitoredOutputs = lilv_world_find_nodes(pWorld, modGuiUrl, modGuiUrids.mod_gui__monitoredOutputs, nullptr);
if (monitoredOutputs) {
LILV_FOREACH(nodes, it, monitoredOutputs.Get()) {
AutoLilvNode monitoredOutput = lilv_nodes_get(monitoredOutputs.Get(), it);
AutoLilvNode symbol = lilv_world_get(pWorld, monitoredOutput, lilvUris.lv2core__symbol, nullptr);
if (symbol) {
this->monitoredOutputs_.push_back(NonNull(lilv_node_as_string(symbol)));
}
}
}
}
ModGui::ptr ModGui::Create(PluginHost *lv2Host, const LilvPlugin *lilvPlugin)
{
LilvWorld *pWorld = lv2Host->getWorld();
ModGuiUris &modGuiUrids = *(lv2Host->mod_gui_uris);
AutoLilvNode modGuiUri;
std::string resourceDirectory;
AutoLilvNodes modGuiNodes = lilv_plugin_get_value(lilvPlugin,modGuiUrids.mod_gui__gui);
if (!modGuiNodes) {
return nullptr; // no mod gui found.
}
LILV_FOREACH(nodes, it, modGuiNodes.Get()) {
AutoLilvNode node = lilv_nodes_get(modGuiNodes.Get(), it);
AutoLilvNode resourceDir = lilv_world_get(pWorld, node, modGuiUrids.mod_gui__resourceDirectory,nullptr);
if (resourceDir) {
resourceDirectory = lilv_file_uri_parse(lilv_node_as_string(resourceDir),nullptr);
modGuiUri = lilv_node_duplicate(node);
} else {
resourceDirectory.clear();
modGuiUri.Free();
}
}
if (!modGuiUri) {
return nullptr; // no mod gui found.
}
return std::shared_ptr<ModGui>(new ModGui(lv2Host, lilvPlugin, resourceDirectory, modGuiUri.Get()));
}
ModGuiPort::ModGuiPort(PluginHost *lv2Host, const LilvNode *portNode)
{
LilvWorld *pWorld = lv2Host->getWorld();
PluginHost::LilvUris &lilvUris = *lv2Host->lilvUris;
this->index_ = lilv_node_as_int(lilv_world_get(pWorld, portNode, lilvUris.lv2core__index, nullptr));
this->symbol_ = NonNull(lilv_node_as_string(lilv_world_get(pWorld, portNode, lilvUris.lv2core__symbol, nullptr)));
this->name_ = NonNull(lilv_node_as_string(lilv_world_get(pWorld, portNode, lilvUris.lv2core__name, nullptr)));
}
static json_variant UnitsObj(const std::string &label, const std::string &render, const std::string &symbol)
{
json_variant obj = json_variant::make_object();
obj["label"] = label;
obj["render"] = render;
obj["symbol"] = symbol;
return obj;
}
static std::map<Units,json_variant> unitsMap
{
{Units::none, UnitsObj("","%f","")},
{Units::unknown, UnitsObj("","%f","")},
{Units::s, UnitsObj("seconds", "%f s", "s")},
{Units::ms, UnitsObj("milliseconds", "%f ms", "ms")},
{Units::min, UnitsObj("minutes", "%f mins", "min")},
{Units::bar, UnitsObj("bars", "%f bars", "bars")},
{Units::beat, UnitsObj("beats", "%f beats", "beats")},
{Units::frame, UnitsObj("audio frames", "%f frames", "frames")},
{Units::m, UnitsObj("metres", "%f m", "m")},
{Units::cm, UnitsObj("centimetres", "%f cm", "cm")},
{Units::mm, UnitsObj("millimetres", "%f mm", "mm")},
{Units::km, UnitsObj("kilometres", "%f km", "km")},
{Units::inch, UnitsObj("inches", "\"%f\"", "in")},
{Units::mile, UnitsObj("miles", "%f mi", "mi")},
{Units::db, UnitsObj("decibels", "%f dB", "dB")},
{Units::pc, UnitsObj("percent", "%f%%", "%")},
{Units::coef, UnitsObj("coefficient", "* %f", "*")},
{Units::hz, UnitsObj("hertz", "%f Hz", "Hz")},
{Units::khz, UnitsObj("kilohertz", "%f kHz", "kHz")},
{Units::mhz, UnitsObj("megahertz", "%f MHz", "MHz")},
{Units::bpm, UnitsObj("beats per minute", "%f BPM", "BPM")},
{Units::oct, UnitsObj("octaves", "%f octaves", "oct")},
{Units::cent, UnitsObj("cents", "%f ct", "ct")},
{Units::semitone12TET, UnitsObj("semitones", "%f semi", "semi")},
{Units::degree, UnitsObj("degrees", "%f°", "°")},
{Units::midiNote, UnitsObj("MIDI note", "MIDI note %d", "note")},
};
static json_variant UnitsToJsonVariant(Units units)
{
if (unitsMap.find(units) != unitsMap.end())
{
return unitsMap[units];
}
return unitsMap[Units::none];
}
template<typename T>
static inline json_variant MakeJsonArray(const std::vector<T> &vec)
{
json_variant array = json_variant::make_array();
auto &arrayRef = *array.as_array();
for (const auto &item : vec)
{
json_variant itemVariant = json_variant(item);
arrayRef.push_back(itemVariant);
}
return array;
}
static std::string AtomTypeToRangesType(const std::string& atomType)
{
if (atomType == LV2_ATOM__Path ||
atomType == LV2_ATOM__String ||
atomType == LV2_ATOM__URI ||
atomType == LV2_ATOM__URID)
{
return "s";
}
if (atomType == LV2_ATOM__Float)
{
return "f";
}
if (atomType == LV2_ATOM__Double)
{
return "d";
}
if (atomType == LV2_ATOM__Long)
{
return "l";
}
if (atomType == LV2_ATOM__Vector)
{
return "v";
}
return "?";
}
json_variant pipedal::MakeModGuiTemplateData(
std::shared_ptr<Lv2PluginInfo> pluginInfo)
{
json_variant context = json_variant::make_object();
auto &contextObj = *(context.as_object());
auto modGui = pluginInfo->modGui();
contextObj["brand"] = modGui->brand();
contextObj["label"] = modGui->label();
contextObj["model"] = modGui->model();
contextObj["panel"] = modGui->panel();
contextObj["color"] = modGui->color();
contextObj["knob"] = modGui->knob();
json_variant controls = json_variant::make_array();
auto &controlArray = *controls.as_array();
size_t ix = 0;
for (const auto& modGuiPort : modGui->ports())
{
const Lv2PortInfo &pluginPort = pluginInfo->getPort(modGuiPort.symbol());
if (!pluginPort.is_control_port() || !pluginPort.is_input() || pluginPort.not_on_gui())
{
continue; // only include control ports
}
json_variant control = json_variant::make_object();
auto &portObj = *control.as_object();
portObj["index"] = double(pluginPort.index());
portObj["symbol"] = pluginPort.symbol();
portObj["name"] = modGuiPort.name();
portObj["comment"] = pluginPort.comment();
portObj["units"] = UnitsToJsonVariant(pluginPort.units());
{
json_variant ranges = json_variant::make_object();
ranges["minimum"] = SS(pluginPort.min_value());
ranges["maximum"] = SS(pluginPort.max_value());
ranges["default"] = SS(pluginPort.default_value());
portObj["ranges"] = json_variant::make_array();
}
json_variant scalePoints = json_variant::make_array();
auto &scalePointsArray = *scalePoints.as_array();
for (const auto &scalePoint: pluginPort.scale_points())
{
json_variant scalePointObj = json_variant::make_object();
scalePointObj["valid"] = true;
scalePointObj["label"] = scalePoint.label();
scalePointObj["value"] = SS(scalePoint.value());
scalePointsArray.push_back(scalePointObj);
}
portObj["scalePoints"] = scalePoints;
controlArray.push_back(control);
}
contextObj["controls"] = controls;
json_variant effect = json_variant::make_object();
contextObj["effect"] = effect;
json_variant ports = json_variant::make_object();
(*effect.as_object())["ports"] = ports;
json_variant audio = json_variant::make_object();
(*ports.as_object())["audio"] = audio;
json_variant audio_input = json_variant::make_array();
(*audio.as_object())["input"] = audio_input;
json_variant audio_output = json_variant::make_array();
(*audio.as_object())["output"] = audio_output;
json_variant midi = json_variant::make_object();
(*ports.as_object())["midi"] = midi;
json_variant midi_input = json_variant::make_array();
(*midi.as_object())["input"] = midi_input;
json_variant midi_output = json_variant::make_array();
(*midi.as_object())["output"] = midi_output;
json_variant cv = json_variant::make_object();
(*ports.as_object())["cv"] = cv;
json_variant cv_input = json_variant::make_array();
(*cv.as_object())["input"] = cv_input;
json_variant cv_output = json_variant::make_array();
(*cv.as_object())["output"] = cv_output;
{
json_variant parametersObj = json_variant::make_object();
json_variant pathObj = json_variant::make_array();
parametersObj["path"] = pathObj;
for (const auto &patchProperty: pluginInfo->patchProperties())
{
json_variant parameterObj = json_variant::make_object();
parameterObj["valid"] = true;
parameterObj["readable"] = patchProperty.readable();
parameterObj["writable"] = patchProperty.writable();
parameterObj["uri"] = patchProperty.uri();
parameterObj["label"] = patchProperty.label();
parameterObj["type"] = patchProperty.type();
json_variant rangesObj = json_variant::make_object();
rangesObj["type"] = AtomTypeToRangesType(patchProperty.type());
rangesObj["atomType"] = patchProperty.type();
parameterObj["ranges"] = rangesObj;
parameterObj["comment"] = patchProperty.comment();
parameterObj["shortName"] = patchProperty.shortName();
parameterObj["fileTypes"] = MakeJsonArray(patchProperty.fileTypes());
parameterObj["supportedExtensions"] = MakeJsonArray(patchProperty.supportedExtensions());
if (patchProperty.type() == LV2_ATOM__Path && patchProperty.writable()) {
// Feed them no files; they will be added later.
json_variant filesObj = json_variant::make_array();
json_variant fileObj = json_variant::make_object();
fileObj["filetype"] = "{{filetype}}"; // magic tags that we will use to generate files at runtime.
fileObj["fullname"] = "{{fullname}}";
fileObj["basename"] = "{{basename}}";
filesObj.as_array()->push_back(fileObj);
parameterObj["files"] = filesObj;
pathObj.as_array()->push_back(parameterObj);
}
}
effect["parameters"] = parametersObj;
}
for (const auto &pluginPort : pluginInfo->ports())
{
if (pluginPort->is_control_port())
{
continue; // only include control ports
}
if (pluginPort->is_audio_port())
{
json_variant audioPort = json_variant::make_object();
auto &audioPortObj = *audioPort.as_object();
audioPortObj["index"] = double(pluginPort->index());
audioPortObj["symbol"] = pluginPort->symbol();
audioPortObj["name"] = pluginPort->name();
if (pluginPort->is_input())
{
audio_input.as_array()->push_back(audioPort);
}
else
{
audio_output.as_array()->push_back(audioPort);
}
}
else if (pluginPort->is_atom_port())
{
if (pluginPort->supports_midi())
{
json_variant midiPort = json_variant::make_object();
auto &midiPortObj = *midiPort.as_object();
midiPortObj["index"] = double(pluginPort->index());
midiPortObj["symbol"] = pluginPort->symbol();
midiPortObj["name"] = pluginPort->name();
if (pluginPort->is_input())
{
midi_input.as_array()->push_back(midiPort);
}
else
{
midi_output.as_array()->push_back(midiPort);
}
}
}
/* else if plugin_port->is_cv_port()... */
}
return context;
}
JSON_MAP_BEGIN(ModGuiPort)
JSON_MAP_REFERENCE(ModGuiPort,index)
JSON_MAP_REFERENCE(ModGuiPort,symbol)
JSON_MAP_REFERENCE(ModGuiPort,name)
JSON_MAP_END()
JSON_MAP_BEGIN(ModGui)
JSON_MAP_REFERENCE(ModGui,pluginUri)
JSON_MAP_REFERENCE(ModGui,resourceDirectory)
JSON_MAP_REFERENCE(ModGui,iconTemplate)
JSON_MAP_REFERENCE(ModGui,settingsTemplate)
JSON_MAP_REFERENCE(ModGui,screenshot)
JSON_MAP_REFERENCE(ModGui,javascript)
JSON_MAP_REFERENCE(ModGui,stylesheet)
JSON_MAP_REFERENCE(ModGui,thumbnail)
JSON_MAP_REFERENCE(ModGui,discussionUrl)
JSON_MAP_REFERENCE(ModGui,documentationUrl)
JSON_MAP_REFERENCE(ModGui,brand)
JSON_MAP_REFERENCE(ModGui,label)
JSON_MAP_REFERENCE(ModGui,model)
JSON_MAP_REFERENCE(ModGui,panel)
JSON_MAP_REFERENCE(ModGui,color)
JSON_MAP_REFERENCE(ModGui,knob)
JSON_MAP_REFERENCE(ModGui,ports)
JSON_MAP_REFERENCE(ModGui,monitoredOutputs)
JSON_MAP_END()
+180
View File
@@ -0,0 +1,180 @@
/*
* Copyright (c) 2025 Robin E. R. Davies
* All rights reserved.
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include "lv2/urid/urid.h"
#include <string>
#include <lilv/lilv.h>
#include <memory>
#include "AutoLilvNode.hpp"
#include "json.hpp"
#include "json_variant.hpp"
namespace pipedal
{
class MapFeature;
class Lv2PluginInfo;
class ModGuiUris
{
public:
ModGuiUris(LilvWorld *pWorld, MapFeature &mapFeature);
AutoLilvNode mod_gui__gui;
AutoLilvNode mod_gui__modgui;
AutoLilvNode mod_gui__resourceDirectory;
AutoLilvNode mod_gui__iconTemplate;
AutoLilvNode mod_gui__settingsTemplate;
AutoLilvNode mod_gui__javascript;
AutoLilvNode mod_gui__stylesheet;
AutoLilvNode mod_gui__screenshot;
AutoLilvNode mod_gui__thumbnail;
AutoLilvNode mod_gui__discussionURL;
AutoLilvNode mod_gui__documentation;
AutoLilvNode mod_gui__brand;
AutoLilvNode mod_gui__label;
AutoLilvNode mod_gui__model;
AutoLilvNode mod_gui__panel;
AutoLilvNode mod_gui__color;
AutoLilvNode mod_gui__knob;
AutoLilvNode mod_gui__port;
AutoLilvNode mod_gui__monitoredOutputs;
};
class PluginHost;
class ModGuiPort {
public:
ModGuiPort() = default;
ModGuiPort(PluginHost *lv2Host, const LilvNode *portNode);
private:
uint32_t index_ = 0;
std::string symbol_;
std::string name_;
public:
uint32_t index() const { return index_; }
void index(uint32_t value) { index_ = value; }
const std::string &symbol() const { return symbol_; }
void symbol(const std::string &value) { symbol_ = value; }
const std::string &name() const { return name_; }
void name(const std::string &value) { name_ = value; }
DECLARE_JSON_MAP(ModGuiPort);
};
class ModGui
{
private:
ModGui(PluginHost *lv2Host, const LilvPlugin *lilvPlugin, const std::string &resourceDirectory, const LilvNode *modGuiNode);
public:
using self = ModGui;
using ptr = std::shared_ptr<self>;
static ptr Create(PluginHost *lv2Host, const LilvPlugin *lilvPlugin);
ModGui() = default;
virtual ~ModGui() = default;
ModGui(const ModGui &) = default;
ModGui &operator=(const ModGui &) = default;
ModGui(ModGui &&) = default;
ModGui &operator=(ModGui &&) = default;
private:
std::string pluginUri_;
std::string resourceDirectory_;
std::string iconTemplate_;
std::string settingsTemplate_;
std::string javascript_;
std::string stylesheet_;
std::string screenshot_;
std::string thumbnail_;
std::string discussionUrl_;
std::string documentationUrl_;
std::string brand_;
std::string label_;
std::string model_;
std::string panel_;
std::string color_;
std::string knob_;
std::vector<ModGuiPort> ports_;
std::vector<std::string> monitoredOutputs_;
public:
const std::string &pluginUri() const { return pluginUri_; }
void pluginUri(const std::string &value) { pluginUri_ = value; }
const std::string &resourceDirectory() const { return resourceDirectory_; }
void resourceDirectory(const std::string &value) { resourceDirectory_ = value; }
const std::string &iconTemplate() const { return iconTemplate_; }
void iconTemplate(const std::string &value) { iconTemplate_ = value; }
const std::string &settingsTemplate() const { return settingsTemplate_; }
void settingsTemplate(const std::string &value) { settingsTemplate_ = value; }
const std::string &screenshot() const { return screenshot_; }
const std::string &javascript() const { return javascript_; }
void javascript(const std::string &value) { javascript_ = value; }
const std::string &stylesheet() const { return stylesheet_; }
void stylesheet(const std::string &value) { stylesheet_ = value; }
void screenshot(const std::string &value) { screenshot_ = value; }
const std::string &thumbnail() const { return thumbnail_; }
void thumbnail(const std::string &value) { thumbnail_ = value; }
const std::string &discussionUrl() const { return discussionUrl_; }
void discussionUrl(const std::string &value) { discussionUrl_ = value; }
const std::string &documentationUrl() const { return documentationUrl_; }
void documentationUrl(const std::string &value) { documentationUrl_ = value; }
const std::string &brand() const { return brand_; }
void brand(const std::string &value) { brand_ = value; }
const std::string &label() const { return label_; }
void label(const std::string &value) { label_ = value; }
const std::string &model() const { return model_; }
void model(const std::string &value) { model_ = value; }
const std::string &panel() const { return panel_; }
void panel(const std::string &value) { panel_ = value; }
const std::string &color() const { return color_; }
void color(const std::string &value) { color_ = value; }
const std::string &knob() const { return knob_; }
void knob(const std::string &value) { knob_ = value; }
const std::vector<ModGuiPort> &ports() const { return ports_; }
std::vector<ModGuiPort> &ports() { return ports_; }
void ports(const std::vector<ModGuiPort> &value) { ports_ = value; }
const std::vector<std::string> &monitoredOutputs() const { return monitoredOutputs_; }
std::vector<std::string> &monitoredOutputs() { return monitoredOutputs_; }
void monitoredOutputs(const std::vector<std::string> &value) { monitoredOutputs_ = value; }
DECLARE_JSON_MAP(ModGui);
};
json_variant MakeModGuiTemplateData(
std::shared_ptr<Lv2PluginInfo> pluginInfo
);
}
+117
View File
@@ -0,0 +1,117 @@
// Copyright (c) 2025 Robin Davies
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#include "pch.h"
#include "catch.hpp"
#include <sstream>
#include <cstdint>
#include <string>
#include <iostream>
#include <atomic>
#include <thread>
#include "ModTemplateGenerator.hpp"
#include "PiPedalModel.hpp"
using namespace pipedal;
using namespace std;
class PluginHostTest {
public:
static void TestInit()
{
PiPedalModel model;
model.GetPluginHost().LoadLilv();
const auto&plugins = model.GetPluginHost().GetPlugins();
REQUIRE(!plugins.empty());
size_t modGuicount = 0;
for (const auto&plugin : plugins)
{
REQUIRE(plugin != nullptr);
if (plugin->modGui())
{
modGuicount++;
const auto&modGui = plugin->modGui();
REQUIRE(!modGui->iconTemplate().empty());
REQUIRE(modGui->javascript().empty());
REQUIRE(!modGui->stylesheet().empty());
REQUIRE(!modGui->screenshot().empty());
REQUIRE(!modGui->thumbnail().empty());
}
}
}
};
TEST_CASE("ModGui Init Test", "[mod_gui_init]")
{
PluginHostTest::TestInit();
}
TEST_CASE("ModGui Templates", "[mod_gui_templates]")
{
json_variant data = json_variant::make_object();
data["name"] = "Test Plugin";
data["uri"] = "http://example.com/test-plugin";
data["version"] = "1.0.0";
data["author"] = "Test Author";
data["cn"] = "14323";
json_variant items = json_variant::make_array();
for (size_t i = 0; i < 3; ++i)
{
json_variant item = json_variant::make_object();
item["label"] = "Item " + std::to_string(i + 1);
items.as_array()->push_back(item);
}
data["items"] = items;
data["inputs"] = json_variant::make_object();
data["inputs"]["input1"] = "Input 1";
data["inputs"]["input2"] = "Input 2";
data["_cn"] = "?cn=1234";
data["_cns"] = "_toobamp.lv2_ToobAmp__1234";
std::string templateString = R"(
<div>
<h1>{{name}}</h1>
<p>URI: {{uri}}</p>
<p>Version: {{version}}</p>
<p>Author: {{author}}</p>
<ul>
{{#items}}
<li id="item-{{cn}}">{{label}}</li>
{{/items}}
</ul>
<h2>{{inputs.input1}}</h2>
<h2>{{inputs.input2}}</h2>
<h2>{{inputs.input3}}</h2>
<img src="img/helpIcon.png{{{cn}}}" class="{{{cns}}}_help_icon" alt="Help Icon" />
</div>
)";
auto result = GenerateFromTemplateString(templateString, data);
cout << result;
cout << endl;
}
+295
View File
@@ -0,0 +1,295 @@
/*
* Copyright (c) 2025 Robin E. R. Davies
* All rights reserved.
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include "ModTemplateGenerator.hpp"
#include <fstream>
#include <sstream>
#include "util.hpp"
namespace fs = std::filesystem;
namespace pipedal
{
namespace impl
{
static std::string trim(const std::string &str)
{
size_t first = str.find_first_not_of(" \t\n\r");
if (first == std::string::npos)
return "";
size_t last = str.find_last_not_of(" \t\n\r");
return str.substr(first, (last - first + 1));
}
static bool variableEndsWithNumber(const std::string &variableName)
{
size_t pos = variableName.find_last_of(".");
if (pos == std::string::npos)
{
return false;
}
for (size_t i = pos + 1; i < variableName.length(); ++i)
{
if (!std::isdigit(variableName[i]))
{
return false;
}
}
// Check if the last character is a digit
return true;
}
void splitIndexedArrayVariable(
const std::string &variableName,
std::string &arrayName,
int64_t &arrayIndex)
{
size_t pos = variableName.find_last_of(".");
if (pos == std::string::npos)
{
throw std::logic_error("Variable name does not contain an index: " + variableName);
}
else
{
arrayName = variableName.substr(0, pos);
arrayIndex = std::stoll(variableName.substr(pos + 1));
}
}
class VariableContext
{
public:
VariableContext(
const json_variant &context,
VariableContext *parent = nullptr)
: context(context), parent(parent)
{
}
json_variant getVariable(const std::string &name)
{
std::vector<std::string> parts = split(trim(name), '.');
json_variant *current = &context;
for (const auto &part : parts)
{
if (current->is_object())
{
auto ff = current->as_object()->find(part);
if (ff != current->as_object()->end())
{
current = &ff->second;
continue;
}
}
if (parent)
{
return parent->getVariable(name);
}
else
{
return json_variant(json_null());
}
}
return *current;
}
private:
json_variant context;
VariableContext *parent;
};
static std::string GenerateTemplateFromString(
const std::string &content,
VariableContext &context)
{
std::stringstream ss;
size_t ix = 0;
size_t end = content.length();
while (ix < end)
{
// copy content until we find a '{{'
char c = content[ix++];
if (c != '{')
{
ss << c;
continue;
}
if (ix >= end)
{
break;
}
c = content[ix++];
if (c != '{')
{
ss << '{';
ss << c;
continue;
}
if (ix < end && content[ix] == '{')
{
// this is a '{{{', so we just copy it.
++ix; // skip the third '{'
size_t endTagPos = content.find("}}}", ix);
if (endTagPos == std::string::npos)
{
throw std::runtime_error("Unmatched opening tag '{{{' in template.");
}
std::string variableString = content.substr(ix, endTagPos - ix);
ix = endTagPos + 3; // Move past the closing '}}}'
auto result = context.getVariable("_" + variableString);
if (result.is_null())
{
result = ""; // Default to empty string if variable not found
}
if (!result.is_string())
{
throw std::runtime_error("Variable '" + variableString + "' is not a string.");
}
ss << result.as_string();
continue;
}
else
{
// Now we have a '{{' at position ix-2.
size_t endTagPos = content.find("}}", ix);
if (endTagPos == std::string::npos)
{
throw std::runtime_error("Unmatched opening tag '{{' in template.");
}
std::string variableString = content.substr(ix, endTagPos - ix);
ix = endTagPos + 2; // Move past the closing '}}'
if (!variableString.starts_with("#"))
{
// a straightforward variable substitution.
json_variant value = context.getVariable(variableString);
if (value.is_null())
{
value = ""; // Default to empty string if variable not found
}
if (!value.is_string())
{
throw std::runtime_error("Variable '" + variableString + "' is not a string.");
}
ss << value.as_string();
}
else
{
// this is a looping construct.
std::string variableName = variableString.substr(1);
std::string endTag = SS("{{/" << variableName << "}}");
size_t endTagPos = content.find(endTag, ix);
if (endTagPos == std::string::npos)
{
throw std::runtime_error("Unmatched opening tag for '" + variableString + "' in template.");
}
std::string arrayContent = content.substr(ix, endTagPos - ix);
if (variableEndsWithNumber(variableName))
{
std::string arrayName;
int64_t arrayIndex = 0;
splitIndexedArrayVariable(variableName, arrayName, arrayIndex);
json_variant array = context.getVariable(arrayName);
auto &arrayValue = *array.as_array();
if (arrayIndex >= 0 && arrayIndex < static_cast<int64_t>(arrayValue.size()))
{
json_variant contextValue = arrayValue[(size_t)arrayIndex];
// variable frame.
VariableContext itemContext{contextValue, &context};
ss << GenerateTemplateFromString(
arrayContent,
itemContext);
}
}
else
{
json_variant variable = context.getVariable(variableName);
if (variable.is_array())
{
auto &arrayValue = *variable.as_array();
for (auto iter = arrayValue.begin(); iter != arrayValue.end(); ++iter)
{
// variable frame.
VariableContext itemContext{*iter, &context};
ss << GenerateTemplateFromString(
arrayContent,
itemContext);
}
} else if (variable.is_object())
{
VariableContext itemContext(variable,&context);
ss << GenerateTemplateFromString(
arrayContent,
itemContext);
}
}
ix = endTagPos + endTag.length(); // Move past the closing tag
}
}
}
return ss.str();
}
}
using namespace impl;
std::string GenerateFromTemplateString(
const std::string &templateString,
const json_variant &data)
{
VariableContext context{data, nullptr};
return impl::GenerateTemplateFromString(
templateString,
context);
}
std::string GenerateFromTemplateFile(
const std::filesystem::path &templateFilePath,
const json_variant &data)
{
std::string templateContent;
if (!fs::exists(templateFilePath))
{
throw std::runtime_error("Template file not found: " + templateFilePath.string());
}
std::ifstream file(templateFilePath);
if (!file.is_open())
{
throw std::runtime_error("Failed to open template file: " + templateFilePath.string());
}
templateContent.assign((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
file.close();
return GenerateFromTemplateString(
templateContent,
data);
}
}
+41
View File
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2025 Robin E. R. Davies
* All rights reserved.
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include <filesystem>
#include <string>
#include "json_variant.hpp"
namespace pipedal
{
std::string GenerateFromTemplateString(
const std::string& templateString,
const json_variant& data
);
std::string GenerateFromTemplateFile(
const std::filesystem::path& templateFilePath,
const json_variant& data
);
}
+15
View File
@@ -115,6 +115,18 @@ const ControlValue* PedalboardItem::GetControlValue(const std::string&symbol) co
return nullptr;
}
bool Pedalboard::SetItemUseModUi(int64_t pedalItemId, bool enabled)
{
PedalboardItem*item = GetItem(pedalItemId);
if (!item) return false;
if (item->useModUi() != enabled)
{
item->useModUi(enabled);
return true;
}
return false;
}
bool Pedalboard::SetItemEnabled(int64_t pedalItemId, bool enabled)
{
PedalboardItem*item = GetItem(pedalItemId);
@@ -508,11 +520,13 @@ JSON_MAP_BEGIN(PedalboardItem)
JSON_MAP_REFERENCE_CONDITIONAL(PedalboardItem,topChain,IsPedalboardSplitItem)
JSON_MAP_REFERENCE_CONDITIONAL(PedalboardItem,bottomChain,&IsPedalboardSplitItem)
JSON_MAP_REFERENCE(PedalboardItem,midiBindings)
JSON_MAP_REFERENCE(PedalboardItem,midiChannelBinding)
JSON_MAP_REFERENCE(PedalboardItem,stateUpdateCount)
JSON_MAP_REFERENCE(PedalboardItem,lv2State)
JSON_MAP_REFERENCE(PedalboardItem,lilvPresetUri)
JSON_MAP_REFERENCE(PedalboardItem,pathProperties)
JSON_MAP_REFERENCE(PedalboardItem,title)
JSON_MAP_REFERENCE(PedalboardItem,useModUi)
JSON_MAP_END()
@@ -524,6 +538,7 @@ JSON_MAP_BEGIN(Pedalboard)
JSON_MAP_REFERENCE(Pedalboard,nextInstanceId)
JSON_MAP_REFERENCE(Pedalboard,snapshots)
JSON_MAP_REFERENCE(Pedalboard,selectedSnapshot)
JSON_MAP_REFERENCE(Pedalboard,selectedPlugin)
JSON_MAP_END()
JSON_MAP_BEGIN(SnapshotValue)
+18 -12
View File
@@ -81,7 +81,7 @@ public:
};
class PedalboardItem: public JsonMemberWritable {
class PedalboardItem {
public:
using PropertyMap = std::map<std::string,atom_object>;
int64_t instanceId_ = 0;
@@ -99,6 +99,7 @@ public:
std::string lilvPresetUri_;
std::map<std::string,std::string> pathProperties_;
std::string title_;
bool useModUi_ = false;
// non persistent state.
PropertyMap patchProperties;
@@ -128,6 +129,7 @@ public:
GETTER_SETTER(stateUpdateCount)
GETTER_SETTER_REF(lv2State)
GETTER_SETTER_REF(title)
GETTER_SETTER(useModUi)
Lv2PluginState&lv2State() { return lv2State_; } // non-const version.
GETTER_SETTER_REF(lilvPresetUri)
@@ -146,17 +148,17 @@ public:
void AddResetsForMissingProperties(Snapshot&snapshot, size_t*index) const;
virtual void write_members(json_writer&writer) const {
writer.write_member("instanceId",instanceId_);
writer.write_member("uri",uri_);
writer.write_member("pluginName",pluginName_);
writer.write_member("isEnabled",isEnabled_);
if (isSplit())
{
writer.write_member("topChain",topChain_);
writer.write_member("bottomChain",bottomChain_);
}
}
// virtual void write_members(json_writer&writer) const {
// writer.write_member("instanceId",instanceId_);
// writer.write_member("uri",uri_);
// writer.write_member("pluginName",pluginName_);
// writer.write_member("isEnabled",isEnabled_);
// if (isSplit())
// {
// writer.write_member("topChain",topChain_);
// writer.write_member("bottomChain",bottomChain_);
// }
// }
DECLARE_JSON_MAP(PedalboardItem);
};
@@ -195,6 +197,8 @@ class Pedalboard {
std::vector<std::shared_ptr<Snapshot>> snapshots_;
int64_t selectedSnapshot_ = -1;
int64_t selectedPlugin_ = -1;
public:
// deep copy, breaking shared pointers.
Pedalboard DeepCopy();
@@ -203,6 +207,7 @@ public:
bool SetControlValue(int64_t pedalItemId, const std::string &symbol, float value);
bool SetItemTitle(int64_t pedalItemId, const std::string &title);
bool SetItemEnabled(int64_t pedalItemId, bool enabled);
bool SetItemUseModUi(int64_t pedalItemId, bool enabled);
void SetCurrentSnapshotModified(bool modified);
bool IsStructureIdentical(const Pedalboard &other) const; // caan we just send a snapshot-style uddate instead of reloading plugins? All settings are ignored.
@@ -221,6 +226,7 @@ public:
GETTER_SETTER(output_volume_db)
GETTER_SETTER_VEC(snapshots)
GETTER_SETTER(selectedSnapshot)
GETTER_SETTER(selectedPlugin)
DECLARE_JSON_MAP(Pedalboard);
+60 -12
View File
@@ -45,6 +45,7 @@
#include "AvahiService.hpp"
#include "DummyAudioDriver.hpp"
#include "AudioFiles.hpp"
#include "CrashGuard.hpp"
#ifndef NO_MLOCK
#include <sys/mman.h>
@@ -242,7 +243,7 @@ void PiPedalModel::LoadLv2PluginInfo()
}
pluginChangeMonitor = std::make_unique<Lv2PluginChangeMonitor>(*this);
pluginHost.Load(configuration.GetLv2Path().c_str());
pluginHost.LoadLilv(configuration.GetLv2Path().c_str());
// Copy all presets out of Lilv data to json files
// so that we can close lilv while we're actually
@@ -271,20 +272,33 @@ void PiPedalModel::Load()
this->pedalboard = storage.GetCurrentPreset(); // the current *saved* preset.
// the current edited preset, saved only across orderly shutdowns.
CurrentPreset currentPreset;
try
{
if (storage.RestoreCurrentPreset(&currentPreset))
CrashGuard::SetCrashGuardFileName(storage.GetDataRoot() / "crash_guard.data");
if (CrashGuard::HasCrashed()) {
// ignore the current preset, and load a blank pedalboard in order to avoid a potential plugin crash.
this->pedalboard = Pedalboard::MakeDefault();
} else {
CurrentPreset currentPreset;
try
{
this->pedalboard = currentPreset.preset_;
this->hasPresetChanged = currentPreset.modified_;
if (storage.RestoreCurrentPreset(&currentPreset))
{
this->pedalboard = currentPreset.preset_;
this->hasPresetChanged = currentPreset.modified_;
}
}
catch (const std::exception &e)
{
Lv2Log::warning(SS("Failed to load current preset. " << e.what()));
}
}
catch (const std::exception &e)
{
Lv2Log::warning(SS("Failed to load current preset. " << e.what()));
}
UpdateDefaults(&this->pedalboard);
std::unique_ptr<AudioHost> p{AudioHost::CreateInstance(pluginHost.asIHost())};
@@ -617,6 +631,24 @@ void PiPedalModel::UpdateCurrentPedalboard(int64_t clientId, Pedalboard &pedalbo
}
}
void PiPedalModel::SetPedalboardItemUseModUi(int64_t clientId, int64_t instanceId, bool enabled)
{
std::lock_guard<std::recursive_mutex> guard{mutex};
{
this->pedalboard.SetItemUseModUi(instanceId, enabled);
// Notify clients.
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnItemUseModUiChanged(clientId, instanceId, enabled);
}
this->SetPresetChanged(clientId, true);
}
}
void PiPedalModel::SetPedalboardItemEnable(int64_t clientId, int64_t pedalItemId, bool enabled)
{
std::lock_guard<std::recursive_mutex> guard{mutex};
@@ -2221,7 +2253,10 @@ void PiPedalModel::OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetPropert
}
}
}
audioHost->SetListenForAtomOutput(atomOutputListeners.size() != 0);
if (audioHost)
{
audioHost->SetListenForAtomOutput(atomOutputListeners.size() != 0);
}
}
void PiPedalModel::OnNotifyPathPatchPropertyReceived(
@@ -2577,8 +2612,19 @@ std::shared_ptr<Lv2Pedalboard> PiPedalModel::GetLv2Pedalboard()
}
return lv2Pedalboard;
}
void PiPedalModel::SetSelectedPedalboardPlugin(uint64_t clientId, uint64_t pedalboardId)
{
// Thinking on this:
// 1) do NOT mark the pedalboard as changed. This shouldn't set a change flag.
// 2) do NOT broadcast the change. Whoever set it last controls what happens when the plugin is reloaded. Meh.
// 3) Clients must be able to save a non-changed pedalboard.
pedalboard.selectedPlugin(pedalboardId);
}
bool PiPedalModel::LoadCurrentPedalboard()
{
CrashGuardLock crashGuardLock;
if (previousPedalboardLoaded && pedalboard.IsStructureIdentical(previousPedalboard))
{
// then we can send a snapshot update instead!
@@ -3057,3 +3103,5 @@ bool PiPedalModel::HasTone3000Auth() const
return storage.GetTone3000Auth() != "";
}
+7 -1
View File
@@ -59,6 +59,7 @@ namespace pipedal
virtual int64_t GetClientId() = 0;
virtual void OnItemEnabledChanged(int64_t clientId, int64_t pedalItemId, bool enabled) = 0;
virtual void OnItemUseModUiChanged(int64_t clientId, int64_t pedalItemId, bool enabled) = 0;
virtual void OnControlChanged(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value) = 0;
virtual void OnInputVolumeChanged(float value) = 0;
virtual void OnOutputVolumeChanged(float value) = 0;
@@ -331,7 +332,9 @@ namespace pipedal
void LoadLv2PluginInfo();
void Load();
const PluginHost &GetLv2Host() const { return pluginHost; }
const PluginHost &GetPluginHost() const { return pluginHost; }
PluginHost &GetPluginHost() { return pluginHost; }
Pedalboard GetCurrentPedalboardCopy()
{
std::lock_guard<std::recursive_mutex> guard(mutex);
@@ -346,6 +349,7 @@ namespace pipedal
void RemoveNotificationSubsription(std::shared_ptr<IPiPedalModelSubscriber> pSubscriber);
void SetPedalboardItemEnable(int64_t clientId, int64_t instanceId, bool enabled);
void SetPedalboardItemUseModUi(int64_t clientId, int64_t instanceId, bool enabled);
void SetControl(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value);
void PreviewControl(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value);
@@ -485,6 +489,8 @@ namespace pipedal
void SetTone3000Auth(const std::string &apiKey);
bool HasTone3000Auth() const;
void SetSelectedPedalboardPlugin(uint64_t clientId, uint64_t pedalboardId);
};
} // namespace pipedal.
+53 -2
View File
@@ -428,6 +428,22 @@ JSON_MAP_REFERENCE(PedalboardItemEnabledBody, instanceId)
JSON_MAP_REFERENCE(PedalboardItemEnabledBody, enabled)
JSON_MAP_END()
class PedalboardItemUseModGuiBody
{
public:
int64_t clientId_ = -1;
int64_t instanceId_ = -1;
bool useModUi_ = true;
DECLARE_JSON_MAP(PedalboardItemUseModGuiBody);
};
JSON_MAP_BEGIN(PedalboardItemUseModGuiBody)
JSON_MAP_REFERENCE(PedalboardItemUseModGuiBody, clientId)
JSON_MAP_REFERENCE(PedalboardItemUseModGuiBody, instanceId)
JSON_MAP_REFERENCE(PedalboardItemUseModGuiBody, useModUi)
JSON_MAP_END()
class UpdateCurrentPedalboardBody
{
public:
@@ -456,6 +472,21 @@ JSON_MAP_REFERENCE(SetSnapshotsBody, snapshots)
JSON_MAP_REFERENCE(SetSnapshotsBody, selectedSnapshot)
JSON_MAP_END()
class SetSelectedPedalboardPluginBody
{
public:
uint64_t clientId_;
uint64_t pluginInstanceId_;
DECLARE_JSON_MAP(SetSelectedPedalboardPluginBody);
};
JSON_MAP_BEGIN(SetSelectedPedalboardPluginBody)
JSON_MAP_REFERENCE(SetSelectedPedalboardPluginBody, clientId)
JSON_MAP_REFERENCE(SetSelectedPedalboardPluginBody, pluginInstanceId)
JSON_MAP_END()
class SnapshotModifiedBody
{
public:
@@ -1284,6 +1315,12 @@ public:
int64_t result = this->model.SaveCurrentPresetAs(this->clientId, body.name_, body.saveAfterInstanceId_);
Reply(replyTo, "saveCurrentPresetsAs", result);
}
else if (message == "setSelectedPedalboardPlugin")
{
SetSelectedPedalboardPluginBody body;
pReader->read(&body);
this->model.SetSelectedPedalboardPlugin(body.clientId_,body.pluginInstanceId_);
}
else if (message == "savePluginPresetAs")
{
SavePluginPresetAsBody body;
@@ -1303,6 +1340,11 @@ public:
pReader->read(&body);
model.SetPedalboardItemEnable(body.clientId_, body.instanceId_, body.enabled_);
}
else if (message == "setPedalboardItemUseModUi") {
PedalboardItemUseModGuiBody body;
pReader->read(&body);
model.SetPedalboardItemUseModUi(body.clientId_, body.instanceId_, body.useModUi_);
}
else if (message == "updateCurrentPedalboard")
{
{
@@ -1331,12 +1373,12 @@ public:
}
else if (message == "plugins")
{
auto ui_plugins = model.GetLv2Host().GetUiPlugins();
auto ui_plugins = model.GetPluginHost().GetUiPlugins();
Reply(replyTo, "plugins", ui_plugins);
}
else if (message == "pluginClasses")
{
auto classes = model.GetLv2Host().GetLv2PluginClass();
auto classes = model.GetPluginHost().GetLv2PluginClass();
Reply(replyTo, "pluginClasses", classes);
}
else if (message == "hello")
@@ -2221,6 +2263,15 @@ private:
body.enabled_ = enabled;
Send("onItemEnabledChanged", body);
}
virtual void OnItemUseModUiChanged(int64_t clientId, int64_t pedalItemId, bool enabled)
{
PedalboardItemEnabledBody body;
body.clientId_ = clientId;
body.instanceId_ = pedalItemId;
body.enabled_ = enabled;
Send("onUseItemModUiChanged", body);
}
};
std::atomic<uint64_t> PiPedalSocketHandler::nextClientId = 0;
+8
View File
@@ -140,6 +140,7 @@ UiFileType::UiFileType(PluginHost *pHost, const LilvNode *node)
UiFileProperty::UiFileProperty(PluginHost *pHost, const LilvNode *node, const std::filesystem::path &resourcePath)
{
auto pWorld = pHost->getWorld();
@@ -455,6 +456,13 @@ bool UiFileType::IsValidExtension(const std::string&extension) const {
return false;
}
UiFileType::UiFileType(const std::string&label, const std::string &mimeType,const std::string &fileType)
: label_(label)
, mimeType_(mimeType)
, fileExtension_(fileType)
{
}
UiFileType::UiFileType(const std::string&label, const std::string &fileType)
: label_(label)
, fileExtension_(fileType)
+7
View File
@@ -75,6 +75,7 @@ namespace pipedal
{
class PluginHost;
class ModFileTypes;
class UiFileType
{
@@ -87,12 +88,16 @@ namespace pipedal
UiFileType() {}
UiFileType(PluginHost *pHost, const LilvNode *node);
UiFileType(const std::string &label, const std::string &fileType);
UiFileType(const std::string &label, const std::string&mimeType, const std::string &fileType);
static std::vector<UiFileType> GetArray(PluginHost *pHost, const LilvNode *node, const LilvNode *uri);
const std::string &label() const { return label_; }
void label(const std::string &value) { label_ = value; }
const std::string &fileExtension() const { return fileExtension_; }
void fileExtension(const std::string &value) { fileExtension_ = value; }
const std::string &mimeType() const { return mimeType_; }
void mimeType(const std::string &value) { mimeType_ = value; }
bool IsValidExtension(const std::string &extension) const;
public:
@@ -160,6 +165,8 @@ namespace pipedal
const std::vector<UiFileType> &fileTypes() const { return fileTypes_; }
std::vector<UiFileType> &fileTypes() { return fileTypes_; }
void fileTypes(const std::vector<UiFileType> &value) { fileTypes_ = value; }
void fileTypes(std::vector<UiFileType> &&value) { fileTypes_ = std::move(value); }
const std::string &patchProperty() const { return patchProperty_; }
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) 2022 Robin Davies
// Copyright (c) 2025 Robin Davies
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
+285 -62
View File
@@ -136,8 +136,11 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld)
lv2core__symbol = lilv_new_uri(pWorld, LV2_CORE__symbol);
lv2core__name = lilv_new_uri(pWorld, LV2_CORE__name);
lv2core__shortName = lilv_new_uri(pWorld, LV2_CORE_PREFIX "shortName"); // ?? from mod sources
lv2core__index = lilv_new_uri(pWorld, LV2_CORE__index);
lv2core__Parameter = lilv_new_uri(pWorld, LV2_CORE_PREFIX "Parameter");
lv2core__minorVersion = lilv_new_uri(pWorld, LV2_CORE__minorVersion);
lv2core__microVersion = lilv_new_uri(pWorld, LV2_CORE__microVersion);
pipedalUI__ui = lilv_new_uri(pWorld, PIPEDAL_UI__ui);
pipedalUI__fileProperties = lilv_new_uri(pWorld, PIPEDAL_UI__fileProperties);
@@ -150,7 +153,7 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld)
pipedalUI__mimeType = lilv_new_uri(pWorld, PIPEDAL_UI__mimeType);
pipedalUI__outputPorts = lilv_new_uri(pWorld, PIPEDAL_UI__outputPorts);
pipedalUI__text = lilv_new_uri(pWorld, PIPEDAL_UI__text);
pipedalUI__ledColor = lilv_new_uri(pWorld,PIPEDAL_UI__ledColor);
pipedalUI__ledColor = lilv_new_uri(pWorld, PIPEDAL_UI__ledColor);
pipedalUI__frequencyPlot = lilv_new_uri(pWorld, PIPEDAL_UI__frequencyPlot);
pipedalUI__xLeft = lilv_new_uri(pWorld, PIPEDAL_UI__xLeft);
@@ -159,7 +162,7 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld)
pipedalUI__yBottom = lilv_new_uri(pWorld, PIPEDAL_UI__yBottom);
pipedalUI__xLog = lilv_new_uri(pWorld, PIPEDAL_UI__xLog);
pipedalUI__width = lilv_new_uri(pWorld, PIPEDAL_UI__width);
pipedalUI__graphicEq = lilv_new_uri(pWorld,PIPEDAL_UI__graphicEq);
pipedalUI__graphicEq = lilv_new_uri(pWorld, PIPEDAL_UI__graphicEq);
ui__portNotification = lilv_new_uri(pWorld, LV2_UI__portNotification);
ui__plugin = lilv_new_uri(pWorld, LV2_UI__plugin);
@@ -171,6 +174,7 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld)
lv2__port = lilv_new_uri(pWorld, LV2_CORE__port);
#define MOD_PREFIX "http://moddevices.com/ns/mod#"
mod__label = lilv_new_uri(pWorld, MOD_PREFIX "label");
mod__brand = lilv_new_uri(pWorld, MOD_PREFIX "brand");
mod__preferMomentaryOffByDefault = lilv_new_uri(pWorld, MOD_PREFIX "preferMomentaryOffByDefault");
@@ -194,16 +198,39 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld)
patch__writable = lilv_new_uri(pWorld, LV2_PATCH__writable);
patch__readable = lilv_new_uri(pWorld, LV2_PATCH__readable);
pipedal_patch__readable = lilv_new_uri(pWorld, PIPEDAL_PATCH__readable);
pipedal_patch__readable = lilv_new_uri(pWorld, PIPEDAL_PATCH__readable);
dc__format = lilv_new_uri(pWorld, "http://purl.org/dc/terms/format");
mod__fileTypes = lilv_new_uri(pWorld, "http://moddevices.com/ns/mod#fileTypes");
mod__supportedExtensions = lilv_new_uri(pWorld, "http://moddevices.com/ns/mod#supportedExtensions");
}
void PluginHost::LilvUris::Free()
{
}
static int32_t nodesAsInt(const LilvNodes *nodes)
{
if (!nodes)
return 0;
LILV_FOREACH(nodes, iNode, nodes)
{
const LilvNode *node = lilv_nodes_get(nodes, iNode);
if (!node)
return 0;
if (lilv_node_is_int(node))
{
return lilv_node_as_int(node);
}
if (lilv_node_is_float(node))
{
return static_cast<int32_t>(lilv_node_as_float(node));
}
break;
}
return 0;
}
static std::string nodeAsString(const LilvNode *node)
{
@@ -270,7 +297,6 @@ PluginHost::PluginHost()
fileMetadataFeature.Prepare(mapFeature);
lv2Features.push_back(fileMetadataFeature.GetFeature());
lv2Features.push_back(nullptr);
this->urids = new Urids(mapFeature);
@@ -294,8 +320,11 @@ PluginHost::~PluginHost()
{
delete lilvUris;
lilvUris = nullptr;
delete urids;
urids = nullptr;
delete mod_gui_uris;
mod_gui_uris = nullptr;
free_world();
}
@@ -397,7 +426,7 @@ void PluginHost::LoadPluginClassesFromLilv()
}
}
void PluginHost::Load(const char *lv2Path)
void PluginHost::LoadLilv(const char *lv2Path)
{
this->plugins_.clear();
@@ -418,6 +447,9 @@ void PluginHost::Load(const char *lv2Path)
lilv_world_load_all(pWorld);
}
// Not a safe pointer,because auto-deleting after freeWorld() would be death.
this->mod_gui_uris = new ModGuiUris(pWorld, mapFeature);
// LilvNode*lv2_path = lilv_new_file_uri(pWorld,NULL,lv2Path);
// lilv_world_set_option(world,LILV_OPTION_LV2_PATH,lv)
@@ -439,7 +471,8 @@ void PluginHost::Load(const char *lv2Path)
if (pluginInfo->hasCvPorts())
{
Lv2Log::debug("Plugin %s (%s) skipped. (Has CV ports).", pluginInfo->name().c_str(), pluginInfo->uri().c_str());
} else if (pluginInfo->hasUnsupportedPatchProperties())
}
else if (pluginInfo->hasUnsupportedPatchProperties())
{
Lv2Log::debug("Plugin %s (%s) skipped. (Has unsupported patch parameters).", pluginInfo->name().c_str(), pluginInfo->uri().c_str());
}
@@ -497,20 +530,20 @@ void PluginHost::Load(const char *lv2Path)
if (plugin->is_valid())
{
#if 1
// no plugins with more than 2 inputs or outputs.
// no zero-input or zero-output plugins (temporarily disables midi plugins)
// no zero output devices (permanent, I think)
if (info.audio_inputs() > 2 || info.audio_outputs() > 2) {
// no plugins with more than 2 inputs or outputs.
// no zero-input or zero-output plugins (temporarily disables midi plugins)
// no zero output devices (permanent, I think)
if (info.audio_inputs() > 2 || info.audio_outputs() > 2)
{
Lv2Log::debug(
"Plugin %s (%s) skipped. %d inputs, %d outputs.", plugin->name().c_str(), plugin->uri().c_str(),
(int)info.audio_inputs(),(int)info.audio_outputs());
(int)info.audio_inputs(), (int)info.audio_outputs());
}
else if (info.audio_inputs() == 0 && info.audio_outputs() == 0 )
else if (info.audio_inputs() == 0 && info.audio_outputs() == 0)
{
Lv2Log::debug("Plugin %s (%s) skipped. No audio i/o.", plugin->name().c_str(), plugin->uri().c_str());
} else if (info.audio_inputs() == 0)
}
else if (info.audio_inputs() == 0)
{
// temporarily disable this feature.
Lv2Log::debug("Plugin %s (%s) skipped. No inputs.", plugin->name().c_str(), plugin->uri().c_str());
@@ -536,7 +569,8 @@ void PluginHost::Load(const char *lv2Path)
#endif
else
{
if (info.audio_inputs() == 0) {
if (info.audio_inputs() == 0)
{
Lv2Log::debug("************* ZERO INPUTS: %s (%s) skipped. No audio outputs.", plugin->name().c_str(), plugin->uri().c_str());
}
ui_plugins_.push_back(std::move(info));
@@ -613,13 +647,44 @@ bool Lv2PluginInfo::HasFactoryPresets(PluginHost *lv2Host, const LilvPlugin *plu
return result;
}
static std::vector<UiFileType> ToPiPedalFileTypes(
const std::set<std::string> &modFileTypes)
{
std::vector<UiFileType> result;
for (const auto &fileType : modFileTypes)
{
UiFileType uiFileType;
std::string type = fileType;
if (type.find("/") != std::string::npos) // mime type?
{
UiFileType t = UiFileType(type,type,"");
result.push_back(t);
}
else
{
// add a leading dot.
if (!type.starts_with(".")) {
type = "." + type;
}
auto t = UiFileType(SS(type << " file"), type);
result.push_back(t);
}
}
std::sort(result.begin(), result.end(),
[](const UiFileType &left, const UiFileType &right)
{
return left.label() < right.label();
});
return result;
}
Lv2PluginInfo::FindWritablePathPropertiesResult
Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin *pPlugin)
{
// example:
// <http://github.com/mikeoliphant/neural-amp-modeler-lv2#model>
// a lv2:Parameter;
// a lv2:Parameter;
// rdfs:label "Model";
// rdfs:range atom:Path.
// ...
@@ -644,7 +709,6 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
if (lilv_world_ask(pWorld, propertyUri, lv2Host->lilvUris->rdfs__range, lv2Host->lilvUris->atom__Path))
{
AutoLilvNode label = lilv_world_get(pWorld, propertyUri, lv2Host->lilvUris->rdfs__label, nullptr);
std::string strLabel = label.AsString();
@@ -662,28 +726,30 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
std::make_shared<UiFileProperty>(
strLabel, propertyUri.AsUri(), lv2DirectoryName);
AutoLilvNode indexNode = lilv_world_get(pWorld, propertyUri, lv2Host->lilvUris->lv2core__index, nullptr);
int32_t index = indexNode.AsInt(-1);
fileProperty->index(index);
// if there's a pipedalui_fileTypes node, use that instead.
AutoLilvNode mod__fileTypes = lilv_world_get(pWorld, propertyUri, lv2Host->lilvUris->mod__fileTypes, nullptr);
// default: everything.
std::string fileTypes = ModFileTypes::DEFAULT_FILE_TYPES;
std::string fileTypes = ModFileTypes::DEFAULT_FILE_TYPES;
if (mod__fileTypes)
{
// "nam,nammodel"
fileTypes = mod__fileTypes.AsString();
}
std::string modFileExtensions;
AutoLilvNode mod__extensionsNode = lilv_world_get(pWorld, propertyUri, lv2Host->lilvUris->mod__supportedExtensions, nullptr);
{
// "wav,flac,mp3"
modFileExtensions = mod__extensionsNode.AsString();
}
std::string fileExtensions = modFileExtensions;
ModFileTypes modFileTypes(fileTypes);
// Legacy case: audio_uploads/<plugin_directory> exists.
std::filesystem::path bundleDirectoryName = std::filesystem::path(bundle_path()).parent_path().filename();
@@ -693,24 +759,32 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
fileProperty->directory(bundleDirectoryName);
if (std::filesystem::exists(legacyUploadPath)
&& !std::filesystem::exists(legacyUploadPath / ".migrated"))
if (std::filesystem::exists(legacyUploadPath) && !std::filesystem::exists(legacyUploadPath / ".migrated"))
{
fileProperty->useLegacyModDirectory(true);
fileProperty->directory(bundleDirectoryName);
modFileTypes.rootDirectories().push_back(bundleDirectoryName.filename()); // push the private directory!
} else {
}
else
{
if (modFileTypes.rootDirectories().size() == 1)
{
std::string modName = modFileTypes.rootDirectories()[0];
auto modDirectory = modFileTypes.GetModDirectory(modName);
fileProperty->directory(modDirectory->pipedalPath);
std::set<std::string> fileExtensions = modDirectory->fileExtensions;
if (!modFileExtensions.empty()) {
auto extensions = split(modFileExtensions, ',');
fileExtensions = std::set<std::string>(extensions.begin(), extensions.end());
}
fileProperty->fileTypes(ToPiPedalFileTypes(fileExtensions));
}
}
fileProperties.push_back(fileProperty);
} else {
}
else
{
unsupportedPatchProperty = true;
}
}
@@ -718,24 +792,28 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
}
FindWritablePathPropertiesResult result;
if (fileProperties.size() != 0)
{
std::sort(fileProperties.begin(),fileProperties.end(),[](const UiFileProperty::ptr& left,const UiFileProperty::ptr&right) {
// properies with indexes first.
int32_t indexL = left->index();
if (indexL < 0) indexL = std::numeric_limits<int32_t>::max();
int32_t indexR = right->index();
if (indexR < 0) indexR = std::numeric_limits<int32_t>::max();
if (indexL < indexR) return true;
if (indexL > indexR) return false;
std::sort(
fileProperties.begin(),
fileProperties.end(),
[](const UiFileProperty::ptr &left, const UiFileProperty::ptr &right)
{
// properies with indexes first.
int32_t indexL = left->index();
if (indexL < 0)
indexL = std::numeric_limits<int32_t>::max();
int32_t indexR = right->index();
if (indexR < 0)
indexR = std::numeric_limits<int32_t>::max();
if (indexL < indexR)
return true;
if (indexL > indexR)
return false;
// there is no natural order. TTL indexing means that the order we see them in is random.
// We can't order them sensibly. Let's at least order them consistently.
return left->label() < right->label();
});
// there is no natural order. TTL indexing means that the order we see them in is random.
// We can't order them sensibly. Let's at least order them consistently.
return left->label() < right->label(); });
result.pipedalUi = std::make_shared<PiPedalUI>(std::move(fileProperties));
}
result.hasUnsupportedPatchProperties = unsupportedPatchProperty;
@@ -750,7 +828,10 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP
std::string bundleUri = bundleUriNode.AsUri();
std::string bundlePath = lilv_file_uri_parse(bundleUri.c_str(), nullptr);
char *lilvBundlePath = lilv_file_uri_parse(bundleUri.c_str(), nullptr);
std::string bundlePath = lilvBundlePath;
lilv_free(lilvBundlePath);
if (bundlePath.length() == 0)
throw std::logic_error("Bundle uri is not a file uri.");
@@ -764,6 +845,12 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP
AutoLilvNode name = (lilv_plugin_get_name(pPlugin));
this->name_ = nodeAsString(name);
AutoLilvNodes minorVersion = lilv_plugin_get_value(pPlugin, lv2Host->lilvUris->lv2core__minorVersion);
this->minorVersion_ = nodesAsInt(minorVersion);
AutoLilvNodes microVersion = lilv_plugin_get_value(pPlugin, lv2Host->lilvUris->lv2core__microVersion);
this->microVersion_ = nodesAsInt(microVersion);
AutoLilvNode brand = lilv_world_get(pWorld, plugUri, lv2Host->lilvUris->mod__brand, nullptr);
this->brand_ = nodeAsString(brand);
@@ -838,6 +925,74 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP
}
std::sort(ports_.begin(), ports_.end(), ports_sort_compare);
// Fetch patch properties.
{
AutoLilvNodes patchWritables = lilv_world_find_nodes(pWorld, plugUri, lv2Host->lilvUris->patch__writable, nullptr);
bool unsupportedPatchProperty = false;
LILV_FOREACH(nodes, iNode, patchWritables)
{
AutoLilvNode propertyUri = lilv_nodes_get(patchWritables, iNode);
if (propertyUri)
{
// a lv2:Parameter?
if (lilv_world_ask(pWorld, propertyUri, lv2Host->lilvUris->isA, lv2Host->lilvUris->lv2core__Parameter))
{
Lv2PatchPropertyInfo propertyInfo{lv2Host, propertyUri};
propertyInfo.writable(true);
patchProperties_.push_back(std::move(propertyInfo));
}
}
}
}
{
AutoLilvNodes patchReadables = lilv_world_find_nodes(pWorld, plugUri, lv2Host->lilvUris->patch__readable, nullptr);
bool unsupportedPatchProperty = false;
LILV_FOREACH(nodes, iNode, patchReadables)
{
AutoLilvNode propertyUri = lilv_nodes_get(patchReadables, iNode);
if (propertyUri)
{
std::string uri = propertyUri.AsUri();
bool found = false;
for (auto &property : patchProperties_)
{
if (property.uri() == uri)
{
property.readable(true);
found = true;
break;
}
}
if (!found)
{
if (lilv_world_ask(pWorld, propertyUri, lv2Host->lilvUris->isA, lv2Host->lilvUris->lv2core__Parameter))
{
Lv2PatchPropertyInfo propertyInfo{lv2Host, propertyUri};
propertyInfo.readable(true);
patchProperties_.push_back(std::move(propertyInfo));
}
}
}
}
// default state.
{
AutoLilvNodes stateNodes = lilv_plugin_get_value(pPlugin, lv2Host->lilvUris->state__state);
if (stateNodes)
{
auto stateNode = lilv_nodes_get_first(stateNodes);
LilvState *defaultState = lilv_state_new_from_world(pWorld, lv2Host->GetMapFeature().GetMap(), stateNode);
if (defaultState)
{
Lv2Log::debug("Plugin %s (%s) has default state.", this->name_.c_str(), this->uri_.c_str());
this->hasDefaultState_ = true;
lilv_state_free(defaultState);
}
}
}
}
// Fetch pipedal plugin UI
@@ -886,7 +1041,11 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP
++nOutputs;
}
}
if (nOutputs == 0 || nInputs == 0)
{
isValid = false;
}
this->modGui_ = ModGui::Create(lv2Host, pPlugin);
this->is_valid_ = isValid;
}
@@ -1012,22 +1171,21 @@ Lv2PortInfo::Lv2PortInfo(PluginHost *host, const LilvPlugin *plugin, const LilvP
this->integer_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->integer_property_uri);
this->mod_momentaryOffByDefault_ = lilv_port_has_property(plugin, pPort, host->lilvUris->mod__preferMomentaryOffByDefault);
this->mod_momentaryOnByDefault_ = lilv_port_has_property(plugin, pPort, host->lilvUris->mod__preferMomentaryOnByDefault);
this->pipedal_graphicEq_ = lilv_port_has_property(plugin, pPort, host->lilvUris->pipedalUI__graphicEq);
this->pipedal_graphicEq_ = lilv_port_has_property(plugin, pPort, host->lilvUris->pipedalUI__graphicEq);
this->enumeration_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->enumeration_property_uri);
this->toggled_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->core__toggled);
this->not_on_gui_ = lilv_port_has_property(plugin, pPort, host->lilvUris->portprops__not_on_gui_property_uri);
this->connection_optional_ = lilv_port_has_property(plugin, pPort, host->lilvUris->core__connectionOptional);
this->trigger_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->portprops__trigger);
AutoLilvNode port_ledColor = lilv_port_get(plugin,pPort,host->lilvUris->pipedalUI__ledColor);
AutoLilvNode port_ledColor = lilv_port_get(plugin, pPort, host->lilvUris->pipedalUI__ledColor);
if (port_ledColor)
{
auto value = lilv_node_as_string(port_ledColor);
if (value) {
if (value)
{
this->pipedal_ledColor_ = value;
}
}
@@ -1164,6 +1322,8 @@ bool PluginHost::is_a(const std::string &class_, const std::string &target_class
Lv2PluginUiInfo::Lv2PluginUiInfo(PluginHost *pHost, const Lv2PluginInfo *plugin)
: uri_(plugin->uri()),
name_(plugin->name()),
minorVersion_(plugin->minorVersion()),
microVersion_(plugin->microVersion()),
brand_(plugin->brand()),
label_(plugin->label()),
author_name_(plugin->author_name()),
@@ -1173,7 +1333,9 @@ Lv2PluginUiInfo::Lv2PluginUiInfo(PluginHost *pHost, const Lv2PluginInfo *plugin)
audio_outputs_(0),
description_(plugin->comment()),
has_midi_input_(false),
has_midi_output_(false)
has_midi_output_(false),
modGui_(plugin->modGui()),
patchProperties_(plugin->patchProperties())
{
PLUGIN_MAP_CHECK();
auto pluginClass = pHost->GetPluginClass(plugin->plugin_class());
@@ -1253,7 +1415,7 @@ std::shared_ptr<Lv2PluginInfo> PluginHost::GetPluginInfo(const std::string &uri)
return ff->second;
}
Lv2Pedalboard *PluginHost::UpdateLv2PedalboardStructure(Pedalboard &pedalboard,Lv2Pedalboard *existingPedalboard,Lv2PedalboardErrorList &errorList)
Lv2Pedalboard *PluginHost::UpdateLv2PedalboardStructure(Pedalboard &pedalboard, Lv2Pedalboard *existingPedalboard, Lv2PedalboardErrorList &errorList)
{
ExistingEffectMap existingEffects;
@@ -1261,7 +1423,8 @@ Lv2Pedalboard *PluginHost::UpdateLv2PedalboardStructure(Pedalboard &pedalboard,L
{
for (auto &effect : existingPedalboard->GetSharedEffectList())
{
if (effect->IsLv2Effect()) {
if (effect->IsLv2Effect())
{
existingEffects[effect->GetInstanceId()] = effect;
}
}
@@ -1269,7 +1432,7 @@ Lv2Pedalboard *PluginHost::UpdateLv2PedalboardStructure(Pedalboard &pedalboard,L
Lv2Pedalboard *pPedalboard = new Lv2Pedalboard();
try
{
pPedalboard->Prepare(this, pedalboard, errorList,&existingEffects);
pPedalboard->Prepare(this, pedalboard, errorList, &existingEffects);
return pPedalboard;
}
catch (const std::exception &e)
@@ -1495,9 +1658,9 @@ Lv2PortGroup::Lv2PortGroup(PluginHost *lv2Host, const std::string &groupUri)
this->uri_ = groupUri;
AutoLilvNode uri = lilv_new_uri(pWorld, groupUri.c_str());
LilvNode *symbolNode = lilv_world_get(pWorld, uri, lv2Host->lilvUris->lv2core__symbol, nullptr);
AutoLilvNode symbolNode = lilv_world_get(pWorld, uri, lv2Host->lilvUris->lv2core__symbol, nullptr);
symbol_ = nodeAsString(symbolNode);
LilvNode *nameNode = lilv_world_get(pWorld, uri, lv2Host->lilvUris->lv2core__name, nullptr);
AutoLilvNode nameNode = lilv_world_get(pWorld, uri, lv2Host->lilvUris->lv2core__name, nullptr);
name_ = nodeAsString(nameNode);
}
@@ -1589,17 +1752,16 @@ static void createTargetLinks(const std::filesystem::path &sourceDirectory, cons
}
}
std::string PluginHost::MapResourcePath(const std::string&pluginUri, const std::string&filePath)
std::string PluginHost::MapResourcePath(const std::string &pluginUri, const std::string &filePath)
{
auto plugin = GetPluginInfo(pluginUri);
if (plugin) {
if (plugin)
{
return filePath;
}
return filePath;
}
void PluginHost::CheckForResourceInitialization(const std::string &pluginUri, const std::filesystem::path &pluginUploadDirectory)
@@ -1679,6 +1841,47 @@ std::string PluginHost::AbstractPath(const std::string &path)
}
return path;
}
Lv2PatchPropertyInfo::Lv2PatchPropertyInfo(PluginHost *pluginHost, const LilvNode *propertyUri)
{
LilvWorld *pWorld = pluginHost->getWorld();
this->uri_ = nodeAsString(propertyUri);
AutoLilvNode range = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->rdfs__range, nullptr);
if (range)
{
this->type_ = range.AsUri();
}
AutoLilvNode comment = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->rdfs__Comment, nullptr);
if (comment)
{
this->comment_ = comment.AsString();
}
AutoLilvNode shortName = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->lv2core__shortName, nullptr);
if (shortName)
{
this->shortName_ = shortName.AsString();
}
else
{
this->shortName_ = this->label_;
}
AutoLilvNode indexNode = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->lv2core__index, nullptr);
this->index_ = indexNode.AsInt(-1);
AutoLilvNode modFileTypesNode = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->mod__fileTypes, nullptr);
if (modFileTypesNode)
{
std::string strFileTypes = modFileTypesNode.AsString();
this->fileTypes_ = split(strFileTypes, ',');
}
AutoLilvNode modSupportedExtensionsNode = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->mod__supportedExtensions, nullptr);
if (modSupportedExtensionsNode)
{
std::string strSupportedExtensions = modSupportedExtensionsNode.AsString();
this->supportedExtensions_ = split(strSupportedExtensions, ',');
}
}
// void PiPedalHostLogError(const std::string &error)
// {
@@ -1757,6 +1960,8 @@ json_map::storage_type<Lv2PortGroup> Lv2PortGroup::jmap{{
json_map::storage_type<Lv2PluginInfo> Lv2PluginInfo::jmap{{
json_map::reference("bundle_path", &Lv2PluginInfo::bundle_path_),
json_map::reference("uri", &Lv2PluginInfo::uri_),
json_map::reference("minorVersion", &Lv2PluginInfo::minorVersion_),
json_map::reference("microVersion", &Lv2PluginInfo::microVersion_),
json_map::reference("name", &Lv2PluginInfo::name_),
json_map::reference("brand", &Lv2PluginInfo::brand_),
json_map::reference("label", &Lv2PluginInfo::label_),
@@ -1774,6 +1979,10 @@ json_map::storage_type<Lv2PluginInfo> Lv2PluginInfo::jmap{{
json_map::reference("port_groups", &Lv2PluginInfo::port_groups_),
json_map::reference("is_valid", &Lv2PluginInfo::is_valid_),
json_map::reference("has_factory_presets", &Lv2PluginInfo::has_factory_presets_),
json_map::reference("modGui", &Lv2PluginInfo::modGui_),
json_map::reference("patchProperties", &Lv2PluginInfo::patchProperties_),
json_map::reference("hasDefaultState", &Lv2PluginInfo::hasDefaultState_),
}};
json_map::storage_type<Lv2PluginClass> Lv2PluginClass::jmap{{
@@ -1831,6 +2040,8 @@ json_map::storage_type<Lv2PluginUiInfo>
{
json_map::reference("uri", &Lv2PluginUiInfo::uri_),
json_map::reference("name", &Lv2PluginUiInfo::name_),
json_map::reference("minorVersion", &Lv2PluginUiInfo::minorVersion_),
json_map::reference("microVersion", &Lv2PluginUiInfo::microVersion_),
json_map::reference("brand", &Lv2PluginUiInfo::brand_),
json_map::reference("label", &Lv2PluginUiInfo::label_),
json_map::enum_reference("plugin_type", &Lv2PluginUiInfo::plugin_type_, get_plugin_type_enum_converter()),
@@ -1848,4 +2059,16 @@ json_map::storage_type<Lv2PluginUiInfo>
json_map::reference("fileProperties", &Lv2PluginUiInfo::fileProperties_),
json_map::reference("frequencyPlots", &Lv2PluginUiInfo::frequencyPlots_),
json_map::reference("uiPortNotifications", &Lv2PluginUiInfo::uiPortNotifications_),
json_map::reference("modGui", &Lv2PluginUiInfo::modGui_),
json_map::reference("patchProperties", &Lv2PluginUiInfo::patchProperties_),
}};
JSON_MAP_BEGIN(Lv2PatchPropertyInfo)
JSON_MAP_REFERENCE(Lv2PatchPropertyInfo, uri)
JSON_MAP_REFERENCE(Lv2PatchPropertyInfo, label)
JSON_MAP_REFERENCE(Lv2PatchPropertyInfo, type)
JSON_MAP_REFERENCE(Lv2PatchPropertyInfo, comment)
JSON_MAP_REFERENCE(Lv2PatchPropertyInfo, shortName)
JSON_MAP_REFERENCE(Lv2PatchPropertyInfo, fileTypes)
JSON_MAP_REFERENCE(Lv2PatchPropertyInfo, supportedExtensions)
JSON_MAP_END()
+71 -4
View File
@@ -1,4 +1,4 @@
// Copyright (c) 2022 Robin Davies
// Copyright (c) 2025 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
@@ -32,6 +32,7 @@
#include <string>
#include "IHost.hpp"
#include <set>
#include "ModGui.hpp"
//#include "lv2.h"
#include "Units.hpp"
@@ -42,6 +43,7 @@
#include "AutoLilvNode.hpp"
#include "PiPedalUI.hpp"
#include "MapPathFeature.hpp"
#include "ModGui.hpp"
namespace pipedal
{
@@ -325,6 +327,38 @@ namespace pipedal
static json_map::storage_type<Lv2PortInfo> jmap;
};
class Lv2PatchPropertyInfo {
private:
std::string uri_;
bool writable_ = false;
bool readable_ = false;
std::string label_;
uint32_t index_ = -1;
std::string type_;
std::string comment_;
std::string shortName_;
std::vector<std::string> fileTypes_;
std::vector<std::string> supportedExtensions_;
public:
Lv2PatchPropertyInfo() {}
Lv2PatchPropertyInfo(PluginHost *pluginHost, const LilvNode *propertyUri);
LV2_PROPERTY_GETSET(uri);
LV2_PROPERTY_GETSET(writable);
LV2_PROPERTY_GETSET(readable);
LV2_PROPERTY_GETSET(label);
LV2_PROPERTY_GETSET_SCALAR(index);
LV2_PROPERTY_GETSET(type);
LV2_PROPERTY_GETSET(comment);
LV2_PROPERTY_GETSET(shortName);
LV2_PROPERTY_GETSET(fileTypes);
LV2_PROPERTY_GETSET(supportedExtensions);
DECLARE_JSON_MAP(Lv2PatchPropertyInfo);
};
class Lv2PortGroup
{
private:
@@ -364,6 +398,8 @@ namespace pipedal
bool HasFactoryPresets(PluginHost *lv2Host, const LilvPlugin *plugin);
std::string bundle_path_;
std::string uri_;
uint32_t minorVersion_ = 0;
uint32_t microVersion_ = 0;
std::string name_;
std::string plugin_class_;
std::string brand_;
@@ -380,8 +416,14 @@ namespace pipedal
std::string comment_;
std::vector<std::shared_ptr<Lv2PortInfo>> ports_;
std::vector<std::shared_ptr<Lv2PortGroup>> port_groups_;
std::vector<Lv2PatchPropertyInfo> patchProperties_;
bool hasDefaultState_;
bool is_valid_ = false;
PiPedalUI::ptr piPedalUI_;
ModGui::ptr modGui_;
bool hasUnsupportedPatchProperties_ = false;
bool IsSupportedFeature(const std::string &feature) const;
@@ -390,6 +432,8 @@ namespace pipedal
LV2_PROPERTY_GETSET(bundle_path)
LV2_PROPERTY_GETSET(uri)
LV2_PROPERTY_GETSET(name)
LV2_PROPERTY_GETSET(minorVersion)
LV2_PROPERTY_GETSET(microVersion)
LV2_PROPERTY_GETSET(brand)
LV2_PROPERTY_GETSET(label)
LV2_PROPERTY_GETSET(plugin_class)
@@ -406,6 +450,9 @@ namespace pipedal
LV2_PROPERTY_GETSET(has_factory_presets)
LV2_PROPERTY_GETSET(piPedalUI)
LV2_PROPERTY_GETSET(hasUnsupportedPatchProperties)
LV2_PROPERTY_GETSET(modGui)
LV2_PROPERTY_GETSET(patchProperties)
LV2_PROPERTY_GETSET(hasDefaultState)
const Lv2PortInfo &getPort(const std::string &symbol)
{
@@ -618,6 +665,8 @@ namespace pipedal
private:
std::string uri_;
std::string name_;
uint32_t minorVersion_ = 0;
uint32_t microVersion_ = 0;;
std::string brand_;
std::string label_;
std::string author_name_;
@@ -636,10 +685,15 @@ namespace pipedal
std::vector<UiFileProperty::ptr> fileProperties_;
std::vector<UiFrequencyPlot::ptr> frequencyPlots_;
std::vector<UiPortNotification::ptr> uiPortNotifications_;
ModGui::ptr modGui_;
std::vector<Lv2PatchPropertyInfo> patchProperties_;
public:
LV2_PROPERTY_GETSET(uri)
LV2_PROPERTY_GETSET(name)
LV2_PROPERTY_GETSET(minorVersion)
LV2_PROPERTY_GETSET(microVersion)
LV2_PROPERTY_GETSET(brand)
LV2_PROPERTY_GETSET(label)
LV2_PROPERTY_GETSET(author_name)
@@ -657,6 +711,8 @@ namespace pipedal
LV2_PROPERTY_GETSET(fileProperties)
LV2_PROPERTY_GETSET(frequencyPlots)
LV2_PROPERTY_GETSET(uiPortNotifications)
LV2_PROPERTY_GETSET(modGui)
LV2_PROPERTY_GETSET(patchProperties)
static json_map::storage_type<Lv2PluginUiInfo> jmap;
};
@@ -679,6 +735,8 @@ namespace pipedal
#endif
friend class pipedal::AutoLilvNode;
friend class pipedal::PiPedalUI;
friend class PluginHostTest;
static const char *RDFS__comment;
static const char *RDFS__range;
@@ -707,6 +765,7 @@ namespace pipedal
AutoLilvNode invada_units__unit; // typo in invada plugins.
AutoLilvNode invada_portprops__logarithmic; // typo in invada plugins.
AutoLilvNode atom__bufferType;
AutoLilvNode atom__Path;
AutoLilvNode presets__preset;
@@ -714,8 +773,11 @@ namespace pipedal
AutoLilvNode rdfs__label;
AutoLilvNode lv2core__symbol;
AutoLilvNode lv2core__name;
AutoLilvNode lv2core__shortName;
AutoLilvNode lv2core__index;
AutoLilvNode lv2core__Parameter;
AutoLilvNode lv2core__minorVersion;
AutoLilvNode lv2core__microVersion;
AutoLilvNode pipedalUI__ui;
AutoLilvNode pipedalUI__fileProperties;
AutoLilvNode pipedalUI__directory;
@@ -766,6 +828,7 @@ namespace pipedal
AutoLilvNode patch__readable;
AutoLilvNode pipedal_patch__readable;
AutoLilvNode mod__brand;
AutoLilvNode mod__label;
AutoLilvNode mod__preferMomentaryOffByDefault;
@@ -773,6 +836,7 @@ namespace pipedal
AutoLilvNode dc__format;
AutoLilvNode mod__fileTypes;
AutoLilvNode mod__supportedExtensions;
AutoLilvNode pipedalui__fileTypes;
@@ -817,6 +881,7 @@ namespace pipedal
friend class Lv2PluginInfo;
friend class Lv2PortInfo;
friend class Lv2PortGroup;
friend class ModGui;
std::shared_ptr<Lv2PluginClass> GetPluginClass(const LilvPluginClass *pClass);
std::shared_ptr<Lv2PluginClass> MakePluginClass(const LilvPluginClass *pClass);
@@ -924,7 +989,9 @@ namespace pipedal
class Urids;
Urids *urids;
Urids *urids = nullptr;
ModGuiUris* mod_gui_uris = nullptr;
void OnConfigurationChanged(const JackConfiguration &configuration, const JackChannelSelection &settings);
@@ -933,7 +1000,7 @@ namespace pipedal
std::shared_ptr<Lv2PluginClass> GetLv2PluginClass() const;
std::vector<std::shared_ptr<Lv2PluginInfo>> GetPlugins() const { return plugins_; }
const std::vector<std::shared_ptr<Lv2PluginInfo>>& GetPlugins() const { return plugins_; }
const std::vector<Lv2PluginUiInfo> &GetUiPlugins() const { return ui_plugins_; }
virtual std::shared_ptr<Lv2PluginInfo> GetPluginInfo(const std::string &uri) const;
@@ -941,7 +1008,7 @@ namespace pipedal
static constexpr const char *DEFAULT_LV2_PATH = "/usr/lib/lv2";
void LoadPluginClassesFromJson(std::filesystem::path jsonFile);
void Load(const char *lv2Path = PluginHost::DEFAULT_LV2_PATH);
void LoadLilv(const char *lv2Path = PluginHost::DEFAULT_LV2_PATH);
virtual LV2_URID GetLv2Urid(const char *uri)
{
+4 -1
View File
@@ -23,6 +23,7 @@
#include "json.hpp"
#include <sstream>
#include <string.h>
#include "Lv2Log.hpp"
using namespace pipedal;
@@ -83,6 +84,7 @@ static void CheckState(LV2_State_Status status)
case LV2_State_Status::LV2_STATE_SUCCESS:
return;
default:
case LV2_State_Status::LV2_STATE_ERR_UNKNOWN:
lv2Error = "Unknown error.";
break;
case LV2_State_Status::LV2_STATE_ERR_BAD_TYPE:
@@ -126,7 +128,8 @@ Lv2PluginState StateInterface::Save()
}
catch (const std::exception &e)
{
throw std::logic_error(SS("State save failed. " << e.what()));
Lv2Log::debug(SS("State save failed. " << e.what()));
return Lv2PluginState(); // an invalid state.
}
return std::move(callState.state);
+1
View File
@@ -68,3 +68,4 @@ json_enum_converter<Units> *get_units_enum_converter();
} // namespace.

Some files were not shown because too many files have changed in this diff Show More