From 59263ee715f7d4f838051f79faa98918c9bc26c4 Mon Sep 17 00:00:00 2001 From: "Robin E. R. Davies" Date: Tue, 27 Jan 2026 10:10:36 -0500 Subject: [PATCH] Sync --- PiPedalCommon/src/include/json.hpp | 11 + PiPedalCommon/src/json.cpp | 6 +- src/AlsaDriver.cpp | 337 +++++-- src/AlsaDriverTest.cpp | 8 +- src/AudioDriver.hpp | 24 +- src/AudioHost.cpp | 116 ++- src/AudioHost.hpp | 3 +- src/Banks.hpp | 2 +- src/CMakeLists.txt | 1 + src/ChannelRouterSettings.cpp | 151 ++++ src/ChannelRouterSettings.hpp | 107 +++ src/DummyAudioDriver.cpp | 202 ++--- src/IHost.hpp | 4 +- src/JackConfiguration.hpp | 1 - src/Lv2Pedalboard.cpp | 4 +- src/Pedalboard.cpp | 18 +- src/Pedalboard.hpp | 47 +- src/PiLatencyMain.cpp | 8 +- src/PiPedalCommon.hpp | 32 + src/PiPedalModel.cpp | 52 +- src/PiPedalModel.hpp | 7 +- src/PiPedalSocket.cpp | 16 +- src/PluginHost.cpp | 7 +- src/PluginHost.hpp | 22 +- src/RingBufferReader.hpp | 20 +- src/Storage.cpp | 229 ++++- src/Storage.hpp | 20 +- todo.txt | 1 + vite/src/pipedal/ChannelMixerSettings.tsx | 55 -- .../pipedal/ChannelMixerSettingsDialog.tsx | 524 ----------- vite/src/pipedal/ChannelRouterSettings.tsx | 64 ++ .../pipedal/ChannelRouterSettingsDialog.tsx | 853 ++++++++++++++++++ ...sx => ChannelRouterSettingsHelpDialog.tsx} | 21 +- ...Control.tsx => ChannelRouterUiControl.tsx} | 5 +- vite/src/pipedal/Pedalboard.tsx | 45 +- vite/src/pipedal/PiPedalModel.tsx | 45 +- vite/src/pipedal/SettingsDialog.tsx | 36 +- vite/src/pipedal/ToobParametricEqView.tsx | 2 +- 38 files changed, 2127 insertions(+), 979 deletions(-) create mode 100644 src/ChannelRouterSettings.cpp create mode 100644 src/ChannelRouterSettings.hpp create mode 100644 src/PiPedalCommon.hpp delete mode 100644 vite/src/pipedal/ChannelMixerSettings.tsx delete mode 100644 vite/src/pipedal/ChannelMixerSettingsDialog.tsx create mode 100644 vite/src/pipedal/ChannelRouterSettings.tsx create mode 100644 vite/src/pipedal/ChannelRouterSettingsDialog.tsx rename vite/src/pipedal/{ChannelMixerSettingsHelpDialog.tsx => ChannelRouterSettingsHelpDialog.tsx} (80%) rename vite/src/pipedal/{ChannelMixerUiControl.tsx => ChannelRouterUiControl.tsx} (97%) diff --git a/PiPedalCommon/src/include/json.hpp b/PiPedalCommon/src/include/json.hpp index 7792535..48cb13c 100644 --- a/PiPedalCommon/src/include/json.hpp +++ b/PiPedalCommon/src/include/json.hpp @@ -57,6 +57,17 @@ } \ ; + +#define JSON_GETTER_SETTER_REF(name) \ + const decltype(name##_) &name() const { return name##_; } \ + decltype(name##_) &name() { return name##_; } \ + void name(const decltype(name##_) &value) { name##_ = value; } + +#define JSON_GETTER_SETTER(name) \ + decltype(name##_) name() const { return name##_; } \ + void name(decltype(name##_) value) { name##_ = value; } + + namespace pipedal { class JsonException: public std::runtime_error { diff --git a/PiPedalCommon/src/json.cpp b/PiPedalCommon/src/json.cpp index d486bf8..ca8e86e 100644 --- a/PiPedalCommon/src/json.cpp +++ b/PiPedalCommon/src/json.cpp @@ -562,8 +562,8 @@ void json_reader::skip_array() void json_reader::skip_object() { - int c; consume('{'); + int c; while (true) { c = peek(); @@ -571,11 +571,11 @@ void json_reader::skip_object() if (c == '}') { c = get(); - break; + return; } skip_string(); // name. consume(':'); - skip_object(); + skip_property(); if (peek() == ',') { consume(','); diff --git a/src/AlsaDriver.cpp b/src/AlsaDriver.cpp index c86c554..92472d6 100644 --- a/src/AlsaDriver.cpp +++ b/src/AlsaDriver.cpp @@ -23,6 +23,7 @@ */ #include "pch.h" +#include "PiPedalCommon.hpp" #include "util.hpp" #include #include "Finally.hpp" @@ -39,6 +40,7 @@ #include "CrashGuard.hpp" #include #include +#include "ChannelRouterSettings.hpp" #include "CpuUse.hpp" @@ -328,11 +330,34 @@ namespace pipedal bool inputSwapped = false; bool outputSwapped = false; - std::vector activeCaptureBuffers; - std::vector activePlaybackBuffers; + std::vector> allocatedBuffers; - std::vector captureBuffers; - std::vector playbackBuffers; + std::set usedOutputChannels; + std::vector deviceCaptureBuffers; + std::vector devicePlaybackBuffers; + float *zeroInputBuffer = nullptr; + float *discardOutputBuffer = nullptr; + std::vector mainCaptureBuffers; + std::vector mainPlaybackBuffers; + + std::vector auxCaptureBuffers; + std::vector auxPlaybackBuffers; + + std::vector sendCaptureBuffers; + std::vector sendPlaybackBuffers; + + struct OutputBufferMix { + size_t outputChannel = 0; + std::vector inputBuffers; + float *outputBuffer = nullptr; + }; + + std::vector outputBufferMixes; + + + + std::vector deviceCaptureBuffers; + std::vector devicePlaybackBuffers; std::vector rawCaptureBuffer; std::vector rawPlaybackBuffer; @@ -742,7 +767,7 @@ namespace pipedal { int32_t *p = getCaptureBuffer(rawCaptureBuffer); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; for (size_t frame = 0; frame < frames; ++frame) { @@ -760,7 +785,7 @@ namespace pipedal { float *p = getCaptureBuffer(rawCaptureBuffer); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; for (size_t frame = 0; frame < frames; ++frame) { @@ -776,7 +801,7 @@ namespace pipedal { int16_t *p = getCaptureBuffer(rawCaptureBuffer); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; constexpr double scale = 1.0f / (std::numeric_limits::max() + 1L); for (size_t frame = 0; frame < frames; ++frame) @@ -792,7 +817,7 @@ namespace pipedal { int16_t *p = getCaptureBuffer(rawCaptureBuffer); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; constexpr float scale = 1.0f / (std::numeric_limits::max() + 1L); for (size_t frame = 0; frame < frames; ++frame) @@ -809,7 +834,7 @@ namespace pipedal { int32_t *p = getCaptureBuffer(rawCaptureBuffer); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; constexpr float scale = 1.0f / (std::numeric_limits::max() + 1L); for (size_t frame = 0; frame < frames; ++frame) @@ -825,7 +850,7 @@ namespace pipedal { uint8_t *p = getCaptureBuffer(rawCaptureBuffer); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; constexpr float scale = 1.0f / (std::numeric_limits::max() + 1LL); for (size_t frame = 0; frame < frames; ++frame) @@ -842,7 +867,7 @@ namespace pipedal { uint8_t *p = (uint8_t *)rawCaptureBuffer.data(); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; constexpr float scale = 1.0f / (std::numeric_limits::max() + 1LL); for (size_t frame = 0; frame < frames; ++frame) @@ -859,7 +884,7 @@ namespace pipedal { int32_t *p = (int32_t *)rawCaptureBuffer.data(); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; constexpr float scale = 1.0f / (0x00FFFFFFL + 1L); for (size_t frame = 0; frame < frames; ++frame) @@ -875,7 +900,7 @@ namespace pipedal { int32_t *p = (int32_t *)rawCaptureBuffer.data(); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; constexpr float scale = 1.0f / (0x00FFFFFFL + 1L); for (size_t frame = 0; frame < frames; ++frame) @@ -891,7 +916,7 @@ namespace pipedal { int32_t *p = (int32_t *)rawCaptureBuffer.data(); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; constexpr float scale = 1.0f / (std::numeric_limits::max() + 1L); for (size_t frame = 0; frame < frames; ++frame) @@ -907,7 +932,7 @@ namespace pipedal { int16_t *p = (int16_t *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; constexpr float scale = std::numeric_limits::max(); for (size_t frame = 0; frame < frames; ++frame) @@ -927,7 +952,7 @@ namespace pipedal { int16_t *p = (int16_t *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; constexpr float scale = std::numeric_limits::max(); for (size_t frame = 0; frame < frames; ++frame) @@ -947,7 +972,7 @@ namespace pipedal { int32_t *p = (int32_t *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; constexpr double scale = std::numeric_limits::max(); for (size_t frame = 0; frame < frames; ++frame) @@ -969,7 +994,7 @@ namespace pipedal int32_t *p = (int32_t *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; constexpr double scale = 0x00FFFFFF; for (size_t frame = 0; frame < frames; ++frame) @@ -991,7 +1016,7 @@ namespace pipedal int32_t *p = (int32_t *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; constexpr double scale = 0x00FFFFFF; for (size_t frame = 0; frame < frames; ++frame) @@ -1011,7 +1036,7 @@ namespace pipedal { int32_t *p = (int32_t *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; constexpr double scale = std::numeric_limits::max(); for (size_t frame = 0; frame < frames; ++frame) @@ -1031,7 +1056,7 @@ namespace pipedal { uint8_t *p = (uint8_t *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; constexpr double scale = std::numeric_limits::max(); for (size_t frame = 0; frame < frames; ++frame) @@ -1056,7 +1081,7 @@ namespace pipedal { uint8_t *p = (uint8_t *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; constexpr double scale = std::numeric_limits::max(); for (size_t frame = 0; frame < frames; ++frame) @@ -1082,7 +1107,7 @@ namespace pipedal { float *p = (float *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; for (size_t frame = 0; frame < frames; ++frame) { @@ -1097,7 +1122,7 @@ namespace pipedal { float *p = (float *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; for (size_t frame = 0; frame < frames; ++frame) { @@ -1127,9 +1152,9 @@ namespace pipedal } } - JackChannelSelection channelSelection; + ChannelSelection channelSelection; bool open = false; - virtual void Open(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) + virtual void Open(const JackServerSettings &jackServerSettings, const ChannelSelection &channelSelection) { terminateAudio_ = false; if (open) @@ -1249,7 +1274,7 @@ namespace pipedal rawCaptureBuffer.resize(captureFrameSize * bufferSize * 2); memset(rawCaptureBuffer.data(), 0, rawCaptureBuffer.size()); - AllocateBuffers(captureBuffers, captureChannels); + AllocateBuffers(deviceCaptureBuffers, captureChannels); } virtual std::string GetConfigurationDescription() @@ -1260,8 +1285,15 @@ namespace pipedal << ", " << GetAlsaFormatDescription(this->captureFormat) << ", " << this->sampleRate << ", " << this->bufferSize << "x" << this->numberOfBuffers - << ", in: " << this->InputBufferCount() << "/" << this->captureChannels - << ", out: " << this->OutputBufferCount() << "/" << this->playbackChannels); + << ", " << "device in: " << this->DeviceInputBufferCount() + << ", " << "device out: " << this->DeviceOutputBufferCount() + << ", main in: " << this->MainInputBufferCount() + << ", main out: " << this->MainOutputBufferCount() + << ", aux in: " << this->AuxInputBufferCount() + << ", aux out: " << this->AuxOutputBufferCount() + << ", send in: " << this->SendInputBufferCount() + << ", send out: " << this->SendOutputBufferCount() + ); return result; } void PreparePlaybackFunctions(snd_pcm_format_t playbackFormat) @@ -1321,10 +1353,10 @@ namespace pipedal rawPlaybackBuffer.resize(playbackFrameSize * bufferSize); memset(rawPlaybackBuffer.data(), 0, playbackFrameSize * bufferSize); - AllocateBuffers(playbackBuffers, playbackChannels); + AllocateBuffers(devicePlaybackBuffers, playbackChannels); } - void OpenAudio(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) + void OpenAudio(const JackServerSettings &jackServerSettings, const ChannelSelection &channelSelection) { std::lock_guard lock{restartMutex}; @@ -1717,7 +1749,7 @@ namespace pipedal } return 0; } - void AudioThread() + PIPEDAL_NON_INLINE void AudioThread() { SetThreadName("alsaDriver"); @@ -1797,6 +1829,19 @@ namespace pipedal cpuUse.AddSample(ProfileCategory::Execute); + // Perform any neccessary mixing of outputs. + for (auto &bufferMix : outputBufferMixes) + { + float *outputBuffer = bufferMix.outputBuffer; + for (auto &mixBuffer : bufferMix.inputBuffers) + { + for (size_t i = 0; i < framesRead; ++i) + { + outputBuffer[i] += mixBuffer[i]; + } + } + } + (this->*copyOutputFn)(framesRead); cpuUse.AddSample(ProfileCategory::Driver); // process. @@ -1824,9 +1869,9 @@ namespace pipedal { this->driverHost->OnAlsaDriverStopped(); // zero out input buffers. - for (size_t i = 0; i < this->captureBuffers.size(); ++i) + for (size_t i = 0; i < this->deviceCaptureBuffers.size(); ++i) { - float *pBuffer = captureBuffers[i]; + float *pBuffer = deviceCaptureBuffers[i]; for (size_t j = 0; j < this->bufferSize; ++j) { pBuffer[j] = 0; @@ -1836,7 +1881,7 @@ namespace pipedal { while (!terminateAudio()) { - std::this_thread::sleep_for(std::chrono::milliseconds(10)); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); this->driverHost->OnProcess(this->bufferSize); } } @@ -1849,63 +1894,129 @@ namespace pipedal bool alsaActive = false; - static int IndexFromPortName(const std::string &s) - { - auto pos = s.find_last_of('_'); - if (pos == std::string::npos) - { - throw std::invalid_argument("Bad port name."); - } - const char *p = s.c_str() + (pos + 1); - int v = atoi(p); - if (v < 0) + PIPEDAL_NON_INLINE void AllocateInputChannels( + const std::vector &channelSelection, + std::vector &channelBuffers + ) { + size_t nChannels = channelSelection.size(); + channelBuffers.resize(nChannels); + + for (size_t i = 0; i < nChannels; ++i) { - throw std::invalid_argument("Bad port name."); + int64_t deviceChannel = channelSelection[i]; + if (deviceChannel == -1 || deviceChannel >= captureChannels) + { + channelBuffers[i] = zeroInputBuffer; + } + else + { + channelBuffers[i] = deviceCaptureBuffers[deviceChannel]; + } + } + } + PIPEDAL_NON_INLINE void AddMixBuffer(int64_t outputChannel, float *mixBuffer) + { + for (auto&bufferMix: outputBufferMixes) + { + if (bufferMix.outputChannel == outputChannel) + { + bufferMix.inputBuffers.push_back(mixBuffer); + return; + } + } + OutputBufferMix newMix; + newMix.outputChannel = outputChannel; + newMix.outputBuffer = devicePlaybackBuffers[outputChannel]; + newMix.inputBuffers.push_back(mixBuffer); + outputBufferMixes.push_back(std::move(newMix)); + } + PIPEDAL_NON_INLINE void AllocateOutputChannels( + const std::vector &channelSelection, + std::vector &channelBuffers + ) { + size_t nChannels = channelSelection.size(); + + channelBuffers.resize(nChannels); + for (size_t i = 0; i < nChannels; ++i) + { + int64_t deviceChannel = channelSelection[i]; + if (deviceChannel == -1 || deviceChannel >= playbackChannels) + { + if (discardOutputBuffer == nullptr) + { + discardOutputBuffer = AllocateAudioBuffer(); + } + channelBuffers[i] = discardOutputBuffer; + } + else + { + if (!usedOutputChannels.contains(deviceChannel)) + { + usedOutputChannels.insert(deviceChannel); + channelBuffers[i] = deviceCaptureBuffers[deviceChannel]; + + } else { + // Used once already. We need to mix down. + float *mixBuffer = AllocateAudioBuffer(); + channelBuffers[i] = mixBuffer; + AddMixBuffer(deviceChannel, mixBuffer); + } + } } - return v; } + + bool activated = false; - virtual void Activate() + PIPEDAL_NON_INLINE virtual void Activate() { if (activated) { throw PiPedalStateException("Already activated."); } + activated = true; - this->activeCaptureBuffers.resize(channelSelection.GetInputAudioPorts().size()); + // Reset previously allocated buffers. + usedOutputChannels.clear(); + allocatedBuffers.resize(0); - int ix = 0; - for (auto &x : channelSelection.GetInputAudioPorts()) + + // Allocate device capture buffers. + zeroInputBuffer = AllocateAudioBuffer(); + deviceCaptureBuffers.resize(captureChannels); + for (size_t i = 0; i < captureChannels; ++i) { - int sourceIndex = IndexFromPortName(x); - if (sourceIndex >= captureBuffers.size()) - { - Lv2Log::error(SS("Invalid audio input port: " << x)); - } - else - { - this->activeCaptureBuffers[ix++] = this->captureBuffers[sourceIndex]; - } + deviceCaptureBuffers[i] = AllocateAudioBuffer(); + } + devicePlaybackBuffers.resize(playbackChannels); + for (size_t i = 0; i < playbackChannels; ++i) + { + devicePlaybackBuffers[i] = AllocateAudioBuffer(); } - this->activePlaybackBuffers.resize(channelSelection.GetOutputAudioPorts().size()); - - ix = 0; - for (auto &x : channelSelection.GetOutputAudioPorts()) - { - int sourceIndex = IndexFromPortName(x); - if (sourceIndex >= playbackBuffers.size()) - { - Lv2Log::error(SS("Invalid audio output port: " << x)); - } - else - { - this->activePlaybackBuffers[ix++] = this->playbackBuffers[sourceIndex]; - } - } + AllocateInputChannels( + channelSelection.mainInputChannels(), + this->mainCaptureBuffers); + AllocateOutputChannels( + channelSelection.mainOutputChannels(), + this->mainPlaybackBuffers + ); + AllocateInputChannels( + channelSelection.auxInputChannels(), + this->auxCaptureBuffers); + AllocateOutputChannels( + channelSelection.auxOutputChannels(), + this->auxPlaybackBuffers + ); + AllocateInputChannels( + channelSelection.sendInputChannels(), + this->sendCaptureBuffers); + AllocateOutputChannels( + channelSelection.sendOutputChannels(), + this->sendPlaybackBuffers + ); audioThread = std::make_unique([this]() { AudioThread(); }); @@ -1941,10 +2052,43 @@ namespace pipedal this->alsaSequencer = alsaSequencer; } - virtual size_t InputBufferCount() const { return activeCaptureBuffers.size(); } - virtual float *GetInputBuffer(size_t channel) override + virtual size_t DeviceInputBufferCount() const override { + return deviceCaptureBuffers.size(); + } + virtual size_t DeviceOutputBufferCount() const override { + return devicePlaybackBuffers.size(); + } + + + virtual size_t MainInputBufferCount() const { return mainCaptureBuffers.size(); } + virtual float *GetMainInputBuffer(size_t channel) override { - return activeCaptureBuffers[channel]; + if (channel >= (int64_t)mainCaptureBuffers.size()) + { + throw std::runtime_error("Argument out of range."); + } + return mainCaptureBuffers[channel]; + } + virtual size_t AuxInputBufferCount() const { return auxCaptureBuffers.size(); } + virtual float *GetAuxInputBuffer(size_t channel) override + { + return auxCaptureBuffers[channel]; + } + virtual size_t AuxOutputBufferCount() const { + return auxPlaybackBuffers.size(); } + virtual float *GetAuxOutputBuffer(size_t channel) override + { + return auxPlaybackBuffers[channel]; + } + virtual size_t SendInputBufferCount() const { return sendCaptureBuffers.size(); } + virtual float *GetSendInputBuffer(size_t channel) override + { + return sendCaptureBuffers[channel]; + } + virtual size_t SendOutputBufferCount() const { return sendPlaybackBuffers.size(); } + virtual float *GetSendOutputBuffer(size_t channel) override + { + return sendPlaybackBuffers[channel]; } virtual size_t GetMidiInputEventCount() override @@ -1956,27 +2100,30 @@ namespace pipedal return this->midiEvents.data(); } - virtual size_t OutputBufferCount() const { return activePlaybackBuffers.size(); } - virtual float *GetOutputBuffer(size_t channel) override + virtual size_t MainOutputBufferCount() const { return mainPlaybackBuffers.size(); } + virtual float *GetMainOutputBuffer(size_t channel) override { - return activePlaybackBuffers[channel]; + return mainPlaybackBuffers[channel]; } - void FreeBuffers(std::vector &buffer) - { - for (size_t i = 0; i < buffer.size(); ++i) - { - delete[] buffer[i]; - buffer[i] = 0; - } - buffer.clear(); + float* AllocateAudioBuffer() { + std::vector buffer; + buffer.resize(this->bufferSize); + float*pBuffer = buffer.data(); + allocatedBuffers.push_back(std::move(buffer)); + return pBuffer; } void DeleteBuffers() { - activeCaptureBuffers.clear(); - activePlaybackBuffers.clear(); - FreeBuffers(this->playbackBuffers); - FreeBuffers(this->captureBuffers); + mainCaptureBuffers.clear(); + mainPlaybackBuffers.clear(); + auxCaptureBuffers.clear(); + auxPlaybackBuffers.clear(); + sendCaptureBuffers.clear(); + sendPlaybackBuffers.clear(); + zeroInputBuffer = nullptr; + discardOutputBuffer = nullptr; + allocatedBuffers.clear(); } virtual void Close() { @@ -2241,7 +2388,7 @@ namespace pipedal { value += 1.0f * (c) / (128.0 * 256.0 * 256.0); } - this->playbackBuffers[c][i] = value; + this->devicePlaybackBuffers[c][i] = value; } } @@ -2257,7 +2404,7 @@ namespace pipedal for (size_t c = 0; c < captureChannels; ++c) { float error = - this->captureBuffers[c][i] - this->playbackBuffers[c][i]; + this->deviceCaptureBuffers[c][i] - this->devicePlaybackBuffers[c][i]; assert(std::abs(error) < 4e-5); } diff --git a/src/AlsaDriverTest.cpp b/src/AlsaDriverTest.cpp index 2508570..129fa37 100644 --- a/src/AlsaDriverTest.cpp +++ b/src/AlsaDriverTest.cpp @@ -218,16 +218,16 @@ public: if (testType == TestType::NullTest) return; - size_t inputs = audioDriver->InputBufferCount(); - size_t outputs = audioDriver->OutputBufferCount(); + size_t inputs = audioDriver->MainInputBufferCount(); + size_t outputs = audioDriver->MainOutputBufferCount(); for (size_t i = 0; i < inputs; ++i) { - inputBuffers[i] = audioDriver->GetInputBuffer(i); + inputBuffers[i] = audioDriver->GetMainInputBuffer(i); } for (size_t i = 0; i < outputs; ++i) { - outputBuffers[i] = audioDriver->GetOutputBuffer(i); + outputBuffers[i] = audioDriver->GetMainOutputBuffer(i); } if (this->testType == TestType::Oscillator) { diff --git a/src/AudioDriver.hpp b/src/AudioDriver.hpp index 3899b05..ea88c28 100644 --- a/src/AudioDriver.hpp +++ b/src/AudioDriver.hpp @@ -35,6 +35,8 @@ namespace pipedal { using ProcessCallback = std::function; + class ChannelSelection; + class AudioDriverHost { public: @@ -58,14 +60,26 @@ namespace pipedal { virtual size_t GetMidiInputEventCount() = 0; virtual MidiEvent*GetMidiEvents() = 0; - virtual size_t InputBufferCount() const = 0; - virtual float*GetInputBuffer(size_t channel) = 0; + virtual size_t DeviceInputBufferCount() const = 0; + virtual size_t DeviceOutputBufferCount() const = 0; + virtual size_t MainInputBufferCount() const = 0; + virtual float*GetMainInputBuffer(size_t channel) = 0; + virtual size_t MainOutputBufferCount() const = 0; + virtual float*GetMainOutputBuffer(size_t channe) = 0; - virtual size_t OutputBufferCount() const = 0; - virtual float*GetOutputBuffer(size_t channe) = 0; + virtual size_t AuxInputBufferCount() const = 0; + virtual float*GetAuxInputBuffer(size_t channel) = 0; + virtual size_t AuxOutputBufferCount() const = 0; + virtual float*GetAuxOutputBuffer(size_t channel) = 0; - virtual void Open(const JackServerSettings & jackServerSettings,const JackChannelSelection &channelSelection) = 0; + virtual size_t SendInputBufferCount() const = 0; + virtual float*GetSendInputBuffer(size_t channel) = 0; + virtual size_t SendOutputBufferCount() const = 0; + virtual float*GetSendOutputBuffer(size_t channel) = 0; + + + virtual void Open(const JackServerSettings & jackServerSettings,const ChannelSelection &channelSelection) = 0; virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) = 0; virtual void Activate() = 0; virtual void Deactivate() = 0; diff --git a/src/AudioHost.cpp b/src/AudioHost.cpp index a744f84..c1eb2ca 100644 --- a/src/AudioHost.cpp +++ b/src/AudioHost.cpp @@ -18,6 +18,7 @@ // 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 "PiPedalCommon.hpp" #include "AudioHost.hpp" #include "util.hpp" #include @@ -519,12 +520,17 @@ private: SystemMidiBinding rebootMidiBinding; SystemMidiBinding shutdownMidiBinding; - JackChannelSelection channelSelection; + ChannelSelection channelSelection; std::atomic active = false; std::atomic audioStopped = false; std::atomic isDummyAudioDriver = false; std::shared_ptr currentPedalboard; + std::shared_ptr currentMainInsertPedalboard; + std::shared_ptr currentAuxInsertPedalboard; + + + std::vector> activePedalboards; // pedalboards that have been sent to the audio queue. Lv2Pedalboard *realtimeActivePedalboard = nullptr; @@ -620,9 +626,9 @@ private: void ZeroOutputBuffers(size_t nframes) { - for (size_t i = 0; i < audioDriver->OutputBufferCount(); ++i) + for (size_t i = 0; i < audioDriver->MainOutputBufferCount(); ++i) { - float * out = (float *)audioDriver->GetOutputBuffer(i); + float * out = (float *)audioDriver->GetMainOutputBuffer(i); if (out) { ZeroBuffer(out, nframes); @@ -1173,9 +1179,9 @@ private: float *inputBuffers[4]; float *outputBuffers[4]; bool buffersValid = true; - for (int i = 0; i < audioDriver->InputBufferCount(); ++i) + for (int i = 0; i < audioDriver->MainInputBufferCount(); ++i) { - float *input = (float *)audioDriver->GetInputBuffer(i); + float *input = (float *)audioDriver->GetMainInputBuffer(i); if (input == nullptr) { buffersValid = false; @@ -1183,11 +1189,11 @@ private: } inputBuffers[i] = input; } - inputBuffers[audioDriver->InputBufferCount()] = nullptr; + inputBuffers[audioDriver->MainInputBufferCount()] = nullptr; - for (int i = 0; i < audioDriver->OutputBufferCount(); ++i) + for (int i = 0; i < audioDriver->MainOutputBufferCount(); ++i) { - float *output = audioDriver->GetOutputBuffer(i); + float *output = audioDriver->GetMainOutputBuffer(i); if (output == nullptr) { buffersValid = false; @@ -1195,7 +1201,7 @@ private: } outputBuffers[i] = output; } - outputBuffers[audioDriver->OutputBufferCount()] = nullptr; + outputBuffers[audioDriver->MainOutputBufferCount()] = nullptr; if (buffersValid) { @@ -1670,7 +1676,7 @@ public: return result; } - virtual void Open(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) + virtual void Open(const JackServerSettings &jackServerSettings, const ChannelSelection &channelSelection_) { std::lock_guard guard(mutex); @@ -1692,11 +1698,6 @@ public: this->audioDriver = std::unique_ptr(CreateAlsaDriver(this)); } - if (channelSelection.GetInputAudioPorts().size() == 0 || channelSelection.GetOutputAudioPorts().size() == 0) - { - return; - } - this->currentSample = 0; this->underruns = 0; @@ -1707,7 +1708,7 @@ public: this->realtimeReader.Reset(); this->realtimeWriter.Reset(); - this->channelSelection = channelSelection; + this->channelSelection = channelSelection_; StartReaderThread(); @@ -1795,6 +1796,7 @@ public: } } + virtual void SetPluginPreset(uint64_t instanceId, const std::vector &values) { std::lock_guard guard(mutex); @@ -1896,6 +1898,49 @@ public: pendingSnapshots.clear(); } + PIPEDAL_NON_INLINE RealtimePedalboardItemIndex GetRealtimeItemIndex(int64_t instanceId) + { + Pedalboard::InstanceType instanceType = Pedalboard::GetInstanceTypeFromInstanceId(instanceId); + int64_t index = -1; + switch (instanceType) + { + case Pedalboard::InstanceType::MainPedalboard: + if (this->currentPedalboard) { + if (instanceId == Pedalboard::START_CONTROL_ID) { + index = Pedalboard::START_CONTROL_ID; + } else if (instanceId == Pedalboard::END_CONTROL_ID) { + index = Pedalboard::END_CONTROL_ID; + } else { + index = this->currentPedalboard->GetIndexOfInstanceId(instanceId); + } + } + break; + case Pedalboard::InstanceType::MainInsert: + if (this->currentMainInsertPedalboard) { + if (instanceId == Pedalboard::MAIN_INSERT_START_CONTROL_ID) { + index = Pedalboard::MAIN_INSERT_START_CONTROL_ID; + } else if (instanceId == Pedalboard::MAIN_INSERT_END_CONTROL_ID) { + index = Pedalboard::END_CONTROL_ID; + } else { + index = this->currentMainInsertPedalboard->GetIndexOfInstanceId(instanceId); + } + } + break; + case Pedalboard::InstanceType::AuxInsert: + if (this->currentAuxInsertPedalboard) { + if (instanceId == Pedalboard::AUX_INSERT_START_CONTROL_ID) { + index = Pedalboard::START_CONTROL_ID; + } else if (instanceId == Pedalboard::AUX_INSERT_END_CONTROL_ID) { + index = Pedalboard::END_CONTROL_ID; + } else { + index = this->currentAuxInsertPedalboard->GetIndexOfInstanceId(instanceId); + } + } + break; + } + RealtimePedalboardItemIndex result { instanceType, index}; + return result; + } virtual void SetVuSubscriptions(const std::vector &instanceIds) { std::lock_guard guard(mutex); @@ -1917,7 +1962,7 @@ public: auto effect = this->currentPedalboard->GetEffect(instanceId); if (effect) { - int index = this->currentPedalboard->GetIndexOfInstanceId(instanceIds[i]); + RealtimePedalboardItemIndex index = this->GetRealtimeItemIndex(instanceIds[i]); vuConfig->enabledIndexes.push_back(index); VuUpdate v; v.instanceId_ = instanceId; @@ -1929,20 +1974,41 @@ public: vuConfig->vuUpdateWorkingData.push_back(v); vuConfig->vuUpdateResponseData.push_back(v); } - else if (instanceId == Pedalboard::INPUT_VOLUME_ID || instanceId == Pedalboard::OUTPUT_VOLUME_ID) + else if ( + instanceId == Pedalboard::START_CONTROL_ID || instanceId == Pedalboard::END_CONTROL_ID + || instanceId == Pedalboard::MAIN_INSERT_START_CONTROL_ID || instanceId == Pedalboard::MAIN_INSERT_END_CONTROL_ID + || instanceId == Pedalboard::AUX_INSERT_START_CONTROL_ID || instanceId == Pedalboard::AUX_INSERT_END_CONTROL_ID + ) { - int index = (int)instanceId; + auto index = GetRealtimeItemIndex(instanceId); VuUpdate v; vuConfig->enabledIndexes.push_back(index); + v.instanceId_ = instanceId; - if (instanceId == Pedalboard::INPUT_VOLUME_ID) + size_t nChannels = 0; + + switch (instanceId) { - v.isStereoInput_ = v.isStereoOutput_ = this->pHost->GetNumberOfInputAudioChannels() > 1; - } - else - { - v.isStereoInput_ = v.isStereoOutput_ = this->pHost->GetNumberOfOutputAudioChannels() > 1; + case Pedalboard::START_CONTROL_ID: + nChannels = this->pHost->GetChannelSelection().mainInputChannels().size(); + break; + case Pedalboard::END_CONTROL_ID: + nChannels = this->pHost->GetChannelSelection().mainOutputChannels().size(); + break; + case Pedalboard::MAIN_INSERT_START_CONTROL_ID: + nChannels = this->pHost->GetChannelSelection().mainOutputChannels().size(); + break; + case Pedalboard::MAIN_INSERT_END_CONTROL_ID: + nChannels = this->pHost->GetChannelSelection().mainOutputChannels().size(); + break; + case Pedalboard::AUX_INSERT_START_CONTROL_ID: + nChannels = this->pHost->GetChannelSelection().auxInputChannels().size(); + break; + case Pedalboard::AUX_INSERT_END_CONTROL_ID: + nChannels = this->pHost->GetChannelSelection().auxOutputChannels().size(); + break; } + v.isStereoInput_ = v.isStereoOutput_ = nChannels > 1; vuConfig->vuUpdateWorkingData.push_back(v); vuConfig->vuUpdateResponseData.push_back(v); } diff --git a/src/AudioHost.hpp b/src/AudioHost.hpp index f582547..d5af186 100644 --- a/src/AudioHost.hpp +++ b/src/AudioHost.hpp @@ -32,6 +32,7 @@ #include "Promise.hpp" #include "json_variant.hpp" #include "RealtimeMidiEventType.hpp" +#include "ChannelRouterSettings.hpp" namespace pipedal { @@ -227,7 +228,7 @@ namespace pipedal virtual std::string AtomToJson(const LV2_Atom *atom) = 0; - virtual void Open(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) = 0; + virtual void Open(const JackServerSettings &jackServerSettings, const ChannelSelection &channelSelection) = 0; virtual void Close() = 0; virtual void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) = 0; diff --git a/src/Banks.hpp b/src/Banks.hpp index 4522426..18951f3 100644 --- a/src/Banks.hpp +++ b/src/Banks.hpp @@ -240,7 +240,7 @@ namespace pipedal { // zero length? We can never have a zero-length bank. // Add a default preset and make it the selected preset. - Pedalboard pedalboard = Pedalboard::MakeDefault(); + Pedalboard pedalboard = Pedalboard::MakeDefault(Pedalboard::InstanceType::MainPedalboard); this->addPreset(pedalboard); newSelection = presets_[0]->instanceId(); } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 97a2ffb..e8f0bae 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -204,6 +204,7 @@ else() endif() set (PIPEDAL_SOURCES + ChannelRouterSettings.cpp ChannelRouterSettings.hpp CrashGuard.cpp CrashGuard.hpp ModTemplateGenerator.cpp ModTemplateGenerator.hpp WebServerMod.cpp WebServerMod.hpp diff --git a/src/ChannelRouterSettings.cpp b/src/ChannelRouterSettings.cpp new file mode 100644 index 0000000..0b9e9be --- /dev/null +++ b/src/ChannelRouterSettings.cpp @@ -0,0 +1,151 @@ +// Copyright (c) 2026 Robin 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. + + +#include "ChannelRouterSettings.hpp" +#include + +using namespace pipedal; + +static uint64_t countChannels(const std::vector &channels) +{ + if (channels.size() != 2) + { + throw std::runtime_error("Invalid Channel Router channel configuration."); + } + if (channels[0] == -1) + { + if (channels[1] == -1) + { + return 0; + } + else + { + return 1; + } + } + else + { + if (channels[1] == -1) + { + return 1; + } + if (channels[0] == channels[1]) + { + return 1; + } + return 2; + } +} + +uint64_t ChannelRouterSettings::numberOfAudioInputChannels() const +{ + if (!configured_) + { + return 2; + } + return countChannels(mainInputChannels_); +} +uint64_t ChannelRouterSettings::numberOfAudioOutputChannels() const +{ + if (!configured_) + { + return 2; + } + return countChannels(mainOutputChannels_); +} + +ChannelSelection::ChannelSelection(ChannelRouterSettings&settings) +: mainInputChannels_(settings.mainInputChannels()) +, mainOutputChannels_(settings.mainOutputChannels()) +, auxInputChannels_(settings.auxInputChannels()) +, auxOutputChannels_(settings.auxOutputChannels()) +, sendInputChannels_(settings.sendInputChannels()) +, sendOutputChannels_(settings.sendOutputChannels()) +{ + normalizeChannelSelection(); +} + +static void normalizeInputChannels(std::vector&channels) { + if (channels.size() == 2) { + if (channels[0] == -1 && channels[1] == -1) + { + channels.resize(0); + } else if (channels[0] == channels[1]) { + channels.resize(1); + } else if (channels[1] == -1) { + channels.resize(1);} { + } + } +} + +static void normalizeOutputChannels(std::vector&channels) { + normalizeInputChannels(channels); +} + +void ChannelSelection::normalizeChannelSelection() { + normalizeInputChannels(mainInputChannels_); + normalizeOutputChannels(mainOutputChannels_); + normalizeInputChannels(auxInputChannels_); + normalizeOutputChannels(auxOutputChannels_); + normalizeInputChannels(sendInputChannels_); + normalizeOutputChannels(sendOutputChannels_); + + // If either aux inputs or outputs are zero, don't do ANY aux processing. + if (auxInputChannels_.size() == 0) + { + auxOutputChannels_.resize(0); + } else if (auxOutputChannels_.size() == 0) { + auxInputChannels_.resize(0); + } + // If either main inputs or outputs are empty, add send/receive dummy buffers. (Lv2 plugins shouldn't have to deal with this) + if (mainInputChannels_.size() == 0) + { + mainInputChannels_.resize(1); + mainInputChannels_[0] = -1; + } + if (mainOutputChannels_.size() == 0) { + mainInputChannels_.resize(1); + mainOutputChannels_[0] = -1; + } + // Send buffers are what they are. Let the send plugin deal with it. +} + + +ChannelRouterSettings::ChannelRouterSettings() +: mainInserts_(Pedalboard::InstanceType::MainInsert) +, auxInserts_(Pedalboard::InstanceType::AuxInsert) +{ + +} + + +JSON_MAP_BEGIN(ChannelRouterSettings) +JSON_MAP_REFERENCE(ChannelRouterSettings, configured) +JSON_MAP_REFERENCE(ChannelRouterSettings, channelRouterPresetId) +JSON_MAP_REFERENCE(ChannelRouterSettings, mainInputChannels) +JSON_MAP_REFERENCE(ChannelRouterSettings, mainOutputChannels) +JSON_MAP_REFERENCE(ChannelRouterSettings, mainInserts) +JSON_MAP_REFERENCE(ChannelRouterSettings, auxInputChannels) +JSON_MAP_REFERENCE(ChannelRouterSettings, auxOutputChannels) +JSON_MAP_REFERENCE(ChannelRouterSettings, auxInserts) +JSON_MAP_REFERENCE(ChannelRouterSettings, sendInputChannels) +JSON_MAP_REFERENCE(ChannelRouterSettings, sendOutputChannels) +JSON_MAP_REFERENCE(ChannelRouterSettings, controlValues) +JSON_MAP_END() diff --git a/src/ChannelRouterSettings.hpp b/src/ChannelRouterSettings.hpp new file mode 100644 index 0000000..885b6e9 --- /dev/null +++ b/src/ChannelRouterSettings.hpp @@ -0,0 +1,107 @@ +// Copyright (c) 2026 Robin 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. + +#pragma once + +#include +#include "json.hpp" +#include "Pedalboard.hpp" +#include + +namespace pipedal +{ + + class ChannelRouterSettings + { + protected: + static constexpr int64_t CHANNEL_ROUTER_MAIN_INSERT_ID = -4; // Reserved Instance ID for Router Main Inserts. + static constexpr int64_t CHANNEL_ROUTER_AUX_INSERT_ID = -5; // Reserved Instance ID for Router Aux inserts. + + bool configured_ = false; + int64_t channelRouterPresetId_ = -1; + + std::vector mainInputChannels_ = {1, 1}; + std::vector mainOutputChannels_ = {0, 1}; + Pedalboard mainInserts_; + + std::vector auxInputChannels_ = {-1, -1}; + std::vector auxOutputChannels_ = {-1, -1}; + Pedalboard auxInserts_; + + std::vector sendInputChannels_ = {-1, -1}; + std::vector sendOutputChannels_ = {-1, -1}; + + std::vector controlValues_; + + public: + using self = ChannelRouterSettings; + using ptr = std::shared_ptr; + + ChannelRouterSettings(); + + uint64_t numberOfAudioInputChannels() const; + uint64_t numberOfAudioOutputChannels() const; + + JSON_GETTER_SETTER(configured) + JSON_GETTER_SETTER(channelRouterPresetId) + JSON_GETTER_SETTER_REF(mainInputChannels) + JSON_GETTER_SETTER_REF(mainOutputChannels) + JSON_GETTER_SETTER_REF(mainInserts) + JSON_GETTER_SETTER_REF(auxInputChannels) + JSON_GETTER_SETTER_REF(auxOutputChannels) + JSON_GETTER_SETTER_REF(auxInserts) + JSON_GETTER_SETTER_REF(sendInputChannels) + JSON_GETTER_SETTER_REF(sendOutputChannels) + JSON_GETTER_SETTER_REF(controlValues) + + + DECLARE_JSON_MAP(ChannelRouterSettings); + + }; + + // just the channel selecttions. + class ChannelSelection { + public: + ChannelSelection() = default; + ChannelSelection(ChannelRouterSettings&settings); + ChannelSelection(const ChannelSelection&other) = default; + ChannelSelection(ChannelSelection&&other) = default; + ChannelSelection& operator=(const ChannelSelection&other) = default; + ChannelSelection& operator=(ChannelSelection&&other) = default; + ~ChannelSelection() = default; + + const std::vector&mainInputChannels() const { return mainInputChannels_; } + const std::vector&mainOutputChannels() const { return mainOutputChannels_; } + const std::vector&auxInputChannels() const { return auxInputChannels_; } + const std::vector&auxOutputChannels() const { return auxOutputChannels_; } + const std::vector&sendInputChannels() const { return sendInputChannels_; } + const std::vector&sendOutputChannels() const { return sendOutputChannels_; } + + private: + void normalizeChannelSelection(); + + std::vector mainInputChannels_; + std::vector mainOutputChannels_; + std::vector auxInputChannels_; + std::vector auxOutputChannels_; + std::vector sendInputChannels_; + std::vector sendOutputChannels_; + }; + +} \ No newline at end of file diff --git a/src/DummyAudioDriver.cpp b/src/DummyAudioDriver.cpp index dd2301b..0d298d7 100644 --- a/src/DummyAudioDriver.cpp +++ b/src/DummyAudioDriver.cpp @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2024 Robin E. R. Davies + * Copyright (c) 2026 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 @@ -23,6 +23,7 @@ */ #include "pch.h" +#include "PiPedalCommon.hpp" #include "util.hpp" #include #include @@ -39,6 +40,7 @@ #include "ss.hpp" #include "SchedulerPriority.hpp" #include "CrashGuard.hpp" +#include "ChannelRouterSettings.hpp" #include "CpuUse.hpp" @@ -85,14 +87,19 @@ namespace pipedal uint32_t captureFrameSize = 0; - std::vector activeCaptureBuffers; - std::vector activePlaybackBuffers; + std::vector> allocatedBuffers; + std::vector mainCaptureBuffers; + std::vector mainPlaybackBuffers; - std::vector captureBuffers; - std::vector playbackBuffers; + std::vector auxCaptureBuffers; + std::vector auxPlaybackBuffers; + + std::vector sendCaptureBuffers; + std::vector sendPlaybackBuffers; + + std::vector deviceCaptureBuffers; + std::vector devicePlaybackBuffers; - uint8_t *rawCaptureBuffer = nullptr; - uint8_t *rawPlaybackBuffer = nullptr; AudioDriverHost *driverHost = nullptr; uint32_t channels = 2; @@ -145,7 +152,7 @@ namespace pipedal std::atomic terminateAudio_ = false; - void terminateAudio(bool terminate) + PIPEDAL_NON_INLINE void terminateAudio(bool terminate) { this->terminateAudio_ = terminate; } @@ -161,32 +168,28 @@ namespace pipedal } private: - void AllocateBuffers(std::vector &buffers, size_t n) + PIPEDAL_NON_INLINE void AllocateBuffers(std::vector &buffers, size_t n) { buffers.resize(n); for (size_t i = 0; i < n; ++i) { - buffers[i] = new float[this->bufferSize]; - for (size_t j = 0; j < this->bufferSize; ++j) - { - buffers[i][j] = 0; - } + buffers[i] = AllocateAudioBuffer(); } } - virtual size_t GetMidiInputEventCount() override + PIPEDAL_NON_INLINE virtual size_t GetMidiInputEventCount() override { return midiEventCount; } - virtual MidiEvent *GetMidiEvents() override + PIPEDAL_NON_INLINE virtual MidiEvent *GetMidiEvents() override { return this->midiEvents.data(); } - JackChannelSelection channelSelection; + ChannelSelection channelSelection; bool open = false; - virtual void Open(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) + PIPEDAL_NON_INLINE virtual void Open(const JackServerSettings &jackServerSettings, const ChannelSelection &channelSelection) { terminateAudio_ = false; if (open) @@ -208,7 +211,7 @@ namespace pipedal throw; } } - virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) override + PIPEDAL_NON_INLINE virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) override { this->alsaSequencer = alsaSequencer; } @@ -220,23 +223,28 @@ namespace pipedal << ", " << "Native float" << ", " << this->sampleRate << ", " << this->bufferSize << "x" << this->numberOfBuffers - << ", in: " << this->InputBufferCount() << "/" << this->captureChannels - << ", out: " << this->OutputBufferCount() << "/" << this->playbackChannels); + << ", " << "device in: " << this->DeviceInputBufferCount() + << ", " << "device out: " << this->DeviceOutputBufferCount() + << ", main in: " << this->MainInputBufferCount() + << ", main out: " << this->MainOutputBufferCount() + << ", aux in: " << this->AuxInputBufferCount() + << ", aux out: " << this->AuxOutputBufferCount() + << ", send in: " << this->SendInputBufferCount() + << ", send out: " << this->SendOutputBufferCount() + ); return result; } - void OpenAudio(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) + void OpenAudio(const JackServerSettings &jackServerSettings, const ChannelSelection &channelSelection) { int err; this->numberOfBuffers = jackServerSettings.GetNumberOfBuffers(); this->bufferSize = jackServerSettings.GetBufferSize(); - AllocateBuffers(captureBuffers, channels); - AllocateBuffers(playbackBuffers, channels); } - std::jthread *audioThread; + std::unique_ptr audioThread; bool audioRunning; bool block = false; @@ -303,7 +311,7 @@ namespace pipedal this->driverHost->OnProcess(framesRead); /// no attempt at realtime. Just as long as we run occasionally. - std::this_thread::sleep_for(std::chrono::milliseconds(20)); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); } @@ -318,22 +326,6 @@ namespace pipedal bool alsaActive = false; - static int IndexFromPortName(const std::string &s) - { - auto pos = s.find_last_of('_'); - if (pos == std::string::npos) - { - throw std::invalid_argument("Bad port name."); - } - const char *p = s.c_str() + (pos + 1); - - int v = atoi(p); - if (v < 0) - { - throw std::invalid_argument("Bad port name."); - } - return v; - } bool activated = false; virtual void Activate() @@ -344,41 +336,17 @@ namespace pipedal } activated = true; - this->activeCaptureBuffers.resize(channelSelection.GetInputAudioPorts().size()); + AllocateBuffers(deviceCaptureBuffers, channels); + AllocateBuffers(devicePlaybackBuffers, channels); - playbackBuffers.resize(channels); + AllocateBuffers(mainCaptureBuffers, channelSelection.mainInputChannels().size()); + AllocateBuffers(mainPlaybackBuffers, channelSelection.mainOutputChannels().size()); + AllocateBuffers(auxCaptureBuffers, channelSelection.auxInputChannels().size()); + AllocateBuffers(auxPlaybackBuffers, channelSelection.auxOutputChannels().size()); + AllocateBuffers(sendCaptureBuffers, channelSelection.sendInputChannels().size()); + AllocateBuffers(sendPlaybackBuffers, channelSelection.sendOutputChannels().size()); - int ix = 0; - for (auto &x : channelSelection.GetInputAudioPorts()) - { - int sourceIndex = IndexFromPortName(x); - if (sourceIndex >= captureBuffers.size()) - { - Lv2Log::error(SS("Invalid audio input port: " << x)); - } - else - { - this->activeCaptureBuffers[ix++] = this->captureBuffers[sourceIndex]; - } - } - - this->activePlaybackBuffers.resize(channelSelection.GetOutputAudioPorts().size()); - - ix = 0; - for (auto &x : channelSelection.GetOutputAudioPorts()) - { - int sourceIndex = IndexFromPortName(x); - if (sourceIndex >= playbackBuffers.size()) - { - Lv2Log::error(SS("Invalid audio output port: " << x)); - } - else - { - this->activePlaybackBuffers[ix++] = this->playbackBuffers[sourceIndex]; - } - } - - audioThread = new std::jthread([this]() + audioThread = std::make_unique([this]() { AudioThread(); }); } @@ -393,7 +361,7 @@ namespace pipedal if (audioThread) { this->audioThread->join(); - this->audioThread = 0; + this->audioThread = nullptr; } Lv2Log::debug("Audio thread joined."); } @@ -402,46 +370,64 @@ namespace pipedal public: + virtual size_t DeviceInputBufferCount() const override { return deviceCaptureBuffers.size(); } + virtual size_t DeviceOutputBufferCount() const override { return devicePlaybackBuffers.size(); } - virtual size_t InputBufferCount() const { return activeCaptureBuffers.size(); } - virtual float *GetInputBuffer(size_t channel) + virtual size_t MainInputBufferCount() const override { return mainCaptureBuffers.size(); } + virtual float *GetMainInputBuffer(size_t channel) override { - return activeCaptureBuffers[channel]; + return mainCaptureBuffers[channel]; } - virtual size_t OutputBufferCount() const { return activePlaybackBuffers.size(); } - virtual float *GetOutputBuffer(size_t channel) + virtual size_t MainOutputBufferCount() const { return mainPlaybackBuffers.size(); } + virtual float *GetMainOutputBuffer(size_t channel) override { - return activePlaybackBuffers[channel]; + return mainPlaybackBuffers[channel]; } - void FreeBuffers(std::vector &buffer) + virtual size_t AuxInputBufferCount() const override { return auxCaptureBuffers.size(); } + virtual float *GetAuxInputBuffer(size_t channel) override { - for (size_t i = 0; i < buffer.size(); ++i) - { - // delete[] buffer[i]; - buffer[i] = 0; - } - buffer.clear(); + return auxCaptureBuffers[channel]; } - void DeleteBuffers() + + virtual size_t AuxOutputBufferCount() const override { return auxPlaybackBuffers.size(); } + virtual float *GetAuxOutputBuffer(size_t channel) override { - activeCaptureBuffers.clear(); - activePlaybackBuffers.clear(); - FreeBuffers(this->playbackBuffers); - FreeBuffers(this->captureBuffers); - if (rawCaptureBuffer) - { - delete[] rawCaptureBuffer; - rawCaptureBuffer = nullptr; - } - if (rawPlaybackBuffer) - { - delete[] rawPlaybackBuffer; - rawPlaybackBuffer = nullptr; - } + return auxPlaybackBuffers[channel]; } - virtual void Close() + + virtual size_t SendInputBufferCount() const override { return sendCaptureBuffers.size(); } + virtual float *GetSendInputBuffer(size_t channel) override + { + return sendCaptureBuffers[channel]; + } + + virtual size_t SendOutputBufferCount() const override { return sendPlaybackBuffers.size(); } + virtual float *GetSendOutputBuffer(size_t channel) override + { + return sendPlaybackBuffers[channel]; + } + + + + + + PIPEDAL_NON_INLINE float*AllocateAudioBuffer() { + allocatedBuffers.push_back(std::vector(bufferSize)); + return allocatedBuffers.back().data(); + } + PIPEDAL_NON_INLINE void DeleteBuffers() + { + mainCaptureBuffers.clear(); + mainPlaybackBuffers.clear(); + auxCaptureBuffers.clear(); + auxPlaybackBuffers.clear(); + sendCaptureBuffers.clear(); + sendPlaybackBuffers.clear(); + allocatedBuffers.clear(); + } + PIPEDAL_NON_INLINE virtual void Close() { if (!open) { @@ -453,18 +439,18 @@ namespace pipedal DeleteBuffers(); } - virtual float CpuUse() + PIPEDAL_NON_INLINE virtual float CpuUse() { return 0; } - virtual float CpuOverhead() + PIPEDAL_NON_INLINE virtual float CpuOverhead() { return 0.1; } }; - AudioDriver *CreateDummyAudioDriver(AudioDriverHost *driverHost,const std::string&deviceName) + PIPEDAL_NON_INLINE AudioDriver *CreateDummyAudioDriver(AudioDriverHost *driverHost,const std::string&deviceName) { return new DummyDriverImpl(driverHost,deviceName); } diff --git a/src/IHost.hpp b/src/IHost.hpp index ab02649..48c7c52 100644 --- a/src/IHost.hpp +++ b/src/IHost.hpp @@ -26,6 +26,7 @@ namespace pipedal { class Lv2PluginInfo; class IEffect; class HostWorkerThread; + class ChannelSelection; class IHost { @@ -43,13 +44,12 @@ namespace pipedal { virtual size_t GetAtomBufferSize() const = 0; virtual bool HasMidiInputChannel() const = 0; - virtual int GetNumberOfInputAudioChannels() const = 0; - virtual int GetNumberOfOutputAudioChannels() const = 0; virtual std::shared_ptr GetPluginInfo(const std::string &uri) const = 0; virtual IEffect *CreateEffect(PedalboardItem &pedalboard) = 0; virtual std::string GetPluginStoragePath() const = 0; + virtual const ChannelSelection&GetChannelSelection() const = 0; }; } \ No newline at end of file diff --git a/src/JackConfiguration.hpp b/src/JackConfiguration.hpp index b76abb0..a6a9bc4 100644 --- a/src/JackConfiguration.hpp +++ b/src/JackConfiguration.hpp @@ -124,5 +124,4 @@ namespace pipedal DECLARE_JSON_MAP(JackChannelSelection); }; - } // namespace. \ No newline at end of file diff --git a/src/Lv2Pedalboard.cpp b/src/Lv2Pedalboard.cpp index 4c750c8..34d0055 100644 --- a/src/Lv2Pedalboard.cpp +++ b/src/Lv2Pedalboard.cpp @@ -574,7 +574,7 @@ void Lv2Pedalboard::ComputeVus(RealtimeVuBuffers *vuConfiguration, uint32_t samp { int index = vuConfiguration->enabledIndexes[i]; VuUpdate *pUpdate = &vuConfiguration->vuUpdateWorkingData[i]; - if (index == Pedalboard::INPUT_VOLUME_ID) + if (index == Pedalboard::START_CONTROL_ID) { if (this->pedalboardInputBuffers.size() > 1) { @@ -588,7 +588,7 @@ void Lv2Pedalboard::ComputeVus(RealtimeVuBuffers *vuConfiguration, uint32_t samp pUpdate->AccumulateOutputs(&(this->pedalboardInputBuffers[0][0]), samples); // after input volume applied. } } - else if (index == Pedalboard::OUTPUT_VOLUME_ID) + else if (index == Pedalboard::END_CONTROL_ID) { if (this->pedalboardOutputBuffers.size() > 1) { diff --git a/src/Pedalboard.cpp b/src/Pedalboard.cpp index 97d7f0f..5cdc8fb 100644 --- a/src/Pedalboard.cpp +++ b/src/Pedalboard.cpp @@ -210,7 +210,7 @@ PedalboardItem Pedalboard::MakeSplit() -Pedalboard Pedalboard::MakeDefault() +Pedalboard Pedalboard::MakeDefault(InstanceType instanceType) { // copy insanity. but it happens so rarely. Pedalboard result; @@ -510,6 +510,22 @@ Snapshot Pedalboard::MakeSnapshotFromCurrentSettings(const Pedalboard &previousP } +Pedalboard::Pedalboard(InstanceType instanceType = InstanceType::MainPedalboard) +{ + switch (instanceType) + { + case InstanceType::MainPedalboard: + nextInstanceId_ = 0; + break; + case InstanceType::MainInsert: + nextInstanceId_ = MAIN_INSERT_INSTANCE_BASE;; + break; + case InstanceType::AuxInsert: + nextInstanceId_ = AUX_INSERT_INSTANCE_BASE;; + break; + } +} + JSON_MAP_BEGIN(ControlValue) JSON_MAP_REFERENCE(ControlValue,key) diff --git a/src/Pedalboard.hpp b/src/Pedalboard.hpp index 173b497..9532efb 100644 --- a/src/Pedalboard.hpp +++ b/src/Pedalboard.hpp @@ -230,11 +230,52 @@ class Pedalboard { int64_t selectedPlugin_ = -1; + static constexpr int64_t TWO_POWER_52 = 4503599627370496; // Maximum Javascript integer value. + static constexpr int64_t TWO_POWER_48 = 281474976710656; // Number of possible instance IDs for Main Pedalboards. + static constexpr int64_t MAX_INSTANCE_ID = TWO_POWER_48; + public: + + enum class InstanceType { + MainPedalboard, + MainInsert, + AuxInsert + }; + static constexpr int64_t MAIN_INSERT_INSTANCE_BASE = TWO_POWER_52-2*MAX_INSTANCE_ID; + static constexpr int64_t AUX_INSERT_INSTANCE_BASE = TWO_POWER_52-1*MAX_INSTANCE_ID; + + static constexpr int64_t CHANNEL_ROUTER_CONTROLS_INSTANCE_ID = -4; // Reserved Instance ID for Router Controls. + + static constexpr int64_t START_CONTROL_ID = -2; // synthetic PedalboardItem for input volume. + static constexpr int64_t END_CONTROL_ID = -3; // synthetic PedalboardItem for output volume. + static constexpr int64_t MAIN_INSERT_START_CONTROL_ID = MAIN_INSERT_INSTANCE_BASE + MAX_INSTANCE_ID -2; + static constexpr int64_t MAIN_INSERT_END_CONTROL_ID = MAIN_INSERT_INSTANCE_BASE + MAX_INSTANCE_ID -3; + static constexpr int64_t AUX_INSERT_START_CONTROL_ID = AUX_INSERT_INSTANCE_BASE + MAX_INSTANCE_ID -2; + static constexpr int64_t AUX_INSERT_END_CONTROL_ID = AUX_INSERT_INSTANCE_BASE + MAX_INSTANCE_ID -3; + + static InstanceType GetInstanceTypeFromInstanceId(int64_t instanceId) { + if (instanceId == START_CONTROL_ID) return InstanceType::MainPedalboard; + if (instanceId == END_CONTROL_ID) return InstanceType::MainPedalboard; + + if (instanceId >= MAIN_INSERT_INSTANCE_BASE) { + return InstanceType::MainInsert; + } else if (instanceId >= AUX_INSERT_INSTANCE_BASE) { + return InstanceType::AuxInsert; + } else { + return InstanceType::MainPedalboard; + } + } + InstanceType GetInstanceType() const { + return GetInstanceTypeFromInstanceId(this->nextInstanceId_); // instanceId is irrelevant here. + } + + + + Pedalboard(InstanceType instanceType = InstanceType::MainPedalboard); + // deep copy, breaking shared pointers. Pedalboard DeepCopy(); - static constexpr int64_t INPUT_VOLUME_ID = -2; // synthetic PedalboardItem for input volume. - static constexpr int64_t OUTPUT_VOLUME_ID = -3; // synthetic PedalboardItem for output volume. + bool SetControlValue(int64_t pedalItemId, const std::string &symbol, float value); bool SetItemTitle(int64_t pedalItemId, const std::string &title, const std::string&iconColor); bool SetItemEnabled(int64_t pedalItemId, bool enabled); @@ -266,7 +307,7 @@ public: PedalboardItem MakeSplit(); - static Pedalboard MakeDefault(); + static Pedalboard MakeDefault(Pedalboard::InstanceType instanceType); }; #undef GETTER_SETTER_REF diff --git a/src/PiLatencyMain.cpp b/src/PiLatencyMain.cpp index 01887e5..edc6d4f 100644 --- a/src/PiLatencyMain.cpp +++ b/src/PiLatencyMain.cpp @@ -364,16 +364,16 @@ public: virtual void OnProcess(size_t nFrames) { - size_t inputs = audioDriver->InputBufferCount(); - size_t outputs = audioDriver->OutputBufferCount(); + size_t inputs = audioDriver->MainInputBufferCount(); + size_t outputs = audioDriver->MainOutputBufferCount(); for (size_t i = 0; i < inputs; ++i) { - inputBuffers[i] = audioDriver->GetInputBuffer(i); + inputBuffers[i] = audioDriver->GetMainInputBuffer(i); } for (size_t i = 0; i < outputs; ++i) { - outputBuffers[i] = audioDriver->GetOutputBuffer(i); + outputBuffers[i] = audioDriver->GetMainOutputBuffer(i); } for (size_t i = 0; i < nFrames; ++i) diff --git a/src/PiPedalCommon.hpp b/src/PiPedalCommon.hpp new file mode 100644 index 0000000..a464e39 --- /dev/null +++ b/src/PiPedalCommon.hpp @@ -0,0 +1,32 @@ +// Copyright (c) 2026 Robin 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. + + + +#pragma once + +#ifndef PIPEDAL_NON_INLINE +#if defined(__GNUC__) || defined(__clang__) +#define PIPEDAL_NON_INLINE __attribute__((noinline)) +#elif defined(_MSC_VER) +#define PIPEDAL_NON_INLINE __declspec(noinline) +#else +#define PIPEDAL_NON_INLINE +#endif +#endif diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index 7e9f4ff..f07b645 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -81,7 +81,7 @@ PiPedalModel::PiPedalModel() this->updater = Updater::Create(); this->updater->Start(); this->currentUpdateStatus = updater->GetCurrentStatus(); - this->pedalboard = Pedalboard::MakeDefault(); + this->pedalboard = Pedalboard::MakeDefault(Pedalboard::InstanceType::MainPedalboard); #if JACK_HOST this->jackServerSettings = this->storage.GetJackServerSettings(); // to get onboarding flag. this->jackServerSettings.ReadJackDaemonConfiguration(); @@ -216,6 +216,7 @@ void PiPedalModel::Init(const PiPedalConfiguration &configuration) storage.Initialize(); pluginHost.SetPluginStoragePath(storage.GetPluginUploadDirectory()); + this->channelRouterSettings = storage.GetChannelRouterSettings(); this->systemMidiBindings = storage.GetSystemMidiBindings(); #if JACK_HOST @@ -298,7 +299,7 @@ void PiPedalModel::Load() if (CrashGuard::HasCrashed()) { // ignore the current preset, and load a blank pedalboard in order to avoid a potential plugin crash. - this->pedalboard = Pedalboard::MakeDefault(); + this->pedalboard = Pedalboard::MakeDefault(Pedalboard::InstanceType::MainPedalboard); } else { @@ -1422,12 +1423,8 @@ void PiPedalModel::RestartAudio(bool useDummyAudioDriver) throw std::runtime_error("Audio configuration not valid."); } - auto channelSelection = this->storage.GetJackChannelSelection(jackConfiguration); + const auto & channelSelection = this->storage.GetChannelSelection(); - if (!channelSelection.isValid()) - { - throw std::runtime_error("Audio configuration not valid."); - } this->audioHost->Open(jackServerSettings, channelSelection); this->pluginHost.OnConfigurationChanged(jackConfiguration, channelSelection); @@ -1528,21 +1525,6 @@ std::vector PiPedalModel::GetAlsaSequencerPorts() return result; } -void PiPedalModel::SetJackChannelSelection(int64_t clientId, const JackChannelSelection &channelSelection) -{ - { - std::lock_guard lock(mutex); // copy atomically. - this->storage.SetJackChannelSelection(channelSelection); - - this->pluginHost.OnConfigurationChanged(jackConfiguration, channelSelection); - - CancelAudioRetry(); - } - - RestartAudio(); // no lock to avoid mutex deadlock when reader thread is sending notifications.. - - this->FireChannelSelectionChanged(clientId); -} void PiPedalModel::FireChannelSelectionChanged(int64_t clientId) { @@ -1671,7 +1653,7 @@ void PiPedalModel::UpdateRealtimeVuSubscriptions() for (int i = 0; i < activeVuSubscriptions.size(); ++i) { auto instanceId = activeVuSubscriptions[i].instanceid; - if (pedalboard.HasItem(instanceId) || instanceId == Pedalboard::INPUT_VOLUME_ID || instanceId == Pedalboard::OUTPUT_VOLUME_ID) + if (pedalboard.HasItem(instanceId) || instanceId == Pedalboard::START_CONTROL_ID || instanceId == Pedalboard::END_CONTROL_ID) { addedInstances.insert(activeVuSubscriptions[i].instanceid); } @@ -3330,3 +3312,27 @@ int64_t PiPedalModel::CopyPresetsToBank(int64_t bankInstanceId, const std::vecto return lastAdded; } + + +ChannelRouterSettings::ptr PiPedalModel::GetChannelRouterSettings() +{ + std::lock_guard lock(mutex); + return this->channelRouterSettings; +} + +void PiPedalModel::SetChannelRouterSettings(int64_t clientId,ChannelRouterSettings::ptr &settings) +{ + { + std::lock_guard lock(mutex); + this->channelRouterSettings = settings; + this->storage.SetChannelRouterSettings(settings); + this->pluginHost.OnConfigurationChanged(jackConfiguration, *settings); + CancelAudioRetry(); + } + RestartAudio(); // no lock to avoid mutex deadlock when reader thread is sending notifications.. + + this->FireChannelSelectionChanged(clientId); + +} + + diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index 897f524..17f1cf9 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -40,6 +40,7 @@ #include "Promise.hpp" #include "AtomConverter.hpp" #include "FileEntry.hpp" +#include "ChannelRouterSettings.hpp" #include namespace pipedal @@ -176,6 +177,8 @@ namespace pipedal AtomConverter atomConverter; // must be AFTER pluginHost! Pedalboard pedalboard; + ChannelRouterSettings::ptr channelRouterSettings; + bool previousPedalboardLoaded = false; Pedalboard previousPedalboard; Storage storage; @@ -397,7 +400,6 @@ namespace pipedal JackConfiguration GetJackConfiguration(); - void SetJackChannelSelection(int64_t clientId, const JackChannelSelection &channelSelection); JackChannelSelection GetJackChannelSelection(); void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration); @@ -497,6 +499,9 @@ namespace pipedal std::vector RequestBankPresets(int64_t bankInstanceId); int64_t ImportPresetsFromBank(int64_t bankInstanceId, const std::vector &presets); int64_t CopyPresetsToBank(int64_t bankInstanceId, const std::vector &presets); + + void SetChannelRouterSettings(int64_t clientId, ChannelRouterSettings::ptr &settings); + ChannelRouterSettings::ptr GetChannelRouterSettings(); }; } // namespace pipedal. \ No newline at end of file diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index a3bf46b..85d0624 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2022-2023 Robin Davies +// Copyright (c) 2026 Robin 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 @@ -1422,12 +1422,6 @@ public: this->model.AddNotificationSubscription(shared_from_this()); Reply(replyTo, "ehlo", clientId); } - else if (message == "setJackSettings") - { - JackChannelSelection jackSettings; - pReader->read(&jackSettings); - this->model.SetJackChannelSelection(this->clientId, jackSettings); - } else if (message == "setShowStatusMonitor") { bool showStatusMonitor; @@ -1847,6 +1841,14 @@ public: pReader->read(&args); auto result = this->model.CopyPresetsToBank(args.bankInstanceId_, args.presets_); this->Reply(replyTo,"copyPresetsToBank",result); + } else if (message == "getChannelRouterSettings") + { + ChannelRouterSettings::ptr result = this->model.GetChannelRouterSettings(); + this->Reply(replyTo, "getChannelRouterSettings", result); + } else if (message == "setChannelRouterSettings") { + ChannelRouterSettings::ptr args; + pReader->read(&args); + this->model.SetChannelRouterSettings(this->clientId, args); } else { diff --git a/src/PluginHost.cpp b/src/PluginHost.cpp index 6973a5b..ca1c3c7 100644 --- a/src/PluginHost.cpp +++ b/src/PluginHost.cpp @@ -34,6 +34,7 @@ #include "lv2/urid/urid.h" #include "lv2/ui/ui.h" #include "lv2/core/lv2.h" +#include "ChannelRouterSettings.hpp" // #include "lv2.h" #include "lv2/atom/atom.h" @@ -315,15 +316,15 @@ PluginHost::PluginHost() this->urids = new Urids(mapFeature); } -void PluginHost::OnConfigurationChanged(const JackConfiguration &configuration, const JackChannelSelection &settings) + +void PluginHost::OnConfigurationChanged(const JackConfiguration &configuration, const ChannelSelection &channelSelection) { this->sampleRate = configuration.sampleRate(); if (configuration.isValid()) { - this->numberOfAudioInputChannels = settings.GetInputAudioPorts().size(); - this->numberOfAudioOutputChannels = settings.GetOutputAudioPorts().size(); this->maxBufferSize = configuration.blockLength(); } + this->channelSelection = channelSelection; } PluginHost::~PluginHost() diff --git a/src/PluginHost.hpp b/src/PluginHost.hpp index 9e0abec..5b31745 100644 --- a/src/PluginHost.hpp +++ b/src/PluginHost.hpp @@ -43,6 +43,7 @@ #include "PiPedalUI.hpp" #include "MapPathFeature.hpp" #include "ModGui.hpp" +#include "ChannelRouterSettings.hpp" namespace pipedal { @@ -54,6 +55,7 @@ namespace pipedal class PluginHost; class JackConfiguration; class JackChannelSelection; + class ChannelRouterSettings; #ifndef LV2_PROPERTY_GETSET #define LV2_PROPERTY_GETSET(name) \ @@ -893,8 +895,8 @@ namespace pipedal size_t maxBufferSize = 1024; size_t maxAtomBufferSize = 16 * 1024; bool hasMidiInputChannel; - int numberOfAudioInputChannels = 1; - int numberOfAudioOutputChannels = 1; + ChannelSelection channelSelection; + double sampleRate = 48000; std::string vst3CachePath; @@ -955,13 +957,12 @@ namespace pipedal private: // IHost implementation. - virtual void SetMaxAudioBufferSize(size_t size) { maxBufferSize = size; } - virtual size_t GetMaxAudioBufferSize() const { return maxBufferSize; } - virtual size_t GetAtomBufferSize() const { return maxAtomBufferSize; } - virtual bool HasMidiInputChannel() const { return hasMidiInputChannel; } - virtual int GetNumberOfInputAudioChannels() const { return numberOfAudioInputChannels; } - virtual int GetNumberOfOutputAudioChannels() const { return numberOfAudioOutputChannels; } - virtual LV2_Feature *const *GetLv2Features() const { return (LV2_Feature *const *)&(this->lv2Features[0]); } + virtual void SetMaxAudioBufferSize(size_t size) override { maxBufferSize = size; } + virtual size_t GetMaxAudioBufferSize() const override { return maxBufferSize; } + virtual size_t GetAtomBufferSize() const override { return maxAtomBufferSize; } + virtual bool HasMidiInputChannel() const override { return hasMidiInputChannel; } + virtual LV2_Feature *const *GetLv2Features() const override { return (LV2_Feature *const *)&(this->lv2Features[0]); } + virtual const ChannelSelection &GetChannelSelection() const override { return this->channelSelection; } public: virtual MapFeature &GetMapFeature() override { return this->mapFeature; } @@ -1027,7 +1028,8 @@ namespace pipedal Urids *urids = nullptr; ModGuiUris *mod_gui_uris = nullptr; - void OnConfigurationChanged(const JackConfiguration &configuration, const JackChannelSelection &settings); + void OnConfigurationChanged(const JackConfiguration &configuration, const ChannelSelection &channelSelection); + std::shared_ptr GetPluginClass(const std::string &uri) const; bool is_a(const std::string &class_, const std::string &target_class); diff --git a/src/RingBufferReader.hpp b/src/RingBufferReader.hpp index c3260fb..46f8c46 100644 --- a/src/RingBufferReader.hpp +++ b/src/RingBufferReader.hpp @@ -96,6 +96,24 @@ namespace pipedal }; + struct RealtimePedalboardItemIndex { + RealtimePedalboardItemIndex() + : instanceType(Pedalboard::InstanceType::MainPedalboard), index(-1) + { + } + RealtimePedalboardItemIndex( + Pedalboard::InstanceType instanceType, + int index) + : instanceType(instanceType), index(index) + { + } + RealtimePedalboardItemIndex(const RealtimePedalboardItemIndex& other) = default; + RealtimePedalboardItemIndex& operator=(const RealtimePedalboardItemIndex& other) = default; + + Pedalboard::InstanceType instanceType = Pedalboard::InstanceType::MainPedalboard; + int index = -1; + }; + struct RealtimeMidiEventRequest { RealtimeMidiEventType eventType; @@ -159,7 +177,7 @@ namespace pipedal return &vuUpdateResponseData; } - std::vector enabledIndexes; + std::vector enabledIndexes; std::vector vuUpdateWorkingData; std::vector vuUpdateResponseData; diff --git a/src/Storage.cpp b/src/Storage.cpp index 81e283b..83a9524 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -196,11 +196,11 @@ void Storage::SetDataRoot(const std::filesystem::path &path) this->dataRoot__audio_uploads = dataRoot / "audio_uploads"; } -const std::filesystem::path &Storage::GetConfigRoot() +const std::filesystem::path &Storage::GetConfigRoot() const { return this->configRoot; } -const std::filesystem::path &Storage::GetDataRoot() +const std::filesystem::path &Storage::GetDataRoot() const { return this->dataRoot; } @@ -249,7 +249,6 @@ static void removeFileNoThrow(const std::filesystem::path &path) } } - void Storage::UpgradeFactoryPresets() { auto presetsDirectory = this->GetPresetsDirectory(); @@ -387,8 +386,10 @@ void Storage::Initialize() LoadWifiConfigSettings(); LoadWifiDirectConfigSettings(); + this->channelRouterSettings = LoadChannelRouterSettings(); LoadUserSettings(); UpgradeFactoryPresets(); + UpgradeChannelRouterSettings(); } void Storage::LoadBank(int64_t instanceId) @@ -687,40 +688,46 @@ int64_t Storage::SaveCurrentPresetAs(const Pedalboard &pedalboard, int64_t bankI } } -static std::string stripNumericSuffix(const std::string &name) { +static std::string stripNumericSuffix(const std::string &name) +{ // remove (digit*) from the end of the string. // Find the last '(' character size_t pos = name.find_last_of('('); - if (pos == std::string::npos) { + if (pos == std::string::npos) + { return name; } // Check if everything between '(' and ')' is digits - size_t len = name.length()-1; - if (name[len] != ')') + size_t len = name.length() - 1; + if (name[len] != ')') { return name; } bool allDigits = true; - for (size_t i = pos + 1; i < len - 1; ++i) { - if (name[i] < '0' || name[i] > '9') { + for (size_t i = pos + 1; i < len - 1; ++i) + { + if (name[i] < '0' || name[i] > '9') + { allDigits = false; break; } } - if (!allDigits) { + if (!allDigits) + { return name; } // Remove trailing spaces before the '(' - while (pos > 0 && name[pos - 1] == ' ') { + while (pos > 0 && name[pos - 1] == ' ') + { --pos; } return name.substr(0, pos); } -static std::string makeUniqueName(const std::string &name, const std::set&existingNames) +static std::string makeUniqueName(const std::string &name, const std::set &existingNames) { if (!existingNames.contains(name)) { @@ -729,9 +736,11 @@ static std::string makeUniqueName(const std::string &name, const std::set &presets) { - if (bankIndex.selectedBank() == bankInstanceId) { + if (bankIndex.selectedBank() == bankInstanceId) + { throw std::runtime_error("Can't import to self."); } - std::set presetsSet { presets.begin(), presets.end()}; + std::set presetsSet{presets.begin(), presets.end()}; auto indexEntry = this->bankIndex.getBankIndexEntry(bankInstanceId); std::set existingNames; - for (auto&preset: this->currentBank.presets()) { + for (auto &preset : this->currentBank.presets()) + { existingNames.insert(preset->preset().name()); } BankFile bankFile; - LoadBankFile(indexEntry.name(),&bankFile); + LoadBankFile(indexEntry.name(), &bankFile); int64_t lastPresetId = -1; - for (auto &presetEntry: bankFile.presets()) { - if (presetsSet.contains(presetEntry->instanceId())) { - std::string uniqueName = makeUniqueName(presetEntry->preset().name(),existingNames); + for (auto &presetEntry : bankFile.presets()) + { + if (presetsSet.contains(presetEntry->instanceId())) + { + std::string uniqueName = makeUniqueName(presetEntry->preset().name(), existingNames); existingNames.insert(uniqueName); Pedalboard t = presetEntry->preset(); t.name(uniqueName); @@ -767,31 +780,35 @@ int64_t Storage::ImportPresetsFromBank(int64_t bankInstanceId, const std::vector } int64_t Storage::CopyPresetsToBank(int64_t bankInstanceId, const std::vector &presets) { - if (bankIndex.selectedBank() == bankInstanceId) { + if (bankIndex.selectedBank() == bankInstanceId) + { throw std::runtime_error("Can't copy to self."); } auto indexEntry = this->bankIndex.getBankIndexEntry(bankInstanceId); BankFile bankFile; - LoadBankFile(indexEntry.name(),&bankFile); + LoadBankFile(indexEntry.name(), &bankFile); - std::set presetsSet { presets.begin(), presets.end()}; + std::set presetsSet{presets.begin(), presets.end()}; std::set existingNames; - for (auto&preset: bankFile.presets()) { + for (auto &preset : bankFile.presets()) + { existingNames.insert(preset->preset().name()); } - for (auto &presetEntry: this->currentBank.presets()) { - if (presetsSet.contains(presetEntry->instanceId())) { - std::string uniqueName = makeUniqueName(presetEntry->preset().name(),existingNames); + for (auto &presetEntry : this->currentBank.presets()) + { + if (presetsSet.contains(presetEntry->instanceId())) + { + std::string uniqueName = makeUniqueName(presetEntry->preset().name(), existingNames); existingNames.insert(uniqueName); Pedalboard t = presetEntry->preset(); t.name(uniqueName); bankFile.addPreset(t); } } - SaveBankFile(indexEntry.name(),bankFile); + SaveBankFile(indexEntry.name(), bankFile); return -1; } @@ -801,17 +818,15 @@ std::vector Storage::RequestBankPresets(int64_t bankInstanceId std::vector result; BankFile bankFile; - LoadBankFile(indexEntry.name(),&bankFile); - for (auto &preset: bankFile.presets()) { + LoadBankFile(indexEntry.name(), &bankFile); + for (auto &preset : bankFile.presets()) + { result.push_back( - PresetIndexEntry(preset->instanceId(),preset->preset().name()) - ); + PresetIndexEntry(preset->instanceId(), preset->preset().name())); } return result; } - - void Storage::SetPresetIndex(const PresetIndex &presets) { // painful because we must move unique_ptrs. @@ -885,7 +900,8 @@ Pedalboard Storage::GetPreset(int64_t instanceId) const int64_t Storage::DeletePresets(const std::vector &presetInstanceIds) { int64_t newSelection = currentBank.selectedPreset(); - for (auto presetId: presetInstanceIds) { + for (auto presetId : presetInstanceIds) + { newSelection = currentBank.deletePreset(presetId); } SaveCurrentBank(); @@ -977,7 +993,7 @@ int64_t Storage::CreateNewPreset() } } newPedalboard.name(name); - auto t = this->currentBank.addPreset(newPedalboard, -1); + auto t = this->currentBank.addPreset(newPedalboard, -1); SaveCurrentBank(); return t; } @@ -997,7 +1013,7 @@ int64_t Storage::CopyPreset(int64_t fromId, int64_t toId) { auto &toItem = this->currentBank.getItem(toId); toItem.preset(fromItem.preset()); - result = toId; + result = toId; } SaveCurrentBank(); return result; @@ -2075,21 +2091,21 @@ static bool ensureNoDotDot(const std::filesystem::path &path) return true; } -static bool isInfoFile(const std::string&fileName) +static bool isInfoFile(const std::string &fileName) { - if (strcasecmp(fileName.c_str(),"license.txt") == 0) + if (strcasecmp(fileName.c_str(), "license.txt") == 0) { return true; } - if (strcasecmp(fileName.c_str(),"license.md") == 0) + if (strcasecmp(fileName.c_str(), "license.md") == 0) { return true; } - if (strcasecmp(fileName.c_str(),"readme.txt") == 0) + if (strcasecmp(fileName.c_str(), "readme.txt") == 0) { return true; } - if (strcasecmp(fileName.c_str(),"readme.md") == 0) + if (strcasecmp(fileName.c_str(), "readme.md") == 0) { return true; } @@ -3021,11 +3037,13 @@ std::string Storage::GetTone3000Auth() const std::filesystem::path Storage::FromAbstractPathString(const std::string &stringPath) { - if (stringPath.empty()) { + if (stringPath.empty()) + { return ""; } - fs::path path { stringPath}; - if (path.is_absolute()) { + fs::path path{stringPath}; + if (path.is_absolute()) + { return path; } return GetPluginUploadDirectory() / path; @@ -3048,6 +3066,125 @@ std::string Storage::FromAbstractPathJson(const std::string &pathJson) return v.to_string(); } +void Storage::SetChannelRouterSettings(ChannelRouterSettings::ptr settings) +{ + this->channelRouterSettings = settings; + this->channelSelection = ChannelSelection(*settings); + SaveChannelRouterSettings(settings); +} +ChannelRouterSettings::ptr Storage::GetChannelRouterSettings() +{ + return channelRouterSettings; +} + +std::filesystem::path Storage::GetChannelRouterSettingsPath() const +{ + return GetDataRoot() / "ChannelRouterSettings.json"; +} + +void Storage::SaveChannelRouterSettings(ChannelRouterSettings::ptr settings) +{ + std::ofstream os(GetChannelRouterSettingsPath()); + json_writer writer(os); + writer.write(settings); +} +ChannelRouterSettings::ptr Storage::LoadChannelRouterSettings() +{ + auto path = GetChannelRouterSettingsPath(); + if (!fs::exists(path)) + { + // See UpgradeChannelRouterSettings(), which will upgrade from old settings + // or create a default instance. + return nullptr; // will be upgraded later. + } + try { + std::ifstream is(path); + json_reader reader(is); + ChannelRouterSettings::ptr result; + reader.read(&result); + return result; + } catch (const std::exception &e) { + Lv2Log::error("Failed to load Channel Router settings: %s", e.what()); + return std::make_shared(); + } +} + +static int64_t GetUpgradedPortIndex(const std::string &portName) +{ + auto nPos = portName.find_last_of('_'); + if (nPos != std::string::npos) + { + std::string channelStr = portName.substr(nPos + 1); + try + { + int64_t channelIndex = std::stoll(channelStr); + return channelIndex; + } catch (const std::exception &) { + return -1; + } + } + return -1; +} +void Storage::UpgradeChannelRouterSettings() +{ + if (channelRouterSettings == nullptr) + { + channelRouterSettings = std::make_shared(); + if (jackChannelSelection.isValid()) + { + channelRouterSettings->configured(true); + channelRouterSettings->mainInputChannels().resize(2); + const std::vector& oldInputs = jackChannelSelection.GetInputAudioPorts(); + std::vector& newInputs = channelRouterSettings->mainInputChannels(); + + if (oldInputs.size() ==1) + { + int64_t leftIndex = GetUpgradedPortIndex(oldInputs[0]); + newInputs.resize(2); + newInputs[0] = leftIndex; + newInputs[1] = leftIndex; + } else if (oldInputs.size() == 2) { + newInputs.resize(2); + for (size_t i = 0; i < std::min(oldInputs.size(), newInputs.size()); ++i) + { + int64_t portIndex = GetUpgradedPortIndex(oldInputs[i]); + newInputs[i] = portIndex; + } + } + const std::vector& oldOutputs = jackChannelSelection.GetOutputAudioPorts(); + auto &newMainOutputs = channelRouterSettings->mainOutputChannels(); + if (oldOutputs.size() == 1) { + int64_t leftIndex = GetUpgradedPortIndex(oldOutputs[0]); + newMainOutputs.resize(2); + newMainOutputs[0] = leftIndex; + newMainOutputs[1] = leftIndex; + } else if (oldOutputs.size() == 2) { + newMainOutputs.resize(2); + for (size_t i = 0; i < std::min(oldOutputs.size(), newMainOutputs.size()); ++i) + { + int64_t portIndex = GetUpgradedPortIndex(oldOutputs[i]); + newMainOutputs[i] = portIndex; + } + } + + auto &newAuxInputs = channelRouterSettings->auxInputChannels(); + newAuxInputs.resize(2); + newAuxInputs[0] = -1; + newAuxInputs[1] = -1; + auto &newAuxOutputs = channelRouterSettings->auxOutputChannels(); + newAuxOutputs.resize(2); + newAuxOutputs[0] = -1; + newAuxOutputs[1] = -1; + SaveChannelRouterSettings(channelRouterSettings); + } + channelSelection = ChannelSelection(*channelRouterSettings); + } +} + +const ChannelSelection& Storage::GetChannelSelection() const { + return channelSelection; +} + JSON_MAP_BEGIN(UserSettings) JSON_MAP_REFERENCE(UserSettings, governor) diff --git a/src/Storage.hpp b/src/Storage.hpp index 21ae5ba..76b4199 100644 --- a/src/Storage.hpp +++ b/src/Storage.hpp @@ -32,6 +32,8 @@ #include #include "FilePropertyDirectoryTree.hpp" #include "AlsaSequencer.hpp" +#include "ChannelRouterSettings.hpp" + namespace pipedal { @@ -117,6 +119,15 @@ private: WifiDirectConfigSettings wifiDirectConfigSettings; UserSettings userSettings; + + ChannelRouterSettings::ptr channelRouterSettings; + ChannelSelection channelSelection; + + void UpgradeChannelRouterSettings(); + std::filesystem::path GetChannelRouterSettingsPath() const; + void SaveChannelRouterSettings(ChannelRouterSettings::ptr settings); + ChannelRouterSettings::ptr LoadChannelRouterSettings(); + public: Storage(); void Initialize(); @@ -124,8 +135,8 @@ public: void SetDataRoot(const std::filesystem::path& path); void SetConfigRoot(const std::filesystem::path& path); - const std::filesystem::path&GetConfigRoot(); - const std::filesystem::path&GetDataRoot(); + const std::filesystem::path&GetConfigRoot() const; + const std::filesystem::path&GetDataRoot() const; const std::filesystem::path &GetPluginUploadDirectory() const; @@ -206,6 +217,11 @@ public: void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration); AlsaSequencerConfiguration GetAlsaSequencerConfiguration() const; + void SetChannelRouterSettings(ChannelRouterSettings::ptr settings); + ChannelRouterSettings::ptr GetChannelRouterSettings(); + + const ChannelSelection& GetChannelSelection() const; + //std::string MapPropertyFileName(Lv2PluginInfo*pluginInfo, const std::string&path); diff --git a/todo.txt b/todo.txt index 8423926..d3b331d 100644 --- a/todo.txt +++ b/todo.txt @@ -1,3 +1,4 @@ +A way to force input to mono? ls diff --git a/vite/src/pipedal/ChannelMixerSettings.tsx b/vite/src/pipedal/ChannelMixerSettings.tsx deleted file mode 100644 index 983131f..0000000 --- a/vite/src/pipedal/ChannelMixerSettings.tsx +++ /dev/null @@ -1,55 +0,0 @@ - -import { ControlValue } from "../pipedal/Pedalboard.tsx"; - - -export class ChannelMixerStrip { - inputVu: number = -100.0; - inputVolume: number = 0; - outputVu: number = -100.0; - outputVolume: number = 0; -}; - -export default class ChannelMixerSettings { - configured: boolean = false; - - guitarInputChannels: number[] = [1,1]; - - guitarOutputChannels: number[] = [0,1]; - - guitarMixerSettings: ChannelMixerStrip = new ChannelMixerStrip(); - - - auxInputChannels: number[] = [0,0]; - auxOutputChannels: number[] = [-1,-1]; - - auxMixerSettings: ChannelMixerStrip = new ChannelMixerStrip(); - - controls: ControlValue[] = []; - // Inserts... - - getControlValue(symbol: string): number | null { - for (let control of this.controls) { - if (control.key === symbol) { - return control.value; - } - } - return null; - } - setControlValue(symbol: string, value: number): boolean { - for (let control of this.controls) { - if (control.key === symbol) { - if (control.value === value) { - return false; - } - control.value = value; - this.controls = this.controls.slice(); // trigger observers. - return true; - } - } - let newValue = new ControlValue(symbol,value); - this.controls.push(newValue); - this.controls = this.controls.slice(); // trigger observers. - return true; - } - -} \ No newline at end of file diff --git a/vite/src/pipedal/ChannelMixerSettingsDialog.tsx b/vite/src/pipedal/ChannelMixerSettingsDialog.tsx deleted file mode 100644 index 2d34295..0000000 --- a/vite/src/pipedal/ChannelMixerSettingsDialog.tsx +++ /dev/null @@ -1,524 +0,0 @@ -// Copyright (c) 2026 Robin 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 React from 'react'; -import { useState } from 'react'; -import HelpOutlineIcon from '@mui/icons-material/HelpOutline'; -import VuMeter from './VuMeter'; -import Toolbar from '@mui/material/Toolbar'; -import ArrowBackIcon from '@mui/icons-material/ArrowBack'; -import IconButtonEx from './IconButtonEx'; -import Button from '@mui/material/Button'; -import useWindowSize from "./UseWindowSize"; -import DialIcon from './svg/fx_dial.svg?react'; -import Select from '@mui/material/Select'; -import MenuItem from '@mui/material/MenuItem'; -import ChannelMixerSettingsHelpDialog from './ChannelMixerSettingsHelpDialog'; -import PluginControl from './PluginControl'; - -import DialogTitle from '@mui/material/DialogTitle'; -import DialogContent from '@mui/material/DialogContent'; -import Typography from '@mui/material/Typography'; -import ChannelMixerSettings from './ChannelMixerSettings'; -import { makeChanelMixerUiPlugin, CHANNEL_MIXER_INSTANCE_ID } from './ChannelMixerUiControl'; -import { ControlValue } from './Pedalboard'; - -import DialogEx from './DialogEx'; - -import { PiPedalModelFactory } from './PiPedalModel'; - -export interface ChannelMixerSettingsDialogProps { - open: boolean; - onClose: () => void; -} - -let channelMixerUiPlugin = makeChanelMixerUiPlugin(); - -enum RouteType { - Main, - Aux -} -let cellSectionHead: React.CSSProperties = { - border: "0px", - paddingTop: 4, - paddingBottom: 4, - paddingLeft: 0, - paddingRight: 12, - margin: "0px", - textAlign: "left", - verticalAlign: "middle" -}; - -let cellPortraitControlStrip: React.CSSProperties = { - border: "0px", - paddingTop: 12, - paddingBottom: 12, - paddingLeft: 0, - paddingRight: 0, - margin: "0px", - textAlign: "left", - verticalAlign: "middle" -}; -let cellLeft: React.CSSProperties = { - border: "0px", - paddingTop: 4, - paddingBottom: 4, - paddingLeft: 4, - paddingRight: 4, - margin: "0px", - textAlign: "left", - verticalAlign: "middle" -}; -let cellLeftIndent: React.CSSProperties = { - border: "0px", - paddingTop: 4, - paddingBottom: 4, - paddingLeft: 12, - paddingRight: 4, - margin: "0px", - textAlign: "right", - verticalAlign: "middle" -}; -let cellLeftH: React.CSSProperties = { - border: "0px", - paddingTop: 4, - paddingBottom: 4, - paddingLeft: 4, - paddingRight: 4, - margin: "0px", - textAlign: "left", - verticalAlign: "middle" -}; - - -function MakeChannelMenu(channelCount: number): React.ReactElement[] { - let items: React.ReactElement[] = [(None)] - if (channelCount === 0) { - return items; - } - if (channelCount === 2) { - items.push((Left)); - items.push((Right)); - return items; - } - for (let i = 0; i < channelCount; ++i) { - items.push((Ch {i + 1})); - } - return items; -} - - - -function ChannelMixerSettingsDialog(props: ChannelMixerSettingsDialogProps) { - //const classes = useStyles(); - const { open, onClose } = props; - let model = PiPedalModelFactory.getInstance(); - let config = model.jackConfiguration.get(); - - - const [settings, setSettings] = useState(model.channelMixerSettings.get()); - const [controlValues,setControlValues] = useState(model.channelMixerControlValues.get()); - const [showHelp, setShowHelp] = useState(false); - - if (settings) { - } - if (controlValues) { - - } - - - React.useEffect(() => { - if (open) { - let handleSettingsChanged = () => { - setSettings(model.channelMixerSettings.get()); - }; - model.channelMixerSettings.addOnChangedHandler(handleSettingsChanged); - let handleControlsChanged = () => { - setControlValues(model.channelMixerControlValues.get()); - }; - model.channelMixerControlValues.addOnChangedHandler(handleControlsChanged); - return () => { - model.channelMixerSettings.removeOnChangedHandler(handleSettingsChanged); - model.channelMixerControlValues.removeOnChangedHandler(handleControlsChanged); - } - } else { - return () => { }; - } - }, [open]); - - const handleClose = (): void => { - onClose(); - }; - let [windowSize] = useWindowSize(); - let landscape = windowSize.width > windowSize.height; - let fullScreen = windowSize.width < 450 || windowSize.height < 600; - - let Dial = (symbol: string) => { - let instanceId = CHANNEL_MIXER_INSTANCE_ID; - let control = channelMixerUiPlugin.getControl(symbol); - let value = settings.getControlValue(symbol); - if (value === null) { - value = control?.default_value ?? 0; - } - - return ( { - model.previewPedalboardValue(instanceId, symbol, value); - }} - onChange={(value: number) => { - model.setPedalboardControl(instanceId, symbol, value); - }} - requestIMEEdit={() => { }} - value={value} - />); - - return ; - } - let ChannelSelect = (routeType: RouteType, channelIndex: number, input: boolean) => { - let channelCount = input ? config.inputAudioPorts.length : config.outputAudioPorts.length; - - let value: number; - - switch (routeType) { - case RouteType.Main: - value = input ? settings.guitarInputChannels[channelIndex] : settings.guitarOutputChannels[channelIndex]; - break; - case RouteType.Aux: - value = input ? settings.auxInputChannels[channelIndex] : settings.auxOutputChannels[channelIndex]; - break; - } - if (value >= channelCount) { - value = -1; - } - return ( - - ) - } - let Vu = () => { - return ( -
- -
- ); - } - - let LandscapeView = () => { - return ( -
- - - - - - - - - - - - - - - - - - - - - - - - - - {/* Main */} - - - - - - - - - - - - - - - - - - {/* Spacer */} - - - {/* Aux */} - - - - - - - - - - - - - - - - - - -
InOut
- Main - - {ChannelSelect(RouteType.Main, 0, true)} - {Dial("mainIn")}{Vu()} - - {Dial("mainOut")}{Vu()} - {ChannelSelect(RouteType.Main, 0, false)} -
- {(ChannelSelect(RouteType.Main, 1, true))} - - {(ChannelSelect(RouteType.Main, 1, false))} -
- Aux - - {(ChannelSelect(RouteType.Aux, 0, true))} - {Dial("auxIn")}{Vu()} - - {Dial("auxOut")}{Vu()} - {(ChannelSelect(RouteType.Aux, 0, false))} -
- {(ChannelSelect(RouteType.Aux, 1, true))} - - {(ChannelSelect(RouteType.Aux, 1, false))} -
-
- ); - }; - let PortraitView = () => { - return ( -
- - - - - - - - - {/* Main */} - - - - - - - - - - - - - - - - - - - - - {/* Spacer */} - - - {/* Aux */} - - - - - - - - - - - - - - - - - - - - -
- Main -
- In - - {ChannelSelect(RouteType.Main, 0, true)} - - {ChannelSelect(RouteType.Main, 1, true)} -
- - - - - - - - - - - - -
{Dial("mainIn")}{Vu()} -
- -
-
{Dial("mainOut")}{Vu()}
-
- Out - - {ChannelSelect(RouteType.Main, 0, false)} - - {ChannelSelect(RouteType.Main, 1, false)} -
- Aux -
- In - - {(ChannelSelect(RouteType.Aux, 0, true))} - - {(ChannelSelect(RouteType.Aux, 1, true))} -
- - - - - - - - - - - - -
{Dial("auxIn")}{Vu()} - - {Dial("auxOut")}{Vu()}
-
- Out - - {ChannelSelect(RouteType.Aux, 0, false)} - - {ChannelSelect(RouteType.Aux, 1, false)} -
-
- ); - }; - - return ( - - - - { onClose(); }} - > - - - - - Channel Routing - - - { setShowHelp(true); }} - > - - - - - - - - {landscape ? - LandscapeView() - : - PortraitView() - } - - {showHelp && ( - setShowHelp(false)} /> - )} - - ); -} - -export default ChannelMixerSettingsDialog; diff --git a/vite/src/pipedal/ChannelRouterSettings.tsx b/vite/src/pipedal/ChannelRouterSettings.tsx new file mode 100644 index 0000000..bea7ef7 --- /dev/null +++ b/vite/src/pipedal/ChannelRouterSettings.tsx @@ -0,0 +1,64 @@ + +import { Pedalboard, ControlValue } from "./Pedalboard.tsx"; + + + + +export default class ChannelRouterSettings { + configured: boolean = false; + channelRouterPresetId: number = -1; + + mainInputChannels: number[] = [1,1]; + mainOutputChannels: number[] = [0,1]; + mainInserts: Pedalboard = new Pedalboard(); + + auxInputChannels: number[] = [0,0]; + auxOutputChannels: number[] = [-1,-1]; + auxInserts: Pedalboard = new Pedalboard(); + + sendInputChannels: number[] = [-1,-1]; + sendOutputChannels: number[] = [-1,-1]; + + controlValues: ControlValue[] = []; + // Inserts... + + deserialize(obj: any): ChannelRouterSettings { + this.configured = obj.configured; + this.channelRouterPresetId = obj.channelRouterPresetId ? obj.channelRouterPresetId : -1; + this.mainInputChannels = obj.mainInputChannels.slice(); + this.mainOutputChannels = obj.mainOutputChannels.slice(); + this.mainInserts = new Pedalboard().deserialize(obj.mainInserts); + this.auxInputChannels = obj.auxInputChannels.slice(); + this.auxOutputChannels = obj.auxOutputChannels.slice(); + this.auxInserts = new Pedalboard().deserialize(obj.auxInserts); + this.sendInputChannels = obj.sendInputChannels.slice(); + this.sendOutputChannels = obj.sendOutputChannels.slice(); + this.controlValues = ControlValue.deserializeArray(obj.controlValues); + return this; + } + getControlValue(symbol: string): number | null { + for (let control of this.controlValues) { + if (control.key === symbol) { + return control.value; + } + } + return null; + } + setControlValue(symbol: string, value: number): boolean { + for (let control of this.controlValues) { + if (control.key === symbol) { + if (control.value === value) { + return false; + } + control.value = value; + this.controlValues = this.controlValues.slice(); // trigger observers. + return true; + } + } + let newValue = new ControlValue(symbol,value); + this.controlValues.push(newValue); + this.controlValues = this.controlValues.slice(); // trigger observers. + return true; + } + +} \ No newline at end of file diff --git a/vite/src/pipedal/ChannelRouterSettingsDialog.tsx b/vite/src/pipedal/ChannelRouterSettingsDialog.tsx new file mode 100644 index 0000000..5fbb414 --- /dev/null +++ b/vite/src/pipedal/ChannelRouterSettingsDialog.tsx @@ -0,0 +1,853 @@ +// Copyright (c) 2026 Robin 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 React from 'react'; +import { useState } from 'react'; +import HelpOutlineIcon from '@mui/icons-material/HelpOutline'; +import Divider from '@mui/material/Divider'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import IconButtonEx from './IconButtonEx'; +import Button from '@mui/material/Button'; +import useWindowSize from "./UseWindowSize"; +import Select from '@mui/material/Select'; +import MenuItem from '@mui/material/MenuItem'; +import ChannelRouterSettingsHelpDialog from './ChannelRouterSettingsHelpDialog'; +import SpeakerIcon from '@mui/icons-material/Speaker'; +import MicIcon from '@mui/icons-material/Mic'; + +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import Typography from '@mui/material/Typography'; +import ChannelRouterSettings from './ChannelRouterSettings'; +import { Pedalboard, ControlValue } from './Pedalboard'; + +import DialogEx from './DialogEx'; + +import { PiPedalModelFactory } from './PiPedalModel'; + +export interface ChannelRouterSettingsDialogProps { + open: boolean; + onClose: () => void; +} + +enum RouteType { + Main, + Aux, + Send +} + +interface ChannelRouterSettingsPreset { + id: number; + name: string; + settings: ChannelRouterSettings; +} + +let LEFT_LEFT_PRESET_ID = -2; +let RIGHT_STEREO_PRESET_ID = -3; + +let channelRouterPresets: ChannelRouterSettingsPreset[] = [ + { + id: -2, name: "Left -> Left", settings: new ChannelRouterSettings().deserialize({ + configured: true, + channelRouterPresetId: -2, + mainInputChannels: [0, 0], + mainOutputChannels: [0, 0], + mainInserts: new Pedalboard(), + auxInputChannels: [-1, -1], + auxOutputChannels: [-1, -1], + auxInserts: new Pedalboard(), + sendInputChannels: [-1, -1], + sendOutputChannels: [-1, -1], + controlValues: [], + }) + }, + { + id: -3, name: "Left -> Stereo", settings: new ChannelRouterSettings().deserialize({ + configured: true, + channelRouterPresetId: -3, + mainInputChannels: [0, 0], + mainOutputChannels: [0, 1], + mainInserts: new Pedalboard(), + auxInputChannels: [-1, -1], + auxOutputChannels: [-1, -1], + auxInserts: new Pedalboard(), + sendInputChannels: [-1, -1], + sendOutputChannels: [-1, -1], + controlValues: [], + }) + }, + { + id: -4, name: "Right -> Stereo", settings: new ChannelRouterSettings().deserialize({ + configured: true, + channelRouterPresetId: -4, + mainInputChannels: [1, 1], + mainOutputChannels: [0, 1], + mainInserts: new Pedalboard(), + auxInputChannels: [-1, -1], + auxOutputChannels: [-1, -1], + auxInserts: new Pedalboard(), + sendInputChannels: [-1, -1], + sendOutputChannels: [-1, -1], + controlValues: [], + }) + }, + { + id: -5, name: "Right -> Left + re-amp", settings: new ChannelRouterSettings().deserialize({ + configured: true, + channelRouterPresetId: -5, + mainInputChannels: [1, 1], + mainOutputChannels: [0, 0], + mainInserts: new Pedalboard(), + auxInputChannels: [1, 1], + auxOutputChannels: [1, 1], + auxInserts: new Pedalboard(), + sendInputChannels: [-1, - 1], + sendOutputChannels: [-1, -1], + controlValues: [], + }) + }, + { + id: -6, name: "Left -> Left + send", settings: new ChannelRouterSettings().deserialize({ + configured: true, + channelRouterPresetId: -6, + mainInputChannels: [0, 0], + mainOutputChannels: [0, 0], + mainInserts: new Pedalboard(), + auxInputChannels: [-1, -1], + auxOutputChannels: [-1, - 1], + auxInserts: new Pedalboard(), + sendInputChannels: [1, 1], + sendOutputChannels: [1, 1], + controlValues: [], + }) + }, + { + id: -7, name: "Right -> Right + send", settings: new ChannelRouterSettings().deserialize({ + configured: true, + channelRouterPresetId: -7, + mainInputChannels: [1, 1], + mainOutputChannels: [1, 1], + mainInserts: new Pedalboard(), + auxInputChannels: [-1, -1], + auxOutputChannels: [-1, - 1], + auxInserts: new Pedalboard(), + sendInputChannels: [0, 0], + sendOutputChannels: [0, 0], + controlValues: [], + }) + }, + { + id: -8, name: "Left -> Stereo + re-amp", settings: new ChannelRouterSettings().deserialize({ + configured: true, + channelRouterPresetId: -8, + mainInputChannels: [0, 0], + mainOutputChannels: [0, 1], + mainInserts: new Pedalboard(), + auxInputChannels: [0, 0], + auxOutputChannels: [2, 2], + auxInserts: new Pedalboard(), + sendInputChannels: [-1, -1], + sendOutputChannels: [-1, -1], + controlValues: [], + + }) + }, + { + id: -9, name: "Left -> Stereo + stereo send", settings: new ChannelRouterSettings().deserialize({ + configured: true, + channelRouterPresetId: -9, + mainInputChannels: [0, 0], + mainOutputChannels: [0, 1], + mainInserts: new Pedalboard(), + auxInputChannels: [-1, -1], + auxOutputChannels: [-1, - 1], + auxInserts: new Pedalboard(), + sendInputChannels: [2, 3], + sendOutputChannels: [2, 3], + controlValues: [], + + }) + }, +]; + +let cellPortraitInOutDiv: React.CSSProperties = { + width: 80, display: "flex", columnGap: 4, flexDirection: "row", alignItems: "center", justifyContent: "right", +}; +let cellPortraitSectionHead: React.CSSProperties = { + border: "0px", + paddingTop: 4, + paddingBottom: 4, + paddingLeft: 0, + paddingRight: 12, + width: 50, + margin: "0px", + textAlign: "left", + verticalAlign: "middle", +}; + +let cellPortraitControlStrip: React.CSSProperties = { + border: "0px", + paddingTop: 12, + paddingBottom: 12, + paddingLeft: 0, + paddingRight: 0, + margin: "0px", + textAlign: "left", + verticalAlign: "middle" +}; +let cellLeft: React.CSSProperties = { + border: "0px", + paddingTop: 4, + paddingBottom: 4, + paddingLeft: 4, + paddingRight: 12, + margin: "0px", + textAlign: "left", + whiteSpace: "nowrap", + verticalAlign: "middle" +}; +let cellLandscapeTitle: React.CSSProperties = { + border: "0px", + paddingTop: 4, + paddingBottom: 4, + paddingLeft: 4, + paddingRight: 20, + margin: "0px", + textAlign: "left", + verticalAlign: "middle" +}; +let cellPortraitInOut: React.CSSProperties = { + border: "0px", + paddingTop: 4, + paddingBottom: 4, + paddingLeft: 12, + paddingRight: 12, + margin: "0px", + width: 80, + textAlign: "right", + verticalAlign: "middle" +}; + +function MakeChannelMenu(channelCount: number): React.ReactElement[] { + let items: React.ReactElement[] = [(None)] + if (channelCount === 0) { + return items; + } + if (channelCount === 2) { + items.push((Left)); + items.push((Right)); + return items; + } + for (let i = 0; i < channelCount; ++i) { + items.push((Ch {i + 1})); + } + return items; +} + + + +function ChannelRouterSettingsDialog(props: ChannelRouterSettingsDialogProps) { + //const classes = useStyles(); + const { open, onClose } = props; + let model = PiPedalModelFactory.getInstance(); + let config = model.jackConfiguration.get(); + + + const [settings, setSettings] = useState(model.channelRouterSettings.get()); + const [controlValues, setControlValues] = useState(model.channelRouterControlValues.get()); + const [showHelp, setShowHelp] = useState(false); + + if (settings) { + } + if (controlValues) { + + } + + + React.useEffect(() => { + if (open) { + let handleSettingsChanged = () => { + setSettings(model.channelRouterSettings.get()); + }; + model.channelRouterSettings.addOnChangedHandler(handleSettingsChanged); + let handleControlsChanged = () => { + setControlValues(model.channelRouterControlValues.get()); + }; + model.channelRouterControlValues.addOnChangedHandler(handleControlsChanged); + return () => { + model.channelRouterSettings.removeOnChangedHandler(handleSettingsChanged); + model.channelRouterControlValues.removeOnChangedHandler(handleControlsChanged); + } + } else { + return () => { }; + } + }, [open]); + + const handleClose = (): void => { + onClose(); + }; + let [windowSize] = useWindowSize(); + let landscape = windowSize.width > windowSize.height; + let fullScreen = windowSize.width < 450 || windowSize.height < 600; + + let ChannelSelect = (routeType: RouteType, channelIndex: number, input: boolean, icon: boolean = true) => { + let channelCount = input ? config.inputAudioPorts.length : config.outputAudioPorts.length; + + let value: number; + + switch (routeType) { + case RouteType.Main: + value = input ? settings.mainInputChannels[channelIndex] : settings.mainOutputChannels[channelIndex]; + break; + case RouteType.Aux: + value = input ? settings.auxInputChannels[channelIndex] : settings.auxOutputChannels[channelIndex]; + break; + case RouteType.Send: + value = input ? settings.sendInputChannels[channelIndex] : settings.sendOutputChannels[channelIndex]; + break; + } + if (value >= channelCount) { + value = -1; + } + return ( + + ) + } + let Vu = () => { + return ( +
+
+ {/* */} +
+ ); + } + + let ApplyPresetId = (presetId: number | string | null | undefined) => { + if (typeof presetId !== 'number') { + return; + } + for (let preset of channelRouterPresets) { + if (preset.id === presetId) { + model.setChannelRouterSettings(preset.settings); + return; + } + } + } + + let CanUsePreset = (preset: ChannelRouterSettingsPreset): boolean => { + let inputChannels = config.inputAudioPorts.length; + let outputChannels = config.outputAudioPorts.length; + return ( + (preset.settings.mainInputChannels.every((ch) => ch < inputChannels || ch === -1)) && + (preset.settings.mainOutputChannels.every((ch) => ch < outputChannels || ch === -1)) && + (preset.settings.auxInputChannels.every((ch) => ch < inputChannels || ch === -1)) && + (preset.settings.auxOutputChannels.every((ch) => ch < outputChannels || ch === -1)) && + (preset.settings.sendInputChannels.every((ch) => ch < inputChannels || ch === -1)) && + (preset.settings.sendOutputChannels.every((ch) => ch < outputChannels || ch === -1)) + ); + } + + let GeneratePresetMenuItems = (): React.ReactElement[] => { + let items: React.ReactElement[] = []; + + for (let preset of channelRouterPresets) { + if (CanUsePreset(preset)) + items.push( + + {preset.name} + + ); + } + if (settings.channelRouterPresetId === -1) { + items.push( + ( + (Custom)) + ) + } + + return items; + } + + let PresetSelect = (width: number | string | undefined) => { + return ( +
+ + + { }} /> + +
+ ) + } + + + let LandscapeView = () => { + return ( +
+ + + + + + + + + + + + + + {/* Main */} + + + + + + + + + + + + + + + + {/* Spacer ---------------- */} + + + {/* Aux ----------------------*/} + + + + + + + + + + + + + + + + {/* Spacer ---------------- */} + + + {/* Send ---------------- */} + + + + + + + + + + + + + + + +
+ Main + + {ChannelSelect(RouteType.Main, 0, true)} + {Vu()} + + {Vu()} + {ChannelSelect(RouteType.Main, 0, false)} +
+ {(ChannelSelect(RouteType.Main, 1, true))} + + {(ChannelSelect(RouteType.Main, 1, false))} +
+ Aux + + {(ChannelSelect(RouteType.Aux, 0, true))} + {Vu()} + + {Vu()} + {(ChannelSelect(RouteType.Aux, 0, false))} +
+ {(ChannelSelect(RouteType.Aux, 1, true))} + + {(ChannelSelect(RouteType.Aux, 1, false))} +
+ Send + + {(ChannelSelect(RouteType.Send, 0, false))} + {Vu()} + {/* No button */} + {Vu()} + {(ChannelSelect(RouteType.Send, 0, true))} +
+ {(ChannelSelect(RouteType.Send, 1, false))} + + {(ChannelSelect(RouteType.Send, 1, true))} +
+
+ ); + }; + let PortraitView = () => { + return ( +
+ + + + + + + + + + {/* Main */} + + + + + + + + + + + + + + + + + + + + + + + {/* Spacer ---------------- */} + + {/* Aux */} + + + + + + + + + + + + + + + + + + + + + + {/* Spacer ---------------- */} + + + {/* Send */} + + + + + + + + + + + + + + + + + + + + + + +
+ Main + + +
+
+ In + +
+
+ {ChannelSelect(RouteType.Main, 0, true, false)} + + {ChannelSelect(RouteType.Main, 1, true, false)} +
+ + + + + + + + + + +
{Vu()} +
+ +
+
{Vu()}
+
+
+ Out + +
+
+ + {ChannelSelect(RouteType.Main, 0, false, false)} + + {ChannelSelect(RouteType.Main, 1, false, false)} +
+ Aux +
+
+ In + +
+
+ {(ChannelSelect(RouteType.Aux, 0, true, false))} + + {(ChannelSelect(RouteType.Aux, 1, true, false))} +
+ + + + + + + + + + +
{Vu()} +
+ +
+
{Vu()}
+
+
+ Out + +
+
+ {ChannelSelect(RouteType.Aux, 0, false, false)} + + {ChannelSelect(RouteType.Aux, 1, false, false)} +
+ Send +
+
+ Send + +
+
+ {(ChannelSelect(RouteType.Send, 0, false, false))} + + {(ChannelSelect(RouteType.Send, 1, false, false))} +
+ + + + + + + + + + +
{Vu()} +
+ +
+
{Vu()}
+
+
+ Return + +
+
+ {ChannelSelect(RouteType.Send, 0, true, false)} + + {ChannelSelect(RouteType.Send, 1, false, false)} +
+
+ ); + }; + + return ( + + +
+ { onClose(); }} + > + + + + + Channel Routing + + { + landscape ? ( + <> +
 
+
+ {PresetSelect(200)} +
+
 
+ + + ) : ( +
 
+ ) + } + + { setShowHelp(true); }} + > + + + +
+
+ {!landscape && ( + <> +
+ {PresetSelect("60%")} +
+ + + )} + + + {landscape ? + LandscapeView() + : + PortraitView() + } + + { + showHelp && ( + setShowHelp(false)} /> + ) + } +
+ ); +} + +export default ChannelRouterSettingsDialog; diff --git a/vite/src/pipedal/ChannelMixerSettingsHelpDialog.tsx b/vite/src/pipedal/ChannelRouterSettingsHelpDialog.tsx similarity index 80% rename from vite/src/pipedal/ChannelMixerSettingsHelpDialog.tsx rename to vite/src/pipedal/ChannelRouterSettingsHelpDialog.tsx index 345737b..f2d98c2 100644 --- a/vite/src/pipedal/ChannelMixerSettingsHelpDialog.tsx +++ b/vite/src/pipedal/ChannelRouterSettingsHelpDialog.tsx @@ -18,33 +18,30 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import React from 'react'; -import Button from '@mui/material/Button'; import DialogEx from './DialogEx'; -import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import Typography from '@mui/material/Typography'; import ResizeResponsiveComponent from './ResizeResponsiveComponent'; -import Divider from '@mui/material/Divider'; import DialogTitle from '@mui/material/DialogTitle'; import Toolbar from '@mui/material/Toolbar'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import IconButtonEx from './IconButtonEx'; -export interface ChannelMixerSettingsHelpDialogProps { +export interface ChannelRouterSettingsHelpDialogProps { open: boolean, onClose: () => void, }; -export interface ChannelMixerSettingsHelpDialogState { +export interface ChannelRouterSettingsHelpDialogState { fullScreen: boolean; }; -export default class ChannelMixerSettingsHelpDialog extends ResizeResponsiveComponent { +export default class ChannelRouterSettingsHelpDialog extends ResizeResponsiveComponent { refText: React.RefObject; - constructor(props: ChannelMixerSettingsHelpDialogProps) { + constructor(props: ChannelRouterSettingsHelpDialogProps) { super(props); this.state = { fullScreen: false @@ -137,6 +134,16 @@ export default class ChannelMixerSettingsHelpDialog extends ResizeResponsiveComp channels to both the Main and Aux routes. If you have a 2x4 or 4x4 audio interface with two guitar input channels you can also pass through stereo dry signals by configuring Main and Aux input and output channels appropriately.

+

The Send route, in conjunction with the TooB Send plugin allows you to send and return audio signals to an external physical device + from within a PiPedal preset. This allows you to integrate external hardware effects into your PiPedal presets. + Connect a cable from the audio interface output Send channel(s) selected here to the input of your + hardware effect, and a cable from the output of your hardware effect to the audio interface's Return channel + assigned here. Then insert the TooB Send plugin into your PiPedal preset. The input of the + TooB Send plugin will be sent to the external hardware effect, and the output the external hardware + effect will be returned to PiPedal at the output of the TooB Send plugin. PiPedal currently supports + only one Send plugin instance per preset; and the Send and Return channels may not be shared with the Main + or Aux routes. +

diff --git a/vite/src/pipedal/ChannelMixerUiControl.tsx b/vite/src/pipedal/ChannelRouterUiControl.tsx similarity index 97% rename from vite/src/pipedal/ChannelMixerUiControl.tsx rename to vite/src/pipedal/ChannelRouterUiControl.tsx index 731a3f3..186e891 100644 --- a/vite/src/pipedal/ChannelMixerUiControl.tsx +++ b/vite/src/pipedal/ChannelRouterUiControl.tsx @@ -19,13 +19,12 @@ import Units from './Units'; import {UiPlugin, UiControl, PluginType, ControlType, ScalePoint} from './Lv2Plugin'; -export const ChannelMixerUiUri = "uri://two-play/pipedal/ppChannelMixer"; -export const CHANNEL_MIXER_INSTANCE_ID = -4; +export const ChannelRouterUiUri = "uri://two-play/pipedal/ppChannelRouter"; export function makeChanelMixerUiPlugin(): UiPlugin { return new UiPlugin().deserialize({ - uri: ChannelMixerUiUri, + uri: ChannelRouterUiUri, name: "Channel Mixer", brand: "", label: "", diff --git a/vite/src/pipedal/Pedalboard.tsx b/vite/src/pipedal/Pedalboard.tsx index 31ae610..c96f0d0 100644 --- a/vite/src/pipedal/Pedalboard.tsx +++ b/vite/src/pipedal/Pedalboard.tsx @@ -357,14 +357,36 @@ export class PedalboardSplitItem extends PedalboardItem { } +let TWO_POWER_52 = 4503599627370496; // Maximum Javascript integer value. +let TWO_POWER_48 = 281474976710656; // Number of possible instance IDs for Main Pedalboards. +let MAX_INSTANCE_ID = TWO_POWER_48; +export enum InstanceType { + MainPedalboard, + MainInsert, + AuxInsert +} + + export class Pedalboard implements Deserializable { + static readonly CHANNEL_ROUTER_CONTROLS_INSTANCE_ID = -4; // Reserved Instance ID for Router Main Inserts. + static readonly CHANNEL_ROUTER_MAIN_INSERT_ID = -5; // Reserved Instance ID for Router Main Inserts. + static readonly CHANNEL_ROUTER_AUX_INSERT_ID = -6; // Reserved Instance ID for Router Aux inserts. + + static readonly MAIN_INSERT_INSTANCE_BASE = TWO_POWER_52-2*MAX_INSTANCE_ID; + static readonly AUX_INSERT_INSTANCE_BASE = TWO_POWER_52-1*MAX_INSTANCE_ID; + static readonly START_CONTROL = -2; // synthetic PedalboardItem for input volume. static readonly END_CONTROL = -3; // synthetic PedalboardItem for output volume. - static readonly CHANNEL_MIXER_INSTANCE_ID = -4; // Synthetic instance ID for the channel routing mixer. + static readonly MAIN_INSERT_START_CONTROL_ID = Pedalboard.MAIN_INSERT_INSTANCE_BASE + MAX_INSTANCE_ID -2; + static readonly MAIN_INSERT_END_CONTROL_ID = Pedalboard.MAIN_INSERT_INSTANCE_BASE + MAX_INSTANCE_ID -3; + static readonly AUX_INSERT_START_CONTROL_ID = Pedalboard.AUX_INSERT_INSTANCE_BASE + MAX_INSTANCE_ID -2; + static readonly AUX_INSERT_END_CONTROL_ID = Pedalboard.AUX_INSERT_INSTANCE_BASE + MAX_INSTANCE_ID -3; + + static readonly START_PEDALBOARD_ITEM_URI = "uri://two-play/pipedal/pedalboard#Start"; static readonly END_PEDALBOARD_ITEM_URI = "uri://two-play/pipedal/pedalboard#End"; @@ -373,7 +395,7 @@ export class Pedalboard implements Deserializable { this.input_volume_db = input.input_volume_db; this.output_volume_db = input.output_volume_db; this.items = PedalboardItem.deserializeArray(input.items); - this.nextInstanceId = input.nextInstanceId ?? -1; + this.nextInstanceId = input.nextInstanceId ?? 0; this.snapshots = input.snapshots ? Snapshot.deserializeArray(input.snapshots): []; this.selectedSnapshot = input.selectedSnapshot; this.pathProperties = input.pathProperties; @@ -381,6 +403,25 @@ export class Pedalboard implements Deserializable { return this; } + static getInstanceTypeFromInstanceId(instanceId: number): InstanceType { + if (instanceId >= Pedalboard.MAIN_INSERT_INSTANCE_BASE) { + return InstanceType.MainInsert; + } + if (instanceId >= Pedalboard.AUX_INSERT_INSTANCE_BASE) { + return InstanceType.AuxInsert; + } + return InstanceType.MainPedalboard; + } + getInstanceType(): InstanceType { + if (this.nextInstanceId >= Pedalboard.MAIN_INSERT_INSTANCE_BASE) { + return InstanceType.MainInsert; + } + if (this.nextInstanceId >= Pedalboard.AUX_INSERT_INSTANCE_BASE) { + return InstanceType.AuxInsert; + } + return InstanceType.MainPedalboard; + } + clone(): Pedalboard { return new Pedalboard().deserialize(this); } diff --git a/vite/src/pipedal/PiPedalModel.tsx b/vite/src/pipedal/PiPedalModel.tsx index 083603e..569a2e3 100644 --- a/vite/src/pipedal/PiPedalModel.tsx +++ b/vite/src/pipedal/PiPedalModel.tsx @@ -46,7 +46,7 @@ import AudioFileMetadata from './AudioFileMetadata'; import { pathFileName } from './FileUtils'; import { AlsaSequencerConfiguration, AlsaSequencerPortSelection } from './AlsaSequencer'; import { getDefaultModGuiPreference } from './ModGuiHost'; -import ChannelMixerSettings from './ChannelMixerSettings'; +import ChannelRouterSettings from './ChannelRouterSettings'; export enum State { Loading, @@ -530,13 +530,9 @@ export class PiPedalModel //implements PiPedalModel jackServerSettings: ObservableProperty = new ObservableProperty(new JackServerSettings()); - channelMixerControlValues: ObservableProperty = new ObservableProperty([]); - channelMixerSettings: ObservableProperty = new ObservableProperty(new ChannelMixerSettings()); + channelRouterControlValues: ObservableProperty = new ObservableProperty([]); + channelRouterSettings: ObservableProperty = new ObservableProperty(new ChannelRouterSettings()); - setChannelMixerSettings(settings: ChannelMixerSettings) { - this.channelMixerSettings.set(settings); - this.channelMixerControlValues.set(settings.controls); - } wifiConfigSettings: ObservableProperty = new ObservableProperty(new WifiConfigSettings()); wifiDirectConfigSettings: ObservableProperty = new ObservableProperty(new WifiDirectConfigSettings()); governorSettings: ObservableProperty = new ObservableProperty(new GovernorSettings()); @@ -1255,6 +1251,11 @@ export class PiPedalModel //implements PiPedalModel await this.getWebSocket().request("getJackServerSettings") ) ); + this.channelRouterSettings.set( + new ChannelRouterSettings().deserialize( + await this.getWebSocket().request("getChannelRouterSettings") + ) + ) this.jackConfiguration.set(new JackConfiguration().deserialize( await this.getWebSocket().request("getJackConfiguration") )); @@ -1651,15 +1652,15 @@ export class PiPedalModel //implements PiPedalModel private lastControlMessageWasSentbyMe = false; - private _setChannelMixerControlValue(key: string, value: number, notifyServer: boolean) : void { - let channelMixerSettings = this.channelMixerSettings.get(); - let changed = channelMixerSettings.setControlValue(key, value); + private _setChannelRouterControlValue(key: string, value: number, notifyServer: boolean) : void { + let channelRouterSettings = this.channelRouterSettings.get(); + let changed = channelRouterSettings.setControlValue(key, value); if (changed) { - this.channelMixerControlValues.set(this.channelMixerSettings.get().controls); + this.channelRouterControlValues.set(this.channelRouterSettings.get().controlValues); if (notifyServer) { this.lastControlMessageWasSentbyMe = true; - this._setServerControl("setControl", Pedalboard.CHANNEL_MIXER_INSTANCE_ID, key, value); + this._setServerControl("setControl", Pedalboard.CHANNEL_ROUTER_CONTROLS_INSTANCE_ID, key, value); } } } @@ -1667,8 +1668,8 @@ export class PiPedalModel //implements PiPedalModel let pedalboard = this.pedalboard.get(); if (pedalboard === undefined) throw new PiPedalStateError("Pedalboard not ready."); - if (instanceId == Pedalboard.CHANNEL_MIXER_INSTANCE_ID) { - this._setChannelMixerControlValue(key,value,notifyServer); + if (instanceId == Pedalboard.CHANNEL_ROUTER_CONTROLS_INSTANCE_ID) { + this._setChannelRouterControlValue(key,value,notifyServer); return; } else { let changed: boolean; @@ -2316,12 +2317,6 @@ export class PiPedalModel //implements PiPedalModel } - setJackSettings(jackSettings: JackChannelSelection): void { - this.expectDisconnect(ReconnectReason.LoadingSettings); - this.webSocket?.send("setJackSettings", jackSettings); - } - - monitorPortSubscriptions: MonitorPortHandleImpl[] = []; monitorPort(instanceId: number, key: string, updateRateSeconds: number, onUpdated: (value: number) => void): MonitorPortHandle { @@ -3310,7 +3305,6 @@ export class PiPedalModel //implements PiPedalModel if (this.webSocket) { this.webSocket.send("setFavorites", newFavorites); } - // stub: update server. } @@ -3680,6 +3674,15 @@ export class PiPedalModel //implements PiPedalModel } this.removeInvalidSidechains(pedalboard); } + + setChannelRouterSettings(settings: ChannelRouterSettings) { + this.channelRouterSettings.set(settings); + this.channelRouterControlValues.set(settings.controlValues); + if (this.webSocket) { + this.webSocket.send("setChannelRouterSettings", settings); + } + } + }; let instance: PiPedalModel | undefined = undefined; diff --git a/vite/src/pipedal/SettingsDialog.tsx b/vite/src/pipedal/SettingsDialog.tsx index 7a47b71..dc579ee 100644 --- a/vite/src/pipedal/SettingsDialog.tsx +++ b/vite/src/pipedal/SettingsDialog.tsx @@ -20,8 +20,8 @@ import React, { SyntheticEvent, Component } from 'react'; import Switch from "@mui/material/Switch"; -import ChannelMixerSettings from './ChannelMixerSettings'; -import ChannelMixerSettingsDialog from './ChannelMixerSettingsDialog'; +import ChannelRouterSettings from './ChannelRouterSettings'; +import ChannelRouterSettingsDialog from './ChannelRouterSettingsDialog'; import OkCancelDialog from './OkCancelDialog'; import RadioSelectDialog from './RadioSelectDialog'; import IconButtonEx from './IconButtonEx'; @@ -78,7 +78,7 @@ interface SettingsDialogState { showStatusMonitorDialog: boolean; jackConfiguration: JackConfiguration; jackSettings: JackChannelSelection | null; - channelMixerSettings: ChannelMixerSettings | null; + channelRouterSettings: ChannelRouterSettings | null; jackServerSettings: JackServerSettings; alsaSequencerConfiguration: AlsaSequencerConfiguration; keepScreenOn: boolean; @@ -101,7 +101,7 @@ interface SettingsDialogState { showMidiSelectDialog: boolean; showThemeSelectDialog: boolean; showJackServerSettingsDialog: boolean; - showChannelMixerSettingsDialog: boolean; + showChannelRouterSettingsDialog: boolean; shuttingDown: boolean; restarting: boolean; isAndroidHosted: boolean; @@ -194,7 +194,7 @@ const SettingsDialog = withStyles( showStatusMonitorDialog: false, jackServerSettings: this.model.jackServerSettings.get(), - channelMixerSettings: this.model.channelMixerSettings.get(), + channelRouterSettings: this.model.channelRouterSettings.get(), jackConfiguration: this.model.jackConfiguration.get(), jackStatus: undefined, jackSettings: this.model.jackSettings.get(), @@ -215,7 +215,7 @@ const SettingsDialog = withStyles( showMidiSelectDialog: false, showThemeSelectDialog: false, showJackServerSettingsDialog: false, - showChannelMixerSettingsDialog: false, + showChannelRouterSettingsDialog: false, shuttingDown: false, restarting: false, showShutdownOkDialog: false, @@ -228,7 +228,7 @@ const SettingsDialog = withStyles( this.handleAlsaSequencerConfigurationChanged = this.handleAlsaSequencerConfigurationChanged.bind(this); this.handleJackSettingsChanged = this.handleJackSettingsChanged.bind(this); this.handleJackServerSettingsChanged = this.handleJackServerSettingsChanged.bind(this); - this.handleChannelMixerSettingsChanged = this.handleChannelMixerSettingsChanged.bind(this); + this.handleChannelRouterSettingsChanged = this.handleChannelRouterSettingsChanged.bind(this); this.handleWifiConfigSettingsChanged = this.handleWifiConfigSettingsChanged.bind(this); this.handleWifiDirectConfigSettingsChanged = this.handleWifiDirectConfigSettingsChanged.bind(this); this.handleGovernorSettingsChanged = this.handleGovernorSettingsChanged.bind(this); @@ -330,10 +330,10 @@ const SettingsDialog = withStyles( continueDisabled: !this.model.jackServerSettings.get().valid }); } - handleChannelMixerSettingsChanged(): void { + handleChannelRouterSettingsChanged(): void { this.setState({ - channelMixerSettings: this.model.channelMixerSettings.get(), - continueDisabled: !this.model.channelMixerSettings.get().configured + channelRouterSettings: this.model.channelRouterSettings.get(), + continueDisabled: !this.model.channelRouterSettings.get().configured }); } @@ -378,7 +378,7 @@ const SettingsDialog = withStyles( this.model.jackConfiguration.addOnChangedHandler(this.handleJackConfigurationChanged); this.model.alsaSequencerConfiguration.addOnChangedHandler(this.handleAlsaSequencerConfigurationChanged); this.model.jackServerSettings.addOnChangedHandler(this.handleJackServerSettingsChanged); - this.model.channelMixerSettings.addOnChangedHandler(this.handleChannelMixerSettingsChanged); + this.model.channelRouterSettings.addOnChangedHandler(this.handleChannelRouterSettingsChanged); this.model.wifiConfigSettings.addOnChangedHandler(this.handleWifiConfigSettingsChanged); this.model.wifiDirectConfigSettings.addOnChangedHandler(this.handleWifiDirectConfigSettingsChanged); this.model.governorSettings.addOnChangedHandler(this.handleGovernorSettingsChanged); @@ -401,7 +401,7 @@ const SettingsDialog = withStyles( this.handleJackSettingsChanged(); this.handleShowStatusMonitorChanged(); this.handleJackServerSettingsChanged(); - this.handleChannelMixerSettingsChanged(); + this.handleChannelRouterSettingsChanged(); this.handleWifiConfigSettingsChanged(); this.handleWifiDirectConfigSettingsChanged(); this.setState({ hasWifiDevice: this.model.hasWifiDevice.get() }); @@ -420,7 +420,7 @@ const SettingsDialog = withStyles( this.model.alsaSequencerConfiguration.removeOnChangedHandler(this.handleAlsaSequencerConfigurationChanged); this.model.jackSettings.removeOnChangedHandler(this.handleJackSettingsChanged); this.model.jackServerSettings.removeOnChangedHandler(this.handleJackServerSettingsChanged); - this.model.channelMixerSettings.removeOnChangedHandler(this.handleChannelMixerSettingsChanged); + this.model.channelRouterSettings.removeOnChangedHandler(this.handleChannelRouterSettingsChanged); this.model.wifiConfigSettings.removeOnChangedHandler(this.handleWifiConfigSettingsChanged); this.model.wifiDirectConfigSettings.removeOnChangedHandler(this.handleWifiDirectConfigSettingsChanged); this.model.governorSettings.removeOnChangedHandler(this.handleGovernorSettingsChanged); @@ -720,10 +720,10 @@ const SettingsDialog = withStyles( {this.state.jackServerSettings.getSummaryText()} - {this.state.showChannelMixerSettingsDialog && ( - this.setState({ showChannelMixerSettingsDialog: false })} + {this.state.showChannelRouterSettingsDialog && ( + this.setState({ showChannelRouterSettingsDialog: false })} /> )} {this.state.showJackServerSettingsDialog && ( @@ -744,7 +744,7 @@ const SettingsDialog = withStyles( { this.setState({ - showChannelMixerSettingsDialog: true + showChannelRouterSettingsDialog: true }); }} disabled={!hasAudioConfig} diff --git a/vite/src/pipedal/ToobParametricEqView.tsx b/vite/src/pipedal/ToobParametricEqView.tsx index 7bced53..8a6bb16 100644 --- a/vite/src/pipedal/ToobParametricEqView.tsx +++ b/vite/src/pipedal/ToobParametricEqView.tsx @@ -372,7 +372,7 @@ const ToobParametricEqView = ])} {panel("", [ controls.gain - ])} + ])} {divider()} {panel("Low", [controls.low_level,