/* * DISTRHO Cardinal Plugin * Copyright (C) 2021-2022 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 3 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 LICENSE file. */ /** * This file uses code adapted from VCVRack's CV_MIDI.cpp and midi.hpp * Copyright (C) 2016-2021 VCV. * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 3 of * the License, or (at your option) any later version. */ #include "plugincontext.hpp" #include "Expander.hpp" #ifndef HEADLESS # include "ImGuiWidget.hpp" # include "ModuleWidgets.hpp" # include "extra/FileBrowserDialog.hpp" # include "extra/ScopedPointer.hpp" # include "extra/Thread.hpp" # include "../../src/extra/SharedResourcePointer.hpp" #else # include "extra/Mutex.hpp" #endif #include "CarlaNativePlugin.h" #include "CarlaBackendUtils.hpp" #include "CarlaEngine.hpp" #include "water/streams/MemoryOutputStream.h" #include "water/xml/XmlDocument.h" #include #ifndef CARDINAL_SYSDEPS // private method that takes ownership, we can use it to avoid superfulous allocations extern "C" { json_t *jsonp_stringn_nocheck_own(const char* value, size_t len); } #endif // defined elsewhere namespace rack { #ifdef ARCH_WIN enum SpecialPath { kSpecialPathUserProfile, kSpecialPathCommonProgramFiles, kSpecialPathProgramFiles, kSpecialPathAppData, }; std::string getSpecialPath(const SpecialPath type); #endif std::string homeDir(); } #define BUFFER_SIZE 128 // generates a warning if this is defined as anything else #define CARLA_API // -------------------------------------------------------------------------------------------------------------------- // strcasestr #ifdef DISTRHO_OS_WINDOWS # include namespace ildaeil { inline const char* strcasestr(const char* const haystack, const char* const needle) { return StrStrIA(haystack, needle); } // using strcasestr = StrStrIA; } #else namespace ildaeil { using ::strcasestr; } #endif // -------------------------------------------------------------------------------------------------------------------- using namespace CARLA_BACKEND_NAMESPACE; static uint32_t host_get_buffer_size(NativeHostHandle); static double host_get_sample_rate(NativeHostHandle); static bool host_is_offline(NativeHostHandle); static const NativeTimeInfo* host_get_time_info(NativeHostHandle handle); static bool host_write_midi_event(NativeHostHandle handle, const NativeMidiEvent* event); static void host_ui_parameter_changed(NativeHostHandle handle, uint32_t index, float value); static void host_ui_midi_program_changed(NativeHostHandle handle, uint8_t channel, uint32_t bank, uint32_t program); static void host_ui_custom_data_changed(NativeHostHandle handle, const char* key, const char* value); static void host_ui_closed(NativeHostHandle handle); static const char* host_ui_open_file(NativeHostHandle handle, bool isDir, const char* title, const char* filter); static const char* host_ui_save_file(NativeHostHandle handle, bool isDir, const char* title, const char* filter); static intptr_t host_dispatcher(NativeHostHandle handle, NativeHostDispatcherOpcode opcode, int32_t index, intptr_t value, void* ptr, float opt); static void projectLoadedFromDSP(void* ui); // -------------------------------------------------------------------------------------------------------------------- static Mutex sPluginInfoLoadMutex; static const char* getPathForJSFX() { static std::string path; if (path.empty()) { #if defined(CARLA_OS_MAC) path = homeDir() + "/Library/Application Support/REAPER/Effects"; #elif defined(CARLA_OS_WIN) path = getSpecialPath(kSpecialPathAppData) + "\\REAPER\\Effects"; if (! system::exists(path)) path = getSpecialPath(kSpecialPathProgramFiles) + "\\REAPER\\InstallData\\Effects"; #else if (const char* const configHome = std::getenv("XDG_CONFIG_HOME")) path = configHome; else path = homeDir() + "/.config"; path += "/REAPER/Effects"; #endif } return path.c_str(); } /* #ifndef HEADLESS struct JuceInitializer { JuceInitializer() { carla_juce_init(); } ~JuceInitializer() { carla_juce_cleanup(); } }; #endif */ struct IldaeilModule : Module { enum ParamIds { NUM_PARAMS }; enum InputIds { INPUT1, INPUT2, NUM_INPUTS }; enum OutputIds { OUTPUT1, OUTPUT2, NUM_OUTPUTS }; enum LightIds { NUM_LIGHTS }; /* #ifndef HEADLESS SharedResourcePointer juceInitializer; #endif */ const CardinalPluginContext* const pcontext; const NativePluginDescriptor* fCarlaPluginDescriptor = nullptr; NativePluginHandle fCarlaPluginHandle = nullptr; NativeHostDescriptor fCarlaHostDescriptor = {}; CarlaHostHandle fCarlaHostHandle = nullptr; NativeTimeInfo fCarlaTimeInfo; void* fUI = nullptr; bool canUseBridges = true; float audioDataIn1[BUFFER_SIZE]; float audioDataIn2[BUFFER_SIZE]; float audioDataOut1[BUFFER_SIZE]; float audioDataOut2[BUFFER_SIZE]; unsigned audioDataFill = 0; uint32_t lastProcessCounter = 0; CardinalExpanderFromCarlaMIDIToCV* midiOutExpander = nullptr; volatile bool resetMeterIn = true; volatile bool resetMeterOut = true; float meterInL = 0.0f; float meterInR = 0.0f; float meterOutL = 0.0f; float meterOutR = 0.0f; IldaeilModule() : pcontext(static_cast(APP)) { config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); for (uint i=0; i<2; ++i) { const char name[] = { 'A','u','d','i','o',' ','#',static_cast('0'+i+1),'\0' }; configInput(i, name); configOutput(i, name); } std::memset(audioDataOut1, 0, sizeof(audioDataOut1)); std::memset(audioDataOut2, 0, sizeof(audioDataOut2)); fCarlaPluginDescriptor = carla_get_native_rack_plugin(); DISTRHO_SAFE_ASSERT_RETURN(fCarlaPluginDescriptor != nullptr,); memset(&fCarlaHostDescriptor, 0, sizeof(fCarlaHostDescriptor)); memset(&fCarlaTimeInfo, 0, sizeof(fCarlaTimeInfo)); fCarlaHostDescriptor.handle = this; fCarlaHostDescriptor.resourceDir = carla_get_library_folder(); fCarlaHostDescriptor.uiName = "Ildaeil"; fCarlaHostDescriptor.uiParentId = 0; fCarlaHostDescriptor.get_buffer_size = host_get_buffer_size; fCarlaHostDescriptor.get_sample_rate = host_get_sample_rate; fCarlaHostDescriptor.is_offline = host_is_offline; fCarlaHostDescriptor.get_time_info = host_get_time_info; fCarlaHostDescriptor.write_midi_event = host_write_midi_event; fCarlaHostDescriptor.ui_parameter_changed = host_ui_parameter_changed; fCarlaHostDescriptor.ui_midi_program_changed = host_ui_midi_program_changed; fCarlaHostDescriptor.ui_custom_data_changed = host_ui_custom_data_changed; fCarlaHostDescriptor.ui_closed = host_ui_closed; fCarlaHostDescriptor.ui_open_file = host_ui_open_file; fCarlaHostDescriptor.ui_save_file = host_ui_save_file; fCarlaHostDescriptor.dispatcher = host_dispatcher; fCarlaPluginHandle = fCarlaPluginDescriptor->instantiate(&fCarlaHostDescriptor); DISTRHO_SAFE_ASSERT_RETURN(fCarlaPluginHandle != nullptr,); fCarlaHostHandle = carla_create_native_plugin_host_handle(fCarlaPluginDescriptor, fCarlaPluginHandle); DISTRHO_SAFE_ASSERT_RETURN(fCarlaHostHandle != nullptr,); #if defined(CARLA_OS_MAC) if (system::exists("~/Applications/Carla.app")) { carla_set_engine_option(fCarlaHostHandle, ENGINE_OPTION_PATH_BINARIES, 0, "~/Applications/Carla.app/Contents/MacOS"); carla_set_engine_option(fCarlaHostHandle, ENGINE_OPTION_PATH_RESOURCES, 0, "~/Applications/Carla.app/Contents/MacOS/resources"); } else if (system::exists("/Applications/Carla.app")) { carla_set_engine_option(fCarlaHostHandle, ENGINE_OPTION_PATH_BINARIES, 0, "/Applications/Carla.app/Contents/MacOS"); carla_set_engine_option(fCarlaHostHandle, ENGINE_OPTION_PATH_RESOURCES, 0, "/Applications/Carla.app/Contents/MacOS/resources"); } #elif defined(CARLA_OS_WIN) const std::string winBinaryDir = system::join(asset::systemDir, "Carla"); if (system::exists(winBinaryDir)) { const std::string winResourceDir = system::join(winBinaryDir, "resources"); carla_set_engine_option(fCarlaHostHandle, ENGINE_OPTION_PATH_BINARIES, 0, winBinaryDir.c_str()); carla_set_engine_option(fCarlaHostHandle, ENGINE_OPTION_PATH_RESOURCES, 0, winResourceDir.c_str()); } #else if (system::exists("/usr/local/lib/carla")) { carla_set_engine_option(fCarlaHostHandle, ENGINE_OPTION_PATH_BINARIES, 0, "/usr/local/lib/carla"); carla_set_engine_option(fCarlaHostHandle, ENGINE_OPTION_PATH_RESOURCES, 0, "/usr/local/share/carla/resources"); } else if (system::exists("/usr/lib/carla")) { carla_set_engine_option(fCarlaHostHandle, ENGINE_OPTION_PATH_BINARIES, 0, "/usr/lib/carla"); carla_set_engine_option(fCarlaHostHandle, ENGINE_OPTION_PATH_RESOURCES, 0, "/usr/share/carla/resources"); } #endif else { canUseBridges = false; static bool warningShown = false; if (! warningShown) { warningShown = true; async_dialog_message("Carla is not installed on this system, bridged plugins will not work"); } } if (const char* const path = std::getenv("LV2_PATH")) carla_set_engine_option(fCarlaHostHandle, ENGINE_OPTION_PLUGIN_PATH, PLUGIN_LV2, path); carla_set_engine_option(fCarlaHostHandle, ENGINE_OPTION_PLUGIN_PATH, PLUGIN_JSFX, getPathForJSFX()); #ifdef CARLA_OS_MAC carla_set_engine_option(fCarlaHostHandle, ENGINE_OPTION_PREFER_UI_BRIDGES, 0, nullptr); #endif fCarlaPluginDescriptor->dispatcher(fCarlaPluginHandle, NATIVE_PLUGIN_OPCODE_HOST_USES_EMBED, 0, 0, nullptr, 0.0f); fCarlaPluginDescriptor->activate(fCarlaPluginHandle); } ~IldaeilModule() override { if (fCarlaPluginHandle != nullptr) fCarlaPluginDescriptor->deactivate(fCarlaPluginHandle); if (fCarlaHostHandle != nullptr) carla_host_handle_free(fCarlaHostHandle); if (fCarlaPluginHandle != nullptr) fCarlaPluginDescriptor->cleanup(fCarlaPluginHandle); } const NativeTimeInfo* hostGetTimeInfo() const noexcept { return &fCarlaTimeInfo; } intptr_t hostDispatcher(const NativeHostDispatcherOpcode opcode, const int32_t index, const intptr_t value, void* const ptr, const float opt) { switch (opcode) { // cannnot be supported case NATIVE_HOST_OPCODE_HOST_IDLE: break; // other stuff case NATIVE_HOST_OPCODE_NULL: case NATIVE_HOST_OPCODE_UPDATE_PARAMETER: case NATIVE_HOST_OPCODE_UPDATE_MIDI_PROGRAM: case NATIVE_HOST_OPCODE_RELOAD_PARAMETERS: case NATIVE_HOST_OPCODE_RELOAD_MIDI_PROGRAMS: case NATIVE_HOST_OPCODE_RELOAD_ALL: case NATIVE_HOST_OPCODE_UI_UNAVAILABLE: case NATIVE_HOST_OPCODE_INTERNAL_PLUGIN: case NATIVE_HOST_OPCODE_QUEUE_INLINE_DISPLAY: case NATIVE_HOST_OPCODE_UI_TOUCH_PARAMETER: case NATIVE_HOST_OPCODE_REQUEST_IDLE: case NATIVE_HOST_OPCODE_GET_FILE_PATH: case NATIVE_HOST_OPCODE_UI_RESIZE: case NATIVE_HOST_OPCODE_PREVIEW_BUFFER_DATA: // TESTING d_stdout("dispatcher %i, %i, %li, %p, %f", opcode, index, value, ptr, opt); break; } return 0; } json_t* dataToJson() override { if (fCarlaHostHandle == nullptr) return nullptr; CarlaEngine* const engine = carla_get_engine_from_handle(fCarlaHostHandle); water::MemoryOutputStream projectState; engine->saveProjectInternal(projectState); const size_t dataSize = projectState.getDataSize(); #ifndef CARDINAL_SYSDEPS return jsonp_stringn_nocheck_own(static_cast(projectState.getDataAndRelease()), dataSize); #else return json_stringn(static_cast(projectState.getData()), dataSize); #endif } void dataFromJson(json_t* const rootJ) override { if (fCarlaHostHandle == nullptr) return; const char* const projectState = json_string_value(rootJ); DISTRHO_SAFE_ASSERT_RETURN(projectState != nullptr,); CarlaEngine* const engine = carla_get_engine_from_handle(fCarlaHostHandle); water::XmlDocument xml(projectState); { const MutexLocker cml(sPluginInfoLoadMutex); engine->loadProjectInternal(xml, true); } projectLoadedFromDSP(fUI); } void process(const ProcessArgs& args) override { if (fCarlaPluginHandle == nullptr) return; const unsigned i = audioDataFill++; audioDataIn1[i] = inputs[INPUT1].getVoltage() * 0.1f; audioDataIn2[i] = inputs[INPUT2].getVoltage() * 0.1f; outputs[OUTPUT1].setVoltage(audioDataOut1[i] * 10.0f); outputs[OUTPUT2].setVoltage(audioDataOut2[i] * 10.0f); if (audioDataFill == BUFFER_SIZE) { const uint32_t processCounter = pcontext->processCounter; // Update time position if running a new audio block if (lastProcessCounter != processCounter) { lastProcessCounter = processCounter; fCarlaTimeInfo.playing = pcontext->playing; fCarlaTimeInfo.frame = pcontext->frame; fCarlaTimeInfo.bbt.valid = pcontext->bbtValid; fCarlaTimeInfo.bbt.bar = pcontext->bar; fCarlaTimeInfo.bbt.beat = pcontext->beat; fCarlaTimeInfo.bbt.tick = pcontext->tick; fCarlaTimeInfo.bbt.barStartTick = pcontext->barStartTick; fCarlaTimeInfo.bbt.beatsPerBar = pcontext->beatsPerBar; fCarlaTimeInfo.bbt.beatType = pcontext->beatType; fCarlaTimeInfo.bbt.ticksPerBeat = pcontext->ticksPerBeat; fCarlaTimeInfo.bbt.beatsPerMinute = pcontext->beatsPerMinute; } // or advance time by BUFFER_SIZE frames if still under the same audio block else if (fCarlaTimeInfo.playing) { fCarlaTimeInfo.frame += BUFFER_SIZE; // adjust BBT as well if (fCarlaTimeInfo.bbt.valid) { const double samplesPerTick = 60.0 * args.sampleRate / fCarlaTimeInfo.bbt.beatsPerMinute / fCarlaTimeInfo.bbt.ticksPerBeat; int32_t newBar = fCarlaTimeInfo.bbt.bar; int32_t newBeat = fCarlaTimeInfo.bbt.beat; double newTick = fCarlaTimeInfo.bbt.tick + (double)BUFFER_SIZE / samplesPerTick; while (newTick >= fCarlaTimeInfo.bbt.ticksPerBeat) { newTick -= fCarlaTimeInfo.bbt.ticksPerBeat; if (++newBeat > fCarlaTimeInfo.bbt.beatsPerBar) { newBeat = 1; ++newBar; fCarlaTimeInfo.bbt.barStartTick += fCarlaTimeInfo.bbt.beatsPerBar * fCarlaTimeInfo.bbt.ticksPerBeat; } } fCarlaTimeInfo.bbt.bar = newBar; fCarlaTimeInfo.bbt.beat = newBeat; fCarlaTimeInfo.bbt.tick = newTick; } } NativeMidiEvent* midiEvents; uint midiEventCount; if (CardinalExpanderFromCVToCarlaMIDI* const midiInExpander = leftExpander.module != nullptr && leftExpander.module->model == modelExpanderInputMIDI ? static_cast(leftExpander.module) : nullptr) { midiEvents = midiInExpander->midiEvents; midiEventCount = midiInExpander->midiEventCount; midiInExpander->midiEventCount = midiInExpander->frame = 0; } else { midiEvents = nullptr; midiEventCount = 0; } if ((midiOutExpander = rightExpander.module != nullptr && rightExpander.module->model == modelExpanderOutputMIDI ? static_cast(rightExpander.module) : nullptr)) midiOutExpander->midiEventCount = 0; audioDataFill = 0; float* ins[2] = { audioDataIn1, audioDataIn2 }; float* outs[2] = { audioDataOut1, audioDataOut2 }; if (resetMeterIn) meterInL = meterInR = 0.0f; meterInL = std::max(meterInL, d_findMaxNormalizedFloat(audioDataIn1, BUFFER_SIZE)); meterInR = std::max(meterInR, d_findMaxNormalizedFloat(audioDataIn2, BUFFER_SIZE)); fCarlaPluginDescriptor->process(fCarlaPluginHandle, ins, outs, BUFFER_SIZE, midiEvents, midiEventCount); if (resetMeterOut) meterOutL = meterOutR = 0.0f; meterOutL = std::max(meterOutL, d_findMaxNormalizedFloat(audioDataOut1, BUFFER_SIZE)); meterOutR = std::max(meterOutR, d_findMaxNormalizedFloat(audioDataOut2, BUFFER_SIZE)); resetMeterIn = resetMeterOut = false; } } void onReset() override { resetMeterIn = resetMeterOut = true; midiOutExpander = nullptr; } void onSampleRateChange(const SampleRateChangeEvent& e) override { if (fCarlaPluginHandle == nullptr) return; resetMeterIn = resetMeterOut = true; midiOutExpander = nullptr; fCarlaPluginDescriptor->deactivate(fCarlaPluginHandle); fCarlaPluginDescriptor->dispatcher(fCarlaPluginHandle, NATIVE_PLUGIN_OPCODE_SAMPLE_RATE_CHANGED, 0, 0, nullptr, e.sampleRate); fCarlaPluginDescriptor->activate(fCarlaPluginHandle); } DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(IldaeilModule) }; // ----------------------------------------------------------------------------------------------------------- static uint32_t host_get_buffer_size(const NativeHostHandle handle) { return BUFFER_SIZE; } static double host_get_sample_rate(const NativeHostHandle handle) { const CardinalPluginContext* const pcontext = static_cast(handle)->pcontext; DISTRHO_SAFE_ASSERT_RETURN(pcontext != nullptr, 48000.0); return pcontext->sampleRate; } static bool host_is_offline(NativeHostHandle) { return false; } static const NativeTimeInfo* host_get_time_info(const NativeHostHandle handle) { return static_cast(handle)->hostGetTimeInfo(); } static bool host_write_midi_event(const NativeHostHandle handle, const NativeMidiEvent* const event) { if (CardinalExpanderFromCarlaMIDIToCV* const expander = static_cast(handle)->midiOutExpander) { if (expander->midiEventCount == CardinalExpanderFromCarlaMIDIToCV::MAX_MIDI_EVENTS) return false; NativeMidiEvent& expanderEvent(expander->midiEvents[expander->midiEventCount++]); carla_copyStruct(expanderEvent, *event); return true; } return false; } static void host_ui_midi_program_changed(NativeHostHandle handle, uint8_t channel, uint32_t bank, uint32_t program) { d_stdout("%s %p %u %u %u", __FUNCTION__, handle, channel, bank, program); } static void host_ui_custom_data_changed(NativeHostHandle handle, const char* key, const char* value) { d_stdout("%s %p %s %s", __FUNCTION__, handle, key, value); } static void host_ui_closed(NativeHostHandle handle) { d_stdout("%s %p", __FUNCTION__, handle); } static const char* host_ui_save_file(NativeHostHandle, bool, const char*, const char*) { return nullptr; } static intptr_t host_dispatcher(const NativeHostHandle handle, const NativeHostDispatcherOpcode opcode, const int32_t index, const intptr_t value, void* const ptr, const float opt) { return static_cast(handle)->hostDispatcher(opcode, index, value, ptr, opt); } // -------------------------------------------------------------------------------------------------------------------- #ifndef HEADLESS struct IldaeilWidget : ImGuiWidget, IdleCallback, Thread { static constexpr const uint kButtonHeight = 20; struct PluginInfoCache { char* name; char* label; PluginInfoCache() : name(nullptr), label(nullptr) {} ~PluginInfoCache() { std::free(name); std::free(label); } }; struct PluginGenericUI { char* title; uint parameterCount; struct Parameter { char* name; char* printformat; uint32_t rindex; bool boolean, bvalue, log, readonly; float min, max, power; Parameter() : name(nullptr), printformat(nullptr), rindex(0), boolean(false), bvalue(false), log(false), readonly(false), min(0.0f), max(1.0f) {} ~Parameter() { std::free(name); std::free(printformat); } }* parameters; float* values; PluginGenericUI() : title(nullptr), parameterCount(0), parameters(nullptr), values(nullptr) {} ~PluginGenericUI() { std::free(title); delete[] parameters; delete[] values; } }; enum { kDrawingLoading, kDrawingPluginError, kDrawingPluginList, kDrawingPluginGenericUI, kDrawingErrorInit, kDrawingErrorDraw } fDrawingState = kDrawingLoading; enum { kIdleInit, kIdleInitPluginAlreadyLoaded, kIdleLoadSelectedPlugin, kIdlePluginLoadedFromDSP, kIdleResetPlugin, kIdleShowCustomUI, kIdleHidePluginUI, kIdleGiveIdleToUI, kIdleChangePluginType, kIdleNothing } fIdleState = kIdleInit; PluginType fPluginType = PLUGIN_LV2; PluginType fNextPluginType = fPluginType; uint fPluginCount = 0; int fPluginSelected = -1; bool fPluginScanningFinished = false; bool fPluginHasCustomUI = false; bool fPluginHasOutputParameters = false; bool fPluginRunning = false; bool fPluginWillRunInBridgeMode = false; PluginInfoCache* fPlugins = nullptr; ScopedPointer fPluginGenericUI; bool fPluginSearchActive = false; bool fPluginSearchFirstShow = false; char fPluginSearchString[0xff] = {}; String fPopupError; FileBrowserHandle fileBrowserHandle = nullptr; bool idleCallbackActive = false; IldaeilModule* const module; IldaeilWidget(IldaeilModule* const m) : ImGuiWidget(), module(m) { std::strcpy(fPluginSearchString, "Search..."); if (m != nullptr) { if (m->fCarlaHostHandle == nullptr) { fDrawingState = kDrawingErrorInit; fIdleState = kIdleNothing; fPopupError = "Ildaeil backend failed to init properly, cannot continue."; return; } if (checkIfPluginIsLoaded()) fIdleState = kIdleInitPluginAlreadyLoaded; m->fUI = this; } else { fDrawingState = kDrawingPluginList; fIdleState = kIdleNothing; } } ~IldaeilWidget() override { if (module != nullptr && module->fCarlaHostHandle != nullptr) { if (idleCallbackActive) { module->pcontext->removeIdleCallback(this); if (fileBrowserHandle != nullptr) fileBrowserClose(fileBrowserHandle); } if (fPluginRunning) carla_show_custom_ui(module->fCarlaHostHandle, 0, false); carla_set_engine_option(module->fCarlaHostHandle, ENGINE_OPTION_FRONTEND_WIN_ID, 0, "0"); module->fUI = nullptr; } if (isThreadRunning()) stopThread(-1); fPluginGenericUI = nullptr; delete[] fPlugins; } bool checkIfPluginIsLoaded() { const CarlaHostHandle handle = module->fCarlaHostHandle; if (carla_get_current_plugin_count(handle) != 0) { const uint hints = carla_get_plugin_info(handle, 0)->hints; fPluginRunning = true; fPluginHasCustomUI = hints & PLUGIN_HAS_CUSTOM_UI; return true; } return false; } void projectLoadedFromDSP() { if (checkIfPluginIsLoaded()) fIdleState = kIdlePluginLoadedFromDSP; } void changeParameterFromDSP(const uint32_t index, const float value) { if (PluginGenericUI* const ui = fPluginGenericUI) { for (uint32_t i=0; i < ui->parameterCount; ++i) { if (ui->parameters[i].rindex != index) continue; ui->values[i] = value; if (ui->parameters[i].boolean) ui->parameters[i].bvalue = value > ui->parameters[i].min; break; } } setDirty(true); } void openFileFromDSP(bool /* isDir */, const char* const title, const char* /* filter */) { DISTRHO_SAFE_ASSERT_RETURN(idleCallbackActive,); DISTRHO_SAFE_ASSERT_RETURN(fPluginType == PLUGIN_INTERNAL || fPluginType == PLUGIN_LV2,); const CardinalPluginContext* const pcontext = module->pcontext; // FIXME isEmbed FileBrowserOptions opts; opts.title = title; fileBrowserHandle = fileBrowserCreate(true, pcontext->nativeWindowId, pcontext->window->pixelRatio, opts); } void createOrUpdatePluginGenericUI(const CarlaHostHandle handle) { const CarlaPluginInfo* const info = carla_get_plugin_info(handle, 0); fDrawingState = kDrawingPluginGenericUI; fPluginHasCustomUI = info->hints & PLUGIN_HAS_CUSTOM_UI; if (fPluginGenericUI == nullptr) createPluginGenericUI(handle, info); else updatePluginGenericUI(handle); setDirty(true); } void hidePluginUI(const CarlaHostHandle handle) { DISTRHO_SAFE_ASSERT_RETURN(fPluginRunning,); carla_show_custom_ui(handle, 0, false); } void createPluginGenericUI(const CarlaHostHandle handle, const CarlaPluginInfo* const info) { PluginGenericUI* const ui = new PluginGenericUI; String title(info->name); title += " by "; title += info->maker; ui->title = title.getAndReleaseBuffer(); fPluginHasOutputParameters = false; const uint32_t pcount = ui->parameterCount = carla_get_parameter_count(handle, 0); // make count of valid parameters for (uint32_t i=0; i < pcount; ++i) { const ParameterData* const pdata = carla_get_parameter_data(handle, 0, i); if ((pdata->hints & PARAMETER_IS_ENABLED) == 0x0) { --ui->parameterCount; continue; } if (pdata->type == PARAMETER_OUTPUT) fPluginHasOutputParameters = true; } ui->parameters = new PluginGenericUI::Parameter[ui->parameterCount]; ui->values = new float[ui->parameterCount]; // now safely fill in details for (uint32_t i=0, j=0; i < pcount; ++i) { const ParameterData* const pdata = carla_get_parameter_data(handle, 0, i); if ((pdata->hints & PARAMETER_IS_ENABLED) == 0x0) continue; const CarlaParameterInfo* const pinfo = carla_get_parameter_info(handle, 0, i); const ::ParameterRanges* const pranges = carla_get_parameter_ranges(handle, 0, i); String printformat; if (pdata->hints & PARAMETER_IS_INTEGER) printformat = "%.0f "; else printformat = "%.3f "; printformat += pinfo->unit; PluginGenericUI::Parameter& param(ui->parameters[j]); param.name = strdup(pinfo->name); param.printformat = printformat.getAndReleaseBuffer(); param.rindex = i; param.boolean = pdata->hints & PARAMETER_IS_BOOLEAN; param.log = pdata->hints & PARAMETER_IS_LOGARITHMIC; param.readonly = pdata->type != PARAMETER_INPUT || (pdata->hints & PARAMETER_IS_READ_ONLY); param.min = pranges->min; param.max = pranges->max; ui->values[j] = carla_get_current_parameter_value(handle, 0, i); if (param.boolean) param.bvalue = ui->values[j] > param.min; else param.bvalue = false; ++j; } fPluginGenericUI = ui; } void updatePluginGenericUI(const CarlaHostHandle handle) { PluginGenericUI* const ui = fPluginGenericUI; DISTRHO_SAFE_ASSERT_RETURN(ui != nullptr,); for (uint32_t i=0; i < ui->parameterCount; ++i) { ui->values[i] = carla_get_current_parameter_value(handle, 0, ui->parameters[i].rindex); if (ui->parameters[i].boolean) ui->parameters[i].bvalue = ui->values[i] > ui->parameters[i].min; } } void loadPlugin(const CarlaHostHandle handle, const char* const label) { if (fPluginRunning) { carla_show_custom_ui(handle, 0, false); carla_replace_plugin(handle, 0); } carla_set_engine_option(handle, ENGINE_OPTION_PREFER_PLUGIN_BRIDGES, fPluginWillRunInBridgeMode, nullptr); const MutexLocker cml(sPluginInfoLoadMutex); if (carla_add_plugin(handle, BINARY_NATIVE, fPluginType, nullptr, nullptr, label, 0, 0x0, PLUGIN_OPTIONS_NULL)) { fPluginRunning = true; fPluginGenericUI = nullptr; createOrUpdatePluginGenericUI(handle); } else { fPopupError = carla_get_last_error(handle); d_stdout("got error: %s", fPopupError.buffer()); fDrawingState = kDrawingPluginError; } setDirty(true); } void onContextCreate(const ContextCreateEvent& e) override { ImGuiWidget::onContextCreate(e); widgetCreated(); } void onContextDestroy(const ContextDestroyEvent& e) override { widgetDestroyed(); ImGuiWidget::onContextDestroy(e); } void onAdd(const AddEvent& e) override { ImGuiWidget::onAdd(e); widgetCreated(); } void onRemove(const RemoveEvent& e) override { widgetDestroyed(); ImGuiWidget::onRemove(e); } void widgetCreated() { if (module == nullptr) return; if (const CarlaHostHandle handle = module->fCarlaHostHandle) { const CardinalPluginContext* const pcontext = module->pcontext; char winIdStr[24]; std::snprintf(winIdStr, sizeof(winIdStr), "%llx", (ulonglong)pcontext->nativeWindowId); module->fCarlaHostDescriptor.uiParentId = pcontext->nativeWindowId; carla_set_engine_option(handle, ENGINE_OPTION_FRONTEND_WIN_ID, 0, winIdStr); if (pcontext->window != nullptr) carla_set_engine_option(handle, ENGINE_OPTION_FRONTEND_UI_SCALE, pcontext->window->pixelRatio*1000, nullptr); if (! idleCallbackActive) { idleCallbackActive = pcontext->addIdleCallback(this); } } } void widgetDestroyed() { if (module == nullptr) return; if (const CarlaHostHandle handle = module->fCarlaHostHandle) { const CardinalPluginContext* const pcontext = module->pcontext; module->fCarlaHostDescriptor.uiParentId = 0; carla_set_engine_option(handle, ENGINE_OPTION_FRONTEND_WIN_ID, 0, "0"); if (idleCallbackActive) { idleCallbackActive = false; pcontext->removeIdleCallback(this); if (fileBrowserHandle != nullptr) { fileBrowserClose(fileBrowserHandle); fileBrowserHandle = nullptr; } } } } void idleCallback() override { const CarlaHostHandle handle = module->fCarlaHostHandle; DISTRHO_SAFE_ASSERT_RETURN(handle != nullptr,); /* carla_juce_idle(); */ if (fileBrowserHandle != nullptr && fileBrowserIdle(fileBrowserHandle)) { if (const char* const path = fileBrowserGetPath(fileBrowserHandle)) carla_set_custom_data(handle, 0, CUSTOM_DATA_TYPE_PATH, "file", path); fileBrowserClose(fileBrowserHandle); fileBrowserHandle = nullptr; } if (fDrawingState == kDrawingPluginGenericUI && fPluginGenericUI != nullptr && fPluginHasOutputParameters) { updatePluginGenericUI(handle); setDirty(true); } switch (fIdleState) { case kIdleInit: fIdleState = kIdleNothing; startThread(); break; case kIdleInitPluginAlreadyLoaded: fIdleState = kIdleNothing; createOrUpdatePluginGenericUI(handle); startThread(); break; case kIdlePluginLoadedFromDSP: fIdleState = kIdleNothing; createOrUpdatePluginGenericUI(handle); break; case kIdleLoadSelectedPlugin: fIdleState = kIdleNothing; loadSelectedPlugin(handle); break; case kIdleResetPlugin: fIdleState = kIdleNothing; loadPlugin(handle, carla_get_plugin_info(handle, 0)->label); break; case kIdleShowCustomUI: fIdleState = kIdleGiveIdleToUI; carla_show_custom_ui(handle, 0, true); break; case kIdleHidePluginUI: fIdleState = kIdleNothing; carla_show_custom_ui(handle, 0, false); break; case kIdleGiveIdleToUI: module->fCarlaPluginDescriptor->ui_idle(module->fCarlaPluginHandle); break; case kIdleChangePluginType: fIdleState = kIdleNothing; fPluginSelected = -1; if (isThreadRunning()) stopThread(-1); fPluginType = fNextPluginType; startThread(); break; case kIdleNothing: break; } } void loadSelectedPlugin(const CarlaHostHandle handle) { DISTRHO_SAFE_ASSERT_RETURN(fPluginSelected >= 0,); const PluginInfoCache& info(fPlugins[fPluginSelected]); const char* label = nullptr; switch (fPluginType) { case PLUGIN_INTERNAL: case PLUGIN_AU: case PLUGIN_JSFX: case PLUGIN_SFZ: label = info.label; break; case PLUGIN_LV2: { const char* const slash = std::strchr(info.label, DISTRHO_OS_SEP); DISTRHO_SAFE_ASSERT_RETURN(slash != nullptr,); label = slash+1; break; } default: break; } DISTRHO_SAFE_ASSERT_RETURN(label != nullptr,); d_stdout("Loading %s...", info.name); loadPlugin(handle, label); } void run() override { const char* path; switch (fPluginType) { case PLUGIN_LV2: path = std::getenv("LV2_PATH"); break; case PLUGIN_JSFX: path = getPathForJSFX(); break; default: path = nullptr; break; } if (path != nullptr) carla_set_engine_option(module->fCarlaHostHandle, ENGINE_OPTION_PLUGIN_PATH, fPluginType, path); fPluginCount = 0; delete[] fPlugins; uint count; { const MutexLocker cml(sPluginInfoLoadMutex); d_stdout("Will scan plugins now..."); count = carla_get_cached_plugin_count(fPluginType, path); d_stdout("Scanning found %u plugins", count); } if (fDrawingState == kDrawingLoading) { fDrawingState = kDrawingPluginList; fPluginSearchFirstShow = true; } if (count != 0) { fPlugins = new PluginInfoCache[count]; for (uint i=0, j; i < count && ! shouldThreadExit(); ++i) { const MutexLocker cml(sPluginInfoLoadMutex); const CarlaCachedPluginInfo* const info = carla_get_cached_plugin_info(fPluginType, i); DISTRHO_SAFE_ASSERT_CONTINUE(info != nullptr); if (! info->valid) continue; if (info->audioIns != 0 && info->audioIns != 2) continue; if (info->midiIns != 0 && info->midiIns != 1) continue; if (info->midiOuts != 0 && info->midiOuts != 1) continue; if (fPluginType == PLUGIN_INTERNAL) { if (std::strcmp(info->label, "audiogain_s") == 0) continue; if (std::strcmp(info->label, "cv2audio") == 0) continue; if (std::strcmp(info->label, "lfo") == 0) continue; if (std::strcmp(info->label, "midi2cv") == 0) continue; if (std::strcmp(info->label, "midithrough") == 0) continue; if (std::strcmp(info->label, "3bandsplitter") == 0) continue; } j = fPluginCount; fPlugins[j].name = strdup(info->name); fPlugins[j].label = strdup(info->label); ++fPluginCount; } } else { fPlugins = nullptr; } if (! shouldThreadExit()) fPluginScanningFinished = true; } void drawImGui() override { switch (fDrawingState) { case kDrawingLoading: drawLoading(); break; case kDrawingPluginError: ImGui::OpenPopup("Plugin Error"); // call ourselves again with the plugin list fDrawingState = kDrawingPluginList; drawImGui(); break; case kDrawingPluginList: drawPluginList(); break; case kDrawingPluginGenericUI: drawTopBar(); drawGenericUI(); break; case kDrawingErrorInit: fDrawingState = kDrawingErrorDraw; drawError(true); break; case kDrawingErrorDraw: drawError(false); break; } } void drawError(const bool open) { const float scaleFactor = getScaleFactor(); ImGui::SetNextWindowPos(ImVec2(0, 0)); ImGui::SetNextWindowSize(ImVec2(box.size.x * scaleFactor, box.size.y * scaleFactor)); const int flags = ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse; if (ImGui::Begin("Error Window", nullptr, flags)) { if (open) ImGui::OpenPopup("Engine Error"); const int pflags = ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_AlwaysUseWindowPadding; if (ImGui::BeginPopupModal("Engine Error", nullptr, pflags)) { ImGui::TextUnformatted(fPopupError.buffer(), nullptr); ImGui::EndPopup(); } } ImGui::End(); } void drawTopBar() { const float scaleFactor = getScaleFactor(); const float padding = ImGui::GetStyle().WindowPadding.y * 2; ImGui::SetNextWindowPos(ImVec2(0, 0)); ImGui::SetNextWindowSize(ImVec2(box.size.x * scaleFactor, kButtonHeight * scaleFactor + padding)); const int flags = ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse; if (ImGui::Begin("Current Plugin", nullptr, flags)) { if (ImGui::Button("Pick Another...")) { fIdleState = kIdleHidePluginUI; fDrawingState = kDrawingPluginList; } ImGui::SameLine(); if (ImGui::Button("Reset")) fIdleState = kIdleResetPlugin; if (fDrawingState == kDrawingPluginGenericUI && fPluginHasCustomUI) { ImGui::SameLine(); if (ImGui::Button("Show Custom GUI")) fIdleState = kIdleShowCustomUI; } } ImGui::End(); } void setupMainWindowPos() { const float scaleFactor = getScaleFactor(); float y = 0; float width = box.size.x * scaleFactor; float height = box.size.y * scaleFactor; if (fDrawingState == kDrawingPluginGenericUI) { y = kButtonHeight * scaleFactor + ImGui::GetStyle().WindowPadding.y * 2 - scaleFactor; height -= y; } ImGui::SetNextWindowPos(ImVec2(0, y)); ImGui::SetNextWindowSize(ImVec2(width, height)); } void drawGenericUI() { setupMainWindowPos(); PluginGenericUI* const ui = fPluginGenericUI; DISTRHO_SAFE_ASSERT_RETURN(ui != nullptr,); const int pflags = ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize; if (ImGui::Begin(ui->title, nullptr, pflags)) { const CarlaHostHandle handle = module->fCarlaHostHandle; for (uint32_t i=0; i < ui->parameterCount; ++i) { PluginGenericUI::Parameter& param(ui->parameters[i]); if (param.readonly) { ImGui::BeginDisabled(); ImGui::SliderFloat(param.name, &ui->values[i], param.min, param.max, param.printformat, ImGuiSliderFlags_NoInput); ImGui::EndDisabled(); continue; } if (param.boolean) { if (ImGui::Checkbox(param.name, &ui->parameters[i].bvalue)) { if (ImGui::IsItemActivated()) { carla_set_parameter_touch(handle, 0, param.rindex, true); // editParameter(0, true); } ui->values[i] = ui->parameters[i].bvalue ? ui->parameters[i].max : ui->parameters[i].min; carla_set_parameter_value(handle, 0, param.rindex, ui->values[i]); // setParameterValue(0, ui->values[i]); } } else { const bool ret = param.log ? ImGui::SliderFloat(param.name, &ui->values[i], param.min, param.max, param.printformat, 2.0f) : ImGui::SliderFloat(param.name, &ui->values[i], param.min, param.max, param.printformat); if (ret) { if (ImGui::IsItemActivated()) { carla_set_parameter_touch(handle, 0, param.rindex, true); // editParameter(0, true); } carla_set_parameter_value(handle, 0, param.rindex, ui->values[i]); // setParameterValue(0, ui->values[i]); } } if (ImGui::IsItemDeactivated()) { carla_set_parameter_touch(handle, 0, param.rindex, false); // editParameter(0, false); } } } ImGui::End(); } void drawLoading() { setupMainWindowPos(); if (ImGui::Begin("Plugin List", nullptr, ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoResize)) ImGui::TextUnformatted("Loading...", nullptr); ImGui::End(); } void drawPluginList() { static const char* pluginTypes[] = { getPluginTypeAsString(PLUGIN_INTERNAL), getPluginTypeAsString(PLUGIN_LV2), getPluginTypeAsString(PLUGIN_JSFX), }; setupMainWindowPos(); if (ImGui::Begin("Plugin List", nullptr, ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoResize)) { const int pflags = ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_AlwaysAutoResize; if (ImGui::BeginPopupModal("Plugin Error", nullptr, pflags)) { ImGui::TextWrapped("Failed to load plugin, error was:\n%s", fPopupError.buffer()); ImGui::Separator(); if (ImGui::Button("Ok")) ImGui::CloseCurrentPopup(); ImGui::SameLine(); ImGui::Dummy(ImVec2(500 * getScaleFactor(), 1)); ImGui::EndPopup(); } else if (fPluginSearchFirstShow) { fPluginSearchFirstShow = false; ImGui::SetKeyboardFocusHere(); } if (ImGui::InputText("##pluginsearch", fPluginSearchString, sizeof(fPluginSearchString)-1, ImGuiInputTextFlags_CharsNoBlank|ImGuiInputTextFlags_AutoSelectAll)) fPluginSearchActive = true; if (ImGui::IsKeyDown(ImGuiKey_Escape)) fPluginSearchActive = false; ImGui::SameLine(); ImGui::PushItemWidth(-1.0f); int current; switch (fPluginType) { case PLUGIN_JSFX: current = 2; break; case PLUGIN_LV2: current = 1; break; default: current = 0; break; } if (ImGui::Combo("##plugintypes", ¤t, pluginTypes, ARRAY_SIZE(pluginTypes))) { fIdleState = kIdleChangePluginType; switch (current) { case 0: fNextPluginType = PLUGIN_INTERNAL; break; case 1: fNextPluginType = PLUGIN_LV2; break; case 2: fNextPluginType = PLUGIN_JSFX; break; } } ImGui::BeginDisabled(!fPluginScanningFinished || fPluginSelected < 0); if (ImGui::Button("Load Plugin")) fIdleState = kIdleLoadSelectedPlugin; if (fPluginType != PLUGIN_INTERNAL && (module == nullptr || module->canUseBridges)) { ImGui::SameLine(); ImGui::Checkbox("Run in bridge mode", &fPluginWillRunInBridgeMode); } ImGui::EndDisabled(); if (fPluginRunning) { ImGui::SameLine(); if (ImGui::Button("Cancel")) fDrawingState = kDrawingPluginGenericUI; } if (ImGui::BeginChild("pluginlistwindow")) { if (ImGui::BeginTable("pluginlist", 2, ImGuiTableFlags_NoSavedSettings)) { const char* const search = fPluginSearchActive && fPluginSearchString[0] != '\0' ? fPluginSearchString : nullptr; switch (fPluginType) { case PLUGIN_INTERNAL: case PLUGIN_AU: case PLUGIN_SFZ: case PLUGIN_JSFX: ImGui::TableSetupColumn("Name"); ImGui::TableSetupColumn("Label"); ImGui::TableHeadersRow(); break; case PLUGIN_LV2: ImGui::TableSetupColumn("Name"); ImGui::TableSetupColumn("URI"); ImGui::TableHeadersRow(); break; default: break; } for (uint i=0; i= 0 && static_cast(fPluginSelected) == i; switch (fPluginType) { case PLUGIN_INTERNAL: case PLUGIN_AU: case PLUGIN_JSFX: case PLUGIN_SFZ: ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::Selectable(info.name, &selected); ImGui::TableSetColumnIndex(1); ImGui::Selectable(info.label, &selected); break; case PLUGIN_LV2: { const char* const slash = std::strchr(info.label, DISTRHO_OS_SEP); DISTRHO_SAFE_ASSERT_CONTINUE(slash != nullptr); ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::Selectable(info.name, &selected); ImGui::TableSetColumnIndex(1); ImGui::Selectable(slash+1, &selected); break; } default: break; } if (selected) fPluginSelected = i; } ImGui::EndTable(); } ImGui::EndChild(); } } ImGui::End(); } DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(IldaeilWidget) }; // -------------------------------------------------------------------------------------------------------------------- static void host_ui_parameter_changed(const NativeHostHandle handle, const uint32_t index, const float value) { if (IldaeilWidget* const ui = static_cast(static_cast(handle)->fUI)) ui->changeParameterFromDSP(index, value); } static const char* host_ui_open_file(const NativeHostHandle handle, const bool isDir, const char* const title, const char* const filter) { if (IldaeilWidget* const ui = static_cast(static_cast(handle)->fUI)) ui->openFileFromDSP(isDir, title, filter); return nullptr; } static void projectLoadedFromDSP(void* const ui) { if (IldaeilWidget* const uiw = static_cast(ui)) uiw->projectLoadedFromDSP(); } // -------------------------------------------------------------------------------------------------------------------- struct IldaeilNanoMeterIn : NanoMeter { IldaeilModule* const module; IldaeilNanoMeterIn(IldaeilModule* const m) : module(m) { withBackground = false; } void updateMeters() override { if (module == nullptr || module->resetMeterIn) return; // Only fetch new values once DSP side is updated gainMeterL = module->meterInL; gainMeterR = module->meterInR; module->resetMeterIn = true; } }; struct IldaeilNanoMeterOut : NanoMeter { IldaeilModule* const module; IldaeilNanoMeterOut(IldaeilModule* const m) : module(m) { withBackground = false; } void updateMeters() override { if (module == nullptr || module->resetMeterOut) return; // Only fetch new values once DSP side is updated gainMeterL = module->meterOutL; gainMeterR = module->meterOutR; module->resetMeterOut = true; } }; // -------------------------------------------------------------------------------------------------------------------- struct IldaeilModuleWidget : ModuleWidgetWithSideScrews<26> { bool hasLeftSideExpander = false; bool hasRightSideExpander = false; IldaeilWidget* ildaeilWidget = nullptr; IldaeilModuleWidget(IldaeilModule* const module) { setModule(module); setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/Ildaeil.svg"))); createAndAddScrews(); if (module == nullptr || module->pcontext != nullptr) { ildaeilWidget = new IldaeilWidget(module); ildaeilWidget->box.pos = Vec(3 * RACK_GRID_WIDTH, 0); ildaeilWidget->box.size = Vec(box.size.x - 6 * RACK_GRID_WIDTH, box.size.y); addChild(ildaeilWidget); } for (uint i=0; ibox.pos = Vec(2.0f, startY + padding * 2); meterIn->box.size = Vec(RACK_GRID_WIDTH * 3 - 2.0f, box.size.y - meterIn->box.pos.y - 19.0f); addChild(meterIn); IldaeilNanoMeterOut* const meterOut = new IldaeilNanoMeterOut(module); meterOut->box.pos = Vec(box.size.x - RACK_GRID_WIDTH * 3 + 1.0f, startY + padding * 2); meterOut->box.size = Vec(RACK_GRID_WIDTH * 3 - 2.0f, box.size.y - meterOut->box.pos.y - 19.0f); addChild(meterOut); } void draw(const DrawArgs& args) override { drawBackground(args.vg); if (hasLeftSideExpander) { nvgBeginPath(args.vg); nvgRect(args.vg, 1, 90 - 19, 18, 49 * 6 - 4); nvgFillPaint(args.vg, nvgLinearGradient(args.vg, 0, 0, 18, 0, nvgRGB(0xd0, 0xd0, 0xd0), nvgRGBA(0xd0, 0xd0, 0xd0, 0))); nvgFill(args.vg); for (int i=1; i<6; ++i) { const float y = 90 + 49 * i - 23; const int col1 = 0x18 + static_cast((y / box.size.y) * (0x21 - 0x18) + 0.5f); const int col2 = 0x19 + static_cast((y / box.size.y) * (0x22 - 0x19) + 0.5f); nvgBeginPath(args.vg); nvgRect(args.vg, 1, y, 18, 4); nvgFillColor(args.vg, nvgRGB(col1, col2, col2)); nvgFill(args.vg); } } if (hasRightSideExpander) { // i == 0 nvgBeginPath(args.vg); nvgRect(args.vg, box.size.x - 19, 90 - 19, 18, 49 - 4); nvgFillColor(args.vg, nvgRGB(0xd0, 0xd0, 0xd0)); nvgFill(args.vg); // gradient for i > 0 nvgBeginPath(args.vg); nvgRect(args.vg, box.size.x - 19, 90 + 49 - 23, 18, 49 * 5); nvgFillPaint(args.vg, nvgLinearGradient(args.vg, box.size.x - 19, 0, box.size.x - 1, 0, nvgRGBA(0xd0, 0xd0, 0xd0, 0), nvgRGB(0xd0, 0xd0, 0xd0))); nvgFill(args.vg); for (int i=1; i<6; ++i) { const float y = 90 + 49 * i - 23; const int col1 = 0x18 + static_cast((y / box.size.y) * (0x21 - 0x18) + 0.5f); const int col2 = 0x19 + static_cast((y / box.size.y) * (0x22 - 0x19) + 0.5f); nvgBeginPath(args.vg); nvgRect(args.vg, box.size.x - 19, y, 18, 4); nvgFillColor(args.vg, nvgRGB(col1, col2, col2)); nvgFill(args.vg); } } drawOutputJacksArea(args.vg, 2); ModuleWidgetWithSideScrews<26>::draw(args); } void step() override { hasLeftSideExpander = module != nullptr && module->leftExpander.module != nullptr && module->leftExpander.module->model == modelExpanderInputMIDI; hasRightSideExpander = module != nullptr && module->rightExpander.module != nullptr && module->rightExpander.module->model == modelExpanderOutputMIDI; ModuleWidgetWithSideScrews<26>::step(); } DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(IldaeilModuleWidget) }; #else static void host_ui_parameter_changed(NativeHostHandle, uint32_t, float) {} static const char* host_ui_open_file(NativeHostHandle, bool, const char*, const char*) { return nullptr; } static void projectLoadedFromDSP(void*) {} struct IldaeilModuleWidget : ModuleWidget { IldaeilModuleWidget(IldaeilModule* const module) { setModule(module); addInput(createInput({}, module, IldaeilModule::INPUT1)); addInput(createInput({}, module, IldaeilModule::INPUT2)); addOutput(createOutput({}, module, IldaeilModule::OUTPUT1)); addOutput(createOutput({}, module, IldaeilModule::OUTPUT2)); } }; #endif // -------------------------------------------------------------------------------------------------------------------- Model* modelIldaeil = createModel("Ildaeil"); // --------------------------------------------------------------------------------------------------------------------