/* * DISTRHO Cardinal Plugin * Copyright (C) 2021-2024 Filipe Coelho * SPDX-License-Identifier: GPL-3.0-or-later */ /** * This file is partially based on VCVRack's patch.cpp * Copyright (C) 2016-2021 VCV. * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 3 of * the License, or (at your option) any later version. */ #include "CardinalCommon.hpp" #include "AsyncDialog.hpp" #include "CardinalPluginContext.hpp" #include "DistrhoPluginUtils.hpp" #include #include #include #include #include #include #include #include #include #include #include #ifndef DISTRHO_PLUGIN_WANT_DIRECT_ACCESS # error wrong build #endif #if (defined(STATIC_BUILD) && !defined(__MOD_DEVICES__)) || CARDINAL_VARIANT_MINI # undef CARDINAL_INIT_OSC_THREAD #endif #ifdef NDEBUG # undef DEBUG #endif // for finding special paths #ifdef ARCH_WIN # include #else # include # include #endif #ifdef ARCH_LIN # include #endif #ifdef HAVE_LIBLO # include #endif #ifdef DISTRHO_OS_WASM # include #endif #if defined(CARDINAL_COMMON_DSP_ONLY) || defined(HEADLESS) # define HEADLESS_BEHAVIOUR #endif #if CARDINAL_VARIANT_FX # define CARDINAL_VARIANT_NAME "fx" #elif CARDINAL_VARIANT_MINI # define CARDINAL_VARIANT_NAME "mini" #elif CARDINAL_VARIANT_NATIVE # define CARDINAL_VARIANT_NAME "native" #elif CARDINAL_VARIANT_SYNTH # define CARDINAL_VARIANT_NAME "synth" #else # define CARDINAL_VARIANT_NAME "main" #endif #ifdef DISTRHO_OS_WASM # if CARDINAL_VARIANT_MINI # define CARDINAL_WASM_WELCOME_TEMPLATE_FILENAME "welcome-wasm-mini" # else # define CARDINAL_WASM_WELCOME_TEMPLATE_FILENAME "welcome-wasm" # endif #endif namespace rack { namespace asset { std::string patchesPath(); void destroy(); } namespace plugin { void initStaticPlugins(); void destroyStaticPlugins(); } } const std::string CARDINAL_VERSION = "24.12"; // ----------------------------------------------------------------------------------------------------------- #ifndef HEADLESS void handleHostParameterDrag(const CardinalPluginContext* pcontext, uint index, bool started) { DISTRHO_SAFE_ASSERT_RETURN(pcontext->ui != nullptr,); #ifndef CARDINAL_COMMON_DSP_ONLY if (started) { pcontext->ui->editParameter(index, true); pcontext->ui->setParameterValue(index, pcontext->parameters[index]); } else { pcontext->ui->editParameter(index, false); } #endif } #endif // -------------------------------------------------------------------------------------------------------------------- CardinalPluginContext::CardinalPluginContext(Plugin* const p) : bufferSize(p != nullptr ? p->getBufferSize() : 0), processCounter(0), sampleRate(p != nullptr ? p->getSampleRate() : 0.0), #if CARDINAL_VARIANT_MAIN variant(kCardinalVariantMain), #elif CARDINAL_VARIANT_MINI variant(kCardinalVariantMini), #elif CARDINAL_VARIANT_FX variant(kCardinalVariantFX), #elif CARDINAL_VARIANT_NATIVE variant(kCardinalVariantNative), #elif CARDINAL_VARIANT_SYNTH variant(kCardinalVariantSynth), #else #error cardinal variant not set #endif bypassed(false), playing(false), reset(false), bbtValid(false), bar(1), beat(1), beatsPerBar(4), beatType(4), frame(0), barStartTick(0.0), beatsPerMinute(120.0), tick(0.0), tickClock(0.0), ticksPerBeat(0.0), ticksPerClock(0.0), ticksPerFrame(0.0), nativeWindowId(0), dataIns(nullptr), dataOuts(nullptr), midiEvents(nullptr), midiEventCount(0), plugin(p), tlw(nullptr), ui(nullptr) { std::memset(parameters, 0, sizeof(parameters)); } bool CardinalPluginContext::addIdleCallback(IdleCallback* const cb) const { #if !(defined(HEADLESS) || defined(CARDINAL_COMMON_DSP_ONLY)) if (ui != nullptr) { ui->addIdleCallback(cb); return true; } #else // unused (void)cb; #endif return false; } void CardinalPluginContext::removeIdleCallback(IdleCallback* const cb) const { #if !(defined(HEADLESS) || defined(CARDINAL_COMMON_DSP_ONLY)) if (ui != nullptr) ui->removeIdleCallback(cb); #else // unused (void)cb; #endif } void CardinalPluginContext::writeMidiMessage(const rack::midi::Message& message, const uint8_t channel) { if (bypassed) return; const size_t size = message.bytes.size(); DISTRHO_SAFE_ASSERT_RETURN(size > 0,); DISTRHO_SAFE_ASSERT_RETURN(message.frame >= 0,); MidiEvent event; event.frame = message.frame; switch (message.bytes[0] & 0xF0) { case 0x80: case 0x90: case 0xA0: case 0xB0: case 0xE0: event.size = 3; break; case 0xC0: case 0xD0: event.size = 2; break; case 0xF0: switch (message.bytes[0] & 0x0F) { case 0x0: case 0x4: case 0x5: case 0x7: case 0x9: case 0xD: // unsupported return; case 0x1: case 0x2: case 0x3: case 0xE: event.size = 3; break; case 0x6: case 0x8: case 0xA: case 0xB: case 0xC: case 0xF: event.size = 1; break; } break; default: // invalid return; } DISTRHO_SAFE_ASSERT_RETURN(size >= event.size,); std::memcpy(event.data, message.bytes.data(), event.size); if (channel != 0 && event.data[0] < 0xF0) event.data[0] |= channel & 0x0F; plugin->writeMidiEvent(event); } // ----------------------------------------------------------------------------------------------------------- namespace rack { namespace midi { struct InputQueue::Internal { CardinalPluginContext* const pcontext = static_cast(APP); const CardinalDISTRHO::MidiEvent* midiEvents = nullptr; uint32_t midiEventsLeft = 0; uint32_t lastProcessCounter = 0; int64_t lastBlockFrame = 0; }; InputQueue::InputQueue() { internal = new Internal; } InputQueue::~InputQueue() { delete internal; } bool InputQueue::tryPop(Message* const messageOut, int64_t maxFrame) { const uint32_t processCounter = internal->pcontext->processCounter; const bool processCounterChanged = internal->lastProcessCounter != processCounter; if (processCounterChanged) { internal->lastBlockFrame = internal->pcontext->engine->getBlockFrame(); internal->lastProcessCounter = processCounter; internal->midiEvents = internal->pcontext->midiEvents; internal->midiEventsLeft = internal->pcontext->midiEventCount; } if (internal->midiEventsLeft == 0 || maxFrame < internal->lastBlockFrame) return false; const uint32_t frame = maxFrame - internal->lastBlockFrame; if (frame > internal->midiEvents->frame) return false; const CardinalDISTRHO::MidiEvent& midiEvent(*internal->midiEvents); const uint8_t* data; if (midiEvent.size > CardinalDISTRHO::MidiEvent::kDataSize) { data = midiEvent.dataExt; messageOut->bytes.resize(midiEvent.size); } else { data = midiEvent.data; } messageOut->frame = frame; std::memcpy(messageOut->bytes.data(), data, midiEvent.size); ++internal->midiEvents; --internal->midiEventsLeft; return true; } json_t* InputQueue::toJson() const { return nullptr; } void InputQueue::fromJson(json_t* rootJ) { } } } // ----------------------------------------------------------------------------------------------------------- START_NAMESPACE_DISTRHO // ----------------------------------------------------------------------------------------------------------- #ifdef HAVE_LIBLO static void osc_error_handler(int num, const char* msg, const char* path) { d_stderr("Cardinal OSC Error: code: %i, msg: \"%s\", path: \"%s\")", num, msg, path); } static int osc_fallback_handler(const char* const path, const char* const types, lo_arg**, int, lo_message, void*) { d_stderr("Cardinal OSC unhandled message \"%s\" with types \"%s\"", path, types); return 0; } static int osc_hello_handler(const char*, const char*, lo_arg**, int, const lo_message m, void* const self) { d_stdout("Hello received from OSC, saying hello back to them o/"); const lo_address source = lo_message_get_source(m); const lo_server server = static_cast(self)->oscServer; // send list of features first #ifdef CARDINAL_INIT_OSC_THREAD lo_send_from(source, server, LO_TT_IMMEDIATE, "/resp", "ss", "features", ":screenshot:"); #else lo_send_from(source, server, LO_TT_IMMEDIATE, "/resp", "ss", "features", ""); #endif // then finally hello reply lo_send_from(source, server, LO_TT_IMMEDIATE, "/resp", "ss", "hello", "ok"); return 0; } static int osc_load_handler(const char*, const char* types, lo_arg** argv, int argc, const lo_message m, void* const self) { d_debug("osc_load_handler()"); DISTRHO_SAFE_ASSERT_RETURN(argc == 1, 0); DISTRHO_SAFE_ASSERT_RETURN(types != nullptr && types[0] == 'b', 0); const int32_t size = argv[0]->blob.size; DISTRHO_SAFE_ASSERT_RETURN(size > 4, 0); const uint8_t* const blob = (uint8_t*)(&argv[0]->blob.data); DISTRHO_SAFE_ASSERT_RETURN(blob != nullptr, 0); bool ok = false; if (CardinalBasePlugin* const plugin = static_cast(self)->remotePluginInstance) { CardinalPluginContext* const context = plugin->context; std::vector data(size); std::memcpy(data.data(), blob, size); #ifdef CARDINAL_INIT_OSC_THREAD rack::contextSet(context); #endif rack::system::removeRecursively(context->patch->autosavePath); rack::system::createDirectories(context->patch->autosavePath); try { rack::system::unarchiveToDirectory(data, context->patch->autosavePath); context->patch->loadAutosave(); ok = true; } catch (rack::Exception& e) { WARN("%s", e.what()); } #ifdef CARDINAL_INIT_OSC_THREAD rack::contextSet(nullptr); #endif } const lo_address source = lo_message_get_source(m); const lo_server server = static_cast(self)->oscServer; lo_send_from(source, server, LO_TT_IMMEDIATE, "/resp", "ss", "load", ok ? "ok" : "fail"); return 0; } static int osc_param_handler(const char*, const char* types, lo_arg** argv, int argc, const lo_message m, void* const self) { d_debug("osc_param_handler()"); DISTRHO_SAFE_ASSERT_RETURN(argc == 3, 0); DISTRHO_SAFE_ASSERT_RETURN(types != nullptr, 0); DISTRHO_SAFE_ASSERT_RETURN(types[0] == 'h', 0); DISTRHO_SAFE_ASSERT_RETURN(types[1] == 'i', 0); DISTRHO_SAFE_ASSERT_RETURN(types[2] == 'f', 0); if (CardinalBasePlugin* const plugin = static_cast(self)->remotePluginInstance) { CardinalPluginContext* const context = plugin->context; const int64_t moduleId = argv[0]->h; const int paramId = argv[1]->i; const float paramValue = argv[2]->f; #ifdef CARDINAL_INIT_OSC_THREAD rack::contextSet(context); #endif rack::engine::Module* const module = context->engine->getModule(moduleId); DISTRHO_SAFE_ASSERT_RETURN(module != nullptr, 0); context->engine->setParamValue(module, paramId, paramValue); #ifdef CARDINAL_INIT_OSC_THREAD rack::contextSet(nullptr); #endif } return 0; } static int osc_host_param_handler(const char*, const char* types, lo_arg** argv, int argc, const lo_message m, void* const self) { d_debug("osc_host_param_handler()"); DISTRHO_SAFE_ASSERT_RETURN(argc == 2, 0); DISTRHO_SAFE_ASSERT_RETURN(types != nullptr, 0); DISTRHO_SAFE_ASSERT_RETURN(types[0] == 'i', 0); DISTRHO_SAFE_ASSERT_RETURN(types[1] == 'f', 0); if (CardinalBasePlugin* const plugin = static_cast(self)->remotePluginInstance) { CardinalPluginContext* const context = plugin->context; const int paramId = argv[0]->i; DISTRHO_SAFE_ASSERT_RETURN(paramId >= 0, 0); const uint uparamId = static_cast(paramId); DISTRHO_SAFE_ASSERT_UINT2_RETURN(uparamId < kModuleParameterCount, uparamId, kModuleParameterCount, 0); const float paramValue = argv[1]->f; context->parameters[uparamId] = paramValue; } return 0; } # ifdef CARDINAL_INIT_OSC_THREAD static int osc_screenshot_handler(const char*, const char* types, lo_arg** argv, int argc, const lo_message m, void* const self) { d_debug("osc_screenshot_handler()"); DISTRHO_SAFE_ASSERT_RETURN(argc == 1, 0); DISTRHO_SAFE_ASSERT_RETURN(types != nullptr && types[0] == 'b', 0); const int32_t size = argv[0]->blob.size; DISTRHO_SAFE_ASSERT_RETURN(size > 4, 0); const uint8_t* const blob = (uint8_t*)(&argv[0]->blob.data); DISTRHO_SAFE_ASSERT_RETURN(blob != nullptr, 0); bool ok = false; if (CardinalBasePlugin* const plugin = static_cast(self)->remotePluginInstance) { if (char* const screenshot = String::asBase64(blob, size).getAndReleaseBuffer()) { ok = plugin->updateStateValue("screenshot", screenshot); std::free(screenshot); } } const lo_address source = lo_message_get_source(m); const lo_server server = static_cast(self)->oscServer; lo_send_from(source, server, LO_TT_IMMEDIATE, "/resp", "ss", "screenshot", ok ? "ok" : "fail"); return 0; } # endif #endif // ----------------------------------------------------------------------------------------------------------- #if defined(DISTRHO_OS_WASM) && !defined(CARDINAL_COMMON_UI_ONLY) static void WebBrowserDataLoaded(void* const data) { static_cast(data)->loadSettings(true); } #endif // ----------------------------------------------------------------------------------------------------------- Initializer::Initializer(const CardinalBasePlugin* const plugin, const CardinalBaseUI* const ui) { using namespace rack; // Cardinal default settings, potentially overriding VCV Rack ones settings::allowCursorLock = false; settings::tooltips = true; settings::cableOpacity = 0.5f; settings::cableTension = 0.75f; settings::rackBrightness = 1.0f; settings::haloBrightness = 0.25f; settings::knobMode = settings::KNOB_MODE_LINEAR; settings::knobScroll = false; settings::knobScrollSensitivity = 0.001f; settings::lockModules = false; settings::browserSort = settings::BROWSER_SORT_UPDATED; settings::browserZoom = -1.f; settings::invertZoom = false; settings::squeezeModules = true; settings::preferDarkPanels = true; settings::uiTheme = "dark"; // runtime behaviour settings::devMode = true; settings::isPlugin = true; #ifdef HEADLESS_BEHAVIOUR settings::headless = true; #endif // copied from https://community.vcvrack.com/t/16-colour-cable-palette/15951 settings::cableColors = { color::fromHexString("#ff5252"), color::fromHexString("#ff9352"), color::fromHexString("#ffd452"), color::fromHexString("#e8ff52"), color::fromHexString("#a8ff52"), color::fromHexString("#67ff52"), color::fromHexString("#52ff7d"), color::fromHexString("#52ffbe"), color::fromHexString("#52ffff"), color::fromHexString("#52beff"), color::fromHexString("#527dff"), color::fromHexString("#6752ff"), color::fromHexString("#a852ff"), color::fromHexString("#e952ff"), color::fromHexString("#ff52d4"), color::fromHexString("#ff5293"), }; system::init(); logger::init(); random::init(); ui::init(); #ifdef CARDINAL_COMMON_UI_ONLY constexpr const bool isRealInstance = true; #else const bool isRealInstance = !plugin->isDummyInstance(); #endif if (asset::systemDir.empty()) { if (const char* const bundlePath = (plugin != nullptr ? plugin->getBundlePath() : #if DISTRHO_PLUGIN_HAS_UI ui != nullptr ? ui->getBundlePath() : #endif nullptr)) { if (const char* const resourcePath = getResourcePath(bundlePath)) { asset::systemDir = resourcePath; asset::bundlePath = system::join(asset::systemDir, "PluginManifests"); } } if (asset::systemDir.empty() || ! system::exists(asset::systemDir) || ! system::exists(asset::bundlePath)) { #ifdef CARDINAL_PLUGIN_SOURCE_DIR // Make system dir point to source code location as fallback asset::systemDir = CARDINAL_PLUGIN_SOURCE_DIR DISTRHO_OS_SEP_STR "Rack"; asset::bundlePath.clear(); // If source code dir does not exist use install target prefix as system dir if (!system::exists(system::join(asset::systemDir, "res"))) #endif { #if defined(DISTRHO_OS_WASM) asset::systemDir = "/resources"; #elif defined(ARCH_MAC) asset::systemDir = "/Library/Application Support/Cardinal"; #elif defined(ARCH_WIN) asset::systemDir = system::join(getSpecialPath(kSpecialPathCommonProgramFiles), "Cardinal"); #else asset::systemDir = CARDINAL_PLUGIN_PREFIX "/share/cardinal"; #endif asset::bundlePath = system::join(asset::systemDir, "PluginManifests"); } } } if (asset::userDir.empty()) { #if defined(DISTRHO_OS_WASM) asset::userDir = "/userfiles"; #elif defined(ARCH_MAC) asset::userDir = system::join(homeDir(), "Documents", "Cardinal"); #elif defined(ARCH_WIN) asset::userDir = system::join(getSpecialPath(kSpecialPathMyDocuments), "Cardinal"); #else std::string xdgConfigDir; if (const char* const xdgEnv = getenv("XDG_CONFIG_HOME")) xdgConfigDir = xdgEnv; if (xdgConfigDir.empty()) xdgConfigDir = system::join(homeDir(), ".config"); const std::string xdgDirsConfigPath(system::join(xdgConfigDir, "user-dirs.dirs")); if (system::exists(xdgDirsConfigPath)) { std::ifstream xdgDirsConfigFile(xdgDirsConfigPath, std::ios::in|std::ios::ate); std::string xdgDirsConfig(xdgDirsConfigFile.tellg(), 0); xdgDirsConfigFile.seekg(0); xdgDirsConfigFile.read(&xdgDirsConfig[0], xdgDirsConfig.size()); if (const char* const xdgDocsDir = std::strstr(xdgDirsConfig.c_str(), "XDG_DOCUMENTS_DIR=\"")) { if (const char* const xdgDocsDirNL = std::strstr(xdgDocsDir, "\"\n")) { asset::userDir = std::string(xdgDocsDir + 19, xdgDocsDirNL - xdgDocsDir - 19); if (string::startsWith(asset::userDir, "$HOME")) asset::userDir.replace(asset::userDir.begin(), asset::userDir.begin() + 5, homeDir()); if (! system::exists(asset::userDir)) asset::userDir.clear(); } } } if (asset::userDir.empty()) asset::userDir = system::join(homeDir(), "Documents", "Cardinal"); #endif if (isRealInstance) { system::createDirectory(asset::userDir); #if defined(DISTRHO_OS_WASM) && !defined(CARDINAL_COMMON_UI_ONLY) EM_ASM({ Module.FS.mount(Module.IDBFS, {}, '/userfiles'); Module.FS.syncfs(true, function(err) { if (!err) { dynCall('vi', $0, [$1]) } }); }, WebBrowserDataLoaded, this); #endif } } #ifndef CARDINAL_COMMON_DSP_ONLY if (asset::configDir.empty()) { #if defined(ARCH_MAC) || defined(ARCH_WIN) || defined(DISTRHO_OS_WASM) asset::configDir = asset::userDir; #else if (const char* const xdgEnv = getenv("XDG_CONFIG_HOME")) asset::configDir = system::join(xdgEnv, "Cardinal"); else asset::configDir = system::join(homeDir(), ".config", "Cardinal"); if (isRealInstance) system::createDirectory(asset::configDir); #endif } #endif if (settings::settingsPath.empty()) settings::settingsPath = asset::config(CARDINAL_VARIANT_NAME ".json"); templatePath = asset::user("templates/" CARDINAL_VARIANT_NAME ".vcv"); #ifdef DISTRHO_OS_WASM factoryTemplatePath = system::join(asset::patchesPath(), CARDINAL_WASM_WELCOME_TEMPLATE_FILENAME ".vcv"); #else factoryTemplatePath = system::join(asset::patchesPath(), "templates/" CARDINAL_VARIANT_NAME ".vcv"); #endif // Log environment INFO("%s %s %s, compatible with Rack version %s", APP_NAME.c_str(), APP_EDITION.c_str(), CARDINAL_VERSION.c_str(), APP_VERSION.c_str()); INFO("%s", system::getOperatingSystemInfo().c_str()); INFO("Binary filename: %s", getBinaryFilename()); if (plugin != nullptr) { INFO("Bundle path: %s", plugin->getBundlePath()); #if DISTRHO_PLUGIN_HAS_UI } else if (ui != nullptr) { INFO("Bundle path: %s", ui->getBundlePath()); #endif } INFO("System directory: %s", asset::systemDir.c_str()); INFO("User directory: %s", asset::userDir.c_str()); INFO("Template patch: %s", templatePath.c_str()); INFO("System template patch: %s", factoryTemplatePath.c_str()); // Report to user if something is wrong with the installation if (asset::systemDir.empty()) { d_stderr2("Failed to locate Cardinal plugin bundle.\n" "Install Cardinal with its bundle folder intact and try again."); } else if (! system::exists(asset::systemDir)) { d_stderr2("System directory \"%s\" does not exist.\n" "Make sure Cardinal was downloaded and installed correctly.", asset::systemDir.c_str()); } INFO("Initializing plugins"); plugin::initStaticPlugins(); INFO("Initializing plugin browser DB"); app::browserInit(); loadSettings(isRealInstance); #if defined(CARDINAL_INIT_OSC_THREAD) INFO("Initializing OSC Remote control"); const char* port; if (const char* const portEnv = std::getenv("CARDINAL_REMOTE_HOST_PORT")) port = portEnv; else port = CARDINAL_DEFAULT_REMOTE_PORT; startRemoteServer(port); #elif defined(HAVE_LIBLO) if (isStandalone()) { INFO("OSC Remote control is available on request"); } else { INFO("OSC Remote control is not available on plugin variants"); } #else INFO("OSC Remote control is not enabled in this build"); #endif } Initializer::~Initializer() { using namespace rack; #ifdef HAVE_LIBLO stopRemoteServer(); #endif if (shouldSaveSettings) { INFO("Save settings"); settings::save(); } INFO("Clearing asset paths"); asset::bundlePath.clear(); asset::systemDir.clear(); asset::userDir.clear(); INFO("Destroying plugins"); plugin::destroyStaticPlugins(); INFO("Destroying colourized assets"); asset::destroy(); INFO("Destroying settings"); settings::destroy(); INFO("Destroying logger"); logger::destroy(); } void Initializer::loadSettings(const bool isRealInstance) { using namespace rack; if (isRealInstance) { INFO("Loading settings"); settings::load(); shouldSaveSettings = true; } // enforce settings that do not make sense as anything else settings::safeMode = false; settings::token.clear(); settings::windowMaximized = false; settings::windowPos = math::Vec(0, 0); settings::pixelRatio = 0.0; settings::sampleRate = 0; settings::threadCount = 1; settings::autosaveInterval = 0; settings::skipLoadOnLaunch = true; settings::autoCheckUpdates = false; settings::showTipsOnLaunch = false; settings::tipIndex = -1; if (settings::uiTheme != "dark" && settings::uiTheme != "light") { settings::uiTheme = "dark"; rack::ui::refreshTheme(); } // reload dark/light mode as necessary switchDarkMode(settings::uiTheme == "dark"); } #ifdef HAVE_LIBLO bool Initializer::startRemoteServer(const char* const port) { #ifdef CARDINAL_INIT_OSC_THREAD if (oscServerThread != nullptr) return true; if ((oscServerThread = lo_server_thread_new_with_proto(port, LO_UDP, osc_error_handler)) == nullptr) return false; oscServer = lo_server_thread_get_server(oscServerThread); lo_server_thread_add_method(oscServerThread, "/hello", "", osc_hello_handler, this); lo_server_thread_add_method(oscServerThread, "/host-param", "if", osc_host_param_handler, this); lo_server_thread_add_method(oscServerThread, "/load", "b", osc_load_handler, this); lo_server_thread_add_method(oscServerThread, "/param", "hif", osc_param_handler, this); lo_server_thread_add_method(oscServerThread, "/screenshot", "b", osc_screenshot_handler, this); lo_server_thread_add_method(oscServerThread, nullptr, nullptr, osc_fallback_handler, nullptr); lo_server_thread_start(oscServerThread); #else if (oscServer != nullptr) return true; if ((oscServer = lo_server_new_with_proto(port, LO_UDP, osc_error_handler)) == nullptr) return false; lo_server_add_method(oscServer, "/hello", "", osc_hello_handler, this); lo_server_add_method(oscServer, "/host-param", "if", osc_host_param_handler, this); lo_server_add_method(oscServer, "/load", "b", osc_load_handler, this); lo_server_add_method(oscServer, "/param", "hif", osc_param_handler, this); lo_server_add_method(oscServer, nullptr, nullptr, osc_fallback_handler, nullptr); #endif return true; } void Initializer::stopRemoteServer() { DISTRHO_SAFE_ASSERT(remotePluginInstance == nullptr); #ifdef CARDINAL_INIT_OSC_THREAD if (oscServerThread != nullptr) { lo_server_thread_stop(oscServerThread); lo_server_thread_del_method(oscServerThread, nullptr, nullptr); lo_server_thread_free(oscServerThread); oscServerThread = oscServer = nullptr; } #else if (oscServer != nullptr) { lo_server_del_method(oscServer, nullptr, nullptr); lo_server_free(oscServer); oscServer = nullptr; } #endif } void Initializer::stepRemoteServer() { DISTRHO_SAFE_ASSERT_RETURN(oscServer != nullptr,); DISTRHO_SAFE_ASSERT_RETURN(remotePluginInstance != nullptr,); #ifndef CARDINAL_INIT_OSC_THREAD for (;;) { try { if (lo_server_recv_noblock(oscServer, 0) == 0) break; } DISTRHO_SAFE_EXCEPTION_CONTINUE("stepRemoteServer") } #endif } #endif // HAVE_LIBLO // -------------------------------------------------------------------------------------------------------------------- END_NAMESPACE_DISTRHO // -------------------------------------------------------------------------------------------------------------------- namespace rack { bool isMini() { #if CARDINAL_VARIANT_MINI return true; #else return false; #endif } bool isStandalone() { static const bool standalone = std::strstr(getPluginFormatName(), "Standalone") != nullptr; return standalone; } #ifdef ARCH_WIN std::string getSpecialPath(const SpecialPath type) { int csidl; switch (type) { case kSpecialPathUserProfile: csidl = CSIDL_PROFILE; break; case kSpecialPathCommonProgramFiles: csidl = CSIDL_PROGRAM_FILES_COMMON; break; case kSpecialPathProgramFiles: csidl = CSIDL_PROGRAM_FILES; break; case kSpecialPathAppData: csidl = CSIDL_APPDATA; break; case kSpecialPathMyDocuments: csidl = CSIDL_MYDOCUMENTS; break; default: return {}; } WCHAR path[MAX_PATH] = {}; if (SHGetFolderPathW(nullptr, csidl, nullptr, SHGFP_TYPE_CURRENT, path) == S_OK) return string::UTF16toUTF8(path); return {}; } #endif #ifdef DISTRHO_OS_WASM char* patchFromURL = nullptr; char* patchRemoteURL = nullptr; char* patchStorageSlug = nullptr; void syncfs() { settings::save(); #ifndef CARDINAL_COMMON_UI_ONLY EM_ASM({ Module.FS.syncfs(false, function(){} ); }); #endif } #endif std::string homeDir() { #ifdef ARCH_WIN return getSpecialPath(kSpecialPathUserProfile); #else if (const char* const home = getenv("HOME")) return home; if (struct passwd* const pwd = getpwuid(getuid())) return pwd->pw_dir; #endif return {}; } } // namespace rack // -------------------------------------------------------------------------------------------------------------------- namespace patchUtils { using namespace rack; #ifndef HEADLESS_BEHAVIOUR static void promptClear(const char* const message, const std::function action) { if (APP->history->isSaved() || APP->scene->rack->hasModules()) return action(); asyncDialog::create(message, action); } #endif void loadDialog() { #ifndef HEADLESS_BEHAVIOUR promptClear("The current patch is unsaved. Clear it and open a new patch?", []() { std::string dir; if (! APP->patch->path.empty()) dir = system::getDirectory(APP->patch->path); else dir = homeDir(); CardinalPluginContext* const pcontext = static_cast(APP); DISTRHO_SAFE_ASSERT_RETURN(pcontext != nullptr,); CardinalBaseUI* const ui = static_cast(pcontext->ui); DISTRHO_SAFE_ASSERT_RETURN(ui != nullptr,); DISTRHO_NAMESPACE::FileBrowserOptions opts; opts.saving = ui->saving = false; opts.startDir = dir.c_str(); opts.title = "Open patch"; ui->openFileBrowser(opts); }); #endif } void loadPathDialog(const std::string& path, const bool asTemplate) { #ifndef HEADLESS_BEHAVIOUR promptClear("The current patch is unsaved. Clear it and open the new patch?", [path, asTemplate]() { APP->patch->loadAction(path); if (asTemplate) { APP->patch->path = ""; APP->history->setSaved(); } #ifdef DISTRHO_OS_WASM syncfs(); #endif if (remoteUtils::RemoteDetails* const remoteDetails = remoteUtils::getRemote()) if (remoteDetails->autoDeploy) remoteUtils::sendFullPatchToRemote(remoteDetails); }); #endif } void loadSelectionDialog() { app::RackWidget* const w = APP->scene->rack; std::string selectionDir = asset::user("selections"); system::createDirectories(selectionDir); async_dialog_filebrowser(false, nullptr, selectionDir.c_str(), "Import selection", [w](char* pathC) { if (!pathC) { // No path selected return; } try { w->loadSelection(pathC); } catch (Exception& e) { async_dialog_message(e.what()); } std::free(pathC); #ifdef DISTRHO_OS_WASM syncfs(); #endif if (remoteUtils::RemoteDetails* const remoteDetails = remoteUtils::getRemote()) if (remoteDetails->autoDeploy) remoteUtils::sendFullPatchToRemote(remoteDetails); }); } void loadTemplate(const bool factory) { try { APP->patch->load(factory ? APP->patch->factoryTemplatePath : APP->patch->templatePath); } catch (Exception& e) { // if user template failed, try the factory one if (!factory) return loadTemplate(true); const std::string message = string::f("Could not load template patch, clearing rack: %s", e.what()); asyncDialog::create(message.c_str()); APP->patch->clear(); APP->patch->clearAutosave(); } // load() sets the patch's original patch, but we don't want to use that. APP->patch->path.clear(); APP->history->setSaved(); #ifdef DISTRHO_OS_WASM syncfs(); #endif if (remoteUtils::RemoteDetails* const remoteDetails = remoteUtils::getRemote()) if (remoteDetails->autoDeploy) remoteUtils::sendFullPatchToRemote(remoteDetails); } void loadTemplateDialog(const bool factory) { #ifndef HEADLESS_BEHAVIOUR promptClear("The current patch is unsaved. Clear it and start a new patch?", [factory]() { loadTemplate(factory); }); #endif } void revertDialog() { #ifndef HEADLESS_BEHAVIOUR if (APP->patch->path.empty()) return; promptClear("Revert patch to the last saved state?", []{ APP->patch->loadAction(APP->patch->path); #ifdef DISTRHO_OS_WASM syncfs(); #endif if (remoteUtils::RemoteDetails* const remoteDetails = remoteUtils::getRemote()) if (remoteDetails->autoDeploy) remoteUtils::sendFullPatchToRemote(remoteDetails); }); #endif } void saveDialog(const std::string& path) { #ifndef HEADLESS_BEHAVIOUR if (path.empty()) { return; } // Note: If save() fails below, this should probably be reset. But we need it so toJson() doesn't set the "unsaved" property. APP->history->setSaved(); try { APP->patch->save(path); } catch (Exception& e) { asyncDialog::create(string::f("Could not save patch: %s", e.what()).c_str()); return; } APP->patch->pushRecentPath(path); #ifdef DISTRHO_OS_WASM syncfs(); #else rack::settings::save(); #endif #endif } #ifndef HEADLESS_BEHAVIOUR static void saveAsDialog(const bool uncompressed) { std::string dir; if (! APP->patch->path.empty()) { dir = system::getDirectory(APP->patch->path); } else { dir = asset::user("patches"); system::createDirectories(dir); } CardinalPluginContext* const pcontext = static_cast(APP); DISTRHO_SAFE_ASSERT_RETURN(pcontext != nullptr,); CardinalBaseUI* const ui = static_cast(pcontext->ui); DISTRHO_SAFE_ASSERT_RETURN(ui != nullptr,); DISTRHO_NAMESPACE::FileBrowserOptions opts; opts.saving = ui->saving = true; opts.defaultName = "patch.vcv"; opts.startDir = dir.c_str(); opts.title = "Save patch"; ui->savingUncompressed = uncompressed; ui->openFileBrowser(opts); } #endif void saveAsDialog() { #ifndef HEADLESS_BEHAVIOUR saveAsDialog(false); #endif } void saveAsDialogUncompressed() { #ifndef HEADLESS_BEHAVIOUR saveAsDialog(true); #endif } void saveTemplateDialog() { asyncDialog::create("Overwrite template patch?", []{ rack::system::createDirectories(system::getDirectory(APP->patch->templatePath)); try { APP->patch->save(APP->patch->templatePath); } catch (Exception& e) { asyncDialog::create(string::f("Could not save template patch: %s", e.what()).c_str()); return; } #ifdef DISTRHO_OS_WASM syncfs(); #endif }); } void openBrowser(const std::string& url) { #ifdef DISTRHO_OS_WASM EM_ASM({ window.open(UTF8ToString($0), '_blank'); }, url.c_str()); #else system::openBrowser(url); #endif } } // -------------------------------------------------------------------------------------------------------------------- void async_dialog_filebrowser(const bool saving, const char* const defaultName, const char* const startDir, const char* const title, const std::function action) { #ifndef HEADLESS_BEHAVIOUR CardinalPluginContext* const pcontext = static_cast(APP); DISTRHO_SAFE_ASSERT_RETURN(pcontext != nullptr,); CardinalBaseUI* const ui = static_cast(pcontext->ui); DISTRHO_SAFE_ASSERT_RETURN(ui != nullptr,); // only 1 dialog possible at a time DISTRHO_SAFE_ASSERT_RETURN(ui->filebrowserhandle == nullptr,); DISTRHO_NAMESPACE::FileBrowserOptions opts; opts.saving = saving; opts.defaultName = defaultName; opts.startDir = startDir; opts.title = title; ui->filebrowseraction = action; ui->filebrowserhandle = fileBrowserCreate(true, pcontext->nativeWindowId, pcontext->window->pixelRatio, opts); #endif } void async_dialog_message(const char* const message) { #ifndef HEADLESS_BEHAVIOUR asyncDialog::create(message); #endif } void async_dialog_message(const char* const message, const std::function action) { #ifndef HEADLESS_BEHAVIOUR asyncDialog::create(message, action); #endif } void async_dialog_text_input(const char* const message, const char* const text, const std::function action) { #ifndef HEADLESS_BEHAVIOUR asyncDialog::textInput(message, text, action); #endif }