From 751e4f96bb2d21c8a7ed07f26b258fa1380ecca7 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 14 Dec 2020 21:14:35 +0100 Subject: [PATCH] Add the reception of CC messages for editor --- editor/src/editor/Editor.cpp | 125 ++++++++++++++++++++++++++++++++++- src/sfizz/Synth.cpp | 20 +++--- src/sfizz/Synth.h | 3 +- src/sfizz/SynthMessaging.cpp | 9 +-- src/sfizz/SynthPrivate.h | 13 ++-- src/sfizz/utility/BitArray.h | 81 +++++++++++++++++++++++ tests/SynthT.cpp | 73 ++++++++++---------- 7 files changed, 263 insertions(+), 61 deletions(-) create mode 100644 src/sfizz/utility/BitArray.h diff --git a/editor/src/editor/Editor.cpp b/editor/src/editor/Editor.cpp index 525689bf..72c99ed7 100644 --- a/editor/src/editor/Editor.cpp +++ b/editor/src/editor/Editor.cpp @@ -13,14 +13,17 @@ #include #include #include +#include #include #include +#include #include #include #include #include #include #include +#include #include "utility/vstgui_before.h" #include "vstgui/vstgui.h" @@ -106,6 +109,13 @@ struct Editor::Impl : EditorController::Receiver, IControlListener { void uiReceiveValue(EditId id, const EditValue& v) override; void uiReceiveMessage(const char* path, const char* sig, const sfizz_arg_t* args) override; + // queued OSC API; sends OSC with intermediate delay between messages + // to prevent message bursts overloading the buffer + void sendQueuedOSC(const char* path, const char* sig, const sfizz_arg_t* args); + void tickOSCQueue(CVSTGUITimer* timer); + std::queue oscSendQueue_; + SharedPointer oscSendQueueTimer_; + void createFrameContents(); template @@ -142,6 +152,10 @@ struct Editor::Impl : EditorController::Receiver, IControlListener { void updateTuningFrequencyLabel(float tuningFrequency); void updateStretchedTuningLabel(float stretchedTuning); + void updateCCUsed(unsigned cc, bool used); + void updateCCValue(unsigned cc, float value); + void updateCCLabel(unsigned cc, const char* label); + void setActivePanel(unsigned panelId); static void formatLabel(CTextLabel* label, const char* fmt, ...); @@ -164,6 +178,11 @@ Editor::Editor(EditorController& ctrl) ctrl.decorate(&impl); impl.createFrameContents(); + + uint32_t oscSendInterval = 1; // milliseconds + impl.oscSendQueueTimer_ = makeOwned( + [this](CVSTGUITimer* timer) { impl_->tickOSCQueue(timer); }, + oscSendInterval, false); } Editor::~Editor() @@ -182,6 +201,9 @@ void Editor::open(CFrame& frame) impl.frame_ = &frame; frame.addView(impl.mainView_.get()); + + // request the whole CC information + impl.sendQueuedOSC("/cc/slots", "", nullptr); } void Editor::close() @@ -336,9 +358,92 @@ void Editor::Impl::uiReceiveValue(EditId id, const EditValue& v) } } +/// +static constexpr unsigned kMessageMaxIndices = 8; + +static bool matchMessage(const char* pattern, const char* path, unsigned* indices) +{ + unsigned nthIndex = 0; + + while (const char *endp = strchr(pattern, '&')) { + if (nthIndex == kMessageMaxIndices) + return false; + + size_t length = endp - pattern; + if (strncmp(pattern, path, length)) + return false; + pattern += length; + path += length; + + length = 0; + while (absl::ascii_isdigit(path[length])) + ++length; + + if (!absl::SimpleAtoi(absl::string_view(path, length), &indices[nthIndex++])) + return false; + + pattern += 1; + path += length; + } + + return !strcmp(path, pattern); +} + +/// void Editor::Impl::uiReceiveMessage(const char* path, const char* sig, const sfizz_arg_t* args) { - // TODO handle the message... + unsigned indices[kMessageMaxIndices]; + + if (!strcmp(path, "/cc/slots") && !strcmp(sig, "b")) { + const uint8_t* bitChunks = args[0].b->data; + uint32_t byteSize = args[0].b->size; + + for (unsigned cc = 0; cc < 8 * byteSize; ++cc) { + bool used = bitChunks[cc / 8] & (1u << (cc % 8)); + updateCCUsed(cc, used); + if (used) { + char pathBuf[256]; + sprintf(pathBuf, "/cc%u/value", cc); + sendQueuedOSC(pathBuf, "", nullptr); + sprintf(pathBuf, "/cc%u/label", cc); + sendQueuedOSC(pathBuf, "", nullptr); + } + } + } + else if (matchMessage("/cc&/value", path, indices) && !strcmp(sig, "f")) { + updateCCValue(indices[0], args[0].f); + } + else if (matchMessage("/cc&/label", path, indices) && !strcmp(sig, "s")) { + updateCCLabel(indices[0], args[0].s); + } + else { + //fprintf(stderr, "Receive unhandled OSC: %s\n", path); + } +} + +void Editor::Impl::sendQueuedOSC(const char* path, const char* sig, const sfizz_arg_t* args) +{ + uint32_t oscSize = sfizz_prepare_message(nullptr, 0, path, sig, args); + std::string oscData(oscSize, '\0'); + sfizz_prepare_message(&oscData[0], oscSize, path, sig, args); + oscSendQueue_.push(std::move(oscData)); + oscSendQueueTimer_->start(); +} + +void Editor::Impl::tickOSCQueue(CVSTGUITimer* timer) +{ + if (oscSendQueue_.empty()) { + timer->stop(); + return; + } + const std::string& msg = oscSendQueue_.front(); + const char* path; + const char* sig; + const sfizz_arg_t* args; + uint8_t buffer[1024]; + if (sfizz_extract_message(msg.data(), msg.size(), buffer, sizeof(buffer), &path, &sig, &args) > 0) + ctrl_->uiSendMessage(path, sig, args); + oscSendQueue_.pop(); } void Editor::Impl::createFrameContents() @@ -1062,6 +1167,24 @@ void Editor::Impl::updateStretchedTuningLabel(float stretchedTuning) label->setText(text); } +void Editor::Impl::updateCCUsed(unsigned cc, bool used) +{ + // TODO + fprintf(stderr, "CC%u used: %d\n", cc, used); +} + +void Editor::Impl::updateCCValue(unsigned cc, float value) +{ + // TODO + fprintf(stderr, "CC%u value: %f\n", cc, value); +} + +void Editor::Impl::updateCCLabel(unsigned cc, const char* label) +{ + // TODO + fprintf(stderr, "CC%u label: %s\n", cc, label); +} + void Editor::Impl::setActivePanel(unsigned panelId) { panelId = std::max(0, std::min(kNumPanels - 1, static_cast(panelId))); diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index e50df231..6e736690 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -696,7 +696,7 @@ void Synth::Impl::finalizeSfzLoad() regions_.resize(currentRegionCount); // collect all CCs used in regions, with matrix not yet connected - std::bitset usedCCs; + BitArray usedCCs; for (const RegionPtr& regionPtr : regions_) { const Region& region = *regionPtr; collectUsedCCsFromRegion(usedCCs, region); @@ -1328,8 +1328,8 @@ std::string Synth::exportMidnam(absl::string_view model) const } } - for (unsigned i = 0, n = std::min(128, anonymousCCs.size()); i < n; ++i) { - if (anonymousCCs[i]) { + for (unsigned i = 0, n = std::min(128, anonymousCCs.bit_size()); i < n; ++i) { + if (anonymousCCs.test(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()); @@ -1716,7 +1716,7 @@ void Synth::allSoundOff() noexcept effectBus->clear(); } -const std::bitset& Synth::getUsedCCs() const noexcept +const BitArray& Synth::getUsedCCs() const noexcept { Impl& impl = *impl_; return impl.currentUsedCCs_; @@ -1729,7 +1729,7 @@ void sfz::Synth::setBroadcastCallback(sfizz_receive_t* broadcast, void* data) impl.broadcastData = data; } -void Synth::Impl::collectUsedCCsFromRegion(std::bitset& usedCCs, const Region& region) +void Synth::Impl::collectUsedCCsFromRegion(BitArray& usedCCs, const Region& region) { collectUsedCCsFromCCMap(usedCCs, region.offsetCC); collectUsedCCsFromCCMap(usedCCs, region.amplitudeEG.ccAttack); @@ -1763,11 +1763,11 @@ void Synth::Impl::collectUsedCCsFromRegion(std::bitset& usedCCs, collectUsedCCsFromCCMap(usedCCs, region.crossfadeCCOutRange); } -void Synth::Impl::collectUsedCCsFromModulations(std::bitset& usedCCs, const ModMatrix& mm) +void Synth::Impl::collectUsedCCsFromModulations(BitArray& usedCCs, const ModMatrix& mm) { class CCSourceCollector : public ModMatrix::KeyVisitor { public: - explicit CCSourceCollector(std::bitset& used) + explicit CCSourceCollector(BitArray& used) : used_(used) { } @@ -1778,16 +1778,16 @@ void Synth::Impl::collectUsedCCsFromModulations(std::bitset& use used_.set(key.parameters().cc); return true; } - std::bitset& used_; + BitArray& used_; }; CCSourceCollector vtor(usedCCs); mm.visitSources(vtor); } -std::bitset Synth::Impl::collectAllUsedCCs() +BitArray Synth::Impl::collectAllUsedCCs() { - std::bitset used; + BitArray used; for (const Impl::RegionPtr& region : regions_) collectUsedCCsFromRegion(used, *region); collectUsedCCsFromModulations(used, resources_.modMatrix); diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 9afb3104..168900f9 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -17,6 +17,7 @@ #include #include #include +template class BitArray; namespace sfz { @@ -599,7 +600,7 @@ public: * * @return const std::bitset& */ - const std::bitset& getUsedCCs() const noexcept; + const BitArray& getUsedCCs() const noexcept; /** * @brief Dispatch the incoming message to the synth engine diff --git a/src/sfizz/SynthMessaging.cpp b/src/sfizz/SynthMessaging.cpp index 68dbf362..9902b58b 100644 --- a/src/sfizz/SynthMessaging.cpp +++ b/src/sfizz/SynthMessaging.cpp @@ -39,13 +39,8 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co //---------------------------------------------------------------------- MATCH("/cc/slots", "") { - uint8_t data[(config::numCCs + 7) / 8] = {}; - - const std::bitset& ccs = impl.currentUsedCCs_; - for (unsigned i = 0; i < config::numCCs; ++i) - data[i / 8] |= ccs.test(i) << (i % 8); - - sfizz_blob_t blob { data, sizeof(data) }; + const BitArray& ccs = impl.currentUsedCCs_; + sfizz_blob_t blob { ccs.data(), static_cast(ccs.byte_size()) }; client.receive<'b'>(delay, path, &blob); } break; diff --git a/src/sfizz/SynthPrivate.h b/src/sfizz/SynthPrivate.h index 43c5b5a8..893e436e 100644 --- a/src/sfizz/SynthPrivate.h +++ b/src/sfizz/SynthPrivate.h @@ -9,6 +9,7 @@ #include "modulations/sources/Controller.h" #include "modulations/sources/FlexEnvelope.h" #include "modulations/sources/LFO.h" +#include "utility/BitArray.h" namespace sfz { @@ -168,16 +169,16 @@ struct Synth::Impl final: public Parser::Listener { void finalizeSfzLoad(); template - static void collectUsedCCsFromCCMap(std::bitset& usedCCs, const CCMap map) noexcept + static void collectUsedCCsFromCCMap(BitArray& usedCCs, const CCMap map) noexcept { for (auto& mod : map) - usedCCs[mod.cc] = true; + usedCCs.set(mod.cc); } - static void collectUsedCCsFromRegion(std::bitset& usedCCs, const Region& region); - static void collectUsedCCsFromModulations(std::bitset& usedCCs, const ModMatrix& mm); + static void collectUsedCCsFromRegion(BitArray& usedCCs, const Region& region); + static void collectUsedCCsFromModulations(BitArray& usedCCs, const ModMatrix& mm); - std::bitset collectAllUsedCCs(); + BitArray collectAllUsedCCs(); const std::string* getCCLabel(int ccNumber); void setCCLabel(int ccNumber, std::string name); @@ -283,7 +284,7 @@ struct Synth::Impl final: public Parser::Listener { absl::optional modificationTime_ { }; std::array defaultCCValues_; - std::bitset currentUsedCCs_; + BitArray currentUsedCCs_; // Messaging sfizz_receive_t* broadcastReceiver = nullptr; diff --git a/src/sfizz/utility/BitArray.h b/src/sfizz/utility/BitArray.h new file mode 100644 index 00000000..020a7433 --- /dev/null +++ b/src/sfizz/utility/BitArray.h @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include +#include +#include + +/// +class ConstBitSpan { +public: + ConstBitSpan() noexcept = default; + ConstBitSpan(const uint8_t* data, size_t bits) noexcept : data_(data), bits_(bits) {} + const uint8_t* data() const noexcept { return data_; } + size_t bit_size() const noexcept { return bits_; } + size_t byte_size() const noexcept { return (bits_ + 7) / 8; } + bool test(size_t i) const noexcept { return data_[i / 8] & (1u << (i % 8)); } + bool all() const noexcept + { + size_t n = bits_; + for (size_t i = 0; i < n / 8; ++i) { + if (data_[i] != 0xff) + return false; + } + return n % 8 == 0 || data_[n / 8] == (1u << (n % 8)) - 1u; + } + bool any() const noexcept + { + size_t n = bits_; + for (size_t i = 0; i < n / 8; ++i) { + if (data_[i] != 0x00) + return true; + } + return n % 8 != 0 && (data_[n / 8] & ((1u << (n % 8)) - 1u)) != 0; + } + bool none() const noexcept { return !any(); } + +private: + const uint8_t* data_ = nullptr; + size_t bits_ = 0; +}; + +/// +class BitSpan : public ConstBitSpan { +public: + BitSpan() noexcept = default; + BitSpan(const uint8_t* data, size_t bits) noexcept : ConstBitSpan(data, bits) {} + uint8_t* data() const noexcept { return const_cast(ConstBitSpan::data()); } + void clear() { memset(data(), 0, byte_size()); } + void set(size_t i) noexcept { data()[i / 8] |= 1u << (i % 8); } + void set(size_t i, bool b) noexcept { if (b) set(i); else reset(i); } + void reset(size_t i) noexcept { data()[i / 8] &= ~(1u << (i % 8)); } + void flip(size_t i) noexcept { data()[i / 8] ^= 1u << (i % 8); } +}; + +/// +template +class BitArray { +public: + BitArray() noexcept = default; + uint8_t* data() noexcept { return data_; } + const uint8_t* data() const noexcept { return data_; } + static constexpr size_t bit_size() noexcept { return N; } + static constexpr size_t byte_size() noexcept { return (N + 7) / 8; } + BitSpan span() noexcept { return BitSpan(data_, N); }; + ConstBitSpan span() const noexcept { return ConstBitSpan(data_, N); }; + bool test(size_t i) const noexcept { return span().test(i); } + void set(size_t i) noexcept { span().set(i); } + void set(size_t i, bool b) noexcept { span().set(i, b); } + void reset(size_t i) noexcept { span().reset(i); } + void flip(size_t i) noexcept { span().flip(i); } + bool all() const noexcept { return span().all(); } + bool any() const noexcept { return span().any(); } + bool none() const noexcept { return span().none(); } + +private: + uint8_t data_[byte_size()] {}; +}; diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 5565d5ff..b9bf7ab8 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -8,6 +8,7 @@ #include "sfizz/SisterVoiceRing.h" #include "sfizz/SfzHelpers.h" #include "sfizz/utility/NumericId.h" +#include "sfizz/utility/BitArray.h" #include "TestHelpers.h" #include #include "catch2/catch.hpp" @@ -1087,18 +1088,18 @@ TEST_CASE("[Synth] Used CCs") 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] ); + REQUIRE( usedCCs.test(1) ); + REQUIRE( usedCCs.test(2) ); + REQUIRE( !usedCCs.test(3) ); + REQUIRE( usedCCs.test(4) ); + REQUIRE( usedCCs.test(5) ); + REQUIRE( !usedCCs.test(6) ); + REQUIRE( usedCCs.test(42) ); + REQUIRE( usedCCs.test(44) ); + REQUIRE( usedCCs.test(56) ); + REQUIRE( usedCCs.test(67) ); + REQUIRE( usedCCs.test(98) ); + REQUIRE( !usedCCs.test(127) ); } TEST_CASE("[Synth] Used CCs EGs") @@ -1135,31 +1136,31 @@ TEST_CASE("[Synth] Used CCs EGs") 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] ); + REQUIRE( usedCCs.test(1) ); + REQUIRE( usedCCs.test(2) ); + REQUIRE( usedCCs.test(3) ); + REQUIRE( usedCCs.test(4) ); + REQUIRE( usedCCs.test(5) ); + REQUIRE( usedCCs.test(6) ); + REQUIRE( usedCCs.test(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] ); + // REQUIRE( !usedCCs.test(8) ); + // REQUIRE( usedCCs.test(11) ); + // REQUIRE( usedCCs.test(12) ); + // REQUIRE( usedCCs.test(13) ); + // REQUIRE( usedCCs.test(14) ); + // REQUIRE( usedCCs.test(15) ); + // REQUIRE( usedCCs.test(16) ); + // REQUIRE( usedCCs.test(17) ); + // REQUIRE( !usedCCs.test(18) ); + // REQUIRE( usedCCs.test(21) ); + // REQUIRE( usedCCs.test(22) ); + // REQUIRE( usedCCs.test(23) ); + // REQUIRE( usedCCs.test(24) ); + // REQUIRE( usedCCs.test(25) ); + // REQUIRE( usedCCs.test(26) ); + // REQUIRE( usedCCs.test(27) ); + // REQUIRE( !usedCCs.test(28) ); } TEST_CASE("[Synth] Activate also on the sustain CC")