#include "ConnectionWorker.hpp" #include "RTLogger.hpp" using namespace juce; static std::string KeyPressToDescription(const KeyPress & k) { std::string root; int code = k.getKeyCode(); if(code == KeyPress::spaceKey) { root = " "; } else if(code == KeyPress::backspaceKey) { root = "backspace"; } else if(code == KeyPress::deleteKey) { root = "delete"; } else if(code == KeyPress::tabKey) { root = "tab"; } else if(code == KeyPress::leftKey) { root = "arrowleft"; } else if(code == KeyPress::upKey) { root = "arrowup"; } else if(code == KeyPress::rightKey) { root = "arrowright"; } else if(code == KeyPress::downKey) { root = "arrowdown"; } else if(code == KeyPress::numberPadEquals) { root = "="; } else if(code == KeyPress::numberPadDelete) { root = "delete"; } else if(code == KeyPress::numberPadDecimalPoint) { root = "."; } else if(code == KeyPress::numberPadSeparator) { root = ","; } else if(code == KeyPress::escapeKey) { root = "escape"; } else if(code == KeyPress::returnKey) { root = "return"; } else if(code == KeyPress::numberPad0) { root = "0"; } else if(code == KeyPress::numberPad1) { root = "1"; } else if(code == KeyPress::numberPad2) { root = "2"; } else if(code == KeyPress::numberPad3) { root = "3"; } else if(code == KeyPress::numberPad4) { root = "4"; } else if(code == KeyPress::numberPad5) { root = "5"; } else if(code == KeyPress::numberPad6) { root = "6"; } else if(code == KeyPress::numberPad7) { root = "7"; } else if(code == KeyPress::numberPad8) { root = "8"; } else if(code == KeyPress::numberPad9) { root = "9"; } else if(code == KeyPress::numberPadAdd) { root = "+"; } else if(code == KeyPress::numberPadSubtract) { root = "-"; } else if(code == KeyPress::numberPadMultiply) { root = "*"; } else if(code == KeyPress::numberPadDivide) { root = "/"; } else { root = " "; root[0] = std::tolower(code); } std::string modifiers; auto mods = k.getModifiers(); if(mods.isCommandDown()) { modifiers += "ctrl+"; } if(mods.isAltDown()) { modifiers += "alt+"; } if(mods.isShiftDown()) { modifiers += "shift+"; } return modifiers + root; } class WavtoolKeyListener final : public KeyListener { public: WavtoolKeyListener(std::function && keyHandler) : keyHandler(std::move(keyHandler)) { } virtual bool keyPressed(const KeyPress & key, Component *) override { if(heldKeys.find(key.getKeyCode()) != heldKeys.end()) { return true; } auto desc = KeyDescription{ .keyDown = true, .keyDescription = KeyPressToDescription(key) }; keyHandler(desc); heldKeys.insert(std::make_pair(key.getKeyCode(), desc.keyDescription)); return true; } virtual bool keyStateChanged(bool, Component *) override { std::set upKeys; for(auto & p : heldKeys) { if(!KeyPress::isKeyCurrentlyDown(p.first)) { upKeys.insert(p.first); } } for(auto code : upKeys) { keyHandler(KeyDescription{ .keyDown = false, .keyDescription = heldKeys[code] }); heldKeys.erase(code); } return !upKeys.empty(); } void reset() { for(auto & p : heldKeys) { keyHandler(KeyDescription{ .keyDown = false, .keyDescription = p.second }); } heldKeys.clear(); } private: std::map heldKeys; std::function keyHandler; }; #ifdef _MSC_VER constexpr const int HEADER_HEIGHT = 20; class EditorKeyboardGrabWrapper final : public Component, public Button::Listener, public ComponentListener, public Timer { public: EditorKeyboardGrabWrapper(AudioProcessorEditor* p, std::function && toggleCallbackIn) : p(p), toggleCallback(std::move(toggleCallbackIn)), buttonToggleState(true) { keyboardGrabEnabled = juce::ImageCache::getFromMemory(BinaryData::EnabledState_png, BinaryData::EnabledState_pngSize); keyboardGrabDisabled = juce::ImageCache::getFromMemory(BinaryData::DisabledState_png, BinaryData::DisabledState_pngSize); keyboardGrabMiniEnabled = juce::ImageCache::getFromMemory(BinaryData::MiniEnabledState_png, BinaryData::MiniEnabledState_pngSize); keyboardGrabMiniDisabled = juce::ImageCache::getFromMemory(BinaryData::MiniDisabledState_png, BinaryData::MiniDisabledState_pngSize); frameLogo = juce::ImageCache::getFromMemory(BinaryData::FrameLogo_png, BinaryData::FrameLogo_pngSize); activityIndicator = juce::ImageCache::getFromMemory(BinaryData::ActivityIndicator_png, BinaryData::ActivityIndicator_pngSize); button.addListener(this); addAndMakeVisible(button); keyboardActivityIndicator.setSize(HEADER_HEIGHT, HEADER_HEIGHT); keyboardActivityIndicator.setImage(activityIndicator); keyboardActivityIndicator.setAlpha(0.2); addAndMakeVisible(keyboardActivityIndicator); addAndMakeVisible(logo); addAndMakeVisible(p); p->setTopLeftPosition(0, 0); p->addComponentListener(this); setSize(p->getWidth(), p->getHeight() + HEADER_HEIGHT); layout(); } ~EditorKeyboardGrabWrapper() { p->removeComponentListener(this); button.removeListener(this); if (isTimerRunning()) { stopTimer(); } delete p; } void resized() override { if (p->getWidth() != getWidth() || p->getHeight() + HEADER_HEIGHT != getHeight()) { p->setSize(getWidth(), getHeight() - HEADER_HEIGHT); } layout(); } void layout() { if (getWidth() < 180) { button.setImages(true, true, false, buttonToggleState ? keyboardGrabMiniEnabled : keyboardGrabMiniDisabled, 0.9, Colour{}, Image{}, 1.0, Colour{}, Image{}, 1.0, Colour{}, 0.0); } else { button.setImages(true, true, false, buttonToggleState ? keyboardGrabEnabled : keyboardGrabDisabled, 0.9, Colour{}, Image{}, 1.0, Colour{}, Image{}, 1.0, Colour{}, 0.0); } button.setSize(button.getWidth() / 2, button.getHeight() / 2); button.setTopLeftPosition((getWidth() - button.getWidth()) / 2, 0); auto buttonBounds = button.getBoundsInParent(); keyboardActivityIndicator.setTopLeftPosition(buttonBounds.getTopRight().x, (HEADER_HEIGHT - keyboardActivityIndicator.getHeight()) / 2); logo.setTopLeftPosition(0, 0); logo.setSize(66, HEADER_HEIGHT); if (getWidth() >= 300) { logo.setImage(frameLogo); } else { logo.setImage(Image{}); } p->setTopLeftPosition(0, HEADER_HEIGHT); } ComponentBoundsConstrainer* getConstrainer() { return &constrainer; } void componentMovedOrResized(Component& c, bool, bool) override { if (&c != p) { return; } if (c.getWidth() != getWidth() || c.getHeight() + HEADER_HEIGHT != getHeight()) { setSize(c.getWidth(), c.getHeight() + HEADER_HEIGHT); } layout(); } void buttonClicked(Button* b) override { if (b != &button) { return; } toggleCallback(!buttonToggleState); } void setToggleState(bool newState) { buttonToggleState = newState; layout(); } void paint(Graphics& g) override { g.fillAll(Colour{ 0x11, 0x11, 0x11 }); } void keyboardActivity() { keyboardActivityIndicator.setAlpha(0.7); startTimer(100); } void timerCallback() override { keyboardActivityIndicator.setAlpha(0.2); stopTimer(); } private: AudioProcessorEditor* p; std::function toggleCallback; ImageButton button{ "keyboardCaptureModeToggle" }; ImageComponent logo, keyboardActivityIndicator; Image keyboardGrabEnabled, keyboardGrabDisabled, keyboardGrabMiniEnabled, keyboardGrabMiniDisabled, frameLogo, activityIndicator; bool buttonToggleState; class C final : public BorderedComponentBoundsConstrainer { public: explicit C(EditorKeyboardGrabWrapper& comp) : comp(comp) {} ComponentBoundsConstrainer* getWrappedConstrainer() const override { return comp.p != nullptr ? comp.p->getConstrainer() : nullptr; } BorderSize getAdditionalBorder() const override { return BorderSize(0, 0, HEADER_HEIGHT, 0); } private: EditorKeyboardGrabWrapper& comp; }; C constrainer{ *this }; }; #endif Component* createPlatformEditorWrapper(AudioProcessorEditor* ui, std::function && toggleCallback) { #ifdef _MSC_VER return new EditorKeyboardGrabWrapper(ui, std::move(toggleCallback)); #else return ui; #endif } ComponentBoundsConstrainer* tryGetConstrainer(Component* comp) { auto* asEditor = dynamic_cast (comp); if (asEditor) { return asEditor->getConstrainer(); } #ifdef _MSC_VER auto* asWrapper = dynamic_cast (comp); if (asWrapper) { return asWrapper->getConstrainer(); } #endif return nullptr; } struct CallbackTimer final : public Timer { CallbackTimer(std::function&& callback, int interval) : callback(std::move(callback)) { startTimer(interval); } ~CallbackTimer() { stopTimer(); } void timerCallback() override { callback(); } private: std::function callback; }; static bool rectangleOverlapsWithSomeDisplay(const juce::Rectangle & rect) { auto lst = Desktop::getInstance().getDisplays().getRectangleList(false); std::cerr << "Display rectangles:\n"; for (auto r : lst) { std::cerr << r.getTopLeft().getX() << "," << r.getTopLeft().getY() << " " << r.getWidth() << "x" << r.getHeight() << "\n"; } std::cerr << "Testing: " << rect.getTopLeft().getX() << "," << rect.getTopLeft().getY() << " " << rect.getWidth() << "x" << rect.getHeight() << "\n"; auto res = Desktop::getInstance().getDisplays().getRectangleList(false).intersectsRectangle(rect); std::cerr << "Result: " << res << "\n"; return res; } class PluginWindow final : public DocumentWindow { public: PluginWindow (AudioProcessor* p, ConnectionWorker* cw, std::optional> position) : DocumentWindow (p->getName(), LookAndFeel::getDefaultLookAndFeel().findColour (ResizableWindow::backgroundColourId), DocumentWindow::minimiseButton | DocumentWindow::closeButton), processor (p), context (cw) { std::cerr << "Creating plugin window for " << p << "\n"; setSize(400, 300); setOpaque(true); setUsingNativeTitleBar(true); auto* editorUi = p->createEditorIfNeeded(); if (!editorUi) { context->closePluginWindow(); return; } setContentOwned(createPlatformEditorWrapper(editorUi, [this](bool enabled) { setKeyboardEnabled(enabled, true); }), true); setResizable(editorUi->isResizable(), false); setConstrainer (&constrainer); Point proposedPosition(40, 40); if(position.has_value()) { const auto [x, y] = position.value(); proposedPosition.setX(x); proposedPosition.setY(y); } else { auto primaryDisplay = Desktop::getInstance().getDisplays().getPrimaryDisplay(); if(primaryDisplay) { auto r = primaryDisplay->userArea; proposedPosition.setX((r.getWidth() - getWidth()) / 2); proposedPosition.setY((r.getHeight() - getHeight()) / 2); } } #ifdef _MSC_VER // on Windows, window rect excludes title bar juce::Rectangle titleBarApprox(proposedPosition.getX(), proposedPosition.getY() - 20, getWidth(), 20); #else // on Mac, window rect includes title bar juce::Rectangle titleBarApprox(proposedPosition.getX(), proposedPosition.getY(), getWidth(), 20); #endif if(!rectangleOverlapsWithSomeDisplay(titleBarApprox)) { proposedPosition.setX(40); proposedPosition.setY(40); } setTopLeftPosition(proposedPosition); context->currentWindowPosition = std::make_pair(getX(), getY()); context->editorMovedHandler(context->currentWindowPosition); #ifndef _MSC_VER addKeyListener(cw->keyListener.get()); if(context->alwaysOnTop) { #endif setVisible (true); toFront (true); setAlwaysOnTop(true); #ifndef _MSC_VER } Process::setDockIconVisible(false); #endif startActivationPoller(); } ~PluginWindow() override { #ifndef _MSC_VER removeKeyListener(context->keyListener.get()); #endif clearContentComponent(); } void startActivationPoller() { #ifdef _MSC_VER activationPoller = std::make_unique([this] { HWND thisWindow = (HWND)getPeer()->getNativeHandle(); if (thisWindow == NULL) { return; } if (IsWindowVisible(thisWindow)) { context->launchingChangedHandler(false); activationPoller.reset(); } }, 50); #endif } void activate() { if (isVisible() && isAlwaysOnTop()) { return; } #ifdef _MSC_VER context->launchingChangedHandler(true); #endif setVisible(true); #ifndef _MSC_VER Process::setDockIconVisible(false); #endif setAlwaysOnTop(true); toFront(true); startActivationPoller(); } void deactivate() { activationPoller.reset(); context->launchingChangedHandler(false); setVisible(false); } #ifndef _MSC_VER void focusLost(FocusChangeType) override { if(context->keyListener) { context->keyListener->reset(); } } #endif void setKeyboardEnabled(bool isEnabled, bool callHandler) { #ifdef _MSC_VER auto* asWrapper = dynamic_cast (getContentComponent()); if (!asWrapper) { return; } asWrapper->setToggleState(isEnabled); #endif if (callHandler) { context->keyboardEnabledHandler(isEnabled); } } void keyboardActivity() { #ifdef _MSC_VER auto* asWrapper = dynamic_cast (getContentComponent()); if (!asWrapper) { return; } asWrapper->keyboardActivity(); #endif } void moved() override { ResizableWindow::moved(); auto newPosn = std::make_pair(getX(), getY()); if(newPosn == context->currentWindowPosition) { return; } context->currentWindowPosition = newPosn; context->lastWindowPositionChangeTime = ConnectionWorker::clock_t::now(); context->hasWindowPositionChange = true; } void closeButtonPressed() override { context->editorClosedHandler(); context->closePluginWindow(); } const AudioProcessor * processor; ConnectionWorker* context; private: class DecoratorConstrainer final : public BorderedComponentBoundsConstrainer { public: explicit DecoratorConstrainer (DocumentWindow& windowIn) : window (windowIn) {} ComponentBoundsConstrainer* getWrappedConstrainer() const override { return tryGetConstrainer(window.getContentComponent()); } BorderSize getAdditionalBorder() const override { const auto nativeFrame = [&]() -> BorderSize { if (auto* peer = window.getPeer()) if (const auto frameSize = peer->getFrameSizeIfPresent()) return *frameSize; return {}; }(); return nativeFrame.addedTo(window.getContentComponentBorder()); } private: DocumentWindow& window; }; DecoratorConstrainer constrainer { *this }; float getDesktopScaleFactor() const override { return 1.0f; } std::unique_ptr activationPoller; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PluginWindow) }; class WavtoolPlayhead final : public AudioPlayHead { public: WavtoolPlayhead() { p.setTimeInSamples(0); p.setTimeInSeconds(0.0); p.setBpm(120.0); p.setTimeSignature(TimeSignature{.numerator=4, .denominator=4}); p.setBarCount(0.0); p.setPpqPositionOfLastBarStart(0.0); p.setPpqPosition(0.0); p.setLoopPoints(LoopPoints{.ppqStart=0, .ppqEnd=0}); p.setIsPlaying(false); p.setIsRecording(false); p.setIsLooping(false); } void setFromShm(const ShmSetTransportData & data, int sampleRate) { p.setTimeInSamples(data.samplePosition); p.setTimeInSeconds((double)data.samplePosition / (double)sampleRate); p.setBpm(data.bpm); p.setTimeSignature(TimeSignature{.numerator=data.beatNumerator, .denominator=data.beatDenominator}); const auto beat = samplesToBeats(data.samplePosition, sampleRate, data.bpm); const auto barCount = std::floor(beat / (double)data.beatNumerator); const auto loopStartBeat = samplesToBeats(data.sampleLoopStart, sampleRate, data.bpm); const auto loopEndBeat = samplesToBeats(data.sampleLoopEnd, sampleRate, data.bpm); p.setBarCount(barCount); p.setPpqPositionOfLastBarStart(barCount * (double)data.beatNumerator); p.setPpqPosition(beat); p.setLoopPoints(LoopPoints{.ppqStart=loopStartBeat, .ppqEnd=loopEndBeat}); p.setIsPlaying(data.playing); p.setIsRecording(data.recording); p.setIsLooping(data.looping); } void advance(int numSamples, int sampleRate) { if(!p.getIsPlaying()) { return; } const auto bpm = *p.getBpm(); const auto lastBeat = samplesToBeats(*p.getTimeInSamples(), sampleRate, bpm); const auto loopPoints = *p.getLoopPoints(); if(p.getIsLooping() && lastBeat >= loopPoints.ppqStart && lastBeat <= loopPoints.ppqEnd && loopPoints.ppqEnd != loopPoints.ppqStart && bpm > 0.0) { const auto deltaBeat = samplesToBeats(numSamples, sampleRate, bpm); const auto nextBeat = std::fmod(lastBeat + deltaBeat - loopPoints.ppqStart, loopPoints.ppqEnd - loopPoints.ppqStart) + loopPoints.ppqStart; p.setTimeInSamples(nextBeat / (double)bpm * 60.0 * (double)sampleRate); } else { p.setTimeInSamples(*p.getTimeInSamples() + numSamples); } p.setTimeInSeconds(*p.getTimeInSamples() / (double)sampleRate); const auto numerator = p.getTimeSignature()->numerator; const auto beat = samplesToBeats(*p.getTimeInSamples(), sampleRate, bpm); const auto barCount = std::floor(beat / (double)numerator); p.setBarCount(barCount); p.setPpqPositionOfLastBarStart(barCount * (double)numerator); p.setPpqPosition(beat); } virtual Optional getPosition() const override { return p; } virtual bool canControlTransport() override { return false; } virtual void transportPlay (bool) override { }; virtual void transportRecord (bool) override { }; virtual void transportRewind() override { }; private: double samplesToBeats(int64_t samples, int sampleRate, double bpm) { return (double)samples / ((double)sampleRate * 60.0) * bpm; } PositionInfo p; }; ConnectionWorker::ConnectionWorker(const PluginDescription & desc, int bufferSize, int sampleRate, std::string && bridgeKeySeed, std::function && loadedHandler, std::function && errorHandler, std::function && saveHandler, std::function && parameterGestureHandler, std::function && keyHandler, std::function && editorClosedHandler, std::function)> && editorMovedHandler, std::function && trustHandler, std::function && latencyChangedHandler, std::function && launchingChangedHandler, std::function&& keyboardEnabledHandler) : bufferSize(bufferSize), sampleRate(sampleRate), loadedHandler(std::move(loadedHandler)), errorHandler(std::move(errorHandler)), saveHandler(std::move(saveHandler)), parameterGestureHandler(std::move(parameterGestureHandler)), editorClosedHandler(std::move(editorClosedHandler)), editorMovedHandler(std::move(editorMovedHandler)), trustHandler(std::move(trustHandler)), latencyChangedHandler(std::move(latencyChangedHandler)), launchingChangedHandler(std::move(launchingChangedHandler)), keyboardEnabledHandler(std::move(keyboardEnabledHandler)), keyListener(std::make_unique(std::move(keyHandler))), playhead(std::make_unique()), isLoadingState(false), thunkQ(100), hasUncommittedStateUpdate(false), gesturesActive(0), alwaysOnTop(false), audioBuffer(2, 1024), triggerAsyncUpdateSema(0), triggerAsyncUpdateShouldExit(false), stateIntegrity(bridgeKeySeed), waitingForTrustResolution(false), sendLatencyChangedNextBlock(false), lastPeriodicSaveTime(clock_t::now()), hasWindowPositionChange(false), lastWindowPositionChangeTime(clock_t::now()), currentWindowPosition(std::make_pair(0,0)), unsetLaunchingArmed(true), keyboardEnabled(false), previousInputBuses(-1), previousOutputBuses(-1), desiredInputBuses(-1), desiredOutputBuses(-1), wantsBusLayoutUpdate(false) { RTLogger::log("Loading plugin %s from %s", desc.name.toRawUTF8(), desc.fileOrIdentifier.toRawUTF8()); triggerAsyncUpdateThread = std::make_unique([this] { for(;;) { bool didAcquire = triggerAsyncUpdateSema.try_acquire_for(std::chrono::milliseconds(100)); if(triggerAsyncUpdateShouldExit) { break; } if(didAcquire) { triggerAsyncUpdate(); } } }); pluginToLoad = std::make_unique(desc); enqueueThunk([this]{ loadPlugin(); }); } ConnectionWorker::~ConnectionWorker() { pluginWindow.reset(); launchingChangedHandler(false); plugin.reset(); triggerAsyncUpdateShouldExit = true; triggerAsyncUpdateThread->join(); triggerAsyncUpdateThread.reset(); } MidiBuffer ConnectionWorker::getClearMidiBuffer() { MidiBuffer res; for(int i = 0; i < 128; i++) { res.addEvent(MidiMessage{0x80, i, 64}, 0); } return res; } void ConnectionWorker::setKeyboardEnabled(bool isEnabled) { enqueueThunk([this,isEnabled] { keyboardEnabled = isEnabled; if (pluginWindow) { pluginWindow->setKeyboardEnabled(isEnabled, false); } }); } void ConnectionWorker::keyboardActivity() { enqueueThunk([this] { if (pluginWindow) { pluginWindow->keyboardActivity(); } }); } void ConnectionWorker::audioProcessorParameterChanged(AudioProcessor *processor, int parameterIndex, float newValue) { // TODO we want to capture changes of non-automated paramters that also aren't mapped to MIDI CCs if(processor != plugin.get()) { return; } RTLogger::log("audioProcessorParameterChanged"); if(gesturesActive > 0 || parameterIndex < 0 || isLoadingState) { return; } auto didChange = parameterIndex >= pluginParameterValues.size() && parametersDidChange(); // need to rebuild values vector if(didChange || pluginParameterValues[parameterIndex] != newValue) { RTLogger::log("audioProcessorParameterChanged: gesturesActive was already zero and parametersDidChange"); pluginParameterValues[parameterIndex] = newValue; stateUpdateCallbackFired(); } } void ConnectionWorker::audioProcessorChanged(AudioProcessor *processor, const ChangeDetails &details) { auto paramsDidChange = parametersDidChange(); if(details.latencyChanged) { sendLatencyChangedNextBlock = true; } if(isLoadingState || !(details.parameterInfoChanged || paramsDidChange)) { RTLogger::log("ignoring audioProcessorChanged: latencyChanged=%d, parameterInfoChanged=%d, isLoadingState=%d, paramsDidChange=%d", (int)details.latencyChanged, (int)details.parameterInfoChanged, (int)isLoadingState, (int)paramsDidChange); return; } RTLogger::log("audioProcessorChanged latencyChanged=%d, nonParam=%d, paramInfo=%d, programChange=%d", (int)details.latencyChanged, (int)details.nonParameterStateChanged, (int)details.parameterInfoChanged, (int)details.programChanged); if(processor != plugin.get()) { return; } stateUpdateCallbackFired(); } void ConnectionWorker::audioProcessorParameterChangeGestureBegin(AudioProcessor * processor, int parameterIndex) { if(processor != plugin.get()) { return; } gesturesActive++; if(parameterIndex < 0 || parameterIndex >= pluginParameters.size()) { return; } auto param = pluginParameters[parameterIndex]; if(!param) { return; } parameterGestureHandler(ParameterDescription{.index = parameterIndex, .name = param->getName(128).toStdString()}); } void ConnectionWorker::audioProcessorParameterChangeGestureEnd(AudioProcessor *processor, int parameterIndex) { if(processor != plugin.get()) { return; } gesturesActive--; if(gesturesActive > 0 || parameterIndex < 0 || parameterIndex >= pluginParameters.size() || !pluginParameters[parameterIndex]) { return; } auto newValue = pluginParameters[parameterIndex]->getValue(); auto didChange = parameterIndex >= pluginParameterValues.size() && parametersDidChange(); // need to rebuild values vector if(didChange || pluginParameterValues[parameterIndex] != newValue) { RTLogger::log("gesturesActive hit zero and parametersDidChange"); pluginParameterValues[parameterIndex] = newValue; stateUpdateCallbackFired(); } } bool ConnectionWorker::parametersDidChange() { if(pluginParameterValues.size() != pluginParameters.size()) { pluginParameterValues.resize(pluginParameters.size()); for(size_t i = 0; i < pluginParameters.size(); i++) { if(!pluginParameters[i]) { continue; } pluginParameterValues[i] = pluginParameters[i]->getValue(); } return true; } bool didChange = false; for(size_t i = 0; i < pluginParameters.size(); i++) { if(!pluginParameters[i]) { continue; } auto currentVal = pluginParameters[i]->getValue(); if(currentVal != pluginParameterValues[i]) { didChange = true; } pluginParameterValues[i] = currentVal; } return didChange; } void ConnectionWorker::stateUpdateCallbackFired() { lastStateUpdateCallbackTime = clock_t::now(); hasUncommittedStateUpdate = true; } void ConnectionWorker::handleBlock(ShmData & block) { if(block.messageType == ShmMessageType::SET_TRANSPORT) { setTransport(block.setTransport); } else if(block.messageType == ShmMessageType::PROCESS_BLOCK) { processBlock(block.processBlock, block.shouldReset); block.shouldReset = false; } else { throw std::runtime_error("Bad block type"); } } void ConnectionWorker::loadState(const std::string_view & data) { auto dataLocal = std::string(data); enqueueThunk([this, dataLocal=std::move(dataLocal)]() mutable { if(!loadStateOnMessageThread(dataLocal)) { pendingStateChanges.emplace_back(std::move(dataLocal)); } }); } bool ConnectionWorker::loadStateOnMessageThread(const std::string_view & data) { if(!plugin) { return true; } std::string detachedState; try { detachedState = stateIntegrity.verify(data); } catch(const StateIntegrity::PublicKeyNotTrusted & e) { if(e.getIsKnown()) { std::cerr << "WARNING: refusing to load state signed by denylisted key\n"; return true; } trustHandler(e.getPublicKey()); waitingForTrustResolution = true; alwaysOnTopEdgeTrigger(); return false; } catch(const StateIntegrity::SignatureVerificationFailed &) { std::cerr << "WARNING: refusing to load VST state with malformed signature\n"; return true; } if(detachedState == lastSavedState) { std::cerr << "Not reloading state equal to last saved state\n"; return true; } std::cerr << "Loading state!\n"; lastSavedState = std::move(detachedState); isLoadingState = true; plugin->setStateInformation(lastSavedState.data(), (int)lastSavedState.size()); parametersDidChange(); isLoadingState = false; return true; } void ConnectionWorker::trustResolution(const XmlElement & elem) { XmlElement localElem(elem); enqueueThunk([this,localElem=std::move(localElem)] { stateIntegrity.loadTrustStore(localElem); waitingForTrustResolution = false; while(!pendingStateChanges.empty()) { auto data = std::move(pendingStateChanges.front()); pendingStateChanges.pop_front(); if(!loadStateOnMessageThread(data)) { pendingStateChanges.emplace_front(std::move(data)); break; } } alwaysOnTopEdgeTrigger(); }); } void ConnectionWorker::requestSaveState(bool periodic) { hasUncommittedStateUpdate = false; lastPeriodicSaveTime = clock_t::now(); enqueueThunk([=,this] { if(!plugin) { return; } MemoryBlock mb; plugin->getStateInformation(mb); auto view = std::string_view((char*)mb.getData(), mb.getSize()); if(lastSavedState != view) { std::cerr << "Saving state! periodic=" << periodic << "\n"; lastSavedState = view; saveHandler(stateIntegrity.sign(view), periodic); } }); } void ConnectionWorker::setTransport(ShmSetTransportData & data) { if(!plugin) { return; } std::stringstream ss; ss << "setTransport\n" << " bpm: " << data.bpm << "\n" << " signature: " << data.beatNumerator << "/" << data.beatDenominator << "\n" << " position: " << data.samplePosition << "\n" << " loop: [" << data.sampleLoopStart << ", " << data.sampleLoopEnd << "] " << (data.looping ? "on" : "off") << "\n" << " playing: " << (data.playing ? "yes" : "no") << "\n" << " recording: " << (data.recording ? "yes" : "no"); auto str = ss.str(); RTLogger::log("%s", str.c_str()); playhead->setFromShm(data, sampleRate); data.samplesLatency = plugin->getLatencySamples(); } void ConnectionWorker::processBlock(ShmProcessBlockData & block, bool shouldReset) { if(!plugin) { return; } auto cexExpectedValue = true; if(sendLatencyChangedNextBlock.compare_exchange_strong(cexExpectedValue, false)) { latencyChangedHandler(plugin->getLatencySamples()); } if(block.numSamplesPerChannel > bufferSize || block.numSamplesPerChannel == 0) { throw std::runtime_error("block.numSamplesPerChannel must be between 1 and bufferSize"); } if(block.parameterSampleStride > block.numSamplesPerChannel || block.parameterSampleStride == 0) { throw std::runtime_error("block.parameterSampleStride must be between 1 and block.numSamplesPerChannel"); } if(block.numSamplesPerChannel % block.parameterSampleStride != 0) { throw std::runtime_error("block.numSamplesPerChannel must be divisible by block.parameterSampleStride"); } // configure buses if(desiredInputBuses != block.numInputs || desiredOutputBuses != block.numOutputs || previousInputBuses != desiredInputBuses || previousOutputBuses != desiredOutputBuses) { desiredInputBuses = block.numInputs; desiredOutputBuses = block.numOutputs; wantsBusLayoutUpdate = true; } if(wantsBusLayoutUpdate) { for(int i = 0; i < 2; i++) { memset(block.channelss[0].samples[i], 0, sizeof(float) * MAX_BUFFER_SIZE); } block.numMidiEvents = 0; return; } const auto pluginNumChannels = jmax(plugin->getTotalNumInputChannels(), plugin->getTotalNumOutputChannels()); if(pluginNumChannels > audioBuffer.getNumChannels() || bufferSize > audioBuffer.getNumSamples()) { audioBuffer.setSize(pluginNumChannels, bufferSize); } if(shouldReset) { auto clearMidiBuffer = getClearMidiBuffer(); AudioBuffer clearBuffer(audioBuffer.getArrayOfWritePointers(), pluginNumChannels, 64); plugin->processBlock(clearBuffer, clearMidiBuffer); plugin->reset(); } const uint32_t maxSubblockCount = block.numSamplesPerChannel / block.parameterSampleStride; bool subblockCuts[MAX_BUFFER_SIZE]; for(uint32_t i = 0; i < maxSubblockCount - 1; i++) { subblockCuts[i] = false; } for(uint32_t i = 0; i < block.numParameterAutomations; i++) { const auto & aut = block.automations[i]; for(uint32_t j = 0; j < maxSubblockCount - 1; j++) { const auto leftVal = aut.values[j]; const auto rightVal = aut.values[j + 1]; if(leftVal != rightVal) { subblockCuts[j] = true; } } } uint32_t subblockLengths[MAX_BUFFER_SIZE]; for(uint32_t i = 0; i < maxSubblockCount; i++) { subblockLengths[i] = 0; } uint32_t numSubblocks = 0; for(uint32_t i = 0; i < maxSubblockCount - 1; i++) { subblockLengths[numSubblocks] += block.parameterSampleStride; if(subblockCuts[i]) { numSubblocks += 1; } } subblockLengths[numSubblocks] += block.parameterSampleStride; numSubblocks += 1; auto outputMidiBuffer = MidiBuffer(); uint32_t subblockStartSample = 0; for(uint32_t subblockIndex = 0; subblockIndex < numSubblocks; subblockIndex++) { // set parameters for(uint32_t i = 0; i < block.numParameterAutomations; i++) { const auto & aut = block.automations[i]; if(aut.parameterIndex >= pluginParameters.size()) { continue; } auto * param = pluginParameters[aut.parameterIndex]; if(param == nullptr) { continue; } param->setValue(aut.values[subblockStartSample / block.parameterSampleStride]); } // load input audio const auto subblockLength = subblockLengths[subblockIndex]; AudioBuffer subblockBuffer(audioBuffer.getArrayOfWritePointers(), pluginNumChannels, subblockLength); const auto writePtrs = subblockBuffer.getArrayOfWritePointers(); const auto inBusesToLoad = jmin(plugin->getBusCount(true), (int)block.numInputs); for(int busIndex = 0; busIndex < inBusesToLoad; busIndex++) { const auto channelLayout = plugin->getChannelLayoutOfBus(true, busIndex); const auto leftIdx = channelLayout.getChannelIndexForType(AudioChannelSet::ChannelType::left); const auto rightIdx = channelLayout.getChannelIndexForType(AudioChannelSet::ChannelType::right); const auto centreIdx = channelLayout.getChannelIndexForType(AudioChannelSet::ChannelType::centre); if(leftIdx != -1 && rightIdx != -1) { // bus is stereo const auto leftBufIdx = plugin->getChannelIndexInProcessBlockBuffer(true, busIndex, leftIdx); const auto rightBufIdx = plugin->getChannelIndexInProcessBlockBuffer(true, busIndex, rightIdx); memcpy(writePtrs[leftBufIdx], block.channelss[busIndex].samples[0] + subblockStartSample, sizeof(float) * subblockLength); memcpy(writePtrs[rightBufIdx], block.channelss[busIndex].samples[1] + subblockStartSample, sizeof(float) * subblockLength); } else if(centreIdx != -1) { // bus is mono const auto centreBufIdx = plugin->getChannelIndexInProcessBlockBuffer(true, busIndex, centreIdx); const auto & inSamples = block.channelss[busIndex].samples; for(size_t i = 0; i < subblockLength; i++) { writePtrs[centreBufIdx][i] = (inSamples[0][i + subblockStartSample] + inSamples[1][i + subblockStartSample]) / 2.f; } } } // load input midi auto midiBuffer = MidiBuffer(); for(int i = 0; i < block.numMidiEvents; i++) { const auto & evt = block.midiEvents[i]; if(evt.sampleIndex >= subblockStartSample && evt.sampleIndex < subblockStartSample + subblockLength) { midiBuffer.addEvent(evt.data, evt.length, evt.sampleIndex - subblockStartSample); } } plugin->processBlock(subblockBuffer, midiBuffer); // unload output midi outputMidiBuffer.addEvents(midiBuffer, 0, subblockLength, subblockStartSample); // unload output audio const auto outBusesToLoad = jmin(plugin->getBusCount(false), (int)block.numOutputs); for(int busIndex = 0; busIndex < outBusesToLoad; busIndex++) { const auto channelLayout = plugin->getChannelLayoutOfBus(false, busIndex); const auto leftIdx = channelLayout.getChannelIndexForType(AudioChannelSet::ChannelType::left); const auto rightIdx = channelLayout.getChannelIndexForType(AudioChannelSet::ChannelType::right); const auto centreIdx = channelLayout.getChannelIndexForType(AudioChannelSet::ChannelType::centre); if(leftIdx != -1 && rightIdx != -1) { // bus is stereo const auto leftBufIdx = plugin->getChannelIndexInProcessBlockBuffer(false, busIndex, leftIdx); const auto rightBufIdx = plugin->getChannelIndexInProcessBlockBuffer(false, busIndex, rightIdx); memcpy(block.channelss[busIndex].samples[0] + subblockStartSample, writePtrs[leftBufIdx], sizeof(float) * subblockLength); memcpy(block.channelss[busIndex].samples[1] + subblockStartSample, writePtrs[rightBufIdx], sizeof(float) * subblockLength); } else if(centreIdx != -1) { // bus is mono const auto centreBufIdx = plugin->getChannelIndexInProcessBlockBuffer(false, busIndex, centreIdx); auto & outSamples = block.channelss[busIndex].samples; for(size_t i = 0; i < subblockLength; i++) { auto val = writePtrs[centreBufIdx][i]; val /= 2.f; outSamples[0][i + subblockStartSample] = val; outSamples[1][i + subblockStartSample] = val; } } } playhead->advance((int)subblockLength, sampleRate); subblockStartSample += subblockLength; } // unload output midi block.numMidiEvents = 0; for(const auto juceEvt : outputMidiBuffer) { if(block.numMidiEvents >= MAX_MIDI_EVENTS) { break; } if(juceEvt.numBytes > MAX_MIDI_EVENT_LENGTH) { continue; } auto & evt = block.midiEvents[block.numMidiEvents++]; evt.sampleIndex = juceEvt.samplePosition; evt.length = juceEvt.numBytes; memcpy(evt.data, juceEvt.data, juceEvt.numBytes); } } void ConnectionWorker::doBusLayoutUpdate() { plugin->releaseResources(); auto numInputs = desiredInputBuses.load(); auto numOutputs = desiredOutputBuses.load(); for(const auto & [isInput, desiredBusCount] : { std::make_pair(false, numOutputs), std::make_pair(true, numInputs) }) { const int deltaBuses = plugin->getBusCount(isInput) - desiredBusCount; if(deltaBuses > 0) { for(int i = 0; i < deltaBuses; i++) { if(!plugin->removeBus(isInput)) { break; } } } else { for(int i = 0; i < -deltaBuses; i++) { if(!plugin->addBus(isInput)) { break; } } } for(int busIndex = 0; busIndex < plugin->getBusCount(isInput); busIndex++) { auto bus = plugin->getBus(isInput, busIndex); if(busIndex < desiredBusCount) { bus->setCurrentLayout(AudioChannelSet::stereo()); } else { bus->setCurrentLayout(AudioChannelSet::disabled()); } } } plugin->prepareToPlay(sampleRate, bufferSize); previousInputBuses = numInputs; previousOutputBuses = numOutputs; } void ConnectionWorker::enqueueThunk(uWS::MoveOnlyFunction && thunk) { // To prevent livelock between the message thread and realtime thread, this cannot block if(!thunkQ.try_emplace(std::move(thunk))) { RTLogger::log("ConnectionWorker::enqueueThunk failed because the queue is full!"); } try { triggerAsyncUpdateSema.release(); } catch(std::system_error &) { RTLogger::log("failed to release triggerAsyncUpdateSema"); } } void ConnectionWorker::handleAsyncUpdate() { for(;;) { uWS::MoveOnlyFunction thunk; if(!thunkQ.try_pop(thunk)) { break; } thunk(); } } void ConnectionWorker::loadPlugin() { if(!formatManager) { formatManager = std::make_unique(); formatManager->addDefaultFormats(); } formatManager->createPluginInstanceAsync(*pluginToLoad, 44100.0, 1024, [this] (std::unique_ptr instance, const String& error) { addPluginCallback(std::move(instance), error); }); } void ConnectionWorker::addPluginCallback(std::unique_ptr instance, const String& error) { if(!instance) { errorHandler("Failed to initialize plugin!\n"); return; } instance->setProcessingPrecision(AudioProcessor::ProcessingPrecision::singlePrecision); instance->enableAllBuses(); instance->disableNonMainBuses(); instance->setNonRealtime(false); instance->setPlayHead(playhead.get()); instance->addListener(this); instance->setPlayConfigDetails(2, 2, sampleRate, bufferSize); instance->prepareToPlay(sampleRate, bufferSize); auto params = instance->getParameters(); size_t maxIndex = 0; for(auto param : params) { auto index = param->getParameterIndex(); if(index < 0) { continue; } maxIndex = std::max(maxIndex, (size_t)param->getParameterIndex()); } pluginParameters.clear(); pluginParameters.resize(maxIndex + 1, nullptr); for(auto param : params) { auto index = param->getParameterIndex(); if(index < 0 || index >= pluginParameters.size()) { continue; } pluginParameters[index] = param; } plugin = std::move(instance); lastStateUpdateCallbackTime = clock_t::now(); parametersDidChange(); loadedHandler(); pluginLoadTime = clock_t::now(); startTimer(50); } void ConnectionWorker::openPluginWindow(std::optional> pos) { enqueueThunk([=,this] { if (!plugin) { return; } if(!pluginWindow) { #ifdef _MSC_VER launchingChangedHandler(true); #endif pluginWindow = std::make_unique(plugin.get(), this, pos); pluginWindow->setKeyboardEnabled(keyboardEnabled, false); } if (pluginWindow && pluginWindow->isVisible() && pluginWindow->isAlwaysOnTop()) { pluginWindow->toFront(true); } alwaysOnTopEdgeTrigger(); }); } void ConnectionWorker::closePluginWindow() { enqueueThunk([this] { launchingChangedHandler(false); pluginWindow.reset(); keyListener->reset(); }); } void ConnectionWorker::alwaysOnTopEdgeTrigger() { if(!pluginWindow) { return; } if(alwaysOnTop && !waitingForTrustResolution) { pluginWindow->activate(); } else { pluginWindow->deactivate(); } } void ConnectionWorker::setPluginWindowAlwaysOnTop(bool onTop) { enqueueThunk([this, onTop] { if (onTop == alwaysOnTop) { return; } alwaysOnTop = onTop; alwaysOnTopEdgeTrigger(); }); } void ConnectionWorker::timerCallback() { auto now = clock_t::now(); if(hasUncommittedStateUpdate && now - lastStateUpdateCallbackTime.load() > std::chrono::milliseconds(100)) { requestSaveState(false); } if(now - lastPeriodicSaveTime.load() > std::chrono::seconds(5)) { requestSaveState(true); } if(hasWindowPositionChange && now - lastWindowPositionChangeTime.load() > std::chrono::milliseconds(300)) { enqueueThunk([this] { hasWindowPositionChange = false; editorMovedHandler(currentWindowPosition); }); } if (unsetLaunchingArmed && now - pluginLoadTime.load() > std::chrono::milliseconds(200)) { unsetLaunchingArmed = false; enqueueThunk([this] { launchingChangedHandler(false); }); } if(wantsBusLayoutUpdate) { enqueueThunk([this] { if(wantsBusLayoutUpdate) { doBusLayoutUpdate(); wantsBusLayoutUpdate = false; } }); } }