Subscription Mutex Deadlock
This commit is contained in:
Vendored
+1
-1
@@ -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.
@@ -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
@@ -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
@@ -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.
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<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();
|
||||
@@ -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
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"ui_plugins": [],
|
||||
"enable_auto_update": true,
|
||||
"has_wifi_device": false,
|
||||
"tone3000_A2_models": false,
|
||||
"tone3000_A2_models": true,
|
||||
"end": true
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 } : {}) };
|
||||
|
||||
Reference in New Issue
Block a user