From e7cdd38056d2f6f307884580d60d7c3d5a77d1b6 Mon Sep 17 00:00:00 2001 From: "Robin E. R. Davies" Date: Mon, 7 Jul 2025 02:30:30 -0400 Subject: [PATCH] Checkpoint --- .vscode/launch.json | 5 +- .vscode/settings.json | 2 +- PiPedalCommon/src/DBusDispatcher.cpp | 11 +- PiPedalCommon/src/include/DBusDispatcher.hpp | 4 +- PiPedalCommon/src/include/HtmlHelper.hpp | 3 + artifacts/mod-ui.svgz | Bin 0 -> 1002 bytes artifacts/pp-ui.svgz | Bin 0 -> 1509 bytes artifacts/red-light-on.png | 112 ++ artifacts/red-light.svgz | Bin 0 -> 5542 bytes src/CMakeLists.txt | 5 + src/Lv2HostLeakTest.cpp | 2 +- src/MimeTypes.cpp | 12 +- src/ModFileTypes.cpp | 1 + src/ModFileTypes.hpp | 2 + src/ModGui.cpp | 531 ++++++ src/ModGui.hpp | 180 ++ src/ModGuiTest.cpp | 117 ++ src/ModTemplateGenerator.cpp | 295 ++++ src/ModTemplateGenerator.hpp | 41 + src/PiPedalModel.cpp | 2 +- src/PiPedalModel.hpp | 4 +- src/PiPedalSocket.cpp | 4 +- src/PipewireInputStreamTest.cpp | 2 +- src/PluginHost.cpp | 266 ++- src/PluginHost.hpp | 70 +- src/Units.hpp | 1 + src/WebServer.cpp | 9 + src/WebServer.hpp | 1 + src/WebServerConfig.cpp | 6 +- src/WebServerMod.cpp | 308 ++++ src/WebServerMod.hpp | 61 + todo.txt | 9 +- vite/CMakeLists.txt | 3 - vite/index.html | 2 +- vite/public/css/modGui.css | 64 + vite/public/css/roboto.css | 8 +- vite/public/fonts/comforta/Comfortaa-Bold.ttf | Bin 0 -> 105220 bytes .../public/fonts/comforta/Comfortaa-Light.ttf | Bin 0 -> 127532 bytes .../fonts/comforta/Comfortaa-Regular.ttf | Bin 0 -> 98408 bytes .../cooper/cooperhewitt-book-webfont.woff2 | Bin 0 -> 11152 bytes .../cooperhewitt-bookitalic-webfont.woff2 | Bin 0 -> 11728 bytes .../cooper/cooperhewitt-light-webfont.woff2 | Bin 0 -> 10984 bytes .../cooperhewitt-lightitalic-webfont.woff2 | Bin 0 -> 11952 bytes .../cooperhewitt-semibold-webfont.woff2 | Bin 0 -> 10964 bytes .../fonts/england-hand/england-webfont.woff | Bin 0 -> 43872 bytes vite/public/fonts/england-hand/stylesheet.css | 10 + vite/public/fonts/epf/epf_lul-webfont.woff | Bin 0 -> 14728 bytes vite/public/fonts/epf/stylesheet.css | 7 + .../fonts/nexa/Nexa_Free_Bold-webfont.woff | Bin 0 -> 30992 bytes vite/public/fonts/nexa/stylesheet.css | 33 + .../fonts/pirulen/pirulen_rg-webfont.woff | Bin 0 -> 15836 bytes vite/public/fonts/pirulen/stylesheet.css | 9 + .../questrial/questrial-regular-webfont.woff | Bin 0 -> 21868 bytes vite/public/fonts/questrial/stylesheet.css | 10 + vite/public/img/BlackKnob.png | Bin 0 -> 114549 bytes vite/public/img/audio-input.png | Bin 0 -> 5590 bytes vite/public/img/audio-output.png | Bin 0 -> 5672 bytes vite/public/img/footswitch_strip.png | Bin 0 -> 42663 bytes vite/public/img/red-light-off.png | Bin 0 -> 766 bytes vite/public/img/red-light-on.png | Bin 0 -> 1790 bytes vite/public/vite.svg | 1 - vite/src/App.css | 0 vite/src/App.tsx | 36 - vite/src/pipedal/AboutDialog.tsx | 2 +- vite/src/pipedal/App.tsx | 23 +- vite/src/pipedal/AppThemed.tsx | 2 + vite/src/pipedal/AutoZoom.tsx | 23 + vite/src/pipedal/ControlViewFactory.tsx | 20 +- vite/src/pipedal/FontTest.tsx | 75 + vite/src/pipedal/IconButtonEx.tsx | 6 +- vite/src/pipedal/LoadPluginDialog.tsx | 25 +- vite/src/pipedal/Lv2Plugin.tsx | 171 +- vite/src/pipedal/MainPage.tsx | 63 +- vite/src/pipedal/ModGuiErrorBoundary.tsx | 104 ++ vite/src/pipedal/ModGuiHost.tsx | 1453 +++++++++++++++++ vite/src/pipedal/ModGuiTest.tsx | 346 ++++ vite/src/pipedal/OkDialog.tsx | 105 ++ vite/src/pipedal/PiPedalModel.tsx | 10 +- vite/src/pipedal/PluginControlView.tsx | 123 +- vite/src/pipedal/ScratchClass.tsx | 91 ++ vite/src/pipedal/svg/mod_ui.svg | 25 + vite/src/pipedal/svg/pp_ui.svg | 23 + vite/vite.config.ts | 8 + 83 files changed, 4733 insertions(+), 214 deletions(-) create mode 100644 artifacts/mod-ui.svgz create mode 100644 artifacts/pp-ui.svgz create mode 100644 artifacts/red-light-on.png create mode 100644 artifacts/red-light.svgz create mode 100644 src/ModGui.cpp create mode 100644 src/ModGui.hpp create mode 100644 src/ModGuiTest.cpp create mode 100644 src/ModTemplateGenerator.cpp create mode 100644 src/ModTemplateGenerator.hpp create mode 100644 src/WebServerMod.cpp create mode 100644 src/WebServerMod.hpp create mode 100644 vite/public/css/modGui.css create mode 100755 vite/public/fonts/comforta/Comfortaa-Bold.ttf create mode 100755 vite/public/fonts/comforta/Comfortaa-Light.ttf create mode 100755 vite/public/fonts/comforta/Comfortaa-Regular.ttf create mode 100644 vite/public/fonts/cooper/cooperhewitt-book-webfont.woff2 create mode 100644 vite/public/fonts/cooper/cooperhewitt-bookitalic-webfont.woff2 create mode 100644 vite/public/fonts/cooper/cooperhewitt-light-webfont.woff2 create mode 100644 vite/public/fonts/cooper/cooperhewitt-lightitalic-webfont.woff2 create mode 100644 vite/public/fonts/cooper/cooperhewitt-semibold-webfont.woff2 create mode 100755 vite/public/fonts/england-hand/england-webfont.woff create mode 100755 vite/public/fonts/england-hand/stylesheet.css create mode 100755 vite/public/fonts/epf/epf_lul-webfont.woff create mode 100755 vite/public/fonts/epf/stylesheet.css create mode 100755 vite/public/fonts/nexa/Nexa_Free_Bold-webfont.woff create mode 100755 vite/public/fonts/nexa/stylesheet.css create mode 100755 vite/public/fonts/pirulen/pirulen_rg-webfont.woff create mode 100755 vite/public/fonts/pirulen/stylesheet.css create mode 100755 vite/public/fonts/questrial/questrial-regular-webfont.woff create mode 100755 vite/public/fonts/questrial/stylesheet.css create mode 100755 vite/public/img/BlackKnob.png create mode 100644 vite/public/img/audio-input.png create mode 100644 vite/public/img/audio-output.png create mode 100644 vite/public/img/footswitch_strip.png create mode 100644 vite/public/img/red-light-off.png create mode 100644 vite/public/img/red-light-on.png delete mode 100644 vite/public/vite.svg delete mode 100644 vite/src/App.css delete mode 100644 vite/src/App.tsx create mode 100644 vite/src/pipedal/AutoZoom.tsx create mode 100644 vite/src/pipedal/FontTest.tsx create mode 100644 vite/src/pipedal/ModGuiErrorBoundary.tsx create mode 100644 vite/src/pipedal/ModGuiHost.tsx create mode 100644 vite/src/pipedal/ModGuiTest.tsx create mode 100644 vite/src/pipedal/OkDialog.tsx create mode 100644 vite/src/pipedal/ScratchClass.tsx create mode 100644 vite/src/pipedal/svg/mod_ui.svg create mode 100644 vite/src/pipedal/svg/pp_ui.svg diff --git a/.vscode/launch.json b/.vscode/launch.json index 9dce54a..300d90f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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, diff --git a/.vscode/settings.json b/.vscode/settings.json index 625c4ba..429b7a3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -161,7 +161,7 @@ "cSpell.ignoreWords": [ "nammodel" ], - "cSpell.enabled": true, + "cSpell.enabled": false, // Disable all automatic completion suggestions - only show when Ctrl+Space is pressed "editor.quickSuggestions": { diff --git a/PiPedalCommon/src/DBusDispatcher.cpp b/PiPedalCommon/src/DBusDispatcher.cpp index 234efdf..844e7a5 100644 --- a/PiPedalCommon/src/DBusDispatcher.cpp +++ b/PiPedalCommon/src/DBusDispatcher.cpp @@ -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(); } } diff --git a/PiPedalCommon/src/include/DBusDispatcher.hpp b/PiPedalCommon/src/include/DBusDispatcher.hpp index af4288c..1da5414 100644 --- a/PiPedalCommon/src/include/DBusDispatcher.hpp +++ b/PiPedalCommon/src/include/DBusDispatcher.hpp @@ -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 stopping; std::vector postedEvents; - bool threadJoined; + bool threadStarted; std::thread serviceThread; std::mutex postMutex; }; \ No newline at end of file diff --git a/PiPedalCommon/src/include/HtmlHelper.hpp b/PiPedalCommon/src/include/HtmlHelper.hpp index 215f5ac..fc0d5cb 100644 --- a/PiPedalCommon/src/include/HtmlHelper.hpp +++ b/PiPedalCommon/src/include/HtmlHelper.hpp @@ -35,6 +35,9 @@ public: static std::string encode_url_segment(const char*pStart, const char*pEnd, bool isQuerySegment = false); + static std::string encode_url_segment(const std::string &segment, bool isQuerySegment = false) { + return encode_url_segment(segment.c_str(), segment.c_str() + segment.length(), isQuerySegment); + } static void encode_url_segment(std::ostream&os, const char*pStart, const char *pEnd, bool isQuerySegment = false); static void encode_url_segment(std::ostream&os, const std::string&segment, bool isQuerySegment = false) { diff --git a/artifacts/mod-ui.svgz b/artifacts/mod-ui.svgz new file mode 100644 index 0000000000000000000000000000000000000000..d8e198c461beea11af10decbf8b3fa97b53f26b7 GIT binary patch literal 1002 zcmVC3JZ6WiDrvPo8IS5+(Zal7vfI0+iS z5s+jizrNQtpGjubG8zq>d+xdCUSHVVe0?hmbWwGa>uT#!pLj@Bsm}81u=W1={s)dc z)U-*JC55ik)~mGlb+_33f-(BNR!OTebj{l%`m1_v(&VJjr(@fm)~nU^diC=T^mTn$ zeMT7X7K=@DIV=!@tgJn5J;purbX57_*ba}ETwQ9k5pXtmTCKIcBj-Yf$q zRW<8geCvJGsL5_lNvfJvk5BUZIzxUB_>Df-soH~t@+;M@zJ32T1TpcmHXCJT)vtlA z%LpMWa62t_IsBBl1L|hz+!)Wy=#7Az=>SR3fv4o4QeEhJ>;1a7R~LBEb*Ab*MC>&S zX&6b~zQdXNOmvaqq?)6?8tLYkWcmtwa8EG7i2`{RPmd~nRrQNbYOwx5$yD>&`^%{L zd0^=DT&k+Y=PGYu#%0+tjJ-o`2C`3zM&0M#v@T)Es2|WklIT91!Xo3)7ZHyG_Rv`h z4Z-mW$pzyqz7JpX3MPWP*M&x*{D9HFLX^;l#yZN|lc{u}_m4wm@|KtRO+oh$!s)rL z&A^J}UBO)KAL^l%U93#)n{k!Q6YE2VJDnnVQN`B#+vKB%Du-I1Pw+abu7WqJb`^IP z*_@L0=wfhY?Rx8iE2T}lshp~*flv5P$0!ayM1G=EVmqJFA z2~Z(RBO?RGLjV%Vu$aYRB=5~J8B%KwRAX)eW{NbHaETcxtQ{_%42VS$37{8jQe_;{ zk?;K8i3o5poSHkT!N-J-2PY#c?JPQ#Cu4F}B@*)*reTt?sq>R`IG`9Kawa5>z$HoHn4QOG5pIGN=*Y`#HeVOUK2 Y722#0@c%ROulsKCFJmF$Xx<3`07^3VkN^Mx literal 0 HcmV?d00001 diff --git a/artifacts/pp-ui.svgz b/artifacts/pp-ui.svgz new file mode 100644 index 0000000000000000000000000000000000000000..dfc4be1803c181660ca3fc6fe3a35befba3e19c8 GIT binary patch literal 1509 zcmVefL+e>`MZO)XwZ(l^mc63KU4+(hs0TS|Tiw z0!dr8zrM4}i)4}xf&fJ;!!UVv?lU__UVnN%9)hQ=s`Ikggb=eZ$ck;5=EZIk{{8Kb z$b>=NBt@DW$|Bo@MHzm&y;=VfMZxDPOPVYV&Uv#B{wyBqZF0(j5BsJ$t(MF4`5fmR z7?;&<`7wy1+nbwp{j|FY0@6yG{U$^ay0?9n@Al2``jltq-^=Gs$O0Cqm<#JZ?n$=i zmw>?odAbS7aC_RjU0sP`jKPOHmKb4_6tD=8Ax3}^gO6URrkqdoyGqM#ksK-2(MHLHv?U!N9k$>p*=5`EP*$7p*L(lz z4(`e-&8q&0`Oll9l1h>{FVxZ)Wp^?(s@|7pXS&`eX?dnG7z?<=D+_Gye7n!K4_S3r zCKZJ~5lOT9p^q;G_cM_ww~xoHXrjj=Z>Yw{V<+Ll+f}Y0_sOBo#(J-1d7O!POY>`I zlHF1Ypl1%wc|qAn-OT`_^g9y$Z2<<8Bs!7jnF@4AFYg~6ljr=Hzh;!_#PIg9s$6jn z$xB8x^nV}>mVSuR?7qH?jeEoXXg9KRNba)3Cj9F3gNc`2RX(1MWtw#!!i&|;6EKRS#i$a{Vv1o4aRs_nRIHPtdUsO9WYBA}&YEFH zk5#Fci-;DI)gp^pIcCvx<}_vIRk`hhLuooOrTTCMc2W}g5U#H}6%?eHI0cGL)1lIX zQ!_ZFW_CGMr>c4H7244xdDBbDHd;jNlW%<*Y7>wrKlt35!6|5xnF6_;|{9A5z%K%@EKLP#Hoy{fv>*d=?xtZSH#l`wK}Yxz)j*)|)E z?Q^~Ajtfr2j3#YR6_QD3Ptb*ll=gyGGI~&I1z$UkdykHcf`5^Y zj)8C^E@6SOrpeLRZWm>QL{Cv}t|(x-2?DgC8qp%sfbOgG6*v&g923nE(SO53oYt|R zbxj}*s2gq$v>EXl=T$7!V7S<8S0rZ&y;w~ly`b} zJ?+f0&XjYelw*H{ad1R9VoEn^$~I<>Y~&2pOqmA9MDu-~F~i~=(TtpB89qZYV~&|n z%nZYr8G`vyei_pX%(2Tnxs18x$Ean7S>}jkhF8p#R(^t2>^r2gUhZyh*6#oG+nav? Li + + + diff --git a/artifacts/red-light.svgz b/artifacts/red-light.svgz new file mode 100644 index 0000000000000000000000000000000000000000..6df12600ca033b9e080568ad8a487bdf0a08dbcb GIT binary patch literal 5542 zcmV;X636ZtGUOPh()Z387tGxaY9=^TtQYUkxH28jN zF2{HD?M+$PscR=$>V3aWv)lju_u#kxxx2gJ3(u}R_vRE;;f<8WrDNyb%}-%f<=-Ia zbUNKfkB#?P5kNoR+}-_qF!-%(g2BzrYoEyNxgm1D--12_Lob)7$gRTfw~tq$7X@MU zeAz_aiObsWx9|;ogP}K%%_W$34*q>}GbgnVV7i6p-X-T-@4?Xh#Rx3;TbbJq=*b6X_CN^Uv5wsMeh*w4n}@{dz07ZSyA2jQQ|##zQ2c> zsPdq^-X)O(MTY=-p3Gh5`}cVoTpgly^XU66LtgFdUU!0(q~*6Klkc~mA%C9GWyK}* z)c)%JC(xene`>fdv$}9R9}MB$r(Ok(mZRs+JNVwM+*iZ22^jm$98TE9JHjvwbm@Ju zzGjEQFbsiRdOfu!_@U*$e{(&*Jlj8n&-n_$NYD=4d3GTdwi|hA^KN|!FJW%JxmZVL*nRlzc>aHFv45~5{8jK;}}J7nj&HBx?_?i zFqB4^>yEKMJG2~Z5$3svXb>6RxWFFqvSBB9f z0-&R>1rZIyUp#w0hGF!Fy<|X;knkstNti~!-*vy}7yXi8C-EBj-=Uj*=qKlq-YagfYm;EayB3PT+ZT`Abrw6HH;TqTq0fhiA<*jy z<4@5i20ysI?wF#^VgbAE_fO>uio{@=_$*N|z$;4O*A@(r{<{x70tn}80_ptva0p?P zVeqf&B;bqqDpz0ih7vdfe*z6vYtj@{EK=T^uKQTuhr9+D(PETc>ed> ze_g@thp@f`P>X>1zNE|!T)yJr7Jb_bE?;y}@Hn2CMir>iYET{<=gM)Onx%JH>)F6g z{?etL8RYfS-nu|g{AJ}1sQD4%{;b;37s7Ko(24;mhBC;@TKucF{#D!J`z?M?<0SH0 z{yu@REbO!dW$@wq?ZK{!sQn4~1wOy;NgSsz1Si1-4u%B6ok4&8N5Q;g^3TCULH2qJ zCixac@-2MiTinRkfYJ9jbAJ7!>%C~s&)17ljXk|e-xBM6BjB<Ys?N%j1k6tdzOJ7C(-z=WRzA`W-9;Py%2?{NuL2vGuRo{^&ejjbxkz1tE=tC}mI* zR65=r4Ok4hhZz#60Py*qCJ+JzTYU~+U`^lz@$&`ezdpNNp0a(O-GFq$ekaa)^JGTSxA}GMZ9+cQ)$1W>euXyTu5chAo9yP~B ze?EoMGmvGNoz6p-q3vcTuz>dl=gwCnz}Nbn&~sw1*kyL%zNn>V6W1%_r}??a`Tb~d zb{-Vx1^agL4D9oLeU=@r%;+56cNn0%4~sd6I7(gVI7KN~-QDB!euO5ePb{9k13+K| z`NX8Xvcku%?(?pL-A0F~_uLnm_cHCeC_u2?C3fc(&tFiSLnU};85CKaA2Jsx%!~2u zOUPWVKMbANJ1_Zu%ZX2jKgd%zDsyndw=_#Vpp*-6l%C@Bokf(MkIAoolqBE&g{(Sim^Uu9^t_@ClQ9SPbQ|s$N-ajaK zSCl-=6rOZDad|7nnzzXPNw zO}*mgfw3<*c}WuGT>bnmpy0m@sK=RuV}`s(>Vp=21Pr~WNt%IQ+ZXLwkYevj zE6{$<2lpg{60a-UkAQ}Nj?Q12KGV%>$~n^j3Zu*?6mubg^oR8NHjE!_4pDozOT5&* zJJT32Ee>|_=@y;!?tguY&P&hjDg;vPC60eAw%^L>_gDqVW58ZTlJb$73!e3`rBOXl2&(GQiPW!!D@`LVv9C%T9=gI`|T69)d?^GW4!q@M9 zPjh}f{TPNVbD$D+yNa^&&wbKYHPx#E&b2H1+rmuSV!^%Ja!LB@iY zYM7y52H+EazGMTQwZMN|@1+HNZr~;=Z>%(lX9LM+!#KK_B`0H?^n>#N$cbe~uES|j zwDT@^B(S~;VH0Hz?!*n9gUPy>i%kznZDZ=*+HlGAz;s)psTbUN5}IL{IyYF2qcOGG zs`j{DjTh7$+B6Nejj;s{$)S*z3a{pHD`)XIN(`CjVbRP;RZ)%Bt2xe5PTTd=EOpg1 zWOEROT353NYj2fvUFXA-spNtxTeg`WtDV^TOO=2acPw`qyd!dRvyB&{fO6O|>AL80BGQ<_B{I@MrLS1~t>J3?LTb9H9}EaK;~#v2e`y zQqXmj)JbJtf+F6sh;%&J({sAMKNasVcVF>14d-V7$e7bF^RB zF2+yi4JE=&!ZqrI)Xamu-um&BKa~q;FLHzR5k^m8RLm`WugeO3!Vv<~GCr>*YazfK z%cW_g=z^t|x#1@SJsfdNSFenK3yus@?|QfmR(7#6Nn=2W>AcoCna<)>7%}UL+MOsh zu=J#?#1k;&9?c=#t)}%TT5ZBJFDZAwac3HXmMjb@7Fp2fj7QRW!B{>pkFk5a3Gis*FwPpr#&L6crk#+o-5EFkBSdF_{g~Dece0Yv4ssePEfA6lx5u?1=k206 zlGVnmi-B98W=ZWzHsITRs7^GWx5uaDObzVB2@|>KU9HPaWhX8IP7_jmh7rR}j4t}2 zFT#GTEc=B8D-x1gADr?!FFxz0qvFKy4drWw?;@gv= z39PN3_M=!8FoP7l&3>bm6gM9a3uK);%67BkcCnzxTQO@k$iQ97!Um#iVl6ZV*sQ%Y zX?@ceLU^P?YDJoGE=dNCiUB5yvx?y`N$zLVJP$@QPbm?%Yo=J5)%;?Q4oIDx9M}k( z@DAh6m(my!Oxi-#z1wdbl^ca>x?L}~Ei>PX4C2+uq;?vN4We>H>1oQdH!V)Zy+k=o<;m1!NmH|C(o7r!uWN$@{8d<{$qTaUj|+Me*`g)cJP1$1s}RSZa9O zCdVVQy3o6jjF+)dYX_bmyvfCvbOI#g>jw`n*}8ju@kzOVPGXk?_=yU6lg@tdNZ_28 z|38C1W>A0roI%^IKiwnkWbD|GGt3D!|_3o+X^pVaoF1D8C2sj}>S%^0M9gN8W;lC@Y<_MDgl5N5 zt}Eh+ie{aXWP&^b!c(H=cz0M^$rM>^BGFa_Bbbh)U2Pu5a^#$*?B4NZJ`{q1E`g)9 zQ@23^K16GCyF4xT+C~sHu9fGcqe~}VHv~N>r}iORVtj?~I=-VzAG|P6w%G6>WnQjIkN-)kW@Qc0R*;Lma6!rmV3_c7Zfi zXrh#j*+!mZdbaikJ9>*d{3d0Y7+xk#fgX1`e<-Or>);g=#XLIp^tG^JHIa4Z2SZmK znVF*eOwU4FHWJdBMCpzs9Jv_}WCJ10@otUCU5Sh(Jor3|v-Jo@lxlez5sTLD)j38D(ZRM($wN6N zZIkC`b7(ZINO+Ag)U2R}emU~}KDCEac;WleCh^8biYhgUMz$PHM1D^9sZbYImwIc- z@CT;r@V>!>g(*p5yiAsBj@l1{?a|xVJBq=XNiim!@Me4=Dc)g{G23|nS3GR`_j@(WRw<+ymn#Zp4vVI&W+Ea-8k&>?JA;wqic@%V zCd6$l?j>ogM2T+TW1dgDY2-pjOc)xKwNuP^p0BJWp-7ltv!dwWhDowvpz?|yOW#8{$5|NbwYF0)kCkw=4lKBZe?f5(`bH(6i-OA$0RXbyCL|Tn=1D(yr ze5{#3K59WOC8{vh;Nx!1n*ky5ad2n{ssL0(M(0Pz%~EY^jliNlZ|-Ck1i26OGfhnrUjf&ZRR_){c!$O2Anas+p zWust}hV+87wnJevs~78HZG!xmOTKjq=dPcJi$f9mSh34Of(qt#6Z@&x7|cK)2DBAA z8elPOQWAv969Jl5GY@Ad#olewqQIAl<~U4AhEtvu#A!IuMgbBb63aFzoSO1dpNFoi z^8;^I#HlhRx=w?dFr_Jzv0W0i6(B~0$()}WV0RU5=8lK}o9Osl#`f@T$#En#A#8;n zdPjmL7%^Y9?SP($BVE^`Z7~z9RwD(>uUB$;WxB9Bp1 zL5H2w^qJ%zI2J5!wDEdf9fXibw?0{(tVKIs4849a@mz_JvlJQuimO1A;@lYIgSlu3r@=5J3`MuePS9j+9j({3TOd_OsOuU7IJ>K= zK~%AH+6Is->VQNA&74iM##JOgoOVjHfC>`vz17qj4$3(Yp)RP$IYv>nJ(mHhntf+9n1)5@ptDm{)JHQMo1^uj z?kRuKK+Q4=Ehw=@!8&AS4Luhoqv1H;AGhrWSj8NXL0VnYMu-a|SdS0xDYAK{HE6a0Jde{`Z;Oi?!g0L=H|4FCWD literal 0 HcmV?d00001 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7055495..787c8b5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -206,6 +206,9 @@ else() endif() set (PIPEDAL_SOURCES + ModTemplateGenerator.cpp ModTemplateGenerator.hpp + WebServerMod.cpp WebServerMod.hpp + ModGui.cpp ModGui.hpp PipewireInputStream.cpp PipewireInputStream.hpp AudioFiles.cpp AudioFiles.hpp AudioFileMetadataReader.cpp AudioFileMetadataReader.hpp @@ -414,6 +417,8 @@ add_executable(AuxInTest add_executable(pipedaltest testMain.cpp + + ModGuiTest.cpp PipewireInputStreamTest.cpp AudioFilesTest.cpp diff --git a/src/Lv2HostLeakTest.cpp b/src/Lv2HostLeakTest.cpp index d5738b6..a36a5a6 100644 --- a/src/Lv2HostLeakTest.cpp +++ b/src/Lv2HostLeakTest.cpp @@ -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(); diff --git a/src/MimeTypes.cpp b/src/MimeTypes.cpp index e98b804..8940182 100644 --- a/src/MimeTypes.cpp +++ b/src/MimeTypes.cpp @@ -22,6 +22,7 @@ #include "ss.hpp" #include #include +#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; } diff --git a/src/ModFileTypes.cpp b/src/ModFileTypes.cpp index 05a24cb..5d94d55 100644 --- a/src/ModFileTypes.cpp +++ b/src/ModFileTypes.cpp @@ -95,6 +95,7 @@ ModFileTypes::ModFileTypes(const std::string &fileTypes) if (wellKnownType) { rootDirectories_.push_back(type); + modFileTypes_.push_back(type); } else { diff --git a/src/ModFileTypes.hpp b/src/ModFileTypes.hpp index a0f5673..e6065fd 100644 --- a/src/ModFileTypes.hpp +++ b/src/ModFileTypes.hpp @@ -32,12 +32,14 @@ namespace pipedal ModFileTypes(const std::string &fileTypes); const std::vector &rootDirectories() const { return rootDirectories_; } std::vector &rootDirectories() { return rootDirectories_; } + const std::vector &modFileTypes() const { return modFileTypes_; } const std::vector &fileTypes() const { return fileTypes_; } static constexpr const char *DEFAULT_FILE_TYPES = "audio,nammodel,mlmodel,sf2,sfz,midisong,midiclip,*"; // (everything) private: std::vector rootDirectories_; + std::vector modFileTypes_; // e.g. "audio", "nammodel", etc. std::vector fileTypes_; public: diff --git a/src/ModGui.cpp b/src/ModGui.cpp new file mode 100644 index 0000000..1ed55ed --- /dev/null +++ b/src/ModGui.cpp @@ -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(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 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 +static inline json_variant MakeJsonArray(const std::vector &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 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() + diff --git a/src/ModGui.hpp b/src/ModGui.hpp new file mode 100644 index 0000000..bfa4f06 --- /dev/null +++ b/src/ModGui.hpp @@ -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 +#include +#include +#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; + 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 ports_; + + std::vector 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 &ports() const { return ports_; } + std::vector &ports() { return ports_; } + void ports(const std::vector &value) { ports_ = value; } + + + const std::vector &monitoredOutputs() const { return monitoredOutputs_; } + std::vector &monitoredOutputs() { return monitoredOutputs_; } + void monitoredOutputs(const std::vector &value) { monitoredOutputs_ = value; } + + DECLARE_JSON_MAP(ModGui); + + }; + + json_variant MakeModGuiTemplateData( + std::shared_ptr pluginInfo + ); + +} \ No newline at end of file diff --git a/src/ModGuiTest.cpp b/src/ModGuiTest.cpp new file mode 100644 index 0000000..8ad0c0f --- /dev/null +++ b/src/ModGuiTest.cpp @@ -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 +#include +#include +#include +#include +#include +#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"( +
+

