@@ -31,8 +31,10 @@ struct Model { | |||
std::string slug; | |||
/** Human readable name for your model, e.g. "Voltage Controlled Oscillator" */ | |||
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 */ | |||
std::string description; | |||
@@ -1,15 +1,27 @@ | |||
#pragma once | |||
#include <common.hpp> | |||
#include <set> | |||
#include <vector> | |||
namespace rack { | |||
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 | |||
@@ -46,15 +46,18 @@ static float modelScore(plugin::Model* model, const std::string& search) { | |||
s += model->name; | |||
s += " "; | |||
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)); | |||
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 | |||
if (search != "") { | |||
float score = modelScore(model, search); | |||
@@ -69,15 +72,9 @@ static bool isModelVisible(plugin::Model* model, const std::string& search, cons | |||
} | |||
// 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; | |||
} | |||
@@ -255,7 +252,8 @@ struct ModelBox : widget::OpaqueWidget { | |||
for (size_t i = 0; i < model->tags.size(); i++) { | |||
if (i > 0) | |||
text += ", "; | |||
text += model->tags[i]; | |||
int tagId = model->tags[i]; | |||
text += tag::tagAliases[tagId][0]; | |||
} | |||
// Description | |||
if (model->description != "") { | |||
@@ -285,6 +283,7 @@ struct BrandItem : ui::MenuItem { | |||
struct TagItem : ui::MenuItem { | |||
int tagId; | |||
void onAction(const event::Action& e) override; | |||
void step() override; | |||
}; | |||
@@ -378,9 +377,10 @@ struct BrowserSidebar : widget::Widget { | |||
tagList = new ui::List; | |||
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; | |||
item->text = tag; | |||
item->text = tag::tagAliases[tagId][0]; | |||
item->tagId = tagId; | |||
tagList->addChild(item); | |||
} | |||
} | |||
@@ -421,7 +421,7 @@ struct ModuleBrowser : widget::OpaqueWidget { | |||
std::string search; | |||
std::string brand; | |||
std::string tag; | |||
int tagId = -1; | |||
ModuleBrowser() { | |||
sidebar = new BrowserSidebar; | |||
@@ -453,7 +453,7 @@ struct ModuleBrowser : widget::OpaqueWidget { | |||
} | |||
} | |||
refresh(); | |||
clear(); | |||
} | |||
void step() override { | |||
@@ -484,7 +484,7 @@ struct ModuleBrowser : widget::OpaqueWidget { | |||
for (Widget* w : modelContainer->children) { | |||
ModelBox* m = dynamic_cast<ModelBox*>(w); | |||
assert(m); | |||
m->visible = isModelVisible(m->model, search, brand, tag); | |||
m->visible = isModelVisible(m->model, search, brand, tagId); | |||
} | |||
// Sort ModelBoxes | |||
@@ -524,13 +524,13 @@ struct ModuleBrowser : widget::OpaqueWidget { | |||
for (Widget* w : modelContainer->children) { | |||
ModelBox* m = dynamic_cast<ModelBox*>(w); | |||
assert(m); | |||
if (isModelVisible(m->model, search, "", "")) | |||
if (isModelVisible(m->model, search, "", -1)) | |||
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) { | |||
if (isModelVisible(model, "", brand, tag)) | |||
if (isModelVisible(model, "", brand, tagId)) | |||
return true; | |||
} | |||
return false; | |||
@@ -541,7 +541,7 @@ struct ModuleBrowser : widget::OpaqueWidget { | |||
for (Widget* w : sidebar->brandList->children) { | |||
BrandItem* item = dynamic_cast<BrandItem*>(w); | |||
assert(item); | |||
item->disabled = !hasModel(item->text, tag); | |||
item->disabled = !hasModel(item->text, tagId); | |||
if (!item->disabled) | |||
brandsLen++; | |||
} | |||
@@ -551,7 +551,7 @@ struct ModuleBrowser : widget::OpaqueWidget { | |||
for (Widget* w : sidebar->tagList->children) { | |||
TagItem* item = dynamic_cast<TagItem*>(w); | |||
assert(item); | |||
item->disabled = !hasModel(brand, item->text); | |||
item->disabled = !hasModel(brand, item->tagId); | |||
if (!item->disabled) | |||
tagsLen++; | |||
} | |||
@@ -570,7 +570,7 @@ struct ModuleBrowser : widget::OpaqueWidget { | |||
search = ""; | |||
sidebar->searchField->setText(""); | |||
brand = ""; | |||
tag = ""; | |||
tagId = -1; | |||
refresh(); | |||
} | |||
@@ -601,17 +601,17 @@ inline void BrandItem::step() { | |||
inline void TagItem::onAction(const event::Action& e) { | |||
ModuleBrowser* browser = getAncestorOfType<ModuleBrowser>(); | |||
if (browser->tag == text) | |||
browser->tag = ""; | |||
if (browser->tagId == tagId) | |||
browser->tagId = -1; | |||
else | |||
browser->tag = text; | |||
browser->tagId = tagId; | |||
browser->refresh(); | |||
} | |||
inline void TagItem::step() { | |||
MenuItem::step(); | |||
ModuleBrowser* browser = getAncestorOfType<ModuleBrowser>(); | |||
active = (browser->tag == text); | |||
active = (browser->tagId == tagId); | |||
} | |||
inline void BrowserSearchField::onSelectKey(const event::SelectKey& e) { | |||
@@ -31,10 +31,9 @@ void Model::fromJson(json_t* rootJ) { | |||
json_t* tagJ; | |||
json_array_foreach(tagsJ, i, 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 { | |||
/** 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); | |||
// 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 rack |