diff --git a/source/backend/plugin/CarlaPlugin.cpp b/source/backend/plugin/CarlaPlugin.cpp index ae59d12bf..8a73eaa07 100644 --- a/source/backend/plugin/CarlaPlugin.cpp +++ b/source/backend/plugin/CarlaPlugin.cpp @@ -30,6 +30,7 @@ using juce::CharPointer_UTF8; using juce::File; using juce::MemoryOutputStream; +using juce::Result; using juce::ScopedPointer; using juce::String; using juce::XmlDocument; @@ -922,9 +923,200 @@ bool CarlaPlugin::exportAsLV2(const char* const lv2path) CARLA_SAFE_ASSERT_RETURN(lv2path != nullptr && lv2path[0] != '\0', false); carla_debug("CarlaPlugin::exportAsLV2(\"%s\")", lv2path); - // TODO + CarlaString bundlepath(lv2path); - return false; + if (! bundlepath.endsWith(".lv2")) + bundlepath += ".lv2"; + + const File bundlefolder(bundlepath.buffer()); + + if (bundlefolder.existsAsFile()) + { + pData->engine->setLastError("Requested filename already exists as file, use a folder instead"); + return false; + } + + if (! bundlefolder.exists()) + { + const Result res(bundlefolder.createDirectory()); + + if (res.failed()) + { + pData->engine->setLastError(res.getErrorMessage().toRawUTF8()); + return false; + } + } + + CarlaString symbol(pData->name); + symbol.toBasic(); + + char strBufName[STR_MAX+1]; + char strBufSymbol[STR_MAX+1]; + strBufName[STR_MAX] = strBufSymbol[STR_MAX] = '\0'; + + { + const CarlaString pluginFilename(bundlepath + CARLA_OS_SEP_STR + symbol + ".xml"); + + if (! saveStateToFile(pluginFilename)) + return false; + } + + { + MemoryOutputStream manifestStream; + + manifestStream << "@prefix lv2: .\n"; + manifestStream << "@prefix rdfs: .\n"; + manifestStream << "\n"; + manifestStream << "<" << symbol.buffer() << ".ttl>\n"; + manifestStream << " a lv2:Plugin ;\n"; + manifestStream << " lv2:binary <" << symbol.buffer() << ".so> ;\n"; + manifestStream << " rdfs:seeAlso <" << symbol.buffer() << ".ttl> .\n"; + + const CarlaString manifestFilename(bundlepath + CARLA_OS_SEP_STR "manifest.ttl"); + const File manifestFile(manifestFilename.buffer()); + + if (! manifestFile.replaceWithData(manifestStream.getData(), manifestStream.getDataSize())) + { + pData->engine->setLastError("Failed to write manifest.ttl file"); + return false; + } + } + + { + MemoryOutputStream mainStream; + + mainStream << "@prefix doap: .\n"; + mainStream << "@prefix lv2: .\n"; + mainStream << "@prefix rdfs: .\n"; + mainStream << "\n"; + mainStream << "<>\n"; + mainStream << " a lv2:Plugin ;\n"; + mainStream << "\n"; + mainStream << " lv2:requiredFeature ,\n"; + mainStream << " ,\n"; + mainStream << " ;\n"; + mainStream << "\n"; + + int portIndex = 0; + + for (uint32_t i=0; iaudioIn.count; ++i) + { + const String portIndexNum(portIndex++); + const String portIndexLabel(portIndex); + + mainStream << " lv2:port [\n"; + mainStream << " a lv2:InputPort, lv2:AudioPort ;\n"; + mainStream << " lv2:index " << portIndexNum << " ;\n"; + mainStream << " lv2:symbol \"lv2_audio_in_" << portIndexLabel << "\" ;\n"; + mainStream << " lv2:name \"Audio Input " << portIndexLabel << "\" ;\n"; + mainStream << " ] ;\n"; + } + + for (uint32_t i=0; iaudioOut.count; ++i) + { + const String portIndexNum(portIndex++); + const String portIndexLabel(portIndex); + + mainStream << " lv2:port [\n"; + mainStream << " a lv2:OutputPort, lv2:AudioPort ;\n"; + mainStream << " lv2:index " << portIndexNum << " ;\n"; + mainStream << " lv2:symbol \"lv2_audio_out_" << portIndexLabel << "\" ;\n"; + mainStream << " lv2:name \"Audio Output " << portIndexLabel << "\" ;\n"; + mainStream << " ] ;\n"; + } + + mainStream << " lv2:port [\n"; + mainStream << " a lv2:InputPort, lv2:ControlPort ;\n"; + mainStream << " lv2:index " << String(portIndex++) << " ;\n"; + mainStream << " lv2:name \"freewheel\" ;\n"; + mainStream << " lv2:symbol \"freewheel\" ;\n"; + mainStream << " lv2:default 0 ;\n"; + mainStream << " lv2:minimum 0 ;\n"; + mainStream << " lv2:maximum 1 ;\n"; + mainStream << " lv2:portProperty lv2:toggled , lv2:integer;\n"; + // TODO designation, hidegui + mainStream << " ] ;\n"; + + for (uint32_t i=0; iparam.count; ++i) + { + const ParameterData& paramData(pData->param.data[i]); + const ParameterRanges& paramRanges(pData->param.ranges[i]); + + const String portIndexNum(portIndex++); + const String portIndexLabel(portIndex); + + mainStream << " lv2:port [\n"; + + if (paramData.type == PARAMETER_INPUT) + mainStream << " a lv2:InputPort, lv2:ControlPort ;\n"; + else + mainStream << " a lv2:OutputPort, lv2:ControlPort ;\n"; + + if (paramData.hints & PARAMETER_IS_BOOLEAN) + mainStream << " lv2:portProperty lv2:toggled ;\n"; + + if (paramData.hints & PARAMETER_IS_INTEGER) + mainStream << " lv2:portProperty lv2:integer ;\n"; + + // TODO logarithmic, enabled (not on gui), automable, samplerate, scalepoints + + strBufName[0] = strBufSymbol[0] = '\0'; + getParameterName(i, strBufName); + getParameterSymbol(i, strBufSymbol); + + if (strBufSymbol[0] == '\0') + { + CarlaString s(strBufName); + s.toBasic(); + std::memcpy(strBufSymbol, s.buffer(), s.length()+1); + } + + mainStream << " lv2:index " << portIndexNum << " ;\n"; + mainStream << " lv2:symbol \"" << strBufSymbol << "\" ;\n"; + mainStream << " lv2:name \"\"\"" << strBufName << "\"\"\" ;\n"; + mainStream << " lv2:default " << String(paramRanges.def) << " ;\n"; + mainStream << " lv2:minimum " << String(paramRanges.min) << " ;\n"; + mainStream << " lv2:maximum " << String(paramRanges.max) << " ;\n"; + + // TODO midiCC, midiChannel + + mainStream << " ] ;\n"; + } + + mainStream << " rdfs:comment \"Plugin generated using Carla LV2 export.\" ;\n"; + mainStream << " doap:name \"\"\"" << getName() << "\"\"\" .\n"; + mainStream << "\n"; + + const CarlaString mainFilename(bundlepath + CARLA_OS_SEP_STR + symbol + ".ttl"); + const File mainFile(mainFilename.buffer()); + + if (! mainFile.replaceWithData(mainStream.getData(), mainStream.getDataSize())) + { + pData->engine->setLastError("Failed to write main plugin ttl file"); + return false; + } + } + + const CarlaString binaryFilename(bundlepath + CARLA_OS_SEP_STR + symbol + ".so"); + + const File binaryFileSource(File::getSpecialLocation(File::currentExecutableFile).getSiblingFile("carla-lv2-single.so")); + const File binaryFileTarget(binaryFilename.buffer()); + + if (! binaryFileSource.createSymbolicLink(binaryFileTarget, true)) + { + pData->engine->setLastError("Failed to create symbolik link of plugin binary"); + return false; + } + + const EngineOptions& opts(pData->engine->getOptions()); + + const CarlaString binFolderTarget(bundlepath + CARLA_OS_SEP_STR + "bin"); + const CarlaString resFolderTarget(bundlepath + CARLA_OS_SEP_STR + "res"); + + File(opts.binaryDir).createSymbolicLink(File(binFolderTarget.buffer()), true); + File(opts.resourceDir).createSymbolicLink(File(resFolderTarget.buffer()), true); + + return true; } // ------------------------------------------------------------------- diff --git a/source/carla_skin.py b/source/carla_skin.py index 3780bd41e..32349ab5c 100755 --- a/source/carla_skin.py +++ b/source/carla_skin.py @@ -1040,6 +1040,10 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta): if self.fIdleTimerId != 0: actRemove.setVisible(False) + menu.addSeparator() + + actExportLV2 = menu.addAction(self.tr("Export LV2...")) + # ------------------------------------------------------------- # exec @@ -1113,6 +1117,21 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta): self.host.get_last_error(), QMessageBox.Ok, QMessageBox.Ok) # ------------------------------------------------------------- + # Export LV2 + + elif actSel == actExportLV2: + ret = QFileDialog.getSaveFileName(self, self.tr("Export Plugin as LV2"), "", "", QFileDialog.ShowDirsOnly|QFileDialog.HideNameFilterDetails) + + if config_UseQt5: + ret = ret[0] + if not ret: + return + + if not self.host.export_plugin_lv2(self.fPluginId, ret): + CustomMessageBox(self, QMessageBox.Warning, self.tr("Error"), self.tr("Operation failed"), + self.host.get_last_error(), QMessageBox.Ok, QMessageBox.Ok) + + # ------------------------------------------------------------- @pyqtSlot() def slot_knobCustomMenu(self): diff --git a/source/libjack/libjack.cpp b/source/libjack/libjack.cpp index d09d5d5bd..0ec37d57b 100644 --- a/source/libjack/libjack.cpp +++ b/source/libjack/libjack.cpp @@ -853,6 +853,7 @@ void CarlaJackAppClient::runNonRealtimeThread() if (quitReceived) break; + /* if (fLastPingTime > 0 && Time::currentTimeMillis() > fLastPingTime + 30000) { carla_stderr("Did not receive ping message from server for 30 secs, closing..."); @@ -861,6 +862,7 @@ void CarlaJackAppClient::runNonRealtimeThread() fRealtimeThread.signalThreadShouldExit(); break; } + */ } //callback(ENGINE_CALLBACK_ENGINE_STOPPED, 0, 0, 0, 0.0f, nullptr); diff --git a/source/plugin/carla-lv2-single.cpp b/source/plugin/carla-lv2-single.cpp index a3f3305ad..96398f1c1 100644 --- a/source/plugin/carla-lv2-single.cpp +++ b/source/plugin/carla-lv2-single.cpp @@ -38,8 +38,11 @@ public: fIsOffline(false) { // xxxxx + CarlaString binaryDir(bundlePath); + binaryDir += CARLA_OS_SEP_STR "bin" CARLA_OS_SEP_STR; + CarlaString resourceDir(bundlePath); - resourceDir += CARLA_OS_SEP_STR "resources" CARLA_OS_SEP_STR; + resourceDir += CARLA_OS_SEP_STR "res" CARLA_OS_SEP_STR; pData->bufferSize = bufferSize; pData->sampleRate = sampleRate; @@ -57,12 +60,15 @@ public: if (pData->options.binaryDir != nullptr) delete[] pData->options.binaryDir; + pData->options.binaryDir = binaryDir.dup(); pData->options.resourceDir = resourceDir.dup(); - pData->options.binaryDir = carla_strdup(carla_get_library_folder()); setCallback(_engine_callback, this); - if (! addPlugin(BINARY_NATIVE, PLUGIN_VST2, "/usr/lib/vst/3BandEQ-vst.so", nullptr, nullptr, 0, nullptr, 0x0)) + using juce::File; + const File pluginFile(File::getSpecialLocation(File::currentExecutableFile).withFileExtension("xml")); + + if (! loadProject(pluginFile.getFullPathName().toRawUTF8())) { carla_stderr2("Failed to init plugin, possible reasons: %s", getLastError()); return; @@ -462,7 +468,13 @@ static LV2_Handle lv2_instantiate(const LV2_Descriptor* lv2Descriptor, double sa return nullptr; } - return new CarlaEngineLV2Single(bufferSize, sampleRate, bundlePath, uridMap); + CarlaEngineLV2Single* const instance(new CarlaEngineLV2Single(bufferSize, sampleRate, bundlePath, uridMap)); + + if (instance->hasPlugin()) + return (LV2_Handle)instance; + + delete instance; + return nullptr; } #define instancePtr ((CarlaEngineLV2Single*)instance)