Subscription Mutex Deadlock

This commit is contained in:
Robin E.R. Davies
2026-05-23 18:48:49 -04:00
parent aa411574fb
commit 0ff96636ed
19 changed files with 1077 additions and 225 deletions
+1 -1
View File
@@ -338,7 +338,7 @@
"args": [
"--no-profile",
"--preset-file", "/home/robin/Downloads/A1 Profiling.piPreset",
"--seconds", "1",
"--seconds", "100",
"-w"
],
"stopAtEntry": false,
Binary file not shown.
+1
View File
@@ -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
+221 -146
View File
@@ -47,6 +47,7 @@
#include "AudioFiles.hpp"
#include "CrashGuard.hpp"
#include "Tone3000Downloader.hpp"
#include "HtmlHelper.hpp"
#ifndef NO_MLOCK
#include <sys/mman.h>
@@ -210,7 +211,7 @@ PiPedalModel::~PiPedalModel()
void PiPedalModel::Init(const PiPedalConfiguration &configuration)
{
std::lock_guard<std::recursive_mutex> 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<std::recursive_mutex> 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<std::recursive_mutex> lock(mutex);
for (auto subscriber : this->subscribers)
SubscriberList subscribers;
{
std::lock_guard<std::recursive_mutex> 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<std::recursive_mutex> lock(mutex);
for (auto subscriber : this->subscribers)
SubscriberList subscribers;
{
std::lock_guard<std::recursive_mutex> 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<std::recursive_mutex> lock(mutex);
for (auto subscriber : this->subscribers)
SubscriberList subscribers;
{
std::lock_guard<std::recursive_mutex> 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<std::recursive_mutex> lock(mutex);
for (auto subscriber : this->subscribers)
SubscriberList subscribers;
{
std::lock_guard<std::recursive_mutex> 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<std::recursive_mutex> lock(mutex);
SubscriberList subscribers;
{
std::lock_guard<std::recursive_mutex> 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<IPiPedalModelSubscriber::ptr> 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<std::recursive_mutex> 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<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
SubscriberList subscribers;
{
std::lock_guard<std::recursive_mutex> 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<std::recursive_mutex> 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<IPiPedalModelSubscriber::ptr> 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<std::recursive_mutex> lock(mutex);
subscribers = this->subscribers;
}
// noify subscribers.
std::vector<IPiPedalModelSubscriber::ptr> 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<std::recursive_mutex> lock(mutex);
subscribers = this->subscribers;
}
// noify subscribers.
std::vector<IPiPedalModelSubscriber::ptr> 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<std::recursive_mutex> lock(mutex);
subscribers = this->subscribers;
UpdateRealtimeVuSubscriptions();
UpdateRealtimeMonitorPortSubscriptions();
if (loadAudioThread)
{
// notify the audio thread.
if (audioHost && audioHost->IsOpen())
{
LoadCurrentPedalboard();
UpdateRealtimeVuSubscriptions();
UpdateRealtimeMonitorPortSubscriptions();
}
}
}
// noify subscribers.
std::vector<IPiPedalModelSubscriber::ptr> 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<std::recursive_mutex> 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<std::recursive_mutex> lock(mutex);
if (this->pedalboard.ApplySnapshot(selectedSnapshot, pluginHost))
bool pedalboardChanged = false;
{
this->pedalboard.selectedSnapshot(selectedSnapshot);
for (auto snapshot : this->pedalboard.snapshots())
std::lock_guard<std::recursive_mutex> 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<std::shared_ptr<Snapshot>> &snapshots, int64_t selectedSnapshot)
{
std::lock_guard<std::recursive_mutex> 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<std::recursive_mutex> 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<std::recursive_mutex> 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<IPiPedalModelSubscriber::ptr> 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<std::recursive_mutex> guard{mutex};
{
// take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us)
std::vector<IPiPedalModelSubscriber::ptr> 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<std::recursive_mutex> 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<IPiPedalModelSubscriber::ptr> 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<std::recursive_mutex> 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<IPiPedalModelSubscriber::ptr> 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<std::recursive_mutex> guard{mutex};
{
PresetIndex presets;
subscribers = this->subscribers;
GetPresets(&presets);
std::vector<IPiPedalModelSubscriber::ptr> 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<std::recursive_mutex> 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<IPiPedalModelSubscriber::ptr> 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<std::recursive_mutex> guard{mutex};
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
SubscriberList subscribers;
{
std::lock_guard<std::recursive_mutex> 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<VuUpdateX> &updates)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
std::vector<IPiPedalModelSubscriber::ptr> subscribers;
{
std::lock_guard<std::recursive_mutex> 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<IPiPedalModelSubscriber::ptr> 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<std::recursive_mutex> lock(mutex);
std::string propertyUri = pluginHost.GetMapFeature().UridToString(patchSetProperty);
std::vector<IPiPedalModelSubscriber::ptr> subscribers;
std::vector<AtomOutputListener> atomOutputListeners;
std::string propertyUri;
{
PedalboardItem *item = pedalboard.GetItem((int64_t)instanceId);
if (item == nullptr)
return;
atom_object atomObject{atomValue};
std::lock_guard<std::recursive_mutex> 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);
}
+9 -10
View File
@@ -44,6 +44,7 @@
#include <unordered_map>
#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> tone3000Downloader;
void CancelAudioRetry();
clock::time_point lastRestartTime = clock::time_point::min();
@@ -212,7 +212,8 @@ namespace pipedal
std::shared_ptr<Lv2Pedalboard> lv2Pedalboard;
std::filesystem::path webRoot;
std::vector<std::shared_ptr<IPiPedalModelSubscriber>> subscribers;
using SubscriberList = std::vector<std::shared_ptr<IPiPedalModelSubscriber>>;
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.
+24
View File
@@ -36,6 +36,7 @@
#include "Promise.hpp"
#include <mutex>
#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;
+4
View File
@@ -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> 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> 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),
+9
View File
@@ -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;
+487
View File
@@ -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 << "&lt;";
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<std::string, std::string> &enumValues)
{
auto ff = enumValues.find(value);
if (ff == enumValues.end())
{
return value;
}
return ff->second;
}
static std::string mdEnumList(
const std::vector<std::string> &values,
const std::map<std::string, std::string> &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<std::string, std::string> sizeEnumValues =
{
{"standard", "Standard"},
{"lite", "Lite"},
{"feather", "Feather"},
{"nano", "Nano"},
{"custom", "Custom"}};
static std::string mdSizes(const std::vector<std::string> &sizes)
{
return mdEnumList(sizes, sizeEnumValues);
}
static std::map<std::string, std::string> 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<LicenseInfo> 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<std::string, LicenseInfo> makeLicenseEnum()
{
std::map<std::string, LicenseInfo> result;
for (const auto &license : licenses)
{
result[license.key] = license;
}
return result;
}
static std::map<std::string, LicenseInfo> 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 << "<img id='cc_img' src='" << url << "'/>";
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 <br> 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>{LicenseFlags::Cc, LicenseFlags::By, LicenseFlags::Cc0, LicenseFlags::Nc, LicenseFlags::Sa, LicenseFlags::Nd})
{
if (licenseInfo.licenseFlags & licenseFlag)
{
os << mdLicenseIcon(licenseFlag);
}
}
os << " ";
}
if (licenseInfo.url != "")
{
os << "<a href="
<< mdHref(licenseInfo.url)
<< " target='_blank' rel='noopener noreferrer' >" << mdSanitize(licenseInfo.displayName) << "</a>";
}
else
{
os << mdSanitize(licenseInfo.displayName);
}
return os.str();
}
// static std::string mdTestLicenses()
// {
// std::ostringstream os;
// for (auto license: licenseEnum)
// {
// os << "<br/>" << mdLicense(license.first) << "\n";
// }
// return os.str();
// }
static std::map<std::string, std::string> 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 <<
"<div id='tone3000_float' >\n";
if (!thumbnailUrl.empty())
{
of << " <img id='tone3000_thumbnail' src='"
<< thumbnailUrl << "' alt='' />\n";
}
of << " <div id='tone3000_float_content' >\n"
<< " <h3 id='tone3000_title'>" << mdSanitize(tone.title()) << "</h3>\n"
<< " <div>\n"
<< " <p id='tone3000_subtitle'>\n"
<< mdDate(tone.updated_at())
<< " "
<< mdUser(tone.user())
<< " <br/>\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 << "<br/>\n";
if (tone.license() != "") {
of << " License: " << mdLicense(tone.license()) << "\n";
}
of
// << " " << mdTestLicenses()
<< " </p>\n"
<< " </div>\n"
<< " </div>\n"
<< "</div>\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();
+141
View File
@@ -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 <cstdint>
#include <string>
#include <vector>
#include <optional>
#include "json.hpp"
#include <filesystem>
#include <chrono>
#include <functional>
#include "Tone3000DownloadProgress.hpp"
#include <memory>
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<std::string> 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<std::string> 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<std::string> images_;
bool is_public_ = true;
std::vector<std::string> links_;
std::string platform_;
int64_t models_count_ = 0;
int64_t favorites_count_ = 0;
int64_t downloads_count_ = 0;
std::string license_;
std::optional<std::vector<std::string>> 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);
}
}
+22 -2
View File
@@ -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.");
}
+1 -1
View File
@@ -7,6 +7,6 @@
"ui_plugins": [],
"enable_auto_update": true,
"has_wifi_device": false,
"tone3000_A2_models": false,
"tone3000_A2_models": true,
"end": true
}
+5 -1
View File
@@ -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);
}
+2
View File
@@ -593,6 +593,7 @@ export class UiControl implements Deserializable<UiControl> {
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<UiControl> {
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;
+56 -1
View File
@@ -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<void> {
if (!this.webSocket) {
throw new Error("Server disconnected.");
}
await this.webSocket.request<void>(
"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;
+5
View File
@@ -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((
<tr>
<td className={controlHeadeClass} style={{ verticalAlign: "top", paddingTop: 8 }} colSpan={2}>
+82 -8
View File
@@ -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;
}
}
+6 -54
View File
@@ -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 = (
<Select key="model_weight_control" variant="standard" size="small" value={this.state.modelMetadata.modelWeight}
onChange={(e) => { this.SelectModelWeight(Number(e.target.value)) }}
style={{ marginLeft: 4, marginRight: 4, width: 140, fontSize: 12, marginTop: 4 }}
>
{/*
{this.state.modelMetadata.slimmable_weights.map((weight, index) => {
return (
<MenuItem key={weight.toString()} value={weight}
style={{ fontSize: 14 }}
>
{this.ModelWeightToString(weight)}
</MenuItem>
);
})}
*/}
</Select>
);
return (
<div style={{ marginLeft: 12 }}>
<CustomPluginControl title="Model Weight" mainControl={selectControl} isSelect={true} />
</div>);
}
private handleConnectionStateChanged(state: State) {
if (state === State.Ready) {
+1 -1
View File
@@ -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 } : {}) };