From f9f64bb585b20728497f0bf1bea29f5825f88183 Mon Sep 17 00:00:00 2001 From: Paul Dempsey Date: Wed, 27 Sep 2023 08:31:51 -0700 Subject: [PATCH] Import HE group, arrange favorites menu, doc --- Makefile | 1 + doc/HC-1.md | 15 +++-- doc/HC-3.md | 14 +++-- src/HC-1/HC-1-menu.cpp | 74 ++++++++++++++++------ src/HC-1/HC-1-presets.cpp | 38 ++++++++++- src/HC-1/HC-1-ui.cpp | 17 ----- src/HC-1/HC-1.hpp | 2 + src/he_group.cpp | 128 ++++++++++++++++++++++++++++++++++++++ src/he_group.hpp | 37 +++++++++++ 9 files changed, 276 insertions(+), 50 deletions(-) create mode 100644 src/he_group.cpp create mode 100644 src/he_group.hpp diff --git a/Makefile b/Makefile index 6af629d..af01828 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ SOURCES += src/components.cpp SOURCES += src/em_midi.cpp SOURCES += src/em_pedal.cpp SOURCES += src/em_types.cpp +SOURCES += src/he_group.cpp SOURCES += src/misc.cpp SOURCES += src/module_broker.cpp SOURCES += src/open_file.cpp diff --git a/doc/HC-1.md b/doc/HC-1.md index 2875885..412431a 100644 --- a/doc/HC-1.md +++ b/doc/HC-1.md @@ -192,9 +192,14 @@ Right click on the **Favorite** tab to see the **Favorite menu**. | -- | -- | -- | | **Sort** | **Alphabetically** | Sort favorites alphabetically by name. | | | **by Category** | Sort favorites first by Category, then alphabetically within the category. | -| **Clear** | | Empty the Favorites list. | | **Open...** | | Load favorites from a file (`.fav` or `.json`) | -| **Save as...** | | Save current favorites to a `.fav` or `.json` file. | +| **Add from...** | | Append to the current list from a favorites file. | +| **Import Haken Editor group list** | | Create Favorites from a Haken editor group listing. Presets named in the group file are first matched by name from user presets, then from system presets. The first matching name is used. If a name is not found, it is silently omitted. | +| **Forget and clear** | | Forget the current favorites file (if any) and empty the list. | +| File _name_ | | Shows current favorites file name or `(none)` if no favorites file is open. | +| **Save as...** | | Save current favorites to a `.fav` or `.json` file, and switch to that file. | +| **Save copy as...** | | Save current favorites to a new `.fav` or `.json` file, but remain in the current favorites file. | +| **Clear** | | Empty the current Favorites file. | ## System tab menu @@ -203,9 +208,9 @@ Right click on the **System** tab to see the **System menu** for navigation and | Menu | Option | Description | | -- | -- | -- | | **Go to category** | **Strings (ST)
Winds (WI)
Vocal (VO)
Keyboard (KY)
Classic (CL)
Other (OT)
Percussion (PE)
Tuned (PT)
Processor (PR)
Drone (DO)
Midi (MD)
Control (CV)
Utility (UT)** | Moves to the **System** tab with **Sort by Category** and selects the first preset in the category, or the current preset if it is in that category. | -| **Sort System presets** | **Alphabetically** | (default) Show system presets alphabetically by name. | -| | **by Category** | Sort first by Category, then alphabetically within the category. | -| | **in System order** | Show system presets in internal System order. This is the numerical order in the Haken Editor "File 2" list, which is mostly but not entirely alphabetical. On the Continuum display, presets are in Category order. | +| **Sort System presets** | **Alphabetically** | (default) Sort system presets alphabetically by name. | +| | **by Category** | Sort first by Category, then alphabetically within the category. This is the order that presets are shown on a Continuum. | +| | **in System order** | Show system presets in internal System order. This is the numerical order in the Haken Editor "File 2" list, which is mostly but not entirely alphabetical. | ## Notes diff --git a/doc/HC-3.md b/doc/HC-3.md index ecf5010..afc743c 100644 --- a/doc/HC-3.md +++ b/doc/HC-3.md @@ -5,17 +5,19 @@ If you want access to more than 16, just add another instance of HC-3. ![HC-3 module](HC-3.png) -HC-3 initially blank. Right click a button to choose an existing favorites file for that button. +HC-3 is initially blank when added to a ptach. Right click a button to choose an existing favorites file for that button. +If a Favorites file is currently open in HC-1, you can choose **Use _file_** so attach it to the button. If you don't have any favorites files yet, you can create one by saving from the HC-1 **Favorite** tab menu. Once there's a file associated with the button, clicking it will load that file into the **Favorite** tab in the associated HC-1 and the LED on the button lights up. -Clicking again, and the file is forgotton in HC-1 (but the favorites remain), and the light goes off. +Click again and the file is forgotton in HC-1 and the light goes off. The favorites remain listed in HC-1. -Any favorites file open in the companion HC-1 is automatically updated as you change the favorites in the **Favorite** tab (Add, Remove, Sort, Clear, ...). There is no undo. +Any favorites file open in HC-1 is automatically updated as you change the favorites in the **Favorite** tab (Add, Remove, Sort, Clear, ...). There is no undo. -To save a set of favorites files, use the options in the module Preset menu (just like loading and saving configuration of any Rack module). +Once you've configured HC-3, I recommend saving your work as a Preset using the module Preset menu (just like loading and saving configuration of any Rack module). +This allows you to use the same set of favorites in other Rack patches by selecting that preset. -If you use a favorites file from one device on another device, you'll only get the favorites that have the same name on both devices. -If the favorites file contains user presets that are no longer on the device, they are silently ignored. +If you use a favorites file from one EM device on another EM device, you'll only get the favorites that have the same name on both devices, and the file will be changed to reflect the common set. +Similarly, if the favorites file contains user presets that are no longer on the device, they are silently removed. ![pachde (#d) logo](Logo.svg) diff --git a/src/HC-1/HC-1-menu.cpp b/src/HC-1/HC-1-menu.cpp index 3e28969..47e2af9 100644 --- a/src/HC-1/HC-1-menu.cpp +++ b/src/HC-1/HC-1-menu.cpp @@ -86,10 +86,27 @@ void Hc1ModuleWidget::addSystemMenu(Menu *menu) void Hc1ModuleWidget::addFavoritesMenu(Menu *menu) { - bool ready = my_module->ready(); + bool unready = !my_module->ready(); std::string filename = my_module->favoritesFile.empty() ? "(none)" : system::getFilename(my_module->favoritesFile); + menu->addChild(createMenuItem("Favorite presets", "", [](){}, true)); + menu->addChild(new MenuSeparator); + if (my_module) { + menu->addChild(createSubmenuItem("Sort", "", [=](Menu* menu) { + menu->addChild(createMenuItem("Alphabetically", "", [=](){ + my_module->sortFavorites(PresetOrder::Alpha); + my_module->saveFavorites(); + populatePresetWidgets(); + }, unready)); + menu->addChild(createMenuItem("by Category", "", [=](){ + my_module->sortFavorites(PresetOrder::Category); + my_module->saveFavorites(); + populatePresetWidgets(); + }, unready)); + }, unready)); + } + menu->addChild(createMenuItem("Open...", "", [=]() { std::string path; std::string folder = asset::user(pluginInstance->slug); @@ -101,15 +118,46 @@ void Hc1ModuleWidget::addFavoritesMenu(Menu *menu) path)) { my_module->openFavoritesFile(path); } - }, !ready)); + }, unready)); + + menu->addChild(createMenuItem("Add from...", "", [=]() { + std::string path; + std::string folder = asset::user(pluginInstance->slug); + system::createDirectories(folder); + if (openFileDialog( + folder, + "Favorites (.fav):fav;Json (.json):json;Any (*):*))", + "", + path)) { + my_module->readFavoritesFile(path, false); + updatePresetWidgets(); + } + }, unready)); + menu->addChild(createMenuItem("Import Haken Editor group list...", "", [=]() { + std::string path; + std::string folder = asset::user(pluginInstance->slug); + system::createDirectories(folder); + if (openFileDialog( + folder, + "Haken Editor group list (.txt):txt;Any (*):*.*", + "", + path)) { + my_module->importHEGroupFile(path); + } + }, unready)); + menu->addChild(createMenuItem("Forget and clear", "", [=]() { + my_module->favoritesFile = ""; + my_module->clearFavorites(); + my_module->notifyFavoritesFileChanged(); + })); menu->addChild(new MenuSeparator); menu->addChild(createMenuLabel(format_string("File: %s", filename.c_str()))); if (!my_module->favoritesFile.empty()) { menu->addChild(createMenuItem(format_string("Forget %s", filename.c_str()), "", [=]() { my_module->favoritesFile = ""; - }, !ready)); + }, unready)); } std::string prompt = my_module->favoritesFile.empty() ? "Save as..." : format_string("Save %s as...", filename.c_str()); @@ -126,7 +174,7 @@ void Hc1ModuleWidget::addFavoritesMenu(Menu *menu) my_module->favoritesFile = path; my_module->notifyFavoritesFileChanged(); } - }, !ready)); + }, unready)); menu->addChild(createMenuItem("Save copy as...", "", [=]() { std::string path; @@ -140,21 +188,7 @@ void Hc1ModuleWidget::addFavoritesMenu(Menu *menu) path)) { my_module->writeFavoritesFile(path); } - }, !ready || my_module->favoritesFile.empty())); - - menu->addChild(createMenuItem("Add from...", "", [=]() { - std::string path; - std::string folder = asset::user(pluginInstance->slug); - system::createDirectories(folder); - if (openFileDialog( - folder, - "Favorites (.fav):fav;Json (.json):json;Any (*):*))", - "", - path)) { - my_module->readFavoritesFile(path, false); - updatePresetWidgets(); - } - }, !ready)); + }, unready || my_module->favoritesFile.empty())); prompt = my_module->favoritesFile.empty() ? "Clear" : format_string("Clear %s", filename.c_str()); menu->addChild(createMenuItem(prompt, "", [=](){ @@ -163,7 +197,7 @@ void Hc1ModuleWidget::addFavoritesMenu(Menu *menu) if (tab == PresetTab::Favorite) { updatePresetWidgets(); } - }, !ready || my_module->favorite_presets.empty())); + }, unready || my_module->favorite_presets.empty())); } diff --git a/src/HC-1/HC-1-presets.cpp b/src/HC-1/HC-1-presets.cpp index f38c074..fc6e186 100644 --- a/src/HC-1/HC-1-presets.cpp +++ b/src/HC-1/HC-1-presets.cpp @@ -1,4 +1,5 @@ #include "HC-1.hpp" +#include "../he_group.hpp" namespace pachde { void Hc1Module::tryCachedPresets() { @@ -227,10 +228,10 @@ void Hc1Module::clearFavorites() { favorite_presets.clear(); for (auto p: user_presets) { - if (p->favorite) p->favorite = false; + p->favorite = false; } for (auto p: system_presets) { - if (p->favorite) p->favorite = false; + p->favorite = false; } } @@ -339,6 +340,39 @@ void Hc1Module::openFavoritesFile(const std::string& path) notifyFavoritesFileChanged(); } +void Hc1Module::importHEGroupFile(const std::string& path) +{ + auto items = he_group::ReadGroupFile(path); + favoritesFile = ""; + clearFavorites(); + auto bf = BulkFavoritingMode(this); + for (auto item : items) { + auto p = findDefinedPresetByName(item.name); + if (p) { + addFavorite(p); + } + } + notifyFavoritesFileChanged(); +} + +std::shared_ptr Hc1Module::findDefinedPresetByName(std::string name) +{ + if (name.empty()) return nullptr; + if (!user_presets.empty()) { + auto it = std::find_if(user_presets.begin(), user_presets.end(), [name](std::shared_ptr& p) { return p->name == name; }); + if (it != user_presets.end()) { + return *it; + } + } + if (!system_presets.empty()) { + auto it = std::find_if(system_presets.begin(), system_presets.end(),[name](std::shared_ptr& p) { return p->name == name; }); + if (it != system_presets.end()) { + return *it; + } + } + return nullptr; +} + std::shared_ptr Hc1Module::findDefinedPreset(std::shared_ptr preset) { if (preset) { diff --git a/src/HC-1/HC-1-ui.cpp b/src/HC-1/HC-1-ui.cpp index bd54e57..93e8a51 100644 --- a/src/HC-1/HC-1-ui.cpp +++ b/src/HC-1/HC-1-ui.cpp @@ -54,23 +54,6 @@ void Hc1ModuleWidget::createPresetGrid() tab_bar->addTab("User", PresetTab::User, nullptr); tab_bar->addTab("Favorite", PresetTab::Favorite, [=](Menu* menu) { - bool unready = !my_module->ready(); - menu->addChild(createMenuItem("Favorite presets", "", [](){}, true)); - menu->addChild(new MenuSeparator); - if (my_module) { - menu->addChild(createSubmenuItem("Sort", "", [=](Menu* menu) { - menu->addChild(createMenuItem("Alphabetically", "", [=](){ - my_module->sortFavorites(PresetOrder::Alpha); - my_module->saveFavorites(); - populatePresetWidgets(); - }, unready)); - menu->addChild(createMenuItem("by Category", "", [=](){ - my_module->sortFavorites(PresetOrder::Category); - my_module->saveFavorites(); - populatePresetWidgets(); - }, unready)); - }, unready)); - } addFavoritesMenu(menu); }); tab_bar->addTab("System", PresetTab::System, [=](Menu* menu){ diff --git a/src/HC-1/HC-1.hpp b/src/HC-1/HC-1.hpp index c8a6a40..317aae9 100644 --- a/src/HC-1/HC-1.hpp +++ b/src/HC-1/HC-1.hpp @@ -98,6 +98,7 @@ struct Hc1Module : IPresetHolder, ISendMidi, ISetDevice, midi::Input, Module bool readFavoritesFile(const std::string& path, bool fresh); void writeFavoritesFile(const std::string& path); void openFavoritesFile(const std::string& path); + void importHEGroupFile(const std::string& path); json_t* favoritesToJson(); void favoritesFromPresets(); @@ -344,6 +345,7 @@ struct Hc1Module : IPresetHolder, ISendMidi, ISetDevice, midi::Input, Module void sortFavorites(PresetOrder order = PresetOrder::Favorite); void sendSavedPreset(); + std::shared_ptr findDefinedPresetByName(std::string name); std::shared_ptr findDefinedPreset(std::shared_ptr preset); void syncStatusLights(); diff --git a/src/he_group.cpp b/src/he_group.cpp new file mode 100644 index 0000000..0baa35c --- /dev/null +++ b/src/he_group.cpp @@ -0,0 +1,128 @@ +#include "he_group.hpp" +namespace he_group { + +#define VERBOSE_LOG +#include "debug_log.hpp" + +bool he_item_order(const HEPresetItem& i1, const HEPresetItem& i2) +{ + return i1.index < i2.index; +} + +using CBIT = std::vector::const_iterator; + +enum class HakenPresetItemParseState { // Haken Preset Item Parse State + start, + num, + comma, + quote, + end +}; +using PS = HakenPresetItemParseState; + +bool parse_line(CBIT it, CBIT end, std::vector& result) +{ + PS state = PS::start; + int num = 0; + while (it != end && state != PS::end) { + switch (state) { + case PS::start: + if (std::isspace(*it)) { + ++it; + } else if (std::isdigit(*it)) { + num = (*it - '0'); + ++it; + state = PS::num; + } else { + return false; + } + break; + + case PS::num: + if (std::isdigit(*it)) { + num = num * 10 + (*it - '0'); + ++it; + } else { + state = PS::comma; + } + break; + + case PS::comma: + if (',' == *it) { + state = PS::quote; + ++it; + } else if (std::isspace(*it)) { + ++it; + } else { + return false; + } + break; + + case PS::quote: + if ('"' == *it) { + ++it; + state = PS::end; + } else if (std::isspace(*it)) { + ++it; + } else { + state = PS::end; + } + break; + + case PS::end: + assert(false); + break; + } + } + + CBIT last = end; + // trim trailing space and quote + --last; + while (*last <= 32) { --last; } + if ('"' == *last) { --last; } + end = last + 1; + + bool dot_mid = false; + if ('d' == *last || 'D' == *last) { + --last; + if ('i' == *last || 'I' == *last) { + --last; + if ('m' == *last || 'M' == *last) { + --last; + if ('.' == *last || '.' == *last) { + dot_mid = true; + } + } + } + } + if (dot_mid) end = last; + result.push_back(HEPresetItem{num, std::string(it, end)}); + return true; +} + + +std::vector ReadGroupFile(const std::string& path) +{ + std::vector result; + if (path.empty()) return result; + if (!system::isFile(path)) return result; + try { + auto bytes = system::readFile(path); + if (bytes.size() <= 5) { return result; } + + auto it = bytes.cbegin(); + while (it != bytes.cend()) { + auto line_start = it; + while (*it != '\n' && it != bytes.cend()) { + ++it; + } + if (!parse_line(line_start, it, result)) { break; } + while (*it <= 32 && it != bytes.cend()) { ++it; } + } + std::sort(result.begin(), result.end(), he_item_order); + } catch(Exception& e) { + DebugLog("Exception %s", e.msg.c_str()); + } + return result; +} +} \ No newline at end of file diff --git a/src/he_group.hpp b/src/he_group.hpp new file mode 100644 index 0000000..f25c81b --- /dev/null +++ b/src/he_group.hpp @@ -0,0 +1,37 @@ +// parse Haken Editor group file +#pragma once +#include +using namespace ::rack; + +namespace he_group { + +/* -- sample group file -- +16,"SineSpray Rain via Surface.mid" + 15,"Zipper.mid" + 14,"Mojo of FDN.mid" + 13,"Marlin Perkins 1.mid" + 11,"Bowed Mood.mid" + 10,"Living Pad.mid" + 9,"Shimmer.mid" + 8,"Cumulus.mid" + 7,"Choir.mid" + 6,"Bass Monster.mid" + 5,"Snap Bass.mid" + 4,"Jaymar Toy Piano.mid" + 3,"Woodwind.mid" + 2,"Tin Whistle.mid" + 1,"Vln Vla Cel Bass 2.mid" +# put comments at end. Anything invalid stops import, so +anything after this won't get imported. +12,"Ishango Bone.mid" +*/ + +struct HEPresetItem +{ + int index; + std::string name; +}; + +std::vector ReadGroupFile(const std::string& path); + +} \ No newline at end of file