diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 22b1366af8..f5839e40d8 100755
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -17,6 +17,10 @@ if(TARGET score_plugin_media)
add_subdirectory(vstpuppet)
endif()
+if(TARGET score_plugin_vst3)
+ add_subdirectory(vst3puppet)
+endif()
+
add_subdirectory(app)
if(SCORE_PLAYER)
diff --git a/src/vst3puppet/CMakeLists.txt b/src/vst3puppet/CMakeLists.txt
new file mode 100644
index 0000000000..e7ffa2f7ad
--- /dev/null
+++ b/src/vst3puppet/CMakeLists.txt
@@ -0,0 +1,82 @@
+project(vst3puppet CXX)
+find_package(${QT_VERSION} OPTIONAL_COMPONENTS WebSockets)
+
+set(VST3_SDK_ROOT "${3RDPARTY_FOLDER}/vst3")
+if(NOT TARGET ${QT_PREFIX}::WebSockets)
+ message("VST loading requires QtWebSockets.")
+ return()
+endif()
+add_executable(ossia-score-vst3puppet vst3puppet.cpp)
+
+
+if(WIN32)
+ target_sources(ossia-score-vst3puppet PRIVATE
+ "${VST3_SDK_ROOT}/public.sdk/source/vst/hosting/module_win32.cpp"
+ )
+elseif(APPLE)
+ target_sources(ossia-score-vst3puppet PRIVATE
+ "${VST3_SDK_ROOT}/public.sdk/source/vst/hosting/module_mac.mm"
+ )
+else()
+ target_sources(ossia-score-vst3puppet PRIVATE
+ "${VST3_SDK_ROOT}/public.sdk/source/vst/hosting/module_linux.cpp"
+ )
+endif()
+
+target_compile_definitions(ossia-score-vst3puppet PUBLIC HAS_VST3)
+target_link_libraries(
+ ossia-score-vst3puppet
+ PRIVATE
+ ${QT_PREFIX}::Core
+ ${QT_PREFIX}::Gui
+ ${QT_PREFIX}::WebSockets
+ ${CMAKE_DL_LIBS}
+ sdk_common sdk_hosting
+ )
+
+if(APPLE)
+ find_library(Foundation_FK Foundation)
+ target_link_libraries(ossia-score-vst3puppet PRIVATE
+ ${Foundation_FK}
+ )
+endif()
+
+target_include_directories(
+ ossia-score-vst3puppet
+ PRIVATE
+ "${SCORE_SRC}/plugins/score-plugin-media"
+)
+
+setup_score_common_exe_features(ossia-score-vst3puppet)
+
+set_target_properties(
+ ossia-score-vst3puppet
+ PROPERTIES
+ DISABLE_PRECOMPILE_HEADERS TRUE
+)
+
+if(APPLE AND DEPLOYMENT_BUILD)
+ set_target_properties(
+ ossia-score-vst3puppet
+ PROPERTIES
+ MACOSX_BUNDLE TRUE
+ MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.in"
+ RUNTIME_OUTPUT_DIRECTORY score.app/Contents/MacOS)
+ install(
+ TARGETS ossia-score-vst3puppet
+ BUNDLE DESTINATION score.app/Contents/MacOS
+ COMPONENT OssiaScore)
+elseif(WIN32)
+ install(
+ TARGETS ossia-score-vst3puppet
+ RUNTIME DESTINATION .
+ COMPONENT OssiaScore)
+else()
+ install(
+ TARGETS ossia-score-vst3puppet
+ RUNTIME DESTINATION bin
+ COMPONENT OssiaScore)
+endif()
+
+disable_qt_plugins(ossia-score-vst3puppet)
+enable_minimal_qt_plugins(ossia-score-vst3puppet)
diff --git a/src/vst3puppet/Info.plist.in b/src/vst3puppet/Info.plist.in
new file mode 100644
index 0000000000..58095a0cf0
--- /dev/null
+++ b/src/vst3puppet/Info.plist.in
@@ -0,0 +1,40 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ English
+ CFBundleExecutable
+ ossia-score-vst3puppet
+ CFBundleGetInfoString
+ ossia-score-vst3puppet
+ CFBundleIdentifier
+ ossia-score-vst3puppet
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleLongVersionString
+ 1.0
+ CFBundleName
+ ossia-score-vst3puppet
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1.0
+ CSResourcesFileMapped
+
+ LSRequiresCarbon
+
+ NSPrincipalClass
+ NSApplication
+ NSHighResolutionCapable
+
+ LSUIElement
+ 1
+ NSHumanReadableCopyright
+ ossia score
+
+
diff --git a/src/vst3puppet/entitlements.plist b/src/vst3puppet/entitlements.plist
new file mode 100644
index 0000000000..4f4a998976
--- /dev/null
+++ b/src/vst3puppet/entitlements.plist
@@ -0,0 +1,38 @@
+
+
+
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.cs.disable-library-validation
+
+ com.apple.security.assets.music.read-write
+
+ com.apple.security.files.downloads.read-write
+
+ com.apple.security.device.firewire
+
+ com.apple.security.device.microphone
+
+ com.apple.security.device.usb
+
+ com.apple.security.device.audio-input
+
+ com.apple.security.device.camera
+
+ com.apple.security.device.bluetooth
+
+ com.apple.security.network.server
+
+ com.apple.security.network.client
+
+ com.apple.security.files.user-selected.read-write
+
+ com.apple.security.files.bookmarks.app-scope
+
+
+
diff --git a/src/vst3puppet/vst3puppet.cpp b/src/vst3puppet/vst3puppet.cpp
new file mode 100644
index 0000000000..f36e9727b0
--- /dev/null
+++ b/src/vst3puppet/vst3puppet.cpp
@@ -0,0 +1,121 @@
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+using namespace Steinberg;
+QString load_vst(const QString& path, int id)
+{
+ try
+ {
+ bool isFile = QFile(QUrl(path).toString(QUrl::PreferLocalFile)).exists();
+ if (!isFile)
+ {
+ std::cerr << "Invalid path: " << path.toStdString() << std::endl;
+ return {};
+ }
+
+ std::string err;
+ auto module = VST3::Hosting::Module::create(path.toStdString(), err);
+
+ if (!module)
+ {
+ std::cerr << "Failed to load VST3 " << path.toStdString() << err << std::endl;
+ }
+
+ const auto& info = module->getFactory().info();
+ QJsonArray arr;
+ for(const auto& cls : module->getFactory().classInfos())
+ {
+ if (cls.category() == kVstAudioEffectClass)
+ {
+ QJsonObject obj;
+
+ obj["Author"] = QString::fromStdString(cls.vendor());
+ obj["PrettyName"] = QString::fromStdString(cls.name());
+ obj["Subcategories"] = QString::fromStdString(cls.subCategoriesString());
+ obj["Version"] = QString::fromStdString(cls.version());
+ obj["UID"] = QString::fromStdString(cls.ID().toString());
+ obj["Path"] = path;
+ obj["Request"] = id;
+
+ arr.push_back(obj);
+ }
+ }
+ return QJsonDocument{arr}.toJson();
+ }
+ catch (const std::runtime_error& e)
+ {
+ std::cerr << e.what() << std::endl;
+ }
+ return {};
+}
+
+int main(int argc, char** argv)
+{
+ if (argc > 1)
+ {
+ int id = 0;
+ if(argc > 2) {
+ id = QString(argv[2]).toInt();
+ }
+ QGuiApplication app(argc, argv);
+ QWindow w;
+ w.setWidth(1);
+ w.setHeight(1);
+ w.setFlag(Qt::FramelessWindowHint);
+ w.setFlag(Qt::X11BypassWindowManagerHint);
+ w.show();
+
+ QWebSocket socket;
+
+ bool socket_ready{}, vst_ready{};
+ QString json_ret;
+
+ auto onReady = [&] {
+ if(socket_ready && vst_ready) {
+ socket.sendTextMessage(json_ret);
+ socket.flush();
+ socket.close();
+ app.exit(json_ret.isEmpty() ? 1 : 0);
+ }
+ };
+
+ QTimer::singleShot(32, [&] {
+ json_ret = load_vst(argv[1], id);
+ std::cout << json_ret.toStdString();
+ vst_ready = true;
+ onReady();
+ });
+
+ QObject::connect(&socket, &QWebSocket::connected,
+ &app, [&] {
+ socket_ready = true;
+ onReady();
+ });
+
+ QObject::connect(&socket, qOverload(&QWebSocket::error), &app,
+ [&] { qDebug() << socket.errorString(); app.exit(1); });
+ QObject::connect(&socket, &QWebSocket::disconnected, &app,
+ [&] { qDebug() << socket.errorString(); app.exit(1); });
+
+ QTimer::singleShot(10000, [&] { qDebug() << "timeout"; qApp->exit(1); });
+
+ socket.open(QUrl("ws://127.0.0.1:37587"));
+ app.exec();
+ }
+ return 1;
+}