diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 40ab7256b..bd2270362 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -65,11 +65,12 @@ jobs:
sudo dpkg --add-architecture i386
sudo apt-get update -qq
sudo apt-get install -yq libasound2-dev libfluidsynth-dev libgl1-mesa-dev liblo-dev libmagic-dev libpulse-dev libqt4-dev libsndfile1-dev libx11-dev libxcursor-dev libxext-dev libxrandr-dev pkg-config pyqt5-dev-tools qtbase5-dev
- sudo apt-get install -yq g++-multilib libfreetype6:i386 libx11-6:i386 libxext6:i386
+ sudo apt-get install -yq g++-multilib libfreetype6:i386 libfontconfig1:i386 libx11-6:i386 libxext6:i386
# Fix 32bit bridge build
sudo ln -s /usr/lib/i386-linux-gnu/libX11.so.6 /usr/lib/i386-linux-gnu/libX11.so
sudo ln -s /usr/lib/i386-linux-gnu/libXext.so.6 /usr/lib/i386-linux-gnu/libXext.so
sudo ln -s /usr/lib/i386-linux-gnu/libfreetype.so.6 /usr/lib/i386-linux-gnu/libfreetype.so
+ sudo ln -s /usr/lib/i386-linux-gnu/libfontconfig.so.1 /usr/lib/i386-linux-gnu/libfontconfig.so
- name: make features
run: make features
- name: make
@@ -91,11 +92,12 @@ jobs:
sudo dpkg --add-architecture i386
sudo apt-get update -qq
sudo apt-get install -yq libasound2-dev libfluidsynth-dev libgl1-mesa-dev liblo-dev libmagic-dev libpulse-dev libsndfile1-dev libx11-dev libxcursor-dev libxext-dev libxrandr-dev pkg-config pyqt5-dev-tools qtbase5-dev
- sudo apt-get install -yq g++-multilib libfreetype6:i386 libx11-6:i386 libxext6:i386
+ sudo apt-get install -yq g++-multilib libfreetype6:i386 libfontconfig1:i386 libx11-6:i386 libxext6:i386
# Fix 32bit bridge build
sudo ln -s /usr/lib/i386-linux-gnu/libX11.so.6 /usr/lib/i386-linux-gnu/libX11.so
sudo ln -s /usr/lib/i386-linux-gnu/libXext.so.6 /usr/lib/i386-linux-gnu/libXext.so
sudo ln -s /usr/lib/i386-linux-gnu/libfreetype.so.6 /usr/lib/i386-linux-gnu/libfreetype.so
+ sudo ln -s /usr/lib/i386-linux-gnu/libfontconfig.so.1 /usr/lib/i386-linux-gnu/libfontconfig.so
- name: make features
run: make features
- name: make
diff --git a/Makefile b/Makefile
index 1414106b5..8bcb1f25d 100644
--- a/Makefile
+++ b/Makefile
@@ -88,6 +88,8 @@ ifeq ($(USING_RTAUDIO),true)
3RD_LIBS += $(MODULEDIR)/rtmidi.a
endif
+3RD_LIBS += $(MODULEDIR)/ysfx.a
+
ALL_LIBS += $(3RD_LIBS)
3rd: $(3RD_LIBS)
diff --git a/data/carla-single b/data/carla-single
index 6d25d9c57..d5bfdca81 100755
--- a/data/carla-single
+++ b/data/carla-single
@@ -50,6 +50,7 @@ Possible formats:
- lv2
- vst|vst2
- vst3
+ - jsfx
- sf2
- sfz
@@ -97,7 +98,7 @@ if len(sys.argv) == arg:
# --------------------------------------------------------------------------------------------------------
# Set format
-if sys.argv[arg] in ("internal", "ladspa", "dssi", "lv2", "vst", "vst2", "vst3", "au", "sf2", "sfz"):
+if sys.argv[arg] in ("internal", "ladspa", "dssi", "lv2", "vst", "vst2", "vst3", "au", "jsfx", "sf2", "sfz"):
FORMAT = sys.argv[arg]
arg += 1
@@ -193,7 +194,7 @@ if ARCH not in ("native", "posix32", "posix64", "win32", "win64"):
print("Invalid arch")
sys.exit(1)
-if FORMAT not in ("internal", "ladspa", "dssi", "lv2", "vst2", "vst3", "au", "sf2", "sfz"):
+if FORMAT not in ("internal", "ladspa", "dssi", "lv2", "vst2", "vst3", "au", "jsfx", "sf2", "sfz"):
print("Invalid format")
sys.exit(1)
diff --git a/resources/ui/carla_database.ui b/resources/ui/carla_database.ui
index 0fe170c11..782759c80 100644
--- a/resources/ui/carla_database.ui
+++ b/resources/ui/carla_database.ui
@@ -692,6 +692,13 @@
+ -
+
+
+ JSFX
+
+
+
-
diff --git a/resources/ui/carla_refresh.ui b/resources/ui/carla_refresh.ui
index 979eb3981..4c2dbe2db 100644
--- a/resources/ui/carla_refresh.ui
+++ b/resources/ui/carla_refresh.ui
@@ -85,6 +85,13 @@
+ -
+
+
+ JSFX
+
+
+
-
diff --git a/resources/ui/carla_settings.ui b/resources/ui/carla_settings.ui
index 15f52ee03..e499be796 100644
--- a/resources/ui/carla_settings.ui
+++ b/resources/ui/carla_settings.ui
@@ -2154,6 +2154,11 @@
SFZ
+ -
+
+ JSFX
+
+
-
@@ -2358,6 +2363,28 @@
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+
+
-
diff --git a/source/Makefile.deps.mk b/source/Makefile.deps.mk
index 13a3257cc..7c0824fe1 100644
--- a/source/Makefile.deps.mk
+++ b/source/Makefile.deps.mk
@@ -509,6 +509,14 @@ JUCE_GUI_BASICS_LIBS = -lgdi32 -limm32 -lcomdlg32 -lole32
endif # USING_JUCE
endif # WIN32
+YSFX_FLAGS = -I$(CWD)/modules/ysfx/include
+ifeq ($(MACOS),true)
+YSFX_GRAPHICS_LIBS = -framework Cocoa -framework Carbon -framework Metal -framework Foundation
+else ifneq ($(WIN32),true)
+YSFX_GRAPHICS_FLAGS = $(shell $(PKG_CONFIG) $(PKG_CONFIG_FLAGS) --cflags freetype2 fontconfig)
+YSFX_GRAPHICS_LIBS = $(shell $(PKG_CONFIG) $(PKG_CONFIG_FLAGS) --libs freetype2 fontconfig)
+endif
+
# ---------------------------------------------------------------------------------------------------------------------
ifeq ($(STATIC_PLUGIN_TARGET),true)
diff --git a/source/backend/CarlaPlugin.hpp b/source/backend/CarlaPlugin.hpp
index 4a09c45e8..8b398cec1 100644
--- a/source/backend/CarlaPlugin.hpp
+++ b/source/backend/CarlaPlugin.hpp
@@ -978,6 +978,7 @@ public:
static CarlaPluginPtr newVST2(const Initializer& init);
static CarlaPluginPtr newVST3(const Initializer& init);
static CarlaPluginPtr newAU(const Initializer& init);
+ static CarlaPluginPtr newJSFX(const Initializer& init);
static CarlaPluginPtr newJuce(const Initializer& init, const char* format);
static CarlaPluginPtr newFluidSynth(const Initializer& init, PluginType ptype, bool use16Outs);
diff --git a/source/backend/Makefile b/source/backend/Makefile
index a027d179b..e50086a72 100644
--- a/source/backend/Makefile
+++ b/source/backend/Makefile
@@ -64,6 +64,8 @@ STANDALONE_LIBS += $(MODULEDIR)/rtaudio.a
STANDALONE_LIBS += $(MODULEDIR)/rtmidi.a
endif
+STANDALONE_LIBS += $(MODULEDIR)/ysfx.a
+
# ---------------------------------------------------------------------------------------------------------------------
STANDALONE_LINK_FLAGS = $(HYLIA_LIBS)
@@ -97,6 +99,8 @@ STANDALONE_LINK_FLAGS += $(RTAUDIO_LIBS)
STANDALONE_LINK_FLAGS += $(RTMIDI_LIBS)
endif
+STANDALONE_LINK_FLAGS += $(YSFX_GRAPHICS_LIBS)
+
ifeq ($(JACKBRIDGE_DIRECT),true)
STANDALONE_LINK_FLAGS += $(JACK_LIBS)
endif
diff --git a/source/backend/engine/CarlaEngine.cpp b/source/backend/engine/CarlaEngine.cpp
index ff57e5264..467d56c75 100644
--- a/source/backend/engine/CarlaEngine.cpp
+++ b/source/backend/engine/CarlaEngine.cpp
@@ -596,6 +596,7 @@ bool CarlaEngine::addPlugin(const BinaryType btype,
&& ptype != PLUGIN_GIG
&& ptype != PLUGIN_SF2
&& ptype != PLUGIN_SFZ
+ && ptype != PLUGIN_JSFX
&& ptype != PLUGIN_JACK;
// Prefer bridges for some specific plugins
@@ -705,10 +706,6 @@ bool CarlaEngine::addPlugin(const BinaryType btype,
plugin = CarlaPlugin::newAU(initializer);
break;
- case PLUGIN_JSFX:
- setLastError("Not implemented yet");
- break;
-
#ifndef BUILD_BRIDGE_ALTERNATIVE_ARCH
case PLUGIN_INTERNAL:
plugin = CarlaPlugin::newNative(initializer);
@@ -741,6 +738,10 @@ bool CarlaEngine::addPlugin(const BinaryType btype,
# endif
break;
+ case PLUGIN_JSFX:
+ plugin = CarlaPlugin::newJSFX(initializer);
+ break;
+
case PLUGIN_JACK:
# ifndef STATIC_PLUGIN_TARGET
plugin = CarlaPlugin::newJackApp(initializer);
@@ -754,6 +755,7 @@ bool CarlaEngine::addPlugin(const BinaryType btype,
case PLUGIN_GIG:
case PLUGIN_SF2:
case PLUGIN_SFZ:
+ case PLUGIN_JSFX:
case PLUGIN_JACK:
setLastError("Plugin bridges cannot handle this binary");
break;
diff --git a/source/backend/plugin/CarlaPluginJSFX.cpp b/source/backend/plugin/CarlaPluginJSFX.cpp
new file mode 100644
index 000000000..6d4919f48
--- /dev/null
+++ b/source/backend/plugin/CarlaPluginJSFX.cpp
@@ -0,0 +1,1072 @@
+/*
+ * Carla JSFX Plugin
+ * Copyright (C) 2021 Filipe Coelho
+ *
+ * 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 2 of
+ * the License, or 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.
+ *
+ * For a full copy of the GNU General Public License see the doc/GPL.txt file.
+ */
+
+// TODO(jsfx) graphics section
+
+#include "CarlaPluginInternal.hpp"
+#include "CarlaEngine.hpp"
+#include "CarlaJsfxUtils.hpp"
+#include "CarlaBackendUtils.hpp"
+#include "CarlaUtils.hpp"
+
+#include "water/files/File.h"
+#include "water/text/StringArray.h"
+
+#include
+#include
+#include
+
+using water::CharPointer_UTF8;
+using water::File;
+using water::String;
+using water::StringArray;
+
+CARLA_BACKEND_START_NAMESPACE
+
+// -------------------------------------------------------------------------------------------------------------------
+// Fallback data
+
+static const ExternalMidiNote kExternalMidiNoteFallback = { -1, 0, 0 };
+
+// -------------------------------------------------------------------------------------------------------------------
+
+class CarlaPluginJSFX : public CarlaPlugin
+{
+public:
+ CarlaPluginJSFX(CarlaEngine* const engine, const uint id) noexcept
+ : CarlaPlugin(engine, id),
+ fMapOfSliderToParameter(ysfx_max_sliders, -1)
+ {
+ carla_debug("CarlaPluginJSFX::CarlaPluginJSFX(%p, %i)", engine, id);
+ }
+
+ ~CarlaPluginJSFX()
+ {
+ carla_debug("CarlaPluginJSFX::~CarlaPluginJSFX()");
+
+ pData->singleMutex.lock();
+ pData->masterMutex.lock();
+
+ if (pData->client != nullptr && pData->client->isActive())
+ pData->client->deactivate(true);
+
+ if (pData->active)
+ {
+ deactivate();
+ pData->active = false;
+ }
+
+ clearBuffers();
+ }
+
+ // -------------------------------------------------------------------
+ // Information (base)
+
+ PluginType getType() const noexcept override
+ {
+ return PLUGIN_JSFX;
+ }
+
+ PluginCategory getCategory() const noexcept override
+ {
+ CARLA_SAFE_ASSERT_RETURN(fEffect != nullptr, CarlaPlugin::getCategory());
+
+ return CarlaJsfxCategories::getFromEffect(fEffect.get());
+ }
+
+ uint32_t getLatencyInFrames() const noexcept override
+ {
+ CARLA_SAFE_ASSERT_RETURN(fEffect != nullptr, 0);
+
+ ysfx_real sampleRate = ysfx_get_sample_rate(fEffect.get());
+ ysfx_real latencyInSeconds = ysfx_get_pdc_delay(fEffect.get());
+
+ //NOTE: `pdc_bot_ch` and `pdc_top_ch` channel range ignored
+
+ int32_t latencyInFrames = water::roundToInt(latencyInSeconds * sampleRate);
+ wassert(latencyInFrames >= 0);
+
+ return (uint32_t)latencyInFrames;
+ }
+
+ // -------------------------------------------------------------------
+ // Information (count)
+
+ uint32_t getMidiInCount() const noexcept override
+ {
+ return 1;
+ }
+
+ uint32_t getMidiOutCount() const noexcept override
+ {
+ return 1;
+ }
+
+ uint32_t getParameterScalePointCount(const uint32_t parameterId) const noexcept override
+ {
+ CARLA_SAFE_ASSERT_RETURN(parameterId < pData->param.count, 0);
+
+ int32_t rindex = pData->param.data[parameterId].rindex;
+ return ysfx_slider_get_enum_names(fEffect.get(), (uint32_t)rindex, nullptr, 0);;
+ }
+
+ // -------------------------------------------------------------------
+ // Information (current data)
+
+ std::size_t getChunkData(void** dataPtr) noexcept override
+ {
+ CARLA_SAFE_ASSERT_RETURN(pData->options & PLUGIN_OPTION_USE_CHUNKS, 0);
+ CARLA_SAFE_ASSERT_RETURN(dataPtr != nullptr, 0);
+
+ ysfx_state_u state(ysfx_save_state(fEffect.get()));
+ CARLA_SAFE_ASSERT_RETURN(state, 0);
+
+ fChunkText = CarlaJsfxState::convertToString(*state);
+
+ *dataPtr = (void*)fChunkText.toRawUTF8();
+ return (std::size_t)fChunkText.getNumBytesAsUTF8();
+ }
+
+ // -------------------------------------------------------------------
+ // Information (per-plugin data)
+
+ uint getOptionsAvailable() const noexcept override
+ {
+ uint options = 0x0;
+
+ options |= PLUGIN_OPTION_USE_CHUNKS;
+
+ options |= PLUGIN_OPTION_SEND_CONTROL_CHANGES;
+ options |= PLUGIN_OPTION_SEND_CHANNEL_PRESSURE;
+ options |= PLUGIN_OPTION_SEND_NOTE_AFTERTOUCH;
+ options |= PLUGIN_OPTION_SEND_PITCHBEND;
+ options |= PLUGIN_OPTION_SEND_ALL_SOUND_OFF;
+ options |= PLUGIN_OPTION_SEND_PROGRAM_CHANGES;
+ options |= PLUGIN_OPTION_SKIP_SENDING_NOTES;
+
+ return options;
+ }
+
+ float getParameterValue(const uint32_t parameterId) const noexcept override
+ {
+ CARLA_SAFE_ASSERT_RETURN(parameterId < pData->param.count, 0.0f);
+
+ int32_t rindex = pData->param.data[parameterId].rindex;
+ return ysfx_slider_get_value(fEffect.get(), (uint32_t)rindex);
+ }
+
+ bool getParameterName(const uint32_t parameterId, char* const strBuf) const noexcept override
+ {
+ CARLA_SAFE_ASSERT_RETURN(fEffect != nullptr, false);
+ CARLA_SAFE_ASSERT_RETURN(parameterId < pData->param.count, false);
+
+ int32_t rindex = pData->param.data[parameterId].rindex;
+ const char *name = ysfx_slider_get_name(fEffect.get(), (uint32_t)rindex);
+ std::snprintf(strBuf, STR_MAX, "%s", name);
+ return true;
+ }
+
+
+ bool getParameterText(const uint32_t parameterId, char* const strBuf) noexcept override
+ {
+ CARLA_SAFE_ASSERT_RETURN(fEffect != nullptr, false);
+ CARLA_SAFE_ASSERT_RETURN(parameterId < pData->param.count, false);
+
+ int32_t rindex = pData->param.data[parameterId].rindex;
+ float value = ysfx_slider_get_value(fEffect.get(), (uint32_t)rindex);
+
+ int32_t enumIndex = -1;
+ if (ysfx_slider_is_enum(fEffect.get(), (uint32_t)rindex))
+ {
+ uint32_t enumCount = ysfx_slider_get_enum_names(fEffect.get(), (uint32_t)rindex, nullptr, 0);
+ if ((int32_t)value >= 0 && (uint32_t)value < enumCount)
+ enumIndex = (int32_t)value;
+ }
+
+ if (enumIndex != -1)
+ std::snprintf(strBuf, STR_MAX, "%s", ysfx_slider_get_name(fEffect.get(), (uint32_t)enumIndex));
+ else
+ std::snprintf(strBuf, STR_MAX, "%.12g", value);
+
+ return true;
+ }
+
+ float getParameterScalePointValue(const uint32_t parameterId, const uint32_t scalePointId) const noexcept override
+ {
+ CARLA_SAFE_ASSERT_RETURN(parameterId < getParameterCount(), 0.0f);
+ CARLA_SAFE_ASSERT_RETURN(scalePointId < getParameterScalePointCount(parameterId), 0.0f);
+ return (float)scalePointId;
+ }
+
+ bool getParameterScalePointLabel(const uint32_t parameterId, const uint32_t scalePointId, char* const strBuf) const noexcept override
+ {
+ CARLA_SAFE_ASSERT_RETURN(parameterId < getParameterCount(), false);
+
+ int32_t rindex = pData->param.data[parameterId].rindex;
+
+ uint32_t enumCount = ysfx_slider_get_enum_names(fEffect.get(), (uint32_t)rindex, nullptr, 0);
+ CARLA_SAFE_ASSERT_RETURN(scalePointId < enumCount, false);
+
+ std::snprintf(strBuf, STR_MAX, "%s", ysfx_slider_get_enum_name(fEffect.get(), (uint32_t)rindex, scalePointId));
+ return true;
+ }
+
+ bool getLabel(char* const strBuf) const noexcept override
+ {
+ std::strncpy(strBuf, fUnit.getFileId().toRawUTF8(), STR_MAX);
+ return true;
+ }
+
+ // -------------------------------------------------------------------
+ // Set data (plugin-specific stuff)
+
+ void setParameterValue(const uint32_t parameterId, const float value, const bool sendGui, const bool sendOsc, const bool sendCallback) noexcept override
+ {
+ CARLA_SAFE_ASSERT_RETURN(fEffect != nullptr,);
+ CARLA_SAFE_ASSERT_RETURN(parameterId < pData->param.count,);
+
+ int32_t rindex = pData->param.data[parameterId].rindex;
+ ysfx_slider_set_value(fEffect.get(), rindex, value);
+
+ CarlaPlugin::setParameterValue(parameterId, value, sendGui, sendOsc, sendCallback);
+ }
+
+ void setParameterValueRT(const uint32_t parameterId, const float value, const bool sendCallbackLater) noexcept override
+ {
+ CARLA_SAFE_ASSERT_RETURN(fEffect != nullptr,);
+ CARLA_SAFE_ASSERT_RETURN(parameterId < pData->param.count,);
+
+ int32_t rindex = pData->param.data[parameterId].rindex;
+ ysfx_slider_set_value(fEffect.get(), rindex, value);
+
+ CarlaPlugin::setParameterValueRT(parameterId, value, sendCallbackLater);
+ }
+
+ void setChunkData(const void* data, std::size_t dataSize) override
+ {
+ CARLA_SAFE_ASSERT_RETURN(pData->options & PLUGIN_OPTION_USE_CHUNKS,);
+
+ water::String dataText(water::CharPointer_UTF8((const char*)data), dataSize);
+
+ ysfx_state_u state(CarlaJsfxState::convertFromString(dataText));
+ CARLA_SAFE_ASSERT_RETURN(state,);
+ CARLA_SAFE_ASSERT_RETURN(ysfx_load_state(fEffect.get(), state.get()),);
+ }
+
+ // -------------------------------------------------------------------
+ // Plugin state
+
+ void reload() override
+ {
+ CARLA_SAFE_ASSERT_RETURN(pData->engine != nullptr,);
+ CARLA_SAFE_ASSERT_RETURN(fEffect != nullptr,);
+ carla_debug("CarlaPluginJSFX::reload()");
+
+ const EngineProcessMode processMode(pData->engine->getProccessMode());
+
+ // Safely disable plugin for reload
+ const ScopedDisabler sd(this);
+
+ if (pData->active)
+ deactivate();
+
+ clearBuffers();
+
+ // ---------------------------------------------------------------
+
+ // initialize the block size and sample rate
+ // loading the chunk can invoke @slider which makes computations based on these
+ ysfx_set_sample_rate(fEffect.get(), pData->engine->getSampleRate());
+ ysfx_set_block_size(fEffect.get(), (uint32_t)pData->engine->getBufferSize());
+ ysfx_init(fEffect.get());
+
+ const uint32_t aIns = ysfx_get_num_inputs(fEffect.get());
+ const uint32_t aOuts = ysfx_get_num_outputs(fEffect.get());
+
+ // perhaps we obtained a latency value from @init
+ pData->client->setLatency(getLatencyInFrames());
+
+ if (aIns > 0)
+ {
+ pData->audioIn.createNew(aIns);
+ }
+
+ if (aOuts > 0)
+ {
+ pData->audioOut.createNew(aOuts);
+ }
+
+ // count the sliders and establish the mappings between parameter and slider
+ uint32_t params = 0;
+ uint32_t mapOfParameterToSlider[ysfx_max_sliders];
+ for (uint32_t rindex = 0; rindex < ysfx_max_sliders; ++rindex)
+ {
+ if (ysfx_slider_exists(fEffect.get(), rindex))
+ {
+ mapOfParameterToSlider[params] = rindex;
+ fMapOfSliderToParameter[rindex] = (int32_t)params;
+ ++params;
+ }
+ else
+ {
+ fMapOfSliderToParameter[rindex] = -1;
+ }
+ }
+
+ if (params > 0)
+ {
+ pData->param.createNew(params, false);
+ }
+
+ const uint portNameSize(pData->engine->getMaxPortNameSize());
+ CarlaString portName;
+
+ // Audio Ins
+ for (uint32_t j = 0; j < aIns; ++j)
+ {
+ portName.clear();
+
+ if (processMode == ENGINE_PROCESS_MODE_SINGLE_CLIENT)
+ {
+ portName = pData->name;
+ portName += ":";
+ }
+
+ const char* const inputName = ysfx_get_input_name(fEffect.get(), j);
+ if (inputName && inputName[0])
+ {
+ portName += inputName;
+ }
+ else if (aIns > 1)
+ {
+ portName += "input_";
+ portName += CarlaString(j+1);
+ }
+ else
+ portName += "input";
+
+ portName.truncate(portNameSize);
+
+ pData->audioIn.ports[j].port = (CarlaEngineAudioPort*)pData->client->addPort(kEnginePortTypeAudio, portName, true, j);
+ pData->audioIn.ports[j].rindex = j;
+ }
+
+ // Audio Outs
+ for (uint32_t j = 0; j < aOuts; ++j)
+ {
+ portName.clear();
+
+ if (processMode == ENGINE_PROCESS_MODE_SINGLE_CLIENT)
+ {
+ portName = pData->name;
+ portName += ":";
+ }
+
+ const char* const outputName = ysfx_get_input_name(fEffect.get(), j);
+ if (outputName && outputName[0])
+ {
+ portName += outputName;
+ }
+ else if (aOuts > 1)
+ {
+ portName += "output_";
+ portName += CarlaString(j+1);
+ }
+ else
+ portName += "output";
+
+ portName.truncate(portNameSize);
+
+ pData->audioOut.ports[j].port = (CarlaEngineAudioPort*)pData->client->addPort(kEnginePortTypeAudio, portName, false, j);
+ pData->audioOut.ports[j].rindex = j;
+ }
+
+ // Parameters
+ for (uint32_t j = 0; j < params; ++j)
+ {
+ const uint32_t rindex = mapOfParameterToSlider[j];
+ pData->param.data[j].type = PARAMETER_INPUT;
+ pData->param.data[j].index = (int32_t)j;
+ pData->param.data[j].rindex = (int32_t)rindex;
+
+ ysfx_slider_range_t range = {};
+ ysfx_slider_get_range(fEffect.get(), rindex, &range);
+
+ float min = (float)range.min;
+ float max = (float)range.max;
+ float def = (float)range.def;
+ float step = (float)range.inc;
+
+ // only use values as integer if we have a proper range
+ bool isEnum = ysfx_slider_is_enum(fEffect.get(), rindex) &&
+ min == 0.0f && max >= 0.0f &&
+ max + 1.0f == (float)ysfx_slider_get_enum_names(fEffect.get(), rindex, nullptr, 0);
+
+ // NOTE: in case of incomplete slider specification without ;
+ // these are usually output-only sliders.
+ if (min == max)
+ {
+ // replace with a dummy range
+ min = 0.0f;
+ max = 1.0f;
+ }
+
+ if (min > max)
+ std::swap(min, max);
+
+ if (def < min)
+ def = min;
+ else if (def > max)
+ def = max;
+
+ float stepSmall;
+ float stepLarge;
+ if (isEnum)
+ {
+ step = 1.0f;
+ stepSmall = 1.0f;
+ stepLarge = 10.0f;
+ }
+ else
+ {
+ stepSmall = step/10.0f;
+ stepLarge = step*10.0f;
+ }
+
+ pData->param.data[j].hints |= PARAMETER_IS_ENABLED;
+
+ if (isEnum)
+ {
+ pData->param.data[j].hints |= PARAMETER_IS_INTEGER;
+ pData->param.data[j].hints |= PARAMETER_USES_SCALEPOINTS;
+ pData->param.data[j].hints |= PARAMETER_USES_CUSTOM_TEXT;
+ }
+ else
+ {
+ pData->param.data[j].hints |= PARAMETER_CAN_BE_CV_CONTROLLED;
+ }
+
+ pData->param.ranges[j].min = min;
+ pData->param.ranges[j].max = max;
+ pData->param.ranges[j].def = def;
+ pData->param.ranges[j].step = step;
+ pData->param.ranges[j].stepSmall = stepSmall;
+ pData->param.ranges[j].stepLarge = stepLarge;
+ }
+
+ //if (needsCtrlIn)
+ {
+ portName.clear();
+
+ if (processMode == ENGINE_PROCESS_MODE_SINGLE_CLIENT)
+ {
+ portName = pData->name;
+ portName += ":";
+ }
+
+ portName += "events-in";
+ portName.truncate(portNameSize);
+
+ pData->event.portIn = (CarlaEngineEventPort*)pData->client->addPort(kEnginePortTypeEvent, portName, true, 0);
+ }
+
+ //if (needsCtrlOut)
+ {
+ portName.clear();
+
+ if (processMode == ENGINE_PROCESS_MODE_SINGLE_CLIENT)
+ {
+ portName = pData->name;
+ portName += ":";
+ }
+
+ portName += "events-out";
+ portName.truncate(portNameSize);
+
+ pData->event.portOut = (CarlaEngineEventPort*)pData->client->addPort(kEnginePortTypeEvent, portName, false, 0);
+ }
+ }
+
+ // -------------------------------------------------------------------
+ // Plugin processing
+
+ void activate() noexcept override
+ {
+ CARLA_SAFE_ASSERT_RETURN(fEffect,);
+
+ ysfx_set_sample_rate(fEffect.get(), pData->engine->getSampleRate());
+ ysfx_set_block_size(fEffect.get(), (uint32_t)pData->engine->getBufferSize());
+ ysfx_init(fEffect.get());
+
+ fTransportValues.tempo = 120;
+ fTransportValues.playback_state = ysfx_playback_paused;
+ fTransportValues.time_position = 0;
+ fTransportValues.beat_position = 0;
+ fTransportValues.time_signature[0] = 4;
+ fTransportValues.time_signature[1] = 4;
+ }
+
+ void process(const float* const* const audioIn, float** const audioOut,
+ const float* const* const, float**,
+ const uint32_t frames) override
+ {
+ CARLA_SAFE_ASSERT_RETURN(fEffect,);
+
+ // --------------------------------------------------------------------------------------------------------
+ // Set TimeInfo
+
+ const EngineTimeInfo timeInfo = pData->engine->getTimeInfo();
+ const EngineTimeInfoBBT& bbt = timeInfo.bbt;
+
+ fTransportValues.playback_state = timeInfo.playing ?
+ ysfx_playback_playing : ysfx_playback_paused;
+ fTransportValues.time_position = 1e-6*double(timeInfo.usecs);
+
+ if (bbt.valid)
+ {
+ const double samplePos = double(timeInfo.frame);
+ const double sampleRate = pData->engine->getSampleRate();
+ fTransportValues.tempo = bbt.beatsPerMinute;
+ fTransportValues.beat_position = samplePos / (sampleRate * 60 / bbt.beatsPerMinute);
+ fTransportValues.time_signature[0] = (uint32_t)bbt.beatsPerBar;
+ fTransportValues.time_signature[1] = (uint32_t)bbt.beatType;
+ }
+
+ ysfx_set_time_info(fEffect.get(), &fTransportValues);
+
+ // --------------------------------------------------------------------------------------------------------
+ // Event Input and Processing
+
+ if (pData->event.portIn != nullptr)
+ {
+ // ----------------------------------------------------------------------------------------------------
+ // MIDI Input (External)
+
+ if (pData->extNotes.mutex.tryLock())
+ {
+ for (RtLinkedList::Itenerator it = pData->extNotes.data.begin2(); it.valid(); it.next())
+ {
+ const ExternalMidiNote& note(it.getValue(kExternalMidiNoteFallback));
+ CARLA_SAFE_ASSERT_CONTINUE(note.channel >= 0 && note.channel < MAX_MIDI_CHANNELS);
+
+ uint8_t midiData[3];
+ midiData[0] = uint8_t((note.velo > 0 ? MIDI_STATUS_NOTE_ON : MIDI_STATUS_NOTE_OFF) | (note.channel & MIDI_CHANNEL_BIT));
+ midiData[1] = note.note;
+ midiData[2] = note.velo;
+
+ ysfx_midi_event_t event;
+ event.bus = 0;
+ event.offset = 0;
+ event.size = 3;
+ event.data = midiData;
+ ysfx_send_midi(fEffect.get(), &event);
+ }
+
+ pData->extNotes.data.clear();
+ pData->extNotes.mutex.unlock();
+
+ } // End of MIDI Input (External)
+
+ // ----------------------------------------------------------------------------------------------------
+ // Event Input (System)
+
+#ifndef BUILD_BRIDGE_ALTERNATIVE_ARCH
+ bool allNotesOffSent = false;
+#endif
+
+ for (uint32_t i=0, numEvents=pData->event.portIn->getEventCount(); i < numEvents; ++i)
+ {
+ EngineEvent& event(pData->event.portIn->getEvent(i));
+
+ if (event.time >= frames)
+ continue;
+
+ auto addInputEvent = [this]
+ (uint32_t offset, const uint8_t *data, uint32_t size)
+ {
+ ysfx_midi_event_t event;
+ event.bus = 0;
+ event.offset = offset;
+ event.size = size;
+ event.data = data;
+ ysfx_send_midi(fEffect.get(), &event);
+ };
+
+ switch (event.type)
+ {
+ case kEngineEventTypeNull:
+ break;
+
+ case kEngineEventTypeControl: {
+ EngineControlEvent& ctrlEvent(event.ctrl);
+
+ switch (ctrlEvent.type)
+ {
+ case kEngineControlEventTypeNull:
+ break;
+
+ case kEngineControlEventTypeParameter: {
+ float value;
+
+#ifndef BUILD_BRIDGE_ALTERNATIVE_ARCH
+ // non-midi
+ if (event.channel == kEngineEventNonMidiChannel)
+ {
+ const uint32_t k = ctrlEvent.param;
+ CARLA_SAFE_ASSERT_CONTINUE(k < pData->param.count);
+
+ ctrlEvent.handled = true;
+ value = pData->param.getFinalUnnormalizedValue(k, ctrlEvent.normalizedValue);
+ setParameterValueRT(k, value, true);
+ continue;
+ }
+
+ // Control backend stuff
+ if (event.channel == pData->ctrlChannel)
+ {
+ if (MIDI_IS_CONTROL_BREATH_CONTROLLER(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_DRYWET) != 0)
+ {
+ ctrlEvent.handled = true;
+ value = ctrlEvent.normalizedValue;
+ setDryWetRT(value, true);
+ }
+ else if (MIDI_IS_CONTROL_CHANNEL_VOLUME(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_VOLUME) != 0)
+ {
+ ctrlEvent.handled = true;
+ value = ctrlEvent.normalizedValue*127.0f/100.0f;
+ setVolumeRT(value, true);
+ }
+ else if (MIDI_IS_CONTROL_BALANCE(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_BALANCE) != 0)
+ {
+ float left, right;
+ value = ctrlEvent.normalizedValue/0.5f - 1.0f;
+
+ if (value < 0.0f)
+ {
+ left = -1.0f;
+ right = (value*2.0f)+1.0f;
+ }
+ else if (value > 0.0f)
+ {
+ left = (value*2.0f)-1.0f;
+ right = 1.0f;
+ }
+ else
+ {
+ left = -1.0f;
+ right = 1.0f;
+ }
+
+ ctrlEvent.handled = true;
+ setBalanceLeftRT(left, true);
+ setBalanceRightRT(right, true);
+ }
+ }
+#endif
+ // Control plugin parameters
+ uint32_t k;
+ for (k=0; k < pData->param.count; ++k)
+ {
+ if (pData->param.data[k].midiChannel != event.channel)
+ continue;
+ if (pData->param.data[k].mappedControlIndex != ctrlEvent.param)
+ continue;
+ if (pData->param.data[k].type != PARAMETER_INPUT)
+ continue;
+ if ((pData->param.data[k].hints & PARAMETER_IS_AUTOMABLE) == 0)
+ continue;
+
+ ctrlEvent.handled = true;
+ value = pData->param.getFinalUnnormalizedValue(k, ctrlEvent.normalizedValue);
+ setParameterValueRT(k, value, true);
+ }
+
+ if ((pData->options & PLUGIN_OPTION_SEND_CONTROL_CHANGES) != 0 && ctrlEvent.param < MAX_MIDI_VALUE)
+ {
+ uint8_t midiData[3];
+ midiData[0] = uint8_t(MIDI_STATUS_CONTROL_CHANGE | (event.channel & MIDI_CHANNEL_BIT));
+ midiData[1] = uint8_t(ctrlEvent.param);
+ midiData[2] = uint8_t(ctrlEvent.normalizedValue*127.0f);
+
+ addInputEvent(event.time, midiData, 3);
+ }
+
+#ifndef BUILD_BRIDGE_ALTERNATIVE_ARCH
+ if (! ctrlEvent.handled)
+ checkForMidiLearn(event);
+#endif
+ break;
+ } // case kEngineControlEventTypeParameter
+
+ case kEngineControlEventTypeMidiBank:
+ if ((pData->options & PLUGIN_OPTION_SEND_PROGRAM_CHANGES) != 0)
+ {
+ uint8_t midiData[3];
+ midiData[0] = uint8_t(MIDI_STATUS_CONTROL_CHANGE | (event.channel & MIDI_CHANNEL_BIT));
+ midiData[1] = MIDI_CONTROL_BANK_SELECT;
+ midiData[2] = 0;
+ addInputEvent(event.time, midiData, 3);
+
+ midiData[1] = MIDI_CONTROL_BANK_SELECT__LSB;
+ midiData[2] = uint8_t(ctrlEvent.normalizedValue*127.0f);
+ addInputEvent(event.time, midiData, 3);
+ }
+ break;
+
+ case kEngineControlEventTypeMidiProgram:
+ if (event.channel == pData->ctrlChannel && (pData->options & PLUGIN_OPTION_MAP_PROGRAM_CHANGES) != 0)
+ {
+ if (ctrlEvent.param < pData->prog.count)
+ {
+ setProgramRT(ctrlEvent.param, true);
+ }
+ }
+ else if ((pData->options & PLUGIN_OPTION_SEND_PROGRAM_CHANGES) != 0)
+ {
+ uint8_t midiData[3];
+ midiData[0] = uint8_t(MIDI_STATUS_PROGRAM_CHANGE | (event.channel & MIDI_CHANNEL_BIT));
+ midiData[1] = uint8_t(ctrlEvent.normalizedValue*127.0f);
+ addInputEvent(event.time, midiData, 2);
+ }
+ break;
+
+ case kEngineControlEventTypeAllSoundOff:
+ if (pData->options & PLUGIN_OPTION_SEND_ALL_SOUND_OFF)
+ {
+ uint8_t midiData[3];
+ midiData[0] = uint8_t(MIDI_STATUS_CONTROL_CHANGE | (event.channel & MIDI_CHANNEL_BIT));
+ midiData[1] = MIDI_CONTROL_ALL_SOUND_OFF;
+ midiData[2] = 0;
+
+ addInputEvent(event.time, midiData, 3);
+ }
+ break;
+
+ case kEngineControlEventTypeAllNotesOff:
+ if (pData->options & PLUGIN_OPTION_SEND_ALL_SOUND_OFF)
+ {
+#ifndef BUILD_BRIDGE_ALTERNATIVE_ARCH
+ if (event.channel == pData->ctrlChannel && ! allNotesOffSent)
+ {
+ allNotesOffSent = true;
+ postponeRtAllNotesOff();
+ }
+#endif
+
+ uint8_t midiData[3];
+ midiData[0] = uint8_t(MIDI_STATUS_CONTROL_CHANGE | (event.channel & MIDI_CHANNEL_BIT));
+ midiData[1] = MIDI_CONTROL_ALL_NOTES_OFF;
+ midiData[2] = 0;
+
+ addInputEvent(event.time, midiData, 3);
+ }
+ break;
+ } // switch (ctrlEvent.type)
+ break;
+ } // case kEngineEventTypeControl
+
+ case kEngineEventTypeMidi: {
+ const EngineMidiEvent& midiEvent(event.midi);
+
+ const uint8_t* const midiData(midiEvent.size > EngineMidiEvent::kDataSize ? midiEvent.dataExt : midiEvent.data);
+
+ uint8_t status = uint8_t(MIDI_GET_STATUS_FROM_DATA(midiData));
+
+ if ((status == MIDI_STATUS_NOTE_OFF || status == MIDI_STATUS_NOTE_ON) && (pData->options & PLUGIN_OPTION_SKIP_SENDING_NOTES))
+ continue;
+ if (status == MIDI_STATUS_CHANNEL_PRESSURE && (pData->options & PLUGIN_OPTION_SEND_CHANNEL_PRESSURE) == 0)
+ continue;
+ if (status == MIDI_STATUS_CONTROL_CHANGE && (pData->options & PLUGIN_OPTION_SEND_CONTROL_CHANGES) == 0)
+ continue;
+ if (status == MIDI_STATUS_POLYPHONIC_AFTERTOUCH && (pData->options & PLUGIN_OPTION_SEND_NOTE_AFTERTOUCH) == 0)
+ continue;
+ if (status == MIDI_STATUS_PITCH_WHEEL_CONTROL && (pData->options & PLUGIN_OPTION_SEND_PITCHBEND) == 0)
+ continue;
+
+ // Fix bad note-off
+ if (status == MIDI_STATUS_NOTE_ON && midiData[2] == 0)
+ status = MIDI_STATUS_NOTE_OFF;
+
+ // put back channel in data
+ uint8_t midiData2[midiEvent.size];
+ midiData2[0] = uint8_t(status | (event.channel & MIDI_CHANNEL_BIT));
+ std::memcpy(midiData2+1, midiData+1, static_cast(midiEvent.size-1));
+
+ addInputEvent(event.time, midiData2, midiEvent.size);
+
+ if (status == MIDI_STATUS_NOTE_ON)
+ {
+ pData->postponeNoteOnRtEvent(true, event.channel, midiData[1], midiData[2]);
+ }
+ else if (status == MIDI_STATUS_NOTE_OFF)
+ {
+ pData->postponeNoteOffRtEvent(true, event.channel, midiData[1]);
+ }
+ } break;
+ } // switch (event.type)
+ }
+
+ pData->postRtEvents.trySplice();
+
+ } // End of Event Input and Processing
+
+ // --------------------------------------------------------------------------------------------------------
+ // Plugin processing
+
+ const uint32_t numInputs = ysfx_get_num_inputs(fEffect.get());
+ const uint32_t numOutputs = ysfx_get_num_outputs(fEffect.get());
+ ysfx_process_float(fEffect.get(), audioIn, audioOut, numInputs, numOutputs, frames);
+
+ // End of Plugin processing (no events)
+
+ // --------------------------------------------------------------------------------------------------------
+ // MIDI Output
+
+ if (pData->event.portOut != nullptr)
+ {
+ ysfx_midi_event_t event;
+
+ while (ysfx_receive_midi(fEffect.get(), &event))
+ {
+ CARLA_SAFE_ASSERT_BREAK(event.offset < frames);
+ CARLA_SAFE_ASSERT_BREAK(event.size > 0);
+ CARLA_SAFE_ASSERT_CONTINUE(event.size <= 0xff);
+
+ if (! pData->event.portOut->writeMidiEvent(event.offset,
+ static_cast(event.size),
+ event.data))
+ break;
+ }
+
+ } // End of MIDI Output
+
+ // --------------------------------------------------------------------------------------------------------
+ // Control Output
+
+ {
+ uint64_t changes = ysfx_fetch_slider_changes(fEffect.get());
+ uint64_t automations = ysfx_fetch_slider_automations(fEffect.get());
+
+ if ((changes|automations) != 0)
+ {
+ for (uint32_t rindex = 0; rindex < ysfx_max_sliders; ++rindex)
+ {
+ uint64_t mask = (uint64_t)1 << rindex;
+
+ //TODO: automations and changes are handled identically
+ // refer to `sliderchange` vs `slider_automate`
+
+ if (((changes|automations) & mask) != 0)
+ {
+ int32_t parameterIndex = fMapOfSliderToParameter[rindex];
+ CARLA_SAFE_ASSERT_CONTINUE(parameterIndex != -1);
+
+ float newValue = ysfx_slider_get_value(fEffect.get(), (uint32_t)parameterIndex);
+ setParameterValueRT(parameterIndex, newValue, true);
+ }
+ }
+ }
+
+ //TODO: slider visibility changes, if this feature can be supported
+ }
+ }
+
+ // -------------------------------------------------------------------
+
+ bool initJSFX(const CarlaPluginPtr plugin,
+ const char* const filename, const char* name, const char* const label, const uint options)
+ {
+ CARLA_SAFE_ASSERT_RETURN(pData->engine != nullptr, false);
+
+ // ---------------------------------------------------------------
+ // first checks
+
+ if (pData->client != nullptr)
+ {
+ pData->engine->setLastError("Plugin client is already registered");
+ return false;
+ }
+
+ if ((filename == nullptr || filename[0] == '\0') &&
+ (label == nullptr || label[0] == '\0'))
+ {
+ pData->engine->setLastError("null filename and label");
+ return false;
+ }
+
+ // ---------------------------------------------------------------
+
+ fUnit = CarlaJsfxUnit();
+
+ {
+ StringArray splitPaths;
+
+ if (const char* paths = pData->engine->getOptions().pathJSFX)
+ splitPaths = StringArray::fromTokens(CharPointer_UTF8(paths), CARLA_OS_SPLIT_STR, "");
+
+ File file;
+ if (filename && filename[0] != '\0')
+ file = File(CharPointer_UTF8(filename));
+
+ if (file.isNotNull() && file.existsAsFile())
+ {
+ // find which engine search path we're in, and use this as the root
+ for (int i = 0; i < splitPaths.size() && !fUnit; ++i)
+ {
+ const File currentPath(splitPaths[i]);
+ if (file.isAChildOf(currentPath))
+ fUnit = CarlaJsfxUnit(currentPath, file);
+ }
+
+ // if not found in engine search paths, use parent directory as the root
+ if (! fUnit)
+ fUnit = CarlaJsfxUnit(file.getParentDirectory(), file);
+ }
+ else if (label && label[0] != '\0')
+ {
+ // search a matching file in plugin paths
+ for (int i = 0; i < splitPaths.size() && !fUnit; ++i)
+ {
+ const File currentPath(splitPaths[i]);
+ const File currentFile = currentPath.getChildFile(CharPointer_UTF8(label));
+ const CarlaJsfxUnit currentUnit(currentPath, currentFile);
+ if (currentUnit.getFilePath().existsAsFile())
+ fUnit = currentUnit;
+ }
+ }
+ }
+
+ if (! fUnit)
+ {
+ pData->engine->setLastError("Cannot locate the JSFX plugin");
+ return false;
+ }
+
+ // ---------------------------------------------------------------
+
+ ysfx_config_u config(ysfx_config_new());
+ CARLA_SAFE_ASSERT_RETURN(config != nullptr, false);
+
+ const water::String rootPath = fUnit.getRootPath().getFullPathName();
+ const water::String filePath = fUnit.getFilePath().getFullPathName();
+
+ ysfx_register_builtin_audio_formats(config.get());
+ ysfx_set_import_root(config.get(), rootPath.toRawUTF8());
+ ysfx_guess_file_roots(config.get(), filePath.toRawUTF8());
+ ysfx_set_log_reporter(config.get(), &CarlaJsfxLogging::logAll);
+ ysfx_set_user_data(config.get(), (intptr_t)this);
+
+ fEffect.reset(ysfx_new(config.get()));
+ CARLA_SAFE_ASSERT_RETURN(fEffect != nullptr, false);
+
+ // ---------------------------------------------------------------
+ // get info
+
+ {
+ if (! ysfx_load_file(fEffect.get(), filePath.toRawUTF8(), 0))
+ {
+ pData->engine->setLastError("Failed to load JSFX");
+ return false;
+ }
+
+ // TODO(jsfx) adapt when implementing these features
+ const int compileFlags = 0
+ //| ysfx_compile_no_serialize
+ | ysfx_compile_no_gfx
+ ;
+
+ if (! ysfx_compile(fEffect.get(), compileFlags))
+ {
+ pData->engine->setLastError("Failed to compile JSFX");
+ return false;
+ }
+ }
+
+ if (name != nullptr && name[0] != '\0')
+ {
+ pData->name = pData->engine->getUniquePluginName(name);
+ }
+ else
+ {
+ pData->name = carla_strdup(ysfx_get_name(fEffect.get()));
+ }
+
+ pData->filename = carla_strdup(filePath.toRawUTF8());
+
+ // ---------------------------------------------------------------
+ // register client
+
+ pData->client = pData->engine->addClient(plugin);
+
+ if (pData->client == nullptr || ! pData->client->isOk())
+ {
+ pData->engine->setLastError("Failed to register plugin client");
+ return false;
+ }
+
+ // ---------------------------------------------------------------
+ // set options
+
+ pData->options = 0x0;
+
+ if (isPluginOptionEnabled(options, PLUGIN_OPTION_USE_CHUNKS))
+ pData->options |= PLUGIN_OPTION_USE_CHUNKS;
+
+ if (isPluginOptionEnabled(options, PLUGIN_OPTION_SEND_CONTROL_CHANGES))
+ pData->options |= PLUGIN_OPTION_SEND_CONTROL_CHANGES;
+ if (isPluginOptionEnabled(options, PLUGIN_OPTION_SEND_CHANNEL_PRESSURE))
+ pData->options |= PLUGIN_OPTION_SEND_CHANNEL_PRESSURE;
+ if (isPluginOptionEnabled(options, PLUGIN_OPTION_SEND_PITCHBEND))
+ pData->options |= PLUGIN_OPTION_SEND_PITCHBEND;
+ if (isPluginOptionEnabled(options, PLUGIN_OPTION_SEND_ALL_SOUND_OFF))
+ pData->options |= PLUGIN_OPTION_SEND_ALL_SOUND_OFF;
+ if (isPluginOptionEnabled(options, PLUGIN_OPTION_MAP_PROGRAM_CHANGES))
+ pData->options |= PLUGIN_OPTION_MAP_PROGRAM_CHANGES;
+ if (isPluginOptionInverseEnabled(options, PLUGIN_OPTION_SKIP_SENDING_NOTES))
+ pData->options |= PLUGIN_OPTION_SKIP_SENDING_NOTES;
+ if (isPluginOptionEnabled(options, PLUGIN_OPTION_SEND_NOTE_AFTERTOUCH))
+ pData->options |= PLUGIN_OPTION_SEND_NOTE_AFTERTOUCH;
+
+ return true;
+ }
+
+private:
+ ysfx_u fEffect = nullptr;
+ CarlaJsfxUnit fUnit;
+ water::String fChunkText;
+ ysfx_time_info_t fTransportValues = {};
+ std::vector fMapOfSliderToParameter;
+};
+
+// -------------------------------------------------------------------------------------------------------------------
+
+CarlaPluginPtr CarlaPlugin::newJSFX(const Initializer& init)
+{
+ carla_debug("CarlaPlugin::newJSFX({%p, \"%s\", \"%s\", \"%s\", " P_INT64 "})",
+ init.engine, init.filename, init.name, init.label, init.uniqueId);
+
+ std::shared_ptr plugin(new CarlaPluginJSFX(init.engine, init.id));
+
+ if (! plugin->initJSFX(plugin, init.filename, init.name, init.label, init.options))
+ return nullptr;
+
+ return plugin;
+}
+
+// -------------------------------------------------------------------------------------------------------------------
+
+CARLA_BACKEND_END_NAMESPACE
diff --git a/source/backend/plugin/Makefile b/source/backend/plugin/Makefile
index 5911d2e13..5c5126545 100644
--- a/source/backend/plugin/Makefile
+++ b/source/backend/plugin/Makefile
@@ -34,6 +34,7 @@ OBJS = \
$(OBJDIR)/CarlaPluginVST2.cpp.o \
$(OBJDIR)/CarlaPluginVST3.cpp.o \
$(OBJDIR)/CarlaPluginAU.cpp.o \
+ $(OBJDIR)/CarlaPluginJSFX.cpp.o \
$(OBJDIR)/CarlaPluginJuce.cpp.o \
$(OBJDIR)/CarlaPluginFluidSynth.cpp.o \
$(OBJDIR)/CarlaPluginSFZero.cpp.o
@@ -72,6 +73,11 @@ $(OBJDIR)/CarlaPluginFluidSynth.cpp.o: CarlaPluginFluidSynth.cpp
@echo "Compiling $<"
$(SILENT)$(CXX) $< $(BUILD_CXX_FLAGS) $(FLUIDSYNTH_FLAGS) -c -o $@
+$(OBJDIR)/CarlaPluginJSFX.cpp.o: CarlaPluginJSFX.cpp
+ -@mkdir -p $(OBJDIR)
+ @echo "Compiling $<"
+ @$(CXX) $< $(BUILD_CXX_FLAGS) $(YSFX_FLAGS) -c -o $@
+
ifeq ($(MACOS),true)
$(OBJDIR)/CarlaPluginVST2.cpp.o: CarlaPluginVST2.cpp
-@mkdir -p $(OBJDIR)
diff --git a/source/backend/utils/CachedPlugins.cpp b/source/backend/utils/CachedPlugins.cpp
index 7d5de043c..fae175bcc 100644
--- a/source/backend/utils/CachedPlugins.cpp
+++ b/source/backend/utils/CachedPlugins.cpp
@@ -21,6 +21,7 @@
#include "CarlaString.hpp"
#include "CarlaBackendUtils.hpp"
#include "CarlaLv2Utils.hpp"
+#include "CarlaJsfxUtils.hpp"
#if defined(USING_JUCE) && defined(CARLA_OS_MAC)
# include "AppConfig.h"
@@ -96,6 +97,37 @@ static void findSFZs(const char* const sfzPaths)
}
}
}
+// -------------------------------------------------------------------------------------------------------------------
+
+static water::Array gJSFXs;
+
+static void findJSFXs(const char* const jsfxPaths)
+{
+ gJSFXs.clearQuick();
+
+ CARLA_SAFE_ASSERT_RETURN(jsfxPaths != nullptr,);
+
+ if (jsfxPaths[0] == '\0')
+ return;
+
+ const water::StringArray splitPaths(water::StringArray::fromTokens(jsfxPaths, CARLA_OS_SPLIT_STR, ""));
+
+ for (water::String *it = splitPaths.begin(), *end = splitPaths.end(); it != end; ++it)
+ {
+ water::Array results;
+ water::File path(*it);
+
+ if (path.findChildFiles(results, water::File::findFiles|water::File::ignoreHiddenFiles, true, "*") > 0)
+ {
+ for (const water::File& file : results)
+ {
+ water::String fileExt = file.getFileExtension();
+ if (fileExt.isEmpty() || fileExt.equalsIgnoreCase(".jsfx"))
+ gJSFXs.add(CarlaJsfxUnit(path, file));
+ }
+ }
+ }
+}
// -------------------------------------------------------------------------------------------------------------------
@@ -630,6 +662,81 @@ static const CarlaCachedPluginInfo* get_cached_plugin_sfz(const water::File& fil
// -------------------------------------------------------------------------------------------------------------------
+static const CarlaCachedPluginInfo* get_cached_plugin_jsfx(const CarlaJsfxUnit& unit)
+{
+ static CarlaCachedPluginInfo info;
+
+ ysfx_config_u config(ysfx_config_new());
+
+ const water::String rootPath = unit.getRootPath().getFullPathName();
+ const water::String filePath = unit.getFilePath().getFullPathName();
+
+ ysfx_register_builtin_audio_formats(config.get());
+ ysfx_set_import_root(config.get(), rootPath.toRawUTF8());
+ ysfx_guess_file_roots(config.get(), filePath.toRawUTF8());
+ ysfx_set_log_reporter(config.get(), &CarlaJsfxLogging::logErrorsOnly);
+
+ ysfx_u effect(ysfx_new(config.get()));
+
+ if (! ysfx_load_file(effect.get(), filePath.toRawUTF8(), 0))
+ {
+ info.valid = false;
+ return &info;
+ }
+
+ // plugins with neither @block nor @sample are valid, but they are useless
+ // also use this as a sanity check against misdetected files
+ // since JSFX parsing is so permissive, it might accept lambda text files
+ if (! ysfx_has_section(effect.get(), ysfx_section_block) &&
+ ! ysfx_has_section(effect.get(), ysfx_section_sample))
+ {
+ info.valid = false;
+ return &info;
+ }
+
+ static CarlaString name, label, maker;
+ label = unit.getFileId().toRawUTF8();
+ name = ysfx_get_name(effect.get());
+ maker = ysfx_get_author(effect.get());
+
+ info.valid = true;
+
+ info.category = CarlaJsfxCategories::getFromEffect(effect.get());
+
+ info.audioIns = ysfx_get_num_inputs(effect.get());
+ info.audioOuts = ysfx_get_num_outputs(effect.get());
+
+ info.cvIns = 0;
+ info.cvOuts = 0;
+
+ info.midiIns = 1;
+ info.midiOuts = 1;
+
+ info.parameterIns = 0;
+ info.parameterOuts = 0;
+ for (uint32_t sliderIndex = 0; sliderIndex < ysfx_max_sliders; ++sliderIndex)
+ {
+ if (ysfx_slider_exists(effect.get(), sliderIndex))
+ ++info.parameterIns;
+ }
+
+ info.hints = 0;
+
+#if 0 // TODO(jsfx) when supporting custom graphics
+ if (ysfx_has_section(effect.get(), ysfx_section_gfx))
+ info.hints |= CB::PLUGIN_HAS_CUSTOM_UI;
+#endif
+
+ info.name = name.buffer();
+ info.label = label.buffer();
+ info.maker = maker.buffer();
+ info.copyright = gCachedPluginsNullCharPtr;
+
+ return &info;
+}
+
+// -------------------------------------------------------------------------------------------------------------------
+
uint carla_get_cached_plugin_count(CB::PluginType ptype, const char* pluginPath)
{
CARLA_SAFE_ASSERT_RETURN(isCachedPluginType(ptype), 0);
@@ -661,6 +768,11 @@ uint carla_get_cached_plugin_count(CB::PluginType ptype, const char* pluginPath)
return static_cast(gSFZs.size());
}
+ case CB::PLUGIN_JSFX: {
+ findJSFXs(pluginPath);
+ return static_cast(gJSFXs.size());
+ }
+
default:
return 0;
}
@@ -706,6 +818,11 @@ const CarlaCachedPluginInfo* carla_get_cached_plugin_info(CB::PluginType ptype,
return get_cached_plugin_sfz(gSFZs[index]);
}
+ case CB::PLUGIN_JSFX: {
+ CARLA_SAFE_ASSERT_BREAK(index < static_cast(gJSFXs.size()));
+ return get_cached_plugin_jsfx(gJSFXs.getUnchecked(static_cast(index)));
+ }
+
default:
break;
}
diff --git a/source/backend/utils/Information.cpp b/source/backend/utils/Information.cpp
index ff37c3391..b9e803f27 100644
--- a/source/backend/utils/Information.cpp
+++ b/source/backend/utils/Information.cpp
@@ -76,6 +76,8 @@ const char* carla_get_complete_license_text()
"AU plugin support (using JUCE)"
#endif
+ "JSFX plugin support (using ysfx)"
+
// Sample kit libraries
#if defined(HAVE_FLUIDSYNTH) && !defined(BUILD_BRIDGE_ALTERNATIVE_ARCH)
"FluidSynth library v" FLUIDSYNTH_VERSION " for SF2/3 support"
@@ -185,6 +187,9 @@ const char* const* carla_get_supported_file_extensions()
// SFZ
"sfz",
+ // JSFX
+ "jsfx",
+
// terminator
nullptr
};
diff --git a/source/backend/utils/Makefile b/source/backend/utils/Makefile
index 3aec90beb..5f1278c5a 100644
--- a/source/backend/utils/Makefile
+++ b/source/backend/utils/Makefile
@@ -10,6 +10,7 @@ include ../Makefile.mk
# ---------------------------------------------------------------------------------------------------------------------
BUILD_CXX_FLAGS += $(FLUIDSYNTH_FLAGS)
+BUILD_CXX_FLAGS += $(YSFX_FLAGS)
# ---------------------------------------------------------------------------------------------------------------------
@@ -62,6 +63,9 @@ LINK_FLAGS += $(JUCE_GUI_BASICS_LIBS)
LINK_FLAGS += $(JUCE_GUI_EXTRA_LIBS)
endif
+LINK_FLAGS += $(MODULEDIR)/ysfx.a
+LINK_FLAGS += $(YSFX_GRAPHICS_LIBS)
+
# ---------------------------------------------------------------------------------------------------------------------
all: $(TARGETS)
diff --git a/source/bridges-plugin/Makefile b/source/bridges-plugin/Makefile
index ccb9ebdce..de369fd78 100644
--- a/source/bridges-plugin/Makefile
+++ b/source/bridges-plugin/Makefile
@@ -147,6 +147,9 @@ LIBS_win64 += $(MODULEDIR)/juce_gui_extra.win64.a
LINK_FLAGS += $(JUCE_GUI_EXTRA_LIBS)
endif
+LIBS_native += $(MODULEDIR)/ysfx.a
+LINK_FLAGS += $(YSFX_GRAPHICS_LIBS)
+
ifeq ($(JACKBRIDGE_DIRECT),true)
LINK_FLAGS += $(JACK_LIBS)
endif
@@ -205,6 +208,7 @@ OBJS_native = \
$(OBJDIR)/CarlaPluginVST2.cpp.o \
$(OBJDIR)/CarlaPluginVST3.cpp.o \
$(OBJDIR)/CarlaPluginAU.cpp.o \
+ $(OBJDIR)/CarlaPluginJSFX.cpp.o \
$(OBJDIR)/CarlaPluginJuce.cpp.o \
$(OBJDIR)/CarlaPluginFluidSynth.cpp.o \
$(OBJDIR)/CarlaPluginSFZero.cpp.o \
@@ -306,6 +310,11 @@ $(BINDIR)/$(MODULENAME)-win64.exe: $(OBJS_win64) $(LIBS_win64)
# ---------------------------------------------------------------------------------------------------------------------
# native
+$(OBJDIR)/CarlaPluginJSFX.cpp.o: $(CWD)/backend/plugin/CarlaPluginJSFX.cpp
+ -@mkdir -p $(OBJDIR)
+ @echo "Compiling CarlaPluginJSFX.cpp (bridge)"
+ @$(CXX) $< $(BUILD_CXX_FLAGS) $(NATIVE_BUILD_FLAGS) $(YSFX_FLAGS) -c -o $@
+
ifeq ($(MACOS),true)
$(OBJDIR)/CarlaPluginVST2.cpp.o: $(CWD)/backend/plugin/CarlaPluginVST2.cpp
-@mkdir -p $(OBJDIR)
diff --git a/source/discovery/Makefile b/source/discovery/Makefile
index 04d114216..f3ac79991 100644
--- a/source/discovery/Makefile
+++ b/source/discovery/Makefile
@@ -69,6 +69,8 @@ NATIVE_BUILD_FLAGS += $(FLUIDSYNTH_FLAGS)
NATIVE_LINK_FLAGS += $(FLUIDSYNTH_LIBS)
endif
+NATIVE_BUILD_FLAGS += $(YSFX_FLAGS)
+
# ---------------------------------------------------------------------------------------------------------------------
LIBS_native = $(MODULEDIR)/lilv.a
@@ -152,6 +154,9 @@ LIBS_win64 += $(MODULEDIR)/juce_gui_extra.win64.a
LINK_FLAGS += $(JUCE_GUI_EXTRA_LIBS)
endif # USING_JUCE
+LIBS_native += $(MODULEDIR)/ysfx.a
+LINK_FLAGS += $(YSFX_GRAPHICS_LIBS)
+
# ---------------------------------------------------------------------------------------------------------------------
OBJS_native = $(OBJDIR)/$(MODULENAME).cpp.o
diff --git a/source/discovery/carla-discovery.cpp b/source/discovery/carla-discovery.cpp
index 3c8a56f30..45f120d8b 100644
--- a/source/discovery/carla-discovery.cpp
+++ b/source/discovery/carla-discovery.cpp
@@ -18,6 +18,7 @@
#include "CarlaBackendUtils.hpp"
#include "CarlaLibUtils.hpp"
#include "CarlaMathUtils.hpp"
+#include "CarlaScopeUtils.hpp"
#include "CarlaMIDI.h"
#include "LinkedList.hpp"
@@ -54,6 +55,7 @@
#ifndef BUILD_BRIDGE
# include "CarlaDssiUtils.cpp"
+# include "CarlaJsfxUtils.hpp"
# include "../backend/utils/CachedPlugins.cpp"
#endif
@@ -2112,6 +2114,67 @@ static void do_fluidsynth_check(const char* const filename, const PluginType typ
#endif
}
+// -------------------------------------------------------------------------------------------------------------------
+
+#ifndef BUILD_BRIDGE
+static void do_jsfx_check(const char* const filename, bool doInit)
+{
+ const water::File file = File(CharPointer_UTF8(filename));
+
+ ysfx_config_u config(ysfx_config_new());
+
+ ysfx_register_builtin_audio_formats(config.get());
+ ysfx_guess_file_roots(config.get(), filename);
+ ysfx_set_log_reporter(config.get(), &CarlaJsfxLogging::logErrorsOnly);
+
+ ysfx_u effect(ysfx_new(config.get()));
+
+ uint hints = 0;
+
+ // do not attempt to compile it, because the import path is not known
+ (void)doInit;
+
+ if (! ysfx_load_file(effect.get(), filename, 0))
+ {
+ DISCOVERY_OUT("error", "Cannot read the JSFX header");
+ return;
+ }
+
+ const char* const name = ysfx_get_name(effect.get());
+
+ // author and category are extracted from the pseudo-tags
+ const char* const author = ysfx_get_author(effect.get());
+ const CB::PluginCategory category = CarlaJsfxCategories::getFromEffect(effect.get());
+
+ const uint32_t audioIns = ysfx_get_num_inputs(effect.get());
+ const uint32_t audioOuts = ysfx_get_num_outputs(effect.get());
+
+ const uint32_t midiIns = 1;
+ const uint32_t midiOuts = 1;
+
+ uint32_t parameters = 0;
+ for (uint32_t sliderIndex = 0; sliderIndex < ysfx_max_sliders; ++sliderIndex)
+ {
+ if (ysfx_slider_exists(effect.get(), sliderIndex))
+ ++parameters;
+ }
+
+ DISCOVERY_OUT("init", "-----------");
+ DISCOVERY_OUT("build", BINARY_NATIVE);
+ DISCOVERY_OUT("hints", hints);
+ DISCOVERY_OUT("category", getPluginCategoryAsString(category));
+ DISCOVERY_OUT("name", name);
+ DISCOVERY_OUT("maker", author);
+ DISCOVERY_OUT("label", filename);
+ DISCOVERY_OUT("audio.ins", audioIns);
+ DISCOVERY_OUT("audio.outs", audioOuts);
+ DISCOVERY_OUT("midi.ins", midiIns);
+ DISCOVERY_OUT("midi.outs", midiOuts);
+ DISCOVERY_OUT("parameters.ins", parameters);
+ DISCOVERY_OUT("end", "------------");
+}
+#endif
+
// ------------------------------ main entry point ------------------------------
int main(int argc, char* argv[])
@@ -2282,6 +2345,12 @@ int main(int argc, char* argv[])
#endif
break;
+#ifndef BUILD_BRIDGE
+ case PLUGIN_JSFX:
+ do_jsfx_check(filename, doInit);
+ break;
+#endif
+
case PLUGIN_DLS:
case PLUGIN_GIG:
case PLUGIN_SF2:
diff --git a/source/frontend/C++/carla_database.cpp b/source/frontend/C++/carla_database.cpp
index 913451444..3396233b4 100644
--- a/source/frontend/C++/carla_database.cpp
+++ b/source/frontend/C++/carla_database.cpp
@@ -209,6 +209,7 @@ PluginRefreshW::PluginRefreshW(QWidget* const parent, const CarlaHost& host)
connect(self->ui.ch_au, SIGNAL(clicked()), SLOT(slot_checkTools()));
connect(self->ui.ch_sf2, SIGNAL(clicked()), SLOT(slot_checkTools()));
connect(self->ui.ch_sfz, SIGNAL(clicked()), SLOT(slot_checkTools()));
+ connect(self->ui.ch_jsfx, SIGNAL(clicked()), SLOT(slot_checkTools()));
connect(&self->fThread, SIGNAL(pluginLook(float, QString)), SLOT(slot_handlePluginLook(float, QString)));
connect(&self->fThread, SIGNAL(finished(int)), SLOT(slot_handlePluginThreadFinished()));
diff --git a/source/frontend/C++/carla_host.cpp b/source/frontend/C++/carla_host.cpp
index 013e2433c..9a39ddae0 100644
--- a/source/frontend/C++/carla_host.cpp
+++ b/source/frontend/C++/carla_host.cpp
@@ -2676,6 +2676,7 @@ QString setEngineSettings(CarlaHost& host)
QStringList VST3_PATH = settings.valueStringList(CARLA_KEY_PATHS_VST3, CARLA_DEFAULT_VST3_PATH);
QStringList SF2_PATH = settings.valueStringList(CARLA_KEY_PATHS_SF2, CARLA_DEFAULT_SF2_PATH);
QStringList SFZ_PATH = settings.valueStringList(CARLA_KEY_PATHS_SFZ, CARLA_DEFAULT_SFZ_PATH);
+ QStringList JSFX_PATH = settings.valueStringList(CARLA_KEY_PATHS_JSFX, CARLA_DEFAULT_JSFX_PATH);
/*
// TODO
@@ -2686,6 +2687,7 @@ QString setEngineSettings(CarlaHost& host)
carla_set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_VST3, splitter.join(VST3_PATH))
carla_set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_SF2, splitter.join(SF2_PATH))
carla_set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_SFZ, splitter.join(SFZ_PATH))
+ carla_set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_JSFX, splitter.join(JSFX_PATH))
*/
//-----------------------------------------------------------------------------------------------------------------
diff --git a/source/frontend/C++/carla_settings.cpp b/source/frontend/C++/carla_settings.cpp
index 7c7f77e44..f0f3778ea 100644
--- a/source/frontend/C++/carla_settings.cpp
+++ b/source/frontend/C++/carla_settings.cpp
@@ -426,7 +426,8 @@ enum PluginPathIndexes {
PLUGINPATH_INDEX_VST2,
PLUGINPATH_INDEX_VST3,
PLUGINPATH_INDEX_SF2,
- PLUGINPATH_INDEX_SFZ
+ PLUGINPATH_INDEX_SFZ,
+ PLUGINPATH_INDEX_JSFX
};
/*
@@ -662,6 +663,7 @@ struct CarlaSettingsW::PrivateData {
QStringList vst3s = settings.valueStringList(CARLA_KEY_PATHS_VST3, CARLA_DEFAULT_VST3_PATH);
QStringList sf2s = settings.valueStringList(CARLA_KEY_PATHS_SF2, CARLA_DEFAULT_SF2_PATH);
QStringList sfzs = settings.valueStringList(CARLA_KEY_PATHS_SFZ, CARLA_DEFAULT_SFZ_PATH);
+ QStringList jsfxs = settings.valueStringList(CARLA_KEY_PATHS_JSFX, CARLA_DEFAULT_JSFX_PATH);
ladspas.sort();
dssis.sort();
@@ -670,6 +672,7 @@ struct CarlaSettingsW::PrivateData {
vst3s.sort();
sf2s.sort();
sfzs.sort();
+ jsfxs.sort();
for (const QString& ladspa : ladspas)
{
@@ -713,6 +716,12 @@ struct CarlaSettingsW::PrivateData {
ui.lw_sfz->addItem(sfz);
}
+ for (const QString& jsfx : jsfxs)
+ {
+ if (jsfx.isEmpty()) continue;
+ ui.lw_jsfx->addItem(jsfx);
+ }
+
// ------------------------------------------------------------------------------------------------------------
// Wine
@@ -883,6 +892,7 @@ struct CarlaSettingsW::PrivateData {
QStringList vst3s;
QStringList sf2s;
QStringList sfzs;
+ QStringList jsfxs;
for (int i=0; i < ui.lw_ladspa->count(); ++i)
ladspas.append(ui.lw_ladspa->item(i)->text());
@@ -905,6 +915,9 @@ struct CarlaSettingsW::PrivateData {
for (int i=0; i < ui.lw_sfz->count(); ++i)
sfzs.append(ui.lw_sfz->item(i)->text());
+ for (int i=0; i < ui.lw_jsfx->count(); ++i)
+ jsfxs.append(ui.lw_jsfx->item(i)->text());
+
/* TODO
host.set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_LADSPA, splitter.join(ladspas));
host.set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_DSSI, splitter.join(dssis));
@@ -913,6 +926,7 @@ struct CarlaSettingsW::PrivateData {
host.set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_VST3, splitter.join(vst3s));
host.set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_SF2, splitter.join(sf2s));
host.set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_SFZ, splitter.join(sfzs));
+ host.set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_JSFX, splitter.join(jsfxs));
*/
settings.setValue(CARLA_KEY_PATHS_LADSPA, ladspas);
@@ -922,6 +936,7 @@ struct CarlaSettingsW::PrivateData {
settings.setValue(CARLA_KEY_PATHS_VST3, vst3s);
settings.setValue(CARLA_KEY_PATHS_SF2, sf2s);
settings.setValue(CARLA_KEY_PATHS_SFZ, sfzs);
+ settings.setValue(CARLA_KEY_PATHS_JSFX, jsfxs);
// ------------------------------------------------------------------------------------------------------------
// Wine
@@ -1147,6 +1162,19 @@ struct CarlaSettingsW::PrivateData {
ui.lw_sfz->addItem(path);
}
break;
+
+ case PLUGINPATH_INDEX_JSFX:
+ paths = CARLA_DEFAULT_JSFX_PATH;
+ paths.sort();
+ ui.lw_jsfx->clear();
+
+ for (const auto& path : paths)
+ {
+ if (path.isEmpty())
+ continue;
+ ui.lw_jsfx->addItem(path);
+ }
+ break;
}
break;
}
@@ -1310,6 +1338,7 @@ CarlaSettingsW::CarlaSettingsW(QWidget* const parent, CarlaHost& host, const boo
connect(self->ui.lw_vst3, SIGNAL(currentRowChanged(int)), SLOT(slot_pluginPathRowChanged(int)));
connect(self->ui.lw_sf2, SIGNAL(currentRowChanged(int)), SLOT(slot_pluginPathRowChanged(int)));
connect(self->ui.lw_sfz, SIGNAL(currentRowChanged(int)), SLOT(slot_pluginPathRowChanged(int)));
+ connect(self->ui.lw_jsfx, SIGNAL(currentRowChanged(int)), SLOT(slot_pluginPathRowChanged(int)));
connect(self->ui.b_filepaths_add, SIGNAL(clicked()), SLOT(slot_addFilePath()));
connect(self->ui.b_filepaths_remove, SIGNAL(clicked()), SLOT(slot_removeFilePath()));
@@ -1335,6 +1364,7 @@ CarlaSettingsW::CarlaSettingsW(QWidget* const parent, CarlaHost& host, const boo
self->ui.lw_vst3->setCurrentRow(0);
self->ui.lw_sf2->setCurrentRow(0);
self->ui.lw_sfz->setCurrentRow(0);
+ self->ui.lw_jsfx->setCurrentRow(0);
self->ui.lw_files_audio->setCurrentRow(0);
self->ui.lw_files_midi->setCurrentRow(0);
@@ -1485,6 +1515,9 @@ void CarlaSettingsW::slot_addPluginPath()
case PLUGINPATH_INDEX_SFZ:
self->ui.lw_sfz->addItem(newPath);
break;
+ case PLUGINPATH_INDEX_JSFX:
+ self->ui.lw_jsfx->addItem(newPath);
+ break;
}
}
@@ -1513,6 +1546,9 @@ void CarlaSettingsW::slot_removePluginPath()
case PLUGINPATH_INDEX_SFZ:
self->ui.lw_sfz->takeItem(self->ui.lw_sfz->currentRow());
break;
+ case PLUGINPATH_INDEX_JSFX:
+ self->ui.lw_jsfx->takeItem(self->ui.lw_jsfx->currentRow());
+ break;
}
}
@@ -1545,6 +1581,9 @@ void CarlaSettingsW::slot_changePluginPath()
case PLUGINPATH_INDEX_SFZ:
currentPath = self->ui.lw_sfz->currentItem()->text();
break;
+ case PLUGINPATH_INDEX_JSFX:
+ currentPath = self->ui.lw_jsfx->currentItem()->text();
+ break;
}
const QString newPath = QFileDialog::getExistingDirectory(this, tr("Add Path"), currentPath, QFileDialog::ShowDirsOnly);
@@ -1575,6 +1614,9 @@ void CarlaSettingsW::slot_changePluginPath()
case PLUGINPATH_INDEX_SFZ:
self->ui.lw_sfz->currentItem()->setText(newPath);
break;
+ case PLUGINPATH_INDEX_JSFX:
+ self->ui.lw_jsfx->currentItem()->setText(newPath);
+ break;
}
}
@@ -1607,6 +1649,9 @@ void CarlaSettingsW::slot_pluginPathTabChanged(const int index)
case PLUGINPATH_INDEX_SFZ:
row = self->ui.lw_sfz->currentRow();
break;
+ case PLUGINPATH_INDEX_JSFX:
+ row = self->ui.lw_jsfx->currentRow();
+ break;
default:
row = -1;
break;
diff --git a/source/frontend/C++/carla_shared.hpp b/source/frontend/C++/carla_shared.hpp
index c283782f0..b53d4de5d 100644
--- a/source/frontend/C++/carla_shared.hpp
+++ b/source/frontend/C++/carla_shared.hpp
@@ -172,6 +172,7 @@ static const char* const* const MIDI_CC_LIST = {
#define CARLA_KEY_PATHS_VST3 "Paths/VST3"
#define CARLA_KEY_PATHS_SF2 "Paths/SF2"
#define CARLA_KEY_PATHS_SFZ "Paths/SFZ"
+#define CARLA_KEY_PATHS_JSFX "Paths/JSFX"
#define CARLA_KEY_WINE_EXECUTABLE "Wine/Executable" /* str */
#define CARLA_KEY_WINE_AUTO_PREFIX "Wine/AutoPrefix" /* bool */
@@ -299,6 +300,7 @@ static const char* const* const MIDI_CC_LIST = {
#define DEFAULT_VST3_PATH ""
#define DEFAULT_SF2_PATH ""
#define DEFAULT_SFZ_PATH ""
+#define DEFAULT_JSFX_PATH ""
#ifdef CARLA_OS_WIN
# define CARLA_PATH_SPLITTER ";"
@@ -338,6 +340,7 @@ if WINDOWS:
# define CARLA_DEFAULT_VST3_PATH = std::getenv("VST3_PATH", DEFAULT_VST3_PATH).split(CARLA_PATH_SPLITTER)
# define CARLA_DEFAULT_SF2_PATH = std::getenv("SF2_PATH", DEFAULT_SF2_PATH).split(CARLA_PATH_SPLITTER)
# define CARLA_DEFAULT_SFZ_PATH = std::getenv("SFZ_PATH", DEFAULT_SFZ_PATH).split(CARLA_PATH_SPLITTER)
+# define CARLA_DEFAULT_JSFX_PATH = std::getenv("JSFX_PATH", DEFAULT_JSFX_PATH).split(CARLA_PATH_SPLITTER)
#else
*/
# define CARLA_DEFAULT_LADSPA_PATH QString(DEFAULT_LADSPA_PATH).split(CARLA_PATH_SPLITTER)
@@ -347,6 +350,7 @@ if WINDOWS:
# define CARLA_DEFAULT_VST3_PATH QString(DEFAULT_VST3_PATH).split(CARLA_PATH_SPLITTER)
# define CARLA_DEFAULT_SF2_PATH QString(DEFAULT_SF2_PATH).split(CARLA_PATH_SPLITTER)
# define CARLA_DEFAULT_SFZ_PATH QString(DEFAULT_SFZ_PATH).split(CARLA_PATH_SPLITTER)
+# define CARLA_DEFAULT_JSFX_PATH QString(DEFAULT_JSFX_PATH).split(CARLA_PATH_SPLITTER)
/*
#endif
*/
diff --git a/source/frontend/carla_database.py b/source/frontend/carla_database.py
index 7a5473c05..db5901d01 100755
--- a/source/frontend/carla_database.py
+++ b/source/frontend/carla_database.py
@@ -410,6 +410,7 @@ class SearchPluginsThread(QThread):
self.fCheckAU = False
self.fCheckSF2 = False
self.fCheckSFZ = False
+ self.fCheckJSFX = False
if WINDOWS:
toolNative = "carla-discovery-native.exe"
@@ -447,7 +448,7 @@ class SearchPluginsThread(QThread):
self.fCheckWin32 = win32
self.fCheckWin64 = win64
- def setSearchPluginTypes(self, ladspa, dssi, lv2, vst2, vst3, au, sf2, sfz):
+ def setSearchPluginTypes(self, ladspa, dssi, lv2, vst2, vst3, au, sf2, sfz, jsfx):
self.fCheckLADSPA = ladspa
self.fCheckDSSI = dssi
self.fCheckLV2 = lv2
@@ -456,6 +457,7 @@ class SearchPluginsThread(QThread):
self.fCheckAU = au and MACOS
self.fCheckSF2 = sf2
self.fCheckSFZ = sfz
+ self.fCheckJSFX = jsfx
def stop(self):
self.fContinueChecking = False
@@ -522,6 +524,12 @@ class SearchPluginsThread(QThread):
else:
self.fCheckSF2 = False
+ if self.fCheckJSFX:
+ if self.fCheckNative:
+ self.fCurCount += 1
+ else:
+ self.fCheckJSFX = False
+
if self.fCurCount == 0:
return
@@ -721,6 +729,11 @@ class SearchPluginsThread(QThread):
settingsDB.setValue("Plugins/SFZ", kits)
settingsDB.sync()
+ if self.fCheckJSFX:
+ kits = self._checkJsfxCached()
+ settingsDB.setValue("Plugins/JSFX", kits)
+ settingsDB.sync()
+
def _checkLADSPA(self, OS, tool, isWine=False):
ladspaBinaries = []
ladspaPlugins = []
@@ -994,6 +1007,36 @@ class SearchPluginsThread(QThread):
self.fLastCheckValue += self.fCurPercentValue
return sfzKits
+ def _checkJsfxCached(self):
+ settings = QSafeSettings("falkTX", "Carla2")
+ PLUG_PATH = splitter.join(settings.value(CARLA_KEY_PATHS_JSFX, CARLA_DEFAULT_JSFX_PATH, list))
+ del settings
+
+ jsfxPlugins = []
+ self._pluginLook(self.fLastCheckValue, "JSFX plugins...")
+
+ count = gCarla.utils.get_cached_plugin_count(PLUGIN_JSFX, PLUG_PATH)
+
+ if not self.fContinueChecking:
+ return jsfxPlugins
+
+ for i in range(count):
+ descInfo = gCarla.utils.get_cached_plugin_info(PLUGIN_JSFX, i)
+
+ percent = ( float(i) / count ) * self.fCurPercentValue
+ self._pluginLook(self.fLastCheckValue + percent, descInfo['label'])
+
+ if not descInfo['valid']:
+ continue
+
+ jsfxPlugins.append(checkPluginCached(descInfo, PLUGIN_JSFX))
+
+ if not self.fContinueChecking:
+ break
+
+ self.fLastCheckValue += self.fCurPercentValue
+ return jsfxPlugins
+
def _pluginLook(self, percent, plugin):
self.pluginLook.emit(percent, plugin)
@@ -1227,6 +1270,7 @@ class PluginRefreshW(QDialog):
self.ui.ch_au.clicked.connect(self.slot_checkTools)
self.ui.ch_sf2.clicked.connect(self.slot_checkTools)
self.ui.ch_sfz.clicked.connect(self.slot_checkTools)
+ self.ui.ch_jsfx.clicked.connect(self.slot_checkTools)
self.fThread.pluginLook.connect(self.slot_handlePluginLook)
self.fThread.finished.connect(self.slot_handlePluginThreadFinished)
@@ -1267,6 +1311,9 @@ class PluginRefreshW(QDialog):
check = settings.value("PluginDatabase/SearchSFZ", False, bool) and self.ui.ch_sfz.isEnabled()
self.ui.ch_sfz.setChecked(check)
+ check = settings.value("PluginDatabase/SearchJSFX", True, bool) and self.ui.ch_jsfx.isEnabled()
+ self.ui.ch_jsfx.setChecked(check)
+
check = settings.value("PluginDatabase/SearchNative", True, bool) and self.ui.ch_native.isEnabled()
self.ui.ch_native.setChecked(check)
@@ -1297,6 +1344,7 @@ class PluginRefreshW(QDialog):
settings.setValue("PluginDatabase/SearchAU", self.ui.ch_au.isChecked())
settings.setValue("PluginDatabase/SearchSF2", self.ui.ch_sf2.isChecked())
settings.setValue("PluginDatabase/SearchSFZ", self.ui.ch_sfz.isChecked())
+ settings.setValue("PluginDatabase/SearchJSFX", self.ui.ch_jsfx.isChecked())
settings.setValue("PluginDatabase/SearchNative", self.ui.ch_native.isChecked())
settings.setValue("PluginDatabase/SearchPOSIX32", self.ui.ch_posix32.isChecked())
settings.setValue("PluginDatabase/SearchPOSIX64", self.ui.ch_posix64.isChecked())
@@ -1326,13 +1374,14 @@ class PluginRefreshW(QDialog):
self.ui.ch_posix32.isChecked(), self.ui.ch_posix64.isChecked(),
self.ui.ch_win32.isChecked(), self.ui.ch_win64.isChecked())
- ladspa, dssi, lv2, vst, vst3, au, sf2, sfz = (self.ui.ch_ladspa.isChecked(), self.ui.ch_dssi.isChecked(),
- self.ui.ch_lv2.isChecked(), self.ui.ch_vst.isChecked(),
- self.ui.ch_vst3.isChecked(), self.ui.ch_au.isChecked(),
- self.ui.ch_sf2.isChecked(), self.ui.ch_sfz.isChecked())
+ ladspa, dssi, lv2, vst, vst3, au, sf2, sfz, jsfx = (self.ui.ch_ladspa.isChecked(), self.ui.ch_dssi.isChecked(),
+ self.ui.ch_lv2.isChecked(), self.ui.ch_vst.isChecked(),
+ self.ui.ch_vst3.isChecked(), self.ui.ch_au.isChecked(),
+ self.ui.ch_sf2.isChecked(), self.ui.ch_sfz.isChecked(),
+ self.ui.ch_jsfx.isChecked())
self.fThread.setSearchBinaryTypes(native, posix32, posix64, win32, win64)
- self.fThread.setSearchPluginTypes(ladspa, dssi, lv2, vst, vst3, au, sf2, sfz)
+ self.fThread.setSearchPluginTypes(ladspa, dssi, lv2, vst, vst3, au, sf2, sfz, jsfx)
self.fThread.start()
# -----------------------------------------------------------------------------------------------------------------
@@ -1352,7 +1401,8 @@ class PluginRefreshW(QDialog):
enabled2 = bool(self.ui.ch_ladspa.isChecked() or self.ui.ch_dssi.isChecked() or
self.ui.ch_lv2.isChecked() or self.ui.ch_vst.isChecked() or
self.ui.ch_vst3.isChecked() or self.ui.ch_au.isChecked() or
- self.ui.ch_sf2.isChecked() or self.ui.ch_sfz.isChecked())
+ self.ui.ch_sf2.isChecked() or self.ui.ch_sfz.isChecked() or
+ self.ui.ch_jsfx.isChecked())
self.ui.b_start.setEnabled(enabled1 and enabled2)
@@ -1512,6 +1562,7 @@ class PluginDatabaseW(QDialog):
self.ui.ch_vst.clicked.connect(self.slot_checkFilters)
self.ui.ch_vst3.clicked.connect(self.slot_checkFilters)
self.ui.ch_au.clicked.connect(self.slot_checkFilters)
+ self.ui.ch_jsfx.clicked.connect(self.slot_checkFilters)
self.ui.ch_kits.clicked.connect(self.slot_checkFilters)
self.ui.ch_effects.clicked.connect(self.slot_checkFilters)
self.ui.ch_instruments.clicked.connect(self.slot_checkFilters)
@@ -1704,6 +1755,7 @@ class PluginDatabaseW(QDialog):
self.ui.ch_dssi.setChecked(True)
self.ui.ch_lv2.setChecked(True)
self.ui.ch_vst.setChecked(True)
+ self.ui.ch_jsfx.setChecked(True)
self.ui.ch_kits.setChecked(True)
self.ui.ch_instruments.setChecked(True)
@@ -1762,6 +1814,7 @@ class PluginDatabaseW(QDialog):
settings.setValue("PluginDatabase/ShowVST2", self.ui.ch_vst.isChecked())
settings.setValue("PluginDatabase/ShowVST3", self.ui.ch_vst3.isChecked())
settings.setValue("PluginDatabase/ShowAU", self.ui.ch_au.isChecked())
+ settings.setValue("PluginDatabase/ShowJSFX", self.ui.ch_jsfx.isChecked())
settings.setValue("PluginDatabase/ShowKits", self.ui.ch_kits.isChecked())
settings.setValue("PluginDatabase/ShowNative", self.ui.ch_native.isChecked())
settings.setValue("PluginDatabase/ShowBridged", self.ui.ch_bridged.isChecked())
@@ -1822,6 +1875,7 @@ class PluginDatabaseW(QDialog):
self.ui.ch_vst.setChecked(settings.value("PluginDatabase/ShowVST2", True, bool))
self.ui.ch_vst3.setChecked(settings.value("PluginDatabase/ShowVST3", (MACOS or WINDOWS), bool))
self.ui.ch_au.setChecked(settings.value("PluginDatabase/ShowAU", MACOS, bool))
+ self.ui.ch_jsfx.setChecked(settings.value("PluginDatabase/ShowJSFX", True, bool))
self.ui.ch_kits.setChecked(settings.value("PluginDatabase/ShowKits", True, bool))
self.ui.ch_native.setChecked(settings.value("PluginDatabase/ShowNative", True, bool))
self.ui.ch_bridged.setChecked(settings.value("PluginDatabase/ShowBridged", True, bool))
@@ -1897,6 +1951,7 @@ class PluginDatabaseW(QDialog):
hideVST2 = not self.ui.ch_vst.isChecked()
hideVST3 = not self.ui.ch_vst3.isChecked()
hideAU = not self.ui.ch_au.isChecked()
+ hideJSFX = not self.ui.ch_jsfx.isChecked()
hideKits = not self.ui.ch_kits.isChecked()
hideNative = not self.ui.ch_native.isChecked()
@@ -1973,6 +2028,8 @@ class PluginDatabaseW(QDialog):
self.ui.tableWidget.hideRow(i)
elif hideAU and ptype == PLUGIN_AU:
self.ui.tableWidget.hideRow(i)
+ elif hideJSFX and ptype == PLUGIN_JSFX:
+ self.ui.tableWidget.hideRow(i)
elif hideNative and isNative:
self.ui.tableWidget.hideRow(i)
elif hideBridged and isBridged:
@@ -2012,7 +2069,7 @@ class PluginDatabaseW(QDialog):
def _addPluginToTable(self, plugin, ptype):
if plugin['API'] != PLUGIN_QUERY_API_VERSION:
return
- if ptype in (self.tr("Internal"), "LV2", "SF2", "SFZ"):
+ if ptype in (self.tr("Internal"), "LV2", "SF2", "SFZ", "JSFX"):
plugin['build'] = BINARY_NATIVE
index = self.fLastTableIndex
@@ -2048,6 +2105,7 @@ class PluginDatabaseW(QDialog):
#elif ptype == PLUGIN_SFZ:
#ptypeStr = "SFZ"
#ptypeStrTr = ptypeStr
+ # TODO(jsfx) what to do here?
else:
return 0
@@ -2159,6 +2217,11 @@ class PluginDatabaseW(QDialog):
auPlugins32 = settingsDB.value("Plugins/AU_posix32", [], list) if MACOS else []
+ # ----------------------------------------------------------------------------------------------------
+ # JSFX
+
+ jsfxPlugins = settingsDB.value("Plugins/JSFX", [], list)
+
# ----------------------------------------------------------------------------------------------------
# Kits
@@ -2173,6 +2236,7 @@ class PluginDatabaseW(QDialog):
vstCount = 0
vst3Count = 0
au32Count = 0
+ jsfxCount = len(jsfxPlugins)
sf2Count = 0
sfzCount = len(sfzs)
@@ -2195,15 +2259,15 @@ class PluginDatabaseW(QDialog):
sf2Count += len(plugins)
self.ui.tableWidget.setRowCount(self.fLastTableIndex +
- ladspaCount + dssiCount + vstCount + vst3Count + au32Count +
+ ladspaCount + dssiCount + vstCount + vst3Count + au32Count + jsfxCount +
sf2Count + sfzCount)
if MACOS:
- self.ui.label.setText(self.tr("Have %i Internal, %i LADSPA, %i DSSI, %i LV2, %i VST2, %i VST3 and %i AudioUnit plugins, plus %i Sound Kits" % (
- internalCount, ladspaCount, dssiCount, lv2Count, vstCount, vst3Count, auCount+au32Count, sf2Count+sfzCount)))
+ self.ui.label.setText(self.tr("Have %i Internal, %i LADSPA, %i DSSI, %i LV2, %i VST2, %i VST3, %i AudioUnit plugins and %i JSFX plugins, plus %i Sound Kits" % (
+ internalCount, ladspaCount, dssiCount, lv2Count, vstCount, vst3Count, auCount+au32Count, jsfxCount, sf2Count+sfzCount)))
else:
- self.ui.label.setText(self.tr("Have %i Internal, %i LADSPA, %i DSSI, %i LV2, %i VST2 and %i VST3 plugins, plus %i Sound Kits" % (
- internalCount, ladspaCount, dssiCount, lv2Count, vstCount, vst3Count, sf2Count+sfzCount)))
+ self.ui.label.setText(self.tr("Have %i Internal, %i LADSPA, %i DSSI, %i LV2, %i VST2, %i VST3 plugins and %i JSFX plugins, plus %i Sound Kits" % (
+ internalCount, ladspaCount, dssiCount, lv2Count, vstCount, vst3Count, jsfxCount, sf2Count+sfzCount)))
# ----------------------------------------------------------------------------------------------------
# now add all plugins to the table
@@ -2228,6 +2292,9 @@ class PluginDatabaseW(QDialog):
for plugin in plugins:
self._addPluginToTable(plugin, "AU")
+ for plugin in jsfxPlugins:
+ self._addPluginToTable(plugin, "JSFX")
+
for sf2 in sf2s:
for sf2_i in sf2:
self._addPluginToTable(sf2_i, "SF2")
diff --git a/source/frontend/carla_host.py b/source/frontend/carla_host.py
index 5c30b5eb4..8a8596f98 100644
--- a/source/frontend/carla_host.py
+++ b/source/frontend/carla_host.py
@@ -3393,6 +3393,7 @@ def setEngineSettings(host, oscPort = None):
VST3_PATH = settings.value(CARLA_KEY_PATHS_VST3, CARLA_DEFAULT_VST3_PATH, list)
SF2_PATH = settings.value(CARLA_KEY_PATHS_SF2, CARLA_DEFAULT_SF2_PATH, list)
SFZ_PATH = settings.value(CARLA_KEY_PATHS_SFZ, CARLA_DEFAULT_SFZ_PATH, list)
+ JSFX_PATH = settings.value(CARLA_KEY_PATHS_JSFX, CARLA_DEFAULT_JSFX_PATH, list)
host.set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_LADSPA, splitter.join(LADSPA_PATH))
host.set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_DSSI, splitter.join(DSSI_PATH))
@@ -3401,6 +3402,7 @@ def setEngineSettings(host, oscPort = None):
host.set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_VST3, splitter.join(VST3_PATH))
host.set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_SF2, splitter.join(SF2_PATH))
host.set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_SFZ, splitter.join(SFZ_PATH))
+ host.set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_JSFX, splitter.join(JSFX_PATH))
# --------------------------------------------------------------------------------------------------------
# don't continue if plugin
diff --git a/source/frontend/carla_settings.py b/source/frontend/carla_settings.py
index 82ee0459d..18923372f 100755
--- a/source/frontend/carla_settings.py
+++ b/source/frontend/carla_settings.py
@@ -45,7 +45,8 @@ from carla_backend import (
PLUGIN_VST2,
PLUGIN_VST3,
PLUGIN_SF2,
- PLUGIN_SFZ
+ PLUGIN_SFZ,
+ PLUGIN_JSFX
)
from carla_shared import (
@@ -97,6 +98,7 @@ from carla_shared import (
CARLA_KEY_PATHS_VST3,
CARLA_KEY_PATHS_SF2,
CARLA_KEY_PATHS_SFZ,
+ CARLA_KEY_PATHS_JSFX,
CARLA_KEY_WINE_EXECUTABLE,
CARLA_KEY_WINE_AUTO_PREFIX,
CARLA_KEY_WINE_FALLBACK_PREFIX,
@@ -171,6 +173,7 @@ from carla_shared import (
CARLA_DEFAULT_VST3_PATH,
CARLA_DEFAULT_SF2_PATH,
CARLA_DEFAULT_SFZ_PATH,
+ CARLA_DEFAULT_JSFX_PATH,
getAndSetPath,
getIcon,
fontMetricsHorizontalAdvance,
@@ -476,6 +479,7 @@ class CarlaSettingsW(QDialog):
PLUGINPATH_INDEX_VST3 = 4
PLUGINPATH_INDEX_SF2 = 5
PLUGINPATH_INDEX_SFZ = 6
+ PLUGINPATH_INDEX_JSFX = 7
# Single and Multiple client mode is only for JACK,
# but we still want to match QComboBox index to backend defines,
@@ -604,6 +608,7 @@ class CarlaSettingsW(QDialog):
self.ui.lw_vst3.currentRowChanged.connect(self.slot_pluginPathRowChanged)
self.ui.lw_sf2.currentRowChanged.connect(self.slot_pluginPathRowChanged)
self.ui.lw_sfz.currentRowChanged.connect(self.slot_pluginPathRowChanged)
+ self.ui.lw_jsfx.currentRowChanged.connect(self.slot_pluginPathRowChanged)
self.ui.b_filepaths_add.clicked.connect(self.slot_addFilePath)
self.ui.b_filepaths_remove.clicked.connect(self.slot_removeFilePath)
@@ -629,6 +634,7 @@ class CarlaSettingsW(QDialog):
self.ui.lw_vst3.setCurrentRow(0)
self.ui.lw_sf2.setCurrentRow(0)
self.ui.lw_sfz.setCurrentRow(0)
+ self.ui.lw_jsfx.setCurrentRow(0)
self.ui.lw_files_audio.setCurrentRow(0)
self.ui.lw_files_midi.setCurrentRow(0)
@@ -839,6 +845,7 @@ class CarlaSettingsW(QDialog):
vst3s = settings.value(CARLA_KEY_PATHS_VST3, CARLA_DEFAULT_VST3_PATH, list)
sf2s = settings.value(CARLA_KEY_PATHS_SF2, CARLA_DEFAULT_SF2_PATH, list)
sfzs = settings.value(CARLA_KEY_PATHS_SFZ, CARLA_DEFAULT_SFZ_PATH, list)
+ jsfxs = settings.value(CARLA_KEY_PATHS_JSFX, CARLA_DEFAULT_JSFX_PATH, list)
ladspas.sort()
dssis.sort()
@@ -847,6 +854,7 @@ class CarlaSettingsW(QDialog):
vst3s.sort()
sf2s.sort()
sfzs.sort()
+ jsfxs.sort()
for ladspa in ladspas:
if not ladspa:
@@ -883,6 +891,11 @@ class CarlaSettingsW(QDialog):
continue
self.ui.lw_sfz.addItem(sfz)
+ for jsfx in jsfxs:
+ if not jsfx:
+ continue
+ self.ui.lw_jsfx.addItem(jsfx)
+
# -------------------------------------------------------------------------------------------------------------
# Wine
@@ -1053,6 +1066,7 @@ class CarlaSettingsW(QDialog):
vst3s = []
sf2s = []
sfzs = []
+ jsfxs = []
for i in range(self.ui.lw_ladspa.count()):
ladspas.append(self.ui.lw_ladspa.item(i).text())
@@ -1075,6 +1089,9 @@ class CarlaSettingsW(QDialog):
for i in range(self.ui.lw_sfz.count()):
sfzs.append(self.ui.lw_sfz.item(i).text())
+ for i in range(self.ui.lw_jsfx.count()):
+ jsfxs.append(self.ui.lw_jsfx.item(i).text())
+
self.host.set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_LADSPA, splitter.join(ladspas))
self.host.set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_DSSI, splitter.join(dssis))
self.host.set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_LV2, splitter.join(lv2s))
@@ -1082,6 +1099,7 @@ class CarlaSettingsW(QDialog):
self.host.set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_VST3, splitter.join(vst3s))
self.host.set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_SF2, splitter.join(sf2s))
self.host.set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_SFZ, splitter.join(sfzs))
+ self.host.set_engine_option(ENGINE_OPTION_PLUGIN_PATH, PLUGIN_JSFX, splitter.join(jsfxs))
settings.setValue(CARLA_KEY_PATHS_LADSPA, ladspas)
settings.setValue(CARLA_KEY_PATHS_DSSI, dssis)
@@ -1090,6 +1108,7 @@ class CarlaSettingsW(QDialog):
settings.setValue(CARLA_KEY_PATHS_VST3, vst3s)
settings.setValue(CARLA_KEY_PATHS_SF2, sf2s)
settings.setValue(CARLA_KEY_PATHS_SFZ, sfzs)
+ settings.setValue(CARLA_KEY_PATHS_JSFX, jsfxs)
# -------------------------------------------------------------------------------------------------------------
# Wine
@@ -1277,6 +1296,16 @@ class CarlaSettingsW(QDialog):
continue
self.ui.lw_sfz.addItem(path)
+ elif curIndex == self.PLUGINPATH_INDEX_JSFX:
+ paths = CARLA_DEFAULT_JSFX_PATH
+ paths.sort()
+ self.ui.lw_jsfx.clear()
+
+ for path in paths:
+ if not path:
+ continue
+ self.ui.lw_jsfx.addItem(path)
+
# -------------------------------------------------------------------------------------------------------------
# Wine
@@ -1405,6 +1434,8 @@ class CarlaSettingsW(QDialog):
self.ui.lw_sf2.addItem(newPath)
elif curIndex == self.PLUGINPATH_INDEX_SFZ:
self.ui.lw_sfz.addItem(newPath)
+ elif curIndex == self.PLUGINPATH_INDEX_JSFX:
+ self.ui.lw_jsfx.addItem(newPath)
@pyqtSlot()
def slot_removePluginPath(self):
@@ -1424,6 +1455,8 @@ class CarlaSettingsW(QDialog):
self.ui.lw_sf2.takeItem(self.ui.lw_sf2.currentRow())
elif curIndex == self.PLUGINPATH_INDEX_SFZ:
self.ui.lw_sfz.takeItem(self.ui.lw_sfz.currentRow())
+ elif curIndex == self.PLUGINPATH_INDEX_JSFX:
+ self.ui.lw_jsfx.takeItem(self.ui.lw_jsfx.currentRow())
@pyqtSlot()
def slot_changePluginPath(self):
@@ -1443,6 +1476,8 @@ class CarlaSettingsW(QDialog):
currentPath = self.ui.lw_sf2.currentItem().text()
elif curIndex == self.PLUGINPATH_INDEX_SFZ:
currentPath = self.ui.lw_sfz.currentItem().text()
+ elif curIndex == self.PLUGINPATH_INDEX_JSFX:
+ currentPath = self.ui.lw_jsfx.currentItem().text()
else:
currentPath = ""
@@ -1465,6 +1500,8 @@ class CarlaSettingsW(QDialog):
self.ui.lw_sf2.currentItem().setText(newPath)
elif curIndex == self.PLUGINPATH_INDEX_SFZ:
self.ui.lw_sfz.currentItem().setText(newPath)
+ elif curIndex == self.PLUGINPATH_INDEX_JSFX:
+ self.ui.lw_jsfx.currentItem().setText(newPath)
# -----------------------------------------------------------------------------------------------------------------
@@ -1484,6 +1521,8 @@ class CarlaSettingsW(QDialog):
row = self.ui.lw_sf2.currentRow()
elif index == self.PLUGINPATH_INDEX_SFZ:
row = self.ui.lw_sfz.currentRow()
+ elif index == self.PLUGINPATH_INDEX_JSFX:
+ row = self.ui.lw_jsfx.currentRow()
else:
row = -1
diff --git a/source/frontend/carla_shared.py b/source/frontend/carla_shared.py
index 6a02dc714..6647f0c96 100644
--- a/source/frontend/carla_shared.py
+++ b/source/frontend/carla_shared.py
@@ -233,6 +233,7 @@ CARLA_KEY_PATHS_VST2 = "Paths/VST2"
CARLA_KEY_PATHS_VST3 = "Paths/VST3"
CARLA_KEY_PATHS_SF2 = "Paths/SF2"
CARLA_KEY_PATHS_SFZ = "Paths/SFZ"
+CARLA_KEY_PATHS_JSFX = "Paths/JSFX"
CARLA_KEY_WINE_EXECUTABLE = "Wine/Executable" # str
CARLA_KEY_WINE_AUTO_PREFIX = "Wine/AutoPrefix" # bool
@@ -352,6 +353,7 @@ DEFAULT_VST2_PATH = ""
DEFAULT_VST3_PATH = ""
DEFAULT_SF2_PATH = ""
DEFAULT_SFZ_PATH = ""
+DEFAULT_JSFX_PATH = ""
if WINDOWS:
splitter = ";"
@@ -388,6 +390,9 @@ if WINDOWS:
DEFAULT_VST2_PATH = PROGRAMFILES + "\\VstPlugins"
DEFAULT_VST2_PATH += ";" + PROGRAMFILES + "\\Steinberg\\VstPlugins"
+ DEFAULT_JSFX_PATH = APPDATA + "\\REAPER\Effects"
+ #DEFAULT_JSFX_PATH += ";" + PROGRAMFILES + "\\REAPER\\InstallData\\Effects"
+
if kIs64bit:
DEFAULT_VST2_PATH += ";" + COMMONPROGRAMFILES + "\\VST2"
@@ -402,6 +407,7 @@ if WINDOWS:
DEFAULT_DSSI_PATH += ";" + PROGRAMFILESx86 + "\\DSSI"
DEFAULT_VST2_PATH += ";" + PROGRAMFILESx86 + "\\VstPlugins"
DEFAULT_VST2_PATH += ";" + PROGRAMFILESx86 + "\\Steinberg\\VstPlugins"
+ #DEFAULT_JSFX_PATH += ";" + PROGRAMFILESx86 + "\\REAPER\\InstallData\\Effects"
if COMMONPROGRAMFILESx86:
DEFAULT_VST3_PATH += COMMONPROGRAMFILESx86 + "\\VST3"
@@ -444,7 +450,12 @@ elif MACOS:
DEFAULT_VST3_PATH = HOME + "/Library/Audio/Plug-Ins/VST3"
DEFAULT_VST3_PATH += ":/Library/Audio/Plug-Ins/VST3"
+ DEFAULT_JSFX_PATH = HOME + "/Library/Application Support/REAPER/Effects"
+ #DEFAULT_JSFX_PATH += ":/Applications/REAPER.app/Contents/InstallFiles/Effects"
+
else:
+ CONFIG_HOME = os.getenv("XDG_CONFIG_HOME", HOME + "/.config")
+
splitter = ":"
DEFAULT_LADSPA_PATH = HOME + "/.ladspa"
@@ -480,6 +491,9 @@ else:
DEFAULT_SFZ_PATH = HOME + "/.sounds/sfz"
DEFAULT_SFZ_PATH += ":/usr/share/sounds/sfz"
+ DEFAULT_JSFX_PATH = CONFIG_HOME + "/REAPER/Effects"
+ #DEFAULT_JSFX_PATH += ":" + "/opt/REAPER/InstallData/Effects"
+
if not WINDOWS:
winePrefix = os.getenv("WINEPREFIX")
@@ -527,6 +541,7 @@ if readEnvVars:
CARLA_DEFAULT_VST3_PATH = os.getenv("VST3_PATH", DEFAULT_VST3_PATH).split(splitter)
CARLA_DEFAULT_SF2_PATH = os.getenv("SF2_PATH", DEFAULT_SF2_PATH).split(splitter)
CARLA_DEFAULT_SFZ_PATH = os.getenv("SFZ_PATH", DEFAULT_SFZ_PATH).split(splitter)
+ CARLA_DEFAULT_JSFX_PATH = os.getenv("JSFX_PATH", DEFAULT_JSFX_PATH).split(splitter)
else:
CARLA_DEFAULT_LADSPA_PATH = DEFAULT_LADSPA_PATH.split(splitter)
@@ -536,6 +551,7 @@ else:
CARLA_DEFAULT_VST3_PATH = DEFAULT_VST3_PATH.split(splitter)
CARLA_DEFAULT_SF2_PATH = DEFAULT_SF2_PATH.split(splitter)
CARLA_DEFAULT_SFZ_PATH = DEFAULT_SFZ_PATH.split(splitter)
+ CARLA_DEFAULT_JSFX_PATH = DEFAULT_JSFX_PATH.split(splitter)
# ------------------------------------------------------------------------------------------------------------
# Default Plugin Folders (cleanup)
diff --git a/source/frontend/carla_utils.py b/source/frontend/carla_utils.py
index a430f9456..ea1079f19 100644
--- a/source/frontend/carla_utils.py
+++ b/source/frontend/carla_utils.py
@@ -43,6 +43,7 @@ from carla_backend import (
PLUGIN_VST2,
PLUGIN_VST3,
PLUGIN_AU,
+ PLUGIN_JSFX,
PLUGIN_DLS,
PLUGIN_GIG,
PLUGIN_SF2,
@@ -85,6 +86,8 @@ def getPluginTypeAsString(ptype):
return "VST3"
if ptype == PLUGIN_AU:
return "AU"
+ if ptype == PLUGIN_JSFX:
+ return "JSFX"
if ptype == PLUGIN_DLS:
return "DLS"
if ptype == PLUGIN_GIG:
@@ -123,6 +126,8 @@ def getPluginTypeFromString(stype):
return PLUGIN_VST3
if stype in ("au", "audiounit"):
return PLUGIN_AU
+ if stype == "jsfx":
+ return PLUGIN_JSFX
if stype == "dls":
return PLUGIN_DLS
if stype == "gig":
diff --git a/source/modules/ysfx/COPYING b/source/modules/ysfx/COPYING
new file mode 100644
index 000000000..85d81ada5
--- /dev/null
+++ b/source/modules/ysfx/COPYING
@@ -0,0 +1,13 @@
+Copyright 2021 Jean Pierre Cimalando
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/source/modules/ysfx/LICENSE-2.0.txt b/source/modules/ysfx/LICENSE-2.0.txt
new file mode 100644
index 000000000..d64569567
--- /dev/null
+++ b/source/modules/ysfx/LICENSE-2.0.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/source/modules/ysfx/Makefile b/source/modules/ysfx/Makefile
new file mode 100644
index 000000000..2bb986f82
--- /dev/null
+++ b/source/modules/ysfx/Makefile
@@ -0,0 +1,169 @@
+#!/usr/bin/make -f
+# Makefile for eel2 #
+# ----------------- #
+# Created by falkTX
+#
+
+CWD=../..
+MODULENAME=ysfx
+include ../Makefile.mk
+
+# ---------------------------------------------------------------------------------------------------------------------
+
+BUILD_C_FLAGS += $(YSFX_FLAGS)
+BUILD_C_FLAGS += $(YSFX_GRAPHICS_FLAGS)
+BUILD_C_FLAGS += -Isources
+BUILD_C_FLAGS += -Ithirdparty/WDL/source -Ithirdparty/dr_libs -Ithirdparty/stb
+BUILD_C_FLAGS += -DYSFX_NO_STANDARD_MUTEX
+BUILD_C_FLAGS += -DEELSCRIPT_NO_NET
+BUILD_C_FLAGS += -DEELSCRIPT_NO_LICE
+BUILD_C_FLAGS += -DWDL_FFT_REALSIZE=8
+BUILD_C_FLAGS += -DWDL_LINEPARSE_ATOF=ysfx_wdl_atof
+BUILD_C_FLAGS += -DNSEEL_ATOF=ysfx_wdl_atof
+
+BUILD_CXX_FLAGS += $(YSFX_FLAGS)
+BUILD_CXX_FLAGS += $(YSFX_GRAPHICS_FLAGS)
+BUILD_CXX_FLAGS += -Isources
+BUILD_CXX_FLAGS += -Ithirdparty/WDL/source -Ithirdparty/dr_libs -Ithirdparty/stb
+BUILD_CXX_FLAGS += -DYSFX_NO_STANDARD_MUTEX
+BUILD_CXX_FLAGS += -DEELSCRIPT_NO_NET
+BUILD_CXX_FLAGS += -DEELSCRIPT_NO_LICE
+BUILD_CXX_FLAGS += -DWDL_FFT_REALSIZE=8
+BUILD_CXX_FLAGS += -DWDL_LINEPARSE_ATOF=ysfx_wdl_atof
+BUILD_CXX_FLAGS += -DNSEEL_ATOF=ysfx_wdl_atof
+
+ifneq ($(WIN32),true)
+# NOTE: not compatible with MingGW, breaks win32_utf8
+BUILD_C_FLAGS += -D_FILE_OFFSET_BITS=64
+BUILD_CXX_FLAGS += -D_FILE_OFFSET_BITS=64
+endif
+
+ifeq ($(WIN32),true)
+BUILD_C_FLAGS += -DNOMINMAX
+BUILD_CXX_FLAGS += -DNOMINMAX
+endif
+
+ifneq ($(WIN32),true)
+ifneq ($(MACOS),true)
+BUILD_C_FLAGS += -DSWELL_LICE_GDI -DSWELL_FONTCONFIG -DSWELL_FREETYPE
+BUILD_CXX_FLAGS += -DSWELL_LICE_GDI -DSWELL_FONTCONFIG -DSWELL_FREETYPE
+endif
+endif
+
+# ---------------------------------------------------------------------------------------------------------------------
+
+OBJS = $(OBJDIR)/sources/ysfx.cpp.o
+OBJS += $(OBJDIR)/sources/ysfx_api_eel.cpp.o
+OBJS += $(OBJDIR)/sources/ysfx_api_file.cpp.o
+OBJS += $(OBJDIR)/sources/ysfx_api_gfx.cpp.o
+OBJS += $(OBJDIR)/sources/ysfx_api_reaper.cpp.o
+OBJS += $(OBJDIR)/sources/ysfx_audio_flac.cpp.o
+OBJS += $(OBJDIR)/sources/ysfx_audio_wav.cpp.o
+OBJS += $(OBJDIR)/sources/ysfx_config.cpp.o
+OBJS += $(OBJDIR)/sources/ysfx_eel_utils.cpp.o
+OBJS += $(OBJDIR)/sources/ysfx_midi.cpp.o
+OBJS += $(OBJDIR)/sources/ysfx_parse.cpp.o
+OBJS += $(OBJDIR)/sources/ysfx_reader.cpp.o
+OBJS += $(OBJDIR)/sources/ysfx_utils.cpp.o
+OBJS += $(OBJDIR)/sources/ysfx_utils_fts.cpp.o
+OBJS += $(OBJDIR)/sources/eel2-gas/sources/asm-nseel-x64-sse.S.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/eel2/nseel-caltab.c.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/eel2/nseel-cfunc.c.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/eel2/nseel-compiler.c.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/eel2/nseel-eval.c.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/eel2/nseel-lextab.c.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/eel2/nseel-ram.c.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/eel2/nseel-yylex.c.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/fft.c.o
+ifeq ($(WIN32),true)
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/win32_utf8.c.o
+endif
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/lice/lice.cpp.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/lice/lice_arc.cpp.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/lice/lice_colorspace.cpp.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/lice/lice_image.cpp.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/lice/lice_line.cpp.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/lice/lice_palette.cpp.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/lice/lice_texgen.cpp.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/lice/lice_text.cpp.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/lice/lice_textnew.cpp.o
+OBJS += $(OBJDIR)/sources/lice_stb/lice_stb_loaders.cpp.o
+OBJS += $(OBJDIR)/sources/lice_stb/lice_stb_bmp.cpp.o
+OBJS += $(OBJDIR)/sources/lice_stb/lice_stb_gif.cpp.o
+OBJS += $(OBJDIR)/sources/lice_stb/lice_stb_jpg.cpp.o
+OBJS += $(OBJDIR)/sources/lice_stb/lice_stb_png.cpp.o
+OBJS += $(OBJDIR)/sources/lice_stb/lice_stb_write.cpp.o
+ifneq ($(WIN32),true)
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/swell/swell-ini.cpp.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/swell/swell.cpp.o
+ifeq ($(MACOS),true)
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/swell/swell-appstub.mm.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/swell/swell-dlg.mm.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/swell/swell-gdi.mm.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/swell/swell-kb.mm.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/swell/swell-menu.mm.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/swell/swell-misc.mm.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/swell/swell-miscdlg.mm.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/swell/swell-modstub.mm.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/swell/swell-wnd.mm.o
+else
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/swell/swell-appstub-generic.cpp.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/swell/swell-dlg-generic.cpp.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/swell/swell-gdi-generic.cpp.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/swell/swell-gdi-lice.cpp.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/swell/swell-generic-gdk.cpp.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/swell/swell-generic-headless.cpp.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/swell/swell-kb-generic.cpp.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/swell/swell-menu-generic.cpp.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/swell/swell-misc-generic.cpp.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/swell/swell-miscdlg-generic.cpp.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/swell/swell-modstub-generic.cpp.o
+OBJS += $(OBJDIR)/thirdparty/WDL/source/WDL/swell/swell-wnd-generic.cpp.o
+endif
+endif
+
+# ---------------------------------------------------------------------------------------------------------------------
+
+all: $(MODULEDIR)/$(MODULENAME).a
+
+# ---------------------------------------------------------------------------------------------------------------------
+
+clean:
+ rm -f $(OBJS) $(MODULEDIR)/$(MODULENAME)*.a
+
+debug:
+ $(MAKE) DEBUG=true
+
+# ---------------------------------------------------------------------------------------------------------------------
+
+$(MODULEDIR)/$(MODULENAME).a: $(OBJS)
+ -@mkdir -p $(MODULEDIR)
+ @echo "Creating $(MODULENAME).a"
+ @rm -f $@
+ @$(AR) crs $@ $^
+
+# ---------------------------------------------------------------------------------------------------------------------
+
+$(OBJDIR)/%.c.o: %.c
+ -@mkdir -p $(dir $@)
+ @echo "Compiling $<"
+ @$(CC) $< $(BUILD_C_FLAGS) -c -o $@
+
+$(OBJDIR)/%.cpp.o: %.cpp
+ -@mkdir -p $(dir $@)
+ @echo "Compiling $<"
+ @$(CXX) $< $(BUILD_CXX_FLAGS) -c -o $@
+
+$(OBJDIR)/%.mm.o: %.mm
+ -@mkdir -p $(dir $@)
+ @echo "Compiling $<"
+ @$(CXX) $< $(BUILD_CXX_FLAGS) -c -o $@
+
+$(OBJDIR)/%.S.o: %.S
+ -@mkdir -p $(dir $@)
+ @echo "Compiling $<"
+ @$(CC) $< $(BUILD_ASM_FLAGS) -c -o $@
+
+-include $(OBJS:%.o=%.d)
+
+# ---------------------------------------------------------------------------------------------------------------------
diff --git a/source/modules/ysfx/include/ysfx.h b/source/modules/ysfx/include/ysfx.h
new file mode 100644
index 000000000..a85d228da
--- /dev/null
+++ b/source/modules/ysfx/include/ysfx.h
@@ -0,0 +1,484 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#pragma once
+#if !defined(YSFX_INCLUDED_YSFX_H)
+#define YSFX_INCLUDED_YSFX_H
+
+#include
+#include
+#include
+#include
+#if defined(_WIN32)
+# include
+#endif
+
+//------------------------------------------------------------------------------
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#if !defined(YSFX_API)
+# if defined(_WIN32) && defined(YSFX_DLL_BUILD)
+# define YSFX_API __declspec(dllexport)
+# elif defined(__GNUC__)
+# define YSFX_API __attribute__((visibility("default")))
+# else
+# define YSFX_API
+# endif
+#endif
+
+//------------------------------------------------------------------------------
+// YSFX definitions
+
+typedef double ysfx_real;
+
+enum {
+ ysfx_max_sliders = 64,
+ ysfx_max_channels = 64,
+ ysfx_max_midi_buses = 16,
+ ysfx_max_triggers = 10,
+};
+
+typedef enum ysfx_log_level_e {
+ ysfx_log_info,
+ ysfx_log_warning,
+ ysfx_log_error,
+} ysfx_log_level;
+
+typedef void (ysfx_log_reporter_t)(intptr_t userdata, ysfx_log_level level, const char *message);
+
+//------------------------------------------------------------------------------
+// YSFX configuration
+
+typedef struct ysfx_config_s ysfx_config_t;
+typedef struct ysfx_audio_format_s ysfx_audio_format_t;
+
+// create a new configuration
+YSFX_API ysfx_config_t *ysfx_config_new();
+// delete a configuration
+YSFX_API void ysfx_config_free(ysfx_config_t *config);
+// increase the reference counter
+YSFX_API void ysfx_config_add_ref(ysfx_config_t *config);
+// set the path of the import root, a folder usually named "Effects"
+YSFX_API void ysfx_set_import_root(ysfx_config_t *config, const char *root);
+// set the path of the data root, a folder usually named "Data"
+YSFX_API void ysfx_set_data_root(ysfx_config_t *config, const char *root);
+// get the path of the import root, a folder usually named "Effects"
+YSFX_API const char *ysfx_get_import_root(ysfx_config_t *config);
+// get the path of the data root, a folder usually named "Data"
+YSFX_API const char *ysfx_get_data_root(ysfx_config_t *config);
+// guess the undefined root folders, based on the path to the JSFX file
+YSFX_API void ysfx_guess_file_roots(ysfx_config_t *config, const char *sourcepath);
+// register an audio format into the system
+YSFX_API void ysfx_register_audio_format(ysfx_config_t *config, ysfx_audio_format_t *afmt);
+// register the builtin audio formats (at least WAV file support)
+YSFX_API void ysfx_register_builtin_audio_formats(ysfx_config_t *config);
+// set the log reporting function
+YSFX_API void ysfx_set_log_reporter(ysfx_config_t *config, ysfx_log_reporter_t *reporter);
+// set the callback user data
+YSFX_API void ysfx_set_user_data(ysfx_config_t *config, intptr_t userdata);
+
+// get a string which textually represents the log level
+YSFX_API const char *ysfx_log_level_string(ysfx_log_level level);
+
+//------------------------------------------------------------------------------
+// YSFX effect
+
+typedef struct ysfx_s ysfx_t;
+
+// create a new effect, taking a reference to config
+YSFX_API ysfx_t *ysfx_new(ysfx_config_t *config);
+// delete an effect
+YSFX_API void ysfx_free(ysfx_t *fx);
+// increase the reference counter
+YSFX_API void ysfx_add_ref(ysfx_t *fx);
+
+// get the configuration
+YSFX_API ysfx_config_t *ysfx_get_config(ysfx_t *fx);
+
+typedef enum ysfx_load_option_e {
+ // skip imports; useful just for accessing header information and nothing else
+ ysfx_load_ignoring_imports = 1,
+} ysfx_load_option_t;
+
+// load the source code from file without compiling
+YSFX_API bool ysfx_load_file(ysfx_t *fx, const char *filepath, uint32_t loadopts);
+// unload the source code and any compiled code
+YSFX_API void ysfx_unload(ysfx_t *fx);
+// check whether the effect is loaded
+YSFX_API bool ysfx_is_loaded(ysfx_t *fx);
+
+// get the name of the effect
+YSFX_API const char *ysfx_get_name(ysfx_t *fx);
+// get the path of the file which is loaded
+YSFX_API const char *ysfx_get_file_path(ysfx_t *fx);
+// get the author of the effect
+YSFX_API const char *ysfx_get_author(ysfx_t *fx);
+// get the number of tags of the effect
+#define ysfx_get_num_tags(fx) ysfx_get_tags((fx), NULL, 0)
+// get the list of tags of the effect
+YSFX_API uint32_t ysfx_get_tags(ysfx_t *fx, const char **dest, uint32_t destsize);
+// get a single tag of the effect
+YSFX_API const char *ysfx_get_tag(ysfx_t *fx, uint32_t index);
+// get the number of inputs
+YSFX_API uint32_t ysfx_get_num_inputs(ysfx_t *fx);
+// get the number of outputs
+YSFX_API uint32_t ysfx_get_num_outputs(ysfx_t *fx);
+// get the name of the input
+YSFX_API const char *ysfx_get_input_name(ysfx_t *fx, uint32_t index);
+// get the name of the output
+YSFX_API const char *ysfx_get_output_name(ysfx_t *fx, uint32_t index);
+// get whether this effect wants metering
+YSFX_API bool ysfx_wants_meters(ysfx_t *fx);
+// get requested dimensions of the graphics area; 0 means host should decide
+YSFX_API bool ysfx_get_gfx_dim(ysfx_t *fx, uint32_t dim[2]);
+
+typedef enum ysfx_section_type_e {
+ ysfx_section_init = 1,
+ ysfx_section_slider = 2,
+ ysfx_section_block = 3,
+ ysfx_section_sample = 4,
+ ysfx_section_gfx = 5,
+ ysfx_section_serialize = 6,
+} ysfx_section_type_t;
+
+// get whether the source has the given section
+YSFX_API bool ysfx_has_section(ysfx_t *fx, uint32_t type);
+
+typedef struct ysfx_slider_range_s {
+ ysfx_real def;
+ ysfx_real min;
+ ysfx_real max;
+ ysfx_real inc;
+} ysfx_slider_range_t;
+
+// determine if slider exists; call from 0 to max-1 to scan available ones
+YSFX_API bool ysfx_slider_exists(ysfx_t *fx, uint32_t index);
+// get the name of a slider
+YSFX_API const char *ysfx_slider_get_name(ysfx_t *fx, uint32_t index);
+// get the range of a slider
+YSFX_API bool ysfx_slider_get_range(ysfx_t *fx, uint32_t index, ysfx_slider_range_t *range);
+// get whether the slider is an enumeration
+YSFX_API bool ysfx_slider_is_enum(ysfx_t *fx, uint32_t index);
+// get the number of labels for the enumeration slider
+#define ysfx_slider_get_enum_size(fx, index) ysfx_slider_get_enum_names((fx), (index), NULL, 0)
+// get the list of labels for the enumeration slider
+YSFX_API uint32_t ysfx_slider_get_enum_names(ysfx_t *fx, uint32_t index, const char **dest, uint32_t destsize);
+// get a single label for the enumeration slider
+YSFX_API const char *ysfx_slider_get_enum_name(ysfx_t *fx, uint32_t slider_index, uint32_t enum_index);
+// get whether the slider is a path (implies enumeration)
+YSFX_API bool ysfx_slider_is_path(ysfx_t *fx, uint32_t index);
+// get whether the slider is initially visible
+YSFX_API bool ysfx_slider_is_initially_visible(ysfx_t *fx, uint32_t index);
+
+// get the value of the slider
+YSFX_API ysfx_real ysfx_slider_get_value(ysfx_t *fx, uint32_t index);
+// set the value of the slider, and call @slider later if value has changed
+YSFX_API void ysfx_slider_set_value(ysfx_t *fx, uint32_t index, ysfx_real value);
+
+typedef enum ysfx_compile_option_e {
+ // skip compiling the @serialize section
+ ysfx_compile_no_serialize = 1 << 0,
+ // skip compiling the @gfx section
+ ysfx_compile_no_gfx = 1 << 1,
+} ysfx_compile_option_t;
+
+// compile the previously loaded source
+YSFX_API bool ysfx_compile(ysfx_t *fx, uint32_t compileopts);
+// check whether the effect is compiled
+YSFX_API bool ysfx_is_compiled(ysfx_t *fx);
+
+// get the block size
+YSFX_API uint32_t ysfx_get_block_size(ysfx_t *fx);
+// get the sample rate
+YSFX_API ysfx_real ysfx_get_sample_rate(ysfx_t *fx);
+// update the block size; don't forget to call @init
+YSFX_API void ysfx_set_block_size(ysfx_t *fx, uint32_t blocksize);
+// update the sample rate; don't forget to call @init
+YSFX_API void ysfx_set_sample_rate(ysfx_t *fx, ysfx_real samplerate);
+
+// set the capacity of the MIDI buffer
+YSFX_API void ysfx_set_midi_capacity(ysfx_t *fx, uint32_t capacity, bool extensible);
+
+// activate and invoke @init
+YSFX_API void ysfx_init(ysfx_t *fx);
+
+// get the output latency
+YSFX_API ysfx_real ysfx_get_pdc_delay(ysfx_t *fx);
+// get the range of channels where output latency applies (end not included)
+YSFX_API void ysfx_get_pdc_channels(ysfx_t *fx, uint32_t channels[2]);
+// get whether the output latency applies to MIDI as well
+YSFX_API bool ysfx_get_pdc_midi(ysfx_t *fx);
+
+typedef enum ysfx_playback_state_e {
+ ysfx_playback_error = 0,
+ ysfx_playback_playing = 1,
+ ysfx_playback_paused = 2,
+ ysfx_playback_recording = 5,
+ ysfx_playback_recording_paused = 6,
+} ysfx_playback_state_t;
+
+typedef struct ysfx_time_info_s {
+ // tempo in beats/minute
+ ysfx_real tempo;
+ // state of the playback (ysfx_playback_state_t)
+ uint32_t playback_state;
+ // time position in seconds
+ ysfx_real time_position;
+ // time position in beats
+ ysfx_real beat_position;
+ // time signature in fraction form
+ uint32_t time_signature[2];
+} ysfx_time_info_t;
+
+// update time information; do this before processing the cycle
+YSFX_API void ysfx_set_time_info(ysfx_t *fx, const ysfx_time_info_t *info);
+
+typedef struct ysfx_midi_event_s {
+ // the bus number
+ uint32_t bus;
+ // the frame when it happens within the cycle
+ uint32_t offset;
+ // the size of the message
+ uint32_t size;
+ // the contents of the message
+ const uint8_t *data;
+} ysfx_midi_event_t;
+
+// send MIDI, it will be processed during the cycle
+YSFX_API bool ysfx_send_midi(ysfx_t *fx, const ysfx_midi_event_t *event);
+// receive MIDI, after having processed the cycle
+YSFX_API bool ysfx_receive_midi(ysfx_t *fx, ysfx_midi_event_t *event);
+// receive MIDI from a single bus (do not mix with API above, use either)
+YSFX_API bool ysfx_receive_midi_from_bus(ysfx_t *fx, uint32_t bus, ysfx_midi_event_t *event);
+
+// send a trigger, it will be processed during the cycle
+YSFX_API bool ysfx_send_trigger(ysfx_t *fx, uint32_t index);
+
+// get a bit mask of sliders whose values must be redisplayed, and clear it to zero
+YSFX_API uint64_t ysfx_fetch_slider_changes(ysfx_t *fx);
+// get a bit mask of sliders whose values must be automated, and clear it to zero
+YSFX_API uint64_t ysfx_fetch_slider_automations(ysfx_t *fx);
+// get a bit mask of sliders currently visible
+YSFX_API uint64_t ysfx_get_slider_visibility(ysfx_t *fx);
+
+// process a cycle in 32-bit float
+YSFX_API void ysfx_process_float(ysfx_t *fx, const float *const *ins, float *const *outs, uint32_t num_ins, uint32_t num_outs, uint32_t num_frames);
+// process a cycle in 64-bit float
+YSFX_API void ysfx_process_double(ysfx_t *fx, const double *const *ins, double *const *outs, uint32_t num_ins, uint32_t num_outs, uint32_t num_frames);
+
+typedef struct ysfx_state_slider_s {
+ // index of the slider
+ uint32_t index;
+ // value of the slider
+ ysfx_real value;
+} ysfx_state_slider_t;
+
+typedef struct ysfx_state_s {
+ // values of the sliders
+ ysfx_state_slider_t *sliders;
+ // number of sliders
+ uint32_t slider_count;
+ // serialized data
+ uint8_t *data;
+ // size of serialized data
+ size_t data_size;
+} ysfx_state_t;
+
+// load state
+YSFX_API bool ysfx_load_state(ysfx_t *fx, ysfx_state_t *state);
+// save current state; release this object when done
+YSFX_API ysfx_state_t *ysfx_save_state(ysfx_t *fx);
+// release a saved state object
+YSFX_API void ysfx_state_free(ysfx_state_t *state);
+// duplicate a state object
+YSFX_API ysfx_state_t *ysfx_state_dup(ysfx_state_t *state);
+
+typedef struct ysfx_preset_s {
+ // name of the preset
+ char *name;
+ // state of the preset
+ ysfx_state_t *state;
+} ysfx_preset_t;
+
+typedef struct ysfx_bank_s {
+ // name of the bank
+ char *name;
+ // list of presets
+ ysfx_preset_t *presets;
+ // number of programs
+ uint32_t preset_count;
+} ysfx_bank_t;
+
+// get the path of the RPL preset bank of the loaded JSFX, if present
+YSFX_API const char *ysfx_get_bank_path(ysfx_t *fx);
+// read a preset bank from RPL file
+YSFX_API ysfx_bank_t *ysfx_load_bank(const char *path);
+// free a preset bank
+YSFX_API void ysfx_bank_free(ysfx_bank_t *bank);
+
+// type of a function which can enumerate VM variables; returning 0 ends the search
+typedef int (ysfx_enum_vars_callback_t)(const char *name, ysfx_real *var, void *userdata);
+// enumerate all variables currently in the VM
+YSFX_API void ysfx_enum_vars(ysfx_t *fx, ysfx_enum_vars_callback_t *callback, void *userdata);
+// find a single variable in the VM
+YSFX_API ysfx_real *ysfx_find_var(ysfx_t *fx, const char *name);
+// read a chunk of virtual memory from the VM
+YSFX_API void ysfx_read_vmem(ysfx_t *fx, uint32_t addr, ysfx_real *dest, uint32_t count);
+
+//------------------------------------------------------------------------------
+// YSFX graphics
+
+// NOTE: all `ysfx_gfx_*` functions must be invoked from a dedicated UI thread
+
+typedef struct ysfx_gfx_config_s {
+ // opaque user data passed to callbacks
+ void *user_data;
+ // the width of the frame buffer (having the scale factor applied)
+ uint32_t pixel_width;
+ // the height of the frame buffer (having the scale factor applied)
+ uint32_t pixel_height;
+ // the distance in bytes between lines; if 0, it defaults to (4*width)
+ // currently it is required to be a multiple of 4
+ uint32_t pixel_stride;
+ // the pixel data of the frame buffer, of size (stride*height) bytes
+ // the byte order in little-endian is 'BGRA', big-endian is 'ARGB'
+ uint8_t *pixels;
+ // the scale factor of the display; 1.0 or greater, 2.0 for Retina display
+ ysfx_real scale_factor;
+ // show a menu and run it synchronously; returns an item ID >= 1, or 0 if none
+ int32_t (*show_menu)(void *user_data, const char *menu_spec, int32_t xpos, int32_t ypos);
+ // change the cursor
+ void (*set_cursor)(void *user_data, int32_t cursor);
+ // if index is not -1, get the dropped file at this index (otherwise null)
+ // if index is -1, clear the list of dropped files, and return null
+ const char *(*get_drop_file)(void *user_data, int32_t index);
+} ysfx_gfx_config_t;
+
+// set up the graphics rendering
+YSFX_API void ysfx_gfx_setup(ysfx_t *fx, ysfx_gfx_config_t *gc);
+// get whether the current effect is requesting Retina support
+YSFX_API bool ysfx_gfx_wants_retina(ysfx_t *fx);
+// push a key to the input queue
+YSFX_API void ysfx_gfx_add_key(ysfx_t *fx, uint32_t mods, uint32_t key, bool press);
+// update mouse information; position is relative to canvas; wheel should be in steps normalized to ±1.0
+YSFX_API void ysfx_gfx_update_mouse(ysfx_t *fx, uint32_t mods, int32_t xpos, int32_t ypos, uint32_t buttons, ysfx_real wheel, ysfx_real hwheel);
+// invoke @gfx to paint the graphics; returns whether the framer buffer is modified
+YSFX_API bool ysfx_gfx_run(ysfx_t *fx);
+
+//------------------------------------------------------------------------------
+// YSFX key map
+
+// these key definitions match those of pugl
+
+enum {
+ ysfx_mod_shift = 1 << 0,
+ ysfx_mod_ctrl = 1 << 1,
+ ysfx_mod_alt = 1 << 2,
+ ysfx_mod_super = 1 << 3,
+};
+
+enum {
+ ysfx_key_backspace = 0x08,
+ ysfx_key_escape = 0x1b,
+ ysfx_key_delete = 0x7f,
+
+ ysfx_key_f1 = 0xe000,
+ ysfx_key_f2,
+ ysfx_key_f3,
+ ysfx_key_f4,
+ ysfx_key_f5,
+ ysfx_key_f6,
+ ysfx_key_f7,
+ ysfx_key_f8,
+ ysfx_key_f9,
+ ysfx_key_f10,
+ ysfx_key_f11,
+ ysfx_key_f12,
+ ysfx_key_left,
+ ysfx_key_up,
+ ysfx_key_right,
+ ysfx_key_down,
+ ysfx_key_page_up,
+ ysfx_key_page_down,
+ ysfx_key_home,
+ ysfx_key_end,
+ ysfx_key_insert,
+};
+
+enum {
+ ysfx_button_left = 1 << 0,
+ ysfx_button_middle = 1 << 1,
+ ysfx_button_right = 1 << 2,
+};
+
+//------------------------------------------------------------------------------
+// YSFX audio formats
+
+typedef struct ysfx_audio_reader_s ysfx_audio_reader_t;
+
+typedef struct ysfx_audio_file_info_s {
+ uint32_t channels;
+ ysfx_real sample_rate;
+} ysfx_audio_file_info_t;
+
+typedef struct ysfx_audio_format_s {
+ // quickly checks if this format would be able to handle the given file
+ bool (*can_handle)(const char *path);
+ // open an audio file of this format for reading
+ ysfx_audio_reader_t *(*open)(const char *path);
+ // close the audio file
+ void (*close)(ysfx_audio_reader_t *reader);
+ // get the sample rate and the channel count
+ ysfx_audio_file_info_t (*info)(ysfx_audio_reader_t *reader);
+ // get the number of samples left to read
+ uint64_t (*avail)(ysfx_audio_reader_t *reader);
+ // move the read pointer back to the beginning
+ void (*rewind)(ysfx_audio_reader_t *reader);
+ // read the next block of samples
+ uint64_t (*read)(ysfx_audio_reader_t *reader, ysfx_real *samples, uint64_t count);
+} ysfx_audio_format_t;
+
+//------------------------------------------------------------------------------
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+//------------------------------------------------------------------------------
+// YSFX RAII helpers
+
+#if defined(__cplusplus) && (__cplusplus >= 201103L || (defined(_MSC_VER) && _MSVC_LANG >= 201103L))
+#include
+
+#define YSFX_DEFINE_AUTO_PTR(aptr, styp, freefn) \
+ struct aptr##_deleter { \
+ void operator()(styp *x) const noexcept { freefn(x); } \
+ }; \
+ using aptr = std::unique_ptr
+
+YSFX_DEFINE_AUTO_PTR(ysfx_config_u, ysfx_config_t, ysfx_config_free);
+YSFX_DEFINE_AUTO_PTR(ysfx_u, ysfx_t, ysfx_free);
+YSFX_DEFINE_AUTO_PTR(ysfx_state_u, ysfx_state_t, ysfx_state_free);
+YSFX_DEFINE_AUTO_PTR(ysfx_bank_u, ysfx_bank_t, ysfx_bank_free);
+#endif // defined(__cplusplus) && (__cplusplus >= 201103L || (defined(_MSC_VER) && _MSVC_LANG >= 201103L))
+
+//------------------------------------------------------------------------------
+
+#endif // !defined(YSFX_INCLUDED_YSFX_H)
diff --git a/source/modules/ysfx/sources/base64/Base64.hpp b/source/modules/ysfx/sources/base64/Base64.hpp
new file mode 100644
index 000000000..fcdd8ce6e
--- /dev/null
+++ b/source/modules/ysfx/sources/base64/Base64.hpp
@@ -0,0 +1,139 @@
+/*
+ * DISTRHO Plugin Framework (DPF)
+ * Copyright (C) 2012-2016 Filipe Coelho
+ * Copyright (C) 2022 Jean Pierre Cimalando
+ *
+ * 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.
+ */
+
+#pragma once
+#include
+#include
+#include
+#include
+#include
+
+// -----------------------------------------------------------------------
+// base64 stuff, based on http://www.adp-gmbh.ch/cpp/common/base64.html
+
+/*
+ Copyright (C) 2004-2008 René Nyffenegger
+
+ This source code is provided 'as-is', without any express or implied
+ warranty. In no event will the author be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this source code must not be misrepresented; you must not
+ claim that you wrote the original source code. If you use this source code
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original source code.
+
+ 3. This notice may not be removed or altered from any source distribution.
+
+ René Nyffenegger rene.nyffenegger@adp-gmbh.ch
+*/
+
+// -----------------------------------------------------------------------
+// Helpers
+
+#ifndef DOXYGEN
+namespace DistrhoBase64Helpers {
+
+static
+std::array createCharIndexTable()
+{
+ std::array table;
+ table.fill(-1);
+ int8_t index = 0;
+ for (uint8_t c = 'A'; c <= 'Z'; ++c) table[c] = index++;
+ for (uint8_t c = 'a'; c <= 'z'; ++c) table[c] = index++;
+ for (uint8_t c = '0'; c <= '9'; ++c) table[c] = index++;
+ table['+'] = index++;
+ table['/'] = index++;
+ return table;
+}
+
+static const std::array kCharIndexTable = createCharIndexTable();
+
+} // namespace DistrhoBase64Helpers
+#endif
+
+// -----------------------------------------------------------------------
+
+static inline
+std::vector d_getChunkFromBase64String(const char* const base64string, std::size_t base64stringLen = ~std::size_t(0))
+{
+ std::vector ret;
+
+ if (! base64string)
+ return ret;
+
+ uint32_t i=0, j=0;
+ uint32_t charArray3[3], charArray4[4];
+
+ if (base64stringLen == ~std::size_t(0))
+ base64stringLen = std::strlen(base64string);
+
+ ret.reserve(base64stringLen*3/4 + 4);
+
+ for (std::size_t l=0; l(c);
+
+ if (i == 4)
+ {
+ for (i=0; i<4; ++i)
+ charArray4[i] = static_cast(DistrhoBase64Helpers::kCharIndexTable[charArray4[i]]);
+
+ charArray3[0] = (charArray4[0] << 2) + ((charArray4[1] & 0x30) >> 4);
+ charArray3[1] = ((charArray4[1] & 0xf) << 4) + ((charArray4[2] & 0x3c) >> 2);
+ charArray3[2] = ((charArray4[2] & 0x3) << 6) + charArray4[3];
+
+ for (i=0; i<3; ++i)
+ ret.push_back(static_cast(charArray3[i]));
+
+ i = 0;
+ }
+ }
+
+ if (i != 0)
+ {
+ for (j=0; j(DistrhoBase64Helpers::kCharIndexTable[charArray4[j]]);
+
+ for (j=i; j<4; ++j)
+ charArray4[j] = 0;
+
+ charArray3[0] = (charArray4[0] << 2) + ((charArray4[1] & 0x30) >> 4);
+ charArray3[1] = ((charArray4[1] & 0xf) << 4) + ((charArray4[2] & 0x3c) >> 2);
+ charArray3[2] = ((charArray4[2] & 0x3) << 6) + charArray4[3];
+
+ for (j=0; i>0 && j(charArray3[j]));
+ }
+
+ return ret;
+}
diff --git a/source/modules/ysfx/sources/eel2-gas/LICENSE.txt b/source/modules/ysfx/sources/eel2-gas/LICENSE.txt
new file mode 100644
index 000000000..e880e13bb
--- /dev/null
+++ b/source/modules/ysfx/sources/eel2-gas/LICENSE.txt
@@ -0,0 +1,20 @@
+Copyright (C) 2005 and later Cockos Incorporated
+Copyright (C) 2021 and later Jean Pierre Cimalando
+
+Portions copyright other contributors, see each source file for more information
+
+This software is provided 'as-is', without any express or implied
+warranty. In no event will the authors be held liable for any damages
+arising from the use of this software.
+
+Permission is granted to anyone to use this software for any purpose,
+including commercial applications, and to alter it and redistribute it
+freely, subject to the following restrictions:
+
+1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+3. This notice may not be removed or altered from any source distribution.
diff --git a/source/modules/ysfx/sources/eel2-gas/sources/asm-nseel-x64-sse.S b/source/modules/ysfx/sources/eel2-gas/sources/asm-nseel-x64-sse.S
new file mode 100644
index 000000000..82bf6b542
--- /dev/null
+++ b/source/modules/ysfx/sources/eel2-gas/sources/asm-nseel-x64-sse.S
@@ -0,0 +1,1399 @@
+## Copyright (C) 2005 and later Cockos Incorporated
+## Copyright (C) 2021 and later Jean Pierre Cimalando
+##
+## Portions copyright other contributors, see each source file for more information
+##
+## This software is provided 'as-is', without any express or implied
+## warranty. In no event will the authors be held liable for any damages
+## arising from the use of this software.
+##
+## Permission is granted to anyone to use this software for any purpose,
+## including commercial applications, and to alter it and redistribute it
+## freely, subject to the following restrictions:
+##
+## 1. The origin of this software must not be misrepresented; you must not
+## claim that you wrote the original software. If you use this software
+## in a product, an acknowledgment in the product documentation would be
+## appreciated but is not required.
+## 2. Altered source versions must be plainly marked as such, and must not be
+## misrepresented as being the original software.
+## 3. This notice may not be removed or altered from any source distribution.
+##
+## SPDX-License-Identifier: Zlib
+##
+
+## EEL2 support assembly for x64 SSE (GAS version)
+##
+## This is a rewrite of `asm-nseel-x64-sse.asm` in preprocessed GAS format.
+## The outputs of GAS and NASM are expected to disassemble to identical code.
+
+#if defined(__x86_64__)
+// NOTE(jpc) build this code only on x64 processor
+// disabled on other architectures, which helps Apple universal builds
+
+//NOTE(jpc) system-specific definitions
+#if !defined(_WIN32)
+# define AMD64ABI 1
+#endif
+#if !defined(__APPLE__)
+# define MANGLE(x) x
+#else
+# define MANGLE(x) _##x
+#endif
+
+.intel_syntax noprefix
+
+## these must be synced with any changes in ns-eel-int.h
+#define NSEEL_RAM_BLOCKS_DEFAULTMAX 128
+#define NSEEL_RAM_BLOCKS_LOG2 9
+#define NSEEL_RAM_ITEMSPERBLOCK_LOG2 16
+#define NSEEL_RAM_BLOCKS (1 << NSEEL_RAM_BLOCKS_LOG2)
+#define NSEEL_RAM_ITEMSPERBLOCK (1< filename && *p != '\\' && *p != '/' && *p != '.')
+ p--;
+ if (stricmp(p, ".bmp"))
+ return nullptr;
+ }
+ return LICE_LoadSTB(filename, bmpbase);
+ }
+
+ static const char *get_extlist()
+ {
+ return "BMP files (*.BMP)\0*.BMP\0";
+ }
+};
+
+void lice_stb_install_bmp_loader()
+{
+ static LICE_stb_BMPLoader loader;
+}
diff --git a/source/modules/ysfx/sources/lice_stb/lice_stb_generic.hpp b/source/modules/ysfx/sources/lice_stb/lice_stb_generic.hpp
new file mode 100644
index 000000000..a00fa460c
--- /dev/null
+++ b/source/modules/ysfx/sources/lice_stb/lice_stb_generic.hpp
@@ -0,0 +1,76 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#define STB_IMAGE_IMPLEMENTATION
+#define STB_IMAGE_STATIC
+#define STBI_WINDOWS_UTF8
+
+#if defined(__GNUC__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wunused-function"
+#endif
+#include "stb_image.h"
+#if defined(__GNUC__)
+# pragma GCC diagnostic pop
+#endif
+
+//------------------------------------------------------------------------------
+#include "WDL/lice/lice.h"
+#include "WDL/wdltypes.h"
+
+static LICE_IBitmap *LICE_LoadSTB(const char *filename, LICE_IBitmap *bmp)
+{
+ LICE_IBitmap *delbmp = nullptr;
+ stbi_uc *srcpx = nullptr;
+ LICE_pixel *dstpx = nullptr;
+ bool dstflip = false;
+ unsigned dstspan = 0;
+ unsigned w = 0;
+ unsigned h = 0;
+ unsigned ch = 0;
+
+ srcpx = stbi_load(filename, (int *)&w, (int *)&h, (int *)&ch, 4);
+ if (!srcpx)
+ goto fail;
+
+ if (bmp)
+ bmp->resize(w, h);
+ else
+ bmp = delbmp = new WDL_NEW LICE_MemBitmap(w, h);
+
+ if (!bmp || (unsigned)bmp->getWidth() != w || (unsigned)bmp->getHeight() != h)
+ goto fail;
+
+ dstpx = bmp->getBits();
+ dstflip = bmp->isFlipped();
+ dstspan = bmp->getRowSpan();
+
+ for (unsigned row = 0; row < h; ++row) {
+ const stbi_uc *src = srcpx + row * (4 * w);
+ LICE_pixel *dst = dstpx + dstspan * (dstflip ? (h - 1 - row) : row);
+ for (unsigned col = 0; col < w; ++col, src += 4, ++dst)
+ *dst = LICE_RGBA(src[0], src[1], src[2], src[3]);
+ }
+
+ stbi_image_free(srcpx);
+ return bmp;
+
+fail:
+ delete delbmp;
+ stbi_image_free(srcpx);
+ return nullptr;
+}
diff --git a/source/modules/ysfx/sources/lice_stb/lice_stb_gif.cpp b/source/modules/ysfx/sources/lice_stb/lice_stb_gif.cpp
new file mode 100644
index 000000000..bdcea3e8a
--- /dev/null
+++ b/source/modules/ysfx/sources/lice_stb/lice_stb_gif.cpp
@@ -0,0 +1,67 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#define STBI_ONLY_GIF
+#include "lice_stb_generic.hpp"
+
+LICE_IBitmap *LICE_LoadGIF(const char *filename, LICE_IBitmap *bmp, int *nframes)
+{
+ LICE_IBitmap *ret = LICE_LoadSTB(filename, bmp);
+ if (ret) {
+ if (nframes) // animation not implemented
+ *nframes = 1;
+ }
+ return ret;
+}
+
+class LICE_stb_GIFLoader
+{
+ _LICE_ImageLoader_rec rec;
+
+public:
+ LICE_stb_GIFLoader()
+ {
+ rec.loadfunc = loadfunc;
+ rec.get_extlist = get_extlist;
+ rec._next = LICE_ImageLoader_list;
+ LICE_ImageLoader_list = &rec;
+ }
+
+ static LICE_IBitmap *loadfunc(const char *filename, bool checkFileName, LICE_IBitmap *bmpbase)
+ {
+ if (checkFileName) {
+ const char *p = filename;
+ while (*p)
+ p++;
+ while (p > filename && *p != '\\' && *p != '/' && *p != '.')
+ p--;
+ if (stricmp(p, ".gif"))
+ return nullptr;
+ }
+ return LICE_LoadSTB(filename, bmpbase);
+ }
+
+ static const char *get_extlist()
+ {
+ return "GIF files (*.GIF)\0*.GIF\0";
+ }
+};
+
+void lice_stb_install_gif_loader()
+{
+ static LICE_stb_GIFLoader loader;
+}
diff --git a/source/modules/ysfx/sources/lice_stb/lice_stb_jpg.cpp b/source/modules/ysfx/sources/lice_stb/lice_stb_jpg.cpp
new file mode 100644
index 000000000..8ae493342
--- /dev/null
+++ b/source/modules/ysfx/sources/lice_stb/lice_stb_jpg.cpp
@@ -0,0 +1,62 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#define STBI_ONLY_JPEG
+#include "lice_stb_generic.hpp"
+
+LICE_IBitmap *LICE_LoadJPG(const char *filename, LICE_IBitmap *bmp)
+{
+ return LICE_LoadSTB(filename, bmp);
+}
+
+class LICE_stb_JPGLoader
+{
+ _LICE_ImageLoader_rec rec;
+
+public:
+ LICE_stb_JPGLoader()
+ {
+ rec.loadfunc = loadfunc;
+ rec.get_extlist = get_extlist;
+ rec._next = LICE_ImageLoader_list;
+ LICE_ImageLoader_list = &rec;
+ }
+
+ static LICE_IBitmap *loadfunc(const char *filename, bool checkFileName, LICE_IBitmap *jpgbase)
+ {
+ if (checkFileName) {
+ const char *p = filename;
+ while (*p)
+ p++;
+ while (p > filename && *p != '\\' && *p != '/' && *p != '.')
+ p--;
+ if (stricmp(p, ".jpg") && stricmp(p, ".jpeg") && stricmp(p, ".jfif"))
+ return nullptr;
+ }
+ return LICE_LoadSTB(filename, jpgbase);
+ }
+
+ static const char *get_extlist()
+ {
+ return "JPEG files (*.JPG;*.JPEG;*.JFIF)\0*.JPG;*.JPEG;*.JFIF\0";
+ }
+};
+
+void lice_stb_install_jpg_loader()
+{
+ static LICE_stb_JPGLoader loader;
+}
diff --git a/source/modules/ysfx/sources/lice_stb/lice_stb_loaders.cpp b/source/modules/ysfx/sources/lice_stb/lice_stb_loaders.cpp
new file mode 100644
index 000000000..2f80f1388
--- /dev/null
+++ b/source/modules/ysfx/sources/lice_stb/lice_stb_loaders.cpp
@@ -0,0 +1,26 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#include "lice_stb_loaders.hpp"
+
+void lice_stb_install_loaders()
+{
+ lice_stb_install_bmp_loader();
+ lice_stb_install_gif_loader();
+ lice_stb_install_jpg_loader();
+ lice_stb_install_png_loader();
+}
diff --git a/source/modules/ysfx/sources/lice_stb/lice_stb_loaders.hpp b/source/modules/ysfx/sources/lice_stb/lice_stb_loaders.hpp
new file mode 100644
index 000000000..2ff28c564
--- /dev/null
+++ b/source/modules/ysfx/sources/lice_stb/lice_stb_loaders.hpp
@@ -0,0 +1,23 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+void lice_stb_install_loaders();
+
+void lice_stb_install_bmp_loader();
+void lice_stb_install_gif_loader();
+void lice_stb_install_jpg_loader();
+void lice_stb_install_png_loader();
diff --git a/source/modules/ysfx/sources/lice_stb/lice_stb_png.cpp b/source/modules/ysfx/sources/lice_stb/lice_stb_png.cpp
new file mode 100644
index 000000000..a750d36a3
--- /dev/null
+++ b/source/modules/ysfx/sources/lice_stb/lice_stb_png.cpp
@@ -0,0 +1,62 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#define STBI_ONLY_PNG
+#include "lice_stb_generic.hpp"
+
+LICE_IBitmap *LICE_LoadPNG(const char *filename, LICE_IBitmap *bmp)
+{
+ return LICE_LoadSTB(filename, bmp);
+}
+
+class LICE_stb_PNGLoader
+{
+ _LICE_ImageLoader_rec rec;
+
+public:
+ LICE_stb_PNGLoader()
+ {
+ rec.loadfunc = loadfunc;
+ rec.get_extlist = get_extlist;
+ rec._next = LICE_ImageLoader_list;
+ LICE_ImageLoader_list = &rec;
+ }
+
+ static LICE_IBitmap *loadfunc(const char *filename, bool checkFileName, LICE_IBitmap *bmpbase)
+ {
+ if (checkFileName) {
+ const char *p = filename;
+ while (*p)
+ p++;
+ while (p > filename && *p != '\\' && *p != '/' && *p != '.')
+ p--;
+ if (stricmp(p, ".png"))
+ return nullptr;
+ }
+ return LICE_LoadSTB(filename, bmpbase);
+ }
+
+ static const char *get_extlist()
+ {
+ return "PNG files (*.PNG)\0*.PNG\0";
+ }
+};
+
+void lice_stb_install_png_loader()
+{
+ static LICE_stb_PNGLoader loader;
+}
diff --git a/source/modules/ysfx/sources/lice_stb/lice_stb_write.cpp b/source/modules/ysfx/sources/lice_stb/lice_stb_write.cpp
new file mode 100644
index 000000000..5af3f8deb
--- /dev/null
+++ b/source/modules/ysfx/sources/lice_stb/lice_stb_write.cpp
@@ -0,0 +1,101 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#define STBIW_WINDOWS_UTF8
+#define STB_IMAGE_WRITE_STATIC
+#define STB_IMAGE_WRITE_IMPLEMENTATION
+
+#if defined(__GNUC__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wunused-function"
+#endif
+#include "stb_image_write.h"
+#if defined(__GNUC__)
+# pragma GCC diagnostic pop
+#endif
+
+//------------------------------------------------------------------------------
+#define WDL_NO_DEFINE_MINMAX
+#include "WDL/lice/lice.h"
+#include "WDL/wdltypes.h"
+#include
+
+static std::unique_ptr bmp_to_stbi(LICE_IBitmap *bmp, unsigned ch)
+{
+ unsigned w = (unsigned)bmp->getWidth();
+ unsigned h = (unsigned)bmp->getHeight();
+ unsigned srcspan = (unsigned)bmp->getRowSpan();
+ bool srcflip = bmp->isFlipped();
+ LICE_pixel *srcpx = bmp->getBits();
+ std::unique_ptr result;
+
+ if (ch == 4) {
+ unsigned char *dstpx = new unsigned char[4 * w * h];
+ result.reset(dstpx);
+ for (unsigned row = 0; row < h; ++row) {
+ LICE_pixel *src = srcpx + srcspan * (srcflip ? (h - 1 - row) : row);
+ unsigned char *dst = dstpx + row * (4 * w);
+ for (unsigned col = 0; col < w; ++col, ++src, dst += 4) {
+ LICE_pixel px = *src;
+ dst[0] = LICE_GETR(px);
+ dst[1] = LICE_GETG(px);
+ dst[2] = LICE_GETB(px);
+ dst[3] = LICE_GETA(px);
+ }
+ }
+ }
+ else if (ch == 3) {
+ unsigned char *dstpx = new unsigned char[3 * w * h];
+ result.reset(dstpx);
+ for (unsigned row = 0; row < h; ++row) {
+ LICE_pixel *src = srcpx + srcspan * (srcflip ? (h - 1 - row) : row);
+ unsigned char *dst = dstpx + row * (3 * w);
+ for (unsigned col = 0; col < w; ++col, ++src, dst += 3) {
+ LICE_pixel px = *src;
+ dst[0] = LICE_GETR(px);
+ dst[1] = LICE_GETG(px);
+ dst[2] = LICE_GETB(px);
+ }
+ }
+ }
+
+ return result;
+}
+
+bool LICE_WritePNG(const char *filename, LICE_IBitmap *bmp, bool wantalpha)
+{
+ unsigned ch = wantalpha ? 4 : 3;
+ std::unique_ptr data = bmp_to_stbi(bmp, ch);
+
+ if (!data)
+ return false;
+
+ return stbi_write_png(filename, bmp->getWidth(), bmp->getHeight(), (int)ch, data.get(), 0);
+}
+
+bool LICE_WriteJPG(const char *filename, LICE_IBitmap *bmp, int quality, bool force_baseline)
+{
+ (void)force_baseline; // always baseline
+
+ unsigned ch = 4;
+ std::unique_ptr data = bmp_to_stbi(bmp, ch);
+
+ if (!data)
+ return false;
+
+ return stbi_write_jpg(filename, bmp->getWidth(), bmp->getHeight(), (int)ch, data.get(), quality);
+}
diff --git a/source/modules/ysfx/sources/utility/sync_bitset.hpp b/source/modules/ysfx/sources/utility/sync_bitset.hpp
new file mode 100644
index 000000000..064c4b18d
--- /dev/null
+++ b/source/modules/ysfx/sources/utility/sync_bitset.hpp
@@ -0,0 +1,169 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#pragma once
+#include
+#include
+#include
+
+namespace ysfx {
+
+//------------------------------------------------------------------------------
+// sync_bitset64: A lock-free synchronized bitset of size 64
+//
+// This is implemented by a single qword on 64-bit machine, otherwise a pair of
+// dwords on 32-bit machine, which is to ensure lock-freedom.
+//
+// This bitset is synchronized but not atomic; a thread might see a
+// partially updated set after a modification. Conceptually, it can be seen as
+// atomic on individual bit flips (that is, masking operations do not race).
+
+class sync_bitset64_single;
+class sync_bitset64_dual;
+
+// could use std::atomic::is_always_lock_free on C++17 and up
+using sync_bitset64 = std::conditional<
+ sizeof(intptr_t) <= 4, sync_bitset64_dual, sync_bitset64_single>::type;
+
+//------------------------------------------------------------------------------
+class sync_bitset64_single {
+public:
+ uint64_t load() const
+ {
+ return bits_.load(std::memory_order_relaxed);
+ }
+
+ void store(uint64_t value)
+ {
+ bits_.store(value, std::memory_order_relaxed);
+ }
+
+ uint64_t exchange(uint64_t value)
+ {
+ return bits_.exchange(value, std::memory_order_relaxed);
+ }
+
+ uint64_t fetch_or(uint64_t value)
+ {
+ return bits_.fetch_or(value, std::memory_order_relaxed);
+ }
+
+ uint64_t fetch_and(uint64_t value)
+ {
+ return bits_.fetch_and(value, std::memory_order_relaxed);
+ }
+
+ uint64_t fetch_xor(uint64_t value)
+ {
+ return bits_.fetch_xor(value, std::memory_order_relaxed);
+ }
+
+ void operator|=(uint64_t value)
+ {
+ fetch_or(value);
+ }
+
+ void operator&=(uint64_t value)
+ {
+ fetch_and(value);
+ }
+
+ void operator^=(uint64_t value)
+ {
+ fetch_xor(value);
+ }
+
+private:
+ std::atomic bits_{0};
+};
+
+//------------------------------------------------------------------------------
+class sync_bitset64_dual {
+public:
+ uint64_t load() const
+ {
+ return join(lobits_.load(std::memory_order_relaxed),
+ hibits_.load(std::memory_order_relaxed));
+ }
+
+ void store(uint64_t value)
+ {
+ lobits_.store(lo(value), std::memory_order_relaxed);
+ hibits_.store(hi(value), std::memory_order_relaxed);
+ }
+
+ uint64_t exchange(uint64_t value)
+ {
+ return join(lobits_.exchange(lo(value), std::memory_order_relaxed),
+ hibits_.exchange(hi(value), std::memory_order_relaxed));
+ }
+
+ uint64_t fetch_or(uint64_t value)
+ {
+ return join(lobits_.fetch_or(lo(value), std::memory_order_relaxed),
+ hibits_.fetch_or(hi(value), std::memory_order_relaxed));
+ }
+
+ uint64_t fetch_and(uint64_t value)
+ {
+ return join(lobits_.fetch_and(lo(value), std::memory_order_relaxed),
+ hibits_.fetch_and(hi(value), std::memory_order_relaxed));
+ }
+
+ uint64_t fetch_xor(uint64_t value)
+ {
+ return join(lobits_.fetch_xor(lo(value), std::memory_order_relaxed),
+ hibits_.fetch_xor(hi(value), std::memory_order_relaxed));
+ }
+
+ void operator|=(uint64_t value)
+ {
+ fetch_or(value);
+ }
+
+ void operator&=(uint64_t value)
+ {
+ fetch_and(value);
+ }
+
+ void operator^=(uint64_t value)
+ {
+ fetch_xor(value);
+ }
+
+private:
+ static constexpr uint64_t join(uint32_t lo, uint32_t hi)
+ {
+ return (uint64_t)lo | ((uint64_t)hi << 32);
+ }
+
+ static constexpr uint32_t lo(uint64_t value)
+ {
+ return (uint32_t)(value & 0xFFFFFFFFu);
+ }
+
+ static constexpr uint32_t hi(uint64_t value)
+ {
+ return (uint32_t)(value >> 32);
+ }
+
+private:
+ std::atomic lobits_{0};
+ std::atomic hibits_{0};
+};
+
+} // namespace ysfx
diff --git a/source/modules/ysfx/sources/ysfx.cpp b/source/modules/ysfx/sources/ysfx.cpp
new file mode 100644
index 000000000..368d2ac88
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx.cpp
@@ -0,0 +1,1540 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#include "ysfx.hpp"
+#include "ysfx_config.hpp"
+#include "ysfx_eel_utils.hpp"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+static_assert(std::is_same::value,
+ "ysfx_real is incorrectly defined");
+
+enum {
+ ysfx_max_file_handles = 64, // change if it needs more
+};
+
+//------------------------------------------------------------------------------
+static thread_local ysfx_thread_id_t ysfx_thread_id;
+
+ysfx_thread_id_t ysfx_get_thread_id()
+{
+ return ysfx_thread_id;
+}
+
+void ysfx_set_thread_id(ysfx_thread_id_t id)
+{
+ ysfx_thread_id = id;
+}
+
+//------------------------------------------------------------------------------
+struct ysfx_api_initializer {
+private:
+ ysfx_api_initializer();
+ ~ysfx_api_initializer();
+public:
+ static void init_once();
+};
+
+ysfx_api_initializer::ysfx_api_initializer()
+{
+ if (NSEEL_init() != 0)
+ throw std::runtime_error("NSEEL_init");
+
+ ysfx_api_init_eel();
+ ysfx_api_init_reaper();
+ ysfx_api_init_file();
+ ysfx_api_init_gfx();
+}
+
+ysfx_api_initializer::~ysfx_api_initializer()
+{
+ NSEEL_quit();
+}
+
+void ysfx_api_initializer::init_once()
+{
+ static ysfx_api_initializer init;
+}
+
+//------------------------------------------------------------------------------
+ysfx_t *ysfx_new(ysfx_config_t *config)
+{
+ ysfx_u fx{new ysfx_t};
+
+ ysfx_config_add_ref(config);
+ fx->config.reset(config);
+
+ fx->string_ctx.reset(ysfx_eel_string_context_new());
+
+ ysfx_api_initializer::init_once();
+
+ NSEEL_VMCTX vm = NSEEL_VM_alloc();
+ if (!vm)
+ throw std::bad_alloc();
+ fx->vm.reset(vm);
+
+ NSEEL_VM_SetCustomFuncThis(vm, fx.get());
+
+ ysfx_eel_string_initvm(vm);
+
+#if !defined(YSFX_NO_GFX)
+ fx->gfx.state.reset(ysfx_gfx_state_new(fx.get()));
+#endif
+
+ auto var_resolver = [](void *userdata, const char *name) -> EEL_F * {
+ ysfx_t *fx = (ysfx_t *)userdata;
+ auto it = fx->source.slider_alias.find(name);
+ if (it != fx->source.slider_alias.end())
+ return fx->var.slider[it->second];
+ return nullptr;
+ };
+ NSEEL_VM_set_var_resolver(vm, var_resolver, fx.get());
+
+ for (uint32_t i = 0; i < ysfx_max_channels; ++i) {
+ std::string name = "spl" + std::to_string(i);
+ EEL_F *var = NSEEL_VM_regvar(vm, name.c_str());
+ *(fx->var.spl[i] = var) = 0;
+ }
+ for (uint32_t i = 0; i < ysfx_max_sliders; ++i) {
+ std::string name = "slider" + std::to_string(i + 1);
+ EEL_F *var = NSEEL_VM_regvar(vm, name.c_str());
+ *(fx->var.slider[i] = var) = 0;
+ fx->slider_of_var[var] = i;
+ }
+
+ #define AUTOVAR(name, value) *(fx->var.name = NSEEL_VM_regvar(vm, #name)) = (value)
+ AUTOVAR(srate, fx->sample_rate);
+ AUTOVAR(num_ch, fx->valid_input_channels);
+ AUTOVAR(samplesblock, fx->block_size);
+ AUTOVAR(trigger, 0);
+ AUTOVAR(tempo, 120);
+ AUTOVAR(play_state, 1);
+ AUTOVAR(play_position, 0);
+ AUTOVAR(beat_position, 0);
+ AUTOVAR(ts_num, 0);
+ AUTOVAR(ts_denom, 4);
+ AUTOVAR(ext_noinit, 0);
+ AUTOVAR(ext_nodenorm, 0);
+ AUTOVAR(ext_midi_bus, 0);
+ AUTOVAR(midi_bus, 0);
+ AUTOVAR(pdc_delay, 0);
+ AUTOVAR(pdc_bot_ch, 0);
+ AUTOVAR(pdc_top_ch, 0);
+ AUTOVAR(pdc_midi, 0);
+ // gfx variables
+ AUTOVAR(gfx_r, 0);
+ AUTOVAR(gfx_g, 0);
+ AUTOVAR(gfx_b, 0);
+ AUTOVAR(gfx_a, 0);
+ AUTOVAR(gfx_a2, 0);
+ AUTOVAR(gfx_w, 0);
+ AUTOVAR(gfx_h, 0);
+ AUTOVAR(gfx_x, 0);
+ AUTOVAR(gfx_y, 0);
+ AUTOVAR(gfx_mode, 0);
+ AUTOVAR(gfx_clear, 0);
+ AUTOVAR(gfx_texth, 0);
+ AUTOVAR(gfx_dest, 0);
+ AUTOVAR(gfx_ext_retina, 0);
+ AUTOVAR(mouse_x, 0);
+ AUTOVAR(mouse_y, 0);
+ AUTOVAR(mouse_cap, 0);
+ AUTOVAR(mouse_wheel, 0);
+ AUTOVAR(mouse_hwheel, 0);
+ #undef AUTOVAR
+
+ fx->midi.in.reset(new ysfx_midi_buffer_t);
+ fx->midi.out.reset(new ysfx_midi_buffer_t);
+ ysfx_set_midi_capacity(fx.get(), 1024, true);
+
+ fx->file.list.reserve(16);
+ fx->file.list.emplace_back(new ysfx_serializer_t(fx->vm.get()));
+
+ return fx.release();
+}
+
+void ysfx_free(ysfx_t *fx)
+{
+ if (!fx)
+ return;
+
+ if (fx->ref_count.fetch_sub(1, std::memory_order_acq_rel) == 1)
+ delete fx;
+}
+
+void ysfx_add_ref(ysfx_t *fx)
+{
+ fx->ref_count.fetch_add(1, std::memory_order_relaxed);
+}
+
+ysfx_config_t *ysfx_get_config(ysfx_t *fx)
+{
+ return fx->config.get();
+}
+
+bool ysfx_load_file(ysfx_t *fx, const char *filepath, uint32_t loadopts)
+{
+ ysfx_unload(fx);
+
+ //--------------------------------------------------------------------------
+ // failure guard
+
+ auto fail_guard = ysfx::defer([fx]() { ysfx_unload_source(fx); });
+
+ //--------------------------------------------------------------------------
+ // load the main file
+
+ ysfx::file_uid main_uid;
+
+ {
+ ysfx_source_unit_u main{new ysfx_source_unit_t};
+
+ ysfx::FILE_u stream{ysfx::fopen_utf8(filepath, "rb")};
+ if (!stream || !ysfx::get_stream_file_uid(stream.get(), main_uid)) {
+ ysfx_logf(*fx->config, ysfx_log_error, "%s: cannot open file for reading", ysfx::path_file_name(filepath).c_str());
+ return false;
+ }
+
+ ysfx::stdio_text_reader reader(stream.get());
+
+ ysfx_parse_error error;
+ if (!ysfx_parse_toplevel(reader, main->toplevel, &error)) {
+ ysfx_logf(*fx->config, ysfx_log_error, "%s:%u: %s", ysfx::path_file_name(filepath).c_str(), error.line + 1, error.message.c_str());
+ return false;
+ }
+ ysfx_parse_header(main->toplevel.header.get(), main->header);
+
+ // validity check
+ if (main->header.desc.empty()) {
+ ysfx_logf(*fx->config, ysfx_log_warning, "%s: the required `desc` field is missing", ysfx::path_file_name(filepath).c_str());
+ main->header.desc = ysfx::path_file_name(filepath);
+ }
+
+ if (loadopts & ysfx_load_ignoring_imports)
+ main->header.imports.clear();
+
+ // if no pins are specified and we have @sample, the default is stereo
+ if (main->toplevel.sample && !main->header.explicit_pins &&
+ main->header.in_pins.empty() && main->header.out_pins.empty())
+ {
+ main->header.in_pins = {"JS input 1", "JS input 2"};
+ main->header.out_pins = {"JS output 1", "JS output 2"};
+ }
+
+ // register variables aliased to sliders
+ for (uint32_t i = 0; i < ysfx_max_sliders; ++i) {
+ if (main->header.sliders[i].exists) {
+ if (!main->header.sliders[i].var.empty())
+ fx->source.slider_alias.insert({main->header.sliders[i].var, i});
+ }
+ }
+
+ fx->source.main = std::move(main);
+ fx->source.main_file_path.assign(filepath);
+
+ // find the bank file, if present
+ ysfx::case_resolve(
+ ysfx::path_directory(filepath).c_str(),
+ (ysfx::path_file_name(filepath) + ".rpl").c_str(),
+ fx->source.bank_path);
+
+ // fill the file enums with the contents of directories
+ ysfx_fill_file_enums(fx);
+
+ // find incorrect enums and fix them
+ ysfx_fix_invalid_enums(fx);
+
+ // set the initial mask of visible sliders
+ ysfx_update_slider_visibility_mask(fx);
+ }
+
+ //--------------------------------------------------------------------------
+ // load the imports
+
+ // we load the imports recursively using post-order
+
+ static constexpr uint32_t max_import_level = 32;
+ std::set seen;
+
+ std::function do_next_import =
+ [fx, &seen, &do_next_import]
+ (const std::string &name, const std::string &origin, uint32_t level) -> bool
+ {
+ if (level >= max_import_level) {
+ ysfx_logf(*fx->config, ysfx_log_error, "%s: %s", ysfx::path_file_name(origin.c_str()).c_str(), "too many import levels");
+ return false;
+ }
+
+ std::string imported_path = ysfx_resolve_import_path(fx, name, origin);
+ if (imported_path.empty()) {
+ ysfx_logf(*fx->config, ysfx_log_error, "%s: cannot find import: %s", ysfx::path_file_name(origin.c_str()).c_str(), name.c_str());
+ return false;
+ }
+
+ ysfx::file_uid imported_uid;
+ ysfx::FILE_u stream{ysfx::fopen_utf8(imported_path.c_str(), "rb")};
+ if (!stream || !ysfx::get_stream_file_uid(stream.get(), imported_uid)) {
+ ysfx_logf(*fx->config, ysfx_log_error, "%s: cannot open file for reading", ysfx::path_file_name(imported_path.c_str()).c_str());
+ return false;
+ }
+
+ // this file was already visited, skip
+ if (!seen.insert(imported_uid).second)
+ return true;
+
+ // parse it
+ ysfx_source_unit_u unit{new ysfx_source_unit_t};
+ ysfx::stdio_text_reader reader(stream.get());
+
+ ysfx_parse_error error;
+ if (!ysfx_parse_toplevel(reader, unit->toplevel, &error)) {
+ ysfx_logf(*fx->config, ysfx_log_error, "%s:%u: %s", ysfx::path_file_name(imported_path.c_str()).c_str(), error.line + 1, error.message.c_str());
+ return false;
+ }
+ ysfx_parse_header(unit->toplevel.header.get(), unit->header);
+
+ // process the imported dependencies, *first*
+ for (const std::string &name : unit->header.imports) {
+ if (!do_next_import(name, imported_path.c_str(), level + 1))
+ return false;
+ }
+
+ // add it to the import sources, *second*
+ fx->source.imports.push_back(std::move(unit));
+
+ return true;
+ };
+
+ for (const std::string &name : fx->source.main->header.imports) {
+ if (!do_next_import(name, filepath, 0))
+ return false;
+ }
+
+ //--------------------------------------------------------------------------
+ // initialize the sliders to defaults
+
+ for (uint32_t i = 0; i < ysfx_max_sliders; ++i)
+ *fx->var.slider[i] = fx->source.main->header.sliders[i].def;
+
+ //--------------------------------------------------------------------------
+
+ fail_guard.disarm();
+ return true;
+}
+
+bool ysfx_compile(ysfx_t *fx, uint32_t compileopts)
+{
+ ysfx_unload_code(fx);
+
+ if (!fx->source.main) {
+ ysfx_logf(*fx->config, ysfx_log_error, "???: no source is loaded, cannot compile");
+ return false;
+ }
+
+ //--------------------------------------------------------------------------
+ // failure guard
+
+ auto fail_guard = ysfx::defer([fx]() { ysfx_unload_code(fx); });
+
+ //--------------------------------------------------------------------------
+ // configure VM
+
+ NSEEL_VMCTX vm = fx->vm.get();
+
+ {
+ uint32_t maxmem = fx->source.main->header.options.maxmem;
+ if (maxmem == 0)
+ maxmem = 8 * 1024 * 1024;
+ if (maxmem > 32 * 1024 * 1024)
+ maxmem = 32 * 1024 * 1024;
+
+ NSEEL_VM_setramsize(vm, (int)maxmem);
+ }
+
+ //--------------------------------------------------------------------------
+ // compile
+
+ auto compile_section =
+ [fx](ysfx_section_t *section, const char *name, NSEEL_CODEHANDLE_u &dest) -> bool
+ {
+ NSEEL_VMCTX vm = fx->vm.get();
+ if (section->text.empty()) {
+ // NOTE: check for empty source, which would return null code
+ dest.reset();
+ return true;
+ }
+ NSEEL_CODEHANDLE_u code{NSEEL_code_compile_ex(vm, section->text.c_str(), section->line_offset, NSEEL_CODE_COMPILE_FLAG_COMMONFUNCS)};
+ if (!code) {
+ ysfx_logf(*fx->config, ysfx_log_error, "%s: %s", name, NSEEL_code_getcodeerror(vm));
+ return false;
+ }
+ dest = std::move(code);
+ return true;
+ };
+
+ // compile the multiple @init sections, imports first
+ {
+ std::vector secs;
+ secs.reserve(fx->source.imports.size() + 1);
+
+ // collect init sections: imports first, main second
+ for (size_t i = 0; i < fx->source.imports.size(); ++i)
+ secs.push_back(fx->source.imports[i]->toplevel.init.get());
+ secs.push_back(fx->source.main->toplevel.init.get());
+
+ for (ysfx_section_t *sec : secs) {
+ NSEEL_CODEHANDLE_u code;
+ if (sec && !compile_section(sec, "@init", code))
+ return false;
+ fx->code.init.push_back(std::move(code));
+ }
+ }
+
+ // compile the other sections, single
+ // a non-@init section is searched in the main file first;
+ // if not found, it's inherited from the first import which has it.
+ ysfx_section_t *slider = ysfx_search_section(fx, ysfx_section_slider);
+ ysfx_section_t *block = ysfx_search_section(fx, ysfx_section_block);
+ ysfx_section_t *sample = ysfx_search_section(fx, ysfx_section_sample);
+ ysfx_section_t *gfx = nullptr;
+ ysfx_section_t *serialize = nullptr;
+ if ((compileopts & ysfx_compile_no_gfx) == 0)
+ gfx = ysfx_search_section(fx, ysfx_section_gfx);
+ if ((compileopts & ysfx_compile_no_serialize) == 0)
+ serialize = ysfx_search_section(fx, ysfx_section_serialize);
+
+ if (slider && !compile_section(slider, "@slider", fx->code.slider))
+ return false;
+ if (block && !compile_section(block, "@block", fx->code.block))
+ return false;
+ if (sample && !compile_section(sample, "@sample", fx->code.sample))
+ return false;
+ if (gfx && !compile_section(gfx, "@gfx", fx->code.gfx))
+ return false;
+ if (serialize && !compile_section(serialize, "@serialize", fx->code.serialize))
+ return false;
+
+ fx->code.compiled = true;
+ fx->is_freshly_compiled = true;
+ fx->must_compute_init = true;
+
+ ///
+ ysfx_eel_string_context_update_named_vars(fx->string_ctx.get(), vm);
+
+ fail_guard.disarm();
+ return true;
+}
+
+bool ysfx_is_compiled(ysfx_t *fx)
+{
+ return fx->code.compiled;
+}
+
+void ysfx_unload_source(ysfx_t *fx)
+{
+ fx->source = {};
+}
+
+void ysfx_unload_code(ysfx_t *fx)
+{
+#if !defined(YSFX_NO_GFX)
+ // get rid of gfx first, to prevent a UI thread from trying
+ // to access VM and invoke code
+ {
+ std::lock_guard lock{fx->gfx.mutex};
+ fx->gfx.ready = false;
+ fx->gfx.wants_retina = false;
+ fx->gfx.must_init.store(false);
+ }
+#endif
+
+ fx->code = {};
+
+ fx->is_freshly_compiled = false;
+ fx->must_compute_init = false;
+ fx->must_compute_slider = false;
+
+ NSEEL_VMCTX vm = fx->vm.get();
+ NSEEL_code_compile_ex(vm, nullptr, 0, NSEEL_CODE_COMPILE_FLAG_COMMONFUNCS_RESET);
+ NSEEL_VM_remove_unused_vars(vm);
+ NSEEL_VM_remove_all_nonreg_vars(vm);
+ NSEEL_VM_freeRAM(vm);
+}
+
+void ysfx_unload(ysfx_t *fx)
+{
+ ysfx_unload_code(fx);
+ ysfx_unload_source(fx);
+}
+
+bool ysfx_is_loaded(ysfx_t *fx)
+{
+ return fx->source.main != nullptr;
+}
+
+void ysfx_fill_file_enums(ysfx_t *fx)
+{
+ if (fx->config->data_root.empty())
+ return;
+
+ for (uint32_t i = 0; i < ysfx_max_sliders; ++i) {
+ ysfx_slider_t &slider = fx->source.main->header.sliders[i];
+ if (slider.path.empty())
+ continue;
+
+ std::string dirpath = ysfx::path_ensure_final_separator((fx->config->data_root + slider.path).c_str());
+ ysfx::string_list entries = ysfx::list_directory(dirpath.c_str());
+
+ for (const std::string &filename : entries) {
+ if (!filename.empty() && ysfx::is_path_separator(filename.back()))
+ continue;
+
+ std::string filepath = dirpath + filename;
+
+ ysfx_file_type_t ftype = ysfx_detect_file_type(fx, filepath.c_str(), nullptr);
+ if (ftype == ysfx_file_type_none)
+ continue;
+
+ slider.enum_names.push_back(std::move(filename));
+ }
+
+ if (!slider.enum_names.empty())
+ slider.max = (EEL_F)(slider.enum_names.size() - 1);
+ }
+}
+
+void ysfx_fix_invalid_enums(ysfx_t *fx)
+{
+ //NOTE: regardless of the range of enum sliders in source, it is <0,N-1,1>
+ // if there is a mismatch, correct and output a warning
+
+ for (uint32_t i = 0; i < ysfx_max_sliders; ++i) {
+ ysfx_slider_t &slider = fx->source.main->header.sliders[i];
+ if (!slider.is_enum)
+ continue;
+
+ uint32_t count = (uint32_t)slider.enum_names.size();
+ if (count == 0) {
+ bool is_file = !slider.path.empty();
+ ysfx_logf(*fx->config, ysfx_log_warning, "slider%u: the enumeration does not contain any %s", i + 1, is_file ? "files" : "items");
+ slider.enum_names.emplace_back();
+ slider.min = 0;
+ slider.max = 0;
+ slider.inc = 1;
+ }
+ else if (slider.min != 0 || slider.inc != 1 || slider.max != (EEL_F)(count - 1)) {
+ ysfx_logf(*fx->config, ysfx_log_warning, "slider%u: the enumeration has an invalid range", i + 1);
+ slider.min = 0;
+ slider.max = (EEL_F)(count - 1);
+ slider.inc = 1;
+ }
+ }
+}
+
+const char *ysfx_get_name(ysfx_t *fx)
+{
+ ysfx_source_unit_t *main = fx->source.main.get();
+ if (!main)
+ return "";
+ return main->header.desc.c_str();
+}
+
+const char *ysfx_get_file_path(ysfx_t *fx)
+{
+ return fx->source.main_file_path.c_str();
+}
+
+const char *ysfx_get_author(ysfx_t *fx)
+{
+ ysfx_source_unit_t *main = fx->source.main.get();
+ if (!main)
+ return "";
+ return main->header.author.c_str();
+}
+
+uint32_t ysfx_get_tags(ysfx_t *fx, const char **dest, uint32_t destsize)
+{
+ ysfx_source_unit_t *main = fx->source.main.get();
+ if (!main)
+ return 0;
+
+ uint32_t count = (uint32_t)main->header.tags.size();
+
+ uint32_t copysize = (destsize < count) ? destsize : count;
+ for (uint32_t i = 0; i < copysize; ++i)
+ dest[i] = main->header.tags[i].c_str();
+
+ return count;
+}
+
+const char *ysfx_get_tag(ysfx_t *fx, uint32_t index)
+{
+ ysfx_source_unit_t *main = fx->source.main.get();
+ if (!main || index >= main->header.tags.size())
+ return "";
+ return main->header.tags[index].c_str();
+}
+
+uint32_t ysfx_get_num_inputs(ysfx_t *fx)
+{
+ ysfx_source_unit_t *main = fx->source.main.get();
+ if (!main)
+ return 0;
+ return (uint32_t)main->header.in_pins.size();
+}
+
+uint32_t ysfx_get_num_outputs(ysfx_t *fx)
+{
+ ysfx_source_unit_t *main = fx->source.main.get();
+ if (!main)
+ return 0;
+ return (uint32_t)main->header.out_pins.size();
+}
+
+const char *ysfx_get_input_name(ysfx_t *fx, uint32_t index)
+{
+ ysfx_source_unit_t *main = fx->source.main.get();
+ if (!main || index >= main->header.in_pins.size())
+ return "";
+ return main->header.in_pins[index].c_str();
+}
+
+const char *ysfx_get_output_name(ysfx_t *fx, uint32_t index)
+{
+ ysfx_source_unit_t *main = fx->source.main.get();
+ if (!main || index >= main->header.out_pins.size())
+ return "";
+ return main->header.out_pins[index].c_str();
+}
+
+bool ysfx_wants_meters(ysfx_t *fx)
+{
+ ysfx_source_unit_t *main = fx->source.main.get();
+ if (!main)
+ return false;
+
+ return !main->header.options.no_meter;
+}
+
+bool ysfx_get_gfx_dim(ysfx_t *fx, uint32_t dim[2])
+{
+ ysfx_toplevel_t *origin = nullptr;
+ ysfx_section_t *sec = ysfx_search_section(fx, ysfx_section_gfx, &origin);
+
+ if (!sec) {
+ if (dim) {
+ dim[0] = 0;
+ dim[1] = 0;
+ }
+ return false;
+ }
+
+ if (dim) {
+ dim[0] = origin->gfx_w;
+ dim[1] = origin->gfx_h;
+ }
+ return true;
+}
+
+ysfx_section_t *ysfx_search_section(ysfx_t *fx, uint32_t type, ysfx_toplevel_t **origin)
+{
+ if (!fx->source.main)
+ return nullptr;
+
+ auto search =
+ [fx](ysfx_section_t *(*test)(ysfx_toplevel_t &tl), ysfx_toplevel_t **origin) -> ysfx_section_t *
+ {
+ ysfx_toplevel_t *tl = &fx->source.main->toplevel;
+ ysfx_section_t *sec = test(*tl);
+ for (size_t i = 0; !sec && i < fx->source.imports.size(); ++i) {
+ tl = &fx->source.imports[i]->toplevel;
+ sec = test(*tl);
+ }
+ if (origin)
+ *origin = sec ? tl : nullptr;
+ return sec;
+ };
+
+ switch (type) {
+ case ysfx_section_init:
+ return search([](ysfx_toplevel_t &tl) { return tl.init.get(); }, origin);
+ case ysfx_section_slider:
+ return search([](ysfx_toplevel_t &tl) { return tl.slider.get(); }, origin);
+ case ysfx_section_block:
+ return search([](ysfx_toplevel_t &tl) { return tl.block.get(); }, origin);
+ case ysfx_section_sample:
+ return search([](ysfx_toplevel_t &tl) { return tl.sample.get(); }, origin);
+ case ysfx_section_gfx:
+ return search([](ysfx_toplevel_t &tl) { return tl.gfx.get(); }, origin);
+ case ysfx_section_serialize:
+ return search([](ysfx_toplevel_t &tl) { return tl.serialize.get(); }, origin);
+ default:
+ return nullptr;
+ }
+}
+
+bool ysfx_has_section(ysfx_t *fx, uint32_t type)
+{
+ return ysfx_search_section(fx, type) != nullptr;
+}
+
+bool ysfx_slider_exists(ysfx_t *fx, uint32_t index)
+{
+ ysfx_source_unit_t *main = fx->source.main.get();
+ if (index >= ysfx_max_sliders || !main)
+ return false;
+
+ ysfx_slider_t &slider = main->header.sliders[index];
+ return slider.exists;
+}
+
+const char *ysfx_slider_get_name(ysfx_t *fx, uint32_t index)
+{
+ ysfx_source_unit_t *main = fx->source.main.get();
+ if (index >= ysfx_max_sliders || !main)
+ return "";
+
+ ysfx_slider_t &slider = main->header.sliders[index];
+ return slider.desc.c_str();
+}
+
+bool ysfx_slider_get_range(ysfx_t *fx, uint32_t index, ysfx_slider_range_t *range)
+{
+ ysfx_source_unit_t *main = fx->source.main.get();
+ if (index >= ysfx_max_sliders || !main)
+ return false;
+
+ ysfx_slider_t &slider = main->header.sliders[index];
+ range->def = slider.def;
+ range->min = slider.min;
+ range->max = slider.max;
+ range->inc = slider.inc;
+ return true;
+}
+
+bool ysfx_slider_is_enum(ysfx_t *fx, uint32_t index)
+{
+ ysfx_source_unit_t *main = fx->source.main.get();
+ if (index >= ysfx_max_sliders || !main)
+ return false;
+
+ ysfx_slider_t &slider = main->header.sliders[index];
+ return slider.is_enum;
+}
+
+uint32_t ysfx_slider_get_enum_names(ysfx_t *fx, uint32_t index, const char **dest, uint32_t destsize)
+{
+ ysfx_source_unit_t *main = fx->source.main.get();
+ if (index >= ysfx_max_sliders || !main)
+ return 0;
+
+ ysfx_slider_t &slider = main->header.sliders[index];
+ uint32_t count = (uint32_t)slider.enum_names.size();
+
+ uint32_t copysize = (destsize < count) ? destsize : count;
+ for (uint32_t i = 0; i < copysize; ++i)
+ dest[i] = slider.enum_names[i].c_str();
+
+ return count;
+}
+
+const char *ysfx_slider_get_enum_name(ysfx_t *fx, uint32_t slider_index, uint32_t enum_index)
+{
+ ysfx_source_unit_t *main = fx->source.main.get();
+ if (slider_index >= ysfx_max_sliders || !main)
+ return 0;
+
+ ysfx_slider_t &slider = main->header.sliders[slider_index];
+ if (enum_index >= slider.enum_names.size())
+ return "";
+
+ return slider.enum_names[enum_index].c_str();
+}
+
+bool ysfx_slider_is_path(ysfx_t *fx, uint32_t index)
+{
+ ysfx_source_unit_t *main = fx->source.main.get();
+ if (index >= ysfx_max_sliders || !main)
+ return false;
+
+ ysfx_slider_t &slider = main->header.sliders[index];
+ return !slider.path.empty();
+}
+
+bool ysfx_slider_is_initially_visible(ysfx_t *fx, uint32_t index)
+{
+ ysfx_source_unit_t *main = fx->source.main.get();
+ if (index >= ysfx_max_sliders || !main)
+ return false;
+
+ ysfx_slider_t &slider = main->header.sliders[index];
+ return slider.initially_visible;
+}
+
+ysfx_real ysfx_slider_get_value(ysfx_t *fx, uint32_t index)
+{
+ if (index >= ysfx_max_sliders)
+ return 0;
+ return *fx->var.slider[index];
+}
+
+void ysfx_slider_set_value(ysfx_t *fx, uint32_t index, ysfx_real value)
+{
+ if (index >= ysfx_max_sliders)
+ return;
+ if (*fx->var.slider[index] != value) {
+ *fx->var.slider[index] = value;
+ fx->must_compute_slider = true;
+ }
+}
+
+std::string ysfx_resolve_import_path(ysfx_t *fx, const std::string &name, const std::string &origin)
+{
+ std::vector dirs;
+
+ // create the list of search directories
+ {
+ dirs.reserve(2);
+
+ if (!origin.empty())
+ dirs.push_back(ysfx::path_directory(origin.c_str()));
+
+ const std::string &import_root = fx->config->import_root;
+ if (!import_root.empty() && dirs[0] != import_root)
+ dirs.push_back(import_root);
+ }
+
+ // the search should be case-insensitive
+ static constexpr bool nocase = true;
+
+ static auto *check_existence = +[](const std::string &dir, const std::string &file, std::string &result_path) -> int {
+ if (nocase)
+ return ysfx::case_resolve(dir.c_str(), file.c_str(), result_path);
+ else {
+ result_path = dir + file;
+ return ysfx::exists(result_path.c_str());
+ }
+ };
+
+ // search for the file in these directories directly
+ for (const std::string &dir : dirs) {
+ std::string resolved;
+ if (check_existence(dir, name, resolved))
+ return resolved;
+ }
+
+ // search for the file recursively
+ for (const std::string &dir : dirs) {
+ struct visit_data {
+ const std::string *name = nullptr;
+ std::string resolved;
+ };
+ visit_data vd;
+ vd.name = &name;
+ auto visit = [](const std::string &dir, void *data) -> bool {
+ visit_data &vd = *(visit_data *)data;
+ std::string resolved;
+ if (check_existence(dir, *vd.name, resolved)) {
+ vd.resolved = std::move(resolved);
+ return false;
+ }
+ return true;
+ };
+ ysfx::visit_directories(dir.c_str(), +visit, &vd);
+ if (!vd.resolved.empty())
+ return vd.resolved;
+ }
+
+ return std::string{};
+}
+
+
+uint32_t ysfx_get_block_size(ysfx_t *fx)
+{
+ return fx->block_size;
+}
+
+ysfx_real ysfx_get_sample_rate(ysfx_t *fx)
+{
+ return fx->sample_rate;
+}
+
+void ysfx_set_block_size(ysfx_t *fx, uint32_t blocksize)
+{
+ if (fx->block_size != blocksize) {
+ fx->block_size = blocksize;
+ fx->must_compute_init = true;
+ }
+}
+
+void ysfx_set_sample_rate(ysfx_t *fx, ysfx_real samplerate)
+{
+ if (fx->sample_rate != samplerate) {
+ fx->sample_rate = samplerate;
+ fx->must_compute_init = true;
+ }
+}
+
+void ysfx_set_midi_capacity(ysfx_t *fx, uint32_t capacity, bool extensible)
+{
+ ysfx_midi_reserve(fx->midi.in.get(), capacity, extensible);
+ ysfx_midi_reserve(fx->midi.out.get(), capacity, extensible);
+}
+
+void ysfx_init(ysfx_t *fx)
+{
+ if (!fx->code.compiled)
+ return;
+
+ if (fx->is_freshly_compiled) {
+ ysfx_first_init(fx);
+ fx->is_freshly_compiled = false;
+ }
+
+ ysfx_clear_files(fx);
+
+ for (size_t i = 0; i < fx->code.init.size(); ++i)
+ NSEEL_code_execute(fx->code.init[i].get());
+
+ fx->must_compute_init = false;
+ fx->must_compute_slider = true;
+
+#if !defined(YSFX_NO_GFX)
+ // do initializations on next @gfx, on the gfx thread
+ // release-acquire order is for VM `gfx_*` variables and `wants_retina`
+ fx->gfx.wants_retina = *fx->var.gfx_ext_retina > 0;
+ fx->gfx.must_init.store(true, std::memory_order_release);
+#endif
+}
+
+void ysfx_first_init(ysfx_t *fx)
+{
+ assert(fx->code.compiled);
+ assert(fx->is_freshly_compiled);
+
+ *fx->var.samplesblock = (EEL_F)fx->block_size;
+ *fx->var.srate = fx->sample_rate;
+
+ ysfx_clear_files(fx);
+
+ fx->slider.automate_mask.store(0);
+ fx->slider.change_mask.store(0);
+ ysfx_update_slider_visibility_mask(fx);
+
+ *fx->var.pdc_delay = 0;
+ *fx->var.pdc_bot_ch = 0;
+ *fx->var.pdc_top_ch = 0;
+ *fx->var.pdc_midi = 0;
+}
+
+ysfx_real ysfx_get_pdc_delay(ysfx_t *fx)
+{
+ ysfx_real value = *fx->var.pdc_delay;
+ return (value > 0) ? value : 0;
+}
+
+void ysfx_get_pdc_channels(ysfx_t *fx, uint32_t channels[2])
+{
+ if (!channels)
+ return;
+
+ int64_t bot = (int64_t)*fx->var.pdc_bot_ch;
+ bot = (bot > 0) ? bot : 0;
+ bot = (bot < ysfx_max_channels) ? bot : ysfx_max_channels;
+ channels[0] = (uint32_t)bot;
+
+ int64_t top = (int64_t)*fx->var.pdc_top_ch;
+ top = (top > bot) ? top : bot;
+ top = (top < ysfx_max_channels) ? top : ysfx_max_channels;
+ channels[1] = (uint32_t)top;
+}
+
+bool ysfx_get_pdc_midi(ysfx_t *fx)
+{
+ return (bool)*fx->var.pdc_midi;
+}
+
+void ysfx_update_slider_visibility_mask(ysfx_t *fx)
+{
+ uint64_t visible = 0;
+ for (uint32_t i = 0; i < ysfx_max_sliders; ++i) {
+ ysfx_slider_t &slider = fx->source.main->header.sliders[i];
+ visible |= (uint64_t)slider.initially_visible << i;
+ }
+ fx->slider.visible_mask.store(visible);
+}
+
+void ysfx_set_time_info(ysfx_t *fx, const ysfx_time_info_t *info)
+{
+ uint32_t prev_state = (uint32_t)*fx->var.play_state;
+ uint32_t new_state = info->playback_state;
+
+ // unless `ext_noinit`, we should call @init every transport restart
+ if (!*fx->var.ext_noinit) {
+ auto is_running = [](uint32_t state) {
+ return state == ysfx_playback_playing ||
+ state == ysfx_playback_recording;
+ };
+ if (!is_running(prev_state) && is_running(new_state))
+ fx->must_compute_init = true;
+ }
+
+ *fx->var.tempo = info->tempo;
+ *fx->var.play_state = (EEL_F)new_state;
+ *fx->var.play_position = info->time_position;
+ *fx->var.beat_position = info->beat_position;
+ *fx->var.ts_num = (EEL_F)info->time_signature[0];
+ *fx->var.ts_denom = (EEL_F)info->time_signature[1];
+}
+
+bool ysfx_send_midi(ysfx_t *fx, const ysfx_midi_event_t *event)
+{
+ return ysfx_midi_push(fx->midi.in.get(), event);
+}
+
+bool ysfx_receive_midi(ysfx_t *fx, ysfx_midi_event_t *event)
+{
+ return ysfx_midi_get_next(fx->midi.out.get(), event);
+}
+
+bool ysfx_receive_midi_from_bus(ysfx_t *fx, uint32_t bus, ysfx_midi_event_t *event)
+{
+ return ysfx_midi_get_next_from_bus(fx->midi.out.get(), 0, event);
+}
+
+uint32_t ysfx_current_midi_bus(ysfx_t *fx)
+{
+ uint32_t bus = 0;
+ if (*fx->var.ext_midi_bus)
+ bus = (int32_t)*fx->var.midi_bus;
+ return bus;
+}
+
+bool ysfx_send_trigger(ysfx_t *fx, uint32_t index)
+{
+ if (index >= ysfx_max_triggers)
+ return false;
+
+ fx->triggers |= 1u << index;
+ return true;
+}
+
+uint64_t ysfx_fetch_slider_changes(ysfx_t *fx)
+{
+ return fx->slider.change_mask.exchange(0);
+}
+
+uint64_t ysfx_fetch_slider_automations(ysfx_t *fx)
+{
+ return fx->slider.automate_mask.exchange(0);
+}
+
+uint64_t ysfx_get_slider_visibility(ysfx_t *fx)
+{
+ return fx->slider.visible_mask.load();
+}
+
+template
+void ysfx_process_generic(ysfx_t *fx, const Real *const *ins, Real *const *outs, uint32_t num_ins, uint32_t num_outs, uint32_t num_frames)
+{
+ ysfx_set_thread_id(ysfx_thread_id_dsp);
+
+ // prepare MIDI input for reading, output for writing
+ assert(fx->midi.in->read_pos == 0);
+ ysfx_midi_clear(fx->midi.out.get());
+
+ // prepare triggers
+ *fx->var.trigger = (EEL_F)fx->triggers;
+ fx->triggers = 0;
+
+ if (!fx->code.compiled) {
+ for (uint32_t ch = 0; ch < num_outs; ++ch)
+ memset(outs[ch], 0, num_frames * sizeof(Real));
+ }
+ else {
+ // compute @init if needed
+ if (fx->must_compute_init)
+ ysfx_init(fx);
+
+ const uint32_t orig_num_outs = num_outs;
+ const uint32_t num_code_ins = (uint32_t)fx->source.main->header.in_pins.size();
+ const uint32_t num_code_outs = (uint32_t)fx->source.main->header.out_pins.size();
+ if (num_ins > num_code_ins)
+ num_ins = num_code_ins;
+ if (num_outs > num_code_outs)
+ num_outs = num_code_outs;
+
+ fx->valid_input_channels = num_ins;
+
+ *fx->var.samplesblock = (EEL_F)num_frames;
+ *fx->var.num_ch = (EEL_F)num_ins;
+
+ // compute @slider if needed
+ if (fx->must_compute_slider) {
+ NSEEL_code_execute(fx->code.slider.get());
+ fx->must_compute_slider = false;
+ }
+
+ // compute @block
+ NSEEL_code_execute(fx->code.block.get());
+
+ // compute @sample, once per frame
+ if (fx->code.sample) {
+ EEL_F **spl = fx->var.spl;
+ for (uint32_t i = 0; i < num_frames; ++i) {
+ for (uint32_t ch = 0; ch < num_ins; ++ch)
+ *spl[ch] = (EEL_F)ins[ch][i];
+ for (uint32_t ch = num_ins; ch < num_code_ins; ++ch)
+ *spl[ch] = 0;
+ NSEEL_code_execute(fx->code.sample.get());
+ for (uint32_t ch = 0; ch < num_outs; ++ch)
+ outs[ch][i] = (Real)*spl[ch];
+ }
+ }
+
+ // clear any output channels above the maximum count
+ for (uint32_t ch = num_outs; ch < orig_num_outs; ++ch)
+ memset(outs[ch], 0, num_frames * sizeof(Real));
+ }
+
+ // prepare MIDI input for writing, output for reading
+ assert(fx->midi.out->read_pos == 0);
+ ysfx_midi_clear(fx->midi.in.get());
+
+ ysfx_set_thread_id(ysfx_thread_id_none);
+}
+
+void ysfx_process_float(ysfx_t *fx, const float *const *ins, float *const *outs, uint32_t num_ins, uint32_t num_outs, uint32_t num_frames)
+{
+ ysfx_process_generic(fx, ins, outs, num_ins, num_outs, num_frames);
+}
+
+void ysfx_process_double(ysfx_t *fx, const double *const *ins, double *const *outs, uint32_t num_ins, uint32_t num_outs, uint32_t num_frames)
+{
+ ysfx_process_generic(fx, ins, outs, num_ins, num_outs, num_frames);
+}
+
+void ysfx_clear_files(ysfx_t *fx)
+{
+ std::lock_guard list_lock(fx->file.list_mutex);
+
+ // delete all except the serializer
+ while (fx->file.list.size() > 1) {
+ ysfx_file_t *file = fx->file.list.back().get();
+ std::unique_ptr file_mutex;
+ std::unique_lock file_lock;
+ if (file) {
+ file_lock = std::unique_lock{*fx->file.list.back()->m_mutex};
+ file_mutex = std::move(fx->file.list.back()->m_mutex);
+ }
+ fx->file.list.pop_back();
+ }
+}
+
+ysfx_file_t *ysfx_get_file(ysfx_t *fx, uint32_t handle, std::unique_lock &lock, std::unique_lock *list_lock)
+{
+ std::unique_lock local_list_lock;
+ if (list_lock)
+ *list_lock = std::unique_lock(fx->file.list_mutex);
+ else
+ local_list_lock = std::unique_lock(fx->file.list_mutex);
+ if (handle >= fx->file.list.size())
+ return nullptr;
+ ysfx_file_t *file = fx->file.list[handle].get();
+ if (!file)
+ return nullptr;
+ lock = std::unique_lock{*file->m_mutex};
+ return file;
+}
+
+int32_t ysfx_insert_file(ysfx_t *fx, ysfx_file_t *file)
+{
+ std::lock_guard lock(fx->file.list_mutex);
+
+ //
+ size_t noneidx = ~(size_t)0;
+ size_t freeidx = noneidx;
+
+ for (size_t i = 0, n = fx->file.list.size(); i < n && freeidx == noneidx; ++i) {
+ if (!fx->file.list[i])
+ freeidx = i;
+ }
+ if (freeidx != noneidx) {
+ fx->file.list[freeidx].reset(file);
+ return (uint32_t)freeidx;
+ }
+
+ enum { max_file_handles = 64 };
+
+ size_t pos = fx->file.list.size();
+ if (pos >= max_file_handles)
+ return -1;
+
+ fx->file.list.emplace_back(file);
+ return (uint32_t)pos;
+}
+
+bool ysfx_load_state(ysfx_t *fx, ysfx_state_t *state)
+{
+ if (!fx->code.compiled)
+ return false;
+
+ // restore the serialization
+ std::string buffer((char *)state->data, state->data_size);
+
+ // restore the sliders
+ for (uint32_t i = 0; i < ysfx_max_sliders; ++i)
+ *fx->var.slider[i] = fx->source.main->header.sliders[i].def;
+
+ for (uint32_t i = 0, n = state->slider_count; i < n; ++i) {
+ uint32_t j = state->sliders[i].index;
+ if (j < ysfx_max_sliders && fx->source.main->header.sliders[j].exists)
+ *fx->var.slider[j] = state->sliders[i].value;
+ }
+ fx->must_compute_slider = true;
+
+ // invoke @serialize
+ {
+ std::unique_lock lock;
+ ysfx_serializer_t *serializer = static_cast(ysfx_get_file(fx, 0, lock));
+ assert(serializer);
+ serializer->begin(false, buffer);
+ lock.unlock();
+ ysfx_serialize(fx);
+ lock.lock();
+ serializer->end();
+ }
+
+ return true;
+}
+
+ysfx_state_t *ysfx_save_state(ysfx_t *fx)
+{
+ if (!fx->code.compiled)
+ return nullptr;
+
+ std::string buffer;
+
+ // invoke @serialize
+ {
+ std::unique_lock lock;
+ ysfx_serializer_t *serializer = static_cast(ysfx_get_file(fx, 0, lock));
+ assert(serializer);
+ serializer->begin(true, buffer);
+ lock.unlock();
+ ysfx_serialize(fx);
+ lock.lock();
+ serializer->end();
+ }
+
+ // save the sliders
+ ysfx_state_u state{new ysfx_state_t};
+ uint32_t slider_count = 0;
+ for (uint32_t i = 0; i < ysfx_max_sliders; ++i)
+ slider_count += fx->source.main->header.sliders[i].exists;
+
+ state->sliders = new ysfx_state_slider_t[slider_count]{};
+ state->slider_count = slider_count;
+
+ for (uint32_t i = 0, j = 0; i < slider_count; ++i) {
+ if (fx->source.main->header.sliders[i].exists) {
+ state->sliders[j].index = i;
+ state->sliders[j].value = *fx->var.slider[i];
+ ++j;
+ }
+ }
+
+ // save the serialization
+ state->data_size = buffer.size();
+ state->data = new uint8_t[state->data_size];
+ memcpy(state->data, buffer.data(), state->data_size);
+
+ //
+ return state.release();
+}
+
+void ysfx_state_free(ysfx_state_t *state)
+{
+ if (!state)
+ return;
+
+ delete[] state->sliders;
+ delete[] state->data;
+ delete state;
+}
+
+ysfx_state_t *ysfx_state_dup(ysfx_state_t *state_in)
+{
+ if (!state_in)
+ return nullptr;
+
+ ysfx_state_u state_out{new ysfx_state_t};
+
+ uint32_t slider_count = state_out->slider_count = state_in->slider_count;
+ size_t data_size = state_out->data_size = state_in->data_size;
+
+ state_out->sliders = new ysfx_state_slider_t[slider_count];
+ memcpy(state_out->sliders, state_in->sliders, slider_count * sizeof(ysfx_state_slider_t));
+
+ state_out->data = new uint8_t[data_size];
+ memcpy(state_out->data, state_in->data, data_size);
+
+ return state_out.release();
+}
+
+void ysfx_serialize(ysfx_t *fx)
+{
+ if (fx->code.serialize) {
+ if (fx->must_compute_init)
+ ysfx_init(fx);
+ NSEEL_code_execute(fx->code.serialize.get());
+ }
+}
+
+uint32_t ysfx_get_slider_of_var(ysfx_t *fx, EEL_F *var)
+{
+ auto it = fx->slider_of_var.find(var);
+ if (it == fx->slider_of_var.end())
+ return ~(uint32_t)0;
+ return it->second;
+}
+
+const char *ysfx_get_bank_path(ysfx_t *fx)
+{
+ return fx->source.bank_path.c_str();
+}
+
+void ysfx_enum_vars(ysfx_t *fx, ysfx_enum_vars_callback_t *callback, void *userdata)
+{
+ NSEEL_VM_enumallvars(fx->vm.get(), callback, userdata);
+}
+
+ysfx_real *ysfx_find_var(ysfx_t *fx, const char *name)
+{
+ struct find_data {
+ ysfx_real *var = nullptr;
+ const char *name = nullptr;
+ };
+ find_data fd;
+ fd.name = name;
+ auto callback = [](const char *name, EEL_F *var, void *userdata) -> int {
+ find_data *fd = (find_data *)userdata;
+ if (strcmp(name, fd->name) != 0)
+ return 1;
+ fd->var = var;
+ return 0;
+ };
+ NSEEL_VM_enumallvars(fx->vm.get(), +callback, &fd);
+ return fd.var;
+}
+
+void ysfx_read_vmem(ysfx_t *fx, uint32_t addr, ysfx_real *dest, uint32_t count)
+{
+ ysfx_eel_ram_reader reader(fx->vm.get(), addr);
+ for (uint32_t i = 0; i < count; ++i)
+ dest[i] = reader.read_next();
+}
+
+bool ysfx_find_data_file(ysfx_t *fx, EEL_F *file, std::string &result)
+{
+ // 3 possibilities for file
+ // - slider
+ // - index of filename
+ // - string
+
+ std::string filepart;
+
+ bool accept_absolute = false;
+ bool accept_relative = false;
+
+ int32_t index = ysfx_eel_round(*file);
+ uint32_t slideridx = ysfx_get_slider_of_var(fx, file);
+ ysfx_slider_t *slider = nullptr;
+
+ if (slideridx != ~(uint32_t)0)
+ slider = &fx->source.main->header.sliders[slideridx];
+
+ if (slider && !slider->path.empty()) {
+ int32_t value = ysfx_eel_round(*fx->var.slider[slideridx]);
+ if (value < 0 || (uint32_t)value >= slider->enum_names.size())
+ return false;
+
+ filepart = slider->path + '/' + slider->enum_names[(uint32_t)value];
+ accept_relative = true;
+ }
+ else if (index >= 0 && (uint32_t)index < fx->source.main->header.filenames.size()) {
+ filepart = fx->source.main->header.filenames[(uint32_t)index];
+ accept_relative = true;
+ }
+ else if (ysfx_string_get(fx, *file, filepart)) {
+ accept_absolute = true;
+ accept_relative = true;
+ }
+ else
+ return false;
+
+ std::vector filecandidates;
+ filecandidates.reserve(2);
+
+ if (accept_absolute && !ysfx::path_is_relative(filepart.c_str()))
+ filecandidates.push_back(filepart);
+ else if (accept_relative) {
+ filecandidates.push_back(ysfx::path_directory(fx->source.main_file_path.c_str()) + filepart);
+ if (!fx->config->data_root.empty())
+ filecandidates.push_back(fx->config->data_root + filepart);
+ }
+
+ for (const std::string &filepath : filecandidates) {
+ if (ysfx::exists(filepath.c_str())) {
+ result.assign(filepath);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+ysfx_file_type_t ysfx_detect_file_type(ysfx_t *fx, const char *path, void **fmtobj)
+{
+ if (ysfx::path_has_suffix(path, "txt"))
+ return ysfx_file_type_txt;
+ if (ysfx::path_has_suffix(path, "raw"))
+ return ysfx_file_type_raw;
+ for (ysfx_audio_format_t &fmt : fx->config->audio_formats) {
+ if (fmt.can_handle(path)) {
+ if (fmtobj)
+ *fmtobj = &fmt;
+ return ysfx_file_type_audio;
+ }
+ }
+ return ysfx_file_type_none;
+}
+
+void ysfx_gfx_setup(ysfx_t *fx, ysfx_gfx_config_t *gc)
+{
+#if !defined(YSFX_NO_GFX)
+ bool doinit = false;
+ ysfx_scoped_gfx_t scope{fx, doinit};
+
+ ysfx_gfx_state_set_bitmap(fx->gfx.state.get(), gc->pixels, gc->pixel_width, gc->pixel_height, gc->pixel_stride);
+ ysfx_real scale = fx->gfx.wants_retina ? gc->scale_factor : 1;
+ ysfx_gfx_state_set_scale_factor(fx->gfx.state.get(), scale);
+
+ ysfx_gfx_state_set_callback_data(fx->gfx.state.get(), gc->user_data);
+ ysfx_gfx_state_set_show_menu_callback(fx->gfx.state.get(), gc->show_menu);
+ ysfx_gfx_state_set_set_cursor_callback(fx->gfx.state.get(), gc->set_cursor);
+ ysfx_gfx_state_set_get_drop_file_callback(fx->gfx.state.get(), gc->get_drop_file);
+#else
+ (void)fx;
+ (void)gc;
+#endif
+}
+
+bool ysfx_gfx_wants_retina(ysfx_t *fx)
+{
+#if !defined(YSFX_NO_GFX)
+ return fx->gfx.wants_retina;
+#else
+ (void)fx;
+ return false;
+#endif
+}
+
+void ysfx_gfx_add_key(ysfx_t *fx, uint32_t mods, uint32_t key, bool press)
+{
+#if !defined(YSFX_NO_GFX)
+ bool doinit = true;
+ ysfx_scoped_gfx_t scope{fx, doinit};
+
+ if (!fx->gfx.ready)
+ return;
+
+ ysfx_gfx_state_add_key(fx->gfx.state.get(), mods, key, press);
+#else
+ (void)fx;
+ (void)mods;
+ (void)key;
+#endif
+}
+
+void ysfx_gfx_update_mouse(ysfx_t *fx, uint32_t mods, int32_t xpos, int32_t ypos, uint32_t buttons, ysfx_real wheel, ysfx_real hwheel)
+{
+#if !defined(YSFX_NO_GFX)
+ bool doinit = true;
+ ysfx_scoped_gfx_t scope{fx, doinit};
+
+ if (!fx->gfx.ready)
+ return;
+
+ *fx->var.mouse_x = (EEL_F)xpos;
+ *fx->var.mouse_y = (EEL_F)ypos;
+ *fx->var.mouse_wheel += 120 * wheel;
+ *fx->var.mouse_hwheel += 120 * hwheel;
+
+ uint32_t mouse_cap = 0;
+ if (mods & ysfx_mod_shift)
+ mouse_cap |= 8;
+ if (mods & ysfx_mod_ctrl)
+ mouse_cap |= 4;
+ if (mods & ysfx_mod_alt)
+ mouse_cap |= 16;
+ if (mods & ysfx_mod_super)
+ mouse_cap |= 32;
+ if (buttons & ysfx_button_left)
+ mouse_cap |= 1;
+ if (buttons & ysfx_button_middle)
+ mouse_cap |= 64;
+ if (buttons & ysfx_button_right)
+ mouse_cap |= 2;
+ *fx->var.mouse_cap = (EEL_F)mouse_cap;
+
+#else
+ (void)fx;
+ (void)mods;
+ (void)xpos;
+ (void)ypos;
+ (void)buttons;
+ (void)vwheel;
+ (void)hwheel;
+#endif
+}
+
+bool ysfx_gfx_run(ysfx_t *fx)
+{
+#if !defined(YSFX_NO_GFX)
+ bool doinit = true;
+ ysfx_scoped_gfx_t scope{fx, doinit};
+
+ if (!fx->gfx.ready)
+ return false;
+
+ ysfx_gfx_prepare(fx);
+ NSEEL_code_execute(fx->code.gfx.get());
+
+ return ysfx_gfx_state_is_dirty(fx->gfx.state.get());
+#else
+ (void)fx;
+#endif
+}
diff --git a/source/modules/ysfx/sources/ysfx.hpp b/source/modules/ysfx/sources/ysfx.hpp
new file mode 100644
index 000000000..94ae5fd42
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx.hpp
@@ -0,0 +1,192 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#pragma once
+#include "ysfx.h"
+#include "ysfx_midi.hpp"
+#include "ysfx_parse.hpp"
+#include "ysfx_api_eel.hpp"
+#include "ysfx_api_reaper.hpp"
+#include "ysfx_api_file.hpp"
+#include "ysfx_api_gfx.hpp"
+#include "ysfx_utils.hpp"
+#include "utility/sync_bitset.hpp"
+#include "WDL/eel2/ns-eel.h"
+#include "WDL/eel2/ns-eel-int.h"
+#include
+#include
+
+YSFX_DEFINE_AUTO_PTR(NSEEL_VMCTX_u, void, NSEEL_VM_free); // NOTE: `NSEEL_VMCTX` is `void *`
+YSFX_DEFINE_AUTO_PTR(NSEEL_CODEHANDLE_u, void, NSEEL_code_free); // NOTE: `NSEEL_CODEHANDLE` is `void *`
+
+struct ysfx_source_unit_t {
+ ysfx_toplevel_t toplevel;
+ ysfx_header_t header;
+};
+using ysfx_source_unit_u = std::unique_ptr< ysfx_source_unit_t>;
+
+enum ysfx_file_type_t {
+ ysfx_file_type_none,
+ ysfx_file_type_txt,
+ ysfx_file_type_raw,
+ ysfx_file_type_audio,
+};
+
+enum ysfx_thread_id_t {
+ ysfx_thread_id_none,
+ ysfx_thread_id_dsp,
+ ysfx_thread_id_gfx,
+};
+
+struct ysfx_s {
+ ysfx_config_u config;
+ eel_string_context_state_u string_ctx;
+ ysfx::mutex string_mutex;
+ ysfx::mutex atomic_mutex;
+ NSEEL_VMCTX_u vm;
+
+ // some default values, these are not standard, just arbitrary
+ uint32_t block_size = 128;
+ ysfx_real sample_rate = 44100;
+ uint32_t valid_input_channels = 2;
+
+ bool is_freshly_compiled = false;
+ bool must_compute_init = false;
+ bool must_compute_slider = false;
+
+ std::unordered_map slider_of_var;
+
+ // source
+ struct {
+ std::string main_file_path;
+ std::string bank_path;
+ ysfx_source_unit_u main;
+ std::vector imports;
+ std::unordered_map slider_alias;
+ } source;
+
+ // compilation
+ struct {
+ bool compiled = false;
+ std::vector init;
+ NSEEL_CODEHANDLE_u slider;
+ NSEEL_CODEHANDLE_u block;
+ NSEEL_CODEHANDLE_u sample;
+ NSEEL_CODEHANDLE_u gfx;
+ NSEEL_CODEHANDLE_u serialize;
+ } code;
+
+ // VM variables
+ struct {
+ EEL_F *spl[ysfx_max_channels] = {};
+ EEL_F *slider[ysfx_max_sliders] = {};
+ EEL_F *srate = nullptr;
+ EEL_F *num_ch = nullptr;
+ EEL_F *samplesblock = nullptr;
+ EEL_F *trigger = nullptr;
+ EEL_F *tempo = nullptr;
+ EEL_F *play_state = nullptr;
+ EEL_F *play_position = nullptr;
+ EEL_F *beat_position = nullptr;
+ EEL_F *ts_num = nullptr;
+ EEL_F *ts_denom = nullptr;
+ EEL_F *ext_noinit = nullptr;
+ EEL_F *ext_nodenorm = nullptr;
+ EEL_F *ext_midi_bus = nullptr;
+ EEL_F *midi_bus = nullptr;
+ EEL_F *pdc_delay = nullptr;
+ EEL_F *pdc_bot_ch = nullptr;
+ EEL_F *pdc_top_ch = nullptr;
+ EEL_F *pdc_midi = nullptr;
+ // gfx variables
+ EEL_F *gfx_r = nullptr;
+ EEL_F *gfx_g = nullptr;
+ EEL_F *gfx_b = nullptr;
+ EEL_F *gfx_a = nullptr;
+ EEL_F *gfx_a2 = nullptr;
+ EEL_F *gfx_w = nullptr;
+ EEL_F *gfx_h = nullptr;
+ EEL_F *gfx_x = nullptr;
+ EEL_F *gfx_y = nullptr;
+ EEL_F *gfx_mode = nullptr;
+ EEL_F *gfx_clear = nullptr;
+ EEL_F *gfx_texth = nullptr;
+ EEL_F *gfx_dest = nullptr;
+ EEL_F *gfx_ext_retina = nullptr;
+ EEL_F *mouse_x = nullptr;
+ EEL_F *mouse_y = nullptr;
+ EEL_F *mouse_cap = nullptr;
+ EEL_F *mouse_wheel = nullptr;
+ EEL_F *mouse_hwheel = nullptr;
+ // other
+ EEL_F ret_temp = 0;
+ } var;
+
+ // MIDI
+ struct {
+ ysfx_midi_buffer_u in;
+ ysfx_midi_buffer_u out;
+ } midi;
+
+ // Slider
+ struct {
+ ysfx::sync_bitset64 automate_mask;
+ ysfx::sync_bitset64 change_mask;
+ ysfx::sync_bitset64 visible_mask;
+ } slider;
+
+ // Triggers
+ uint32_t triggers = 0;
+
+ // Files
+ struct {
+ std::vector list;
+ ysfx::mutex list_mutex;
+ } file;
+
+#if !defined(YSFX_NO_GFX)
+ // Graphics
+ struct {
+ ysfx_gfx_state_u state;
+ ysfx::mutex mutex;
+ volatile bool ready = false;
+ volatile bool wants_retina = false;
+ std::atomic must_init{false};
+ } gfx;
+#endif
+
+ std::atomic ref_count{1};
+};
+
+ysfx_thread_id_t ysfx_get_thread_id();
+void ysfx_set_thread_id(ysfx_thread_id_t id);
+void ysfx_unload_source(ysfx_t *fx);
+void ysfx_unload_code(ysfx_t *fx);
+void ysfx_first_init(ysfx_t *fx);
+void ysfx_update_slider_visibility_mask(ysfx_t *fx);
+void ysfx_fill_file_enums(ysfx_t *fx);
+void ysfx_fix_invalid_enums(ysfx_t *fx);
+ysfx_section_t *ysfx_search_section(ysfx_t *fx, uint32_t type, ysfx_toplevel_t **origin = nullptr);
+std::string ysfx_resolve_import_path(ysfx_t *fx, const std::string &name, const std::string &origin);
+uint32_t ysfx_current_midi_bus(ysfx_t *fx);
+void ysfx_clear_files(ysfx_t *fx);
+ysfx_file_t *ysfx_get_file(ysfx_t *fx, uint32_t handle, std::unique_lock &lock, std::unique_lock *list_lock = nullptr);
+int32_t ysfx_insert_file(ysfx_t *fx, ysfx_file_t *file);
+void ysfx_serialize(ysfx_t *fx);
+uint32_t ysfx_get_slider_of_var(ysfx_t *fx, EEL_F *var);
+bool ysfx_find_data_file(ysfx_t *fx, EEL_F *file, std::string &result);
+ysfx_file_type_t ysfx_detect_file_type(ysfx_t *fx, const char *path, void **fmtobj);
diff --git a/source/modules/ysfx/sources/ysfx_api_eel.cpp b/source/modules/ysfx/sources/ysfx_api_eel.cpp
new file mode 100644
index 000000000..2e59b0dbb
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx_api_eel.cpp
@@ -0,0 +1,144 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#include "ysfx.hpp"
+#include "ysfx_api_eel.hpp"
+#include "ysfx_utils.hpp"
+#include
+#include
+#include
+
+#include "WDL/ptrlist.h"
+#include "WDL/assocarray.h"
+#include "WDL/mutex.h"
+
+#ifndef EELSCRIPT_NO_STDIO
+# define EEL_STRING_STDOUT_WRITE(x,len) { fwrite(x,len,1,stdout); fflush(stdout); }
+#endif
+
+//TODO: thread-safety considerations with strings
+
+#define EEL_STRING_MAXUSERSTRING_LENGTH_HINT ysfx_string_max_length
+
+static ysfx::mutex atomic_mutex;
+#define EEL_ATOMIC_SET_SCOPE(opaque) ysfx::mutex *mutex = ((opaque) ? &((ysfx_t *)(opaque))->atomic_mutex : &atomic_mutex);
+#define EEL_ATOMIC_ENTER mutex->lock()
+#define EEL_ATOMIC_LEAVE mutex->unlock()
+
+#include "WDL/eel2/eel_strings.h"
+#include "WDL/eel2/eel_misc.h"
+#include "WDL/eel2/eel_fft.h"
+#include "WDL/eel2/eel_mdct.h"
+#include "WDL/eel2/eel_atomic.h"
+
+//------------------------------------------------------------------------------
+void ysfx_api_init_eel()
+{
+ EEL_string_register();
+ EEL_fft_register();
+ EEL_mdct_register();
+ EEL_string_register();
+ EEL_misc_register();
+ EEL_atomic_register();
+}
+
+//------------------------------------------------------------------------------
+void ysfx_eel_string_initvm(NSEEL_VMCTX vm)
+{
+ eel_string_initvm(vm);
+}
+
+//------------------------------------------------------------------------------
+eel_string_context_state *ysfx_eel_string_context_new()
+{
+ return new eel_string_context_state;
+}
+
+void ysfx_eel_string_context_free(eel_string_context_state *state)
+{
+ delete state;
+}
+
+void ysfx_eel_string_context_update_named_vars(eel_string_context_state *state, NSEEL_VMCTX vm)
+{
+ state->update_named_vars(vm);
+}
+
+//------------------------------------------------------------------------------
+static_assert(
+ ysfx_string_max_length == EEL_STRING_MAXUSERSTRING_LENGTH_HINT,
+ "string max lengths do not match");
+
+bool ysfx_string_access(ysfx_t *fx, ysfx_real id, bool for_write, void (*access)(void *, WDL_FastString &), void *userdata)
+{
+ void *opaque = fx;
+ eel_string_context_state *ctx = EEL_STRING_GET_CONTEXT_POINTER(opaque);
+ EEL_STRING_MUTEXLOCK_SCOPE
+
+ EEL_STRING_STORAGECLASS *wr = nullptr;
+ ctx->GetStringForIndex(id, &wr, for_write);
+ if (!wr)
+ return false;
+
+ access(userdata, *wr);
+ return true;
+}
+
+bool ysfx_string_get(ysfx_t *fx, ysfx_real id, std::string &txt)
+{
+ return ysfx_string_access(fx, id, false, [](void *ud, WDL_FastString &str) {
+ ((std::string *)ud)->assign(str.Get(), (uint32_t)str.GetLength());
+ }, &txt);
+}
+
+bool ysfx_string_set(ysfx_t *fx, ysfx_real id, const std::string &txt)
+{
+ return ysfx_string_access(fx, id, true, [](void *ud, WDL_FastString &str) {
+ const std::string *txt = (const std::string *)ud;
+ size_t size = txt->size();
+ if (size > ysfx_string_max_length)
+ size = ysfx_string_max_length;
+ str.SetRaw(txt->data(), (int)size);
+ }, (void *)&txt);
+}
+
+void ysfx_string_lock(ysfx_t *fx)
+{
+ fx->string_mutex.lock();
+}
+
+void ysfx_string_unlock(ysfx_t *fx)
+{
+ fx->string_mutex.unlock();
+}
+
+const char *ysfx_string_access_unlocked(ysfx_t *fx, ysfx_real id, WDL_FastString **fs, bool for_write)
+{
+ return fx->string_ctx->GetStringForIndex(id, fs, for_write);
+}
+
+//------------------------------------------------------------------------------
+// NOTE(jpc) implement this? I guess probably not.
+// DSP and UI should not mutex each other.
+
+void NSEEL_HOSTSTUB_EnterMutex()
+{
+}
+
+void NSEEL_HOSTSTUB_LeaveMutex()
+{
+}
diff --git a/source/modules/ysfx/sources/ysfx_api_eel.hpp b/source/modules/ysfx/sources/ysfx_api_eel.hpp
new file mode 100644
index 000000000..494fbb18c
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx_api_eel.hpp
@@ -0,0 +1,57 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#pragma once
+#include "ysfx.h"
+#include
+
+typedef void *NSEEL_VMCTX;
+class WDL_FastString;
+
+//------------------------------------------------------------------------------
+void ysfx_api_init_eel();
+
+//------------------------------------------------------------------------------
+void ysfx_eel_string_initvm(NSEEL_VMCTX vm);
+
+//------------------------------------------------------------------------------
+class eel_string_context_state;
+eel_string_context_state *ysfx_eel_string_context_new();
+void ysfx_eel_string_context_free(eel_string_context_state *state);
+void ysfx_eel_string_context_update_named_vars(eel_string_context_state *state, NSEEL_VMCTX vm);
+YSFX_DEFINE_AUTO_PTR(eel_string_context_state_u, eel_string_context_state, ysfx_eel_string_context_free);
+
+//------------------------------------------------------------------------------
+enum { ysfx_string_max_length = 1 << 16 };
+bool ysfx_string_access(ysfx_t *fx, ysfx_real id, bool for_write, void (*access)(void *, WDL_FastString &), void *userdata);
+bool ysfx_string_get(ysfx_t *fx, ysfx_real id, std::string &txt);
+bool ysfx_string_set(ysfx_t *fx, ysfx_real id, const std::string &txt);
+void ysfx_string_lock(ysfx_t *fx);
+void ysfx_string_unlock(ysfx_t *fx);
+const char *ysfx_string_access_unlocked(ysfx_t *fx, ysfx_real id, WDL_FastString **fs, bool for_write);
+
+struct ysfx_string_scoped_lock {
+ ysfx_string_scoped_lock(ysfx_t *fx) : m_fx(fx) { ysfx_string_lock(fx); }
+ ~ysfx_string_scoped_lock() { ysfx_string_unlock(m_fx); }
+private:
+ ysfx_t *m_fx = nullptr;
+};
+
+#define EEL_STRING_GET_CONTEXT_POINTER(opaque) (((ysfx_t *)(opaque))->string_ctx.get())
+#define EEL_STRING_GET_FOR_INDEX(x, wr) (ysfx_string_access_unlocked((ysfx_t *)(opaque), x, wr, false))
+#define EEL_STRING_GET_FOR_WRITE(x, wr) (ysfx_string_access_unlocked((ysfx_t *)(opaque), x, wr, true))
+#define EEL_STRING_MUTEXLOCK_SCOPE ysfx_string_scoped_lock lock{(ysfx_t *)(opaque)};
diff --git a/source/modules/ysfx/sources/ysfx_api_file.cpp b/source/modules/ysfx/sources/ysfx_api_file.cpp
new file mode 100644
index 000000000..95a9f9a44
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx_api_file.cpp
@@ -0,0 +1,574 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#include "ysfx.hpp"
+#include "ysfx_config.hpp"
+#include "ysfx_api_file.hpp"
+#include "ysfx_eel_utils.hpp"
+#include
+#include
+#include
+
+ysfx_raw_file_t::ysfx_raw_file_t(NSEEL_VMCTX vm, const char *filename)
+ : m_vm(vm),
+ m_stream(ysfx::fopen_utf8(filename, "rb"))
+{
+}
+
+int32_t ysfx_raw_file_t::avail()
+{
+ if (!m_stream)
+ return 0;
+
+ int64_t cur_off = ysfx::ftell_lfs(m_stream.get());
+ if (cur_off == -1)
+ return 0;
+
+ if (ysfx::fseek_lfs(m_stream.get(), 0, SEEK_END) == -1)
+ return 0;
+
+ int64_t end_off = ysfx::ftell_lfs(m_stream.get());
+ if (end_off == -1)
+ return 0;
+
+ if (ysfx::fseek_lfs(m_stream.get(), cur_off, SEEK_SET) == -1)
+ return 0;
+
+ if ((uint64_t)end_off < (uint64_t)cur_off)
+ return 0;
+
+ uint64_t byte_count = (uint64_t)end_off - (uint64_t)cur_off;
+ uint64_t f32_count = byte_count / 4;
+ return (f32_count > 0x7fffffff) ? 0x7fffffff : (uint32_t)f32_count;
+}
+
+void ysfx_raw_file_t::rewind()
+{
+ if (!m_stream)
+ return;
+
+ ::rewind(m_stream.get());
+}
+
+bool ysfx_raw_file_t::var(ysfx_real *var)
+{
+ if (!m_stream)
+ return false;
+
+ uint8_t data[4];
+ if (fread(data, 1, 4, m_stream.get()) != 4)
+ return false;
+
+ *var = (EEL_F)ysfx::unpack_f32le(data);
+ return true;
+}
+
+uint32_t ysfx_raw_file_t::mem(uint32_t offset, uint32_t length)
+{
+ if (!m_stream)
+ return 0;
+
+ ysfx_eel_ram_writer writer{m_vm, offset};
+
+ uint32_t read;
+ for (read = 0; read < length; ++read) {
+ ysfx_real value;
+ if (!var(&value))
+ break;
+ writer.write_next(value);
+ }
+
+ return read;
+}
+
+uint32_t ysfx_raw_file_t::string(std::string &str)
+{
+ if (!m_stream)
+ return 0;
+
+ uint8_t data[4];
+ if (fread(data, 1, 4, m_stream.get()) != 4)
+ return 0;
+
+ str.clear();
+
+ uint32_t srclen = ysfx::unpack_u32le(data);
+ str.reserve((srclen < ysfx_string_max_length) ? srclen : ysfx_string_max_length);
+
+ uint32_t count = 0;
+ for (int byte; count < srclen && (byte = fgetc(m_stream.get())) != EOF; ) {
+ if (str.size() < ysfx_string_max_length)
+ str.push_back((unsigned char)byte);
+ ++count;
+ }
+ return count;
+}
+
+//------------------------------------------------------------------------------
+ysfx_text_file_t::ysfx_text_file_t(NSEEL_VMCTX vm, const char *filename)
+ : m_vm(vm),
+ m_stream(ysfx::fopen_utf8(filename, "rb"))
+{
+ m_buf.reserve(256);
+}
+
+int32_t ysfx_text_file_t::avail()
+{
+ if (!m_stream || ferror(m_stream.get()))
+ return -1;
+
+ return (feof(m_stream.get()) == 0) ? 0 : 1;
+}
+
+void ysfx_text_file_t::rewind()
+{
+ if (!m_stream)
+ return;
+
+ ::rewind(m_stream.get());
+}
+
+bool ysfx_text_file_t::var(ysfx_real *var)
+{
+ if (!m_stream)
+ return false;
+
+ //TODO support the expression language for arithmetic
+
+ int ch;
+ do {
+ // get the next number separated by newline or comma
+ // but skip invalid lines
+ m_buf.clear();
+ while ((ch = fgetc(m_stream.get())) != EOF && ch != '\n' && ch != ',')
+ m_buf.push_back((unsigned char)ch);
+ const char *startp = m_buf.c_str();
+ const char *endp = (char *)startp;
+ double value = ysfx::dot_strtod(startp, (char **)&endp);
+ if (endp != startp) {
+ *var = (EEL_F)value;
+ return true;
+ }
+ } while (ch != EOF);
+
+ return false;
+}
+
+uint32_t ysfx_text_file_t::mem(uint32_t offset, uint32_t length)
+{
+ if (!m_stream)
+ return 0;
+
+ ysfx_eel_ram_writer writer{m_vm, offset};
+
+ uint32_t read;
+ for (read = 0; read < length; ++read) {
+ ysfx_real value;
+ if (!var(&value))
+ break;
+ writer.write_next(value);
+ }
+
+ return read;
+}
+
+uint32_t ysfx_text_file_t::string(std::string &str)
+{
+ if (!m_stream)
+ return 0;
+
+ str.clear();
+ str.reserve(256);
+
+ int ch;
+ do {
+ ch = fgetc(m_stream.get());
+ if (ch != EOF && str.size() < ysfx_string_max_length)
+ str.push_back((unsigned char)ch);
+ } while (ch != EOF && ch != '\n');
+
+ return (uint32_t)str.size();
+}
+
+//------------------------------------------------------------------------------
+ysfx_audio_file_t::ysfx_audio_file_t(NSEEL_VMCTX vm, const ysfx_audio_format_t &fmt, const char *filename)
+ : m_vm(vm),
+ m_fmt(fmt),
+ m_reader(fmt.open(filename), fmt.close)
+{
+}
+
+int32_t ysfx_audio_file_t::avail()
+{
+ if (!m_reader)
+ return -1;
+
+ uint64_t avail = m_fmt.avail(m_reader.get());
+ return (avail > 0x7fffffff) ? 0x7fffffff : (int32_t)avail;
+}
+
+void ysfx_audio_file_t::rewind()
+{
+ if (!m_reader)
+ return;
+
+ m_fmt.rewind(m_reader.get());
+}
+
+bool ysfx_audio_file_t::var(ysfx_real *var)
+{
+ if (!m_reader)
+ return false;
+
+ return m_fmt.read(m_reader.get(), var, 1) == 1;
+}
+
+uint32_t ysfx_audio_file_t::mem(uint32_t offset, uint32_t length)
+{
+ if (!m_reader)
+ return 0;
+
+ uint32_t numread = 0;
+ ysfx_real *buf = m_buf.get();
+ ysfx_eel_ram_writer writer(m_vm, offset);
+
+ while (numread < length) {
+ uint32_t n = length - numread;
+ if (n > buffer_size)
+ n = buffer_size;
+
+ uint32_t m = (uint32_t)m_fmt.read(m_reader.get(), buf, n);
+ for (uint32_t i = 0; i < m; ++i)
+ writer.write_next(buf[i]);
+
+ numread += m;
+ if (m < n)
+ break;
+ }
+
+ return numread;
+}
+
+uint32_t ysfx_audio_file_t::string(std::string &str)
+{
+ (void)str;
+ return 0;
+}
+
+bool ysfx_audio_file_t::riff(uint32_t &nch, ysfx_real &samplerate)
+{
+ if (!m_reader)
+ return false;
+
+ ysfx_audio_file_info_t info = m_fmt.info(m_reader.get());
+ nch = info.channels;
+ samplerate = info.sample_rate;
+ return true;
+}
+
+//------------------------------------------------------------------------------
+ysfx_serializer_t::ysfx_serializer_t(NSEEL_VMCTX vm)
+ : m_vm(vm)
+{
+}
+
+void ysfx_serializer_t::begin(bool write, std::string &buffer)
+{
+ m_write = (int)write;
+ m_buffer = &buffer;
+ m_pos = 0;
+}
+
+void ysfx_serializer_t::end()
+{
+ m_write = -1;
+ m_buffer = nullptr;
+}
+
+int32_t ysfx_serializer_t::avail()
+{
+ if (m_write)
+ return -1;
+ else
+ return 0;
+}
+
+void ysfx_serializer_t::rewind()
+{
+}
+
+bool ysfx_serializer_t::var(ysfx_real *var)
+{
+ if (m_write == 1) {
+ uint8_t buf[4];
+ ysfx::pack_f32le((float)*var, buf);
+ m_buffer->append((char *)buf, 4);
+ return true;
+ }
+ else if (m_write == 0) {
+ if (m_pos + 4 > m_buffer->size()) {
+ m_pos = m_buffer->size();
+ *var = 0;
+ return false;
+ }
+ *var = (EEL_F)ysfx::unpack_f32le((uint8_t *)&(*m_buffer)[m_pos]);
+ m_pos += 4;
+ return true;
+ }
+ return false;
+}
+
+uint32_t ysfx_serializer_t::mem(uint32_t offset, uint32_t length)
+{
+ if (m_write == 1) {
+ ysfx_eel_ram_reader reader{m_vm, offset};
+ for (uint32_t i = 0; i < length; ++i) {
+ ysfx_real value = reader.read_next();
+ if (!var(&value))
+ return i;
+ }
+ return length;
+ }
+ else if (m_write == 0) {
+ ysfx_eel_ram_writer writer{m_vm, offset};
+ for (uint32_t i = 0; i < length; ++i) {
+ ysfx_real value{};
+ if (!var(&value))
+ return i;
+ writer.write_next(value);
+ }
+ return length;
+ }
+ return 0;
+}
+
+uint32_t ysfx_serializer_t::string(std::string &str)
+{
+ // TODO implement me; docs claim support in Reaper 4.59+ but it seems
+ // non-working (as of Reaper 6.40)
+ return 0;
+}
+
+//------------------------------------------------------------------------------
+static EEL_F NSEEL_CGEN_CALL ysfx_api_file_open(void *opaque, EEL_F *file_)
+{
+ ysfx_t *fx = (ysfx_t *)opaque;
+
+ std::string filepath;
+ if (!ysfx_find_data_file(fx, file_, filepath))
+ return -1;
+
+ void *fmtobj = nullptr;
+ ysfx_file_type_t ftype = ysfx_detect_file_type(fx, filepath.c_str(), &fmtobj);
+
+ ysfx_file_u file;
+ switch (ftype) {
+ case ysfx_file_type_txt:
+ file.reset(new ysfx_text_file_t(fx->vm.get(), filepath.c_str()));
+ break;
+ case ysfx_file_type_raw:
+ file.reset(new ysfx_raw_file_t(fx->vm.get(), filepath.c_str()));
+ break;
+ case ysfx_file_type_audio:
+ file.reset(new ysfx_audio_file_t(fx->vm.get(), *(ysfx_audio_format_t *)fmtobj, filepath.c_str()));
+ break;
+ case ysfx_file_type_none:
+ break;
+ default:
+ assert(false);
+ }
+
+ if (file) {
+ int32_t handle = ysfx_insert_file(fx, file.get());
+ if (handle == -1)
+ return -1;
+ (void)file.release();
+ return (EEL_F)(uint32_t)handle;
+ }
+
+ return -1;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_file_close(void *opaque, EEL_F *handle_)
+{
+ int32_t handle = ysfx_eel_round(*handle_);
+ if (handle <= 0) //NOTE: cannot close the serializer handle (0)
+ return -1;
+
+ ysfx_t *fx = (ysfx_t *)opaque;
+ std::unique_ptr file_mutex;
+ std::unique_lock lock;
+ std::unique_lock list_lock;
+
+ // hold both locks to protect file and list access during removal
+ if (!ysfx_get_file(fx, (uint32_t)handle, lock, &list_lock))
+ return -1;
+
+ // preserve the locked mutex of the object being removed
+ file_mutex = std::move(fx->file.list[(uint32_t)handle]->m_mutex);
+
+ fx->file.list[(uint32_t)handle].reset();
+ return 0;
+}
+
+static EEL_F *NSEEL_CGEN_CALL ysfx_api_file_rewind(void *opaque, EEL_F *handle_)
+{
+ int32_t handle = ysfx_eel_round(*handle_);
+ if (handle < 0)
+ return handle_;
+
+ ysfx_t *fx = (ysfx_t *)opaque;
+ std::unique_lock lock;
+ ysfx_file_t *file = ysfx_get_file(fx, (uint32_t)handle, lock);
+ if (!file)
+ return 0;
+
+ file->rewind();
+ return handle_;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_file_var(void *opaque, EEL_F *handle_, EEL_F *var)
+{
+ int32_t handle = ysfx_eel_round(*handle_);
+ if (handle < 0)
+ return 0;
+
+ ysfx_t *fx = (ysfx_t *)opaque;
+ std::unique_lock lock;
+ ysfx_file_t *file = ysfx_get_file(fx, (uint32_t)handle, lock);
+ if (!file)
+ return 0;
+
+ if (!file->var(var))
+ return 0;
+
+ return 1;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_file_mem(void *opaque, EEL_F *handle_, EEL_F *offset_, EEL_F *length_)
+{
+ int32_t handle = ysfx_eel_round(*handle_);
+ int32_t offset = ysfx_eel_round(*offset_);
+ int32_t length = ysfx_eel_round(*length_);
+ if (handle < 0 || offset < 0 || length <= 0)
+ return 0;
+
+ ysfx_t *fx = (ysfx_t *)opaque;
+ std::unique_lock lock;
+ ysfx_file_t *file = ysfx_get_file(fx, (uint32_t)handle, lock);
+ if (!file)
+ return 0;
+
+ return (EEL_F)file->mem((uint32_t)offset, (uint32_t)length);
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_file_avail(void *opaque, EEL_F *handle_)
+{
+ int32_t handle = ysfx_eel_round(*handle_);
+ if (handle < 0)
+ return 0;
+
+ ysfx_t *fx = (ysfx_t *)opaque;
+ std::unique_lock lock;
+ ysfx_file_t *file = ysfx_get_file(fx, (uint32_t)handle, lock);
+ if (!file)
+ return 0;
+
+ return file->avail();
+}
+
+static EEL_F *NSEEL_CGEN_CALL ysfx_api_file_riff(void *opaque, EEL_F *handle_, EEL_F *nch_, EEL_F *samplerate_)
+{
+ int32_t handle = ysfx_eel_round(*handle_);
+ if (handle < 0)
+ return 0;
+
+ ysfx_t *fx = (ysfx_t *)opaque;
+ std::unique_lock lock;
+ ysfx_file_t *file = ysfx_get_file(fx, (uint32_t)handle, lock);
+ if (!file) {
+ *nch_ = 0;
+ *samplerate_ = 0;
+ return nch_;
+ }
+
+ uint32_t nch = 0;
+ ysfx_real samplerate = 0;
+ if (!file->riff(nch, samplerate)) {
+ *nch_ = 0;
+ *samplerate_ = 0;
+ return nch_;
+ }
+
+ *nch_ = (EEL_F)nch;
+ *samplerate_ = samplerate;
+ return nch_;
+}
+
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_file_text(void *opaque, EEL_F *handle_)
+{
+ int32_t handle = ysfx_eel_round(*handle_);
+ if (handle < 0)
+ return 0;
+
+ ysfx_t *fx = (ysfx_t *)opaque;
+ std::unique_lock lock;
+ ysfx_file_t *file = ysfx_get_file(fx, (uint32_t)handle, lock);
+ if (!file)
+ return 0;
+
+ return (EEL_F)file->is_text();
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_file_string(void *opaque, EEL_F *handle_, EEL_F *string_)
+{
+ int32_t handle = ysfx_eel_round(*handle_);
+ if (handle < 0)
+ return 0;
+
+ ysfx_t *fx = (ysfx_t *)opaque;
+ std::unique_lock lock;
+ ysfx_file_t *file = ysfx_get_file(fx, (uint32_t)handle, lock);
+ if (!file)
+ return 0;
+
+ std::string txt;
+ uint32_t count;
+ if (!file->is_in_write_mode()) {
+ count = file->string(txt);
+ ysfx_string_set(fx, *string_, txt);
+ }
+ else {
+ ysfx_string_get(fx, *string_, txt);
+ count = file->string(txt);
+ }
+ return (EEL_F)count;
+}
+
+void ysfx_api_init_file()
+{
+ NSEEL_addfunc_retval("file_open", 1, NSEEL_PProc_THIS, &ysfx_api_file_open);
+ NSEEL_addfunc_retval("file_close", 1, NSEEL_PProc_THIS, &ysfx_api_file_close);
+ NSEEL_addfunc_retptr("file_rewind", 1, NSEEL_PProc_THIS, &ysfx_api_file_rewind);
+ NSEEL_addfunc_retval("file_var", 2, NSEEL_PProc_THIS, &ysfx_api_file_var);
+ NSEEL_addfunc_retval("file_mem", 3, NSEEL_PProc_THIS, &ysfx_api_file_mem);
+ NSEEL_addfunc_retval("file_avail", 1, NSEEL_PProc_THIS, &ysfx_api_file_avail);
+ NSEEL_addfunc_retptr("file_riff", 3, NSEEL_PProc_THIS, &ysfx_api_file_riff);
+ NSEEL_addfunc_retval("file_text", 1, NSEEL_PProc_THIS, &ysfx_api_file_text);
+ NSEEL_addfunc_retval("file_string", 2, NSEEL_PProc_THIS, &ysfx_api_file_string);
+}
diff --git a/source/modules/ysfx/sources/ysfx_api_file.hpp b/source/modules/ysfx/sources/ysfx_api_file.hpp
new file mode 100644
index 000000000..f94e7ebb5
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx_api_file.hpp
@@ -0,0 +1,127 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#pragma once
+#include "ysfx.h"
+#include "ysfx_utils.hpp"
+#include "WDL/eel2/ns-eel.h"
+#include "WDL/eel2/ns-eel-int.h"
+#include
+#include
+
+struct ysfx_file_t {
+ virtual ~ysfx_file_t() {}
+
+ virtual int32_t avail() = 0;
+ virtual void rewind() = 0;
+ virtual bool var(ysfx_real *var) = 0;
+ virtual uint32_t mem(uint32_t offset, uint32_t length) = 0;
+ virtual uint32_t string(std::string &str) = 0;
+ virtual bool riff(uint32_t &nch, ysfx_real &samplerate) = 0;
+ virtual bool is_text() = 0;
+ virtual bool is_in_write_mode() = 0;
+
+ std::unique_ptr m_mutex{new ysfx::mutex};
+};
+
+using ysfx_file_u = std::unique_ptr;
+
+//------------------------------------------------------------------------------
+
+struct ysfx_raw_file_t final : ysfx_file_t {
+ ysfx_raw_file_t(NSEEL_VMCTX vm, const char *filename);
+
+ int32_t avail() override;
+ void rewind() override;
+ bool var(ysfx_real *var) override;
+ uint32_t mem(uint32_t offset, uint32_t length) override;
+ uint32_t string(std::string &str) override;
+ bool riff(uint32_t &, ysfx_real &) override { return false; }
+ bool is_text() override { return false; }
+ bool is_in_write_mode() override { return false; }
+
+ NSEEL_VMCTX m_vm = nullptr;
+ ysfx::FILE_u m_stream;
+};
+
+//------------------------------------------------------------------------------
+
+struct ysfx_text_file_t final : ysfx_file_t {
+ ysfx_text_file_t(NSEEL_VMCTX vm, const char *filename);
+
+ int32_t avail() override;
+ void rewind() override;
+ bool var(ysfx_real *var) override;
+ uint32_t mem(uint32_t offset, uint32_t length) override;
+ uint32_t string(std::string &str) override;
+ bool riff(uint32_t &, ysfx_real &) override { return false; }
+ bool is_text() override { return true; }
+ bool is_in_write_mode() override { return false; }
+
+ NSEEL_VMCTX m_vm = nullptr;
+ ysfx::FILE_u m_stream;
+ std::string m_buf;
+};
+
+//------------------------------------------------------------------------------
+
+struct ysfx_audio_file_t final : ysfx_file_t {
+ ysfx_audio_file_t(NSEEL_VMCTX vm, const ysfx_audio_format_t &fmt, const char *filename);
+
+ int32_t avail() override;
+ void rewind() override;
+ bool var(ysfx_real *var) override;
+ uint32_t mem(uint32_t offset, uint32_t length) override;
+ uint32_t string(std::string &str) override;
+ bool riff(uint32_t &nch, ysfx_real &samplerate) override;
+ bool is_text() override { return false; }
+ bool is_in_write_mode() override { return false; }
+
+ NSEEL_VMCTX m_vm = nullptr;
+ ysfx_audio_format_t m_fmt{};
+ std::unique_ptr m_reader;
+ enum { buffer_size = 256 };
+ std::unique_ptr m_buf{new ysfx_real[buffer_size]};
+};
+
+//------------------------------------------------------------------------------
+
+struct ysfx_serializer_t final : ysfx_file_t {
+ explicit ysfx_serializer_t(NSEEL_VMCTX vm);
+
+ void begin(bool write, std::string &buffer);
+ void end();
+
+ int32_t avail() override;
+ void rewind() override;
+ bool var(ysfx_real *var) override;
+ uint32_t mem(uint32_t offset, uint32_t length) override;
+ uint32_t string(std::string &str) override;
+ bool riff(uint32_t &, ysfx_real &) override { return false; }
+ bool is_text() override { return false; }
+ bool is_in_write_mode() override { return m_write == 1; }
+
+ NSEEL_VMCTX m_vm{};
+ int m_write = -1;
+ std::string *m_buffer = nullptr;
+ size_t m_pos = 0;
+};
+
+using ysfx_serializer_u = std::unique_ptr;
+
+//------------------------------------------------------------------------------
+void ysfx_api_init_file();
diff --git a/source/modules/ysfx/sources/ysfx_api_gfx.cpp b/source/modules/ysfx/sources/ysfx_api_gfx.cpp
new file mode 100644
index 000000000..e9a0cfa55
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx_api_gfx.cpp
@@ -0,0 +1,470 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#include "ysfx.hpp"
+#include "ysfx_config.hpp"
+#include "ysfx_api_gfx.hpp"
+#include "ysfx_eel_utils.hpp"
+#if !defined(YSFX_NO_GFX)
+# include "lice_stb/lice_stb_loaders.hpp"
+# define WDL_NO_DEFINE_MINMAX
+# include "WDL/swell/swell.h"
+# include "WDL/lice/lice.h"
+# include "WDL/lice/lice_text.h"
+# include "WDL/wdlstring.h"
+#endif
+#include
+#include
+#include
+#include
+#include
+#include
+
+#if !defined(YSFX_NO_GFX)
+#define GFX_GET_CONTEXT(opaque) (((opaque)) ? ysfx_gfx_get_context((ysfx_t *)(opaque)) : nullptr)
+
+enum {
+ ysfx_gfx_max_images = 1024,
+ ysfx_gfx_max_fonts = 128,
+ ysfx_gfx_max_input = 1024,
+};
+
+class eel_lice_state;
+
+struct ysfx_gfx_state_t {
+ ysfx_gfx_state_t(ysfx_t *fx);
+ ~ysfx_gfx_state_t();
+ std::unique_ptr lice;
+ std::queue input_queue;
+ std::unordered_set keys_pressed;
+ ysfx_real scale = 0.0;
+ void *callback_data = nullptr;
+ int (*show_menu)(void *, const char *, int32_t, int32_t) = nullptr;
+ void (*set_cursor)(void *, int32_t) = nullptr;
+ const char *(*get_drop_file)(void *user_data, int32_t index) = nullptr;
+};
+
+//------------------------------------------------------------------------------
+#if !defined(YSFX_NO_GFX)
+static bool eel_lice_get_filename_for_string(void *opaque, EEL_F idx, WDL_FastString *fs, int iswrite)
+{
+ if (iswrite)
+ return false; // this is neither supported nor used
+
+ ysfx_t *fx = (ysfx_t *)opaque;
+
+ std::string filepath;
+ if (!ysfx_find_data_file(fx, &idx, filepath))
+ return false;
+
+ if (fs) fs->Set(filepath.data(), (uint32_t)filepath.size());
+ return true;
+}
+
+#define EEL_LICE_GET_FILENAME_FOR_STRING(idx, fs, p) \
+ eel_lice_get_filename_for_string(opaque, (idx), (fs), (p))
+
+#endif
+
+//------------------------------------------------------------------------------
+#if !defined(YSFX_NO_GFX)
+# include "ysfx_api_gfx_lice.hpp"
+#else
+# include "ysfx_api_gfx_dummy.hpp"
+#endif
+
+//------------------------------------------------------------------------------
+#if !defined(YSFX_NO_GFX)
+
+static bool translate_special_key(uint32_t uni_key, uint32_t &jsfx_key)
+{
+ auto key_c = [](uint8_t a, uint8_t b, uint8_t c, uint8_t d) -> uint32_t {
+ return a | (b << 8) | (c << 16) | (d << 24);
+ };
+
+ switch (uni_key) {
+ default: return false;
+ case ysfx_key_delete: jsfx_key = key_c('d', 'e', 'l', 0); break;
+ case ysfx_key_f1: jsfx_key = key_c('f', '1', 0, 0); break;
+ case ysfx_key_f2: jsfx_key = key_c('f', '2', 0, 0); break;
+ case ysfx_key_f3: jsfx_key = key_c('f', '3', 0, 0); break;
+ case ysfx_key_f4: jsfx_key = key_c('f', '4', 0, 0); break;
+ case ysfx_key_f5: jsfx_key = key_c('f', '5', 0, 0); break;
+ case ysfx_key_f6: jsfx_key = key_c('f', '6', 0, 0); break;
+ case ysfx_key_f7: jsfx_key = key_c('f', '7', 0, 0); break;
+ case ysfx_key_f8: jsfx_key = key_c('f', '8', 0, 0); break;
+ case ysfx_key_f9: jsfx_key = key_c('f', '9', 0, 0); break;
+ case ysfx_key_f10: jsfx_key = key_c('f', '1', '0', 0); break;
+ case ysfx_key_f11: jsfx_key = key_c('f', '1', '1', 0); break;
+ case ysfx_key_f12: jsfx_key = key_c('f', '1', '2', 0); break;
+ case ysfx_key_left: jsfx_key = key_c('l', 'e', 'f', 't'); break;
+ case ysfx_key_up: jsfx_key = key_c('u', 'p', 0, 0); break;
+ case ysfx_key_right: jsfx_key = key_c('r', 'g', 'h', 't'); break;
+ case ysfx_key_down: jsfx_key = key_c('d', 'o', 'w', 'n'); break;
+ case ysfx_key_page_up: jsfx_key = key_c('p', 'g', 'u', 'p'); break;
+ case ysfx_key_page_down: jsfx_key = key_c('p', 'g', 'd', 'n'); break;
+ case ysfx_key_home: jsfx_key = key_c('h', 'o', 'm', 'e'); break;
+ case ysfx_key_end: jsfx_key = key_c('e', 'n', 'd', 0); break;
+ case ysfx_key_insert: jsfx_key = key_c('i', 'n', 's', 0); break;
+ }
+
+ return true;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_gfx_getchar(void *opaque, EEL_F *p)
+{
+ ysfx_gfx_state_t *state = GFX_GET_CONTEXT(opaque);
+ if (!state)
+ return 0;
+
+ if (*p >= 1/*2*/) { // NOTE(jpc) this is 2.0 originally, which seems wrong
+ if (*p == 65536) {
+ // TODO implement window flags
+ return 0;
+ }
+
+ // current key down status
+ uint32_t key = (uint32_t)*p;
+ uint32_t key_id;
+ if (translate_special_key(key, key))
+ key_id = key;
+ else if (key < 256)
+ key_id = ysfx::latin1_tolower(key);
+ else // support the Latin-1 character set only
+ return 0;
+ return (EEL_F)(state->keys_pressed.find(key_id) != state->keys_pressed.end());
+ }
+
+ if (!state->input_queue.empty()) {
+ uint32_t key = state->input_queue.front();
+ state->input_queue.pop();
+ return (EEL_F)key;
+ }
+
+ return 0;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_gfx_showmenu(void *opaque, INT_PTR nparms, EEL_F **parms)
+{
+ ysfx_gfx_state_t *state = GFX_GET_CONTEXT(opaque);
+ if (!state || !state->show_menu)
+ return 0;
+
+ ysfx_t *fx = (ysfx_t *)state->lice->m_user_ctx;
+
+ std::string desc;
+ if (!ysfx_string_get(fx, *parms[0], desc) || desc.empty())
+ return 0;
+
+ int32_t x = (int32_t)*fx->var.gfx_x;
+ int32_t y = (int32_t)*fx->var.gfx_y;
+ return state->show_menu(state->callback_data, desc.c_str(), x, y);
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_gfx_setcursor(void *opaque, INT_PTR nparms, EEL_F **parms)
+{
+ ysfx_gfx_state_t *state = GFX_GET_CONTEXT(opaque);
+ if (!state || !state->set_cursor)
+ return 0;
+
+ int32_t id = (int32_t)*parms[0];
+ state->set_cursor(state->callback_data, id);
+ return 0;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_gfx_getdropfile(void *opaque, INT_PTR np, EEL_F **parms)
+{
+ ysfx_gfx_state_t *state = GFX_GET_CONTEXT(opaque);
+ if (!state || !state->get_drop_file)
+ return 0;
+
+ const int32_t idx = (int)*parms[0];
+ if (idx < 0) {
+ state->get_drop_file(state->callback_data, -1);
+ return 0;
+ }
+
+ const char *file = state->get_drop_file(state->callback_data, idx);
+ if (!file)
+ return 0;
+
+ if (np > 1) {
+ ysfx_t *fx = (ysfx_t *)state->lice->m_user_ctx;
+ ysfx_string_set(fx, *parms[1], file);
+ }
+
+ return 1;
+}
+
+#endif
+
+//------------------------------------------------------------------------------
+#if !defined(YSFX_NO_GFX)
+ysfx_gfx_state_t::ysfx_gfx_state_t(ysfx_t *fx)
+ : lice{new eel_lice_state{fx->vm.get(), fx, ysfx_gfx_max_images, ysfx_gfx_max_fonts}}
+{
+ lice->m_framebuffer = new LICE_WrapperBitmap{nullptr, 0, 0, 0, false};
+}
+
+ysfx_gfx_state_t::~ysfx_gfx_state_t()
+{
+}
+#endif
+
+ysfx_gfx_state_t *ysfx_gfx_state_new(ysfx_t *fx)
+{
+ return new ysfx_gfx_state_t{fx};
+}
+
+void ysfx_gfx_state_free(ysfx_gfx_state_t *state)
+{
+ delete state;
+}
+
+void ysfx_gfx_state_set_bitmap(ysfx_gfx_state_t *state, uint8_t *data, uint32_t w, uint32_t h, uint32_t stride)
+{
+ if (stride == 0)
+ stride = 4 * w;
+
+ eel_lice_state *lice = state->lice.get();
+
+ assert(stride % 4 == 0);
+ *static_cast(lice->m_framebuffer) = LICE_WrapperBitmap{(LICE_pixel *)data, (int)w, (int)h, (int)(stride / 4), false};
+}
+
+void ysfx_gfx_state_set_scale_factor(ysfx_gfx_state_t *state, ysfx_real scale)
+{
+ state->scale = scale;
+}
+
+void ysfx_gfx_state_set_callback_data(ysfx_gfx_state_t *state, void *callback_data)
+{
+ state->callback_data = callback_data;
+}
+
+void ysfx_gfx_state_set_show_menu_callback(ysfx_gfx_state_t *state, int (*callback)(void *, const char *, int32_t, int32_t))
+{
+ state->show_menu = callback;
+}
+
+void ysfx_gfx_state_set_set_cursor_callback(ysfx_gfx_state_t *state, void (*callback)(void *, int32_t))
+{
+ state->set_cursor = callback;
+}
+
+void ysfx_gfx_state_set_get_drop_file_callback(ysfx_gfx_state_t *state, const char *(*callback)(void *, int32_t))
+{
+ state->get_drop_file = callback;
+}
+
+bool ysfx_gfx_state_is_dirty(ysfx_gfx_state_t *state)
+{
+ return state->lice->m_framebuffer_dirty;
+}
+
+void ysfx_gfx_state_add_key(ysfx_gfx_state_t *state, uint32_t mods, uint32_t key, bool press)
+{
+ if (key < 1)
+ return;
+
+ uint32_t key_id;
+ if (translate_special_key(key, key))
+ key_id = key;
+ else if (key < 256)
+ key_id = ysfx::latin1_tolower(key);
+ else // support the Latin-1 character set only
+ return;
+
+ uint32_t key_with_mod = key;
+ if (key_id >= 'a' && key_id <= 'z') {
+ uint32_t off = (uint32_t)(key_id - 'a');
+ if (mods & (ysfx_mod_ctrl|ysfx_mod_alt))
+ key_with_mod = off + 257;
+ else if (mods & ysfx_mod_ctrl)
+ key_with_mod = off + 1;
+ else if (mods & ysfx_mod_alt)
+ key_with_mod = off + 321;
+ }
+
+ if (press && key_with_mod > 0) {
+ while (state->input_queue.size() >= ysfx_gfx_max_input)
+ state->input_queue.pop();
+ state->input_queue.push(key_with_mod);
+ }
+
+ if (press)
+ state->keys_pressed.insert(key_id);
+ else
+ state->keys_pressed.erase(key_id);
+}
+
+//------------------------------------------------------------------------------
+void ysfx_gfx_enter(ysfx_t *fx, bool doinit)
+{
+ fx->gfx.mutex.lock();
+ ysfx_gfx_state_t *state = fx->gfx.state.get();
+
+ if (doinit) {
+ if (fx->gfx.must_init.exchange(false, std::memory_order_acquire)) {
+ *fx->var.gfx_r = 1.0;
+ *fx->var.gfx_g = 1.0;
+ *fx->var.gfx_b = 1.0;
+ *fx->var.gfx_a = 1.0;
+ *fx->var.gfx_a2 = 1.0;
+ *fx->var.gfx_dest = -1.0;
+ *fx->var.mouse_wheel = 0.0;
+ *fx->var.mouse_hwheel = 0.0;
+ // NOTE the above are according to eel_lice.h `resetVarsToStock`
+ // it helps to reset a few more, especially for clearing
+ *fx->var.gfx_mode = 0;
+ *fx->var.gfx_clear = 0;
+ *fx->var.gfx_texth = 0;
+ *fx->var.mouse_cap = 0;
+
+ // reset key state
+ state->input_queue = {};
+ state->keys_pressed = {};
+
+ // reset lice
+ eel_lice_state *lice = state->lice.get();
+ LICE_WrapperBitmap framebuffer = *static_cast(lice->m_framebuffer);
+ state->lice.reset();
+ lice = new eel_lice_state{fx->vm.get(), fx, ysfx_gfx_max_images, ysfx_gfx_max_fonts};
+ state->lice.reset(lice);
+ lice->m_framebuffer = new LICE_WrapperBitmap(framebuffer);
+
+ // load images from filenames
+ uint32_t numfiles = (uint32_t)fx->source.main->header.filenames.size();
+ for (uint32_t i = 0; i < numfiles; ++i)
+ lice->gfx_loadimg(fx, (int32_t)i, (EEL_F)i);
+
+ fx->gfx.ready = true;
+ }
+ }
+
+ ysfx_set_thread_id(ysfx_thread_id_gfx);
+}
+
+void ysfx_gfx_leave(ysfx_t *fx)
+{
+ ysfx_set_thread_id(ysfx_thread_id_none);
+
+ fx->gfx.mutex.unlock();
+}
+
+ysfx_gfx_state_t *ysfx_gfx_get_context(ysfx_t *fx)
+{
+ if (!fx)
+ return nullptr;
+
+ // NOTE: make sure that this will be used from the @gfx thread only
+ if (ysfx_get_thread_id() != ysfx_thread_id_gfx)
+ return nullptr;
+
+ return fx->gfx.state.get();
+}
+
+void ysfx_gfx_prepare(ysfx_t *fx)
+{
+ ysfx_gfx_state_t *state = ysfx_gfx_get_context(fx);
+ eel_lice_state *lice = state->lice.get();
+
+ lice->m_framebuffer_dirty = false;
+
+ // set variables `gfx_w` and `gfx_h`
+ ysfx_real gfx_w = (ysfx_real)lice->m_framebuffer->getWidth();
+ ysfx_real gfx_h = (ysfx_real)lice->m_framebuffer->getHeight();
+ if (state->scale > 1.0) {
+ gfx_w *= state->scale;
+ gfx_h *= state->scale;
+ *fx->var.gfx_ext_retina = state->scale;
+ }
+ *fx->var.gfx_w = gfx_w;
+ *fx->var.gfx_h = gfx_h;
+}
+#endif
+
+//------------------------------------------------------------------------------
+void ysfx_api_init_gfx()
+{
+#if !defined(YSFX_NO_GFX)
+ lice_stb_install_loaders();
+#endif
+
+ NSEEL_addfunc_retptr("gfx_lineto", 3, NSEEL_PProc_THIS, &ysfx_api_gfx_lineto);
+ NSEEL_addfunc_retptr("gfx_lineto", 2, NSEEL_PProc_THIS, &ysfx_api_gfx_lineto2);
+ NSEEL_addfunc_retptr("gfx_rectto", 2, NSEEL_PProc_THIS, &ysfx_api_gfx_rectto);
+ NSEEL_addfunc_varparm("gfx_rect", 4, NSEEL_PProc_THIS, &ysfx_api_gfx_rect);
+ NSEEL_addfunc_varparm("gfx_line", 4, NSEEL_PProc_THIS, &ysfx_api_gfx_line);
+ NSEEL_addfunc_varparm("gfx_gradrect", 8, NSEEL_PProc_THIS, &ysfx_api_gfx_gradrect);
+ NSEEL_addfunc_varparm("gfx_muladdrect", 7, NSEEL_PProc_THIS, &ysfx_api_gfx_muladdrect);
+ NSEEL_addfunc_varparm("gfx_deltablit", 9, NSEEL_PProc_THIS, &ysfx_api_gfx_deltablit);
+ NSEEL_addfunc_exparms("gfx_transformblit", 8, NSEEL_PProc_THIS, &ysfx_api_gfx_transformblit);
+ NSEEL_addfunc_varparm("gfx_circle", 3, NSEEL_PProc_THIS, &ysfx_api_gfx_circle);
+ NSEEL_addfunc_varparm("gfx_triangle", 6, NSEEL_PProc_THIS, &ysfx_api_gfx_triangle);
+ NSEEL_addfunc_varparm("gfx_roundrect", 5, NSEEL_PProc_THIS, &ysfx_api_gfx_roundrect);
+ NSEEL_addfunc_varparm("gfx_arc", 5, NSEEL_PProc_THIS, &ysfx_api_gfx_arc);
+ NSEEL_addfunc_retptr("gfx_blurto", 2, NSEEL_PProc_THIS, &ysfx_api_gfx_blurto);
+ NSEEL_addfunc_exparms("gfx_showmenu", 1, NSEEL_PProc_THIS, &ysfx_api_gfx_showmenu);
+ NSEEL_addfunc_varparm("gfx_setcursor", 1, NSEEL_PProc_THIS, &ysfx_api_gfx_setcursor);
+ NSEEL_addfunc_retptr("gfx_drawnumber", 2, NSEEL_PProc_THIS, &ysfx_api_gfx_drawnumber);
+ NSEEL_addfunc_retptr("gfx_drawchar", 1, NSEEL_PProc_THIS, &ysfx_api_gfx_drawchar);
+ NSEEL_addfunc_varparm("gfx_drawstr", 1, NSEEL_PProc_THIS, &ysfx_api_gfx_drawstr);
+ NSEEL_addfunc_retptr("gfx_measurestr", 3, NSEEL_PProc_THIS, &ysfx_api_gfx_measurestr);
+ NSEEL_addfunc_retptr("gfx_measurechar", 3, NSEEL_PProc_THIS, &ysfx_api_gfx_measurechar);
+ NSEEL_addfunc_varparm("gfx_printf", 1, NSEEL_PProc_THIS, &ysfx_api_gfx_printf);
+ NSEEL_addfunc_retptr("gfx_setpixel", 3, NSEEL_PProc_THIS, &ysfx_api_gfx_setpixel);
+ NSEEL_addfunc_retptr("gfx_getpixel", 3, NSEEL_PProc_THIS, &ysfx_api_gfx_getpixel);
+ NSEEL_addfunc_retptr("gfx_getimgdim", 3, NSEEL_PProc_THIS, &ysfx_api_gfx_getimgdim);
+ NSEEL_addfunc_retval("gfx_setimgdim", 3, NSEEL_PProc_THIS, &ysfx_api_gfx_setimgdim);
+ NSEEL_addfunc_retval("gfx_loadimg", 2, NSEEL_PProc_THIS, &ysfx_api_gfx_loadimg);
+ NSEEL_addfunc_retptr("gfx_blit", 3, NSEEL_PProc_THIS, &ysfx_api_gfx_blit);
+ NSEEL_addfunc_retptr("gfx_blitext", 3, NSEEL_PProc_THIS, &ysfx_api_gfx_blitext);
+ NSEEL_addfunc_varparm("gfx_blit", 4, NSEEL_PProc_THIS, &ysfx_api_gfx_blit2);
+ NSEEL_addfunc_varparm("gfx_setfont", 1, NSEEL_PProc_THIS, &ysfx_api_gfx_setfont);
+ NSEEL_addfunc_varparm("gfx_getfont", 1, NSEEL_PProc_THIS, &ysfx_api_gfx_getfont);
+ NSEEL_addfunc_varparm("gfx_set", 1, NSEEL_PProc_THIS, &ysfx_api_gfx_set);
+ NSEEL_addfunc_varparm("gfx_getdropfile", 1, NSEEL_PProc_THIS, &ysfx_api_gfx_getdropfile);
+ NSEEL_addfunc_varparm("gfx_getsyscol", 0, NSEEL_PProc_THIS, &ysfx_api_gfx_getsyscol);
+ NSEEL_addfunc_retval("gfx_getchar", 1, NSEEL_PProc_THIS, &ysfx_api_gfx_getchar);
+}
+
+//------------------------------------------------------------------------------
+// SWELL helpers
+
+#if !defined(YSFX_NO_GFX)
+
+// implement these GL functions on SWELL targets where they are missing
+
+#if !defined(SWELL_TARGET_GDK) && !defined(SWELL_TARGET_OSX)
+void SWELL_SetViewGL(HWND h, char wantGL)
+{
+}
+
+bool SWELL_GetViewGL(HWND h)
+{
+ return false;
+}
+
+bool SWELL_SetGLContextToView(HWND h)
+{
+ return false;
+}
+
+#endif // !defined(SWELL_TARGET_GDK) && !defined(SWELL_TARGET_OSX)
+
+#endif
diff --git a/source/modules/ysfx/sources/ysfx_api_gfx.hpp b/source/modules/ysfx/sources/ysfx_api_gfx.hpp
new file mode 100644
index 000000000..8bbc0c70a
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx_api_gfx.hpp
@@ -0,0 +1,50 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#pragma once
+#include "ysfx.h"
+
+#if !defined(YSFX_NO_GFX)
+struct ysfx_gfx_state_t;
+ysfx_gfx_state_t *ysfx_gfx_state_new(ysfx_t *fx);
+void ysfx_gfx_state_free(ysfx_gfx_state_t *state);
+YSFX_DEFINE_AUTO_PTR(ysfx_gfx_state_u, ysfx_gfx_state_t, ysfx_gfx_state_free);
+void ysfx_gfx_state_set_bitmap(ysfx_gfx_state_t *state, uint8_t *data, uint32_t w, uint32_t h, uint32_t stride);
+void ysfx_gfx_state_set_scale_factor(ysfx_gfx_state_t *state, ysfx_real scale);
+void ysfx_gfx_state_set_callback_data(ysfx_gfx_state_t *state, void *callback_data);
+void ysfx_gfx_state_set_show_menu_callback(ysfx_gfx_state_t *state, int (*callback)(void *, const char *, int32_t, int32_t));
+void ysfx_gfx_state_set_set_cursor_callback(ysfx_gfx_state_t *state, void (*callback)(void *, int32_t));
+void ysfx_gfx_state_set_get_drop_file_callback(ysfx_gfx_state_t *state, const char *(*callback)(void *, int32_t));
+bool ysfx_gfx_state_is_dirty(ysfx_gfx_state_t *state);
+void ysfx_gfx_state_add_key(ysfx_gfx_state_t *state, uint32_t mods, uint32_t key, bool press);
+void ysfx_gfx_state_update_mouse(ysfx_gfx_state_t *state, uint32_t mods, int xpos, int ypos, uint32_t buttons, int wheel, int hwheel);
+
+//------------------------------------------------------------------------------
+void ysfx_gfx_enter(ysfx_t *fx, bool doinit);
+void ysfx_gfx_leave(ysfx_t *fx);
+ysfx_gfx_state_t *ysfx_gfx_get_context(ysfx_t *fx);
+void ysfx_gfx_prepare(ysfx_t *fx);
+
+struct ysfx_scoped_gfx_t {
+ ysfx_scoped_gfx_t(ysfx_t *fx, bool doinit) : m_fx(fx) { ysfx_gfx_enter(fx, doinit); }
+ ~ysfx_scoped_gfx_t() { ysfx_gfx_leave(m_fx); }
+ ysfx_t *m_fx = nullptr;
+};
+#endif
+
+//------------------------------------------------------------------------------
+void ysfx_api_init_gfx();
diff --git a/source/modules/ysfx/sources/ysfx_api_gfx_dummy.hpp b/source/modules/ysfx/sources/ysfx_api_gfx_dummy.hpp
new file mode 100644
index 000000000..10a10070f
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx_api_gfx_dummy.hpp
@@ -0,0 +1,91 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#pragma once
+#include "ysfx_eel_utils.hpp"
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_gfx_stub_varparm(void *, INT_PTR, EEL_F **)
+{
+ return 0;
+}
+
+static EEL_F *NSEEL_CGEN_CALL ysfx_api_gfx_stub_retptr1(void *, EEL_F *arg)
+{
+ return arg;
+}
+
+static EEL_F *NSEEL_CGEN_CALL ysfx_api_gfx_stub_retptr2(void *, EEL_F *arg, EEL_F *)
+{
+ return arg;
+}
+
+static EEL_F *NSEEL_CGEN_CALL ysfx_api_gfx_stub_retptr3(void *, EEL_F *arg, EEL_F *, EEL_F *)
+{
+ return arg;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_gfx_stub_retval1(void *, EEL_F *)
+{
+ return 0;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_gfx_stub_retval2(void *, EEL_F *, EEL_F *)
+{
+ return 0;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_gfx_stub_retval3(void *, EEL_F *, EEL_F *, EEL_F *)
+{
+ return 0;
+}
+
+#define ysfx_api_gfx_lineto ysfx_api_gfx_stub_retptr3
+#define ysfx_api_gfx_lineto2 ysfx_api_gfx_stub_retptr2
+#define ysfx_api_gfx_rectto ysfx_api_gfx_stub_retptr2
+#define ysfx_api_gfx_rect ysfx_api_gfx_stub_varparm
+#define ysfx_api_gfx_line ysfx_api_gfx_stub_varparm
+#define ysfx_api_gfx_gradrect ysfx_api_gfx_stub_varparm
+#define ysfx_api_gfx_muladdrect ysfx_api_gfx_stub_varparm
+#define ysfx_api_gfx_deltablit ysfx_api_gfx_stub_varparm
+#define ysfx_api_gfx_transformblit ysfx_api_gfx_stub_varparm
+#define ysfx_api_gfx_circle ysfx_api_gfx_stub_varparm
+#define ysfx_api_gfx_triangle ysfx_api_gfx_stub_varparm
+#define ysfx_api_gfx_roundrect ysfx_api_gfx_stub_varparm
+#define ysfx_api_gfx_arc ysfx_api_gfx_stub_varparm
+#define ysfx_api_gfx_blurto ysfx_api_gfx_stub_retptr2
+#define ysfx_api_gfx_showmenu ysfx_api_gfx_stub_varparm
+#define ysfx_api_gfx_setcursor ysfx_api_gfx_stub_varparm
+#define ysfx_api_gfx_drawnumber ysfx_api_gfx_stub_retptr2
+#define ysfx_api_gfx_drawchar ysfx_api_gfx_stub_retptr1
+#define ysfx_api_gfx_drawstr ysfx_api_gfx_stub_varparm
+#define ysfx_api_gfx_measurestr ysfx_api_gfx_stub_retptr3
+#define ysfx_api_gfx_measurechar ysfx_api_gfx_stub_retptr3
+#define ysfx_api_gfx_printf ysfx_api_gfx_stub_varparm
+#define ysfx_api_gfx_setpixel ysfx_api_gfx_stub_retptr3
+#define ysfx_api_gfx_getpixel ysfx_api_gfx_stub_retptr3
+#define ysfx_api_gfx_getimgdim ysfx_api_gfx_stub_retptr3
+#define ysfx_api_gfx_setimgdim ysfx_api_gfx_stub_retval3
+#define ysfx_api_gfx_loadimg ysfx_api_gfx_stub_retval2
+#define ysfx_api_gfx_blit ysfx_api_gfx_stub_retptr3
+#define ysfx_api_gfx_blitext ysfx_api_gfx_stub_retptr3
+#define ysfx_api_gfx_blit2 ysfx_api_gfx_stub_varparm
+#define ysfx_api_gfx_setfont ysfx_api_gfx_stub_varparm
+#define ysfx_api_gfx_getfont ysfx_api_gfx_stub_varparm
+#define ysfx_api_gfx_set ysfx_api_gfx_stub_varparm
+#define ysfx_api_gfx_getdropfile ysfx_api_gfx_stub_varparm
+#define ysfx_api_gfx_getsyscol ysfx_api_gfx_stub_varparm
+#define ysfx_api_gfx_getchar ysfx_api_gfx_stub_retval1
diff --git a/source/modules/ysfx/sources/ysfx_api_gfx_lice.hpp b/source/modules/ysfx/sources/ysfx_api_gfx_lice.hpp
new file mode 100644
index 000000000..e4151ca53
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx_api_gfx_lice.hpp
@@ -0,0 +1,1490 @@
+//
+// This file is based in part on modified source code from `WDL/eel2/eel_lice.h`.
+// The zlib license from the WDL applies to this source file.
+//
+//------------------------------------------------------------------------------
+//
+// Copyright (C) 2021 and later Jean Pierre Cimalando
+// Copyright (C) 2005 and later Cockos Incorporated
+//
+//
+// Portions copyright other contributors, see each source file for more information
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+// 3. This notice may not be removed or altered from any source distribution.
+//
+// SPDX-License-Identifier: Zlib
+//
+
+#pragma once
+#include "ysfx_api_eel.hpp"
+#include "WDL/wdlstring.h"
+#include "WDL/wdlcstring.h"
+#include "WDL/wdlutf8.h"
+#include
+#include
+#include
+#include
+
+// help clangd to figure things out
+#if defined(__CLANGD__)
+# include "ysfx_api_gfx.cpp"
+#endif
+
+#define EEL_LICE_GET_CONTEXT(opaque) ((opaque) ? ((ysfx_t *)(opaque))->gfx.state->lice.get() : nullptr)
+
+//------------------------------------------------------------------------------
+
+#define LICE_FUNCTION_VALID(x) (sizeof(int) > 0)
+
+static HDC LICE__GetDC(LICE_IBitmap *bm)
+{
+ return bm->getDC();
+}
+static int LICE__GetWidth(LICE_IBitmap *bm)
+{
+ return bm->getWidth();
+}
+static int LICE__GetHeight(LICE_IBitmap *bm)
+{
+ return bm->getHeight();
+}
+static void LICE__Destroy(LICE_IBitmap *bm)
+{
+ delete bm;
+}
+static void LICE__SetFromHFont(LICE_IFont * ifont, HFONT font, int flags)
+{
+ if (ifont) ifont->SetFromHFont(font,flags);
+}
+static LICE_pixel LICE__SetTextColor(LICE_IFont* ifont, LICE_pixel color)
+{
+ if (ifont) return ifont->SetTextColor(color);
+ return 0;
+}
+static void LICE__SetTextCombineMode(LICE_IFont* ifont, int mode, float alpha)
+{
+ if (ifont) ifont->SetCombineMode(mode, alpha);
+}
+static int LICE__DrawText(LICE_IFont* ifont, LICE_IBitmap *bm, const char *str, int strcnt, RECT *rect, UINT dtFlags)
+{
+ if (ifont) return ifont->DrawText(bm, str, strcnt, rect, dtFlags);
+ return 0;
+}
+
+
+static LICE_IFont *LICE_CreateFont()
+{
+ return new LICE_CachedFont();
+}
+static void LICE__DestroyFont(LICE_IFont *bm)
+{
+ delete bm;
+}
+static bool LICE__resize(LICE_IBitmap *bm, int w, int h)
+{
+ return bm->resize(w,h);
+}
+
+static LICE_IBitmap *__LICE_CreateBitmap(int mode, int w, int h)
+{
+ if (mode==1) return new LICE_SysBitmap(w,h);
+ return new LICE_MemBitmap(w,h);
+}
+
+class eel_lice_state
+{
+public:
+
+ eel_lice_state(NSEEL_VMCTX vm, void *ctx, int image_slots, int font_slots);
+ ~eel_lice_state();
+
+ LICE_IBitmap *m_framebuffer, *m_framebuffer_extra;
+ int m_framebuffer_dirty;
+ WDL_TypedBuf m_gfx_images;
+ struct gfxFontStruct {
+ LICE_IFont *font;
+ char last_fontname[128];
+ char actual_fontname[128];
+ int last_fontsize;
+ int last_fontflag;
+
+ int use_fonth;
+ };
+ WDL_TypedBuf m_gfx_fonts;
+ enum {
+ EELFONT_FLAG_BOLD = (1<<24),
+ EELFONT_FLAG_ITALIC = (2<<24),
+ EELFONT_FLAG_UNDERLINE = (4<<24),
+ EELFONT_FLAG_MASK = EELFONT_FLAG_BOLD|EELFONT_FLAG_ITALIC|EELFONT_FLAG_UNDERLINE
+ };
+
+ int m_gfx_font_active; // -1 for default, otherwise index into gfx_fonts (NOTE: this differs from the exposed API, which defines 0 as default, 1-n)
+ LICE_IFont *GetActiveFont() { return m_gfx_font_active>=0&&m_gfx_font_active-2.0)
+ {
+ if (idx < 0.0) return m_framebuffer;
+
+ const int a = (int)idx;
+ if (a >= 0 && a < m_gfx_images.GetSize()) return m_gfx_images.Get()[a];
+ }
+ return NULL;
+ };
+
+ void SetImageDirty(LICE_IBitmap *bm)
+ {
+ if (bm == m_framebuffer && !m_framebuffer_dirty)
+ {
+ if (m_gfx_clear && *m_gfx_clear > -1.0)
+ {
+ const int a=(int)*m_gfx_clear;
+ if (LICE_FUNCTION_VALID(LICE_Clear)) LICE_Clear(m_framebuffer,LICE_RGBA((a&0xff),((a>>8)&0xff),((a>>16)&0xff),0));
+ }
+ m_framebuffer_dirty=1;
+ }
+ }
+
+ // R, G, B, A, w, h, x, y, mode(1=add,0=copy)
+ EEL_F *m_gfx_r, *m_gfx_g, *m_gfx_b, *m_gfx_w, *m_gfx_h, *m_gfx_a, *m_gfx_x, *m_gfx_y, *m_gfx_mode, *m_gfx_clear, *m_gfx_texth,*m_gfx_dest, *m_gfx_a2;
+ EEL_F *m_mouse_x, *m_mouse_y, *m_mouse_cap, *m_mouse_wheel, *m_mouse_hwheel;
+ EEL_F *m_gfx_ext_retina;
+
+ NSEEL_VMCTX m_vmref;
+ void *m_user_ctx;
+
+ void gfx_lineto(EEL_F xpos, EEL_F ypos, EEL_F aaflag);
+ void gfx_rectto(EEL_F xpos, EEL_F ypos);
+ void gfx_line(int np, EEL_F **parms);
+ void gfx_rect(int np, EEL_F **parms);
+ void gfx_roundrect(int np, EEL_F **parms);
+ void gfx_arc(int np, EEL_F **parms);
+ void gfx_set(int np, EEL_F **parms);
+ void gfx_grad_or_muladd_rect(int mode, int np, EEL_F **parms);
+ void gfx_setpixel(EEL_F r, EEL_F g, EEL_F b);
+ void gfx_getpixel(EEL_F *r, EEL_F *g, EEL_F *b);
+ void gfx_drawnumber(EEL_F n, EEL_F ndigits);
+ void gfx_drawchar(EEL_F ch);
+ void gfx_getimgdim(EEL_F img, EEL_F *w, EEL_F *h);
+ EEL_F gfx_setimgdim(int img, EEL_F *w, EEL_F *h);
+ void gfx_blurto(EEL_F x, EEL_F y);
+ void gfx_blit(EEL_F img, EEL_F scale, EEL_F rotate);
+ void gfx_blitext(EEL_F img, EEL_F *coords, EEL_F angle);
+ void gfx_blitext2(int np, EEL_F **parms, int mode); // 0=blit, 1=deltablit
+ void gfx_transformblit(EEL_F **parms, int div_w, int div_h, EEL_F *tab); // parms[0]=src, 1-4=x,y,w,h
+ void gfx_circle(float x, float y, float r, bool fill, bool aaflag);
+ void gfx_triangle(EEL_F** parms, int nparms);
+ void gfx_drawstr(void *opaque, EEL_F **parms, int nparms, int formatmode); // formatmode=1 for format, 2 for purely measure no format, 3 for measure char
+ EEL_F gfx_loadimg(void *opaque, int img, EEL_F loadFrom);
+ EEL_F gfx_setfont(void *opaque, int np, EEL_F **parms);
+ EEL_F gfx_getfont(void *opaque, int np, EEL_F **parms);
+
+ LICE_pixel getCurColor();
+ int getCurMode();
+ int getCurModeForBlit(bool isFBsrc);
+
+ // these have to be **parms because of the hack for getting string from parm index
+ EEL_F gfx_setcursor(void* opaque, EEL_F** parms, int nparms);
+};
+
+eel_lice_state::eel_lice_state(NSEEL_VMCTX vm, void *ctx, int image_slots, int font_slots)
+{
+ m_user_ctx=ctx;
+ m_vmref= vm;
+ m_gfx_font_active=-1;
+ m_gfx_fonts.Resize(font_slots);
+ memset(m_gfx_fonts.Get(),0,m_gfx_fonts.GetSize()*sizeof(m_gfx_fonts.Get()[0]));
+
+ m_gfx_images.Resize(image_slots);
+ memset(m_gfx_images.Get(),0,m_gfx_images.GetSize()*sizeof(m_gfx_images.Get()[0]));
+ m_framebuffer=m_framebuffer_extra=0;
+ m_framebuffer_dirty=0;
+
+ m_gfx_r = NSEEL_VM_regvar(vm,"gfx_r");
+ m_gfx_g = NSEEL_VM_regvar(vm,"gfx_g");
+ m_gfx_b = NSEEL_VM_regvar(vm,"gfx_b");
+ m_gfx_a = NSEEL_VM_regvar(vm,"gfx_a");
+ m_gfx_a2 = NSEEL_VM_regvar(vm,"gfx_a2");
+
+ m_gfx_w = NSEEL_VM_regvar(vm,"gfx_w");
+ m_gfx_h = NSEEL_VM_regvar(vm,"gfx_h");
+ m_gfx_x = NSEEL_VM_regvar(vm,"gfx_x");
+ m_gfx_y = NSEEL_VM_regvar(vm,"gfx_y");
+ m_gfx_mode = NSEEL_VM_regvar(vm,"gfx_mode");
+ m_gfx_clear = NSEEL_VM_regvar(vm,"gfx_clear");
+ m_gfx_texth = NSEEL_VM_regvar(vm,"gfx_texth");
+ m_gfx_dest = NSEEL_VM_regvar(vm,"gfx_dest");
+ m_gfx_ext_retina = NSEEL_VM_regvar(vm,"gfx_ext_retina");
+
+ m_mouse_x = NSEEL_VM_regvar(vm,"mouse_x");
+ m_mouse_y = NSEEL_VM_regvar(vm,"mouse_y");
+ m_mouse_cap = NSEEL_VM_regvar(vm,"mouse_cap");
+ m_mouse_wheel=NSEEL_VM_regvar(vm,"mouse_wheel");
+ m_mouse_hwheel=NSEEL_VM_regvar(vm,"mouse_hwheel");
+
+ if (m_gfx_texth) *m_gfx_texth=8;
+}
+eel_lice_state::~eel_lice_state()
+{
+ if (LICE_FUNCTION_VALID(LICE__Destroy))
+ {
+ LICE__Destroy(m_framebuffer_extra);
+ LICE__Destroy(m_framebuffer);
+ int x;
+ for (x=0;x>4)&0xf;
+ if (sm > LICE_BLIT_MODE_COPY && sm <= LICE_BLIT_MODE_HSVADJ) return sm;
+
+ return (gmode&1) ? LICE_BLIT_MODE_ADD : LICE_BLIT_MODE_COPY;
+}
+int eel_lice_state::getCurModeForBlit(bool isFBsrc)
+{
+ const int gmode = (int) (*m_gfx_mode);
+
+ const int sm=(gmode>>4)&0xf;
+
+ int mode;
+ if (sm > LICE_BLIT_MODE_COPY && sm <= LICE_BLIT_MODE_HSVADJ) mode=sm;
+ else mode=((gmode&1) ? LICE_BLIT_MODE_ADD : LICE_BLIT_MODE_COPY);
+
+
+ if (!isFBsrc && !(gmode&2)) mode|=LICE_BLIT_USE_ALPHA;
+ if (!(gmode&4)) mode|=LICE_BLIT_FILTER_BILINEAR;
+
+ return mode;
+}
+LICE_pixel eel_lice_state::getCurColor()
+{
+ int red=(int) (*m_gfx_r*255.0);
+ int green=(int) (*m_gfx_g*255.0);
+ int blue=(int) (*m_gfx_b*255.0);
+ int a2=(int) (*m_gfx_a2*255.0);
+ if (red<0) red=0;else if (red>255)red=255;
+ if (green<0) green=0;else if (green>255)green=255;
+ if (blue<0) blue=0; else if (blue>255) blue=255;
+ if (a2<0) a2=0; else if (a2>255) a2=255;
+ return LICE_RGBA(red,green,blue,a2);
+}
+
+
+static EEL_F * NSEEL_CGEN_CALL ysfx_api_gfx_lineto(void *opaque, EEL_F *xpos, EEL_F *ypos, EEL_F *useaa)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx) ctx->gfx_lineto(*xpos, *ypos, *useaa);
+ return xpos;
+}
+static EEL_F * NSEEL_CGEN_CALL ysfx_api_gfx_lineto2(void *opaque, EEL_F *xpos, EEL_F *ypos)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx) ctx->gfx_lineto(*xpos, *ypos, 1.0f);
+ return xpos;
+}
+
+static EEL_F * NSEEL_CGEN_CALL ysfx_api_gfx_rectto(void *opaque, EEL_F *xpos, EEL_F *ypos)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx) ctx->gfx_rectto(*xpos, *ypos);
+ return xpos;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_gfx_line(void *opaque, INT_PTR np, EEL_F **parms)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx) ctx->gfx_line((int)np,parms);
+ return 0.0;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_gfx_rect(void *opaque, INT_PTR np, EEL_F **parms)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx) ctx->gfx_rect((int)np,parms);
+ return 0.0;
+}
+static EEL_F NSEEL_CGEN_CALL ysfx_api_gfx_roundrect(void *opaque, INT_PTR np, EEL_F **parms)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx) ctx->gfx_roundrect((int)np,parms);
+ return 0.0;
+}
+static EEL_F NSEEL_CGEN_CALL ysfx_api_gfx_arc(void *opaque, INT_PTR np, EEL_F **parms)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx) ctx->gfx_arc((int)np,parms);
+ return 0.0;
+}
+static EEL_F NSEEL_CGEN_CALL ysfx_api_gfx_set(void *opaque, INT_PTR np, EEL_F **parms)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx) ctx->gfx_set((int)np,parms);
+ return 0.0;
+}
+static EEL_F NSEEL_CGEN_CALL ysfx_api_gfx_gradrect(void *opaque, INT_PTR np, EEL_F **parms)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx) ctx->gfx_grad_or_muladd_rect(0,(int)np,parms);
+ return 0.0;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_gfx_muladdrect(void *opaque, INT_PTR np, EEL_F **parms)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx) ctx->gfx_grad_or_muladd_rect(1,(int)np,parms);
+ return 0.0;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_gfx_deltablit(void *opaque, INT_PTR np, EEL_F **parms)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx) ctx->gfx_blitext2((int)np,parms,1);
+ return 0.0;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_gfx_transformblit(void *opaque, INT_PTR np, EEL_F **parms)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx)
+ {
+#ifndef EEL_LICE_NO_RAM
+ const int divw = (int) (parms[5][0]+0.5);
+ const int divh = (int) (parms[6][0]+0.5);
+ if (divw < 1 || divh < 1) return 0.0;
+ const int sz = divw*divh*2;
+
+#ifdef EEL_LICE_RAMFUNC
+ EEL_F *d = EEL_LICE_RAMFUNC(opaque,7,sz);
+ if (!d) return 0.0;
+#else
+ EEL_F **blocks = ctx->m_vmref ? ((compileContext*)ctx->m_vmref)->ram_state->blocks : 0;
+ if (!blocks || np < 8) return 0.0;
+
+ const int addr1= (int) (parms[7][0]+0.5);
+ EEL_F *d=__NSEEL_RAMAlloc(blocks,addr1);
+ if (sz>NSEEL_RAM_ITEMSPERBLOCK)
+ {
+ int x;
+ for(x=NSEEL_RAM_ITEMSPERBLOCK;xgfx_transformblit(parms,divw,divh,d);
+#endif
+ }
+ return 0.0;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_gfx_circle(void *opaque, INT_PTR np, EEL_F **parms)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ bool aa = true, fill = false;
+ if (np>3) fill = parms[3][0] > 0.5;
+ if (np>4) aa = parms[4][0] > 0.5;
+ if (ctx) ctx->gfx_circle((float)parms[0][0], (float)parms[1][0], (float)parms[2][0], fill, aa);
+ return 0.0;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_gfx_triangle(void* opaque, INT_PTR np, EEL_F **parms)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx) ctx->gfx_triangle(parms, (int)np);
+ return 0.0;
+}
+
+static EEL_F * NSEEL_CGEN_CALL ysfx_api_gfx_drawnumber(void *opaque, EEL_F *n, EEL_F *nd)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx) ctx->gfx_drawnumber(*n, *nd);
+ return n;
+}
+
+static EEL_F * NSEEL_CGEN_CALL ysfx_api_gfx_drawchar(void *opaque, EEL_F *n)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx) ctx->gfx_drawchar(*n);
+ return n;
+}
+
+static EEL_F * NSEEL_CGEN_CALL ysfx_api_gfx_measurestr(void *opaque, EEL_F *str, EEL_F *xOut, EEL_F *yOut)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx)
+ {
+ EEL_F *p[3]={str,xOut,yOut};
+ ctx->gfx_drawstr(opaque,p,3,2);
+ }
+ return str;
+}
+static EEL_F * NSEEL_CGEN_CALL ysfx_api_gfx_measurechar(void *opaque, EEL_F *str, EEL_F *xOut, EEL_F *yOut)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx)
+ {
+ EEL_F *p[3]={str,xOut,yOut};
+ ctx->gfx_drawstr(opaque,p,3,3);
+ }
+ return str;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_gfx_drawstr(void *opaque, INT_PTR nparms, EEL_F **parms)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx) ctx->gfx_drawstr(opaque,parms,(int)nparms,0);
+ return parms[0][0];
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_gfx_printf(void *opaque, INT_PTR nparms, EEL_F **parms)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx && nparms>0)
+ {
+ EEL_F v= **parms;
+ ctx->gfx_drawstr(opaque,parms,(int)nparms,1);
+ return v;
+ }
+ return 0.0;
+}
+
+static EEL_F * NSEEL_CGEN_CALL ysfx_api_gfx_setpixel(void *opaque, EEL_F *r, EEL_F *g, EEL_F *b)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx) ctx->gfx_setpixel(*r, *g, *b);
+ return r;
+}
+
+static EEL_F * NSEEL_CGEN_CALL ysfx_api_gfx_getpixel(void *opaque, EEL_F *r, EEL_F *g, EEL_F *b)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx) ctx->gfx_getpixel(r, g, b);
+ return r;
+}
+
+static EEL_F * NSEEL_CGEN_CALL ysfx_api_gfx_blit(void *opaque, EEL_F *img, EEL_F *scale, EEL_F *rotate)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx) ctx->gfx_blit(*img,*scale,*rotate);
+ return img;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_gfx_setfont(void *opaque, INT_PTR np, EEL_F **parms)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx) return ctx->gfx_setfont(opaque,(int)np,parms);
+ return 0.0;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_gfx_getfont(void *opaque, INT_PTR np, EEL_F **parms)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx)
+ {
+ const int idx=ctx->m_gfx_font_active;
+ if (idx>=0 && idx < ctx->m_gfx_fonts.GetSize())
+ {
+ eel_lice_state::gfxFontStruct* f=ctx->m_gfx_fonts.Get()+idx;
+
+ EEL_STRING_MUTEXLOCK_SCOPE
+
+#ifdef NOT_EEL_STRING_UPDATE_STRING
+ NOT_EEL_STRING_UPDATE_STRING(parms[0][0],f->actual_fontname);
+#else
+ WDL_FastString *fs=NULL;
+ EEL_STRING_GET_FOR_WRITE(parms[0][0],&fs);
+ if (fs) fs->Set(f->actual_fontname);
+#endif
+ }
+ return idx;
+ }
+ return 0.0;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_gfx_blit2(void *opaque, INT_PTR np, EEL_F **parms)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx && np>=3)
+ {
+ ctx->gfx_blitext2((int)np,parms,0);
+ return *(parms[0]);
+ }
+ return 0.0;
+}
+
+static EEL_F * NSEEL_CGEN_CALL ysfx_api_gfx_blitext(void *opaque, EEL_F *img, EEL_F *coordidx, EEL_F *rotate)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx)
+ {
+#ifndef EEL_LICE_NO_RAM
+#ifdef EEL_LICE_RAMFUNC
+ EEL_F *buf = EEL_LICE_RAMFUNC(opaque,1,10);
+ if (!buf) return img;
+#else
+ EEL_F fc = *coordidx;
+ if (fc < -0.5 || fc >= NSEEL_RAM_BLOCKS*NSEEL_RAM_ITEMSPERBLOCK) return img;
+ int a=(int)fc;
+ if (a<0) return img;
+
+ EEL_F buf[10];
+ int x;
+ EEL_F **blocks = ctx->m_vmref ? ((compileContext*)ctx->m_vmref)->ram_state->blocks : 0;
+ if (!blocks) return img;
+ for (x = 0;x < 10; x ++)
+ {
+ EEL_F *d=__NSEEL_RAMAlloc(blocks,a++);
+ if (!d || d==&nseel_ramalloc_onfail) return img;
+ buf[x]=*d;
+ }
+#endif
+ // read megabuf
+ ctx->gfx_blitext(*img,buf,*rotate);
+#endif
+ }
+ return img;
+}
+
+
+static EEL_F * NSEEL_CGEN_CALL ysfx_api_gfx_blurto(void *opaque, EEL_F *x, EEL_F *y)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx) ctx->gfx_blurto(*x,*y);
+ return x;
+}
+
+static EEL_F * NSEEL_CGEN_CALL ysfx_api_gfx_getimgdim(void *opaque, EEL_F *img, EEL_F *w, EEL_F *h)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx) ctx->gfx_getimgdim(*img,w,h);
+ return img;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_gfx_loadimg(void *opaque, EEL_F *img, EEL_F *fr)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx) return ctx->gfx_loadimg(opaque,(int)*img,*fr);
+ return 0.0;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_gfx_setimgdim(void *opaque, EEL_F *img, EEL_F *w, EEL_F *h)
+{
+ eel_lice_state *ctx=EEL_LICE_GET_CONTEXT(opaque);
+ if (ctx) return ctx->gfx_setimgdim((int)*img,w,h);
+ return 0.0;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_gfx_getsyscol(void* ctxe, INT_PTR np, EEL_F **parms)
+{
+ return (EEL_F)LICE_RGBA_FROMNATIVE(GetSysColor(COLOR_3DFACE));
+}
+
+void eel_lice_state::gfx_lineto(EEL_F xpos, EEL_F ypos, EEL_F aaflag)
+{
+ LICE_IBitmap *dest = GetImageForIndex(*m_gfx_dest,"gfx_lineto");
+ if (!dest) return;
+
+ int x1=(int)floor(xpos),y1=(int)floor(ypos),x2=(int)floor(*m_gfx_x), y2=(int)floor(*m_gfx_y);
+ if (LICE_FUNCTION_VALID(LICE__GetWidth) && LICE_FUNCTION_VALID(LICE__GetHeight) && LICE_FUNCTION_VALID(LICE_Line) &&
+ LICE_FUNCTION_VALID(LICE_ClipLine) &&
+ LICE_ClipLine(&x1,&y1,&x2,&y2,0,0,LICE__GetWidth(dest),LICE__GetHeight(dest)))
+ {
+ SetImageDirty(dest);
+ LICE_Line(dest,x1,y1,x2,y2,getCurColor(),(float) *m_gfx_a,getCurMode(),aaflag > 0.5);
+ }
+ *m_gfx_x = xpos;
+ *m_gfx_y = ypos;
+
+}
+
+void eel_lice_state::gfx_circle(float x, float y, float r, bool fill, bool aaflag)
+{
+ LICE_IBitmap *dest = GetImageForIndex(*m_gfx_dest,"gfx_circle");
+ if (!dest) return;
+
+ if (LICE_FUNCTION_VALID(LICE_Circle) && LICE_FUNCTION_VALID(LICE_FillCircle))
+ {
+ SetImageDirty(dest);
+ if(fill)
+ LICE_FillCircle(dest, x, y, r, getCurColor(), (float) *m_gfx_a, getCurMode(), aaflag);
+ else
+ LICE_Circle(dest, x, y, r, getCurColor(), (float) *m_gfx_a, getCurMode(), aaflag);
+ }
+}
+
+void eel_lice_state::gfx_triangle(EEL_F** parms, int np)
+{
+ LICE_IBitmap *dest = GetImageForIndex(*m_gfx_dest, "gfx_triangle");
+ if (np >= 6)
+ {
+ np &= ~1;
+ SetImageDirty(dest);
+ if (np == 6)
+ {
+ if (!LICE_FUNCTION_VALID(LICE_FillTriangle)) return;
+
+ LICE_FillTriangle(dest, (int)parms[0][0], (int)parms[1][0], (int)parms[2][0], (int)parms[3][0],
+ (int)parms[4][0], (int)parms[5][0], getCurColor(), (float)*m_gfx_a, getCurMode());
+ }
+ else
+ {
+ if (!LICE_FUNCTION_VALID(LICE_FillConvexPolygon)) return;
+
+ const int maxpt = 512;
+ const int n = wdl_min(np/2, maxpt);
+ int i, rdi=0;
+ int x[maxpt], y[maxpt];
+ for (i=0; i < n; i++)
+ {
+ x[i]=(int)parms[rdi++][0];
+ y[i]=(int)parms[rdi++][0];
+ }
+
+ LICE_FillConvexPolygon(dest, x, y, n, getCurColor(), (float)*m_gfx_a, getCurMode());
+ }
+ }
+}
+
+void eel_lice_state::gfx_rectto(EEL_F xpos, EEL_F ypos)
+{
+ LICE_IBitmap *dest = GetImageForIndex(*m_gfx_dest,"gfx_rectto");
+ if (!dest) return;
+
+ EEL_F x1=xpos,y1=ypos,x2=*m_gfx_x, y2=*m_gfx_y;
+ if (x2 0.5 && y2-y1 > 0.5)
+ {
+ SetImageDirty(dest);
+ LICE_FillRect(dest,(int)x1,(int)y1,(int)(x2-x1),(int)(y2-y1),getCurColor(),(float)*m_gfx_a,getCurMode());
+ }
+ *m_gfx_x = xpos;
+ *m_gfx_y = ypos;
+}
+
+
+void eel_lice_state::gfx_line(int np, EEL_F **parms)
+{
+ LICE_IBitmap *dest = GetImageForIndex(*m_gfx_dest,"gfx_line");
+ if (!dest) return;
+
+ int x1=(int)floor(parms[0][0]),y1=(int)floor(parms[1][0]),x2=(int)floor(parms[2][0]), y2=(int)floor(parms[3][0]);
+ if (LICE_FUNCTION_VALID(LICE__GetWidth) &&
+ LICE_FUNCTION_VALID(LICE__GetHeight) &&
+ LICE_FUNCTION_VALID(LICE_Line) &&
+ LICE_FUNCTION_VALID(LICE_ClipLine) && LICE_ClipLine(&x1,&y1,&x2,&y2,0,0,LICE__GetWidth(dest),LICE__GetHeight(dest)))
+ {
+ SetImageDirty(dest);
+ LICE_Line(dest,x1,y1,x2,y2,getCurColor(),(float)*m_gfx_a,getCurMode(),np< 5 || parms[4][0] > 0.5);
+ }
+}
+
+void eel_lice_state::gfx_rect(int np, EEL_F **parms)
+{
+ LICE_IBitmap *dest = GetImageForIndex(*m_gfx_dest,"gfx_rect");
+ if (!dest) return;
+
+ int x1=(int)floor(parms[0][0]),y1=(int)floor(parms[1][0]),w=(int)floor(parms[2][0]),h=(int)floor(parms[3][0]);
+ int filled=(np < 5 || parms[4][0] > 0.5);
+
+ if (LICE_FUNCTION_VALID(LICE_FillRect) && LICE_FUNCTION_VALID(LICE_DrawRect) && w>0 && h>0)
+ {
+ SetImageDirty(dest);
+ if (filled) LICE_FillRect(dest,x1,y1,w,h,getCurColor(),(float)*m_gfx_a,getCurMode());
+ else LICE_DrawRect(dest, x1, y1, w-1, h-1, getCurColor(), (float)*m_gfx_a, getCurMode());
+ }
+}
+
+void eel_lice_state::gfx_roundrect(int np, EEL_F **parms)
+{
+ LICE_IBitmap *dest = GetImageForIndex(*m_gfx_dest,"gfx_roundrect");
+ if (!dest) return;
+
+ const bool aa = np <= 5 || parms[5][0]>0.5;
+
+ if (LICE_FUNCTION_VALID(LICE_RoundRect) && parms[2][0]>0 && parms[3][0]>0)
+ {
+ SetImageDirty(dest);
+ LICE_RoundRect(dest, (float)parms[0][0], (float)parms[1][0], (float)parms[2][0], (float)parms[3][0], (int)parms[4][0], getCurColor(), (float)*m_gfx_a, getCurMode(), aa);
+ }
+}
+
+void eel_lice_state::gfx_arc(int np, EEL_F **parms)
+{
+ LICE_IBitmap *dest = GetImageForIndex(*m_gfx_dest,"gfx_arc");
+ if (!dest) return;
+
+ const bool aa = np <= 5 || parms[5][0]>0.5;
+
+ if (LICE_FUNCTION_VALID(LICE_Arc))
+ {
+ SetImageDirty(dest);
+ LICE_Arc(dest, (float)parms[0][0], (float)parms[1][0], (float)parms[2][0], (float)parms[3][0], (float)parms[4][0], getCurColor(), (float)*m_gfx_a, getCurMode(), aa);
+ }
+}
+
+void eel_lice_state::gfx_grad_or_muladd_rect(int whichmode, int np, EEL_F **parms)
+{
+ LICE_IBitmap *dest = GetImageForIndex(*m_gfx_dest,whichmode==0?"gfx_gradrect":"gfx_muladdrect");
+ if (!dest) return;
+
+ const int x1=(int)floor(parms[0][0]),y1=(int)floor(parms[1][0]),w=(int)floor(parms[2][0]), h=(int)floor(parms[3][0]);
+
+ if (w>0 && h>0)
+ {
+ SetImageDirty(dest);
+ if (whichmode==0 && LICE_FUNCTION_VALID(LICE_GradRect) && np > 7)
+ {
+ LICE_GradRect(dest,x1,y1,w,h,(float)parms[4][0],(float)parms[5][0],(float)parms[6][0],(float)parms[7][0],
+ np > 8 ? (float)parms[8][0]:0.0f, np > 9 ? (float)parms[9][0]:0.0f, np > 10 ? (float)parms[10][0]:0.0f, np > 11 ? (float)parms[11][0]:0.0f,
+ np > 12 ? (float)parms[12][0]:0.0f, np > 13 ? (float)parms[13][0]:0.0f, np > 14 ? (float)parms[14][0]:0.0f, np > 15 ? (float)parms[15][0]:0.0f,
+ getCurMode());
+ }
+ else if (whichmode==1 && LICE_FUNCTION_VALID(LICE_MultiplyAddRect) && np > 6)
+ {
+ const double sc = 255.0;
+ LICE_MultiplyAddRect(dest,x1,y1,w,h,(float)parms[4][0],(float)parms[5][0],(float)parms[6][0],np>7 ? (float)parms[7][0]:1.0f,
+ (float)(np > 8 ? sc*parms[8][0]:0.0), (float)(np > 9 ? sc*parms[9][0]:0.0), (float)(np > 10 ? sc*parms[10][0]:0.0), (float)(np > 11 ? sc*parms[11][0]:0.0));
+ }
+ }
+}
+
+
+
+void eel_lice_state::gfx_setpixel(EEL_F r, EEL_F g, EEL_F b)
+{
+ LICE_IBitmap *dest = GetImageForIndex(*m_gfx_dest,"gfx_setpixel");
+ if (!dest) return;
+
+ int red=(int) (r*255.0);
+ int green=(int) (g*255.0);
+ int blue=(int) (b*255.0);
+ if (red<0) red=0;else if (red>255)red=255;
+ if (green<0) green=0;else if (green>255)green=255;
+ if (blue<0) blue=0; else if (blue>255) blue=255;
+
+ if (LICE_FUNCTION_VALID(LICE_PutPixel))
+ {
+ SetImageDirty(dest);
+ LICE_PutPixel(dest,(int)*m_gfx_x, (int)*m_gfx_y,LICE_RGBA(red,green,blue,255), (float)*m_gfx_a,getCurMode());
+ }
+}
+
+void eel_lice_state::gfx_getimgdim(EEL_F img, EEL_F *w, EEL_F *h)
+{
+ *w=*h=0;
+#ifdef DYNAMIC_LICE
+ if (!LICE__GetWidth || !LICE__GetHeight) return;
+#endif
+
+ LICE_IBitmap *bm=GetImageForIndex(img,"gfx_getimgdim");
+ if (bm)
+ {
+ *w=LICE__GetWidth(bm);
+ *h=LICE__GetHeight(bm);
+ }
+}
+
+EEL_F eel_lice_state::gfx_loadimg(void *opaque, int img, EEL_F loadFrom)
+{
+#ifdef DYNAMIC_LICE
+ if (!__LICE_LoadImage || !LICE__Destroy) return 0.0;
+#endif
+
+ if (img >= 0 && img < m_gfx_images.GetSize())
+ {
+ WDL_FastString fs;
+ bool ok = EEL_LICE_GET_FILENAME_FOR_STRING(loadFrom,&fs,0);
+
+ if (ok && fs.GetLength())
+ {
+ LICE_IBitmap *bm = LICE_LoadImage(fs.Get(),NULL,false);
+ if (bm)
+ {
+ LICE__Destroy(m_gfx_images.Get()[img]);
+ m_gfx_images.Get()[img]=bm;
+ return img;
+ }
+ }
+ }
+ return -1.0;
+
+}
+
+EEL_F eel_lice_state::gfx_setimgdim(int img, EEL_F *w, EEL_F *h)
+{
+ int rv=0;
+#ifdef DYNAMIC_LICE
+ if (!LICE__resize ||!LICE__GetWidth || !LICE__GetHeight||!__LICE_CreateBitmap) return 0.0;
+#endif
+
+ int use_w = (int)*w;
+ int use_h = (int)*h;
+ if (use_w<1 || use_h < 1) use_w=use_h=0;
+ if (use_w > 8192) use_w=8192;
+ if (use_h > 8192) use_h=8192;
+
+ LICE_IBitmap *bm=NULL;
+ if (img >= 0 && img < m_gfx_images.GetSize())
+ {
+ bm=m_gfx_images.Get()[img];
+ if (!bm)
+ {
+ m_gfx_images.Get()[img] = bm = __LICE_CreateBitmap(1,use_w,use_h);
+ rv=!!bm;
+ }
+ else
+ {
+ rv=LICE__resize(bm,use_w,use_h);
+ }
+ }
+
+ return rv?1.0:0.0;
+}
+
+void eel_lice_state::gfx_blurto(EEL_F x, EEL_F y)
+{
+ LICE_IBitmap *dest = GetImageForIndex(*m_gfx_dest,"gfx_blurto");
+ if (!dest
+#ifdef DYNAMIC_LICE
+ ||!LICE_Blur
+#endif
+ ) return;
+
+ SetImageDirty(dest);
+
+ int srcx = (int)x;
+ int srcy = (int)y;
+ int srcw=(int) (*m_gfx_x-x);
+ int srch=(int) (*m_gfx_y-y);
+ if (srch < 0) { srch=-srch; srcy = (int)*m_gfx_y; }
+ if (srcw < 0) { srcw=-srcw; srcx = (int)*m_gfx_x; }
+ LICE_Blur(dest,dest,srcx,srcy,srcx,srcy,srcw,srch);
+ *m_gfx_x = x;
+ *m_gfx_y = y;
+}
+
+static bool CoordsSrcDestOverlap(EEL_F *coords)
+{
+ if (coords[0]+coords[2] < coords[4]) return false;
+ if (coords[0] > coords[4] + coords[6]) return false;
+ if (coords[1]+coords[3] < coords[5]) return false;
+ if (coords[1] > coords[5] + coords[7]) return false;
+ return true;
+}
+
+void eel_lice_state::gfx_transformblit(EEL_F **parms, int div_w, int div_h, EEL_F *tab)
+{
+ LICE_IBitmap *dest = GetImageForIndex(*m_gfx_dest,"gfx_transformblit");
+
+ if (!dest
+#ifdef DYNAMIC_LICE
+ ||!LICE_ScaledBlit || !LICE_TransformBlit2 ||!LICE__GetWidth||!LICE__GetHeight
+#endif
+ ) return;
+
+ LICE_IBitmap *bm=GetImageForIndex(parms[0][0],"gfx_transformblit:src");
+ if (!bm) return;
+
+ const int bmw=LICE__GetWidth(bm);
+ const int bmh=LICE__GetHeight(bm);
+
+ const bool isFromFB = bm==m_framebuffer;
+
+ SetImageDirty(dest);
+
+ if (bm == dest)
+ {
+ if (!m_framebuffer_extra && LICE_FUNCTION_VALID(__LICE_CreateBitmap)) m_framebuffer_extra=__LICE_CreateBitmap(0,bmw,bmh);
+ if (m_framebuffer_extra)
+ {
+
+ LICE__resize(bm=m_framebuffer_extra,bmw,bmh);
+ LICE_ScaledBlit(bm,dest, // copy the entire image
+ 0,0,bmw,bmh,
+ 0.0f,0.0f,(float)bmw,(float)bmh,
+ 1.0f,LICE_BLIT_MODE_COPY);
+ }
+ }
+ LICE_TransformBlit2(dest,bm,(int)floor(parms[1][0]),(int)floor(parms[2][0]),(int)floor(parms[3][0]),(int)floor(parms[4][0]),tab,div_w,div_h, (float)*m_gfx_a,getCurModeForBlit(isFromFB));
+}
+
+EEL_F eel_lice_state::gfx_setfont(void *opaque, int np, EEL_F **parms)
+{
+ int a = np>0 ? ((int)floor(parms[0][0]))-1 : -1;
+
+ if (a>=0 && a < m_gfx_fonts.GetSize())
+ {
+ gfxFontStruct *s = m_gfx_fonts.Get()+a;
+ if (np>1 && LICE_FUNCTION_VALID(LICE_CreateFont) && LICE_FUNCTION_VALID(LICE__SetFromHFont))
+ {
+ const int sz=np>2 ? (int)parms[2][0] : 10;
+
+ bool doCreate=false;
+ int fontflag=0;
+ if (!s->font) s->actual_fontname[0]=0;
+
+ {
+ EEL_STRING_MUTEXLOCK_SCOPE
+
+ const char *face=EEL_STRING_GET_FOR_INDEX(parms[1][0],NULL);
+ #ifdef EEL_STRING_DEBUGOUT
+ if (!face) EEL_STRING_DEBUGOUT("gfx_setfont: invalid string identifier %f",parms[1][0]);
+ #endif
+ if (!face || !*face) face="Arial";
+
+ {
+ unsigned int c = np > 3 ? (unsigned int) parms[3][0] : 0;
+ while (c)
+ {
+ switch (toupper(c&0xff))
+ {
+ case 'B': fontflag|=EELFONT_FLAG_BOLD; break;
+ case 'I': fontflag|=EELFONT_FLAG_ITALIC; break;
+ case 'U': fontflag|=EELFONT_FLAG_UNDERLINE; break;
+ case 'R': fontflag|=16; break; //LICE_FONT_FLAG_FX_BLUR
+ case 'V': fontflag|=32; break; //LICE_FONT_FLAG_FX_INVERT
+ case 'M': fontflag|=64; break; //LICE_FONT_FLAG_FX_MONO
+ case 'S': fontflag|=128; break; //LICE_FONT_FLAG_FX_SHADOW
+ case 'O': fontflag|=256; break; //LICE_FONT_FLAG_FX_OUTLINE
+ case 'Z': fontflag|=1; break; //LICE_FONT_FLAG_VERTICAL
+ case 'Y': fontflag|=1|2; break; //LICE_FONT_FLAG_VERTICAL|LICE_FONT_FLAG_VERTICAL_BOTTOMUP
+ }
+ c>>=8;
+ }
+ }
+
+
+ if (fontflag != s->last_fontflag || sz!=s->last_fontsize || strncmp(s->last_fontname,face,sizeof(s->last_fontname)-1))
+ {
+ lstrcpyn_safe(s->last_fontname,face,sizeof(s->last_fontname));
+ s->last_fontsize=sz;
+ s->last_fontflag=fontflag;
+ doCreate=1;
+ }
+ }
+
+ if (doCreate)
+ {
+ s->actual_fontname[0]=0;
+ if (!s->font) s->font=LICE_CreateFont();
+ if (s->font)
+ {
+ const int fw = (fontflag&EELFONT_FLAG_BOLD) ? FW_BOLD : FW_NORMAL;
+ const bool italic = !!(fontflag&EELFONT_FLAG_ITALIC);
+ const bool underline = !!(fontflag&EELFONT_FLAG_UNDERLINE);
+ HFONT hf=NULL;
+#if defined(_WIN32) && !defined(WDL_NO_SUPPORT_UTF8)
+ WCHAR wf[256];
+ if (WDL_DetectUTF8(s->last_fontname)>0 &&
+ GetVersion()<0x80000000 &&
+ MultiByteToWideChar(CP_UTF8,MB_ERR_INVALID_CHARS,s->last_fontname,-1,wf,256))
+ {
+ hf = CreateFontW(sz,0,0,0,fw,italic,underline,FALSE,DEFAULT_CHARSET,OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,DEFAULT_QUALITY,DEFAULT_PITCH,wf);
+ }
+#endif
+ if (!hf) hf = CreateFont(sz,0,0,0,fw,italic,underline,FALSE,DEFAULT_CHARSET,OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,DEFAULT_QUALITY,DEFAULT_PITCH,s->last_fontname);
+
+ if (!hf)
+ {
+ s->use_fonth=0; // disable this font
+ }
+ else
+ {
+ TEXTMETRIC tm;
+ tm.tmHeight = sz;
+
+ if (!m_framebuffer && LICE_FUNCTION_VALID(__LICE_CreateBitmap)) m_framebuffer=__LICE_CreateBitmap(1,64,64);
+
+ if (m_framebuffer && LICE_FUNCTION_VALID(LICE__GetDC))
+ {
+ HGDIOBJ oldFont = 0;
+ HDC hdc=LICE__GetDC(m_framebuffer);
+ if (hdc)
+ {
+ oldFont = SelectObject(hdc,hf);
+ GetTextMetrics(hdc,&tm);
+
+#if defined(_WIN32) && !defined(WDL_NO_SUPPORT_UTF8)
+ if (GetVersion()<0x80000000 &&
+ GetTextFaceW(hdc,sizeof(wf)/sizeof(wf[0]),wf) &&
+ WideCharToMultiByte(CP_UTF8,0,wf,-1,s->actual_fontname,sizeof(s->actual_fontname),NULL,NULL))
+ {
+ s->actual_fontname[sizeof(s->actual_fontname)-1]=0;
+ }
+ else
+#endif
+ GetTextFace(hdc, sizeof(s->actual_fontname), s->actual_fontname);
+ SelectObject(hdc,oldFont);
+ }
+ }
+
+ s->use_fonth=wdl_max(tm.tmHeight,1);
+ LICE__SetFromHFont(s->font,hf, (fontflag & ~EELFONT_FLAG_MASK) | 512 /*LICE_FONT_FLAG_OWNS_HFONT*/);
+ }
+ }
+ }
+ }
+
+
+ if (s->font && s->use_fonth)
+ {
+ m_gfx_font_active=a;
+ if (m_gfx_texth) *m_gfx_texth=s->use_fonth;
+ return 1.0;
+ }
+ // try to init this font
+ }
+ #ifdef EEL_STRING_DEBUGOUT
+ if (a >= m_gfx_fonts.GetSize()) EEL_STRING_DEBUGOUT("gfx_setfont: invalid font %d specified",a);
+ #endif
+
+ if (a<0||a>=m_gfx_fonts.GetSize()||!m_gfx_fonts.Get()[a].font)
+ {
+ m_gfx_font_active=-1;
+ if (m_gfx_texth) *m_gfx_texth=8;
+ return 1.0;
+ }
+ return 0.0;
+}
+
+void eel_lice_state::gfx_blitext2(int np, EEL_F **parms, int blitmode)
+{
+ LICE_IBitmap *dest = GetImageForIndex(*m_gfx_dest,"gfx_blitext2");
+
+ if (!dest
+#ifdef DYNAMIC_LICE
+ ||!LICE_ScaledBlit || !LICE_RotatedBlit||!LICE__GetWidth||!LICE__GetHeight
+#endif
+ ) return;
+
+ LICE_IBitmap *bm=GetImageForIndex(parms[0][0],"gfx_blitext2:src");
+ if (!bm) return;
+
+ const int bmw=LICE__GetWidth(bm);
+ const int bmh=LICE__GetHeight(bm);
+
+ // 0=img, 1=scale, 2=rotate
+ double coords[8];
+ const double sc = blitmode==0 ? parms[1][0] : 1.0,
+ angle = blitmode==0 ? parms[2][0] : 0.0;
+ if (blitmode==0)
+ {
+ parms+=2;
+ np -= 2;
+ }
+
+ coords[0]=np > 1 ? parms[1][0] : 0.0f;
+ coords[1]=np > 2 ? parms[2][0] : 0.0f;
+ coords[2]=np > 3 ? parms[3][0] : bmw;
+ coords[3]=np > 4 ? parms[4][0] : bmh;
+ coords[4]=np > 5 ? parms[5][0] : *m_gfx_x;
+ coords[5]=np > 6 ? parms[6][0] : *m_gfx_y;
+ coords[6]=np > 7 ? parms[7][0] : coords[2]*sc;
+ coords[7]=np > 8 ? parms[8][0] : coords[3]*sc;
+
+ const bool isFromFB = bm == m_framebuffer;
+ SetImageDirty(dest);
+
+ if (bm == dest && CoordsSrcDestOverlap(coords))
+ {
+ if (!m_framebuffer_extra && LICE_FUNCTION_VALID(__LICE_CreateBitmap)) m_framebuffer_extra=__LICE_CreateBitmap(0,bmw,bmh);
+ if (m_framebuffer_extra)
+ {
+
+ LICE__resize(bm=m_framebuffer_extra,bmw,bmh);
+ LICE_ScaledBlit(bm,dest, // copy the source portion
+ (int)coords[0],(int)coords[1],(int)coords[2],(int)coords[3],
+ (float)coords[0],(float)coords[1],(float)coords[2],(float)coords[3],
+ 1.0f,LICE_BLIT_MODE_COPY);
+ }
+ }
+
+ if (blitmode==1)
+ {
+ if (LICE_FUNCTION_VALID(LICE_DeltaBlit))
+ LICE_DeltaBlit(dest,bm,(int)coords[4],(int)coords[5],(int)coords[6],(int)coords[7],
+ (float)coords[0],(float)coords[1],(float)coords[2],(float)coords[3],
+ np > 9 ? (float)parms[9][0]:1.0f, // dsdx
+ np > 10 ? (float)parms[10][0]:0.0f, // dtdx
+ np > 11 ? (float)parms[11][0]:0.0f, // dsdy
+ np > 12 ? (float)parms[12][0]:1.0f, // dtdy
+ np > 13 ? (float)parms[13][0]:0.0f, // dsdxdy
+ np > 14 ? (float)parms[14][0]:0.0f, // dtdxdy
+ np <= 15 || parms[15][0] > 0.5, (float)*m_gfx_a,getCurModeForBlit(isFromFB));
+ }
+ else if (fabs(angle)>0.000000001)
+ {
+ LICE_RotatedBlit(dest,bm,(int)coords[4],(int)coords[5],(int)coords[6],(int)coords[7],
+ (float)coords[0],(float)coords[1],(float)coords[2],(float)coords[3],
+ (float)angle,true, (float)*m_gfx_a,getCurModeForBlit(isFromFB),
+ np > 9 ? (float)parms[9][0] : 0.0f,
+ np > 10 ? (float)parms[10][0] : 0.0f);
+ }
+ else
+ {
+ LICE_ScaledBlit(dest,bm,(int)coords[4],(int)coords[5],(int)coords[6],(int)coords[7],
+ (float)coords[0],(float)coords[1],(float)coords[2],(float)coords[3], (float)*m_gfx_a,getCurModeForBlit(isFromFB));
+ }
+}
+
+void eel_lice_state::gfx_blitext(EEL_F img, EEL_F *coords, EEL_F angle)
+{
+ LICE_IBitmap *dest = GetImageForIndex(*m_gfx_dest,"gfx_blitext");
+
+ if (!dest
+#ifdef DYNAMIC_LICE
+ ||!LICE_ScaledBlit || !LICE_RotatedBlit||!LICE__GetWidth||!LICE__GetHeight
+#endif
+ ) return;
+
+ LICE_IBitmap *bm=GetImageForIndex(img,"gfx_blitext:src");
+ if (!bm) return;
+
+ SetImageDirty(dest);
+ const bool isFromFB = bm == m_framebuffer;
+
+ int bmw=LICE__GetWidth(bm);
+ int bmh=LICE__GetHeight(bm);
+
+ if (bm == dest && CoordsSrcDestOverlap(coords))
+ {
+ if (!m_framebuffer_extra && LICE_FUNCTION_VALID(__LICE_CreateBitmap)) m_framebuffer_extra=__LICE_CreateBitmap(0,bmw,bmh);
+ if ( m_framebuffer_extra)
+ {
+
+ LICE__resize(bm=m_framebuffer_extra,bmw,bmh);
+ LICE_ScaledBlit(bm,dest, // copy the source portion
+ (int)coords[0],(int)coords[1],(int)coords[2],(int)coords[3],
+ (float)coords[0],(float)coords[1],(float)coords[2],(float)coords[3],
+ 1.0f,LICE_BLIT_MODE_COPY);
+ }
+ }
+
+ if (fabs(angle)>0.000000001)
+ {
+ LICE_RotatedBlit(dest,bm,(int)coords[4],(int)coords[5],(int)coords[6],(int)coords[7],
+ (float)coords[0],(float)coords[1],(float)coords[2],(float)coords[3],(float)angle,
+ true, (float)*m_gfx_a,getCurModeForBlit(isFromFB),
+ (float)coords[8],(float)coords[9]);
+ }
+ else
+ {
+ LICE_ScaledBlit(dest,bm,(int)coords[4],(int)coords[5],(int)coords[6],(int)coords[7],
+ (float)coords[0],(float)coords[1],(float)coords[2],(float)coords[3], (float)*m_gfx_a,getCurModeForBlit(isFromFB));
+ }
+}
+
+void eel_lice_state::gfx_blit(EEL_F img, EEL_F scale, EEL_F rotate)
+{
+ LICE_IBitmap *dest = GetImageForIndex(*m_gfx_dest,"gfx_blit");
+ if (!dest
+#ifdef DYNAMIC_LICE
+ ||!LICE_ScaledBlit || !LICE_RotatedBlit||!LICE__GetWidth||!LICE__GetHeight
+#endif
+ ) return;
+
+ LICE_IBitmap *bm=GetImageForIndex(img,"gfx_blit:src");
+
+ if (!bm) return;
+
+ SetImageDirty(dest);
+ const bool isFromFB = bm == m_framebuffer;
+
+ int bmw=LICE__GetWidth(bm);
+ int bmh=LICE__GetHeight(bm);
+ if (fabs(rotate)>0.000000001)
+ {
+ LICE_RotatedBlit(dest,bm,(int)*m_gfx_x,(int)*m_gfx_y,(int) (bmw*scale),(int) (bmh*scale),0.0f,0.0f,(float)bmw,(float)bmh,(float)rotate,true, (float)*m_gfx_a,getCurModeForBlit(isFromFB),
+ 0.0f,0.0f);
+ }
+ else
+ {
+ LICE_ScaledBlit(dest,bm,(int)*m_gfx_x,(int)*m_gfx_y,(int) (bmw*scale),(int) (bmh*scale),0.0f,0.0f,(float)bmw,(float)bmh, (float)*m_gfx_a,getCurModeForBlit(isFromFB));
+ }
+}
+
+void eel_lice_state::gfx_set(int np, EEL_F **parms)
+{
+ if (np < 1) return;
+ if (m_gfx_r) *m_gfx_r = parms[0][0];
+ if (m_gfx_g) *m_gfx_g = np > 1 ? parms[1][0] : parms[0][0];
+ if (m_gfx_b) *m_gfx_b = np > 2 ? parms[2][0] : parms[0][0];
+ if (m_gfx_a) *m_gfx_a = np > 3 ? parms[3][0] : 1.0;
+ if (m_gfx_mode) *m_gfx_mode = np > 4 ? parms[4][0] : 0;
+ if (np > 5 && m_gfx_dest) *m_gfx_dest = parms[5][0];
+ if (m_gfx_a2) *m_gfx_a2 = np > 6 ? parms[6][0] : 1.0;
+}
+
+void eel_lice_state::gfx_getpixel(EEL_F *r, EEL_F *g, EEL_F *b)
+{
+ LICE_IBitmap *dest = GetImageForIndex(*m_gfx_dest,"gfx_getpixel");
+ if (!dest) return;
+
+ int ret=LICE_FUNCTION_VALID(LICE_GetPixel)?LICE_GetPixel(dest,(int)*m_gfx_x, (int)*m_gfx_y):0;
+
+ *r=LICE_GETR(ret)/255.0;
+ *g=LICE_GETG(ret)/255.0;
+ *b=LICE_GETB(ret)/255.0;
+
+}
+
+
+static int __drawTextWithFont(LICE_IBitmap *dest, const RECT *rect, LICE_IFont *font, const char *buf, int buflen,
+ int fg, int mode, float alpha, int flags, EEL_F *wantYoutput, EEL_F **measureOnly)
+{
+ if (font && LICE_FUNCTION_VALID(LICE__DrawText))
+ {
+ RECT tr=*rect;
+ LICE__SetTextColor(font,fg);
+ LICE__SetTextCombineMode(font,mode,alpha);
+
+ int maxx=0;
+ RECT r={0,0,tr.left,0};
+ while (buflen>0)
+ {
+ int thislen = 0;
+ while (thislen < buflen && buf[thislen] != '\n') thislen++;
+ memset(&r,0,sizeof(r));
+ int lineh = LICE__DrawText(font,dest,buf,thislen?thislen:1,&r,DT_SINGLELINE|DT_NOPREFIX|DT_CALCRECT);
+ if (!measureOnly)
+ {
+ r.right += tr.left;
+ lineh = LICE__DrawText(font,dest,buf,thislen?thislen:1,&tr,DT_SINGLELINE|DT_NOPREFIX|flags);
+ if (wantYoutput) *wantYoutput = tr.top;
+ }
+ else
+ {
+ if (r.right > maxx) maxx=r.right;
+ }
+ tr.top += lineh;
+
+ buflen -= thislen+1;
+ buf += thislen+1;
+ }
+ if (measureOnly)
+ {
+ measureOnly[0][0] = maxx;
+ measureOnly[1][0] = tr.top;
+ }
+ return r.right;
+ }
+ else
+ {
+ int xpos=rect->left, ypos=rect->top;
+ int x;
+ int maxx=0,maxy=0;
+
+ LICE_SubBitmap sbm(
+#ifdef DYNAMIC_LICE
+ (LICE_IBitmap_disabledAPI*)
+#endif
+ dest,rect->left,rect->top,rect->right-rect->left,rect->bottom-rect->top);
+
+ if (!measureOnly)
+ {
+ if (!(flags & DT_NOCLIP))
+ {
+ if (rect->right <= rect->left || rect->bottom <= rect->top) return 0; // invalid clip rect hm
+
+ xpos = ypos = 0;
+ dest = &sbm;
+ }
+ if (flags & (DT_RIGHT|DT_BOTTOM|DT_CENTER|DT_VCENTER))
+ {
+ EEL_F w=0.0,h=0.0;
+ EEL_F *mo[2] = { &w,&h};
+ RECT tr={0,};
+ __drawTextWithFont(dest,&tr,NULL,buf,buflen,0,0,0.0f,0,NULL,mo);
+
+ if (flags & DT_RIGHT) xpos += (rect->right-rect->left) - (int)floor(w);
+ else if (flags & DT_CENTER) xpos += (rect->right-rect->left)/2 - (int)floor(w*.5);
+
+ if (flags & DT_BOTTOM) ypos += (rect->bottom-rect->top) - (int)floor(h);
+ else if (flags & DT_VCENTER) ypos += (rect->bottom-rect->top)/2 - (int)floor(h*.5);
+ }
+ }
+ const int sxpos = xpos;
+
+ if (LICE_FUNCTION_VALID(LICE_DrawChar)) for(x=0;x maxx) maxx=xpos;
+ maxy = ypos + 8;
+ break;
+ }
+ }
+ if (measureOnly)
+ {
+ measureOnly[0][0]=maxx;
+ measureOnly[1][0]=maxy;
+ }
+ else
+ {
+ if (wantYoutput) *wantYoutput=ypos;
+ }
+ return xpos;
+ }
+}
+
+void eel_lice_state::gfx_drawstr(void *opaque, EEL_F **parms, int nparms, int formatmode)// formatmode=1 for format, 2 for purely measure no format
+{
+ int nfmtparms = nparms-1;
+ EEL_F **fmtparms = parms+1;
+ const char *funcname = formatmode==1?"gfx_printf":
+ formatmode==2?"gfx_measurestr":
+ formatmode==3?"gfx_measurechar" : "gfx_drawstr";
+
+ LICE_IBitmap *dest = GetImageForIndex(*m_gfx_dest,funcname);
+ if (!dest) return;
+
+#ifdef DYNAMIC_LICE
+ if (!LICE__GetWidth || !LICE__GetHeight) return;
+#endif
+
+ EEL_STRING_MUTEXLOCK_SCOPE
+
+ WDL_FastString *fs=NULL;
+ char buf[4096];
+ int s_len=0;
+
+ const char *s;
+ if (formatmode==3)
+ {
+ s_len = WDL_MakeUTFChar(buf, (int)parms[0][0], sizeof(buf));
+ s=buf;
+ }
+ else
+ {
+ s=EEL_STRING_GET_FOR_INDEX(parms[0][0],&fs);
+ #ifdef EEL_STRING_DEBUGOUT
+ if (!s) EEL_STRING_DEBUGOUT("gfx_%s: invalid string identifier %f",funcname,parms[0][0]);
+ #endif
+ if (!s)
+ {
+ s="";
+ s_len = 12;
+ }
+ else if (formatmode==1)
+ {
+ extern int eel_format_strings(void *, const char *s, const char *ep, char *, int, int, EEL_F **);
+ s_len = eel_format_strings(opaque,s,fs?(s+fs->GetLength()):NULL,buf,sizeof(buf),nfmtparms,fmtparms);
+ if (s_len<1) return;
+ s=buf;
+ }
+ else
+ {
+ s_len = fs?fs->GetLength():(int)strlen(s);
+ }
+ }
+
+ if (s_len)
+ {
+ SetImageDirty(dest);
+ if (formatmode>=2)
+ {
+ if (nfmtparms==2)
+ {
+ RECT r={0,0,0,0};
+ __drawTextWithFont(dest,&r,GetActiveFont(),s,s_len,
+ getCurColor(),getCurMode(),(float)*m_gfx_a,0,NULL,fmtparms);
+ }
+ }
+ else
+ {
+ RECT r={(int)floor(*m_gfx_x),(int)floor(*m_gfx_y),0,0};
+ int flags=DT_NOCLIP;
+ if (formatmode == 0 && nparms >= 4)
+ {
+ flags=(int)*parms[1];
+ flags &= (DT_CENTER|DT_RIGHT|DT_VCENTER|DT_BOTTOM|DT_NOCLIP);
+ r.right=(int)*parms[2];
+ r.bottom=(int)*parms[3];
+ }
+ *m_gfx_x=__drawTextWithFont(dest,&r,GetActiveFont(),s,s_len,
+ getCurColor(),getCurMode(),(float)*m_gfx_a,flags,m_gfx_y,NULL);
+ }
+ }
+}
+
+void eel_lice_state::gfx_drawchar(EEL_F ch)
+{
+ LICE_IBitmap *dest = GetImageForIndex(*m_gfx_dest,"gfx_drawchar");
+ if (!dest) return;
+
+ SetImageDirty(dest);
+
+ int a=(int)(ch+0.5);
+ if (a == '\r' || a=='\n') a=' ';
+
+ char buf[32];
+ const int buflen = WDL_MakeUTFChar(buf, a, sizeof(buf));
+
+ RECT r={(int)floor(*m_gfx_x),(int)floor(*m_gfx_y),0,0};
+ *m_gfx_x = __drawTextWithFont(dest,&r,
+ GetActiveFont(),buf,buflen,
+ getCurColor(),getCurMode(),(float)*m_gfx_a,DT_NOCLIP,NULL,NULL);
+
+}
+
+
+void eel_lice_state::gfx_drawnumber(EEL_F n, EEL_F ndigits)
+{
+ LICE_IBitmap *dest = GetImageForIndex(*m_gfx_dest,"gfx_drawnumber");
+ if (!dest) return;
+
+ SetImageDirty(dest);
+
+ char buf[512];
+ int a=(int)(ndigits+0.5);
+ if (a <0)a=0;
+ else if (a > 16) a=16;
+ snprintf(buf,sizeof(buf),"%.*f",a,n);
+
+ RECT r={(int)floor(*m_gfx_x),(int)floor(*m_gfx_y),0,0};
+ *m_gfx_x = __drawTextWithFont(dest,&r,
+ GetActiveFont(),buf,(int)strlen(buf),
+ getCurColor(),getCurMode(),(float)*m_gfx_a,DT_NOCLIP,NULL,NULL);
+}
diff --git a/source/modules/ysfx/sources/ysfx_api_reaper.cpp b/source/modules/ysfx/sources/ysfx_api_reaper.cpp
new file mode 100644
index 000000000..2fdcb7274
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx_api_reaper.cpp
@@ -0,0 +1,439 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#include "ysfx.hpp"
+#include "ysfx_api_reaper.hpp"
+#include "ysfx_api_eel.hpp"
+#include "ysfx_eel_utils.hpp"
+#include "ysfx_utils.hpp"
+#include
+#include
+#include
+
+#include "WDL/wdlstring.h"
+
+#define REAPER_GET_INTERFACE(opaque) ((opaque) ? (ysfx_t *)(opaque) : nullptr)
+
+static EEL_F *NSEEL_CGEN_CALL ysfx_api_spl(void *opaque, EEL_F *n_)
+{
+ //NOTE: callable from @gfx thread
+
+ ysfx_t *fx = REAPER_GET_INTERFACE(opaque);
+ int32_t n = ysfx_eel_round(*n_);
+
+ if (n < 0 || n >= ysfx_max_channels) {
+ fx->var.ret_temp = 0;
+ return &fx->var.ret_temp;
+ }
+
+ return fx->var.spl[(uint32_t)n];
+}
+
+static EEL_F *NSEEL_CGEN_CALL ysfx_api_slider(void *opaque, EEL_F *n_)
+{
+ //NOTE: callable from @gfx thread
+
+ ysfx_t *fx = REAPER_GET_INTERFACE(opaque);
+ int32_t n = ysfx_eel_round(*n_);
+
+ if (n < 1 || n > ysfx_max_sliders) {
+ fx->var.ret_temp = 0;
+ return &fx->var.ret_temp;
+ }
+
+ n -= 1;
+ return fx->var.slider[(uint32_t)n];
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_slider_next_chg(void *opaque, EEL_F *index_, EEL_F *val_)
+{
+ //TODO frame-accurate slider changes
+ (void)opaque;
+ (void)index_;
+ (void)val_;
+ return -1;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_slider_automate(void *opaque, EEL_F *mask_or_slider_)
+{
+ //NOTE: callable from @gfx thread
+
+ ysfx_t *fx = REAPER_GET_INTERFACE(opaque);
+ uint32_t slider = ysfx_get_slider_of_var(fx, mask_or_slider_);
+
+ uint64_t mask;
+ if (slider < ysfx_max_sliders)
+ mask = (uint64_t)1 << slider;
+ else
+ mask = ysfx_eel_round(std::fabs(*mask_or_slider_));
+
+ fx->slider.automate_mask |= mask;
+ fx->slider.change_mask |= mask;
+ return 0;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_sliderchange(void *opaque, EEL_F *mask_or_slider_)
+{
+ //NOTE: callable from @gfx thread
+
+ ysfx_t *fx = REAPER_GET_INTERFACE(opaque);
+ uint32_t slider = ysfx_get_slider_of_var(fx, mask_or_slider_);
+
+ uint64_t mask;
+ if (slider < ysfx_max_sliders)
+ mask = (uint64_t)1 << slider;
+ else
+ mask = ysfx_eel_round(std::fabs(*mask_or_slider_));
+
+ fx->slider.change_mask |= mask;
+ return 0;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_slider_show(void *opaque, EEL_F *mask_or_slider_, EEL_F *value_)
+{
+ //NOTE: callable from @gfx thread
+
+ ysfx_t *fx = REAPER_GET_INTERFACE(opaque);
+ uint32_t slider = ysfx_get_slider_of_var(fx, mask_or_slider_);
+
+ uint64_t mask;
+ if (slider < ysfx_max_sliders)
+ mask = (uint64_t)1 << slider;
+ else
+ mask = ysfx_eel_round(std::fabs(*mask_or_slider_));
+
+ if (*value_ >= (EEL_F)+0.5) {
+ // show
+ fx->slider.visible_mask |= mask;
+ }
+ else if (*value_ >= (EEL_F)-0.5) {
+ // hide
+ mask = ~mask;
+ fx->slider.visible_mask &= mask;
+ }
+ else {
+ // toggle
+ mask = fx->slider.visible_mask.fetch_xor(mask) ^ mask;
+ }
+
+ return (EEL_F)mask;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_midisend(void *opaque, INT_PTR np, EEL_F **parms)
+{
+ if (ysfx_get_thread_id() != ysfx_thread_id_dsp)
+ return 0;
+
+ int32_t offset;
+ uint8_t msg1;
+ uint8_t msg2;
+ uint8_t msg3;
+
+ switch (np) {
+ case 3:
+ {
+ offset = ysfx_eel_round(*parms[0]);
+ msg1 = (uint8_t)ysfx_eel_round(*parms[1]);
+ const uint32_t msg23 = ysfx_eel_round(*parms[2]);
+ msg2 = (uint8_t)(msg23 & 0xff);
+ msg3 = (uint8_t)(msg23 >> 8);
+ break;
+ }
+ case 4:
+ offset = ysfx_eel_round(*parms[0]);
+ msg1 = (uint8_t)ysfx_eel_round(*parms[1]);
+ msg2 = (uint8_t)ysfx_eel_round(*parms[2]);
+ msg3 = (uint8_t)ysfx_eel_round(*parms[3]);
+ break;
+ default:
+ assert(false);
+ return 0;
+ }
+
+ if (offset < 0)
+ offset = 0;
+
+ // NOTE(jpc) correct the length of the message
+ // in case it should be less than 3 bytes
+ uint32_t length = ysfx_midi_sizeof(msg1);
+ if (length == 0) // don't know what message this is
+ length = 3;
+
+ ysfx_t *fx = REAPER_GET_INTERFACE(opaque);
+ ysfx_midi_event_t event;
+ const uint8_t data[] = {msg1, msg2, msg3};
+ event.bus = ysfx_current_midi_bus(fx);
+ event.offset = (uint32_t)offset;
+ event.size = length;
+ event.data = data;
+ if (!ysfx_midi_push(fx->midi.out.get(), &event))
+ return 0;
+
+ return msg1;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_midisend_buf(void *opaque, EEL_F *offset_, EEL_F *buf_, EEL_F *len_)
+{
+ if (ysfx_get_thread_id() != ysfx_thread_id_dsp)
+ return 0;
+
+ int32_t offset = ysfx_eel_round(*offset_);
+ int32_t buf = ysfx_eel_round(*buf_);
+ int32_t len = ysfx_eel_round(*len_);
+
+ if (len <= 0)
+ return 0;
+ if (offset < 0)
+ offset = 0;
+
+ ysfx_t *fx = REAPER_GET_INTERFACE(opaque);
+
+ ysfx_midi_push_t mp;
+ if (!ysfx_midi_push_begin(fx->midi.out.get(), ysfx_current_midi_bus(fx), (uint32_t)offset, &mp))
+ return 0;
+
+ ysfx_eel_ram_reader reader{fx->vm.get(), buf};
+ for (uint32_t i = 0; i < (uint32_t)len; ++i) {
+ uint8_t byte = (uint8_t)ysfx_eel_round(reader.read_next());
+ if (!ysfx_midi_push_data(&mp, &byte, 1))
+ break;
+ }
+
+ if (!ysfx_midi_push_end(&mp))
+ return 0;
+
+ return len;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_midisend_str(void *opaque, EEL_F *offset_, EEL_F *str_)
+{
+ if (ysfx_get_thread_id() != ysfx_thread_id_dsp)
+ return 0;
+
+ int32_t offset = ysfx_eel_round(*offset_);
+
+ if (offset < 0)
+ offset = 0;
+
+ ysfx_t *fx = REAPER_GET_INTERFACE(opaque);
+
+ ///
+ struct process_data {
+ ysfx_t *fx = nullptr;
+ uint32_t offset = 0;
+ uint32_t result = 0;
+ };
+
+ process_data pdata;
+ pdata.offset = (uint32_t)offset;
+ pdata.fx = fx;
+
+ auto process_str = [](void *userdata, WDL_FastString &str) {
+ process_data *pdata = (process_data *)userdata;
+ ysfx_t *fx = pdata->fx;
+ ysfx_midi_event_t event;
+ event.bus = ysfx_current_midi_bus(fx);
+ event.offset = pdata->offset;
+ event.size = (uint32_t)str.GetLength();
+ event.data = (const uint8_t *)str.Get();
+ pdata->result = ysfx_midi_push(fx->midi.out.get(), &event) ? event.size : 0;
+ };
+
+ ///
+ if (!ysfx_string_access(fx, *str_, false, +process_str, &pdata))
+ return 0;
+
+ return pdata.result;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_midisyx(void *opaque, EEL_F *offset_, EEL_F *buf_, EEL_F *len_)
+{
+ if (ysfx_get_thread_id() != ysfx_thread_id_dsp)
+ return 0;
+
+ int32_t offset = ysfx_eel_round(*offset_);
+ int32_t buf = ysfx_eel_round(*buf_);
+ int32_t len = ysfx_eel_round(*len_);
+
+ if (len <= 0)
+ return 0;
+ if (offset < 0)
+ offset = 0;
+
+ ysfx_t *fx = REAPER_GET_INTERFACE(opaque);
+
+ ysfx_midi_push_t mp;
+ if (!ysfx_midi_push_begin(fx->midi.out.get(), ysfx_current_midi_bus(fx), (uint32_t)offset, &mp))
+ return 0;
+
+ ysfx_eel_ram_reader reader{fx->vm.get(), buf};
+ for (uint32_t i = 0; i < (uint32_t)len; ++i) {
+ uint8_t byte = (uint8_t)ysfx_eel_round(reader.read_next());
+ const uint8_t head = 0xf0;
+ const uint8_t tail = 0xf7;
+ if (i == 0 && byte != head) {
+ if (!ysfx_midi_push_data(&mp, &head, 1))
+ break;
+ }
+ if (!ysfx_midi_push_data(&mp, &byte, 1))
+ break;
+ if (i + 1 == (uint32_t)len && byte != tail) {
+ if (!ysfx_midi_push_data(&mp, &tail, 1))
+ break;
+ }
+ }
+
+ if (!ysfx_midi_push_end(&mp))
+ return 0;
+
+ return len;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_midirecv(void *opaque, INT_PTR np, EEL_F **parms)
+{
+ if (ysfx_get_thread_id() != ysfx_thread_id_dsp)
+ return 0;
+
+ ysfx_t *fx = REAPER_GET_INTERFACE(opaque);
+ uint32_t bus = ysfx_current_midi_bus(fx);
+
+ ysfx_midi_event_t event;
+ bool have_event = ysfx_midi_get_next_from_bus(fx->midi.in.get(), bus, &event);
+ // pass through the sysex events
+ while (have_event && event.size > 3) {
+ ysfx_midi_push(fx->midi.out.get(), &event);
+ have_event = ysfx_midi_get_next_from_bus(fx->midi.in.get(), bus, &event);
+ }
+ if (!have_event)
+ return 0;
+
+ uint8_t msg1 = 0;
+ uint8_t msg2 = 0;
+ uint8_t msg3 = 0;
+
+ switch (event.size) {
+ case 3: msg3 = event.data[2]; // fall through
+ case 2: msg2 = event.data[1]; // fall through
+ case 1: msg1 = event.data[0]; break;
+ }
+
+ *parms[0] = (EEL_F)event.offset;
+ *parms[1] = (EEL_F)msg1;
+ switch (np) {
+ case 4:
+ *parms[2] = (EEL_F)msg2;
+ *parms[3] = (EEL_F)msg3;
+ break;
+ case 3:
+ *parms[2] = (EEL_F)(msg2 + (msg3 << 8));
+ break;
+ default:
+ assert(false);
+ return 0;
+ }
+
+ return 1;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_midirecv_buf(void *opaque, EEL_F *offset_, EEL_F *buf_, EEL_F *recvlen_)
+{
+ if (ysfx_get_thread_id() != ysfx_thread_id_dsp)
+ return 0;
+
+ ysfx_t *fx = REAPER_GET_INTERFACE(opaque);
+ NSEEL_VMCTX vm = fx->vm.get();
+
+ int32_t buf = ysfx_eel_round(*buf_);
+ int32_t recvlen = ysfx_eel_round(*recvlen_);
+
+ if (recvlen < 0)
+ recvlen = 0;
+
+ uint32_t bus = ysfx_current_midi_bus(fx);
+
+ ysfx_midi_event_t event;
+ bool have_event = ysfx_midi_get_next_from_bus(fx->midi.in.get(), bus, &event);
+ // pass through the events larger than the buffer
+ while (have_event && event.size > (uint32_t)recvlen) {
+ ysfx_midi_push(fx->midi.out.get(), &event);
+ have_event = ysfx_midi_get_next_from_bus(fx->midi.in.get(), bus, &event);
+ }
+ if (!have_event)
+ return 0;
+
+ *offset_ = (EEL_F)event.offset;
+
+ ysfx_eel_ram_writer writer{vm, buf};
+ for (uint32_t i = 0; i < event.size; ++i)
+ writer.write_next(event.data[i]);
+
+ return event.size;
+}
+
+static EEL_F NSEEL_CGEN_CALL ysfx_api_midirecv_str(void *opaque, EEL_F *offset_, EEL_F *str_)
+{
+ if (ysfx_get_thread_id() != ysfx_thread_id_dsp)
+ return 0;
+
+ ysfx_t *fx = REAPER_GET_INTERFACE(opaque);
+
+ uint32_t bus = ysfx_current_midi_bus(fx);
+
+ ysfx_midi_event_t event;
+ bool have_event = ysfx_midi_get_next_from_bus(fx->midi.in.get(), bus, &event);
+ // pass through the events larger than the maximum string
+ while (have_event && event.size > ysfx_string_max_length) {
+ ysfx_midi_push(fx->midi.out.get(), &event);
+ have_event = ysfx_midi_get_next_from_bus(fx->midi.in.get(), bus, &event);
+ }
+ if (!have_event)
+ return 0;
+
+ auto process_str = [](void *userdata, WDL_FastString &str) {
+ ysfx_midi_event_t *event = (ysfx_midi_event_t *)userdata;
+ str.SetRaw((const char *)event->data, (int32_t)event->size);
+ };
+
+ ///
+ if (!ysfx_string_access(fx, *str_, true, +process_str, &event))
+ return 0;
+
+ *offset_ = (EEL_F)event.offset;
+ return event.size;
+}
+
+//------------------------------------------------------------------------------
+void ysfx_api_init_reaper()
+{
+ NSEEL_addfunc_retptr("spl", 1, NSEEL_PProc_THIS, &ysfx_api_spl);
+ NSEEL_addfunc_retptr("slider", 1, NSEEL_PProc_THIS, &ysfx_api_slider);
+
+ NSEEL_addfunc_retval("slider_next_chg", 2, NSEEL_PProc_THIS, &ysfx_api_slider_next_chg);
+ NSEEL_addfunc_retval("slider_automate", 1, NSEEL_PProc_THIS, &ysfx_api_slider_automate);
+ NSEEL_addfunc_retval("sliderchange", 1, NSEEL_PProc_THIS, &ysfx_api_sliderchange);
+ NSEEL_addfunc_retval("slider_show", 2, NSEEL_PProc_THIS, &ysfx_api_slider_show);
+
+ NSEEL_addfunc_exparms("midisend", 3, NSEEL_PProc_THIS, &ysfx_api_midisend);
+ NSEEL_addfunc_exparms("midisend", 4, NSEEL_PProc_THIS, &ysfx_api_midisend);
+ NSEEL_addfunc_retval("midisend_buf", 3, NSEEL_PProc_THIS, &ysfx_api_midisend_buf);
+ NSEEL_addfunc_retval("midisend_str", 2, NSEEL_PProc_THIS, &ysfx_api_midisend_str);
+ NSEEL_addfunc_exparms("midirecv", 3, NSEEL_PProc_THIS, &ysfx_api_midirecv);
+ NSEEL_addfunc_exparms("midirecv", 4, NSEEL_PProc_THIS, &ysfx_api_midirecv);
+ NSEEL_addfunc_retval("midirecv_buf", 3, NSEEL_PProc_THIS, &ysfx_api_midirecv_buf);
+ NSEEL_addfunc_retval("midirecv_str", 2, NSEEL_PProc_THIS, &ysfx_api_midirecv_str);
+ NSEEL_addfunc_retval("midisyx", 3, NSEEL_PProc_THIS, &ysfx_api_midisyx);
+}
diff --git a/source/modules/ysfx/sources/ysfx_api_reaper.hpp b/source/modules/ysfx/sources/ysfx_api_reaper.hpp
new file mode 100644
index 000000000..153797b94
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx_api_reaper.hpp
@@ -0,0 +1,20 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#pragma once
+
+void ysfx_api_init_reaper();
diff --git a/source/modules/ysfx/sources/ysfx_audio_flac.cpp b/source/modules/ysfx/sources/ysfx_audio_flac.cpp
new file mode 100644
index 000000000..35c6e329c
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx_audio_flac.cpp
@@ -0,0 +1,166 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#include "ysfx_audio_flac.hpp"
+#include "ysfx_utils.hpp"
+#include
+#include
+
+#if defined(__GNUC__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wunused-function"
+#endif
+
+#define DR_FLAC_IMPLEMENTATION
+#define DRFLAC_API static
+#define DRFLAC_PRIVATE static
+#include "dr_flac.h"
+
+#if defined(__GNUC__)
+# pragma GCC diagnostic pop
+#endif
+
+struct drflac_u_deleter {
+ void operator()(drflac *x) const noexcept { drflac_close(x); }
+};
+using drflac_u = std::unique_ptr;
+
+///
+struct ysfx_flac_reader_t {
+ drflac_u flac;
+ uint32_t nbuff = 0;
+ std::unique_ptr buff;
+};
+
+static bool ysfx_flac_can_handle(const char *path)
+{
+ return ysfx::path_has_suffix(path, "flac");
+}
+
+static ysfx_audio_reader_t *ysfx_flac_open(const char *path)
+{
+#if !defined(_WIN32)
+ drflac_u flac{drflac_open_file(path, NULL)};
+#else
+ drflac_u flac{drflac_open_file_w(ysfx::widen(path).c_str(), NULL)};
+#endif
+ if (!flac)
+ return nullptr;
+ std::unique_ptr reader{new ysfx_flac_reader_t};
+ reader->flac = std::move(flac);
+ reader->buff.reset(new float[reader->flac->channels]);
+ return (ysfx_audio_reader_t *)reader.release();
+}
+
+static void ysfx_flac_close(ysfx_audio_reader_t *reader_)
+{
+ ysfx_flac_reader_t *reader = (ysfx_flac_reader_t *)reader_;
+ delete reader;
+}
+
+static ysfx_audio_file_info_t ysfx_flac_info(ysfx_audio_reader_t *reader_)
+{
+ ysfx_flac_reader_t *reader = (ysfx_flac_reader_t *)reader_;
+ ysfx_audio_file_info_t info;
+ info.channels = reader->flac->channels;
+ info.sample_rate = (ysfx_real)reader->flac->sampleRate;
+ return info;
+}
+
+static uint64_t ysfx_flac_avail(ysfx_audio_reader_t *reader_)
+{
+ ysfx_flac_reader_t *reader = (ysfx_flac_reader_t *)reader_;
+ return reader->nbuff + reader->flac->channels * (reader->flac->totalPCMFrameCount - reader->flac->currentPCMFrame);
+}
+
+static void ysfx_flac_rewind(ysfx_audio_reader_t *reader_)
+{
+ ysfx_flac_reader_t *reader = (ysfx_flac_reader_t *)reader_;
+ drflac_seek_to_pcm_frame(reader->flac.get(), 0);
+ reader->nbuff = 0;
+}
+
+static uint64_t ysfx_flac_unload_buffer(ysfx_audio_reader_t *reader_, ysfx_real *samples, uint64_t count)
+{
+ ysfx_flac_reader_t *reader = (ysfx_flac_reader_t *)reader_;
+
+ uint32_t nbuff = reader->nbuff;
+ if (nbuff > count)
+ nbuff = (uint32_t)count;
+
+ if (nbuff == 0)
+ return 0;
+
+ const float *src = &reader->buff[reader->flac->channels - reader->nbuff];
+ for (uint32_t i = 0; i < nbuff; ++i)
+ samples[i] = src[i];
+
+ reader->nbuff -= nbuff;
+ return nbuff;
+}
+
+static uint64_t ysfx_flac_read(ysfx_audio_reader_t *reader_, ysfx_real *samples, uint64_t count)
+{
+ ysfx_flac_reader_t *reader = (ysfx_flac_reader_t *)reader_;
+ uint32_t channels = reader->flac->channels;
+ uint64_t readtotal = 0;
+
+ if (count == 0)
+ return readtotal;
+ else {
+ uint64_t copied = ysfx_flac_unload_buffer(reader_, samples, count);
+ samples += copied;
+ count -= copied;
+ readtotal += copied;
+ }
+
+ if (count == 0)
+ return readtotal;
+ else {
+ float *f32buf = (float *)samples;
+ uint64_t readframes = drflac_read_pcm_frames_f32(reader->flac.get(), count / channels, f32buf);
+ uint64_t readsamples = channels * readframes;
+ // f32->f64
+ for (uint64_t i = readsamples; i-- > 0; )
+ samples[i] = f32buf[i];
+ samples += readsamples;
+ count -= readsamples;
+ readtotal += readsamples;
+ }
+
+ if (count == 0)
+ return readtotal;
+ else if (drflac_read_pcm_frames_f32(reader->flac.get(), 1, reader->buff.get()) == 1) {
+ reader->nbuff = channels;
+ uint64_t copied = ysfx_flac_unload_buffer(reader_, samples, count);
+ samples += copied;
+ count -= copied;
+ readtotal += copied;
+ }
+
+ return readtotal;
+}
+
+const ysfx_audio_format_t ysfx_audio_format_flac = {
+ &ysfx_flac_can_handle,
+ &ysfx_flac_open,
+ &ysfx_flac_close,
+ &ysfx_flac_info,
+ &ysfx_flac_avail,
+ &ysfx_flac_rewind,
+ &ysfx_flac_read,
+};
diff --git a/source/modules/ysfx/sources/ysfx_audio_flac.hpp b/source/modules/ysfx/sources/ysfx_audio_flac.hpp
new file mode 100644
index 000000000..c886ebe92
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx_audio_flac.hpp
@@ -0,0 +1,21 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#pragma once
+#include "ysfx.h"
+
+extern const ysfx_audio_format_t ysfx_audio_format_flac;
diff --git a/source/modules/ysfx/sources/ysfx_audio_wav.cpp b/source/modules/ysfx/sources/ysfx_audio_wav.cpp
new file mode 100644
index 000000000..a88756858
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx_audio_wav.cpp
@@ -0,0 +1,162 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#include "ysfx_audio_wav.hpp"
+#include "ysfx_utils.hpp"
+#include
+#include
+
+#if defined(__GNUC__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wunused-function"
+#endif
+
+#define DR_WAV_IMPLEMENTATION
+#define DRWAV_API static
+#define DRWAV_PRIVATE static
+#include "dr_wav.h"
+
+#if defined(__GNUC__)
+# pragma GCC diagnostic pop
+#endif
+
+struct ysfx_wav_reader_t {
+ ~ysfx_wav_reader_t() { drwav_uninit(wav.get()); }
+ std::unique_ptr wav;
+ uint32_t nbuff = 0;
+ std::unique_ptr buff;
+};
+
+static bool ysfx_wav_can_handle(const char *path)
+{
+ return ysfx::path_has_suffix(path, "wav");
+}
+
+static ysfx_audio_reader_t *ysfx_wav_open(const char *path)
+{
+ std::unique_ptr wav{new drwav};
+#if !defined(_WIN32)
+ drwav_bool32 initok = drwav_init_file(wav.get(), path, nullptr);
+#else
+ drwav_bool32 initok = drwav_init_file_w(wav.get(), ysfx::widen(path).c_str(), nullptr);
+#endif
+ if (!initok)
+ return nullptr;
+ std::unique_ptr reader{new ysfx_wav_reader_t};
+ reader->wav = std::move(wav);
+ reader->buff.reset(new float[reader->wav->channels]);
+ return (ysfx_audio_reader_t *)reader.release();
+}
+
+static void ysfx_wav_close(ysfx_audio_reader_t *reader_)
+{
+ ysfx_wav_reader_t *reader = (ysfx_wav_reader_t *)reader_;
+ delete reader;
+}
+
+static ysfx_audio_file_info_t ysfx_wav_info(ysfx_audio_reader_t *reader_)
+{
+ ysfx_wav_reader_t *reader = (ysfx_wav_reader_t *)reader_;
+ ysfx_audio_file_info_t info;
+ info.channels = reader->wav->channels;
+ info.sample_rate = (ysfx_real)reader->wav->sampleRate;
+ return info;
+}
+
+static uint64_t ysfx_wav_avail(ysfx_audio_reader_t *reader_)
+{
+ ysfx_wav_reader_t *reader = (ysfx_wav_reader_t *)reader_;
+ return reader->nbuff + reader->wav->channels * (reader->wav->totalPCMFrameCount - reader->wav->readCursorInPCMFrames);
+}
+
+static void ysfx_wav_rewind(ysfx_audio_reader_t *reader_)
+{
+ ysfx_wav_reader_t *reader = (ysfx_wav_reader_t *)reader_;
+ drwav_seek_to_pcm_frame(reader->wav.get(), 0);
+ reader->nbuff = 0;
+}
+
+static uint64_t ysfx_wav_unload_buffer(ysfx_audio_reader_t *reader_, ysfx_real *samples, uint64_t count)
+{
+ ysfx_wav_reader_t *reader = (ysfx_wav_reader_t *)reader_;
+
+ uint32_t nbuff = reader->nbuff;
+ if (nbuff > count)
+ nbuff = (uint32_t)count;
+
+ if (nbuff == 0)
+ return 0;
+
+ const float *src = &reader->buff[reader->wav->channels - reader->nbuff];
+ for (uint32_t i = 0; i < nbuff; ++i)
+ samples[i] = src[i];
+
+ reader->nbuff -= nbuff;
+ return nbuff;
+}
+
+static uint64_t ysfx_wav_read(ysfx_audio_reader_t *reader_, ysfx_real *samples, uint64_t count)
+{
+ ysfx_wav_reader_t *reader = (ysfx_wav_reader_t *)reader_;
+ uint32_t channels = reader->wav->channels;
+ uint64_t readtotal = 0;
+
+ if (count == 0)
+ return readtotal;
+ else {
+ uint64_t copied = ysfx_wav_unload_buffer(reader_, samples, count);
+ samples += copied;
+ count -= copied;
+ readtotal += copied;
+ }
+
+ if (count == 0)
+ return readtotal;
+ else {
+ float *f32buf = (float *)samples;
+ uint64_t readframes = drwav_read_pcm_frames_f32(reader->wav.get(), count / channels, f32buf);
+ uint64_t readsamples = channels * readframes;
+ // f32->f64
+ for (uint64_t i = readsamples; i-- > 0; )
+ samples[i] = f32buf[i];
+ samples += readsamples;
+ count -= readsamples;
+ readtotal += readsamples;
+ }
+
+ if (count == 0)
+ return readtotal;
+ else if (drwav_read_pcm_frames_f32(reader->wav.get(), 1, reader->buff.get()) == 1) {
+ reader->nbuff = channels;
+ uint64_t copied = ysfx_wav_unload_buffer(reader_, samples, count);
+ samples += copied;
+ count -= copied;
+ readtotal += copied;
+ }
+
+ return readtotal;
+}
+
+const ysfx_audio_format_t ysfx_audio_format_wav = {
+ &ysfx_wav_can_handle,
+ &ysfx_wav_open,
+ &ysfx_wav_close,
+ &ysfx_wav_info,
+ &ysfx_wav_avail,
+ &ysfx_wav_rewind,
+ &ysfx_wav_read,
+};
diff --git a/source/modules/ysfx/sources/ysfx_audio_wav.hpp b/source/modules/ysfx/sources/ysfx_audio_wav.hpp
new file mode 100644
index 000000000..3873c2b06
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx_audio_wav.hpp
@@ -0,0 +1,21 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#pragma once
+#include "ysfx.h"
+
+extern const ysfx_audio_format_t ysfx_audio_format_wav;
diff --git a/source/modules/ysfx/sources/ysfx_config.cpp b/source/modules/ysfx/sources/ysfx_config.cpp
new file mode 100644
index 000000000..35552de0b
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx_config.cpp
@@ -0,0 +1,159 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#include "ysfx_config.hpp"
+#include "ysfx_utils.hpp"
+#include "ysfx_audio_wav.hpp"
+#include "ysfx_audio_flac.hpp"
+#include
+
+ysfx_config_t *ysfx_config_new()
+{
+ return new ysfx_config_t;
+}
+
+void ysfx_config_free(ysfx_config_t *config)
+{
+ if (!config)
+ return;
+
+ if (config->ref_count.fetch_sub(1, std::memory_order_acq_rel) == 1)
+ delete config;
+}
+
+void ysfx_config_add_ref(ysfx_config_t *config)
+{
+ config->ref_count.fetch_add(1, std::memory_order_relaxed);
+}
+
+void ysfx_set_import_root(ysfx_config_t *config, const char *root)
+{
+ config->import_root = ysfx::path_ensure_final_separator(root ? root : "");
+}
+
+void ysfx_set_data_root(ysfx_config_t *config, const char *root)
+{
+ config->data_root = ysfx::path_ensure_final_separator(root ? root : "");
+}
+
+const char *ysfx_get_import_root(ysfx_config_t *config)
+{
+ return config->import_root.c_str();
+}
+
+const char *ysfx_get_data_root(ysfx_config_t *config)
+{
+ return config->data_root.c_str();
+}
+
+void ysfx_guess_file_roots(ysfx_config_t *config, const char *sourcepath)
+{
+ if (config->import_root.empty()) {
+ bool stop = false;
+ const std::string sourcedir = ysfx::path_directory(sourcepath);
+
+ std::string cur_dir = sourcedir + "../";
+ ysfx::file_uid cur_uid;
+ if (!ysfx::get_file_uid(cur_dir.c_str(), cur_uid))
+ stop = true;
+
+ while (!stop) {
+ bool match =
+ ysfx::exists((cur_dir + "Effects/").c_str()) &&
+ ysfx::exists((cur_dir + "Data/").c_str());
+ if (match) {
+ stop = true;
+ config->import_root = cur_dir + "Effects/";
+ }
+ else {
+ cur_dir += "../";
+ ysfx::file_uid old_uid = cur_uid;
+ if (!ysfx::get_file_uid(cur_dir.c_str(), cur_uid) || old_uid == cur_uid)
+ stop = true;
+ }
+ }
+ }
+
+ if (config->data_root.empty() && !config->import_root.empty()) {
+ const std::string datadir = config->import_root + "../Data/";
+
+ bool match = ysfx::exists(datadir.c_str());
+ if (match)
+ config->data_root = datadir;
+ }
+}
+
+void ysfx_register_audio_format(ysfx_config_t *config, ysfx_audio_format_t *afmt)
+{
+ config->audio_formats.push_back(*afmt);
+}
+
+void ysfx_register_builtin_audio_formats(ysfx_config_t *config)
+{
+ config->audio_formats.push_back(ysfx_audio_format_wav);
+ config->audio_formats.push_back(ysfx_audio_format_flac);
+}
+
+void ysfx_set_log_reporter(ysfx_config_t *config, ysfx_log_reporter_t *reporter)
+{
+ config->log_reporter = reporter;
+}
+
+void ysfx_set_user_data(ysfx_config_t *config, intptr_t userdata)
+{
+ config->userdata = userdata;
+}
+
+//------------------------------------------------------------------------------
+const char *ysfx_log_level_string(ysfx_log_level level)
+{
+ switch (level) {
+ case ysfx_log_info:
+ return "info";
+ case ysfx_log_warning:
+ return "warning";
+ case ysfx_log_error:
+ return "error";
+ default:
+ assert(false);
+ return "?";
+ }
+}
+
+void ysfx_log(ysfx_config_t &conf, ysfx_log_level level, const char *message)
+{
+ if (conf.log_reporter)
+ conf.log_reporter(conf.userdata, level, message);
+ else
+ fprintf(stderr, "[ysfx] %s: %s\n", ysfx_log_level_string(level), message);
+}
+
+void ysfx_logfv(ysfx_config_t &conf, ysfx_log_level level, const char *format, va_list ap)
+{
+ char buf[256];
+ vsnprintf(buf, sizeof(buf), format, ap);
+ buf[sizeof(buf)-1] = '\0';
+ ysfx_log(conf, level, buf);
+}
+
+void ysfx_logf(ysfx_config_t &conf, ysfx_log_level level, const char *format, ...)
+{
+ va_list ap;
+ va_start(ap, format);
+ ysfx_logfv(conf, level, format, ap);
+ va_end(ap);
+}
diff --git a/source/modules/ysfx/sources/ysfx_config.hpp b/source/modules/ysfx/sources/ysfx_config.hpp
new file mode 100644
index 000000000..40b84562b
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx_config.hpp
@@ -0,0 +1,39 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#pragma once
+#include "ysfx.h"
+#include
+#include
+#include
+#include
+
+struct ysfx_config_s {
+ std::string import_root;
+ std::string data_root;
+ std::vector audio_formats;
+ ysfx_log_reporter_t *log_reporter = nullptr;
+ intptr_t userdata = 0;
+ std::atomic ref_count{1};
+};
+
+void ysfx_log(ysfx_config_t &conf, ysfx_log_level level, const char *message);
+void ysfx_logfv(ysfx_config_t &conf, ysfx_log_level level, const char *format, va_list ap);
+#if defined(__GNUC__)
+__attribute__((format(printf, 3, 4)))
+#endif
+void ysfx_logf(ysfx_config_t &conf, ysfx_log_level level, const char *format, ...);
diff --git a/source/modules/ysfx/sources/ysfx_eel_utils.cpp b/source/modules/ysfx/sources/ysfx_eel_utils.cpp
new file mode 100644
index 000000000..a9a247b29
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx_eel_utils.cpp
@@ -0,0 +1,66 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#include "ysfx_eel_utils.hpp"
+
+ysfx_eel_ram_reader::ysfx_eel_ram_reader(NSEEL_VMCTX vm, int64_t addr)
+ : m_vm(vm),
+ m_addr(addr)
+{
+}
+
+EEL_F ysfx_eel_ram_reader::read_next()
+{
+ if (m_block_avail == 0) {
+ m_block = (m_addr < 0 || m_addr > 0xFFFFFFFFu) ? nullptr :
+ NSEEL_VM_getramptr_noalloc(m_vm, (uint32_t)m_addr, (int32_t *)&m_block_avail);
+ if (m_block)
+ m_addr += m_block_avail;
+ else {
+ m_addr += 1;
+ m_block_avail = 1;
+ }
+ }
+ EEL_F value = m_block ? *m_block++ : 0;
+ m_block_avail -= 1;
+ return value;
+}
+
+//------------------------------------------------------------------------------
+ysfx_eel_ram_writer::ysfx_eel_ram_writer(NSEEL_VMCTX vm, int64_t addr)
+ : m_vm(vm),
+ m_addr(addr)
+{
+}
+
+bool ysfx_eel_ram_writer::write_next(EEL_F value)
+{
+ if (m_block_avail == 0) {
+ m_block = (m_addr < 0 || m_addr > 0xFFFFFFFFu) ? nullptr :
+ NSEEL_VM_getramptr(m_vm, (uint32_t)m_addr, (int32_t *)&m_block_avail);
+ if (m_block)
+ m_addr += m_block_avail;
+ else {
+ m_addr += 1;
+ m_block_avail = 1;
+ }
+ }
+ if (m_block)
+ *m_block++ = value;
+ m_block_avail -= 1;
+ return true;
+}
diff --git a/source/modules/ysfx/sources/ysfx_eel_utils.hpp b/source/modules/ysfx/sources/ysfx_eel_utils.hpp
new file mode 100644
index 000000000..3933decc2
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx_eel_utils.hpp
@@ -0,0 +1,57 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#pragma once
+#include "WDL/eel2/ns-eel.h"
+#include "WDL/eel2/ns-eel-int.h"
+#include
+#include
+
+template
+inline typename std::enable_if::value, T>::type
+ysfx_eel_round(EEL_F value)
+{
+ return (T)(value + (EEL_F)0.0001); // same one as used in eel2 everywhere
+}
+
+//------------------------------------------------------------------------------
+class ysfx_eel_ram_reader {
+public:
+ ysfx_eel_ram_reader() = default;
+ ysfx_eel_ram_reader(NSEEL_VMCTX vm, int64_t addr);
+ EEL_F read_next();
+
+private:
+ NSEEL_VMCTX m_vm{};
+ int64_t m_addr = 0;
+ const EEL_F *m_block = nullptr;
+ uint32_t m_block_avail = 0;
+};
+
+//------------------------------------------------------------------------------
+class ysfx_eel_ram_writer {
+public:
+ ysfx_eel_ram_writer() = default;
+ ysfx_eel_ram_writer(NSEEL_VMCTX vm, int64_t addr);
+ bool write_next(EEL_F value);
+
+private:
+ NSEEL_VMCTX m_vm{};
+ int64_t m_addr = 0;
+ EEL_F *m_block = nullptr;
+ uint32_t m_block_avail = 0;
+};
diff --git a/source/modules/ysfx/sources/ysfx_midi.cpp b/source/modules/ysfx/sources/ysfx_midi.cpp
new file mode 100644
index 000000000..2e13faf1b
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx_midi.cpp
@@ -0,0 +1,214 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#include "ysfx_midi.hpp"
+#include
+#include
+
+void ysfx_midi_reserve(ysfx_midi_buffer_t *midi, uint32_t capacity, bool extensible)
+{
+ std::vector data;
+ data.reserve(capacity);
+ std::swap(data, midi->data);
+ midi->extensible = extensible;
+ ysfx_midi_rewind(midi);
+}
+
+void ysfx_midi_clear(ysfx_midi_buffer_t *midi)
+{
+ midi->data.clear();
+ ysfx_midi_rewind(midi);
+}
+
+bool ysfx_midi_push(ysfx_midi_buffer_t *midi, const ysfx_midi_event_t *event)
+{
+ if (event->size > ysfx_midi_message_max_size)
+ return false;
+ if (event->bus >= ysfx_max_midi_buses)
+ return false;
+
+ ysfx_midi_header_t header;
+
+ if (!midi->extensible) {
+ size_t writable = midi->data.capacity() - midi->data.size();
+ if (writable < sizeof(header) + event->size)
+ return false;
+ }
+
+ const uint8_t *data = event->data;
+ const uint8_t *headp = (const uint8_t *)&header;
+ header.bus = event->bus;
+ header.offset = event->offset;
+ header.size = event->size;
+
+ midi->data.insert(midi->data.end(), headp, headp + sizeof(header));
+ midi->data.insert(midi->data.end(), data, data + header.size);
+ return true;
+}
+
+void ysfx_midi_rewind(ysfx_midi_buffer_t *midi)
+{
+ midi->read_pos = 0;
+ for (uint32_t i = 0; i < ysfx_max_midi_buses; ++i)
+ midi->read_pos_for_bus[i] = 0;
+}
+
+bool ysfx_midi_get_next(ysfx_midi_buffer_t *midi, ysfx_midi_event_t *event)
+{
+ size_t *pos_ptr = &midi->read_pos;
+ size_t pos = *pos_ptr;
+
+ size_t avail = midi->data.size() - pos;
+ ysfx_midi_header_t header;
+ if (avail == 0)
+ return false;
+
+ assert(avail >= sizeof(header));
+ memcpy(&header, &midi->data[pos], sizeof(header));
+ assert(avail >= sizeof(header) + header.size);
+
+ event->bus = header.bus;
+ event->offset = header.offset;
+ event->size = header.size;
+ event->data = &midi->data[pos + sizeof(header)];
+ *pos_ptr = pos + (sizeof(header) + header.size);
+ return true;
+}
+
+bool ysfx_midi_get_next_from_bus(ysfx_midi_buffer_t *midi, uint32_t bus, ysfx_midi_event_t *event)
+{
+ if (bus >= ysfx_max_midi_buses)
+ return false;
+
+ size_t *pos_ptr = &midi->read_pos_for_bus[bus];
+ size_t pos = *pos_ptr;
+ size_t avail = midi->data.size() - pos;
+ ysfx_midi_header_t header;
+
+ bool found = false;
+ while (!found && avail > 0) {
+ assert(avail >= sizeof(header));
+ memcpy(&header, &midi->data[pos], sizeof(header));
+ assert(avail >= sizeof(header) + header.size);
+
+ found = header.bus == bus;
+ if (!found) {
+ pos += sizeof(header) + header.size;
+ avail -= sizeof(header) + header.size;
+ }
+ }
+
+ if (!found) {
+ *pos_ptr = pos;
+ return false;
+ }
+
+ event->bus = header.bus;
+ event->offset = header.offset;
+ event->size = header.size;
+ event->data = &midi->data[pos + sizeof(header)];
+ *pos_ptr = pos + (sizeof(header) + header.size);
+ return true;
+}
+
+bool ysfx_midi_push_begin(ysfx_midi_buffer_t *midi, uint32_t bus, uint32_t offset, ysfx_midi_push_t *mp)
+{
+ ysfx_midi_header_t header;
+
+ mp->midi = midi;
+ mp->start = midi->data.size();
+ mp->count = 0;
+ mp->eob = false;
+
+ if (!midi->extensible) {
+ size_t writable = midi->data.capacity() - midi->data.size();
+ if (writable < sizeof(header)) {
+ mp->eob = true;
+ return false;
+ }
+ }
+
+ header.bus = bus;
+ header.offset = offset;
+ header.size = 0;
+
+ const uint8_t *headp = (const uint8_t *)&header;
+ midi->data.insert(midi->data.end(), headp, headp + sizeof(header));
+
+ return true;
+}
+
+bool ysfx_midi_push_data(ysfx_midi_push_t *mp, const uint8_t *data, uint32_t size)
+{
+ if (mp->eob)
+ return false;
+
+ if (size > ysfx_midi_message_max_size || mp->count + size > ysfx_midi_message_max_size) {
+ mp->eob = true;
+ return false;
+ }
+
+ ysfx_midi_buffer_t *midi = mp->midi;
+
+ if (!midi->extensible) {
+ size_t writable = midi->data.capacity() - midi->data.size();
+ if (writable < size) {
+ mp->eob = true;
+ return false;
+ }
+ }
+
+ midi->data.insert(midi->data.end(), data, data + size);
+ mp->count += size;
+ return true;
+}
+
+bool ysfx_midi_push_end(ysfx_midi_push_t *mp)
+{
+ if (mp->eob) {
+ mp->midi->data.resize(mp->start);
+ return false;
+ }
+
+ ysfx_midi_header_t header;
+ uint8_t *headp = &mp->midi->data[mp->start];
+ memcpy(&header, headp, sizeof(header));
+ header.size = mp->count;
+ memcpy(headp, &header, sizeof(header));
+ return true;
+}
+
+//------------------------------------------------------------------------------
+uint32_t ysfx_midi_sizeof(uint8_t id)
+{
+ if ((id >> 7) == 0) {
+ return 0;
+ }
+ else if ((id >> 4) != 0b1111) {
+ static const uint8_t sizetable[8] = {
+ 3, 3, 3, 3, 2, 2, 3,
+ };
+ return sizetable[(id >> 4) & 0b111];
+ }
+ else {
+ static const uint8_t sizetable[16] = {
+ 0, 2, 3, 2, 1, 1, 1, 0,
+ 1, 1, 1, 1, 1, 1, 1, 1,
+ };
+ return sizetable[id & 0b1111];
+ }
+}
diff --git a/source/modules/ysfx/sources/ysfx_midi.hpp b/source/modules/ysfx/sources/ysfx_midi.hpp
new file mode 100644
index 000000000..a36c21c46
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx_midi.hpp
@@ -0,0 +1,72 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#pragma once
+#include "ysfx.h"
+#include
+#include
+
+struct ysfx_midi_header_t {
+ uint32_t bus;
+ uint32_t offset;
+ uint32_t size;
+};
+
+struct ysfx_midi_buffer_t {
+ std::vector data;
+ size_t read_pos = 0;
+ size_t read_pos_for_bus[ysfx_max_midi_buses] = {};
+ bool extensible = false;
+};
+using ysfx_midi_buffer_u = std::unique_ptr;
+
+enum {
+ ysfx_midi_message_max_size = 1 << 24,
+};
+
+// NOTE: regarding buses,
+// The buffer keeps 2 kinds of read positions: global, and per-bus.
+//
+// These are tracked separately, so use either global/per-bus reading API,
+// but not both mixed in the same piece of code.
+//
+// The JSFX API `midi*` implementations should always use per-bus access:
+// if `ext_midi_bus` is true, use the bus defined by `midi_bus`, otherwise 0.
+
+void ysfx_midi_reserve(ysfx_midi_buffer_t *midi, uint32_t capacity, bool extensible);
+void ysfx_midi_clear(ysfx_midi_buffer_t *midi);
+bool ysfx_midi_push(ysfx_midi_buffer_t *midi, const ysfx_midi_event_t *event);
+void ysfx_midi_rewind(ysfx_midi_buffer_t *midi);
+bool ysfx_midi_get_next(ysfx_midi_buffer_t *midi, ysfx_midi_event_t *event);
+bool ysfx_midi_get_next_from_bus(ysfx_midi_buffer_t *midi, uint32_t bus, ysfx_midi_event_t *event);
+
+// incremental writer into a midi buffer
+struct ysfx_midi_push_t {
+ ysfx_midi_buffer_t *midi = nullptr;
+ size_t start = 0;
+ uint32_t count = 0;
+ bool eob = false;
+};
+bool ysfx_midi_push_begin(ysfx_midi_buffer_t *midi, uint32_t bus, uint32_t offset, ysfx_midi_push_t *mp);
+bool ysfx_midi_push_data(ysfx_midi_push_t *mp, const uint8_t *data, uint32_t size);
+bool ysfx_midi_push_end(ysfx_midi_push_t *mp);
+
+//------------------------------------------------------------------------------
+
+// determine the length of a midi message according to its status byte
+// if length is dynamic, returns 0
+uint32_t ysfx_midi_sizeof(uint8_t id);
diff --git a/source/modules/ysfx/sources/ysfx_parse.cpp b/source/modules/ysfx/sources/ysfx_parse.cpp
new file mode 100644
index 000000000..8e9f93f35
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx_parse.cpp
@@ -0,0 +1,380 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#include "ysfx_parse.hpp"
+#include "ysfx_utils.hpp"
+#include
+#include
+
+bool ysfx_parse_toplevel(ysfx::text_reader &reader, ysfx_toplevel_t &toplevel, ysfx_parse_error *error)
+{
+ toplevel = ysfx_toplevel_t{};
+
+ ysfx_section_t *current = new ysfx_section_t;
+ toplevel.header.reset(current);
+
+ std::string line;
+ uint32_t lineno = 0;
+
+ line.reserve(256);
+
+ while (reader.read_next_line(line)) {
+ const char *linep = line.c_str();
+
+ if (linep[0] == '@') {
+ // a new section starts
+ ysfx::string_list tokens = ysfx::split_strings_noempty(linep, &ysfx::ascii_isspace);
+
+ current = new ysfx_section_t;
+
+ if (tokens[0] == "@init")
+ toplevel.init.reset(current);
+ else if (tokens[0] == "@slider")
+ toplevel.slider.reset(current);
+ else if (tokens[0] == "@block")
+ toplevel.block.reset(current);
+ else if (tokens[0] == "@sample")
+ toplevel.sample.reset(current);
+ else if (tokens[0] == "@serialize")
+ toplevel.serialize.reset(current);
+ else if (tokens[0] == "@gfx") {
+ toplevel.gfx.reset(current);
+ long gfx_w = 0;
+ long gfx_h = 0;
+ if (tokens.size() > 1)
+ gfx_w = (long)ysfx::dot_atof(tokens[1].c_str());
+ if (tokens.size() > 2)
+ gfx_h = (long)ysfx::dot_atof(tokens[2].c_str());
+ toplevel.gfx_w = (gfx_w > 0) ? (uint32_t)gfx_w : 0;
+ toplevel.gfx_h = (gfx_h > 0) ? (uint32_t)gfx_h : 0;
+ }
+ else {
+ delete current;
+ if (error) {
+ error->line = lineno;
+ error->message = std::string("Invalid section: ") + line;
+ }
+ return false;
+ }
+ current->line_offset = lineno + 1;
+ }
+ else {
+ current->text.append(line);
+ current->text.push_back('\n');
+ }
+
+ ++lineno;
+ }
+
+ return true;
+}
+
+void ysfx_parse_header(ysfx_section_t *section, ysfx_header_t &header)
+{
+ header = ysfx_header_t{};
+
+ ysfx::string_text_reader reader(section->text.c_str());
+
+ std::string line;
+ //uint32_t lineno = section->line_offset;
+
+ line.reserve(256);
+
+ ///
+ auto unprefix = [](const char *text, const char **restp, const char *prefix) -> bool {
+ size_t len = strlen(prefix);
+ if (strncmp(text, prefix, len))
+ return false;
+ if (restp)
+ *restp = text + len;
+ return true;
+ };
+
+ //--------------------------------------------------------------------------
+ // pass 1: regular metadata
+
+ while (reader.read_next_line(line)) {
+ const char *linep = line.c_str();
+ const char *rest = nullptr;
+
+ ysfx_slider_t slider;
+ ysfx_parsed_filename_t filename;
+
+ ///
+ if (unprefix(linep, &rest, "desc:")) {
+ if (header.desc.empty())
+ header.desc = ysfx::trim(rest, &ysfx::ascii_isspace);
+ }
+ else if (unprefix(linep, &rest, "author:")) {
+ if (header.author.empty())
+ header.author = ysfx::trim(rest, &ysfx::ascii_isspace);
+ }
+ else if (unprefix(linep, &rest, "tags:")) {
+ if (header.tags.empty()) {
+ for (const std::string &tag : ysfx::split_strings_noempty(rest, &ysfx::ascii_isspace))
+ header.tags.push_back(tag);
+ }
+ }
+ else if (unprefix(linep, &rest, "in_pin:")) {
+ header.explicit_pins = true;
+ header.in_pins.push_back(ysfx::trim(rest, &ysfx::ascii_isspace));
+ }
+ else if (unprefix(linep, &rest, "out_pin:")) {
+ header.explicit_pins = true;
+ header.out_pins.push_back(ysfx::trim(rest, &ysfx::ascii_isspace));
+ }
+ else if (unprefix(linep, &rest, "options:")) {
+ for (const std::string &opt : ysfx::split_strings_noempty(rest, &ysfx::ascii_isspace)) {
+ size_t pos = opt.find('=');
+ std::string name = (pos == opt.npos) ? opt : opt.substr(0, pos);
+ std::string value = (pos == opt.npos) ? std::string{} : opt.substr(pos + 1);
+ if (name == "gmem")
+ header.options.gmem = value;
+ else if (name == "maxmem") {
+ int32_t maxmem = (int32_t)ysfx::dot_atof(value.c_str());
+ header.options.maxmem = (maxmem < 0) ? 0 : (uint32_t)maxmem;
+ }
+ else if (name == "want_all_kb")
+ header.options.want_all_kb = true;
+ else if (name == "no_meter")
+ header.options.no_meter = true;
+ }
+ }
+ else if (unprefix(linep, &rest, "import") && ysfx::ascii_isspace(rest[0]))
+ header.imports.push_back(ysfx::trim(rest + 1, &ysfx::ascii_isspace));
+ else if (ysfx_parse_slider(linep, slider)) {
+ if (slider.id >= ysfx_max_sliders)
+ continue;
+ slider.exists = true;
+ header.sliders[slider.id] = slider;
+ }
+ else if (ysfx_parse_filename(linep, filename)) {
+ if (filename.index != header.filenames.size())
+ continue;
+ header.filenames.push_back(std::move(filename.filename));
+ }
+
+ //++lineno;
+ }
+
+ //--------------------------------------------------------------------------
+ // pass 2: comments
+
+ reader = ysfx::string_text_reader{section->text.c_str()};
+
+ while (reader.read_next_line(line)) {
+ const char *linep = line.c_str();
+ const char *rest = nullptr;
+
+ // some files contain metadata in the form of comments
+ // this is not part of spec, but we'll take this info regardless
+
+ if (unprefix(linep, &rest, "//author:")) {
+ if (header.author.empty())
+ header.author = ysfx::trim(rest, &ysfx::ascii_isspace);
+ }
+ else if (unprefix(linep, &rest, "//tags:")) {
+ if (header.tags.empty()) {
+ for (const std::string &tag : ysfx::split_strings_noempty(rest, &ysfx::ascii_isspace))
+ header.tags.push_back(tag);
+ }
+ }
+ }
+
+ //--------------------------------------------------------------------------
+ if (header.in_pins.size() == 1 && !ysfx::ascii_casecmp(header.in_pins.front().c_str(), "none"))
+ header.in_pins.clear();
+ if (header.out_pins.size() == 1 && !ysfx::ascii_casecmp(header.out_pins.front().c_str(), "none"))
+ header.out_pins.clear();
+
+ if (header.in_pins.size() > ysfx_max_channels)
+ header.in_pins.resize(ysfx_max_channels);
+ if (header.out_pins.size() > ysfx_max_channels)
+ header.out_pins.resize(ysfx_max_channels);
+}
+
+bool ysfx_parse_slider(const char *line, ysfx_slider_t &slider)
+{
+ // NOTE this parser is intentionally very permissive,
+ // in order to match the reference behavior
+
+ slider = ysfx_slider_t{};
+
+ #define PARSE_FAIL do { \
+ /*fprintf(stderr, "parse error (line %d): `%s`\n", __LINE__, line);*/ \
+ return false; \
+ } while (0)
+
+ const char *cur = line;
+
+ for (const char *p = "slider"; *p; ++p) {
+ if (*cur++ != *p)
+ PARSE_FAIL;
+ }
+
+ // parse ID (1-index)
+ unsigned long id = strtoul(cur, (char **)&cur, 10);
+ if (id < 1 || id > ysfx_max_sliders)
+ PARSE_FAIL;
+ slider.id = (uint32_t)--id;
+
+ // semicolon
+ if (*cur++ != ':')
+ PARSE_FAIL;
+
+ // search if there is an '=' sign prior to any '<' or ','
+ // if there is, it's a custom variable
+ {
+ const char *pos = cur;
+ for (char c; pos && (c = *pos) && c != '='; )
+ pos = (c == '<' || c == ',') ? nullptr : (pos + 1);
+ if (pos && *pos) {
+ slider.var.assign(cur, pos);
+ cur = pos + 1;
+ }
+ else
+ slider.var = "slider" + std::to_string(id + 1);
+ }
+
+ // a regular slider
+ if (*cur != '/') {
+ slider.def = (ysfx_real)ysfx::dot_strtod(cur, (char **)&cur);
+
+ while (*cur && *cur != ',' && *cur != '<') ++cur;
+ if (!*cur) PARSE_FAIL;
+
+ // no range specification
+ if (*cur == ',')
+ ++cur;
+ // range specification
+ else if (*cur == '<') {
+ ++cur;
+
+ // min
+ slider.min = (ysfx_real)ysfx::dot_strtod(cur, (char **)&cur);
+
+ while (*cur && *cur != ',' && *cur != '>') ++cur;
+ if (!*cur) PARSE_FAIL;
+
+ // max
+ if (*cur == ',') {
+ ++cur;
+ slider.max = (ysfx_real)ysfx::dot_strtod(cur, (char **)&cur);
+
+ while (*cur && *cur != ',' && *cur != '>') ++cur;
+ if (!*cur) PARSE_FAIL;
+ }
+
+ // inc
+ if (*cur == ',') {
+ ++cur;
+ slider.inc = (ysfx_real)ysfx::dot_strtod(cur, (char **)&cur);
+
+ while (*cur && *cur != '{' && *cur != ',' && *cur != '>') ++cur;
+ if (!*cur) PARSE_FAIL;
+
+ // enumeration values
+ if (*cur == '{') {
+ const char *pos = ++cur;
+
+ while (*cur && *cur != '}' && *cur != '>') ++cur;
+ if (!*cur) PARSE_FAIL;
+
+ slider.is_enum = true;
+ slider.enum_names = ysfx::split_strings_noempty(
+ std::string(pos, cur).c_str(),
+ [](char c) -> bool { return c == ','; });
+ for (std::string &name : slider.enum_names)
+ name = ysfx::trim(name.c_str(), &ysfx::ascii_isspace);
+ }
+ }
+
+ while (*cur && *cur != '>') ++cur;
+ if (!*cur) PARSE_FAIL;
+
+ ++cur;
+ }
+ else
+ PARSE_FAIL;
+
+ // NOTE: skip ',' and whitespace. not sure why, it's how it is
+ while (*cur && (*cur == ',' || ysfx::ascii_isspace(*cur))) ++cur;
+ if (!*cur) PARSE_FAIL;
+ }
+ // a path slider
+ else
+ {
+ const char *pos = cur;
+ while (*cur && *cur != ':') ++cur;
+ if (!*cur) PARSE_FAIL;
+
+ slider.path.assign(pos, cur);
+ ++cur;
+ slider.def = (ysfx_real)ysfx::dot_strtod(cur, (char **)&cur);
+ slider.inc = 1;
+ slider.is_enum = true;
+
+ while (*cur && *cur != ':') ++cur;
+ if (!*cur) PARSE_FAIL;
+
+ ++cur;
+ }
+
+ // description and visibility
+ while (ysfx::ascii_isspace(*cur))
+ ++cur;
+
+ slider.initially_visible = true;
+ if (*cur == '-') {
+ ++cur;
+ slider.initially_visible = false;
+ }
+
+ slider.desc = ysfx::trim(cur, &ysfx::ascii_isspace);
+ if (slider.desc.empty())
+ PARSE_FAIL;
+
+ //
+ #undef PARSE_FAIL
+
+ return true;
+}
+
+bool ysfx_parse_filename(const char *line, ysfx_parsed_filename_t &filename)
+{
+ filename = ysfx_parsed_filename_t{};
+
+ const char *cur = line;
+
+ for (const char *p = "filename:"; *p; ++p) {
+ if (*cur++ != *p)
+ return false;
+ }
+
+ int64_t index = (int64_t)ysfx::dot_strtod(cur, (char **)&cur);
+ if (index < 0 || index > ~(uint32_t)0)
+ return false;
+
+ while (*cur && *cur != ',') ++cur;
+ if (!*cur) return false;;
+
+ ++cur;
+
+ filename.index = (uint32_t)index;
+ filename.filename.assign(cur);
+ return true;
+}
diff --git a/source/modules/ysfx/sources/ysfx_parse.hpp b/source/modules/ysfx/sources/ysfx_parse.hpp
new file mode 100644
index 000000000..cfbae62ac
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx_parse.hpp
@@ -0,0 +1,101 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#pragma once
+#include "ysfx.h"
+#include "ysfx_reader.hpp"
+#include "ysfx_utils.hpp"
+#include
+#include
+#include
+
+struct ysfx_section_t;
+struct ysfx_toplevel_t;
+struct ysfx_slider_t;
+struct ysfx_header_t;
+using ysfx_section_u = std::unique_ptr;
+using ysfx_toplevel_u = std::unique_ptr;
+using ysfx_slider_u = std::unique_ptr;
+using ysfx_header_u = std::unique_ptr;
+
+struct ysfx_section_t {
+ uint32_t line_offset = 0;
+ std::string text;
+};
+
+struct ysfx_toplevel_t {
+ ysfx_section_u header;
+ ysfx_section_u init;
+ ysfx_section_u slider;
+ ysfx_section_u block;
+ ysfx_section_u sample;
+ ysfx_section_u serialize;
+ ysfx_section_u gfx;
+ uint32_t gfx_w = 0;
+ uint32_t gfx_h = 0;
+};
+
+struct ysfx_parse_error {
+ uint32_t line = 0;
+ std::string message;
+ explicit operator bool() { return !message.empty(); }
+};
+
+struct ysfx_slider_t {
+ uint32_t id = 0;
+ bool exists = false;
+ ysfx_real def = 0;
+ ysfx_real min = 0;
+ ysfx_real max = 0;
+ ysfx_real inc = 0;
+ std::string var;
+ std::string path;
+ bool is_enum = false;
+ ysfx::string_list enum_names;
+ std::string desc;
+ bool initially_visible = false;
+};
+
+struct ysfx_options_t {
+ std::string gmem;
+ uint32_t maxmem = 0;
+ bool want_all_kb = false;
+ bool no_meter = false;
+};
+
+struct ysfx_header_t {
+ std::string desc;
+ std::string author;
+ ysfx::string_list tags;
+ ysfx::string_list imports;
+ ysfx::string_list in_pins;
+ ysfx::string_list out_pins;
+ bool explicit_pins = false;
+ ysfx::string_list filenames;
+ ysfx_options_t options;
+ ysfx_slider_t sliders[ysfx_max_sliders];
+};
+
+struct ysfx_parsed_filename_t {
+ uint32_t index;
+ std::string filename;
+};
+
+bool ysfx_parse_toplevel(ysfx::text_reader &reader, ysfx_toplevel_t &toplevel, ysfx_parse_error *error);
+bool ysfx_parse_slider(const char *line, ysfx_slider_t &slider);
+bool ysfx_parse_filename(const char *line, ysfx_parsed_filename_t &filename);
+void ysfx_parse_header(ysfx_section_t *section, ysfx_header_t &header);
diff --git a/source/modules/ysfx/sources/ysfx_preset.cpp b/source/modules/ysfx/sources/ysfx_preset.cpp
new file mode 100644
index 000000000..297d71b2d
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx_preset.cpp
@@ -0,0 +1,171 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#include "ysfx.h"
+#include "ysfx_preset.hpp"
+#include "ysfx_utils.hpp"
+#include
+#include
+#include
+
+#include "WDL/lineparse.h"
+
+static void ysfx_preset_clear(ysfx_preset_t *preset);
+static ysfx_bank_t *ysfx_load_bank_from_rpl_text(const std::string &text);
+static void ysfx_parse_preset_from_rpl_blob(ysfx_preset_t *preset, const char *name, const std::vector &data);
+
+ysfx_bank_t *ysfx_load_bank(const char *path)
+{
+ ysfx::FILE_u stream{fopen(path, "rb")};
+ if (!stream)
+ return nullptr;
+
+ std::string input;
+ constexpr uint32_t max_input = 1u << 24;
+ input.reserve(1u << 16);
+
+ for (int ch; input.size() < max_input && (ch = fgetc(stream.get())) != EOF; ) {
+ ch = (ch == '\r' || ch == '\n') ? ' ' : ch;
+ input.push_back((unsigned char)ch);
+ }
+
+ if (ferror(stream.get()))
+ return nullptr;
+
+ stream.reset();
+ return ysfx_load_bank_from_rpl_text(input);
+}
+
+void ysfx_bank_free(ysfx_bank_t *bank)
+{
+ if (!bank)
+ return;
+
+ delete[] bank->name;
+
+ if (ysfx_preset_t *presets = bank->presets) {
+ uint32_t count = bank->preset_count;
+ for (uint32_t i = 0; i < count; ++i)
+ ysfx_preset_clear(&presets[i]);
+ delete[] presets;
+ }
+
+ delete bank;
+}
+
+static void ysfx_preset_clear(ysfx_preset_t *preset)
+{
+ delete[] preset->name;
+ preset->name = nullptr;
+
+ ysfx_state_free(preset->state);
+ preset->state = nullptr;
+}
+
+static ysfx_bank_t *ysfx_load_bank_from_rpl_text(const std::string &text)
+{
+ LineParser parser;
+ if (parser.parse(text.c_str()) < 0)
+ return nullptr;
+
+ ///
+ std::vector preset_list;
+ preset_list.reserve(256);
+
+ auto list_cleanup = ysfx::defer([&preset_list]() {
+ for (ysfx_preset_t &pst : preset_list)
+ ysfx_preset_clear(&pst);
+ });
+
+ ///
+ int ntok = parser.getnumtokens();
+ int itok = 0;
+
+ if (strcmp("", (part = parser.gettoken_str(itok++))) != 0; )
+ base64.append(part);
+
+ preset_list.emplace_back();
+ ysfx_preset_t &preset = preset_list.back();
+
+ ysfx_parse_preset_from_rpl_blob(
+ &preset, preset_name,
+ ysfx::decode_base64(base64.data(), base64.size()));
+ }
+ }
+
+ ///
+ ysfx_bank_u bank{new ysfx_bank_t{}};
+ bank->name = ysfx::strdup_using_new(bank_name);
+ bank->presets = new ysfx_preset_t[(uint32_t)preset_list.size()]{};
+ bank->preset_count = (uint32_t)preset_list.size();
+
+ for (uint32_t i = (uint32_t)preset_list.size(); i-- > 0; ) {
+ bank->presets[i] = preset_list[i];
+ preset_list.pop_back();
+ }
+
+ return bank.release();
+}
+
+static void ysfx_parse_preset_from_rpl_blob(ysfx_preset_t *preset, const char *name, const std::vector &data)
+{
+ ysfx_state_t state{};
+ std::vector sliders;
+
+ size_t len = data.size();
+ size_t pos = 0;
+ while (pos < len && data[pos] != 0) ++pos;
+
+ if (pos++ < len) {
+ state.data = const_cast(&data[pos]);
+ state.data_size = len - pos;
+
+ LineParser parser;
+ if (parser.parse((const char *)data.data()) >= 0) {
+ sliders.reserve(ysfx_max_sliders);
+
+ for (uint32_t i = 0; i < 64; ++i) {
+ int success = false;
+ ysfx_state_slider_t slider{};
+ slider.index = i;
+ slider.value = (ysfx_real)parser.gettoken_float(i, &success);
+ if (success)
+ sliders.push_back(slider);
+ }
+
+ state.sliders = sliders.data();
+ state.slider_count = (uint32_t)sliders.size();
+ }
+ }
+
+ preset->name = ysfx::strdup_using_new(name);
+ preset->state = ysfx_state_dup(&state);
+}
diff --git a/source/modules/ysfx/sources/ysfx_preset.hpp b/source/modules/ysfx/sources/ysfx_preset.hpp
new file mode 100644
index 000000000..8435637ae
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx_preset.hpp
@@ -0,0 +1,18 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#pragma once
diff --git a/source/modules/ysfx/sources/ysfx_reader.cpp b/source/modules/ysfx/sources/ysfx_reader.cpp
new file mode 100644
index 000000000..9b3cb2ad9
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx_reader.cpp
@@ -0,0 +1,98 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#include "ysfx_reader.hpp"
+
+namespace ysfx {
+
+//------------------------------------------------------------------------------
+bool text_reader::read_next_line(std::string &line)
+{
+ line.clear();
+
+ char next = read_next_char();
+ if (next == '\0')
+ return false;
+
+ while (next != '\0' && next != '\r' && next != '\n') {
+ line.push_back(next);
+ next = read_next_char();
+ }
+
+ if (next == '\r') {
+ next = peek_next_char();
+ if (next == '\n')
+ read_next_char();
+ }
+
+ return true;
+}
+
+//------------------------------------------------------------------------------
+char string_text_reader::read_next_char()
+{
+ const char *ptr = m_char_ptr;
+
+ if (!ptr || *ptr == '\0')
+ return '\0';
+
+ char next = *ptr;
+ m_char_ptr = ptr + 1;
+ return next;
+}
+
+char string_text_reader::peek_next_char()
+{
+ const char *ptr = m_char_ptr;
+
+ if (!ptr)
+ return '\0';
+
+ return *ptr;
+}
+
+//------------------------------------------------------------------------------
+char stdio_text_reader::read_next_char()
+{
+ FILE *stream = m_stream;
+
+ if (!stream)
+ return '\0';
+
+ int next = fgetc(stream);
+ if (next == EOF)
+ return '\0';
+
+ return (unsigned char)next;
+}
+
+char stdio_text_reader::peek_next_char()
+{
+ FILE *stream = m_stream;
+
+ if (!stream)
+ return '\0';
+
+ int next = fgetc(stream);
+ if (next == EOF)
+ return '\0';
+
+ ungetc(next, stream);
+ return (unsigned char)next;
+}
+
+} // namespace ysfx
diff --git a/source/modules/ysfx/sources/ysfx_reader.hpp b/source/modules/ysfx/sources/ysfx_reader.hpp
new file mode 100644
index 000000000..a24c1d807
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx_reader.hpp
@@ -0,0 +1,56 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#pragma once
+#include
+#include
+#include
+
+namespace ysfx {
+
+class text_reader
+{
+public:
+ virtual ~text_reader() = default;
+ virtual char read_next_char() = 0;
+ virtual char peek_next_char() = 0;
+ bool read_next_line(std::string &line);
+};
+
+//------------------------------------------------------------------------------
+class string_text_reader : public text_reader
+{
+public:
+ explicit string_text_reader(const char *text) : m_char_ptr(text) {}
+ char read_next_char() override;
+ char peek_next_char() override;
+private:
+ const char *m_char_ptr = nullptr;
+};
+
+//------------------------------------------------------------------------------
+class stdio_text_reader : public text_reader
+{
+public:
+ explicit stdio_text_reader(FILE *stream) : m_stream(stream) {}
+ char read_next_char() override;
+ char peek_next_char() override;
+private:
+ FILE *m_stream = nullptr;
+};
+
+} // namespace ysfx
diff --git a/source/modules/ysfx/sources/ysfx_utils.cpp b/source/modules/ysfx/sources/ysfx_utils.cpp
new file mode 100644
index 000000000..c859072f5
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx_utils.cpp
@@ -0,0 +1,730 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#include "ysfx_utils.hpp"
+#include "base64/Base64.hpp"
+#include
+#include
+#include
+#include
+#include
+#include
+#if !defined(_WIN32)
+# include
+# include
+# include
+# include
+# include
+#else
+# include
+# include
+#endif
+
+namespace ysfx {
+
+#if !defined(_WIN32)
+static_assert(sizeof(off_t) == 8, "64-bit large file support is not enabled");
+#endif
+
+FILE *fopen_utf8(const char *path, const char *mode)
+{
+#if defined(_WIN32)
+ return _wfopen(widen(path).c_str(), widen(mode).c_str());
+#else
+ return fopen(path, mode);
+#endif
+}
+
+int64_t fseek_lfs(FILE *stream, int64_t off, int whence)
+{
+#if defined(_WIN32)
+ return _fseeki64(stream, off, whence);
+#else
+ return fseeko(stream, off, whence);
+#endif
+}
+
+int64_t ftell_lfs(FILE *stream)
+{
+#if defined(_WIN32)
+ return _ftelli64(stream);
+#else
+ return ftello(stream);
+#endif
+}
+
+//------------------------------------------------------------------------------
+
+namespace {
+
+struct scoped_c_locale
+{
+ scoped_c_locale(int lc, const char *name);
+ ~scoped_c_locale();
+ c_locale_t m_loc{};
+
+ scoped_c_locale(const scoped_c_locale &) = delete;
+ scoped_c_locale &operator=(const scoped_c_locale &) = delete;
+};
+
+scoped_c_locale::scoped_c_locale(int lc, const char *name)
+{
+#if defined(_WIN32)
+ m_loc = _create_locale(lc, name);
+#else
+ switch (lc) {
+ case LC_ALL:
+ m_loc = newlocale(LC_ALL_MASK, name, c_locale_t{});
+ break;
+ case LC_CTYPE:
+ m_loc = newlocale(LC_CTYPE_MASK, name, c_locale_t{});
+ break;
+ case LC_COLLATE:
+ m_loc = newlocale(LC_COLLATE_MASK, name, c_locale_t{});
+ break;
+ case LC_MONETARY:
+ m_loc = newlocale(LC_MONETARY_MASK, name, c_locale_t{});
+ break;
+ case LC_NUMERIC:
+ m_loc = newlocale(LC_NUMERIC_MASK, name, c_locale_t{});
+ break;
+ case LC_TIME:
+ m_loc = newlocale(LC_TIME_MASK, name, c_locale_t{});
+ break;
+ case LC_MESSAGES:
+ m_loc = newlocale(LC_MESSAGES_MASK, name, c_locale_t{});
+ break;
+ default:
+ errno = EINVAL;
+ break;
+ }
+#endif
+ if (!m_loc)
+ throw std::system_error(errno, std::generic_category());
+}
+
+scoped_c_locale::~scoped_c_locale()
+{
+ if (!m_loc) return;
+#if !defined(_WIN32)
+ freelocale(m_loc);
+#else
+ _free_locale(m_loc);
+#endif
+}
+
+#if !defined(_WIN32)
+struct scoped_posix_uselocale {
+ explicit scoped_posix_uselocale(c_locale_t loc);
+ ~scoped_posix_uselocale();
+
+ c_locale_t m_loc{};
+ c_locale_t m_old{};
+
+ scoped_posix_uselocale(const scoped_posix_uselocale &) = delete;
+ scoped_posix_uselocale &operator=(const scoped_posix_uselocale &) = delete;
+};
+
+scoped_posix_uselocale::scoped_posix_uselocale(c_locale_t loc)
+{
+ if (loc)
+ {
+ m_loc = loc;
+ m_old = uselocale(loc);
+ }
+}
+
+scoped_posix_uselocale::~scoped_posix_uselocale()
+{
+ if (m_loc)
+ uselocale(m_old);
+}
+#endif
+
+} // namespace
+
+//------------------------------------------------------------------------------
+
+c_locale_t c_numeric_locale()
+{
+ static scoped_c_locale loc(LC_NUMERIC, "C");
+ return loc.m_loc;
+}
+
+//------------------------------------------------------------------------------
+
+double c_atof(const char *text, c_locale_t loc)
+{
+#if defined(_WIN32)
+ return _atof_l(text, loc);
+#else
+ scoped_posix_uselocale use(loc);
+ return atof(text);
+#endif
+}
+
+double c_strtod(const char *text, char **endp, c_locale_t loc)
+{
+#if defined(_WIN32)
+ return _strtod_l(text, endp, loc);
+#else
+ scoped_posix_uselocale use(loc);
+ return strtod(text, endp);
+#endif
+}
+
+double dot_atof(const char *text)
+{
+ return c_atof(text, c_numeric_locale());
+}
+
+double dot_strtod(const char *text, char **endp)
+{
+ return c_strtod(text, endp, c_numeric_locale());
+}
+
+bool ascii_isspace(char c)
+{
+ switch (c) {
+ case ' ': case '\f': case '\n': case '\r': case '\t': case '\v':
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool ascii_isalpha(char c)
+{
+ return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
+}
+
+char ascii_tolower(char c)
+{
+ return (c >= 'A' && c <= 'Z') ? (c - 'A' + 'a') : c;
+}
+
+char ascii_toupper(char c)
+{
+ return (c >= 'a' && c <= 'z') ? (c - 'a' + 'A') : c;
+}
+
+int ascii_casecmp(const char *a, const char *b)
+{
+ for (char ca, cb; (ca = *a++) | (cb = *b++); ) {
+ ca = ascii_tolower(ca);
+ cb = ascii_tolower(cb);
+ if (ca < cb) return -1;
+ if (ca > cb) return +1;
+ }
+ return 0;
+}
+
+uint32_t latin1_toupper(uint32_t c)
+{
+ if (c >= 'a' && c <= 'z')
+ return c - 'a' + 'A';
+ if ((c >= 0xe0 && c <= 0xf6) || (c >= 0xf8 && c <= 0xfe))
+ return c - 0x20;
+ return c;
+}
+
+uint32_t latin1_tolower(uint32_t c)
+{
+ if (c >= 'A' && c <= 'Z')
+ return c - 'A' + 'a';
+ if ((c >= 0xc0 && c <= 0xd6) || (c >= 0xd8 && c <= 0xde))
+ return c + 0x20;
+ return c;
+}
+
+char *strdup_using_new(const char *src)
+{
+ size_t len = strlen(src);
+ char *dst = new char[len + 1];
+ memcpy(dst, src, len + 1);
+ return dst;
+}
+
+string_list split_strings_noempty(const char *input, bool(*pred)(char))
+{
+ string_list list;
+
+ if (input) {
+ std::string acc;
+ acc.reserve(256);
+
+ for (char c; (c = *input++) != '\0'; ) {
+ if (!pred(c))
+ acc.push_back(c);
+ else {
+ if (!acc.empty()) {
+ list.push_back(acc);
+ acc.clear();
+ }
+ }
+ }
+
+ if (!acc.empty())
+ list.push_back(acc);
+ }
+
+ return list;
+}
+
+std::string trim(const char *input, bool(*pred)(char))
+{
+ const char *start = input;
+ while (*start != '\0' && pred(*start))
+ ++start;
+
+ const char *end = start + strlen(start);
+ while (end > start && pred(*(end - 1)))
+ --end;
+
+ return std::string(start, end);
+}
+
+//------------------------------------------------------------------------------
+
+void pack_u32le(uint32_t value, uint8_t data[4])
+{
+ data[0] = value & 0xff;
+ data[1] = (value >> 8) & 0xff;
+ data[2] = (value >> 16) & 0xff;
+ data[3] = value >> 24;
+}
+
+void pack_f32le(float value, uint8_t data[4])
+{
+ uint32_t u;
+ memcpy(&u, &value, 4);
+ pack_u32le(u, data);
+}
+
+uint32_t unpack_u32le(const uint8_t data[4])
+{
+ return data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
+}
+
+float unpack_f32le(const uint8_t data[4])
+{
+ float value;
+ uint32_t u = unpack_u32le(data);
+ memcpy(&value, &u, 4);
+ return value;
+}
+
+//------------------------------------------------------------------------------
+
+std::vector decode_base64(const char *text, size_t len)
+{
+ return d_getChunkFromBase64String(text, len);
+}
+
+//------------------------------------------------------------------------------
+
+bool get_file_uid(const char *path, file_uid &uid)
+{
+#ifdef _WIN32
+ HANDLE handle = CreateFileW(widen(path).c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr);
+ if (handle == INVALID_HANDLE_VALUE)
+ return false;
+ bool success = get_handle_file_uid((void *)handle, uid);
+ CloseHandle(handle);
+ return success;
+#else
+ int fd = open(path, O_RDONLY);
+ if (fd == -1)
+ return false;
+ bool success = get_descriptor_file_uid(fd, uid);
+ close(fd);
+ return success;
+#endif
+}
+
+bool get_stream_file_uid(FILE *stream, file_uid &uid)
+{
+#if !defined(_WIN32)
+ int fd = fileno(stream);
+ if (fd == -1)
+ return false;
+#else
+ int fd = _fileno(stream);
+ if (fd == -1)
+ return false;
+#endif
+ return get_descriptor_file_uid(fd, uid);
+}
+
+bool get_descriptor_file_uid(int fd, file_uid &uid)
+{
+#if !defined(_WIN32)
+ struct stat st;
+ if (fstat(fd, &st) != 0)
+ return false;
+ uid.first = (uint64_t)st.st_dev;
+ uid.second = (uint64_t)st.st_ino;
+ return true;
+#else
+ HANDLE handle = (HANDLE)_get_osfhandle(fd);
+ if (handle == INVALID_HANDLE_VALUE)
+ return false;
+ return get_handle_file_uid((void *)handle, uid);
+#endif
+}
+
+#if defined(_WIN32)
+bool get_handle_file_uid(void *handle, file_uid &uid)
+{
+ BY_HANDLE_FILE_INFORMATION info;
+ if (!GetFileInformationByHandle((HANDLE)handle, &info))
+ return false;
+ uid.first = info.dwVolumeSerialNumber;
+ uid.second = (uint64_t)info.nFileIndexLow | ((uint64_t)info.nFileIndexHigh << 32);
+ return true;
+}
+#endif
+
+//------------------------------------------------------------------------------
+
+bool is_path_separator(char ch)
+{
+#if !defined(_WIN32)
+ return ch == '/';
+#else
+ return ch == '/' || ch == '\\';
+#endif
+}
+
+split_path_t split_path(const char *path)
+{
+ split_path_t sp;
+#if !defined(_WIN32)
+ size_t npos = ~(size_t)0;
+ size_t pos = npos;
+ for (size_t i = 0; path[i] != '\0'; ++i) {
+ if (is_path_separator(path[i]))
+ pos = i;
+ }
+ if (pos == npos)
+ sp.file.assign(path);
+ else {
+ sp.dir.assign(path, pos + 1);
+ sp.file.assign(path + pos + 1);
+ }
+#else
+ std::wstring wpath = widen(path);
+ std::unique_ptr drive{new wchar_t[wpath.size() + 1]{}};
+ std::unique_ptr dir{new wchar_t[wpath.size() + 1]{}};
+ std::unique_ptr fname{new wchar_t[wpath.size() + 1]{}};
+ std::unique_ptr ext{new wchar_t[wpath.size() + 1]{}};
+ _wsplitpath(wpath.c_str(), drive.get(), dir.get(), fname.get(), ext.get());
+ sp.drive = narrow(drive.get());
+ if (!drive[0] || dir[0] == L'/' || dir[0] == L'\\')
+ sp.dir = narrow(dir.get());
+ else
+ sp.dir = narrow(L'\\' + std::wstring(dir.get()));
+ sp.file = narrow(std::wstring(fname.get()) + std::wstring(ext.get()));
+#endif
+ return sp;
+}
+
+std::string path_file_name(const char *path)
+{
+ return split_path(path).file;
+}
+
+std::string path_directory(const char *path)
+{
+ split_path_t sp = split_path(path);
+ return sp.dir.empty() ? std::string("./") : (sp.drive + sp.dir);
+}
+
+std::string path_ensure_final_separator(const char *path)
+{
+ std::string result(path);
+
+ if (!result.empty() && !is_path_separator(result.back()))
+ result.push_back('/');
+
+ return result;
+}
+
+bool path_has_suffix(const char *path, const char *suffix)
+{
+ if (*suffix == '.')
+ ++suffix;
+
+ size_t plen = strlen(path);
+ size_t slen = strlen(suffix);
+ if (plen < slen + 2)
+ return false;
+
+ return path[plen - slen - 1] == '.' &&
+ ascii_casecmp(suffix, &path[plen - slen]) == 0;
+}
+
+bool path_is_relative(const char *path)
+{
+#if !defined(_WIN32)
+ return !is_path_separator(path[0]);
+#else
+ return !is_path_separator(split_path(path).dir.c_str()[0]);
+#endif
+}
+
+//------------------------------------------------------------------------------
+
+#if !defined(_WIN32)
+bool exists(const char *path)
+{
+ return access(path, F_OK) == 0;
+}
+
+string_list list_directory(const char *path)
+{
+ string_list list;
+
+ DIR *dir = opendir(path);
+ if (!dir)
+ return list;
+ auto dir_cleanup = defer([dir]() { closedir(dir); });
+
+ list.reserve(256);
+
+ std::string pathbuf;
+ pathbuf.reserve(1024);
+
+ while (dirent *ent = readdir(dir)) {
+ const char *name = ent->d_name;
+ if (!strcmp(name, ".") || !strcmp(name, ".."))
+ continue;
+
+ pathbuf.assign(name);
+ if (ent->d_type == DT_DIR)
+ pathbuf.push_back('/');
+
+ list.push_back(pathbuf);
+ }
+
+ std::sort(list.begin(), list.end());
+ return list;
+}
+
+// void visit_directories(const char *rootpath, bool (*visit)(const std::string &, void *), void *data);
+// NOTE: implemented in separate file `ysfx_utils_fts.cpp`
+#else
+bool exists(const char *path)
+{
+ return _waccess(widen(path).c_str(), 0) == 0;
+}
+
+string_list list_directory(const char *path)
+{
+ string_list list;
+
+ std::wstring pattern = widen(path) + L"\\*";
+
+ WIN32_FIND_DATAW fd;
+ HANDLE handle = FindFirstFileW(pattern.c_str(), &fd);
+ if (handle == INVALID_HANDLE_VALUE)
+ return list;
+ auto handle_cleanup = defer([handle]() { FindClose(handle); });
+
+ list.reserve(256);
+
+ do {
+ const wchar_t *name = fd.cFileName;
+ if (!wcscmp(name, L".") || !wcscmp(name, L".."))
+ continue;
+
+ std::string entry = narrow(name);
+ if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) && !(fd.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT))
+ entry.push_back('/');
+
+ list.push_back(std::move(entry));
+ } while (FindNextFileW(handle, &fd));
+
+ std::sort(list.begin(), list.end());
+ return list;
+}
+
+void visit_directories(const char *rootpath, bool (*visit)(const std::string &, void *), void *data)
+{
+ std::deque dirs;
+ dirs.push_back(widen(path_ensure_final_separator(rootpath)));
+
+ std::wstring pathbuf;
+ pathbuf.reserve(1024);
+
+ std::vector entries;
+ entries.reserve(256);
+
+ while (!dirs.empty()) {
+ std::wstring dir = std::move(dirs.front());
+ dirs.pop_front();
+
+ if (!visit(narrow(dir), data))
+ return;
+
+ pathbuf.assign(dir);
+ pathbuf.append(L"\\*");
+
+ WIN32_FIND_DATAW fd;
+ HANDLE handle = FindFirstFileW(pathbuf.c_str(), &fd);
+ if (handle == INVALID_HANDLE_VALUE)
+ continue;
+ auto handle_cleanup = defer([handle]() { FindClose(handle); });
+
+ entries.clear();
+ do {
+ if (fd.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
+ continue;
+ if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
+ const wchar_t *name = fd.cFileName;
+ if (!wcscmp(name, L".") || !wcscmp(name, L".."))
+ continue;
+ pathbuf.assign(dir);
+ pathbuf.append(name);
+ pathbuf.push_back(L'\\');
+ entries.push_back(pathbuf);
+ }
+ } while (FindNextFileW(handle, &fd));
+
+ std::sort(entries.begin(), entries.end());
+ for (size_t n = entries.size(); n-- > 0; )
+ dirs.push_front(std::move(entries[n]));
+ }
+}
+#endif
+
+int case_resolve(const char *root_, const char *fragment, std::string &result)
+{
+ if (fragment[0] == '\0')
+ return 0;
+
+ std::string root = path_ensure_final_separator(root_);
+
+ std::string pathbuf;
+ pathbuf.reserve(1024);
+
+ pathbuf.assign(root);
+ pathbuf.append(fragment);
+ if (exists(pathbuf.c_str())) {
+ result = std::move(pathbuf);
+ return 1;
+ }
+
+ struct Item {
+ std::string root;
+ string_list components;
+ };
+
+ std::deque
- worklist;
+
+ {
+ Item item;
+ item.root = root;
+ item.components = split_strings_noempty(fragment, &is_path_separator);
+ if (item.components.empty())
+ return 0;
+ for (size_t i = 0; i + 1 < item.components.size(); ++i)
+ item.components[i].push_back('/');
+ if (is_path_separator(fragment[strlen(fragment) - 1]))
+ item.components.back().push_back('/');
+ worklist.push_back(std::move(item));
+ }
+
+ while (!worklist.empty()) {
+ Item item = std::move(worklist.front());
+ worklist.pop_front();
+
+ for (const std::string &entry : list_directory(item.root.c_str())) {
+ if (ascii_casecmp(entry.c_str(), item.components[0].c_str()) != 0)
+ continue;
+
+ if (item.components.size() == 1) {
+ pathbuf.assign(item.root);
+ pathbuf.append(entry);
+ if (exists(pathbuf.c_str())) {
+ result = std::move(pathbuf);
+ return 2;
+ }
+ }
+ else {
+ assert(item.components.size() > 1);
+ Item newitem;
+ newitem.root = item.root + entry;
+ newitem.components.assign(item.components.begin() + 1, item.components.end());
+ worklist.push_front(std::move(newitem));
+ }
+ }
+ }
+
+ return 0;
+}
+
+//------------------------------------------------------------------------------
+
+#if defined(_WIN32)
+std::wstring widen(const std::string &u8str)
+{
+ return widen(u8str.data(), u8str.size());
+}
+
+std::wstring widen(const char *u8data, size_t u8len)
+{
+ if (u8len == ~(size_t)0)
+ u8len = strlen(u8data);
+ std::wstring wstr;
+ int wch = MultiByteToWideChar(CP_UTF8, 0, u8data, (int)u8len, nullptr, 0);
+ if (wch != 0) {
+ wstr.resize((size_t)wch);
+ MultiByteToWideChar(CP_UTF8, 0, u8data, (int)u8len, &wstr[0], wch);
+ }
+ return wstr;
+}
+
+std::string narrow(const std::wstring &wstr)
+{
+ return narrow(wstr.data(), wstr.size());
+}
+
+std::string narrow(const wchar_t *wdata, size_t wlen)
+{
+ if (wlen == ~(size_t)0)
+ wlen = wcslen(wdata);
+ std::string u8str;
+ int u8ch = WideCharToMultiByte(CP_UTF8, 0, wdata, (int)wlen, nullptr, 0, nullptr, nullptr);
+ if (u8ch != 0) {
+ u8str.resize((size_t)u8ch);
+ WideCharToMultiByte(CP_UTF8, 0, wdata, (int)wlen, &u8str[0], u8ch, nullptr, nullptr);
+ }
+ return u8str;
+}
+#endif
+
+} // namespace ysfx
+
+//------------------------------------------------------------------------------
+// WDL helpers
+
+// our replacement `atof` for WDL, which is unaffected by current locale
+extern "C" double ysfx_wdl_atof(const char *text)
+{
+ return ysfx::dot_atof(text);
+}
diff --git a/source/modules/ysfx/sources/ysfx_utils.hpp b/source/modules/ysfx/sources/ysfx_utils.hpp
new file mode 100644
index 000000000..3940ad024
--- /dev/null
+++ b/source/modules/ysfx/sources/ysfx_utils.hpp
@@ -0,0 +1,185 @@
+// Copyright 2021 Jean Pierre Cimalando
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+#pragma once
+#include "ysfx.h"
+#include