@@ -31,8 +31,10 @@ struct Model { | |||||
std::string slug; | std::string slug; | ||||
/** Human readable name for your model, e.g. "Voltage Controlled Oscillator" */ | /** Human readable name for your model, e.g. "Voltage Controlled Oscillator" */ | ||||
std::string name; | std::string name; | ||||
/** List of tags representing the function(s) of the module */ | |||||
std::vector<std::string> tags; | |||||
/** List of tag IDs representing the function(s) of the module. | |||||
Tag IDs are not part of the ABI and may change at any time. | |||||
*/ | |||||
std::vector<int> tags; | |||||
/** A one-line summary of the module's purpose */ | /** A one-line summary of the module's purpose */ | ||||
std::string description; | std::string description; | ||||
@@ -1,15 +1,27 @@ | |||||
#pragma once | #pragma once | ||||
#include <common.hpp> | #include <common.hpp> | ||||
#include <set> | |||||
#include <vector> | |||||
namespace rack { | namespace rack { | ||||
namespace tag { | namespace tag { | ||||
extern const std::set<std::string> allowedTags; | |||||
/** Searches for a tag ID. | |||||
Searches tag aliases. | |||||
Case-insensitive. | |||||
Returns -1 if not found. | |||||
*/ | |||||
int findId(const std::string& tag); | |||||
std::string normalize(const std::string& tag); | |||||
/** List of tags and their aliases. | |||||
The first alias of each tag `tag[tagId][0]` is the "canonical" alias. | |||||
It is guaranteed to exist and should be used as the human-readable form. | |||||
This list is manually alphabetized by the canonical alias. | |||||
*/ | |||||
extern const std::vector<std::vector<std::string>> tagAliases; | |||||
} // namespace tag | } // namespace tag | ||||
@@ -46,15 +46,18 @@ static float modelScore(plugin::Model* model, const std::string& search) { | |||||
s += model->name; | s += model->name; | ||||
s += " "; | s += " "; | ||||
s += model->slug; | s += model->slug; | ||||
for (const std::string& tag : model->tags) { | |||||
s += " "; | |||||
s += tag; | |||||
for (int tagId : model->tags) { | |||||
// Add all aliases of a tag | |||||
for (const std::string& alias : tag::tagAliases[tagId]) { | |||||
s += " "; | |||||
s += alias; | |||||
} | |||||
} | } | ||||
float score = string::fuzzyScore(string::lowercase(s), string::lowercase(search)); | float score = string::fuzzyScore(string::lowercase(s), string::lowercase(search)); | ||||
return score; | return score; | ||||
} | } | ||||
static bool isModelVisible(plugin::Model* model, const std::string& search, const std::string& brand, const std::string& tag) { | |||||
static bool isModelVisible(plugin::Model* model, const std::string& search, const std::string& brand, int tagId) { | |||||
// Filter search query | // Filter search query | ||||
if (search != "") { | if (search != "") { | ||||
float score = modelScore(model, search); | float score = modelScore(model, search); | ||||
@@ -69,15 +72,9 @@ static bool isModelVisible(plugin::Model* model, const std::string& search, cons | |||||
} | } | ||||
// Filter tag | // Filter tag | ||||
if (tag != "") { | |||||
bool found = false; | |||||
for (const std::string& modelTag : model->tags) { | |||||
if (modelTag == tag) { | |||||
found = true; | |||||
break; | |||||
} | |||||
} | |||||
if (!found) | |||||
if (tagId >= 0) { | |||||
auto it = std::find(model->tags.begin(), model->tags.end(), tagId); | |||||
if (it == model->tags.end()) | |||||
return false; | return false; | ||||
} | } | ||||
@@ -255,7 +252,8 @@ struct ModelBox : widget::OpaqueWidget { | |||||
for (size_t i = 0; i < model->tags.size(); i++) { | for (size_t i = 0; i < model->tags.size(); i++) { | ||||
if (i > 0) | if (i > 0) | ||||
text += ", "; | text += ", "; | ||||
text += model->tags[i]; | |||||
int tagId = model->tags[i]; | |||||
text += tag::tagAliases[tagId][0]; | |||||
} | } | ||||
// Description | // Description | ||||
if (model->description != "") { | if (model->description != "") { | ||||
@@ -285,6 +283,7 @@ struct BrandItem : ui::MenuItem { | |||||
struct TagItem : ui::MenuItem { | struct TagItem : ui::MenuItem { | ||||
int tagId; | |||||
void onAction(const event::Action& e) override; | void onAction(const event::Action& e) override; | ||||
void step() override; | void step() override; | ||||
}; | }; | ||||
@@ -378,9 +377,10 @@ struct BrowserSidebar : widget::Widget { | |||||
tagList = new ui::List; | tagList = new ui::List; | ||||
tagScroll->container->addChild(tagList); | tagScroll->container->addChild(tagList); | ||||
for (const std::string& tag : tag::allowedTags) { | |||||
for (int tagId = 0; tagId < (int) tag::tagAliases.size(); tagId++) { | |||||
TagItem* item = new TagItem; | TagItem* item = new TagItem; | ||||
item->text = tag; | |||||
item->text = tag::tagAliases[tagId][0]; | |||||
item->tagId = tagId; | |||||
tagList->addChild(item); | tagList->addChild(item); | ||||
} | } | ||||
} | } | ||||
@@ -421,7 +421,7 @@ struct ModuleBrowser : widget::OpaqueWidget { | |||||
std::string search; | std::string search; | ||||
std::string brand; | std::string brand; | ||||
std::string tag; | |||||
int tagId = -1; | |||||
ModuleBrowser() { | ModuleBrowser() { | ||||
sidebar = new BrowserSidebar; | sidebar = new BrowserSidebar; | ||||
@@ -453,7 +453,7 @@ struct ModuleBrowser : widget::OpaqueWidget { | |||||
} | } | ||||
} | } | ||||
refresh(); | |||||
clear(); | |||||
} | } | ||||
void step() override { | void step() override { | ||||
@@ -484,7 +484,7 @@ struct ModuleBrowser : widget::OpaqueWidget { | |||||
for (Widget* w : modelContainer->children) { | for (Widget* w : modelContainer->children) { | ||||
ModelBox* m = dynamic_cast<ModelBox*>(w); | ModelBox* m = dynamic_cast<ModelBox*>(w); | ||||
assert(m); | assert(m); | ||||
m->visible = isModelVisible(m->model, search, brand, tag); | |||||
m->visible = isModelVisible(m->model, search, brand, tagId); | |||||
} | } | ||||
// Sort ModelBoxes | // Sort ModelBoxes | ||||
@@ -524,13 +524,13 @@ struct ModuleBrowser : widget::OpaqueWidget { | |||||
for (Widget* w : modelContainer->children) { | for (Widget* w : modelContainer->children) { | ||||
ModelBox* m = dynamic_cast<ModelBox*>(w); | ModelBox* m = dynamic_cast<ModelBox*>(w); | ||||
assert(m); | assert(m); | ||||
if (isModelVisible(m->model, search, "", "")) | |||||
if (isModelVisible(m->model, search, "", -1)) | |||||
filteredModels.push_back(m->model); | filteredModels.push_back(m->model); | ||||
} | } | ||||
auto hasModel = [&](const std::string & brand, const std::string & tag) -> bool { | |||||
auto hasModel = [&](const std::string & brand, int tagId) -> bool { | |||||
for (plugin::Model* model : filteredModels) { | for (plugin::Model* model : filteredModels) { | ||||
if (isModelVisible(model, "", brand, tag)) | |||||
if (isModelVisible(model, "", brand, tagId)) | |||||
return true; | return true; | ||||
} | } | ||||
return false; | return false; | ||||
@@ -541,7 +541,7 @@ struct ModuleBrowser : widget::OpaqueWidget { | |||||
for (Widget* w : sidebar->brandList->children) { | for (Widget* w : sidebar->brandList->children) { | ||||
BrandItem* item = dynamic_cast<BrandItem*>(w); | BrandItem* item = dynamic_cast<BrandItem*>(w); | ||||
assert(item); | assert(item); | ||||
item->disabled = !hasModel(item->text, tag); | |||||
item->disabled = !hasModel(item->text, tagId); | |||||
if (!item->disabled) | if (!item->disabled) | ||||
brandsLen++; | brandsLen++; | ||||
} | } | ||||
@@ -551,7 +551,7 @@ struct ModuleBrowser : widget::OpaqueWidget { | |||||
for (Widget* w : sidebar->tagList->children) { | for (Widget* w : sidebar->tagList->children) { | ||||
TagItem* item = dynamic_cast<TagItem*>(w); | TagItem* item = dynamic_cast<TagItem*>(w); | ||||
assert(item); | assert(item); | ||||
item->disabled = !hasModel(brand, item->text); | |||||
item->disabled = !hasModel(brand, item->tagId); | |||||
if (!item->disabled) | if (!item->disabled) | ||||
tagsLen++; | tagsLen++; | ||||
} | } | ||||
@@ -570,7 +570,7 @@ struct ModuleBrowser : widget::OpaqueWidget { | |||||
search = ""; | search = ""; | ||||
sidebar->searchField->setText(""); | sidebar->searchField->setText(""); | ||||
brand = ""; | brand = ""; | ||||
tag = ""; | |||||
tagId = -1; | |||||
refresh(); | refresh(); | ||||
} | } | ||||
@@ -601,17 +601,17 @@ inline void BrandItem::step() { | |||||
inline void TagItem::onAction(const event::Action& e) { | inline void TagItem::onAction(const event::Action& e) { | ||||
ModuleBrowser* browser = getAncestorOfType<ModuleBrowser>(); | ModuleBrowser* browser = getAncestorOfType<ModuleBrowser>(); | ||||
if (browser->tag == text) | |||||
browser->tag = ""; | |||||
if (browser->tagId == tagId) | |||||
browser->tagId = -1; | |||||
else | else | ||||
browser->tag = text; | |||||
browser->tagId = tagId; | |||||
browser->refresh(); | browser->refresh(); | ||||
} | } | ||||
inline void TagItem::step() { | inline void TagItem::step() { | ||||
MenuItem::step(); | MenuItem::step(); | ||||
ModuleBrowser* browser = getAncestorOfType<ModuleBrowser>(); | ModuleBrowser* browser = getAncestorOfType<ModuleBrowser>(); | ||||
active = (browser->tag == text); | |||||
active = (browser->tagId == tagId); | |||||
} | } | ||||
inline void BrowserSearchField::onSelectKey(const event::SelectKey& e) { | inline void BrowserSearchField::onSelectKey(const event::SelectKey& e) { | ||||
@@ -31,10 +31,9 @@ void Model::fromJson(json_t* rootJ) { | |||||
json_t* tagJ; | json_t* tagJ; | ||||
json_array_foreach(tagsJ, i, tagJ) { | json_array_foreach(tagsJ, i, tagJ) { | ||||
std::string tag = json_string_value(tagJ); | std::string tag = json_string_value(tagJ); | ||||
// Normalize tag | |||||
tag = tag::normalize(tag); | |||||
if (tag != "") | |||||
tags.push_back(tag); | |||||
int tagId = tag::findId(tag); | |||||
if (tagId >= 0) | |||||
tags.push_back(tagId); | |||||
} | } | ||||
} | } | ||||
@@ -8,108 +8,76 @@ namespace rack { | |||||
namespace tag { | namespace tag { | ||||
/** List of allowed tags in human display form, alphabetized. | |||||
All tags here should be in sentence caps for display consistency. | |||||
However, tags are case-insensitive in plugin metadata. | |||||
*/ | |||||
const std::set<std::string> allowedTags = { | |||||
"Arpeggiator", | |||||
"Attenuator", // With a level knob and not much else. | |||||
"Blank", // No parameters or ports. Serves no purpose except visual. | |||||
"Chorus", | |||||
"Clock generator", | |||||
"Clock modulator", // Clock dividers, multipliers, etc. | |||||
"Compressor", // With threshold, ratio, knee, etc parameters. | |||||
"Controller", // Use only if the artist "performs" with this module. Simply having knobs is not enough. 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", | |||||
"Expander", // Expands the functionality of a "mother" module when placed next to it. Expanders should inherit the tags of its mother module. | |||||
"External", | |||||
"Filter", | |||||
"Flanger", | |||||
"Function generator", | |||||
"Granular", | |||||
"Limiter", | |||||
"Logic", | |||||
"Low-frequency oscillator", | |||||
"Low-pass gate", | |||||
"MIDI", | |||||
"Mixer", | |||||
"Multiple", | |||||
"Noise", | |||||
"Oscillator", | |||||
"Panning", | |||||
"Phaser", | |||||
"Physical modeling", | |||||
"Polyphonic", | |||||
"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, at the minimum, a built-in oscillator and envelope. | |||||
"Tuner", | |||||
"Utility", // Serves only extremely basic functions, like inverting, max, min, multiplying by 2, etc. | |||||
"Visual", | |||||
"Vocoder", | |||||
"Voltage-controlled amplifier", | |||||
"Waveshaper", | |||||
}; | |||||
/** List of common synonyms for allowed tags. | |||||
Aliases and tags must be lowercase. | |||||
*/ | |||||
const std::map<std::string, std::string> tagAliases = { | |||||
{"amplifier", "voltage-controlled amplifier"}, | |||||
{"clock", "clock generator"}, | |||||
{"drums", "drum"}, | |||||
{"eq", "equalizer"}, | |||||
{"lfo", "low-frequency oscillator"}, | |||||
{"low frequency oscillator", "low-frequency oscillator"}, | |||||
{"low pass gate", "low-pass gate"}, | |||||
{"lowpass gate", "low-pass gate"}, | |||||
{"percussion", "drum"}, | |||||
{"poly", "polyphonic"}, | |||||
{"s&h", "sample and hold"}, | |||||
{"sample & hold", "sample and hold"}, | |||||
{"vca", "voltage-controlled amplifier"}, | |||||
{"vcf", "filter"}, | |||||
{"vco", "oscillator"}, | |||||
{"voltage controlled amplifier", "voltage-controlled amplifier"}, | |||||
{"voltage controlled filter", "filter"}, | |||||
{"voltage controlled oscillator", "oscillator"}, | |||||
}; | |||||
std::string normalize(const std::string& tag) { | |||||
int findId(const std::string& tag) { | |||||
std::string lowercaseTag = string::lowercase(tag); | std::string lowercaseTag = string::lowercase(tag); | ||||
// Transform aliases | |||||
auto it = tagAliases.find(lowercaseTag); | |||||
if (it != tagAliases.end()) | |||||
lowercaseTag = it->second; | |||||
// Find allowed tag | |||||
for (const std::string& allowedTag : allowedTags) { | |||||
if (lowercaseTag == string::lowercase(allowedTag)) | |||||
return allowedTag; | |||||
for (int tagId = 0; tagId < (int) tagAliases.size(); tagId++) { | |||||
for (const std::string& alias : tagAliases[tagId]) { | |||||
if (string::lowercase(alias) == lowercaseTag) | |||||
return tagId; | |||||
} | |||||
} | } | ||||
return ""; | |||||
return -1; | |||||
} | } | ||||
const std::vector<std::vector<std::string>> tagAliases = { | |||||
{"Arpeggiator"}, | |||||
{"Attenuator"}, // With a level knob and not much else. | |||||
{"Blank"}, // No parameters or ports. Serves no purpose except visual. | |||||
{"Chorus"}, | |||||
{"Clock generator", "Clock"}, | |||||
{"Clock modulator"}, // Clock dividers, multipliers, etc. | |||||
{"Compressor"}, // With threshold, ratio, knee, etc parameters. | |||||
{"Controller"}, // Use only if the artist "performs" with this module. Simply having knobs is not enough. Examples: on-screen keyboard, XY pad. | |||||
{"Delay"}, | |||||
{"Digital"}, | |||||
{"Distortion"}, | |||||
{"Drum", "Drums", "Percussion"}, | |||||
{"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", "EQ"}, | |||||
{"Expander"}, // Expands the functionality of a "mother" module when placed next to it. Expanders should inherit the tags of its mother module. | |||||
{"External"}, | |||||
{"Filter", "VCF", "Voltage controlled filter"}, | |||||
{"Flanger"}, | |||||
{"Function generator"}, | |||||
{"Granular"}, | |||||
{"Limiter"}, | |||||
{"Logic"}, | |||||
{"Low-frequency oscillator", "LFO", "Low frequency oscillator"}, | |||||
{"Low-pass gate", "Low pass gate", "Lowpass gate"}, | |||||
{"MIDI"}, | |||||
{"Mixer"}, | |||||
{"Multiple"}, | |||||
{"Noise"}, | |||||
{"Oscillator", "VCO", "Voltage controlled oscillator"}, | |||||
{"Panning", "Pan"}, | |||||
{"Phaser"}, | |||||
{"Physical modeling"}, | |||||
{"Polyphonic", "Poly"}, | |||||
{"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", "S&H", "Sample & hold"}, | |||||
{"Sampler"}, | |||||
{"Sequencer"}, | |||||
{"Slew limiter"}, | |||||
{"Switch"}, | |||||
{"Synth voice"}, // A synth voice must have, at the minimum, a built-in oscillator and envelope. | |||||
{"Tuner"}, | |||||
{"Utility"}, // Serves only extremely basic functions, like inverting, max, min, multiplying by 2, etc. | |||||
{"Visual"}, | |||||
{"Vocoder"}, | |||||
{"Voltage-controlled amplifier", "Amplifier", "VCA", "Voltage controlled amplifier"}, | |||||
{"Waveshaper"}, | |||||
}; | |||||
} // namespace tag | } // namespace tag | ||||
} // namespace rack | } // namespace rack |