diff --git a/CMakeLists.txt b/CMakeLists.txt index 26caa26..a279c18 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,13 +1,13 @@ cmake_minimum_required(VERSION 3.16.0) project(pipedal - VERSION 2.0.101 + VERSION 2.0.102 DESCRIPTION "PiPedal Guitar Effect Pedal For Raspberry Pi" HOMEPAGE_URL "https://rerdavies.github.io/pipedal" ) execute_process( COMMAND dpkg --print-architecture COMMAND tr -d '\n' OUTPUT_VARIABLE DEBIAN_ARCHITECTURE ) -set (DISPLAY_VERSION "PiPedal v2.0.101-Alpha") +set (DISPLAY_VERSION "PiPedal v2.0.102-Alpha") set (PACKAGE_ARCHITECTURE ${DEBIAN_ARCHITECTURE}) set (CMAKE_INSTALL_PREFIX "/usr/") diff --git a/PiPedalCommon/src/DBusDispatcher.cpp b/PiPedalCommon/src/DBusDispatcher.cpp index 412db24..e440552 100644 --- a/PiPedalCommon/src/DBusDispatcher.cpp +++ b/PiPedalCommon/src/DBusDispatcher.cpp @@ -208,8 +208,9 @@ void DBusDispatcher::WaitForClose() serviceThread.join(); } } - catch (const std::exception &) + catch (const std::exception &e) { + std::cout << "DBusDispatcher: " << e.what() << std::endl; } if (eventFd != -1) { diff --git a/PiPedalCommon/src/include/DBusDispatcher.hpp b/PiPedalCommon/src/include/DBusDispatcher.hpp index 1da5414..9bc7299 100644 --- a/PiPedalCommon/src/include/DBusDispatcher.hpp +++ b/PiPedalCommon/src/include/DBusDispatcher.hpp @@ -78,7 +78,7 @@ private: std::atomic stopping; std::vector postedEvents; - bool threadStarted; + bool threadStarted = false; std::thread serviceThread; std::mutex postMutex; }; \ No newline at end of file diff --git a/README.md b/README.md index 9e08823..4e136d3 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,11 @@ -Download: v2.0.101 +Download: v2.0.102 Website: [https://rerdavies.github.io/pipedal](https://rerdavies.github.io/pipedal). Documentation: [https://rerdavies.github.io/pipedal/Documentation.html](https://rerdavies.github.io/pipedal/Documentation.html). -#### NEW version 2.0.101 Release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details. TooB NAM calibration, manipulation of i/r files in Toob Convolution Reverb, new Parametric EQ and 3 Band EQ plugins, and MORE! +#### NEW version 2.0.102 Release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details. TooB NAM calibration, manipulation of i/r files in Toob Convolution Reverb, new Parametric EQ and 3 Band EQ plugins, and MORE!   diff --git a/docs/Installing.md b/docs/Installing.md index dab56b5..f81af1e 100644 --- a/docs/Installing.md +++ b/docs/Installing.md @@ -13,18 +13,18 @@ page_icon: img/Install4.jpg Download the most recent Debian (.deb) package for your platform: -- [Raspberry Pi OS bookworm (aarch64) v2.0.101](https://github.com/rerdavies/pipedal/releases/download/v2.0.101/pipedal_2.0.101_arm64.deb) -- [Ubuntu 24.04 through 25.04 (aarch64) v2.0.101](https://github.com/rerdavies/pipedal/releases/download/v2.0.101/pipedal_2.0.101_arm64.deb) -- [Ubuntu 24.04 through 25.04 (amd64) v2.0.101](https://github.com/rerdavies/pipedal/releases/download/v2.0.101/pipedal_2.0.101_amd64.deb) +- [Raspberry Pi OS bookworm (aarch64) v2.0.102](https://github.com/rerdavies/pipedal/releases/download/v2.0.102/pipedal_2.0.102_arm64.deb) +- [Ubuntu 24.04 through 25.04 (aarch64) v2.0.102](https://github.com/rerdavies/pipedal/releases/download/v2.0.102/pipedal_2.0.102_arm64.deb) +- [Ubuntu 24.04 through 25.04 (amd64) v2.0.102](https://github.com/rerdavies/pipedal/releases/download/v2.0.102/pipedal_2.0.102_amd64.deb) -Version 2.0.101 has been tested on Raspberry Pi OS bookworm, Ubuntu 24.04 (amd64), Ubuntu 24.10 (aarch64), and Ubuntu 25.04 (aarch64). Download the appropriate package for your platform, and install using the following procedure: +Version 2.0.102 has been tested on Raspberry Pi OS bookworm, Ubuntu 24.04 (amd64), Ubuntu 24.10 (aarch64), and Ubuntu 25.04 (aarch64). Download the appropriate package for your platform, and install using the following procedure: ``` sudo apt update sudo apt upgrade cd ~/Downloads - sudo apt-get install ./pipedal_2.0.101_arm64.deb + sudo apt-get install ./pipedal_2.0.102_arm64.deb ``` You MUST use `apt-get`. `apt` will not install downloaded packages; and `dpkg -i` will not install dependencies. diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 51de7f4..58dd5b5 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,15 +1,39 @@ # Release Notes -## PiPedal 2.0.101 Internal +## PiPedal 2.0.102 Alpha -- TooB Tone parameter tweaks. +PiPedal 2.0 provides support for Neural Amp Modeler A2 models. -## PiPedal 2.0.100 Internal +NAM A2 provides significant improvements in sound quality and performance compared to NAM A1. NAM A2 also provides "Slimable" models. Slimmable models provide multiple levels of performance and sound quality in a single model file, allowing you to choose the level of performance and sound quality that best suits your needs. This is particularly useful for users of Raspberry Pi 4, since NAM A2 models can provide better sound quality than NAM A1 models, while consuming significantly less CPU resources. -- Tone3000 direct uploads to PiPedal server (Toob NAM and Toob CabIr) -- New TooB Tone plugin: a simple one-knob tone control. -- Channel Routing: configure audio channels as Aux or re-amp channels. +A new integrated Tone3000 model downloader provides a convenient way to download the latest NAM A2 models and IIR files from Tone3000 directly to the PiPedal server. All existing NAM A1 models on Tone3000 have been retrained for NAM A2. When +you download from Tone3000 using the integrated downloader, you will get NAM A2 models. Just click on the DOWNLOAD MODELS FROM TONE3000 button. +Currently, the best way to get access to NAM A2 models is via the integrated Tone3000 model downloader. Models downloaded externally from the Tone3000 website will still be NAM A1 models; however this should change soon with a forthcoming announcement from Tone3000. + +A new channel routing dialog replaces the channel configurationg dialog. The Channel Routing dialog allows global (i.e. not per-preset) configuration of audio channels. You can now configure audio channels as Aux channels (e.g. using the non-guitar channel on your audio interface as an input for backing tracks), or sending unprocessed guitar input signal to an unused output channel for re-amping in a DAW or external hardware. Refer to the (?) help dialog in the Channel Routing Dialog for more information on how to use this feature. + +### Features + +- Support for Neural Amp Modeler A2 models. The latest version of the TooB Neural Amp Modeler plugin now supports NAM A2 models, which provide improved performance and sound quality compared to older NAM A1 models. + +- Download Tone3000 NAM models and IIR files directly to the PiPedal server. Click on the DOWNLOAD MODELS FROM TONE3000 button in the NAM file selection dialog to download the latest NAM A2 models. + +- Download IIR files directly into TooB Cab IR from Tone3000. + +- New Channel Routing Dialog. + +- New TooB Tone plugin: a simple one-knob tone control. The new TooB Tone plugin provides a simple one-knob tone control that can be used to adjust the overall tone of your guitar signal. It is a great tool for quickly shaping your tone without having to use a more complex EQ plugin. + +- Copy and Paste plugins (and their settings) between pedalboards. Accessible via the "Copy Plugin" and "Paste Plugin" menu items in the former Plugin Preset selector menu. + +### Minor features and bug fixes + +- Better handling of step sizes in numeric edit controls. + +- Short names of TooB CE-2 Chorus and TooB BF-2 Flanger changed to "TooB Chorus" and "TooB Flanger", repsectively. + +- Split controls now support double-tap to reset to a default value. (Other controls already support this feature). ## PiPedal 1.5.99 Beta diff --git a/docs/download.md b/docs/download.md index 2d4adfb..18b0db2 100644 --- a/docs/download.md +++ b/docs/download.md @@ -4,9 +4,9 @@ Download the most recent Debian (.deb) package for your platform: -- [Raspberry Pi OS bookworm (aarch64) v2.0.101 Beta](https://github.com/rerdavies/pipedal/releases/download/v2.0.101/pipedal_2.0.101_arm64.deb) -- [Ubuntu 24.x, 25.04 (aarch64) v2.0.101 Alpha](https://github.com/rerdavies/pipedal/releases/download/v2.0.101/pipedal_2.0.101_arm64.deb) -- [Ubuntu 24.x, 25.04 (amd64) v2.0.101 Alpha](https://github.com/rerdavies/pipedal/releases/download/v2.0.101/pipedal_2.0.101_amd64.deb) +- [Raspberry Pi OS bookworm (aarch64) v2.0.102 Beta](https://github.com/rerdavies/pipedal/releases/download/v2.0.102/pipedal_2.0.102_arm64.deb) +- [Ubuntu 24.x, 25.04 (aarch64) v2.0.102 Alpha](https://github.com/rerdavies/pipedal/releases/download/v2.0.102/pipedal_2.0.102_arm64.deb) +- [Ubuntu 24.x, 25.04 (amd64) v2.0.102 Alpha](https://github.com/rerdavies/pipedal/releases/download/v2.0.102/pipedal_2.0.102_amd64.deb) Install the package by running @@ -14,7 +14,7 @@ Install the package by running ``` sudo apt update cd ~/Downloads - sudo apt-get install ./pipedal_2.0.101_arm64.deb + sudo apt-get install ./pipedal_2.0.102_arm64.deb ``` You MUST use `apt-get` to install the package. `apt install` will NOT install the package correctly. The message about missing permissions given by `apt-get` is expected, and can be safely ignored. diff --git a/lv2/x86_64/toobamp_1.2.76_amd64.deb b/lv2/x86_64/toobamp_1.3.78_amd64.deb similarity index 53% rename from lv2/x86_64/toobamp_1.2.76_amd64.deb rename to lv2/x86_64/toobamp_1.3.78_amd64.deb index f1de7d2..2f6ba9b 100644 Binary files a/lv2/x86_64/toobamp_1.2.76_amd64.deb and b/lv2/x86_64/toobamp_1.3.78_amd64.deb differ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7707dd3..f9f02d6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -207,6 +207,7 @@ else() endif() set (PIPEDAL_SOURCES + Tone3000Tone.hpp Tone3000Tone.cpp AesDigest.cpp AesDigest.hpp ChannelRouterSettings.cpp ChannelRouterSettings.hpp Curl.cpp Curl.hpp diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index f7834a0..76d59b5 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -47,6 +47,7 @@ #include "AudioFiles.hpp" #include "CrashGuard.hpp" #include "Tone3000Downloader.hpp" +#include "HtmlHelper.hpp" #ifndef NO_MLOCK #include @@ -80,7 +81,6 @@ PiPedalModel::PiPedalModel() atomConverter(pluginHost.GetMapFeature()) { this->updater = Updater::Create(); - this->updater->Start(); this->currentUpdateStatus = updater->GetCurrentStatus(); this->pedalboard = Pedalboard::MakeDefault(); @@ -211,6 +211,10 @@ PiPedalModel::~PiPedalModel() void PiPedalModel::Init(const PiPedalConfiguration &configuration) { std::lock_guard lock(mutex); // prevent callbacks while we're initializing. + if (updaterEnabled) + { + this->updater->Start(); + } this->configuration = configuration; pluginHost.SetConfiguration(configuration); @@ -239,10 +243,9 @@ void PiPedalModel::Init(const PiPedalConfiguration &configuration) int64_t PiPedalModel::DownloadModelsFromTone3000( const std::string &uri, - const Tone3000PkceParams&pckeParams, + const Tone3000PkceParams &pckeParams, const std::string &downloadPath, - Tone3000DownloadType downloadType -) + Tone3000DownloadType downloadType) { std::lock_guard lock(mutex); @@ -271,8 +274,7 @@ int64_t PiPedalModel::DownloadModelsFromTone3000( uri, pckeParams, downloadPath, - downloadType - ); + downloadType); } void PiPedalModel::CancelTone3000Download( @@ -290,8 +292,13 @@ void PiPedalModel::CancelTone3000Download( // Tone3000Downloader::Listener implementation void PiPedalModel::OnStartTone3000Download(int64_t handle, const std::string &title) { - std::lock_guard lock(mutex); - for (auto subscriber : this->subscribers) + + SubscriberList subscribers; + { + std::lock_guard lock(mutex); + subscribers = this->subscribers; + } + for (auto &subscriber : this->subscribers) { subscriber->OnTone3000DownloadStarted(handle, title); } @@ -299,8 +306,12 @@ void PiPedalModel::OnStartTone3000Download(int64_t handle, const std::string &ti void PiPedalModel::OnTone3000Progress(const Tone3000DownloadProgress &progress) { - std::lock_guard lock(mutex); - for (auto subscriber : this->subscribers) + SubscriberList subscribers; + { + std::lock_guard lock(mutex); + subscribers = this->subscribers; + } + for (auto &subscriber : subscribers) { subscriber->OnTone3000DownloadProgress(progress); } @@ -308,8 +319,12 @@ void PiPedalModel::OnTone3000Progress(const Tone3000DownloadProgress &progress) void PiPedalModel::OnTone3000DownloadComplete(int64_t handle, const std::string &resultPath) { - std::lock_guard lock(mutex); - for (auto subscriber : this->subscribers) + SubscriberList subscribers; + { + std::lock_guard lock(mutex); + subscribers = this->subscribers; + } + for (auto subscriber : subscribers) { subscriber->OnTone3000DownloadComplete(handle, resultPath); } @@ -317,8 +332,12 @@ void PiPedalModel::OnTone3000DownloadComplete(int64_t handle, const std::string void PiPedalModel::OnTone3000DownloadError(int64_t handle, const std::string &errorMessage) { - std::lock_guard lock(mutex); - for (auto subscriber : this->subscribers) + SubscriberList subscribers; + { + std::lock_guard lock(mutex); + subscribers = this->subscribers; + } + for (auto subscriber : subscribers) { subscriber->OnTone3000DownloadError(handle, errorMessage); } @@ -551,12 +570,15 @@ void PiPedalModel::SetInputVolume(float value) { PreviewInputVolume(value); { - std::lock_guard lock(mutex); + SubscriberList subscribers; + { + std::lock_guard lock(mutex); + subscribers = this->subscribers; - this->pedalboard.input_volume_db(value); + this->pedalboard.input_volume_db(value); + } // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) - std::vector t{subscribers.begin(), subscribers.end()}; - for (auto &subscriber : t) + for (auto &subscriber : subscribers) { subscriber->OnInputVolumeChanged(value); } @@ -568,11 +590,14 @@ void PiPedalModel::SetOutputVolume(float value) { PreviewOutputVolume(value); { - std::lock_guard lock(mutex); - this->pedalboard.output_volume_db(value); - // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) - std::vector t{subscribers.begin(), subscribers.end()}; - for (auto &subscriber : t) + SubscriberList subscribers; + { + std::lock_guard lock(mutex); + subscribers = this->subscribers; + + this->pedalboard.output_volume_db(value); + } + for (auto &subscriber : subscribers) { subscriber->OnOutputVolumeChanged(value); } @@ -591,8 +616,10 @@ void PiPedalModel::PreviewOutputVolume(float value) void PiPedalModel::SetControl(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value) { + SubscriberList subscribers; { std::lock_guard lock(mutex); + subscribers = this->subscribers; if (!this->pedalboard.SetControlValue(pedalItemId, symbol, value)) { @@ -609,27 +636,28 @@ void PiPedalModel::SetControl(int64_t clientId, int64_t pedalItemId, const std:: return; } PreviewControl(clientId, pedalItemId, symbol, value); - - { - - // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) - std::vector t{subscribers.begin(), subscribers.end()}; - for (auto &subscriber : t) - { - subscriber->OnControlChanged(clientId, pedalItemId, symbol, value); - } - - this->SetPresetChanged(clientId, true); - } } + + for (auto &subscriber : subscribers) + { + subscriber->OnControlChanged(clientId, pedalItemId, symbol, value); + } + + this->SetPresetChanged(clientId, true); } void PiPedalModel::FireJackConfigurationChanged(const JackConfiguration &jackConfiguration) { + + SubscriberList subscribers; + { + std::lock_guard lock(mutex); + subscribers = this->subscribers; + } + // noify subscribers. - std::vector t{subscribers.begin(), subscribers.end()}; - for (auto &subscriber : t) + for (auto &subscriber : subscribers) { subscriber->OnJackConfigurationChanged(jackConfiguration); } @@ -637,9 +665,13 @@ void PiPedalModel::FireJackConfigurationChanged(const JackConfiguration &jackCon void PiPedalModel::FireBanksChanged(int64_t clientId) { + SubscriberList subscribers; + { + std::lock_guard lock(mutex); + subscribers = this->subscribers; + } // noify subscribers. - std::vector t{subscribers.begin(), subscribers.end()}; - for (auto &subscriber : t) + for (auto &subscriber : subscribers) { subscriber->OnBankIndexChanged(this->storage.GetBanks()); } @@ -647,20 +679,25 @@ void PiPedalModel::FireBanksChanged(int64_t clientId) void PiPedalModel::FirePedalboardChanged(int64_t clientId, bool loadAudioThread) { - if (loadAudioThread) + SubscriberList subscribers; { - // notify the audio thread. - if (audioHost && audioHost->IsOpen()) - { - LoadCurrentPedalboard(); + std::lock_guard lock(mutex); + subscribers = this->subscribers; - UpdateRealtimeVuSubscriptions(); - UpdateRealtimeMonitorPortSubscriptions(); + if (loadAudioThread) + { + // notify the audio thread. + if (audioHost && audioHost->IsOpen()) + { + LoadCurrentPedalboard(); + + UpdateRealtimeVuSubscriptions(); + UpdateRealtimeMonitorPortSubscriptions(); + } } } // noify subscribers. - std::vector t{subscribers.begin(), subscribers.end()}; - for (auto &subscriber : t) + for (auto &subscriber : subscribers) { subscriber->OnPedalboardChanged(clientId, this->pedalboard); } @@ -671,45 +708,53 @@ void PiPedalModel::SetPedalboard(int64_t clientId, Pedalboard &pedalboard) std::lock_guard lock(mutex); this->pedalboard = pedalboard; UpdateDefaults(&this->pedalboard); - - this->FirePedalboardChanged(clientId); - this->SetPresetChanged(clientId, true); } + this->FirePedalboardChanged(clientId); + this->SetPresetChanged(clientId, true); } void PiPedalModel::SetSnapshot(int64_t selectedSnapshot) { - std::lock_guard lock(mutex); - if (this->pedalboard.ApplySnapshot(selectedSnapshot, pluginHost)) + bool pedalboardChanged = false; { - this->pedalboard.selectedSnapshot(selectedSnapshot); - for (auto snapshot : this->pedalboard.snapshots()) + std::lock_guard lock(mutex); + if (this->pedalboard.ApplySnapshot(selectedSnapshot, pluginHost)) { - if (snapshot) + this->pedalboard.selectedSnapshot(selectedSnapshot); + for (auto snapshot : this->pedalboard.snapshots()) { - snapshot->isModified_ = false; + if (snapshot) + { + snapshot->isModified_ = false; + } } + pedalboardChanged = true; } + } + if (pedalboardChanged) + { FirePedalboardChanged(-1, true); } } void PiPedalModel::SetSnapshots(std::vector> &snapshots, int64_t selectedSnapshot) { - std::lock_guard lock(mutex); - - UpdateVst3Settings(pedalboard); - - this->pedalboard.snapshots(std::move(snapshots)); - - if (selectedSnapshot != -1) { - this->pedalboard.selectedSnapshot(selectedSnapshot); - for (auto &snapshot : pedalboard.snapshots()) + std::lock_guard lock(mutex); + + UpdateVst3Settings(pedalboard); + + this->pedalboard.snapshots(std::move(snapshots)); + + if (selectedSnapshot != -1) { - if (snapshot) + this->pedalboard.selectedSnapshot(selectedSnapshot); + for (auto &snapshot : pedalboard.snapshots()) { - snapshot->isModified_ = false; + if (snapshot) + { + snapshot->isModified_ = false; + } } } } @@ -745,10 +790,9 @@ void PiPedalModel::UpdateCurrentPedalboard(int64_t clientId, Pedalboard &pedalbo UpdateRealtimeVuSubscriptions(); UpdateRealtimeMonitorPortSubscriptions(); - - this->FirePedalboardChanged(clientId, false); - this->SetPresetChanged(clientId, true); } + this->FirePedalboardChanged(clientId, false); + this->SetPresetChanged(clientId, true); } void PiPedalModel::SetPedalboardItemUseModUi(int64_t clientId, int64_t instanceId, bool enabled) @@ -763,14 +807,17 @@ void PiPedalModel::SetPedalboardItemUseModUi(int64_t clientId, int64_t instanceI { subscriber->OnItemUseModUiChanged(clientId, instanceId, enabled); } - this->SetPresetChanged(clientId, true); } + this->SetPresetChanged(clientId, true); } void PiPedalModel::SetPedalboardItemEnable(int64_t clientId, int64_t pedalItemId, bool enabled) { + SubscriberList subscribers; std::lock_guard guard{mutex}; { + subscribers = this->subscribers; + this->pedalboard.SetItemEnabled(pedalItemId, enabled); PedalboardItem *pPedalboardItem = this->pedalboard.GetItem(pedalItemId); if (pPedalboardItem) @@ -787,18 +834,16 @@ void PiPedalModel::SetPedalboardItemEnable(int64_t clientId, int64_t pedalItemId } } } - - // Notify clients. - std::vector t{subscribers.begin(), subscribers.end()}; - for (auto &subscriber : t) - { - subscriber->OnItemEnabledChanged(clientId, pedalItemId, enabled); - } - this->SetPresetChanged(clientId, true); - // Notify audo thread. - this->audioHost->SetBypass(pedalItemId, enabled); } + this->audioHost->SetBypass(pedalItemId, enabled); + + // Notify clients. + for (auto &subscriber : subscribers) + { + subscriber->OnItemEnabledChanged(clientId, pedalItemId, enabled); + } + this->SetPresetChanged(clientId, true); } void PiPedalModel::GetPresets(PresetIndex *pResult) @@ -845,72 +890,73 @@ void PiPedalModel::SetPresetChanged(int64_t clientId, bool value, bool changeSna void PiPedalModel::FireSnapshotModified(int64_t snapshotIndex, bool modified) { + SubscriberList subscribers; std::lock_guard guard{mutex}; { - // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) - std::vector t{subscribers.begin(), subscribers.end()}; - for (auto &subscriber : t) - { - subscriber->OnSnapshotModified(snapshotIndex, modified); - } + subscribers = this->subscribers; + } + for (auto &subscriber : subscribers) + { + subscriber->OnSnapshotModified(snapshotIndex, modified); } } void PiPedalModel::FireSelectedSnapshotChanged(int64_t selectedSnapshot) { + SubscriberList subscribers; std::lock_guard guard{mutex}; { + subscribers = this->subscribers; // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) - std::vector t{subscribers.begin(), subscribers.end()}; - for (auto &subscriber : t) - { - subscriber->OnSelectedSnapshotChanged(selectedSnapshot); - } + } + for (auto &subscriber : subscribers) + { + subscriber->OnSelectedSnapshotChanged(selectedSnapshot); } } void PiPedalModel::FirePresetChanged(bool changed) { + SubscriberList subscribers; + PresetIndex presets; std::lock_guard guard{mutex}; { + subscribers = this->subscribers; // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) - PresetIndex presets; GetPresets(&presets); - - std::vector t{subscribers.begin(), subscribers.end()}; - for (auto &subscriber : t) - { - subscriber->OnPresetChanged(changed); - } + } + for (auto &subscriber : subscribers) + { + subscriber->OnPresetChanged(changed); } } void PiPedalModel::FirePresetsChanged(int64_t clientId) { + SubscriberList subscribers; + PresetIndex presets; std::lock_guard guard{mutex}; { - - PresetIndex presets; + subscribers = this->subscribers; GetPresets(&presets); - - std::vector t{subscribers.begin(), subscribers.end()}; - for (auto &subscriber : t) - { - subscriber->OnPresetsChanged(clientId, presets); - } + } + for (auto &subscriber : subscribers) + { + subscriber->OnPresetsChanged(clientId, presets); } } void PiPedalModel::FirePluginPresetsChanged(const std::string &pluginUri) { + SubscriberList subscribers; std::lock_guard guard{mutex}; { + subscribers = this->subscribers; // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) - std::vector t{subscribers.begin(), subscribers.end()}; - for (auto &subscriber : t) - { - subscriber->OnPluginPresetsChanged(pluginUri); - } + } + for (auto &subscriber : subscribers) + { + subscriber->OnPluginPresetsChanged(pluginUri); } } @@ -940,10 +986,13 @@ void PiPedalModel::UpdateVst3Settings(Pedalboard &pedalboard) void PiPedalModel::FireLv2StateChanged(int64_t instanceId, const Lv2PluginState &lv2State) { - std::lock_guard guard{mutex}; - std::vector t{subscribers.begin(), subscribers.end()}; + SubscriberList subscribers; + { + std::lock_guard guard{mutex}; + subscribers = this->subscribers; + } - for (auto &subscriber : t) + for (auto &subscriber : subscribers) { subscriber->OnLv2StateChanged(instanceId, lv2State); } @@ -1737,12 +1786,15 @@ void PiPedalModel::OnNotifyMidiValueChanged(int64_t instanceId, int portIndex, f void PiPedalModel::OnNotifyVusSubscription(const std::vector &updates) { - std::lock_guard lock(mutex); + std::vector subscribers; + { + std::lock_guard lock(mutex); + subscribers = this->subscribers; + } for (size_t i = 0; i < updates.size(); ++i) { // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) - std::vector t{subscribers.begin(), subscribers.end()}; - for (auto &subscriber : t) + for (auto &subscriber : subscribers) { subscriber->OnVuMeterUpdate(updates); } @@ -1797,10 +1849,10 @@ int64_t PiPedalModel::MonitorPort(int64_t instanceId, const std::string &key, fl int64_t subscriptionId = ++nextSubscriptionId; activeMonitorPortSubscriptions.push_back( MonitorPortSubscription{ - subscriptionId, - instanceId, - key, - updateInterval, + subscriptionId, + instanceId, + key, + updateInterval, onUpdate}); UpdateRealtimeMonitorPortSubscriptions(); @@ -2513,28 +2565,38 @@ void PiPedalModel::DeleteMidiListeners(int64_t clientId) void PiPedalModel::OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetProperty, const LV2_Atom *atomValue) { - std::lock_guard lock(mutex); - - std::string propertyUri = pluginHost.GetMapFeature().UridToString(patchSetProperty); - + std::vector subscribers; + std::vector atomOutputListeners; + std::string propertyUri; { - PedalboardItem *item = pedalboard.GetItem((int64_t)instanceId); - if (item == nullptr) - return; - atom_object atomObject{atomValue}; + std::lock_guard lock(mutex); - PedalboardItem::PropertyMap &properties = item->PatchProperties(); - if (properties.contains(propertyUri) && properties[propertyUri] == atomObject) - { - // do noting. - } - else - { - properties[propertyUri] = std::move(atomObject); - } + subscribers = this->subscribers; + atomOutputListeners = this->atomOutputListeners; + propertyUri = pluginHost.GetMapFeature().UridToString(patchSetProperty); - OnNotifyMaybeLv2StateChanged(instanceId); + { + PedalboardItem *item = pedalboard.GetItem((int64_t)instanceId); + if (item == nullptr) + return; + atom_object atomObject{atomValue}; + + PedalboardItem::PropertyMap &properties = item->PatchProperties(); + if (properties.contains(propertyUri) && properties[propertyUri] == atomObject) + { + // do noting. + } + else + { + properties[propertyUri] = std::move(atomObject); + } + } + if (audioHost) + { + audioHost->SetListenForAtomOutput(atomOutputListeners.size() != 0); + } } + OnNotifyMaybeLv2StateChanged(instanceId); bool hasAtomJson = false; std::string atomJson; @@ -2561,10 +2623,6 @@ void PiPedalModel::OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetPropert } } } - if (audioHost) - { - audioHost->SetListenForAtomOutput(atomOutputListeners.size() != 0); - } } void PiPedalModel::OnNotifyPathPatchPropertyReceived( @@ -3451,4 +3509,29 @@ std::string PiPedalModel::Tone3000ThumbnailDirectory() return "/var/pipedal/tone3000_thumbnails"; } +void PiPedalModel::EnableUpdater(bool enable) +{ + this->updaterEnabled = enable; +} +static bool IsSafeMediaPath(const fs::path path) +{ + if (!HtmlHelper::IsSafeFileName(path)) + { + return false; + } + if (!(path.string().starts_with("/var/pipedal/audio_uploads"))) + { + return false; + } + return true; +} + +void PiPedalModel::WriteTone3000Readme(const std::filesystem::path &filePath, const tone3000::Tone &tone, const std::string &thumbnailUrl) +{ + if (!IsSafeMediaPath(filePath)) + { + throw std::runtime_error("Invalid media path."); + } + tone3000::WriteTone3000Readme(filePath, tone, thumbnailUrl); +} diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index d62d84b..2e14f5f 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -44,6 +44,7 @@ #include #include "Tone3000Downloader.hpp" #include "Uri.hpp" +#include "Tone3000Tone.hpp" namespace pipedal { @@ -94,7 +95,7 @@ namespace pipedal // virtual void OnPatchPropertyChanged(int64_t clientId, int64_t instanceId,const std::string& propertyUri,const json_variant& value) = 0; virtual void OnErrorMessage(const std::string &message) = 0; - + virtual void OnTone3000DownloadStarted(int64_t handle, const std::string &title) = 0; virtual void OnTone3000DownloadProgress(const Tone3000DownloadProgress &progress) = 0; virtual void OnTone3000DownloadComplete(int64_t handle, const std::string &resultPath) = 0; @@ -119,6 +120,7 @@ namespace pipedal using NetworkChangedListener = std::function; private: + bool updaterEnabled = true; PedalboardItem *GetPedalboardItemForFileProperty(const UiFileProperty &fileProperty); // Tone3000Downloader::Listener implementation @@ -127,7 +129,6 @@ namespace pipedal virtual void OnTone3000DownloadComplete(int64_t handle, const std::string &resultPath) override; virtual void OnTone3000DownloadError(int64_t handle, const std::string &errorMessage) override; - std::shared_ptr tone3000Downloader; void CancelAudioRetry(); clock::time_point lastRestartTime = clock::time_point::min(); @@ -211,7 +212,8 @@ namespace pipedal std::shared_ptr lv2Pedalboard; std::filesystem::path webRoot; - std::vector> subscribers; + using SubscriberList = std::vector>; + SubscriberList subscribers; void SetPresetChanged(int64_t clientId, bool value, bool changeSnapshotSelect = true); void FireSnapshotModified(int64_t snapshotIndex, bool modified); void FireSelectedSnapshotChanged(int64_t selectedSnapshot); @@ -296,13 +298,12 @@ namespace pipedal Decrease }; - + void EnableUpdater(bool enable); Storage &GetStorage() { return storage; } virtual std::string Tone3000ThumbnailDirectory() override; - bool GetHasWifi(); void NextBank(Direction direction = Direction::Increase); @@ -311,16 +312,14 @@ namespace pipedal void PreviousPreset() { NextPreset(Direction::Decrease); } int64_t DownloadModelsFromTone3000( - const std::string&responseuri, - const Tone3000PkceParams& pkce, + const std::string &responseuri, + const Tone3000PkceParams &pkce, const std::string &downloadPath, - Tone3000DownloadType downloadType - ); + Tone3000DownloadType downloadType); void CancelTone3000Download( int64_t clientId, - int64_t downloadHandle - ); + int64_t downloadHandle); void RequestShutdown(bool restart); virtual PostHandle Post(PostCallback &&fn); @@ -530,6 +529,7 @@ namespace pipedal void SetChannelRouterSettings(int64_t clientId, ChannelRouterSettings::ptr &settings); ChannelRouterSettings::ptr GetChannelRouterSettings(); + void WriteTone3000Readme(const std::filesystem::path &filePath, const tone3000::Tone &tone, const std::string &thumbnailUrl); }; } // namespace pipedal. \ No newline at end of file diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index 71abf71..cc6df5a 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -36,6 +36,7 @@ #include "Promise.hpp" #include #include "Tone3000Downloader.hpp" +#include "Tone3000Tone.hpp" #include "AdminClient.hpp" #include "WifiConfigSettings.hpp" @@ -62,6 +63,20 @@ JSON_MAP_REFERENCE(CopyPresetsToBankBody, bankInstanceId) JSON_MAP_REFERENCE(CopyPresetsToBankBody, presets) JSON_MAP_END(); +class WriteTone3000ReadmeBody { +public: + std::string filePath_; + tone3000::Tone tone_; + std::string thumbnailUrl_; + DECLARE_JSON_MAP(WriteTone3000ReadmeBody); +}; + +JSON_MAP_BEGIN(WriteTone3000ReadmeBody) +JSON_MAP_REFERENCE(WriteTone3000ReadmeBody, filePath) +JSON_MAP_REFERENCE(WriteTone3000ReadmeBody, tone) +JSON_MAP_REFERENCE(WriteTone3000ReadmeBody, thumbnailUrl) +JSON_MAP_END(); + class DownloadModelsFromTone3000Body { public: @@ -1193,6 +1208,15 @@ public: } REGISTER_MESSAGE_HANDLER(makeTone3000Pkce); + + void handle_writeTone3000Readme(int replyTo, json_reader*pReader) + { + WriteTone3000ReadmeBody body; + pReader->read(&body); + model.WriteTone3000Readme(body.filePath_, body.tone_,body.thumbnailUrl_); + } + REGISTER_MESSAGE_HANDLER(writeTone3000Readme) + void handle_sha256Base64url(int replyTo, json_reader *pReader) { std::string input; diff --git a/src/PluginHost.cpp b/src/PluginHost.cpp index 09a3b5a..4eee514 100644 --- a/src/PluginHost.cpp +++ b/src/PluginHost.cpp @@ -125,6 +125,7 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld) core__isSideChain = lilv_new_uri(pWorld, LV2_CORE_PREFIX "isSideChain"); // missing in lv2.h portprops__not_on_gui_property_uri = lilv_new_uri(pWorld, LV2_PORT_PROPS__notOnGUI); portprops__trigger = lilv_new_uri(pWorld, LV2_PORT_PROPS__trigger); + portprops__expensive = lilv_new_uri(pWorld, LV2_PORT_PROPS__expensive); midi__event = lilv_new_uri(pWorld, LV2_MIDI__MidiEvent); core__designation = lilv_new_uri(pWorld, LV2_CORE__designation); portgroups__group = lilv_new_uri(pWorld, LV2_PORT_GROUPS__group); @@ -1265,6 +1266,7 @@ 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->is_expensive_ = lilv_port_has_property(plugin, pPort, host->lilvUris->portprops__expensive); 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); @@ -2321,6 +2323,7 @@ json_map::storage_type Lv2PortInfo::jmap{ MAP_REF(Lv2PortInfo, toggled_property), MAP_REF(Lv2PortInfo, mod_momentaryOffByDefault), MAP_REF(Lv2PortInfo, mod_momentaryOnByDefault), + MAP_REF(Lv2PortInfo, is_expensive), MAP_REF(Lv2PortInfo, pipedal_graphicEq), MAP_REF(Lv2PortInfo, not_on_gui), MAP_REF(Lv2PortInfo, buffer_type), @@ -2408,6 +2411,7 @@ json_map::storage_type Lv2PluginUiPort::jmap{{ MAP_REF(Lv2PluginUiPort, mod_momentaryOffByDefault), MAP_REF(Lv2PluginUiPort, mod_momentaryOnByDefault), + MAP_REF(Lv2PluginUiPort, is_expensive), MAP_REF(Lv2PluginUiPort, pipedal_graphicEq), MAP_REF(Lv2PluginUiPort, enumeration_property), MAP_REF(Lv2PluginUiPort, not_on_gui), diff --git a/src/PluginHost.hpp b/src/PluginHost.hpp index 5b31745..9ea0762 100644 --- a/src/PluginHost.hpp +++ b/src/PluginHost.hpp @@ -226,6 +226,7 @@ namespace pipedal bool mod_momentaryOffByDefault_ = false; bool mod_momentaryOnByDefault_ = false; + bool is_expensive_ = false; bool pipedal_graphicEq_ = false; bool not_on_gui_ = false; @@ -304,6 +305,7 @@ namespace pipedal LV2_PROPERTY_GETSET_SCALAR(integer_property); LV2_PROPERTY_GETSET_SCALAR(mod_momentaryOffByDefault); LV2_PROPERTY_GETSET_SCALAR(mod_momentaryOnByDefault); + LV2_PROPERTY_GETSET_SCALAR(is_expensive); LV2_PROPERTY_GETSET_SCALAR(pipedal_graphicEq); LV2_PROPERTY_GETSET_SCALAR(enumeration_property); LV2_PROPERTY_GETSET_SCALAR(toggled_property); @@ -591,6 +593,7 @@ namespace pipedal integer_property_(pPort->integer_property()), mod_momentaryOffByDefault_(pPort->mod_momentaryOffByDefault()), mod_momentaryOnByDefault_(pPort->mod_momentaryOnByDefault()), + is_expensive_(pPort->is_expensive()), pipedal_graphicEq_(pPort->pipedal_graphicEq()), enumeration_property_(pPort->enumeration_property()), @@ -641,6 +644,7 @@ namespace pipedal bool mod_momentaryOffByDefault_ = false; bool mod_momentaryOnByDefault_ = false; + bool is_expensive_ = false; bool pipedal_graphicEq_ = false; bool enumeration_property_ = false; @@ -671,6 +675,10 @@ namespace pipedal LV2_PROPERTY_GETSET_SCALAR(display_priority); LV2_PROPERTY_GETSET_SCALAR(is_logarithmic); LV2_PROPERTY_GETSET_SCALAR(integer_property); + LV2_PROPERTY_GETSET_SCALAR(mod_momentaryOffByDefault); + LV2_PROPERTY_GETSET_SCALAR(mod_momentaryOnByDefault); + LV2_PROPERTY_GETSET_SCALAR(is_expensive); + LV2_PROPERTY_GETSET_SCALAR(pipedal_graphicEq); LV2_PROPERTY_GETSET_SCALAR(enumeration_property); LV2_PROPERTY_GETSET_SCALAR(toggled_property); LV2_PROPERTY_GETSET_SCALAR(trigger_property); @@ -798,6 +806,7 @@ namespace pipedal AutoLilvNode core__isSideChain; AutoLilvNode portprops__not_on_gui_property_uri; AutoLilvNode portprops__trigger; + AutoLilvNode portprops__expensive; AutoLilvNode midi__event; AutoLilvNode core__designation; AutoLilvNode portgroups__group; diff --git a/src/Tone3000Tone.cpp b/src/Tone3000Tone.cpp new file mode 100644 index 0000000..a5ef391 --- /dev/null +++ b/src/Tone3000Tone.cpp @@ -0,0 +1,487 @@ +/* + * Copyright (c) 2026 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 "Tone3000Tone.hpp" +#include "ss.hpp" + + + +using namespace pipedal; +using namespace pipedal::tone3000; +namespace fs = std::filesystem; + + +Tone::Tone(const std::string &json) +{ + std::istringstream ss(json); + json_reader reader(ss); + reader.read(this); +} + +namespace +{ + static std::string mdSanitize(const std::string &str, const std::string &illegalCharacters = "") + { + std::ostringstream os; + for (char c : str) + { + if ((unsigned char)c < 0x20) + continue; + if (c == '<') + { + os << "<"; + continue; + } + auto npos = illegalCharacters.find_first_of(c); + if (npos != std::string::npos) + { + continue; + } + os << c; + } + return os.str(); + } + + static std::string mdDate(const tone3000_time_point &date_) + { + std::ostringstream os; + // C++ doesn't handle timezones. Just zap the timezone, and replace it with "Z" + std::string date = date_; + + // Remove timezone offset if present and replace with Z + size_t plusPos = date.find('+'); + if (plusPos != std::string::npos) + { + date = date.substr(0, plusPos) + "Z"; + } + // Parse ISO 8601 date string into tm + std::tm tm = {}; + std::istringstream ss(date); + ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S"); + if (ss.fail()) + { + os << date; + return os.str(); + } + + os << std::put_time(&tm, "%x"); + return os.str(); + } + + static std::string mdEnum(const std::string &value, const std::map &enumValues) + { + auto ff = enumValues.find(value); + if (ff == enumValues.end()) + { + return value; + } + return ff->second; + } + + static std::string mdEnumList( + const std::vector &values, + const std::map &enumValues) + { + if (values.size() == 1) + { + return mdEnum(values[0], enumValues); + } + std::ostringstream os; + os << '['; + bool first = true; + for (const auto &value : values) + { + if (first) + { + first = false; + } + else + { + os << ", "; + } + os << mdEnum(value, enumValues); + } + os << ']'; + return os.str(); + } + + static std::map sizeEnumValues = + { + {"standard", "Standard"}, + {"lite", "Lite"}, + {"feather", "Feather"}, + {"nano", "Nano"}, + {"custom", "Custom"}}; + static std::string mdSizes(const std::vector &sizes) + { + return mdEnumList(sizes, sizeEnumValues); + } + + static std::map gearEnumValues = + { + {"amp", "Amp only"}, + {"full-rig", "Full rig"}, + {"pedal", "Pedal"}, + {"outboard", "Outboard"}, + {"ir", "I/R"}}; + + static std::string mdGear(const std::string &gear) + { + return mdEnum(gear, gearEnumValues); + } + + namespace + { + enum class LicenseFlags + { + None = 0, + Cc = 1, + By = 2, + CcBy = 3, // = Cc | By + Sa = 4, + Nc = 8, + Nd = 16, + Cc0 = 32, + }; + + static bool operator&(LicenseFlags v1, LicenseFlags v2) + { + return (((int)v1) & ((int)v2)) != 0; + } + static LicenseFlags operator|(LicenseFlags v1, LicenseFlags v2) + { + return (LicenseFlags)(((int)(v1)) | ((int)(v2))); + } + + struct LicenseInfo + { + std::string key; + std::string displayName; + std::string url; + LicenseFlags licenseFlags; + }; + } + + static std::vector licenses{ + {.key = "t3k", + .displayName = "T3K", + .url = "licenses/t3k_license.html", + .licenseFlags = LicenseFlags::None}, + {.key = "cc-by", + .displayName = "CC BY 4.0", + .url = "https://creativecommons.org/licenses/by/4.0/", + .licenseFlags = LicenseFlags::CcBy}, + {.key = "cc-by-sa", + .displayName = "CC BY-SA 4.0", + .url = "https://creativecommons.org/licenses/by-sa/4.0/", + .licenseFlags = LicenseFlags::CcBy | LicenseFlags::Sa}, + {.key = "cc-by-nc", + .displayName = "CC BY-NC 4.0", + .url = "https://creativecommons.org/licenses/by-nc/4.0/", + .licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nc}, + {.key = "cc-by-nc-sa", + .displayName = "CC BY-NC-SA 4.0", + .url = "https://creativecommons.org/licenses/by-nc-sa/4.0/", + .licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nc | LicenseFlags::Sa}, + {.key = "cc-by-nd", + .displayName = "CC BY-ND 4.0", + .url = "https://creativecommons.org/licenses/by-nd/4.0/", + .licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nd}, + {.key = "cc-by-nc-nd", + .displayName = "CC BY-NC-ND 4.0", + .url = "https://creativecommons.org/licenses/by-nc-nd/4.0/", + .licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nc | LicenseFlags::Nd}, + { + .key = "cco", + .displayName = "CC0", + .url = "https://creativecommons.org/publicdomain/zero/1.0/", + .licenseFlags = LicenseFlags::Cc | LicenseFlags::Cc0, + + }, + }; + + static std::map makeLicenseEnum() + { + std::map result; + for (const auto &license : licenses) + { + result[license.key] = license; + } + return result; + } + + static std::map licenseEnum = makeLicenseEnum(); + + static std::string mdLicenseIcon(LicenseFlags licenseFlag) + { + std::ostringstream os; + std::string url; + + switch (licenseFlag) + { + case LicenseFlags::Cc: + url = "img/cc.svg"; + break; + case LicenseFlags::By: + url = "img/by.svg"; + break; + case LicenseFlags::Sa: + url = "img/sa.svg"; + break; + case LicenseFlags::Nc: + url = "img/nc.svg"; + break; + case LicenseFlags::Nd: + url = "img/nd.svg"; + break; + case LicenseFlags::Cc0: + url = "img/cc0.svg"; + break; + + case LicenseFlags::None: + throw std::runtime_error("Invalid argument."); + } + os << ""; + return os.str(); + } + + static void mdEscapeString(std::ostream &f, const std::string &str) + { + size_t ix = 0; + while (ix < str.size()) + { + char c = str[ix++]; + if (c == '\n') + { + if (ix < str.size() && str[ix] == '\n') + { + f << "\n\n"; + } + else + { + f << " \n"; // a
in md. + } + } + else + { + f << c; + } + } + } + + static std::string mdHref(const std::string &url) + { + std::ostringstream os; + os << '\''; + for (char c : url) + { + if (c == '\'') + { + continue; + } + os << c; + } + os << '\''; + return os.str(); + } + static std::string mdLicense(const std::string &license) + { + + auto ff = licenseEnum.find(license); + LicenseInfo licenseInfo; + if (ff != licenseEnum.end()) + { + licenseInfo = ff->second; + } + else + { + licenseInfo.displayName = license; + } + + std::ostringstream os; + + if (licenseInfo.licenseFlags != LicenseFlags::None) + { + for (LicenseFlags licenseFlag : std::vector{LicenseFlags::Cc, LicenseFlags::By, LicenseFlags::Cc0, LicenseFlags::Nc, LicenseFlags::Sa, LicenseFlags::Nd}) + { + if (licenseInfo.licenseFlags & licenseFlag) + { + os << mdLicenseIcon(licenseFlag); + } + } + os << " "; + } + + if (licenseInfo.url != "") + { + os << "" << mdSanitize(licenseInfo.displayName) << ""; + } + else + { + os << mdSanitize(licenseInfo.displayName); + } + return os.str(); + } + // static std::string mdTestLicenses() + // { + // std::ostringstream os; + // for (auto license: licenseEnum) + // { + // os << "
" << mdLicense(license.first) << "\n"; + // } + // return os.str(); + // } + static std::map platformEnumValues = + { + {"nam", "NAM"}, + {"ir", "I/R"}, + {"aida-x", "Aida X"}, + {"aa-snapshot", "aa-snapshot"}, + {"proteus", "Proteus"}}; + static std::string mdPlatform(const std::string &platform) + { + return mdEnum(platform, platformEnumValues); + } + static std::string mdUser(const tone3000::User &user) + { + return mdSanitize(user.username()); + } + + static void mdLink(std::ostream &f, const std::string &label, const std::string &url) + { + f << "[" << mdSanitize(label, "]") << "](" << mdSanitize(url, ")") << ")"; + } + +}; + +void pipedal::tone3000::WriteTone3000Readme(const std::filesystem::path &filePath, const tone3000::Tone &tone, const std::string &thumbnailUrl_) +{ + std::string thumbnailUrl = SS("/var/tone3000_thumbnail?id=" << tone.id()); + std::ofstream of{filePath}; + if (!of.is_open()) + { + throw std::runtime_error(SS("Unable to create file " << filePath)); + } + + // clang-format off + of << + + "
\n"; + if (!thumbnailUrl.empty()) + { + + of << " \n"; + } + of << "
\n" + << "

