This commit is contained in:
Robin E. R. Davies
2026-05-23 21:51:37 -04:00
31 changed files with 1334 additions and 358 deletions
+2 -2
View File
@@ -1,13 +1,13 @@
cmake_minimum_required(VERSION 3.16.0) cmake_minimum_required(VERSION 3.16.0)
project(pipedal project(pipedal
VERSION 2.0.101 VERSION 2.0.102
DESCRIPTION "PiPedal Guitar Effect Pedal For Raspberry Pi" DESCRIPTION "PiPedal Guitar Effect Pedal For Raspberry Pi"
HOMEPAGE_URL "https://rerdavies.github.io/pipedal" HOMEPAGE_URL "https://rerdavies.github.io/pipedal"
) )
execute_process( COMMAND dpkg --print-architecture COMMAND tr -d '\n' OUTPUT_VARIABLE DEBIAN_ARCHITECTURE ) execute_process( COMMAND dpkg --print-architecture COMMAND tr -d '\n' OUTPUT_VARIABLE DEBIAN_ARCHITECTURE )
set (DISPLAY_VERSION "PiPedal v2.0.101-Alpha") set (DISPLAY_VERSION "PiPedal v2.0.102-Alpha")
set (PACKAGE_ARCHITECTURE ${DEBIAN_ARCHITECTURE}) set (PACKAGE_ARCHITECTURE ${DEBIAN_ARCHITECTURE})
set (CMAKE_INSTALL_PREFIX "/usr/") set (CMAKE_INSTALL_PREFIX "/usr/")
+2 -1
View File
@@ -208,8 +208,9 @@ void DBusDispatcher::WaitForClose()
serviceThread.join(); serviceThread.join();
} }
} }
catch (const std::exception &) catch (const std::exception &e)
{ {
std::cout << "DBusDispatcher: " << e.what() << std::endl;
} }
if (eventFd != -1) if (eventFd != -1)
{ {
+1 -1
View File
@@ -78,7 +78,7 @@ private:
std::atomic<bool> stopping; std::atomic<bool> stopping;
std::vector<CallbackEntry> postedEvents; std::vector<CallbackEntry> postedEvents;
bool threadStarted; bool threadStarted = false;
std::thread serviceThread; std::thread serviceThread;
std::mutex postMutex; std::mutex postMutex;
}; };
+2 -2
View File
@@ -9,11 +9,11 @@
<img src="https://img.shields.io/github/downloads/rerdavies/pipedal/total?color=%23808080&link=https%3A%2F%2Frerdavies.github.io%2Fpipedal%2Fdownload.html"/> <img src="https://img.shields.io/github/downloads/rerdavies/pipedal/total?color=%23808080&link=https%3A%2F%2Frerdavies.github.io%2Fpipedal%2Fdownload.html"/>
Download:&nbsp;<a href='https://rerdavies.github.io/pipedal/download.html'>v2.0.101</a> Download:&nbsp;<a href='https://rerdavies.github.io/pipedal/download.html'>v2.0.102</a>
Website:&nbsp;[https://rerdavies.github.io/pipedal](https://rerdavies.github.io/pipedal). Website:&nbsp;[https://rerdavies.github.io/pipedal](https://rerdavies.github.io/pipedal).
Documentation:&nbsp;[https://rerdavies.github.io/pipedal/Documentation.html](https://rerdavies.github.io/pipedal/Documentation.html). Documentation:&nbsp;[https://rerdavies.github.io/pipedal/Documentation.html](https://rerdavies.github.io/pipedal/Documentation.html).
#### NEW version 2.0.101 Release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details. TooB NAM calibration, manipulation of i/r files in Toob Convolution Reverb, new Parametric EQ and 3 Band EQ plugins, and MORE! #### NEW version 2.0.102 Release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details. TooB NAM calibration, manipulation of i/r files in Toob Convolution Reverb, new Parametric EQ and 3 Band EQ plugins, and MORE!
&nbsp; &nbsp;
+5 -5
View File
@@ -13,18 +13,18 @@ page_icon: img/Install4.jpg
Download the most recent Debian (.deb) package for your platform: Download the most recent Debian (.deb) package for your platform:
- [Raspberry Pi OS bookworm (aarch64) v2.0.101](https://github.com/rerdavies/pipedal/releases/download/v2.0.101/pipedal_2.0.101_arm64.deb) - [Raspberry Pi OS bookworm (aarch64) v2.0.102](https://github.com/rerdavies/pipedal/releases/download/v2.0.102/pipedal_2.0.102_arm64.deb)
- [Ubuntu 24.04 through 25.04 (aarch64) v2.0.101](https://github.com/rerdavies/pipedal/releases/download/v2.0.101/pipedal_2.0.101_arm64.deb) - [Ubuntu 24.04 through 25.04 (aarch64) v2.0.102](https://github.com/rerdavies/pipedal/releases/download/v2.0.102/pipedal_2.0.102_arm64.deb)
- [Ubuntu 24.04 through 25.04 (amd64) v2.0.101](https://github.com/rerdavies/pipedal/releases/download/v2.0.101/pipedal_2.0.101_amd64.deb) - [Ubuntu 24.04 through 25.04 (amd64) v2.0.102](https://github.com/rerdavies/pipedal/releases/download/v2.0.102/pipedal_2.0.102_amd64.deb)
Version 2.0.101 has been tested on Raspberry Pi OS bookworm, Ubuntu 24.04 (amd64), Ubuntu 24.10 (aarch64), and Ubuntu 25.04 (aarch64). Download the appropriate package for your platform, and install using the following procedure: Version 2.0.102 has been tested on Raspberry Pi OS bookworm, Ubuntu 24.04 (amd64), Ubuntu 24.10 (aarch64), and Ubuntu 25.04 (aarch64). Download the appropriate package for your platform, and install using the following procedure:
``` ```
sudo apt update sudo apt update
sudo apt upgrade sudo apt upgrade
cd ~/Downloads cd ~/Downloads
sudo apt-get install ./pipedal_2.0.101_arm64.deb sudo apt-get install ./pipedal_2.0.102_arm64.deb
``` ```
You MUST use `apt-get`. `apt` will not install downloaded packages; and `dpkg -i` will not install dependencies. You MUST use `apt-get`. `apt` will not install downloaded packages; and `dpkg -i` will not install dependencies.
+30 -6
View File
@@ -1,15 +1,39 @@
# Release Notes # Release Notes
## PiPedal 2.0.101 Internal ## PiPedal 2.0.102 Alpha
- TooB Tone parameter tweaks. PiPedal 2.0 provides support for Neural Amp Modeler A2 models.
## PiPedal 2.0.100 Internal NAM A2 provides significant improvements in sound quality and performance compared to NAM A1. NAM A2 also provides "Slimable" models. Slimmable models provide multiple levels of performance and sound quality in a single model file, allowing you to choose the level of performance and sound quality that best suits your needs. This is particularly useful for users of Raspberry Pi 4, since NAM A2 models can provide better sound quality than NAM A1 models, while consuming significantly less CPU resources.
- Tone3000 direct uploads to PiPedal server (Toob NAM and Toob CabIr) A new integrated Tone3000 model downloader provides a convenient way to download the latest NAM A2 models and IIR files from Tone3000 directly to the PiPedal server. All existing NAM A1 models on Tone3000 have been retrained for NAM A2. When
- New TooB Tone plugin: a simple one-knob tone control. you download from Tone3000 using the integrated downloader, you will get NAM A2 models. Just click on the DOWNLOAD MODELS FROM TONE3000 button.
- Channel Routing: configure audio channels as Aux or re-amp channels.
Currently, the best way to get access to NAM A2 models is via the integrated Tone3000 model downloader. Models downloaded externally from the Tone3000 website will still be NAM A1 models; however this should change soon with a forthcoming announcement from Tone3000.
A new channel routing dialog replaces the channel configurationg dialog. The Channel Routing dialog allows global (i.e. not per-preset) configuration of audio channels. You can now configure audio channels as Aux channels (e.g. using the non-guitar channel on your audio interface as an input for backing tracks), or sending unprocessed guitar input signal to an unused output channel for re-amping in a DAW or external hardware. Refer to the (?) help dialog in the Channel Routing Dialog for more information on how to use this feature.
### Features
- Support for Neural Amp Modeler A2 models. The latest version of the TooB Neural Amp Modeler plugin now supports NAM A2 models, which provide improved performance and sound quality compared to older NAM A1 models.
- Download Tone3000 NAM models and IIR files directly to the PiPedal server. Click on the DOWNLOAD MODELS FROM TONE3000 button in the NAM file selection dialog to download the latest NAM A2 models.
- Download IIR files directly into TooB Cab IR from Tone3000.
- New Channel Routing Dialog.
- New TooB Tone plugin: a simple one-knob tone control. The new TooB Tone plugin provides a simple one-knob tone control that can be used to adjust the overall tone of your guitar signal. It is a great tool for quickly shaping your tone without having to use a more complex EQ plugin.
- Copy and Paste plugins (and their settings) between pedalboards. Accessible via the "Copy Plugin" and "Paste Plugin" menu items in the former Plugin Preset selector menu.
### Minor features and bug fixes
- Better handling of step sizes in numeric edit controls.
- Short names of TooB CE-2 Chorus and TooB BF-2 Flanger changed to "TooB Chorus" and "TooB Flanger", repsectively.
- Split controls now support double-tap to reset to a default value. (Other controls already support this feature).
## PiPedal 1.5.99 Beta ## PiPedal 1.5.99 Beta
+4 -4
View File
@@ -4,9 +4,9 @@
Download the most recent Debian (.deb) package for your platform: Download the most recent Debian (.deb) package for your platform:
- [Raspberry Pi OS bookworm (aarch64) v2.0.101 Beta](https://github.com/rerdavies/pipedal/releases/download/v2.0.101/pipedal_2.0.101_arm64.deb) - [Raspberry Pi OS bookworm (aarch64) v2.0.102 Beta](https://github.com/rerdavies/pipedal/releases/download/v2.0.102/pipedal_2.0.102_arm64.deb)
- [Ubuntu 24.x, 25.04 (aarch64) v2.0.101 Alpha](https://github.com/rerdavies/pipedal/releases/download/v2.0.101/pipedal_2.0.101_arm64.deb) - [Ubuntu 24.x, 25.04 (aarch64) v2.0.102 Alpha](https://github.com/rerdavies/pipedal/releases/download/v2.0.102/pipedal_2.0.102_arm64.deb)
- [Ubuntu 24.x, 25.04 (amd64) v2.0.101 Alpha](https://github.com/rerdavies/pipedal/releases/download/v2.0.101/pipedal_2.0.101_amd64.deb) - [Ubuntu 24.x, 25.04 (amd64) v2.0.102 Alpha](https://github.com/rerdavies/pipedal/releases/download/v2.0.102/pipedal_2.0.102_amd64.deb)
Install the package by running Install the package by running
@@ -14,7 +14,7 @@ Install the package by running
``` ```
sudo apt update sudo apt update
cd ~/Downloads cd ~/Downloads
sudo apt-get install ./pipedal_2.0.101_arm64.deb sudo apt-get install ./pipedal_2.0.102_arm64.deb
``` ```
You MUST use `apt-get` to install the package. `apt install` will NOT install the package correctly. The message about missing permissions given by `apt-get` is You MUST use `apt-get` to install the package. `apt install` will NOT install the package correctly. The message about missing permissions given by `apt-get` is
expected, and can be safely ignored. expected, and can be safely ignored.
+1
View File
@@ -207,6 +207,7 @@ else()
endif() endif()
set (PIPEDAL_SOURCES set (PIPEDAL_SOURCES
Tone3000Tone.hpp Tone3000Tone.cpp
AesDigest.cpp AesDigest.hpp AesDigest.cpp AesDigest.hpp
ChannelRouterSettings.cpp ChannelRouterSettings.hpp ChannelRouterSettings.cpp ChannelRouterSettings.hpp
Curl.cpp Curl.hpp Curl.cpp Curl.hpp
+153 -70
View File
@@ -47,6 +47,7 @@
#include "AudioFiles.hpp" #include "AudioFiles.hpp"
#include "CrashGuard.hpp" #include "CrashGuard.hpp"
#include "Tone3000Downloader.hpp" #include "Tone3000Downloader.hpp"
#include "HtmlHelper.hpp"
#ifndef NO_MLOCK #ifndef NO_MLOCK
#include <sys/mman.h> #include <sys/mman.h>
@@ -80,7 +81,6 @@ PiPedalModel::PiPedalModel()
atomConverter(pluginHost.GetMapFeature()) atomConverter(pluginHost.GetMapFeature())
{ {
this->updater = Updater::Create(); this->updater = Updater::Create();
this->updater->Start();
this->currentUpdateStatus = updater->GetCurrentStatus(); this->currentUpdateStatus = updater->GetCurrentStatus();
this->pedalboard = Pedalboard::MakeDefault(); this->pedalboard = Pedalboard::MakeDefault();
@@ -211,6 +211,10 @@ PiPedalModel::~PiPedalModel()
void PiPedalModel::Init(const PiPedalConfiguration &configuration) void PiPedalModel::Init(const PiPedalConfiguration &configuration)
{ {
std::lock_guard<std::recursive_mutex> lock(mutex); // prevent callbacks while we're initializing. std::lock_guard<std::recursive_mutex> lock(mutex); // prevent callbacks while we're initializing.
if (updaterEnabled)
{
this->updater->Start();
}
this->configuration = configuration; this->configuration = configuration;
pluginHost.SetConfiguration(configuration); pluginHost.SetConfiguration(configuration);
@@ -241,8 +245,7 @@ int64_t PiPedalModel::DownloadModelsFromTone3000(
const std::string &uri, const std::string &uri,
const Tone3000PkceParams &pckeParams, const Tone3000PkceParams &pckeParams,
const std::string &downloadPath, const std::string &downloadPath,
Tone3000DownloadType downloadType Tone3000DownloadType downloadType)
)
{ {
std::lock_guard<std::recursive_mutex> lock(mutex); std::lock_guard<std::recursive_mutex> lock(mutex);
@@ -271,8 +274,7 @@ int64_t PiPedalModel::DownloadModelsFromTone3000(
uri, uri,
pckeParams, pckeParams,
downloadPath, downloadPath,
downloadType downloadType);
);
} }
void PiPedalModel::CancelTone3000Download( void PiPedalModel::CancelTone3000Download(
@@ -289,36 +291,53 @@ void PiPedalModel::CancelTone3000Download(
// Tone3000Downloader::Listener implementation // Tone3000Downloader::Listener implementation
void PiPedalModel::OnStartTone3000Download(int64_t handle, const std::string &title) void PiPedalModel::OnStartTone3000Download(int64_t handle, const std::string &title)
{
SubscriberList subscribers;
{ {
std::lock_guard<std::recursive_mutex> lock(mutex); std::lock_guard<std::recursive_mutex> lock(mutex);
for (auto subscriber : this->subscribers) subscribers = this->subscribers;
}
for (auto &subscriber : this->subscribers)
{ {
subscriber->OnTone3000DownloadStarted(handle, title); subscriber->OnTone3000DownloadStarted(handle, title);
} }
} }
void PiPedalModel::OnTone3000Progress(const Tone3000DownloadProgress &progress) void PiPedalModel::OnTone3000Progress(const Tone3000DownloadProgress &progress)
{
SubscriberList subscribers;
{ {
std::lock_guard<std::recursive_mutex> lock(mutex); std::lock_guard<std::recursive_mutex> lock(mutex);
for (auto subscriber : this->subscribers) subscribers = this->subscribers;
}
for (auto &subscriber : subscribers)
{ {
subscriber->OnTone3000DownloadProgress(progress); subscriber->OnTone3000DownloadProgress(progress);
} }
} }
void PiPedalModel::OnTone3000DownloadComplete(int64_t handle, const std::string &resultPath) void PiPedalModel::OnTone3000DownloadComplete(int64_t handle, const std::string &resultPath)
{
SubscriberList subscribers;
{ {
std::lock_guard<std::recursive_mutex> lock(mutex); std::lock_guard<std::recursive_mutex> lock(mutex);
for (auto subscriber : this->subscribers) subscribers = this->subscribers;
}
for (auto subscriber : subscribers)
{ {
subscriber->OnTone3000DownloadComplete(handle, resultPath); subscriber->OnTone3000DownloadComplete(handle, resultPath);
} }
} }
void PiPedalModel::OnTone3000DownloadError(int64_t handle, const std::string &errorMessage) void PiPedalModel::OnTone3000DownloadError(int64_t handle, const std::string &errorMessage)
{
SubscriberList subscribers;
{ {
std::lock_guard<std::recursive_mutex> lock(mutex); std::lock_guard<std::recursive_mutex> lock(mutex);
for (auto subscriber : this->subscribers) subscribers = this->subscribers;
}
for (auto subscriber : subscribers)
{ {
subscriber->OnTone3000DownloadError(handle, errorMessage); subscriber->OnTone3000DownloadError(handle, errorMessage);
} }
@@ -550,13 +569,16 @@ bool PiPedalModel::OnNotifyMaybeLv2StateChanged(uint64_t instanceId)
void PiPedalModel::SetInputVolume(float value) void PiPedalModel::SetInputVolume(float value)
{ {
PreviewInputVolume(value); PreviewInputVolume(value);
{
SubscriberList subscribers;
{ {
std::lock_guard<std::recursive_mutex> lock(mutex); 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) // 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 : subscribers)
for (auto &subscriber : t)
{ {
subscriber->OnInputVolumeChanged(value); subscriber->OnInputVolumeChanged(value);
} }
@@ -567,12 +589,15 @@ void PiPedalModel::SetInputVolume(float value)
void PiPedalModel::SetOutputVolume(float value) void PiPedalModel::SetOutputVolume(float value)
{ {
PreviewOutputVolume(value); PreviewOutputVolume(value);
{
SubscriberList subscribers;
{ {
std::lock_guard<std::recursive_mutex> lock(mutex); std::lock_guard<std::recursive_mutex> lock(mutex);
subscribers = this->subscribers;
this->pedalboard.output_volume_db(value); 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 : subscribers)
for (auto &subscriber : t)
{ {
subscriber->OnOutputVolumeChanged(value); subscriber->OnOutputVolumeChanged(value);
} }
@@ -591,8 +616,10 @@ void PiPedalModel::PreviewOutputVolume(float value)
void PiPedalModel::SetControl(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value) 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); std::lock_guard<std::recursive_mutex> lock(mutex);
subscribers = this->subscribers;
if (!this->pedalboard.SetControlValue(pedalItemId, symbol, value)) if (!this->pedalboard.SetControlValue(pedalItemId, symbol, value))
{ {
@@ -609,27 +636,28 @@ void PiPedalModel::SetControl(int64_t clientId, int64_t pedalItemId, const std::
return; return;
} }
PreviewControl(clientId, pedalItemId, symbol, value); PreviewControl(clientId, pedalItemId, symbol, value);
}
{ for (auto &subscriber : 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->OnControlChanged(clientId, pedalItemId, symbol, value); subscriber->OnControlChanged(clientId, pedalItemId, symbol, value);
} }
this->SetPresetChanged(clientId, true); this->SetPresetChanged(clientId, true);
} }
}
}
void PiPedalModel::FireJackConfigurationChanged(const JackConfiguration &jackConfiguration) void PiPedalModel::FireJackConfigurationChanged(const JackConfiguration &jackConfiguration)
{ {
SubscriberList subscribers;
{
std::lock_guard<std::recursive_mutex> lock(mutex);
subscribers = this->subscribers;
}
// noify subscribers. // noify subscribers.
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()}; for (auto &subscriber : subscribers)
for (auto &subscriber : t)
{ {
subscriber->OnJackConfigurationChanged(jackConfiguration); subscriber->OnJackConfigurationChanged(jackConfiguration);
} }
@@ -637,9 +665,13 @@ void PiPedalModel::FireJackConfigurationChanged(const JackConfiguration &jackCon
void PiPedalModel::FireBanksChanged(int64_t clientId) void PiPedalModel::FireBanksChanged(int64_t clientId)
{ {
SubscriberList subscribers;
{
std::lock_guard<std::recursive_mutex> lock(mutex);
subscribers = this->subscribers;
}
// noify subscribers. // noify subscribers.
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()}; for (auto &subscriber : subscribers)
for (auto &subscriber : t)
{ {
subscriber->OnBankIndexChanged(this->storage.GetBanks()); subscriber->OnBankIndexChanged(this->storage.GetBanks());
} }
@@ -647,6 +679,11 @@ void PiPedalModel::FireBanksChanged(int64_t clientId)
void PiPedalModel::FirePedalboardChanged(int64_t clientId, bool loadAudioThread) void PiPedalModel::FirePedalboardChanged(int64_t clientId, bool loadAudioThread)
{ {
SubscriberList subscribers;
{
std::lock_guard<std::recursive_mutex> lock(mutex);
subscribers = this->subscribers;
if (loadAudioThread) if (loadAudioThread)
{ {
// notify the audio thread. // notify the audio thread.
@@ -658,9 +695,9 @@ void PiPedalModel::FirePedalboardChanged(int64_t clientId, bool loadAudioThread)
UpdateRealtimeMonitorPortSubscriptions(); UpdateRealtimeMonitorPortSubscriptions();
} }
} }
}
// noify subscribers. // noify subscribers.
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()}; for (auto &subscriber : subscribers)
for (auto &subscriber : t)
{ {
subscriber->OnPedalboardChanged(clientId, this->pedalboard); subscriber->OnPedalboardChanged(clientId, this->pedalboard);
} }
@@ -671,13 +708,14 @@ void PiPedalModel::SetPedalboard(int64_t clientId, Pedalboard &pedalboard)
std::lock_guard<std::recursive_mutex> lock(mutex); std::lock_guard<std::recursive_mutex> lock(mutex);
this->pedalboard = pedalboard; this->pedalboard = pedalboard;
UpdateDefaults(&this->pedalboard); UpdateDefaults(&this->pedalboard);
}
this->FirePedalboardChanged(clientId); this->FirePedalboardChanged(clientId);
this->SetPresetChanged(clientId, true); this->SetPresetChanged(clientId, true);
} }
}
void PiPedalModel::SetSnapshot(int64_t selectedSnapshot) void PiPedalModel::SetSnapshot(int64_t selectedSnapshot)
{
bool pedalboardChanged = false;
{ {
std::lock_guard<std::recursive_mutex> lock(mutex); std::lock_guard<std::recursive_mutex> lock(mutex);
if (this->pedalboard.ApplySnapshot(selectedSnapshot, pluginHost)) if (this->pedalboard.ApplySnapshot(selectedSnapshot, pluginHost))
@@ -690,11 +728,17 @@ void PiPedalModel::SetSnapshot(int64_t selectedSnapshot)
snapshot->isModified_ = false; snapshot->isModified_ = false;
} }
} }
pedalboardChanged = true;
}
}
if (pedalboardChanged)
{
FirePedalboardChanged(-1, true); FirePedalboardChanged(-1, true);
} }
} }
void PiPedalModel::SetSnapshots(std::vector<std::shared_ptr<Snapshot>> &snapshots, int64_t selectedSnapshot) void PiPedalModel::SetSnapshots(std::vector<std::shared_ptr<Snapshot>> &snapshots, int64_t selectedSnapshot)
{
{ {
std::lock_guard<std::recursive_mutex> lock(mutex); std::lock_guard<std::recursive_mutex> lock(mutex);
@@ -713,6 +757,7 @@ void PiPedalModel::SetSnapshots(std::vector<std::shared_ptr<Snapshot>> &snapshot
} }
} }
} }
}
this->FirePedalboardChanged(-1, false); // notify clients (but don't change the running pedalboard, because it's still the same) this->FirePedalboardChanged(-1, false); // notify clients (but don't change the running pedalboard, because it's still the same)
// this means that all clients get an up-to-date copy of the snapshots AND the currently selected snapshot if that applies // this means that all clients get an up-to-date copy of the snapshots AND the currently selected snapshot if that applies
@@ -745,11 +790,10 @@ void PiPedalModel::UpdateCurrentPedalboard(int64_t clientId, Pedalboard &pedalbo
UpdateRealtimeVuSubscriptions(); UpdateRealtimeVuSubscriptions();
UpdateRealtimeMonitorPortSubscriptions(); UpdateRealtimeMonitorPortSubscriptions();
}
this->FirePedalboardChanged(clientId, false); this->FirePedalboardChanged(clientId, false);
this->SetPresetChanged(clientId, true); this->SetPresetChanged(clientId, true);
} }
}
void PiPedalModel::SetPedalboardItemUseModUi(int64_t clientId, int64_t instanceId, bool enabled) void PiPedalModel::SetPedalboardItemUseModUi(int64_t clientId, int64_t instanceId, bool enabled)
{ {
@@ -763,14 +807,17 @@ void PiPedalModel::SetPedalboardItemUseModUi(int64_t clientId, int64_t instanceI
{ {
subscriber->OnItemUseModUiChanged(clientId, instanceId, enabled); subscriber->OnItemUseModUiChanged(clientId, instanceId, enabled);
} }
this->SetPresetChanged(clientId, true);
} }
this->SetPresetChanged(clientId, true);
} }
void PiPedalModel::SetPedalboardItemEnable(int64_t clientId, int64_t pedalItemId, bool enabled) void PiPedalModel::SetPedalboardItemEnable(int64_t clientId, int64_t pedalItemId, bool enabled)
{ {
SubscriberList subscribers;
std::lock_guard<std::recursive_mutex> guard{mutex}; std::lock_guard<std::recursive_mutex> guard{mutex};
{ {
subscribers = this->subscribers;
this->pedalboard.SetItemEnabled(pedalItemId, enabled); this->pedalboard.SetItemEnabled(pedalItemId, enabled);
PedalboardItem *pPedalboardItem = this->pedalboard.GetItem(pedalItemId); PedalboardItem *pPedalboardItem = this->pedalboard.GetItem(pedalItemId);
if (pPedalboardItem) if (pPedalboardItem)
@@ -787,18 +834,16 @@ void PiPedalModel::SetPedalboardItemEnable(int64_t clientId, int64_t pedalItemId
} }
} }
} }
// Notify audo thread.
}
this->audioHost->SetBypass(pedalItemId, enabled);
// Notify clients. // Notify clients.
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()}; for (auto &subscriber : subscribers)
for (auto &subscriber : t)
{ {
subscriber->OnItemEnabledChanged(clientId, pedalItemId, enabled); subscriber->OnItemEnabledChanged(clientId, pedalItemId, enabled);
} }
this->SetPresetChanged(clientId, true); this->SetPresetChanged(clientId, true);
// Notify audo thread.
this->audioHost->SetBypass(pedalItemId, enabled);
}
} }
void PiPedalModel::GetPresets(PresetIndex *pResult) void PiPedalModel::GetPresets(PresetIndex *pResult)
@@ -845,74 +890,75 @@ void PiPedalModel::SetPresetChanged(int64_t clientId, bool value, bool changeSna
void PiPedalModel::FireSnapshotModified(int64_t snapshotIndex, bool modified) void PiPedalModel::FireSnapshotModified(int64_t snapshotIndex, bool modified)
{ {
SubscriberList subscribers;
std::lock_guard<std::recursive_mutex> guard{mutex}; 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) subscribers = this->subscribers;
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()}; }
for (auto &subscriber : t) for (auto &subscriber : subscribers)
{ {
subscriber->OnSnapshotModified(snapshotIndex, modified); subscriber->OnSnapshotModified(snapshotIndex, modified);
} }
} }
}
void PiPedalModel::FireSelectedSnapshotChanged(int64_t selectedSnapshot) void PiPedalModel::FireSelectedSnapshotChanged(int64_t selectedSnapshot)
{ {
SubscriberList subscribers;
std::lock_guard<std::recursive_mutex> guard{mutex}; 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) // 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->OnSelectedSnapshotChanged(selectedSnapshot); subscriber->OnSelectedSnapshotChanged(selectedSnapshot);
} }
} }
}
void PiPedalModel::FirePresetChanged(bool changed) void PiPedalModel::FirePresetChanged(bool changed)
{ {
SubscriberList subscribers;
PresetIndex presets;
std::lock_guard<std::recursive_mutex> guard{mutex}; 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) // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us)
PresetIndex presets;
GetPresets(&presets); GetPresets(&presets);
}
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()}; for (auto &subscriber : subscribers)
for (auto &subscriber : t)
{ {
subscriber->OnPresetChanged(changed); subscriber->OnPresetChanged(changed);
} }
} }
}
void PiPedalModel::FirePresetsChanged(int64_t clientId) void PiPedalModel::FirePresetsChanged(int64_t clientId)
{ {
SubscriberList subscribers;
PresetIndex presets;
std::lock_guard<std::recursive_mutex> guard{mutex}; std::lock_guard<std::recursive_mutex> guard{mutex};
{ {
subscribers = this->subscribers;
PresetIndex presets;
GetPresets(&presets); GetPresets(&presets);
}
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()}; for (auto &subscriber : subscribers)
for (auto &subscriber : t)
{ {
subscriber->OnPresetsChanged(clientId, presets); subscriber->OnPresetsChanged(clientId, presets);
} }
} }
}
void PiPedalModel::FirePluginPresetsChanged(const std::string &pluginUri) void PiPedalModel::FirePluginPresetsChanged(const std::string &pluginUri)
{ {
SubscriberList subscribers;
std::lock_guard<std::recursive_mutex> guard{mutex}; 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) // 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->OnPluginPresetsChanged(pluginUri); subscriber->OnPluginPresetsChanged(pluginUri);
} }
} }
}
void PiPedalModel::UpdateVst3Settings(Pedalboard &pedalboard) void PiPedalModel::UpdateVst3Settings(Pedalboard &pedalboard)
{ {
@@ -939,11 +985,14 @@ void PiPedalModel::UpdateVst3Settings(Pedalboard &pedalboard)
} }
void PiPedalModel::FireLv2StateChanged(int64_t instanceId, const Lv2PluginState &lv2State) void PiPedalModel::FireLv2StateChanged(int64_t instanceId, const Lv2PluginState &lv2State)
{
SubscriberList subscribers;
{ {
std::lock_guard<std::recursive_mutex> guard{mutex}; std::lock_guard<std::recursive_mutex> guard{mutex};
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()}; subscribers = this->subscribers;
}
for (auto &subscriber : t) for (auto &subscriber : subscribers)
{ {
subscriber->OnLv2StateChanged(instanceId, lv2State); subscriber->OnLv2StateChanged(instanceId, lv2State);
} }
@@ -1736,13 +1785,16 @@ void PiPedalModel::OnNotifyMidiValueChanged(int64_t instanceId, int portIndex, f
} }
void PiPedalModel::OnNotifyVusSubscription(const std::vector<VuUpdateX> &updates) void PiPedalModel::OnNotifyVusSubscription(const std::vector<VuUpdateX> &updates)
{
std::vector<IPiPedalModelSubscriber::ptr> subscribers;
{ {
std::lock_guard<std::recursive_mutex> lock(mutex); std::lock_guard<std::recursive_mutex> lock(mutex);
subscribers = this->subscribers;
}
for (size_t i = 0; i < updates.size(); ++i) 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) // 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 : subscribers)
for (auto &subscriber : t)
{ {
subscriber->OnVuMeterUpdate(updates); subscriber->OnVuMeterUpdate(updates);
} }
@@ -2512,10 +2564,16 @@ void PiPedalModel::DeleteMidiListeners(int64_t clientId)
} }
void PiPedalModel::OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetProperty, const LV2_Atom *atomValue) void PiPedalModel::OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetProperty, const LV2_Atom *atomValue)
{
std::vector<IPiPedalModelSubscriber::ptr> subscribers;
std::vector<AtomOutputListener> atomOutputListeners;
std::string propertyUri;
{ {
std::lock_guard<std::recursive_mutex> lock(mutex); std::lock_guard<std::recursive_mutex> lock(mutex);
std::string propertyUri = pluginHost.GetMapFeature().UridToString(patchSetProperty); subscribers = this->subscribers;
atomOutputListeners = this->atomOutputListeners;
propertyUri = pluginHost.GetMapFeature().UridToString(patchSetProperty);
{ {
PedalboardItem *item = pedalboard.GetItem((int64_t)instanceId); PedalboardItem *item = pedalboard.GetItem((int64_t)instanceId);
@@ -2532,9 +2590,13 @@ void PiPedalModel::OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetPropert
{ {
properties[propertyUri] = std::move(atomObject); properties[propertyUri] = std::move(atomObject);
} }
OnNotifyMaybeLv2StateChanged(instanceId);
} }
if (audioHost)
{
audioHost->SetListenForAtomOutput(atomOutputListeners.size() != 0);
}
}
OnNotifyMaybeLv2StateChanged(instanceId);
bool hasAtomJson = false; bool hasAtomJson = false;
std::string atomJson; std::string atomJson;
@@ -2561,10 +2623,6 @@ void PiPedalModel::OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetPropert
} }
} }
} }
if (audioHost)
{
audioHost->SetListenForAtomOutput(atomOutputListeners.size() != 0);
}
} }
void PiPedalModel::OnNotifyPathPatchPropertyReceived( void PiPedalModel::OnNotifyPathPatchPropertyReceived(
@@ -3451,4 +3509,29 @@ std::string PiPedalModel::Tone3000ThumbnailDirectory()
return "/var/pipedal/tone3000_thumbnails"; 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);
}
+8 -8
View File
@@ -44,6 +44,7 @@
#include <unordered_map> #include <unordered_map>
#include "Tone3000Downloader.hpp" #include "Tone3000Downloader.hpp"
#include "Uri.hpp" #include "Uri.hpp"
#include "Tone3000Tone.hpp"
namespace pipedal namespace pipedal
{ {
@@ -119,6 +120,7 @@ namespace pipedal
using NetworkChangedListener = std::function<void(void)>; using NetworkChangedListener = std::function<void(void)>;
private: private:
bool updaterEnabled = true;
PedalboardItem *GetPedalboardItemForFileProperty(const UiFileProperty &fileProperty); PedalboardItem *GetPedalboardItemForFileProperty(const UiFileProperty &fileProperty);
// Tone3000Downloader::Listener implementation // Tone3000Downloader::Listener implementation
@@ -127,7 +129,6 @@ namespace pipedal
virtual void OnTone3000DownloadComplete(int64_t handle, const std::string &resultPath) override; virtual void OnTone3000DownloadComplete(int64_t handle, const std::string &resultPath) override;
virtual void OnTone3000DownloadError(int64_t handle, const std::string &errorMessage) override; virtual void OnTone3000DownloadError(int64_t handle, const std::string &errorMessage) override;
std::shared_ptr<Tone3000Downloader> tone3000Downloader; std::shared_ptr<Tone3000Downloader> tone3000Downloader;
void CancelAudioRetry(); void CancelAudioRetry();
clock::time_point lastRestartTime = clock::time_point::min(); clock::time_point lastRestartTime = clock::time_point::min();
@@ -211,7 +212,8 @@ namespace pipedal
std::shared_ptr<Lv2Pedalboard> lv2Pedalboard; std::shared_ptr<Lv2Pedalboard> lv2Pedalboard;
std::filesystem::path webRoot; 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 SetPresetChanged(int64_t clientId, bool value, bool changeSnapshotSelect = true);
void FireSnapshotModified(int64_t snapshotIndex, bool modified); void FireSnapshotModified(int64_t snapshotIndex, bool modified);
void FireSelectedSnapshotChanged(int64_t selectedSnapshot); void FireSelectedSnapshotChanged(int64_t selectedSnapshot);
@@ -296,13 +298,12 @@ namespace pipedal
Decrease Decrease
}; };
void EnableUpdater(bool enable);
Storage &GetStorage() { return storage; } Storage &GetStorage() { return storage; }
virtual std::string Tone3000ThumbnailDirectory() override; virtual std::string Tone3000ThumbnailDirectory() override;
bool GetHasWifi(); bool GetHasWifi();
void NextBank(Direction direction = Direction::Increase); void NextBank(Direction direction = Direction::Increase);
@@ -314,13 +315,11 @@ namespace pipedal
const std::string &responseuri, const std::string &responseuri,
const Tone3000PkceParams &pkce, const Tone3000PkceParams &pkce,
const std::string &downloadPath, const std::string &downloadPath,
Tone3000DownloadType downloadType Tone3000DownloadType downloadType);
);
void CancelTone3000Download( void CancelTone3000Download(
int64_t clientId, int64_t clientId,
int64_t downloadHandle int64_t downloadHandle);
);
void RequestShutdown(bool restart); void RequestShutdown(bool restart);
virtual PostHandle Post(PostCallback &&fn); virtual PostHandle Post(PostCallback &&fn);
@@ -530,6 +529,7 @@ namespace pipedal
void SetChannelRouterSettings(int64_t clientId, ChannelRouterSettings::ptr &settings); void SetChannelRouterSettings(int64_t clientId, ChannelRouterSettings::ptr &settings);
ChannelRouterSettings::ptr GetChannelRouterSettings(); ChannelRouterSettings::ptr GetChannelRouterSettings();
void WriteTone3000Readme(const std::filesystem::path &filePath, const tone3000::Tone &tone, const std::string &thumbnailUrl);
}; };
} // namespace pipedal. } // namespace pipedal.
+24
View File
@@ -36,6 +36,7 @@
#include "Promise.hpp" #include "Promise.hpp"
#include <mutex> #include <mutex>
#include "Tone3000Downloader.hpp" #include "Tone3000Downloader.hpp"
#include "Tone3000Tone.hpp"
#include "AdminClient.hpp" #include "AdminClient.hpp"
#include "WifiConfigSettings.hpp" #include "WifiConfigSettings.hpp"
@@ -62,6 +63,20 @@ JSON_MAP_REFERENCE(CopyPresetsToBankBody, bankInstanceId)
JSON_MAP_REFERENCE(CopyPresetsToBankBody, presets) JSON_MAP_REFERENCE(CopyPresetsToBankBody, presets)
JSON_MAP_END(); 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 class DownloadModelsFromTone3000Body
{ {
public: public:
@@ -1193,6 +1208,15 @@ public:
} }
REGISTER_MESSAGE_HANDLER(makeTone3000Pkce); 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) void handle_sha256Base64url(int replyTo, json_reader *pReader)
{ {
std::string input; 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 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__not_on_gui_property_uri = lilv_new_uri(pWorld, LV2_PORT_PROPS__notOnGUI);
portprops__trigger = lilv_new_uri(pWorld, LV2_PORT_PROPS__trigger); 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); midi__event = lilv_new_uri(pWorld, LV2_MIDI__MidiEvent);
core__designation = lilv_new_uri(pWorld, LV2_CORE__designation); core__designation = lilv_new_uri(pWorld, LV2_CORE__designation);
portgroups__group = lilv_new_uri(pWorld, LV2_PORT_GROUPS__group); 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->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_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->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->pipedal_graphicEq_ = lilv_port_has_property(plugin, pPort, host->lilvUris->pipedalUI__graphicEq);
this->enumeration_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->enumeration_property_uri); this->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, toggled_property),
MAP_REF(Lv2PortInfo, mod_momentaryOffByDefault), MAP_REF(Lv2PortInfo, mod_momentaryOffByDefault),
MAP_REF(Lv2PortInfo, mod_momentaryOnByDefault), MAP_REF(Lv2PortInfo, mod_momentaryOnByDefault),
MAP_REF(Lv2PortInfo, is_expensive),
MAP_REF(Lv2PortInfo, pipedal_graphicEq), MAP_REF(Lv2PortInfo, pipedal_graphicEq),
MAP_REF(Lv2PortInfo, not_on_gui), MAP_REF(Lv2PortInfo, not_on_gui),
MAP_REF(Lv2PortInfo, buffer_type), 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_momentaryOffByDefault),
MAP_REF(Lv2PluginUiPort, mod_momentaryOnByDefault), MAP_REF(Lv2PluginUiPort, mod_momentaryOnByDefault),
MAP_REF(Lv2PluginUiPort, is_expensive),
MAP_REF(Lv2PluginUiPort, pipedal_graphicEq), MAP_REF(Lv2PluginUiPort, pipedal_graphicEq),
MAP_REF(Lv2PluginUiPort, enumeration_property), MAP_REF(Lv2PluginUiPort, enumeration_property),
MAP_REF(Lv2PluginUiPort, not_on_gui), MAP_REF(Lv2PluginUiPort, not_on_gui),
+9
View File
@@ -226,6 +226,7 @@ namespace pipedal
bool mod_momentaryOffByDefault_ = false; bool mod_momentaryOffByDefault_ = false;
bool mod_momentaryOnByDefault_ = false; bool mod_momentaryOnByDefault_ = false;
bool is_expensive_ = false;
bool pipedal_graphicEq_ = false; bool pipedal_graphicEq_ = false;
bool not_on_gui_ = false; bool not_on_gui_ = false;
@@ -304,6 +305,7 @@ namespace pipedal
LV2_PROPERTY_GETSET_SCALAR(integer_property); LV2_PROPERTY_GETSET_SCALAR(integer_property);
LV2_PROPERTY_GETSET_SCALAR(mod_momentaryOffByDefault); LV2_PROPERTY_GETSET_SCALAR(mod_momentaryOffByDefault);
LV2_PROPERTY_GETSET_SCALAR(mod_momentaryOnByDefault); LV2_PROPERTY_GETSET_SCALAR(mod_momentaryOnByDefault);
LV2_PROPERTY_GETSET_SCALAR(is_expensive);
LV2_PROPERTY_GETSET_SCALAR(pipedal_graphicEq); LV2_PROPERTY_GETSET_SCALAR(pipedal_graphicEq);
LV2_PROPERTY_GETSET_SCALAR(enumeration_property); LV2_PROPERTY_GETSET_SCALAR(enumeration_property);
LV2_PROPERTY_GETSET_SCALAR(toggled_property); LV2_PROPERTY_GETSET_SCALAR(toggled_property);
@@ -591,6 +593,7 @@ namespace pipedal
integer_property_(pPort->integer_property()), integer_property_(pPort->integer_property()),
mod_momentaryOffByDefault_(pPort->mod_momentaryOffByDefault()), mod_momentaryOffByDefault_(pPort->mod_momentaryOffByDefault()),
mod_momentaryOnByDefault_(pPort->mod_momentaryOnByDefault()), mod_momentaryOnByDefault_(pPort->mod_momentaryOnByDefault()),
is_expensive_(pPort->is_expensive()),
pipedal_graphicEq_(pPort->pipedal_graphicEq()), pipedal_graphicEq_(pPort->pipedal_graphicEq()),
enumeration_property_(pPort->enumeration_property()), enumeration_property_(pPort->enumeration_property()),
@@ -641,6 +644,7 @@ namespace pipedal
bool mod_momentaryOffByDefault_ = false; bool mod_momentaryOffByDefault_ = false;
bool mod_momentaryOnByDefault_ = false; bool mod_momentaryOnByDefault_ = false;
bool is_expensive_ = false;
bool pipedal_graphicEq_ = false; bool pipedal_graphicEq_ = false;
bool enumeration_property_ = false; bool enumeration_property_ = false;
@@ -671,6 +675,10 @@ namespace pipedal
LV2_PROPERTY_GETSET_SCALAR(display_priority); LV2_PROPERTY_GETSET_SCALAR(display_priority);
LV2_PROPERTY_GETSET_SCALAR(is_logarithmic); LV2_PROPERTY_GETSET_SCALAR(is_logarithmic);
LV2_PROPERTY_GETSET_SCALAR(integer_property); 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(enumeration_property);
LV2_PROPERTY_GETSET_SCALAR(toggled_property); LV2_PROPERTY_GETSET_SCALAR(toggled_property);
LV2_PROPERTY_GETSET_SCALAR(trigger_property); LV2_PROPERTY_GETSET_SCALAR(trigger_property);
@@ -798,6 +806,7 @@ namespace pipedal
AutoLilvNode core__isSideChain; AutoLilvNode core__isSideChain;
AutoLilvNode portprops__not_on_gui_property_uri; AutoLilvNode portprops__not_on_gui_property_uri;
AutoLilvNode portprops__trigger; AutoLilvNode portprops__trigger;
AutoLilvNode portprops__expensive;
AutoLilvNode midi__event; AutoLilvNode midi__event;
AutoLilvNode core__designation; AutoLilvNode core__designation;
AutoLilvNode portgroups__group; 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)) { if (!HtmlHelper::IsSafeFileName(path)) {
return false; 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; return false;
} }
@@ -383,6 +399,10 @@ public:
{ {
throw PiPedalException("Can't download files of this type."); 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); res.set(HttpField::content_type, mimeType);
size_t contentLength = std::filesystem::file_size(path); size_t contentLength = std::filesystem::file_size(path);
res.setContentLength(contentLength); res.setContentLength(contentLength);
@@ -984,7 +1004,7 @@ public:
{ {
throw std::runtime_error("Invalid path"); throw std::runtime_error("Invalid path");
} }
if (!IsSafeMediaPath(targetPath)) if (!IsSafeMediaPath(targetPath) && !IsSafeThumbnailPath(targetPath))
{ {
throw std::runtime_error("Unsafe path."); throw std::runtime_error("Unsafe path.");
} }
+2
View File
@@ -132,7 +132,9 @@ void profilePlugin(const ProfileOptions &profileOptions)
throw std::runtime_error(s.str()); throw std::runtime_error(s.str());
} }
model.EnableUpdater(false);
model.Init(configuration); model.Init(configuration);
model.GetPluginHost().asIHost()->SetMaxAudioBufferSize(64);
model.LoadLv2PluginInfo(); model.LoadLv2PluginInfo();
+1 -1
View File
@@ -7,6 +7,6 @@
"ui_plugins": [], "ui_plugins": [],
"enable_auto_update": true, "enable_auto_update": true,
"has_wifi_device": false, "has_wifi_device": false,
"tone3000_A2_models": false, "tone3000_A2_models": true,
"end": true "end": true
} }
+1
View File
@@ -1,4 +1,5 @@
// Copyright (c) 2022 Robin Davies // Copyright (c) 2022 Robin Davies
// Copyright (c) Fulgencio Ruiz Rubio.
// //
// Permission is hereby granted, free of charge, to any person obtaining a copy of // 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 // this software and associated documentation files (the "Software"), to deal in
+5 -1
View File
@@ -572,8 +572,12 @@ export default withStyles(
return; return;
} }
if (resultPath.startsWith(this.state.navDirectory)) { if (resultPath.startsWith(this.state.navDirectory)) {
let selectedFile = resultPath;
if (selectedFile.endsWith("/")) {
selectedFile = selectedFile.slice(0, -1);
}
this.setState({ this.setState({
selectedFile: resultPath, selectedFile: selectedFile,
}); });
this.requestFiles(this.state.navDirectory); 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.max_value = input.max_value;
this.default_value = input.default_value; this.default_value = input.default_value;
this.is_logarithmic = input.is_logarithmic; this.is_logarithmic = input.is_logarithmic;
this.is_expensive = input.is_expensive;
this.display_priority = input.display_priority; this.display_priority = input.display_priority;
this.range_steps = input.range_steps; this.range_steps = input.range_steps;
this.integer_property = input.integer_property; this.integer_property = input.integer_property;
@@ -715,6 +716,7 @@ export class UiControl implements Deserializable<UiControl> {
integer_property: boolean = false; integer_property: boolean = false;
mod_momentaryOffByDefault: boolean = false; mod_momentaryOffByDefault: boolean = false;
mod_momentaryOnByDefault: boolean = false; mod_momentaryOnByDefault: boolean = false;
is_expensive: boolean = false;
pipedal_graphicEq: boolean = false; pipedal_graphicEq: boolean = false;
enumeration_property: boolean = false; enumeration_property: boolean = false;
trigger_property: boolean = false; trigger_property: boolean = false;
+8 -9
View File
@@ -427,7 +427,6 @@ export const MainPage =
let title = ""; let title = "";
let author = ""; let author = "";
let infoPluginUri = ""; let infoPluginUri = "";
let presetsUri = "";
let missing = false; let missing = false;
let canEditTitle = false; let canEditTitle = false;
let modGuiButtonVisible = false; let modGuiButtonVisible = false;
@@ -439,7 +438,6 @@ export const MainPage =
} else if (pedalboardItem.isSyntheticItem()) { } else if (pedalboardItem.isSyntheticItem()) {
title = pedalboardItem.pluginName ?? "#error"; title = pedalboardItem.pluginName ?? "#error";
author = ""; author = "";
presetsUri = "";
infoPluginUri = ""; infoPluginUri = "";
} }
else { else {
@@ -456,7 +454,6 @@ export const MainPage =
title = uiPlugin.name; title = uiPlugin.name;
author = uiPlugin.author_name; author = uiPlugin.author_name;
} }
presetsUri = uiPlugin.uri;
// if (uiPlugin.description.length > 20) { // if (uiPlugin.description.length > 20) {
// } // }
infoPluginUri = uiPlugin.uri; infoPluginUri = uiPlugin.uri;
@@ -544,10 +541,6 @@ export const MainPage =
<div style={{ flex: "0 0 auto", verticalAlign: "center" }}> <div style={{ flex: "0 0 auto", verticalAlign: "center" }}>
<PluginInfoDialog plugin_uri={infoPluginUri} /> <PluginInfoDialog plugin_uri={infoPluginUri} />
</div> </div>
<div style={{ flex: "0 0 auto" }}>
<PluginPresetSelector pluginUri={presetsUri} instanceId={pedalboardItem?.instanceId ?? 0}
/>
</div>
{modGuiButtonVisible && ( {modGuiButtonVisible && (
<div style={{ flex: "0 0 auto" }}> <div style={{ flex: "0 0 auto" }}>
@@ -634,6 +627,7 @@ export const MainPage =
canShowModUi = this.canShowModUi(pedalboardItem); canShowModUi = this.canShowModUi(pedalboardItem);
canDelete = pedalboard.canDeleteItem(pedalboardItem.instanceId); canDelete = pedalboard.canDeleteItem(pedalboardItem.instanceId);
instanceId = pedalboardItem.instanceId; instanceId = pedalboardItem.instanceId;
pluginUri = pedalboardItem.uri;
if (pedalboardItem.isEmpty()) { if (pedalboardItem.isEmpty()) {
canInsert = true; canInsert = true;
canAppend = true; canAppend = true;
@@ -699,7 +693,12 @@ export const MainPage =
</div> </div>
{this.props.enableStructureEditing && ( {this.props.enableStructureEditing && (
<div style={{ flex: "0 0 auto", display: "flex", flexFlow: "row nowrap", alignItems: "center" }}> <div style={{ flex: "0 0 auto", display: "flex", flexFlow: "row nowrap", alignItems: "center" }}>
<div style={{ flex: "0 0 auto", display: (canInsert || canAppend) ? "block" : "none", paddingRight: 8 }}> <div style={{ flex: "0 0 auto" }}>
<PluginPresetSelector pedalboardItem={pedalboardItem} instanceId={pedalboardItem?.instanceId ?? 0}
/>
</div>
<div style={{ flex: "0 0 auto", display: (canInsert || canAppend) ? "block" : "none" }}>
<IconButtonEx tooltip="Add pedal slot" onClick={(e) => { this.onAddClick(e) }} size="large"> <IconButtonEx tooltip="Add pedal slot" onClick={(e) => { this.onAddClick(e) }} size="large">
<AddIcon style={{ height: 24, width: 24, fill: this.props.theme.palette.text.primary, opacity: 0.6 }} /> <AddIcon style={{ height: 24, width: 24, fill: this.props.theme.palette.text.primary, opacity: 0.6 }} />
</IconButtonEx> </IconButtonEx>
@@ -718,7 +717,7 @@ export const MainPage =
{canAppend && (<MenuItem onClick={() => this.onAppendSplit(instanceId)}>Append split</MenuItem>)} {canAppend && (<MenuItem onClick={() => this.onAppendSplit(instanceId)}>Append split</MenuItem>)}
</Menu> </Menu>
</div> </div>
<div style={{ flex: "0 0 auto", display: canDelete ? "block" : "none", paddingRight: 8 }}> <div style={{ flex: "0 0 auto", display: canDelete ? "block" : "none", }}>
<IconButtonEx tooltip="Delete pedal" <IconButtonEx tooltip="Delete pedal"
onClick={() => { this.onDeletePedal(pedalboardItem?.instanceId ?? -1) }} onClick={() => { this.onDeletePedal(pedalboardItem?.instanceId ?? -1) }}
size="large"> size="large">
+6
View File
@@ -59,6 +59,9 @@ export class ControlValue implements Deserializable<ControlValue> {
} }
export class PedalboardItem implements Deserializable<PedalboardItem> { export class PedalboardItem implements Deserializable<PedalboardItem> {
clone() {
return new PedalboardItem().deserialize(this);
}
deserializePedalboardItem(input: any): PedalboardItem { deserializePedalboardItem(input: any): PedalboardItem {
this.instanceId = input.instanceId ?? -1; this.instanceId = input.instanceId ?? -1;
this.title = input.title ?? ""; this.title = input.title ?? "";
@@ -753,11 +756,14 @@ export class Pedalboard implements Deserializable<Pedalboard> {
replaceItem(instanceId: number, newItem: PedalboardItem) replaceItem(instanceId: number, newItem: PedalboardItem)
{ {
newItem.instanceId = ++this.nextInstanceId;
let result = this._replaceItem(this.items,instanceId,newItem); let result = this._replaceItem(this.items,instanceId,newItem);
if (!result) if (!result)
{ {
throw new PiPedalArgumentError("instanceId not found."); throw new PiPedalArgumentError("instanceId not found.");
} }
return newItem.instanceId;
} }
private _addItem(items: PedalboardItem[], newItem: PedalboardItem, instanceId: number, append: boolean) private _addItem(items: PedalboardItem[], newItem: PedalboardItem, instanceId: number, append: boolean)
{ {
+56 -1
View File
@@ -50,6 +50,7 @@ import { AlsaSequencerConfiguration, AlsaSequencerPortSelection } from './AlsaSe
import { getDefaultModGuiPreference } from './ModGuiHost'; import { getDefaultModGuiPreference } from './ModGuiHost';
import ChannelRouterSettings from './ChannelRouterSettings'; import ChannelRouterSettings from './ChannelRouterSettings';
import Tone3000DownloadProgress from './Tone3000DownloadProgress'; import Tone3000DownloadProgress from './Tone3000DownloadProgress';
import { Tone } from './t3k/types';
export enum State { 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; maxFileUploadSize: number = 512 * 1024 * 1024;
maxPresetUploadSize: number = 1024 * 1024; maxPresetUploadSize: number = 1024 * 1024;
@@ -2058,7 +2076,10 @@ export class PiPedalModel //implements PiPedalModel
previewPedalboardValue(instanceId: number, key: string, value: number): void { previewPedalboardValue(instanceId: number, key: string, value: number): void {
// mouse is down. Don't update EVERYBODY, but we must change // mouse is down. Don't update EVERYBODY, but we must change
// the control on the running audio plugin. // 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") { if (instanceId === Pedalboard.START_CONTROL_ID && key === "volume_db") {
this.previewInputVolume(value); this.previewInputVolume(value);
return; return;
@@ -2067,6 +2088,23 @@ export class PiPedalModel //implements PiPedalModel
return; 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); this._setServerControl("previewControl", instanceId, key, value);
} }
@@ -2086,6 +2124,23 @@ export class PiPedalModel //implements PiPedalModel
return result; 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 { movePedalboardItemBefore(fromInstanceId: number, toInstanceId: number): void {
if (fromInstanceId === toInstanceId) return; if (fromInstanceId === toInstanceId) return;
+5
View File
@@ -153,6 +153,11 @@ function makeControls(plugin: UiPlugin, controlHeadeClass: string) {
if (portGroup !== lastPortGroup) { if (portGroup !== lastPortGroup) {
if (portGroup !== null) if (portGroup !== null)
{ {
// Hide Tone port group on Toob NAM.
if (portGroup.uri === 'http://two-play.com/plugins/toob-nam#eqGroup')
{
continue;
}
trs.push(( trs.push((
<tr> <tr>
<td className={controlHeadeClass} style={{ verticalAlign: "top", paddingTop: 8 }} colSpan={2}> <td className={controlHeadeClass} style={{ verticalAlign: "top", paddingTop: 8 }} colSpan={2}>
+125 -50
View File
@@ -36,16 +36,17 @@ import Divider from "@mui/material/Divider";
import PluginPresetsIcon from "./svg/ic_pluginpreset.svg?react"; import PluginPresetsIcon from "./svg/ic_pluginpreset.svg?react";
import PluginPresetIcon from "./svg/ic_pluginpreset2.svg?react"; import PluginPresetIcon from "./svg/ic_pluginpreset2.svg?react";
import { Pedalboard, PedalboardItem } from './Pedalboard';
interface PluginPresetSelectorProps extends WithStyles<typeof styles> { interface PluginPresetSelectorProps extends WithStyles<typeof styles> {
pluginUri?: string; pedalboardItem: PedalboardItem | null;
instanceId: number; instanceId: number;
} }
interface PluginPresetSelectorState { interface PluginPresetSelectorState {
presets: PluginUiPresets; presets: PluginUiPresets;
enabled: boolean; //enabled: boolean;
presetChanged: boolean; presetChanged: boolean;
showPresetsDialog: boolean; showPresetsDialog: boolean;
showEditPresetsDialog: boolean; showEditPresetsDialog: boolean;
@@ -57,6 +58,10 @@ interface PluginPresetSelectorState {
renameDialogOnOk?: (name: string) => void; renameDialogOnOk?: (name: string) => void;
saveAsName: string; saveAsName: string;
isVisible: boolean;
hasPresets: boolean;
isPastePluginEnabled: boolean;
} }
@@ -74,6 +79,8 @@ const styles = (theme: Theme) => createStyles({
}); });
let pluginClipboardContents: PedalboardItem | null = null;
const PluginPresetSelector = const PluginPresetSelector =
withStyles( withStyles(
class extends Component<PluginPresetSelectorProps, PluginPresetSelectorState> { class extends Component<PluginPresetSelectorProps, PluginPresetSelectorState> {
@@ -85,7 +92,7 @@ const PluginPresetSelector =
this.model = PiPedalModelFactory.getInstance(); this.model = PiPedalModelFactory.getInstance();
this.state = { this.state = {
presets: new PluginUiPresets(), presets: new PluginUiPresets(),
enabled: false, isVisible: this.isVisible(props.pedalboardItem),
presetChanged: false, presetChanged: false,
showPresetsDialog: false, showPresetsDialog: false,
showEditPresetsDialog: false, showEditPresetsDialog: false,
@@ -94,26 +101,44 @@ const PluginPresetSelector =
renameDialogDefaultName: "", renameDialogDefaultName: "",
renameDialogActionName: "", renameDialogActionName: "",
renameDialogOnOk: undefined, renameDialogOnOk: undefined,
saveAsName: "" saveAsName: "",
hasPresets: this.hasPresets(this.props.pedalboardItem),
isPastePluginEnabled: pluginClipboardContents !== null
}; };
this.handleDialogClose = this.handleDialogClose.bind(this); this.handleDialogClose = this.handleDialogClose.bind(this);
this.handlePresetsMenuClose = this.handlePresetsMenuClose.bind(this); this.handlePresetsMenuClose = this.handlePresetsMenuClose.bind(this);
} }
handleLoadPluginPreset(instanceId: number) hasPresets(pedalboardItem: PedalboardItem | null): boolean {
{ if (pedalboardItem === null) return false;
if (!pedalboardItem.uri) return false;
if (pedalboardItem.isStart() || pedalboardItem.isEnd()
|| pedalboardItem.isEmpty() || pedalboardItem.isSplit()) {
return false;
}
return true;
}
isVisible(pedalboardItem: PedalboardItem | null): boolean {
if (pedalboardItem === null) return false;
if (!pedalboardItem.uri) return false;
if (pedalboardItem.isStart() || pedalboardItem.isEnd()
|| pedalboardItem.isSplit()) {
return false;
}
return true;
}
handleLoadPluginPreset(instanceId: number) {
this.handlePresetsMenuClose(); this.handlePresetsMenuClose();
this.model.loadPluginPreset(this.props.instanceId, instanceId); this.model.loadPluginPreset(this.props.instanceId, instanceId);
let presetName = this.getPresetName(instanceId); let presetName = this.getPresetName(instanceId);
this.setState({ saveAsName: presetName }); this.setState({ saveAsName: presetName });
} }
getPresetName(instanceId: number) getPresetName(instanceId: number) {
{ for (let preset of this.state.presets.presets) {
for (let preset of this.state.presets.presets) if (preset.instanceId === instanceId) {
{
if (preset.instanceId === instanceId)
{
return preset.label; return preset.label;
} }
} }
@@ -171,62 +196,64 @@ const PluginPresetSelector =
} }
loadPresets(): void { loadPresets(): void {
if (!this.currentUri) return; if (this.props.pedalboardItem === null) return;
let captureUri: string = this.currentUri; if (!this.hasPresets(this.props.pedalboardItem)) return;
let captureUri: string = this.props.pedalboardItem.uri;
this.model.getPluginPresets(captureUri) this.model.getPluginPresets(captureUri)
.then((presets: PluginUiPresets) => { .then((presets: PluginUiPresets) => {
if (captureUri === this.currentUri) if (captureUri === this.props.pedalboardItem?.uri) {
{
this.setState({ presets: presets }); this.setState({ presets: presets });
} }
}) })
.catch(error => { .catch(error => {
if (captureUri === this.currentUri) if (captureUri === this.props.pedalboardItem?.uri) {
{
this.setState({ presets: new PluginUiPresets() }); this.setState({ presets: new PluginUiPresets() });
} }
}); });
} }
onPluginPresetsChanged(pluginUri: string): void { onPluginPresetsChanged(pluginUri: string): void {
if (pluginUri === this.props.pluginUri) if (pluginUri === this.props.pedalboardItem?.uri) {
{
this.loadPresets(); this.loadPresets();
} }
} }
_pluginPresetsChangedHandle?: PluginPresetsChangedHandle; _pluginPresetsChangedHandle?: PluginPresetsChangedHandle;
componentDidMount() componentDidMount() {
{
this._pluginPresetsChangedHandle = this.model.addPluginPresetsChangedListener( this._pluginPresetsChangedHandle = this.model.addPluginPresetsChangedListener(
(pluginUri: string) => { (pluginUri: string) => {
this.onPluginPresetsChanged(pluginUri); this.onPluginPresetsChanged(pluginUri);
} }
); );
} }
componentWillUnmount() componentWillUnmount() {
{ if (this._pluginPresetsChangedHandle) {
if (this._pluginPresetsChangedHandle)
{
this.model.removePluginPresetsChangedListener(this._pluginPresetsChangedHandle); this.model.removePluginPresetsChangedListener(this._pluginPresetsChangedHandle);
this._pluginPresetsChangedHandle = undefined; this._pluginPresetsChangedHandle = undefined;
} }
} }
currentUri?: string = ""; currentPedalboard?: PedalboardItem;
componentDidUpdate() componentDidUpdate(
prevProps: Readonly<PluginPresetSelectorProps>,
prevState: Readonly<PluginPresetSelectorState>,
snapshot?: any): void
{ {
if (this.currentUri !== this.props.pluginUri) if (this.props.pedalboardItem !== prevProps.pedalboardItem) {
{ this.setState({
this.currentUri = this.props.pluginUri; isVisible: this.isVisible(this.props.pedalboardItem),
if (!this.props.pluginUri) hasPresets: this.hasPresets(this.props.pedalboardItem) });
if (this.props.pedalboardItem) {
if (this.props.pedalboardItem.isEmpty())
{ {
this.setState({ presets: new PluginUiPresets() }); this.setState({ presets: new PluginUiPresets() });
} else { } else
{
this.loadPresets(); this.loadPresets();
} }
} }
} }
}
handleRenameDialogClose(): void { handleRenameDialogClose(): void {
this.setState({ this.setState({
renameDialogOpen: false, renameDialogOpen: false,
@@ -244,8 +271,38 @@ const PluginPresetSelector =
} }
isMenuCopyPluginEnabled(): boolean {
return this.state.hasPresets;
}
handleMenuCopyPlugin(): void {
this.handlePresetsMenuClose();
let pedalboard: Pedalboard = this.model.pedalboard.get();;
handleMenuEditPluginPresets(): void {
let pedalboardItem: PedalboardItem | null = pedalboard.tryGetItem(this.props.instanceId);
if (pedalboardItem === null) {
pluginClipboardContents = null;
this.setState({ isPastePluginEnabled: false });
return;
}
pluginClipboardContents = pedalboardItem.clone();
this.setState({ isPastePluginEnabled: true });
}
private isMenuPastePluginEnabled(): boolean {
return this.state.isPastePluginEnabled;
}
private handleMenuPastePlugin(): void {
this.handlePresetsMenuClose();
if (pluginClipboardContents) {
this.model.replacePedalboarditem(this.props.instanceId, pluginClipboardContents.clone());
}
}
private handleMenuEditPluginPresets(): void {
this.handlePresetsMenuClose(); this.handlePresetsMenuClose();
this.showEditPluginPresetsDialog(true); this.showEditPluginPresetsDialog(true);
} }
@@ -282,13 +339,42 @@ const PluginPresetSelector =
//this.model.loadPreset(event.target.value as number); //this.model.loadPreset(event.target.value as number);
} }
buildMenuItems() {
let result : React.ReactNode[] = [];
if (this.state.hasPresets) {
result.push((<MenuItem key="menuSaveAs" onClick={(e) => this.handlePluginPresetsMenuSaveAs(e)}>Save plugin preset...</MenuItem>));
result.push((<MenuItem key="menuEdit" onClick={(e) => this.handleMenuEditPluginPresets()}>Manage plugin presets...</MenuItem>));
result.push((<Divider key="divider1" />));
}
let hasPresets = false;
for (let preset of this.state.presets.presets) {
result.push((
<MenuItem key={preset.instanceId}
onClick={(e) => this.handleLoadPluginPreset(preset.instanceId)}
>
<PluginPresetIcon className={this.props.classes?.pluginMenuIcon ?? ""} />
{preset.label}</MenuItem>
));
hasPresets = true;
}
if (hasPresets) {
result.push((<Divider key="divider2" />));
}
result.push(
<MenuItem key="menuCopy" disabled={!this.isMenuCopyPluginEnabled()} onClick={(e) => this.handleMenuCopyPlugin()}>Copy plugin</MenuItem>
);
result.push(
<MenuItem key="menuPaste" disabled={!this.isMenuPastePluginEnabled()} style={{ opacity: this.isMenuPastePluginEnabled() ? 1 : 0.4 }} onClick={(e) => this.handleMenuPastePlugin()}>Paste plugin</MenuItem>
);
return result;
}
render() { render() {
const classes = withStyles.getClasses(this.props); const classes = withStyles.getClasses(this.props);
//const classes = withStyles.getClasses(this.props); //const classes = withStyles.getClasses(this.props);
//const classes = withStyles.getClasses(this.props); //const classes = withStyles.getClasses(this.props);
if ((!this.props.pluginUri)) if (!this.state.isVisible) {
{
return (<div />); return (<div />);
} }
return ( return (
@@ -311,29 +397,18 @@ const PluginPresetSelector =
} }
> >
<MenuItem onClick={(e) => this.handlePluginPresetsMenuSaveAs(e)}>Save plugin preset...</MenuItem>
<MenuItem onClick={(e) => this.handleMenuEditPluginPresets()}>Manage plugin presets...</MenuItem>
{ this.state.presets.presets.length !== 0 &&
(<Divider/>)
}
{ {
this.state.presets.presets.map((preset) => { this.buildMenuItems()
return (<MenuItem key={preset.instanceId}
onClick={(e) => this.handleLoadPluginPreset(preset.instanceId)}
>
<PluginPresetIcon className={classes.pluginMenuIcon}/>
{preset.label}</MenuItem>);
})
} }
</Menu> </Menu>
{this.state.hasPresets && this.state.showPresetsDialog && (
<PluginPresetsDialog <PluginPresetsDialog
instanceId={this.props.instanceId} instanceId={this.props.instanceId}
presets={this.state.presets} presets={this.state.presets}
show={this.state.showPresetsDialog} show={this.state.showPresetsDialog}
isEditDialog={this.state.showEditPresetsDialog} isEditDialog={this.state.showEditPresetsDialog}
onDialogClose={() => this.handleDialogClose()} /> onDialogClose={() => this.handleDialogClose()} />
)}
<RenameDialog open={this.state.renameDialogOpen} <RenameDialog open={this.state.renameDialogOpen}
title="Rename" title="Rename"
defaultName={this.state.renameDialogDefaultName} defaultName={this.state.renameDialogDefaultName}
+80 -6
View File
@@ -238,6 +238,10 @@ export class Tone3000DownloadHandler {
if (!tokenResponse.ok) { if (!tokenResponse.ok) {
throw new Error(tokenResponse.error); throw new Error(tokenResponse.error);
} }
if (tokenResponse.canceled) {
this.onTone3000DownloadComplete("");
return;
}
if (this.checkForCancel()) { if (this.checkForCancel()) {
return; return;
} }
@@ -308,7 +312,6 @@ export class Tone3000DownloadHandler {
let lastUpdateTime = Date.now(); let lastUpdateTime = Date.now();
for (const model of models) { for (const model of models) {
this.progress.title = model.name;; this.progress.title = model.name;;
if (Date.now() - lastUpdateTime > 250) { if (Date.now() - lastUpdateTime > 250) {
@@ -377,15 +380,86 @@ export class Tone3000DownloadHandler {
throw new Error(`Upload failed: ${uploadResult.error ?? "Unknown error."}`); throw new Error(`Upload failed: ${uploadResult.error ?? "Unknown error."}`);
} }
this.progress.progress++; 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. let readmePath = toneUploadPath + "README.md";
// yyy: write the reaadme file. this.model.writeTone3000Readme(readmePath, tone, toobThubmnailUrl);
this.onTone3000DownloadComplete(uploadPath); this.onTone3000DownloadComplete(toneUploadPath);
} catch (error) { } catch (error) {
this.onTone3000DownloadError(getErrorMessage(error) let message = getErrorMessage(error);
+ " " + this.progress.progress.toString() + "/" + this.progress.total.toString() + " models downloaded."); if (this.progress.progress > 0) {
message += " (" + this.progress.progress.toString() + "/" + this.progress.total.toString() + " models downloaded)";
}
this.onTone3000DownloadError(message);
return; return;
} }
} }
+1
View File
@@ -1,4 +1,5 @@
// Copyright (c) 2022 Robin Davies // Copyright (c) 2022 Robin Davies
// Copyright (c) Fulgencio Ruiz Rubio.
// //
// Permission is hereby granted, free of charge, to any person obtaining a copy of // 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 // this software and associated documentation files (the "Software"), to deal in
+34 -76
View File
@@ -30,17 +30,27 @@ import { PiPedalModelFactory, PiPedalModel, ListenHandle, State } from "./PiPeda
import { PedalboardItem, ControlValue } from './Pedalboard'; import { PedalboardItem, ControlValue } from './Pedalboard';
import PluginControlView, { ICustomizationHost, ControlGroup, ControlViewCustomization } from './PluginControlView'; import PluginControlView, { ICustomizationHost, ControlGroup, ControlViewCustomization } from './PluginControlView';
import { UiPlugin, UiControl, ControlType } from './Lv2Plugin'; 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_METADATA_URI = "http://two-play.com/plugins/toob-nam#model_metadata";
const TOOB_NAM__MODEL_WEIGHT_URI = "http://two-play.com/plugins/toob-nam#modelWeight";
enum NamModelType {
None = 0,
A1 = 1,
A2 = 2
};
function floatToModelType(value: number): NamModelType {
switch (value) {
case 0: return NamModelType.None;
case 1: return NamModelType.A1;
case 2: return NamModelType.A2;
default: return NamModelType.None;
}
}
// offset 0: an integer with the folloing bits set. // offset 0: an integer with the folloing bits set.
// Must match ToobAmp/src/NeuralAmpModeler_Lv2Extensions.hpp enum TOOB_NAM_METADATA_OFFSETS // Must match ToobAmp/src/NeuralAmpModeler_Lv2Extensions.hpp enum TOOB_NAM_METADATA_OFFSETS
const MAX_SLIMMABLE_WEIGHTS = 16;
class TOOB_NAM_METADATA_OFFSETS { class TOOB_NAM_METADATA_OFFSETS {
static readonly flags = 0; static readonly flags = 0;
static readonly preset_version = 1; static readonly preset_version = 1;
@@ -48,11 +58,10 @@ class TOOB_NAM_METADATA_OFFSETS {
static readonly gain = 3; static readonly gain = 3;
static readonly input_level_dbu = 4; static readonly input_level_dbu = 4;
static readonly output_level_dbu = 5; static readonly output_level_dbu = 5;
static readonly is_a2 = 6; static readonly has_slimmable_weights = 6;
static readonly model_weight = 7; static readonly model_weight = 7;
static readonly slimmable_weights_size = 8; static readonly model_type = 8;
static readonly slimmable_weights = 9; static readonly max_medatadata = 9;
static readonly max_medatadata = TOOB_NAM_METADATA_OFFSETS.slimmable_weights + MAX_SLIMMABLE_WEIGHTS;
}; };
@@ -81,12 +90,9 @@ class ModelMetadata {
this.inputlevelDBU = metadataValues[TOOB_NAM_METADATA_OFFSETS.input_level_dbu]; this.inputlevelDBU = metadataValues[TOOB_NAM_METADATA_OFFSETS.input_level_dbu];
this.outputlevelDBU = metadataValues[TOOB_NAM_METADATA_OFFSETS.output_level_dbu]; this.outputlevelDBU = metadataValues[TOOB_NAM_METADATA_OFFSETS.output_level_dbu];
this.preset_version = metadataValues[TOOB_NAM_METADATA_OFFSETS.preset_version]; this.preset_version = metadataValues[TOOB_NAM_METADATA_OFFSETS.preset_version];
this.isA2 = metadataValues[TOOB_NAM_METADATA_OFFSETS.is_a2] !== 0; this.hasSlimmableWeights = metadataValues[TOOB_NAM_METADATA_OFFSETS.has_slimmable_weights] !== 0;
this.modelWeight = metadataValues[TOOB_NAM_METADATA_OFFSETS.model_weight]; this.modelWeight = metadataValues[TOOB_NAM_METADATA_OFFSETS.model_weight];
let slimmableWeightsSize = metadataValues[TOOB_NAM_METADATA_OFFSETS.slimmable_weights_size]; this.modelType = floatToModelType(metadataValues[TOOB_NAM_METADATA_OFFSETS.model_type]);
for (let i = 0; i < slimmableWeightsSize && i < 16; ++i) {
this.slimmable_weights.push(metadataValues[TOOB_NAM_METADATA_OFFSETS.slimmable_weights + i]);
}
} else { } else {
this.hasModel = false; this.hasModel = false;
this.hasLoudness = false; this.hasLoudness = false;
@@ -96,34 +102,29 @@ class ModelMetadata {
this.loudness = 0; this.loudness = 0;
this.gain = 0; this.gain = 0;
this.hasSlimmableWeights = false;
this.inputlevelDBU = 0; this.inputlevelDBU = 0;
this.outputlevelDBU = 0; this.outputlevelDBU = 0;
this.preset_version = 1; this.preset_version = 1;
this.modelWeight = -1; this.modelWeight = -1;
this.isA2 = false;
this.slimmable_weights = [];
} }
} }
preset_version: number; preset_version: number;
isA2: boolean = false; hasSlimmableWeights: boolean = false;
modelWeight: number = 1.0; modelWeight: number = 1.0;
slimmable_weights: number[] = []; modelType: NamModelType = NamModelType.None;
hasModel: boolean; hasModel: boolean = false;
hasLoudness: boolean; hasLoudness: boolean = false;
hasGain: boolean; hasGain: boolean = false;
hasInputLevelDBU: boolean; hasInputLevelDBU: boolean = false;
hasOutputLevelDBU: boolean; hasOutputLevelDBU: boolean = false;
loudness: number; loudness: number;
gain: number; gain: number;
inputlevelDBU: number; inputlevelDBU: number;
outputlevelDBU: number; outputlevelDBU: number;
hasSlimmableWeights(): boolean {
return this.slimmable_weights.length > 0;
}
} }
@@ -213,10 +214,9 @@ const ToobNamView =
} }
modifyControls(host: ICustomizationHost, controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] { modifyControls(host: ICustomizationHost, controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] {
let EqPos = 7; const ModelWeightControlPos = 3;
const CalibrationGroupPos = 8; const EqPos = 8;
const ModelSizeControlPos = 9; const CalibrationGroupPos = 9;
const ModelWeightControlPos = 10;
let calibrationGroup = controls[CalibrationGroupPos] as ControlGroup; let calibrationGroup = controls[CalibrationGroupPos] as ControlGroup;
@@ -250,57 +250,15 @@ const ToobNamView =
} else { } else {
controls[CalibrationGroupPos] = null; controls[CalibrationGroupPos] = null;
} }
controls[ModelSizeControlPos] = null;
controls[ModelWeightControlPos] = null;
if (!this.state.showEqSection) { if (!this.state.showEqSection) {
controls.splice(EqPos, 1); controls[EqPos] = null;
} }
if (this.state.modelMetadata.hasSlimmableWeights()) { if (!this.state.modelMetadata.hasSlimmableWeights) {
controls.splice(3, 0, this.MakeModelWeightSelector(host)); controls[ModelWeightControlPos] = null;
} }
return controls; 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) { private handleConnectionStateChanged(state: State) {
if (state === State.Ready) { 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, ' ')); 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'); throw new Error('Missing both toneId and modelId in OAuth callback response');
} }
return { ok: true, tokens, toneId, modelId, ...(canceled ? { canceled: true } : {}) }; return { ok: true, tokens, toneId, modelId, ...(canceled ? { canceled: true } : {}) };