diff --git a/modules/juce_audio_plugin_client/juce_audio_plugin_client_LV2.cpp b/modules/juce_audio_plugin_client/juce_audio_plugin_client_LV2.cpp index 361465def7..01480ad4e9 100644 --- a/modules/juce_audio_plugin_client/juce_audio_plugin_client_LV2.cpp +++ b/modules/juce_audio_plugin_client/juce_audio_plugin_client_LV2.cpp @@ -55,7 +55,7 @@ #include #include "JuceLV2Defines.h" -#include +#include #include diff --git a/modules/juce_audio_processors/format_types/juce_LV2PluginFormat.cpp b/modules/juce_audio_processors/format_types/juce_LV2PluginFormat.cpp index c17fa8efdf..1c85a00d36 100644 --- a/modules/juce_audio_processors/format_types/juce_LV2PluginFormat.cpp +++ b/modules/juce_audio_processors/format_types/juce_LV2PluginFormat.cpp @@ -34,4702 +34,14 @@ #if JUCE_PLUGINHOST_LV2 && (! (JUCE_ANDROID || JUCE_IOS)) -#include -#include -#include +#include #include -#include - namespace juce { namespace lv2_host { -template -auto with (Struct s, Value Struct::* member, Value value) noexcept -{ - s.*member = std::move (value); - return s; -} - -/* Converts a void* to an LV2_Atom* if the buffer looks like it holds a well-formed Atom, or - returns nullptr otherwise. -*/ -static const LV2_Atom* convertToAtomPtr (const void* ptr, size_t size) -{ - if (size < sizeof (LV2_Atom)) - { - jassertfalse; - return nullptr; - } - - const auto header = readUnaligned (ptr); - - if (size < header.size + sizeof (LV2_Atom)) - { - jassertfalse; - return nullptr; - } - - // This is UB _if_ the ptr doesn't really point to an LV2_Atom. - return reinterpret_cast (ptr); -} - -// Allows mutable access to the items in a vector, without allowing the vector itself -// to be modified. -template -class SimpleSpan -{ -public: - constexpr SimpleSpan (T* beginIn, T* endIn) : b (beginIn), e (endIn) {} - - constexpr auto begin() const { return b; } - constexpr auto end() const { return e; } - - JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4814) - constexpr auto& operator[] (size_t index) { return b[index]; } - JUCE_END_IGNORE_WARNINGS_MSVC - - constexpr auto size() const { return (size_t) (e - b); } - -private: - T* b; - T* e; -}; - -template -constexpr auto makeSimpleSpan (T* b, T* e) { return SimpleSpan { b, e }; } - -template -constexpr auto makeSimpleSpan (R& r) { return makeSimpleSpan (r.data(), r.data() + r.size()); } - -struct PhysicalResizeListener -{ - virtual ~PhysicalResizeListener() = default; - virtual void viewRequestedResizeInPhysicalPixels (int width, int height) = 0; -}; - -struct LogicalResizeListener -{ - virtual ~LogicalResizeListener() = default; - virtual void viewRequestedResizeInLogicalPixels (int width, int height) = 0; -}; - -#if JUCE_WINDOWS -class WindowSizeChangeDetector -{ -public: - WindowSizeChangeDetector() - : hook (SetWindowsHookEx (WH_CALLWNDPROC, - callWndProc, - (HINSTANCE) juce::Process::getCurrentModuleInstanceHandle(), - GetCurrentThreadId())) - {} - - ~WindowSizeChangeDetector() noexcept - { - UnhookWindowsHookEx (hook); - } - - static void addListener (HWND hwnd, PhysicalResizeListener& listener) - { - getActiveEditors().emplace (hwnd, &listener); - } - - static void removeListener (HWND hwnd) - { - getActiveEditors().erase (hwnd); - } - -private: - static std::map& getActiveEditors() - { - static std::map map; - return map; - } - - static void processMessage (int nCode, const CWPSTRUCT* info) - { - if (nCode < 0 || info == nullptr) - return; - - constexpr UINT events[] { WM_SIZING, WM_SIZE, WM_WINDOWPOSCHANGING, WM_WINDOWPOSCHANGED }; - - if (std::find (std::begin (events), std::end (events), info->message) == std::end (events)) - return; - - auto& map = getActiveEditors(); - auto iter = map.find (info->hwnd); - - if (iter == map.end()) - return; - - RECT rect; - GetWindowRect (info->hwnd, &rect); - iter->second->viewRequestedResizeInPhysicalPixels (rect.right - rect.left, rect.bottom - rect.top); - } - - static LRESULT CALLBACK callWndProc (int nCode, WPARAM wParam, LPARAM lParam) - { - processMessage (nCode, lv2_shared::wordCast (lParam)); - return CallNextHookEx ({}, nCode, wParam, lParam); - } - - HHOOK hook; -}; - -class WindowSizeChangeListener -{ -public: - WindowSizeChangeListener (HWND hwndIn, PhysicalResizeListener& l) - : hwnd (hwndIn) - { - detector->addListener (hwnd, l); - } - - ~WindowSizeChangeListener() - { - detector->removeListener (hwnd); - } - -private: - SharedResourcePointer detector; - HWND hwnd; - - JUCE_LEAK_DETECTOR (WindowSizeChangeListener) -}; -#endif - -struct FreeLilvNode -{ - void operator() (LilvNode* ptr) const noexcept { lilv_node_free (ptr); } -}; - -using OwningNode = std::unique_ptr; - -template -class TypesafeLilvNode -{ -public: - template - TypesafeLilvNode (Ts&&... ts) - : node (Traits::construct (std::forward (ts)...)) {} - - bool equals (const TypesafeLilvNode& other) const noexcept - { - return lilv_node_equals (node.get(), other.node.get()); - } - - const LilvNode* get() const noexcept { return node.get(); } - - auto getTyped() const noexcept -> decltype (Traits::access (nullptr)) - { - return Traits::access (node.get()); - } - - static TypesafeLilvNode claim (LilvNode* node) - { - return TypesafeLilvNode { node }; - } - - static TypesafeLilvNode copy (const LilvNode* node) - { - return TypesafeLilvNode { lilv_node_duplicate (node) }; - } - -private: - explicit TypesafeLilvNode (LilvNode* ptr) - : node (ptr) - { - jassert (ptr == nullptr || Traits::verify (node.get())); - } - - OwningNode node; - - JUCE_LEAK_DETECTOR (TypesafeLilvNode) -}; - -struct UriConstructorTrait -{ - static LilvNode* construct (LilvWorld* world, const char* uri) noexcept - { - return lilv_new_uri (world, uri); - } - - static LilvNode* construct (LilvWorld* world, const char* host, const char* path) noexcept - { - return lilv_new_file_uri (world, host, path); - } - - static constexpr auto verify = lilv_node_is_uri; - static constexpr auto access = lilv_node_as_uri; -}; - -struct StringConstructorTrait { static constexpr auto construct = lilv_new_string; - static constexpr auto verify = lilv_node_is_string; - static constexpr auto access = lilv_node_as_string; }; - -using NodeUri = TypesafeLilvNode; -using NodeString = TypesafeLilvNode; - -struct UsefulUris -{ - explicit UsefulUris (LilvWorld* worldIn) - : world (worldIn) {} - - LilvWorld* const world = nullptr; - - #define X(str) const NodeUri m##str { world, str }; - X (LV2_ATOM__AtomPort) - X (LV2_ATOM__atomTransfer) - X (LV2_ATOM__eventTransfer) - X (LV2_CORE__AudioPort) - X (LV2_CORE__CVPort) - X (LV2_CORE__ControlPort) - X (LV2_CORE__GeneratorPlugin) - X (LV2_CORE__InputPort) - X (LV2_CORE__InstrumentPlugin) - X (LV2_CORE__OutputPort) - X (LV2_CORE__enumeration) - X (LV2_CORE__integer) - X (LV2_CORE__toggled) - X (LV2_RESIZE_PORT__minimumSize) - X (LV2_UI__floatProtocol) - X (LV2_WORKER__interface) - #undef X -}; - -template -struct OwningPtrTraits -{ - using type = std::unique_ptr; - static const Ptr* get (const type& t) noexcept { return t.get(); } -}; - -template -struct NonOwningPtrTraits -{ - using type = const Ptr*; - static const Ptr* get (const type& t) noexcept { return t; } -}; - -struct PluginsIteratorTraits -{ - using Container = const LilvPlugins*; - using Iter = LilvIter*; - static constexpr auto begin = lilv_plugins_begin; - static constexpr auto next = lilv_plugins_next; - static constexpr auto isEnd = lilv_plugins_is_end; - static constexpr auto get = lilv_plugins_get; -}; - -using PluginsIterator = lv2_shared::Iterator; - -struct PluginClassesIteratorTraits -{ - using Container = const LilvPluginClasses*; - using Iter = LilvIter*; - static constexpr auto begin = lilv_plugin_classes_begin; - static constexpr auto next = lilv_plugin_classes_next; - static constexpr auto isEnd = lilv_plugin_classes_is_end; - static constexpr auto get = lilv_plugin_classes_get; -}; - -using PluginClassesIterator = lv2_shared::Iterator; - -struct NodesIteratorTraits -{ - using Container = const LilvNodes*; - using Iter = LilvIter*; - static constexpr auto begin = lilv_nodes_begin; - static constexpr auto next = lilv_nodes_next; - static constexpr auto isEnd = lilv_nodes_is_end; - static constexpr auto get = lilv_nodes_get; -}; - -using NodesIterator = lv2_shared::Iterator; - -struct ScalePointsIteratorTraits -{ - using Container = const LilvScalePoints*; - using Iter = LilvIter*; - static constexpr auto begin = lilv_scale_points_begin; - static constexpr auto next = lilv_scale_points_next; - static constexpr auto isEnd = lilv_scale_points_is_end; - static constexpr auto get = lilv_scale_points_get; -}; - -using ScalePointsIterator = lv2_shared::Iterator; - -struct UisIteratorTraits -{ - using Container = const LilvUIs*; - using Iter = LilvIter*; - static constexpr auto begin = lilv_uis_begin; - static constexpr auto next = lilv_uis_next; - static constexpr auto isEnd = lilv_uis_is_end; - static constexpr auto get = lilv_uis_get; -}; - -using UisIterator = lv2_shared::Iterator; - -template -class NodesImpl -{ -public: - using type = typename PtrTraits::type; - - template - explicit NodesImpl (Ptr* ptr) - : nodes (type { ptr }) {} - - explicit NodesImpl (type ptr) - : nodes (std::move (ptr)) {} - - unsigned size() const noexcept { return lilv_nodes_size (PtrTraits::get (nodes)); } - - NodesIterator begin() const noexcept - { - return nodes == nullptr ? NodesIterator{} - : NodesIterator { PtrTraits::get (nodes) }; - } - - NodesIterator end() const noexcept { return {}; } - -private: - type nodes{}; -}; - -struct NodesFree -{ - void operator() (LilvNodes* ptr) const noexcept { lilv_nodes_free (ptr); } -}; - -using OwningNodes = NodesImpl>; -using NonOwningNodes = NodesImpl>; - -class ScalePoints -{ -public: - explicit ScalePoints (const LilvScalePoints* pt) - : points (pt) {} - - ScalePointsIterator begin() const noexcept - { - return points == nullptr ? ScalePointsIterator{} - : ScalePointsIterator { points }; - } - - ScalePointsIterator end() const noexcept { return {}; } - -private: - const LilvScalePoints* points = nullptr; -}; - -class ScalePoint -{ -public: - explicit ScalePoint (const LilvScalePoint* pt) - : point (pt) {} - - const LilvNode* getLabel() const noexcept { return lilv_scale_point_get_label (point); } - const LilvNode* getValue() const noexcept { return lilv_scale_point_get_value (point); } - -private: - const LilvScalePoint* point = nullptr; -}; - -struct PortRange -{ - float defaultValue, min, max; -}; - -class Port -{ -public: - enum class Kind - { - control, - audio, - cv, - atom, - unknown, - }; - - enum class Direction - { - input, - output, - unknown, - }; - - Port (const LilvPlugin* pluginIn, const LilvPort* portIn) - : plugin (pluginIn), port (portIn) {} - - Direction getDirection (const UsefulUris& uris) const noexcept - { - if (isA (uris.mLV2_CORE__InputPort)) - return Direction::input; - - if (isA (uris.mLV2_CORE__OutputPort)) - return Direction::output; - - return Direction::unknown; - } - - Kind getKind (const UsefulUris& uris) const noexcept - { - if (isA (uris.mLV2_CORE__ControlPort)) - return Kind::control; - - if (isA (uris.mLV2_CORE__AudioPort)) - return Kind::audio; - - if (isA (uris.mLV2_CORE__CVPort)) - return Kind::cv; - - if (isA (uris.mLV2_ATOM__AtomPort)) - return Kind::atom; - - return Kind::unknown; - } - - OwningNode get (const LilvNode* predicate) const noexcept - { - return OwningNode { lilv_port_get (plugin, port, predicate) }; - } - - NonOwningNodes getClasses() const noexcept - { - return NonOwningNodes { lilv_port_get_classes (plugin, port) }; - } - - NodeString getName() const noexcept - { - return NodeString::claim (lilv_port_get_name (plugin, port)); - } - - NodeString getSymbol() const noexcept - { - return NodeString::copy (lilv_port_get_symbol (plugin, port)); - } - - OwningNodes getProperties() const noexcept - { - return OwningNodes { lilv_port_get_properties (plugin, port) }; - } - - ScalePoints getScalePoints() const noexcept - { - return ScalePoints { lilv_port_get_scale_points (plugin, port) }; - } - - bool hasProperty (const NodeUri& uri) const noexcept - { - return lilv_port_has_property (plugin, port, uri.get()); - } - - uint32_t getIndex() const noexcept { return lilv_port_get_index (plugin, port); } - - static float getFloatValue (const LilvNode* node, float fallback) - { - if (lilv_node_is_float (node) || lilv_node_is_int (node)) - return lilv_node_as_float (node); - - return fallback; - } - - bool supportsEvent (const LilvNode* node) const noexcept - { - return lilv_port_supports_event (plugin, port, node); - } - - PortRange getRange() const noexcept - { - LilvNode* def = nullptr; - LilvNode* min = nullptr; - LilvNode* max = nullptr; - - lilv_port_get_range (plugin, port, &def, &min, &max); - - const OwningNode defOwner { def }; - const OwningNode minOwner { min }; - const OwningNode maxOwner { max }; - - return { getFloatValue (def, 0.0f), - getFloatValue (min, 0.0f), - getFloatValue (max, 1.0f) }; - } - - bool isValid() const noexcept { return port != nullptr; } - -private: - bool isA (const NodeUri& uri) const noexcept - { - return lilv_port_is_a (plugin, port, uri.get()); - } - - const LilvPlugin* plugin = nullptr; - const LilvPort* port = nullptr; - - JUCE_LEAK_DETECTOR (Port) -}; - -class Plugin -{ -public: - explicit Plugin (const LilvPlugin* p) : plugin (p) {} - - bool verify() const noexcept { return lilv_plugin_verify (plugin); } - NodeUri getUri() const noexcept { return NodeUri::copy (lilv_plugin_get_uri (plugin)); } - NodeUri getBundleUri() const noexcept { return NodeUri::copy (lilv_plugin_get_bundle_uri (plugin)); } - NodeUri getLibraryUri() const noexcept { return NodeUri::copy (lilv_plugin_get_library_uri (plugin)); } - NodeString getName() const noexcept { return NodeString::claim (lilv_plugin_get_name (plugin)); } - NodeString getAuthorName() const noexcept { return NodeString::claim (lilv_plugin_get_author_name (plugin)); } - uint32_t getNumPorts() const noexcept { return lilv_plugin_get_num_ports (plugin); } - const LilvPluginClass* getClass() const noexcept { return lilv_plugin_get_class (plugin); } - OwningNodes getValue (const LilvNode* predicate) const noexcept { return OwningNodes { lilv_plugin_get_value (plugin, predicate) }; } - - Port getPortByIndex (uint32_t index) const noexcept - { - return Port { plugin, lilv_plugin_get_port_by_index (plugin, index) }; - } - - Port getPortByDesignation (const LilvNode* portClass, const LilvNode* designation) const noexcept - { - return Port { plugin, lilv_plugin_get_port_by_designation (plugin, portClass, designation) }; - } - - OwningNodes getRequiredFeatures() const noexcept - { - return OwningNodes { lilv_plugin_get_required_features (plugin) }; - } - - OwningNodes getOptionalFeatures() const noexcept - { - return OwningNodes { lilv_plugin_get_optional_features (plugin) }; - } - - bool hasExtensionData (const NodeUri& uri) const noexcept - { - return lilv_plugin_has_extension_data (plugin, uri.get()); - } - - bool hasFeature (const NodeUri& uri) const noexcept - { - return lilv_plugin_has_feature (plugin, uri.get()); - } - - template - uint32_t getNumPortsOfClass (const Classes&... classes) const noexcept - { - return lilv_plugin_get_num_ports_of_class (plugin, classes.get()..., 0); - } - - const LilvPlugin* get() const noexcept { return plugin; } - - bool hasLatency() const noexcept { return lilv_plugin_has_latency (plugin); } - uint32_t getLatencyPortIndex() const noexcept { return lilv_plugin_get_latency_port_index (plugin); } - -private: - const LilvPlugin* plugin = nullptr; - - JUCE_LEAK_DETECTOR (Plugin) -}; - -/* - This is very similar to the symap implementation in jalv. -*/ -class SymbolMap -{ -public: - SymbolMap() = default; - - SymbolMap (std::initializer_list uris) - { - for (const auto* str : uris) - map (str); - } - - LV2_URID map (const char* uri) - { - const auto comparator = [this] (size_t index, const String& str) - { - return strings[index] < str; - }; - - const auto uriString = String::fromUTF8 (uri); - const auto it = std::lower_bound (indices.cbegin(), indices.cend(), uriString, comparator); - - if (it != indices.cend() && strings[*it] == uriString) - return static_cast (*it + 1); - - const auto index = strings.size(); - indices.insert (it, index); - strings.push_back (uriString); - return static_cast (index + 1); - } - - const char* unmap (LV2_URID urid) const - { - const auto index = urid - 1; - return index < strings.size() ? strings[index].toRawUTF8() - : nullptr; - } - - static LV2_URID mapUri (LV2_URID_Map_Handle handle, const char* uri) - { - return static_cast (handle)->map (uri); - } - - static const char* unmapUri (LV2_URID_Unmap_Handle handle, LV2_URID urid) - { - return static_cast (handle)->unmap (urid); - } - - LV2_URID_Map getMapFeature() { return { this, mapUri }; } - LV2_URID_Unmap getUnmapFeature() { return { this, unmapUri }; } - -private: - std::vector strings; - std::vector indices; - - JUCE_LEAK_DETECTOR (SymbolMap) -}; - -struct UsefulUrids -{ - explicit UsefulUrids (SymbolMap& m) : symap (m) {} - - SymbolMap& symap; - - #define X(token) const LV2_URID m##token = symap.map (token); - X (LV2_ATOM__Bool) - X (LV2_ATOM__Double) - X (LV2_ATOM__Float) - X (LV2_ATOM__Int) - X (LV2_ATOM__Long) - X (LV2_ATOM__Object) - X (LV2_ATOM__Sequence) - X (LV2_ATOM__atomTransfer) - X (LV2_ATOM__beatTime) - X (LV2_ATOM__eventTransfer) - X (LV2_ATOM__frameTime) - X (LV2_LOG__Error) - X (LV2_LOG__Note) - X (LV2_LOG__Trace) - X (LV2_LOG__Warning) - X (LV2_MIDI__MidiEvent) - X (LV2_PATCH__Set) - X (LV2_PATCH__property) - X (LV2_PATCH__value) - X (LV2_STATE__StateChanged) - X (LV2_TIME__Position) - X (LV2_TIME__barBeat) - X (LV2_TIME__beat) - X (LV2_TIME__beatUnit) - X (LV2_TIME__beatsPerBar) - X (LV2_TIME__beatsPerMinute) - X (LV2_TIME__frame) - X (LV2_TIME__speed) - X (LV2_TIME__bar) - X (LV2_UI__floatProtocol) - X (LV2_UNITS__beat) - X (LV2_UNITS__frame) - #undef X -}; - -class Log -{ -public: - explicit Log (const UsefulUrids* u) : urids (u) {} - - LV2_Log_Log* getLogFeature() { return &logFeature; } - -private: - int vprintfCallback ([[maybe_unused]] LV2_URID type, const char* fmt, va_list ap) const - { - // If this is hit, the plugin has encountered some kind of error - ignoreUnused (urids); - jassert (type != urids->mLV2_LOG__Error && type != urids->mLV2_LOG__Warning); - return std::vfprintf (stderr, fmt, ap); - } - - static int vprintfCallback (LV2_Log_Handle handle, - LV2_URID type, - const char* fmt, - va_list ap) - { - return static_cast (handle)->vprintfCallback (type, fmt, ap); - } - - static int printfCallback (LV2_Log_Handle handle, LV2_URID type, const char* fmt, ...) - { - va_list list; - va_start (list, fmt); - auto result = vprintfCallback (handle, type, fmt, list); - va_end (list); - return result; - } - - const UsefulUrids* urids = nullptr; - LV2_Log_Log logFeature { this, printfCallback, vprintfCallback }; - - JUCE_LEAK_DETECTOR (Log) -}; - -struct Features -{ - explicit Features (std::vector&& f) - : features (std::move (f)) {} - - static std::vector getUris (const std::vector& features) - { - std::vector result; - result.reserve (features.size()); - - for (const auto& feature : features) - result.push_back (String::fromUTF8 (feature.URI)); - - return result; - } - - std::vector features; - std::vector pointers = makeNullTerminatedArray(); - -private: - std::vector makeNullTerminatedArray() - { - std::vector result; - result.reserve (features.size() + 1); - - for (const auto& feature : features) - result.push_back (&feature); - - result.push_back (nullptr); - - return result; - } - - JUCE_LEAK_DETECTOR (Features) -}; - -template -struct OptionalExtension -{ - OptionalExtension() = default; - - explicit OptionalExtension (Extension extensionIn) : extension (extensionIn), valid (true) {} - - Extension extension; - bool valid = false; -}; - -class Instance -{ - struct Free - { - void operator() (LilvInstance* ptr) const noexcept { lilv_instance_free (ptr); } - }; - -public: - using Ptr = std::unique_ptr; - using GetExtensionData = const void* (*) (const char*); - - Instance (const Plugin& pluginIn, double sampleRate, const LV2_Feature* const* features) - : plugin (pluginIn), - instance (lilv_plugin_instantiate (plugin.get(), sampleRate, features)) {} - - void activate() { lilv_instance_activate (instance.get()); } - void run (uint32_t sampleCount) { lilv_instance_run (instance.get(), sampleCount); } - void deactivate() { lilv_instance_deactivate (instance.get()); } - - const char* getUri() const noexcept { return lilv_instance_get_uri (instance.get()); } - - LV2_Handle getHandle() const noexcept { return lilv_instance_get_handle (instance.get()); } - - LilvInstance* get() const noexcept { return instance.get(); } - - void connectPort (uint32_t index, void* data) - { - lilv_instance_connect_port (instance.get(), index, data); - } - - template - OptionalExtension getExtensionData (const NodeUri& uri) const noexcept - { - if (plugin.get() == nullptr || ! plugin.hasExtensionData (uri) || instance.get() == nullptr) - return {}; - - return OptionalExtension { readUnaligned (lilv_instance_get_extension_data (instance.get(), uri.getTyped())) }; - } - - GetExtensionData getExtensionDataCallback() const noexcept - { - return instance->lv2_descriptor->extension_data; - } - - bool operator== (std::nullptr_t) const noexcept { return instance == nullptr; } - bool operator!= (std::nullptr_t) const noexcept { return ! (*this == nullptr); } - -private: - Plugin plugin; - Ptr instance; - - JUCE_LEAK_DETECTOR (Instance) -}; - -enum class Realtime { no, yes }; - -// Must be trivial! -struct WorkResponder -{ - static WorkResponder getDefault() { return { nullptr, nullptr }; } - - LV2_Worker_Status processResponse (uint32_t size, const void* data) const - { - return worker->work_response (handle, size, data); - } - - bool isValid() const { return handle != nullptr && worker != nullptr; } - - LV2_Handle handle; - const LV2_Worker_Interface* worker; -}; - -struct WorkerResponseListener -{ - virtual ~WorkerResponseListener() = default; - virtual LV2_Worker_Status responseGenerated (WorkResponder, uint32_t, const void*) = 0; -}; - -struct RespondHandle -{ - LV2_Worker_Status respond (uint32_t size, const void* data) const - { - if (realtime == Realtime::yes) - return listener.responseGenerated (responder, size, data); - - return responder.processResponse (size, data); - } - - static LV2_Worker_Status respond (LV2_Worker_Respond_Handle handle, - uint32_t size, - const void* data) - { - return static_cast (handle)->respond (size, data); - } - - WorkResponder responder; - WorkerResponseListener& listener; - Realtime realtime; -}; - -// Must be trivial! -struct WorkSubmitter -{ - static WorkSubmitter getDefault() { return { nullptr, nullptr, nullptr, nullptr }; } - - LV2_Worker_Status doWork (Realtime realtime, uint32_t size, const void* data) const - { - // The Worker spec says that the host "MUST NOT make concurrent calls to [work] from - // several threads". - // Taking the work mutex here ensures that only one piece of work is done at a time. - // If we didn't take the work mutex, there would be a danger of work happening - // simultaneously on the worker thread and the render thread when switching between - // realtime/offline modes (in realtime mode, work happens on the worker thread; in - // offline mode, work happens immediately on the render/audio thread). - const ScopedLock lock (*workMutex); - - RespondHandle respondHandle { WorkResponder { handle, worker }, *listener, realtime }; - return worker->work (handle, RespondHandle::respond, &respondHandle, size, data); - } - - bool isValid() const { return handle != nullptr && worker != nullptr && listener != nullptr && workMutex != nullptr; } - - LV2_Handle handle; - const LV2_Worker_Interface* worker; - WorkerResponseListener* listener; - CriticalSection* workMutex; -}; - -template -static auto toChars (Trivial value) -{ - static_assert (std::is_trivial_v); - std::array result; - writeUnaligned (result.data(), value); - return result; -} - -template -class WorkQueue -{ -public: - static_assert (std::is_trivial_v, "Context must be copyable as bytes"); - - explicit WorkQueue (int size) - : fifo (size), data (static_cast (size)) {} - - LV2_Worker_Status push (Context context, size_t size, const void* contents) - { - const auto* bytes = static_cast (contents); - const auto numToWrite = sizeof (Header) + size; - - if (static_cast (fifo.getFreeSpace()) < numToWrite) - return LV2_WORKER_ERR_NO_SPACE; - - Header header { size, context }; - const auto headerBuffer = toChars (header); - - const auto scope = fifo.write (static_cast (numToWrite)); - jassert (scope.blockSize1 + scope.blockSize2 == static_cast (numToWrite)); - - size_t index = 0; - scope.forEach ([&] (int i) - { - data[static_cast (i)] = index < headerBuffer.size() ? headerBuffer[index] - : bytes[index - headerBuffer.size()]; - ++index; - }); - - return LV2_WORKER_SUCCESS; - } - - Context pop (std::vector& dest) - { - // If the vector is too small we'll have to resize it on the audio thread - jassert (dest.capacity() >= data.size()); - dest.clear(); - - const auto numReady = fifo.getNumReady(); - - if (static_cast (numReady) < sizeof (Header)) - { - jassert (numReady == 0); - return Context::getDefault(); - } - - std::array headerBuffer; - - { - size_t index = 0; - fifo.read (sizeof (Header)).forEach ([&] (int i) - { - headerBuffer[index++] = data[static_cast (i)]; - }); - } - - const auto header = readUnaligned
(headerBuffer.data()); - - jassert (static_cast (fifo.getNumReady()) >= header.size); - - dest.resize (header.size); - - { - size_t index = 0; - fifo.read (static_cast (header.size)).forEach ([&] (int i) - { - dest[index++] = data[static_cast (i)]; - }); - } - - return header.context; - } - -private: - struct Header - { - size_t size; - Context context; - }; - - AbstractFifo fifo; - std::vector data; - - JUCE_LEAK_DETECTOR (WorkQueue) -}; - -/* - Keeps track of active plugin instances, so that we can avoid sending work - messages to dead plugins. -*/ -class HandleRegistry -{ -public: - void insert (LV2_Handle handle) - { - const SpinLock::ScopedLockType lock (mutex); - handles.insert (handle); - } - - void erase (LV2_Handle handle) - { - const SpinLock::ScopedLockType lock (mutex); - handles.erase (handle); - } - - template - LV2_Worker_Status ifContains (LV2_Handle handle, Fn&& callback) - { - const SpinLock::ScopedLockType lock (mutex); - - if (handles.find (handle) != handles.cend()) - return callback(); - - return LV2_WORKER_ERR_UNKNOWN; - } - -private: - std::set handles; - SpinLock mutex; - - JUCE_LEAK_DETECTOR (HandleRegistry) -}; - -/* - Implements an LV2 Worker, allowing work to be scheduled in realtime - by the plugin instance. - - IMPORTANT this will die pretty hard if `getExtensionData (LV2_WORKER__interface)` - returns garbage, so make sure to check that the plugin `hasExtensionData` before - constructing one of these! -*/ -class SharedThreadedWorker final : public WorkerResponseListener -{ -public: - ~SharedThreadedWorker() noexcept override - { - shouldExit = true; - thread.join(); - } - - LV2_Worker_Status schedule (WorkSubmitter submitter, - uint32_t size, - const void* data) - { - return registry.ifContains (submitter.handle, [&] - { - return incoming.push (submitter, size, data); - }); - } - - LV2_Worker_Status responseGenerated (WorkResponder responder, - uint32_t size, - const void* data) override - { - return registry.ifContains (responder.handle, [&] - { - return outgoing.push (responder, size, data); - }); - } - - void processResponses() - { - for (;;) - { - auto workerResponder = outgoing.pop (message); - - if (! message.empty() && workerResponder.isValid()) - workerResponder.processResponse (static_cast (message.size()), message.data()); - else - break; - } - } - - void registerHandle (LV2_Handle handle) { registry.insert (handle); } - void deregisterHandle (LV2_Handle handle) { registry.erase (handle); } - -private: - static constexpr auto queueSize = 8192; - std::atomic shouldExit { false }; - WorkQueue incoming { queueSize }; - WorkQueue outgoing { queueSize }; - std::vector message = std::vector (queueSize); - std::thread thread { [this] - { - std::vector buffer (queueSize); - - while (! shouldExit) - { - const auto submitter = incoming.pop (buffer); - - if (! buffer.empty() && submitter.isValid()) - submitter.doWork (Realtime::yes, (uint32_t) buffer.size(), buffer.data()); - else - std::this_thread::sleep_for (std::chrono::milliseconds (1)); - } - } }; - HandleRegistry registry; - - JUCE_LEAK_DETECTOR (SharedThreadedWorker) -}; - -struct HandleHolder -{ - virtual ~HandleHolder() = default; - virtual LV2_Handle getHandle() const = 0; - virtual const LV2_Worker_Interface* getWorkerInterface() const = 0; -}; - -class WorkScheduler -{ -public: - explicit WorkScheduler (HandleHolder& handleHolderIn) - : handleHolder (handleHolderIn) {} - - void processResponses() { workerThread->processResponses(); } - - LV2_Worker_Schedule& getWorkerSchedule() { return schedule; } - - void setNonRealtime (bool nonRealtime) { realtime = ! nonRealtime; } - - void registerHandle (LV2_Handle handle) { workerThread->registerHandle (handle); } - void deregisterHandle (LV2_Handle handle) { workerThread->deregisterHandle (handle); } - -private: - LV2_Worker_Status scheduleWork (uint32_t size, const void* data) - { - WorkSubmitter submitter { handleHolder.getHandle(), - handleHolder.getWorkerInterface(), - workerThread, - &workMutex }; - - // If we're in realtime mode, the work should go onto a background thread, - // and we'll process it later. - // If we're offline, we can just do the work immediately, without worrying about - // drop-outs - return realtime ? workerThread->schedule (submitter, size, data) - : submitter.doWork (Realtime::no, size, data); - } - - static LV2_Worker_Status scheduleWork (LV2_Worker_Schedule_Handle handle, - uint32_t size, - const void* data) - { - return static_cast (handle)->scheduleWork (size, data); - } - - SharedResourcePointer workerThread; - HandleHolder& handleHolder; - LV2_Worker_Schedule schedule { this, scheduleWork }; - CriticalSection workMutex; - bool realtime = true; - - JUCE_LEAK_DETECTOR (WorkScheduler) -}; - -struct FeaturesDataListener -{ - virtual ~FeaturesDataListener() = default; - virtual LV2_Resize_Port_Status resizeCallback (uint32_t index, size_t size) = 0; -}; - -class Resize -{ -public: - explicit Resize (FeaturesDataListener& l) - : listener (l) {} - - LV2_Resize_Port_Resize& getFeature() { return resize; } - -private: - LV2_Resize_Port_Status resizeCallback (uint32_t index, size_t size) - { - return listener.resizeCallback (index, size); - } - - static LV2_Resize_Port_Status resizeCallback (LV2_Resize_Port_Feature_Data data, uint32_t index, size_t size) - { - return static_cast (data)->resizeCallback (index, size); - } - - FeaturesDataListener& listener; - LV2_Resize_Port_Resize resize { this, resizeCallback }; -}; - -class FeaturesData -{ -public: - FeaturesData (HandleHolder& handleHolder, - FeaturesDataListener& l, - int32_t maxBlockSizeIn, - int32_t sequenceSizeIn, - const UsefulUrids* u) - : urids (u), - resize (l), - maxBlockSize (maxBlockSizeIn), - sequenceSize (sequenceSizeIn), - workScheduler (handleHolder) - {} - - LV2_Options_Option* getOptions() noexcept { return options.data(); } - - int32_t getMaxBlockSize() const noexcept { return maxBlockSize; } - - void setNonRealtime (bool newValue) { realtime = ! newValue; } - - const LV2_Feature* const* getFeatureArray() const noexcept { return features.pointers.data(); } - - static std::vector getFeatureUris() - { - return Features::getUris (makeFeatures ({}, {}, {}, {}, {}, {})); - } - - void processResponses() { workScheduler.processResponses(); } - - void registerHandle (LV2_Handle handle) { workScheduler.registerHandle (handle); } - void deregisterHandle (LV2_Handle handle) { workScheduler.deregisterHandle (handle); } - -private: - static std::vector makeFeatures (LV2_URID_Map* map, - LV2_URID_Unmap* unmap, - LV2_Options_Option* options, - LV2_Worker_Schedule* schedule, - LV2_Resize_Port_Resize* resize, - [[maybe_unused]] LV2_Log_Log* log) - { - return { LV2_Feature { LV2_STATE__loadDefaultState, nullptr }, - LV2_Feature { LV2_BUF_SIZE__boundedBlockLength, nullptr }, - LV2_Feature { LV2_URID__map, map }, - LV2_Feature { LV2_URID__unmap, unmap }, - LV2_Feature { LV2_OPTIONS__options, options }, - LV2_Feature { LV2_WORKER__schedule, schedule }, - LV2_Feature { LV2_STATE__threadSafeRestore, nullptr }, - #if JUCE_DEBUG - LV2_Feature { LV2_LOG__log, log }, - #endif - LV2_Feature { LV2_RESIZE_PORT__resize, resize } }; - } - - LV2_Options_Option makeOption (const char* uid, const int32_t* ptr) - { - return { LV2_OPTIONS_INSTANCE, - 0, // INSTANCE kinds must have a subject of 0 - urids->symap.map (uid), - sizeof (int32_t), - urids->symap.map (LV2_ATOM__Int), - ptr }; - } - - const UsefulUrids* urids; - Resize resize; - Log log { urids }; - - const int32_t minBlockSize = 0, maxBlockSize = 0, sequenceSize = 0; - - std::vector options - { - makeOption (LV2_BUF_SIZE__minBlockLength, &minBlockSize), - makeOption (LV2_BUF_SIZE__maxBlockLength, &maxBlockSize), - makeOption (LV2_BUF_SIZE__sequenceSize, &sequenceSize), - { LV2_OPTIONS_INSTANCE, 0, 0, 0, 0, nullptr }, // The final entry must be nulled out - }; - - WorkScheduler workScheduler; - - LV2_URID_Map map = urids->symap.getMapFeature(); - LV2_URID_Unmap unmap = urids->symap.getUnmapFeature(); - Features features { makeFeatures (&map, - &unmap, - options.data(), - &workScheduler.getWorkerSchedule(), - &resize.getFeature(), - log.getLogFeature()) }; - - bool realtime = true; - - JUCE_LEAK_DETECTOR (FeaturesData) -}; - -//============================================================================== -struct TryLockAndCall -{ - template - void operator() (SpinLock& mutex, Fn&& fn) - { - const SpinLock::ScopedTryLockType lock (mutex); - - if (lock.isLocked()) - fn(); - } -}; - -struct LockAndCall -{ - template - void operator() (SpinLock& mutex, Fn&& fn) - { - const SpinLock::ScopedLockType lock (mutex); - fn(); - } -}; - -struct RealtimeReadTrait -{ - using Read = TryLockAndCall; - using Write = LockAndCall; -}; - -struct RealtimeWriteTrait -{ - using Read = LockAndCall; - using Write = TryLockAndCall; -}; - -struct MessageHeader -{ - uint32_t portIndex; - uint32_t protocol; -}; - -template -struct MessageBufferInterface -{ - virtual ~MessageBufferInterface() = default; - virtual void pushMessage (Header header, uint32_t size, const void* buffer) = 0; -}; - -template -class Messages final : public MessageBufferInterface
-{ - using Read = typename LockTraits::Read; - using Write = typename LockTraits::Write; - - struct FullHeader - { - Header header; - uint32_t size; - }; - -public: - Messages() { data.reserve (initialBufferSize); } - - void pushMessage (Header header, uint32_t size, const void* buffer) override - { - Write{} (mutex, [&] - { - const auto chars = toChars (FullHeader { header, size }); - const auto bufferAsChars = static_cast (buffer); - data.insert (data.end(), chars.begin(), chars.end()); - data.insert (data.end(), bufferAsChars, bufferAsChars + size); - }); - } - - template - void readAllAndClear (Callback&& callback) - { - Read{} (mutex, [&] - { - if (data.empty()) - return; - - const auto end = data.data() + data.size(); - - for (auto ptr = data.data(); ptr < end;) - { - const auto header = readUnaligned (ptr); - callback (header.header, header.size, ptr + sizeof (header)); - ptr += sizeof (header) + header.size; - } - - data.clear(); - }); - } - -private: - static constexpr auto initialBufferSize = 8192; - SpinLock mutex; - std::vector data; - - JUCE_LEAK_DETECTOR (Messages) -}; - -//============================================================================== -struct UiEventListener : public MessageBufferInterface -{ - virtual int idle() = 0; -}; - -struct UiMessageHeader -{ - UiEventListener* listener; - MessageHeader header; -}; - -class ProcessorToUi final : public MessageBufferInterface -{ -public: - ProcessorToUi() { timer.startTimerHz (60); } - - void addUi (UiEventListener& l) { JUCE_ASSERT_MESSAGE_THREAD; activeUis.insert (&l); } - void removeUi (UiEventListener& l) { JUCE_ASSERT_MESSAGE_THREAD; activeUis.erase (&l); } - - void pushMessage (UiMessageHeader header, uint32_t size, const void* buffer) override - { - processorToUi.pushMessage (header, size, buffer); - } - -private: - Messages processorToUi; - std::set activeUis; - TimedCallback timer { [this] - { - for (auto* l : activeUis) - if (l->idle() != 0) - return; - - processorToUi.readAllAndClear ([&] (const UiMessageHeader& header, uint32_t size, const char* data) - { - if (activeUis.find (header.listener) != activeUis.cend()) - header.listener->pushMessage (header.header, size, data); - }); - } }; -}; - -/* These type identifiers may be used to check the type of the incoming data. */ -struct StatefulPortUrids -{ - explicit StatefulPortUrids (SymbolMap& map) - : Float (map.map (LV2_ATOM__Float)), - Double (map.map (LV2_ATOM__Double)), - Int (map.map (LV2_ATOM__Int)), - Long (map.map (LV2_ATOM__Long)) - {} - - const LV2_URID Float, Double, Int, Long; -}; - -/* - A bit like SortedSet, but only requires `operator<` and not `operator==`, so - it behaves a bit more like a std::set. -*/ -template -class SafeSortedSet -{ -public: - using iterator = typename std::vector:: iterator; - using const_iterator = typename std::vector::const_iterator; - - template - const_iterator find (const Other& other) const noexcept - { - const auto it = std::lower_bound (storage.cbegin(), storage.cend(), other); - - if (it != storage.cend() && ! (other < *it)) - return it; - - return storage.cend(); - } - - void insert (Value&& value) { insertImpl (std::move (value)); } - void insert (const Value& value) { insertImpl (value); } - - size_t size() const noexcept { return storage.size(); } - bool empty() const noexcept { return storage.empty(); } - - iterator begin() noexcept { return storage. begin(); } - const_iterator begin() const noexcept { return storage. begin(); } - const_iterator cbegin() const noexcept { return storage.cbegin(); } - - iterator end() noexcept { return storage. end(); } - const_iterator end() const noexcept { return storage. end(); } - const_iterator cend() const noexcept { return storage.cend(); } - - auto& operator[] (size_t index) const { return storage[index]; } - -private: - template - void insertImpl (Arg&& value) - { - const auto it = std::lower_bound (storage.cbegin(), storage.cend(), value); - - if (it == storage.cend() || value < *it) - storage.insert (it, std::forward (value)); - } - - std::vector storage; -}; - -struct StoredScalePoint -{ - String label; - float value; - - bool operator< (const StoredScalePoint& other) const noexcept { return value < other.value; } -}; - -inline bool operator< (const StoredScalePoint& a, float b) noexcept { return a.value < b; } -inline bool operator< (float a, const StoredScalePoint& b) noexcept { return a < b.value; } - -struct ParameterInfo -{ - ParameterInfo() = default; - - ParameterInfo (SafeSortedSet scalePointsIn, - String identifierIn, - float defaultValueIn, - float minIn, - float maxIn, - bool isToggleIn, - bool isIntegerIn, - bool isEnumIn) - : scalePoints (std::move (scalePointsIn)), - identifier (std::move (identifierIn)), - defaultValue (defaultValueIn), - min (minIn), - max (maxIn), - isToggle (isToggleIn), - isInteger (isIntegerIn), - isEnum (isEnumIn) - {} - - static SafeSortedSet getScalePoints (const Port& port) - { - SafeSortedSet scalePoints; - - for (const LilvScalePoint* p : port.getScalePoints()) - { - const ScalePoint wrapper { p }; - const auto value = wrapper.getValue(); - const auto label = wrapper.getLabel(); - - if (lilv_node_is_float (value) || lilv_node_is_int (value)) - scalePoints.insert ({ lilv_node_as_string (label), lilv_node_as_float (value) }); - } - - return scalePoints; - } - - static ParameterInfo getInfoForPort (const UsefulUris& uris, const Port& port) - { - const auto range = port.getRange(); - - return { getScalePoints (port), - "sym:" + String::fromUTF8 (port.getSymbol().getTyped()), - range.defaultValue, - range.min, - range.max, - port.hasProperty (uris.mLV2_CORE__toggled), - port.hasProperty (uris.mLV2_CORE__integer), - port.hasProperty (uris.mLV2_CORE__enumeration) }; - } - - SafeSortedSet scalePoints; - - /* This is the 'symbol' of a port, or the 'designation' of a parameter without a symbol. */ - String identifier; - - float defaultValue = 0.0f, min = 0.0f, max = 1.0f; - bool isToggle = false, isInteger = false, isEnum = false; - - JUCE_LEAK_DETECTOR (ParameterInfo) -}; - -struct PortHeader -{ - String name; - String symbol; - uint32_t index; - Port::Direction direction; -}; - -struct ControlPort -{ - ControlPort (const PortHeader& headerIn, const ParameterInfo& infoIn) - : header (headerIn), info (infoIn) {} - - PortHeader header; - ParameterInfo info; - float currentValue = info.defaultValue; -}; - -struct CVPort -{ - PortHeader header; -}; - -struct AudioPort -{ - PortHeader header; -}; - -template -class SingleSizeAlignedStorage -{ -public: - SingleSizeAlignedStorage() = default; - - explicit SingleSizeAlignedStorage (size_t sizeInBytes) - : storage (new char[sizeInBytes + Alignment]), - alignedPointer (storage.get()), - space (sizeInBytes + Alignment) - { - alignedPointer = std::align (Alignment, sizeInBytes, alignedPointer, space); - } - - void* data() const { return alignedPointer; } - size_t size() const { return space; } - -private: - std::unique_ptr storage; - void* alignedPointer = nullptr; - size_t space = 0; -}; - -template -static SingleSizeAlignedStorage grow (SingleSizeAlignedStorage storage, size_t size) -{ - if (size <= storage.size()) - return storage; - - SingleSizeAlignedStorage newStorage { jmax (size, (storage.size() * 3) / 2) }; - std::memcpy (newStorage.data(), storage.data(), storage.size()); - return newStorage; -} - -enum class SupportsTime { no, yes }; - -class AtomPort -{ -public: - AtomPort (PortHeader h, size_t bytes, SymbolMap& map, SupportsTime supportsTime) - : header (h), contents (bytes), forge (map.getMapFeature()), time (supportsTime) {} - - PortHeader header; - - void replaceWithChunk() - { - forge.setBuffer (data(), size()); - forge.writeChunk ((uint32_t) (size() - sizeof (LV2_Atom))); - } - - void replaceBufferWithAtom (const LV2_Atom* atom) - { - const auto totalSize = atom->size + sizeof (LV2_Atom); - - if (totalSize <= size()) - std::memcpy (data(), atom, totalSize); - else - replaceWithChunk(); - } - - void beginSequence() - { - forge.setBuffer (data(), size()); - lv2_atom_forge_sequence_head (forge.get(), &frame, 0); - } - - void endSequence() - { - lv2_atom_forge_pop (forge.get(), &frame); - } - - /* For this to work, the 'atom' pointer must be well-formed. - - It must be followed by an atom header, then at least 'size' bytes of body. - */ - void addAtomToSequence (int64_t timestamp, const LV2_Atom* atom) - { - // This reinterpret_cast is not UB, casting to a char* is acceptable. - // Doing arithmetic on this pointer is dubious, but I can't think of a better alternative - // given that we don't have any way of knowing the concrete type of the atom. - addEventToSequence (timestamp, - atom->type, - atom->size, - reinterpret_cast (atom) + sizeof (LV2_Atom)); - } - - void addEventToSequence (int64_t timestamp, uint32_t type, uint32_t size, const void* content) - { - lv2_atom_forge_frame_time (forge.get(), timestamp); - lv2_atom_forge_atom (forge.get(), size, type); - lv2_atom_forge_write (forge.get(), content, size); - } - - void ensureSizeInBytes (size_t size) - { - contents = grow (std::move (contents), size); - } - - char* data() noexcept { return data (*this); } - const char* data() const noexcept { return data (*this); } - - size_t size() const noexcept { return contents.size(); } - - lv2_shared::AtomForge& getForge() { return forge; } - const lv2_shared::AtomForge& getForge() const { return forge; } - - bool getSupportsTime() const { return time == SupportsTime::yes; } - -private: - template - static auto data (This& t) -> decltype (t.data()) - { - return unalignedPointerCast (t.contents.data()); - } - - // Atoms are required to be 64-bit aligned - SingleSizeAlignedStorage<8> contents; - lv2_shared::AtomForge forge; - LV2_Atom_Forge_Frame frame; - SupportsTime time = SupportsTime::no; -}; - -struct FreeString { void operator() (void* ptr) const noexcept { lilv_free (ptr); } }; - -static File bundlePathFromUri (const char* uri) -{ - return File { std::unique_ptr { lilv_file_uri_parse (uri, nullptr) }.get() }; -} - -class Plugins -{ -public: - explicit Plugins (const LilvPlugins* list) noexcept : plugins (list) {} - - unsigned size() const noexcept { return lilv_plugins_size (plugins); } - - PluginsIterator begin() const noexcept { return PluginsIterator { plugins }; } - PluginsIterator end() const noexcept { return PluginsIterator{}; } - - const LilvPlugin* getByUri (const NodeUri& uri) const - { - return lilv_plugins_get_by_uri (plugins, uri.get()); - } - - const LilvPlugin* getByFile (const File& file) const - { - for (const auto* plugin : *this) - { - if (bundlePathFromUri (lilv_node_as_uri (lilv_plugin_get_bundle_uri (plugin))) == file) - return plugin; - } - - return nullptr; - } - -private: - const LilvPlugins* plugins = nullptr; -}; - -template -class PluginClassesImpl -{ -public: - using type = typename PtrTraits::type; - - explicit PluginClassesImpl (type ptr) - : classes (std::move (ptr)) {} - - unsigned size() const noexcept { return lilv_plugin_classes_size (PtrTraits::get (classes)); } - - PluginClassesIterator begin() const noexcept { return PluginClassesIterator { PtrTraits::get (classes) }; } - PluginClassesIterator end() const noexcept { return PluginClassesIterator{}; } - - const LilvPluginClass* getByUri (const NodeUri& uri) const noexcept - { - return lilv_plugin_classes_get_by_uri (PtrTraits::get (classes), uri.get()); - } - -private: - type classes{}; -}; - -struct PluginClassesFree -{ - void operator() (LilvPluginClasses* ptr) const noexcept { lilv_plugin_classes_free (ptr); } -}; - -using OwningPluginClasses = PluginClassesImpl>; -using NonOwningPluginClasses = PluginClassesImpl>; - -class World -{ -public: - World() : world (lilv_world_new()) {} - - void loadAllFromPaths (const NodeString& paths) - { - lilv_world_set_option (world.get(), LILV_OPTION_LV2_PATH, paths.get()); - lilv_world_load_all (world.get()); - } - - void loadBundle (const NodeUri& uri) { lilv_world_load_bundle (world.get(), uri.get()); } - void unloadBundle (const NodeUri& uri) { lilv_world_unload_bundle (world.get(), uri.get()); } - - void loadResource (const NodeUri& uri) { lilv_world_load_resource (world.get(), uri.get()); } - void unloadResource (const NodeUri& uri) { lilv_world_unload_resource (world.get(), uri.get()); } - - void loadSpecifications() { lilv_world_load_specifications (world.get()); } - void loadPluginClasses() { lilv_world_load_plugin_classes (world.get()); } - - Plugins getAllPlugins() const { return Plugins { lilv_world_get_all_plugins (world.get()) }; } - NonOwningPluginClasses getPluginClasses() const { return NonOwningPluginClasses { lilv_world_get_plugin_classes (world.get()) }; } - - NodeUri newUri (const char* uri) { return NodeUri { world.get(), uri }; } - NodeUri newFileUri (const char* host, const char* path) { return NodeUri { world.get(), host, path }; } - NodeString newString (const char* str) { return NodeString { world.get(), str }; } - - bool ask (const LilvNode* subject, const LilvNode* predicate, const LilvNode* object) const - { - return lilv_world_ask (world.get(), subject, predicate, object); - } - - OwningNode get (const LilvNode* subject, const LilvNode* predicate, const LilvNode* object) const - { - return OwningNode { lilv_world_get (world.get(), subject, predicate, object) }; - } - - OwningNodes findNodes (const LilvNode* subject, const LilvNode* predicate, const LilvNode* object) const - { - return OwningNodes { lilv_world_find_nodes (world.get(), subject, predicate, object) }; - } - - LilvWorld* get() const { return world.get(); } - -private: - struct Free - { - void operator() (LilvWorld* ptr) const noexcept { lilv_world_free (ptr); } - }; - - std::unique_ptr world; -}; - -class Ports -{ -public: - static constexpr auto sequenceSize = 8192; - - template - void forEachPort (Callback&& callback) const - { - for (const auto& port : controlPorts) - callback (port.header); - - for (const auto& port : cvPorts) - callback (port.header); - - for (const auto& port : audioPorts) - callback (port.header); - - for (const auto& port : atomPorts) - callback (port.header); - } - - auto getControlPorts() { return makeSimpleSpan (controlPorts); } - auto getControlPorts() const { return makeSimpleSpan (controlPorts); } - auto getCvPorts() { return makeSimpleSpan (cvPorts); } - auto getCvPorts() const { return makeSimpleSpan (cvPorts); } - auto getAudioPorts() { return makeSimpleSpan (audioPorts); } - auto getAudioPorts() const { return makeSimpleSpan (audioPorts); } - auto getAtomPorts() { return makeSimpleSpan (atomPorts); } - auto getAtomPorts() const { return makeSimpleSpan (atomPorts); } - - static Optional getPorts (World& world, const UsefulUris& uris, const Plugin& plugin, SymbolMap& symap) - { - Ports value; - bool successful = true; - - const auto numPorts = plugin.getNumPorts(); - const auto timeNode = world.newUri (LV2_TIME__Position); - - for (uint32_t i = 0; i != numPorts; ++i) - { - const auto port = plugin.getPortByIndex (i); - - const PortHeader header { String::fromUTF8 (port.getName().getTyped()), - String::fromUTF8 (port.getSymbol().getTyped()), - i, - port.getDirection (uris) }; - - switch (port.getKind (uris)) - { - case Port::Kind::control: - { - value.controlPorts.push_back ({ header, ParameterInfo::getInfoForPort (uris, port) }); - break; - } - - case Port::Kind::cv: - value.cvPorts.push_back ({ header }); - break; - - case Port::Kind::audio: - { - value.audioPorts.push_back ({ header }); - break; - } - - case Port::Kind::atom: - { - const auto supportsTime = port.supportsEvent (timeNode.get()); - value.atomPorts.push_back ({ header, - (size_t) Ports::sequenceSize, - symap, - supportsTime ? SupportsTime::yes : SupportsTime::no }); - break; - } - - case Port::Kind::unknown: - successful = false; - break; - } - } - - for (auto& atomPort : value.atomPorts) - { - const auto port = plugin.getPortByIndex (atomPort.header.index); - const auto minSize = port.get (uris.mLV2_RESIZE_PORT__minimumSize.get()); - - if (minSize != nullptr) - atomPort.ensureSizeInBytes ((size_t) lilv_node_as_int (minSize.get())); - } - - return successful ? makeOptional (std::move (value)) : nullopt; - } - -private: - std::vector controlPorts; - std::vector cvPorts; - std::vector audioPorts; - std::vector atomPorts; -}; - -class InstanceWithSupports final : private FeaturesDataListener, - private HandleHolder -{ -public: - InstanceWithSupports (World& world, - std::unique_ptr&& symapIn, - const Plugin& plugin, - Ports portsIn, - int32_t initialBufferSize, - double sampleRate) - : symap (std::move (symapIn)), - ports (std::move (portsIn)), - features (*this, *this, initialBufferSize, lv2_host::Ports::sequenceSize, &urids), - instance (plugin, sampleRate, features.getFeatureArray()), - workerInterface (instance.getExtensionData (world.newUri (LV2_WORKER__interface))) - { - if (instance == nullptr) - return; - - for (auto& port : ports.getControlPorts()) - instance.connectPort (port.header.index, &port.currentValue); - - for (auto& port : ports.getAtomPorts()) - instance.connectPort (port.header.index, port.data()); - - for (auto& port : ports.getCvPorts()) - instance.connectPort (port.header.index, nullptr); - - for (auto& port : ports.getAudioPorts()) - instance.connectPort (port.header.index, nullptr); - - features.registerHandle (instance.getHandle()); - } - - ~InstanceWithSupports() override - { - if (instance != nullptr) - features.deregisterHandle (instance.getHandle()); - } - - std::unique_ptr symap; - const UsefulUrids urids { *symap }; - Ports ports; - FeaturesData features; - Instance instance; - Messages uiToProcessor; - -private: - LV2_Handle handle = instance == nullptr ? nullptr : instance.getHandle(); - OptionalExtension workerInterface; - - LV2_Handle getHandle() const override { return handle; } - const LV2_Worker_Interface* getWorkerInterface() const override { return workerInterface.valid ? &workerInterface.extension : nullptr; } - - LV2_Resize_Port_Status resizeCallback (uint32_t index, size_t size) override - { - if (ports.getAtomPorts().size() <= index) - return LV2_RESIZE_PORT_ERR_UNKNOWN; - - auto& port = ports.getAtomPorts()[index]; - - if (port.header.direction != Port::Direction::output) - return LV2_RESIZE_PORT_ERR_UNKNOWN; - - port.ensureSizeInBytes (size); - instance.connectPort (port.header.index, port.data()); - - return LV2_RESIZE_PORT_SUCCESS; - } - - JUCE_DECLARE_NON_COPYABLE (InstanceWithSupports) - JUCE_DECLARE_NON_MOVEABLE (InstanceWithSupports) - JUCE_LEAK_DETECTOR (InstanceWithSupports) -}; - -struct PortState -{ - const void* data; - uint32_t size; - uint32_t kind; -}; - -class PortMap -{ -public: - explicit PortMap (Ports& ports) - { - for (auto& port : ports.getControlPorts()) - symbolToControlPortMap.emplace (port.header.symbol, &port); - } - - PortState getState (const String& symbol, const StatefulPortUrids& urids) - { - if (auto* port = getControlPortForSymbol (symbol)) - return { &port->currentValue, sizeof (float), urids.Float }; - - // At time of writing, lilv_state_new_from_instance did not attempt to store - // the state of non-control ports. Perhaps that has changed? - jassertfalse; - return { nullptr, 0, 0 }; - } - - void restoreState (const String& symbol, const StatefulPortUrids& urids, PortState ps) - { - if (auto* port = getControlPortForSymbol (symbol)) - { - port->currentValue = [&]() -> float - { - if (ps.kind == urids.Float) - return getValueFrom (ps.data, ps.size); - - if (ps.kind == urids.Double) - return getValueFrom (ps.data, ps.size); - - if (ps.kind == urids.Int) - return getValueFrom (ps.data, ps.size); - - if (ps.kind == urids.Long) - return getValueFrom (ps.data, ps.size); - - jassertfalse; - return {}; - }(); - } - else - jassertfalse; // Restoring state for non-control ports is not currently supported. - } - -private: - template - static float getValueFrom (const void* data, [[maybe_unused]] uint32_t size) - { - jassert (size == sizeof (Value)); - return (float) readUnaligned (data); - } - - ControlPort* getControlPortForSymbol (const String& symbol) const - { - const auto iter = symbolToControlPortMap.find (symbol); - return iter != symbolToControlPortMap.cend() ? iter->second : nullptr; - } - - std::map symbolToControlPortMap; - JUCE_LEAK_DETECTOR (PortMap) -}; - -class PluginState -{ -public: - PluginState() = default; - - explicit PluginState (LilvState* ptr) - : state (ptr) {} - - const LilvState* get() const noexcept { return state.get(); } - - void restore (InstanceWithSupports& instance, PortMap& portMap) const - { - if (state != nullptr) - SaveRestoreHandle { instance, portMap }.restore (state.get()); - } - - std::string toString (LilvWorld* world, LV2_URID_Map* map, LV2_URID_Unmap* unmap, const char* uri) const - { - std::unique_ptr result { lilv_state_to_string (world, - map, - unmap, - state.get(), - uri, - nullptr) }; - return std::string { result.get() }; - } - - String getLabel() const - { - return String::fromUTF8 (lilv_state_get_label (state.get())); - } - - void setLabel (const String& label) - { - lilv_state_set_label (state.get(), label.toRawUTF8()); - } - - class SaveRestoreHandle - { - public: - explicit SaveRestoreHandle (InstanceWithSupports& instanceIn, PortMap& portMap) - : instance (instanceIn.instance.get()), - features (instanceIn.features.getFeatureArray()), - urids (*instanceIn.symap), - map (portMap) - {} - - PluginState save (const LilvPlugin* plugin, LV2_URID_Map* mapFeature) - { - return PluginState { lilv_state_new_from_instance (plugin, - instance, - mapFeature, - nullptr, - nullptr, - nullptr, - nullptr, - getPortValue, - this, - LV2_STATE_IS_POD | LV2_STATE_IS_PORTABLE, - features ) }; - } - - void restore (const LilvState* stateIn) - { - lilv_state_restore (stateIn, - instance, - setPortValue, - this, - 0, - features); - } - - private: - static const void* getPortValue (const char* portSymbol, - void* userData, - uint32_t* size, - uint32_t* type) - { - auto& handle = *static_cast (userData); - - const auto state = handle.map.getState (portSymbol, handle.urids); - *size = state.size; - *type = state.kind; - return state.data; - } - - static void setPortValue (const char* portSymbol, - void* userData, - const void* value, - uint32_t size, - uint32_t type) - { - const auto& handle = *static_cast (userData); - handle.map.restoreState (portSymbol, handle.urids, { static_cast (value), size, type }); - } - - LilvInstance* instance = nullptr; - const LV2_Feature* const* features = nullptr; - const StatefulPortUrids urids; - PortMap& map; - }; - -private: - struct Free - { - void operator() (LilvState* ptr) const noexcept { lilv_state_free (ptr); } - }; - - std::unique_ptr state; - - JUCE_LEAK_DETECTOR (PluginState) -}; - -/* - Wraps an LV2 UI bundle, providing access to the descriptor (if available). -*/ -struct UiDescriptorLibrary -{ - using GetDescriptor = LV2UI_Descriptor* (*) (uint32_t); - - UiDescriptorLibrary() = default; - - explicit UiDescriptorLibrary (const String& libraryPath) - : library (std::make_unique (libraryPath)), - getDescriptor (lv2_shared::wordCast (library->getFunction ("lv2ui_descriptor"))) {} - - std::shared_ptr library; - GetDescriptor getDescriptor = nullptr; -}; - -class UiDescriptorArgs -{ -public: - String libraryPath; - String uiUri; - - auto withLibraryPath (String v) const noexcept { return with (&UiDescriptorArgs::libraryPath, v); } - auto withUiUri (String v) const noexcept { return with (&UiDescriptorArgs::uiUri, v); } - -private: - UiDescriptorArgs with (String UiDescriptorArgs::* member, String value) const noexcept - { - return juce::lv2_host::with (*this, member, std::move (value)); - } -}; - -/* - Stores a pointer to the descriptor for a specific UI bundle and UI URI. -*/ -class UiDescriptor -{ -public: - UiDescriptor() = default; - - explicit UiDescriptor (const UiDescriptorArgs& args) - : library (args.libraryPath), - descriptor (extractUiDescriptor (library, args.uiUri.toRawUTF8())) - {} - - void portEvent (LV2UI_Handle ui, - uint32_t portIndex, - uint32_t bufferSize, - uint32_t format, - const void* buffer) const - { - JUCE_ASSERT_MESSAGE_THREAD - - if (auto* lv2Descriptor = get()) - if (auto* callback = lv2Descriptor->port_event) - callback (ui, portIndex, bufferSize, format, buffer); - } - - bool hasExtensionData (World& world, const char* uid) const - { - return world.ask (world.newUri (descriptor->URI).get(), - world.newUri (LV2_CORE__extensionData).get(), - world.newUri (uid).get()); - } - - template - OptionalExtension getExtensionData (World& world, const char* uid) const - { - if (! hasExtensionData (world, uid)) - return {}; - - if (auto* lv2Descriptor = get()) - if (auto* extension = lv2Descriptor->extension_data) - return OptionalExtension (readUnaligned (extension (uid))); - - return {}; - } - - const LV2UI_Descriptor* get() const noexcept { return descriptor; } - -private: - static const LV2UI_Descriptor* extractUiDescriptor (const UiDescriptorLibrary& lib, const char* uiUri) - { - if (lib.getDescriptor == nullptr) - return nullptr; - - for (uint32_t i = 0;; ++i) - { - const auto* descriptor = lib.getDescriptor (i); - - if (descriptor == nullptr) - return nullptr; - - if (strcmp (uiUri, descriptor->URI) == 0) - return descriptor; - } - } - - UiDescriptorLibrary library; - const LV2UI_Descriptor* descriptor = nullptr; - - JUCE_LEAK_DETECTOR (UiDescriptor) -}; - -enum class Update { no, yes }; - -/* A bit like the FlaggedFloatCache used by the VST3 host/client. - - While the FlaggedFloatCache always clears all set flags during the ifSet() call, - this class stores the "value changed" flags for the processor and UI separately, - so that they can be read at different rates. -*/ -class ParameterValuesAndFlags -{ -public: - ParameterValuesAndFlags() = default; - - explicit ParameterValuesAndFlags (size_t sizeIn) - : values (sizeIn), - needsUiUpdate (sizeIn), - needsProcessorUpdate (sizeIn) - { - std::fill (values.begin(), values.end(), 0.0f); - } - - size_t size() const noexcept { return values.size(); } - - void set (size_t index, float value, Update update) - { - jassert (index < size()); - values[index].store (value, std::memory_order_relaxed); - needsUiUpdate .set (index, update == Update::yes ? 1 : 0); - needsProcessorUpdate.set (index, update == Update::yes ? 1 : 0); - } - - float get (size_t index) const noexcept - { - jassert (index < size()); - return values[index].load (std::memory_order_relaxed); - } - - template - void ifProcessorValuesChanged (Callback&& callback) - { - ifChanged (needsProcessorUpdate, std::forward (callback)); - } - - template - void ifUiValuesChanged (Callback&& callback) - { - ifChanged (needsUiUpdate, std::forward (callback)); - } - - void clearUiFlags() { needsUiUpdate.clear(); } - -private: - template - void ifChanged (FlagCache<1>& flags, Callback&& callback) - { - flags.ifSet ([this, &callback] (size_t groupIndex, uint32_t) - { - callback (groupIndex, values[groupIndex].load (std::memory_order_relaxed)); - }); - } - - std::vector> values; - FlagCache<1> needsUiUpdate; - FlagCache<1> needsProcessorUpdate; - - JUCE_LEAK_DETECTOR (ParameterValuesAndFlags) -}; - -class LV2Parameter : public AudioPluginInstance::HostedParameter -{ -public: - LV2Parameter (const String& nameIn, - const ParameterInfo& infoIn, - ParameterValuesAndFlags& floatCache) - : cache (floatCache), - info (infoIn), - range (info.min, info.max), - name (nameIn), - normalisedDefault (range.convertTo0to1 (infoIn.defaultValue)) - {} - - float getValue() const noexcept override - { - return range.convertTo0to1 (getDenormalisedValue()); - } - - void setValue (float f) override - { - cache.set ((size_t) getParameterIndex(), range.convertFrom0to1 (f), Update::yes); - } - - void setDenormalisedValue (float denormalised) - { - cache.set ((size_t) getParameterIndex(), denormalised, Update::yes); - sendValueChangedMessageToListeners (range.convertTo0to1 (denormalised)); - } - - void setDenormalisedValueWithoutTriggeringUpdate (float denormalised) - { - cache.set ((size_t) getParameterIndex(), denormalised, Update::no); - sendValueChangedMessageToListeners (range.convertTo0to1 (denormalised)); - } - - float getDenormalisedValue() const noexcept - { - return cache.get ((size_t) getParameterIndex()); - } - - float getDefaultValue() const override { return normalisedDefault; } - float getDenormalisedDefaultValue() const { return info.defaultValue; } - - float getValueForText (const String& text) const override - { - if (! info.isEnum) - return range.convertTo0to1 (text.getFloatValue()); - - const auto it = std::find_if (info.scalePoints.begin(), - info.scalePoints.end(), - [&] (const StoredScalePoint& stored) { return stored.label == text; }); - return it != info.scalePoints.end() ? range.convertTo0to1 (it->value) : normalisedDefault; - } - - int getNumSteps() const override - { - if (info.isToggle) - return 2; - - if (info.isEnum) - return static_cast (info.scalePoints.size()); - - if (info.isInteger) - return static_cast (range.getRange().getLength()) + 1; - - return AudioProcessorParameter::getNumSteps(); - } - - bool isDiscrete() const override { return info.isEnum || info.isInteger || info.isToggle; } - bool isBoolean() const override { return info.isToggle; } - - StringArray getAllValueStrings() const override - { - if (! info.isEnum) - return {}; - - return AudioProcessorParameter::getAllValueStrings(); - } - - String getText (float normalisedValue, int) const override - { - const auto denormalised = range.convertFrom0to1 (normalisedValue); - - if (info.isEnum && ! info.scalePoints.empty()) - { - // The normalised value might not correspond to the exact value of a scale point. - // In this case, we find the closest label by searching the midpoints of the scale - // point values. - const auto index = std::distance (midPoints.begin(), - std::lower_bound (midPoints.begin(), midPoints.end(), denormalised)); - jassert (isPositiveAndBelow (index, info.scalePoints.size())); - return info.scalePoints[(size_t) index].label; - } - - return getFallbackParameterString (denormalised); - } - - String getParameterID() const override - { - return info.identifier; - } - - String getName (int maxLength) const override - { - return name.substring (0, maxLength); - } - - String getLabel() const override - { - // TODO - return {}; - } - -private: - String getFallbackParameterString (float denormalised) const - { - if (info.isToggle) - return denormalised > 0.0f ? "On" : "Off"; - - if (info.isInteger) - return String { static_cast (denormalised) }; - - return String { denormalised }; - } - - static std::vector findScalePointMidPoints (const SafeSortedSet& set) - { - if (set.size() < 2) - return {}; - - std::vector result; - result.reserve (set.size() - 1); - - for (auto it = std::next (set.begin()); it != set.end(); ++it) - result.push_back ((std::prev (it)->value + it->value) * 0.5f); - - jassert (std::is_sorted (result.begin(), result.end())); - jassert (result.size() + 1 == set.size()); - return result; - } - - ParameterValuesAndFlags& cache; - const ParameterInfo info; - const std::vector midPoints = findScalePointMidPoints (info.scalePoints); - const NormalisableRange range; - const String name; - const float normalisedDefault; - - JUCE_LEAK_DETECTOR (LV2Parameter) -}; - -class UiInstanceArgs -{ -public: - File bundlePath; - URL pluginUri; - - auto withBundlePath (File v) const noexcept { return withMember (*this, &UiInstanceArgs::bundlePath, std::move (v)); } - auto withPluginUri (URL v) const noexcept { return withMember (*this, &UiInstanceArgs::pluginUri, std::move (v)); } -}; - -struct TouchListener -{ - virtual ~TouchListener() = default; - virtual void controlGrabbed (uint32_t port, bool grabbed) = 0; -}; - -class UiFeaturesDataOptions -{ -public: - float initialScaleFactor = 0.0f, sampleRate = 0.0f; - - auto withInitialScaleFactor (float v) const { return with (&UiFeaturesDataOptions::initialScaleFactor, v); } - auto withSampleRate (float v) const { return with (&UiFeaturesDataOptions::sampleRate, v); } - -private: - UiFeaturesDataOptions with (float UiFeaturesDataOptions::* member, float value) const - { - return juce::lv2_host::with (*this, member, value); - } -}; - -class UiFeaturesData -{ -public: - UiFeaturesData (PhysicalResizeListener& rl, - TouchListener& tl, - LV2_Handle instanceIn, - LV2UI_Widget parentIn, - Instance::GetExtensionData getExtensionData, - const Ports& ports, - SymbolMap& symapIn, - const UiFeaturesDataOptions& optIn) - : opts (optIn), - resizeListener (rl), - touchListener (tl), - instance (instanceIn), - parent (parentIn), - symap (symapIn), - dataAccess { getExtensionData }, - portIndices (makePortIndices (ports)) - { - } - - const LV2_Feature* const* getFeatureArray() const noexcept { return features.pointers.data(); } - - static std::vector getFeatureUris() - { - return Features::getUris (makeFeatures ({}, {}, {}, {}, {}, {}, {}, {}, {}, {})); - } - - Rectangle getLastRequestedBounds() const { return { lastRequestedWidth, lastRequestedHeight }; } - -private: - static std::vector makeFeatures (LV2UI_Resize* resize, - LV2UI_Widget parent, - LV2_Handle handle, - LV2_Extension_Data_Feature* data, - LV2_URID_Map* map, - LV2_URID_Unmap* unmap, - LV2UI_Port_Map* portMap, - LV2UI_Touch* touch, - LV2_Options_Option* options, - LV2_Log_Log* log) - { - return { LV2_Feature { LV2_UI__resize, resize }, - LV2_Feature { LV2_UI__parent, parent }, - LV2_Feature { LV2_UI__idleInterface, nullptr }, - LV2_Feature { LV2_INSTANCE_ACCESS_URI, handle }, - LV2_Feature { LV2_DATA_ACCESS_URI, data }, - LV2_Feature { LV2_URID__map, map }, - LV2_Feature { LV2_URID__unmap, unmap}, - LV2_Feature { LV2_UI__portMap, portMap }, - LV2_Feature { LV2_UI__touch, touch }, - LV2_Feature { LV2_OPTIONS__options, options }, - LV2_Feature { LV2_LOG__log, log } }; - } - - int resizeCallback (int width, int height) - { - lastRequestedWidth = width; - lastRequestedHeight = height; - resizeListener.viewRequestedResizeInPhysicalPixels (width, height); - return 0; - } - - static int resizeCallback (LV2UI_Feature_Handle handle, int width, int height) - { - return static_cast (handle)->resizeCallback (width, height); - } - - uint32_t portIndexCallback (const char* symbol) const - { - const auto it = portIndices.find (symbol); - return it != portIndices.cend() ? it->second : LV2UI_INVALID_PORT_INDEX; - } - - static uint32_t portIndexCallback (LV2UI_Feature_Handle handle, const char* symbol) - { - return static_cast (handle)->portIndexCallback (symbol); - } - - void touchCallback (uint32_t portIndex, bool grabbed) const - { - touchListener.controlGrabbed (portIndex, grabbed); - } - - static void touchCallback (LV2UI_Feature_Handle handle, uint32_t index, bool b) - { - return static_cast (handle)->touchCallback (index, b); - } - - static std::map makePortIndices (const Ports& ports) - { - std::map result; - - ports.forEachPort ([&] (const PortHeader& header) - { - [[maybe_unused]] const auto emplaced = result.emplace (header.symbol, header.index); - - // This will complain if there are duplicate port symbols. - jassert (emplaced.second); - }); - - return result; - } - - const UiFeaturesDataOptions opts; - PhysicalResizeListener& resizeListener; - TouchListener& touchListener; - LV2_Handle instance{}; - LV2UI_Widget parent{}; - SymbolMap& symap; - const UsefulUrids urids { symap }; - Log log { &urids }; - int lastRequestedWidth = 0, lastRequestedHeight = 0; - std::vector options { { LV2_OPTIONS_INSTANCE, - 0, - symap.map (LV2_UI__scaleFactor), - sizeof (float), - symap.map (LV2_ATOM__Float), - &opts.initialScaleFactor }, - { LV2_OPTIONS_INSTANCE, - 0, - symap.map (LV2_PARAMETERS__sampleRate), - sizeof (float), - symap.map (LV2_ATOM__Float), - &opts.sampleRate }, - { LV2_OPTIONS_INSTANCE, 0, 0, 0, 0, nullptr } }; // The final entry must be nulled out - LV2UI_Resize resize { this, resizeCallback }; - LV2_URID_Map map = symap.getMapFeature(); - LV2_URID_Unmap unmap = symap.getUnmapFeature(); - LV2UI_Port_Map portMap { this, portIndexCallback }; - LV2UI_Touch touch { this, touchCallback }; - LV2_Extension_Data_Feature dataAccess; - std::map portIndices; - Features features { makeFeatures (&resize, - parent, - instance, - &dataAccess, - &map, - &unmap, - &portMap, - &touch, - options.data(), - log.getLogFeature()) }; - - JUCE_LEAK_DETECTOR (UiFeaturesData) -}; - -struct RequiredFeatures -{ - explicit RequiredFeatures (OwningNodes nodes) - : values (std::move (nodes)) {} - - OwningNodes values; -}; - -struct OptionalFeatures -{ - explicit OptionalFeatures (OwningNodes nodes) - : values (std::move (nodes)) {} - - OwningNodes values; -}; - -template -static bool noneOf (Range&& range, Predicate&& pred) -{ - // Not a mistake, this is for ADL - using std::begin; - using std::end; - return std::none_of (begin (range), end (range), std::forward (pred)); -} - -class Uis -{ -public: - explicit Uis (const LilvPlugin* plugin) noexcept : uis (lilv_plugin_get_uis (plugin)) {} - - unsigned size() const noexcept { return lilv_uis_size (uis.get()); } - - UisIterator begin() const noexcept { return UisIterator { uis.get() }; } - UisIterator end() const noexcept { return UisIterator{}; } - - const LilvUI* getByUri (const NodeUri& uri) const - { - return lilv_uis_get_by_uri (uis.get(), uri.get()); - } - -private: - struct Free - { - void operator() (LilvUIs* ptr) const noexcept { lilv_uis_free (ptr); } - }; - - std::unique_ptr uis; -}; - -//============================================================================== -class PluginClass -{ -public: - explicit PluginClass (const LilvPluginClass* c) : pluginClass (c) {} - - NodeUri getParentUri() const noexcept { return NodeUri::copy (lilv_plugin_class_get_parent_uri (pluginClass)); } - NodeUri getUri() const noexcept { return NodeUri::copy (lilv_plugin_class_get_uri (pluginClass)); } - NodeString getLabel() const noexcept { return NodeString::copy (lilv_plugin_class_get_label (pluginClass)); } - OwningPluginClasses getChildren() const noexcept - { - return OwningPluginClasses { OwningPluginClasses::type { lilv_plugin_class_get_children (pluginClass) } }; - } - -private: - const LilvPluginClass* pluginClass = nullptr; -}; - -using FloatWriter = void (*) (LV2_Atom_Forge*, float); - -struct ParameterWriterUrids -{ - LV2_URID mLV2_PATCH__Set; - LV2_URID mLV2_PATCH__property; - LV2_URID mLV2_PATCH__value; - LV2_URID mLV2_ATOM__eventTransfer; -}; - -struct MessageHeaderAndSize -{ - MessageHeader header; - uint32_t size; -}; - -class ParameterWriter -{ -public: - ParameterWriter (ControlPort* p) - : data (PortBacking { p }), kind (Kind::port) {} - - ParameterWriter (FloatWriter write, LV2_URID urid, uint32_t controlPortIndex) - : data (PatchBacking { write, urid, controlPortIndex }), kind (Kind::patch) {} - - void writeToProcessor (const ParameterWriterUrids urids, LV2_Atom_Forge* forge, float value) const - { - switch (kind) - { - case Kind::patch: - { - if (forge != nullptr) - { - lv2_atom_forge_frame_time (forge, 0); - writeSetToForge (urids, *forge, value); - } - - break; - } - - case Kind::port: - data.port.port->currentValue = value; - break; - } - } - - MessageHeaderAndSize writeToUi (const ParameterWriterUrids urids, LV2_Atom_Forge& forge, float value) const - { - const auto getWrittenBytes = [&]() -> uint32_t - { - if (const auto* atom = convertToAtomPtr (forge.buf, forge.size)) - return (uint32_t) (atom->size + sizeof (LV2_Atom)); - - jassertfalse; - return 0; - }; - - switch (kind) - { - case Kind::patch: - writeSetToForge (urids, forge, value); - return { { data.patch.controlPortIndex, urids.mLV2_ATOM__eventTransfer }, getWrittenBytes() }; - - case Kind::port: - lv2_atom_forge_raw (&forge, &value, sizeof (value)); - return { { data.port.port->header.index, 0 }, sizeof (value) }; - } - - return { { 0, 0 }, 0 }; - } - - const LV2_URID* getUrid() const - { - return kind == Kind::patch ? &data.patch.urid : nullptr; - } - - const uint32_t* getPortIndex() const - { - return kind == Kind::port ? &data.port.port->header.index : nullptr; - } - -private: - void writeSetToForge (const ParameterWriterUrids urids, LV2_Atom_Forge& forge, float value) const - { - lv2_shared::ObjectFrame object { &forge, (uint32_t) 0, urids.mLV2_PATCH__Set }; - - lv2_atom_forge_key (&forge, urids.mLV2_PATCH__property); - lv2_atom_forge_urid (&forge, data.patch.urid); - - lv2_atom_forge_key (&forge, urids.mLV2_PATCH__value); - data.patch.write (&forge, value); - } - - struct PortBacking - { - ControlPort* port; - }; - - struct PatchBacking - { - FloatWriter write; - LV2_URID urid; - uint32_t controlPortIndex; - }; - - union Data - { - static_assert (std::is_trivial_v, "PortBacking must be trivial"); - static_assert (std::is_trivial_v, "PatchBacking must be trivial"); - - explicit Data (PortBacking p) : port (p) {} - explicit Data (PatchBacking p) : patch (p) {} - - PortBacking port; - PatchBacking patch; - }; - - enum class Kind { port, patch }; - - Data data; - Kind kind; - - JUCE_LEAK_DETECTOR (ParameterWriter) -}; - -static String lilvNodeToUriString (const LilvNode* node) -{ - return node != nullptr ? String::fromUTF8 (lilv_node_as_uri (node)) : String{}; -} - -static String lilvNodeToString (const LilvNode* node) -{ - return node != nullptr ? String::fromUTF8 (lilv_node_as_string (node)) : String{}; -} - -/* This holds all of the discovered groups in the plugin's manifest, and allows us to - add parameters to these groups as we discover them. - - Once all the parameters have been added with addParameter(), you can call - getTree() to convert this class' contents (which are optimised for fast lookup - and modification) into a plain old AudioProcessorParameterGroup. -*/ -class IntermediateParameterTree -{ -public: - explicit IntermediateParameterTree (World& worldIn) - : world (worldIn) - { - const auto groups = getGroups (world); - const auto symbolNode = world.newUri (LV2_CORE__symbol); - const auto nameNode = world.newUri (LV2_CORE__name); - - for (const auto& group : groups) - { - const auto symbol = lilvNodeToString (world.get (group.get(), symbolNode.get(), nullptr).get()); - const auto name = lilvNodeToString (world.get (group.get(), nameNode .get(), nullptr).get()); - owning.emplace (lilvNodeToUriString (group.get()), - std::make_unique (symbol, name, "|")); - } - } - - void addParameter (StringRef group, std::unique_ptr param) - { - if (param == nullptr) - return; - - const auto it = owning.find (group); - (it != owning.cend() ? *it->second : topLevel).addChild (std::move (param)); - } - - static AudioProcessorParameterGroup getTree (IntermediateParameterTree tree) - { - std::map nonowning; - - for (const auto& pair : tree.owning) - nonowning.emplace (pair.first, pair.second.get()); - - const auto groups = getGroups (tree.world); - const auto subgroupNode = tree.world.newUri (LV2_PORT_GROUPS__subGroupOf); - - for (const auto& group : groups) - { - const auto innerIt = tree.owning.find (lilvNodeToUriString (group.get())); - - if (innerIt == tree.owning.cend()) - continue; - - const auto outer = lilvNodeToUriString (tree.world.get (group.get(), subgroupNode.get(), nullptr).get()); - const auto outerIt = nonowning.find (outer); - - if (outerIt != nonowning.cend() && containsParameters (outerIt->second)) - outerIt->second->addChild (std::move (innerIt->second)); - } - - for (auto& subgroup : tree.owning) - if (containsParameters (subgroup.second.get())) - tree.topLevel.addChild (std::move (subgroup.second)); - - return std::move (tree.topLevel); - } - -private: - static std::vector getGroups (World& world) - { - std::vector names; - - for (auto* uri : { LV2_PORT_GROUPS__Group, LV2_PORT_GROUPS__InputGroup, LV2_PORT_GROUPS__OutputGroup }) - for (const auto* group : world.findNodes (nullptr, world.newUri (LILV_NS_RDF "type").get(), world.newUri (uri).get())) - names.push_back (OwningNode { lilv_node_duplicate (group) }); - - return names; - } - - static bool containsParameters (const AudioProcessorParameterGroup* g) - { - if (g == nullptr) - return false; - - for (auto* node : *g) - { - if (node->getParameter() != nullptr) - return true; - - if (auto* group = node->getGroup()) - if (containsParameters (group)) - return true; - } - - return false; - } - - World& world; - AudioProcessorParameterGroup topLevel; - std::map> owning; - - JUCE_LEAK_DETECTOR (IntermediateParameterTree) -}; - -struct BypassParameter final : public LV2Parameter -{ - BypassParameter (const ParameterInfo& parameterInfo, ParameterValuesAndFlags& cacheIn) - : LV2Parameter ("Bypass", parameterInfo, cacheIn) {} - - float getValue() const noexcept override - { - return LV2Parameter::getValue() > 0.0f ? 0.0f : 1.0f; - } - - void setValue (float newValue) override - { - LV2Parameter::setValue (newValue > 0.0f ? 0.0f : 1.0f); - } - - float getDefaultValue() const override { return 0.0f; } - bool isAutomatable() const override { return true; } - bool isDiscrete() const override { return true; } - bool isBoolean() const override { return true; } - int getNumSteps() const override { return 2; } - StringArray getAllValueStrings() const override { return { TRANS ("Off"), TRANS ("On") }; } -}; - -struct ParameterData -{ - ParameterInfo info; - ParameterWriter writer; - String group; - String name; -}; - -template -static auto getPortPointers (SimpleSpan range) -{ - using std::begin; - std::vector result; - - for (auto& port : range) - { - result.resize (std::max ((size_t) (port.header.index + 1), result.size()), nullptr); - result[port.header.index] = &port; - } - - return result; -} - -static std::unique_ptr makeParameter (const uint32_t* enabledPortIndex, - const ParameterData& data, - ParameterValuesAndFlags& cache) -{ - // The bypass parameter is a bit special, in that JUCE expects the parameter to be a bypass - // (where 0 is active, 1 is inactive), but the LV2 version is called "enabled" and has - // different semantics (0 is inactive, 1 is active). - // To work around this, we wrap the LV2 parameter in a special inverting JUCE parameter. - - if (enabledPortIndex != nullptr) - if (auto* index = data.writer.getPortIndex()) - if (*index == *enabledPortIndex) - return std::make_unique (data.info, cache); - - return std::make_unique (data.name, data.info, cache); -} - -class ControlPortAccelerationStructure -{ -public: - ControlPortAccelerationStructure (SimpleSpan controlPorts) - : indexedControlPorts (getPortPointers (controlPorts)) - { - for (const auto& port : controlPorts) - if (port.header.direction == Port::Direction::output) - outputPorts.push_back (&port); - } - - const std::vector& getIndexedControlPorts() { return indexedControlPorts; } - - ControlPort* getControlPortByIndex (uint32_t index) const - { - if (isPositiveAndBelow (index, indexedControlPorts.size())) - return indexedControlPorts[index]; - - return nullptr; - } - - void writeOutputPorts (UiEventListener* target, MessageBufferInterface& uiMessages) const - { - if (target == nullptr) - return; - - for (const auto* port : outputPorts) - { - const auto chars = toChars (port->currentValue); - uiMessages.pushMessage ({ target, { port->header.index, 0 } }, (uint32_t) chars.size(), chars.data()); - } - } - -private: - std::vector indexedControlPorts; - std::vector outputPorts; -}; - -class ParameterValueCache -{ -public: - /* This takes some information about all the parameters that this plugin wants to expose, - then builds and installs the actual parameters. - */ - ParameterValueCache (AudioPluginInstance& processor, - World& world, - LV2_URID_Map mapFeature, - const std::vector& data, - ControlPort* enabledPort) - : uiForge (mapFeature), - cache (data.size()) - { - // Parameter indices are unknown until we add the parameters to the processor. - // This map lets us keep track of which ParameterWriter corresponds to each parameter. - // After the parameters have been added to the processor, we'll convert this - // to a simple vector that stores each ParameterWriter at the same index - // as the corresponding parameter. - std::map writerForParameter; - - IntermediateParameterTree tree { world }; - - const auto* enabledPortIndex = enabledPort != nullptr ? &enabledPort->header.index - : nullptr; - - for (const auto& item : data) - { - auto param = makeParameter (enabledPortIndex, item, cache); - - if (auto* urid = item.writer.getUrid()) - urids.emplace (*urid, param.get()); - - if (auto* index = item.writer.getPortIndex()) - portIndices.emplace (*index, param.get()); - - writerForParameter.emplace (param.get(), item.writer); - - tree.addParameter (item.group, std::move (param)); - } - - processor.setHostedParameterTree (IntermediateParameterTree::getTree (std::move (tree))); - - // Build the vector of writers - writers.reserve (data.size()); - - for (auto* param : processor.getParameters()) - { - const auto it = writerForParameter.find (param); - jassert (it != writerForParameter.end()); - writers.push_back (it->second); // The writer must exist at the same index as the parameter! - } - - // Duplicate port indices or urids? - jassert (processor.getParameters().size() == (int) (urids.size() + portIndices.size())); - - // Set parameters to default values - const auto setToDefault = [] (auto& container) - { - for (auto& item : container) - item.second->setDenormalisedValueWithoutTriggeringUpdate (item.second->getDenormalisedDefaultValue()); - }; - - setToDefault (urids); - setToDefault (portIndices); - } - - void postChangedParametersToProcessor (const ParameterWriterUrids helperUrids, - LV2_Atom_Forge* forge) - { - cache.ifProcessorValuesChanged ([&] (size_t index, float value) - { - writers[index].writeToProcessor (helperUrids, forge, value); - }); - } - - void postChangedParametersToUi (UiEventListener* target, - const ParameterWriterUrids helperUrids, - MessageBufferInterface& uiMessages) - { - if (target == nullptr) - return; - - cache.ifUiValuesChanged ([&] (size_t index, float value) - { - writeParameterToUi (target, writers[index], value, helperUrids, uiMessages); - }); - } - - void postAllParametersToUi (UiEventListener* target, - const ParameterWriterUrids helperUrids, - MessageBufferInterface& uiMessages) - { - if (target == nullptr) - return; - - const auto numWriters = writers.size(); - - for (size_t i = 0; i < numWriters; ++i) - writeParameterToUi (target, writers[i], cache.get (i), helperUrids, uiMessages); - - cache.clearUiFlags(); - } - - LV2Parameter* getParamByUrid (LV2_URID urid) const - { - const auto it = urids.find (urid); - return it != urids.end() ? it->second : nullptr; - } - - LV2Parameter* getParamByPortIndex (uint32_t portIndex) const - { - const auto it = portIndices.find (portIndex); - return it != portIndices.end() ? it->second : nullptr; - } - - void updateFromControlPorts (const ControlPortAccelerationStructure& ports) const - { - for (const auto& pair : portIndices) - if (auto* port = ports.getControlPortByIndex (pair.first)) - if (auto* param = pair.second) - param->setDenormalisedValueWithoutTriggeringUpdate (port->currentValue); - } - -private: - void writeParameterToUi (UiEventListener* target, - const ParameterWriter& writer, - float value, - const ParameterWriterUrids helperUrids, - MessageBufferInterface& uiMessages) - { - JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED - - uiForge.setBuffer (forgeStorage.data(), forgeStorage.size()); - const auto messageHeader = writer.writeToUi (helperUrids, *uiForge.get(), value); - uiMessages.pushMessage ({ target, messageHeader.header }, messageHeader.size, forgeStorage.data()); - } - - SingleSizeAlignedStorage<8> forgeStorage { 256 }; - lv2_shared::AtomForge uiForge; - - ParameterValuesAndFlags cache; - std::vector writers; - std::map urids; - std::map portIndices; - - JUCE_LEAK_DETECTOR (ParameterValueCache) -}; - -struct PatchSetCallback -{ - explicit PatchSetCallback (ParameterValueCache& x) : cache (x) {} - - // If we receive a patch set from the processor, we can assume that the UI will - // put itself into the correct state when it receives the message. - void setParameter (LV2_URID property, float value) const noexcept - { - if (auto* param = cache.getParamByUrid (property)) - param->setDenormalisedValueWithoutTriggeringUpdate (value); - } - - // TODO gesture support will probably go here, once it's part of the LV2 spec - - ParameterValueCache& cache; -}; - -struct SupportedParameter -{ - ParameterInfo info; - bool supported; - LV2_URID type; -}; - -static SupportedParameter getInfoForPatchParameter (World& worldIn, - const UsefulUrids& urids, - const NodeUri& property) -{ - const auto rangeUri = worldIn.newUri (LILV_NS_RDFS "range"); - const auto type = worldIn.get (property.get(), rangeUri.get(), nullptr); - - if (type == nullptr) - return { {}, false, {} }; - - const auto typeUrid = urids.symap.map (lilv_node_as_uri (type.get())); - - const LV2_URID types[] { urids.mLV2_ATOM__Int, - urids.mLV2_ATOM__Long, - urids.mLV2_ATOM__Float, - urids.mLV2_ATOM__Double, - urids.mLV2_ATOM__Bool }; - - if (std::find (std::begin (types), std::end (types), typeUrid) == std::end (types)) - return { {}, false, {} }; - - const auto getValue = [&] (const char* uri, float fallback) - { - return Port::getFloatValue (worldIn.get (property.get(), worldIn.newUri (uri).get(), nullptr).get(), fallback); - }; - - const auto hasPortProperty = [&] (const char* uri) - { - return worldIn.ask (property.get(), - worldIn.newUri (LV2_CORE__portProperty).get(), - worldIn.newUri (uri).get()); - }; - - const auto metadataScalePoints = worldIn.findNodes (property.get(), - worldIn.newUri (LV2_CORE__scalePoint).get(), - nullptr); - SafeSortedSet parsedScalePoints; - - for (const auto* scalePoint : metadataScalePoints) - { - const auto label = worldIn.get (scalePoint, worldIn.newUri (LILV_NS_RDFS "label").get(), nullptr); - const auto value = worldIn.get (scalePoint, worldIn.newUri (LILV_NS_RDF "value").get(), nullptr); - - if (label != nullptr && value != nullptr) - parsedScalePoints.insert ({ lilv_node_as_string (label.get()), lilv_node_as_float (value.get()) }); - else - jassertfalse; // A ScalePoint must have both a rdfs:label and a rdf:value - } - - const auto minimum = getValue (LV2_CORE__minimum, 0.0f); - const auto maximum = getValue (LV2_CORE__maximum, 1.0f); - - return { { std::move (parsedScalePoints), - "des:" + String::fromUTF8 (property.getTyped()), - getValue (LV2_CORE__default, (minimum + maximum) * 0.5f), - minimum, - maximum, - typeUrid == urids.mLV2_ATOM__Bool || hasPortProperty (LV2_CORE__toggled), - typeUrid == urids.mLV2_ATOM__Int || typeUrid == urids.mLV2_ATOM__Long, - hasPortProperty (LV2_CORE__enumeration) }, - true, - typeUrid }; -} - -static std::vector getPortBasedParameters (World& world, - const Plugin& plugin, - std::initializer_list hiddenPorts, - SimpleSpan controlPorts) -{ - std::vector result; - - const auto groupNode = world.newUri (LV2_PORT_GROUPS__group); - - for (auto& port : controlPorts) - { - if (port.header.direction != Port::Direction::input) - continue; - - if (std::find (std::begin (hiddenPorts), std::end (hiddenPorts), &port) != std::end (hiddenPorts)) - continue; - - const auto lilvPort = plugin.getPortByIndex (port.header.index); - const auto group = lilvNodeToUriString (lilvPort.get (groupNode.get()).get()); - - result.push_back ({ port.info, ParameterWriter { &port }, group, port.header.name }); - } - - return result; -} - -static void writeFloatToForge (LV2_Atom_Forge* forge, float value) { lv2_atom_forge_float (forge, value); } -static void writeDoubleToForge (LV2_Atom_Forge* forge, float value) { lv2_atom_forge_double (forge, (double) value); } -static void writeIntToForge (LV2_Atom_Forge* forge, float value) { lv2_atom_forge_int (forge, (int32_t) value); } -static void writeLongToForge (LV2_Atom_Forge* forge, float value) { lv2_atom_forge_long (forge, (int64_t) value); } -static void writeBoolToForge (LV2_Atom_Forge* forge, float value) { lv2_atom_forge_bool (forge, value > 0.5f); } - -static std::vector getPatchBasedParameters (World& world, - const Plugin& plugin, - const UsefulUrids& urids, - uint32_t controlPortIndex) -{ - // This returns our writable parameters in an indeterminate order. - // We want our parameters to be in a consistent order between runs, so - // we'll create all the parameters in one pass, sort them, and then - // add them in a separate pass. - const auto writableControls = world.findNodes (plugin.getUri().get(), - world.newUri (LV2_PATCH__writable).get(), - nullptr); - - struct DataAndUri - { - ParameterData data; - String uri; - }; - - std::vector resultWithUris; - - const auto groupNode = world.newUri (LV2_PORT_GROUPS__group); - - for (auto* ctrl : writableControls) - { - const auto labelString = [&] - { - if (auto label = world.get (ctrl, world.newUri (LILV_NS_RDFS "label").get(), nullptr)) - return String::fromUTF8 (lilv_node_as_string (label.get())); - - return String(); - }(); - - const auto uri = String::fromUTF8 (lilv_node_as_uri (ctrl)); - const auto info = getInfoForPatchParameter (world, urids, world.newUri (uri.toRawUTF8())); - - if (! info.supported) - continue; - - const auto write = [&] - { - if (info.type == urids.mLV2_ATOM__Int) - return writeIntToForge; - - if (info.type == urids.mLV2_ATOM__Long) - return writeLongToForge; - - if (info.type == urids.mLV2_ATOM__Double) - return writeDoubleToForge; - - if (info.type == urids.mLV2_ATOM__Bool) - return writeBoolToForge; - - return writeFloatToForge; - }(); - - const auto group = lilvNodeToUriString (world.get (ctrl, groupNode.get(), nullptr).get()); - resultWithUris.push_back ({ { info.info, - ParameterWriter { write, urids.symap.map (uri.toRawUTF8()), controlPortIndex }, - group, - labelString }, - uri }); - } - - const auto compareUris = [] (const DataAndUri& a, const DataAndUri& b) { return a.uri < b.uri; }; - std::sort (resultWithUris.begin(), resultWithUris.end(), compareUris); - - std::vector result; - - for (const auto& item : resultWithUris) - result.push_back (item.data); - - return result; -} - -static std::vector getJuceParameterInfo (World& world, - const Plugin& plugin, - const UsefulUrids& urids, - std::initializer_list hiddenPorts, - SimpleSpan controlPorts, - uint32_t controlPortIndex) -{ - auto port = getPortBasedParameters (world, plugin, hiddenPorts, controlPorts); - auto patch = getPatchBasedParameters (world, plugin, urids, controlPortIndex); - - port.insert (port.end(), patch.begin(), patch.end()); - return port; -} - -// Rather than sprinkle #ifdef everywhere, risking the wrath of the entire C++ -// standards committee, we put all of our conditionally-compiled stuff into a -// specialised template that compiles away to nothing when editor support is -// not available. -#if JUCE_MAC || JUCE_WINDOWS || JUCE_LINUX || JUCE_BSD - constexpr auto editorFunctionalityEnabled = true; -#else - constexpr auto editorFunctionalityEnabled = false; -#endif - -//============================================================================== -class LV2AudioPluginInstanceHeadless : public AudioPluginInstance -{ -public: - LV2AudioPluginInstanceHeadless (std::shared_ptr worldIn, - const Plugin& pluginIn, - const UsefulUris& uris, - std::unique_ptr&& in, - PluginDescription&& desc, - std::vector knownPresetUris, - PluginState stateToApply, - String, - UiDescriptor) - : LV2AudioPluginInstanceHeadless (worldIn, - pluginIn, - std::move (in), - std::move (desc), - std::move (knownPresetUris), - std::move (stateToApply), - getParsedBuses (*worldIn, pluginIn, uris)) - { - } - - void fillInPluginDescription (PluginDescription& d) const override { d = description; } - - const String getName() const override { return description.name; } - - void prepareToPlay (double sampleRate, int numSamples) override - { - // In REAPER, changing the sample rate will deactivate the plugin, - // save its state, destroy it, create a new instance, restore the - // state, and then activate the new instance. - // We'll do the same, because there's no way to retroactively change the - // plugin sample rate. - // This is a bit expensive, so try to avoid changing the sample rate too - // frequently. - - // In addition to the above, we also need to destroy the custom view, - // and recreate it after creating the new plugin instance. - // Ideally this should all happen in the same Component. - - deactivate(); - - MemoryBlock mb; - getStateInformation (mb); - - instance = std::make_unique (*world, - std::move (instance->symap), - plugin, - std::move (instance->ports), - numSamples, - sampleRate); - - // prepareToPlay is *guaranteed* not to be called concurrently with processBlock - setStateInformationImpl (mb.getData(), (int) mb.getSize(), ConcurrentWithAudioCallback::no); - - jassert (numSamples == instance->features.getMaxBlockSize()); - activate(); - } - - void releaseResources() override { deactivate(); } - - using AudioPluginInstance::processBlock; - using AudioPluginInstance::processBlockBypassed; - - void processBlock (AudioBuffer& audio, MidiBuffer& midi) override - { - processBlockImpl (audio, midi); - } - - void processBlockBypassed (AudioBuffer& audio, MidiBuffer& midi) override - { - if (bypassParam != nullptr) - processBlockImpl (audio, midi); - else - AudioPluginInstance::processBlockBypassed (audio, midi); - } - - double getTailLengthSeconds() const override { return {}; } // TODO - - bool acceptsMidi() const override - { - if (instance == nullptr) - return false; - - auto ports = instance->ports.getAtomPorts(); - - return std::any_of (ports.begin(), ports.end(), [&] (const AtomPort& a) - { - if (a.header.direction != Port::Direction::input) - return false; - - return portAtIndexSupportsMidi (a.header.index); - }); - } - - bool producesMidi() const override - { - if (instance == nullptr) - return false; - - auto ports = instance->ports.getAtomPorts(); - - return std::any_of (ports.begin(), ports.end(), [&] (const AtomPort& a) - { - if (a.header.direction != Port::Direction::output) - return false; - - return portAtIndexSupportsMidi (a.header.index); - }); - } - - AudioProcessorEditor* createEditor() override { return nullptr; } - bool hasEditor() const override { return false; } - - int getNumPrograms() override { return (int) presetUris.size(); } - - int getCurrentProgram() override - { - return lastAppliedPreset; - } - - void setCurrentProgram (int newProgram) override - { - JUCE_ASSERT_MESSAGE_THREAD; - - if (! isPositiveAndBelow (newProgram, presetUris.size())) - return; - - lastAppliedPreset = newProgram; - applyStateWithAppropriateLocking (loadStateWithUri (presetUris[(size_t) newProgram]), - ConcurrentWithAudioCallback::yes); - } - - const String getProgramName (int program) override - { - JUCE_ASSERT_MESSAGE_THREAD; - - if (isPositiveAndBelow (program, presetUris.size())) - return loadStateWithUri (presetUris[(size_t) program]).getLabel(); - - return {}; - } - - void changeProgramName (int program, const String& label) override - { - JUCE_ASSERT_MESSAGE_THREAD; - - if (isPositiveAndBelow (program, presetUris.size())) - loadStateWithUri (presetUris[(size_t) program]).setLabel (label); - } - - void getStateInformation (MemoryBlock& block) override - { - JUCE_ASSERT_MESSAGE_THREAD; - - // TODO where should the state URI come from? - PortMap portStateManager (instance->ports); - const auto stateUri = String::fromUTF8 (instance->instance.getUri()) + "/savedState"; - auto mapFeature = instance->symap->getMapFeature(); - auto unmapFeature = instance->symap->getUnmapFeature(); - const auto state = PluginState::SaveRestoreHandle (*instance, portStateManager).save (plugin.get(), &mapFeature); - const auto string = state.toString (world->get(), &mapFeature, &unmapFeature, stateUri.toRawUTF8()); - block.replaceAll (string.data(), string.size()); - } - - void setStateInformation (const void* data, int size) override - { - setStateInformationImpl (data, size, ConcurrentWithAudioCallback::yes); - } - - void setNonRealtime (bool newValue) noexcept override - { - JUCE_ASSERT_MESSAGE_THREAD; - - AudioPluginInstance::setNonRealtime (newValue); - instance->features.setNonRealtime (newValue); - } - - bool isBusesLayoutSupported (const BusesLayout& layout) const override - { - for (const auto& pair : { std::make_tuple (&layout.inputBuses, &declaredBusLayout.inputs), - std::make_tuple (&layout.outputBuses, &declaredBusLayout.outputs) }) - { - const auto& requested = *std::get<0> (pair); - const auto& allowed = *std::get<1> (pair); - - if ((size_t) requested.size() != allowed.size()) - return false; - - for (size_t busIndex = 0; busIndex < allowed.size(); ++busIndex) - { - const auto& requestedBus = requested[(int) busIndex]; - const auto& allowedBus = allowed[busIndex]; - - if (! allowedBus.isCompatible (requestedBus)) - return false; - } - } - - return true; - } - - void processorLayoutsChanged() override { ioMap = lv2_shared::PortToAudioBufferMap { getBusesLayout(), declaredBusLayout }; } - - AudioProcessorParameter* getBypassParameter() const override { return bypassParam; } - - void postAllParametersToUi (ProcessorToUi& channel, UiEventListener* uiEventListener) - { - parameterValues.postAllParametersToUi (uiEventListener, getParameterWriterUrids(), channel); - controlPortStructure.writeOutputPorts (uiEventListener, channel); - } - - void postChangedParametersToUi (ProcessorToUi& channel, UiEventListener* uiEventListener) - { - parameterValues.postChangedParametersToUi (uiEventListener, getParameterWriterUrids(), channel); - controlPortStructure.writeOutputPorts (uiEventListener, channel); - } - - /* The current instance will be *recreated* every time the sample rate changes! */ - InstanceWithSupports* getCurrentInstance() const - { - return instance.get(); - } - - AudioProcessorParameter* getParamByPortIndex (uint32_t portIndex) const - { - return parameterValues.getParamByPortIndex (portIndex); - } - -private: - enum class ConcurrentWithAudioCallback { no, yes }; - - virtual void sendOutgoingPortMessageToUi (UiMessageHeader, uint32_t, const void*) {} - - LV2AudioPluginInstanceHeadless (std::shared_ptr worldIn, - const Plugin& pluginIn, - std::unique_ptr&& in, - PluginDescription&& desc, - std::vector knownPresetUris, - PluginState stateToApply, - const lv2_shared::ParsedBuses& parsedBuses) - : AudioPluginInstance (getBusesProperties (parsedBuses, *worldIn)), - declaredBusLayout (parsedBuses), - world (std::move (worldIn)), - plugin (pluginIn), - description (std::move (desc)), - presetUris (std::move (knownPresetUris)), - instance (std::move (in)) - { - applyStateWithAppropriateLocking (std::move (stateToApply), ConcurrentWithAudioCallback::no); - } - - void setStateInformationImpl (const void* data, int size, ConcurrentWithAudioCallback concurrent) - { - JUCE_ASSERT_MESSAGE_THREAD; - - if (data == nullptr || size == 0) - return; - - auto begin = static_cast (data); - std::vector copy (begin, begin + size); - copy.push_back (0); - auto mapFeature = instance->symap->getMapFeature(); - applyStateWithAppropriateLocking (PluginState { lilv_state_new_from_string (world->get(), &mapFeature, copy.data()) }, - concurrent); - } - - void activate() - { - if (! active) - instance->instance.activate(); - - active = true; - } - - void deactivate() - { - if (active) - instance->instance.deactivate(); - - active = false; - } - - void processBlockImpl (AudioBuffer& audio, MidiBuffer& midi) - { - preparePortsForRun (audio, midi); - - instance->instance.run (static_cast (audio.getNumSamples())); - instance->features.processResponses(); - - processPortsAfterRun (midi); - } - - bool portAtIndexSupportsMidi (uint32_t index) const noexcept - { - const auto port = plugin.getPortByIndex (index); - - if (! port.isValid()) - return false; - - return port.supportsEvent (world->newUri (LV2_MIDI__MidiEvent).get()); - } - - ParameterWriterUrids getParameterWriterUrids() const - { - return { instance->urids.mLV2_PATCH__Set, - instance->urids.mLV2_PATCH__property, - instance->urids.mLV2_PATCH__value, - instance->urids.mLV2_ATOM__eventTransfer }; - } - - void applyStateWithAppropriateLocking (PluginState&& state, ConcurrentWithAudioCallback concurrent) - { - PortMap portStateManager (instance->ports); - - // If a plugin supports threadSafeRestore, its restore method is thread-safe - // and may be called concurrently with audio class functions. - if (hasThreadSafeRestore || concurrent == ConcurrentWithAudioCallback::no) - { - state.restore (*instance, portStateManager); - } - else - { - const ScopedLock lock (getCallbackLock()); - state.restore (*instance, portStateManager); - } - - parameterValues.updateFromControlPorts (controlPortStructure); - } - - PluginState loadStateWithUri (const String& str) - { - auto mapFeature = instance->symap->getMapFeature(); - const auto presetUri = world->newUri (str.toRawUTF8()); - lilv_world_load_resource (world->get(), presetUri.get()); - return PluginState { lilv_state_new_from_world (world->get(), &mapFeature, presetUri.get()) }; - } - - void connectPorts (AudioBuffer& audio) - { - // Plugins that cannot process in-place will require the feature "inPlaceBroken". - // We don't support that feature, so if we made it to this point we can assume that - // in-place processing works. - for (const auto& port : instance->ports.getAudioPorts()) - { - const auto channel = ioMap.getChannelForPort (port.header.index); - auto* ptr = isPositiveAndBelow (channel, audio.getNumChannels()) ? audio.getWritePointer (channel) - : nullptr; - instance->instance.connectPort (port.header.index, ptr); - } - - for (const auto& port : instance->ports.getCvPorts()) - instance->instance.connectPort (port.header.index, nullptr); - - for (auto& port : instance->ports.getAtomPorts()) - instance->instance.connectPort (port.header.index, port.data()); - } - - void writeTimeInfoToPort (AtomPort& port) - { - if (port.header.direction != Port::Direction::input || ! port.getSupportsTime()) - return; - - auto* forge = port.getForge().get(); - auto* playhead = getPlayHead(); - - if (playhead == nullptr) - return; - - // Write timing info to the control port - const auto info = playhead->getPosition(); - - if (! info.hasValue()) - return; - - const auto& urids = instance->urids; - - lv2_atom_forge_frame_time (forge, 0); - - lv2_shared::ObjectFrame object { forge, (uint32_t) 0, urids.mLV2_TIME__Position }; - - lv2_atom_forge_key (forge, urids.mLV2_TIME__speed); - lv2_atom_forge_float (forge, info->getIsPlaying() ? 1.0f : 0.0f); - - if (const auto samples = info->getTimeInSamples()) - { - lv2_atom_forge_key (forge, urids.mLV2_TIME__frame); - lv2_atom_forge_long (forge, *samples); - } - - if (const auto bar = info->getBarCount()) - { - lv2_atom_forge_key (forge, urids.mLV2_TIME__bar); - lv2_atom_forge_long (forge, *bar); - } - - if (const auto beat = info->getPpqPosition()) - { - if (const auto barStart = info->getPpqPositionOfLastBarStart()) - { - lv2_atom_forge_key (forge, urids.mLV2_TIME__barBeat); - lv2_atom_forge_float (forge, (float) (*beat - *barStart)); - } - - lv2_atom_forge_key (forge, urids.mLV2_TIME__beat); - lv2_atom_forge_double (forge, *beat); - } - - if (const auto sig = info->getTimeSignature()) - { - lv2_atom_forge_key (forge, urids.mLV2_TIME__beatUnit); - lv2_atom_forge_int (forge, sig->denominator); - - lv2_atom_forge_key (forge, urids.mLV2_TIME__beatsPerBar); - lv2_atom_forge_float (forge, (float) sig->numerator); - } - - if (const auto bpm = info->getBpm()) - { - lv2_atom_forge_key (forge, urids.mLV2_TIME__beatsPerMinute); - lv2_atom_forge_float (forge, (float) *bpm); - } - } - - void preparePortsForRun (AudioBuffer& audio, MidiBuffer& midiBuffer) - { - connectPorts (audio); - - for (auto& port : instance->ports.getAtomPorts()) - { - switch (port.header.direction) - { - case Port::Direction::input: - port.beginSequence(); - break; - - case Port::Direction::output: - port.replaceWithChunk(); - break; - - case Port::Direction::unknown: - jassertfalse; - break; - } - } - - for (auto& port : instance->ports.getAtomPorts()) - writeTimeInfoToPort (port); - - const auto controlPortForge = controlPort != nullptr ? controlPort->getForge().get() - : nullptr; - - parameterValues.postChangedParametersToProcessor (getParameterWriterUrids(), controlPortForge); - - instance->uiToProcessor.readAllAndClear ([this] (MessageHeader header, uint32_t size, const void* buffer) - { - pushMessage (header, size, buffer); - }); - - for (auto& port : instance->ports.getAtomPorts()) - { - if (port.header.direction == Port::Direction::input) - { - for (const auto meta : midiBuffer) - { - port.addEventToSequence (meta.samplePosition, - instance->urids.mLV2_MIDI__MidiEvent, - static_cast (meta.numBytes), - meta.data); - } - - port.endSequence(); - } - } - - if (freeWheelingPort != nullptr) - freeWheelingPort->currentValue = isNonRealtime() ? freeWheelingPort->info.max - : freeWheelingPort->info.min; - } - - void pushMessage (MessageHeader header, [[maybe_unused]] uint32_t size, const void* data) - { - if (header.protocol == 0 || header.protocol == instance->urids.mLV2_UI__floatProtocol) - { - const auto value = readUnaligned (data); - - if (auto* param = parameterValues.getParamByPortIndex (header.portIndex)) - { - param->setDenormalisedValue (value); - } - else if (auto* port = controlPortStructure.getControlPortByIndex (header.portIndex)) - { - // No parameter corresponds to this port, write to the port directly - port->currentValue = value; - } - } - else if (auto* atomPort = header.portIndex < atomPorts.size() ? atomPorts[header.portIndex] : nullptr) - { - if (header.protocol == instance->urids.mLV2_ATOM__eventTransfer) - { - if (const auto* atom = convertToAtomPtr (data, (size_t) size)) - { - atomPort->addAtomToSequence (0, atom); - - // Not UB; LV2_Atom_Object has LV2_Atom as its first member - if (atom->type == instance->urids.mLV2_ATOM__Object) - patchSetHelper.processPatchSet (reinterpret_cast (data), PatchSetCallback { parameterValues }); - } - } - else if (header.protocol == instance->urids.mLV2_ATOM__atomTransfer) - { - if (const auto* atom = convertToAtomPtr (data, (size_t) size)) - atomPort->replaceBufferWithAtom (atom); - } - } - } - - void processPortsAfterRun (MidiBuffer& midi) - { - midi.clear(); - - for (auto& port : instance->ports.getAtomPorts()) - processAtomPort (port, midi); - - if (latencyPort != nullptr) - setLatencySamples ((int) latencyPort->currentValue); - } - - void processAtomPort (const AtomPort& port, MidiBuffer& midi) - { - if (port.header.direction != Port::Direction::output) - return; - - // The port holds an Atom, by definition - const auto* atom = reinterpret_cast (port.data()); - - if (atom->type != instance->urids.mLV2_ATOM__Sequence) - return; - - // The Atom said that it was of sequence type, so this isn't UB - const auto* sequence = reinterpret_cast (port.data()); - - // http://lv2plug.in/ns/ext/atom#Sequence - run() stamps are always audio frames - jassert (sequence->body.unit == 0 || sequence->body.unit == instance->urids.mLV2_UNITS__frame); - - for (const auto* event : lv2_shared::SequenceIterator { lv2_shared::SequenceWithSize { sequence } }) - { - sendOutgoingPortMessageToUi ({ nullptr, { port.header.index, instance->urids.mLV2_ATOM__eventTransfer } }, - (uint32_t) (event->body.size + sizeof (LV2_Atom)), - &event->body); - - if (event->body.type == instance->urids.mLV2_MIDI__MidiEvent) - midi.addEvent (event + 1, static_cast (event->body.size), static_cast (event->time.frames)); - - if (lv2_atom_forge_is_object_type (port.getForge().get(), event->body.type)) - if (reinterpret_cast (event + 1)->otype == instance->urids.mLV2_STATE__StateChanged) - updateHostDisplay (ChangeDetails{}.withNonParameterStateChanged (true)); - - patchSetHelper.processPatchSet (event, PatchSetCallback { parameterValues }); - } - } - - // Check for duplicate channel designations, and convert the set to a discrete channel layout - // if any designations are duplicated. - static std::set validateAndRedesignatePorts (std::set info) - { - const auto channelSet = lv2_shared::ParsedGroup::getEquivalentSet (info); - - if ((int) info.size() == channelSet.size()) - return info; - - std::set result; - auto designation = (int) AudioChannelSet::discreteChannel0; - - for (auto& item : info) - { - auto copy = item; - copy.designation = (AudioChannelSet::ChannelType) designation++; - result.insert (copy); - } - - return result; - } - - static AudioChannelSet::ChannelType getPortDesignation (World& world, const Port& port, size_t indexInGroup) - { - const auto defaultResult = (AudioChannelSet::ChannelType) (AudioChannelSet::discreteChannel0 + indexInGroup); - const auto node = port.get (world.newUri (LV2_CORE__designation).get()); - - if (node == nullptr) - return defaultResult; - - const auto it = lv2_shared::channelDesignationMap.find (lilvNodeToUriString (node.get())); - - if (it == lv2_shared::channelDesignationMap.end()) - return defaultResult; - - return it->second; - } - - static lv2_shared::ParsedBuses getParsedBuses (World& world, const Plugin& p, const UsefulUris& uris) - { - const auto groupPropertyUri = world.newUri (LV2_PORT_GROUPS__group); - const auto optionalUri = world.newUri (LV2_CORE__connectionOptional); - - std::map> inputGroups, outputGroups; - std::set ungroupedInputs, ungroupedOutputs; - - for (uint32_t i = 0, numPorts = p.getNumPorts(); i < numPorts; ++i) - { - const auto port = p.getPortByIndex (i); - - if (port.getKind (uris) != Port::Kind::audio) - continue; - - const auto groupUri = lilvNodeToUriString (port.get (groupPropertyUri.get()).get()); - - auto& set = [&]() -> auto& - { - if (groupUri.isEmpty()) - return port.getDirection (uris) == Port::Direction::input ? ungroupedInputs : ungroupedOutputs; - - auto& group = port.getDirection (uris) == Port::Direction::input ? inputGroups : outputGroups; - return group[groupUri]; - }(); - - set.insert ({ port.getIndex(), getPortDesignation (world, port, set.size()), port.hasProperty (optionalUri) }); - } - - for (auto* groups : { &inputGroups, &outputGroups }) - for (auto& pair : *groups) - pair.second = validateAndRedesignatePorts (std::move (pair.second)); - - JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4702) - const auto getMainGroupName = [&] (const char* propertyName) - { - for (const auto* item : p.getValue (world.newUri (propertyName).get())) - return lilvNodeToUriString (item); - - return String{}; - }; - JUCE_END_IGNORE_WARNINGS_MSVC - - return { findStableBusOrder (getMainGroupName (LV2_PORT_GROUPS__mainInput), inputGroups, ungroupedInputs), - findStableBusOrder (getMainGroupName (LV2_PORT_GROUPS__mainOutput), outputGroups, ungroupedOutputs) }; - } - - static String getNameForUri (World& world, StringRef uri) - { - if (uri.isEmpty()) - return String(); - - const auto node = world.get (world.newUri (uri).get(), - world.newUri (LV2_CORE__name).get(), - nullptr); - - if (node == nullptr) - return String(); - - return String::fromUTF8 (lilv_node_as_string (node.get())); - } - - static BusesProperties getBusesProperties (const lv2_shared::ParsedBuses& parsedBuses, World& world) - { - BusesProperties result; - - for (const auto& pair : { std::make_tuple (&parsedBuses.inputs, &result.inputLayouts), - std::make_tuple (&parsedBuses.outputs, &result.outputLayouts) }) - { - const auto& buses = *std::get<0> (pair); - auto& layout = *std::get<1> (pair); - - for (const auto& bus : buses) - { - layout.add (AudioProcessor::BusProperties { getNameForUri (world, bus.uid), - bus.getEquivalentSet(), - bus.isRequired() }); - } - } - - return result; - } - - LV2_URID map (const char* str) const - { - return instance != nullptr ? instance->symap->map (str) - : LV2_URID(); - } - - ControlPort* findControlPortWithIndex (uint32_t index) const - { - auto ports = instance->ports.getControlPorts(); - const auto indexMatches = [&] (const ControlPort& p) { return p.header.index == index; }; - const auto it = std::find_if (ports.begin(), ports.end(), indexMatches); - - return it != ports.end() ? &(*it) : nullptr; - } - - const lv2_shared::ParsedBuses declaredBusLayout; - lv2_shared::PortToAudioBufferMap ioMap { getBusesLayout(), declaredBusLayout }; - std::shared_ptr world; - Plugin plugin; - PluginDescription description; - std::vector presetUris; - std::unique_ptr instance; - - std::vector atomPorts = getPortPointers (instance->ports.getAtomPorts()); - - AtomPort* const controlPort = [&]() -> AtomPort* - { - const auto port = plugin.getPortByDesignation (world->newUri (LV2_CORE__InputPort).get(), - world->newUri (LV2_CORE__control).get()); - - if (! port.isValid()) - return nullptr; - - const auto index = port.getIndex(); - - if (! isPositiveAndBelow (index, atomPorts.size())) - return nullptr; - - return atomPorts[index]; - }(); - - ControlPort* const latencyPort = [&]() -> ControlPort* - { - if (! plugin.hasLatency()) - return nullptr; - - return findControlPortWithIndex (plugin.getLatencyPortIndex()); - }(); - - ControlPort* const freeWheelingPort = [&]() -> ControlPort* - { - const auto port = plugin.getPortByDesignation (world->newUri (LV2_CORE__InputPort).get(), - world->newUri (LV2_CORE__freeWheeling).get()); - - if (! port.isValid()) - return nullptr; - - return findControlPortWithIndex (port.getIndex()); - }(); - - ControlPort* const enabledPort = [&]() -> ControlPort* - { - const auto port = plugin.getPortByDesignation (world->newUri (LV2_CORE__InputPort).get(), - world->newUri (LV2_CORE_PREFIX "enabled").get()); - - if (! port.isValid()) - return nullptr; - - return findControlPortWithIndex (port.getIndex()); - }(); - - lv2_shared::PatchSetHelper patchSetHelper { instance->symap->getMapFeature(), plugin.getUri().getTyped() }; - ControlPortAccelerationStructure controlPortStructure { instance->ports.getControlPorts() }; - ParameterValueCache parameterValues { *this, - *world, - instance->symap->getMapFeature(), - getJuceParameterInfo (*world, - plugin, - instance->urids, - { latencyPort, freeWheelingPort }, - instance->ports.getControlPorts(), - controlPort != nullptr ? controlPort->header.index : 0), - enabledPort }; - LV2Parameter* bypassParam = enabledPort != nullptr ? parameterValues.getParamByPortIndex (enabledPort->header.index) - : nullptr; - - int lastAppliedPreset = 0; - bool hasThreadSafeRestore = plugin.hasExtensionData (world->newUri (LV2_STATE__threadSafeRestore)); - bool active { false }; - - JUCE_LEAK_DETECTOR (LV2AudioPluginInstanceHeadless) -}; - -} // namespace lv2_host - -//============================================================================== -class LV2PluginFormat::Pimpl -{ -public: - Pimpl() - { - loadAllPluginsFromPaths (getDefaultLocationsToSearch()); - - const auto tempFile = lv2ResourceFolder.getFile(); - - if (tempFile.createDirectory()) - { - for (const auto& bundle : lv2::Bundle::getAllBundles()) - { - const auto pathToBundle = tempFile.getChildFile (bundle.name + String (".lv2")); - - if (! pathToBundle.createDirectory()) - continue; - - for (const auto& resource : bundle.contents) - pathToBundle.getChildFile (resource.name).replaceWithText (resource.contents); - - const auto pathString = File::addTrailingSeparator (pathToBundle.getFullPathName()); - world->loadBundle (world->newFileUri (nullptr, pathString.toRawUTF8())); - } - } - } - - ~Pimpl() - { - lv2ResourceFolder.getFile().deleteRecursively(); - } - - void findAllTypesForFile (OwnedArray& result, - const String& identifier) - { - if (File::isAbsolutePath (identifier)) - world->loadBundle (world->newFileUri (nullptr, File::addTrailingSeparator (identifier).toRawUTF8())); - - for (const auto& plugin : { findPluginByUri (identifier), findPluginByFile (identifier) }) - { - if (auto desc = getDescription (plugin); desc.fileOrIdentifier.isNotEmpty()) - { - result.add (std::make_unique (desc)); - break; - } - } - } - - bool fileMightContainThisPluginType (const String& file) const - { - // If the string looks like a URI, then it could be a valid LV2 identifier - const auto* data = file.toRawUTF8(); - const auto numBytes = file.getNumBytesAsUTF8(); - std::vector vec (numBytes + 1, 0); - std::copy (data, data + numBytes, vec.begin()); - return serd_uri_string_has_scheme (vec.data()) || file.endsWith (".lv2"); - } - - String getNameOfPluginFromIdentifier (const String& identifier) - { - // We would have to actually load the bundle to get its name, - // and the bundle may contain multiple plugins - return identifier; - } - - bool pluginNeedsRescanning (const PluginDescription&) - { - return true; - } - - bool doesPluginStillExist (const PluginDescription& description) - { - return findPluginByUri (description.fileOrIdentifier) != nullptr; - } - - StringArray searchPathsForPlugins (const FileSearchPath& paths, bool, bool) - { - loadAllPluginsFromPaths (paths); - - StringArray result; - - for (const auto* plugin : world->getAllPlugins()) - result.add (lv2_host::Plugin { plugin }.getUri().getTyped()); - - return result; - } - - FileSearchPath getDefaultLocationsToSearch() - { - #if JUCE_MAC - return { "~/Library/Audio/Plug-Ins/LV2;" - "~/.lv2;" - "/usr/local/lib/lv2;" - "/usr/lib/lv2;" - "/Library/Audio/Plug-Ins/LV2;" }; - #elif JUCE_WINDOWS - return { "%APPDATA%\\LV2;" - "%COMMONPROGRAMFILES%\\LV2" }; - #else - #if JUCE_64BIT - if (File ("/usr/lib64/lv2").exists() || File ("/usr/local/lib64/lv2").exists()) - return { "~/.lv2;" - "/usr/lib64/lv2;" - "/usr/local/lib64/lv2" }; - #endif - - return { "~/.lv2;" - "/usr/lib/lv2;" - "/usr/local/lib/lv2" }; - #endif - } - - const LilvUI* findEmbeddableUi (const lv2_host::Uis* pluginUis, std::true_type) - { - if (pluginUis == nullptr) - return nullptr; - - const std::vector allUis (pluginUis->begin(), pluginUis->end()); - - if (allUis.empty()) - return nullptr; - - constexpr const char* rawUri = - #if JUCE_MAC - LV2_UI__CocoaUI; - #elif JUCE_WINDOWS - LV2_UI__WindowsUI; - #elif JUCE_LINUX || JUCE_BSD - LV2_UI__X11UI; - #else - nullptr; - #endif - - jassert (rawUri != nullptr); - const auto nativeUiUri = world->newUri (rawUri); - - struct UiWithSuitability - { - const LilvUI* ui; - unsigned suitability; - - bool operator< (const UiWithSuitability& other) const noexcept - { - return suitability < other.suitability; - } - - static unsigned uiIsSupported (const char* hostUri, const char* pluginUri) - { - if (strcmp (hostUri, pluginUri) == 0) - return 1; - - return 0; - } - }; - - std::vector uisWithSuitability; - uisWithSuitability.reserve (allUis.size()); - - std::transform (allUis.cbegin(), allUis.cend(), std::back_inserter (uisWithSuitability), [&] (const LilvUI* ui) - { - const LilvNode* type = nullptr; - return UiWithSuitability { ui, lilv_ui_is_supported (ui, UiWithSuitability::uiIsSupported, nativeUiUri.get(), &type) }; - }); - - std::sort (uisWithSuitability.begin(), uisWithSuitability.end()); - - if (uisWithSuitability.back().suitability != 0) - return uisWithSuitability.back().ui; - - return nullptr; - } - - const LilvUI* findEmbeddableUi (const lv2_host::Uis*, std::false_type) - { - return nullptr; - } - - const LilvUI* findEmbeddableUi (const lv2_host::Uis* pluginUis) - { - return findEmbeddableUi (pluginUis, std::integral_constant{}); - } - - static lv2_host::UiDescriptor getUiDescriptor (const LilvUI* ui) - { - if (ui == nullptr) - return {}; - - const auto libraryFile = StringPtr { lilv_file_uri_parse (lilv_node_as_uri (lilv_ui_get_binary_uri (ui)), nullptr) }; - - return lv2_host::UiDescriptor { lv2_host::UiDescriptorArgs{}.withLibraryPath (libraryFile.get()) - .withUiUri (lilv_node_as_uri (lilv_ui_get_uri (ui))) }; - } - - // Returns the name of a missing feature, if any. - template - static std::vector findMissingFeatures (RequiredFeatures&& required, - AvailableFeatures&& available) - { - std::vector result; - - for (const auto* node : required) - { - const auto nodeString = String::fromUTF8 (lilv_node_as_uri (node)); - - if (std::find (std::begin (available), std::end (available), nodeString) == std::end (available)) - result.push_back (nodeString); - } - - return result; - } - - template - static void createPluginInstance (LV2PluginFormat& format, - const PluginDescription& desc, - double initialSampleRate, - int initialBufferSize, - PluginCreationCallback callback) - { - const auto* pluginPtr = format.pimpl->findPluginByUri (desc.fileOrIdentifier); - - if (pluginPtr == nullptr) - return callback (nullptr, "Unable to locate plugin with the requested URI"); - - const lv2_host::Plugin plugin { pluginPtr }; - - auto symap = std::make_unique(); - - const auto missingFeatures = findMissingFeatures (plugin.getRequiredFeatures(), - lv2_host::FeaturesData::getFeatureUris()); - - if (! missingFeatures.empty()) - { - const auto missingFeaturesString = StringArray (missingFeatures.data(), (int) missingFeatures.size()).joinIntoString (", "); - - return callback (nullptr, "plugin requires missing features: " + missingFeaturesString); - } - - auto world = format.pimpl->world; - auto& uris = format.pimpl->uris; - - auto stateToApply = [&] - { - if (! plugin.hasFeature (world->newUri (LV2_STATE__loadDefaultState))) - return lv2_host::PluginState{}; - - auto map = symap->getMapFeature(); - return lv2_host::PluginState { lilv_state_new_from_world (world->get(), &map, plugin.getUri().get()) }; - }(); - - auto ports = lv2_host::Ports::getPorts (*world, uris, plugin, *symap); - - if (! ports.hasValue()) - return callback (nullptr, "Plugin has ports of an unsupported type"); - - auto instance = std::make_unique (*world, - std::move (symap), - plugin, - std::move (*ports), - (int32_t) initialBufferSize, - initialSampleRate); - - if (instance->instance == nullptr) - return callback (nullptr, "Plugin was located, but could not be opened"); - - auto potentialPresets = world->findNodes (nullptr, - world->newUri (LV2_CORE__appliesTo).get(), - plugin.getUri().get()); - - const lv2_host::Uis pluginUis { plugin.get() }; - - const auto uiToUse = [&]() -> const LilvUI* - { - const auto bestMatch = format.pimpl->findEmbeddableUi (&pluginUis); - - if (bestMatch == nullptr) - return bestMatch; - - const auto uiUri = lilv_ui_get_uri (bestMatch); - lilv_world_load_resource (world->get(), uiUri); - - const auto queryUi = [&] (const char* featureUri) - { - const auto featureUriNode = world->newUri (featureUri); - return world->findNodes (uiUri, featureUriNode.get(), nullptr); - }; - - const auto missingUiFeatures = findMissingFeatures (queryUi (LV2_CORE__requiredFeature), - lv2_host::UiFeaturesData::getFeatureUris()); - - return missingUiFeatures.empty() ? bestMatch : nullptr; - }(); - - auto uiBundleUri = uiToUse != nullptr ? String::fromUTF8 (lilv_node_as_uri (lilv_ui_get_bundle_uri (uiToUse))) - : String(); - - auto wrapped = std::make_unique (world, - plugin, - uris, - std::move (instance), - format.pimpl->getDescription (pluginPtr), - format.pimpl->findPresetUrisForPlugin (plugin.get()), - std::move (stateToApply), - std::move (uiBundleUri), - getUiDescriptor (uiToUse)); - callback (std::move (wrapped), {}); - } - -private: - void loadAllPluginsFromPaths (const FileSearchPath& path) - { - const auto joined = path.toStringWithSeparator (LILV_PATH_SEP); - world->loadAllFromPaths (world->newString (joined.toRawUTF8())); - } - - struct Free { void operator() (char* ptr) const noexcept { free (ptr); } }; - using StringPtr = std::unique_ptr; - - const LilvPlugin* findPluginByUri (const String& s) - { - return world->getAllPlugins().getByUri (world->newUri (s.toRawUTF8())); - } - - const LilvPlugin* findPluginByFile (const File& f) - { - return world->getAllPlugins().getByFile (f); - } - - template - void visitParentClasses (const LilvPluginClass* c, Fn&& fn) const - { - if (c == nullptr) - return; - - const lv2_host::PluginClass wrapped { c }; - fn (wrapped); - - const auto parentUri = wrapped.getParentUri(); - - if (parentUri.get() != nullptr) - visitParentClasses (world->getPluginClasses().getByUri (parentUri), fn); - } - - std::vector collectPluginClassUris (const LilvPluginClass* c) const - { - std::vector results; - - visitParentClasses (c, [&results] (const lv2_host::PluginClass& wrapped) - { - results.emplace_back (wrapped.getUri()); - }); - - return results; - } - - PluginDescription getDescription (const LilvPlugin* plugin) - { - if (plugin == nullptr) - return {}; - - const auto wrapped = lv2_host::Plugin { plugin }; - const auto bundle = wrapped.getBundleUri().getTyped(); - const auto bundleFile = File { StringPtr { lilv_file_uri_parse (bundle, nullptr) }.get() }; - - const auto numInputs = wrapped.getNumPortsOfClass (uris.mLV2_CORE__AudioPort, uris.mLV2_CORE__InputPort); - const auto numOutputs = wrapped.getNumPortsOfClass (uris.mLV2_CORE__AudioPort, uris.mLV2_CORE__OutputPort); - - PluginDescription result; - result.name = wrapped.getName().getTyped(); - result.descriptiveName = wrapped.getName().getTyped(); - result.lastFileModTime = bundleFile.getLastModificationTime(); - result.lastInfoUpdateTime = Time::getCurrentTime(); - result.manufacturerName = wrapped.getAuthorName().getTyped(); - result.pluginFormatName = LV2PluginFormat::getFormatName(); - result.numInputChannels = static_cast (numInputs); - result.numOutputChannels = static_cast (numOutputs); - - const auto classPtr = wrapped.getClass(); - const auto classes = collectPluginClassUris (classPtr); - const auto isInstrument = std::any_of (classes.cbegin(), - classes.cend(), - [this] (const lv2_host::NodeUri& uri) - { - return uri.equals (uris.mLV2_CORE__GeneratorPlugin); - }); - - result.category = lv2_host::PluginClass { classPtr }.getLabel().getTyped(); - result.isInstrument = isInstrument; - - // The plugin URI is required to be globally unique, so a hash of it should be too - result.fileOrIdentifier = wrapped.getUri().getTyped(); - - const auto uid = DefaultHashFunctions::generateHash (result.fileOrIdentifier, std::numeric_limits::max());; - result.deprecatedUid = result.uniqueId = uid; - return result; - } - - std::vector findPresetUrisForPlugin (const LilvPlugin* plugin) - { - std::vector presetUris; - - lv2_host::OwningNodes potentialPresets { lilv_plugin_get_related (plugin, world->newUri (LV2_PRESETS__Preset).get()) }; - - for (const auto* potentialPreset : potentialPresets) - presetUris.push_back (lilv_node_as_string (potentialPreset)); - - return presetUris; - } - - TemporaryFile lv2ResourceFolder; - std::shared_ptr world = std::make_shared(); - lv2_host::UsefulUris uris { world->get() }; -}; - -namespace lv2_host -{ - /* Creates and holds a UI instance for a plugin with a specific URI, using the provided descriptor. */ @@ -5659,57 +971,6 @@ private: } // namespace lv2_host -LV2PluginFormat::LV2PluginFormat() - : pimpl (std::make_unique()) {} - -LV2PluginFormat::~LV2PluginFormat() = default; - -void LV2PluginFormat::findAllTypesForFile (OwnedArray& results, - const String& fileOrIdentifier) -{ - pimpl->findAllTypesForFile (results, fileOrIdentifier); -} - -bool LV2PluginFormat::fileMightContainThisPluginType (const String& fileOrIdentifier) -{ - return pimpl->fileMightContainThisPluginType (fileOrIdentifier); -} - -String LV2PluginFormat::getNameOfPluginFromIdentifier (const String& fileOrIdentifier) -{ - return pimpl->getNameOfPluginFromIdentifier (fileOrIdentifier); -} - -bool LV2PluginFormat::pluginNeedsRescanning (const PluginDescription& desc) -{ - return pimpl->pluginNeedsRescanning (desc); -} - -bool LV2PluginFormat::doesPluginStillExist (const PluginDescription& desc) -{ - return pimpl->doesPluginStillExist (desc); -} - -bool LV2PluginFormat::canScanForPlugins() const { return true; } -bool LV2PluginFormat::isTrivialToScan() const { return true; } - -StringArray LV2PluginFormat::searchPathsForPlugins (const FileSearchPath& directoriesToSearch, - bool recursive, - bool allowAsync) -{ - return pimpl->searchPathsForPlugins (directoriesToSearch, recursive, allowAsync); -} - -FileSearchPath LV2PluginFormat::getDefaultLocationsToSearch() -{ - return pimpl->getDefaultLocationsToSearch(); -} - -bool LV2PluginFormat::requiresUnblockedMessageThreadDuringCreation (const PluginDescription&) const -{ - return false; -} - void LV2PluginFormat::createPluginInstance (const PluginDescription& desc, double sampleRate, int bufferSize, diff --git a/modules/juce_audio_processors/format_types/juce_LV2PluginFormat.h b/modules/juce_audio_processors/format_types/juce_LV2PluginFormat.h index 49fff2ad12..44ef7a78a5 100644 --- a/modules/juce_audio_processors/format_types/juce_LV2PluginFormat.h +++ b/modules/juce_audio_processors/format_types/juce_LV2PluginFormat.h @@ -42,44 +42,9 @@ namespace juce @tags{Audio} */ -class JUCE_API LV2PluginFormat : public AudioPluginFormat +class JUCE_API LV2PluginFormat : public LV2PluginFormatHeadless { -public: - LV2PluginFormat(); - ~LV2PluginFormat() override; - - static String getFormatName() { return "LV2"; } - String getName() const override { return getFormatName(); } - - void findAllTypesForFile (OwnedArray& results, - const String& fileOrIdentifier) override; - - bool fileMightContainThisPluginType (const String& fileOrIdentifier) override; - - String getNameOfPluginFromIdentifier (const String& fileOrIdentifier) override; - - bool pluginNeedsRescanning (const PluginDescription&) override; - - bool doesPluginStillExist (const PluginDescription&) override; - - bool canScanForPlugins() const override; - - bool isTrivialToScan() const override; - - StringArray searchPathsForPlugins (const FileSearchPath& directoriesToSearch, - bool recursive, - bool allowPluginsWhichRequireAsynchronousInstantiation = false) override; - - FileSearchPath getDefaultLocationsToSearch() override; - -private: - bool requiresUnblockedMessageThreadDuringCreation (const PluginDescription&) const override; void createPluginInstance (const PluginDescription&, double, int, PluginCreationCallback) override; - - class Pimpl; - std::unique_ptr pimpl; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LV2PluginFormat) }; #endif diff --git a/modules/juce_audio_processors/format_types/juce_LV2PluginFormat_test.cpp b/modules/juce_audio_processors/format_types/juce_LV2PluginFormat_test.cpp index 363656498e..9caaf81823 100644 --- a/modules/juce_audio_processors/format_types/juce_LV2PluginFormat_test.cpp +++ b/modules/juce_audio_processors/format_types/juce_LV2PluginFormat_test.cpp @@ -32,7 +32,7 @@ ============================================================================== */ -#include "juce_LV2Common.h" +#include namespace juce { diff --git a/modules/juce_audio_processors/format_types/juce_LV2Common.h b/modules/juce_audio_processors_headless/format_types/juce_LV2Common.h similarity index 100% rename from modules/juce_audio_processors/format_types/juce_LV2Common.h rename to modules/juce_audio_processors_headless/format_types/juce_LV2Common.h diff --git a/modules/juce_audio_processors_headless/format_types/juce_LV2PluginFormatHeadless.cpp b/modules/juce_audio_processors_headless/format_types/juce_LV2PluginFormatHeadless.cpp new file mode 100644 index 0000000000..d7e8354cc3 --- /dev/null +++ b/modules/juce_audio_processors_headless/format_types/juce_LV2PluginFormatHeadless.cpp @@ -0,0 +1,104 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +#if JUCE_PLUGINHOST_LV2 && (! (JUCE_ANDROID || JUCE_IOS)) + +#include + +namespace juce +{ + +//============================================================================== +LV2PluginFormatHeadless::LV2PluginFormatHeadless() + : pimpl (std::make_unique()) {} + +LV2PluginFormatHeadless::~LV2PluginFormatHeadless() = default; + +void LV2PluginFormatHeadless::findAllTypesForFile (OwnedArray& results, + const String& fileOrIdentifier) +{ + pimpl->findAllTypesForFile (results, fileOrIdentifier); +} + +bool LV2PluginFormatHeadless::fileMightContainThisPluginType (const String& fileOrIdentifier) +{ + return pimpl->fileMightContainThisPluginType (fileOrIdentifier); +} + +String LV2PluginFormatHeadless::getNameOfPluginFromIdentifier (const String& fileOrIdentifier) +{ + return pimpl->getNameOfPluginFromIdentifier (fileOrIdentifier); +} + +bool LV2PluginFormatHeadless::pluginNeedsRescanning (const PluginDescription& desc) +{ + return pimpl->pluginNeedsRescanning (desc); +} + +bool LV2PluginFormatHeadless::doesPluginStillExist (const PluginDescription& desc) +{ + return pimpl->doesPluginStillExist (desc); +} + +bool LV2PluginFormatHeadless::canScanForPlugins() const { return true; } +bool LV2PluginFormatHeadless::isTrivialToScan() const { return true; } + +StringArray LV2PluginFormatHeadless::searchPathsForPlugins (const FileSearchPath& directoriesToSearch, + bool recursive, + bool allowAsync) +{ + return pimpl->searchPathsForPlugins (directoriesToSearch, recursive, allowAsync); +} + +FileSearchPath LV2PluginFormatHeadless::getDefaultLocationsToSearch() +{ + return pimpl->getDefaultLocationsToSearch(); +} + +bool LV2PluginFormatHeadless::requiresUnblockedMessageThreadDuringCreation (const PluginDescription&) const +{ + return false; +} + +void LV2PluginFormatHeadless::createPluginInstance (const PluginDescription& desc, + double sampleRate, + int bufferSize, + PluginCreationCallback callback) +{ + Pimpl::createPluginInstance (*this, desc, sampleRate, bufferSize, std::move (callback)); +} + +} // namespace juce + +#endif diff --git a/modules/juce_audio_processors_headless/format_types/juce_LV2PluginFormatHeadless.h b/modules/juce_audio_processors_headless/format_types/juce_LV2PluginFormatHeadless.h new file mode 100644 index 0000000000..cd8c89dcf2 --- /dev/null +++ b/modules/juce_audio_processors_headless/format_types/juce_LV2PluginFormatHeadless.h @@ -0,0 +1,89 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +#if (JUCE_PLUGINHOST_LV2 && (! (JUCE_ANDROID || JUCE_IOS))) || DOXYGEN + +/** + Implements a plugin format for LV2 plugins. + + @tags{Audio} +*/ +class JUCE_API LV2PluginFormatHeadless : public AudioPluginFormat +{ +public: + LV2PluginFormatHeadless(); + ~LV2PluginFormatHeadless() override; + + static String getFormatName() { return "LV2"; } + String getName() const override { return getFormatName(); } + + void findAllTypesForFile (OwnedArray& results, + const String& fileOrIdentifier) override; + + bool fileMightContainThisPluginType (const String& fileOrIdentifier) override; + + String getNameOfPluginFromIdentifier (const String& fileOrIdentifier) override; + + bool pluginNeedsRescanning (const PluginDescription&) override; + + bool doesPluginStillExist (const PluginDescription&) override; + + bool canScanForPlugins() const override; + + bool isTrivialToScan() const override; + + StringArray searchPathsForPlugins (const FileSearchPath& directoriesToSearch, + bool recursive, + bool allowPluginsWhichRequireAsynchronousInstantiation = false) override; + + FileSearchPath getDefaultLocationsToSearch() override; + + /** @internal */ + class Pimpl; + +private: + bool requiresUnblockedMessageThreadDuringCreation (const PluginDescription&) const override; + void createPluginInstance (const PluginDescription&, double, int, PluginCreationCallback) override; + + std::unique_ptr pimpl; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LV2PluginFormatHeadless) +}; + +#endif + +} // namespace juce diff --git a/modules/juce_audio_processors_headless/format_types/juce_LV2PluginFormatImpl.h b/modules/juce_audio_processors_headless/format_types/juce_LV2PluginFormatImpl.h new file mode 100644 index 0000000000..5eeb307453 --- /dev/null +++ b/modules/juce_audio_processors_headless/format_types/juce_LV2PluginFormatImpl.h @@ -0,0 +1,4733 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +#if JUCE_PLUGINHOST_LV2 && (! (JUCE_ANDROID || JUCE_IOS)) + +#include +#include +#include + +#include + +namespace juce +{ +namespace lv2_host +{ + +template +auto with (Struct s, Value Struct::* member, Value value) noexcept +{ + s.*member = std::move (value); + return s; +} + +/* Converts a void* to an LV2_Atom* if the buffer looks like it holds a well-formed Atom, or + returns nullptr otherwise. +*/ +static const LV2_Atom* convertToAtomPtr (const void* ptr, size_t size) +{ + if (size < sizeof (LV2_Atom)) + { + jassertfalse; + return nullptr; + } + + const auto header = readUnaligned (ptr); + + if (size < header.size + sizeof (LV2_Atom)) + { + jassertfalse; + return nullptr; + } + + // This is UB _if_ the ptr doesn't really point to an LV2_Atom. + return reinterpret_cast (ptr); +} + +// Allows mutable access to the items in a vector, without allowing the vector itself +// to be modified. +template +class SimpleSpan +{ +public: + constexpr SimpleSpan (T* beginIn, T* endIn) : b (beginIn), e (endIn) {} + + constexpr auto begin() const { return b; } + constexpr auto end() const { return e; } + + JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4814) + constexpr auto& operator[] (size_t index) { return b[index]; } + JUCE_END_IGNORE_WARNINGS_MSVC + + constexpr auto size() const { return (size_t) (e - b); } + +private: + T* b; + T* e; +}; + +template +constexpr auto makeSimpleSpan (T* b, T* e) { return SimpleSpan { b, e }; } + +template +constexpr auto makeSimpleSpan (R& r) { return makeSimpleSpan (r.data(), r.data() + r.size()); } + +struct PhysicalResizeListener +{ + virtual ~PhysicalResizeListener() = default; + virtual void viewRequestedResizeInPhysicalPixels (int width, int height) = 0; +}; + +struct LogicalResizeListener +{ + virtual ~LogicalResizeListener() = default; + virtual void viewRequestedResizeInLogicalPixels (int width, int height) = 0; +}; + +#if JUCE_WINDOWS +class WindowSizeChangeDetector +{ +public: + WindowSizeChangeDetector() + : hook (SetWindowsHookEx (WH_CALLWNDPROC, + callWndProc, + (HINSTANCE) juce::Process::getCurrentModuleInstanceHandle(), + GetCurrentThreadId())) + {} + + ~WindowSizeChangeDetector() noexcept + { + UnhookWindowsHookEx (hook); + } + + static void addListener (HWND hwnd, PhysicalResizeListener& listener) + { + getActiveEditors().emplace (hwnd, &listener); + } + + static void removeListener (HWND hwnd) + { + getActiveEditors().erase (hwnd); + } + +private: + static std::map& getActiveEditors() + { + static std::map map; + return map; + } + + static void processMessage (int nCode, const CWPSTRUCT* info) + { + if (nCode < 0 || info == nullptr) + return; + + constexpr UINT events[] { WM_SIZING, WM_SIZE, WM_WINDOWPOSCHANGING, WM_WINDOWPOSCHANGED }; + + if (std::find (std::begin (events), std::end (events), info->message) == std::end (events)) + return; + + auto& map = getActiveEditors(); + auto iter = map.find (info->hwnd); + + if (iter == map.end()) + return; + + RECT rect; + GetWindowRect (info->hwnd, &rect); + iter->second->viewRequestedResizeInPhysicalPixels (rect.right - rect.left, rect.bottom - rect.top); + } + + static LRESULT CALLBACK callWndProc (int nCode, WPARAM wParam, LPARAM lParam) + { + processMessage (nCode, lv2_shared::wordCast (lParam)); + return CallNextHookEx ({}, nCode, wParam, lParam); + } + + HHOOK hook; +}; + +class WindowSizeChangeListener +{ +public: + WindowSizeChangeListener (HWND hwndIn, PhysicalResizeListener& l) + : hwnd (hwndIn) + { + detector->addListener (hwnd, l); + } + + ~WindowSizeChangeListener() + { + detector->removeListener (hwnd); + } + +private: + SharedResourcePointer detector; + HWND hwnd; + + JUCE_LEAK_DETECTOR (WindowSizeChangeListener) +}; +#endif + +struct FreeLilvNode +{ + void operator() (LilvNode* ptr) const noexcept { lilv_node_free (ptr); } +}; + +using OwningNode = std::unique_ptr; + +template +class TypesafeLilvNode +{ +public: + template + TypesafeLilvNode (Ts&&... ts) + : node (Traits::construct (std::forward (ts)...)) {} + + bool equals (const TypesafeLilvNode& other) const noexcept + { + return lilv_node_equals (node.get(), other.node.get()); + } + + const LilvNode* get() const noexcept { return node.get(); } + + auto getTyped() const noexcept -> decltype (Traits::access (nullptr)) + { + return Traits::access (node.get()); + } + + static TypesafeLilvNode claim (LilvNode* node) + { + return TypesafeLilvNode { node }; + } + + static TypesafeLilvNode copy (const LilvNode* node) + { + return TypesafeLilvNode { lilv_node_duplicate (node) }; + } + +private: + explicit TypesafeLilvNode (LilvNode* ptr) + : node (ptr) + { + jassert (ptr == nullptr || Traits::verify (node.get())); + } + + OwningNode node; + + JUCE_LEAK_DETECTOR (TypesafeLilvNode) +}; + +struct UriConstructorTrait +{ + static LilvNode* construct (LilvWorld* world, const char* uri) noexcept + { + return lilv_new_uri (world, uri); + } + + static LilvNode* construct (LilvWorld* world, const char* host, const char* path) noexcept + { + return lilv_new_file_uri (world, host, path); + } + + static constexpr auto verify = lilv_node_is_uri; + static constexpr auto access = lilv_node_as_uri; +}; + +struct StringConstructorTrait { static constexpr auto construct = lilv_new_string; + static constexpr auto verify = lilv_node_is_string; + static constexpr auto access = lilv_node_as_string; }; + +using NodeUri = TypesafeLilvNode; +using NodeString = TypesafeLilvNode; + +struct UsefulUris +{ + explicit UsefulUris (LilvWorld* worldIn) + : world (worldIn) {} + + LilvWorld* const world = nullptr; + + #define X(str) const NodeUri m##str { world, str }; + X (LV2_ATOM__AtomPort) + X (LV2_ATOM__atomTransfer) + X (LV2_ATOM__eventTransfer) + X (LV2_CORE__AudioPort) + X (LV2_CORE__CVPort) + X (LV2_CORE__ControlPort) + X (LV2_CORE__GeneratorPlugin) + X (LV2_CORE__InputPort) + X (LV2_CORE__InstrumentPlugin) + X (LV2_CORE__OutputPort) + X (LV2_CORE__enumeration) + X (LV2_CORE__integer) + X (LV2_CORE__toggled) + X (LV2_RESIZE_PORT__minimumSize) + X (LV2_UI__floatProtocol) + X (LV2_WORKER__interface) + #undef X +}; + +template +struct OwningPtrTraits +{ + using type = std::unique_ptr; + static const Ptr* get (const type& t) noexcept { return t.get(); } +}; + +template +struct NonOwningPtrTraits +{ + using type = const Ptr*; + static const Ptr* get (const type& t) noexcept { return t; } +}; + +struct PluginsIteratorTraits +{ + using Container = const LilvPlugins*; + using Iter = LilvIter*; + static constexpr auto begin = lilv_plugins_begin; + static constexpr auto next = lilv_plugins_next; + static constexpr auto isEnd = lilv_plugins_is_end; + static constexpr auto get = lilv_plugins_get; +}; + +using PluginsIterator = lv2_shared::Iterator; + +struct PluginClassesIteratorTraits +{ + using Container = const LilvPluginClasses*; + using Iter = LilvIter*; + static constexpr auto begin = lilv_plugin_classes_begin; + static constexpr auto next = lilv_plugin_classes_next; + static constexpr auto isEnd = lilv_plugin_classes_is_end; + static constexpr auto get = lilv_plugin_classes_get; +}; + +using PluginClassesIterator = lv2_shared::Iterator; + +struct NodesIteratorTraits +{ + using Container = const LilvNodes*; + using Iter = LilvIter*; + static constexpr auto begin = lilv_nodes_begin; + static constexpr auto next = lilv_nodes_next; + static constexpr auto isEnd = lilv_nodes_is_end; + static constexpr auto get = lilv_nodes_get; +}; + +using NodesIterator = lv2_shared::Iterator; + +struct ScalePointsIteratorTraits +{ + using Container = const LilvScalePoints*; + using Iter = LilvIter*; + static constexpr auto begin = lilv_scale_points_begin; + static constexpr auto next = lilv_scale_points_next; + static constexpr auto isEnd = lilv_scale_points_is_end; + static constexpr auto get = lilv_scale_points_get; +}; + +using ScalePointsIterator = lv2_shared::Iterator; + +struct UisIteratorTraits +{ + using Container = const LilvUIs*; + using Iter = LilvIter*; + static constexpr auto begin = lilv_uis_begin; + static constexpr auto next = lilv_uis_next; + static constexpr auto isEnd = lilv_uis_is_end; + static constexpr auto get = lilv_uis_get; +}; + +using UisIterator = lv2_shared::Iterator; + +template +class NodesImpl +{ +public: + using type = typename PtrTraits::type; + + template + explicit NodesImpl (Ptr* ptr) + : nodes (type { ptr }) {} + + explicit NodesImpl (type ptr) + : nodes (std::move (ptr)) {} + + unsigned size() const noexcept { return lilv_nodes_size (PtrTraits::get (nodes)); } + + NodesIterator begin() const noexcept + { + return nodes == nullptr ? NodesIterator{} + : NodesIterator { PtrTraits::get (nodes) }; + } + + NodesIterator end() const noexcept { return {}; } + +private: + type nodes{}; +}; + +struct NodesFree +{ + void operator() (LilvNodes* ptr) const noexcept { lilv_nodes_free (ptr); } +}; + +using OwningNodes = NodesImpl>; +using NonOwningNodes = NodesImpl>; + +class ScalePoints +{ +public: + explicit ScalePoints (const LilvScalePoints* pt) + : points (pt) {} + + ScalePointsIterator begin() const noexcept + { + return points == nullptr ? ScalePointsIterator{} + : ScalePointsIterator { points }; + } + + ScalePointsIterator end() const noexcept { return {}; } + +private: + const LilvScalePoints* points = nullptr; +}; + +class ScalePoint +{ +public: + explicit ScalePoint (const LilvScalePoint* pt) + : point (pt) {} + + const LilvNode* getLabel() const noexcept { return lilv_scale_point_get_label (point); } + const LilvNode* getValue() const noexcept { return lilv_scale_point_get_value (point); } + +private: + const LilvScalePoint* point = nullptr; +}; + +struct PortRange +{ + float defaultValue, min, max; +}; + +class Port +{ +public: + enum class Kind + { + control, + audio, + cv, + atom, + unknown, + }; + + enum class Direction + { + input, + output, + unknown, + }; + + Port (const LilvPlugin* pluginIn, const LilvPort* portIn) + : plugin (pluginIn), port (portIn) {} + + Direction getDirection (const UsefulUris& uris) const noexcept + { + if (isA (uris.mLV2_CORE__InputPort)) + return Direction::input; + + if (isA (uris.mLV2_CORE__OutputPort)) + return Direction::output; + + return Direction::unknown; + } + + Kind getKind (const UsefulUris& uris) const noexcept + { + if (isA (uris.mLV2_CORE__ControlPort)) + return Kind::control; + + if (isA (uris.mLV2_CORE__AudioPort)) + return Kind::audio; + + if (isA (uris.mLV2_CORE__CVPort)) + return Kind::cv; + + if (isA (uris.mLV2_ATOM__AtomPort)) + return Kind::atom; + + return Kind::unknown; + } + + OwningNode get (const LilvNode* predicate) const noexcept + { + return OwningNode { lilv_port_get (plugin, port, predicate) }; + } + + NonOwningNodes getClasses() const noexcept + { + return NonOwningNodes { lilv_port_get_classes (plugin, port) }; + } + + NodeString getName() const noexcept + { + return NodeString::claim (lilv_port_get_name (plugin, port)); + } + + NodeString getSymbol() const noexcept + { + return NodeString::copy (lilv_port_get_symbol (plugin, port)); + } + + OwningNodes getProperties() const noexcept + { + return OwningNodes { lilv_port_get_properties (plugin, port) }; + } + + ScalePoints getScalePoints() const noexcept + { + return ScalePoints { lilv_port_get_scale_points (plugin, port) }; + } + + bool hasProperty (const NodeUri& uri) const noexcept + { + return lilv_port_has_property (plugin, port, uri.get()); + } + + uint32_t getIndex() const noexcept { return lilv_port_get_index (plugin, port); } + + static float getFloatValue (const LilvNode* node, float fallback) + { + if (lilv_node_is_float (node) || lilv_node_is_int (node)) + return lilv_node_as_float (node); + + return fallback; + } + + bool supportsEvent (const LilvNode* node) const noexcept + { + return lilv_port_supports_event (plugin, port, node); + } + + PortRange getRange() const noexcept + { + LilvNode* def = nullptr; + LilvNode* min = nullptr; + LilvNode* max = nullptr; + + lilv_port_get_range (plugin, port, &def, &min, &max); + + const OwningNode defOwner { def }; + const OwningNode minOwner { min }; + const OwningNode maxOwner { max }; + + return { getFloatValue (def, 0.0f), + getFloatValue (min, 0.0f), + getFloatValue (max, 1.0f) }; + } + + bool isValid() const noexcept { return port != nullptr; } + +private: + bool isA (const NodeUri& uri) const noexcept + { + return lilv_port_is_a (plugin, port, uri.get()); + } + + const LilvPlugin* plugin = nullptr; + const LilvPort* port = nullptr; + + JUCE_LEAK_DETECTOR (Port) +}; + +class Plugin +{ +public: + explicit Plugin (const LilvPlugin* p) : plugin (p) {} + + bool verify() const noexcept { return lilv_plugin_verify (plugin); } + NodeUri getUri() const noexcept { return NodeUri::copy (lilv_plugin_get_uri (plugin)); } + NodeUri getBundleUri() const noexcept { return NodeUri::copy (lilv_plugin_get_bundle_uri (plugin)); } + NodeUri getLibraryUri() const noexcept { return NodeUri::copy (lilv_plugin_get_library_uri (plugin)); } + NodeString getName() const noexcept { return NodeString::claim (lilv_plugin_get_name (plugin)); } + NodeString getAuthorName() const noexcept { return NodeString::claim (lilv_plugin_get_author_name (plugin)); } + uint32_t getNumPorts() const noexcept { return lilv_plugin_get_num_ports (plugin); } + const LilvPluginClass* getClass() const noexcept { return lilv_plugin_get_class (plugin); } + OwningNodes getValue (const LilvNode* predicate) const noexcept { return OwningNodes { lilv_plugin_get_value (plugin, predicate) }; } + + Port getPortByIndex (uint32_t index) const noexcept + { + return Port { plugin, lilv_plugin_get_port_by_index (plugin, index) }; + } + + Port getPortByDesignation (const LilvNode* portClass, const LilvNode* designation) const noexcept + { + return Port { plugin, lilv_plugin_get_port_by_designation (plugin, portClass, designation) }; + } + + OwningNodes getRequiredFeatures() const noexcept + { + return OwningNodes { lilv_plugin_get_required_features (plugin) }; + } + + OwningNodes getOptionalFeatures() const noexcept + { + return OwningNodes { lilv_plugin_get_optional_features (plugin) }; + } + + bool hasExtensionData (const NodeUri& uri) const noexcept + { + return lilv_plugin_has_extension_data (plugin, uri.get()); + } + + bool hasFeature (const NodeUri& uri) const noexcept + { + return lilv_plugin_has_feature (plugin, uri.get()); + } + + template + uint32_t getNumPortsOfClass (const Classes&... classes) const noexcept + { + return lilv_plugin_get_num_ports_of_class (plugin, classes.get()..., 0); + } + + const LilvPlugin* get() const noexcept { return plugin; } + + bool hasLatency() const noexcept { return lilv_plugin_has_latency (plugin); } + uint32_t getLatencyPortIndex() const noexcept { return lilv_plugin_get_latency_port_index (plugin); } + +private: + const LilvPlugin* plugin = nullptr; + + JUCE_LEAK_DETECTOR (Plugin) +}; + +/* + This is very similar to the symap implementation in jalv. +*/ +class SymbolMap +{ +public: + SymbolMap() = default; + + SymbolMap (std::initializer_list uris) + { + for (const auto* str : uris) + map (str); + } + + LV2_URID map (const char* uri) + { + const auto comparator = [this] (size_t index, const String& str) + { + return strings[index] < str; + }; + + const auto uriString = String::fromUTF8 (uri); + const auto it = std::lower_bound (indices.cbegin(), indices.cend(), uriString, comparator); + + if (it != indices.cend() && strings[*it] == uriString) + return static_cast (*it + 1); + + const auto index = strings.size(); + indices.insert (it, index); + strings.push_back (uriString); + return static_cast (index + 1); + } + + const char* unmap (LV2_URID urid) const + { + const auto index = urid - 1; + return index < strings.size() ? strings[index].toRawUTF8() + : nullptr; + } + + static LV2_URID mapUri (LV2_URID_Map_Handle handle, const char* uri) + { + return static_cast (handle)->map (uri); + } + + static const char* unmapUri (LV2_URID_Unmap_Handle handle, LV2_URID urid) + { + return static_cast (handle)->unmap (urid); + } + + LV2_URID_Map getMapFeature() { return { this, mapUri }; } + LV2_URID_Unmap getUnmapFeature() { return { this, unmapUri }; } + +private: + std::vector strings; + std::vector indices; + + JUCE_LEAK_DETECTOR (SymbolMap) +}; + +struct UsefulUrids +{ + explicit UsefulUrids (SymbolMap& m) : symap (m) {} + + SymbolMap& symap; + + #define X(token) const LV2_URID m##token = symap.map (token); + X (LV2_ATOM__Bool) + X (LV2_ATOM__Double) + X (LV2_ATOM__Float) + X (LV2_ATOM__Int) + X (LV2_ATOM__Long) + X (LV2_ATOM__Object) + X (LV2_ATOM__Sequence) + X (LV2_ATOM__atomTransfer) + X (LV2_ATOM__beatTime) + X (LV2_ATOM__eventTransfer) + X (LV2_ATOM__frameTime) + X (LV2_LOG__Error) + X (LV2_LOG__Note) + X (LV2_LOG__Trace) + X (LV2_LOG__Warning) + X (LV2_MIDI__MidiEvent) + X (LV2_PATCH__Set) + X (LV2_PATCH__property) + X (LV2_PATCH__value) + X (LV2_STATE__StateChanged) + X (LV2_TIME__Position) + X (LV2_TIME__barBeat) + X (LV2_TIME__beat) + X (LV2_TIME__beatUnit) + X (LV2_TIME__beatsPerBar) + X (LV2_TIME__beatsPerMinute) + X (LV2_TIME__frame) + X (LV2_TIME__speed) + X (LV2_TIME__bar) + X (LV2_UI__floatProtocol) + X (LV2_UNITS__beat) + X (LV2_UNITS__frame) + #undef X +}; + +class Log +{ +public: + explicit Log (const UsefulUrids* u) : urids (u) {} + + LV2_Log_Log* getLogFeature() { return &logFeature; } + +private: + int vprintfCallback ([[maybe_unused]] LV2_URID type, const char* fmt, va_list ap) const + { + // If this is hit, the plugin has encountered some kind of error + ignoreUnused (urids); + jassert (type != urids->mLV2_LOG__Error && type != urids->mLV2_LOG__Warning); + return std::vfprintf (stderr, fmt, ap); + } + + static int vprintfCallback (LV2_Log_Handle handle, + LV2_URID type, + const char* fmt, + va_list ap) + { + return static_cast (handle)->vprintfCallback (type, fmt, ap); + } + + static int printfCallback (LV2_Log_Handle handle, LV2_URID type, const char* fmt, ...) + { + va_list list; + va_start (list, fmt); + auto result = vprintfCallback (handle, type, fmt, list); + va_end (list); + return result; + } + + const UsefulUrids* urids = nullptr; + LV2_Log_Log logFeature { this, printfCallback, vprintfCallback }; + + JUCE_LEAK_DETECTOR (Log) +}; + +struct Features +{ + explicit Features (std::vector&& f) + : features (std::move (f)) {} + + static std::vector getUris (const std::vector& features) + { + std::vector result; + result.reserve (features.size()); + + for (const auto& feature : features) + result.push_back (String::fromUTF8 (feature.URI)); + + return result; + } + + std::vector features; + std::vector pointers = makeNullTerminatedArray(); + +private: + std::vector makeNullTerminatedArray() + { + std::vector result; + result.reserve (features.size() + 1); + + for (const auto& feature : features) + result.push_back (&feature); + + result.push_back (nullptr); + + return result; + } + + JUCE_LEAK_DETECTOR (Features) +}; + +template +struct OptionalExtension +{ + OptionalExtension() = default; + + explicit OptionalExtension (Extension extensionIn) : extension (extensionIn), valid (true) {} + + Extension extension; + bool valid = false; +}; + +class Instance +{ + struct Free + { + void operator() (LilvInstance* ptr) const noexcept { lilv_instance_free (ptr); } + }; + +public: + using Ptr = std::unique_ptr; + using GetExtensionData = const void* (*) (const char*); + + Instance (const Plugin& pluginIn, double sampleRate, const LV2_Feature* const* features) + : plugin (pluginIn), + instance (lilv_plugin_instantiate (plugin.get(), sampleRate, features)) {} + + void activate() { lilv_instance_activate (instance.get()); } + void run (uint32_t sampleCount) { lilv_instance_run (instance.get(), sampleCount); } + void deactivate() { lilv_instance_deactivate (instance.get()); } + + const char* getUri() const noexcept { return lilv_instance_get_uri (instance.get()); } + + LV2_Handle getHandle() const noexcept { return lilv_instance_get_handle (instance.get()); } + + LilvInstance* get() const noexcept { return instance.get(); } + + void connectPort (uint32_t index, void* data) + { + lilv_instance_connect_port (instance.get(), index, data); + } + + template + OptionalExtension getExtensionData (const NodeUri& uri) const noexcept + { + if (plugin.get() == nullptr || ! plugin.hasExtensionData (uri) || instance.get() == nullptr) + return {}; + + return OptionalExtension { readUnaligned (lilv_instance_get_extension_data (instance.get(), uri.getTyped())) }; + } + + GetExtensionData getExtensionDataCallback() const noexcept + { + return instance->lv2_descriptor->extension_data; + } + + bool operator== (std::nullptr_t) const noexcept { return instance == nullptr; } + bool operator!= (std::nullptr_t) const noexcept { return ! (*this == nullptr); } + +private: + Plugin plugin; + Ptr instance; + + JUCE_LEAK_DETECTOR (Instance) +}; + +enum class Realtime { no, yes }; + +// Must be trivial! +struct WorkResponder +{ + static WorkResponder getDefault() { return { nullptr, nullptr }; } + + LV2_Worker_Status processResponse (uint32_t size, const void* data) const + { + return worker->work_response (handle, size, data); + } + + bool isValid() const { return handle != nullptr && worker != nullptr; } + + LV2_Handle handle; + const LV2_Worker_Interface* worker; +}; + +struct WorkerResponseListener +{ + virtual ~WorkerResponseListener() = default; + virtual LV2_Worker_Status responseGenerated (WorkResponder, uint32_t, const void*) = 0; +}; + +struct RespondHandle +{ + LV2_Worker_Status respond (uint32_t size, const void* data) const + { + if (realtime == Realtime::yes) + return listener.responseGenerated (responder, size, data); + + return responder.processResponse (size, data); + } + + static LV2_Worker_Status respond (LV2_Worker_Respond_Handle handle, + uint32_t size, + const void* data) + { + return static_cast (handle)->respond (size, data); + } + + WorkResponder responder; + WorkerResponseListener& listener; + Realtime realtime; +}; + +// Must be trivial! +struct WorkSubmitter +{ + static WorkSubmitter getDefault() { return { nullptr, nullptr, nullptr, nullptr }; } + + LV2_Worker_Status doWork (Realtime realtime, uint32_t size, const void* data) const + { + // The Worker spec says that the host "MUST NOT make concurrent calls to [work] from + // several threads". + // Taking the work mutex here ensures that only one piece of work is done at a time. + // If we didn't take the work mutex, there would be a danger of work happening + // simultaneously on the worker thread and the render thread when switching between + // realtime/offline modes (in realtime mode, work happens on the worker thread; in + // offline mode, work happens immediately on the render/audio thread). + const ScopedLock lock (*workMutex); + + RespondHandle respondHandle { WorkResponder { handle, worker }, *listener, realtime }; + return worker->work (handle, RespondHandle::respond, &respondHandle, size, data); + } + + bool isValid() const { return handle != nullptr && worker != nullptr && listener != nullptr && workMutex != nullptr; } + + LV2_Handle handle; + const LV2_Worker_Interface* worker; + WorkerResponseListener* listener; + CriticalSection* workMutex; +}; + +template +static auto toChars (Trivial value) +{ + static_assert (std::is_trivial_v); + std::array result; + writeUnaligned (result.data(), value); + return result; +} + +template +class WorkQueue +{ +public: + static_assert (std::is_trivial_v, "Context must be copyable as bytes"); + + explicit WorkQueue (int size) + : fifo (size), data (static_cast (size)) {} + + LV2_Worker_Status push (Context context, size_t size, const void* contents) + { + const auto* bytes = static_cast (contents); + const auto numToWrite = sizeof (Header) + size; + + if (static_cast (fifo.getFreeSpace()) < numToWrite) + return LV2_WORKER_ERR_NO_SPACE; + + Header header { size, context }; + const auto headerBuffer = toChars (header); + + const auto scope = fifo.write (static_cast (numToWrite)); + jassert (scope.blockSize1 + scope.blockSize2 == static_cast (numToWrite)); + + size_t index = 0; + scope.forEach ([&] (int i) + { + data[static_cast (i)] = index < headerBuffer.size() ? headerBuffer[index] + : bytes[index - headerBuffer.size()]; + ++index; + }); + + return LV2_WORKER_SUCCESS; + } + + Context pop (std::vector& dest) + { + // If the vector is too small we'll have to resize it on the audio thread + jassert (dest.capacity() >= data.size()); + dest.clear(); + + const auto numReady = fifo.getNumReady(); + + if (static_cast (numReady) < sizeof (Header)) + { + jassert (numReady == 0); + return Context::getDefault(); + } + + std::array headerBuffer; + + { + size_t index = 0; + fifo.read (sizeof (Header)).forEach ([&] (int i) + { + headerBuffer[index++] = data[static_cast (i)]; + }); + } + + const auto header = readUnaligned
(headerBuffer.data()); + + jassert (static_cast (fifo.getNumReady()) >= header.size); + + dest.resize (header.size); + + { + size_t index = 0; + fifo.read (static_cast (header.size)).forEach ([&] (int i) + { + dest[index++] = data[static_cast (i)]; + }); + } + + return header.context; + } + +private: + struct Header + { + size_t size; + Context context; + }; + + AbstractFifo fifo; + std::vector data; + + JUCE_LEAK_DETECTOR (WorkQueue) +}; + +/* + Keeps track of active plugin instances, so that we can avoid sending work + messages to dead plugins. +*/ +class HandleRegistry +{ +public: + void insert (LV2_Handle handle) + { + const SpinLock::ScopedLockType lock (mutex); + handles.insert (handle); + } + + void erase (LV2_Handle handle) + { + const SpinLock::ScopedLockType lock (mutex); + handles.erase (handle); + } + + template + LV2_Worker_Status ifContains (LV2_Handle handle, Fn&& callback) + { + const SpinLock::ScopedLockType lock (mutex); + + if (handles.find (handle) != handles.cend()) + return callback(); + + return LV2_WORKER_ERR_UNKNOWN; + } + +private: + std::set handles; + SpinLock mutex; + + JUCE_LEAK_DETECTOR (HandleRegistry) +}; + +/* + Implements an LV2 Worker, allowing work to be scheduled in realtime + by the plugin instance. + + IMPORTANT this will die pretty hard if `getExtensionData (LV2_WORKER__interface)` + returns garbage, so make sure to check that the plugin `hasExtensionData` before + constructing one of these! +*/ +class SharedThreadedWorker final : public WorkerResponseListener +{ +public: + ~SharedThreadedWorker() noexcept override + { + shouldExit = true; + thread.join(); + } + + LV2_Worker_Status schedule (WorkSubmitter submitter, + uint32_t size, + const void* data) + { + return registry.ifContains (submitter.handle, [&] + { + return incoming.push (submitter, size, data); + }); + } + + LV2_Worker_Status responseGenerated (WorkResponder responder, + uint32_t size, + const void* data) override + { + return registry.ifContains (responder.handle, [&] + { + return outgoing.push (responder, size, data); + }); + } + + void processResponses() + { + for (;;) + { + auto workerResponder = outgoing.pop (message); + + if (! message.empty() && workerResponder.isValid()) + workerResponder.processResponse (static_cast (message.size()), message.data()); + else + break; + } + } + + void registerHandle (LV2_Handle handle) { registry.insert (handle); } + void deregisterHandle (LV2_Handle handle) { registry.erase (handle); } + +private: + static constexpr auto queueSize = 8192; + std::atomic shouldExit { false }; + WorkQueue incoming { queueSize }; + WorkQueue outgoing { queueSize }; + std::vector message = std::vector (queueSize); + std::thread thread { [this] + { + std::vector buffer (queueSize); + + while (! shouldExit) + { + const auto submitter = incoming.pop (buffer); + + if (! buffer.empty() && submitter.isValid()) + submitter.doWork (Realtime::yes, (uint32_t) buffer.size(), buffer.data()); + else + std::this_thread::sleep_for (std::chrono::milliseconds (1)); + } + } }; + HandleRegistry registry; + + JUCE_LEAK_DETECTOR (SharedThreadedWorker) +}; + +struct HandleHolder +{ + virtual ~HandleHolder() = default; + virtual LV2_Handle getHandle() const = 0; + virtual const LV2_Worker_Interface* getWorkerInterface() const = 0; +}; + +class WorkScheduler +{ +public: + explicit WorkScheduler (HandleHolder& handleHolderIn) + : handleHolder (handleHolderIn) {} + + void processResponses() { workerThread->processResponses(); } + + LV2_Worker_Schedule& getWorkerSchedule() { return schedule; } + + void setNonRealtime (bool nonRealtime) { realtime = ! nonRealtime; } + + void registerHandle (LV2_Handle handle) { workerThread->registerHandle (handle); } + void deregisterHandle (LV2_Handle handle) { workerThread->deregisterHandle (handle); } + +private: + LV2_Worker_Status scheduleWork (uint32_t size, const void* data) + { + WorkSubmitter submitter { handleHolder.getHandle(), + handleHolder.getWorkerInterface(), + workerThread, + &workMutex }; + + // If we're in realtime mode, the work should go onto a background thread, + // and we'll process it later. + // If we're offline, we can just do the work immediately, without worrying about + // drop-outs + return realtime ? workerThread->schedule (submitter, size, data) + : submitter.doWork (Realtime::no, size, data); + } + + static LV2_Worker_Status scheduleWork (LV2_Worker_Schedule_Handle handle, + uint32_t size, + const void* data) + { + return static_cast (handle)->scheduleWork (size, data); + } + + SharedResourcePointer workerThread; + HandleHolder& handleHolder; + LV2_Worker_Schedule schedule { this, scheduleWork }; + CriticalSection workMutex; + bool realtime = true; + + JUCE_LEAK_DETECTOR (WorkScheduler) +}; + +struct FeaturesDataListener +{ + virtual ~FeaturesDataListener() = default; + virtual LV2_Resize_Port_Status resizeCallback (uint32_t index, size_t size) = 0; +}; + +class Resize +{ +public: + explicit Resize (FeaturesDataListener& l) + : listener (l) {} + + LV2_Resize_Port_Resize& getFeature() { return resize; } + +private: + LV2_Resize_Port_Status resizeCallback (uint32_t index, size_t size) + { + return listener.resizeCallback (index, size); + } + + static LV2_Resize_Port_Status resizeCallback (LV2_Resize_Port_Feature_Data data, uint32_t index, size_t size) + { + return static_cast (data)->resizeCallback (index, size); + } + + FeaturesDataListener& listener; + LV2_Resize_Port_Resize resize { this, resizeCallback }; +}; + +class FeaturesData +{ +public: + FeaturesData (HandleHolder& handleHolder, + FeaturesDataListener& l, + int32_t maxBlockSizeIn, + int32_t sequenceSizeIn, + const UsefulUrids* u) + : urids (u), + resize (l), + maxBlockSize (maxBlockSizeIn), + sequenceSize (sequenceSizeIn), + workScheduler (handleHolder) + {} + + LV2_Options_Option* getOptions() noexcept { return options.data(); } + + int32_t getMaxBlockSize() const noexcept { return maxBlockSize; } + + void setNonRealtime (bool newValue) { realtime = ! newValue; } + + const LV2_Feature* const* getFeatureArray() const noexcept { return features.pointers.data(); } + + static std::vector getFeatureUris() + { + return Features::getUris (makeFeatures ({}, {}, {}, {}, {}, {})); + } + + void processResponses() { workScheduler.processResponses(); } + + void registerHandle (LV2_Handle handle) { workScheduler.registerHandle (handle); } + void deregisterHandle (LV2_Handle handle) { workScheduler.deregisterHandle (handle); } + +private: + static std::vector makeFeatures (LV2_URID_Map* map, + LV2_URID_Unmap* unmap, + LV2_Options_Option* options, + LV2_Worker_Schedule* schedule, + LV2_Resize_Port_Resize* resize, + [[maybe_unused]] LV2_Log_Log* log) + { + return { LV2_Feature { LV2_STATE__loadDefaultState, nullptr }, + LV2_Feature { LV2_BUF_SIZE__boundedBlockLength, nullptr }, + LV2_Feature { LV2_URID__map, map }, + LV2_Feature { LV2_URID__unmap, unmap }, + LV2_Feature { LV2_OPTIONS__options, options }, + LV2_Feature { LV2_WORKER__schedule, schedule }, + LV2_Feature { LV2_STATE__threadSafeRestore, nullptr }, + #if JUCE_DEBUG + LV2_Feature { LV2_LOG__log, log }, + #endif + LV2_Feature { LV2_RESIZE_PORT__resize, resize } }; + } + + LV2_Options_Option makeOption (const char* uid, const int32_t* ptr) + { + return { LV2_OPTIONS_INSTANCE, + 0, // INSTANCE kinds must have a subject of 0 + urids->symap.map (uid), + sizeof (int32_t), + urids->symap.map (LV2_ATOM__Int), + ptr }; + } + + const UsefulUrids* urids; + Resize resize; + Log log { urids }; + + const int32_t minBlockSize = 0, maxBlockSize = 0, sequenceSize = 0; + + std::vector options + { + makeOption (LV2_BUF_SIZE__minBlockLength, &minBlockSize), + makeOption (LV2_BUF_SIZE__maxBlockLength, &maxBlockSize), + makeOption (LV2_BUF_SIZE__sequenceSize, &sequenceSize), + { LV2_OPTIONS_INSTANCE, 0, 0, 0, 0, nullptr }, // The final entry must be nulled out + }; + + WorkScheduler workScheduler; + + LV2_URID_Map map = urids->symap.getMapFeature(); + LV2_URID_Unmap unmap = urids->symap.getUnmapFeature(); + Features features { makeFeatures (&map, + &unmap, + options.data(), + &workScheduler.getWorkerSchedule(), + &resize.getFeature(), + log.getLogFeature()) }; + + bool realtime = true; + + JUCE_LEAK_DETECTOR (FeaturesData) +}; + +//============================================================================== +struct TryLockAndCall +{ + template + void operator() (SpinLock& mutex, Fn&& fn) + { + const SpinLock::ScopedTryLockType lock (mutex); + + if (lock.isLocked()) + fn(); + } +}; + +struct LockAndCall +{ + template + void operator() (SpinLock& mutex, Fn&& fn) + { + const SpinLock::ScopedLockType lock (mutex); + fn(); + } +}; + +struct RealtimeReadTrait +{ + using Read = TryLockAndCall; + using Write = LockAndCall; +}; + +struct RealtimeWriteTrait +{ + using Read = LockAndCall; + using Write = TryLockAndCall; +}; + +struct MessageHeader +{ + uint32_t portIndex; + uint32_t protocol; +}; + +template +struct MessageBufferInterface +{ + virtual ~MessageBufferInterface() = default; + virtual void pushMessage (Header header, uint32_t size, const void* buffer) = 0; +}; + +template +class Messages final : public MessageBufferInterface
+{ + using Read = typename LockTraits::Read; + using Write = typename LockTraits::Write; + + struct FullHeader + { + Header header; + uint32_t size; + }; + +public: + Messages() { data.reserve (initialBufferSize); } + + void pushMessage (Header header, uint32_t size, const void* buffer) override + { + Write{} (mutex, [&] + { + const auto chars = toChars (FullHeader { header, size }); + const auto bufferAsChars = static_cast (buffer); + data.insert (data.end(), chars.begin(), chars.end()); + data.insert (data.end(), bufferAsChars, bufferAsChars + size); + }); + } + + template + void readAllAndClear (Callback&& callback) + { + Read{} (mutex, [&] + { + if (data.empty()) + return; + + const auto end = data.data() + data.size(); + + for (auto ptr = data.data(); ptr < end;) + { + const auto header = readUnaligned (ptr); + callback (header.header, header.size, ptr + sizeof (header)); + ptr += sizeof (header) + header.size; + } + + data.clear(); + }); + } + +private: + static constexpr auto initialBufferSize = 8192; + SpinLock mutex; + std::vector data; + + JUCE_LEAK_DETECTOR (Messages) +}; + +//============================================================================== +struct UiEventListener : public MessageBufferInterface +{ + virtual int idle() = 0; +}; + +struct UiMessageHeader +{ + UiEventListener* listener; + MessageHeader header; +}; + +class ProcessorToUi final : public MessageBufferInterface +{ +public: + ProcessorToUi() { timer.startTimerHz (60); } + + void addUi (UiEventListener& l) { JUCE_ASSERT_MESSAGE_THREAD; activeUis.insert (&l); } + void removeUi (UiEventListener& l) { JUCE_ASSERT_MESSAGE_THREAD; activeUis.erase (&l); } + + void pushMessage (UiMessageHeader header, uint32_t size, const void* buffer) override + { + processorToUi.pushMessage (header, size, buffer); + } + +private: + Messages processorToUi; + std::set activeUis; + TimedCallback timer { [this] + { + for (auto* l : activeUis) + if (l->idle() != 0) + return; + + processorToUi.readAllAndClear ([&] (const UiMessageHeader& header, uint32_t size, const char* data) + { + if (activeUis.find (header.listener) != activeUis.cend()) + header.listener->pushMessage (header.header, size, data); + }); + } }; +}; + +/* These type identifiers may be used to check the type of the incoming data. */ +struct StatefulPortUrids +{ + explicit StatefulPortUrids (SymbolMap& map) + : Float (map.map (LV2_ATOM__Float)), + Double (map.map (LV2_ATOM__Double)), + Int (map.map (LV2_ATOM__Int)), + Long (map.map (LV2_ATOM__Long)) + {} + + const LV2_URID Float, Double, Int, Long; +}; + +/* + A bit like SortedSet, but only requires `operator<` and not `operator==`, so + it behaves a bit more like a std::set. +*/ +template +class SafeSortedSet +{ +public: + using iterator = typename std::vector:: iterator; + using const_iterator = typename std::vector::const_iterator; + + template + const_iterator find (const Other& other) const noexcept + { + const auto it = std::lower_bound (storage.cbegin(), storage.cend(), other); + + if (it != storage.cend() && ! (other < *it)) + return it; + + return storage.cend(); + } + + void insert (Value&& value) { insertImpl (std::move (value)); } + void insert (const Value& value) { insertImpl (value); } + + size_t size() const noexcept { return storage.size(); } + bool empty() const noexcept { return storage.empty(); } + + iterator begin() noexcept { return storage. begin(); } + const_iterator begin() const noexcept { return storage. begin(); } + const_iterator cbegin() const noexcept { return storage.cbegin(); } + + iterator end() noexcept { return storage. end(); } + const_iterator end() const noexcept { return storage. end(); } + const_iterator cend() const noexcept { return storage.cend(); } + + auto& operator[] (size_t index) const { return storage[index]; } + +private: + template + void insertImpl (Arg&& value) + { + const auto it = std::lower_bound (storage.cbegin(), storage.cend(), value); + + if (it == storage.cend() || value < *it) + storage.insert (it, std::forward (value)); + } + + std::vector storage; +}; + +struct StoredScalePoint +{ + String label; + float value; + + bool operator< (const StoredScalePoint& other) const noexcept { return value < other.value; } +}; + +inline bool operator< (const StoredScalePoint& a, float b) noexcept { return a.value < b; } +inline bool operator< (float a, const StoredScalePoint& b) noexcept { return a < b.value; } + +struct ParameterInfo +{ + ParameterInfo() = default; + + ParameterInfo (SafeSortedSet scalePointsIn, + String identifierIn, + float defaultValueIn, + float minIn, + float maxIn, + bool isToggleIn, + bool isIntegerIn, + bool isEnumIn) + : scalePoints (std::move (scalePointsIn)), + identifier (std::move (identifierIn)), + defaultValue (defaultValueIn), + min (minIn), + max (maxIn), + isToggle (isToggleIn), + isInteger (isIntegerIn), + isEnum (isEnumIn) + {} + + static SafeSortedSet getScalePoints (const Port& port) + { + SafeSortedSet scalePoints; + + for (const LilvScalePoint* p : port.getScalePoints()) + { + const ScalePoint wrapper { p }; + const auto value = wrapper.getValue(); + const auto label = wrapper.getLabel(); + + if (lilv_node_is_float (value) || lilv_node_is_int (value)) + scalePoints.insert ({ lilv_node_as_string (label), lilv_node_as_float (value) }); + } + + return scalePoints; + } + + static ParameterInfo getInfoForPort (const UsefulUris& uris, const Port& port) + { + const auto range = port.getRange(); + + return { getScalePoints (port), + "sym:" + String::fromUTF8 (port.getSymbol().getTyped()), + range.defaultValue, + range.min, + range.max, + port.hasProperty (uris.mLV2_CORE__toggled), + port.hasProperty (uris.mLV2_CORE__integer), + port.hasProperty (uris.mLV2_CORE__enumeration) }; + } + + SafeSortedSet scalePoints; + + /* This is the 'symbol' of a port, or the 'designation' of a parameter without a symbol. */ + String identifier; + + float defaultValue = 0.0f, min = 0.0f, max = 1.0f; + bool isToggle = false, isInteger = false, isEnum = false; + + JUCE_LEAK_DETECTOR (ParameterInfo) +}; + +struct PortHeader +{ + String name; + String symbol; + uint32_t index; + Port::Direction direction; +}; + +struct ControlPort +{ + ControlPort (const PortHeader& headerIn, const ParameterInfo& infoIn) + : header (headerIn), info (infoIn) {} + + PortHeader header; + ParameterInfo info; + float currentValue = info.defaultValue; +}; + +struct CVPort +{ + PortHeader header; +}; + +struct AudioPort +{ + PortHeader header; +}; + +template +class SingleSizeAlignedStorage +{ +public: + SingleSizeAlignedStorage() = default; + + explicit SingleSizeAlignedStorage (size_t sizeInBytes) + : storage (new char[sizeInBytes + Alignment]), + alignedPointer (storage.get()), + space (sizeInBytes + Alignment) + { + alignedPointer = std::align (Alignment, sizeInBytes, alignedPointer, space); + } + + void* data() const { return alignedPointer; } + size_t size() const { return space; } + +private: + std::unique_ptr storage; + void* alignedPointer = nullptr; + size_t space = 0; +}; + +template +static SingleSizeAlignedStorage grow (SingleSizeAlignedStorage storage, size_t size) +{ + if (size <= storage.size()) + return storage; + + SingleSizeAlignedStorage newStorage { jmax (size, (storage.size() * 3) / 2) }; + std::memcpy (newStorage.data(), storage.data(), storage.size()); + return newStorage; +} + +enum class SupportsTime { no, yes }; + +class AtomPort +{ +public: + AtomPort (PortHeader h, size_t bytes, SymbolMap& map, SupportsTime supportsTime) + : header (h), contents (bytes), forge (map.getMapFeature()), time (supportsTime) {} + + PortHeader header; + + void replaceWithChunk() + { + forge.setBuffer (data(), size()); + forge.writeChunk ((uint32_t) (size() - sizeof (LV2_Atom))); + } + + void replaceBufferWithAtom (const LV2_Atom* atom) + { + const auto totalSize = atom->size + sizeof (LV2_Atom); + + if (totalSize <= size()) + std::memcpy (data(), atom, totalSize); + else + replaceWithChunk(); + } + + void beginSequence() + { + forge.setBuffer (data(), size()); + lv2_atom_forge_sequence_head (forge.get(), &frame, 0); + } + + void endSequence() + { + lv2_atom_forge_pop (forge.get(), &frame); + } + + /* For this to work, the 'atom' pointer must be well-formed. + + It must be followed by an atom header, then at least 'size' bytes of body. + */ + void addAtomToSequence (int64_t timestamp, const LV2_Atom* atom) + { + // This reinterpret_cast is not UB, casting to a char* is acceptable. + // Doing arithmetic on this pointer is dubious, but I can't think of a better alternative + // given that we don't have any way of knowing the concrete type of the atom. + addEventToSequence (timestamp, + atom->type, + atom->size, + reinterpret_cast (atom) + sizeof (LV2_Atom)); + } + + void addEventToSequence (int64_t timestamp, uint32_t type, uint32_t size, const void* content) + { + lv2_atom_forge_frame_time (forge.get(), timestamp); + lv2_atom_forge_atom (forge.get(), size, type); + lv2_atom_forge_write (forge.get(), content, size); + } + + void ensureSizeInBytes (size_t size) + { + contents = grow (std::move (contents), size); + } + + char* data() noexcept { return data (*this); } + const char* data() const noexcept { return data (*this); } + + size_t size() const noexcept { return contents.size(); } + + lv2_shared::AtomForge& getForge() { return forge; } + const lv2_shared::AtomForge& getForge() const { return forge; } + + bool getSupportsTime() const { return time == SupportsTime::yes; } + +private: + template + static auto data (This& t) -> decltype (t.data()) + { + return unalignedPointerCast (t.contents.data()); + } + + // Atoms are required to be 64-bit aligned + SingleSizeAlignedStorage<8> contents; + lv2_shared::AtomForge forge; + LV2_Atom_Forge_Frame frame; + SupportsTime time = SupportsTime::no; +}; + +struct FreeString { void operator() (void* ptr) const noexcept { lilv_free (ptr); } }; + +static File bundlePathFromUri (const char* uri) +{ + return File { std::unique_ptr { lilv_file_uri_parse (uri, nullptr) }.get() }; +} + +class Plugins +{ +public: + explicit Plugins (const LilvPlugins* list) noexcept : plugins (list) {} + + unsigned size() const noexcept { return lilv_plugins_size (plugins); } + + PluginsIterator begin() const noexcept { return PluginsIterator { plugins }; } + PluginsIterator end() const noexcept { return PluginsIterator{}; } + + const LilvPlugin* getByUri (const NodeUri& uri) const + { + return lilv_plugins_get_by_uri (plugins, uri.get()); + } + + const LilvPlugin* getByFile (const File& file) const + { + for (const auto* plugin : *this) + { + if (bundlePathFromUri (lilv_node_as_uri (lilv_plugin_get_bundle_uri (plugin))) == file) + return plugin; + } + + return nullptr; + } + +private: + const LilvPlugins* plugins = nullptr; +}; + +template +class PluginClassesImpl +{ +public: + using type = typename PtrTraits::type; + + explicit PluginClassesImpl (type ptr) + : classes (std::move (ptr)) {} + + unsigned size() const noexcept { return lilv_plugin_classes_size (PtrTraits::get (classes)); } + + PluginClassesIterator begin() const noexcept { return PluginClassesIterator { PtrTraits::get (classes) }; } + PluginClassesIterator end() const noexcept { return PluginClassesIterator{}; } + + const LilvPluginClass* getByUri (const NodeUri& uri) const noexcept + { + return lilv_plugin_classes_get_by_uri (PtrTraits::get (classes), uri.get()); + } + +private: + type classes{}; +}; + +struct PluginClassesFree +{ + void operator() (LilvPluginClasses* ptr) const noexcept { lilv_plugin_classes_free (ptr); } +}; + +using OwningPluginClasses = PluginClassesImpl>; +using NonOwningPluginClasses = PluginClassesImpl>; + +class World +{ +public: + World() : world (lilv_world_new()) {} + + void loadAllFromPaths (const NodeString& paths) + { + lilv_world_set_option (world.get(), LILV_OPTION_LV2_PATH, paths.get()); + lilv_world_load_all (world.get()); + } + + void loadBundle (const NodeUri& uri) { lilv_world_load_bundle (world.get(), uri.get()); } + void unloadBundle (const NodeUri& uri) { lilv_world_unload_bundle (world.get(), uri.get()); } + + void loadResource (const NodeUri& uri) { lilv_world_load_resource (world.get(), uri.get()); } + void unloadResource (const NodeUri& uri) { lilv_world_unload_resource (world.get(), uri.get()); } + + void loadSpecifications() { lilv_world_load_specifications (world.get()); } + void loadPluginClasses() { lilv_world_load_plugin_classes (world.get()); } + + Plugins getAllPlugins() const { return Plugins { lilv_world_get_all_plugins (world.get()) }; } + NonOwningPluginClasses getPluginClasses() const { return NonOwningPluginClasses { lilv_world_get_plugin_classes (world.get()) }; } + + NodeUri newUri (const char* uri) { return NodeUri { world.get(), uri }; } + NodeUri newFileUri (const char* host, const char* path) { return NodeUri { world.get(), host, path }; } + NodeString newString (const char* str) { return NodeString { world.get(), str }; } + + bool ask (const LilvNode* subject, const LilvNode* predicate, const LilvNode* object) const + { + return lilv_world_ask (world.get(), subject, predicate, object); + } + + OwningNode get (const LilvNode* subject, const LilvNode* predicate, const LilvNode* object) const + { + return OwningNode { lilv_world_get (world.get(), subject, predicate, object) }; + } + + OwningNodes findNodes (const LilvNode* subject, const LilvNode* predicate, const LilvNode* object) const + { + return OwningNodes { lilv_world_find_nodes (world.get(), subject, predicate, object) }; + } + + LilvWorld* get() const { return world.get(); } + +private: + struct Free + { + void operator() (LilvWorld* ptr) const noexcept { lilv_world_free (ptr); } + }; + + std::unique_ptr world; +}; + +class Ports +{ +public: + static constexpr auto sequenceSize = 8192; + + template + void forEachPort (Callback&& callback) const + { + for (const auto& port : controlPorts) + callback (port.header); + + for (const auto& port : cvPorts) + callback (port.header); + + for (const auto& port : audioPorts) + callback (port.header); + + for (const auto& port : atomPorts) + callback (port.header); + } + + auto getControlPorts() { return makeSimpleSpan (controlPorts); } + auto getControlPorts() const { return makeSimpleSpan (controlPorts); } + auto getCvPorts() { return makeSimpleSpan (cvPorts); } + auto getCvPorts() const { return makeSimpleSpan (cvPorts); } + auto getAudioPorts() { return makeSimpleSpan (audioPorts); } + auto getAudioPorts() const { return makeSimpleSpan (audioPorts); } + auto getAtomPorts() { return makeSimpleSpan (atomPorts); } + auto getAtomPorts() const { return makeSimpleSpan (atomPorts); } + + static Optional getPorts (World& world, const UsefulUris& uris, const Plugin& plugin, SymbolMap& symap) + { + Ports value; + bool successful = true; + + const auto numPorts = plugin.getNumPorts(); + const auto timeNode = world.newUri (LV2_TIME__Position); + + for (uint32_t i = 0; i != numPorts; ++i) + { + const auto port = plugin.getPortByIndex (i); + + const PortHeader header { String::fromUTF8 (port.getName().getTyped()), + String::fromUTF8 (port.getSymbol().getTyped()), + i, + port.getDirection (uris) }; + + switch (port.getKind (uris)) + { + case Port::Kind::control: + { + value.controlPorts.push_back ({ header, ParameterInfo::getInfoForPort (uris, port) }); + break; + } + + case Port::Kind::cv: + value.cvPorts.push_back ({ header }); + break; + + case Port::Kind::audio: + { + value.audioPorts.push_back ({ header }); + break; + } + + case Port::Kind::atom: + { + const auto supportsTime = port.supportsEvent (timeNode.get()); + value.atomPorts.push_back ({ header, + (size_t) Ports::sequenceSize, + symap, + supportsTime ? SupportsTime::yes : SupportsTime::no }); + break; + } + + case Port::Kind::unknown: + successful = false; + break; + } + } + + for (auto& atomPort : value.atomPorts) + { + const auto port = plugin.getPortByIndex (atomPort.header.index); + const auto minSize = port.get (uris.mLV2_RESIZE_PORT__minimumSize.get()); + + if (minSize != nullptr) + atomPort.ensureSizeInBytes ((size_t) lilv_node_as_int (minSize.get())); + } + + return successful ? makeOptional (std::move (value)) : nullopt; + } + +private: + std::vector controlPorts; + std::vector cvPorts; + std::vector audioPorts; + std::vector atomPorts; +}; + +class InstanceWithSupports final : private FeaturesDataListener, + private HandleHolder +{ +public: + InstanceWithSupports (World& world, + std::unique_ptr&& symapIn, + const Plugin& plugin, + Ports portsIn, + int32_t initialBufferSize, + double sampleRate) + : symap (std::move (symapIn)), + ports (std::move (portsIn)), + features (*this, *this, initialBufferSize, lv2_host::Ports::sequenceSize, &urids), + instance (plugin, sampleRate, features.getFeatureArray()), + workerInterface (instance.getExtensionData (world.newUri (LV2_WORKER__interface))) + { + if (instance == nullptr) + return; + + for (auto& port : ports.getControlPorts()) + instance.connectPort (port.header.index, &port.currentValue); + + for (auto& port : ports.getAtomPorts()) + instance.connectPort (port.header.index, port.data()); + + for (auto& port : ports.getCvPorts()) + instance.connectPort (port.header.index, nullptr); + + for (auto& port : ports.getAudioPorts()) + instance.connectPort (port.header.index, nullptr); + + features.registerHandle (instance.getHandle()); + } + + ~InstanceWithSupports() override + { + if (instance != nullptr) + features.deregisterHandle (instance.getHandle()); + } + + std::unique_ptr symap; + const UsefulUrids urids { *symap }; + Ports ports; + FeaturesData features; + Instance instance; + Messages uiToProcessor; + +private: + LV2_Handle handle = instance == nullptr ? nullptr : instance.getHandle(); + OptionalExtension workerInterface; + + LV2_Handle getHandle() const override { return handle; } + const LV2_Worker_Interface* getWorkerInterface() const override { return workerInterface.valid ? &workerInterface.extension : nullptr; } + + LV2_Resize_Port_Status resizeCallback (uint32_t index, size_t size) override + { + if (ports.getAtomPorts().size() <= index) + return LV2_RESIZE_PORT_ERR_UNKNOWN; + + auto& port = ports.getAtomPorts()[index]; + + if (port.header.direction != Port::Direction::output) + return LV2_RESIZE_PORT_ERR_UNKNOWN; + + port.ensureSizeInBytes (size); + instance.connectPort (port.header.index, port.data()); + + return LV2_RESIZE_PORT_SUCCESS; + } + + JUCE_DECLARE_NON_COPYABLE (InstanceWithSupports) + JUCE_DECLARE_NON_MOVEABLE (InstanceWithSupports) + JUCE_LEAK_DETECTOR (InstanceWithSupports) +}; + +struct PortState +{ + const void* data; + uint32_t size; + uint32_t kind; +}; + +class PortMap +{ +public: + explicit PortMap (Ports& ports) + { + for (auto& port : ports.getControlPorts()) + symbolToControlPortMap.emplace (port.header.symbol, &port); + } + + PortState getState (const String& symbol, const StatefulPortUrids& urids) + { + if (auto* port = getControlPortForSymbol (symbol)) + return { &port->currentValue, sizeof (float), urids.Float }; + + // At time of writing, lilv_state_new_from_instance did not attempt to store + // the state of non-control ports. Perhaps that has changed? + jassertfalse; + return { nullptr, 0, 0 }; + } + + void restoreState (const String& symbol, const StatefulPortUrids& urids, PortState ps) + { + if (auto* port = getControlPortForSymbol (symbol)) + { + port->currentValue = [&]() -> float + { + if (ps.kind == urids.Float) + return getValueFrom (ps.data, ps.size); + + if (ps.kind == urids.Double) + return getValueFrom (ps.data, ps.size); + + if (ps.kind == urids.Int) + return getValueFrom (ps.data, ps.size); + + if (ps.kind == urids.Long) + return getValueFrom (ps.data, ps.size); + + jassertfalse; + return {}; + }(); + } + else + jassertfalse; // Restoring state for non-control ports is not currently supported. + } + +private: + template + static float getValueFrom (const void* data, [[maybe_unused]] uint32_t size) + { + jassert (size == sizeof (Value)); + return (float) readUnaligned (data); + } + + ControlPort* getControlPortForSymbol (const String& symbol) const + { + const auto iter = symbolToControlPortMap.find (symbol); + return iter != symbolToControlPortMap.cend() ? iter->second : nullptr; + } + + std::map symbolToControlPortMap; + JUCE_LEAK_DETECTOR (PortMap) +}; + +class PluginState +{ +public: + PluginState() = default; + + explicit PluginState (LilvState* ptr) + : state (ptr) {} + + const LilvState* get() const noexcept { return state.get(); } + + void restore (InstanceWithSupports& instance, PortMap& portMap) const + { + if (state != nullptr) + SaveRestoreHandle { instance, portMap }.restore (state.get()); + } + + std::string toString (LilvWorld* world, LV2_URID_Map* map, LV2_URID_Unmap* unmap, const char* uri) const + { + std::unique_ptr result { lilv_state_to_string (world, + map, + unmap, + state.get(), + uri, + nullptr) }; + return std::string { result.get() }; + } + + String getLabel() const + { + return String::fromUTF8 (lilv_state_get_label (state.get())); + } + + void setLabel (const String& label) + { + lilv_state_set_label (state.get(), label.toRawUTF8()); + } + + class SaveRestoreHandle + { + public: + explicit SaveRestoreHandle (InstanceWithSupports& instanceIn, PortMap& portMap) + : instance (instanceIn.instance.get()), + features (instanceIn.features.getFeatureArray()), + urids (*instanceIn.symap), + map (portMap) + {} + + PluginState save (const LilvPlugin* plugin, LV2_URID_Map* mapFeature) + { + return PluginState { lilv_state_new_from_instance (plugin, + instance, + mapFeature, + nullptr, + nullptr, + nullptr, + nullptr, + getPortValue, + this, + LV2_STATE_IS_POD | LV2_STATE_IS_PORTABLE, + features ) }; + } + + void restore (const LilvState* stateIn) + { + lilv_state_restore (stateIn, + instance, + setPortValue, + this, + 0, + features); + } + + private: + static const void* getPortValue (const char* portSymbol, + void* userData, + uint32_t* size, + uint32_t* type) + { + auto& handle = *static_cast (userData); + + const auto state = handle.map.getState (portSymbol, handle.urids); + *size = state.size; + *type = state.kind; + return state.data; + } + + static void setPortValue (const char* portSymbol, + void* userData, + const void* value, + uint32_t size, + uint32_t type) + { + const auto& handle = *static_cast (userData); + handle.map.restoreState (portSymbol, handle.urids, { static_cast (value), size, type }); + } + + LilvInstance* instance = nullptr; + const LV2_Feature* const* features = nullptr; + const StatefulPortUrids urids; + PortMap& map; + }; + +private: + struct Free + { + void operator() (LilvState* ptr) const noexcept { lilv_state_free (ptr); } + }; + + std::unique_ptr state; + + JUCE_LEAK_DETECTOR (PluginState) +}; + +/* + Wraps an LV2 UI bundle, providing access to the descriptor (if available). +*/ +struct UiDescriptorLibrary +{ + using GetDescriptor = LV2UI_Descriptor* (*) (uint32_t); + + UiDescriptorLibrary() = default; + + explicit UiDescriptorLibrary (const String& libraryPath) + : library (std::make_unique (libraryPath)), + getDescriptor (lv2_shared::wordCast (library->getFunction ("lv2ui_descriptor"))) {} + + std::shared_ptr library; + GetDescriptor getDescriptor = nullptr; +}; + +class UiDescriptorArgs +{ +public: + String libraryPath; + String uiUri; + + auto withLibraryPath (String v) const noexcept { return with (&UiDescriptorArgs::libraryPath, v); } + auto withUiUri (String v) const noexcept { return with (&UiDescriptorArgs::uiUri, v); } + +private: + UiDescriptorArgs with (String UiDescriptorArgs::* member, String value) const noexcept + { + return juce::lv2_host::with (*this, member, std::move (value)); + } +}; + +/* + Stores a pointer to the descriptor for a specific UI bundle and UI URI. +*/ +class UiDescriptor +{ +public: + UiDescriptor() = default; + + explicit UiDescriptor (const UiDescriptorArgs& args) + : library (args.libraryPath), + descriptor (extractUiDescriptor (library, args.uiUri.toRawUTF8())) + {} + + void portEvent (LV2UI_Handle ui, + uint32_t portIndex, + uint32_t bufferSize, + uint32_t format, + const void* buffer) const + { + JUCE_ASSERT_MESSAGE_THREAD + + if (auto* lv2Descriptor = get()) + if (auto* callback = lv2Descriptor->port_event) + callback (ui, portIndex, bufferSize, format, buffer); + } + + bool hasExtensionData (World& world, const char* uid) const + { + return world.ask (world.newUri (descriptor->URI).get(), + world.newUri (LV2_CORE__extensionData).get(), + world.newUri (uid).get()); + } + + template + OptionalExtension getExtensionData (World& world, const char* uid) const + { + if (! hasExtensionData (world, uid)) + return {}; + + if (auto* lv2Descriptor = get()) + if (auto* extension = lv2Descriptor->extension_data) + return OptionalExtension (readUnaligned (extension (uid))); + + return {}; + } + + const LV2UI_Descriptor* get() const noexcept { return descriptor; } + +private: + static const LV2UI_Descriptor* extractUiDescriptor (const UiDescriptorLibrary& lib, const char* uiUri) + { + if (lib.getDescriptor == nullptr) + return nullptr; + + for (uint32_t i = 0;; ++i) + { + const auto* descriptor = lib.getDescriptor (i); + + if (descriptor == nullptr) + return nullptr; + + if (strcmp (uiUri, descriptor->URI) == 0) + return descriptor; + } + } + + UiDescriptorLibrary library; + const LV2UI_Descriptor* descriptor = nullptr; + + JUCE_LEAK_DETECTOR (UiDescriptor) +}; + +enum class Update { no, yes }; + +/* A bit like the FlaggedFloatCache used by the VST3 host/client. + + While the FlaggedFloatCache always clears all set flags during the ifSet() call, + this class stores the "value changed" flags for the processor and UI separately, + so that they can be read at different rates. +*/ +class ParameterValuesAndFlags +{ +public: + ParameterValuesAndFlags() = default; + + explicit ParameterValuesAndFlags (size_t sizeIn) + : values (sizeIn), + needsUiUpdate (sizeIn), + needsProcessorUpdate (sizeIn) + { + std::fill (values.begin(), values.end(), 0.0f); + } + + size_t size() const noexcept { return values.size(); } + + void set (size_t index, float value, Update update) + { + jassert (index < size()); + values[index].store (value, std::memory_order_relaxed); + needsUiUpdate .set (index, update == Update::yes ? 1 : 0); + needsProcessorUpdate.set (index, update == Update::yes ? 1 : 0); + } + + float get (size_t index) const noexcept + { + jassert (index < size()); + return values[index].load (std::memory_order_relaxed); + } + + template + void ifProcessorValuesChanged (Callback&& callback) + { + ifChanged (needsProcessorUpdate, std::forward (callback)); + } + + template + void ifUiValuesChanged (Callback&& callback) + { + ifChanged (needsUiUpdate, std::forward (callback)); + } + + void clearUiFlags() { needsUiUpdate.clear(); } + +private: + template + void ifChanged (FlagCache<1>& flags, Callback&& callback) + { + flags.ifSet ([this, &callback] (size_t groupIndex, uint32_t) + { + callback (groupIndex, values[groupIndex].load (std::memory_order_relaxed)); + }); + } + + std::vector> values; + FlagCache<1> needsUiUpdate; + FlagCache<1> needsProcessorUpdate; + + JUCE_LEAK_DETECTOR (ParameterValuesAndFlags) +}; + +class LV2Parameter : public AudioPluginInstance::HostedParameter +{ +public: + LV2Parameter (const String& nameIn, + const ParameterInfo& infoIn, + ParameterValuesAndFlags& floatCache) + : cache (floatCache), + info (infoIn), + range (info.min, info.max), + name (nameIn), + normalisedDefault (range.convertTo0to1 (infoIn.defaultValue)) + {} + + float getValue() const noexcept override + { + return range.convertTo0to1 (getDenormalisedValue()); + } + + void setValue (float f) override + { + cache.set ((size_t) getParameterIndex(), range.convertFrom0to1 (f), Update::yes); + } + + void setDenormalisedValue (float denormalised) + { + cache.set ((size_t) getParameterIndex(), denormalised, Update::yes); + sendValueChangedMessageToListeners (range.convertTo0to1 (denormalised)); + } + + void setDenormalisedValueWithoutTriggeringUpdate (float denormalised) + { + cache.set ((size_t) getParameterIndex(), denormalised, Update::no); + sendValueChangedMessageToListeners (range.convertTo0to1 (denormalised)); + } + + float getDenormalisedValue() const noexcept + { + return cache.get ((size_t) getParameterIndex()); + } + + float getDefaultValue() const override { return normalisedDefault; } + float getDenormalisedDefaultValue() const { return info.defaultValue; } + + float getValueForText (const String& text) const override + { + if (! info.isEnum) + return range.convertTo0to1 (text.getFloatValue()); + + const auto it = std::find_if (info.scalePoints.begin(), + info.scalePoints.end(), + [&] (const StoredScalePoint& stored) { return stored.label == text; }); + return it != info.scalePoints.end() ? range.convertTo0to1 (it->value) : normalisedDefault; + } + + int getNumSteps() const override + { + if (info.isToggle) + return 2; + + if (info.isEnum) + return static_cast (info.scalePoints.size()); + + if (info.isInteger) + return static_cast (range.getRange().getLength()) + 1; + + return AudioProcessorParameter::getNumSteps(); + } + + bool isDiscrete() const override { return info.isEnum || info.isInteger || info.isToggle; } + bool isBoolean() const override { return info.isToggle; } + + StringArray getAllValueStrings() const override + { + if (! info.isEnum) + return {}; + + return AudioProcessorParameter::getAllValueStrings(); + } + + String getText (float normalisedValue, int) const override + { + const auto denormalised = range.convertFrom0to1 (normalisedValue); + + if (info.isEnum && ! info.scalePoints.empty()) + { + // The normalised value might not correspond to the exact value of a scale point. + // In this case, we find the closest label by searching the midpoints of the scale + // point values. + const auto index = std::distance (midPoints.begin(), + std::lower_bound (midPoints.begin(), midPoints.end(), denormalised)); + jassert (isPositiveAndBelow (index, info.scalePoints.size())); + return info.scalePoints[(size_t) index].label; + } + + return getFallbackParameterString (denormalised); + } + + String getParameterID() const override + { + return info.identifier; + } + + String getName (int maxLength) const override + { + return name.substring (0, maxLength); + } + + String getLabel() const override + { + // TODO + return {}; + } + +private: + String getFallbackParameterString (float denormalised) const + { + if (info.isToggle) + return denormalised > 0.0f ? "On" : "Off"; + + if (info.isInteger) + return String { static_cast (denormalised) }; + + return String { denormalised }; + } + + static std::vector findScalePointMidPoints (const SafeSortedSet& set) + { + if (set.size() < 2) + return {}; + + std::vector result; + result.reserve (set.size() - 1); + + for (auto it = std::next (set.begin()); it != set.end(); ++it) + result.push_back ((std::prev (it)->value + it->value) * 0.5f); + + jassert (std::is_sorted (result.begin(), result.end())); + jassert (result.size() + 1 == set.size()); + return result; + } + + ParameterValuesAndFlags& cache; + const ParameterInfo info; + const std::vector midPoints = findScalePointMidPoints (info.scalePoints); + const NormalisableRange range; + const String name; + const float normalisedDefault; + + JUCE_LEAK_DETECTOR (LV2Parameter) +}; + +class UiInstanceArgs +{ +public: + File bundlePath; + URL pluginUri; + + auto withBundlePath (File v) const noexcept { return withMember (*this, &UiInstanceArgs::bundlePath, std::move (v)); } + auto withPluginUri (URL v) const noexcept { return withMember (*this, &UiInstanceArgs::pluginUri, std::move (v)); } +}; + +struct TouchListener +{ + virtual ~TouchListener() = default; + virtual void controlGrabbed (uint32_t port, bool grabbed) = 0; +}; + +class UiFeaturesDataOptions +{ +public: + float initialScaleFactor = 0.0f, sampleRate = 0.0f; + + auto withInitialScaleFactor (float v) const { return with (&UiFeaturesDataOptions::initialScaleFactor, v); } + auto withSampleRate (float v) const { return with (&UiFeaturesDataOptions::sampleRate, v); } + +private: + UiFeaturesDataOptions with (float UiFeaturesDataOptions::* member, float value) const + { + return juce::lv2_host::with (*this, member, value); + } +}; + +class UiFeaturesData +{ +public: + UiFeaturesData (PhysicalResizeListener& rl, + TouchListener& tl, + LV2_Handle instanceIn, + LV2UI_Widget parentIn, + Instance::GetExtensionData getExtensionData, + const Ports& ports, + SymbolMap& symapIn, + const UiFeaturesDataOptions& optIn) + : opts (optIn), + resizeListener (rl), + touchListener (tl), + instance (instanceIn), + parent (parentIn), + symap (symapIn), + dataAccess { getExtensionData }, + portIndices (makePortIndices (ports)) + { + } + + const LV2_Feature* const* getFeatureArray() const noexcept { return features.pointers.data(); } + + static std::vector getFeatureUris() + { + return Features::getUris (makeFeatures ({}, {}, {}, {}, {}, {}, {}, {}, {}, {})); + } + + Rectangle getLastRequestedBounds() const { return { lastRequestedWidth, lastRequestedHeight }; } + +private: + static std::vector makeFeatures (LV2UI_Resize* resize, + LV2UI_Widget parent, + LV2_Handle handle, + LV2_Extension_Data_Feature* data, + LV2_URID_Map* map, + LV2_URID_Unmap* unmap, + LV2UI_Port_Map* portMap, + LV2UI_Touch* touch, + LV2_Options_Option* options, + LV2_Log_Log* log) + { + return { LV2_Feature { LV2_UI__resize, resize }, + LV2_Feature { LV2_UI__parent, parent }, + LV2_Feature { LV2_UI__idleInterface, nullptr }, + LV2_Feature { LV2_INSTANCE_ACCESS_URI, handle }, + LV2_Feature { LV2_DATA_ACCESS_URI, data }, + LV2_Feature { LV2_URID__map, map }, + LV2_Feature { LV2_URID__unmap, unmap}, + LV2_Feature { LV2_UI__portMap, portMap }, + LV2_Feature { LV2_UI__touch, touch }, + LV2_Feature { LV2_OPTIONS__options, options }, + LV2_Feature { LV2_LOG__log, log } }; + } + + int resizeCallback (int width, int height) + { + lastRequestedWidth = width; + lastRequestedHeight = height; + resizeListener.viewRequestedResizeInPhysicalPixels (width, height); + return 0; + } + + static int resizeCallback (LV2UI_Feature_Handle handle, int width, int height) + { + return static_cast (handle)->resizeCallback (width, height); + } + + uint32_t portIndexCallback (const char* symbol) const + { + const auto it = portIndices.find (symbol); + return it != portIndices.cend() ? it->second : LV2UI_INVALID_PORT_INDEX; + } + + static uint32_t portIndexCallback (LV2UI_Feature_Handle handle, const char* symbol) + { + return static_cast (handle)->portIndexCallback (symbol); + } + + void touchCallback (uint32_t portIndex, bool grabbed) const + { + touchListener.controlGrabbed (portIndex, grabbed); + } + + static void touchCallback (LV2UI_Feature_Handle handle, uint32_t index, bool b) + { + return static_cast (handle)->touchCallback (index, b); + } + + static std::map makePortIndices (const Ports& ports) + { + std::map result; + + ports.forEachPort ([&] (const PortHeader& header) + { + [[maybe_unused]] const auto emplaced = result.emplace (header.symbol, header.index); + + // This will complain if there are duplicate port symbols. + jassert (emplaced.second); + }); + + return result; + } + + const UiFeaturesDataOptions opts; + PhysicalResizeListener& resizeListener; + TouchListener& touchListener; + LV2_Handle instance{}; + LV2UI_Widget parent{}; + SymbolMap& symap; + const UsefulUrids urids { symap }; + Log log { &urids }; + int lastRequestedWidth = 0, lastRequestedHeight = 0; + std::vector options { { LV2_OPTIONS_INSTANCE, + 0, + symap.map (LV2_UI__scaleFactor), + sizeof (float), + symap.map (LV2_ATOM__Float), + &opts.initialScaleFactor }, + { LV2_OPTIONS_INSTANCE, + 0, + symap.map (LV2_PARAMETERS__sampleRate), + sizeof (float), + symap.map (LV2_ATOM__Float), + &opts.sampleRate }, + { LV2_OPTIONS_INSTANCE, 0, 0, 0, 0, nullptr } }; // The final entry must be nulled out + LV2UI_Resize resize { this, resizeCallback }; + LV2_URID_Map map = symap.getMapFeature(); + LV2_URID_Unmap unmap = symap.getUnmapFeature(); + LV2UI_Port_Map portMap { this, portIndexCallback }; + LV2UI_Touch touch { this, touchCallback }; + LV2_Extension_Data_Feature dataAccess; + std::map portIndices; + Features features { makeFeatures (&resize, + parent, + instance, + &dataAccess, + &map, + &unmap, + &portMap, + &touch, + options.data(), + log.getLogFeature()) }; + + JUCE_LEAK_DETECTOR (UiFeaturesData) +}; + +struct RequiredFeatures +{ + explicit RequiredFeatures (OwningNodes nodes) + : values (std::move (nodes)) {} + + OwningNodes values; +}; + +struct OptionalFeatures +{ + explicit OptionalFeatures (OwningNodes nodes) + : values (std::move (nodes)) {} + + OwningNodes values; +}; + +template +static bool noneOf (Range&& range, Predicate&& pred) +{ + // Not a mistake, this is for ADL + using std::begin; + using std::end; + return std::none_of (begin (range), end (range), std::forward (pred)); +} + +class Uis +{ +public: + explicit Uis (const LilvPlugin* plugin) noexcept : uis (lilv_plugin_get_uis (plugin)) {} + + unsigned size() const noexcept { return lilv_uis_size (uis.get()); } + + UisIterator begin() const noexcept { return UisIterator { uis.get() }; } + UisIterator end() const noexcept { return UisIterator{}; } + + const LilvUI* getByUri (const NodeUri& uri) const + { + return lilv_uis_get_by_uri (uis.get(), uri.get()); + } + +private: + struct Free + { + void operator() (LilvUIs* ptr) const noexcept { lilv_uis_free (ptr); } + }; + + std::unique_ptr uis; +}; + +//============================================================================== +class PluginClass +{ +public: + explicit PluginClass (const LilvPluginClass* c) : pluginClass (c) {} + + NodeUri getParentUri() const noexcept { return NodeUri::copy (lilv_plugin_class_get_parent_uri (pluginClass)); } + NodeUri getUri() const noexcept { return NodeUri::copy (lilv_plugin_class_get_uri (pluginClass)); } + NodeString getLabel() const noexcept { return NodeString::copy (lilv_plugin_class_get_label (pluginClass)); } + OwningPluginClasses getChildren() const noexcept + { + return OwningPluginClasses { OwningPluginClasses::type { lilv_plugin_class_get_children (pluginClass) } }; + } + +private: + const LilvPluginClass* pluginClass = nullptr; +}; + +using FloatWriter = void (*) (LV2_Atom_Forge*, float); + +struct ParameterWriterUrids +{ + LV2_URID mLV2_PATCH__Set; + LV2_URID mLV2_PATCH__property; + LV2_URID mLV2_PATCH__value; + LV2_URID mLV2_ATOM__eventTransfer; +}; + +struct MessageHeaderAndSize +{ + MessageHeader header; + uint32_t size; +}; + +class ParameterWriter +{ +public: + ParameterWriter (ControlPort* p) + : data (PortBacking { p }), kind (Kind::port) {} + + ParameterWriter (FloatWriter write, LV2_URID urid, uint32_t controlPortIndex) + : data (PatchBacking { write, urid, controlPortIndex }), kind (Kind::patch) {} + + void writeToProcessor (const ParameterWriterUrids urids, LV2_Atom_Forge* forge, float value) const + { + switch (kind) + { + case Kind::patch: + { + if (forge != nullptr) + { + lv2_atom_forge_frame_time (forge, 0); + writeSetToForge (urids, *forge, value); + } + + break; + } + + case Kind::port: + data.port.port->currentValue = value; + break; + } + } + + MessageHeaderAndSize writeToUi (const ParameterWriterUrids urids, LV2_Atom_Forge& forge, float value) const + { + const auto getWrittenBytes = [&]() -> uint32_t + { + if (const auto* atom = convertToAtomPtr (forge.buf, forge.size)) + return (uint32_t) (atom->size + sizeof (LV2_Atom)); + + jassertfalse; + return 0; + }; + + switch (kind) + { + case Kind::patch: + writeSetToForge (urids, forge, value); + return { { data.patch.controlPortIndex, urids.mLV2_ATOM__eventTransfer }, getWrittenBytes() }; + + case Kind::port: + lv2_atom_forge_raw (&forge, &value, sizeof (value)); + return { { data.port.port->header.index, 0 }, sizeof (value) }; + } + + return { { 0, 0 }, 0 }; + } + + const LV2_URID* getUrid() const + { + return kind == Kind::patch ? &data.patch.urid : nullptr; + } + + const uint32_t* getPortIndex() const + { + return kind == Kind::port ? &data.port.port->header.index : nullptr; + } + +private: + void writeSetToForge (const ParameterWriterUrids urids, LV2_Atom_Forge& forge, float value) const + { + lv2_shared::ObjectFrame object { &forge, (uint32_t) 0, urids.mLV2_PATCH__Set }; + + lv2_atom_forge_key (&forge, urids.mLV2_PATCH__property); + lv2_atom_forge_urid (&forge, data.patch.urid); + + lv2_atom_forge_key (&forge, urids.mLV2_PATCH__value); + data.patch.write (&forge, value); + } + + struct PortBacking + { + ControlPort* port; + }; + + struct PatchBacking + { + FloatWriter write; + LV2_URID urid; + uint32_t controlPortIndex; + }; + + union Data + { + static_assert (std::is_trivial_v, "PortBacking must be trivial"); + static_assert (std::is_trivial_v, "PatchBacking must be trivial"); + + explicit Data (PortBacking p) : port (p) {} + explicit Data (PatchBacking p) : patch (p) {} + + PortBacking port; + PatchBacking patch; + }; + + enum class Kind { port, patch }; + + Data data; + Kind kind; + + JUCE_LEAK_DETECTOR (ParameterWriter) +}; + +static String lilvNodeToUriString (const LilvNode* node) +{ + return node != nullptr ? String::fromUTF8 (lilv_node_as_uri (node)) : String{}; +} + +static String lilvNodeToString (const LilvNode* node) +{ + return node != nullptr ? String::fromUTF8 (lilv_node_as_string (node)) : String{}; +} + +/* This holds all of the discovered groups in the plugin's manifest, and allows us to + add parameters to these groups as we discover them. + + Once all the parameters have been added with addParameter(), you can call + getTree() to convert this class' contents (which are optimised for fast lookup + and modification) into a plain old AudioProcessorParameterGroup. +*/ +class IntermediateParameterTree +{ +public: + explicit IntermediateParameterTree (World& worldIn) + : world (worldIn) + { + const auto groups = getGroups (world); + const auto symbolNode = world.newUri (LV2_CORE__symbol); + const auto nameNode = world.newUri (LV2_CORE__name); + + for (const auto& group : groups) + { + const auto symbol = lilvNodeToString (world.get (group.get(), symbolNode.get(), nullptr).get()); + const auto name = lilvNodeToString (world.get (group.get(), nameNode .get(), nullptr).get()); + owning.emplace (lilvNodeToUriString (group.get()), + std::make_unique (symbol, name, "|")); + } + } + + void addParameter (StringRef group, std::unique_ptr param) + { + if (param == nullptr) + return; + + const auto it = owning.find (group); + (it != owning.cend() ? *it->second : topLevel).addChild (std::move (param)); + } + + static AudioProcessorParameterGroup getTree (IntermediateParameterTree tree) + { + std::map nonowning; + + for (const auto& pair : tree.owning) + nonowning.emplace (pair.first, pair.second.get()); + + const auto groups = getGroups (tree.world); + const auto subgroupNode = tree.world.newUri (LV2_PORT_GROUPS__subGroupOf); + + for (const auto& group : groups) + { + const auto innerIt = tree.owning.find (lilvNodeToUriString (group.get())); + + if (innerIt == tree.owning.cend()) + continue; + + const auto outer = lilvNodeToUriString (tree.world.get (group.get(), subgroupNode.get(), nullptr).get()); + const auto outerIt = nonowning.find (outer); + + if (outerIt != nonowning.cend() && containsParameters (outerIt->second)) + outerIt->second->addChild (std::move (innerIt->second)); + } + + for (auto& subgroup : tree.owning) + if (containsParameters (subgroup.second.get())) + tree.topLevel.addChild (std::move (subgroup.second)); + + return std::move (tree.topLevel); + } + +private: + static std::vector getGroups (World& world) + { + std::vector names; + + for (auto* uri : { LV2_PORT_GROUPS__Group, LV2_PORT_GROUPS__InputGroup, LV2_PORT_GROUPS__OutputGroup }) + for (const auto* group : world.findNodes (nullptr, world.newUri (LILV_NS_RDF "type").get(), world.newUri (uri).get())) + names.push_back (OwningNode { lilv_node_duplicate (group) }); + + return names; + } + + static bool containsParameters (const AudioProcessorParameterGroup* g) + { + if (g == nullptr) + return false; + + for (auto* node : *g) + { + if (node->getParameter() != nullptr) + return true; + + if (auto* group = node->getGroup()) + if (containsParameters (group)) + return true; + } + + return false; + } + + World& world; + AudioProcessorParameterGroup topLevel; + std::map> owning; + + JUCE_LEAK_DETECTOR (IntermediateParameterTree) +}; + +struct BypassParameter final : public LV2Parameter +{ + BypassParameter (const ParameterInfo& parameterInfo, ParameterValuesAndFlags& cacheIn) + : LV2Parameter ("Bypass", parameterInfo, cacheIn) {} + + float getValue() const noexcept override + { + return LV2Parameter::getValue() > 0.0f ? 0.0f : 1.0f; + } + + void setValue (float newValue) override + { + LV2Parameter::setValue (newValue > 0.0f ? 0.0f : 1.0f); + } + + float getDefaultValue() const override { return 0.0f; } + bool isAutomatable() const override { return true; } + bool isDiscrete() const override { return true; } + bool isBoolean() const override { return true; } + int getNumSteps() const override { return 2; } + StringArray getAllValueStrings() const override { return { TRANS ("Off"), TRANS ("On") }; } +}; + +struct ParameterData +{ + ParameterInfo info; + ParameterWriter writer; + String group; + String name; +}; + +template +static auto getPortPointers (SimpleSpan range) +{ + using std::begin; + std::vector result; + + for (auto& port : range) + { + result.resize (std::max ((size_t) (port.header.index + 1), result.size()), nullptr); + result[port.header.index] = &port; + } + + return result; +} + +static std::unique_ptr makeParameter (const uint32_t* enabledPortIndex, + const ParameterData& data, + ParameterValuesAndFlags& cache) +{ + // The bypass parameter is a bit special, in that JUCE expects the parameter to be a bypass + // (where 0 is active, 1 is inactive), but the LV2 version is called "enabled" and has + // different semantics (0 is inactive, 1 is active). + // To work around this, we wrap the LV2 parameter in a special inverting JUCE parameter. + + if (enabledPortIndex != nullptr) + if (auto* index = data.writer.getPortIndex()) + if (*index == *enabledPortIndex) + return std::make_unique (data.info, cache); + + return std::make_unique (data.name, data.info, cache); +} + +class ControlPortAccelerationStructure +{ +public: + ControlPortAccelerationStructure (SimpleSpan controlPorts) + : indexedControlPorts (getPortPointers (controlPorts)) + { + for (const auto& port : controlPorts) + if (port.header.direction == Port::Direction::output) + outputPorts.push_back (&port); + } + + const std::vector& getIndexedControlPorts() { return indexedControlPorts; } + + ControlPort* getControlPortByIndex (uint32_t index) const + { + if (isPositiveAndBelow (index, indexedControlPorts.size())) + return indexedControlPorts[index]; + + return nullptr; + } + + void writeOutputPorts (UiEventListener* target, MessageBufferInterface& uiMessages) const + { + if (target == nullptr) + return; + + for (const auto* port : outputPorts) + { + const auto chars = toChars (port->currentValue); + uiMessages.pushMessage ({ target, { port->header.index, 0 } }, (uint32_t) chars.size(), chars.data()); + } + } + +private: + std::vector indexedControlPorts; + std::vector outputPorts; +}; + +class ParameterValueCache +{ +public: + /* This takes some information about all the parameters that this plugin wants to expose, + then builds and installs the actual parameters. + */ + ParameterValueCache (AudioPluginInstance& processor, + World& world, + LV2_URID_Map mapFeature, + const std::vector& data, + ControlPort* enabledPort) + : uiForge (mapFeature), + cache (data.size()) + { + // Parameter indices are unknown until we add the parameters to the processor. + // This map lets us keep track of which ParameterWriter corresponds to each parameter. + // After the parameters have been added to the processor, we'll convert this + // to a simple vector that stores each ParameterWriter at the same index + // as the corresponding parameter. + std::map writerForParameter; + + IntermediateParameterTree tree { world }; + + const auto* enabledPortIndex = enabledPort != nullptr ? &enabledPort->header.index + : nullptr; + + for (const auto& item : data) + { + auto param = makeParameter (enabledPortIndex, item, cache); + + if (auto* urid = item.writer.getUrid()) + urids.emplace (*urid, param.get()); + + if (auto* index = item.writer.getPortIndex()) + portIndices.emplace (*index, param.get()); + + writerForParameter.emplace (param.get(), item.writer); + + tree.addParameter (item.group, std::move (param)); + } + + processor.setHostedParameterTree (IntermediateParameterTree::getTree (std::move (tree))); + + // Build the vector of writers + writers.reserve (data.size()); + + for (auto* param : processor.getParameters()) + { + const auto it = writerForParameter.find (param); + jassert (it != writerForParameter.end()); + writers.push_back (it->second); // The writer must exist at the same index as the parameter! + } + + // Duplicate port indices or urids? + jassert (processor.getParameters().size() == (int) (urids.size() + portIndices.size())); + + // Set parameters to default values + const auto setToDefault = [] (auto& container) + { + for (auto& item : container) + item.second->setDenormalisedValueWithoutTriggeringUpdate (item.second->getDenormalisedDefaultValue()); + }; + + setToDefault (urids); + setToDefault (portIndices); + } + + void postChangedParametersToProcessor (const ParameterWriterUrids helperUrids, + LV2_Atom_Forge* forge) + { + cache.ifProcessorValuesChanged ([&] (size_t index, float value) + { + writers[index].writeToProcessor (helperUrids, forge, value); + }); + } + + void postChangedParametersToUi (UiEventListener* target, + const ParameterWriterUrids helperUrids, + MessageBufferInterface& uiMessages) + { + if (target == nullptr) + return; + + cache.ifUiValuesChanged ([&] (size_t index, float value) + { + writeParameterToUi (target, writers[index], value, helperUrids, uiMessages); + }); + } + + void postAllParametersToUi (UiEventListener* target, + const ParameterWriterUrids helperUrids, + MessageBufferInterface& uiMessages) + { + if (target == nullptr) + return; + + const auto numWriters = writers.size(); + + for (size_t i = 0; i < numWriters; ++i) + writeParameterToUi (target, writers[i], cache.get (i), helperUrids, uiMessages); + + cache.clearUiFlags(); + } + + LV2Parameter* getParamByUrid (LV2_URID urid) const + { + const auto it = urids.find (urid); + return it != urids.end() ? it->second : nullptr; + } + + LV2Parameter* getParamByPortIndex (uint32_t portIndex) const + { + const auto it = portIndices.find (portIndex); + return it != portIndices.end() ? it->second : nullptr; + } + + void updateFromControlPorts (const ControlPortAccelerationStructure& ports) const + { + for (const auto& pair : portIndices) + if (auto* port = ports.getControlPortByIndex (pair.first)) + if (auto* param = pair.second) + param->setDenormalisedValueWithoutTriggeringUpdate (port->currentValue); + } + +private: + void writeParameterToUi (UiEventListener* target, + const ParameterWriter& writer, + float value, + const ParameterWriterUrids helperUrids, + MessageBufferInterface& uiMessages) + { + JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED + + uiForge.setBuffer (forgeStorage.data(), forgeStorage.size()); + const auto messageHeader = writer.writeToUi (helperUrids, *uiForge.get(), value); + uiMessages.pushMessage ({ target, messageHeader.header }, messageHeader.size, forgeStorage.data()); + } + + SingleSizeAlignedStorage<8> forgeStorage { 256 }; + lv2_shared::AtomForge uiForge; + + ParameterValuesAndFlags cache; + std::vector writers; + std::map urids; + std::map portIndices; + + JUCE_LEAK_DETECTOR (ParameterValueCache) +}; + +struct PatchSetCallback +{ + explicit PatchSetCallback (ParameterValueCache& x) : cache (x) {} + + // If we receive a patch set from the processor, we can assume that the UI will + // put itself into the correct state when it receives the message. + void setParameter (LV2_URID property, float value) const noexcept + { + if (auto* param = cache.getParamByUrid (property)) + param->setDenormalisedValueWithoutTriggeringUpdate (value); + } + + // TODO gesture support will probably go here, once it's part of the LV2 spec + + ParameterValueCache& cache; +}; + +struct SupportedParameter +{ + ParameterInfo info; + bool supported; + LV2_URID type; +}; + +static SupportedParameter getInfoForPatchParameter (World& worldIn, + const UsefulUrids& urids, + const NodeUri& property) +{ + const auto rangeUri = worldIn.newUri (LILV_NS_RDFS "range"); + const auto type = worldIn.get (property.get(), rangeUri.get(), nullptr); + + if (type == nullptr) + return { {}, false, {} }; + + const auto typeUrid = urids.symap.map (lilv_node_as_uri (type.get())); + + const LV2_URID types[] { urids.mLV2_ATOM__Int, + urids.mLV2_ATOM__Long, + urids.mLV2_ATOM__Float, + urids.mLV2_ATOM__Double, + urids.mLV2_ATOM__Bool }; + + if (std::find (std::begin (types), std::end (types), typeUrid) == std::end (types)) + return { {}, false, {} }; + + const auto getValue = [&] (const char* uri, float fallback) + { + return Port::getFloatValue (worldIn.get (property.get(), worldIn.newUri (uri).get(), nullptr).get(), fallback); + }; + + const auto hasPortProperty = [&] (const char* uri) + { + return worldIn.ask (property.get(), + worldIn.newUri (LV2_CORE__portProperty).get(), + worldIn.newUri (uri).get()); + }; + + const auto metadataScalePoints = worldIn.findNodes (property.get(), + worldIn.newUri (LV2_CORE__scalePoint).get(), + nullptr); + SafeSortedSet parsedScalePoints; + + for (const auto* scalePoint : metadataScalePoints) + { + const auto label = worldIn.get (scalePoint, worldIn.newUri (LILV_NS_RDFS "label").get(), nullptr); + const auto value = worldIn.get (scalePoint, worldIn.newUri (LILV_NS_RDF "value").get(), nullptr); + + if (label != nullptr && value != nullptr) + parsedScalePoints.insert ({ lilv_node_as_string (label.get()), lilv_node_as_float (value.get()) }); + else + jassertfalse; // A ScalePoint must have both a rdfs:label and a rdf:value + } + + const auto minimum = getValue (LV2_CORE__minimum, 0.0f); + const auto maximum = getValue (LV2_CORE__maximum, 1.0f); + + return { { std::move (parsedScalePoints), + "des:" + String::fromUTF8 (property.getTyped()), + getValue (LV2_CORE__default, (minimum + maximum) * 0.5f), + minimum, + maximum, + typeUrid == urids.mLV2_ATOM__Bool || hasPortProperty (LV2_CORE__toggled), + typeUrid == urids.mLV2_ATOM__Int || typeUrid == urids.mLV2_ATOM__Long, + hasPortProperty (LV2_CORE__enumeration) }, + true, + typeUrid }; +} + +static std::vector getPortBasedParameters (World& world, + const Plugin& plugin, + std::initializer_list hiddenPorts, + SimpleSpan controlPorts) +{ + std::vector result; + + const auto groupNode = world.newUri (LV2_PORT_GROUPS__group); + + for (auto& port : controlPorts) + { + if (port.header.direction != Port::Direction::input) + continue; + + if (std::find (std::begin (hiddenPorts), std::end (hiddenPorts), &port) != std::end (hiddenPorts)) + continue; + + const auto lilvPort = plugin.getPortByIndex (port.header.index); + const auto group = lilvNodeToUriString (lilvPort.get (groupNode.get()).get()); + + result.push_back ({ port.info, ParameterWriter { &port }, group, port.header.name }); + } + + return result; +} + +static void writeFloatToForge (LV2_Atom_Forge* forge, float value) { lv2_atom_forge_float (forge, value); } +static void writeDoubleToForge (LV2_Atom_Forge* forge, float value) { lv2_atom_forge_double (forge, (double) value); } +static void writeIntToForge (LV2_Atom_Forge* forge, float value) { lv2_atom_forge_int (forge, (int32_t) value); } +static void writeLongToForge (LV2_Atom_Forge* forge, float value) { lv2_atom_forge_long (forge, (int64_t) value); } +static void writeBoolToForge (LV2_Atom_Forge* forge, float value) { lv2_atom_forge_bool (forge, value > 0.5f); } + +static std::vector getPatchBasedParameters (World& world, + const Plugin& plugin, + const UsefulUrids& urids, + uint32_t controlPortIndex) +{ + // This returns our writable parameters in an indeterminate order. + // We want our parameters to be in a consistent order between runs, so + // we'll create all the parameters in one pass, sort them, and then + // add them in a separate pass. + const auto writableControls = world.findNodes (plugin.getUri().get(), + world.newUri (LV2_PATCH__writable).get(), + nullptr); + + struct DataAndUri + { + ParameterData data; + String uri; + }; + + std::vector resultWithUris; + + const auto groupNode = world.newUri (LV2_PORT_GROUPS__group); + + for (auto* ctrl : writableControls) + { + const auto labelString = [&] + { + if (auto label = world.get (ctrl, world.newUri (LILV_NS_RDFS "label").get(), nullptr)) + return String::fromUTF8 (lilv_node_as_string (label.get())); + + return String(); + }(); + + const auto uri = String::fromUTF8 (lilv_node_as_uri (ctrl)); + const auto info = getInfoForPatchParameter (world, urids, world.newUri (uri.toRawUTF8())); + + if (! info.supported) + continue; + + const auto write = [&] + { + if (info.type == urids.mLV2_ATOM__Int) + return writeIntToForge; + + if (info.type == urids.mLV2_ATOM__Long) + return writeLongToForge; + + if (info.type == urids.mLV2_ATOM__Double) + return writeDoubleToForge; + + if (info.type == urids.mLV2_ATOM__Bool) + return writeBoolToForge; + + return writeFloatToForge; + }(); + + const auto group = lilvNodeToUriString (world.get (ctrl, groupNode.get(), nullptr).get()); + resultWithUris.push_back ({ { info.info, + ParameterWriter { write, urids.symap.map (uri.toRawUTF8()), controlPortIndex }, + group, + labelString }, + uri }); + } + + const auto compareUris = [] (const DataAndUri& a, const DataAndUri& b) { return a.uri < b.uri; }; + std::sort (resultWithUris.begin(), resultWithUris.end(), compareUris); + + std::vector result; + + for (const auto& item : resultWithUris) + result.push_back (item.data); + + return result; +} + +static std::vector getJuceParameterInfo (World& world, + const Plugin& plugin, + const UsefulUrids& urids, + std::initializer_list hiddenPorts, + SimpleSpan controlPorts, + uint32_t controlPortIndex) +{ + auto port = getPortBasedParameters (world, plugin, hiddenPorts, controlPorts); + auto patch = getPatchBasedParameters (world, plugin, urids, controlPortIndex); + + port.insert (port.end(), patch.begin(), patch.end()); + return port; +} + +// Rather than sprinkle #ifdef everywhere, risking the wrath of the entire C++ +// standards committee, we put all of our conditionally-compiled stuff into a +// specialised template that compiles away to nothing when editor support is +// not available. +#if JUCE_MAC || JUCE_WINDOWS || JUCE_LINUX || JUCE_BSD + constexpr auto editorFunctionalityEnabled = true; +#else + constexpr auto editorFunctionalityEnabled = false; +#endif + +//============================================================================== +class LV2AudioPluginInstanceHeadless : public AudioPluginInstance +{ +public: + LV2AudioPluginInstanceHeadless (std::shared_ptr worldIn, + const Plugin& pluginIn, + const UsefulUris& uris, + std::unique_ptr&& in, + PluginDescription&& desc, + std::vector knownPresetUris, + PluginState stateToApply, + String, + UiDescriptor) + : LV2AudioPluginInstanceHeadless (worldIn, + pluginIn, + std::move (in), + std::move (desc), + std::move (knownPresetUris), + std::move (stateToApply), + getParsedBuses (*worldIn, pluginIn, uris)) + { + } + + void fillInPluginDescription (PluginDescription& d) const override { d = description; } + + const String getName() const override { return description.name; } + + void prepareToPlay (double sampleRate, int numSamples) override + { + // In REAPER, changing the sample rate will deactivate the plugin, + // save its state, destroy it, create a new instance, restore the + // state, and then activate the new instance. + // We'll do the same, because there's no way to retroactively change the + // plugin sample rate. + // This is a bit expensive, so try to avoid changing the sample rate too + // frequently. + + // In addition to the above, we also need to destroy the custom view, + // and recreate it after creating the new plugin instance. + // Ideally this should all happen in the same Component. + + deactivate(); + + MemoryBlock mb; + getStateInformation (mb); + + instance = std::make_unique (*world, + std::move (instance->symap), + plugin, + std::move (instance->ports), + numSamples, + sampleRate); + + // prepareToPlay is *guaranteed* not to be called concurrently with processBlock + setStateInformationImpl (mb.getData(), (int) mb.getSize(), ConcurrentWithAudioCallback::no); + + jassert (numSamples == instance->features.getMaxBlockSize()); + activate(); + } + + void releaseResources() override { deactivate(); } + + using AudioPluginInstance::processBlock; + using AudioPluginInstance::processBlockBypassed; + + void processBlock (AudioBuffer& audio, MidiBuffer& midi) override + { + processBlockImpl (audio, midi); + } + + void processBlockBypassed (AudioBuffer& audio, MidiBuffer& midi) override + { + if (bypassParam != nullptr) + processBlockImpl (audio, midi); + else + AudioPluginInstance::processBlockBypassed (audio, midi); + } + + double getTailLengthSeconds() const override { return {}; } // TODO + + bool acceptsMidi() const override + { + if (instance == nullptr) + return false; + + auto ports = instance->ports.getAtomPorts(); + + return std::any_of (ports.begin(), ports.end(), [&] (const AtomPort& a) + { + if (a.header.direction != Port::Direction::input) + return false; + + return portAtIndexSupportsMidi (a.header.index); + }); + } + + bool producesMidi() const override + { + if (instance == nullptr) + return false; + + auto ports = instance->ports.getAtomPorts(); + + return std::any_of (ports.begin(), ports.end(), [&] (const AtomPort& a) + { + if (a.header.direction != Port::Direction::output) + return false; + + return portAtIndexSupportsMidi (a.header.index); + }); + } + + AudioProcessorEditor* createEditor() override { return nullptr; } + bool hasEditor() const override { return false; } + + int getNumPrograms() override { return (int) presetUris.size(); } + + int getCurrentProgram() override + { + return lastAppliedPreset; + } + + void setCurrentProgram (int newProgram) override + { + JUCE_ASSERT_MESSAGE_THREAD; + + if (! isPositiveAndBelow (newProgram, presetUris.size())) + return; + + lastAppliedPreset = newProgram; + applyStateWithAppropriateLocking (loadStateWithUri (presetUris[(size_t) newProgram]), + ConcurrentWithAudioCallback::yes); + } + + const String getProgramName (int program) override + { + JUCE_ASSERT_MESSAGE_THREAD; + + if (isPositiveAndBelow (program, presetUris.size())) + return loadStateWithUri (presetUris[(size_t) program]).getLabel(); + + return {}; + } + + void changeProgramName (int program, const String& label) override + { + JUCE_ASSERT_MESSAGE_THREAD; + + if (isPositiveAndBelow (program, presetUris.size())) + loadStateWithUri (presetUris[(size_t) program]).setLabel (label); + } + + void getStateInformation (MemoryBlock& block) override + { + JUCE_ASSERT_MESSAGE_THREAD; + + // TODO where should the state URI come from? + PortMap portStateManager (instance->ports); + const auto stateUri = String::fromUTF8 (instance->instance.getUri()) + "/savedState"; + auto mapFeature = instance->symap->getMapFeature(); + auto unmapFeature = instance->symap->getUnmapFeature(); + const auto state = PluginState::SaveRestoreHandle (*instance, portStateManager).save (plugin.get(), &mapFeature); + const auto string = state.toString (world->get(), &mapFeature, &unmapFeature, stateUri.toRawUTF8()); + block.replaceAll (string.data(), string.size()); + } + + void setStateInformation (const void* data, int size) override + { + setStateInformationImpl (data, size, ConcurrentWithAudioCallback::yes); + } + + void setNonRealtime (bool newValue) noexcept override + { + JUCE_ASSERT_MESSAGE_THREAD; + + AudioPluginInstance::setNonRealtime (newValue); + instance->features.setNonRealtime (newValue); + } + + bool isBusesLayoutSupported (const BusesLayout& layout) const override + { + for (const auto& pair : { std::make_tuple (&layout.inputBuses, &declaredBusLayout.inputs), + std::make_tuple (&layout.outputBuses, &declaredBusLayout.outputs) }) + { + const auto& requested = *std::get<0> (pair); + const auto& allowed = *std::get<1> (pair); + + if ((size_t) requested.size() != allowed.size()) + return false; + + for (size_t busIndex = 0; busIndex < allowed.size(); ++busIndex) + { + const auto& requestedBus = requested[(int) busIndex]; + const auto& allowedBus = allowed[busIndex]; + + if (! allowedBus.isCompatible (requestedBus)) + return false; + } + } + + return true; + } + + void processorLayoutsChanged() override { ioMap = lv2_shared::PortToAudioBufferMap { getBusesLayout(), declaredBusLayout }; } + + AudioProcessorParameter* getBypassParameter() const override { return bypassParam; } + + void postAllParametersToUi (ProcessorToUi& channel, UiEventListener* uiEventListener) + { + parameterValues.postAllParametersToUi (uiEventListener, getParameterWriterUrids(), channel); + controlPortStructure.writeOutputPorts (uiEventListener, channel); + } + + void postChangedParametersToUi (ProcessorToUi& channel, UiEventListener* uiEventListener) + { + parameterValues.postChangedParametersToUi (uiEventListener, getParameterWriterUrids(), channel); + controlPortStructure.writeOutputPorts (uiEventListener, channel); + } + + /* The current instance will be *recreated* every time the sample rate changes! */ + InstanceWithSupports* getCurrentInstance() const + { + return instance.get(); + } + + AudioProcessorParameter* getParamByPortIndex (uint32_t portIndex) const + { + return parameterValues.getParamByPortIndex (portIndex); + } + +private: + enum class ConcurrentWithAudioCallback { no, yes }; + + virtual void sendOutgoingPortMessageToUi (UiMessageHeader, uint32_t, const void*) {} + + LV2AudioPluginInstanceHeadless (std::shared_ptr worldIn, + const Plugin& pluginIn, + std::unique_ptr&& in, + PluginDescription&& desc, + std::vector knownPresetUris, + PluginState stateToApply, + const lv2_shared::ParsedBuses& parsedBuses) + : AudioPluginInstance (getBusesProperties (parsedBuses, *worldIn)), + declaredBusLayout (parsedBuses), + world (std::move (worldIn)), + plugin (pluginIn), + description (std::move (desc)), + presetUris (std::move (knownPresetUris)), + instance (std::move (in)) + { + applyStateWithAppropriateLocking (std::move (stateToApply), ConcurrentWithAudioCallback::no); + } + + void setStateInformationImpl (const void* data, int size, ConcurrentWithAudioCallback concurrent) + { + JUCE_ASSERT_MESSAGE_THREAD; + + if (data == nullptr || size == 0) + return; + + auto begin = static_cast (data); + std::vector copy (begin, begin + size); + copy.push_back (0); + auto mapFeature = instance->symap->getMapFeature(); + applyStateWithAppropriateLocking (PluginState { lilv_state_new_from_string (world->get(), &mapFeature, copy.data()) }, + concurrent); + } + + void activate() + { + if (! active) + instance->instance.activate(); + + active = true; + } + + void deactivate() + { + if (active) + instance->instance.deactivate(); + + active = false; + } + + void processBlockImpl (AudioBuffer& audio, MidiBuffer& midi) + { + preparePortsForRun (audio, midi); + + instance->instance.run (static_cast (audio.getNumSamples())); + instance->features.processResponses(); + + processPortsAfterRun (midi); + } + + bool portAtIndexSupportsMidi (uint32_t index) const noexcept + { + const auto port = plugin.getPortByIndex (index); + + if (! port.isValid()) + return false; + + return port.supportsEvent (world->newUri (LV2_MIDI__MidiEvent).get()); + } + + ParameterWriterUrids getParameterWriterUrids() const + { + return { instance->urids.mLV2_PATCH__Set, + instance->urids.mLV2_PATCH__property, + instance->urids.mLV2_PATCH__value, + instance->urids.mLV2_ATOM__eventTransfer }; + } + + void applyStateWithAppropriateLocking (PluginState&& state, ConcurrentWithAudioCallback concurrent) + { + PortMap portStateManager (instance->ports); + + // If a plugin supports threadSafeRestore, its restore method is thread-safe + // and may be called concurrently with audio class functions. + if (hasThreadSafeRestore || concurrent == ConcurrentWithAudioCallback::no) + { + state.restore (*instance, portStateManager); + } + else + { + const ScopedLock lock (getCallbackLock()); + state.restore (*instance, portStateManager); + } + + parameterValues.updateFromControlPorts (controlPortStructure); + } + + PluginState loadStateWithUri (const String& str) + { + auto mapFeature = instance->symap->getMapFeature(); + const auto presetUri = world->newUri (str.toRawUTF8()); + lilv_world_load_resource (world->get(), presetUri.get()); + return PluginState { lilv_state_new_from_world (world->get(), &mapFeature, presetUri.get()) }; + } + + void connectPorts (AudioBuffer& audio) + { + // Plugins that cannot process in-place will require the feature "inPlaceBroken". + // We don't support that feature, so if we made it to this point we can assume that + // in-place processing works. + for (const auto& port : instance->ports.getAudioPorts()) + { + const auto channel = ioMap.getChannelForPort (port.header.index); + auto* ptr = isPositiveAndBelow (channel, audio.getNumChannels()) ? audio.getWritePointer (channel) + : nullptr; + instance->instance.connectPort (port.header.index, ptr); + } + + for (const auto& port : instance->ports.getCvPorts()) + instance->instance.connectPort (port.header.index, nullptr); + + for (auto& port : instance->ports.getAtomPorts()) + instance->instance.connectPort (port.header.index, port.data()); + } + + void writeTimeInfoToPort (AtomPort& port) + { + if (port.header.direction != Port::Direction::input || ! port.getSupportsTime()) + return; + + auto* forge = port.getForge().get(); + auto* playhead = getPlayHead(); + + if (playhead == nullptr) + return; + + // Write timing info to the control port + const auto info = playhead->getPosition(); + + if (! info.hasValue()) + return; + + const auto& urids = instance->urids; + + lv2_atom_forge_frame_time (forge, 0); + + lv2_shared::ObjectFrame object { forge, (uint32_t) 0, urids.mLV2_TIME__Position }; + + lv2_atom_forge_key (forge, urids.mLV2_TIME__speed); + lv2_atom_forge_float (forge, info->getIsPlaying() ? 1.0f : 0.0f); + + if (const auto samples = info->getTimeInSamples()) + { + lv2_atom_forge_key (forge, urids.mLV2_TIME__frame); + lv2_atom_forge_long (forge, *samples); + } + + if (const auto bar = info->getBarCount()) + { + lv2_atom_forge_key (forge, urids.mLV2_TIME__bar); + lv2_atom_forge_long (forge, *bar); + } + + if (const auto beat = info->getPpqPosition()) + { + if (const auto barStart = info->getPpqPositionOfLastBarStart()) + { + lv2_atom_forge_key (forge, urids.mLV2_TIME__barBeat); + lv2_atom_forge_float (forge, (float) (*beat - *barStart)); + } + + lv2_atom_forge_key (forge, urids.mLV2_TIME__beat); + lv2_atom_forge_double (forge, *beat); + } + + if (const auto sig = info->getTimeSignature()) + { + lv2_atom_forge_key (forge, urids.mLV2_TIME__beatUnit); + lv2_atom_forge_int (forge, sig->denominator); + + lv2_atom_forge_key (forge, urids.mLV2_TIME__beatsPerBar); + lv2_atom_forge_float (forge, (float) sig->numerator); + } + + if (const auto bpm = info->getBpm()) + { + lv2_atom_forge_key (forge, urids.mLV2_TIME__beatsPerMinute); + lv2_atom_forge_float (forge, (float) *bpm); + } + } + + void preparePortsForRun (AudioBuffer& audio, MidiBuffer& midiBuffer) + { + connectPorts (audio); + + for (auto& port : instance->ports.getAtomPorts()) + { + switch (port.header.direction) + { + case Port::Direction::input: + port.beginSequence(); + break; + + case Port::Direction::output: + port.replaceWithChunk(); + break; + + case Port::Direction::unknown: + jassertfalse; + break; + } + } + + for (auto& port : instance->ports.getAtomPorts()) + writeTimeInfoToPort (port); + + const auto controlPortForge = controlPort != nullptr ? controlPort->getForge().get() + : nullptr; + + parameterValues.postChangedParametersToProcessor (getParameterWriterUrids(), controlPortForge); + + instance->uiToProcessor.readAllAndClear ([this] (MessageHeader header, uint32_t size, const void* buffer) + { + pushMessage (header, size, buffer); + }); + + for (auto& port : instance->ports.getAtomPorts()) + { + if (port.header.direction == Port::Direction::input) + { + for (const auto meta : midiBuffer) + { + port.addEventToSequence (meta.samplePosition, + instance->urids.mLV2_MIDI__MidiEvent, + static_cast (meta.numBytes), + meta.data); + } + + port.endSequence(); + } + } + + if (freeWheelingPort != nullptr) + freeWheelingPort->currentValue = isNonRealtime() ? freeWheelingPort->info.max + : freeWheelingPort->info.min; + } + + void pushMessage (MessageHeader header, [[maybe_unused]] uint32_t size, const void* data) + { + if (header.protocol == 0 || header.protocol == instance->urids.mLV2_UI__floatProtocol) + { + const auto value = readUnaligned (data); + + if (auto* param = parameterValues.getParamByPortIndex (header.portIndex)) + { + param->setDenormalisedValue (value); + } + else if (auto* port = controlPortStructure.getControlPortByIndex (header.portIndex)) + { + // No parameter corresponds to this port, write to the port directly + port->currentValue = value; + } + } + else if (auto* atomPort = header.portIndex < atomPorts.size() ? atomPorts[header.portIndex] : nullptr) + { + if (header.protocol == instance->urids.mLV2_ATOM__eventTransfer) + { + if (const auto* atom = convertToAtomPtr (data, (size_t) size)) + { + atomPort->addAtomToSequence (0, atom); + + // Not UB; LV2_Atom_Object has LV2_Atom as its first member + if (atom->type == instance->urids.mLV2_ATOM__Object) + patchSetHelper.processPatchSet (reinterpret_cast (data), PatchSetCallback { parameterValues }); + } + } + else if (header.protocol == instance->urids.mLV2_ATOM__atomTransfer) + { + if (const auto* atom = convertToAtomPtr (data, (size_t) size)) + atomPort->replaceBufferWithAtom (atom); + } + } + } + + void processPortsAfterRun (MidiBuffer& midi) + { + midi.clear(); + + for (auto& port : instance->ports.getAtomPorts()) + processAtomPort (port, midi); + + if (latencyPort != nullptr) + setLatencySamples ((int) latencyPort->currentValue); + } + + void processAtomPort (const AtomPort& port, MidiBuffer& midi) + { + if (port.header.direction != Port::Direction::output) + return; + + // The port holds an Atom, by definition + const auto* atom = reinterpret_cast (port.data()); + + if (atom->type != instance->urids.mLV2_ATOM__Sequence) + return; + + // The Atom said that it was of sequence type, so this isn't UB + const auto* sequence = reinterpret_cast (port.data()); + + // http://lv2plug.in/ns/ext/atom#Sequence - run() stamps are always audio frames + jassert (sequence->body.unit == 0 || sequence->body.unit == instance->urids.mLV2_UNITS__frame); + + for (const auto* event : lv2_shared::SequenceIterator { lv2_shared::SequenceWithSize { sequence } }) + { + sendOutgoingPortMessageToUi ({ nullptr, { port.header.index, instance->urids.mLV2_ATOM__eventTransfer } }, + (uint32_t) (event->body.size + sizeof (LV2_Atom)), + &event->body); + + if (event->body.type == instance->urids.mLV2_MIDI__MidiEvent) + midi.addEvent (event + 1, static_cast (event->body.size), static_cast (event->time.frames)); + + if (lv2_atom_forge_is_object_type (port.getForge().get(), event->body.type)) + if (reinterpret_cast (event + 1)->otype == instance->urids.mLV2_STATE__StateChanged) + updateHostDisplay (ChangeDetails{}.withNonParameterStateChanged (true)); + + patchSetHelper.processPatchSet (event, PatchSetCallback { parameterValues }); + } + } + + // Check for duplicate channel designations, and convert the set to a discrete channel layout + // if any designations are duplicated. + static std::set validateAndRedesignatePorts (std::set info) + { + const auto channelSet = lv2_shared::ParsedGroup::getEquivalentSet (info); + + if ((int) info.size() == channelSet.size()) + return info; + + std::set result; + auto designation = (int) AudioChannelSet::discreteChannel0; + + for (auto& item : info) + { + auto copy = item; + copy.designation = (AudioChannelSet::ChannelType) designation++; + result.insert (copy); + } + + return result; + } + + static AudioChannelSet::ChannelType getPortDesignation (World& world, const Port& port, size_t indexInGroup) + { + const auto defaultResult = (AudioChannelSet::ChannelType) (AudioChannelSet::discreteChannel0 + indexInGroup); + const auto node = port.get (world.newUri (LV2_CORE__designation).get()); + + if (node == nullptr) + return defaultResult; + + const auto it = lv2_shared::channelDesignationMap.find (lilvNodeToUriString (node.get())); + + if (it == lv2_shared::channelDesignationMap.end()) + return defaultResult; + + return it->second; + } + + static lv2_shared::ParsedBuses getParsedBuses (World& world, const Plugin& p, const UsefulUris& uris) + { + const auto groupPropertyUri = world.newUri (LV2_PORT_GROUPS__group); + const auto optionalUri = world.newUri (LV2_CORE__connectionOptional); + + std::map> inputGroups, outputGroups; + std::set ungroupedInputs, ungroupedOutputs; + + for (uint32_t i = 0, numPorts = p.getNumPorts(); i < numPorts; ++i) + { + const auto port = p.getPortByIndex (i); + + if (port.getKind (uris) != Port::Kind::audio) + continue; + + const auto groupUri = lilvNodeToUriString (port.get (groupPropertyUri.get()).get()); + + auto& set = [&]() -> auto& + { + if (groupUri.isEmpty()) + return port.getDirection (uris) == Port::Direction::input ? ungroupedInputs : ungroupedOutputs; + + auto& group = port.getDirection (uris) == Port::Direction::input ? inputGroups : outputGroups; + return group[groupUri]; + }(); + + set.insert ({ port.getIndex(), getPortDesignation (world, port, set.size()), port.hasProperty (optionalUri) }); + } + + for (auto* groups : { &inputGroups, &outputGroups }) + for (auto& pair : *groups) + pair.second = validateAndRedesignatePorts (std::move (pair.second)); + + JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4702) + const auto getMainGroupName = [&] (const char* propertyName) + { + for (const auto* item : p.getValue (world.newUri (propertyName).get())) + return lilvNodeToUriString (item); + + return String{}; + }; + JUCE_END_IGNORE_WARNINGS_MSVC + + return { findStableBusOrder (getMainGroupName (LV2_PORT_GROUPS__mainInput), inputGroups, ungroupedInputs), + findStableBusOrder (getMainGroupName (LV2_PORT_GROUPS__mainOutput), outputGroups, ungroupedOutputs) }; + } + + static String getNameForUri (World& world, StringRef uri) + { + if (uri.isEmpty()) + return String(); + + const auto node = world.get (world.newUri (uri).get(), + world.newUri (LV2_CORE__name).get(), + nullptr); + + if (node == nullptr) + return String(); + + return String::fromUTF8 (lilv_node_as_string (node.get())); + } + + static BusesProperties getBusesProperties (const lv2_shared::ParsedBuses& parsedBuses, World& world) + { + BusesProperties result; + + for (const auto& pair : { std::make_tuple (&parsedBuses.inputs, &result.inputLayouts), + std::make_tuple (&parsedBuses.outputs, &result.outputLayouts) }) + { + const auto& buses = *std::get<0> (pair); + auto& layout = *std::get<1> (pair); + + for (const auto& bus : buses) + { + layout.add (AudioProcessor::BusProperties { getNameForUri (world, bus.uid), + bus.getEquivalentSet(), + bus.isRequired() }); + } + } + + return result; + } + + LV2_URID map (const char* str) const + { + return instance != nullptr ? instance->symap->map (str) + : LV2_URID(); + } + + ControlPort* findControlPortWithIndex (uint32_t index) const + { + auto ports = instance->ports.getControlPorts(); + const auto indexMatches = [&] (const ControlPort& p) { return p.header.index == index; }; + const auto it = std::find_if (ports.begin(), ports.end(), indexMatches); + + return it != ports.end() ? &(*it) : nullptr; + } + + const lv2_shared::ParsedBuses declaredBusLayout; + lv2_shared::PortToAudioBufferMap ioMap { getBusesLayout(), declaredBusLayout }; + std::shared_ptr world; + Plugin plugin; + PluginDescription description; + std::vector presetUris; + std::unique_ptr instance; + + std::vector atomPorts = getPortPointers (instance->ports.getAtomPorts()); + + AtomPort* const controlPort = [&]() -> AtomPort* + { + const auto port = plugin.getPortByDesignation (world->newUri (LV2_CORE__InputPort).get(), + world->newUri (LV2_CORE__control).get()); + + if (! port.isValid()) + return nullptr; + + const auto index = port.getIndex(); + + if (! isPositiveAndBelow (index, atomPorts.size())) + return nullptr; + + return atomPorts[index]; + }(); + + ControlPort* const latencyPort = [&]() -> ControlPort* + { + if (! plugin.hasLatency()) + return nullptr; + + return findControlPortWithIndex (plugin.getLatencyPortIndex()); + }(); + + ControlPort* const freeWheelingPort = [&]() -> ControlPort* + { + const auto port = plugin.getPortByDesignation (world->newUri (LV2_CORE__InputPort).get(), + world->newUri (LV2_CORE__freeWheeling).get()); + + if (! port.isValid()) + return nullptr; + + return findControlPortWithIndex (port.getIndex()); + }(); + + ControlPort* const enabledPort = [&]() -> ControlPort* + { + const auto port = plugin.getPortByDesignation (world->newUri (LV2_CORE__InputPort).get(), + world->newUri (LV2_CORE_PREFIX "enabled").get()); + + if (! port.isValid()) + return nullptr; + + return findControlPortWithIndex (port.getIndex()); + }(); + + lv2_shared::PatchSetHelper patchSetHelper { instance->symap->getMapFeature(), plugin.getUri().getTyped() }; + ControlPortAccelerationStructure controlPortStructure { instance->ports.getControlPorts() }; + ParameterValueCache parameterValues { *this, + *world, + instance->symap->getMapFeature(), + getJuceParameterInfo (*world, + plugin, + instance->urids, + { latencyPort, freeWheelingPort }, + instance->ports.getControlPorts(), + controlPort != nullptr ? controlPort->header.index : 0), + enabledPort }; + LV2Parameter* bypassParam = enabledPort != nullptr ? parameterValues.getParamByPortIndex (enabledPort->header.index) + : nullptr; + + int lastAppliedPreset = 0; + bool hasThreadSafeRestore = plugin.hasExtensionData (world->newUri (LV2_STATE__threadSafeRestore)); + bool active { false }; + + JUCE_LEAK_DETECTOR (LV2AudioPluginInstanceHeadless) +}; + +} // namespace lv2_host + +//============================================================================== +class LV2PluginFormatHeadless::Pimpl +{ +public: + Pimpl() + { + loadAllPluginsFromPaths (getDefaultLocationsToSearch()); + + const auto tempFile = lv2ResourceFolder.getFile(); + + if (tempFile.createDirectory()) + { + for (const auto& bundle : lv2::Bundle::getAllBundles()) + { + const auto pathToBundle = tempFile.getChildFile (bundle.name + String (".lv2")); + + if (! pathToBundle.createDirectory()) + continue; + + for (const auto& resource : bundle.contents) + pathToBundle.getChildFile (resource.name).replaceWithText (resource.contents); + + const auto pathString = File::addTrailingSeparator (pathToBundle.getFullPathName()); + world->loadBundle (world->newFileUri (nullptr, pathString.toRawUTF8())); + } + } + } + + ~Pimpl() + { + lv2ResourceFolder.getFile().deleteRecursively(); + } + + void findAllTypesForFile (OwnedArray& result, + const String& identifier) + { + if (File::isAbsolutePath (identifier)) + world->loadBundle (world->newFileUri (nullptr, File::addTrailingSeparator (identifier).toRawUTF8())); + + for (const auto& plugin : { findPluginByUri (identifier), findPluginByFile (identifier) }) + { + if (auto desc = getDescription (plugin); desc.fileOrIdentifier.isNotEmpty()) + { + result.add (std::make_unique (desc)); + break; + } + } + } + + bool fileMightContainThisPluginType (const String& file) const + { + // If the string looks like a URI, then it could be a valid LV2 identifier + const auto* data = file.toRawUTF8(); + const auto numBytes = file.getNumBytesAsUTF8(); + std::vector vec (numBytes + 1, 0); + std::copy (data, data + numBytes, vec.begin()); + return serd_uri_string_has_scheme (vec.data()) || file.endsWith (".lv2"); + } + + String getNameOfPluginFromIdentifier (const String& identifier) + { + // We would have to actually load the bundle to get its name, + // and the bundle may contain multiple plugins + return identifier; + } + + bool pluginNeedsRescanning (const PluginDescription&) + { + return true; + } + + bool doesPluginStillExist (const PluginDescription& description) + { + return findPluginByUri (description.fileOrIdentifier) != nullptr; + } + + StringArray searchPathsForPlugins (const FileSearchPath& paths, bool, bool) + { + loadAllPluginsFromPaths (paths); + + StringArray result; + + for (const auto* plugin : world->getAllPlugins()) + result.add (lv2_host::Plugin { plugin }.getUri().getTyped()); + + return result; + } + + FileSearchPath getDefaultLocationsToSearch() + { + #if JUCE_MAC + return { "~/Library/Audio/Plug-Ins/LV2;" + "~/.lv2;" + "/usr/local/lib/lv2;" + "/usr/lib/lv2;" + "/Library/Audio/Plug-Ins/LV2;" }; + #elif JUCE_WINDOWS + return { "%APPDATA%\\LV2;" + "%COMMONPROGRAMFILES%\\LV2" }; + #else + #if JUCE_64BIT + if (File ("/usr/lib64/lv2").exists() || File ("/usr/local/lib64/lv2").exists()) + return { "~/.lv2;" + "/usr/lib64/lv2;" + "/usr/local/lib64/lv2" }; + #endif + + return { "~/.lv2;" + "/usr/lib/lv2;" + "/usr/local/lib/lv2" }; + #endif + } + + const LilvUI* findEmbeddableUi (const lv2_host::Uis* pluginUis, std::true_type) + { + if (pluginUis == nullptr) + return nullptr; + + const std::vector allUis (pluginUis->begin(), pluginUis->end()); + + if (allUis.empty()) + return nullptr; + + constexpr const char* rawUri = + #if JUCE_MAC + LV2_UI__CocoaUI; + #elif JUCE_WINDOWS + LV2_UI__WindowsUI; + #elif JUCE_LINUX || JUCE_BSD + LV2_UI__X11UI; + #else + nullptr; + #endif + + jassert (rawUri != nullptr); + const auto nativeUiUri = world->newUri (rawUri); + + struct UiWithSuitability + { + const LilvUI* ui; + unsigned suitability; + + bool operator< (const UiWithSuitability& other) const noexcept + { + return suitability < other.suitability; + } + + static unsigned uiIsSupported (const char* hostUri, const char* pluginUri) + { + if (strcmp (hostUri, pluginUri) == 0) + return 1; + + return 0; + } + }; + + std::vector uisWithSuitability; + uisWithSuitability.reserve (allUis.size()); + + std::transform (allUis.cbegin(), allUis.cend(), std::back_inserter (uisWithSuitability), [&] (const LilvUI* ui) + { + const LilvNode* type = nullptr; + return UiWithSuitability { ui, lilv_ui_is_supported (ui, UiWithSuitability::uiIsSupported, nativeUiUri.get(), &type) }; + }); + + std::sort (uisWithSuitability.begin(), uisWithSuitability.end()); + + if (uisWithSuitability.back().suitability != 0) + return uisWithSuitability.back().ui; + + return nullptr; + } + + const LilvUI* findEmbeddableUi (const lv2_host::Uis*, std::false_type) + { + return nullptr; + } + + const LilvUI* findEmbeddableUi (const lv2_host::Uis* pluginUis) + { + return findEmbeddableUi (pluginUis, std::integral_constant{}); + } + + static lv2_host::UiDescriptor getUiDescriptor (const LilvUI* ui) + { + if (ui == nullptr) + return {}; + + const auto libraryFile = StringPtr { lilv_file_uri_parse (lilv_node_as_uri (lilv_ui_get_binary_uri (ui)), nullptr) }; + + return lv2_host::UiDescriptor { lv2_host::UiDescriptorArgs{}.withLibraryPath (libraryFile.get()) + .withUiUri (lilv_node_as_uri (lilv_ui_get_uri (ui))) }; + } + + // Returns the name of a missing feature, if any. + template + static std::vector findMissingFeatures (RequiredFeatures&& required, + AvailableFeatures&& available) + { + std::vector result; + + for (const auto* node : required) + { + const auto nodeString = String::fromUTF8 (lilv_node_as_uri (node)); + + if (std::find (std::begin (available), std::end (available), nodeString) == std::end (available)) + result.push_back (nodeString); + } + + return result; + } + + template + static void createPluginInstance (LV2PluginFormatHeadless& format, + const PluginDescription& desc, + double initialSampleRate, + int initialBufferSize, + PluginCreationCallback callback) + { + const auto* pluginPtr = format.pimpl->findPluginByUri (desc.fileOrIdentifier); + + if (pluginPtr == nullptr) + return callback (nullptr, "Unable to locate plugin with the requested URI"); + + const lv2_host::Plugin plugin { pluginPtr }; + + auto symap = std::make_unique(); + + const auto missingFeatures = findMissingFeatures (plugin.getRequiredFeatures(), + lv2_host::FeaturesData::getFeatureUris()); + + if (! missingFeatures.empty()) + { + const auto missingFeaturesString = StringArray (missingFeatures.data(), (int) missingFeatures.size()).joinIntoString (", "); + + return callback (nullptr, "plugin requires missing features: " + missingFeaturesString); + } + + auto world = format.pimpl->world; + auto& uris = format.pimpl->uris; + + auto stateToApply = [&] + { + if (! plugin.hasFeature (world->newUri (LV2_STATE__loadDefaultState))) + return lv2_host::PluginState{}; + + auto map = symap->getMapFeature(); + return lv2_host::PluginState { lilv_state_new_from_world (world->get(), &map, plugin.getUri().get()) }; + }(); + + auto ports = lv2_host::Ports::getPorts (*world, uris, plugin, *symap); + + if (! ports.hasValue()) + return callback (nullptr, "Plugin has ports of an unsupported type"); + + auto instance = std::make_unique (*world, + std::move (symap), + plugin, + std::move (*ports), + (int32_t) initialBufferSize, + initialSampleRate); + + if (instance->instance == nullptr) + return callback (nullptr, "Plugin was located, but could not be opened"); + + auto potentialPresets = world->findNodes (nullptr, + world->newUri (LV2_CORE__appliesTo).get(), + plugin.getUri().get()); + + const lv2_host::Uis pluginUis { plugin.get() }; + + const auto uiToUse = [&]() -> const LilvUI* + { + const auto bestMatch = format.pimpl->findEmbeddableUi (&pluginUis); + + if (bestMatch == nullptr) + return bestMatch; + + const auto uiUri = lilv_ui_get_uri (bestMatch); + lilv_world_load_resource (world->get(), uiUri); + + const auto queryUi = [&] (const char* featureUri) + { + const auto featureUriNode = world->newUri (featureUri); + return world->findNodes (uiUri, featureUriNode.get(), nullptr); + }; + + const auto missingUiFeatures = findMissingFeatures (queryUi (LV2_CORE__requiredFeature), + lv2_host::UiFeaturesData::getFeatureUris()); + + return missingUiFeatures.empty() ? bestMatch : nullptr; + }(); + + auto uiBundleUri = uiToUse != nullptr ? String::fromUTF8 (lilv_node_as_uri (lilv_ui_get_bundle_uri (uiToUse))) + : String(); + + auto wrapped = std::make_unique (world, + plugin, + uris, + std::move (instance), + format.pimpl->getDescription (pluginPtr), + format.pimpl->findPresetUrisForPlugin (plugin.get()), + std::move (stateToApply), + std::move (uiBundleUri), + getUiDescriptor (uiToUse)); + callback (std::move (wrapped), {}); + } + +private: + void loadAllPluginsFromPaths (const FileSearchPath& path) + { + const auto joined = path.toStringWithSeparator (LILV_PATH_SEP); + world->loadAllFromPaths (world->newString (joined.toRawUTF8())); + } + + struct Free { void operator() (char* ptr) const noexcept { free (ptr); } }; + using StringPtr = std::unique_ptr; + + const LilvPlugin* findPluginByUri (const String& s) + { + return world->getAllPlugins().getByUri (world->newUri (s.toRawUTF8())); + } + + const LilvPlugin* findPluginByFile (const File& f) + { + return world->getAllPlugins().getByFile (f); + } + + template + void visitParentClasses (const LilvPluginClass* c, Fn&& fn) const + { + if (c == nullptr) + return; + + const lv2_host::PluginClass wrapped { c }; + fn (wrapped); + + const auto parentUri = wrapped.getParentUri(); + + if (parentUri.get() != nullptr) + visitParentClasses (world->getPluginClasses().getByUri (parentUri), fn); + } + + std::vector collectPluginClassUris (const LilvPluginClass* c) const + { + std::vector results; + + visitParentClasses (c, [&results] (const lv2_host::PluginClass& wrapped) + { + results.emplace_back (wrapped.getUri()); + }); + + return results; + } + + PluginDescription getDescription (const LilvPlugin* plugin) + { + if (plugin == nullptr) + return {}; + + const auto wrapped = lv2_host::Plugin { plugin }; + const auto bundle = wrapped.getBundleUri().getTyped(); + const auto bundleFile = File { StringPtr { lilv_file_uri_parse (bundle, nullptr) }.get() }; + + const auto numInputs = wrapped.getNumPortsOfClass (uris.mLV2_CORE__AudioPort, uris.mLV2_CORE__InputPort); + const auto numOutputs = wrapped.getNumPortsOfClass (uris.mLV2_CORE__AudioPort, uris.mLV2_CORE__OutputPort); + + PluginDescription result; + result.name = wrapped.getName().getTyped(); + result.descriptiveName = wrapped.getName().getTyped(); + result.lastFileModTime = bundleFile.getLastModificationTime(); + result.lastInfoUpdateTime = Time::getCurrentTime(); + result.manufacturerName = wrapped.getAuthorName().getTyped(); + result.pluginFormatName = LV2PluginFormatHeadless::getFormatName(); + result.numInputChannels = static_cast (numInputs); + result.numOutputChannels = static_cast (numOutputs); + + const auto classPtr = wrapped.getClass(); + const auto classes = collectPluginClassUris (classPtr); + const auto isInstrument = std::any_of (classes.cbegin(), + classes.cend(), + [this] (const lv2_host::NodeUri& uri) + { + return uri.equals (uris.mLV2_CORE__GeneratorPlugin); + }); + + result.category = lv2_host::PluginClass { classPtr }.getLabel().getTyped(); + result.isInstrument = isInstrument; + + // The plugin URI is required to be globally unique, so a hash of it should be too + result.fileOrIdentifier = wrapped.getUri().getTyped(); + + const auto uid = DefaultHashFunctions::generateHash (result.fileOrIdentifier, std::numeric_limits::max());; + result.deprecatedUid = result.uniqueId = uid; + return result; + } + + std::vector findPresetUrisForPlugin (const LilvPlugin* plugin) + { + std::vector presetUris; + + lv2_host::OwningNodes potentialPresets { lilv_plugin_get_related (plugin, world->newUri (LV2_PRESETS__Preset).get()) }; + + for (const auto* potentialPreset : potentialPresets) + presetUris.push_back (lilv_node_as_string (potentialPreset)); + + return presetUris; + } + + TemporaryFile lv2ResourceFolder; + std::shared_ptr world = std::make_shared(); + lv2_host::UsefulUris uris { world->get() }; +}; + +} // namespace juce + +#endif diff --git a/modules/juce_audio_processors/format_types/juce_LV2Resources.h b/modules/juce_audio_processors_headless/format_types/juce_LV2Resources.h similarity index 96% rename from modules/juce_audio_processors/format_types/juce_LV2Resources.h rename to modules/juce_audio_processors_headless/format_types/juce_LV2Resources.h index 834cf3143a..b5f8f5d124 100644 --- a/modules/juce_audio_processors/format_types/juce_LV2Resources.h +++ b/modules/juce_audio_processors_headless/format_types/juce_LV2Resources.h @@ -56,7 +56,7 @@ struct Bundle const char* name; std::vector contents; - static std::vector getAllBundles(); + static inline std::vector getAllBundles(); }; } // namespace juce::lv2 diff --git a/modules/juce_audio_processors/format_types/juce_LV2SupportLibs.cpp b/modules/juce_audio_processors_headless/format_types/juce_LV2SupportLibs.cpp similarity index 100% rename from modules/juce_audio_processors/format_types/juce_LV2SupportLibs.cpp rename to modules/juce_audio_processors_headless/format_types/juce_LV2SupportLibs.cpp diff --git a/modules/juce_audio_processors_headless/juce_audio_processors_headless.cpp b/modules/juce_audio_processors_headless/juce_audio_processors_headless.cpp index 9b4044a617..cbd6b45972 100644 --- a/modules/juce_audio_processors_headless/juce_audio_processors_headless.cpp +++ b/modules/juce_audio_processors_headless/juce_audio_processors_headless.cpp @@ -66,3 +66,4 @@ #include #include #include +#include diff --git a/modules/juce_audio_processors_headless/juce_audio_processors_headless.h b/modules/juce_audio_processors_headless/juce_audio_processors_headless.h index 9881e2020e..8b0daaa615 100644 --- a/modules/juce_audio_processors_headless/juce_audio_processors_headless.h +++ b/modules/juce_audio_processors_headless/juce_audio_processors_headless.h @@ -91,3 +91,4 @@ #include #include #include +#include diff --git a/modules/juce_audio_processors/juce_audio_processors_lv2_libs.cpp b/modules/juce_audio_processors_headless/juce_audio_processors_headless_lv2_libs.cpp similarity index 93% rename from modules/juce_audio_processors/juce_audio_processors_lv2_libs.cpp rename to modules/juce_audio_processors_headless/juce_audio_processors_headless_lv2_libs.cpp index 2d00a1776c..7e1f74627f 100644 --- a/modules/juce_audio_processors/juce_audio_processors_lv2_libs.cpp +++ b/modules/juce_audio_processors_headless/juce_audio_processors_headless_lv2_libs.cpp @@ -40,5 +40,5 @@ #include #include - #include "format_types/juce_LV2SupportLibs.cpp" + #include #endif