From 1f60e02ce0d07158d30ee189dfdf65076b2fc58e Mon Sep 17 00:00:00 2001 From: "Robin E.R. Davies" Date: Thu, 5 Mar 2026 09:19:51 -0500 Subject: [PATCH] Checkpoint point, pre instantiation of supplementary LV2pedalboards. --- artifacts/Pi-Icon-Inkscape.svg | 83 ++- artifacts/Pi-Icon-Svg.svg | 91 ++- artifacts/logo192.png | Bin 0 -> 9914 bytes src/AlsaDriver.cpp | 451 ++++++++---- src/AlsaDriver.hpp | 13 +- src/AlsaDriverTest.cpp | 6 +- src/AudioDriver.hpp | 33 +- src/AudioHost.cpp | 654 +++++++++++++----- src/AudioHost.hpp | 3 +- src/Banks.hpp | 2 +- src/ChannelRouterSettings.cpp | 4 +- src/ChannelRouterSettings.hpp | 5 + src/DeviceVus.hpp | 43 ++ src/DummyAudioDriver.cpp | 3 + src/DummyAudioDriver.hpp | 2 + src/JackConfiguration.cpp | 8 +- src/JackServerSettings.cpp | 2 + src/JackServerSettings.hpp | 20 +- src/Lv2Pedalboard.cpp | 43 +- src/Lv2Pedalboard.hpp | 5 +- src/Pedalboard.cpp | 4 +- src/Pedalboard.hpp | 497 ++++++------- src/PiLatencyMain.cpp | 4 + src/PiPedalCommon.hpp | 10 + src/PiPedalModel.cpp | 33 +- src/PiPedalModel.hpp | 4 +- src/PiPedalSocket.cpp | 8 +- src/RingBufferReader.hpp | 15 +- src/Storage.cpp | 2 +- src/VuUpdate.cpp | 19 +- src/VuUpdate.hpp | 15 +- vite/public/logo192.png | Bin 9754 -> 9914 bytes vite/public/logo512.png | Bin 27877 -> 28337 bytes vite/src/pipedal/ChannelRouterSettings.tsx | 27 +- .../pipedal/ChannelRouterSettingsDialog.tsx | 62 +- vite/src/pipedal/Pedalboard.tsx | 2 + vite/src/pipedal/PiPedalModel.tsx | 4 + vite/src/pipedal/VuMeter.tsx | 17 +- vite/vite.config.ts | 2 +- 39 files changed, 1502 insertions(+), 694 deletions(-) create mode 100644 artifacts/logo192.png create mode 100644 src/DeviceVus.hpp diff --git a/artifacts/Pi-Icon-Inkscape.svg b/artifacts/Pi-Icon-Inkscape.svg index 4315f57..1925dbd 100644 --- a/artifacts/Pi-Icon-Inkscape.svg +++ b/artifacts/Pi-Icon-Inkscape.svg @@ -7,9 +7,9 @@ viewBox="0 0 16.933332 16.933334" version="1.1" id="svg1" - inkscape:export-filename="logo192.png" - inkscape:export-xdpi="288" - inkscape:export-ydpi="288" + inkscape:export-filename="../vite/public/logo192.png" + inkscape:export-xdpi="288.00006" + inkscape:export-ydpi="288.00006" inkscape:version="1.4.3 (1:1.4.3+202512261035+0d15f75042)" sodipodi:docname="Pi-Icon-Inkscape.svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" @@ -25,18 +25,18 @@ inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" - inkscape:document-units="mm" - showgrid="true" + inkscape:document-units="px" + showgrid="false" inkscape:lockguides="true" - inkscape:zoom="14.609375" - inkscape:cx="32" - inkscape:cy="32" - inkscape:window-width="2141" - inkscape:window-height="1161" + inkscape:zoom="3.1914063" + inkscape:cx="61.101591" + inkscape:cy="46.844553" + inkscape:window-width="1920" + inkscape:window-height="1043" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" - inkscape:current-layer="layer1"> + inkscape:current-layer="layer2"> + id="defs1"> + + + + + + + + + style="display:none"> + + + + diff --git a/artifacts/Pi-Icon-Svg.svg b/artifacts/Pi-Icon-Svg.svg index 0fb2d6f..1925dbd 100644 --- a/artifacts/Pi-Icon-Svg.svg +++ b/artifacts/Pi-Icon-Svg.svg @@ -7,9 +7,9 @@ viewBox="0 0 16.933332 16.933334" version="1.1" id="svg1" - inkscape:export-filename="../vite/public/logo512.png" - inkscape:export-xdpi="768" - inkscape:export-ydpi="768" + inkscape:export-filename="../vite/public/logo192.png" + inkscape:export-xdpi="288.00006" + inkscape:export-ydpi="288.00006" inkscape:version="1.4.3 (1:1.4.3+202512261035+0d15f75042)" sodipodi:docname="Pi-Icon-Inkscape.svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" @@ -25,18 +25,18 @@ inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" - inkscape:document-units="mm" - showgrid="true" + inkscape:document-units="px" + showgrid="false" inkscape:lockguides="true" - inkscape:zoom="14.609375" - inkscape:cx="32" - inkscape:cy="32" - inkscape:window-width="2141" - inkscape:window-height="1161" + inkscape:zoom="3.1914063" + inkscape:cx="61.101591" + inkscape:cy="46.844553" + inkscape:window-width="1920" + inkscape:window-height="1043" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" - inkscape:current-layer="layer1"> + inkscape:current-layer="layer2"> + id="defs1"> + + + + + + + + + style="display:none"> + r="8.4666653" /> + transform="matrix(1.1241207,0,0,1.1241207,-1.0580763,-1.0508884)"> + + + + diff --git a/artifacts/logo192.png b/artifacts/logo192.png new file mode 100644 index 0000000000000000000000000000000000000000..2bb4cca7156d51e9248a8a0ecf39005bb61fb3ce GIT binary patch literal 9914 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 bcq6T<>y)F&2LFZX&Hz$k@}iZ(`u_g|!Q6bw literal 0 HcmV?d00001 diff --git a/src/AlsaDriver.cpp b/src/AlsaDriver.cpp index 7e999bc..06733c8 100644 --- a/src/AlsaDriver.cpp +++ b/src/AlsaDriver.cpp @@ -273,7 +273,7 @@ namespace pipedal auto inAvail = snd_pcm_avail_update(this->captureHandle); auto outAvail = snd_pcm_avail_update(this->playbackHandle); - auto total = (inAvail >= 0? inAvail: 0) + (outAvail >= 0 ? outAvail: 0) + framesInBuffer; + auto total = (inAvail >= 0 ? inAvail : 0) + (outAvail >= 0 ? outAvail : 0) + framesInBuffer; bufferTraces[bufferTraceIndex++] = { time, inAvail, @@ -332,23 +332,26 @@ namespace pipedal std::vector> allocatedBuffers; - std::set usedOutputChannels; - std::vector deviceCaptureBuffers; - std::vector devicePlaybackBuffers; + std::vector deviceCaptureBuffers; + std::vector devicePlaybackBuffers; float *zeroInputBuffer = nullptr; float *discardOutputBuffer = nullptr; std::vector mainCaptureBuffers; std::vector mainPlaybackBuffers; + std::vector mainInsertPlaybackBuffers; + std::vector mainVuPlaybackBuffers; std::vector auxCaptureBuffers; std::vector auxPlaybackBuffers; + std::vector auxVuPlaybackBuffers; std::vector sendCaptureBuffers; std::vector sendPlaybackBuffers; - struct OutputBufferMix { + struct OutputBufferMix + { size_t outputChannel = 0; - std::vector inputBuffers; + std::vector inputBuffers; float *outputBuffer = nullptr; }; @@ -362,7 +365,9 @@ namespace pipedal void validate_capture_handle() { // leftover debugging for a buffer overrun :-/ #ifdef DEBUG - if (snd_pcm_type(captureHandle) != SND_PCM_TYPE_HW) + auto pcmType = snd_pcm_type(captureHandle); + + if (pcmType != SND_PCM_TYPE_HW && pcmType != SND_PCM_TYPE_NULL) { throw std::runtime_error("Capture handle has been overwritten"); } @@ -1130,27 +1135,9 @@ namespace pipedal } } - public: - void TestFormatEncodeDecode(snd_pcm_format_t captureFormat); - - private: - 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; - } - } - } - - ChannelSelection channelSelection; - bool open = false; - virtual void Open(const JackServerSettings &jackServerSettings, const ChannelSelection &channelSelection) + virtual void Open(const JackServerSettings &jackServerSettings, const ChannelSelection &channelSelection) override { + this->isDummyDriver = jackServerSettings.IsDummyAudioDevice(); terminateAudio_ = false; if (open) { @@ -1174,6 +1161,27 @@ namespace pipedal } } + public: + void TestFormatEncodeDecode(snd_pcm_format_t captureFormat); + + private: + 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; + } + } + } + + ChannelSelection channelSelection; + + bool open = false; + void RestartAlsa() { std::lock_guard lock{restartMutex}; @@ -1200,13 +1208,13 @@ namespace pipedal throw std::runtime_error("Unable to restart the audio stream."); } int err; - + if ((err = snd_pcm_start(captureHandle)) < 0) { Lv2Log::error(SS("Unable to restart ALSA capture: " << snd_strerror(err))); throw PiPedalStateException("Unable to restart ALSA capture."); } - TraceBufferPositions(0,'+'); + TraceBufferPositions(0, '+'); audioRunning = true; } @@ -1280,15 +1288,11 @@ namespace pipedal << ", " << GetAlsaFormatDescription(this->captureFormat) << ", " << this->sampleRate << ", " << this->bufferSize << "x" << this->numberOfBuffers - << ", " << "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() - ); + << ", " << "device: " << this->DeviceInputBufferCount() << "/" << this->DeviceOutputBufferCount() + << ", main: " << this->MainInputBufferCount() << "/" << this->MainOutputBufferCount() + << ", main ins: " << this->MainOutputBufferCount() << "/" << this->MainOutputBufferCount() + << ", aux: " << this->AuxInputBufferCount() << "/" << this->AuxOutputBufferCount() + << ", send: " << this->SendOutputBufferCount() << "/" << this->SendInputBufferCount()); return result; } void PreparePlaybackFunctions(snd_pcm_format_t playbackFormat) @@ -1357,10 +1361,9 @@ namespace pipedal int err; - std::string inputName = jackServerSettings.GetAlsaInputDevice(); + std::string inputName = jackServerSettings.GetAlsaInputDevice(); std::string outputName = jackServerSettings.GetAlsaOutputDevice(); - this->numberOfBuffers = jackServerSettings.GetNumberOfBuffers(); this->bufferSize = jackServerSettings.GetBufferSize(); this->user_threshold = jackServerSettings.GetBufferSize(); @@ -1403,7 +1406,7 @@ namespace pipedal { snd_pcm_nonblock(playbackHandle, 0); } - + this->alsa_device_name = inputName; err = snd_pcm_open(&captureHandle, inputName.c_str(), SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK); @@ -1494,6 +1497,10 @@ namespace pipedal memset(rawPlaybackBuffer.data(), 0, rawPlaybackBuffer.size()); int retry = 0; + if (this->isDummyDriver) + { + return; // dummy driver is insatiable. + } while (true) { auto avail = snd_pcm_avail(this->playbackHandle); @@ -1513,7 +1520,7 @@ namespace pipedal { throw PiPedalStateException(SS("Audio playback failed. " << snd_strerror(err))); } - std::this_thread::sleep_for(std::chrono::milliseconds(10)); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); continue; } if (avail == 0) @@ -1658,7 +1665,7 @@ namespace pipedal auto frame_bytes = this->captureFrameSize; do { - TraceBufferPositions(framesRead,'1'); + TraceBufferPositions(framesRead, '1'); framesRead = snd_pcm_readi(handle, buffer, frames); if (framesRead < 0) @@ -1676,7 +1683,7 @@ namespace pipedal } } while (frames > 0); - TraceBufferPositions(framesRead,'2'); + TraceBufferPositions(framesRead, '2'); return framesRead; } @@ -1727,6 +1734,7 @@ namespace pipedal } private: + bool isDummyDriver = false; long WriteBuffer(snd_pcm_t *handle, uint8_t *buf, size_t frames) { long framesRead; @@ -1827,17 +1835,30 @@ namespace pipedal // Perform any neccessary mixing of outputs. for (auto &bufferMix : outputBufferMixes) { - float *outputBuffer = bufferMix.outputBuffer; - for (auto &mixBuffer : bufferMix.inputBuffers) + float * PIPEDAL_RESTRICT outputBuffer = bufferMix.outputBuffer; + float * PIPEDAL_RESTRICT inputBuffer = bufferMix.inputBuffers[0]; + for (size_t i = 0; i < framesRead; ++i) { + outputBuffer[i] = inputBuffer[i]; + } + for (size_t nInput = 1; nInput < bufferMix.inputBuffers.size(); ++nInput) + { + float * PIPEDAL_RESTRICT inputBuffer = bufferMix.inputBuffers[nInput]; for (size_t i = 0; i < framesRead; ++i) { - outputBuffer[i] += mixBuffer[i]; + outputBuffer[i] += inputBuffer[i]; } } } + // final downmix. (this->*copyOutputFn)(framesRead); + + if (this->driverHost) + { + driverHost->OnRealtimeUpdateDeviceVus(framesRead); + } + cpuUse.AddSample(ProfileCategory::Driver); // process. @@ -1850,6 +1871,10 @@ namespace pipedal recover_from_output_underrun(captureHandle, playbackHandle, err, framesRead); framesRead = 0; } + if (isDummyDriver) + { + std::this_thread::sleep_for(std::chrono::milliseconds(1000 / 30)); + } cpuUse.AddSample(ProfileCategory::Write); } } @@ -1889,41 +1914,39 @@ namespace pipedal bool alsaActive = false; - PIPEDAL_NON_INLINE void AllocateInputChannels( const std::vector &channelSelection, - std::vector &channelBuffers - ) { + std::vector &channelBuffers) + { size_t nChannels = channelSelection.size(); if (nChannels == 0) { - channelBuffers.resize(1); - channelBuffers[0] = zeroInputBuffer; + channelBuffers.resize(0); return; } channelBuffers.resize(nChannels); - for (size_t i = 0; i < nChannels; ++i) + for (size_t i = 0; i < nChannels; ++i) { int64_t deviceChannel = channelSelection[i]; - if (deviceChannel == -1 || deviceChannel >= captureChannels) + if (deviceChannel == -1 || deviceChannel >= captureChannels) { channelBuffers[i] = zeroInputBuffer; - } - else + } + else { - channelBuffers[i] = deviceCaptureBuffers[deviceChannel]; + channelBuffers[i] = deviceCaptureBuffers[deviceChannel]; } } } - PIPEDAL_NON_INLINE void AddMixBuffer(int64_t outputChannel, float *mixBuffer) + PIPEDAL_NON_INLINE float *AddMixBuffer(int64_t outputChannel, float *mixBuffer) { - for (auto&bufferMix: outputBufferMixes) + for (auto &bufferMix : outputBufferMixes) { if (bufferMix.outputChannel == outputChannel) { bufferMix.inputBuffers.push_back(mixBuffer); - return; + return bufferMix.outputBuffer; } } OutputBufferMix newMix; @@ -1931,53 +1954,103 @@ namespace pipedal newMix.outputBuffer = devicePlaybackBuffers[outputChannel]; newMix.inputBuffers.push_back(mixBuffer); outputBufferMixes.push_back(std::move(newMix)); + return devicePlaybackBuffers[outputChannel]; } PIPEDAL_NON_INLINE void AllocateOutputChannels( const std::vector &channelSelection, - std::vector &channelBuffers - ) { + std::vector &channelBuffers) + { size_t nChannels = channelSelection.size(); if (nChannels == 0) { - channelBuffers.resize(1); - if (discardOutputBuffer != nullptr) - { - discardOutputBuffer = AllocateAudioBuffer(); - } - channelBuffers[0] = discardOutputBuffer; + channelBuffers.resize(0); return; } channelBuffers.resize(nChannels); - for (size_t i = 0; i < nChannels; ++i) + for (size_t i = 0; i < nChannels; ++i) { int64_t deviceChannel = channelSelection[i]; - if (deviceChannel == -1 || deviceChannel >= playbackChannels) + if (deviceChannel == -1) { - if (discardOutputBuffer == nullptr) + channelBuffers[i] = this->GetDiscardOutputBuffer(); + ; + } + else + { + float *mixBuffer = AllocateAudioBuffer(); + channelBuffers[i] = mixBuffer; + } + } + } + PIPEDAL_NON_INLINE void AllocateOutputChannels( + const std::vector &channelSelection, + std::vector &channelBuffers, + std::vector &vuBuffers, + const std::set &summedChannels) + { + size_t nChannels = channelSelection.size(); + + if (nChannels == 0) + { + channelBuffers.resize(0); + vuBuffers.resize(0); + return; + } + channelBuffers.resize(nChannels); + vuBuffers.resize(nChannels); + for (size_t i = 0; i < nChannels; ++i) + { + int64_t deviceChannel = channelSelection[i]; + if (deviceChannel == -1 || deviceChannel >= playbackChannels) + { + channelBuffers[i] = GetDiscardOutputBuffer(); + } + else + { + if (!summedChannels.contains(deviceChannel)) { - discardOutputBuffer = AllocateAudioBuffer(); + vuBuffers[i] = channelBuffers[i] = this->devicePlaybackBuffers[deviceChannel]; } - channelBuffers[i] = discardOutputBuffer; - } - else - { - if (!usedOutputChannels.contains(deviceChannel)) + else { - 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); + vuBuffers[i] = AddMixBuffer(deviceChannel, mixBuffer); } } } } - + std::set ComputeSummedChannels() + { + std::set usedOutputChannels; + std::set result; + for (auto ch : channelSelection.mainOutputChannels()) + { + if (usedOutputChannels.contains(ch)) + { + result.insert(ch); + } + else + { + usedOutputChannels.insert(ch); + } + } + for (auto ch : channelSelection.auxOutputChannels()) + { + if (usedOutputChannels.contains(ch)) + { + result.insert(ch); + } + else + { + usedOutputChannels.insert(ch); + } + } + return result; + } bool activated = false; PIPEDAL_NON_INLINE virtual void Activate() @@ -1990,10 +2063,8 @@ namespace pipedal activated = true; // Reset previously allocated buffers. - usedOutputChannels.clear(); allocatedBuffers.resize(0); - // Allocate device capture buffers. zeroInputBuffer = AllocateAudioBuffer(); deviceCaptureBuffers.resize(captureChannels); @@ -2007,27 +2078,33 @@ namespace pipedal devicePlaybackBuffers[i] = AllocateAudioBuffer(); } + std::set summedOutputChannels = ComputeSummedChannels(); + AllocateInputChannels( channelSelection.mainInputChannels(), this->mainCaptureBuffers); AllocateOutputChannels( channelSelection.mainOutputChannels(), - this->mainPlaybackBuffers - ); + this->mainPlaybackBuffers); + AllocateOutputChannels( + channelSelection.mainOutputChannels(), + this->mainInsertPlaybackBuffers, + this->mainVuPlaybackBuffers, + summedOutputChannels); AllocateInputChannels( channelSelection.auxInputChannels(), this->auxCaptureBuffers); AllocateOutputChannels( channelSelection.auxOutputChannels(), - this->auxPlaybackBuffers - ); + this->auxPlaybackBuffers, + this->auxVuPlaybackBuffers, + summedOutputChannels); AllocateInputChannels( channelSelection.sendInputChannels(), this->sendCaptureBuffers); AllocateOutputChannels( channelSelection.sendOutputChannels(), - this->sendPlaybackBuffers - ); + this->sendPlaybackBuffers); audioThread = std::make_unique([this]() { AudioThread(); }); @@ -2058,23 +2135,74 @@ namespace pipedal AlsaSequencer::ptr alsaSequencer; public: + virtual const ChannelSelection &GetChannelSelection() const override + { + return channelSelection; + } + virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) override { this->alsaSequencer = alsaSequencer; } - virtual size_t DeviceInputBufferCount() const override { + virtual size_t DeviceInputBufferCount() const override + { return deviceCaptureBuffers.size(); } - virtual size_t DeviceOutputBufferCount() const override { + virtual size_t DeviceOutputBufferCount() const override + { return devicePlaybackBuffers.size(); } + virtual float *GetDeviceInputBuffer(size_t channel) const override + { + if (channel >= deviceCaptureBuffers.size()) + return nullptr; + return deviceCaptureBuffers[channel]; + } + virtual float *GetDeviceOutputBuffer(size_t channel) const override + { + if (channel >= devicePlaybackBuffers.size()) + return nullptr; + return devicePlaybackBuffers[channel]; + } + + virtual float *GetZeroInputBuffer() + { + if (zeroInputBuffer == nullptr) + { + zeroInputBuffer = AllocateAudioBuffer(); + } + return zeroInputBuffer; + } + virtual float *GetDiscardOutputBuffer() + { + if (discardOutputBuffer == nullptr) + { + discardOutputBuffer = AllocateAudioBuffer(); + } + return discardOutputBuffer; + } + + virtual std::vector &DeviceInputBuffers() override { return this->deviceCaptureBuffers; } + virtual std::vector &DeviceOutputBuffers() override { return this->devicePlaybackBuffers; } + + virtual std::vector &MainInputBuffers() override { return this->mainCaptureBuffers; } + virtual std::vector &MainOutputBuffers() override { return this->mainPlaybackBuffers; } + virtual std::vector &MainInsertOutputBuffers() override { return this->mainInsertPlaybackBuffers; } + virtual std::vector &MainVuOutputBuffers() override { return this->mainVuPlaybackBuffers; } + + virtual std::vector &AuxInputBuffers() override { return this->auxCaptureBuffers; } + virtual std::vector &AuxOutputBuffers() override { return this->auxPlaybackBuffers; } + virtual std::vector &AuxVuOutputBuffers() override { return this->auxVuPlaybackBuffers; } + + virtual const std::vector &SendInputBuffers() const override { return this->sendCaptureBuffers; } + virtual const std::vector &SendOutputBuffers() const override { return this->sendPlaybackBuffers; } virtual size_t MainInputBufferCount() const { return mainCaptureBuffers.size(); } virtual float *GetMainInputBuffer(size_t channel) override { - if (channel >= (int64_t)mainCaptureBuffers.size()) + if (channel >= (int64_t)mainCaptureBuffers.size()) { throw std::runtime_error("Argument out of range."); } @@ -2085,8 +2213,10 @@ namespace pipedal { return auxCaptureBuffers[channel]; } - virtual size_t AuxOutputBufferCount() const { - return auxPlaybackBuffers.size(); } + virtual size_t AuxOutputBufferCount() const + { + return auxPlaybackBuffers.size(); + } virtual float *GetAuxOutputBuffer(size_t channel) override { return auxPlaybackBuffers[channel]; @@ -2116,11 +2246,25 @@ namespace pipedal { return mainPlaybackBuffers[channel]; } + virtual size_t MainInsertOutputBufferCount() const override + { + return mainInsertPlaybackBuffers.size(); + } + virtual float *GetMainInsertOutputBuffer(size_t channel) const + { + if (channel >= (int64_t)mainCaptureBuffers.size()) + { + throw std::runtime_error("GetMainInsertOutputBuffer: Argument out of range."); + } - float* AllocateAudioBuffer() { + return mainInsertPlaybackBuffers[channel]; + } + + float *AllocateAudioBuffer() + { std::vector buffer; buffer.resize(this->bufferSize); - float*pBuffer = buffer.data(); + float *pBuffer = buffer.data(); allocatedBuffers.push_back(std::move(buffer)); return pBuffer; } @@ -2173,19 +2317,19 @@ namespace pipedal std::vector &inputAudioPorts, std::vector &outputAudioPorts) { - if (jackServerSettings.IsDummyAudioDevice()) - { - auto nChannels = GetDummyAudioChannels(jackServerSettings.GetAlsaInputDevice()); + // if (jackServerSettings.IsDummyAudioDevice()) + // { + // auto nChannels = GetDummyAudioChannels(jackServerSettings.GetAlsaInputDevice()); - inputAudioPorts.clear(); - outputAudioPorts.clear(); - for (uint32_t i = 0; i < nChannels; ++i) - { - inputAudioPorts.push_back(std::string(SS("system::capture_" << i))); - outputAudioPorts.push_back(std::string(SS("system::playback_" << i))); - } - return true; - } + // inputAudioPorts.clear(); + // outputAudioPorts.clear(); + // for (uint32_t i = 0; i < nChannels; ++i) + // { + // inputAudioPorts.push_back(std::string(SS("system::capture_" << i))); + // outputAudioPorts.push_back(std::string(SS("system::playback_" << i))); + // } + // return true; + // } snd_pcm_t *playbackHandle = nullptr; snd_pcm_t *captureHandle = nullptr; @@ -2422,7 +2566,7 @@ namespace pipedal } } - void AlsaFormatEncodeDecodeTest(AudioDriverHost *testDriverHost) + void test::AlsaFormatEncodeDecodeTest(AudioDriverHost *testDriverHost) { static snd_pcm_format_t formats[] = { snd_pcm_format_t::SND_PCM_FORMAT_S16_LE, @@ -2444,7 +2588,7 @@ namespace pipedal alsaDriver->TestFormatEncodeDecode(format); } } - void MidiDecoderTest() + void test::MidiDecoderTest() { #ifdef JUNK AlsaDriverImpl::AlsaMidiDeviceImpl midiState; @@ -2513,37 +2657,41 @@ namespace pipedal { using namespace std; int savedPrecision = cout.precision(); - auto savedFlags = cout.flags(); + auto savedFlags = cout.flags(); size_t ix = bufferTraceIndex; if (ix < nEntries) { - ix = ix + bufferTraces.size()-nEntries; - } else { + ix = ix + bufferTraces.size() - nEntries; + } + else + { ix -= nEntries; } uint64_t t0; - if (bufferTraceIndex == 0) { - t0 = bufferTraces[bufferTraces.size()-1].time; - } else { - t0 = bufferTraces[bufferTraceIndex-1].time; + if (bufferTraceIndex == 0) + { + t0 = bufferTraces[bufferTraces.size() - 1].time; + } + else + { + t0 = bufferTraces[bufferTraceIndex - 1].time; } while (ix != bufferTraceIndex) { - auto&bufferTrace = bufferTraces[ix]; + auto &bufferTrace = bufferTraces[ix]; if (bufferTrace.time != 0) { int64_t dt = (int64_t)bufferTrace.time - (int64_t)t0; - - cout << bufferTrace.code << " " - << fixed << setprecision(3) << dt*0.001 - << " " << "inAvail: " << bufferTrace.inAvail - << " " << "outAvail: " << bufferTrace.outAvail - << " " << "buffered: " << bufferTrace.buffered - << " " << "total: " << bufferTrace.total - << endl; + cout << bufferTrace.code << " " + << fixed << setprecision(3) << dt * 0.001 + << " " << "inAvail: " << bufferTrace.inAvail + << " " << "outAvail: " << bufferTrace.outAvail + << " " << "buffered: " << bufferTrace.buffered + << " " << "total: " << bufferTrace.total + << endl; } ++ix; @@ -2557,4 +2705,51 @@ namespace pipedal cout.flags(savedFlags); } + AlsaDeviceInfo MakeDummyDeviceInfo(uint32_t channels) + { + AlsaDeviceInfo result; + constexpr int DUMMY_DEVICE_ID_OFFSET = 100974; + result.cardId_ = DUMMY_DEVICE_ID_OFFSET + channels; + result.id_ = SS("dummy:channels_" << channels); + result.name_ = SS("Dummy Device (" << channels << " channels)"); + result.longName_ = result.name_; + result.sampleRates_.push_back(44100); + result.sampleRates_.push_back(48000); + result.minBufferSize_ = 16; + result.maxBufferSize_ = 1024; + result.supportsCapture_ = true; + result.supportsPlayback_ = true; + return result; + } + + uint32_t GetDummyAudioChannels(const std::string &deviceName) + { + uint32_t channels; + int pos = deviceName.find_last_of('_'); + if (pos == std::string::npos) + { + throw std::runtime_error("Invalid dummy device name"); + } + std::istringstream ss(deviceName.substr(pos + 1)); + ss >> channels; + return channels; + } + AudioDriver *CreateDummyAudioDriver(AudioDriverHost *driverHost, const JackServerSettings &jackServerSettings, const ChannelSelection &channelSelection) + { + auto dummyServerSettings = jackServerSettings; + dummyServerSettings.UseDummyAudioDevice(); + auto dummyChannelSelection = channelSelection; + AudioDriver *driver = new AlsaDriverImpl(driverHost); + try + { + driver->Open(jackServerSettings, channelSelection); + return driver; + } + catch (const std::exception &e) + { + delete driver; + throw; + } + } + } // namespace diff --git a/src/AlsaDriver.hpp b/src/AlsaDriver.hpp index 3d833a8..a6a2c15 100644 --- a/src/AlsaDriver.hpp +++ b/src/AlsaDriver.hpp @@ -38,10 +38,17 @@ namespace pipedal { AudioDriver* CreateAlsaDriver(AudioDriverHost*driverHost); - // test only. - void AlsaFormatEncodeDecodeTest(AudioDriverHost*driverHost); - void MidiDecoderTest(); + namespace test { + // Private use, test only. + void AlsaFormatEncodeDecodeTest(AudioDriverHost*driverHost); + void MidiDecoderTest(); + } void FreeAlsaGlobals(); // for valgrind. Free the Alsa configuration cache. + + // AlsaDeviceInfo MakeDummyDeviceInfo(uint32_t channels); + uint32_t GetDummyAudioChannels(const std::string &deviceName); + // std::shared_ptr CreateDummyAudioDriver(AudioDriverHost*driverHost,const std::string&deviceId); + } diff --git a/src/AlsaDriverTest.cpp b/src/AlsaDriverTest.cpp index 9ef3504..1550f48 100644 --- a/src/AlsaDriverTest.cpp +++ b/src/AlsaDriverTest.cpp @@ -63,6 +63,8 @@ public: delete[] outputBuffers; } + bool OnRealtimeUpdateDeviceVus(size_t nFrames) override { return false; } + void initializeChannelSelection(JackConfiguration &jackConfiguration, ChannelSelection &channelSelection) { size_t nInputs = std::max(jackConfiguration.inputAudioPorts().size(),size_t(2)); @@ -85,7 +87,7 @@ public: void Test() { - AlsaFormatEncodeDecodeTest(this); + test::AlsaFormatEncodeDecodeTest(this); JackServerSettings serverSettings("hw:M2","hw:M2", 48000, 32, 3); @@ -295,5 +297,5 @@ TEST_CASE("alsa_midi_test", "[alsa_midi_test]") { AlsaTester alsaDriver(AlsaTester::TestType::Oscillator); - MidiDecoderTest(); + test::MidiDecoderTest(); } diff --git a/src/AudioDriver.hpp b/src/AudioDriver.hpp index ea88c28..cfcfb22 100644 --- a/src/AudioDriver.hpp +++ b/src/AudioDriver.hpp @@ -27,6 +27,7 @@ #include #include "AlsaSequencer.hpp" #include "MidiEvent.hpp" +#include "DeviceVus.hpp" @@ -37,15 +38,14 @@ namespace pipedal { class ChannelSelection; - class AudioDriverHost { public: virtual void OnProcess(size_t nFrames) = 0; + virtual bool OnRealtimeUpdateDeviceVus(size_t nFrames) = 0; + virtual void OnUnderrun() = 0; virtual void OnAlsaDriverStopped() = 0; virtual void OnAudioTerminated() = 0; - - }; class AudioDriver { public: @@ -60,12 +60,33 @@ namespace pipedal { virtual size_t GetMidiInputEventCount() = 0; virtual MidiEvent*GetMidiEvents() = 0; + virtual const ChannelSelection &GetChannelSelection() const = 0; + + + virtual std::vector &DeviceInputBuffers() = 0; + virtual std::vector &DeviceOutputBuffers() = 0; + + virtual std::vector &MainInputBuffers() = 0; + virtual std::vector &MainOutputBuffers() = 0; + virtual std::vector&MainInsertOutputBuffers() = 0; + virtual std::vector&MainVuOutputBuffers() = 0; + + virtual std::vector&AuxInputBuffers() = 0; + virtual std::vector&AuxOutputBuffers() = 0; + virtual std::vector&AuxVuOutputBuffers() = 0; + virtual size_t DeviceInputBufferCount() const = 0; virtual size_t DeviceOutputBufferCount() const = 0; + virtual float* GetDeviceInputBuffer(size_t channel) const = 0; + virtual float* GetDeviceOutputBuffer(size_t channel) 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 float*GetMainOutputBuffer(size_t channel) = 0; + + virtual size_t MainInsertOutputBufferCount() const = 0; + virtual float*GetMainInsertOutputBuffer(size_t channe) const = 0; virtual size_t AuxInputBufferCount() const = 0; @@ -75,9 +96,13 @@ namespace pipedal { virtual size_t SendInputBufferCount() const = 0; virtual float*GetSendInputBuffer(size_t channel) = 0; + virtual const std::vector&SendInputBuffers() const = 0; virtual size_t SendOutputBufferCount() const = 0; virtual float*GetSendOutputBuffer(size_t channel) = 0; + virtual const std::vector&SendOutputBuffers() const = 0; + virtual float*GetZeroInputBuffer() = 0; + virtual float*GetDiscardOutputBuffer() = 0; virtual void Open(const JackServerSettings & jackServerSettings,const ChannelSelection &channelSelection) = 0; virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) = 0; diff --git a/src/AudioHost.cpp b/src/AudioHost.cpp index 212c327..fbd16cb 100644 --- a/src/AudioHost.cpp +++ b/src/AudioHost.cpp @@ -348,7 +348,8 @@ bool SystemMidiBinding::IsTriggered(const MidiEvent &event) if (currentBinding.switchControlType() == SwitchControlTypeT::TRIGGER_ON_RISING_EDGE) { result = value >= 0x64 && lastControlValue < 0x64; - } else if (currentBinding.switchControlType() == SwitchControlTypeT::TRIGGER_ON_ANY) + } + else if (currentBinding.switchControlType() == SwitchControlTypeT::TRIGGER_ON_ANY) { result = true; } @@ -529,8 +530,6 @@ private: std::shared_ptr currentMainInsertPedalboard; std::shared_ptr currentAuxInsertPedalboard; - - std::vector> activePedalboards; // pedalboards that have been sent to the audio queue. Lv2Pedalboard *realtimeActivePedalboard = nullptr; Lv2RoutingInserts realtimeRoutingInserts; @@ -544,7 +543,7 @@ private: std::string GetAtomObjectType(uint8_t *pData) { - LV2_Atom_Object *pAtom = (LV2_Atom_Object *)pData; + LV2_Atom_Object *pAtom = (LV2_Atom_Object *)pData; if (pAtom->atom.type != uris.atom_Object) { throw std::invalid_argument("Not an Lv2 Object"); @@ -629,7 +628,7 @@ private: { for (size_t i = 0; i < audioDriver->MainOutputBufferCount(); ++i) { - float * out = (float *)audioDriver->GetMainOutputBuffer(i); + float *out = (float *)audioDriver->GetMainOutputBuffer(i); if (out) { ZeroBuffer(out, nframes); @@ -638,7 +637,7 @@ private: } RealtimeVuBuffers *realtimeVuBuffers = nullptr; size_t vuSamplesPerUpdate = 0; - int64_t vuSamplesRemaining = 0; + int64_t realtimeVuSamplesRemaining = 0; void freeRealtimeVuConfiguration() { @@ -664,11 +663,15 @@ private: virtual void SetSystemMidiBindings(const std::vector &bindings); - void writeVu() + void realtimeWriteVus() { // throttling: we send one; but won't send another until the host thread // acknowledges receipt. + if (!realtimeVuBuffers) + { + return; + } if (!realtimeVuBuffers->waitingForAcknowledge) { auto pResult = realtimeVuBuffers->GetResult(currentSample); @@ -678,19 +681,27 @@ private: } } - void processMonitorPortSubscriptions(uint32_t nframes) + void processMonitorPortSubscriptions( + PedalboardType pedalboardType, + Lv2Pedalboard *pedalboard, + uint32_t nframes) { for (size_t i = 0; i < this->realtimeMonitorPortSubscriptions->subscriptions.size(); ++i) { auto &portSubscription = realtimeMonitorPortSubscriptions->subscriptions[i]; + if (pedalboardType != portSubscription.pedalboardType) + { + continue; + } + portSubscription.samplesToNextCallback -= portSubscription.sampleRate; if (portSubscription.samplesToNextCallback < 0) { portSubscription.samplesToNextCallback += portSubscription.sampleRate; if (!portSubscription.waitingForAck) { - float value = realtimeActivePedalboard->GetControlOutputValue( + float value = pedalboard->GetControlOutputValue( portSubscription.instanceIndex, portSubscription.portIndex); if (value != portSubscription.lastValue) @@ -852,7 +863,7 @@ private: { this->realtimeVuBuffers->waitingForAcknowledge = false; } - vuSamplesRemaining = vuSamplesPerUpdate; + realtimeVuSamplesRemaining = vuSamplesPerUpdate; break; } @@ -971,7 +982,7 @@ private: return (event.size == 3 && event.buffer[0] == 0xB0 && event.buffer[1] == 0x00); } - bool onMidiEvent(Lv2EventBufferWriter &eventBufferWriter, Lv2EventBufferWriter::LV2_EvBuf_Iterator &iterator, MidiEvent &event) + bool ProcessMidiMonitor(Lv2EventBufferWriter &eventBufferWriter, Lv2EventBufferWriter::LV2_EvBuf_Iterator &iterator, MidiEvent &event) { // eventBufferWriter.writeMidiEvent(iterator, 0, event.size, event.buffer); @@ -995,7 +1006,7 @@ private: } if (isNote || isControl) { - MidiNotifyBody notifyBody (event.buffer[0],event.buffer[1], event.buffer[2]); + MidiNotifyBody notifyBody(event.buffer[0], event.buffer[1], event.buffer[2]); realtimeWriter.OnMidiListen(notifyBody); } } @@ -1108,7 +1119,7 @@ private: } else { - onMidiEvent(eventBufferWriter, iterator, event); + ProcessMidiMonitor(eventBufferWriter, iterator, event); } } @@ -1151,7 +1162,69 @@ private: } } } - void ProcessMidiInput() + bool GetAudioDriverBuffers(PedalboardType pedalboardType, + float **inputBuffers, + float **outputBuffers) + { + bool buffersValid = true; + if (!audioDriver) + { + inputBuffers[0] = nullptr; + outputBuffers[0] = nullptr; + return false; + } + switch (pedalboardType) + { + case PedalboardType::MainPedalboard: + for (int i = 0; i < audioDriver->MainInputBufferCount(); ++i) + { + float *input = (float *)audioDriver->GetMainInputBuffer(i); + if (input == nullptr) + { + buffersValid = false; + } + inputBuffers[i] = input; + } + inputBuffers[audioDriver->MainInputBufferCount()] = nullptr; + + for (int i = 0; i < audioDriver->MainOutputBufferCount(); ++i) + { + float *output = audioDriver->GetMainOutputBuffer(i); + if (output == nullptr) + { + buffersValid = false; + } + outputBuffers[i] = output; + } + outputBuffers[audioDriver->MainOutputBufferCount()] = nullptr; + break; + case PedalboardType::MainInserts: + for (int i = 0; i < audioDriver->MainInputBufferCount(); ++i) + { + float *input = (float *)audioDriver->GetMainInputBuffer(i); + if (input == nullptr) + { + buffersValid = false; + } + inputBuffers[i] = input; + } + inputBuffers[audioDriver->MainInputBufferCount()] = nullptr; + + for (int i = 0; i < audioDriver->MainOutputBufferCount(); ++i) + { + float *output = audioDriver->GetMainOutputBuffer(i); + if (output == nullptr) + { + buffersValid = false; + } + outputBuffers[i] = output; + } + outputBuffers[audioDriver->MainOutputBufferCount()] = nullptr; + break; + } + return buffersValid; + } + void ProcessGlobalMidiInput() { Lv2EventBufferWriter eventBufferWriter(this->eventBufferUrids); Lv2EventBufferWriter::LV2_EvBuf_Iterator iterator = eventBufferWriter.begin(); @@ -1190,11 +1263,253 @@ private: Lv2Log::info("Audio thread terminated."); } + PIPEDAL_NON_INLINE void ProcessLv2Pedalboard(PedalboardType pedalboardType, size_t nframes) + { + Lv2Pedalboard *pedalboard = nullptr; + + std::vector *pInputBuffers; + std::vector *pOutputBuffers; + switch (pedalboardType) + { + case PedalboardType::MainPedalboard: + pedalboard = this->realtimeActivePedalboard; + pInputBuffers = &(audioDriver->MainInputBuffers()); + if (this->realtimeRoutingInserts.mainInserts != nullptr) + { + pOutputBuffers = &(audioDriver->MainOutputBuffers()); + } + else + { + pOutputBuffers = &(audioDriver->MainInsertOutputBuffers()); + } + break; + case PedalboardType::MainInserts: + + pedalboard = this->realtimeRoutingInserts.mainInserts; + pInputBuffers = &(audioDriver->MainOutputBuffers()); + pOutputBuffers = &(audioDriver->MainInsertOutputBuffers()); + break; + case PedalboardType::AuxInserts: + pedalboard = this->realtimeRoutingInserts.auxInserts; + pInputBuffers = &(audioDriver->AuxInputBuffers()); + pOutputBuffers = &(audioDriver->AuxOutputBuffers()); + break; + } + if (pInputBuffers->size() == 0 || pOutputBuffers->size() == 0) + { + return; + } + float *inputBuffers[3]; + float *outputBuffers[3]; + bool buffersValid = true; + inputBuffers[0] = pInputBuffers->at(0); + inputBuffers[1] = pInputBuffers->size() >= 2 ? pInputBuffers->at(1) : nullptr; + inputBuffers[2] = nullptr; + + outputBuffers[0] = pOutputBuffers->at(0); + outputBuffers[1] = pOutputBuffers->size() >= 2 ? pOutputBuffers->at(1) : nullptr; + outputBuffers[2] = nullptr; + + if (pedalboard != nullptr) + { + pedalboard->ProcessParameterRequests(pParameterRequests, nframes); + + pedalboard->Run(inputBuffers, outputBuffers, (uint32_t)nframes, &realtimeWriter); + pedalboard->GatherPatchProperties(pParameterRequests); + pedalboard->GatherPathPatchProperties(this); + + if (this->realtimeMonitorPortSubscriptions != nullptr) + { + processMonitorPortSubscriptions(pedalboardType, pedalboard, nframes); + } + } + else + { + switch (pedalboardType) + { + case PedalboardType::MainPedalboard: + { + // zero output buffers. + size_t ix = 0; + while (outputBuffers[ix]) + { + float *outputBuffer = outputBuffers[ix]; + for (size_t i = 0; i < nframes; ++i) + { + outputBuffer[i] = 0; + } + ++ix; + } + } + break; + case PedalboardType::MainInserts: + // do nothing. Main pedalboard wrote to Main insert outputs directly in this case. + break; + case PedalboardType::AuxInserts: + // Copy inputs to outputs. + { + if (outputBuffers[0] != nullptr && inputBuffers[0] != nullptr) + { + float *PIPEDAL_RESTRICT outputBuffer = outputBuffers[0]; + float *PIPEDAL_RESTRICT inputBuffer = inputBuffers[0]; + for (size_t i = 0; i < nframes; ++i) + { + outputBuffer[i] = inputBuffer[i]; + } + } + if (outputBuffers[1] != nullptr && inputBuffers[0] != nullptr) + { + float *PIPEDAL_RESTRICT outputBuffer = outputBuffers[1]; + float *PIPEDAL_RESTRICT inputBuffer = inputBuffers[1] != nullptr ? inputBuffers[1] : inputBuffers[0]; + for (size_t i = 0; i < nframes; ++i) + { + outputBuffer[i] = inputBuffer[i]; + } + } + } + break; + case PedalboardType::Invalid: + throw std::runtime_error("Invalid argument."); + break; + } + } + } + + void GetMasterChannels(PedalboardType pedalboardType, float **masterInputBuffers, float **masterOutputBuffers) + { + size_t inputIx = 0; + size_t outputIx = 0; + + masterInputBuffers[0] = nullptr; + masterInputBuffers[1] = nullptr; + masterOutputBuffers[0] = nullptr; + masterOutputBuffers[1] = nullptr; + + const ChannelSelection &channelSelection = audioDriver->GetChannelSelection(); + + switch (pedalboardType) + { + case PedalboardType::MainPedalboard: + for (auto nChannel : channelSelection.mainInputChannels()) + { + masterInputBuffers[inputIx++] = audioDriver->GetDeviceInputBuffer(nChannel); + } + for (auto nChannel : channelSelection.mainOutputChannels()) + { + masterOutputBuffers[outputIx++] = audioDriver->GetDeviceOutputBuffer(nChannel); + } + break; + case PedalboardType::MainInserts: + if (this->realtimeActivePedalboard) + { + for (float *buffer : this->realtimeActivePedalboard->GetoutputBuffers()) + { + masterInputBuffers[inputIx++] = buffer; + } + for (auto nChannel : channelSelection.mainOutputChannels()) + { + masterOutputBuffers[outputIx++] = audioDriver->GetDeviceOutputBuffer(nChannel); + } + } + break; + case PedalboardType::AuxInserts: + for (auto nChannel : channelSelection.auxInputChannels()) + { + switch (nChannel) + { + case ChannelRouterSettings::MAIN_OUT_LEFT_CHANNEL: + { + float *pBuffer = nullptr; + if (this->realtimeActivePedalboard) + { + auto &mainOutputBuffers = realtimeActivePedalboard->GetoutputBuffers(); + if (mainOutputBuffers.size() >= 1) + { + pBuffer = mainOutputBuffers[0]; + } + } + if (pBuffer == nullptr) + { + pBuffer = audioDriver->GetZeroInputBuffer(); + } + masterInputBuffers[inputIx++] = pBuffer; + } + break; + case ChannelRouterSettings::MAIN_OUT_RIGHT_CHANNEL: + { + float *pBuffer = nullptr; + if (this->realtimeActivePedalboard) + { + auto &mainOutputBuffers = realtimeActivePedalboard->GetoutputBuffers(); + if (mainOutputBuffers.size() == 1) + { + pBuffer = mainOutputBuffers[0]; + } + else if (mainOutputBuffers.size() == 2) + { + pBuffer = mainOutputBuffers[0]; + } + } + if (pBuffer == nullptr) + { + pBuffer = audioDriver->GetZeroInputBuffer(); + } + masterInputBuffers[inputIx++] = pBuffer; + } + break; + default: + masterInputBuffers[outputIx++] = audioDriver->GetDeviceInputBuffer(nChannel); + }; + } + for (auto nChannel : channelSelection.auxOutputChannels()) + { + masterOutputBuffers[outputIx++] = audioDriver->GetDeviceOutputBuffer(nChannel); + } + break; + + default: + throw std::runtime_error("GetMasterChannels: Argument out of range."); + } + } + bool OnRealtimeUpdateDeviceVus(size_t nFrames) override + { + // all lv2pedalboards processed, and all channels downmixed. + // Now we can do the real vu update work! + + float *masterInputBuffers[2]; + float *masterOutputBuffers[2]; + + if (this->realtimeActivePedalboard != nullptr) + { + GetMasterChannels(PedalboardType::MainPedalboard, masterInputBuffers, masterOutputBuffers); + realtimeActivePedalboard->ComputeVus(this->realtimeVuBuffers, nFrames, masterInputBuffers, masterOutputBuffers); + } + if (this->realtimeRoutingInserts.mainInserts) + { + GetMasterChannels(PedalboardType::MainInserts, masterInputBuffers, masterOutputBuffers); + this->realtimeRoutingInserts.mainInserts->ComputeVus(this->realtimeVuBuffers, nFrames, masterInputBuffers, masterOutputBuffers); + } + if (this->realtimeRoutingInserts.auxInserts) + { + GetMasterChannels(PedalboardType::AuxInserts, masterInputBuffers, masterOutputBuffers); + this->realtimeRoutingInserts.auxInserts->ComputeVus(this->realtimeVuBuffers, nFrames, masterInputBuffers, masterOutputBuffers); + } + + // periodically send updates. + realtimeVuSamplesRemaining -= nFrames; + if (realtimeVuSamplesRemaining <= 0) + { + realtimeWriteVus(); + realtimeVuSamplesRemaining = vuSamplesPerUpdate; + } + + return true; + } virtual void OnProcess(size_t nframes) { try { - float * restrict in , * restrict out; + float *restrict in, *restrict out; Lv2Pedalboard *pedalboard = nullptr; pedalboard = this->realtimeActivePedalboard; @@ -1202,6 +1517,15 @@ private: { pedalboard->ResetAtomBuffers(); } + if (this->realtimeRoutingInserts.auxInserts) + { + this->realtimeRoutingInserts.auxInserts->ResetAtomBuffers(); + } + if (this->realtimeRoutingInserts.mainInserts) + { + this->realtimeRoutingInserts.mainInserts->ResetAtomBuffers(); + } + while (true) { @@ -1212,71 +1536,18 @@ private: // else a new pedalboard was installed. start again. pedalboard = this->realtimeActivePedalboard; } - bool processed = false; if (pedalboard != nullptr) { - ProcessMidiInput(); - float *inputBuffers[4]; - float *outputBuffers[4]; - bool buffersValid = true; - for (int i = 0; i < audioDriver->MainInputBufferCount(); ++i) - { - float *input = (float *)audioDriver->GetMainInputBuffer(i); - if (input == nullptr) - { - buffersValid = false; - break; - } - inputBuffers[i] = input; - } - inputBuffers[audioDriver->MainInputBufferCount()] = nullptr; - - for (int i = 0; i < audioDriver->MainOutputBufferCount(); ++i) - { - float *output = audioDriver->GetMainOutputBuffer(i); - if (output == nullptr) - { - buffersValid = false; - break; - } - outputBuffers[i] = output; - } - outputBuffers[audioDriver->MainOutputBufferCount()] = nullptr; - - if (buffersValid) - { - pedalboard->ProcessParameterRequests(pParameterRequests,nframes); - - processed = pedalboard->Run(inputBuffers, outputBuffers, (uint32_t)nframes, &realtimeWriter); - if (processed) - { - if (this->realtimeVuBuffers != nullptr) - { - pedalboard->ComputeVus(this->realtimeVuBuffers, (uint32_t)nframes, inputBuffers, outputBuffers); - - vuSamplesRemaining -= nframes; - if (vuSamplesRemaining <= 0) - { - writeVu(); - vuSamplesRemaining += vuSamplesPerUpdate; - } - } - if (this->realtimeMonitorPortSubscriptions != nullptr) - { - processMonitorPortSubscriptions(nframes); - } - } - pedalboard->GatherPatchProperties(pParameterRequests); - pedalboard->GatherPathPatchProperties(this); - } + ProcessGlobalMidiInput(); } - - if (!processed) + ProcessLv2Pedalboard(PedalboardType::MainPedalboard, nframes); + if (this->realtimeRoutingInserts.mainInserts != nullptr) // prevent zero-ing of main outputs if there's no mainInserts { - ZeroOutputBuffers(nframes); + ProcessLv2Pedalboard(PedalboardType::MainInserts, nframes); } + ProcessLv2Pedalboard(PedalboardType::AuxInserts, nframes); if (pParameterRequests != nullptr) { @@ -1319,13 +1590,12 @@ public: this->alsaDeviceMonitor->StartMonitoring( [this]( - AlsaSequencerDeviceMonitor::MonitorAction action, + AlsaSequencerDeviceMonitor::MonitorAction action, int client, - const std::string &clientName) - { - HandleAlsaSequencerDevicesChanged(action, client, clientName); - } - ); + const std::string &clientName) + { + HandleAlsaSequencerDevicesChanged(action, client, clientName); + }); } virtual ~AudioHostImpl() { @@ -1359,10 +1629,10 @@ public: } else if (action == AlsaSequencerDeviceMonitor::MonitorAction::DeviceAdded) { - pNotifyCallbacks->OnAlsaSequencerDeviceAdded(client,clientName); + pNotifyCallbacks->OnAlsaSequencerDeviceAdded(client, clientName); } } - } + } void HandleAudioTerminatedAbnormally() { Lv2Log::error("Audio processing terminated unexpectedly."); @@ -1509,7 +1779,7 @@ public: } else if (command == RingBufferCommand::SendVuUpdate) { - const std::vector *updates = nullptr; + const std::vector *updates = nullptr; hostReader.read(&updates); if (this->pNotifyCallbacks) @@ -1564,7 +1834,7 @@ public: if (property != nullptr && value != nullptr && property->type == uris.atom_URID) { LV2_URID propertyUrid = ((LV2_Atom_URID *)property)->body; - if (this->pNotifyCallbacks) + if (this->pNotifyCallbacks) { this->pNotifyCallbacks->OnPatchSetReply(instanceId, propertyUrid, value); } @@ -1603,7 +1873,6 @@ public: { OnActivePedalboardReleased(body.auxInserts); } - } else if (command == RingBufferCommand::FreeSnapshot) { @@ -1743,16 +2012,8 @@ public: } isOpen = true; - if (jackServerSettings.IsDummyAudioDevice()) - { - this->isDummyAudioDriver = true; - this->audioDriver = std::unique_ptr(CreateDummyAudioDriver(this, jackServerSettings.GetAlsaInputDevice())); - } - else - { - this->isDummyAudioDriver = false; - this->audioDriver = std::unique_ptr(CreateAlsaDriver(this)); - } + this->isDummyAudioDriver = jackServerSettings.IsDummyAudioDevice(); + this->audioDriver = std::unique_ptr(CreateAlsaDriver(this)); this->currentSample = 0; this->underruns = 0; @@ -1839,24 +2100,23 @@ public: } virtual void SetChannelRoutingInserts( - const std::shared_ptr &mainInserts, - const std::shared_ptr &auxInserts - ) override { - if (active && mainInserts) + const std::shared_ptr &mainInserts, + const std::shared_ptr &auxInserts) override + { + if (active && mainInserts) { mainInserts->Activate(); this->activePedalboards.push_back(mainInserts); - } - if (active && auxInserts) + } + if (active && auxInserts) { auxInserts->Activate(); this->activePedalboards.push_back(auxInserts); } - Lv2RoutingInserts routingInserts { mainInserts: mainInserts.get(), auxInserts: auxInserts.get()}; + Lv2RoutingInserts routingInserts{mainInserts : mainInserts.get(), auxInserts : auxInserts.get()}; hostWriter.ReplaceRoutingInserts(routingInserts); } - virtual void SetBypass(uint64_t instanceId, bool enabled) { std::lock_guard guard(mutex); @@ -1871,7 +2131,6 @@ public: } } - virtual void SetPluginPreset(uint64_t instanceId, const std::vector &values) { std::lock_guard guard(mutex); @@ -1942,7 +2201,6 @@ public: virtual void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) override; - void OnNotifyPathPatchPropertyReceived( int64_t instanceId, const std::string &pathPatchPropertyUri, @@ -1975,45 +2233,63 @@ public: PIPEDAL_NON_INLINE RealtimePedalboardItemIndex GetRealtimeItemIndex(int64_t instanceId) { - Pedalboard::PedalboardType instanceType = Pedalboard::GetInstanceTypeFromInstanceId(instanceId); + PedalboardType instanceType = Pedalboard::GetPedalboardTypeFromInstanceId(instanceId); int64_t index = -1; switch (instanceType) { - case Pedalboard::PedalboardType::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); - } + case PedalboardType::MainPedalboard: + if (this->currentPedalboard) + { + if (instanceId == Pedalboard::START_CONTROL_ID) + { + index = Pedalboard::START_CONTROL_ID; } - break; - case Pedalboard::PedalboardType::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::PedalboardType::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); - } + else if (instanceId == Pedalboard::END_CONTROL_ID) + { + index = Pedalboard::END_CONTROL_ID; } - break; + else + { + index = this->currentPedalboard->GetIndexOfInstanceId(instanceId); + } + } + break; + case PedalboardType::MainInserts: + 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 PedalboardType::AuxInserts: + 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}; + RealtimePedalboardItemIndex result{instanceType, index}; return result; } virtual void SetVuSubscriptions(const std::vector &instanceIds) @@ -2034,54 +2310,79 @@ public: for (size_t i = 0; i < instanceIds.size(); ++i) { int64_t instanceId = instanceIds[i]; - auto effect = this->currentPedalboard->GetEffect(instanceId); - if (effect) + PedalboardType pedalboardType = Pedalboard::GetPedalboardTypeFromInstanceId(instanceId); + std::shared_ptr pedalboard; + switch (pedalboardType) { - RealtimePedalboardItemIndex index = this->GetRealtimeItemIndex(instanceIds[i]); + case PedalboardType::MainPedalboard: + pedalboard = this->currentPedalboard; + break; + case PedalboardType::MainInserts: + pedalboard = this->currentMainInsertPedalboard; + break; + case PedalboardType::AuxInserts: + pedalboard = this->currentAuxInsertPedalboard; + break; + default: + throw std::runtime_error("SetVuSubscriptions: Value out of range."); + break; + } + int64_t effectIndex = -1; + if (pedalboard != nullptr) + { + effectIndex = pedalboard->GetIndexOfInstanceId(instanceId); + } + + if (effectIndex != -1) + { + IEffect *effect = pedalboard->GetEffect(instanceId); + RealtimePedalboardItemIndex index = RealtimePedalboardItemIndex(pedalboardType, effectIndex); vuConfig->enabledIndexes.push_back(index); - VuUpdate v; + VuUpdateX v; + v.pedalboardType(pedalboardType); v.instanceId_ = instanceId; // Display mono VUs if a stereo device is being fed identical L/R inputs. - v.isStereoInput_ = effect->GetNumberOfInputAudioBuffers() >= 2 - && effect->GetAudioInputBuffer(0) != effect->GetAudioInputBuffer(1); + v.isStereoInput_ = effect->GetNumberOfInputAudioBuffers() >= 2 && effect->GetAudioInputBuffer(0) != effect->GetAudioInputBuffer(1); v.isStereoOutput_ = effect->GetNumberOfOutputAudioBuffers() >= 2; vuConfig->vuUpdateWorkingData.push_back(v); vuConfig->vuUpdateResponseData.push_back(v); } 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 - ) + 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) { auto index = GetRealtimeItemIndex(instanceId); - VuUpdate v; + VuUpdateX v; vuConfig->enabledIndexes.push_back(index); - + v.instanceId_ = instanceId; size_t nChannels = 0; - switch (instanceId) + switch (instanceId) { - 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; + 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); @@ -2094,10 +2395,25 @@ public: } } + Lv2Pedalboard *GetPedalboard(PedalboardType pedalboardType) + { + switch (pedalboardType) + { + case PedalboardType::MainPedalboard: + return this->currentPedalboard.get(); + case PedalboardType::MainInserts: + return this->realtimeRoutingInserts.mainInserts; + case PedalboardType::AuxInserts: + return this->realtimeRoutingInserts.auxInserts; + default: + throw std::runtime_error("AudioHostImpl::GetPedalboard: invalid argument."); + } + } RealtimeMonitorPortSubscription MakeRealtimeSubscription(const MonitorPortSubscription &subscription) { RealtimeMonitorPortSubscription result; result.subscriptionHandle = subscription.subscriptionHandle; + result.pedalboardType = Pedalboard::GetPedalboardTypeFromInstanceId(subscription.subscriptionHandle); result.instanceIndex = this->currentPedalboard->GetIndexOfInstanceId(subscription.instanceid); IEffect *pEffect = this->currentPedalboard->GetEffect(subscription.instanceid); @@ -2459,8 +2775,6 @@ void AudioHostImpl::SetAlsaSequencerConfiguration(const AlsaSequencerConfigurati this->alsaSequencer->SetConfiguration(alsaSequencerConfiguration); } - - JSON_MAP_BEGIN(JackHostStatus) JSON_MAP_REFERENCE(JackHostStatus, active) JSON_MAP_REFERENCE(JackHostStatus, errorMessage) diff --git a/src/AudioHost.hpp b/src/AudioHost.hpp index 843e0be..4c78e4f 100644 --- a/src/AudioHost.hpp +++ b/src/AudioHost.hpp @@ -148,6 +148,7 @@ namespace pipedal { public: int64_t subscriptionHandle; + PedalboardType pedalboardType; int64_t instanceid; std::string key; float updateInterval; @@ -159,7 +160,7 @@ namespace pipedal public: virtual void OnNotifyLv2StateChanged(uint64_t instanceId) = 0; virtual bool OnNotifyMaybeLv2StateChanged(uint64_t instanceId) = 0; - virtual void OnNotifyVusSubscription(const std::vector &updates) = 0; + virtual void OnNotifyVusSubscription(const std::vector &updates) = 0; virtual void OnNotifyMonitorPort(const MonitorPortUpdate &update) = 0; virtual void OnNotifyMidiValueChanged(int64_t instanceId, int portIndex, float value) = 0; virtual void OnNotifyMidiListen(uint8_t cc0, uint8_t cc1, uint8_t cc2) = 0; diff --git a/src/Banks.hpp b/src/Banks.hpp index cc24fce..3c793d1 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::PedalboardType::MainPedalboard); + Pedalboard pedalboard = Pedalboard::MakeDefault(PedalboardType::MainPedalboard); this->addPreset(pedalboard); newSelection = presets_[0]->instanceId(); } diff --git a/src/ChannelRouterSettings.cpp b/src/ChannelRouterSettings.cpp index 926b589..675a9d5 100644 --- a/src/ChannelRouterSettings.cpp +++ b/src/ChannelRouterSettings.cpp @@ -129,8 +129,8 @@ void ChannelSelection::normalizeChannelSelection() { ChannelRouterSettings::ChannelRouterSettings() -: mainInserts_(Pedalboard::PedalboardType::MainInsert) -, auxInserts_(Pedalboard::PedalboardType::AuxInsert) +: mainInserts_(PedalboardType::MainInserts) +, auxInserts_(PedalboardType::AuxInserts) { } diff --git a/src/ChannelRouterSettings.hpp b/src/ChannelRouterSettings.hpp index 15af777..6ab621f 100644 --- a/src/ChannelRouterSettings.hpp +++ b/src/ChannelRouterSettings.hpp @@ -33,6 +33,7 @@ namespace pipedal 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; bool changed_ = false; @@ -51,6 +52,10 @@ namespace pipedal std::vector controlValues_; public: + + static constexpr int64_t MAIN_OUT_LEFT_CHANNEL = -2; + static constexpr int64_t MAIN_OUT_RIGHT_CHANNEL = -3; + using self = ChannelRouterSettings; using ptr = std::shared_ptr; diff --git a/src/DeviceVus.hpp b/src/DeviceVus.hpp new file mode 100644 index 0000000..4f192ec --- /dev/null +++ b/src/DeviceVus.hpp @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026 Robin E. R. Davies + * All rights reserved. + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once +#include +#include "json.hpp" + + +namespace pipedal { + class RealtimeDeviceVus { + public: + static constexpr size_t MAX_CHANNELS = 64; + size_t inputChannels; + size_t outputChannels; + float values[MAX_CHANNELS*2]; + + size_t MessageSize() const { + return sizeof(RealtimeDeviceVus)-sizeof(float)*( + MAX_CHANNELS*2- + (inputChannels+outputChannels+1)/2*2); + } + }; +}; \ No newline at end of file diff --git a/src/DummyAudioDriver.cpp b/src/DummyAudioDriver.cpp index 0d298d7..a52c21b 100644 --- a/src/DummyAudioDriver.cpp +++ b/src/DummyAudioDriver.cpp @@ -58,6 +58,8 @@ using namespace pipedal; +#ifdef JUNK + namespace pipedal { @@ -524,3 +526,4 @@ uint32_t pipedal::GetDummyAudioChannels(const std::string &deviceName) } +#endif \ No newline at end of file diff --git a/src/DummyAudioDriver.hpp b/src/DummyAudioDriver.hpp index 59d5f09..59b2c9b 100644 --- a/src/DummyAudioDriver.hpp +++ b/src/DummyAudioDriver.hpp @@ -24,6 +24,7 @@ #pragma once +#ifdef JUNK #include #include "AudioDriver.hpp" #include "JackServerSettings.hpp" @@ -36,4 +37,5 @@ namespace pipedal { AudioDriver* CreateDummyAudioDriver(AudioDriverHost*driverHost,const std::string&deviceId); } +#endif diff --git a/src/JackConfiguration.cpp b/src/JackConfiguration.cpp index 2298e70..cd111f3 100644 --- a/src/JackConfiguration.cpp +++ b/src/JackConfiguration.cpp @@ -130,12 +130,8 @@ void JackConfiguration::AlsaInitialize( this->isValid_ = false; this->errorStatus_ = ""; - if (jackServerSettings.IsDummyAudioDevice()) - { - this->inputMidiDevices_.clear(); - } else { - this->inputMidiDevices_ = GetAlsaSequencers(); // NB: Sequencers, not rawmidi devices, anymore. - } + this->inputMidiDevices_ = GetAlsaSequencers(); // NB: Sequencers, not rawmidi devices, anymore. + if (jackServerSettings.IsValid()) { this->blockLength_ = jackServerSettings.GetBufferSize(); diff --git a/src/JackServerSettings.cpp b/src/JackServerSettings.cpp index 74830ac..15ce110 100644 --- a/src/JackServerSettings.cpp +++ b/src/JackServerSettings.cpp @@ -155,6 +155,8 @@ void JackServerSettings::FixUpDeviceNames() } } + + JSON_MAP_BEGIN(JackServerSettings) JSON_MAP_REFERENCE(JackServerSettings, valid) JSON_MAP_REFERENCE(JackServerSettings, isOnboarding) diff --git a/src/JackServerSettings.hpp b/src/JackServerSettings.hpp index d1c278d..dfcc0c2 100644 --- a/src/JackServerSettings.hpp +++ b/src/JackServerSettings.hpp @@ -60,7 +60,7 @@ namespace pipedal { FixUpDeviceNames(); } - + uint64_t GetSampleRate() const { return sampleRate_; } uint32_t GetBufferSize() const { return bufferSize_; } @@ -78,17 +78,17 @@ namespace pipedal void UseDummyAudioDevice() { this->valid_ = true; if (sampleRate_ == 0) sampleRate_ = 48000; - this->alsaDevice_ = "dummy:channels_2"; - this->alsaInputDevice_ = "dummy:channels_2"; - this->alsaInputDeviceName_ = "Stopped"; - this->alsaOutputDevice_ = "dummy:channels_2"; - this->alsaOutputDeviceName_ = "Stopped"; + bufferSize_ = 256; + numberOfBuffers_ = 6; + + this->alsaDevice_ = ""; + this->alsaInputDevice_ = "null"; + this->alsaInputDeviceName_ = "null"; + this->alsaOutputDevice_ = "null"; + this->alsaOutputDeviceName_ = "null"; } bool IsDummyAudioDevice() const { - return - this->alsaDevice_.starts_with("__DUMMY_AUDIO__") - || this->alsaDevice_.starts_with("dummy:") - || this->alsaInputDevice_.starts_with("dummy:"); + return alsaInputDevice_ == "null" && alsaOutputDevice_ == "null"; } bool IsValid() const { return valid_; } diff --git a/src/Lv2Pedalboard.cpp b/src/Lv2Pedalboard.cpp index ad2b5cf..090884f 100644 --- a/src/Lv2Pedalboard.cpp +++ b/src/Lv2Pedalboard.cpp @@ -29,6 +29,7 @@ #include "Lv2Log.hpp" #include "CrashGuard.hpp" #include "restrict.hpp" +#include "AudioDriver.hpp" using namespace pipedal; @@ -571,42 +572,46 @@ void Lv2Pedalboard::SetBypass(int effectIndex, bool enabled) effect->SetBypass(enabled); } -void Lv2Pedalboard::ComputeVus(RealtimeVuBuffers *vuConfiguration, uint32_t samples, float **inputBuffers, float **outputBuffers) + +void Lv2Pedalboard::ComputeVus(RealtimeVuBuffers *realtimeVuBuffers, uint32_t samples, float**masterInputBuffers, float**masterOutputBuffers) { - for (size_t i = 0; i < vuConfiguration->enabledIndexes.size(); ++i) + if (realtimeVuBuffers == nullptr) { - auto& rtIndex = vuConfiguration->enabledIndexes[i]; + return; + } + for (size_t i = 0; i < realtimeVuBuffers->enabledIndexes.size(); ++i) + { + auto& rtIndex = realtimeVuBuffers->enabledIndexes[i]; if (rtIndex.instanceType != this->pedalboardType) { continue; } int index = rtIndex.index; - VuUpdate *pUpdate = &vuConfiguration->vuUpdateWorkingData[i]; + VuUpdateX *pUpdate = &realtimeVuBuffers->vuUpdateWorkingData[i]; if (index == Pedalboard::START_CONTROL_ID) { - if (this->pedalboardInputBuffers.size() > 1) + if (this->pedalboardInputBuffers.size() == 2) { - GetInputBuffers(); - pUpdate->AccumulateInputs(inputBuffers[0], inputBuffers[1], samples); + pUpdate->AccumulateInputs(masterInputBuffers[0], masterInputBuffers[1], samples); pUpdate->AccumulateOutputs(&(this->pedalboardInputBuffers[0][0]), &(this->pedalboardInputBuffers[1][0]), samples); // after outputVolume applied. } - else + else if (this->pedalboardInputBuffers.size() == 1) { - pUpdate->AccumulateInputs(inputBuffers[0], samples); + pUpdate->AccumulateInputs(masterInputBuffers[0], samples); pUpdate->AccumulateOutputs(&(this->pedalboardInputBuffers[0][0]), samples); // after input volume applied. } } else if (index == Pedalboard::END_CONTROL_ID) { - if (this->pedalboardOutputBuffers.size() > 1) + if (this->pedalboardOutputBuffers.size() == 2) { pUpdate->AccumulateInputs(&(this->pedalboardOutputBuffers[0][0]), &(this->pedalboardOutputBuffers[1][0]), samples); - pUpdate->AccumulateOutputs(outputBuffers[0], outputBuffers[1], samples); + pUpdate->AccumulateOutputs(masterOutputBuffers[0], masterOutputBuffers[1], samples); } - else + else if (this->pedalboardOutputBuffers.size() == 1) { pUpdate->AccumulateInputs(&(this->pedalboardOutputBuffers[0][0]), samples); - pUpdate->AccumulateOutputs(outputBuffers[0], samples); + pUpdate->AccumulateOutputs(masterOutputBuffers[0], samples); } } else @@ -1029,11 +1034,11 @@ void Lv2Pedalboard::handleTapTempo(uint8_t value, const MidiTimestamp& timestamp size_t Lv2Pedalboard::GetNumberOfAudioInputChannels() const { switch (pedalboardType) { - case Pedalboard::PedalboardType::MainPedalboard: + case PedalboardType::MainPedalboard: return pHost->GetChannelSelection().mainInputChannels().size(); - case Pedalboard::PedalboardType::MainInsert: + case PedalboardType::MainInserts: return pHost->GetChannelSelection().mainOutputChannels().size(); - case Pedalboard::PedalboardType::AuxInsert: + case PedalboardType::AuxInserts: return pHost->GetChannelSelection().auxInputChannels().size(); default: throw std::runtime_error("Invalid argument"); @@ -1043,11 +1048,11 @@ size_t Lv2Pedalboard::GetNumberOfAudioInputChannels() const { size_t Lv2Pedalboard::GetNumberOfAudioOutputChannels() const { switch (pedalboardType) { - case Pedalboard::PedalboardType::MainPedalboard: + case PedalboardType::MainPedalboard: return pHost->GetChannelSelection().mainOutputChannels().size(); - case Pedalboard::PedalboardType::MainInsert: + case PedalboardType::MainInserts: return pHost->GetChannelSelection().mainOutputChannels().size(); - case Pedalboard::PedalboardType::AuxInsert: + case PedalboardType::AuxInserts: return pHost->GetChannelSelection().auxOutputChannels().size(); default: throw std::runtime_error("Invalid argument"); diff --git a/src/Lv2Pedalboard.hpp b/src/Lv2Pedalboard.hpp index 620c035..b478c14 100644 --- a/src/Lv2Pedalboard.hpp +++ b/src/Lv2Pedalboard.hpp @@ -31,6 +31,7 @@ namespace pipedal { + class AudioDriver; class IPatchWriterCallback; class RealtimeVuBuffers; class RealtimePatchPropertyRequest; @@ -51,7 +52,7 @@ namespace pipedal class Lv2Pedalboard { private: - Pedalboard::PedalboardType pedalboardType; + PedalboardType pedalboardType; IHost *pHost = nullptr; size_t currentFrameOffset = 0; DbDezipper inputVolume; @@ -173,7 +174,7 @@ namespace pipedal void SetOutputVolume(float value) { this->outputVolume.SetTarget(value); } void SetBypass(int effectIndex, bool enabled); - void ComputeVus(RealtimeVuBuffers *vuConfiguration, uint32_t samples, float **inputBuffers, float **outputBuffers); + void ComputeVus(RealtimeVuBuffers *vuConfiguration, uint32_t samples, float**masterInputBuffers, float **masterOutputBuffers); float GetControlOutputValue(int effectIndex, int portIndex); diff --git a/src/Pedalboard.cpp b/src/Pedalboard.cpp index 6a3f0fb..6ddcce3 100644 --- a/src/Pedalboard.cpp +++ b/src/Pedalboard.cpp @@ -517,10 +517,10 @@ Pedalboard::Pedalboard(PedalboardType instanceType) case PedalboardType::MainPedalboard: nextInstanceId_ = 0; break; - case PedalboardType::MainInsert: + case PedalboardType::MainInserts: nextInstanceId_ = MAIN_INSERT_INSTANCE_BASE;; break; - case PedalboardType::AuxInsert: + case PedalboardType::AuxInserts: nextInstanceId_ = AUX_INSERT_INSTANCE_BASE;; break; } diff --git a/src/Pedalboard.hpp b/src/Pedalboard.hpp index df7f425..66a023f 100644 --- a/src/Pedalboard.hpp +++ b/src/Pedalboard.hpp @@ -25,13 +25,14 @@ #include "StateInterface.hpp" #include "atom_object.hpp" -namespace pipedal { +namespace pipedal +{ class SnapshotValue; class Snapshot; class PluginHost; - -#define SPLIT_PEDALBOARD_ITEM_URI "uri://two-play/pipedal/pedalboard#Split" -#define EMPTY_PEDALBOARD_ITEM_URI "uri://two-play/pipedal/pedalboard#Empty" + +#define SPLIT_PEDALBOARD_ITEM_URI "uri://two-play/pipedal/pedalboard#Split" +#define EMPTY_PEDALBOARD_ITEM_URI "uri://two-play/pipedal/pedalboard#Empty" #define SPLIT_SPLITTYPE_KEY "splitType" #define SPLIT_SELECT_KEY "select" @@ -41,280 +42,284 @@ namespace pipedal { #define SPLIT_PANR_KEY "panR" #define SPLIT_VOLR_KEY "volR" - -#define GETTER_SETTER_REF(name) \ - const decltype(name##_)& name() const { return name##_;} \ +#define GETTER_SETTER_REF(name) \ + const decltype(name##_) &name() const { return name##_; } \ void name(const decltype(name##_) &value) { name##_ = value; } -#define GETTER_SETTER_VEC(name) \ - decltype(name##_)& name() { return name##_;} \ - const decltype(name##_)& name() const { return name##_;} \ - void name(decltype(name##_)&&value) { this->name##_ = std::move(value); }\ - void name(const decltype(name##_)&value) { this->name##_ = value; } +#define GETTER_SETTER_VEC(name) \ + decltype(name##_) &name() { return name##_; } \ + const decltype(name##_) &name() const { return name##_; } \ + void name(decltype(name##_) &&value) { this->name##_ = std::move(value); } \ + void name(const decltype(name##_) &value) { this->name##_ = value; } - - -#define GETTER_SETTER(name) \ - decltype(name##_) name() const { return name##_;} \ +#define GETTER_SETTER(name) \ + decltype(name##_) name() const { return name##_; } \ void name(decltype(name##_) value) { name##_ = value; } -class ControlValue { -private: - std::string key_; - float value_; -public: - ControlValue() + class ControlValue { + private: + std::string key_; + float value_; - } - ControlValue(const char*key, float value) - :key_(key) - , value_(value) - { - - } - ControlValue(const std::string&key, float value) - :key_(key) - , value_(value) - { - - } - GETTER_SETTER_REF(key) - GETTER_SETTER_REF(value) - - DECLARE_JSON_MAP(ControlValue); - - -}; - -class PedalboardItem { -public: - using PropertyMap = std::map; - int64_t instanceId_ = 0; - std::string uri_; - std::string pluginName_; - bool isEnabled_ = true; - std::vector controlValues_; - std::vector topChain_; - std::vector bottomChain_; - std::vector midiBindings_; - std::optional midiChannelBinding_; - std::string vstState_; - uint32_t stateUpdateCount_ = 0; - Lv2PluginState lv2State_; - std::string lilvPresetUri_; - std::map pathProperties_; - std::string title_; - bool useModUi_ = false; - std::string iconColor_; - int64_t sideChainInputId_ = -1; - - // non persistent state. - PropertyMap patchProperties; -public: - ControlValue*GetControlValue(const std::string&symbol); - const ControlValue*GetControlValue(const std::string&symbol) const; - bool SetControlValue(const std::string&key, float value); - - bool IsStructurallyIdentical(const PedalboardItem&other) const; - - void ApplySnapshotValue(SnapshotValue*snapshotValue); - void ApplyDefaultValues(PluginHost&pluginHost); - bool hasLv2State() const { - return lv2State_.isValid_ != 0; - } - GETTER_SETTER(instanceId) - GETTER_SETTER_REF(uri) - GETTER_SETTER_REF(vstState); - GETTER_SETTER_REF(pluginName) - GETTER_SETTER(isEnabled) - GETTER_SETTER_VEC(controlValues) - GETTER_SETTER_VEC(topChain) - GETTER_SETTER_VEC(bottomChain) - GETTER_SETTER_VEC(midiBindings) - GETTER_SETTER_REF(midiChannelBinding) - GETTER_SETTER_REF(pathProperties) - GETTER_SETTER(stateUpdateCount) - GETTER_SETTER_REF(lv2State) - GETTER_SETTER_REF(title) - GETTER_SETTER_REF(iconColor) - GETTER_SETTER(useModUi) - GETTER_SETTER(sideChainInputId) - - Lv2PluginState&lv2State() { return lv2State_; } // non-const version. - GETTER_SETTER_REF(lilvPresetUri) - - PropertyMap&PatchProperties() { return patchProperties; } - const PropertyMap&PatchProperties() const { return patchProperties; } - - - bool isSplit() const - { - return uri_ == SPLIT_PEDALBOARD_ITEM_URI; - } - bool isEmpty() const { - return uri_ == EMPTY_PEDALBOARD_ITEM_URI; - } - - void AddToSnapshotFromCurrentSettings(Snapshot&snapshot) const; - void AddResetsForMissingProperties(Snapshot&snapshot, size_t*index) const; - - - // virtual void write_members(json_writer&writer) const { - // writer.write_member("instanceId",instanceId_); - // writer.write_member("uri",uri_); - // writer.write_member("pluginName",pluginName_); - // writer.write_member("isEnabled",isEnabled_); - // if (isSplit()) - // { - // writer.write_member("topChain",topChain_); - // writer.write_member("bottomChain",bottomChain_); - // } - // } - - DECLARE_JSON_MAP(PedalboardItem); - -}; - -class SnapshotValue { -public: - uint64_t instanceId_; - bool isEnabled_ = true; - std::vector controlValues_; - Lv2PluginState lv2State_; - std::map pathProperties_; - DECLARE_JSON_MAP(SnapshotValue); - - ControlValue*GetControlValue(const std::string& key) { - for (ControlValue&controlValue: controlValues_) { - if (controlValue.key() == key) { - return &controlValue; - } + public: + ControlValue() + { } - return nullptr; - } - void SetControlValue(const std::string& key,float value) { - for (ControlValue&controlValue: controlValues_) { - if (controlValue.key() == key) { - controlValue.value(value); - return; - } + ControlValue(const char *key, float value) + : key_(key), value_(value) + { } - controlValues_.push_back(ControlValue{key,value}); - } - -}; + ControlValue(const std::string &key, float value) + : key_(key), value_(value) + { + } + GETTER_SETTER_REF(key) + GETTER_SETTER_REF(value) -class Snapshot { -public: - std::string name_; - std::string color_; - bool isModified_ = false; - std::vector values_; - - DECLARE_JSON_MAP(Snapshot); - -}; - -class Pedalboard { - std::string name_; - float input_volume_db_ = 0; - float output_volume_db_ = 0; - - std::vector items_; - uint64_t nextInstanceId_ = 0; - uint64_t NextInstanceId() { return ++nextInstanceId_; } - - std::vector> snapshots_; - int64_t selectedSnapshot_ = -1; - - 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 PedalboardType { - MainPedalboard, - MainInsert, - AuxInsert + DECLARE_JSON_MAP(ControlValue); }; - 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. + class PedalboardItem + { + public: + using PropertyMap = std::map; + int64_t instanceId_ = 0; + std::string uri_; + std::string pluginName_; + bool isEnabled_ = true; + std::vector controlValues_; + std::vector topChain_; + std::vector bottomChain_; + std::vector midiBindings_; + std::optional midiChannelBinding_; + std::string vstState_; + uint32_t stateUpdateCount_ = 0; + Lv2PluginState lv2State_; + std::string lilvPresetUri_; + std::map pathProperties_; + std::string title_; + bool useModUi_ = false; + std::string iconColor_; + int64_t sideChainInputId_ = -1; - 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; + // non persistent state. + PropertyMap patchProperties; - static PedalboardType GetInstanceTypeFromInstanceId(int64_t instanceId) { - if (instanceId == START_CONTROL_ID) return PedalboardType::MainPedalboard; - if (instanceId == END_CONTROL_ID) return PedalboardType::MainPedalboard; + public: + ControlValue *GetControlValue(const std::string &symbol); + const ControlValue *GetControlValue(const std::string &symbol) const; + bool SetControlValue(const std::string &key, float value); - if (instanceId >= MAIN_INSERT_INSTANCE_BASE) { - return PedalboardType::MainInsert; - } else if (instanceId >= AUX_INSERT_INSTANCE_BASE) { - return PedalboardType::AuxInsert; - } else { - return PedalboardType::MainPedalboard; + bool IsStructurallyIdentical(const PedalboardItem &other) const; + + void ApplySnapshotValue(SnapshotValue *snapshotValue); + void ApplyDefaultValues(PluginHost &pluginHost); + bool hasLv2State() const + { + return lv2State_.isValid_ != 0; } - } - PedalboardType GetInstanceType() const { - return GetInstanceTypeFromInstanceId(this->nextInstanceId_); // instanceId is irrelevant here. - } + GETTER_SETTER(instanceId) + GETTER_SETTER_REF(uri) + GETTER_SETTER_REF(vstState); + GETTER_SETTER_REF(pluginName) + GETTER_SETTER(isEnabled) + GETTER_SETTER_VEC(controlValues) + GETTER_SETTER_VEC(topChain) + GETTER_SETTER_VEC(bottomChain) + GETTER_SETTER_VEC(midiBindings) + GETTER_SETTER_REF(midiChannelBinding) + GETTER_SETTER_REF(pathProperties) + GETTER_SETTER(stateUpdateCount) + GETTER_SETTER_REF(lv2State) + GETTER_SETTER_REF(title) + GETTER_SETTER_REF(iconColor) + GETTER_SETTER(useModUi) + GETTER_SETTER(sideChainInputId) + Lv2PluginState &lv2State() { return lv2State_; } // non-const version. + GETTER_SETTER_REF(lilvPresetUri) + PropertyMap &PatchProperties() { return patchProperties; } + const PropertyMap &PatchProperties() const { return patchProperties; } - Pedalboard(PedalboardType instanceType = PedalboardType::MainPedalboard); + bool isSplit() const + { + return uri_ == SPLIT_PEDALBOARD_ITEM_URI; + } + bool isEmpty() const + { + return uri_ == EMPTY_PEDALBOARD_ITEM_URI; + } - // deep copy, breaking shared pointers. - Pedalboard DeepCopy(); - - 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); - bool SetItemUseModUi(int64_t pedalItemId, bool enabled); - void SetCurrentSnapshotModified(bool modified); + void AddToSnapshotFromCurrentSettings(Snapshot &snapshot) const; + void AddResetsForMissingProperties(Snapshot &snapshot, size_t *index) const; - bool IsStructureIdentical(const Pedalboard &other) const; // caan we just send a snapshot-style uddate instead of reloading plugins? All settings are ignored. - Snapshot MakeSnapshotFromCurrentSettings(const Pedalboard &previousPedalboard); + // virtual void write_members(json_writer&writer) const { + // writer.write_member("instanceId",instanceId_); + // writer.write_member("uri",uri_); + // writer.write_member("pluginName",pluginName_); + // writer.write_member("isEnabled",isEnabled_); + // if (isSplit()) + // { + // writer.write_member("topChain",topChain_); + // writer.write_member("bottomChain",bottomChain_); + // } + // } - PedalboardItem*GetItem(int64_t pedalItemId); - const PedalboardItem*GetItem(int64_t pedalItemId) const; - std::vectorGetAllPlugins(); + DECLARE_JSON_MAP(PedalboardItem); + }; - bool HasItem(int64_t pedalItemid) const { return GetItem(pedalItemid) != nullptr; } - bool ApplySnapshot(int64_t snapshotIndex, PluginHost &pluginHost); + class SnapshotValue + { + public: + uint64_t instanceId_; + bool isEnabled_ = true; + std::vector controlValues_; + Lv2PluginState lv2State_; + std::map pathProperties_; + DECLARE_JSON_MAP(SnapshotValue); - GETTER_SETTER_REF(name) - GETTER_SETTER_VEC(items) - GETTER_SETTER(input_volume_db) - GETTER_SETTER(output_volume_db) - GETTER_SETTER_VEC(snapshots) - GETTER_SETTER(selectedSnapshot) - GETTER_SETTER(selectedPlugin) + ControlValue *GetControlValue(const std::string &key) + { + for (ControlValue &controlValue : controlValues_) + { + if (controlValue.key() == key) + { + return &controlValue; + } + } + return nullptr; + } + void SetControlValue(const std::string &key, float value) + { + for (ControlValue &controlValue : controlValues_) + { + if (controlValue.key() == key) + { + controlValue.value(value); + return; + } + } + controlValues_.push_back(ControlValue{key, value}); + } + }; + class Snapshot + { + public: + std::string name_; + std::string color_; + bool isModified_ = false; + std::vector values_; - DECLARE_JSON_MAP(Pedalboard); + DECLARE_JSON_MAP(Snapshot); + }; - PedalboardItem MakeEmptyItem(); - PedalboardItem MakeSplit(); + enum class PedalboardType + { + Invalid = 0, + MainPedalboard = 1, + MainInserts = 2, + AuxInserts = 3 + }; + class Pedalboard + { + std::string name_; + float input_volume_db_ = 0; + float output_volume_db_ = 0; - static Pedalboard MakeDefault(Pedalboard::PedalboardType instanceType = Pedalboard::PedalboardType::MainPedalboard); -}; + std::vector items_; + uint64_t nextInstanceId_ = 0; + uint64_t NextInstanceId() { return ++nextInstanceId_; } + + std::vector> snapshots_; + int64_t selectedSnapshot_ = -1; + + 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: + 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 PedalboardType GetPedalboardTypeFromInstanceId(int64_t instanceId) + { + if (instanceId == START_CONTROL_ID) + return PedalboardType::MainPedalboard; + if (instanceId == END_CONTROL_ID) + return PedalboardType::MainPedalboard; + + if (instanceId >= MAIN_INSERT_INSTANCE_BASE) + { + return PedalboardType::MainInserts; + } + else if (instanceId >= AUX_INSERT_INSTANCE_BASE) + { + return PedalboardType::AuxInserts; + } + else + { + return PedalboardType::MainPedalboard; + } + } + PedalboardType GetInstanceType() const + { + return GetPedalboardTypeFromInstanceId(this->nextInstanceId_); // instanceId is irrelevant here. + } + + Pedalboard(PedalboardType instanceType = PedalboardType::MainPedalboard); + + // deep copy, breaking shared pointers. + Pedalboard DeepCopy(); + + 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); + bool SetItemUseModUi(int64_t pedalItemId, bool enabled); + void SetCurrentSnapshotModified(bool modified); + + bool IsStructureIdentical(const Pedalboard &other) const; // caan we just send a snapshot-style uddate instead of reloading plugins? All settings are ignored. + Snapshot MakeSnapshotFromCurrentSettings(const Pedalboard &previousPedalboard); + + PedalboardItem *GetItem(int64_t pedalItemId); + const PedalboardItem *GetItem(int64_t pedalItemId) const; + std::vector GetAllPlugins(); + + bool HasItem(int64_t pedalItemid) const { return GetItem(pedalItemid) != nullptr; } + bool ApplySnapshot(int64_t snapshotIndex, PluginHost &pluginHost); + + GETTER_SETTER_REF(name) + GETTER_SETTER_VEC(items) + GETTER_SETTER(input_volume_db) + GETTER_SETTER(output_volume_db) + GETTER_SETTER_VEC(snapshots) + GETTER_SETTER(selectedSnapshot) + GETTER_SETTER(selectedPlugin) + + DECLARE_JSON_MAP(Pedalboard); + + PedalboardItem MakeEmptyItem(); + PedalboardItem MakeSplit(); + + static Pedalboard MakeDefault(PedalboardType instanceType = PedalboardType::MainPedalboard); + }; #undef GETTER_SETTER_REF #undef GETTER_SETTER_VEC #undef GETTER_SETTER - - } // namespace pipedal \ No newline at end of file diff --git a/src/PiLatencyMain.cpp b/src/PiLatencyMain.cpp index d8deec9..1ad385a 100644 --- a/src/PiLatencyMain.cpp +++ b/src/PiLatencyMain.cpp @@ -169,6 +169,10 @@ public: delete[] inputBuffers; delete[] outputBuffers; } + virtual bool OnRealtimeUpdateDeviceVus(size_t nFrames) { + return true; + } + std::vector SelectChannels(const std::vector &available, const std::vector &selection) { std::vector result; diff --git a/src/PiPedalCommon.hpp b/src/PiPedalCommon.hpp index a464e39..3a8e138 100644 --- a/src/PiPedalCommon.hpp +++ b/src/PiPedalCommon.hpp @@ -30,3 +30,13 @@ #define PIPEDAL_NON_INLINE #endif #endif + +#ifndef PIPEDAL_RESTRICT +#if defined(__GNUC__) || defined(__clang__) +#define PIPEDAL_RESTRICT __restrict__ +#elif defined(_MSC_VER) +#define PIPEDAL_RESTRICT __restrict +#else +#define PIPEDAL_RESTRICT +#endif +#endif diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index 1041dbe..5c256ae 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -82,7 +82,7 @@ PiPedalModel::PiPedalModel() this->updater = Updater::Create(); this->updater->Start(); this->currentUpdateStatus = updater->GetCurrentStatus(); - this->pedalboard = Pedalboard::MakeDefault(Pedalboard::PedalboardType::MainPedalboard); + this->pedalboard = Pedalboard::MakeDefault(PedalboardType::MainPedalboard); this->jackServerSettings = this->storage.GetJackServerSettings(); @@ -394,7 +394,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(Pedalboard::PedalboardType::MainPedalboard); + this->pedalboard = Pedalboard::MakeDefault(PedalboardType::MainPedalboard); } else { @@ -1732,7 +1732,7 @@ void PiPedalModel::OnNotifyMidiValueChanged(int64_t instanceId, int portIndex, f } } -void PiPedalModel::OnNotifyVusSubscription(const std::vector &updates) +void PiPedalModel::OnNotifyVusSubscription(const std::vector &updates) { std::lock_guard lock(mutex); for (size_t i = 0; i < updates.size(); ++i) @@ -1746,6 +1746,22 @@ void PiPedalModel::OnNotifyVusSubscription(const std::vector &updates) } } +static bool isStartOrEndControl(int64_t controlId) +{ + switch (controlId) + { + case Pedalboard::START_CONTROL_ID: + case Pedalboard::END_CONTROL_ID: + case Pedalboard::MAIN_INSERT_START_CONTROL_ID: + case Pedalboard::MAIN_INSERT_END_CONTROL_ID: + case Pedalboard::AUX_INSERT_START_CONTROL_ID: + case Pedalboard::AUX_INSERT_END_CONTROL_ID: + return true; + default: + return false; + } +} + void PiPedalModel::UpdateRealtimeVuSubscriptions() { std::set addedInstances; @@ -1753,7 +1769,7 @@ void PiPedalModel::UpdateRealtimeVuSubscriptions() for (int i = 0; i < activeVuSubscriptions.size(); ++i) { auto instanceId = activeVuSubscriptions[i].instanceid; - if (pedalboard.HasItem(instanceId) || instanceId == Pedalboard::START_CONTROL_ID || instanceId == Pedalboard::END_CONTROL_ID) + if (pedalboard.HasItem(instanceId) || isStartOrEndControl(instanceId)) { addedInstances.insert(activeVuSubscriptions[i].instanceid); } @@ -1778,8 +1794,15 @@ int64_t PiPedalModel::MonitorPort(int64_t instanceId, const std::string &key, fl { std::lock_guard lock(mutex); int64_t subscriptionId = ++nextSubscriptionId; + PedalboardType pedalboardType = Pedalboard::GetPedalboardTypeFromInstanceId(instanceId); activeMonitorPortSubscriptions.push_back( - MonitorPortSubscription{subscriptionId, instanceId, key, updateInterval, onUpdate}); + MonitorPortSubscription{ + subscriptionId, + pedalboardType, + instanceId, + key, + updateInterval, + onUpdate}); UpdateRealtimeMonitorPortSubscriptions(); diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index 07203cf..107bb7e 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -75,7 +75,7 @@ namespace pipedal virtual void OnSelectedSnapshotChanged(int64_t selectedSnapshot) = 0; virtual void OnPluginPresetsChanged(const std::string &pluginUri) = 0; virtual void OnChannelRouterSettingsChanged(int64_t clientId, const ChannelRouterSettings &channelRouterSettings) = 0; - virtual void OnVuMeterUpdate(const std::vector &updates) = 0; + virtual void OnVuMeterUpdate(const std::vector &updates) = 0; virtual void OnBankIndexChanged(const BankIndex &bankIndex) = 0; virtual void OnJackServerSettingsChanged(const JackServerSettings &jackServerSettings) = 0; virtual void OnJackConfigurationChanged(const JackConfiguration &jackServerConfiguration) = 0; @@ -251,7 +251,7 @@ namespace pipedal private: // IAudioHostCallbacks virtual void OnNotifyLv2StateChanged(uint64_t instanceId) override; virtual bool OnNotifyMaybeLv2StateChanged(uint64_t instanceId) override; - virtual void OnNotifyVusSubscription(const std::vector &updates) override; + virtual void OnNotifyVusSubscription(const std::vector &updates) override; virtual void OnNotifyMonitorPort(const MonitorPortUpdate &update) override; virtual void OnNotifyMidiValueChanged(int64_t instanceId, int portIndex, float value) override; virtual void OnNotifyMidiListen(uint8_t cc0, uint8_t cc1, uint8_t cc2) override; diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index 2509d57..9d6457b 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -2371,15 +2371,15 @@ private: int updateRequestOutstanding = 0; bool vuUpdateDropped = false; - virtual void OnVuMeterUpdate(const std::vector &updates) + virtual void OnVuMeterUpdate(const std::vector &updates) { std::lock_guard guard(subscriptionMutex); - if (updateRequestOutstanding < 5) // throttle to accomodate a web page that can't keep up. + if (updateRequestOutstanding < 1) // throttle to accomodate a web page that can't keep up. { vuUpdateDropped = false; for (int i = 0; i < updates.size(); ++i) { - const VuUpdate &vuUpdate = updates[i]; + const VuUpdateX &vuUpdate = updates[i]; bool interested = false; for (int i = 0; i < this->activeVuSubscriptions.size(); ++i) { @@ -2392,7 +2392,7 @@ private: if (interested) { updateRequestOutstanding++; - this->Request( + this->Request( "onVuUpdate", vuUpdate, [this](const bool &result) diff --git a/src/RingBufferReader.hpp b/src/RingBufferReader.hpp index 5e684a9..4b8bbb9 100644 --- a/src/RingBufferReader.hpp +++ b/src/RingBufferReader.hpp @@ -106,11 +106,11 @@ namespace pipedal struct RealtimePedalboardItemIndex { RealtimePedalboardItemIndex() - : instanceType(Pedalboard::PedalboardType::MainPedalboard), index(-1) + : instanceType(PedalboardType::MainPedalboard), index(-1) { } RealtimePedalboardItemIndex( - Pedalboard::PedalboardType instanceType, + PedalboardType instanceType, int64_t index) : instanceType(instanceType), index(index) { @@ -118,7 +118,7 @@ namespace pipedal RealtimePedalboardItemIndex(const RealtimePedalboardItemIndex& other) = default; RealtimePedalboardItemIndex& operator=(const RealtimePedalboardItemIndex& other) = default; - Pedalboard::PedalboardType instanceType = Pedalboard::PedalboardType::MainPedalboard; + PedalboardType instanceType = PedalboardType::MainPedalboard; int64_t index = -1; }; @@ -150,6 +150,7 @@ namespace pipedal { public: RealtimeMonitorPortSubscription() { delete callbackPtr; } + PedalboardType pedalboardType = PedalboardType::MainPedalboard; int64_t subscriptionHandle; int instanceIndex = 0; int portIndex = 0; @@ -174,7 +175,7 @@ namespace pipedal } bool waitingForAcknowledge = false; - const std::vector *GetResult(size_t currentSample) + const std::vector *GetResult(size_t currentSample) { for (size_t i = 0; i < vuUpdateWorkingData.size(); ++i) { @@ -186,8 +187,8 @@ namespace pipedal } std::vector enabledIndexes; - std::vector vuUpdateWorkingData; - std::vector vuUpdateResponseData; + std::vector vuUpdateWorkingData; + std::vector vuUpdateResponseData; void Reset() { @@ -494,7 +495,7 @@ namespace pipedal write(RingBufferCommand::SendMonitorPortUpdate, body); } - void SendVuUpdate(const std::vector *pUpdates) + void SendVuUpdate(const std::vector *pUpdates) { write(RingBufferCommand::SendVuUpdate, pUpdates); } diff --git a/src/Storage.cpp b/src/Storage.cpp index 49bc39c..265dd70 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -472,7 +472,7 @@ void Storage::LoadBankIndex() if (bankIndex.entries().size() == 0) { currentBank.clear(); - Pedalboard defaultPedalboard = Pedalboard::MakeDefault(Pedalboard::PedalboardType::MainPedalboard); + Pedalboard defaultPedalboard = Pedalboard::MakeDefault(PedalboardType::MainPedalboard); int64_t instanceId = currentBank.addPreset(defaultPedalboard); currentBank.selectedPreset(instanceId); diff --git a/src/VuUpdate.cpp b/src/VuUpdate.cpp index da1c876..e322c2f 100644 --- a/src/VuUpdate.cpp +++ b/src/VuUpdate.cpp @@ -23,14 +23,15 @@ using namespace pipedal; -JSON_MAP_BEGIN(VuUpdate) - JSON_MAP_REFERENCE(VuUpdate,instanceId) - JSON_MAP_REFERENCE(VuUpdate,sampleTime) - JSON_MAP_REFERENCE(VuUpdate,isStereoInput) - JSON_MAP_REFERENCE(VuUpdate,isStereoOutput) - JSON_MAP_REFERENCE(VuUpdate,inputMaxValueL) - JSON_MAP_REFERENCE(VuUpdate,inputMaxValueR) - JSON_MAP_REFERENCE(VuUpdate,outputMaxValueL) - JSON_MAP_REFERENCE(VuUpdate,outputMaxValueR) +JSON_MAP_BEGIN(VuUpdateX) + JSON_MAP_REFERENCE(VuUpdateX,pedalboardType) + JSON_MAP_REFERENCE(VuUpdateX,instanceId) + JSON_MAP_REFERENCE(VuUpdateX,sampleTime) + JSON_MAP_REFERENCE(VuUpdateX,isStereoInput) + JSON_MAP_REFERENCE(VuUpdateX,isStereoOutput) + JSON_MAP_REFERENCE(VuUpdateX,inputMaxValueL) + JSON_MAP_REFERENCE(VuUpdateX,inputMaxValueR) + JSON_MAP_REFERENCE(VuUpdateX,outputMaxValueL) + JSON_MAP_REFERENCE(VuUpdateX,outputMaxValueR) JSON_MAP_END() diff --git a/src/VuUpdate.hpp b/src/VuUpdate.hpp index 277fa5b..7d0e81b 100644 --- a/src/VuUpdate.hpp +++ b/src/VuUpdate.hpp @@ -20,12 +20,14 @@ #pragma once #include "json.hpp" +#include "Pedalboard.hpp" namespace pipedal { - class VuUpdate + class VuUpdateX { public: + int pedalboardType_ = (int)PedalboardType::Invalid; int64_t instanceId_ = 0; long sampleTime_ = 0; bool isStereoInput_ = false; @@ -36,6 +38,9 @@ namespace pipedal float outputMaxValueR_ = 0; public: + + PedalboardType pedalboardType() const { return (PedalboardType)pedalboardType_; } + void pedalboardType(PedalboardType value) { pedalboardType_ = (int)value; } void reset() { inputMaxValueL_ = 0; inputMaxValueR_ = 0; @@ -45,7 +50,10 @@ namespace pipedal void AccumulateVu(float *value,float *input, uint32_t samples) { - + if (input == nullptr) { + *value = 0; + return; + } float v = *value; for (uint32_t i = 0; i < samples; ++i) { @@ -65,7 +73,6 @@ namespace pipedal { AccumulateVu(&inputMaxValueL_,inputL,samples); AccumulateVu(&inputMaxValueR_,inputR,samples); - } void AccumulateOutputs(float* output, uint32_t samples) { @@ -78,6 +85,6 @@ namespace pipedal AccumulateVu(&outputMaxValueR_,outputR,samples); } - DECLARE_JSON_MAP(VuUpdate); + DECLARE_JSON_MAP(VuUpdateX); }; } \ No newline at end of file diff --git a/vite/public/logo192.png b/vite/public/logo192.png index b5e5434d561dfbf7e8f17a52292a63e257752ccc..2bb4cca7156d51e9248a8a0ecf39005bb61fb3ce 100644 GIT binary patch delta 9894 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 JuMpPv`yU%MickOm delta 9732 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 KQZ*8$A^!&*nh#w7 diff --git a/vite/public/logo512.png b/vite/public/logo512.png index 9c7691a4b22783af1406f3dea05f45aef64bf4f1..99a73a15bd4654209d32078e6d8c3c892c97cb5a 100644 GIT binary patch literal 28337 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
Tpa5Vba10)G^e<_ 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 Mtcpyflu6M40cf&|S^xk5 diff --git a/vite/src/pipedal/ChannelRouterSettings.tsx b/vite/src/pipedal/ChannelRouterSettings.tsx index 3e65a42..3dc8c47 100644 --- a/vite/src/pipedal/ChannelRouterSettings.tsx +++ b/vite/src/pipedal/ChannelRouterSettings.tsx @@ -15,10 +15,24 @@ function isActiveChannel(channels: number[]): boolean { return channels[1] !== -1 || channels[0] !== -1; } -function chName(ch: number): string { +function chName(ch: number, maxChannels: number): string { if (ch === -1) { return "None"; } + if (ch === MAIN_OUT_LEFT_CHANNEL) { + return "Main Out L"; + } + if (ch === MAIN_OUT_RIGHT_CHANNEL) { + return "Main Out R"; + } + if (maxChannels <= 2) { + if (ch === 0) { + return "Left"; + } + if (ch === 1) { + return "Right"; + } + } return "Ch" + (ch + 1); } function channelPairName(channels: number[], maxChannels: number, input: boolean): string { @@ -39,17 +53,14 @@ function channelPairName(channels: number[], maxChannels: number, input: boolean if (channels[0] === 0 && (channels[0] === channels[1] || channels[1] === -1)) { return "Left"; } - if (channels[1] === 1 && (channels[0] === channels[1])) { + if (channels[1] === 1 && (channels[0] === channels[1] || channels[0] === -1)) { return "Right"; } - if (channels[0] === 1 && channels[1] === 0) { - return "Right,Left"; - } } - if (channels[0] === channels[1]) { - return chName(channels[0]); + if (channels[0] === channels[1] || channels[1] === -1) { + return chName(channels[0],maxChannels); } - return '[' + chName(channels[0]) + " " + chName(channels[1]) + "]"; + return '[' + chName(channels[0],maxChannels) + " " + chName(channels[1],maxChannels) + "]"; } diff --git a/vite/src/pipedal/ChannelRouterSettingsDialog.tsx b/vite/src/pipedal/ChannelRouterSettingsDialog.tsx index 57ae590..a5cbdbf 100644 --- a/vite/src/pipedal/ChannelRouterSettingsDialog.tsx +++ b/vite/src/pipedal/ChannelRouterSettingsDialog.tsx @@ -19,6 +19,7 @@ import React from 'react'; import { useState } from 'react'; +import VuMeter from './VuMeter'; import HelpOutlineIcon from '@mui/icons-material/HelpOutline'; import Divider from '@mui/material/Divider'; import MoreVertIcon from '@mui/icons-material/MoreVert'; @@ -45,8 +46,8 @@ import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; import { JackConfiguration } from './Jack'; -let debugInputChannels: number | null = 1; -let debugOutputChannels: number | null = 1; +let debugInputChannels: number | null = null; +let debugOutputChannels: number | null = null; export interface ChannelRouterSettingsDialogProps { @@ -539,11 +540,25 @@ function ChannelRouterSettingsDialog(props: ChannelRouterSettingsDialogProps) { ) } - let Vu = () => { + let Vu = (routeType: RouteType, input: boolean) => { + let instanceId: number; + switch (routeType) { + case RouteType.Main: + instanceId = input ? Pedalboard.START_CONTROL : Pedalboard.MAIN_INSERT_END_CONTROL_ID; + break; + case RouteType.Aux: + instanceId = input ? Pedalboard.AUX_INSERT_START_CONTROL_ID : Pedalboard.AUX_INSERT_END_CONTROL_ID; + break; + case RouteType.Send: + instanceId = input ? Pedalboard.SEND_RETURN_CONTROL : Pedalboard.SEND_OUTPUT_CONTROL; + + } return (
-
- {/* */} + +
); } @@ -603,7 +618,7 @@ function ChannelRouterSettingsDialog(props: ChannelRouterSettingsDialogProps) { let PresetSelect = (width: number | string | undefined) => { return ( -
+
- + { }} />
@@ -658,14 +673,14 @@ function ChannelRouterSettingsDialog(props: ChannelRouterSettingsDialogProps) { {ChannelSelect(RouteType.Main, 0, true)} - {Vu()} + {Vu(RouteType.Main, true)} - {Vu()} + {Vu(RouteType.Main, false)} {ChannelSelect(RouteType.Main, 0, false)} @@ -691,18 +706,17 @@ function ChannelRouterSettingsDialog(props: ChannelRouterSettingsDialogProps) { {(ChannelSelect(RouteType.Aux, 0, true))} - {Vu()} + {Vu(RouteType.Aux, true)} - {Vu()} + {Vu(RouteType.Aux, false)} {(ChannelSelect(RouteType.Aux, 0, false))} @@ -728,11 +742,11 @@ function ChannelRouterSettingsDialog(props: ChannelRouterSettingsDialogProps) { {(ChannelSelect(RouteType.Send, 0, false))} - {Vu()} + {Vu(RouteType.Send,false)} {/* No button */} - {Vu()} + {Vu(RouteType.Send,true)} {(ChannelSelect(RouteType.Send, 0, true))} @@ -798,7 +812,7 @@ function ChannelRouterSettingsDialog(props: ChannelRouterSettingsDialogProps) { - + - + @@ -862,7 +876,7 @@ function ChannelRouterSettingsDialog(props: ChannelRouterSettingsDialogProps) {
{Vu()}{Vu(RouteType.Main,true)}
{Vu()}{Vu(RouteType.Main,false)}
- + - + @@ -929,7 +943,7 @@ function ChannelRouterSettingsDialog(props: ChannelRouterSettingsDialogProps) {
{Vu()}{Vu(RouteType.Aux, true)}
{Vu()}{Vu(RouteType.Aux, false)}
- + - + @@ -1000,11 +1014,11 @@ function ChannelRouterSettingsDialog(props: ChannelRouterSettingsDialogProps) { { landscape ? ( <> -
 
-
- {PresetSelect(200)} +
+
+ {PresetSelect("325px")}
-
 
+
) : ( diff --git a/vite/src/pipedal/Pedalboard.tsx b/vite/src/pipedal/Pedalboard.tsx index 423640f..affe053 100644 --- a/vite/src/pipedal/Pedalboard.tsx +++ b/vite/src/pipedal/Pedalboard.tsx @@ -381,6 +381,8 @@ export class Pedalboard implements Deserializable { static readonly START_CONTROL = -2; // synthetic PedalboardItem for input volume. static readonly END_CONTROL = -3; // synthetic PedalboardItem for output volume. + static readonly SEND_OUTPUT_CONTROL = -4; // synthetic PedalboardItem for send output volume. + static readonly SEND_RETURN_CONTROL = -5; // synthetic PedalboardItem for send return volume. static readonly MAIN_INSERT_START_CONTROL_ID = Pedalboard.MAIN_INSERT_INSTANCE_BASE + -2 + MAX_INSTANCE_ID; static readonly MAIN_INSERT_END_CONTROL_ID = Pedalboard.MAIN_INSERT_INSTANCE_BASE + -3 + MAX_INSTANCE_ID; static readonly AUX_INSERT_START_CONTROL_ID = Pedalboard.AUX_INSERT_INSTANCE_BASE + -2 + MAX_INSTANCE_ID diff --git a/vite/src/pipedal/PiPedalModel.tsx b/vite/src/pipedal/PiPedalModel.tsx index e753e52..1cafe2c 100644 --- a/vite/src/pipedal/PiPedalModel.tsx +++ b/vite/src/pipedal/PiPedalModel.tsx @@ -2517,6 +2517,7 @@ export class PiPedalModel //implements PiPedalModel } else { let newTarget = new VuSubscriptionTarget(); newTarget.subscribers.push(result); + this.vuSubscriptions[instanceId] = newTarget; this.webSocket.request("addVuSubscription", instanceId) .then((subscriptionHandle) => { @@ -2526,6 +2527,9 @@ export class PiPedalModel //implements PiPedalModel } else { newTarget.serverSubscriptionHandle = subscriptionHandle; } + }).catch((error) => { + // failed to subscribe. + this.vuSubscriptions[instanceId] = undefined; }); this.vuSubscriptions[instanceId] = newTarget; diff --git a/vite/src/pipedal/VuMeter.tsx b/vite/src/pipedal/VuMeter.tsx index ebcd333..d3d0d22 100644 --- a/vite/src/pipedal/VuMeter.tsx +++ b/vite/src/pipedal/VuMeter.tsx @@ -200,6 +200,7 @@ interface VuMeterProps extends WithStyles { instanceId: number; display: "input" | "output"; theme: Theme; + height: number | undefined; displayText?: boolean; } @@ -462,6 +463,20 @@ export const VuMeter = render() { let displayText = this.props.displayText??false; const classes = withStyles.getClasses(this.props); + if (this.props.height !== undefined) + { + let height = this.props.height; + let scale = height / DISPLAY_HEIGHT; + return ( +
+
+ { + this.renderVus() + } +
+
+ ); + } if (displayText) { return ( @@ -526,7 +541,7 @@ export const VuMeter = componentDidMount() { this.model.state.addOnChangedHandler(this.onStateChanged); - this.addVuSubscription(); + //this.addVuSubscription(); } componentWillUnmount() { this.removeVuSubscription(); diff --git a/vite/vite.config.ts b/vite/vite.config.ts index 6c069b2..065f001 100644 --- a/vite/vite.config.ts +++ b/vite/vite.config.ts @@ -14,7 +14,7 @@ export default defineConfig({ target: 'http://localhost:8080', changeOrigin: false, }, - '/var': { + '^/var/(?!config\\.json$).*': { target: 'http://localhost:8080', changeOrigin: false, },
{Vu()}{Vu(RouteType.Send, false)}
{Vu()}{Vu(RouteType.Send, true)}