diff --git a/src/sfizz.h b/src/sfizz.h index 69faa3ed..a941d59a 100644 --- a/src/sfizz.h +++ b/src/sfizz.h @@ -380,6 +380,7 @@ SFIZZ_EXPORTED_API void sfizz_all_sound_off(sfizz_synth_t* synth); * @brief Add external definitions prior to loading; * Note that these do not get reset by loading or resetting the synth. * You need to call sfizz_clear_external_definitions() to erase them. + * @since 0.4.0-dev * * @param synth * @param id @@ -389,11 +390,59 @@ SFIZZ_EXPORTED_API void sfizz_add_external_definitions(sfizz_synth_t* synth, con /** * @brief Clears external definitions for the next file loading. + * @since 0.4.0-dev * * @param synth */ SFIZZ_EXPORTED_API void sfizz_clear_external_definitions(sfizz_synth_t* synth); +#define SFIZZ_OUT_OF_BOUNDS_LABEL_INDEX -1 + +/** + * @brief Get the number of key labels registered in the current sfz file + * @since 0.4.0-dev + */ +SFIZZ_EXPORTED_API unsigned int sfizz_get_num_key_labels(sfizz_synth_t* synth); + +/** + * @brief Get the key number for the label registered at index label_index. + * @since 0.4.0-dev + * + * @returns the number or SFIZZ_OUT_OF_BOUNDS_LABEL_INDEX if the index is out of bounds. + */ +SFIZZ_EXPORTED_API int sfizz_get_key_label_number(sfizz_synth_t* synth, int label_index); + +/** + * @brief Get the key text for the label registered at index label_index. + * @since 0.4.0-dev + * + * @returns the label or NULL if the index is out of bounds. + */ +SFIZZ_EXPORTED_API const char * sfizz_get_key_label_text(sfizz_synth_t* synth, int label_index); + +/** + * @brief Get the number of CC labels registered in the current sfz file + * @since 0.4.0-dev + * + */ +SFIZZ_EXPORTED_API unsigned int sfizz_get_num_cc_labels(sfizz_synth_t* synth); + +/** + * @brief Get the CC number for the label registered at index label_index. + * @since 0.4.0-dev + * + * @returns the number or SFIZZ_OUT_OF_BOUNDS_LABEL_INDEX if the index is out of bounds. + */ + +SFIZZ_EXPORTED_API int sfizz_get_cc_label_number(sfizz_synth_t* synth, int label_index); +/** + * @brief Get the CC text for the label registered at index label_index. + * @since 0.4.0-dev + * + * @returns the label or NULL if the index is out of bounds. + */ +SFIZZ_EXPORTED_API const char * sfizz_get_cc_label_text(sfizz_synth_t* synth, int label_index); + #ifdef __cplusplus } #endif diff --git a/src/sfizz.hpp b/src/sfizz.hpp index 49e9cd7a..9c9d9208 100644 --- a/src/sfizz.hpp +++ b/src/sfizz.hpp @@ -10,6 +10,7 @@ */ #include +#include #include #include @@ -287,7 +288,8 @@ public: void setLoggingPrefix(const std::string& prefix) noexcept; /** - * @brief Disable logging. + * @brief + * */ void disableLogging() noexcept; @@ -300,6 +302,7 @@ public: * @brief Add external definitions prior to loading; * Note that these do not get reset by loading or resetting the synth. * You need to call clearExternalDefintions() to erase them. + * @since 0.4.0-dev * * @param id * @param value @@ -308,10 +311,23 @@ public: /** * @brief Clears external definitions for the next file loading. + * @since 0.4.0-dev * */ void clearExternalDefinitions(); + /** + * @brief Get the key labels, if any + * @since 0.4.0-dev + * + */ + const std::vector>& getKeyLabels() const noexcept; + /** + * @brief Get the CC labels, if any + * @since 0.4.0-dev + * + */ + const std::vector>& getCCLabels() const noexcept; private: std::unique_ptr synth; }; diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 2d95854a..0c6d954c 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -208,6 +208,9 @@ bool sfz::Region::parseOpcode(const Opcode& opcode) setValueFromOpcode(opcode, keyswitch, Default::keyRange); keySwitched = false; break; + case hash("sw_label"): + keyswitchLabel = opcode.value; + break; case hash("sw_down"): setValueFromOpcode(opcode, keyswitchDown, Default::keyRange); keySwitched = false; diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index cd954671..0ab2dd82 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -261,6 +261,7 @@ struct Region { CCMap> ccConditions { Default::ccValueRange }; Range keyswitchRange { Default::keyRange }; // sw_hikey and sw_lokey absl::optional keyswitch {}; // sw_last + absl::optional keyswitchLabel {}; absl::optional keyswitchUp {}; // sw_up absl::optional keyswitchDown {}; // sw_down absl::optional previousNote {}; // sw_previous diff --git a/src/sfizz/SfzHelpers.h b/src/sfizz/SfzHelpers.h index 60979282..5d581a74 100644 --- a/src/sfizz/SfzHelpers.h +++ b/src/sfizz/SfzHelpers.h @@ -21,6 +21,8 @@ namespace sfz { using CCNamePair = std::pair; +using NoteNamePair = std::pair; + template using MidiNoteArray = std::array; template diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 8f9f1b4f..2783718c 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -142,7 +142,9 @@ void sfz::Synth::clear() numMasters = 0; defaultSwitch = absl::nullopt; defaultPath = ""; - ccNames.clear(); + resources.midiState.reset(); + ccLabels.clear(); + keyLabels.clear(); globalOpcodes.clear(); masterOpcodes.clear(); groupOpcodes.clear(); @@ -208,7 +210,11 @@ void sfz::Synth::handleControlOpcodes(const std::vector& members) case hash("Label_cc&"): // fallthrough case hash("label_cc&"): if (Default::ccNumberRange.containsWithEnd(member.parameters.back())) - ccNames.emplace_back(member.parameters.back(), std::string(member.value)); + ccLabels.emplace_back(member.parameters.back(), std::string(member.value)); + break; + case hash("label_key&"): + if (Default::keyRange.containsWithEnd(member.parameters.back())) + keyLabels.emplace_back(member.parameters.back(), std::string(member.value)); break; case hash("Default_path"): // fallthrough @@ -396,6 +402,10 @@ bool sfz::Synth::loadSfzFile(const fs::path& file) } } + if (region->keyswitchLabel && region->keyswitch) + keyswitchLabels.push_back({ *region->keyswitch, *region->keyswitchLabel }); + + // Some regions had group number but no "group-level" opcodes handled the polyphony while (groupMaxPolyphony.size() <= region->group) groupMaxPolyphony.push_back(config::maxVoices); @@ -872,12 +882,15 @@ std::string sfz::Synth::exportMidnam(absl::string_view model) const chns.append_child("UsesControlNameList") .append_attribute("Name") .set_value("Controls"); + chns.append_child("UsesNoteNameList") + .append_attribute("Name") + .set_value("Notes"); } { pugi::xml_node cns = device.append_child("ControlNameList"); cns.append_attribute("Name").set_value("Controls"); - for (const CCNamePair& pair : ccNames) { + for (const auto& pair : ccLabels) { pugi::xml_node cn = cns.append_child("Control"); cn.append_attribute("Type").set_value("7bit"); cn.append_attribute("Number").set_value(std::to_string(pair.first).c_str()); @@ -885,6 +898,21 @@ std::string sfz::Synth::exportMidnam(absl::string_view model) const } } + { + pugi::xml_node nnl = device.append_child("NoteNameList"); + nnl.append_attribute("Name").set_value("Notes"); + for (const auto& pair : keyswitchLabels) { + pugi::xml_node nn = nnl.append_child("Note"); + nn.append_attribute("Number").set_value(std::to_string(pair.first).c_str()); + nn.append_attribute("Name").set_value(pair.second.c_str()); + } + for (const auto& pair : keyLabels) { + pugi::xml_node nn = nnl.append_child("Note"); + nn.append_attribute("Number").set_value(std::to_string(pair.first).c_str()); + nn.append_attribute("Name").set_value(pair.second.c_str()); + } + } + /// struct string_writer : pugi::xml_writer { std::string result; diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 4d34d8e8..0eddea0b 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -388,6 +388,19 @@ public: */ const Parser& getParser() const noexcept { return parser; } + /** + * @brief Get the key labels, if any + * + * @return const std::vector& + */ + const std::vector& getKeyLabels() const noexcept { return keyLabels; } + /** + * @brief Get the CC labels, if any + * + * @return const std::vector& + */ + const std::vector& getCCLabels() const noexcept { return ccLabels; } + protected: /** * @brief The parser callback; this is called by the parent object each time @@ -494,8 +507,12 @@ private: * @return Voice* */ Voice* findFreeVoice() noexcept; - // Names for the cc as set by the label_cc opcode - std::vector ccNames; + + // Names for the CC and notes as set by label_cc and label_key + std::vector ccLabels; + std::vector keyLabels; + std::vector keyswitchLabels; + // Default active switch if multiple keyswitchable regions are present absl::optional defaultSwitch; std::vector unknownOpcodes; diff --git a/src/sfizz/sfizz.cpp b/src/sfizz/sfizz.cpp index 3e17af01..5ce7c133 100644 --- a/src/sfizz/sfizz.cpp +++ b/src/sfizz/sfizz.cpp @@ -220,3 +220,13 @@ void sfz::Sfizz::clearExternalDefinitions() { synth->getParser().clearExternalDefinitions(); } + +const std::vector>& sfz::Sfizz::getKeyLabels() const noexcept +{ + return synth->getKeyLabels(); +} + +const std::vector>& sfz::Sfizz::getCCLabels() const noexcept +{ + return synth->getCCLabels(); +} diff --git a/src/sfizz/sfizz_wrapper.cpp b/src/sfizz/sfizz_wrapper.cpp index 3c9d88f0..e27d0872 100644 --- a/src/sfizz/sfizz_wrapper.cpp +++ b/src/sfizz/sfizz_wrapper.cpp @@ -8,6 +8,7 @@ #include "Macros.h" #include "Synth.h" #include "sfizz.h" +#include #ifdef __cplusplus extern "C" { @@ -268,6 +269,80 @@ void sfizz_clear_external_definitions(sfizz_synth_t* synth) self->getParser().clearExternalDefinitions(); } +unsigned int sfizz_get_num_key_labels(sfizz_synth_t* synth) +{ + auto self = reinterpret_cast(synth); + return self->getKeyLabels().size(); +} + +int sfizz_get_key_label_number(sfizz_synth_t* synth, int label_index) +{ + auto self = reinterpret_cast(synth); + const auto keyLabels = self->getKeyLabels(); + if (label_index < 0) + return SFIZZ_OUT_OF_BOUNDS_LABEL_INDEX; + + if (static_cast(label_index) >= keyLabels.size()) + return SFIZZ_OUT_OF_BOUNDS_LABEL_INDEX; + + // Sanity checks for the future or platforms + static_assert( + std::numeric_limits::max() < std::numeric_limits::max(), + "The C API sends back an int but the note index in NoteNamePair can overflow it on this platform" + ); + return static_cast(keyLabels[label_index].first); +} + +const char * sfizz_get_key_label_text(sfizz_synth_t* synth, int label_index) +{ + auto self = reinterpret_cast(synth); + const auto keyLabels = self->getKeyLabels(); + if (label_index < 0) + return NULL; + + if (static_cast(label_index) >= keyLabels.size()) + return NULL; + + return keyLabels[label_index].second.c_str(); +} + +unsigned int sfizz_get_num_cc_labels(sfizz_synth_t* synth) +{ + auto self = reinterpret_cast(synth); + return self->getCCLabels().size(); +} + +int sfizz_get_cc_label_number(sfizz_synth_t* synth, int label_index) +{ + auto self = reinterpret_cast(synth); + const auto ccLabels = self->getCCLabels(); + if (label_index < 0) + return SFIZZ_OUT_OF_BOUNDS_LABEL_INDEX; + + if (static_cast(label_index) >= ccLabels.size()) + return SFIZZ_OUT_OF_BOUNDS_LABEL_INDEX; + + // Sanity checks for the future or platforms + static_assert( + std::numeric_limits::max() < std::numeric_limits::max(), + "The C API sends back an int but the cc index in CCNamePair can overflow it on this platform" + ); + return static_cast(ccLabels[label_index].first); +} + +const char * sfizz_get_cc_label_text(sfizz_synth_t* synth, int label_index) +{ + auto self = reinterpret_cast(synth); + const auto ccLabels = self->getCCLabels(); + if (label_index < 0) + return NULL; + + if (static_cast(label_index) >= ccLabels.size()) + return NULL; + + return ccLabels[label_index].second.c_str(); +} + #ifdef __cplusplus } #endif diff --git a/tests/FilesT.cpp b/tests/FilesT.cpp index f332186c..e6c2394e 100644 --- a/tests/FilesT.cpp +++ b/tests/FilesT.cpp @@ -563,3 +563,36 @@ TEST_CASE("[Files] Empty file") REQUIRE(!synth.loadSfzFile({})); REQUIRE(parser.getIncludedFiles().empty()); } + +TEST_CASE("[Files] Labels") +{ + sfz::Synth synth; + synth.loadSfzFile(fs::current_path() / "tests/TestFiles/labels.sfz"); + auto keyLabels = synth.getKeyLabels(); + auto ccLabels = synth.getCCLabels(); + REQUIRE( keyLabels.size() == 2); + REQUIRE( keyLabels[0].first == 12 ); + REQUIRE( keyLabels[0].second == "Cymbals" ); + REQUIRE( keyLabels[1].first == 65 ); + REQUIRE( keyLabels[1].second == "Crash" ); + REQUIRE( ccLabels.size() == 2); + REQUIRE( ccLabels[0].first == 54 ); + REQUIRE( ccLabels[0].second == "Gain" ); + REQUIRE( ccLabels[1].first == 2 ); + REQUIRE( ccLabels[1].second == "Other" ); + const std::string xmlMidnam = synth.exportMidnam(); + REQUIRE(xmlMidnam.find("") != xmlMidnam.npos); + REQUIRE(xmlMidnam.find("") != xmlMidnam.npos); + REQUIRE(xmlMidnam.find("") != xmlMidnam.npos); + REQUIRE(xmlMidnam.find("") != xmlMidnam.npos); +} + +TEST_CASE("[Files] Switch labels") +{ + sfz::Synth synth; + synth.loadSfzFile(fs::current_path() / "tests/TestFiles/labels_sw.sfz"); + const std::string xmlMidnam = synth.exportMidnam(); + REQUIRE(xmlMidnam.find("") != xmlMidnam.npos); + REQUIRE(xmlMidnam.find("") != xmlMidnam.npos); + REQUIRE(xmlMidnam.find("") != xmlMidnam.npos); +} diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index 7cc2864d..14176337 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -295,6 +295,15 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.keyswitchRange == sfz::Range(0, 0)); } + SECTION("sw_label") + { + REQUIRE(!region.keyswitchLabel); + region.parseOpcode({ "sw_label", "note" }); + REQUIRE(region.keyswitchLabel == "note"); + region.parseOpcode({ "sw_label", "ring" }); + REQUIRE(region.keyswitchLabel == "ring"); + } + SECTION("sw_last") { REQUIRE(!region.keyswitch); diff --git a/tests/TestFiles/labels.sfz b/tests/TestFiles/labels.sfz new file mode 100644 index 00000000..ceb9f17b --- /dev/null +++ b/tests/TestFiles/labels.sfz @@ -0,0 +1,7 @@ + +label_cc54=Gain +label_cc2=Other +label_key12=Cymbals +label_key65=Crash +label_key128=Ignored + sample=*sine diff --git a/tests/TestFiles/labels_sw.sfz b/tests/TestFiles/labels_sw.sfz new file mode 100644 index 00000000..5c1721f9 --- /dev/null +++ b/tests/TestFiles/labels_sw.sfz @@ -0,0 +1,4 @@ + sw_lokey=36 sw_hikey=40 sw_default=36 + sw_last=36 sw_label=Sine lokey=41 sample=*sine + sw_last=38 sw_label=Triangle lokey=41 sample=*triangle + sw_last=40 sw_label=Saw lokey=41 sample=*saw