diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index c746f273a..d44d6d8ff 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -1195,14 +1195,26 @@ std::string sfz::Synth::exportMidnam(absl::string_view model) const } { + auto anonymousCCs = getUsedCCs(); + pugi::xml_node cns = device.append_child("ControlNameList"); cns.append_attribute("Name").set_value("Controls"); for (const auto& pair : ccLabels) { + anonymousCCs.set(pair.first, false); 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()); cn.append_attribute("Name").set_value(pair.second.c_str()); } + + for (unsigned i = 0; i < anonymousCCs.size(); ++i) { + if (anonymousCCs[i]) { + pugi::xml_node cn = cns.append_child("Control"); + cn.append_attribute("Type").set_value("7bit"); + cn.append_attribute("Number").set_value(std::to_string(i).c_str()); + cn.append_attribute("Name").set_value(("Unnamed CC " + std::to_string(i)).c_str()); + } + } } { @@ -1581,3 +1593,64 @@ void sfz::Synth::setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexce polyphonyGroups[groupIdx].setPolyphonyLimit(polyphony); } + +std::bitset sfz::Synth::getUsedCCs() const noexcept +{ + std::bitset used; + for (const RegionPtr& region : regions) + updateUsedCCsFromRegion(used, *region); + updateUsedCCsFromModulations(used, resources.modMatrix); + return used; +} + +void sfz::Synth::updateUsedCCsFromRegion(std::bitset& usedCCs, const Region& region) +{ + updateUsedCCsFromCCMap(usedCCs, region.offsetCC); + updateUsedCCsFromCCMap(usedCCs, region.amplitudeEG.ccAttack); + updateUsedCCsFromCCMap(usedCCs, region.amplitudeEG.ccRelease); + updateUsedCCsFromCCMap(usedCCs, region.amplitudeEG.ccDecay); + updateUsedCCsFromCCMap(usedCCs, region.amplitudeEG.ccDelay); + updateUsedCCsFromCCMap(usedCCs, region.amplitudeEG.ccHold); + updateUsedCCsFromCCMap(usedCCs, region.amplitudeEG.ccStart); + updateUsedCCsFromCCMap(usedCCs, region.amplitudeEG.ccSustain); + updateUsedCCsFromCCMap(usedCCs, region.pitchEG.ccAttack); + updateUsedCCsFromCCMap(usedCCs, region.pitchEG.ccRelease); + updateUsedCCsFromCCMap(usedCCs, region.pitchEG.ccDecay); + updateUsedCCsFromCCMap(usedCCs, region.pitchEG.ccDelay); + updateUsedCCsFromCCMap(usedCCs, region.pitchEG.ccHold); + updateUsedCCsFromCCMap(usedCCs, region.pitchEG.ccStart); + updateUsedCCsFromCCMap(usedCCs, region.pitchEG.ccSustain); + updateUsedCCsFromCCMap(usedCCs, region.filterEG.ccAttack); + updateUsedCCsFromCCMap(usedCCs, region.filterEG.ccRelease); + updateUsedCCsFromCCMap(usedCCs, region.filterEG.ccDecay); + updateUsedCCsFromCCMap(usedCCs, region.filterEG.ccDelay); + updateUsedCCsFromCCMap(usedCCs, region.filterEG.ccHold); + updateUsedCCsFromCCMap(usedCCs, region.filterEG.ccStart); + updateUsedCCsFromCCMap(usedCCs, region.filterEG.ccSustain); + updateUsedCCsFromCCMap(usedCCs, region.ccConditions); + updateUsedCCsFromCCMap(usedCCs, region.ccTriggers); + updateUsedCCsFromCCMap(usedCCs, region.crossfadeCCInRange); + updateUsedCCsFromCCMap(usedCCs, region.crossfadeCCOutRange); +} + +void sfz::Synth::updateUsedCCsFromModulations(std::bitset& usedCCs, const ModMatrix& mm) +{ + class CCSourceCollector : public ModMatrix::KeyVisitor { + public: + explicit CCSourceCollector(std::bitset& used) + : used_(used) + { + } + + bool visit(const ModKey& key) override + { + if (key.id() == ModId::Controller) + used_.set(key.parameters().cc); + return true; + } + std::bitset& used_; + }; + + CCSourceCollector vtor(usedCCs); + mm.visitSources(vtor); +} diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 8133b676e..3b7ce3b8f 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -575,6 +575,13 @@ class Synth final : public Voice::StateListener, public Parser::Listener { */ const std::vector& getCCLabels() const noexcept { return ccLabels; } + /** + * @brief Get the used CCs + * + * @return const std::bitset& + */ + std::bitset getUsedCCs() const noexcept; + protected: /** * @brief The voice callback which is called during a change of state. @@ -697,6 +704,15 @@ class Synth final : public Voice::StateListener, public Parser::Listener { void noteOnDispatch(int delay, int noteNumber, float velocity) noexcept; void noteOffDispatch(int delay, int noteNumber, float velocity) noexcept; + template + static void updateUsedCCsFromCCMap(std::bitset& usedCCs, const CCMap map) + { + for (auto& mod : map) + usedCCs[mod.cc] = true; + } + static void updateUsedCCsFromRegion(std::bitset& usedCCs, const Region& region); + static void updateUsedCCsFromModulations(std::bitset& usedCCs, const ModMatrix& mm); + // Opcode memory; these are used to build regions, as a new region // will integrate opcodes from the group, master and global block std::vector globalOpcodes; diff --git a/src/sfizz/modulations/ModMatrix.cpp b/src/sfizz/modulations/ModMatrix.cpp index 41b1c8441..694a9cc0e 100644 --- a/src/sfizz/modulations/ModMatrix.cpp +++ b/src/sfizz/modulations/ModMatrix.cpp @@ -428,4 +428,28 @@ std::string ModMatrix::toDotGraph() const return dot; } +bool ModMatrix::visitSources(KeyVisitor& vtor) const +{ + const Impl& impl = *impl_; + + for (const Impl::Source& item : impl.sources_) { + if (!vtor.visit(item.key)) + return false; + } + + return true; +} + +bool ModMatrix::visitTargets(KeyVisitor& vtor) const +{ + const Impl& impl = *impl_; + + for (const Impl::Target& item : impl.targets_) { + if (!vtor.visit(item.key)) + return false; + } + + return true; +} + } // namespace sfz diff --git a/src/sfizz/modulations/ModMatrix.h b/src/sfizz/modulations/ModMatrix.h index fa1f26332..0a451becc 100644 --- a/src/sfizz/modulations/ModMatrix.h +++ b/src/sfizz/modulations/ModMatrix.h @@ -173,6 +173,34 @@ class ModMatrix { */ std::string toDotGraph() const; + class KeyVisitor { + public: + virtual ~KeyVisitor() {} + /** + * @brief Visit a key of the modulation matrix. + * + * @param key + * @return true to continue visiting, false to stop + */ + virtual bool visit(const ModKey& key) = 0; + }; + + /** + * @brief Visit the keys of all the sources in the matrix. + * + * @param vtor a visitor object + * @return last return code from the visitor + */ + bool visitSources(KeyVisitor& vtor) const; + + /** + * @brief Visit the keys of all the sources in the matrix. + * + * @param vtor a visitor object + * @return last return code from the visitor + */ + bool visitTargets(KeyVisitor& vtor) const; + private: struct Impl; std::unique_ptr impl_; diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index a6ec1aa29..2d39c7542 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -981,3 +981,91 @@ TEST_CASE("[Synth] sw_default works at a group level") synth.noteOn(0, 62, 85); REQUIRE( synth.getNumActiveVoices(true) == 1 ); } + +TEST_CASE("[Synth] Used CCs") +{ + sfz::Synth synth; + REQUIRE( !synth.getUsedCCs().any() ); + synth.loadSfzString(fs::current_path(), R"( + amplitude_cc1=100 + volume_oncc2=5 + locc4=64 hicc67=32 pan_cc5=200 sample=*sine + width_cc98=200 sample=*sine + position_cc42=200 pitch_oncc56=200 sample=*sine + start_locc44=200 hikey=-1 sample=*sine + )"); + auto usedCCs = synth.getUsedCCs(); + REQUIRE( usedCCs[1] ); + REQUIRE( usedCCs[2] ); + REQUIRE( !usedCCs[3] ); + REQUIRE( usedCCs[4] ); + REQUIRE( usedCCs[5] ); + REQUIRE( !usedCCs[6] ); + REQUIRE( usedCCs[42] ); + REQUIRE( usedCCs[44] ); + REQUIRE( usedCCs[56] ); + REQUIRE( usedCCs[67] ); + REQUIRE( usedCCs[98] ); + REQUIRE( !usedCCs[127] ); +} + +TEST_CASE("[Synth] Used CCs EGs") +{ + sfz::Synth synth; + REQUIRE( !synth.getUsedCCs().any() ); + synth.loadSfzString(fs::current_path(), R"( + + ampeg_attack_oncc1=1 + ampeg_sustain_oncc2=2 + ampeg_start_oncc3=3 + ampeg_hold_oncc4=4 + ampeg_decay_oncc5=5 + ampeg_delay_oncc6=6 + ampeg_release_oncc7=7 + sample=*sine + + pitcheg_attack_oncc11=11 + pitcheg_sustain_oncc12=12 + pitcheg_start_oncc13=13 + pitcheg_hold_oncc14=14 + pitcheg_decay_oncc15=15 + pitcheg_delay_oncc16=16 + pitcheg_release_oncc17=17 + sample=*sine + + fileg_attack_oncc21=21 + fileg_sustain_oncc22=22 + fileg_start_oncc23=23 + fileg_hold_oncc24=24 + fileg_decay_oncc25=25 + fileg_delay_oncc26=26 + fileg_release_oncc27=27 + sample=*sine + )"); + auto usedCCs = synth.getUsedCCs(); + REQUIRE( usedCCs[1] ); + REQUIRE( usedCCs[2] ); + REQUIRE( usedCCs[3] ); + REQUIRE( usedCCs[4] ); + REQUIRE( usedCCs[5] ); + REQUIRE( usedCCs[6] ); + REQUIRE( usedCCs[7] ); + // FIXME: enable when supported + // REQUIRE( !usedCCs[8] ); + // REQUIRE( usedCCs[11] ); + // REQUIRE( usedCCs[12] ); + // REQUIRE( usedCCs[13] ); + // REQUIRE( usedCCs[14] ); + // REQUIRE( usedCCs[15] ); + // REQUIRE( usedCCs[16] ); + // REQUIRE( usedCCs[17] ); + // REQUIRE( !usedCCs[18] ); + // REQUIRE( usedCCs[21] ); + // REQUIRE( usedCCs[22] ); + // REQUIRE( usedCCs[23] ); + // REQUIRE( usedCCs[24] ); + // REQUIRE( usedCCs[25] ); + // REQUIRE( usedCCs[26] ); + // REQUIRE( usedCCs[27] ); + // REQUIRE( !usedCCs[28] ); +}