{{name}}

+

URI: {{uri}}

+

Version: {{version}}

+

Author: {{author}}

+
    + {{#items}} +
  • {{label}}
  • + {{/items}} +
+

{{inputs.input1}}

+

{{inputs.input2}}

+

{{inputs.input3}}

+ Help Icon +
+ )"; + auto result = GenerateFromTemplateString(templateString, data); + + cout << result; + cout << endl; +} + + diff --git a/src/ModTemplateGenerator.cpp b/src/ModTemplateGenerator.cpp new file mode 100644 index 0000000..e259b19 --- /dev/null +++ b/src/ModTemplateGenerator.cpp @@ -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 +#include +#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 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(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(file)), + std::istreambuf_iterator()); + file.close(); + + return GenerateFromTemplateString( + templateContent, + data); + } + +} \ No newline at end of file diff --git a/src/ModTemplateGenerator.hpp b/src/ModTemplateGenerator.hpp new file mode 100644 index 0000000..2c7f463 --- /dev/null +++ b/src/ModTemplateGenerator.hpp @@ -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 +#include +#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 + ); +} \ No newline at end of file diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index 91fe6b8..d3b300a 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -242,7 +242,7 @@ void PiPedalModel::LoadLv2PluginInfo() } pluginChangeMonitor = std::make_unique(*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 diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index d665545..3218b63 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -331,7 +331,9 @@ namespace pipedal void LoadLv2PluginInfo(); void Load(); - const PluginHost &GetLv2Host() const { return pluginHost; } + const PluginHost &GetPluginHost() const { return pluginHost; } + PluginHost &GetPluginHost() { return pluginHost; } + Pedalboard GetCurrentPedalboardCopy() { std::lock_guard guard(mutex); diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index 4740747..32c5ea2 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -1331,12 +1331,12 @@ public: } else if (message == "plugins") { - auto ui_plugins = model.GetLv2Host().GetUiPlugins(); + auto ui_plugins = model.GetPluginHost().GetUiPlugins(); Reply(replyTo, "plugins", ui_plugins); } else if (message == "pluginClasses") { - auto classes = model.GetLv2Host().GetLv2PluginClass(); + auto classes = model.GetPluginHost().GetLv2PluginClass(); Reply(replyTo, "pluginClasses", classes); } else if (message == "hello") diff --git a/src/PipewireInputStreamTest.cpp b/src/PipewireInputStreamTest.cpp index 09cb03c..fb59bf9 100644 --- a/src/PipewireInputStreamTest.cpp +++ b/src/PipewireInputStreamTest.cpp @@ -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 diff --git a/src/PluginHost.cpp b/src/PluginHost.cpp index e6355cf..1f4d2a1 100644 --- a/src/PluginHost.cpp +++ b/src/PluginHost.cpp @@ -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(lilv_node_as_float(node)); + } + break; + } + return 0; +} static std::string nodeAsString(const LilvNode *node) { @@ -270,7 +297,6 @@ PluginHost::PluginHost() fileMetadataFeature.Prepare(mapFeature); lv2Features.push_back(fileMetadataFeature.GetFeature()); - lv2Features.push_back(nullptr); this->urids = new Urids(mapFeature); @@ -294,8 +320,11 @@ PluginHost::~PluginHost() { delete lilvUris; lilvUris = nullptr; + delete urids; urids = nullptr; + delete mod_gui_uris; + mod_gui_uris = nullptr; free_world(); } @@ -397,7 +426,7 @@ void PluginHost::LoadPluginClassesFromLilv() } } -void PluginHost::Load(const char *lv2Path) +void PluginHost::LoadLilv(const char *lv2Path) { this->plugins_.clear(); @@ -418,6 +447,9 @@ void PluginHost::Load(const char *lv2Path) lilv_world_load_all(pWorld); } + // Not a safe pointer,because auto-deleting after freeWorld() would be death. + this->mod_gui_uris = new ModGuiUris(pWorld, mapFeature); + // LilvNode*lv2_path = lilv_new_file_uri(pWorld,NULL,lv2Path); // lilv_world_set_option(world,LILV_OPTION_LV2_PATH,lv) @@ -439,7 +471,8 @@ void PluginHost::Load(const char *lv2Path) if (pluginInfo->hasCvPorts()) { Lv2Log::debug("Plugin %s (%s) skipped. (Has CV ports).", pluginInfo->name().c_str(), pluginInfo->uri().c_str()); - } else if (pluginInfo->hasUnsupportedPatchProperties()) + } + else if (pluginInfo->hasUnsupportedPatchProperties()) { Lv2Log::debug("Plugin %s (%s) skipped. (Has unsupported patch parameters).", pluginInfo->name().c_str(), pluginInfo->uri().c_str()); } @@ -497,20 +530,20 @@ void PluginHost::Load(const char *lv2Path) if (plugin->is_valid()) { #if 1 - // no plugins with more than 2 inputs or outputs. - // no zero-input or zero-output plugins (temporarily disables midi plugins) - // no zero output devices (permanent, I think) - if (info.audio_inputs() > 2 || info.audio_outputs() > 2) { + // no plugins with more than 2 inputs or outputs. + // no zero-input or zero-output plugins (temporarily disables midi plugins) + // no zero output devices (permanent, I think) + if (info.audio_inputs() > 2 || info.audio_outputs() > 2) + { Lv2Log::debug( "Plugin %s (%s) skipped. %d inputs, %d outputs.", plugin->name().c_str(), plugin->uri().c_str(), - (int)info.audio_inputs(),(int)info.audio_outputs()); - + (int)info.audio_inputs(), (int)info.audio_outputs()); } - else if (info.audio_inputs() == 0 && info.audio_outputs() == 0 ) + else if (info.audio_inputs() == 0 && info.audio_outputs() == 0) { Lv2Log::debug("Plugin %s (%s) skipped. No audio i/o.", plugin->name().c_str(), plugin->uri().c_str()); - - } else if (info.audio_inputs() == 0) + } + else if (info.audio_inputs() == 0) { // temporarily disable this feature. Lv2Log::debug("Plugin %s (%s) skipped. No inputs.", plugin->name().c_str(), plugin->uri().c_str()); @@ -536,7 +569,8 @@ void PluginHost::Load(const char *lv2Path) #endif else { - if (info.audio_inputs() == 0) { + if (info.audio_inputs() == 0) + { Lv2Log::debug("************* ZERO INPUTS: %s (%s) skipped. No audio outputs.", plugin->name().c_str(), plugin->uri().c_str()); } ui_plugins_.push_back(std::move(info)); @@ -619,7 +653,7 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin // example: // -// a lv2:Parameter; + // a lv2:Parameter; // rdfs:label "Model"; // rdfs:range atom:Path. // ... @@ -644,7 +678,6 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin if (lilv_world_ask(pWorld, propertyUri, lv2Host->lilvUris->rdfs__range, lv2Host->lilvUris->atom__Path)) { - AutoLilvNode label = lilv_world_get(pWorld, propertyUri, lv2Host->lilvUris->rdfs__label, nullptr); std::string strLabel = label.AsString(); @@ -662,19 +695,16 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin std::make_shared( strLabel, propertyUri.AsUri(), lv2DirectoryName); - AutoLilvNode indexNode = lilv_world_get(pWorld, propertyUri, lv2Host->lilvUris->lv2core__index, nullptr); int32_t index = indexNode.AsInt(-1); fileProperty->index(index); - // if there's a pipedalui_fileTypes node, use that instead. - AutoLilvNode mod__fileTypes = lilv_world_get(pWorld, propertyUri, lv2Host->lilvUris->mod__fileTypes, nullptr); // default: everything. - std::string fileTypes = ModFileTypes::DEFAULT_FILE_TYPES; + std::string fileTypes = ModFileTypes::DEFAULT_FILE_TYPES; if (mod__fileTypes) { // "nam,nammodel" @@ -682,8 +712,6 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin } ModFileTypes modFileTypes(fileTypes); - - // Legacy case: audio_uploads/ exists. std::filesystem::path bundleDirectoryName = std::filesystem::path(bundle_path()).parent_path().filename(); @@ -693,13 +721,14 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin fileProperty->directory(bundleDirectoryName); - if (std::filesystem::exists(legacyUploadPath) - && !std::filesystem::exists(legacyUploadPath / ".migrated")) + if (std::filesystem::exists(legacyUploadPath) && !std::filesystem::exists(legacyUploadPath / ".migrated")) { fileProperty->useLegacyModDirectory(true); fileProperty->directory(bundleDirectoryName); modFileTypes.rootDirectories().push_back(bundleDirectoryName.filename()); // push the private directory! - } else { + } + else + { if (modFileTypes.rootDirectories().size() == 1) { std::string modName = modFileTypes.rootDirectories()[0]; @@ -708,9 +737,10 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin } } - fileProperties.push_back(fileProperty); - } else { + } + else + { unsupportedPatchProperty = true; } } @@ -718,24 +748,25 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin } FindWritablePathPropertiesResult result; - - if (fileProperties.size() != 0) { - std::sort(fileProperties.begin(),fileProperties.end(),[](const UiFileProperty::ptr& left,const UiFileProperty::ptr&right) { - // properies with indexes first. - int32_t indexL = left->index(); - if (indexL < 0) indexL = std::numeric_limits::max(); - int32_t indexR = right->index(); - if (indexR < 0) indexR = std::numeric_limits::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::max(); + int32_t indexR = right->index(); + if (indexR < 0) + indexR = std::numeric_limits::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(std::move(fileProperties)); } result.hasUnsupportedPatchProperties = unsupportedPatchProperty; @@ -764,6 +795,12 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP AutoLilvNode name = (lilv_plugin_get_name(pPlugin)); this->name_ = nodeAsString(name); + AutoLilvNodes minorVersion = lilv_plugin_get_value(pPlugin, lv2Host->lilvUris->lv2core__minorVersion); + this->minorVersion_ = nodesAsInt(minorVersion); + + AutoLilvNodes microVersion = lilv_plugin_get_value(pPlugin, lv2Host->lilvUris->lv2core__microVersion); + this->microVersion_ = nodesAsInt(microVersion); + AutoLilvNode brand = lilv_world_get(pWorld, plugUri, lv2Host->lilvUris->mod__brand, nullptr); this->brand_ = nodeAsString(brand); @@ -838,6 +875,59 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP } std::sort(ports_.begin(), ports_.end(), ports_sort_compare); + // Fetch patch properties. + { + AutoLilvNodes patchWritables = lilv_world_find_nodes(pWorld, plugUri, lv2Host->lilvUris->patch__writable, nullptr); + + bool unsupportedPatchProperty = false; + LILV_FOREACH(nodes, iNode, patchWritables) + { + AutoLilvNode propertyUri = lilv_nodes_get(patchWritables, iNode); + if (propertyUri) + { + // a lv2:Parameter? + if (lilv_world_ask(pWorld, propertyUri, lv2Host->lilvUris->isA, lv2Host->lilvUris->lv2core__Parameter)) + { + Lv2PatchPropertyInfo propertyInfo{lv2Host, propertyUri}; + propertyInfo.writable(true); + patchProperties_.push_back(std::move(propertyInfo)); + } + } + } + } + { + AutoLilvNodes patchReadables = lilv_world_find_nodes(pWorld, plugUri, lv2Host->lilvUris->patch__readable, nullptr); + + bool unsupportedPatchProperty = false; + LILV_FOREACH(nodes, iNode, patchReadables) + { + AutoLilvNode propertyUri = lilv_nodes_get(patchReadables, iNode); + if (propertyUri) + { + std::string uri = propertyUri.AsUri(); + bool found = false; + for (auto &property : patchProperties_) + { + if (property.uri() == uri) + { + property.readable(true); + found = true; + break; + } + } + if (!found) + { + if (lilv_world_ask(pWorld, propertyUri, lv2Host->lilvUris->isA, lv2Host->lilvUris->lv2core__Parameter)) + { + Lv2PatchPropertyInfo propertyInfo{lv2Host, propertyUri}; + propertyInfo.readable(true); + patchProperties_.push_back(std::move(propertyInfo)); + } + } + } + } + + } // Fetch pipedal plugin UI @@ -886,7 +976,7 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP ++nOutputs; } } - + this->modGui_ = ModGui::Create(lv2Host, pPlugin); this->is_valid_ = isValid; } @@ -1012,22 +1102,21 @@ Lv2PortInfo::Lv2PortInfo(PluginHost *host, const LilvPlugin *plugin, const LilvP this->integer_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->integer_property_uri); this->mod_momentaryOffByDefault_ = lilv_port_has_property(plugin, pPort, host->lilvUris->mod__preferMomentaryOffByDefault); this->mod_momentaryOnByDefault_ = lilv_port_has_property(plugin, pPort, host->lilvUris->mod__preferMomentaryOnByDefault); - this->pipedal_graphicEq_ = lilv_port_has_property(plugin, pPort, host->lilvUris->pipedalUI__graphicEq); - + this->pipedal_graphicEq_ = lilv_port_has_property(plugin, pPort, host->lilvUris->pipedalUI__graphicEq); this->enumeration_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->enumeration_property_uri); - this->toggled_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->core__toggled); this->not_on_gui_ = lilv_port_has_property(plugin, pPort, host->lilvUris->portprops__not_on_gui_property_uri); this->connection_optional_ = lilv_port_has_property(plugin, pPort, host->lilvUris->core__connectionOptional); this->trigger_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->portprops__trigger); - AutoLilvNode port_ledColor = lilv_port_get(plugin,pPort,host->lilvUris->pipedalUI__ledColor); + AutoLilvNode port_ledColor = lilv_port_get(plugin, pPort, host->lilvUris->pipedalUI__ledColor); if (port_ledColor) { auto value = lilv_node_as_string(port_ledColor); - if (value) { + if (value) + { this->pipedal_ledColor_ = value; } } @@ -1164,6 +1253,8 @@ bool PluginHost::is_a(const std::string &class_, const std::string &target_class Lv2PluginUiInfo::Lv2PluginUiInfo(PluginHost *pHost, const Lv2PluginInfo *plugin) : uri_(plugin->uri()), name_(plugin->name()), + minorVersion_(plugin->minorVersion()), + microVersion_(plugin->microVersion()), brand_(plugin->brand()), label_(plugin->label()), author_name_(plugin->author_name()), @@ -1173,7 +1264,9 @@ Lv2PluginUiInfo::Lv2PluginUiInfo(PluginHost *pHost, const Lv2PluginInfo *plugin) audio_outputs_(0), description_(plugin->comment()), has_midi_input_(false), - has_midi_output_(false) + has_midi_output_(false), + modGui_(plugin->modGui()), + patchProperties_(plugin->patchProperties()) { PLUGIN_MAP_CHECK(); auto pluginClass = pHost->GetPluginClass(plugin->plugin_class()); @@ -1253,7 +1346,7 @@ std::shared_ptr PluginHost::GetPluginInfo(const std::string &uri) return ff->second; } -Lv2Pedalboard *PluginHost::UpdateLv2PedalboardStructure(Pedalboard &pedalboard,Lv2Pedalboard *existingPedalboard,Lv2PedalboardErrorList &errorList) +Lv2Pedalboard *PluginHost::UpdateLv2PedalboardStructure(Pedalboard &pedalboard, Lv2Pedalboard *existingPedalboard, Lv2PedalboardErrorList &errorList) { ExistingEffectMap existingEffects; @@ -1261,7 +1354,8 @@ Lv2Pedalboard *PluginHost::UpdateLv2PedalboardStructure(Pedalboard &pedalboard,L { for (auto &effect : existingPedalboard->GetSharedEffectList()) { - if (effect->IsLv2Effect()) { + if (effect->IsLv2Effect()) + { existingEffects[effect->GetInstanceId()] = effect; } } @@ -1269,7 +1363,7 @@ Lv2Pedalboard *PluginHost::UpdateLv2PedalboardStructure(Pedalboard &pedalboard,L Lv2Pedalboard *pPedalboard = new Lv2Pedalboard(); try { - pPedalboard->Prepare(this, pedalboard, errorList,&existingEffects); + pPedalboard->Prepare(this, pedalboard, errorList, &existingEffects); return pPedalboard; } catch (const std::exception &e) @@ -1589,17 +1683,16 @@ static void createTargetLinks(const std::filesystem::path &sourceDirectory, cons } } -std::string PluginHost::MapResourcePath(const std::string&pluginUri, const std::string&filePath) +std::string PluginHost::MapResourcePath(const std::string &pluginUri, const std::string &filePath) { auto plugin = GetPluginInfo(pluginUri); - if (plugin) { - + if (plugin) + { + return filePath; } - return filePath; - } void PluginHost::CheckForResourceInitialization(const std::string &pluginUri, const std::filesystem::path &pluginUploadDirectory) @@ -1679,6 +1772,43 @@ std::string PluginHost::AbstractPath(const std::string &path) } return path; } +Lv2PatchPropertyInfo::Lv2PatchPropertyInfo(PluginHost *pluginHost, const LilvNode *propertyUri) +{ + LilvWorld *pWorld = pluginHost->getWorld(); + this->uri_ = nodeAsString(propertyUri); + + AutoLilvNode range = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->rdfs__range, nullptr); + if (range) + { + this->type_ = range.AsUri(); + } + AutoLilvNode comment = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->rdfs__Comment, nullptr); + if (comment) + { + this->comment_ = comment.AsString(); + } + AutoLilvNode shortName = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->lv2core__shortName, nullptr); + if (shortName) + { + this->shortName_ = shortName.AsString(); + } else { + this->shortName_ = this->label_; + } + + AutoLilvNode indexNode = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->lv2core__index, nullptr); + this->index_ = indexNode.AsInt(-1); + + AutoLilvNode modFileTypesNode = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->mod__fileTypes, nullptr); + if (modFileTypesNode) { + std::string strFileTypes = modFileTypesNode.AsString(); + this->fileTypes_ = split(strFileTypes,','); + } + AutoLilvNode modSupportedExtensionsNode = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->mod__supportedExtensions, nullptr); + if (modSupportedExtensionsNode) { + std::string strSupportedExtensions = modSupportedExtensionsNode.AsString(); + this->supportedExtensions_ = split(strSupportedExtensions,','); + } +} // void PiPedalHostLogError(const std::string &error) // { @@ -1757,6 +1887,8 @@ json_map::storage_type Lv2PortGroup::jmap{{ json_map::storage_type Lv2PluginInfo::jmap{{ json_map::reference("bundle_path", &Lv2PluginInfo::bundle_path_), json_map::reference("uri", &Lv2PluginInfo::uri_), + json_map::reference("minorVersion", &Lv2PluginInfo::minorVersion_), + json_map::reference("microVersion", &Lv2PluginInfo::microVersion_), json_map::reference("name", &Lv2PluginInfo::name_), json_map::reference("brand", &Lv2PluginInfo::brand_), json_map::reference("label", &Lv2PluginInfo::label_), @@ -1774,6 +1906,8 @@ json_map::storage_type Lv2PluginInfo::jmap{{ json_map::reference("port_groups", &Lv2PluginInfo::port_groups_), json_map::reference("is_valid", &Lv2PluginInfo::is_valid_), json_map::reference("has_factory_presets", &Lv2PluginInfo::has_factory_presets_), + json_map::reference("modGui", &Lv2PluginInfo::modGui_), + json_map::reference("patchProperties", &Lv2PluginInfo::patchProperties_), }}; json_map::storage_type Lv2PluginClass::jmap{{ @@ -1831,6 +1965,8 @@ json_map::storage_type { json_map::reference("uri", &Lv2PluginUiInfo::uri_), json_map::reference("name", &Lv2PluginUiInfo::name_), + json_map::reference("minorVersion", &Lv2PluginUiInfo::minorVersion_), + json_map::reference("microVersion", &Lv2PluginUiInfo::microVersion_), json_map::reference("brand", &Lv2PluginUiInfo::brand_), json_map::reference("label", &Lv2PluginUiInfo::label_), json_map::enum_reference("plugin_type", &Lv2PluginUiInfo::plugin_type_, get_plugin_type_enum_converter()), @@ -1848,4 +1984,16 @@ json_map::storage_type 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() \ No newline at end of file diff --git a/src/PluginHost.hpp b/src/PluginHost.hpp index cafa183..6e2b995 100644 --- a/src/PluginHost.hpp +++ b/src/PluginHost.hpp @@ -32,6 +32,7 @@ #include #include "IHost.hpp" #include +#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 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 fileTypes_; + std::vector supportedExtensions_; + public: + Lv2PatchPropertyInfo() {} + Lv2PatchPropertyInfo(PluginHost *pluginHost, const LilvNode *propertyUri); + + LV2_PROPERTY_GETSET(uri); + LV2_PROPERTY_GETSET(writable); + LV2_PROPERTY_GETSET(readable); + LV2_PROPERTY_GETSET(label); + LV2_PROPERTY_GETSET_SCALAR(index); + LV2_PROPERTY_GETSET(type); + LV2_PROPERTY_GETSET(comment); + LV2_PROPERTY_GETSET(shortName); + LV2_PROPERTY_GETSET(fileTypes); + LV2_PROPERTY_GETSET(supportedExtensions); + + DECLARE_JSON_MAP(Lv2PatchPropertyInfo); + + }; + class Lv2PortGroup { private: @@ -364,6 +398,8 @@ namespace pipedal bool HasFactoryPresets(PluginHost *lv2Host, const LilvPlugin *plugin); std::string bundle_path_; std::string uri_; + uint32_t minorVersion_ = 0; + uint32_t microVersion_ = 0; std::string name_; std::string plugin_class_; std::string brand_; @@ -380,8 +416,12 @@ namespace pipedal std::string comment_; std::vector> ports_; std::vector> port_groups_; + std::vector patchProperties_; + bool is_valid_ = false; PiPedalUI::ptr piPedalUI_; + ModGui::ptr modGui_; + bool hasUnsupportedPatchProperties_ = false; bool IsSupportedFeature(const std::string &feature) const; @@ -390,6 +430,8 @@ namespace pipedal LV2_PROPERTY_GETSET(bundle_path) LV2_PROPERTY_GETSET(uri) LV2_PROPERTY_GETSET(name) + LV2_PROPERTY_GETSET(minorVersion) + LV2_PROPERTY_GETSET(microVersion) LV2_PROPERTY_GETSET(brand) LV2_PROPERTY_GETSET(label) LV2_PROPERTY_GETSET(plugin_class) @@ -406,6 +448,8 @@ namespace pipedal LV2_PROPERTY_GETSET(has_factory_presets) LV2_PROPERTY_GETSET(piPedalUI) LV2_PROPERTY_GETSET(hasUnsupportedPatchProperties) + LV2_PROPERTY_GETSET(modGui) + LV2_PROPERTY_GETSET(patchProperties) const Lv2PortInfo &getPort(const std::string &symbol) { @@ -618,6 +662,8 @@ namespace pipedal private: std::string uri_; std::string name_; + uint32_t minorVersion_ = 0; + uint32_t microVersion_ = 0;; std::string brand_; std::string label_; std::string author_name_; @@ -636,10 +682,15 @@ namespace pipedal std::vector fileProperties_; std::vector frequencyPlots_; std::vector uiPortNotifications_; + ModGui::ptr modGui_; + std::vector patchProperties_; + public: LV2_PROPERTY_GETSET(uri) LV2_PROPERTY_GETSET(name) + LV2_PROPERTY_GETSET(minorVersion) + LV2_PROPERTY_GETSET(microVersion) LV2_PROPERTY_GETSET(brand) LV2_PROPERTY_GETSET(label) LV2_PROPERTY_GETSET(author_name) @@ -657,6 +708,8 @@ namespace pipedal LV2_PROPERTY_GETSET(fileProperties) LV2_PROPERTY_GETSET(frequencyPlots) LV2_PROPERTY_GETSET(uiPortNotifications) + LV2_PROPERTY_GETSET(modGui) + LV2_PROPERTY_GETSET(patchProperties) static json_map::storage_type jmap; }; @@ -679,6 +732,8 @@ namespace pipedal #endif friend class pipedal::AutoLilvNode; friend class pipedal::PiPedalUI; + friend class PluginHostTest; + static const char *RDFS__comment; static const char *RDFS__range; @@ -707,6 +762,7 @@ namespace pipedal AutoLilvNode invada_units__unit; // typo in invada plugins. AutoLilvNode invada_portprops__logarithmic; // typo in invada plugins. + AutoLilvNode atom__bufferType; AutoLilvNode atom__Path; AutoLilvNode presets__preset; @@ -714,8 +770,11 @@ namespace pipedal AutoLilvNode rdfs__label; AutoLilvNode lv2core__symbol; AutoLilvNode lv2core__name; + AutoLilvNode lv2core__shortName; AutoLilvNode lv2core__index; AutoLilvNode lv2core__Parameter; + AutoLilvNode lv2core__minorVersion; + AutoLilvNode lv2core__microVersion; AutoLilvNode pipedalUI__ui; AutoLilvNode pipedalUI__fileProperties; AutoLilvNode pipedalUI__directory; @@ -766,6 +825,7 @@ namespace pipedal AutoLilvNode patch__readable; AutoLilvNode pipedal_patch__readable; + AutoLilvNode mod__brand; AutoLilvNode mod__label; AutoLilvNode mod__preferMomentaryOffByDefault; @@ -773,6 +833,7 @@ namespace pipedal AutoLilvNode dc__format; AutoLilvNode mod__fileTypes; + AutoLilvNode mod__supportedExtensions; AutoLilvNode pipedalui__fileTypes; @@ -817,6 +878,7 @@ namespace pipedal friend class Lv2PluginInfo; friend class Lv2PortInfo; friend class Lv2PortGroup; + friend class ModGui; std::shared_ptr GetPluginClass(const LilvPluginClass *pClass); std::shared_ptr MakePluginClass(const LilvPluginClass *pClass); @@ -924,7 +986,9 @@ namespace pipedal class Urids; - Urids *urids; + Urids *urids = nullptr; + ModGuiUris* mod_gui_uris = nullptr; + void OnConfigurationChanged(const JackConfiguration &configuration, const JackChannelSelection &settings); @@ -933,7 +997,7 @@ namespace pipedal std::shared_ptr GetLv2PluginClass() const; - std::vector> GetPlugins() const { return plugins_; } + const std::vector>& GetPlugins() const { return plugins_; } const std::vector &GetUiPlugins() const { return ui_plugins_; } virtual std::shared_ptr GetPluginInfo(const std::string &uri) const; @@ -941,7 +1005,7 @@ namespace pipedal static constexpr const char *DEFAULT_LV2_PATH = "/usr/lib/lv2"; void LoadPluginClassesFromJson(std::filesystem::path jsonFile); - void Load(const char *lv2Path = PluginHost::DEFAULT_LV2_PATH); + void LoadLilv(const char *lv2Path = PluginHost::DEFAULT_LV2_PATH); virtual LV2_URID GetLv2Urid(const char *uri) { diff --git a/src/Units.hpp b/src/Units.hpp index 3ba14f0..604a1f5 100644 --- a/src/Units.hpp +++ b/src/Units.hpp @@ -68,3 +68,4 @@ json_enum_converter *get_units_enum_converter(); } // namespace. + diff --git a/src/WebServer.cpp b/src/WebServer.cpp index 946088f..e159704 100644 --- a/src/WebServer.cpp +++ b/src/WebServer.cpp @@ -37,6 +37,7 @@ #include "Ipv6Helpers.hpp" #include "util.hpp" #include "ofstream_synced.hpp" +#include "ModTemplateGenerator.hpp" #include #include "WebServer.hpp" @@ -741,6 +742,14 @@ namespace pipedal request.set_body_file(bodyFile->Path(),bodyFile->DeleteFile()); bodyFile->Detach(); } + + + virtual void clearBody() override { + // clear the body, but leave the file size intact (e.g for a HEAD request). + request.set_body(""); + + // STUB: Don't know how to clear the body file!! + } virtual void setBodyFile(std::filesystem::path&path, bool deleteWhenDone) override { // cast away const to do what ther request would do if it had that method. request.set_body_file(path,deleteWhenDone); diff --git a/src/WebServer.hpp b/src/WebServer.hpp index e121cc2..1d9bd03 100644 --- a/src/WebServer.hpp +++ b/src/WebServer.hpp @@ -43,6 +43,7 @@ public: virtual void setBody(const std::string&body) = 0; virtual void setBodyFile(std::shared_ptr&temporaryFile) = 0; virtual void setBodyFile(std::filesystem::path&path, bool deleteWhenDone) = 0; + virtual void clearBody() = 0; // but leave the file size intact (e.g for a HEAD request). virtual void keepAlive(bool value) = 0; }; diff --git a/src/WebServerConfig.cpp b/src/WebServerConfig.cpp index ae23dc9..abda288 100644 --- a/src/WebServerConfig.cpp +++ b/src/WebServerConfig.cpp @@ -39,6 +39,7 @@ #include "AudioFiles.hpp" #include "util.hpp" #include "HtmlHelper.hpp" +#include "WebServerMod.hpp" #define OLD_PRESET_EXTENSION ".piPreset" #define PRESET_EXTENSION ".piPreset" @@ -249,7 +250,7 @@ public: void GetPluginPresets(const uri &request_uri, std::string *pName, std::string *pContent) { std::string pluginUri = request_uri.query("id"); - auto plugin = model->GetLv2Host().GetPluginInfo(pluginUri); + auto plugin = model->GetPluginHost().GetPluginInfo(pluginUri); *pName = plugin->name(); PluginPresets pluginPresets = model->GetPluginPresets(pluginUri); @@ -1102,4 +1103,7 @@ void pipedal::ConfigureWebServer( std::shared_ptr downloadIntercept = std::make_shared(&model); server.AddRequestHandler(downloadIntercept); + + std::shared_ptr modWebIntercept = ModWebIntercept::Create(&model); + server.AddRequestHandler(modWebIntercept); } diff --git a/src/WebServerMod.cpp b/src/WebServerMod.cpp new file mode 100644 index 0000000..e557ac8 --- /dev/null +++ b/src/WebServerMod.cpp @@ -0,0 +1,308 @@ +/* + * Copyright (c) 2025 Robin E. R. Davies + * All rights reserved. + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "WebServerMod.hpp" +#include +#include "json_variant.hpp" +#include "ss.hpp" +#include "ModTemplateGenerator.hpp" +#include "HtmlHelper.hpp" +#include "MimeTypes.hpp" + +using namespace pipedal; +namespace fs = std::filesystem; + +namespace pipedal::impl +{ + + class ModWebInterceptImpl : public ModWebIntercept + { + private: + using super = ModWebIntercept; + using self = ModWebInterceptImpl; + + PiPedalModel *model; + + public: + ModWebInterceptImpl(PiPedalModel *model) + : super("/resources"), + model(model) + { + } + virtual bool wants(const std::string &method, const uri &request_uri) const override; + + virtual void head_response( + const uri &request_uri, + HttpRequest &req, + HttpResponse &res, + std::error_code &ec) override; + + virtual void get_response( + const uri &request_uri, + HttpRequest &req, + HttpResponse &res, + std::error_code &ec) override; + + private: + std::string GenerateTemplate( + const fs::path &templateFile, + std::shared_ptr pluginInfo); + }; +} + +using namespace pipedal::impl; + +ModWebIntercept::ptr ModWebIntercept::Create(PiPedalModel *model) +{ + return std::make_shared(model); +} + +bool ModWebInterceptImpl::wants(const std::string &method, const uri &request_uri) const +{ + if (request_uri.segment_count() < 2 || request_uri.segment(0) != "resources") + { + return false; + } + return true; +} + +void ModWebInterceptImpl::head_response( + const uri &request_uri, + HttpRequest &req, + HttpResponse &res, + std::error_code &ec) +{ + get_response(request_uri, req, res, ec); + res.clearBody(); +} + +static void setCacheControl(HttpResponse &res, const fs::path &path) +{ + if (fs::exists(path)) + { + res.set("Cache-Control", "public, max-age=31536000"); // 1 month + auto lastModified = std::filesystem::last_write_time(path); + res.set(HttpField::LastModified, HtmlHelper::timeToHttpDate(lastModified)); + } + else + { + res.set("Cache-Control", "no-cache, no-store, must-revalidate"); + } +} +void ModWebInterceptImpl::get_response( + const uri &request_uri, + HttpRequest &req, + HttpResponse &res, + std::error_code &ec) +{ + try + { + std::string ns = request_uri.query("ns"); + std::string strVersion = request_uri.query("v"); + (void)strVersion; // this is just cache-busting, so we don't actually use it. + + std::shared_ptr pluginInfo; + if (!ns.empty()) + { + pluginInfo = model->GetPluginInfo(ns); + if (!pluginInfo) + { + throw std::runtime_error("Plugin not found."); + } + } + if (!pluginInfo->modGui()) + { + throw std::runtime_error("Plugin does not have a ModGui."); + } + if (request_uri.segment_count() < 2) + { + throw std::runtime_error("Invalid request URI."); + } + if (request_uri.segment(0) != "resources") + { + throw std::runtime_error("Invalid request URI: expected 'resources'."); + } + std::string segment = request_uri.segment(1); + if (segment == "_") + { + segment = request_uri.segment(2); + if (segment == "iconTemplate") + { + res.set("Content-Type", "text/html"); + res.setBody(GenerateTemplate( + pluginInfo->modGui()->iconTemplate(), + pluginInfo)); + setCacheControl(res, pluginInfo->modGui()->iconTemplate()); + } + else if (segment == "stylesheet") + { + res.set("Content-Type", "text/css"); + res.setBody(GenerateTemplate( + pluginInfo->modGui()->stylesheet(), + pluginInfo)); + setCacheControl(res, pluginInfo->modGui()->stylesheet()); + + } + + else if (segment == "screenshot") + { + std::filesystem::path path = pluginInfo->modGui()->screenshot(); + if (!fs::exists(path)) + { + ec = std::make_error_code(std::errc::no_such_file_or_directory); + return; + } + if (!fs::is_regular_file(path)) + { + throw std::runtime_error("Screenshot is not a regular file: " + path.string()); + } + + size_t size = fs::file_size(path); + std::string mimeType = MimeTypes::instance().MimeTypeFromExtension(path.extension().string()); + if (mimeType.empty()) + { + throw std::runtime_error("Unknown file type for screenshot: " + path.string()); + } + res.setBodyFile( + path, + false); // delete when done + res.set("Content-Type", mimeType); + res.setContentLength(size); + setCacheControl(res, path); + + } + else if (segment == "thumbnail") + { + std::filesystem::path path = pluginInfo->modGui()->thumbnail(); + if (!fs::exists(path)) + { + throw std::runtime_error("Thumbnail not found: " + path.string()); + } + if (!fs::is_regular_file(path)) + { + throw std::runtime_error("Thumbnail is not a regular file: " + path.string()); + } + + size_t size = fs::file_size(path); + std::string mimeType = MimeTypes::instance().MimeTypeFromExtension(path.extension().string()); + if (mimeType.empty()) + { + throw std::runtime_error("Unknown file type for thumbnail: " + path.string()); + } + res.setBodyFile( + path, + false); // delete when done + res.set("Content-Type", mimeType); + setCacheControl(res, path); + res.setContentLength(size); + } else { + throw std::runtime_error("Unknown resource: _/" + segment); + } + } + else + { + // a request for a plugin resource file. + fs::path resourcefile = pluginInfo->modGui()->resourceDirectory(); + for (size_t i = 1; i < request_uri.segment_count(); ++i) + { + resourcefile /= request_uri.segment(i); + } + if (!fs::exists(resourcefile) && !fs::is_regular_file(resourcefile)) + { + ec = std::make_error_code(std::errc::no_such_file_or_directory); + return; + } + size_t size = fs::file_size(resourcefile); + std::string mimeType = MimeTypes::instance().MimeTypeFromExtension(resourcefile.extension().string()); + if (mimeType.empty()) + { + throw std::runtime_error("Unknown file type for resource: " + resourcefile.string()); + } + res.setBodyFile( + resourcefile, + false); // delete when done + res.set("Content-Type", mimeType); + Lv2Log::debug(SS("ModWebIntercept: Serving resource file: " << resourcefile.string() << " (" << size << " bytes)")); + Lv2Log::debug(SS(" url: " << request_uri.str())); + setCacheControl(res, resourcefile); + res.setContentLength(size); + } + } + catch (const std::exception &e) + { + res.setBody("Error: " + std::string(e.what())); + ec = boost::system::errc::make_error_code(boost::system::errc::invalid_argument); + return; + } +} + +static std::string makeCns(const std::string &encodedUri, int64_t instanceId) +{ + std::stringstream ss; + ss << "_"; + for (char c : encodedUri) + { + if ( + (c >= 'a' && c <= 'z') || (c >= 'A' & c <= 'Z') || (c >= '0' && c <= '9')) + { + ss << c; + } + else + { + ss << '_'; // replace non-alphanumeric characters with '_' + } + } + ss << "_"; + if (instanceId < 0) + { + ss << "_"; + instanceId = -instanceId; + } + ss << instanceId; + return ss.str(); +} + +std::string ModWebInterceptImpl::GenerateTemplate( + const fs::path &templateFile, + std::shared_ptr pluginInfo) +{ + if (!fs::exists(templateFile)) + { + throw std::runtime_error("File not found. " + templateFile.string()); + } + + json_variant context = MakeModGuiTemplateData(pluginInfo); + int64_t version = pluginInfo->minorVersion() * 1000 + + pluginInfo->microVersion(); + + std::string encodedUri = HtmlHelper::encode_url_segment(pluginInfo->uri(), true); + std::string ns = SS("?ns=" << encodedUri << "&v=" << version); + context["_ns"] = ns; + + std::string cns = makeCns(encodedUri, version); + context["_cns"] = cns; + + return GenerateFromTemplateFile( + templateFile, + context); +} diff --git a/src/WebServerMod.hpp b/src/WebServerMod.hpp new file mode 100644 index 0000000..0326be5 --- /dev/null +++ b/src/WebServerMod.hpp @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2025 Robin E. R. Davies + * All rights reserved. + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include "WebServer.hpp" + +#include "PiPedalModel.hpp" +#include + + +namespace pipedal { + + + class ModWebIntercept : public RequestHandler + { + + PiPedalModel *model; + protected: + ModWebIntercept(const std::string &target_url) + : RequestHandler(target_url.c_str()) + { + } + public: + using self = ModWebIntercept; + using super = RequestHandler; + using ptr = std::shared_ptr; + + static ptr Create(PiPedalModel *model); + + ModWebIntercept(PiPedalModel *model) + : RequestHandler("/var"), + model(model) + { + } + virtual ~ModWebIntercept() = default; + + + }; + +} \ No newline at end of file diff --git a/todo.txt b/todo.txt index 940fc5d..7e60566 100644 --- a/todo.txt +++ b/todo.txt @@ -1,4 +1,11 @@ -Exctly one underrun per seek in Toob Player + +check reload after change of LV2 plugins. + +Vu Meters, move rendering to background images, in order to reduce layout overhead. + +Remove FullScreenIME in PluginControlView.tsx + +Exactly one underrun per seek in Toob Player Test autohotspot detection. diff --git a/vite/CMakeLists.txt b/vite/CMakeLists.txt index f5f87e3..b881fe9 100644 --- a/vite/CMakeLists.txt +++ b/vite/CMakeLists.txt @@ -108,7 +108,6 @@ add_custom_command( public/iso_codes.json public/logo512.png public/sample_lv2_plugins.json - public/vite.svg src/pipedal/SplitUiControls.tsx src/pipedal/JackHostStatus.tsx src/pipedal/VirtualKeyboardHandler.tsx @@ -291,8 +290,6 @@ add_custom_command( src/pipedal/MidiChannelBinding.tsx src/pipedal/ModFileTypes.tsx src/assets/react.svg - src/App.css - src/App.tsx src/vite-env.d.ts src/index.css src/main.tsx diff --git a/vite/index.html b/vite/index.html index 462145c..68c9bd8 100644 --- a/vite/index.html +++ b/vite/index.html @@ -10,7 +10,7 @@ - + "; + let cssDoc = parser.parseFromString(cssText, "text/html"); + let cssElement = cssDoc.getElementById('mod-gui-style'); + if (!cssElement) { + setModError("No style element found."); + return; + } + if (hostDivRef === null) { + return; // cancelled. + } + prepareElement(element); + // add element to the hostDivRef + hostDivRef.appendChild(cssElement); + hostDivRef.appendChild(element); + let width = element.clientWidth; + let height = element.clientHeight; + hostDivRef.style.width = width + "px"; + hostDivRef.style.height = height + "px"; + } catch (error) { + setModError("Error loading UI: " + (error instanceof Error ? error.message : String(error))); + } + + } + + React.useEffect(() => { + let tHostDivRef = hostDivRef; + if (ready) { + if (hostDivRef !== null) { + requestContent(); + } + } + let stateHandler = (state: State) => { + setReady(state === State.Ready); + } + xmodel.state.addOnChangedHandler(stateHandler) + return () => { + xmodel.state.removeOnChangedHandler(stateHandler); + if (tHostDivRef !== null) { + // unmount the custom content. + let children = tHostDivRef.children; + for (let i = children.length - 1; i >= 0; i--) { + let child = children[i]; + if (child instanceof HTMLElement) { + child.remove(); + } + } + } + let mc = modGuiControls; + for (let i = 0; i < mc.length; i++) { + let modGuiControl = mc[i]; + modGuiControl.onUnmount(); + } + mc.length = 0; // Clear the controls array + }; + }, [hostDivRef, plugin, ready]); + + return ( + { props.onClose(); setErrorMessage(null); }}> +
{ + // Prevent click events from propagating to the parent element. + event.stopPropagation(); + }} + > + onClose()} style={{ + position: "absolute", top: 16, right: 16, + background: "#FFF5", zIndex: 600 + }}> + + + +
+ +
+
{ + // Prevent click events from propagating to the parent element. + event.stopPropagation(); + } + } + /> +
+ +
+ +
+ { + setErrorMessage(null); + onClose(); + }} + text={errorMessage ?? ""} + /> + + ); +} +export default ModGuiHost; \ No newline at end of file diff --git a/vite/src/pipedal/ModGuiTest.tsx b/vite/src/pipedal/ModGuiTest.tsx new file mode 100644 index 0000000..1e0d616 --- /dev/null +++ b/vite/src/pipedal/ModGuiTest.tsx @@ -0,0 +1,346 @@ +/* + * Copyright (c) 2025 Robin E. R. Davies + * All rights reserved. + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React from 'react'; +import Divider from '@mui/material/Divider'; +import { PiPedalModel, PiPedalModelFactory, MonitorPortHandle, State, ListenHandle, PatchPropertyListener } from './PiPedalModel'; +import { useTheme } from '@mui/material/styles'; +import CircularProgress from '@mui/material/CircularProgress'; +import LoadPluginDialog from './LoadPluginDialog'; +import ButtonEx from './ButtonEx'; +import Typography from '@mui/material/Typography/Typography'; +import ModGuiHost, { IModGuiHostSite } from './ModGuiHost'; +import { UiPlugin } from './Lv2Plugin'; +import ModGuiErrorBoundary from './ModGuiErrorBoundary'; + +class MyPortHandle implements MonitorPortHandle { + private static nextHandle: number = 1; + constructor() { + this.handle = MyPortHandle.nextHandle++; + } + handle: number; +}; + +type MonitorCallback = (value: number) => void; + +class ValueEntry { + constructor(defaultValue: number) { + this.value = defaultValue; + } + private listeners: { [handle: number]: MonitorCallback } = {}; + + monitor(interval: number, callback: MonitorCallback): MyPortHandle { + let handle = new MyPortHandle(); + this.listeners[handle.handle] = callback; + return handle; + } + unmonitor(handle: MonitorPortHandle) { + let h = (handle as MyPortHandle).handle; + // remove entry h from listeners + if (!(h in this.listeners)) { + throw new Error("Invalid handle: " + h); + } + // remove the callback from the listeners + delete this.listeners[h]; + } + setValue(value: number) { + if (value !== this.value) { + this.value = value; + // notify all listeners + for (let handle in this.listeners) { + this.listeners[handle](value); + } + } + } + value: number; +}; + +class PatchListenerEntry { + constructor(instanceId: number, propertyUri: string) { + this.instanceId = instanceId; + this.propertyUri = propertyUri; + } + instanceId: number; + propertyUri: string; + value: any = ""; + listeners: { handle: number, listener: PatchPropertyListener}[] = [];; +}; + + +class ValueHandler { + + private valueDictionary: { [symbol: string]: ValueEntry } = {}; + private handleToValueEnry: { [handle: number]: ValueEntry } = {}; + + constructor(instanceId: number, plugin: UiPlugin) { + for (let control of plugin.controls) { + this.valueDictionary[control.symbol] = new ValueEntry(control.default_value); + } + this.valueDictionary["_bypass"] = new ValueEntry(0); + } + monitorPort(instanceId: number, symbol: string, interval: number, callback: MonitorCallback): MonitorPortHandle { + let valueEntry = this.valueDictionary[symbol]; + + let portHandle = valueEntry.monitor(interval, callback); + this.handleToValueEnry[(portHandle as MyPortHandle).handle] = valueEntry; + callback(valueEntry.value); + return portHandle; + } + unmonitorPort(handle: MonitorPortHandle): void { + let h = (handle as MyPortHandle).handle; + if (!(h in this.handleToValueEnry)) { + throw new Error("Invalid handle: " + h); + } + let valueEntry = this.handleToValueEnry[h]; + valueEntry.unmonitor(handle); + delete this.handleToValueEnry[h]; + } + setValue(instanceId: number, symbol: string, value: number) { + // add a delay just for funsies. + window.setTimeout(() => { + if (!(symbol in this.valueDictionary)) { + throw new Error("Invalid symbol: " + symbol); + } + let valueEntry = this.valueDictionary[symbol]; + valueEntry.setValue(value); + }, 100); + } + getValue(instanceId: number, symbol: string): number { + if (!(symbol in this.valueDictionary)) { + throw new Error("Invalid symbol: " + symbol); + } + let valueEntry = this.valueDictionary[symbol]; + return valueEntry.value; + } + private nextListenHandle: number = 1; + + + private patchListeners: PatchListenerEntry[] = []; + + monitorPatchProperty(instanceId: number, propertyUri: string, onReceived: PatchPropertyListener): ListenHandle { + let h = this.nextListenHandle++; + for (let entry of this.patchListeners) { + if (entry.instanceId === instanceId && entry.propertyUri === propertyUri) { + // already listening to this property + entry.listeners.push({ handle: h, listener: onReceived }); + onReceived(instanceId,propertyUri,entry.value); + return { _handle: h }; + } + } + let entry = new PatchListenerEntry(instanceId, propertyUri); + this.patchListeners.push(entry); + entry.listeners.push({ handle: h, listener: onReceived }); + onReceived(instanceId, propertyUri, entry.value); + return {_handle: h}; + } + cancelMonitorPatchProperty(listenHandle: ListenHandle): void { + for (let i = 0; i < this.patchListeners.length; ++i) { + let entry = this.patchListeners[i]; + for (let j = 0; j < entry.listeners.length; ++j) { + if (entry.listeners[j].handle === listenHandle._handle) { + // found the listener, remove it + entry.listeners.splice(j, 1); + if (entry.listeners.length === 0) { + // no more listeners, remove the entry + this.patchListeners.splice(i, 1); + } + return; + } + } + } + } + getPatchProperty(instanceId: number, uri: string): Promise { + let promise = new Promise((resolve, reject) => { + window.setTimeout(() => { + for (let entry of this.patchListeners) { + if (entry.instanceId === instanceId && entry.propertyUri === uri) { + window.setTimeout(() => { + // simulate a delay for getting the property + entry.listeners.forEach(l => l.listener(instanceId, uri, entry.value)); + }, 100); + return resolve(entry.value); + } + } + reject(new Error(`Patch property not found: ${uri} for instance ${instanceId}`)); + },50); + }); + return promise; + } + setPatchProperty(instanceId: number, uri: string, value: any): Promise { + // Implementation here + for (let entry of this.patchListeners) { + if (entry.instanceId === instanceId && entry.propertyUri === uri) { + entry.value = value; + // notify all listeners + entry.listeners.forEach(l => l.listener(instanceId, uri, value)); + return Promise.resolve(true); + } + } + // if we reach here, the property was not found + return Promise.reject("Not found."); + } +}; + +function ModGuiTest() { + const [pluginUrl, setPluginUrl] = React.useState(""); + const [uiPlugin, setUiPlugin] = React.useState(null); + const [pluginDialogOpen, setPluginDialogOpen] = React.useState(false); + const [loading, setLoading] = React.useState(true); + const [valueHandler, setValueHandler] = React.useState(null); + + const [modGuiHostSite] = React.useState({ + monitorPort: (instanceId: number, symbol: string, interval: number, callback: (value: number) => void) => { + if (!valueHandler) { + throw new Error("ValueHandler is not set"); + } + return valueHandler.monitorPort(instanceId, symbol, interval, callback); + }, + unmonitorPort: (handle) => { + if (!valueHandler) { + throw new Error("ValueHandler is not set"); + } + valueHandler.unmonitorPort(handle); + }, + setPedalboardControl: (instanceId, symbol, value) => { + if (!valueHandler) { + throw new Error("ValueHandler is not set"); + } + valueHandler.setValue(instanceId, symbol, value); + }, + monitorPatchProperty(instanceId: number, propertyUri: string, onReceived: PatchPropertyListener): ListenHandle { + if (!valueHandler) { + throw new Error("ValueHandler is not set"); + } + + return valueHandler.monitorPatchProperty(instanceId, propertyUri, onReceived); + }, + cancelMonitorPatchProperty: (listenHandle) => { + if (!valueHandler) { + throw new Error("ValueHandler is not set"); + } + valueHandler.cancelMonitorPatchProperty(listenHandle); + }, + getPatchProperty: async (instanceId, uri) => { + if (!valueHandler) { + throw new Error("ValueHandler is not set"); + } + return valueHandler.getPatchProperty(instanceId, uri) as Promise; + }, + setPatchProperty: async (instanceId, uri, value) => { + if (!valueHandler) { + throw new Error("ValueHandler is not set"); + } + return valueHandler.setPatchProperty(instanceId, uri, value) as Promise; + } + }); + + let model: PiPedalModel = PiPedalModelFactory.getInstance(); + + React.useEffect(() => { + + const onStateChange = (state: State) => { + if (state == State.Ready) { + setLoading(false); + } + }; + + model.state.addOnChangedHandler(onStateChange); + + return () => { + model.state.removeOnChangedHandler(onStateChange); + }; + }, []) + + const theme = useTheme(); + + return ( +
+ +
+ +
+ + +
+ ModGUI Test + + {pluginUrl ? pluginUrl : "No plugin loaded"} + + setPluginDialogOpen(true)} > + Load + +
+ + + + {uiPlugin && ( +
+ { + setPluginUrl(""); + setUiPlugin(null); + setValueHandler(null); + }} > + + { + setPluginUrl(""); + setUiPlugin(null); + }} + hostSite={modGuiHostSite} + /> + +
+ )} + {pluginDialogOpen && + { + setPluginUrl(url as string); + let uiPlugin = model.getUiPlugin(url); + setUiPlugin(uiPlugin); + if (uiPlugin) { + setValueHandler(new ValueHandler(-1, uiPlugin)); + } + setPluginDialogOpen(false); + }} + onCancel={() => setPluginDialogOpen(false)} + uri={pluginUrl} + modGuiOnly={true} + /> + } +
+ ); +} + +export default ModGuiTest; \ No newline at end of file diff --git a/vite/src/pipedal/OkDialog.tsx b/vite/src/pipedal/OkDialog.tsx new file mode 100644 index 0000000..cc0d4cc --- /dev/null +++ b/vite/src/pipedal/OkDialog.tsx @@ -0,0 +1,105 @@ +/* + * MIT License + * + * Copyright (c) 2022 Robin E. R. Davies + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React from 'react'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import IconButtonEx from './IconButtonEx'; +import Button from '@mui/material/Button'; +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import DialogEx from './DialogEx'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; + +import ReportGmailerrorredIcon from '@mui/icons-material/ReportGmailerrorred'; + +export interface OkDialogProps { + open: boolean, + title?: string, + text: string, + okButtonText?: string, + onClose: () => void +}; + +export interface OkDialogState { +}; + +export default class OkDialog extends React.Component { + + + constructor(props: OkDialogProps) { + super(props); + this.state = { + }; + } + + render() { + let props = this.props; + let { open, okButtonText, text, onClose } = props; + + const handleClose = () => { + onClose(); + }; + + const handleOk = () => { + onClose(); + } + return ( + + {props.title && ( + + + {this.props.onClose();}} aria-label="back" + > + + + + {props.title} + + + )} + + +
+ + {text} +
+
+ + + + +
+ ); + } +} \ No newline at end of file diff --git a/vite/src/pipedal/PiPedalModel.tsx b/vite/src/pipedal/PiPedalModel.tsx index c9c2e5a..70d26ce 100644 --- a/vite/src/pipedal/PiPedalModel.tsx +++ b/vite/src/pipedal/PiPedalModel.tsx @@ -416,6 +416,7 @@ export class PiPedalModel //implements PiPedalModel socketServerUrl: string = ""; varServerUrl: string = ""; + modResourcesUrl: string = ""; lv2Path: string = ""; webSocket?: PiPedalSocket; @@ -972,13 +973,17 @@ export class PiPedalModel //implements PiPedalModel // anything could have changed while we were disconnected. await this.loadServerState(); } - makeSocketServerUrl(hostName: string, port: number): string { + private makeSocketServerUrl(hostName: string, port: number): string { return "ws://" + hostName + ":" + port + "/pipedal"; } - makeVarServerUrl(protocol: string, hostName: string, port: number): string { + private makeVarServerUrl(protocol: string, hostName: string, port: number): string { return protocol + "://" + hostName + ":" + port + "/var/"; + } + private makeModResourceUrl(protocol: string, hostName: string, port: number): string { + return protocol + "://" + hostName + ":" + port + "/resources/"; + } async getNextAudioFile(filePath: string): Promise { @@ -1053,6 +1058,7 @@ export class PiPedalModel //implements PiPedalModel if (!socket_server_port) socket_server_port = 8080; let socket_server = this.makeSocketServerUrl(socket_server_address, socket_server_port); let var_server_url = this.makeVarServerUrl("http", socket_server_address, socket_server_port); + this.modResourcesUrl = this.makeModResourceUrl("http", socket_server_address, socket_server_port); this.socketServerUrl = socket_server; this.varServerUrl = var_server_url; diff --git a/vite/src/pipedal/PluginControlView.tsx b/vite/src/pipedal/PluginControlView.tsx index 0a73410..6c3c097 100644 --- a/vite/src/pipedal/PluginControlView.tsx +++ b/vite/src/pipedal/PluginControlView.tsx @@ -23,6 +23,8 @@ import WithStyles, { withTheme } from './WithStyles'; import { createStyles } from './WithStyles'; import { css } from '@emotion/react'; +import ModGuiHost, { IModGuiHostSite } from './ModGuiHost'; + import { withStyles } from "tss-react/mui"; import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; import { UiPlugin, UiControl, UiFileProperty, UiFrequencyPlot, ScalePoint } from './Lv2Plugin'; @@ -340,6 +342,8 @@ export interface PluginControlViewProps extends WithStyles { item: PedalboardItem; customization?: ControlViewCustomization; customizationId?: number; + showModGui: boolean; + onSetShowModGui: (instanceId: number, showModGui: boolean) => void; } type PluginControlViewState = { landscapeGrid: boolean; @@ -360,7 +364,6 @@ const PluginControlView = constructor(props: PluginControlViewProps) { super(props); this.model = PiPedalModelFactory.getInstance(); - this.state = { landscapeGrid: false, imeUiControl: undefined, @@ -376,6 +379,19 @@ const PluginControlView = this.onControlValueChanged = this.onControlValueChanged.bind(this); this.onPreviewChange = this.onPreviewChange.bind(this); } + // unmonitorPort: (handle: MonitorPortHandle) => void; + // onValueChanged: (instanceId: number, symbol: string, value: number) => void; + + // monitorPatchProperty( + // instanceId: number, + // propertyUri: string, + // onReceived: PatchPropertyListener + // ): ListenHandle; + + // cancelMonitorPatchProperty(listenHandle: ListenHandle): void; + + // getPatchProperty(instanceId: number, uri: string): Promise; + // setPatchProperty(instanceId: number, uri: string, value: any): Promise onPreviewChange(key: string, value: number): void { @@ -758,19 +774,45 @@ const PluginControlView = } return false; } + makeHostSite(): IModGuiHostSite { + // Yuck. :-( This worked out badly. + let model = this.model; + return { + monitorPort: this.model.monitorPort.bind(model), + unmonitorPort: this.model.unmonitorPort.bind(model), + setPedalboardControl: this.model.setPedalboardControl.bind(model), + monitorPatchProperty: this.model.monitorPatchProperty.bind(model), + cancelMonitorPatchProperty: this.model.cancelMonitorPatchProperty.bind(model), + getPatchProperty: this.model.getPatchProperty.bind(model), + setPatchProperty: this.model.setPatchProperty.bind(model), + }; + } - render(): ReactNode { - this.controlKeyIndex = 0; - const classes = withStyles.getClasses(this.props); - let pedalboardItem: PedalboardItem; - let pedalboard = this.model.pedalboard.get(); - if (this.props.instanceId === Pedalboard.START_CONTROL) { - pedalboardItem = pedalboard.makeStartItem(); - } else if (this.props.instanceId === Pedalboard.END_CONTROL) { - pedalboardItem = pedalboard.makeEndItem(); - } else { - pedalboardItem = pedalboard.getItem(this.props.instanceId); + renderModGui(): ReactNode { + let uiPlugin = this.model.getUiPlugin(this.props.item.uri); + if (!uiPlugin) { + return (
); } + return ( + { + if (this.props.onSetShowModGui) { + this.props.onSetShowModGui(this.props.instanceId, false); + } + }} + hostSite={this.makeHostSite()} + /> + ) + } + + renderPiPedalControl(pedalboardItem?: PedalboardItem): ReactNode { + this.controlKeyIndex = 0; + + + const classes = withStyles.getClasses(this.props); + let pedalboard = this.model.pedalboard.get(); if (!pedalboardItem) return (
); @@ -792,19 +834,13 @@ const PluginControlView = controlValues = this.filterNotOnGui(controlValues, plugin); - - let gridClass = this.state.landscapeGrid ? classes.landscapeGrid : classes.normalGrid; let scrollClass = this.state.landscapeGrid ? classes.frameScrollLandscape : classes.frameScrollPortrait; - let vuMeterRClass = this.state.landscapeGrid ? classes.vuMeterRLandscape : classes.vuMeterR; - let controlNodes: ControlNodes; - let frameClass = classes.frame; - if (this.fullScreen()) { gridClass = classes.noScrollGrid; scrollClass = classes.frameScrollNone; - frameClass = classes.noScrollFrame; } + let controlNodes: ControlNodes; controlNodes = this.getStandardControlNodes(plugin, controlValues); @@ -822,16 +858,7 @@ const PluginControlView = if (pedalboardItem.midiChannelBinding) { nodes.push(this.midiBindingControl(pedalboardItem)); } - - return ( -
-
- -
-
- -
{ @@ -848,6 +875,44 @@ const PluginControlView = }
+ ); + + + } + render(): ReactNode { + const classes = withStyles.getClasses(this.props); + let pedalboard = this.model.pedalboard.get(); + + let pedalboardItem: PedalboardItem; + if (this.props.instanceId === Pedalboard.START_CONTROL) { + pedalboardItem = pedalboard.makeStartItem(); + } else if (this.props.instanceId === Pedalboard.END_CONTROL) { + pedalboardItem = pedalboard.makeEndItem(); + } else { + pedalboardItem = pedalboard.getItem(this.props.instanceId); + } + + let vuMeterRClass = this.state.landscapeGrid ? classes.vuMeterRLandscape : classes.vuMeterR; + let frameClass = classes.frame; + + if (this.fullScreen()) { + frameClass = classes.noScrollFrame; + } + + + return ( +
+
+ +
+
+ +
+ { + this.props.showModGui && this.renderModGui() + || this.renderPiPedalControl(pedalboardItem) + } + {this.state.showFileDialog && ( )} - + {/* xxx: I don't think we need this anyore. */} this.onImeValueChange(key, value)} diff --git a/vite/src/pipedal/ScratchClass.tsx b/vite/src/pipedal/ScratchClass.tsx new file mode 100644 index 0000000..c6655f8 --- /dev/null +++ b/vite/src/pipedal/ScratchClass.tsx @@ -0,0 +1,91 @@ +// Copyright (c) 2022 Robin Davies +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import ResizeResponsiveComponent from './ResizeResponsiveComponent'; +import { Theme } from '@mui/material/styles'; +import WithStyles from './WithStyles'; +import { withStyles } from "tss-react/mui"; +import {createStyles} from './WithStyles'; + +import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; + +const styles = (theme: Theme) => createStyles({ + pgraph: { + paddingBottom: 16 + } +}); + + +export interface ScratchClassProps extends WithStyles { +} + +export interface ScratchClassState { + screenWidth: number, + screenHeight: number +} + + + +const ScratchClass = withStyles( + class extends ResizeResponsiveComponent { + private model: PiPedalModel; + constructor(props: ScratchClassProps) { + super(props); + + this.model = PiPedalModelFactory.getInstance(); + void this.model; // suppress unused variable warning + + this.state = { + screenWidth: this.windowSize.width, + screenHeight: this.windowSize.height + }; + } + mounted: boolean = false; + + + + onWindowSizeChanged(width: number, height: number): void { + this.setState({ screenWidth: width, screenHeight: height }); + } + + + componentDidMount() { + super.componentDidMount(); + this.mounted = true; + } + componentWillUnmount() { + this.mounted = false; + super.componentWillUnmount(); + } + + + componentDidUpdate(prevProps: ScratchClassProps) { + } + + + + render() { + const classes = withStyles.getClasses(this.props); + void classes; // suppress unused variable warning + return (
); + } + }, + styles); + +export default ScratchClass; \ No newline at end of file diff --git a/vite/src/pipedal/svg/mod_ui.svg b/vite/src/pipedal/svg/mod_ui.svg new file mode 100644 index 0000000..89f032a --- /dev/null +++ b/vite/src/pipedal/svg/mod_ui.svg @@ -0,0 +1,25 @@ + + + + + + + + + + diff --git a/vite/src/pipedal/svg/pp_ui.svg b/vite/src/pipedal/svg/pp_ui.svg new file mode 100644 index 0000000..16bd714 --- /dev/null +++ b/vite/src/pipedal/svg/pp_ui.svg @@ -0,0 +1,23 @@ + + + + + + + + + + diff --git a/vite/vite.config.ts b/vite/vite.config.ts index 8a12749..62dcc1e 100644 --- a/vite/vite.config.ts +++ b/vite/vite.config.ts @@ -8,4 +8,12 @@ export default defineConfig({ chunkSizeWarningLimit: 2000 }, plugins: [react(),svgr()], + server: { + proxy: { + '/resources': { + target: 'http://localhost:8080', + changeOrigin: false, + }, + } +} })