From 66ad2d861a08364575e5f40c45a4a8ee74503c47 Mon Sep 17 00:00:00 2001 From: reuk Date: Fri, 25 Aug 2023 13:31:30 +0100 Subject: [PATCH] MIDI-CI: Add demo --- examples/Audio/CapabilityInquiryDemo.h | 5024 +++++++++++++++++ .../DemoRunner.xcodeproj/project.pbxproj | 6 +- .../iOS/DemoRunner.xcodeproj/project.pbxproj | 6 +- .../AudioPluginHost.xcodeproj/project.pbxproj | 6 +- .../AudioPluginHost.xcodeproj/project.pbxproj | 6 +- .../UnitTestRunner.xcodeproj/project.pbxproj | 6 +- 6 files changed, 5034 insertions(+), 20 deletions(-) create mode 100644 examples/Audio/CapabilityInquiryDemo.h diff --git a/examples/Audio/CapabilityInquiryDemo.h b/examples/Audio/CapabilityInquiryDemo.h new file mode 100644 index 0000000000..9aeefb79a7 --- /dev/null +++ b/examples/Audio/CapabilityInquiryDemo.h @@ -0,0 +1,5024 @@ +/* + ============================================================================== + + This file is part of the JUCE examples. + Copyright (c) 2022 - Raw Material Software Limited + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, + WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR + PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +/******************************************************************************* + The block below describes the properties of this PIP. A PIP is a short snippet + of code that can be read by the Projucer and used to generate a JUCE project. + + BEGIN_JUCE_PIP_METADATA + + name: CapabilityInquiryDemo + version: 1.0.0 + vendor: JUCE + website: http://juce.com + description: Performs MIDI Capability Inquiry transactions + + dependencies: juce_audio_basics, juce_audio_devices, juce_core, + juce_data_structures, juce_events, juce_graphics, + juce_gui_basics, juce_midi_ci + exporters: xcode_mac, vs2022, linux_make, androidstudio, xcode_iphone + + moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1 + + type: Component + mainClass: CapabilityInquiryDemo + + useLocalCopy: 1 + + END_JUCE_PIP_METADATA + +*******************************************************************************/ + +#pragma once + +/** This allows listening for changes to some data. Unlike ValueTree, it remembers the types + of all the data that's stored, which makes it a bit nicer to use. Also unlike ValueTree, + every mutation necessitates a deep copy of the model, so this isn't necessarily suitable + for large models that change frequently. + + Use operator-> or operator* to find the current state (without copying!). + + Use operator= to assign new state. + After assigning new state, the old and new states will be compared, and + observers will be notified if the new state is different to the old state. + + Use operator[] to retrieve nested states. + This is useful when a component only depends on a small part of a larger + model. Assigning a new value to the nested state will also cause observers + of the parent state to be notified. Similarly, assigning a new state to + the parent state will cause observers of the nested state to be notified, + if the nested state actually changes in value. +*/ +template +class State +{ +public: + State() : State (Value{}) {} + + State (Value v, std::function n) + : impl (std::make_unique (std::move (v), std::move (n))) {} + + explicit State (Value v) + : State (std::move (v), [] (const Value&, Value newer) { return std::move (newer); }) {} + + template + State (State p, Value Parent::* member) + : impl (std::make_unique> (std::move (p), member)) {} + + template + State (State p, Ind index) + : impl (std::make_unique> (std::move (p), std::move (index))) {} + + State& operator= (const Value& v) + { + impl->set (v); + return *this; + } + + State& operator= (Value&& v) + { + impl->set (std::move (v)); + return *this; + } + + const Value& operator* () const { return impl->get(); } + const Value* operator->() const { return &impl->get(); } + + ErasedScopeGuard observe (std::function onChange) + { + // The idea is that observers want to know whenever the state changes, in order to repaint. + // They'll also want to repaint upon first observing, so that painting is up-to-date. + onChange (impl->get()); + + // In the case that observe is called on a temporary, caching impl means that the + // ErasedScopeGuard can still detach safely + return impl->observe ([cached = *this, fn = std::move (onChange)] (const auto& x) + { + fn (x); + }); + } + + template + auto operator[] (T member) const + -> State().*std::declval())>> + { + return { *this, std::move (member) }; + } + + template + auto operator[] (T index) const + -> State()[std::declval()])>> + { + return { *this, std::move (index) }; + } + +private: + class Provider : public std::enable_shared_from_this + { + public: + virtual ~Provider() = default; + virtual void set (const Value&) = 0; + virtual void set (Value&&) = 0; + virtual const Value& get() const = 0; + + ErasedScopeGuard observe (std::function fn) + { + return ErasedScopeGuard { [t = this->shared_from_this(), it = list.insert (list.end(), std::move (fn))] + { + t->list.erase (it); + } }; + } + + void notify (const Value& v) const + { + for (auto& callback : list) + callback (v); + } + + private: + using List = std::list>; + List list; + }; + + class Root final : public Provider + { + public: + explicit Root (Value v, std::function n) + : value (std::move (v)), normalise (std::move (n)) {} + + void set (const Value& m) override { setImpl (m); } + void set (Value&& m) override { setImpl (std::move (m)); } + const Value& get() const override { return value; } + + private: + template + void setImpl (T&& t) + { + // If this fails, you're updating the model from inside an observer callback. + // That's not very unidirectional-data-flow of you! + jassert (! reentrant); + const ScopedValueSetter scope { reentrant, true }; + + auto normalised = normalise (value, std::forward (t)); + + if (normalised != value) + this->notify (std::exchange (value, std::move (normalised))); + } + + Value value; + std::function normalise; + bool reentrant = false; + }; + + template + class Member final : public Provider + { + public: + Member (State p, Value Parent::* m) : parent (std::move (p)), member (m) {} + + void set (const Value& m) override { setImpl (m); } + void set (Value&& m) override { setImpl (std::move (m)); } + const Value& get() const override { return (*parent).*member; } + + private: + template + void setImpl (T&& t) + { + auto updated = *parent; + updated.*member = std::forward (t); + parent = std::move (updated); + } + + State parent; + Value Parent::*member; + ErasedScopeGuard listener = parent.observe ([this] (const auto& old) + { + if (old.*member != get()) + this->notify (old.*member); + }); + }; + + template + class Index final : public Provider + { + public: + Index (State p, Ind i) : parent (std::move (p)), index (i) {} + + void set (const Value& m) override { setImpl (m); } + void set (Value&& m) override { setImpl (std::move (m)); } + const Value& get() const override { return (*parent)[index]; } + + private: + template + void setImpl (T&& t) + { + auto updated = *parent; + updated[index] = std::forward (t); + parent = std::move (updated); + } + + State parent; + Ind index; + ErasedScopeGuard listener = parent.observe ([this] (const auto& old) + { + if (isPositiveAndBelow (index, std::size (*parent)) + && isPositiveAndBelow (index, std::size (old)) + && old[index] != get()) + { + this->notify (old[index]); + } + }); + }; + + std::shared_ptr impl; +}; + +/** + Data types used to represent the state of the application. + These should all be types with value semantics, so there should not be pointers or references + between different parts of the model. +*/ +struct Model +{ + Model() = delete; + + template + static Array toVarArray (Range&& range, Fn&& fn) + { + Array result; + result.resize ((int) std::size (range)); + std::transform (std::begin (range), + std::end (range), + result.begin(), + std::forward (fn)); + return result; + } + + template + static auto fromVarArray (var in, Fn&& fn, std::vector fallback) -> std::vector + { + auto* list = in.getArray(); + + if (list == nullptr) + return fallback; + + std::vector result; + for (auto& t : *list) + result.push_back (fn (t)); + return result; + } + + #define JUCE_TUPLE_RELATIONAL_OP(classname, op) \ + bool operator op (const classname& other) const \ + { \ + return tie() op other.tie(); \ + } + + #define JUCE_TUPLE_RELATIONAL_OPS(classname) \ + JUCE_TUPLE_RELATIONAL_OP(classname, ==) \ + JUCE_TUPLE_RELATIONAL_OP(classname, !=) + + template + struct ListWithSelection + { + std::vector items; + int selection = -1; + + static constexpr auto marshallingVersion = std::nullopt; + + template + static auto serialise (Archive& archive, This& t) + { + return archive (named ("items", t.items), + named ("selection", t.selection)); + } + + auto tie() const { return std::tie (items, selection); } + JUCE_TUPLE_RELATIONAL_OPS (ListWithSelection) + + private: + template + static auto* get (This& t, Index index) + { + if (isPositiveAndBelow (index, t.items.size())) + return &t.items[(size_t) index]; + + return static_cast (nullptr); + } + + template + static auto* getSelected (This& t) + { + return get (t, t.selection); + } + + public: + auto* getSelected() { return getSelected (*this); } + auto* getSelected() const { return getSelected (*this); } + + auto* get (int index) { return get (*this, index); } + auto* get (int index) const { return get (*this, index); } + }; + + enum class ProfileMode + { + edit, + use, + }; + + struct Profiles + { + ListWithSelection profiles; + std::map channels; + std::optional selectedChannel; + ProfileMode profileMode{}; + + std::optional getSelectedProfileAtAddress() const + { + if (selectedChannel.has_value()) + if (auto* item = profiles.getSelected()) + return ci::ProfileAtAddress { *item, *selectedChannel }; + + return {}; + } + + static constexpr auto marshallingVersion = 0; + + template + static auto serialise (Archive& archive, This& t) + { + return archive (named ("profiles", t.profiles), + named ("channels", t.channels), + named ("selectedChannel", t.selectedChannel), + named ("profileMode", t.profileMode)); + } + + auto tie() const { return std::tie (profiles, channels, selectedChannel, profileMode); } + JUCE_TUPLE_RELATIONAL_OPS (Profiles) + }; + + enum class DataViewMode + { + ascii, + hex, + }; + + #define JUCE_CAN_SET X(none) X(full) X(partial) + + enum class CanSet + { + #define X(name) name, + JUCE_CAN_SET + #undef X + }; + + struct CanSetUtils + { + CanSetUtils() = delete; + + static std::optional fromString (const char* str) + { + #define X(name) if (StringRef (str) == StringRef (#name)) return CanSet::name; + JUCE_CAN_SET + #undef X + + return std::nullopt; + } + + static const char* toString (CanSet c) + { + switch (c) + { + #define X(name) case CanSet::name: return #name; + JUCE_CAN_SET + #undef X + } + + return nullptr; + } + }; + + #undef JUCE_CAN_SET + + struct PropertyValue + { + std::vector bytes; ///< decoded bytes + String mediaType = "application/json"; + + static constexpr auto marshallingVersion = 0; + + template + static auto serialise (Archive& archive, This& t) + { + return archive (named ("bytes", t.bytes), + named ("mediaType", t.mediaType)); + } + + auto tie() const { return std::tie (bytes, mediaType); } + JUCE_TUPLE_RELATIONAL_OPS (PropertyValue) + }; + + struct ReadableDeviceInfo + { + String manufacturer, family, model, version; + }; + + struct Property + { + String name; + var schema; + std::set encodings { ci::Encoding::ascii }; + std::vector mediaTypes { "application/json" }; + var columns; + CanSet canSet = CanSet::none; + bool canGet = true, canSubscribe = false, requireResId = false, canPaginate = false; + PropertyValue value; + + [[nodiscard]] std::optional getBestCommonEncoding ( + ci::Encoding firstChoice = ci::Encoding::ascii) const + { + if (encodings.count (firstChoice) != 0) + return firstChoice; + + if (! encodings.empty()) + return *encodings.begin(); + + return {}; + } + + std::optional getReadableDeviceInfo() const + { + if (name != "DeviceInfo") + return {}; + + const auto parsed = ci::Encodings::jsonFrom7BitText (value.bytes); + auto* object = parsed.getDynamicObject(); + + if (object == nullptr) + return {}; + + if (! object->hasProperty ("manufacturer") + || ! object->hasProperty ("family") + || ! object->hasProperty ("model") + || ! object->hasProperty ("version")) + { + return {}; + } + + return ReadableDeviceInfo { object->getProperty ("manufacturer"), + object->getProperty ("family"), + object->getProperty ("model"), + object->getProperty ("version") }; + } + + static Property fromResourceListEntry (var entry) + { + Model::Property p; + p.name = entry.getProperty ("resource", ""); + p.canGet = entry.getProperty ("canGet", p.canGet); + const auto set = entry.getProperty ("canSet", {}).toString(); + p.canSet = Model::CanSetUtils::fromString (set.toRawUTF8()).value_or (p.canSet); + p.canSubscribe = entry.getProperty ("canSubscribe", p.canSubscribe); + p.requireResId = entry.getProperty ("requireResId", p.requireResId); + p.mediaTypes = fromVarArray (entry.getProperty ("mediaTypes", {}), + [] (String str) { return str; }, + p.mediaTypes); + p.schema = entry.getProperty ("schema", p.schema); + p.canPaginate = entry.getProperty ("canPaginate", p.canPaginate); + p.columns = entry.getProperty ("columns", p.columns); + + const auto stringToEncoding = [] (String str) + { + return ci::EncodingUtils::toEncoding (str.toRawUTF8()); + }; + const auto parsedEncodings = fromVarArray (entry.getProperty ("encodings", {}), + stringToEncoding, + std::vector>{}); + std::set encodingsSet; + for (const auto& parsed : parsedEncodings) + if (parsed.has_value()) + encodingsSet.insert (*parsed); + + p.encodings = encodingsSet.empty() ? p.encodings : encodingsSet; + return p; + } + + var getResourceListEntry() const + { + const Model::Property defaultInfo; + auto obj = std::make_unique(); + + obj->setProperty ("resource", name); + + if (canGet != defaultInfo.canGet) + obj->setProperty ("canGet", canGet); + + if (canSet != defaultInfo.canSet) + obj->setProperty ("canSet", Model::CanSetUtils::toString (canSet)); + + if (canSubscribe != defaultInfo.canSubscribe) + obj->setProperty ("canSubscribe", canSubscribe); + + if (requireResId != defaultInfo.requireResId) + obj->setProperty ("requireResId", requireResId); + + if (mediaTypes != defaultInfo.mediaTypes) + { + obj->setProperty ("mediaTypes", + toVarArray (mediaTypes, [] (auto str) { return str; })); + } + + if (encodings != defaultInfo.encodings) + { + obj->setProperty ("encodings", + toVarArray (encodings, ci::EncodingUtils::toString)); + } + + if (schema != defaultInfo.schema) + obj->setProperty ("schema", schema); + + if (name.endsWith ("List")) + { + if (canPaginate != defaultInfo.canPaginate) + obj->setProperty ("canPaginate", canPaginate); + + if (columns != defaultInfo.columns) + obj->setProperty ("columns", columns); + } + + return obj.release(); + } + + static constexpr auto marshallingVersion = 0; + + template + static auto serialise (Archive& archive, This& t) + { + return archive (named ("name", t.name), + named ("schema", t.schema), + named ("encodings", t.encodings), + named ("mediaTypes", t.mediaTypes), + named ("columns", t.columns), + named ("canSet", t.canSet), + named ("canGet", t.canGet), + named ("canSubscribe", t.canSubscribe), + named ("requireResId", t.requireResId), + named ("canPaginate", t.canPaginate), + named ("value", t.value)); + } + + auto tie() const + { + return std::tie (name, + schema, + encodings, + mediaTypes, + columns, + canSet, + canGet, + canSubscribe, + requireResId, + canPaginate, + value); + } + + JUCE_TUPLE_RELATIONAL_OPS (Property) + }; + + struct Properties + { + ListWithSelection properties; + DataViewMode mode{}; + + std::optional getReadableDeviceInfo() const + { + for (const auto& prop : properties.items) + if (auto info = prop.getReadableDeviceInfo()) + return info; + + return {}; + } + + static constexpr auto marshallingVersion = 0; + + template + static auto serialise (Archive& archive, This& t) + { + return archive (named ("properties", t.properties), + named ("mode", t.mode)); + } + + auto tie() const { return std::tie (properties, mode); } + JUCE_TUPLE_RELATIONAL_OPS (Properties) + }; + + struct IOSelection + { + std::optional input, output; + + static constexpr auto marshallingVersion = 0; + + template + static auto serialise (Archive& archive, This& t) + { + return archive (named ("input", t.input), + named ("output", t.output)); + } + + auto tie() const { return std::tie (input, output); } + JUCE_TUPLE_RELATIONAL_OPS (IOSelection) + }; + + struct DeviceInfo + { + ump::DeviceInfo deviceInfo; + size_t maxSysExSize { 512 }; + uint8_t numPropertyExchangeTransactions { 127 }; + bool profilesSupported { false }, propertiesSupported { false }; + + static constexpr auto marshallingVersion = 0; + + template + static auto serialise (Archive& archive, This& t) + { + return archive (named ("deviceInfo", t.deviceInfo), + named ("maxSysExSize", t.maxSysExSize), + named ("numPropertyExchangeTransactions", + t.numPropertyExchangeTransactions), + named ("profilesSupported", t.profilesSupported), + named ("propertiesSupported", t.propertiesSupported)); + } + + auto tie() const + { + return std::tie (deviceInfo, + maxSysExSize, + numPropertyExchangeTransactions, + profilesSupported, + propertiesSupported); + } + JUCE_TUPLE_RELATIONAL_OPS (DeviceInfo) + }; + + enum class MessageKind + { + incoming, + outgoing, + }; + + struct LogView + { + std::optional filter; + DataViewMode mode = DataViewMode::ascii; + + static constexpr auto marshallingVersion = 0; + + template + static auto serialise (Archive& archive, This& t) + { + return archive (named ("filter", t.filter), + named ("mode", t.mode)); + } + + auto tie() const { return std::tie (filter, mode); } + JUCE_TUPLE_RELATIONAL_OPS (LogView) + }; + + /** + The bits of the app state that we want to save and restore + */ + struct Saved + { + DeviceInfo fundamentals; + IOSelection ioSelection; + Profiles profiles; + Properties properties; + LogView logView; + + static constexpr auto marshallingVersion = 0; + + template + static auto serialise (Archive& archive, This& t) + { + return archive (named ("fundamentals", t.fundamentals), + named ("ioSelection", t.ioSelection), + named ("profiles", t.profiles), + named ("properties", t.properties), + named ("logView", t.logView)); + } + + auto tie() const + { + return std::tie (fundamentals, + ioSelection, + profiles, + properties, + logView); + } + JUCE_TUPLE_RELATIONAL_OPS (Saved) + }; + + struct Device + { + ci::MUID muid = ci::MUID::makeUnchecked (0); + DeviceInfo info; + Profiles profiles; + Properties properties; + std::map subscribeIdForResource; + + std::optional getSubscriptionId (const String& resource) const + { + const auto iter = subscribeIdForResource.find (resource); + return iter != subscribeIdForResource.end() ? std::optional (iter->second) + : std::nullopt; + } + + template + static auto serialise (Archive& archive, This& t) + { + return archive (named ("muid", t.muid), + named ("info", t.info), + named ("profiles", t.profiles), + named ("properties", t.properties), + named ("subscribeIdForResource", t.subscribeIdForResource)); + } + + auto tie() const + { + return std::tie (muid, info, profiles, properties, subscribeIdForResource); + } + JUCE_TUPLE_RELATIONAL_OPS (Device) + }; + + struct LogEntry + { + std::vector message; + uint8_t group{}; + Time time; + MessageKind kind; + + template + static auto serialise (Archive& archive, This& t) + { + return archive (named ("message", t.message), + named ("group", t.group), + named ("time", t.time), + named ("kind", t.kind)); + } + + auto tie() const { return std::tie (message, group, time, kind); } + JUCE_TUPLE_RELATIONAL_OPS (LogEntry) + }; + + /** + App state that needs to be displayed somehow, but which shouldn't be saved or restored. + */ + struct Transient + { + // property -> device -> subId + std::map>> subscribers; + ListWithSelection devices; + std::deque logEntries; + + template + static auto serialise (Archive& archive, This& t) + { + return archive (named ("subscribers", t.subscribers), + named ("devices", t.devices), + named ("logEntries", t.logEntries)); + } + + auto tie() const { return std::tie (subscribers, devices, logEntries); } + JUCE_TUPLE_RELATIONAL_OPS (Transient) + }; + + struct App + { + Saved saved; + Transient transient; + + /** Removes properties from Transient::subscribers that are no longer present in + Properties::properties. + */ + void syncSubscribers() + { + std::set currentProperties; + for (auto& v : saved.properties.properties.items) + currentProperties.insert (v.name); + + std::set removedProperties; + for (const auto& oldSubscriber : transient.subscribers) + if (currentProperties.find (oldSubscriber.first) == currentProperties.end()) + removedProperties.insert (oldSubscriber.first); + + for (const auto& removed : removedProperties) + transient.subscribers.erase (removed); + } + + template + static auto serialise (Archive& archive, This& t) + { + return archive (named ("saved", t.saved), + named ("transient", t.transient)); + } + + auto tie() const { return std::tie (saved, transient); } + JUCE_TUPLE_RELATIONAL_OPS (App) + }; + + #undef JUCE_TUPLE_RELATIONAL_OPS + #undef JUCE_TUPLE_RELATIONAL_OP +}; + +template <> +struct juce::SerialisationTraits +{ + static constexpr auto marshallingVersion = std::nullopt; + + template + static auto serialise (Archive& archive, This& x) + { + archive (named ("name", x.name), named ("identifier", x.identifier)); + } +}; + +template <> +struct juce::SerialisationTraits +{ + static constexpr auto marshallingVersion = std::nullopt; + + template + static auto load (Archive& archive, ci::ChannelAddress& x) + { + auto group = x.getGroup(); + auto channel = x.getChannel(); + archive (named ("group", group), + named ("channel", channel)); + x = ci::ChannelAddress{}.withGroup (group).withChannel (channel); + } + + template + static auto save (Archive& archive, const ci::ChannelAddress& x) + { + archive (named ("group", x.getGroup()), + named ("channel", x.getChannel())); + } +}; + +template <> +struct juce::SerialisationTraits +{ + static constexpr auto marshallingVersion = std::nullopt; + + template + static auto serialise (Archive& archive, This& x) + { + archive (named ("profile", x.profile), named ("address", x.address)); + } +}; + +template <> +struct juce::SerialisationTraits +{ + static constexpr auto marshallingVersion = std::nullopt; + + template + static auto serialise (Archive& archive, This& x) + { + archive (named ("supported", x.supported), named ("active", x.active)); + } +}; + +class MonospaceEditor : public TextEditor +{ +public: + MonospaceEditor() + { + setFont (Font { Font::getDefaultMonospacedFontName(), 12, 0 }); + } + + void onCommit (std::function fn) + { + onEscapeKey = onReturnKey = onFocusLost = std::move (fn); + } + + void set (const String& str) + { + setText (str, false); + } +}; + +class MonospaceLabel : public Label +{ +public: + MonospaceLabel() + { + setFont (Font { Font::getDefaultMonospacedFontName(), 12, 0 }); + setMinimumHorizontalScale (1.0f); + setInterceptsMouseClicks (false, false); + } + + void onCommit (std::function) {} + + void set (const String& str) + { + setText (str, dontSendNotification); + } +}; + +enum class Editable +{ + no, + yes, +}; + +template +class TextField; + +template <> +class TextField : public MonospaceEditor { using MonospaceEditor::MonospaceEditor; }; + +template <> +class TextField : public MonospaceLabel { using MonospaceLabel::MonospaceLabel; }; + +struct Utils +{ + Utils() = delete; + + static constexpr auto padding = 10; + static constexpr auto standardControlHeight = 30; + + template + static auto makeVector (T&& t, Ts&&... ts) + { + std::vector result; + result.reserve (1 + sizeof... (ts)); + result.emplace_back (std::forward (t)); + (result.emplace_back (std::forward (ts)), ...); + return result; + } + + static std::unique_ptr