diff --git a/CMakeLists.txt b/CMakeLists.txt index f4448b6..9b349d8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,13 +1,13 @@ cmake_minimum_required(VERSION 3.16.0) project(pipedal - VERSION 1.5.93 + VERSION 1.5.94 DESCRIPTION "PiPedal Guitar Effect Pedal For Raspberry Pi" HOMEPAGE_URL "https://rerdavies.github.io/pipedal" ) execute_process( COMMAND dpkg --print-architecture COMMAND tr -d '\n' OUTPUT_VARIABLE DEBIAN_ARCHITECTURE ) -set (DISPLAY_VERSION "PiPedal v1.5.93-Experimental") +set (DISPLAY_VERSION "PiPedal v1.5.94-Experimental") set (PACKAGE_ARCHITECTURE ${DEBIAN_ARCHITECTURE}) set (CMAKE_INSTALL_PREFIX "/usr/") diff --git a/README.md b/README.md index b2416e1..e9be1df 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,11 @@ -Download: v1.5.93 +Download: v1.5.94 Website: [https://rerdavies.github.io/pipedal](https://rerdavies.github.io/pipedal). Documentation: [https://rerdavies.github.io/pipedal/Documentation.html](https://rerdavies.github.io/pipedal/Documentation.html). -#### NEW version 1.5.93 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 1.5.94 Release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details. TooB NAM calibration, manipulation of i/r files in Toob Convolution Reverb, new Parametric EQ and 3 Band EQ plugins, and MORE!   diff --git a/docs/Installing.md b/docs/Installing.md index 25bd308..ce9e32b 100644 --- a/docs/Installing.md +++ b/docs/Installing.md @@ -13,18 +13,18 @@ page_icon: img/Install4.jpg Download the most recent Debian (.deb) package for your platform: -- [Raspberry Pi OS bookworm (aarch64) v1.5.93](https://github.com/rerdavies/pipedal/releases/download/v1.5.93/pipedal_1.5.93_arm64.deb) -- [Ubuntu 24.04 through 25.04 (aarch64) v1.5.93](https://github.com/rerdavies/pipedal/releases/download/v1.5.93/pipedal_1.5.93_arm64.deb) -- [Ubuntu 24.04 through 25.04 (amd64) v1.5.93](https://github.com/rerdavies/pipedal/releases/download/v1.5.93/pipedal_1.5.93_amd64.deb) +- [Raspberry Pi OS bookworm (aarch64) v1.5.94](https://github.com/rerdavies/pipedal/releases/download/v1.5.94/pipedal_1.5.94_arm64.deb) +- [Ubuntu 24.04 through 25.04 (aarch64) v1.5.94](https://github.com/rerdavies/pipedal/releases/download/v1.5.94/pipedal_1.5.94_arm64.deb) +- [Ubuntu 24.04 through 25.04 (amd64) v1.5.94](https://github.com/rerdavies/pipedal/releases/download/v1.5.94/pipedal_1.5.94_amd64.deb) -Version 1.5.93 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 1.5.94 has been tested on Raspberry Pi OS bookworm, Ubuntu 24.04 (amd64), Ubuntu 24.10 (aarch64), and Ubuntu 25.04 (aarch64). Download the appropriate package for your platform, and install using the following procedure: ``` sudo apt update sudo apt upgrade cd ~/Downloads - sudo apt-get install ./pipedal_1.5.93_arm64.deb + sudo apt-get install ./pipedal_1.5.94_arm64.deb ``` You MUST use `apt-get`. `apt` will not install downloaded packages; and `dpkg -i` will not install dependencies. diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index cc7141c..389e425 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,5 +1,16 @@ # Release Notes +## PiPedal 1.5.94 Beta + +A minor release that allows use of plugins with audio sidechains. + +### Features in this release + +- **Sidechains**. You can now load plugins that have sidechain inputs. This allows you to use compressor and gate plugins, particularly, that often have sidechains, but also enables a broad class of dynamics processing plugins that would not be loadable in previous versions of PiPedal. Plugins that have sidechain inputs will show a dropdown control in their control view which allows you to select the source of the sidechain input. The source plugin for the sidechain must appear earlier in the signal chain. + + Although sidechain processing is occasionally useful in guitar effect pedalboards, the more important part of this feature is that it allows you to load plugins that were rejected by previous versions of PiPedal because they had optional sidechain inputs. + + ## PiPedal 1.5.93 Release ### Major Features in This Build diff --git a/docs/UsingLv2Plugins.md b/docs/UsingLv2Plugins.md index c6f89a8..1e8f0ac 100644 --- a/docs/UsingLv2Plugins.md +++ b/docs/UsingLv2Plugins.md @@ -17,7 +17,7 @@ Here is a brief list of other particularly recommended plugin collections. | Collection | To Install | Description | |---------------------------------|---------------------------------------|------------------| | [Guitarix](https://guitarix.org) ★★★☆☆ | `sudo apt install guitarix-lv2` | A large collection of guitar amplifiers and effects. Prior to the advent of neural network amplifier emulations, Guitarix was the go-to source for open-source amp modeling.| -| [GxPlugins](https://github.com/brummer10/GxPlugins.lv2) ★★★★★ | `sudo apt install gxplugins` | Additional amps and effects from the the principle developer of Guitarix, many of which are significantly superior to the original Guitarix plugins. Most of these plugins +| [GxPlugins](https://github.com/brummer10/GxPlugins.lv2) ★★★★★ | On Raspberry Pi Os: `sudo apt install gxplugins`, on amd64/x64 donwload from [Github](https://github.com/brummer10/GxPlugins.lv2/releases) | Additional amps and effects from the the principle developer of Guitarix, many of which are significantly superior to the original Guitarix plugins. Most of these plugins use circuit simulation to directly model actual original circuits. The results are uniformly excellent.| | [DPF Plugins](https://github.com/DISTRHO/DPF-Plugins) ★★★★★ | `sudo apt install dpf-plugins-lv2` | Useful and interesting guitar effect plugins. Bit crusher, pitch shift, reverbs, 3-band EQ and more. | | [MDA Plugins](http://drobilla.net/software/mda-lv2.html) ★★★★☆ | `sudo apt install mda-lv2` | 36 high-quality plugs | diff --git a/docs/download.md b/docs/download.md index c54df26..e3fdef9 100644 --- a/docs/download.md +++ b/docs/download.md @@ -4,9 +4,9 @@ Download the most recent Debian (.deb) package for your platform: -- [Raspberry Pi OS bookworm (aarch64) v1.5.93 Beta](https://github.com/rerdavies/pipedal/releases/download/v1.5.93/pipedal_1.5.93_arm64.deb) -- [Ubuntu 24.x, 25.04 (aarch64) v1.5.93 Alpha](https://github.com/rerdavies/pipedal/releases/download/v1.5.93/pipedal_1.5.93_arm64.deb) -- [Ubuntu 24.x, 25.04 (amd64) v1.5.93 Alpha](https://github.com/rerdavies/pipedal/releases/download/v1.5.93/pipedal_1.5.93_amd64.deb) +- [Raspberry Pi OS bookworm (aarch64) v1.5.94 Beta](https://github.com/rerdavies/pipedal/releases/download/v1.5.94/pipedal_1.5.94_arm64.deb) +- [Ubuntu 24.x, 25.04 (aarch64) v1.5.94 Alpha](https://github.com/rerdavies/pipedal/releases/download/v1.5.94/pipedal_1.5.94_arm64.deb) +- [Ubuntu 24.x, 25.04 (amd64) v1.5.94 Alpha](https://github.com/rerdavies/pipedal/releases/download/v1.5.94/pipedal_1.5.94_amd64.deb) Install the package by running @@ -14,7 +14,7 @@ Install the package by running ``` sudo apt update cd ~/Downloads - sudo apt-get install ./pipedal_1.5.93_arm64.deb + sudo apt-get install ./pipedal_1.5.94_arm64.deb ``` You MUST use `apt-get` to install the package. `apt install` will NOT install the package correctly. The message about missing permissions given by `apt-get` is expected, and can be safely ignored. diff --git a/modules/libzip b/modules/libzip index 9203514..fb59b49 160000 --- a/modules/libzip +++ b/modules/libzip @@ -1 +1 @@ -Subproject commit 92035140dfd80f4d31dd33590acb018da21cdfb0 +Subproject commit fb59b49dff933d52e85b3a772b52bf004aa60fc8 diff --git a/src/IEffect.hpp b/src/IEffect.hpp index b5a4ce7..beec4ee 100644 --- a/src/IEffect.hpp +++ b/src/IEffect.hpp @@ -54,7 +54,9 @@ namespace pipedal { virtual int GetNumberOfInputAudioBuffers() const = 0; // may be different if plugin has zero inputs. virtual int GetNumberOfOutputAudioBuffers() const = 0; // may be different if plugin has zero inputs. + virtual int GetNumberOfSidechainAudioBuffers() const { return 0; } virtual float *GetAudioInputBuffer(int index) const = 0; + virtual float *GetAudioSidechainBuffer(int index) const { throw std::runtime_error("Not implemented"); } virtual float *GetAudioOutputBuffer(int index) const = 0; virtual void ResetAtomBuffers() = 0; @@ -65,6 +67,7 @@ namespace pipedal { virtual void SetAudioInputBuffer(int index, float *buffer) = 0; virtual void SetAudioOutputBuffer(int index, float*buffer) = 0; + virtual void SetAudioSidechainBuffer(int index, float *buffer) { throw std::runtime_error("Not implemented");} virtual void Activate() = 0; diff --git a/src/Lv2Effect.cpp b/src/Lv2Effect.cpp index fd339b0..4551259 100644 --- a/src/Lv2Effect.cpp +++ b/src/Lv2Effect.cpp @@ -384,7 +384,12 @@ void Lv2Effect::PreparePortIndices() { if (port->is_input()) { - this->inputAudioPortIndices.push_back(portIndex); + if (port->is_sidechain()) + { + this->inputSidechainPortIndices.push_back(portIndex); + } else { + this->inputAudioPortIndices.push_back(portIndex); + } } else { @@ -432,6 +437,7 @@ void Lv2Effect::PreparePortIndices() this->maxInputControlPort = maxInputControlPort; inputAudioBuffers.resize(inputAudioPortIndices.size()); + inputSidechainBuffers.resize(inputSidechainPortIndices.size()); outputAudioBuffers.resize(outputAudioPortIndices.size()); inputAtomBuffers.resize(inputAtomPortIndices.size()); outputAtomBuffers.resize(outputAtomPortIndices.size()); @@ -439,9 +445,7 @@ void Lv2Effect::PreparePortIndices() if (RequiresBufferStaging()) { EnableBufferStaging( - GetStagedBufferSize(), - this->GetNumberOfInputAudioBuffers(), - this->GetNumberOfOutputAudioBuffers()); + GetStagedBufferSize()); } } @@ -473,6 +477,7 @@ void Lv2Effect::PrepareNoInputEffect(int numberOfInputs, size_t maxBufferSize) } } + void Lv2Effect::SetAudioInputBuffer(int index, float *buffer) { this->inputAudioBuffers.at(index) = buffer; @@ -512,6 +517,32 @@ void Lv2Effect::SetAudioInputBuffer(int index, float *buffer) // } } } +void Lv2Effect::SetAudioSidechainBuffer(int index, float *buffer) +{ + this->inputSidechainBuffers.at(index) = buffer; + + if (borrowedEffect) + { + // Already running on the realtime thread, + // so don't update the audio ports until the effect gets placed on the realtime thread. + return; + } + + if (stagingBufferSize != 0) + { + int pluginIndex = this->inputSidechainPortIndices.at(index); + if (index >= sidechainStagingBufferPointers.size()) + { + throw std::runtime_error("Invalid input staging buffer index."); + } + lilv_instance_connect_port(this->pInstance, pluginIndex, sidechainStagingBufferPointers.at(index)); + } + else + { + int pluginIndex = this->inputSidechainPortIndices.at(index); + lilv_instance_connect_port(this->pInstance, pluginIndex, buffer); + } +} void Lv2Effect::SetAudioInputBuffer(float *left) { @@ -664,6 +695,14 @@ void Lv2Effect::UpdateAudioPorts() lilv_instance_connect_port(pInstance, portIndex, outputStagingBufferPointers.at(i)); } } + for (size_t i = 0; i < this->inputSidechainPortIndices.size(); ++i) + { + int portIndex = this->inputSidechainPortIndices.at(i); + if (sidechainStagingBufferPointers.at(i) != nullptr) + { + lilv_instance_connect_port(pInstance, portIndex, sidechainStagingBufferPointers.at(i)); + } + } for (size_t i = 0; i < this->inputAtomPortIndices.size(); ++i) { if (i == 0) @@ -710,6 +749,15 @@ void Lv2Effect::UpdateAudioPorts() lilv_instance_connect_port(pInstance, portIndex, GetAudioOutputBuffer(i)); } } + for (size_t i = 0; i < this->inputSidechainPortIndices.size(); ++i) + { + int portIndex = this->inputSidechainPortIndices.at(i); + if (GetAudioSidechainBuffer(i) != nullptr) + { + lilv_instance_connect_port(pInstance, portIndex, GetAudioSidechainBuffer(i)); + } + } + for (size_t i = 0; i < this->inputAtomPortIndices.size(); ++i) { auto atomInputBuffer = this->GetAtomInputBuffer(i); @@ -877,6 +925,16 @@ size_t Lv2Effect::stageToInput(size_t inputSampleOffset, size_t samples) pOutput[i] = pInput[i]; } } + for (size_t nSidechain = 0; nSidechain < this->inputSidechainBuffers.size(); ++nSidechain) + { + float *restrict pInput = this->inputSidechainBuffers[nSidechain] + inputSampleOffset; + float *restrict pOutput = this->sidechainStagingBufferPointers.at(nSidechain) + this->stagingInputIx; + for (size_t i = 0; i < thisTime; ++i) + { + pOutput[i] = pInput[i]; + } + + } this->stagingInputIx += thisTime; inputSampleOffset += thisTime; @@ -1487,8 +1545,11 @@ void Lv2Effect::SetPathPatchProperty(const std::string &propertyUri, const std:: mainThreadPathProperties[propertyUri] = jsonAtom; } -void Lv2Effect::EnableBufferStaging(size_t bufferSize, size_t nInputs, size_t nOutputs) +void Lv2Effect::EnableBufferStaging(size_t bufferSize ) { + size_t nInputs = this->GetNumberOfInputAudioBuffers(); + size_t nSidechainInputs = this->GetNumberOfSidechainAudioBuffers(); + size_t nOutputs = this->GetNumberOfOutputAudioBuffers(); stagingBufferSize = bufferSize; stagingOutputIx = bufferSize; @@ -1520,6 +1581,12 @@ void Lv2Effect::EnableBufferStaging(size_t bufferSize, size_t nInputs, size_t nO inputStagingBuffers[i][bufferSize] = 99.9f; // guard entry inputStagingBufferPointers.at(i) = inputStagingBuffers.at(i).data(); } + for (size_t i = 0; i < nSidechainInputs; ++i) + { + sidechainStagingBuffers.at(i).resize(bufferSize + 1); + sidechainStagingBuffers[i][bufferSize] = 99.9f; // guard entry + sidechainStagingBufferPointers.at(i) = sidechainStagingBuffers.at(i).data(); + } for (size_t i = 0; i < nOutputs; ++i) { outputStagingBuffers.at(i).resize(bufferSize + 1); @@ -1571,6 +1638,12 @@ float *Lv2Effect::GetAudioInputBuffer(int index) const throw std::range_error("Lv2Effect::GetAudioInputBuffer"); return this->inputAudioBuffers.at(index); } +float *Lv2Effect::GetAudioSidechainBuffer(int index) const +{ + if (index < 0 || index >= this->inputSidechainBuffers.size()) + throw std::range_error("Lv2Effect::GetAudioSidechainBuffer"); + return this->inputSidechainBuffers.at(index); +} float *Lv2Effect::GetAudioOutputBuffer(int index) const { diff --git a/src/Lv2Effect.hpp b/src/Lv2Effect.hpp index 4abba0d..76c577c 100644 --- a/src/Lv2Effect.hpp +++ b/src/Lv2Effect.hpp @@ -81,6 +81,8 @@ namespace pipedal std::vector inputAudioPortIndices; std::vector outputAudioPortIndices; + std::vector inputSidechainPortIndices; + std::vector inputAtomPortIndices; std::vector outputAtomPortIndices; @@ -90,6 +92,7 @@ namespace pipedal std::vector midiInputIndices; std::vector inputAudioBuffers; + std::vector inputSidechainBuffers; std::vector outputAudioBuffers; std::vector inputAtomBuffers; @@ -212,7 +215,7 @@ namespace pipedal bool borrowedEffect = false; bool activated = false; - void EnableBufferStaging(size_t bufferSize, size_t nInputs, size_t nOutputs); + void EnableBufferStaging(size_t bufferSize); void CheckStagingBufferSentries(); public: @@ -262,8 +265,10 @@ namespace pipedal size_t stagingInputIx = 0; size_t stagingOutputIx = 0; std::vector> inputStagingBuffers; + std::vector> sidechainStagingBuffers; std::vector> outputStagingBuffers; std::vector inputStagingBufferPointers; + std::vector sidechainStagingBufferPointers; std::vector outputStagingBufferPointers; std::vector stagedInputAtomBuffer; @@ -297,22 +302,26 @@ namespace pipedal virtual void ResetAtomBuffers(); virtual uint64_t GetInstanceId() const { return instanceId; } - virtual int GetNumberOfInputAudioPorts() const { return inputAudioPortIndices.size(); } - virtual int GetNumberOfOutputAudioPorts() const { return outputAudioPortIndices.size(); } + virtual int GetNumberOfInputAudioPorts() const override { return inputAudioPortIndices.size(); } + virtual int GetNumberOfOutputAudioPorts() const override { return outputAudioPortIndices.size(); } - virtual int GetNumberOfInputAtomPorts() const { return inputAtomPortIndices.size(); } + virtual int GetNumberOfInputAtomPorts() const { return inputAtomPortIndices.size(); } - virtual int GetNumberOfOutputAtomPorts() const { return outputAtomPortIndices.size(); } - virtual int GetNumberOfMidiInputPorts() const { return inputMidiPortIndices.size(); } - virtual int GetNumberOfMidiOutputPorts() const { return outputMidiPortIndices.size(); } + virtual int GetNumberOfOutputAtomPorts() const { return outputAtomPortIndices.size(); } + virtual int GetNumberOfMidiInputPorts() const { return inputMidiPortIndices.size(); } + virtual int GetNumberOfMidiOutputPorts() const { return outputMidiPortIndices.size(); } - virtual int GetNumberOfInputAudioBuffers() const { return this->inputAudioBuffers.size(); } - virtual int GetNumberOfOutputAudioBuffers() const {return this->outputAudioBuffers.size(); } + virtual int GetNumberOfInputAudioBuffers() const override { return this->inputAudioBuffers.size(); } + virtual int GetNumberOfSidechainAudioBuffers() const override { return this->inputSidechainBuffers.size(); } + virtual int GetNumberOfOutputAudioBuffers() const override {return this->outputAudioBuffers.size(); } - virtual void SetAudioInputBuffer(int index, float *buffer); - virtual float *GetAudioInputBuffer(int index) const; + virtual void SetAudioInputBuffer(int index, float *buffer) override; + virtual float *GetAudioInputBuffer(int index) const override; + + virtual void SetAudioSidechainBuffer(int index, float *buffer) override; + virtual float *GetAudioSidechainBuffer(int index) const override; virtual void SetAudioInputBuffer(float *buffer); diff --git a/src/Lv2Pedalboard.cpp b/src/Lv2Pedalboard.cpp index 780ce82..ede9b78 100644 --- a/src/Lv2Pedalboard.cpp +++ b/src/Lv2Pedalboard.cpp @@ -176,6 +176,60 @@ std::vector Lv2Pedalboard::PrepareItems( auto bufferR = inputBuffers[1]; } } + // Connect sidechain buffers. + + if (pLv2Effect->GetNumberOfSidechainAudioBuffers() != 0) + { + if (item.sideChainInputId() == -2) // -2 means "use pedalboard inputs". + { + for (size_t i = 0; i < pLv2Effect->GetNumberOfSidechainAudioBuffers(); ++i) + { + if (i < this->pedalboardInputBuffers.size()) + { + pLv2Effect->SetAudioSidechainBuffer(i, this->pedalboardInputBuffers[i]); + } + else + { + // just use the first output buffer for all sidechain inputs. + pLv2Effect->SetAudioSidechainBuffer(i, this->pedalboardInputBuffers[0]); + } + } + } else if (item.sideChainInputId() != -1) { + IEffect *pSideChainInput = GetEffect(item.sideChainInputId()); + + if (pSideChainInput) + { + for (size_t i = 0; i < pLv2Effect->GetNumberOfSidechainAudioBuffers(); ++i) + { + if (i < pSideChainInput->GetNumberOfOutputAudioBuffers()) + { + pLv2Effect->SetAudioSidechainBuffer(i, pSideChainInput->GetAudioOutputBuffer(i)); + } + else + { + // just use the first output buffer for all sidechain inputs. + pLv2Effect->SetAudioSidechainBuffer(i, pSideChainInput->GetAudioOutputBuffer(0)); + } + } + } else { + throw std::runtime_error("Internal error: Sidechain input IEffect not found."); + } + } + else + { + // No sidechain input. Feed zero buffer to all sidechain audio ports.. + for (size_t i = 0; i < pLv2Effect->GetNumberOfSidechainAudioBuffers(); ++i) + { + + // just use one buffer for all plugins + if (!this->pedalboardSidechainBuffer) + { + this->pedalboardSidechainBuffer = CreateNewAudioBuffer(); + } + pLv2Effect->SetAudioSidechainBuffer(i, this->pedalboardSidechainBuffer); + } + } + } // check to see whether we need buffer staging. bool requiresBufferStaging = false; @@ -194,7 +248,6 @@ std::vector Lv2Pedalboard::PrepareItems( } } - if (!requiresBufferStaging) { this->processActions.push_back( diff --git a/src/Lv2Pedalboard.hpp b/src/Lv2Pedalboard.hpp index 5cb3fe9..961b3e4 100644 --- a/src/Lv2Pedalboard.hpp +++ b/src/Lv2Pedalboard.hpp @@ -57,6 +57,7 @@ namespace pipedal BufferPool bufferPool; std::vector pedalboardInputBuffers; std::vector pedalboardOutputBuffers; + float *pedalboardSidechainBuffer = nullptr; std::vector> effects; std::vector realtimeEffects; diff --git a/src/Pedalboard.cpp b/src/Pedalboard.cpp index 89324c9..3c8ad17 100644 --- a/src/Pedalboard.cpp +++ b/src/Pedalboard.cpp @@ -534,6 +534,7 @@ JSON_MAP_BEGIN(PedalboardItem) JSON_MAP_REFERENCE(PedalboardItem,title) JSON_MAP_REFERENCE(PedalboardItem,useModUi) JSON_MAP_REFERENCE(PedalboardItem,iconColor) + JSON_MAP_REFERENCE(PedalboardItem,sideChainInputId) JSON_MAP_END() diff --git a/src/Pedalboard.hpp b/src/Pedalboard.hpp index 995ddcb..173b497 100644 --- a/src/Pedalboard.hpp +++ b/src/Pedalboard.hpp @@ -107,6 +107,7 @@ public: std::string title_; bool useModUi_ = false; std::string iconColor_; + int64_t sideChainInputId_ = -1; // non persistent state. PropertyMap patchProperties; @@ -138,6 +139,7 @@ public: GETTER_SETTER_REF(title) GETTER_SETTER_REF(iconColor) GETTER_SETTER(useModUi) + GETTER_SETTER(sideChainInputId) Lv2PluginState&lv2State() { return lv2State_; } // non-const version. GETTER_SETTER_REF(lilvPresetUri) diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index 831de12..70adb50 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -2428,7 +2428,7 @@ void PiPedalModel::OnNotifyPathPatchPropertyReceived( auto i = pedalboardItem->pathProperties_.find(pathPatchPropertyUri); if (i != pedalboardItem->pathProperties_.end()) { - std::string abstractAtomString = storage.ToAbstractPathJson(atomString); + std::string abstractAtomString = storage.ToAbstractPathFromJson(atomString); pedalboardItem->pathProperties_[pathPatchPropertyUri] = abstractAtomString; std::vector t{subscribers.begin(), subscribers.end()}; diff --git a/src/PluginHost.cpp b/src/PluginHost.cpp index d4d7bd2..60ddc35 100644 --- a/src/PluginHost.cpp +++ b/src/PluginHost.cpp @@ -121,6 +121,7 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld) enumeration_property_uri = lilv_new_uri(pWorld, LV2_CORE__enumeration); core__toggled = lilv_new_uri(pWorld, LV2_CORE__toggled); core__connectionOptional = lilv_new_uri(pWorld, LV2_CORE__connectionOptional); + core__isSideChain = lilv_new_uri(pWorld, LV2_CORE_PREFIX "isSideChain"); // missing in lv2.h portprops__not_on_gui_property_uri = lilv_new_uri(pWorld, LV2_PORT_PROPS__notOnGUI); portprops__trigger = lilv_new_uri(pWorld, LV2_PORT_PROPS__trigger); midi__event = lilv_new_uri(pWorld, LV2_MIDI__MidiEvent); @@ -134,6 +135,7 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld) atom__bufferType = lilv_new_uri(pWorld, LV2_ATOM__bufferType); atom__Path = lilv_new_uri(pWorld, LV2_ATOM__Path); + atom__String = lilv_new_uri(pWorld, LV2_ATOM__String); presets__preset = lilv_new_uri(pWorld, LV2_PRESETS__Preset); state__state = lilv_new_uri(pWorld, LV2_STATE__state); rdfs__label = lilv_new_uri(pWorld, LILV_NS_RDFS "label"); @@ -214,6 +216,7 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld) buf_size__maxBlockLength = lilv_new_uri(pWorld, LV2_BUF_SIZE__maxBlockLength); buf_size__fixedBlockLength = lilv_new_uri(pWorld, LV2_BUF_SIZE__fixedBlockLength); buf_size__coarseBlockLength = lilv_new_uri(pWorld, LV2_BUF_SIZE__coarseBlockLength); + port_groups__sideChainOf = lilv_new_uri(pWorld, LV2_PORT_GROUPS__sideChainOf); } @@ -794,7 +797,12 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin } else { - unsupportedPatchProperty = true; + if (lilv_world_ask(pWorld, propertyUri, lv2Host->lilvUris->rdfs__range, lv2Host->lilvUris->atom__String)) + { + + } else { + unsupportedPatchProperty = true; + } } } } @@ -945,6 +953,60 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP std::sort(ports_.begin(), ports_.end(), ports_sort_compare); + + + // Check the portgroup for audio inputs to see if they are sidechains. + std::string last_sidechain_title; + for (auto&port: this->ports_) + { + if (port->is_audio_port() && port->is_input()) { + if (!port->port_group().empty()) + { + for (auto &pg: this->port_groups_) + { + if (pg->uri() == port->port_group()) + { + if (!pg->sideChainOf().empty()) + { + port->is_sidechain(true); + audio_sidechain_title_ = pg->name(); + if (last_sidechain_title.empty()) { + last_sidechain_title = pg->name(); + } + else if (last_sidechain_title != pg->name()) { + // multiple sidechain titles! + isValid = false; + Lv2Log::debug("Plugin %s (%s) has multiple sidechain titles (%s and %s).", + this->name_.c_str(), + this->uri_.c_str(), + last_sidechain_title.c_str(), + pg->name().c_str()); + } + } + break; + } + } + } else { + if (port->is_sidechain()) { + // port is marked as sidechain, but has no port group. + if (last_sidechain_title.empty()) + { + last_sidechain_title = port->name(); + this->audio_sidechain_title_ = port->name(); + } + else if (last_sidechain_title != port->name()) { + isValid = false; + Lv2Log::debug("Plugin %s (%s) has multiple sidechain ports, but no portgroup. (%s)", + this->name_.c_str(), + this->uri_.c_str(), + port->name().c_str()); + } + } + } + + } + } + // Fetch patch properties. { AutoLilvNodes patchWritables = lilv_world_find_nodes(pWorld, plugUri, lv2Host->lilvUris->patch__writable, nullptr); @@ -1034,17 +1096,28 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP } int nInputs = 0; + int nSideChains = 0; for (size_t i = 0; i < ports_.size(); ++i) { auto port = ports_[i]; if (port->is_audio_port() && port->is_input()) { - if (nInputs >= 2 && !port->connection_optional()) - { - isValid = false; - break; + if (!port->is_sidechain()) { + if (nInputs >= 2) + { + isValid = false; + Lv2Log::debug("Plugin %s (%s) has more than 2 audio inputs.", this->name_.c_str(), this->uri_.c_str()); + break; + } + ++nInputs; + } else { + if (nSideChains >= 2) { + Lv2Log::debug("Plugin %s (%s) has more than 2 audio sidechain inputs.", this->name_.c_str(), this->uri_.c_str()); + isValid = false; + break; + } + ++nSideChains; } - ++nInputs; } } int nOutputs = 0; @@ -1199,6 +1272,7 @@ Lv2PortInfo::Lv2PortInfo(PluginHost *host, const LilvPlugin *plugin, const LilvP this->not_on_gui_ = lilv_port_has_property(plugin, pPort, host->lilvUris->portprops__not_on_gui_property_uri); this->connection_optional_ = lilv_port_has_property(plugin, pPort, host->lilvUris->core__connectionOptional); this->trigger_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->portprops__trigger); + this->is_sidechain_ = lilv_port_has_property(plugin,pPort,host->lilvUris->core__isSideChain); AutoLilvNode port_ledColor = lilv_port_get(plugin, pPort, host->lilvUris->pipedalUI__ledColor); if (port_ledColor) @@ -1402,7 +1476,12 @@ Lv2PluginUiInfo::Lv2PluginUiInfo(PluginHost *pHost, const Lv2PluginInfo *plugin) } else if (port->is_audio_port()) { - ++audio_inputs_; + if (port->is_sidechain()) { + // sidechain outputs don't count. + ++audio_side_chain_inputs_; + } else { + ++audio_inputs_; + } } } else if (port->is_output()) @@ -1918,12 +1997,29 @@ Lv2PortGroup::Lv2PortGroup(PluginHost *lv2Host, const std::string &groupUri) { LilvWorld *pWorld = lv2Host->pWorld; + // Enumerate all Lilv nodes for groupUri that have an 'a' predicate + this->uri_ = groupUri; AutoLilvNode uri = lilv_new_uri(pWorld, groupUri.c_str()); + + + AutoLilvNode aPredicate = lilv_new_uri(pWorld, "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"); + + AutoLilvNodes types = lilv_world_find_nodes(pWorld, uri, aPredicate, nullptr); + LILV_FOREACH(nodes, iNode, types) + { + AutoLilvNode typeNode = lilv_nodes_get(types, iNode); + std::string typeUri = nodeAsString(typeNode); + this->isA_.push_back(std::move(typeUri)); + } + + AutoLilvNode symbolNode = lilv_world_get(pWorld, uri, lv2Host->lilvUris->lv2core__symbol, nullptr); symbol_ = nodeAsString(symbolNode); AutoLilvNode nameNode = lilv_world_get(pWorld, uri, lv2Host->lilvUris->lv2core__name, nullptr); name_ = nodeAsString(nameNode); + AutoLilvNode sideChainOfNode = lilv_world_get(pWorld, uri, lv2Host->lilvUris->port_groups__sideChainOf, nullptr); + sideChainOf_ = nodeAsString(sideChainOfNode); } bool Lv2PluginInfo::isSplit() const @@ -2187,6 +2283,7 @@ json_map::storage_type Lv2PortInfo::jmap{ json_map::reference("is_input", &Lv2PortInfo::is_input_), json_map::reference("is_output", &Lv2PortInfo::is_output_), + json_map::reference("is_sidechain", &Lv2PortInfo::is_sidechain_), json_map::reference("is_control_port", &Lv2PortInfo::is_control_port_), json_map::reference("is_audio_port", &Lv2PortInfo::is_audio_port_), @@ -2226,7 +2323,11 @@ json_map::storage_type Lv2PortInfo::jmap{ json_map::storage_type Lv2PortGroup::jmap{{ + MAP_REF(Lv2PortGroup, isA), MAP_REF(Lv2PortGroup, uri), + MAP_REF(Lv2PortGroup, name), + MAP_REF(Lv2PortGroup, symbol), + MAP_REF(Lv2PortGroup, sideChainOf), }}; @@ -2281,6 +2382,7 @@ json_map::storage_type Lv2PluginUiPort::jmap{{ json_map::reference("name", &Lv2PluginUiPort::name_), MAP_REF(Lv2PluginUiPort, index), MAP_REF(Lv2PluginUiPort, is_input), + MAP_REF(Lv2PluginUiPort, is_sidechain), MAP_REF(Lv2PluginUiPort, min_value), MAP_REF(Lv2PluginUiPort, max_value), MAP_REF(Lv2PluginUiPort, default_value), @@ -2324,7 +2426,9 @@ json_map::storage_type json_map::reference("author_name", &Lv2PluginUiInfo::author_name_), json_map::reference("author_homepage", &Lv2PluginUiInfo::author_homepage_), json_map::reference("audio_inputs", &Lv2PluginUiInfo::audio_inputs_), + json_map::reference("audio_side_chain_inputs", &Lv2PluginUiInfo::audio_side_chain_inputs_), json_map::reference("audio_outputs", &Lv2PluginUiInfo::audio_outputs_), + json_map::reference("audio_side_chain_title", &Lv2PluginUiInfo::audio_side_chain_title_), json_map::reference("description", &Lv2PluginUiInfo::description_), json_map::reference("has_midi_input", &Lv2PluginUiInfo::has_midi_input_), json_map::reference("has_midi_output", &Lv2PluginUiInfo::has_midi_output_), diff --git a/src/PluginHost.hpp b/src/PluginHost.hpp index 2712bad..ef9e220 100644 --- a/src/PluginHost.hpp +++ b/src/PluginHost.hpp @@ -33,7 +33,7 @@ #include #include "ModGui.hpp" -//#include "lv2.h" +// #include "lv2.h" #include "Units.hpp" #include "PluginPreset.hpp" @@ -206,6 +206,7 @@ namespace pipedal bool is_control_port_ = false; bool is_audio_port_ = false; + bool is_sidechain_ = false; bool is_atom_port_ = false; bool is_cv_port_ = false; bool connection_optional_ = false; @@ -287,6 +288,7 @@ namespace pipedal LV2_PROPERTY_GETSET_SCALAR(is_output); LV2_PROPERTY_GETSET_SCALAR(is_control_port); LV2_PROPERTY_GETSET_SCALAR(is_audio_port); + LV2_PROPERTY_GETSET_SCALAR(is_sidechain); LV2_PROPERTY_GETSET_SCALAR(is_atom_port); LV2_PROPERTY_GETSET_SCALAR(connection_optional); LV2_PROPERTY_GETSET_SCALAR(is_cv_port); @@ -321,7 +323,6 @@ namespace pipedal return Lv2BufferType::Unknown; } - public: Lv2PortInfo() {} ~Lv2PortInfo() = default; @@ -330,7 +331,8 @@ namespace pipedal static json_map::storage_type jmap; }; - class Lv2PatchPropertyInfo { + class Lv2PatchPropertyInfo + { private: std::string uri_; @@ -343,9 +345,10 @@ namespace pipedal std::string shortName_; std::vector fileTypes_; std::vector supportedExtensions_; + public: Lv2PatchPropertyInfo() {} - Lv2PatchPropertyInfo(PluginHost *pluginHost, const LilvNode *propertyUri); + Lv2PatchPropertyInfo(PluginHost *pluginHost, const LilvNode *propertyUri); LV2_PROPERTY_GETSET(uri); LV2_PROPERTY_GETSET(writable); @@ -359,21 +362,24 @@ namespace pipedal LV2_PROPERTY_GETSET(supportedExtensions); DECLARE_JSON_MAP(Lv2PatchPropertyInfo); - }; class Lv2PortGroup { private: + std::vector isA_; std::string uri_; std::string symbol_; std::string name_; + std::string sideChainOf_; public: + LV2_PROPERTY_GETSET(isA); LV2_PROPERTY_GETSET(uri); LV2_PROPERTY_GETSET(symbol); LV2_PROPERTY_GETSET(name); - + LV2_PROPERTY_GETSET(sideChainOf); + Lv2PortGroup() {} Lv2PortGroup(PluginHost *lv2Host, const std::string &groupUri); @@ -389,10 +395,11 @@ namespace pipedal using ptr = std::shared_ptr; Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvPlugin *); Lv2PluginInfo() {} - bool isSplit() const ; + bool isSplit() const; private: - struct FindWritablePathPropertiesResult { + struct FindWritablePathPropertiesResult + { std::shared_ptr pipedalUi; bool hasUnsupportedPatchProperties = false; }; @@ -406,7 +413,7 @@ namespace pipedal std::string name_; std::string plugin_class_; std::string brand_; - std::string label_; + std::string label_; std::vector supported_features_; std::vector required_features_; std::vector optional_features_; @@ -420,9 +427,9 @@ namespace pipedal std::vector> ports_; std::vector> port_groups_; std::vector patchProperties_; - + std::string audio_sidechain_title_; bool hasDefaultState_; - + bool is_valid_ = false; PiPedalUI::ptr piPedalUI_; ModGui::ptr modGui_; @@ -463,6 +470,7 @@ namespace pipedal LV2_PROPERTY_GETSET(minBlockLength) LV2_PROPERTY_GETSET(maxBlockLength) LV2_PROPERTY_GETSET(powerOf2BlockLength) + LV2_PROPERTY_GETSET(audio_sidechain_title) bool WantsWorkerThread() const; @@ -529,9 +537,11 @@ namespace pipedal class Lv2PluginUiPortGroup { private: + std::vector isA_; std::string uri_; std::string symbol_; std::string name_; + std::string sideChainOf_; std::string parent_group_; int32_t program_list_id_ = -1; // used by VST3. @@ -546,9 +556,7 @@ namespace pipedal public: Lv2PluginUiPortGroup() {} Lv2PluginUiPortGroup(Lv2PortGroup *pPortGroup) - : symbol_(pPortGroup->symbol()) - , name_(pPortGroup->name()) - , uri_(pPortGroup->uri()) + : isA_(pPortGroup->isA()), symbol_(pPortGroup->symbol()), name_(pPortGroup->name()), uri_(pPortGroup->uri()), sideChainOf_(pPortGroup->sideChainOf()) { } Lv2PluginUiPortGroup( @@ -570,14 +578,18 @@ namespace pipedal } Lv2PluginUiPort(const Lv2PluginInfo *pPlugin, const Lv2PortInfo *pPort) : symbol_(pPort->symbol()), index_(pPort->index()), - is_input_(pPort->is_input()), name_(pPort->name()), min_value_(pPort->min_value()), max_value_(pPort->max_value()), + is_input_(pPort->is_input()), + is_sidechain_(pPort->is_sidechain()), + name_(pPort->name()), + min_value_(pPort->min_value()), + max_value_(pPort->max_value()), default_value_(pPort->default_value()), range_steps_(pPort->range_steps()), display_priority_(pPort->display_priority()), - is_logarithmic_(pPort->is_logarithmic()), - integer_property_(pPort->integer_property()), + is_logarithmic_(pPort->is_logarithmic()), + integer_property_(pPort->integer_property()), mod_momentaryOffByDefault_(pPort->mod_momentaryOffByDefault()), mod_momentaryOnByDefault_(pPort->mod_momentaryOnByDefault()), pipedal_graphicEq_(pPort->pipedal_graphicEq()), - + enumeration_property_(pPort->enumeration_property()), toggled_property_(pPort->toggled_property()), not_on_gui_(pPort->not_on_gui()), scale_points_(pPort->scale_points()), trigger_property_(pPort->trigger_property()), @@ -606,7 +618,7 @@ namespace pipedal } } } - is_bypass_ = + is_bypass_ = pPort->is_bypass() || name_ == "bypass" || name_ == "Bypass" || symbol_ == "bypass" || symbol_ == "Bypass"; } @@ -616,6 +628,7 @@ namespace pipedal int index_; std::string name_; bool is_input_ = true; + bool is_sidechain_ = false; float min_value_ = 0, max_value_ = 1, default_value_ = 0; bool is_logarithmic_ = false; int display_priority_ = -1; @@ -682,7 +695,8 @@ namespace pipedal std::string uri_; std::string name_; uint32_t minorVersion_ = 0; - uint32_t microVersion_ = 0;; + uint32_t microVersion_ = 0; + ; std::string brand_; std::string label_; std::string author_name_; @@ -690,12 +704,17 @@ namespace pipedal PluginType plugin_type_; std::string plugin_display_type_; int audio_inputs_ = 0; + int audio_side_chain_inputs_ = 0; int audio_outputs_ = 0; + std::string audio_side_chain_title_; int has_midi_input_ = false; int has_midi_output_ = false; std::string description_; bool is_vst3_ = false; + int64_t side_chain_input_id = -1; + + std::vector controls_; std::vector port_groups_; std::vector fileProperties_; @@ -704,7 +723,6 @@ namespace pipedal ModGui::ptr modGui_; std::vector patchProperties_; - public: LV2_PROPERTY_GETSET(uri) LV2_PROPERTY_GETSET(name) @@ -718,6 +736,7 @@ namespace pipedal LV2_PROPERTY_GETSET(plugin_display_type) LV2_PROPERTY_GETSET_SCALAR(audio_inputs) LV2_PROPERTY_GETSET_SCALAR(audio_outputs) + LV2_PROPERTY_GETSET(audio_side_chain_title) LV2_PROPERTY_GETSET_SCALAR(has_midi_input) LV2_PROPERTY_GETSET_SCALAR(has_midi_output) LV2_PROPERTY_GETSET_SCALAR(description) @@ -729,6 +748,7 @@ namespace pipedal LV2_PROPERTY_GETSET(uiPortNotifications) LV2_PROPERTY_GETSET(modGui) LV2_PROPERTY_GETSET(patchProperties) + LV2_PROPERTY_GETSET_SCALAR(audio_side_chain_inputs) static json_map::storage_type jmap; }; @@ -772,6 +792,7 @@ namespace pipedal AutoLilvNode enumeration_property_uri; AutoLilvNode core__toggled; AutoLilvNode core__connectionOptional; + AutoLilvNode core__isSideChain; AutoLilvNode portprops__not_on_gui_property_uri; AutoLilvNode portprops__trigger; AutoLilvNode midi__event; @@ -782,9 +803,9 @@ namespace pipedal AutoLilvNode invada_units__unit; // typo in invada plugins. AutoLilvNode invada_portprops__logarithmic; // typo in invada plugins. - AutoLilvNode atom__bufferType; AutoLilvNode atom__Path; + AutoLilvNode atom__String; AutoLilvNode presets__preset; AutoLilvNode state__state; AutoLilvNode rdfs__label; @@ -816,7 +837,6 @@ namespace pipedal AutoLilvNode pipedalUI__width; AutoLilvNode pipedalUI__graphicEq; - AutoLilvNode pipedalUI__outputPorts; AutoLilvNode pipedalUI__text; @@ -846,7 +866,6 @@ namespace pipedal AutoLilvNode patch__readable; AutoLilvNode pipedal_patch__readable; - AutoLilvNode mod__brand; AutoLilvNode mod__label; AutoLilvNode mod__preferMomentaryOffByDefault; @@ -861,11 +880,9 @@ namespace pipedal AutoLilvNode buf_size__maxBlockLength; AutoLilvNode buf_size__fixedBlockLength; AutoLilvNode buf_size__coarseBlockLength; - - - + AutoLilvNode port_groups__sideChainOf; }; - LilvUris* lilvUris = nullptr; + LilvUris *lilvUris = nullptr; private: bool vst3Enabled = true; @@ -896,7 +913,7 @@ namespace pipedal void free_world(); std::vector> plugins_; - std::map> pluginsByUri; + std::map> pluginsByUri; std::vector ui_plugins_; std::map> classesMap; @@ -947,22 +964,19 @@ namespace pipedal public: virtual MapFeature &GetMapFeature() override { return this->mapFeature; } - void CheckForResourceInitialization(const std::string& pluginUri,const std::filesystem::path& pluginUploadDirectory); + void CheckForResourceInitialization(const std::string &pluginUri, const std::filesystem::path &pluginUploadDirectory); - - - std::string MapResourcePath(const std::string&uri, const std::string&relativePath); + std::string MapResourcePath(const std::string &uri, const std::string &relativePath); void ReloadPlugins(); // equivalent to LV2 MapPath AbstractPath features. std::string MapPath(const std::string &abstractPath); - std::string AbstractPath(const std::string&path); - json_variant MapPath(const json_variant&json); - json_variant AbstractPath(const json_variant&json); + std::string AbstractPath(const std::string &path); + json_variant MapPath(const json_variant &json); + json_variant AbstractPath(const json_variant &json); private: - std::set pluginsThatHaveBeenCheckedForResources; void CheckForResourceInitization(const std::string pluginUri); @@ -973,12 +987,11 @@ namespace pipedal } static void PortValueCallback(const char *symbol, void *user_data, const void *value, uint32_t size, uint32_t type); static void StateRestoreCallback( - const char* port_symbol, - void* user_data, - const void* value, - uint32_t size, - uint32_t type); - + const char *port_symbol, + void *user_data, + const void *value, + uint32_t size, + uint32_t type); virtual IEffect *CreateEffect(PedalboardItem &pedalboardItem); void LoadPluginClassesFromLilv(); @@ -995,9 +1008,9 @@ namespace pipedal IHost *asIHost() { return this; } - virtual Lv2Pedalboard *CreateLv2Pedalboard(Pedalboard &pedalboard,Lv2PedalboardErrorList &errorList); + virtual Lv2Pedalboard *CreateLv2Pedalboard(Pedalboard &pedalboard, Lv2PedalboardErrorList &errorList); - virtual Lv2Pedalboard *UpdateLv2PedalboardStructure(Pedalboard &pedalboard,Lv2Pedalboard *existingPedalboard,Lv2PedalboardErrorList &errorList); + virtual Lv2Pedalboard *UpdateLv2PedalboardStructure(Pedalboard &pedalboard, Lv2Pedalboard *existingPedalboard, Lv2PedalboardErrorList &errorList); void setSampleRate(double sampleRate) { @@ -1010,9 +1023,8 @@ namespace pipedal class Urids; - Urids *urids = nullptr; - ModGuiUris* mod_gui_uris = nullptr; - + Urids *urids = nullptr; + ModGuiUris *mod_gui_uris = nullptr; void OnConfigurationChanged(const JackConfiguration &configuration, const JackChannelSelection &settings); @@ -1021,7 +1033,7 @@ namespace pipedal std::shared_ptr GetLv2PluginClass() const; - const std::vector>& GetPlugins() const { return plugins_; } + const std::vector> &GetPlugins() const { return plugins_; } const std::vector &GetUiPlugins() const { return ui_plugins_; } virtual std::shared_ptr GetPluginInfo(const std::string &uri) const; diff --git a/src/Storage.cpp b/src/Storage.cpp index b6979ad..1a70acd 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -1717,7 +1717,7 @@ void Storage::ToAbstractPaths(PluginPreset &pluginPreset) std::map newPathProperties; for (const auto &pair : pluginPreset.pathProperties_) { - newPathProperties[pair.first] = this->ToAbstractPathJson(pair.second); + newPathProperties[pair.first] = this->ToAbstractPathFromJson(pair.second); } pluginPreset.pathProperties_ = newPathProperties; } @@ -2999,7 +2999,19 @@ std::string Storage::GetTone3000Auth() const return tone3000Auth; } -std::string Storage::ToAbstractPathJson(const std::string &pathJson) +std::filesystem::path Storage::FromAbstractPathString(const std::string &stringPath) +{ + if (stringPath.empty()) { + return ""; + } + fs::path path { stringPath}; + if (path.is_absolute()) { + return path; + } + return GetPluginUploadDirectory() / path; +} + +std::string Storage::ToAbstractPathFromJson(const std::string &pathJson) { json_variant v = json_variant::parse(pathJson); diff --git a/src/Storage.hpp b/src/Storage.hpp index 765bc6f..f6044fa 100644 --- a/src/Storage.hpp +++ b/src/Storage.hpp @@ -129,8 +129,9 @@ public: const std::filesystem::path &GetPluginUploadDirectory() const; - std::string ToAbstractPathJson(const std::string &jsonAtomPath); + std::string ToAbstractPathFromJson(const std::string &jsonAtomPath); std::string FromAbstractPathJson(const std::string &jsonAtomPath); + std::filesystem::path FromAbstractPathString(const std::string &stringPath); //std::vector GetPedalboards(); diff --git a/src/WebServerConfig.cpp b/src/WebServerConfig.cpp index af19e1c..8045bd6 100644 --- a/src/WebServerConfig.cpp +++ b/src/WebServerConfig.cpp @@ -520,7 +520,7 @@ public: res.set(HttpField::content_type, "application/json"); res.set(HttpField::cache_control, "no-cache"); - fs::path path = model->GetStorage().FromAbstractPathJson(request_uri.query("path")); + fs::path path = model->GetStorage().FromAbstractPathString(request_uri.query("path")); if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path)) { diff --git a/vite/src/pipedal/AutoZoom.tsx b/vite/src/pipedal/AutoZoom.tsx index 2914403..8e50e90 100644 --- a/vite/src/pipedal/AutoZoom.tsx +++ b/vite/src/pipedal/AutoZoom.tsx @@ -24,17 +24,40 @@ import { withStyles } from "tss-react/mui"; import { createStyles } from './WithStyles'; import FullscreenIcon from '@mui/icons-material/Fullscreen'; import IconButtonEx from './IconButtonEx'; -import { isDarkMode } from './DarkMode'; import Backdrop from '@mui/material/Backdrop'; -import CloseIcon from '@mui/icons-material/Close'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; + import Rect from './Rect'; +import { css } from '@emotion/react'; + import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; const styles = (theme: Theme) => createStyles({ - pgraph: { + pgraph: css({ paddingBottom: 16 - } + }), + backdrop: css({ + zIndex: 1101, + overflow: "clip", + background: theme.palette.background.default, + + }), + noToolbar: css({ + display: "none" + }), + topToolBar: css({ + position: "absolute", + zIndex: 1101, + top: 0, + display: "flex", flexFlow: "row nowrap", justifyContent: "start", columnGap: 8, + }), + rightToolbar: css({ + position: "absolute", + zIndex: 1101, + top: 24, + display: "flex", flexFlow: "column nowrap", rowGap: 8 + }) }); @@ -42,6 +65,7 @@ export interface AutoZoomProps extends WithStyles { leftPad: number, rightPad: number, children?: React.ReactNode, + toolbarChildren?: React.ReactNode[], contentReady: boolean, showZoomed: boolean, setShowZoomed: (value: boolean) => void; @@ -53,6 +77,15 @@ export interface AutoZoomState { } +// non-react zoom state. +class UnzoomedLayout { + horizontalToolbar = false; + effectiveClientRect: Rect = new Rect(); + showToolbar = false; + zoom: number = 1; + showZoomButton = false; + contentRect: Rect = new Rect(); +} const AutoZoom = withStyles( class extends ResizeResponsiveComponent { @@ -97,38 +130,43 @@ const AutoZoom = withStyles( if (this.normalRef) { this.updateZoom(this.normalRef); } - if (this.maximizedRef) { - this.updateMaximizedZoom(); - } } } resizeObserver: ResizeObserver | null = null; - calculateZoomedClientRect( + calculateUnzoomedLayout( clientRect: Rect, contentWidth: number, contentHeight: number, buttonPadding: number - ): Rect { + ): UnzoomedLayout { + + let result = new UnzoomedLayout(); - console.log("Zoom: " + clientRect.toString() + " content: " + contentWidth + "x" + contentHeight + " buttonPadding: " + buttonPadding); let rightButtonZoom = Math.min((clientRect.width - buttonPadding) / contentWidth, clientRect.height / contentHeight); let topButtonZoom = Math.min((clientRect.width - buttonPadding) / contentWidth, (clientRect.height - buttonPadding) / contentHeight); let effectiveClientRect = clientRect.copy(); if (rightButtonZoom > topButtonZoom) { + result.horizontalToolbar = false; effectiveClientRect.width -= buttonPadding; } else { + result.horizontalToolbar = true; effectiveClientRect.y += buttonPadding; effectiveClientRect.height -= buttonPadding; } + result.effectiveClientRect = effectiveClientRect; let zoom = Math.min(effectiveClientRect.width / contentWidth, effectiveClientRect.height / contentHeight); if (zoom > 1) { zoom = 1; } + result.zoom = zoom; + result.showZoomButton = zoom !== 1; + result.showToolbar = result.showZoomButton || !!this.props.toolbarChildren; + let leftPad = (effectiveClientRect.width - contentWidth * zoom) / 2; if (leftPad < 0) { leftPad = 0; @@ -137,7 +175,8 @@ const AutoZoom = withStyles( if (topPad < 0) { topPad = 0; } - return new Rect(effectiveClientRect.x + leftPad, effectiveClientRect.y + topPad, contentWidth * zoom, contentHeight * zoom); + result.contentRect = new Rect(effectiveClientRect.x + leftPad, effectiveClientRect.y + topPad, contentWidth * zoom, contentHeight * zoom); + return result; } updateZoom(ref: HTMLDivElement) { @@ -145,59 +184,58 @@ const AutoZoom = withStyles( return; } let content = ref.firstElementChild as HTMLElement | null; + const classes = withStyles.getClasses(this.props); if (content) { const { leftPad, rightPad } = this.props; let bounds = new Rect(leftPad, 0, ref.clientWidth - leftPad - rightPad, ref.clientHeight - 8); - let zoomedClientRect = this.calculateZoomedClientRect( + let layout = this.calculateUnzoomedLayout( bounds, content.clientWidth, content.clientHeight, 48); + + let contentRect = layout.contentRect; + content.style.left = "0px"; content.style.top = "0px"; content.style.transformOrigin = "0 0"; // top-left corner content.style.transform = - `translate(${zoomedClientRect.x}px, ${zoomedClientRect.y}px) scale(${zoomedClientRect.width / content.clientWidth}, ${zoomedClientRect.height / content.clientHeight})`; + `translate(${contentRect.x}px, ${contentRect.y}px) scale(${layout.zoom}, ${layout.zoom})`; - if (this.zoomButton) { - this.zoomButton.style.display = zoomedClientRect.width < content.clientWidth ? "block" : "none"; - let rightSizeSpace = ref.clientWidth - zoomedClientRect.right; - if (rightSizeSpace - rightPad >= 48) { - this.zoomButton.style.top = `${zoomedClientRect.y + 24}px`; - this.zoomButton.style.left = `${zoomedClientRect.right + 4}px`; + if (this.zoomToolbar) { + if (!layout.showToolbar) { + + this.zoomToolbar.className = classes.noToolbar; + } else if (layout.horizontalToolbar) { + this.zoomToolbar.className = classes.topToolBar; + this.zoomToolbar.style.left = `${contentRect.x +16}px` + this.zoomToolbar.style.top = `${contentRect.y - 48}px`; + this.zoomToolbar.style.width = `${contentRect.width}px`; } else { - this.zoomButton.style.top = `${zoomedClientRect.y - 48}px`; - this.zoomButton.style.left = `${zoomedClientRect.right - 64 - 12}px`; + this.zoomToolbar.className = classes.rightToolbar; + this.zoomToolbar.style.left = ""; + this.zoomToolbar.style.top = `${contentRect.y}px`; + this.zoomToolbar.style.left = `${contentRect.right + 4}px`; + this.zoomToolbar.style.width = ""; } } + if (this.zoomButton) { + this.zoomButton.style.display = layout.showZoomButton ? "block" : "none"; + } // save the position of the Mod UI in page coordinates for later zooming. - this.zoomStartRect = zoomedClientRect.copy(); + this.zoomStartRect = layout.contentRect.copy(); let refBounds = ref.getBoundingClientRect(); this.zoomStartRect.x += refBounds.left; this.zoomStartRect.y += refBounds.top; + } else { + if (this.zoomToolbar) { + this.zoomToolbar.className = classes.noToolbar; + } } } updateMaximizedZoom() { - let ref = this.maximizedRef; - if (!this.mounted || !ref) { - return; - } - let content = ref.firstElementChild as HTMLElement | null; - - if (content) { - let leftPad = 16; let rightPad = 16; - let topPad = 16, bottomPad = 16; - let bounds: Rect; - bounds = new Rect(leftPad, topPad, ref.clientWidth - leftPad - rightPad, ref.clientHeight - topPad - bottomPad); - - let zoomedClientRect = this.calculateZoomedClientRect( - bounds, content.clientWidth, content.clientHeight, 48); - content.style.left = "0px"; - content.style.top = "0px"; - content.style.transformOrigin = "0 0"; // top-left corner - content.style.transform = `translate(${zoomedClientRect.x}px, ${zoomedClientRect.y}px) scale(${zoomedClientRect.width / content.clientWidth}, ${zoomedClientRect.height / content.clientHeight})`; - //console.log("Zoomed to: ", zoomedClientRect.toString()); - + if (this.normalRef) { + this.updateZoom(this.normalRef); } } @@ -234,20 +272,20 @@ const AutoZoom = withStyles( } this.maximizedAnimateFrameHandle = window.requestAnimationFrame(() => { this.maximizedAnimateFrameHandle = null; - if (this.maximizedRef) { - this.updateMaximizedZoom(); - } + this.updateMaximizedZoom(); }); } + private zoomToolbar: HTMLElement | null = null; private zoomButton: HTMLElement | null = null; private normalRef: HTMLDivElement | null = null; - setNormalRef(ref: HTMLDivElement | null) { + setContentRef(ref: HTMLDivElement | null) { this.normalRef = ref; if (ref) { - this.zoomButton = ref.querySelector("#maximize-button") as HTMLDivElement | null; + this.zoomToolbar = ref.querySelector("#zoom-toolbar") as HTMLDivElement | null; + this.zoomButton = ref.querySelector("#zoom-button") as HTMLDivElement | null; this.resizeObserver = new ResizeObserver(() => { this.requestZoomUpdate(); }); @@ -261,36 +299,15 @@ const AutoZoom = withStyles( if (this.resizeObserver) { this.resizeObserver.disconnect(); this.resizeObserver = null; - this.zoomButton = null; + this.zoomToolbar = null; } this.zoomStartRect = null; + this.zoomToolbar = null; + this.zoomButton = null; } } - private maximizedRef: HTMLDivElement | null = null; - private maximedResizeObserver: ResizeObserver | null = null; - - setMaximizedRef(ref: HTMLDivElement | null) { - this.maximizedRef = ref; - if (ref) { - this.maximedResizeObserver = new ResizeObserver(() => { - this.requestMaximizedZoomUpdate(); - }); - this.maximedResizeObserver.observe(ref); - let child = ref.firstElementChild as HTMLElement | null; - if (child) { - this.maximedResizeObserver.observe(child); - } - this.requestMaximizedZoomUpdate(); - } else { - if (this.maximedResizeObserver) { - this.maximedResizeObserver.disconnect(); - this.maximedResizeObserver = null; - } - } - } - private zoomStartRect: Rect | null = null; startZoom() { @@ -300,70 +317,77 @@ const AutoZoom = withStyles( this.props.setShowZoomed(true); } - render() { + content() { const classes = withStyles.getClasses(this.props); void classes; // suppress unused variable warning - const landscape = this.state.screenWidth > this.state.screenHeight; - return (
{ this.setNormalRef(ref); }}> -
- {!this.props.showZoomed && this.props.children} -
-
{ this.setContentRef(ref); }} + style={{ width: "100%", height: "100%" }} > - { - this.startZoom(); +
+ {this.props.children} +
+
- - -
- {this.props.showZoomed && ( - ({ - zIndex: this.props.showZoomed ? 1101 : -1, - overflow: "clip", - background: theme.palette.mode === 'dark' ? 'rgba(0, 0, 0, 0.9)' : 'rgba(255, 255, 255, 0.9)' - })} - open={this.props.showZoomed} - > -
{ this.setMaximizedRef(ref); }} - style={{ - position: "absolute", left: 0, top: 0, right: 0, bottom: 0, overflow: "hidden", - visibility: this.props.contentReady ? "visible" : "hidden" - }}> - -
- { - this.props.showZoomed && this.props.children - } -
- - { + this.startZoom(); }} + > + + + + ) : ( + { e.stopPropagation(); this.props.setShowZoomed(false); }} > - + -
+ )} + + + {this.props.toolbarChildren && this.props.toolbarChildren.map((child, index) => ( +
+ {child} +
+ ))} +
+ +
+ ); + + } + + render() { + const classes = withStyles.getClasses(this.props); + void classes; // suppress unused variable warning + + return (
+ {!this.props.showZoomed && this.content()} + + {this.props.showZoomed && ( + + {this.content()} + )}
); diff --git a/vite/src/pipedal/Lv2Plugin.tsx b/vite/src/pipedal/Lv2Plugin.tsx index e7d4b09..cdaca30 100644 --- a/vite/src/pipedal/Lv2Plugin.tsx +++ b/vite/src/pipedal/Lv2Plugin.tsx @@ -47,6 +47,7 @@ export class Port implements Deserializable { this.scale_points = ScalePoint.deserialize_array(input.scale_points); this.is_input = input.is_input; this.is_output = input.is_output; + this.is_sidechain = input.is_sidechain; this.is_control_port = input.is_control_port; this.is_audio_port = input.is_audio_port; this.is_atom_port = input.is_atom_port; @@ -80,6 +81,7 @@ export class Port implements Deserializable { is_output: boolean = false is_control_port: boolean = false; is_audio_port: boolean = false; + is_sidechain: boolean = false; is_atom_port: boolean = false; is_valid: boolean = false; supports_midi: boolean = false; @@ -92,9 +94,11 @@ export class Port implements Deserializable { export class PortGroup { deserialize(input: any): PortGroup { + this.isA = input.isA; this.uri = input.uri; this.symbol = input.symbol; this.name = input.name; + this.sideChainof = input.sideChainOf; this.parent_group = input.parent_group; this.program_list_id = input.program_list_id ?? -1; @@ -108,9 +112,12 @@ export class PortGroup { return result; } + isA: string[] = []; uri: string = ""; symbol: string = ""; name: string = ""; + sideChainof: string = ""; + parent_group: string = ""; program_list_id: number = -1; }; @@ -967,6 +974,8 @@ export class UiPlugin implements Deserializable { this.author_name = input.author_name; this.author_homepage = input.author_homepage; this.audio_inputs = input.audio_inputs; + this.audio_side_chain_inputs = input.audio_side_chain_inputs ?? 0; + this.audio_side_chain_title = input.audio_side_chain_title ?? ""; this.audio_outputs = input.audio_outputs; this.has_midi_input = input.has_midi_input; this.has_midi_output = input.has_midi_output; @@ -1057,7 +1066,9 @@ export class UiPlugin implements Deserializable { author_name: string = ""; author_homepage: string = ""; audio_inputs: number = 0; + audio_side_chain_inputs: number = 0; audio_outputs: number = 0; + audio_side_chain_title: string = ""; has_midi_input: number = 0; has_midi_output: number = 0; description: string = ""; @@ -1084,6 +1095,7 @@ export function makeSplitUiPlugin(): UiPlugin { author_name: "", author_homepage: "", audio_inputs: 1, + audio_side_chain_inputs: 0, audio_outputs: 1, has_midi_input: 0, has_midi_output: 0, diff --git a/vite/src/pipedal/Pedalboard.tsx b/vite/src/pipedal/Pedalboard.tsx index 157a5e4..1012687 100644 --- a/vite/src/pipedal/Pedalboard.tsx +++ b/vite/src/pipedal/Pedalboard.tsx @@ -81,6 +81,7 @@ export class PedalboardItem implements Deserializable { this.pathProperties = input.pathProperties; this.useModUi = input.useModUi ?? false; this.iconColor = input.iconColor??""; + this.sideChainInputId = input.sideChainInputId ?? -1; return this; } @@ -217,6 +218,7 @@ export class PedalboardItem implements Deserializable { pathProperties: {[Name: string]: string} = {}; useModUi: boolean = false; // true if this item should use the mod-ui. iconColor: string = ""; + sideChainInputId: number = -1; // -1 means no sidechain input. }; export class SnapshotValue { @@ -392,6 +394,7 @@ export class Pedalboard implements Deserializable { pathProperties: {[Name: string]: string} = {}; selectedPlugin: number = -1; + // yields all items in the pedalboard, including split items. Splits are yielded before their children. *itemsGenerator(): Generator { let it = itemGenerator_(this.items); while (true) @@ -401,6 +404,16 @@ export class Pedalboard implements Deserializable { yield v.value; } } + // same as itemsGenerator, but yields split items after their chains. + *itemsGeneratorSplitAfter(): Generator { + let it = itemGeneratorSplitAfter_(this.items); + while (true) + { + let v = it.next(); + if (v.done) break; + yield v.value; + } + } makeSnapshot(): Snapshot { let result = new Snapshot(); @@ -791,4 +804,28 @@ function* itemGenerator_(items: PedalboardItem[]): Generator { + for (let i = 0; i < items.length; ++i) { + let item = items[i]; + if (item.uri === SPLIT_PEDALBOARD_ITEM_URI) { + + let splitItem = item as PedalboardSplitItem; + + let it = itemGenerator_(splitItem.topChain); + while (true) { + let v = it.next(); + if (v.done) break; + yield v.value; + } + it = itemGenerator_(splitItem.bottomChain); + while (true) { + let v = it.next(); + if (v.done) break; + yield v.value; + } + } + yield item; + } +} + diff --git a/vite/src/pipedal/PiPedalModel.tsx b/vite/src/pipedal/PiPedalModel.tsx index 88c2882..7f4fd54 100644 --- a/vite/src/pipedal/PiPedalModel.tsx +++ b/vite/src/pipedal/PiPedalModel.tsx @@ -550,19 +550,16 @@ export class PiPedalModel //implements PiPedalModel ); zoomedUiControl: ObservableProperty = new ObservableProperty(undefined); + uiPluginsByUri: Map = new Map(); + svgImgUrl(svgImage: string): string { - //return this.varServerUrl + "img/" + svgImage; + // return this.varServerUrl + "img/" + svgImage; return "img/" + svgImage; } getUiPlugin(uri: string): UiPlugin | null { - let plugins: UiPlugin[] = this.ui_plugins.get(); - - for (let plugin of plugins) { - if (plugin.uri === uri) return plugin; - } - return null; + return this.uiPluginsByUri.get(uri) ?? null; } androidHost?: AndroidHostInterface; @@ -662,6 +659,7 @@ export class PiPedalModel //implements PiPedalModel } private setModelPedalboard(pedalboard: Pedalboard) { + this.removeInvalidSidechains(pedalboard); this.pedalboard.set(pedalboard); this.selectedSnapshot.set(pedalboard.selectedSnapshot); this.updateEnabledItems(pedalboard); @@ -1210,6 +1208,11 @@ export class PiPedalModel //implements PiPedalModel this.ui_plugins.set( UiPlugin.deserialize_array(await this.getWebSocket().request("plugins")) ); + // index ui plugins. + this.uiPluginsByUri = new Map(); + for (let i of this.ui_plugins.get()) { + this.uiPluginsByUri.set(i.uri, i); + } this.setModelPedalboard( new Pedalboard().deserialize( await this.getWebSocket().request("currentPedalboard") @@ -1761,13 +1764,17 @@ export class PiPedalModel //implements PiPedalModel } let it = newPedalboard.itemsGenerator(); + let oldInstanceId = -1; + let newInstanceId = -1; while (true) { let v = it.next(); if (v.done) break; let item = v.value; if (item.instanceId === itemId) { + oldInstanceId = item.instanceId; item.deserialize(new PedalboardItem()); // skeezy way to re-initialize. item.instanceId = ++newPedalboard.nextInstanceId; + newInstanceId = item.instanceId; item.uri = selectedUri; item.pluginName = plugin.name; item.controlValues = this.getDefaultValues(item.uri); @@ -1782,6 +1789,9 @@ export class PiPedalModel //implements PiPedalModel item.pathProperties[fileProperty.patchProperty] = "null"; } newPedalboard.selectedPlugin = item.instanceId; + if (oldInstanceId !== -1) { + this.updateSidechainReferences(newPedalboard, oldInstanceId, newInstanceId); + } this.setModelPedalboard(newPedalboard); this.updateServerPedalboard() return item.instanceId; @@ -1936,6 +1946,9 @@ export class PiPedalModel //implements PiPedalModel newPedalboard.replaceItem(fromInstanceId, emptyItem); newPedalboard.replaceItem(toInstanceId, fromItem); newPedalboard.selectedPlugin = fromItem.instanceId; + if (!toItem.isEmpty()) { + this.updateSidechainReferences(newPedalboard, toItem.instanceId, fromItem.instanceId); + } this.setModelPedalboard(newPedalboard); this.updateServerPedalboard(); @@ -3543,14 +3556,66 @@ export class PiPedalModel //implements PiPedalModel } requestBankPresets(bankInstanceId: number): Promise { - return nullCast(this.webSocket).request("requestBankPresets", {bankInstanceId: bankInstanceId}); - } + return nullCast(this.webSocket).request("requestBankPresets", { bankInstanceId: bankInstanceId }); + } importPresetsFromBank(bankInstanceId: number, presets: number[]): Promise { - return nullCast(this.webSocket).request("importPresetsFromBank", {bankInstanceId: bankInstanceId, presets: presets}); + return nullCast(this.webSocket).request("importPresetsFromBank", { bankInstanceId: bankInstanceId, presets: presets }); } copyPresetsToBank(bankInstanceId: number, presets: number[]): Promise { - return nullCast(this.webSocket).request("copyPresetsToBank", {bankInstanceId: bankInstanceId, presets: presets}); + return nullCast(this.webSocket).request("copyPresetsToBank", { bankInstanceId: bankInstanceId, presets: presets }); + } + setPedalboardSideChainInput(instanceId: number, sideChainInputId: number): void { + let pedalboard = this.pedalboard.get().clone(); + let pedalboardItem = pedalboard.getItem(instanceId); + if (!pedalboardItem) { + return; + } + pedalboardItem.sideChainInputId = sideChainInputId; + let oldId = pedalboardItem.instanceId; + let newId = ++pedalboard.nextInstanceId; + + pedalboardItem.instanceId = newId; // force reload of pedalboard. + pedalboard.selectedPlugin = pedalboardItem.instanceId; + + this.updateSidechainReferences(pedalboard, oldId, newId); + this.setModelPedalboard(pedalboard); + this.updateServerPedalboard(); + + } + + removeInvalidSidechains(pedalboard: Pedalboard) { + let it = pedalboard.itemsGeneratorSplitAfter(); + let previousItemIds: Set = new Set(); + previousItemIds.add(-2); // start is visible. + while (true) { + let r = it.next(); + if (r.done) { + break; + } + let item = r.value; + if (item.sideChainInputId !== -1) { + if (!previousItemIds.has(item.sideChainInputId)) { + item.sideChainInputId = -1; + } + } + if (!item.isEmpty()) { // empty items are not valid sidechain targets. + previousItemIds.add(item.instanceId); + } + } + } + updateSidechainReferences(pedalboard: Pedalboard, oldItemId: number, newItemId: number) { + let it = pedalboard.itemsGenerator(); + + while (true) { + let r = it.next(); + if (r.done) break; + let item = r.value; + if (item.sideChainInputId === oldItemId) { + item.sideChainInputId = newItemId; + } + } + this.removeInvalidSidechains(pedalboard); } }; diff --git a/vite/src/pipedal/PluginControlView.tsx b/vite/src/pipedal/PluginControlView.tsx index 66ef9fb..6c64e8a 100644 --- a/vite/src/pipedal/PluginControlView.tsx +++ b/vite/src/pipedal/PluginControlView.tsx @@ -22,6 +22,9 @@ import { Theme } from '@mui/material/styles'; import WithStyles, { withTheme } from './WithStyles'; import { createStyles } from './WithStyles'; import { css } from '@emotion/react'; +import IconButtonEx from './IconButtonEx'; + +import SidechainConnectIcon from './svg/ic_sidechain_open_48.svg?react'; import AutoZoom from './AutoZoom'; @@ -37,6 +40,7 @@ import { } from './Pedalboard'; import PluginControl from './PluginControl'; import ResizeResponsiveComponent from './ResizeResponsiveComponent'; +import SideChainSelectControl, {SideChainSelectDialog} from './SideChainSelectControl'; import VuMeter from './VuMeter'; import { nullCast } from './Utility' import { PiPedalStateError } from './PiPedalError'; @@ -66,6 +70,11 @@ export interface ICustomizationHost { isLandscapeGrid(): boolean; } +interface SideChainSelectItem { + instanceId: number; + title: string; +} + function makeIoPluginInfo(name: string, uri: string): UiPlugin { let result = new UiPlugin(); result.name = name; @@ -367,11 +376,12 @@ type PluginControlViewState = { dialogFileValue: string, modGuiContentReady: boolean, showModGuiZoomed: boolean, + sidechainDialogOpen: boolean }; const PluginControlView = withTheme(withStyles( - class extends ResizeResponsiveComponent implements ICustomizationHost { + class extends ResizeResponsiveComponent { model: PiPedalModel; constructor(props: PluginControlViewProps) { @@ -387,7 +397,8 @@ const PluginControlView = dialogFileProperty: new UiFileProperty(), dialogFileValue: "", modGuiContentReady: false, - showModGuiZoomed: false + showModGuiZoomed: false, + sidechainDialogOpen: false } this.onPedalboardChanged = this.onPedalboardChanged.bind(this); @@ -553,6 +564,68 @@ const PluginControlView = } } + private getSidechainSelectItems(): SideChainSelectItem[] { + let myInstanceId = this.props.item.instanceId; + + let items: SideChainSelectItem[] = []; + items.push({ instanceId: -1, title: "None" }); + items.push({ instanceId: -2, title: "Input" }); + + let pedalboard = this.model.pedalboard.get(); + if (!pedalboard) return items; + + let it = pedalboard.itemsGenerator(); + + let found = false; + while (true) { + let v = it.next(); + if (v.done) { + break; + } + let pedalboardItem = v.value; + if (pedalboardItem.isSplit()) { + continue; + } + if (pedalboardItem.instanceId === myInstanceId) { + found = true; + break; + } + if (pedalboardItem.uri.length === 0) { + continue; + } + let pluginInfo = this.model.getUiPlugin(pedalboardItem.uri); + if (!pluginInfo) continue; + let name = pluginInfo.name; + if (pedalboardItem.title.length !== 0) { + name = pedalboardItem.title; + } + items.push({ instanceId: pedalboardItem.instanceId, title: name }); + } + if (!found) { + return [{ instanceId: -1, title: "None" }]; + } + return items; + } + + private makeSideChainSelect(uiPlugin: UiPlugin) { + let items = this.getSidechainSelectItems(); + let selectedInstanceId = this.props.item.sideChainInputId; + let title = uiPlugin.audio_side_chain_title ? uiPlugin.audio_side_chain_title : "Side chain"; + + return ( + { + this.model.setPedalboardSideChainInput(this.props.item.instanceId, instanceId); + + }} + /> + ); + + } + isLandscapeGrid(): boolean { return this.state.landscapeGrid; } @@ -575,7 +648,7 @@ const PluginControlView = } return (
{controlGroup.name !== "" && (
@@ -711,6 +784,11 @@ const PluginControlView = ); } } + if (plugin.audio_side_chain_inputs !== 0) { + + result.push(this.makeSideChainSelect(plugin)); + + } return result; } @@ -803,6 +881,9 @@ const PluginControlView = return false; } + handleSelectSidechainInput() { + this.setState({sidechainDialogOpen: true}); + } handleModGuiFileProperty(instanceId: number, filePropertyUri: string, selectedFile: string) { let plugin = this.model.getUiPlugin(this.props.item.uri); @@ -827,10 +908,30 @@ const PluginControlView = if (!uiPlugin) { return (
); } + let toolBarChildren: ReactNode[] = []; + if (uiPlugin.audio_side_chain_inputs > 0) { + toolBarChildren.push( + { + this.handleSelectSidechainInput(); + }} + > + + + ); + } return (
{ this.setState({ showModGuiZoomed: value }); } } @@ -1012,6 +1113,16 @@ const PluginControlView = } /> )} + {this.state.sidechainDialogOpen && ( + { this.setState({sidechainDialogOpen: false});}} + selectItems={this.getSidechainSelectItems()} + selectedInstanceId={this.props.item.sideChainInputId} + onChanged={(instanceId: number) => { + this.model.setPedalboardSideChainInput(this.props.item.instanceId, instanceId); + }} + /> + )} {/* xxx: I don't think we need this anyore. */} c.minDb && levelDb <= c.maxDb) { ix = i; @@ -122,18 +121,18 @@ const styles = (theme: Theme) => createStyles({ }), titleSection: css({ - flex: "0 0 auto", alignSelf:"stretch", marginBottom: 8, marginLeft: 0, marginRight: 0 + flex: "0 0 auto", alignSelf: "stretch", marginBottom: 8, marginLeft: 0, marginRight: 0 }), midSection: css({ - flex: "1 1 1", display: "flex",flexFlow: "column nowrap",alignContent: "center",justifyContent: "center" + flex: "1 1 1", display: "flex", flexFlow: "column nowrap", alignContent: "center", justifyContent: "center" }), editSection: css({ flex: "0 0 28", position: "relative", width: 60, height: 28, minHeight: 28 }), editSectionNoContent: css({ - flex: "0 0 28", position: "relative", width: 1, height: 28, minHeight: 28 + flex: "0 0 28", position: "relative", width: 1, height: 28, minHeight: 28 }) }); @@ -153,12 +152,12 @@ const PluginOutputControl = class extends Component { private model: PiPedalModel; - private vuRef: React.RefObject; - private progressRef: React.RefObject; - private dbVuRef: React.RefObject; - private dbVuTelltaleRef: React.RefObject; - private dbVuTextRef: React.RefObject; - private lampRef: React.RefObject; + private vuRef: React.RefObject; + private progressRef: React.RefObject; + private dbVuRef: React.RefObject; + private dbVuTelltaleRef: React.RefObject; + private dbVuTextRef: React.RefObject; + private lampRef: React.RefObject; constructor(props: PluginOutputControlProps) { @@ -210,7 +209,7 @@ const PluginOutputControl = } } - private PROGRESS_WIDTH = 40-4; + private PROGRESS_WIDTH = 40 - 4; private VU_HEIGHT = 60 - 4; private DB_VU_HEIGHT = 60 - 4; private animationHandle: number | undefined = undefined; @@ -240,26 +239,23 @@ const PluginOutputControl = } if (this.dbVuTextRef.current) { let text: string; - if (this.dbVuTelltale <= this.props.uiControl.min_value) - { + if (this.dbVuTelltale <= this.props.uiControl.min_value) { text = "-"; } else { - if (Math.abs(this.dbVuTelltale) >= 99.5) - { + if (Math.abs(this.dbVuTelltale) >= 99.5) { text = Math.round(this.dbVuTelltale).toString(); } else { text = this.dbVuTelltale.toFixed(1); } if (!text.startsWith("-")) { - text = '+'+text; - } + text = '+' + text; + } } - if (this.lastDbText !== text) - { + if (this.lastDbText !== text) { this.lastDbText = text; this.dbVuTextRef.current.innerText = text; } - } + } this.animationHandle = undefined; this.updateDbVuTelltale(); } @@ -322,7 +318,7 @@ const PluginOutputControl = ) } - } + } else if (this.vuRef.current) { let control = this.props.uiControl; let range = (value - control.min_value) / (control.max_value - control.min_value); @@ -389,8 +385,7 @@ const PluginOutputControl = if (value > control.max_value) { value = control.max_value; } - if (value < control.min_value) - { + if (value < control.min_value) { value = control.min_value; } let y = (control.max_value - value) * this.DB_VU_HEIGHT / (control.max_value - control.min_value); @@ -439,13 +434,13 @@ const PluginOutputControl = } else if (control.isProgress()) { item_width = undefined; return ( -
{/* TITLE SECTION */} -
+
- {control.name === "" ? "\u00A0" : control.name} @@ -453,15 +448,18 @@ const PluginOutputControl =
{/* CONTROL SECTION */} -
-
-
-
-
+
+
+
+ }} />
@@ -478,45 +476,48 @@ const PluginOutputControl = let vuColors = defaultVuColors; return ( -
{/* TITLE SECTION */} -
- - {control.name === "" ? "\u00A0" : control.name} - +
+ {control.name === "" ? "\u00A0" : control.name}
{/* CONTROL SECTION */} -
-
-
-
- { - vuColors.map((vuColor,ix)=>{ - let top = Math.floor(this.dbVuMap(vuColor.maxDb)); - let bottom = Math.ceil(this.dbVuMap(vuColor.minDb))+1; + - return ( -
- ); - }) - } +
+
+
+ { + vuColors.map((vuColor, ix) => { + let top = Math.floor(this.dbVuMap(vuColor.maxDb)); + let bottom = Math.ceil(this.dbVuMap(vuColor.minDb)) + 1; + + return ( +
+ ); + }) + } +
+ +
+
- -
-
-
+
@@ -533,26 +534,28 @@ const PluginOutputControl = else if (control.isVu()) { item_width = undefined; return ( -
+
{/* TITLE SECTION */} -
- - {control.name === "" ? "\u00A0" : control.name} - + + {control.name === "" ? "\u00A0" : control.name} +
{/* CONTROL SECTION */}
-
-
-
+ style={{ display: "flex", justifyContent: "center", alignItems: "start", flexFlow: "row nowrap" }}> + +
+
+
+
-
+
@@ -575,11 +578,11 @@ const PluginOutputControl = } return (
+ style={{ width: attachedLamp ? 15 : item_width }} + > {/* TITLE SECTION */} -
+
{/* CONTROL SECTION */} -
@@ -613,23 +616,23 @@ const PluginOutputControl = ); } if (control.isOutputText()) { return ( -
+
{/* TITLE SECTION */} -
{control.name === "" ? "\u00A0": control.name} + }}> {control.name === "" ? "\u00A0" : control.name}
{/* CONTROL SECTION */}
@@ -646,10 +649,10 @@ const PluginOutputControl = } else { return ( -
+
{/* TITLE SECTION */} -
{/* CONTROL SECTION */} -
{text} diff --git a/vite/src/pipedal/SideChainSelectControl.tsx b/vite/src/pipedal/SideChainSelectControl.tsx new file mode 100644 index 0000000..f4f3177 --- /dev/null +++ b/vite/src/pipedal/SideChainSelectControl.tsx @@ -0,0 +1,223 @@ +/* + * MIT License + * + * Copyright (c) Robin E. R. Davies + * + * 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. + */ + +import { Component } from 'react'; +import { Theme } from '@mui/material/styles'; +import { css } from '@emotion/react'; + +import DialogEx from './DialogEx'; +import DialogTitle from '@mui/material/DialogTitle'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import Toolbar from '@mui/material/Toolbar'; +import DialogContent from '@mui/material/DialogContent'; +import IconButtonEx from './IconButtonEx'; +import Typography from '@mui/material/Typography'; +import { PiPedalModel, PiPedalModelFactory, State } from './PiPedalModel'; +import Select from '@mui/material/Select'; +import MenuItem from '@mui/material/MenuItem'; + + +export const StandardItemSize = { width: 80, height: 140 } + + +import { withStyles } from "tss-react/mui"; +import WithStyles from './WithStyles'; +import { withTheme } from './WithStyles'; + +const styles = (theme: Theme) => { + return { + frame: css({ + position: "relative", + margin: "12px" + }), + controlFrame: css({ + display: "flex", flexDirection: "column", alignItems: "start", justifyContent: "space-between", height: 116 + }), + displayValue: css({ + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 4, + textAlign: "center", + background: theme.mainBackground, + color: theme.palette.text.secondary, + // zIndex: -1, + }), + titleSection: css({ + flex: "0 0 auto", alignSelf: "stretch", marginBottom: 8, marginLeft: 8, marginRight: 0 + }), + midSection: css({ + flex: "1 1 1", display: "flex", flexFlow: "column nowrap", alignContent: "center", justifyContent: "center" + }), + editSection: css({ + flex: "0 0 0", position: "relative", width: 60, height: 28, minHeight: 28 + }) + } +} + ; + + +export interface SideChainSelectControlProps extends WithStyles { + title: string; + selectItems: { instanceId: number, title: string }[]; + selectedInstanceId: number; + onChanged(instanceId: number): void; + theme: Theme; +} +type SideChainSelectControlState = { +}; + +const SideChainSelectControl = + withTheme(withStyles( + class extends Component { + + model: PiPedalModel; + + + constructor(props: SideChainSelectControlProps) { + super(props); + + this.state = { + }; + this.model = PiPedalModelFactory.getInstance(); + this.onStateChanged = this.onStateChanged.bind(this); + } + + + onStateChanged(state: State) { + } + mounted: boolean = false; + componentDidMount() { + this.model.state.addOnChangedHandler(this.onStateChanged); + this.mounted = true; + } + componentWillUnmount() { + this.mounted = false; + this.model.state.removeOnChangedHandler(this.onStateChanged); + } + componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any): void { + } + + render() { + //const classes = withStyles.getClasses(this.props); + + const classes = withStyles.getClasses(this.props); + + return ( +
+ {/* TITLE SECTION */} +
+ {this.props.title} +
+ {/* CONTROL SECTION */} + +
+ + +
+ + {/* LABEL/EDIT SECTION*/} +
+
+
+ ); + } + } + , styles) + ) + ; +export default SideChainSelectControl; + +export interface SideChainSelectDialogProps { + open: boolean, + onClose: () => void, + + selectItems: { instanceId: number, title: string }[]; + selectedInstanceId: number; + onChanged(instanceId: number): void; + +} +export function SideChainSelectDialog(props: SideChainSelectDialogProps) { + let { open, onClose, selectItems, selectedInstanceId, onChanged } = props; + return ( + { }} + > + + + { onClose(); }} aria-label="back" + > + + + + Sidechain Input + + + + + + + + ); +} \ No newline at end of file diff --git a/vite/src/pipedal/SnapshotEditor.tsx b/vite/src/pipedal/SnapshotEditor.tsx index 5708b44..5bb0110 100644 --- a/vite/src/pipedal/SnapshotEditor.tsx +++ b/vite/src/pipedal/SnapshotEditor.tsx @@ -24,7 +24,7 @@ import CloseIcon from '@mui/icons-material/Close'; import AppBar from '@mui/material/AppBar'; import Toolbar from '@mui/material/Toolbar'; import CssBaseline from '@mui/material/CssBaseline'; -import {createStyles} from './WithStyles'; +import { createStyles } from './WithStyles'; import { withStyles } from "tss-react/mui"; import IconButtonEx from './IconButtonEx'; @@ -148,10 +148,12 @@ const SnapshotEditor = withStyles( this.model_.zoomedUiControl.addOnChangedHandler( () => { - this.setState({ - zoomedControlOpen: this.model_.zoomedUiControl.get() !== undefined, - zoomedControlInfo: this.model_.zoomedUiControl.get() - }); + if (this.mounted93) { + this.setState({ + zoomedControlOpen: this.model_.zoomedUiControl.get() !== undefined, + zoomedControlInfo: this.model_.zoomedUiControl.get() + }); + } } ); @@ -182,9 +184,11 @@ const SnapshotEditor = withStyles( this.setState({ isFullScreen: !this.state.isFullScreen }); } + private mounted93: boolean = false; componentDidMount() { super.componentDidMount(); + this.mounted93 = true; this.model_.showStatusMonitor.addOnChangedHandler(this.showStatusMonitorHandler); } @@ -206,6 +210,7 @@ const SnapshotEditor = withStyles( } componentWillUnmount() { + this.mounted93 = false; super.componentWillUnmount(); this.model_.banks.removeOnChangedHandler(this.showStatusMonitorHandler); } diff --git a/vite/src/pipedal/svg/ic_sidechain_open_48.svg b/vite/src/pipedal/svg/ic_sidechain_open_48.svg new file mode 100644 index 0000000..3a68ca5 --- /dev/null +++ b/vite/src/pipedal/svg/ic_sidechain_open_48.svg @@ -0,0 +1,17 @@ + + + + + + +