" << mdSanitize(tone.title()) << "

\n" + << "
\n" + << "

\n" + << mdDate(tone.updated_at()) + << " " + << mdUser(tone.user()) + << "
\n" + << " "; + if (tone.sizes().has_value() && tone.sizes().value().size() != 0) { + of + << mdSizes(tone.sizes().value()) << ", " + ; + } + if (tone.platform() == "ir") + { + of + << (mdPlatform(tone.platform())); + } else { + of + << mdGear(tone.gear()) << ", " + << mdPlatform(tone.platform()); + } + of << "
\n"; + if (tone.license() != "") { + of << " License: " << mdLicense(tone.license()) << "\n"; + } + of + // << " " << mdTestLicenses() + << "

\n" + << "
\n" + << "
\n" + << "
\n"; + // clang-format on + of << "\n\n"; + mdEscapeString(of, tone.description()); + + if (tone.links().size() > 0) + { + of << std::endl; + of << "## Links" << std::endl + << std::endl; + for (auto &link : tone.links()) + { + of << "- " << "[" << link << "](" << link << ")" << std::endl; + } + } +} + +JSON_MAP_BEGIN(Model) +JSON_MAP_REFERENCE(Model, id) +JSON_MAP_REFERENCE(Model, name) +JSON_MAP_REFERENCE(Model, model_url) +JSON_MAP_REFERENCE(Model, created_at) +JSON_MAP_REFERENCE(Model, size) +JSON_MAP_REFERENCE(Model, user_id) +JSON_MAP_END() + +JSON_MAP_BEGIN(User) +JSON_MAP_REFERENCE(User, id) +JSON_MAP_REFERENCE(User, username) +JSON_MAP_REFERENCE(User, avatar_url) +JSON_MAP_REFERENCE(User, url) +JSON_MAP_END() + +JSON_MAP_BEGIN(Tone) +JSON_MAP_REFERENCE(Tone, id) +JSON_MAP_REFERENCE(Tone, user_id) +JSON_MAP_REFERENCE(Tone, title) +JSON_MAP_REFERENCE(Tone, description) +JSON_MAP_REFERENCE(Tone, created_at) +JSON_MAP_REFERENCE(Tone, updated_at) +JSON_MAP_REFERENCE(Tone, gear) +JSON_MAP_REFERENCE(Tone, images) +JSON_MAP_REFERENCE(Tone, is_public) +JSON_MAP_REFERENCE(Tone, links) +JSON_MAP_REFERENCE(Tone, platform) +JSON_MAP_REFERENCE(Tone, models_count) +JSON_MAP_REFERENCE(Tone, favorites_count) +JSON_MAP_REFERENCE(Tone, downloads_count) +JSON_MAP_REFERENCE(Tone, license) +JSON_MAP_REFERENCE(Tone, sizes) +JSON_MAP_REFERENCE(Tone, a1_models_count) +JSON_MAP_REFERENCE(Tone, a2_models_count) +JSON_MAP_REFERENCE(Tone, irs_count) +JSON_MAP_REFERENCE(Tone, custom_models_count) +JSON_MAP_REFERENCE(Tone, user) +JSON_MAP_REFERENCE(Tone, url) +JSON_MAP_REFERENCE(Tone, user) +JSON_MAP_END(); diff --git a/src/Tone3000Tone.hpp b/src/Tone3000Tone.hpp new file mode 100644 index 0000000..efab74a --- /dev/null +++ b/src/Tone3000Tone.hpp @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2026 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 +#include +#include "json.hpp" +#include +#include +#include +#include "Tone3000DownloadProgress.hpp" +#include + +namespace pipedal +{ + namespace tone3000 + { + + using tone3000_time_point = std::string; // Date in ISO 8601 format. (C++ doesn't handle timezones) + + class Model + { + private: + int64_t id_ = -1; + std::string name_; + std::string model_url_; + tone3000_time_point created_at_; + std::optional size_; + std::string user_id_; + + public: + JSON_GETTER_SETTER(id) + JSON_GETTER_SETTER_REF(name) + JSON_GETTER_SETTER_REF(model_url) + JSON_GETTER_SETTER_REF(created_at) + JSON_GETTER_SETTER_REF(size) + JSON_GETTER_SETTER_REF(user_id) + + DECLARE_JSON_MAP(Model); + }; + + class User + { + private: + std::string id_; + std::optional avatar_url_; + std::string username_; + std::string url_; + + public: + JSON_GETTER_SETTER_REF(id) + JSON_GETTER_SETTER(username) + JSON_GETTER_SETTER(url) + + DECLARE_JSON_MAP(User); + }; + + class Tone + { + private: + int64_t id_ = -1; + std::string user_id_; + std::string title_; + std::string description_; + tone3000_time_point created_at_; + tone3000_time_point updated_at_; + std::string gear_; + std::vector images_; + bool is_public_ = true; + std::vector links_; + std::string platform_; + int64_t models_count_ = 0; + int64_t favorites_count_ = 0; + int64_t downloads_count_ = 0; + std::string license_; + std::optional> sizes_; + int64_t a1_models_count_ = 0; + int64_t a2_models_count_ = 0; + int64_t irs_count_ = 0; + int64_t custom_models_count_ = 0; + User user_; + std::string url_; + + public: + Tone() = default; + Tone(const std::string &json); + + JSON_GETTER_SETTER(id) + JSON_GETTER_SETTER_REF(user_id) + JSON_GETTER_SETTER_REF(title) + JSON_GETTER_SETTER_REF(description) + JSON_GETTER_SETTER_REF(created_at) + JSON_GETTER_SETTER_REF(updated_at) + JSON_GETTER_SETTER_REF(gear) + JSON_GETTER_SETTER_REF(images) + JSON_GETTER_SETTER(is_public) + JSON_GETTER_SETTER_REF(links) + JSON_GETTER_SETTER_REF(platform) + JSON_GETTER_SETTER(models_count) + JSON_GETTER_SETTER(favorites_count) + JSON_GETTER_SETTER(downloads_count) + JSON_GETTER_SETTER_REF(license) + JSON_GETTER_SETTER_REF(sizes) + JSON_GETTER_SETTER_REF(a1_models_count) + JSON_GETTER_SETTER_REF(a2_models_count) + JSON_GETTER_SETTER_REF(irs_count) + JSON_GETTER_SETTER_REF(custom_models_count) + JSON_GETTER_SETTER_REF(user) + JSON_GETTER_SETTER_REF(url) + + DECLARE_JSON_MAP(Tone); + }; + + void WriteTone3000Readme(const std::filesystem::path &filePath, const tone3000::Tone &tone, const std::string &thumbnailUrl); + + + } +} diff --git a/src/WebServerConfig.cpp b/src/WebServerConfig.cpp index b711322..6ba1b74 100644 --- a/src/WebServerConfig.cpp +++ b/src/WebServerConfig.cpp @@ -86,7 +86,23 @@ static bool IsSafeMediaPath(const fs::path path) if (!HtmlHelper::IsSafeFileName(path)) { return false; } - if (!(path.string().starts_with("/var/pipedal/audio_uploads"))) + if (!(path.string().starts_with("/var/pipedal/audio_uploads/"))) + { + return false; + } + return true; +} +static bool IsSafeThumbnailPath(const fs::path path) +{ + if (!HtmlHelper::IsSafeFileName(path)) { + return false; + } + if (!(path.string().starts_with("/var/pipedal/tone3000_thumbnails/"))) + { + return false; + } + auto extension = path.extension(); + if (extension != ".jpg" && extension != ".jpeg" && extension != ".png" && extension != ".webm") { return false; } @@ -383,6 +399,10 @@ public: { throw PiPedalException("Can't download files of this type."); } + if (!mimeType.starts_with("image/")) + { + throw PiPedalException("Can't download files of this type."); + } res.set(HttpField::content_type, mimeType); size_t contentLength = std::filesystem::file_size(path); res.setContentLength(contentLength); @@ -984,7 +1004,7 @@ public: { throw std::runtime_error("Invalid path"); } - if (!IsSafeMediaPath(targetPath)) + if (!IsSafeMediaPath(targetPath) && !IsSafeThumbnailPath(targetPath)) { throw std::runtime_error("Unsafe path."); } diff --git a/src/profilePluginMain.cpp b/src/profilePluginMain.cpp index 215db9d..4c89cef 100644 --- a/src/profilePluginMain.cpp +++ b/src/profilePluginMain.cpp @@ -132,7 +132,9 @@ void profilePlugin(const ProfileOptions &profileOptions) throw std::runtime_error(s.str()); } + model.EnableUpdater(false); model.Init(configuration); + model.GetPluginHost().asIHost()->SetMaxAudioBufferSize(64); model.LoadLv2PluginInfo(); diff --git a/vite/public/var/config.json b/vite/public/var/config.json index 758c227..ed6f056 100644 --- a/vite/public/var/config.json +++ b/vite/public/var/config.json @@ -7,6 +7,6 @@ "ui_plugins": [], "enable_auto_update": true, "has_wifi_device": false, - "tone3000_A2_models": false, + "tone3000_A2_models": true, "end": true } \ No newline at end of file diff --git a/vite/src/pipedal/ControlViewFactory.tsx b/vite/src/pipedal/ControlViewFactory.tsx index 0de9746..2dd5857 100644 --- a/vite/src/pipedal/ControlViewFactory.tsx +++ b/vite/src/pipedal/ControlViewFactory.tsx @@ -1,4 +1,5 @@ // Copyright (c) 2022 Robin Davies +// Copyright (c) Fulgencio Ruiz Rubio. // // 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/vite/src/pipedal/FilePropertyDialog.tsx b/vite/src/pipedal/FilePropertyDialog.tsx index bbd1536..cda47c8 100644 --- a/vite/src/pipedal/FilePropertyDialog.tsx +++ b/vite/src/pipedal/FilePropertyDialog.tsx @@ -572,8 +572,12 @@ export default withStyles( return; } if (resultPath.startsWith(this.state.navDirectory)) { + let selectedFile = resultPath; + if (selectedFile.endsWith("/")) { + selectedFile = selectedFile.slice(0, -1); + } this.setState({ - selectedFile: resultPath, + selectedFile: selectedFile, }); this.requestFiles(this.state.navDirectory); } diff --git a/vite/src/pipedal/Lv2Plugin.tsx b/vite/src/pipedal/Lv2Plugin.tsx index c1c0bba..beb6f34 100644 --- a/vite/src/pipedal/Lv2Plugin.tsx +++ b/vite/src/pipedal/Lv2Plugin.tsx @@ -593,6 +593,7 @@ export class UiControl implements Deserializable { this.max_value = input.max_value; this.default_value = input.default_value; this.is_logarithmic = input.is_logarithmic; + this.is_expensive = input.is_expensive; this.display_priority = input.display_priority; this.range_steps = input.range_steps; this.integer_property = input.integer_property; @@ -715,6 +716,7 @@ export class UiControl implements Deserializable { integer_property: boolean = false; mod_momentaryOffByDefault: boolean = false; mod_momentaryOnByDefault: boolean = false; + is_expensive: boolean = false; pipedal_graphicEq: boolean = false; enumeration_property: boolean = false; trigger_property: boolean = false; diff --git a/vite/src/pipedal/MainPage.tsx b/vite/src/pipedal/MainPage.tsx index 176b85d..613f451 100644 --- a/vite/src/pipedal/MainPage.tsx +++ b/vite/src/pipedal/MainPage.tsx @@ -427,7 +427,6 @@ export const MainPage = let title = ""; let author = ""; let infoPluginUri = ""; - let presetsUri = ""; let missing = false; let canEditTitle = false; let modGuiButtonVisible = false; @@ -439,7 +438,6 @@ export const MainPage = } else if (pedalboardItem.isSyntheticItem()) { title = pedalboardItem.pluginName ?? "#error"; author = ""; - presetsUri = ""; infoPluginUri = ""; } else { @@ -456,7 +454,6 @@ export const MainPage = title = uiPlugin.name; author = uiPlugin.author_name; } - presetsUri = uiPlugin.uri; // if (uiPlugin.description.length > 20) { // } infoPluginUri = uiPlugin.uri; @@ -544,10 +541,6 @@ export const MainPage =
-
- -
{modGuiButtonVisible && (
@@ -634,6 +627,7 @@ export const MainPage = canShowModUi = this.canShowModUi(pedalboardItem); canDelete = pedalboard.canDeleteItem(pedalboardItem.instanceId); instanceId = pedalboardItem.instanceId; + pluginUri = pedalboardItem.uri; if (pedalboardItem.isEmpty()) { canInsert = true; canAppend = true; @@ -699,7 +693,12 @@ export const MainPage =
{this.props.enableStructureEditing && (
-
+
+ +
+ +
{ this.onAddClick(e) }} size="large"> @@ -718,7 +717,7 @@ export const MainPage = {canAppend && ( this.onAppendSplit(instanceId)}>Append split)}
-
+
{ this.onDeletePedal(pedalboardItem?.instanceId ?? -1) }} size="large"> diff --git a/vite/src/pipedal/Pedalboard.tsx b/vite/src/pipedal/Pedalboard.tsx index 0667f1c..3ee1777 100644 --- a/vite/src/pipedal/Pedalboard.tsx +++ b/vite/src/pipedal/Pedalboard.tsx @@ -59,6 +59,9 @@ export class ControlValue implements Deserializable { } export class PedalboardItem implements Deserializable { + clone() { + return new PedalboardItem().deserialize(this); + } deserializePedalboardItem(input: any): PedalboardItem { this.instanceId = input.instanceId ?? -1; this.title = input.title ?? ""; @@ -753,11 +756,14 @@ export class Pedalboard implements Deserializable { replaceItem(instanceId: number, newItem: PedalboardItem) { + newItem.instanceId = ++this.nextInstanceId; + let result = this._replaceItem(this.items,instanceId,newItem); if (!result) { throw new PiPedalArgumentError("instanceId not found."); } + return newItem.instanceId; } private _addItem(items: PedalboardItem[], newItem: PedalboardItem, instanceId: number, append: boolean) { diff --git a/vite/src/pipedal/PiPedalModel.tsx b/vite/src/pipedal/PiPedalModel.tsx index 69fe1c2..bc2868f 100644 --- a/vite/src/pipedal/PiPedalModel.tsx +++ b/vite/src/pipedal/PiPedalModel.tsx @@ -50,6 +50,7 @@ import { AlsaSequencerConfiguration, AlsaSequencerPortSelection } from './AlsaSe import { getDefaultModGuiPreference } from './ModGuiHost'; import ChannelRouterSettings from './ChannelRouterSettings'; import Tone3000DownloadProgress from './Tone3000DownloadProgress'; +import { Tone } from './t3k/types'; export enum State { @@ -1331,6 +1332,23 @@ export class PiPedalModel //implements PiPedalModel } } + async writeTone3000Readme( + filePath: string, + tone: Tone, + thumbnailUrl: string + + ): Promise { + if (!this.webSocket) { + throw new Error("Server disconnected."); + } + await this.webSocket.request( + "writeTone3000Readme", + { + filePath: filePath, + tone: tone, + thumbnailUrl: thumbnailUrl + }); + } maxFileUploadSize: number = 512 * 1024 * 1024; maxPresetUploadSize: number = 1024 * 1024; @@ -2058,7 +2076,10 @@ export class PiPedalModel //implements PiPedalModel previewPedalboardValue(instanceId: number, key: string, value: number): void { // mouse is down. Don't update EVERYBODY, but we must change // the control on the running audio plugin. - // TODO: respect "expensive" port attribute. + + // respect "expensive" port attribute. + + // Handle special cases for input/output volume if (instanceId === Pedalboard.START_CONTROL_ID && key === "volume_db") { this.previewInputVolume(value); return; @@ -2067,6 +2088,23 @@ export class PiPedalModel //implements PiPedalModel return; } + // Get the control info to check if it's expensive + let pedalboard = this.pedalboard.get(); + if (pedalboard) { + let item = pedalboard.tryGetItem(instanceId); + if (item) { + let plugin = this.getUiPlugin(item.uri); + if (plugin) { + let control = plugin.getControl(key); + if (control && control.is_expensive) { + // Don't send preview for expensive controls + // The final value will be sent when the control is committed + return; + } + } + } + } + this._setServerControl("previewControl", instanceId, key, value); } @@ -2086,6 +2124,23 @@ export class PiPedalModel //implements PiPedalModel return result; + } + replacePedalboarditem(instanceId: number, newItem: PedalboardItem): void { + let pedalboard = this.pedalboard.get(); + let newPedalboard = pedalboard.clone(); + + this.updateVst3State(newPedalboard); + + let fromItem = newPedalboard.getItem(instanceId); + if (fromItem === null) { + throw new PiPedalArgumentError("fromInstanceId not found."); + } + + let newInstanceId = newPedalboard.replaceItem(instanceId, newItem.clone()); + newPedalboard.selectedPlugin = newInstanceId; + this.setModelPedalboard(newPedalboard); + this.updateServerPedalboard(); + } movePedalboardItemBefore(fromInstanceId: number, toInstanceId: number): void { if (fromInstanceId === toInstanceId) return; diff --git a/vite/src/pipedal/PluginInfoDialog.tsx b/vite/src/pipedal/PluginInfoDialog.tsx index 0054eb0..945a82b 100644 --- a/vite/src/pipedal/PluginInfoDialog.tsx +++ b/vite/src/pipedal/PluginInfoDialog.tsx @@ -153,6 +153,11 @@ function makeControls(plugin: UiPlugin, controlHeadeClass: string) { if (portGroup !== lastPortGroup) { if (portGroup !== null) { + // Hide Tone port group on Toob NAM. + if (portGroup.uri === 'http://two-play.com/plugins/toob-nam#eqGroup') + { + continue; + } trs.push(( diff --git a/vite/src/pipedal/PluginPresetSelector.tsx b/vite/src/pipedal/PluginPresetSelector.tsx index 4773d07..00eb640 100644 --- a/vite/src/pipedal/PluginPresetSelector.tsx +++ b/vite/src/pipedal/PluginPresetSelector.tsx @@ -23,29 +23,30 @@ import { PiPedalModel, PiPedalModelFactory, PluginPresetsChangedHandle } from '. import { Theme } from '@mui/material/styles'; import WithStyles from './WithStyles'; import { withStyles } from "tss-react/mui"; -import {createStyles} from './WithStyles'; +import { createStyles } from './WithStyles'; import PluginPresetsDialog from './PluginPresetsDialog'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; import Fade from '@mui/material/Fade'; import RenameDialog from './RenameDialog' -import {PluginUiPresets} from './PluginPreset'; +import { PluginUiPresets } from './PluginPreset'; import Divider from "@mui/material/Divider"; import PluginPresetsIcon from "./svg/ic_pluginpreset.svg?react"; import PluginPresetIcon from "./svg/ic_pluginpreset2.svg?react"; +import { Pedalboard, PedalboardItem } from './Pedalboard'; interface PluginPresetSelectorProps extends WithStyles { - pluginUri?: string; + pedalboardItem: PedalboardItem | null; instanceId: number; } interface PluginPresetSelectorState { - presets : PluginUiPresets; - enabled: boolean; + presets: PluginUiPresets; + //enabled: boolean; presetChanged: boolean; showPresetsDialog: boolean; showEditPresetsDialog: boolean; @@ -57,6 +58,10 @@ interface PluginPresetSelectorState { renameDialogOnOk?: (name: string) => void; saveAsName: string; + + isVisible: boolean; + hasPresets: boolean; + isPastePluginEnabled: boolean; } @@ -65,15 +70,17 @@ const styles = (theme: Theme) => createStyles({ width: 24, height: 24, marginRight: "4px", opacity: 0.6 }, pluginIcon: { - width: 24, height: 24, opacity: 0.6,fill: theme.palette.text.primary + width: 24, height: 24, opacity: 0.6, fill: theme.palette.text.primary }, pluginMenuIcon: { - width: 24, height: 24, opacity: 0.6,fill: theme.palette.text.primary,marginRight: 4 + width: 24, height: 24, opacity: 0.6, fill: theme.palette.text.primary, marginRight: 4 } }); +let pluginClipboardContents: PedalboardItem | null = null; + const PluginPresetSelector = withStyles( class extends Component { @@ -85,7 +92,7 @@ const PluginPresetSelector = this.model = PiPedalModelFactory.getInstance(); this.state = { presets: new PluginUiPresets(), - enabled: false, + isVisible: this.isVisible(props.pedalboardItem), presetChanged: false, showPresetsDialog: false, showEditPresetsDialog: false, @@ -94,26 +101,44 @@ const PluginPresetSelector = renameDialogDefaultName: "", renameDialogActionName: "", renameDialogOnOk: undefined, - saveAsName: "" + saveAsName: "", + hasPresets: this.hasPresets(this.props.pedalboardItem), + isPastePluginEnabled: pluginClipboardContents !== null + }; this.handleDialogClose = this.handleDialogClose.bind(this); this.handlePresetsMenuClose = this.handlePresetsMenuClose.bind(this); } - handleLoadPluginPreset(instanceId: number) - { - this.handlePresetsMenuClose(); - this.model.loadPluginPreset(this.props.instanceId,instanceId); - let presetName = this.getPresetName(instanceId); - this.setState({saveAsName: presetName}); + hasPresets(pedalboardItem: PedalboardItem | null): boolean { + if (pedalboardItem === null) return false; + if (!pedalboardItem.uri) return false; + if (pedalboardItem.isStart() || pedalboardItem.isEnd() + || pedalboardItem.isEmpty() || pedalboardItem.isSplit()) { + return false; + } + return true; } - getPresetName(instanceId: number) - { - for (let preset of this.state.presets.presets) - { - if (preset.instanceId === instanceId) - { + isVisible(pedalboardItem: PedalboardItem | null): boolean { + if (pedalboardItem === null) return false; + if (!pedalboardItem.uri) return false; + if (pedalboardItem.isStart() || pedalboardItem.isEnd() + || pedalboardItem.isSplit()) { + return false; + } + return true; + } + + handleLoadPluginPreset(instanceId: number) { + this.handlePresetsMenuClose(); + this.model.loadPluginPreset(this.props.instanceId, instanceId); + let presetName = this.getPresetName(instanceId); + this.setState({ saveAsName: presetName }); + } + getPresetName(instanceId: number) { + for (let preset of this.state.presets.presets) { + if (preset.instanceId === instanceId) { return preset.label; } } @@ -135,8 +160,8 @@ const PluginPresetSelector = this.renameDialogOpen(name, "Save As") .then((newName) => { - this.setState({saveAsName: newName}); - return this.model.saveCurrentPluginPresetAs(this.props.instanceId,newName); + this.setState({ saveAsName: newName }); + return this.model.saveCurrentPluginPresetAs(this.props.instanceId, newName); }) .then((newInstanceId) => { // s'fine. dealt with by updates, but we do need error handling. @@ -170,60 +195,62 @@ const PluginPresetSelector = return result; } - loadPresets():void { - if (!this.currentUri) return; - let captureUri: string = this.currentUri; + loadPresets(): void { + if (this.props.pedalboardItem === null) return; + if (!this.hasPresets(this.props.pedalboardItem)) return; + let captureUri: string = this.props.pedalboardItem.uri; this.model.getPluginPresets(captureUri) - .then((presets: PluginUiPresets) => { - if (captureUri === this.currentUri) - { - this.setState({presets: presets}); - } - }) - .catch(error => { - if (captureUri === this.currentUri) - { - this.setState({presets: new PluginUiPresets()}); - } - }); + .then((presets: PluginUiPresets) => { + if (captureUri === this.props.pedalboardItem?.uri) { + this.setState({ presets: presets }); + } + }) + .catch(error => { + if (captureUri === this.props.pedalboardItem?.uri) { + this.setState({ presets: new PluginUiPresets() }); + } + }); } onPluginPresetsChanged(pluginUri: string): void { - if (pluginUri === this.props.pluginUri) - { + if (pluginUri === this.props.pedalboardItem?.uri) { this.loadPresets(); } } _pluginPresetsChangedHandle?: PluginPresetsChangedHandle; - componentDidMount() - { + componentDidMount() { this._pluginPresetsChangedHandle = this.model.addPluginPresetsChangedListener( (pluginUri: string) => { this.onPluginPresetsChanged(pluginUri); } ); } - componentWillUnmount() - { - if (this._pluginPresetsChangedHandle) - { + componentWillUnmount() { + if (this._pluginPresetsChangedHandle) { this.model.removePluginPresetsChangedListener(this._pluginPresetsChangedHandle); this._pluginPresetsChangedHandle = undefined; } } - currentUri?: string = ""; - componentDidUpdate() + currentPedalboard?: PedalboardItem; + componentDidUpdate( + prevProps: Readonly, + prevState: Readonly, + snapshot?: any): void { - if (this.currentUri !== this.props.pluginUri) - { - this.currentUri = this.props.pluginUri; - if (!this.props.pluginUri) - { - this.setState({presets: new PluginUiPresets()}); - } else { - this.loadPresets(); + if (this.props.pedalboardItem !== prevProps.pedalboardItem) { + this.setState({ + isVisible: this.isVisible(this.props.pedalboardItem), + hasPresets: this.hasPresets(this.props.pedalboardItem) }); + if (this.props.pedalboardItem) { + if (this.props.pedalboardItem.isEmpty()) + { + this.setState({ presets: new PluginUiPresets() }); + } else + { + this.loadPresets(); + } } } } @@ -244,8 +271,38 @@ const PluginPresetSelector = } + isMenuCopyPluginEnabled(): boolean { + return this.state.hasPresets; + } + handleMenuCopyPlugin(): void { + this.handlePresetsMenuClose(); + let pedalboard: Pedalboard = this.model.pedalboard.get();; - handleMenuEditPluginPresets(): void { + + let pedalboardItem: PedalboardItem | null = pedalboard.tryGetItem(this.props.instanceId); + if (pedalboardItem === null) { + pluginClipboardContents = null; + this.setState({ isPastePluginEnabled: false }); + return; + } + pluginClipboardContents = pedalboardItem.clone(); + this.setState({ isPastePluginEnabled: true }); + + } + + private isMenuPastePluginEnabled(): boolean { + return this.state.isPastePluginEnabled; + } + private handleMenuPastePlugin(): void { + this.handlePresetsMenuClose(); + + if (pluginClipboardContents) { + this.model.replacePedalboarditem(this.props.instanceId, pluginClipboardContents.clone()); + } + } + + + private handleMenuEditPluginPresets(): void { this.handlePresetsMenuClose(); this.showEditPluginPresetsDialog(true); } @@ -282,21 +339,50 @@ const PluginPresetSelector = //this.model.loadPreset(event.target.value as number); } + buildMenuItems() { + let result : React.ReactNode[] = []; + if (this.state.hasPresets) { + result.push(( this.handlePluginPresetsMenuSaveAs(e)}>Save plugin preset...)); + result.push(( this.handleMenuEditPluginPresets()}>Manage plugin presets...)); + result.push(()); + } + let hasPresets = false; + for (let preset of this.state.presets.presets) { + result.push(( + this.handleLoadPluginPreset(preset.instanceId)} + > + + + {preset.label} + )); + hasPresets = true; + } + if (hasPresets) { + result.push(()); + } + result.push( + this.handleMenuCopyPlugin()}>Copy plugin + ); + result.push( + this.handleMenuPastePlugin()}>Paste plugin + ); + return result; + } render() { const classes = withStyles.getClasses(this.props); //const classes = withStyles.getClasses(this.props); //const classes = withStyles.getClasses(this.props); - if ((!this.props.pluginUri)) - { - return (
); + if (!this.state.isVisible) { + return (
); } return (
- this.handlePresetMenuClick(e)} size="large"> - + onClick={(e) => this.handlePresetMenuClick(e)} size="large"> + - this.handlePluginPresetsMenuSaveAs(e)}>Save plugin preset... - this.handleMenuEditPluginPresets()}>Manage plugin presets... - - { this.state.presets.presets.length !== 0 && - () - } { - this.state.presets.presets.map((preset) => { - return ( this.handleLoadPluginPreset(preset.instanceId)} - > - - - {preset.label}); - }) + this.buildMenuItems() } - this.handleDialogClose()} /> + )} 250) { this.onTone3000DownloadProgress(this.progress); lastUpdateTime = Date.now(); } - if (!await this.throttleRequests()) return; + if (!await this.throttleRequests()) return; if (!model.model_url) { throw new Error("Model " + model.name + " does not have a model URL."); @@ -377,15 +380,86 @@ export class Tone3000DownloadHandler { throw new Error(`Upload failed: ${uploadResult.error ?? "Unknown error."}`); } this.progress.progress++; + } + // the image file and readme. + let toobThubmnailUrl: string = ""; + if (tone.images && tone.images.length > 0) { + + await this.throttleRequests(); + + let accessToken = await this.t3kClient.getAccessToken(); + let thumbnailUrl = tone.images[0]; + let thumbnailResult = await fetch(thumbnailUrl, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + duplex: 'half', + } as RequestInit & { duplex: 'half' } + + ); + if (!thumbnailResult.ok) { + throw new Error(`Thumbnail download failed: ${thumbnailResult.status} ${thumbnailResult.statusText}`); + } + if (this.checkForCancel()) { + return; + } + let mediaType = thumbnailResult.headers.get("Content-Type"); + if (mediaType == null) { + throw new Error("Thumbnail download failed: Content-Type header is missing."); + } + let extension: string; + switch (mediaType) { + case "image/jpeg": + extension = ".jpeg"; + break; + case "image/webp": + extension = ".webp"; + break; + case "image/png": + extension = ".png"; + break; + default: + throw new Error(`Thumbnail download failed: Unexpected media type '${mediaType}'.`); + } + let blob = await thumbnailResult.blob(); + const TONE3000_THUMBNAIL_PATH = "/var/pipedal/tone3000_thumbnails/"; + let thumbnailUploadPath = TONE3000_THUMBNAIL_PATH + tone.id + extension; + + let serverUrl = this.model.varServerUrl + "t3k_uploadAsset?path=" + + encodeURIComponent(thumbnailUploadPath); + + const uploadResponse = await fetch(serverUrl, { + method: 'POST', + body: blob, + headers: { + 'Content-Type': mediaType, + "Content-Length": blob.size.toString(), + "Transfer-Encoding": "chunked" + }, + }); + + if (!uploadResponse.ok) { + throw new Error(`Thumbnail upload failed: ${uploadResponse.statusText}`); + } + let uploadResult: any = await uploadResponse.json(); + + if (!uploadResult.ok === true) { + throw new Error(`Thumbnail upload failed: ${uploadResult.error ?? "Unknown error."}`); + } + toobThubmnailUrl = "/var/t3k_thumbnail?id=" + tone.id; } - // yyy: get the image file. - // yyy: write the reaadme file. + let readmePath = toneUploadPath + "README.md"; + this.model.writeTone3000Readme(readmePath, tone, toobThubmnailUrl); - this.onTone3000DownloadComplete(uploadPath); + this.onTone3000DownloadComplete(toneUploadPath); } catch (error) { - this.onTone3000DownloadError(getErrorMessage(error) - + " " + this.progress.progress.toString() + "/" + this.progress.total.toString() + " models downloaded."); + let message = getErrorMessage(error); + if (this.progress.progress > 0) { + message += " (" + this.progress.progress.toString() + "/" + this.progress.total.toString() + " models downloaded)"; + } + this.onTone3000DownloadError(message); return; } } diff --git a/vite/src/pipedal/ToobMLView.tsx b/vite/src/pipedal/ToobMLView.tsx index e9ce76a..b768f2b 100644 --- a/vite/src/pipedal/ToobMLView.tsx +++ b/vite/src/pipedal/ToobMLView.tsx @@ -1,4 +1,5 @@ // Copyright (c) 2022 Robin Davies +// Copyright (c) Fulgencio Ruiz Rubio. // // 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/vite/src/pipedal/ToobNamView.tsx b/vite/src/pipedal/ToobNamView.tsx index dfa2677..82c41ed 100644 --- a/vite/src/pipedal/ToobNamView.tsx +++ b/vite/src/pipedal/ToobNamView.tsx @@ -30,17 +30,27 @@ import { PiPedalModelFactory, PiPedalModel, ListenHandle, State } from "./PiPeda import { PedalboardItem, ControlValue } from './Pedalboard'; import PluginControlView, { ICustomizationHost, ControlGroup, ControlViewCustomization } from './PluginControlView'; import { UiPlugin, UiControl, ControlType } from './Lv2Plugin'; -import Select from '@mui/material/Select/Select'; -import MenuItem from '@mui/material/MenuItem/MenuItem'; -import { CustomPluginControl } from './PluginControl'; const TOOB_NAM__MODEL_METADATA_URI = "http://two-play.com/plugins/toob-nam#model_metadata"; -const TOOB_NAM__MODEL_WEIGHT_URI = "http://two-play.com/plugins/toob-nam#modelWeight"; + +enum NamModelType { + None = 0, + A1 = 1, + A2 = 2 +}; + +function floatToModelType(value: number): NamModelType { + switch (value) { + case 0: return NamModelType.None; + case 1: return NamModelType.A1; + case 2: return NamModelType.A2; + default: return NamModelType.None; + } +} // offset 0: an integer with the folloing bits set. // Must match ToobAmp/src/NeuralAmpModeler_Lv2Extensions.hpp enum TOOB_NAM_METADATA_OFFSETS -const MAX_SLIMMABLE_WEIGHTS = 16; class TOOB_NAM_METADATA_OFFSETS { static readonly flags = 0; static readonly preset_version = 1; @@ -48,11 +58,10 @@ class TOOB_NAM_METADATA_OFFSETS { static readonly gain = 3; static readonly input_level_dbu = 4; static readonly output_level_dbu = 5; - static readonly is_a2 = 6; + static readonly has_slimmable_weights = 6; static readonly model_weight = 7; - static readonly slimmable_weights_size = 8; - static readonly slimmable_weights = 9; - static readonly max_medatadata = TOOB_NAM_METADATA_OFFSETS.slimmable_weights + MAX_SLIMMABLE_WEIGHTS; + static readonly model_type = 8; + static readonly max_medatadata = 9; }; @@ -81,12 +90,9 @@ class ModelMetadata { this.inputlevelDBU = metadataValues[TOOB_NAM_METADATA_OFFSETS.input_level_dbu]; this.outputlevelDBU = metadataValues[TOOB_NAM_METADATA_OFFSETS.output_level_dbu]; this.preset_version = metadataValues[TOOB_NAM_METADATA_OFFSETS.preset_version]; - this.isA2 = metadataValues[TOOB_NAM_METADATA_OFFSETS.is_a2] !== 0; + this.hasSlimmableWeights = metadataValues[TOOB_NAM_METADATA_OFFSETS.has_slimmable_weights] !== 0; this.modelWeight = metadataValues[TOOB_NAM_METADATA_OFFSETS.model_weight]; - let slimmableWeightsSize = metadataValues[TOOB_NAM_METADATA_OFFSETS.slimmable_weights_size]; - for (let i = 0; i < slimmableWeightsSize && i < 16; ++i) { - this.slimmable_weights.push(metadataValues[TOOB_NAM_METADATA_OFFSETS.slimmable_weights + i]); - } + this.modelType = floatToModelType(metadataValues[TOOB_NAM_METADATA_OFFSETS.model_type]); } else { this.hasModel = false; this.hasLoudness = false; @@ -96,34 +102,29 @@ class ModelMetadata { this.loudness = 0; this.gain = 0; + this.hasSlimmableWeights = false; this.inputlevelDBU = 0; this.outputlevelDBU = 0; this.preset_version = 1; this.modelWeight = -1; - this.isA2 = false; - this.slimmable_weights = []; } } preset_version: number; - isA2: boolean = false; + hasSlimmableWeights: boolean = false; modelWeight: number = 1.0; - slimmable_weights: number[] = []; - hasModel: boolean; - hasLoudness: boolean; - hasGain: boolean; - hasInputLevelDBU: boolean; - hasOutputLevelDBU: boolean; + modelType: NamModelType = NamModelType.None; + hasModel: boolean = false; + hasLoudness: boolean = false; + hasGain: boolean = false; + hasInputLevelDBU: boolean = false; + hasOutputLevelDBU: boolean = false; loudness: number; gain: number; inputlevelDBU: number; outputlevelDBU: number; - hasSlimmableWeights(): boolean { - return this.slimmable_weights.length > 0; - } - } @@ -213,10 +214,9 @@ const ToobNamView = } modifyControls(host: ICustomizationHost, controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] { - let EqPos = 7; - const CalibrationGroupPos = 8; - const ModelSizeControlPos = 9; - const ModelWeightControlPos = 10; + const ModelWeightControlPos = 3; + const EqPos = 8; + const CalibrationGroupPos = 9; let calibrationGroup = controls[CalibrationGroupPos] as ControlGroup; @@ -250,57 +250,15 @@ const ToobNamView = } else { controls[CalibrationGroupPos] = null; } - controls[ModelSizeControlPos] = null; - controls[ModelWeightControlPos] = null; if (!this.state.showEqSection) { - controls.splice(EqPos, 1); + controls[EqPos] = null; } - if (this.state.modelMetadata.hasSlimmableWeights()) { - controls.splice(3, 0, this.MakeModelWeightSelector(host)); + if (!this.state.modelMetadata.hasSlimmableWeights) { + controls[ModelWeightControlPos] = null; } return controls; } - ModelWeightToString(weight: number): string { - switch (weight) { - case 1.0: return "Standard"; - case 0.5: return "Lite"; - case 0.25: return "Feather"; - default: return weight.toString(); - } - } - - SelectModelWeight(weight: number) { - this.setState({ modelWeight: weight }); - this.model.setPatchProperty(this.props.instanceId, TOOB_NAM__MODEL_WEIGHT_URI, weight); - - } - MakeModelWeightSelector(host: ICustomizationHost): React.ReactNode { - if ((this.state.modelMetadata.modelWeight === undefined || this.state.modelMetadata.modelWeight || this.state.modelMetadata.hasSlimmableWeights()) === false) { - return null; - } - let selectControl = ( - - ); - return ( -
- -
); - } private handleConnectionStateChanged(state: State) { if (state === State.Ready) { diff --git a/vite/src/pipedal/t3k/tone3000-client.ts b/vite/src/pipedal/t3k/tone3000-client.ts index f3bea64..b0438c7 100644 --- a/vite/src/pipedal/t3k/tone3000-client.ts +++ b/vite/src/pipedal/t3k/tone3000-client.ts @@ -542,7 +542,7 @@ export async function handleOAuthCallback( console.debug("PiPedal handleOAuthCallback Response: " + JSON.stringify(data).replace(/\n/g, ' ')); } - if (toneId === undefined && modelId === undefined) { + if (toneId === undefined && modelId === undefined && !canceled) { throw new Error('Missing both toneId and modelId in OAuth callback response'); } return { ok: true, tokens, toneId, modelId, ...(canceled ? { canceled: true } : {}) };