Browse Source

Restructure the tag/alias data structure.

tags/v1.1.4
Andrew Belt 4 years ago
parent
commit
469d66024a
5 changed files with 117 additions and 136 deletions
  1. +4
    -2
      include/plugin/Model.hpp
  2. +15
    -3
      include/tag.hpp
  3. +29
    -29
      src/app/ModuleBrowser.cpp
  4. +3
    -4
      src/plugin/Model.cpp
  5. +66
    -98
      src/tag.cpp

+ 4
- 2
include/plugin/Model.hpp View File

@@ -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;



+ 15
- 3
include/tag.hpp View File

@@ -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


+ 29
- 29
src/app/ModuleBrowser.cpp View File

@@ -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) {


+ 3
- 4
src/plugin/Model.cpp View File

@@ -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);
}
}



+ 66
- 98
src/tag.cpp View File

@@ -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

Loading…
Cancel
Save