diff --git a/.vscode/launch.json b/.vscode/launch.json index dac42b7..c696ea3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -338,7 +338,7 @@ "args": [ "--no-profile", "--preset-file", "/home/robin/Downloads/A1 Profiling.piPreset", - "--seconds", "1", + "--seconds", "100", "-w" ], "stopAtEntry": false, diff --git a/lv2/x86_64/toobamp_1.2.76_amd64.deb b/lv2/x86_64/toobamp_1.2.76_amd64.deb deleted file mode 100644 index f1de7d2..0000000 Binary files a/lv2/x86_64/toobamp_1.2.76_amd64.deb and /dev/null 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 f3188ad..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 @@ -210,7 +211,7 @@ PiPedalModel::~PiPedalModel() void PiPedalModel::Init(const PiPedalConfiguration &configuration) { std::lock_guard lock(mutex); // prevent callbacks while we're initializing. - if (updaterEnabled) + if (updaterEnabled) { this->updater->Start(); } @@ -242,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); @@ -274,8 +274,7 @@ int64_t PiPedalModel::DownloadModelsFromTone3000( uri, pckeParams, downloadPath, - downloadType - ); + downloadType); } void PiPedalModel::CancelTone3000Download( @@ -293,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); } @@ -302,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); } @@ -311,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); } @@ -320,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); } @@ -554,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); } @@ -571,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); } @@ -594,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)) { @@ -612,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); } @@ -640,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()); } @@ -650,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); } @@ -674,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; + } } } } @@ -748,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) @@ -766,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) @@ -790,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) @@ -848,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); } } @@ -943,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); } @@ -1740,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); } @@ -1800,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(); @@ -2516,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; @@ -2564,10 +2623,6 @@ void PiPedalModel::OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetPropert } } } - if (audioHost) - { - audioHost->SetListenForAtomOutput(atomOutputListeners.size() != 0); - } } void PiPedalModel::OnNotifyPathPatchPropertyReceived( @@ -3454,9 +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 df7661a..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; @@ -128,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(); @@ -212,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); @@ -303,7 +304,6 @@ namespace pipedal virtual std::string Tone3000ThumbnailDirectory() override; - bool GetHasWifi(); void NextBank(Direction direction = Direction::Increase); @@ -312,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); @@ -531,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/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/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/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/Tone3000Downloader.tsx b/vite/src/pipedal/Tone3000Downloader.tsx index 1545636..d60a1ad 100644 --- a/vite/src/pipedal/Tone3000Downloader.tsx +++ b/vite/src/pipedal/Tone3000Downloader.tsx @@ -238,6 +238,10 @@ export class Tone3000DownloadHandler { if (!tokenResponse.ok) { throw new Error(tokenResponse.error); } + if (tokenResponse.canceled) { + this.onTone3000DownloadComplete(""); + return; + } if (this.checkForCancel()) { return; } @@ -308,15 +312,14 @@ export class Tone3000DownloadHandler { let lastUpdateTime = Date.now(); - - for (const model of models) { + for (const model of models) { this.progress.title = model.name;; if (Date.now() - lastUpdateTime > 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/ToobNamView.tsx b/vite/src/pipedal/ToobNamView.tsx index d7710df..82c41ed 100644 --- a/vite/src/pipedal/ToobNamView.tsx +++ b/vite/src/pipedal/ToobNamView.tsx @@ -30,13 +30,9 @@ 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 { @@ -218,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; @@ -255,58 +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 } : {}) };