@@ -1,9 +1,9 @@ | |||
RACK_DIR ?= . | |||
VERSION = 1.dev | |||
FLAGS += \ | |||
-Iinclude \ | |||
-Idep/include -Idep/lib/libzip/include | |||
FLAGS += -DVERSION=$(VERSION) | |||
FLAGS += -Iinclude | |||
FLAGS += -Idep/include -Idep/lib/libzip/include | |||
include arch.mk | |||
@@ -2,15 +2,10 @@ ifndef RACK_DIR | |||
$(error RACK_DIR is not defined) | |||
endif | |||
ifndef VERSION | |||
$(error VERSION is not defined) | |||
endif | |||
include $(RACK_DIR)/arch.mk | |||
OBJCOPY ?= objcopy | |||
FLAGS += -DVERSION=$(VERSION) | |||
# Generate dependency files alongside the object files | |||
FLAGS += -MMD -MP | |||
FLAGS += -g | |||
@@ -20,7 +20,7 @@ struct Param { | |||
std::string label; | |||
std::string unit; | |||
void setup(float minValue, float maxValue, float defaultValue, std::string label = "", std::string unit = "", int displayPrecision = 2) { | |||
void setup(float minValue, float maxValue, float defaultValue, std::string label = "", std::string unit = "", int displayPrecision = 2, float displayBase = 0.f, float displayMultiplier = 1.f) { | |||
this->value = defaultValue; | |||
this->minValue = minValue; | |||
this->maxValue = maxValue; | |||
@@ -28,6 +28,8 @@ struct Param { | |||
this->label = label; | |||
this->unit = unit; | |||
this->displayPrecision = displayPrecision; | |||
this->displayBase = displayBase; | |||
this->displayMultiplier = displayMultiplier; | |||
} | |||
json_t *toJson(); | |||
@@ -12,7 +12,7 @@ namespace rack { | |||
template <class TModule, class TModuleWidget, typename... Tags> | |||
Model *createModel(std::string author, std::string slug, std::string name, Tags... tags) { | |||
Model *createModel(std::string slug) { | |||
struct TModel : Model { | |||
Module *createModule() override { | |||
TModule *o = new TModule; | |||
@@ -32,10 +32,7 @@ Model *createModel(std::string author, std::string slug, std::string name, Tags. | |||
}; | |||
Model *o = new TModel; | |||
o->author = author; | |||
o->slug = slug; | |||
o->name = name; | |||
o->tags = {tags...}; | |||
return o; | |||
} | |||
@@ -46,25 +43,28 @@ TWidget *createWidget(math::Vec pos) { | |||
return o; | |||
} | |||
template <class TWidget> | |||
TWidget *createWidgetCentered(math::Vec pos) { | |||
TWidget *o = new TWidget; | |||
o->box.pos = pos.minus(o->box.size.div(2));; | |||
return o; | |||
} | |||
template <class TParamWidget> | |||
TParamWidget *createParam(math::Vec pos, Module *module, int paramId, float minValue, float maxValue, float defaultValue) { | |||
TParamWidget *createParam(math::Vec pos, Module *module, int paramId) { | |||
TParamWidget *o = new TParamWidget; | |||
o->box.pos = pos; | |||
o->quantity->module = module; | |||
o->quantity->paramId = paramId; | |||
o->setLimits(minValue, maxValue); | |||
o->setDefaultValue(defaultValue); | |||
return o; | |||
} | |||
template <class TParamWidget> | |||
TParamWidget *createParamCentered(math::Vec pos, Module *module, int paramId, float minValue, float maxValue, float defaultValue) { | |||
TParamWidget *createParamCentered(math::Vec pos, Module *module, int paramId) { | |||
TParamWidget *o = new TParamWidget; | |||
o->box.pos = pos.minus(o->box.size.div(2)); | |||
o->quantity->module = module; | |||
o->quantity->paramId = paramId; | |||
o->setLimits(minValue, maxValue); | |||
o->setDefaultValue(defaultValue); | |||
return o; | |||
} | |||
@@ -19,11 +19,11 @@ Caller must json_decref(). | |||
*/ | |||
json_t *requestJson(Method method, std::string url, json_t *dataJ); | |||
/** Returns true if downloaded successfully */ | |||
bool requestDownload(std::string url, std::string filename, float *progress); | |||
bool requestDownload(std::string url, const std::string &filename, float *progress); | |||
/** URL-encodes `s` */ | |||
std::string encodeUrl(std::string s); | |||
std::string encodeUrl(const std::string &s); | |||
/** Computes the SHA256 of the file at `filename` */ | |||
std::string computeSHA256File(std::string filename); | |||
std::string computeSHA256File(const std::string &filename); | |||
} // namespace network | |||
@@ -2,6 +2,7 @@ | |||
#include "common.hpp" | |||
#include "plugin/Plugin.hpp" | |||
#include "plugin/Model.hpp" | |||
#include <vector> | |||
#include <list> | |||
@@ -19,6 +20,7 @@ void cancelDownload(); | |||
bool isLoggedIn(); | |||
Plugin *getPlugin(std::string pluginSlug); | |||
Model *getModel(std::string pluginSlug, std::string modelSlug); | |||
std::string getAllowedTag(std::string tag); | |||
extern std::list<Plugin*> plugins; | |||
@@ -27,6 +29,7 @@ extern bool isDownloading; | |||
extern float downloadProgress; | |||
extern std::string downloadName; | |||
extern std::string loginStatus; | |||
extern const std::vector<std::string> allowedTags; | |||
} // namespace plugin | |||
@@ -1,7 +1,7 @@ | |||
#pragma once | |||
#include "common.hpp" | |||
#include "plugin/Plugin.hpp" | |||
#include "tags.hpp" | |||
#include <jansson.h> | |||
#include <list> | |||
@@ -14,19 +14,17 @@ struct Module; | |||
struct Model { | |||
Plugin *plugin = NULL; | |||
/** An identifier for the model, e.g. "VCO". Used for saving patches. | |||
The model slug must be unique in your plugin, but it doesn't need to be unique among different plugins. | |||
*/ | |||
std::string slug; | |||
/** Human readable name for your model, e.g. "Voltage Controlled Oscillator" */ | |||
std::string name; | |||
/** The author name of the module. | |||
This might be different than the plugin slug. For example, if you create multiple plugins but want them to be branded similarly, you may use the same author in multiple plugins. | |||
You may even have multiple authors in one plugin, although this property will be moved to Plugin for Rack 1.0. | |||
*/ | |||
std::string author; | |||
/** List of tags representing the function(s) of the module (optional) */ | |||
std::list<ModelTag> tags; | |||
/** A one-line summary of the module's purpose */ | |||
std::string description; | |||
/** List of tags representing the function(s) of the module */ | |||
std::list<std::string> tags; | |||
virtual ~Model() {} | |||
/** Creates a headless Module */ | |||
@@ -35,6 +33,8 @@ struct Model { | |||
virtual ModuleWidget *createModuleWidget() { return NULL; } | |||
/** Creates a ModuleWidget with no Module, useful for previews */ | |||
virtual ModuleWidget *createModuleWidgetNull() { return NULL; } | |||
void fromJson(json_t *rootJ); | |||
}; | |||
@@ -1,5 +1,6 @@ | |||
#pragma once | |||
#include "common.hpp" | |||
#include <jansson.h> | |||
#include <list> | |||
@@ -22,19 +23,26 @@ struct Plugin { | |||
To guarantee uniqueness, it is a good idea to prefix the slug by your "company name" if available, e.g. "MyCompany-MyPlugin" | |||
*/ | |||
std::string slug; | |||
/** The version of your plugin | |||
Plugins should follow the versioning scheme described at https://github.com/VCVRack/Rack/issues/266 | |||
Do not include the "v" in "v1.0" for example. | |||
*/ | |||
std::string version; | |||
/** Deprecated, do not use. */ | |||
std::string website; | |||
std::string manual; | |||
/** Human readable name for your plugin, e.g. "Voltage Controlled Oscillator" */ | |||
std::string name; | |||
std::string author; | |||
std::string license; | |||
std::string authorEmail; | |||
std::string pluginUrl; | |||
std::string authorUrl; | |||
std::string manualUrl; | |||
std::string sourceUrl; | |||
std::string donateUrl; | |||
virtual ~Plugin(); | |||
void addModel(Model *model); | |||
Model *getModel(std::string slug); | |||
void fromJson(json_t *rootJ); | |||
}; | |||
@@ -6,23 +6,25 @@ namespace rack { | |||
namespace string { | |||
/** Converts a printf format string and optional arguments into a std::string */ | |||
/** Converts a `printf()` format string and optional arguments into a std::string | |||
Remember that "%s" must reference a `char *`, so use `.c_str()` for `std::string`s. | |||
*/ | |||
std::string f(const char *format, ...); | |||
/** Replaces all characters to lowercase letters */ | |||
std::string lowercase(std::string s); | |||
/** Replaces all characters to uppercase letters */ | |||
std::string uppercase(std::string s); | |||
/** Truncates and adds "..." to a string, not exceeding `len` characters */ | |||
std::string ellipsize(std::string s, size_t len); | |||
bool startsWith(std::string str, std::string prefix); | |||
bool endsWith(std::string str, std::string suffix); | |||
std::string ellipsize(const std::string &s, size_t len); | |||
bool startsWith(const std::string &str, const std::string &prefix); | |||
bool endsWith(const std::string &str, const std::string &suffix); | |||
/** Extracts portions of a path */ | |||
std::string directory(std::string path); | |||
std::string filename(std::string path); | |||
std::string directory(const std::string &path); | |||
std::string filename(const std::string &path); | |||
/** Extracts the portion of a path without the extension */ | |||
std::string basename(std::string path); | |||
std::string basename(const std::string &path); | |||
/** Extracts the extension of a path */ | |||
std::string extension(std::string path); | |||
std::string extension(const std::string &path); | |||
struct CaseInsensitiveCompare { | |||
bool operator()(const std::string &a, const std::string &b) const { | |||
@@ -7,17 +7,17 @@ namespace rack { | |||
namespace system { | |||
std::list<std::string> listEntries(std::string path); | |||
bool isFile(std::string path); | |||
bool isDirectory(std::string path); | |||
void copyFile(std::string srcPath, std::string destPath); | |||
void createDirectory(std::string path); | |||
std::list<std::string> listEntries(const std::string &path); | |||
bool isFile(const std::string &path); | |||
bool isDirectory(const std::string &path); | |||
void copyFile(const std::string &srcPath, const std::string &destPath); | |||
void createDirectory(const std::string &path); | |||
/** Opens a URL, also happens to work with PDFs and folders. | |||
Shell injection is possible, so make sure the URL is trusted or hard coded. | |||
May block, so open in a new thread. | |||
*/ | |||
void openBrowser(std::string url); | |||
void openBrowser(const std::string &url); | |||
} // namespace system | |||
@@ -1,75 +0,0 @@ | |||
#pragma once | |||
#include "common.hpp" | |||
namespace rack { | |||
/** Describes the type(s) of each module | |||
To see comments, turn word wrap on. I'm using inline comments so I can automatically sort the list when more tags are added. | |||
*/ | |||
enum ModelTag { | |||
NO_TAG, // Don't use this in `Model::create(...)`. Instead, just omit the tags entirely. | |||
AMPLIFIER_TAG, | |||
ARPEGGIATOR_TAG, | |||
ATTENUATOR_TAG, | |||
BLANK_TAG, | |||
CHORUS_TAG, | |||
CLOCK_MODULATOR_TAG, // Clock dividers, multipliers, etc. | |||
CLOCK_TAG, | |||
COMPRESSOR_TAG, | |||
CONTROLLER_TAG, // Use only if the artist "performs" with this module. Knobs are not sufficient. Examples: on-screen keyboard, XY pad. | |||
DELAY_TAG, | |||
DIGITAL_TAG, | |||
DISTORTION_TAG, | |||
DRUM_TAG, | |||
DUAL_TAG, // The core functionality times two. If multiple channels are a requirement for the module to exist (ring modulator, mixer, etc), it is not a Dual module. | |||
DYNAMICS_TAG, | |||
EFFECT_TAG, | |||
ENVELOPE_FOLLOWER_TAG, | |||
ENVELOPE_GENERATOR_TAG, | |||
EQUALIZER_TAG, | |||
EXTERNAL_TAG, | |||
FILTER_TAG, | |||
FLANGER_TAG, | |||
FUNCTION_GENERATOR_TAG, | |||
GRANULAR_TAG, | |||
LFO_TAG, | |||
LIMITER_TAG, | |||
LOGIC_TAG, | |||
LOW_PASS_GATE_TAG, | |||
MIDI_TAG, | |||
MIXER_TAG, | |||
MULTIPLE_TAG, | |||
NOISE_TAG, | |||
OSCILLATOR_TAG, | |||
PANNING_TAG, | |||
PHASER_TAG, | |||
PHYSICAL_MODELING_TAG, | |||
QUAD_TAG, // The core functionality times four. If multiple channels are a requirement for the module to exist (ring modulator, mixer, etc), it is not a Quad module. | |||
QUANTIZER_TAG, | |||
RANDOM_TAG, | |||
RECORDING_TAG, | |||
REVERB_TAG, | |||
RING_MODULATOR_TAG, | |||
SAMPLE_AND_HOLD_TAG, | |||
SAMPLER_TAG, | |||
SEQUENCER_TAG, | |||
SLEW_LIMITER_TAG, | |||
SWITCH_TAG, | |||
SYNTH_VOICE_TAG, // A synth voice must have an envelope built-in. | |||
TUNER_TAG, | |||
UTILITY_TAG, // Serves only extremely basic functions, like inverting, max, min, multiplying by 2, etc. | |||
VISUAL_TAG, | |||
VOCODER_TAG, | |||
WAVESHAPER_TAG, | |||
NUM_TAGS | |||
}; | |||
void tagsInit(); | |||
extern std::string gTagNames[NUM_TAGS]; | |||
} // namespace rack |
@@ -2,13 +2,13 @@ ifndef RACK_DIR | |||
$(error RACK_DIR is not defined) | |||
endif | |||
ifndef SLUG | |||
$(error SLUG is not defined) | |||
endif | |||
SLUG := $(shell jq ".slug" plugin.json) | |||
VERSION := $(shell jq ".version" plugin.json) | |||
STRIP ?= strip | |||
FLAGS += -DSLUG=$(SLUG) | |||
DISTRIBUTABLES += plugin.json | |||
FLAGS += -fPIC | |||
FLAGS += -I$(RACK_DIR)/include -I$(RACK_DIR)/dep/include | |||
@@ -282,4 +282,4 @@ struct AudioInterfaceWidget : ModuleWidget { | |||
}; | |||
Model *modelAudioInterface = createModel<AudioInterface, AudioInterfaceWidget>("Core", "AudioInterface", "Audio", EXTERNAL_TAG); | |||
Model *modelAudioInterface = createModel<AudioInterface, AudioInterfaceWidget>("AudioInterface"); |
@@ -121,4 +121,4 @@ struct BlankWidget : ModuleWidget { | |||
}; | |||
Model *modelBlank = createModel<Module, BlankWidget>("Core", "Blank", "Blank", BLANK_TAG); | |||
Model *modelBlank = createModel<Module, BlankWidget>("Blank"); |
@@ -216,4 +216,4 @@ struct MIDICCToCVInterfaceWidget : ModuleWidget { | |||
}; | |||
Model *modelMIDICCToCVInterface = createModel<MIDICCToCVInterface, MIDICCToCVInterfaceWidget>("Core", "MIDICCToCVInterface", "MIDI-CC", MIDI_TAG, EXTERNAL_TAG); | |||
Model *modelMIDICCToCVInterface = createModel<MIDICCToCVInterface, MIDICCToCVInterfaceWidget>("MIDICCToCVInterface"); |
@@ -328,4 +328,4 @@ struct MIDIToCVInterfaceWidget : ModuleWidget { | |||
}; | |||
Model *modelMIDIToCVInterface = createModel<MIDIToCVInterface, MIDIToCVInterfaceWidget>("Core", "MIDIToCVInterface", "MIDI-1", MIDI_TAG, EXTERNAL_TAG); | |||
Model *modelMIDIToCVInterface = createModel<MIDIToCVInterface, MIDIToCVInterfaceWidget>("MIDIToCVInterface"); |
@@ -251,4 +251,4 @@ struct MIDITriggerToCVInterfaceWidget : ModuleWidget { | |||
}; | |||
Model *modelMIDITriggerToCVInterface = createModel<MIDITriggerToCVInterface, MIDITriggerToCVInterfaceWidget>("Core", "MIDITriggerToCVInterface", "MIDI-Trig", MIDI_TAG, EXTERNAL_TAG); | |||
Model *modelMIDITriggerToCVInterface = createModel<MIDITriggerToCVInterface, MIDITriggerToCVInterfaceWidget>("MIDITriggerToCVInterface"); |
@@ -41,4 +41,4 @@ struct NotesWidget : ModuleWidget { | |||
}; | |||
Model *modelNotes = createModel<Module, NotesWidget>("Core", "Notes", "Notes", BLANK_TAG); | |||
Model *modelNotes = createModel<Module, NotesWidget>("Notes"); |
@@ -366,5 +366,5 @@ struct QuadMIDIToCVInterfaceWidget : ModuleWidget { | |||
}; | |||
Model *modelQuadMIDIToCVInterface = createModel<QuadMIDIToCVInterface, QuadMIDIToCVInterfaceWidget>("Core", "QuadMIDIToCVInterface", "MIDI-4", MIDI_TAG, EXTERNAL_TAG, QUAD_TAG); | |||
Model *modelQuadMIDIToCVInterface = createModel<QuadMIDIToCVInterface, QuadMIDIToCVInterfaceWidget>("QuadMIDIToCVInterface"); | |||
@@ -22,7 +22,7 @@ namespace rack { | |||
static std::set<Model*> sFavoriteModels; | |||
static std::string sAuthorFilter; | |||
static ModelTag sTagFilter = NO_TAG; | |||
static std::string sTagFilter; | |||
@@ -38,14 +38,17 @@ static bool isModelMatch(Model *model, std::string search) { | |||
std::string s; | |||
s += model->plugin->slug; | |||
s += " "; | |||
s += model->author; | |||
s += model->plugin->author; | |||
s += " "; | |||
s += model->name; | |||
s += " "; | |||
s += model->slug; | |||
for (ModelTag tag : model->tags) { | |||
s += " "; | |||
s += gTagNames[tag]; | |||
for (std::string tag : model->tags) { | |||
std::string allowedTag = plugin::getAllowedTag(tag); | |||
if (!allowedTag.empty()) { | |||
s += " "; | |||
s += allowedTag; | |||
} | |||
} | |||
return isMatch(s, search); | |||
} | |||
@@ -185,16 +188,16 @@ struct AuthorItem : BrowserListItem { | |||
struct TagItem : BrowserListItem { | |||
ModelTag tag; | |||
std::string tag; | |||
void setTag(ModelTag tag) { | |||
void setTag(std::string tag) { | |||
clearChildren(); | |||
this->tag = tag; | |||
Label *tagLabel = createWidget<Label>(math::Vec(0, 0 + itemMargin)); | |||
if (tag == NO_TAG) | |||
if (tag.empty()) | |||
tagLabel->text = "Show all tags"; | |||
else | |||
tagLabel->text = gTagNames[tag]; | |||
tagLabel->text = tag; | |||
addChild(tagLabel); | |||
} | |||
@@ -299,12 +302,12 @@ struct ModuleBrowser : OpaqueWidget { | |||
ScrollWidget *moduleScroll; | |||
BrowserList *moduleList; | |||
std::set<std::string, string::CaseInsensitiveCompare> availableAuthors; | |||
std::set<ModelTag> availableTags; | |||
std::set<std::string> availableTags; | |||
ModuleBrowser() { | |||
box.size.x = 450; | |||
sAuthorFilter = ""; | |||
sTagFilter = NO_TAG; | |||
sTagFilter = ""; | |||
// Search | |||
searchField = new SearchModuleField; | |||
@@ -324,13 +327,14 @@ struct ModuleBrowser : OpaqueWidget { | |||
// Collect authors | |||
for (Plugin *plugin : plugin::plugins) { | |||
// Insert author | |||
if (!plugin->author.empty()) | |||
availableAuthors.insert(plugin->author); | |||
for (Model *model : plugin->models) { | |||
// Insert author | |||
if (!model->author.empty()) | |||
availableAuthors.insert(model->author); | |||
// Insert tag | |||
for (ModelTag tag : model->tags) { | |||
if (tag != NO_TAG) | |||
for (std::string tag : model->tags) { | |||
std::string allowedTag = plugin::getAllowedTag(tag); | |||
if (!allowedTag.empty()) | |||
availableTags.insert(tag); | |||
} | |||
} | |||
@@ -350,12 +354,10 @@ struct ModuleBrowser : OpaqueWidget { | |||
} | |||
bool isModelFiltered(Model *model) { | |||
if (!sAuthorFilter.empty() && model->author != sAuthorFilter) | |||
if (!sAuthorFilter.empty() && model->plugin->author != sAuthorFilter) | |||
return false; | |||
if (sTagFilter != NO_TAG) { | |||
auto it = std::find(model->tags.begin(), model->tags.end(), sTagFilter); | |||
if (it == model->tags.end()) | |||
return false; | |||
if (!sTagFilter.empty()) { | |||
// TODO filter tags | |||
} | |||
return true; | |||
} | |||
@@ -364,7 +366,7 @@ struct ModuleBrowser : OpaqueWidget { | |||
std::string search = searchField->text; | |||
moduleList->clearChildren(); | |||
moduleList->selected = 0; | |||
bool filterPage = !(sAuthorFilter.empty() && sTagFilter == NO_TAG); | |||
bool filterPage = !(sAuthorFilter.empty() && sTagFilter.empty()); | |||
if (!filterPage) { | |||
// Favorites | |||
@@ -399,8 +401,8 @@ struct ModuleBrowser : OpaqueWidget { | |||
item->setText("Tags"); | |||
moduleList->addChild(item); | |||
} | |||
for (ModelTag tag : availableTags) { | |||
if (isMatch(gTagNames[tag], search)) { | |||
for (std::string tag : availableTags) { | |||
if (isMatch(tag, search)) { | |||
TagItem *item = new TagItem; | |||
item->setTag(tag); | |||
moduleList->addChild(item); | |||
@@ -423,8 +425,8 @@ struct ModuleBrowser : OpaqueWidget { | |||
SeparatorItem *item = new SeparatorItem; | |||
if (!sAuthorFilter.empty()) | |||
item->setText(sAuthorFilter); | |||
else if (sTagFilter != NO_TAG) | |||
item->setText("Tag: " + gTagNames[sTagFilter]); | |||
else if (!sTagFilter.empty()) | |||
item->setText("Tag: " + sTagFilter); | |||
moduleList->addChild(item); | |||
} | |||
// Modules | |||
@@ -474,7 +476,7 @@ void TagItem::onAction(event::Action &e) { | |||
void ClearFilterItem::onAction(event::Action &e) { | |||
ModuleBrowser *moduleBrowser = getAncestorOfType<ModuleBrowser>(); | |||
sAuthorFilter = ""; | |||
sTagFilter = NO_TAG; | |||
sTagFilter = ""; | |||
moduleBrowser->refreshSearch(); | |||
e.target = this; | |||
} | |||
@@ -450,7 +450,7 @@ Menu *ModuleWidget::createContextMenu() { | |||
Menu *menu = createMenu(); | |||
MenuLabel *menuLabel = new MenuLabel; | |||
menuLabel->text = model->author + " " + model->name + " " + model->plugin->version; | |||
menuLabel->text = model->plugin->author + " " + model->name + " " + model->plugin->version; | |||
menu->addChild(menuLabel); | |||
ModuleResetItem *resetItem = new ModuleResetItem; | |||
@@ -9,7 +9,6 @@ | |||
#include "settings.hpp" | |||
#include "engine/Engine.hpp" | |||
#include "app/Scene.hpp" | |||
#include "tags.hpp" | |||
#include "plugin.hpp" | |||
#include "context.hpp" | |||
#include "ui.hpp" | |||
@@ -64,7 +63,6 @@ int main(int argc, char *argv[]) { | |||
random::init(); | |||
asset::init(devMode); | |||
logger::init(devMode); | |||
tagsInit(); | |||
midi::init(); | |||
rtmidiInit(); | |||
bridgeInit(); | |||
@@ -115,7 +115,7 @@ static int xferInfoCallback(void *clientp, curl_off_t dltotal, curl_off_t dlnow, | |||
return 0; | |||
} | |||
bool requestDownload(std::string url, std::string filename, float *progress) { | |||
bool requestDownload(std::string url, const std::string &filename, float *progress) { | |||
if (progress) | |||
*progress = 0.f; | |||
@@ -150,7 +150,7 @@ bool requestDownload(std::string url, std::string filename, float *progress) { | |||
return res == CURLE_OK; | |||
} | |||
std::string encodeUrl(std::string s) { | |||
std::string encodeUrl(const std::string &s) { | |||
CURL *curl = curl_easy_init(); | |||
assert(curl); | |||
char *escaped = curl_easy_escape(curl, s.c_str(), s.size()); | |||
@@ -160,7 +160,7 @@ std::string encodeUrl(std::string s) { | |||
return ret; | |||
} | |||
std::string computeSHA256File(std::string filename) { | |||
std::string computeSHA256File(const std::string &filename) { | |||
FILE *f = fopen(filename.c_str(), "rb"); | |||
if (!f) | |||
return ""; | |||
@@ -40,6 +40,28 @@ namespace plugin { | |||
//////////////////// | |||
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); | |||
}); | |||
// Load plugin library | |||
std::string libraryFilename; | |||
#if ARCH_LIN | |||
libraryFilename = path + "/" + "plugin.so"; | |||
@@ -91,6 +113,7 @@ static bool loadPlugin(std::string path) { | |||
plugin->path = path; | |||
plugin->handle = handle; | |||
initCallback(plugin); | |||
plugin->fromJson(rootJ); | |||
// Reject plugin if slug already exists | |||
Plugin *oldPlugin = getPlugin(plugin->slug); | |||
@@ -473,14 +496,21 @@ Plugin *getPlugin(std::string pluginSlug) { | |||
Model *getModel(std::string pluginSlug, std::string modelSlug) { | |||
Plugin *plugin = getPlugin(pluginSlug); | |||
if (plugin) { | |||
for (Model *model : plugin->models) { | |||
if (model->slug == modelSlug) { | |||
return model; | |||
} | |||
} | |||
if (!plugin) | |||
return NULL; | |||
Model *model = plugin->getModel(modelSlug); | |||
if (!model) | |||
return NULL; | |||
return model; | |||
} | |||
std::string getAllowedTag(std::string tag) { | |||
tag = string::lowercase(tag); | |||
for (std::string allowedTag : allowedTags) { | |||
if (tag == string::lowercase(allowedTag)) | |||
return allowedTag; | |||
} | |||
return NULL; | |||
return ""; | |||
} | |||
@@ -492,5 +522,62 @@ std::string downloadName; | |||
std::string loginStatus; | |||
const std::vector<std::string> allowedTags = { | |||
"VCA", | |||
"Arpeggiator", | |||
"Attenuator", | |||
"Blank", | |||
"Chorus", | |||
"Clock Modulator", // Clock dividers, multipliers, etc. | |||
"Clock", | |||
"Compressor", | |||
"Controller", // Use only if the artist "performs" with this module. Knobs are not sufficient. Examples: on-screen keyboard, XY pad. | |||
"Delay", | |||
"Digital", | |||
"Distortion", | |||
"Drum", | |||
"Dual", // The core functionality times two. If multiple channels are a requirement for the module to exist (ring modulator, mixer, etc), it is not a Dual module. | |||
"Dynamics", | |||
"Effect", | |||
"Envelope Follower", | |||
"Envelope Generator", | |||
"Equalizer", | |||
"External", | |||
"Filter", | |||
"Flanger", | |||
"Function Generator", | |||
"Granular", | |||
"LFO", | |||
"Limiter", | |||
"Logic", | |||
"Low Pass Gate", | |||
"MIDI", | |||
"Mixer", | |||
"Multiple", | |||
"Noise", | |||
"VCO", | |||
"Panning", | |||
"Phaser", | |||
"Physical Modeling", | |||
"Quad", // The core functionality times four. If multiple channels are a requirement for the module to exist (ring modulator, mixer, etc), it is not a Quad module. | |||
"Quantizer", | |||
"Random", | |||
"Recording", | |||
"Reverb", | |||
"Ring Modulator", | |||
"Sample and Hold", | |||
"Sampler", | |||
"Sequencer", | |||
"Slew Limiter", | |||
"Switch", | |||
"Synth Voice", // A synth voice must have an envelope built-in. | |||
"Tuner", | |||
"Utility", // Serves only extremely basic functions, like inverting, max, min, multiplying by 2, etc. | |||
"Visual", | |||
"Vocoder", | |||
"Waveshaper", | |||
}; | |||
} // namespace plugin | |||
} // namespace rack |
@@ -0,0 +1,27 @@ | |||
#include "plugin/Model.hpp" | |||
#include "logger.hpp" | |||
namespace rack { | |||
void Model::fromJson(json_t *rootJ) { | |||
json_t *nameJ = json_object_get(rootJ, "name"); | |||
if (nameJ) | |||
name = json_string_value(nameJ); | |||
DEBUG("name: %s", name.c_str()); | |||
json_t *tagsJ = json_object_get(rootJ, "tags"); | |||
if (tagsJ) { | |||
size_t i; | |||
json_t *tagJ; | |||
json_array_foreach(tagsJ, i, tagJ) { | |||
std::string tag = json_string_value(tagJ); | |||
DEBUG("tag: %s", tag.c_str()); | |||
} | |||
} | |||
} | |||
} // namespace rack |
@@ -1,5 +1,6 @@ | |||
#include "plugin/Plugin.hpp" | |||
#include "plugin/Model.hpp" | |||
#include "logger.hpp" | |||
namespace rack { | |||
@@ -17,5 +18,75 @@ void Plugin::addModel(Model *model) { | |||
models.push_back(model); | |||
} | |||
Model *Plugin::getModel(std::string slug) { | |||
for (Model *model : models) { | |||
if (model->slug == slug) { | |||
return model; | |||
} | |||
} | |||
return NULL; | |||
} | |||
void Plugin::fromJson(json_t *rootJ) { | |||
json_t *slugJ = json_object_get(rootJ, "slug"); | |||
if (slugJ) | |||
slug = json_string_value(slugJ); | |||
json_t *versionJ = json_object_get(rootJ, "version"); | |||
if (versionJ) | |||
version = json_string_value(versionJ); | |||
json_t *nameJ = json_object_get(rootJ, "name"); | |||
if (nameJ) | |||
name = json_string_value(nameJ); | |||
json_t *authorJ = json_object_get(rootJ, "author"); | |||
if (authorJ) | |||
author = json_string_value(authorJ); | |||
json_t *licenseJ = json_object_get(rootJ, "license"); | |||
if (licenseJ) | |||
license = json_string_value(licenseJ); | |||
json_t *authorEmailJ = json_object_get(rootJ, "authorEmail"); | |||
if (authorEmailJ) | |||
authorEmail = json_string_value(authorEmailJ); | |||
json_t *pluginUrlJ = json_object_get(rootJ, "pluginUrl"); | |||
if (pluginUrlJ) | |||
pluginUrl = json_string_value(pluginUrlJ); | |||
json_t *authorUrlJ = json_object_get(rootJ, "authorUrl"); | |||
if (authorUrlJ) | |||
authorUrl = json_string_value(authorUrlJ); | |||
json_t *manualUrlJ = json_object_get(rootJ, "manualUrl"); | |||
if (manualUrlJ) | |||
manualUrl = json_string_value(manualUrlJ); | |||
json_t *sourceUrlJ = json_object_get(rootJ, "sourceUrl"); | |||
if (sourceUrlJ) | |||
sourceUrl = json_string_value(sourceUrlJ); | |||
json_t *donateUrlJ = json_object_get(rootJ, "donateUrl"); | |||
if (donateUrlJ) | |||
donateUrl = json_string_value(donateUrlJ); | |||
json_t *modulesJ = json_object_get(rootJ, "modules"); | |||
if (modulesJ) { | |||
const char *slug; | |||
json_t *moduleJ; | |||
json_object_foreach(modulesJ, slug, moduleJ) { | |||
Model *model = getModel(slug); | |||
if (!model) { | |||
WARN("Metadata references module \"%s\" but it is not registered in the plugin library", slug); | |||
continue; | |||
} | |||
model->fromJson(moduleJ); | |||
} | |||
} | |||
} | |||
} // namespace rack |
@@ -34,43 +34,41 @@ std::string uppercase(std::string s) { | |||
return s; | |||
} | |||
/** Truncates and adds "..." to a string, not exceeding `len` characters */ | |||
std::string ellipsize(std::string s, size_t len) { | |||
std::string ellipsize(const std::string &s, size_t len) { | |||
if (s.size() <= len) | |||
return s; | |||
else | |||
return s.substr(0, len - 3) + "..."; | |||
} | |||
bool startsWith(std::string str, std::string prefix) { | |||
bool startsWith(const std::string &str, const std::string &prefix) { | |||
return str.substr(0, prefix.size()) == prefix; | |||
} | |||
bool endsWith(std::string str, std::string suffix) { | |||
bool endsWith(const std::string &str, const std::string &suffix) { | |||
return str.substr(str.size() - suffix.size(), suffix.size()) == suffix; | |||
} | |||
/** Extracts portions of a path */ | |||
std::string directory(std::string path) { | |||
std::string directory(const std::string &path) { | |||
char *pathDup = strdup(path.c_str()); | |||
std::string directory = dirname(pathDup); | |||
free(pathDup); | |||
return directory; | |||
} | |||
std::string filename(std::string path) { | |||
std::string filename(const std::string &path) { | |||
char *pathDup = strdup(path.c_str()); | |||
std::string filename = basename(pathDup); | |||
free(pathDup); | |||
return filename; | |||
} | |||
std::string basename(std::string path) { | |||
std::string basename(const std::string &path) { | |||
size_t pos = path.rfind('.'); | |||
return std::string(path, 0, pos); | |||
} | |||
std::string extension(std::string path) { | |||
std::string extension(const std::string &path) { | |||
size_t pos = path.rfind('.'); | |||
return std::string(path, pos); | |||
} | |||
@@ -12,7 +12,7 @@ namespace rack { | |||
namespace system { | |||
std::list<std::string> listEntries(std::string path) { | |||
std::list<std::string> listEntries(const std::string &path) { | |||
std::list<std::string> filenames; | |||
DIR *dir = opendir(path.c_str()); | |||
if (dir) { | |||
@@ -28,21 +28,21 @@ std::list<std::string> listEntries(std::string path) { | |||
return filenames; | |||
} | |||
bool isFile(std::string path) { | |||
bool isFile(const std::string &path) { | |||
struct stat statbuf; | |||
if (stat(path.c_str(), &statbuf)) | |||
return false; | |||
return S_ISREG(statbuf.st_mode); | |||
} | |||
bool isDirectory(std::string path) { | |||
bool isDirectory(const std::string &path) { | |||
struct stat statbuf; | |||
if (stat(path.c_str(), &statbuf)) | |||
return false; | |||
return S_ISDIR(statbuf.st_mode); | |||
} | |||
void copyFile(std::string srcPath, std::string destPath) { | |||
void copyFile(const std::string &srcPath, const std::string &destPath) { | |||
// Open files | |||
FILE *source = fopen(srcPath.c_str(), "rb"); | |||
if (!source) | |||
@@ -69,7 +69,7 @@ void copyFile(std::string srcPath, std::string destPath) { | |||
} | |||
} | |||
void createDirectory(std::string path) { | |||
void createDirectory(const std::string &path) { | |||
#if ARCH_WIN | |||
CreateDirectory(path.c_str(), NULL); | |||
#else | |||
@@ -77,7 +77,7 @@ void createDirectory(std::string path) { | |||
#endif | |||
} | |||
void openBrowser(std::string url) { | |||
void openBrowser(const std::string &url) { | |||
#if ARCH_LIN | |||
std::string command = "xdg-open " + url; | |||
(void) std::system(command.c_str()); | |||
@@ -1,67 +0,0 @@ | |||
#include "tags.hpp" | |||
namespace rack { | |||
std::string gTagNames[NUM_TAGS]; | |||
void tagsInit() { | |||
gTagNames[AMPLIFIER_TAG] = "Amplifier/VCA"; | |||
gTagNames[ATTENUATOR_TAG] = "Attenuator"; | |||
gTagNames[ARPEGGIATOR_TAG] = "Arpeggiator"; | |||
gTagNames[BLANK_TAG] = "Blank"; | |||
gTagNames[CHORUS_TAG] = "Chorus"; | |||
gTagNames[CLOCK_TAG] = "Clock"; | |||
gTagNames[CLOCK_MODULATOR_TAG] = "Clock Modulator"; | |||
gTagNames[COMPRESSOR_TAG] = "Compressor"; | |||
gTagNames[CONTROLLER_TAG] = "Controller"; | |||
gTagNames[DELAY_TAG] = "Delay"; | |||
gTagNames[DIGITAL_TAG] = "Digital"; | |||
gTagNames[DISTORTION_TAG] = "Distortion"; | |||
gTagNames[DRUM_TAG] = "Drum"; | |||
gTagNames[DUAL_TAG] = "Dual/Stereo"; | |||
gTagNames[DYNAMICS_TAG] = "Dynamics"; | |||
gTagNames[EFFECT_TAG] = "Effect"; | |||
gTagNames[ENVELOPE_FOLLOWER_TAG] = "Envelope Follower"; | |||
gTagNames[ENVELOPE_GENERATOR_TAG] = "Envelope Generator"; | |||
gTagNames[EQUALIZER_TAG] = "Equalizer"; | |||
gTagNames[EXTERNAL_TAG] = "External"; | |||
gTagNames[FILTER_TAG] = "Filter/VCF"; | |||
gTagNames[FLANGER_TAG] = "Flanger"; | |||
gTagNames[FUNCTION_GENERATOR_TAG] = "Function Generator"; | |||
gTagNames[GRANULAR_TAG] = "Granular"; | |||
gTagNames[LFO_TAG] = "LFO"; | |||
gTagNames[LIMITER_TAG] = "Limiter"; | |||
gTagNames[LOGIC_TAG] = "Logic"; | |||
gTagNames[LOW_PASS_GATE_TAG] = "Low Pass Gate"; | |||
gTagNames[MIDI_TAG] = "MIDI"; | |||
gTagNames[MIXER_TAG] = "Mixer"; | |||
gTagNames[MULTIPLE_TAG] = "Multiple"; | |||
gTagNames[NOISE_TAG] = "Noise"; | |||
gTagNames[OSCILLATOR_TAG] = "Oscillator/VCO"; | |||
gTagNames[PANNING_TAG] = "Panning"; | |||
gTagNames[PHASER_TAG] = "Phaser"; | |||
gTagNames[PHYSICAL_MODELING_TAG] = "Physical Modeling"; | |||
gTagNames[QUAD_TAG] = "Quad"; | |||
gTagNames[QUANTIZER_TAG] = "Quantizer"; | |||
gTagNames[RANDOM_TAG] = "Random"; | |||
gTagNames[RECORDING_TAG] = "Recording"; | |||
gTagNames[REVERB_TAG] = "Reverb"; | |||
gTagNames[RING_MODULATOR_TAG] = "Ring Modulator"; | |||
gTagNames[SAMPLE_AND_HOLD_TAG] = "Sample and Hold"; | |||
gTagNames[SAMPLER_TAG] = "Sampler"; | |||
gTagNames[SEQUENCER_TAG] = "Sequencer"; | |||
gTagNames[SLEW_LIMITER_TAG] = "Slew Limiter"; | |||
gTagNames[SWITCH_TAG] = "Switch"; | |||
gTagNames[SYNTH_VOICE_TAG] = "Synth Voice"; | |||
gTagNames[TUNER_TAG] = "Tuner"; | |||
gTagNames[UTILITY_TAG] = "Utility"; | |||
gTagNames[VISUAL_TAG] = "Visual"; | |||
gTagNames[VOCODER_TAG] = "Vocoder"; | |||
gTagNames[WAVESHAPER_TAG] = "Waveshaper"; | |||
} | |||
} // namespace rack |