@@ -0,0 +1,113 @@ | |||||
{ | |||||
"slug": "Core", | |||||
"name": "Core", | |||||
"version": "1.0.0", | |||||
"license": "GPL-3.0-only", | |||||
"author": "VCV", | |||||
"brand": "VCV", | |||||
"authorEmail": "contact@vcvrack.com", | |||||
"authorUrl": "https://vcvrack.com/", | |||||
"pluginUrl": "https://vcvrack.com/", | |||||
"manualUrl": "https://vcvrack.com/manual/Core.html", | |||||
"sourceUrl": "https://github.com/VCVRack/Rack", | |||||
"donateUrl": "", | |||||
"modules": [ | |||||
{ | |||||
"slug": "AudioInterface", | |||||
"name": "Audio-8", | |||||
"description": "Sends audio and CV to/from an audio device", | |||||
"tags": [ | |||||
"External" | |||||
] | |||||
}, | |||||
{ | |||||
"slug": "AudioInterface16", | |||||
"name": "Audio-16", | |||||
"description": "Sends audio and CV to/from an audio device", | |||||
"tags": [ | |||||
"External" | |||||
] | |||||
}, | |||||
{ | |||||
"slug": "MIDIToCVInterface", | |||||
"name": "MIDI-CV", | |||||
"description": "Converts MIDI from an external device to CV and gates", | |||||
"tags": [ | |||||
"External", | |||||
"MIDI", | |||||
"Polyphonic" | |||||
] | |||||
}, | |||||
{ | |||||
"slug": "MIDICCToCVInterface", | |||||
"name": "MIDI-CC", | |||||
"description": "Converts MIDI CC from an external device to CV", | |||||
"tags": [ | |||||
"External", | |||||
"MIDI" | |||||
] | |||||
}, | |||||
{ | |||||
"slug": "MIDITriggerToCVInterface", | |||||
"name": "MIDI-Gate", | |||||
"description": "Converts MIDI notes from an external device to gates", | |||||
"tags": [ | |||||
"External", | |||||
"MIDI" | |||||
] | |||||
}, | |||||
{ | |||||
"slug": "MIDI-Map", | |||||
"name": "MIDI-Map", | |||||
"description": "Controls parameters (knobs, sliders, switches) directly with MIDI CC", | |||||
"tags": [ | |||||
"External", | |||||
"MIDI" | |||||
] | |||||
}, | |||||
{ | |||||
"slug": "CV-MIDI", | |||||
"name": "CV-MIDI", | |||||
"description": "Converts CV to MIDI and sends to an external device", | |||||
"tags": [ | |||||
"External", | |||||
"MIDI", | |||||
"Polyphonic" | |||||
] | |||||
}, | |||||
{ | |||||
"slug": "CV-CC", | |||||
"name": "CV-CC", | |||||
"description": "Converts CV to MIDI CC and sends to an external device", | |||||
"tags": [ | |||||
"External", | |||||
"MIDI" | |||||
] | |||||
}, | |||||
{ | |||||
"slug": "CV-Gate", | |||||
"name": "CV-Gate", | |||||
"description": "Converts gates to MIDI notes and sends to an external device", | |||||
"tags": [ | |||||
"External", | |||||
"MIDI" | |||||
] | |||||
}, | |||||
{ | |||||
"slug": "Blank", | |||||
"name": "Blank", | |||||
"description": "A resizable blank panel", | |||||
"tags": [ | |||||
"Blank" | |||||
] | |||||
}, | |||||
{ | |||||
"slug": "Notes", | |||||
"name": "Notes", | |||||
"description": "Write text for patch notes or artist attribution", | |||||
"tags": [ | |||||
"Blank" | |||||
] | |||||
} | |||||
] | |||||
} |
@@ -14,8 +14,9 @@ namespace app { | |||||
extern std::string APP_NAME; | extern std::string APP_NAME; | ||||
extern std::string APP_VERSION; | extern std::string APP_VERSION; | ||||
extern std::string APP_NEW_VERSION; | |||||
extern std::string APP_VERSION_UPDATE; | |||||
extern std::string API_URL; | extern std::string API_URL; | ||||
extern std::string API_VERSION; | |||||
static const float SVG_DPI = 75.0; | static const float SVG_DPI = 75.0; | ||||
static const float MM_PER_IN = 25.4; | static const float MM_PER_IN = 25.4; | ||||
@@ -14,13 +14,21 @@ namespace rack { | |||||
namespace plugin { | namespace plugin { | ||||
struct Update { | |||||
std::string pluginSlug; | |||||
std::string version; | |||||
std::string changelogUrl; | |||||
float progress = 0.f; | |||||
}; | |||||
void init(); | void init(); | ||||
void destroy(); | void destroy(); | ||||
void logIn(const std::string &email, const std::string &password); | void logIn(const std::string &email, const std::string &password); | ||||
void logOut(); | void logOut(); | ||||
void queryUpdates(); | void queryUpdates(); | ||||
void syncUpdate(Update *update); | |||||
void syncUpdates(); | void syncUpdates(); | ||||
void cancelDownload(); | |||||
bool isLoggedIn(); | bool isLoggedIn(); | ||||
Plugin *getPlugin(const std::string &pluginSlug); | Plugin *getPlugin(const std::string &pluginSlug); | ||||
Model *getModel(const std::string &pluginSlug, const std::string &modelSlug); | Model *getModel(const std::string &pluginSlug, const std::string &modelSlug); | ||||
@@ -31,20 +39,11 @@ bool isSlugValid(const std::string &slug); | |||||
std::string normalizeSlug(const std::string &slug); | std::string normalizeSlug(const std::string &slug); | ||||
struct Update { | |||||
std::string pluginSlug; | |||||
std::string version; | |||||
std::string changelogUrl; | |||||
}; | |||||
extern const std::set<std::string> allowedTags; | extern const std::set<std::string> allowedTags; | ||||
extern std::vector<Plugin*> plugins; | extern std::vector<Plugin*> plugins; | ||||
extern std::string loginStatus; | extern std::string loginStatus; | ||||
extern std::vector<Update> updates; | extern std::vector<Update> updates; | ||||
extern float downloadProgress; | |||||
extern std::string downloadName; | |||||
} // namespace plugin | } // namespace plugin | ||||
@@ -2,70 +2,15 @@ | |||||
void init(rack::Plugin *p) { | void init(rack::Plugin *p) { | ||||
p->slug = "Core"; | |||||
p->version = TOSTRING(VERSION); | |||||
p->license = "BSD-3-Clause"; | |||||
p->name = "Core"; | |||||
p->brand = "Core"; | |||||
p->author = "VCV"; | |||||
p->authorEmail = "contact@vcvrack.com"; | |||||
p->authorUrl = "https://vcvrack.com/"; | |||||
p->pluginUrl = "https://vcvrack.com/"; | |||||
p->manualUrl = "https://vcvrack.com/manual/Core.html"; | |||||
p->sourceUrl = "https://github.com/VCVRack/Rack"; | |||||
modelAudioInterface->name = "Audio-8"; | |||||
modelAudioInterface->description = "Sends audio and CV to/from an audio device"; | |||||
modelAudioInterface->tags = {"External"}; | |||||
p->addModel(modelAudioInterface); | p->addModel(modelAudioInterface); | ||||
modelAudioInterface16->name = "Audio-16"; | |||||
modelAudioInterface16->description = "Sends audio and CV to/from an audio device"; | |||||
modelAudioInterface16->tags = {"External"}; | |||||
p->addModel(modelAudioInterface16); | p->addModel(modelAudioInterface16); | ||||
modelMIDI_CV->name = "MIDI-CV"; | |||||
modelMIDI_CV->description = "Converts MIDI from an external device to CV and gates"; | |||||
modelMIDI_CV->tags = {"External", "MIDI"}; | |||||
p->addModel(modelMIDI_CV); | p->addModel(modelMIDI_CV); | ||||
modelMIDI_CC->name = "MIDI-CC"; | |||||
modelMIDI_CC->description = "Converts MIDI CC from an external device to CV"; | |||||
modelMIDI_CC->tags = {"External", "MIDI"}; | |||||
p->addModel(modelMIDI_CC); | p->addModel(modelMIDI_CC); | ||||
modelMIDI_Gate->name = "MIDI-Gate"; | |||||
modelMIDI_Gate->description = "Converts MIDI notes from an external device to gates"; | |||||
modelMIDI_Gate->tags = {"External", "MIDI"}; | |||||
p->addModel(modelMIDI_Gate); | p->addModel(modelMIDI_Gate); | ||||
modelMIDI_Map->name = "MIDI-Map"; | |||||
modelMIDI_Map->description = ""; | |||||
modelMIDI_Map->tags = {"External", "MIDI"}; | |||||
p->addModel(modelMIDI_Map); | p->addModel(modelMIDI_Map); | ||||
modelCV_MIDI->name = "CV-MIDI"; | |||||
modelCV_MIDI->description = "Converts CV to MIDI and sends to an external device"; | |||||
modelCV_MIDI->tags = {"External", "MIDI"}; | |||||
p->addModel(modelCV_MIDI); | p->addModel(modelCV_MIDI); | ||||
modelCV_CC->name = "CV-CC"; | |||||
modelCV_CC->description = "Converts CV to MIDI CC and sends to an external device"; | |||||
modelCV_CC->tags = {"External", "MIDI"}; | |||||
p->addModel(modelCV_CC); | p->addModel(modelCV_CC); | ||||
modelCV_Gate->name = "CV-Gate"; | |||||
modelCV_Gate->description = "Converts gates to MIDI notes and sends to an external device"; | |||||
modelCV_Gate->tags = {"External", "MIDI"}; | |||||
p->addModel(modelCV_Gate); | p->addModel(modelCV_Gate); | ||||
modelBlank->name = "Blank"; | |||||
modelBlank->description = "A resizable blank panel"; | |||||
modelBlank->tags = {"Blank"}; | |||||
p->addModel(modelBlank); | p->addModel(modelBlank); | ||||
modelNotes->name = "Notes"; | |||||
modelNotes->description = "Write text for patch notes or artist attribution"; | |||||
modelNotes->tags = {"Blank"}; | |||||
p->addModel(modelNotes); | p->addModel(modelNotes); | ||||
} | } |
@@ -188,7 +188,7 @@ struct EditButton : MenuButton { | |||||
menu->addChild(redoItem); | menu->addChild(redoItem); | ||||
DisconnectCablesItem *disconnectCablesItem = new DisconnectCablesItem; | DisconnectCablesItem *disconnectCablesItem = new DisconnectCablesItem; | ||||
disconnectCablesItem->text = "Disconnect cables"; | |||||
disconnectCablesItem->text = "Clear cables"; | |||||
menu->addChild(disconnectCablesItem); | menu->addChild(disconnectCablesItem); | ||||
} | } | ||||
}; | }; | ||||
@@ -459,6 +459,7 @@ struct AccountEmailField : ui::TextField { | |||||
struct AccountPasswordField : ui::PasswordField { | struct AccountPasswordField : ui::PasswordField { | ||||
ui::MenuItem *logInItem; | ui::MenuItem *logInItem; | ||||
void onSelectKey(const event::SelectKey &e) override { | void onSelectKey(const event::SelectKey &e) override { | ||||
if (e.action == GLFW_PRESS && (e.key == GLFW_KEY_ENTER || e.key == GLFW_KEY_KP_ENTER)) { | if (e.action == GLFW_PRESS && (e.key == GLFW_KEY_ENTER || e.key == GLFW_KEY_KP_ENTER)) { | ||||
logInItem->doAction(); | logInItem->doAction(); | ||||
@@ -489,6 +490,7 @@ struct LogInItem : ui::MenuItem { | |||||
disabled = isLoggingIn; | disabled = isLoggingIn; | ||||
text = "Log in"; | text = "Log in"; | ||||
rightText = plugin::loginStatus; | rightText = plugin::loginStatus; | ||||
MenuItem::step(); | |||||
} | } | ||||
}; | }; | ||||
@@ -498,9 +500,57 @@ struct SyncItem : ui::MenuItem { | |||||
plugin::syncUpdates(); | plugin::syncUpdates(); | ||||
}); | }); | ||||
t.detach(); | t.detach(); | ||||
e.consume(NULL); | |||||
} | |||||
}; | |||||
struct PluginSyncItem : ui::MenuItem { | |||||
plugin::Update *update; | |||||
void setUpdate(plugin::Update *update) { | |||||
this->update = update; | |||||
text = update->pluginSlug; | |||||
plugin::Plugin *p = plugin::getPlugin(update->pluginSlug); | |||||
if (p) { | |||||
rightText += "v" + p->version + " → "; | |||||
} | |||||
rightText += "v" + update->version; | |||||
} | |||||
ui::Menu *createChildMenu() override { | |||||
if (update->changelogUrl != "") { | |||||
ui::Menu *menu = new ui::Menu; | |||||
UrlItem *changelogUrl = new UrlItem; | |||||
changelogUrl->text = "Changelog"; | |||||
changelogUrl->url = update->changelogUrl; | |||||
menu->addChild(changelogUrl); | |||||
return menu; | |||||
} | |||||
return NULL; | |||||
} | |||||
void step() override { | |||||
if (update->progress >= 1) { | |||||
rightText = CHECKMARK_STRING; | |||||
} | |||||
else if (update->progress > 0) { | |||||
rightText = string::f("%.0f%%", update->progress * 100.f); | |||||
} | |||||
MenuItem::step(); | |||||
} | |||||
void onAction(const event::Action &e) override { | |||||
std::thread t([=]() { | |||||
plugin::syncUpdate(update); | |||||
}); | |||||
t.detach(); | |||||
e.consume(NULL); | |||||
} | } | ||||
}; | }; | ||||
#if 0 | #if 0 | ||||
struct SyncButton : ui::Button { | struct SyncButton : ui::Button { | ||||
bool checked = false; | bool checked = false; | ||||
@@ -544,46 +594,48 @@ struct LogOutItem : ui::MenuItem { | |||||
} | } | ||||
}; | }; | ||||
struct DownloadQuantity : Quantity { | |||||
float getValue() override { | |||||
return plugin::downloadProgress; | |||||
} | |||||
float getDisplayValue() override { | |||||
return getValue() * 100.f; | |||||
} | |||||
int getDisplayPrecision() override {return 0;} | |||||
std::string getLabel() override { | |||||
return "Downloading " + plugin::downloadName; | |||||
} | |||||
std::string getUnit() override {return "%";} | |||||
}; | |||||
struct PluginsMenu : ui::Menu { | struct PluginsMenu : ui::Menu { | ||||
int state = 0; | |||||
bool loggedIn = false; | |||||
PluginsMenu() { | PluginsMenu() { | ||||
refresh(); | refresh(); | ||||
} | } | ||||
void step() override { | void step() override { | ||||
if (!loggedIn && plugin::isLoggedIn()) | |||||
refresh(); | |||||
Menu::step(); | Menu::step(); | ||||
} | } | ||||
void refresh() { | void refresh() { | ||||
clearChildren(); | clearChildren(); | ||||
{ | |||||
ui::MenuLabel *disabledLable = new ui::MenuLabel; | |||||
disabledLable->text = "Server not yet available"; | |||||
addChild(disabledLable); | |||||
return; | |||||
if (!plugin::isLoggedIn()) { | |||||
UrlItem *registerItem = new UrlItem; | |||||
registerItem->text = "Register VCV account"; | |||||
registerItem->url = "https://vcvrack.com/"; | |||||
addChild(registerItem); | |||||
AccountEmailField *emailField = new AccountEmailField; | |||||
emailField->placeholder = "Email"; | |||||
emailField->box.size.x = 240.0; | |||||
addChild(emailField); | |||||
AccountPasswordField *passwordField = new AccountPasswordField; | |||||
passwordField->placeholder = "Password"; | |||||
passwordField->box.size.x = 240.0; | |||||
emailField->passwordField = passwordField; | |||||
addChild(passwordField); | |||||
LogInItem *logInItem = new LogInItem; | |||||
logInItem->emailField = emailField; | |||||
logInItem->passwordField = passwordField; | |||||
passwordField->logInItem = logInItem; | |||||
addChild(logInItem); | |||||
} | } | ||||
else { | |||||
loggedIn = true; | |||||
if (plugin::isLoggedIn()) { | |||||
UrlItem *manageItem = new UrlItem; | UrlItem *manageItem = new UrlItem; | ||||
manageItem->text = "Manage"; | manageItem->text = "Manage"; | ||||
manageItem->url = "https://vcvrack.com/plugins.html"; | manageItem->url = "https://vcvrack.com/plugins.html"; | ||||
@@ -602,49 +654,20 @@ struct PluginsMenu : ui::Menu { | |||||
addChild(new ui::MenuEntry); | addChild(new ui::MenuEntry); | ||||
ui::MenuLabel *updatesLabel = new ui::MenuLabel; | ui::MenuLabel *updatesLabel = new ui::MenuLabel; | ||||
updatesLabel->text = "Updates (click for changelog)"; | |||||
updatesLabel->text = "Updates"; | |||||
addChild(updatesLabel); | addChild(updatesLabel); | ||||
for (const plugin::Update &update : plugin::updates) { | |||||
UrlItem *updateItem = new UrlItem; | |||||
updateItem->text = update.pluginSlug; | |||||
plugin::Plugin *p = plugin::getPlugin(update.pluginSlug); | |||||
if (p) { | |||||
updateItem->rightText += "v" + p->version + " → "; | |||||
} | |||||
updateItem->rightText += "v" + update.version; | |||||
updateItem->url = update.changelogUrl; | |||||
updateItem->disabled = update.changelogUrl.empty(); | |||||
for (plugin::Update &update : plugin::updates) { | |||||
PluginSyncItem *updateItem = new PluginSyncItem; | |||||
updateItem->setUpdate(&update); | |||||
addChild(updateItem); | addChild(updateItem); | ||||
} | } | ||||
} | } | ||||
} | } | ||||
else { | |||||
UrlItem *registerItem = new UrlItem; | |||||
registerItem->text = "Register VCV account"; | |||||
registerItem->url = "https://vcvrack.com/"; | |||||
addChild(registerItem); | |||||
AccountEmailField *emailField = new AccountEmailField; | |||||
emailField->placeholder = "Email"; | |||||
emailField->box.size.x = 220.0; | |||||
addChild(emailField); | |||||
AccountPasswordField *passwordField = new AccountPasswordField; | |||||
passwordField->placeholder = "Password"; | |||||
passwordField->box.size.x = 220.0; | |||||
emailField->passwordField = passwordField; | |||||
addChild(passwordField); | |||||
LogInItem *logInItem = new LogInItem; | |||||
logInItem->emailField = emailField; | |||||
logInItem->passwordField = passwordField; | |||||
passwordField->logInItem = logInItem; | |||||
addChild(logInItem); | |||||
} | |||||
} | } | ||||
}; | }; | ||||
struct PluginsButton : MenuButton { | struct PluginsButton : MenuButton { | ||||
NotificationIcon *notification; | NotificationIcon *notification; | ||||
@@ -704,7 +727,7 @@ struct HelpButton : MenuButton { | |||||
if (hasUpdate()) { | if (hasUpdate()) { | ||||
UrlItem *updateItem = new UrlItem; | UrlItem *updateItem = new UrlItem; | ||||
updateItem->text = "Update " + APP_NAME; | updateItem->text = "Update " + APP_NAME; | ||||
updateItem->rightText = APP_VERSION + " → " + APP_NEW_VERSION; | |||||
updateItem->rightText = APP_VERSION + " → " + APP_VERSION_UPDATE; | |||||
updateItem->url = "https://vcvrack.com/"; | updateItem->url = "https://vcvrack.com/"; | ||||
menu->addChild(updateItem); | menu->addChild(updateItem); | ||||
} | } | ||||
@@ -721,7 +744,7 @@ struct HelpButton : MenuButton { | |||||
} | } | ||||
bool hasUpdate() { | bool hasUpdate() { | ||||
return !APP_NEW_VERSION.empty() && APP_NEW_VERSION != APP_VERSION; | |||||
return !APP_VERSION_UPDATE.empty() && APP_VERSION_UPDATE != APP_VERSION; | |||||
} | } | ||||
}; | }; | ||||
@@ -10,8 +10,10 @@ namespace app { | |||||
std::string APP_NAME = "VCV Rack"; | std::string APP_NAME = "VCV Rack"; | ||||
std::string APP_VERSION = TOSTRING(VERSION); | std::string APP_VERSION = TOSTRING(VERSION); | ||||
std::string APP_NEW_VERSION; | |||||
std::string APP_VERSION_UPDATE; | |||||
std::string API_URL = "https://api.vcvrack.com"; | std::string API_URL = "https://api.vcvrack.com"; | ||||
std::string API_VERSION = "1"; | |||||
static void checkVersion() { | static void checkVersion() { | ||||
std::string versionUrl = app::API_URL + "/version"; | std::string versionUrl = app::API_URL + "/version"; | ||||
@@ -26,7 +28,7 @@ static void checkVersion() { | |||||
json_t *versionJ = json_object_get(versionResJ, "version"); | json_t *versionJ = json_object_get(versionResJ, "version"); | ||||
if (versionJ) | if (versionJ) | ||||
APP_NEW_VERSION = json_string_value(versionJ); | |||||
APP_VERSION_UPDATE = json_string_value(versionJ); | |||||
} | } | ||||
void init() { | void init() { | ||||
@@ -22,11 +22,11 @@ | |||||
#include <jansson.h> | #include <jansson.h> | ||||
#if defined ARCH_WIN | #if defined ARCH_WIN | ||||
#include <windows.h> | |||||
#include <direct.h> | |||||
#define mkdir(_dir, _perms) _mkdir(_dir) | |||||
#include <windows.h> | |||||
#include <direct.h> | |||||
#define mkdir(_dir, _perms) _mkdir(_dir) | |||||
#else | #else | ||||
#include <dlfcn.h> | |||||
#include <dlfcn.h> | |||||
#endif | #endif | ||||
#include <dirent.h> | #include <dirent.h> | ||||
#include <osdialog.h> | #include <osdialog.h> | ||||
@@ -40,42 +40,22 @@ namespace plugin { | |||||
// private API | // private API | ||||
//////////////////// | //////////////////// | ||||
static bool loadPlugin(std::string path) { | |||||
// Load plugin.json | |||||
std::string metadataFilename = path + "/plugin.json"; | |||||
FILE *file = fopen(metadataFilename.c_str(), "r"); | |||||
if (!file) { | |||||
WARN("Plugin metadata file %s does not exist", metadataFilename.c_str()); | |||||
return false; | |||||
} | |||||
DEFER({ | |||||
fclose(file); | |||||
}); | |||||
json_error_t error; | |||||
json_t *rootJ = json_loadf(file, 0, &error); | |||||
if (!rootJ) { | |||||
WARN("JSON parsing error at %s %d:%d %s", error.source, error.line, error.column, error.text); | |||||
return false; | |||||
} | |||||
DEFER({ | |||||
json_decref(rootJ); | |||||
}); | |||||
typedef void (*InitCallback)(Plugin*); | |||||
static InitCallback loadLibrary(Plugin *plugin) { | |||||
// Load plugin library | // Load plugin library | ||||
std::string libraryFilename; | std::string libraryFilename; | ||||
#if defined ARCH_LIN | #if defined ARCH_LIN | ||||
libraryFilename = path + "/" + "plugin.so"; | |||||
libraryFilename = plugin->path + "/" + "plugin.so"; | |||||
#elif defined ARCH_WIN | #elif defined ARCH_WIN | ||||
libraryFilename = path + "/" + "plugin.dll"; | |||||
libraryFilename = plugin->path + "/" + "plugin.dll"; | |||||
#elif ARCH_MAC | #elif ARCH_MAC | ||||
libraryFilename = path + "/" + "plugin.dylib"; | |||||
libraryFilename = plugin->path + "/" + "plugin.dylib"; | |||||
#endif | #endif | ||||
// Check file existence | // Check file existence | ||||
if (!system::isFile(libraryFilename)) { | if (!system::isFile(libraryFilename)) { | ||||
WARN("Plugin file %s does not exist", libraryFilename.c_str()); | |||||
return false; | |||||
throw UserException(string::f("Library %s does not exist", libraryFilename.c_str())); | |||||
} | } | ||||
// Load dynamic/shared library | // Load dynamic/shared library | ||||
@@ -85,19 +65,17 @@ static bool loadPlugin(std::string path) { | |||||
SetErrorMode(0); | SetErrorMode(0); | ||||
if (!handle) { | if (!handle) { | ||||
int error = GetLastError(); | int error = GetLastError(); | ||||
WARN("Failed to load library %s: code %d", libraryFilename.c_str(), error); | |||||
return false; | |||||
throw UserException(string::f("Failed to load library %s: code %d", libraryFilename.c_str(), error)); | |||||
} | } | ||||
#else | #else | ||||
void *handle = dlopen(libraryFilename.c_str(), RTLD_NOW); | void *handle = dlopen(libraryFilename.c_str(), RTLD_NOW); | ||||
if (!handle) { | if (!handle) { | ||||
WARN("Failed to load library %s: %s", libraryFilename.c_str(), dlerror()); | |||||
return false; | |||||
throw UserException(string::f("Failed to load library %s: %s", libraryFilename.c_str(), dlerror())); | |||||
} | } | ||||
#endif | #endif | ||||
plugin->handle = handle; | |||||
// Call plugin's init() function | |||||
typedef void (*InitCallback)(Plugin *); | |||||
// Get plugin's init() function | |||||
InitCallback initCallback; | InitCallback initCallback; | ||||
#if defined ARCH_WIN | #if defined ARCH_WIN | ||||
initCallback = (InitCallback) GetProcAddress(handle, "init"); | initCallback = (InitCallback) GetProcAddress(handle, "init"); | ||||
@@ -105,95 +83,103 @@ static bool loadPlugin(std::string path) { | |||||
initCallback = (InitCallback) dlsym(handle, "init"); | initCallback = (InitCallback) dlsym(handle, "init"); | ||||
#endif | #endif | ||||
if (!initCallback) { | if (!initCallback) { | ||||
WARN("Failed to read init() symbol in %s", libraryFilename.c_str()); | |||||
return false; | |||||
throw UserException(string::f("Failed to read init() symbol in %s", libraryFilename.c_str())); | |||||
} | } | ||||
// Construct and initialize Plugin instance | |||||
Plugin *plugin = new Plugin; | |||||
plugin->path = path; | |||||
plugin->handle = handle; | |||||
initCallback(plugin); | |||||
plugin->fromJson(rootJ); | |||||
// Check slug | |||||
if (!isSlugValid(plugin->slug)) { | |||||
WARN("Plugin slug \"%s\" is invalid", plugin->slug.c_str()); | |||||
// TODO Fix memory leak with `plugin` | |||||
return false; | |||||
} | |||||
return initCallback; | |||||
} | |||||
// Reject plugin if slug already exists | |||||
Plugin *oldPlugin = getPlugin(plugin->slug); | |||||
if (oldPlugin) { | |||||
WARN("Plugin \"%s\" is already loaded, not attempting to load it again", plugin->slug.c_str()); | |||||
// TODO Fix memory leak with `plugin` | |||||
return false; | |||||
} | |||||
// Add plugin to list | |||||
plugins.push_back(plugin); | |||||
INFO("Loaded plugin %s v%s from %s", plugin->slug.c_str(), plugin->version.c_str(), libraryFilename.c_str()); | |||||
// Normalize tags | |||||
for (Model *model : plugin->models) { | |||||
std::vector<std::string> normalizedTags; | |||||
for (const std::string &tag : model->tags) { | |||||
std::string normalizedTag = normalizeTag(tag); | |||||
if (!normalizedTag.empty()) | |||||
normalizedTags.push_back(normalizedTag); | |||||
/** If path is blank, loads Core */ | |||||
static Plugin *loadPlugin(std::string path) { | |||||
Plugin *plugin = new Plugin; | |||||
try { | |||||
plugin->path = path; | |||||
// Load plugin.json | |||||
std::string metadataFilename; | |||||
if (path == "") | |||||
metadataFilename = asset::system("Core.json"); | |||||
else | |||||
metadataFilename = path + "/plugin.json"; | |||||
FILE *file = fopen(metadataFilename.c_str(), "r"); | |||||
if (!file) { | |||||
throw UserException(string::f("Metadata file %s does not exist", metadataFilename.c_str())); | |||||
} | } | ||||
model->tags = normalizedTags; | |||||
} | |||||
DEFER({ | |||||
fclose(file); | |||||
}); | |||||
json_error_t error; | |||||
json_t *rootJ = json_loadf(file, 0, &error); | |||||
if (!rootJ) { | |||||
throw UserException(string::f("JSON parsing error at %s %d:%d %s", metadataFilename.c_str(), error.line, error.column, error.text)); | |||||
} | |||||
DEFER({ | |||||
json_decref(rootJ); | |||||
}); | |||||
// Call init callback | |||||
InitCallback initCallback; | |||||
if (path == "") { | |||||
initCallback = ::init; | |||||
} | |||||
else { | |||||
initCallback = loadLibrary(plugin); | |||||
} | |||||
initCallback(plugin); | |||||
// Search for presets | |||||
for (Model *model : plugin->models) { | |||||
std::string presetDir = asset::plugin(plugin, "presets/" + model->slug); | |||||
for (const std::string &presetPath : system::getEntries(presetDir)) { | |||||
model->presetPaths.push_back(presetPath); | |||||
// Load manifest | |||||
plugin->fromJson(rootJ); | |||||
// Check slug | |||||
if (!isSlugValid(plugin->slug)) { | |||||
throw UserException(string::f("Plugin slug \"%s\" is invalid", plugin->slug.c_str())); | |||||
} | } | ||||
} | |||||
return true; | |||||
} | |||||
// Reject plugin if slug already exists | |||||
Plugin *oldPlugin = getPlugin(plugin->slug); | |||||
if (oldPlugin) { | |||||
throw UserException(string::f("Plugin %s is already loaded, not attempting to load it again", plugin->slug.c_str())); | |||||
} | |||||
static bool syncUpdate(const Update &update) { | |||||
#if defined ARCH_WIN | |||||
std::string arch = "win"; | |||||
#elif ARCH_MAC | |||||
std::string arch = "mac"; | |||||
#elif defined ARCH_LIN | |||||
std::string arch = "lin"; | |||||
#endif | |||||
// Normalize tags | |||||
for (Model *model : plugin->models) { | |||||
std::vector<std::string> normalizedTags; | |||||
for (const std::string &tag : model->tags) { | |||||
std::string normalizedTag = normalizeTag(tag); | |||||
if (!normalizedTag.empty()) | |||||
normalizedTags.push_back(normalizedTag); | |||||
} | |||||
model->tags = normalizedTags; | |||||
} | |||||
std::string downloadUrl = app::API_URL + "/download"; | |||||
downloadUrl += "?token=" + network::encodeUrl(settings::token); | |||||
downloadUrl += "&slug=" + network::encodeUrl(update.pluginSlug); | |||||
downloadUrl += "&version=" + network::encodeUrl(update.version); | |||||
downloadUrl += "&arch=" + network::encodeUrl(arch); | |||||
// Search for presets | |||||
for (Model *model : plugin->models) { | |||||
std::string presetDir = asset::plugin(plugin, "presets/" + model->slug); | |||||
for (const std::string &presetPath : system::getEntries(presetDir)) { | |||||
model->presetPaths.push_back(presetPath); | |||||
} | |||||
} | |||||
// downloadName = name; | |||||
downloadProgress = 0.0; | |||||
INFO("Downloading plugin %s %s %s", update.pluginSlug.c_str(), update.version.c_str(), arch.c_str()); | |||||
INFO("Loaded plugin %s v%s from %s", plugin->slug.c_str(), plugin->version.c_str(), path.c_str()); | |||||
plugins.push_back(plugin); | |||||
// Download zip | |||||
std::string pluginDest = asset::user("plugins/" + update.pluginSlug + ".zip"); | |||||
if (!network::requestDownload(downloadUrl, pluginDest, &downloadProgress)) { | |||||
WARN("Plugin %s download was unsuccessful", update.pluginSlug.c_str()); | |||||
return false; | |||||
return plugin; | |||||
} | |||||
catch (UserException &e) { | |||||
WARN("Could not load plugin %s: %s", path.c_str(), e.what()); | |||||
delete plugin; | |||||
return NULL; | |||||
} | } | ||||
// downloadName = ""; | |||||
return true; | |||||
} | } | ||||
static void loadPlugins(std::string path) { | static void loadPlugins(std::string path) { | ||||
std::string message; | |||||
for (std::string pluginPath : system::getEntries(path)) { | for (std::string pluginPath : system::getEntries(path)) { | ||||
if (!system::isDirectory(pluginPath)) | if (!system::isDirectory(pluginPath)) | ||||
continue; | continue; | ||||
if (!loadPlugin(pluginPath)) { | if (!loadPlugin(pluginPath)) { | ||||
// Ignore bad plugins. They are reported in log.txt. | |||||
// Ignore bad plugins. They are reported in the log. | |||||
} | } | ||||
} | } | ||||
} | } | ||||
@@ -233,7 +219,7 @@ static int extractZipHandle(zip_t *za, const char *dir) { | |||||
continue; | continue; | ||||
while (1) { | while (1) { | ||||
char buffer[1<<15]; | |||||
char buffer[1 << 15]; | |||||
int len = zip_fread(zf, buffer, sizeof(buffer)); | int len = zip_fread(zf, buffer, sizeof(buffer)); | ||||
if (len <= 0) | if (len <= 0) | ||||
break; | break; | ||||
@@ -267,7 +253,7 @@ static int extractZip(const char *filename, const char *path) { | |||||
return err; | return err; | ||||
} | } | ||||
static void extractPackages(const std::string &path) { | |||||
static void extractPackages(std::string path) { | |||||
std::string message; | std::string message; | ||||
for (std::string packagePath : system::getEntries(path)) { | for (std::string packagePath : system::getEntries(path)) { | ||||
@@ -296,10 +282,7 @@ static void extractPackages(const std::string &path) { | |||||
void init() { | void init() { | ||||
// Load Core | // Load Core | ||||
Plugin *corePlugin = new Plugin; | |||||
// This function is defined in Core/plugin.cpp | |||||
::init(corePlugin); | |||||
plugins.push_back(corePlugin); | |||||
loadPlugin(""); | |||||
// Get user plugins directory | // Get user plugins directory | ||||
std::string pluginsDir = asset::user("plugins"); | std::string pluginsDir = asset::user("plugins"); | ||||
@@ -309,7 +292,7 @@ void init() { | |||||
extractPackages(pluginsDir); | extractPackages(pluginsDir); | ||||
loadPlugins(pluginsDir); | loadPlugins(pluginsDir); | ||||
// Copy Fundamental package to plugins directory if Fundamental is not loaded | |||||
// If Fundamental wasn't loaded, copy the bundled Fundamental package and load it | |||||
std::string fundamentalSrc = asset::system("Fundamental.zip"); | std::string fundamentalSrc = asset::system("Fundamental.zip"); | ||||
std::string fundamentalDir = asset::user("plugins/Fundamental"); | std::string fundamentalDir = asset::user("plugins/Fundamental"); | ||||
if (!settings::devMode && !getPlugin("Fundamental") && system::isFile(fundamentalSrc)) { | if (!settings::devMode && !getPlugin("Fundamental") && system::isFile(fundamentalSrc)) { | ||||
@@ -318,9 +301,8 @@ void init() { | |||||
loadPlugin(fundamentalDir); | loadPlugin(fundamentalDir); | ||||
} | } | ||||
// TEMP | |||||
// Sync in a detached thread | // Sync in a detached thread | ||||
std::thread t([]{ | |||||
std::thread t([] { | |||||
queryUpdates(); | queryUpdates(); | ||||
}); | }); | ||||
t.detach(); | t.detach(); | ||||
@@ -387,9 +369,9 @@ void queryUpdates() { | |||||
updates.clear(); | updates.clear(); | ||||
// Get user's plugins list | // Get user's plugins list | ||||
std::string pluginsUrl = app::API_URL + "/plugins"; | |||||
json_t *pluginsReqJ = json_object(); | json_t *pluginsReqJ = json_object(); | ||||
json_object_set(pluginsReqJ, "token", json_string(settings::token.c_str())); | json_object_set(pluginsReqJ, "token", json_string(settings::token.c_str())); | ||||
std::string pluginsUrl = app::API_URL + "/plugins"; | |||||
json_t *pluginsResJ = network::requestJson(network::METHOD_GET, pluginsUrl, pluginsReqJ); | json_t *pluginsResJ = network::requestJson(network::METHOD_GET, pluginsUrl, pluginsReqJ); | ||||
json_decref(pluginsReqJ); | json_decref(pluginsReqJ); | ||||
if (!pluginsResJ) { | if (!pluginsResJ) { | ||||
@@ -406,11 +388,14 @@ void queryUpdates() { | |||||
return; | return; | ||||
} | } | ||||
// Get community manifests | |||||
std::string manifestsUrl = app::API_URL + "/community/manifests"; | |||||
json_t *manifestsResJ = network::requestJson(network::METHOD_GET, manifestsUrl, NULL); | |||||
// Get library manifests | |||||
std::string manifestsUrl = app::API_URL + "/library/manifests"; | |||||
json_t *manifestsReq = json_object(); | |||||
json_object_set(manifestsReq, "version", json_string(app::API_VERSION.c_str())); | |||||
json_t *manifestsResJ = network::requestJson(network::METHOD_GET, manifestsUrl, manifestsReq); | |||||
json_decref(manifestsReq); | |||||
if (!manifestsResJ) { | if (!manifestsResJ) { | ||||
WARN("Request for community manifests failed"); | |||||
WARN("Request for library manifests failed"); | |||||
return; | return; | ||||
} | } | ||||
DEFER({ | DEFER({ | ||||
@@ -434,7 +419,7 @@ void queryUpdates() { | |||||
// Get version | // Get version | ||||
// TODO Change this to "version" when API changes | // TODO Change this to "version" when API changes | ||||
json_t *versionJ = json_object_get(manifestJ, "latestVersion"); | |||||
json_t *versionJ = json_object_get(manifestJ, "version"); | |||||
if (!versionJ) { | if (!versionJ) { | ||||
WARN("Plugin %s has no version in manifest", update.pluginSlug.c_str()); | WARN("Plugin %s has no version in manifest", update.pluginSlug.c_str()); | ||||
continue; | continue; | ||||
@@ -464,15 +449,37 @@ void queryUpdates() { | |||||
} | } | ||||
} | } | ||||
void syncUpdate(Update *update) { | |||||
#if defined ARCH_WIN | |||||
std::string arch = "win"; | |||||
#elif ARCH_MAC | |||||
std::string arch = "mac"; | |||||
#elif defined ARCH_LIN | |||||
std::string arch = "lin"; | |||||
#endif | |||||
std::string downloadUrl = app::API_URL + "/download"; | |||||
downloadUrl += "?token=" + network::encodeUrl(settings::token); | |||||
downloadUrl += "&slug=" + network::encodeUrl(update->pluginSlug); | |||||
downloadUrl += "&version=" + network::encodeUrl(update->version); | |||||
downloadUrl += "&arch=" + network::encodeUrl(arch); | |||||
INFO("Downloading plugin %s %s %s", update->pluginSlug.c_str(), update->version.c_str(), arch.c_str()); | |||||
// Download zip | |||||
std::string pluginDest = asset::user("plugins/" + update->pluginSlug + ".zip"); | |||||
if (!network::requestDownload(downloadUrl, pluginDest, &update->progress)) { | |||||
WARN("Plugin %s download was unsuccessful", update->pluginSlug.c_str()); | |||||
return; | |||||
} | |||||
} | |||||
void syncUpdates() { | void syncUpdates() { | ||||
if (settings::token.empty()) | if (settings::token.empty()) | ||||
return; | return; | ||||
downloadProgress = 0.0; | |||||
downloadName = "Updating plugins..."; | |||||
for (const Update &update : updates) { | |||||
syncUpdate(update); | |||||
for (Update &update : updates) { | |||||
syncUpdate(&update); | |||||
} | } | ||||
} | } | ||||
@@ -626,8 +633,6 @@ std::vector<Plugin*> plugins; | |||||
std::string loginStatus; | std::string loginStatus; | ||||
std::vector<Update> updates; | std::vector<Update> updates; | ||||
float downloadProgress = 0.f; | |||||
std::string downloadName; | |||||
} // namespace plugin | } // namespace plugin | ||||
@@ -1,6 +1,7 @@ | |||||
#include <plugin/Plugin.hpp> | #include <plugin/Plugin.hpp> | ||||
#include <plugin/Model.hpp> | #include <plugin/Model.hpp> | ||||
#include <plugin.hpp> | #include <plugin.hpp> | ||||
#include <string.hpp> | |||||
namespace rack { | namespace rack { | ||||
@@ -18,8 +19,7 @@ void Plugin::addModel(Model *model) { | |||||
assert(!model->plugin); | assert(!model->plugin); | ||||
// Check model slug | // Check model slug | ||||
if (!isSlugValid(model->slug)) { | if (!isSlugValid(model->slug)) { | ||||
WARN("Module slug \"%s\" is invalid", model->slug.c_str()); | |||||
return; | |||||
throw UserException(string::f("Module slug \"%s\" is invalid", model->slug.c_str())); | |||||
} | } | ||||
model->plugin = this; | model->plugin = this; | ||||
models.push_back(model); | models.push_back(model); | ||||
@@ -99,8 +99,7 @@ void Plugin::fromJson(json_t *rootJ) { | |||||
Model *model = getModel(modelSlug); | Model *model = getModel(modelSlug); | ||||
if (!model) { | if (!model) { | ||||
WARN("plugin.json of \"%s\" contains module \"%s\" but it is not defined in the plugin", slug.c_str(), modelSlug.c_str()); | |||||
continue; | |||||
throw UserException(string::f("plugin.json of \"%s\" contains module \"%s\" but it is not defined in the plugin", slug.c_str(), modelSlug.c_str())); | |||||
} | } | ||||
model->fromJson(moduleJ); | model->fromJson(moduleJ); | ||||
@@ -164,7 +164,7 @@ static void keyCallback(GLFWwindow *win, int key, int scancode, int action, int | |||||
return; | return; | ||||
// Keyboard MIDI driver | // Keyboard MIDI driver | ||||
if ((mods & RACK_MOD_MASK) == 0 && action == GLFW_PRESS) { | |||||
if (action == GLFW_PRESS && (mods & RACK_MOD_MASK) == 0) { | |||||
keyboard::press(key); | keyboard::press(key); | ||||
} | } | ||||
if (action == GLFW_RELEASE) { | if (action == GLFW_RELEASE) { | ||||