diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d1dc708..b3820cb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,9 +3,9 @@ name: build on: [push, pull_request] env: - CACHE_VERSION_LINUX: 24 - CACHE_VERSION_MACOS: 20 - CACHE_VERSION_WIN64: 19 + CACHE_VERSION_LINUX: 25 + CACHE_VERSION_MACOS: 25 + CACHE_VERSION_WIN64: 25 DEBIAN_FRONTEND: noninteractive HOMEBREW_NO_AUTO_UPDATE: 1 PAWPAW_FAST_MATH: 1 diff --git a/.gitmodules b/.gitmodules index 9d99888..cf8e5ef 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,6 @@ [submodule "src/PawPaw"] path = src/PawPaw url = https://github.com/DISTRHO/PawPaw.git +[submodule "src/DPF"] + path = src/DPF + url = https://github.com/DISTRHO/DPF.git diff --git a/Makefile b/Makefile index 932f3f0..9c4337e 100644 --- a/Makefile +++ b/Makefile @@ -79,7 +79,11 @@ PAWPAW_PREFIX = $(PAWPAW_DIR)/targets/$(PAWPAW_TARGET)$(PAWPAW_SUFFIX) # --------------------------------------------------------------------------------------------------------------------- # List of files created by PawPaw bootstrap, to ensure we have run it at least once +ifeq ($(MACOS),true) BOOTSTRAP_FILES = $(PAWPAW_PREFIX)/bin/cxfreeze +else +BOOTSTRAP_FILES = $(PAWPAW_PREFIX)/bin/cxfreeze-quickstart +endif BOOTSTRAP_FILES += $(PAWPAW_PREFIX)/bin/jackd$(APP_EXT) BOOTSTRAP_FILES += $(PAWPAW_PREFIX)/include/armadillo @@ -102,6 +106,7 @@ TARGETS += build/mod-desktop.app/Contents/MacOS/jack/jack-session.conf TARGETS += build/mod-desktop.app/Contents/MacOS/jack/jack_coreaudio.so TARGETS += build/mod-desktop.app/Contents/MacOS/jack/jack_coremidi.so TARGETS += build/mod-desktop.app/Contents/MacOS/jack/jack_dummy.so +TARGETS += build/mod-desktop.app/Contents/MacOS/jack/jack_mod-desktop.so TARGETS += build/mod-desktop.app/Contents/MacOS/jack/mod-host.so TARGETS += build/mod-desktop.app/Contents/MacOS/jack/mod-midi-broadcaster.so TARGETS += build/mod-desktop.app/Contents/MacOS/jack/mod-midi-merger.so @@ -143,6 +148,7 @@ TARGETS += build/pedalboards TARGETS += build/VERSION ifeq ($(WINDOWS),true) TARGETS += build/jack/jack_dummy.dll +TARGETS += build/jack/jack_mod-desktop.dll TARGETS += build/jack/jack_portaudio.dll TARGETS += build/jack/jack_winmme.dll TARGETS += build/libjack64.dll @@ -162,6 +168,7 @@ else TARGETS += build/jack/alsa_midi.so TARGETS += build/jack/jack_alsa.so TARGETS += build/jack/jack_dummy.so +TARGETS += build/jack/jack_mod-desktop.so TARGETS += build/jack/jack_portaudio.so TARGETS += build/jack/jack-session-alsamidi.conf TARGETS += build/libjack.so.0 @@ -274,10 +281,13 @@ TARGETS += $(foreach PLUGIN,$(PLUGINS),$(call PLUGIN_STAMP,$(PLUGIN))) # --------------------------------------------------------------------------------------------------------------------- all: $(TARGETS) + ./utils/run.sh $(PAWPAW_TARGET) $(MAKE) -C src/plugin clean: + $(MAKE) clean -C src/DPF $(MAKE) clean -C src/mod-host $(MAKE) clean -C src/mod-ui/utils + $(MAKE) clean -C src/plugin $(MAKE) clean -C src/systray rm -rf build rm -rf build-midi-merger @@ -318,6 +328,9 @@ win64-app: win64-bootstrap: ./src/PawPaw/bootstrap-mod.sh win64 +win64-plugin: + ./utils/run.sh win64 $(MAKE) -C src/plugin + win64-plugins: $(MAKE) PAWPAW_TARGET=win64 plugins diff --git a/src/DPF b/src/DPF new file mode 160000 index 0000000..ba985c6 --- /dev/null +++ b/src/DPF @@ -0,0 +1 @@ +Subproject commit ba985c6578e55291671685edb7485f04dd0ac9fe diff --git a/src/PawPaw b/src/PawPaw index adf8b2a..43cc25c 160000 --- a/src/PawPaw +++ b/src/PawPaw @@ -1 +1 @@ -Subproject commit adf8b2ad4768aed4b6204ee6301739b64faf8896 +Subproject commit 43cc25c79cac4525cf07e1aaad82ae395c436883 diff --git a/src/mod-host b/src/mod-host index 8ac6a8f..c6a567c 160000 --- a/src/mod-host +++ b/src/mod-host @@ -1 +1 @@ -Subproject commit 8ac6a8f742bfebe7ece9f4122b8283dd3c073a16 +Subproject commit c6a567c36afe6aa59a19884580c54ccfc20c898d diff --git a/src/mod-ui b/src/mod-ui index 0595788..a1e043c 160000 --- a/src/mod-ui +++ b/src/mod-ui @@ -1 +1 @@ -Subproject commit 0595788f54ade32e58b897d4292d865fd805b972 +Subproject commit a1e043cb5886fb61d193fd97ec49b280e63f1a5e diff --git a/src/plugin/ChildProcess.hpp b/src/plugin/ChildProcess.hpp new file mode 100644 index 0000000..33983a8 --- /dev/null +++ b/src/plugin/ChildProcess.hpp @@ -0,0 +1,279 @@ +// SPDX-FileCopyrightText: 2023-2024 MOD Audio UG +// SPDX-License-Identifier: AGPL-3.0-or-later + +#pragma once + +#include "extra/Sleep.hpp" +#include "Time.hpp" + +#if defined(DISTRHO_OS_MAC) +#elif defined(DISTRHO_OS_WINDOWS) +#else +#endif + +#ifdef DISTRHO_OS_WINDOWS +# include +# include +# include +#else +# include +# include +# include +# include +#endif + +// #include + +START_NAMESPACE_DISTRHO + +// ----------------------------------------------------------------------------------------------------------- + +class ChildProcess +{ + #ifdef _WIN32 + PROCESS_INFORMATION process = { INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE, 0, 0 }; + #else + pid_t pid = -1; + #endif + +public: + ChildProcess() + { + } + + ~ChildProcess() + { + stop(); + } + + #ifdef _WIN32 + bool start(const char* const args[], const WCHAR* const envp) + #else + bool start(const char* const args[], char* const* const envp = nullptr) + #endif + { + #ifdef _WIN32 + std::string cmd; + + for (uint i = 0; args[i] != nullptr; ++i) + { + if (i != 0) + cmd += " "; + + if (std::strchr(args[i], ' ') != nullptr) + { + cmd += "\""; + cmd += args[i]; + cmd += "\""; + } + else + { + cmd += args[i]; + } + } + + wchar_t wcmd[PATH_MAX]; + if (MultiByteToWideChar(CP_UTF8, 0, cmd.data(), -1, wcmd, PATH_MAX) <= 0) + return false; + + STARTUPINFOW si = {}; + si.cb = sizeof(si); + + d_stdout("will start process with args '%s'", cmd.data()); + + return CreateProcessW(nullptr, // lpApplicationName + wcmd, // lpCommandLine + nullptr, // lpProcessAttributes + nullptr, // lpThreadAttributes + TRUE, // bInheritHandles + /* CREATE_NO_WINDOW | */ CREATE_UNICODE_ENVIRONMENT, // dwCreationFlags + const_cast(envp), // lpEnvironment + nullptr, // lpCurrentDirectory + &si, // lpStartupInfo + &process) != FALSE; + #else + const pid_t ret = pid = vfork(); + + switch (ret) + { + // child process + case 0: + if (envp != nullptr) + execve(args[0], const_cast(args), envp); + else + execvp(args[0], const_cast(args)); + + d_stderr2("exec failed: %d:%s", errno, std::strerror(errno)); + _exit(1); + break; + + // error + case -1: + d_stderr2("vfork() failed: %d:%s", errno, std::strerror(errno)); + break; + } + + return ret > 0; + #endif + } + + void stop(const uint32_t timeoutInMilliseconds = 2000) + { + const uint32_t timeout = d_gettime_ms() + timeoutInMilliseconds; + bool sendTerminate = true; + + #ifdef _WIN32 + if (process.hProcess == INVALID_HANDLE_VALUE) + return; + + const PROCESS_INFORMATION oprocess = process; + process = { INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE, 0, 0 }; + + for (;;) + { + switch (WaitForSingleObject(oprocess.hProcess, 0)) + { + case WAIT_OBJECT_0: + case WAIT_FAILED: + CloseHandle(oprocess.hThread); + CloseHandle(oprocess.hProcess); + return; + } + + if (sendTerminate) + { + sendTerminate = false; + TerminateProcess(oprocess.hProcess, 15); + } + if (d_gettime_ms() < timeout) + { + d_msleep(5); + continue; + } + d_stderr("ChildProcess::stop() - timed out"); + TerminateProcess(oprocess.hProcess, 9); + d_msleep(5); + CloseHandle(oprocess.hThread); + CloseHandle(oprocess.hProcess); + break; + } + #else + if (pid <= 0) + return; + + const pid_t opid = pid; + pid = -1; + + for (pid_t ret;;) + { + try { + ret = ::waitpid(opid, nullptr, WNOHANG); + } DISTRHO_SAFE_EXCEPTION_BREAK("waitpid"); + + switch (ret) + { + case -1: + if (errno == ECHILD) + { + // success, child doesn't exist + return; + } + else + { + d_stderr("ChildProcess::stop() - waitpid failed: %d:%s", errno, std::strerror(errno)); + return; + } + break; + + case 0: + if (sendTerminate) + { + sendTerminate = false; + kill(opid, SIGTERM); + } + if (d_gettime_ms() < timeout) + { + d_msleep(5); + continue; + } + + d_stderr("ChildProcess::stop() - timed out"); + kill(opid, SIGKILL); + waitpid(opid, nullptr, WNOHANG); + break; + + default: + if (ret == opid) + { + // success + return; + } + else + { + d_stderr("ChildProcess::stop() - got wrong pid %i (requested was %i)", int(ret), int(opid)); + return; + } + } + + break; + } + #endif + } + + bool isRunning() + { + #ifdef _WIN32 + if (process.hProcess == INVALID_HANDLE_VALUE) + return false; + + if (WaitForSingleObject(process.hProcess, 0) == WAIT_FAILED) + { + const PROCESS_INFORMATION oprocess = process; + process = { INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE, 0, 0 }; + CloseHandle(oprocess.hThread); + CloseHandle(oprocess.hProcess); + return false; + } + + return true; + #else + if (pid <= 0) + return false; + + const pid_t ret = ::waitpid(pid, nullptr, WNOHANG); + + if (ret == pid || (ret == -1 && errno == ECHILD)) + { + pid = 0; + return false; + } + + return true; + #endif + } + + #ifndef _WIN32 + void signal(const int sig) + { + if (pid > 0) + kill(pid, sig); + } + #endif + + void terminate() + { + #ifdef _WIN32 + if (process.hProcess != INVALID_HANDLE_VALUE) + TerminateProcess(process.hProcess, 15); + #else + if (pid > 0) + kill(pid, SIGTERM); + #endif + } + + DISTRHO_DECLARE_NON_COPYABLE(ChildProcess) +}; + +// ----------------------------------------------------------------------------------------------------------- + +END_NAMESPACE_DISTRHO diff --git a/src/plugin/DesktopPlugin.cpp b/src/plugin/DesktopPlugin.cpp new file mode 100644 index 0000000..c15c05f --- /dev/null +++ b/src/plugin/DesktopPlugin.cpp @@ -0,0 +1,544 @@ +// SPDX-FileCopyrightText: 2023-2024 MOD Audio UG +// SPDX-License-Identifier: AGPL-3.0-or-later + +#include "DistrhoPlugin.hpp" + +#include "ChildProcess.hpp" +#include "SharedMemory.hpp" +#include "extra/Runner.hpp" + +#include "utils.hpp" + +START_NAMESPACE_DISTRHO + +// ----------------------------------------------------------------------------------------------------------- + +class DesktopPlugin : public Plugin, + public Runner +{ + ChildProcess jackd; + ChildProcess mod_ui; + SharedMemory shm; + bool startingJackd = false; + bool startingModUI = false; + bool processing = false; + bool firstTimeActivating = true; + bool firstTimeProcessing = true; + float parameters[kParameterCount] = {}; + float* tmpBuffers[2] = {}; + uint32_t numFramesInShmBuffer = 0; + uint32_t numFramesInTmpBuffer = 0; + uint portBaseNum = 0; + + #ifdef DISTRHO_OS_WINDOWS + const WCHAR* envp; + #else + char* const* envp; + #endif + +public: + DesktopPlugin() + : Plugin(kParameterCount, 0, 0), + envp(nullptr) + { + if (isDummyInstance()) + return; + + // TODO check available ports + static int port = 1; + int availablePortNum = port; + port += 4; + + envp = getEvironment(availablePortNum); + + if (envp == nullptr) + { + parameters[kParameterBasePortNumber] = -kErrorAppDirNotFound; + return; + } + + portBaseNum = availablePortNum; + + bufferSizeChanged(getBufferSize()); + } + + ~DesktopPlugin() + { + stopRunner(); + + if (processing && jackd.isRunning()) + shm.stopWait(); + + jackd.stop(); + mod_ui.stop(); + + shm.deinit(); + + delete[] tmpBuffers[0]; + delete[] tmpBuffers[1]; + + if (envp != nullptr) + { + #ifndef DISTRHO_OS_WINDOWS + for (uint i = 0; envp[i] != nullptr; ++i) + std::free(envp[i]); + #endif + + delete[] envp; + } + } + +protected: + /* -------------------------------------------------------------------------------------------------------- + * Keep services running */ + + bool run() override + { + #ifdef DISTRHO_OS_WINDOWS + #define APP_EXT ".exe" + #else + #define APP_EXT "" + #endif + + if (! jackd.isRunning()) + { + if (startingJackd) + { + startingJackd = false; + parameters[kParameterBasePortNumber] = -kErrorJackdExecFailed; + return false; + } + + const String appDir(getAppDir()); + const String jackdStr(appDir + DISTRHO_OS_SEP_STR "jackd" APP_EXT); + const String jacksessionStr(appDir + DISTRHO_OS_SEP_STR "jack" DISTRHO_OS_SEP_STR "jack-session.conf"); + const String servernameStr("mod-desktop-" + String(portBaseNum)); + const String sampleRateStr(static_cast(getSampleRate())); + + const char* const jackd_args[] = { + jackdStr.buffer(), + "-R", + "-S", + "-n", servernameStr.buffer(), + "-C", jacksessionStr.buffer(), + "-d", "mod-desktop", + "-r", sampleRateStr.buffer(), + nullptr + }; + + startingJackd = true; + if (jackd.start(jackd_args, envp)) + return true; + + parameters[kParameterBasePortNumber] = -kErrorJackdExecFailed; + return false; + } + + startingJackd = false; + + if (! processing) + { + if (shm.sync()) + processing = true; + + return true; + } + + if (! mod_ui.isRunning()) + { + if (startingModUI) + { + startingModUI = false; + parameters[kParameterBasePortNumber] = -kErrorModUiExecFailed; + return false; + } + + const String appDir(getAppDir()); + const String moduiStr(appDir + DISTRHO_OS_SEP_STR "mod-ui" APP_EXT); + + const char* const mod_ui_args[] = { + moduiStr.buffer(), + nullptr + }; + + startingModUI = true; + if (mod_ui.start(mod_ui_args, envp)) + return true; + + parameters[kParameterBasePortNumber] = -kErrorModUiExecFailed; + return false; + } + + startingModUI = false; + + parameters[kParameterBasePortNumber] = portBaseNum; + return true; + } + + /* -------------------------------------------------------------------------------------------------------- + * Information */ + + /** + Get the plugin label. + A plugin label follows the same rules as Parameter::symbol, with the exception that it can start with numbers. + */ + const char* getLabel() const override + { + return "mod_desktop"; + } + + /** + Get an extensive comment/description about the plugin. + */ + const char* getDescription() const override + { + return ""; + } + + /** + Get the plugin author/maker. + */ + const char* getMaker() const override + { + return "MOD Audio"; + } + + /** + Get the plugin homepage. + */ + const char* getHomePage() const override + { + return "https://github.com/moddevices/mod-desktop"; + } + + /** + Get the plugin license name (a single line of text). + For commercial plugins this should return some short copyright information. + */ + const char* getLicense() const override + { + return "AGPL-3.0-or-later"; + } + + /** + Get the plugin version, in hexadecimal. + */ + uint32_t getVersion() const override + { + return d_version(1, 0, 0); + } + + /* -------------------------------------------------------------------------------------------------------- + * Init */ + + /** + Initialize the audio port @a index.@n + This function will be called once, shortly after the plugin is created. + */ + void initAudioPort(bool input, uint32_t index, AudioPort& port) override + { + // treat meter audio ports as stereo + port.groupId = kPortGroupStereo; + + // everything else is as default + Plugin::initAudioPort(input, index, port); + } + + /** + Initialize the parameter @a index.@n + This function will be called once, shortly after the plugin is created. + */ + void initParameter(uint32_t index, Parameter& parameter) override + { + switch (index) + { + case kParameterBasePortNumber: + parameter.hints = kParameterIsOutput | kParameterIsInteger; + parameter.name = "base port number"; + parameter.symbol = "base_port_num"; + parameter.ranges.min = -kErrorUndefined; + parameter.ranges.max = 512.f; + parameter.ranges.def = 0.f; + break; + } + } + + /** + Set a state key and default value. + This function will be called once, shortly after the plugin is created. + */ + void initState(uint32_t, String&, String&) override + { + // we are using states but don't want them saved in the host + } + + /* -------------------------------------------------------------------------------------------------------- + * Internal data */ + + /** + Get the current value of a parameter. + */ + float getParameterValue(const uint32_t index) const override + { + return parameters[index]; + } + + /** + Change a parameter value. + */ + void setParameterValue(const uint32_t index, const float value) override + { + // parameters[index] = value; + } + + /** + Change an internal state. + */ + void setState(const char*, const char*) override + { + } + + /* -------------------------------------------------------------------------------------------------------- + * Process */ + + void activate() override + { + if (firstTimeActivating) + { + firstTimeActivating = false; + + if (envp != nullptr && shm.init() && run()) + startRunner(500); + } + else + { + shm.reset(); + } + + numFramesInShmBuffer = numFramesInTmpBuffer = 0; + } + + void deactivate() override + { + } + + /** + Run/process function for plugins without MIDI input. + */ + void run(const float** const inputs, float** const outputs, const uint32_t frames, + const MidiEvent* midiEvents, uint32_t midiEventCount) override + { + if (! processing) + { + std::memset(outputs[0], 0, sizeof(float) * frames); + std::memset(outputs[1], 0, sizeof(float) * frames); + return; + } + + uint32_t ti = numFramesInShmBuffer; + uint32_t to = numFramesInTmpBuffer; + uint32_t framesDone = 0; + + for (uint32_t i = 0; i < frames; ++i) + { + shm.data->audio[ti] = inputs[0][i]; + shm.data->audio[128 + ti] = inputs[1][i]; + + if (++ti == 128) + { + ti = 0; + + if (midiEventCount != 0) + { + uint16_t mec = shm.data->midiEventCount; + + while (midiEventCount != 0 && mec != 511) + { + if (midiEvents->size > 4) + { + --midiEventCount; + ++midiEvents; + continue; + } + + if (midiEvents->frame >= framesDone + 128) + break; + + shm.data->midiFrames[mec] = midiEvents->frame - framesDone; + shm.data->midiData[mec * 4 + 0] = midiEvents->data[0]; + shm.data->midiData[mec * 4 + 1] = midiEvents->data[1]; + shm.data->midiData[mec * 4 + 2] = midiEvents->data[2]; + shm.data->midiData[mec * 4 + 3] = midiEvents->data[3]; + + --midiEventCount; + ++midiEvents; + ++mec; + } + + shm.data->midiEventCount = mec; + } + + if (! shm.process(tmpBuffers, to)) + { + d_stdout("shm processing failed"); + processing = false; + std::memset(outputs[0], 0, sizeof(float) * frames); + std::memset(outputs[1], 0, sizeof(float) * frames); + return; + } + + for (uint16_t j = 0; j < shm.data->midiEventCount; ++j) + { + MidiEvent midiEvent = { + framesDone + shm.data->midiFrames[j] - to, + 4, + { + shm.data->midiData[j * 4 + 0], + shm.data->midiData[j * 4 + 1], + shm.data->midiData[j * 4 + 2], + shm.data->midiData[j * 4 + 3], + }, + nullptr + }; + + if (! writeMidiEvent(midiEvent)) + break; + } + + to += 128; + + if (firstTimeProcessing) + { + firstTimeProcessing = false; + std::memset(outputs[0], 0, sizeof(float) * i); + std::memset(outputs[1], 0, sizeof(float) * i); + } + else + { + const uint32_t framesToCopy = std::min(128u, frames - framesDone); + std::memcpy(outputs[0] + framesDone, tmpBuffers[0], sizeof(float) * framesToCopy); + std::memcpy(outputs[1] + framesDone, tmpBuffers[1], sizeof(float) * framesToCopy); + + to -= framesToCopy; + framesDone += framesToCopy; + std::memmove(tmpBuffers[0], tmpBuffers[0] + framesToCopy, sizeof(float) * to); + std::memmove(tmpBuffers[1], tmpBuffers[1] + framesToCopy, sizeof(float) * to); + } + } + } + + if (firstTimeProcessing) + { + std::memset(outputs[0], 0, sizeof(float) * frames); + std::memset(outputs[1], 0, sizeof(float) * frames); + + if (midiEventCount != 0) + { + uint16_t mec = shm.data->midiEventCount; + + while (midiEventCount != 0 && mec != 511) + { + if (midiEvents->size > 4) + { + --midiEventCount; + ++midiEvents; + continue; + } + + shm.data->midiFrames[mec] = midiEvents->frame; + shm.data->midiData[mec * 4 + 0] = midiEvents->data[0]; + shm.data->midiData[mec * 4 + 1] = midiEvents->data[1]; + shm.data->midiData[mec * 4 + 2] = midiEvents->data[2]; + shm.data->midiData[mec * 4 + 3] = midiEvents->data[3]; + + --midiEventCount; + ++midiEvents; + ++mec; + } + + shm.data->midiEventCount = mec; + } + } + else if (framesDone != frames) + { + const uint32_t framesToCopy = frames - framesDone; + std::memcpy(outputs[0] + framesDone, tmpBuffers[0], sizeof(float) * framesToCopy); + std::memcpy(outputs[1] + framesDone, tmpBuffers[1], sizeof(float) * framesToCopy); + + to -= framesToCopy; + std::memmove(tmpBuffers[0], tmpBuffers[0] + framesToCopy, sizeof(float) * to); + std::memmove(tmpBuffers[1], tmpBuffers[1] + framesToCopy, sizeof(float) * to); + + if (midiEventCount != 0) + { + uint16_t mec = shm.data->midiEventCount; + + while (midiEventCount != 0 && mec != 511) + { + if (midiEvents->size > 4) + { + --midiEventCount; + ++midiEvents; + continue; + } + + shm.data->midiFrames[mec] = ti + framesDone - midiEvents->frame; + shm.data->midiData[mec * 4 + 0] = midiEvents->data[0]; + shm.data->midiData[mec * 4 + 1] = midiEvents->data[1]; + shm.data->midiData[mec * 4 + 2] = midiEvents->data[2]; + shm.data->midiData[mec * 4 + 3] = midiEvents->data[3]; + + --midiEventCount; + ++midiEvents; + ++mec; + } + + shm.data->midiEventCount = mec; + } + } + + numFramesInShmBuffer = ti; + numFramesInTmpBuffer = to; + } + + void bufferSizeChanged(const uint32_t bufferSize) override + { + delete[] tmpBuffers[0]; + delete[] tmpBuffers[1]; + tmpBuffers[0] = new float[bufferSize + 256]; + tmpBuffers[1] = new float[bufferSize + 256]; + std::memset(tmpBuffers[0], 0, sizeof(float) * (bufferSize + 256)); + std::memset(tmpBuffers[1], 0, sizeof(float) * (bufferSize + 256)); + } + + void sampleRateChanged(double) override + { + if (jackd.isRunning()) + { + stopRunner(); + jackd.stop(); + + if (run()) + startRunner(500); + } + } + + // ------------------------------------------------------------------------------------------------------- + + /** + Set our plugin class as non-copyable and add a leak detector just in case. + */ + DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(DesktopPlugin) +}; + +/* ------------------------------------------------------------------------------------------------------------ + * Plugin entry point, called by DPF to create a new plugin instance. */ + +Plugin* createPlugin() +{ + return new DesktopPlugin(); +} + +// ----------------------------------------------------------------------------------------------------------- + +END_NAMESPACE_DISTRHO diff --git a/src/plugin/DesktopUI.cpp b/src/plugin/DesktopUI.cpp new file mode 100644 index 0000000..3158eef --- /dev/null +++ b/src/plugin/DesktopUI.cpp @@ -0,0 +1,233 @@ +// SPDX-FileCopyrightText: 2023-2024 MOD Audio UG +// SPDX-License-Identifier: AGPL-3.0-or-later + +#include "DistrhoUI.hpp" +#include "DistrhoPluginUtils.hpp" +#include "NanoButton.hpp" +#include "WebView.hpp" + +#include "utils.hpp" + +START_NAMESPACE_DISTRHO + +// ----------------------------------------------------------------------------------------------------------- + +class DesktopUI : public UI, + public ButtonEventHandler::Callback +{ + Button buttonRefresh; + Button buttonOpenWebGui; + Button buttonOpenUserFilesDir; + String label; + String error; + String errorDetail; + uint port = 0; + void* webview = nullptr; + +public: + DesktopUI() + : UI(DISTRHO_UI_DEFAULT_WIDTH, DISTRHO_UI_DEFAULT_HEIGHT), + buttonRefresh(this, this), + buttonOpenWebGui(this, this), + buttonOpenUserFilesDir(this, this) + { + loadSharedResources(); + + const double scaleFactor = getScaleFactor(); + + buttonRefresh.setId(1); + buttonRefresh.setLabel("Refresh"); + buttonRefresh.setFontScale(scaleFactor); + buttonRefresh.setAbsolutePos(2 * scaleFactor, 2 * scaleFactor); + buttonRefresh.setSize(70 * scaleFactor, 26 * scaleFactor); + + buttonOpenWebGui.setId(2); + buttonOpenWebGui.setLabel("Open in Web Browser"); + buttonOpenWebGui.setFontScale(scaleFactor); + buttonOpenWebGui.setAbsolutePos(74 * scaleFactor, 2 * scaleFactor); + buttonOpenWebGui.setSize(150 * scaleFactor, 26 * scaleFactor); + + buttonOpenUserFilesDir.setId(3); + buttonOpenUserFilesDir.setLabel("Open User Files Dir"); + buttonOpenUserFilesDir.setFontScale(scaleFactor); + buttonOpenUserFilesDir.setAbsolutePos(226 * scaleFactor, 2 * scaleFactor); + buttonOpenUserFilesDir.setSize(140 * scaleFactor, 26 * scaleFactor); + + label = "MOD Desktop "; + label += getPluginFormatName(); + label += " v" VERSION; + + if (d_isNotEqual(scaleFactor, 1.0)) + { + setGeometryConstraints((DISTRHO_UI_DEFAULT_WIDTH - 100) * scaleFactor, + DISTRHO_UI_DEFAULT_HEIGHT * scaleFactor); + setSize(DISTRHO_UI_DEFAULT_WIDTH * scaleFactor, DISTRHO_UI_DEFAULT_HEIGHT * scaleFactor); + } + else + { + setGeometryConstraints(DISTRHO_UI_DEFAULT_WIDTH - 100, DISTRHO_UI_DEFAULT_HEIGHT); + } + } + + ~DesktopUI() override + { + if (webview != nullptr) + destroyWebView(webview); + } + +protected: + /* -------------------------------------------------------------------------------------------------------- + * DSP/Plugin Callbacks */ + + /** + A parameter has changed on the plugin side. + This is called by the host to inform the UI about parameter changes. + */ + void parameterChanged(const uint32_t index, const float value) override + { + if (index == kParameterBasePortNumber) + { + if (d_isZero(value)) + return; + + if (value < 0.f) + { + switch (-d_roundToIntNegative(value)) + { + case kErrorAppDirNotFound: + error = "Error: MOD Desktop application directory not found"; + errorDetail = "Make sure to install the standalone and run it at least once"; + break; + case kErrorJackdExecFailed: + error = "Error: Failed to start jackd"; + errorDetail = ""; + break; + case kErrorModUiExecFailed: + error = "Error: Failed to start mod-ui"; + errorDetail = ""; + break; + case kErrorUndefined: + error = "Error initializing MOD Desktop plugin"; + errorDetail = ""; + break; + } + repaint(); + return; + } + + if (error.isNotEmpty()) + { + error.clear(); + errorDetail.clear(); + repaint(); + } + + if (webview != nullptr) + { + destroyWebView(webview); + webview = nullptr; + } + + port = d_roundToUnsignedInt(value); + DISTRHO_SAFE_ASSERT_RETURN(port != 0,); + + port += kPortNumOffset; + webview = addWebView(getWindow().getNativeWindowHandle(), getScaleFactor(), port); + } + } + + /** + A state has changed on the plugin side. + This is called by the host to inform the UI about state changes. + */ + void stateChanged(const char*, const char*) override + { + // nothing here + } + + /* -------------------------------------------------------------------------------------------------------- + * Widget Callbacks */ + + /** + The drawing function. + */ + void onNanoDisplay() override + { + const double scaleFactor = getScaleFactor(); + + fillColor(255, 255, 255, 255); + fontSize(18 * scaleFactor); + textAlign(ALIGN_CENTER | ALIGN_MIDDLE); + text(getWidth() / 2, kVerticalOffset * scaleFactor / 2, label, nullptr); + + if (error.isNotEmpty()) + { + fontSize(36 * scaleFactor); + text(getWidth() / 2, getHeight() / 2 - 18 * scaleFactor, error, nullptr); + + if (errorDetail.isNotEmpty()) + { + fontSize(18 * scaleFactor); + text(getWidth() / 2, getHeight() / 2 + 18 * scaleFactor, errorDetail, nullptr); + } + } + } + + void buttonClicked(SubWidget* const widget, int) override + { + switch (widget->getId()) + { + case 1: + if (webview != nullptr) + reloadWebView(webview); + break; + case 2: + openWebGui(port); + break; + case 3: + openUserFilesDir(); + break; + } + } + + void onResize(const ResizeEvent& ev) override + { + UI::onResize(ev); + + if (webview == nullptr) + return; + + const double scaleFactor = getScaleFactor(); + + uint offset = kVerticalOffset * scaleFactor; + uint width = ev.size.getWidth(); + uint height = ev.size.getHeight() - offset; + + #ifdef DISTRHO_OS_MAC + offset /= scaleFactor; + width /= scaleFactor; + height /= scaleFactor; + #endif + + resizeWebView(webview, offset, width, height); + } + + // ------------------------------------------------------------------------------------------------------- + + /** + Set our UI class as non-copyable and add a leak detector just in case. + */ + DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(DesktopUI) +}; + +/* ------------------------------------------------------------------------------------------------------------ + * UI entry point, called by DPF to create a new UI instance. */ + +UI* createUI() +{ + return new DesktopUI(); +} + +// ----------------------------------------------------------------------------------------------------------- + +END_NAMESPACE_DISTRHO diff --git a/src/plugin/DistrhoPluginInfo.h b/src/plugin/DistrhoPluginInfo.h new file mode 100644 index 0000000..926c8be --- /dev/null +++ b/src/plugin/DistrhoPluginInfo.h @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2023-2024 MOD Audio UG +// SPDX-License-Identifier: AGPL-3.0-or-later + +#pragma once + +#define DISTRHO_PLUGIN_BRAND "MOD Audio" +#define DISTRHO_PLUGIN_NAME "MOD Desktop" +#define DISTRHO_PLUGIN_URI "https://mod.audio/desktop/" +#define DISTRHO_PLUGIN_CLAP_ID "audio.mod.desktop" + +#define DISTRHO_PLUGIN_BRAND_ID MODa +#define DISTRHO_PLUGIN_UNIQUE_ID dskt + +#define DISTRHO_PLUGIN_HAS_UI 1 +#define DISTRHO_PLUGIN_IS_RT_SAFE 0 +#define DISTRHO_PLUGIN_NUM_INPUTS 2 +#define DISTRHO_PLUGIN_NUM_OUTPUTS 2 +#define DISTRHO_PLUGIN_WANT_MIDI_INPUT 1 +#define DISTRHO_PLUGIN_WANT_MIDI_OUTPUT 1 +#define DISTRHO_PLUGIN_WANT_STATE 1 +#define DISTRHO_UI_FILE_BROWSER 0 +#define DISTRHO_UI_DEFAULT_WIDTH 1170 +#define DISTRHO_UI_DEFAULT_HEIGHT 600 +#define DISTRHO_UI_USE_NANOVG 1 +#define DISTRHO_UI_USER_RESIZABLE 1 + +static const constexpr unsigned int kVerticalOffset = 30; +static const constexpr unsigned int kPortNumOffset = 18190; + +enum Error { + kErrorAppDirNotFound = 1, + kErrorJackdExecFailed, + kErrorModUiExecFailed, + kErrorUndefined +}; + +enum Parameters { + kParameterBasePortNumber, + kParameterCount +}; diff --git a/src/plugin/Makefile b/src/plugin/Makefile new file mode 100644 index 0000000..3f7768c --- /dev/null +++ b/src/plugin/Makefile @@ -0,0 +1,54 @@ +#!/usr/bin/make -f + +export DISTRHO_NAMESPACE = DesktopDISTRHO +export DGL_NAMESPACE = DesktopDGL +export NVG_FONT_TEXTURE_FLAGS = NVG_IMAGE_NEAREST + +include ../DPF/Makefile.base.mk + +# --------------------------------------------------------------------------------------------------------------------- +# Project name, used for binaries + +NAME = MOD-Desktop + +# --------------------------------------------------------------------------------------------------------------------- +# Files to build + +FILES_DSP = DesktopPlugin.cpp utils.cpp +FILES_UI = DesktopUI.cpp NanoButton.cpp utils.cpp + +ifeq ($(MACOS),true) +FILES_UI += WebView.mm +else ifeq ($(WINDOWS),true) +FILES_UI += WebViewWin32.cpp +else +FILES_UI += WebViewX11.cpp +endif + +# --------------------------------------------------------------------------------------------------------------------- +# Do some magic + +DPF_BUILD_DIR = ../../build-plugin/build +DPF_TARGET_DIR = ../../build-plugin + +include ../DPF/Makefile.plugins.mk + +BUILD_CXX_FLAGS += -DVERSION='"$(shell cat ../../VERSION)"' + +BUILD_CXX_FLAGS += -pthread +LINK_FLAGS += -pthread + +ifeq ($(MACOS),true) +LINK_FLAGS += -framework IOKit -framework WebKit +else ifeq ($(WINDOWS),true) +LINK_FLAGS += -lwinmm +else ifeq ($(LINUX),true) +BUILD_CXX_FLAGS += $(shell $(PKG_CONFIG) --cflags Qt5Core) -std=gnu++14 +LINK_FLAGS += -ldl -lrt +endif + +TARGETS = clap lv2_sep vst2 vst3 + +# --------------------------------------------------------------------------------------------------------------------- + +all: $(TARGETS) diff --git a/src/plugin/NanoButton.cpp b/src/plugin/NanoButton.cpp new file mode 100644 index 0000000..406dd9a --- /dev/null +++ b/src/plugin/NanoButton.cpp @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2018-2019 Rob van den Berg + * Copyright (C) 2020-2021 Filipe Coelho + * + * This file is part of CharacterCompressor + * + * Nnjas2 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. + * + * CharacterCompressor is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with CharacterCompressor. If not, see . + */ + +#include "NanoButton.hpp" +#include "Window.hpp" + +START_NAMESPACE_DGL + +Button::Button(Widget* const parent, ButtonEventHandler::Callback* const cb) + : NanoWidget(parent), + ButtonEventHandler(this), + backgroundColor(32, 32, 32), + labelColor(255, 255, 255), + label("button"), + fontScale(1.0f) +{ +#ifdef DGL_NO_SHARED_RESOURCES + createFontFromFile("sans", "/usr/share/fonts/truetype/ttf-dejavu/DejaVuSans.ttf"); +#else + loadSharedResources(); +#endif + ButtonEventHandler::setCallback(cb); +} + +Button::~Button() +{ +} + +void Button::setBackgroundColor(const Color color) +{ + backgroundColor = color; +} + +void Button::setFontScale(const float scale) +{ + fontScale = scale; +} + +void Button::setLabel(const std::string& label2) +{ + label = label2; +} + +void Button::setLabelColor(const Color color) +{ + labelColor = color; +} + +void Button::onNanoDisplay() +{ + const uint w = getWidth(); + const uint h = getHeight(); + const float margin = 1.0f; + + // Background + beginPath(); + fillColor(backgroundColor); + strokeColor(labelColor); + rect(margin, margin, w - 2 * margin, h - 2 * margin); + fill(); + stroke(); + closePath(); + + // Label + beginPath(); + fontSize(14 * fontScale); + fillColor(labelColor); + Rectangle bounds; + textBounds(0, 0, label.c_str(), NULL, bounds); + float tx = w / 2.0f ; + float ty = h / 2.0f; + textAlign(ALIGN_CENTER | ALIGN_MIDDLE); + + fillColor(255, 255, 255, 255); + text(tx, ty, label.c_str(), NULL); + closePath(); +} + +bool Button::onMouse(const MouseEvent& ev) +{ + return ButtonEventHandler::mouseEvent(ev); +} + +bool Button::onMotion(const MotionEvent& ev) +{ + return ButtonEventHandler::motionEvent(ev); +} + +END_NAMESPACE_DGL diff --git a/src/plugin/NanoButton.hpp b/src/plugin/NanoButton.hpp new file mode 100644 index 0000000..20eac38 --- /dev/null +++ b/src/plugin/NanoButton.hpp @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2018-2019 Rob van den Berg + * Copyright (C) 2020-2021 Filipe Coelho + * + * This file is part of CharacterCompressor + * + * Nnjas2 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. + * + * CharacterCompressor is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with CharacterCompressor. If not, see . + */ + +#ifndef NANO_BUTTON_HPP_INCLUDED +#define NANO_BUTTON_HPP_INCLUDED + +#include "NanoVG.hpp" +#include "EventHandlers.hpp" + +#include + +START_NAMESPACE_DGL + +class Button : public NanoSubWidget, + public ButtonEventHandler +{ +public: + explicit Button(Widget* parent, ButtonEventHandler::Callback* cb); + ~Button() override; + + void setBackgroundColor(Color color); + void setFontScale(float scale); + void setLabel(const std::string& label); + void setLabelColor(Color color); + +protected: + void onNanoDisplay() override; + bool onMouse(const MouseEvent& ev) override; + bool onMotion(const MotionEvent& ev) override; + +private: + Color backgroundColor; + Color labelColor; + std::string label; + float fontScale; + + DISTRHO_LEAK_DETECTOR(Button) +}; + +END_NAMESPACE_DGL + +#endif // NANO_BUTTON_HPP_INCLUDED diff --git a/src/plugin/SharedMemory.hpp b/src/plugin/SharedMemory.hpp new file mode 100644 index 0000000..7601799 --- /dev/null +++ b/src/plugin/SharedMemory.hpp @@ -0,0 +1,322 @@ +// SPDX-FileCopyrightText: 2023-2024 MOD Audio UG +// SPDX-License-Identifier: AGPL-3.0-or-later + +#pragma once + +#include "DistrhoUtils.hpp" + +#ifdef DISTRHO_OS_WINDOWS +#else +# include +# include +# ifdef DISTRHO_OS_MAC +# include +# include +# include +# else +# include +# include +# include +# include +# endif +#endif + +START_NAMESPACE_DISTRHO + +// -------------------------------------------------------------------------------------------------------------------- + +class SharedMemory +{ +public: + struct Data { + uint32_t magic; + int32_t padding1; + #if defined(DISTRHO_OS_MAC) + char bootname1[32]; + char bootname2[32]; + #elif defined(DISTRHO_OS_WINDOWS) + HANDLE sem1; + HANDLE sem2; + #else + int32_t sem1; + int32_t sem2; + #endif + uint16_t midiEventCount; + uint16_t midiFrames[511]; + uint8_t midiData[511 * 4]; + uint8_t padding2[4]; + float audio[]; + }* data = nullptr; + + SharedMemory() + { + } + + ~SharedMemory() + { + deinit(); + } + + bool init() + { + void* ptr; + + #ifdef DISTRHO_OS_WINDOWS + SECURITY_ATTRIBUTES sa = {}; + sa.nLength = sizeof(sa); + sa.bInheritHandle = TRUE; + + shm = CreateFileMappingA(INVALID_HANDLE_VALUE, + &sa, + PAGE_READWRITE|SEC_COMMIT, + 0, + static_cast(kDataSize), + "/mod-desktop-test1"); + DISTRHO_SAFE_ASSERT_RETURN(shm != nullptr, false); + + ptr = MapViewOfFile(shm, FILE_MAP_ALL_ACCESS, 0, 0, kDataSize); + DISTRHO_SAFE_ASSERT_RETURN(ptr != nullptr, fail_deinit()); + + VirtualLock(ptr, kDataSize); + #else + // FIXME + shm_unlink("/mod-desktop-test1"); + + shmfd = shm_open("/mod-desktop-test1", O_CREAT|O_EXCL|O_RDWR, 0600); + DISTRHO_SAFE_ASSERT_RETURN(shmfd >= 0, false); + + DISTRHO_SAFE_ASSERT_RETURN(ftruncate(shmfd, static_cast(kDataSize)) == 0, fail_deinit()); + + #ifdef MAP_LOCKED + ptr = mmap(nullptr, kDataSize, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_LOCKED, shmfd, 0); + if (ptr == nullptr || ptr == MAP_FAILED) + #endif + { + ptr = mmap(nullptr, kDataSize, PROT_READ|PROT_WRITE, MAP_SHARED, shmfd, 0); + } + DISTRHO_SAFE_ASSERT_RETURN(ptr != nullptr, fail_deinit()); + DISTRHO_SAFE_ASSERT_RETURN(ptr != MAP_FAILED, fail_deinit()); + + #ifndef MAP_LOCKED + mlock(ptr, kDataSize); + #endif + #endif + + data = static_cast(ptr); + + std::memset(data, 0, kDataSize); + data->magic = 1337; + + #if defined(DISTRHO_OS_MAC) + task = mach_task_self(); + + mach_port_t bootport1, bootport2; + DISTRHO_SAFE_ASSERT_RETURN(task_get_bootstrap_port(task, &bootport1) == KERN_SUCCESS, fail_deinit()); + DISTRHO_SAFE_ASSERT_RETURN(task_get_bootstrap_port(task, &bootport2) == KERN_SUCCESS, fail_deinit()); + DISTRHO_SAFE_ASSERT_RETURN(semaphore_create(task, &sem1, SYNC_POLICY_FIFO, 0) == KERN_SUCCESS, fail_deinit()); + DISTRHO_SAFE_ASSERT_RETURN(semaphore_create(task, &sem2, SYNC_POLICY_FIFO, 0) == KERN_SUCCESS, fail_deinit()); + + static int bootcounter = 0; + std::snprintf(data->bootname1, 31, "mdskt_%d_%d_%p", ++bootcounter, getpid(), &sem1); + std::snprintf(data->bootname2, 31, "mdskt_%d_%d_%p", ++bootcounter, getpid(), &sem2); + data->bootname1[31] = data->bootname2[31] = '\0'; + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + DISTRHO_SAFE_ASSERT_RETURN(bootstrap_register(bootport1, data->bootname1, sem1) == KERN_SUCCESS, fail_deinit()); + DISTRHO_SAFE_ASSERT_RETURN(bootstrap_register(bootport2, data->bootname2, sem2) == KERN_SUCCESS, fail_deinit()); + #pragma clang diagnostic pop + #elif defined(DISTRHO_OS_WINDOWS) + data->sem1 = CreateSemaphoreA(&sa, 0, 1, nullptr); + DISTRHO_SAFE_ASSERT_RETURN(data->sem1 != nullptr, fail_deinit()); + + data->sem2 = CreateSemaphoreA(&sa, 0, 1, nullptr); + DISTRHO_SAFE_ASSERT_RETURN(data->sem2 != nullptr, fail_deinit()); + #endif + + return true; + } + + void deinit() + { + #if defined(DISTRHO_OS_MAC) + if (sem1 != MACH_PORT_NULL) + { + semaphore_destroy(task, sem1); + sem1 = MACH_PORT_NULL; + } + + if (sem2 != MACH_PORT_NULL) + { + semaphore_destroy(task, sem2); + sem2 = MACH_PORT_NULL; + } + #endif + + #ifdef DISTRHO_OS_WINDOWS + if (data != nullptr) + { + if (data->sem1 != nullptr) + { + CloseHandle(data->sem1); + data->sem1 = nullptr; + } + + if (data->sem2 != nullptr) + { + CloseHandle(data->sem2); + data->sem2 = nullptr; + } + + UnmapViewOfFile(data); + data = nullptr; + } + + if (shm != nullptr) + { + CloseHandle(shm); + shm = nullptr; + } + #else + if (data != nullptr) + { + munmap(data, kDataSize); + data = nullptr; + } + + if (shmfd >= 0) + { + close(shmfd); + shm_unlink("/mod-desktop-test1"); + shmfd = -1; + } + #endif + } + + // ---------------------------------------------------------------------------------------------------------------- + + void reset() + { + if (data == nullptr) + return; + + data->midiEventCount = 0; + std::memset(data->audio, 0, sizeof(float) * 128 * 2); + } + + bool sync() + { + if (data == nullptr) + return false; + + reset(); + post(); + return wait(); + } + + void stopWait() + { + if (data == nullptr) + return; + + data->magic = 7331; + post(); + wait(); + } + + bool process(float** output, const uint32_t offset) + { + // unlock RT waiter + post(); + + // wait for processing + if (! wait()) + return false; + + // copy processed buffer + std::memcpy(output[0] + offset, data->audio, sizeof(float) * 128); + std::memcpy(output[1] + offset, data->audio + 128, sizeof(float) * 128); + + return true; + } + + // ---------------------------------------------------------------------------------------------------------------- + +private: + static constexpr const size_t kDataSize = sizeof(Data) + sizeof(float) * 128 * 2; + + // ---------------------------------------------------------------------------------------------------------------- + // shared memory details + + #ifdef DISTRHO_OS_WINDOWS + HANDLE shm; + #else + int shmfd = -1; + #endif + + #ifdef DISTRHO_OS_MAC + mach_port_t task = MACH_PORT_NULL; + semaphore_t sem1 = MACH_PORT_NULL; + semaphore_t sem2 = MACH_PORT_NULL; + #endif + + // ---------------------------------------------------------------------------------------------------------------- + // semaphore details + + #if defined(DISTRHO_OS_MAC) + void post() + { + semaphore_signal(sem1); + } + + bool wait() + { + const mach_timespec timeout = { 1, 0 }; + return semaphore_timedwait(sem2, timeout) == KERN_SUCCESS; + } + #elif defined(DISTRHO_OS_WINDOWS) + void post() + { + ReleaseSemaphore(data->sem1, 1, nullptr); + } + + bool wait() + { + return WaitForSingleObject(data->sem2, 1000) == WAIT_OBJECT_0; + } + #else + void post() + { + const bool unlocked = __sync_bool_compare_and_swap(&data->sem1, 0, 1); + DISTRHO_SAFE_ASSERT_RETURN(unlocked,); + syscall(__NR_futex, &data->sem1, FUTEX_WAKE, 1, nullptr, nullptr, 0); + } + + bool wait() + { + const timespec timeout = { 1, 0 }; + + for (;;) + { + if (__sync_bool_compare_and_swap(&data->sem2, 1, 0)) + return true; + + if (syscall(__NR_futex, &data->sem2, FUTEX_WAIT, 0, &timeout, nullptr, 0) != 0) + if (errno != EAGAIN && errno != EINTR) + return false; + } + } + #endif + + bool fail_deinit() + { + deinit(); + return false; + } + + DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SharedMemory) +}; + +// -------------------------------------------------------------------------------------------------------------------- + +END_NAMESPACE_DISTRHO diff --git a/src/plugin/Time.hpp b/src/plugin/Time.hpp new file mode 100644 index 0000000..91a1987 --- /dev/null +++ b/src/plugin/Time.hpp @@ -0,0 +1,127 @@ +/* + * DISTRHO Plugin Framework (DPF) + * Copyright (C) 2012-2024 Filipe Coelho + * + * Permission to use, copy, modify, and/or distribute this software for any purpose with + * or without fee is hereby granted, provided that the above copyright notice and this + * permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD + * TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN + * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL + * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef DISTRHO_TIME_HPP_INCLUDED +#define DISTRHO_TIME_HPP_INCLUDED + +#include "DistrhoUtils.hpp" + +#ifdef DISTRHO_OS_WINDOWS +# include +# include +# include +#else +# include +#endif + +START_NAMESPACE_DISTRHO + +// ----------------------------------------------------------------------------------------------------------- +// d_gettime_* + +/* + * Get a monotonically-increasing time in milliseconds. + */ +static inline +uint32_t d_gettime_ms() noexcept +{ + #if defined(DISTRHO_OS_MAC) + static const time_t s = clock_gettime_nsec_np(CLOCK_UPTIME_RAW) / 1000000; + return (clock_gettime_nsec_np(CLOCK_UPTIME_RAW) / 1000000) - s; + #elif defined(DISTRHO_OS_WINDOWS) + return static_cast(timeGetTime()); + #else + static struct { + timespec ts; + int r; + uint32_t ms; + } s = { {}, clock_gettime(CLOCK_MONOTONIC, &s.ts), static_cast(s.ts.tv_sec * 1000 + + s.ts.tv_nsec / 1000000) }; + timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (ts.tv_sec * 1000 + ts.tv_nsec / 1000000) - s.ms; + #endif +} + +/* + * Get a monotonically-increasing time in microseconds. + */ +static inline +uint64_t d_gettime_us() noexcept +{ + #if defined(DISTRHO_OS_MAC) + static const uint64_t s = clock_gettime_nsec_np(CLOCK_UPTIME_RAW) / 1000; + return (clock_gettime_nsec_np(CLOCK_UPTIME_RAW) / 1000) - s; + #elif defined(DISTRHO_OS_WINDOWS) + static struct { + LARGE_INTEGER freq; + LARGE_INTEGER counter; + BOOL r1, r2; + } s = { {}, {}, QueryPerformanceFrequency(&s.freq), QueryPerformanceCounter(&s.counter) }; + + LARGE_INTEGER counter; + QueryPerformanceCounter(&counter); + return (counter.QuadPart - s.counter.QuadPart) * 1000000 / s.freq.QuadPart; + #else + static struct { + timespec ts; + int r; + uint64_t us; + } s = { {}, clock_gettime(CLOCK_MONOTONIC, &s.ts), static_cast(s.ts.tv_sec * 1000000 + + s.ts.tv_nsec / 1000) }; + timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (ts.tv_sec * 1000000 + ts.tv_nsec / 1000) - s.us; + #endif +} + +/* + * Get a monotonically-increasing time in nanoseconds. + */ +static inline +uint64_t d_gettime_ns() noexcept +{ + #if defined(DISTRHO_OS_MAC) + static const uint64_t s = clock_gettime_nsec_np(CLOCK_UPTIME_RAW); + return clock_gettime_nsec_np(CLOCK_UPTIME_RAW) - s; + #elif defined(DISTRHO_OS_WINDOWS) + static struct { + LARGE_INTEGER freq; + LARGE_INTEGER counter; + BOOL r1, r2; + } s = { {}, {}, QueryPerformanceFrequency(&s.freq), QueryPerformanceCounter(&s.counter) }; + + LARGE_INTEGER counter; + QueryPerformanceCounter(&counter); + return (counter.QuadPart - s.counter.QuadPart) * 1000000000ULL / s.freq.QuadPart; + #else + static struct { + timespec ts; + int r; + uint64_t ns; + } s = { {}, clock_gettime(CLOCK_MONOTONIC, &s.ts), static_cast(s.ts.tv_sec * 1000000000ULL + + s.ts.tv_nsec) }; + timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (ts.tv_sec * 1000000000ULL + ts.tv_nsec) - s.ns; + #endif +} + +// ----------------------------------------------------------------------------------------------------------- + +END_NAMESPACE_DISTRHO + +#endif // DISTRHO_TIME_HPP_INCLUDED diff --git a/src/plugin/WebView.hpp b/src/plugin/WebView.hpp new file mode 100644 index 0000000..63d2da8 --- /dev/null +++ b/src/plugin/WebView.hpp @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2023-2024 MOD Audio UG +// SPDX-License-Identifier: AGPL-3.0-or-later + +#include "DistrhoUtils.hpp" + +START_NAMESPACE_DISTRHO + +// ----------------------------------------------------------------------------------------------------------- + +void* addWebView(uintptr_t parentWinId, double scaleFactor, uint port); +void destroyWebView(void* webview); +void reloadWebView(void* webview); +void resizeWebView(void* webview, uint offset, uint width, uint height); + +// ----------------------------------------------------------------------------------------------------------- + +END_NAMESPACE_DISTRHO diff --git a/src/plugin/WebView.mm b/src/plugin/WebView.mm new file mode 100644 index 0000000..da71c69 --- /dev/null +++ b/src/plugin/WebView.mm @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2023-2024 MOD Audio UG +// SPDX-License-Identifier: AGPL-3.0-or-later + +#include "WebView.hpp" + +#include "DistrhoPluginInfo.h" + +#import +#import + +START_NAMESPACE_DISTRHO + +// ----------------------------------------------------------------------------------------------------------- + +struct WebViewImpl { + NSView* const view; + WKWebView* const webview; + NSURLRequest* const urlreq; +}; + +// ----------------------------------------------------------------------------------------------------------- + +void* addWebView(const uintptr_t parentWinId, double, const uint port) +{ + NSView* const view = reinterpret_cast(parentWinId); + + const CGRect rect = CGRectMake(0, + kVerticalOffset, + DISTRHO_UI_DEFAULT_WIDTH, + DISTRHO_UI_DEFAULT_HEIGHT - kVerticalOffset); + + WKWebView* const webview = [[WKWebView alloc] initWithFrame: rect]; + [[[webview configuration] preferences] setValue: @(true) forKey: @"developerExtrasEnabled"]; + [view addSubview:webview]; + + char url[32]; + std::snprintf(url, 31, "http://127.0.0.1:%u/", port); + NSString* const nsurl = [[NSString alloc] + initWithBytes:url + length:std::strlen(url) + encoding:NSUTF8StringEncoding]; + NSURLRequest* const urlreq = [[NSURLRequest alloc] initWithURL: [NSURL URLWithString: nsurl]]; + [nsurl release]; + + [webview loadRequest: urlreq]; + [webview setHidden:NO]; + + return new WebViewImpl{view, webview, urlreq}; +} + +void destroyWebView(void* const webview) +{ + WebViewImpl* const impl = static_cast(webview); + + [impl->webview setHidden:YES]; + [impl->webview removeFromSuperview]; + [impl->urlreq release]; + + delete impl; +} + +void reloadWebView(void* const webview) +{ + WebViewImpl* const impl = static_cast(webview); + + [impl->webview loadRequest: impl->urlreq]; +} + +void resizeWebView(void* const webview, const uint offset, const uint width, const uint height) +{ + WebViewImpl* const impl = static_cast(webview); + + [impl->webview setFrame:CGRectMake(0, offset, width, height)]; +} + +// ----------------------------------------------------------------------------------------------------------- + +END_NAMESPACE_DISTRHO diff --git a/src/plugin/WebViewQt.cpp b/src/plugin/WebViewQt.cpp new file mode 100644 index 0000000..e69de29 diff --git a/src/plugin/WebViewWin32.cpp b/src/plugin/WebViewWin32.cpp new file mode 100644 index 0000000..c0f74e0 --- /dev/null +++ b/src/plugin/WebViewWin32.cpp @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2023-2024 MOD Audio UG +// SPDX-License-Identifier: AGPL-3.0-or-later + +#include "WebView.hpp" + +START_NAMESPACE_DISTRHO + +// ----------------------------------------------------------------------------------------------------------- + +void* addWebView(const uintptr_t parentWinId, const double scaleFactor, const uint port) +{ + return nullptr; +} + +void destroyWebView(void* const webviewptr) +{ +} + +void reloadWebView(void* const webviewptr) +{ +} + +void resizeWebView(void* const webviewptr, const uint offset, const uint width, const uint height) +{ +} + +// ----------------------------------------------------------------------------------------------------------- + +END_NAMESPACE_DISTRHO diff --git a/src/plugin/WebViewX11.cpp b/src/plugin/WebViewX11.cpp new file mode 100644 index 0000000..e429504 --- /dev/null +++ b/src/plugin/WebViewX11.cpp @@ -0,0 +1,517 @@ +// SPDX-FileCopyrightText: 2023-2024 MOD Audio UG +// SPDX-License-Identifier: AGPL-3.0-or-later + +#include +#include +#include +#undef signals + +#include "WebView.hpp" + +#include "ChildProcess.hpp" +#include "utils.hpp" + +#include "extra/String.hpp" +#include "DistrhoPluginUtils.hpp" + +#include +#include +#include +#include +#include + +START_NAMESPACE_DISTRHO + +// ----------------------------------------------------------------------------------------------------------- + +struct WebViewIPC { + ChildProcess p; + ::Display* display; + ::Window childWindow; + ::Window ourWindow; +}; + +// ----------------------------------------------------------------------------------------------------------- + +void* addWebView(const uintptr_t parentWinId, const double scaleFactor, const uint port) +{ + char ldlinux[PATH_MAX] = {}; + { + Dl_info info = {}; + dladdr(dlsym(nullptr, "_rtld_global"), &info); + + if (info.dli_fname[0] == '.') + { + getcwd(ldlinux, PATH_MAX - 1); + std::strncat(ldlinux, info.dli_fname + 1, PATH_MAX - 1); + } + else if (info.dli_fname[0] != '/') + { + getcwd(ldlinux, PATH_MAX - 1); + std::strncat(ldlinux, "/", PATH_MAX - 1); + std::strncat(ldlinux, info.dli_fname, PATH_MAX - 1); + } + else + { + std::strncpy(ldlinux, info.dli_fname, PATH_MAX - 1); + } + } + d_stdout("ld-linux is '%s'", ldlinux); + + ::Display* const display = XOpenDisplay(nullptr); + DISTRHO_SAFE_ASSERT_RETURN(display != nullptr, nullptr); + + // set up custom child environment + uint envsize = 0; + while (environ[envsize] != nullptr) + ++envsize; + + char** const envp = new char*[envsize + 32]; + + for (uint i = 0; i < envsize; ++i) + envp[i] = strdup(environ[i]); + + for (uint i = 0; i < 32; ++i) + envp[envsize + i] = nullptr; + + set_envp_value(envp, "LANG=en_US.UTF-8"); + set_envp_value(envp, "DPF_WEBVIEW_PORT", String(port)); + set_envp_value(envp, "DPF_WEBVIEW_SCALE_FACTOR", String(scaleFactor)); + set_envp_value(envp, "DPF_WEBVIEW_WIN_ID", String(parentWinId)); + + WebViewIPC* const ipc = new WebViewIPC(); + ipc->display = display; + ipc->childWindow = 0; + ipc->ourWindow = parentWinId; + + const char* const args[] = { ldlinux, getBinaryFilename(), nullptr }; + ipc->p.start(args, envp); + + for (uint i = 0; envp[i] != nullptr; ++i) + std::free(envp[i]); + delete[] envp; + + return ipc; +} + +void destroyWebView(void* const webviewptr) +{ + WebViewIPC* const ipc = static_cast(webviewptr); + + XCloseDisplay(ipc->display); + delete ipc; +} + +void reloadWebView(void* const webviewptr) +{ + WebViewIPC* const ipc = static_cast(webviewptr); + + ipc->p.signal(SIGUSR1); +} + +void resizeWebView(void* const webviewptr, const uint offset, const uint width, const uint height) +{ + WebViewIPC* const ipc = static_cast(webviewptr); + + if (ipc->childWindow == 0) + { + ::Window rootWindow, parentWindow; + ::Window* childWindows = nullptr; + uint numChildren = 0; + + XFlush(ipc->display); + XQueryTree(ipc->display, ipc->ourWindow, &rootWindow, &parentWindow, &childWindows, &numChildren); + + if (numChildren == 0 || childWindows == nullptr) + return; + + ipc->childWindow = childWindows[0]; + XFree(childWindows); + } + + XMoveResizeWindow(ipc->display, ipc->childWindow, 0, offset, width, height); + XFlush(ipc->display); +} + +// ----------------------------------------------------------------------------------------------------------- + +struct GtkContainer; +struct GtkPlug; +struct GtkWidget; +struct GtkWindow; +struct WebKitSettings; +struct WebKitWebView; + +#define GTK_CONTAINER(p) reinterpret_cast(p) +#define GTK_PLUG(p) reinterpret_cast(p) +#define GTK_WINDOW(p) reinterpret_cast(p) +#define WEBKIT_WEB_VIEW(p) reinterpret_cast(p) + +struct QApplication; +struct QUrl; +struct QWebEngineView; +struct QWindow; + +// ----------------------------------------------------------------------------------------------------------- + +#define JOIN(A, B) A ## B + +#define AUTOSYM(S) \ + using JOIN(gtk3_, S) = decltype(&S); \ + JOIN(gtk3_, S) S = reinterpret_cast(dlsym(nullptr, #S)); \ + DISTRHO_SAFE_ASSERT_RETURN(S != nullptr, false); + +#define CSYM(S, NAME) \ + S NAME = reinterpret_cast(dlsym(nullptr, #NAME)); \ + DISTRHO_SAFE_ASSERT_RETURN(NAME != nullptr, false); + +#define CPPSYM(S, NAME, SN) \ + S NAME = reinterpret_cast(dlsym(nullptr, #SN)); \ + DISTRHO_SAFE_ASSERT_RETURN(NAME != nullptr, false); + +// ----------------------------------------------------------------------------------------------------------- +// gtk3 variant + +static bool gtk3(Display* const display, const Window winId, double scaleFactor, const char* const url) +{ + void* lib; + if ((lib = dlopen("libwebkit2gtk-4.0.so.37", RTLD_NOW|RTLD_GLOBAL)) == nullptr || + (lib = dlopen("libwebkit2gtk-4.0.so", RTLD_NOW|RTLD_GLOBAL)) == nullptr) + return false; + + using gdk_set_allowed_backends_t = void (*)(const char*); + using gtk_container_add_t = void (*)(GtkContainer*, GtkWidget*); + using gtk_init_check_t = bool (*)(int*, char***); + using gtk_main_t = void (*)(); + using gtk_plug_get_id_t = Window (*)(GtkPlug*); + using gtk_plug_new_t = GtkWidget* (*)(Window); + using gtk_widget_show_all_t = void (*)(GtkWidget*); + using gtk_window_move_t = void (*)(GtkWindow*, int, int); + using gtk_window_set_default_size_t = void (*)(GtkWindow*, int, int); + using webkit_settings_new_t = WebKitSettings* (*)(); + using webkit_settings_set_hardware_acceleration_policy_t = void (*)(WebKitSettings*, int); + using webkit_settings_set_javascript_can_access_clipboard_t = void (*)(WebKitSettings*, bool); + using webkit_web_view_load_uri_t = void (*)(WebKitWebView*, const char*); + using webkit_web_view_new_with_settings_t = GtkWidget* (*)(WebKitSettings*); + + CSYM(gdk_set_allowed_backends_t, gdk_set_allowed_backends) + CSYM(gtk_container_add_t, gtk_container_add) + CSYM(gtk_init_check_t, gtk_init_check) + CSYM(gtk_main_t, gtk_main) + CSYM(gtk_plug_get_id_t, gtk_plug_get_id) + CSYM(gtk_plug_new_t, gtk_plug_new) + CSYM(gtk_widget_show_all_t, gtk_widget_show_all) + CSYM(gtk_window_move_t, gtk_window_move) + CSYM(gtk_window_set_default_size_t, gtk_window_set_default_size) + CSYM(webkit_settings_new_t, webkit_settings_new) + CSYM(webkit_settings_set_hardware_acceleration_policy_t, webkit_settings_set_hardware_acceleration_policy) + CSYM(webkit_settings_set_javascript_can_access_clipboard_t, webkit_settings_set_javascript_can_access_clipboard) + CSYM(webkit_web_view_load_uri_t, webkit_web_view_load_uri) + CSYM(webkit_web_view_new_with_settings_t, webkit_web_view_new_with_settings) + + const int gdkScale = std::fmod(scaleFactor, 1.0) >= 0.75 + ? static_cast(scaleFactor + 0.5) + : static_cast(scaleFactor); + + if (gdkScale != 1) + { + char scale[8] = {}; + std::snprintf(scale, 7, "%d", gdkScale); + setenv("GDK_SCALE", scale, 1); + + std::snprintf(scale, 7, "%.2f", (1.0 / scaleFactor) * 1.2); + setenv("GDK_DPI_SCALE", scale, 1); + } + else if (scaleFactor > 1.0) + { + char scale[8] = {}; + std::snprintf(scale, 7, "%.2f", (1.0 / scaleFactor) * 1.4); + setenv("GDK_DPI_SCALE", scale, 1); + } + + scaleFactor /= gdkScale; + + gdk_set_allowed_backends("x11"); + + if (! gtk_init_check (nullptr, nullptr)) + return false; + + GtkWidget* const window = gtk_plug_new(winId); + DISTRHO_SAFE_ASSERT_RETURN(window != nullptr, false); + + gtk_window_set_default_size(GTK_WINDOW(window), + DISTRHO_UI_DEFAULT_WIDTH * scaleFactor, + (DISTRHO_UI_DEFAULT_HEIGHT - kVerticalOffset) * scaleFactor); + gtk_window_move(GTK_WINDOW(window), 0, kVerticalOffset * scaleFactor); + + WebKitSettings* const settings = webkit_settings_new(); + DISTRHO_SAFE_ASSERT_RETURN(settings != nullptr, false); + + webkit_settings_set_javascript_can_access_clipboard(settings, true); + webkit_settings_set_hardware_acceleration_policy(settings, 2 /* WEBKIT_HARDWARE_ACCELERATION_POLICY_NEVER */); + + GtkWidget* const webview = webkit_web_view_new_with_settings(settings); + DISTRHO_SAFE_ASSERT_RETURN(webview != nullptr, false); + + webkit_web_view_load_uri (WEBKIT_WEB_VIEW (webview), url); + + gtk_container_add(GTK_CONTAINER(window), webview); + + gtk_widget_show_all(window); + + Window wid = gtk_plug_get_id(GTK_PLUG(window)); + XMapWindow(display, wid); + XFlush(display); + + gtk_main(); + + dlclose(lib); + return true; +} + +// ----------------------------------------------------------------------------------------------------------- +// qt5webengine variant + +static bool qt5webengine(const Window winId, const double scaleFactor, const char* const url) +{ + void* lib; + if ((lib = dlopen("libQt5WebEngineWidgets.so.5", RTLD_NOW|RTLD_GLOBAL)) == nullptr || + (lib = dlopen("libQt5WebEngineWidgets.so", RTLD_NOW|RTLD_GLOBAL)) == nullptr) + return false; + + using QApplication__init_t = void (*)(QApplication*, int&, char**, int); + using QApplication_exec_t = void (*)(); + using QApplication_setAttribute_t = void (*)(Qt::ApplicationAttribute, bool); + using QString__init_t = void (*)(void*, const QChar*, qsizetype); + using QUrl__init_t = void (*)(void*, const QString&, int /* QUrl::ParsingMode */); + using QWebEngineView__init_t = void (*)(QWebEngineView*, void*); + using QWebEngineView_move_t = void (*)(QWebEngineView*, const QPoint&); + using QWebEngineView_resize_t = void (*)(QWebEngineView*, const QSize&); + using QWebEngineView_setUrl_t = void (*)(QWebEngineView*, const QUrl&); + using QWebEngineView_show_t = void (*)(QWebEngineView*); + using QWebEngineView_winId_t = ulonglong (*)(QWebEngineView*); + using QWebEngineView_windowHandle_t = QWindow* (*)(QWebEngineView*); + using QWindow_fromWinId_t = QWindow* (*)(ulonglong); + using QWindow_setParent_t = void (*)(QWindow*, void*); + + CPPSYM(QApplication__init_t, QApplication__init, _ZN12QApplicationC1ERiPPci) + CPPSYM(QApplication_exec_t, QApplication_exec, _ZN15QGuiApplication4execEv) + CPPSYM(QApplication_setAttribute_t, QApplication_setAttribute, _ZN16QCoreApplication12setAttributeEN2Qt20ApplicationAttributeEb) + CPPSYM(QString__init_t, QString__init, _ZN7QStringC2EPK5QChari) + CPPSYM(QUrl__init_t, QUrl__init, _ZN4QUrlC1ERK7QStringNS_11ParsingModeE) + CPPSYM(QWebEngineView__init_t, QWebEngineView__init, _ZN14QWebEngineViewC1EP7QWidget) + CPPSYM(QWebEngineView_move_t, QWebEngineView_move, _ZN7QWidget4moveERK6QPoint) + CPPSYM(QWebEngineView_resize_t, QWebEngineView_resize, _ZN7QWidget6resizeERK5QSize) + CPPSYM(QWebEngineView_setUrl_t, QWebEngineView_setUrl, _ZN14QWebEngineView6setUrlERK4QUrl) + CPPSYM(QWebEngineView_show_t, QWebEngineView_show, _ZN7QWidget4showEv) + CPPSYM(QWebEngineView_winId_t, QWebEngineView_winId, _ZNK7QWidget5winIdEv) + CPPSYM(QWebEngineView_windowHandle_t, QWebEngineView_windowHandle, _ZNK7QWidget12windowHandleEv) + CPPSYM(QWindow_fromWinId_t, QWindow_fromWinId, _ZN7QWindow9fromWinIdEy) + CPPSYM(QWindow_setParent_t, QWindow_setParent, _ZN7QWindow9setParentEPS_) + + unsetenv("QT_FONT_DPI"); + unsetenv("QT_SCREEN_SCALE_FACTORS"); + unsetenv("QT_USE_PHYSICAL_DPI"); + setenv("QT_AUTO_SCREEN_SCALE_FACTOR", "0", 1); + + char scale[8] = {}; + std::snprintf(scale, 7, "%.2f", scaleFactor); + setenv("QT_SCALE_FACTOR", scale, 1); + + QApplication_setAttribute(Qt::AA_X11InitThreads, true); + QApplication_setAttribute(Qt::AA_EnableHighDpiScaling, true); + QApplication_setAttribute(Qt::AA_UseHighDpiPixmaps, true); + + static int argc = 0; + static char* argv[] = { nullptr }; + + uint8_t _app[64]; // sizeof(QApplication) == 16 + QApplication* const app = reinterpret_cast(_app); + QApplication__init(app, argc, argv, 0); + + uint8_t _qstrurl[32]; // sizeof(QString) == 8 + QString* const qstrurl(reinterpret_cast(_qstrurl)); + + { + const size_t url_len = std::strlen(url); + QChar* const url_qchar = new QChar[url_len + 1]; + + for (size_t i = 0; i < url_len; ++i) + url_qchar[i] = QChar(url[i]); + + url_qchar[url_len] = 0; + + QString__init(qstrurl, url_qchar, url_len); + } + + uint8_t _qurl[32]; // sizeof(QUrl) == 8 + QUrl* const qurl(reinterpret_cast(_qurl)); + QUrl__init(qurl, *qstrurl, 1 /* QUrl::StrictMode */); + + uint8_t _webview[128]; // sizeof(QWebEngineView) == 56 + QWebEngineView* const webview = reinterpret_cast(_webview); + QWebEngineView__init(webview, nullptr); + + QWebEngineView_move(webview, QPoint(0, kVerticalOffset)); + QWebEngineView_resize(webview, QSize(DISTRHO_UI_DEFAULT_WIDTH, DISTRHO_UI_DEFAULT_HEIGHT - kVerticalOffset)); + QWebEngineView_winId(webview); + QWindow_setParent(QWebEngineView_windowHandle(webview), QWindow_fromWinId(winId)); + QWebEngineView_setUrl(webview, *qurl); + QWebEngineView_show(webview); + + QApplication_exec(); + + dlclose(lib); + return true; +} + +// ----------------------------------------------------------------------------------------------------------- +// qt6webengine variant (same as qt5 but `QString__init_t` has different arguments) + +static bool qt6webengine(const Window winId, const double scaleFactor, const char* const url) +{ + void* lib; + if ((lib = dlopen("libQt6WebEngineWidgets.so.6", RTLD_NOW|RTLD_GLOBAL)) == nullptr || + (lib = dlopen("libQt6WebEngineWidgets.so", RTLD_NOW|RTLD_GLOBAL)) == nullptr) + return false; + + using QApplication__init_t = void (*)(QApplication*, int&, char**, int); + using QApplication_exec_t = void (*)(); + using QApplication_setAttribute_t = void (*)(Qt::ApplicationAttribute, bool); + using QString__init_t = void (*)(void*, const QChar*, long long); + using QUrl__init_t = void (*)(void*, const QString&, int /* QUrl::ParsingMode */); + using QWebEngineView__init_t = void (*)(QWebEngineView*, void*); + using QWebEngineView_move_t = void (*)(QWebEngineView*, const QPoint&); + using QWebEngineView_resize_t = void (*)(QWebEngineView*, const QSize&); + using QWebEngineView_setUrl_t = void (*)(QWebEngineView*, const QUrl&); + using QWebEngineView_show_t = void (*)(QWebEngineView*); + using QWebEngineView_winId_t = ulonglong (*)(QWebEngineView*); + using QWebEngineView_windowHandle_t = QWindow* (*)(QWebEngineView*); + using QWindow_fromWinId_t = QWindow* (*)(ulonglong); + using QWindow_setParent_t = void (*)(QWindow*, void*); + + CPPSYM(QApplication__init_t, QApplication__init, _ZN12QApplicationC1ERiPPci) + CPPSYM(QApplication_exec_t, QApplication_exec, _ZN15QGuiApplication4execEv) + CPPSYM(QApplication_setAttribute_t, QApplication_setAttribute, _ZN16QCoreApplication12setAttributeEN2Qt20ApplicationAttributeEb) + CPPSYM(QString__init_t, QString__init, _ZN7QStringC2EPK5QCharx) + CPPSYM(QUrl__init_t, QUrl__init, _ZN4QUrlC1ERK7QStringNS_11ParsingModeE) + CPPSYM(QWebEngineView__init_t, QWebEngineView__init, _ZN14QWebEngineViewC1EP7QWidget) + CPPSYM(QWebEngineView_move_t, QWebEngineView_move, _ZN7QWidget4moveERK6QPoint) + CPPSYM(QWebEngineView_resize_t, QWebEngineView_resize, _ZN7QWidget6resizeERK5QSize) + CPPSYM(QWebEngineView_setUrl_t, QWebEngineView_setUrl, _ZN14QWebEngineView6setUrlERK4QUrl) + CPPSYM(QWebEngineView_show_t, QWebEngineView_show, _ZN7QWidget4showEv) + CPPSYM(QWebEngineView_winId_t, QWebEngineView_winId, _ZNK7QWidget5winIdEv) + CPPSYM(QWebEngineView_windowHandle_t, QWebEngineView_windowHandle, _ZNK7QWidget12windowHandleEv) + CPPSYM(QWindow_fromWinId_t, QWindow_fromWinId, _ZN7QWindow9fromWinIdEy) + CPPSYM(QWindow_setParent_t, QWindow_setParent, _ZN7QWindow9setParentEPS_) + + unsetenv("QT_FONT_DPI"); + unsetenv("QT_SCREEN_SCALE_FACTORS"); + unsetenv("QT_USE_PHYSICAL_DPI"); + setenv("QT_AUTO_SCREEN_SCALE_FACTOR", "0", 1); + + char scale[8] = {}; + std::snprintf(scale, 7, "%.2f", scaleFactor); + setenv("QT_SCALE_FACTOR", scale, 1); + + QApplication_setAttribute(Qt::AA_X11InitThreads, true); + QApplication_setAttribute(Qt::AA_EnableHighDpiScaling, true); + QApplication_setAttribute(Qt::AA_UseHighDpiPixmaps, true); + + static int argc = 0; + static char* argv[] = { nullptr }; + + uint8_t _app[64]; // sizeof(QApplication) == 16 + QApplication* const app = reinterpret_cast(_app); + QApplication__init(app, argc, argv, 0); + + uint8_t _qstrurl[32]; // sizeof(QString) == 8 + QString* const qstrurl(reinterpret_cast(_qstrurl)); + + { + const size_t url_len = std::strlen(url); + QChar* const url_qchar = new QChar[url_len + 1]; + + for (size_t i = 0; i < url_len; ++i) + url_qchar[i] = QChar(url[i]); + + url_qchar[url_len] = 0; + + QString__init(qstrurl, url_qchar, url_len); + } + + uint8_t _qurl[32]; // sizeof(QUrl) == 8 + QUrl* const qurl(reinterpret_cast(_qurl)); + QUrl__init(qurl, *qstrurl, 1 /* QUrl::StrictMode */); + + uint8_t _webview[128]; // sizeof(QWebEngineView) == 56 + QWebEngineView* const webview = reinterpret_cast(_webview); + QWebEngineView__init(webview, nullptr); + + QWebEngineView_move(webview, QPoint(0, kVerticalOffset)); + QWebEngineView_resize(webview, QSize(DISTRHO_UI_DEFAULT_WIDTH, DISTRHO_UI_DEFAULT_HEIGHT - kVerticalOffset)); + QWebEngineView_winId(webview); + QWindow_setParent(QWebEngineView_windowHandle(webview), QWindow_fromWinId(winId)); + QWebEngineView_setUrl(webview, *qurl); + QWebEngineView_show(webview); + + QApplication_exec(); + + dlclose(lib); + return true; +} + +END_NAMESPACE_DISTRHO + +// ----------------------------------------------------------------------------------------------------------- +// startup via ld-linux + +static void signalHandler(const int sig) +{ + DISTRHO_SAFE_ASSERT_RETURN(sig == SIGUSR1,); + + d_stdout("TODO: refresh web view"); +} + +DISTRHO_PLUGIN_EXPORT +void _start() +{ + uselocale(newlocale(LC_NUMERIC_MASK, "C", nullptr)); + + const char* const envPort = std::getenv("DPF_WEBVIEW_PORT"); + DISTRHO_SAFE_ASSERT_RETURN(envPort != nullptr, exit(1)); + + const char* const envScaleFactor = std::getenv("DPF_WEBVIEW_SCALE_FACTOR"); + DISTRHO_SAFE_ASSERT_RETURN(envScaleFactor != nullptr, exit(1)); + + const char* const envWinId = std::getenv("DPF_WEBVIEW_WIN_ID"); + DISTRHO_SAFE_ASSERT_RETURN(envWinId != nullptr, exit(1)); + + const Window winId = std::strtoul(envWinId, nullptr, 10); + DISTRHO_SAFE_ASSERT_RETURN(winId != 0, exit(1)); + + const double scaleFactor = std::atof(envScaleFactor); + DISTRHO_SAFE_ASSERT_RETURN(scaleFactor > 0.0, exit(1)); + + Display* const display = XOpenDisplay(nullptr); + DISTRHO_SAFE_ASSERT_RETURN(display != nullptr, exit(1)); + + char url[32] = {}; + std::snprintf(url, 31, "http://127.0.0.1:%s/", envPort); + + struct sigaction sig = {}; + sig.sa_handler = signalHandler; + sig.sa_flags = SA_RESTART; + sigemptyset(&sig.sa_mask); + sigaction(SIGUSR1, &sig, nullptr); + + qt5webengine(winId, scaleFactor, url) || + qt6webengine(winId, scaleFactor, url) || + gtk3(display, winId, scaleFactor, url); + + XCloseDisplay(display); + + exit(0); +} + +// ----------------------------------------------------------------------------------------------------------- diff --git a/src/plugin/utils.cpp b/src/plugin/utils.cpp new file mode 100644 index 0000000..8efd8f6 --- /dev/null +++ b/src/plugin/utils.cpp @@ -0,0 +1,594 @@ +// SPDX-FileCopyrightText: 2023-2024 MOD Audio UG +// SPDX-License-Identifier: AGPL-3.0-or-later + +#include "utils.hpp" + +#ifdef _WIN32 +#include +#include +#include +#include +// FIXME +namespace std { + using ::wcschr; + using ::wcslen; + using ::wcsncat; + using ::wcsncmp; + using ::wcsncpy; + using ::snwprintf; +} +#else +#include +#include +#endif + +#if defined(__APPLE__) +#include +#include +#include +#elif defined(__linux__) +#include +#endif + +#include +#include +#include + +START_NAMESPACE_DISTRHO + +// ----------------------------------------------------------------------------------------------------------- + +constexpr const uint8_t mod_desktop_hash[] = { + 0xe4, 0xf7, 0x0d, 0xe9, 0x77, 0xb8, 0x47, 0xe0, 0xba, 0x2e, 0x70, 0x14, 0x93, 0x7a, 0xce, 0xa7 +}; + +constexpr uint8_t char2u8(const uint8_t c) +{ + return c >= '0' && c <= '9' ? c - '0' + : c >= 'a' && c <= 'f' ? 0xa + c - 'a' + : c >= 'A' && c <= 'F' ? 0xa + c - 'A' + : 0; +} + +// ----------------------------------------------------------------------------------------------------------- + +// FIXME share with systray +#ifdef _WIN32 +static const wchar_t* getDataDirW() +{ + static wchar_t dataDir[MAX_PATH] = {}; + + if (dataDir[0] == 0) + { + SHGetSpecialFolderPathW(nullptr, dataDir, CSIDL_MYDOCUMENTS, false); + _wmkdir(dataDir); + + std::wcsncat(dataDir, L"\\MOD Desktop", MAX_PATH - 1); + _wmkdir(dataDir); + } + + return dataDir; +} +#else +static const char* getDataDir() +{ + static char dataDir[PATH_MAX] = {}; + + if (dataDir[0] == 0) + { + // TODO + std::strncpy(dataDir, getenv("HOME"), PATH_MAX - 1); + mkdir(dataDir, 0777); + + std::strncat(dataDir, "/Documents", PATH_MAX - 1); + mkdir(dataDir, 0777); + + std::strncat(dataDir, "/MOD Desktop", PATH_MAX - 1); + mkdir(dataDir, 0777); + } + + return dataDir; +} +#endif + +// ----------------------------------------------------------------------------------------------------------- + +#ifdef _WIN32 +static const wchar_t* getAppDirW() +{ + static wchar_t appDir[MAX_PATH] = {}; + + if (appDir[0] == 0) + { + SHGetSpecialFolderPathW(nullptr, appDir, CSIDL_PROGRAM_FILES, false); + std::wcsncat(appDir, L"\\MOD Desktop", MAX_PATH - 1); + } + + return appDir; +} +#endif + +const char* getAppDir() +{ + #ifdef DISTRHO_OS_MAC + return "/Applications/MOD Desktop.app/Contents/MacOS"; + #else + static char appDir[PATH_MAX] = {}; + + if (appDir[0] == 0) + { + #ifdef _WIN32 + WideCharToMultiByte(CP_UTF8, 0, getAppDirW(), -1, appDir, PATH_MAX, nullptr, nullptr); + #else + std::strncpy(appDir, getDataDir(), PATH_MAX - 1); + std::strncat(appDir, "/.last-known-location-" VERSION, PATH_MAX - 1); + + if (FILE* const f = std::fopen(appDir, "r")) + { + std::memset(appDir, 0, PATH_MAX - 1); + + if (std::fread(appDir, PATH_MAX - 1, 1, f) != 0) + appDir[0] = 0; + + else if (access(appDir, F_OK) != 0) + appDir[0] = 0; + + std::fclose(f); + } + else + { + appDir[0] = 0; + return nullptr; + } + + if (appDir[0] == 0) + return nullptr; + #endif + } + + return appDir; + #endif +} + +// ----------------------------------------------------------------------------------------------------------- + +#ifdef _WIN32 +static void set_envp_value(wchar_t** envp, const wchar_t* const fullvalue) +{ + const size_t keylen = (std::wcschr(fullvalue, L'=') - fullvalue) / sizeof(wchar_t); + + for (; *envp != nullptr; ++envp) + { + if (std::wcsncmp(*envp, fullvalue, keylen + 1) == 0) + { + const size_t fulllen = std::wcslen(fullvalue); + *envp = static_cast(std::realloc(*envp, (fulllen + 1) * sizeof(wchar_t))); + std::memcpy(*envp + (keylen + 1), fullvalue + (keylen + 1), (fulllen - keylen) * sizeof(wchar_t)); + return; + } + } + + *envp = _wcsdup(fullvalue); +} + +static void set_envp_value(wchar_t** envp, const wchar_t* const key, const wchar_t* const value) +{ + const size_t keylen = std::wcslen(key); + const size_t valuelen = std::wcslen(value); + + for (; *envp != nullptr; ++envp) + { + if (std::wcsncmp(*envp, key, keylen) == 0 && (*envp)[keylen] == L'=') + { + *envp = static_cast(std::realloc(*envp, (keylen + valuelen + 2) * sizeof(wchar_t))); + std::memcpy(*envp + (keylen + 1), value, (valuelen + 1) * sizeof(wchar_t)); + return; + } + } + + *envp = static_cast(std::malloc((keylen + valuelen + 2) * sizeof(wchar_t))); + std::memcpy(*envp, key, keylen * sizeof(wchar_t)); + std::memcpy(*envp + (keylen + 1), value, (valuelen + 1) * sizeof(wchar_t)); + (*envp)[keylen] = '='; +} +#else +void set_envp_value(char** envp, const char* const fullvalue) +{ + const size_t keylen = std::strchr(fullvalue, '=') - fullvalue; + + for (; *envp != nullptr; ++envp) + { + if (std::strncmp(*envp, fullvalue, keylen + 1) == 0) + { + const size_t fulllen = std::strlen(fullvalue); + *envp = static_cast(std::realloc(*envp, fulllen + 1)); + std::memcpy(*envp + (keylen + 1), fullvalue + (keylen + 1), fulllen - keylen); + return; + } + } + + *envp = strdup(fullvalue); +} + +void set_envp_value(char** envp, const char* const key, const char* const value) +{ + const size_t keylen = std::strlen(key); + const size_t valuelen = std::strlen(value); + + for (; *envp != nullptr; ++envp) + { + if (std::strncmp(*envp, key, keylen) == 0 && (*envp)[keylen] == '=') + { + *envp = static_cast(std::realloc(*envp, keylen + valuelen + 2)); + std::memcpy(*envp + (keylen + 1), value, valuelen + 1); + return; + } + } + + *envp = static_cast(std::malloc(keylen + valuelen + 2)); + std::memcpy(*envp, key, keylen); + std::memcpy(*envp + (keylen + 1), value, valuelen + 1); + (*envp)[keylen] = '='; +} +#endif + +// ----------------------------------------------------------------------------------------------------------- + +// NOTE this needs to match initEvironment from systray side +#ifdef _WIN32 +const wchar_t* getEvironment(const uint portBaseNum) +#else +char* const* getEvironment(const uint portBaseNum) +#endif +{ + // get directory of the mod-desktop application + #ifdef _WIN32 + const wchar_t* const appDir = getAppDirW(); + #else + const char* const appDir = getAppDir(); + #endif + + // can be null, in case of not found + if (appDir == nullptr) + return nullptr; + + #ifdef DISTRHO_OS_MAC + const char* const* const* const environptr = _NSGetEnviron(); + DISTRHO_SAFE_ASSERT_RETURN(environptr != nullptr, nullptr); + + const char* const* const environ = *environptr; + DISTRHO_SAFE_ASSERT_RETURN(environ != nullptr, nullptr); + #endif + + uint envsize = 0; + #ifdef _WIN32 + wchar_t** envpl; + + if (wchar_t* const envs = GetEnvironmentStringsW()) + { + for (wchar_t* envsi = envs; *envsi != '\0'; envsi += (std::wcslen(envsi) + 1)) + ++envsize; + + envpl = new wchar_t*[envsize + 32]; + + uint i = 0; + for (wchar_t* envsi = envs; *envsi != '\0'; envsi += (std::wcslen(envsi) + 1)) + envpl[i] = _wcsdup(envsi); + + FreeEnvironmentStringsW(envs); + } + else + { + envpl = new wchar_t*[32]; + } + + for (uint i = 0; i < 32; ++i) + envpl[envsize + i] = nullptr; + + // base environment details + set_envp_value(envpl, L"LANG=en_US.UTF-8"); + set_envp_value(envpl, L"MOD_DESKTOP=1"); + set_envp_value(envpl, L"MOD_DESKTOP_PLUGIN=1"); + set_envp_value(envpl, L"PYTHONUNBUFFERED=1"); + #else + while (environ[envsize] != nullptr) + ++envsize; + + char** const envp = new char*[envsize + 32]; + + for (uint i = 0; i < envsize; ++i) + envp[i] = strdup(environ[i]); + + for (uint i = 0; i < 32; ++i) + envp[envsize + i] = nullptr; + + // base environment details + set_envp_value(envp, "LANG=en_US.UTF-8"); + set_envp_value(envp, "MOD_DESKTOP=1"); + set_envp_value(envp, "MOD_DESKTOP_PLUGIN=1"); + set_envp_value(envp, "PYTHONUNBUFFERED=1"); + #endif + + // get and set directory to our documents and settings, under "user documents"; also make sure it exists + #ifdef _WIN32 + const wchar_t* const dataDir = getDataDirW(); + set_envp_value(envpl, L"MOD_DATA_DIR", dataDir); + #else + const char* const dataDir = getDataDir(); + set_envp_value(envp, "MOD_DATA_DIR", dataDir); + #endif + + // reusable + #ifdef _WIN32 + wchar_t path[MAX_PATH] = {}; + const size_t appDirLen = std::wcslen(appDir); + const size_t dataDirLen = std::wcslen(dataDir); + #else + char path[PATH_MAX] = {}; + const size_t appDirLen = std::strlen(appDir); + const size_t dataDirLen = std::strlen(dataDir); + #endif + + // generate UID + uint8_t key[16] = {}; + #if defined(__APPLE__) + if (const CFMutableDictionaryRef deviceRef = IOServiceMatching("IOPlatformExpertDevice")) + { + const io_service_t service = IOServiceGetMatchingService(MACH_PORT_NULL, deviceRef); + + if (service != IO_OBJECT_NULL) + { + const CFTypeRef uuidRef = IORegistryEntryCreateCFProperty(service, CFSTR("IOPlatformUUID"), kCFAllocatorDefault, 0); + const CFStringRef uuidStr = reinterpret_cast(uuidRef); + + if (CFGetTypeID(uuidRef) == CFStringGetTypeID() && CFStringGetLength(uuidStr) >= 36) + { + CFStringGetCString(uuidStr, path, PATH_MAX - 1, kCFStringEncodingUTF8); + + for (int i=0, j=0; i+j<36; ++i) + { + if (path[i*2+j] == '-') + ++j; + key[i] = char2u8(path[i*2+j]) << 4; + key[i] |= char2u8(path[i*2+j+1]) << 0; + } + } + + CFRelease(uuidRef); + IOObjectRelease(service); + } + } + #elif defined(_WIN32) + // TODO + #else + if (FILE* const f = fopen("/etc/machine-id", "r")) + { + if (fread(path, PATH_MAX - 1, 1, f) == 0 && strlen(path) >= 33) + { + for (int i=0; i<16; ++i) + { + key[i] = char2u8(path[i*2]) << 4; + key[i] |= char2u8(path[i*2+1]) << 0; + } + } + fclose(f); + } + #endif + + /* + { + QMessageAuthenticationCode qhash(QCryptographicHash::Sha256); + qhash.setKey(QByteArray::fromRawData(reinterpret_cast(key), sizeof(key))); + qhash.addData(reinterpret_cast(mod_desktop_hash), sizeof(mod_desktop_hash)); + + QByteArray qresult(qhash.result().toHex(':').toUpper()); + qresult.truncate(32 + 15); + + #ifdef _WIN32 + set_envp_value(envp, L"MOD_DEVICE_UID", qresult.constData()); + #else + set_envp_value(envp, "MOD_DEVICE_UID", qresult.constData(), 1); + #endif + } + */ + + // set path to factory pedalboards + #ifdef _WIN32 + std::memcpy(path, appDir, appDirLen * sizeof(WCHAR)); + std::wcsncpy(path + appDirLen, L"\\pedalboards", MAX_PATH - appDirLen - 1); + set_envp_value(envpl, L"MOD_FACTORY_PEDALBOARDS_DIR", path); + #else + std::memcpy(path, appDir, appDirLen); + #ifdef __APPLE__ + std::strncpy(path + appDirLen - 5, "Resources/pedalboards", PATH_MAX - appDirLen - 1); + #else + std::strncpy(path + appDirLen, "/pedalboards", PATH_MAX - appDirLen - 1); + #endif + set_envp_value(envp, "MOD_FACTORY_PEDALBOARDS_DIR", path); + #endif + + // set path to plugin loadable "user-files"; also make sure it exists + #ifdef _WIN32 + std::memcpy(path, dataDir, dataDirLen * sizeof(WCHAR)); + std::wcsncpy(path + dataDirLen, L"\\user-files", MAX_PATH - dataDirLen - 1); + _wmkdir(path); + set_envp_value(envpl, L"MOD_USER_FILES_DIR", path); + #else + std::memcpy(path, dataDir, dataDirLen); + std::strncpy(path + dataDirLen, "/user-files", PATH_MAX - dataDirLen - 1); + mkdir(path, 0777); + set_envp_value(envp, "MOD_USER_FILES_DIR", path); + #endif + + // set path to MOD keys (plugin licenses) + // NOTE must terminate with a path separator + #ifdef _WIN32 + std::memcpy(path, dataDir, dataDirLen * sizeof(WCHAR)); + std::wcsncpy(path + dataDirLen, L"\\keys\\", MAX_PATH - dataDirLen - 1); + set_envp_value(envpl, L"MOD_KEYS_PATH", path); + #else + std::memcpy(path, dataDir, dataDirLen); + std::strncpy(path + dataDirLen, "/keys/", PATH_MAX - dataDirLen - 1); + set_envp_value(envp, "MOD_KEYS_PATH", path); + #endif + + // set path to MOD LV2 plugins (first local/user, then app/system) + #ifdef _WIN32 + std::memcpy(path, dataDir, dataDirLen * sizeof(WCHAR)); + std::wcsncpy(path + dataDirLen, L"\\lv2;", MAX_PATH - dataDirLen - 1); + std::wcsncat(path, appDir, MAX_PATH - 1); + std::wcsncat(path, L"\\plugins", MAX_PATH - 1); + set_envp_value(envpl, L"LV2_PATH", path); + #else + std::memcpy(path, dataDir, dataDirLen); + std::strncpy(path + dataDirLen, "/lv2:", PATH_MAX - dataDirLen - 1); + std::strncat(path, appDir, PATH_MAX - 1); + #ifdef __APPLE__ + path[dataDirLen + 5 + appDirLen - 5] = '\0'; // remove "MacOS" + std::strncat(path, "LV2", PATH_MAX - 1); + #else + std::strncat(path, "/plugins", PATH_MAX - 1); + #endif + set_envp_value(envp, "LV2_PATH", path); + #endif + + #if !(defined(__APPLE__) || defined(_WIN32)) + // add our custom lib path on top of LD_LIBRARY_PATH to make sure jackd can run + if (const char* const ldpath = getenv("LD_LIBRARY_PATH")) + { + std::memcpy(path, appDir, appDirLen); + path[appDirLen] = ':'; + std::strncpy(path + appDirLen + 1, ldpath, PATH_MAX - appDirLen - 2); + set_envp_value(envp, "LD_LIBRARY_PATH", path); + } + else + { + set_envp_value(envp, "LD_LIBRARY_PATH", appDir); + } + #endif + + // other + #ifndef _WIN32 + std::memcpy(path, appDir, appDirLen); + std::strncpy(path + appDirLen, "/jack", PATH_MAX - appDirLen - 1); + set_envp_value(envp, "JACK_DRIVER_DIR", path); + #endif + + // plugin-specific port details + #ifdef _WIN32 + std::snwprintf(path, PATH_MAX - 1, L"MOD_DESKTOP_SERVER_NAME=mod-desktop-%u", portBaseNum); + set_envp_value(envpl, path); + + std::snwprintf(path, PATH_MAX - 1, L"MOD_DEVICE_HOST_PORT=%u", kPortNumOffset + portBaseNum + 1); + set_envp_value(envpl, path); + + std::snwprintf(path, PATH_MAX - 1, L"MOD_DEVICE_WEBSERVER_PORT=%u", kPortNumOffset + portBaseNum); + set_envp_value(envpl, path); + #else + std::snprintf(path, PATH_MAX - 1, "MOD_DESKTOP_SERVER_NAME=mod-desktop-%u", portBaseNum); + set_envp_value(envp, path); + + std::snprintf(path, PATH_MAX - 1, "MOD_DEVICE_HOST_PORT=%u", kPortNumOffset + portBaseNum + 1); + set_envp_value(envp, path); + + std::snprintf(path, PATH_MAX - 1, "MOD_DEVICE_WEBSERVER_PORT=%u", kPortNumOffset + portBaseNum); + set_envp_value(envp, path); + #endif + + #ifdef _WIN32 + // merge env var list into single string + envsize = 0; + for (uint i = 0; envpl[i] != nullptr; ++i) + envsize += std::wcslen(envpl[i]) + 1; + + wchar_t* const envp = new wchar_t[envsize + 1]; + + envsize = 0; + for (uint i = 0; envpl[i] != nullptr; ++i) + { + const uint len = std::wcslen(envpl[i]) + 1; + std::memcpy(envp + envsize, envpl[i], len * sizeof(wchar_t)); + std::free(envpl[i]); + envsize += len; + } + envp[envsize] = 0; + #endif + + return envp; +} + +// ----------------------------------------------------------------------------------------------------------- + +static void* _openWebGui(void* const arg) +{ + const uint* const portptr = static_cast(arg); + const uint port = *portptr; + delete portptr; + + #ifdef _WIN32 + WCHAR url[32] = {}; + std::snwprintf(url, 31, L"http://127.0.0.1:%u", port); + ShellExecuteW(nullptr, L"open", url, nullptr, nullptr, SW_SHOWDEFAULT); + #else + #ifdef __APPLE__ + String cmd("open \"http://127.0.0.1:"); + #else + String cmd("xdg-open \"http://127.0.0.1:"); + #endif + cmd += String(port); + cmd += "/\""; + std::system(cmd); + #endif + + return nullptr; +} + +void openWebGui(const uint port) +{ + uint* const portptr = new uint; + *portptr = port; + + pthread_t thread; + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, 1); + + if (pthread_create(&thread, &attr, _openWebGui, portptr) != 0) + delete portptr; + + pthread_attr_destroy(&attr); +} + +// ----------------------------------------------------------------------------------------------------------- + +static void* _openUserFilesDir(void*) +{ + #ifdef _WIN32 + ShellExecuteW(NULL, L"explore", getDataDirW(), nullptr, nullptr, SW_SHOWDEFAULT); + #else + #ifdef __APPLE__ + String cmd("open \""); + #else + String cmd("xdg-open \""); + #endif + cmd += getDataDir(); + cmd += "\""; + std::system(cmd); + #endif + + return nullptr; +} + +void openUserFilesDir() +{ + pthread_t thread; + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, 1); + pthread_create(&thread, &attr, _openUserFilesDir, nullptr); + pthread_attr_destroy(&attr); +} + +// ----------------------------------------------------------------------------------------------------------- + +END_NAMESPACE_DISTRHO diff --git a/src/plugin/utils.hpp b/src/plugin/utils.hpp new file mode 100644 index 0000000..27d0350 --- /dev/null +++ b/src/plugin/utils.hpp @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2023-2024 MOD Audio UG +// SPDX-License-Identifier: AGPL-3.0-or-later + +#pragma once + +#include "DistrhoPluginUtils.hpp" + +START_NAMESPACE_DISTRHO + +// ----------------------------------------------------------------------------------------------------------- + +/* Get the MOD Desktop application directory. + */ +const char* getAppDir(); + +/* Get environment to be used for a child process. + */ +#ifdef _WIN32 +const wchar_t* getEvironment(uint portBaseNum); +#else +char* const* getEvironment(uint portBaseNum); + +// helpers +void set_envp_value(char** envp, const char* const fullvalue); +void set_envp_value(char** envp, const char* const key, const char* const value); +#endif + +/* Open a web browser with the mod-ui URL as address. + */ +void openWebGui(uint port); + +/* Open the "user files" directory in a file manager/explorer. + */ +void openUserFilesDir(); + +// ----------------------------------------------------------------------------------------------------------- + +END_NAMESPACE_DISTRHO diff --git a/src/plugin/zita-resampler/resampler-table.cc b/src/plugin/zita-resampler/resampler-table.cc new file mode 100644 index 0000000..5c517d4 --- /dev/null +++ b/src/plugin/zita-resampler/resampler-table.cc @@ -0,0 +1,149 @@ +// ---------------------------------------------------------------------------- +// +// Copyright (C) 2006-2023 Fons Adriaensen +// +// 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. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ---------------------------------------------------------------------------- + + +#include +#include +#include +#include +#include "resampler-table.h" + + +#undef ENABLE_VEC4 +#if (defined(__SSE2_MATH__) || defined(__ARM_NEON) || defined(__ARM_NEON__)) && !defined(_WIN32) +# define ENABLE_VEC4 +#endif + + +static double sinc (double x) +{ + x = fabs (x); + if (x < 1e-6) return 1.0; + x *= M_PI; + return sin (x) / x; +} + + +static double wind (double x) +{ + x = fabs (x); + if (x >= 1.0) return 0.0f; + x *= M_PI; + return 0.384 + 0.500 * cos (x) + 0.116 * cos (2 * x); +} + + +Resampler_table *Resampler_table::_list = 0; +Resampler_mutex Resampler_table::_mutex; + + +Resampler_table::Resampler_table (double fr, unsigned int hl, unsigned int np) : + _next (0), + _refc (0), + _fr (fr), + _hl (hl), + _np (np) +{ + unsigned int i, j, n; + double t; + float *p; + + n = hl * (np + 1); +#ifdef ENABLE_VEC4 + posix_memalign ((void **) &_ctab, 16, n * sizeof (float)); +#else + _ctab = new float [n]; +#endif + p = _ctab; + for (j = 0; j <= np; j++) + { + t = (double) j / (double) np; + for (i = 0; i < hl; i++) + { + p [hl - i - 1] = (float)(fr * sinc (t * fr) * wind (t / hl)); + t += 1; + } + p += hl; + } +} + + +Resampler_table::~Resampler_table (void) +{ +#ifdef ENABLE_VEC4 + free (_ctab); +#else + delete[] _ctab; +#endif +} + + +Resampler_table *Resampler_table::create (double fr, unsigned int hl, unsigned int np) +{ + Resampler_table *P; + + _mutex.lock (); + P = _list; + while (P) + { + if ((fr >= P->_fr * 0.999) && (fr <= P->_fr * 1.001) && (hl == P->_hl) && (np == P->_np)) + { + P->_refc++; + _mutex.unlock (); + return P; + } + P = P->_next; + } + P = new Resampler_table (fr, hl, np); + P->_refc = 1; + P->_next = _list; + _list = P; + _mutex.unlock (); + return P; +} + + +void Resampler_table::destroy (Resampler_table *T) +{ + Resampler_table *P, *Q; + + _mutex.lock (); + if (T) + { + T->_refc--; + if (T->_refc == 0) + { + P = _list; + Q = 0; + while (P) + { + if (P == T) + { + if (Q) Q->_next = T->_next; + else _list = T->_next; + break; + } + Q = P; + P = P->_next; + } + delete T; + } + } + _mutex.unlock (); +} diff --git a/src/plugin/zita-resampler/resampler-table.h b/src/plugin/zita-resampler/resampler-table.h new file mode 100644 index 0000000..73d8f97 --- /dev/null +++ b/src/plugin/zita-resampler/resampler-table.h @@ -0,0 +1,68 @@ +// ---------------------------------------------------------------------------- +// +// Copyright (C) 2006-2023 Fons Adriaensen +// +// 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. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ---------------------------------------------------------------------------- + + +#ifndef __RESAMPLER_TABLE_H +#define __RESAMPLER_TABLE_H + + +#include + + +class Resampler_mutex +{ +private: + + friend class Resampler_table; + + Resampler_mutex (void) { pthread_mutex_init (&_mutex, nullptr); } + ~Resampler_mutex (void) { pthread_mutex_destroy (&_mutex); } + void lock (void) { pthread_mutex_lock (&_mutex); } + void unlock (void) { pthread_mutex_unlock (&_mutex); } + + pthread_mutex_t _mutex; +}; + + +class Resampler_table +{ +private: + + Resampler_table (double fr, unsigned int hl, unsigned int np); + ~Resampler_table (void); + + friend class Resampler; + friend class VResampler; + + Resampler_table *_next; + unsigned int _refc; + float *_ctab; + double _fr; + unsigned int _hl; + unsigned int _np; + + static Resampler_table *create (double fr, unsigned int hl, unsigned int np); + static void destroy (Resampler_table *T); + + static Resampler_table *_list; + static Resampler_mutex _mutex; +}; + + +#endif diff --git a/src/plugin/zita-resampler/resampler.cc b/src/plugin/zita-resampler/resampler.cc new file mode 100644 index 0000000..526af07 --- /dev/null +++ b/src/plugin/zita-resampler/resampler.cc @@ -0,0 +1,342 @@ +// ---------------------------------------------------------------------------- +// +// Copyright (C) 2006-2023 Fons Adriaensen +// +// 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. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ---------------------------------------------------------------------------- + + +#include +#include +#include +#include + +#include "resampler.h" + +#undef ENABLE_VEC4 +#ifndef _WIN32 +# if defined(__SSE2_MATH__) +# define ENABLE_VEC4 +# include +# elif defined(__ARM_NEON) || defined(__ARM_NEON__) +# define ENABLE_VEC4 +# include +# endif +#endif + + +static unsigned int gcd (unsigned int a, unsigned int b) +{ + if (a == 0) return b; + if (b == 0) return a; + while (1) + { + if (a > b) + { + a = a % b; + if (a == 0) return b; + if (a == 1) return 1; + } + else + { + b = b % a; + if (b == 0) return a; + if (b == 1) return 1; + } + } + return 1; +} + + +Resampler::Resampler (void) noexcept : + _table (0), + _nchan (0), + _buff (0) +{ + reset (); +} + + +Resampler::~Resampler (void) +{ + clear (); +} + + +bool Resampler::setup (unsigned int fs_inp, + unsigned int fs_out, + unsigned int nchan, + unsigned int hlen) +{ + return setup (fs_inp, fs_out, nchan, hlen, 1.0 - 2.6 / hlen); +} + + +bool Resampler::setup (unsigned int fs_inp, + unsigned int fs_out, + unsigned int nchan, + unsigned int hlen, + double frel) +{ + unsigned int np, dp, mi, hl, n; + double r; + Resampler_table *T = 0; + + if (!nchan || (hlen < 8) || (hlen > 96)) + { + clear (); + return false; + } + + r = (double) fs_out / (double) fs_inp; + n = gcd (fs_out, fs_inp); + np = fs_out / n; + dp = fs_inp / n; + if ((64 * r < 1.0) || (np > 1000)) + { + clear (); + return false; + } + + hl = hlen; + mi = 32; + if (r < 1.0) + { + frel *= r; + hl = (unsigned int)(ceil (hl / r)); + mi = (unsigned int)(ceil (mi / r)); + } +#ifdef ENABLE_VEC4 + hl = (hl + 3) & ~3; +#endif + T = Resampler_table::create (frel, hl, np); + + clear (); + if (T) + { + _table = T; + n = nchan * (2 * hl + mi); +#ifdef ENABLE_VEC4 + posix_memalign ((void **)(&_buff), 16, n * sizeof (float)); + memset (_buff, 0, n * sizeof (float)); +#else + _buff = new float [n]; +#endif + _nchan = nchan; + _inmax = mi; + _pstep = dp; + return reset (); + } + else return false; +} + + +void Resampler::clear (void) +{ + Resampler_table::destroy (_table); +#ifdef ENABLE_VEC4 + free (_buff); +#else + delete[] _buff; +#endif + _buff = 0; + _table = 0; + _nchan = 0; + _inmax = 0; + _pstep = 0; + reset (); +} + + +double Resampler::inpdist (void) const noexcept +{ + if (!_table) return 0; + return (int)(_table->_hl + 1 - _nread) - (double)_phase / _table->_np; +} + + +int Resampler::inpsize (void) const noexcept +{ + if (!_table) return 0; + return 2 * _table->_hl; +} + + +bool Resampler::reset (void) noexcept +{ + if (!_table) return false; + + inp_count = 0; + out_count = 0; + inp_data = 0; + out_data = 0; + _index = 0; + _nread = 0; + _nzero = 0; + _phase = 0; + if (_table) + { + _nread = 2 * _table->_hl; + return true; + } + return false; +} + + +bool Resampler::process (void) +{ + unsigned int hl, np, ph, dp, in, nr, nz, di, i, j, n; + float *c1, *c2, *p1, *p2, *q1, *q2; + + if (!_table) return false; + hl = _table->_hl; + np = _table->_np; + dp = _pstep; + in = _index; + nr = _nread; + nz = _nzero; + ph = _phase; + + p1 = _buff + in; + p2 = p1 + 2 * hl - nr; + di = 2 * hl + _inmax; + + while (out_count) + { + while (nr && inp_count) + { + if (inp_data) + { + for (j = 0; j < _nchan; j++) p2 [j * di] = inp_data [j]; + inp_data += _nchan; + nz = 0; + } + else + { + for (j = 0; j < _nchan; j++) p2 [j * di] = 0; + if (nz < 2 * hl) nz++; + } + p2++; + nr--; + inp_count--; + } + if (nr) break; + + if (out_data) + { + if (nz < 2 * hl) + { + c1 = _table->_ctab + hl * ph; + c2 = _table->_ctab + hl * (np - ph); + +#if defined(__SSE2_MATH__) && !defined(_WIN32) + __m128 C1, C2, Q1, Q2, S; + for (j = 0; j < _nchan; j++) + { + q1 = p1 + j * di; + q2 = p2 + j * di; + S = _mm_setzero_ps (); + for (i = 0; i < hl; i += 4) + { + C1 = _mm_load_ps (c1 + i); + Q1 = _mm_loadu_ps (q1); + q2 -= 4; + S = _mm_add_ps (S, _mm_mul_ps (C1, Q1)); + C2 = _mm_loadr_ps (c2 + i); + Q2 = _mm_loadu_ps (q2); + q1 += 4; + S = _mm_add_ps (S, _mm_mul_ps (C2, Q2)); + } + *out_data++ = S [0] + S [1] + S [2] + S [3]; + } + +#elif (defined(__ARM_NEON) || defined(__ARM_NEON__)) && !defined(_WIN32) + // ARM64 version by Nicolas Belin + float32x4_t *C1 = (float32x4_t *)c1; + float32x4_t *C2 = (float32x4_t *)c2; + float32x4_t S, T; + for (j = 0; j < _nchan; j++) + { + q1 = p1 + j * di; + q2 = p2 + j * di - 4; + T = vrev64q_f32 (vld1q_f32 (q2)); + S = vmulq_f32 (vextq_f32 (T, T, 2), C2 [0]); + S = vmlaq_f32 (S, vld1q_f32(q1), C1 [0]); + for (i = 1; i < (hl>>2); i++) + { + q2 -= 4; + q1 += 4; + T = vrev64q_f32 (vld1q_f32 (q2)); + S = vmlaq_f32 (S, vextq_f32 (T, T, 2), C2 [i]); + S = vmlaq_f32 (S, vld1q_f32 (q1), C1 [i]); + } + *out_data++ = S [0] + S [1] + S [2] + S [3]; + } + +#else + float s; + for (j = 0; j < _nchan; j++) + { + q1 = p1 + j * di; + q2 = p2 + j * di; + s = 1e-30f; + for (i = 0; i < hl; i++) + { + q2--; + s += *q1 * c1 [i] + *q2 * c2 [i]; + q1++; + } + *out_data++ = s - 1e-30f; + } +#endif + } + else + { + for (j = 0; j < _nchan; j++) *out_data++ = 0; + } + } + out_count--; + + ph += dp; + if (ph >= np) + { + nr = ph / np; + ph -= nr * np; + in += nr; + p1 += nr; + if (in >= _inmax) + { + n = 2 * hl - nr; + p2 = _buff; + for (j = 0; j < _nchan; j++) + { + memmove (p2 + j * di, p1 + j * di, n * sizeof (float)); + } + in = 0; + p1 = _buff; + p2 = p1 + n; + } + } + } + + _index = in; + _nread = nr; + _phase = ph; + _nzero = nz; + + return true; +} + + diff --git a/src/plugin/zita-resampler/resampler.h b/src/plugin/zita-resampler/resampler.h new file mode 100644 index 0000000..38b1fe4 --- /dev/null +++ b/src/plugin/zita-resampler/resampler.h @@ -0,0 +1,76 @@ +// ---------------------------------------------------------------------------- +// +// Copyright (C) 2006-2023 Fons Adriaensen +// +// 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. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ---------------------------------------------------------------------------- + + +#ifndef __RESAMPLER_H +#define __RESAMPLER_H + + +#include "resampler-table.h" + + +class Resampler +{ +public: + + Resampler (void) noexcept; + ~Resampler (void); + + bool setup (unsigned int fs_inp, + unsigned int fs_out, + unsigned int nchan, + unsigned int hlen); + + bool setup (unsigned int fs_inp, + unsigned int fs_out, + unsigned int nchan, + unsigned int hlen, + double frel); + + void clear (void); + bool reset (void) noexcept; + int nchan (void) const noexcept { return _nchan; } + int filtlen (void) const noexcept { return inpsize (); } // Deprecated + int inpsize (void) const noexcept; + double inpdist (void) const noexcept; + bool process (void); + + unsigned int inp_count; + unsigned int out_count; + float *inp_data; + float *out_data; + float **inp_list; + float **out_list; + +private: + + Resampler_table *_table; + unsigned int _nchan; + unsigned int _inmax; + unsigned int _index; + unsigned int _nread; + unsigned int _nzero; + unsigned int _phase; + unsigned int _pstep; + float *_buff; + void *_dummy [8]; +}; + + +#endif diff --git a/src/systray/main.cpp b/src/systray/main.cpp index 80b1a62..2ff61ba 100644 --- a/src/systray/main.cpp +++ b/src/systray/main.cpp @@ -9,6 +9,10 @@ int main(int argc, char* argv[]) { + QApplication::setAttribute(Qt::AA_X11InitThreads); + QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); + initEvironment(); setupControlCloseSignal(); diff --git a/src/systray/mod-desktop.hpp b/src/systray/mod-desktop.hpp index 3f02f38..efe1c43 100644 --- a/src/systray/mod-desktop.hpp +++ b/src/systray/mod-desktop.hpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include #include diff --git a/src/systray/utils.cpp b/src/systray/utils.cpp index 9aa0957..1aa6c93 100644 --- a/src/systray/utils.cpp +++ b/src/systray/utils.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 MOD Audio UG +// SPDX-FileCopyrightText: 2023-2024 MOD Audio UG // SPDX-License-Identifier: AGPL-3.0-or-later #include "utils.hpp" @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -46,6 +47,7 @@ constexpr uint8_t char2u8(const uint8_t c) : 0; } +// NOTE this needs to match getEvironment from plugin side void initEvironment() { // base environment details @@ -53,11 +55,15 @@ void initEvironment() SetEnvironmentVariableW(L"JACK_NO_START_SERVER", L"1"); SetEnvironmentVariableW(L"LANG", L"en_US.UTF-8"); SetEnvironmentVariableW(L"MOD_DESKTOP", L"1"); + SetEnvironmentVariableW(L"MOD_DEVICE_HOST_PORT", L"18182"); + SetEnvironmentVariableW(L"MOD_DEVICE_WEBSERVER_PORT", L"18181"); SetEnvironmentVariableW(L"PYTHONUNBUFFERED", L"1"); #else setenv("JACK_NO_START_SERVER", "1", 1); setenv("LANG", "en_US.UTF-8", 1); setenv("MOD_DESKTOP", "1", 1); + setenv("MOD_DEVICE_HOST_PORT", "18182", 1); + setenv("MOD_DEVICE_WEBSERVER_PORT", "18181", 1); setenv("PYTHONUNBUFFERED", "1", 1); #endif @@ -92,7 +98,7 @@ void initEvironment() } } - if (char* const c = strrchr(appDir, '/')) + if (char* const c = std::strrchr(appDir, '/')) *c = 0; #endif @@ -140,6 +146,21 @@ void initEvironment() const size_t dataDirLen = std::strlen(dataDir); #endif + // write our location to disk so the plugin version knows where to find us + #ifdef __linux__ + if (access(dataDir, F_OK) == 0) + { + std::memcpy(path, dataDir, dataDirLen); + std::strncpy(path + dataDirLen, "/.last-known-location-" VERSION, PATH_MAX - dataDirLen - 1); + + if (FILE* const f = std::fopen(path, "w")) + { + std::fwrite(appDir, appDirLen, 1, f); + std::fclose(f); + } + } + #endif + // generate UID uint8_t key[16] = {}; #if defined(__APPLE__) @@ -172,9 +193,9 @@ void initEvironment() #elif defined(_WIN32) // TODO #else - if (FILE* const f = fopen("/etc/machine-id", "r")) + if (FILE* const f = std::fopen("/etc/machine-id", "r")) { - if (fread(path, PATH_MAX - 1, 1, f) == 0 && strlen(path) >= 33) + if (std::fread(path, PATH_MAX - 1, 1, f) == 0 && std::strlen(path) >= 33) { for (int i=0; i<16; ++i) { @@ -182,7 +203,7 @@ void initEvironment() key[i] |= char2u8(path[i*2+1]) << 0; } } - fclose(f); + std::fclose(f); } #endif @@ -263,7 +284,6 @@ void initEvironment() #if !(defined(__APPLE__) || defined(_WIN32)) // special handling for PipeWire JACK, need to find full path to shared lib - bool usingPipeWire = false; if (void* const pwlib = dlmopen(LM_ID_NEWLM, "libjack.so.0", RTLD_NOW|RTLD_LOCAL)) { typedef int (*jacksym)(void); @@ -275,7 +295,6 @@ void initEvironment() Dl_info info = {}; dladdr(sym2, &info); setenv("JACKBRIDGE_FILENAME", info.dli_fname, 1); - usingPipeWire = true; fprintf(stdout, "MOD Desktop DEBUG: jacklib syms %p %p | %d | using pipewire with filename '%s'\n", sym1, sym2, sym1(), info.dli_fname); } else @@ -287,7 +306,7 @@ void initEvironment() dlclose(pwlib); } - // if LD_LIBRARY_PATH is set, add our custom lib path on top to make sure jackd can run + // add our custom lib path on top of LD_LIBRARY_PATH to make sure jackd can run if (const char* const ldpath = getenv("LD_LIBRARY_PATH")) { std::memcpy(path, appDir, appDirLen); @@ -295,8 +314,7 @@ void initEvironment() std::strncpy(path + appDirLen + 1, ldpath, PATH_MAX - appDirLen - 2); setenv("LD_LIBRARY_PATH", path, 1); } - // always set path in case of PipeWire - else if (usingPipeWire) + else { setenv("LD_LIBRARY_PATH", appDir, 1); } diff --git a/src/systray/utils.hpp b/src/systray/utils.hpp index 15f6f97..da34700 100644 --- a/src/systray/utils.hpp +++ b/src/systray/utils.hpp @@ -5,6 +5,8 @@ #include +class QMainWindow; + static inline QWidget* getLastParentOrSelf(QWidget* const w) noexcept { diff --git a/utils/cxfreeze/mod-ui-setup.py b/utils/cxfreeze/mod-ui-setup.py index 0bba05d..17df057 100644 --- a/utils/cxfreeze/mod-ui-setup.py +++ b/utils/cxfreeze/mod-ui-setup.py @@ -29,8 +29,8 @@ os.environ['MOD_DEFAULT_PEDALBOARD'] = os.path.join(resdir, 'default.pedalboard') os.environ['MOD_DESKTOP'] = '1' os.environ['MOD_DEV_ENVIRONMENT'] = '0' -os.environ['MOD_DEVICE_HOST_PORT'] = '18182' -os.environ['MOD_DEVICE_WEBSERVER_PORT'] = '18181' +os.environ['MOD_DEVICE_HOST_PORT'] = os.environ.get("MOD_DEVICE_HOST_PORT", '18182') +os.environ['MOD_DEVICE_WEBSERVER_PORT'] = os.environ.get("MOD_DEVICE_WEBSERVER_PORT", '18181') os.environ['MOD_HARDWARE_DESC_FILE'] = os.path.join(resdir, 'mod-hardware-descriptor.json') os.environ['MOD_HTML_DIR'] = os.path.join(resdir, 'html') os.environ['MOD_IMAGE_VERSION_PATH'] = os.path.join(resdir, 'VERSION') diff --git a/utils/debug/jackd b/utils/debug/jackd index 555e80a..8c345e3 100755 --- a/utils/debug/jackd +++ b/utils/debug/jackd @@ -70,6 +70,8 @@ else fi export LV2_PATH +export MOD_DESKTOP=1 +export MOD_DEVICE_HOST_PORT=18182 export MOD_KEYS_PATH="$(convert_path "${DOCS_DIR}/MOD Desktop/keys/")" export MOD_USER_FILES_DIR="$(convert_path "${DOCS_DIR}/MOD Desktop/user-files")" diff --git a/utils/jack/jack-session-alsamidi.conf b/utils/jack/jack-session-alsamidi.conf index 43e6b19..c57309c 100644 --- a/utils/jack/jack-session-alsamidi.conf +++ b/utils/jack/jack-session-alsamidi.conf @@ -1,4 +1,4 @@ l system_midi alsa_midi l mod-midi-merger mod-midi-merger l mod-midi-broadcaster mod-midi-broadcaster -l mod-host mod-host 18182 +l mod-host mod-host diff --git a/utils/jack/jack-session.conf b/utils/jack/jack-session.conf index 76ac792..c37e69f 100644 --- a/utils/jack/jack-session.conf +++ b/utils/jack/jack-session.conf @@ -1,3 +1,3 @@ l mod-midi-merger mod-midi-merger l mod-midi-broadcaster mod-midi-broadcaster -l mod-host mod-host 18182 +l mod-host mod-host diff --git a/utils/macos/entitlements.plist b/utils/macos/entitlements.plist index f00fbb5..62962f6 100644 --- a/utils/macos/entitlements.plist +++ b/utils/macos/entitlements.plist @@ -6,5 +6,7 @@ com.apple.security.cs.allow-unsigned-executable-memory + com.apple.security.device.audio-input +