From 42dea5b18ed4e545d1aa21eb1d1a03f21d78887b Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sat, 16 May 2026 01:48:40 +0200 Subject: [PATCH 01/35] Initial support for audio plugin hosting --- cmake/yup_audio_plugin.cmake | 34 +- cmake/yup_dependencies.cmake | 146 +++ cmake/yup_modules.cmake | 5 + cmake/yup_standalone.cmake | 6 + examples/graphics/CMakeLists.txt | 4 + .../graphics/source/examples/PluginHost.h | 1046 ++++++++++++++++ examples/graphics/source/main.cpp | 2 + .../host/yup_AudioPluginDescription.h | 92 ++ .../host/yup_AudioPluginFormat.h | 95 ++ .../host/yup_AudioPluginFormatType.h | 34 + .../host/yup_AudioPluginHostContext.h | 56 + .../host/yup_AudioPluginInstance.cpp | 44 + .../host/yup_AudioPluginInstance.h | 62 + .../host/yup_AudioPluginScanner.cpp | 251 ++++ .../host/yup_AudioPluginScanner.h | 124 ++ .../native/yup_AudioPluginInstance_AUv2.h | 58 + .../native/yup_AudioPluginInstance_AUv2.mm | 1085 +++++++++++++++++ .../native/yup_AudioPluginInstance_CLAP.cpp | 597 +++++++++ .../native/yup_AudioPluginInstance_CLAP.h | 53 + .../native/yup_AudioPluginInstance_VST3.cpp | 613 ++++++++++ .../native/yup_AudioPluginInstance_VST3.h | 53 + .../yup_audio_plugin_host.cpp | 93 ++ .../yup_audio_plugin_host.h | 60 + .../yup_audio_plugin_host.mm | 22 + modules/yup_core/misc/yup_ResultValue.h | 12 +- modules/yup_gui/native/yup_Windowing_sdl2.cpp | 6 +- modules/yup_gui/yup_gui.h | 9 + tests/CMakeLists.txt | 1 + tests/yup_audio_plugin_host.cpp | 25 + .../AudioPluginDescription.cpp | 43 + .../AudioPluginInstance.cpp | 113 ++ .../AudioPluginScanner.cpp | 103 ++ .../AudioPluginState.cpp | 82 ++ 33 files changed, 5001 insertions(+), 28 deletions(-) create mode 100644 examples/graphics/source/examples/PluginHost.h create mode 100644 modules/yup_audio_plugin_host/host/yup_AudioPluginDescription.h create mode 100644 modules/yup_audio_plugin_host/host/yup_AudioPluginFormat.h create mode 100644 modules/yup_audio_plugin_host/host/yup_AudioPluginFormatType.h create mode 100644 modules/yup_audio_plugin_host/host/yup_AudioPluginHostContext.h create mode 100644 modules/yup_audio_plugin_host/host/yup_AudioPluginInstance.cpp create mode 100644 modules/yup_audio_plugin_host/host/yup_AudioPluginInstance.h create mode 100644 modules/yup_audio_plugin_host/host/yup_AudioPluginScanner.cpp create mode 100644 modules/yup_audio_plugin_host/host/yup_AudioPluginScanner.h create mode 100644 modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.h create mode 100644 modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.mm create mode 100644 modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.cpp create mode 100644 modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.h create mode 100644 modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.cpp create mode 100644 modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.h create mode 100644 modules/yup_audio_plugin_host/yup_audio_plugin_host.cpp create mode 100644 modules/yup_audio_plugin_host/yup_audio_plugin_host.h create mode 100644 modules/yup_audio_plugin_host/yup_audio_plugin_host.mm create mode 100644 tests/yup_audio_plugin_host.cpp create mode 100644 tests/yup_audio_plugin_host/AudioPluginDescription.cpp create mode 100644 tests/yup_audio_plugin_host/AudioPluginInstance.cpp create mode 100644 tests/yup_audio_plugin_host/AudioPluginScanner.cpp create mode 100644 tests/yup_audio_plugin_host/AudioPluginState.cpp diff --git a/cmake/yup_audio_plugin.cmake b/cmake/yup_audio_plugin.cmake index cf2f05579..d5f7ce766 100644 --- a/cmake/yup_audio_plugin.cmake +++ b/cmake/yup_audio_plugin.cmake @@ -91,14 +91,19 @@ function (yup_audio_plugin) _yup_fetch_sdl2() list (APPEND additional_libraries sdl2::sdl2) + _yup_target_list_contains ("${YUP_ARG_MODULES}" yup_audio_plugin_host has_audio_plugin_host) + if (has_audio_plugin_host) + _yup_collect_audio_plugin_host_dependencies ("${YUP_ARG_DEFINITIONS}" audio_plugin_host_libraries) + list (APPEND additional_libraries ${audio_plugin_host_libraries}) + if (audio_plugin_host_libraries) + target_link_libraries (${target_name}_shared INTERFACE + ${audio_plugin_host_libraries}) + endif() + endif() + # ==== Fetch clap SDK and build clap target if (YUP_ARG_PLUGIN_CREATE_CLAP) - _yup_message (STATUS "Fetching CLAP SDK") - _yup_fetchcontent_declare (clap - GIT_REPOSITORY https://github.com/free-audio/clap.git - GIT_TAG main) - FetchContent_MakeAvailable (clap) - set_target_properties (clap-tests PROPERTIES FOLDER "Tests") + _yup_fetch_clap() _yup_message (STATUS "Setting up CLAP plugin client") _yup_module_setup_plugin_client ( @@ -144,22 +149,7 @@ function (yup_audio_plugin) # ==== Fetch vst3 SDK and build vst3 target if (YUP_ARG_PLUGIN_CREATE_VST3) - _yup_message (STATUS "Fetching VST3 SDK") - set (SMTG_CREATE_MODULE_INFO OFF) - set (SMTG_ADD_VST3_UTILITIES OFF) - set (SMTG_ENABLE_VST3_HOSTING_EXAMPLES OFF) - set (SMTG_ENABLE_VST3_PLUGIN_EXAMPLES OFF) - set (SMTG_ENABLE_VSTGUI_SUPPORT OFF) - set (SMTG_CREATE_PLUGIN_LINK OFF) - if (NOT YUP_PLATFORM_MAC OR XCODE) - set (SMTG_RUN_VST_VALIDATOR ON) - else() - set (SMTG_RUN_VST_VALIDATOR OFF) - endif() - _yup_fetchcontent_declare (vst3sdk - GIT_REPOSITORY https://github.com/steinbergmedia/vst3sdk.git - GIT_TAG master) - FetchContent_MakeAvailable (vst3sdk) + _yup_fetch_vst3sdk() _yup_message (STATUS "Setting up VST3 plugin client") smtg_enable_vst3_sdk() diff --git a/cmake/yup_dependencies.cmake b/cmake/yup_dependencies.cmake index 2fa317996..d3de61497 100644 --- a/cmake/yup_dependencies.cmake +++ b/cmake/yup_dependencies.cmake @@ -77,6 +77,152 @@ endfunction() #============================================================================== +function (_yup_fetch_clap) + if (NOT TARGET clap) + _yup_message (STATUS "Fetching CLAP SDK") + _yup_fetchcontent_declare (clap + GIT_REPOSITORY https://github.com/free-audio/clap.git + GIT_TAG main) + + FetchContent_MakeAvailable (clap) + endif() + + if (TARGET clap-tests) + set_target_properties (clap-tests PROPERTIES FOLDER "Tests") + endif() +endfunction() + +#============================================================================== + +function (_yup_fetch_vst3sdk) + if (NOT TARGET sdk) + _yup_message (STATUS "Fetching VST3 SDK") + + set (SMTG_CREATE_MODULE_INFO OFF) + set (SMTG_ADD_VST3_UTILITIES OFF) + set (SMTG_ENABLE_VST3_HOSTING_EXAMPLES OFF) + set (SMTG_ENABLE_VST3_PLUGIN_EXAMPLES OFF) + set (SMTG_ENABLE_VSTGUI_SUPPORT OFF) + set (SMTG_CREATE_PLUGIN_LINK OFF) + if (NOT YUP_PLATFORM_MAC OR XCODE) + set (SMTG_RUN_VST_VALIDATOR ON) + else() + set (SMTG_RUN_VST_VALIDATOR OFF) + endif() + + _yup_fetchcontent_declare (vst3sdk + GIT_REPOSITORY https://github.com/steinbergmedia/vst3sdk.git + GIT_TAG master) + + FetchContent_MakeAvailable (vst3sdk) + endif() + + if (NOT TARGET yup_audio_plugin_host_vst3sdk) + add_library (yup_audio_plugin_host_vst3sdk INTERFACE) + target_link_libraries (yup_audio_plugin_host_vst3sdk INTERFACE sdk) + + set (vst3sdk_source_dir "") + if (DEFINED vst3sdk_SOURCE_DIR) + set (vst3sdk_source_dir "${vst3sdk_SOURCE_DIR}") + elseif (TARGET sdk) + get_target_property (vst3sdk_source_dir sdk SOURCE_DIR) + endif() + + if (vst3sdk_source_dir AND EXISTS "${vst3sdk_source_dir}/public.sdk/source/common/memorystream.cpp") + target_sources (yup_audio_plugin_host_vst3sdk INTERFACE + "${vst3sdk_source_dir}/public.sdk/source/common/memorystream.cpp") + endif() + + if (vst3sdk_source_dir AND EXISTS "${vst3sdk_source_dir}/public.sdk/source/vst/hosting/parameterchanges.cpp") + target_sources (yup_audio_plugin_host_vst3sdk INTERFACE + "${vst3sdk_source_dir}/public.sdk/source/vst/hosting/parameterchanges.cpp") + endif() + endif() +endfunction() + +#============================================================================== + +function (_yup_target_list_contains target_list target_name output_variable) + foreach (target IN LISTS target_list) + if ("${target}" STREQUAL "${target_name}" OR "${target}" STREQUAL "yup::${target_name}") + set (${output_variable} ON PARENT_SCOPE) + return() + endif() + + if (TARGET "${target}") + get_target_property (aliased_target "${target}" ALIASED_TARGET) + if ("${aliased_target}" STREQUAL "${target_name}") + set (${output_variable} ON PARENT_SCOPE) + return() + endif() + endif() + endforeach() + + set (${output_variable} OFF PARENT_SCOPE) +endfunction() + +#============================================================================== + +function (_yup_definitions_enable definitions definition_name output_variable) + set (enabled OFF) + + foreach (definition IN LISTS definitions) + string (REGEX REPLACE "^-D" "" normalized_definition "${definition}") + + if (normalized_definition MATCHES "^${definition_name}($|=)") + set (enabled ON) + + if (normalized_definition MATCHES "^${definition_name}=") + string (REGEX REPLACE "^${definition_name}=(.*)$" "\\1" definition_value "${normalized_definition}") + string (STRIP "${definition_value}" definition_value) + string (REGEX REPLACE "^\"(.*)\"$" "\\1" definition_value "${definition_value}") + string (REGEX REPLACE "^'(.*)'$" "\\1" definition_value "${definition_value}") + string (TOUPPER "${definition_value}" definition_value) + + if ("${definition_value}" STREQUAL "0" + OR "${definition_value}" STREQUAL "OFF" + OR "${definition_value}" STREQUAL "FALSE" + OR "${definition_value}" STREQUAL "NO") + set (enabled OFF) + endif() + endif() + endif() + endforeach() + + set (${output_variable} "${enabled}" PARENT_SCOPE) +endfunction() + +#============================================================================== + +function (_yup_collect_audio_plugin_host_dependencies definitions output_variable) + set (dependencies "") + + _yup_definitions_enable ("${definitions}" YUP_AUDIO_PLUGIN_HOST_ENABLE_CLAP enable_clap) + if (enable_clap) + _yup_fetch_clap() + list (APPEND dependencies clap) + endif() + + _yup_definitions_enable ("${definitions}" YUP_AUDIO_PLUGIN_HOST_ENABLE_VST3 enable_vst3) + if (enable_vst3) + _yup_fetch_vst3sdk() + list (APPEND dependencies yup_audio_plugin_host_vst3sdk) + endif() + + _yup_definitions_enable ("${definitions}" YUP_AUDIO_PLUGIN_HOST_ENABLE_AU enable_au) + if (enable_au AND YUP_PLATFORM_MAC) + list (APPEND dependencies + "-framework AudioUnit" + "-framework AudioToolbox" + "-framework CoreAudio" + "-framework CoreFoundation") + endif() + + set (${output_variable} "${dependencies}" PARENT_SCOPE) +endfunction() + +#============================================================================== + function (_yup_fetch_perfetto) if (TARGET perfetto::perfetto) return() diff --git a/cmake/yup_modules.cmake b/cmake/yup_modules.cmake index 7228ac17a..c9be8ac3f 100644 --- a/cmake/yup_modules.cmake +++ b/cmake/yup_modules.cmake @@ -802,6 +802,11 @@ function (yup_add_module module_path modules_definitions module_group) list (APPEND module_defines ${module_definition}) endforeach() + if ("${module_name}" STREQUAL "yup_audio_plugin_host") + _yup_collect_audio_plugin_host_dependencies ("${module_defines}" audio_plugin_host_dependencies) + list (APPEND module_dependencies ${audio_plugin_host_dependencies}) + endif() + # ==== Prepare include paths get_filename_component (module_include_path ${module_path} DIRECTORY) list (APPEND module_include_paths "${module_include_path}" "${module_path}") diff --git a/cmake/yup_standalone.cmake b/cmake/yup_standalone.cmake index fd0abb187..3465afaee 100644 --- a/cmake/yup_standalone.cmake +++ b/cmake/yup_standalone.cmake @@ -102,6 +102,12 @@ function (yup_standalone_app) list (APPEND additional_libraries perfetto::perfetto) endif() + _yup_target_list_contains ("${YUP_ARG_MODULES}" yup_audio_plugin_host has_audio_plugin_host) + if (has_audio_plugin_host) + _yup_collect_audio_plugin_host_dependencies ("${YUP_ARG_DEFINITIONS}" audio_plugin_host_libraries) + list (APPEND additional_libraries ${audio_plugin_host_libraries}) + endif() + # ==== Prepare executable set (executable_options "") if (NOT "${target_console}") diff --git a/examples/graphics/CMakeLists.txt b/examples/graphics/CMakeLists.txt index 8d0dd340e..f4762971e 100644 --- a/examples/graphics/CMakeLists.txt +++ b/examples/graphics/CMakeLists.txt @@ -60,6 +60,9 @@ yup_standalone_app ( TARGET_APP_NAMESPACE "org.yup" DEFINITIONS YUP_EXAMPLE_GRAPHICS_RIVE_FILE="${rive_file}" + YUP_AUDIO_PLUGIN_HOST_ENABLE_CLAP=1 + YUP_AUDIO_PLUGIN_HOST_ENABLE_AU=1 + YUP_AUDIO_PLUGIN_HOST_ENABLE_VST3=1 PRELOAD_FILES "${CMAKE_CURRENT_LIST_DIR}/${rive_file}@${rive_file}" MODULES @@ -73,6 +76,7 @@ yup_standalone_app ( yup::yup_audio_gui yup::yup_audio_processors yup::yup_audio_formats + yup::yup_audio_plugin_host pffft_library opus_library flac_library diff --git a/examples/graphics/source/examples/PluginHost.h b/examples/graphics/source/examples/PluginHost.h new file mode 100644 index 000000000..12ca9eef0 --- /dev/null +++ b/examples/graphics/source/examples/PluginHost.h @@ -0,0 +1,1046 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +//============================================================================== + +/** + Demonstrates scanning for audio plugins, listing results, loading one, + and running a drum loop through it — similar to CrossoverDemo. +*/ +class PluginHostDemo : public yup::Component + , public yup::AudioIODeviceCallback + , public yup::Timer +{ + class PluginEditorWindow : public yup::DocumentWindow + { + public: + PluginEditorWindow (yup::StringRef windowTitle, + yup::AudioProcessorEditor* editorToOwn, + std::function onCloseCallback) + : yup::DocumentWindow (makeWindowOptions (editorToOwn), yup::Color (0xff101417)) + , editor (editorToOwn) + , onClose (std::move (onCloseCallback)) + { + setTitle (windowTitle); + + addAndMakeVisible (*editor); + // DocumentWindow is already native here, so late children need this hook manually. + editor->attachedToNative(); + takeKeyboardFocus(); + } + + void resized() override + { + editor->setBounds (getLocalBounds()); + } + + void userTriedToCloseWindow() override + { + if (onClose != nullptr) + yup::MessageManager::callAsync (onClose); + } + + private: + static yup::ComponentNative::Options makeWindowOptions (yup::AudioProcessorEditor* editor) + { + return yup::ComponentNative::Options() + .withResizableWindow (editor != nullptr && editor->isResizable()) + .withRenderContinuous (editor != nullptr && editor->shouldRenderContinuous()); + } + + std::unique_ptr editor; + std::function onClose; + }; + + struct ParameterRow : public yup::Component + { + ParameterRow() + : slider (yup::Slider::LinearHorizontal) + { + setOpaque (true); + setWantsMouseEvents (false, true); + + const auto font = yup::ApplicationTheme::getGlobalTheme()->getDefaultFont().withHeight (12.0f); + nameLabel.setFont (font); + valueLabel.setFont (font); + valueLabel.setJustification (yup::Justification::right); + + nameLabel.setColor (yup::Label::Style::textFillColorId, yup::Color (0xffe8ecef)); + valueLabel.setColor (yup::Label::Style::textFillColorId, yup::Color (0xffb7c1c8)); + + slider.setColor (yup::Slider::Style::backgroundColorId, yup::Color (0xff343b40)); + slider.setColor (yup::Slider::Style::trackColorId, yup::Color (0xff6d7d88)); + slider.setColor (yup::Slider::Style::thumbColorId, yup::Color (0xffe0a84d)); + slider.setColor (yup::Slider::Style::thumbOverColorId, yup::Color (0xfff0ba60)); + slider.setColor (yup::Slider::Style::thumbDownColorId, yup::Color (0xffc78f36)); + + addAndMakeVisible (nameLabel); + addAndMakeVisible (valueLabel); + addAndMakeVisible (slider); + + slider.onDragStart = [this] (const yup::MouseEvent&) + { + if (parameter != nullptr) + parameter->beginChangeGesture(); + }; + + slider.onValueChanged = [this] (double value) + { + if (parameter == nullptr) + return; + + parameter->setValueNotifyingHost (static_cast (value)); + updateValueLabel(); + }; + + slider.onDragEnd = [this] (const yup::MouseEvent&) + { + if (parameter != nullptr) + parameter->endChangeGesture(); + }; + } + + void setParameter (yup::AudioParameter::Ptr newParameter) + { + parameter = std::move (newParameter); + + if (parameter == nullptr) + { + nameLabel.setText ({}, yup::dontSendNotification); + valueLabel.setText ({}, yup::dontSendNotification); + return; + } + + nameLabel.setText (parameter->getName(), yup::dontSendNotification); + slider.setRange (static_cast (parameter->getMinimumValue()), + static_cast (parameter->getMaximumValue())); + slider.setDefaultValue (static_cast (parameter->getDefaultValue())); + refreshValue(); + } + + void refreshValue() + { + if (parameter == nullptr) + return; + + slider.setValue (static_cast (parameter->getValue()), yup::dontSendNotification); + updateValueLabel(); + } + + void paint (yup::Graphics& g) override + { + g.setFillColor (yup::Color (0xff20262a)); + g.fillAll(); + + g.setStrokeColor (yup::Color (0xff31383e)); + g.setStrokeWidth (1.0f); + g.strokeLine (0.0f, getHeight() - 0.5f, getWidth(), getHeight() - 0.5f); + } + + void resized() override + { + auto bounds = getLocalBounds().reduced (8, 4); + auto labelArea = bounds.removeFromLeft (yup::jmin (180.0f, bounds.getWidth() * 0.35f)); + nameLabel.setBounds (labelArea.removeFromTop (bounds.getHeight())); + + bounds.removeFromLeft (8); + valueLabel.setBounds (bounds.removeFromRight (84)); + bounds.removeFromRight (8); + slider.setBounds (bounds); + } + + private: + void updateValueLabel() + { + if (parameter != nullptr) + valueLabel.setText (parameter->convertToString (parameter->getValue()), yup::dontSendNotification); + } + + yup::AudioParameter::Ptr parameter; + yup::Label nameLabel; + yup::Label valueLabel; + yup::Slider slider; + }; + + struct ParameterListModel : public yup::ListBoxModel + { + int getNumRows() override + { + return static_cast (parameters.size()); + } + + yup::Component* refreshComponentForRow (int rowIndex, yup::Component* existingComponent) override + { + auto* row = dynamic_cast (existingComponent); + if (row == nullptr) + row = new ParameterRow(); + + if (rowIndex >= 0 && rowIndex < static_cast (parameters.size())) + row->setParameter (parameters[static_cast (rowIndex)]); + else + row->setParameter (nullptr); + + return row; + } + + void setParameters (std::vector newParameters) + { + parameters = std::move (newParameters); + } + + void clear() + { + parameters.clear(); + } + + private: + std::vector parameters; + }; + +public: + PluginHostDemo() + : dryWetSlider (yup::Slider::LinearHorizontal) + { + // Load the drum loop audio file + loadAudioFile(); + + // Audio device manager + audioDeviceManager.initialiseWithDefaultDevices (0, 2); + + // Initialize the plugin scanner + scanner = std::make_unique(); + + // Register available format backends +#if YUP_AUDIO_PLUGIN_HOST_ENABLE_VST3 + scanner->addFormat (std::make_unique()); +#endif +#if YUP_AUDIO_PLUGIN_HOST_ENABLE_CLAP + scanner->addFormat (std::make_unique()); +#endif +#if YUP_AUDIO_PLUGIN_HOST_ENABLE_AU && YUP_MAC + scanner->addFormat (std::make_unique()); +#endif + + // Build UI + createUI(); + + startTimerHz (20); + } + + ~PluginHostDemo() override + { + scanLifetime->store (false); + if (scanner != nullptr) + scanner->cancelPendingScans(); + + unloadPlugin(); + audioDeviceManager.removeAudioCallback (this); + audioDeviceManager.closeAudioDevice(); + } + + void resized() override + { + auto bounds = getLocalBounds().reduced (8); + + // Top bar: scan button and status + auto topBar = bounds.removeFromTop (32); + scanButton.setBounds (topBar.removeFromLeft (140)); + topBar.removeFromLeft (8); + statusLabel.setBounds (topBar); + + bounds.removeFromTop (8); + + // Plugin list takes left portion + auto listArea = bounds.removeFromLeft (220); + scanResultsLabel.setBounds (listArea.removeFromTop (22)); + pluginList.setBounds (listArea.reduced (0, 4)); + + bounds.removeFromLeft (8); + + // Plugin info + load/unload controls + auto infoArea = bounds.removeFromTop (126); + + auto infoLeft = infoArea.removeFromLeft (infoArea.getWidth() / 2); + nameLabel.setBounds (infoLeft.removeFromTop (20)); + vendorLabel.setBounds (infoLeft.removeFromTop (18)); + formatLabel.setBounds (infoLeft.removeFromTop (18)); + infoLeft.removeFromTop (8); + presetLabel.setBounds (infoLeft.removeFromTop (18)); + presetCombo.setBounds (infoLeft.removeFromTop (28).reduced (0, 2)); + + auto infoRight = infoArea; + infoRight.removeFromTop (4); + loadButton.setBounds (infoRight.removeFromTop (28).reduced (2, 0)); + infoRight.removeFromTop (6); + unloadButton.setBounds (infoRight.removeFromTop (28).reduced (2, 0)); + infoRight.removeFromTop (6); + openEditorButton.setBounds (infoRight.removeFromTop (28).reduced (2, 0)); + infoRight.removeFromTop (6); + dryWetLabel.setBounds (infoRight.removeFromTop (20)); + + bounds.removeFromTop (8); + + // Dry/wet slider + auto sliderRow = bounds.removeFromTop (36); + dryWetSlider.setBounds (sliderRow); + + bounds.removeFromTop (8); + + // Parameter controls area (generic fallback UI) + parameterList.setBounds (bounds); + } + + void paint (yup::Graphics& g) override + { + g.setFillColor (yup::Color (0xff1b2024)); + g.fillAll(); + } + + void visibilityChanged() override + { + if (! isVisible()) + audioDeviceManager.removeAudioCallback (this); + else + audioDeviceManager.addAudioCallback (this); + } + + //============================================================================== + // AudioIODeviceCallback + + void audioDeviceIOCallbackWithContext (const float* const* inputChannelData, + int numInputChannels, + float* const* outputChannelData, + int numOutputChannels, + int numSamples, + const yup::AudioIODeviceCallbackContext& context) override + { + // Zero outputs + for (int ch = 0; ch < numOutputChannels; ++ch) + yup::FloatVectorOperations::clear (outputChannelData[ch], numSamples); + + // Read from drum loop buffer into a temp buffer for processing + for (int i = 0; i < numSamples; ++i) + { + // Read drum loop (looping) + float sample = 0.0f; + if (audioBufferLoaded) + { + const int numChannels = audioBuffer.getNumChannels(); + const int totalSamples = audioBuffer.getNumSamples(); + + for (int ch = 0; ch < yup::jmin (2, numChannels); ++ch) + sample += audioBuffer.getSample (ch, readPosition) * 0.5f; + sample /= yup::jmin (2, numChannels); + + readPosition++; + if (readPosition >= totalSamples) + readPosition = 0; + } + + // Write as stereo + processBuffer.setSample (0, i, sample); + processBuffer.setSample (1, i, sample); + dryBuffer.setSample (0, i, sample); + dryBuffer.setSample (1, i, sample); + } + + // If a plugin is loaded, process through it + yup::MidiBuffer midi; + + { + const yup::ScopedLock lock (pluginLock); + + if (loadedPlugin != nullptr) + loadedPlugin->processBlock (processBuffer, midi); + } + + // Mix dry/wet into output + const float wet = dryWetMix.getNextValue(); + const float dry = 1.0f - wet; + + for (int i = 0; i < numSamples; ++i) + { + for (int ch = 0; ch < yup::jmin (2, numOutputChannels); ++ch) + { + outputChannelData[ch][i] = dryBuffer.getSample (ch, i) * dry + + processBuffer.getSample (ch, i) * wet; + } + } + } + + void audioDeviceAboutToStart (yup::AudioIODevice* device) override + { + const auto sr = device->getCurrentSampleRate(); + const auto bs = device->getCurrentBufferSizeSamples(); + + readPosition = 0; + dryWetMix.reset (sr, 0.02); + + processBuffer.setSize (2, bs); + dryBuffer.setSize (2, bs); + + const yup::ScopedLock lock (pluginLock); + if (loadedPlugin != nullptr) + loadedPlugin->prepareToPlay (sr, bs); + } + + void audioDeviceStopped() override + { + const yup::ScopedLock lock (pluginLock); + if (loadedPlugin != nullptr) + loadedPlugin->releaseResources(); + } + + //============================================================================== + // Timer + + void timerCallback() override + { + updateStatusLine(); + + // Update visible parameter rows from plugin values + const yup::ScopedLock lock (pluginLock); + if (loadedPlugin != nullptr) + refreshVisibleParameterRows(); + } + +private: + //============================================================================== + + void loadAudioFile() + { +#if YUP_WASM + auto dataDir = yup::File ("/data"); +#else + auto dataDir = yup::File (__FILE__) + .getParentDirectory() + .getParentDirectory() + .getParentDirectory() + .getChildFile ("data"); +#endif + + yup::File audioFile = dataDir.getChildFile ("break_boomblastic_92bpm.mp3"); + if (! audioFile.existsAsFile()) + { + statusText = "Could not find drum loop file"; + return; + } + + yup::AudioFormatManager formatManager; + formatManager.registerDefaultFormats(); + + if (auto reader = formatManager.createReaderFor (audioFile)) + { + audioBuffer.setSize ((int) reader->numChannels, (int) reader->lengthInSamples); + reader->read (&audioBuffer, 0, (int) reader->lengthInSamples, 0, true, true); + audioBufferLoaded = true; + + std::cout << "Loaded: " << audioFile.getFileName() << std::endl; + std::cout << "Sample rate: " << reader->sampleRate << " Hz, " + << reader->numChannels << " ch, " + << reader->lengthInSamples << " samples" << std::endl; + } + else + { + statusText = "Failed to read drum loop"; + } + } + + void createUI() + { + setOpaque (false); + + // Scan button + scanButton.setButtonText ("Scan for Plugins"); + scanButton.onClick = [this] + { + performScan(); + }; + addAndMakeVisible (scanButton); + + // Status label + statusLabel.setFont (yup::ApplicationTheme::getGlobalTheme()->getDefaultFont().withHeight (12.0f)); + statusLabel.setText ("Ready. Click 'Scan for Plugins' to begin.", yup::dontSendNotification); + addAndMakeVisible (statusLabel); + + // Scan results label + scanResultsLabel.setFont (yup::ApplicationTheme::getGlobalTheme()->getDefaultFont().withHeight (12.0f)); + scanResultsLabel.setText ("Plugins (0 found)", yup::dontSendNotification); + addAndMakeVisible (scanResultsLabel); + + // Plugin list box + pluginList.setRowHeight (24); + pluginList.setModel (&pluginListModel); + pluginListModel.onSelectedRowChanged = [this] (int row) + { + if (row >= 0 && row < static_cast (discoveredPlugins.size())) + { + selectedPluginIndex = row; + updateInfoForSelectedPlugin(); + } + }; + addAndMakeVisible (pluginList); + + // Info labels + auto infoFont = yup::ApplicationTheme::getGlobalTheme()->getDefaultFont().withHeight (13.0f); + nameLabel.setFont (infoFont); + nameLabel.setText ("No plugin selected", yup::dontSendNotification); + addAndMakeVisible (nameLabel); + + vendorLabel.setFont (infoFont); + vendorLabel.setText ("", yup::dontSendNotification); + addAndMakeVisible (vendorLabel); + + formatLabel.setFont (infoFont); + formatLabel.setText ("", yup::dontSendNotification); + addAndMakeVisible (formatLabel); + + // Load / Unload buttons + loadButton.setButtonText ("Load Plugin"); + loadButton.onClick = [this] + { + loadSelectedPlugin(); + }; + addAndMakeVisible (loadButton); + + unloadButton.setButtonText ("Unload Plugin"); + unloadButton.onClick = [this] + { + unloadPlugin(); + }; + addAndMakeVisible (unloadButton); + + openEditorButton.setButtonText ("Open Editor"); + openEditorButton.onClick = [this] + { + openPluginEditor(); + }; + openEditorButton.setEnabled (false); + addAndMakeVisible (openEditorButton); + + presetLabel.setFont (infoFont); + presetLabel.setText ("Preset", yup::dontSendNotification); + addAndMakeVisible (presetLabel); + + presetCombo.setTextWhenNothingSelected ("No presets"); + presetCombo.setEnabled (false); + presetCombo.onSelectedItemChanged = [this] + { + changeSelectedPreset(); + }; + addAndMakeVisible (presetCombo); + + // Dry/Wet + dryWetLabel.setFont (infoFont); + dryWetLabel.setText ("Dry / Wet Mix", yup::dontSendNotification); + addAndMakeVisible (dryWetLabel); + + dryWetSlider.setRange (0.0, 1.0); + dryWetSlider.setValue (0.5); + dryWetSlider.onValueChanged = [this] (double value) + { + dryWetMix.setTargetValue ((float) value); + }; + addAndMakeVisible (dryWetSlider); + + dryWetMix.setCurrentAndTargetValue (0.5f); + + // Parameter list area + parameterList.setRowHeight (40); + parameterList.setSelectionMode (yup::ListBox::SelectionMode::none); + parameterList.setColor (yup::ListBox::Style::backgroundColorId, yup::Color (0xff20262a)); + parameterList.setColor (yup::ListBox::Style::outlineColorId, yup::Color (0xff31383e)); + parameterList.setColor (yup::ListBox::Style::rowBackgroundColorId, yup::Color (0xff20262a)); + parameterList.setColor (yup::ListBox::Style::selectedRowBackgroundColorId, yup::Color (0xff20262a)); + parameterList.setColor (yup::ListBox::Style::hoveredRowBackgroundColorId, yup::Color (0xff242b30)); + parameterList.setModel (¶meterListModel); + addAndMakeVisible (parameterList); + parameterList.setVisible (false); + + // Start audio + audioDeviceManager.addAudioCallback (this); + } + + void performScan() + { + if (scanInProgress.exchange (true)) + { + statusText = "Scan already in progress."; + return; + } + + discoveredPlugins.clear(); + pluginListModel.clear(); + pluginList.updateContent(); + selectedPluginIndex = -1; + clearPresetCombo(); + updateEditorButtonState(); + + if (scanner == nullptr || scanner->getNumFormats() == 0) + { + scanInProgress = false; + statusText = "No plugin formats registered."; + return; + } + + statusText = "Scanning..."; + scanButton.setEnabled (false); + scanResultsLabel.setText ("Plugins (scanning...)", yup::dontSendNotification); + + const auto lifetime = scanLifetime; + scanner->scanDefaultsAsync ([this, lifetime] (yup::AudioPluginScanner::ScanResult result) mutable + { + if (! lifetime->load()) + return; + + yup::MessageManager::callAsync ([this, lifetime, result = std::move (result)]() mutable + { + if (! lifetime->load()) + return; + + handleScanFinished (std::move (result)); + }); + }); + } + + void handleScanFinished (yup::AudioPluginScanner::ScanResult result) + { + discoveredPlugins = std::move (result.discovered); + statusText = yup::String ("Found ") + yup::String (discoveredPlugins.size()) + " plugins."; + + if (! result.failedPaths.empty()) + statusText += " (" + yup::String ((int) result.failedPaths.size()) + " failed)"; + + // Populate list model + yup::Array names; + for (const auto& desc : discoveredPlugins) + names.add (desc.name + " [" + formatTypeToString (desc.formatType) + "]"); + + pluginListModel.setItems (std::move (names)); + pluginList.updateContent(); + scanResultsLabel.setText ("Plugins (" + yup::String (discoveredPlugins.size()) + " found)", + yup::dontSendNotification); + scanButton.setEnabled (true); + scanInProgress = false; + } + + void updateInfoForSelectedPlugin() + { + if (selectedPluginIndex < 0 || selectedPluginIndex >= static_cast (discoveredPlugins.size())) + return; + + const auto& desc = discoveredPlugins[static_cast (selectedPluginIndex)]; + + nameLabel.setText (desc.name, yup::dontSendNotification); + vendorLabel.setText ("Vendor: " + desc.vendor, yup::dontSendNotification); + + auto formatText = "Format: " + formatTypeToString (desc.formatType); + if (desc.numInputChannels > 0 || desc.numOutputChannels > 0) + { + formatText += " | " + yup::String (desc.numInputChannels) + " in / " + + yup::String (desc.numOutputChannels) + " out"; + } + + formatLabel.setText (formatText, yup::dontSendNotification); + } + + void loadSelectedPlugin() + { + if (selectedPluginIndex < 0 || selectedPluginIndex >= static_cast (discoveredPlugins.size())) + { + statusText = "No plugin selected."; + return; + } + + const auto& desc = discoveredPlugins[static_cast (selectedPluginIndex)]; + + // Find the right format backend + auto* format = scanner->getFormatForType (desc.formatType); + if (format == nullptr) + { + statusText = "No format backend for: " + formatTypeToString (desc.formatType); + return; + } + + yup::AudioPluginHostContext hostCtx; + hostCtx.sampleRate = audioDeviceManager.getCurrentAudioDevice() + ? audioDeviceManager.getCurrentAudioDevice()->getCurrentSampleRate() + : 44100.0f; + hostCtx.maxBlockSize = audioDeviceManager.getCurrentAudioDevice() + ? audioDeviceManager.getCurrentAudioDevice()->getCurrentBufferSizeSamples() + : 512; + + auto result = format->loadPlugin (desc, hostCtx); + + if (result.failed()) + { + statusText = "Failed to load: " + result.getErrorMessage(); + return; + } + + closePluginEditor(); + clearParameterSliders(); + clearPresetCombo(); + + { + const yup::ScopedLock lock (pluginLock); + unloadPluginInternal(); + loadedPlugin = std::move (result).getValue(); + loadedPlugin->prepareToPlay (hostCtx.sampleRate, hostCtx.maxBlockSize); + } + + buildParameterSliders(); + populatePresetCombo(); + updateEditorButtonState(); + + if (statusText.isEmpty() || ! statusText.startsWith ("Loaded: ")) + statusText = "Loaded: " + desc.name; + } + + void unloadPlugin() + { + closePluginEditor(); + clearParameterSliders(); + clearPresetCombo(); + + { + const yup::ScopedLock lock (pluginLock); + unloadPluginInternal(); + } + + updateEditorButtonState(); + statusText = "Plugin unloaded."; + } + + void unloadPluginInternal() + { + if (loadedPlugin != nullptr) + { + loadedPlugin->releaseResources(); + loadedPlugin.reset(); + } + } + + void openPluginEditor() + { + if (pluginEditorWindow != nullptr) + { + pluginEditorWindow->toFront (true); + return; + } + + yup::AudioProcessorEditor* editor = nullptr; + yup::String pluginName; + + { + const yup::ScopedLock lock (pluginLock); + + if (loadedPlugin == nullptr) + { + statusText = "No plugin loaded."; + return; + } + + if (! loadedPlugin->hasEditor()) + { + statusText = "Loaded plugin has no editor."; + return; + } + + pluginName = loadedPlugin->getDescription().name; + editor = loadedPlugin->createEditor(); + } + + if (editor == nullptr) + { + statusText = "Could not create plugin editor."; + updateEditorButtonState(); + return; + } + + const auto preferredSize = editor->getPreferredSize(); + yup::WeakReference weakThis (this); + + pluginEditorWindow = std::make_unique ( + pluginName, + editor, + [weakThis] + { + if (auto* component = weakThis.get()) + if (auto* demo = dynamic_cast (component)) + demo->closePluginEditor(); + }); + + pluginEditorWindow->centreWithSize (preferredSize); + pluginEditorWindow->setVisible (true); + pluginEditorWindow->toFront (true); + + updateEditorButtonState(); + } + + void closePluginEditor() + { + pluginEditorWindow.reset(); + updateEditorButtonState(); + } + + void updateEditorButtonState() + { + bool canOpenEditor = false; + + { + const yup::ScopedLock lock (pluginLock); + canOpenEditor = loadedPlugin != nullptr && loadedPlugin->hasEditor(); + } + + openEditorButton.setEnabled (canOpenEditor); + openEditorButton.setButtonText (pluginEditorWindow != nullptr ? "Show Editor" : "Open Editor"); + } + + void populatePresetCombo() + { + clearPresetCombo(); + + const yup::ScopedLock lock (pluginLock); + if (loadedPlugin == nullptr) + return; + + const auto numPresets = loadedPlugin->getNumPresets(); + if (numPresets <= 0) + return; + + for (int i = 0; i < numPresets; ++i) + { + auto name = loadedPlugin->getPresetName (i); + if (name.isEmpty()) + name = "Preset " + yup::String (i + 1); + + presetCombo.addItem (name, i + 1); + } + + const auto currentPreset = loadedPlugin->getCurrentPreset(); + if (yup::isPositiveAndBelow (currentPreset, numPresets)) + presetCombo.setSelectedId (currentPreset + 1, yup::dontSendNotification); + + presetCombo.setEnabled (true); + } + + void clearPresetCombo() + { + presetCombo.clear(); + presetCombo.setEnabled (false); + } + + void changeSelectedPreset() + { + const auto selectedPreset = presetCombo.getSelectedId() - 1; + if (selectedPreset < 0) + return; + + yup::String presetName; + + { + const yup::ScopedLock lock (pluginLock); + if (loadedPlugin == nullptr) + return; + + loadedPlugin->setCurrentPreset (selectedPreset); + presetName = loadedPlugin->getPresetName (selectedPreset); + } + + statusText = "Preset: " + (presetName.isNotEmpty() ? presetName : yup::String (selectedPreset + 1)); + refreshVisibleParameterRows(); + } + + void buildParameterSliders() + { + clearParameterSliders(); + + if (loadedPlugin == nullptr) + return; + + const auto params = loadedPlugin->getParameters(); + if (params.empty()) + { + statusText = "Loaded: " + loadedPlugin->getDescription().name + " (no adjustable parameters)"; + return; + } + + std::vector parameters; + parameters.reserve (params.size()); + + for (auto& param : params) + parameters.push_back (param); + + parameterListModel.setParameters (std::move (parameters)); + parameterList.updateContent(); + parameterList.setVisible (true); + + resized(); + } + + void clearParameterSliders() + { + parameterListModel.clear(); + parameterList.updateContent(); + parameterList.setVisible (false); + resized(); + } + + void refreshVisibleParameterRows() + { + if (parameterListModel.getNumRows() <= 0) + return; + + const auto visibleRows = parameterList.getVisibleRowRange(); + for (int row = visibleRows.getStart(); row < visibleRows.getEnd(); ++row) + if (auto* parameterRow = dynamic_cast (parameterList.getComponentForRow (row))) + parameterRow->refreshValue(); + } + + void updateStatusLine() + { + statusLabel.setText (statusText, yup::dontSendNotification); + } + + static yup::String formatTypeToString (yup::AudioPluginFormatType type) + { + switch (type) + { + case yup::AudioPluginFormatType::vst3: + return "VST3"; + case yup::AudioPluginFormatType::clap: + return "CLAP"; + case yup::AudioPluginFormatType::audioUnit: + return "AU"; + default: + return "Unknown"; + } + } + + //============================================================================== + // Plugin list model + + struct PluginListModel : public yup::ListBoxModel + { + int getNumRows() override + { + return static_cast (items.size()); + } + + yup::String getRowText (int rowIndex) override + { + if (rowIndex >= 0 && rowIndex < static_cast (items.size())) + return items[rowIndex]; + return {}; + } + + void selectedRowsChanged (const yup::Array& selectedRows) override + { + if (onSelectedRowChanged && ! selectedRows.isEmpty()) + onSelectedRowChanged (selectedRows[0]); + } + + void setItems (yup::Array items) + { + this->items = std::move (items); + } + + void clear() + { + items.clear(); + } + + std::function onSelectedRowChanged; + + private: + yup::Array items; + }; + + //============================================================================== + + // Audio + yup::AudioDeviceManager audioDeviceManager; + yup::AudioBuffer audioBuffer; + yup::AudioBuffer processBuffer; + yup::AudioBuffer dryBuffer; + + bool audioBufferLoaded = false; + int readPosition = 0; + + // Plugin hosting + std::shared_ptr> scanLifetime { std::make_shared> (true) }; + std::atomic scanInProgress { false }; + std::unique_ptr scanner; + std::vector discoveredPlugins; + int selectedPluginIndex = -1; + std::unique_ptr loadedPlugin; + std::unique_ptr pluginEditorWindow; + yup::CriticalSection pluginLock; + + // Dry/wet mix + yup::SmoothedValue dryWetMix; + + // UI + yup::TextButton scanButton; + yup::Label statusLabel; + yup::Label scanResultsLabel; + yup::ListBox pluginList; + PluginListModel pluginListModel; + + yup::Label nameLabel; + yup::Label vendorLabel; + yup::Label formatLabel; + + yup::TextButton loadButton; + yup::TextButton unloadButton; + yup::TextButton openEditorButton; + + yup::Label presetLabel; + yup::ComboBox presetCombo; + + yup::Label dryWetLabel; + yup::Slider dryWetSlider; + + yup::ListBox parameterList; + ParameterListModel parameterListModel; + + yup::String statusText; +}; diff --git a/examples/graphics/source/main.cpp b/examples/graphics/source/main.cpp index a943ca89a..b0ba11e12 100644 --- a/examples/graphics/source/main.cpp +++ b/examples/graphics/source/main.cpp @@ -47,6 +47,7 @@ #include "examples/LayoutFonts.h" #include "examples/OpaqueDemo.h" #include "examples/Paths.h" +#include "examples/PluginHost.h" #include "examples/PopupMenu.h" #include "examples/ScrollBarDemo.h" #include "examples/SliderDemo.h" @@ -149,6 +150,7 @@ class CustomWindow registerDemo ("Layout Fonts", counter++); registerDemo ("Opaque Demo", counter++); registerDemo ("Paths", counter++); + registerDemo ("Plugin Host", counter++); registerDemo ("Popup Menu", counter++); registerDemo ("ScrollBar", counter++); registerDemo ("Sliders", counter++); diff --git a/modules/yup_audio_plugin_host/host/yup_AudioPluginDescription.h b/modules/yup_audio_plugin_host/host/yup_AudioPluginDescription.h new file mode 100644 index 000000000..b1f7892d7 --- /dev/null +++ b/modules/yup_audio_plugin_host/host/yup_AudioPluginDescription.h @@ -0,0 +1,92 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +/** + Stable metadata describing a discovered audio plugin. + + Instances are produced by AudioPluginScanner and consumed by + AudioPluginFormat::loadPlugin(). Fields are format-independent. +*/ +struct AudioPluginDescription +{ + /** The plugin format. */ + AudioPluginFormatType formatType = AudioPluginFormatType::unknown; + + /** Human-readable name (e.g. "Surge XT"). */ + String name; + + /** Plugin vendor / manufacturer name. */ + String vendor; + + /** Version string as reported by the plugin. */ + String version; + + /** Plugin category string (e.g. "Instrument", "Fx"). */ + String category; + + /** + Format-specific unique identifier. + - VST3: base64-encoded 16-byte FUID + - CLAP: clap_plugin_descriptor::id string + - AUv2: "type/subt/mfgr" four-char-code triplet + */ + String identifier; + + /** Absolute path to the plugin file or bundle on disk. */ + String fileOrBundlePath; + + /** True when the plugin is a synthesiser / instrument. */ + bool isInstrument = false; + + /** True when the plugin is an audio effect. */ + bool isEffect = false; + + /** Reported input channel count (sum across main audio input buses). */ + int numInputChannels = 0; + + /** Reported output channel count (sum across main audio output buses). */ + int numOutputChannels = 0; + + bool operator== (const AudioPluginDescription& other) const noexcept + { + return formatType == other.formatType + && name == other.name + && vendor == other.vendor + && version == other.version + && category == other.category + && identifier == other.identifier + && fileOrBundlePath == other.fileOrBundlePath + && isInstrument == other.isInstrument + && isEffect == other.isEffect + && numInputChannels == other.numInputChannels + && numOutputChannels == other.numOutputChannels; + } + + bool operator!= (const AudioPluginDescription& other) const noexcept + { + return ! (*this == other); + } +}; + +} // namespace yup diff --git a/modules/yup_audio_plugin_host/host/yup_AudioPluginFormat.h b/modules/yup_audio_plugin_host/host/yup_AudioPluginFormat.h new file mode 100644 index 000000000..4914c9d63 --- /dev/null +++ b/modules/yup_audio_plugin_host/host/yup_AudioPluginFormat.h @@ -0,0 +1,95 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +class AudioPluginInstance; + +/** + Abstract interface implemented by each native plugin format backend. + + One concrete subclass exists per supported format (VST3, CLAP, AUv2). + Clients register format objects with AudioPluginScanner. + + scanFile() may be called from scanner worker threads. Other methods must + be safe for the host call site that invokes them. +*/ +class AudioPluginFormat +{ +public: + virtual ~AudioPluginFormat() = default; + + //============================================================================== + + /** Returns the format type this backend handles. */ + virtual AudioPluginFormatType getFormatType() const = 0; + + /** Returns a human-readable name for the format (e.g. "VST3"). */ + virtual String getFormatName() const = 0; + + //============================================================================== + + /** + Returns the platform-standard search paths for this format. + + - VST3 macOS: /Library/Audio/Plug-Ins/VST3, ~/Library/Audio/Plug-Ins/VST3 + - VST3 Windows: %CommonProgramFiles%\VST3, %APPDATA%\VST3 + - VST3 Linux: /usr/lib/vst3, ~/.vst3 + - CLAP macOS: /Library/Audio/Plug-Ins/CLAP, ~/Library/Audio/Plug-Ins/CLAP + - CLAP Windows: %CommonProgramFiles%\CLAP, %APPDATA%\CLAP + - CLAP Linux: /usr/lib/clap, ~/.clap + - AUv2: Returns empty — AUv2 discovery uses AudioComponent registry. + */ + virtual FileSearchPath getDefaultSearchPaths() const = 0; + + //============================================================================== + + /** + Scans one file or bundle and returns all plugin descriptions it contains. + + Returns a failure result if the file is not a supported plugin for this + format. The scanner calls this only for files whose extension matches + expectations (e.g. ".vst3", ".clap"). + + @param file Absolute path to a file or bundle. + @return All descriptions found, or a failure message. + */ + virtual ResultValue> scanFile (const File& file) = 0; + + //============================================================================== + + /** + Loads and activates a plugin described by @p description. + + The returned instance is ready for prepareToPlay() but is not yet + prepared. Returns a failure result on any error. + + @param description A description previously produced by scanFile(). + @param context Host context supplying sample rate, block size, etc. + @return Owning pointer to the live instance, or a failure. + */ + virtual ResultValue> loadPlugin ( + const AudioPluginDescription& description, + const AudioPluginHostContext& context) = 0; +}; + +} // namespace yup diff --git a/modules/yup_audio_plugin_host/host/yup_AudioPluginFormatType.h b/modules/yup_audio_plugin_host/host/yup_AudioPluginFormatType.h new file mode 100644 index 000000000..f0da8990f --- /dev/null +++ b/modules/yup_audio_plugin_host/host/yup_AudioPluginFormatType.h @@ -0,0 +1,34 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +/** Identifies the native format of a plugin. */ +enum class AudioPluginFormatType +{ + vst3, + clap, + audioUnit, + unknown +}; + +} // namespace yup diff --git a/modules/yup_audio_plugin_host/host/yup_AudioPluginHostContext.h b/modules/yup_audio_plugin_host/host/yup_AudioPluginHostContext.h new file mode 100644 index 000000000..42a3b0861 --- /dev/null +++ b/modules/yup_audio_plugin_host/host/yup_AudioPluginHostContext.h @@ -0,0 +1,56 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +/** + Runtime context provided to a plugin when it is loaded. + + Pass one AudioPluginHostContext to AudioPluginFormat::loadPlugin(). + The same context can be shared across multiple instances; only + sampleRate and maxBlockSize have per-instance meaning. +*/ +struct AudioPluginHostContext +{ + /** Sample rate the plugin will be prepared at. */ + float sampleRate = 44100.0f; + + /** Maximum number of samples per processBlock() call. */ + int maxBlockSize = 512; + + /** True to prefer double-precision processing where the plugin supports it. */ + bool preferDoublePrecision = false; + + /** Optional playhead — the host sets this before processing. */ + AudioPlayHead* playHead = nullptr; + + /** Host application name reported to the plugin. */ + String hostName = "YUP Audio Plugin Host"; + + /** Host vendor reported to the plugin. */ + String hostVendor = "YUP"; + + /** Host version string reported to the plugin. */ + String hostVersion = "1.0.0"; +}; + +} // namespace yup diff --git a/modules/yup_audio_plugin_host/host/yup_AudioPluginInstance.cpp b/modules/yup_audio_plugin_host/host/yup_AudioPluginInstance.cpp new file mode 100644 index 000000000..9115386d3 --- /dev/null +++ b/modules/yup_audio_plugin_host/host/yup_AudioPluginInstance.cpp @@ -0,0 +1,44 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +AudioPluginInstance::AudioPluginInstance (const AudioPluginDescription& description, + AudioBusLayout busLayout) + : AudioProcessor (description.name, std::move (busLayout)) + , pluginDescription (description) +{ +} + +AudioPluginInstance::~AudioPluginInstance() = default; + +const AudioPluginDescription& AudioPluginInstance::getDescription() const noexcept +{ + return pluginDescription; +} + +AudioPluginFormatType AudioPluginInstance::getFormatType() const noexcept +{ + return pluginDescription.formatType; +} + +} // namespace yup diff --git a/modules/yup_audio_plugin_host/host/yup_AudioPluginInstance.h b/modules/yup_audio_plugin_host/host/yup_AudioPluginInstance.h new file mode 100644 index 000000000..a4cf3bb78 --- /dev/null +++ b/modules/yup_audio_plugin_host/host/yup_AudioPluginInstance.h @@ -0,0 +1,62 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +/** + An AudioProcessor that wraps a loaded third-party plugin. + + Concrete subclasses are created by AudioPluginFormat::loadPlugin() — callers + never instantiate this directly. Interact with the plugin through the + AudioProcessor interface; format-specific behaviour stays in the subclass. + + Ownership: returned as std::unique_ptr from loadPlugin(). +*/ +class AudioPluginInstance : public AudioProcessor +{ +public: + //============================================================================== + + /** @internal Used by format backends. */ + AudioPluginInstance (const AudioPluginDescription& description, + AudioBusLayout busLayout); + + ~AudioPluginInstance() override; + + //============================================================================== + + /** Returns the description that was used to load this instance. */ + const AudioPluginDescription& getDescription() const noexcept; + + //============================================================================== + + /** Returns the format type of this instance. */ + AudioPluginFormatType getFormatType() const noexcept; + +protected: + AudioPluginDescription pluginDescription; + +private: + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioPluginInstance) +}; + +} // namespace yup diff --git a/modules/yup_audio_plugin_host/host/yup_AudioPluginScanner.cpp b/modules/yup_audio_plugin_host/host/yup_AudioPluginScanner.cpp new file mode 100644 index 000000000..118a0d8a4 --- /dev/null +++ b/modules/yup_audio_plugin_host/host/yup_AudioPluginScanner.cpp @@ -0,0 +1,251 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +namespace +{ + +bool canScanFileWithFormat (AudioPluginFormatType type, const File& file) +{ + switch (type) + { + case AudioPluginFormatType::vst3: + return file.hasFileExtension (".vst3"); + + case AudioPluginFormatType::clap: + return file.hasFileExtension (".clap"); + + case AudioPluginFormatType::audioUnit: + return false; + } + + return false; +} + +struct FileScanTask +{ + File file; + AudioPluginFormat* format = nullptr; + std::vector discovered; + String failedPath; +}; + +void runFileScanTask (FileScanTask& task) +{ + jassert (task.format != nullptr); + + auto scanResult = task.format->scanFile (task.file); + + if (scanResult.wasOk()) + { + task.discovered = std::move (scanResult).getValue(); + return; + } + + task.failedPath = task.file.getFullPathName(); +} + +} // namespace + +AudioPluginScanner::AudioPluginScanner() + : backgroundScanPool (ThreadPool::Options {} + .withThreadName ("YUP Plugin Scanner") + .withNumberOfThreads (1)) +{ +} + +AudioPluginScanner::~AudioPluginScanner() +{ + cancelPendingScans(); +} + +void AudioPluginScanner::addFormat (std::unique_ptr format) +{ + jassert (format != nullptr); + + const auto type = format->getFormatType(); + + for (auto& existing : formats) + { + if (existing->getFormatType() == type) + { + existing = std::move (format); + return; + } + } + + formats.push_back (std::move (format)); +} + +int AudioPluginScanner::getNumFormats() const noexcept +{ + return static_cast (formats.size()); +} + +AudioPluginFormat* AudioPluginScanner::getFormat (int index) const noexcept +{ + if (index < 0 || index >= static_cast (formats.size())) + return nullptr; + + return formats[static_cast (index)].get(); +} + +AudioPluginFormat* AudioPluginScanner::getFormatForType (AudioPluginFormatType type) const noexcept +{ + for (const auto& f : formats) + { + if (f->getFormatType() == type) + return f.get(); + } + + return nullptr; +} + +AudioPluginScanner::ScanResult AudioPluginScanner::scan (const FileSearchPath& searchPath) +{ + ScanResult result; + + // AUv2 does not use file system scanning; delegate directly if registered. + if (auto* auFormat = getFormatForType (AudioPluginFormatType::audioUnit)) + { + // AUv2 scanFile() with an invalid File triggers registry enumeration + auto auResult = auFormat->scanFile (File {}); + if (auResult.wasOk()) + { + auto descriptions = auResult.getValue(); + result.discovered.insert (result.discovered.end(), + descriptions.begin(), + descriptions.end()); + } + } + + std::vector tasks; + const int numPaths = searchPath.getNumPaths(); + + for (int p = 0; p < numPaths; ++p) + { + Array files; + searchPath[p].findChildFiles (files, File::findFilesAndDirectories, true, "*", File::FollowSymlinks::no); + + for (const auto& file : files) + { + for (const auto& format : formats) + { + if (! canScanFileWithFormat (format->getFormatType(), file)) + continue; + + FileScanTask task; + task.file = file; + task.format = format.get(); + tasks.push_back (std::move (task)); + } + } + } + + if (tasks.empty()) + return result; + + if (tasks.size() == 1) + { + runFileScanTask (tasks.front()); + } + else + { + WaitableEvent finished; + std::atomic remainingTasks { static_cast (tasks.size()) }; + + ThreadPool pool (ThreadPool::Options {} + .withThreadName ("YUP Plugin File Scanner") + .withNumberOfThreads (jmax (1, jmin (SystemStats::getNumCpus(), static_cast (tasks.size()))))); + + for (std::size_t i = 0; i < tasks.size(); ++i) + { + pool.addJob ([&tasks, &remainingTasks, &finished, i] + { + runFileScanTask (tasks[i]); + + if (--remainingTasks == 0) + finished.signal(); + }); + } + + finished.wait(); + } + + for (auto& task : tasks) + { + result.discovered.insert (result.discovered.end(), + task.discovered.begin(), + task.discovered.end()); + + if (task.failedPath.isNotEmpty()) + result.failedPaths.push_back (task.failedPath); + } + + return result; +} + +AudioPluginScanner::ScanResult AudioPluginScanner::scanDefaults() +{ + FileSearchPath combined; + + for (const auto& format : formats) + { + const auto paths = format->getDefaultSearchPaths(); + const int n = paths.getNumPaths(); + + for (int i = 0; i < n; ++i) + combined.addIfNotAlreadyThere (paths[i]); + } + + return scan (combined); +} + +void AudioPluginScanner::scanAsync (const FileSearchPath& searchPath, ScanCallback callback) +{ + backgroundScanPool.addJob ([this, searchPath, callback = std::move (callback)]() mutable + { + auto result = scan (searchPath); + + if (callback) + callback (std::move (result)); + }); +} + +void AudioPluginScanner::scanDefaultsAsync (ScanCallback callback) +{ + backgroundScanPool.addJob ([this, callback = std::move (callback)]() mutable + { + auto result = scanDefaults(); + + if (callback) + callback (std::move (result)); + }); +} + +void AudioPluginScanner::cancelPendingScans() +{ + backgroundScanPool.removeAllJobs (true, -1); +} + +} // namespace yup diff --git a/modules/yup_audio_plugin_host/host/yup_AudioPluginScanner.h b/modules/yup_audio_plugin_host/host/yup_AudioPluginScanner.h new file mode 100644 index 000000000..e59d9aaae --- /dev/null +++ b/modules/yup_audio_plugin_host/host/yup_AudioPluginScanner.h @@ -0,0 +1,124 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +/** + Scans directories for plugins across all registered formats. + + Construct one AudioPluginScanner, register format backends via addFormat(), + then call scan(), scanDefaults(), scanAsync(), or scanDefaultsAsync(). + Results are not cached. + + Thread safety: formats may be scanned concurrently by the scanner. Do not + modify the registered formats while a scan is running. +*/ +class AudioPluginScanner +{ +public: + //============================================================================== + + /** Aggregated output of a scan operation. */ + struct ScanResult + { + /** All successfully parsed plugin descriptions, in discovery order. */ + std::vector discovered; + + /** Absolute paths of files that could not be parsed. */ + std::vector failedPaths; + }; + + //============================================================================== + + /** Callback invoked when an asynchronous scan finishes. */ + using ScanCallback = std::function; + + //============================================================================== + + AudioPluginScanner(); + ~AudioPluginScanner(); + + //============================================================================== + + /** + Registers a format backend. + + If a format with the same AudioPluginFormatType is already registered it + is replaced. Takes ownership. + */ + void addFormat (std::unique_ptr format); + + /** Returns the number of registered formats. */ + int getNumFormats() const noexcept; + + /** Returns the format at @p index, or nullptr if out of range. */ + AudioPluginFormat* getFormat (int index) const noexcept; + + /** Returns the format for the given type, or nullptr if not registered. */ + AudioPluginFormat* getFormatForType (AudioPluginFormatType type) const noexcept; + + //============================================================================== + + /** + Scans all files found under @p searchPath using all registered formats. + + Files and bundles are offered to registered formats whose extension + matches the format. If a format returns a success, its results are + appended; if it fails, the path is added to ScanResult::failedPaths. + + AUv2 scanning enumerates the AudioComponent registry and does not walk + the search path — pass an empty FileSearchPath when only AUv2 is registered. + */ + ScanResult scan (const FileSearchPath& searchPath); + + /** + Scans each registered format's default search paths. + */ + ScanResult scanDefaults(); + + /** + Starts scanning @p searchPath on a background thread. + + The callback is invoked from a scanner worker thread. UI clients should + dispatch back to the message thread before touching components. + */ + void scanAsync (const FileSearchPath& searchPath, ScanCallback callback); + + /** + Starts scanning each registered format's default search paths on a + background thread. + */ + void scanDefaultsAsync (ScanCallback callback); + + /** + Attempts to stop queued or running asynchronous scans. + */ + void cancelPendingScans(); + +private: + std::vector> formats; + ThreadPool backgroundScanPool; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioPluginScanner) +}; + +} // namespace yup diff --git a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.h b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.h new file mode 100644 index 000000000..3aa7e74e8 --- /dev/null +++ b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.h @@ -0,0 +1,58 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#if YUP_AUDIO_PLUGIN_HOST_ENABLE_AU && YUP_MAC + +namespace yup +{ + +/** + AudioPluginFormat implementation for AUv2 (macOS only). + + Enumerates AudioComponents via AudioComponentFindNext() and wraps each + component via AudioComponentInstanceNew(). +*/ +class AUv2Format : public AudioPluginFormat +{ +public: + AUv2Format(); + ~AUv2Format() override; + + AudioPluginFormatType getFormatType() const override; + String getFormatName() const override; + + /** Returns an empty FileSearchPath — AUv2 uses AudioComponent registry. */ + FileSearchPath getDefaultSearchPaths() const override; + + /** + Passing an invalid File triggers full AudioComponent registry scan. + Otherwise scans only the component matching the file's bundle identifier. + */ + ResultValue> scanFile (const File& file) override; + + ResultValue> loadPlugin ( + const AudioPluginDescription& description, + const AudioPluginHostContext& context) override; +}; + +} // namespace yup + +#endif // YUP_AUDIO_PLUGIN_HOST_ENABLE_AU && YUP_MAC diff --git a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.mm b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.mm new file mode 100644 index 000000000..bcacb399d --- /dev/null +++ b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.mm @@ -0,0 +1,1085 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#if YUP_AUDIO_PLUGIN_HOST_ENABLE_AU && YUP_MAC + +namespace yup +{ + +namespace +{ + +//============================================================================== +// Converts a four-char code to a 4-char String (e.g. 0x61756666 -> "auff"). +String fourCCToString (OSType code) +{ + char buf[5] = {}; + buf[0] = static_cast ((code >> 24) & 0xFF); + buf[1] = static_cast ((code >> 16) & 0xFF); + buf[2] = static_cast ((code >> 8) & 0xFF); + buf[3] = static_cast (code & 0xFF); + return String (buf); +} + +String copyCFString (CFStringRef string) +{ + return String::fromCFString (string).trim(); +} + +// Builds a stable identifier string "type/subt/mfgr" from AudioComponentDescription. +String makeIdentifier (const AudioComponentDescription& acd) +{ + return fourCCToString (acd.componentType) + "/" + + fourCCToString (acd.componentSubType) + "/" + + fourCCToString (acd.componentManufacturer); +} + +AudioPluginDescription descriptionFromComponent (AudioComponent comp, + const AudioComponentDescription& acd) +{ + AudioPluginDescription desc; + desc.formatType = AudioPluginFormatType::audioUnit; + desc.identifier = makeIdentifier (acd); + + CFStringRef nameRef = nullptr; + AudioComponentCopyName (comp, &nameRef); + if (nameRef != nullptr) + { + desc.name = copyCFString (nameRef); + CFRelease (nameRef); + } + + // Collect vendor from the manufacturer four-char code + desc.vendor = fourCCToString (acd.componentManufacturer); + + desc.isInstrument = (acd.componentType == kAudioUnitType_MusicDevice); + desc.isEffect = (acd.componentType == kAudioUnitType_Effect + || acd.componentType == kAudioUnitType_MusicEffect); + + return desc; +} + +String makeParameterName (const AudioUnitParameterInfo& info, + AudioUnitParameterID paramId) +{ + String name; + + if ((info.flags & kAudioUnitParameterFlag_HasCFNameString) != 0) + name = copyCFString (info.cfNameString); + + if (name.isEmpty()) + name = String (info.name).trim(); + + return name.isNotEmpty() ? name : "Parameter " + String (paramId); +} + +void releaseParameterInfoStrings (AudioUnitParameterInfo& info) +{ + if ((info.flags & kAudioUnitParameterFlag_CFNameRelease) == 0) + return; + + if (info.cfNameString != nullptr) + { + CFRelease (info.cfNameString); + info.cfNameString = nullptr; + } + + if (info.unitName != nullptr) + { + CFRelease (info.unitName); + info.unitName = nullptr; + } +} + +NormalisableRange makeParameterRange (const AudioUnitParameterInfo& info) +{ + const auto minValue = static_cast (info.minValue); + const auto maxValue = static_cast (info.maxValue); + + if (maxValue > minValue) + return { minValue, maxValue }; + + return { 0.0f, 1.0f }; +} + +AudioUnitParameter makeAUParameter (AudioUnit audioUnit, AudioUnitParameterID paramId) +{ + AudioUnitParameter parameter; + parameter.mAudioUnit = audioUnit; + parameter.mParameterID = paramId; + parameter.mScope = kAudioUnitScope_Global; + parameter.mElement = 0; + return parameter; +} + +AudioUnitEvent makeAUParameterEvent (AudioUnit audioUnit, + AudioUnitParameterID paramId, + AudioUnitEventType eventType) +{ + AudioUnitEvent event; + event.mEventType = eventType; + event.mArgument.mParameter = makeAUParameter (audioUnit, paramId); + return event; +} + +} // namespace + +//============================================================================== + +class AUv2Editor : public AudioProcessorEditor +{ +public: + static std::unique_ptr create (AudioUnit audioUnit) + { + UInt32 size = 0; + Boolean writable = false; + + if (AudioUnitGetPropertyInfo (audioUnit, + kAudioUnitProperty_CocoaUI, + kAudioUnitScope_Global, 0, + &size, &writable) != noErr) + { + return nullptr; + } + + if (size < offsetof (AudioUnitCocoaViewInfo, mCocoaAUViewClass) + sizeof (CFStringRef)) + return nullptr; + + std::vector storage (size); + auto* viewInfo = reinterpret_cast (storage.data()); + + if (AudioUnitGetProperty (audioUnit, + kAudioUnitProperty_CocoaUI, + kAudioUnitScope_Global, 0, + viewInfo, &size) != noErr) + { + return nullptr; + } + + NSView* view = nil; + NSBundle* viewBundle = nil; + id viewFactory = nil; + + if (viewInfo->mCocoaAUViewBundleLocation != nullptr && viewInfo->mCocoaAUViewClass[0] != nullptr) + { + viewBundle = [NSBundle bundleWithURL:(__bridge NSURL*) viewInfo->mCocoaAUViewBundleLocation]; + + if (viewBundle != nil && [viewBundle load]) + { + Class viewClass = [viewBundle classNamed:(__bridge NSString*) viewInfo->mCocoaAUViewClass[0]]; + + if (viewClass != Nil && [viewClass conformsToProtocol:@protocol (AUCocoaUIBase)]) + { + viewFactory = [[viewClass alloc] init]; + view = [viewFactory uiViewForAudioUnit:audioUnit withSize:NSZeroSize]; + } + } + } + + if (viewInfo->mCocoaAUViewBundleLocation != nullptr) + CFRelease (viewInfo->mCocoaAUViewBundleLocation); + + const auto numViewClasses = static_cast ((size - offsetof (AudioUnitCocoaViewInfo, mCocoaAUViewClass)) / sizeof (CFStringRef)); + for (int i = 0; i < numViewClasses; ++i) + if (viewInfo->mCocoaAUViewClass[i] != nullptr) + CFRelease (viewInfo->mCocoaAUViewClass[i]); + + if (view == nil) + return nullptr; + + return std::unique_ptr (new AUv2Editor (view, viewBundle, viewFactory)); + } + + ~AUv2Editor() override + { + detachCocoaView(); + } + + bool isResizable() const override { return true; } + + Size getPreferredSize() const override { return preferredSize; } + + void paint (Graphics& g) override + { + g.setFillColor (Color (0xff101417)); + g.fillAll(); + } + + void resized() override + { + updateCocoaViewFrame(); + } + + void attachedToNative() override + { + attachCocoaView(); + } + + void detachedFromNative() override + { + detachCocoaView(); + } + +private: + AUv2Editor (NSView* view, NSBundle* viewBundle, id viewFactory) + : cocoaView (view) + , cocoaViewBundle (viewBundle) + , cocoaViewFactory (viewFactory) + { + auto size = [cocoaView frame].size; + + if (size.width <= 1.0 || size.height <= 1.0) + size = [cocoaView fittingSize]; + + preferredSize = { + jmax (320, static_cast (std::ceil (size.width))), + jmax (240, static_cast (std::ceil (size.height))) + }; + + [cocoaView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + [cocoaView setFrameSize:NSMakeSize (preferredSize.getWidth(), preferredSize.getHeight())]; + } + + void attachCocoaView() + { + if (cocoaView == nil) + return; + + auto* nativeComponent = getNativeComponent(); + if (nativeComponent == nullptr) + return; + + NSWindow* window = (__bridge NSWindow*) nativeComponent->getNativeHandle(); + NSView* contentView = [window contentView]; + + if (contentView == nil) + return; + + if ([cocoaView superview] != contentView) + [contentView addSubview:cocoaView positioned:NSWindowAbove relativeTo:nil]; + + updateCocoaViewFrame(); + } + + void detachCocoaView() + { + if (cocoaView != nil) + [cocoaView removeFromSuperview]; + } + + void updateCocoaViewFrame() + { + if (cocoaView == nil) + return; + + auto* nativeComponent = getNativeComponent(); + if (nativeComponent == nullptr) + return; + + NSWindow* window = (__bridge NSWindow*) nativeComponent->getNativeHandle(); + NSView* contentView = [window contentView]; + + if (contentView == nil) + return; + + const auto bounds = getBoundsRelativeToTopLevelComponent(); + const auto contentBounds = [contentView bounds]; + const auto width = jmax (1.0f, bounds.getWidth()); + const auto height = jmax (1.0f, bounds.getHeight()); + + [cocoaView setFrame:NSMakeRect (static_cast (bounds.getX()), + static_cast (NSHeight (contentBounds) - bounds.getY() - height), + static_cast (width), + static_cast (height))]; + } + + Size preferredSize { 640, 480 }; + NSView* __strong cocoaView = nil; + NSBundle* __strong cocoaViewBundle = nil; + id __strong cocoaViewFactory = nil; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AUv2Editor) +}; + +//============================================================================== + +class AUv2Instance : public AudioPluginInstance + , private AudioParameter::Listener +{ +public: + AUv2Instance (const AudioPluginDescription& desc, + AudioUnit unit, + AudioBusLayout busLayout) + : AudioPluginInstance (desc, std::move (busLayout)) + , audioUnit (unit) + { + buildParameterList(); + } + + ~AUv2Instance() override + { + removeParameterListeners(); + releaseResources(); + + if (audioUnit != nullptr) + { + AudioUnitUninitialize (audioUnit); + AudioComponentInstanceDispose (audioUnit); + audioUnit = nullptr; + } + } + + //============================================================================== + + void prepareToPlay (float sampleRate, int maxBlockSize) override + { + releaseResources(); + + renderSampleTime = 0.0; + + const auto numHostedChannels = jmax (2, + pluginDescription.numInputChannels, + pluginDescription.numOutputChannels); + prepareRenderStorage (numHostedChannels, maxBlockSize); + + const Float64 sampleRateValue = static_cast (sampleRate); + AudioUnitSetProperty (audioUnit, + kAudioUnitProperty_SampleRate, + kAudioUnitScope_Global, 0, + &sampleRateValue, sizeof (sampleRateValue)); + + UInt32 blockSize = static_cast (maxBlockSize); + AudioUnitSetProperty (audioUnit, + kAudioUnitProperty_MaximumFramesPerSlice, + kAudioUnitScope_Global, 0, + &blockSize, sizeof (blockSize)); + + configureStreamFormat (sampleRateValue, numHostedChannels); + installInputCallback(); + + AudioUnitInitialize (audioUnit); + } + + void releaseResources() override + { + if (audioUnit != nullptr) + AudioUnitUninitialize (audioUnit); + } + + void processBlock (AudioBuffer& audioBuffer, MidiBuffer&) override + { + const int numSamples = audioBuffer.getNumSamples(); + const int numChannels = audioBuffer.getNumChannels(); + + if (numSamples <= 0 || numChannels <= 0) + return; + + if (numChannels > preparedNumChannels || numSamples > preparedMaxBlockSize) + { + jassertfalse; + return; + } + + for (int c = 0; c < numChannels; ++c) + { + std::memcpy (inputBuffer.getWritePointer (c), + audioBuffer.getReadPointer (c), + static_cast (numSamples) * sizeof (float)); + } + + currentInputBuffer = &inputBuffer; + currentInputNumChannels = numChannels; + currentInputNumSamples = numSamples; + + AudioUnitRenderActionFlags flags = 0; + AudioTimeStamp timeStamp{}; + timeStamp.mFlags = kAudioTimeStampSampleTimeValid; + timeStamp.mSampleTime = renderSampleTime; + renderSampleTime += numSamples; + + AudioBufferList* abl = makeOutputABL (audioBuffer); + + const auto status = AudioUnitRender (audioUnit, + &flags, + &timeStamp, + 0, // output bus + static_cast (numSamples), + abl); + + currentInputBuffer = nullptr; + currentInputNumChannels = 0; + currentInputNumSamples = 0; + + if (status != noErr) + { + copyInputBackTo (audioBuffer, numChannels, numSamples); + return; + } + + // Copy rendered output back to the YUP buffer + for (int c = 0; c < numChannels; ++c) + { + std::memcpy (audioBuffer.getWritePointer (c), + abl->mBuffers[c].mData, + static_cast (numSamples) * sizeof (float)); + } + + syncParameterValuesFromAudioUnit(); + } + + //============================================================================== + + int getCurrentPreset() const noexcept override { return currentPreset; } + + void setCurrentPreset (int index) noexcept override + { + CFArrayRef presets = nullptr; + UInt32 size = sizeof (presets); + + if (AudioUnitGetProperty (audioUnit, + kAudioUnitProperty_FactoryPresets, + kAudioUnitScope_Global, 0, + &presets, &size) != noErr) + { + return; + } + + if (presets == nullptr || ! isPositiveAndBelow (index, static_cast (CFArrayGetCount (presets)))) + { + if (presets != nullptr) + CFRelease (presets); + + return; + } + + auto* auPreset = static_cast (CFArrayGetValueAtIndex (presets, index)); + if (auPreset != nullptr) + { + AUPreset preset = *auPreset; + + if (AudioUnitSetProperty (audioUnit, + kAudioUnitProperty_PresentPreset, + kAudioUnitScope_Global, 0, + &preset, sizeof (preset)) == noErr) + { + currentPreset = index; + syncParameterValuesFromAudioUnit(); + notifyAudioUnitParametersChanged(); + } + } + + CFRelease (presets); + } + + int getNumPresets() const override + { + CFArrayRef presets = nullptr; + UInt32 size = sizeof (presets); + + AudioUnitGetProperty (audioUnit, + kAudioUnitProperty_FactoryPresets, + kAudioUnitScope_Global, 0, + &presets, &size); + + if (presets == nullptr) + return 0; + + const int count = static_cast (CFArrayGetCount (presets)); + CFRelease (presets); + return count; + } + + String getPresetName (int index) const override + { + CFArrayRef presets = nullptr; + UInt32 size = sizeof (presets); + + AudioUnitGetProperty (audioUnit, + kAudioUnitProperty_FactoryPresets, + kAudioUnitScope_Global, 0, + &presets, &size); + + if (presets == nullptr || ! isPositiveAndBelow (index, static_cast (CFArrayGetCount (presets)))) + { + if (presets != nullptr) CFRelease (presets); + return {}; + } + + auto* auPreset = static_cast (CFArrayGetValueAtIndex (presets, index)); + const String name = auPreset->presetName != nullptr + ? String::fromCFString (auPreset->presetName) + : String(); + CFRelease (presets); + return name; + } + + void setPresetName (int, StringRef) override {} + + //============================================================================== + + Result loadStateFromMemory (const MemoryBlock& memoryBlock) override + { + CFDataRef data = CFDataCreateWithBytesNoCopy (kCFAllocatorDefault, + static_cast (memoryBlock.getData()), + static_cast (memoryBlock.getSize()), + kCFAllocatorNull); + if (data == nullptr) + return Result::fail ("Failed to create CFData"); + + CFPropertyListRef plist = CFPropertyListCreateWithData (kCFAllocatorDefault, + data, + kCFPropertyListImmutable, + nullptr, nullptr); + CFRelease (data); + + if (plist == nullptr) + return Result::fail ("Failed to deserialize AU state"); + + const OSStatus status = AudioUnitSetProperty (audioUnit, + kAudioUnitProperty_ClassInfo, + kAudioUnitScope_Global, 0, + &plist, sizeof (plist)); + CFRelease (plist); + + if (status != noErr) + return Result::fail ("AudioUnitSetProperty ClassInfo failed: " + String (status)); + + syncParameterValuesFromAudioUnit(); + notifyAudioUnitParametersChanged(); + return Result::ok(); + } + + Result saveStateIntoMemory (MemoryBlock& memoryBlock) override + { + CFPropertyListRef plist = nullptr; + UInt32 size = sizeof (plist); + + const OSStatus status = AudioUnitGetProperty (audioUnit, + kAudioUnitProperty_ClassInfo, + kAudioUnitScope_Global, 0, + &plist, &size); + if (status != noErr || plist == nullptr) + return Result::fail ("AudioUnitGetProperty ClassInfo failed: " + String (status)); + + CFDataRef data = CFPropertyListCreateData (kCFAllocatorDefault, + plist, + kCFPropertyListBinaryFormat_v1_0, + 0, nullptr); + CFRelease (plist); + + if (data == nullptr) + return Result::fail ("Failed to serialize AU state"); + + memoryBlock.replaceAll (CFDataGetBytePtr (data), + static_cast (CFDataGetLength (data))); + CFRelease (data); + return Result::ok(); + } + + //============================================================================== + + bool hasEditor() const override + { + UInt32 size = 0; + Boolean writable = false; + + return AudioUnitGetPropertyInfo (audioUnit, + kAudioUnitProperty_CocoaUI, + kAudioUnitScope_Global, 0, + &size, &writable) == noErr + && size >= offsetof (AudioUnitCocoaViewInfo, mCocoaAUViewClass) + sizeof (CFStringRef); + } + + AudioProcessorEditor* createEditor() override + { + if (auto editor = AUv2Editor::create (audioUnit)) + return editor.release(); + + return nullptr; + } + + //============================================================================== + + static std::unique_ptr create (const AudioPluginDescription& desc, + const AudioPluginHostContext&) + { + // Parse "type/subt/mfgr" identifier + const auto tokens = StringArray::fromTokens (desc.identifier, "/", ""); + if (tokens.size() != 3) + return nullptr; + + auto fourCC = [] (const String& s) -> OSType + { + if (s.length() != 4) return 0; + return static_cast ( + (static_cast (s[0]) << 24) + | (static_cast (s[1]) << 16) + | (static_cast (s[2]) << 8) + | static_cast (s[3])); + }; + + AudioComponentDescription acd{}; + acd.componentType = fourCC (tokens[0]); + acd.componentSubType = fourCC (tokens[1]); + acd.componentManufacturer = fourCC (tokens[2]); + + AudioComponent comp = AudioComponentFindNext (nullptr, &acd); + if (comp == nullptr) + return nullptr; + + AudioUnit unit = nullptr; + if (AudioComponentInstanceNew (comp, &unit) != noErr || unit == nullptr) + return nullptr; + + AudioBusLayout busLayout ({}, {}); + // TODO: query kAudioUnitProperty_ElementCount for proper bus layout + + return std::make_unique (desc, unit, std::move (busLayout)); + } + +private: + void parameterValueChanged (const AudioParameter::Ptr& parameter, int indexInContainer) override + { + if (audioUnit == nullptr || ! isPositiveAndBelow (indexInContainer, static_cast (auParameterIds.size()))) + return; + + const auto auParameter = makeAUParameter (audioUnit, auParameterIds[static_cast (indexInContainer)]); + + AUParameterSet (eventListener, + this, + &auParameter, + static_cast (parameter->getValue()), + 0); + } + + void parameterGestureBegin (const AudioParameter::Ptr&, int indexInContainer) override + { + notifyParameterGesture (indexInContainer, kAudioUnitEvent_BeginParameterChangeGesture); + } + + void parameterGestureEnd (const AudioParameter::Ptr&, int indexInContainer) override + { + notifyParameterGesture (indexInContainer, kAudioUnitEvent_EndParameterChangeGesture); + } + + void notifyParameterGesture (int indexInContainer, AudioUnitEventType eventType) + { + if (audioUnit == nullptr || ! isPositiveAndBelow (indexInContainer, static_cast (auParameterIds.size()))) + return; + + const auto event = makeAUParameterEvent (audioUnit, + auParameterIds[static_cast (indexInContainer)], + eventType); + + AUEventListenerNotify (eventListener, this, &event); + } + + static void parameterEventCallback (void* userData, + void*, + const AudioUnitEvent* event, + UInt64, + AudioUnitParameterValue parameterValue) + { + auto* instance = static_cast (userData); + if (instance == nullptr || event == nullptr || event->mEventType != kAudioUnitEvent_ParameterValueChange) + return; + + instance->handleAudioUnitParameterChanged (event->mArgument.mParameter, parameterValue); + } + + void handleAudioUnitParameterChanged (const AudioUnitParameter& parameter, + AudioUnitParameterValue parameterValue) + { + if (parameter.mAudioUnit != audioUnit + || parameter.mScope != kAudioUnitScope_Global + || parameter.mElement != 0) + { + return; + } + + const auto params = getParameters(); + const auto numParams = jmin (params.size(), auParameterIds.size()); + + for (std::size_t i = 0; i < numParams; ++i) + { + if (auParameterIds[i] == parameter.mParameterID) + { + params[i]->setValue (static_cast (parameterValue)); + return; + } + } + } + + static OSStatus inputRenderCallback (void* refCon, + AudioUnitRenderActionFlags*, + const AudioTimeStamp*, + UInt32, + UInt32 numFrames, + AudioBufferList* ioData) + { + auto* instance = static_cast (refCon); + if (ioData == nullptr) + return noErr; + + if (instance == nullptr || instance->currentInputBuffer == nullptr) + { + for (UInt32 bufferIndex = 0; bufferIndex < ioData->mNumberBuffers; ++bufferIndex) + { + auto& buffer = ioData->mBuffers[bufferIndex]; + auto* dest = static_cast (buffer.mData); + + if (dest != nullptr) + FloatVectorOperations::clear (dest, static_cast (numFrames)); + + buffer.mDataByteSize = static_cast (numFrames * sizeof (float)); + } + + return noErr; + } + + const int framesToCopy = jmin (static_cast (numFrames), + instance->currentInputNumSamples); + + for (UInt32 bufferIndex = 0; bufferIndex < ioData->mNumberBuffers; ++bufferIndex) + { + auto& buffer = ioData->mBuffers[bufferIndex]; + auto* dest = static_cast (buffer.mData); + + if (dest == nullptr) + continue; + + if (static_cast (bufferIndex) < instance->currentInputNumChannels) + { + FloatVectorOperations::copy (dest, + instance->currentInputBuffer->getReadPointer (static_cast (bufferIndex)), + framesToCopy); + } + else + { + FloatVectorOperations::clear (dest, framesToCopy); + } + + if (framesToCopy < static_cast (numFrames)) + FloatVectorOperations::clear (dest + framesToCopy, + static_cast (numFrames) - framesToCopy); + + buffer.mDataByteSize = static_cast (numFrames * sizeof (float)); + } + + return noErr; + } + + void prepareRenderStorage (int numChannels, int maxBlockSize) + { + preparedNumChannels = jmax (1, numChannels); + preparedMaxBlockSize = jmax (1, maxBlockSize); + + inputBuffer.setSize (preparedNumChannels, + preparedMaxBlockSize, + false, + false, + true); + + outputABLStorage.resize (getAudioBufferListSize (preparedNumChannels)); + } + + void configureStreamFormat (Float64 sampleRate, int numChannels) + { + AudioStreamBasicDescription streamFormat{}; + streamFormat.mSampleRate = sampleRate; + streamFormat.mFormatID = kAudioFormatLinearPCM; + streamFormat.mFormatFlags = kAudioFormatFlagIsFloat + | kAudioFormatFlagIsPacked + | kAudioFormatFlagIsNonInterleaved + | kAudioFormatFlagsNativeEndian; + streamFormat.mBytesPerPacket = sizeof (float); + streamFormat.mFramesPerPacket = 1; + streamFormat.mBytesPerFrame = sizeof (float); + streamFormat.mChannelsPerFrame = static_cast (numChannels); + streamFormat.mBitsPerChannel = static_cast (sizeof (float) * 8); + + AudioUnitSetProperty (audioUnit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Input, 0, + &streamFormat, sizeof (streamFormat)); + + AudioUnitSetProperty (audioUnit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Output, 0, + &streamFormat, sizeof (streamFormat)); + } + + void installInputCallback() + { + AURenderCallbackStruct callback{}; + callback.inputProc = inputRenderCallback; + callback.inputProcRefCon = this; + + AudioUnitSetProperty (audioUnit, + kAudioUnitProperty_SetRenderCallback, + kAudioUnitScope_Input, 0, + &callback, sizeof (callback)); + } + + static std::size_t getAudioBufferListSize (int numChannels) + { + return sizeof (AudioBufferList) + + static_cast (jmax (0, numChannels - 1)) * sizeof (::AudioBuffer); + } + + AudioBufferList* makeOutputABL (AudioBuffer& buffer) + { + const int numChannels = buffer.getNumChannels(); + auto* abl = reinterpret_cast (outputABLStorage.data()); + abl->mNumberBuffers = static_cast (numChannels); + + for (int c = 0; c < numChannels; ++c) + { + abl->mBuffers[c].mNumberChannels = 1; + abl->mBuffers[c].mDataByteSize = static_cast (buffer.getNumSamples()) * sizeof (float); + abl->mBuffers[c].mData = buffer.getWritePointer (c); + } + + return abl; + } + + void copyInputBackTo (AudioBuffer& audioBuffer, int numChannels, int numSamples) + { + for (int c = 0; c < numChannels; ++c) + { + FloatVectorOperations::copy (audioBuffer.getWritePointer (c), + inputBuffer.getReadPointer (c), + numSamples); + } + } + + void buildParameterList() + { + UInt32 size = 0; + if (AudioUnitGetPropertyInfo (audioUnit, + kAudioUnitProperty_ParameterList, + kAudioUnitScope_Global, 0, + &size, nullptr) != noErr) + { + return; + } + + const int count = static_cast (size / sizeof (AudioUnitParameterID)); + if (count == 0) + return; + + std::vector paramIds (static_cast (count)); + if (AudioUnitGetProperty (audioUnit, + kAudioUnitProperty_ParameterList, + kAudioUnitScope_Global, 0, + paramIds.data(), &size) != noErr) + { + return; + } + + installParameterEventListener(); + + for (auto paramId : paramIds) + { + AudioUnitParameterInfo info{}; + UInt32 infoSize = sizeof (info); + if (AudioUnitGetProperty (audioUnit, + kAudioUnitProperty_ParameterInfo, + kAudioUnitScope_Global, paramId, + &info, &infoSize) != noErr) + { + continue; + } + + const auto name = makeParameterName (info, paramId); + const auto range = makeParameterRange (info); + releaseParameterInfoStrings (info); + + auto param = AudioParameterBuilder() + .withID (name + "_" + String (paramId)) + .withName (name) + .withRange (range) + .withDefault (info.defaultValue) + .withSmoothing (0.0f) + .build(); + + param->addListener (this); + + auParameterIds.push_back (paramId); + addParameter (std::move (param)); + + addAudioUnitParameterListener (paramId); + } + + syncParameterValuesFromAudioUnit(); + } + + void installParameterEventListener() + { + if (eventListener != nullptr) + return; + + AUEventListenerCreate (parameterEventCallback, + this, + CFRunLoopGetMain(), + kCFRunLoopCommonModes, + 0.02f, + 0.01f, + &eventListener); + } + + void addAudioUnitParameterListener (AudioUnitParameterID paramId) + { + if (eventListener == nullptr) + return; + + const auto event = makeAUParameterEvent (audioUnit, paramId, kAudioUnitEvent_ParameterValueChange); + AUEventListenerAddEventType (eventListener, this, &event); + } + + void notifyAudioUnitParametersChanged() + { + if (audioUnit == nullptr) + return; + + for (auto paramId : auParameterIds) + { + const auto parameter = makeAUParameter (audioUnit, paramId); + AUParameterListenerNotify (eventListener, this, ¶meter); + } + } + + void syncParameterValuesFromAudioUnit() noexcept + { + const auto params = getParameters(); + const auto numParams = jmin (params.size(), auParameterIds.size()); + + for (std::size_t i = 0; i < numParams; ++i) + { + AudioUnitParameterValue value = 0.0f; + if (AudioUnitGetParameter (audioUnit, + auParameterIds[i], + kAudioUnitScope_Global, + 0, + &value) == noErr) + { + params[i]->setValue (static_cast (value)); + } + } + } + + void removeParameterListeners() + { + for (auto& param : getParameters()) + param->removeListener (this); + + if (eventListener != nullptr) + { + AUListenerDispose (eventListener); + eventListener = nullptr; + } + } + + AudioUnit audioUnit = nullptr; + AUEventListenerRef eventListener = nullptr; + int currentPreset = 0; + double renderSampleTime = 0.0; + std::vector auParameterIds; + AudioBuffer inputBuffer; + AudioBuffer* currentInputBuffer = nullptr; + int currentInputNumChannels = 0; + int currentInputNumSamples = 0; + int preparedNumChannels = 0; + int preparedMaxBlockSize = 0; + std::vector outputABLStorage; +}; + +//============================================================================== + +AUv2Format::AUv2Format() = default; +AUv2Format::~AUv2Format() = default; + +AudioPluginFormatType AUv2Format::getFormatType() const +{ + return AudioPluginFormatType::audioUnit; +} + +String AUv2Format::getFormatName() const +{ + return "AUv2"; +} + +FileSearchPath AUv2Format::getDefaultSearchPaths() const +{ + return {}; +} + +ResultValue> AUv2Format::scanFile (const File&) +{ + // Scan by enumerating the AudioComponent registry (ignores file argument) + std::vector results; + + const OSType types[] = { + kAudioUnitType_MusicDevice, + kAudioUnitType_Effect, + kAudioUnitType_MusicEffect, + kAudioUnitType_Generator, + kAudioUnitType_MIDIProcessor + }; + + for (OSType type : types) + { + AudioComponentDescription search{}; + search.componentType = type; + + AudioComponent comp = nullptr; + while ((comp = AudioComponentFindNext (comp, &search)) != nullptr) + { + AudioComponentDescription acd{}; + AudioComponentGetDescription (comp, &acd); + auto desc = descriptionFromComponent (comp, acd); + results.push_back (std::move (desc)); + } + } + + if (results.empty()) + return ResultValue>::fail ( + "No AudioComponents found in registry"); + + return ResultValue>::ok (std::move (results)); +} + +ResultValue> AUv2Format::loadPlugin ( + const AudioPluginDescription& description, + const AudioPluginHostContext& context) +{ + auto instance = AUv2Instance::create (description, context); + + if (instance == nullptr) + return ResultValue>::fail ( + "Failed to instantiate AUv2 plugin: " + description.name); + + return ResultValue>::ok (std::move (instance)); +} + +} // namespace yup + +#endif // YUP_AUDIO_PLUGIN_HOST_ENABLE_AU && YUP_MAC diff --git a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.cpp b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.cpp new file mode 100644 index 000000000..5c875d5fc --- /dev/null +++ b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.cpp @@ -0,0 +1,597 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#if YUP_AUDIO_PLUGIN_HOST_ENABLE_CLAP + +namespace yup +{ + +namespace +{ + +//============================================================================== +struct CLAPModule +{ + CLAPModuleHandle handle = nullptr; + const clap_plugin_entry_t* entry = nullptr; + + static std::unique_ptr load (const File& file) + { + auto m = std::make_unique(); + m->handle = clapLoadModule (file.getFullPathName().toRawUTF8()); + + if (m->handle == nullptr) + return nullptr; + + m->entry = reinterpret_cast ( + clapGetAddress (m->handle, "clap_entry")); + + if (m->entry == nullptr || m->entry->init == nullptr) + { + clapUnloadModule (m->handle); + return nullptr; + } + + if (! m->entry->init (file.getFullPathName().toRawUTF8())) + { + clapUnloadModule (m->handle); + return nullptr; + } + + return m; + } + + ~CLAPModule() + { + if (entry != nullptr && entry->deinit != nullptr) + entry->deinit(); + + if (handle != nullptr) + clapUnloadModule (handle); + } +}; + +//============================================================================== +// Minimal clap_host_t implementation supplied to each plugin instance. +struct YUPCLAPHost +{ + clap_host_t host {}; + + YUPCLAPHost (const AudioPluginHostContext& ctx) + { + host.clap_version = CLAP_VERSION; + host.host_data = this; + host.name = ctx.hostName.toRawUTF8(); + host.vendor = ctx.hostVendor.toRawUTF8(); + host.url = ""; + host.version = ctx.hostVersion.toRawUTF8(); + host.get_extension = [] (const clap_host_t*, const char*) -> const void* + { + return nullptr; + }; + host.request_restart = [] (const clap_host_t*) {}; + host.request_process = [] (const clap_host_t*) {}; + host.request_callback = [] (const clap_host_t*) {}; + } +}; + +struct CLAPInputEvents +{ + std::vector parameterEvents; + clap_input_events_t inputEvents {}; + + CLAPInputEvents() + { + inputEvents.ctx = this; + inputEvents.size = [] (const clap_input_events_t* events) -> uint32_t + { + auto* self = static_cast (events->ctx); + return static_cast (self->parameterEvents.size()); + }; + inputEvents.get = [] (const clap_input_events_t* events, uint32_t index) -> const clap_event_header_t* + { + auto* self = static_cast (events->ctx); + if (index >= self->parameterEvents.size()) + return nullptr; + + return &self->parameterEvents[static_cast (index)].header; + }; + } + + void addParameterValue (clap_id id, double value) + { + clap_event_param_value_t event {}; + event.header.size = sizeof (event); + event.header.time = 0; + event.header.space_id = CLAP_CORE_EVENT_SPACE_ID; + event.header.type = CLAP_EVENT_PARAM_VALUE; + event.param_id = id; + event.note_id = -1; + event.port_index = -1; + event.channel = -1; + event.key = -1; + event.value = value; + + parameterEvents.push_back (event); + } +}; + +struct CLAPOutputEvents +{ + clap_output_events_t outputEvents {}; + + CLAPOutputEvents() + { + outputEvents.ctx = this; + outputEvents.try_push = [] (const clap_output_events_t*, const clap_event_header_t*) -> bool + { + return true; + }; + } +}; + +} // namespace + +//============================================================================== + +class CLAPInstance : public AudioPluginInstance +{ +public: + CLAPInstance (const AudioPluginDescription& desc, + const AudioPluginHostContext& context, + std::unique_ptr module, + std::unique_ptr clapHost, + const clap_plugin_t* plugin) + : AudioPluginInstance (desc, buildBusLayout (plugin)) + , hostContext (context) + , clapModule (std::move (module)) + , yupHost (std::move (clapHost)) + , clapPlugin (plugin) + { + buildParameterList(); + } + + ~CLAPInstance() override + { + releaseResources(); + + if (clapPlugin != nullptr) + { + clapPlugin->destroy (clapPlugin); + clapPlugin = nullptr; + } + } + + //============================================================================== + + void prepareToPlay (float sampleRate, int maxBlockSize) override + { + clapPlugin->activate (clapPlugin, sampleRate, static_cast (maxBlockSize), static_cast (maxBlockSize)); + + clapPlugin->start_processing (clapPlugin); + } + + void releaseResources() override + { + if (clapPlugin != nullptr) + { + clapPlugin->stop_processing (clapPlugin); + clapPlugin->deactivate (clapPlugin); + } + } + + void processBlock (AudioBuffer& audioBuffer, MidiBuffer& /*midiBuffer*/) override + { + const int numSamples = audioBuffer.getNumSamples(); + const int numChannels = audioBuffer.getNumChannels(); + + // Build audio buffer descriptors + std::vector inPtrs (static_cast (numChannels)); + std::vector outPtrs (static_cast (numChannels)); + + for (int c = 0; c < numChannels; ++c) + { + inPtrs[static_cast (c)] = audioBuffer.getReadPointer (c); + outPtrs[static_cast (c)] = audioBuffer.getWritePointer (c); + } + + clap_audio_buffer_t inputBuf {}; + inputBuf.data32 = const_cast (inPtrs.data()); + inputBuf.channel_count = static_cast (numChannels); + inputBuf.latency = 0; + inputBuf.constant_mask = 0; + + clap_audio_buffer_t outputBuf {}; + outputBuf.data32 = outPtrs.data(); + outputBuf.channel_count = static_cast (numChannels); + + clap_process_t process {}; + process.steady_time = -1; + process.frames_count = static_cast (numSamples); + process.audio_inputs = &inputBuf; + process.audio_inputs_count = 1; + process.audio_outputs = &outputBuf; + process.audio_outputs_count = 1; + + CLAPInputEvents inputEvents; + const auto params = getParameters(); + const auto numParams = yup::jmin (params.size(), clapParameterIds.size()); + inputEvents.parameterEvents.reserve (numParams); + + for (std::size_t i = 0; i < numParams; ++i) + inputEvents.addParameterValue (clapParameterIds[i], static_cast (params[i]->getValue())); + + CLAPOutputEvents outputEvents; + process.in_events = &inputEvents.inputEvents; + process.out_events = &outputEvents.outputEvents; + + // TODO: map MidiBuffer to CLAP event list in a later task + + clapPlugin->process (clapPlugin, &process); + } + + //============================================================================== + + int getCurrentPreset() const noexcept override { return currentPreset; } + + void setCurrentPreset (int index) noexcept override + { + currentPreset = index; + } + + int getNumPresets() const override { return 0; } + + String getPresetName (int) const override { return {}; } + + void setPresetName (int, StringRef) override {} + + //============================================================================== + + Result loadStateFromMemory (const MemoryBlock& memoryBlock) override + { + auto* stateExt = reinterpret_cast ( + clapPlugin->get_extension (clapPlugin, CLAP_EXT_STATE)); + + if (stateExt == nullptr) + return Result::fail ("Plugin does not support CLAP state extension"); + + struct MemStream + { + const MemoryBlock* block; + std::size_t position = 0; + }; + + MemStream ms { &memoryBlock }; + + clap_istream_t stream {}; + stream.ctx = &ms; + stream.read = [] (const clap_istream_t* s, void* buf, uint64_t size) -> int64_t + { + auto* ms = static_cast (s->ctx); + const std::size_t remaining = ms->block->getSize() - ms->position; + const std::size_t toRead = std::min (static_cast (size), remaining); + + std::memcpy (buf, + static_cast (ms->block->getData()) + ms->position, + toRead); + ms->position += toRead; + return static_cast (toRead); + }; + + return stateExt->load (clapPlugin, &stream) + ? Result::ok() + : Result::fail ("CLAP state load failed"); + } + + Result saveStateIntoMemory (MemoryBlock& memoryBlock) override + { + auto* stateExt = reinterpret_cast ( + clapPlugin->get_extension (clapPlugin, CLAP_EXT_STATE)); + + if (stateExt == nullptr) + return Result::fail ("Plugin does not support CLAP state extension"); + + memoryBlock.reset(); + + clap_ostream_t stream {}; + stream.ctx = &memoryBlock; + stream.write = [] (const clap_ostream_t* s, const void* buf, uint64_t size) -> int64_t + { + auto* block = static_cast (s->ctx); + block->append (buf, static_cast (size)); + return static_cast (size); + }; + + return stateExt->save (clapPlugin, &stream) + ? Result::ok() + : Result::fail ("CLAP state save failed"); + } + + //============================================================================== + + bool hasEditor() const override { return false; } + + //============================================================================== + + static std::unique_ptr create (const AudioPluginDescription& desc, + const AudioPluginHostContext& context) + { + auto mod = CLAPModule::load (File (desc.fileOrBundlePath)); + if (mod == nullptr) + return nullptr; + + const clap_plugin_factory_t* factory = reinterpret_cast ( + mod->entry->get_factory (CLAP_PLUGIN_FACTORY_ID)); + + if (factory == nullptr) + return nullptr; + + auto yupHost = std::make_unique (context); + + const uint32_t count = factory->get_plugin_count (factory); + for (uint32_t i = 0; i < count; ++i) + { + const clap_plugin_descriptor_t* plugDesc = factory->get_plugin_descriptor (factory, i); + if (plugDesc == nullptr) + continue; + + if (String (plugDesc->id) != desc.identifier) + continue; + + const clap_plugin_t* plugin = factory->create_plugin (factory, + &yupHost->host, + plugDesc->id); + if (plugin == nullptr) + continue; + + if (! plugin->init (plugin)) + { + plugin->destroy (plugin); + continue; + } + + return std::make_unique (desc, context, std::move (mod), std::move (yupHost), plugin); + } + + return nullptr; + } + +private: + static AudioBusLayout buildBusLayout (const clap_plugin_t* plugin) + { + std::vector inputs, outputs; + + auto* portsExt = reinterpret_cast ( + plugin->get_extension (plugin, CLAP_EXT_AUDIO_PORTS)); + + if (portsExt != nullptr) + { + const uint32_t numInputs = portsExt->count (plugin, true); + for (uint32_t i = 0; i < numInputs; ++i) + { + clap_audio_port_info_t info {}; + portsExt->get (plugin, i, true, &info); + inputs.emplace_back (String (info.name), AudioBus::Type::Audio, AudioBus::Direction::Input, static_cast (info.channel_count)); + } + + const uint32_t numOutputs = portsExt->count (plugin, false); + for (uint32_t i = 0; i < numOutputs; ++i) + { + clap_audio_port_info_t info {}; + portsExt->get (plugin, i, false, &info); + outputs.emplace_back (String (info.name), AudioBus::Type::Audio, AudioBus::Direction::Output, static_cast (info.channel_count)); + } + } + + return AudioBusLayout (std::move (inputs), std::move (outputs)); + } + + void buildParameterList() + { + auto* paramsExt = reinterpret_cast ( + clapPlugin->get_extension (clapPlugin, CLAP_EXT_PARAMS)); + + if (paramsExt == nullptr) + return; + + const uint32_t count = paramsExt->count (clapPlugin); + for (uint32_t i = 0; i < count; ++i) + { + clap_param_info_t info {}; + if (! paramsExt->get_info (clapPlugin, i, &info)) + continue; + + auto param = AudioParameterBuilder() + .withID (String (info.name)) + .withName (String (info.name)) + .withRange (static_cast (info.min_value), + static_cast (info.max_value)) + .withDefault (static_cast (info.default_value)) + .build(); + + clapParameterIds.push_back (info.id); + addParameter (std::move (param)); + } + } + + AudioPluginHostContext hostContext; + std::unique_ptr clapModule; + std::unique_ptr yupHost; + const clap_plugin_t* clapPlugin = nullptr; + std::vector clapParameterIds; + int currentPreset = 0; +}; + +//============================================================================== + +CLAPFormat::CLAPFormat() = default; +CLAPFormat::~CLAPFormat() = default; + +AudioPluginFormatType CLAPFormat::getFormatType() const +{ + return AudioPluginFormatType::clap; +} + +String CLAPFormat::getFormatName() const +{ + return "CLAP"; +} + +FileSearchPath CLAPFormat::getDefaultSearchPaths() const +{ + FileSearchPath paths; + +#if YUP_MAC + paths.add (File ("/Library/Audio/Plug-Ins/CLAP")); + paths.add (File::getSpecialLocation (File::userHomeDirectory) + .getChildFile ("Library/Audio/Plug-Ins/CLAP")); +#elif YUP_WINDOWS + if (const char* pf = getenv ("CommonProgramFiles")) + paths.add (File (String (pf) + "\\CLAP")); + if (const char* appdata = getenv ("APPDATA")) + paths.add (File (String (appdata) + "\\CLAP")); +#elif YUP_LINUX + paths.add (File ("/usr/lib/clap")); + paths.add (File ("/usr/local/lib/clap")); + paths.add (File::getSpecialLocation (File::userHomeDirectory).getChildFile (".clap")); +#endif + + return paths; +} + +ResultValue> CLAPFormat::scanFile (const File& file) +{ + if (file.getFileExtension().toLowerCase() != ".clap") + return ResultValue>::fail ("Not a CLAP file"); + + auto mod = CLAPModule::load (file); + if (mod == nullptr) + return ResultValue>::fail ( + "Failed to load CLAP module: " + file.getFullPathName()); + + const clap_plugin_factory_t* factory = reinterpret_cast ( + mod->entry->get_factory (CLAP_PLUGIN_FACTORY_ID)); + + if (factory == nullptr) + return ResultValue>::fail ( + "No plugin factory in: " + file.getFullPathName()); + + std::vector results; + const uint32_t count = factory->get_plugin_count (factory); + + for (uint32_t i = 0; i < count; ++i) + { + const clap_plugin_descriptor_t* plugDesc = factory->get_plugin_descriptor (factory, i); + if (plugDesc == nullptr) + continue; + + AudioPluginDescription desc; + desc.formatType = AudioPluginFormatType::clap; + desc.fileOrBundlePath = file.getFullPathName(); + desc.name = String (plugDesc->name); + desc.vendor = String (plugDesc->vendor); + desc.version = String (plugDesc->version); + desc.identifier = String (plugDesc->id); + + if (plugDesc->features != nullptr) + { + for (int j = 0; plugDesc->features[j] != nullptr; ++j) + { + const String feature (plugDesc->features[j]); + if (feature == CLAP_PLUGIN_FEATURE_INSTRUMENT) + desc.isInstrument = true; + if (feature == CLAP_PLUGIN_FEATURE_AUDIO_EFFECT) + desc.isEffect = true; + } + } + + // Briefly instantiate the plugin to collect channel counts + { + clap_host_t host {}; + host.clap_version = CLAP_VERSION; + host.host_data = nullptr; + host.name = "YUP Scanner"; + host.vendor = "yup"; + host.url = ""; + host.version = "1.0.0"; + host.get_extension = [] (const clap_host_t*, const char*) -> const void* + { + return nullptr; + }; + host.request_restart = [] (const clap_host_t*) {}; + host.request_process = [] (const clap_host_t*) {}; + host.request_callback = [] (const clap_host_t*) {}; + + const clap_plugin_t* plugin = factory->create_plugin (factory, &host, plugDesc->id); + if (plugin != nullptr && plugin->init (plugin)) + { + auto* portsExt = reinterpret_cast ( + plugin->get_extension (plugin, CLAP_EXT_AUDIO_PORTS)); + + if (portsExt != nullptr) + { + const uint32_t numInputs = portsExt->count (plugin, true); + for (uint32_t p = 0; p < numInputs; ++p) + { + clap_audio_port_info_t info {}; + if (portsExt->get (plugin, p, true, &info)) + desc.numInputChannels += static_cast (info.channel_count); + } + + const uint32_t numOutputs = portsExt->count (plugin, false); + for (uint32_t p = 0; p < numOutputs; ++p) + { + clap_audio_port_info_t info {}; + if (portsExt->get (plugin, p, false, &info)) + desc.numOutputChannels += static_cast (info.channel_count); + } + } + + plugin->destroy (plugin); + } + } + + results.push_back (std::move (desc)); + } + + if (results.empty()) + return ResultValue>::fail ( + "No plugins found in: " + file.getFullPathName()); + + return ResultValue>::ok (std::move (results)); +} + +ResultValue> CLAPFormat::loadPlugin ( + const AudioPluginDescription& description, + const AudioPluginHostContext& context) +{ + auto instance = CLAPInstance::create (description, context); + + if (instance == nullptr) + return ResultValue>::fail ( + "Failed to load CLAP plugin: " + description.name); + + return ResultValue>::ok (std::move (instance)); +} + +} // namespace yup + +#endif // YUP_AUDIO_PLUGIN_HOST_ENABLE_CLAP diff --git a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.h b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.h new file mode 100644 index 000000000..c91069e32 --- /dev/null +++ b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.h @@ -0,0 +1,53 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#if YUP_AUDIO_PLUGIN_HOST_ENABLE_CLAP + +namespace yup +{ + +/** + AudioPluginFormat implementation for CLAP. + + Scans .clap bundles by loading the shared library and enumerating the + clap_plugin_entry factory. Loads plugins via clap_host_t + clap_plugin_t. +*/ +class CLAPFormat : public AudioPluginFormat +{ +public: + CLAPFormat(); + ~CLAPFormat() override; + + AudioPluginFormatType getFormatType() const override; + String getFormatName() const override; + + FileSearchPath getDefaultSearchPaths() const override; + + ResultValue> scanFile (const File& file) override; + + ResultValue> loadPlugin ( + const AudioPluginDescription& description, + const AudioPluginHostContext& context) override; +}; + +} // namespace yup + +#endif // YUP_AUDIO_PLUGIN_HOST_ENABLE_CLAP diff --git a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.cpp b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.cpp new file mode 100644 index 000000000..6a4f1fcca --- /dev/null +++ b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.cpp @@ -0,0 +1,613 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#if YUP_AUDIO_PLUGIN_HOST_ENABLE_VST3 + +namespace yup +{ + +using namespace Steinberg; + +namespace +{ + +//============================================================================== +// Converts a Steinberg TChar (UTF-16) string to a yup::String. +String toString (const Vst::TChar* src) +{ + return String (CharPointer_UTF16 (reinterpret_cast (src))); +} + +//============================================================================== +// RAII wrapper around a dynamically loaded VST3 module. +struct VST3Module +{ + ModuleHandle handle = nullptr; + using FactoryFn = IPluginFactory* (*) (); + FactoryFn getFactory = nullptr; + + static std::unique_ptr load (const File& file) + { + auto m = std::make_unique(); + +#if YUP_MAC + const auto bundlePath = file.getFullPathName().toStdString(); + m->handle = loadModule (bundlePath.c_str()); +#else + m->handle = loadModule (file.getFullPathName().toRawUTF8()); +#endif + + if (m->handle == nullptr) + return nullptr; + + m->getFactory = reinterpret_cast ( + getFunctionAddress (m->handle, "GetPluginFactory")); + + if (m->getFactory == nullptr) + { + unloadModule (m->handle); + return nullptr; + } + + return m; + } + + ~VST3Module() + { + if (handle != nullptr) + unloadModule (handle); + } +}; + +//============================================================================== +// Simple VST3 parameter queue for one parameter during processBlock(). +class SingleParamQueue : public Vst::IParamValueQueue +{ +public: + explicit SingleParamQueue (Vst::ParamID id) + : paramId (id) + { + } + + Vst::ParamID PLUGIN_API getParameterId() override { return paramId; } + + int32 PLUGIN_API getPointCount() override + { + return static_cast (points.size()); + } + + tresult PLUGIN_API getPoint (int32 index, int32& sampleOffset, Vst::ParamValue& value) override + { + if (index < 0 || index >= static_cast (points.size())) + return kResultFalse; + + sampleOffset = points[static_cast (index)].first; + value = points[static_cast (index)].second; + return kResultOk; + } + + tresult PLUGIN_API addPoint (int32 sampleOffset, Vst::ParamValue value, int32& index) override + { + index = static_cast (points.size()); + points.emplace_back (sampleOffset, value); + return kResultOk; + } + + DECLARE_FUNKNOWN_METHODS + +private: + Vst::ParamID paramId; + std::vector> points; +}; + +//============================================================================== +// Minimal IComponentHandler stub — required by IAudioProcessor::initialize(). +class HostComponentHandler : public Vst::IComponentHandler +{ +public: + tresult PLUGIN_API beginEdit (Vst::ParamID) override { return kResultOk; } + + tresult PLUGIN_API performEdit (Vst::ParamID, Vst::ParamValue) override { return kResultOk; } + + tresult PLUGIN_API endEdit (Vst::ParamID) override { return kResultOk; } + + tresult PLUGIN_API restartComponent (int32) override { return kResultOk; } + + DECLARE_FUNKNOWN_METHODS +}; + +} // namespace + +//============================================================================== + +class VST3Instance : public AudioPluginInstance +{ +public: + VST3Instance (const AudioPluginDescription& desc, + const AudioPluginHostContext& context, + std::unique_ptr module, + IPtr component, + IPtr processor, + IPtr controller) + : AudioPluginInstance (desc, + buildBusLayout (component.get())) + , hostContext (context) + , vst3Module (std::move (module)) + , vst3Component (std::move (component)) + , vst3Processor (std::move (processor)) + , vst3Controller (std::move (controller)) + { + buildParameterList(); + } + + ~VST3Instance() override + { + releaseResources(); + vst3Processor = nullptr; + vst3Controller = nullptr; + vst3Component->terminate(); + vst3Component = nullptr; + } + + //============================================================================== + + void prepareToPlay (float sampleRate, int maxBlockSize) override + { + Vst::ProcessSetup setup; + setup.processMode = Vst::kRealtime; + setup.symbolicSampleSize = hostContext.preferDoublePrecision + ? Vst::kSample64 + : Vst::kSample32; + setup.maxSamplesPerBlock = maxBlockSize; + setup.sampleRate = sampleRate; + + vst3Processor->setupProcessing (setup); + + const int numInputs = vst3Component->getBusCount (Vst::kAudio, Vst::kInput); + for (int i = 0; i < numInputs; ++i) + vst3Component->activateBus (Vst::kAudio, Vst::kInput, i, true); + + const int numOutputs = vst3Component->getBusCount (Vst::kAudio, Vst::kOutput); + for (int i = 0; i < numOutputs; ++i) + vst3Component->activateBus (Vst::kAudio, Vst::kOutput, i, true); + + vst3Component->setActive (true); + vst3Processor->setProcessing (true); + } + + void releaseResources() override + { + if (vst3Processor != nullptr) + vst3Processor->setProcessing (false); + + if (vst3Component != nullptr) + vst3Component->setActive (false); + } + + void processBlock (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) override + { + Vst::ProcessData data; + data.processMode = Vst::kRealtime; + data.symbolicSampleSize = Vst::kSample32; + data.numSamples = audioBuffer.getNumSamples(); + + // Input busses + Vst::AudioBusBuffers inputBus; + inputBus.numChannels = audioBuffer.getNumChannels(); + inputBus.channelBuffers32 = const_cast (audioBuffer.getArrayOfReadPointers()); + data.inputs = &inputBus; + data.numInputs = 1; + + // Output busses + Vst::AudioBusBuffers outputBus; + outputBus.numChannels = audioBuffer.getNumChannels(); + outputBus.channelBuffers32 = const_cast (audioBuffer.getArrayOfWritePointers()); + data.outputs = &outputBus; + data.numOutputs = 1; + + // Events (MIDI) + // TODO: map MidiBuffer to Vst::EventList when MIDI support is added in a later task + + Vst::ParameterChanges inputParameterChanges (static_cast (vst3ParameterIds.size())); + const auto params = getParameters(); + const auto numParams = yup::jmin (params.size(), vst3ParameterIds.size()); + + for (std::size_t i = 0; i < numParams; ++i) + { + int32 queueIndex = 0; + if (auto* queue = inputParameterChanges.addParameterData (vst3ParameterIds[i], queueIndex)) + { + int32 pointIndex = 0; + queue->addPoint (0, + static_cast (params[i]->getValue()), + pointIndex); + } + } + + data.inputParameterChanges = &inputParameterChanges; + + // Process context + if (hostContext.playHead != nullptr) + { + const auto optPos = hostContext.playHead->getPosition(); + + if (optPos.has_value()) + { + const auto& posInfo = optPos.value(); + vst3ProcessContext.state = Vst::ProcessContext::kPlaying; + vst3ProcessContext.sampleRate = getSampleRate(); + + if (auto timeSamples = posInfo.getTimeInSamples()) + vst3ProcessContext.projectTimeSamples = *timeSamples; + + if (auto tempo = posInfo.getBpm()) + vst3ProcessContext.tempo = *tempo; + + data.processContext = &vst3ProcessContext; + } + } + + vst3Processor->process (data); + } + + //============================================================================== + + int getCurrentPreset() const noexcept override { return currentPreset; } + + void setCurrentPreset (int index) noexcept override + { + if (vst3Controller != nullptr) + { + // VST3 presets are loaded externally; track index only + currentPreset = index; + } + } + + int getNumPresets() const override { return numPresets; } + + String getPresetName (int index) const override + { + if (index < 0 || index >= numPresets) + return {}; + + return "Preset " + String (index); + } + + void setPresetName (int, StringRef) override {} + + //============================================================================== + + Result loadStateFromMemory (const MemoryBlock& memoryBlock) override + { + if (vst3Component == nullptr) + return Result::fail ("Plugin not loaded"); + + MemoryBlock mutable_copy = memoryBlock; + IBStream* stream = new MemoryStream (mutable_copy.getData(), + static_cast (mutable_copy.getSize())); + + const auto res = vst3Component->setState (stream); + stream->release(); + + if (res != kResultOk) + return Result::fail ("VST3 setState() failed"); + + return Result::ok(); + } + + Result saveStateIntoMemory (MemoryBlock& memoryBlock) override + { + if (vst3Component == nullptr) + return Result::fail ("Plugin not loaded"); + + MemoryStream* stream = new MemoryStream(); + const auto res = vst3Component->getState (stream); + + if (res != kResultOk) + { + stream->release(); + return Result::fail ("VST3 getState() failed"); + } + + memoryBlock.replaceAll (stream->getData(), static_cast (stream->getSize())); + stream->release(); + return Result::ok(); + } + + //============================================================================== + + bool hasEditor() const override { return false; } + + //============================================================================== + + static std::unique_ptr create (const AudioPluginDescription& desc, + const AudioPluginHostContext& context) + { + auto mod = VST3Module::load (File (desc.fileOrBundlePath)); + if (mod == nullptr) + return nullptr; + + IPluginFactory* rawFactory = mod->getFactory(); + if (rawFactory == nullptr) + return nullptr; + + IPtr factory (rawFactory); + + // Find the component class matching identifier + const int classCount = factory->countClasses(); + for (int i = 0; i < classCount; ++i) + { + PClassInfo classInfo; + if (factory->getClassInfo (i, &classInfo) != kResultOk) + continue; + + if (String (classInfo.name) != desc.name && classInfo.category != String ("Audio Module Class")) + continue; + + Vst::IComponent* rawComponent = nullptr; + if (factory->createInstance (classInfo.cid, Vst::IComponent::iid, reinterpret_cast (&rawComponent)) != kResultOk) + continue; + + IPtr component = IPtr::adopt (rawComponent); + + IPtr host; + if (component->initialize (host) != kResultOk) + continue; + + Vst::IAudioProcessor* rawProcessor = nullptr; + if (component->queryInterface (Vst::IAudioProcessor::iid, + reinterpret_cast (&rawProcessor)) + != kResultOk) + continue; + IPtr processor = IPtr::adopt (rawProcessor); + + Vst::IEditController* rawController = nullptr; + component->queryInterface (Vst::IEditController::iid, + reinterpret_cast (&rawController)); + IPtr controller = IPtr::adopt (rawController); + + return std::make_unique (desc, context, std::move (mod), std::move (component), std::move (processor), std::move (controller)); + } + + return nullptr; + } + +private: + static AudioBusLayout buildBusLayout (Vst::IComponent* component) + { + std::vector inputs, outputs; + + const int numInputs = component->getBusCount (Vst::kAudio, Vst::kInput); + for (int i = 0; i < numInputs; ++i) + { + Vst::BusInfo info; + component->getBusInfo (Vst::kAudio, Vst::kInput, i, info); + inputs.emplace_back (toString (info.name), AudioBus::Type::Audio, AudioBus::Direction::Input, static_cast (info.channelCount)); + } + + const int numOutputs = component->getBusCount (Vst::kAudio, Vst::kOutput); + for (int i = 0; i < numOutputs; ++i) + { + Vst::BusInfo info; + component->getBusInfo (Vst::kAudio, Vst::kOutput, i, info); + outputs.emplace_back (toString (info.name), AudioBus::Type::Audio, AudioBus::Direction::Output, static_cast (info.channelCount)); + } + + return AudioBusLayout (std::move (inputs), std::move (outputs)); + } + + void buildParameterList() + { + if (vst3Controller == nullptr) + return; + + const int count = vst3Controller->getParameterCount(); + numPresets = 0; + + for (int i = 0; i < count; ++i) + { + Vst::ParameterInfo info; + vst3Controller->getParameterInfo (i, info); + + if (info.flags & Vst::ParameterInfo::kIsProgramChange) + { + numPresets = static_cast (info.stepCount) + 1; + continue; + } + + auto param = AudioParameterBuilder() + .withID (toString (info.title)) + .withName (toString (info.title)) + .withRange (0.0f, 1.0f) + .withDefault (static_cast (info.defaultNormalizedValue)) + .build(); + + vst3ParameterIds.push_back (info.id); + addParameter (std::move (param)); + } + } + + AudioPluginHostContext hostContext; + std::unique_ptr vst3Module; + IPtr vst3Component; + IPtr vst3Processor; + IPtr vst3Controller; + Vst::ProcessContext vst3ProcessContext {}; + std::vector vst3ParameterIds; + int currentPreset = 0; + int numPresets = 0; +}; + +//============================================================================== + +VST3Format::VST3Format() = default; +VST3Format::~VST3Format() = default; + +AudioPluginFormatType VST3Format::getFormatType() const +{ + return AudioPluginFormatType::vst3; +} + +String VST3Format::getFormatName() const +{ + return "VST3"; +} + +FileSearchPath VST3Format::getDefaultSearchPaths() const +{ + FileSearchPath paths; + +#if YUP_MAC + paths.add (File ("/Library/Audio/Plug-Ins/VST3")); + paths.add (File::getSpecialLocation (File::userHomeDirectory) + .getChildFile ("Library/Audio/Plug-Ins/VST3")); +#elif YUP_WINDOWS + // %CommonProgramFiles%\VST3 + if (const char* pf = getenv ("CommonProgramFiles")) + paths.add (File (String (pf) + "\\VST3")); + // %APPDATA%\VST3 + if (const char* appdata = getenv ("APPDATA")) + paths.add (File (String (appdata) + "\\VST3")); +#elif YUP_LINUX + paths.add (File ("/usr/lib/vst3")); + paths.add (File ("/usr/local/lib/vst3")); + paths.add (File::getSpecialLocation (File::userHomeDirectory).getChildFile (".vst3")); +#endif + + return paths; +} + +ResultValue> VST3Format::scanFile (const File& file) +{ + if (file.getFileExtension().toLowerCase() != ".vst3" + && ! file.isDirectory()) + return ResultValue>::fail ("Not a VST3 file"); + + auto mod = VST3Module::load (file); + if (mod == nullptr) + return ResultValue>::fail ( + "Failed to load VST3 module: " + file.getFullPathName()); + + IPluginFactory* rawFactory = mod->getFactory(); + if (rawFactory == nullptr) + return ResultValue>::fail ( + "No factory in " + file.getFullPathName()); + + IPtr factory (rawFactory); + + std::vector results; + const int classCount = factory->countClasses(); + + for (int i = 0; i < classCount; ++i) + { + PClassInfo2 info2 {}; + if (auto* factory2 = FUnknownPtr (factory).getInterface()) + { + if (factory2->getClassInfo2 (i, &info2) != kResultOk) + continue; + } + else + { + PClassInfo info; + if (factory->getClassInfo (i, &info) != kResultOk) + continue; + + std::memcpy (info2.cid, info.cid, sizeof (TUID)); + std::memcpy (info2.name, info.name, PClassInfo::kNameSize); + std::memcpy (info2.category, info.category, PClassInfo::kCategorySize); + info2.vendor[0] = '\0'; + info2.version[0] = '\0'; + info2.subCategories[0] = '\0'; + } + + if (String (info2.category) != "Audio Module Class") + continue; + + AudioPluginDescription desc; + desc.formatType = AudioPluginFormatType::vst3; + desc.fileOrBundlePath = file.getFullPathName(); + desc.name = String (info2.name); + desc.vendor = String (info2.vendor); + desc.version = String (info2.version); + desc.category = String (info2.subCategories); + desc.isInstrument = String (info2.subCategories).containsIgnoreCase ("Instrument"); + desc.isEffect = ! desc.isInstrument; + + // Encode FUID as hex string for use as identifier + char uidStr[33] = {}; + snprintf (uidStr, sizeof (uidStr), "%08X%08X%08X%08X", info2.cid[0], info2.cid[1], info2.cid[2], info2.cid[3]); + desc.identifier = String (uidStr); + + // Briefly instantiate the component to collect channel counts + Vst::IComponent* rawComponent = nullptr; + if (factory->createInstance (info2.cid, Vst::IComponent::iid, reinterpret_cast (&rawComponent)) == kResultOk) + { + IPtr component = IPtr::adopt (rawComponent); + IPtr host; + + if (component->initialize (host) == kResultOk) + { + const int numInputs = component->getBusCount (Vst::kAudio, Vst::kInput); + for (int b = 0; b < numInputs; ++b) + { + Vst::BusInfo busInfo; + if (component->getBusInfo (Vst::kAudio, Vst::kInput, b, busInfo) == kResultOk) + desc.numInputChannels += static_cast (busInfo.channelCount); + } + + const int numOutputs = component->getBusCount (Vst::kAudio, Vst::kOutput); + for (int b = 0; b < numOutputs; ++b) + { + Vst::BusInfo busInfo; + if (component->getBusInfo (Vst::kAudio, Vst::kOutput, b, busInfo) == kResultOk) + desc.numOutputChannels += static_cast (busInfo.channelCount); + } + + component->terminate(); + } + } + + results.push_back (std::move (desc)); + } + + if (results.empty()) + return ResultValue>::fail ( + "No Audio Module Class entries in " + file.getFullPathName()); + + return ResultValue>::ok (std::move (results)); +} + +ResultValue> VST3Format::loadPlugin ( + const AudioPluginDescription& description, + const AudioPluginHostContext& context) +{ + auto instance = VST3Instance::create (description, context); + + if (instance == nullptr) + return ResultValue>::fail ( + "Failed to load VST3 plugin: " + description.name); + + return ResultValue>::ok (std::move (instance)); +} + +} // namespace yup + +#endif // YUP_AUDIO_PLUGIN_HOST_ENABLE_VST3 diff --git a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.h b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.h new file mode 100644 index 000000000..90d59d8ef --- /dev/null +++ b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.h @@ -0,0 +1,53 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#if YUP_AUDIO_PLUGIN_HOST_ENABLE_VST3 + +namespace yup +{ + +/** + AudioPluginFormat implementation for VST3. + + Scans .vst3 bundles/folders by loading their module factory and enumerating + classes. Loads plugins via IComponent + IAudioProcessor setup. +*/ +class VST3Format : public AudioPluginFormat +{ +public: + VST3Format(); + ~VST3Format() override; + + AudioPluginFormatType getFormatType() const override; + String getFormatName() const override; + + FileSearchPath getDefaultSearchPaths() const override; + + ResultValue> scanFile (const File& file) override; + + ResultValue> loadPlugin ( + const AudioPluginDescription& description, + const AudioPluginHostContext& context) override; +}; + +} // namespace yup + +#endif // YUP_AUDIO_PLUGIN_HOST_ENABLE_VST3 diff --git a/modules/yup_audio_plugin_host/yup_audio_plugin_host.cpp b/modules/yup_audio_plugin_host/yup_audio_plugin_host.cpp new file mode 100644 index 000000000..5be88a873 --- /dev/null +++ b/modules/yup_audio_plugin_host/yup_audio_plugin_host.cpp @@ -0,0 +1,93 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#if YUP_MAC +#define YUP_CORE_INCLUDE_OBJC_HELPERS 1 +#endif + +#include "yup_audio_plugin_host.h" + +//============================================================================== +#include "host/yup_AudioPluginScanner.cpp" +#include "host/yup_AudioPluginInstance.cpp" + +//============================================================================== +#if YUP_AUDIO_PLUGIN_HOST_ENABLE_VST3 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if YUP_WINDOWS +#include +using ModuleHandle = HMODULE; +#define loadModule(path) LoadLibraryA (path) +#define getFunctionAddress GetProcAddress +#define unloadModule FreeLibrary +#elif YUP_MAC || YUP_LINUX +#include +using ModuleHandle = void*; +#define loadModule(path) dlopen (path, RTLD_LAZY | RTLD_LOCAL) +#define getFunctionAddress dlsym +#define unloadModule dlclose +#endif + +#include "native/yup_AudioPluginInstance_VST3.cpp" +#endif + +#if YUP_AUDIO_PLUGIN_HOST_ENABLE_CLAP +#include + +#if YUP_WINDOWS +#include +using CLAPModuleHandle = HMODULE; +#define clapLoadModule(p) LoadLibraryA (p) +#define clapGetAddress GetProcAddress +#define clapUnloadModule FreeLibrary +#else +#include +using CLAPModuleHandle = void*; +#define clapLoadModule(p) dlopen (p, RTLD_LAZY | RTLD_LOCAL) +#define clapGetAddress dlsym +#define clapUnloadModule dlclose +#endif + +#include "native/yup_AudioPluginInstance_CLAP.cpp" +#endif + +#if YUP_AUDIO_PLUGIN_HOST_ENABLE_AU && YUP_MAC +#import +#import +#import +#import +#import +#import + +#include "native/yup_AudioPluginInstance_AUv2.mm" +#endif diff --git a/modules/yup_audio_plugin_host/yup_audio_plugin_host.h b/modules/yup_audio_plugin_host/yup_audio_plugin_host.h new file mode 100644 index 000000000..e861f3987 --- /dev/null +++ b/modules/yup_audio_plugin_host/yup_audio_plugin_host.h @@ -0,0 +1,60 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +/* + ============================================================================== + + BEGIN_YUP_MODULE_DECLARATION + + ID: yup_audio_plugin_host + vendor: yup + version: 1.0.0 + name: YUP Audio Plugin Host + description: In-process hosting of VST3, CLAP, and AUv2 audio plugins. + website: https://github.com/kunitoki/yup + license: ISC + minimumCppStandard: 17 + + dependencies: yup_audio_processors + searchpaths: native + + END_YUP_MODULE_DECLARATION + + ============================================================================== +*/ + +#pragma once +#define YUP_AUDIO_PLUGIN_HOST_H_INCLUDED + +#include + +//============================================================================== +#include "host/yup_AudioPluginFormatType.h" +#include "host/yup_AudioPluginDescription.h" +#include "host/yup_AudioPluginHostContext.h" +#include "host/yup_AudioPluginFormat.h" +#include "host/yup_AudioPluginScanner.h" +#include "host/yup_AudioPluginInstance.h" + +//============================================================================== +#include "native/yup_AudioPluginInstance_VST3.h" +#include "native/yup_AudioPluginInstance_CLAP.h" +#include "native/yup_AudioPluginInstance_AUv2.h" diff --git a/modules/yup_audio_plugin_host/yup_audio_plugin_host.mm b/modules/yup_audio_plugin_host/yup_audio_plugin_host.mm new file mode 100644 index 000000000..7b8f41c3a --- /dev/null +++ b/modules/yup_audio_plugin_host/yup_audio_plugin_host.mm @@ -0,0 +1,22 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include "yup_audio_plugin_host.cpp" diff --git a/modules/yup_core/misc/yup_ResultValue.h b/modules/yup_core/misc/yup_ResultValue.h index c94c615a0..18b45d7c5 100644 --- a/modules/yup_core/misc/yup_ResultValue.h +++ b/modules/yup_core/misc/yup_ResultValue.h @@ -108,13 +108,23 @@ class YUP_API ResultValue } /** Returns a copy of the value that was set when this result was created. */ - T getValue() const + T getValue() const& + requires std::copy_constructible { jassert (valueOrErrorMessage.index() == 1); // Trying to access the value of the result, when the result is holding an error instead! return std::get<1> (valueOrErrorMessage); } + /** Returns a moved from value that was set when this result was created. */ + T getValue() && + requires std::move_constructible + { + jassert (valueOrErrorMessage.index() == 1); // Trying to access the value of the result, when the result is holding an error instead! + + return std::get<1> (std::move (valueOrErrorMessage)); + } + /** Returns the mutable reference that was set when this result was created. */ T& getReference() noexcept { diff --git a/modules/yup_gui/native/yup_Windowing_sdl2.cpp b/modules/yup_gui/native/yup_Windowing_sdl2.cpp index 360e2ae24..90545bb79 100644 --- a/modules/yup_gui/native/yup_Windowing_sdl2.cpp +++ b/modules/yup_gui/native/yup_Windowing_sdl2.cpp @@ -23,11 +23,7 @@ namespace yup { //============================================================================== -#ifndef YUP_WINDOWING_LOGGING -#define YUP_WINDOWING_LOGGING 1 -#endif - -#if YUP_WINDOWING_LOGGING +#if YUP_ENABLE_WINDOWING_EVENT_LOGGING #define YUP_WINDOWING_LOG(textToWrite) YUP_DBG (textToWrite) #else #define YUP_WINDOWING_LOG(textToWrite) \ diff --git a/modules/yup_gui/yup_gui.h b/modules/yup_gui/yup_gui.h index 0945cc29e..130177e09 100644 --- a/modules/yup_gui/yup_gui.h +++ b/modules/yup_gui/yup_gui.h @@ -60,6 +60,15 @@ #define YUP_ENABLE_COMPONENT_REPAINT_DEBUGGING 0 #endif +//============================================================================== +/** Config: YUP_ENABLE_WINDOWING_EVENT_LOGGING + + Enable logging of windowing events like movement, resizes, mouse interactions. +*/ +#ifndef YUP_ENABLE_WINDOWING_EVENT_LOGGING +#define YUP_ENABLE_WINDOWING_EVENT_LOGGING 0 +#endif + //============================================================================== #include diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 685900b61..cd1a97d9e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -62,6 +62,7 @@ set (target_modules yup_events yup_data_model yup_graphics + yup_audio_plugin_host dr_libs pffft_library opus_library diff --git a/tests/yup_audio_plugin_host.cpp b/tests/yup_audio_plugin_host.cpp new file mode 100644 index 000000000..da18dcb84 --- /dev/null +++ b/tests/yup_audio_plugin_host.cpp @@ -0,0 +1,25 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include "yup_audio_plugin_host/AudioPluginDescription.cpp" +#include "yup_audio_plugin_host/AudioPluginScanner.cpp" +#include "yup_audio_plugin_host/AudioPluginInstance.cpp" +#include "yup_audio_plugin_host/AudioPluginState.cpp" diff --git a/tests/yup_audio_plugin_host/AudioPluginDescription.cpp b/tests/yup_audio_plugin_host/AudioPluginDescription.cpp new file mode 100644 index 000000000..9f218a75f --- /dev/null +++ b/tests/yup_audio_plugin_host/AudioPluginDescription.cpp @@ -0,0 +1,43 @@ +#include + +#include + +using namespace yup; + +TEST (AudioPluginDescriptionTests, DefaultFormatTypeIsUnknown) +{ + AudioPluginDescription desc; + EXPECT_EQ (AudioPluginFormatType::unknown, desc.formatType); +} + +TEST (AudioPluginDescriptionTests, IsInstrumentDefaultsFalse) +{ + AudioPluginDescription desc; + EXPECT_FALSE (desc.isInstrument); +} + +TEST (AudioPluginDescriptionTests, EqualityMatchesAllFields) +{ + AudioPluginDescription a; + a.name = "Synth"; + a.identifier = "com.vendor.synth"; + a.formatType = AudioPluginFormatType::vst3; + + AudioPluginDescription b = a; + EXPECT_EQ (a, b); + + b.version = "2.0"; + EXPECT_NE (a, b); +} + +TEST (AudioPluginHostContextTests, DefaultSampleRate) +{ + AudioPluginHostContext ctx; + EXPECT_FLOAT_EQ (44100.0f, ctx.sampleRate); +} + +TEST (AudioPluginHostContextTests, DefaultMaxBlockSize) +{ + AudioPluginHostContext ctx; + EXPECT_EQ (512, ctx.maxBlockSize); +} diff --git a/tests/yup_audio_plugin_host/AudioPluginInstance.cpp b/tests/yup_audio_plugin_host/AudioPluginInstance.cpp new file mode 100644 index 000000000..abcd6b8f7 --- /dev/null +++ b/tests/yup_audio_plugin_host/AudioPluginInstance.cpp @@ -0,0 +1,113 @@ +#include + +#include + +using namespace yup; + +namespace +{ + +class FakePluginInstance : public AudioPluginInstance +{ +public: + FakePluginInstance() + : AudioPluginInstance (makeDescription(), + AudioBusLayout ({ AudioBus ("Input", AudioBus::Type::Audio, AudioBus::Direction::Input, 2) }, + { AudioBus ("Output", AudioBus::Type::Audio, AudioBus::Direction::Output, 2) })) + { + } + + void prepareToPlay (float, int) override { prepared = true; } + + void releaseResources() override { prepared = false; } + + void processBlock (AudioBuffer& audio, MidiBuffer&) override + { + audio.clear(); + processCallCount++; + } + + int getCurrentPreset() const noexcept override { return 0; } + + void setCurrentPreset (int) noexcept override {} + + int getNumPresets() const override { return 0; } + + String getPresetName (int) const override { return {}; } + + void setPresetName (int, StringRef) override {} + + Result loadStateFromMemory (const MemoryBlock& block) override + { + lastLoadedState = block; + return Result::ok(); + } + + Result saveStateIntoMemory (MemoryBlock& block) override + { + block = lastLoadedState; + return Result::ok(); + } + + bool hasEditor() const override { return false; } + + bool prepared = false; + int processCallCount = 0; + MemoryBlock lastLoadedState; + +private: + static AudioPluginDescription makeDescription() + { + AudioPluginDescription d; + d.name = "FakePlugin"; + d.identifier = "fake.plugin"; + d.formatType = AudioPluginFormatType::vst3; + return d; + } +}; + +} // namespace + +class AudioPluginInstanceTests : public ::testing::Test +{ +protected: + FakePluginInstance instance; +}; + +TEST_F (AudioPluginInstanceTests, DescriptionIsStoredCorrectly) +{ + EXPECT_EQ ("FakePlugin", instance.getDescription().name); + EXPECT_EQ (AudioPluginFormatType::vst3, instance.getDescription().formatType); +} + +TEST_F (AudioPluginInstanceTests, PrepareToPlaySetsPreparedFlag) +{ + instance.prepareToPlay (44100.0f, 512); + EXPECT_TRUE (instance.prepared); + EXPECT_FLOAT_EQ (44100.0f, instance.getSampleRate()); + EXPECT_EQ (512, instance.getSamplesPerBlock()); +} + +TEST_F (AudioPluginInstanceTests, ReleaseResourcesClearsPreparedFlag) +{ + instance.prepareToPlay (44100.0f, 512); + instance.releaseResources(); + EXPECT_FALSE (instance.prepared); +} + +TEST_F (AudioPluginInstanceTests, ProcessBlockIncrementsCounter) +{ + AudioBuffer audio (2, 512); + MidiBuffer midi; + + instance.prepareToPlay (44100.0f, 512); + instance.processBlock (audio, midi); + + EXPECT_EQ (1, instance.processCallCount); +} + +TEST_F (AudioPluginInstanceTests, BusLayoutMatchesConstructorArg) +{ + EXPECT_EQ (1, instance.getNumAudioInputs()); + EXPECT_EQ (1, instance.getNumAudioOutputs()); +} diff --git a/tests/yup_audio_plugin_host/AudioPluginScanner.cpp b/tests/yup_audio_plugin_host/AudioPluginScanner.cpp new file mode 100644 index 000000000..95088fe66 --- /dev/null +++ b/tests/yup_audio_plugin_host/AudioPluginScanner.cpp @@ -0,0 +1,103 @@ +#include + +#include + +using namespace yup; + +namespace +{ + +class FakeFormat : public AudioPluginFormat +{ +public: + explicit FakeFormat (AudioPluginFormatType type) + : fakeType (type) + { + } + + AudioPluginFormatType getFormatType() const override { return fakeType; } + + String getFormatName() const override { return "Fake"; } + + FileSearchPath getDefaultSearchPaths() const override { return {}; } + + ResultValue> scanFile (const File& file) override + { + if (file.getFileName() == "bad.vst3") + return ResultValue>::fail ("unsupported"); + + AudioPluginDescription desc; + desc.formatType = fakeType; + desc.name = file.getFileNameWithoutExtension(); + desc.identifier = "fake." + file.getFileNameWithoutExtension(); + return std::vector { desc }; + } + + ResultValue> loadPlugin ( + const AudioPluginDescription&, + const AudioPluginHostContext&) override + { + return ResultValue>::fail ("not implemented"); + } + +private: + AudioPluginFormatType fakeType; +}; + +} // namespace + +class AudioPluginScannerTests : public ::testing::Test +{ +protected: + void SetUp() override + { + scanner.addFormat (std::make_unique (AudioPluginFormatType::vst3)); + } + + AudioPluginScanner scanner; +}; + +TEST_F (AudioPluginScannerTests, EmptyPathReturnsNoResults) +{ + auto result = scanner.scan (FileSearchPath {}); + EXPECT_TRUE (result.discovered.empty()); + EXPECT_TRUE (result.failedPaths.empty()); +} + +TEST_F (AudioPluginScannerTests, BadPluginAppearsInFailedPaths) +{ + File tmpDir = File::getSpecialLocation (File::tempDirectory).getChildFile ("yup_scanner_test"); + tmpDir.createDirectory(); + tmpDir.getChildFile ("bad.vst3").create(); + + FileSearchPath path; + path.add (tmpDir); + + auto result = scanner.scan (path); + EXPECT_TRUE (result.discovered.empty()); + EXPECT_EQ (1, (int) result.failedPaths.size()); + + tmpDir.deleteRecursively(); +} + +TEST_F (AudioPluginScannerTests, GoodPluginAppearsInDiscovered) +{ + File tmpDir = File::getSpecialLocation (File::tempDirectory).getChildFile ("yup_scanner_test2"); + tmpDir.createDirectory(); + tmpDir.getChildFile ("MySynth.vst3").create(); + + FileSearchPath path; + path.add (tmpDir); + + auto result = scanner.scan (path); + ASSERT_EQ (1, (int) result.discovered.size()); + EXPECT_EQ ("MySynth", result.discovered[0].name); + + tmpDir.deleteRecursively(); +} + +TEST_F (AudioPluginScannerTests, AddingDuplicateFormatTypeReplacesExisting) +{ + scanner.addFormat (std::make_unique (AudioPluginFormatType::vst3)); + EXPECT_EQ (1, scanner.getNumFormats()); +} diff --git a/tests/yup_audio_plugin_host/AudioPluginState.cpp b/tests/yup_audio_plugin_host/AudioPluginState.cpp new file mode 100644 index 000000000..551f5a3bf --- /dev/null +++ b/tests/yup_audio_plugin_host/AudioPluginState.cpp @@ -0,0 +1,82 @@ +#include + +#include + +using namespace yup; + +namespace +{ + +class StatefulFakeInstance : public AudioPluginInstance +{ +public: + StatefulFakeInstance() + : AudioPluginInstance (AudioPluginDescription {}, + AudioBusLayout ({}, {})) + { + } + + void prepareToPlay (float, int) override {} + + void releaseResources() override {} + + void processBlock (AudioBuffer&, MidiBuffer&) override {} + + int getCurrentPreset() const noexcept override { return currentPreset; } + + void setCurrentPreset (int i) noexcept override { currentPreset = i; } + + int getNumPresets() const override { return 3; } + + String getPresetName (int i) const override { return "Preset " + String (i); } + + void setPresetName (int, StringRef) override {} + + bool hasEditor() const override { return false; } + + Result loadStateFromMemory (const MemoryBlock& block) override + { + storedState = block; + return Result::ok(); + } + + Result saveStateIntoMemory (MemoryBlock& block) override + { + block = storedState; + return Result::ok(); + } + + int currentPreset = 0; + MemoryBlock storedState; +}; + +} // namespace + +TEST (AudioPluginStateTests, RoundTripPreservesData) +{ + StatefulFakeInstance instance; + + const char rawData[] = { 0x01, 0x02, 0x03, 0x04 }; + MemoryBlock written (rawData, sizeof (rawData)); + + EXPECT_TRUE (instance.loadStateFromMemory (written).wasOk()); + + MemoryBlock readBack; + EXPECT_TRUE (instance.saveStateIntoMemory (readBack).wasOk()); + + EXPECT_EQ (written, readBack); +} + +TEST (AudioPluginStateTests, PresetNameByIndex) +{ + StatefulFakeInstance instance; + EXPECT_EQ ("Preset 0", instance.getPresetName (0)); + EXPECT_EQ ("Preset 2", instance.getPresetName (2)); +} + +TEST (AudioPluginStateTests, SetAndGetCurrentPreset) +{ + StatefulFakeInstance instance; + instance.setCurrentPreset (2); + EXPECT_EQ (2, instance.getCurrentPreset()); +} From 22c6fe70da7e6bb86812b8b868477d9e74f0a5df Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sat, 16 May 2026 15:53:33 +0200 Subject: [PATCH 02/35] Fix audio allocations --- .../native/yup_AudioPluginInstance_AUv2.mm | 4 +- .../native/yup_AudioPluginInstance_CLAP.cpp | 37 +++++++++----- .../native/yup_AudioPluginInstance_VST3.cpp | 48 +++---------------- 3 files changed, 32 insertions(+), 57 deletions(-) diff --git a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.mm b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.mm index bcacb399d..75f1ceb43 100644 --- a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.mm +++ b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.mm @@ -386,6 +386,8 @@ void releaseResources() override void processBlock (AudioBuffer& audioBuffer, MidiBuffer&) override { + ScopedNoDenormals noDenormals; + const int numSamples = audioBuffer.getNumSamples(); const int numChannels = audioBuffer.getNumChannels(); @@ -441,8 +443,6 @@ void processBlock (AudioBuffer& audioBuffer, MidiBuffer&) override abl->mBuffers[c].mData, static_cast (numSamples) * sizeof (float)); } - - syncParameterValuesFromAudioUnit(); } //============================================================================== diff --git a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.cpp b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.cpp index 5c875d5fc..62bcbd256 100644 --- a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.cpp +++ b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.cpp @@ -184,6 +184,11 @@ class CLAPInstance : public AudioPluginInstance void prepareToPlay (float sampleRate, int maxBlockSize) override { + const int numChannels = jmax (2, pluginDescription.numInputChannels, pluginDescription.numOutputChannels); + preparedInPtrs.resize (static_cast (numChannels)); + preparedOutPtrs.resize (static_cast (numChannels)); + clapInputEvents.parameterEvents.reserve (clapParameterIds.size()); + clapPlugin->activate (clapPlugin, sampleRate, static_cast (maxBlockSize), static_cast (maxBlockSize)); clapPlugin->start_processing (clapPlugin); @@ -200,27 +205,31 @@ class CLAPInstance : public AudioPluginInstance void processBlock (AudioBuffer& audioBuffer, MidiBuffer& /*midiBuffer*/) override { + ScopedNoDenormals noDenormals; + const int numSamples = audioBuffer.getNumSamples(); const int numChannels = audioBuffer.getNumChannels(); - // Build audio buffer descriptors - std::vector inPtrs (static_cast (numChannels)); - std::vector outPtrs (static_cast (numChannels)); + if (static_cast (preparedInPtrs.size()) < numChannels) + { + jassertfalse; + return; + } for (int c = 0; c < numChannels; ++c) { - inPtrs[static_cast (c)] = audioBuffer.getReadPointer (c); - outPtrs[static_cast (c)] = audioBuffer.getWritePointer (c); + preparedInPtrs[static_cast (c)] = audioBuffer.getReadPointer (c); + preparedOutPtrs[static_cast (c)] = audioBuffer.getWritePointer (c); } clap_audio_buffer_t inputBuf {}; - inputBuf.data32 = const_cast (inPtrs.data()); + inputBuf.data32 = const_cast (preparedInPtrs.data()); inputBuf.channel_count = static_cast (numChannels); inputBuf.latency = 0; inputBuf.constant_mask = 0; clap_audio_buffer_t outputBuf {}; - outputBuf.data32 = outPtrs.data(); + outputBuf.data32 = preparedOutPtrs.data(); outputBuf.channel_count = static_cast (numChannels); clap_process_t process {}; @@ -231,17 +240,15 @@ class CLAPInstance : public AudioPluginInstance process.audio_outputs = &outputBuf; process.audio_outputs_count = 1; - CLAPInputEvents inputEvents; + clapInputEvents.parameterEvents.clear(); const auto params = getParameters(); const auto numParams = yup::jmin (params.size(), clapParameterIds.size()); - inputEvents.parameterEvents.reserve (numParams); for (std::size_t i = 0; i < numParams; ++i) - inputEvents.addParameterValue (clapParameterIds[i], static_cast (params[i]->getValue())); + clapInputEvents.addParameterValue (clapParameterIds[i], static_cast (params[i]->getValue())); - CLAPOutputEvents outputEvents; - process.in_events = &inputEvents.inputEvents; - process.out_events = &outputEvents.outputEvents; + process.in_events = &clapInputEvents.inputEvents; + process.out_events = &clapOutputEvents.outputEvents; // TODO: map MidiBuffer to CLAP event list in a later task @@ -436,6 +443,10 @@ class CLAPInstance : public AudioPluginInstance std::unique_ptr clapModule; std::unique_ptr yupHost; const clap_plugin_t* clapPlugin = nullptr; + CLAPInputEvents clapInputEvents; + CLAPOutputEvents clapOutputEvents; + std::vector preparedInPtrs; + std::vector preparedOutPtrs; std::vector clapParameterIds; int currentPreset = 0; }; diff --git a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.cpp b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.cpp index 6a4f1fcca..d492cd6e5 100644 --- a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.cpp +++ b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.cpp @@ -77,47 +77,6 @@ struct VST3Module } }; -//============================================================================== -// Simple VST3 parameter queue for one parameter during processBlock(). -class SingleParamQueue : public Vst::IParamValueQueue -{ -public: - explicit SingleParamQueue (Vst::ParamID id) - : paramId (id) - { - } - - Vst::ParamID PLUGIN_API getParameterId() override { return paramId; } - - int32 PLUGIN_API getPointCount() override - { - return static_cast (points.size()); - } - - tresult PLUGIN_API getPoint (int32 index, int32& sampleOffset, Vst::ParamValue& value) override - { - if (index < 0 || index >= static_cast (points.size())) - return kResultFalse; - - sampleOffset = points[static_cast (index)].first; - value = points[static_cast (index)].second; - return kResultOk; - } - - tresult PLUGIN_API addPoint (int32 sampleOffset, Vst::ParamValue value, int32& index) override - { - index = static_cast (points.size()); - points.emplace_back (sampleOffset, value); - return kResultOk; - } - - DECLARE_FUNKNOWN_METHODS - -private: - Vst::ParamID paramId; - std::vector> points; -}; - //============================================================================== // Minimal IComponentHandler stub — required by IAudioProcessor::initialize(). class HostComponentHandler : public Vst::IComponentHandler @@ -204,6 +163,8 @@ class VST3Instance : public AudioPluginInstance void processBlock (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) override { + ScopedNoDenormals noDenormals; + Vst::ProcessData data; data.processMode = Vst::kRealtime; data.symbolicSampleSize = Vst::kSample32; @@ -226,7 +187,7 @@ class VST3Instance : public AudioPluginInstance // Events (MIDI) // TODO: map MidiBuffer to Vst::EventList when MIDI support is added in a later task - Vst::ParameterChanges inputParameterChanges (static_cast (vst3ParameterIds.size())); + inputParameterChanges.clearQueue(); const auto params = getParameters(); const auto numParams = yup::jmin (params.size(), vst3ParameterIds.size()); @@ -443,6 +404,8 @@ class VST3Instance : public AudioPluginInstance vst3ParameterIds.push_back (info.id); addParameter (std::move (param)); } + + inputParameterChanges.setMaxParameters (static_cast (vst3ParameterIds.size())); } AudioPluginHostContext hostContext; @@ -451,6 +414,7 @@ class VST3Instance : public AudioPluginInstance IPtr vst3Processor; IPtr vst3Controller; Vst::ProcessContext vst3ProcessContext {}; + Vst::ParameterChanges inputParameterChanges; std::vector vst3ParameterIds; int currentPreset = 0; int numPresets = 0; From 0cec8897befc0ac33b2458eab9ddb57b7becd4b4 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sat, 16 May 2026 16:06:08 +0200 Subject: [PATCH 03/35] Fixes to AU --- .../host/yup_AudioPluginScanner.cpp | 11 +- .../native/yup_AudioPluginInstance_AUv2.mm | 759 +++++++++--------- 2 files changed, 393 insertions(+), 377 deletions(-) diff --git a/modules/yup_audio_plugin_host/host/yup_AudioPluginScanner.cpp b/modules/yup_audio_plugin_host/host/yup_AudioPluginScanner.cpp index 118a0d8a4..d9a6efea6 100644 --- a/modules/yup_audio_plugin_host/host/yup_AudioPluginScanner.cpp +++ b/modules/yup_audio_plugin_host/host/yup_AudioPluginScanner.cpp @@ -29,14 +29,23 @@ bool canScanFileWithFormat (AudioPluginFormatType type, const File& file) { switch (type) { +#if YUP_AUDIO_PLUGIN_HOST_ENABLE_VST3 case AudioPluginFormatType::vst3: return file.hasFileExtension (".vst3"); +#endif +#if YUP_AUDIO_PLUGIN_HOST_ENABLE_CLAP case AudioPluginFormatType::clap: return file.hasFileExtension (".clap"); +#endif +#if YUP_AUDIO_PLUGIN_HOST_ENABLE_AU && YUP_MAC case AudioPluginFormatType::audioUnit: return false; +#endif + + default: + break; } return false; @@ -70,7 +79,7 @@ void runFileScanTask (FileScanTask& task) AudioPluginScanner::AudioPluginScanner() : backgroundScanPool (ThreadPool::Options {} .withThreadName ("YUP Plugin Scanner") - .withNumberOfThreads (1)) + .withNumberOfThreads (2)) { } diff --git a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.mm b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.mm index 75f1ceb43..6adfa4d66 100644 --- a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.mm +++ b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.mm @@ -29,98 +29,95 @@ //============================================================================== // Converts a four-char code to a 4-char String (e.g. 0x61756666 -> "auff"). -String fourCCToString (OSType code) +String fourCCToString(OSType code) { char buf[5] = {}; - buf[0] = static_cast ((code >> 24) & 0xFF); - buf[1] = static_cast ((code >> 16) & 0xFF); - buf[2] = static_cast ((code >> 8) & 0xFF); - buf[3] = static_cast (code & 0xFF); - return String (buf); + buf[0] = static_cast((code >> 24) & 0xFF); + buf[1] = static_cast((code >> 16) & 0xFF); + buf[2] = static_cast((code >> 8) & 0xFF); + buf[3] = static_cast(code & 0xFF); + return String(buf); } -String copyCFString (CFStringRef string) +String copyCFString(CFStringRef string) { - return String::fromCFString (string).trim(); + return String::fromCFString(string).trim(); } // Builds a stable identifier string "type/subt/mfgr" from AudioComponentDescription. -String makeIdentifier (const AudioComponentDescription& acd) +String makeIdentifier(const AudioComponentDescription& acd) { - return fourCCToString (acd.componentType) + "/" - + fourCCToString (acd.componentSubType) + "/" - + fourCCToString (acd.componentManufacturer); + return fourCCToString(acd.componentType) + "/" + fourCCToString(acd.componentSubType) + "/" + fourCCToString(acd.componentManufacturer); } -AudioPluginDescription descriptionFromComponent (AudioComponent comp, - const AudioComponentDescription& acd) +AudioPluginDescription descriptionFromComponent(AudioComponent comp, + const AudioComponentDescription& acd) { AudioPluginDescription desc; - desc.formatType = AudioPluginFormatType::audioUnit; - desc.identifier = makeIdentifier (acd); + desc.formatType = AudioPluginFormatType::audioUnit; + desc.identifier = makeIdentifier(acd); CFStringRef nameRef = nullptr; - AudioComponentCopyName (comp, &nameRef); + AudioComponentCopyName(comp, &nameRef); if (nameRef != nullptr) { - desc.name = copyCFString (nameRef); - CFRelease (nameRef); + desc.name = copyCFString(nameRef); + CFRelease(nameRef); } // Collect vendor from the manufacturer four-char code - desc.vendor = fourCCToString (acd.componentManufacturer); + desc.vendor = fourCCToString(acd.componentManufacturer); desc.isInstrument = (acd.componentType == kAudioUnitType_MusicDevice); - desc.isEffect = (acd.componentType == kAudioUnitType_Effect - || acd.componentType == kAudioUnitType_MusicEffect); + desc.isEffect = (acd.componentType == kAudioUnitType_Effect || acd.componentType == kAudioUnitType_MusicEffect); return desc; } -String makeParameterName (const AudioUnitParameterInfo& info, - AudioUnitParameterID paramId) +String makeParameterName(const AudioUnitParameterInfo& info, + AudioUnitParameterID paramId) { String name; if ((info.flags & kAudioUnitParameterFlag_HasCFNameString) != 0) - name = copyCFString (info.cfNameString); + name = copyCFString(info.cfNameString); if (name.isEmpty()) - name = String (info.name).trim(); + name = String(info.name).trim(); - return name.isNotEmpty() ? name : "Parameter " + String (paramId); + return name.isNotEmpty() ? name : "Parameter " + String(paramId); } -void releaseParameterInfoStrings (AudioUnitParameterInfo& info) +void releaseParameterInfoStrings(AudioUnitParameterInfo& info) { if ((info.flags & kAudioUnitParameterFlag_CFNameRelease) == 0) return; if (info.cfNameString != nullptr) { - CFRelease (info.cfNameString); + CFRelease(info.cfNameString); info.cfNameString = nullptr; } if (info.unitName != nullptr) { - CFRelease (info.unitName); + CFRelease(info.unitName); info.unitName = nullptr; } } -NormalisableRange makeParameterRange (const AudioUnitParameterInfo& info) +NormalisableRange makeParameterRange(const AudioUnitParameterInfo& info) { - const auto minValue = static_cast (info.minValue); - const auto maxValue = static_cast (info.maxValue); + const auto minValue = static_cast(info.minValue); + const auto maxValue = static_cast(info.maxValue); if (maxValue > minValue) - return { minValue, maxValue }; + return {minValue, maxValue}; - return { 0.0f, 1.0f }; + return {0.0f, 1.0f}; } -AudioUnitParameter makeAUParameter (AudioUnit audioUnit, AudioUnitParameterID paramId) +AudioUnitParameter makeAUParameter(AudioUnit audioUnit, AudioUnitParameterID paramId) { AudioUnitParameter parameter; parameter.mAudioUnit = audioUnit; @@ -130,13 +127,13 @@ AudioUnitParameter makeAUParameter (AudioUnit audioUnit, AudioUnitParameterID pa return parameter; } -AudioUnitEvent makeAUParameterEvent (AudioUnit audioUnit, - AudioUnitParameterID paramId, - AudioUnitEventType eventType) +AudioUnitEvent makeAUParameterEvent(AudioUnit audioUnit, + AudioUnitParameterID paramId, + AudioUnitEventType eventType) { AudioUnitEvent event; event.mEventType = eventType; - event.mArgument.mParameter = makeAUParameter (audioUnit, paramId); + event.mArgument.mParameter = makeAUParameter(audioUnit, paramId); return event; } @@ -146,30 +143,30 @@ AudioUnitEvent makeAUParameterEvent (AudioUnit audioUnit, class AUv2Editor : public AudioProcessorEditor { -public: - static std::unique_ptr create (AudioUnit audioUnit) + public: + static std::unique_ptr create(AudioUnit audioUnit) { UInt32 size = 0; Boolean writable = false; - if (AudioUnitGetPropertyInfo (audioUnit, - kAudioUnitProperty_CocoaUI, - kAudioUnitScope_Global, 0, - &size, &writable) != noErr) + if (AudioUnitGetPropertyInfo(audioUnit, + kAudioUnitProperty_CocoaUI, + kAudioUnitScope_Global, 0, + &size, &writable) != noErr) { return nullptr; } - if (size < offsetof (AudioUnitCocoaViewInfo, mCocoaAUViewClass) + sizeof (CFStringRef)) + if (size < offsetof(AudioUnitCocoaViewInfo, mCocoaAUViewClass) + sizeof(CFStringRef)) return nullptr; - std::vector storage (size); - auto* viewInfo = reinterpret_cast (storage.data()); + std::vector storage(size); + auto* viewInfo = reinterpret_cast(storage.data()); - if (AudioUnitGetProperty (audioUnit, - kAudioUnitProperty_CocoaUI, - kAudioUnitScope_Global, 0, - viewInfo, &size) != noErr) + if (AudioUnitGetProperty(audioUnit, + kAudioUnitProperty_CocoaUI, + kAudioUnitScope_Global, 0, + viewInfo, &size) != noErr) { return nullptr; } @@ -180,13 +177,13 @@ AudioUnitEvent makeAUParameterEvent (AudioUnit audioUnit, if (viewInfo->mCocoaAUViewBundleLocation != nullptr && viewInfo->mCocoaAUViewClass[0] != nullptr) { - viewBundle = [NSBundle bundleWithURL:(__bridge NSURL*) viewInfo->mCocoaAUViewBundleLocation]; + viewBundle = [NSBundle bundleWithURL:(__bridge NSURL*)viewInfo->mCocoaAUViewBundleLocation]; if (viewBundle != nil && [viewBundle load]) { - Class viewClass = [viewBundle classNamed:(__bridge NSString*) viewInfo->mCocoaAUViewClass[0]]; + Class viewClass = [viewBundle classNamed:(__bridge NSString*)viewInfo->mCocoaAUViewClass[0]]; - if (viewClass != Nil && [viewClass conformsToProtocol:@protocol (AUCocoaUIBase)]) + if (viewClass != Nil && [viewClass conformsToProtocol:@protocol(AUCocoaUIBase)]) { viewFactory = [[viewClass alloc] init]; view = [viewFactory uiViewForAudioUnit:audioUnit withSize:NSZeroSize]; @@ -195,17 +192,17 @@ AudioUnitEvent makeAUParameterEvent (AudioUnit audioUnit, } if (viewInfo->mCocoaAUViewBundleLocation != nullptr) - CFRelease (viewInfo->mCocoaAUViewBundleLocation); + CFRelease(viewInfo->mCocoaAUViewBundleLocation); - const auto numViewClasses = static_cast ((size - offsetof (AudioUnitCocoaViewInfo, mCocoaAUViewClass)) / sizeof (CFStringRef)); + const auto numViewClasses = static_cast((size - offsetof(AudioUnitCocoaViewInfo, mCocoaAUViewClass)) / sizeof(CFStringRef)); for (int i = 0; i < numViewClasses; ++i) if (viewInfo->mCocoaAUViewClass[i] != nullptr) - CFRelease (viewInfo->mCocoaAUViewClass[i]); + CFRelease(viewInfo->mCocoaAUViewClass[i]); if (view == nil) return nullptr; - return std::unique_ptr (new AUv2Editor (view, viewBundle, viewFactory)); + return std::unique_ptr(new AUv2Editor(view, viewBundle, viewFactory)); } ~AUv2Editor() override @@ -217,9 +214,9 @@ AudioUnitEvent makeAUParameterEvent (AudioUnit audioUnit, Size getPreferredSize() const override { return preferredSize; } - void paint (Graphics& g) override + void paint(Graphics& g) override { - g.setFillColor (Color (0xff101417)); + g.setFillColor(Color(0xff101417)); g.fillAll(); } @@ -238,11 +235,9 @@ void detachedFromNative() override detachCocoaView(); } -private: - AUv2Editor (NSView* view, NSBundle* viewBundle, id viewFactory) - : cocoaView (view) - , cocoaViewBundle (viewBundle) - , cocoaViewFactory (viewFactory) + private: + AUv2Editor(NSView* view, NSBundle* viewBundle, id viewFactory) + : cocoaView(view), cocoaViewBundle(viewBundle), cocoaViewFactory(viewFactory) { auto size = [cocoaView frame].size; @@ -250,12 +245,11 @@ void detachedFromNative() override size = [cocoaView fittingSize]; preferredSize = { - jmax (320, static_cast (std::ceil (size.width))), - jmax (240, static_cast (std::ceil (size.height))) - }; + jmax(320, static_cast(std::ceil(size.width))), + jmax(240, static_cast(std::ceil(size.height)))}; [cocoaView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; - [cocoaView setFrameSize:NSMakeSize (preferredSize.getWidth(), preferredSize.getHeight())]; + [cocoaView setFrameSize:NSMakeSize(preferredSize.getWidth(), preferredSize.getHeight())]; } void attachCocoaView() @@ -267,7 +261,7 @@ void attachCocoaView() if (nativeComponent == nullptr) return; - NSWindow* window = (__bridge NSWindow*) nativeComponent->getNativeHandle(); + NSWindow* window = (__bridge NSWindow*)nativeComponent->getNativeHandle(); NSView* contentView = [window contentView]; if (contentView == nil) @@ -294,7 +288,7 @@ void updateCocoaViewFrame() if (nativeComponent == nullptr) return; - NSWindow* window = (__bridge NSWindow*) nativeComponent->getNativeHandle(); + NSWindow* window = (__bridge NSWindow*)nativeComponent->getNativeHandle(); NSView* contentView = [window contentView]; if (contentView == nil) @@ -302,34 +296,32 @@ void updateCocoaViewFrame() const auto bounds = getBoundsRelativeToTopLevelComponent(); const auto contentBounds = [contentView bounds]; - const auto width = jmax (1.0f, bounds.getWidth()); - const auto height = jmax (1.0f, bounds.getHeight()); + const auto width = jmax(1.0f, bounds.getWidth()); + const auto height = jmax(1.0f, bounds.getHeight()); - [cocoaView setFrame:NSMakeRect (static_cast (bounds.getX()), - static_cast (NSHeight (contentBounds) - bounds.getY() - height), - static_cast (width), - static_cast (height))]; + [cocoaView setFrame:NSMakeRect(static_cast(bounds.getX()), + static_cast(NSHeight(contentBounds) - bounds.getY() - height), + static_cast(width), + static_cast(height))]; } - Size preferredSize { 640, 480 }; + Size preferredSize{640, 480}; NSView* __strong cocoaView = nil; NSBundle* __strong cocoaViewBundle = nil; id __strong cocoaViewFactory = nil; - YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AUv2Editor) + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AUv2Editor) }; //============================================================================== -class AUv2Instance : public AudioPluginInstance - , private AudioParameter::Listener +class AUv2Instance : public AudioPluginInstance, private AudioParameter::Listener { -public: - AUv2Instance (const AudioPluginDescription& desc, - AudioUnit unit, - AudioBusLayout busLayout) - : AudioPluginInstance (desc, std::move (busLayout)) - , audioUnit (unit) + public: + AUv2Instance(const AudioPluginDescription& desc, + AudioUnit unit, + AudioBusLayout busLayout) + : AudioPluginInstance(desc, std::move(busLayout)), audioUnit(unit) { buildParameterList(); } @@ -341,50 +333,73 @@ void updateCocoaViewFrame() if (audioUnit != nullptr) { - AudioUnitUninitialize (audioUnit); - AudioComponentInstanceDispose (audioUnit); + AudioUnitUninitialize(audioUnit); + AudioComponentInstanceDispose(audioUnit); audioUnit = nullptr; } } //============================================================================== - void prepareToPlay (float sampleRate, int maxBlockSize) override + void prepareToPlay(float sampleRate, int maxBlockSize) override { releaseResources(); renderSampleTime = 0.0; - const auto numHostedChannels = jmax (2, - pluginDescription.numInputChannels, - pluginDescription.numOutputChannels); - prepareRenderStorage (numHostedChannels, maxBlockSize); + const auto numHostedChannels = jmax(2, + pluginDescription.numInputChannels, + pluginDescription.numOutputChannels); + prepareRenderStorage(numHostedChannels, maxBlockSize); - const Float64 sampleRateValue = static_cast (sampleRate); - AudioUnitSetProperty (audioUnit, - kAudioUnitProperty_SampleRate, - kAudioUnitScope_Global, 0, - &sampleRateValue, sizeof (sampleRateValue)); + const Float64 sampleRateValue = static_cast(sampleRate); + AudioUnitSetProperty(audioUnit, + kAudioUnitProperty_SampleRate, + kAudioUnitScope_Global, 0, + &sampleRateValue, sizeof(sampleRateValue)); - UInt32 blockSize = static_cast (maxBlockSize); - AudioUnitSetProperty (audioUnit, - kAudioUnitProperty_MaximumFramesPerSlice, - kAudioUnitScope_Global, 0, - &blockSize, sizeof (blockSize)); + UInt32 blockSize = static_cast(maxBlockSize); + AudioUnitSetProperty(audioUnit, + kAudioUnitProperty_MaximumFramesPerSlice, + kAudioUnitScope_Global, 0, + &blockSize, sizeof(blockSize)); - configureStreamFormat (sampleRateValue, numHostedChannels); + configureStreamFormat(sampleRateValue, numHostedChannels); installInputCallback(); - AudioUnitInitialize (audioUnit); + if (![NSThread isMainThread]) + { + AudioUnitInitialize(audioUnit); + return; + } + + // AudioUnitInitialize for AUv3 plugins (exposed via AUv2 API) deadlocks + // on the main thread: allocateRenderResources triggers WorkgroupManager + // init which tries to acquire HALB_Mutex while the HAL I/O thread holds + // it waiting on the main run loop. Dispatching to a background thread + // and pumping the run loop breaks the cycle. + AudioUnit capturedUnit = audioUnit; + __block bool initialized = false; + + dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ + AudioUnitInitialize(capturedUnit); + dispatch_async(dispatch_get_main_queue(), ^{ + initialized = true; + }); + }); + + while (!initialized) + [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode + beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.005]]; } void releaseResources() override { if (audioUnit != nullptr) - AudioUnitUninitialize (audioUnit); + AudioUnitUninitialize(audioUnit); } - void processBlock (AudioBuffer& audioBuffer, MidiBuffer&) override + void processBlock(AudioBuffer& audioBuffer, MidiBuffer&) override { ScopedNoDenormals noDenormals; @@ -402,9 +417,9 @@ void processBlock (AudioBuffer& audioBuffer, MidiBuffer&) override for (int c = 0; c < numChannels; ++c) { - std::memcpy (inputBuffer.getWritePointer (c), - audioBuffer.getReadPointer (c), - static_cast (numSamples) * sizeof (float)); + std::memcpy(inputBuffer.getWritePointer(c), + audioBuffer.getReadPointer(c), + static_cast(numSamples) * sizeof(float)); } currentInputBuffer = &inputBuffer; @@ -417,14 +432,14 @@ void processBlock (AudioBuffer& audioBuffer, MidiBuffer&) override timeStamp.mSampleTime = renderSampleTime; renderSampleTime += numSamples; - AudioBufferList* abl = makeOutputABL (audioBuffer); + AudioBufferList* abl = makeOutputABL(audioBuffer); - const auto status = AudioUnitRender (audioUnit, - &flags, - &timeStamp, - 0, // output bus - static_cast (numSamples), - abl); + const auto status = AudioUnitRender(audioUnit, + &flags, + &timeStamp, + 0, // output bus + static_cast(numSamples), + abl); currentInputBuffer = nullptr; currentInputNumChannels = 0; @@ -432,16 +447,16 @@ void processBlock (AudioBuffer& audioBuffer, MidiBuffer&) override if (status != noErr) { - copyInputBackTo (audioBuffer, numChannels, numSamples); + copyInputBackTo(audioBuffer, numChannels, numSamples); return; } // Copy rendered output back to the YUP buffer for (int c = 0; c < numChannels; ++c) { - std::memcpy (audioBuffer.getWritePointer (c), - abl->mBuffers[c].mData, - static_cast (numSamples) * sizeof (float)); + std::memcpy(audioBuffer.getWritePointer(c), + abl->mBuffers[c].mData, + static_cast(numSamples) * sizeof(float)); } } @@ -449,36 +464,36 @@ void processBlock (AudioBuffer& audioBuffer, MidiBuffer&) override int getCurrentPreset() const noexcept override { return currentPreset; } - void setCurrentPreset (int index) noexcept override + void setCurrentPreset(int index) noexcept override { CFArrayRef presets = nullptr; - UInt32 size = sizeof (presets); + UInt32 size = sizeof(presets); - if (AudioUnitGetProperty (audioUnit, - kAudioUnitProperty_FactoryPresets, - kAudioUnitScope_Global, 0, - &presets, &size) != noErr) + if (AudioUnitGetProperty(audioUnit, + kAudioUnitProperty_FactoryPresets, + kAudioUnitScope_Global, 0, + &presets, &size) != noErr) { return; } - if (presets == nullptr || ! isPositiveAndBelow (index, static_cast (CFArrayGetCount (presets)))) + if (presets == nullptr || !isPositiveAndBelow(index, static_cast(CFArrayGetCount(presets)))) { if (presets != nullptr) - CFRelease (presets); + CFRelease(presets); return; } - auto* auPreset = static_cast (CFArrayGetValueAtIndex (presets, index)); + auto* auPreset = static_cast(CFArrayGetValueAtIndex(presets, index)); if (auPreset != nullptr) { AUPreset preset = *auPreset; - if (AudioUnitSetProperty (audioUnit, - kAudioUnitProperty_PresentPreset, - kAudioUnitScope_Global, 0, - &preset, sizeof (preset)) == noErr) + if (AudioUnitSetProperty(audioUnit, + kAudioUnitProperty_PresentPreset, + kAudioUnitScope_Global, 0, + &preset, sizeof(preset)) == noErr) { currentPreset = index; syncParameterValuesFromAudioUnit(); @@ -486,111 +501,112 @@ void setCurrentPreset (int index) noexcept override } } - CFRelease (presets); + CFRelease(presets); } int getNumPresets() const override { CFArrayRef presets = nullptr; - UInt32 size = sizeof (presets); + UInt32 size = sizeof(presets); - AudioUnitGetProperty (audioUnit, - kAudioUnitProperty_FactoryPresets, - kAudioUnitScope_Global, 0, - &presets, &size); + AudioUnitGetProperty(audioUnit, + kAudioUnitProperty_FactoryPresets, + kAudioUnitScope_Global, 0, + &presets, &size); if (presets == nullptr) return 0; - const int count = static_cast (CFArrayGetCount (presets)); - CFRelease (presets); + const int count = static_cast(CFArrayGetCount(presets)); + CFRelease(presets); return count; } - String getPresetName (int index) const override + String getPresetName(int index) const override { CFArrayRef presets = nullptr; - UInt32 size = sizeof (presets); + UInt32 size = sizeof(presets); - AudioUnitGetProperty (audioUnit, - kAudioUnitProperty_FactoryPresets, - kAudioUnitScope_Global, 0, - &presets, &size); + AudioUnitGetProperty(audioUnit, + kAudioUnitProperty_FactoryPresets, + kAudioUnitScope_Global, 0, + &presets, &size); - if (presets == nullptr || ! isPositiveAndBelow (index, static_cast (CFArrayGetCount (presets)))) + if (presets == nullptr || !isPositiveAndBelow(index, static_cast(CFArrayGetCount(presets)))) { - if (presets != nullptr) CFRelease (presets); + if (presets != nullptr) + CFRelease(presets); return {}; } - auto* auPreset = static_cast (CFArrayGetValueAtIndex (presets, index)); + auto* auPreset = static_cast(CFArrayGetValueAtIndex(presets, index)); const String name = auPreset->presetName != nullptr - ? String::fromCFString (auPreset->presetName) - : String(); - CFRelease (presets); + ? String::fromCFString(auPreset->presetName) + : String(); + CFRelease(presets); return name; } - void setPresetName (int, StringRef) override {} + void setPresetName(int, StringRef) override {} //============================================================================== - Result loadStateFromMemory (const MemoryBlock& memoryBlock) override + Result loadStateFromMemory(const MemoryBlock& memoryBlock) override { - CFDataRef data = CFDataCreateWithBytesNoCopy (kCFAllocatorDefault, - static_cast (memoryBlock.getData()), - static_cast (memoryBlock.getSize()), - kCFAllocatorNull); + CFDataRef data = CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, + static_cast(memoryBlock.getData()), + static_cast(memoryBlock.getSize()), + kCFAllocatorNull); if (data == nullptr) - return Result::fail ("Failed to create CFData"); + return Result::fail("Failed to create CFData"); - CFPropertyListRef plist = CFPropertyListCreateWithData (kCFAllocatorDefault, - data, - kCFPropertyListImmutable, - nullptr, nullptr); - CFRelease (data); + CFPropertyListRef plist = CFPropertyListCreateWithData(kCFAllocatorDefault, + data, + kCFPropertyListImmutable, + nullptr, nullptr); + CFRelease(data); if (plist == nullptr) - return Result::fail ("Failed to deserialize AU state"); + return Result::fail("Failed to deserialize AU state"); - const OSStatus status = AudioUnitSetProperty (audioUnit, - kAudioUnitProperty_ClassInfo, - kAudioUnitScope_Global, 0, - &plist, sizeof (plist)); - CFRelease (plist); + const OSStatus status = AudioUnitSetProperty(audioUnit, + kAudioUnitProperty_ClassInfo, + kAudioUnitScope_Global, 0, + &plist, sizeof(plist)); + CFRelease(plist); if (status != noErr) - return Result::fail ("AudioUnitSetProperty ClassInfo failed: " + String (status)); + return Result::fail("AudioUnitSetProperty ClassInfo failed: " + String(status)); syncParameterValuesFromAudioUnit(); notifyAudioUnitParametersChanged(); return Result::ok(); } - Result saveStateIntoMemory (MemoryBlock& memoryBlock) override + Result saveStateIntoMemory(MemoryBlock& memoryBlock) override { CFPropertyListRef plist = nullptr; - UInt32 size = sizeof (plist); + UInt32 size = sizeof(plist); - const OSStatus status = AudioUnitGetProperty (audioUnit, - kAudioUnitProperty_ClassInfo, - kAudioUnitScope_Global, 0, - &plist, &size); + const OSStatus status = AudioUnitGetProperty(audioUnit, + kAudioUnitProperty_ClassInfo, + kAudioUnitScope_Global, 0, + &plist, &size); if (status != noErr || plist == nullptr) - return Result::fail ("AudioUnitGetProperty ClassInfo failed: " + String (status)); + return Result::fail("AudioUnitGetProperty ClassInfo failed: " + String(status)); - CFDataRef data = CFPropertyListCreateData (kCFAllocatorDefault, - plist, - kCFPropertyListBinaryFormat_v1_0, - 0, nullptr); - CFRelease (plist); + CFDataRef data = CFPropertyListCreateData(kCFAllocatorDefault, + plist, + kCFPropertyListBinaryFormat_v1_0, + 0, nullptr); + CFRelease(plist); if (data == nullptr) - return Result::fail ("Failed to serialize AU state"); + return Result::fail("Failed to serialize AU state"); - memoryBlock.replaceAll (CFDataGetBytePtr (data), - static_cast (CFDataGetLength (data))); - CFRelease (data); + memoryBlock.replaceAll(CFDataGetBytePtr(data), + static_cast(CFDataGetLength(data))); + CFRelease(data); return Result::ok(); } @@ -601,16 +617,16 @@ bool hasEditor() const override UInt32 size = 0; Boolean writable = false; - return AudioUnitGetPropertyInfo (audioUnit, - kAudioUnitProperty_CocoaUI, - kAudioUnitScope_Global, 0, - &size, &writable) == noErr - && size >= offsetof (AudioUnitCocoaViewInfo, mCocoaAUViewClass) + sizeof (CFStringRef); + return AudioUnitGetPropertyInfo(audioUnit, + kAudioUnitProperty_CocoaUI, + kAudioUnitScope_Global, 0, + &size, &writable) == noErr && + size >= offsetof(AudioUnitCocoaViewInfo, mCocoaAUViewClass) + sizeof(CFStringRef); } AudioProcessorEditor* createEditor() override { - if (auto editor = AUv2Editor::create (audioUnit)) + if (auto editor = AUv2Editor::create(audioUnit)) return editor.release(); return nullptr; @@ -618,124 +634,120 @@ bool hasEditor() const override //============================================================================== - static std::unique_ptr create (const AudioPluginDescription& desc, - const AudioPluginHostContext&) + static std::unique_ptr create(const AudioPluginDescription& desc, + const AudioPluginHostContext&) { // Parse "type/subt/mfgr" identifier - const auto tokens = StringArray::fromTokens (desc.identifier, "/", ""); + const auto tokens = StringArray::fromTokens(desc.identifier, "/", ""); if (tokens.size() != 3) return nullptr; - auto fourCC = [] (const String& s) -> OSType + auto fourCC = [](const String& s) -> OSType { - if (s.length() != 4) return 0; - return static_cast ( - (static_cast (s[0]) << 24) - | (static_cast (s[1]) << 16) - | (static_cast (s[2]) << 8) - | static_cast (s[3])); + if (s.length() != 4) + return 0; + return static_cast( + (static_cast(s[0]) << 24) | (static_cast(s[1]) << 16) | (static_cast(s[2]) << 8) | static_cast(s[3])); }; AudioComponentDescription acd{}; - acd.componentType = fourCC (tokens[0]); - acd.componentSubType = fourCC (tokens[1]); - acd.componentManufacturer = fourCC (tokens[2]); + acd.componentType = fourCC(tokens[0]); + acd.componentSubType = fourCC(tokens[1]); + acd.componentManufacturer = fourCC(tokens[2]); - AudioComponent comp = AudioComponentFindNext (nullptr, &acd); + AudioComponent comp = AudioComponentFindNext(nullptr, &acd); if (comp == nullptr) return nullptr; AudioUnit unit = nullptr; - if (AudioComponentInstanceNew (comp, &unit) != noErr || unit == nullptr) + if (AudioComponentInstanceNew(comp, &unit) != noErr || unit == nullptr) return nullptr; - AudioBusLayout busLayout ({}, {}); + AudioBusLayout busLayout({}, {}); // TODO: query kAudioUnitProperty_ElementCount for proper bus layout - return std::make_unique (desc, unit, std::move (busLayout)); + return std::make_unique(desc, unit, std::move(busLayout)); } -private: - void parameterValueChanged (const AudioParameter::Ptr& parameter, int indexInContainer) override + private: + void parameterValueChanged(const AudioParameter::Ptr& parameter, int indexInContainer) override { - if (audioUnit == nullptr || ! isPositiveAndBelow (indexInContainer, static_cast (auParameterIds.size()))) + if (audioUnit == nullptr || !isPositiveAndBelow(indexInContainer, static_cast(auParameterIds.size()))) return; - const auto auParameter = makeAUParameter (audioUnit, auParameterIds[static_cast (indexInContainer)]); + const auto auParameter = makeAUParameter(audioUnit, auParameterIds[static_cast(indexInContainer)]); - AUParameterSet (eventListener, - this, - &auParameter, - static_cast (parameter->getValue()), - 0); + AUParameterSet(eventListener, + this, + &auParameter, + static_cast(parameter->getValue()), + 0); } - void parameterGestureBegin (const AudioParameter::Ptr&, int indexInContainer) override + void parameterGestureBegin(const AudioParameter::Ptr&, int indexInContainer) override { - notifyParameterGesture (indexInContainer, kAudioUnitEvent_BeginParameterChangeGesture); + notifyParameterGesture(indexInContainer, kAudioUnitEvent_BeginParameterChangeGesture); } - void parameterGestureEnd (const AudioParameter::Ptr&, int indexInContainer) override + void parameterGestureEnd(const AudioParameter::Ptr&, int indexInContainer) override { - notifyParameterGesture (indexInContainer, kAudioUnitEvent_EndParameterChangeGesture); + notifyParameterGesture(indexInContainer, kAudioUnitEvent_EndParameterChangeGesture); } - void notifyParameterGesture (int indexInContainer, AudioUnitEventType eventType) + void notifyParameterGesture(int indexInContainer, AudioUnitEventType eventType) { - if (audioUnit == nullptr || ! isPositiveAndBelow (indexInContainer, static_cast (auParameterIds.size()))) + if (audioUnit == nullptr || !isPositiveAndBelow(indexInContainer, static_cast(auParameterIds.size()))) return; - const auto event = makeAUParameterEvent (audioUnit, - auParameterIds[static_cast (indexInContainer)], - eventType); + const auto event = makeAUParameterEvent(audioUnit, + auParameterIds[static_cast(indexInContainer)], + eventType); - AUEventListenerNotify (eventListener, this, &event); + AUEventListenerNotify(eventListener, this, &event); } - static void parameterEventCallback (void* userData, - void*, - const AudioUnitEvent* event, - UInt64, - AudioUnitParameterValue parameterValue) + static void parameterEventCallback(void* userData, + void*, + const AudioUnitEvent* event, + UInt64, + AudioUnitParameterValue parameterValue) { - auto* instance = static_cast (userData); + auto* instance = static_cast(userData); if (instance == nullptr || event == nullptr || event->mEventType != kAudioUnitEvent_ParameterValueChange) return; - instance->handleAudioUnitParameterChanged (event->mArgument.mParameter, parameterValue); + instance->handleAudioUnitParameterChanged(event->mArgument.mParameter, parameterValue); } - void handleAudioUnitParameterChanged (const AudioUnitParameter& parameter, - AudioUnitParameterValue parameterValue) + void handleAudioUnitParameterChanged(const AudioUnitParameter& parameter, + AudioUnitParameterValue parameterValue) { - if (parameter.mAudioUnit != audioUnit - || parameter.mScope != kAudioUnitScope_Global - || parameter.mElement != 0) + if (parameter.mAudioUnit != audioUnit || parameter.mScope != kAudioUnitScope_Global || parameter.mElement != 0) { return; } const auto params = getParameters(); - const auto numParams = jmin (params.size(), auParameterIds.size()); + const auto numParams = jmin(params.size(), auParameterIds.size()); for (std::size_t i = 0; i < numParams; ++i) { if (auParameterIds[i] == parameter.mParameterID) { - params[i]->setValue (static_cast (parameterValue)); + params[i]->setValue(static_cast(parameterValue)); return; } } } - static OSStatus inputRenderCallback (void* refCon, - AudioUnitRenderActionFlags*, - const AudioTimeStamp*, - UInt32, - UInt32 numFrames, - AudioBufferList* ioData) + static OSStatus inputRenderCallback(void* refCon, + AudioUnitRenderActionFlags*, + const AudioTimeStamp*, + UInt32, + UInt32 numFrames, + AudioBufferList* ioData) { - auto* instance = static_cast (refCon); + auto* instance = static_cast(refCon); if (ioData == nullptr) return noErr; @@ -744,87 +756,84 @@ static OSStatus inputRenderCallback (void* refCon, for (UInt32 bufferIndex = 0; bufferIndex < ioData->mNumberBuffers; ++bufferIndex) { auto& buffer = ioData->mBuffers[bufferIndex]; - auto* dest = static_cast (buffer.mData); + auto* dest = static_cast(buffer.mData); if (dest != nullptr) - FloatVectorOperations::clear (dest, static_cast (numFrames)); + FloatVectorOperations::clear(dest, static_cast(numFrames)); - buffer.mDataByteSize = static_cast (numFrames * sizeof (float)); + buffer.mDataByteSize = static_cast(numFrames * sizeof(float)); } return noErr; } - const int framesToCopy = jmin (static_cast (numFrames), - instance->currentInputNumSamples); + const int framesToCopy = jmin(static_cast(numFrames), + instance->currentInputNumSamples); for (UInt32 bufferIndex = 0; bufferIndex < ioData->mNumberBuffers; ++bufferIndex) { auto& buffer = ioData->mBuffers[bufferIndex]; - auto* dest = static_cast (buffer.mData); + auto* dest = static_cast(buffer.mData); if (dest == nullptr) continue; - if (static_cast (bufferIndex) < instance->currentInputNumChannels) + if (static_cast(bufferIndex) < instance->currentInputNumChannels) { - FloatVectorOperations::copy (dest, - instance->currentInputBuffer->getReadPointer (static_cast (bufferIndex)), - framesToCopy); + FloatVectorOperations::copy(dest, + instance->currentInputBuffer->getReadPointer(static_cast(bufferIndex)), + framesToCopy); } else { - FloatVectorOperations::clear (dest, framesToCopy); + FloatVectorOperations::clear(dest, framesToCopy); } - if (framesToCopy < static_cast (numFrames)) - FloatVectorOperations::clear (dest + framesToCopy, - static_cast (numFrames) - framesToCopy); + if (framesToCopy < static_cast(numFrames)) + FloatVectorOperations::clear(dest + framesToCopy, + static_cast(numFrames) - framesToCopy); - buffer.mDataByteSize = static_cast (numFrames * sizeof (float)); + buffer.mDataByteSize = static_cast(numFrames * sizeof(float)); } return noErr; } - void prepareRenderStorage (int numChannels, int maxBlockSize) + void prepareRenderStorage(int numChannels, int maxBlockSize) { - preparedNumChannels = jmax (1, numChannels); - preparedMaxBlockSize = jmax (1, maxBlockSize); + preparedNumChannels = jmax(1, numChannels); + preparedMaxBlockSize = jmax(1, maxBlockSize); - inputBuffer.setSize (preparedNumChannels, - preparedMaxBlockSize, - false, - false, - true); + inputBuffer.setSize(preparedNumChannels, + preparedMaxBlockSize, + false, + false, + true); - outputABLStorage.resize (getAudioBufferListSize (preparedNumChannels)); + outputABLStorage.resize(getAudioBufferListSize(preparedNumChannels)); } - void configureStreamFormat (Float64 sampleRate, int numChannels) + void configureStreamFormat(Float64 sampleRate, int numChannels) { AudioStreamBasicDescription streamFormat{}; streamFormat.mSampleRate = sampleRate; streamFormat.mFormatID = kAudioFormatLinearPCM; - streamFormat.mFormatFlags = kAudioFormatFlagIsFloat - | kAudioFormatFlagIsPacked - | kAudioFormatFlagIsNonInterleaved - | kAudioFormatFlagsNativeEndian; - streamFormat.mBytesPerPacket = sizeof (float); + streamFormat.mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked | kAudioFormatFlagIsNonInterleaved | kAudioFormatFlagsNativeEndian; + streamFormat.mBytesPerPacket = sizeof(float); streamFormat.mFramesPerPacket = 1; - streamFormat.mBytesPerFrame = sizeof (float); - streamFormat.mChannelsPerFrame = static_cast (numChannels); - streamFormat.mBitsPerChannel = static_cast (sizeof (float) * 8); - - AudioUnitSetProperty (audioUnit, - kAudioUnitProperty_StreamFormat, - kAudioUnitScope_Input, 0, - &streamFormat, sizeof (streamFormat)); - - AudioUnitSetProperty (audioUnit, - kAudioUnitProperty_StreamFormat, - kAudioUnitScope_Output, 0, - &streamFormat, sizeof (streamFormat)); + streamFormat.mBytesPerFrame = sizeof(float); + streamFormat.mChannelsPerFrame = static_cast(numChannels); + streamFormat.mBitsPerChannel = static_cast(sizeof(float) * 8); + + AudioUnitSetProperty(audioUnit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Input, 0, + &streamFormat, sizeof(streamFormat)); + + AudioUnitSetProperty(audioUnit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Output, 0, + &streamFormat, sizeof(streamFormat)); } void installInputCallback() @@ -833,64 +842,63 @@ void installInputCallback() callback.inputProc = inputRenderCallback; callback.inputProcRefCon = this; - AudioUnitSetProperty (audioUnit, - kAudioUnitProperty_SetRenderCallback, - kAudioUnitScope_Input, 0, - &callback, sizeof (callback)); + AudioUnitSetProperty(audioUnit, + kAudioUnitProperty_SetRenderCallback, + kAudioUnitScope_Input, 0, + &callback, sizeof(callback)); } - static std::size_t getAudioBufferListSize (int numChannels) + static std::size_t getAudioBufferListSize(int numChannels) { - return sizeof (AudioBufferList) - + static_cast (jmax (0, numChannels - 1)) * sizeof (::AudioBuffer); + return sizeof(AudioBufferList) + static_cast(jmax(0, numChannels - 1)) * sizeof(::AudioBuffer); } - AudioBufferList* makeOutputABL (AudioBuffer& buffer) + AudioBufferList* makeOutputABL(AudioBuffer& buffer) { const int numChannels = buffer.getNumChannels(); - auto* abl = reinterpret_cast (outputABLStorage.data()); - abl->mNumberBuffers = static_cast (numChannels); + auto* abl = reinterpret_cast(outputABLStorage.data()); + abl->mNumberBuffers = static_cast(numChannels); for (int c = 0; c < numChannels; ++c) { abl->mBuffers[c].mNumberChannels = 1; - abl->mBuffers[c].mDataByteSize = static_cast (buffer.getNumSamples()) * sizeof (float); - abl->mBuffers[c].mData = buffer.getWritePointer (c); + abl->mBuffers[c].mDataByteSize = static_cast(buffer.getNumSamples()) * sizeof(float); + abl->mBuffers[c].mData = buffer.getWritePointer(c); } return abl; } - void copyInputBackTo (AudioBuffer& audioBuffer, int numChannels, int numSamples) + void copyInputBackTo(AudioBuffer& audioBuffer, int numChannels, int numSamples) { for (int c = 0; c < numChannels; ++c) { - FloatVectorOperations::copy (audioBuffer.getWritePointer (c), - inputBuffer.getReadPointer (c), - numSamples); + FloatVectorOperations::copy(audioBuffer.getWritePointer(c), + inputBuffer.getReadPointer(c), + numSamples); } } void buildParameterList() { UInt32 size = 0; - if (AudioUnitGetPropertyInfo (audioUnit, - kAudioUnitProperty_ParameterList, - kAudioUnitScope_Global, 0, - &size, nullptr) != noErr) + if (AudioUnitGetPropertyInfo(audioUnit, + kAudioUnitProperty_ParameterList, + kAudioUnitScope_Global, 0, + &size, nullptr) != noErr) { return; } - const int count = static_cast (size / sizeof (AudioUnitParameterID)); + const int count = static_cast(size / sizeof(AudioUnitParameterID)); if (count == 0) return; - std::vector paramIds (static_cast (count)); - if (AudioUnitGetProperty (audioUnit, - kAudioUnitProperty_ParameterList, - kAudioUnitScope_Global, 0, - paramIds.data(), &size) != noErr) + std::vector paramIds(static_cast(count)); + if (AudioUnitGetProperty(audioUnit, + kAudioUnitProperty_ParameterList, + kAudioUnitScope_Global, 0, + paramIds.data(), &size) != noErr) { return; } @@ -900,33 +908,33 @@ void buildParameterList() for (auto paramId : paramIds) { AudioUnitParameterInfo info{}; - UInt32 infoSize = sizeof (info); - if (AudioUnitGetProperty (audioUnit, - kAudioUnitProperty_ParameterInfo, - kAudioUnitScope_Global, paramId, - &info, &infoSize) != noErr) + UInt32 infoSize = sizeof(info); + if (AudioUnitGetProperty(audioUnit, + kAudioUnitProperty_ParameterInfo, + kAudioUnitScope_Global, paramId, + &info, &infoSize) != noErr) { continue; } - const auto name = makeParameterName (info, paramId); - const auto range = makeParameterRange (info); - releaseParameterInfoStrings (info); + const auto name = makeParameterName(info, paramId); + const auto range = makeParameterRange(info); + releaseParameterInfoStrings(info); auto param = AudioParameterBuilder() - .withID (name + "_" + String (paramId)) - .withName (name) - .withRange (range) - .withDefault (info.defaultValue) - .withSmoothing (0.0f) - .build(); + .withID(name + "_" + String(paramId)) + .withName(name) + .withRange(range) + .withDefault(info.defaultValue) + .withSmoothing(0.0f) + .build(); - param->addListener (this); + param->addListener(this); - auParameterIds.push_back (paramId); - addParameter (std::move (param)); + auParameterIds.push_back(paramId); + addParameter(std::move(param)); - addAudioUnitParameterListener (paramId); + addAudioUnitParameterListener(paramId); } syncParameterValuesFromAudioUnit(); @@ -937,22 +945,22 @@ void installParameterEventListener() if (eventListener != nullptr) return; - AUEventListenerCreate (parameterEventCallback, - this, - CFRunLoopGetMain(), - kCFRunLoopCommonModes, - 0.02f, - 0.01f, - &eventListener); + AUEventListenerCreate(parameterEventCallback, + this, + CFRunLoopGetMain(), + kCFRunLoopCommonModes, + 0.02f, + 0.01f, + &eventListener); } - void addAudioUnitParameterListener (AudioUnitParameterID paramId) + void addAudioUnitParameterListener(AudioUnitParameterID paramId) { if (eventListener == nullptr) return; - const auto event = makeAUParameterEvent (audioUnit, paramId, kAudioUnitEvent_ParameterValueChange); - AUEventListenerAddEventType (eventListener, this, &event); + const auto event = makeAUParameterEvent(audioUnit, paramId, kAudioUnitEvent_ParameterValueChange); + AUEventListenerAddEventType(eventListener, this, &event); } void notifyAudioUnitParametersChanged() @@ -962,26 +970,26 @@ void notifyAudioUnitParametersChanged() for (auto paramId : auParameterIds) { - const auto parameter = makeAUParameter (audioUnit, paramId); - AUParameterListenerNotify (eventListener, this, ¶meter); + const auto parameter = makeAUParameter(audioUnit, paramId); + AUParameterListenerNotify(eventListener, this, ¶meter); } } void syncParameterValuesFromAudioUnit() noexcept { const auto params = getParameters(); - const auto numParams = jmin (params.size(), auParameterIds.size()); + const auto numParams = jmin(params.size(), auParameterIds.size()); for (std::size_t i = 0; i < numParams; ++i) { AudioUnitParameterValue value = 0.0f; - if (AudioUnitGetParameter (audioUnit, - auParameterIds[i], - kAudioUnitScope_Global, - 0, - &value) == noErr) + if (AudioUnitGetParameter(audioUnit, + auParameterIds[i], + kAudioUnitScope_Global, + 0, + &value) == noErr) { - params[i]->setValue (static_cast (value)); + params[i]->setValue(static_cast(value)); } } } @@ -989,11 +997,11 @@ void syncParameterValuesFromAudioUnit() noexcept void removeParameterListeners() { for (auto& param : getParameters()) - param->removeListener (this); + param->removeListener(this); if (eventListener != nullptr) { - AUListenerDispose (eventListener); + AUListenerDispose(eventListener); eventListener = nullptr; } } @@ -1032,7 +1040,7 @@ void removeParameterListeners() return {}; } -ResultValue> AUv2Format::scanFile (const File&) +ResultValue> AUv2Format::scanFile(const File&) { // Scan by enumerating the AudioComponent registry (ignores file argument) std::vector results; @@ -1042,8 +1050,7 @@ void removeParameterListeners() kAudioUnitType_Effect, kAudioUnitType_MusicEffect, kAudioUnitType_Generator, - kAudioUnitType_MIDIProcessor - }; + kAudioUnitType_MIDIProcessor}; for (OSType type : types) { @@ -1051,33 +1058,33 @@ void removeParameterListeners() search.componentType = type; AudioComponent comp = nullptr; - while ((comp = AudioComponentFindNext (comp, &search)) != nullptr) + while ((comp = AudioComponentFindNext(comp, &search)) != nullptr) { AudioComponentDescription acd{}; - AudioComponentGetDescription (comp, &acd); - auto desc = descriptionFromComponent (comp, acd); - results.push_back (std::move (desc)); + AudioComponentGetDescription(comp, &acd); + auto desc = descriptionFromComponent(comp, acd); + results.push_back(std::move(desc)); } } if (results.empty()) - return ResultValue>::fail ( + return ResultValue>::fail( "No AudioComponents found in registry"); - return ResultValue>::ok (std::move (results)); + return ResultValue>::ok(std::move(results)); } -ResultValue> AUv2Format::loadPlugin ( +ResultValue> AUv2Format::loadPlugin( const AudioPluginDescription& description, const AudioPluginHostContext& context) { - auto instance = AUv2Instance::create (description, context); + auto instance = AUv2Instance::create(description, context); if (instance == nullptr) - return ResultValue>::fail ( + return ResultValue>::fail( "Failed to instantiate AUv2 plugin: " + description.name); - return ResultValue>::ok (std::move (instance)); + return ResultValue>::ok(std::move(instance)); } } // namespace yup From 36137dda2e44b397bcbdfbd9c548d4f1d195b504 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sat, 16 May 2026 16:06:18 +0200 Subject: [PATCH 04/35] Listbox fixes --- modules/yup_gui/widgets/yup_ListBox.cpp | 84 +++++++++++++++++++++---- 1 file changed, 73 insertions(+), 11 deletions(-) diff --git a/modules/yup_gui/widgets/yup_ListBox.cpp b/modules/yup_gui/widgets/yup_ListBox.cpp index 9ccfb8656..9e285b490 100644 --- a/modules/yup_gui/widgets/yup_ListBox.cpp +++ b/modules/yup_gui/widgets/yup_ListBox.cpp @@ -420,6 +420,8 @@ void ListBox::updateContent() // Update scrolling calculations updateScrolling(); + updateScrollBars(); + updateScrolling(); // Layout visible rows layoutRows(); @@ -525,7 +527,13 @@ bool ListBox::isVariableWidthEnabled() const noexcept //============================================================================== void ListBox::setMinimumContentSize (int minSize) { - minimumContentSize = jmax (0, minSize); + auto newMinimumContentSize = jmax (0, minSize); + + if (minimumContentSize != newMinimumContentSize) + { + minimumContentSize = newMinimumContentSize; + updateContent(); + } } int ListBox::getMinimumContentSize() const noexcept @@ -563,11 +571,18 @@ Rectangle ListBox::getRowBounds (int rowIndex) const auto position = getRowPosition (rowIndex); auto size = getRowSize (rowIndex); + auto contentArea = getContentArea(); if (orientation == Orientation::vertical) - return Rectangle (0.0f, position - scrollOffset, getWidth(), size); + return Rectangle (contentArea.getX(), + contentArea.getY() + position - scrollOffset, + contentArea.getWidth(), + size); else - return Rectangle (position - scrollOffset, 0.0f, size, getHeight()); + return Rectangle (contentArea.getX() + position - scrollOffset, + contentArea.getY(), + size, + contentArea.getHeight()); } //============================================================================== @@ -637,8 +652,6 @@ void ListBox::mouseMove (const MouseEvent& event) void ListBox::mouseWheel (const MouseEvent& event, const MouseWheelData& wheelData) { - ignoreUnused (event); - if (needsScrolling()) { auto delta = (orientation == Orientation::vertical) @@ -648,6 +661,7 @@ void ListBox::mouseWheel (const MouseEvent& event, const MouseWheelData& wheelDa // Scroll by approximately 3 rows worth of content auto scrollAmount = delta * fixedRowHeight * 3.0f; scrollBy (-scrollAmount); + updateHoveredRow (event.getPosition()); } } @@ -837,6 +851,12 @@ void ListBox::layoutRows() visibleRowRange = Range (firstVisible >= 0 ? firstVisible : 0, lastVisible >= 0 ? lastVisible + 1 : 0); + for (auto& pair : rowComponents) + { + if (! visibleRowRange.contains (pair.first)) + pair.second->setVisible (false); + } + // Create or update visible rows for (int i = visibleRowRange.getStart(); i < visibleRowRange.getEnd(); ++i) { @@ -863,6 +883,12 @@ void ListBox::layoutRows() row->setVisible (true); } } + + if (verticalScrollBar != nullptr) + verticalScrollBar->toFront (false); + + if (horizontalScrollBar != nullptr) + horizontalScrollBar->toFront (false); } void ListBox::createOrUpdateRow (int rowIndex) @@ -918,13 +944,19 @@ void ListBox::removeUnusedRows() //============================================================================== void ListBox::scrollBy (float delta) { + auto oldScrollOffset = scrollOffset; + scrollOffset += delta; // Clamp scroll offset auto maxScroll = jmax (0.0f, totalContentSize - viewportSize); scrollOffset = jlimit (0.0f, maxScroll, scrollOffset); + if (oldScrollOffset == scrollOffset) + return; + layoutRows(); + updateScrollBars(); repaint(); } @@ -955,6 +987,7 @@ void ListBox::scrollToRow (int rowIndex) scrollOffset = jlimit (0.0f, maxScroll, scrollOffset); layoutRows(); + updateScrollBars(); repaint(); } @@ -1076,13 +1109,17 @@ int ListBox::getRowIndexAt (Point position) const if (model == nullptr) return -1; + auto contentArea = getContentArea(); + if (! contentArea.contains (position)) + return -1; + auto numRows = model->getNumRows(); if (numRows == 0) return -1; float searchPosition = (orientation == Orientation::vertical) - ? position.getY() + scrollOffset - : position.getX() + scrollOffset; + ? position.getY() - contentArea.getY() + scrollOffset + : position.getX() - contentArea.getX() + scrollOffset; float currentPosition = 0.0f; @@ -1186,7 +1223,26 @@ ScrollBar* ListBox::getHorizontalScrollBar() const noexcept void ListBox::updateScrollBars() { if (model == nullptr) + { + if (verticalScrollBar != nullptr) + verticalScrollBar->setVisible (false); + + if (horizontalScrollBar != nullptr) + horizontalScrollBar->setVisible (false); + return; + } + + if (orientation == Orientation::vertical) + { + if (horizontalScrollBar != nullptr) + horizontalScrollBar->setVisible (false); + } + else + { + if (verticalScrollBar != nullptr) + verticalScrollBar->setVisible (false); + } auto contentArea = getContentArea(); auto totalContent = getTotalContentSize(); @@ -1194,14 +1250,20 @@ void ListBox::updateScrollBars() if (orientation == Orientation::vertical) { // Update vertical scrollbar - verticalScrollBar->setRangeLimits (0.0, totalContent); - verticalScrollBar->setCurrentRange (scrollOffset, scrollOffset + contentArea.getHeight()); + if (verticalScrollBar != nullptr) + { + verticalScrollBar->setRangeLimits (0.0, totalContent); + verticalScrollBar->setCurrentRange (scrollOffset, scrollOffset + contentArea.getHeight()); + } } else { // Update horizontal scrollbar - horizontalScrollBar->setRangeLimits (0.0, totalContent); - horizontalScrollBar->setCurrentRange (scrollOffset, scrollOffset + contentArea.getWidth()); + if (horizontalScrollBar != nullptr) + { + horizontalScrollBar->setRangeLimits (0.0, totalContent); + horizontalScrollBar->setCurrentRange (scrollOffset, scrollOffset + contentArea.getWidth()); + } } } From aa302921e7700f3976123547faa64b919fcd0357 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sat, 16 May 2026 16:46:48 +0200 Subject: [PATCH 05/35] More plugin host fixes --- .../yup_audio_plugin_host/host/yup_AudioPluginScanner.cpp | 6 ------ tests/yup_audio_plugin_host.cpp | 8 ++++---- ...uginDescription.cpp => yup_AudioPluginDescription.cpp} | 0 ...udioPluginInstance.cpp => yup_AudioPluginInstance.cpp} | 0 ...{AudioPluginScanner.cpp => yup_AudioPluginScanner.cpp} | 2 +- .../{AudioPluginState.cpp => yup_AudioPluginState.cpp} | 0 6 files changed, 5 insertions(+), 11 deletions(-) rename tests/yup_audio_plugin_host/{AudioPluginDescription.cpp => yup_AudioPluginDescription.cpp} (100%) rename tests/yup_audio_plugin_host/{AudioPluginInstance.cpp => yup_AudioPluginInstance.cpp} (100%) rename tests/yup_audio_plugin_host/{AudioPluginScanner.cpp => yup_AudioPluginScanner.cpp} (96%) rename tests/yup_audio_plugin_host/{AudioPluginState.cpp => yup_AudioPluginState.cpp} (100%) diff --git a/modules/yup_audio_plugin_host/host/yup_AudioPluginScanner.cpp b/modules/yup_audio_plugin_host/host/yup_AudioPluginScanner.cpp index d9a6efea6..37de1995e 100644 --- a/modules/yup_audio_plugin_host/host/yup_AudioPluginScanner.cpp +++ b/modules/yup_audio_plugin_host/host/yup_AudioPluginScanner.cpp @@ -29,20 +29,14 @@ bool canScanFileWithFormat (AudioPluginFormatType type, const File& file) { switch (type) { -#if YUP_AUDIO_PLUGIN_HOST_ENABLE_VST3 case AudioPluginFormatType::vst3: return file.hasFileExtension (".vst3"); -#endif -#if YUP_AUDIO_PLUGIN_HOST_ENABLE_CLAP case AudioPluginFormatType::clap: return file.hasFileExtension (".clap"); -#endif -#if YUP_AUDIO_PLUGIN_HOST_ENABLE_AU && YUP_MAC case AudioPluginFormatType::audioUnit: return false; -#endif default: break; diff --git a/tests/yup_audio_plugin_host.cpp b/tests/yup_audio_plugin_host.cpp index da18dcb84..9f3cd1d60 100644 --- a/tests/yup_audio_plugin_host.cpp +++ b/tests/yup_audio_plugin_host.cpp @@ -19,7 +19,7 @@ ============================================================================== */ -#include "yup_audio_plugin_host/AudioPluginDescription.cpp" -#include "yup_audio_plugin_host/AudioPluginScanner.cpp" -#include "yup_audio_plugin_host/AudioPluginInstance.cpp" -#include "yup_audio_plugin_host/AudioPluginState.cpp" +#include "yup_audio_plugin_host/yup_AudioPluginDescription.cpp" +#include "yup_audio_plugin_host/yup_AudioPluginScanner.cpp" +#include "yup_audio_plugin_host/yup_AudioPluginInstance.cpp" +#include "yup_audio_plugin_host/yup_AudioPluginState.cpp" diff --git a/tests/yup_audio_plugin_host/AudioPluginDescription.cpp b/tests/yup_audio_plugin_host/yup_AudioPluginDescription.cpp similarity index 100% rename from tests/yup_audio_plugin_host/AudioPluginDescription.cpp rename to tests/yup_audio_plugin_host/yup_AudioPluginDescription.cpp diff --git a/tests/yup_audio_plugin_host/AudioPluginInstance.cpp b/tests/yup_audio_plugin_host/yup_AudioPluginInstance.cpp similarity index 100% rename from tests/yup_audio_plugin_host/AudioPluginInstance.cpp rename to tests/yup_audio_plugin_host/yup_AudioPluginInstance.cpp diff --git a/tests/yup_audio_plugin_host/AudioPluginScanner.cpp b/tests/yup_audio_plugin_host/yup_AudioPluginScanner.cpp similarity index 96% rename from tests/yup_audio_plugin_host/AudioPluginScanner.cpp rename to tests/yup_audio_plugin_host/yup_AudioPluginScanner.cpp index 95088fe66..88108e8d4 100644 --- a/tests/yup_audio_plugin_host/AudioPluginScanner.cpp +++ b/tests/yup_audio_plugin_host/yup_AudioPluginScanner.cpp @@ -30,7 +30,7 @@ class FakeFormat : public AudioPluginFormat desc.formatType = fakeType; desc.name = file.getFileNameWithoutExtension(); desc.identifier = "fake." + file.getFileNameWithoutExtension(); - return std::vector { desc }; + return ResultValue>::ok (std::vector { desc }); } ResultValue> loadPlugin ( diff --git a/tests/yup_audio_plugin_host/AudioPluginState.cpp b/tests/yup_audio_plugin_host/yup_AudioPluginState.cpp similarity index 100% rename from tests/yup_audio_plugin_host/AudioPluginState.cpp rename to tests/yup_audio_plugin_host/yup_AudioPluginState.cpp From 28562e5c45048624bf9e890b5a2cca8670d8694e Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sat, 16 May 2026 17:40:11 +0200 Subject: [PATCH 06/35] Fix audio processor --- .../yup_audio_plugin_host/host/yup_AudioPluginScanner.cpp | 2 +- .../yup_audio_processors/processors/yup_AudioProcessor.cpp | 2 -- .../yup_audio_processors/processors/yup_AudioProcessor.h | 6 +++++- tests/CMakeLists.txt | 1 + tests/yup_audio_plugin_host/yup_AudioPluginInstance.cpp | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/modules/yup_audio_plugin_host/host/yup_AudioPluginScanner.cpp b/modules/yup_audio_plugin_host/host/yup_AudioPluginScanner.cpp index 37de1995e..7024ace6c 100644 --- a/modules/yup_audio_plugin_host/host/yup_AudioPluginScanner.cpp +++ b/modules/yup_audio_plugin_host/host/yup_AudioPluginScanner.cpp @@ -148,7 +148,7 @@ AudioPluginScanner::ScanResult AudioPluginScanner::scan (const FileSearchPath& s for (int p = 0; p < numPaths; ++p) { Array files; - searchPath[p].findChildFiles (files, File::findFilesAndDirectories, true, "*", File::FollowSymlinks::no); + searchPath[p].findChildFiles (files, File::findFilesAndDirectories, true, "*", File::FollowSymlinks::noCycles); for (const auto& file : files) { diff --git a/modules/yup_audio_processors/processors/yup_AudioProcessor.cpp b/modules/yup_audio_processors/processors/yup_AudioProcessor.cpp index d72013c58..c5fbfc27b 100644 --- a/modules/yup_audio_processors/processors/yup_AudioProcessor.cpp +++ b/modules/yup_audio_processors/processors/yup_AudioProcessor.cpp @@ -89,10 +89,8 @@ bool AudioProcessor::isSuspended() const void AudioProcessor::setPlaybackConfiguration (float sampleRate, int samplesPerBlock) { releaseResources(); - this->sampleRate = sampleRate; this->samplesPerBlock = samplesPerBlock; - prepareToPlay (sampleRate, samplesPerBlock); } diff --git a/modules/yup_audio_processors/processors/yup_AudioProcessor.h b/modules/yup_audio_processors/processors/yup_AudioProcessor.h index 61018a8e1..b08a80103 100644 --- a/modules/yup_audio_processors/processors/yup_AudioProcessor.h +++ b/modules/yup_audio_processors/processors/yup_AudioProcessor.h @@ -67,7 +67,11 @@ class YUP_API AudioProcessor //============================================================================== - /** Prepares the processor for playback. */ + /** Prepares the processor for playback. + + getSampleRate() and getSamplesPerBlock() are guaranteed to return the correct + values when this is called. Subclasses do not need to call the base class. + */ virtual void prepareToPlay (float sampleRate, int maxBlockSize) = 0; /** Releases resources. */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index cd1a97d9e..c763d8aeb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -58,6 +58,7 @@ set (target_modules yup_audio_basics yup_audio_devices yup_audio_formats + yup_audio_processors yup_dsp yup_events yup_data_model diff --git a/tests/yup_audio_plugin_host/yup_AudioPluginInstance.cpp b/tests/yup_audio_plugin_host/yup_AudioPluginInstance.cpp index abcd6b8f7..452daec9f 100644 --- a/tests/yup_audio_plugin_host/yup_AudioPluginInstance.cpp +++ b/tests/yup_audio_plugin_host/yup_AudioPluginInstance.cpp @@ -82,7 +82,7 @@ TEST_F (AudioPluginInstanceTests, DescriptionIsStoredCorrectly) TEST_F (AudioPluginInstanceTests, PrepareToPlaySetsPreparedFlag) { - instance.prepareToPlay (44100.0f, 512); + instance.setPlaybackConfiguration (44100.0f, 512); EXPECT_TRUE (instance.prepared); EXPECT_FLOAT_EQ (44100.0f, instance.getSampleRate()); EXPECT_EQ (512, instance.getSamplesPerBlock()); From 2e1f795ee58672187cf5f251c4fb92c9f9c1e18b Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sun, 17 May 2026 08:42:07 +0200 Subject: [PATCH 07/35] Cleanup compiler support --- modules/yup_core/system/yup_CompilerSupport.h | 25 +++++-------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/modules/yup_core/system/yup_CompilerSupport.h b/modules/yup_core/system/yup_CompilerSupport.h index 2b4dbe442..383369d77 100644 --- a/modules/yup_core/system/yup_CompilerSupport.h +++ b/modules/yup_core/system/yup_CompilerSupport.h @@ -57,8 +57,8 @@ #endif #endif -#define YUP_CXX17_IS_AVAILABLE (__cplusplus >= 201703L) #define YUP_CXX20_IS_AVAILABLE (__cplusplus >= 202002L) +#define YUP_CXX23_IS_AVAILABLE (__cplusplus >= 202302L) #endif @@ -89,8 +89,8 @@ #error Please upgrade to Xcode 15.1 or higher #endif -#define YUP_CXX17_IS_AVAILABLE (__cplusplus >= 201703L) #define YUP_CXX20_IS_AVAILABLE (__cplusplus >= 202002L) +#define YUP_CXX23_IS_AVAILABLE (__cplusplus >= 202302L) #endif @@ -108,24 +108,11 @@ #endif #endif -#define YUP_CXX17_IS_AVAILABLE (_MSVC_LANG >= 201703L) -#define YUP_CXX20_IS_AVAILABLE (_MSVC_LANG >= 202002L) +#define YUP_CXX20_IS_AVAILABLE (_MSVC_LANG == 202002L) +#define YUP_CXX23_IS_AVAILABLE (_MSVC_LANG > 202002L) #endif //============================================================================== -#if ! YUP_CXX17_IS_AVAILABLE -#error "YUP requires C++17 or later" -#endif - -//============================================================================== -#ifndef DOXYGEN -// These are old flags that are now supported on all compatible build targets -#define YUP_CXX14_IS_AVAILABLE 1 -#define YUP_COMPILER_SUPPORTS_OVERRIDE_AND_FINAL 1 -#define YUP_COMPILER_SUPPORTS_VARIADIC_TEMPLATES 1 -#define YUP_COMPILER_SUPPORTS_INITIALIZER_LISTS 1 -#define YUP_COMPILER_SUPPORTS_NOEXCEPT 1 -#define YUP_DELETED_FUNCTION = delete -#define YUP_CONSTEXPR constexpr -#define YUP_NODISCARD [[nodiscard]] +#if ! YUP_CXX20_IS_AVAILABLE +#error "YUP requires C++20 or later" #endif From 132df185b8d6d69f46007c4cd6301cb3bb6b363e Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sun, 17 May 2026 10:27:30 +0200 Subject: [PATCH 08/35] Audio graph --- .../graphics/source/examples/AudioGraphDemo.h | 549 ++++++ .../graphics/source/examples/PluginHost.h | 3 - examples/graphics/source/main.cpp | 2 + .../graph/yup_AudioGraphProcessor.cpp | 1207 ++++++++++++ .../graph/yup_AudioGraphProcessor.h | 293 +++ modules/yup_audio_graph/yup_audio_graph.cpp | 34 + modules/yup_audio_graph/yup_audio_graph.h | 59 + modules/yup_audio_graph/yup_audio_graph.mm | 22 + .../graph/yup_AudioGraphComponent.cpp | 922 +++++++++ .../graph/yup_AudioGraphComponent.h | 222 +++ .../graph/yup_AudioGraphNodeView.cpp | 317 +++ .../graph/yup_AudioGraphNodeView.h | 164 ++ modules/yup_audio_gui/yup_audio_gui.cpp | 2 + modules/yup_audio_gui/yup_audio_gui.h | 6 +- .../themes/theme_v1/yup_ThemeVersion1.cpp | 9 +- tests/CMakeLists.txt | 1 + tests/yup_audio_graph.cpp | 22 + .../yup_AudioGraphProcessor.cpp | 1753 +++++++++++++++++ 18 files changed, 5579 insertions(+), 8 deletions(-) create mode 100644 examples/graphics/source/examples/AudioGraphDemo.h create mode 100644 modules/yup_audio_graph/graph/yup_AudioGraphProcessor.cpp create mode 100644 modules/yup_audio_graph/graph/yup_AudioGraphProcessor.h create mode 100644 modules/yup_audio_graph/yup_audio_graph.cpp create mode 100644 modules/yup_audio_graph/yup_audio_graph.h create mode 100644 modules/yup_audio_graph/yup_audio_graph.mm create mode 100644 modules/yup_audio_gui/graph/yup_AudioGraphComponent.cpp create mode 100644 modules/yup_audio_gui/graph/yup_AudioGraphComponent.h create mode 100644 modules/yup_audio_gui/graph/yup_AudioGraphNodeView.cpp create mode 100644 modules/yup_audio_gui/graph/yup_AudioGraphNodeView.h create mode 100644 tests/yup_audio_graph.cpp create mode 100644 tests/yup_audio_graph/yup_AudioGraphProcessor.cpp diff --git a/examples/graphics/source/examples/AudioGraphDemo.h b/examples/graphics/source/examples/AudioGraphDemo.h new file mode 100644 index 000000000..d8580c3ea --- /dev/null +++ b/examples/graphics/source/examples/AudioGraphDemo.h @@ -0,0 +1,549 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +#include + +#include +#include + +//============================================================================== +class AudioGraphDemoProcessor : public yup::AudioProcessor +{ +public: + AudioGraphDemoProcessor (yup::StringRef name, yup::AudioBusLayout layout) + : AudioProcessor (name, std::move (layout)) + { + } + + int getCurrentPreset() const noexcept override { return 0; } + + void setCurrentPreset (int) noexcept override {} + + int getNumPresets() const override { return 0; } + + yup::String getPresetName (int) const override { return {}; } + + void setPresetName (int, yup::StringRef) override {} + + yup::Result loadStateFromMemory (const yup::MemoryBlock&) override { return yup::Result::ok(); } + + yup::Result saveStateIntoMemory (yup::MemoryBlock&) override { return yup::Result::ok(); } + + bool hasEditor() const override { return false; } + + yup::AudioProcessorEditor* createEditor() override { return nullptr; } +}; + +class OscillatorProcessor final : public AudioGraphDemoProcessor +{ +public: + OscillatorProcessor() + : AudioGraphDemoProcessor ("Oscillator", + yup::AudioBusLayout ({}, + { yup::AudioBus ("Main", yup::AudioBus::Audio, yup::AudioBus::Output, 2) })) + { + } + + void prepareToPlay (float newSampleRate, int) override + { + sampleRate = newSampleRate; + phase = 0.0; + } + + void releaseResources() override {} + + void processBlock (yup::AudioBuffer& audioBuffer, yup::MidiBuffer&) override + { + const auto currentFrequency = static_cast (frequency.load (std::memory_order_relaxed)); + const auto increment = yup::MathConstants::twoPi * currentFrequency / static_cast (sampleRate); + + for (int sample = 0; sample < audioBuffer.getNumSamples(); ++sample) + { + const auto value = static_cast (std::sin (phase) * 0.22); + + for (int channel = 0; channel < audioBuffer.getNumChannels(); ++channel) + audioBuffer.setSample (channel, sample, value); + + phase += increment; + if (phase >= yup::MathConstants::twoPi) + phase -= yup::MathConstants::twoPi; + } + } + + double getFrequency() const noexcept { return static_cast (frequency.load (std::memory_order_relaxed)); } + + void setFrequency (double newFrequency) noexcept + { + frequency.store (static_cast (yup::jlimit (40.0, 2000.0, newFrequency)), std::memory_order_relaxed); + } + +private: + double sampleRate = 44100.0; + double phase = 0.0; + std::atomic frequency { 440.0f }; +}; + +class GainProcessor final : public AudioGraphDemoProcessor +{ +public: + GainProcessor() + : AudioGraphDemoProcessor ("Gain", + yup::AudioBusLayout ({ yup::AudioBus ("Main", yup::AudioBus::Audio, yup::AudioBus::Input, 2) }, + { yup::AudioBus ("Main", yup::AudioBus::Audio, yup::AudioBus::Output, 2) })) + { + } + + void prepareToPlay (float, int) override {} + + void releaseResources() override {} + + void processBlock (yup::AudioBuffer& audioBuffer, yup::MidiBuffer&) override + { + audioBuffer.applyGain (gain.load (std::memory_order_relaxed)); + } + + float getGain() const noexcept { return gain.load (std::memory_order_relaxed); } + + void setGain (float newGain) noexcept + { + gain.store (yup::jlimit (0.0f, 1.5f, newGain), std::memory_order_relaxed); + } + +private: + std::atomic gain { 0.75f }; +}; + +class LowPassFilterProcessor final : public AudioGraphDemoProcessor +{ +public: + LowPassFilterProcessor() + : AudioGraphDemoProcessor ("Low-pass", + yup::AudioBusLayout ({ yup::AudioBus ("Main", yup::AudioBus::Audio, yup::AudioBus::Input, 2) }, + { yup::AudioBus ("Main", yup::AudioBus::Audio, yup::AudioBus::Output, 2) })) + { + } + + void prepareToPlay (float newSampleRate, int) override + { + sampleRate = newSampleRate; + state[0] = 0.0f; + state[1] = 0.0f; + } + + void releaseResources() override {} + + void processBlock (yup::AudioBuffer& audioBuffer, yup::MidiBuffer&) override + { + const auto currentCutoff = static_cast (cutoff.load (std::memory_order_relaxed)); + const auto alpha = static_cast (1.0 - std::exp (-yup::MathConstants::twoPi * currentCutoff / static_cast (sampleRate))); + + for (int channel = 0; channel < audioBuffer.getNumChannels(); ++channel) + { + auto y = state[static_cast (yup::jmin (channel, 1))]; + + for (int sample = 0; sample < audioBuffer.getNumSamples(); ++sample) + { + y += alpha * (audioBuffer.getSample (channel, sample) - y); + audioBuffer.setSample (channel, sample, y); + } + + state[static_cast (yup::jmin (channel, 1))] = y; + } + } + + double getCutoff() const noexcept { return static_cast (cutoff.load (std::memory_order_relaxed)); } + + void setCutoff (double newCutoff) noexcept + { + cutoff.store (static_cast (yup::jlimit (80.0, 12000.0, newCutoff)), std::memory_order_relaxed); + } + +private: + float sampleRate = 44100.0f; + std::atomic cutoff { 800.0f }; + float state[2] {}; +}; + +namespace +{ +void configureParameterSlider (yup::Slider& slider, yup::Color accent) +{ + slider.setSliderType (yup::Slider::LinearBarHorizontal); + slider.setTextBoxStyle (yup::Slider::NoTextBox); + slider.setColor (yup::Slider::Style::backgroundColorId, yup::Color (0xff26282c)); + slider.setColor (yup::Slider::Style::trackColorId, accent.withAlpha (0.65f)); + slider.setColor (yup::Slider::Style::thumbColorId, accent); + slider.setColor (yup::Slider::Style::thumbOverColorId, accent.brighter (0.15f)); + slider.setColor (yup::Slider::Style::thumbDownColorId, accent.darker (0.15f)); +} + +yup::Rectangle getInlineSliderBounds (const yup::Component& component, int preferredWidth) +{ + const auto bounds = component.getLocalBounds(); + const auto scale = bounds.getWidth() / static_cast (preferredWidth); + return { 62.0f * scale, + 49.0f * scale, + yup::jmax (42.0f * scale, bounds.getWidth() - (150.0f * scale)), + 20.0f * scale }; +} +} // namespace + +//============================================================================== +class OscillatorNodeView final : public yup::AudioGraphNodeView +{ +public: + OscillatorNodeView (yup::AudioGraphNodeID nodeID, OscillatorProcessor& processorIn) + : AudioGraphNodeView (nodeID) + , processor (processorIn) + , frequencySlider (yup::Slider::LinearBarHorizontal) + { + configureParameterSlider (frequencySlider, getPortKindColor (PortKind::parameter)); + frequencySlider.setRange (40.0, 2000.0); + frequencySlider.setSkewFactorFromMidpoint (440.0); + frequencySlider.setValue (processor.getFrequency(), yup::dontSendNotification); + frequencySlider.onValueChanged = [this] (double value) + { + processor.setFrequency (value); + repaint(); + }; + addAndMakeVisible (frequencySlider); + } + + yup::String getNodeTitle() const override { return "OSC"; } + + int getNumInputPorts() const override { return 0; } + + int getNumOutputPorts() const override { return 1; } + + int getPreferredWidth() const override { return 220; } + + yup::Color getNodeColor() const override { return yup::Color (0xff2563eb); } + + yup::String getNodeSubtitle() const override { return yup::String (processor.getFrequency(), 0) + " Hz"; } + + PortInfo getOutputPortInfo (int) const override { return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; } + + int getNumParameterRows() const override { return 1; } + + ParameterInfo getParameterInfo (int) const override + { + return { "Frequency", yup::String (processor.getFrequency(), 0), getPortKindColor (PortKind::parameter), -1.0f, PortKind::parameter }; + } + + void resized() override + { + frequencySlider.setBounds (getInlineSliderBounds (*this, getPreferredWidth())); + } + +private: + OscillatorProcessor& processor; + yup::Slider frequencySlider; +}; + +class GainNodeView final : public yup::AudioGraphNodeView +{ +public: + GainNodeView (yup::AudioGraphNodeID nodeID, GainProcessor& processorIn) + : AudioGraphNodeView (nodeID) + , processor (processorIn) + , gainSlider (yup::Slider::LinearBarHorizontal) + { + configureParameterSlider (gainSlider, getPortKindColor (PortKind::parameter)); + gainSlider.setRange (0.0, 1.5); + gainSlider.setValue (processor.getGain(), yup::dontSendNotification); + gainSlider.onValueChanged = [this] (double value) + { + processor.setGain (static_cast (value)); + repaint(); + }; + addAndMakeVisible (gainSlider); + } + + yup::String getNodeTitle() const override { return "GAIN"; } + + int getNumInputPorts() const override { return 1; } + + int getNumOutputPorts() const override { return 1; } + + int getPreferredWidth() const override { return 220; } + + yup::Color getNodeColor() const override { return yup::Color (0xff10b981); } + + yup::String getNodeSubtitle() const override { return yup::String ("x ") + yup::String (processor.getGain(), 2); } + + PortInfo getInputPortInfo (int) const override { return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; } + + PortInfo getOutputPortInfo (int) const override { return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; } + + int getNumParameterRows() const override { return 1; } + + ParameterInfo getParameterInfo (int) const override + { + return { "Gain", yup::String (processor.getGain(), 2), getPortKindColor (PortKind::parameter), -1.0f, PortKind::parameter }; + } + + void resized() override + { + gainSlider.setBounds (getInlineSliderBounds (*this, getPreferredWidth())); + } + +private: + GainProcessor& processor; + yup::Slider gainSlider; +}; + +class LowPassFilterNodeView final : public yup::AudioGraphNodeView +{ +public: + LowPassFilterNodeView (yup::AudioGraphNodeID nodeID, LowPassFilterProcessor& processorIn) + : AudioGraphNodeView (nodeID) + , processor (processorIn) + , cutoffSlider (yup::Slider::LinearBarHorizontal) + { + configureParameterSlider (cutoffSlider, getPortKindColor (PortKind::parameter)); + cutoffSlider.setRange (80.0, 12000.0); + cutoffSlider.setSkewFactorFromMidpoint (1000.0); + cutoffSlider.setValue (processor.getCutoff(), yup::dontSendNotification); + cutoffSlider.onValueChanged = [this] (double value) + { + processor.setCutoff (value); + repaint(); + }; + addAndMakeVisible (cutoffSlider); + } + + yup::String getNodeTitle() const override { return "LPF"; } + + int getNumInputPorts() const override { return 1; } + + int getNumOutputPorts() const override { return 1; } + + int getPreferredWidth() const override { return 220; } + + yup::Color getNodeColor() const override { return yup::Color (0xff7c3aed); } + + yup::String getNodeSubtitle() const override { return yup::String (processor.getCutoff(), 0) + " Hz"; } + + PortInfo getInputPortInfo (int) const override { return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; } + + PortInfo getOutputPortInfo (int) const override { return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; } + + int getNumParameterRows() const override { return 1; } + + ParameterInfo getParameterInfo (int) const override + { + return { "Cutoff", yup::String (processor.getCutoff(), 0), getPortKindColor (PortKind::parameter), -1.0f, PortKind::parameter }; + } + + void resized() override + { + cutoffSlider.setBounds (getInlineSliderBounds (*this, getPreferredWidth())); + } + +private: + LowPassFilterProcessor& processor; + yup::Slider cutoffSlider; +}; + +class SoundCardInputNodeView final : public yup::AudioGraphNodeView +{ +public: + SoundCardInputNodeView() + : AudioGraphNodeView (yup::AudioGraphNodeID::invalid()) + { + } + + yup::String getNodeTitle() const override { return "INPUT"; } + + yup::String getNodeSubtitle() const override { return "sound card"; } + + int getNumInputPorts() const override { return 0; } + + int getNumOutputPorts() const override { return 1; } + + int getPreferredWidth() const override { return 150; } + + yup::Color getNodeColor() const override { return yup::Color (0xfff97316); } + + PortInfo getOutputPortInfo (int) const override { return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; } +}; + +class SoundCardOutputNodeView final : public yup::AudioGraphNodeView +{ +public: + SoundCardOutputNodeView() + : AudioGraphNodeView (yup::AudioGraphNodeID::invalid()) + { + } + + yup::String getNodeTitle() const override { return "OUTPUT"; } + + yup::String getNodeSubtitle() const override { return "sound card"; } + + int getNumInputPorts() const override { return 1; } + + int getNumOutputPorts() const override { return 0; } + + int getPreferredWidth() const override { return 150; } + + yup::Color getNodeColor() const override { return yup::Color (0xff06b6d4); } + + PortInfo getInputPortInfo (int) const override { return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; } +}; + +//============================================================================== +class AudioGraphDemo final + : public yup::Component + , public yup::AudioIODeviceCallback +{ +public: + AudioGraphDemo() + { + deviceManager.initialiseWithDefaultDevices (2, 2); + + graph = std::make_shared(); + + auto oscillator = std::make_unique(); + auto* oscillatorProcessor = oscillator.get(); + const auto oscillatorID = graph->addNode (std::move (oscillator)); + + auto gain = std::make_unique(); + auto* gainProcessor = gain.get(); + const auto gainID = graph->addNode (std::move (gain)); + + auto filter = std::make_unique(); + auto* filterProcessor = filter.get(); + const auto filterID = graph->addNode (std::move (filter)); + + graph->addConnection ({ yup::AudioGraphEndpoint::graphInput (0), + yup::AudioGraphEndpoint::nodeInput (gainID, 0) }); + graph->addConnection ({ yup::AudioGraphEndpoint::nodeOutput (oscillatorID, 0), + yup::AudioGraphEndpoint::nodeInput (gainID, 0) }); + graph->addConnection ({ yup::AudioGraphEndpoint::nodeOutput (gainID, 0), + yup::AudioGraphEndpoint::nodeInput (filterID, 0) }); + graph->addConnection ({ yup::AudioGraphEndpoint::nodeOutput (filterID, 0), + yup::AudioGraphEndpoint::graphOutput (0) }); + graph->commitChanges(); + + graphComponent = std::make_unique (graph); + addAndMakeVisible (*graphComponent); + + graphComponent->setGraphInputView (std::make_unique(), { 40.0f, 210.0f }); + graphComponent->addNodeView (oscillatorID, std::make_unique (oscillatorID, *oscillatorProcessor), { 70.0f, 80.0f }); + graphComponent->addNodeView (gainID, std::make_unique (gainID, *gainProcessor), { 300.0f, 145.0f }); + graphComponent->addNodeView (filterID, std::make_unique (filterID, *filterProcessor), { 530.0f, 145.0f }); + graphComponent->setGraphOutputView (std::make_unique(), { 760.0f, 145.0f }); + graphComponent->zoomToFitNodes(); + } + + ~AudioGraphDemo() override + { + removeAudioCallback(); + deviceManager.closeAudioDevice(); + } + + void resized() override + { + if (graphComponent != nullptr) + graphComponent->setBounds (getLocalBounds()); + } + + void paint (yup::Graphics& g) override + { + g.setFillColor (yup::Color (0xff101522)); + g.fillAll(); + } + + void visibilityChanged() override + { + if (isVisible()) + addAudioCallback(); + else + removeAudioCallback(); + } + + void audioDeviceIOCallbackWithContext (const float* const* inputChannelData, + int numInputChannels, + float* const* outputChannelData, + int numOutputChannels, + int numSamples, + const yup::AudioIODeviceCallbackContext&) override + { + for (int channel = 0; channel < numOutputChannels; ++channel) + { + if (outputChannelData[channel] != nullptr) + yup::FloatVectorOperations::clear (outputChannelData[channel], numSamples); + } + + if (graph == nullptr || numOutputChannels <= 0 || numSamples <= 0) + return; + + yup::AudioBuffer outputBuffer (outputChannelData, numOutputChannels, numSamples); + const auto channelsToCopy = yup::jmin (numInputChannels, numOutputChannels); + + for (int channel = 0; channel < channelsToCopy; ++channel) + { + if (inputChannelData != nullptr && inputChannelData[channel] != nullptr && outputChannelData[channel] != nullptr) + yup::FloatVectorOperations::copy (outputChannelData[channel], inputChannelData[channel], numSamples); + } + + yup::MidiBuffer midi; + graph->processBlock (outputBuffer, midi); + } + + void audioDeviceAboutToStart (yup::AudioIODevice* device) override + { + if (graph != nullptr && device != nullptr) + graph->prepareToPlay (static_cast (device->getCurrentSampleRate()), device->getCurrentBufferSizeSamples()); + } + + void audioDeviceStopped() override + { + if (graph != nullptr) + graph->releaseResources(); + } + +private: + void addAudioCallback() + { + if (! audioCallbackRegistered) + { + deviceManager.addAudioCallback (this); + audioCallbackRegistered = true; + } + } + + void removeAudioCallback() + { + if (audioCallbackRegistered) + { + deviceManager.removeAudioCallback (this); + audioCallbackRegistered = false; + } + } + + yup::AudioDeviceManager deviceManager; + std::shared_ptr graph; + std::unique_ptr graphComponent; + bool audioCallbackRegistered = false; +}; diff --git a/examples/graphics/source/examples/PluginHost.h b/examples/graphics/source/examples/PluginHost.h index 12ca9eef0..f4a9d976b 100644 --- a/examples/graphics/source/examples/PluginHost.h +++ b/examples/graphics/source/examples/PluginHost.h @@ -593,9 +593,6 @@ class PluginHostDemo : public yup::Component parameterList.setModel (¶meterListModel); addAndMakeVisible (parameterList); parameterList.setVisible (false); - - // Start audio - audioDeviceManager.addAudioCallback (this); } void performScan() diff --git a/examples/graphics/source/main.cpp b/examples/graphics/source/main.cpp index b0ba11e12..79d1ed1a4 100644 --- a/examples/graphics/source/main.cpp +++ b/examples/graphics/source/main.cpp @@ -39,6 +39,7 @@ #include "examples/Artboard.h" #include "examples/Audio.h" #include "examples/AudioFileDemo.h" +#include "examples/AudioGraphDemo.h" #include "examples/ColorLab.h" #include "examples/ConvolutionDemo.h" #include "examples/CrossoverDemo.h" @@ -142,6 +143,7 @@ class CustomWindow registerDemo ("Audio", counter++); registerDemo ("Audio File", counter++); + registerDemo ("Audio Graph", counter++); registerDemo ("Color Lab", counter++); registerDemo ("Convolution Demo", counter++); registerDemo ("Crossover Demo", counter++); diff --git a/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.cpp b/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.cpp new file mode 100644 index 000000000..e970da2be --- /dev/null +++ b/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.cpp @@ -0,0 +1,1207 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +namespace +{ +enum class GraphSignalType +{ + audio, + midi +}; + +struct BusDescriptor +{ + GraphSignalType type = GraphSignalType::audio; + int channels = 0; + int offset = 0; +}; + +int getTotalAudioChannels (Span buses) +{ + int result = 0; + + for (const auto& bus : buses) + if (bus.getType() == AudioBus::Type::Audio) + result += bus.getNumChannels(); + + return result; +} + +std::vector getAudioBusOffsets (Span buses) +{ + std::vector offsets (static_cast (buses.size()), 0); + int offset = 0; + + for (int i = 0; i < static_cast (buses.size()); ++i) + { + offsets[static_cast (i)] = offset; + + if (buses[static_cast (i)].getType() == AudioBus::Type::Audio) + offset += buses[static_cast (i)].getNumChannels(); + } + + return offsets; +} + +GraphSignalType toSignalType (AudioBus::Type busType) +{ + return busType == AudioBus::Type::Audio ? GraphSignalType::audio : GraphSignalType::midi; +} + +bool isValidBusIndex (Span buses, int busIndex) +{ + return busIndex >= 0 && busIndex < static_cast (buses.size()); +} +} // namespace + +//============================================================================== +class AudioGraphProcessor::Pimpl +{ +public: + explicit Pimpl (AudioGraphProcessor& ownerIn) + : owner (ownerIn) + { + } + + ~Pimpl() + { + resizeWorkers (0); + delete pendingPlan.exchange (nullptr); + delete currentPlan; + deleteRetiredPlans(); + } + + struct ModelNode + { + AudioGraphNodeID id; + std::shared_ptr processor; + float preparedSampleRate = 0.0f; + int preparedBlockSize = 0; + bool prepared = false; + }; + + struct DelayLine + { + void initialise (GraphSignalType signalTypeIn, int numChannels, int delaySamplesIn, int maxBlockSize, size_t midiReserveBytes = 4096) + { + signalType = signalTypeIn; + delaySamples = delaySamplesIn; + channels = numChannels; + writePosition = 0; + + if (signalType == GraphSignalType::audio && delaySamples > 0) + { + audio.setSize (channels, delaySamples + maxBlockSize + 1, false, true, false); + audio.clear(); + } + + pendingMidi.ensureSize (midiReserveBytes); + nextPendingMidi.ensureSize (midiReserveBytes); + } + + void reset() + { + writePosition = 0; + audio.clear(); + pendingMidi.clear(); + nextPendingMidi.clear(); + } + + GraphSignalType signalType = GraphSignalType::audio; + int delaySamples = 0; + int channels = 0; + int writePosition = 0; + AudioBuffer audio; + MidiBuffer pendingMidi; + MidiBuffer nextPendingMidi; + }; + + struct CompiledConnection + { + AudioGraphConnection connection; + GraphSignalType type = GraphSignalType::audio; + int sourceNodeIndex = -1; + int destinationNodeIndex = -1; + int sourceOffset = 0; + int destinationOffset = 0; + int channels = 0; + int delayLineIndex = -1; + int delaySamples = 0; + }; + + struct NodeRuntime + { + AudioGraphNodeID id; + std::shared_ptr processor; + int inputChannels = 0; + int outputChannels = 0; + int workChannels = 0; + int inputLatencySamples = 0; + int outputLatencySamples = 0; + std::vector incomingConnections; + AudioBuffer audioBuffer; + MidiBuffer midiBuffer; + }; + + struct CompiledGraph + { + CompiledGraph* nextRetired = nullptr; + + int maxBlockSize = 0; + int graphLatencySamples = 0; + int graphInputChannels = 0; + int graphOutputChannels = 0; + std::vector graphInputBusOffsets; + std::vector graphOutputBusOffsets; + std::vector nodes; + std::vector topologicalOrder; + std::vector> executionLevels; + std::vector connections; + std::vector graphOutputConnections; + std::vector delayLines; + AudioBuffer graphInputAudio; + MidiBuffer graphInputMidi; + AudioGraphAllocationStats stats; + }; + + struct ResolvedEndpoint + { + GraphSignalType type = GraphSignalType::audio; + int channels = 0; + int offset = 0; + int nodeIndex = -1; + }; + + class WorkerThread final : public Thread + { + public: + WorkerThread (Pimpl& ownerIn, int index); + ~WorkerThread() override; + + void run() override; + + private: + Pimpl& owner; + }; + + AudioGraphNodeID addNode (std::unique_ptr processor) + { + if (processor == nullptr) + return AudioGraphNodeID::invalid(); + + const std::lock_guard lock (modelMutex); + + const auto id = AudioGraphNodeID (++nextNodeID); + modelNodes.push_back ({ id, std::shared_ptr (std::move (processor)) }); + dirty.store (true); + return id; + } + + bool removeNode (AudioGraphNodeID nodeID) + { + if (! nodeID.isValid()) + return false; + + const std::lock_guard lock (modelMutex); + + const auto nodeIterator = std::find_if (modelNodes.begin(), modelNodes.end(), [nodeID] (const ModelNode& node) + { + return node.id == nodeID; + }); + + if (nodeIterator == modelNodes.end()) + return false; + + modelNodes.erase (nodeIterator); + + modelConnections.erase (std::remove_if (modelConnections.begin(), modelConnections.end(), [nodeID] (const AudioGraphConnection& connection) + { + return connection.source.getNodeID() == nodeID || connection.destination.getNodeID() == nodeID; + }), + modelConnections.end()); + + dirty.store (true); + return true; + } + + Result addConnection (const AudioGraphConnection& connection) + { + if (! connection.source.isSource()) + return Result::fail ("Audio graph connection source is not a source endpoint"); + + if (! connection.destination.isDestination()) + return Result::fail ("Audio graph connection destination is not a destination endpoint"); + + const std::lock_guard lock (modelMutex); + + if (std::find (modelConnections.begin(), modelConnections.end(), connection) != modelConnections.end()) + return Result::fail ("Audio graph connection already exists"); + + modelConnections.push_back (connection); + dirty.store (true); + return Result::ok(); + } + + bool removeConnection (const AudioGraphConnection& connection) + { + const std::lock_guard lock (modelMutex); + const auto oldSize = modelConnections.size(); + + modelConnections.erase (std::remove (modelConnections.begin(), modelConnections.end(), connection), modelConnections.end()); + + const bool removed = modelConnections.size() != oldSize; + if (removed) + dirty.store (true); + + return removed; + } + + std::vector getConnections() const + { + const std::lock_guard lock (modelMutex); + return modelConnections; + } + + void clear() + { + const std::lock_guard lock (modelMutex); + + modelConnections.clear(); + modelNodes.clear(); + dirty.store (true); + } + + Result commitChanges() + { + std::vector nodesSnapshot; + std::vector connectionsSnapshot; + + { + const std::lock_guard lock (modelMutex); + nodesSnapshot = modelNodes; + connectionsSnapshot = modelConnections; + } + + for (auto& node : nodesSnapshot) + { + if (node.processor == nullptr) + return Result::fail ("Audio graph contains an empty node"); + + if (! node.prepared || node.preparedSampleRate != sampleRate || node.preparedBlockSize != maxBlockSize) + { + node.processor->setPlayHead (owner.getPlayHead()); + node.processor->setPlaybackConfiguration (sampleRate, maxBlockSize); + } + } + + auto compiled = std::make_unique(); + const auto result = compileGraph (*compiled, nodesSnapshot, connectionsSnapshot); + + if (! result) + return result; + + { + const std::lock_guard lock (modelMutex); + + for (auto& modelNode : modelNodes) + { + const auto preparedIterator = std::find_if (nodesSnapshot.begin(), nodesSnapshot.end(), [&modelNode] (const ModelNode& preparedNode) + { + return preparedNode.id == modelNode.id; + }); + + if (preparedIterator != nodesSnapshot.end()) + { + modelNode.prepared = true; + modelNode.preparedSampleRate = sampleRate; + modelNode.preparedBlockSize = maxBlockSize; + } + } + } + + storeStats (compiled->stats); + latestLatencySamples.store (compiled->graphLatencySamples); + dirty.store (false); + + delete pendingPlan.exchange (compiled.release()); + deleteRetiredPlans(); + + return Result::ok(); + } + + void prepareToPlay (float newSampleRate, int newMaxBlockSize) + { + sampleRate = newSampleRate; + maxBlockSize = std::max (1, newMaxBlockSize); + + const auto result = commitChanges(); + jassert (result.wasOk()); + } + + void releaseResources() + { + const std::lock_guard lock (modelMutex); + + for (auto& node : modelNodes) + { + if (node.processor != nullptr && node.prepared) + node.processor->releaseResources(); + + node.prepared = false; + node.preparedSampleRate = 0.0f; + node.preparedBlockSize = 0; + } + } + + void processBlock (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) + { + ScopedNoDenormals noDenormals; + swapPendingPlan(); + + auto* graph = currentPlan; + if (graph == nullptr || owner.isSuspended()) + { + audioBuffer.clear(); + midiBuffer.clear(); + return; + } + + const int numSamples = std::min (audioBuffer.getNumSamples(), graph->maxBlockSize); + if (numSamples <= 0) + { + audioBuffer.clear(); + midiBuffer.clear(); + return; + } + + captureGraphInput (*graph, audioBuffer, midiBuffer, numSamples); + + audioBuffer.clear(); + midiBuffer.clear(); + + if (desiredWorkerThreads.load() > 0) + { + processLevels (*graph, numSamples); + } + else + { + for (const auto nodeIndex : graph->topologicalOrder) + processNode (*graph, nodeIndex, numSamples); + } + + for (const auto connectionIndex : graph->graphOutputConnections) + routeConnection (*graph, graph->connections[static_cast (connectionIndex)], audioBuffer, midiBuffer, numSamples); + } + + void flush() + { + if (auto* graph = currentPlan) + { + for (auto& node : graph->nodes) + { + node.processor->flush(); + node.midiBuffer.clear(); + node.audioBuffer.clear(); + } + + for (auto& delayLine : graph->delayLines) + delayLine.reset(); + } + } + + void setNumWorkerThreads (int numThreads) + { + const int newNumThreads = std::max (0, numThreads); + desiredWorkerThreads.store (newNumThreads); + resizeWorkers (newNumThreads); + } + + void setAudioWorkgroup (AudioWorkgroup newWorkgroup) + { + const std::lock_guard lock (workgroupMutex); + workgroup = std::move (newWorkgroup); + } + + void joinWorkgroup (WorkgroupToken& token) + { + AudioWorkgroup currentWorkgroup; + + { + const std::lock_guard lock (workgroupMutex); + currentWorkgroup = workgroup; + } + + if (currentWorkgroup) + currentWorkgroup.join (token); + else + token.reset(); + } + + AudioGraphAllocationStats getAllocationStats() const noexcept + { + AudioGraphAllocationStats stats; + stats.scratchAudioBuffers = latestScratchAudioBuffers.load(); + stats.midiBuffers = latestMidiBuffers.load(); + stats.delayLines = latestDelayLines.load(); + stats.totalCompensationSamples = latestTotalCompensationSamples.load(); + stats.maxPreallocatedChannels = latestMaxPreallocatedChannels.load(); + stats.maxPreallocatedBlockSize = latestMaxPreallocatedBlockSize.load(); + return stats; + } + + AudioProcessor* getNodeProcessor (AudioGraphNodeID nodeID) const noexcept + { + const std::lock_guard lock (modelMutex); + + const auto iterator = std::find_if (modelNodes.begin(), modelNodes.end(), [nodeID] (const ModelNode& node) + { + return node.id == nodeID; + }); + + return iterator != modelNodes.end() ? iterator->processor.get() : nullptr; + } + + Result compileGraph (CompiledGraph& graph, + const std::vector& nodesSnapshot, + const std::vector& connectionsSnapshot) + { + graph.maxBlockSize = maxBlockSize; + graph.graphInputChannels = getTotalAudioChannels (owner.getBusLayout().getInputBuses()); + graph.graphOutputChannels = getTotalAudioChannels (owner.getBusLayout().getOutputBuses()); + graph.graphInputBusOffsets = getAudioBusOffsets (owner.getBusLayout().getInputBuses()); + graph.graphOutputBusOffsets = getAudioBusOffsets (owner.getBusLayout().getOutputBuses()); + graph.graphInputAudio.setSize (graph.graphInputChannels, maxBlockSize, false, true, false); + graph.graphInputAudio.clear(); + + std::unordered_map nodeIndexByID; + + for (int i = 0; i < static_cast (nodesSnapshot.size()); ++i) + { + const auto& modelNode = nodesSnapshot[static_cast (i)]; + + if (modelNode.processor == nullptr) + return Result::fail ("Audio graph contains an empty node"); + + if (! nodeIndexByID.emplace (modelNode.id.getRawID(), i).second) + return Result::fail ("Audio graph contains duplicate node IDs"); + + NodeRuntime runtime; + runtime.id = modelNode.id; + runtime.processor = modelNode.processor; + runtime.inputChannels = getTotalAudioChannels (modelNode.processor->getBusLayout().getInputBuses()); + runtime.outputChannels = getTotalAudioChannels (modelNode.processor->getBusLayout().getOutputBuses()); + runtime.workChannels = std::max (runtime.inputChannels, runtime.outputChannels); + runtime.audioBuffer.setSize (runtime.workChannels, maxBlockSize, false, true, false); + runtime.audioBuffer.clear(); + graph.nodes.push_back (std::move (runtime)); + } + + std::vector> adjacency (nodesSnapshot.size()); + std::vector indegrees (nodesSnapshot.size(), 0); + + for (const auto& modelConnection : connectionsSnapshot) + { + if (std::count (connectionsSnapshot.begin(), connectionsSnapshot.end(), modelConnection) > 1) + return Result::fail ("Audio graph contains duplicate connections"); + + CompiledConnection connection; + connection.connection = modelConnection; + + const auto source = resolveEndpoint (modelConnection.source, true, nodesSnapshot, nodeIndexByID); + if (! source) + return Result::fail (source.getErrorMessage()); + + const auto destination = resolveEndpoint (modelConnection.destination, false, nodesSnapshot, nodeIndexByID); + if (! destination) + return Result::fail (destination.getErrorMessage()); + + const auto& sourceEndpoint = sourceEndpointScratch; + const auto& destinationEndpoint = destinationEndpointScratch; + + if (sourceEndpoint.type != destinationEndpoint.type) + return Result::fail ("Audio graph connection mixes audio and MIDI endpoints"); + + if (sourceEndpoint.type == GraphSignalType::audio && sourceEndpoint.channels != destinationEndpoint.channels) + return Result::fail ("Audio graph connection has incompatible channel counts"); + + connection.type = sourceEndpoint.type; + connection.sourceNodeIndex = sourceEndpoint.nodeIndex; + connection.destinationNodeIndex = destinationEndpoint.nodeIndex; + connection.sourceOffset = sourceEndpoint.offset; + connection.destinationOffset = destinationEndpoint.offset; + connection.channels = sourceEndpoint.channels; + + const int connectionIndex = static_cast (graph.connections.size()); + graph.connections.push_back (connection); + + if (connection.destinationNodeIndex >= 0) + { + graph.nodes[static_cast (connection.destinationNodeIndex)].incomingConnections.push_back (connectionIndex); + + if (connection.sourceNodeIndex >= 0) + { + adjacency[static_cast (connection.sourceNodeIndex)].push_back (connection.destinationNodeIndex); + ++indegrees[static_cast (connection.destinationNodeIndex)]; + } + } + else + { + graph.graphOutputConnections.push_back (connectionIndex); + } + } + + int numMidiConnections = 0; + for (const auto& conn : graph.connections) + if (conn.type == GraphSignalType::midi) + ++numMidiConnections; + + const size_t midiReserveBytes = static_cast (std::max (1, numMidiConnections + 1)) * 4096; + + graph.graphInputMidi.ensureSize (midiReserveBytes); + + for (auto& node : graph.nodes) + node.midiBuffer.ensureSize (midiReserveBytes); + + auto topologicalResult = buildTopologicalOrder (graph, adjacency, indegrees); + if (! topologicalResult) + return topologicalResult; + + buildExecutionLevels (graph); + computeLatenciesAndDelays (graph, midiReserveBytes); + fillStats (graph); + + return Result::ok(); + } + + Result resolveEndpoint (const AudioGraphEndpoint& endpoint, + bool source, + const std::vector& nodesSnapshot, + const std::unordered_map& nodeIndexByID) + { + auto& result = source ? sourceEndpointScratch : destinationEndpointScratch; + result = {}; + + if (source && ! endpoint.isSource()) + return Result::fail ("Audio graph endpoint is not a source"); + + if (! source && ! endpoint.isDestination()) + return Result::fail ("Audio graph endpoint is not a destination"); + + Span buses; + std::vector offsets; + + switch (endpoint.getKind()) + { + case AudioGraphEndpoint::Kind::graphInput: + buses = owner.getBusLayout().getInputBuses(); + offsets = getAudioBusOffsets (buses); + break; + + case AudioGraphEndpoint::Kind::graphOutput: + buses = owner.getBusLayout().getOutputBuses(); + offsets = getAudioBusOffsets (buses); + break; + + case AudioGraphEndpoint::Kind::nodeInput: + case AudioGraphEndpoint::Kind::nodeOutput: + { + const auto iterator = nodeIndexByID.find (endpoint.getNodeID().getRawID()); + if (iterator == nodeIndexByID.end()) + return Result::fail ("Audio graph connection references a missing node"); + + result.nodeIndex = iterator->second; + const auto& processor = *nodesSnapshot[static_cast (result.nodeIndex)].processor; + + buses = endpoint.getKind() == AudioGraphEndpoint::Kind::nodeInput + ? processor.getBusLayout().getInputBuses() + : processor.getBusLayout().getOutputBuses(); + offsets = getAudioBusOffsets (buses); + break; + } + } + + if (! isValidBusIndex (buses, endpoint.getBusIndex())) + return Result::fail ("Audio graph connection references an invalid bus index"); + + const auto& bus = buses[static_cast (endpoint.getBusIndex())]; + result.type = toSignalType (bus.getType()); + result.channels = bus.getType() == AudioBus::Type::Audio ? bus.getNumChannels() : 0; + result.offset = bus.getType() == AudioBus::Type::Audio ? offsets[static_cast (endpoint.getBusIndex())] : 0; + return Result::ok(); + } + + Result buildTopologicalOrder (CompiledGraph& graph, + const std::vector>& adjacency, + std::vector indegrees) + { + std::vector ready; + + for (int i = 0; i < static_cast (indegrees.size()); ++i) + if (indegrees[static_cast (i)] == 0) + ready.push_back (i); + + while (! ready.empty()) + { + const int nodeIndex = ready.front(); + ready.erase (ready.begin()); + graph.topologicalOrder.push_back (nodeIndex); + + for (const auto destination : adjacency[static_cast (nodeIndex)]) + { + auto& indegree = indegrees[static_cast (destination)]; + --indegree; + + if (indegree == 0) + ready.push_back (destination); + } + } + + if (graph.topologicalOrder.size() != graph.nodes.size()) + return Result::fail ("Audio graph contains a cycle"); + + return Result::ok(); + } + + void buildExecutionLevels (CompiledGraph& graph) + { + std::vector nodeLevels (graph.nodes.size(), 0); + + for (const auto nodeIndex : graph.topologicalOrder) + { + int level = 0; + const auto& node = graph.nodes[static_cast (nodeIndex)]; + + for (const auto connectionIndex : node.incomingConnections) + { + const auto& connection = graph.connections[static_cast (connectionIndex)]; + if (connection.sourceNodeIndex >= 0) + level = std::max (level, nodeLevels[static_cast (connection.sourceNodeIndex)] + 1); + } + + nodeLevels[static_cast (nodeIndex)] = level; + + if (level >= static_cast (graph.executionLevels.size())) + graph.executionLevels.resize (static_cast (level + 1)); + + graph.executionLevels[static_cast (level)].push_back (nodeIndex); + } + } + + int getSourceLatency (const CompiledGraph& graph, const CompiledConnection& connection) const + { + if (connection.sourceNodeIndex < 0) + return 0; + + return graph.nodes[static_cast (connection.sourceNodeIndex)].outputLatencySamples; + } + + void computeLatenciesAndDelays (CompiledGraph& graph, size_t midiReserveBytes) + { + for (const auto nodeIndex : graph.topologicalOrder) + { + auto& node = graph.nodes[static_cast (nodeIndex)]; + + for (const auto connectionIndex : node.incomingConnections) + node.inputLatencySamples = std::max (node.inputLatencySamples, + getSourceLatency (graph, graph.connections[static_cast (connectionIndex)])); + + node.outputLatencySamples = node.inputLatencySamples + std::max (0, node.processor->getLatencySamples()); + } + + for (const auto connectionIndex : graph.graphOutputConnections) + graph.graphLatencySamples = std::max (graph.graphLatencySamples, + getSourceLatency (graph, graph.connections[static_cast (connectionIndex)])); + + for (auto& connection : graph.connections) + { + const int destinationLatency = connection.destinationNodeIndex >= 0 + ? graph.nodes[static_cast (connection.destinationNodeIndex)].inputLatencySamples + : graph.graphLatencySamples; + + connection.delaySamples = std::max (0, destinationLatency - getSourceLatency (graph, connection)); + + if (connection.delaySamples > 0) + { + DelayLine delayLine; + delayLine.initialise (connection.type, connection.channels, connection.delaySamples, graph.maxBlockSize, midiReserveBytes); + connection.delayLineIndex = static_cast (graph.delayLines.size()); + graph.delayLines.push_back (std::move (delayLine)); + } + } + } + + void fillStats (CompiledGraph& graph) + { + graph.stats.scratchAudioBuffers = static_cast (graph.nodes.size()); + graph.stats.midiBuffers = static_cast (graph.nodes.size()); + graph.stats.delayLines = static_cast (graph.delayLines.size()); + graph.stats.totalCompensationSamples = graph.graphLatencySamples; + graph.stats.maxPreallocatedBlockSize = graph.maxBlockSize; + graph.stats.maxPreallocatedChannels = std::max (graph.graphInputChannels, graph.graphOutputChannels); + + for (const auto& node : graph.nodes) + graph.stats.maxPreallocatedChannels = std::max (graph.stats.maxPreallocatedChannels, node.workChannels); + } + + void storeStats (const AudioGraphAllocationStats& stats) noexcept + { + latestScratchAudioBuffers.store (stats.scratchAudioBuffers); + latestMidiBuffers.store (stats.midiBuffers); + latestDelayLines.store (stats.delayLines); + latestTotalCompensationSamples.store (stats.totalCompensationSamples); + latestMaxPreallocatedChannels.store (stats.maxPreallocatedChannels); + latestMaxPreallocatedBlockSize.store (stats.maxPreallocatedBlockSize); + } + + void captureGraphInput (CompiledGraph& graph, + AudioBuffer& audioBuffer, + MidiBuffer& midiBuffer, + int numSamples) + { + graph.graphInputAudio.clear (0, numSamples); + + const int numChannels = std::min (graph.graphInputChannels, audioBuffer.getNumChannels()); + for (int channel = 0; channel < numChannels; ++channel) + graph.graphInputAudio.copyFrom (channel, 0, audioBuffer, channel, 0, numSamples); + + graph.graphInputMidi.clear(); + graph.graphInputMidi.addEvents (midiBuffer, 0, numSamples, 0); + } + + void processNode (CompiledGraph& graph, int nodeIndex, int numSamples) + { + auto& node = graph.nodes[static_cast (nodeIndex)]; + + node.audioBuffer.setSize (node.workChannels, numSamples, false, false, true); + node.audioBuffer.clear (0, numSamples); + node.midiBuffer.clear(); + + for (const auto connectionIndex : node.incomingConnections) + routeConnection (graph, graph.connections[static_cast (connectionIndex)], node.audioBuffer, node.midiBuffer, numSamples); + + node.processor->processBlock (node.audioBuffer, node.midiBuffer); + } + + void processLevels (CompiledGraph& graph, int numSamples) + { + for (auto& level : graph.executionLevels) + { + if (level.empty()) + continue; + + activeGraph = &graph; + activeLevel = &level; + activeNumSamples = numSamples; + nextJobIndex.store (0, std::memory_order_relaxed); + remainingJobs.store (static_cast (level.size()), std::memory_order_release); + workGeneration.fetch_add (1, std::memory_order_release); + workerReadyEvent.reset(); + workerReadyEvent.signal(); + + drainActiveJobs(); + + while (remainingJobs.load (std::memory_order_acquire) > 0) + ; + } + + activeGraph = nullptr; + activeLevel = nullptr; + activeNumSamples = 0; + } + + void drainActiveJobs() + { + auto* graph = activeGraph; + auto* level = activeLevel; + + if (graph == nullptr || level == nullptr) + return; + + for (;;) + { + const int jobIndex = nextJobIndex.fetch_add (1); + + if (jobIndex >= static_cast (level->size())) + break; + + processNode (*graph, (*level)[static_cast (jobIndex)], activeNumSamples); + + remainingJobs.fetch_sub (1, std::memory_order_release); + } + } + + void routeConnection (CompiledGraph& graph, + CompiledConnection& connection, + AudioBuffer& destinationAudio, + MidiBuffer& destinationMidi, + int numSamples) + { + if (connection.type == GraphSignalType::audio) + { + const auto& sourceAudio = getSourceAudioBuffer (graph, connection); + + if (connection.delayLineIndex >= 0) + routeDelayedAudio (graph.delayLines[static_cast (connection.delayLineIndex)], sourceAudio, connection, destinationAudio, numSamples); + else + routeAudio (sourceAudio, connection, destinationAudio, numSamples); + + return; + } + + const auto& sourceMidi = getSourceMidiBuffer (graph, connection); + + if (connection.delayLineIndex >= 0) + routeDelayedMidi (graph.delayLines[static_cast (connection.delayLineIndex)], sourceMidi, destinationMidi, numSamples); + else + destinationMidi.addEvents (sourceMidi, 0, numSamples, 0); + } + + const AudioBuffer& getSourceAudioBuffer (CompiledGraph& graph, const CompiledConnection& connection) const + { + if (connection.sourceNodeIndex < 0) + return graph.graphInputAudio; + + return graph.nodes[static_cast (connection.sourceNodeIndex)].audioBuffer; + } + + const MidiBuffer& getSourceMidiBuffer (CompiledGraph& graph, const CompiledConnection& connection) const + { + if (connection.sourceNodeIndex < 0) + return graph.graphInputMidi; + + return graph.nodes[static_cast (connection.sourceNodeIndex)].midiBuffer; + } + + void routeAudio (const AudioBuffer& sourceAudio, + const CompiledConnection& connection, + AudioBuffer& destinationAudio, + int numSamples) + { + const int channels = std::min ({ connection.channels, + std::max (0, sourceAudio.getNumChannels() - connection.sourceOffset), + std::max (0, destinationAudio.getNumChannels() - connection.destinationOffset) }); + + for (int channel = 0; channel < channels; ++channel) + destinationAudio.addFrom (connection.destinationOffset + channel, + 0, + sourceAudio, + connection.sourceOffset + channel, + 0, + numSamples); + } + + void routeDelayedAudio (DelayLine& delayLine, + const AudioBuffer& sourceAudio, + const CompiledConnection& connection, + AudioBuffer& destinationAudio, + int numSamples) + { + if (delayLine.delaySamples <= 0) + { + routeAudio (sourceAudio, connection, destinationAudio, numSamples); + return; + } + + const int channels = std::min ({ connection.channels, + std::max (0, sourceAudio.getNumChannels() - connection.sourceOffset), + std::max (0, destinationAudio.getNumChannels() - connection.destinationOffset), + delayLine.audio.getNumChannels() }); + const int ringSize = delayLine.audio.getNumSamples(); + + for (int sample = 0; sample < numSamples; ++sample) + { + const int readPosition = (delayLine.writePosition + ringSize - delayLine.delaySamples) % ringSize; + + for (int channel = 0; channel < channels; ++channel) + { + const float delayedSample = delayLine.audio.getReadPointer (channel)[readPosition]; + destinationAudio.addFrom (connection.destinationOffset + channel, sample, &delayedSample, 1); + + delayLine.audio.getWritePointer (channel)[delayLine.writePosition] = + sourceAudio.getReadPointer (connection.sourceOffset + channel)[sample]; + } + + delayLine.writePosition = (delayLine.writePosition + 1) % ringSize; + } + } + + void routeDelayedMidi (DelayLine& delayLine, + const MidiBuffer& sourceMidi, + MidiBuffer& destinationMidi, + int numSamples) + { + delayLine.nextPendingMidi.clear(); + + for (const auto metadata : delayLine.pendingMidi) + { + if (metadata.samplePosition < numSamples) + destinationMidi.addEvent (metadata.data, metadata.numBytes, metadata.samplePosition); + else + delayLine.nextPendingMidi.addEvent (metadata.data, metadata.numBytes, metadata.samplePosition - numSamples); + } + + for (const auto metadata : sourceMidi) + { + if (metadata.samplePosition < 0 || metadata.samplePosition >= numSamples) + continue; + + const int delayedPosition = metadata.samplePosition + delayLine.delaySamples; + + if (delayedPosition < numSamples) + destinationMidi.addEvent (metadata.data, metadata.numBytes, delayedPosition); + else + delayLine.nextPendingMidi.addEvent (metadata.data, metadata.numBytes, delayedPosition - numSamples); + } + + delayLine.pendingMidi.swapWith (delayLine.nextPendingMidi); + } + + void swapPendingPlan() + { + if (auto* nextPlan = pendingPlan.exchange (nullptr)) + { + retirePlan (currentPlan); + currentPlan = nextPlan; + } + } + + void retirePlan (CompiledGraph* plan) noexcept + { + if (plan == nullptr) + return; + + auto* head = retiredPlans.load(); + + do + { + plan->nextRetired = head; + } while (! retiredPlans.compare_exchange_weak (head, plan)); + } + + void deleteRetiredPlans() + { + auto* plan = retiredPlans.exchange (nullptr); + + while (plan != nullptr) + { + auto* next = plan->nextRetired; + delete plan; + plan = next; + } + } + + void resizeWorkers (int newNumThreads) + { + while (static_cast (workers.size()) > newNumThreads) + { + workers.back()->signalThreadShouldExit(); + workerReadyEvent.signal(); + workers.pop_back(); + } + + if (! workers.empty()) + workerReadyEvent.reset(); + + while (static_cast (workers.size()) < newNumThreads) + { + auto worker = std::make_unique (*this, static_cast (workers.size())); + worker->startThread (Thread::Priority::highest); + workers.push_back (std::move (worker)); + } + } + + AudioGraphProcessor& owner; + mutable std::mutex modelMutex; + mutable std::mutex workgroupMutex; + std::vector modelNodes; + std::vector modelConnections; + uint64_t nextNodeID = 0; + std::atomic dirty { true }; + std::atomic desiredWorkerThreads { 0 }; + std::atomic latestLatencySamples { 0 }; + std::atomic latestScratchAudioBuffers { 0 }; + std::atomic latestMidiBuffers { 0 }; + std::atomic latestDelayLines { 0 }; + std::atomic latestTotalCompensationSamples { 0 }; + std::atomic latestMaxPreallocatedChannels { 0 }; + std::atomic latestMaxPreallocatedBlockSize { 0 }; + AudioWorkgroup workgroup; + float sampleRate = 44100.0f; + int maxBlockSize = 1024; + std::atomic pendingPlan { nullptr }; + std::atomic retiredPlans { nullptr }; + CompiledGraph* currentPlan = nullptr; + ResolvedEndpoint sourceEndpointScratch; + ResolvedEndpoint destinationEndpointScratch; + std::vector> workers; + WaitableEvent workerReadyEvent { true }; + std::atomic workGeneration { 0 }; + std::atomic nextJobIndex { 0 }; + std::atomic remainingJobs { 0 }; + CompiledGraph* activeGraph = nullptr; + std::vector* activeLevel = nullptr; + int activeNumSamples = 0; +}; + +//============================================================================== +AudioGraphProcessor::Pimpl::WorkerThread::WorkerThread (Pimpl& ownerIn, int index) + : Thread ("Audio Graph Worker " + String (index + 1)) + , owner (ownerIn) +{ +} + +AudioGraphProcessor::Pimpl::WorkerThread::~WorkerThread() +{ + stopThread (2000); +} + +void AudioGraphProcessor::Pimpl::WorkerThread::run() +{ + int lastGeneration = 0; + WorkgroupToken workgroupToken; + + while (! threadShouldExit()) + { + owner.workerReadyEvent.wait (-1.0); + + if (threadShouldExit()) + break; + + const int generation = owner.workGeneration.load (std::memory_order_acquire); + if (generation == lastGeneration) + continue; + + lastGeneration = generation; + ScopedNoDenormals noDenormals; + owner.joinWorkgroup (workgroupToken); + owner.drainActiveJobs(); + } +} + +//============================================================================== +AudioBusLayout AudioGraphProcessor::createDefaultBusLayout() +{ + return AudioBusLayout ({ AudioBus ("Input", AudioBus::Type::Audio, AudioBus::Direction::Input, 2) }, + { AudioBus ("Output", AudioBus::Type::Audio, AudioBus::Direction::Output, 2) }); +} + +AudioGraphProcessor::AudioGraphProcessor (AudioBusLayout busLayout) + : AudioProcessor ("Audio Graph", std::move (busLayout)) + , pimpl (std::make_unique (*this)) +{ +} + +AudioGraphProcessor::~AudioGraphProcessor() = default; + +AudioGraphNodeID AudioGraphProcessor::addNode (std::unique_ptr processor) +{ + return pimpl->addNode (std::move (processor)); +} + +bool AudioGraphProcessor::removeNode (AudioGraphNodeID nodeID) +{ + return pimpl->removeNode (nodeID); +} + +Result AudioGraphProcessor::addConnection (const AudioGraphConnection& connection) +{ + return pimpl->addConnection (connection); +} + +bool AudioGraphProcessor::removeConnection (const AudioGraphConnection& connection) +{ + return pimpl->removeConnection (connection); +} + +std::vector AudioGraphProcessor::getConnections() const +{ + return pimpl->getConnections(); +} + +void AudioGraphProcessor::clear() +{ + pimpl->clear(); +} + +Result AudioGraphProcessor::commitChanges() +{ + return pimpl->commitChanges(); +} + +void AudioGraphProcessor::setNumWorkerThreads (int numThreads) +{ + pimpl->setNumWorkerThreads (numThreads); +} + +int AudioGraphProcessor::getNumWorkerThreads() const noexcept +{ + return pimpl->desiredWorkerThreads.load(); +} + +void AudioGraphProcessor::setAudioWorkgroup (AudioWorkgroup workgroup) +{ + pimpl->setAudioWorkgroup (std::move (workgroup)); +} + +AudioGraphAllocationStats AudioGraphProcessor::getAllocationStats() const noexcept +{ + return pimpl->getAllocationStats(); +} + +bool AudioGraphProcessor::hasUncommittedChanges() const noexcept +{ + return pimpl->dirty.load(); +} + +AudioProcessor* AudioGraphProcessor::getNodeProcessor (AudioGraphNodeID nodeID) const noexcept +{ + return pimpl->getNodeProcessor (nodeID); +} + +void AudioGraphProcessor::prepareToPlay (float sampleRate, int maxBlockSize) +{ + pimpl->prepareToPlay (sampleRate, maxBlockSize); +} + +void AudioGraphProcessor::releaseResources() +{ + pimpl->releaseResources(); +} + +void AudioGraphProcessor::processBlock (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) +{ + pimpl->processBlock (audioBuffer, midiBuffer); +} + +void AudioGraphProcessor::flush() +{ + pimpl->flush(); +} + +int AudioGraphProcessor::getLatencySamples() +{ + return pimpl->latestLatencySamples.load(); +} + +} // namespace yup diff --git a/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.h b/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.h new file mode 100644 index 000000000..c1cd91d5c --- /dev/null +++ b/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.h @@ -0,0 +1,293 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +/** + Opaque identifier for a processor node owned by an AudioGraphProcessor. + + Node identifiers remain stable until the node is removed. The invalid identifier + is used by graph input and graph output endpoints, and by failed addNode calls. +*/ +class AudioGraphNodeID +{ +public: + /** Creates an invalid node identifier. */ + constexpr AudioGraphNodeID() noexcept = default; + + /** Returns an invalid node identifier. */ + static constexpr AudioGraphNodeID invalid() noexcept { return {}; } + + /** Returns true when this identifier names a graph node. */ + constexpr bool isValid() const noexcept { return value != 0; } + + /** Returns the raw integer identifier. */ + constexpr uint64_t getRawID() const noexcept { return value; } + + /** Creates an identifier from a raw value. Prefer IDs returned by addNode(). */ + explicit constexpr AudioGraphNodeID (uint64_t rawValue) noexcept + : value (rawValue) + { + } + + constexpr bool operator== (AudioGraphNodeID other) const noexcept { return value == other.value; } + + constexpr bool operator!= (AudioGraphNodeID other) const noexcept { return value != other.value; } + + constexpr bool operator< (AudioGraphNodeID other) const noexcept { return value < other.value; } + +private: + uint64_t value = 0; +}; + +//============================================================================== +/** + Describes one routable audio or MIDI endpoint in an AudioGraphProcessor. + + Source endpoints are graphInput() and nodeOutput(). Destination endpoints are + graphOutput() and nodeInput(). The bus index refers to the AudioBusLayout of the + graph or the addressed processor node. +*/ +class AudioGraphEndpoint +{ +public: + /** The endpoint owner and direction. */ + enum class Kind + { + graphInput, + graphOutput, + nodeInput, + nodeOutput + }; + + /** Creates an invalid graph input endpoint. */ + AudioGraphEndpoint() = default; + + /** Creates a source endpoint for one graph input bus. */ + static AudioGraphEndpoint graphInput (int busIndex) noexcept + { + return AudioGraphEndpoint (Kind::graphInput, AudioGraphNodeID::invalid(), busIndex); + } + + /** Creates a destination endpoint for one graph output bus. */ + static AudioGraphEndpoint graphOutput (int busIndex) noexcept + { + return AudioGraphEndpoint (Kind::graphOutput, AudioGraphNodeID::invalid(), busIndex); + } + + /** Creates a destination endpoint for one node input bus. */ + static AudioGraphEndpoint nodeInput (AudioGraphNodeID nodeID, int busIndex) noexcept + { + return AudioGraphEndpoint (Kind::nodeInput, nodeID, busIndex); + } + + /** Creates a source endpoint for one node output bus. */ + static AudioGraphEndpoint nodeOutput (AudioGraphNodeID nodeID, int busIndex) noexcept + { + return AudioGraphEndpoint (Kind::nodeOutput, nodeID, busIndex); + } + + /** Returns the endpoint kind. */ + Kind getKind() const noexcept { return kind; } + + /** Returns the addressed node, or an invalid ID for graph endpoints. */ + AudioGraphNodeID getNodeID() const noexcept { return nodeID; } + + /** Returns the bus index on the graph or processor layout. */ + int getBusIndex() const noexcept { return busIndex; } + + /** Returns true when this endpoint can appear as a connection source. */ + bool isSource() const noexcept { return kind == Kind::graphInput || kind == Kind::nodeOutput; } + + /** Returns true when this endpoint can appear as a connection destination. */ + bool isDestination() const noexcept { return kind == Kind::graphOutput || kind == Kind::nodeInput; } + + bool operator== (const AudioGraphEndpoint& other) const noexcept + { + return kind == other.kind && nodeID == other.nodeID && busIndex == other.busIndex; + } + + bool operator!= (const AudioGraphEndpoint& other) const noexcept { return ! (*this == other); } + +private: + AudioGraphEndpoint (Kind endpointKind, AudioGraphNodeID endpointNodeID, int endpointBusIndex) noexcept + : kind (endpointKind) + , nodeID (endpointNodeID) + , busIndex (endpointBusIndex) + { + } + + Kind kind = Kind::graphInput; + AudioGraphNodeID nodeID; + int busIndex = -1; +}; + +//============================================================================== +/** + A directed connection between two graph endpoints. +*/ +class AudioGraphConnection +{ +public: + AudioGraphConnection() = default; + + /** Creates a connection from sourceEndpoint to destinationEndpoint. */ + AudioGraphConnection (AudioGraphEndpoint sourceEndpoint, AudioGraphEndpoint destinationEndpoint) noexcept + : source (sourceEndpoint) + , destination (destinationEndpoint) + { + } + + bool operator== (const AudioGraphConnection& other) const noexcept + { + return source == other.source && destination == other.destination; + } + + bool operator!= (const AudioGraphConnection& other) const noexcept { return ! (*this == other); } + + /** Source endpoint. */ + AudioGraphEndpoint source; + + /** Destination endpoint. */ + AudioGraphEndpoint destination; +}; + +//============================================================================== +/** + Lightweight diagnostics describing the most recently compiled graph plan. +*/ +struct AudioGraphAllocationStats +{ + /** Number of preallocated node scratch audio buffers. */ + int scratchAudioBuffers = 0; + + /** Number of preallocated node MIDI buffers. */ + int midiBuffers = 0; + + /** Number of per-connection delay lines. */ + int delayLines = 0; + + /** Maximum latency compensation inserted on any graph output path. */ + int totalCompensationSamples = 0; + + /** Maximum preallocated audio channel count used by any node or graph endpoint. */ + int maxPreallocatedChannels = 0; + + /** Maximum preallocated block size for the compiled plan. */ + int maxPreallocatedBlockSize = 0; +}; + +//============================================================================== +/** + An AudioProcessor that owns and executes an acyclic graph of AudioProcessor nodes. + + Edits are made to a control-thread graph model. commitChanges() validates the + model, prepares newly compiled nodes for the current playback configuration, and + publishes an immutable processing plan. processBlock() only swaps pending plans at + block boundaries and keeps retired plans alive until a later control-thread commit + or destruction. +*/ +class YUP_API AudioGraphProcessor final : public AudioProcessor +{ +public: + /** Creates the default stereo graph bus layout. */ + static AudioBusLayout createDefaultBusLayout(); + + /** Constructs an audio graph with the supplied graph input/output bus layout. */ + explicit AudioGraphProcessor (AudioBusLayout busLayout = createDefaultBusLayout()); + + /** Destructs the graph and releases all owned processors. */ + ~AudioGraphProcessor() override; + + /** Adds a processor node and returns its stable node identifier. */ + AudioGraphNodeID addNode (std::unique_ptr processor); + + /** Removes a processor node and all connections that mention it. */ + bool removeNode (AudioGraphNodeID nodeID); + + /** Adds a connection to the control-thread graph model. */ + Result addConnection (const AudioGraphConnection& connection); + + /** Removes a connection from the control-thread graph model. */ + bool removeConnection (const AudioGraphConnection& connection); + + /** Returns a snapshot of the current control-thread graph model connections. */ + std::vector getConnections() const; + + /** Removes all graph nodes and connections. */ + void clear(); + + /** Validates, compiles, and publishes the current graph model. */ + Result commitChanges(); + + /** Sets the desired worker thread count for future processing blocks. */ + void setNumWorkerThreads (int numThreads); + + /** Returns the desired worker thread count. */ + int getNumWorkerThreads() const noexcept; + + /** Supplies the audio workgroup that worker threads should join when supported. */ + void setAudioWorkgroup (AudioWorkgroup workgroup); + + /** Returns diagnostics for the last successfully compiled graph. */ + AudioGraphAllocationStats getAllocationStats() const noexcept; + + /** Returns true when graph edits have not yet been committed. */ + bool hasUncommittedChanges() const noexcept; + + /** Returns the processor for a node, or nullptr when the node is not present. */ + AudioProcessor* getNodeProcessor (AudioGraphNodeID nodeID) const noexcept; + + void prepareToPlay (float sampleRate, int maxBlockSize) override; + void releaseResources() override; + void processBlock (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) override; + void flush() override; + + int getLatencySamples() override; + + int getCurrentPreset() const noexcept override { return 0; } + + void setCurrentPreset (int) noexcept override {} + + int getNumPresets() const override { return 0; } + + String getPresetName (int) const override { return {}; } + + void setPresetName (int, StringRef) override {} + + Result loadStateFromMemory (const MemoryBlock&) override { return Result::ok(); } + + Result saveStateIntoMemory (MemoryBlock&) override { return Result::ok(); } + + bool hasEditor() const override { return false; } + + AudioProcessorEditor* createEditor() override { return nullptr; } + +private: + class Pimpl; + std::unique_ptr pimpl; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioGraphProcessor) +}; + +} // namespace yup diff --git a/modules/yup_audio_graph/yup_audio_graph.cpp b/modules/yup_audio_graph/yup_audio_graph.cpp new file mode 100644 index 000000000..5f3576af7 --- /dev/null +++ b/modules/yup_audio_graph/yup_audio_graph.cpp @@ -0,0 +1,34 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#ifdef YUP_AUDIO_GRAPH_H_INCLUDED +/* When you add this cpp file to your project, you mustn't include it in a file where you've + already included any other headers - just put it inside a file on its own, possibly with your config + flags preceding it, but don't include anything else. That also includes avoiding any automatic prefix + header files that the compiler may be using. +*/ +#error "Incorrect use of YUP cpp file" +#endif + +#include "yup_audio_graph.h" + +//============================================================================== +#include "graph/yup_AudioGraphProcessor.cpp" diff --git a/modules/yup_audio_graph/yup_audio_graph.h b/modules/yup_audio_graph/yup_audio_graph.h new file mode 100644 index 000000000..6689929b8 --- /dev/null +++ b/modules/yup_audio_graph/yup_audio_graph.h @@ -0,0 +1,59 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +/* + ============================================================================== + + BEGIN_YUP_MODULE_DECLARATION + + ID: yup_audio_graph + vendor: yup + version: 1.0.0 + name: YUP Audio Graph + description: AudioProcessor-based audio and MIDI processing graph. + website: https://github.com/kunitoki/yup + license: ISC + minimumCppStandard: 17 + + dependencies: yup_audio_processors + searchpaths: native + + END_YUP_MODULE_DECLARATION + + ============================================================================== +*/ + +#pragma once +#define YUP_AUDIO_GRAPH_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +//============================================================================== +#include "graph/yup_AudioGraphProcessor.h" diff --git a/modules/yup_audio_graph/yup_audio_graph.mm b/modules/yup_audio_graph/yup_audio_graph.mm new file mode 100644 index 000000000..1b195b8cd --- /dev/null +++ b/modules/yup_audio_graph/yup_audio_graph.mm @@ -0,0 +1,22 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include "yup_audio_graph.cpp" diff --git a/modules/yup_audio_gui/graph/yup_AudioGraphComponent.cpp b/modules/yup_audio_gui/graph/yup_AudioGraphComponent.cpp new file mode 100644 index 000000000..64de751c4 --- /dev/null +++ b/modules/yup_audio_gui/graph/yup_AudioGraphComponent.cpp @@ -0,0 +1,922 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +namespace +{ +constexpr float minZoom = 0.1f; +constexpr float maxZoom = 4.0f; +constexpr float wireHitDistance = 8.0f; +constexpr float dragWireThreshold = 4.0f; + +bool endpointsMatch (const AudioGraphEndpoint& a, const AudioGraphEndpoint& b) noexcept +{ + return a == b; +} +} // namespace + +//============================================================================== +AudioGraphComponent::AudioGraphComponent (std::shared_ptr graphIn) + : graph (std::move (graphIn)) +{ + setOpaque (true); + setWantsKeyboardFocus (true); + setWantsMouseEvents (true, true); + enableRenderingUnclipped (true); +} + +AudioGraphComponent::~AudioGraphComponent() +{ + for (auto& node : nodes) + { + if (node.view != nullptr) + node.view->removeMouseListener (this); + } +} + +//============================================================================== +void AudioGraphComponent::addNodeView (AudioGraphNodeID nodeID, std::unique_ptr view, Point initialCanvasPosition) +{ + if (view == nullptr) + return; + + removeNodeView (nodeID); + + view->setViewScale (zoom); + view->addMouseListener (this); + addAndMakeVisible (*view); + + nodes.push_back ({ nodeID, std::move (view), initialCanvasPosition, NodeItem::Kind::processor }); + updateNodeBounds(); + repaint(); +} + +void AudioGraphComponent::setGraphInputView (std::unique_ptr view, Point initialCanvasPosition) +{ + if (view == nullptr) + return; + + auto* item = findNodeItemByKind (NodeItem::Kind::graphInput); + if (item != nullptr) + { + if (item->view != nullptr) + { + item->view->removeMouseListener (this); + removeChildComponent (item->view.get()); + } + + item->view = std::move (view); + item->canvasPosition = initialCanvasPosition; + } + else + { + nodes.push_back ({ AudioGraphNodeID::invalid(), std::move (view), initialCanvasPosition, NodeItem::Kind::graphInput }); + item = &nodes.back(); + } + + item->view->setViewScale (zoom); + item->view->addMouseListener (this); + addAndMakeVisible (*item->view); + updateNodeBounds(); + repaint(); +} + +void AudioGraphComponent::setGraphOutputView (std::unique_ptr view, Point initialCanvasPosition) +{ + if (view == nullptr) + return; + + auto* item = findNodeItemByKind (NodeItem::Kind::graphOutput); + if (item != nullptr) + { + if (item->view != nullptr) + { + item->view->removeMouseListener (this); + removeChildComponent (item->view.get()); + } + + item->view = std::move (view); + item->canvasPosition = initialCanvasPosition; + } + else + { + nodes.push_back ({ AudioGraphNodeID::invalid(), std::move (view), initialCanvasPosition, NodeItem::Kind::graphOutput }); + item = &nodes.back(); + } + + item->view->setViewScale (zoom); + item->view->addMouseListener (this); + addAndMakeVisible (*item->view); + updateNodeBounds(); + repaint(); +} + +void AudioGraphComponent::removeNodeView (AudioGraphNodeID nodeID) +{ + const auto iterator = std::find_if (nodes.begin(), nodes.end(), [nodeID] (const NodeItem& item) + { + return item.kind == NodeItem::Kind::processor && item.nodeID == nodeID; + }); + + if (iterator == nodes.end()) + return; + + if (iterator->view != nullptr) + { + iterator->view->removeMouseListener (this); + removeChildComponent (iterator->view.get()); + } + + nodes.erase (iterator); + repaint(); +} + +AudioGraphNodeView* AudioGraphComponent::getNodeView (AudioGraphNodeID nodeID) const noexcept +{ + const auto iterator = std::find_if (nodes.begin(), nodes.end(), [nodeID] (const NodeItem& item) + { + return item.kind == NodeItem::Kind::processor && item.nodeID == nodeID; + }); + + return iterator != nodes.end() ? iterator->view.get() : nullptr; +} + +//============================================================================== +void AudioGraphComponent::setZoom (float newZoom) +{ + const auto clampedZoom = jlimit (minZoom, maxZoom, newZoom); + if (zoom == clampedZoom) + return; + + needsInitialReset = false; + needsInitialZoomToFit = false; + zoom = clampedZoom; + updateNodeBounds(); + repaint(); +} + +void AudioGraphComponent::setCanvasOffset (Point offset) +{ + needsInitialReset = false; + needsInitialZoomToFit = false; + canvasOffset = offset; + updateNodeBounds(); + repaint(); +} + +void AudioGraphComponent::resetView() +{ + if (getWidth() <= 0.0f || getHeight() <= 0.0f) + { + needsInitialReset = true; + needsInitialZoomToFit = false; + return; + } + + needsInitialReset = false; + needsInitialZoomToFit = false; + zoom = 1.0f; + + centerViewOnNodes(); +} + +void AudioGraphComponent::centerViewOnNodes() +{ + if (getWidth() <= 0.0f || getHeight() <= 0.0f) + { + needsInitialReset = true; + needsInitialZoomToFit = false; + return; + } + + needsInitialReset = false; + needsInitialZoomToFit = false; + + const auto bounds = getNodesCanvasBounds(); + if (! bounds.has_value()) + { + canvasOffset = getLocalBounds().getCenter(); + updateNodeBounds(); + repaint(); + return; + } + + canvasOffset = getLocalBounds().getCenter() - (bounds->getCenter() * zoom); + updateNodeBounds(); + repaint(); +} + +void AudioGraphComponent::zoomToFitNodes (float padding, float maximumZoom) +{ + if (getWidth() <= 0.0f || getHeight() <= 0.0f) + { + needsInitialReset = false; + needsInitialZoomToFit = true; + pendingZoomToFitPadding = padding; + pendingZoomToFitMaximumZoom = maximumZoom; + return; + } + + needsInitialReset = false; + needsInitialZoomToFit = false; + const auto upperZoom = jlimit (minZoom, maxZoom, maximumZoom); + + const auto bounds = getNodesCanvasBounds(); + if (! bounds.has_value()) + { + zoom = jlimit (minZoom, upperZoom, 1.0f); + canvasOffset = getLocalBounds().getCenter(); + updateNodeBounds(); + repaint(); + return; + } + + const auto safePadding = jmax (0.0f, padding); + const auto availableWidth = jmax (1.0f, static_cast (getWidth()) - (safePadding * 2.0f)); + const auto availableHeight = jmax (1.0f, static_cast (getHeight()) - (safePadding * 2.0f)); + const auto fitZoom = jmin (availableWidth / jmax (1.0f, bounds->getWidth()), + availableHeight / jmax (1.0f, bounds->getHeight())); + + zoom = jlimit (minZoom, upperZoom, fitZoom); + canvasOffset = getLocalBounds().getCenter() - (bounds->getCenter() * zoom); + updateNodeBounds(); + repaint(); +} + +Point AudioGraphComponent::canvasToScreen (Point canvasPos) const +{ + return (canvasPos * zoom) + canvasOffset; +} + +Point AudioGraphComponent::screenToCanvas (Point screenPos) const +{ + return (screenPos - canvasOffset) / zoom; +} + +void AudioGraphComponent::addListener (Listener* listener) +{ + listeners.add (listener); +} + +void AudioGraphComponent::removeListener (Listener* listener) +{ + listeners.remove (listener); +} + +//============================================================================== +void AudioGraphComponent::resized() +{ + if (needsInitialZoomToFit) + zoomToFitNodes (pendingZoomToFitPadding, pendingZoomToFitMaximumZoom); + else if (needsInitialReset || canvasOffset.isOrigin()) + resetView(); + else + updateNodeBounds(); +} + +void AudioGraphComponent::paint (Graphics& g) +{ + g.setFillColor (Color (0xff101522)); + g.fillAll(); + + drawGrid (g); + + if (graph != nullptr) + { + for (const auto& connection : graph->getConnections()) + drawConnection (g, connection, 1.0f); + } + + drawPendingWire (g); +} + +//============================================================================== +void AudioGraphComponent::mouseDown (const MouseEvent& event) +{ + takeKeyboardFocus(); + + const auto screenPos = eventPositionInThisComponent (event); + mouseDownScreen = screenPos; + lastMouseScreen = screenPos; + + if (event.isRightButtonDown()) + { + if (auto endpoint = hitTestEndpoint (screenPos)) + { + removeConnectionsForEndpoint (endpoint->endpoint); + return; + } + + if (auto connection = hitTestConnection (screenPos)) + removeConnectionAndCommit (*connection); + + return; + } + + if (spacebarDown) + { + interaction = Interaction::panningCanvas; + panStartOffset = canvasOffset; + return; + } + + if (auto endpoint = hitTestEndpoint (screenPos)) + { + if (interaction == Interaction::armedPort && activeEndpoint.has_value()) + { + if (tryConnect (*activeEndpoint, endpoint->endpoint)) + { + interaction = Interaction::idle; + activeEndpoint.reset(); + } + else + { + activeEndpoint = endpoint->endpoint; + interaction = Interaction::armedPort; + } + } + else + { + activeEndpoint = endpoint->endpoint; + interaction = Interaction::armedPort; + } + + pendingWireEnd = screenPos; + repaint(); + return; + } + + if (interaction == Interaction::armedPort) + { + interaction = Interaction::idle; + activeEndpoint.reset(); + repaint(); + return; + } + + auto* source = event.getSourceComponent(); + if (auto* nodeView = dynamic_cast (source)) + { + if (auto* item = findNodeItemForView (nodeView)) + { + draggedNodeIndex = static_cast (item - nodes.data()); + dragStartCanvasMouse = screenToCanvas (screenPos); + dragStartNodePosition = item->canvasPosition; + interaction = Interaction::draggingNode; + } + } +} + +void AudioGraphComponent::mouseDrag (const MouseEvent& event) +{ + const auto screenPos = eventPositionInThisComponent (event); + lastMouseScreen = screenPos; + + switch (interaction) + { + case Interaction::panningCanvas: + setCanvasOffset (panStartOffset + (screenPos - mouseDownScreen)); + break; + + case Interaction::draggingNode: + { + if (isPositiveAndBelow (draggedNodeIndex, static_cast (nodes.size()))) + { + nodes[static_cast (draggedNodeIndex)].canvasPosition = dragStartNodePosition + (screenToCanvas (screenPos) - dragStartCanvasMouse); + updateNodeBounds(); + repaint(); + } + + break; + } + + case Interaction::armedPort: + if (mouseDownScreen.distanceTo (screenPos) > dragWireThreshold) + interaction = Interaction::draggingWire; + + pendingWireEnd = screenPos; + repaint(); + break; + + case Interaction::draggingWire: + pendingWireEnd = screenPos; + repaint(); + break; + + case Interaction::idle: + break; + } +} + +void AudioGraphComponent::mouseUp (const MouseEvent& event) +{ + const auto screenPos = eventPositionInThisComponent (event); + lastMouseScreen = screenPos; + + switch (interaction) + { + case Interaction::draggingWire: + if (activeEndpoint.has_value()) + { + if (auto endpoint = hitTestEndpoint (screenPos)) + tryConnect (*activeEndpoint, endpoint->endpoint); + } + + activeEndpoint.reset(); + interaction = Interaction::idle; + repaint(); + break; + + case Interaction::draggingNode: + { + if (isPositiveAndBelow (draggedNodeIndex, static_cast (nodes.size()))) + { + const auto& item = nodes[static_cast (draggedNodeIndex)]; + + if (item.kind == NodeItem::Kind::processor) + listeners.call ([&] (Listener& listener) + { + listener.nodeViewMoved (item.nodeID, item.canvasPosition); + }); + } + + interaction = Interaction::idle; + draggedNodeIndex = -1; + break; + } + + case Interaction::panningCanvas: + interaction = Interaction::idle; + break; + + case Interaction::armedPort: + case Interaction::idle: + break; + } +} + +void AudioGraphComponent::mouseWheel (const MouseEvent& event, const MouseWheelData& wheelData) +{ + const auto screenPos = eventPositionInThisComponent (event); + const auto canvasPos = screenToCanvas (screenPos); + const auto zoomFactor = std::pow (1.12f, wheelData.getDeltaY()); + const auto newZoom = jlimit (minZoom, maxZoom, zoom * zoomFactor); + + if (newZoom == zoom) + return; + + needsInitialReset = false; + needsInitialZoomToFit = false; + zoom = newZoom; + canvasOffset = screenPos - (canvasPos * zoom); + updateNodeBounds(); + repaint(); +} + +void AudioGraphComponent::keyDown (const KeyPress& keys, const Point&) +{ + if (keys.getKey() == KeyPress::spaceKey) + spacebarDown = true; +} + +void AudioGraphComponent::keyUp (const KeyPress& keys, const Point&) +{ + if (keys.getKey() == KeyPress::spaceKey) + spacebarDown = false; +} + +//============================================================================== +void AudioGraphComponent::updateNodeBounds() +{ + for (auto& node : nodes) + { + if (node.view == nullptr) + continue; + + node.view->setViewScale (zoom); + + const auto canvasBounds = getNodeCanvasBounds (node); + const auto screenTopLeft = canvasToScreen (canvasBounds.getPosition()); + node.view->setBounds ({ screenTopLeft, canvasBounds.getSize() * zoom }); + } +} + +Rectangle AudioGraphComponent::getNodeCanvasBounds (const NodeItem& item) const +{ + if (item.view == nullptr) + return {}; + + return { item.canvasPosition, + static_cast (item.view->getPreferredWidth()), + static_cast (item.view->getPreferredHeight()) }; +} + +std::optional> AudioGraphComponent::getNodesCanvasBounds() const +{ + std::optional> result; + + for (const auto& node : nodes) + { + if (node.view == nullptr) + continue; + + const auto bounds = getNodeCanvasBounds (node); + if (bounds.isEmpty()) + continue; + + if (! result.has_value()) + { + result = bounds; + continue; + } + + const auto left = jmin (result->getLeft(), bounds.getLeft()); + const auto top = jmin (result->getTop(), bounds.getTop()); + const auto right = jmax (result->getRight(), bounds.getRight()); + const auto bottom = jmax (result->getBottom(), bounds.getBottom()); + result = Rectangle { left, top, right - left, bottom - top }; + } + + return result; +} + +Point AudioGraphComponent::eventPositionInThisComponent (const MouseEvent& event) const +{ + if (event.getSourceComponent() == this || event.getSourceComponent() == nullptr) + return event.getPosition(); + + return getLocalPoint (event.getSourceComponent(), event.getPosition()); +} + +AudioGraphComponent::NodeItem* AudioGraphComponent::findNodeItemForView (const AudioGraphNodeView* view) noexcept +{ + const auto iterator = std::find_if (nodes.begin(), nodes.end(), [view] (const NodeItem& item) + { + return item.view.get() == view; + }); + + return iterator != nodes.end() ? &*iterator : nullptr; +} + +const AudioGraphComponent::NodeItem* AudioGraphComponent::findNodeItemForView (const AudioGraphNodeView* view) const noexcept +{ + const auto iterator = std::find_if (nodes.begin(), nodes.end(), [view] (const NodeItem& item) + { + return item.view.get() == view; + }); + + return iterator != nodes.end() ? &*iterator : nullptr; +} + +AudioGraphComponent::NodeItem* AudioGraphComponent::findNodeItemByKind (NodeItem::Kind kind) noexcept +{ + const auto iterator = std::find_if (nodes.begin(), nodes.end(), [kind] (const NodeItem& item) + { + return item.kind == kind; + }); + + return iterator != nodes.end() ? &*iterator : nullptr; +} + +const AudioGraphComponent::NodeItem* AudioGraphComponent::findNodeItemByKind (NodeItem::Kind kind) const noexcept +{ + const auto iterator = std::find_if (nodes.begin(), nodes.end(), [kind] (const NodeItem& item) + { + return item.kind == kind; + }); + + return iterator != nodes.end() ? &*iterator : nullptr; +} + +std::optional AudioGraphComponent::hitTestEndpoint (Point screenPos) const +{ + for (const auto& node : nodes) + { + if (node.view == nullptr) + continue; + + const auto localPos = node.view->getLocalPoint (this, screenPos); + if (auto port = node.view->hitTestPort (localPos)) + { + AudioGraphEndpoint endpoint; + + if (node.kind == NodeItem::Kind::graphInput) + endpoint = AudioGraphEndpoint::graphInput (port->busIndex); + else if (node.kind == NodeItem::Kind::graphOutput) + endpoint = AudioGraphEndpoint::graphOutput (port->busIndex); + else + endpoint = port->isInput + ? AudioGraphEndpoint::nodeInput (node.nodeID, port->busIndex) + : AudioGraphEndpoint::nodeOutput (node.nodeID, port->busIndex); + + return EndpointHit { endpoint, *port, node.view.get() }; + } + } + + return {}; +} + +std::optional AudioGraphComponent::hitTestConnection (Point screenPos) const +{ + if (graph == nullptr) + return {}; + + for (const auto& connection : graph->getConnections()) + { + const auto start = getEndpointScreenPosition (connection.source); + const auto end = getEndpointScreenPosition (connection.destination); + const auto controlOffset = jmax (60.0f, std::abs (end.getX() - start.getX()) * 0.5f); + const auto cp1 = Point { start.getX() + controlOffset, start.getY() }; + const auto cp2 = Point { end.getX() - controlOffset, end.getY() }; + + auto previous = start; + const auto approximateLength = jmax (40.0f, start.distanceTo (end)); + const auto numSteps = jmax (8, roundToInt (approximateLength / 10.0f)); + + for (int i = 1; i <= numSteps; ++i) + { + const auto t = static_cast (i) / static_cast (numSteps); + const auto current = cubicPoint (start, cp1, cp2, end, t); + + if (distanceToSegment (screenPos, previous, current) <= wireHitDistance) + return connection; + + previous = current; + } + } + + return {}; +} + +Point AudioGraphComponent::getEndpointScreenPosition (const AudioGraphEndpoint& endpoint) const +{ + if (endpoint.getKind() == AudioGraphEndpoint::Kind::graphInput || endpoint.getKind() == AudioGraphEndpoint::Kind::graphOutput) + { + if (const auto* item = findNodeItemByKind (endpoint.getKind() == AudioGraphEndpoint::Kind::graphInput ? NodeItem::Kind::graphInput : NodeItem::Kind::graphOutput)) + { + if (item->view != nullptr) + { + const auto local = endpoint.getKind() == AudioGraphEndpoint::Kind::graphInput + ? item->view->getOutputPortCenter (endpoint.getBusIndex()) + : item->view->getInputPortCenter (endpoint.getBusIndex()); + + return getLocalPoint (item->view.get(), local); + } + } + + auto left = 0.0f; + auto right = 720.0f; + auto top = 96.0f; + + if (! nodes.empty()) + { + left = std::numeric_limits::max(); + right = std::numeric_limits::lowest(); + top = std::numeric_limits::max(); + + for (const auto& node : nodes) + { + const auto bounds = getNodeCanvasBounds (node); + left = jmin (left, bounds.getLeft()); + right = jmax (right, bounds.getRight()); + top = jmin (top, bounds.getTop()); + } + } + + const auto x = endpoint.getKind() == AudioGraphEndpoint::Kind::graphInput ? left - 96.0f : right + 96.0f; + const auto y = top + 60.0f + static_cast (endpoint.getBusIndex()) * 36.0f; + return canvasToScreen ({ x, y }); + } + + if (auto* view = getNodeView (endpoint.getNodeID())) + { + const auto local = endpoint.getKind() == AudioGraphEndpoint::Kind::nodeInput + ? view->getInputPortCenter (endpoint.getBusIndex()) + : view->getOutputPortCenter (endpoint.getBusIndex()); + + return getLocalPoint (view, local); + } + + return {}; +} + +Color AudioGraphComponent::getEndpointColor (const AudioGraphEndpoint& endpoint) const +{ + if (endpoint.getKind() == AudioGraphEndpoint::Kind::graphInput || endpoint.getKind() == AudioGraphEndpoint::Kind::graphOutput) + { + if (const auto* item = findNodeItemByKind (endpoint.getKind() == AudioGraphEndpoint::Kind::graphInput ? NodeItem::Kind::graphInput : NodeItem::Kind::graphOutput)) + { + if (item->view != nullptr) + { + return endpoint.getKind() == AudioGraphEndpoint::Kind::graphInput + ? item->view->getOutputPortInfo (endpoint.getBusIndex()).color + : item->view->getInputPortInfo (endpoint.getBusIndex()).color; + } + } + } + + if (auto* view = getNodeView (endpoint.getNodeID())) + { + if (endpoint.getKind() == AudioGraphEndpoint::Kind::nodeInput) + return view->getInputPortInfo (endpoint.getBusIndex()).color; + + if (endpoint.getKind() == AudioGraphEndpoint::Kind::nodeOutput) + return view->getOutputPortInfo (endpoint.getBusIndex()).color; + } + + return AudioGraphNodeView::getPortKindColor (AudioGraphNodeView::PortKind::audio); +} + +bool AudioGraphComponent::tryConnect (const AudioGraphEndpoint& first, const AudioGraphEndpoint& second) +{ + if (graph == nullptr || ! isCompatiblePair (first, second)) + return false; + + const auto connection = makeConnection (first, second); + + if (graph->addConnection (connection).failed()) + return false; + + if (graph->commitChanges().failed()) + { + graph->removeConnection (connection); + graph->commitChanges(); + return false; + } + + listeners.call ([&] (Listener& listener) + { + listener.connectionAdded (connection); + }); + repaint(); + return true; +} + +bool AudioGraphComponent::removeConnectionAndCommit (const AudioGraphConnection& connection) +{ + if (graph == nullptr || ! graph->removeConnection (connection)) + return false; + + if (graph->commitChanges().failed()) + { + graph->addConnection (connection); + graph->commitChanges(); + return false; + } + + listeners.call ([&] (Listener& listener) + { + listener.connectionRemoved (connection); + }); + repaint(); + return true; +} + +void AudioGraphComponent::removeConnectionsForEndpoint (const AudioGraphEndpoint& endpoint) +{ + if (graph == nullptr) + return; + + const auto connections = graph->getConnections(); + for (const auto& connection : connections) + { + if (endpointsMatch (connection.source, endpoint) || endpointsMatch (connection.destination, endpoint)) + removeConnectionAndCommit (connection); + } +} + +bool AudioGraphComponent::isCompatiblePair (const AudioGraphEndpoint& first, const AudioGraphEndpoint& second) const noexcept +{ + return (first.isSource() && second.isDestination()) || (second.isSource() && first.isDestination()); +} + +AudioGraphConnection AudioGraphComponent::makeConnection (const AudioGraphEndpoint& first, const AudioGraphEndpoint& second) const noexcept +{ + return first.isSource() ? AudioGraphConnection { first, second } + : AudioGraphConnection { second, first }; +} + +//============================================================================== +void AudioGraphComponent::drawGrid (Graphics& g) +{ + const auto spacing = 24.0f * zoom; + if (spacing < 6.0f) + return; + + const auto startX = std::fmod (canvasOffset.getX(), spacing); + const auto startY = std::fmod (canvasOffset.getY(), spacing); + + g.setFillColor (Colors::white.withAlpha (0.045f)); + + for (auto x = startX; x < getWidth(); x += spacing) + { + for (auto y = startY; y < getHeight(); y += spacing) + g.fillEllipse (x - 1.0f, y - 1.0f, 2.0f, 2.0f); + } +} + +void AudioGraphComponent::drawConnection (Graphics& g, const AudioGraphConnection& connection, float opacity) +{ + const auto start = getEndpointScreenPosition (connection.source); + const auto end = getEndpointScreenPosition (connection.destination); + + if (! getLocalBounds().reduced (-200.0f).contains (start) && ! getLocalBounds().reduced (-200.0f).contains (end)) + return; + + const auto controlOffset = jmax (60.0f * zoom, std::abs (end.getX() - start.getX()) * 0.5f); + const auto cp1 = Point { start.getX() + controlOffset, start.getY() }; + const auto cp2 = Point { end.getX() - controlOffset, end.getY() }; + + drawBezierHalf (g, start, cp1, cp2, end, getEndpointColor (connection.source).withMultipliedAlpha (opacity), true); + drawBezierHalf (g, start, cp1, cp2, end, getEndpointColor (connection.destination).withMultipliedAlpha (opacity), false); +} + +void AudioGraphComponent::drawBezierHalf (Graphics& g, Point p0, Point p1, Point p2, Point p3, Color color, bool firstHalf) +{ + const auto p01 = (p0 + p1) * 0.5f; + const auto p12 = (p1 + p2) * 0.5f; + const auto p23 = (p2 + p3) * 0.5f; + const auto p012 = (p01 + p12) * 0.5f; + const auto p123 = (p12 + p23) * 0.5f; + const auto midpoint = (p012 + p123) * 0.5f; + + Path path; + if (firstHalf) + path.moveTo (p0).cubicTo (p01, p012.getX(), p012.getY(), midpoint.getX(), midpoint.getY()); + else + path.moveTo (midpoint).cubicTo (p123, p23.getX(), p23.getY(), p3.getX(), p3.getY()); + + g.setStrokeColor (color); + g.setStrokeWidth (jmax (1.5f, 3.0f * zoom)); + g.setStrokeCap (StrokeCap::Round); + g.strokePath (path); +} + +void AudioGraphComponent::drawPendingWire (Graphics& g) +{ + if (! activeEndpoint.has_value() || (interaction != Interaction::armedPort && interaction != Interaction::draggingWire)) + return; + + const auto start = getEndpointScreenPosition (*activeEndpoint); + const auto end = interaction == Interaction::draggingWire ? pendingWireEnd : lastMouseScreen; + const auto controlOffset = jmax (60.0f * zoom, std::abs (end.getX() - start.getX()) * 0.5f); + const auto cp1 = Point { start.getX() + (activeEndpoint->isSource() ? controlOffset : -controlOffset), start.getY() }; + const auto cp2 = Point { end.getX() - (activeEndpoint->isSource() ? controlOffset : -controlOffset), end.getY() }; + + Path path; + path.moveTo (start).cubicTo (cp1, cp2.getX(), cp2.getY(), end.getX(), end.getY()); + + g.setStrokeColor (getEndpointColor (*activeEndpoint).withAlpha (0.55f)); + g.setStrokeWidth (jmax (1.5f, 2.5f * zoom)); + g.setStrokeCap (StrokeCap::Round); + g.strokePath (path); + + g.setFillColor (getEndpointColor (*activeEndpoint).withAlpha (0.20f)); + const auto center = getEndpointScreenPosition (*activeEndpoint); + const auto radius = 14.0f * zoom; + g.fillEllipse (center.getX() - radius, center.getY() - radius, radius * 2.0f, radius * 2.0f); +} + +Point AudioGraphComponent::cubicPoint (Point p0, Point p1, Point p2, Point p3, float t) const noexcept +{ + const auto u = 1.0f - t; + return (p0 * (u * u * u)) + + (p1 * (3.0f * u * u * t)) + + (p2 * (3.0f * u * t * t)) + + (p3 * (t * t * t)); +} + +float AudioGraphComponent::distanceToSegment (Point point, Point start, Point end) const noexcept +{ + const auto segment = end - start; + const auto lengthSquared = segment.distanceToSquared ({}); + + if (lengthSquared <= 0.0f) + return point.distanceTo (start); + + const auto pointVector = point - start; + const auto t = jlimit (0.0f, 1.0f, ((pointVector.getX() * segment.getX()) + (pointVector.getY() * segment.getY())) / lengthSquared); + return point.distanceTo (start + (segment * t)); +} + +} // namespace yup diff --git a/modules/yup_audio_gui/graph/yup_AudioGraphComponent.h b/modules/yup_audio_gui/graph/yup_AudioGraphComponent.h new file mode 100644 index 000000000..5abae9b5c --- /dev/null +++ b/modules/yup_audio_gui/graph/yup_AudioGraphComponent.h @@ -0,0 +1,222 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +/** + Zoomable and pannable editor canvas for an AudioGraphProcessor. + + The component owns only the visual node views. The AudioGraphProcessor remains + the source of truth for nodes and connections. + + @see AudioGraphNodeView, AudioGraphProcessor +*/ +class YUP_API AudioGraphComponent : public Component +{ +public: + /** Receives notifications for graph editor gestures that committed changes. */ + struct Listener + { + virtual ~Listener() = default; + + /** Called after a connection has been added and committed. */ + virtual void connectionAdded (const AudioGraphConnection&) {} + + /** Called after a connection has been removed and committed. */ + virtual void connectionRemoved (const AudioGraphConnection&) {} + + /** Called when a node view is released after dragging. */ + virtual void nodeViewMoved (AudioGraphNodeID, Point newCanvasPos) {} + }; + + /** Creates an editor for graph. */ + explicit AudioGraphComponent (std::shared_ptr graph); + + /** Destructor. */ + ~AudioGraphComponent() override; + + /** Adds or replaces the view for a node at an initial canvas position. */ + void addNodeView (AudioGraphNodeID nodeID, + std::unique_ptr view, + Point initialCanvasPosition); + + /** Adds or replaces the view that represents the graph input buses. */ + void setGraphInputView (std::unique_ptr view, + Point initialCanvasPosition); + + /** Adds or replaces the view that represents the graph output buses. */ + void setGraphOutputView (std::unique_ptr view, + Point initialCanvasPosition); + + /** Removes the view for a node. */ + void removeNodeView (AudioGraphNodeID nodeID); + + /** Returns the view for a node, or nullptr. */ + AudioGraphNodeView* getNodeView (AudioGraphNodeID nodeID) const noexcept; + + /** Sets the zoom factor, clamped to [0.1, 4.0]. */ + void setZoom (float zoom); + + /** Returns the zoom factor. */ + float getZoom() const noexcept { return zoom; } + + /** Sets the canvas-to-screen offset. */ + void setCanvasOffset (Point offset); + + /** Returns the canvas-to-screen offset. */ + Point getCanvasOffset() const noexcept { return canvasOffset; } + + /** Resets zoom and centers the current node views. */ + void resetView(); + + /** Centers the current node views without changing the zoom factor. */ + void centerViewOnNodes(); + + /** Zooms and centers the current node views so they fit in the viewport. */ + void zoomToFitNodes (float padding = 48.0f, float maximumZoom = 1.0f); + + /** Converts a canvas-space point to component-local screen space. */ + Point canvasToScreen (Point canvasPos) const; + + /** Converts component-local screen space to canvas space. */ + Point screenToCanvas (Point screenPos) const; + + /** Adds a listener. */ + void addListener (Listener* listener); + + /** Removes a listener. */ + void removeListener (Listener* listener); + + /** @internal */ + void resized() override; + + /** @internal */ + void paint (Graphics& g) override; + + /** @internal */ + void mouseDown (const MouseEvent& event) override; + + /** @internal */ + void mouseDrag (const MouseEvent& event) override; + + /** @internal */ + void mouseUp (const MouseEvent& event) override; + + /** @internal */ + void mouseWheel (const MouseEvent& event, const MouseWheelData& wheelData) override; + + /** @internal */ + void keyDown (const KeyPress& keys, const Point& position) override; + + /** @internal */ + void keyUp (const KeyPress& keys, const Point& position) override; + +private: + struct NodeItem + { + enum class Kind + { + processor, + graphInput, + graphOutput + }; + + AudioGraphNodeID nodeID; + std::unique_ptr view; + Point canvasPosition; + Kind kind = Kind::processor; + }; + + struct EndpointHit + { + AudioGraphEndpoint endpoint; + AudioGraphNodeView::PortHit port; + AudioGraphNodeView* view = nullptr; + }; + + enum class Interaction + { + idle, + panningCanvas, + draggingNode, + draggingWire, + armedPort + }; + + void updateNodeBounds(); + Rectangle getNodeCanvasBounds (const NodeItem& item) const; + std::optional> getNodesCanvasBounds() const; + + Point eventPositionInThisComponent (const MouseEvent& event) const; + NodeItem* findNodeItemForView (const AudioGraphNodeView* view) noexcept; + const NodeItem* findNodeItemForView (const AudioGraphNodeView* view) const noexcept; + NodeItem* findNodeItemByKind (NodeItem::Kind kind) noexcept; + const NodeItem* findNodeItemByKind (NodeItem::Kind kind) const noexcept; + std::optional hitTestEndpoint (Point screenPos) const; + std::optional hitTestConnection (Point screenPos) const; + + Point getEndpointScreenPosition (const AudioGraphEndpoint& endpoint) const; + Color getEndpointColor (const AudioGraphEndpoint& endpoint) const; + + bool tryConnect (const AudioGraphEndpoint& first, const AudioGraphEndpoint& second); + bool removeConnectionAndCommit (const AudioGraphConnection& connection); + void removeConnectionsForEndpoint (const AudioGraphEndpoint& endpoint); + + bool isCompatiblePair (const AudioGraphEndpoint& first, const AudioGraphEndpoint& second) const noexcept; + AudioGraphConnection makeConnection (const AudioGraphEndpoint& first, const AudioGraphEndpoint& second) const noexcept; + + void drawGrid (Graphics& g); + void drawConnection (Graphics& g, const AudioGraphConnection& connection, float opacity); + void drawBezierHalf (Graphics& g, Point p0, Point p1, Point p2, Point p3, Color color, bool firstHalf); + void drawPendingWire (Graphics& g); + + Point cubicPoint (Point p0, Point p1, Point p2, Point p3, float t) const noexcept; + float distanceToSegment (Point point, Point start, Point end) const noexcept; + + std::shared_ptr graph; + std::vector nodes; + ListenerList listeners; + + Interaction interaction = Interaction::idle; + float zoom = 1.0f; + Point canvasOffset { 0.0f, 0.0f }; + bool spacebarDown = false; + bool needsInitialReset = true; + bool needsInitialZoomToFit = false; + float pendingZoomToFitPadding = 48.0f; + float pendingZoomToFitMaximumZoom = 1.0f; + + Point mouseDownScreen; + Point lastMouseScreen; + Point panStartOffset; + Point dragStartCanvasMouse; + Point dragStartNodePosition; + int draggedNodeIndex = -1; + + std::optional activeEndpoint; + Point pendingWireEnd; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioGraphComponent) +}; + +} // namespace yup diff --git a/modules/yup_audio_gui/graph/yup_AudioGraphNodeView.cpp b/modules/yup_audio_gui/graph/yup_AudioGraphNodeView.cpp new file mode 100644 index 000000000..28a1454f0 --- /dev/null +++ b/modules/yup_audio_gui/graph/yup_AudioGraphNodeView.cpp @@ -0,0 +1,317 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +namespace +{ +constexpr float baseHeaderHeight = 32.0f; +constexpr float basePortRowHeight = 24.0f; +constexpr float basePortTopPadding = 8.0f; +constexpr float baseParameterRowHeight = 25.0f; +constexpr float basePortRadius = 6.0f; +constexpr float baseCornerRadius = 7.0f; +constexpr float baseContentHeight = 8.0f; + +Rectangle ellipseBounds (Point center, float radius) +{ + return { center.getX() - radius, center.getY() - radius, radius * 2.0f, radius * 2.0f }; +} + +void fillFeatheredRoundedRect (Graphics& g, Rectangle bounds, float corner, Color color, float viewScale) +{ + constexpr int numLayers = 5; + + for (int i = numLayers; i > 0; --i) + { + const auto amount = static_cast (i) * 1.4f * viewScale; + const auto alpha = 0.020f + (static_cast (numLayers - i) * 0.018f); + g.setFillColor (color.withAlpha (alpha)); + g.fillRoundedRect (bounds.reduced (-amount), corner + amount); + } +} + +void fillFeatheredEllipse (Graphics& g, Point center, float radius, Color color) +{ + constexpr int numLayers = 4; + + for (int i = numLayers; i > 0; --i) + { + const auto layerRadius = radius * (1.0f + (static_cast (i) * 0.22f)); + const auto alpha = 0.040f + (static_cast (numLayers - i) * 0.045f); + g.setFillColor (color.withAlpha (alpha)); + g.fillEllipse (ellipseBounds (center, layerRadius)); + } +} +} // namespace + +//============================================================================== +AudioGraphNodeView::AudioGraphNodeView (AudioGraphNodeID nodeIDIn) + : nodeID (nodeIDIn) +{ + enableRenderingUnclipped (true); + setWantsMouseEvents (true, true); +} + +AudioGraphNodeView::~AudioGraphNodeView() = default; + +//============================================================================== +Color AudioGraphNodeView::getNodeColor() const +{ + return Colors::dimgray; +} + +String AudioGraphNodeView::getNodeSubtitle() const +{ + return {}; +} + +AudioGraphNodeView::PortInfo AudioGraphNodeView::getInputPortInfo (int busIndex) const +{ + return { String ("in ") + String (busIndex + 1), getPortKindColor (PortKind::audio), PortKind::audio }; +} + +AudioGraphNodeView::PortInfo AudioGraphNodeView::getOutputPortInfo (int busIndex) const +{ + return { String ("out ") + String (busIndex + 1), getPortKindColor (PortKind::audio), PortKind::audio }; +} + +int AudioGraphNodeView::getNumParameterRows() const +{ + return 0; +} + +AudioGraphNodeView::ParameterInfo AudioGraphNodeView::getParameterInfo (int parameterIndex) const +{ + return { String ("param ") + String (parameterIndex + 1), {}, getPortKindColor (PortKind::parameter), -1.0f, PortKind::parameter }; +} + +void AudioGraphNodeView::paintNodeContent (Graphics&, Rectangle) +{ +} + +int AudioGraphNodeView::getPreferredWidth() const +{ + return 140; +} + +int AudioGraphNodeView::getPreferredHeight() const +{ + const auto portRows = jmax (1, jmax (getNumInputPorts(), getNumOutputPorts())); + return roundToInt (baseHeaderHeight + + baseContentHeight + + (static_cast (getNumParameterRows()) * baseParameterRowHeight) + + basePortTopPadding + + (static_cast (portRows) * basePortRowHeight) + + 10.0f); +} + +Point AudioGraphNodeView::getInputPortCenter (int busIndex) const +{ + return getPortCenter (busIndex, true); +} + +Point AudioGraphNodeView::getOutputPortCenter (int busIndex) const +{ + return getPortCenter (busIndex, false); +} + +float AudioGraphNodeView::getPortRadius() const +{ + return basePortRadius * viewScale; +} + +std::optional AudioGraphNodeView::hitTestPort (Point localPos) const +{ + const auto radiusSquared = getPortRadius() * getPortRadius(); + + for (int i = 0; i < getNumInputPorts(); ++i) + { + if (localPos.distanceToSquared (getInputPortCenter (i)) <= radiusSquared) + return PortHit { i, true }; + } + + for (int i = 0; i < getNumOutputPorts(); ++i) + { + if (localPos.distanceToSquared (getOutputPortCenter (i)) <= radiusSquared) + return PortHit { i, false }; + } + + return {}; +} + +Color AudioGraphNodeView::getPortKindColor (PortKind kind) +{ + switch (kind) + { + case PortKind::audio: + return Color (0xffffc43b); + case PortKind::midi: + return Color (0xffff4d67); + case PortKind::parameter: + return Color (0xff2f8cff); + } + + return Colors::dimgray; +} + +void AudioGraphNodeView::setViewScale (float newScale) +{ + viewScale = jlimit (0.1f, 4.0f, newScale); +} + +//============================================================================== +void AudioGraphNodeView::paint (Graphics& g) +{ + const auto bounds = getLocalBounds().reduced (getPortRadius() * 0.5f, 0.0f); + const auto bodyBounds = bounds.reduced (2.0f * viewScale); + const auto corner = baseCornerRadius * viewScale; + const auto accent = getNodeColor(); + const auto canvasBackground = Color (0xff101522); + const auto headerHeight = baseHeaderHeight * viewScale; + + fillFeatheredRoundedRect (g, bodyBounds.translated (0.0f, 3.0f * viewScale), corner, Colors::black, viewScale); + + g.setFillColor (accent.withAlpha (0.11f)); + g.fillRoundedRect (bodyBounds.reduced (-1.5f * viewScale), corner + 2.0f * viewScale); + + g.setFillColor (Color (0xff1e2535)); + g.fillRoundedRect (bodyBounds, corner); + + auto headerBounds = bodyBounds.withHeight (headerHeight); + g.setFillColor (Color (0xff141a26)); + g.fillRoundedRect (headerBounds, corner, corner, 0.0f, 0.0f); + + g.setStrokeColor (accent.withAlpha (0.70f)); + g.setStrokeWidth (1.35f * viewScale); + g.strokeRoundedRect (bodyBounds, corner); + + const auto headerInner = headerBounds.reduced (9.0f * viewScale, 5.0f * viewScale); + + const auto font = ApplicationTheme::getGlobalTheme()->getDefaultFont(); + g.setFillColor (accent); + g.fillFittedText (getNodeTitle(), font.withHeight (12.0f * viewScale), headerInner.withHeight (18.0f * viewScale), Justification::left); + + const auto subtitle = getNodeSubtitle(); + if (subtitle.isNotEmpty()) + { + g.setFillColor (Color (0xffb8b8b8)); + g.fillFittedText (subtitle, font.withHeight (9.0f * viewScale), headerInner.withHeight (18.0f * viewScale), Justification::right); + } + + const auto ruleY = bodyBounds.getY() + headerHeight; + g.setStrokeColor (accent.withAlpha (0.16f)); + g.strokeLine ({ bodyBounds.getX(), ruleY }, { bodyBounds.getRight(), ruleY }); + + auto contentBounds = bodyBounds.reduced (10.0f * viewScale, 0.0f); + contentBounds = contentBounds.withY (ruleY + 4.0f * viewScale).withHeight (baseContentHeight * viewScale); + paintNodeContent (g, contentBounds); + + auto parameterBounds = bodyBounds.reduced (10.0f * viewScale, 0.0f); + parameterBounds = parameterBounds.withY (ruleY + (baseContentHeight + 5.0f) * viewScale); + + const auto parameterFont = font.withHeight (9.5f * viewScale); + for (int i = 0; i < getNumParameterRows(); ++i) + { + const auto info = getParameterInfo (i); + auto row = parameterBounds.removeFromTop (baseParameterRowHeight * viewScale).reduced (0.0f, 2.0f * viewScale); + auto valueBox = row.removeFromRight (58.0f * viewScale); + + g.setFillColor (Color (0xff263044)); + g.fillRoundedRect (row.reduced (0.0f, 1.0f * viewScale), 3.0f * viewScale); + + if (info.normalizedValue >= 0.0f) + { + const auto fillWidth = row.getWidth() * jlimit (0.0f, 1.0f, info.normalizedValue); + g.setFillColor (info.color.withAlpha (0.20f)); + g.fillRoundedRect (row.withWidth (fillWidth).reduced (0.0f, 1.0f * viewScale), 3.0f * viewScale); + } + + g.setFillColor (Color (0xffd1d5db)); + g.fillFittedText (info.name, parameterFont, row.reduced (6.0f * viewScale, 0.0f), Justification::left); + + g.setFillColor (Color (0xff1a2130)); + g.fillRoundedRect (valueBox.reduced (0.0f, 1.0f * viewScale), 3.0f * viewScale); + g.setFillColor (info.color); + g.fillFittedText (info.value, parameterFont, valueBox.reduced (5.0f * viewScale, 0.0f), Justification::right); + } + + const auto labelFont = font.withHeight (10.5f * viewScale); + const auto portRadius = getPortRadius(); + + for (int i = 0; i < getNumInputPorts(); ++i) + { + const auto info = getInputPortInfo (i); + const auto center = getInputPortCenter (i); + + fillFeatheredEllipse (g, center, portRadius * 1.25f, info.color); + g.setFillColor (info.color); + g.fillEllipse (ellipseBounds (center, portRadius)); + g.setFillColor (canvasBackground); + g.fillEllipse (ellipseBounds (center, portRadius * 0.45f)); + g.setStrokeColor (info.color.withAlpha (0.72f)); + g.setStrokeWidth (1.2f * viewScale); + g.strokeEllipse (ellipseBounds (center, portRadius * 1.15f)); + + g.setFillColor (Color (0xffd6d6d6)); + g.fillFittedText (info.name, labelFont, { center.getX() + 12.0f * viewScale, center.getY() - 8.0f * viewScale, 72.0f * viewScale, 16.0f * viewScale }, Justification::left); + } + + for (int i = 0; i < getNumOutputPorts(); ++i) + { + const auto info = getOutputPortInfo (i); + const auto center = getOutputPortCenter (i); + + fillFeatheredEllipse (g, center, portRadius * 1.25f, info.color); + g.setFillColor (info.color); + g.fillEllipse (ellipseBounds (center, portRadius)); + g.setFillColor (canvasBackground); + g.fillEllipse (ellipseBounds (center, portRadius * 0.45f)); + g.setStrokeColor (info.color.withAlpha (0.72f)); + g.setStrokeWidth (1.2f * viewScale); + g.strokeEllipse (ellipseBounds (center, portRadius * 1.15f)); + + g.setFillColor (Color (0xffd6d6d6)); + g.fillFittedText (info.name, labelFont, { center.getX() - 84.0f * viewScale, center.getY() - 8.0f * viewScale, 72.0f * viewScale, 16.0f * viewScale }, Justification::right); + } +} + +Point AudioGraphNodeView::getPortCenter (int busIndex, bool isInput) const +{ + const auto portCount = isInput ? getNumInputPorts() : getNumOutputPorts(); + if (busIndex < 0 || busIndex >= portCount) + return {}; + + const auto portArea = getPortArea(); + const auto y = portArea.getY() + basePortTopPadding * viewScale + (static_cast (busIndex) + 0.5f) * basePortRowHeight * viewScale; + const auto x = isInput ? portArea.getX() : portArea.getRight(); + return { x, y }; +} + +Rectangle AudioGraphNodeView::getPortArea() const +{ + const auto bodyBounds = getLocalBounds().reduced (getPortRadius() * 0.5f + 2.0f * viewScale, 2.0f * viewScale); + const auto y = bodyBounds.getY() + (baseHeaderHeight + baseContentHeight + 4.0f + static_cast (getNumParameterRows()) * baseParameterRowHeight) * viewScale; + return { bodyBounds.getX(), y, bodyBounds.getWidth(), jmax (0.0f, bodyBounds.getBottom() - y - 6.0f * viewScale) }; +} + +} // namespace yup diff --git a/modules/yup_audio_gui/graph/yup_AudioGraphNodeView.h b/modules/yup_audio_gui/graph/yup_AudioGraphNodeView.h new file mode 100644 index 000000000..b11b93415 --- /dev/null +++ b/modules/yup_audio_gui/graph/yup_AudioGraphNodeView.h @@ -0,0 +1,164 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +/** + A visual node embedded by AudioGraphComponent. + + AudioGraphNodeView is a regular Component, so subclasses can add child controls, + meters, or custom painting while the graph canvas handles placement, panning, + zooming, and connection gestures. + + @see AudioGraphComponent, AudioGraphProcessor +*/ +class YUP_API AudioGraphNodeView : public Component +{ +public: + /** Semantic signal type for ports and future connection routing. */ + enum class PortKind + { + audio, + midi, + parameter + }; + + /** Describes a single input or output port. */ + struct PortInfo + { + /** Display name shown next to the port. */ + String name; + + /** Port fill color. */ + Color color; + + /** Signal type carried by the port. */ + PortKind kind = PortKind::audio; + }; + + /** Describes an inline parameter row inside a node. */ + struct ParameterInfo + { + /** Parameter display name. */ + String name; + + /** Current parameter value display text. */ + String value; + + /** Accent color for the value and optional modulation port. */ + Color color; + + /** Normalized value in [0, 1], or a negative value to hide the bar. */ + float normalizedValue = -1.0f; + + /** Signal type used for this row's future modulation port. */ + PortKind kind = PortKind::parameter; + }; + + /** Identifies the port hit by the user. */ + struct PortHit + { + /** Bus index on the node processor. */ + int busIndex = -1; + + /** True for input ports, false for output ports. */ + bool isInput = false; + }; + + /** Creates a node view for a graph node identifier. */ + explicit AudioGraphNodeView (AudioGraphNodeID nodeID); + + /** Destructor. */ + ~AudioGraphNodeView() override; + + /** Returns the graph node identifier represented by this view. */ + AudioGraphNodeID getNodeID() const noexcept { return nodeID; } + + /** Returns the title rendered in the node header. */ + virtual String getNodeTitle() const = 0; + + /** Returns the number of input ports. */ + virtual int getNumInputPorts() const = 0; + + /** Returns the number of output ports. */ + virtual int getNumOutputPorts() const = 0; + + /** Returns the node accent color. */ + virtual Color getNodeColor() const; + + /** Returns the subtitle rendered below the title. */ + virtual String getNodeSubtitle() const; + + /** Returns display metadata for an input port. */ + virtual PortInfo getInputPortInfo (int busIndex) const; + + /** Returns display metadata for an output port. */ + virtual PortInfo getOutputPortInfo (int busIndex) const; + + /** Returns the number of inline parameter rows. */ + virtual int getNumParameterRows() const; + + /** Returns display metadata for an inline parameter row. */ + virtual ParameterInfo getParameterInfo (int parameterIndex) const; + + /** Paints optional custom content between the header and the port rows. */ + virtual void paintNodeContent (Graphics& g, Rectangle contentBounds); + + /** Returns the preferred width in canvas units. */ + virtual int getPreferredWidth() const; + + /** Returns the computed preferred height in canvas units. */ + int getPreferredHeight() const; + + /** Returns the local center of an input port. */ + Point getInputPortCenter (int busIndex) const; + + /** Returns the local center of an output port. */ + Point getOutputPortCenter (int busIndex) const; + + /** Returns the port radius in current local display units. */ + float getPortRadius() const; + + /** Returns the port at localPos when it is within the port radius. */ + std::optional hitTestPort (Point localPos) const; + + /** Returns the default color for a semantic port kind. */ + static Color getPortKindColor (PortKind kind); + + /** @internal Sets the display scale applied by AudioGraphComponent. */ + void setViewScale (float newScale); + + /** @internal */ + void paint (Graphics& g) override; + +private: + Point getPortCenter (int busIndex, bool isInput) const; + Rectangle getPortArea() const; + + AudioGraphNodeID nodeID; + float viewScale = 1.0f; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioGraphNodeView) +}; + +} // namespace yup diff --git a/modules/yup_audio_gui/yup_audio_gui.cpp b/modules/yup_audio_gui/yup_audio_gui.cpp index a1d1e2254..4a2409062 100644 --- a/modules/yup_audio_gui/yup_audio_gui.cpp +++ b/modules/yup_audio_gui/yup_audio_gui.cpp @@ -40,3 +40,5 @@ #include "displays/yup_SpectrumAnalyzerComponent.cpp" #include "displays/yup_CartesianPlane.cpp" #include "metering/yup_KMeterComponent.cpp" +#include "graph/yup_AudioGraphNodeView.cpp" +#include "graph/yup_AudioGraphComponent.cpp" diff --git a/modules/yup_audio_gui/yup_audio_gui.h b/modules/yup_audio_gui/yup_audio_gui.h index 1574a0ac9..79ecaef4b 100644 --- a/modules/yup_audio_gui/yup_audio_gui.h +++ b/modules/yup_audio_gui/yup_audio_gui.h @@ -32,7 +32,7 @@ website: https://github.com/kunitoki/yup license: ISC - dependencies: yup_audio_basics yup_audio_formats yup_dsp yup_gui + dependencies: yup_audio_basics yup_audio_formats yup_audio_processors yup_audio_graph yup_dsp yup_gui END_YUP_MODULE_DECLARATION @@ -44,6 +44,8 @@ #include #include +#include +#include #include #include @@ -57,3 +59,5 @@ #include "displays/yup_SpectrumAnalyzerComponent.h" #include "displays/yup_CartesianPlane.h" #include "metering/yup_KMeterComponent.h" +#include "graph/yup_AudioGraphNodeView.h" +#include "graph/yup_AudioGraphComponent.h" diff --git a/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp b/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp index 8d1cb6526..c66346647 100644 --- a/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp +++ b/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp @@ -160,10 +160,11 @@ void paintLinearSlider (Graphics& g, const ApplicationTheme& theme, const Slider sliderValue * sliderBounds.getHeight(), 2.0f); } - - // Draw thumb - g.setFillColor (isMouseDown ? colors.thumbDown : (isMouseOver ? colors.thumbOver : colors.thumb)); - g.fillEllipse (thumbBounds); + else + { + g.setFillColor (isMouseDown ? colors.thumbDown : (isMouseOver ? colors.thumbOver : colors.thumb)); + g.fillEllipse (thumbBounds); + } // Draw focus outline if needed if (slider.hasKeyboardFocus()) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c763d8aeb..405cf2bd7 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -59,6 +59,7 @@ set (target_modules yup_audio_devices yup_audio_formats yup_audio_processors + yup_audio_graph yup_dsp yup_events yup_data_model diff --git a/tests/yup_audio_graph.cpp b/tests/yup_audio_graph.cpp new file mode 100644 index 000000000..262daeec4 --- /dev/null +++ b/tests/yup_audio_graph.cpp @@ -0,0 +1,22 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include "yup_audio_graph/yup_AudioGraphProcessor.cpp" diff --git a/tests/yup_audio_graph/yup_AudioGraphProcessor.cpp b/tests/yup_audio_graph/yup_AudioGraphProcessor.cpp new file mode 100644 index 000000000..497b675f7 --- /dev/null +++ b/tests/yup_audio_graph/yup_AudioGraphProcessor.cpp @@ -0,0 +1,1753 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include +#include +#include + +#include + +using namespace yup; + +namespace +{ +AudioBusLayout stereoLayout() +{ + return AudioBusLayout ({ AudioBus ("Input", AudioBus::Type::Audio, AudioBus::Direction::Input, 2) }, + { AudioBus ("Output", AudioBus::Type::Audio, AudioBus::Direction::Output, 2) }); +} + +AudioBusLayout monoLayout() +{ + return AudioBusLayout ({ AudioBus ("Input", AudioBus::Type::Audio, AudioBus::Direction::Input, 1) }, + { AudioBus ("Output", AudioBus::Type::Audio, AudioBus::Direction::Output, 1) }); +} + +AudioBusLayout midiLayout() +{ + return AudioBusLayout ({ AudioBus ("MIDI In", AudioBus::Type::MIDI, AudioBus::Direction::Input, 0) }, + { AudioBus ("MIDI Out", AudioBus::Type::MIDI, AudioBus::Direction::Output, 0) }); +} + +class TestProcessor : public AudioProcessor +{ +public: + explicit TestProcessor (float gainToUse = 1.0f, int latencyToUse = 0) + : AudioProcessor ("Test", stereoLayout()) + , gain (gainToUse) + , latency (latencyToUse) + { + } + + void prepareToPlay (float sampleRate, int maxBlockSize) override + { + preparedSampleRate = sampleRate; + preparedBlockSize = maxBlockSize; + prepared = true; + } + + void releaseResources() override { prepared = false; } + + void processBlock (AudioBuffer& audioBuffer, MidiBuffer&) override + { + for (int channel = 0; channel < audioBuffer.getNumChannels(); ++channel) + audioBuffer.applyGain (channel, 0, audioBuffer.getNumSamples(), gain); + } + + int getLatencySamples() override { return latency; } + + int getCurrentPreset() const noexcept override { return 0; } + + void setCurrentPreset (int) noexcept override {} + + int getNumPresets() const override { return 0; } + + String getPresetName (int) const override { return {}; } + + void setPresetName (int, StringRef) override {} + + Result loadStateFromMemory (const MemoryBlock&) override { return Result::ok(); } + + Result saveStateIntoMemory (MemoryBlock&) override { return Result::ok(); } + + bool hasEditor() const override { return false; } + + float preparedSampleRate = 0.0f; + int preparedBlockSize = 0; + bool prepared = false; + +private: + float gain = 1.0f; + int latency = 0; +}; + +class MidiPassthroughProcessor : public AudioProcessor +{ +public: + explicit MidiPassthroughProcessor (int latencyToUse = 0) + : AudioProcessor ("MIDI Passthrough", midiLayout()) + , latency (latencyToUse) + { + } + + void prepareToPlay (float, int) override {} + + void releaseResources() override {} + + void processBlock (AudioBuffer&, MidiBuffer&) override {} + + int getLatencySamples() override { return latency; } + + int getCurrentPreset() const noexcept override { return 0; } + + void setCurrentPreset (int) noexcept override {} + + int getNumPresets() const override { return 0; } + + String getPresetName (int) const override { return {}; } + + void setPresetName (int, StringRef) override {} + + Result loadStateFromMemory (const MemoryBlock&) override { return Result::ok(); } + + Result saveStateIntoMemory (MemoryBlock&) override { return Result::ok(); } + + bool hasEditor() const override { return false; } + +private: + int latency = 0; +}; + +class MidiDelayingProcessor : public AudioProcessor +{ +public: + explicit MidiDelayingProcessor (int latencyToUse) + : AudioProcessor ("MIDI Delay", midiLayout()) + , latency (latencyToUse) + { + pendingMidi.ensureSize (4096); + nextPendingMidi.ensureSize (4096); + } + + void prepareToPlay (float, int) override + { + pendingMidi.clear(); + nextPendingMidi.clear(); + } + + void releaseResources() override + { + pendingMidi.clear(); + nextPendingMidi.clear(); + } + + void flush() override + { + pendingMidi.clear(); + nextPendingMidi.clear(); + } + + void processBlock (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) override + { + const int numSamples = audioBuffer.getNumSamples(); + const MidiBuffer inputMidi = midiBuffer; + + midiBuffer.clear(); + nextPendingMidi.clear(); + + for (const auto metadata : pendingMidi) + { + if (metadata.samplePosition < numSamples) + { + midiBuffer.addEvent (metadata.data, metadata.numBytes, metadata.samplePosition); + } + else + { + nextPendingMidi.addEvent (metadata.data, metadata.numBytes, metadata.samplePosition - numSamples); + } + } + + for (const auto metadata : inputMidi) + { + if (metadata.samplePosition < 0 || metadata.samplePosition >= numSamples) + { + continue; + } + + const int delayedPosition = metadata.samplePosition + latency; + + if (delayedPosition < numSamples) + { + midiBuffer.addEvent (metadata.data, metadata.numBytes, delayedPosition); + } + else + { + nextPendingMidi.addEvent (metadata.data, metadata.numBytes, delayedPosition - numSamples); + } + } + + pendingMidi.swapWith (nextPendingMidi); + } + + int getLatencySamples() override { return latency; } + + int getCurrentPreset() const noexcept override { return 0; } + + void setCurrentPreset (int) noexcept override {} + + int getNumPresets() const override { return 0; } + + String getPresetName (int) const override { return {}; } + + void setPresetName (int, StringRef) override {} + + Result loadStateFromMemory (const MemoryBlock&) override { return Result::ok(); } + + Result saveStateIntoMemory (MemoryBlock&) override { return Result::ok(); } + + bool hasEditor() const override { return false; } + +private: + int latency = 0; + MidiBuffer pendingMidi; + MidiBuffer nextPendingMidi; +}; + +class MonoLayoutProcessor : public AudioProcessor +{ +public: + MonoLayoutProcessor() + : AudioProcessor ("Mono", monoLayout()) + { + } + + void prepareToPlay (float, int) override {} + + void releaseResources() override {} + + void processBlock (AudioBuffer&, MidiBuffer&) override {} + + int getCurrentPreset() const noexcept override { return 0; } + + void setCurrentPreset (int) noexcept override {} + + int getNumPresets() const override { return 0; } + + String getPresetName (int) const override { return {}; } + + void setPresetName (int, StringRef) override {} + + Result loadStateFromMemory (const MemoryBlock&) override { return Result::ok(); } + + Result saveStateIntoMemory (MemoryBlock&) override { return Result::ok(); } + + bool hasEditor() const override { return false; } +}; + +class DelayingProcessor : public TestProcessor +{ +public: + explicit DelayingProcessor (int latencyToUse) + : TestProcessor (1.0f, latencyToUse) + , delaySamples (latencyToUse) + , history (2, latencyToUse + 32) + { + history.clear(); + } + + void processBlock (AudioBuffer& audioBuffer, MidiBuffer&) override + { + const int ringSize = history.getNumSamples(); + + for (int sample = 0; sample < audioBuffer.getNumSamples(); ++sample) + { + const int readPosition = (writePosition + ringSize - delaySamples) % ringSize; + + for (int channel = 0; channel < audioBuffer.getNumChannels(); ++channel) + { + const float input = audioBuffer.getReadPointer (channel)[sample]; + audioBuffer.getWritePointer (channel)[sample] = history.getReadPointer (channel)[readPosition]; + history.getWritePointer (channel)[writePosition] = input; + } + + writePosition = (writePosition + 1) % ringSize; + } + } + +private: + int delaySamples = 0; + int writePosition = 0; + AudioBuffer history; +}; + +void fillImpulse (AudioBuffer& buffer) +{ + buffer.clear(); + buffer.getWritePointer (0)[0] = 1.0f; + buffer.getWritePointer (1)[0] = 1.0f; +} + +void fillImpulseAt (AudioBuffer& buffer, int samplePosition) +{ + buffer.clear(); + buffer.getWritePointer (0)[samplePosition] = 1.0f; + buffer.getWritePointer (1)[samplePosition] = 1.0f; +} + +int countMidiEventsAt (const MidiBuffer& midi, int samplePosition) +{ + int count = 0; + + for (const auto metadata : midi) + if (metadata.samplePosition == samplePosition) + ++count; + + return count; +} + +int countMidiEvents (const MidiBuffer& midi) +{ + int count = 0; + + for (const auto metadata : midi) + if (metadata.data != nullptr) + ++count; + + return count; +} + +bool resetGraphToSingleGainPath (AudioGraphProcessor& graph, float gain) +{ + graph.clear(); + + const auto node = graph.addNode (std::make_unique (gain)); + if (! node.isValid()) + return false; + + if (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), + AudioGraphEndpoint::nodeInput (node, 0) }) + .failed()) + return false; + + if (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (node, 0), + AudioGraphEndpoint::graphOutput (0) }) + .failed()) + return false; + + return graph.commitChanges().wasOk(); +} + +bool isExpectedGain (float sample) +{ + return std::abs (sample - 0.25f) < 0.000001f + || std::abs (sample - 0.75f) < 0.000001f; +} + +class DenormalCheckProcessor : public AudioProcessor +{ +public: + DenormalCheckProcessor() + : AudioProcessor ("DenormalCheck", stereoLayout()) + { + } + + void prepareToPlay (float, int) override {} + + void releaseResources() override {} + + void processBlock (AudioBuffer& audioBuffer, MidiBuffer&) override + { + for (int channel = 0; channel < audioBuffer.getNumChannels(); ++channel) + audioBuffer.applyGain (channel, 0, audioBuffer.getNumSamples(), 1.0f); + + denormalsWereDisabled = FloatVectorOperations::areDenormalsDisabled(); + } + + int getCurrentPreset() const noexcept override { return 0; } + + void setCurrentPreset (int) noexcept override {} + + int getNumPresets() const override { return 0; } + + String getPresetName (int) const override { return {}; } + + void setPresetName (int, StringRef) override {} + + Result loadStateFromMemory (const MemoryBlock&) override { return Result::ok(); } + + Result saveStateIntoMemory (MemoryBlock&) override { return Result::ok(); } + + bool hasEditor() const override { return false; } + + bool denormalsWereDisabled = false; +}; + +AudioBusLayout mixedLayout() +{ + return AudioBusLayout ({ AudioBus ("Audio In", AudioBus::Type::Audio, AudioBus::Direction::Input, 2), + AudioBus ("MIDI In", AudioBus::Type::MIDI, AudioBus::Direction::Input, 0) }, + { AudioBus ("Audio Out", AudioBus::Type::Audio, AudioBus::Direction::Output, 2), + AudioBus ("MIDI Out", AudioBus::Type::MIDI, AudioBus::Direction::Output, 0) }); +} + +class MixedProcessor : public AudioProcessor +{ +public: + explicit MixedProcessor (float gainToUse = 1.0f, int latencyToUse = 0) + : AudioProcessor ("Mixed", mixedLayout()) + , gain (gainToUse) + , latency (latencyToUse) + { + } + + void prepareToPlay (float, int) override {} + + void releaseResources() override {} + + void processBlock (AudioBuffer& audioBuffer, MidiBuffer&) override + { + for (int channel = 0; channel < audioBuffer.getNumChannels(); ++channel) + audioBuffer.applyGain (channel, 0, audioBuffer.getNumSamples(), gain); + } + + int getLatencySamples() override { return latency; } + + int getCurrentPreset() const noexcept override { return 0; } + + void setCurrentPreset (int) noexcept override {} + + int getNumPresets() const override { return 0; } + + String getPresetName (int) const override { return {}; } + + void setPresetName (int, StringRef) override {} + + Result loadStateFromMemory (const MemoryBlock&) override { return Result::ok(); } + + Result saveStateIntoMemory (MemoryBlock&) override { return Result::ok(); } + + bool hasEditor() const override { return false; } + +private: + float gain = 1.0f; + int latency = 0; +}; + +class MixedDelayingProcessor : public AudioProcessor +{ +public: + explicit MixedDelayingProcessor (int latencyToUse) + : AudioProcessor ("MixedDelay", mixedLayout()) + , delaySamples (latencyToUse) + , history (2, latencyToUse + 32) + { + history.clear(); + } + + void prepareToPlay (float, int) override + { + history.clear(); + writePosition = 0; + } + + void releaseResources() override {} + + void flush() override + { + history.clear(); + writePosition = 0; + } + + void processBlock (AudioBuffer& audioBuffer, MidiBuffer&) override + { + const int ringSize = history.getNumSamples(); + + for (int sample = 0; sample < audioBuffer.getNumSamples(); ++sample) + { + const int readPosition = (writePosition + ringSize - delaySamples) % ringSize; + + for (int channel = 0; channel < audioBuffer.getNumChannels(); ++channel) + { + const float input = audioBuffer.getReadPointer (channel)[sample]; + audioBuffer.getWritePointer (channel)[sample] = history.getReadPointer (channel)[readPosition]; + history.getWritePointer (channel)[writePosition] = input; + } + + writePosition = (writePosition + 1) % ringSize; + } + } + + int getLatencySamples() override { return delaySamples; } + + int getCurrentPreset() const noexcept override { return 0; } + + void setCurrentPreset (int) noexcept override {} + + int getNumPresets() const override { return 0; } + + String getPresetName (int) const override { return {}; } + + void setPresetName (int, StringRef) override {} + + Result loadStateFromMemory (const MemoryBlock&) override { return Result::ok(); } + + Result saveStateIntoMemory (MemoryBlock&) override { return Result::ok(); } + + bool hasEditor() const override { return false; } + +private: + int delaySamples = 0; + int writePosition = 0; + AudioBuffer history; +}; +} // namespace + +TEST (AudioGraphProcessorTests, CommitPreparesNodes) +{ + AudioGraphProcessor graph; + auto processor = std::make_unique(); + auto* processorPtr = processor.get(); + const auto node = graph.addNode (std::move (processor)); + + EXPECT_TRUE (node.isValid()); + + graph.prepareToPlay (48000.0f, 128); + const auto result = graph.commitChanges(); + + EXPECT_TRUE (result.wasOk()); + EXPECT_TRUE (processorPtr->prepared); + EXPECT_FLOAT_EQ (48000.0f, processorPtr->preparedSampleRate); + EXPECT_EQ (128, processorPtr->preparedBlockSize); +} + +TEST (AudioGraphProcessorTests, RejectsMissingNodeConnection) +{ + AudioGraphProcessor graph; + + const auto result = graph.addConnection ({ AudioGraphEndpoint::nodeOutput (AudioGraphNodeID::invalid(), 0), + AudioGraphEndpoint::graphOutput (0) }); + + EXPECT_TRUE (result.wasOk()); + EXPECT_TRUE (graph.commitChanges().failed()); +} + +TEST (AudioGraphProcessorTests, RejectsCycles) +{ + AudioGraphProcessor graph; + const auto first = graph.addNode (std::make_unique()); + const auto second = graph.addNode (std::make_unique()); + + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (first, 0), + AudioGraphEndpoint::nodeInput (second, 0) }) + .wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (second, 0), + AudioGraphEndpoint::nodeInput (first, 0) }) + .wasOk()); + + EXPECT_TRUE (graph.commitChanges().failed()); +} + +TEST (AudioGraphProcessorTests, ProcessesSerialAudioChain) +{ + AudioGraphProcessor graph; + const auto first = graph.addNode (std::make_unique (2.0f)); + const auto second = graph.addNode (std::make_unique (0.5f)); + + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (first, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (first, 0), AudioGraphEndpoint::nodeInput (second, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (second, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + fillImpulse (audio); + + graph.processBlock (audio, midi); + + EXPECT_FLOAT_EQ (1.0f, audio.getReadPointer (0)[0]); + EXPECT_FLOAT_EQ (1.0f, audio.getReadPointer (1)[0]); +} + +TEST (AudioGraphProcessorTests, MixesFanIn) +{ + AudioGraphProcessor graph; + const auto left = graph.addNode (std::make_unique (0.25f)); + const auto right = graph.addNode (std::make_unique (0.75f)); + + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (left, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (right, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (left, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (right, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + fillImpulse (audio); + + graph.processBlock (audio, midi); + + EXPECT_FLOAT_EQ (1.0f, audio.getReadPointer (0)[0]); + EXPECT_FLOAT_EQ (1.0f, audio.getReadPointer (1)[0]); +} + +TEST (AudioGraphProcessorTests, PreservesMidiTimestamps) +{ + AudioBusLayout midiLayout ({ AudioBus ("MIDI In", AudioBus::Type::MIDI, AudioBus::Direction::Input, 0) }, + { AudioBus ("MIDI Out", AudioBus::Type::MIDI, AudioBus::Direction::Output, 0) }); + AudioGraphProcessor graph (midiLayout); + + const auto node = graph.addNode (std::make_unique()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (node, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (node, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + AudioBuffer audio (0, 32); + MidiBuffer midi; + const uint8 noteOn[] = { 0x90, 60, 100 }; + midi.addEvent (noteOn, 3, 7); + + graph.processBlock (audio, midi); + + EXPECT_EQ (1, countMidiEventsAt (midi, 7)); +} + +TEST (AudioGraphProcessorTests, CompensatesShorterParallelPaths) +{ + AudioGraphProcessor graph; + const auto dry = graph.addNode (std::make_unique (1.0f, 0)); + const auto latent = graph.addNode (std::make_unique (4)); + + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (dry, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (latent, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (dry, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (latent, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + fillImpulse (audio); + + graph.processBlock (audio, midi); + + EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[0]); + EXPECT_FLOAT_EQ (2.0f, audio.getReadPointer (0)[4]); + EXPECT_EQ (4, graph.getLatencySamples()); + EXPECT_EQ (4, graph.getAllocationStats().totalCompensationSamples); +} + +TEST (AudioGraphProcessorTests, NullNodeIsRejected) +{ + AudioGraphProcessor graph; + + EXPECT_FALSE (graph.addNode (nullptr).isValid()); + EXPECT_TRUE (graph.hasUncommittedChanges()); +} + +TEST (AudioGraphProcessorTests, RejectsInvalidEndpointDirectionImmediately) +{ + AudioGraphProcessor graph; + const auto node = graph.addNode (std::make_unique()); + + const auto badSource = graph.addConnection ({ AudioGraphEndpoint::nodeInput (node, 0), + AudioGraphEndpoint::graphOutput (0) }); + const auto badDestination = graph.addConnection ({ AudioGraphEndpoint::graphInput (0), + AudioGraphEndpoint::nodeOutput (node, 0) }); + + EXPECT_TRUE (badSource.failed()); + EXPECT_TRUE (badDestination.failed()); +} + +TEST (AudioGraphProcessorTests, RejectsDuplicateConnectionsImmediately) +{ + AudioGraphProcessor graph; + const auto node = graph.addNode (std::make_unique()); + const AudioGraphConnection connection { AudioGraphEndpoint::graphInput (0), + AudioGraphEndpoint::nodeInput (node, 0) }; + + EXPECT_TRUE (graph.addConnection (connection).wasOk()); + EXPECT_TRUE (graph.addConnection (connection).failed()); +} + +TEST (AudioGraphProcessorTests, RejectsInvalidBusIndexAtCommit) +{ + AudioGraphProcessor graph; + const auto node = graph.addNode (std::make_unique()); + + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), + AudioGraphEndpoint::nodeInput (node, 99) }) + .wasOk()); + EXPECT_TRUE (graph.commitChanges().failed()); +} + +TEST (AudioGraphProcessorTests, RejectsAudioMidiTypeMismatchAtCommit) +{ + AudioGraphProcessor graph; + const auto audioNode = graph.addNode (std::make_unique()); + const auto midiNode = graph.addNode (std::make_unique()); + + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (audioNode, 0), + AudioGraphEndpoint::nodeInput (midiNode, 0) }) + .wasOk()); + EXPECT_TRUE (graph.commitChanges().failed()); +} + +TEST (AudioGraphProcessorTests, RejectsAudioChannelMismatchAtCommit) +{ + AudioGraphProcessor graph; + const auto stereoNode = graph.addNode (std::make_unique()); + const auto monoNode = graph.addNode (std::make_unique()); + + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (stereoNode, 0), + AudioGraphEndpoint::nodeInput (monoNode, 0) }) + .wasOk()); + EXPECT_TRUE (graph.commitChanges().failed()); +} + +TEST (AudioGraphProcessorTests, MissingCommitKeepsPreviousPlan) +{ + AudioGraphProcessor graph; + const auto first = graph.addNode (std::make_unique (0.5f)); + + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (first, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (first, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + const auto second = graph.addNode (std::make_unique (0.25f)); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (first, 0), AudioGraphEndpoint::nodeInput (second, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (second, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.hasUncommittedChanges()); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + fillImpulse (audio); + + graph.processBlock (audio, midi); + + EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (0)[0]); +} + +TEST (AudioGraphProcessorTests, RemoveConnectionStopsRoutingAfterCommit) +{ + AudioGraphProcessor graph; + const auto node = graph.addNode (std::make_unique()); + const AudioGraphConnection inputConnection { AudioGraphEndpoint::graphInput (0), + AudioGraphEndpoint::nodeInput (node, 0) }; + const AudioGraphConnection outputConnection { AudioGraphEndpoint::nodeOutput (node, 0), + AudioGraphEndpoint::graphOutput (0) }; + + EXPECT_TRUE (graph.addConnection (inputConnection).wasOk()); + EXPECT_TRUE (graph.addConnection (outputConnection).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + EXPECT_TRUE (graph.removeConnection (outputConnection)); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + fillImpulse (audio); + + graph.processBlock (audio, midi); + + EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[0]); + EXPECT_FALSE (graph.removeConnection (outputConnection)); +} + +TEST (AudioGraphProcessorTests, RemoveNodePrunesConnections) +{ + AudioGraphProcessor graph; + const auto node = graph.addNode (std::make_unique()); + + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (node, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (node, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.removeNode (node)); + EXPECT_EQ (nullptr, graph.getNodeProcessor (node)); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + fillImpulse (audio); + + graph.processBlock (audio, midi); + + EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[0]); + EXPECT_FALSE (graph.removeNode (node)); +} + +TEST (AudioGraphProcessorTests, ClearRemovesAllRouting) +{ + AudioGraphProcessor graph; + const auto node = graph.addNode (std::make_unique()); + + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (node, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (node, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + graph.clear(); + EXPECT_TRUE (graph.hasUncommittedChanges()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + EXPECT_FALSE (graph.hasUncommittedChanges()); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + fillImpulse (audio); + + graph.processBlock (audio, midi); + + EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[0]); +} + +TEST (AudioGraphProcessorTests, ReleaseResourcesMarksPreparedNodesUnprepared) +{ + AudioGraphProcessor graph; + auto processor = std::make_unique(); + auto* processorPtr = processor.get(); + const auto node = graph.addNode (std::move (processor)); + + EXPECT_TRUE (node.isValid()); + graph.prepareToPlay (44100.0f, 64); + EXPECT_TRUE (processorPtr->prepared); + + graph.releaseResources(); + + EXPECT_FALSE (processorPtr->prepared); +} + +TEST (AudioGraphProcessorTests, ReportsAllocationStats) +{ + AudioGraphProcessor graph; + const auto first = graph.addNode (std::make_unique()); + const auto second = graph.addNode (std::make_unique()); + + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (first, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (second, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (first, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (second, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + const auto stats = graph.getAllocationStats(); + + EXPECT_EQ (2, stats.scratchAudioBuffers); + EXPECT_EQ (2, stats.midiBuffers); + EXPECT_GE (stats.maxPreallocatedChannels, 2); + EXPECT_GE (stats.maxPreallocatedBlockSize, 1); +} + +TEST (AudioGraphProcessorTests, WorkerThreadOutputMatchesSingleThreadOutput) +{ + AudioGraphProcessor singleThreaded; + const auto singleLeft = singleThreaded.addNode (std::make_unique (0.25f)); + const auto singleRight = singleThreaded.addNode (std::make_unique (0.75f)); + + EXPECT_TRUE (singleThreaded.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (singleLeft, 0) }).wasOk()); + EXPECT_TRUE (singleThreaded.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (singleRight, 0) }).wasOk()); + EXPECT_TRUE (singleThreaded.addConnection ({ AudioGraphEndpoint::nodeOutput (singleLeft, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (singleThreaded.addConnection ({ AudioGraphEndpoint::nodeOutput (singleRight, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (singleThreaded.commitChanges().wasOk()); + + AudioGraphProcessor threaded; + threaded.setNumWorkerThreads (2); + const auto threadedLeft = threaded.addNode (std::make_unique (0.25f)); + const auto threadedRight = threaded.addNode (std::make_unique (0.75f)); + + EXPECT_TRUE (threaded.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (threadedLeft, 0) }).wasOk()); + EXPECT_TRUE (threaded.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (threadedRight, 0) }).wasOk()); + EXPECT_TRUE (threaded.addConnection ({ AudioGraphEndpoint::nodeOutput (threadedLeft, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (threaded.addConnection ({ AudioGraphEndpoint::nodeOutput (threadedRight, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (threaded.commitChanges().wasOk()); + + AudioBuffer singleAudio (2, 16); + AudioBuffer threadedAudio (2, 16); + MidiBuffer singleMidi; + MidiBuffer threadedMidi; + fillImpulse (singleAudio); + fillImpulse (threadedAudio); + + singleThreaded.processBlock (singleAudio, singleMidi); + threaded.processBlock (threadedAudio, threadedMidi); + + for (int channel = 0; channel < 2; ++channel) + { + for (int sample = 0; sample < 16; ++sample) + { + EXPECT_FLOAT_EQ (singleAudio.getReadPointer (channel)[sample], + threadedAudio.getReadPointer (channel)[sample]); + } + } +} + +TEST (AudioGraphProcessorTests, WorkerThreadsMatchSingleThreadOutputUnderLoad) +{ + constexpr int numBranches = 16; + constexpr int numSamples = 64; + constexpr int numBlocks = 128; + + AudioGraphProcessor singleThreaded; + AudioGraphProcessor threaded; + threaded.setNumWorkerThreads (4); + + for (int branch = 0; branch < numBranches; ++branch) + { + const float gain = 1.0f / static_cast (numBranches); + const auto singleNode = singleThreaded.addNode (std::make_unique (gain)); + const auto threadedNode = threaded.addNode (std::make_unique (gain)); + + EXPECT_TRUE (singleNode.isValid()); + EXPECT_TRUE (threadedNode.isValid()); + EXPECT_TRUE (singleThreaded.addConnection ({ AudioGraphEndpoint::graphInput (0), + AudioGraphEndpoint::nodeInput (singleNode, 0) }) + .wasOk()); + EXPECT_TRUE (singleThreaded.addConnection ({ AudioGraphEndpoint::nodeOutput (singleNode, 0), + AudioGraphEndpoint::graphOutput (0) }) + .wasOk()); + EXPECT_TRUE (threaded.addConnection ({ AudioGraphEndpoint::graphInput (0), + AudioGraphEndpoint::nodeInput (threadedNode, 0) }) + .wasOk()); + EXPECT_TRUE (threaded.addConnection ({ AudioGraphEndpoint::nodeOutput (threadedNode, 0), + AudioGraphEndpoint::graphOutput (0) }) + .wasOk()); + } + + EXPECT_TRUE (singleThreaded.commitChanges().wasOk()); + EXPECT_TRUE (threaded.commitChanges().wasOk()); + + AudioBuffer singleAudio (2, numSamples); + AudioBuffer threadedAudio (2, numSamples); + MidiBuffer singleMidi; + MidiBuffer threadedMidi; + + for (int block = 0; block < numBlocks; ++block) + { + singleAudio.clear(); + threadedAudio.clear(); + singleMidi.clear(); + threadedMidi.clear(); + + for (int channel = 0; channel < 2; ++channel) + { + for (int sample = 0; sample < numSamples; ++sample) + { + const float input = static_cast ((block + 1) * (sample + 1)) * 0.0001f; + singleAudio.getWritePointer (channel)[sample] = input; + threadedAudio.getWritePointer (channel)[sample] = input; + } + } + + singleThreaded.processBlock (singleAudio, singleMidi); + threaded.processBlock (threadedAudio, threadedMidi); + + for (int channel = 0; channel < 2; ++channel) + { + for (int sample = 0; sample < numSamples; ++sample) + { + EXPECT_FLOAT_EQ (singleAudio.getReadPointer (channel)[sample], + threadedAudio.getReadPointer (channel)[sample]); + } + } + } +} + +TEST (AudioGraphProcessorTests, SwitchingWorkerThreadsBetweenBlocksPreservesOutput) +{ + constexpr int numBranches = 8; + constexpr int numSamples = 48; + constexpr int numBlocks = 64; + constexpr int threadCounts[] = { 0, 1, 4, 2, 0, 3 }; + constexpr int numThreadCounts = sizeof (threadCounts) / sizeof (threadCounts[0]); + + AudioGraphProcessor singleThreaded; + AudioGraphProcessor threaded; + + for (int branch = 0; branch < numBranches; ++branch) + { + const float gain = 1.0f / static_cast (numBranches); + const auto singleNode = singleThreaded.addNode (std::make_unique (gain)); + const auto threadedNode = threaded.addNode (std::make_unique (gain)); + + EXPECT_TRUE (singleNode.isValid()); + EXPECT_TRUE (threadedNode.isValid()); + EXPECT_TRUE (singleThreaded.addConnection ({ AudioGraphEndpoint::graphInput (0), + AudioGraphEndpoint::nodeInput (singleNode, 0) }) + .wasOk()); + EXPECT_TRUE (singleThreaded.addConnection ({ AudioGraphEndpoint::nodeOutput (singleNode, 0), + AudioGraphEndpoint::graphOutput (0) }) + .wasOk()); + EXPECT_TRUE (threaded.addConnection ({ AudioGraphEndpoint::graphInput (0), + AudioGraphEndpoint::nodeInput (threadedNode, 0) }) + .wasOk()); + EXPECT_TRUE (threaded.addConnection ({ AudioGraphEndpoint::nodeOutput (threadedNode, 0), + AudioGraphEndpoint::graphOutput (0) }) + .wasOk()); + } + + EXPECT_TRUE (singleThreaded.commitChanges().wasOk()); + EXPECT_TRUE (threaded.commitChanges().wasOk()); + + AudioBuffer singleAudio (2, numSamples); + AudioBuffer threadedAudio (2, numSamples); + MidiBuffer singleMidi; + MidiBuffer threadedMidi; + + for (int block = 0; block < numBlocks; ++block) + { + threaded.setNumWorkerThreads (threadCounts[static_cast (block) % numThreadCounts]); + + singleAudio.clear(); + threadedAudio.clear(); + singleMidi.clear(); + threadedMidi.clear(); + + for (int channel = 0; channel < 2; ++channel) + { + for (int sample = 0; sample < numSamples; ++sample) + { + const float input = static_cast ((block + channel + 1) * (sample + 3)) * 0.0002f; + singleAudio.getWritePointer (channel)[sample] = input; + threadedAudio.getWritePointer (channel)[sample] = input; + } + } + + singleThreaded.processBlock (singleAudio, singleMidi); + threaded.processBlock (threadedAudio, threadedMidi); + + for (int channel = 0; channel < 2; ++channel) + { + for (int sample = 0; sample < numSamples; ++sample) + { + EXPECT_FLOAT_EQ (singleAudio.getReadPointer (channel)[sample], + threadedAudio.getReadPointer (channel)[sample]); + } + } + } +} + +TEST (AudioGraphProcessorTests, ConcurrentCommitsDoNotInvalidateAudioThreadPlan) +{ + AudioGraphProcessor graph; + graph.setNumWorkerThreads (2); + graph.prepareToPlay (48000.0f, 32); + + ASSERT_TRUE (resetGraphToSingleGainPath (graph, 0.25f)); + + std::atomic keepProcessing { true }; + std::atomic startProcessing { false }; + std::atomic invalidBlocks { 0 }; + std::atomic commitFailures { 0 }; + std::atomic processedBlocks { 0 }; + + std::thread audioThread ([&] + { + AudioBuffer audio (2, 32); + MidiBuffer midi; + + while (! startProcessing.load()) + std::this_thread::yield(); + + while (keepProcessing.load()) + { + audio.clear(); + midi.clear(); + + for (int channel = 0; channel < audio.getNumChannels(); ++channel) + for (int sample = 0; sample < audio.getNumSamples(); ++sample) + audio.getWritePointer (channel)[sample] = 1.0f; + + graph.processBlock (audio, midi); + + for (int channel = 0; channel < audio.getNumChannels(); ++channel) + { + for (int sample = 0; sample < audio.getNumSamples(); ++sample) + { + if (! isExpectedGain (audio.getReadPointer (channel)[sample])) + { + ++invalidBlocks; + break; + } + } + } + + ++processedBlocks; + std::this_thread::yield(); + } + }); + + std::thread controlThread ([&] + { + startProcessing.store (true); + + for (int waitCount = 0; waitCount < 10000 && processedBlocks.load() == 0; ++waitCount) + std::this_thread::yield(); + + for (int iteration = 0; iteration < 250; ++iteration) + { + const float gain = (iteration % 2) == 0 ? 0.75f : 0.25f; + + if (! resetGraphToSingleGainPath (graph, gain)) + ++commitFailures; + + std::this_thread::yield(); + } + + keepProcessing.store (false); + }); + + controlThread.join(); + audioThread.join(); + + EXPECT_EQ (0, commitFailures.load()); + EXPECT_EQ (0, invalidBlocks.load()); + EXPECT_GT (processedBlocks.load(), 0); +} + +TEST (AudioGraphProcessorTests, MidiCompensationCanSpillIntoNextBlock) +{ + AudioBusLayout layout = midiLayout(); + AudioGraphProcessor graph (layout); + const auto latent = graph.addNode (std::make_unique (6)); + + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (latent, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (latent, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + AudioBuffer audio (0, 8); + MidiBuffer midi; + const uint8 noteOn[] = { 0x90, 64, 100 }; + midi.addEvent (noteOn, 3, 5); + + graph.processBlock (audio, midi); + + EXPECT_EQ (1, countMidiEventsAt (midi, 5)); + EXPECT_EQ (1, countMidiEvents (midi)); + + midi.clear(); + graph.processBlock (audio, midi); + + EXPECT_EQ (1, countMidiEventsAt (midi, 3)); +} + +TEST (AudioGraphProcessorTests, PdcAccumulatesSerialPathLatencyAtGraphOutput) +{ + AudioGraphProcessor graph; + const auto dry = graph.addNode (std::make_unique()); + const auto firstDelay = graph.addNode (std::make_unique (3)); + const auto secondDelay = graph.addNode (std::make_unique (4)); + + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (dry, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (dry, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (firstDelay, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (firstDelay, 0), AudioGraphEndpoint::nodeInput (secondDelay, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (secondDelay, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + fillImpulse (audio); + + graph.processBlock (audio, midi); + + EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[0]); + EXPECT_FLOAT_EQ (2.0f, audio.getReadPointer (0)[7]); + EXPECT_EQ (7, graph.getLatencySamples()); + EXPECT_EQ (7, graph.getAllocationStats().totalCompensationSamples); + EXPECT_EQ (1, graph.getAllocationStats().delayLines); +} + +TEST (AudioGraphProcessorTests, PdcCompensatesFanInAtProcessorInput) +{ + AudioGraphProcessor graph; + const auto dry = graph.addNode (std::make_unique()); + const auto delayed = graph.addNode (std::make_unique (5)); + const auto summer = graph.addNode (std::make_unique()); + + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (dry, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (delayed, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (dry, 0), AudioGraphEndpoint::nodeInput (summer, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (delayed, 0), AudioGraphEndpoint::nodeInput (summer, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (summer, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + fillImpulse (audio); + + graph.processBlock (audio, midi); + + EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[0]); + EXPECT_FLOAT_EQ (2.0f, audio.getReadPointer (0)[5]); + EXPECT_EQ (5, graph.getLatencySamples()); + EXPECT_EQ (1, graph.getAllocationStats().delayLines); +} + +TEST (AudioGraphProcessorTests, PdcDelaysDirectAudioOutputToMatchLatentAudioPath) +{ + AudioGraphProcessor graph; + const auto delayed = graph.addNode (std::make_unique (5)); + + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (delayed, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (delayed, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + fillImpulseAt (audio, 3); + + graph.processBlock (audio, midi); + + EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[3]); + EXPECT_FLOAT_EQ (2.0f, audio.getReadPointer (0)[8]); + EXPECT_EQ (5, graph.getLatencySamples()); + EXPECT_EQ (1, graph.getAllocationStats().delayLines); +} + +TEST (AudioGraphProcessorTests, PdcAudioDelayLongerThanBlockSpillsAcrossBlocks) +{ + AudioGraphProcessor graph; + const auto dry = graph.addNode (std::make_unique()); + const auto delayed = graph.addNode (std::make_unique (10)); + + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (dry, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (delayed, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (dry, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (delayed, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + AudioBuffer audio (2, 4); + MidiBuffer midi; + fillImpulse (audio); + + graph.processBlock (audio, midi); + EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[0]); + + audio.clear(); + graph.processBlock (audio, midi); + EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[0]); + + audio.clear(); + graph.processBlock (audio, midi); + EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[1]); + EXPECT_FLOAT_EQ (2.0f, audio.getReadPointer (0)[2]); + EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[3]); +} + +TEST (AudioGraphProcessorTests, PdcDirectAudioOutputCompensationSpillsAcrossBlocks) +{ + AudioGraphProcessor graph; + const auto delayed = graph.addNode (std::make_unique (6)); + + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (delayed, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (delayed, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + AudioBuffer audio (2, 4); + MidiBuffer midi; + fillImpulseAt (audio, 1); + + graph.processBlock (audio, midi); + EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[1]); + + audio.clear(); + graph.processBlock (audio, midi); + EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[2]); + EXPECT_FLOAT_EQ (2.0f, audio.getReadPointer (0)[3]); +} + +TEST (AudioGraphProcessorTests, PdcFlushClearsPendingAudioCompensation) +{ + AudioGraphProcessor graph; + const auto latencyReporter = graph.addNode (std::make_unique (0.0f, 6)); + + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (latencyReporter, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (latencyReporter, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + AudioBuffer audio (2, 4); + MidiBuffer midi; + fillImpulse (audio); + + graph.processBlock (audio, midi); + graph.flush(); + + audio.clear(); + graph.processBlock (audio, midi); + EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[0]); + + audio.clear(); + graph.processBlock (audio, midi); + EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[2]); +} + +TEST (AudioGraphProcessorTests, PdcMidiDelayLongerThanOneBlockAlignsWithDelayedProcessor) +{ + AudioGraphProcessor graph (midiLayout()); + const auto delayed = graph.addNode (std::make_unique (18)); + + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (delayed, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (delayed, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + AudioBuffer audio (0, 8); + MidiBuffer midi; + const uint8 noteOn[] = { 0x90, 67, 100 }; + midi.addEvent (noteOn, 3, 6); + + graph.processBlock (audio, midi); + EXPECT_EQ (0, countMidiEvents (midi)); + + midi.clear(); + graph.processBlock (audio, midi); + EXPECT_EQ (0, countMidiEvents (midi)); + + midi.clear(); + graph.processBlock (audio, midi); + EXPECT_EQ (0, countMidiEvents (midi)); + + midi.clear(); + graph.processBlock (audio, midi); + EXPECT_EQ (2, countMidiEventsAt (midi, 0)); + EXPECT_EQ (2, countMidiEvents (midi)); +} + +TEST (AudioGraphProcessorTests, PdcFlushClearsPendingMidiCompensation) +{ + AudioGraphProcessor graph (midiLayout()); + const auto latencyReporter = graph.addNode (std::make_unique (10)); + + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (latencyReporter, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (latencyReporter, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + AudioBuffer audio (0, 8); + MidiBuffer midi; + const uint8 noteOn[] = { 0x90, 69, 100 }; + midi.addEvent (noteOn, 3, 7); + + graph.processBlock (audio, midi); + EXPECT_EQ (1, countMidiEventsAt (midi, 7)); + + graph.flush(); + + midi.clear(); + graph.processBlock (audio, midi); + EXPECT_EQ (0, countMidiEvents (midi)); + + midi.clear(); + graph.processBlock (audio, midi); + EXPECT_EQ (0, countMidiEvents (midi)); +} + +TEST (AudioGraphProcessorTests, PdcRecompileAfterRemovingLatencyPathClearsCompensation) +{ + AudioGraphProcessor graph; + const auto delayed = graph.addNode (std::make_unique (0.0f, 12)); + + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (delayed, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (delayed, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + EXPECT_EQ (12, graph.getLatencySamples()); + EXPECT_EQ (1, graph.getAllocationStats().delayLines); + + EXPECT_TRUE (graph.removeNode (delayed)); + EXPECT_TRUE (graph.commitChanges().wasOk()); + EXPECT_EQ (0, graph.getLatencySamples()); + EXPECT_EQ (0, graph.getAllocationStats().delayLines); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + fillImpulse (audio); + + graph.processBlock (audio, midi); + + EXPECT_FLOAT_EQ (1.0f, audio.getReadPointer (0)[0]); +} + +TEST (AudioGraphProcessorTests, WorkerThreadsProcessManyBlocksCorrectly) +{ + AudioGraphProcessor graph; + graph.setNumWorkerThreads (2); + + const auto left = graph.addNode (std::make_unique (0.5f)); + const auto right = graph.addNode (std::make_unique (0.5f)); + + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (left, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (right, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (left, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (right, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + + for (int block = 0; block < 1000; ++block) + { + fillImpulse (audio); + graph.processBlock (audio, midi); + EXPECT_FLOAT_EQ (1.0f, audio.getReadPointer (0)[0]) << "block " << block; + } +} + +TEST (AudioGraphProcessorTests, WorkerThreadCountCanBeChangedAfterProcessing) +{ + AudioGraphProcessor graph; + + const auto node = graph.addNode (std::make_unique (0.5f)); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (node, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (node, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + + graph.setNumWorkerThreads (2); + fillImpulse (audio); + graph.processBlock (audio, midi); + EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (0)[0]); + + graph.setNumWorkerThreads (0); + fillImpulse (audio); + graph.processBlock (audio, midi); + EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (0)[0]); + + graph.setNumWorkerThreads (4); + fillImpulse (audio); + graph.processBlock (audio, midi); + EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (0)[0]); + + graph.setNumWorkerThreads (0); +} + +TEST (AudioGraphProcessorTests, ZeroWorkerThreadsProcessesCorrectly) +{ + AudioGraphProcessor graph; + graph.setNumWorkerThreads (0); + + const auto node = graph.addNode (std::make_unique (0.5f)); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (node, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (node, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + + for (int block = 0; block < 10; ++block) + { + fillImpulse (audio); + graph.processBlock (audio, midi); + EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (0)[0]) << "block " << block; + } +} + +TEST (AudioGraphProcessorTests, WorkerThreadOutputMatchesSingleThreadOutputManyBlocks) +{ + auto runGraph = [] (int numWorkers) -> std::vector + { + AudioGraphProcessor graph; + graph.setNumWorkerThreads (numWorkers); + + const auto left = graph.addNode (std::make_unique (0.25f)); + const auto right = graph.addNode (std::make_unique (0.25f)); + + graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (left, 0) }); + graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (right, 0) }); + graph.addConnection ({ AudioGraphEndpoint::nodeOutput (left, 0), AudioGraphEndpoint::graphOutput (0) }); + graph.addConnection ({ AudioGraphEndpoint::nodeOutput (right, 0), AudioGraphEndpoint::graphOutput (0) }); + graph.commitChanges(); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + + std::vector results; + for (int block = 0; block < 100; ++block) + { + fillImpulse (audio); + graph.processBlock (audio, midi); + results.push_back (audio.getReadPointer (0)[0]); + } + + return results; + }; + + const auto singleResults = runGraph (0); + const auto multiResults = runGraph (2); + + ASSERT_EQ (singleResults.size(), multiResults.size()); + for (size_t i = 0; i < singleResults.size(); ++i) + EXPECT_FLOAT_EQ (singleResults[i], multiResults[i]) << "block " << i; +} + +TEST (AudioGraphProcessorTests, RapidWorkerThreadResizeDoesNotCrash) +{ + AudioGraphProcessor graph; + + const auto node = graph.addNode (std::make_unique (1.0f)); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (node, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (node, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + + const int threadCounts[] = { 0, 1, 2, 4, 2, 1, 0, 3, 0 }; + + for (int count : threadCounts) + { + graph.setNumWorkerThreads (count); + fillImpulse (audio); + graph.processBlock (audio, midi); + EXPECT_FLOAT_EQ (1.0f, audio.getReadPointer (0)[0]); + } +} + +TEST (AudioGraphProcessorTests, ProcessBlockDisablesDenormals) +{ + AudioGraphProcessor graph; + + auto proc = std::make_unique(); + auto* procPtr = proc.get(); + const auto node = graph.addNode (std::move (proc)); + + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (node, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (node, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + fillImpulse (audio); + + graph.processBlock (audio, midi); + + EXPECT_TRUE (procPtr->denormalsWereDisabled); +} + +TEST (AudioGraphProcessorTests, ManyMidiEventsPassThroughGraphCorrectly) +{ + AudioBusLayout layout = midiLayout(); + AudioGraphProcessor graph (layout); + + const auto node = graph.addNode (std::make_unique()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (node, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (node, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + AudioBuffer audio (0, 128); + MidiBuffer midi; + + const int numEvents = 64; + for (int i = 0; i < numEvents; ++i) + { + const uint8 noteOn[] = { static_cast (0x90), static_cast (i % 128), 100 }; + midi.addEvent (noteOn, 3, i % 128); + } + + graph.processBlock (audio, midi); + + EXPECT_EQ (numEvents, countMidiEvents (midi)); +} + +TEST (AudioGraphProcessorTests, WorkerThreadsPdcCompensatesParallelPaths) +{ + // Mirror of CompensatesShorterParallelPaths with 2 worker threads. + // Verifies that PDC delay-line insertion still aligns a zero-latency branch + // with a 4-sample latent branch under parallel scheduling. + AudioGraphProcessor graph; + graph.setNumWorkerThreads (2); + + const auto dry = graph.addNode (std::make_unique (1.0f, 0)); + const auto latent = graph.addNode (std::make_unique (4)); + + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (dry, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (latent, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (dry, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (latent, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + fillImpulse (audio); + + graph.processBlock (audio, midi); + + EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[0]); + EXPECT_FLOAT_EQ (2.0f, audio.getReadPointer (0)[4]); + EXPECT_EQ (4, graph.getLatencySamples()); + EXPECT_EQ (4, graph.getAllocationStats().totalCompensationSamples); +} + +TEST (AudioGraphProcessorTests, WorkerThreadsPdcMatchesSingleThreadOutput) +{ + // Build the same graph (dry path + serial 3+4 delay path) in both single-threaded + // and multi-threaded configurations, then verify sample-exact output over several + // blocks including the initial PDC fill period. + auto buildGraph = [] (int numWorkers) -> std::unique_ptr + { + auto g = std::make_unique(); + g->setNumWorkerThreads (numWorkers); + + const auto dry = g->addNode (std::make_unique (1.0f, 0)); + const auto firstDelay = g->addNode (std::make_unique (3)); + const auto secondDelay = g->addNode (std::make_unique (4)); + + g->addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (dry, 0) }); + g->addConnection ({ AudioGraphEndpoint::nodeOutput (dry, 0), AudioGraphEndpoint::graphOutput (0) }); + g->addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (firstDelay, 0) }); + g->addConnection ({ AudioGraphEndpoint::nodeOutput (firstDelay, 0), AudioGraphEndpoint::nodeInput (secondDelay, 0) }); + g->addConnection ({ AudioGraphEndpoint::nodeOutput (secondDelay, 0), AudioGraphEndpoint::graphOutput (0) }); + g->commitChanges(); + + return g; + }; + + const auto single = buildGraph (0); + const auto multi = buildGraph (3); + + EXPECT_EQ (single->getLatencySamples(), multi->getLatencySamples()); + + AudioBuffer singleAudio (2, 16); + AudioBuffer multiAudio (2, 16); + MidiBuffer singleMidi; + MidiBuffer multiMidi; + + for (int block = 0; block < 8; ++block) + { + fillImpulse (singleAudio); + fillImpulse (multiAudio); + + single->processBlock (singleAudio, singleMidi); + multi->processBlock (multiAudio, multiMidi); + + for (int channel = 0; channel < 2; ++channel) + { + for (int sample = 0; sample < 16; ++sample) + { + EXPECT_FLOAT_EQ (singleAudio.getReadPointer (channel)[sample], + multiAudio.getReadPointer (channel)[sample]) + << "block=" << block << " ch=" << channel << " sample=" << sample; + } + } + } +} + +TEST (AudioGraphProcessorTests, MixedAudioMidiProcessorPassesThroughBothSignals) +{ + // A single mixed-bus node (audio bus 0, MIDI bus 1) applies gain to audio + // and passes MIDI through unchanged. Both signals must arrive at the graph + // output with the correct values and timestamps. + AudioGraphProcessor graph (mixedLayout()); + const auto node = graph.addNode (std::make_unique (0.5f)); + + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (node, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (1), AudioGraphEndpoint::nodeInput (node, 1) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (node, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (node, 1), AudioGraphEndpoint::graphOutput (1) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + fillImpulse (audio); + const uint8 noteOn[] = { 0x90, 60, 100 }; + midi.addEvent (noteOn, 3, 5); + + graph.processBlock (audio, midi); + + EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (0)[0]); + EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (1)[0]); + EXPECT_EQ (1, countMidiEventsAt (midi, 5)); + EXPECT_EQ (1, countMidiEvents (midi)); +} + +TEST (AudioGraphProcessorTests, MixedAudioMidiSerialChainProcessesBothSignals) +{ + // Two mixed-bus nodes in series: audio gain compounds (0.5 * 0.5 = 0.25), + // and MIDI events pass through both nodes unchanged. + AudioGraphProcessor graph (mixedLayout()); + const auto first = graph.addNode (std::make_unique (0.5f)); + const auto second = graph.addNode (std::make_unique (0.5f)); + + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (first, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (1), AudioGraphEndpoint::nodeInput (first, 1) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (first, 0), AudioGraphEndpoint::nodeInput (second, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (first, 1), AudioGraphEndpoint::nodeInput (second, 1) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (second, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (second, 1), AudioGraphEndpoint::graphOutput (1) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + fillImpulse (audio); + const uint8 noteOn[] = { 0x90, 60, 100 }; + midi.addEvent (noteOn, 3, 3); + + graph.processBlock (audio, midi); + + EXPECT_FLOAT_EQ (0.25f, audio.getReadPointer (0)[0]); + EXPECT_FLOAT_EQ (0.25f, audio.getReadPointer (1)[0]); + EXPECT_EQ (1, countMidiEventsAt (midi, 3)); + EXPECT_EQ (1, countMidiEvents (midi)); +} + +TEST (AudioGraphProcessorTests, MixedAudioMidiProcessorPdcCompensatesDirectPaths) +{ + // A mixed-bus node with a 5-sample ring-buffer audio delay sits in parallel + // with direct graph input → output connections on both buses. + // + // PDC must insert: + // - a 5-sample audio delay line on the direct audio path so both audio + // paths arrive at graphOutput(0) aligned (sum = 2.0f at sample 7) + // - a 5-sample MIDI delay line on the direct MIDI path so the direct event + // arrives at graphOutput(1) at position 7 + // + // The node passes MIDI through without physical delay, so its MIDI output + // arrives at graphOutput(1) at the original position (2), giving two total + // events at different timestamps. + AudioGraphProcessor graph (mixedLayout()); + const auto latentNode = graph.addNode (std::make_unique (5)); + + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (latentNode, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (latentNode, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (1), AudioGraphEndpoint::graphOutput (1) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (1), AudioGraphEndpoint::nodeInput (latentNode, 1) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (latentNode, 1), AudioGraphEndpoint::graphOutput (1) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + EXPECT_EQ (5, graph.getLatencySamples()); + EXPECT_EQ (2, graph.getAllocationStats().delayLines); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + fillImpulseAt (audio, 2); + const uint8 noteOn[] = { 0x90, 60, 100 }; + midi.addEvent (noteOn, 3, 2); + + graph.processBlock (audio, midi); + + EXPECT_FLOAT_EQ (0.0f, audio.getReadPointer (0)[2]); + EXPECT_FLOAT_EQ (2.0f, audio.getReadPointer (0)[7]); + EXPECT_EQ (1, countMidiEventsAt (midi, 2)); + EXPECT_EQ (1, countMidiEventsAt (midi, 7)); + EXPECT_EQ (2, countMidiEvents (midi)); +} From bdc4984f7e105a11b474b0e06eeaab5b6288e8c9 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sun, 17 May 2026 16:21:27 +0200 Subject: [PATCH 09/35] Update readme --- README.md | 9 +++++++++ docs/images/yup_audio_graph.png | Bin 0 -> 63991 bytes docs/images/yup_audio_host.png | Bin 0 -> 584928 bytes docs/images/yup_audio_waveform.png | Bin 0 -> 258573 bytes 4 files changed, 9 insertions(+) create mode 100644 docs/images/yup_audio_graph.png create mode 100644 docs/images/yup_audio_host.png create mode 100644 docs/images/yup_audio_waveform.png diff --git a/README.md b/README.md index af37eada0..d34125141 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,15 @@ +
+ + +
+ +
+ +
+
diff --git a/docs/images/yup_audio_graph.png b/docs/images/yup_audio_graph.png new file mode 100644 index 0000000000000000000000000000000000000000..39a81263007d8b1d2c683a9eeb67afb7a6294f4f GIT binary patch literal 63991 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

8G|LgeeMG-dp;pn>T^S_O# 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≥+Yt4 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 F{vTN1EKvXe literal 0 HcmV?d00001 diff --git a/docs/images/yup_audio_host.png b/docs/images/yup_audio_host.png new file mode 100644 index 0000000000000000000000000000000000000000..5eb198934c91d2264c09c4d464ba1b70d7e44640 GIT binary patch literal 584928 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 mCC!laTVH71e!N}uEfK2Yo~C%zLr@CIn&qV3z; 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

>vg#>UdaLV0Z zY7W$RE3H1Y^UMZXAudH)TD+|W5H!n(74A_)UdgBenr}s6f`=q=70ba-TQI3WExI9I zWQ)NAM`8HmK)Q_}_X0qGAezkP;@V8|$$1=!2___a!@Pn;gNT~QoeRv*Md++^ym#-` zg^QP7fAjl~AKlB|94zy90G|?b^K(SQcu$`BcNN-lK1`j z7$z_oy@=%CO$mmgz=wy1hyM9L{nIyII@xuyn|-I4o(5qIXO}-_%6JDKDz~@G-U_1= zlP$!L&AxyLa)=qU1><0+9nsGswKQM`W#Ev;H;mL2tEy|D&>2=HL?#ADnBs12WzQGe zO~z622Dntmx6<%J37!Ox7D-5d)-D&?>P*y_p7`1lkLdGd_2lbDBiQiO*lnb75p z$s5h=Qh4-eP?4c(LkgG972mSGc#Mb$(XiDPyXJJXxABZH`6~{|7Tuy`=lT4v?|l2O z{^0k23d|C2(x<9I3M|p%!CQ#6t=e^o9*P#e1Q%zMcw(Bs zlcF{QASAsIx9>E{1qg7Db5H)}kO}+&i$5agfPvTwlLVR&lM*3T@Bk29d|Kk^Uj;{E zArllKmO#@+OdOFS3E~H4`2mzgbpgeAxYXOR`^E-uE+tr@bqqXA0M=Qf&?yuX<%d(j zt1Y=HsFDlfwHFkSo72;?z-E(Y+r^L@^OZx^$)9#!AoyJ(T(!_f*M^Du@$a-kqDXFl zq$+5-bNkliE7x2Cu(iQcf%q{Vig%b~evYk-@sSX6o)t2^r#}4fJ;oXcIuW826faRK zt_aK8(?&Stru77|s2H2GtTXIQh{nrIUwrxX$?*~P+{TBn0R+=h_l>JNK2lfT zc=_scyiIFje5CZZ4bx%!cf!Z5s4QRrM-o)ZRM{W`rc*WdU>-b;OHp~{UcuXMV1$NB zqL}k++!_YJj0%)StffGa58x!rAg0isA!@lC-v%`5uyIcyE+(Q8kw-egi$umLu?}WM z+}Mcyp6=eg_0A7|bm7u-voljPhy?xFxM$Oic{bUug-s%P+@j(9#VaGjg9{6@I9sM) z1da=$mec?vbPh5h3zLdc=z|5rke?98*7_0^9wj1;tD#+ zb#-i_#EzzWM{DX%G`I3L1x6@ZTbri`hwv@HvA+so9QpF3GDTQU`QE?bY73VjP>cV& zJDVLHoxEiT(Af(_)S-&#gk?ZYjl8#EVPbNcQ4o0+&$tPv@s*HDo9~<8aVW*E>4UIT zD06dwP)JyTD&U3OcZw8#MTfkGYTRweH@*?eM60+9N@6zH%dM|({ncOoHTV9L|MWj% zSD_e7R`sfI(midK| z;UQvS_8xOaIcv99*O=hf_woiIriXcElQx2uk{-|GAKj%`LV_rC^Aa5^&^ICAMJ)4_&?b!tl`GvHC{nT3TLV zyAc`;(%}kfCy%I0exilQf;2cB5C|=CE&VNCXuy^*s3>JEmivXlaF3QWTBk479t{S` zK{il_r}(1Cq9o~z9i%jH)GZ5*JbbGsIkLcvJ5tU)AW6AHHz}Y*m1*)Sv+@^vjJlCO zHCnyQP=a7qbg^h9KAEtWpc9vO>g4IC15bEa-sl*c_%8+hgY3lxeBkhk8;pX-z0aJz z@Z|BsbLTER8+yu2K2DSPo(#p~TOn12r&gMdIS{~8=jpF*07?-aw69a_dovh40rhh9C{bI4)>M zMS*VHv5e^T=t|N{NM~F;2bew`c=oUV^v}5U8^8X87hZgxjI{3f*ST39(mc@8CL2L4 z`ly}5WM~&i$OJR*dk*ynrzWTW^}qTvw&!{N+H*hn;SYQ=AKS9y762fC9QJlko$UR! z-};B2-Tay{zvk8^x|ISp+$6jJ`CPCS(8}6JAN=yxty`_l&Hvzcej6_Yb><_d&c~V| zoZ5k@M9_Iov5ewU?3C^hoAS*kdg=`|Cr9zDFc6kwp4uipk#mKM&Km^8`4THrq-q&A!-E_R+ z&9~nsDl$?}S?iJ8G+&YFQdj!1Jw2z0_@}3*5bVIfBk-XJLZ6tZXbM~) z<>i-O<>g&8wCoSeK6q@Z2^>aW@bKUo%>AM;U?m9+Emv?YQMlSg zdZFxPLk<;64oW$x3J;tpCwIID<`8KC%Qz;)=);#^d^s@i^wi0d*I#>?u>drOa=0P& z6E+Lkb{Vp5-2ji}I!;Xn9F)pW8sJi$8d{`G{o##n4t(J%OlH*+6Jop`rmd}Gc<33U z*CV6DvNQ)PyrUp{j0GUHvKbYU>N$1Vw#*?3W`1ZlLMQ;F>cG74QI|Qn+?kwXqmvz7Z7ofWc<<~+R5bDsQ|b(M(uTLRoS1p` z3#MAaAE!q>j}3nooVYl@#_?1>mBS$ z@TE^sJSVF@H?~nM_}kLjIx;-G`|36u`QzD;23bI=$WMJiM67A1ZBsebRxr0quc%4l zmt4}yS6rp5?(q7SIT%Q~bXM3!wPxUwU26&ZSp3IYdeBDU>@CH3@imYZ0wV@cIB<|L zn;-qgk1k%kNWA#h@BWNsq9ABXgy_-;;B%Mf7u=0W$K&gwa8)=^2o^x;UwI-ksWF?$ ztuSDMe&-DdICKs(SLCa&gb4W$vnd8xPB6KZm1TzIMDoxfr}NxNi*`{l%dqkf71T;A zTR>`lu-~dF$T?-{(NY=Arw@Df?0E+Nct69OV$$A{zG-riVT-1zf9 z`xBl8p*M2z{N+cFAK(_jhbWfO8P$YJ7;VRSv0B+%7#SU9wS#fPW{>h?_}Ja(Yp-qo_3Plp+rHS$6Aqx>;E9gIQZ`kbnVTOTn_#KH(C{ds8l-IVkYE{> z3!@V0DmWSo9a^YyOiOYVDazm?Quk%9CVYSVWU#ZdeR7Jo>Mix11`-j48bx;MG~lO< z&3R;{hykN``Spq$35rO8U4zet=&_(t<6{%gUo$?o>b!QNv7v?yD&nmL0h3@9O`5HM zA*q#;xPKTPp}<5}Cr|c1e)RZzZ+{POu$jI%l!%igwubhLIMvU?Q&o4q&woY(PuseCY&9@PTsb zWGX~Og^>k#7y?CmteylboIbN!tPG=XjmOtoB?;0*S#AoR{A!Umy#$lwf!XQ*yvSI>;@FLP}QA;5YP3s0+j@WHzbnUDjk#2eF_bECa{C6)l+5o83IEEL;I^J9ZvHD?_d$}>4J z`qMuj!ISW=y$csF-oJkrX|Z=8O2tta8Tncbq%r(VV9s)0EP`#*@O!Sg^d8h3P!!sO zs%jz2xLv_DXR!pf`Wzk_!FBLX3x4Z< zRjT2;Z6Y829$%c(?86g=Ul_jPwH)w#YKnIgFX~SZ9AIE?ae0YCfI3ohgzfmjZgyt; z!(WVbboMa0bh@wqsnM`|eLD*4Af?uw(iwHoiA77nOfV9mFZH2CM)4)te^(M5WKbVG zODj)<1t4Bn+1$qPH?ILSQWU2g_()bNGs?)zWN{@FVmi!c?Dn}qt^(Gs$=@8Lc}QKl zkM^SgeYCEIE&n-@0;BzS!c!@itudk-M%uR ziRFNj;z)*8fK&*v%Vl}e;ug7qLsVRPj9>~<2QU>Et?z8JofvN(djKYaLL z@Y&$V@F-w7VEBa#=gyotbL!Md%$xT&@Ipfn>*?xw^5k((PahAV%+Jj*!XaNOv>1ts z-|lWheFF~|kBkoWc68EQQ|a8o95EBGj(IvjkT&?uH@8bgd|$qD`Q)kIbLTG*E7R!m zW)3K2jUiie;;+#!#;BvFV5)P6o3_0)gd8+-und2+vh8&;9zFi9-jk|ZrHGQtZbM@GlL ztLXgsb5z9piBYq`!6$UcK2jP%>&n%u_~oJDX9VSDS3@IjV*~w^MNnaqGr&?{!lM75t@i-VE6WZukplrDgCG$| z1m>Jm2X?dbOovgJku+*~X1%toDrr+`%igkGTf1B9@>*WYv#zbxmaLU$q{#}CG&4ON zNOm`KCP)G#G64`E03rv1AV}`_o%gf5$MPfi|L=eA-g8g7=bn4-x%Wa6&mvwSjXkBX zJ~{b$7(~WDU80OTqak6(x+L1#2``4bgAmyn%%QZfPmAdf?KwXvDP~XCbYdf@Wi&|4 zXea1s!QzPYBqP+>{^8wi~xf3S9t99!(z$4Bh zudQ3TVtI8}S3Bpr!1)AWN)Z|ox5Q{{)O;0y=OioO0ExU@b>{C~l$7lzk(}))5&?&s zxT4xSnohfF(-X~VB#Ok8!KTVisUQ(7GjRxv)PsW6@(YjY&Ys3nE4G`^rguKq zw|YB~KB_{&pkP@+DVTA?A0@KBYHD(l<-E%(D@I4jH^LA&%D}k5?c4o>w+C2oAkpw` z$&$u~dX7)UJR+Lzu8!D>^KnCcLu<=*;NVh^jIcCHzK4=n7=mWV99htm5P4Ely}i4; zduC>m-f0LI4KR7=4R~2|{OzS?B?yatAkFc+2!b8e>47m6jTjh1)zat~nTNNsH>!1_ zj0hAR^PznthI0dQN&@_#BIpKCnY>k19@VdG@~rnq4;e(~q*uaVSY$vF4x*)R)7Q(1 zS*ZpQ&SSsQuPS3^;MUpM@ICykaO2DVhuL}MDX zN#H0esFDr;DK!eq4_3ej4A1IkJQ8R8=dpjR;7x2)<`Z}0Z4+j+vq&6lrSPMA+#XrgomJ3yH6 zYt>gtY9nBR?y8YSvP#TRVyBzb6_A84By`|V6NXZ_^ApKJuEAeXOUDI_Tw(+WCNEQk zQlwxtj?_u!#*vGI_HKQR?!=xMai$FbLwyN)bxbMBN1R~Kp4s@cNRyIe7+A3sqMlKx zUJm!BogSmrBHrcPnX?~#_;DhSm!5yBx~7JatYh!LfBx)w{x)si^wcv?Ew5V6zzIJ2 z(&Cb54jry2D?fAQ#MszKdhDVa$kRxx`?M}Jhm5*y-?4k$#?5`b-D7v}-Wj@6P*{Lx z_1@3l8y>M5Z(6@*^XBzAIr*KPT_?|+Z*6IP@16JF{KA`Cw{L;!yLX3{S5+^ssav~x z%^mjPxILh6snyWrQH-3FEcj|;z(CH+FTKj-(Yx=xKRDP=t$6eGH{N>lt@qx4ADf1? zx^(X1ouOes;52UAx{<)Lo}S*1Pn_nwuZw9H_8;8W*wloc&~kAcAxvMjs$uJvEoG%; zEGp^f?5L})BaCBYcqB8&CP`Ehi6KLkA(=MI0OsWj=ci}fpn2E!O>5V#VHX_cDK1{Q z#;Jk?t8Uu5Y30h5$b|Yr8f7dbH6$7dM5)&zNME2EM68!FO?-*%zDr^3MKPIAM`Uayc7v=}=4Cdz%P){|6=XPHRK?G^L zPa?}>>qde%7-CN(EEE_wAKeu6qla`sNCcnBgXshez6S*Jj!#XHvXy=ckprl)p#@Hn z2Xt#0HvpjbiDe9bk&W^fsMuT~4T|Ez=GgusD3ZfrYDdB`ssxaOZfylw_#`>wZL#b* zjeYeN?lVRovI+l59@+T;f;AXe5S-A?QN3c#f8Is$B%nw}#~F=Yva}eM5uF{)FE2c2 zI5;J}ngyUbBcD8Ufk^mgIy$_k2+)>xY+LlQ9viTs&55FzrNonCkU%Goi>E3OI0BX7 zI(j;DKMDWT3 zmbtNcUPou=zP3``Xm}=uh z$smEj7q8$P>A*%YN0@>{2U!X_gJn8QL`Wsc$QU}{;W89XGJAzQmtl}1HKv6TsR2d1 z-y%&5thTWlJ%&^RHL~zcTAr(cQlt2IC=9U*_MmJ^FpjwJv7IMb=nVtC_M&(0*tK)# z-qst}#^^uw_YzXW)bQntmj(uhfVO$VTJ~aN9}bqZe)#bzdcPOXU);NY@9MRy0Y&U* zEcU9Zs-%hO=xpD=Z$I25M*hPOkHDw|l`tyPM@=ww6s@Vc!rMg{d+1a%7^X@ zf*Bz+&mI08t;6w;Kcv+M2jwpEP=gAg50<3{Sgh)ST4+GPj`p1GMQyZ6MO}819c5Qz z!~CIO42yH@1k)D`c`|n?2tYl0TwYm8Qf7PTxT2-uhR6+zfg?|@n6P;AL!ik;J9nS{ z0b7=5EZ%=uyp-w2{KBP07z@{faB0DSmBu9TKz|=S6%>ele^7#i64;u^m4uoYB!KOJ zT8J_qq)T)E?LHwhu8^Mglb`(P%{RZ0$Qj#=-?o3>0R$EdLEy?i1e)ltKdB^_E?uBL zFt^C)B6uNT=9TIj8aHg%MBO)0LakW?=Rn?PQId5KhO29MSJ~a&T_;YR@L*sFaFW!P zm~rVBb{#e-RTT-m%`5`~3U`#7ar(^Z!8-$-se~OuiZl|;D#PqY?wB2gq-Qo?ximdJ zRo}Fhlz7~T^cTPQ#kFPzlhXg;Z@;x`$Ho;kHA~B?8A$%hSAOSaTgSJ)_1!DYS8sLS z`qG!bR9jm!IM7#JSI6N!$B!Q!9==1I4H>~OsG(m#br2_-Iu1hq_@fWD?buURv8?4< zGXd<^TW7xqJI|ZGPp2r`D`qRbEw_S5VH}zm&`ee%rQ z?EKd4TdAbPknY;Ki(yd+gr!%SFRxm;GB!j=YBHRs4r@A*1)N7qqXkUwXzMtA>I^8o zdida>r+3#k)Rk7&=HwL-TG2l+_};r8eE+Zh=A&aD??1SI&6+0jB2A26XoX^>g$Ror za#bx*JUYskFP*(NH@$uLKAcoSO__*kzH+tY`VBIC=?gC(*t?CzK*bfc1Yge0z1`K* z`@_He(UD^xj*X1&KCqWrq5j*qYF5@WG|Rq0 z))Vb5T3USi^cg~S*nY0Fvy*TQXgP4;=@rXYFqmg;RRa7^bTPR}V?1bZ@D4|bN8O#+-Y@Q9x~ckZOQrGbg=myMGk18^Yd`5{9?!^cm2O!x~868-BW0o;Sh;>({# z)%L8Ty^SG2qJrqIefN9cdHe0J@Do-u|2s1^_0&_(uHU$UY7WoP`4ltA7DBpqeh)T<9Zc%Ru`tP|>y(9R7*AlTub{q%S-ktx_ufO%X|7865iBo^_Cx42Cq02{*AFF}~?hJ0-v8TJM z^Zc0;_@A0aSgMBGs6$FvsRR?(!JRm9Y;t;X!=|kR1AWue69gikJ$n& z?%P4A%s0OC^RIm6?eg-ntJhnB$;CjxE3dq|V)=43mY>0M=~CQ&ODyBi-QC^bMwF-3M29N)@yLJ; zhN&?(p<_vbaEcB<#@Vwc*#x1!X$^5^?E1vHz3;vIetTOx!`tv=$My{?s%w^(R5r@%6vFe!Z3Pt=B*QDkFRhAJjeDgn}JC`YsL{Gj~7_`(~YuN*W>w6Qpd! z6$+>~)EwrTFkA{Nv?oD~Pnn7|(H|DL&>3L*k_{SYu#(K$nT*V`vI+*>i3y<+5U!+eaXf4*uCtUQ#tT}nqB2TEjx3sK;<8oQs4_x}wx%Z5eqBjFjcHE+U zWr&~S_*j(|{bOyA@U6ni07|iYK~sc3Sc1h+S_J2_LUoa;u04d4J3@Ppm5Ovupr@mw zgQWz30ULxN<*KisVVgJU={MWjI#QZDt|#B3=mTnU z7U!qac?Mfu*8=AVc+UOJwzhk-966*lFy9j>L7$mnS8GcP^AQ#jy=&L5yxi=s{fGaj`N|b~-mBNF;ghb+v5$^1 zOi$nwgIIJOnwu|ivXv3{=#nId7(#4vA}gz}uRrmwzWVkTUOjw(E+kQU zzk&%%ysJO_Z@%)>-feC;PRJ4k6;@QO$XT~xN$NWmU~r zE@MOxJ;u>0p-wtfuAhSDs4!5XS`h?5Q-L*PmXJOZ5qqnfnjCuU?8N7IFh9>A0x=4V zxmfr4S$0E}GxOiN?5k|+NB^h%TniUH#ALgwH@`2H{vMyX&s&2WcL*% zx^o=nDVw3et5>hi%-+LNPzt6Xsbm2r07$7oge%cPgV@gQPH+Hy{4LB3I*KKqnVz_I z?HY~&K2z`|VGSy+HFB|TQUmnaFQ5}kSJrB^5m>C-5HI@6=9k70%sBNi-(QUqXL5T&{5 z>g>Sn4l_r{X@JB(OPR!Z9zetMdk-T~th>j!t}hoLsNH{OlLsdJ#)iP*kv_yDv8{oi@G! zOSN<7w#}P<@Av;Z#+*2rbj#K)M6-N!>^atcg@;-|%HEsY!I3bHb@atS0o zas0^0&@d%7H#g&5u|fDB{=t_SZ3pJey?JORzH808HEY(b-@j+~AOG>!e*DvSnBsWu z@Zqx3@(X89);H8MJWY**g>c64ikLUoXH9#E@4KY10G?p9UwYwH%BCBSjRFNGdYLm} zT#Nw-_#a9Yyb6{o42B7;>~rJe6F>%gF%KDF1p4~=7~aFv3h~I_xKSJ-7roy8+qbPX zN-6RJG3p3UL=JAK$h@S-zyK1}f?RkMGZ)UBASM{B&YV7lcg2Y8AN=DlZr!*BqnEQJ zKbIz|u#^UI)0VA!ckTS6Kl(p^@&3`HM?Ze^3$GU~ExCH>d`)dFt~^{JelCY#IKo5i zL*n_+XAQ!_*rMSO3~4+@%V3;86&C2i5Y;9yO_W<{R+N>x8EjTgaS477k(1b%hb#gm z9b3Btfk>W=+&rdTvU4);;b*CJazws)pKLVBcXB7pWmSFL5O}iW!E?=@rUJC#)4Zn2%3VQ6k2AWOSsXtAk1mK3wE} zz$#-03DR`|<#ZS_HNegxhm@dExSzTCc?_1c-DpE#;vX$J0ZcTm7%37{rrTUQWuP%- zFp&=)B02_L7>#3?QKJcXdH{BVlXf&Xo1VrNV!$A(2KF=WcH;Pnyd_J2_p5KMUt3RE zY$i+8bRPZ)V?<1DWoOqnHhkrGeyh8y>uO6ISnYpm{{!sG%nUvd4$ADkS?d|($qE%D z=7sEVU~^f^=J90&yevGpbm3HBlva0yp3&lTtpX`>w1XvA&eS z&a2H=pytKr_kaGCrEgyV3W zGyK7pW|owdzxLY8bk@KAt?#$rY_F?Z343XEiV z3Qf3l(eeD>|Hr?%bNdETEHDeSu#juajjDm;yS%#kOJDqzp02LfTvMwl1lh{YvE3v=^q``Ovq zPCF;Q3O;2L_^Eo6*H(=v@;ugo_ucBpV<#OtiimNonwu|TC8DJ^F)C+2RZAH}tL<3K zkfPv0%{hANcxg$|$ndCrUzwww0`r#Z7n#R|934kXaikW<9)82O#2C??03St1Ml1lp zW2OW3!)phBvVafcRlU8P1O2^oGxyl!qPU!0aF&u3E+y#Eop8cU!$RDcRuUW@J+LoF zm~8{HqqekI0=={6?t zNtUpVPqgX!jcd#~f)XHj0u#b2Ilj)wE+t~(0;a2i0FPer1#;kD`-GEIy)w(r?b^{5|T3{g&|Bruv;p~N`#zqY3EVyGj!qnMFtW6(eJ#urfDUW?lCd9G>9@b)gU&mZu+mj`gRw~ zFS~lXJ9~EQ*fupYg;U4?5Q|}nX2Fu!vZRWo)?ks49*jbB^+VJt7zaEit{vcJ5s~rP zwz{$;5EXsRCOaLHfU%0=;xcMDd;QN$O%f%>5<c_)zw~EwW4YDdZxONCsM&Z~XhwOo=&ARs?HgH34 z;g!;wpEsx|>X+Y$8>UrNE~BBst5W?Ow+%$Rrp(E~o#j}` zH4Q8G@7aF(+~tn8&TTuk&CktY_0S=}0Ui~Ub|vG`CI!JSMPh3k!hp7D2jDp9jg1c9 zywL`Y2N~(f%f&Bbel7bkJD0GkmV}x3m*4yT4-V|vQBs&UdUxpP$7lNc`!{ad2p5%X z7=h3oHw3XhTRG%^VtL`M57RRUbs8KTYFbsjbK3@9Qs3M~=5baKs4{RdLOwn=x@9AA zS@mu0J$NlEs+VK31)?%>Y18>k?xU{R;xZ~g6UbBxI5UZWB0YOx$A)!{IA?HV$x(;O14EE`XiR$I-&qTOrqRk?Xroclq#((M&aY)8y zA-)@JH&FD&#oS-K_s;zNdBjZ;#?lZK?hGBes8(XkmFTJ#Q|Am*B0-`Dn?5a0O(a^A z1n8a}O*KU+ke&ucrABj~Xg!<|QJ%2ifWWA^dog02mB`E zOGyeQl8FxMl7ix;C1UI)q@>ns&h*tJEx_882Jq-fS}Ub4kx}FYzPmItxgff};+t~) z3Z3V}TKTD5!suC?l>!RMrF??kG={yWrly!wE-5a#aPd4lVbUogm{nwCI+5nEvp_*d zn+I4x@tguF)R0HcGv-)`6sCcrQVHY&Ct9om(B%M!KzP3@L@w%Z)D+PrVWg9>sZ3~H zT&$Mi+s8f-UG`0+K}9G16_S#(X49_34d0d=EEfm3he3oPEQ^aA78G>Z>N7)4>zTsB z$H5;t#KpY@Q#`6m3G51JJ+qF;OGJ&zv9&T!rlzI~sWQC)06+jqL_t)qU2P!`mTJio zHqf4B5bU*2N1K z)~sF4#M$)J6p>X(1nF5Epf&!G%3^|SDnj$sEA)w(R6X|L5d?--;Am17h0^a@vBFKQ zLe^j>E1^;d+}apE!PswYLGTcEWuQ~#WiCzSq0d$9BsHA$rwt};L`3Onc2GD+8u-L> z@aQex15JQ&yaH%YNR}ZJ7%LmZ>(>;)%ZGNaTDbx)Sw}u?5YylZ$PNt+(Xzhs;&bhP z`WJ*xFx1Bc1`Q&!Uf9i|vQmmoOd`lx1p-Sxq?t?H!Bp zEH^xZWiBo|c(8E)X6rRpDN)>>{m*iw4R(WC4`2?T00JW6GcLa^g4*rxD3Z57@&Lg3>c8D=L{lblEnXrB2xF z>c7~l0UQC+9>5R199U5$Fz1H8`Q&v{EXV+#%p_#t`S5_$l zi-KmRr|{Ll5@Z#lMM1$L;RR?f3}dE%t}_uiy#3;rew7W}=mgHrpdoX-MW+xH?b*Wo zTvx|UVyzbL&u-e#L}FskncW~STVT)?N6-vzW2}PJ=tpR(1{`B;3lje!*rtH&e8VPI z+;RKm-~81Lo3`Oo%}q1QItNUYgS`xnd~o!8-~Fp+4(_QeFJjnr?V4329==2Dk*q~b zAy4Zhd^MIB;;b?NR@OY{4H|`3;5m`^&9~lI)v%VeXbfnv_z_XT5$ZL|aPG}ab+om1 zw%ue81e`DukByFjB`q<~5w~h1HQY%BPv{467}?a+1V#-<6W+oQEG+3>e)W~gs@l1k z1qwoBtUI9~yduIZCdTfz-MG%mVR}A?pV>oVwspWm#kriFp24uQ&&~Y&9OW@YZFx2T z(vlPl->5dAgehbl0CQ5fR(NL63tt&~a`W|+%26j&UwFk=+=U);8+8Shum&j(#Djcv z0<0BLpUAAX$8HWh0F>Eyf@(&6;i3CRsNce&S(QDBnkj6C-0->?xWYAI&nic`s5;yL z3AYt!UT zk}c*;IiaX-8l1?EBG@ArubPfN<|aXew}b%mJgk8keZrsth*68>*aDgb0(=Bgh$Ya2 zYPcur4gp$Re30GpB~g12T*a`6XTon#16)9hWJzjLLeH z1ES!6kS4GE`vgeTL#z!(ibGv6Hh87$H*CZg1ac^~bp)7631SXA;)?{b0^b+c7F&ab z*2F*gOYT#jJk}J$m(ZS|TU7ZIf0+go_zIHZi9b~7&s^b?13`|lau30 zCdj5I*ZP}r~(n~uc>ZOqfHo?EchtMAJ!Ofe~y^B!!JY zDFlx&5D6T{n`FF+Aw81d<1+V3$KcuLo?lW}K0AG%ezGGG>~%+YG&6u%nInU@;mPg( zTgxhnw{2N>mzAI6lYnFSm*ujH*u=Q=_+eB7^N;~7a>R$iDM$T*!~El?3MFsUri6dm zHW_Kx6w2tSl$O}7q7Jau>uZ1fZ@&JGZ~W1}{uh@o5ra>?Q=yPAU2C$_=Ic?KTbq_# zzchjc@Z>@_u`||KA_~n29^wML9ER?*^QNXJW~MoiQd~iZr6_0*9M=+wG7CBtIn5$Y zud)Z}s1i?@FG<3r@;_c=m)4yT*G!YlOc-iFT|i`17WC7MfwYW^u_Zo{7G?X!%~hY^ z4QUi_Vl-aIhGHe$Ty!Y4ovah&K~nw1zzX1uhPh(23)O++Ezilyz14AJZebMOq8TMRDEC{hUh3>< zXAu`Ia8Y4?S$WB|>n+b6e%?y+V4%(WAzFFt#4S7Cm}%=lf_?iUs0*VZm?X=#1x z>8Gg;7!f*=V3^F2!$W1ENS&kL);|h&0pkJ#OB7U7TT@(G&h*i(&YSlZ?$d9j`&8s= zvbp9<=UD^zh~SUMi)F=2Nz4v)TefaStSr%^0~&e)Fy06lt-x?sSJ%`xtU=|cCP$Gt zTO0TF53XOgn&8Uf(n`i!`g=QY0UnnuX6F|ovdM|jD;G`!8J<$mvT{x;OheF2M`BAD zgsZMywQ^O{;9%dao^B|Em}r&4<|IFS5|)7ciwi63RyH-QCU0wN%LGGcu|^M(P%E$y z$eOQcb3)G&ys6(92ZWL87{n_iqCTf%AU!)i{2o8Cn$f%UE&3C6)EXL$)tP3k2CgwL z;XT|-0!sWvO^V(ssHD+vajqF_1HFQcI>iSxQETWGB}6&vW%rW6AS+|kFmeFR3WI84 zKnr-jEfz;$uf}xf? zl*d?2^XJ4?1qmpj1przO3cznl9-s`2FVReqjE7{CKX{utdN#;`hgJp9mjaw{T`u<+ zPH@4Zq3w?!KKtwoIOas)ojrTjb$n5ls90^-_{SMu;PIaVK@u&23W7cO6vKG^392a$ zgjN6ql+c#Px?~Wck}0^TEFoy_Wr6sy63y)rr6UTI)kp^UhTV>WkZJ;mVw9S{MZ=eg zDpE+I7z+&NOTND&mubLeae+x&8Xgrdp-eDn2#`pu?TCnY%Jx0B$?d^7>@WMkQ_mI@ z7(>I>3=d~#qPvxbr7A8hBS^fb{RSJ*(^BBp&{v=x>h8GNdcB#g)~T0;`MC}CwQV=s z4jpe3#ChoZ_e!c8X4WRI_~9`LyfAvoguZ_BCu}zHE8z>a`oO zX#?!=PW(93+S;HG)-U=F&pZQxOGk!>E?m5Te~eD?AMWFks@b9ZveJy8D%)*t8Ox-b zNN+0m>}&WIQpS^tThp6ektq&1PKTNC9%>+#iaqo)A0@{*@SYT=w>l^k#(1oaXf}x( z8X7Armt)gd3^+MSgQ${n`29;eNVJX(h$1Q^L9q4fYee%;|&ZI#YnP z)m2xn-ehH4WkngIwmEtEO=~t{T?YpGh~~uaNzhgbBJ3}f7JT%9G8pMVk-5o<;Y?gm zFf%$eNgI??U-EMeH&>wz%6!N~Z9!3KMs7J(CRig}D8l`s@={kx=>vI|x_;Tg`s6YD zdf~wVgAoq39^YRX&_iOS=)Kt)7`&>!fiNEi_wYwpL4cc2yafsce3>5wQ4z7u8Z2L^ za}althup8mINp}jSY4) z?eV14N>}nhi3G;M7YI(wXtITjY5^YsLeW3~1|Y}-EKB6TGIrn`9|L;45TGD3GYHJm zpkQVANx^W$?^H&?W@fUnCw4O{EfXi2LrG93*@?x8+RhkeZS6`z8Kx#Cd=(0po0Z;bs$H%DTv`B8?#ymg3-~!Bq6*ySsXPmhfZw6*3CUxGR?kxZAaySwkq)mu%gYKQOi z;uMpaGLB=RUssR-bGuZD0rgfm!B^DVb}pX zLb?ei3R|x(D_=(QIx{iO>l-&ZJ~-9<^7GFW=4Z`KPe!@yZ<)KivZBA=Np!}RX)<9W z^Nb9CMxz2GD@f>1;elK2S&vJLOVK%;S9o>e)cIvAd)L(0^!0WM z#kuLn(6YR$VrpgvouwcKKkzFZFw7!cDbW^7LK@6mvs(nVnf;pS<&h91N{R~ly4w~P z<_h!ju@ubCa60jY=C&1Ww~Lo#jSLSWT;v2liaDxG*L)F|7v~{6H-C9`9o2}%GF=bb znFZn%6#;36CWbQE=%Sw1XHH(JuWy)`m?Vy>a@q3U+XH1~Y{!E4Kz9a@P*iPC9HycQ zP?S1JMm_*(oL@XiUZByWX1Ok}QRC2H(i>5vNsjh_fn*a$O4}#^VRntXdQ7Yd{a4Ln zzyun|Q_F~eq`Jc!T3)JvFI72~J~M;_)woGC%^d3>erDD!W7-AF8B3YssjB0ib4(zW zs6eh-;G}0Z)6-c8q-N-o%U`0F!ke6n#H!7pG~5CS{xJeTl_|`JSjO2%9HTErfPrC< zrxgOSV129cw*`U`JTTUTQldoO!sB3F0V*QH#nxOlTXeCY-0qa=`2acyK(7`iU@!!N zDkiKGB(U7KmT1pB0HPTBI0-fY6^J-RDyAOT3Ydz?Phda62$Yn!g$7?Oi7}wEl2TUr z(M4%&YG6>v9eyGrjm98}VFxKiD)s==2|#4}&FCVXOkw^PE=t8$WG>1RO!Eq8fMqD4 zsY`r?<|NngtP}#T!@SJgKW`HCw5cD7k8@Y49V%vIN5{BeD#>GV|IeC@eA{XGNS zEh9sB7_>2IQ(H&1W0TTH{fzp`;m}M)OU+^}j_qlS?diqKm%sxw zsFiGJl3SQ;G=N5dg32tbl@Bc`Sz`lX`VunOD^wZ)kC-2ZZzzHzK0%+1CM(Jx4I&*~ z`cIe;coMFEphOy-dqbaX?v=N?j+|*y>sHn?u8b6T%oqg4(VgmSYgxX$8Xgh?NAGlQ zYUbpH8*A71rY*AX;~0(+Ji%@!8_OYWng~oNi@3`xSK$5vjS&ZmqZ~C51ib_fEg}pW zHdJ8BeOx}}y)J5`SM}7Kd5@9hnK1$X5EIau5M=ISldSi=;BR);V4grv=!@r zcKx!01>m?e@bHN_HGZm_ni9B5J;b<|l@u{P3A+JL@u&-F!&oek5e0JcmE^cqVIlF{ zvA5!mBmqY$nl^4k4w(1u&lB~zckkYo)>h6xs;RD~F=Nhu`LY$57}8m7WKWViVUzhO zg`rvyK(qiCS^V_Hhs0hGBF!iWW$`#chPQUtohu1@oia5%F%9TZVF(XoS`GZMi991E z!1HqQ*wYAhlPpNE_eKWgB|NPm6>V~|51q)*&B>%m!c>bMrq)C#!Hu??H*Q?l$FlPZ z;XbE_^x)pi-M$`*${{*9J-zq#-FH7a`+9m>RTaAzvX+vz6NEjAmGpR5=rBE$I|GFH zwGNNm!Nf#7KHzYfP~}YGso(kekI@EPVFpei1Uuk)mCQj-jP{P*MOI6)IXZ3O#QEz> zm*f?43@++~$x{|o^+(L4P!C=;wH(Rdu_4T4mv7W$2D6t^q3f4s}B?DYYA~ba< z`D15WEzBu+-tn<0H=hHZ)@|OtW8Xo>d)Y28oC96MXm?$-PqU4@0LLFKxBhuf zIN3ajs*N(h^+d9yS8!R`lbo)2Z+3QcczR?Q%yTnx=&o1QHGS*b-+5-=wsq@QGXjB} zu!wX>*+#sgvMPykL(i#k#JWN(+zqg*18q`!`wh0XU$c4w|cK! zRgW)&QRW_`eL;H40w2vs#DjwzCRvf06O4J=)7=?br1emy(K}=xV+J6Zv?ZhU99j@f zu*Q5L{S=IVB;e3kdcaI|K^-)TTRI|s1e0lF(0?#ZQ4&lF7bu_!i#M{*Pk&j_;s`c+ zCU${%s6mT~W`;9$8p0O?7wOjq1owsDogCn~;B8FE&B)rVTeqMYrlu<;w8`v-q=8F%WlATX-Gki|`KwDeefddClpFYK* zBDAtJMM!7Ws(L1x6GJNK1kJz~RPWESTzHD&p2$eNKfR;{^TjAv0ZY~0qan7Lg?w3% zN|$^~J4gljX>t6E4IvyLRdD#xit6f;;!-EJ0BX9hyitUtB{wNWiKt-%*QinyDNGnjMPPEN^~TKu2M%7o ze2JF2y1JTqK-j)~c?~TiH;L|wXP6FIA2OsYy0z`?2F`{a`^g)Wk70O%qHw~LQe>b! zTfE)6DEFyfS|@h>LSG zBYoY}o!n>;d-{iu96!(Qhm2Ga%1jH3l7U0Q6I6nN(DH%4uGVW;*sZm^tb~>9K%;ns z1p%4FKWmSIMzIRh6g~ zm6L2r{*r#7^3mM_4<1X33;V%APl)pNZjvDsHGSY~xA2A}&(bQ;m*)T6*}dy#1#aq{9_v!NxM)Kf;ZW z0DDvc&Hl|iO&!zcG?Z@NazaR}SvnGu+hmz%xBZ6MRx>=6`{4j0QN805w0fN~Uqf|f6@y4Bq~J~i94%3V4cZOh8Z8y&fm zUsU?`Z@kF_OFyTL&fTNpVGSq7r`kJvwryhrg8Yf8N!CfgUOKNRw5xyZq{Pw%7=}b3 zJOoRcP+&d~tSUje^2&;ij?U4^nYH|)Z$hYJddAu{8#e9Uzc4$IwK&ggEmp>eoresw zO<%pJ zabcwj5+i$Z$LF7^eEJ}AO)@F+{udKOtJQlBAyGj5bN%7uzuKvqpM_jB_ z2@ukUS_6safCb7Ds(ns2gb`gYOe=L}S9kl^=s4rnAWrnX)xh9T=c(~8(9i@RrBlVZ zCqdu_lRYpbX?l4~>a1LLF1Sq}1T}*{pro56H>kW|U`^yx(Fo=Vq7uxj#cYj$M4*49 zBrntG7cX5T>e1N?(uc(ZAh&WV*gT~|VWhygdC2cJ#p11{GS^xy8MV|o1eagGp>WBT{Gg98Jc{l!$TQh9PIim+pB z0?h1;HaxkK^0@y5E?#U_7R%HbEFT*mg%)s-*H6+v!4U8v1E0(Fu<+rLgPenW83O~i zF$m1;1?Om)NEwolfhpm5ONB_(HuPIKSanAGf+F+K$IHqhCW+p)4wmv!*@Aa!ulgpp zu-4XSqVeQLlp^Af(~17ZTj8yvG7ldTXhZJNi5XM?+mTqRHLKTc-hE(Uc5-@jczB%S zx~J)RCYI__bL*=5Rg5K%++8GGl2J?s;DJv-g*G8n4$;UZFmc5X^@9$8yWYcj?kTRj z|IYm|{?rvF`x$!Qy4Oj=-bWAS?=8dj((a^;|DeX76E$G0%UleSAnLpbma2`;8}JPS z6@~X{X@rh+baos+ahxTAG)z39#tjb-_x1H*$Uh@p6^JasEWILlX4_#)r|d$|^02d$ z_H>?~DZMZYX3795Djkgkn7 zdwXc%;p3$RHi#M7?Ao8!ack(ef9s2Twyqr*yv<=G^be^u59S|mN+9W(GgTSTUU-p_ zUYO5GWsgZh6@wet8Hz(IVF0KE5{yGo2`QOB*^ruG3f%0;q|hWiYll7dkpX|vmY%4K%7U~08<&hedRm`BU zHp-;%TW^W_#15Y!$p8S;gNnYlG?uE)I0=st7 zaYxhI3C(KF4H@|jB0!;eP<-}Zbg7h5;wosA?O2nYwGoe} z#yKRiv9Z3Zt7m9*qN1XlRTjnNRW~|%me_cw>cEn7r>Y_YJYkcm{L!11A(c^n9L zWbOL3Fpt9%KwetlGUYkiLv)UIM7hgw(1?!@;`Me}`@2@dMcTf*tu*Z`g<> z+P!0I`_0zo>+OdQ?#L-BoSK=V4wIj5J^mtp^e?R-cA+fDBEv_7iTpDF^zkD`5vZz# zg-fcIFPoa~9=LP2u6hMjBeL5=qmMGPUwiGg?$U){5#H zL`Fqo3Dx-g7}CT5FA}0gY!hxHURKZ}+f-&43-@q=GdMkz#`4!9_P@d!a>p=Jeb0|c|)ip+> z5z__ayNu3~)<)djwj=oRC1)T?GouYiS+H)Of`I6q9{16@=z)PQMpKWR7bOCmdcBtvUcyVA0 zyFU<`L(OHW8eqefQmKMD0tAPoK@~xLvP`GLEG$U^nNGX2Z5$ic#v_mjaY&4+D2c-1 zL(C*GTmyIwG$s(>#wV|UfIh>3+}r|?AvN5yIAIu*mzotq9knEuo=5}8C>J8eTguMP zV{UV6NxhD?mZWhJss(Dy4AbNkmz31htl)H<;k)eKQh_R! zl`mVjd2{v3l{1smEUYUjDWuiHY$V42T5AU|Dk>|8u8ZX_ONj1<+gw-$8YBoy7i9r7T~Ss5AN;%>Z? z*_>R~4<^isPm(`U*wIZUVajL*5Iz!{`Q+D=Z;GVuVpV~w6|p+TJEjXWvnU{m0!fnN zWpg9Ke!@D9&4Fg4;Ap~JG$y8(16$Qjt_t^MrqF~m?^dC z8Od4_c!-_auyG?)o;lad5vX+yYpJ2vTiV`u!1Jps=9iV3`7ZuEH2EapuT~jtF7(Zwr-)d zW4K~zVKIF#Hpd71#InIamtn=FA)>+1@~FW4@Gp(7*a92U(xov$^u=Y1>k?361df6$ ziKMl&yBLieAXRv$^H93DkecrZz49kaBe502A~l`940IBV$nJKKgz+b}!tuC8SY?$? zUBXi+l;L62o_r9&AejkB>MXPTw0Z zi;4`n+9>skIxhBFB*%gMp?Ts-v}Z~C>NC>Qg+;8Uk|+Lw7+BDZ$Tcq*=p}Ak@Px(* zYivUUfWS%eECnC{HCd9FOu;=QC_c{4%R{aNutvpF7%;_Fu(^?1Dj=ZBe$~!KhbxuQ zmH@UZByZ#*h4J1=5WC~p5^DrZQvFS^3Rqqa&m4!Qw`7>8tAN z0dnR%#y_pLVfFfrTUS)CWHksa3U3J<{H*aG9vwS;_%Oapf8VX5!eW*<(vDD2RD6!Q zQq2%>!qCJ;b*1_Jdypw6%ENY6O`+64NA|%Hxh{L;4fy)RQ6(OkFp?%IQ824;BEmu^d4feqdi?H;k$?!@pjb**U81q3Dp5(t$Hp-q;K^Ce>o;s5`PmCs z7anKUHmsewx7gO+{l@FBmzP!0Ai}u(oa_>M>Up_sUA@zDbFaPj8pCpoV$vWos85rK zX9)`6X0xn$l@L@6y15dPkcoAN5n>p_TDrs>>9L4w${$McgG{mr*7^FJ3<`_(c!`qZ3imlA2VtsI z;1)F{3?ii^ijJd0(UdR6pshO4Iiu|~+Luul4$>;Gs04=NazP9wB2%TJAZ=T~z?$~i zs%^yzOw$v)AN#;qir?Vv>Q!2|UmlHF=o@v!JD_U-JT)ukq{=>8@&DURf z^`$jyR?`w^yE}MZVH^Q@e)2x0?cTkM7MflH7LW?YfGw&TR8k5UQ!3@#o1siZ z$IefjZp1DaB_+jAJ@XV0zW&X>o|w5mI6TU}9WTG~B76pU_KVHSV#E zTAzRY^NjzoX&Yz9(sZ!LE+hpN^ohM_!C6HwC*V;iFK;0olTGtr)5e7Kqcl2dyY}oR z{Wrh+!{O1XRZXke_%WJ(BIPr4@)%8{uU0fYJOA_djE7FG!k7a5f_403!=zq7b_~uIlO{`zWv=FVzBC&XDnPw8&5MuLAABjuf4Q?V0iM< zwYK+?c%)N~qb6tv={`xn6NJ0DdA7Br61_C+co7gz^XaFJ# zslqBU)ii3a>c$O!PTqyyVj`G#*uQ7r`nBt+z1Sc=NtW;%g@yOBgJMDhgEjtA7F<#i zFT?OfyRFMY$%#HD3`A8-XBOZI)jU2;$MAt4&W`mCaK1OO%1y{%^!|AgMpcQ6onlGm z)N~GXxAk+(h~?tOK5niP5b!|^t!MtXa+A=JyW|)q)L3-1cL1^~M>@+-DjCmgidYE= z&N;aZ_kQAFC5ikL8?%cS`K%$qoA?A|%AlG=e0>rOVx%Ne`XIT2ZNBpYGk2B&xv+P^ z87YJWUZGl&6ZFUd2QvZza$>r8VLDw#zIe^AfFL;e7=xOWEQD~a%mx?YGmMIIkULU@ zG^CP8xWt<_Zu#76Z!BB39E%Fj6txKl^~DpelA9a}Ev!{qiP_n`chA;s+e+NGnurt5 zHd!QCXZ4yjbhK#pYHDk+6%q`Jfoqg60u&)~&?j$$%~)_=nMQLAVMfy5YK1I z=MQ$N1W7*VmPe5;Ngi@b0rcw$bPA0GCgmPK5<@R^*(-xBCP9a%mh2xd;T;u{c?kyR z*q&?q_H97@&R_k6jaOGTu4X#e*OB-zxufqD{7%4_+X7o_#y8? zlAt7F);|3{04Be!$Xri2&ePPd#7mPFEhNMNFcGPs)m8jmSNH<<6S3fXBxtE6=)Wc* zMv#U5V(t-pd84Qty z$;A!h*fI^9ViCy>kcB1W(pPFfefE_U+@R zFaM{%`~k4O{N*pxINfMzt*)(Oi`Uxw3un(BXY&=;P!R0p2r+zxep_gs5J$3k%XU1u zix%fB$Utnx-%Q>aP$)fB!z+0s=(x@7*76z0vW#zj^Ok%gy!c z))9c!-`~gTo0^)sn%b3=%f?QqgHejsEZ5<~w8XUAV!*4|*MNQMnXtp9AU=@@JrH7B zPfi$YzTEsT{^kG5lCx(H9%N=v(GriAy=3A6dog$Z^^cBRxO8>Zsyd8F_pL6xx7xZI zRGG2;7@C40g6hVZgJm9xmAs+jIIPI4V^0PviZ>&kG)50T2L&O@x@#;U6PjM2tnv12h8(K%~TI z=q!Z+DSk40A#PIO#TOD|fRG9s4?+w4fYW;4NDk=|8V(-JxFnuSut_u$FaQA!6eYQU z#B=Da_i7lqd$+s0i$jDM+6JY7u}1YpRval+Q&ZS&HOF0|66yv(pWDiUj;3U)bj~ zwvOaLu)VA58?QEBn4BD2y=L{6Et{@hz5YM{FaIicNx`9~pI%x_xFm_`3+LZo9J$%n z`GcPvJ#+3#P4$W$+qd`j^G5FTrBd=wDOZ5wBm3^-H^1)EIxS^$>}FsCXA zW5g9Mk(}Zhn^texx(nCy_F!LSW%>St2Y&Q}AAS2ff7L&5XX}NPKpjAM~skClqpaGQ_5naBbRt15Xd2)oR$ZE!6FP0jiOqM)P0sRN2Cb{ z%j{le@(VzsrnR6%|35(x3lgPcd8uz9%f$HkI@FFGFOvogVR9bPtoI#@97Dv#^V4x_|;#O5KtB!SG zvM+^UU@H4kSyQA+T0#OMp=^eTy1To$Ntj`iwz$YR(AqU?83QmA$)&^6D56(>EOyH*z zBndBNJPo1h^?>L6s2F9pcY@KlB@|Abq|Cn;|S`Ht6 zfr>^>ZA8&oJo50-)a(rGT2Zx}^;Wz0 z?mTtsR7ZQ~fBg^t$!o8?xM%O)<;zz*xPPy!>*l44S3WvDzzP>&VTb>(09`6}lqSI;8L<)|G=tiKE zG>44Gu`D-g4GgogPzFmM)~??e$uzF1N|cz2sQNsKUrJRh?EWcqWgxPs-=p-(HS` z!Xf^2;sEE945Q;aIy$rfu+7JMB=j+uQm2Th+LFMP6emUYHmL{nw76A@((O1OcKcCR zcL(wXf@Pb@Axv6I_B4n51P&af^dtdKtT-9$NIZM1uSdGTkItXQ-763SDUwr46ogmz z=&|ejJQ%0T)otuVBRLs(a`mg~msKu{hCeX3809E{fI`tI4978nB+UsWaxpDPbi&G& zb$BpT_AFy}2*Bca5NBbu$iYa+QNn=?RYo>=rFVCCF5bNh9D*-+OD)5Y*0c4|lT~aI z5-DMn2n9x;kQj7nUcnF`^jmKAz=~+CC=!KPe1KhIeH@HPN)LjZ_=Z;~p?V6M>FIP% z?y)eOm|8eO#iHG^ay{g@8Dbf)UcGd0ezv5%oYNPce&zs!%}f*ivw!-}_w3s7+~Mch zI}q{r^>sC0Y5v)}AA)aVW5eMW4o^%?wBBgJ+_khchl{We7E9-_OLqnbFl)Q_>~6k%<$K@xEB?OvmABV#+*ne?b`K+*3je{86TP?H z-tYC-U#Bg*a;2H|I*@bh=m)!Y?Sb|QcYAUZa>-AasmkF189-sU6=9IB!4G{-takY+4p1tA+r^~HZ+g{IJ&MRTolmBLd*?JUjBu_%E{!;-wiw!u9V2oiV_ zl}~_puOwpEA6dhn#Yc*igXK>zGbR`HUGmc~GXO{!$!9=Lr(;S&5||~GK`V*htMaBK zu|XgE z!CI5J_ziGHn>8~#H#Bse;YUFE%J=+F=rm*Uz#AY3AtV4v#xRgdVh-{`vB-o$?CR`d z4XXGhtObUp3ow|b!s3@Kt6HAC5NF>-l1RzG1oUxKGgIRjuXOgsDk>q%)*V}L9p8WF z-5WRB`7_sNuFab^Zrrqq;b<0Af?i2U`P}^dnd#yFzw2cgHr+Ej{n4@Jtwh;x)~_MJ z=;)E3BLa3Y<){ptLu%=3pMULx_mA{+-}<|szQf;V%1Ns%FWa(xds9;rak8{nEa+v5 z4)@~psSokbFsx95#L+hX928|H#|JbkteDkWVQxCRI&mMLdg__oJ9nQta{~D63$SWs zJ*cr-XYlsm>9gng`)s+4EpgmlePbhA+uy!@J3A|v;Ww4RX66wJaP-&_V#Sy_g-V|2 zm1pAj7#qQS;?8Y5h@T*C;(h);TM*%pJ9qA^t*d1>F9>9@ zDsQm>kTP<2_|%!>wbgYyckZTzp(*-A&g_G7?b^jlXFsOl$yl)*b1*bAvV3{vj-A_E zT5b#s41M!k-~DVU+^=qGT(@BZ@G-$t)6;czt5~TpF){Y-zx*?1NCAi|pAr_fSnm|N zWF=!)h!Dmy@>zrYD87mqsu-A1+c=H`Lqk(y zXTyh@OPzq8TRjtFo@AV8ZlsUbRsl6TDoIjJY#VqAGzK@=iW(%~jG(OhHhZvfY^;qU zNP~`(SF0M891!uux@Pf7d0{>vjcH$0H*B3{@&s!zJvBKze3yM+L!Wu1Ag+~_E3Vv3|d5gQ?n`ae+!Us#E#s@N%)n&K)s z8i(U38A(Zf{k<%i3{0r6x>9D>fP{YmY0#b$gU`qao&X`%2RY(6=H^QYG%>mhVqKlx zj*3|{uu^fP3ACw9`478A{#BO$G02M*&ZmM1f9KbfD&H%N>BnMrRF*^`R#3(XlC%CC4TbhQ!rsXj4d$O zu~WK|q)7`E07#QagdHn5sE*n|9+FWMAn?^V1^LL4cOa>}yn;oA3}2u;uYK;758gk< z(HG|~T;k94Pp*>Ur5rZg*w{EZHQC+WU0q#+rR98!qaXatV}uG>0wqIi>XJ$jT!udm z#kuoY-nla;2{k5&Cxb)aIPY(CgmbhoIIS(Mbn3qTjc-0FD)qf_6 za^MHXnT_MwP@XfdP%68E3|pu{S`>;00qJ&OklIVRPB_R(8t6>6<(!>idvs$r@mT9|cXV`NU=Tksk(4idZwVAfC^ITYlRXRobWHgDLtp}4q+ zT{9zuw@{*{ZV&XdadMYO#>CzhpvFof+9~m&JrqspQP=S|Xhl?VdyhVm7_6mdDS@_g z%%OI4glYvpf@%XDLNRy&bNMO7Q8^;NEAZ2tP{&ew9^6gTZ2TWSA5~ z0S=Edf#sGni2x8RXrn7MxXhK1kHiqhhG0zHIeZnO1X1Ad$pe{I;97Zxx znvTwnXyH^2oaOru7Z}E%c%a~T(Y}jTSO+1gMvp2BD^NQ}R!9bZIE5MMSa1vqyUKzj zDPy;Z1P^)19m#MFcm+8!MxeBFX=RlaNP?ni($o!@z=jb-($CM+p(7tz#NM+A|telFN6|e&+aszf?23rzAbKW@?ldx`X3YHeI(>g=boK`_6$?cJ#lEDUM zNjdzflK3ksHS6dJb4P(B3U8dm@^cF3(h2!mB(E zJD8%XMJGX1$|GIhM`zBQ8Mwnf#ScF|{_%6qKDTGrUZS{m?%Xvrbf=?t|{_-zDfF@w%{42z204KVP+y|g zISCchNPn-epqM#+>sr(cN9mv`RxJT1x2R-xW||$^$}8AWu!Nn0>3tIR-rd=f;A9B^ z06+jqL_t)AQAT&v;jApS@nis&Kxx0HAPgcLxpu7?3&M~U@epiRh@zEOR#{b0Ms2X& z5A-8cPo)Y4v`%0cHIeC$Q4hu}>5q?&j`a`RE-Q6kct#W0QXeLVdN~=vL++IWFoe(l zhm6KSIQX+@D1d7z&=uBx16dL++;0su!bX14JP1(fM)w7?X>6I%1)LW#H_ak;z@#h^ zsCp&=O`R41Bdg`5MC~Sizphv`iB{Ms&w4=Jr@)M5l@$cHQp*GvmKT;Vb%N!J`lt$H zW-xm4OWf5lNX3f#gW@a{b>ys+`z$X~KT#^-i#{_7^PEO_PhZU%VyS@A#G8*Brt@5| z8Pn0juOa)Nuo1lvSLwRPC}o$Re^ONVZu1n~N_}^VFX%k)S%C zzi_r{*@~;zt}&VgQJ4iPIJJqFiJ;A8iS0bB2a-n;g(lbLj}y4zc*7{QXUpk z8qoqV403{Kg(R~=R0xZS1^R(><2lu_q@)ZR+tApEIVbRxj-uuurvBMai!I0a#28*_ zZpN?>M~m@ipph{tPD4+a181OB;C`uq{I`}7Dt_*4ZA~q^cI!vLP1>Fe2eyMtrbOsG z<}Yr5g9#y@u;7^MUgIYqB_V?*{;mYtiX~03K)CpT%UC%kXK~5lRT9|4Ydt;zTE7&N zyh&sQP}(f10!oMzM&1AtjEG;6Ixm(F{1kX3K#waI#EmDN)Ym=P|fr3_}K* z4YonFSi!QYx>`;FnVn%bTI(s;l0Vkv>*sjM0C+-W%|xfrD)dSUg_5+5pT=M%8RC5B zp)blWQiQ^W0RDg0?(DhG?7R~@HUixDec#Dm*lc!--I6TJQarXSJ2jI`eIiI~}%99`j6Udi_}DvK%f?F{IPrDJKJ+_-s* zL2lc=o$y3RT3sd=E20cWJ1pH4gf8&GG>EBy>3?d5l5& z#8yi>sVb3L{{Vld{+POR+iVO2VMuxO(CN9x8~gQ6l`5hV(b&eiQdePN)GM^5s3)_J zZrpkTYbfc&`t-Bkz5e(84K?^DLHu38zrz!!(8KyXV=ym-=5dP$|Pj242WuAffNi0P${E@Z3 zoaV&~mw+-!+yF;g@#>f=6Z5$zTZ7S0-pW9#*h_H9-#Hx^Z7z&OKBYxt%1ya+c0D?VPRvGIKsX3>9?tx+p?alYNV4qX_ahhxxLoZ zl*J~7){|3~6@(RwoE#J5QzJ%GJgdS|93mi`Qp;|G8~lY4L{mc^u`lLtMvJ?2nIt92 zWI~oW4$c)9fzaDEhwm&(b43EFAyY-9=ceSIC@RC*Rb25Q4k~6Ep0ulZ4%C;c@GC)X zv>qtJiVk$uq$2QZL-2TzkX>Q|BO!~liBq(BV*7Pb5{^jGn3l%R!X+)sD%TPyP$RVf z2u2GORN|BoF-97tn5j{f74VW*UU$Vv;{c0>t_^+m#TT15Zc-a59cHCbeT#d|h$4{8 z6Qx2PHe5jP6GIArT{!$^=U@;*q_;eg1g(fv2KlG8qeOZV#QbKqsvZVfr~;XO#N_f0 zZJo2lHWTK!)+(rK!#ICvYL41u;gRhR{TJ4-;`iVG-WOkfWn*Sx4O&=%bh_9d-@IX* zbh{(T)6MfhXAf@@u(ma3vlJ${HQq*Yu+aJ~<&80DGl(Q0Aok~cc;mI}*E&@;V@31x zH*elrvdr2hjcd_GziLQ{+|)KJ4ANcq^zoB>qyu{7SR9ZmgsQ^AkLW>(k`GxlHa>Rx z%t^b_3k{2hUXy;jpIl)~p&3P{QH`@@u>ly2!}DA|BZWD}NqIbX)r*KXS1F;GRcxfSG?_qbq?l_2W= z@+yVTFM>+?7ima}>tfw`+N?iE!JC!@sAt#;E^3`AbNj zdO)-jK2zGerimo&jP`n(W zISLY*qvJ`J=Sh`-)@0gPMV@rnQZ#myE2%+H1ZvU=>Os20iXDYf!mMn8AqwVb498?o zxhQm@BOS=1A3tSIUZv5+LR;`{`@Wq!cbz)*6%p>=zgKlQIyy#yY^n6X-eojJ#Kfxl z`kmQw<%;7PN{>{-VIV%Gi;y4yHn|U`C{EoJ1#lnIQ*5_wQ63*Y{N~+h?U1R>Teb*C zPM$n%vLu~#UNK-5)R{6lzh&zViAP7S330F!1Hu_WFcAP1QX-0WTYU1Xxf1o3Mnm>B z8M}Go_SB@gO?uqK9bw955`+}4Bc2cNYJiE_CuMS;x zQkZ_tsk?WCW%xj*kkNrOIC%N)wCQf22V>}-1 z$1cE=CNEmWokx#GMS(V{B903e_0^{<)LV?rF@J3AhQO}WJBFZ?CH8da=`m1v_-IIL1Y42#=*!k8F8u>#S6R~GXKGH9yK6_(^i8T5`g%8Q zTn`AWqQI^kC>Di=XcRKFweJE~6*8RaImChoxteSx4JH7-as4{b>{Wq3R4Aw6#-G$U zMR^vP(r;3Ma>@~Yxx__rqp>rTd#(H`H!pKV!2&PTolW9FA&jfaJAO$raN}E~ z&lDf4-Uz|}>f0-cX)WVDGU(LEh$399PN>tr1MpV{C|$9u!x;0j#ft z!qxZ=(;_lX*A0AFV*(9sC*`J6kwLc-(*d;MU%(OXP>_D`kT6Hb+L?%k2jwj@Jde%M4*8gA%xC&BCy;w+*;Aa z;OIPZlw0K`Nh_)&q{L%X0EOu!1{oKkFiDh8N|-n;g@v5X6)MmhF|>znGqenVgz?o$ zSU2!2^YA2^;;NEciUOBsgs$7tOvDm5$mtggosH0jb>qA?Jj2i1GDNr)=YvVGNSEjA z?7@St84BkOHf-8pRN=&l(^^WbD7jL(#t&1TEt|6h=iIrkbp|MmPQtp;1$sSkPH+Zf zi6EvUf2fNZDAg^O=lpo5)9yWR;ImIYVyRq~aLq=bwl9oYR+Y-tfHBa&c+cMbXV0Dn zC%o%0D)!WVHl76sY^^jJLWd3>T(kQ7cc!OoBnwCg4GZxx$dDB-C4F+F0aOV>NC%ql zS1xJs0M!8lMv~VRelq#MsS+B1$Q1YUBc)0cTQdNsZMMO7UVHYRJn_}N`_n2leBqd= zAOY!%(#MeCd`Q%(5f#VB?%28O;)SnMZdt1d?e@Z7 z+SA2X&R+U-)!`r6R)6W@wVKhh5J}_>Q?*mX4b|v8CYuoQ?77jl)aqtHL|JN6t1#IW z^)$GYj3Fkg1guYoYSCE?Ej9{hq)DU{euNVex}T2tOLVwP=gUa_q_EMSQZ6L9WX&Ong)E&I^(&zteO$ zHWA+&s0F)QACiFb;r$1?vb4rtd-JeJ;O6*ELQQ2`y?_ok6jE&4wv9b!e>ozEJp&LX zS=Psl{lb%`=bULc>JQ-w=`w1mXHY#fIias8`fkcRjXitzJXUBPkdG1-XFD&GCswXl zNgR+Y|N4zhhI){dhM!mz6k>Q~tJ#AzMid#%>98eveeYn#xd&kbsq^dBuYcps*DqbX zT#Y|_f9KS~Ac~C|J{g*h>YuxC4r~n2s07LQtk$HK3R+~OA=-GB;Ur&h>C(>A9ts6I z@IdP8vrf>HD_5>yjv!UWgkC8NqvF44+~D99{PRllNNz!rNkwU`N>s{FOwJMCmTVmjYvHlkCyO;R@R^6s4}@PcTdcC;~QU`qH| zY%I6up{JAzg4iHx((kxy=N?VE4?g_O?Wy}_#V{V{6Nnlki3lJeN2;3V8CF(`9dhV? zfu47M_Pg&KZAIH5VrDw^58KOb63bV_3S$`PtFJO}Fk@YuX-&2eMmcG*@XTuDW5id)JcvfT7j{{G{-ha80Cuhzy0L50fAC@2aLgGfwJLzBvg zfkw6pp%FfUQkRB!<_-tpVsWssf@d}>W=f11a7cWdv_a36#DNk=gmq93!ilZA&Cdx1 z&0dltDKm)>g>8H`N?74e%mnf%hf;9i0$5O(Efl!X88!H`3B847E~S~kW~f^SP=^nfhOiMsXc1nxe`*l9zArtET5$We{#g-xP@KZ zL#?Mtos{NWR7(~jk;)i-XnlDlKRdNGdD;mmQH)}C@NEkW zi{Xr}d)`r}w{K3&+JPl_N*R87$tv|G4rE?Rknj&maEZd@n}NURHd*#B{^DQn z*}L!fd+-0d|Mh>la_Q1rZy#l+uu(873@Mn2VN-Ykmdf$)YlnLKdvD#EP(K2pvH-{D zovEoUTecWaG<1fXwBGb1cFjMYyuJ4ifA-`5>3<&k_y6w1)xn7!&$sN^^Z3C7fjF#E zr#4TbM}%FeF(a^QF7SR<%;*v~)SBUnP^q-kgUtg)T{A$$)n>2V;-NGcOHGmLOghJU z;!2b{b`Um6^{pPPJZ=~0*@>$=%b+8i2sPpW*v?6YNtUmu-zBH`g`o26$-~xX5|JcW zRLHz73Ts^_dBj>Mzr+!m3uF$Wn?LG3f8o5Xjc?qzp2jiCKoO#Tj6m>6=f+8K0ZTz1egsbt_=-p z@v^SQ3@WANAz#PLNI2@LE33z%LY<^8&tQ(C%rn-<$5Dg?LM7^O-0yJKs~BCVWvK$h z)2C0+cm-1t!pZ?vFXjg?56Z5X!r7AY#b%#OP2Q$|istGe<`TKbNXqquq7)Mw%d?cg z*h6c?UJh<d&yz-LsHYhf5{o2*5gO@YJgBv+kqK~OdZX=0!#PB>Bq&$1(q$NDmID~~! z#8Y)s#6)2NIc3v|;$$(&)ArDe#(|sTq1(y8&trert@8bcwcaE;E{>;CWomZT1?hR1|&DoDxee$yLiCCkr8M72FaJu zx{Pty*e~g6uc4KlJ9d~;k`Oa}0dPy*oN{ z)vAO14F%vaq!nHOc*yG zY>^ly$F+tb#qFq~$}46mc4PuUyfWhl@*LR9b z5y6N3L*wI5KC%+iQU-b!$=Bv;Cij>c2t*YV%9EuaswVT)lu9gx0*MZJeX#*Vt!s%%fW;DFI^mrt(E_gi#gxCIW}?YhV9&nbzyUqJ;7Eu z+pYuxE?o|dc;E_gzue)c-YLbosWruE#Hm$nkwx)TA}y9cmKz_2IBF`#0HrOyASS+S zp)_|XM|rZ)R+jj?Pd?tbVVy29v9)AaV&T80n!uOHI?5fK8e-v`U*HnDtveysM2aNO zJ>0r=n;2By6x#IvW#+ITfw#%Zw#esB9y+WRGkM>Fr2qQg z{x^2phOJq-@X#b4G)hya#Xta?ObfPV2xRj>a`aoxp2HN#oe3_CAri>Je^E#ax`KlT$eFmaz5Mf)o3<*?Fa_XJlj)4#3N=c1&a+4Gyy~+kJq9Y2`+}0j#~H7!r2jQ1(-cZE?cf*meAFM-ndRV!g6^jYKJvIFS%8+r^A3@HHuK<#k>O=a z;hf{itEiA#Ruzk@`#=xJ=I16SC;0|CEH{t@2y@L0%>_q}Q1n)g?VFRKz$vM;}-TS`kjzHT|yt!OVk59F&w@=tpS5BmZ;LdFhl}+^6~$ewr7^ zh{SyL8vZ?)?|Ss$5krl9I{@pev`)&FIFAZBKNW4M&E%vbDV3C`?V(RC8I+2iW0EaB zw8E-NS^=4j!mZ&@DS99CY@#V3I6yk+iK_|F=%*#gfAohx)qwfM-~UG^(DA$wz*zg; zsESNOE+uaaEf$280V*$ZGZT>@ipiD6tL|>=ol|b3F>U7XEyP3w(Dft!rk?weYRQ@i zKps4BTxJ<&4%e!3<~cVTN%YTU5qjKUAOu7N<9Ar(cR|fPvSCoq1@zP!o(t$Di%lcU zM$kW(B)QUt4>(uSB{OA17en+cfnA!>T0z*BA!g1KCV3u;%&%)bM5(f>%6G2RMq90! zwh^5K%K`F^yU|r#5x*QMFA_}mVH|T~M6qrGpGI5J;W;suCCR9-H^m0YI8th6mCyE_ z+cQv@Q*_H8<|G+P4Y5X9JY-7j6kH{dYbqF!ivPWfYzp-UKm3DDo45S>H~(ZvF2sf< zHb*q2+hAx;ovJI`E?8cN7$EdNYCL(`l7ua#U%J`Cf5_)arB3n^lb_4z?`1ln{^Vc%Z*x6MyH~G#Dz+xen%Nn0$fqyLnaITC zVhg7-f}dsDdTGVftpXBuM;_w5#WiEE!--jQOD42v{_2)~~ z=2z0b@{wP~LQt@X6ns8HT;oeE*6$^tg_joZb=UoDvF?Mi!P#9E=jnf zC>dB0;#SX_LXliBbSLzkKYLcB%q^my9+*-vG<21Rf^p~}BA;a(7=p{Koq=G9$UZ?4=i5IDVs}{4A8Y2KEsb*dWI|-L)hg+ z%tY3~uy%O(I*gryU~h1p;Z|pe{CE^6UGepE&pBt4%KOztC`utE(_##!hN~shs6}wd z7)P2ihQri|x}L+Uxw#FS%p#f}ymDFRnX~qATV3UB4899zunIHV72XAKo_K`=?TI)) zwE9WRh>V&gse^iOe#NroPZ5z(1H(HSAzk+~t)YMI`ZbRpKOo<=Yu14}Id#inQdZ3< z4PiWr>b6RYRC5!j(&PrZU#S(~VmR#K)7k8X!buv<4q`SI?4hFbz0a2{TV}gty>}Xg zxatPdOBW0A#jmq75AH89?%1z=1K)TKL1<%Iho9kTct@tn%8@20`}w{GKlexV?Rk~k1c9cK0W)>6JTz72rdQ`7wPNa-jiX<@&>@BFOR6!mzgKlvks%CqFo8#yW9^QJQ zN;r4!oO+9l{KO%G%jG_;f&i8#IZUm!+(LX`J!&2r$WS-rZCwB|*r97zE#?3#z9|jp zfKp=km4TQxVLWFcqsNpO)aArCUWL;_TE z>A@ktqF*fS`t>2<4!A{<=b<_+9J~)`X#2*9=t>?1v!os1{+28?v8hl^eWoU!p1ybe z`f&19dlsxDO`gm|R4CzMyPiVmK|w-AM*)ilcop#24~BY(OKKmeHep%cE#bNi>v+zK z7tW)|e+Y>`sH+H`n8KqdX})8@`IWCmwVxppK~d#c6s$FA03uV zK{RaJyog6jAVz0iQ7)q0u8@{JNDEs+S2a>8q z0<(-XZ;KVUQ>M2_-@j2pU8=VyCVus+UmiZN-w7@^CT_&2vP8*_4c3KN!IRyCP1pjP zM3As;+t#tMF@vNwbf8FDK?q#FbXg89jWzJbSo)%G#q#r$caJ)Tc*(%v^xgaR%HFki zczi5@Tf)@L@`t;=DjHTqM~9);NJ4uUQMEOEJU4gPgC@sHGfyjcP6QPL)6bXMD0|*M ze{6G#VusJU36rw{RbBbfxr+PpAdK$JCOqRia!KIRP;U9kWK}jpi zvA5sRKIVFK-m)Uotu9@>`0l&T#W`-xoq-`^PX?jJ#zyz-*{d!W##vjZ%Vfj4jTbLn zVC^O-gzk`+UY7iyU)aov3-BNgsQb{t*BIpG%NKX=-eVgc`x zjh#Dx&R8T-n~rX|_?^2`-+T8xgC<5Xe)2~@{n?-V+2>z;j#vjH z!(yxuKucneQSVq%nCB~{G_KMd_ugtSfUjrww;z4jS9AAc_9@WT+o^5HyPQ(8rYikP0tali)I4lDz-AV*x=OERIxZ}6T34D zaLJNQD_024;!x6{i<%XbL@;2r^bvgpTS361OlRpfMURdt#qTgQf}3AvX=iVL-;x1- z+pu_~7{@`0eBfJp64XT8lE$oONl-On*>9LFfoEmv4&GkwBpIoAEzgmVFaY1m4K(Un zdfo$uOeWYtudB-n_n_Uma>r(IEJY?j0Ak^#@2Ezn^$1~@hjMA^YTrKn4}XfoaO7f% z^6PV7>z=feNb}FZFEwi)b?xX{8^H)&ifl0R=@*NZ3%x1FZ$;YenFn6E5|Bb65mYHH z%Ck?Qn(8Ir04#hGUI3)H3{HW)K$=^1{QC9TKSI$(k#~5`dng-=h^7#$!U2qimtR)} zIe@Rd4|e$vlNT$OFI8nq=_)ydBT83aDMVjYtj4Q6B9xK>ZREl9Va%t_3IetHlQFor zj=ViQJZ$3_cwW4C{`J@1;P_6SJYfsvfAzuyxyGUk&w!yZ zK7sTH?*_gYR;`Q}L&EWFBx!x$zWw{TN826SH0Sc*mG_Pxm*I=AzBqpT{XhMapPf8) zlFJgtagt`xo;dNPR-$M__M$(ZF`jS;<6TN+JduR3=70Y`{tqGp#w&>8NoeWTZQCmeEat8Kl{_4tJ#Ra9zfJPhlYsHotC6Eu(hK{VTWI=I!B180;Y0bjc#?TD&MiwU zz6MyB5*;wtuTxyU7kJ7&#n<>#Z9LHqs30z4iC2wo%NgBG8Sc?%7>iTH#881R&&f2n z*n|clS>rI3-giCB)fL zuz~8`Z|qqyr~`c$gCNYn%+E)&#E58+U1m)VYd42rIRqndziJ z$`i!})n9-FWDK~hI4PPeWr4q`h1>V+b2iJv2M_dcY~8xe(Li*a**g(AGtVA9TCr?} zwf$xcST}cI-vL3o;J`MO;$=M{wuyi9%{R1WV8U|t?>k@zQH`0;KK~>}qUE83y0xUL zpCc$mRAGrMTeoc8v112@8+X{Yb*u6CYuB!B-n7x!_L9ZV_V3>(w!^2rd-jUG)b0c! z-e*9JN-m&sQvoZ$i7l!s3U#FsYQ)rgDKiJT@7%Fd=z}ObLv7l)nLjcM#K6|%)a|2h z9km}R-?3}guC&RzyVSK7Hl98EHD+rgic184r%#{K`&GBzjx08gs|nX#&6ts>*(7;sHpUOmyP&OopNe* zCU6{i>!?6S*vG9%%=`ERu)ub-Bb@c$M{Gfv3MywD(M5K0^;dUP-emr}WVP-Fis`X; z1=Zv=*;= zd;j?$iEyXDHqWE*ayRh}98_6cHJGL2KP*|Aef+HV`9KY|6l*J`OzEu*dbpuPK#~9A znd+z9ZA>`tVOL-IZVF&WUFTMNLkhBCQj$bxlCY{Du>zl!b7|^iktAY2h^;gQute-Z z^|G|=aV#K24LV_Hv=C>cZD>RvllJA6#F*t#je3M}#6ck~!thxEdLq?AGOUvj4g$U~ z3Yck-uu?soILqO zbq7TCD1!MHXw3ft-cYT=qn*XuhCM6{iQISCtIfxG$YaOe5kr`Hym#+D=uO<7*tTt( zO`&MA_tt(u)hjboST;>j}Pb-me~x)>~n9uDpvT6 zQl-gV06Z*-1XkgPs}WWwa_9&J|HD;&qoW)tE6hy&TNl?upLK^5mrO|=AoVU4bS{a_ zH(0ZBmEq%~$KLrj|8}o1!Fi792Q)%ItBzRn+Tqv6MwioGgGf#wfQ&isHom9n>-VkW z@NLR3@y^}5t`1&-l-@0~zd^Q5f`#07A3xdRSi>#bdLBR7w{<($Xe&X6o%UAHEs#mdFe_*qar7CVr!I+=Y7$mmq@23s=WxC>P8)r z+wWXZR|Gwyd0CWH7cEz>D$)WyxR9~ORaS01$8;PCWQD|xrHYb^MSPUo3oU5Ca{qm2 z*t)A1-%T9Tm*e0zgFD=`h)lXzceAkMiccx&$1nhoM)*y28h+n=LW3_9M(I9_HEADL zr&}fhJ3I`d)S%-U8y&zC5y;cHt9Xy*hZ$5wFg1H3-C2bjS`s-67 zfH&WK^B?~4A9Y_VcgyB23?B7nRDAsL#}q5uazG+7Z~fI3^QE^sTNIWUUd$%hfWA$E zi`D~eOpI$Hx|o+DKeJ>0D3h%{qn|nLXKHHFm={_k#Idqw1t0=><-`2YJPhkXT_}!M z4B<)9OTG02K04!6h*Mwf+_Bv}08KH0mzYVI0ym?&#!YlG=@Rej%~{THkU+P&u3!Y| z&u#h}!_QTPDK84s9A%ow))(q4`uKMr{p?SFzGn3{LBr6{5adl&hM4gY!-BVOXAYJY z82Q@Ga(HBzXFPT4gw6ckc-Eg=;{DH%y7b`)Yd;l#*i>1QZ-^`f}C z6%l2jUk0SJ(=5}-8rT$$wJ3*56eK#+^2UoTVE-*>*+VFjQgn13#>V3^11vC_7f@7s zapp}n;>x0v_^sc_+m<0JN_*meqt$+7fT2P?!6ljC9-0fYI7GzCC_9u`AXKBGg?ao9 zXh2Y13Hofbn-S+=6V0g=P}MeS##>wjIJ$*|s7ys&Y9~JzNCq-Uu~S&Ni*ket9IHF; zqnnu>?=#5xLsFFpe_eWhzIw^dFPa?^#L9yOV#=Wo<=R6-Tt~SVi07vygr2LcJxr?@ zE8Z+&T#pfowX4@2|K9O``tSpm%LgDcBta-{FfIv@cHEmBjTCo zoybIjI9twGxbGua`H|=PkHdw{HFCUw&=?fd#huP604)sQS!p;S7~l8$`Z^ z6Xv)I!^lf#Gg|lT*$aN{vAI-np4czOFojVIn?l>t&;BgG{N>;O=nsBW#bq{*@uN_V zU4h2;mkO1tcp3w!6@S*QzW@FYKKb3J+(jfCGsMcc{;-A9jsd|nXti9yuN0E6%cF*P z7?z8kU{nzpVJ2(Aaql4(iWB1HI#Xq3_r9?`>54h2P^PBu!bdDJc4JhX7>+<9c+A@z zSTg&sUK_qf^Kaa|3CBzt)2Mx7OHqq0=5#1*7dg$%Sx|rL#w|N4$1@PAL7U$Br}J|o zQ+N9B&A@5q(E}@WyB006X_zD-JBZ{f)t`UVVBTI_S>YQEFHDLjp`M8G0UBt>=deKE zjaeKtR1bSSFL1F_8lUZ0(yLuik~$1Pf2&36!7CqPH}Rkz*0}lNW@%EmssK|94KJk0 z-mZ!{K&Or|5U!zoP6;EG1(zBmtAE8E-DrvkTwdtlc{+Ratz)})@3GhD(fmI zGo{8br|;gYH+iSLXi86)8dO~(P4cGL(BUm#SIX)~@8elBTa2Rzu!qt97Ut&|xW&Vx z!!{i_^5&b<_wG79L(vjaI0__+G6Tt_2m@WCW22WYUoxI(edN^Sgq~sHmn~RrCT`ik zt?;M~GL5JTW7IB6iCPp_!|BAqsmTget-FkDiU!n|`f{8zO-5pa$lnz=D}``s=S-FQz%diHKLk>rRLh z9tf}S#;0|omNh8Ux>4nd{}dH*hj%{8uXh+8e~_Rqo+WCAE*len@x|xIkH2^1&7ZDk zst^__DO4%DCbtSE^t!PQZxoa_RzFNnYjIBWPUsiZjLEL?D^|?hp8;myzJ10Mxrb#- z*J&|?(-C_RHf6k$=soqIhLvZZdZYhITg%1(nQONGDB%iEqF{i4Y$>y8H7#{qz0%51c-G)-pF( zC|4sue5|;Vt|Bs}Hhx-617IMbg;$(oP1L=~Ed(oaes1Ne6}C8sTZbfQrfv?2$sVj5 zt-f+qzP#1G3I|%pNmv!ph#pX#A}evg6iZUBq*YJnO$uvS;-~3=rq}dT2B}NHAq516 z2)C#~L$_rskBzQqTUMz)&pHi_?SRdPz&M!e25v)ysT55BNNuXIq$8Y#+rSdcYR+Ix z`bkqYq~cp-UKHR!HTPJNp@LwPeyn=qw%#fZm+~q!r5~%^PE&!-#K>7=5rtY_9MOfp)1x3=aPbSVI}6FiGywhbkE#>@Z%r<^zgygE?&6s z;NiVR^O++b{QDP~Xj|NBj11c&ri2%zq9s%QDl#av1Kl6lEw?MRrh@WHw;NG1m)*r1 zroGZFSkwnoaz#jx^NEQ`mful13pN{(SRJ~(3g!rq{o(30t9nM#vzUT>P=NFo^nU!? zkAC)Ne{Pz`$o1>ydvde2PiUI`ni9=#nd+-%7P5-=OPBHZEX<$j?3egMKw|cN% zzEd`C_B_u#aj1nc_32BEm0cS%7~FB8FobxfFeg$DbX609W0_yDl*MzdKU7A&T~Ua3 zL;H;%>E!tM~PSOVBC_>%j+YM*}7d>jKoL02Q6nEq1 z&0v~t0!au3jb!uDs88nHMLm4wrCa`7KwuP4&=Mhdl0LKS_qc7-cAb+FdRVGg8WF9i ztl@Rf-RpOrz0egm5E-gpni5GM?0EkC(1F924p||;ZQBl=MwUz}K@3Bs&YnFJuHug= zM9-feIBM4`nwWdfmDl{POwdpa0$8 z{oP;wePk^#E$`E{ zX4P6YXSS`$C^8!i{rg}3?cO~H1Yx%)C)ciBqs#D5{^VyyMI8>UXQ+%btZ3d1g(0%( zOiF}BoCJ>>+N;jM2hCYNg|XQd!E@3R;91EvK0fA*qu#z5r?m3gIxG{w$xv`m0h)po z0hElnNpX1{8htBzzFYMC{h$2t(6#F#!XrnHniOK=9Q`QU_G}a6#hsNl%AZ`7)!Ph7 znq9Rtr$GnrZ(ov%!&J9Y>2D_#Y0Y8B?SccBrs0Ky7-iKU0J_BVjC~tYeo0LHChD5B zZYT&hq9^`b$XhVtuCBqWmo+eW)+Sh6wr)2#&OtY1(lJxK36Q!1 zERZ_?l#Y-3(2TI_`owBnjpow&xw$p#)^6Xn)5PIuZ@Az1>Ry64P>?`~E_K{kP?i1u zgPFQY116&CHQ+bbajyGFyJ5_mzAO1 zZQFOIMxWJ;O~WOxf@ecTAsGUf)CE9Nu)m?iQu$+bwe@>52)3f0%$UNJQojc64v5^}r>LEZ)6u`C;QhK{D zoA5A0x7UY=;EylIPG_{!391&%-bLMb&AU=3cq!bfo`Qn(4&$h~GJ zsb(_jm9;Wd*L(?SwD;0Z?wZGWS=AW3n`{75wdFP#XomZwdWfmH`8#ioSyKa5fU55YR6J#r2 zfDk>1YMHB7uLM*_BKa0-Z`-!rv>xyE$d%lN;Z57atNnv zF&SlIO4)m&InVRg;@9|Iodue^ZcP7$8)cS2=K`x#bG~uph}yxTC9_;?N@v>Z%4|{} zL6AaRXdxU!!e+a@=}c+J38IL~eSS(7_dEA2JUu-JCG1QE)IhJ(-=#SFOT2u8^m&qZdXw{b=o)!}^@YMn|B>&oVh(C+r^% zdv@=>cmLkhojaygnTk%H%EuY*H{C1Q8z}_GkEWp$MbLldkU{AcG(0>kw%fXO%lP;f zN5E>V%szc$6ho)<$mock7~MSC?dkgU0WmA4l9xhOTbgz5^ShgeukNPe1(d!*!V<-ZMElsnKi;V+8KqyXX7ge_wAFaLyy8 z#TbII%2zJ>s;tPoX*RZaf+2eJY=3h4^i?4PP5^66P=){b7oVlo-qm$?db-cD#YIM4 z)#gvlSLGM55vFQTvFukTz6y1*D7ISv=rrl#BOz8p9}-AV!x4}|L9_ z@CbaaIvxU?Ak2?{@4YWie5p>77qjeQWhs_>(4YsUFv(RUlopo?#k)n6&1JZi!l1I_ z=-k*BEj9>3U*FZC!8>>Fneq@IOrwSj0ApDS6{p4=Xv2bkS7KdZ9(qs;Pu0f#dovR@ zc~5U*d0AM5To{DORa*DR@bJ1ds~J0cT!8Zmv=~U{<4;W7bST@@l+)Z|Zm6a#DNIYN zY?gz@KS1HnCXDHs5K6-vEWDL*eC)bf*_af1gG8hqgkYUsy;NP9D7Uy|Y%7Qc0z52G4T|XzmwZ zW|lD~7fSGJx)^4Wf?o^*Uf#x)N0L6$gQDivSR$GZvC{YUSw%?yu`Vd*>S#r%Evh|2 zSB@f8j)yU~;;ItzBoR(9@+*tDwi%u76r=9gQ}w2EMx zHRgR+u3Rzxg)VTITe(L9X|RJ#qoqER$2rSu16eYHv?gW)Byj!K4S2x+AzZN=8J<0eClj)JU@)1sgK`}a#Xc;(7Q z0}UHC5ScBCHf`9%YnjlwZ{Pl*p+T!D(N?Y_6@ODsN~;7q*hs^9o^JB??d>~uD#4oK zHS5;kHjjGi_O0u~IVV0QG3joJ002M$NklC>xP*9$Y-?WMOs=_313IKdSgTLK;$zB94ocI)aI>g9px+%n){uBccmTm@> zEkBt^Ir%F?eS9mx3Onk82e~RLAQ(eJh2mqGy18mkpFY9UmR2VBCH3$eDWNPG(U+X% zS_ug;UApE-Lh50)AzzEwz} z`T-X5DKH#0^Z%(OKbae7-iJWJ99Ai^wUR{XR1Y#}$pyn;*_cM|<^yqy{6nFKoSqlY zUg#EBwRWY7zSxh0F;Oy1&o6{BkWo7HJo7jN%t-*#y!4;;blw7#5%pKD?uvfaz0Ch9*>R=tC>Fg>{D?i zRng@RTQMbW#Mo2$V4d<~As&VA89sRQ!ABn`nr?me*=GWAximLI{)-oi(>h0wBSNjo zKmvSV8m+{6uC`T50UoxYfpj9_avW|Xjv7#O6S2FyCns;~nnHrM;IwvOxpf7NjEwRo z#0pXS^HO`KeKFmkp{uH1oUet4XMwlO3b+ysKAIyH9gCzO6c39f88|ROLHDd$*=80b zi_#)DH6nhf4i$vSDmkYs{@pMBP6B$M|L>nZ_%&;xa(;$ODGOmC32m*Y1ec{NswQOv zcktz9h-_DZYe2o~|D#b&p? zMZJtuiRcMA-@ed@Er!4zJ$Y*M$8Pv~-2x_TeFdekBg8LVx_Hl?9kN7=zjK9SdM2Jd zy*G0ohgYp!F4#pfMuoss=Z&Q?ljn2UySxr`N|M{RCo43ROsZ%Om$G`*^435kT`$6Y z@&G4V2SYQfU`70~J?{uAyq&k<_#-?oZs$Fr;lFVN6yizmyp0=F049G28X>KKmd8-v5;XmZ%g>uR+q+9b$7PJX*h6b;; z=Xo7YhP@W6@D#yFrw_T7!G5{WGzUo^ItFk^rM_!RxqVA;ZpcK7I9(FH24wHKxTPRw= zZcqvCF-bC7)}L`J$$^mo!zA`r8I`6Krg}q@sWQqQWINCzM`;CB9HaIo5_DY{Ir-JE ze&snuG9C8P#t9Hs96QEj%EaKU2VW8^>1qYumf&Ewu;0nVw;~|GO$=( zFg|uemos-E61644-~PwHl}cs($tS<-{`3=TbgUL0zd8QbfBjdcED^3~FI2`_LtTVc zPAQH>;gMB%MjnR0e*5unZOyL?voBI)URjZvQ9{ZPHb?Bq*D%YERH>YQI5vG}8v1VX z7cZQ^E3*kZ6y;&d;zr5XZc6BYZMJIgrwA4{L_g_~pY3Ww30MdOR#1=xHY&1-n>WYD zM%CS#sp^9N^pC%i(ra^h1u$n6<{qq%y(LQb~SY5Newh9CPpRtHdwW2ncBUXpER%!k>(OcCgEf_T7MK2 zd5VOfN2PO_WK$9kB>*ahlG#M@;q2_jO`9ZxbD$tI=UQYC0r&z;|L`qSg0Mwv*%yMS zM44lf1Y|@W!&OyG$m0WvD}FkOlyD)mSx5Z0-@wD5>JP1;JAwW4IJN*I3pYe zAr)>-DF-YgwRqe%%#Z_Hn$Ik>LU;W)F18d#am4D0>J+p;HjMEC`7q4ZRPn5tWFrAO z8H3{R(1@&3?e?WQ1a@&33rU-vl7XfKiVJeotEg-EgHSXnGJ9)<{!GNnhSyYuCBR&f z{`AV|gEeuDa_eQHK#n{}@>~flGFajlxam!Fn;Q?@BC2>I-U_8bp<9oxVFeLeR))eN ztOh5Rm>nR2aTPvs;=oZ*g1ZTWERTcDH#Z?hCA(fw@n^jVMR4xf$==E7%gLEc! z0`C!{K#9tm&D*9y={jk$j6h#G^-dKY?kFzNZVk5u&q7u7pSQ#SQ)uyn6ewDej@#2G zzv@}kr%{_UIG{4XI?1Ai>4Od7Tv>%{Br!8{-_%iD4fKER!XWD$XbWUqCaBL=XPA-M zvaU!y@WZ=qi<|D=osQG|7oedkHWA+-1q9>K1XHQbu`r~OB@g8-OVhyP$AFJdmi`q4 zF@|nuJx+?2Iq7Jv#21K(Y&>!0d1R8uaW~LO>{Kl9 zLo)ghlMmM-N)$%fJ3xHLCjg;U$P6k!OjBJ2xTL`um<|WI_g1!z21x($#4T-owv0J6 z+GlewjajgbnlpytW5d!tZK8BRuU(-oB}J*@9-DxrLuZJSD%^JCcfEvBiK3Lb0)=Ac z0>JtX;=#M9&g3edvtFbcE2#0z5KPvB-_Ud)NqbX|lLZ{-Q!|HI7BIIrKzTSj=F|EpiCB+r_@;bPsAQ$@6R}y`Kp`TA``4LRw21?w4Py$G` z7R(0OvytEYoF)EdRqdBA{T*5#JxhjyA|Al zC51Y=J{LX;Q2Rkk;yHtUoD>AYyqXWVY}t|(tJb2wQbtx0e+o-rl<(kEIH#TMwt}EE za1t(%3k{FMz{r!oDX!g-iV|I z$+LnSB-G*t_8lPTzPOhoEK+g_Uhz_5Y@BSoTrT-tNi(0b_CTYHke3#tVm5GA8JOKg z-euKQVyYqhlf9s1exE!LP9eBat-oNH@Hf?6;M8ST_aZK`F*RG8Kyf}zpDC42b_^@B z6IiNALU0^YxDqc>ChD1_d6w!DQcVzXI69Ujw$?7FM(S~DDLi=!z2aFp;%f#Z}OA)|B2&E<-#7QYi!W z#umVpC1jLhMf{Dqg_7D;84y8+#D+Nn3y;z#AIMq_$)77<5|*$d!}!!PzPtpH_R{KNub)zTO`iiim{XaW>pEcXN0M&>05d5{_ZA?f(1r~`g4@A8 z-+GUt1teUW6P}1~kf?HRD9NypUh)!5OX_JVk&XgHri#gfd;mwXNeEXw2?pVh-{DUs zqdwHTdN8we_wA!ctwc5rVCdSQu8M$BbY)2nrc6j>vS2n8BmoM+l7;!L_(B1uUgcjs z&kYPX&SVX9gHiOv4Prq6DiA7V@^-E~FI)l#2!DQu3Ld4>6hIGx2RNaWakM-rDOV=s#11VR4AUAD*0-4mF z29ly0tG)8D)qPcnk5bJ52>_Ze4}H^CRH&FEWMzcp$s4b~zGdsyk>TrCuU^dad;CPgDp}lNLu3*`&VlGU)@%lwhm~bu_E&+u(Fy$+QUX=!< zk_^b09L5hf!FYw#=ClC~4gf|x(MVXF3(q9F6lKY|^?y0cT55j)4jIjXa!;=-OGQ#G6eg};(D9TwH z0!XUM;#LyyKOI(g_}v~xu&~EBZTvteiFy};q86?UJSH_*iX)eBC&m|Fkc1tD^D88Y zMj@W0rI(VY?-!8)NI&);JPhemr%qcdYff8%QyFBU2|VNlBNaS`#`8o4!9-8C3IsN& zKr@KDNqHrSl#m@M)u|yTEDWFX0|aOnl|e*4P-;zYw9SJC)8lkcCGN>MU3HSko7gbU zG0YCHWYE1NPux@x^3Cs~H;7d%6?zL(r%u+6-NmZ3%em%`O+{Cm^r}J$T(5%}$VD8e z&@e3$Od&#}BFM%waSbSkQV?J@$wb_N~FnL`i$_31AdJ3qO?>I${{KSn*L> zW>z%eRkRKp4Jn21&=h|4EbbA1rX8u|pvQPZUGUZhg@29+ECggyLeRv^;M-@5b}CtM zBs5cE=tZo?NiYwg$rFDRUg`QadE(7Cj~+dC%y6*Ta)vdLtLiWomh_Ma^TEe~M38`w$x?o}e>+^~^SE}h`fmlXBQMW<7AHbY6Ta-$A`5p56yDaFX(K`bn} zLkAgnTq@RqNGl+0ngu!1@xJaX6A??4`=jTzWw$wjnyx|{4!nSeIACRx}~`buoM+U zvOFR%yz;sXBHadymsn%JAg97q;zv&&rOU2THj$;sX3Y%;C;}&Gb1T6sg<2R?NCr^Q z&ifZScg6QbR!GQGz+%P7EpW1W03g%@$KwEJSpno#z^8cMbGk7eKkDhT12ij;EFbyK zoVl1el1T?_g&H$`^YCk%Hg3#FdV=hL=v;wXc_WHeMm=c7xcw$UrAX%BU5SEEZoJ9x zUFJWH-Wb1h`AVA$v2X9bUE8;r@IYZCjNTPp+kK!lrqNj5Yk|U$ zrc7ECd{hdl4pKp2V{oa4lPSO|?nR54kDV$5I!pAp}A{iK8=i) zs&Fbcri_<2RWvXqh`jM2u!B#dzsktZvV_*uvA4+V*v(rOgoT_`2J!(B-nYAmqBYD) z9;tpg{|GQp>$4*o#dYEwI16N-J=0~7mf|ZYB|4Fgm4qF^F&_(8bv))ra-^KPWGFEt z3*JQG%dnEBrc%|f4xq}Fd(8(r-9WXXf=SI92?k*dF9Ds3%yJ(K@~S$wXKVy@?CrO| z`0|S+o_}GXlWjIp6{_Pz%G1VCMW_mu3j554o49pbZ(*a3>~+alMqx)2x*mq5($YAU zgR?GS1XL8r1yWrYt|ne&I@guUmsxDXpD$3Ktz5e5^~0};D2WCC{aZi=(ZMz7^aK+P zWf~T@Sh1Y%gb)(c2LwD}`icJRB#d2UV;Sp7<&t_GbNH6DT7p=r=NLtu5|~g-2#^-D z-j-DDDK)OCT#`b(OE+QO6#k`1VhdeaNlG@&!+*qFyjZXX`xue5;e%Ln&MVW3}|@}5lPfm0-AItFrIZXQugA{ zQ&p5#SY5z%s7Rdt-araIDYfZO*``7|7xscczIh8rXk%X#XwG7#ojZ4~TC?WN*QeB= z9^rCQB~bPwT$PgCz4Ex43@JsLJj%a(c;%`@RaRCr1gpp<&dI9d*~5ol_vrljvj&0z z^(GD0vdM771uAAWau6Ak{Hy=b!%9ONu|IAg1$bm?m1AyA(cO|AXeZLgHaCCp;31}g z7JP?BKn8u)Gnvkg4Uar_XxG94y#@E155SSBN6PlLmKqpAuvAJvSs)RBgdJdhL}Lu1 zUK_e*Wg>|pv%$b6@Zmo?R z)5-!yjtI(>9j7P>sZ-e~rTDM%tv1A5PL=6jyyV{fhZiqiDD0D}Lhy9VzjN%JvdUJw z1hat2&TcoD~2h8 zM^PQJUC|R@76@r?9Yecp#nj}Kc+vhNzy7EH2S3=H$vu1b?&A#koWh~xT51+_m0SFm z?9AEIohLyu%w!xteE)~m_CmN4MvSheu71ELuC&lEZUp&|GT(LX{MkE`Q{uK5HGsW2 zY08>7JNZ>(wF9bf56tAM?@$^E7hI5Xj8fmV>(?(`Jl~vN5@#&l3V@Y|55I;Zcu`ra z(<$z?!<}H<~Ege7blYqxFGz`-_+y(%*O&LVMDiVoL24G zyNhLwk7ogsi5H)L{^hNS+YxFt4}QO(2K^Ou{xijS{Sx6yN?2I5fBzo#tRG8b&w4|< zH#^Ak-DB^5@Zkq+ijAADux;Bm9h|Kt(SmfYsw4|wQk!Eev^=m`X*vO!nZTU~F+@4HdyKw&E#Y{)@#@%LXuKTw?WOP;E4Vd z5q?q_?Nn5$)+k#vJBpMmg;bF+&&2#>@;llls!(V5+eSYwMfv4e3Z^vQ9wH1BR|A(& zzs3bRlYgM?;CnltUr z#&qPTrnUTM?fP|eLbsE(Y?jOMp}-b0sntP?-wZUfo#r1t%B~8*tHj`~@Z;gbN2kx6 zu}fR=H-+bF5)YE$T6CcEBB&LDJ+{@?`bUw@GgF4brR0mN}KPa2ud&Dz@*AALkxH1e}!K~c$C45h(h1Q z<7bcU>;xx;77i~qY}~YM>vm_+(@(R@Bm>gCd!VSC&tROO9Cb))Kov)9rD##q z%(M&e=d(JBY$FvmRR^kG-d(%*$Y#+ivwZ3-RnEy=@(0cY)U7!nx&)V&b_xnL3#`LG zKN=Ca*QsVjY7oGAGuZ4KvSIV42M-_GJlfnE?F^07`}eOlgrz4ViE&0|$|I#Yk_h@X zud0mtU}9!g-Mn$rfreB%do;v+LadMIbGxD;J@%=0o+AeTOVB*G(-M>{kF~1HtT?_t z)&NL@RP=?_;i@Yld-%Y)=@Nt=d8QJzt;NiYMVa>>J}PhGd331cTiNJB7#GGw!cf8Uq6IsQwq5&o}8lmVYsOQ1SAE1co>3#U)Er4 ze^U)HD`sv}c>JJ17C&Ac9JClPj;$b(L->`yQj7#x&x~?{xxi|OXqohy(RJg-1j#mK z&r^=NVJUmHSM^YuOsl)ZV}d9JjX*dnRnPTn!~fy`{(Czf+6bcoZ@K^f{RH`wPrm%q zpZ$3A)-7@9P=qlJX(mc9f?J+eOk+O zfy}|bP${s0RQ%u26RuW(75J}S7Eh|!vL;;hW*>xwTrp~)kd9gF)fJpP0D2v$G01!s zXG_0R(GX!#=S>xp&WQ+*7xRH4xdh!8FI{#x*TqlIr+NsT*^ThUyp(Twkd7d@(ng07Xy`<1s6qfDP|<3tFyYdQ({yAH8p4_^0O6ZbPR(Qh>u^GU$@ad z3vr6oa7?`mES<}`++_}yj|oLSK|S{&wrp9ZjiRdzzJwR?UKy{eg0Vzd8kEoH*4v+V zV^-cieDWX__IIw9{nf{W-F~`(%(BHxcWvK3bbZ*WLx7=?Dnt?{5tib%8Ek2B`scH^ zf06Eil`B>qIB@vP89l~Nwfvk6i0;LUml(FUupo{2u5NDl@#BYEx9(WKVe`3jrz1I} zgabOSa_?VgO9BS%vUP%T?f4?f z*|B50smEwCGuM6%J9q8fvwPofKmJWwWY8x%A_E$uqgEj?<@zo`#DK92`DP#Q-m`!2 z-UGk;-EY{o|Sav19KslFvT-V8FI6+3sJZn+TPGbjnFl zMQE6r|EP31)(XN$kNv=AuOI*R*VZDK%tTQaZQs_UcJSaqssfy?M()o{|L70@m2l_u zsn4x2NFPALyUb@y)$50HBcY-Z(^^S1YT;r#76IYO^`Yx0PM%z|W>q+0Bi!jbK?ZjU z5PoDCQ9J<6@3IM2b|D3=Waa61uc`?V>pz3SW5gzUZ7C@ePZDOo=ytB4AwnE(1b!Ek zWK02F_1{gi%+5Gh`V@7?CLE9XK?@0PH*Q#C?mVrFf))oTExK@tVb0Gz5$MDL7VFW4 z6nauZ300*LvT8a~d80ooWTc4E*h)?C!U9b%LVy0;PVQQn4n?&WwST7bERSKfeoCBd z3YDnHGDB0!9N9i_WBg{;B1j{C%}UAVYUpIn3W zd{-@6F*-69BPsS_mn;#YgFZ4(G4MlDsrAcPO`84zn<1sF_?mJ!F*NCl#(0ll9);mp z_%VC1ojrF(tI3`+dgGD@;k`YUxTi0_JZ5PfGy^A1RuJGBB5d)$+=eNUN9hFCfjK%l z98Sfko8gD>7-3AWgdfM2dpmx$8a=guBh^&AA3o@=aKmx)!9Vc>4*cD@HN*mtN z<4V7fd?8mDk*gKn?!%Xm*%8inA6AZ+AKXQ+i~WUn{ub*G5Li7BA^nB}2Xg z`(^rXZWQ`z>0WerQhL$Sy-f2>wuL)rS_S#g*5{&X)+&TLfBxKBhe@3IE%EAQh;{Vz zaI?+m!dMq=$ag%FyAnda?Tz>F{{0TM-lzKNzI{h0T(pD9aZO1lMd0LE2yXV7RlsXj z*DcOpp&*QK>3$3Ddbh$^O8Zy?=r^JvnWBH z!qFF|25`)vl(YIL(61hC@eQn!Gi9dV#-Tmc*Uc|{`swGt{>{hG3k#bpD{I&%T_pe8_-C(Nwm};Y+Q&2;Msnbo+cIv zdsPToX(bl#?Vq_jWox*1yN+KQx@;qRF^c_Kvt03MRvjB85(KLfX70@#KmL842M_K| zuUWUQIit`ON>W5VVjbZMuI5q9U5Sy7cAGPB_U=D0Gc&E<2v|OaCpGy=kXOz6<#2Nd z#>$mz-~HbE7cQKA_GH%14s#4YbwI9)Ns9G&+GJPd(h6b~cGJ>+=Qccf^us^=i5`e7 zum#lE$5SUq$uX~^&T(3j8Xz$5<;c-vzy43Zq~cM_xFaZMz&*p&P{-QH#+w$8!Xin+ zY!+`JrMl#EC- zH-!SxgMk4VB?7$Iz~nM#g`#?BWFd?p)}rwDVHK$6!9nXEu(QMxQ+6QF&MR>s3%%jP zOgL(Y$L2$SzVcDZPR+FVP4>c})Q%NhNybyOOVWyULUHcL@gWr@F9$ZpoOUBf4CX zaQOtE zf)Lc&C4n2@yoITr<4i-MEeHZ8FWVK`{9^J;mqxEbaXe>&HsBzV_eEN}^(-Yy)|#m~ zYRnSOJdg>vu&cX=<4Sw7d=!8|r;tf+CCRGxaz)Twial&&Twc6HEM-SkEW1mHo{GjE z^nw7rfbrN1@#rm~j^gjJ+C8 z3FxZDTDs2XLg{jOTcGBZ&YRh9eR6V|MXX%uY%v{9sU^uhO#IH(m}&2og!SkVkM?K# zF6YiIU#>B`BIaB+j#myqZ}$sK2vd_s0|TIp28IcB|4m^M&{+ILH8mmK7-^&`Z}u{W zKTm7oUsg6jA|Mo*+X9u=j6?A3YDotqUMT$=Vj^z#nBmUe@f%a?*R4!%-0V|@dhTlI z$>6?W_+21gN-x$F@WpSpl0sZ%fmGBcdKI}dtHeej+kbw53Th?zjYTw>6^C_H+3PAd zMp#I8^st^2U3#v7mCy?hQ(CZdTLD(G*HszZ!7cVuDL6I>Lt&Le)ik{9-7HPZ1Dm{s zk)R<7#Xu_jvS1o*nX5b|Ta*Pib%!XGfseevNH3hS_ok}e=1gi4P^4#Z(h(|bm`0__ zguWnOWi}!d)rgX*=nI7*#lAVImL(0QKEWw7EH}cjaBl`-u_UXqNHG2gkocblDsbK5 zJP_${%3HUMm+AP3b;|_Rl$BTIE=ow@L7@gHc2b?2H*bCL(Z_~|&1+FsGA&&4;f8@E zKDA8u<%`HGh){ywp3%|qFHf9&`|Y>FI^^ z(}q6z_=8QGw>VDK9D*i&z^F%fp6VnDs1SUE1$D!D6zcx0GGClKd-}E4kDU1Ov+M)t zi1tU1wOO-b`Tl*yq0YuIZ@+u|(c_0hS1+zvyME@u%)p`n0!m#_N)?z$3%NL&4sicH zOzZ5)Q$TIRI(=tiboBZ=@4k2P%TIwq4r8|3PEs4|+ap~uI_lmz{)4fxk?A|P4Y66~ zZC-g=72#AWaaP_k030l{<9ZCdV958 zLs;QN&&zy3YFZJRDIw+7vy<{dVaTnrb1Ok;C^d_tZ|}n_AoJ9BY$G!iqYW)1(~VLP zP)@)S>@MhGptU@BkeW%I4-B)4#bk0LV~)e&ixjKFC2~kW$ro|Z!32Pmzkr<7$?FaL zB$71$tC)Bn&do2`JE5e^aZ?388U!A2N$x7(<;4O_vIW*kMkv81iKLD)L{AdFs5^sL zV+z7Lo+`n52Mw0Ec*)YkubIc)M46M#2KPI-RBnp!UH4?auYy%|4L8oO% z;Z=xt>h7Sk0II(I4wPtarSstig!-klz6&J`s|&*C&RyHSW8?B=ON2*z z_H6&?qq9qw*|((k{rBEr_1CVAZ`iO#1atP>(2*ni4`KOQV zNEd3pjd_FP6Y!KDDm$>XpZ4tC&#m9hww$Q$x*BIIirLvL+3NUz8zrI`((7S+tcSt(DRc* zinYmActZ{)rmdy$*aSKf+nQaT`|>0-D&0(xK)EF#5ZnHj%aIq$B5iojXi*#>Lk#u_|FC7v#c@34$l~p9q^+xtDX3xxiA^eYT ziB|cGw5weR|DasmODbb~4i%sf1sah?M4B~Eo>c6jQdBgIbSFS83M$p}r{QcC0H>{A zn*doT^z<1ZdQ+51BXI$ewkI*yq_6MEbe*2oLAbu;ou0mb^X5oT@9M(`w{a@3zHRrE z!(OA))A!QnpCyC=9Y>XNVdUyWz0e|pAlcKqZsR5g2XTRBAesevcX~?A4IrBSQv&I=xf(NFh7)Py+*D4tO#=v2MM(AsuRy6So9J5K~ZU zYZXUWj&Lk5% z_BigA<04D4Y)Paji4|Z$B?+*g5Fk1M62SZY_PL-W$A8Qx>f+w>owNJdXP>SjnJ>y7 zb0nP0SQwYBuCBi_JjjyfD?|OB5Qfy8Bc+ywt`sRautA#@d;@pIFkZ+8-Bo2}RaJEt zFL3_y@P&&P5Sn)44P2865eK@`7#HL(UA?-TDU7PxhT);1(VVN3Q{zhu=(^b_k_+I% z2D?_3Av#o&Q5AFsBF>?)%44Rxxn*N>OUpByxtC5Wm~*oj-^s@(Iw8F9C@*N{;_&II zj;@|_=Xx2R3LPb55Eq$wuoEa~Zr;GvkE{*Ko28%@PE~>$oFEw~Nbt>Q6!ywJ8;4#$ z%Kmm{02WOvpe1WG>=VYZzjdPsrfZdp?tH_zYIK zO_>9OG>K%i5KCr{7z%knI->HGChrFY@!P;>Mh_#;kfkgQO6WKtD1cDJz9~ghCr#g$ zZ+8vG8MJnzU#wEhMtaj20R!K}{Q*r_NICrJjW~-4XA;_irbIvH;d(E-ltHG0@f1Uz zJjuL~9uN{20T7Q*pVK)bv`C8rVRXHeXo@q!Ho(g$th?u6)A)`AtKr%Xe)uPTvc3Hbhul(&IYbbk4DTUZV33!&VFjKtTfV%A1gH&#XwgUoe~!b^ks24;WRM3;sUZ_5 zFvT3EsEfg?0AbF-NMFD4sM)!BZ@yFY+Aw&b4PMZG0EZkN8;Fc<1RUh}MNs@iUnftV z``o{Lf>7o!KJnL|`uo4+5Z+(-$WMInOW)Yovc9Ed{pm9uO^ugm@~OE84!laieS3Gk z>xsF)`@7FDzD2`-6J`P!P}K{~u5R;9&zp0SEb3Fe?|_>piVnx`z*MtM|A+g&;2 zVO7o5;SuCQ&XA$!l9e$RUAh`Dj!8g9^2MOkAz6yVaJ%{lr3*~v5%OK)+ShkMn+YoM z;IO}>Ag`vm1;St(hsdeWgiXSL(6Bi^VSH%#3R~-`HqgOsHoJE0=GLcEr`y;|g76ha z$P=3=;`~{ueM1jOb$nuK$Bvf&@bf?3)qUHlVEH4CszC zCzmZ_CT&G!Wo1_OF!LrX?0`<1961fGrYQz6=yVZ*6(@GqY0#|oyMFb`Fl!ySnU3|j zkk@zNJS(+R12Q>2L4b#|fPMybN>*}voj2zlKl(ajqC{i0weN8G@K|7W>{}ag~fGV9UbEw2lo82mk^F<{_$Ul4C*3P1jpY!d{ANI;(g{ug^%^Vg6^4WiV5=PA zUe@MK8>xfAb=1FL)}Q_-0U;b=`yJeY1>)&7UA;2G7Y7}ci56H>U0t@i46cEd$_h%& zQ8pNg4f1AwQ;QDa$A$%6FN zWnRp>s&oZi^v{0oE7jE%yLWB>o!|YFjV)_G_<{HS`6o(S+fHxWwuw9;6^-i6a7-5= zb_YTp_vG&1e-D}1RA49_HpU{kmF%f5T?JtRQe8rdD99L>bYc;busVK(<%q5ujaP}xo!3XX>)86&*KlmSSf9wJBP#&-H7AT89WKMk6 zLBGgp1}}mlkxxoqr%TC8Ta;X92c%e!pakDg9OD>tZ^9SJAkh@-M9o3|#1=v`@N!Zm z8!u~XX#Nb!w**+~DIPo$Cwosb0}64ZW@wZOkbw|e$JYn(NEm7I3Wra)7m~L#VIc(U z)EIM$`9y2LBGJkqfe^vC0YB$hs9xkvc{w@j)~#!8JI-QKqe$M(FJReZwuq2<0#*DB zINAM|E;0UM-AhWfU7Y7^6qHR_(k%e9!CK-^A{QY@r9Y&OHg0fmkP{t=pjqcm+rt|S z$1rv-i!rCj1uQJtLdX|f5gg0Vr9QS?F?g9jbN%|hTW-DWw%Zv<<6xb$XL}gbz_}Yr zM;!=+Z>c9h92^?2tF4B^cfR8hy31dG`o-oA)j$x_o1Vl6{Lm^Q`LJdLWoU(3LPV(f zI$pJ@X?=H3C*d6tC)cfTqzz8dFL;~{#*ud}q~C@~dJ?J`J}F{q{GzVDs~Pn|hg%-;SRH+s1YYhskPd3f+j zPj~mJlc#?OZ&OoaePaW6BQjcXWoQ7XSOV+As;leR19<3_m+)WU6RqrGJVcT#U1#(8 zhNh;q$B(^UU0uWCO&UcK3166m?dk05>hAd=L@h1NwRN>?sw$_aCYbR95nDc45O&u+ z4-uif_S(zD8OD$r3I+pb&dRfpYv<0rtf@Ns+JX9phJk_pD}#fK2e2ILd|%($?z0yz z_Wux~ty?$O)>ad7T)oO|Fiv0AR@Zht5z|y~nqMdHG<1n zJ2;P{Ch7t;V-k!$GBnQ6JtI(@w?Li7K>#F7C2=F23}AQ2A8U;63$dpYfV^z1_u@@_?4kyqDg{3OvPZ4iaF1i2Z19)u;U;WLGT4G z)WMPdP=^Be1=J8=#zBDE9e3R4n3os3q(uzvsGD)JiE+j$rHfBV5^(w>_@oHnXI_}o zD+<_tMUgkRw9wCx43o}~4e)?LIkQM$z(t1aj=VZDO3w!;6KFyr$O`QcwpbfvCrY?% zjzQD4O>3D7-?(Yh%LfnWoal>0RL$x@*ukj6U=$r+G9)uJ8d6I!_IO0Sp93wJ>KuawW&s(6;$zfIvry${p2VA85n>3 zhyS&`qwBMu`@+_(TtCeDh}vO1^3E4o;jb7{I-lUoXm&9vlS1?ZcO;Hp9b?$$9dCbd zd}4xu!?rf|1)lh^r=Gauwv8+c*|1@)1rtkxTLfcT`e_mb5VA90(b(8TxYpd$&HiKftSzt7G&%Br6IE|BVNxB#8J!%hUcCQn~?phJoZ)il_#d65c+ngn>o} zJRL084AmfS{1UxF0!{JoKLSLy*bJM9LcuBH!V-FAbJH&F@m!nhj(<7QZ5d#_CBU-Q z#>3re)6+8$iseSYmVAnwNI&GoRU!N&2~w!uR6;@8ZK8ttB5I;H5aaB)VHOF?7H3R3 zH0`65m4Mgoe)qf2)YsJm1PzsxAPxtUIHSRk5S0AyE_9nE3c|iv6eE|`(O@mXeS~O~ z0`U-+@3J{r5iw1M+t#H73rbI?tiNaHi_MUfBFf~fpq9&?k)v3IZ&bu}r*z2RQC~qx zqfQl|uwukrVLHsiHBU=%9~?=~*c+R_@RF6~Yq~qTugzTp1#@S~)gqv zKJ?n*XP}X6N(Pt*w9UwZpHxdbp?OY;((oZQHj~Ge<^-SuDME*S`ANnlqgeEMHL+uC{(N$%OZtFft(i>ns~ zvRM1|ws*ddPR0*YU-WXzA0B2JM;Z-Eo}IGSN>Vvb=`Zig$|IDY8`U{?{BgYH#%L|JOmM)2D3y@Q1y>;}$h2HV834C7E zRi%J^!!;R5)ApE)H?n|IEsg)I0o1#KeD+UJJ)@}rnmbMi7tZ(gGQ@~*ASvXlzm)<; zQzQd7Q`9JL8AmJs#+gIVY_%qfOD2W_l`x&`c5}Sc(dZV9=Myk7$X`vF|j|_y;kmPUtRZ z+`jnbFKyqtjd7QVaVcecaQ^(+Q= zzxvv1Sdz?SOq6unFCN`Ey`s1bN|osrb)tNMD)uA-(?&slU~V4MJ>7nq1rnG6nM_p| zyo)LqDL|1fYe+PXGLm4yrJ&oX6BB z?q0JA*n$r%K~L`!?|eJMA>aSOL6(!>bNB6pr$75M?_vOHWMs6wY}I$3`QE$V{dSJ~ zVOJU_Up(~S-Fx@$+_bsnpZ@9paM|$a7#;XzITD$Xod~o`r0ZABHvWp2FPVqTp=wpY zM%{-Wda$vvj+*t`|LqU=-g3*{TXucq=YNWm<>2ncmtOwO-vs5kF8}~Q07*naRQR>B z)vGpa*hFAq!v`yG0<{Q|zqGtm6vF!1uYL8)9L`5^Q=PZ+vTb&8_Vo0q+{GtoMpjBY z8WtO5b~{l-?Py+Rae`)~jGW54zDe6OpkZfR-i>A6%{Q5IUZ z)l<%IdXfrABH{~YhEf<~3;M!!Kr7i%31LWT{qY9G2N(?|6f1~$j~G*9{#S_*vL zf-6%5aM99|0C z5-1X7qIXynKmE;bKKs?LKD}ed4%)~>=NtL>7KEc1>JBjRYFHB< zlbn8anVz8@?)#NjUS8j_nRgQtlSEsFVXfNb!}D$q0f#k`naT*3t3x4!lDUANqZ zkgtvmv0mlqk)vl$pMjKL`uV4J@7PpbQ}v~hRJ~;VbDy-{L?e{kG1I(_9q2b>}S@FuN49a|;e0e3c_% zn(C{6>^+Zc-`c`#=-}{|?z(&b_U&8W{p5Rp@WK!N^v^zV=#|&$&1~Jeh1LDF^-b(2 zIP&^I?lNU!ks8R88aXX5EIIIo{*b6ff|#=jjytA+QrjV%`_KhFYqo zhRQ>Jr|h*Y>Jq0^1)zrvA%~-9%%@kZM5TdTlIe;&G7mFQK5)Rox(wRCA2fFKq4=3jKCs1u=uqJmoUWU#?0xj1EX)ymQU zqe)6Dc@QDlLizzvf}>!18PsJhj4Tc_gvNm9&GbxOi=6+$jq^p3DgpoE^+};u45H9i z7>t&#ilLlR2?t-O3#<^3`hY<=1)}tjhpNpAQVPl&EQ@t1$| z@Iw!<;0YVJ<(3^ogTsIPC!hMYU-=Nj>(4*`;)RQs8BYGpXTSE#zx0#T0#1Fq@4h>~ z^re3r7#t}o@{ZR87t}Wq@Qq)JYZ39A-;#}=sIsP}@!Yw-E5jp?KmH)YRL73BZQ8Wu zwIf|${>s1IamQX%{prtqv7}^8NpTV3-TpgwfBp;4G&NSzR=x?9IDrN`CW6nC>%BbZ zr2{Wfn^vu4P7qy54Ar5exY!)(FY;PhLjek32thW9idx995Q(fLp{o{BM7mnOh~XW$ zl+75Sj*I^@hLVhpyp{lDXjeV|@B=hf6>r; zRT4RA#JmW}SzpPmCSg~vBh8^eg_W44sn?q(PnNhAn9hyE|2R~NF;TZ0=fwq)qJ);W zT$OW^Z{9K0#Z1JEt7>A+m#BRYI*_DjBQy|=Z}3Ijk|0oP5xlaFrlN2~alsU88-0Ru zlI@bl&w$F=8*C`0c}_c8)frnK0g;&?W0VhTV|9nc^@-Yo%&|VyV*{luu!VSC{~!8f z+SzenHrKO+h;dP8eJH+oJF3(Yeey>o*v<(AA=kmk zut?hmz&Q3;RaM2RGK^+$aDWtm1#qCMZK;jNZ}G5B=+vpUzxeY%B2MR)^#|_1_jLQ& zl49!}7usmWVfRSYKCL^`HOCN84IYax&IK4?o0q!?xCATph)FRfdab zhwuoxGexC~mFbASLqusc7+c_TSWGiMI{fOP19TiYs4zuWzxZ=McK=;_3)#l+2_iJ} z1@v`T4z+&Wx`w*iU;mBYJl)>CqOJIjJ8z$y9DC^p&o?zTvDKXZF6OSzGnG@ka_Z3x z1t7Z6cebPbbivZ)?Wfy`uJZDiee_p9v~Two;s>G&`us4cX_cnBeAg+tG79-JBxAVbDZZ>(@5? z{7=1ib8{0_-7&od#$c9}tY9%x`KFfU)uk(c^S6Kh=#k@<73K8}^(T(JdTnZAbtz)G zN-ta+h)Rl`U{?`vhhb?V*c9S~Lk6zd?iZ^~=jT}>Cv9n52)SdU(Uxlf76J_Sz&eQQ zhB^a<92^f-*c9q&b~YXK6)QYJm|JrDFGgEqJBvM*2UZF&dU&3&@>W_ zkqz+VBU)og8dLdD3#oR;TzZ(rcm}PbRR@JBm+;TR)`3N};ib1c88k;mWeh~IAscTU zkd6pIbLLr68on+R0)apv5>iGE=a?F-iw&EgAcV^xW}54;Iz%`~9rj~z88y_JXD8Lg zIurzqwBi|!U~~ea462f>NC2<@xF|ykY0+JUMkc5QB#^}o^6&&?4TlkI1Cuco5_knT zo{$YN;r52t^h&V(6S;O6Cgo8Daz{y&%&aCN%FtmQCsXhnv-AAWAuzDaDVi%wO4xGY z*rKFzb&1SFA(^7#^B3Qvoq%fRCt!jU00xOc547^vBX4oN+Eu)Sn$Ur7Q#aSlq%J1Fh_`2It{ZUWag z8+XqamlSbR14~hn^G&r9lqP{dXAq>-25Rd%UyUV9%an;%7Pks1Oe3V=%sxF-pnwbn z;bdSTX+cmJQqtGx%a|A@mR=^jZ~kNiXv>xs7WJpZwd*$g@qhmZmZ??OG~Bp8_w>`> zWRkgQZRIci$Nxfx_3NAQNk1fi(j{FlVWW6sR#QxtrgOpAj10JPhGn-)ne+%=6b9LO zpP@Q*7|ooO){rWx6<870+m+J<{mnl-DIT`CaM|e8B%UyzMBHqMm>lkTi6;uAQcm`~bHK6X9yIUJ z=nM;?W@ZWsxM_=X9?1|SQVAjyg&bhph{RzY@Rr!hz)LfdtS(|~7HIBphEvMGE)et= z&RYtSrDjAmF-ez|O;-~5kjZ2XbK#cqF=jM3|KHSEykM7Bef`=OUO2!+48)9$PtrO< z$q!{SZ*V|d$O0)=A!tzM1D6#5GVra}XE+?6E!)dnO_Gz#d>_MC6#c0WKCye}MurK= zpX*N7>qd!=uycV__WJefKJv34{+*BikLRC#p5+CV)oX}%XRlpnF*Gh4tFd;#x}FVe zwAwI2GZu;r8bHswYT_08Ti66^l{(wo$@}9^KDuwu7L>)P5qQz7CllEP1#ebkQ^U{v z8i5c ziSq{i%97y=w|A*vTfrMdpEOis;}b}9VCc%JQ*f01{wE))tF2(k!Ln@34-@j3c*ID- z-28&uZr%H#AA8TI|Ie4Yx_WABYq{BmBTl&;h~r)jxTD3D9R#Y*QZF!C%}9d?q|8Wa z$;uVGoA+=QGvin=fVeP!jb1B47}`RrjE?Pi_8V=vT~Ao%;#F)Qu}=yk2c93uFo2Do zqmXK!yecl3%~0Ym2-A@wGx9<_c$ag|NYQde0hx0QXA_+0K55n^ zpBk&$a7|WwS;RmUFOBv*YwZg#y0BYW2qJ9@aXFkb1!k*eHs=p#c3cW8m?g6Z|AWAj zEhDC9+LSJquo5Jp$)Gpj&cGXf!$yOGNXTZ^b{lIqRDw;RLD(FPI=ZkbK!E_k;G?+W z0h$;HcN_}?tiV`xsGaaT3DO!b>jn@JXYi9}CUx z?=hoC?-9sQKncs@=!7A0BnUNrkpSUP*wnPCp}wKBv-9}zV_dy!WsRUsXBfmoNotcM zIOE6w;g~lEa!Or#co|}fH6>m+rhD|3bRGqw)rs8inG#7W0+U~SAz2fB<|Cab(LF-y zl(OnHWBE!fTmi$1vIz*l!<|S%o(z!!{w8&y97kS)nLDo2XBL_o{JSfSFx zpaPooWk}Y9IPQ|-YCEL zjYMi1CQW+>@oX%suWN2>T6_N7xwf`e)?6a1XpTaGK(R-_69JQ=BVC&cI1Y4$aszsx zBSV^PM+=GTH;7QSg5m|vfq`H$huCB~xd2rc@KI=b6`+6kgLf_O#zS*M22i9xlk^{gB36;udkm@ATH88CGK4?_s+^u=&!a;jywPiiy;Dxw z9orTL#QJI5Ar{|8agsuMq#-e0XuvoGY?q#|omk-sn?L%dR1M4?2u+~sMrRRI&(vh= zxnR*WwGj^1GXgLm{Wnp6npcvQCTmm)W+nu4NqZWjCh z0TXgpUU1ESa-1DU(}ta}!iJ^6@j8?;%F1N>wKHvzws<<@XN}|Irx~H%ka^}tW>GQ zc)|h-DBzQgjrbEzga`TH&=Bjo_TRRSNfgAKd{oZ#zSXWcc`~vh7 z$Jj&Q?Ts7!q@rEEeCg7KzLpJ*EHdlt?B26y7aJuxe0r1P# z9!X+9QPGIb;k~A|uA{S)xQ|W%aW!ERiw-TFd{(TvG-f3U$^jlt0!w8v5ZXl$uz8*d zL3&rL*5|xT{VNw#hh`bwl;;pG@H8E?j9Gz?`vxYKEG=BdZ13d6I3l8*gMLV)3k26p zH#n$l(B%Lsyg)w0Lt4nun!yV`QxZrFxR?%@xpsAAMD}??8gyM)!MI{YsTB#H!>@(O zImaK~DC1dlUIMny4MMUV-tRZ9PD`CB-tIxT0 zls*zUGXf8Nt7gaA0j-Aw7W)-cA_<8|E7}6ntjm@xO=UHb_}?g$(vnJwx!(*M36PEP z?+9&g!c$*>Dl9FcNaByufPpE$kd)|4(&VQP(0OI-NL3|6NXSnLkQzKx?;Z?ZBq*b98Zuz)uCp&NQ;mur6;Bk9f*|BBBz-PkE87(Eh=#;;V1b( zfghl*L7OxYtaFsib&k1`<$wPK0SioY1Yd8&zSs<yd0WC*{ul@x)H zoMXq2H8wV}N1MKJ+O0)8u7V3I`2uBI^~ujqwJoW+%8+{3o`J}Mg!*|W*CfZ`3x zc&&}{2Wat0x=XjGroz6|2I-N94E&|yM#V|Gz6n0_MMym0#eoL~-9GT|?DGSldT+@LSDFVtHcVDJ~tEir2F zz|UNcSO@`Gca$Ws?N<_(I&b1q4arJ0MSzu|@{K%bFNgh2Z9bU9LD-UR71Y zu}@|QJQU7Q&TqaMI%S_fe-68+ApujsLcX&VYbw|eOzUe$8A{Y|tZg?$h?0p{8So`6 zKn}uY;S|9EBZx_za7{I1#Yt^dEfc_t1ZYwg4Of!DQI|%Bhi7U3;#9`ao7)qxSu~3* z&^7)L(S&Nmo9xDK@lb^t>MQc|t09n;{yNitmnSlU5MVTSd9t=xTM!b|CWR3uE?dr; zT5BfKxOm|r1s{6of##NNgO@Kde@AP~ZB;aebGdBBS;Beir&^C+?(JQ;G1FMThRhdw zd#9&&G7iUn78W{i(k4n_nKrLNW(pC5z$kF*I1_>bUUBf|GVb_W;5_0<<}5~r-Q@qa zM<3a^WykQq!1%}@F$xE9LN=?F2^1#AMozaLzj*%K{Pk<~wUuNhg4wdwjxy`mxP6_e z$=U03%ui($MGh*!+&qV7;ra=<;S^`YVVI;p$U@PzEtmUxSy_*i**O)%DV|Ch z^~B(%i!iuhZCwOdSL*8PIX9U>GEOQOA7_6Yj?uscL55;!xvzTye&re#bFizFO2M__ zAiC$i`zx!fE}lEf#2-C2IGQ6)jcDdZn;TU?0vi2X#^cyxhIFZTjs|lcG$)r{x^$5d!EmySs~OJM*3^O&%|?}^gq2-e zvYu$@L_(Yx(myzW+z=5ca5rRvJ7P7wkM++8!+|>5N=Q(hJgG+8TBBTRD87uE1!evH z{g`wbD}r$2w(P8$nmVF$>=rnQa?z*%qk;-Zpwt7ZfW%qdv$DH;I(35i?ze?JA^Df@c<;`2YJem5Sf!!SPP77Qw7An5d_lXDkoxb3F8UuqL+%rgdhm+KmmAC zkt&pMrgoy!%z=esH8nMKAEK$y&k8~jp?F~f0VT3;ltbM(Q^OD>(v1lN0-21+mS8E$!+$YezSD zNld;8wE%81?hl_uL8MUT$B85qL!bQ*Ms)>zv0OoKCm0c z*guK!o*tR?VsdJRkcQn)i>CzwG(&L&SpM`$Jo1XId0gFv`{QEUXhfqvWHK34@?XLc zg@QnxvS0YH{sao@JkD}@e0tWk*NG_T!F5N2Oil~ru*t5jwxPVDthKFGcji%Ga~w#0 z{P@w)(Q$adMC@GNX zGz|T8VYzA-yemqIh^xvfD#bfEh&Vh7YjzpOfu3nQ(tEBa;VoQRz^Koej-C(>Nb;MS zw`||Fk28}FzVb3#`AA1FM$Z+Mk3IH8ZvK*?p~1f1bA*r=E?insM4YpP8d6?a-Faqs zVqg#@bM#d0_1VFJORpb#nWLmIBPcB?;>Mm_C}A5BX2WFsrtQ0S-EzmJ3+G=s_!4HK zeB+SY=9ZQnJN6>izP_`)y=Q4(`v-=0@7RhG7(pznSku|wc8$|zib{IgPC(d&-m`}f zz06tpsFo$-t5>doeQsK0oQo;)!s1oicQ)U8`@J3QZHEsZVO`*S78m2OnFS46xj|$@ zL6w0~E?AtN+R)s>SSHOI2xo6xvz^b*!9y|Oh-D&njYKnHRygJdohGniqPC*qLeh0{ zVbw^F(_fDtJHlbM*bpx;3YWGyNHYq8-Jh=&q{lOLSW%^Q@GsP2)d;<0>Y$0@iE=sE*p@9@ zIaTuTkt2&iE!#l~`ucj=L5qD7he%dP$}$E*Q9LJ$?NOYiBOd9ONFX4PBS(%nO|OnD zX>yR6K1c@)f(*;CCF3=DqIIB-FiYsDgEVq&N&udN5@CaN*WNb`W zmIgI&n4u0CnxqEvIZ$J~zyC;t3sxVhhL|X25$3=&26NBPFE3h7JJ8&`uC}(y84udS zG^yeO4_sF?Xz(DDAy7UoDVIWoZ^ad$BUwF!C+2VXWDGTrfg+|{uw({1k%{QpuU%1o z*FAS4UQUZ(umGcC`o`uHY<&0t-R33$gO^fLAps!>sW1ghRDu{TMlnAA#K$fE^?yD@(|Xgu$JWEriB@ z{C3n~`WibfuoG{GMI4YBQdbDrGZcqC$)EHm8l*#=C1LpszOon-Vu9*44#-6GqDz|R ztt5D7|wPQNN8Gc#yxheoMH%StqD17SIRG# z^6XUg2dDz0#{#mki~(ZV{zTk!qV*V*aV0042w3<6@kob-%xkI`P(jqdqr5nH0ui{? z9cJgDlrdibyGnz=XdW+Fva)og=9LkT4jL_>2DJ{6A)uH~;JcFYVFYr9LzbCia_u}t z`427%iwoK1ZZ<7IOE9nV3x1L;|5#_NQh-2A2Z$s~IdL<{DLUsaFjjf@Umjo)VpJFf zxcI;L)35X`!!3V;wp16H?Ay4xIh+tu2DQpbsA$E=N?l@rXX$dM|D=TDai^v^nsT`R z!kM;(l`Bh=4fppCl$Nh9uP%S}!1o9GFA@jF{5>~7FIiE-a!c$?bs-8I8HZd?O^(4h z!K1bp2PGt3AOf|T?I6VH=;&NlE_?G7lY_mU?=muYslDyUs+FtpWOy?MftD2&ZmXz$ z{goH{FJGXNKs@Z=9^B!A{V}FP{NdTD zkpnM2&o)DVqAARq^WP&68LAgI`XxXYXJ>h9^=@(Fr2AfW%Ks1>VTBrn-_s zSP8LHn%{(J#fskyNAFwRi-wK6#TQP`POn^1#MC*jNcEqMj;^SDO&KO=Rx2mzehzVQ@Q8d-{y{p`0Z^oqViBbfd`hK0r@O0%1Q5li7_qb}Je$1O3{aMA zdA3;l#f%0(;=V%S)Fo@4L|FhK6{7%ctq0QgaD0 z(9{~hbf-e)fq?j8GdwxLp@WG`Q;{q%IEjtC&ivwAqQ#O>i|aP=>Oh1AAdwOP{fP!S z|5`C7w|rLU()URw99cp? zoZU;SR+mDIf@jqg!(#l;cmAS?V4sQd42Ae6xu`Tw0TEM|(1iW5|SbE~8w^(g?ctHXJD{ zxgmVnl6-c7vFbOLPyi_;!n+?1u;K+JF*kSM;iQ%n%+mTOKBW}sJ(UDYB0=gSh=vnG z7u;gFzmS3ipe&XG+N(~i=*#$r^;oAU6jhRi8oAT#`B-o;Du&xM*e4v8| z3qXbxkw87s(51qk85;Hc^eNR88Gxg_kf8V0ai+FA)P3iJSSfCt%MWyGoo4Y)ge>A! zqfp_LCt)vz_>~~biBzZ!InuEBF6(?ZBIdtbCv|App*k>lwfl~p+dDhE5e++0Ss^4q zt&xW>{!a>!gDZjXg$G$--}I0w*OWCiHG+xE3eao3%}XAvllX@j{Z|gXRJNKKwhF2< z^&h0%>cV_q&W+jOOBbn>OLCS%+=Gw3t8i)li6gI3<1liJ5MgrU(#X|ODloocVT#~v zvhQ5CXQk%lFe^v>l+R=jM!50?2UsB?(YL?-^qR`*y8619kI2PNpvvV|Oc!3h#1QS$ zJeFN&-}~^}SC$l>IQ)9x^`-&r2KqP`ll=5Q;EQ;!r|V=}Yb#xh^72X)s&bkC;-0mu zSHAqE&#hm-fh(05{6_}3u#1-+t0OwT0)vE3?3P`!>>clZhzkTx9A&r&qXN+amwK== z{nF*Z=9XrLJ|TbHY0ASKg|KA#@>Q!=W5s0SKenEpKXY7Fo_bG(8X4*zx_sg2kwfK` zH8>iq2vNYPo>Ki;CvYZ618+viM0J|FHv9Yw-$loq#gAcV`Wl2g2MwdNL`!P-fWVAE z!;kbG7#aqD;t4~_)Hk3btXXz`IA|dtGg3)`l>qR^jlfny1~#U~$sbivP*CW^gDPhS zU0`8w=q4BdgbT(j0jO>JF5)XzrrXhiU(tl!`b{H(N!CjtH*V-6ftncY6h=+Iib@32 z@;`53TI?VI5Q|VlAP5>Eg#!o@BtG)KtKkP6cA4+OK7+zRVmJd-8Xm)6LD5l zY`SZqf)q3d!GZ!R)C&(eg%jW_2`myF8Xg{-=;ahX{es8;iXVXDwIE+IgeVE6LShmR z?{$WeEhU@o0B1bVC9aS!KrNk|6J!MhcqUXJU2F)GHA5lz3_j8|c8B@^lvLm=XHm0g zUo(w9QdP&HUnW0*BoMUa_MospRl~yvZK@PO2;C+ig=BaO8q6o8YCS}h!J%WKDRUlc zXz1v&YTvCAbJ=bMAcDIvtNGl6-3Mi?Lk}5G+l!zSO;{lh%F#Y6aEv#5VLOj@h zP@3!z&eLj0lpEcnDD;1vgZtq*e`!l(0vx7ILkqnhZPhdlaymS|23ahoagB27M#j zzHZPGVTy4=*pu3~5^ISvVyd>rH)OARRlNQeol258F-n9iE}c&#^El>@#ZFOS7W9qpZi!(;0k>kF0^uzi4?HCuP=YS^&pYJYEaRV6pSFt~!1 zpcN*4`Yv4Fzkfd~cp#DmSjd14XsPpu`HRDKa)wz7+iGdkj!l-9u4ZF6Hc^nb<&#;qxsvDgVFaMm893TrBaslAFy#x)fTqF@xMs=A897p{y>)YnuMuor=KQH4d@ zckivLUpv&>#Tm^*SBJe~-A?e0p&=JUH#Ic^5Sb%Y6k1+UjkM@ZfsS;w$wmz>5h}0Z z;>~3QY;5B%$gf;e-r042Z1P(9>Qe5KAE|tla1wm_4|K6a_a?&P?y!v#++U_GH@$Ce0nZ)a5B2G9!`< zNl+6>dPrrcQAu!$eu7BBs>)ETxub;bikYzm{fs;&)hDf!+vVRuPw*TCg$K_`n*JbF zIH(4DyvPw!B2YLdHWh3J6YU@@HmtZrj$-(0J<9sq=lkbXiFbdca_on{--G zZG^z0<)b**5lsj)rTZCm4JjF-WfWq$MdbVyd32v-JVGKt8`O(36a*04V2f8{at`4v^22Ep)uhS@6U2XPW`a&eX!N?|hC73M zw#7`-Y~?LmLfdy8tCmUL`@AK>V=5yG#xY}K<6NNi8+FXL`9a_*c@>p z_(KKCIJ%znfH<(w05W*@Z|E8*99+A?ZDLTB&B!(z%GR%4*L(ham^c>c@OUsS*Pspm zuHt!=hW|@}KNU@gM;8R`L@TplNJ;bYIzj>7kdEwh)ew)jus>mM4-_Fd8Q>kvf!yk?PrkP$%w41Q)jxj zZ)sV*iZeu4tZUiauztOS-QfO&B`fFLRXClOGci5Wef|=e*R5ZN335WLLzOY*N`r@L zp_NaP6pd*0Tgpn@ju%D-=EaTrOUqUR>Udkn&TX43%U3U5R=loxQ)BZ6I3<+fI^h*_ zMVRLlhfvR4KilU9y>;u>F(}23p{lA{VB=FjhYa8pIrv4KwmtAjFO;4-g5_K*%5rkp zrsnm$z+ZCkyVvWy?7gtpD<0_qhv=4fRZXudb+W-m-OdW!0odkaJxw*ZP)F zV`i5WT$~&_cG9(?6uEL`5NY5_upeA1h~Rjwe|Fv4b?et} zK&r=%9c4~U6PjOOBLxbERE!p?G@R>Y?Rk1|P!YQbLT49r#w%g!d^u)JHXvY8 zuq1|{WLd27inn|Pp(2vPg@RlbS;Xq>04s*LLWj~(3@k>zAso2yJ<{NlB|D5+V>YS` zuf?VfL}`nHqY#DoY4F17;9Ho#;MhzK?AX3_J4e(OEWzH$ZkIBdv0{rZqJGG4dNx^z zNn2)=WF&}`f0t4*OFo7CscX>+yTuX$5rF>;E*-f<*pZ#O<7sBV;9<#BVxR9JKx-BY z4d5gOgcQ7ru(!|PH=%=9zS0rJ_9GyXt`G%k=asupv`At5Kq~GF@+b~LFbbAvj+AOV zvcf<5W07CV6LWlG=J?%RyZ5xTZ0zajK6>=#_ zjG;6S6S?))+W-!z-t)cp+;cBs)Azss!iM!55CMCGI9X%ouH98@D%;Prx1Mah*9=P! z9y|brT*J`W*~zTmh7HYoZ`sQlrUM6GW|d)bjKUAWgw|x8hJpjpxV4(Y+|T!(*PxUb zRS*@W8E2Iq(y*)J6udtr7h!sq(;Jdb+1w1vd-50oIpBvpjY^R%{+v(V!fGBJd)ql5 z)M0^`U-PvlPEoT_i^s;MZrQUJ_6PbeooGF=dGprpu1*-7n3}{8jvqVL(AcbVSI6!>d;9u&nO?a44o-U?ef70h?z!teCJNfyPjz&*(@IL8lSmG6 zp#`b3IVF7Og*6K@utWIZ6-we8Mk(uz`y|R`m9b1W5}`OgNrqVl9iSZzik z8psb-s!8*S$M2ejf3tHg^z{u74r5Z2Q;5KDgjS@YymC!d6$4|~No12OzFA%$ZF{M* zW4IMiJn=o3&HUXx-MC@oNJ03*J1t$ga^uELxH)8JDm*V5Ahq8BD^h!UdRQAobc^H4 z7<}Yo5Xn)%TuzausQwAB$glh?x~=*11qRqyzZBR`S}Q3oNQgA#2Z4%2T-d&zL^r3L z6D1T5_M%w07ZBjfm~|~&UNks3#-)4f)~{{v=qA>A&wJjrcI}4BHPzG(?o_rCgu~;U zG{(kGcb@p(i-(GqEoo|6%i-gM8H|Ln#0}+Xi9UK|e3M43lO~jvJ_v?>V6m})eOP#+ zD_8nCpR;LgQ%`r#;p3;Cdf$6DtZ%8Tsv$g^60f#{VHwhmSt=&AMvZ2S zRg()F(h^CBA0XmQh_2e&j*%-eyGV{?TE~rBwiYd0jxq3poFa3CBZX@kgp2}HLMH77$x~AA-HVfXMh!Po!l#Q^X$_9~u;wSu4m~%0>iwQ#| zHxDU5TUok_V~wy+g`%;FmnV4eXZkH|3APAbZ zz>7jHYXCNH+QKc@tkY1%c4ll65V2MkeNoJWvY{ze4}EX%d3FlZFr$vf zh9+nt)G&(hCpl@*mN@W6yxTaUw1 zNimnx4{$hpbxkb+8OurO;72?Iru2_oII#H_U;4pqx8HHO|1z3gwtOi~ER~|Rrmnoa z{Cb=&sop|nLAC1RCz51KepgpV5__rzBAB0RX>Q>z2t*WGNjxfH6&?bG3+~QD+H#}| z(>Y~;B26dn9GXAjECuw)`dj-{Dk7AQB5 zqnBWTQyM5JQ&%8$G$mX=?+NJGV9ziqYdEBv2#z&hj+=qYK^zE^uh1BggB|+0nHvjq9Etlv z8+s)sK)I5c**!}z74^u&k79z~{?4~nm97j~kbmyn`Ey=D8r4qm2owmmg8qsmDi28< z&o+*0oM1}Rel}lEww+{%f-Dj#Uwnw3KS<2-K}<>sV}KRznw$cnXGcexKaV7?pl86ztyh?L4F3hP; zOpI^Zyajn&xiYY2`?eQfc=0oz`|_qO+js2VOIX9E9Wt|qh!H#JnwrW!`dZsRe)=mQ zy5r9MENo{R^F8-Gz+uPu1?*WR8tsElYeA$Sj8Ju&@Mi1IJ;x5e%I_UJwjDcm;*)>( zY2?U_KnyC*-k7HG^Gc+mVrt^m(TNui9{c*gJr9)aJGS)?T;|fl?K}5ex_Du7YJ%~@ z(5^K!aTG(UFol)JqxNkwP#iV;-uoWt>}WrC{%l!A1tMU(%GHtK3w;;pzN>Bx$EX}o z)GvfjlYqsddaMr|xE@|12lHiTpF4MstC8W0oN$TrAWAn5fad7((C`R`y2ew`Y`xms+sDVe0u=yq7Tm>0 zfRXXUygz|M2I{2Izp}`sC(RjP%q;T`vM*k|I5;?@jet5>wLp|2DSf3};$47f3;EOA z*oB?6MV$nA$7 zOvZFACWixzul`~i(-rd5hOtZ&ldeO>tN@&u>Fw>Cb*GDB7jcHjH)M&jD2q1;!c)~o zFobCAGo9+`?rvzP>+9>|3<4BQ2!em()2(f+-;DvEvel*Bjl(VqB9*Uw{c9k7-}|28 z!~p`J?5U~V-rg&;3z93{`d>aZUZ|0)_=ES(z1+`L8kjqIgU8L2(xqaj5rvduj_ddC%vg%a9DNOpc*Iuoyt-byBI|us*Xh59RB=~l( zMY3>YDsI+$eBc8gSY2BB^f$hN%Vm5Q`RmEK>~mtmgQoO85G#$K@FNTmQPYyrq@s%9 z5stzszIt_3#&X#PUuc>G!LPe zaP?y0#H}RsXWj0@H>{D79U5k`U@LMQMuuVYz)p<+hZL|SV*n?e7}G#yMA~G4TILD} zzVmb0hG`KI;6e!-X0CIt9Wo?3TArVi#R{F06GhgD7Nu zQ$tgIE!C;0aM_4w6EKmj8PVn;9oi}e*Pw~QsM%(Y*^7!x3O$xa?l2gb#v|w(6lPZC@-@JL1}M~=j5ib=TAE{_ zp|v_Zq(V_R!HW7eUEx>)05l)eQ-}v_ge};E1$fP4LED&+FSg>`2XFc5OFvns$ssW- zR^4{neuQ(m{~~4T>+ARLzvIOhU;dLn{R?&}zwMz1ILV2jO?=4I^!4+7mk+*r^dJ8B zmjU5vQcX=gXM2b_P%{w>o1BjvK9s-2!A_$`LRf1xgOO}%bIJw=m_~pDhO>%_S3Lai z+ZlcAzkCtntEepJe&r*tAN`+y^*0mK(~rOHk?Pu-ynN16;a~ucKykmr>wOmo4!w5t zZ~pI5{J;~rjJ==EdW(xKBXTF&&USv#*j^)WDrbv5GU0wgc1CP*1cXXaY zYjt%s>zmhgws-yZ@BIGNv9bH^x~r_Rf{qIHc5-^Ar>F0w1FwJfU%yH5$KUaIadF}C z6UT|3ISPN<&OOVE3J<>g0xc6_4^`?cLL)hh0&d>2^N#)Z4D??nm9V&>v5p&**$ep} z{^KwI;m3b(-z|Gql$Ig}?3QJ0taW?tg+u@LogaV*A4luj!3nyRRotS}vS}ODul>~V zrG+KQ`okct<+>sb#706DVpMvs1-koGZ;Y_As*0XEv!RHBsR8ZJ={W5L&GSmpIK0hq zWmz{C82w#J3U!3GoLu@}S5?oCk6$%AZp_+|<(4i-yeX(()`kp~?WBgeQO-$g2r4Nm znx1A!nK+Y(FzvyCe#kE1U^C%|RmK31rW}$4DP2SB23&bPoM(xldYHm>Zf;pm)q!=k zxDvZC-NSKf)ED+W5w=n!kGW`CAUk}hoWT&KSoD_YK}EKaFLt+z`b{2+vJ%T z9qqf0$7i3=JR42e10|TKo`8({2xNE;1n3Y^EHH#jIPDGVbklFl%-nnT9afOJ7Nmcv zub0y=5G>kRv1%1o5$-}r8V1n}%llaPSgaiEqcP?{JRPr;vr{eNMS3cY1lN?X5@l&; zX9x2;42KZPB*o*XuM7_}S03s`bv~3<^1zAVA{q`tKsI2OW9qwguo1;$kDt%GTD_TW`JfzWeTHY?6(99?oGW z0+RIme&%H3g&1z2B?te`ox2!-gsMw}m&PW>*;dRc5PSFTqnp&#)m2uu+LlLaQ)0&U z@Fms-{9AW-&xPI#Hjebz9Z<00Ch$r(R8quRtT|Xr>PZNj00LKvW)&{K_paO2rz6wz zo~$s%)wB+-<9}5@nKc1t3@IKBk`5ZxXd2W~(xYu4QSNLrC7zX1xo0N)7 z9$J02T4TD)y)oK{O3IL1`{`4XHa|yan7#ZsRhu5?B~-Nnh-L_wmC*C* z#h5RVm@P5o1FX0Su+pEpTXAdeY#+0)6wZaoD9nfo z$0<#8tz$+i%5DzjE-EzxnG=^6$~NJ>1;f0+5c5)8F{!^C$wW4?g(d z_8mKV`+B*ah0TDS9jAr{FEg?TR6Tr-RfvpGLd1-qpo$NLV13EOyE4`ZUZ%rW6r;?Q z+|tw1(!xTf(`~K)>r?;4Kh8Vdv~eTm(b?7ho$vhM+N@jc?z;P~EnBv9_w+DD%c$Gw z)2){-^;K2Zu#^iY91>0LQduIiV0_^eSKgdF&b5ydoGG=i0Obs;^>lat>7V>L|DJf_ z9qZPvWt{c&>9%K{HR9oU^wCG@0-ic`l8$vlL)oEMUbuMiJWc^!aG0nKxXN>C49U1f zE;e(#`pQe4XWE$Gyf8J%1w^}d?L7SYQMz;=``GWUEGc^1V~^F=)J%+zGHCzu!PhBL zy0YZH2OcaeT*|pqtc*bg-+KDXo44(tMnE||QN*atfJ@mSy0DmJH=2(kWm1yG9~}Qk z!$}W=dWRA4h6Q1iicu@yrX9oUvha&wCG0XN#vP#!7X7eD!1h;s88m?48yP@{MP&=x zM=oz%Cx-DbIeNOhLM-VWGU0?C(gAyYfnD^>7c7NdYXJR2h~-OoW$O|13^7l^5ck#L zVQMASR?e5?b2pio78Z#7U;#%+4bcO*FExhoR4YxIXODpnm#*0({+I_Gn7#~sq*Dri z$9kqj-c`^Hn__)iSnyH@RA3$E7u;HHh3{KysEjj;Fm`o#kUPlpMxDUTVd6MJ<9?yO z^J7S#pTObzN&`fg)j1@9>Q8;?3IJvgmRb;~&^sy$8H0luh>S)BDv!G=zWL0*O-|0# zR8|)iFPBzPQYG{93Z2#UN2WD3$cUc_mp>p9UdY1t&@xb&!7s`*C=dfYUf8IcDM28` z0RbOrxG_7;?6b~))+1CHkBL{hDhBPaI$9G_=-5CM8`Uvsgb!f8q!vU0YB-7%R;S0u ziE^ki9M#1Mf7#iYqfc-wSd@0?!G}xOIS+YCD?i*tcvz$&&8Djw1&7X~7-`6F|DAWS zZHV(n7`{sThb~>dNHDr}%NFeFWLq2I$Enk&Ia`Tlmb!DxEqjg~KTf48D=TBsNPEZW zBpjexg++xtlPHu1zmY3{XxB?ul@c4#dt|Jox9=*;Cy+pIFQ6Ef9dw<-G9y z=VxZ7%FC-gEG7&bVrXaC-Jx(LOatOmu|O;r?AZBX0X|317-!$KSoWxm&v@~MYG)9A zNfG6UP#pHq!zgAsxAkYewKWTbh*=4Q3?NVpYT21Mmk{wips_z4wX$FX07Nh-ni<>X zmW@R7h@*7%Y6c)}d2C1v7^_A|);_Nam|VZ1nRpk`aiBjCXgg?v17R>Ell+U3ct7ix44=*`41h|-{#C4z5fJK^1X{p<(4FH@9H9bJeT{0aggYUztZMao(KuOon5TNW)ggc8hSVZ@sb?IUNF_EWQapl1qqGHB@$#f_|-wgUuYXPhNX+)KzWb%go zlVjPA1nAnh%PC|mNH7ADjuE=yc9eLb!hB(22?IbF=-v0+jjeQ?X~){W`K|BpPr5xyVA;BL>zZ3yFlg#FlLA%MwM?(j zMP!PWD2;JuD~CquLE%9J=u_((aiv_)kmTa34jlLa7J^P!mzK{JE+Dhb+qd9FFI~J$ z*WsCGp5Q}#>u<(hqV6ke`x^;NTwzgIRo!Yua z(6PGx#Id8)7HSbfwvK^}Ps)VNF_zKMeiC0sw8&How>fbn6`P-T@7dXQ@)Rzhe_-%) zpZokxL=_cfjg3u>jg5%o=&_>|XQ9~T{sGi?@TKP&S}=4-p--p_ju0$*1-Yi6y5$iS z-=Ig(XhiF|ZL5%*D8#cS3*+hmhem@|l&9w>hq6ni-#*!~@V6Ac3gH z0-%wCFbe9YRFFeT*Vxb?;f@M&cUmG(8h}O(v``j{8A#_sx^QVh6D49p4AZ^n~3>30d)zH|Ch!kt83{r6*LB9U3y*0xIMJ9<`zCQxr7J z@)7~GnvyJJ#33(yF{j`%GUR(oiA0-U96L=CnIduxtWxd+AMu~G1U~?Rn=EdBI(7PF zZEf9|GiT17JI~4<0;!IU4q5>QDi})X>FJ{CaZoV3p*h^2qg;xMN{${qvVJ`Wmeye> z)Cpq$GpA1v4Gq!iFIF3LB7uvjC5E#PSq{ar$Q30eoN>>Z1eSHPACPeB!qQ9R;HHFF zz|=D_K8~wlKALPaw8xGeVYdWqf`Fs90Pb$4=bTK9YxD>n$9h@4v6POuTOr>BRpYQ)Rw z%1m6q+A{9bi!-#B6&5m(cSNqc=7M)P zL#VQ}C})ZQ#@{>yCfbC1(-=W*Y;3xC`4STX5G~|vuIjW(T9@@!(CiL zVjy{wc+0%y0Ygr{2QRT`EY|a)Tzfn$MvyQG06ToKTkRT=gcAfA^Lh&$e_)`>kGtoV z5oOc$4|#wadf-(e+{~YP*rY}!^RLt+1djwnND%-3({>+FdS2&!;Aa4E2E7e>A<+mD zg%h`#<-aLD#NNvhM)j_kQnt?{lAipWCUsIJfXAh6YCdMJTcT zM5tl350A?06{_Kb2Y5x{>UE-Q8DQc$ktxYr7DuTJ+s22*(Vstg>bQ2io@Z1)@b3G) zb0UqUg?xkya|GtqI_xre6ULSZWC+91btccMN-~(z@MERcTV-v6yV#Us<8#BGti+6J zYV}I3F?8&h&fW=w-Q#Tpx$2J9rXra-V8x$~Yk!zvDwv4J3cgL7H_PtSdLiAZQ&u== zri@iE!sB_H|DdcL9Yl1LWYXj*rS#|cCUcu~VvCdRxO2^>jhp!?dpFQkO$RtETyJhRv{@uGsDr%CBVc||fd7pIhqJuB=QUIdIO%N7ks+BM4afvQP ztLiBU<;=uUqYbZcRjtwnz}pnf>C_;vw77~htl2}Mm=1hR-#ki4Aa%gWVpa;olQVuD zZsNAo{4z$3OM$TzW?JVI-eHu+?(kolz`4Q2x^cnWVbhG$7O{pA!v@ok>prjGm@EWL zi#c}ec(hbOH@IZ84Pog(#8?<#gE1;GK(2r~Uf-5!bs7@~v zDE6Vcv{YVF0oo!HGmg}e*N0Y_u)-V$7|24~!_4qF7VI7yu_8ayM24)YA}XLzA5j~> zK+k=I^TyNw>8$%uJc(JQDdtYT;tr4HXB%=iW zU{eAGHp7EN>}?owgG<@tg}%CRd(v|7GsrZV;?&@v4q)sHcCjqge1o5%n5|p4ydlsT zKi-(yYp=bcN6oMdRyaBW)vVr-t6cl{zfCI~Cl4P(S{pZ6RQRf48}7@Kx8B-7w>9}7 zTvyn^#Nk}L%2Pb&i(Ywo9ltrA>P(pM>Z|KnIp=fk+`v;$KMp_@w+Y6MG*Gph!=RU1 zO6Mimke#2A*x;af+|j3?1C5x=Khebob@ez9PVB)=sf(f)O>4v@V+@mJF-P&AVz%6V zk%J%54RZ*Sq{j7*JGg&8`y~^GaP0U|+O>CYKNh{Y;SJodLhyy>pJlfuw^DBlcg%L|lZ zWF&c+L{N<~8D>kLE3HhJB#GigJB%7YPb5j*!F=q+XoOuPRxB;Do1t79BFw|Q%~iby z;XhjV72|yh>fkMjd+(-B>2&|5l6I7A8Xv7LY;BR)aN3Y+CNphiL2&$<`YZDGp-N*- zNFtS8yLYL&;sKOr7Li_xF+Gl&3Oek9cp{j%al)d1J9czE_rkh9TXNR`L6Ti|Aw)qM zAK&?jN0@}c2xIN)Nd+N=2}xT%A+o?giuI+x`Mdu9eoEY`oXddamVw!R^WlddzWEl` zI7tVuPY+QP*Mkfj${#*tSEq0dlIpE+82zz!XbT)otuSxyX4@srvJ^#&hY{8V-6oi( z8lGN$Q*fjU^N zb=MuV>B!-uW5zPt=FFPCbLWo5ix;Uc1w$D*r=Z?4P-!iXLd6Y`&^S>QfvA9zQaqNskU=JRhkpq&LaZ*9!84~&q!Kf~Mq3I++qdtO z2nRp_^YEdA`ih}`%c4b=aOy?W4ME}X61KPma)rYw1o|0Xj+vz-F!WdpJ^5ow!ymkJ z?5OpFES!KQ_e@5JB8(Sg&``c$+GT2Js^_dih{sEF&CWe%26B2}6eb0sl(NaNWXUq9 z*q5I;c`kC07opKn>#7=?X;Tu1sEi+G?$jB*m|AxM=L%;M^30#VKo4R$`4W9BJjLNv z`;=ZvI1@4J+qiM_^DnNSIipYbUL-egozF6YQ6_19?BgF}vfA;}rcX1Z0%|l}r;h2E z$ris&BTO6ri_d*t=*HECx+^cxxJ4t9ANas`EEE$>oWKL9uGl_Ng^-F%!uTju=5(%m z>1F8kO`k@BR|3l~UhbV^$M3vz&B9w2XS_GJDoc z&2h+FMX!`5wH@78zWlXv%X*%Y@bU{j$bskk9(ri{)M-LG`oqbD>QIu3 zBZWjfnLd2@@S!jM%~zbYRZjG3aJWIS9lG++!|$6l+Yw)50iHriZ<={fnHnb}7;n zW=ex(yhyCJ@|2m!b%}E}ZQ4i*kxCKbJ@ia_w=aJ*p>a96A`uJ+0aE zrUQbHz2~0UMt(6)go+_xfRd`m0+Exav@LNU+z%arA3=n#(Z+Bheovqf5hZlVY4NZD zr%)NpB+E?hwhXEo;T#$S`Cg;IX>kSF=$#^`^8R9r#B(F`sxwL)b&@?>}DI0UmP=-+Q{T^DUDbc=c5rF-HQ^ZH5_nC^P*uE>^hgC zJSTn~U1o0TTEi42o*k1h=1}6FL||s*ag@fZE(#;61&#Esz8Yine(UIxMZr3RxK|)h zlft@1-*1~PZObG?mWL$fSTeygLbI(soIP`ml@hI)k zs+DW@?Ad+l^ofZRrrf;f<_lXa1v*+Cc+nZF2twb zE(*hJVwfm|68i~fW5n>gIC#!l$d#+^c3*{_gk}dn zIJW1+iDPT-ynFn(i5oV&7JpYrMhViU@Lg4g*dp>&NIs)1xz&G`)p_&g-?D7^il?9Y zrf1Xprs1CP1xs^Ma|;b^AYaowRjaF4-znps-3~W3P^+jA63c1)sv?vbMzIUAOiZ`l zDGdwFd=fUEwY8m?EKL_8fDb#j%0}Z6YQZ1wJVQP+NCb`17lsUy>d(VofpROtTT=Ff zImE$5`4Y<(Vhy{7NhBwW^iYG9UjqXdPC6p68f`(g9HlfU)yJ%k3BExkBn_){V0eO{ z6vx)}0G>(U66@h0y}$Bm0F3d7j4eJx<>=!l16-sK^2+ToP41;=u|I8M`eVkGIcvYYl4aarv)SO*5C}4={Orb3HC9FL;mg(dHS`2M3@;uqbL3OgqFE{a6SLrX1~`KlNP`{<2rO&B$#i#>kibX=xK+f%O`2-VQBg#%Dy!_H z#2+2f!jly4vwaVP$%Ak@DjE~W`|#lgB92`PP2#v@quQ7mP%<_hO6dW|9c8*jfI}Nr z4RV3656Fa#H5yA#DmOJieNmMRnM`RDNM))66)=A6_=v17sfb46luSY6ZSn&{YMw0D>QQ>`RBloO4M`BPP(#%a;9*IW9f5kSXkauUB|*wdN*4lLiDVHU zolAisDdl(@0kzZ}S}RfKUGhRG_-aCHK>Cqpd`(8NyCJuu7gL~48{=KfpEkM0Y}%YJ z-0VfUB~!&?P{NjCX(8|a@+)|$Lrs#$Yn~`3srbqgDDjcCnanvZJ!F(PQwN3d<^(6M zl1Mi{FVr|GSqKFLpF(z+Kf0<3h;S6>g< zicllsLlV*LbZSJ3&;eZVRg51r%?AneM5^3Md-1$Vv#J_G2$={jU#BC?YK%MyOJo|= zojq?Zm>x4QZgQBCSHss80oKSX6WDN2)?^$yuTE_w%N33UszX!~;?%{nU+C&hWamxU zkIV@?p_)#9xnUHx1}&0It3|vypw=u{$Q-)tfRLE6lS%vCokjr@M#i6&~-eb2FUBHxBX9yhPa--@Z zxrEio%j#RtYUILq1e~irLlSZNnd&(wqv`$Fxm_d7PeXM}mMz|~tt&HQ%G1$cIzWg5 z5rLha;pB|(3G&4}MOR@ph86P2HgBSo{0p#EA$4!xx@G@9o%uO)P3On5?xj0+?AGA~ zc##)fGgD{R&K)&Go3R_u!?TnW0!qol!}@`6od>ziORXpc+0y3R*^O_#Y3!fcpnF!< zDvs*k-47*N9f>3gWo#!E#4czQ;8Dj|aQ#1`d;-`{U5fgo94hV5}cp1ZN6*^*C96>4Dy2ICEZOp?2){o!D45XVpeSsP_w2>=Iy zhF=siNQtwt5x+Q7%(2(#w5e0{Epd_IjSO<-{MWaVAbB7OPFNSzD@{Hkxm>Co8faNS z6_x(rg3?}&d?p`+kH2%&Oo_8+PMtVmuBSLgaBsquH*t4`U?Lbh=9^`2G}*%$VRT^{)?bLiiF%d?}5W-#!E0Pgy*b(Zy$=a$7gKh?Y zLrAd~?ST}j02v2${8&yphpzi;p-G0lF2mNGgHdvuEGQcbikV?*rkjk*D1R7CsnV>) z=M<4r+@)HQrHW2}K-!bLckj;Zj`RvqkVMtS{YBdlOj?x2o@rBB>t(4?OE2`HogQ^M zRHP<~MNpkrUVeqi&`RlD!B}%E6N00tk~W+@;iL`Q96o$FLsz1T8;IB|oIG^!fYmMf zoD7Xonv4?(<-LeZ0v8%Ex31K@8R)55JkGl-E?9Kw`xA#A?78VgcGSyihQ#wRmNI1X zl;hP$o4B)l=~5d|i8ZTrfrOABg>4+fpOT-45_s5op26<$CtPnA(6M7b!}?OA&KodK zojJXo-|4sTkk=h!CdSgmi>6Li@rJ0-kV1>dPNv}}BR_K!Tx2L5>P4rRl$}?>2DE%2 zS;cRn=jLr&%`26_du=mvXUHVh_{k~h0?^q{0Z6f^4P+ibdMvWvp$bfiH_})IVQo6; z2iQ`$3Y&PJgp6)8-+H3PWUin{3~F;yMFy;`$Gm0X!f8{dLMaLsmWK95NpXQ@CS$Rw z7@Yfn4%MN#h_0mVXT2&ol_q!HUB;1i@9B5?MWFE>#%~Hqv4mz+18HXi6%si+{Z)Td z1^XEs6|U$A5swk2;+ifMQt7bN5x{CYm^FNYhxShQI7dEb!F$ZV(!({^mB!zBm}QEU z)BF_mUI<5)`tC_AAhob={rjs}((|AZWgv4}x;Bt&D z7ulP#_r{yA>1c=nSKJEn#POLH5VsyK2k#2@?ZZBLpbmOdu%6msh6kD9tC(OWT~AL& zuZ-w_CY#g&{LSx07{$?S3u38|7r5X z01_)>F@y{l*`rS}_y8kM4T;{;l@UX1PUxTb-gN%)f-qH42uvZ-+F&4_Q3iR$XQm`S z6pvn6rHZN~vhUezV&+9# z$l#~R#=#{b50R(~KZ05Qn>)5?v(21Nwkg5s5w*&%w|APA=Y<~sVG98FLR8wPt!&uW zTsIH2CPM|H3Ufe7N^JynIQ44FPneKeQ9%+=it@9Pfn%jx%t3Eu)NoMs>>z1oh*((F zC1Llt2@`F=+LSH$=4E2RdXgVJx9mRn3j!Dzh<8hm#SLs0gzk$uZagD5lXPWHOq{19 zBNcok5Rq%S5mx$Kk5gN)(HNOsb9s(q2+@?clOcdvK1_|Z;tY_E5Pq9fcoJx3>?JzRVt1<({Tkoy~r$lW~Mj;NSllojy%N)Wp zG=`$VEHKL9-$Bn4uj2k)W;%|E;_^tfdKX&5eu_vE-(vbt~xeOuFH#wCI@$ouOZ82ang-Zg5jGWoj7%(kvMC8`uFUaI&UkU-|Vw=6rD<=E7VG%@i3%0 zYV^#$J~Qgxc;l@W?{w&Ia)_gl>c5mHSiw|gImm3ZU%z%kYq^V+?sw|3iF+$`T0K$L)%M78P3ZsmLPY*Gay+R~b$U)FF zQCjL`e$p?Cqr7GghHR(>A`vbZS<(SJ3zTX~lE0Ecn1IU3mP=BH82oiEEGgsn9V94# zaskeGWL2V`^VVSl%0puK$Dbx!VxP<2<|ULhbJULuXDO*Zf$tXeoTIN+7K?7FZY@6pzNUE2TzE zN?8);;00sCP^blVb2M>kqw0w3YS=jlHuAXGH)Hzp?&Ujn?4&WR?V^S#e}^u5rRxR$ z-~d`(Rt!oj3WIFN=|s1U!HOqwKkMGB?45`i1;RZjWkdt_)5N=K5n_>g@|#)v&bQPE1TBr~cispdN_Bwl@eme_dEeW6#1&S9raU=YSQhyu8@TDU<^`Z?K} zxY1&FzJjoC8wcr#vMSAJYy5=v!0;6rmp+>tgu4bh1_pIq0IS(xqk0xphin9-vZDZi zrt2EFWrR%h!EMURM+u(hoCN-+856`O6cids}l52!fR^up+5mo>pzb6_ zjQBADQ1}c!9HalLq3VypUqC9FycSs4Ba1jJPzoMDXTZQMG>{HRby*ffLtQ}YH}bZ0 zSkzINuysliRGKE}^s>DZFWNfxDosj2dlHpxv>SN(37U*9@1rx3T7Cw-kQ5;p+-*;* ztc;Sl@h`6vWZ@PhC?}-z(1daIeSG%~u2v#_gMdu&ABBv(1-!u1m`p-2MLo+uZvZK1 zps}(;7c}M=@KrBDM{EMiZ9#V<*~yz(8rd-ffI#4Fs#gKz3J{vPm#2-}tA3X-Qz&3dm)@H! zjr{wsKYskAlUnLfrvE~2K;tTo))N|V`qatq{=|n(l8ibAQ}R{=DzCca163K1BUI!UB_i)P}aE+&OtH3SMM0M zbkJ_d*Ju2ET1C04GEtj2bGmDcTB53w)IhoQU;i0EHqCC6^%t4=UUva0i$S->ZsoIg-o4#&LENVr+M>K0$B@cht+JOoOzVB z))GOj6!4>_fQgbl%q zC|RAiJO=!*Bzy_iA~PeT0{kpOOt6ug%nMA4P!;AVgDsykC7_TD;@D_Z)Sznct&}B& zq|QE%UFS?YQK?`20}c>Otk7`+Sciw0XE+K1ET7>@h8;0#`>>on zJ3h`%xmRCkjDLuC2x+*I;a_VPOfKu_3C+;x!fkn_L!HK{pCdPDuL?3Fl;V6U**CyYMu{tr57WYfltFTb*07f~DoMM}U2 zWN?oRt19u^KawhFF|Hi57Su~2lwH<@6`6)tB!|h00*Wd%qlk9_L)s8LddS&F7tfq^ z#^nv&vV?M!JU|wpW`RP0wj!#4Vpj&LjCGQ6dNmuhSMw2;ZFrGc2PAl<2}vyrtUL{V zT|*Z0Z`rnEM&G4vU59}o*;Tp+iQ?_>E8oKm$&IU$)hFPJ>9DKbNzDHbPPNQ%vtDEq z2J~ZX+O)ZM>`fCVjHOkbRQXEI0zPt|-wj^Wt4a%bxvs*YldxXpL#m$Xc6Qh|b9cWP zx?GFh3?6*&A$w(Q+qP}}%j?FrEitN%L|Yqlu@~jCcQF-Gkjl%{mw~hAd&ZH=<*0Vb zk%n+}*@n^X61S0KF+9`AV?$}4r`aD$;@#rpq9ANvuHF5H?T#2T4F-*!BQ#ubGxDyH zf5p4{F~X2*U!`hrTHBUw+g7Z&J#GHb5*sJmXaZG;Q&PL7H`otVl!Gws$);nRD@R7@ zAeBr*olS+ZvSl<|+ms)J=R19>2tR+}G;LQ#bnt@sM)r;%OJ~v_UIbnGfOXlAlD@?KSucr7`pTC};RVRSDq2v?0^9((8bfBMgV_47ab6A!)bzCZepf95c?#_Hi5 zkw}^b_*(Rp*{z5MEWn+4pmaM8w%Z`@_q z@~jYkMMv?H#Y?Puwdhwj&t2d9Jr>PA``oj$XU$r*YL$-MmtK1DzIVOLxXQY9Yv;{d zux#lv%R^p&{dKXM;!=7>++3wSXM<4sfM?UgVGXU1FU%z(!+Pm(&>;C)h*XQ)q(@(;fV~F*o-c3PK_53P* z4SVTI>Px@X>(l!B9)9>c1mzA0TQG0_)@@twTyrPs6OB!_*4%No#omr1_L!Lnl`;Jx{aJx#cHusKP8!Y8>R(=GIsHS-kPrNkAC?5tftYM zx7WL9i!=rzrc9m+e%ch0DKW8Du?`^4;mnzP?z#icuxdqJ515|IOh?Nkfr*n$X-em^ zL8Woy3sO;p18}`Kv~2O>DOsfQ?a)JNW*rzL96!;_1H&buuj1F}PShq?;K2O3b3XFH zhkG2s6j9yq>ZP^4y#~t|rtSx{$Xl&S-riX(*^!l2q>z$smElJ)4x8g11Dpr8*W%0> zGuN$KzvhlR-gxs3xis527Py-EUO>V|2r8wbnuO_ogX4vz0F5UNS4a!4Gd|O# zTK^F&B#Y-EEzt6<`RCNzF})AJmo7;sDXWaew z-SusRn=FZ9FPd4UDU}g16K!J)t}u|r8a2}@wvf;){xq)?P!Ll}ZEopP9B06*wH|Zw zdqo%$2=cr_aC#ygNpkMIHelk6j^`gV>fPWJBH=EE3$D&x$o5XcYm-e4@yi&~rpg&+ z%Bc_ZR-;9F5Ik%sHRNETIJkQiXuTFlwW*v<*ScURBSK_EA+Nzhs*e4eqI=FMjW!1C z>wEEq7u2UZEHtYU_hi3AN576&o3kh@{!BWws?c86fgskuIM_F3^0$$l!bD@by4aE- z?Qjeq;1SOy2u?fGadouGrcep7&(h~DaUDH|iQGiTE~NSb46r36r#2#{eI!3e0tE{ZEPu2O47W$rL4^+(+rZ?UQ> zXaOf)XWrSP`Uv1G2bQZ%1{bq`L` zzp-vw)L(C z{p0;X%D~f@$Dero=wa*Qdi(ljyle4&2&C3ZQ`y^Fq4i_5w*3{5>j&w~o;Cfi|K{)h zn;-ol{k`kfzrJM2tyJXNl%C)RJxOJq0nc#MO+0M^;)`}RH%3s)aW9zIp))}J>r^Qu z^B@v98X4t=s5fugY?RvJ1L82@!s11Xq~WQBAlkt=CN5FcA3Ahs`SM%s|5je(CPfPg zdfW{=5ZSwr*CPM-MB&LRJcoiufPF zGm9`g{-159tB*r}{nghTxF)o6VrKMV>NFiXd-m=zzkc)P&1Uwe_N}X_85zO8faGZ` z8QJMhba@%*{>4s^mw2d4tKh|#UR=0fzMB)LPjZjvPM=+U$AgZ5<+}p~(%Azg0aoxM zl^PJ)yJycg9{qYOuj*z6@F0491f4MF2-FY}9|LJ6SK&NF0LEXZMcZ5j*o63{FaC|h zu$9YO0J+rr-}N4wdZg>cqw*@=#!!Pw-S|utfFeVGH*MXrM*tUu%$AXxx2`e%gxV$)}%SHGT$anAVK0oGB|zLztSR zfsSwTip1?Q`mTHLjXX27k%JoVyYEA{-L`^9#XHHOEr69jEyN=lr__D%i+_!`9iEly zs~11;kS?*AD$b${8qwPZBx3IT0MK&^HfHmtjn6+j5E0*qhb`0SUUNXTqqr%NCCo`S zMHDMjE7w@Oj^}Tlt!8{MzCsX}|4}hV^Ihtv63l=P%JX!at@*F|CZ`sn>y<+OUyLJ1%`SYgl+*L572IiKrXmJ$;z;TbC9+=arfgf{DG-27z@ilkcwQ|KuBRRwfwXaO?n@%gMLPEUtt>KJ+ z!LjBzLcX|n#Rnxft9j$*y*9TNW>}+q=beiIZjC<6A<%y4Fq+P&tLHCRXv6@Y8|Oy= zDJA&OmPDWB%CdsW*=t7BEHcY{$Ex-dm8e3bkTN+4EJ~baq3?{^IF3(rWv%i+I_z2z zB&hWr_OxEyCkYHATyB!+XRNM*N*g?m_5^OTbA@IHRaZt*WJ+xr-+n9BoQQ z1$TK;owNoX#^_gIL$HEc93mrUo;K`?+Y0kP{6}-znqBBE7^XA_yYPLt&q_-yveLx6O zxyaaOU_wsg=9zCjFWrsecS$umX3Uy1ciz0ChYwBKwcBjsIKz-oaJsv7ixw?L{G*RP`rNmkzvte&Ayb9M1XJpjNNWwOe|h6i{=|=dAp}%ME-sQLA+{x|Rw#}S5%k*;$2hbG9kb&axE@&hei7$au z4j-ne=KHC( zvC+V#UJQ}Fg0QV)@QMwu2`)nnJo3Rq2Xw-LbLjAacinsc^y$<0?%ksl-3GecY$cD{ z>3!o=Ih=?owboA`Ief%6&3fjZctRK2vitA9@AwI4idjdA&7lbes8Ki33Y3ah@>RMN zQAYzuoj-e)6Bn4i^6Gk&XpYiVyUy6+%Nl;%n>TOnv(G-GHOA8=Y5^XLlHf}5vLKjyqP z69)Li)Fp}xql(_jAQZ3~sNzLZWoo~}c3(cG$DR@d%9{nw0mqa7m z>Uenmg%?pYq0T|O8dg{pXo9L7HgM`B4(ohseNG7M2y{3b^ftWt=HuUdeA@IGtY-M} zrEk#aua&aq>VkQM*{06+jqL_t(KB{Yo!-<7HPshO&* zu%l`RuflDNI&?Tkg?!H^zuOWjjfcLzY@Rf>cU*t}9(yut;n2d+L0j;vBlHNFXt-bP zI)$hNL5tjLC7`{|hc?93U|3WoSKhgTsXN%Xt-49;bmxxkPdxFYEzGVU;)6aY>CB+Z4(vxcB@*Kj>`qX=u#?^rXWy?t~4Smtt zH+D98swq&N5!jKuZ1asbcK-a&{V(aTy)mixA zFX|Pq1?5%^Cz9Ly_Mz{1|GV_ya?YRs!k3Zzt^E>zMaUA@lGH+Js41vJ(PHofDmoU3+S`5o<8M!`#zJ%;(>7MEhG z#ZksvLcCU#9$PN*`s=%XB7=?6~FB&-UdzqyJ{6-(_uU0SQwqi z%n6rvh_Q)&s>GLd9chSZr>DO7FDxg zQ+S9JOJ>P2d0X1n?3Bk}xBeyd>ye{}ZDPj12rzYc9yxlnq6ZE%WWyjt4JGn3B10{T zipHFfbj>xCJxPMyneJ0Uw$q7DW5n{Mo-U{1hlnI`NkUru?r`QxV4h|)GbfGneP;dG z`XR0@J6UAdmEUr(vB3yBt`q2MfB)4FfB2(YH*fy=pZmFA_@#fU+SVMbK(QoKuyLcl z2_o|nOi;P~_FIkqeE+8&VK%S6wrTpbN#b{JN0ME_3B*pl!g>iIogwK&iW;XES|^ZM zEcPV@YIaqvb!5E1 zcEFG>L?#+2x(44&D0w26=wW%ru_U!`}dxXT8cJ-us zRqs*JSQ*`8Y3271z0+fAU6JVFjjq$DPNW6d&5mk=sH!$NF61g#n-M$FYe*!NH8&pd zUUR`~lpgW)Ys#w4sGAw3(p@lU;#f8_Owm$)%~RXw(8S_M;+U(i@JhIR(T-8LD&Lra zQ<-uQ0hXV36FtGCbP!UHo?Zu};?wn@c~SODZ%?10L8H!INTe8E)w*e*ILR3^`zX6- z`cuMS3RxObEC#uSKGD+Ty3qCLHy<;0W7B49b>X5OV1s&C#r;K2OqbHY(5r|oa$#-y zw#QIl$;t{sNZCU7>uBCjgKhGM{) z8DfQ3?Q9#-x65-bs;le7(xr>U93HM-b=%>?M`q6Kd*qQ1edEzz)sJ=Wy?5Ml%YxT8 z?4mEX+&mv+CA)3;Qr4}v?4`A@e&|CF{mGyG?X{s~N)45<_l7)%Tiv0H{nvl<5B}x< z^b7BO&%FYykALig2M*XuAfuCS?>qb}|Lhm|f3eZ3Rm-`Tg$w50eb*|Rf38}&T!8g` zpZdg;Pkl>!`IF!M@xTB2vp@6GKO>Oh{T400`L}-Sv%7ZhH8vVmuJc}0zt29S_hGDN zwm`7qKqpNzH&BXWm@P5I;>599_1-&fya9(gxINm`Vbhgglt*Q!ouwb%f9H4q_}~05zlvVL<^S<(zxOZy$6vtt$DjB&_uO-jp}^|`jIqX3 z33}p@Q>@=GXeh*+#GFJ;f`IcKDH*v=LQlEU8m2R4+4QHgdIOZglQ_Pe2eHVyd`7nI zqOZb|vpRiJS#%9B1MDs;X@KwYzoo%IbP5V?@aZ~jtA3r04#SGtXLg)4*qS=R9-g+w z*5ju^UuSnzK?&c`1wXA$Gcppxa`z&bbZ@7`2oHG)^oGp;ig6Kh!ni>$rf>QTk%oJy zp+N_GLgXZ`@G9l=5vnu`OdN+knO~@9o?mkzZfMA;kp3`nt8Y`A!{XEirCq74qTa9=rWL%FtZtIEwG)Jo$)eG4-n)j5F0D$#RaKH={hg*%rj5Ewc)K_ z`X7E-SE|$f;@Qv!P+ckd($%fXT(M%+Z~W#TPM>b;{|QHq9Gf!Pp7I>GL?bDO^HxT9 zhLC7(?12&&Jx07Q03oa)7jcF$EY!vS*1@;>!x?#|G<(Zi?y(gs_Z-PHa^FNr~Hfp6URBD1`($R`gno~Cz7F&IPmn`CXPt;cJ5VY zP-F^r_nOgbbT}EpQ=;_JELpNd_Y&orKW`pA-tguI3$A6XickYRw`odH9?Ql$%p>q9 z62dd?AGD+#+P;1J#e&$0T{IJe5aW&RxZ=F2Ohc45@YMe_~kXb64S+*Pf_{TnO zaL}MtU*Akd5z&-lC@O^#WZXp9nwTpplX#piG9cVAvBT_9l~ev@ljuQP)f>{Xrh&My zaoLik>Nz2y%9xIXajDEA%u*RQ>a1Lz`%H@?$C6UkrKz`~*(t%~~dy=sWMrBMj zYr>RmS|(-gn#ACpJ9CQJ7%)hSL{4$)kX6i+I8d^a>qgMBLY)BYR6A*s+t@|03kbRb z4#qGvf&a0i?@XJLm^%W!_9{x@!Z8)R3F9W()0{?Nb;7s+4N;*?GB5+VH8I+t&VEWq|u(>Jlt?4B@qn`kpnLq<`%4B zHb<_*AWG6nZZK%Heon`=VoPYg9^D1LtJk?N=AG3x4VofFY{;cmn6EuA+Tu(&3ehn% zQv7wH1TXSKt1dzy5FeLnk0nq&wHF)YWwO&^vQyn})zVzjuj;2+jP;t3~7@T1v)err|+$H)b^(q)ug0 zjeZK1*>!cHK7-G}D4}Q@ubP{_pnki+kCyVaE@B=DRWEcYZfyzWe@rKK=dQ zrSI)e|L?#0*Z=b`FPJ~?D_?!=OJ8~Jfp=%>`U@|tGl>9CF5`LN73P=FPtC!3bF|#s z@P#1|s=rYeyD#Vipe434poPpLwVON{#K=}7sG*boVEp20zbv-X!09H_##ZrRq?*T+@OpTFS1!2>g9%rG{zW!pAY@b3P89UFoGo-Sg| zz;l8U8`zFA-sF{E_JrzgIgLOeFXB_nSh5y@FWrS2U;aHm?MyN|7V6 zglh#ckzLVMLQD%(X`xfM^O!Nlnu*qUeeh`4C?@7-p7?)0lVnXQie9E+3aE`>{g)+p z2Q<297uo34>v5=#nGKcN%gU7aBJ|Vbl%08I8n&j%i~BW9EO-XQ^AR)^!aP&O@(l7+ z5h(~t=7JdEDqRR2v8g~E8tv3X9g4v-53YOFW9}tb(INh;J3F~I61;4D;cLh%s~v-Z z(&B}x@GF2;SxMmHpnwGm2vIJ5vPsMUzxn|)vta1uDe$0SL@%s?e4qpVTc5#{2)f3ZO6|D_e=x$mU-AR$q$<9HrPKiw`JPn4KdBz2&N!T^i(>;2? zd8su6RWgc1?a430U+2$7Q=>)HJ08`2SY4FlKD|p!>*B!q^8+gSUQB72gm4I55ny$g zQOId>-jt<_a80a~GKw+8Aj~BA>K+dzbXVnVM94i^g|KSPs!x!RUZI}RMxIEeL|F~z zbakXUj?0KmBzSZc-eAVo5t#&;;LQ88!>CVuN)S(u_(zT$`Q{Uks@x3&nIwyHUiZ+! zLvGAzGp1~s^PT~b!4B;(%|4sEZrHGaf%qu~;&HUnEQfsVx#yv*-4Fkb8{d*$Yuj7u zM)dJYDVsZlR5%GW_>r1>Vf-tvyh29umeyE?)3bT=rZ&<{-f999EP&-@jLZD(7yp_T zXlI)sW%$6Pb>o$KhGMnxzj8!LT#9MMVUam7qDj0LG(Z(U^a1SLzD<0Ltt05|7QBhE-Z6+O3BW2ri)9%}A z*S<)#Oz2tn@>dH znf#Z+|5R#uqro$1_x5u~jvja5&SW!L9c;`rylS${Cme%Z0Lg_jO%0VEEE0~U0po25 z&H0!xhpNN8lv`ke*LAP(zjO&gfg1_L6AUM|L2*1CZ1p*%1s24HUtayKg_m(^S6C-m z^dtjMf>%0k=AtK9>!A=m=#BX{s|b0t*1m_7m<+4YmwR6+qL>UH+@|`|r%hwbMNVOG z?MLHyI5dn}!l4E%0Nu8aVkE#gEp67LdeRsbM$3_RQC-!*aN-iW@qvOL91Eh4)*lIT zUDS<$vaaH~;&TN?Wl&K46^mktariY15-~+1nst^4y7`=`Q>R29I&ccul^@>J60L?N zu@Z2QkBWv0LidoG1S#GB-v(3z%=5V^26RY-bh##oh#cHzed~xIq#VJ>u;A)dxBqYd z>%Y>A@XD)i96Hqho(CTI)^l%s`uo2J27mj-r|!OcS^w@Xhg!C|76%R-(n`^@^=JR- zYb#bPK|Zj<055?pmZ#`2D8rFXV>_^X`4SGw@D}a+#b5qay;<+Ncg^EZtY5O^)~|j2 zi68#V_b&Lzytg)P(Xe^&z4w_mw|3oY-}}jrKlSwUhOeJ}_C@zZv3A|7U;Ww>zw*z1 z_V<4G*B*cT*=0)?|G=j|^^M1#x2i9^3ofzcXhPb+^eS3W^aeezJ0gpgwvN(x0qrFX z)S;n<0@W8-2zCqBF+Pbn)?u2S4?)e%u?8>cfIyK%&nHRKKc1+(NkG<-|pLY5Ch-)p8KHj$A9|eRVx=( zjMV@&<~Phx49`Y))zEQ~;fUhPrE=p8`9ekjG%d1_uq(rS8Rv=9guL`>D0 z0K@Oh2x!^f~=lG!^i;I=){DGlAy!^_kVNe{X+A$8|qOz~SNWKtP$_HAuGy zsr#BR-CliEq@XNt4GIR;4|p;u7;IG?U&GCqoIreKfDtDupdR6y6DR8kxXGkEw!d zG;}Zr+r#`?pkO1=Ag{N{6M0F7U(=4kTR-C-f=*Tyxq?cHOu4?)onyz{M0-3)9V@Bv zI{SqT^`;~WL%+MbXU-fKb#uJ2lY_e^f-AGLgbf-YP@1>^%2zyBD3X+uvO&!a3shK2 z&(AE=Q;+o0A%Ej?s(chC=|aGuUFpNj=29R)D?(Q)s1#Rvl6FJDDIleXxKLXvRVDfn z?s~2~7=^m3&0(5fo`v34uJREJUa>}?R2>Oe9iRn6De1gPd82c>Y6!`H8BKrAtQ=#V)AsBZE+<{ff64;o^msr{%~ z2(Cz|+TdA*7xXf(?uO7B77=`F5hi7N-osjMs@!1EL@L&09s7--1)qp8h=sju#5QJr zhiUbu5mhXJQDWI*sicy3mW9U$0aESEbg4%|vPb2ZjqpBkdlum9Q*5jXV)QX42=!6c zfJbwJb7N8QsXnSSGL6Bf=sXN^#mg~6${DVLI zbK5&io;+dS{==rRuU>tJ9wREDrS$4+8>F{cfVt#<@t0qgYt^dN|Nhs1-;q3D{rjhG zTRzv2+6c@Bxs?_C-KE@==*ET%R;&EcAAjKwKKuFk^Nb7(tXXsCrcFE6uitR{?F%Gu zv;O6`Xue?4;=HeX_3@1xcQ0Eu-!P&uE9L&$Z>?Cl3cL*)Hs5l~!XNwbe{t7ci~IK+ zeDbN6`ue8KnmI)eI`9jYm7-kM^y@BjdLPsdl z+4J$4`4#<9c}HwvbY$M)uvg9HDO$uA`fq`JeA1q$-za zGAn>7P375AEgG+DMc^i9%w?*caJy?a74+0S1$c*X0Cx8<2)LVt&GWU`_ArxG$))V@hkFb*S zvKJ2;;twk#3gQSwcbTe0MF9fXB;T*^1+F-4A0OGJ&Pg zD1+-d^6EV0fhz@zjfVlBoYN^5KKt2WKF5X>L?!6Dp7=Xuc$>nK2C=G1U!H`Qc@&ie zBHY8Wu!N3xTo^PsI|p^yMQi=Kb!MBf;i_o8DOK-$MiYx55QBSx&IjYuFLCVnk-P6+ zK>~bPiiBc;BzBiTJm-~0L5NDpa)ntQMPy-K z)5EY%DX0r&p#r3YQVstW4>dhNi>nF`vG55W@+bnsa0odqqafCcoTk@vp*s^ZUBM*k zotS_Io;!OX6Wl{YK*FdRK%LJ3Z7_m8{v=-(sHT+k&Nhfh)jet3nn0Ew+8i{P;dLbF zf2$(P%2bZA1tBjW@2roj6S}MSyGE^ciAy@p#4WU&D zF$fd|FXX6PdCruSq9PtZN<*^RDG%#;oNw5halc9v<|sVu!1=aeTgc9DkkcrOR)%p6 za>|ZwnT%Bs+Ha}xwb03XH~vP@sWk0rM?i7ng_1%G+vJTaprYYGa$Iwj^xMTYKoKqgOP<$kW)+t*cAbm46O(ti5f<5q?IWz8B+{J zjbHH;Ld8TcbK6*7loe(H1{`U}e^rsGumMRBlH9UssQ6BmNemz-r9XR)2NPxGM-5}6 zM#v^^Dc@~t&@)dy`HU?eK^!4QmXQl0tmV$T@0vJyihEEY5{~KAX{HAGc%xJbz`n6z zqoJP~Am}h*1e6;jTkf-D*=@HhatI(`VL~Vl&LIWnxeseHP!q(X?(!pm={VxSI)q6HJD|Y4P@YBcrHYN zSa-O_dXl_KiA3_=FRwAg5TR|72C{-O35;CoBs2L}-&2zMt75Sc^JO8c;evvE2Uf|Y zr3~oQ93ffsoN6yU#)%}nD&!LTn~$Xh3|u%6e;ypellYk2>L3XjRW&9%q*HL|4(QP! zdGMyfGPj;d!eO-wT=yx^mE1{L7FukITTv|gZBs*pL3Bq6^vCe7W)FP?G}Z)ip&m3t zS0Lsa@>CJ+LWkOBLc4lg&IbzLXhQ4GlwQahAy$0ZkxUp8NFgk&^__ACtq|=tE&b#- z9($7Z((@E{y$ernU%o6RA7Q)rjBpMe^`r7sOVo^+mtT3^gasTZk=3iqKCsdnz2&#v zW*=mt>_`g`fR`13}}SG6scl;dBPgpPUfs7N(@+7H^C~S zSQ(u1Jf*`wpb7fuJI9}Y_F3CBv^uHsbv|19e0Fy)U3ROX`4Cl+WqgDvI>eNGYL1{z zB!eU`t$Udtl=5;=?uSda*Y^2a7A=}NYi2Q`o`V)bhcm@uD0RqEcx4pS9QgB%c0n0#)a=S$sRq(z<;Aj4& z6C$ijCJ#~ylWQ)zpw;t~=*G*i$3GdOQZ^<%H<aiJ}w0zHPIh|IOh z|DdQKKLRk^#sMqsEs-sWhW)ub4Qly;3A&VT$;|8I=Suu@8zF`7z(i|h0J5x!EL_3d zSVo!#ftsAQA)PvHx^pMZ#m33RRf&Unb<1Z2(g~TBqB@KQ%_-ZX)dhP5%=WReGROL~0mRpIJF1ZAGv;!_;jLdKcIT{b)bne8` zou#F>9;!!<9|4HuWu$L$M|~ zymJNKu+7FPS02#3mXX&?kDrxU11GpSOK5d)m^Emz?Wr>Z@Edk1S3u*q)%Vl;W;CuL zDafa|7Q*Dj#OCNij3K;=tBxFf$3SG^)PV-=yss{eTKQi}9zU5YB!U~4LgFy*h_!eL zOcPb(o1eo58#DqlDnWRfbH3Bd7^!N3=WQzox`|gZbAXHSHl`YkgQEjpZ162^K7Ra! z6N_6c5*nkUb>%)Vb5-8Bn`$rcG|Ub_mrvG zayge$TY^CI;~#t#AA)haDdQeGeCX(%_pAhw7xZf4qzSLSvSIqPOc!o^?(iXn=kFhO zHH`~-jb-7R>(t3Jlczd4+fptuO~XeDy5DG1j4bt8#v_X#%+^C@8)(^L`n*Y(7KE`u z_&34;Yf4C+*u{*Q!y<}K01`MQIsj;0`uqKsxUz@B$RS>L(4tY_Z@snEbgQaH^SpR) zg)D;1|EH@-IvkKZKWaq6?4{$!j=cB1_p-(i55MS1BD-=L^&qq2GMI9?DO(s_+L zm_~=XZCGp{2W}3)#tmtP0Y?|@f1}0O7!j0%R`?byIjM|0N)AcxI*MBkBfOS?u6qF> z%(02)W09FsW(8G^7!q9W-Ek|0a}vv;7r~9C$VkTea|2oWWVT3z7X2Ku6`{4p-}w4t zs;=+>7BWOKNpi(avW&c_SA|xkyWU5{5qAa;EMmZStXX9b2(e~aFKiPT(>di9%cG>^ zW>#C!P?YlUpkN!zwq&Mf%$W7-UgkFo)1>R&a{8-TQAn&W0UW7xzk`a_PHM^y`EqGxM6o03eAzso;`E+tQq=7)wJ+AJ|ZsN%N|iOBMZ;GU(~cB@Igv)(PBmgb7%n+@7ZT#;6`0+V zZDKpEZ#qco})C`G6w znaBXt^o~>$FiosbD|OCbv2|jP0}+I&1t3_!FVG4-3C)AdticjT(;rz83vLzbO|&ND zB>IX=jX4&p11*LXes~|U-Ny{`01H!;dLs@5OVj^XPTPZ|@E;GtBq{w!DT9^`z5BuU zIJ)Y=2j4e$?%d5=HzQl=`Ct4@K|oUDX40{>WN#3BvO7Ga|DCeDLPT~3E{PYGsBpo* zUZrRmtAvJpC}~7eQRgv87%m3|rOQ+;_A(nk(*{U?TV6@PdrBDt2s`RusSHrJE?+u# zo{2kWb9PN;3sh?WlX5{1QT18)RPF%uADjl<&)n4yA1VtHk3Q9Z0h}0O#p+w_lk6>^ zUd)<3>!F7q5N_Hx#|)lk4zU+S#{6;>ZNmi?sY*#%N_JC<(RAJ#lZgog!>nW>z z6m69TxhOg?a|KZ;*`FXAvL&mKRM3@JKE&gs?}oo-7&!4HNhNPov7Atl&`PrYTFHUi z6tRlQ2kprTpJ`o7TCqUPuxP8)Lql3+AN|M&?GJbI)EP^~ojInUVt%m#gkndqDMd)c z)aZaoDDfho8e2ks<3`=lNlImFNQGA9O>Ax*eODS~eqO)7+_92$v;ACK%J z6(1GJcwW24V>Mc3;?`2i(2zB>pZVd>uof=TKmad>eVCOYtk$Frw9;;CWrOW9X{NH2f(zxN}hlm^e_my0WQYW?O)s1hT2b zMic6bFTC{Jx7IIMIIk+8lG2SoVode6V};k;UpFfr9zJq($IhJ}`0xh}w1uWn6DF4S z(z9%emi2fEFrmpZjZB0jlFL?fHRt2`7uUY@;wy9K%^fMFA>-$@_>83ahulPsj32%7 z+H3EA_kGw)V|MrVf9u&79F=A_hhTKg1&5;N>K7wPTY{@!U-f%v=-Fpqy8pgay2{8I zW7w@u1O`l6!en~F+h^-@2b@P6Xn|<~C2kC@a3@|NJd`~g1H>3D77y!i@C2@^wlF_Q zjeTeLASRae1T`X&z(;TYd){;Zw5ikN-MxFK#I0QuC8-L4dVwZyKDiQ_DZEROb`3bM zQ%5_G`zU+V<{PtY$>Ikdc(-1hg9rAR{44aKkLeLAHihE&_gE-X$LF9IJp=Si<2JsQgf-F{Vcz`MCa}R&`lY z%^Qk)kOQDx1-zENW-~JXGUFm%pqkp=+IM#J*$jd=SAG9|_ujF36~uS|js=iPi5aEE zGL#1pI4Q*yLQ@Fr!T?f;%wqL^4=q`;_$Pnr$GIjOEP>fNw1~C^mzCaF$!H5*`Pe`S z45~u3k^S&RLh-s@Vs-fh$vkPEAoYaDAuH}Vjx_xDl}1@%=R5Ui!f2ths@7vd#jEse z#eap_1yhgLs3psm%$o6To3`xg-^H=S(-dmO&W1(BMnq77OA1UwQP6Bf zNh>DB4qd#(ZmRR=!BJN(*+ElL3UUuyO5z^1_ANtsb4TG_DJ@AN15&B;;_IS?RcL;8 zRMNpJ^X3j-oZ&DDyAbdLq-g$9cu+$)(dqnQMwvR_6046S6%B#NB!~-3GT|Xe6jS1+ zj3@T=;I^v*?(hKIu(f)B0u;UQjT5{`Ol|m=X0T*{S&s@8OodhCnN|U_WnRUrD10eJ zI9q50GlAt9L^slTeGl87? z$q`C~T6Sg@PJjO%+bdkW&~dnzm+@cg9>er{965S&&%XVA)23*b?B9DpJ=ZsVth1#c z{cQ^hRl^Cer${VPuX#B9VE7wvd&k*0+qzL3T2lW(Zcl(^-Jbr&nqb*w&8*q6wm*@{ zvXQ7V`wAnkD8ui>8l8|Uujnjeh{DzmWN`t>Q*Yw_uslA_6V?Nhihit&IKOvqKYfnp zG6po(o-dgV9XsU*ThTR06nh*&BJwgfMfHDE&sa4_B4=RPm3`ApZfiv-EWMXy$L^c= zh?=HS%Y4U-bVk3^-Aj`prG0r0xY1+Aj@3tI>lxu(*cWV~lkv6kL7v@~vByiIE5?OZ z{y|D$b<~Y^=m|O6_5T)0}OT z>PpdS^!T(w3m_WRyn#4PJqji=!%?&)1PVsd1iQNMN3sNUro364HP*hoX+{^2(2*N8 z(5S4r<8kmTkMcCWBqf$)lQQlbpX~Zpvoa*Chti;+c#Qf3_t`Q4Mg;y-88QJr#_ZB&7g4`39Zbfk)@YfQBe9NONpg zHm_>7khfLc7Jh+-@<=pkgJ|KF-!9Mz#zT$q(V9OQVonEbY^?;8^u;pIV#SR~1p;be zbLUE!5iaBkQDy5;lB+`rL|_mFq0*}KO#Fy~w`jTQO!ch75_)1pSgSb`t;>UOBlvd; zO*t%!C8TYhu40Pnr6&+4RPeznB^nk=j6SZr*-b%>?4&|O!EYs{AR23N^!c6~r;$id z)KGZneE>ah6wDFmy)9INSc4N?iSz>6<+I_Mq6_NB7MSpx7fu+z#yo_H&vUji-6!|&F$E@SY{6}f|!uhN645`bYCyn`fR zaT0`|h``h_IPo+ZS6p~qpa|<_xZGGc{%bX*UHStRarC> z@j*X)OTK7p+oVT|RFIx`UIm_%qg5U_!|c!!NkKW3<~y7Z60o>%{*2=fPoC1kBX5r` zzao^JLw9=06hq+MyLKNsJ8*c^n5ipnTkyskuSz=9A3fBA6X%cIG^Y1lmxBOC4Ub@x zR~FeAm2yLET_g7+H&el zCO1r;K-xl2$CIJm6UiKdHFBo?NU5G{nT;D;6EjUZs&$Dy-Gk$`{D#htx^Q&NP20Rj+i@sXd z3ym_}7+OOtc$QBpTc{RJHRGTJIqC|-3jciPCzUYJhUe$aEe@$gK!y~fsBvDkN~?BI zgZ_E0G+^k^aD9f9uq*7*Ru=aQj52!+-c$vK5N2~)Cz7x$+v|gd8--~cYBjb}u7jn+ zS9#(nu87<-Vx1Q^Bz#c88hK38Vbj zm#QYFmJuyQ`h!svC-4G$_+Kz6Q;0xEuD}XX0V!$eSm$T4+e;*}U8uo|ZW2_Y%QXXGC}a(N67_koT2(!4zNJn5sRsnqZd=KO(D)%rRU8gC`T5R8d* z3K&2oxHPozJ2hp%#~u|76fO{c?)@dUfP7F;0NK012}6U8$8owc=)2}f$SS_2Q5Rlj zyeL_sVxca)4CNspa>=hURZj}K9<#GIIhJOOJz%|~S@ESwA9i=5GrwI8Q0@T6KJ*Ld z`1NE+9#^F)!52T9$QnBnOYsFQO>io6MN*Sf=|Zx{^|Y%247=Oa1Qj-fBjFg$3PVGx z#Kp40KE6gB7H&J4q$eV&@py5!PC_hDkxMTmuMVtONd8hh5yS$33EP6Lo0Mg~i57us zl@L`$OpXpvGI=RfEYfFMKk`LZ@%TZi!Op-x^`iyBiGOMZRFV$ck})~#q@k;LTrYWG zFDo_Q)&ZP0YO)yEE(vGE_~CAfZme~1y7(adXH61CLky)52x*Wj-Gg18ja(S3KD%|< zqMf_;iXp!16YrZpZ_fVxhd%e`U-+qi{9_X)jQ^{@{(Fgk_GkVvaev{j|9;ZMEJ(i5 z1zPDrrxf?am5@xW#*Q8w+hsE2g*6Y{HnI2kmN!pq+kv3bI%?0SuObc_Md-aQstQFk zX@X9miUR!qA?#nk|Gx5rzEhGd+qzknEz7cG%htvBYi39?GbCwRLXtv(lv2t*3q#wz zP@XNjOZRChuq-8|KvST!rL@bk&pyx6b|E1U$emo8WM(`w_ISpZu|4*TE$eDoH%qo` z%aUd7>;3uu<#C2d`0B6U@B2I7b3XU;IiGX+o->9LIrJz^;JY|Pl(_#EX3IC1bOrN} zgfVLOb2Ru7k2B~%J8|Od@#DuG0rqBZ0&f;1ef7Do-g3+BHki5d&f5+gIO=r@uM|D| z>~pu>zU$6AcWQDz|AprtcwpB@2^e%lxZtIA7G{oLJau{9s!JO;ulkPfd3gQWvtNJq z?92NhHlVx`bs#WC>cXdX#T;0FE(#<0dGrUR6g|n!pTh>X9$g;J7>g&U@Fc_d0^z zfSf6R(V`+Sap6mZMpeHezhI)uG*!b_oj;vg zC?VO^rEYjq5*zT1yAD6C8NPbDysK=D=7z)KRahh+Wi}%^FPw)#{3rO64pidNm}Lz< zTu4jRRfl?3VexaGEVpAGwOFMgb8c2XhrfiolvtwIidR`t-Ca_)+tWpp0T;X@)`U&& zI-8V~rkZ#m>J_x%89swL(bWogI*9Cq=E$nV?z;L~lcYGJ3&mJb;!C_0TB;`rfC*_@ zB+pe2VRPzBviE2D&m)a7<&qdV7-%$y1vibGd8BLo;e_dpZ09%V4}c5!)TgRry0EKfT{C*iDlKQ4eQsV1lyiw8~%;v0gO3v%?#YJ&#|b)nm(~i%mhfG zK22LgBpM(zkCAR*@BDjj^371#zZ_}{Yz&Nfgyt~}V~KT&%+R9T!9>W!PI<8?PEa z!JlcC_v~uk=p7NMR&70X%{9iuVI)SJi`6DDAc0IvrGtLxq7IEhV$pRY6MleSNHazR zdxi-=tM3!cGcH*(RBJh<`zbU{jg=fiNt1&z?PU+M3)u+G}E;BnCZ>?wxaFuk;5YK?c4Vr zK75F=Y8~-w!3x^%*tN^?2uF?{L2ssjbn_|dkd1u69r%knvgIomslIXMt%n}i@o)d3 z-}hhsSAPeUf8-DUfp5J0>TAc2|JYyqi;g3(;_xs0`Tz6Z{d@oO)~#E1@4jQlt+)Ky zKmVhTJo2dOcLm0k?{QGLFUl$US-f!88vcnv4Y;koc}{vW^f z)|vyabJn%x<;qgrtWfw-QkSRv*)z{PJt#Ndap%?pufC#!cn8}ybhbj)AK1P7&i(sO ztXlQ1mlA9P^U)AzO8usfD%eQlLy>$7My~nkr=R*W|H=R5fd}sW^dJ9|P6WIAuDz6g z^5p5C`MF>IcR&4ywJ|^Yv%ma9KlJ@x(ma0RV*|GWS9 zLl50AX8kD281sMlwd6OVq+$G-3P{h4=9{lfJdf9A}YHSe65VI!8qy5-OB zWQhZ+O0Z%?jnFWx=?s9AFPPfbS0K4LjvYH%z4Cty)KICANbE&m;L)xoc<1fAwrtsY;?(Pm!I{(S^X0F< z#MkfJcaIZ+UO#zE*YS=!c5BiO9zJNCENkpa)7j^oC?!!9E;x`|J-T@D#v5Rrf`Kw%5P+j(b(2HyjS|#v73ZVwJV}$`NQ)7RW(w zRC3eKTX$L;a^%n<)}_I=wMQP{a@`wvckkKjKqn^<@V>+8qOpO7$YI~0sn>+Pwt4Q{ zxl^Z3+_i59T^&7o^oGq_w1_88o&eB+n9iy{b?Vg4+xF6hv#K1vga$RbxL{m7<$z0! zsXW3P7HW_(f0zT~UJ^&Q9yS-KDpo6pCCujQDmRt|YH+fZ1!kZ9g-m<7>-Iag+;GDS zFT8N`)|=~~u36*YXNP5xi36Yx9y-XEPVt6=x|xqCa}#7q6Hw#~SChY2A`Trof)~&C-cGS^Z_uqRzdwBKWerL{z8yK7xC(t8P{4V%>GPZqt@3N>1 z5rpa_u?HV|@bzP_Q`sYreoTn-^2^^idGh$9kA8=P9d)<1-*U@KUw_FduB7BtF)IGp z$9_le3-5pB&O3JBylva5Qzu`3`DLJ%7&}~HXHz&5pIzL!W2Znt7s8CEYG{{HX1{;` z{*Qg^f`z6p1bwd9k9Io%FCNJZ4saxJ#y4rr40D>W5+D`*}nbO8*kWh z=+MFIZ@mGhY$I7;x^(QdqnQRtXU2#n;IO-uE^%~T4YU+MH4zV?)Tv^G%=5hc?mG`Z z_{cl&zJ1`}{_VH#08lV4eoz@j*qU`Un)|x_zWdL-aa!xSXYW26{l9kfsN>N%B4cj{ z54_6LXoUu;F4L%?y0F~jNS)rS)p+ylx%KM>UZjM$S=lp9cDfrX^+qz8n2B~55WwIl zcCK8W^AI4wfj?oNx6mr6NJW412-X5xDga*0Fu7RxqeUmLD_4|G2U`E6`Vg$8c^GQy z8V-8$_aH*3C;``}XZqB_Q2)^R_IYwJ@DLN~d+>-O83ul@nQzipe^`{KTQ_ggQ$^x~rrKeF?-+nKxm3|^GrFHn2O zt~)hIHqqzoc)$d>2s0&&9Vgd8a79xq-_p%K&Abfh$?Xlk)2Ght*>lI=_?!RF&wt?y z|KNZ5dyhVH-(ULCANz0q)?d}B3d(T!EnpVVKC^{!g$a-ZmZ2%~r20z^y1u;ShBwZi z{+07@Il0-E^NqDbD_>pwJ zTmw7zx%n*eeREc z`VarcXTIp|$De@lQhl3DQRc5`)B8Cr4x1c`ePp6Da8)2yWstLRlWszl9k*n@_Lj@u zx9@H~`o!_$;=*az)-YOC;NgcJ*4e#o{km6Qef5){_#T_F9XWca2@}KfWqI<@uA zJMJ`!AR^aNUTqNto@l7PL(tIZ!`OyB5_jyl?SHa>x_` z)yKkFKO<(TsW>E>i_r#7HpP}`DF5uE(L{7U^6jot7+nK8F#SUBnC(g}3m2&O-gA%RI5%$C^!2ZQ{l5F|J$(2uu=hRmux6VWwd)*F zOhtU|VTf6obVJX5_uo%zRJMEfosU2MMTUWfd+xs5ILh(YU)N)K=z)jcJolFNcF*oT z-gXqSKl98}cinwAM=f0V;upW5r>8i+Nh;4jqMPc~6xg}a>t8Fug9bMVmNM;?B3$Bvy|kkrc+;Tdp>!aMhfnVjfa zFxTWwa*nUQrdeEd*}xPh?_Cpy#1i-%fhJu9)7EZ#K)AfokJgOP}{Z=Mk*zVh-n0OYf37{@@M21HXe!fzW7QlS)U zTJG9+w|5e?fTGMJuN^geaQf8AyYIP&xM*L4t|r>|(binI-*&rqdp$L$boagYY99;J z-#BHn59#P3?FS!t$k-okzwqJ&yX329sej3OL^chRY`eu!@Ef6kWVlR8!=#U;DrMrDDol;` zh&Ul7Do>_cQN{#?b_czkfA7eVBOYUlLlFYQ-+c2;x7@N7!S=>g91Yed z{i*-(Phjp(|3`mLbJdHc>E)w;fu2Mc zJNW58_Iu5pA3A*W5B|a5<492f)1gDJoUnb?sW(3Rxi9|5|F{3(hkxjIlh5z}eLwi} z%dh_UPyCY~{^8%fW9Rmt{K=oaY3ojJ+k6E3N+nr%8ieiF%q-u#xbEb!BX1x3`lG=XPhWT=CG#1o`^R7oHiYi0Jn}U>+}g z?fIFo=*6oqeE#!XvrRi++5gIO&pihthw}NyK6m`}<7ycLtAZ1^KlJ2Dn;9hhFx5$m z4Zewy9N54AiO0VnaT@#Jj7{o;^2#LQx}+;WOzZuUbm>rlY2|ZP{`o(43w& z15=_d41yz%M(c?uo^Zl97L6_#9;nunjW8J$_d=L3H*fI% zrJJ_iY=RH_{P;hSy6=N~g%L@+!IH5tx%=9y;&6Wr0lGM_%3A=B*xmBL$!bN8D^8QzhzZrq}{1-kCcox#-Gt%Z72U(=sK1{+U{bZHV z8)pC>I((1~Ys)pabZpM(^PhiAUx*zT9aIGb9GtdcQAnqX0w zC9rtrt4~W@?0oU|gR1IPL$_uX;w>N-V+jf!WhP8~{PD-xfZ*xyp+g1^DTQp0968FE zZ@lpaotgdn_jCTFM`1h_o{X72{nS$eK>~5ot3cn}7)FVwSIjrdZ(BS<-*1H zP^h{5+KXTFCOFN}#k0>ot8XvenfG6Q>M6yIOh5j_ICy~Fn9@c0 zlVAFhnJZNQ+E5a|6@mFz9s3Rx9Klpc$;X-;(0GNYn*pOn%@_Giv*upT>J0rYR~EX| ztQy>iiOo7!Zsvu^M!Rz@iv;E?XYnsy)r}cSjV$CMw!pZ&9^jlT5M321tw3r7S{B%< zSv3IN@U<#Th}(1PcXhPV-;<<-Z_`!s6fmpZ@Xx^8fpZGhbQv!ZVk@ z@}>9QeAf^`y98w@I{MRvCc~ta&{qDIdp3IW*3I^@{MBE3Y>_GkIYIY^WS)HTX)U?} z1n|_;&wc61r+4hQV_9kbA)MuAh0t228j}#GqLpjOBgMF;q)&X}<8Quo?%=`0yS${l zbI0cEH~rE7?Z4wpdt>!K@sEDqA+JC5gWr$e&wt^`ojY&+L;u!~{K-G@$7Mc#;?&lg zu4kEx!oi$t`I1(>=9O)_c$Oo$u=cs9);{x<>z;e+{jWUn?pyD9w7PfX6&u2Q*9z7h znM#Q`3dJW+P%N2c)FF!zWCyEM~)tT_>qS_#Ikds zb)7XW$BrDqwQi^h6vi1ZStjs|*YtQ@8kA1W!--L`wIZ@2xAg_7fod?XQLZ&$5#qB)0Pj6vh1QQx+0 zn~^P$Pyk()&{~Qb_?2)Wf-7s}>eiY25N9m;jh4}%N+Jy6&XP=h32vb!p zU~bXw1Z|>lwwKsOZnXgJh+*X)sV;aWa!dw1G{!bPGCa0p=T4zEy)zO6tC|CTBPPfE z!m->h{K9dm9l{uioX{oY%RKO#eAnCWzWe1bKdDFk$fJ*16bS6?yLQ=1SK!NA?%A{F z^yxR)-nhoxBX(*`aEVopHp#XC==7=6B!A;gH;Sc5MC|gp$9_Yj?ri&Acicfu^1yBw z$!r?DXh>nR$wDT*{Imw(>Z@+qc8mCc_LB6<-yj^Lh1^RK{JhB!pbQ4%9hj${{<2A~ zb=O^Y`>xwIZQ4+Uzx;urO-PLkP_D*^9H^H77!?Cl(a?6{_zA;xd-v|P<$*DtOk&z- zj8cEn8oADg1lBTxngzf;Qe@t!U1NcTiR?GezGZGyu%!<`R%BwhC}dhgq3+DFVf?~{ zSrzwzSClSj_GRE_5R5NFTza`Uf9vdRlM_H!zd@dA3Jj=f@4tAVkC3%L##*VEltMN6fF!dj-ZJcVr7-i#kX;ig z`j$uiZpeFP=rX$8W6{?*^DIAIWFk@TqvEOM3Z&r*Mp`T6N&rz2)YWTf*D4cXEKv=P z`VfZ?AJH|`=`_wSQgJ(Op!H-iFgy@SD1z5?9!K2Rmw&z2b2V-K`mg=^p+kr4uwy(- zt#bkqd&17VQkqnUV2)s+Hi`#u!)|JdilM*4Y`^~E*NmwiJfJ&gnTk~+2VOgR7(4j? z`qy9l|NnhYuf?O#Vk-dkw!g-L$eoVsUFpsxhsJ zbQ9eCPlziQsczi3-jeoT{ngKE5p|4y`e*+64L5AM>#lqM^S|=nf8!hbb)f&ofBkol zA3ybTKmSX{Z`36~T+8PbF#QaA4|;6=P$20?=(i6(8;j~ZqdO=?nC%)KvU}gd}aRu^GZjL9Q*N~_-RelJ@@SUhyU=O z9XfPWyZ-v|6QBL;PFpA`fyfSoOF6 z_W$QE{pf%B%+t^D&eyHG_Pf9PV?X(mKSOAL;m`jmGX+2OPk-Uak=K6WAN}l8Pkq%i z<y{eDQ#Y_(Omn%BMP~4SHX%@%=SxuRVWh^|^~5oIJJuvCpjjwO_rk z|Ij5%RnK2CQ{4Mp`LJo-nRtfrp^ULLqsS{#>tTQRNCS;*#W}U4GPlNz9nF5#8k-z? zsomNpzMCVs=bn2N<-dO9NB&Lmy&=Hfk#5B|a}HZzmr79!(rZn%Eyg3~a3KS@WJpTA zfxLdh-Ci=ibm^9xZ~gpD-F*+_7A)H=ST1W9mPIg@)NI&voy|Y#O)W*+{40iV#k&n}8?HEe`qZiX ziVd+Gg0y+FS`@acue$f{d(8>zdhOh~Q+r@neJm<9d5y~5} zPQnh~T)jq@8fw*9l%oDvI?25R3WQXYlUx24O8`@Au>X};UU=>qKG$^Zi!VIC$vE_T z=fCmtH=2O(j;ei1?5O9R(My+9aO~*EX+CPsCfv-04yRyJO4Vzng;>y{3*OQHxUr!{ z!hDZ=jK49Ryz01B;U5v^$VpSTp|p z)KgE%EF2VUjfJn?uyOsCEjNKIJ~oI*NrYp_iuXg3#uH@wl#!L%cg3${mt%RO+^S2W zV9QB{HS2~n?|c=R8>7-RVGv$JV!!muza-p4*yiDyIEfBq1r4?Cz3Z+w&YTui-*od< zR>=tnyELF^`+*<)p$G1N;JtU>1qZcsPVsc%FY~aJ0!^@TcSUYU&W@hu6A zJk_NU=2FWfb^!}f@#2azNyPD$tS#J#Nj@k`3)sqKB|#upt}Ax{vvZcBYAw01ps#K- zU{l@ld{~q*O?Sb$?e%ho#GWF*4xPan;)j0d}B_M?+%HD_*Po zMNXjQZkMaZ_$u8ahWm!GB_n>Owe$inUD~m8$AMSdB2N`%zC17Qo@58}N(`8r1-SQy z7LZBVn}Z!S@V-T|>bCVax;5ONoK>)toUd6Sfn-IV6BZLWi6WlzqCB}&!(E0wlMG%* zVQMnlLJEV{g<9uzq)z>0TRzWwyGux?t+H_P#jn4lH8MH?pH+t%EKMEvq`en@9XWbb zc#fALXMEIzjA*+;--7>}KmX4E@b{La)oX6pedo>GmXTd2v`gH(_ihI}{>}gT@3>!m z)eZOEy;F@zJio;?So0vR&{<{KTW@5r#%m!G9KlRj1S09e2Wu{v1TVvk5|czKEnitb zaxFi7{?)(sclPYr_1Avv|Fvt^Eq~_E{v}zq-@5B_pMUn|t($)K=N{X>ee=)!>@R`0 zZQIUWx7Wx02#jyuj(+`w;zLC6et5rgQ-+C~eZ!_C|9T2g1K9&JpR4`LtYD zL-mPIe(HtipLH~rl|#g(*|wh2@|V|NKYrV7cig&Thd!9LYR8V9Vud6XiBUQK`%K+S zWYl9-ljY>unNAKTF*O5Q{z?^1OyTkbr-Z=puTuIwS<9)zea*EePn~}Iop-n0e6tXc zX89lOrBMxINJ3#nWzUIac!91F9U#mc#0BQ^yh6)%VH2=Douc^_`bXEw!gH< z-f*lI03(!pT6p!w8x{?U^#hJ*4-o3AO*$D;1N=iaY=^;s+Kk#X+a+l=b|`|p$JEjDY#jHiY#)%3MoP_r>R zzOhH*vL&`rmVJBoShOLGP=m0j6}WoU)y8g3yr2hzmJjN{JpaOT-Z{8)_nn{k#3w1j zSm5va*M66k9y@Ql?fOmEfA!gC#0}`I{?;~zGhX%6QnD65)eSAN1HNfcHRYCV+f_7m z#O%LLJ|`xA@g^gs8#NOBi&~5_SS0u7(> z5#23I@c=hZ#RzCX5M-qq8gAcp2hml#-g(;y1lzD)NGY*3oeI=|hb60TPXpS_2vnhl zP#R+vRuvVVKW{JP_XY6NCo0D9mjJ7AYKygx#I{uPMVY<$DH@Zfc5Gzp%L~u%T=G8k z`D@4LO?{b%8Q`kbx8A-RC7U*GcKw=`Vlr%;^)yUO(~lQ&0ZczwnngZrc3#7e4dzKlKkbU4P?G z{N&Gk@csp_equ)x&g^i@+==6w%{TsA|MrjUz31U)pZ)53$Tn4oRXZJLKnqsp+* zr*fp5oMEk$(5Dk0Wyw8w<~h#g(Qh!P#z2)etBtFcPvZt{z{@S7k&phyBs zf?X7EoN%f_aV{N*enNNk+QJvBSQ-V*k%+F)##2P+OfK*TR=J|2X^8aT2?kP#Eb_pT zm5=X}k{<`L*m0F1F%h$}+{akpDyC+k3>wbA`!=V}h75YLY|thIMEEMRCjv@?sM<60 zEFm0@)G@04)nECwH_yDS!hkv)Mb1YrohQ(+3>4cMN4Ba&^?7EaCxbU&8_tXNZ*!HX z?*ALV|M!}9FsCMh<;Ne_3|9+sni(EM70YSP5B9Xu ztRk|Q{?g_5P0eN`N$FApRM)Jv00^R9sk^-TWNvZ&`c0cyEaO=w%k;wxvE_-zpP&-g z7{s`%R_)@2lgCe;d8@$X-FMHKH`%nw8?|QM-(Gw5HNe%Gu`7pNwdRz}M&n7T;WRgm zpW70CYyiQFGm1z7mIXFOCG+MM6wb113uXChFE{D6cUt15ivlbH>zFMvs9B`_Di5pj z57(j4VDMZ|tV1lk3FYc~43as7r1tYU4i&Xl1XYIqbb*(DGiy*e+$g12kEet+FMapAeQ z&dRo7^A>fVYuDR0#R)sH(uI|mbaB&7Tif0u1{XIK_=usSF+#fAxOt0RP>qX=etI88 zGt!&y`Iez2iZu>Z3jZs=^1rdME2cY)Wm<03QEeciWaltDr+n2a)fvBLEu|ogrc{Jz znJeHyuOZKzyIpzI0}}9N!&-IfqA1Ed*ZPec*oo0`OP9U;rjH1kjmd^P*%6XscO}eO zgQd7$C4&^D%a$v!U6Oqn1sUEbeDkbVGCeLjzhN^X+?#h&R}EMai=F_J01tmd9Py0# z&5Ry(W1SI-DqtTcZc0ws-=Sqwj4Vf(zgnrU`Y*M8+@_0E~BO8zv6qM33 zytKnCJwOlLk(-53L@n0Q-yj4QJLbAZTnp$dd&2S>#X$!_#ilr@@r(;Y8+YAvk0yzu zJaAw?8xXf-o{dW6K1I^}yd9?-P_Iwdt}bYZox-p8_=FiWdEda5%4=$lxeMfYHnm=j zUT3M6(Z1`}>0;@K+KGywTGEp(sp_E=`KUigJCKDTq*X7mzN z1F~7eTdqwEgL|pLZDRCt9h{IZhkL>NG9dBxCw6JP?N9^^1%5h2KQ-c0+;5i$g;%` zA!F_9hi?%Mjr`1$t-g@(_I@GW((i>POu|CGsvva^yJwy_t zI1QF!l#Px~Gev&U;TN6DE+rqjf9uUN zEduc7V-oYWqow6c;G%ad_;kZ0VnM8pZVTB+jYrB2V@z~MGiPV3mHWkqPv1qM)Jvi)XGaD}F% zR2^tYb%D^kGOJe^lR%$oP|c(;i|^tIgpvdm-9vL!4?mz5CtY|ij>zBB>Qwk78i_oA z!S+iV#L%jMrAtNtWS|tK#Z_n$!GN3`f{-l3k)C83!og~U)k3I?>JlR-CL=M60k`o% zJ2TZK9x~bx&dt(A=Y&mOD{pX=*;oUvF1&vHoIW`_0l14H3_mj}mxV}Xt8{G%Wg$po z0qx^u>P_Xz{_w0xH5gCP0 zsTCS)qz{NkPmDqRGNuZVfHIq@&*^1J+dW>xs!6DR$VKNp)Ri$R0*BgVlZ1B2TYo{@$D0t#y?Zc-C<&J{v?7!Y#S?1G8%tA2LNhGqqJ@D!pkx8LNZ}k>O!_5ll zy;|Reb*2&-Yp=BcfOOjZqbK9C?;f%FCQmV+KrzKZEXHaJVcaK#?UD)8Y zh_G0z=0xxpjogjiaS_(MHi-A(ErUo43&imuA?jJeUYZp@-Bl;1IgiHKbWT2l+w226 zlDTR&|7(lD%SP`Tb?EJ~h6!+>`?b`!=nP$Fc(|gPP?KX`sh%;9<}DLhAml`$<*q8oIj<~(D+!j`m<)S47IJmSSJtrezeMd~9O zS7;?)RT!{rf^=j9znYnj!3j$Lq062d{B~?4&QD#(;+s*O<0b;MP#c?cK5z!QGAr2qTm%1lgQ{)O!3lN{B2Is?sg>!JfN%TA}%W~tB#KeovT{iYWAi$`u>(*cE#h-?7 z7N0)a2;n!84h;kOo9U4}*n|GfyGiQfL!?Uzhr9WP-RW+^!Jc~SZI!?smxAu8cxhJS z-Buu1uhHi#Sl84}%yywUZ2;+--dU7kA;HTSTicq~O))-1!#q2erjhlpI&dRB0X%~BIL9K9IbKTX~ zt;-ZM%QaVT>@ChHMMDdWs_$s5_NYx+gj{1`kaf*$j#WN#`x_sfi{Uy7FCex$r{U>E z)^X2u<8INT@IC(rcMcY`M5SckD~^a>EuYHQd){&%0r5~V@tWibUCMsh5EmaJAO-}D zHEo2;eSw{SMin4F3+eEvZjPJ>?Ko}>@)%%ITB-biT~)88ZtpN?sjjlN3JYm+=%q?j zaa-XpGU&#j7ZE}}Y{OiW?@T2SlZG66aYBK`Q)Lq1#mET0IRr+d?Fp%9Oeq=3GmB9keLDXk~Txu%+L@pDjtxmfA|kqDzzhCMBv@GByj<6IJB zIZ9GvRm0(4ToHIx&JY;{}n*AW=Bv zeqb3K@u*&E-!OwU;DrrDO-r}V8xkvf@Lj|A5^#NlmnvB@7M%Gv!Ei0%Wh+UUU5h8Bzh^QYKR_bjX7M zxv)yOK`3Skn2r;C^5YONjxxztX(B?A^hRKfkrc5mbpti|fGI;_XabAf%1|*FI-0tU zQ3LA$I-__L7>;seEE%RBmGmcVaR;#vpNMxPc`?)VGFQuhMiGiMUSEOnIfevSWV6nw z3haF4jiXNh?q<;iz|pz#HSu4>Q~`=vw5Whid0+m`;{1Uq7n1hv-TlA&_do5uReeM~ zQLy+&OEvn}yC{HRUCU z717OYA5xMcYU^RETgHC+Z15442zJ>sa1pEd)`uTyvrX4qZ#h$_hTJ38)fEl3poyiw zZ=#!&C2j03i|J+sZ$hhI;h#xQJx<}s#`U&`t&zrmRX~nugBM0>z{6InP>+;zk&cHrk0JR5iX3K?5c= zkEOOivx(P1+sUCQqUEtL)*@V+lIvDa;{=eRNJtXH2vPwO9RUJ7;Z_G1ua8-z zCo>ru;BkWntm^o&&}ARDMe1@@r5k~ugY>b~Lx{k|2zzz!f;SO$x9CgG5;#F9u+?r5 zsa9Hv)ubV!!PLl|VinQgax8#DR|(QVz6WliK?iF5!3ak>63dFiNaY#83ZfESRUavH z#8`yprAm;Of`%s`a*sZsYang7i&VkCl4 zxa^6Au%wVcM;J??vH&<4iPgwn<>YdeJweA=WOCRuY8%F`0QmxQ0h`N9@`?mt*p2Kk zwXa(h4X~QSNH1}|)9v(r6%O8&2f~rL$6tt|WPvc5qHXw%m;|;E3GN~jzg@8wuEDn? z4R`Uj7*&r3enL~TlCHL3X=SD$xpASWoO;TOEj%g+Z*uxw1T&iKS54paiR*=yNK4=mVHd24Y&v{7lL5sQ3_-r6h7WtMNUX zdKC63NkvmYj{cS?EeP-|vm#nyP_yO4yeOiZ$|LCt$~cXnc{DZ}XOk}jt%8ivRQUiY zmYBK;cH;u$jqq64ZVG|SxOz#1x8U8Pc((YSar&nBK*%1~sKK@Ix&<#V%t* z!JT#?PpALpYG~F}qiuw*wG?53X6G028?(d^Wk6sW;1M>$U40-@5brTpyQE?k4Tf5D zL@hL14(CXbh*-+31XzKXYk;|n8-(iZdUxi$ZT^=B6>~xCx>Da(5=-w<8IF`<=br1u&)OpAV zw2bH(X)D2uCca8v3!WZo`WJhRXzklF)KEkFTh#^1 z4BEu7vF1)o1TZ{-NER7FgrR88wUCK$ghHhZVe)sQbAXJsxkY70sHKeTGXvm(lP<*= zq2;t1V>8W$f;qKmZac734ow`x8UY3f@@n$5B{4+LMJBb>o-JVtk!ve8{B`R!wQJX! zWYNuy-9@TNm!;!QO4?mm9A-!C3+M@7qn;BRF&bgMW{X`#RxXASG0;VL$PWl^!In`CarX-;OTDRQit+JTy}q3Q@t zn(eB7gM#1QB+SQEMHn`^5uGnZWMTz;6qJLU=dvir%av)VX;b0pA=)E1?~HJDXvO&q zM*!zQVhM_PtV%+Zm&N#~z^5#DOOIiwf}2E?46d?1b1r0)3PRJ3{{xpqJs<6B#=;Y^ zRc}#aF-Xu;3}n$O2Pi3K2m@VK<*WIAq>&U1gukIV)_BZt4iu334k`_2rBh`qt)hfN zQv7yP)|v!A%dn&;W?K?08^xukfXO4r$`x-=44#E;oM)$50V0RN6c@k`i+d3B3ew8l z1=9Z&$TEAl@{`ldqZ)Q#$P!B*8pyNJ+?XB}OlerkG4k=Kh9W}ZNIV-zB3I4Bmy4)l zRYRBzW8KFvlbOg;sSu8!QjL6hgm82HvFiFI`n2erdNXpm)bfBBZnqF+)l8cV&p|`SaJP`!&&3u! z0#+k93@tEfE#urT9CZ%wfX=f7MtZz)R*}#(J^`e3p_1lE9kABI(5n4dOW+q$GmM;1 z9VaTXkjrRmfK-+t*InGnpT4-ws-g_7KS?h}udOq4b;ztYgk`elr~)1l&a9sZR9CR( zit;kJmV~~trgml~ciTg3Si7a@*VJE6=FyUMr2w_f6;r&e*i2ZbLYMiQ*9yXwv~T{3 zm&qjwJJnO9CP`}R$w-wZxHz~|FoP%AM)elT*$~kU5XBK042??a8Jll;@bFaitIlZ; zNME*=p_JlzefK8t$MH#^$;8}${z4(GtH@)~4SRH}HEoe^n6(U%TB)&u7*P^$D^$c% z+KNlebk1W#;ymEIvj7B+stAhd5U@Nsv)pQh^d)*5D6wUyS)*XBDPbX5!f+8)%U|sF zPgy`bkDNj~NnPbneA-}*uxQD61{t8bs$-p|tKFn%(sX+?6W1l61CD?~{Q>i#XYJ*IMnGeQ|3QCILalxp^JZxrK^ zD#Q*f4{GW3+O-zbcy$FGcJFPKbw|)t%~S~sBaGo&siWIr4$c1!Q!cH!N(&wIV!AeW`GVgqmJbTC{3y9Ae^E>szl zvX}`~!rx6BH_rf=rJ(CJtltRayKlD&!Kl^Zg~wLHl!Ju64+yiNk-`-Nr=U^v=qIaH zVzi|-bm=h{u9F**$rU?V5Ot6X0y9PmCF|M`@h^iWs6w)82^Zm81!pN1!$5#@#sWf_A`Fj5u(3DOov5}rMjGm? zuly7vEXIg?>6!Id3$ltC3Ng_+LPA+1Tfk=<$q=Vv>*N;(f4!Fh%7BWcq&kus{S6-RjWz@HC)37(RhU=qaQMk#^r}AO2=WBZ#q1%xRo8jr7Q>+ zlrr_R|89m3x`>&IjEoRACE~i2Dp9;dFpS_1Ob;uxFjK56*v~ArP*Xg$kMLKxE!>f$ zvJA|MmazsW-1Q)$WbnY&BM(bp`bN zin%Z+aMyw+31@}%NWlwpm6RI%gtVw&;7IFVp{3^_tx&W=Yw#*rW=RP#eMTRpNO6zG zq%t%BGY|c&Mr3cb&UID!2h9ud_XNEQh8ze-ZstLAs*UEjG(b-BDuM|q;uY3juQ9a; zRLjd2SHt3gin4H4NwQDAlLx5XEJ!eO!mt;?csef1Nuo8?QXAwL0VL+-y7JwxD_qCS zM=py>FL-5Qg0>i|j*fIDe+Km7HL{vtRg}OGa<%cp9KZF=l{^*GP&DT|B92K`oWw(1 z(9tsJhF+6uz1sO;=LL^>)j$D5@_8;~_D8Adyhrns7&TN=LyCZGFh)qSWY4 zF&M%c#SHGOXIxOo=z@MO6Nsn?dacn?Cz48}F$m2Vyjhfs{U9o(wFaeiIq{I-U5x_7^NK_U!x-3+a)?cjp{jdyAe)&s#ckhE0sn0z96(>S`><52X z82FX1JSo~^17RAaLOnuCGW1Br<5e1fkc~JN3Rz<@QS#~dj>Sqm5-Kstsvi#OTdcl% z-qM&lloZp0PrC4!Xsb|uuwjh`$b zGA(#s^ox2+K`nHgfN#}R*Fe0eFC7E1R8`(>B9#X5#5vWL8RV(9!BEq#zY#HfUDYZ| z(tfouD#@h5jI#KdAPZVF%dRiFsQ>^#07*naR23ly0vTS&L`s?FfzR=ebFeF*g&F~v zu?@d#PRdBKNK|*li&!EtqMNX^xokfI>7RynZAK`KVwLn%sGKM(Gq4j9c444=vp^*;=09JP5EYzQEk6OO7*Cft`j0V%q5U@Ke%&G<)VVO;_XS*C_=1riskMK$b8PPZ}+j}Tb@9c z3u`dI%c0!n8hh{zq{Q@FO%WQqifMGDua9bFf%0G#UfmV^cYcaQsfvdmuOu`MLPioS+%|W?IaURF3Pw+Sv#2Np1WPqGtBgIOu4Z*G z!X|$$*#wA)6NyLB5ZTjEgteARe;SDqm;=o;@j3cJBNLp#%W8`P!-FqlOT2>CD^+m~ z$^7azOlT7_d+3P|?p}TGYsZf4-Luy$nOC>$=>q;8yY8^DmUm%xI8*e7EndI!vhlus zcNxyH(*E%$AJ^9Q&hRiBP)z4^R6b!Gq=`MQq8>pJI&>uf>lZ50?^4sh+u?Z{)Lvtd z_|QY0y5${s=ak)Z&pk%YY(Hacz@EkEKYHYvt#*{#y4B)zFGb&X?|qgn3uufR8E1O+ ztydx4w(Vv|cRl&#Cl4MvU_>f1B4l(JRc=?*D&2B$0BS&$zXM;!p+oegWTt4kG+vF0PkP;z=>T+ zP$MzW={2Qi`_Tn1O9d`c`6_^>FHF_L2t8*RQC;l^2NMA@qG5eS&M-KcRd$ow0;-m! zH-rEe#ja$ADwCd5rV1d1C7YKde4}lEN}ZC?=P3lLl20|0yo#yRRoO@az`-<4U*;)4 z9HR*1cM}*bm;yFLC{U{tF^gS~D692Xth&;m^i=&H0r}a@5-wDuQ+V_jFV;K>-|!~P4t@$PdPE7%!+N#Qvw*c zqxT9Vjpgb`&T!Hq%Vq{S4Tc2MU>+nDX$@e$IFpMN5CZF_jNLDA<7ko=!pB`AK>3m@ zhdBRyG6xbIL<4ilFoH^4R6NZ)*daitQUf}&r<{S75lR#-f{32cN2LZGz9KCphS=55 z{(O%nTJbztE1QI?+%im#ibcO)F*+h&0E}r3+Ic$tUw1=_jH5=RWU@h%J~Ui`7WO6d z&jS0mB0?xCgP6)d>K6dj`k_8*wU-F(uB94s4M_kcD)}p!d-T$=g&zYX8>=!6C&@kb zrei#e3P^@%B-Qns_&f0jE81kDx_;LrRa6?1F3k=AHONDq)@l)AT;P{}Ccp@`;>Nq< zt}y;K*Eb)!13CH{9_OW%#UQaU^PsD}o3Qr$yOv{Tm#!2RJ)CMA>QIlAsi>@cL_JnEc^b1VQ*(I~ zHq|wSRYigE+rwupfkN$sFc& z&)xUfLD336^ot2T``OQeVD7{+4s>(P@SVP-ljXLuNa~9LGB>z`_<(Kce`)C>$zxXO;c@d&OT3% zJ@%MQP(J>#k9z~(Mt>TcLx&IB<&w3GolZ?obSO#cl1DQOQPsV3=iafGhwaCI^?&`f z-}%W;+Uv#3(gM4Jq1gteodeFlCkCBY=xvbm<};uDtT+EVyW>&^$r-|a^}wr#j~w}) z-}ybbZrFxr$U#7x+x2g*P|u&=gn^{=Iqo>4rFj-^Zd8k@np|`3+IKseWfZ=Y2VH*m zS%6Olu;Nt4_syfMFt{1Fq1=L~O0c@@c9iyBk%;&0TmC4x3D9zYM)n7GN^6oaG`UGQ z^?%;GaItVclbouPd^5ayehjQimNJ!ELdq(o?ohqrKdueV{twxiBU--#p{lD{nFmYUr2dlR~S%x-QZaR~#uq zC{U78KSzB@4i*eht85XW=T-}l3}%u7%{YX4=+Knp%1Hv)Mex!q)z7jdqeMEDsERBm zQ(0gMoGioE;#yV+3k47v!U!-sc5HS#R(p#|XCq%SdJ6uaNMwlgtMs9FaHC-!E%~w^ zV$3(V@{S0Y3K@UNQQ>h&=vs-BCM|^||FJU09?yfA;KtTG9K?e=gOnp2u=`;vIKL_q zKjG|Ivc~GDLz1+~tOy+iFw{zoEF#^7<^sDODc~CcZU#s>3#)@G!<6neATm^3kpR}{ z6pIZOrDV^=k@JzRROL_|e zp9^;qDxpHiJT!*d71tG)bL6=wbB3U^mI->|)MK07U$-uYBYw#Xbs4c3oJl=17#HGx z*)wlkQAnaS{fj*saR^BkU^QWHeO513>!S2Hm5!RFal(;$i79mxSBhcSEdtaV%Saq4 z-%x$pOOqp+*a6EPRzs7t1YHX~qWqVCj4%k!dsDWPwOvH=8RC|*AF(O)4wryeisK+N zx#=pI{ubAY$@COWC~L$6;|WI$WAl$>5S(&=`oX*B-eL*%EHyh?l5)u(Z!+RDhzV%Y zha|tIQk6};FtT7?HX?~eHZ^5z9fYNZhLfwU8V!GbX8@syLe!8ZWJnZSDQxPraj1w) z+z43y7J=kUtJz^JivpnxTzMr8!+7>R1VxgEDRS|}ap0j(Urlg_B{;1yhQIk^gF>y& zQBAT#l%kz!BnM#ESd#1I**4U{Ly0an6&-zOK$iIz&)p9KTEF0(aN7YNI(*1}yGpcA zK?l6QW83J12M<2{@Iyi_V*`8k>`}%>48YkC#gI}c!0H0N#oc&Wejv;!7bOXLuHUky zNp5ZrcMDad6oD%)W7zgcYqCHCMn6H@_JLOObD#Sxz~A?M-=BQOlX?tazZUGPSBbqu zT8@LHC;JhbyTq$eC8KdsSGmER=*PvRr{&3ld_d;`DA!If=O6prXAO&e_ji5Q?B$kD zW>odEVANW)R&%deV|Q}f)!0Rej$yQOw05yq3G@uES$pu{fqi@TdMEL@uRiPL%Fv9J zM-IBD>5PH|5-CIDLZ;h90Vyo_GI9ng0UAooAgg)u`x_GpKm{|kNu6rKhI*!D4}LFxG9{gbxeX z7z~b;xk6A4C-{DZ$fEy1w^2IMFF*ttKaE|ev2wYSJNO<0`i%}BrGPHjc4Z4f-`i29Xi9EIYu6O z7FT95ak`Ru$t!2?y4b&Ts!bFRC*gq!6vcpwufFSZ$B&I9ss>F&_ z#*~hPE2cvg4FhoQN65)fj;SLG7D>KfPI=Kh>E)ghld+E};#rVFxF{}VOya^L>W2M3QpCtg z3F4X64|Vt}eEG5iq=iIYwD3+haUn`=(CvbND~QDbMY7S2g-Oe7yT@R3%rsZzFB+SM zm=Kn-k&wEE!dwFh!Jg@7ob%dNokm+{*@Vp&!F-@hi|j(=wy8sibCNctlA!Dr4k92j z|792QM=Po>m-DTcbqUKg-^s>x`5Mmp+H24RfvJSWSe9qM)#p+)-UuWIkpF7DxMfPX zPRC5dKj#l<(Kl|~yng-0_S~CyET`t?sY4%I*vznS?9-~aj0P>^vohy=bE3`!*{&R*F`D5;QYuy<#*7x_&zMK=leO`*b@q4c-oph5QVA8V^Y32Z8^oX)lmT|; z;eXKig5LE5U!%WbAc?e)DZRt)K^ZT#si4A_R02d~-Wni=DmITc)>=L}GZw(PLgv;A zBweD}i~;3(9nc!ZT(UCe*`7VSoW>D=vbWXsT^wR zI52mZTtXY(!E--r0aH%G!)MivSXACb8sn3Qut@WlS4U5y%P3q-JV)neV^R@WdS+ir zkLD)rbsucfRlyMz&T@AR1s-BU*@_bkDiBy21eV|EQ*PSiyv0Jk8@AlEVbk?aNL@DX zKq1r9NFG6&KfDYXexM{=wU>*)MnEOa!xg4R zO1<8nCHG|HU(skl{E$CIfR2Vjb^x;)L{1YvkCQy6s36K?zqraZs_tfVG?s~IZWioW zGlzlLUI<^lhkN;v6K9Re$h8O!W73sH#0s;E^5I26h&-Md043!cyro56rkHKQiC}oA z>R0B9P6AMl1r!w1DU&$rDJKmj0spHM!OQCTo1iQ~_pmfi7r2Q--i1wKnR1HBq8_IH zHG1`qDA?#QX^o5VWlr0^ca9Ei>#l{KOrqDV{A7XrRV{dAp#^-N=8|_hhA9T8k(qhM zL9Bu9o2F~*l!=IyV*H6mS2&5Nu1j{?*Fe~KpCi*LPqu`C>87o4ka;Y#Q5nO?Vj(nG z|I*898PSJHQD$N%wkG|GVd6CD!bfgEiM{jbLy$h z;wDK}8cxJ38LJpWdy3CU3J4IzIt@MwD4m zLDWTHhzu72>i@hMG2o4bDVpwiK&x2Pc_JC&CVeV=go6NjU)W((nzx1YYJS`oLiQLm z=nS{C5ARZ9S6|&9&2UE?#RS~n#SYN8X4|$MhST0S^TxTi-`u`^$DX_H z5p&pxGIbF4n9LBC6Ul)|7P)qfV;V7OYu|pijd5?f>82xx55KzqRY!+n+U%rnxa|KC# z(PeJvjnikYyVje4>rC?j&3+Dp&BGGPd>QkzWXf& z^U~s}Q^&!)b^8uc#HIJ&LlO|cEDeK#;4-IWjMIIUYpmP%5~Z+J12U$)M9@yGVonq{ zx*H!vnaM(>h&ulTrCgiD#pWa~l?VOQo;g=rO7)vv)i#u)8iq8crwSsX_95#R{Rju8 zSKdvza=Y}bHDC4;Tq3|IhaL?rUCbQ~%BpuyNJwo-f@+E#CvalBtekA`XMjBp058dh zOn?e*Y$;PBjRVOVBfiAGhU_YIv<<6xS`rgS#T(iS?P9XXN}ajs@`t(M1j&n%o9T7PL&9EHnO0oeU#Iia5`=|*2%kV6kvp3Tqb%mGaV8rcJH5srcjwBat|gEDS0 z9~__+;UxpDK0UMz*^EufD|aMC53Xtw+{JjH<~uV>ISDzcf*GjgT7KmZCw1i^dIxMZ zOA+X#kHh$x!mzGoTLS0qq8Xu5Nj1sE@SU@W6;mCa4KmVm)6tUT+ zg3x~YpZ&AR5amDq(kKl*XY;cL29ulmWK{0T_IyJ{6VoICff%|d_mSAW?e zFdIVv@=yQCKUPdtJbdKnW1ssB+WdI@3!neu=N~g)$lhq!wi1~`^F)-*pT<2h;&jPE13>)i%CvTir3YSj+4a>x$5YTuriC6?LWM7*q|-A_gWH-3|dm zx!ReB0%T#(G7XfiTvK(4jTA3i(5ci&aVqp=$+@SuF5i?+9aBw89hK*qNt|03giT$9 zDcyisPmADYULKZ`JBnMkBpP^Ixsi|$6|J#tf+F|IfjFQP~qX*@XEk0~gH zc4)3rIdjQjR7Sl*WKS~L?1Tz@#?b0D&hoqYmhDbg)LI-pe28$>zFTj*-K*>`zx>kc z$Bxq4(3YH5$gDw+oR}|?NJl+h8Tp6;3DuDZ?n4;sH;ZBc9pjFal2gbbtTT>6WMtcnFf8L^CKbj zC$omBuGEN%B2v}x-F@Y*fN$(4OhZIZ0yMz-zLL*P2T6)ON zTK808_Y{BCRoiaawtMehXDnjj_1BJ`K51$1TIUk(xP6!PCudK;A@Yg4AZIhk6Pc1rdskegpm4hlvYo_K z*by8Qk3%6OH&j%#0Zn7Nsv8fLpF*8!oq8<#%}ce+TqAm+0PSofqWDcY;vk~O2*Cry zLpZ9=YK6FGDZls;>IbV717kj8&%dbaV{m}t-I1X!^H5urkX2CO9ny~U6$t6Cjo=LB(`i7GPxU^f< zli{!`lbYjF8I9j`g@!P3ZPLUOG6!VxlXb-f%xqc)~ZvU%q9>&P|(d*m2t~hsvIN>#YLZB(vs%9m{%U>{>#^rzEIH zdeob;Bnhp?vQ~ix@ilE^zizC$xjARGw&Q{=7)FBeJ`&B`+d8ZE4@?YRIDg^l_ujRw zEUl~h3HQ?b>n^ph`kHkcu3qb%hKWm(L88yfYn9kwt=dKB>3I2>P}UY(^q08Gok-wo z_A?eF87fCkkw;4*ZO1S)l{FjfE2vVH8@l~m3@3D|31tN~^BA*X>3%Mjl1$pHBwf=n z_T@?8v=53@Et4^}PTojqkyO7n6?cVkVNd3T7P-&@CC0PiN$$%}v~-vGk^?a9OPBG> zY-X_DjyA&qsS;f=PEElrR`5A@dAxwZEYpS!n>SqVmELRKKKIV?*N=(9JwI~rAkTU8 zE!#HVaQ(>>Cyi-Y3kXQUs-1$o=~PrMkZ?+@nFzuLdfcHaoP)0FlM)ro6-@nd*E)n5 zyNqRd7a8YN`IE?v&e6{zhf>s~Iae4ZgC#)`4Z#og!nmv;f3OgdJcpVES~ASO@)(V3 zB+SL$&=;h_^OQc5VhfK!D+*H9qkMkKO`xvuXAjv&BEXjvzeZn3$-;wNo8BSE`d4-5 z#*qf9LXEPZW&96iCL&i32Svz3u*X)y3_i%qyx5PYb-z1_VT!*yWj)gef!-rr%#_fb4FSGRK9n8I6IyKLm2VDeIJaJEEbZG zk^{EN(bsQ#SxI%lSpMw8qA5siOW`RsBUm4nzw)4F1`mMi&MgZ0FweK%EIRv5sY7-t zwd91?n)K8&lrJQ&e)Ft(o^xBb-Eza08{0&(x7(_$J}4FT5eThM^S{VpkSsE}!hc3U z8mWBT0O<5YK-gRepXcsrp~iGEh(<4>DO{1ZR9Xu#R>FXhb#Y~qhG5)N9$yEY1lP9T zQeLK7HvMmoh^EG3WUh8BS0pc$aI6p?%Y}~7?1DOL21=tIvTQd|@G#JWBPI7S@%0!A ztR+uHsO(%)DvYB+uR6gI*kQpVQi_FadfXPvPAO%QIBxM}$vFjiqVyhn)=P37uF zpFDB=)QRJ4m14=y>Fx$#&Gy=v-R@LZ-Pv^ZjO8{?JA~#!7A%8&#Z5@U#iYktqD}}% zkvoQDaFviBpO48kJSjLS$*0V*sc_{hAhpL7*RgaUVp;N{?x_G02QrxhJ^=EG15MG& zcoejuM2(P;JDdkA4s?j15&lg!^?HyNWdIOXr^hgU@6u;jD4eBi<3YITx6ZtI?AYs_ zW7>Aomqk?Xy!?%A+qU0$<4uMStz)puDkRiGN2w^e_(8q zqEsjmF=}h$Ke&A6)M+jI`s=SxSSdXRJ#066A>qTASTM(oy+eQneGtC-%&C*-FTAs5 z%jRpY-MDt$M$K^6QusCcN@NrST&;Cv6|7hN=1M16pld#b0%gXF%ym+h?xX1iFZ-;S zbIKBUGH=3nl%V8d8U0jjA-j7%w^|ey?2MVG$H5mA`ZXRiAp(+h5Wo-ZCz@kPZoS|60hC_Aj|IgT+#cGyahkdX44mDSGS9P=JnN2puSsWx;ktoZuV%e5u zCx(Ln0h|{H4qzYyG9ZH=f*=SGJIL@5KR8AbAutdrvXMlxC0U|KQ4~#5v^cW|cC(w^ zRbBIR@2xSv|2nrw(c+*}-S_*xbM{_)&1>zo_c_O}+zLvTBtCx+`E4(gx~028uCqVo zivq_WUziG8jFWZsbZlL7yd$#k)nVug%yNEl8fHM|@fXgWJ@v*Z8w8Tkon4nMUbuX@ zrFn0!I(bq~{=jfwzmQlaX;x1C3}*}*oDfb2Q%eF$BK^^ZyXb;jhD!j&w#cMi{uxfi z5lmE!Sae8ap*Su#K{i0FKx0a%hS4F}pRY}KfUpos~pQ5Q~X9_O|5C82XVB^g6KD@%zPr7qn){tQXWu+?`qU}6W-oEj){d87dg=J_%O_5r z%&i^R-#eb?-rs@?7wtwas+Laf#yFrA2@E+#H(Y~2imG;Qd#5HZ9xlB8+BeUhd2@aL!PV7uv5=hf1|sE%br?CN`b8xsz*Wgh9av+2eI|jT zj-!Fcd=?Krk8t z^=*#6CZ_q!zz1pWyD&gEbDV%g&al%r2IH+@ZN|B`;rWV&FFts^<$RoQPgrMhmrGu)4FSKx}T9 zUa)h5DvXr}xawhAx8L~YYb?SYg*GWJF9U`E8dDF$!O~y<%}<1lriCm*V%Y85-N$hC z+QrLP+J5Do@cR?qi@eriWv;c95N9sqURQ!NsCe$NRooDh9iM-s%S#evh>3jp&~h< zq*P{pf$#JvM^3-*!}V#{km=A}1MJ%ZMK=S=50Hvvtp}j%!5jB->&6x>*t>dlKxz|6 z|1z0^P4NQWD2la{H4avBIbnc=`)LN6D8n3sn{OG`j9tU{s@-sWoEk{9W7PCQ}%VUjUl1bA#UlBNA zqCO$*RJ2(bN^U~EN8k@*?&JK@fT?~?%A}e;plvV}{YFxBiB3#viX``dza6Wz(YnNl2 zVcWmH>xXKYH^#GY6JJK;Wg|QGRj>p?@9#*k66744jLn>~7Iew!A3Tm03&hcbLW0T; zgw5Z*{CuHqWSBT6{ul2ZtXdIo$DK29k_6PoxWhrX;GOTWa?YlAq3zf3b<%>&H7(2n zeZe4L3oTSZz(@iV*z>r$TLA-YFlbd}9Q{xif8%3;rVi@@30-NlswgB z5nfCxIYNMMjd_<^?AZQ(S4XvU(fNSZ(f0sPCtn?O4A#NZJ1@}H413x`BS~v#BRZ1H zuj(CGEsf{H4-NdR>!`(&QZx!OZ@09{$L5(@67ryS1GV`n-I)Hmjiuude+Cu9>fI3b z!<|2O)}Uy@fYHaM9L9t%?F<#_pa(2c!Ptb;3hgZD`o`AfE7$g|>`?=QfJ<_O4f{IX z9lWv+M!As2Nl#|*891~PnWBaHsv2aJP52ojx^eaDW&7-{sOcV}6Z5mOF_1wZZlq>@ zOpze{uQ3{z_&9G+FlU=u4gj8cq4%uRA#<9I?4mCY4>K1&hjw!q13QKSqYMk8VT{mK z7L1TZy>P20x7v8SXP<{Sw?I4C!YQ2x4umu|r43g7Cp0ghM^p`mBBLbgR)s=hx-OJ~ zIVTs>PHfX4g2Ek-I~pLB8|8Ct$m?9N8mLc7LPvb35d^9kAQm@k;X1ryjPG3l(S*k{ zatFFgT^q^t5rU)MWYnmq@iXQccbhC@ z1jMba?GO0{HLOdQ2jQ{hnuS;Y>ophPINqzLMjMd+ z3>#xk+>W4^H0y3ru4h%v(@rd*VVcI`i0J9h0&~(islK zVtB#4d3y#}3dPRxAx(*>Flq{C@K{_COVE{_FkgD-M1E%$6?1a2runC_D;88l$aKb( z!arvf=F?2~m=+ZcV`zWFFvf*_ci4G*Xn$B?`aUgy6E84uq4p+PUKL5XRCOf zb82Gi@Nfcjs7B-H#&qgTO`{ImCNe<>B$j=T3#zm4k~Yx~x^`IAGkC%1luivKh3G!ER# zaz(HFAkA2)0!-ZzG$>Q7RHg~jXfp>riXNyK6}*tC7@P6;cQMum(4gH#4c+%}$e0@Z zZgpYWOJjyPT^fbXEC;;ZB+Z5cFbO`CC;@Nls@BjO&>Hlmv?mdT0f2r0=16f^_vrO6t6RK+s8i$@ zhBd+4s2iMTpgM<6RHH0DWTYwlL~B(CW5kVADp~hmX&(YYDe3#)?>tU;G;k+^?@wCP ztszd1>4#?YCe!I3I|j0;qwFqhMh_O{!F z_GN|%KfQ8i9IT)ZqyUws#bq4GDI+i9VV8w|yUxmm(XFmvouM(Ik@f0a?sWvFBP&;L zJAL|`I?u7=hY6N`;6{{d=`T3VwIXAQmJOE}VzD;5xa3Qoo#f(Pej|!!8gM41}L0Sd*G!(O8Y~vq25RtW>x=NQ69tm_H)5C9N>!5lgzs8%|*PB&LOJ`8XPx zyp~WJf^n%6Jf*iz?a`YcL!*s5&L_38m&onJs8j&^vY1cGu_#!l3Cw|Axfop*7Zh*u z#v_8La5xn~Lq3hzc7t{4^l~8~#gbI=qERyNhlk-`|53mG zXalHIV-$Hgx-r5`M&qPLY!MqdU9CCJgl|n%Rc_wgqGp4hi`-G_SGyT*RJR`l3!6rb zNp+v0S`VcFn502{Cg{Ym4#vm*>{(k|QQ`%3a9&6Yl=h4;(x<0C0s&X+R1(}i-IpJD zfPo*JZ4YMv8NKz1&SCGrlwb{Xm+bSqvD^J470JsrmtnvAXxn|3izDfU4KdpdRhBtc z?KPt>SpZ7oh))bYfBwAk+T)Kta^K1OjvhWtr!`5alUF4|j2Jwfjk{@B+zCwjm8qB~ z4af1y>A}Ls(zj7clLu@xHWc z`j<~(6Jv`DqcqOm*+F;&Gqe`2Ob%Xdl6V(16Tf!4jFqbW^yq4&7_|@I2U;Wg-cd&% zOC6q9@I5oCbsTHd?Wj=xv3M(A5Cejr1|n34?|#h)+iYJb)ezy!a5MK>^fFF58Kbjv zqme?>EV$KWp>obcDWY=L`N4&a^Ed3KZE%K#3nWA}%Yl^^wP6tff$&Vxg!76w&ea>2 zvayo91*KwD6rs(_swMyjH|9kqh&uYj%Q*w^@Lb58mDz<4fVryMXh#`Rjg#HL;!o!) z5SYL16o%*UqKc%xXae-Mc`_kZ=b>A^ zHY(zOnp|lim<(YOrtM#CV6epjbI^c4u#k{Yq@^Z0>~eeHts9uBO2N}DS_c}mPz{-I zXlb|rJ5;FU^+uX9o)W)8QJdT3Tsr+T+OC1Xw+Wk0X#BlaX%lLwr73FG-zPk=Nm&*3-uW7iUd6IP%t-y3UxNK zYuN;Od&EU6+fJL(A$FO542MC$yW|vmsX>fPOvBK0pkWe+V*r*D49opO5nR%trP=)G zk2VN_@pQ{};F`kLRzKR6BZ`7Bo{6}VO^}xT43W7Ewr4+S_o0 z?cyR_OpQq-1fVdqcXl=&@yT#-!OzATOGx{t*EZ^8L|eYqVEhlME>b`d2KlG^^uv?> z4aZZDPV$7~@la}fl3Ds1&gE7T$4MrL!d_0eX5%`Ds=f?<6lv6kY#R$X01WaaJwq8W z=I3EBOyD8o_VOLTkw{XMWDYMTT^i>Q(AkGN?QOEG{J*p}R$#}yYooz}zwc4E2y|g% zq(?Gr7p~6tQk6wL;NvT3Y>LIzcT@??r2!Ew5Oi=7tQ16M-uigbH7Km`#D$Tz$W^5f zsD(_d@`XAtCnKtmmsFj{NneJX{DU@NhH9L`tNx7X)qH_~w_^@nj%g{9I9M_bNz*nS zjFgHUI@QnhCV@KSx^c`vO;;+uEL5OLJ5*`wB#YJ0aGCii(_-%yaG)9*+d^D2zako_ zBat0fu3mog^cyd|^zs|0P94~9zq|L)yKYMv_QozbNxX}pKj_e% zW(fpzW6guysjIycfew`8J1SWCv<+}FLxT*r6Y~!mXt^F*u@Lkj-dQ(xTbI4Hdhj~C zA1xp%8{f{Kzwo~Iy!(gV@eUi@W9As=(qDISO6r4930{kOZF&VD*(^dO)BYQuJ3289 zQhGfIDJp%8+UaO`JC1=$28#>i#m13|6KSU1%HvkJo`pleFdK1b5Gw=&HnD-0~mG#?RlNwX4>4100b6ST&k<^p1vPQ^TbmyrfwdpL^p!$xE-3l@GQ zqsY=bKG>1!yN*5Meh7q&j*-{)$uc=|*OJ;ev>|H%V-q!-yVHjkEJfGWp_chyGOz_-`xjP~8vHa!z$3fA4GSRaAa$y&+vJ$m5Qzy8A(=#h-UJ&B(`^D0S+4H!5C zJz{1`Y3DpxRU2FQ&G7c_TQ?ewF$xQXTe(n4 zs#2eQ>w8C$c{&e$aMw>ST}Z^;Et?m^0D~ebrA$k5X}(78h}+KvCO!z9@*x}G6O{&x zVmttx&_Y=Dy^b2Br6cs1PniaN=gdIx)_VzTfL&AMqHV6@9aGilrU$g#xM7of(_k-v zAFbF{jI=r&IiKNJ8@HjvraF(LTo45rGPL$YvyF2KK9NWV{ZjHt&U%lc6&%>u#HX# zEe}$mE#+{2(nB3J29xS68AtcX$Ex2t9AQR7^x;zRD zk1;RyvZ|TjHg2SV2>d9EKEaH6Xn*>tANlZyUwHn7kACc9?q@u7_)rQr^dQ5du-4W} z;CL@47Zc+^6pw@#Cb|BDjG4WARY#lFGQ4%wAUzwj6S!V^zChC3UZG((CYipA-XeP$41ekYp5nQ~LC zI5KAkpSddLwDZo1A+xb#`ZnK-UY@Oh^?2_-5uA^SK2*Qjkc1mCc3w=jWTV&=8P0?6 z$L$UHv@((m*pvv_=vj+^$zc7kt@Fm6Hve{E7>(Wb2d1)4ZmYKKHFTqriSfbrpKAzO zfNX6xiQ&_ERTM8Kl>5V+DRN-KqVO}3-sJ)+ml1Wqs4&S0>F~&*=jv{I%yA84NU^Hv z9VCzdL#(KEW;T2@UQ=OossD|mp9y{4^81dWa^w2nh`Xm&JOdg9fhfXum%vOGF~rxH z>ZCCRZ_<)&xQhNp$zULr!T2DlzKo~PQ{>Lr+J0Nng45^I4Ule(2eA?qgz@s}$8cpq zz+XpP?*F?{^Eu>mH*dQ0V2GDUZSCAuO&-kc7~okf!?EOFuNaG1 zng$eJVK80t#hx&|7|5QLHMh}b9J5g@PVQ6fxN~E(2lv5qHaAb*rI}04#`3?uvdOXN zTXqxe@|uA*nk>$HgfeOr3krM2otc5Vx^A=+mTrKoNv_LtWeYacpi;D29ZAy}_!}?e z+iGfHdx$F9*$B6}dDTxj;AFndo{59F(R;^ymT$V{Eh}yco}jC%gT9K3Tt?bRE%ox* z+0p21XPNcQ(B0oK-J#w7OMkZ|-n_rX8W>G)9;4|FzNH%}TWa4Pkzg0*HI{E-@7C5W zCZL$aV__ib)BLgS{J30XDr&k74n_K)HMKx`;oLx-Tq-UQs$|dx%1uv|8>=_&@Ii^M zE$wM95%1yiv+I26aDE#)ogZr>Q2 za^5Fe;TFJ!)AF08ST&ObC&2-B8J(09pCcEvv*)>KKidtR0Siy$TJ8dbyz!dziWIl+ zY`TMF4lDqWE@?N4oA*Eko=byV4fY(%h{10oVmGqw+p~vbfv}T9V-tyC>rT%C4zbec zQIDBC17qkKk2~_5JjfWx?(gyQ4Fo?8oQGZai_9_O>zHp`M2V{*Mg;kgvN5FVV$7ul zO$G%1Mf4nNH7B9~ikFBpu`czPI0AAKbU?zTtmW=OW923(p>7@HJw>JEWCS_0-5SG( z71XI29UdmhI^Mpsm1~4GE@3i_qdaj}yQv&_x-yqy8ky7vBKJu3C<8y1I`Oy08VlOB z=Q^J@NpD1}U)`hmBc{3pNh z+h2SB1@8+%M416br9GGfM#~Nqb?{kaYLtHU=TC5O1=`{R$R1;3FC!&L*9O4d3f^}B zN;Jksv33d3??3sGqwlasDJ|N&XW!~R#u42;0qdp>BLCW7{)NXMf6OlbxidDSUG5c+ ze;Gl<$3!7bC)~&NnRE2LA2Ums(1^1fTb950n(iFpFZ7$3!{_GXata`th2+-@czKX$ z63@!Aha1L9L0a;o2T-O-*XaZWS0D0*jv1m%uIT~Q%5ph|12V}mtoVSm`hdX7_ilK>E?$Az#Qt{U5 zDTnQrJPJor8*v#%Z zdniII+8Mw0F!3GNb{7=V?a(N2@YLn60E>)*I*MfIZcj5r9G%S1Ck{)N8fG|vX|Z_c zPIm@v8|<`q226@4uPTFrpFGJM7vA%suPN!2lH@@4dk6%f!=v#dq# z>QRBiy8JtQFf^6z6|iXX&v|CiZel#NiC%a0BI7FBMcy(+!Nj&1rO>oVFZO_HNT%B( zUW3vsSV9I$j81pd7*6QxWWR!g;S`H<4hu6wV-17*!_Ndl@qkYX{4NQ{ zT&=x9on*WOigyNDk~u$vK7(lNq|PA6t{po)D4XB2v{G7D3YdcW>pC<%PE}Zu&rqPIKh|mm(MZnGAtZ(ao&qGeC2>h=qhC=^*1We%ISn4E>1YDNy^+=A zdE?UQNiOJy+1Ge!egIu~rR5L{h_TYWzEI*ur@HgA$OwM5{Q2iESh0#8&8(UOIzNG;tF;bWj_cVj`U)r9n^!U1JS}b}` z^^p)I6qBM{8F&S9b3akA{MrcqE4goyM#%+AkLLPwKl?Mk^vl0;`pvUY!D^S*_pkM! zK3(GR-OVT1GwvkhJGMWTAK&Zm${x>K*p&MBu;>Fvaw36@`Mzct--fddu=*!lH@3Fi z0hogtDb(`29N&oqg!6ewjcM`KLk=%rSzXz0IHvXHnKM87;SW9b=%ca#lt630Y!@jf zTVhCpQIt*_inhovG)R4``Oiq5asV6p6xTQZ`zH$J(48#t0x#tx1#0a83JJxvGy=$8 zNJm99b21wP9d;j#D()`v+svOaXg$+#M4UPcz#`mpUzzv(6&kxeITQSrCYJ zaIzprYZyk9Wobipp-~WuF&RnePW9ogE-TjAKj&4UVp4}R7#d*uqm9Fl-2MD?QjXz! zKGXzI9$J=Gcj%k;FFyx|*4GvnrV+ldLszLER>a@&cm|wtTbPLF7Kz4t%69>U zn=YCvH_b#!6ld*>m+Bi)*TyY)%+OiFvW_M<>UPYy0dl17+DRpHtm7Q#Z#;tef}|X7 zYs=dh`L#Q?aT^bCE?(3DW!iTp@z0f-DvDh1)u3|UT^?wTxo!zeQ2{8qO=_|6j`}uN)&0rF!iz2SNxmhOFM4Ul%qu`D zx`fmni{7Lrc$R+CYdIg?1y9dM(`}Dj>?{8nCsk ztqleVF%9r-PdRQHH~Q>cPTSZNmh9Q9CeVu_d#`%BL4HFQ^}N_PETs>G0zGLYNdl$l z5^yd%I`Sc-80C0Qldtm$G?lLAznkCs2Adc-zG?j~1NI`cwtUL7}QYoJE27@dhxHagrot8K?AZ`1W8!p{)& z7@#@BnA@a2PGUMTIC~_c1_B-yyfDr1pgB2UZ_^&Svk1$WvILFuJ`K?>)qEkHVK_@5 znDa5INH;7BJt)uJ3>C}cAs{+^>(0mg+Ef|pHn|ak>feq6AqGv^Bap6(FQt9k(hnHU z`RS@=ekYCwpaTu(h%`CT?7VCQu!A0Hn&no9!-g5fSyWLNnaLaxjLz^#3@pDoCWh7H z7}EzpDw^L&1GyTYv2iiPT*c14rhHmR&RojE4X0>2X`-@aVS85F2e+_epuCJqhd0F? zI(X=(f9fax{{Q}qo+kgqgC|^dC(yzDs{2^gO$mZ4v8Sw2m6t{DeD`PhJ}180AER>} z%mC(;dD57=4v-$z_3guGcYowCg@>uMVbasj0Y)CK>Tt5g;Qs8JsO z>+5@PNINpSCRVfzY*9vt<)5pC?Ap5zQlum8u~^eK8V?T5u3g)6TXK$JDR1!ZnDV$p z)P4{KM{J-T9I4WkM7u33T^tLCJjDGbqLtM^W@)1=d8sru1&3sgW<0@&I7e!@AtW=~ zzw;1K3qN!#sPEprp82(aLZCY14}UjE0$STRQ8w8MozaG%E+n8bq1ue?WkPW+j}|_) zuTf&;JN#|*e0s4{1801Y%VfxI!|QEmJ14@w1> zgcb0T3|Yod~C0{pNjzVVlu9ejZBnpH|qJ#WG6Nm{n;zmh^ChZeOB5U+*{QGPb zm0YNvOOT<0gIJCkE=Hu9B%)>HH=!C7gK}pzzUA+RdTr_pTdf*4*4^5_6xiZ2w|$C}D#2wDnOTOG0{Fr;u(H*BwAj#EA!vwRS-+v|G*{dIp>gG2 z#i6{Loi{}kpc8uecP#NMF=sEW{5VCjtOiZE4L+h~9j6>?I8c{C!!-0J_*juL476DS zFb)pZ88t;WLG5nh~C< zci02(3s}JFX?S0qz)L?Vo-Ym693aDlo-UY1C^|-k%vf$#lZKwd5KxZo8eptzW(L5f zMWe=}Gx>9*EV{us{B*bkK42w_gLWs=lNv0e02m*~Q{|>3%80dd!ADYyje!d@3f9du zCDxEIqR%!=VXZF1?%4%k!$TDZO%12xL_$-|MFLua*8-3aK&ZihwHa#&E0+1>5;IJa9l|k2^d;8lgqj=StvZuGOZEUnPj~~Xi+@HCoc_+Q*!D7D;DdF4tu_(U^^xA5Uu=(V+*PU{T6b1X|8D@<7XrrNi5^oH03A7_mcw zg*!pe4g3YF5#zHnZN916=b0i%9oPYcJQARWZ?qcX3t}L`fxq)tUtXTif*U@OfXW_+ z+ux%h00t{Ji$zyB=XovCG%5p znQlm$gPR*Am)4jx$Vi8ea(BISHVHNa>7yQ&Q(P47s>f(waKbKSp-zk*S5UFw=pmw` zOTf+clw{Msx}f2TUx+n&E*m+yZ=XBWJX^pX@r#*GP0rMQ0(Dl+1G9~1t2V$Z$mfkm zM)I@T6a&n>i@{K35O$#;3&YL%W@XJxdQhR`&1e7P8C*lle zckfwEpeac?nbEVC$pdiA(quE^+1#T$rr`;pd;JK-B=>#x3wVlgON6{qH8z$7cgMAj zYeeR&?w%0nFNH7!gF*!-;c7aPOzQ%2F^>!qTIHWyaH?B1a-j+i@J!(4a@mw$rovVz zj1+3_ab7obN%<#FE{O?X!=&?|XZRsFqYqI_Vuw3yfg-lRw;kG3EnPI7`ExI5N4>t| zvLa_Nq%S3r;g5qeQ5@-%mXU8HKxya&YH%3kOj3D?{!N5{CM3-g$kfsqp!?%Gqv!h9~;?-R#XP5@f zc#|B&MxYJYu&z+fG=TqU|3)b$0%>LnQt&oa82tQ2Na|FSLD3}l0Lr;z34mEgd;Hpa z;3uBwOJwO-PF|ZJ`v9a41-S7Zs?co5I9%oAA$1(I^&I7F=#YU!47043^-EGw()4q@ z6K0|eWYZStX~52mg%)Q}XP_-wn?@+D0gtMSu!f1!jS;oOeHDd4HAlXV3xSX^ZzHM3&a z5U{Q(n^LdJ%VLcoU7gHZ@Nto`^CjXHhAkRyh>Mpll5vtE?xHg|WPyKs>}h}InP-0b zPyf`XKl!_p{}&OAQ!bAw1~D^@kXR9o;{*EogAN?j1b-zpkH*Q7NhEF?GmLqix%Yn8>5e@Ykz4w+q`qbWv5`KzN&B zjK^Gd7x9mbSoWYzGNi#YhHa8XS(;~L<4}^+u|&R2i&ZQp7!(~f(*_Pa0uPAFLZg## zhfRuD4KJkn*P#m}L1ffp@J7fE<4n@mAZ4XQiHwFpZJ9xCXO>I(3SA+rp<}+(7%939 zC#YhachxW;F>l{FhS*B|XI$Y6R4B(p64JFR2Ml8v9B*uG%AuMWoUu z@ufUoU8gEA{0zZCxQeK#$WqJ2;kIxL6GmuMyE2JeG(oOY8i(HTMp&U|Z9MYs+EoazuI(?M zlL^_VTaB=FNiH@94y)q^_W6DL_SH)l_pPpQ&`Sb6djonrlJN*}B2nVlwRc^IMRnOr zy2J`8*oaKCeS5ocjEAH4R3DlMwDyaGz1?y}c1{)N21b@z_F`5_`CCinC4Ab-u(<&X z`6TqsWfP#LIqr_Z$-uraz?4LCj3=8nuWtx@M#oHi?Y_UU(0qDkk+O2)R!s}o__EuJ ze-3Y5_v%KI2gp{KHdsO&BQhA#MPrp?P9rNz(v{eYXAzpE#o_ceyK5MJ31{h#k4b_J zmgt5r^cp@wo)A7&DY_nx&;z^>p8hg#0Y zDJRpNW_PA{6hJqt`(;AWfiNNiJi_1)2R6HV!wNF%C?m;`Nx_8_Bb}FSrdp_2gr3dB zJtvCFxSE_VZ8ubEGt#YbdxjN;+I3 zG9w3DQ?jmaNl8Yb*V1y))rDwa{nW~zRCFeC9R7GvK=M&ZQgr}#rtU@Bw>RmqGsWF( zP5Kg)aV+V6_~C8myt{@rR%gl|PI)mb45>Q;@ojRZ?bOO>`n!vo6Qzu!koeKQ_(75c zvCAW(GQ;wLCfII6nYn8)L9_a7Bf#~-rF+-h2xU4tD-|rtzSB{v=26C7Ti?&9p~-~X zyRmEMg^QQo{HaazVBfMv1!fm`VBKj zA+Y=4VnY4({LTdHE=*js^tHCKZ%d)0$=UZibXxi?IQ5fh5d%ryP6MuPTwh(?l11)c z-w*dU&z|uVdx0l|@h%P;TJYXOZpid<$Mn_CSySDWm2>A#pF4knpsQ*q5~K%UE_bMk zh19KZNCmNl!ud_(I0bFS_TwC}yTpPLtAVB^OP~b9ye`s{*gCdNB(XUuSGHlGzUCt7 zDHzK%F0;(_n2w8f%_PPFWnklZM>5JH@rDP@Fv_>E7XM4UC`ZmE-`zbhkY?e_bYt9l zn1|SSPnu5Go$=F24JU&v8*pM;2Ffvcj6v!;Vxu6z!6iU^!Z4;GHdve{!W_+;qgt;l z7tC;8upa?83&&g5F#OC0EjQs|jxA-fMZ-hh>l+-FDd`ywhjuQtQVln(JAJQ1w8Ybb z!tfiyW5g57V}=~1m0nOmE2uPIHTEkZTMU?#mG zjfO^4)@*PW1@0Rz%3p4HpMycewOLVA%!Vzd-q4{`{*hu0$#)?{7-DsG+Iw-Ty2G}K zbsC)OAJqb+co3iSWXeCZR{Id7S5-K=-IA<*A089-XkCbQ2aS^{Nc*!Y6JnQfy{*}V z!Rwl&G$b?*#2(mEXT=*f1usL#poLo|Gj3VDa>W@&hqZ}d)&@}MhkTBVlPMBb)>=Vp z+{NdnPM|qbvna;tZDV9B>4+)kUd1ZUK@}gXU73+|gH?-1S)6_KtS60u$<5x#1Q=~Z zg?$AA#I>s%;O<+A31eu26U36F5vzgRy6Npe*Egk3ekhg2G4HGPMnzhRYVE7aw0@DR z5v-{_8g`$*h^J%>64NPqqyWv$wk;$^O?-k$vTnK}<(;TZ2jp@4D0zC7DXMy!BZ7e67_$9q=H%9L&@Lho3y)=+Q5yM^0$<3W}az7 zxnMIrXp_aXA0-N)+?+=H5;TS#EQCcz{zxxg3)Khhy>UFMh~l* z6*%i$lgR{J0If;pj8T0twlAax9;eh9d}})0?t*Dk)s{cuClgOPWatq^nyN1?%6fua z%qn4$j&-J!P1t zRVx!%%(~>bc%HF!`)*qvZ}GESHA$HXU^|%-!(uS6eB+g~=g#ciJZv!lE#$yTMJ-57+z^4a-^g)cY%sg9K}+5-dMir zoI?#Djis8|eS+)`BfO;K9yC5*CYdmZ|Fq#Inkuo}wnvZ4A<|to#*_yG>6Lu>zsc1Q zWw3KCUq9Z{xPbJR<=W9~A1-)XjXPCH)c}_uLIO2_#KW z*PD1lNmfaiSaBY5MNPaY{l_l#zZLlvjP~xX(h%z=_yNc*uCA0|VIv)sO=d_XDz1a-Y1%1x1Pz4CQu#O6{@PO&cgi!sIttq+p?{>)vEx*^`}c5ijlEl!6ETauAdf@ zFWols9e2`{m5@GFOYur&$_}wIbB5ehVe>A#6u=B&=g;np2qaCxKB*!;)&? z(Qv0njcB$|jkAWMF@&2tt$}u{Jd9%nt=mc4W#lH$`AsLK zBQ#U3pZ>xS8}9~okkT7$bt)xvf=_4~sd7s$bV(}Qo2Dc_tYl^70F#xyxCA(x$jSI~ z`_Ag>8fFUE&`hfe5?MAu7k7EZ#s9O=9xcmTS{`Xqf~Dnb5x0|(&<#pE((yaj(GQ$= zJyqFl4w5U7A|uL>E?B2&J(0*}CZF^SF7Yb2(>uc*R5J%gNA45LoJE=&)v2*iVQ3;G ziy^HZw|2MxNiWW1@9lj%_At-Z-H6Au-Z+cTZ+-OGnj#q5n*K?uVB9oKBSZi@Jqqwzx{l5E85KZP7!J{H9D2WV$MHXr} z|M~MLI3(O^?m#MH-MGAQU1qqi9AFNcqrTfAQV9d?Vj3$g^nK6cJ1)^uvLYuO2~(Tc z_^BN`ync3NZND7+;)M%M0fZoWw0FfzZ{K+RmGkFMA3JvP#K{NcG}d64ie&4B7hc%B zwsrXMQGScH{5VwY)wX-|DT znv6WR#a+Z#8ap}iOr*@{cq<)&O77@MZMuP|*p&is@p{e#JY$AlPazuj3V#bs(x;WC zP^qI5k$`5ghT#=Z^F-+>9FvR@G9dFwqgSE)XdS9Y(6hs=xn zF-{(T(k+4shc<{HsR|80nOm4Vaz5oBDnmq&kKj+Zo~D&{QE>L*3oZDTYE$Q&2vEEB z!GwU2#{c?`ww7Y^N8N)>wb&`lp$egdnN)fI2HGb8Ta6ZFdo=K}{&*AP(zKWv{?vxx z(xaVs=vl*2S2*&^aqmWxxNe1roT7_va?2g7=-AxaURahANiynb!> z+CUK0SSmdds+xrxg0dmN`L@V}Sn{G{WFR?X64+?ZfSzCulR+p&fDbkuKD__%;WZo> z9p3A)7X)5Zy>)Zz=B@Sh@;Vr4J|4VxiK0&?HUpI)$obHbw(c7>zR}ceQFRq3skZNp znCEEO8ct<_II<>>e1QQn1&FtR`rdg^UT=GZl=2C$!e8L?Llkdk|rsd zR!pk2ICn4R(_m$xkQor4=Un6*jB1C8GaSl1&7nDljuc^UixPALE0RRshdIDMOBR4k(SB@b4zy+fASUJD#bq=2zPcp|58-{KvQmqO0pS}y>bIK_eV zggfGk;HfXYI(F=c)t4sJ>kxGJdSAL6~@2oj;~-J&$#Kz?$auFNJy zk(2&IY#w6A7LbFJ?VOrt>mVDLh2dI4%#58}o8E{?ZLWx=H~3pgjMV+s#@dR9|8cNe z&@X2|Qg`6G!T-*=v!~9SeM2_?@Iy~}fV+4E9x*|69*KAG`0;zNi2;}m7J~>Qi^wO# zyXBHj#d~Brwm+61-~I2-Yg<;I&_Iip{wdx9=es9Z_`D^g;S>CLrSCUNu)cMq7-5vg zB<%^+>inD6uIAItCuVL3i;{vH8=w1wkH7Hs*AE{#`NG#f{piD|-|>$3vj_B@K7D#? z>&B6zWmkG(A46q@6qm|S#0Vite!Qig#&n*tP!Qk7ePEZf2^;%soH?-ob=Hy{T0!LQ zR37AWTZFzC0R1QA97=$TB&6D`t{J~JR061yA)Lvk44UIh3&)1FEow$)|H}6lzc{Y8 zQz=mRrDbsTTMyxP;Kw-R(L!JRU)?43-(^V}J&ViGgf99!@s}A_G2I@0Uh6gxATA*F ztUv43?w5*D1asRuc8}J#={RoUm~vO)qlLVg)@N~n=0hio1!$0MO&-op<7nkO%&=xL zHGhV({pbQLgI;UxnUm2ApZWvE-%ZIZ_jhMXAf`bT0!gzetT8FcxLwj1whG2mGgINX zaeZT-s?6jx{xZ4vOGM^V+)Dh*XV}j~IxG+zQl`%YzP7RoxLdE6X&>0*t>Dg>+ilan zI#QoVR=kKa4FU1sj=bn0&!||>^z|w|iaxQ}RH7u4%A}U|JkAoBwjiQO;4z}P0mY}k zB6Ss;!WCc9G;T5sDc3rWA{*xT%5@=UXS&fN3#v9<2@IB{TD(A%6hXlOE6mU^5{}ec za}QeFw`RW3nnFq4Ecf6%hlOFxVQcdmM+O#^O{L|gJmN|_)r?>bh)B&6L@FZm_U;$%EMQl}7rN6gG#rF2r)fwQ9G}hpmupecM zWgaLs8$^o?fF{xN;xma*yItMDn%z+?h%Pp}WB2;ugVYZRm_-@pEr)S7_1Lh((mNI} zskNx1=Mi9raE}D_HkR~JvOb<=v2mhW9=;IZ z?Hqk}d%MTDLbB6gtJ|nh8Ui8zcJEzpr~mHfzxupo&`dLjlkE`DcH{YxDlUg6eUD|D z*26JZJ3A-v@>}hfZU+t=c*{c%!VOnR!mSo1;14Icf^=zw?E~aRV5J>g|I{5Y28YYA>)31r zWT7y@0y}(e0+V{=Fnx`SmcfUmc)*pV-8{iq1P96_%QBgpc}SSZ7--ID*tp*eg!JY? z;T3Pn4)q#l;y)bn_;Iq$8@ZMkH2YErRf3}2dTW`qE12C}KX8Z=|KXRvoN07SeA54q z9NWT5YRTx^$KbX7^E(YMH9`xqG`;POuz-*y8n3I{wf(nD5 z`(OZJye?7;mFLnv4VAB?f4h5j?hesrnX<;^7EBt{JTe9soMPj_&He*i!+UyBHDWJ0>7KZuK3txQU z<>zB5;e&m2B&z&U&)Mepg_7x5_G?3 znQ_WxAx&ppg$b|0E~*TV@5>P9Mn8(8LAlzb4j?;0XLO-i_8DbfDOc4Fs$mF^+)68{cG1PCYO}>2dk)r6v9ebk0%?}3XM$9cqajDbm#MVi zcRs>6=K8+l1AWrAQi7Pjd-pY!fZa2Fl&2ZOGkd&Y7hZ8gP(dPxIGCp1xA(5>fBdPp zf9BJledEme`|sI*_(1D)svNYD^s=s|j9G~0FkzWuuZ+CKG0rN`eTZA7rMzcZ8Z9q9 z#o`iBhXJ)pEg3Q}Srr*kJ6h@)uRRM+qe)E^NtSS^9e}GVYwnc5**`wksCbouht%a<`PNH*X|9)>|+I8xUQ}!E)@3{}#ddz+tH`^*6#<{0? zPrEtp?rJ;>tv(n6CzK#7fGycG9CNt(f#KxV=Eh#hj=ki~y9*cu+k&Qh4>zvSK^`G7 zHt+7~;l-tvu8QcM%ktPW@7lV(>*K%ssr8keCk{)hS46NlS-dHOQReP8pFKBLR{2Wx z5+xeBO-mK3K4z-^<3*$_1;U$g8>TAJH~B#cwdEdrZ*Qs{7J;~2B#tb0+WDwt z?;&-zt1BbqaZR8tpH)`0z*=`2kRR!Lt37G#jvZ_J*Fp7`9_8nIj-PbZjVx|uIeFg+ zOt^Zr_mLjtOYV5{^m!#f(Js{+>w|HzYTbhH+Szk%2D+@?W{QP5wC>WSbMzkfXi9S( zl#eSXFI+W##NcUhy6y9Lg;+U5^Mf6`X5$WnCU73~mM!#ja_{Q8za)f%$f!Qynsg1F zr8xvwoov1vBv0aGM1yIF@<=7jf(>qF#?mI-V2L+woP796Y2L>_@%#Jtubw!x4_ze^ zQkYU zHhb(J#7V`of=znM=h;C~wtMJ6m%8CUGql`s{Md2Eb>YH=6Zf8wKc7B*O47go;QoUL z4=7Yi4z6CgYD633is#ul@6ps3nj=+HEP{o90T)AY%$M|&(NqAYjZSf&h#Z|G(3;?F zVc032Y$3!co-R(%ibYE~wr=CS{?OxZy>$87CqDVj%~kVA$1b*A5*%Op`Yx z5xe)QRzmyO@#7aSUX-}+Kd`2lePI8=OBXIMNEq6P(#lfyk`)qV>ch}EEFWlOHtA=& zU6x7zM_q9`-zR_S{hs+@#$F7q@;508BV-XPO8cC#r)}8^8p(`DnA++wF=Yh`^tT6XyeLV{((Uva5%Ww(PGaYv9+Ss~zb>rp($4^|nbCE@0nHMVIn}7uqD_G#$Dkz*DJX1)S^(texVxk zQ!$^5C=$naJtje~`3p&P(^nBavAZyY)D^eZpF@H@Z#dq48y?|;kv51l>x+O69^ z#4mFw3KdPEL~K=oNjt1nk1zmlK@giv{XvaW4nNNM1WBtH~K6v%Y#wUOG z6Euo8zwd|NckbNzPyPPyE6A^}tv>$5<0np@+<)M}!2|oHU!VT;XDRz*PdwHtr#z~K z{J2&So47Tl>{1f;0~7iW55}-aKpZg#5_Q(sX46vr)*d(4E(&s<(&wlxnzyG=S|M*XS_`~mC zU)$d(mW1XZWb8_?TCwMb&r-2S_WC7?l()3C5XG3WWvR8`dgeMc8|h$^osa~eKg7w* zxSx-gm0R1dI8*`|B$Zt)jF{>MM|alL;0Cw@Y9^*g`)+Z)$5E?>U%;UD`E zS>4Az`f&-C;`VdzeD2|g9+E5IV>BHR=7a6`~5 z(HpVM2fXYQtO|%l?K6|CGor#BQoS>yM8BSsTKPoda$SR=5%(*0(nma`rPVoa?(~8@ z5-4UUW$ar${pOk1Ui{i`|HJ?J*5RlA+)w}5BabLK#2F&S6KYb(-@77V3KPXk`YFKA-~7%e%E*JUn@1= zCBMvsb*FU&n-ZJ&Qp%@%MTo2|d zmIvN){P^)d{1<=l;!7{e0-t@`Td!QX^7`v<=tWEqJ@_C^ICJJK?t%8scRu&nV~>99 zW50X;+_@*7c;fS)|NJ|C=pF3z6CeM?2R``2Cr;cG%PTBZeHk5VDwTEQG!dL<2$WpV z%L66}i-V)*+$#lbdNj8xr}wS6wi-^cz zJrS@t$sGHNKV^G+&YU{^>Wg3bjsN4ny>amEf9dCc_OZtvp$SHuG^_>XkdPhr)Q=lq zf8~|8+<(v7ss(zm3nLBT+x+#NQGDZ7>B@yCpL~l=zAoTZF~K7zsF5Q{HJ@2J`nv#` zpREwxd(ZJxr%%6o-@dbF&i>9vf14t1Y;1h+Lmzti<*&EFf_M5N1`dh#G&z^bf zvlRT(pZcUwxAVDoy!y&3FTC(P7Tt5=#E<@|A3J~k;&1)tZ{W^>Lx}=N8UVG)0SHAlBU;hVxckg}g`uU&#v-jV3pW*4eZ3Ps-aFAghpe4>0Kqb?a zMWF#LFuC5YC9R!SUSIn9D~~^R(jsAz>f5}(D?}k@?;aD3FMi?W6DN<|fASaza5L6e zK`CZRo2`D=I^Z)$!|M`jgPd@Y3rw$!F{Ko5VeB$H3%V8cldidG5z0Fia z96`5nOH|6-ouVao5%fi|V}f#%4*OeeYK; zN-f&xa`o!DJ$p~=xw+#v|LLzk`OM*Wz3ZnoH($SU<>K+<_oX(5v2LJ_zM*o_dy^cY zM?+KrODONm!$R*GQM39#BFpb8nK_ADv}fx^w(JRr`KE1A2I^Z&gx@mBhA!HnH7jyH z=l;Eq@4T4S%`=vo)AKlnYA$m1a{5S?r4(&8v{u44*3X@Por`1WcNEnM1Edu;yabYoHq+CkPdX@q^-yg_hrhNm>&Q&fNInI}EF>kt3M zA0qRihaUJQt2q7Uy(jLqLh|AZFMjqjpSE!GuxU{Ph9>&0uvJaDxx<=EB@GuBBf1%(Ktn|K~pY2Uo9Le)zFRUVrWNm%skX(IZDsoLGPL z)z`lAm9M<}J@5JQm%iwvz;E(R*5Fo~f`NxZ_2Si5&Dcy%#ISILIk~4Th-?3;Ps-p2 z+EZ|QqMO%xBS-Cpd@W;<8ko!NrnlLAHt38^!9EgFwpqX$RU?vL`?Y`akAM09`njL^ z$v4h#Ubf#H#7PJMv{DAeT;A;)>X1qxn_JiH^8^Mctdr4c04s4`K&4%j$8Srvc}qAd zm1X#J*dCzSsjZ;cgr0n+$IE={Ml7{Yq!~mC!RDJ0GYn!^RSH1 z<(LSr_b^tJWf9ke?v2eeXWqPU;e0j4n>W4u&QC+!+ShNiS2h@v4IZ}*61x`kssL^G4Y`}5+zRtBri)b z-rCWmUljx&bag3B97jPdd?yP)e(l$Plkw$7hF_sF9UnaWdO)2bAP zlBE0~Nej5Q)=6=0$(AyjImVZznxFgJ3xD;m{(1H9uYUD~FMs7Fg}vj)4}az5S2wSn zefHV6>E(WPk@sKfeisO}ymkKUg`sShXVzahfAO9Z_dfX0gKo^6Hhw%^_O0H=eytS4zIIk&mBE-7q4Q`U^F2IqX)Uvo=lk!+3ei)fB&O@{7?U%U-`K| z`%|xQ!~+xC<}Ip(CgcwH^Vvi z?E^|Q^NU}8<$dpc`rrMVf9=efbD#O_7cO49>V^-5KmYt!AAb1pg9q2oox618$bk$T zB2?p#9L|8yQzS#aYc^;ynWM*!KK}F*W;HTAKTn-{Lkv=PVB4h3+z8Qe@11*j1otNY z!Dl~v66B&Z*gnJ!g((^A?U=+#g#dj%ILD% zeWPeqhIj8j73RaSPjRiPzs>aUbe3p ztOUM9TTsaJ&eJyl)F|V@kbP}z5M-J2o;@2JBTc(9)%kccZev~nvL0Qb613VFLx2% zm1rOxK887?Fs;jeANX3sYYv$8={Lgwdg?wh*=X^MQF=%Wb|vt2^gt2ofww&VwHLm) z|IpE6_dIst{OR3C_CEZspP~(jWK`M%#xQ`BYa1muw0WI6^KKATRooVTz^gkLYIHOK zQW`9iY`AuWQ_s(FEiGp?{1qHOjPk7s6CPE+%Sjy4g!b&xvm+^P2HZM91YgZOz& z$N*11$|?^&^uW<0#{lHT4_dlfT^mKYC1l98A9(P=mtOojhxN;PhfCw9hp2_L@WdiNP%)$%5UBD4pN@1H2>+@laTX3! zi*8>pl65=mx~kh>`M3U^U;an`?;|^}zWcc+YTiS`Z}KDE-t9F<868x4iJow1_6d2M zYmFv$kMBt??jkf1X*6hTJHhc)ncvLCBRgnOij6*^JXGa#YD*H7x+@}>Js&xIB;5#p zjZyC!u-dGxuRZzHQ^$@T{lXu9K@M^F(BTW`&UcGGtxyHJdGqO~pITq{A<%85ME!$n#9*0md_&z#?XnFe}x8C^>+9=MtV@?Kf9S&>U^g$l^s?J|-ulc_GA%6+A31XV!eyGJ-YH=u>-7Wc z4?OVTm%j8RYI*wh!4Lh=lP6Cc|HLQ1{I<6~ z&^pVatNMeIS>;bihN{p*U!6RB_{bAaJ;g!!sms|jXNZ6{T1FQmuU@@ub(+vQG?c>g zp@$#2aKR%Sk)M;SeoI-V1i~p}AO`6UvxDJls0j#QKFh=wed|)N0a=Wp{5) zhk05q?R+WcQlzj^QB1ZWfB!n7-uJ^Fcj}X>y1I|M`FRAO6N~e(d8P|H4mwakjt0*g zh^Fk`W!?GFM;<=JJ~qE@-$BvNF15Q&dm+mUOalL z0=6(rYF18999h3~?c$@4zkO@t;v26&|ITOMy0)^;TUu@8c-@k@=oAza?t+k;_jg*4 z;xR(ZpFGGmnvHcATL%P9n+nwAu6cav&(F3NVC-mg9t2(OGx8CqsFh=l!+JK0FFI|vy{-8#RrbUhJBim zm}}$WqPW^GBJL*u06+jqL_t)vh}`u7-c`C$_Z-F4m8nDAvFE@YR+yP8YcP(+8lsZv zF0;4Ke&%!Ud*AygbZ_W_W$>6~$a``n}VAN_eA=lJoXmN?IxIeqTjSx?_@g|gy2Uz$0I-L$2e@Qh*r$qHAF z3S*6p^?V&1@LvIhwb2LEVSfWpgj?cbvxPJ#*3Gh`yr2`=yZc8rIr@9=Jz-JKiF7F; zX-!e(dAl!OxcKpp{?3Pf z6)9f2K(O-3znzA5IOOaw4lH4mC%o9LLp6KhX z+iUCY$APxZh>X6CS#(EkWK-|z>R!GNCoED#CV(wKOJA=aI&k37(@#WAwwB%^_Wbz^ z%6ks{Z+qP4bqf=FuU~iG%#(rO32xvyXz>5qS6?vOe(bSF&9Fsuhux_jH@fl9uUb@E zUo-pc31GIZLG|k|zeHX*H`WpR;f$Z!B)hx~r8Em7CElc5(x+3IYybM4tVUqk3kcfe zZm`(Jwo{|SM4H}e+nps))xC*L1*_51>A$VX6MfNj@W8>7Cr{e8`161IgBRZSjFjcx z6UT+Ivu9tKf@_P7b*|iHxYkgpA8gYXYL=Zss}XSd%(;sn{NM+keDbmX{(ty;AN}aZ zocxP_@#p^hpZn<#eeiv9g-0HK@bjPl$}jxF&ze*`{j0z6_y5;ldhWR=iEBs;PR21u zYCK-oB}l&U#_MVxANb%0s|@YfasGVskmL6p2NNdnl=|q*WOyXE_dfpk<6r&CSMhK# z+<9RC0bJQsvk76h95$IW>AG_1G99x80uc7A2KmTik0`VN`{wD>U}|QK(yM9h(JNfv zymk7_nNz1;_lQVp8*q`C9p#@K1p-=?cO8BcNoJ>%mBnRwNSrg|Gwyn^Tz+OUY{)0o zG3KPn!R^~~-^u&#zxT*r`%53WaP}O9KXJl_=$)rez47Lm({jY}t4v7A)`kmH$A&2C z6hTvOI7fh5B`-O|M0nU7k=zVfB1dxedo{o%&Y(YfAF`Td+u2{jNr6gjcRO`@@L#&U#H4ov_)!|T0xGX}>FUPRD0F*#$zhV|G z3C;oU+;wf^sU4Jo>-`4pLv7*mR`ibUmcVR87t_#(t#% z%ELrPgc!DCVaW3RkN-^$(vLo(RHCy5G%uMu**+%0y8GkqCxqmBNTAMB>$`j5dw(v2 zgb?!EF`L_Ks>hRrMd$0yY2DhANf4bUs&5@Re)8Q%k7Mh~N;gs~DfdXHWTHcV>|45e z?J{24Cb_n^*`e?z%=7{=g3T2dvFN=Y`QmJ-;9XV>l{w7~+(6Eo@T9&A` zEsW9>>lg}rnfpTzKB5FefyRn)E`89#h?r0mg9@z{v;Brx;kBiO(3)d&W8QzP?rII9 zdbRxrt#p8tNz@(e3HdE2<+|zqgl}9te_nA*;$?Ta2n{Jgjv$OM?#7qa2M67}09Fs7kcy`9P6Uw5pvM^SK_9{j zDQc!LS(J+Zy0>1VS8UHzand}=){VGlcGafr#S6g2(q4Sw=#SrPzeqQoX++dI4*5Ne z!XllfUw`Fg5Kf#pQAo3mZ0D|rA9}>YJ!xHTW42^vzZ5ukLWntoP=GX;Q(Gku&Z6N5 zF_-|QRVhxYiwY}x>T2C>1Q<(F$_~CYxlsvtX&^-L=kkv;XV3e4+$Q0}2Ol_j!sA=c zs4AQ}D+N*7Q8dlv;GqAyExAqTc&hS+IZ&V&rJIU0GN)U&-EVR9{FVLt)l>MSwzqTH zib+yO9o<^dLg&PCY-Q<|-z+jImD!C9I6m9pY$peaq{Z}Z9^3^!y4Q~!V^*+fOUvxU zQYqfM5`-e(EEus=&jb)wukVgtk7yCNOqmG9LxaU{;uAFq1!9obMhVB-)NTJ;0iGz5e>^s=nTacJN^Fk3|}r1oe`|e)F|cuYdE*&YkP2)1t9= zziez=KX`Dz5kLCTkA37LKl$GGyyL|eU%q(p(mltIed8Oiz50!BKJ<|4-hHpW`puIk z@7=MZ$FWY=?|`$wq{0!OIeEwH>b>{gvq(S})Zx65EjlYl(K_N|FJiOyp~}WvhA#V- zb|}Jq_OqW=czfufhaP|82`yz#7cXAG7VYKhGY)a=IeG61g=DVV0w`{;t#R^pBVFNz zWYR`_Kr!d!iQFAgO)Q5XBJu|01>$fwqsgDLyY?p-#TjMkl7w7Zel8a?=DC=mzd)v| z9O`VayL|cL>C^TMymai?G3%K3op@RoHu#N~UpaIB47X|VVSRrX-t;yRmB>O&b6pyS zK^k+QV)?=lcJAQu)aO3;2cN%kSrLo&{V~pmTdz}70`%oS{ZIbA7hZhXjLFv2wV_j?`FqUSqodJv&t#V!%uod6arRBnR{kvQP%fTK}MEW?@A28AdkX*`sZ~(P#!cLsq zi*T zh+(;y;EPzM(by>YGbE1=Ug=afKG&w_SGA4M>I6(o2xf;<5(rPj2E6t3gPKw!5u%gdFu2x&zw6W_S+tg#i!o*rX~DCM~>cm&%J6dIGoP* z57pUIyoacaN)V+7*&Cg==+MJ1%@<-wv2O(71G+L{iVpfpQ^T}E_u?YrCsSz!5X*FF zsBs5cdzQ&HJd&Tam2c!akbA@&{y+QnXO$4fJ>hHjOG^GqMt7>HuYKd?Z@l)6SIOwU zx17Yxt?~xdqeHX-f`%{}v>@1bgVU>t&=3YEUFK@1R!1nzGGtq>^?(hD!YtdxZ5XBG zyLRp8{zmPi1&yTe|C9FT!JA&!Vc*#e^uF&vFX(QdK~e+(Eu_Rn5+#ddWJ$K=v7}g* z6Q|0RshO!vr9AUrrX~|l;y+SJDw!!ePU1K-@yt|BRpK~tOv~D0IhHALUqBKdh<)i5 z4WRcGz2x&f&#y^vlU3}>K2m=4Z0u`C%XXb>DDz|4)fEj+Rliuq@p$)@!iuiv;$T{dsB zE5W*$V%CC5V8z^QosGnrlI6w8W!_#-Wwzb9eDT7@OEdP35;lp(y3`Kf zL*=;;eDB`9W^SE0esceV`^gl`c>coqGpA2)-QKJbQ+qfJ)ba`nNQz`_8bva33XfyT zSc61}z~dJA_Y~Rl$6A?PGsa*v%@H;6h-ASFU-C>9!Cl1}XcAY5N z$#2z`%}F8KGSf|xv^)^jui7E?yKkR-fIlYufpgE^eP+hI_S&m9$UJ)F=#HJc@KpJM zk&SXuQnBv_sgvY&u7Rj;P&lxE_wW7Q@BhZH|GB^M*Z%{7>5lE&j~zQIEf-GNP)k;*{Q z$rC5|GgBH2{*JZD@Nr`r9gxMlSGB%<`?hvgvwzLnwUo{pLlUX+E!(!R<;tSTQ%^m8 z;`nj1<}E-masK?dbBOG;UB-!R0EsEvQ*&zK$`ZBjZXE!Pkt|d;enc=TPTl8Mqu54^2w(ptNzNr z`G0uqy^@-nA3b(dBbw6*`Pcr^Z=5=HMx9^$;#Ups0e$KLhCj>I|ASPOtGZ;t-BldT zV~@Xg`?j6m{N}S@G!kvfBhBJE%{j141o8A@HL;F9-@gXRFTC)A17r>!I-sX&)&aw% zn~jO!gqJp;<5LUTG1H48SxKsOqJVL}+%Xu#bTtOztP2vAih&K=Fv8$PPMebP|@zvanrqx(HjdB=d=q^j6-JOqQ8uYk(%M7H zuyq!ME1x6;?7fy5`b=sb2+Y_8@WP|=mh3Qm9p9 zdA+4q8ZLo@3iwE+;*+XHlDBT3I(Z6&83i{+Ks5`ZWo&4ogawdkt0qESmm*zuk08_` zSi_JcAcKCO&S6QON`tWGYHOFCj&*B$Ppr|=R2)VjpQTO{lrNc*adR7d>>N*o6s2u3 zwY!-fuPSi|KX7TtV6BbT@2=ape*d8(_n$hcOUgZ{n?aOUe{lanuB*L7=4G{Qwy%`5 z5Qt}FEozPlPirO(Fla{#4UK+AGN6D7i3qYU>{R6F8es=jQ!(mMsgAJPaKYf98e!lk zOtWCQQ-l=z-g@oTSGR7q4&1?WNw50-MN%uqRkC)KUU)r7B>Cnu&oH$Q9^8+REGDa@ z4g>k{x^vg~K%21JIkVO|?CWofCMQ{6$W)hLGF}?cI8ls%sfc|LN`OO}Nf36#ofdMM zei7FY0km)5Z7LWpgezM%ee`TfYn1MP@WBUk9*&ZA*G#q15p52sme2`#46B*UclPo&ka6~uKjwVI$ut1gt0_&Iat%0A*#N=<75-)^F_~>km_bri=CM7unM5%~ZUVZiS>C>tdcHqmA!$-vN zRv!Zr^gu+m-o>npehJQ@RX(gLvk_gGb@b??|IvT)U;N9z`b+5fJHPu+Q0jf}dk;2! z9 zLo4Okl-V1cBJM9MA5bm>&T|{~HgQTn&2KasKrzlvIQfnCdtq!-BTXPFFNK5=w5le}#WyQ10>g*>w9k(29=6Rp!{PV;XjC+*q0 z2iImu#FUg|$bg0VV-gdjLvlxUwg^Gz1hCfllM>`MRYG*4TA&u{Ev7h{udvVSMrq|lT{_<;^86v{VnF(Ij~f+W4!B2HHF=G>cav1pj67tKQK*uK;H zP_F9Gp@W+?Y;KPp3?h7{gho{#*#`y+nM!;#td@z9-B%K(<%h=vDVeG`CIPG3nK8Y? z1)_}(fDeA!S*Obq8&)M@0JWMsfk3H7mK-q{=b7k~r;!RRDCXUSLZOJLf7=h_y&vzC zBTbhsU);KFTVpTnze*UgGHeDoc|q03!nOp;d5eK%+!=H#(H+$)2^v2=U3qB*xaGZWdkeK6+_(3*)#PVh z_hS8K$^)*U{T(j#_HCn?OvZy}Ms{?gfsYWczOZFI~0A z1ZDGz+R0ub0jOaz^dS@0F3MdNgnTELfMGp|+r$DUF;2vkVKPj^zE_Dn6zaN_P=qEy zD%wj`%7H~G4w9GQ$fC2fku8NfuUbszHmPeYKrYNkzrG0ail=zibLY>4NkqGQ?>?2Q z5LwY}z2&fbcWpFw9S$q>&m;^{bz~y2`}b&DnYrf1M067tPmT#wx?;>y!O(%QU@lL; zdBeuH?B*il-7A%%9$vBO6lxkg`tldQeDvX?NPTzpOygL!`oO`1*2%y8;!D`{$gzh_ zen;0=EJ=U)CGO<;XJ5Qm}gZ#l{K4pLJdG0tb*t~Ye)Br%HJnY3&|++C0BP5|R=1|SEwbYU4Xrez zBIGr`!A4fsed7Ja^B3O#V?T;Rf1F0AVw)$h zlk+=q;)F%S?ilykvUMx9QWAn{?4I}bnnu0Q<4r&q1|Z};tcX!ovdWlUj{r1sH2u;0FZ zO`KzYvPjFf-9G#JnUg0^y!Wy9%4!G^B*q{+K@Ur?&XjGuwyPQ6pL8?J%vxJ$Cz&p_ z&Rjj!BP(5LrV;#&P0J#K+xW7e?S-{2D!DC$z^CQEyQ>7O2$d-s8yXP$%pF-#39o0X`SSD&9vqFk z)UM*J8*6#sz#+kkOsZR3wro0Z;D8ikSS8?`e5QB+#B@Jdl~N$BfA$CCCIgBLYS)5F z41rjixqZjB0VW14T=2zLd8PNg@6rGAZ~uQ5jy&?nyZ+<<QXzj_v<< z{_4N)&U@bTSY~|14uZ!$HzY-z#U2ZZ6Qga6)m^jMPM1covozFu!& zy{d0$ep5#C zF3=UCLs~8dAxrJ+5F#&)p(4Y;*hqFkPaRA~08)2kSv(Q58XT-^+M;Hys8*f!Pe;7e zF85c@hlm2Z2yE_TV=+Unt7Yk{w-$X8MLR_^GHIyE7?SZ?lE7-PZ3g!Vu>v?Kf3N29 zYy*kX+%SqR7-<(IiEqQrZ7P^49Q7HDGWcQft^bxE-}8B$9iykonRb>}`mFZvKSY?= zNDYu~l4)p81PKo=#CuR~cnMQ+Cb>9djoL@j87L zV6j%_KMJ=|gi>wV;-J~JpZok5o`3!&4dKB%aZiB;y&+L5yukPE`w{wkU|uP-)YPl# z>8e%7jy-(rUB~$2OP4O`o!|keGrzM#cD)jFy2qz*$W<*<)S-SiW z$LS0YtuGx*B=Ue>sIaml|M?zo;O5S6M#yq0I7*WGr%e}tMqwai* zP2F{U>j8}6w0iZ2KKv7pJbKLff30Xeg|T^N^CH)L;DaB;2NQLv^uB!$k|G&3DthRl z1N@MF^OjAUZ3Cku#Wkw<(VzWUy2npQccsQCJR&5Jh`=n%WOx7pteL%6I+H^J*WyaO zg3854)A91Ut=fbaq1m+YiFh)Kv_}m~RD?5P7kzQ7{?>a1AIx=eXF5`hkKVNy=B`y% zZ=Gud#8ef%Oo$+kOq9VCZL%iFL8%H9u&NGvKX41Q0u>;T5KOmShB)#n0gb2@LEJ$sq(Gy%0r(PQYm4=5 zFyTr4S^&UkC|RJ^6=kP?BHH9l+ndt-Dg^Ig?!OLtm2gxdXW_2MaqZR1IKi;B2H)=3 z8Hd%4t^(1iM62$aYfM3mio6T3d$w)c{?VWNSvN&jd-m@A_|Ja=%KemAE47)Y`Oy!& z9{?7+Xd=8L579MtcE`r8GFU(JbAN&!d4)_FY5wp>e$v1$;b8z(`!hfHb0V8<+qQoA zr#^xki8R7CAi$)NG0+mc0yvzU{>!AR2M5&9kmj_z3`l8T&8TXUoUM)UoE;_k3>uh_ zlus9mEEa@-U<#fl8)ZzY&8VnF6<*o3WB;a2TNL0uJg4_A^$>ckE52zNfmo8lNC`nv$IT6aA)fV6$d-Rbcj;h|g@#v$+06Ed{X7}E{ zb=?AIuh=vLkcdD;Q8+=p-;UNc!d&ywKk<_ybpQa>w5kvP)Q8}#eS#C_R?^EDA%SZ01 z!)K|QFMZ{czx-!^(b7NCK)Bpwi4+*QY(<+pyZ7vCYqeHW%Y~p-1-MpN?A^0x{FGT= zO;jQuM9TEfeH#y8Fv&n(8KtPu{IO;h!v?54x&lUcau0J#Al`Amyi_Y4Dp0g{|9XQ~ z4SAp&F(lp*qzI`@sXy6~8KPE?I#~yx(hK~c4$x2{ci}8H)g^OSwYW^Kh8@vUH2jx} zZQ8t*!EF_n-e9}Vl+5_t=f3cpzx9vb{q9HAzt9;f|7)*zGGWZO==S94fB3Kc<-cSN zJXMKw4d*opz!w`SK?EPWUwkP&(+YoLbKu}XlEw;qKB~2*`4`(e&W!RX$zmcC^W$;B z2l7sfmO`Xvnn1y{7XeVI6a_Ss8L7d^I1&u^*&$FzPpP%$o+m6tA%$+Gi!@YLQZLMz zxWxe4O_RODq9Hq8Yh4_KYXYP57q9Huvq$C;BFRz-71cN58SiGcI5rF-XwbYvp^{8^ zJq~Om8*%b}8$0m97KS1aYj5sPl5-Pt^#g3sp4w4t@8GN054ji_4RL$q)toMtT7}K^ z%MqGNXK%dKW_3Y4Z5@*oDH>-?=178MXKlB2D~-5oLvRs?J0dQ!!9;G@@L=)K>eWUx zyrVn8e|QU1$eS{76cAw0qG)7O?dH$0hY z;PUk`@I~K(txrLw!=`YVxm=f$dbYW1!{pTl zNMYN2YC_aN2K9}P>o&;cn1W^%A&8sbi|cJ-Qf;YilRn~;6yhVt9yxgMpiqg+x1x~q z01=@jIbO}rMC6cEOOQ4+CQOHar|=VQPk#EO&2CiX`0A=C<$ z_zp&SGy$IjV+bk8fxEB5g12vXcyrI@MNO!-Ms;4%UFCDfQ{Sz8MIitYHYy+;!p)H< z5p4QC>J>nNg{W%m8wfHf)O?;`yqM1I_dW5xcfaRd?cR3ziug&O3|Rb&adQd+P4+(Q zya%ejc+Wr7EDBLSH)mQf7dN~s9CSit+dIiZ3DX;W-MC>QQL?l4!F~IhkkDig(*j$r zq)r_(WD&&Ju?%N@7xB2y^6OdHjICEv$EQU7Gv=kzXhcmG4Lw1d+L}Q4xUnoLq$OOUC`s>Z1 zTtbTm6u{YvH^M54P15KBp@}yOUl%(+`8@YlZZF@y-zVf-PmzE@AY!OWO&oyF`nXj+ z*CGLN%L579`|yT&9!lRU^T+=mPaCaK78Nz38-54_%q)DJ2|lvVhBpJJ|dCHZCh6Jm?n*4Y=7 zS};d5xLepqc-m7NihzO3$Y;^Uw4RW(A{`rxV^mnwi3p4r1if810c%Btf9kowKER<-GfvU`nyL$T6DN{0SnM2A)kiHKg z2hQ(v|DCt~7%mJ+SDk(RE$1d3K61ocNmmL|(uP)b@InSS>c$9Bzo)BfdtuvI723M6nM^!O^^^w=Ic7{SLOu^n~xAT=`U4pwbM@UZhS~hUT~dhAQ=u zuje*mcLPBnBKgu+UG z=r$DG&!lyR!yEjddimE-WO*8?6LL3m&i)2ws{>qo-2k?pQUYh5S!2htY{ZESCBJR| z>nsVvZrYYu1ku`GnvXsh2dT*NYpqJL5vZ}mHUFMcxTBH)y$IchNhyewUJriIO0IwZ&vHftK#J)unY z658!SYRX-rk8b`%>Mvy_ zBN-|nEky6Vr5hY`3W$doBCz&Y2L_$M9LQ@z^*~&T0o0f5#_CmPjvtpx^RuU_^AGV! z{#>#7&YhjR_Z&KWn4I9pa&-4i2ltL+#V4qZK*chYSAkauj`n?tD1Nw;nya`OY$#A_ z0%q)8WDO52=`vTZ9Aq}ZOTq~m*`Zi-lkK#}I_tdvlGe~1eG)*2Z?y9*WP7*12bWP8 zK@t0s&=!sWQXWe&2X-_OWMXkx)Jq#+efI_ARTXIiKJ4d8R!2O^6#=3a`P27MqA=Fa!Gl0Lf?@y;kY% z?A>QOp(16#EEFFFDXr`9bCnVsjX?oz^xPq%6&*He(hfZaz6tQ0tOZD*Fv*B`Q$H@S znjs3pG^~MLps7cSQMxfAk5`Z2L7#(byk1WM62Or4msCX@_L5=o&6;S#J$j!boH4>% z3r#2lu~+M27S`EmrtHl+Yc!nUgY=YkW)P2z#+8HvCt~{u7OIVm^|9Vj%e}7{bNP~`Pi$RkK|nJ6;^?~agt+%HH)# z>H!af$g>h)awi8OxQf$@!4LD9%Co>%(3Y*hkpNAFNP_-?ys<&q88TWVo9W?5+S36U zSg5z}n%GHoDEPC#XG~oHM~IDX1r2Z=0c;2Vc#^D2oayO3=HFt5-K#HN*=UA za1tB+gri~FsD{q1NQyQqhA$H7S_NA41b`-RkRcgxJuSdB?#NPWh(LR4dr%S}hY_U* z!3HsIF;lIk_Ghrj2;9AM-6T6}f80dkOP8)4Jh<-qjY}XI4DyvWIA|VCa-BX=)>N*y!X^zgz(P(|YTUH6l z{|QGD7a1vamI&>rF!U0QHOj+8qJLR>nXZvxLO6zLG~N4U>0+HfX-EN20yqo=Q-)i* zh?)fhrADlr_afU9H zunPOejh#D>`gChVV5&;Y_@i3}ZQX~hhnfSgz}llwsihobVCdA9dQ{9WD!;M>WFZWA zfrny*Ty1`m2RD34#0e@xR1ivBrk_A@3aXOscpWEFYU(qJ>24O>2im4M(;ER{f;!wI z+qsAcVJSmNlp5A#s-eXSMwfK-{%kN(O3bgKJR4`7$vD zj`@DY;}@J{fKtiqM#J362Qb6Y;L)x7?*s$cKz24~2#Fwd)X_`@3<&ViRe(zjr+zB@s*M9uE{)LNTFx>zwaz;(t@Pw!-rd1GX61RjdA<)(4eyy^-8HrN~CE znOf#`B{PfEytvv(c}}@7BA&;BD6;bT&1X*@J+jA8f-q~-rgG!w&tF_h;v<=0s$f5@uSucd#v} zl=PaLYRi&Ep$SJa;3~?N)E>Re!%3Xnp-SfNk!A~$=-voZ;$Sh_wC;6{=gU3Hz7aqb zfw_bqkR+J3+l;GmnK4@U)%2ra6lDWkiFUgHT^g`5q=-1Vk8Bq16+MtKzz}1&^m1MH z+@n4o647~=Vz5xaorQGIK7Y!_0_KgfXwof)8=Nlc5#FNkcX7ens{Q!K-t)o>uK-0O z8imJFRkTZs@@c9s(g{A878<%4G!tl!*Xc|cB~OqFRS+TODI#%wCP zS|{Ny6r2BSk&qqKG85nyyC8cE|MJ2S?xcrwY(abwIWOFdK~U`;5#eYaK0INJE1 z(8Wbo#3h~LlD<%Zp%1yuGx0p+f-pt)gvQKV1JlT0{y+%EvRlHdOgsQ!2ut-77?IzA z2)T6>v}u zt*5Ki27>STDvbkW#9H;>tSW4h>En8oEk!qG3a>Nu#{O=XR~^{|QZQ6GkqGjRrT2D~ zc!td?(MIldv6Bj;DQm3iPMB@D%Inb3m@lJH33pCZP1TuI!Auaq#Dnf*yKS%6$joKf z$F>xa;jo zO z3dS*#%+jJVs*28$G=7O1H9$WBMePPA@V5TitL^>X`{?7QD&4RD!-zpH>Mi8WPtC-P z>pecP$!%G0ph|w=v<3R%Kmi|P93WnuU$2$mhk6`!y z2bGCxNTgcfl(+7^7nyV*?-iW??^Sf=#`R0KhRfC7f8D{|iPsOjREQ>U0-{18fn0sh zDBZHJ;B2T9!LrD-Fw^U@t_~v?vFqTNRBK2x;RT|rx&Cl!KZbWem@+jK7gxl6^nfBfTr;?MuuFaI0==3iy}E?vIy;6AD@D@JH)Bc?7QLK?_Zs@fdAKpJ@k{L*?f zOpNMKq!KzY)4|H`5{gOn$N6?VE2&Yf*iaNTq9{aa^0As{quyoI()^4%XnC@ND*9>Q zrc;(va*`tR&q{JFssOX`h%vUHD^sXB2DJcA{GmmJMQw%0R-`mQ7)7T#qyesZ6lS8_ zR0^+*`)Y&i*oyWAEekT?_$b53PMnEcu8_TT`(OQwzwpd6FMRqlUtYE9#;^R!pFObu zf&ct({oTjj`_PLoo!+;1+c&=P)XrT8>>Y6G^o56y?0NCU=RWkI5B~XI`!oOUU;BT) z`0^R^&bMvbXa|lZjF?&pObr0Y=$^Jvl|iF-*d#C@7iEreoyn8Cj2(-{2*Wq?wdnKk zR5gllVU&{+D3GOFwfgGS%l4a4#ksf6o1tr%#$t6lfSLt?EZAS(P%xJRVEo%eN=qhK z)NsZfH!;`!yB6>TFxUiJNG238`p}{cHeGmW?pl$N*A*jHNNSrI5-94ahC0LInGSyd zr}8jj6idZv5J9_TU!bSg*s*#|vjCoI$1a35bU;T)*4ww;xdNA4tfV$_=Cm91 zz-zh~|PqX(bzM^DT5607NV>!Hxuys2jM>`3wjqHIuKN-vHW`S51{dL7e|LaLB1CE2bTOc1VNZ6Wcgw>D*nzCSyd}<)f*w-{o z)>%koP7Kyop}!;HfXeUsP1i^TwJ0H1^Y=Lt5k#N>Fs@~X(-9OyRFo_ul_EeOT@EBW zD?l72^T+C%qV7-hoTh+;7|PS6Zd;(xyJ6}6(!<$x;GM@=Mrz`vGN>V z^)m1=fjp-J4SX1YYj8hJtc|4CGqps66{a_l$cvOe6H?5oc!?A5M(`rLSm28kiy4 zo)>i^nMa+6^_$jiTD?iA4@I87P#!)L?id7wGRoe-Usu6v8w?S>K+St5EinaIag~W% zU(${R6E?<50-e6;o+aPJRisegfq?Uz$V$O9*?L22Qb?!x@Exb3ys}9~1_CRrR463@ zDix#d>UO}Wr1l%CT-SU6+Q8$GTMRE ztG&A@`MoOcRd(sx^@|rT34QJ6d*O}qvQ5PC2VSY!@~~XAnM7~{w~=K2@X}isoRr}) zeTaHO6Sy(Hvyngkso1F+%_LdnM3If-=Eq->sRW#?2)!Up98(?i`PPMYawLyG{;t3H z8-MPf{=uhz?brUaBT;RSivhp(D?j&V{?sSF`n9KDd+pS}`&a+ZB9*`U-+l5o9)0+U zC*CVF{Leo183Q68{@~-k@+-fjxA0&6zyALhE?m~>|CaC}6D*o+&SB^FN-Gf#evcr+ zP}5|C^-nTVBReZv)l4`tQH3ZZD7|ntxD^e)C>BO3R?4j_DVb+iH1_s>#)_OqW6u&P z|E-K04#GTi4ucANUAhIF9xGl`^)vE<-ax3xTmzXxvZ6IA%cwOXIeiL(%lMMr1g+^c zZ{JXa&Vb`-%>)sV(OFalL8Stmj==8~n$3Ql5JW5y7R&2y^D`XH)Lps*@`F(MyE zM2NFUCetPcqH`D2>~EZkQjEqNI?GSyM^VIsP+JtlOKa~dQ-Z*R8WKLEQ=#nt)iAaS5&JKGh?_wBgT?$2?dwkeCmZ z7IGn4;0q_haR%r_4?JTtykQz49|^=ZDx-}2~BlE$Ks7}M(%~$OEN&;6^uZ;zuV67iOUo@?#d(yQ-0;Rl1wBQusBiLj11dFthH`KXv;nc~&E;Mj3P3 zZZ=>1W^L9?_EQZgKneVsxi#(y_#zjK77^rbXd)c(9qMCG?mDKqrX2{@bRr0p`z-97 zK^|r%*%+H!cpEWrsMsAY*|(7fx`-rR(HzbZcmVmHUaq?1oarw=`Lu;T2CPFlnyy^F zQ)e^uAAjF_Z4kr4T(&S}%Vs748CW^VGct^!>ozbE_{?V3)nRoY!E6-$N5&{FB+?%8 z4mE-)Apxd7V(z{qUmX?mt43B8D@;mqy~VsN93LM@x-+~cC_^YF&JA*o2({+ld{0ub zvtQmD<}OxUwZaF?+HDkr>I%;bko!`+JzB;TilSvK z<4F?&%dB3%K1p>BB`>vHhWSN2P-M)=YknkdM#VS{Zvr`#SOTqxXqu|cqY2q0V4%R@ zCF$8>Wf9qWOco>$igIDFm?W7S^kXZEjx53TJni%<>GYK$uIce%8nz6ceMe+v7HC0L zFLzF~wfCSF(|1T`oTC>~=!Tv=efHJYPW;`!_nQwNJ#6P#Z3Dx7R_s>lknBY;!@VF+qK7`G@E(f&c$j* zB;VEA`$COCn1A70;DtzJPvmV_D)O&3C7cZs#WuZJ%}eRn%VoJ*2rbMtXN$ea7>d?6Oe7ZOu>{zHfmp;H#0$Y_(BE6#np-G(NTdlS{L$Q>?I$d{6}$=+hJ zeIg1DGA+@lj90UTh!75f+ywUN-ZV$Vqfl69vX^!kHz8yDjRB3kD4)=Bz?mtRt~{CK z+DUX92m=@U6mX{qc+?fO0H9Z(MqfZNW>89|4hB*|7N1;N)BwOy+q410tOG)_7d1T1 z3M93N=8mu6K{GX~Gokl+&p@E3AOK_|q-le{ndF3d2%4y*zuxmVW7pW1;cS00;dQa-*%2Nt-Yi^Xe_BpMv@B_1+a>2O%Ae#~@m002M$Nkln+7G3}@hwaL9w|APi^gK~!WQj%9j7p0;W0>y<<1w>Ikd|&jNX768 zGBRppV^q=0xeYQ)cOry}+A!&U^K^?rA?`C*o4_V`K1XfkFcSr2Qi84o6_{d#Is*e* zC`u{-LXeX1O8Cr}Bo6gPvOLNcN|k#Fn8dP8yF(=?lY)MDWjeFAtXuB8tBExvkx1ZU zx9dVv{DK&oTOenEfBi-zPGHm)kiiKS8czTH0%YXFA#|LWTge1;_@bU~v8FrO{G!9< zL>i`cVk#L1iSwKVKf7z!c0J^wImnZy=;)vSA;vJKJBV2t0_&nPdYgmrwzV48=?KTy zBH~ym@3C@5Ze+M?P&@2c?q`r)nN$K;XM zz<1SE`u4Tl>vzCwIm7dW9-S`x*DCnz{In6|~&~;N4dFzVh1ZHk16|PyC2L>Kos9?ni(0agYA> zzx)@nH}|ccjg>8IxwL2Rt}9os{i$F2xHfm>Bg(<|e^9Re0%;ENWJ{~Qw!73w{!0Dri`H*6bxI`@jNX=+u zfepc!r{|t~)@DT?c>m*q$LC*o+4vyPZ~=p;tH~Cx9Y5(1uKf=>N&d>uf8uAXE~djy zvw!W)llyMp{?w;F`{GNl96tO=-P2H5xe}FrCOJfEtYL8^)@MPfFI49#a1sw>!h#uz z^Sv^K=3~C=L@gFpKS`cr(%0NbTx%Y;rYI?EQys#vs1P%~6XTdF|1l8DI_+YP)E*5N zUNe=g`nKecEhy(-5GsTsY>8IR0AN9wVKA8O7>a;Rm9-KUX4^2^vpk+mxW|-c<64(A z{Fg=Wt?8kJ-CWWwGicM^<<7nkOt66TDYk(u-HkY@u;FGLq*k}UR)%jpdR;^Gb(7qo zmSz2##AI<_1c$D;>JDK8LX-J830*=QNjO-N(dZ5LZM=5pDg?bUD%=o5HiwwSq2em) zLmv2lBu;GHDZ!g3o~EIj-4yMCRXxohKpz?cZvcgVmaE{g6zqc3BC%{J%d-qA^^NSB zEQn2xFGO$@$jg@cN75t#gXGFIm-*&(sC2)|g`kYTBXl|&=FQYqba`{kG1H{VHoGE1 z^bwsf5vQ82YY?+_da=q|K~><=ClL_0vZzaK<8LU%7L{-Y`cCtD%B7#gS^(of9KQtF z1a`2{2M7Sat${biIc*Rm^_$+k`s!<+`qZZ!`S+2Jd}#ZQ?V~Nif*icI$n4-a!hja`z$kOq|3SwXR@>jn4<*)qM2Y&3a_dOokCgOXni!TX4?otKxfl)g~0tBsN z@g2)=rv~G36GRYzW&Zn$8sGa5D^1PxVm)m!N{BTM-ozIZi3&iQg^eK|kOG3ICr~?g zS3890k3RKBOyp1h^iSC%tyd{e9n|z!G(shcdZRL7$**D-fSHT*E;+)NND3n7k(NZk|0t}bHH}>4QbNtm;|LBiCW4DSA z|Kv~F3=CBaezAD!wYii}Odq|5pI*u;)AyomvV^4m&w8^VsnLCR-+b$>&wuWVEXjvH z{2?1IbYD_)x_KEravP?M$H-o|(JwvKGQz;bM1%$C^c%zbyDmVn-l3^zNETv8sgVkT zttVi|CNv4tNt!O60RMsg+G{7i`qi&L{P2-S-*t>hSH;cSc66(INnHvJn<{C~GQ51Y?*JiV=>UcswOmzW3iduQBB+D_F`L_V*oc ze`kP&XUn@|*zNpTb8?{}_{U!Z1wsHT5os3C7?b2Y>*MNanV@1eKlQt#5EAQwIrETh zwbmvDX1X(l-WWNFth7-jZRb>u_glEf5JGnG{FN_#@yUlCI`HMMd`->AkDoLMD1LIs zZj*1k`Ih7-b9=#{=c?5|`H`RS==h1#n>T;+lmGDd|Ke}_*<;6!KJ(2Nh{*~g1GZ~) zw9lMH8TeLqWG)9TR7@-Lj&L2H!6G!k&u2h6nQ5`4xV@v8vS{i2QnLa{R3X2m&9Qel z%PK)rL!(_RPNYJ!Qe{-Cv;?EU2i#(dz}t#6dfQC)I!n?d!{4OHZ*OWe`sZrd*5+_% z5zPa{JrX6fmn=o&nvUkQK_Tysq0Eo6aR33J&SU&q?}W#h46)#n;b#xLyZ-Z^|H^>_ z4@zH|XzU;ed}2K8JSn&AtW$lXt5>c`V%ggGr+?~$QdXzWoPGM~Z~mj-`k#O8SAXf) zBS)Tl{w1DoxKP#07vO83LV{hy0!oyi{5`!Kk3N6y`Q!`O->p00%CD z6qk(6n@36t%<-{~4d_%bnY{PUiC-~;cq0oH4;zDl>yBJsbjYf$y8HaI&;RZJ;%}+yv!DI!zx99pzZ^Mw6wc&rm?8qo+#x9_8|Gled0H5}+f^cVBLMrNATM+|EPAYQWYqP@qwKT6F8t-b%;8*luB|Kaa{}e3{l!g?r;OX4okR(IpCG|5yRcMeSJo>mOgNK5e)s(m%j4bzx6w6 zql3Tt>;E!hgO=)yam-ezP`zZ8?!!VJ$h~}yZYSek5aw+}z@_iYm({XZL$5$nET$)g zs1sG|rRyZtQAv|MA;txwtM5C1{@j1}_x=Iv{^VD`{;&Qkf8p4%hfSGHjT-Pr(t`1* zIYAXt^N1zo>q+7XuUSvc@0{HX)M`}gYIc*h;c(K$Gg@^x+q}ubD}a#NAF(XtwU7|= zOccXZ)0B;G`f2K?Hc+~33yDuYr`0HtjD>;!i@CKhMk)%WEhB&v#zeUva1kn{6XMmn z3+juw{;%e)@62G$Eg#U%?AIg)4IuX(eV_j;nP9tUe_ZLsMhSIZ{bqDM?@`7)^SbB& z(e9m9ZDisl(t!FEo$AI+VlxEHP%XyymYNsw=)Jjk>&G5D{2%{6{w52f!ABl>`1u!J zd+Xd))AUWXR%FxWy&E^!SZLkx<1hao|Be5x5$Ml-{!5?y<5=rJVGa(5e>enPrc7E z&|y9nd{l9<(gGw{c1-DL^`tmSHuC6RroN%fntSeNA(R{f8&QQUK|@`uZ{2t0=Cw_m zP5(+ciF6)8q_2Na^V6MOVoisK$sEI1cUs&o1OuqiSerGSl&mkdWg}bMM#<@$-R*8T>&f)H%(3~Lx^Y?|ON2*5(%Npu-SA*^ z3T#K%Yr~&Z2VotF$VhD1$QJ^^>&Dfn#KtY{{*Yra1yvf@oEzDDNq7THPHg3pY)VP1lvm}!MEO)}e1 z5J}L5b#t1N+Q3N*P|H_eJi|k8Wua!1h4ez9>~fAY&TtJvF4D8~!kYnxKK#g-AnaSX zT8LCa#VeG->(WcyNm^XH)oCot!7&T8B3#%fBm;@3M3MFn)}*2?wR-rzWM;+K<+;7S z0^zDVcF+az^()uk^+WGAN^We+B&7sbEhC|^(r%E zFPuMj`0zu=Uwcicnhqqi5Q08qKy6cu!NYx|s_(mC4j4{I$>^`8J9q3p@8_{&kG%TQ zOAc%vvn}yyB93YUDR|hT2~3k)Na)?h~2IS0>p%-Fj!X4$r5l;&)7mge~^2*Dv0G4t$4Z)V;+7S~q%QN0+wg_nu93x+sc;iN@T`(cN z98^vLxpMW5vu`@w25CVn6R~mYHgy`PL`e)Cuf)XZR&+w>+Vz2yNsm#{M_Lr?t>iKj zhd=wie8E|lcYJzJc`srwfD6buI)*drIK(OrE?5@jK~_UYjyxo_!R*s+w9*HF5!Adv z$Y?ZB$x??KCPZt+N8o1_1G2g#Q4s^=wOIJn%=eOt_%X0#A$%{fHD9XprufXOgc3dN zH(IyND_EmjSRh?6mjxa=XKfe(ghP6jU_2#4WoX}w5hN3O?k>SH0?e=e`+*lPt*V7n z3X!>`=m^z!1z54XBHEuW3To0x1o-D%%X>>9RLC7?X{N4Jx+h6WnxCI0*Gc2>66+RC z7^g|HGY#lSc9A2>H*|d-cNRwL&#>d~hYl2P&nTd+Q znfEFtE9i%KnjK{s?9nLqwbnd_&wcK5ANueI?e_aNMYbqWlCSRj{c?mVM=5`eX*!8W zJjPyAT`QJS7Nl9CLuQipq)^Gju-|IiP;*Rh9)|HD80BTE_l4d%IgaX}wvBa88u z&SOUH`rHsCDel*ytmjHVa^a+pSRu|LJn3y|ZSq{6Bcn9N4zl@01Se@#eqWQ^i@d{d8-=so3+ngX|H8|!NU;C#6HhSyjrWUiu3u}ufFwtTuwlWh zem)mYa3vRBp9<4-VDO!-&>r=S;AzS-l470#7kGUvxTC8KvafBnqG%=&NKmR3VgfvnIxSn>J5JPN?sylE`kO4k`Fv?yt$Zj`G z;c7O$IBE53uf66xs2~6FABP#LL<}(s-#J-W5eq#YF;`gbEvGc*uk;W4y8}hyfdL@Q z){Nwsm^0;99NpEcSM*IYD9jS*8v?K*;Rcv-ik~CLo-8ttNhm31#&v83SxU;NDSk!e2&Gy` zs?0GsxlOu>wuufgY8DH2#OfgPPlrkuPN`L9!2rM=0;dA-bgY1?=OV3|_LaQS1aDx| z6*CHy*FYPuP#;1_cslRJ(^N35*Ib({ri#d93D8c67nd_=-+h7CB=^1h(uOKE0_QwL z2qhF$vKf;A4k~>-K61`%qPY+NUoT`xi_f_kG=xjJUGc6O0b0DL>Y{eURve+|CekAc z!<_el{sg13tZtarG#98KI(qaGvh(b--+cJtN49Uew+RJAm@ z+tE6Djnp9pJ->SG3LeQWs!S8tO$n368{(L#&HreW(PMv{v`E^-q!+JT*|5bKT;J}& zhVxwH%BmwVT*QkhMw8mmJ9*1Mg?ToQ=At&3KtSu)-oM%Q@$C@QUJS4)hFZ7o>IEz6 z45N>XsYqF?5izk0sBP&?ue5~Dar2wjuXTuD9#NHQpBKhoi+#-(8tc?dpQH{L@bMuu zD3PJwT)Zzp%K5Sh@>OUiWx)b0?ho{Z*9}0|pM+df9MaX=RL8L1CWxAF+l@fUko5VT z*LVM(ry695^ia#%Rpp?)@=^5m?P`MEl}FAx$d>r4|NedJoyz;@r4?B8U|b2nQVT_h zWZ8|}d5(wnOm7>WN}2(kKX0N*`^2o1e1FtH$`x(Zk<2>mV+-0-b*S3PaC3Mq z%us#hX+_knZ)c+fL&h%OQR4O2PoF*W`j7tTkKA;~Yhw-UPIO*O8F+_4l1U`<7xDEIrLm8WmNigNBH! zKnTOCEA()R3(xw)!|xHWu@ub%bti(;7Q&VfOPo`M;J7PBuYg~a&PLx5Z;FF%fT}r< zqL%z9kZ!GYVp@x|$!04g)~>SdsaLIQyVaheSWfpw!WYw!R44jvn;;L$29j`V$k!QU zf8&iejvPM3c8Pevcl!!?A-7JYydZkJD)to?C5o3EgT8Wp<(z(C76@TbHDoFOID~N_ z(@V{g0SIcqCkl#ogZK?!W&4HoW%rt=>^Wj#IRdJ&V(|tEvDH#sNyi2S6ibyvS{@c* z$X&LiLqM(ouo0N~Z1h_~T8aLY0CP(h#2E%NAtvmUB2EECC$eE!M#U_qYhj>9c@)hr zLJmRr+Z_KWDN&+HBH8&b}C;s?p*0t@pm?T^rY|+F-|2^!(my zj2}2RZ`mw-;n6p3*#dpo1|(lO-AmOD%)`IctF5$<^HW*PWq1p5aQcA=)h-@>#=a>L zz0o(O7Ok=^Z@u-VS5P5v>nv46%7`GPVbbo4>e!^+SgqiVnp&WvfOPU_peF897&S3u9^^+z`+| z@xI6Q?cS-ol^A^QHTD_U<^?)EbLS3xacH#JAf;wC3eqT5zQ05R-vC87-#Gg^)(<(W zH1giPvuE$Vj>%%%7p9~s#3x{3ut4aQx6ZwB@xmoOWjKN&b)afuRg*EWRI^dL@uQj} za=<}tBIlK;Z#dw(iM*vGlJ?RN$P;~_PmA-nEP9I=nsKYzg+_5l%>mq{W(wyluZ)aQ zTpa%}7HA}; z2^_Y=u0EYG(!=;@ln^nA6z2R}Z|&T%gT=`(+mm8Ch`6sIh!o#A`s#M0MU<DP57QY(r$=AomR$&w#I3soAzse&Fa_By#Dl4PkrnYpFsa+ zO&So8ZD5(-e%lC(+6+S38`{C-pZ@IU9)JAtEnByvHxdb%OrJ@FaXXl6qbuRVL ztc(P#p75v#_{9XVC`!;G<_gb>4V>*Q{FR0bMVz0`fT~8ShHiyH-eDc3uBccZ2p0>% zWqswJlkB;ZT_%4%bX{n3K@4(aGAJ9q2y2^`MAoMoc@_0|9<=Uwm1yNBCU&Wqpd|N} zN-gnBQc257;L&LJQzvBrkbRHQ#riM4{PLwMmrtBH{@?=-zVz}-=Pz8)T^Vw$I&|=m zgNf3n9AuP=Bv~p*`kygb_reP=y!F;wQFS^A0u(W^`QritjVps(+MX+E9>Vsk_akIg z!CFyLq%-i$GtVXf6^I%xSXc-Q5{LuA)yF_m^%KXh+;Jax%-XG6&+XoQ?u|1WH*HR> zR%+_qZ(lIFe~nXRc=27k_Usg6@sN`?Ry<3Ba|vxj7}^nyJ@wR67cN||l8KmU5hYdb zAn=EeJ{)I(68#g6P(L{pE zfne`C_UMIk7vydU?WRpzk394+Evp^8=`GC!oSzUMnb~nJh?iRKRPzJH(3g3cU}t|>}o*XLUP z`#y89mIw8Kki$Q6J}>;BkMJC%qemh)N%TmW0QGK2$PGxENxOt@$*LJU(|NjLQpT$< zzw$$my^jRDS)jX^t>D#uqLMB|Z37_ENS zyWjI~{onp=^VZLwJ9p{QMSQTw3EtxZy^`2N?NxV!a9%Nvn_1nj6T5;J@?%8t5<&P10Qe)>laIx)yOXbcxJr}r~x_( z3H~v^jUXI2aNzl8pL_0^XLP~wR0-RB82pFDrp=qRNM+8ve(L7&)>ZAv{QSpQuX^mkB zB-dSIdp(*xB!p`yT>@#0QS&`-+B16HdUBquUb{#UZM2bnH&{Y#G8V~bn${Q%5-}qjM)wm5qC2COsb>y8sjBIE zIqsliJWQMV*Q}8UPGnSG9x>CcJ*E!(X$QFtFtxSFF{vkM%*bTpDy=YsJ=5@=wa9td zd!X89SSK~PeRsqCy8b|()qvjdtSc)FLTqC^&b{@f{4wqR=CjXeV%N@{aa^1Cul~j} zPoFw@DqhEt5@6XMc9MAd1B?4;`qljh_^@(LZi=puS8wMJ|L6}-ojQ%hZYvhjr4Hak z)se)v2*vWywPH}<>HrWGCpC@l?2Sl*2=_ht)vujB`}+JY;SFmwssh#P@J-2dIlB<# zUi-|aKmE*8U)!>6JFr9gJ1=b1Igp||a`X|i$Bv&oL5T>@3U?Cz70)+z8_am*V6Gct z$CY@Z6pq+^jU@g5^uh}c>|!>wyR?&pglXZp3wA<^2Oros&okX_B4&(siS>&ekv2N` zJ(89#UAlaUcz^gqA9DP*8UaJ(E?vI-PygVbPCl%@^uUuXRht3{TcM20BDh8~7Z^O% zyFPy6_>)h5`T8{hR(h*V?FOuNBYSPQ*XJ;_j|v%xyveXkLBucFg9R5ZUiioV`1eHE ztR)pls~W&b7#i8<0OMZuBPT&u{pY{%g;!pB$*^(~@I4n`z-^aN_U-JMGmZx5vYsg_)cRW-DfN>75AaovU&+=4sf^W`sn;rGv-TV|t%lnOF^F&Bc0%|BzQOi=55 z!+K;1Ny!x_lH)58Rk~4cG-X0JnTWE?F*A)Y`(u2E$|)7M2nDvQDG}}L9o8zmGP4>~ zr2x-jTs#VnEgGrlD4|MHTXSx*7O`N_*s%O#uNslg)V09+(j81tueugk)=I(Okt> zJP?zF0p@SHaLwyb2RNqmLZX@wENNY_i=QbH3AlUTp1pg&`sF7#&R+14p&Wjh$UzdHpeAybtD_5@wrvh;%5f=f^q^>CC*qk-S@-0voeB;^}Ju5*RVsQDg z<#-O`ymI-r_%b(_$>p;v!&GRo|XD%gO-WWf}0|4}dDV!j&ysH;J`^ek?^sG+87e-coU9o{2Oh z>c;l%J9Vd?{pL5FPJ=tnEV0H7Mw9csE7H~WiBl&`C;Hl#zw(iv{wbM7y~(th#pkB( z+}yl%YaXu7@oXI__1IwCD!cR`VeDp@?%X|n`t+g0hfbe7dFar72ig|+35o!hAfY_EM0NlS3A0L# zhnmPGcr~{HDWhX2f%F=8&|x#w#VS9nt9$1xv?3c|E9P=1(5rRz=1m)%TV-Gxnk5KE z?7$3hqOYgII>;eIK|^>*7sk$Xg!JiCZy0-e`NfwGA33~h-(JBYMKJ;{p^HZ#xN_~9 zsV7f<=_?N%KKO>5mfb0uCr)3Rg_aP5D$^{9xDGhegvm6dZmmdA>-9BVD?X3n)wjt! zfR#TgJ6^yvvdP&Ce~SO+XNID_U=J178qM`e7etUT^I^n!PV^Iq7XQgz5h>qhG!1aI z?&jdm9t`m(K%9|H#Pk${s6aY}_}RBk=2r7!>d3R#t8qN;7>sBQ$9S3ic(Htjc><0d zWD>0`+rQs5#5QYCt-|JCQHKW}Fcg(y=au5SUvQe=*cmov`K*_oQMf@*1Z&oyB<|Y7 zM;;mqWION|NG`9BFXVU{#OeJ}YIS-`C7^uurB~FW+4%@5Y}^0fezgHZu_`fdVW!t) zlcyFaG41MOTF}}UlQ?qy4D2p7O`R(rI7QucKJ}eU8{N5M`=%|Mv$=DUV34pm%~_0p zNzcq^B*y2?onsZVW85rMhE3bH8E4oTiNI(=&Aj%{dPcjC?+O9B~i$RZy|{K+7xOU?LFgyE!( zwbq0(#i`lK#d_Q_Uj$M_eToMP0OVF;Rw%`3KrNJ{=T^#iOjJ}wE73@0AUATNUK7h6 zcyO-@r}h3rAFtcZj5txcNdZtI;Y&VHl;%bGX>|tecU}-cF~Y1`z5{C-wTfTW6TXB=2iyaB0S^VZ# z^aTqtQ!^oCWO&it@!gq@Bo*Z`GnpWEpvvj%XF?>y5&_4gpaM)GvPjM%`ep%n0|R#K4JEgq_I=zcV$s!kJT&tB9DVQF%x3o>le1$S95Wkbhj~kj{ zn2tSqOz%(#qGoG|!Y!_~1JkC>>$hxPbLjAqoqP7k?`idwmtQ`7AV@&~!bzTBXAGJhfvc}AkZe{*Bnq|}AZMHVN$YqCF>*JbM<<$6q^j@X!J1M*wXV$QDa6LOzK;gCV;FB#R2s#FxqmbmVDo zT$2W~YmOUE-S(o%=)*+T3H@y$(W+AN59T=+JXX>kL61*lffcf|wrJd@srT0eM4K%& zld1bL*>xhiZe#YH*hp@v6od+E`qkG?il?@2+wy@A{MfF&4-h}|VP8LccBH$U&=$_p zT(gavKKP*zN=xu1ufF!`1N$D>vUQu*QY(rxT!tD16WXP(-{3M08$L#Xj034pTy?)P z#ZYx?4wF({T6f)|DIe@a#7`q4ol7O*i;Hv#d%gXe0AkqRHJwWePX~1imWF zps25&MkRO%9`Vpahxb3QUncmq*IwZXh!8;mO}T?8mii=@#h9gsHOaOFZhwoD{9lE0 zA%gw}BD?8^pB{x2MbogPUKURmR*o}pK&BiC(vUDZnsxOV;-DWdQ-wsSptpEMuHTE~ zkHmuta_Z7d3|O_wiDWi^+O&BKto{713oOc;f0m3FYjHs7ybqR#eIU8SGXkXsVW$SR ztN&FW)*0ay+aK2&bI*Lu`0Z#}Q%&4amhv{!1!Qg1*~%((P-dD`aSbDm650Y$`@q-~ zK<;_Zu8SAXvl0S0_Phe~E==)qfTtR)gVa~L3_7gWqVWW;gFOxnLBMJ(p>}Er88ccd zZRc~84lIeC%K&OSGAGjM>8MR5vygE^JR$jBkJZh)l1&M`d?%}CA1qcFw`fNt? zYP9~Phxvg#c)=%wkb5!~yy8|$XR6<1*_$`6V=w_&Sdhdf0cf|{dhT$(|yv31qX^m%I_Aa4j4 z)7YgP`E2)$fHQI_tSO<2WV9vZ6U5P@NZ;DMAxgGt+tw{xtx;D~|5BScp&z!ql?fx4 zULKuf&@<*%IW=6MB zwNAVfn(Y5^?b?-thYlKWF{eOP09HUR3L$hEHgDcWTDEL+^1yoA4XOO~GiP@0+(p)9 ze9!{2941;#++7BahKx4eHTuR|zxmcVrf2=e^?Ua0vu7RAefhM#*A=u~}s}X4rz13K19! zJ^Fe`3`Aqt4D`qwcT;FKTW=#ZQPhp2v0hF>lam}LPJl(x0jXsjQH=l2jK^GnqD-GN z*Ug==dIgverpJpNSD~7zlfa1Cq)ytfm&E>5Sb|0`tRwdzwy*Jp8x8TPloSYzwjqN_U=dDb@=c>BWM=hpF4l<^)qK} z_jTaF{z>7#-GU|tIfvVjzdf;-0*Gg&L`g_!_H;d=j1-AP*-2wMf1X@e$eAk_%8Ef8 zmWVHyrE@|yC6&m&JJ+w*ErB*!ju)$q2^xZ1uRz=Sb-t5$t}qxnDs2>bqGZM__W}@w zWa1ibGrjLEHr1r{-FtT7!quxczW(&*j-NR3^{1XbbNVc#?JoH4cR%{@(W8$Zv&)8# z?>cVj%9XPwzdQd&T<`dx?K^fB5SckR|@!Q(=H*0)$H1PP`JZ z5TAu<@*}uLZx;W<40!AYrh6&7MxM$IzingT*0SZs*6Ng`aaAkHH)b$evH}_?0@KLY zkiO^G^)5=n-L`4|dTG%T1LH@Dab*K5TM}S3sp4z0=+~|}85)B<286cM?*(jtsjFy4 z?WE6A;??&*uy@~!FTS{Y&u$e7u;{@k6lJP30hr)8jKD~GK}Av!(4#c2IZ3MP@<=)6 zusCX3W7{GQjK!8IP=e)GDj^$90^M!kwMt-74fn2ypyH+tEW>BLk9R|wbQyAHUxrv< zPDWbn5-p0%AnxD2%D(--!oYS7j|!w9<;rKY7q+zvCD+qGCjrFhE^?xZ!7s!>HWu5h zzUQ%vNT!Q8nesuTIE)2=mbkzjF9wi&Np@)A@Z3h7Ks9Mjzp=%Vk^ScFJv(zKKG$^H#zHpN1<2FfYFlwy8ntQD zW=zjC7O5t~&=*()8Be2zAqa`?P9asEF4^bQlLv448nPOT8jY`UTOH)(0j-lOL9T@* zaV0hU?hCBZT)hB@^+6+)svK#pH+Q>fnz!rfD6-IjMryFG$h2NzZ_bi=WnjwNW!zTT z?51eDprK)xBs=XR%a}_TfLzQ63urptzf`>dC*rHDpuA*o~^2 zD(}7c9RU_Vmn+WHPCL2hf4=jbd+&bt|4X?b6u6O5q#6Z4DMd5eB2e)rWDxys!3Rq| zIf6BIut4x(Pg0T;q=aHK48n%N`tIrn_U(D-zySjt3MHlsLp-kjG;?l*;WVnI49d{4 zo2resIOXa~*L>|Je)t(|=9NGA!0}T&<5|%a3N}`SMTuR6r(Jp~A}fE4_w9f1L8q(; zU!;^yyne!Zs}m}Njr+rRa@;BmeGM;_m`YuE7;CqMJ) z&#wCPDia!h=}-NlU~A*%&1QO?dHu9GQD6Mr=U#m2#h?GhU$C(?;77wz1t#z!)A1$C z4!v?l=BVWI#hgOynK42a*uiK;Bz)}JvE6*C0s)2Yh-@#d#9wosj zAE}1{zAf)6<3)0p5RSOJFP6D*>5{4HI7Y?kz$_(OsX>$c*~JSN9gZa6J8XlO`Ptr^x=X{*&tBlx&n)(Z- znR@8R9oj;0jmZ_L^XGarNeymKqDm$KKUz}Azg@I6qnMlrjrxz;2h=FCj*4vN2`hb2Xmwo@S$F95XI`S}a zlDJpy*kQYN?SA-?AM8JPNXMs4ntaEtHvsJiPdAnb?{jPIxh?kPM+A`daj)cpFVYJw5Ljn2Jl67L(G|03h$LKReeB9ok5!Y zU*iL+D?0=Nvd^lys+IgKAaQLvclB(`xx)J~NBk?_QJJ{Vh@gwLkSYAYi2mA32M+9) zVg^e-_2hvWGiL;{z(?9*>$#a?!{IWKpUduU{ARWOWBfAB$@4GDJmA)66=t7}g zbTCy_teJd`Z4zup0D`ip=nNB{M8sTFoWjOnxFuykLQ0ypR8+K6?0HAQnDP_+QS;|7 zICJLAteG=+?c4$2A|`QgmDK>q$f8*3eZ~unMR-e{8E*0=v@Vx^0txypnX#?Q1gaUeBKVN!0;&GU`kvc;R!qMMnCDH5@ z6|2rdh?gMH@g6rS$ty2wPJIU9#LtbiaEAKaThLfwO7_* z%+E~VX)rp=JVcklSS-`Q!ID%^mD&?f!sLXg$|;-EsMuzj@I}pV7)6pVra6Np3?jgU zNv>d|s>iRZcdk5nv%fgYqC|0u4ADHYH*~$$z`Al*ABt5H#w=F!t4dsX2M4|S%CFMl z02jrSk&-8b-BiH|Rs>7OVn|ifR=%c;sR)`$`B)yEGMp4Ig6}qy+w2>zLY7N|Oq9A& z0_e8x-+u_EU@%Sk&|*RM&`*#kQ@Fo^uzH@Ruju7+Cnc|RL45@|KEuB?%?sx*XkIAE zhiN5GKv7PmS}a^svuno=EIWPX%+8Ju5jpm=i0nIb@X(gcjG5=I)cMNa)vH!)*syNt z(#2>zMe5rQY0WRxoNL*;YtORPE2d4Gjw5g_qbAksjT0&o+dFd84L6FX-tpo~*WY?G zm&WOnXTJQ!FYVd8SJglF-g`H!T{dg>-0_p9TRJ#(=Im2XKl|xVf96YH{7P%Zo6aL_=%jf1cWg3_4FczF+N(7Mnj`ASH@*Aa|25tqCY~-3YXL( zuM&TkV#8{mdh)65J9Yyj0vEgweH4U-rYJB(mpm$%zWP!b2|~T0f+0hG4bdR?OS^;3 z3m2?gzt;Mcp!LX7nM@;|M|cE(Vl$=e(4w9=WxuKvt}MTc=&}aFW!FZ&2#3>C5DCt=Bq?G`kTBw zi7+b;t@!N{VvyNqoZ;E`M=@|dMQ&=OX%+59Cm2DEJAG3cr`pCSlq{m)-bH}R*(-Lf zj<1rGUAWLbd1?cVRMXu%c}k;ttTOZCh?42PaDm9-;fy~eLAtV7eY6Oz8TH*)KrjbsJaTdE0f1n&&snm@{JJ*pGFd-?RVV=Rfxk zo40J0rvJkq{Q2hQ1^lcRE_6Np#FHB~Zj^3OcQeLBL-&ohc+@}e-ABH6^yqQ?72qqu z9AY-OFYIuIJ;H!PqFHX`sVjn?SIgsa2Rg&9)#FNv*R5H#Wa$!5ckkXUy)erI7=_JB z6@=a2q5AxZ3aEZnyqGuhLU~jgt6r~K&ZFGS-%@UHT4Uq&H{DNj5C8c0Z@5H!Ada~>|7EFeiP z6LMtO3vi|fnd8LON*b9&EqyIR)G4jFZvkQa#PJpWA-a$vNO$?665a7(?b{ASU#|2m zmW@7CbkWU9>v)wY7${N9XyY+o0Bxs~h;m0D8>Co`W*Wh5)LubdtdO8lohqzYm~a(( z1v9~xs)r~2fw&H()Jy|w2ox0)EAEAKl-jkh<~vZz$_d)q1Z~EcRWt48zhHyp&X_sN z`?x69B^_=ZkdfLKhmBWtABbUHZGdKQ@X-DP!)oJ$SL%&qaYu`s+!JUSdk`j1cG8CE zQVntf^XQl+b@m};8UG+U@dR&Ps)3$9Q(289X0_zbHI zA9k*##m;Me_Y|aHE{M(aVB`qNS!7H0R#Zo@m`-I(2?8&|wsu&wV`7sr{=(;3=b~E@ z*CFwfjHZ5Pk%UxP-zejql_hJqlyF%VOrOhmtJdT$OS;iaOW+Jp!AWU1z4|ei!ohPU zR9BJU^^oE9ON8Z~5B(^yE9p+Pz*k=WNgtu+(6ldZ%Wyq-3zwBa8>1N|d0)f0+!{4l zlmGxg07*naR3(=$iCG^d-m=7+E>5H zA_=2JG73m(HOLtHS%K-NtGZu(@mbx>gV$bFJ>{OhqsX(*ZvCnMu zCLezI+pMd(bLM>bm)^B{xKf#jer#|)RqlXXvvycAl#`P;jpZ~>Q zedRsxdAE!b`wkrZ=0o55!WX}~cmJVxz3aYdV&Db_rZ!Gy0Z3myXI2yUGLSI+*d(XU z0OCRg0}meb&iZRMpe#|}_VRYt)rWrRJ$K!H?F89zGRL#dD|pTl?ue<=r#H`^FMr_T_bmcrs8rZ=Ma!7X=J(}CxLM#x-gTr zj5M55J9cbWysfQ$`t-*BhK47ed|ExLmMwC8kh48Su;NbYhJm&Dg%@O5o;`Q|_kQry z?CFj5lN+De{9I>y`>nU$)Hl%a&2M}Q6EZ6s8z+Z=gO0VX(=LZ>=6YG#x~}-O^K?iX9n639(yWc=5A;_rQ`R%^JQ!_E*^8O}|}z@9LK~yjDFa z>=+B)lEFm_=pXyouUbd`+~>b^@zQ|Ix0E1W{n3>GC=V-^y~2=Xw(y4bUVTF;{P}|) zZZ?T@T3l*CVj7!4T--9Ds_HVa45f{rX0FIh(DxtMZz$q#&OFlsUkPS7)ynsb_ku2X z%P5}j@qb`UC>6?SL6#lH15}!evgKwLXma*!OToF0s>~1$?Va7N=UOW> zMI%5|I+YXx`wJ}$UnZ1lcnzu^W#Fi-kuAdB&)BsJrbN2pf1Ol?|G;1F#m$xq^9-Ro zDGk$FUZv4uD79DO@GPC2YYilj!5mXik0@vWrIHviLk5=L6#IqV9(2_4f}QgE>pzff z7znWeNJQ2ni(5rVW}8d8Aq zO3DTiuQRaJTRmXPSviO#2OH`78|%G#N0}5WFU2-WU-Eu94_b{X1%!$fs`tEv*O{Io zc@5IpAyq~Zp^zV+pzUqGQY_H%w-CZr`?j^K)Bg&u;qVpL^$$#q%NPqGCIQYR5Cwl-#YF51q+H8(jD;|>x3l|)4@6O=24>clj_Z`$7hz2|*ihNbqQ`EK4O8K&>@D`pEa}(OSG{o`pHGGHq&u;ra4#hv#I2)5QypmW;!} z06c1hgb`;>o;*2!&h+EQk88*#ZEJrv0W>yDF)=uO`fS6LNtJ*jEdsO5NCn%}AfaE3 zFmqW+lD_@jN6_Zo_rK*WcU~{r-DYXY$Q^q@D)An}u6fa-U;KriJ$3Tpn|up+^QlET7vVTXpaMv?91>ybK63g0%f(t9CI@%RxpU)s$DTgh(%8^oC9YDn zX;43LH`M9*c8;`Y!;J-IPi3K&s8nR-zlWKtYkzsxVD9;1!EnF5O+5{4p3 z{L*Eq1gNTblrm*ccuHkeKsA9j24J4K>ZlM?-)xZg?3{?Os(1FU0{Eir-d)C+NiFx+9XPsXpsx@WhI;T6jF9gow zMlA1}`J`xw9EBO!O!l&=I*EcSo-u5X5EkNCBQ&SGIEvEU@Iy$<<-+q6GFdAt`OMUX zbbiXBX@Fp0XMiX^1xPg2XK@O2=oKd0n>gASyZ9gpmf0Z%kdfjJzO(>mu*C9z#0~L!(wYibvBN9S4L?g9Enp$Yeo1 zrIs*QjSYq;j~})1k+{Rv#oit}DzwOK)fA%>vNMqw7!b!o>G>Y%P!c*~qlCw(yD&Bk z719*K;8*%t%e(-gCJduUR+eMe#@%QW(CAw6_3*)gm7|@J9%*18R%3cA%gsfmRAR1G zst>v8TWuE31HOM@3MB9>KYsL8ct=&io|zIkWYxtJ8^#*CUOi<|UAd2ehF(dWzb^jx zX0B8funJ_*4oy)7VAfpLpFkX`9Iys)skdE?^q+3yV@T`DGe_PMB zP+MAg%WWIy&Y5BQlKpif#tNW-IrJ?7ZT;uk+U7RReAheQ^1#=>yXCpfbLPwjzhf5d z?J7Eja`bXxq_B@B!t`iGD;ZxI7$!p&;F9~GM#yb3jlXMd zcON}}_V_>lxBq;`^oHJxIcqGF)I_|7yIhl|Ou6sgdk!Bu_zz$C`jbyQb^m?u-~-o% zW5-UhfpE;_VHe{DCz@bOnT$gWdwq0Els9mo>;N0dDg6H4}A5Zb!%78ZkjcA%(%9ikie)9*Tgs4L#r7s@q6)X7d4F* zn&6@f3nla|snowq2@B4XPa38@;v>Cr0^+%0YFgw2!o>iWKK?k9Nu?g|?;rw|E zj1xBYQ++I8Pa$+nPIsXbRE{6Q^Y{jY3hS3yK4Aq+R$;6^M(;+b;myQj*syVt>FDr4 zfr6zHzcbQf)Z%AJ3I-?+JSh&ilqgpUXFBD;hS4@AkMkNddL^+Yw1zGRsEjLkGXo-% z18r-E5{N5qhmiH_f(Kxy-l+d#B^Y}3(G~|bqMgrJ5$aT{K|~-KewNoYn4uBT>7%xD zZ3P=u#{#BDW)wDg1f-IPC&Q#zETgYD()bjud!$!EcrI!#yGbHXQPTQm&9Su(sZNxA_R@C%>r#?eo?XWQebzrWTD)fkntq}2)MJ34^aVL=OJk{{1>V!Q*h9=(JvW6NK+*1_19Gct30YogpOf=A#D^m zTyHG<`nsSdaAj5yTOOO5Sefirum*9kEyH0vdj@nUi7PS;!BXWi$2d6EE7{T<;a+2MKZxjU09HQhb|E<;3W_w(scZ?7Cso>RWGIPwLKh zU#K6Ejed9?<_Idp>B5>-E8czI+rRYSx3)aDW%iue*n?&fNEF3{@l>45CB?ATal9bh z*nvKMmo45S-S(~S+2BnhK=>T1TC9m`xmz$;Jkx%Rfsjo(QtcUK&Aws99fz(#zxkSHNsH`=~>Ef@j4rOF0aJP zFYmtQnsxPK$L`yA;KZrZf)vM#eXh*3+S(kW24TewVDz$59NGoxh4#@2nbDY5@?DJ{ z?MP`>X>uv`bxL(Ja{SnFyl#gM9f9?CzVn`$P4mvTS(P9elEpC-P|oNfwde=^5V?0iGaa?PQ2YH`(vzh&zU{1~YRMvQjjWhM#5DA&4wvDr#Y3|*uz5@GpJ>T359nplz3#52SM3D`On_eu{0P+cD)~4AHVMT)L3^1c;wxR?OSgO+7A^J}X<%QGBsIE<}VJe{0>lE37?hj zrN1?U5(UqnH6}*{4Eo{)qKt1fLTt`>Z! z7k;sTg5?Zb0*W$fz;Y!BY|1D}T5{xAHiIWY4gzQ1lCz`^ zl0isxN&1*-t471)Wjw};Py^qlu!st+WyMG4*(7y@EYe7r2ni1I9&5|YFM6xQ{Z+N9 zyg)Pr))gqSJj~Epx@7)GKJ@d?J-77_|LdO&A35cB{`H49Z+>ystm$uk%k7{3n=d^0 zwTJ%GfBappi@o~}e&(|e&X`_yk6*3g$`@6m0TN^)FVvB_E519k$LO*!f>pu^1~S>J zJF8v#x88E&xbgL}A{nR128*$)vqJ%1jjry_g9rA>|G8|*f@#wlWYoRo_FE-V&|3C> zU%X^0hmFP*i(B_~6AN~nX%{NXTo4V4_K9<>sadyfRnwgLnD?kqfEEX{3=E$2t`a=U#{tCCrU(Wnn(Iui)(Ue;Kdaw(a|8zy6Ci-+bMl{`p@& z^X!hbtDEn;_pSfo5B}o!fA2RqP=56zzq@S7Ot^Y|$ya_8@`Q6TA0vuJJ(; zf%r40qe*g2g9uU>Hg?Lysh5r&I{5O7n=f7H&6b$KzQ)NDb>;ApW2;xMqOJJ!FJJCG zAwX)xsKzOik`FK^ASD-hW?&>P;vBU@R!B)ue)&>QcPH;u420w#8rj;?+TPY`>F&Y> zPDIC0n#Ij;%R8d2ra4U{tEaaY4z-Yp z3|4STCKZvHXxRSb=~GTyqSwIR(cbamHi2I?i4?O+XM2HwN|AKcdE}*-X^w$~9yGpWZk2&Nra%UsVK&XAfedQuqEOfbBg_C-tXRn>XZiAltuJof`hrMyY_N3c z(rT@Z`rv(uu@YM*(Gkutfm=1ClWCO2oB_zApfb*ENrSBf4ZV?WrD3)Xo5BI`78w{Q zbV<_qc})}4l-{Ke71~lWGcb8u0zDk23jfksqpY9GNa|f|BN6VG7A#n3lhAU!ZEgYZYkUYwl_x0%_#&7n=e0~^hKm!joNt%~ zxSP6@J@pa>GiNCrz#*lsAWV8us0ngi0}2m!yt!yyEQ$0tavrqgGJL~MG40?0?_`(N zlEpn17cX?b{POmlJ9itxM2<+TC?-`spb`~lR4(WNEU^0IUCBC9%xjwylf4YA88eE# z#JUYBGjj+FsljJBmYa%aCDY?!GT#IMuqC(|@N@vhmZ*F<6{6|6r&yiMsV{6xOk60a zI+~A($$Of&IN#H;V*a9)E`uNBOvJ*U#WeO@_lxs8?rtstz7o@kOP6?_v0QXra24u= zB_x9g{}zIlvUw{iIG7BB{G=Buu(@p8$Fkp$n zRMU?DMui+0k3RaCQM@u=Hf>luWBSyyEv;Lg-+tY-8%%}oedj+~v}nQOk3aq5OFJaQ zOPfF9LYShdK$ysL3IO~th-a~jm{1vvq))SL?Zk=G*Im1D{l-mWMvXjnaDRJGj`$t8 zJmAP0lTq@vYuk%frldLJ)mXm7)?Z4z1#@%!8X+!@ximWI^^hjK71UwkOa1%y?Kyqw zB)3Nuk!>eSAnm`jf8VYdGiHt+H|ET-Q&encd&jl|XO=7xYjdchtu2v#1CCE;H%#$* z+#Y2XDsjP{-P=ajjlKS+Tbz$lIL+BCQ4$(t=T-b2x3Ndzf0ii~;^gP7r4 zd31jS>Z;px#F8cRzw_`9uDNEdIHB&YGppCUgZ0_jaf5-?>-{Vuj_orOIx2i zee$FQ3`H9zP0;9pgNJSH%xp$bh(mL;fx+k|aMt!E3gUvPM@L8d(@#G)X~OuXx%0?@ zIg)~%t*3Wwd%^G_J==mU zckMY~_jUdFoEC>*^hs}Vp2G~Z0f6leNcvGo`stl#5O2kbrOd~pM~+RJJaOsLMFF;0 zV^{)^qeqNt>*{{y>F37OjhHj1iG6h9*va$fyH~GRdiums(HF9b8ttvu^k2NZ|L}=v zr@AMM8F}cy?hH{8kO8R*^qMgT9R27#-+AoF5lbyASFc*RVmXUBp+D)Lx8CG3muhM4 zSigSxFaP5E;9>9H12^BaX~Tv!Sk)%;J8!><;MgQPb!x+X_r1*^WU};~ho7D|cY3Uf zH@m1ZTy(WPL&)KyXYRS@ZK(e{zxRhr7B6`3d+z@yzxKg3t5-bn8XTGn4J zF>T3}E9$&Fs$TOQCHynSLqbLuo6E*ez!<2^0Ht;z4P$s(ITeT}q8Rk5K}cL4)J6Z7 z`Sd0`ARY`sN0Jv-Pt->D>Y4%TV$UTZuv#>v*Nu^aF#%hpv$5f{ zqDPrj$0?KAGTA;8x60ydP^)=!v#OSs=Ta46>?J}3-PhT05NfTV~N3L}xdzCI_Lojr4gk3SY+ zOlNr|CkPTXg#a@J!Xcd;hvUZZYf(jfU0vtJ6gnxm76oPCFNC7JXc%USq0mB2C1T#H zn5|EIr_O2+uHskw>F&KW$oXAd=^;}vuT=2=nl-X=Key2)qLfCpSdq1A z@bwM6`nM7wQ#KM1Bh3wgDZ@<}^IDo4YMstEa9}Gmpe8GFZ{rveZdQ%2S%8M6=8vXFceusPCqpH^_ zFm$D`7BgTKyNWi5K*l9LV(;!<`*!Vg0^s6h%S6Zs*!1#@X$=krmyvnWl*WDgc9=i1 zP4*o*+1YXKnVLuTG>5Hkt34Z+H!W^?%im*+)7=Lq4Lk#!alwjDhB*bg`V^!;y{JXwTI9J)oE3I&+Y zd^5D-@DWpI%v`o&#pKBixDcWU284P*vxQC!CL3+c&&o+snN215qr&gdU%y}jMi|FL zkLLzF!Bj!DzpuWOp+3C1ef!H!JY`gSx?tX1W{oe04j04^L97V<>ath97qXPf zi*tIdX^^Kmw@RbMWZlBgdC7nlsG0=OEkID3$R{tckwB8K}hrPnt4u z)|{s1=H}6Lj=_@<&rmcGbp(OZA6%7?%OV$)qk_^?44?SiWURF-{_;hZ;NJY$c0z{LwhlH1SJbIO#-_uqdnel|6K`$g)C3xx7Y-qchI5oS#@f zAy&>;zxnpJ-qGAV|BpWY$vbYp@s^ve#WoLq?c2BAdc&P}-VAIf*^y;hx~9fqb10_w zLl+J#7eeHe488NyI}qE^erk5p+l4=F+a^82%jRPYruvB!zW;+~fBRp4)O_qWf9rR> z{LlZ-Uz02KFaPR`{4u9bw*z+LhPC(I_l}b%&m3ty{wE**gL7>TOE9|RV9?Tf8q}@< zq(sb`8KzD5Fj{n12IFWze;~q7VMvaxY??Xy__5;#QM=<~DA#fh)?vGg{xXw#NN^s^naqFpgVErXz5PN~eR zxg4;yI2{KDkPUTbu*p9r3dK(_eWismfExP-YC5|*4aTai=C_2)KuNve=bmmEG2*;Y zhC&99?|yh8kWoZ&h%nb;^}3~MYb`FBI)Zd^He-7WJM1nrYb5 zy3Z%Z3)+M4M3Oblo&zvB2-D915lT??L)b5-r|NQ~k7MS|&T=RGojZ zhcYv9io+$5!9a@rmD5=0s>^zgp?H5yudIAB%Ds88%c zZNnx8GsRt!lo}DUiM%pn(xC!H@b~6d`YXfmr2xsjiN&rKsJ;~gD;4I7`8#wG>q2Gi zg>qNkxbpbT|CZ9|8fysOBuypPVl9uN_u&O?kj|Zg>j0CPLF}g_Zh#$gdFAPDM?3U9x;<6=}{b{ln*nwir63< z{X!73yi!wh_MCHhv$vb}kk+W@V&DFwEg$;TpI^3cwuAQ0cc0JNUw^;&>*FWV@x(er zYBTa#X@eEA8vDouIx>kB>9?Yda*SxYF88h>Fs&Em5T+~&HG)zmXF*to(n1Aa^YcwE zHCQ2XTI$rshFP;_c*7?7haY*|qW`j`&2C<^VfnrTN5sad_>JX(qGIp`6Pe_Z0`wpt z{cv^r-FJ1F)j?Pl&=AwJ^Fn*eq0V-|k71)o96W5qZ~m*_WNWaWkk39HU5Et`n(Aw6 zrV9Wtdrnj1)G1QH!%H>7c?T89CmgX3wd0(28AIJa)7r7LS)h!QR%BkNqx}w_Jokb3 z{mjz&GmjoW(%zYT+&t$>37hji2c+a@-L~|V{oxdon&rT!u>4;nKOKWc9VPZaMfG4i z!!|vGb9(9DVZ(BC-B{t?{g(u6qMg@_e>miFZ+G6#GJ<)O>0p|76g6_&oMh@5p6oRX zH)hmcyxc1zt|dh}h*n9kPvTVAVia5^6;u$0uxlb^YwgyobMnc)14l1hygV}tSacQh zX*AZJzp(ke_q_%4pKdu*dL5M6EUDKsXU=5G({G4KT{9gqLwHbfd8#3DHzak1os`H` z7{TgVq!Oc6ffW1pAN-|XcrT++-nDu2=8!B)X?yk@uv~6!^rQnfXEt6Du@7)o!@{8i}>!l4a0dm4NbvnsRPL*bMEEkx(1h zC`FKD1u7$PV&sh(uwirA)~#C%1Cu6CQ3gdY;h8h1CC9>wS!_ZB#)7Fp#udYSI^?^j z@`i+m*;uS7jfQ(KA31vX+_^T46v@oHk&?pvQ6@##1x!CGD)?eZa!p`u7wPac0_p$3 znxiL9$c&sWlv4q)X4W=p^@71#1lMq0BG3GAX~LxejZSoU#kkC1Gn5YWRcBZ9c6PcW z@X^waUfT&+N--o1au{BvDO(H|Ekszzg*$4cfQC3E>U~fqh0~`TF5V67WPz)mrFn0r zQGWo1plFiO07Zv?r}`4HJCwv3b-}USlgnd@P~t-M6=`^;}X_C2BCicHs}!UV5gp=wcdt8K6|6ikMfL zRvYh9-I#0;0Wd@uBS?Xyz8vavOhP0&L66 zrBJ+lY6x3_qWW8Q<>>b-4Za!}7(Cb3dDmTcr2d+kyY9S$QSyTy#(lMR?FxRB>C>kE z@y9>AYUR?KZ@&J)uYdboTX#c4Jx|Q*Fc^)`Sw(BG@rhiSA?nnKv5tD`AMK2f`tf+2 zlYn5%U3)zrm z<{Gkiff$psN}FV&84Y7Qt}Pu?r|0;>J^Kzz^EjqqtevuBCpPSRY3G`?t8cpL=F43z zy%#46Jm#o_(vv4!Ufj03dEr7^%sS5JP(q$wN-{E^_07wMYwTw9)>-1g@FhchBz9UE z1Pi0X0%B&U3$x+QoW|;kWMSTj;*Y(^(y`+$cieshR|M1Q=YICR0DJh6?=?3sX=<7& zq-NQ&<|m)owqV{g_up7ks8CEQz!)}|8exNY*0Q?6#wB$$9UQVA#!r}hwzWeCOSYlh$yjVd>#F&o+@|4>4f^G3u=pVMt zv=vhs1u1a@q2@8qi!blqxOU~F33X$IwNIGx((dPOx#dRrOMBW+O{^bBsh#iWywo@H z?Af-xhmPyQf`#+7%BlcBgE(^DT;iQWMuK55467SU1CkE7uCQ7GV70MKIgz7E>X>9a z@W5j}I@_eVD|@+xkgSJ(qu)o=SOPYCw%LsvWAc>8^ip~XTEX&y|)}acJk5h{qT`TpMB>& zXPRcs=sMrclkrLwum6@bj2J@fzbEPkiTzPqw;o_E-E`Bndiu#veddPi*IOpuy7lF6 zf9Hq)>bE}nuKVA9>eQKI$4_inzZ$nZ_V{M0Hg@dXU0jkQMDLwC-Qs1naY)pSnF{&E zsK|VP>W6S0q|M#T{qzwsvW82b+z+i|-xK+;oB5R*@JM_>q>J(yK4Rv~S?*!JaAYFl zZJW)Of+*wiLXn!thPII3OP>0Y<()ZMrA@2(1m+OMVo7NfHRvDMyvX?5xA#!2{J;=|!#$`bO{^E4 zDT!lFkWV%M=WGhLX02T`^q`2b6=|?BG7po-;c`q#^@3URdab{h4IUyg;kvT^h`J(H=%+kRGFk7Jm zr-Xrn#fqRXUH%o!zwyJvy5Ym?$Bew_IH0nCaP=K%^z^mEWJN$Nuy2I7bAKDPDi7q< zazFNqo|_IKcF7^fZoSO7qjMCZHT)|&#UAw`7S|wljH(G^VZ;*9CxQu4L$4?$>PTE& zsRS)hHV}kYUV9;orI3RMkNwl%_$^P?tXcc?(_41!IhwSt%DTTC`&AMy_yUwji)_Hcq$6L}&N;Lx(fyI$Fo-C|1GH*iPR~-td}fdMZfF zb$sAhf)oii=9!y5ZQ6#7>!07Ub%fFRl0 z8YA8A{YSL2>Do=?nTMIhDL-$SbUBwGv$HH&hk@~7{!l@PJe)VtT9_`!cmmbj#+ZjGN~YwUIUr%j@H&z5?Eh9;fCuse)qc%-+1l%F_Xve z^4E=@xODl7=B3Mf<*6E7Co+$&k^hhdaIxp&iBoYbEm^$S5``XEP)mvzhs6lgNInj# zb~v?{v2e5njsep<4ge0KnKP!XTeI>rfB)q@`;Xjl`>j)_&fK{er2Oc-Bp7D$m()>_Oam{G}HWG-tNn#R#I zw9`jSu1mdr_Jb=!1yOFaN9x8O+83_8j_qh>Hlqzb=fAR==mGOUJU59f>M znlW>l)HqK)yY;DOwk}(;Sg5k>?N4v{{;jv(x^~Ul^XE?T=#R1SZQNz63UK?_6FJEM zhg;JLhbOK`IOAnfLk}9F44-tfchL8Z{NoON+S=N_Z{M*gQzkH^=FVyS?|<~C-e0+L z)i=KN#H^W9zWJ>u=g*t=na_T`rlxz|yoG#h8uG#$uJn(N%UF;W)$LAZ|0`F{`Qlf; z_xGRsq9W60%-Ozu|6l#pXY7etIDhIt`Soazax6~PMw^=Ev91*VPk-?7g$tWku2}Yo zzx>Ly#tAL0T~iw;Jn+@83(va3!}=4QNEiUM z(ktFfrSIj#*})@65*eqAGa2zH$TeD8!5UH7s#kLg zMOLHXORrO{1}K(HP)i3W5VbLdi_YUOh;~ulXkWhQYZfu+!_q-h@eBb9fQ(`xf!VpOYBG>{mg?ftZXX;Dnjz5VDZ7 zFicFn7=MMJj3Y>6F^~~RRwyXWQR0jVNR0&r&*cMf?&!BV<4J-a0EQKUpvpY(ZhUlZ z(!)xs6DneY5Ck5V60Vdl<rH4I9DVI|w^EfpJs*Lqc%*n)qx(Eve2jMqL zL$~}uHx0uE!onqaan3w#X1TCV?h-^#JbRX1@%+dE23+v3goGeSj154;kAT1-SC}*r zUA{q~hNFMOU10@e3JAHbeyM)G{*z;ly2N2Mmq!TniqZW_+1DCa>a9xKZgMG1oa7(8 zZ$G|;^0XB6tpHa*%2KiOfB_p_lFmdbpwkz7LKqm)M0(#;=G=*m5604Z1q|9` zRDt)5`bb>(=v94qb&pY+Y2}U`-Cz9jBlG6UzCGC3Fmcw*Mxr4~+LkU)HQ;+e@~cZ< z`Ce6Sq{B?*?1C_LaXCi*1agkDWWcy~;>y&G*Ie`DlTRKvbfT%L$+2Z~<}GX%o@Ebq zr=-}!$JEgp<1%6oj6QJi*tXsKmo8gs;&rAaOL+Q)9|I4TEqVcsaS~AB&`86;%2vXz zE}0?MuV2H!lHh9f+V#_#MnCn;mV54bdsEZw3+LONJTY=)x8?M%VZ*mRx%JWSKdsJH zt5>&ocHmA6MvT8$av0i#BGWr;7h)3?Di`q$DYaY z*z@Pl6};Nr)2-SpQx@%}l_DMN_(GDti}al(B+S=!p+`njVcIKJEc^VI9$LHMn(>qR zpWFQW2Y&ATLU+46JH&>Hk|Llir5B!h;qSii4XwQ6-h0ILx14E%RT*@6*^Mcq#w0qG zi#p42m#vmu6pBtE=~7W68Hin@ZJHQud;*I{aeDY^37t^JN5BLaEr&wCI3gSFPM>L+ zG;z}Q9Xls*sSXZnJJ+#o`%b!pzh|mLnupcy*}G4Ct5xza+Q>u@>nWBnOQlngHTsL= zikE|_pW}p$sumVSa1Jh6v}paB)mvYB`SB+8%B{EF0yYDYU<>+j^r*?Z z_aFYocYg@-5B%&0Y+yWmD2wCbWQ5x!I@@GdqIofA(#xeG8b8!cS6_Pp%t$6~y|61V!O6DOZOI%Hm;5%iQ?gR&J#3SQh8Skn_ z;vF&ATS+j5rcAts5as0K6hYB)M+I=1bw(8+7aW2SRP{%acDMXe2n}@RoBEl-)E6}cU7lMX@I~-|!7ejZuVq0qzO;g(j`6$9Z@ig*I1-!zA$n)v;)Sk#yZ0PFc1)3l z%?oe4-KbXcxi5VAD_{NEsufGGzwR3H5~#+=W?t9Y*8c5BpZx33KH!}XefUEvX|ZzF z*1mVwPU}`$Dbo&I8~yH`m^jW4<`fQ?QNdJ?URthICp!!aAVQh-g|BsondO_6E_Oi( zJ9NF46jy$Q6Ur$Z$T4Z6?T}dtQ35rQqiBmOufFQ9QvOw}*mLoMd2hfd4tY?k5N1Gb z+0>GQ-QXBw!GifKSFZTn7r)-p-o1SFy4mvzvCpAJTQ94CqPV?V}&F=RSlZ}yPR}%^!6QAsjLp)bw^0>iBJCZm%j4A z4cBj4yLzQM@UWB-JYG_FKKl4`|LuSLwRdj5`R2uo7PhpU;Re|D(l(pvsuYV>?nD%u z%;nfQ3DxLg14A{;Fm=oqBRc7(;UVEv&bSEiXiGMki6$xp1Ng*c`PI2J5thMNko@OX zF|aJ_eBRty^XANsQP6kEsW_M|LvlQ(WJ+Lr&M`y|9RzLJ$q^=x-#r5pBGdV0rxDzWpgqWY`8qQ3Wa-lrni*ivKh*~FOl><1boJJ3{hSsovv?mx4nO27zI~V%34U3{F~$=|E2uQkH0L?Tr|br2L{;FWBqd zOZ5dCNG2>qs@d#(1P{QXTmnx34C>uyZ&9hm9tBGJo5ssRTMz|0FS!ywhrDr25?6Jp zMsH$TDFS)ynyg`|G|JgqRe!=7g+;WjS#MS~sN6pf)mil)4O9#a+Kh(D`4){;E?>hm5iYDrm0l`y_hNB` zFCaqm!|dU(!Hbe%^c0gXHfv~8{ZC+m8Gats8iHXR*kNE$sl;t%fu}8gEV%Vrc(GVZ z8n$SojzE%7EA@hc-b+$LGonn2g5k20FP3hiangCv2XT6ms;VCSlwjbJZ7;vv(9qD>*x>lNDGgJuz4qEuCr|I*y=U*&-ua2$Bv&xf@e3!eZ};l zI4z!@JbsF!sC2UeDFgtL=?@iz*!)ii4<2oA?_9fKongM7J7@8t_r2$*KKF&M96EG3 z9`C-s*>mR5xGiT-?BBcVvB#f&Y5Q(XtY5$G)|+oYdZqy+FI%>3$BrHHYv>MzC|HX6 z1UZoxr9?7|{w!=oIJ!gU-+Y7F{&NJQP0qkm-|JGu3x|U@gF?h z(sr(XGF!G^VtVrduRy8L9AkfEFe#KEJQ}U9U*NgSrL?E@yR0}+t-=Dlb?nG#`A1f+ zTEVtqAkJ=@`GNPm`|rN+rEh%WTl40j$`naWr%amM(|!KP;l0m4|H1=be?+f&_}=oi zyGuOpIWv%^rlzAuj~+j9V)3}<1aR=t7I|junphq^dQ4yR33FY!D%Qv^-^!2bZ$2x| zx%#_$r{bc*QyGz5m8v$IY;A2@y>hV=U_^bbSh4u~k3TVK;uId~j8Yk2(nMOymvAf; z`!C#C@mA3f7295EdT4ZyN-CmS%?6P`4{&UIXWRS*3sef_X3_+_#m^s(^q*gWT9zqUwpCU79;|rZXlG+7 zc?o=x$DlVuTG9LlEx_ol8x_tg0|?R8-qM<-b61IyWiz>k&uDa_FfKD!1VJ$4Q|rDO znDeXD83~FtpmZqb_PMrm6u+;fN6NYb%!s5d#LygF2%+YfCn}68i>eGVu}8KI=HRcRM-JtkxElwl7a9zIE?&Gu z&FM&Z2Dhe_XqS8sbfwzh%zsy^jIQ3le~-purMV(R=4So848jt077#}E)EUf%Z^2gH zfzHe4&Yg83D|j`hQN0gW_%8-fuqx697_mODFhW3*l+y$$8Nmg`i5!KG1mEx)|1B$Z{D)?(|`Yv zjSTX<-FDk;6j@7aEBxZiEl)pv?Tt50o-&!cMTUwnmC5v_0x**I4P3M7I%fzwSI#WT z=Zu-t?|;|5-}&~#FTVKlSHJcR{~&hEsNp@AGBtE9Tef8V`i)*YeDnx}U?dZ}ra5zF z&YA_cP*JS=ZXF#=QbXyu;|}69haR0DV%Z2#7_RVr)Qy^OqUpHBwqev@)41Q@D7$LjkitF_M!e zJGfqqt@ST;qE4x>6f27T$l)VDeC**R%T}nH1t9|IzIVOz!3Vzn!C(F8hd=ZyH{EoD zv{+U&yEojCZy3orh|1K)KF9{_&kSALmhK5_i0jtn0)LI4xuE?d6BBe083&=xkO z7Qw4ucCIxqTBt`tzB;=uu#j%L@p=m9^I!PVL*M+4e+s_(vLv@*<29>REbHm*?mVAZ z8CPTDv^(#zc9WTU^Nl-#XMIVIphHtWyMFE!{wF07%aKQ zsZ#@Gn8zT>KES#~eU=aqb?PhKL-z2P@yRUWxh>Bjf=WyvP0~pK{onvDQ0Q`y;2J`A z1P6XVW$VoJ>C-tiBC|j)7xEoFdekmLYy+!7R7!XjaSQTNHh-?(K^-7f+pL6UE@@sM ziBbJH2b%SEcC?@G=)m?0JJBmcPk>1UU{Im@^Eb(fI)&B;P8>w;;Em=BF~V|HyLmZ?R8yf^ z={KT8F_crG?6*T2WyKlWt)jojj~uox!Np!`h{n>EaA_3n_>YI#y)>E9by$~X(C-NyBy#L#bcRiptzJs;F;5B zIy&~Tp#mGO;JH*o^t!p(5dI`t`qgp;d}N zl5Q4$L7{5!4^>LF)NNIxCPs}hLWHP>7K>aI1mh{g3``bILlKh++b^R_V3Q(1Anj4~$|VV@CaWUalz(4b!Gh2jS6U$00EKueN6F z2pPk1mGJ2!x%Z5(%mv-e2vLGC z$PinwT0WDklwAVPvUiI^`U zOfeG4ls~MpUn%ZM6DR5iuPS+m{21>Wsy_)GMmSR=1fFJ6H^-|>y79?)nikCFh2$e-m6hB{9kyiAid&$ z%up5()E6~`=$g||re-8ZqQ8+NILb19aGAuDlu*$k&#@tjOd(nrK0~MxBpEdWFy#gR zruz~19C8=>q6wRz$S+n*L4xUL1Xc}hfN^jVNohaV1_=E@Qy3{?HJs0zE{u&)PGQoj zuJ}l~qGbH_Q_UXj*}3h+k%PtZ$O~)MBt+Hw^2(k%ei(iq`L zjBIajE7m_{m2+kwggvSr`X}lj@99p!D7=g;!p7JedV#V`mB!zPI;nqV-XxVgOLJ zoV<2QqE9AaamW{&vh1H9Wv#G=3dVdk!P%k0H*Pwv1JVyPOl_p%6U)VPmwcQv9J6Dx za+4v7Ab1NU2s7)2MNf?zbQv&%U zwkqf9fYrKW22Otm2OaH6{F9af5!F&j3rQfdk*VTOE;pm2sWx;#13wBmAk|PwOGR1Y z8G$JVor+k+ZX;u$sNtYcomLyzSf@_HD#d-rU3XB9o1c3gsN1&h@DE6nCrz9o1L>?; z3l=O;_t7JVjcjI7%yhmE%~;^0D~-#=#*1(l(ue26MNxSnQJp!g#PRlmp6`Ov4eQr< z(9QgkoiEBu7tLs?X5i6u%L&ruCS8(JBUZ1|*cnr()9S zJj85Ql+g!~_;DQ^DMCwNi38`W^I5aHXHuY3*aN-5x_<5Qo}NqN#@FG?Rm&H(w{;i@ zB}0(9iQOn%1ZWTiV~ZEf3tEdan6fLe4JGkURy;&^*@=-r7OEKbhL5c~d+KaoPoD%9 z_DD+I-rd*B99+GM%dmFa%R5HZO?I@I{<87|VgF^*30#s$9mz*9Hu1?^HG-~k9V|pl zI~iSK&*ku+NGm)NQhQkG2N^?(9t2X}4-N1}CSZtC26#8zPZ5j zS6mVN!j$C!qe@AZmjrTX8qRioz03|ra4V3iXE-)D%#S3uG_R3o~vIP8&lCdwfi zfRz&grZ8jNam-|7iWtYpNY7O32S<^SofdIZZLB-A45!!D+FQ!Y(%Inzh-CcNh)NW} zHoB$>HDm~neubiztX`95Upq2ibj|Em|syc zs_qNy31}#lFv(gmx)JH`j7ie>4^Xp35Tl1f|w zv`~_^2c#D??M^@~U_%9ELHAZ#M1i~%>&N!yyi%$Pv*b&uO$WnkK@^)Q6F)qIMzm2- zBmg|qhO`R}(RwAdV>(=rk90^6tSM>dj`1D!(>q> z_}ItI@^bo8^<8CPH8&6>X6y?i;XcXvitmTWi(XJ$#JMy)YIGqDjnJQgpp3j>vtXHw zY9c=bRwY`$C~aUQl8^PMn^EW`ltrVu4a|5RLOocAy`inCsw)u2ASQ^&8}kwFDVXGrD(8b^l}P^{Knb@wYkfy!9yQZpl6zaxBgKc6qh@kqb|nq zlWI^pqN@gEiQsN!WFyx z%wXHjIsa~;sj2A$?|9JY}j zmcN9*(hgZ3uPD1Xlc`JMl`T1msu1ur%rME30Ow&IHN=$#l}@3TR%O(P?b~)NY;G1* zv3Bhm3S#g6{lqvu@9xgtfLf{ioa@|gHDt-s#jujf2eZ-LzF`hiEN11A1q_|GIA?v8 z7spU4yH%zdp-7q==;yPfHLMB7$!K>mrnQYQxMzjr6m4xuHeo3nLsI5gM(?`1tt9$@Ubq*DiM%SyE7r|%rTE25_%2^-XHe518chw(-O8{%?#*GO@kT{!0NABFQ z+d^}F4%k^j7KFNIXfw>pCYlKqJdYK`a`x1zc?%XQh#!i^$=+|aW-NrrG%DBNWKEXj z7s(pxduBDM&0{QKsLlL122J5i3e=;plUJO-85M8Vs|VSch-o~HuJF6)A$5hAa3|CE zg$tMyEr;~afGrv$vXe1wsUr-lMH(SIj5RSgX^-#`Ob~}@{iEF-o|b?T%7|^J)lwY8 z6cwTxbwxycftFIR)US&63z4t8|4H%x8}*(3X0rxE#3 zyabH_R(r)VNcF)+L?u_%R_S+XNwE@?1AApaF+3-pl46N=hvh&Q7$Q?;g_KJVa8*4) zV7Y*vtN1WR^K4-WB1_mIxsaxf6R47qUG*nOU9847A!s=kg zf>0Qr1TDdNDVEoiE*-BdhG?zQ?FzrCFr^Baf=ts<=5kno1Oz+ElUW3XF>NYK#9|SR zjwd5D3iRI06|OH646x`ho2f#v*F=eMkO-p8qm)s7`7c;h3sBQl!XIuab8?7rGqle1 zHCR+2t>wIuYPJRhQw8Qek`_89HZcYS_=m-xtyEyn(o9x+qF1ldrBoOUgfxA9=exSC z2#=@E#*g!QK|A^f2}D4u8H}tjR8*KJtsdxa@Q^ylj%rGmtF@Xx|is;3M zp(hGU_a93-fm`WYfh6z0a;cPgoljJYwpR@=#b=A$`*UQai?G<9OzN$RQ35v|6czMdW#N8mtX&^3Ua?%!<8>FTrn|378`O1 zk8dcP8-Zl#QhOK=QZ*Fyl}AuowOX7P**N6}R){b}dqXLlMquJdfJl5-SgI;&fKy2^ zFf$&9@Fi&ws|?p_0F%I1MmkhTXoF1UMnBRjNNCmUe}f2cjTM_CIkW?L4XuRQKC9dk6qpUV#?L_3s>3x0CxE^+Ee46 z8f%nWsU@)`F_VZS2ogEt;`;Yr=Ypb0x$Fbr!SkH6!^&&zwZlOTgjQzZCn>T<$WK%|GtV2_WbU5zk|zm?QPob5IC_l zGjtOkO+K!!`}E4E&pz`snrIU{@}B6RdycbZKrGsB?*zy^BrsI5_lJOiJhK~?PJ>2w zWO)0|9p8E5b=A+FJCC-cs-PN?S68LSZf>z^?v{O~7)r!inQ0>MUQCfdkq#HlX;?c{ z!?qgX7YgW=M6&|q=AJ#09zT32XSvC1d3q&9b9rSGi|H7>))rXD)qxHI_J^HkjYaqF zc^io$U>^OOp$vw})Vc+kg`{Z64_Jv8S33x!IP$?=*)ryAvjg7~!Zdj0#?9M2s%dCM zle2vF)o&>I;YVl1B|s8n7xSkQIVZ^|fb3c7EJRMCs_s~GQg*Xngqa6LN(LMo6A?nP*inbt zZ{GawJMW=~d1S1Ipo|CY=M{y?lU>UV6Y3$-*p6B7p{tHik4$M|QrFjDBP}eo__+H- zi~VVc5{-ZgCc>b6!9j$}Jb6mY3LgPiU^Ib`n05QkoA)2Q^wJqIt|K-M>^mr90aL)> zxRmiC3}_bmQDYKg_$UtJjY!*9jY7jX%Yh7Zz>*h%kcxtd zPe1wWmp}iPXOEs>6P9+q{HZ}v;Iq#iKKOJmW58=Oe7Yey3wJFT)xD&|rZ(7RSHm)L z>9CMwX-#=wd_qmnq(@U_7(IpPZo&Ig^A>p(WePxZ8r6pYc<36-t2#)moD zJi|Bv_#(32mabD{QZuj60F>|3NW>zu{!(1620)Q>O!XgXNPE9{^3?af|2>K@V1PIC zr5kmNx!3ShInyKqK8gFWfg>L5UN97__x^aUKsy3d1|cpm%`HV}e+g>PB3R%O#RVZ@ zc;}t>xXZ*0(qFrPU|!FFnsJC+y+z*1$&D46AdDkwu&|CGZZL1?=-%Tp6zWckxkpuX zDv6%x7POI#v{g%4X6H4j`)I}bxqPuPPUoU+M?`*bqzF5ZE&VCSztLg{=55|#xpk8fsRa*&nCZW# zuRGikqY8Y$^IC(h_j(7AGQ0LTqNue%a9x1JzP!rFjL?Hgq#Gobj!J7Qqhxxxc75^X z$&Ek#pG@!_?R8J3MtFx`OFNUndQxGn&7kOXFzEtT9O6%&|GEFiV0$n|Dv;T`KsN9V z=F#f4cI5*)*B|?n>HGwxLT36_+!{^Ia?RyV=V0VQW+y@e+!;`&Z+MX6&B|F_mGdBl zN#|o-I5wJ43beWNBIrSRRou;8K6EZUGB8*sBZ8`+UGSu}fl&$v2KW^^zDsM2pGci* zXYyDD9@T1spB9_Ed;86|uYP`I_ulVX3~}nzX}QTC{NRT)WTNItfe;g~gxj_A(#4Br zvNmsS9<8))bV3a&D=-jf=rf3`Vj(VA3O5i$>=WVNw9+oL#Fyk^CWL0fdoKoz9+6UHP7b zgl1Db866SB03qf`;t>QdXQZM{s;d{U>0#SWYGbXB8zRfC*Kb^V|GoF$c;h=1PkPU* z#;uSm5{8!+@SjSyabz`{sRZ>r~;?duf zbaREPk3YVACE?O5sx>dL;km=lx)T~XHip%sOdICXp{(tLRaqrlblG&V1$ejwglSSQ zUblK?QCt10Mv+0{rp6-|W+4H%S>J@rfBspFCC`r_^%$b*^LpFXe)F5(y!-BZufP6H z>-0U%y3C*fG@8t=isP|4AD(Kcm=eUU45k~%IIcI}PWA~Hg(9UWRFQDwEaI7)3aP14 z;#NgdjAzQq%&22I<0R1a1@2`<7RzmDyt!ia5*ki}=YT=~hL>4z=a=7aR(9$y-Ff z@@acMJgZnSt}-#FNZr+drdK-pnS^Hzj5@X)IegT9KywjPn@a}|3LJXsAOX-82w_yG zq+}#VgMRDCtIg9p3|`JiYEJNx4AZXd4XptW3LOpK3ddnAQYbt%o56>5qEaWUzWeS6 z)__m5X!7MNUK;#k*{iQPh07d?H@-`$?*oZoS`}nX@xmx*G3CK5GjPt%rHdCpOvPYM zKCnZTGpEmJ6I?z+tB}Ygz!C*!qAhGt-arKhh|TCwDeB7~f2;*12fm0(-cFo2$;~lg z8lm#q1IkdWY5E98uQzS{2fyq@%3y&MuRZYP@4 z63d-LKuDIlh{t?<5RPh1Khv-xWi%9RsAQoQVj-?#_))9w>v4`zNV#Vo5Y+rAW8`_p zL;-^ab}v_*76sBkHDsOept<4*M3@+E8UQ3Rx&S#$jqA9qkTZ$yKfY@Uf~l3Ic>3vQ zPMmabrAbcgUpaa5MY~Ua{_~b$@#{D>kN|Vr^T$t^urfeLq68sAGw*@JKmn2rk(1$5 zk~)k_|KZ77LnVx|P`plq*XIsD=f=D5z3&0WYAI2_ zI9;Zj@QIhE2%=_{3g8zjw3>tjCs_y9*j#|G=`C_24PrfRcV)H`8jTM$A8S;kOa>+o zAKe2dQwX|N+cgVpK$GH?&dsH}R9K>@5%5lb(Nt%aFh{d4R%dW8Gz8GlkfO$A{`HN& z{7Y|pm>8y{g*VFm>#u%gt0}*LESZ~Dvuz&KdGHlI%8%S#(vn zfQ~9cyg;qVK68MNZrr$Iq7$T$hl2x0aB53)mo9VPjrR1ry0Od?F&G~AZr$Z26?h79 z&i2fiGZuOD<~vn@J{OG&@g+(N3l)CsR?_#-3s)GoC+O};E_lcj%HTtqH!mkNuYabNo zplN1;Y7bl880dN)3Tqy;ZiZ&beCEx+#p-3F!4sZ@NwqN+Hpn!?fG6{|& z0QPfXKx1|2B88TrUTynP97sWr5^|7-TH)y? zdr%#=G7&KPmjVw_A63N?T8q8udj2?*-Y8DjgbCzX(tgEY6^*u;Lif;LECr1V7cV#s z;jKTswc867oL9bOvqiL@eR`!HgIq(?K(TcZ@B+4xLk}0c>WpZlgSLR&x^)K@?x>!a zYa)0SQ??k4%2Rqw^cv>H1y}xS7Xf!q`1{$F&w%Rk+9+}1V6M@mhs?x+Y-P%=%8eTi zjww+(dQ?o~zB1VAHH(AK8cmiC2h6NQLDobzXccOci)Ev(eEKm8Yd(Uw#*h*JD_6XD zab6dJtpmmYHbffYV34#uJv0(fo&hF7o={%@r<6$ywJH6Ut(cjCJgoZ-V9FvpFKzoy zn}KpbjCNnt3?BAge!;nb)W1+sNG0g541udK6n>P}oWH$t|rl(Phspb%vN%sn;tU-*wPT!f4yPAGfzMiM;EqnEm(pm z@t7iY)^!m>zeguqewfQ2n?kVgI|B~I8p!EoZMl}(NE$$(5Q|+1SoFcX&UB1GwT_av z34`i|s&B9oi*3Dm{ibYH!|2u3BhMeb|KOe@03d~w>S&2kum##3Oy{l^4R88DA`Mm@ zF}$Ib%Sn@78FSU@qV-h@Q4FRH7xht2iamoyT-ot`s=^@-S62TqrT?3Vi03cX+`s4N1 zmgX4rbjl)B44bez6Q?KAkW^!RhQP%ULMMztVoZPL^l2wY7`*c6MY@S|D7(a4pd?&` z(?$YZU@hdF1PtmY{{d5(C6o~)b;M=nEcGPF5EOSQM7Y=uMDs2#E@n8B-DFw}LkNS# zQcg=oEq$XQ@*xYZDVgbGgd7cU$8eed(7YrLbV8FzSfGNjSuV*_)9$@%Aw43ujaB7b z6#xiE1o(P`)KjNkID7Wo)$2Fxi(~-_Dzssp5omZ&+^p-OSrqH|#*HF8n4Uj>o|m)x zatI~1N*N1AxvsQwq$j_l_99h%A^*s$B>sY-IbPAKCj=>WRAIuKf-EPwT>j*flgE#l zep^DZp>wRq$h2D%rCvfRoX~F~alM!R8B8NvW$RtnaOzH$LKFqmpwswh3A7;VUrv3A zKa_;BKMx*vV28&Uq9Ku;nfe#Fe026hdqM311`X>$TCQEcnvtBOPU@sh9LenXmdIIb zV~?%3cY90SbQ)WrW6JM#ZzHj^FDD&k7A-WI4=UDRnYCZEv6i*yKSHADVB^;xFWqS$ zkLW&whAbplRs)-Azhu(%VsNmwjJ5CzadvqL9SO*SM_l!^<46v}QEe_qljKH6O`t{U zfa*?mU=m}hsiHQD*~4S_-H;=WEh`h8IN(KrULC!DWlr0Vya{RQu;7g;!Jo7)ah0}0 zSFZ)1IfaPJ0F;ni@|W2tht6-hbLZyAmp?JT|Lb4>T7#!^G`R0kYgwYuXXKHiveS@x*joyU|+(}(8BQ*93Xv8Vy)%@cXdz{~Ia7&Q%27+Rm z58rg6r74)zjX=t4Zrbe72eH0}YOH}o8hZZRN6u+O zQ@0a?jAXeeM6L|{!1^d3Dv*tI8NXac6O^$mNihEe3|t}sLnl2zI&Ez_B{mv1Q#}{U zv*FSGdrFq1usmSP7WCwKh`hWGYO{sdmqjj79#lz23LABI*#4E-Pd>1rtv5jtTGM`g zeEnLGrb0!sKwGP#Mhsgj{z-uK1dWrd{i0f1Gtx%La3NuX2(o-WOz z$E@?uEg=C2%%o}waIk%0*{CaNvIf5J%%ZUHN%xQ;fTNlKD*y-jVlunH5w#-43Ld)^pQR>)}|7C z@xp{jCnyi%Y9}eVUTxwscAD67FxdO=e<-AAO;XaZ=m9C}kAC$J6ygUhK`*|O0mpP; zq6TAGq)G`Sf3iBIs5UWa>`ud=-rK;!S|+vQzvZZr$E4YXlFkKprv< zZbeH?ePRgPIw&*u+)N9`$$%cmQko|!G6M#>Z`|VDh-MSa-r_z16$p?Eg9<-!R!ERU zX+e&GxXBXXXIli2wIReDx>uZuJ6`wHdMt!CTWY-(DS}ZJEoW&H386(*5g9J1BFC8) zi75c)3+}+tgwe^9Cs#i>Yl+4jcY@1E>PpxaYi2FfkiEp7yc~Y>2hy>B{nvk$K3uqZh+CYB7sRG|U|@@hD1~&h#g)tOHhD_OM_luC*$5QL@grxmR6>p9Czb-08q^mF zgbHJl0;PE&6klc%gGjh3BtUQj58QO(%z^#;#(`ym5P~U_7L61Wg4Fti-h!HnrLDQZ zmhj9vZU#Xwbz~aL7SbIZkb%xbNnOm4X$?Yb@HRb7Rkr>4AhiErYUbzXe!u>BVMs5Q zkl%6wG*h?38Y~3*mB>)Ql_O@NLrA|e`ybB`ac zTiLCOX&H3nQEY!f@GOi|tZ?bFS7OBGgNV>mF_7=5Y8JhHk<8Q12E$ED&7Oh>%iPg?f9ma|C< zHJLT`duwCB`~uv44mU>70yK>J=)XU6Ew_0?Frm*3!~R%QD}MY*fc2z}ESzfO?k;vu zpHCir{r@%ICs>egSWr*BcG!1LH=@S!3}t1;IB}cN;+|QNjb6&gb+rsrr)uLv(X}zH z;;O(t;`(B-l|YrIv=C5)2+>rIrW6uUwJB}e7KbPy zkDA-IIKdJ>h*M+%3D@@gu_HI_Z@hVvN@wgS@HX$>DpG-Kwy(|4!{QnBvV)={k+y;8 zhDSYO(@+*2_+KV0nMop%b|#fb|Jd;{qxcYzp0hQ8gg7yk)bVzv>e~t`L>8A~P4grz zIMu+OJv%)W>xBFq|E43giaE9zlhcm;B(2FN&Gv{~5^*!Q5#*t98%;KC+q_kniPEQE zdeLE|4#(H53#c*L%OW}NV!_0DVTak;l6yqq;C@XCYGQhsxk%bEkoE`7Tov_{m4qJV zN|O@1**C;J7<^~U>;WjS$WlXkgVXX@&bznnh`e=yZqxpZmNEjlzeH>*kW;&RDH??) z2>8lK!^)a7-Er_1$f8N`7aVkGNs_6NoK|@UJx*}e%C?^7QS;pXhdX-5wfJ0Yfp*E5 z5m9lTLm~{4RY~3U@85s)=uvsB%a^^Jl#MCc(>iGZLUP`#@lyM6>?%rzK5+o);%pGy zg~-b2v*z{N3X|%FcIIN#X%-r54%oKrgF&BGR-y)~O*4P;p-oXyE5vg%IU#gZ0y_pj zAf|yjl|~{naI^vyUfrCMYt2Rd?69;sV=g5!RI-pZ!FFY8nc^wdI(T1_bp08uGCpPS z6^VV>C9w1qIY^T;-ZCkM=w#GrYapVV(8mU%5*oI}5`YIJxA2co|GmqlG?JnR|&Kq@p#zl>#ZU1S1y$Jbx?zO_e>_w3tcqCpVa5&~W4 ztJfGOrVFXYqFGL8;93+Pf+Y_6o)tK{pf-8|3loEh0;5bjX3wag+8)p*h@@iF;fVzH zt}Of{mIPx($(ewe9~D_RAnO4|J`BEERS7_!%J zaT5Ukn^ieqCi$QFzHkMVn`^42D85`VXuhG&m#!x&0ZLw|epD*p`TxGwL|`OX^qeBd zc|$`_*|Cjk}-amdh4t6~*FAPU5$e$^&1p`03LS<67D7Re-~8-wdP z_f{bP>8Dq2Hv`hE=-mDMGfM}Qd1#)%+gU3a#j!1?kgmmufwxN5Ue<8()dCr6>X zl#h|cdvcLkCQ+~hnL6@hwmjI()#8?IpL~4TYuB!Qeq9aYylYMk`urL%Djdt-v;a2YGGs}HaD?#(1yFPy*NEw&0@zjlKXe*E!89`weI z8=rrE#hyy*Rjru*{K{vSE?uG-I9>#j=}By;7&%v0??Z$%gtZvMfxt;IIQ)cTV&YZm z+qX;IpFe-WDNrcsBx=clOBXNcdS)d$!C~+I{Tvd`SVh4CQc)f8rX`^hh1#{s*a6vL z*Z6{|O49swdMD}KxqI)m8+Xs1y{Sbgckkk})AQ>>a{ppmLiD`S_}M#7Q?#8bKejiNU5YfN3&;#EaPH zE*`-`Mxg@PTtSt-JTZ(z9`BOI-^GAx3Git zX*RkBR^&~7V4sBXw)gH2*sC9Z@(HxOp@g3SZLA$DarUEYmQ(`@shvfB|PezWA|Z zFd4zz4?p%Clb5j+Cer15ubmhTytNcd$pOg|;j}q3_09f?BeFtIi zLJEh|df$}p9GH#{sKi=oeypL2(_n4H6+x#i8~mUfiYA<>?eeP+eBhMZ7!dYz*Bk-t=M@!=- zC>?KMaoD1~cW&QYd3YbLLx_&9rB2gPV5=KMJqsMgjH5X9G<4pYhl`M$7%8w}r60q& znDS@&(p?9mwDe*alhSFK@KcQhDQy-TJwUp;Ph5)#$qb9JFvVwue2BI!3_zV(6py6l ztZm#&`Z%zWUad+`l!!J^&XtIY4}N$|Fg@VE8Q4L4Z~*fkVwgk-7r}RZih?D`m6V-SU{YE0`84 zDXbxeahA)KoI=C(K`LPoadBQXsE>43^)4vzJ&eWjb3yF?{gVmHQ&zr;uBB{j z=HmUDZ+$ERp$Fa`3Y17`7~=}zxSS~#9GmR_=kV^?QecV9L!kr>v;0>pZ#1$@s01k@$56tHXfcp4nt91WKnW$ z35_!)5Q_?8LceG+P&qd}TRMs7WYP@ufBKLAC%!tx`!E0EFTCyI|M-vp(X#i`r%(O% zx4*e`>GF5K`)%6wlOO-|nP;A|U5G_1*x0)k2shoprkF5oF91Rqrlgz+k`{T6-~9SFKl#Z|jvYJp+H2qZ?QecdtogBj z`ltWidlkJz#k)jOfX9!1@Pi*(hT?$x!-t;(T%K@j5t-4ZpUGiILp>7lbr{(Q3i<=< zNz9Qe%Ga!}zWLiX&z}8=5`E{}-*UFNot6LBKmYUp@<0Di&SL)Azx@2AmtH*b+;czv z@lWsFyL0r|QKaRYk%TPPHExpF?d!epWZONvciz9U=gP;Qe)6|}^H&J( zOyYdwiW#CEgxJB1X7yrtLe3RsUAYtn(H*xD8KzvMDKJz8EmDjHQ~Cb-2_wC;Ox_4s!=YRgsUPp8K^l2H>fBD(ZDc6BR2hN^5?-($H ze$m+e1AE6cvOC4fsGa&-w9k*PYxklcjMM=L3UHwUBvzFC)?05ehrjyuufO}ox8Hm3 zgWY>}Kl|K~?|tvTI;6~dsF(sGYsSeE3#UMeRKQPaI9##+9V%(gt5{U3{?1$P{OCtNlsv#eanFx`^rJH`pE+<~ zU&L-1drQPLt4Zu2l4wYU5IGTQpq7{-QGJ$Uh&v#Y`vXZ9IHLF0pnS9K3o9${zVpt> z6DJVuv(K*t>W1}j567}rCWW(z{;^J)){B_NQx(fWrX~qO{{Z-S#cs7R^2spSqAlt! zWl(9+AwPXr$G1$VICMSCTp-ncOv1b^IdgwX2(+CZYgi3X6Rh;)k zXC7MEJod9jBbljB`L5%K>0fAjBf*@Ni!wJUo;1O~6-u*zDFbQQSp2TgTnFc-9=^*sj_+PSvLyXXLJDQg8BS}F0qEzb=<_eu@GcpxWL&vplqpZRoi%-#H z^DEcX9=2sA^{6Tu2+_sZg=@TTWJM_pgQB&`1~P|4`~r&8J#j7}AP~t|u~q^E2w!vJ zWjEms6->vbHA{x3%7_j4PT|zalP8~k_9?GwW+J^G@bsC}lP6=f9p1m5b>Qre9y{VY z#!v6rAnzJP(J}NSe(_{n?+P=)0gzzEAX$GbnUr&?Mv$9Vx`&Up?mzSFv+uwAuA4V+ z-Qp>4-Mk^5d+oJvas9`SpO9q|TD|nrOV+6%RR*BBs@TX{jjlx&tU-AZ_#zy7A$&X> znxObgr%wUfz-h;>Jx&j@bBzhG#F%(20~9C4w>frY<;02SKlUSOZ;uigmQS8}>*5Ft#n7V=(AC0o>%k{h>+FmN zUibFOD`x~))-DKkzyDvq|Lt$T!ADBrcpIQ8vuB@u#`|qvJcSxtP}qtSDxHMP;vT{U zSRc$Bs1lnolnOwfEgx4(kUSwcc`ynO0I`l!FmB(viT*YZYKI3peflM@bvyaONnsq0 zyzs(tlQKx8wJeYxTZ?ARW})Qny*szgOOcJUn%x^~1sK)?6y#*@zP*md_~`6eqGQp* z=`*KJo;Z2#-1!9*DJ*e(ua$Xl|KP#>Hh6yjd*A=-zxfL(r51E;c$^6vI}UK&jaC=h z_FV@EIif(g0DC}$zZcI$ob=6H+T=5ANX^Wz6kOpLnLwsd?zFG(qpU3G6^R^@5^deGZPduo>P)icdcNREP=dZ@hBa`$^7U zxcK_FzGX>@7w5hD+BdJY&VBRq$Br&JW2(f)4a|-EAxPeIyU5`I@63-F#Tx~rMdUGvVrG5Lur{PL=#pLtPg>O7txfO7^) zt#hv1AKrR<=g!@Tqw%j_FvY1uPd%sBbVO|P;?PQd66DMlthh0S^UJI2Zrr>fmHkC) zi+ymwGGU&*W9B7uJtR$;fpqyl;UvEh&IklNTH2p`OBRk$CEIgLRRNP7*}O)KfKQ$q zWK7f4YehD)Q}D>wuvv;e5|i@+O$1HpT$D#V!Tu9i6q(7sy}Nhn)QrKmzWL1`{rE?g zIf2z8vmM*F89czTINiT_Q8n~QY}nw%hILj5AcdhqFXIc^qS-+-@Hcis0KR4063y7GJJD() zi9y*?`97MJ6>`fplxBesKYK)|6m02A6wAwbjE0nmtBNCqgrOae9H7|3n~yL3o{1nK z3y^3k-*oKxc1z~c<6$o(69QsU^p)#{GfHP6`1fT@c01$mT-2E<8w zuAxX@)~;?YPnKsxCjAR$BI#P~%Z^EPE)$o%BOP@lI&gUZ%TWV=aFwm7Lr7D zjiS5)FC)IKrQ-OZeSdtC)WjFxRtmE`B??+|?~zH9i6Phk5K3ksCPTBtRj+^hTSqw%Gm-MPBkl#$XeIj0s~Zu z+Vx9NO6T~_=zHWnfsH6S&=Povvk2Dk%>|6_&#F_t&d`4Z2RPFH=8nC48SM0US$ z+-TBFgf}yqi#&Yz2w@|r=%C66?|-!Z#SKqC^K|0a;bHo%9b|`)TceF^W9tGD+{^w1 zxWbc(h%2HbE+d)a?tcB7H}~$gL#@d;ob=Shfz4eLHE!~jBvZo&4v@pv<7de{EaR2Vpjr#Ckne%{74P9nf7=2A39?j45el<03&ybhf%(1|?p>4j zfAo`|{KMb>y-CKi=OwFlc@q)Tm|cxKCPQ5EhqvBB!Tm3vK{qor+JplBk`%m)T&y!W z^9ME??Wv4AdFw_L9HFuCR&RpSl&lG?L|WchFm+q$VpKAc*0HXI7lNjg6jmFJftY*H zl>J8{|H88W^l0c57(I6Nzj58@;QII8{}Ah6e&vi|%=f?d!)w<*mv7cSnqxN${b-$y zc$q18B1Oa&nzzh$C~q@?VN8tD<1(c2Ad-c6)6F=fN93N~`vuc;=g%YBg$oyq51%^p z6k=)i(xr=^e|~KUUe2s$GDi$fvH0LcR@WUoc*q^3a1jBmY5e?!bF$aUV?oQXc$%@L zb(;B)B#Ta|MZeCTKlk?A@9sNr0GFqwKlxCFKJMPLchlA#V=Gp+-?p)U91c22+e!i( zK}0Ya4DEBek$dBMh7O^Z=7wwQ0Va+#3(p>=FL0p3@(4wTxsSQ%V}23R*^GE=@gjrK zj6oH2HvgNRyY%rT69__QssN6$rwgguB%v_o$eOW567x7-J(X#oY;oR~Fw|m1&BYjwAEt9lW(7a{K?!fF+t>wb}<##$0r0K?z%$Q+Z77 zLvzMjwv?&GIA`Y)21*gn^JT+9Lkv46C@eY4Am+oHG1R8OG)#-9SVDUVWyUa^6I3W+ zu4NNQkF0^IafNXoMvXn@T7)dJL%o5V#7B?xm8~3OyH-;Dh`p|C?b-k@)O25WQ)mLL zrT7=0BWKy6#RM{lC3OZxWhN%n!V}#dOI0|P;ecms?P5NPu5`tp^m~Yv1>!;5mC9hX zRcg&QH7kTx+fct2RUAk`NtS7BekgPxx ze|X?O_n42nSMNAuoejEa;nj^>JnLsyoWRsVpTuY7vB{JZFPz9#cNMcwzav#+^p(i~ zLKG)`OziKb+SrOFWtfW4U8`Cf;0@QJutb71+h?DBZWJQvs)U4@t|>!4#Y>M8^r!^j zaW80zqs9<9B*5qe8Fb-8DR?f7NTBGJaItf_6ZjnH{}!#jd)o(nwal2nIPRtZ+IgHE=W{=~BJPh2`{obZ03h zwo7v+(AO5o@g%?r(RoHrV2Cf}=9u6$~Tk39K$rIE@?eVxoo8nfb=h&d7+!!-BQ=AeJY?o1Q;X&bP=i8PZk_FWz&t!#d@38V^ z$9BiQqe5w2Jk2wfkTU>iUIRwSIYWng#Fs{+nbps$#YP!ZmLpy~X57cBHBNi#>8B1k zqI2U$Zw54N{o|kf=$U7wJiQq=$w44)1CCZOX3`pXX6)u^*59KiZXjbcgv~fqpaB5$ znAyp`03g94L=1gX8&|#kecigfd-o5-1t^XVKXODsld=<(dQ0N6mef`4X^1{^810S} z>rY=viKLEcJo;RJg?jWT*#Ryp13A;3DfPGyc2bBui$woSfTar>M5`p#r188W!s0@y zX!jvGgh=bfSo$jPGI?aR4b5!~o^1fw5G=C;Jcna(S{WL=mPN?1SvJ=y9AZ|Ol4q4x z&;b$+`9>ogB89(MgC~$fVE?}TPd)V%hv1PKFiO(&y7c>Gtim854Z6{SF&Ify5*r6o zy0j~rWZ#Gi=y->>-+4<8Mru|7$C9*TW!?JS(%^gd%rH1Mu%2c;eZEy{~C;s^%S>6D6GGg`KQ!InTP7NlAM4Zg_ul`HN)vh)^jxC5Wf z67=c?AzfCHbcNUa0ji6Ri%Kl8diT8#S$cgYv{Q)pJbvPsTCl){+%3cmGX0p;tiG0^ zmjYrWw_7C=*6ZaLbQ+qm+JzeHOzD7H51I!Lf)i#`SHR!l)jBVVY7m%vm;TJ$Rh{;( z-~6adac>zxSln`YV01`?(j9#jN7T>;mR3cL5UPTEx?=Hz`LWIMMPmSG)3D4BT5M3J zB?C=Qx%x@0EfEWby63Q0|JkV^Czt!R?I%71doYP+NH^$09~8a@fD&t3Rbe*={Uw7H zT66vJ3yFvbV#qwt(!&%{F}qm=T4Jty>6zw+CHR^SL>>u#-O$?l);l7(o6-S{*`xXj za@g9MHx&mJ9653nu^+5H5Wkqd0SZHmeJ6YJAC9YQNDHSCMCA&oIxo~(BIM+Nj8(fA5cN)bcHU!GJn3Z zL#7qF^7-ewJ9_k3ZMB5c8o}1PT4KL{@BVE&9EWoM)Qc}TYlfHCa*jC ziDxo4Jrj<(MoR$>a*fCF5KAalTg?05q3K|m087g|9%5CR`lxV+iDX_E9kaqHE*AqJ ztp>(e>$iRY>AlL%rbBCTi%e+^Q4p&jH`-XjW5M-DADz>eI_4h{-}-j~W_Wt`>^XGs z9v<_v5clrhPB!v(SSKbh99ab+NK%a@u<#S`6c`R5adK_l>AZbd#rl3=uP_O+sEfQ~ zxcX{qg;q2&w|w+C_W*6n6Z-4ObxmaRQQk~JHaxNtbTe7-wTdBK6!oe^LC7LoL#}ui z1!-y(#*USsHNQBtMo*RR+`4VAz>%XzPQ7%BcZ36KIMarL+zOu`F z4d1ziW2PgC9uhz^aCsd@q&1yIggA<*o}hcXBpW15oJ1%)M)H!X7Bcq^^;D|Vi<95Q zD;m@mxQm#H+$#J zq5CQBLgx+IOe4}dX- z>{%WbA^=fJNR>em-q>;4_#dv$kV~Qyg#>1-ae?qKIuMf*J=R}Y^Tq(P^=jGI+3l$h zr~0LIR*KPO!Y-220dqRQ77lAJ{Ielw=3x}0V?_-9&l`!&{3&1Epm&~PvkIvIqg1Mk zW5uTa;dr_?OSj=uUX$3^z_ex4ygjODC$+esYbxfXsu$S4Efa zm0g=6oNs+B_m(eHAx{(#U%lY`)ti$W3(vrbvMA&`7f-~eNrV*&87s@CI+q~{9=m}c zB(a!G4p0?>f(-x9<#%ecj-nNwF;K6+aPCsZeX===gcu^`MB^is{pc^=;AmEdWsWaLU zgcgxVLGRtSkDn3T0Mzn!vvuT{d<(n?IzwRn&BY5BPQGx`P=h(p2>m8G+Ut{)hffkT z3=05KLW;HHov`?>{Px{DysJUPV32zL!g-^dT|2jbeEqsSzvV%ey&pMp#MU;pl2A$2 z$foc;U??A z8!oddt*G`xCg7d7-#z)_i;kh9TF*Y~#eeI)IB4&ly+*=3IyTt^@cFfCY}WB(M|A~n zEy9_ZtM69LCpAe^=Yh#H@%RXVC^$A5wL&%0g;ngcE_u|^75H|d-r{$ zrtJiPP$6=t`V!{3lVF*#;^1BS(DEHZL6#=9&$yQJM>*xSsqskH*dXhped#Eqs*B+@ z0C1Lo8F3_Q$=8-~4~T*|D9D2b;d2CsVuyK3TswSiX~RdUchEt4Mc7t0uJ` zi|jS^kN^0Ok_C3%9@w{6%0wdhr59fmzE2B{Ympkk`hrX$5djb`r#A$S%!ba+{*}9G ztl|}r#=*dG9Qi$~2-IK}o|zSacAH+jc+ty!O=`dO`#(G@p>^PZxuHXco}vQ8SWl68 zC<|;1!YpJWr1-}Hcv57X3}Z}DV@%OmjxE-YB<1HbEcvK7kFYPFdBsake(=NZqcdKz z4Xm2F@evB_p@k!wZRMFq?vV|5*<}W#V8tB;7La0bkvK`)7I_ya%klJ@Hqydawl0Q< zx9%t*Fg5IBFq>w+^igLTgzC3WxBH|DlO*iv&2mTY?&cNWtYaVH-gki zBx1bLE#~F73Nhr!M~qRaOeAoO(QGyYRcKC_G;LKg;T`&DYCL8v%ZU>w?3FVos3WOU!LdQ9<`%I> zLo@j7N$?;@8^-gMF?Ylx1D0hgP~hIjA?ix}vFL11(SR93e~rWy!}tc_&2L&a7EEJc zmHf&z9VL-fD?+L>t&O0rvjc)&upojY#*+(WUfIspgcmJY9YKA-vc{nJy9jGzp;+K} z2W`jxVUWV&813sa0w&iZsMkCoSfeODY9Ds4`fju2Ln(INR$hR&r5Gy4SZ`K+aM%7pup=@X z1U5!M4DCe(8Z_W`0Ew|^$EU`roJv6a@z2ub0$lPD!Ix44uU}!VtL{e4s!pYmim#l5 zRaqBaOv&1MQhDI*=aY-osI~rTkE|u8h=Ih2PH&e+jBk3YpaS!WKG485fZ_#{9YdBk zkQfRW4n6MWYsjh`aVD{w8wltj<@QMJq#0`?&DT5RkyKZ5u^n^4e7w_!Cu6@@-I#fAW*- zpFjS*2JBJP%HhN96Kt!zX4X4k<_P?pPWSp7-^L668`h|!TS6K`Cr&3TgP9wcR{l%hOw*&Uv+9s~D~j z)og=9z>6&M>a=CTybd(7OHC%`0gW3V7(u?cQvS}+Pn0%hMI2BXR&!!MDCk#vuo;&# z`uymTDMERBk1&b3JoEC)Bj^v0pE!<&T$*&-ACwNz7UwHB4i>oK< zGEFLNlR<2Nm=G6AHlB$uM4^a4f2;wHuq6Fl_v$Mzq0kthl{05fO_0yxtaDcQGF~^` zB7)qta|a1j^5n6jmJv0j(jJ*)2=yaNA)Or3BGB9bX%yJiJy2o-ma&N}Mekb8-C_kQ zFe$_voB&cH$R*Di3#M&`n8w_e=KG#WZH(u*wUSwEqM-nMin%%4mQ&Q_Eia5?%-Dv8 z!!w9n0B-7KX}OeFPb0ScI4R6qgVhC;qu@9yT4Hi z+fec4mtV?4EL~{VUo^-Ai3R_dIx@v zg*O~(Bw?;GM3G-CYj9bRKEt*?9lxD|4G)Pm}fRDucy>3)mdaE8B3C^!ZG5x+W( zscUVaJjN}7>4%acW>MP6hGaEQZ zmW+pId8n_l(uE8;4n{1K6lW*rl++CVx`0dhj~V9#f8Z5yvO&IZf;fzU(JlU9mh|Mt zJ-c>IF0c{o8Xdp#-|RmC465iF%Pj;f^?>;0IKZ2+0+-y7Q4Hz7eeb^ff|4xx7tN}Q z22}wADpJjaBo<^t5&kl6T)1v91QrsWMxGM;83LPMefIx5U+bIlF%Ajrjx?SL zvRQ=CSM3i#+A?G*X(v8*a6p0Du&Q7;5vZ4VQB>q@o-6|T(#iYxotXsiZ96StG2`c1 znliBhMN-Kh^=e~6&#?>83lN>=n@d>DCOZ;1)&t8hFJ&~;uPOqup#hupB#rYQCj%3$ z>qgG(L2ugX*ir&XHWC=EOOyq8!-L{0a$7S|d1_|*V{t|CrQ6!}1^vWGemG1QN{07X zpf+CMt%CODmas_L2|JBarCiD*5dz(CQU zrTRJfP1X^5)GRk_C^4KH!{iKNJlkvz99xIK9779HF^-xOFn{W4h@?|>@2>!$Jx9o* z9fct%&C0sZxU<#$syC{csUm>on;$*0+b&(ORpNmqHEI#32HKs~fMIS8JYy^~zR@~6 zn}1O(_M+RYnC_8T>P@wP!U&jusW8{Jf_K_7*mDayaL~_^RM0J!C{Kh5vEt)zk_|lK zK&Qc&3&D}L5tETsed;2LlBez!?VvFj`Q&}s|AZi?5j`f zMF4gaOVQtHwYXje^Ja)-`VJjBw9L36S~Y=EGG3)frp;l6MiVzT%}x$Tiy}nAKfR&} z768&BpS}L<*`tbnLoW!4aKeVRxT($LcHO$qKko&3dR$rykM-*h9yIPMHU(GrBY4e# z&^Qhr$*#q! zCc&}kN&kVNN037VPxU3RrkHHVnNz23-nh<^+SY2+q#_BFYqT&!VKA$zdc9&jK%=l5 z8(Lb*&71F@vm3OJ7Y7aZdRZv%#}HI? z1u{=LpDb@COUE3<#^zz)fA^hXI#n7KG!%UF=%trVZ`-kR+;JJ!g)eGHD!q_lrhCJN z^JmZ6G+_M6uAn#`n8Kv{6&f8ORB(@CDSPDYXITSwDd%plqNk{B$G1!S++2x+<{unX zV132dhTa;dO8+oU^E@Xme#g@ zJSXIm4e^YPhB<~I9Szia)J$l#GsbtY2%O|?5x9(_nA=TgH5i2|S>s8ceB;HtdPuX!D?jGCHm89yBx? zEZ9K`u#AindQX)DjlAm;8w?G2&_dM&e|+I=gsMSDu?URRY+xn0?xfRJATDlzfxCP6 z7OgSU>r(4(vQ9)hJ`qqCy$Bo>=hOCmPi=UVo@7@P2dPXLcBz@ zXbunUHH9pCsk=g zk;b5j59s>L_xd79)(OCgT#o>s>M4V9Gqba(p2>)1U%DnSocJXUiJ*x{A=;DL)AQafps3Dzc1^1vlH@efMpz6tv~pAy@|w*vW5Dx9-~2YmQWnuYhMNUHColcc&3% zXE|D#pv!tiyC~qv%B4>(Ia*={50&&NTxQrd-!%}(#9KY>oI7{kfvY=q-d5MMuU#2! z8E#Xox-$E|{n}MRqQ$V#6jG0=M1pYaCqRPASsnvGO_EZDt);W?&%DssPjPChv?yo@ zhDb05Uji81gI@F0P@q5COlHqXH=-OXEI+w;>FVd70S_lpppOySWOspwvTxcXpIuLL z2g>!LXgQ=3ty z(buj#;J*FIIr`kXeJkblBlj1z7nC?v97KNt!opi9j5#PT3R!5im?^7QEpqsxBS5(H zICM?6Lx?UUNa3Y*G-v?TYp+b47#3}j3e^ItUjFp*jq9DP0YSf&Bx=Bj+|wed4sUO3 zm?ac$Dyi{tC3O&Q*uTb*%rU(Uu{7Baw9m(m4MuTv95o-<>>^^%?e8&cXlEAtnNbHr zjdMgG7>|bB@p<}60H8W!U0_WxX~$LmaMIyDjWh@nRF&0Gw;~aV5fEs5Q|l2ndxKTR zDEcRbe-dCJLDdN27^xbBdDb8Q2~*82_>7KC*^2ZR4v7_P{hj%Wo+N0FJV6^iphkt| z_g5`xEE)j_q0}tqARoxx2N-_@jW}TOY$xc4`+%#BE)lDy^g~Kajq3&-b3FPt1g~3i z(C#XpdkhakXVHi{l9Afr$X3R+5k7!p!4Rd855b57CSrVKAj)N2w4Vih#9Qm#V=y~i z*kGG4vnr(9Pj7!^ZE=~Xyq6Ct@_R8o zlh;I)APh6a0mbBw$j=l~r5z8tPnXQA#RslCeviH=-%wKFWK}kPl&xMnk%XW%5>Uu= z+r}*-?4pgtodj03DRTwTP0{EZ#JXs922>NjjzR@rJLrrRfS|Kt{95jfCj`u(T@8Fz znfDrKVwHCAC}sqcWYGD&i&VoNtC|6f0yDxU_5@>~ZYTw_CG8(pR?$+zwJ5XbAMH{Xw_07m5VR>F zGKV(voV4f?$s$k$(dPs%Pz|+kh^m0-PBQAgt~9F&w;*1{rk~`E%F!xQ^JC82AcpQi zKmlhI7ALcT5bhi5v15dw8H{@uKW-%-5z;C`@t6A#?9-;&3)tvjH9|cPLxsAGm~qi9 zFMHPF5Rd??NE(D|@g)Js7~?18LT6y3W$v?$p}KDUt=o6s{N3AjpgHegnhCZ)UPFXq zj`Dl)MN^qlCG9YsT`~&=z@spJmFsLI<^4Y_MC)AwYpzAX!DErQUB2yDM44H$l^7&K ztvb?5el!AsG#L|+eemu(1sSk-`0(Jq-FshuRghN?$8oyL9b zpbFn}o(hr9n3-_l47pC;BIp8UEC&B_Q+O&k_}%Y+4_}8tSi;LyAsPYDrac^5iu>%n z;Yvy+SxY#Bn|BZdUb=9p6BgEpLwMF(_Gh7pWSDj!4G4oq>Ogv7>c27MA&`3l*v1V% z{n@Vw36O~R5b{qx&;;rJ{lEV^@3OS2)rmYp96gd6Q$)-(+~G#Pp^Pa@rlSo1#>aN3 z#QeD;yq*ZU;=nWmYlo5*L~>g^l1u3x@uqe}dci|?W;9B{m=1$R_mLUj z>9?AK+)?(+QC(^t`KP*%kX*Z> zsiHLwsY>*1`EQH~z(7=KCA-rCqsFJ=c0ko z&tl+A8}b))+(vw542nxIG908+P4+iU^s?uo1b(G7S$I+_xdHqyT~J3tws_hoI6_0x z7u44-sK#In*->{W79m#w6Hhfu)8t6ojROnMfQ-(kVH=vu7q0oP3<^az!vd%OXb>6mwlH$Md$dYGS(8*1%|Q+~uWBq@^D`q-vj`7K5O(66d}?ZG zx)wr(bcg^#SY$B^G3y8~!jo-|TckH2Stn@KWmKfq>_YZ8iCMWnq2B7fJ9jvYtr217 z!dhJfoJWoa8aYb5>?SUHoq046Xf)9nZR$H?NxI~a9@x5%=E&T+AyHMiIX(bBIWsyq zSzLF^H&7NO5G}yXg*8E2 zeK!z?@y2bLm~2fPX*lGkAw^T_HLc(F0_l8Nh@JFDyhfnBMWRSof{n9541Zc*#imDa z$T(KMmsIl88=WiWFtVX}6vNs2nqHcoT4ZcS?SckVB#9q+&}Q3+ya98l&GbBYw9_%X zwHY%B8{rzfjtW33PiQq^xrJTs-OP#pXT)wy}OVO zBhW9d`BI8GFhW7}St_*Sb8WwL+d7p<6px_4&Jh?soeNL`FQ{_!cuk>_@1r zn4Yrmo96J+)zu5<&$**Jy0d5hzCHW)TV3cep$2CDvMedDk=C$Unt-~NvbrOU16#E7 zsK!UlM;7QQqx;w{7V_hbG=zVwYZ?VSup#k?u`vctO<)&8GQGUFMnR5?xKPuOjTG53 zzz~3JP7cxVJC6oyx~IosCG*uV#&`=IW|#)?4Yprse`!tdCm)Pcw{AHc4h<3#s9`29 zQz;ZKEHMrodLncy9B3-2Vj}U&4It{$rgn3_0gx8Yz=P3XnG+|~8wp`pvCG83t1Bj` zoiH3C)21CR1KT$T#nhQjEnsrTZ+`^<#+UtbMd}T61(`g z@IdWK!XnR>OPgY2nIlEM(;e;j7nhSR!AHk;)zb2C{-X}dRZm}B2?kp>jx0tDeZP2G z2m=urH^J6%G5d#uL-c zurKR5M4Wz*fft+P#QFybW&suJm-+K+7%>x05vV}W=5CnLHFyb0sQt)9(OPG75p15>F>9>&Z|uNg6+|tSa&vBT)?~{1h0bhs_Zv9^Xhp zoST3q6)0RRvl3;DW6G4&ODV@1Oj2WIhAVRk2H1g;Ln-le;lzv$>Mk<;1A zti*3}5y_bHNzMcR(@US|!6{!I=vkyodm0mf&3%l-II!gd^W})wXasd!$Yn$>gaQ>k z4$Xynv~1m24U>1Re}YjsrbXqMu+`T@f($ExI0On7I&q_t+G=MmXVboS&-I0np{W3~ zAU$kG*yLS@FN~0Dqg0w#L3<}-;ljeu391WxMv!J0kW8a^Jn*NOCSXeQjB&-v%6iUA z021&DD?`DH=n80p5ngOxO9lazfEhHPxJ__d9z4DuwDGTG9uurJ!)2ZzYQ$w-x9cJb zOu7-F71l12_jG`;iojg+FeTFworVOXc3EO-V)U;;-Sv0O>sZYU>ta^*01hkarZO>} zh1hFV8oe+j?17dEoSt>0DvWD#P3m&_N0Uh*IAiks2tjja_aeo8@)hWxg%;>G(@ai|e=}=-+ zy?yJ>*|TTQojYqA1Q-q7L2TF24fXa8v=c<3ctH%3PPDaG=9SUKVmS*V9&rMyY$>b2{WgwA%k zJqA*EkREAm5p3sr;Av(J1!f&|(q8vn(`g1Aj)<>ru1^w?+$J-{VG8RX8DpR8QHMGI zZ~y!M>Sce@c-O99d;9Hoe9^ve@A#RMGGW@)_<{ZV-2_I_CoPko@bUxZZ;lzlE2fMT zjT}cqY{0%zlN2&0;$TelhL7gQ4dWXm%xYcjD_lS9IAljR^V&0*y!YV#=2cN(df{VG z&}1SBgVg2mO9FiV$pICU*?4N}d+Y49g-}8Z^WxWU5cT1#pZO+% zO;#zHb2G*20e^|aHJiZ5cGS6w5t`IITJfqcXCXS*du)0QX0q1LhIuR?(Gf7lV~)DC zT|8Y|ULD1pCg7lVrg zkat497*3nqF#WOgUcY0EI@yJg2q@}0VIYXfZ4{4a*vzwlwHp)ZyvT>U`mZ7L9YSjZ- z&EjD4tX{-Q7de@Zh(v%Pax~THq9(bM=74Wn@v3jq^=Q?K&$H*xdwY^|6Gq=d3XaY2 zPJp{~p(zc;jFiOKG@ZANi>0QJ%TMLi#alfxCWMU#8fi!0pL+U$MVfqd zM~Kbq3!N|qU^Be&E1u@AbR~KqREcRBwR&Gb5=s&(ol}|srbnz31k}HX5$mo5m6GAs*g5X6<)Mt!G(+VB9_0UY*M)ityHZ_egX9{)(DZ$B3+0V)HnMPP|vVgzi?zccg`2vEwuN(9PQ!l~PATOxa&& zDVv&UrbZ(~m|)^>O}pA|VwK2NWYUNSDBtZmVxto8^4;o`35!qABP&$KCkEA~^0b(E z*D&QNKvfcdwct5}CR5xqsN-CEv|M`8L!0|_M>r+yi?MqC*g_(gt$gs31^}MI+SGL# z(f)(@#kS0_7PO75R^48=!prIRf-|8zuc~S-Vp*sK!eN8)ZL#Pz{Vqo zj~qI9z`@J>w`1OZ_q*S1Ik?3cel`N1IC&Bz;3Om#wiAFBK$UN3)B=-y4+@mX*JF_` zJVy?#9Lf<7jtg|uw%AHE4;~LhfR)iFofw5RT=ebyYuk+-&00Qg@1^HA|&`Cr)FAL_G zl-Ubbkf9CfoR}BQo8R30jcoI|DIqE9AZoxw%r)_h{?zEs?K=PxN-gF*^kbPoRs)7N7lZNUN{NGx_-jLYV9nGZ^E(DreFsmi??tHtbghw3Hu|j z;w0l3MK_8^m%IXr1p*mGkN%BBlGwHr=VcLmU>lA@1^*LFQY?Hi&@>UCOkJ;keEarY z3wZB20QBBkkOY2Fh(tGv+5#q2LtsJ@v9o!;kPs9$q?1f3m>V4vMn!y&hzoPi-jNH9 z01_rlh~4CcM9J8ro40Rz0i{>V*$mHdrek0x)$(C!MS~dHmvz)=vW9ulI9_pMA}A0{ zcj-;W#-z|jt1Fyc^uoy#GDx?sUpMh|>(&iq;e9TD@|khMt<_u762QA~;R46##IG1W zlTcb4Y9>6%z4lpz`ds>2CNwuZ&Ol6b@`mYSVLJ-5|3p zeQkMv1x?+s!Mkg&-@NImLr2x3S(czd#i3JVk5whR8l}kQYm$>3C(5F_KiE-_Hl%M{ zSmGhX*gfkiyxL4O8Doe_j4Kz|4llG|P}n-9dQV36mms}G6&Y#_}S^ZqIuiYfT5uo(>v9DAGT@=_n?FCs?3%3Nklhqx@$4b zojV!~g=?C^7r08^c<UzEhr!NZ%pIyDu(6o{jT@D8k#2a%?5KMwZiXimd z_biWol|?Q>=D%i+R@eRZ-~QmN`DI+8=vOXU=1AKYPoL3|Xu~c)CKssK-DqJP7o&zaxBvfoYf&^5su1Il2$&qTVc+v%}wrIoc(-0&9UO&VKko%$sXiSzK`9#0xeM zJg})~JWG6|DoK~JnPOL*)F!!EU3cm7)wkb$-zks1nel5ETAN+9Md0O^U-5c6Z<5}( z??9)J#aBU?mh6_;lqy4XZLl&IeE)Cmm{#zpmCTZ5Mlo7U;pyuZ@jwbkM6XI4VqbU zKzfgwSXe}l3R&SIpa(u?xyGP4f)I>gPZGsSU9Q4jzg&{<5;X8qFj#K_QqZ;>iqarCy7T>Ily_tpCNo{POwdpWlDr z;Efx%xE53~x!};z>sM`KCXXxL-sV#4SHJk#U;n%Rmc5C9PFB-ZDq37cxJUxNbY!`; zeWT}ik~o$5f7BWPwHo{-Pg<+C2^z;+OR9~3?`9XVt%%jos;hP7%H`j^`J2D_+rQ%= z%*IQw-J2KzrWY=pM}Kja(_Wp+`ujKkjSYR})z{#u9iAqyS;SC|p@`OPFk4Bryg@Ip z)3?Hjd3hxi*mNsbtR3|tt)Kn$2Y>ySf9dUkcRso4-IRh28`(Yj^wTSBt~AVn&imQ^ z(MKP#fUke&+Y1WFvM?f406nVA!dZI}Kk~tA$7CdcW%xJ#;%C3`!VtL!*x5;;j4_jw zRm>EuSa-F$YM-{FHoyO0{>wl9hyMUx&1QuoY8je*`7i_;1>8*-wegeqQwL%IFO+45 zbcc5ohM1&8iLgzl;94~}BNJBO{_N*J|Lec{D+Dpty?x`h$~SM{wv^b*f`qxu9P8#3 z&`XX7{rK|huYHSsi}j@$zjU-kTAP%|;LRZs z9%(;9w%Q#b^P*9ko(t4UWRBG=^DSyr6X9#BJP<>qTH7@;oHOItyvEpfMF2N8OwEP| z?Oa)a2L`I@gKb9WpaUNfa4!E48FoN20z`ll$4@%xRQ`b2L(g~u2u33uk2>TqfU3?! zG^WzHUy!H3G+TpF5!T8?I8N_)O6ts^R)MwWG} zmZCd2kQcWg8eo(Zx130vEz%expsB@&=pzdeO?;W_@%5Y64;?%-Jx5dmmbuxq@v|$R zfsk|s?vQ(_5VC6!La=)%Z_Ar6I;rKkxfK4=XLoJ5v*T|FSRz*R8ILLiLKiFIqpTz- zX&Kljz*>k?AQD21NsTePD}qFhnWxV)&&(|4QyNXdxiS<81!WT{ zN3MAznj&6WJmBCmES`S~L#E$EOkh}z@vk#?F%gi-PvQo}MTt151DVtSEFViXfg%8v z7j=rbF-iK2v63nvxG-t=v8SH{MlVF$I7xbSpYXP>t?JkMblHr6t7nN;Wo= zRH4;c>#ezHx}+F%BP5`&)lQ5m5ljlG#Fp&k(6nqKMK6_P4m8$Onk+CB`HW%rDO`hZ zxFI=o0tnpGU<-iLr_b8I_l**xP96h@FtgUx;$`RbC6IgeEW;o!U`ij0ZXpm0QV40$jM8HBcs=tBqnHRDqhs6uJizS6s1-?en*O6~)*Gd{-; zB!Hg|)?6RCsEg-4rvfI|H?Y=l6{ptxj?rQ04j%diLYVV35h9o5?@%vfjHqYeP>>&* z77b75rmLi3Y+N3-!uY{&Y=y8#Qjac;mFgyYr_{nfA!Moy%jJ3;bWOPyp9u)wF>yyh4b$0z` zA`cumq)*WQBvB{My$Bv&M2)lOfy5sJD|N?< z9Mha-76gi)MW6|jmR%GCSqLAO$Pg-c80IYryhRb?QxUa{$hn=w3N|7b<7=ljPL&3C z;J^Vss|U7eZm1*d8558QE+3W~JtaU=_xg^VoF2Z4Ueh|b0{SLgmTh*)Ms6SzLdw#f zGG7B=A&UfXF4@i}$rFyOHd|rS8vA-_2((;PQve}8hhTJsvxCEVrCodX-T%;op6a17 zA}tFP%odq4u=1za5|~HDI2jQ_Y9v7dmHA%(Sv4#3Fg78;!&uAH7znORRSY7EfEb)X zIZqe7I0F{b7*s}0#EvC0WMVu zkTT%{T{Qz|`^2><5P>fE&>Pk2BeFw=Sun|IL{fMI&eg9)sMB$JY-qSI+A<^1$gums zpCn>*%z|084sN^?fyZXPuQf<>#NLs^M@la;mnU3h(V#v$ZBxMrKgKFky1{Pt)l4_J zxs(L=D_>2#xD%#fG`7|U>hDh zzfn_FUn!j~j1w&;diWe9evca!S2R?>!DTQhT}TACoKLLJEE6oDvc`~9#i3OOFd-FE zs?rEBt9HvFcQrMGGE5*GJUYn78oD@SF%%&R!ed)*cq$Yr^Oqd$aePywK3y)PNv%+9 zBU$R%Ydi&QTiL+;Yg1q4KhQAnlig@Z0!hq9qZ#l(<&f{haPvym7JXg1Iq^q81nJFZo<-Ic4B zXHIRL7C)8fmf=BMb$ASGDihi%=waAzoHLrrV7RnJHY*Zq2pw1fC`d_PL~%#+7wTj@ zslH@URL9em@}9W-V#^^bbO2qrpqPxP5R4rsWUL%~${H#|Div3+r?g8j0)^e&ESj6K zE6U>LbrVb8Pm>8?rU2lO?nVt*Fq1oB`f`%2?TcopjpTy~Hjor}eis9T-0?o~qp4r+WQ@VJ%Oq;~%9RK9?_0ZJgYECw3@}^u zf`LFQ=)t0&A0FJXb<5%4~6yVX` zSQY9nk4zsmpyVL-oa$*EH=73|Zc0-mOjTsxtrmzFYGzDP_9DU6m@O6Qn$FU)mpNxi zz{2507-6S~Rm3^lrw#P=b#%<#v19w%b!(?IHqH{$sGBrumy?EvWCZgo<%(UTZ9K!k z#lpsUjW`GA#dz0DV&9la{sb8GIBF;3JCl9Uryhmd<6~v$Si0tfIHj2Q#rCAfyK=h`b#*eZawLE7i+gb5yo-3vg8wW^Q03Q3nO%QwA=z zMAf-XG$LItDz`XEk$Hs-9$ZBurs`D1^f7V8KaN$S5_P-8o7JEdy-ii*pg16)C0w~G z+Zk7ToY_wYWEqb6%C;bAsVeZOR=I!`@bcfj6?U-TI0ez6`Xq-M2 zaEkiHZMu~>mg;>UT!_lM(MWj^;Qb5gL;%b!F=r4TxnqIK9BA#h<0p;|4cfM%)&OHnF?%G83_8#$d5;xh$^sdg|AG!g%( z=;$?7gd~~9WnNj{1hBDFhSTU*IFqEh;1;5ZSS|uQCP!D1!=4o)EroQB7`7 zRZ)K5xY7~-$2oL_s5N9m*lCT0(>%-ENSJect>$P~2z5UetIeu#;~Z2gBO`8hhR5;d z$z4V}bR>v!C641Zj$@cSFrtW$;i4>Yi1>I6H&Fr+84T!QHhyy9Cq#ptuu;Ct!-zIb z9?cN@f)6oCRo-v+20|_`r3MfLOA=YE14Tg@z(LxZuOD!*Lho{^642CwWQZ$ejr7fT zU=HpOJ3tkfeDoSN3CMxF*guhpcn<~7V=pUBgIiS;&WjAl_``0ArDx0>^&|x{XEqQ& zfT|8#s~j2u1fdPCJ`EbLMP@2#?T_;nZ<7Ew!2QtXZuF-*7HzR6Qhw#F2edZ&T#dOscc; z#aqXZ9+}zN3eT4==Tt8T#F2?~F&13)$w#A!bTAl>s|uKKOq?f~rinpR922yRQ8p5n zkI@2U)sHU#30LOSQP8+#-G)u=bLQAZ1}d{nW$~j?!Ac)hJ5y-F#)(5i=PsNdhI1n- zU?VyV%Yv8tQur{cmut#bEtrRi5Go{KhU2$~8FY=zA3t`~xD!kpHr`6iX1BJglP?E9 zdgq1c-YHX}9h^CR`4V5;Xm@mKgRL5iFKKX)`I>wyFVcP!+Ag>-g-77^E@Oj>myRDj zR!*~Qv@bKoDjwP2f9Ck{T?VWIb@DS>XU#Mhj6ko>bxk0*+xg#GyT-k(u{VhqNY(toQr+ZB%#(>Fci5?c#nU~ zg!J~DZm25oU{7+DM157s3g8ZNQ*mJ2=~!u2I6rgV>VJQc&WtxO|KOrT4|!-jHynP zMd6rF&x_JXwcgf{0<4|yQldC0o&zZ`ll^WQ>2rTddVVVm913hWNLH=o1+PL)-Y7YiTs@@kn6qj1aJn{ah zoEY_V({ImX(xL(jQDS1lv+N$Z!p#bmX~WXIh}PwcNq=N+vlWQ(YJs7Gs~{&3(TLd< zAI8u(tLijVw0IUb0U^jQh24x^mu_*sxmjifP^*d*|A7n84fUBaPbbS&{z8tpWBflK z^}`6TQj#$|5wy(O=ZgI<7^=7-@e*EX@-=l_K3`{Nrx`k=45f--A}nr+W0x^%s^Gh# z7t@oqM*xirVi^baO(IkUZZvvv?%_@}``xrVW zFU>bm0%gDufgP8*mqHiG24NI}!A?dqE2G@_@rH#w3$PkH#-j1=`7T;7Jx@)rP&TZA zvI`?4IY*rX5qBZ!n6ZjwkdFe8RH+6lQUXIpIDql^M_jm-;?!7hq<;F#;ZAfog%{Lw z_JCx*2tOOa#Y>JxkKqqC6bQ}=kZO-Ij6g+ughTU15!VpIE`;X9Ea!>?RNmE`5ck#b zG5)>}lEeE!37i<(dfja%`Ngy+3HL_hqChGQqiVgZwSp@zRoyGC!)w4QFesR^`9oTB zH}<@g9|Qdd94tk0r6zC$qSEE`uY57~3yq0x83E_H|0+v%LAqRYS?HI-p^}FM8+fJ+ z{wU=zz0wd2I&C?@!?+=$PM#qsk6-uwKpjjb0pSnLXPfh!=#9UUFE z{u5f`&J*C?-rkXmmo3_9o|H(hYT;Y0@DtB1LKM!{5ILuim}dDj$tW5bncddP7qQTN z==FTB{neFQ7CBXF!7aONEBmo5wC9a-zSs;ksh&U8@7L5k`sh2>uU}&u>z0`_;i-P~(&_$wS*W&{SE*A_ z@OW0wnP%cil-1Oj8N$g&XrtAl#xkZaTV|)TWjiJOK;Wqi`F4&Kzc4&(G`L{lLi-F_ ztK5yTPMtb@_}H0&v&4&cwP3-57CQt@pWfcuYReo)7uy3ci_b4jnKmisCo<7YKaK+?r8Gu5yVb#^E&JG%H(Y6%4W5-UO?i+|ug}zZBeucnezoUerj-s{l zU9Q~wQ+=#T=4rkfyKa;md-$f0MQp29s`^rSuIf*yUft$%)hI=9qiGMT0=ZGZ2a=K3 z184QyU#MQVaK12Y%x&3~$M*_WC4$X#3c?fn)frY{?wpQ!^XA#$yUURyo|bGdWn6iS zTbd@5y5MhAvZq;Y=8#+hlDPX#Aahf^D}s$@6iiS#vv^jv)?udk0*494*~|oE*F&?bCIfj72rUel_KQ~f%s#=Yt&j? z#>@z-Msvl+C;voGy&f=1QSMYK;T;yw!?2M~2v{LXsVg7GE-hOF1nl^;@dFZ=*O|=y zl|6~lYC$2P%gconMa3m9=wCsLz9#~eD5ko~qFTLJvSgV+A|uj!x?6jm7mp|O5Mu>S ztZ%j#p*!`bcco!{-YnwQe1%1Kx_3NxaR{1b%6?WLvl5`lC2O7s^ zlUPoeCZVXn5WXoho8vG>q6eXV!3?uEm??p&1`tuQv=-&&{*TVAloDZb@hdA`7Uw9{oy`obo{d-$W(FNzXsiAf-rE8uhEO2n>#mO}&NM9>4L%?n!kG zwqW4vw6(YMuHdBWz`-5cx4pjo_0r}K{MvBqhE=OqbuL2JfkIdzFoi9lUFSkVG67C^MGuVIvul@|wzhVMTBO}6(}>W%z5BLp-L`ehmNz%K z<+Xg=uwm`3n>H?9vdCNpC+}2G55GrHVc(uNRTAKWo+1Gn|d&yq+iIe;G?zwgIX3IM0 zI5nePQHmiqj*Ji{N}q74Cy~sko5IR`NpwKz5Ut2|q(6@+M-U%7e7tMl-W9j39y&KD zMa=A65}!|(qvi;gz5IV0tVye)Q(Tu!G-!acJ0!U zwQJXf`NM0`fZQbTls`$~FA@H^24FGta&Io%etIXC9unVBxgp zR?ZUt=A~D*eEQRW^0lvhh^618tUtoFJA`dM&TICxOU&Z{h#@h&s+Wk0qLP@>`(QfJRp088^8Z3yzu=`)txf% z`NL0tUOjHU3wuc%FZzPkh!W0S#s&odfR(osr{PhkBUMGEgmnBoL(1@SWjJCga}^ItgPz-- z0KggbRe-(ao;ynoaR*D;ANu7bGcC+?Hj*$KWpY)BQ1BkJ`HGNH?-w{grAMHQrWGh> zZov=|S6AXH6(<$#7MvbBe4wFWiXgXvM04{DA#7+hQ(HQ$$S!%)G3CQwtK zJz>!aksJ*A!VNC*rx?f5e6jh;qqp9fBBBrytFN4*B0Ij-p&2b%&~Wkm1-8#l#f3mn zo)yZvkh{vas;r^-;)2n4Jp53?PCrIS!n(nPL2N@}*^P~S%2<}7*y07&Tpv1n?t=W5 zvcwxqh@#7BPH%HBAnH?Ly0~${cHx3RUp%1%j>5>iN`Q^ClKv7__ksW_)dD4CMB*4W zt~70-{#d9g-BM{mtLe8SV#HUA#Njpx^|T^v8ZY(SWBivoj^O zEP2Q{kMX1+h*}5wRgz1v4-udXeHr#qEewsOJ*1@iQ zZ@j*}bJ_BGu5OHXfQiJ8w~v18BZrS3`|=mR^2eY4Ojp;zcfaQ` zsnfN!Ev9R~_srAHvu4j(u_6eMDj{=G#9^Nd$@uh~9?#?9idyQ3mA@@5v%d4h6Su5e z+c9^JjD!;>PyW&W_@76P9Q}Kr`sl_DtF`!_|NEDI`lsJNZ(c{&fy3YU`ZquKg)bjG zc=!YFf1gOF4O*z77oK^xp{a4>rcGsHH#6&0mu0(v2>T_BeQ8d6`$KPii1$q6i(~co z^?mO1U);81=WqSkt0hr$6$+ z_pV>Rc3R_f;WReofd?O$)!GWxsx|9Z5NX4i(1PuXg3Zb`BV)+c)%2Y-&`60==y>La z*LUsy zy}EPR@>OkZvtD}TwJ(49@jv~u z&vzX-^v-uY%m)Rhlq~Bgr_Y$#*}0Gkq7HPyyH?EY+rMAPp}l1~{pCryZfXuUh44kU z-1xTo`{w8Ff9RF!;}5>^SNe3bs=rd{A5$nX8&UM#d-l$q-w{FPQKid)3PSM{H5!## znW$pHNRk-FEw`*Wkx*}U>uhd%iZzui=_tN%B!P?pt_Ha@wmW7o`781TCrr3#1CCLL zzEK@XzM*-{YK37qLP40@Fp@j?kJ{$!6x_uxN>m0!3B6c$9RpS_8i?8Q21ukO6r+=F z_&T0Ls>+Z%vkIxP`lq&v1eC_Lfx)x17L{IAn3Y4SE|GL?cobzA zr#5C@Zs_cm*7mk`t8^=jLD3{aKCMxLK@b-oIj$0gns8Jw9E5`R-1)NrB~Vs3RSQRs_sHYP6DOS;8pKx;3^-3Q3?YprCidN%f(HsE zYw8=4G^rQdp^)3Q1Et}rE>EcV4BN6E<;uutH$A|2(5@RL^}4-nj<~B#-vHo+Xuwb+ z4ptxxIt!U9qflejwtx`!3j_S9OL;$&EVdG0pV}mELh4vC@o7M>&4b}~={1ODIaBa8 zHes!sxJ3x5C}fp~_wm{|QygVP5LBREpG&N72)+Ph;uD9ItuE!pI>dEi?=D+&wuYc`ve6xQ2hKY6c%=EGq zt7b7F*|{gAbg>2n?a?K)3Ra7QNA!vo^RAVEixw|AKRnblJ%N*5yLMLI)la|g;RoMx z+k*LX&R@Lz;%j?0Z{95I@wT_W{e@?r|J8r-8(;bI*Tgd(c<_FC-?L`7PTDY8CYESH z4#3tU=+LxLh*>v0k@e;^X7`xwOF6@<0ZTpqoUqX2p8w+C{J@=?*Ug^Q+|V?Gb+;4B zJ@?)F=wpvP{hzYGlNK#qy7|^y7cQ9hAAkQ3ta1C$hd*L~ICW~{;%t{P z(`*)I(fw%mq<>|;QfD!m0HUfA+>URTT_KqK1q&CUnzY%*rsh*U!c_lt+LXHg@NfR< z;stZ0X%COso%~wE)TTLe=gpb7;I>U0I~UIXcfa$yBbP=$^$Wky*4{R9VR*^1#nP#o zo2Q2rIr;{=*lx;C0X_adJ% zAJ|C?yc0MC1xKr>5qOU>@Cg@;f=5Od^}(Susb>?QFMs`8ukU&M()D6vY z7#f^VdD*F64&-s{ceXo+!1?p$2}B4D%CI_c_~^g2M%b^E5Z?|LP1V z0AcUmQ$2kH44A=BB(3s+$RR9|T#rh_+ z22J`+*arp%@ODx6H*5OAn~cM+w$Ggx5QJ5-u+uXrC{S1MKiXnmeDL&!Fn1DjQiY0KP?g7tilL0V z?~j*c&Sg1p^rwxo>h0WS+A(`!q~A8bj84Nuak>gxf}l{(mu0I?M@bmrsw^B52?$G% z-REHt<*6qcQ?B%|{Pw6+UHB&xtmc3etO}IYY;#yXRaM-$Ee@b=6o;cM}a+Qju zOV>3vPJx8#5v7p(R0+nE!R3I#EzCu=WTw20%2=kxB2}o2$!4<&WR{|ciC0=6G6ugP zTtP$f)Uxy-Vu@YK7!E@rm18E&v?s5nVOnEPPtVsY@9JkBedKL-H#auIuuU%~N1m;% znIKVQPFt&3%y0ecfB)4leOY8;LFYo#e8(>xllfZ$V3Fm>zEMeuBZL&;>C9w%0#aXr zKkL%3UN$4fc$jnf(q})bcOQJ;+u!xby)&jap*rt7qZ{YvPOj_h?ChA+PDFq8U;Ne= zzwqUyOP4KOy10FAyE4Y{w%WzPLQ6p$$22jG+==>nF6&T(b2>U8ylel#%DejHCw}_= zw``u)&;Ymgu;zg~p|GJ&o*DBxQ{wY~{QW=tZ-4MdzxM0DI#(y#=LqfCaRi@*hm*Wt zP;2bsZ=zO3LL)v=orR(VDikO2S_c`7m=6SP`V{vzh*467S)X@)xT&QHkbI^mpL}xu z+!;UjvG>fH(ZplU3c)Lv=sENa6>G=0%$oI6?|b)=!-u~1&2KMSwD^&CJW_2WC_C6_ zUd|3)NmM{8M}^o^L_E`bKgCGzW-PL{y%7Mn_=w{ zqrn2h;ELsJ%kdMZH&3~%{m$m5 zsproRJ^$h>&wuwN7!ZUbU%fr2WG&X$#iGPmjIXVqI2Babyjq|{Sq%;i%a$WD{L9C` z@t3@-+cvHJ@CV;IS8j-x*x9 zzVVJOkxx=y0cn_1dTx(BPFh%lN+Gr>m9Sr!m}rE{5~+pgLi9*-#d)eU)I@F{i_oOI zaXmGsQgHiSuGCxj6|PbhY*rUskKJXyypS`?+<=)l@Ytmu20%zTJ8A~V*s$_m0ljM6 zgFqHB^ryTl1nWihonpfmMl!90oMq6AJGF#_8`rOW=fn4_eb?)+%dEpI_6Ri~Fz&=b zI0+=B>9pidTfoxkGCB(wsFkp}tzxD(a=PS!SsZr9rZxB8wSj@o8Y1mF+M4S_L6uxr z9KCMMs`tP95$(S4{C6c(S%y7x)=V1~XpyJ|0dP~8wHi`HuuY2R%-xB}!Mc2T+qR^0 zz2}|xZ(P4pcvb>8^dkhAFu&r&h8Wy^$L70lzg5W{+h5m^PFdH+MpApwqJI!b+AN-r zZj2HSXF;ItB)~wvqcoGU8qcdBz*~ktWxaHHC$B|KGxBSf=uVZCq!7`}xm(?!<}X6xg&TE8woO#58JQmV}Pt zUXYF$!mOw&>bPS1v`ai~p+2o~szmCeM~{Bvn@@cFBOjPKW4if~v|EUGVPqJ^Gwhx) zVc_f;8#>>*ZuQ(bt)Knu7mgk~-aN~!WY+gtP^w49tS$hVhS>N!6b)>B5EKBZm$gK6r3W+l&qCR=)DeR;lqATgTxN zw_DhelD1fwoPG8gwqRHvF;gi_@ z`n`|dw{U)&)N->$iXpUO#m-LT;|ejp=g~0U_g;F{ssqoh4dkyoCXCh+L&~tB7&#d- zW&6-kn$f~5QHRFH20F2;>%bFFJk4;u^KEy{oY54@YQRcP$D5QWquz~Nx-@rAn;@Bn zzVfxN_w^5ual=#a4`0Yy(J04=Zdl#m#J7d#qY!>l7rY%h+_P=lR(S$2ykp1f?y0H4 z!SimmZQF(r$4~URw;H1$`6tyfF)2Eun4)M5-NL)%v2ZhKp%7pxoRH(;(rzf51zWZe zrorlkqcB53RGc#9NsS*O49Q_0k3w6s*4BNp>7FMArEEcBl*1*?fkjw0IrdjB+pgv8 znR66KgsAvc+Dp4Gk8CD#al|-;+yS)cy?m99R9AX{H#vrk9f&JksBH4M1;0TOzUK^; z-utb;w1+2T!oZRGi0U4@QZt5U;dDd=I#={VA~r?IBn_ASCQQY!B`W|3(~KicGBwzR z!S*0O{3GW|5Ckbzk)PBAQNa-emDOqS_Vl7^B0f#&zb48u_RN1F1kxmY70X7$MEZcI z;}QxRr!@-dc<4n>^OZ4=8N?7l-Sp-NUFs_r5@^+9S zHa`Lw;E{krNv~xj^QX)(Fpict8%i=nO2j+hYK)D=Jpf6{sUvGM3ss`UM?B=bl<^XM zo(*he<3vrVIT|Rb9i={S#pD$Qdkw3&ca8$)3F^~k@K%^3m4jB>x)wu|T_+ojss2}@ zlqY5|%YGK2eo*R9xC3XXDF+zXvoBGxIK%OA1yd_GrRu0=qci=%LSg@mU;&%|xQJL_ zx6+BMnI#c?wJd=y$&1KWr49UQXI!nGI010@GNI@!yZS4*l@C;-U5*GLl<_wo^1O2?!C*{ zM!UFKRczw$;5pGz!trP93FRvz1x$&=bwVr7EOPuJHuxoJX_& ze|?=@62J4EXXIloUbb@Dv{@s=ml-f)T#8Jnf)hMCdVcTDZ3p)5IzM>EGN$<*!j?5W zIp@65#X68}DUzWd_K$$DLS=<~l$m&fZrYYCQc`OmZdx^}HR6zC`Irs08GK?(Gf*BM zlc;`vu1uDL-K z-YP9jPeW|QY=lvW$7WY_B=YY&aM-&)^}a{u&YgFDm?6&w7^o9n?XVp~h-=rnPaZ$8 zZ{N(8Y4_c;nQlLL=r99g63Fft*bEVolB}sGyo1XtxzFdH%!OYrT)6n|NALahU;C9` z`NzL3C-Rp*^|yZe|NiyX*)6MAFZrkcwRu7V}eUzjP}PTzVze4twRH&GRX z#17|kPGjsl-FN29ISXb7j2q6LyZO^^KF`B5XFVUxbxwep`7JD+C8!y^Wp;&VVRAEr zS-V3Uq~_Vkd@}-U|>ItZcXZUK&E~Gjl=Bwz< z8!6Tbs}yR2F=NqKrL2xjBdJFG&;AQRLEN6;;z#|Iue=-6UF?hBZRaWgl5j(64|HzRbV;RLmH_;&m6Wlth03 zcz|xc6EKKVEHqLK8itFrkmUbUGbm_7V)=X;RzSbJz4G*2!{rc9E$Bx-Ns%cui z452sn931FB*V=CDNJBcqxhvyM&D_}46Q7eelEt>_wLTJ47(<0P?j;0*EEUoHPGL@1 zQAS9DM`<+H+DHxR7o#{fbfLd*fLV|zTi<*&4$9eq?OR_t z)!j{x+J9tbbE6&|I&^5o$`yuKys<={&!1=Gv9eh$242VAM|DP)a83Y=iZ;>;=UhiV zs_eBb+wZ^kp4n~fT!zu%5!-Ll2qvTs>^&;7H#~H1$JSS@B{T%nkCvvXYU}DcU_Sw7 zjs!wYLHLzSW*gnr+6z8;-}V~l+P`1J5{Dtj4zXV32A?|J+tSo5-}PkAQ3+3mi~}P! z@|oOo?BIc}J#a+X0d&UnbkJr5YLG3F$+D3PrfbdGcFdhak>O|_4rJ;kw%0qojqqjx zOioJR^dwx5E|FXtNhc79-rO&`|r2Tq;jGS=C&ngPf#W@2ym zQ7a_Q_4f(5x6f`-_{h z$B@%grVLoGa)h?kWGGGp;2gdFiCjhzM~>|NjbHzV7+8hB@{fLb`SK+M?QeYS11#F2 z&IK6klfUpYD_7V?Zdyyr^k4cv{?YpN>t%}nL|WI8Tw&2@JGv>O7HTYCgs_zv_5drx zgOEaoR5_yFmXlNAU&$1Wl2J{iwQ)s;K*hFQt)D1{YM#hJST$R=?=t-dyTvY)rf>X; z3K2CN8fu+8ud%TyX*$Ls6Yaqfd+UnqBw`x4B3T)}Q#^5O!iqPqXzDoh85u?8n$yW7 zgKd`LnvKscvQ>F}97P|q7asOl0w3MTDJ*?Tjc$|(ZVcGM!%;Z!5M%+%pVUw*c&lJc zuVEn{!^Y9mn!N4R8^0^~swZPLlh#yR460^S&+hu@o!AcBXr%@Wo-W2{CnX~;uYUm` zWYRSc2~K7}evM@HJ`EkGB;6xN|YuI_}lMQoho3E&L$ivKGSoVC#$e3uUURe4t-MC?62p(MdF zuH@K=L}!0vAwjTYSitEODL725aeKjb$X0}&7-}{w0YAq2^(Ov2D06(+ z6;WO0A>upu3>7BhCO8RudIncP?GIfASik|YxP$(vFph|NqWs60ffBfW@j6FZYw)8M z%pR$5I++(Z1`biBql^LUW<26>4vNhYwAqD=u@tvTk^_@e1T7SIgejJ`_W|tqmlfPt z+U~n@PbLwc3Y+|l^$KCgOaMe7sO#Fh_vEo7 zCI=TSTYlwgv(?UCajfLPK%d1pjm<5G4)2wmafyn!;BY1I;F0E;Iti7}39WMNF?_P8$b{j=+1OEGLd0wU0cU-f{Om*2YlHC>8|~mcWM-GvYD7 zPSbO?Y;T*%ilCjc(QaWSeTolVT>f#k`|asI`P8?cbjoPQ{7ywoTnrEOOU2&5cXwNB zJD;z&zuN$Lbol&^eaDw{pS^tHT=&VNUL?*$v}tM_9h-I02pGx1%)KX%o9B`ly>{Jt z1Gu6Jqx~Z@m=ix_$E-TZcHjBSC+aN%ZEah$coCE3_|lrY*6-i9yS;Tz{nYxN)1%i0 z&H#7$EsOe3oqTPu=g`6Z*2|^cao5eSwajSRzyDD5tzF&&!1Bn^OU*GA*$uo~1Ik;o%OXd?3>8-hn zOp7|_y|HJn0>-_34%>%r-?q)#f;;cLy}7wwL!5AVX0})O`d1&Xn>eApW8Sz`%WOot zb=$60E0#DB7`NNptG}<;$-rc!x96CL2lwsS^6K}p+cH%yS#4U#dir#~4hg)7vIuVR zV-_u0vUbfa$cpWvnDi_PsM1m1`0HGnifX0&~_4o&lA~Qw3Q*3b~LRgx^F|=ip-CAH~ z%o4vMYqfG&EE6N(WZ!@@#JOH0?Foh`IFa^BGK2zj~8rwFtJ{u@`+{q^g{fovOA2d%e!jg7IS zg2e>KizJ%G@}VgWr))g1{{B9%->i<0#)R{h;*n(E&PYkzodT1lh)3#CZ{A7jNF;I8 zs*a+6^0WYb1H2o5;~W_3%b*nkZ=Blbhy{}d!Ak~>q%2`GhjeO)s~YP^kTV-a1C{=i z9x*!PJ!Aqs*{rdz@1?<^GqYyS##hXfNd_Hfyqa5D$`o6nH6UmwqYZA&_z6>|p7h{< zLzgs~{^@3|(IcCWC%IWww?hrCw2(})+{&8MGPgkJ=nYf=89fjRUe7-J7I@(qZx!75 z6Th^=5?n%ZP#4XUm{B+dPOn~-Xn*ujS03stEGdxq#rOpa7n=MNgwxri8I|w?U4$-a z6qb}uRqz)Ev&G=CW5;;jm2$p8j+=i&Fa5Ow$qtbgTA@xkLNs&Vr3+bn(A(YZkW=Gn z4mD6S!q1d6PoPqw!4;vP>%3yNhrL6=c#tA=B~oRveRwurQI=nbovSCqn}lg;&h#7S zVo+yIKpHD2r4`chg-)WE5Mp6EA4}(aQNAjXUCk*J+*wxs3e8aI93vg+6m8=2@CI08!Ax)XhE2262lPCavv`f8$#uR&P5j6OEfk`?>U)K zaP-2^$;0~x`+E7n@@~v{@9RDGjc)+shIFn3 zPyVqK ziGy7S)nKxqYFA-(dXBsXdEy5P7jM1SfJ_31=>tKt8yQ8m8!d4nt%TSl_4A*WYu`?ee0AhAw~*!;3CRPdVGyeQ591 zDGl~azbI&D_IbtHHLY#)UVZ8L!NId;{iq^J1Yep=*K8!q3~t@znKNgw7A|AfNO*D! z2-g+U(00%mIDMM^lbOo676PZS$se zpZWCfzWVAmt|~+#?!iMxxK;}m%zgd!ArnXXTXEN49{@E(ZeQPN|Wa2xYt(Pe?UcYj(okyt$Imk70i3 zDlm*=&(57W(>uA2k8AMBX@&$8QAyFIF&peq)KKASqJ%B0`@)39f2tWLTd2>Tv4vO~ z0(LZQ+6YjdumkCK(Un-Xs=Tre_H_ni1)hi)8)m7glxaF0&;~EZ1 z;~oVMv98f_9J6ZtQ#VOuH}WW4r|W>!uahTE$|BX_SiK0be=zANvq`coJyGm`Ll<6x z=|fh)4#@fjTI8-e7c}ZB8O)^~x*?GpUbSd6$ls;dZ=~Z-Qq&2Ts#R64o1gO=Z z#w|vMf%V5+-m||vFWD%9!imVIGH>f=#W@1)Ns7SLdKbAsbzU6;pdikS-3f1LC9c8v zk~3QA7LWwnNiG_f(`%C#jy>~Q98<4(-K)Mz1l>od@eB?%?=$90_x%HbsEzp+!GoG?BK`-0j91;C=`=@rF=}i0dX9G!u_#M38Jy4 zm>}!Mb)*b0iocn3AEqTI@twC3h;0;der?RanR7$5l%T;#j?&D={0EMn+O%=QhBYgP zgnG^o(rqY5#bvlwQzOqB@MOgah-`qBDBvM`SfO+(ULEvu%JT_})TW|k4w?cO%_&T9sQvJ$XZ=1Jp;qm>uM$X&zJbV2|#az82c}4Ej`t|Ez z8^o3YHePFPpF39{9iM~lZjcIEjCS@GjKp>d$6 z|H9>(k3RP3?8eE*Pac()UKUI^^K#Vi43HfibHh#%z$_cYf3j_gvCDpdcrQA@m2{S< zc}+*y@fu7Mgv@l{Kb}gRBX}fmT)o;)bSyx$g}NQpWnRIy<=5k^As9EYskv$Wo%c8m zc<0vFF4sGJa8$1;F4=*HkDde!$cKk8)dbtq%$V6?=Oz~VqO*x5n^Z+d%wt0q z5;1eeOno=y$>AeJQGG=(`>m%xUPT8nf0ZlJdF|TbMYI3&fB6%Y@7Z(U?789Ro_}Tj zy!NM`d2QLUc=j67h2tj%9(rKo*|USj8a@^<3jY5p&HR`$-|tQd-V}ZgJf34twL=Wj z68~tD1TTqKUdkw^w)Vuy?!Nwh^rioaVv@(N*BErMkzmI1#vA&VK2mg(COV@6Ab0?} zzpu}4z?RM=ERh4_vIkLF_67{m7Yt8YA_d^gqB-G`qPL5>!%|X3%iDmAW8tE^aFpY! zRLfJRNwgUECA=w|P|T0sgnxY#v#3l_AjKBE$#`IBD^Km*R>B@pcTB1NMYO}~pr$bh zbS5i6XplwlpMBvaI|Wivd6Xes)#FHtesW~FDfd5cUxQU2Q%u^6)-}L?=|%j2RO*UG zP0oobfbUI}(|1L=!BxeCK)@w_c|q>MWKL(-fv&1wUe_(OQAulStB(q4MQsW|r1A`i z&p?9wA59mCl9fxL)H|Cmmkft8E#viiDpmbpMQ3=?-4GIp4GZ!TmjD<#!W4C2=jox~my=r?s2W)@E z7KIFYoMwa5@m6UQC&V_AOPxLhHR+W1cv|*w^8`6#*VE7Vq3SG5uTTI`Rh$&f=gc*e zT$Va!D1ZWGv{bXF>&WovB?;}a#v7WNhc4HQUa5KPv3D(6)M@rmn22pgZAn*M2abT; zDs(=(SOCk4tW`#&#Wj{%kY}ATo|La(E)zqCvvMMJ*3=MA?MjlF-aLamZ{4|f$Qf{) z6!q6PEL*pJ-KNdr)`Iy~?2GhRwWVFQ@W_#jKIhJzs~1vOj85`g1Ykris*VL$(F=qv zIqczDabSEnv&=0XKH3d9smL0tYiwG*>9#fNZ@v4@JKp=j5AA%T3x17;IqZYOBU`rY zoZe*T_UXntDj;d<6#uZ?f{mwVDd?hcI8xaS3&tswWD!y^WgM4pqcEeSki|Wv3oXhN z&n&}49|?tc9#lvyxaZ!xx9`|_;o=p^@OHwTHe>dg^Ot#%55N6kEB2;0O%pV+IRa&b zcTV)g|5&ngDM-{gI(k7)0*T5ok&;Ui6OBS~gZmXILI4)t55!zyJj?(){)GwIw0Zrr z&%b)!)E-fu7}Brabo9(ec6nSmkU>dV$s=a9|vLp709syksA7SsjQWP57@8sp^!0qWOmv`6uq;39Q0 zg5atqyjb*1mZjtB@h3XK##iztFAPgpH{K`;IjR^jRhN<_DLmaORzh>dLZ%D^e4j2A zY5cC}QplvaG2q;2GJ8N2o5;$!3leB%LnyO|hE6e*>4>U9nLlGp8&q)wna)XBuu{6_ z$YE=B<;csb(Nhunx?sG*Pe#xd_f6G0!sLZgp+8#T^=jUJ_+@PYFd`j(s~$GpKqJcP znoor%^E$(2?>+)IG3#*hQ(892CFAnhgD4FFRHMGbZP5VN1jZ5*0a>^RG`0Lrrq6@8 zgO~?Mup4KkNNEx$jNWXD5#%JU2pD9GZz~bmOl5q^WYe2`pd$enYi@Ht0LBGOBWVeC za=vrw%_EvKY#I7%%Y*Q#f7l?q72;Fhg>(J(Eu$QT99~YDI6z^5ggo5@tqX5nBhKB(P7I*@Sk8!8VdP?IM zTvg2^OZV^J zfB0CBsl%40mYVUC_q@36J@0;O$>ODbJqM>W*gx%>ZG|k{+Ozl2!J{WOZoHL=J#y@* z-l<4G3Q^NWs3lRwt$bnrUmQoO>2ghucEWZVA1}d6yHZWptX;Ep%a((OPjH*2)K8ta zVA0|gD{!W{GR!Rt@7mQVS>thS^1(yLU*FTUc=6)4_Ey_n@@z1q$;z;=@d367!pq1q zO2{D7yuOSUPp{iItF3MJp1lW7bg$>E)Yng4v~|S#0)qOc$UjIk0EIW}>lg|r-HB2xU%u@5XP-TG;?(R7YnoeTja<66bI<+{fA~Z5 z=Pl^#KA@vhuhsFDEMu_>>Z#|q>h1gm3q;*GAMUY<=G2PYEWH#}b1#mhd43YNB#cg0 z3#%E5aS=JYrW6+~?(}j<-#%yR6dRu|?O3!}1VMsSL~+9SsSR}`rhf9ogJ*|ccx{)$ zix)4l(TkxWx*|f^T0NSTOwtk6SgjHfnH}^yC&_6NYu4Q2D4y@WwDo6y_M@{~=YIX0 zPtKb+@3zgi6R~)u$)B4rvAIbY@3rl_E$Y1Ip1VybpBd;6LThX7tpK+{WptcTVD@UF zA9V;AY6y2>#Y`AR$`vcrPp3Kd__65matQA9X>;4>=*X6ByVtB<+0owCIDO`_m8)m9 z&lSI=b|^cWIhdWsUIXWbcJDu|@RG%gbON%Ez{yL2aRJF1u|=vaesI`^8S;! zDs2m&q>MQc1D-S`4vn5gS(i8#o2|q6x0sONT+3CS_*6ke%*`;wjK<7GPU)axJZMMv zxBGb%)K6^iUfFqNIq^z3~UWsF`!n7r@qzZmU zW}C`FeX)?Do-=I}IW*LiEtJ^oujSppl#1_^Qb&Y;!*ebB$hbmLdZP6b+kGb$2pn_u=JB2;)14Rt0fglzgkr?bm_)tj;DVwHaQ~gzn63}aq zhH0F+Sd6baMTi*5VBYYLf$<*H)s&bNRUa5jsHspV90xKZaG`9ico>tU zQ99vLmUUBHsZ}M{$FuKuAMoS`6~NTagcPav!SZIA^FC8XQuO zaAQX6QV%a*o}6{wWs2}h7MrIdg~PoPc1%Q;y% zePQrI3m@@Fx}RlC0rYu~?Bjd&M^(jbzS@_B`qSukc=VT3tKMX4@_C952}gePhf zLG?HXcj7>*baIM?gW?nmILG(0fd@4C$+{;95Q3|>+Yai6i&RWj!4#Bc>rRR?n*x}g z0B2>81f1>PX#WL!h_7A0mV@=#zxevfRjZe*od2!Ae459zY2zkwmN1>v9LqT;HSgMc z=*eeZQTyF@-y<|-??-wnj(L2J5(3q>`Pj^D$7E^=*k}ZKum~soaP6M&IqzMWhPLYF zV14OpPcB`y{E>IuHK)B}a@~{>&IKX#er>JL>*Pr-dk-9a=7nrz_2{GTq=yFw&D7-p z1ZwKJ`5;{Ln)W2|da<$plk`)5g=ZPWYXA zzjo2;57SH$^|Vf`!k<8k<}tDq7qpoZA9P`7=jzp~|MYWTU9xQX<~#43IjfaEl~|2^ z&<^{FX*vem?A~{tec9^HJMOrB(V~UoWQa6*2Mx_sielJ<>{6!WfC#(jyU;h6aOz#_-Ga@rxENy#2OY zUwU~9hwl@g__@`$tQAi+zLFRb79uURR&Y*p@cA<@Zr!?l_sW$k7c7{6=4`(X#xb2f z4TWjpB6-FWF|qj7Z~JEOI7O|%A?~<**^+tlI(Tx6Iy*n|kq<9kvdoc;_RGPy83#a_ zE%=(JpYA{V%9fo@oPPVm51ZlYK5;Dif8032f>&}G7RMf6*M;8R6X$Fyos8sIv|L4# z+_()r@;|?V8aD+&`IxIbk6J2a@>iLg&&$X1Wx1=Wsb2A54A#=-!iMU^tL4_OI05!WyEJ_FU7KJ79nq45LnD)9)FkMt@(xxPA`Sh<*BD&SX=`89 zxmcv|z`-tLp)P0{H>3`4f3dmGQ}O zwE>N0xi927+ads)OeUNVwiox$0Ice`n-m4Nxq{L2*bT=N`4xMPTc}e)`{49uv^4t{ zt&$s!SFUC%xQR0yK_!f3{npk&!LswTl1@SWd;TWqb9kp4&q=^r|^?uR8wB_f2y$W5h zM@BLUdsv0d6$_eaV9~3>biVPKIS4}Bi!Fsm!;V;k9gizOzrl%yAuIhT4zzkuL6@Um z#LF|YmViy?Dd(q#Hx%YG#o zn<~VZ5tzc4-YNqSh=?`NuedyJp{&BHPnaaz@tYdTTshYOXhLf^vEVA$6Xc4lnS#7% zT^*O+R@;bg5oMgDNLuA)xrv;@cBmFiE0>KA=^>1|=6ZsGcYmaDpE{)t}OQ zp?-y^Dv?>KR9*eb4er;ruGgAbO!ePqn(&t~4v~xRoU)}E;VBp_w`JUxZ#Y;4ICcw2 z1&UMzwdQen7HU8SDjIMwrUdQ?KFjpHC_=$Qp2ij>e@#YC(zc3-CiPpSm>eECba2_K z)z(3*zjed5EnC0&mrqMzRmyJNQyZrYogHkLGQH-SyySCx_8tDrXTN^(WcU5|-#26C z%;P6dm=NjjJ3VLKT-c1eK_Ihb`4z4ufeGdvUy$igT}#WThW~uzUCHH{ddr&5&V_He z_ul88efGcn!5=SOxq3n8VuT9yYRAXuULDcy{sYJU>cnnop&x;yr}<-HcuTn za?AlN5f4jU;`)@>mC;|KoxvoTN8!HS)3RY!tX^e1gB7cm@7cZgbD#gpvK6a4<}HAS z(6d&SOp|>X?zwCIQ_pWXc<3nMeeZkUGktoqlv3dnCr&I})Ctw-NS79A45KDaL+X%I z*uw6{8qOGjR)Pk!D;OpX0nGRSFW@Cfa`VN3uS8%GmDHBX^%alQv)!Av5ec5cz1*}}MN{ItL=drb z_`Am7K|=sK)zkaR%dhZsXSU369QEzDZ+>;__TT%x-(S9J`A0tZ0gD+;yn2hy96Ud= zXV0E*efxV)KNs)nBOm^dnVN%L2VhMH4_&;pVdF-rbwsqtLujEtnSRa4B>M&#j1-{- zIB#TP^nOC(MnNG;bkNmgyzX;I#pEdq7tVd~fqOVu&p!WB=b|M`m#?C&%w?)aOm6D5 zW^5(l?Ah;b{nO9pc%_FQeygy#{TI5sdvL+bSr)Wr!3VCUPNV6v-Cs5x_2k^>kj%Bc zd-n2)=ggh+{`Wre|NibDeEy4%FJ8974{;VN2db&~sne#jR=uYNzVYM>-}?3o8rrmZ zBhhe-4`+*qM>7~JP^Q?AP^i75qy3{F{(z~;B*$gHQ>;gq((nmth;2t9#Mt{qA&iX| zAt9Fre1SG06~{k5UIZZ@sw)MR$ioB)O0hCXRHF~6q5L1KqUdgq(x^I$oa4Y!N1BP+ z&v$H0rM($tZY3GOp`;_jsHSO+Ei;-eVO!j}&{F{#FL1Qt!q6%iA0}=M(xfsCeQ&`K6ctMc zLKtwbE;<2(kmb^^o>M&nrRi_E%sJ2-;{8UXNUSRu56TtC(-26}K`6A)C#AocWWp94 zGBJ|w?r!tTdf?=U*ii>$HCmR`V&nrzGjLJ~Ju$CpNLB(cVT{nUb{xBS{P03@2#K3TUuT<@^bZD!ok3sSAe zoK!!rW4=!?3G2aF%(PI2cz-BNpvpsUmDDBQQvBDi_n$sxPZjW?uivF?B!1<})zGWU zxTH`s@PwY}V8k%aaIx>6rE*@&wJT(N|K7c!3m!tdD_7-fBE9#Tv}DqTiqv)*|w&PJz{BvfAd{hO@ISR=>Xgkrn)iYCr2o#P&N9^TN5W!jCyR0 zX!BPj_K;(NU9Xo7;HW|AspLhX8zP!E45ms`iWVlxOVRa(Wd3NLXQnU)R2vNV1_m-b zh?#`(v7*670=7;Z&t4vd@+lk`%auv)a^-WH$shHn0%1!02(eXXsyZutQNKiw1Jbv`ZOE10R=Vw3u5nqYmaCHXH_MJF>_?1_- z{LyE=q~^72*KEG+Hl*!8-4{|_z54o=Z5uXkW-0=G6u>oGlmy>Fc4!O?18#iq{(A8xX$uIovU;2B${CDoWXREbthX=?O3t0>vsD2H?y2Wqad^&Mci$}ytoPJO zYo>I>f}|)TgfLMbQHgqs^{u>h+JiwUY-a8$ zhc+@CA@lfgy+=+y`|T%Iu30Z}N3PN>YgRwF$$CPp~IQAwGmMvfU-uJ!dOJDr*-~PlWKlzKFxcjcVX0^3hdObAQ-*fWN z_U+q0_vI%~pGx-Cr#|_MbK6^OZ6GVi<^?am{PO=t+kFLVmR*N^?>L=v?t44;=WiTmw$F$irEMGZK z%zFIj$sc_8`#X2PE`pTC-|Ns7zZ~gVZ{@)!ucQ`oW?P9R5U%6=K z-q&7z`S< zd0F(LH0xM`z`ZqIY(`iiNCqo1j0A~yee^ENlbBXpF-L#+nPFJe-hFaB60X?S6Qt~3 z6K0_|UaT4~<}iB3e;CB{IURR@Sny1lSINI}ssMR|_B28di&~f>ZG2p`Ydconqcf8P z+d5@d0$`?j&e8$~x^0Zz1{SPJW(_{Y(EdUz>qxCGU%9k!!FJqMFIYS?Yg0S-5eHSe zFuY7F2W2S#*5CQ5Py1QH0oF6nn>xhr(x$NuYdR6*)wO%7&{KHH>V4V7eji5c)v9;mWM zU?tD$hNNHIp1;M4I;(YME$7U*0a7u|(m~A!JZn=gcf>JSp3cDe*fSpklg$VhE?RT@ zdb>u89waKwPcd5^xq^OpI`y0C`nUhiZ$I_q;}C?xg!qTQr*hExnNR+lkV1JZAu&vA z#F!WZ;MhX)6A?cEKY8*USVN8GE?cw==@5&oS^qn_A{Cs-Ra=R3U@3!+F*J6RE+@`d zdHK>6`_j6P@E=}C!Afg4)?|CZ(0>II29*rK7+>SD4~SWx!ATz3lO|@7WRI*fFr+ZE zeWqan%eK12rP6!t2OfIkb&qt>&!2ynRERi57!%h<{8aq|&GdJ0v3LQ+c#6KmXLM2A zgeKJF!&D1i88SvmuY9te$Q%u)@i{5!?0*%jAZcXW|0jvj66tESDhJ7tZ z37K@@$EcB#dZifY-jr~P?iF`=yZsZU*ZVtnjrN!^HTAY;ry8fGy#|?D|FEG^-yCF< z=2Z1iz~~7pKag}YDbH@I+>8W)W)T&$)h%4lz=zjhEB7xlMV$h9E@q-53WdR-?#%3t z{SQBC(cIPRS4|3}a=-XXzx)Tk|NCc8o&JsA{5yU={c|>G*z(S)li&SbZpQVQ&wl3V zr>$s{*EvtGDAoRlAJ$1?IEhW2C=du7613)1V>z;Wag3&w<(4nsBC)@3Ki)dYQlCjO z?9cuAKmP~6_j|&u|NejYAN|O4%x>QjlcK)yBGfJ|K`6{ z^Dlhi^Cs^fJ$mFv&zGBVefcY2e)_4WY!)T1iq!Y+J@MqzCTx!MK)VlcW)P1Z1y$^P zLTkhaliW~@GDxUgzjbvA4h$)9c!oA8ijv|5t)vOU;&}_rZU5@O^(#7j@4owPTl1}2 z^X0F66?6XaAN;}p@teQp=b5LUGQf21+*wDp3;^ft-m~kAU;I*Ib|+7fOi9GeTeffc z>ULcAm0aa1jnJ8^(Ur^q;qJ}2GMrF-h-E8QeevJ;GtMHk(~|Jo5{55*>5D|?AN+&g z{Tu(w|L*6B#~*jPGcJGUyXBKyHeLAI*S=lH~j-NOM zBs0(*__%lPZb1+669zk{`7@vW9ORk@g5_fcsLq;ELR0ksj2IR=eeUOf-iBdUu3e!j zYge!S+Lyl|*Y@>q{_$`B_W$i?-`>4XKJmCIu7C9Pe<~BLp?!Pz{QMWbz``Cre2|eK zW}9tEw{>eF66^{bhvFt~5N}PHa0DGz*fpb=iv2`yx{w*5aD_a>s$dF7I7Ou+&@T>F zKqVrVoMHx{vGu7uP2pm|0AF50gHu&LD4;0H4>Q(u98dvA0OY0%BHCsytBq;Opw5|H zg+29^?o8coA92=KHLzj+sLi5?+g7IA&*S>ln>XqN-??>tN=BlC>S#YB2J>rou3x%z z@n=8rDQKTP`x^6Yl8Q%fzV)VXCvug!RCfUMZX;TY1liM$C#o8j7f?Taagyh3v~l;Y z9hg_FUR90(+>xsY8l==d-w-`Se%ig|4B!cUb!l01NbtgoFPuAb21Q6IVFpSpi8j@z z%p|Bli>ebeee7Q@HWa$-mxkBJkDt)3ZIHCUYp^*+REd|=iAw4u>3V?f&$SXLcQ$NT zNB-T>cp0p9YfZd3b8Z}X98?&?W;9bx}egO;sX0`O8i~WE6r(%9 zuOfdI%(o@JX&EAC2QEo|FgPHz`|+Rr$+yp+ci1zRfF;AZf+-fwf9K@My}Nfr;gU%o zaZNYRoHg>h5R>L$L6FkDL|umjgNT<43P`2f^UZI4>%xWe0)|9x zpsYz=c6@9QRz>yD+>O=c+?qOUyUun@WnTOgCqV*LzWwcQo;`imu#6Yk#-=zIpXJTH z=rC)Z?rw3(0uUcbBxH!)q@%I?fv6T3%% z^H#*&aslNjb(sub08$BMVcTRObq@8Bk6FLOiU=!ngiYTJ0A}S~a zpz7JGY!G*fk=o71xt-x3e(j-#ZrZZhY-=X~+T85o2bV~Ofj}pOqPD(Kq6var4%pDD(=LZpq%CF5 z3sS+Am|HN<2%EQMyIi9oZf@Vc_0Rm`*S_&jziHqJDrM0|yEandWA>OEH9I5za6p)J zju4xHKs_gv;+FDAlsM!MvJYUu0!m?DN0mEn>(ZSMNIyflZ~wl(@bCRQ-~7h6%z=}G znNFkAlzhpi&0F^D*&}m)`t&I#n`Kdtt=s$}9L~A^MIr`a=dr`ybq$KU;d@9 zee>JjvA`5L{R2EZXavNA@vU+}ozLh3-Zl)-LI<5Zwfv@z2S zf*2G4nUE1=F#rqMGU@kqyt{o|<>zfZV%_4O`P!E_gWvhxKU}tAiN5|n`n}(Wwd>JG z9@@5jn@Gq$lOwufXJzKmwQW0KQIC-4oNSi0pATd%i2&PVUQ0Im-aW-GwG_a5r6phb z;^#H={Xcz<0Xul;kRPqMV;_W>nVDTXcLMqF!GmZ;k?d*VYil^{E_UmtIRS)?_GHm@ z>>5!aMfLh%iX)8%ITsw#&^2pS)w!@n&q3@Ph0hQ~rO2Wd6w4n;em6v0Dh}h68PP-2 z4Ra)_P;BJdc@E^yoT(-yOVh<(85^u9oVBG*b9?irlc5(~l|sA6OrI!zF5+Yrp_#xph649uYc`5`Z7E<4yLNNeBR?%n1+>y2JL@s9l5aQ(x>jXLLV z*}U=c#VZaZwIsFq2_s8XUnQO(V6kts6+t$@+6l?L0Nu3yz$h)K2$gh$vAFryZwRv> z;yuuLUHjZL(1*~XS;r!IL=kh};4IN#7(H7^E~b|-%gfd51xc^Erge0O<%f`tmM+OP zNir@X2=5|V!oz6ixolmIlyhuqfX^${gjNn)v-vM_6B2@jk-lVm`c>5#?|AFcbc?qo z06nX~DDH2MD?s#{M$@2<_j=%$2NRY1-gW=2F=A``F5jzuYT(1CzDDQ(ozrlICc-7Q znSwE!DJ7}`Pfgc_|DbijHKA~Gk{Gg!a7GD~K}xS6Yj~$%8AFmBviwWQ46oyWANT6l zvfD1UOuWLu`(X2h&wlo^&pr2?r#p7+Mk7|ZID~YFP@PNW2uFDWL_-a&C{f@a(Mp)x7Q%q05XI`nU-;q|q;m`> znI+0dd6!paov72riALcWU zx?th@^&3s(BKS!C=}&)3;loFdJCR1aS{A@L&wcsQ4u!mO=>yGC6g)qWz6ivMA)C?TQZ5?{yEq@~zXbz%R?djr#@&*^X zhl&6IKmbWZK~$SItXpT}4|83a^)G+vbHw{Q-}zG~E_>=9tWkr{*21}W&-tz9vh{3w z8$d8F=Feh%_96(PbF-kxo2bDiy>8fi#NgN3HEXOAG6{xP``l-L4y4D9pKv@>65aq= zNAN=4Y(I1Pi8t$(%(I4L8;Bwano~Pg)@Qj2Kbpbi79ryii}U%;g(B+|-nOVqQ~qJ@ zh5@PnMy|*z*TQ1p2#~$!;_}HiY9kMU!632V&Z=P$Tvuejlc6Dt2H zd>k(<8(n0bzh2CEjT%&1Tj4O|A@|m$@odzvaMLzAgVB&(xO~MbIasW@b?>^u#weC7 zUj+=U^FF;{M*+ZXELTg0>oy&43L~Lh?AmwmieFx_Dw2)rayg6 z)`UV8A$hM}zJ@^sff06uDV)-80u{aMHIn!!4m2(Y^u=QtrNJg(itQTADgYhRq#z=$ z@ie_OklHe}#8l{v*Fe?koa3m{t_*nYIm8s<6cI-Y0}90U4}GFCFUmX8CIEq2yy10b zPDKd9hDh@#V2euns@@6>-zPyvWGPV)2u;0Ju^eBD#tQimmWjmBg5b)J^$YY%n~UQe0?@oevm?YDgYKi|xH=IM%7k#vfP5rh#?3)D#1EqZN}o6*GVEW* z-hGeFb{!MdQx`H{lt5ZipD@WQAzR`;GQT&CxD`;9*KmelCs#5E2s7MhNNQqm$5J>; z9O4v81Veo6D_-OS>xaj`-tj)QN^esrE#^K*;^6HlNyrLet>cQY1ZFq|L}vk$nP%=M zzyS-XVZxG)aw7LaaYCrV-fcb{Z0;7MrSc*KILR>Rd0Nmb@XHPNEM9&(oECmnOrQ0R(wr{6g} zyJOZ_6OTQ1>(y6ZMkA^(t}vPU_6H`<%6u%h_JQ1(ZakP-wQ7Me67o!geKnUw^FRu{ zgJE(+nVEaHbi<7d_!h+%IhWVTyYr48J3ceBbMw|M=3~A1;tR4KUIDmOuRJ2hV2*}q z!!X@GJ3ArnV5B>ukDd7|~qF1$-!XXoj3kw_z@{TEr90x7Cw<50=E9iDkJ5(NSoXo2v_G50%Kx z*1*$`hYCdg`j?3sA_0uN4$-U&>w}BpdCnNw74Yz(BbJHb!pDE+6Y@`I&zwb7mv)WH zsZw;S(Q{|cN@op`t?M7911-#&97x6I)$nfChiQ8L#ZKT3m52D9y@ky zc@y3@e(YmU>t723gcl@=Oc|`PXBKhP!0EGR_w3yRG2xD8a{u|nRv;${(b?zR&oDpP zefA}()j7F0&zvDinvZ?_w5dSTGdyLclRHbGL2!ykysofl}n0I$q$1 zPV_9C^}F8rXg2YR0$OU`51BPsR(?LPC=z$%tKJ*iW&YmH+l!Yix7C$_iTB<;|HKoI zeC{(ptL_(Gc*X7vIEWVvbh6j1)p=*noRdkFPaXdF6IA>VeEWgeZ4-2RU zqNPp1)JC1UI>f<%1afB31B_>-<)coffaw?xSn4RIbn04TrUXNa757@UNz4(C^(d0| zLeXlXEJKEfY8e;z!n!zVzPVK8ikO8?o>|G#dyRPKHYXt;`6}<^9<)+8GT z3wVqR+@{(DX81zj$<Ph@2nNFKZ4`Bx(c%LW zF~SiFfQ%Ue07E%7oM#u}O(xRKy#&x7y9O0=~9^f@ioO#NYqjZZ&yJeCw=+K_|l~81&#w@etYi0 zPhL}L@MS-PHXv2fR$dt!lq4gG^}t+8f>Qv6Mis3IaOYH$;hZTrM|s+dJyB$U+5mJ} z7afVtjVU)#pa7uKHbFs$YGQz=9I_A!ACj^HD#ev*c98kxS`4tk3*nAdLbr1Wa1^5x z=mADeMohqrd2F>&U^tBG0KT#SsW(2z*ggnpbfHNTl3Pq#C7~AT?&1d@TkB*_zpWks8bD$DXu%d>Y}r zT9KVjDeeI^G9QrD;Nf^gR{oJk9z~AajAl%i6y-?TJK<#MlK0P_xBr;CnJ!fiX2h$B zyxNb7ItPF!`l`;+W_4t`^RJ zaR0uipLtTns`4T%S|^crUo3HQ?yS9fqu;&*Qy$onD=Rt3pFxB`HqPtDUY+?Uyi?3}-k<*Tr?+q4K5g}ecd*f~ z5;Dr8w217&`#Hd|xqI?5z|$7$AWLQqEkp`qoBn}L>0(_pZ6yaxbU20Bwd*$y9(ddE z8ETjiW+B`5nXP6iVHIkt6Krr7UaR@tch94MxgaLyi&N>R=(Ks(!vc(o5y?MV?4Ur# zgy@atvdHsy?c0Zy+UENo97pV-xhPqdRJ83AD`2>A>((tl&Tdzn(c>*|Gp@at(nOFPL%dc8R#n_9*izWV84XvB< zb@7fCfQDK~Qi_U$S@P_`oWCuS(5?c# zG{~zA;Z-Nrdg`qN)gx*)*K4jFZ}n(s)-dDjo|~oesI)JvBr$V&8~V%*jBLv2o*!}a zPyOhEx&zr>8Qzg_F=7#Nj3lMrS{}*xK^f#_V2Rt@1yhJ4B=A~SKcSN%Q4pn?riscE zk(G%{V-QEJNQ%19zMmJkUnfHjehSr3%!;0wKZ)5WfW>jCiap6$Ko$y!ruZDGN8q5I zITT`C-VK^)p{nr0C_G1WWN?fyc?P%!>XktyjT@G&Xdp3bx?W64(@6hV#-Mfjk=dDG zX7QM~oS-Lp1}*rQYO1lT`w8&BlrIy%#%tj~n&6%Mk9S8M1c6MB?ah#-P$5nf`VfZX z$CdQ}&*ZC^FsRAsCFp#A9Et#N^d4e_*^qqjA-75r(U@CZ8kl4S9wyzlTWuYla2&<5 z*nTKwsyN6J?}o4>Pj;ECbLk;Syp|R-+$6yVEx!N_h>+*CDNqxGI8t?o8r~5XhId zSXCLpL%v$rLy^8TxdIeeBxQ)C-2{*@vZ8a;oHu{_wpr6$GDCI9=I`FKTR+cIe4n{4 z1qaLR_qcp!=Pt(vv;NUKu}EBlFpguMF?{uuFBwEn;bE23;%^;}bc_4|kBmCzufY;y zXQbucz3sExh>CWR&UrX^KS*ft)QY_i?l(p~ct95zqh8mvdVm20T9;ksDyEKG>CdTe zfyVP@ckij^noPzc%Mz7*>&*kiRr3O}VTu=8C2M8k*inp`Nnv1tU??Lj$ib3DwKP+K z3us@KNHhAQ3TL%CJFH}0h>sAeIV74HSz;XJ;Z&0D4lJ;Y2x!E})F(dmS=~6+QVSAV zQhGo7(;qrEOZ>;%L?)`TaPh7^yVLDLC}vK@?dZ{rn1q69$+nK_`Tt2OM#@c4Mb|An z@CsIx%*P9ZN=ttBGoLk1dgbCp_Q%AmPk!pvJvnq}|5#>p%aQPZ|O{cH*S`CG9g(gpVNIH*eyeVD8aJAA0fDjSKI+FSII*z2~5wY_~0c zakYueCAn364GIf2a+jISmY^rIW0O4%4t1&X#?>su*kS>aAUV(Uax*rIxjXM>^as#n zT8BDdR757Si3{A_fiEo{y(p`L3XpSpVBnSyJnmT5{1&qM)i}rW7d{uS0$@po+#bJ3g zVCZXb=&l&+OhcndWU8|2`N;sQ^3#>bsKRR^K#cydx(UhTIQ<-zr=FAQdbg^T8UDIW zV;h7`9(qJ>d{Q%-G3d#+943gojw(5s!81CayN=7_tsxNu=DpdL~I$n7&TFg-O9PnJ9?1$|0NR$-9|9vvOFhSQ2)D&*CM&KH5_ z+tL{`AvC&bZBiwlq`U1c<#EmIG<23>9cDkFOiBWY1ea~1 zXfg(aXDnI+I5_XO&sNKh7GaiDxhZ30lPXz_F;ZMQ*T(D5JoB+vUVg<|fVyQwKTcTo zCGre?RJCcgI}{pa!dzW$<})%hX`sk;2@MD`Eh0t07gGua7S7j02GinJcd~THPo5}r zG*Hr>zs>N#J}gB&mAe=mk8z!fLJ%KYDL2mq#F4Rpe3{7PCs8B`lw`@G4eQsMIa9Dhgty40+EW>VK+Ed7 zFTO{|dnbtL2UCv?ewsGgEZX8$=%X_x=pbni$oYMjH&gTKXBbc+PQbET{SW@3af&5= z{ztn?z!?{kL-!(q2RT%`q5RP|zB4;JBiaqBxfW`#Y7A% zLyBehiAodlB3rENI22!TL>q$w6BuwRO2QMp9TXsnYy-nj*m6hH832Q*bVu@13uPxHs}N z8meI-Pqs1=l8eC|(lN!#-HvM7#uWA&G2KFbO`_fiRw5YGdOc)S5_6`v+(b9ONhVuS zvR)M=7l%Qpba}=I9>9$(<#%*Bb5W!-EgyD!G+47>RsZ;k7n4K-PaT*W(MOOxlHw(P zpr}H2Skai}%e=;mM)~g5KtWo4jIa+B6U9$aS+wl6ogUWM+23VB#Ep#B-j0xGAguNL5Gv4{7s zp9rwPJlzki3Ju5Or4Oqa9{}%?Ga!iFsO%g>L+FN0bd$XsN4YU6LtOBQh-Xv9MjS#E zEJ971wGqK$L&D^n)PVp23|O0?9~f$f+n?8yJUX$6G5%#ZfGKFHv!XDQlqFfRaCYYo zsGK@|iXI^rApG`8%_!xnm@LmAs%M&ZmKQ?8j695*Tjl42I}BX%s;_3?W8@q2x@~Aw zrS2;wUQ(GcN>EtPmjxk`oI$}#Rvr|2cZKr4jru^@Q)a~vd%+^;F zk_gnosAdt(Tey4o9zN{dbMJ!91+TCN1!y_)(e5xwiA5QUaRkkvPy_*q+3{)v4<`o& zc3GR!p;UeT4Pyio!@)N6GhT)(ibzC&1VM6ju+oX=gLunkI5jSgVt_O?O`UJN{-(}9 zgU%3x6U;b{HrgF(5ZIauCgn)SL(a@s($`ng^^{7Q`ei zb6+Ux?Ts77c2B?c=37PpHHgP@_zI9HdBnhr;E7!+kw_|9ijNasvF|Z2~)%i1g4zRti@&HWy7#1O;7E+C5fh+Z=Zn_aYNDwf-#!H!F zk8$5IM>lR=w>(NrbMc}B)Vr=ut6aQuZOn-oH=XtA)dXoU*DE6k8#ijIyW>}h0Wk9; z+9e8;42j;hZ{6;Qx64S8$6|A)lqa?9Okz2=;lwi8qsqax&<86vM4sY zI++4J4FbSf16qP@xOR6Ua4vH=fA&52IS&TC_sE+kBv7@3QyD0TGV~Y#T|Fa>RwopD zWP%!80){#PI6l?C4~yz_?)wK9ZQgBtXPFJUHHxmbdO=%cKmmTMkVa3#;Uvv27PHn??*x1DAuJmrR;;Ep za#!K9r5BG~R$Gi{mjv_{3t$RYEnA5>$?d2h9oCA*$N{^cT3KXC6V)P=F=bRmMy+d; z-jkthNHxs7cTq2D^M!+zlk$$FX~wTMcA4guiIJayN-#6cauy@bDmnqg6 zzJ6o$bLz>Dv$jJM>eY~H)DuK7O#}@8=5I7aZaTb|vSv7@7S^EOeux1MPS##SIYc!B2y|ix zI@-Z65-1Y$g&gcM*^XAhU_c;`)`QyPVWi6t+_u)N#O8Q^KoBqQ*MN)+Pz)K( zqb5B9OCmiE((zfP%EO0)q}4b_dSFY;U>U|8`bUw>GS*NG&Gn2IHH*(88SLg=qtS?1 zt@KR*a+Z9xoeVo?cjKnbDzt$Dvh#j%>HZa(V8*$kqCWUVOyLNpVNjA>rY##i(DWPg z?DRiuNb1qNrFnFoWV8uNnIqx;ym?MVRNeXW=fOh`h@e`ura#R{p)s)gKiKBCCUGt+ zXzZyj7urIY2}lqC3b6=ENXK072-38$B|CQRVC5a`0Dx*s3`lS?syNB0UpMuEQC0tp z)gDwp;_9oRloKi9G>nEj>2Cdsq!BUdO6n>opBW5j&p8vef~eCJ$`Hkd3WH!03Ln&L zf#Lp#_M4H7lECM*R5Mr+-qOxM9dcE~pKaviM#l~-LZ>b~1~q)CA&5&u8}|i_FD|OJ zWRb6&uI?|<(g-`eVD*|+yZ7uqdHmSe;sr9mVaq}otbRu>f;DAy1~Kz*c|gX+>)9?d3|b8D zF24yDpoeV+;*J%1Zy!AP_x{g+m=P%y8-NE?2)Mi+ydx5=!nDbI_Poe!R$`zOB7Ghx z4Yc|Wl+4tED_5?3^-G_5=9wp7f8(v+`Gq%}ApI9!j& z%tG4sio@u%v5*?Ae3RsvlRw@rBXv8y)_xLFfP(x5Y8ZcWeyYM2^m{}hsspG#%;a}t z0OVtLI;gVYJ0e-*BvbanqwHQ4qgA-q(ERo5)^6OiiO=w)xr`sY4>tRxr%Qudn!%b; z7%YR^$!c1W2nJ@V4TS6^2%^A(El#vtvCQ0towM6!XJ(b{&FLis@-=|R&zyPyy!#-$ zFCxD6mZAuKG2+}ns$(q@PC3ICawUd91?86~u=)z0$L&dYA zkf2bssn3P?{&F`|fP|<+{g{HBy#or;$b;L5V(ce;Pr}~r&}=5YBwqd3+}LO%`;cIS zZ|GL12qMx&nHb*3QNkzFLU!%m{evI0kKpG&_c?2#FJAf}ywnJd5e+Eo%1Mkn1DX5v zyQ$LL8Pi-5hv7OY`|X2=9LQ@=M{`gMLp3=-ni!$BU>n35(DVgRM5P31#;waswjA2N zZQEN14*0rz&mQHNRQ^Q;8pRC_SNEW;D$Jl1k|cuN^;fT)wJ+|`>Mo?J?kCUWfIHzacs#IR>pTD%mJ>LoDAW?8sRaw>f zij9;dz0y1wroW?9_sH?>JFWF^e;Ou5E|nvju)3&!!6KVQ8>q6n3IrEFxTs3I zr>I7<9cC$JZo;r81Fp@)j2ZI<)&u6oJ8bDB2!Ks!=ugN-KCr~V3g4^?aP`_1K496h z72wpmd3q}0-NjPodhrfcpHV8CREaRmgaS&8!XRD|O z7Co?f|7c8(>xA;l1t=sHq@qnGc&_PJ8Icw7f7p@bkp|*9$MJf+GAj(H_&!=c1C#V>=p|x z0b_#}gta*``a2&`E#|XmaLBPVK;3{gGcXPRrtx2CD5Pq4Ru^i6h^Dd94>-qr@!}g~ z>&A6^foX2Zy~W!WZ5wvDK?>4n9}vVuWXldz>ob8|+KCNrqAX3)G^&~cGJ*%nPxqR0 z9G!9Pl~-Opb^7ds`*tVlX{?{F`+r9Mpc|zYQq!)?sh{zAu4Lus-Wg@)K8ptCJJjaw zg9o>5+j8LTgZ314I(4Q*`E1TlUIgKv-K+>z)B(F+^M{3dG}pp(ZcoMFe8b>VB3$9_wn&sGvVrJapKJM67UKz(*FdSx{Mc5eezym8+pt%@HD^>{NG0S~oV8N-G zcWshc^7RH~9fButfvC@@>PAcG{iYP|cnclKuz()tjEloGgE_n^vGi_THmX%;WhhiC z)hCyDt3PV?f>I;v%rdyIj3+8Z&S8_9=d^wM_P_F<{1s^uyG^=mC3@!UIcZ2HE?utX zM#`+@9YU3E@hBsZYfmqtF?tVJQrSQnnGc`2N#`J3`rzUdPd;gjK|9NMs%7$^Fzus^ zUh2h3xB%7INIg$V%P!Wp;ZH@Fn!I~&W@h%U{MEnepynr@deU*R7cX6eh4)qEL!D-C zvU=lEIeKCcx+nm57Z6KU88BJ$z$bt9lgyfPW*&d+aZ`AsOx0wu)tbW`gRt&bZQ)U- z9nn28+OAHP7fpPf;&lz9OIEHshLw|AvXKJOL->TX4u1}>G6bN&w8s-{)tM@1ZMM&B z|Ihx~Ut^ZpmMd4USlfH{>^b^g;5$|bL5A4cX9t_Ok_ks@4+Vf}dVIFDS9?rDpclzB zFXHaAAA6Ru!qX?7c>Kn#YlAk0YXdwW4N47y75te@W=ELfLTrfQ5_w9fpM^&r#|8fT zf9coFCV%p&rvP&83ZK~FU@A1d2+W9<AH^p8Q5RTXcx}t?4hGXagg{^9mZ@HRjjC<%&25SIX z7&rk2$nmVFI1)fz{qdf3h))RXHO~ieY&y~gZWu<5%LdK8gX-@6CnGpA3pmG-^X!9xel&apAKMQ4D(KW4BkT1M>1UrC+~K*B>3 z!eIr64)Q=3cxdx`bjS`LKCJJ!W9P0z?;LdWin&L|iH;sQ%41n$_27g1EtGxr|-Cph&SJO%YoBRJn_V-Q)iAGIcys8uHCzaAdtz(J#w>xkfbn1oATKJu5bt5 z?NE?j^x(cc#$=Aoz5d~}DAu-1$BrI;>#c*P*y|XbIB{GYWAh}5sD~bW=)hYC-v8jj zBac2N#C-nw=TDqC@yH{OsO60}-V~rv_;njL4AE?~E4xAjI=3ef1a0(Gn--}dj#n&~ zv=if&Et{@fx?&eoihcRwrDG?K$zMJC$it^kpFMWs1RweM7cX71sf7Ky5YoJHm@H@p=RA?<(W560A3lUNk3II-nX~V{ z{@Sao+mnw!@!D%|zIWlh9ka90Jbd_wv-GxZ+q`4v&Ua3nw0YpkQ>XUs*-hddXfrdj zmCXF;x#ymJ=4lrCwb$Q}GI{#xr_P>zSD$|K=1sG++v~}iDOGNUWuQ%|5TT|(OlPa* zT4TEaoH?F<4VO^BszXx1`4MK(4EI1c|1~sK7e7TFnhU|DOIt-~HK63gl`A)1e);96 zo_gZ)wHpTyzWwBrPgp1MqaQuLanlCJi5xq2LdI*)p4~TA-hA`T13bn<`}dI$y5{)p zV@Hn~NIi4rthK8)J4MmwUwF}O9ox69K6d>0v13Q=)95&(4=$cQboB7E&pd_TFTebP z@wJ)lGmk&9?ev**FTe8Y?p=B)J6?F3(q0x0F6fL;pg$!pt;LU?GAITR21)PP#Eg@X6hPs3JaCU z{KZqWk9%rVRorZ97lu*}4p2*V|1?yi9oy=x`lZk((mK`4uIeeT+P!0E-HdLxAg*Pks7Ry7bO0fj#&}%4ZPVxWYIeR=@;W zQ3Ruz35_GB10V^c0J3mwZa2`GH;=PeyT+R9oA17RE^&B8PnwQ*YZ$=<$)@6aD2#pF13N(DH_QQt{U%qllkRfj@ zYO!~p{gHWYw&sQBUpV>B3B~N(qZnpLJH7Y*`)ALcvm8uVkmwBojJrc2eefx|Wr-wF z`V2|InUv&-qsM&q-NfyK2M-!5h5LzPCuCF1EID!V1Zgwp8ka9!y7cw0|B>HL=FuE@ z-hAT?5S~7Dh8=(BUDlrHKHMkrl)+dgieE00TM-Lw&432BPeD(5MZy(@p z54?TAK)$>Q1a)cPa`x;Qo&apDtx)xa7aU)BMf7&$=rQGAd+jxwkDoYs+SCBs?dgKV^pE!2R z0n=wrpW(4hLA!p{`K)(;^ur&%_Uh}D*zt46jvgU)=pZVk-hxrybf_y0ZALjs!grdc zUXlX5W|$^l3r?MKi0m~}2hN>6$5c5H=>Peh-xYTG-kZP$PkSo2s6B;f0C02M)Z&UA-y3yzt)f z<0q`9)ET&O>&A(bC+!o>2L|(yZI(N=p>B5(7(E%=lsUDYyOPU)(a+G4`vw0M$~SIB zR>6GzTIaM(uTC2GuhN$H{(rzW=H6<>?hQ>>7#RY4TZrnu_3dxr!ojx>9eVq)oT2p0 z@BhIcI=(}_QjZRpvU9a#M(iki84XXJvR$pN1r~^L|M)xKJ#_exc!JLpJ^#sfzbnUR z5;|)k@LFQu+hxmNeeLyEUV0U0Ny71CM=9f})2Ho*|NZZM&n_LhG)~VtbohvK9#^m1 zh>5C|@6?&eT>+HUD#w;l7+jKgE4`W)HHmYK;j(=!ro5ca2<5yf{W{rA2bD_gtP^Q67VFou z43eh$7~GpTa?^8ZDs3lR`Mg3d_O1jEI}Av{0psxe^Dhui<;iW(WpyE&r(m2pU?G8T zLOMZDd4ZePr23eJVLnIKD6?^U*>VF5x{D{?K6p?Mfq10C*^6>}8Is5mIPSqy<*oLp zsX>o%6Iq5cSpXN5(=&PL<(Fh4bHg!Wa7IwuM0j|xOI-JT4vp5Lnm%Ol?La5zQ4mb# z#}fo^3Rf>x#c)l2SiS5pfs;^lxoP#Z zt$K~?*+j-rgE$Ol8lEmC25I@)`c46e9D>R*N5uo2o}xU%7mjgsPS=l~f9}|^<4IC5 zL%LvO!kWBTj^j9hu{niUIkz>$vOEow3Q8dR%d;(ABLBPM#EBES{TlVyhBc1PB=*{Z z6;6<`L{I|XdKxTZiU;z9P4bYPLTMYrN%XG*~CZfL%T4&kUG}Vi_wtmUa9YRJ3Y}(`l1mxk_7)ow5pPU&i#m}O7yIP z4y$OzxY145^k;6O#&l~U#CS+jQ7F8{jiZs#lv{QbAXnY8oIocK# zFR+gqnVWnBQ{_@f+u@Egm|G;42`F3AG@im$trK155=d%CNFi#ijk?3V1Y?c)K}x-~ zHEU*V{&fAC?zsJd%=avmweZ>k1XHFV8HZus@f?}2C zC^U*Q5(|S)G)w^0=0>d7rHe;(45#rUA_Cgk*&PnMFe?J-Bv)wBI65G}>|-vY#u;oL zPszPLvEYDpvM1Bpb*s;vdvEX}xlfZ9&v*C?bre4do_u$dmC{v$(`}PX&nnf#w4@;pHzb^HI zp7k$>kFR%&$iy$AG=;^GrG6(nm0gBrwjc&}c@bL(uF+C^7@WO(_Qp+%yo#li_rZyE zN+YX~B3rg#;huf_91v!*Gd<@K&=V{sEaFihl_;(|VY<7)$0p(lqg63xn918g5ENiG z)px_jbvkW(_wJpYo!z)$tz#${F9vPh+BK8hqMO4UPrh?%rQ3f1ph*?sEr)VYe6D9(Hz;jfx-z=J8U-`DzUwvK1e(&xFtiMBN)&krj zDsU$13m0tKxXC#-ckW!|@Dkj6_xA1GYmZZQlXN|}f8Y7@7g&KTNa|3)8i|n7+mpI5 zqHVNjRGR&*&>&0%|HunNL>M5UJ$^H=Wb7kLPB&}Se^%adiiUJE&y$XIdvN(*^!n76 z!l3=JwtDR&uYcCot>gjOldk&L>8aNMuwng1!H|vO>YOZCVDHw=8#i3JdUf4~b+fZG z#LyvEA6&d-Af~8eWU?W^AMDw)`$x~cXlHr+)w7zJ-TvVI{W-l`wr4a@ZA;t354bm`e$uQeXo(GBvwk9aDdNRfg?07`ni*qPsY0JJ%#P>FN(7!+p zb_0>fKgZ^`Ms&_stRS?@UV7){gNF|N+^2tzK})K=&yQvClf&#S!SWsC*~}XC4Pg9_ z52=5)l?LB672%uT_@>63@AcT@PoOe?VqQ^ppm1MBq{}SJ`Yps$nyBH&tN}q)wp

X{#eukePad9_8bOCyMFF0FercBZhnCA7D|TdD8U6#3T#qP`)>O+d<6ficm{B! z9`Z=>IK0TI2Kb;XgWBNQkZKzd>LCcEEO~k5l~+FbsZTO- zC>F**%HGP{*y6>tLdOP)5KV*P0kD`H$xkhT|Fxnil%3aEmVDI$VGQsxZlD_6h!z3&Sm^?vv6+k?tA2dd$|2Y3L4O;8#cDss7F zd|r%}9wDPpfxPAHOHL(kBQT6#=naxVu5_d)^%jWw^t3Z}nmbbxYtUin&rm+K8wDVf zwM`|gExLW}17^pULPoU_6}68@F1~x$*CEvYpj!dE*N2c37J|_b>V!-Mk~9Vy)986( zF$yc+JJ>pY-~TnlaeM)L+N+lYELi!6?H>7s5ZsS=Xu2~Zf6yr=BLL&9}lm1{v zq92Z6@49#O>h)J&e*?%&FrPhkTU}%_M0_B?HZwHW=Yxx{z5cp+3?xkV1!{r-nucO4 z9G{o-OnHe5`%9eNnRu$9SQ%G!hQ>}czRaKZqd)zTUJKaXzwm)kLT(0#%4Cp-`F`i* zsn=h7-Az0-lYq<7DY(!nTE(*zxL(OPwq13aP9g5w2-Tzl-ehjm`eI=EAf$26qakWJ zj^g2%nO$({Sc3tD$Gh_^&j335KmXiwCXfj}aN*^bUOayMxX?tNK$C!xs4zrm0P)bF zLpaOfVSzDIhyYXTxR3)b0WbzAeMb~m>=@2#{sM_ZNuaeGgf(}ZmFuMKOBXNbOamGt z#Hd8)wO3v}ar_#UDH*oioD9xGR`ltXgT83-rqgK1{sA3S_ybtlj@;arz?-MWqT zK9!0_Kw@BTRa3Q~gSZ2oa@}-LCH9!mv%<3X-oNkbV`_ZTnwcKv4vYg#(Uu{kauUsSv^adM@PfbyK9Inb zOb=R+0c20u{M_$Da%o&G`ODD|0Q6)IwMpRBUV>j5X8t|)*+Q@zH*RPUC{t<#9XQ}Y zgXQ*()H-oe8{kYt^A8&|KJ>~ju?j+Eg5&+u4x=0a11&4n z1Xs{9c_OejcxJQ3K#g;yy+aKva8Li|{h67Wr=NaC{)xO#f~|n1^WedEWpGW~0~7$H zPky6SeitF5brhD~QvTyV^X!ftvlrgG;0`R5R&tbWzqb`DSE~s=jMPA|Y#2yrlPi6n zwiXzzaqlCAr3+m|2yQZ_!U3fcDu-qP<*)#F>7WA0T1jKfBwDExx)o~RI5B1*rLRoN z<%SH80L(v+?7V0?W~^}->e6NGfZ|}ojwK?*+~k{RhsjCcLG#Dmsg2-qc$WXJQ%ra*N%Pr_Q>(8t;HoHTcJrWDvLx2J5)ra z&;^jXQz=Y=Dd9%+#!1KX=@q5e#KFF9poO2rOv#e&)u#!gz0_nfFTra^+y*GsniE%) zeEQ$JaaaQdaG(e>VANvr=us*^lqGxC$85FcfD2lE81zTypbwy`8&VZ0q^u_ABq4BI zp!|pn9^Soiv#xC>X7sSVJQdBDs(vV>JApU;_@q4-ii|me!a7%wVflbEni9Aa(hA$$ z_n2Mi76tBvmV)^4R?xd>tlz*v9j=G?nII`Ok1B=-nvcQTw$F&oDe&HXdrklJ zq7fO?ZJ15&=nJ^a z*duhN!Qn;0JwV60Vr0<*(d0WBFSxK}77(CeBpRWlo8VW*ruhzdrWK+wR!Is3J3#1V zz=)a&meW$-dFM1KBDr$rL=IVad$dgrbS+rGQiB6AUm4)0AY>YlTzPj;y{XmG=};#4 z%yEjnZaN5T5jkpN3$jsfS+QJ3FH75$b25C%fw);N z3NUD+EnBkySYdp}3ez;F7bT3{kLCrzp-wM&Wt}wbdR@~C?#kud1i@&8yKR~WX~sT& zx}<<|w{P#;x6iTkcw_7`i>1RdI!-W5t;XzlHH>Ow*{~tmeE@(8rrF9NQ4E?G$c#z7 z#l3s?49IgdnbXm`;FW-cl$CF}@vYQ+?4+eKPYVmiP7`@IPO51$u~9KiLJT9A2km;o zd`UAK>Ce{O8F3|{$!nq2jmi#XflE%u0i^o>7$&NW_fx-)ZaDPU8Sl3OFhxQppqK4AKty_gK^iHB45-Ba^!#eF9jrKt9wS3?v?d zP(FsLQAI^U5wq&Y9w@;AT1A_rKV^C6EoyO62OSC}uV1|;dh@j4H6dxg3~X072ML7H z9aBe=?|xkH2}j2@3PTf-LEU4Y!3-uZ^oqZ8AC;~6^yeo6tVDBM`XtaQ61L|Q5Yewk zJq-_p7=ig9id*+<8?BB&wXH-wK!LFpwsd{-`^MX57cSvRM7a_S{!m^>BMxGD3B|Mn z1;kT|I03v_2aJVbEOJ@;H&L#IAyvA$oYTgQ>jju2&bH~pQb`Nq`~G_uwu??=18&{A z_`&eXAC zZ;tkc_3Ib{DV#7Nfwy_oJ0f}VQ3uHg|+}= zsYDW}>-~`gGB0r=wU#q#5Fs8|*8~oQyy)~gJ?T5Q4bU*AE?8)uZ$Am*jZm-g@9 zk0TYF@!$*#Y!5@BbKdVGg)~sH8=hk^}E5%xzJoyf-du;O- zt-tl=n{0yhQ@P{jEL$JiNF_!bKYq;o3G*rLp1*tS@L`5yYX?X#5~UXe+`X$dn$Oc| zMYSfc0#eVC3~=GO=*cbARb6!8;2~_XW{n|x`IT3&Y3Gg^UL)~JjdkFXdUzVFLj^Pd z5^b8~Jc2}zXfr4>eegRLGguopt~`3|II(2SEyXx;YZw7Ve(d;3BfMmOY6~gq$d2oN(W#-nefTM~$`faMY4vbS z$XJ?+D`IXKXlWK-rJ5`VFs}|bGYr}H+ZI}H-n^OETl}$L6Ztfu>LR6n_UR{W!t(xw z3znWR=z1LnwYgiEz{`q4K7Q-wEfm}=N$M&>ShseQMP8e2;Bx!+TW`I+Y3pYCu73eMWXdyq6l*aj(T!GwS!8|8twh$XJI@9X>|%POjqG!lbTp$LI~C4d=muTSkU~7sTD98P<@dk$eYu-0+qVLy=A05?uJK*F zgDyroFJ%}bUR_F;PXJJI7=X5l4fkx>Jfpg1BN>V!40WFdYXGZ~BwYQu+_A=dfh|13 zsa-k7m?NL1%s}6$)52R<@+?A!l%O~Q4OBVeq~gc|6c%H^taa63hoVM!Q&O>C{O~HG zCDKK}QFJWL@!5T|ruZPNK@ANd;IM7OunuqBAdV^ZBNXY)YSc#6!h*r~9j(|}AFwrO zi9#?J=xYuAs#7(js?cNTH7<$lHQE5NH&s%IX>y?PL&D#v#?qyaKk@jBFTBK5?%TUd zRINo(mbJLe2d?iQL}qK zE?eoy1fk62XiBm$jD)aVL(a0>V~3kG*LCZHn<6zX#D zIygC+FmZ>&Bb?)8l`uq^)QCu+q^RPay_ai3r>)cXJgoNMb}v{PvaJc)KDzrup_M^n z^=ek^-AHd-v^;QMYi8s%{~Tt0zufQmG6n_ zh^V<9_It~gwiesCVLdnrfJMvJp!q?EojZ269_{kw9Xqz0A}t#SXqHz_!t|mKE^Gj& z_(m#`CbKVVRG!juYG%Wn_%9IIw9AhMN60O zG8u05+SRM)tBOrd&kz$wR8awIy-W>_B*_o|ly3e9* zD_7#LfS)hfxpTYz%c_+I=;uBC^b_yC_r4_&dL2(b^@QlbKn@vhg68t2-~lRrXB}A3 zh%!R|VSgAItjFS^9nlQlxH$|M0@Q6I!syB3d##rW%EW4%RD;4^C>AkGRANl|vX|c6d4B69r&7cFj;XNDw4XLPZkojFt%wyrfZHRn$79Od!ERNU%eg2Ct zJa1hyfJ$z0;T<_J&}<|wP)vSBB891b0Cq*GspCOVM97n~@S6wUw|}3U#F?{a&6XR4 zs$Z&Fb!laYI?#fVodiJ);)w;o#ZJxtL-`S7b>O#6deBH|eDxH`hv|#O9;lhk5oYIu zhC4%&%FM6DVVb7iWrnL3DMoA>-oY~VYgaL4*RI|6tP*+%TfJ4iv58<{G>9obciv#P zw^>&t)jBZ?RQMN&qZ9IEb8Mt??aH;id-rIaWHlXbNDBwV9)VLTbf~a)@{c-RS`ozn zdO(H0p})yU+UU%Z7w~E+IoBbIlecB*ZW{xc2uu!19YTm3y#jKwF{rJE zsS%9>&C~GmyFSP?u9})89~xrv1J&h@e+mMog}@CyR%$%+x~hEauSg)&?q4A^E=P_8 z)<|P^ueR~Hve|f_3qtFkS3D5>fqV#zuW`g(@Dp1Y3*OZb>i1(ZR@L(*?T4Pqp%MHM z5i%LFyou@wd{0MZMd@jDiN-qngF1v$N6Kzb0;%MlJ_xX=CEU5Xk*bmXZha=WjiwP( zl@j;mtDO5gxbL)lQcDyn#O~SU7L%_ANtQ z=Iz+Aa{wP*L={C=UPb*nmv^P0)~;_0E($U`I}0j>ef0;H0&zCimI0cJ*>^i<#;iuh zL78PMwy2c5#Tv;W>x8^wyno5!b>Ojb1xwegTjQ<3PSfi5*mlK)6~X~=!&plnSf*l- zoBO5@p&!YTjP6fDHUQJjO0V>DRLC^g*-CO6_rWNMkUc$xGk>FHx>J@e#|yX? zALm{jfO*7O(dCsZ7H{3UDMOf$It^k}(WFqf2j|MO zL6Ve!&4@cHgVW6jUMAo>k!T-@d@$s#!7yE*>cx*bfW z(O#-fWg1n<=~GCZp7b+SYP54uN|t}9GlZ!B2ptm?{XLrKq*PYdPcel28EQ=uOGpNx zu%iG^d=p^<4>bT@)sZ9=mMlt_WptdjiTh?w4lhvRR>_hRK~nwkCYpD1dLP_shHOOk z0KuaSIYDQwgiXFNK2$5z#uO+l%PCN|V545j4#gbch2VhJYjMO?>D;}Lqy(7hp#pvB zc8iKM+*l2RFy^#~b_U18Py<2%+OG^qs@FZL(z#^iM~9Wjm+7n;Fn{Rb{dyg!G0?0m zXJ@hpV6hT*UM6Xim9U#AEL5F%19QZ(B)L&biBuD7@7&w7Z;wdw?)9Not^45x+3Cd$ zF1Fq@0}NmZFf1cSKUsh%3_O^M?WiHw#-8h{Ma&u-LYS{@t}2QT%y4$3e1=_Re0))t z7AN5!#fqii#dK>0DQei&%{t8L0^i*mB@RbiMTP^4P!AK0BW>kH&S(gB2xL@?<3lvm zD&%MuKPd^OXr7XXU|J1&K#n9r@-lt~U%Ut&R}A;ga9h2E<2Wjc#TAqRNytt`bdpBV z@qO->JBe*POQ9$Xf_j#SMWi~NjT{A6ccOQ2q^o_-J$j@OPfHF_5qZ75GUA$uTAZva zW3|EpnWXrERGw-z)~cia90gVxoq>apE3;GcQw%!{jC`+dL33bWCxA^^Cgg_R(Tb`B zq;N`$1iTYi8q+l~$5gecuTafo&D)v_49)TFfJgPn=w>J+TCj|Gsh02_J0fKCCLe`j zLZ4wphdeSztc>Y?s!fc>ujc$1cdvuO2F5MkdJt4NK$)a_Q_)YApBGe zy-Zc<1M@nwN!-R_|JXu!0iZl_7NY(qr05$GqAeDK5}x3fYyg=T5pmqqUyFu_>1&uq zUOy2}RRLMrNkA#tTRj6D-+~*g>Q)3Q$MUo%y zI7e^Ht$`g;lq6L%+p2Q!0|~jptU$zyWm^K~)RDl$8qX+BwJqAQW9BdYrN1DMy?F71 zt5*!dwl$o*yALB45l}YpF{l*aNQ4FMXky9i))apL>^|B87@96OO%iM||H@TDL-WaK z^rMeGJTtTXvKg>erCx5bDzjdFckjsiR&%z*@4&0tdY|192Y@+V98<*)2y*xjGjYw6u8-P zS^$uyX^yS8F!=86o9EA+Up5xTr=NxS1wD0H+9?OrV`-F@+CrjrxQsqfzznP?+YL}G zLQ^k{%njPqpJtti6JRg{Rf&QX9-LDG86EsjaQp*Fs10b<=`FuqK@VRD58#zfxteQ2 zNK;jSaU?31!-ybx!RG0J#!UiCCPyoc^T5*7 z8H#`9>Sfj1sm@zNr9wfY5g%m-_lMcfn(3H9ZID8Lb!=}I(^z{(6*W4nLP}e|_h&xw zGjP%h5`jg9z5vOu$-nukdV^MM>?ANKmRdEq^FSvyET}6^B5i>?zYe6Nju!I{(H9#{ z8yz(cHB~F6igp1yIM`D`FoUmSIVPxh5oSYEvaYC0116F}RA{KGUYhESOEpFI++uIZ z1SS+GjqnuMPyo$fYy45z#E<>^_mNq{&E6+F1cUr3mvN2C{`eNMqjO>C%?ChJt8ASQ z3|(GeWa15m?ApECgt`>xCIgazWpRgWF&t4$U1&87R7fE$0V0e*!qh}LBgQ1&T(W${ z*GTIeXupNsuQBowG3mEL=cTi)-15Z8g`wrK;vyAJ`w%DpQPZ%Cp&$a8-(3VUAS0J|dW~AO z>2&$@jC>@Rv*)eM(}A?qUNFKxQjED;eky~dvFRe609L!o$!d5+obh<9#a;Y>2x-7K zcem%08WNWb#sC0yNNq!e9dr>{)j?yyiJC?wKriqi#v_<5B%#h#iR{A_;}A_Cv4qB4 z3ba!Pf5YzT1%@GuiH^0TWnqM6K_-;cfpgL4$eMhmFkw_0#uq&b>R}39Zz`!;fK|R> zIKb-K)(3LWQwC*mNBc6VFm;W1tGTH9B7sXJ73L?P8=vaP7-V$nv`DlK384Uy>;jzY zQ89T;p(%JE3QkyKUf!ldJ`9H5TV+0PGm1Jols98?$pi#ZQ$+)2&_oDhg7}&!%r|P@ z0oBAwwd9aZ%z|a?n+nb3_)iLyZJ4I6qbUG}waIw_jkZ7&5g^n@6c&<_catYNolp~Q z4pE4ta0QZ{5afLBN9(fC`Vc}ch9C<~U1?DO&|OYk|}kY&l!@`ZJ6`=Jy~lcbg6uJf)*;f=ZbL!tLYMw0X`;aaL)G>*%1$ds|%Rm zX<+Iarpxw?q@M#(+^W+lxQrTJi(I5f@2c(F3V4Q!pmz!R3~f_Ei_tSo)oeGsB~}LJ zd4V^yCNUC^lNdJa&Mjk*KI6ZN$%7^r2J%8lPbSRJDz1qDjA21jg>ELV<1VQ1cS70r zV`lw3BAV1uj?n|`z$-KZnc-1{g14bJYlMj$QZcO5kd@GubR%BcXidPvY)wv@-Ox^? z9Ft}cR#0Z!Dw=3nYuYpTkQNPFjE{{|j{rjIPSZL$?f_GKn0O8z5S3VIZ8?}0vV(I0 z2Npw6W#}VfK-C&Z0EPub#OO%N(34E0;AD!l$`b<^4CD}m#@~h$RCfbDtZ@$}NlL;} z8J9;WgV*?>%T6@O2@XsOg&lQ8I$2?{W+oi#k`X?9#DaPzN;yXD&hR?&H0HjTxl&gf zEaabzDWU0!3$x+(b{CbkC!q~y)ia1hAXO59?AI%OMyPxQeWpIcso0uq87S@DMK(= zdK#*tYiv`7LK3)8$b6usOKo|h73;wzo$agQ>EOadJJ2UI#3G8JY-&=YTJ?G%4PGRApGhqf*$sqAAiez-rUrF?j3F36X%d8x3Czlcc z@w6|+(w7i6#0ah_=VZTClSQ$idYUBs(yk5y6%6VN zU;?hgz%g9yc;2m|2m(XuEPoP&IV&ylkW^QSNK47C3&q0N-pTjE-C*z!@|nl*N%3C7 zA*InM+jy2>EM)>uFcBu^yMtJuG@}J9s#xSaERrEE$qsz!Un}lv&VHx)JRe?<_@Lt>x8kfg-SK3v#%Kg+%zMvMIq!7>Eh9XNj+| zItD2jm*;d+b*8DXSi~E43c;c?NM^y3k8FAiK)qsa6hQcXL?>W?*F#NMDg=uI=NEUL zglipmY>EcBsAvXCjzut4K=&n74AiN@E9^?)gamt72tXB;;Zamuh7d3j?HR4EywV$cdF#XRdJEn}MoCew>}LjCNV$x9B!5E=TX&m8YNKCC2QMPq`BwNA1} zu}S3})p=2Qz^4YyWhj*16!r)Xi7zV3zeY&IVpqV6b`BR6B-7)VBpy0l$y+Ou6=I|+ zl_?gHw(wK9I@S?d41GK`7fA5ZU}0;8Rmja%CKfAE>%lP&&>8lLhV!TlKyf=fb5kN{ zKmvfT%7ko2v}K5|HgCtDP(eAB(@zy8FQJU;3WNZpQ3quUAbc0wFN=#(4M(YprUPWi zN`xYinex#0v=Ows9fkyfe-%7nlXMmDRdWG>98>}sK1m-S5J1sZ{UMFeF@uqWO$G_= z$4Z^$94HknM4U3!q80DOduBM`NBQubp9;m0kkkzOSnJrQUiI*b03IgCu4BC$qd zn5~@S%%NHF&WmMCfK#!m6 zm5jzH$AT{XP&Vp2P4}y+L8b!;pcSn8g2bG*h_)YDP8&7@eVR}qZZPAF9)kzfj5OE? z7J#8z;|K40(L*m5%p`9iMixjJ<>AAyhTmRGrFlEZ^Sa*q>4H7CbiW`{nJ@r2rNNbo`IUhRDtohEB*Ac8R<^x2hw&R>WB80<_} z4Yf!`@?XE(qda^>iORczA04RDfkpO;4*P5}t{8saDK}*MWDd{y`&; zuo82033wU4CdBJ)KSRebOnM=K6|8Zy3V(WtkN51^Bj9R_yXZ<| zBp@1k=495fSc z6NdoCy$*y_7hix5K7HZC(+OlQK;>C-tK%luQ zJOel^iuOUAsg`KR&^^*h31o1X>eIEbr^o3*yh-U|X(R{ygqy-AA$0gK)QmpCG^siB zfEkNqq=7?hrDlKyg}TGEjR1tl2~Enyn7C6IPIX!g$7ME9qp(>p3#SpH4Bez(A+-+T9au^iz+5U1 z*3M;jB`w7@@KdVMVAdU*Wps}0X0nNjwXG3@+<}z@Zz_QdTX><^^~`zmIn4c{rCUT3 zS!q#ND$V(#Mbc$3;jjXx%g@kP2RK*^al)9Uj;_Mt5tpKe7HGtA;-=1{^Lg&*J(B0~-R zCKp48IyMf>tR=wojhcfE8TU~H@TYhvo7yz3;&61#4&dY0`kQ?<-S-Vqzy$xWnn$pg ziaZPc@~^*f5GI5W7-*uTXKEs~X@H#K3K-1l%hZ;wM1uBIWSR=DrN$Jw@@w|it!S!k{QuI=?SPp>ku)V6*U#nqYaF!{S2c*Lg|gF=*H9% ztq2C#VsA0W1zVZh$*rz#s$f-b?+I8knQQg5dGNjaI@xu$X4I$}FYMU9-5#yhRlqQJ zU5GMr#92vQDlJYnAme3b5QIGGvJhYuqy!D2Fi03Y$foxD_U>(&HA_}9Y$yrTKcs2)jMvD0i382VKj}9&Fnn%$HTn`f^Vcjr83@+qpEq#gpE=(6{28_ZIVT)pP z0$J`1A`^wi zQntTXKtkOjC%Jv8$N7?`2{PA3E@z&qN?|4f{=DKJcI0cy^bdzosHdF0rT$oyVw{{^ zQ{YXcE(*`sfP%w)VeE(~Pb*s@rME=~0tGwJP_Pr6_^({R(Y~onJX(o27XibnEg%Nh z31F(HpelCJ3reO!#DHMLfi6h^1#-zRLO~u?tPqzWHe2H<>N|n=dhBb7uM{8Bie*dl z;vAS9!BAe(L2N#cTy9b@IZNzAW+XHcr~qQWSe}!jsX-8;>_JstG!Gq= zr9)*-5Mqq)yc=9mIzuuw!)%HxgaBpjWZ|LsxVT(W-W^n1T$Bu zgdYq1iU7ttz;>UmvAPqYkS|rUzGcZr@eX6LRF&$b5~ zre9<%!$<$J*b1P@orn^PCsB$h6<)$Tp&&;y)3plro1a1&m7|)akTo0DZ5%VkaV|Nr z(T*WjU2v*2fdNmSKGD;Ck{*O9I(W5$BAFjRU8lDIH&>&5&O-+a< zh5eF^5tL}z*uucF&?;#$fGH6OWqG=kPl;@l`7+EIhYxxq37JMmLjyJ)&<3xobV4c3 zWe-5}>%tp@Yeu<`0bx_7ZjhERK*nZKDTzNZQNJ7Iu;g%$`gp+g`1M2R-@t zBkz3g2LqgmNEjBSDz>HasllVa8MhqBevUSbNFN_{D zI+ANW%83&kO(tBsSPmPw`i>m2l3?}9m9ey>H-tb{t?gbGD;@bo09bSTHmekjjB_4Hfrcq@*g1WbXG zVhF@^giKY8NU&Y(rb>W;DG?N4RYM>FhEK%CA^eZXW-6lwNrAztH$ukpR!lI<1q{Ah z{GZ_Pj4{CG^&7Nip(`_C*ChgCn;vYREj>qMwytb}ThqFGSKpDoc~>m9D3Y%my;1au z?=HhUNTE>-GXO+@Bo~BxgS8B_835wJmK%!pX%VB%sq7}bxPHtML|obgu~CCHMzvf#7*&t3W{Y6vjOkmp+jZ3 zHQgXh-o5qOwglS)n{(FB8FftUkg9Q1q2c5MD15Rl&7T%)!^qK`i)n4!P?F; zdsXTvXh=aludbq8(pZ6^82GB<6w(4}rDP=kgee0*0D z0OgTtwQ@=#6IiD}QW!;b_<)BOcJ{3V2{onZ5~PBnmsfA%4?#wV*(yh|s-u@OMxM(O6@Q=tQtCiW8UOrDBSpDqU@UT`x#jrfE^V)lam zkb@42hNNWzO5vZ9haQi?&(cj*12_2Olu!Uj00SjR5CIPL19;F?SjC6$k(mT+A%R-#ytE2>bi)f8J+7V9e( zM-m1S6WYTJ9-y6mK_qq^MKmF21q@ozdUQ0S7r)c!KVvGvW`- z*h%54{lrU>WEOW;uc7>^Yp&n4e*LUDa|G=s*46@^zojz@pf#2_e*Eb6t(!nQZQ2Zd zQ70FWrnYrClq0dO`$VINYs^K4eDnj&Zx-^28rN`!69hsv5j}t8D?@%Jt>Vn*z8)EI8ZSkW zYi?MAG>~@esQp5x?Ae7KwojZ~YnvEQ`n0y7kY?EF5jCdC?6RxO6-$;7F}uxV*i3BK_%|d15t?{ z#q^_H?v&S~RQRBgpp5X79!>5LE2FI9dC>$~j5>|gL#_k`QcOl!NLVmv3`P%omK)_i z^(&1YkYB!pIW?9$8LhclY4t!5;4?|8#fVnrU$~Vw`~SSTg9Q+BodI9w+Vr%%lU8EM zFt8>au58R&Y6<}9eTn^~{ZIqT1W!@qB_33eMJ3X?5Ig`m^#Ec?g}=CsSR!-kf;Rv0 zT=7eSJ;j}>qEH-Ly(!KL%!dngCg*W#Mkj@fBCfGa7Fy7EVHX1hZH2lxCP)cGXXvK9 zx&WDr3}v!8`+(>;1kt=PXK76vQPBSCW#*m(peiG-`jUXT9ut+=svMh`=73%L0z?Ka z&N!=EGP?Ajw3kI9sx8hC=gG~I*&1n2=)S1d*dcG}Df)%IsP*W)tPcowl6(Md*{rn` z5R)H#W^jpehYk|cv${&uNA(0u6LF97qi}TzCs4uET(b9qCDtMs$(_vdK&KnnCk^p0 z^}KLxMw6_Ij%4_&S_(PDU_b1ym`QZ-0X42$t~|u+VM$J2V27=^abX9xGV4t-agfc# zWJbxK3~R_q@d9Grj8K#8uUG=Z0g3n_l8ciks+Y-yphs@|u*{H_M{r2UBH_>|YUL(P zihyc>4X~&YCD~)}$GC<(zVdhiGqhk$B_YH+v`l$y97v(#=kR1mz$C{?Mi*3+;=bkn zkf?99sw6fpE(P?mZTT;`1n~}Gb%4k^?8Qcx7zr5oB`8HHsS`fH;}ylMS2@<#YokMvG71h@78DPz@ z1$C7dF%?rQLX+W5_%@2dAR~K?NiFAtj2Sl}ZhRt?(HP>Cz`=7CV^m8tu>s-4{1NOT z|2k4wH9V=#fLEdyI%BQ`gz05WNCq%s#Zd*tIhbC$7+jIS>DrT%)n&L%`M!T)H874#0it~Y8rR^h6SfRCiNVr-F^75 ziXGh|Ek^IuUq*E4P*FTqid!6NQ3yXNBU{3kAsvKRaKHx*6P|OB=)nUAA$#(aspIS` zIDS0R>zXon2TSXS>(t4y%)!{P6Ata)KWNI70W~$uYUDbbm>6h>D0oS8MSEln$Jj*& zL2;%Fn8uJs#Dxhze(d<(j&`$t`eM`OF{8$mlz*xrZni&yL{MX=I{QM2+JX?yMc~9v za1{ngW3gZH+Oay3rjbY{kU$$nhjF6=T~YEV95s~qN>B;;&>2+~>O@E7Ua%bZH=}e+ z!A6x(-?5{8PSD~%wal75dgSP-jSWId7`s%0yV1q+Qu^{l+wN9ndV0E`Lw;mZW3?#& zgT{rO?9j(@#Q-Hlz$9gxG#xMy(4$a0^9)9GBg77!nREEifoU_Q(Mt^tjp#y|_?$SQ zHe(KIlXkM#$&nt@`ml6hZ>Lic@wL!5$mzkP^y#Lq6cHu^QGmrw!E+>3qrpIi{kUh4 zdhG9btgmm{wCOy4yT@_xW2!O3`CT*se+>MCG&7Pi`{b;jji=YlkuX9hym^oyjsKoKu@iSjzoR?lJTg{ zS4kEHF$hWOyojv<>X}d6Tac#5!Ukt>k>Q|9ROdfI>Doz?5-Vc%L%(#K@`%RHe!FQ^ z>i{Wo5|04ogJTau3)O~uST3LL!K&8thl9nr47UUW)_!=x%bkzrgWhImin>{J=Rdr;E#ExO+uWk zxULZBr!UHT9R>_sOhc$k!(ebz3u(f)649j#cn=zeK7VKpIv z-rL#9Z?-WQo5$8hsdaT9IB>{;Go*q(<>ibUJ7(yp(eoF~>pI+pyV#*~IYwZDD3`nx zd4bG5ny_BSPo2JSCa1juQmZ*N(U0>v0N6R%_Es1*l18l=J7FSVdi#z^>e7(zSgurC zJJ~s#b_!Uyc#(6s1yT0y+gIOIPflw_)kI}NB2Epnz!OBBA2n)>aYbUI+%G(tnLbQ` zrg$*pU^+o;2C9l7@T!=Ll3`znj8wrOy#eDm6VNaiKPm_;LGts1Cd3wcB0q^wNiHhg z$>lv=PCFPpWW@M!wZle?QekPj5G|Zj_^VVI=N4Z@$wY~vNN7_0#grruniq zJ2-vz)X;$v{`$vR?(_GxOzlQ;tF~s z4$;xE*WPT{62;vUk31;P{BSn`RMYU>IyE4O;Z|7fDMPvb+?c#4Tkja7dyL z9E%LLx!O*(1(+?w#*Q6tMjqK5!q~a5(|#vaJYv%1I-bq&5jAt>IX$eehv$3f0RO1I zzEPkdo^`I6+ALa#y3Oe+kX1$Bf$E9oN=Gnw{Co9uc6P8z^zp=rlf7e$kj~D|!w0+c z3j0V5;Gu%2cJgEgiX1w0==jlN?d|PTr(lxAH8|06kc*E-1NVl*$+Pd2V?*enPIiPJFmTiT#He-19$P z3Gq;nT1zxbUHw<}+Px)@b<@vT6Cmbubq+HmD>{>5VDR`@iRzEh~~ zA3w>5T(aHr^@ksJ^N)}i>4mR^qJ`1LT(A1=eCqb9rn(o-w*}iw5lD3a-p4P;E zkd|nIepsq1l}{Vh%T={1(VFl5fI)w0D76>2paQeMOm(M#q+Oi4ud|cI9Y20teO;Xa z83d%Bw0iL>>{7r0waTkUz$gXEuL=@)NI%n(Kjr`ajt5?4kQNj59zE7+2biVNzi7#0i%72c(p&YOLB%gZ&^@sMKb`d}&C@%Fj-l=}q2^o+>XFm@ak$ zI-!_%d*IMvPBL;>r^dXeP>fKbU-?#Ec26sal81$e1qD@S(@H-`zC_iQhgIvP&GJMaNmB(#df|OKW+@^ zZg1PW=9P7?yzVMvOYjVif)1*k_SjpiED_sQdmzf0S^z8WeY@ zm<^2&l(I8=)EFfW9z0ZE-@s#bB881OHg4GT(u*&wUAwNl(*NNk&=uEQ#VMIIsdm^f zhyNV5M!ln>!zqpJ`#ZTTTrm$|*(-@YoCX-tywPlyzcJ(8U5A*`sr3z=`#U8s zj~+d8{Dg@lao5hBFTecqrVX1I@=NWq;!a;(cIC3BX-$(S+vGMwl*8SJlg@#5k)|*p!h<2+a70QI|q~Z-_U3e)JYz8 z?A=QaUw-kWJ*{n*0EPedy3Z_}PD8zd=!s)-CBZ;$$2d4aBkFuNTI~BJmr}pWm~44`P(z=gGRM zQ#iQHI?u_b|C=^%-oAbJr51fzxNyO|d2^@Fm?lZUFb$r0>;INE)Gx$%S!Ly9V!VuxW0cDRz^;aUF+lu)_ViJrQE?sU%P{Kk9uT)QpqDY+F zdx@zz5}04hLTYCUufjCtu2z+nlSQjQsQ z&2`rZjW|M%J>h&m`smME+uA{qMlaTLsn7jiE;g68%av|lq(k{y;pF0bDWC7~6J8uR zVDaKBW;8cDMse8iv10y@Jo2-iBYm+%rKd{2vA*i2YAUWM$bYrX%Q1sV%g=r&{iI1n zMz6ebshDiZOsC*u0MZH@kYR;aL>fdZ)!#3H>o;y5wBg*088d1d#zU07h}OQc{u|%= zn*!T_x8HTk(PPJlj~G2}?C9@)??+EO{hWVy-*L+=x7^4u$FI0>c3*qP`VAYMGE~@( z2pUA%{)u9WXsgHQ97Q2sYvk~D3>6M$+Rqj+T`E*s3x%4MLqpWkIaM&e6cu?Mh{k#VqF#Kw$i<`WIP002M$Nkl&9`Zrq56Km3so&6+)P{J04KWKf@e z;xS9`ZoA_y@d#$6G)mA-tc0yHR!UQVDT!_+w7h23%CTd{HBM_BGe-2K7B!!L_W5sr z>zm$n{o>nh>+b0tGj5`z3%>V*2W-6b_~Vbi?*s3@_PT2wJ4LZDM7wuv+qhxF)i+$v zj1E5{lu@>9Y9DAsRamEzqE2s>z>>0yhDFz2(H z9L$!pe@t^&4{`~nI zrU|u^WDD%wyZ7OT9(w7;mpz|9Z`Sb^Nl+dMN~~Q-@=t9B#3gBGsQtncP)IDCG?nLh_01hD;Ah>BdQ*W~s^NPvsD2ccQRa7ZCs!q* z@xuxo=+t!1ijj^*ObmfIDh={=S&vNo_h>-$c@K++^a?A$P`q zgZhphfAGPFH*MKbKXnQe6*fwZmo@a72445${}b}mQwAB&?rCSDZol&m(^Obs)tWV5 z`RaG(%$o&{*P-=N>s94di&dF)@3QMsflK$(Tz>)lo@DTK8#aFA!|$I|f;~?^^V}0p zzSP{@M8QCUf)@+tovOBrcl^C1Qu%(VK#G>@Qb|?ye@{<^CA#)??)!y%?lAcx#x5ng z3SL03z!ec8t1v`@#=?^pHm0bMNgpUblSq?3Tx# zdTGdriSM}go)3TQqc1%F;-CJ9|Mc*qPe@3-`_8wTH88ihZOis+uDMFQfu>>zGm09P zbqVyTqpJYOhc)DV%Plu=-@2`8)G6JMKlS|MPd%rhfBNBfT)SdPeZ$mm|INcIuD$8n zYp(k6M?doTV^4nW^I!bdxBtep!*$nQZIEsBb+otd_S zb=3HJr!~Is{qNniYtP^Q?e`yg_>n*U_kaAmzyEtq;T$_|d`EkmQ>3oD@g`0SSTd`U zNK!_UF-t{hXZ;{9^S=rD6UW9)82{Eg?%up%z3ew}Nl_|A+VM?1$d-w(GB1 z+T1+-fuB4zwQ<&+cif8l&p!R!fBy6Ta^GKnr|)RrU3cCo8_yK*w(gBLzxAz+P16z( z5zgzMve6=>n;wOuSeC^3z;NM0^Nbn8@4m-cJcf4C#7QQe?)&z4R=&Ji@b&%gyko^x z3;CB{`05Ytx##YAb7$l2pFH%FZ+`nb|Mf3E`>9WT;>s(RNgX-ZnR)y>?|GZlZycwc zGGIu8L`);=lA){~GlbbBhwX{MMu?1H3oLo)@*OdpXO}IVQY3H1sVW%Dqnesgx88o| zhV^T^4jnY{=lQBtD?k6)&qL8YciedE%~!WHH;u1ts2N?09Y6fB53gCh_W%BmfA-@C ze=PX-p^tu81b4!ON!vGXTD*8k^UT?D><}fkupF&e@qF4XV{faDn5aN8XrKvS6R$hA zZfR_4G8Av`==jE0zuwxmcUH@^ciwZ$mCF~Z;qzbp;d|cwt|?RNwr<_=gYW<7fgeBg zng9BizxHdtGJDQ!A*y|8@zqj-7BTv5aiBEi7HS5-G`0|&&A`T)5 zdhajXL!CPHyJK(1v}ruOMtQCR2ElgbyCOW$od#7G!_NawTo(l(IqI*No;(aqI0GeV zvBa?y!&i!6>CAA$U}}pO<*eCIF}a;tfK_Hf&Ws!-@+Av0SoH;0CTl0`r(y`s3!1F7 zO?)o*!BqakpBW($9Iyp$H!?U>Nq8gs8>eY+6wyk7>N5XZ^{xCYz4VQBH8p(Nh(Mht z9g8$xEC=(&J1XI_@>-#Naq7bpsU%(Sg=wZUG+8B6fQ}`@?p34&y8sE?y2M=*w=C0z z-mT*28Cxi<9X_bCnA?Pfw$*BZXleQ%$N=^q>G4IKua_PrgZi>wK+DoiE+b1%{|*hD_Xu}?&rFdt7s zPp3>2-nlq-tl2-pg#)=ewkEd%**sV8i4$$@?M@)h9!U8AAJ%9zKqdK?K}h-v$|Dh% z2vI%9(ls^1uyj{TGGr$(hCn}$GQ~m(gKq$YJwPWUp3A@b7M1N62K8k4p?vdMD zx9+UGt55#&d+xmLI;X9U7*!)oU;{?(*OdB(C5sl#YMJq=fA(9CJ^r*gjGJz{j+I@q zbn(GMUF?kmqf}?l@!=yKfIa-wvC|g!Qdf2qVGhmpG&I;@$CgWHSFT!9ysM$V``aJ8 ze#K(>a5lPRaU{Idnj)%iSiW@0^rreh{9pgwcfb3+y4p#L7cH!>uSZJxSaeV#(#4kJ zPuVJwrjgcFs5k+$|36&#cE}J50NUF-1XL0hNbD*jMr5l(ILM6y^jS%KQbs5U8ygZ8 z8O=i2{Hj(oL{2GnbF6ZB7cE~7pWWR@?*GyKu(x>O{EvU+-g&cT@EeAWAjCsQjvi-m z->lhlmMmUaJ8}Hizjfc|KKq&9`!|1J25t77IjwuztvDOwaK&P5Vzn8tlDT53EGLh2 z%isZ6JZ_eC&|n!t2;0!qWYW+w)#6 zx(WoGT?+%}FIq_MC}dG~-qnJ6bAIKYyc0i|ed|4b9E77rk8R-FPH$eeWN~fngs*(< zTYvr6fBVlq{i|eCz|2~JlX5Pzz9F7LJcSS@J;S5hZ-+IFb-gQ@foz;sq2fKRAw_7+eY0?DKpo^|p@W1`<|Htz$t`fF;+uQCE zr*CO)mXxML>d{LpeIS!wD`X0dTo7Hn86JxshxK_bfh5K;Ax}hs2S3Z{8K)h@NmXW% z5XNwvqVb|7H95pDQ-oR1QKnhrmuB2pDlM6IcyGY@onvAL-A*;#cLOQ_b)X~0U9;MjdcLf|s33z4n&vo;Kv}?ZHzP!J$uLx{?y+@gL z)C4VRIda@)5o)OFeEka-e_twa=}QH`B}o3B*CB?Ap?fJlbko6}ZcBkID~tB6S^zw4 zs2J1bdx?2ys=uddIMREJ15}2^A(L0VF4xuhC!WfRb!X(rEODYU4+lSkfD%@W zY`bC;O;<4xnb^g1Vc)Z7q-`2k7aab52)xw&@+C}Mlx)&SFFmRNc;hE9SQLrmSYb#_ zV!teX_u*biKyk4CzN?auQ2Y|c6t8|(kQ5PmlPWybO>)QVn<83#Mb z`ZcZ?XxdKZrnq*NkA{xa?m)YGo;Z0tTaynPD)HipC!bZrhu(Y7jn^&p=1F;`hVafl zN<}D{B}FT)zUue?a$ zcM7JyK!f%0^zETTT@OBxX_{aE)O(gJnj_V~qC4)D^||^IuOW-WN8ftu%^&~B`~K=1 z_dWi^Q_aoM^=1cv>uf^_g+R%;C2G`B@~67GdSf#GaU3k2uNdIc7wum9e(8&w{}V6# z!$GH@aCjViCm-=tyjI_&Ro5`Dk`7_PRZVuVh9k5MH)`Pm~sgQ$;x;s zwU9ExW&_U%ud)nq8R|=AyZ-uX|M{ms`RAYcf+@ZE3+6j>NsjYz2^PnWIr37AYJei0 z)1dEK!p~JqvwR<$BCh1X!6QEQ!Mo?qnobz`K0#s5OGumk3C7&>wmY|P z+4935KPuiVqr;vY#va|>Jr>xBq3IW-=uvqwrroQ>Gyxz9rhtJ1;tU=*m-?eG$zh@P zV-Md%GH%PJ%`0DA*-$_AgYUhwsj&_}kTNnwb?aco4;N%?nKyUNP_Fu#=`@BxBKp zVFqy{oeSHC|Md|UX2ZfX$8pJ6gA{pK7zdvPhf&1?9r13_gDhq1*%JZ)7EOy(DT!!U zN_VodJKOGsRmyf~bgY1+d2!8!d#ZX1PoRICSgy2ZF{Zhz{a`)v>I#JZ=N925V(D25 zxtT;Uak6X;T4JX4j>o(1lXI|T}^jvSzB&=Gexi+wO~jOCkN?h_;=@Trx8hio{scVBit1Z0JW zwEJe4`$a3pxFCIgS>=@QK(+GkP2Q?%8ZD!8B$J&TgnbsA4+Vpc`{}B11*A7^I_>IM zaWT_xBq;NwV06EHQZGZd?>Hd;jCX|!^sFtjD?+PZnfj@d=g(`|wdasmHgD+!e^1ZR zIdf(-HBEKLwC}??c|bjwr8G)hD%5PpfA793h9-P?;C zBWi|8>D{tz`|OscWlQEmk(KS7DvP6%Z*oC6uW8DHis}7 zY>5^lskVHoJc*EHnJ<)^EfPkHoj-fdicvuB*tzq}`Lj1)zkJ@D7OG8Fe%R$gPHK1Y z;ULJ=_JPjM`EzG3m^Vx2Rcl*2G^!jOt5yg)j1>r@7qEITTM_TgIN|5Gr#5s)@$kV= z%Uy{fy63Z!&5BrmKL`FC?RuqjC45491Ru!>6W}`x=Sk%)H!PnzrB*~r6H)NlelLr5k=t9i;^gt8 zH(r00C-;5lzJrJC)DYEyePh_rEn=KWgR&=xA6isRnNV}`SmcfzL{}45oVk7b4jU~k zSvYs`!nvGxz6%~z5B6Y^5m41fdV2PDv^P(0xMsyt0k+MXH-e4E5>C*IqRG>!E|>Z% z8-zDv<~-NFN7TEDEy{*289(6v#XGNUp3#KgIQ_A`=LeoTc^nKj@&KTt33?76UcU5- zx+%3!J@u^Ep+`udWXWgJvltX&T{>Rb&e(0ZD$`G;-E!oE+Q6Xof`0;-T`nn4m~lVi zDf$%$6g6W6eS%sCbbiE$Q5_xoo_zcXnUq&wxe%s}D8wm4P(!A&%vrwj@e{||T3aWM z8-4HFZ&$-hD_6><)824h_~FM7{lvyD8cN{P|54>g6lCJLGOpu6 za&-ilVuA}JV9w|W{6n|e-RjZeZQpRsGWiBF2QX-u>ihs}iZwyjgk0NN+eA3u^-K4- zvvI>_!%hM+d_*>d(GBJ*${mNrWzY~Wf~$=;NDwHZC={TiDmO7aij>(kK*{h^4iAmA z3G0_h-V9SudxEiqQkAgsjI55_+qK{vI`(X24{S!W1>Fz%tMuxdVM`} z>ZELQiL})_DykSXXiA~}=JPa)f+s(;CRQ@fa8SJYVMERxKPjV=hx@VRjKN#n!oJRedVnoZ*SWJsA0p$PivZBgObCCI_|pb4r=hh z2cKnK&Y$g_K7IPAQG!DO^^F$*T*dcGN{VWI&z%q!vw}3l=ny-BQ)%)%4e^Ha*oGyd<>HkN9j}yl&kq|Khhl zebrS`}I$M@`mfL{>aCEZNLB#kpWYt)GDxSeipZr;6nhns_kPHe0n z-`aWr=gO(#UjLnR7s(^G!yHmchbBd!YOXXv^$q;$m;d-@k3IeQFMQ>jU;o^bPd)$H zzx>j~NeyBH^XARmvSrgJf90QCfBn_B-L~(62Oj;aFaDpO{mZX?;QjYbshhld&AKms z>1#LMaKroFckkyu|Mi}pK3X_EEixExiiUz%;;PZ07XTVXI9Hr5PvvQfCml-&lMiwH z*ooHG4zIrb?f1-{x2VfP@e_SU;3LbSlMrJ6&trW@wr*PAzI)fX(yP1#*X-qoIb_Mdn1p-Wf02aq2YxgHsLf9&!pD_pzrHKM%O`uDhD1&EDV9cK-bF zkdW0$6gheN2M=^?*|6b2M>`jB;euJR^yI)cHrWN|B$Z~TZ;eri%qeB!OOdAVUb_A| zadPYDy3|7q*R9(&V1Ui}Mz*vx?Ao=;hCFDGP|$kur@0Xf zl8~i6O3=b#HLF*zZE9+~>6Tl^nv~hwPBDEV{MenrELiJa=RW%Cw*-FEx!lk28pIaY{N+Rn?sk5q7! z+uL^PY1jUJH6w;BUp(*O{f8u6+c1LLLxiE#c$OtrdA~_2V|(OAksw$GclKaQw7BfF zV$73tPyYc4AxUVobl6j6u>q<=--0EF&kPr3XKlxfp2|;ZZQTRfUwY>+EL?I`pFJb` zx{#UwGGd6a-0XyY9Hv#H(2aj({d%!& zT7r6u=VIqe8*?NLo%y*0?Yhh1(lqe$i=oV-7A`0}QS|LvS5i|%%KnqB$n7lE5 zR6O5=xZJ-D(KHcP3#>1!_wK$Eb+N*pMK1pE79hWE7KexPIZfn@)GNZ=? za^7Ak21gCC_8H9c!4;LudeVT*#%6iL*58Qp%txrAu!lH%qX3uIN z75Dz)oe1++U;R#ZcP~M$aP)?n(uIl%;{Ju?f9Iu=_3L;4=5Kt+GIgN+zyIY+|K?wR zdj9-5t5&_TW5=G~``u5|k3aa~!xm87e%no}*R21uKmXR^Mbl0d8eCp!A#XfX%`vR{ z>CdGCFw(3u`*xW*a)Nd zNFR?|w&S1ur$5rP?pueHIxpf7#}`Jxtzjxcm4*7T2beYqPXz`%d)`K+1%_f2E`jyh zFC+&l{_qd}IrmP!$O8}l^q#lf27%8!^U|k2`LPFo{HPJyiWN(re)@$keEElqu9)WM zYb)sgh3WvKo4iZLD2vM1d}CpV>N|E~R!h^bfBNH-CXRpcrPZ6a?D*gZ-f4XQ)vtf| zSAXRXo2E6;yr2K#xBue5{99x0FMj#k$BvyqvcgWUsSm5NH%8AA4)9!sn5qO02;ZK+ z0O!k=ExPHZ8|TfN{nXRX|JHB*+KLs+Hf`Ga{U7|~&;Rou*G-vhdSXgl?F~1qfPrhT zU2d_Ef!$qq-g?!QOMZC&BjKb%$5$IBEQFi*6c)#scBAq%JWw{j2_lMeH$}a1>o!|t z#x4&yXQF)D_MJ-BPM##qWAc>7om)2^J#Y|0gcE9pPd;|Ecf;D3T6gb6RO)NYsG7-> zC$8VPMM{JT{v$oTmPyjlrf;U!O(vWJ%{E!zciy2mM|bXSg$n2J3~sEqj&kM8uQWAG zHO)6-)acr}hMn6so;Y)wMm*TrAr{@$b#T>7&mFQCW!6idnK*6?sO%y^Xn8Ppg<#ID z37<4^;*6FSk7zpl)UUSu{-1Q=z3n^q|G~fc^z2#9CZ_)V|Mi(a{gXeMGG+3kk3K1W zbK?zH!`tUR_x1O^_nt)y=l}Gvr@wRGqjP50|6LsT^8rPf6#B+dMkJax^yWXvAug&< z=+LOezGI%lfM=LWTN!BkBE)shoi+elT3Y(oBJ9?z`^Olaj^^Q9rz*Zo~`i>nwc*s@)Ei+pfeQmLXFd|}V z=%lm03wpKg*+u9i4pKORWE}JrR@aUlGtx@MNoRKN-0b|Lv7<)sTDt)P4|H~{dht1H z2~B(m*iN1(d4GTup9>e3nr`b7wL5UY-sMxKPHkwchu9(I9MYodi#Wyd6%>=}YFfVE za)o^KFzG{s6wQDW>7pBv1QmRWe{5L4-u_R})6~?&QLyqrk^1^N@zTjt8g}>g!nSCK z0yVWJjgN0$w|eLHtpbulVMZWSlA9dr_+FVR@;&lUJW6dTaQQ!A2u|$TvqPM~sU2(9uAe_|_V#VNL38Eu z#XPMwYd2hf-Br_?8tUq5ueo~J&Yjzez`wqp{MLmM0jB2S{#LWUK~Ht{x0_-m>}aQt z*|TSAw5z-4qaS_uyt%Xf@^fEXan;hRuU;|MnU?eaCka?IsEG62W{KF@15^>%d({lzwpIxiQJf%vG1h?N2cmPyK7hL zTW`CO4{`O?%LknA!SGerF0ZG8N$Vx!D@$6!HOBq;rXT;8<1Nh4xxZ)fs5 z>gI&4Tes=P8?M>2rwt?V@XVMjU%HU1!C4IA`uh@1PedZ9$uhLdj;(hzDSDxK%zsV* zfNTh38)l*?g0MiCf!t74R_!0^Y}>M7ZFl!U!b6wu?>r#See(F4mCrx5ZR;j_Au==I zg6W>R+KF^Jw9`kXp44ntOp}RR*ohj@BLGVMmW1(U5M+ z&7Zws;e|8Do`3rBj&|MD5U+Hgxmul)l|oBGqEHAI(~&z-CuPi5G*J3o=pJPJ5B>w5 zOTTP6ZulXv2j{>0JHKkjsPBFM!8`A~Wx;|u#Np7v!|!_6J=a`wCCL)~J$2GdE;jfd zZT(^mXqurBbj4ByZ8lKMDbbnTWx5O#!&_mq3Z?xTwVZ=R#l$HaU}tTy!9@F^3m3Ak zNTX||=5*860z0>C*u87p8FR-M&WkVZYVG*uxBvG0e{fyUe5$s|c5yQF_|fA;j0~;99TAk-~5*Zsf5?isZAO?IWj!tkDdr zfv&u6TlUi@PoK0$c^ptGNQ#njM-K1byngM01A9d#NMcX-k(H}AeEF}w{^&#ZZ`r)w zD*<@G1tb5;SF2%huQl|d7K9CYBEZ6h#}3RPyW-v(8~;IXdX3%86q-JlL2P&B{J*B1R-Ozef%O% z^D97y=xJm@8eDk5fa%HA~b~M9} z`^4%Zjmdn^R51)BRlc+B=#fJbLfnB$k;&ek?w}DLXgQE$h?pr|64F%+Hi^1piA6gL z1~am&`iZO4vZ00;BWcs^jQPlUW>|x-au7;(7Y|ZrnMBFetk{Bb5R3@rIOJG&LVYHdv7RT)Pbtl)TX9- zt>DDQ#;FxByLY$o?F^a`iMO$F4Y08*Nz19yc{LytHz|+urupr=NMLd4|30 zq=<#$rR_*8bQWtJk0O!<0+HG%skpI-?>=}EijCJ%vJkzK>oxHEjpU<84ddD9=?&Lk zMMvE7mg{Y7f%#;{0FRrcjlF8La!?f2ou)g z&$gCcvu4AL>C>P>=#IAsg-3e(o_glR_rLevVDsF$r=EIY_ny|q#u>i95&krq1tR*I zxI#s~3KETyAO>5GIc}KNRNv9j$&*lRL&KDy>B0po)6GV4R$ALSxK>~M(l>wO*FW)v zFMR_EL{jY#14s@GDX8kK0MOsCGpTgD1fLOIa}WSc3G!YO3Z4`d>iYYc{mP&%{D}cL?^k(^DZ}+~g!zRBZ@XD!p--kbT z;mnB*>(-GE_8t-_8&OiM02OAnaL>rmS+n2R*hobn7PbP<-$l@inE84BqJ;@T8^}HL z+{(M|yhU3sEBv+P5xFBf6DEwOWbKP+JA)ZBW?7GQIXnG4tzLYm@NL*YqmdViOX`ey z!fJj;u2M;(DJoPEiZW(Q877Ow8(b%vbit`vQVXI{sl=9+nKMjt@7ir&F@1RCYL^{Wey8gzS4jnvTFLH*9e>86N=mUqku-Vz8z59B4bjb=*UfI3x zdcP$Y>({P9eL@eE0&{`=p5zc z??XElwrbC|SX1k5U6H)_$W=91}kDp+szJz!2VGQ<)ovlhqKM@S=BM ztk|hUGQ@JwXvKwR&UrTq<89APg=~H zfb0t(Yk~n)UMdncFey=MQDno^v17(E>dN2;sL~PIlFgL-6vCW4+uSlM{IFxEwe69h zGQT0->@>9A1D)Xq+ZLWVGjVdm&3C;0Ku7E0{k=dxeNIkNOj63@rTj54ejU?tTzEXs8 z`=<4U6V9DB-2qo;&kB_(Pxf^RM(rZY@3)tm4w{+eQ1Pg+H76>beL^ahP*E<~%_LAI z3LK{xRJ_X&6ZBByz}fhq&N#PfFLZ>ffNi@3!HM(doH(SE7l#ZIGPCS}Eks^MDZN0HdPZ?Y6IN~|0ot89KBO_cdZ9FbC8>&|%}IZGsRB=FdhmHmtz&`noEe8=@6xxmi+F z;WJ&x^_ud2BkX_y zzNVJ`npK?<5)4G;7RwuIm8UQ>aL{;B;_cgaG2V|o_Kdj5efQl@rwI9e^=sdKdDYtG z%NHro*0#?kUSL?cYVFg{ynw~|H0-DtQfq7b?%k~e1|$zdN2<grZxNhpyTB}-G+uE_*uHCKQ{MPrx zvB>r}zxDmjP7_rZ+B^1s`#V3>@wsznhF<-X0+-iRzE>6?(r);YEJ|N0h_JTRe?2|D zZEYPt_~Aoe`PzML?H#LDuUozPmABq{)25AEKJ(eHifmCFTefW9uyHH2uYG0H6Hh+3 zdd(I%#~2(knM@}2sJWPdegp=I6k3p)V>gm68qFRAoHHt5o`WSnXbm)r6C=1~$BLK% z`M+L2+;fz@lFZ>4qahqg-chAA3MIOe#(RiVn?mA*Q_YK!yC*_-7 zNJ5MZ`ZM+|%;+voJ+)gDE5sW(k!9-0(45;iZ_doN_Rij;ClX+HVA0TFqsC6S`o>#k z&tGIaAG3#2Kr+i8%YD|)UZAq45he$5ax2S$#||^ml6KLOnnBM?9H_wm_=Q%rlPB)n zxf|}pSAP8CpW5K@x@)ic;SV1+CSixDEVJYf|Ku?;$l2=dF;(#<9QgA=a#56Yj#|$0 zQc}V~yYGVDC^4Fg?L^}k2)_!OW$YSM6eDdAn|N_iO-xAa2Q>0*A=ZL3C3o)GE2JqC zoX;_2#7LVA?r1&m!S}!CrW>!fE37;OVuNd?*K~AbO3^+q;pn2BsZ>0M3`~#_NcJcQ zD+Xj%guCXJZ9i?$ka2mAVLtD4mqm*f^I&>&0oB{qW<{tW3py~ z5j$_b*y^;S^B2tL?u51br`^x>_8t++GUAOGCrF=1#42uaf(n?;k%c4iB@VGut8A)Q zHf%k1QdBhy%%$;;tF62C=G$h>oNMRuM)?n8MiEkogQflZ52>@Up^*(YkYZ5PZbSiV zB1uphnv@8lFO}jVR-p@*kPXUl*YV7N;gdY#+A_LUeP{@O2Bsc)iwF=EmeCRc;;C}p zKx1Ph@nBakKLsi>0vS1`yYKYi5u-ow{`W4JH`j3sW>@eVCSVR$zp`=WteG?h6d<@H z2(3CT`}naw%;W810l>!HG3ggu9Po0PrK!o7%7p3I%-L<*-G1WqS^NM?_^Ga8+I2U- zwV|nH()jTW^>wz1ulT_Nw~oD%P|3(-%7#9{6GqLBk32!x$xMV8XyT8j8p**u8!$(+jI&s#It&pJ%v#IhOyk+4 zrj>|Hj<3eaS#raChRzw|{X7IeFlDiJ&X!i_27TLjmYgigdI5~>4A zoSSo5S8kO6&I(7dYB~A>t>P@2XTT~@mfA`c<>R#xtkhgC5eJE}Tu>vD0jHf%#-AHm z;Rs};-GX+mHDW7HUmGWpRXU^?sni_KEmwZT^JGTi8tb}EdQXfQ zGomxcm53IMZET!i$?&*wmZ!qu;2-|zfe|BXr#IE=-W%I3HD?}{@K`05u~#8{)uLAB z&ujeK?>}ng{Qd)djrHRm{P9n1=lw5#=Z{p?)YSaka~lT?_=4|q=g$A*Kl!sslj<4e z-XjMWEb#L$qy#A}r zz6ciHfPWZ+&&iWUsg)_+wr%G(zWE*h{H(8^zHeXa_zCrWM^AnI8-G1vV*Ql5@fb7! zyx}S}_j`BJLhv>y<5?Hpm;r0i_H`co%xAwd zeq0Sd;h~3Lm^5+J{{4=g_>$RCK5o_Mz`+w{Oq=-Vqd!B1-}&8tJAeKapZV;4Hl{w( zdwkfi!Ci-s&Ys=CWM0Bv!D7`lgs0MUJWe)!hvzexF1jz^IM0r%7&62@kmt^fw1MP| z=4k_-7_ehk+oAWk*+Wf{=j)N{*NIbnlSISjmv9Pml( zUDz^HjZ>#iojY%i(;Yewc2BOYH9#FZvF_!y8<#9wa{UcAoH=%Q%;*{n+eZhU8-D2U zk!M$~19#n&I&*ZfEoX&Zumgk#7P^#7k>||hX4eKL2fX}6Cub5-iphZ^K-nbD;8dH;J_2@dk;=)YyjbeNp*7Uu1(upHQLnF#9dGoToGX_bzmKf#vbPo)#2<&n7n`jJUT4_`dBJ> z@FW^Hp+xdr(sY8WgagW+8Oel>#2Dun{R}~&p6M{>i#RIETU)p9S-yCIO~|0={Gegm z+t$AOo%aem9oo0u47;VdLc&neaUf|-ix(|sQ>7CmKe;U41dgG4_RFv}SbT_JCTiLA zZYJiCHS7BA7Xgjb!qoaHwm;aoam&5~T~q4n#I$RtG|XGHcv9VzzMdnTJO)lZz3lb5 z;d{FGy|^mNt(;FPVaKrbMBh;vMb$P9`~wKXpVn$a8iZ635YfU58KLne_%zcR8&&f1 z+D+H3SUR?8tjVFq=`HgXEfS{gKCnOAWQ-dP;YSDcS<7jFmCpC*KwqZ*8tT+hf+i_g~cHw8&Qb`F?k7gs4wgdMJ%jW z=>eDOlI@`Hr_Wkxfb6~uK5-%eHm(JKIuojq%a9eap{y9EthjsZ7lJ` ziA$)|B}Umpc>2 z=eU5T#>vXOsaFTY(x);4uDZ#&h?DwiqiS=eoZ9MYC*Yw86Bf9$c=6HzdG6ezMPs6( zBm|r|WvfGvKtE{u6u#7=Me~dw7A~COT{lrS9V)0*KMxW$$hi%d$`CMt)Zs%$n9ZrW zYZPZqFM*G0Dy-fAKozgO)X%&9(fvPFVC&W$ixw?WgI-XzwY5uhyvgWqU=!GUf743B zl;uiK`ms$38{y?idHC8U3sY%y;lgG}sQNf*(h?;(aElhsVmTHqnytl(?(iSX^w<2l zs&qfjC1HgDe?#|jDW`p7X=%~El-Hw>Cr_S`nh5#1b4!Y*RGT!mHPBedMw1)6>$@+}qcEN|MC6 zv*X84fYB(FB+r(t=-6{w|o21z9THO%798u@Y16j$^;se>5Mwxl%VDQ&+`G# z_Ep7N*Z`sJk*$`9f4TI^Utt~;BVm6R2mU+_7xqv;WI!dtrN7zIFj^2JWE-j-NM0o`ngrJ=ge6+Z{GHuZF|}} ze)BheW6bDr2m21t$s;KH@ngG=9Nn;ahYH^P?swbY!3qP;E)`9?hNEbe98#{i%=myA z$7+#k$gG&8o(t#A@Y*@`^!KCv8^CN-H#oXu07nQ~2*%0H<} z%BiF#nAVB~mh=l|R5&$Dvz2Y1y`IQ0_BXkacUeU#DZ_b?6yZx_F3ixB`}H0 z1p#f%x0Y{@j$M?R|Hwx^;uIH0C@$H2GCOn#p5r~sWrWGmpcEs5gxC@>VHBFM@l`gc ztlU{!#~-XnJrM_a?0zX-@rTk#CWCE;AnE;NN@_8-nr$bYE2V$<;6q2826}SzlRx%} z_V$f`{uXPqUVQOeZvt%3-n|s{?G?Kr-6mqZ1QaML z4!^F-2TL=_Wmnr9iW^(BOjOhAI_ChYonl7Kv>kZp00-+^-+6P#4kv*gAeddiTmA>1{4t5NnYn36>y(Ss*UIl<1NTykpps(Z1QD zmq8@RybfL(g$YR>!D9%N!RLLJLh~%RhPCUdBHu(3jZTAJapGJlKSf&0#qyb4WJz^n zqdSmE4J+o=D%HiUM+;u5y?O1l%u&h!xJi-!;;H%g?yFeZcR!@d@`}4kZ)y!z$Yl0^ zYu8QoWp+B<%9KgmK>z?i07*naREmuXEkn4qcw_0Nz4exxTO44yUZfS!-&JY)6p)K^ zqEwRE%j8$#6%*dt4c4H*xUnyJCVYXJR{Y zJhaUTQ5|A;xncyETG1DEus;|t`aVV>^EV8t;x(mW1g;RtjzFvG8w&(%%^q)H?I z)^JFRNCNB+J+!BQCQBbZ3zCAFpamG7eead`4?rq1RraT_k%unJ&E`h4|F!!y{=Qe9 z^71%(6`DR6qrP;V?uynZh>Aric>N2ySKzPwu6Q}5)|&(XYpZ1{jrQ?M7YL*mkbCJX zxO|t;U_*!?--&*u$Kn_%IS(xg4#OCg2jAbRrd>?1sO6HBMsV6DP!#t%7B|JnfjJrl zC#q^{+F-(Kc4nr_X*M7EkY|7U|N5he%X3@$2k5~FOcF(=k6IcxtY?)kUYh;AKm2pI z{q#@$q-6(lGcz`X0}xU&KYH|&!K_Q2mMNM@To(4)&#cLG#?q}@mKi?v5l@~S+M5j*>(AVG)(SFg=|o4 zv1Bpu6}ETt-^fclXbk{VKz(fBRY(=7gzCb9x$OP__fEC&-cMy9h6$^o9(_TYa4eN4 zY7vH~{yhramHeTsjX7Kuw-1ozljls#Vc+bk5Smx>r<=`!SZ9Yq+zvkO=-BvcZyuk! zws_*ynVj{r{>sfm|xCEd!*- z&^j<>Rw1>q;loM@C4pfNxhB)f9Ekpb{!f1DQ=a^d-}=3o`GuZ=0U*)ao;4xXsL0Oo z0nJV4#xMWt&zl@r_v4@bTPBIFE-Y9dmq8E#;T`oyktSCZpCuad&TZ)gAfb#O#xHx9 z)zw|TbeVHDGBWh!hd;D%{l=fX@D+zi^bTyzd|WBRcPJ1$jgw8Qym{o*=l}4}bo#^- zPnbBnYCq!Z*EOS4UFcl%h3+Z#FZv>p1jNRG_$f@_J&Mrua8ofXzNx%`mPy7e`%g;- z1Jul;<&Ko;+*(?8^bLeO`Q(Rz@h4ySszmSP+b2bEaOO--6O=B4f0wM%aK zD_{F|O?~5*{ywCn0X$h(Z^M@G1Jk8o;m^MEGVmPXaNRlKcB8&^lgAKh8*u3dHFc^N zzoYUL^53>_GsrVw<(?RZ&e8e7M;_wIy!5Tt_*iXfxHfH$SP3WARW~#?+cDLNx;K~Z zeCey-K5=sN(4m6^Tl=j@;5kU7H#RtDBH@G)(WyEQ`NRB01(o;VN!QF)Mbk!;gEB=z zOre6R%yt!1YhMr6EzWBy?L-A~HA&ZqYBF(bNzh8^r79aaX9h2Onkc4-)}^syTI20_ zHqZo)=m#%(^Jz6XV08Ic<50pQ+V8% zUfXHGPX$nINV;k$HB33i*2@?R8t)NeP$nl?QUcUX@ES*?+DC5CHZ zu2O%-so%0?^S8hCit|hmJ>B4p(sbSt*9T~;bdv81&GJ#}fh~aLQ+k1(@PT)srJJ=$ z`WN%H8?!@g%`eH`kx6a zAO(!v`2ZgeM*1FLrTbhg@C1qnFa<#YHp~!(Sb?{(tSd+*<+{JR(6G;gB`DqxUF{>_aW3)!fQ zJ-D3^memu97&VgwH+YZEi@RDz$l1Da{o1i(C+Fv8g;uRAcN~#n^{8c}zxu2H9nO8~ zLmz6}*r5f<2xGFi=glL>Klj@&Sdab06Cd2Yd#Ae!*iBDKQhcm4@SRu^+z1 zLaB8~Gc!{E_f zI3GS!I4p9gT=7j_#F=aL>bJh#ZfCi^etEke{OCtt{^qxATKhl#Prvq`{=H?Vub7?; z!m4Ts$At+e8Nc~E|Mv?n{Or&Etm&|ciE*qoaen;#z0rpcJ<{F1i3@_8IBglGwQ|vZ zNuJ5aS7cvUsbe!yHB~k!iH_AeAfp|$TOyx0pUK}ROAR@d2VIk#K6&i;=$TU=_|S)? zvUcs+^ZbuI|D`W{>3{yUfAY}7`+n}He$qlKDKU?$ELvE+WsURa|LCh{&W`QgwbM~T zGqba{vb%BPrZu74ckSCYG^h>-83DkbGh>9wNg`O-H+!fcFx$klnVgz<>-E>5XiHD8 z!vufm*^iw*efp)BzilUiU;KriH#uvQWrNbG5**csUwiSjKYrnB+WkA9{jA@UlauWE zh4bfUrmvWXb)+`k&fN}ml60sP_*or%}| zPc)@6kuge{a99P0#e&9i!|wwfogqd!M~fE%)9+G(np3Yd;&) z+qku(ql01V=xonk#&_;owedsGKK;QDeL&vO4jg_b!Zr+}0_g3CbllUhQ5!VYzH*4l6)ExOs*(aFsT6;dV`JyJ+nb^ETBLB#`(0T3nE z|G)oInefxI*{a^ki5$d(8*KDq1bok5| z;9R&FM=61La>ZphaiAf=djhNr{kl@VGEhB3DJrBIHR52cQ)8ngmO|Sc?A9o|Al-Ne zbb%jDvYHG_0Qb3PpZ3OLj{oK#|MvrZJ>N`oxPbe)sZBN@$;d z{`vj;_f1Vr&~B2)0|Nt#9ql%QuzauJBHD&9^!4|C_OqXncX8T}Bv}MCseb&NS3dY% zqKH4X_c7mu>GG?z6UAM+s#Fb^=E9-j%?GW~ zvx#{s7b-VA2wK9}MY@BekTU0up=!PD1ZwL{+}J5fVue|Cy_EK6I@BY^qQ(3+jw;iI z5T3E<6Hfw@Yegv0g4DBT8czy2wXL_QCKI)feegp()T>u>W{!j2e&H8?;d8(7xsxYO zig+(xoZP!_-^R|4`PnPSj=l56FTZFwrlNlGr+?PPiHV7#74Pjoc<9RXW!w5oD4InG zl5GH#8kg*fF8x~2mR&=ku9&J-du1K|z~fn&u)HLR^)o;7)AMt4Cr_OE2fy;GyLOEH z;L{)7IK1;J; z88N7Lw22O^CsunOdf4heNfvu)_VxAt7yspd{_DT~&mHjc+0Xvn4?p?DVXIraHz|WT zb>i5cyzt`P`&ps(=}&)pWc$d(#KTM= zSwVsrCX*@%8$}@(P%>>PNU?NXg3Z;cEG_g0rQ8^%qTfKCTF~&I}CLE!sr`sy#2=8$MpX2Ll1rInP;3IgqlcWMcu)} z9}rjLY&;jfO+O~=gjLyxoh%gf!A{5FhA6c}haP>jV`CRhXeM-U+u+arhd=Y%|LS+Y z`sZKs^W3u^d+6bV8#_82tZ@AJ(HFk-b)7mp{L?@6(@tyVSAm_n*u87_{QNwG6FO}& zVDvHTQv@ePw?=15Ip#0BX2!5S^@AV9e&QvmhsPg#OiuKL7ry)(|MGKwq)~V8-YsKx zX=?n)+i#mD)+sIY!_PlQgH2u>XR?rC_ny5v6}l_RWc!Sfn#BA@OyCFzTOPkZ;RnGf z*vNhSP$u1KzQx-x(zy%@i1@gH(*^eff~t;&dNd z>#KA_e=?%H$%>Ojv^YD=ES}SybvmwOJZq~)Y7G~5>F(ZmXT{=8jva8+z6FO~I&~#@ zjZy}xVS+g|o56O}I@B~2y4ohwWp`5M5On^tZ zdzoJ3a-{em3$H+u`B~K2ym<@%k`@cb@8~c2P!v#DP>3GJD;NnCihLG<(Sa}E2Qo4$ zomwpwx2g~VM{{=q56VJ|j?m3L#Lz8UHg7`V&h~bUrEvvG#sVgtVzL5&?mUr67CIdp z_!wy~nKrBk6906JKE??UwJJPoUEqN1Wsq9NaE)eyIu31>2rMeZoTDMSG18CVv`lSQ z(I0qtuh15PSTO@o5&WZ{c#gX~d1-oL>hk~bFMqQz|ASxKM~3(A-P_;aW0OhKJ@Da9 zPOUv9s1{=gXD9{DEc2SE8O(7o)>U!B8XKEX70fn@RE0A%wDr@U`ox#M`VCVIZ@zWJ z&-**tzi%%~517fRi&z3q=KW zr2LOaYXs_`0&)Nu_6Au3AzlisRq#f{pd2jz^@?2WvvW&A}rA- z2%T^ucjs{z(-C60XyDM{Nl$ANLq;c8*|}b2OJ454N<1TDG96>x93AM+VzVi78(l7XmHPQX+i4@bD4 z)_VAn!${AYM3=>yI(wae{6{}Qx?X_^L}Wz4)i;fZ+K+HnK|a^qMn794Js;| z+dA55G%5r)Rk&9XQ{I%DOlY%>IwtHO9~>FBW<;)z4xGO_r;_r(zI{LUw}1A}zw{OS z{byhPs-O4zC2WrzI<$Mw9&@--dq#-FAXHI3I6O=>5q$e{McF5kD}e`gPb~sZPQ2HM zSrE&He;%R*rE<7~JzF+^{J9@8H~eQD`s%gMfBp~N>(Lu^S_;C3hli-=%hQt)Rf~k0 zt@^A_`l>ijkVu#!-X;2g{E9qN6}nDkgasj3H3bquJxT#pnxSyrCaeb%`pga8ASJ^f>>X&Gxu~HwI=gs67*@x_iLW%=^UuJ0h%q=H%8H_ z2?%A^T!})oE1}`2<9F^#wng*d!0+y^_EkyFR z5RTS}uY+Z!968+1SE?GRK$vo?8{`-Zk|(?GOrFfO4X?k_SECxV_tYrLcvHz2z9U5?78uh%D1NsH89{CMD(vz+4+ zL?HYK!|Gd7lL%w*9BZo>Ia>k-H0shln$@6Qqxwrn8VYWUw^uqE=~|gGn(C2Y;ob@LaqI2%&l;nV5-&96RG;iTskmDik2E5Js7*N{x6# zMt~gy=4SW_T{w}h)9^`LaW;ckJD>y)Zh?}RR`1{=ZDdZoCT=LSHpn5ZL@o-RgCNBu zB{5cj39R`T`xwVP!kSTWWnxXE*E0DCRL79hAf8XrafzskP(q`}3)3;{iiq^2B1!b` z86;S@t|cB`GM6DVAmQBav{*ndBX%dQvzU23?le$#IoHDgB^qJY%$JoIAbe>;kVIr= zP|2F0Q=}m%!tdb#ILuslb&Y($+OnV@Dzb@=y$=k^jN8*sf8>={UftNqFbKmIRgV^#t;<(o7_=uaBvW5SpkiT*TEL}r2lirL-A3NMaZ7xN zEJV_C=;m)l#}J*cU1hqeRF=}KBq4rhe0pX+E*$`X8DRk@hRi-1Id~3N_}$#xDxR6U zGSk}8_VB|Gx&PHyUyrmzp%EWz>rC8P!e(ab?xm@zG$fm~!IJ8`Gs=VoUST@aDyTTcyKF*=p-)#ih8!3p5REtZ#yjyJqbq>Ft7cX=3W$?9~lB z`h%h%FYv%y_$Gt-izz}^0&)JR6R)dV=BHEfOkaKdjp<7%q;K3XKYN+tXl-fI5`*KE zlB?Hpwh<|0AjyoNE<`CV3nFUr!2r^@?h0=eris=o9w@8|&}lW@K^`L-a|1H<3i0b% zWvD9(sBoUjJJy=m;FW^pN?CohA;{gC!IiNftuE3&4Trknpxlg2LqGPca+o~U&|}f3 z7+SuEI&i_&+*aJa3?l-YomcD?=V2}}T*5{)P)guqJACPD{d(3{h66$jovd$mw#7*? z61d7ox;2`;)K_{c%z>K)*~JS9oVaRzJ*Dp{eKpl?VDXI1f@641Pi2_p{z`c>qQ?O<_qAON@9r#$7ZBTbN8ynsPg|M!ys!Q0OBG!D(NL*y~#eX2o|Q;Znm7ERMD@QpY0q|MAw5SzHXG)uZI+W|)r z=*D=1J#>#}?M`XSVxwcb;gDgC<;?Eb#tXo>xG(!Afp}OP@#SNk8~F}tYKVVpM`kE*yo13_bp4RT6f!yGOCGf zE%x}~!>_;jwz(N`yc{lrr*+j@lqRfFi)zv7K1#6gZOj||#KXXfSvqvbY;b+nb|8Hv z5m{My_D|0WAVvzAK&aFhfn>3w=J z&Z38-6DaPw%vs-AzKOm{zBL{DT>=&O4As^s3XIK_NM`+R>_!xPS`N2Gr>E_(sAd(= zq*#R4$a5$nxLV)P=z*K}7N;*wx3{(J-LqSd$Bv!IiQ|k<7J4_x=E`buq^J-w#on*4 zZ`4Hz!PbeOkWBC4@Df;n-$1Jy5_{ll;XuBM#G5)S_BAIGwyrg;Gg)C1|6Mz`ulxA( zFZ}5XLTd#sNH5UAa1Qr@`CW5RTrNBWA%?{KhrWa{>6CCwh*WNl+Gof@ zE9k@Y4!E~34-+{#G11%C|Dh+J^pv%q^5XIQ8c?wM=F&~uo$HIAq|mdmbwhM=a@$&o zEs$KmP4EwF_XxN=C9qE}C)C7E$utzcQpv{{HMF+WF4r#4%*^sNckkT=6~|7TKoJg2 z>~X!sr(LdY*4Ec3>Y&(6`vlt3R@9elR@ucg!WPz#(q;}aZ2Gp?Bi%+@8gCu9=#GZv zSxg()sk?8W@2L+zfoDfY$2fCw0o+m&(8egiWr(VKl**Z0jTBM#mVboG) zYW&3&Ad+E6-v%1@8#U#PTEVu5BOY;aV4s-_bpV{2nPeYzo{E_a!R06%qoV{YQHT~T zG7sD7%7ZW)fve&G2x{aUT=sAgZEx{rHr=9nAW;U)zJQsaPlbNfrp>*6-&nXRtjGU1 zu3f9Xw~j|?Ww+6zX=OE@p;TT`e}D1PaCzy>Uy~_Iw%fKH*^N1Spl$5#mK>|9y2-)n zupQ9)2ClKbW6zU`5$-h9+|8V5)EQah1f(3J@YzD)h z=9msU5kmy<3v0u58bl{rILM~E*|WkDB~p!}kQoil#5{*>b?<(*S1Y#pQ0b@}jFOCk z81fl_SEObYmHJB}g{4ml%jgN~BRQ)Gby7 za~Q2h`RVRA@2Uf#12)0vq?Sbpx$_ECTE<;*2icggVNSZB6OyJ2Cc}TAO0Xf7buaN# zJVL7%xDFHzYZrXc_~jh?a3SksuxnKf7=w3}R7QPGtgi|j+!znG+C=;9I%!qVWD`rs zijN2eqomi|CiO^%7p0OO=!8%yQ9JtqFu_sa!Q^%8ISK|H0iZ6(vF9T?kS!e@7bn8V zgrHz40Wl)_R|Xg!$bg%L8V#8>N=(na)0gWJNT1vki7ltG0u}j+OUa6aJ)p}gQpQb) zi=QY2?MM>SA<7EJIq_e2j_YDolc;8^*-BtZuG*Q{d?)HzWyGgd*|2*0^&AP znRve9YPogh@?}s_^3G@XI9EQKe-xA=y*CB|E#B*?$Y&CwLKNuJBQPq|q*z^mSU^Hc zlwJ@MxT0H%!cDV8E({i58poPPgCcc(1%CyNxm>bD1{c$z8BOF5DiJZ?qnbYzM3G^L z1qrR8Gp+dvSy8K)bN3~Pw zg~m}@nF`RrdqFrQloeqTxq7wS%+%`HlP4bgzz66_>ob_ux86P?#Z`B|7H?B((j@T1 zFS$H*NuCDUG+AMSP?4i?>}#)d+F`6HP+sl6_7j?>8JFPLxbRS!Ov?4M{#i zD#^-wN|mDZ==lUc0y*u(9}E*^Kh)z#-fHXEXhhOg4;|e9<{Nie<@NU+3!Po2KwF|U zo8iqFU{gy=h+#R=N>+Mh6(?sjY?k(LNzE#sKJyg?OW}NkF!=*K!4=5?D$F}T<>H0$ z1CJbZ$Ie|lWz^1|IVlR-^KVN&9; zR^G-ahD^asM#>XVnzMi}Gpae%Ty>F@so|s(ukUrR$|Zx75{;l^ z9Jf(4cW=(kTjGEK2`Yd9)jWpsu6PKqtR^T;QwCv^LLMjCj*Y?iE`v8O zcSo23?bNl945x&)p$Sa5x!mL=hLwumh8-#=LK5$jb_l7Q^9H;ug-1y9De64RX<~#b zqY2Bg*m-3*l{`w&TxqKi9IKSu1P0-qRY&HnRNY=nE+CF?Bs)FD?uX)Wt=7z&7y#KW z({PzCyw7=4yHzASs*%uxzsP%C6-|zfGFaF~;W~;BN~0Z8>8`QF-#U=dPhznIUOp5I zM5Yp68FP{gkRJ~WqCrwgZ?GZO8JL@EZMTI=cC7B~Y}>bY*LT1B`mNh5tqnPdUJHl< zwPsuFt|AdtUENF!=~m6sIQMIl*#%CCKG`A%c-crNNsba%M1;&DEid2Mw53bIh`fTQ zd;9trgQG`}+lQD%CnGp2v7nU`8$0cJQ>L4%su~-`a}pA%?T*J1mZN`xTDXG3&6N}+x&xGHXNEux|n3?WV?e`~{0;73G)qeUt> zZ^#FcPtptRQ)Z`MU{DGGgJz>}p+qLqc)^|@_l60|6jGQ|kAWA!`gFk*3z<^$Apeme zy3_R--fRn*a*YNx-R>(cBvc!Y$Gu#Gvj{@jBPx}Zkd?ewl#lCrh@fQ_FGs;t`t)ab z`*!XL6!RnZ?A=8Q&z?EcP$x^2++^IiwKcmKtgUubQdUt2gom8O$MR(;5$jHRi(3g6 z>c5 z=I97iB*kwAq)bgz-%r=D3on4A#sNf*h$-F)HhB3IWIjg;2-GZUQCu(OMztc%(8!Q- z@-t`7CMy-)m>~hu2D`Qo51+?Nc$X~kQ^J(N8s3wXfkz)=;q?y~VQm^Iz(lwTwrS58 zjuQW9+aRI|O3WFRg8R?{b7GI+C-%&XW$}JpQ%k!X1O$3!)22<2Jo@O-BS%b=ajguH zs}svzU1NEpJfDb84$8RnjAoj$5HAoyF>FNPu%5Y&VnJn`9z)IzNlYL_(iUrl!XZAB3XIS7t=2hLA=LU|p4o>pGE0t$lFL=dPa%ppNavp2ZSg(dXeTKM;aX(=vl0^iU^OsUquS(T_T94r=9eYc%X?_h*lti~Y3Hd_{{m6fZ77ejdr=DV<3A%H%y zBjAh`07V;SKPoR*41v`Pt5mQCiX`@={ZxXx#&FlLEC7f}3fEFptULZpDJ5 zwfGYijfh}rJw8Mh(RhRROtnxf-fPouP!L!n(kR%u9rAY|(THk!b>xFz?g(XzmQik{@UcWP90*F8 zHy<5g6n56O6tS?nxfrWPYC2lmTN;~BjgG>^Xg;3al{_~8Qz)yFSR;45TxvzM0p`}(|r zRn(!>U^<#Xj1uIBk-{4I%kpp}v z3x&5mT>MTFN&kc!u}cVnh_QJAEbfi(O-z)N8SVU;ZS5Y+l><^ok-qv;A#)}_wC})vjadH1?cUJX zG{W6^>Z$L%@(P6Vz-Y@d%5^plfYY#UI!SJiD-te<8jI?|y}BWocm-ZqxX4t_;ETYc zO8^TX_fm8f{b0ew0}O26K8#S47bgqLg~d?HgBv$GS2GTGT1_}@3Q1vEI5Ky{WG1c# zM1xRSYsumy&;<`-6~eK#VJF=|gEtr`O{Htvz=&`~_?!i&LJ_&}2S5p_PoerQged2S z7T0ZE_vY?{j~sgE$Pp^hi9>J=49%%EG<7SGD?VRT4?_`(NE{~2(s_Ah+Eji{S}y5Y zh=E7IfIoUHDv@TnZy5nZ7%xgHvPMu?QeAG|(6Sm}74RaDn>KgD0DJ+~2(=Ies=+9_ zkcJK}$p8k_1o!>g)!H(7EeneaRAP&li6dK9J+gh+Y?awJaN&IG?hhY+SD9(q# z<^dO(x3h2|7!1o{>)@R{l3X~;^`h#SA)*I4?r@Phf=sYs27qA|@Fw-ZC=?ees0c!E zwZG(dEyp~?+e_^xY%VR|)E&waSU(<=P-l7VuZ$nU;en>d}{3M`j!@e*wsn_%y~6h~V*!q~!`u+b)`LB#3Mjxz zj97BNK}n7m#YSuKJpfM|g-`%sR#PM#F{b;H<)TYd)loEvvB16isP7Od7}tJoOFAa% z&zDi$oHs)6eVRp@6m$!4nciG7W+Viy=5_5k`JWssm)$iZ=tz;J<)ryDHMJL%ds5e8vk`GzdgQQAa^w z>C|{?6KcY`Iww!4l1bGN8^xH53=^Bd5@*Fss1BloQ?E*lpdLxgsJb(Zdpl$aN(!qA z6v6=HJImi}=nz{aeWiC))0sYjz(d8Xk)BW?#NwA|AkJ_mg)2#EkH)R31dCb#Lx$mE zO?(&Q#tcSaw7Nt-iS9627(hoZM17!6ai^eLUu4z_fUuKyaaORtEQME^(zXpe8)07U z1|)?(GASU=?g{9Ndhy{Piva5fnZh4Q#7eeFr+M#kc)+?X5D*9=t!3i|jHEnb1oGnB z6@6Wp)UdMeM%E`6z73FupqH>jb-lg@ z__M0BedBOCO5_vqDzA$8J$SFj%0&bycFt3@u&pmy9bcv?C=0K;FEk8{@nG=Ay_Kcg zwoCP3Br;~;K{Z~IOyWP;vrMktpjs*OR`|dxV^ro4e0oXsoOe#)S7P3ua*+%mu5sLbDM#vgS@rWbuRL{h?R!qli)D zDC#af2AqU{D3C+|?lm!&%qSip>0!Anh;{=|g=bMUI&pVE&kl-}IW-x$cyp`~k=?y_ z4?8qJKPye*ejO+48d4)A81p15NA5F5=Nj?0X)t&&p^2NKd@SWK0yr@M{1rtdBWvC& zhZEcSUuEtp)vcLDTP#*pkBp4)uHJe39ng89p5L+b@w20Nh=5Kj^z|nutgsx+M4uo= zNibKW+Z1)-$bg_=JnA&IIKhc4p3u17M@ELVGBq*TumNQ&JQoo_BC|6^uMwW`0RT$I z1@_D_N79YG$fowFYx9)?lI-wF7JQ2@Kw^q5VXtvpa>IzC3uSzo@L8Z*ar|JVlp$@blA}YZK z-%6_e%G{;7t24UZ)ZN?LGhl;Gwm8yd>g&#O6iW(H%PdVIqfj{PYOr|^De+}pyt8y& z+K8BNC4EDe^3q(FF?GL(y1F)BnwXe9`R(?BN0>auA(pzdlXl;IeUF0`Zpw>MbW6|; z8j{o4+J5ep7bh=%W?*pe%!#+f26c57Qf1BpModv?ku#*84{I5Cr32UvGZl(yG+fFR zuBlzQvlQ&FTX$=5{_P_#-dbtu?jBmWxp4K!cMd-M0c%$Ak!ZfQDp?$l8%>NyNe?hA zYj>q5$P%QilXYlnY7u;^z2FCk&KY&%6w2@dCD@>{#Zu#_<2bXH?D6!upImhX3iPH) z6AU3W2cG5SrQ`;}g*=FG4+MY#EfGk)GRC4o5mnX~NoY4Om5d&MVmgir&^s2_3&OCY zm;(&NGv8)FHswqFnii9abA~NNE{2A9F3Y3zuT}m@zuUJ}hd!?~?gIu$e z0}9zgIdW{;NUGckj71Zvuhp07cJ&H|qp%TimTxVQX|Zy`kYbt9C~B9rE|ERm0RXX! za0=rEx!kGLy`dF&qo;$E2Ts*Gg;o}zK|BG#4U)qI&zQLUATbJ9ly@Q6*=``%fRp%4 zG_f3s&X?~jlWAK;=mlWmQJBgk8vbQUK@skQA*k>cON0$rN>G_lug#I7Sk0rUqpYp5 zXAi&upcT<;(HIPyR(aagz2wUV&Je?i-1Hh&S+PrWVNw{wxCT%Dj0ib|E>V~X5?DgX zLIRc~tM0VFzCk*t=GO9(U<{|R7g)~y$sW|#F5k-3IV)o27y6pW! zVtg1}r3*}+l`ro1AiX4JR>m$f|I*8^+VU+@V6@SAU=G_D7bpv$}BT#Pk z_rc2-d6hqvuRP%&Z+Op+;ep<*^9$EsdG#wO0Y+csPTf?Vip?kuhO)UDu%q8%oGQ1J zHHEme`(1AUcR9#+LazT{?K_E3UJ@#ER1xcmYr#|{+ee~TFy&2ph+(T>Q=7B~)Nm_U z>zRa#Aj4#=Zl+DM5Gxcd%8)F&igmYc-C9@EFfnn_4qN2KSvRVxON5XpS~{5&yOKB= z7HUnV10W_Yc9$3?;Uv?1H_6n-4I~S$C0vm##J#_2u((lDnn zzC2~{bZ@0SJJA_+x1RwO(b+my7iVFvz0)EOX%;z?#RPg8Wh=Ahh7*c#f= z`YxeE%G6}*nQ^06%a38|++T=Plp@JZ!pshLu!J9UC2^?kS4O z9yeGia51uA;p>;|Z~Eki(Y%n8_T!JhGcZKnZsvd|t6{##Oq^HQ^9Un28Hji=9UJPM z7o6OAOH1vpJ-fQEZm|vkQtj;*`C3{+8e6Sn9hiV0gwR)UFH(g9Ld}zCUl}GiYE61o zS6f)%9>@A%p$*NgWP$}LIidUd4J+2vqq4y{7W2yVmAScjro?(0$X6|pH>*y6u|CwA z(-!i?=K2aT;{9j-JZhYKfJqR|z;sPvLqFbkSAyV@z6}TFsWnRg+zDTTSoYJFdlB~9 z>I+k6$1g8*c5ahS8ofBqy7g@yL>l4@Gv?y4g=ZKQ1{;p`9}L8_7&11{Q~pOZqCMgZ zvb-CbTe!Sb3}WQW92+MMZO83L-*~O&o=l`_xj=^cm?^GPi1y%B#3t_}^6z~S`X$j9 zP9t%34b;lIcV2&O$L>9YBfF2j^-5DqD|C1QQ;K>~4wKOox)I!XICeQm)PSF~ow1eK z6ACgoTU(u)9KB!Fva)=A^vn^Pi95t|>e8saCD1+a=Ebqa<={$hP*6N80cDo5+ouP= zUbt{UEg>on4t9ET@!|v@V%M(SqOQ}YPCFCLN9%5GEMDiP3=a=q9G`$s>0D~})Tz^) z$&u~b)dkx}7>C*Iv$efduf+-mUHGsvQGpi4@vyh`CTa7Nr_MArH4F|7A z)NoH6f~*=xdV03iUhK?!~+B@4*-Vv2hO0OU^Iq%Bcjp#Yjj*{qw&+%IU zDBC{K9UPDf{~##)O|^}Ujm=p~-PX2q=PpA&ckUc9-m`lTV|Mc7N$YsKySGeDPF-8L zicC9pY@fV%$$B&=j+U(l3i?CfBA18+ne%aWy(Z*yf66r}4jK8@l@L#D4 z!vrQ@L@;;mHf?CJVVEtgZ!Ru6t*mu@!>N;}5pmCMJ8<5z;S(|7v7zSp`3vq*m)$oo zV3W)X7tTAGRoVN=lP4^=b4Y5}#?GQ@;HApQiHDxCEF56i!h6_(a;}l*KYiv5%HFI=}(%qH7x+8EV>?cUx^D0uqJn7!#X z*(Ii{i`g|M9u`9DOWq-WXaSOg^N0*Z8-McN>u+?{P2-gx8aPyNIXKX&*L%9Gt<2-*MEjt)oc32Juk+#~E9Oz+sfV|4VKY98mb z*o3LCuir*-yajtS(1{{}GpA2$)AkufZzj6%!e3MhC5p+piPSX)6QPrCZ*;M0H%q`} zb-K)cEnUjxotwY9Z|@!_ES)_w+PSe~=dN8BFJAZCzO)`}_NKM5pSIDPlu-N{0Esic1t3YOHDMQCyGu&G=P-rC^yKf?^1SCWO;_ zD2-KTGg3S!Oe!h-td+88cm3G8bNoC(696`D>>NEiN)_$iyVv80*lZdoyQD5oPE10h zg@hibe~as&8(f^#@z}sC+*IVPyh%ly8+f$zvWS$guyF0F8=QEvZQIcKu?ttO%=QoT zAwA4Espjh3)q$-8;`u9AX57}>!_%6z-oK@JgCoNoV#Fz#o1IsT&zk5sZaoDTp9Uld zf@$Obh*;V>JBcd#F$*i!*W2CJzHxHml06z7D{gZ{z+Ad?skymn z)21!(fX6yIH=a8?di};Va%VUblT-L`^XAQBQBI>5%yT{U&v0af#l2&QlI0;3scTLg z8dE9LpK|255@A=bHKoaU&(B+0zJ6`-RzrQ$*x4hds$1JOPfuU!Xzp;9mvLcBK7XO3 zP3mix3@^0Kw3XBXWNR!zhCZTssHV7z4X+x&+XW~ zZ)s`KDGE-EGREz@4s>toQN(e3c^N=q8nKEFGXWP4{{1V{(^LE*A@JqP_U^cp zr6?P0p_Z&uK|X;?qSDMXkBmVqLm`5cDD&(HRrY&e;QrBfjyV;zZq{zjZI>o4apUIZ z=6dh;TDQdkvet%^x3F;I#EFxD7hr+SS6c`Ah|#?jM+S-5Obo@V^uK82Y;~b){tugbvo0EpCokC#weRSEqdy|tB9zjAIBfa+8tCSnB7bZu~ zjuAijq-OM?fWti$C|w|o*Np)a^$@#MK}f=AhazGlsAfZ!_=)O|d$0cSUR{%zV3kh; zw+q)7xkf@Xr=Brsc4K?{ts}+2EJSuS=2umvKiww8)s z;Ag`F4@&@@o$v4785Cv{k^@DOOtOOc}vuO(W07$hKrLEtjO|G`19P@Ira&D z+B>_BymQnZLc%)%+34uGnCcBpQ5Lv2Gd%1}ads1mWkL|c=L=VhRR%wIeDmgZyJ@=t zP6|AKgTFH4X@@T;;5gbaLew{?_APO0&18 zM?2hwef##&S{Uif*|P&%`-Ngfsgq}J+_bY|H}<%Ae&U@QN72YeYi#hXn<{7tS>2g4qZAzl zE?6>%lc!G87{kNERCigsXJ}dVOwOp95{GAjVTM^%Ek?b0>-zBK5t))#zVqFlo=v-V z?c!lyn!GeTH0=J-QHRXzP!=;~f7O{Qwz+m5?sD6Td`VAFk27H$2>{co#N5eX)3lmR z9y&&qVZ2|rKFb~G6WF}Fdbgx*k|U&W(9cgC+mg{oI8;vNJzMjng?PWocfb~*P1zQD zG3p)@pmG3+yuFfKynAn^#D_JfPmdlru;2Xk_~f+5`uh68p(KkJMZPF=4 z=7nvjVqNGoN*Iyi-suN_C6zAS9Tl6B3 zz`%HqGbH9jvcCR14zRKvm&Gw+@_3&THBs$F+2LSmwaV_t!HKwWIo~ zS2u5wk+H$4slC*Gggh9&6hS1ejn@@r7$-9s+RQ&h9>T{YF!%E?TwsV9OuJp1I*Q7@ zw_;1OdrP-Z96!H%-<_f1UCY-Oge8sZS}Hg+Pf~g+!4E|X*Z#-3#S7dx7)bssiYYs0 zSq&ieS#o6F@b=xLeQxG5BOgukXJ$>Z$*PnX!+2I{~aJ&V>h~3@Y!)Q@n%WQzk zQ=AE90X=pi$vz~Di=Q?LEe8}YG3h5;&PiR)%LJa&L6{Dm>;HS-`2^CW>d!Ld;#CUBsA zTUXacn%8(ZD9Ift9pgTyqNp+nXo@<1CHPG@gwf(gxxI5!96@E`e3!fPWRX2bA>6R1BlJb3lmLRV*pL=MZrnlbJE4 z)nGEtXi_Ri0p;!xmAug+p)!?ixLJdO_F+THJ<5?>n&v0k(OI=YDSk~-4*eTN9yb=|t5;UNmaIxty$q!Hg)F@j#px9rxt0ej8ONbMx@*B2<@0fpn!g+33?#iIHs zS@ej{6I=zz(@1X!3pR{DqMnBk;3h$M(6|xvg9K zZMTX?y1Tou4Vq$C_Gssh?GR;*`k?nyZDsa}=?WMTR{dx3gD}~Ge$iu^R=;9TmLnzUJh{Do^>9tJ= zNE25%?BO>-&=En&owH3++Lm#zCHfoCvCeX2V|{r(}p&X zfQ}qs;{uY<+f+d&sp48@UXmtKHLeU(fJx z|5lzXp;)>#-`3pN+0pAMGxHYNdxN?N6fDAc@v@_+U?dKZ=6qvEipAm}mpujU-V+I3 zzHGf{qcC*l@)gofs5D3+!lQgO#-?x;xByZX*DSR>v#KTnld2g)m9$MNKxJYhc#;3i z&cquk2l4=5=gyti7j9ghPG|z4Xxt3T0E_)7jDYe&x@$MC6E@#!kOv}L3qfa3pQVh5 zrd)Jb6P1`!A!_B7NxGO9ZiEJ9kGi@$%_j=qOU4)62fQBoE1#s z8bjgXDU#rFOTC!S;#LBdnhSJ=20GC>Ojcw&q)Ta#hUKA5lLIi@VtGZIWU{WpI->2M zg+qDb_%SYEBK6hPiwXxA}-UjV$4)NUB7-MOE z0xV`B3q)Zwx)7g=q%|zX#z$106~|PLEpEG-rwwQ!GV9+uaO&i#h?ZiJ6s>a>!!xi5 z-J!?2waKZ;6UR@OIB^3k)4zj5gUZG)U!Fd7`m}JFf=vb3@(KbW69q%Xz>-${pep3} zp~B_oc8rXe+{6(ncmzudNDHrL{bo28A2`MD{P_tL2_*+^Blz{(wr*=}Yri@_Z@S9K z2bqTDxAOC3M(;vpXdT|6;Hu>plXQ$;iu#t0X7IpX!FIeV<653mm2$A)MWRt$TtzW(YPQpE{W9D&~1p}123;GSaP*15#M6-Jl&&+#-ZGwL~AL%7^RKG!Z)L3#$ z@DK{p$w(8;pW?0HUhjc6+ZxkmNF5;t4E0TBxhz5h>FVV3N=@Fm(NWXTKB65(4NFuS zpN4)8HiPN>CI*HO!pl-7nUcD4%k9txa&a6?X3in&yVE;jMMY;8g8t!-)al~b@#Ch> z(bB}Y6mJf~@>ph`vZ1uuQrRP0RpA*w;lwai3b%1g0j z0(TQt;9t1DKtjWxI3!jw@RT>J4DQL3r=>ht)q=HkLLhp6W_C_5UeaA=Cou&fKsh2x zXM#sLSAmMa8Cuw%l9vvM2TPg9<4T0@1DFk<_|11^n2Z!w(6PR@aky`wwPgd3lf$%j zccHIm8!SoeiF;$l82Fs$YkSo&Nsvttf{8SX?8KHrZ-|tqmKQahkRyL2AA@$o!#lV1 z_OEw%-R(Osf8{H0efbYtI=g~^&}7Yt8KwxK54yk_^n1gt&#%f`*U_-N@L~8mC=L_& zDD<#>*uvcW@BHTHW~QeBd*k{w9dq*Bd3|Bt$)nttNH10_ZilAHv&nn{rB&$xP?k4F zkP(VdMs_@WaPOWS+u9EwdTiUaJ&1+ht%b=PAhQj$fD)-FVM6g^({91KS z&pWMF$z%u-Ku!OQ?AT6X$%A&(AYx)DgKIUjb`pYE6bEY(OT{MPa5`CUh0TP>^^~UB zDpkO~DC$NTa-3eM+PiYVXz{%xrttrlQ#r3;E*XPKm&1} z`S=PebM49_!FLQnp$+`OU9OO#277Ye?eFh1fiiV@8t}#oIS>+s5uGLI33G~IR00`J zG^*3MZrvKcctN&K{4qQ{Y~2k8qwhTy&1oBsXjwCZ9+jUfd!VZblc{nKvS$w>erSJnQdrleJ(yU7{iR-^!d^{WMvsrp-ylUc~~=7pD>9UkQy(-el4 z4zQC;Tr=Jnv}nBOlIr(?1sv2jQKDk?+k?BK($q@`{3|Et?j8oR+csNqv%Tz zp@}%~9;d1Jm+WEWzi?4SCMkix`h^HbkDb1HZSmN#(Ho1mjvmcXXn*x1<=Y4sl9?|< zN>~0mPoes;V;5%Ut{*!##-`|tdiwW12pnVaUtnzXUepU3BNiFardoc+0K-6<`AYf( z_hNn+r4K#A$I4ukDeM9&$T7KS-Z87!7Z+55iqnv( z?x3xZ6-_-$qer2Wh3FHSA)?J4*9#+($}XtDmNbcH z-Qu6&<7o9^Or>;&l_vWS>~|Ejdx`{UAutDWASFGM(y=06$*f0lcx4XgzNYB62zjI{ zJn1L?U}0vAEsF5|^2`3J3F))FUUG~VlIkP>T0*X`}YAGzox{d0X)2G zF$@nO9#isp?#|Q;>LtBlBdv%zy+w1n*%e#OVcgz4Wk2Fw>pVA z79_B{5fw7whE#d4uZ;R}qR>(u$B(kE}-n}8&MVH$iCt#jW5h=~f-R16u# zzH9f6rj{mCD<&?=1WqcRi-3hqNfxIA4&BKtf~Tp~sdv{YX9F$dj>?ak&5FhFxXv*t z7sz|eVa9fNc$+X-^l0p=R}lzcxib*OS;-p17$d(xZ2d}vmA%l;(9jT2{)_*;t#=1N1B~1-7dhFAROS_oM$jvI`+HUq=5l!zErGk_Gdz%| zL^R}KeRIpPBX9hl|NNi*)MtLar>|ereArn<0avC}9nN0Kp8dEfE+u1Z(3S4ukw9F= z2yI0^I4eXq2+eIzJ}qQf#Sr#0Gc7(Cwb0s*)*X0=203EbFMLF}tL5 z<O<4l;<)(WCE(u?Y^O^LS35I;m441i9uWNDHW#hwiSN~t*I=Lh9LkgZB{?6suLuvL&MuV zJ~=r_FANOyDU4%$lqSfOh}6k|N4mN&*ebYj5G^Z6w=r5>_g9Y|Jw_lJ8?)ex zD@!z>mv1>_+JgOGT;#88-eOahS|aCwSS3nfq;N$Hl1v`N5_s3tY0+pA!`vbN`*`S6S1{4U-4eeQnu5{9g_ zO0^*w2Lk4O`S0tidUpQ$-~PEDQF77Q*{&q!xo1E8#+&clx)sv`{CAuGcmF=%r=pc< zOXf=0mYB*@jT>mgdo7JkoGOItmx5YXbz^anN{NwMy=yaNQU;kawQ;;`hqex_)z~qh zwbQdR4{h13zU0j}-w=wBban0!$T>hk8endAW@uy^x|?Y8FQ>LURQT#uFl!JCZSC*F znX*ABD?f@NGRpzAB-OAeJ)U4FA&?9M^_JiSZ;;%4Ni{@Gz-hdBpOd6Ysy+$e@Q`I+ z>^jbhOha;lsIYhOvvV6(D6SQAVrcHCAu+u7@lFh;L_YL@l2gmYT``dJW9Mle2F$va zW5-YID4W8cKYyNzkj9JR4?-n-_@f-BrAf=mYFMJuUk% zV-=W%EWDY%{(cIO+9&cXuJ93)bHx!oj5E4aJ)`EMKTn5n+9g=`D>*;X{zAUW;{vFskcfZnAQ- zo~}bp=vNHD&$u!@<5>&<@q|>rUJ#;Cf#QuRVnit!M}*NDS^X!GFPB39@)H!8Mj8~( zlRwWgLmYaNA(j%#v%yhNvC`RqO2}PAHBlmS3*GL74Bn5a73;%p-(4BMI7vCH3~^x2 zsZ*!a@#r2%z$=`<-{rRnO%qHaH-v=l5s`%JMMWS=9C1%6MpVg;loOMa3Q4V5zIbt> ztE*cuPjIx1rr;+uA+aUoW%c?%f1eq{Yu9fe-ImS#vm3XUyLfwGU~`jLg-UFT$9N<3 zeEO1m%VAu|B_VqK`b||JsR=eY+fXm>d-UkZt=n$iTqay|)K(G-b+uis8!t^?*1l$~ zxVShmeq-ULQKXKD!FI;LJl)uW9N17k6O#3uifc{Sd$s`2)5rg069tSS2#?Q@xDo+mbMO`HRsr}C!hTA!w)~axOnr#tKUtTl)efO?k<&Pp+tRi>(uE}Gnc0}v^L2M zS}r>^d136__;+4;HTg$X2+$GRtjMD9b+O$1uctox5w-pDUYHWTbc*?5O`C*G{2;LM z-gRPzK_yTiD;yS`6+p@lbbF}SrlgKj(bd${Z2tujLG+#3hf)$GZ^00&0Hp%hc_ELn zqo@@erf-N949NG$+0xHCW#D~)-tbwv(+oBXx^zZx=uwc+39d)G(IA_;Q!>(MYPpWb zkhZ+%@O7r}OA9_atAwA_;!vu(`cwwAwrVR$rkWbh(wjxu#m>>HI?8~HBd=dj00K+l zGmik4h{CYcn(`*CnF?TC+FDvDAOj>MRuQS4pGDD3V>E#{)9S)P3(VHv-*5Vc zoXbhTjK(GHWc4$ys!a1mpNgStd7`%15bN;vVy@tg-i4(gylg>9mBB!?Me@Y~g#v;E z!!j|^)3Z4<1xb&0Z|v$!E~KtT`7NAi3Qf41NG7rai2zUGf=7kX7|LJs8uX@rfX7)$ zvL@$zzoM06)G(*tIh`%>W@}TkINOjhj_o%)yE+{@f&Co#svMBSAPPs;whU$ioTP>W z1_pY|PI#FYg9=Z^c80?+RQ^Goe^7+_u?Hn+gC_UOdCtx+QfJ_i0?iYr11FBIqR9i#Sdi4oisxXxd+t9&sma>2ppZmdGOsSNWPh1du zr;a3hJTj8?*~Udvk=BlOztOI<%Q+{jT47r7Otk;H)z;?bxTpkyGKyx9N6Eyllmz~X zP!v@bT~T5YpFEa}mC_&Aqp>4ru4Td{EM>SFO_7QZxj*8?+=tO3ck$S~jfrJdeAHu! zHrF^h8V4#z6aF+Fbyr)L&1ly*5FhTo8W-}S`p^LBVWRDkh%&CuMkHl+(0vS*k}`i1 zwR3ifd6<%n@enc-MM1ByaHm?TP(%^a81 z-r7!z#a$lsh=G71zFD@nNF%|5f#nrt;t!Z?(J2#V7D>uJJvSNG`t;&k zAM0D89$ur*)z7(Ezed6}@l*^$gULbn$_9lMAx3W|y$m+iy9BqKB2ggQ&Lmq!q{t5x z+To2rO-7f@0{-?tyBG+qqwY!;zm&u^N)ontu*j4_d6rE8M$BKSJT_i!mPY&zHzpFv zU?Vv(4Y;I47ZH_|`Y4;}>h0Zfd1^A5MOfUcz)x}MvSV$3~Np|(M3yaGwEq1}UDRTDdtyf?9{a^oIYwDZG z149`#?gvZ)@tPV7yKL^tu{}3)%^{#Ds`sj%fgxjJc+Q+U{wu%oE1&tyXD(g7{Coe~ zuic!ThCU<&E+RAjrZJ?0Ne0T;G^4p?eb0{VwvDLj)N#O*75Fx zkf6TAk$`fqB(XP=JF9Y00|1+ z7DzlEeu^Z8-K76i3~&ekU$*W$*t7FI?EBq%@Alqz0W5k4NswR#i$sE?L^84Ls9CWr zO+1-6f7s*LPF#{n>_0M-*d9;fNn%@;RV~@lM6xN76fKb;DGF>LSkUWYd%^bJ^ZCAK zL0g{u?(V(kcg|a${ygvVmILUFPj-P2Y9T2c!G0PKlk#3V2Sr+{AqtVCFa*PrhWj}B zPbV~7UE!igQ9z@NFpZ-?7uP8r2^$ACYFQXD42$;lo8mEu?&~X}tI~E^5l?qQa0Fz7 z2(qtWNk4Uc@toO#Y>YCn%X*rSwVHbvHExm!(hRn#&b_nna9i!D%fV;p@LZxd56T4#6+Aq;uhVGaK^hJBC(|An|1MFsv zreUbz4_UN>QST^#fKi-f8ABFr>i{)~*Z^cwlw^&E9lx^%xR$oyxYePQ#Kt@c}N z-@bM4-d%U!eea*D@n+3P`{suL+0(ai-HN;Ky!H7PUp;c{%y?JyFuMPL9k~|~7A|<^ znOz$;u0C@3s3QUlvF^CzE=|EuNQ2QiT3JIov4^ZC z&6qYUwQdp#OyUS>0ar?)s`R{>M{w&!I;SWWeP+=V_FtzrUEFPj5KVQH^QG!B*b0J!M>u?uvdAAGK9$ zl!z>liVhv8Y%i6OD)DCh7x6<8tw<(?)m_MqO>j+%7ajP~9?fg8s?OM7QdGw)H5N9Bk?qYalAM-k^b)G3!IOru(LqcT;xpGR zW?!<*s`lgx0K~ZHka^5c>0Q&YXmFnqE(uz2Gx+H-;-qVx+?yvw0Npcs)5vn5mdxW{ zeDri!^6Wy3Ts?tl_z%&D3h4H}4?GIum!5g1?S4h=0tubu%zU+cE?tks9d*PS>qWS} z^R}I5&s_LF{>d*~xOBm~-8q3fXBY9VJLydbFA;(ibd13#e@9NAom*~%yiH_z_QZ+Z z-~0aTvK104k$&i`4m0N0udkk4eDd6dtIJke7W0K?U){RO8{e}q5cm(DM0tFY$AYSv z#hHsp9MfQqZ6%qn7#}#xd-qe{hyD3$*EdU#@F>GEbZn|~K_niBp#g3}?L%3?y>M2j zM|U(rwJfxOJRku>!&76OT@wf|S-)<IHhNurArG3*!guZ683Z?3y$5RiCCCC zxhttYjEq=8{4^iK^`T8G1%Q-|_ae^t=ep z8&P2I=V^KrDZCe|+S2Tx2Cm!SAIa7@=A#Q#Vq&5;%9vFL>u}-CuOJQub~0y)diC>m zfb^;vCJDIoSCp9b<*mK#0L4iNj{qFGf%XJ!bAT)qXmLo>KJ=4$#>ZM5D0J z>!E;v2s9n}5DLWlNjo|Mzk}V1aU)Qp|44_u1*6@dNQJmXNXd^%17;lLw5Tx|P;C65 zn2xnmbUy5VClB@U(l@0z-7$PFkjn3Q0O85qxklTu*Ds7(iPd0;p=HRk2pVz1MgB_U zMu3%)0xZoqe!A zj~?|@wZS@2B)k2L&k#}f5{diny_0=hyT&0TSMR&;KH03Ej*k}nt()<;YvPwjoOk@> z>1`X=AeN!$kvI0OU7cTvU}=J1Z~nRQr5o6N&pkVUw_*Ltsjo4qg#JCP>Csnz#&x6m{w3T*5aSxAB|TP_8G z1Ax*p$8Om|lGaFKOd2W;j7;vh7h7G|O4^$kHa;e(9Bm_$ufE+su^^FAgg_)73~ayLLxeri5lyM6gHbP(K;Q; z;O5@bHB}lIgI?Q=x{ecLdI87Fos5&TVp|H9=TGk>{)Q2VtZHFmHY$-Dkx<5b1a??3 z@--d^JLJQ_i6+%5eZ^4prYs^b{K(*=rm`lEl5aW%B)XG9)p&YJyG#E!)=4(XQ zInXRIeA5_xa8l7DGuRM4DS#AP+S?r@Zyo7aV zO0H0Z3Jyk2I~Z2h@gfNAU~OE)74&o(ljQGIF>cI^^Rx9D0mw+Q#cKJ4Mbz-$Fa2a^ zHBu;Kuj@DqL-~9tF1Qd*&=VMy_piyQOCOn@tC!yP@H;lH-}ut=wpiJ7-5ToxsjFO! zAf$ncQ2|nuJsnA^Ap%lmS8E~ixet;I1}oj+55Php$kh?b+a|E++LhVmb8^PJzxRwe zYktWteo&blA4Vnkb!`lWGYk%TCn~`jzhjW5;=q6M0aqd)5EzaZV_khJ37|+f)WEcj z*OPVDuQPUKu59xWXPD3TvZNtv^>1m5MecPpLVgEJc*i%dM^156#AeDFQ3@Aym8H|3 zy=DmHEb?1+&K^GWhShu;xMHEepvuHPEi)WEl8WrfWv}fD+Sa1#i~g9fCX5l3eoN#u z&Y+F;#o4Wed# zJtN#zB$`(*BmtbXk079Z0Ny;0MT}iW-elWg1Vu6Lo^=2=fpd%k>@ty>Dp>|zWNTQa znyakTwS~?{qT7a3)YpS%XWM#4!w^|)RJVQ=5^BQQv`{Lr6DR3t*w13YdK?Vqn0O=o z`V}TDFp?(fWhIqYAes;C(1B2T<9+g7MhA4?4cSUAV>4QWdhm1E%2xCVtBMyqzvH@`S|XF}OuoM8gurCjl_Q$>nG zZ9_X&;*teYvw(mDzfdU;G{ui$Z8053+ZkX;rVO8@57>bgux2apC(`MAI5LAAPwoDk zkhz?(Wx@F;9=0hag&s>@{LZc*^~jKgKrZddmaUt9 z`2Fuh7pu8_SPeg@1SD%3tdJmG=gyBkW=ZjX`f-kl)uyDyJTBa^l}C)c*)j9Et1VSt zvUI08Z6a(btJjTM#^K%ZXV2NZB1*P$RCkc@`wHb994qxAB!V4lmll<=%YGste^W>N zQgJeIECpf;*u1=1Ia|xuuU#wh8%DX@Mh^8&g<@WIaJ(y135*~{J z5r|7LNCP~DOo-vj*|=}rNblHbclxzUEV#egAYN=2eJL@+{sgdQGr^UxXpGI#cv3rK zc;H-LGLLQ+FeJ>Sshy#4`b{7q%wp=qWazh0)5Eg~U$zj(`N2YqXr1ws_ zFAd~LN8Xec89c&FRyvKrDny12dC5w1VnurhiPzvJ{&_p>^Wgft^B42hnaZ(*BauyQ zIf#ZVktW4&bKiOR5)63JTS5+OxB!Qpn9vWZ zj(27pI`JV$4XyN-3X4nqG#0vIY*@@NNRr5e7L18pT#EFi%90R1cpL?ywd)sB78M(_ zdlOo2C_72D;gVpFd5j9M@}+q$0t?96bCpHX_Xy^-Mp}xYmCoq;<=oNsN9Boy0N z_!#J*#%n!f?d9PLvp9%RF?5*wi@l$*{BgEoW}twZR@%gl#_gj8Q3i&qc!Qq_Os(JO z0-fDpt4#;Lw1N70b8Z)Is7oQvp0x<$rkl67c=G&ZMt=}9dJ+TuYAz!A z$>d2sV-*EPxt!Y7Qos4uJWF@gOXD(!`jF>mim%RAFH<(S!p~g&Q zc6Zpc{xrnYcg7b{rx^-|^N_+aRoZnM?~0Fxf*}p`>$r?g2c<%E&_pw_2@&;5R$8w^ zs1z8!)Ki5e&9%dnkCMrdbsia~$eya`k&W;`5ft&;9d)$>B#rBtbB}*MNHo9>q~7q5 z1`X2~>#J+V;o=q1#IlunJ70S(t0;&7#z8FTx3=8rU!efD#sRv=b=C0r#OtA&fW)Gd zRPicXAWsEb>VbhAVOv^)V%t@N0m>tgv$|T`iOxvL++BbpX_2hbtnUj|KmfDs|(LTG3CrB0%?BV&%(d zras-W^d`c`N900w6~ohoD(*+pAPnd|kEkPOJhBz`D=ZsvOiXRA?6sMHROQBgidV{9>|PLMZn~0YE9!qoB`QT87?})NbQ3siA_&4D|*wJ zmXee4bP!pcoI*pbqjH{eZppVkaw%$6!l2$)Yp_qm!z|EN$R;LCnN9rph%sX&LoMdwXW=~V zg2YXiU_f>SGnxZdC6PDSeAH@O<+>3`V2?PX#tT5*8_Xx5BXk~5kt$}=1YhIn7>7~g zvLKdTw$xytDB=@P4rLFnSTOnmMkXNm^(L@OQ?=>rX~xtu1xlHVPRiAjJ$UD68@4Y+2<@g~nRt*5j+-AOT?7<(;T3o+xEp2dV24h26f?@=9b^>oc&i4v zHbp4$1B@KGsaf+APNZ( zBJ6}Gs*E3?4zW=K8DW>+`+_1lR6Uaq*SxNi}7HHQ~FN0lIo7vu|%M&DMz8m2@?+qyhvi+Xw)UCbENa zbP=_tK(P+0F=# z!m4S)bp*ZM|TlkFi?0(RhK3Aw7n{_>1Vn>PE20lj5*r zDVNgmaTxreT)q!d50j+0_D8a6)EvQgSm-CQ(Pc%VaHx@=zKpM_7X#w!Bz1Kho!lGN zFC*h#ztMEKa1X2Pe=KpoI_cPGiU)_rfWBlQI|~c>#Eag>cJibx)8jkwb z4Ae+U;02==+fgwD_=fV9`nq=Ai4D$HWMI*bK1P&5bRI`F^d+*|7~(Bx&J??k+u)q$ zVN6*nh#;pa4*H|uDC^r0&m5TAgZl_HEaRKN`WwUC)-dH61c)oxJ1CnO8Y~<3rR#!< zJYo_!5KlF_4ka)Ws6;Xl)YVW>)JNQ!$|Wmjg7U6ct}j)$@I{EH80?ZxaxhG4 zzenvHOYs5nwDBh-YI#tW2IOz1aRaYYHcNrgmj-LV*8#nj)8c`CUDS%Rb8_%rdM!xy zZrZ393IvhJ7s!CnjY#4z-MD(;(gM%PJGyqz|3a^bPMpUlLqDv5mIfzO$_PfA@j)Vi zg^nWE7?+rc9u}-#Mv?i~sUFSGsYF8!rFBr6+Vqt*LAS|ZCEL>D(JjiO^N>1ElY*%c zcQamjWt|L@m=*OD{e=aBVZUHC!W$X&m~5DZmofbdU@<>K9LI3k$Qk9*veq zjbTQ*lenhQ0h3s2a^UEJdv5t@$d@UHMRce{j{|WuS%;(X02-hTEs7@Nev zLlK7PxfjhM#%Q!(=~R@VnQeI%UMHF0L?x`5x~=C5^RHn&MjD~38C{LF)u!XJ0B?d8 zuyvH`Rul81kw{c;R9qPt+bFG?-Of4qLcP5EIPN7a$vj>M6ob^a0Rl zyatn;D3NHnnFig!*;;$NhVqDeG}az?V%50CM)E?3NQMHQ4N7(c_T9{MO>_*uqWL#d z9Xd>p6N~CA>Ca$203-4wHsf(bR)*U22rbJ!jKQrcv@Y{C;2oX4DQO`Khx)J1Xg!7P z`D8_1MSvkr5n&K!^sZC{1XW~5jY*it9hM_FgMKiTOuQ-THR4&_jw0C7C%2sIQLQK@bkQnHk61Zr!rI!oxJV0LetjN$gWem5Yfd_nIkgN>q0X=GjDi^7sks z*>yDafP>H}F1vw0SO>2Y=DKO}62)2weN}*#1#f<%S)t+bs4zDlfz;X;oJf4eiNUAI4hm`W%f7Vgr9etYrf-;RB_Wi{ zGZ-36f6Q`lO`w%O09EuB!xfKDvwMJPI*gS2OwYwdBdwC?!;;fX8lota+^pm7AcY)E zbwX7aG9U0&E7Z{11vMRp#vi~b3t)%cYf2;2KS2)*rgWOr@(NTTHs$;^ojJ(XjyZ)P zIe)+30Eh#dPWw^s$|b8YEJ|?Rw^e{et=<6{hm#z|h>!+T zpLp9bkYn-YNJK$Xf+_H_VO~&m5e3X2ty#CyNnI9^l)NfM7tJ;3kLe|{XuxE-7@#1L zEG=5(NP@*n+8~|4Ty`%1WNORUak%Xdr@bbsu6PyIX=hp6neJRZqKP#2aN$0{0qlb+ z42L%37-(m#N4m&$gAguEp*G<%6fM~rD?ch}|GUu86C@pBShIdOj2Ocz5fc>1vfEX} zq_|FgqG{D37PvH2wFASP@(>v9B#)i( z5xvr-L-3k3Q7GrTTJbWDJEy8+zAR5?A+xCg9S`q?g(!fGYGNNM3Gfskf#68e z5}jz$9~~3d5tQqWQ2P!5>uHUuom@Z@h_4gFS$Y9S_APJ9Sj7dJEBthAk{4rK*cPp| zV<%2JBFc$EU^atM!Z`U3xyCbNz>uD2f5RE52t`=jDsCQzcQ}E7EQ-7CZKB-Llp3Z%;q-{Wp#sT{>qY z`~rh*XYSjpZ=sO`9>=B`c~**y#oEgjYG(kG1iR30%pW$BXvKctO{!QQJ&e5a`OJJZ z>|28sTgQZ>3y+l4EYMkE9cl2Aj6rV5J!qDCA%65)AwJk6^3_~elApC9mILz=8&Q0G zqBXG}gE|CMOEHau3Kx?HDo~bs7~uF&`E+)YmZnznrtE`$0T7V1H4UpAZ0MLec*Mo{ z8!5Cr#>txNwRS)q?ve})Q>m0r^wggKB-JfigixB^pfv1t4%x+XonApstYR7-wqfUd3*POWXQB!X6Rg&=Xd{Vl+PM#DW>Uqc z6Lk7j5~rLpd+MRaB6;wHF9if(4kTpaH^44lOtH0rzqOVFqQqU0IW&;);zx{Q~1 zv2~`=4ecG44oe1Z84PNpj39a#*=Fn9LB~-b=xeMdZ-9y{@gUfz0a2<&W~-1JL9!sB zOn#HQh4Zbigjtk^P-IQfOZ-$TAyIgmSbl2+@!yHhQYIIqkbtg4Va&(3e@ zKg=|xj4QwN!qcLL0y0CY>rvj)w4??+RXBp?FDMck2TuSsvacEcnYN*;5hrMIeKG{v zNnnx;V&QsN{5Mf>*Jo0`>43Wt?P*cTp?Cx9WUnghh9}1y)OhNod8;LJUKC2jh*~n> z1EAQN#D}|Y{=?mS+-!zDd+wAoSJtiH;7CN!aY8s~#nk{I4@MNk2m0<9=qz}J_0Z$8+>IgSL%4c7(U|yWtii(wUb!ee< z(+3E1)z)K872L9l#{rc~JA$94 z6PvOhgCqqXlM<>p)@P(>tdU*@0uUWiiaTvo+)n&5M^g8qm}UOgqQ4+VsLb!DP@%*1Jkv_CD}7j36JMJbzu5DXH=dy z(0Jq>@9+jsN7g<2+;g|ze!Gg_fBO3lTeip3(R6cpU-fKa9e>7`+->nqGu@~SD$mdf z1ejiouGWytV3^>@C@PbjH1aLZL%IAvkMP;}KG$BwIteR05@-RR!eGis}ZW32zkZJ=P$%y9Hm=8Uzbm&nJ-^cnxui zZqvX(8_dAuN-C^1T0rVx2_(NG)^GyAKz3saUyGcQq{^tF7W>e!dH9rg-nr@xQDBUD5hOJ_5FiL{H>%ZARdz@xQ9w?hW>a_+pr z20^t%3T}G1I)84m9trs%!OYU-PA6_FsIFnk3lw_J%wuUU&5k!EbjC|c3^biahXe;+ z#V~PzKKO&t$wDScTk&qB0Bvtlh;or#dZL*sVJrBwGdXwPZvod3(F|3HijRse> z*&`0aIa+Zrv)EWc(+jT`ZZ|mR7nztcC1E{h18Oi`@7PV{he=fuKHkpYTv6sejI=RW zZX9JS1)L21Se&ROg%ODPK$^*#cSpsddD0UFC_iOmP&CDz07@@Igv&bONENPMaI6W80d&bQ3_AZ-_ej5)&TM<(bu3p<*XV=#kujS#28(m7HharuIir_MOF zdD)yP-?GFZfM~F8p}N0Gdo;|}AkmUb7tb52UR~zxv(^R9%sO^;`6|Ps$O1dmGxHoA zOp+R%$V{n*8OQdxB_7hS6-guyUElPb?ZkmO{TzT%_;fOPgejIuaWP6A)-qN@x0hs| z-oeJ`h=(lzus~10W)f8C)L6(-e2W(^K|@Z04`WUvezE?=1%`zSB|)z`R!&C-=WW*~ zjo@G4e9rT>gMx`~7GfcV#qzy$Np@;xA)2^kAV|&B;1JFkVUj9j6^RL$UYh0_=)%r- z*EFO3pSZ095Grzch$vf=lBZAUB+vWyr>1)31s6tJzkT->Bw1k2nhitR9<6s z!N83OaV?iFE?GX~oK5SwQlT@4Z&|gbPmqTkO*uj}p57AFT z#sCD+OcDn}pg0o1=dK!TB$P3p|AlCdL8l|~pk9mq)VIG4N9T`wzY0tV zngd0ho&&~V@MEH839mLmt~)z2?zsJS%Lf6vY{}fKufAfz;s+mn)c!9~qSsyYT7_fB zH?3ROA-d2nY8hr=5N@)yiA6q-7f5c<9Y+*`yY4*9Ir{2el#as+P-H+-V_G0g^o)%m ziwcb8ttE$QKhN2VJM&%B3HY(8 z5cL2#3JA0s323T#z(?chZ<%tvuDF zj5HH$S^I#G=955bpfu7CJ_G*Mzgc4!5p-jL{=61sZ^>KFOJM1+E|?5{t6( zNqA9K4|F4pK-GT0rin7%{y62?bB!Y!c$o#wj@X4-j0w01Ch$SeKGYerjFE;A_+9>n zi3yTSE8GkDF-JEgV7BHD1aHvRTBDmMkv_nDW#t{GFt?M)N~9%CU(Xh!dG z>K#U;!vI15nFU_V83V$OTs7+Iq#t}tB#YjWPY7aN`G|FlOJFvV95{jkngRj~rPbD6 z5>HaXV8PDZH%P@v%?{2Qhl@y-TLUU8D#-JbV_1S134ksoj!c9#vcx(?3bL*@6hQ*G zb>`t!>GT%V+lo?QHm2E@ck2EDcu7-Zwx+%o%ls}@~)>rJN*pWJ)o^nJ@VZI+K)GxNqh53YQ6*BjsY!ZYi)-Q?i; z)2B{<@sA$oNX$p#x2M4$$`+%Ziod*_kCm(=$;YesH+U3HH z*eQ-tJL3R0mvmN{Qkg$z zSTh3W&YU%=d+zL6Zb?=DY`ozT4jeq-J<~F3mz;<;wv1(k^@;>3P57rfvBxf~{4`mF zR{}aVeM<{X5fW%6z(9+ZgG5P$|Bz5WiFGjdcp*C0$5k!B!K=XX)2%4qvU%&Fg9m>7 zSN`tZZ7 z7DNLlXtM(uoB*;Lf8jlM-sxRoukYFOna}>gi!Z;72wOI9e*dHIbzZ->li3;$5ifHr z&CiXEvnVx$LN#?`UN{CJWb|Yo%vp~S2r>@KkCNF1H0vSaD~n)W`DbdcB%*P2b_&4> zTjW(9?TK8R8fyUJ2a-=>Szx3n0GH`pg0d zHK0NKVTF7Zk!UYP$w-Du+EbfAOBmTGgoJ1B6i!O_7aH~OUBcCCexsQTcY~V&{90U;~66q0L9cN;b7I4O# zrqRWv&Lg+f!3&5p7uJrTUUcJ6Or7;IqQwOG?4>hKdht5NBDu1jsg*|Ig)&~JO`pfX zr{f9#C7c+hNl)4B^a_R1IF75An2yCT$eAR~v))OdRgt(UJ{z;%AWnLOH)Ws)Z)z9= z!s-IK1+;%Q+genL1(QIyJ3fCZz^dT*00Aehn#l?ZuJzbq@G!Y%7H8H0*Ri9#=rl6W zDEVj~mn!pnm5+M3v}7Tdyofzkn1}=k(>D3D;ulE^+lG-Vg#ByDWAZ0#DlHWS#d$ud zM~wg;HE|86l>sNzJt7l5Y>mZ~GZ2L>EG|V_!RlzAZPHM*D7^DKu1C)yuOO?tVhVfI zZT0`b3h}bRg9kAI5=o5juKCC+&_cmY&=I>7JN4;;+7wB*2x?r_Yua9~YKd|>W|N?H zQ)?>5eK6L-u+*bj?PkNstSZPh0nGl^PPVKBj4@LRHloyM>Zcj8qRd()HUUAkBv|Co z2#_uobEVEGN0jBG^oD<$D;GV(kKUC^fK2O{mfQ(qASMX zeyK`gIGC8Qu}{F4wi@$*7Zznjh?cO7S8&n#LX z2W`yPUc*_vNHtwDioY~oD9wf=iQfSrG%q=|NQmTaH?;#vF26K`*G^jl0Vi%Gk)}o^ z=UOYk*$<02*!$AAUfsBL`&#c|oPXQ*zw_j-{UTIx*pEEsX2Q_Az+|nssli-E2Sm8<(O`WZpAev!0s`>yuE5d{ zNU9PPT&HviSMunc9^h0#r~UC!lY!1@SQ9QJ+~@A+A?cFl@atN!FGUy@RMDm?p3Gh~kk}C~mz#uAd8y!u(^v3^^O9_bZt!#3U41(Evwe zGL=X#0Evpub=E^lG{ni_9;A2Ap52l|AN=4C9XN2{`DdQJ^Nu?{@rh49{`liwi}}MJ z{)m^}?B2awc;p1yFMat-d-v?wvUv+JBD$sy0c`$9kSck(#tY3(pR#h!Td>wG2_=ny&x^MqpDE|IuKKt@3 zyWaK4I~aj)e(TBK{oPOh#E<{kANs(fuf4u|_ujn>N5**=9rY8TTpRQo9#DIk4X7tp zjO-Ydp@<$)3R@E&!pWWw5sX~LF%jH-4f$;vCyjDPmZDI`WvZ_cVt}MBxW=S`VznL@ zJghO9bn-X}86lLcRZ$W?^UN7cU?~s!?9{OGgGGVuE2LD^n&t;LgV?p3BSRBgsW6!X z)r8F0x?XUj3@Mc>r7H$T+R)JmI1*#M!4+VdZC%uuJaq>y9s^ORDgrb>*A-}Me;f%5 zc&-yAfl^8$WVu^p8quoyBWk(W+p&l7pHy-yy@G07hDnlSK zGFZT@pu}$<-Yd1l+{U&*^f+rAGmCZ#XqBG?5SZ@&CmAg5j+;6uH5HC-AWYrN*voXD z&yL3RnCqAx249l}xytgfU=6G#i}@jbFZELePDDjhQ)}ViZdA`MgOf~p#M2QbV%%_3 z=FlX9r6*=Clo(Mpqo8$tcX~qNS~JQ+9r;L4-wXUz$-($kw<;9?2Yb=YdMsK=EXUM? zEBEDHs!kw~bi@z@Za7M97s;s+=Ufa@fJgHTMG2(BExE2?#OpcK)kS`89z0>4lmw<2 zl5`5>+)=b=Z42-H_5zMD(u-!ZxmwCsNthw54^EDFD2o6X7HIq-Phe)qLwT{Ox1ooF z8aXYEn_vLaK(m2D#2f_hi@*UNUM@7NpJI-vDzzqTg@&1`5nvI`*84|BT*V8DG4T>C zuz`qKT(I@wf`uj5z?#fW0l zQlw6}rRnSiq1Hta9;6f4mZc9~PXvdr4vCh$L$YTk(Bex4K&P)s-Xd?A`Q{E-0-Qn# z<>m?MfZj<8MPcK1%6{?qc#ZCycXwPqcWlGc-`%tS!0ENyAC#;TC0|>y>EM-j&TjLn zvh`#Dz#BJh{K&^16JFf^&;wH>&19cGer(mMwPHs!(Y|@ADGUN`;u8;|ASv|&cel`n zz`&CWk1}J@&HXwl`Xd9a#}CjLSm#qiSr9Lbu|jJ}o#{RePE!pcw*9ND|51-q3;@O6|rNFgW&i zMESEzRh)VB(GPs?GrwmF#o%Q5s^wPCc>9`APpBaf;Y6%KJbLWNvrj+s?)SdGQ`1tw z%>UwfofLzlh<;7JuMSz5$x;a=Gd11JvI|L(fRbcO2SdyF4%LVhDk2NzLJ7>Kk_!<9 zg-6)ocaT%o&bFY2`mHY&&01O=iBLSxx^?TeY}xv)C%<**(7{`F?(}kN0I>R~p}vxY zgf0+bV;{p=>TCf35_wUbn9^b|Wy}Na-?!guA~$W=KoD^RVbwPf5?(Kzn>C>j<7j31 zc=MzSub$70w|e=~#XbA>z4+2AAARh@?|bjN!RfW3|K{KP_RFul@{te!5cVB7c#s7s z5Gq;QA}16e$yvLR5Sql|hG_UQVkYP-IiiPjgH-2km{~d!4Sf$vJGYt{6eq)DB;>P;u4s6?1Mxjc{b!FF9wSa z4TMsej;k#LjcC_ea|%NKcd7Q$$8w*!%SH3r!tYKvf~2J#DwAH=GB#uCoMD85GSat~ z&pXGuvppM$Ube}`y2)b1f=Ee6uUtbr0^INf zPX`FdV6}9K;+LYp>zQ6Txzy`!7+VCSvU$D53j;OIVESb*cH=^Mz{Q3*TEj@EIE`M| z78VCV3bJc(B<~qdeQ2w3sodCb4O!eAdCUA6{;~n)1XC@#)od3ck7S&7bgR;^XOq;C zaE@a^2NkNGe6ZzX0mfpynwdFuUied%%h|@UB+_g3Pfrm}4?GEn5hUpuinSy(HpDAT zPGDV$k%x0jhKejoab;miq7=y(z16V~p#2_VT3p{X$U0p!P|#d;Q!o>Ok~k=J%$h)m z?Sl@DebX{#Ihhi{O_Gv_&>>ljcye^YkzDLBr_ie@WhNy98cFiOxFjES2NATWtqOya zN6d7kQ4wf#GYNQ#?fo$fX7Pj&FskUN>Ej$(X14nhnB&vqB6olm4k$i_Zo2jjC8-!3 z!3}=BkKsRXqv}Z3iCH_W8gYkb&JURxLf%MVVW3}Q=#M^>7bSJ*f(U2CM=%NoxcJ3V z#>zpEZg#s5I@bZj#lA&LHSK9~oqdloYyrW=Q4OW~uFC}`7!ZvO<2nX}b6Qa#0a~dw zR!HN;2OT`-GtnP;4K46j%aOfI%%Wv0R>j=05S!kfJ9C2lT0FZ{3)Ld}%as zBxlaTPa84XH2~T6C!hTGSHJp%6_f`LAAZ|g-}=clx3rxm%6b+;PW$Wl69F72h z$(2jfqWes<9<{6G7~YZ~rCj-kYdSVDoVA8y^42;{bXhRFK$eaJU2Hdo73|ACC{`!V1&+I$7{My#HSbcwa!A;K{n7?Gz1KYOWQmRY#Vs?Q@{nXCkH7NEKA`4 zOo*vTx&%az@W_|JrozVtwS{7djjt6@c+5X44RIA;d-b)g+qS*%+_QJzbN8mrTb3_h zp(7tp5e0uwo<7BqKk=2X+WJz1>!xfNu!B= zw}?a(8@$G{WIlzZl~d?$R_LD$t;F&=%cIAR0$f7tl~-QA`Ieg*5Sjv>r=R|wJ&bqWeRuPu z@I^MbC4@IxLfZ2eF1-7lkKB68El+*-yD-|id6QsQc46a&^-gs&`;1e&_wH+@R#UM? zp4d~mNL<_7Y%`__Lu8HDH-^n|El~Tw8>x8{CX3{<1Ia@Jw&AdV-lTR`cj#bF6ttca zw5ZYez1UDkn#>N5bS4~G$xLW441aH^qw$G_{_5g3c2E+ zzWMHZ-q^e6;TW?*pX(MP>ELnQw)z=Qb`Wn%jB3=a|lg38t zcfI$$r}pkU_{uAdUIdr+GOt>(<*vK5IB0OqI){r#j_!T#*$9Lb)~+p_w{GW-Q9$r9 zMnS`RVL=c#UIiCZEXK7I=<-ra?N%(iaO90cuf7g8$+^`1+Vy2ywyxg3&E($6UAvDT zIShAev{&7{UFKJX<%?&p5SL@ekn#K(69rQ^dueXjvc>AGS#2C96cuDSLQ{uVu6yP5 zrA7RpcPU)FVDM8mtGKr0LZmaqO&!`E!IGi@im8k0tX)P$l4u-z8F6rN@XM-YO5E&Y zBh;*g>Z)ontxokh=!V~RGdd2@VyhuN2DIa4#D&O1MI$xypg*bO0#0}Vj`!{^Nh z%|(nRwBnz>>>|{wG4GiXkm}fiFU>7&(Rs+Y(~MmB6mF=+sW{d_#t=Jnk@=pCOW`w< z3Zo3MzoO3JZdsVz)jaRL$?25JBIHq_@p>b?Qd#O6bn39Ad;XPt!<^g;6ZkEtA{4UP zR)vdt(hGcYIebn@jxMqt!vq&)WDX;ZtfhV!5XaKK0m;;#nBbz`7Rdh~XmS!t{Tiz*Mmi7EP#3epqT zm@^+1P2iVzA0e5-#1nzQf_h*dLCJ6f$&UNr(uaa-_f>^1W4Z1?7$HOz#R;i=gPY{a zD5~A*C~V&^83BTlAxE4R+beftA4=g}euqqXXHw!M(WM8sGJkonx`Q?CFf#t~SN{0s zn{E<1?A^QfU;NUq-g3*1_rCAlqsWvg9AFPKC`Q34;^!9hi#Z4ASt|bq-={LVkQJBO z%)^Z-hF;%X4xp4O=@IsXJVJS8ivn0IJyvtciN8hvh}3{1jrAD>j};i9j1D{8*RX!7 zpj~3tOFRNcZ1+P)W);Y%cxtnJhcEZ8$am+=&yn%zx?DcW&DK+3y_Lv;LNQ z?uXLGowqJuxsJf`rf(cS#>*z>tZ8Ey6zbu^)Zojl))J zz52>4+js0(w{Zgk(V9t6ld~b6z(P83`kv5WuSaAC`1597efgEsR*^4S`1VI0S+R2E znwE22edFj6uUk<7^tyA7jGYY?u&Tg7P-PX-#PJaK+;i_Yzx7S?hRc>N(;$L?Rp$w~CqZ`X z*wKCa_Pq05@5D2G$wJ+J`yH>m^2)&j2k=6)OYOnWsB{TQ%E{c!%vi+c-EFH^3G!y1 zdG^^gt5*m~pL_24gNF~j|9$T{boh-w{KDhw)@=ZlfIGsCY$880-2pC4(&%9>pf+gR zY?lU67!#FbN!-`+B*PKh$^j6JrwYh)Mr2mE*)c|=fJSY&ATs;ux5um^yCEqMF`*s( zM@})zeIxnGG#L_1=RPR!o#uOQ_b#3~HGhenCG#)7 zzW>4h>F=CA`o`D){{MJI-q_2nvB{J=EQa?q9-TRQaMO?d)C2$dr7BO~8@;-F^`HCHs-4@57;?&Gx6)DGAP1bxY$q;@HT;2|X%Q!9zem>o9s&Q4;esd;!Igc^0H237T|-Z@GkR9QHa zhW__l)5K=}Qp!~yLk_dG2DGiJ7B4x1)tNRGPeT90M>sM8RYUdX6Z`Tt?4t;=3}lWK0_9HJvBj3KdZ1W3XW_*X3sgD; zXwl4Kp7h-LGg6L%w?)nlfa8U;O7k}S3boIN2`i0VYN574PWcR&psTeJAZMlFLUr+= zlKIFkiev&yz5ak?KoSxhDXWkP~Ps&;n%ikqw((g|Yn*?*&xy;%QiL3(28v*uU+@M1o)Vj2?>e|&&1BR*1 zj2l7+ySj1_F11>gUTfmbxuuJ)n=h^r5y>DWutJt(T)u2((6I4n~^P`m=#G17>wNkU=RXxtoL#!q zkouLEUw;0DmxP}C_8qj#On$<;qTl}Qf4y|s%G-ACy#M}tZ@c~0yY9Th5nph0vc_XS z_Ngy^_7C3wp%0lVg$rPiD&pfBx(~(=5iS-41g7;U%QC{*m^evQ;^n$Q=QJ1ZhDVV; zDT0)!uC&C01-oD0J-1B8YWYpuckrM`j~;vS$uGa~!i(Sd#y4Jk@g>FYz3*N;z5l*@ zA9(PAwd>X{S+VTI@#C-W+I8^IAtSHlOP5(uv}WzvVFr_^bc7iHz{@5vZe2fp;^c}| zD-RqxXw$Urq;^bdpE+|zrsuog`=0eiU--h~>N8-U{N%^)zWc6+9(v2|x8EwfL$udk zebq}FU)#0o_S3@ng3S0tj3J+b$R^q-5)xuPz}#mOD%J<-kim78 zNf*Lmz3Tx(F%#PIb=hY|c&z(Q2&+KMWPmPz{cB(Q#@E00xBm9uS-E<(ZsW!D@fx++ z*^L`F$V(kNdUS4s{Nk7X_`rdKGG5O<_x$ZUciw)-ZG_hJ zWf>jV%3k}FXyi?rWFWptiBBd(5K`mCOdKbLZBk-^K&QxzJjGhcKW8{KQqmrpB8v~2 z09ygo`>y1qswqn6T~*huF;q}y}Qg{pg1o zouPnk-}n>-1Hb_dn>H_7zmC({{P5e)pFTCacAYM-Zr#o}F!Ieri-As_`rg0$w==8P zt=f49c&?s0wece#z4cx1I{N(cGb`4vy6tvON}ty-g$0;xO=LEI$Ia*V?>~0n;PTb0 z&mKE@^1%L8kG=cZ_eh&9zw=H}(wbXuH(9ap^0ntb^Z9*${Kt#d zukUDwjT=!6p>I%7eeVDCY1*`Cv8_U7tZy)gkG5ddrXxE*jkvG%eqBt)6P(isk-XAb ziNM8{#Ij3fzhTXFFDyTQ_RQi{tErFhaXfBv2^uM3!#uzn`Adk@B=H~*M!4!G>pX|p zuB`Qpaq#F;G5O}1WpfR5M^^!qlxS@LcjGHskDGC@p62z6L+df^k|&wwRCz)|Jyzsb z&jgRoOPZ0pHh0alSZ6V3j$xOt7!W`pJ1|;R0Wp%)5YAtbe(^6^Cl1IYH-lbz`4Q7F zD*;1TN(?39k+`=YUR!eRnFZ%x8q4@XXmGPE5U>Y2sth5EszXx;(%_hHe)&QxZ99il zucm}1V|T_eLYHS8K*s(KFD5E1(}k$h2Ozrfl@|?s9H^`8ljib@!9wmijfqrOtXyq~uT7l~ZdA(QI%m$YC;PQS#tt4jc+b7}Q{ig6 z!IECHP1h{9foL0Zj9tlG6Ap-E8IV=lAX@W8^$%a<%sqK0Ccruo|n z(EVgiW6l8@$7sncW4)kuG;a1BOk5rzigc+%6jLi9Dj#LrL8-~h9E+B76^myribn5Q z(lQ1ox=&GpVBF7jTfI^aqpqMv+H)QU9)AY4)*Nc^VL83+OFMu zo&C6N+jiLqp3;te0m|XS2eokU;GzHLpZ)@n9((MEfAmK_e($~au3NwUv5!CYwzt1+ z+t#htGZc3_4M8sxUHFl~zR=2TwLl?3j0&j?UV(YAtkj=d!vqYugapIlUJ*b+s!Evk z8^88z@BiQjO=vnyFz(-f;CKGt-}#ka`8AFEyzQa;Hf+Pt=rAhs;6kV1DRJUjRN>XJCYXvz!cG9j<1qN8Z3;HWNo!sLL>!j2#hXz_mly^L6!mK ztnWb|ACyxVxc~%DC~FrH6$2o*T}5fmW`u|?+BR>f@pu&mI$t0f5M>cLbaT6Q$5cR> z#q$^Tz3}3hr@nvfu5dHkBS3uGJT=Jp(T_WYh*vs<=1OAzQ@uzJm_`r^fc%P+sU z{;&Sk3q~8?e{#va4_N^va9*^2!;&>?#x#O6Jg+XETX_2Du@iPPtXtoCq(*LLzBX-S zwK}|Fh~U_CM(VCv|IkWyw=GMxQmS>UidQB`Vk$0NIKRjw`{ISi z_U^rE$=HR(i#BdpBYWwCzE0$k3 zML3wqStwY;3%o4+vKba;(~sdRc9pirGm6nxrxcLPYiVkZ8_DSqg+On;EOAgY^mql+ zrVN53SB3x*Fap;*cPBT5msC|+o#)`tXXahFa&^u;r5LNksBZGVX6o%d&u`Ujzw|=5 zZ-CInn;#-Tqsj7!5S!DGD=T+VwSglkH-5^@6Y;_M)Z^;k7D%AgN<|LVC7@+^{i)tK3JwgEdvOLjy7byXDXvUC zt$`+D`4^Pg_Ano~g>VpL4osf5JA>k+&eAPoFaQc2NhpwH#m4R*6^3&kp_FFDMGB9g zq3)8Y_TK3!)JGTN@t`bl{OE+P0i3EL6$GMCK+57?h_!m<8W+xAw)zBfaN*2p^21Ak zu)?_^zxxw{T1M{x?*0%1sz&j0x5vE(`An^8D)~bayGKqq`sr>(I;KCiUU3 zAtSsAau40ngdz-L<2%eOg*)UY#71g_x<->&T01Jmmu~q1Va-q>OZaV7dUN(&r}0rL z$Y*B$%{SkC|NZy>(I0$v=WTb`yZ+ki`~Q~@et_pVefpH#<)C_fm0g>fleBMcy@Ns^ zLNem$6yHZn5yOosJmUewW?SwN%^Qq|7etUY==OrZdJj=c@-w2zvssl6F3NwzP@e)h zMUxdas=xxJ`qra7Wg*<$QX4q(5Im~}MTLA)A>G({H|a>OlmAI%Tgw><5uqh$7tbBq zy>R&&3$q<(x9;|P-NdXXpM2`mpZ?5UcisK|M<3PBd5O65=LJ}7FW-Fj>=^@ADY>`3 z?JY8YkALASpa1+H{ru1W>?c3@iS_H(TkBBz$DGU6t18IxOp!t9%^{V^`QS?g7-kEF z2QmCgGHfa=xQwF-cd-ug$6f8&TwBFPw4UgJ<(}k_uw%&2aT`#}H zl{J|+BQ~1Wzf&snyi!@XA#gOAxM%n7`yaUf#K{v@vu)kFRccU1&8r&r@7s6e@S(f! zy^jFPbdn8m)sZ7d2+tjN-8HvlSpW=`feOge!UBDvokO#hUcI~qYD<^Q-hb~scinlX z)qVoC?|$$5Pk#F;WIuWOGx{U_; z#t~_$C_@9H1T2$aiia&5KF?Qynqosh0ZQaF;}FMObk1vdf>dxa)Tz@H5YzerOq0&} zr<~Dzv|4-Doe%%R|II#tqt8CG?};bioS0b#w_(GUPyJ`IG7wn1WpfMv9R{{~)uPqT zB_04^=(#gu9s8^IeEZ4gfA_cNwr&sGix+3tuD|JnAK18Un{3dL7hhOt8W&(Kl(<=>({`!!bys~dzU-QzSrV-v}D;_Rz$GT;WfQLBCkP! z=8HaI;1Cd&6N^>Ko7$x$P++2F=NtmKl8Qm!1JcxE>;JM%T8B$|s+;m7^l^k=?wHyj zQhs_-O+KOhaC|pl1u@LE-4++S(C&nKWEcU~M2{c*))ziIg}d;ngs#8cbu;aUG31aYn}| zpnT;9R4I1G;QIMvk^{~#(u|5#S76Xhv)xo&eb)gJ64R$nA*gT}K^+v|1cQ5{md9_L zIRE)?o_+sAt9NW%bmi1{FU(uk0XpPQ3>||MMnWN06K(F-QGJr0f6X#7v0j^%4-jr4$zUis&d~fsioBqb%`a8GX-m>L9n5HHhA>8lP zOa)TOPxRIRUL~(E=kzt&(!NqMK^p-qfowh0g7X(FkeB~!j*J0ncA+tDT#=*z|GBdl ztc+*<1zWx{bD@-d3SWB>Ri8)-0Q16OFleVA7sg~oX&hLCHeVa)Ye!XIF_u!8(sH=f zobY+19;v#R(d%Mnk3VzE#)D74dg=Iy`_^pSz}*VKzWC*@KK{k8NcKopIjrxP)mImL zSq%uSbK?BuRCH(Z$iY*7uiv;%&hHn0@t4iG{oK#}#kFfzU$|_G*|o3z$=AO2)hBE- zeBhx6JEkFqVtx8CNLP$bdM7hMDUhz;y}HCGleI}3AlpTTeBcA`|CL|*m4E(=zx+4; z`rrI#zwl253LCdkKA|HZA+HT&MsRuyL`jar7P~b`=0l_=e_TF=OcgRul@Dk_*cKVV#V^G z{_{U=AI3Z0^WJ@X_X<0-Orf-`qanh$H4r0A3l}U|xUi$V=tUw-Qu3Mr6J2%P!jbj_ zBuMU#XjSZdRf^%VDQp7JN@o%v+@=5|L0Bw;fdUqsKY!K&v5)=eCkm<-FZ9YbcpCqF z@P~egWVJ(W{=AQV{70ZHpJHLYzI0(xT@uS2%r)ix!;gJbEqs6QEf3xIzbu|f2&hihuzQT)Tr@Ph2Tc~3m|j3H~09f`VlVfih$-txA$TH*#G+FnR<rG8{hPnJVD(yZfbn41_Ms1Lwn}oz+-u+a-i1SlX18pu z&9i4#J@i(Ys=bE}&0o2CC>I#!t-krDrOTIJIC<*GGtVsEU=Ae7rhKb!yY1GuJuHZU zg_Y#jPM>|_d*5BOdX0)GqR6ssTX2~~*=L|rz#n<(Wt*ilLX6Ah%S*Oy^U_$-#-?Bo zT31adk%>dRwAeMoa^gk}HVvLXcl`C;W!DCq5Hl{>_p@y2xnsxAyz;8J#yEBH>Q%O1 zTseFC?7scpEufWpymWEngAbU|KJ>*e&0n#6V)~37CL7oH#=LsZR%mk=MGE5G5A7%y zEKm=iWllzH)HckLyiy+kxN_m#=_Fw=l90!u-nO^o>LmsoE*T)!f!NSRQUqm7)}>qM znnm?!SU8nijq24N?uW|inhUkiaM4%;MgI5zwTfDj(|##G0<7`9TT>{cy>_u)>O|rZ zN&b#{%dC{;G6Y-D7BmZBV?}ZXDMH(9u^`bwnnl zi$!h>Cvr1plZs(yYi8_?_YnSU2|CWddT!DD%cm~PKELn613Q;Kc*pV((V)`tKJ6Jq zOb*nKW}vLYVbE#tP(@-N|I1fdNux+nTAntb`egPaLqEi{SOu^?Ir17zu?qq+b+ON^ zfCs~~ka4}}V!+c<6+FzbL$CSWdaj$%4sMJm*^ANNP#>rcsNd{WX?1SE*3|%IYG%OL zoUxVZfl;&i@Pq#NJ#+T_ye~X;^1$)yx9wcLcy`@^v!_?Dli`mx!bQXkZRKRPdxw4v7X;+?pbx39rx^wCf_4pz3rykm93#4SN<2_^^k08>+QUCr-fS^H*c1Gk(-mDNo2-{DCq@>>vn=wj%4P#bt8Ptn)U0k^6O80 z{d1rFgWGPq^=E$ku{-a)MHNx(f=d@1Ax~t$xq8K#5B<;w|L4E^-~SJP_wWC+fBFk= zfBQS$`qsCt-?;IN?C7aeD^^3qhB_G@ z!r3yCVb9$!bgs>205}g98q<`vGfl(cB}v}(xn|A64eROe5XM;3s{F96Q&Elb+VXxH z{b*;rOE!x%2tsJRGzlbsOlVE5ephv7Z_Ov3J=pQw4t;a)j{2{R# z58pB8T#hQo57`1updwH(Mevbaxgnj}H)t39ITqQ}UwgmD9lghr3`-n~Wu}Le)MV$t zQ~m~%waI@DViKX2z+JxT6`XTtY_E9w*#*l+`YJfCuexamv*ZOAFd{RUL4Q}QUAtuC zrgP6fYrh^t!O34eapLK(eeL9~*OuY^gOD2+#56(o_*uk#pA~u1V3-Zie(#8v=fJq zp4+=`@utl{Bwhwxs&1MDSpn%T|Nz5cST83iRM03cDEY8s?Deam2F)rv!hH~~10DJFJhqA`m!N&|vlya3hXhMvI8%k*_2J#VQcLre9sS zO2FX&t@OH2A#J}>rGB+@aQ>>*%QvpQa_00oQ;-WS62HDpnm|OW4w6LA&Nx7gF^8Ld zuSbLna!DhyHH?=&H7QPq*F@t4S=Z6<2Y|(EANTxnJ0fe&931?I`*A;H!a5*FF=3|T zQX*#!VFyG8{RS3|goefLnLCg~5Ua2S2zGK+Mt4)siqvM`#3z?8d1L8`(c5UcQVE9E z=(%KwxfR=e2QOkHs#5|1##9Wt7!VyDndCb=sV~;6gQy46kjaX{${sS+(^4g1g z_ntp<>h!s@5?QMkExLKj7Ax3H4@qL7(1{Z#Z@qQrzyJ4txPAN9pZuwx*uH(+&;QN8 z^4v4e%8L}Rf;dtSt_4l-yF*%H-3Zxz#f1r1I-6A7;BZ2#Mn+Iv*KXRr<=u}w;0=;zPM=-I-xF79#mp#Qx#-lHGd486=aGk>{Lc43`NmYQk?00|7sGR1=1s{hGANj3+^IJfD|GVzLdB%V=3eI-rD(Op`f73_JO}pM)sr;rKaR^0PEE9Ws;ljcF z`%fG{siR4j5Oiuh>iYPSc6Zao51xXg=G|A%{UNDSiK(OMR{V`_d7PRX4}U=TAhHwGHb zM!6FOrA)xk(F|!>MqvzPwNdq66n3LQC^C#dRwjJMJH$XMe^&Nx2%8f1NOx*Fla@W~ z_sCsgU`|BYiUWugDUh2USw>TZ)>j?ayYJlI*Jroil)5?{ciYw#>({A(HLMI$B>yFs z7cDnt_^yFaj1a`U^p!84*t2(b(-ys7J#%`~2R^j(;fLjC0E9q$zqxhW3Ez0}g{BmJ z*xuD^)@|K-=HS7LZyd4Es`?i%EW7>AWouVEUij!MuM(Fjj!=5>+RdxiuNS2`r^?Ay z0;yxKzvgB2=#Wvrcy7rZx5*X7{V~@mL^!^G|HUK!A7^g@ANO_L_hOsD>@;rNw6XJ?H1TWfc$d7$)@q58 zNJ*5qF91kl-!X&13@|$|0Q&viKPb!T`+S=8PY{^-|L_0abI&>VoO91T_uOb@eWUP@ z&pR3${nZgdFgU1{1G*#wX%QeXpiO#b2xsP^_(YK5AV!CqPEAZMr&111$uy)@ z@BxL7r$w?zaUC`cpvB&Sk&==LL0Pi7q@=o}#M#qi?v;zDM2eDd*M{ujUV@FR7#HEf%l zi^a~ocIxFX{CQ({iziT(ULtZ+UQ-QOwsevHhj(Rd7?Z^;`583P5(PEeW9(6sk;Z%m zc4Y9F>>3v|-Ae2(zCbx9#32~AAJnp2hL z3)r~|cx0}p`H2w`ph;`9(An51xXG}88Hc&75}L_2fDT5LQ3*TQ=@v}@q~7NVT2O&W zI`J3$YR|j2kgGo6QFr9+V6z4>Cn`u^hGYGjJTNhpC4v~(?0su=V7vghqCL{SA>^8e%Yv(D* zln)LQSDdJUg4h56+C|A38ZoY9Wl2WRH<+7NY1vG;c|=1X6M%WdUxN(Y6!?NrCZ2Ey z)KG@X%{!E^Rd6POiB~m3h?;~eNjT6Y7FV6j1ON^e1Q?Zo9Rw>Mc}iK%Z6F2W4}S3D z+1MNnaz_u6JHGx%$WxkJ?c$3>0!A z5VB};a-z4lZ^s>5_w3yd%`@o*`hbJl3Oky#Mds0TLE=Xa>!5@f3f>698m{ST& z)^=v#0YYpJ>cK$KRLz`HECCCCn3$VoRti`(iL(5=ebI|`^F`F91*9gwKP7zcFJa#rykYC_$Y&=Bt! z;ct3ME9M?G7?}V>)MdbCbTCR4&ZW_6tW1drdEIDG7%-D+ss#kYEYJw~svs~?K{H-N z5KevdlRL;zP=;UfP_$u;yPy9Hy@2AuUx2XL7lH*d-*qSt*WlR{^K9BkpYz?X;>(DTU&y} zq~{B&VXpI7-uy$;=N7R(J`h5-Px|x1qs_FVjd8V5Wxn z@JNY{R52Gbx|#^7A1q-)H8@ixe8C>R0dOOG%0(wCOzK8%vZ&9lf#mRTx!*71nM=*H z$-wR6nCO&N1Ienh!d5}fX(l{>ffYj^lO?eSSa!EGdICP*o}EM|KpoT#tyrvXCEQ|? zQ!FgoosKd&?8yKiZL&lL)f_>w(oMhs-5~^T85Y$b!#T!oL*Bxb7tUFkcu?L9wC$#Zw|*c+?<~O@UML0-=26lQeCZliZe5>zWmaE`|@9Js131w zxFjz}DKXdt@rfXL#2Gn3)_l%VEND*rvW6d+_*nzX3<`cFS4t*z+%%y{FrzM%Vsc@j zzOLr;pZ(mP-FvJ+*RHNE9^G^7_~8dn{MqmRo8rVRM?)u$CS|5#ccJF8tHcjmNC)HU zl>TUbrm>;n$3-2MlGZrkE3`y1nqC9E^1uQE#uZ3qJH2g{98@`8G7KaBm+>i^zIgiG zkt_YPUN;j>s$vXRq*M8$cXe#qP<`Rb#OZURI4s2=+8xO&j2!w0L02)r_=utN=juSc7<012P>;$q?0UF};o)m^wcdHU@!A`lRaupA`8Zfk1@Jaly9_*DAY zmwJI8`3?puZDpM8(O4Hec%Y|$G=FO(*5pawbEp%se(n0iYwwH^ulCT{>^7V_sWO`w|arvp8x!_!%5E8V-RIh7{$-?Y=~ zLiO+}h+BlRM$1K#`Y0bJoULAPXt0CWh-Km+!2!L7h51dDh;a&#xQ5ZL0>=D8 zRKaQpV(eSZ`A`T5cun|#P@YEKm3XH~Ch6uWgoMYzqct%F6(T4kC7_TJGE}5v5Cl*G zi$SE&gq5M7gav#y>>!TgU2e*+X0qp-JKPt|r93buiYsrwefB%wdvW*9&FDxTY4Gc} z%HIB9zOCPR@3TJ8muPhEM?ZePuKuGY_cyw<63o&;Axr=m2xJEcC@uknmXkJ83bc3L zIrq%>U);KNqd~z>D;Lk8pJk^?dr_Fz0Na%K2Y_ifa7(>ihyQ2lfh?I!_FLb6rlEn8 zP9jUF9BPvQQm~H9=u#z;VCn;SOuKo61i|v~k_bw7N-tlzJ~A?X&poXRiwn3D2&GzB zTzuutH!JPszxxZ1w>Q@13d^IT8u{_B?qZrv>A z^Ei;J0$R+vc=7VXCm&|gLcIK9VqtiAh}06Kg9jo2GqC-X1gwp+w8xrhIKu3d^aO*{ zfHB(G(#rbB_+o}>3Cvj@2yk3E6P1~%(aV?4EzTz}6kL@Rd-rUA=iSQ$#S>rzL$VY* zGd;yZAwlI}3A7klZ!kl>7Dv&HM4YT6rgg$1g=9j>Zq^t=?jikjUrF+iZr~7JwF(NY zypG<)66H9*Y>vDVw1ptWssj~ozjJl(-fawF7~HhUFKb-rRmSVNb644$$yC3&DSYnS z6|(koOl4;~XXcbA5~*lxF_T%|x}}kgV&LlX)zRg`Y<+!OV`DWSNCOGlGE$Sv7&ED2 z8SRji1!Y?DcsSpk*|hk zWORrI2wh0@UuCKNR@cFC?usGRgfX$uFRyH=a+;cbt6Ve1JX;xNZ!^3Mfogc5l z91}^_`)k)`uU)f|cLTukc{}2OfR8|6abz^jK2VNfHr!H15~4yb-hYEiJoPn^mg?E@ zsZ`&U%BmW)045KfB!6A4n{X?ZI9Z0PD9a@m>&;`)EVcJ9-6eX ztxh2GgP=Cj50}9b3wBg1c7Q)9!9Oz~s9YjJnnbh!&zdZPAC4}V8nLzY{P}jd5J_f@E-GWF;scQ>t^hst-~9Rq2A07K zzwpL*OnwU;M#xL$4BL!s5Un_6`M&> z)a{W81>Q+!@lM}#&{_Gwy&Gm{Q$sVU>PAl{1;ZE9G?!hzKCrlLE7KqiFvt%EMy8U? zB9V;v&#Hnh1^~;z^Ym*lF)^F9CyCL)Xw-3p42`A+zeh;~kzX=mAh_K@k~+o|jG%}m zs4wOQ@9@2d0XcY1&m>|bsKh6V1BICpCB<@Fuco}biH3pKSxd~xtetui^KmSF%p|`8 zY|A*#jOj890SrClRWKOJ=a<;Bgv;0Mwt1XZ0FagsporV!WtM?*r8+{?c^APO2v(7> zB%MtG9YR^mU<9RBl@abM%h9=*$7M&cVLI>#xC~dDj#Up=nx2lLE!h0U zQikrzXoTC5Bf^Qrjl>veG;%RSv1womSbBAZRM+SsqD)NdRx4l?uOzM}VhQ5mkY6%g znQKgvEfqoP8jH$+3dk~`&1Jn$f!iPS7cwBzXQFe-#l;%ZVPl`l+au1;5tW6}VPc$d zr)es;TF}{+v%^_e$PujJ=nNM4-gx6p(kq~S*#+iux|s5vyL<)9{;{L?G}Mq8tJLFl zw>3AESCa0k1~_}Vx;@}OG5_2vuaZ<0Hz5=R4`y3E>{k!)gnEq30F~`BW0)&^*uW^dGYMq z2GT@Uc{-a6qlOgH2xLHTnzVo23SE zs2EJEKT9t>{~~!oj~_qU*4B=Q2Kl*MfnmfBP*6vm@4x6U@Of$azjqguR#UFY29(<*Xsr27|h(2!r?$)--VBT?8A)>b$|FrpYQDKVqDR8V1NGw*bD;h5Vpky zQ*oIBDhK@N1f-KCe0abQxY9igB~4e5Y;2ondd4ao{$n_ONda0$zknTHWC}TT3fB!vj=||>`eND<><7lZ3Cy7 zf({Z!OuzWj_}9K#=5p@%y+5dLFVD+93gKSb2^OKXs{F2?P)JcY{ z`lUy9TRIlQT?Y;9jrGKRA_0;U(^v>#Nri=;wb+|yWAF^Q)FTmhbAz{~iO^YD2&OKU zJ5aM6>BIm+?=$+~7qW=KHV!;*_o-M++v0=V*-|A&5{$3P@zW80*%=49~f8`n6L%Sndt(hcoAL$ z-S-{g0I3j6z;OpVckiB@nE3tQ|NXn}x@+IQeQe}LJRN=b#NXVw7J(?_hY%l#Q&CoV zSyXC}PC_0~$~Riz4uc9b0SR~)>Lg7xoVY7X*+?D+gmI56%ol1@I)a`M$Fk<6{po=w z6ygKg+P1OAU1_VY@j0CK{#&!(d8x0viODBhV3vD2LN2GRxz_LXx#s3l-+cD+z-(&& z=K5oIcHmOF)IWc=FLTEoTiuaBdSY-#cTK?SXsY+SoE7~;bKm;mwf1@sydJ7@ee8*y z5RMRn*%*niv)F3%p}Vt&aaU6vtZk^M4Y>j?%!|TvuM?9&vU?J(*}57$Ai+~9&5fjm z29coC=dw%Yt(5HOuEp!tQ0>JhKRg=$=JVHE>YNxd8q6|=m|c}uhuurdt4$3799?tK z%*4zh+KTuyd|4c!MohvIQ&HH3tqqmsiP`B@zuV!~@-u7%zQLz_L8L$Tgwm_bjUWMB ziqOC(iELCDMz)Mt&`)uANUtkAZ*7HTekA5=r(P#9BTU=V({V)eB#o@iw~cCkj-j8{?T(Bi^iPe@V$3ewmL`|fS7~icwF}%IyjY> zeC^aJvKfIK_KqDpc3*li^YTkCp@TU_ZTG(2t<6Sh!Hb$MuuoRL!+3lFwTezvAO0y9 zGEV4aIbLft^oa2WEDE>5(Pq#k%fVU^q+eoG+3{H8wn7CS9by&oFcJq1zt=iNFeYx^ z8V`rO_uRc}bYubrMo{+1=%ixtHccPC!X`#-K!=b*x~PH9I$MJaFjFst}pw#Y!}~-lG19dH4@=X9+TY)YB2?!L-e5LxOOAbdF*!MfTD33?lEDGQWXuwzL0Dm~ zzyhv9u(2J9vZsmm_I7e$s%E@t%ko`;KnlPX&vSXPA6%nro+a2L)3_; zwj>g?Z}Qtk;uNA{gN0 zwZ!NsCzYx&7v=CaFQu%H`DiS8?tIM$PS*Bxt4Ff)UGF!C@+BFFIHicl@#n~(F9Y56H>{Zs^KZk%6NqR7?!XYPK#7=?M5Q|A5T0S zV(r!J__^!uyN8B`EIViK-o4~E!PE4_Q$Ig&>juV*@n5JV86lv>82M&sb8~$JnrL`^ zHJ9Mxk|UpalH`KM|BBynZ1uK9jMREg#uki|N*XYvzl8cQ%*K|MJgo-?yttB6PW! z%@U&q8IMnvRN4L=&9#l{w zA#NnJk*CIYw}M75w$=y83i;aUfyH#e=T){hog^x%!BxTrd(>E(^i_FT=#;ep=EX=F z2$~o(7%JUm`Eo`JHiJ+Dmx`)bX3EC{k|jalH^h=GY>QKkDnN2;7@NA}S)o>VlQS_q zw{F~e?%8LzZr_TPaQ@uI#>U22EIKtch5sWOjZREVa_mO$_5K?-`g?l1M@L4lT)FP? z_($13<8m{fzIfq670He(Y^33CZf!dpW5(m<8G(iFr934YTdg&Cvz#Q2&{^GwK-K@2{KLWCUfn?V}9 zilEVa_;%~EuEr-~58i+91CKqpu$W40Ns{?Y=`*DNo}HbK&83^0LseD&^XD&- zFr5{d4?K2!em?nkU;W0$jXi820r|l7UPlbV6;c4=(J^#fMz?-R+<*ljM!zF)0GsXyl;J=WYxQ-^&tMT_Z?l=x7airJ}i^(HBurIhDp{ zr-yIdWCPUBja}b=^-Mh{$QrqfnS}**^~$yAu^764Y;;sLFOjz$m82uBz!EX2+yppY zp?#M2Ft9jNW!KIfEW=HVPjWoVu3fuGa0PN(TUyA;PF&a4&E3e?%;JK>&1otGOE|Sp zP)l-^J9h54c;R9uzubV2S<(atUP;Hstvx$;FGeS_3-j}fNqCHveaa`3EN|tuY}rDP zKKUyE1VzFRd@k5S(2TjcRvViBaG@E9$Kqkzx9{lHzD zcJBY9fAL}~-2T0}M^v&5%qlcu!<2EWj_rsiTl z3n*zW5mOYEx&m50!B$3sGPe}3OYe&l#gTl#y!Lnq6}L08;2EY#VpwZv0F5p+t3Tov zRmf+~T7LFN#O1%j=bwA>B-YibQ?H*oYAq1B3a!_ z+S{7nIDPisdkBij-|poeu{!|UcfEn;X>0}TbpqGZzvS|9?| z%x9QWrlW06q72^_*pE^aJ zgec@H~q$q^XJc#iFa&#{KSb9J9q7U z=j@rPaJA_L>>!>2ROqg(p$3_<=+ICGuT*IMnqS;zUcX#G$AbaU9R$0aZmg*B@o^gd z!WX{qE5Gt9q@;tn+1EWfIyyNu+1=gE3K0fm-sU1uigf^L$78l^mZoTcuP`RBfJ~}~ zJRrn$6`WY6Bya2B5qMr0vcjSfg>tLN>53`M8--kQerhV4CJuvL#|E?1%K)*JRfnCa zrRBFSj&scW*mTn6wh^dcq1NM*?-1`qsm)VTW+L&~5Fw``WJAv|oLxh`7ZYRcZinNs zhmN>y%jbKOBY3dvV8Mg-*;aeNEV7)+X4?9Y;SJxr8sgnnQ?i_ zogP-VQ4)6e#?5cmNi{zc_Pss~{jx88>oSfx;x=6#1hKFSw%cl43;CtDE{^eTY(Aqf zKC~%_r`u*cLq!sjWpenri_zJIp|J#VArkU<1McF|EJ#5Aa_$8ZiwQ68R7(m-Cu!ts zq6C^C0W%Wfg4uuuE-|0LKCDmWimK6E0hx$|*AGwN2!3m9=$0|l^i5QwDqUty!0YKu znvsLmpUGzWdi$^~#8`@=Lkh8**u`X#pgr~w)znlo`DWdZUF{Sg9-yhYX<~eWviHLOcv1JghD7v;I=ob|!HCQA!| zRlVt{#tDei3KZfIUmj$kJX;RIrVvUug#|&RLC)w}NX|U;(0vmVQ=j>_|Dm)z_@Dmx zH!;XQ_R$aDd-%{Br{6{^9KPq^OE15%d)K!8`*xx2P?J~&pZ=G>eAk`(NT%@lFMM@# zPa`un8Zwvl2WN)FkcbKX^|_2h27DGIq7Sf@dV+zOM90LdSF$Tz;g$wq9~u~@64DS* z4Ee_5QSZgeCypI$Z0d+jPbFj1U=WjcH4~k0*U_+?|LJaQZQH!< z4nDzI*r{yMVKLd{?J7*oN|f)~sV#&YAec#~Odc>XJ-d@2Sz4#@7U}NyBQyWQ1zy5+v^(Hbzk}9Qy+NgmDg&ktNdghDik;k`NEan z4I4JVz)LI3*m)#c1B(nI)eCosGZltKJG9CNi?@X$?qHQ`N@>Cwi{v<9=}P|)weS*$ zsA^5b%pL$2WO5lElkv)D7N)T(@W&AHMV!9ZPO2)&P%)_C#&zB_z2%NZaS6Fb^SgF! z`}#M&Q(M=JNZ-G2=d;f~cle%NiNr$RjqxX+c`L3=e&XuAUXWo2w z`}WPFqZ4p1uYiY_UVfbq51%;7RtfqGO{24OR6aN~a`xjO@}%7LgsS!ivJAxp3ja7r*$$Pk;K;H8nM`3P{6+gx7$S zxy)Euuw$_zB*HWxf=4L1!rX=Fo43h0}44`=%_Cjk&!WLBeP*D43-#KZ(? zd=Vv&KmK@ibv2y90Ap^-1u0&-mMKZk;yILfp@y)Ku|Yvc@{8a zSiI2E9$A(^4sBx8bk_%Ke6DZ5aI>x66An6-bNG;ia{`I7OU0*O=ugithnw7y5E)|R zoyHI%=&BS_f`8JCRg^CmmS?g=aILc3Q(qHcO_^cOY@8A%kgg0dhcg_;y&Zypq=QiqCJ1?UVE~EY4B1u)1upT4 zYSb?%!X4xY4cFJzu`Gz$G&4O5Fc&Xg%IETUR;)7cB@+Uu$utC_iT8i@`nPEZTO6ADUFQi;vJxWIuHagX0WAD^WFsHRM) zu{b_Dc=gg*&RRihk;21OX~zqkA}^u@opg_q!^l;mnA5h-w$82%^c09R@ln44P@EtN zg1)k{fq_9r-m&9HxWy774gkWHxS*Idc;O|446kEPJKUT}PcCJS1|gZ0k7Jveqi^1_ ziCW)&=Sp{1XIEEOO-+6KhK;yjv#E^imvXjc*h^e;`ra5Ad-d(h6z=Zn#sP-K4`m4K zf_cFh9hX+r&|GI1^+fl9xr89r)ZzjXuc4ulM67ST{>H%2L}OD+I9$`#zM;CdF`pyL z|LPsvx<|&w-+lM$gU1dbuWk;G@%D)m56Q~mge%UZ3x}B2TRdP8p#^2|p*x16a4*u& z()rRZx~-)^Zt|IN0i$7l7!ZwzRo*PnW2To<$*R@r17;29`4F(df;$RBY(W#MO?}em z_g&BerV2+Io_XfE4}ItZfA!@r#N&zY{osYiKk(3(|MJfWYx~Oo_w7P%?v6V)GmUxh z{v#|Me(wjbx3)yU$@XoVU{}iE_W0|+`Odbjn>ssNFI~E(wdK_{vKkE!k8Rt&>D;-? z1WWQdvjtifkUs;08EEE?!H;@jcm`gRD+L14O}dKYF(Vq~S4R9CxyA4b)1_b_fbR;g zA=oBx3a2dl4|oIFg5V+_0UU}9AzcZzQ5mQ4NHj>pB5ovVU@hF?$TA$U;744iT=t+H z5SYO5B6g5a$B;7Mt>Up`ZIPr=6h=7dSWtz6F{ZKAG$)OZj~Akou8wY+A?DZJ+3BgO zVvriDiGqY>Ndb$?@MM(6Cnodpc%@1H#49$YEOoW=J{rmrE5u2;DHeUNULm`IV1Om; z_O2uM5`&eze5tabfga)61M^hQn2+7O$;Ksgh;$fplCG|;k3CjhQ)eP-I04=k9~r^9 zg*#RdXIIF7Y76Dyl>g-otEp3GAT&GAE- zp=gdI{+#fT9bb6$tt)%Gf{!2DRuza0j+cMuC%qH%Iil*$UKsw{XRqz)slkln=hBT? zB5G2Zr6c=VF^DG<`NpvG^$TOYgL9y5Dv=qP%06_U4Qq@Qy=#LD_w8)Gqo?kfSNg9E z&INq-TvkiVz<+%_eQbyKU;m2(exHLFtna?me`{(HmKz^WTrxRmUD}x$qiLy$AD+M(6x_FeFIxHB*>_*~@eB9dcX;#W9>gSU z!^%KenevZNGcV~KfzNw_@gH%iBur)AK)TA2NDV@WslgJ>nBXcwIkpAR0(wrxqz}NM z(VVoo-nyx4z2%9?=@SnfMGP~mV!?u`&r?sG1lmZr>Q6uaS6eoB|LUhd{)tchyTADJ z-~07n`{xWOD#atRW5;GH@4GR?TjJ@Tc1O?}f>4K`rg5I4yN#X637|5_tg*meWzMLl zgaSD^Jk%^PkeMCfNY5B(tca6hV0@sAwKL=_l7_TNc!nIKLqh|%IQtw%foQY?7^n(h zL+Mx@Gi?hac%8L9hqymn;s&7gM^f07Xe#3N=|A5UByM9O1KJDu7C2yt;cam8bEa1p0CvnBA^A3po?t8Z@Gv8StN3!zS=6vwEPh-G6o zd$22QLnD(vdHvmddf`)_`c!LMTM}n-nvRtF2oq+`pu$K~u{*K`70y@M5|zy)keon7 zGtEU~1c5#B=t&H$r+@ImhK)V@58cZ#e9SWtnppmreRm(+@yxTY4vb7D7g9et^%j*J zx$i#oVsc>-X+=TG!>u1N0#n^_q3M8ZK7LbUYHSTO}H5* z5^oCN*NJ3cmFczUwPxr zO9%IDjmG9LTpT>}=7k;GyC)`Rn9}$3Z2s&2{!O2kjRFdRZ)pzG)*t@SpW`Nc>#a+z zt&L0{iMsd?fB0XwZR@%_&LBPm z9>`y@;|bD1!W6*tmArw5in-8|2j~68SWFLuMQEr~Ty{4!_^UW8No3We3^q`}oQyfj zpunRyZW^3Gt5UqCo-JGbL2|fDrfImL|82IZXq40=&h`y3Cq|op1Lq?9Mq+Wc;0b{e z=4F+EfXnMEET+hSMRri5rit#%S9Nz+kpWAmX%PzISY%uK&3=2R+Ca^*w~jzlqsQ-C zGLsxUlq?U!1e_Y`u;?Q8)AB16S&_J=U!aC@Rq6Jw64Y$@Pi zn<$1CcQE$|qGVAZl`nJ$nUIpyONSDI4iYgM;i7)iC+NjZm`CDXEkL&QH~;`Z07*na zR5LG^v4vcgY$`{yvV?|tj0}<*Q@Ra-Ye0msXgHi(T7EUMOY(~eie`Qd5qEL9s-@b~ z+z@bkor{av&N^>ro!`Mh>lMWVo!%;+&WGXX0FQ5}rPhxb81&k$i? zR=FJZY<_u`Wxn!qpTqvt;nst@n(0Mtz9w+aE}bo`^|XiGE?Z)L zEf&jqyepH_i$r{*Y&e9Hj31rt5hvRQHSocYI0-z1C0I7U7+WfBK)%3lpi@#n5(D`_ zLJ=m82ofU<4L^W+h-J~uFoR(T5s^woT=NIwSe28Br?^9UpZ%A=`Shng!cyS)_+)SIjXAOy7YfzYVG>t9a`H$zlYQ;=x1M_P zK349CZs9&tR<;D3Jb8R!A@%BO7g}4YjYOjxw<~9ACbLtGf*gUwfB8I60$5i7B%O_b z$)y(ZvqNhRpCb?|t#HVa#plF^<|Xal6yyS%P^Qx5Od2`GFsiAm*|KY2F`xFj$Uv)b zduwE-S$HF^2s5AVn#-a#q{I!3Vw;?vAbk}1>>02OUaO3_O>Jso#x5IZ%|VAOV=-~r zw{I_;Acv)nWN=|6z!by7qcHj2y}RKbQU(BC4=cv*-Zc zVzJ~Mcl2l@$l6*}NT;A&?;Y}a38QeZ=Z2f%NL4nQhaz~h=!iIzHue9#%Wx1pDoKPJ zC96paXi$U%m;5(ea49{4$LJ&_={CXuoesUUmP7P6&MFL}e37tNCHPb1nU#`?Gwz&N zLSS5NRFnY^UQA8q%)y=PxI{gnTp%Hw3a-$w?8L?N$1elYzD@xUq7$1s z9f_IQ_?4?Rju{1Yj6R3MTVG4M3ZMovV2MkDPN5ohMveoO!_HFuT^w@dbj=Ou056;I&*^?7Oq}H6&N;f2Zxg+1tnEs&*Kkmp+*HODQ)-4mY%wv&InB~q7Q^z`*t+xsSJU&4V~3( zZDc4ZVV4y>ZEp#Ml}hCw*k4D`dPy-?=Pz#vh3eZn zXnB5Sj7^0&35j&P@1Ax;H7&Ekc_0*Al7E_UE2_gr#$&`=`dKYm$}W~DC=bO3U&32Ep&-DH4ait(Z_0n#BMRhR*4<_&z)B~%#opIKZSAD>923Hp)O9qr3- zRC(2Fc*!6Ru;6%EU0m9ir*@$N#ZWU;NV`l)$>PnRI)40EC=@2Bf_CVTfHD3s&`XyA z$LKBb?f==OPQiWpV<<{}W@I8F^%2Mfzwl-1H|vE)^Z>bW85*>hXOw~nexK`ifA0^? z4Wb-S!JcQn_dLt&+;KR=`P`h_TMgfRsFtZ?tVry?y@L;>w*>^^IDjI*xp4WIp!KmIqr{h8;Ve~F(@f9exk zwr#BnhDh!_J~nXq@|ABr4c-PGFJ(A+#b8?C9Ud+O((nvc)D^y16E^?&`&1IO<>eDC3grY6pe zibW@G-t2qp?ei><(c=I8+rPbY*UtGwJef$ctGH*&wlk++$KvD=!1d{SA;cgg%It=9cOJyQ=$r_7;8dPiwV*!g1Dy34<+}aXy zmyZn$QsYA2B)=8}Vizt)nL+>!q zlZXwp8m1vpXi5$Y3Rc%Ju#g^DQRpVtOxd}N$%rP#Rv@by&ISs3;5se;Ne`2u5u1a> z3dS@!(j0M#7gD{KU0Zje?0}mU#&AOe&av3&a50}l%mU=;2|UOZFj#)d8^)9@XZw91$eLDia#}qzq~{0)&z4PC zim9PdPVb>5{UDmimE~j-u5-9i!uSqNW>B6HH_Rb93c3dzmInl$*p)U!IMj;rkD7r5 zR|s-A4NxE=@kRVKBS=l4m6nO4;&63As*z`oQLbDjEvvGwP(Qqc;3D9U?YhIm!)0 zs#*QObIZK&=ACr_sl|mLTi)6CikQ6K*)3k%s`ufTZ1{C?+nO~`= zjBv|;f(%(mWHrSiM!i@d2riS((bA@_FmeK=>+`!v#DMd=sOFR}Q-UbZ^2jkD+s5^Gh;sc|S+Pa!hq>2=4#5lk3;!E>$@vV1k10;H7EDRwT zXoYG>u+FH!@=v&|?$9$3W#}Sd$^fK~8Xo-RnK}r4W!5o_3`+TCsA!nhaHxQJuVI7B z((T)~>qUkgV6Lu3A^}DRA8~Bk?25ADmA2Me=EU`NAsC6K8X6+R?OE2$`|6`&Q#p;w zM3v$}?3)$=QQrhHHLsQyvKfyp;jZ%MmUFNQoX_eT@1wL-i*XVJEA1mwGaK7lBO$Vk zRTI(_2-oD`By`;J2E`{7hn1^)&7I)sh@l5*zsc?pNAcbs5iq) z)ek=Wh-nK`0EkeNu!@XP#scKjoKxi1irWJxAHl}M&&JLRCXFUr#HYMI-_zfD`uP`s z!VfRukLpYspUg#1H0V^bkR|@t zL{U4MNfpJ)9Z(UJLTpNVYb$^eydUCTr3OMf`2tm+*?x<%Y2-yJlEe}Q5BZyCv&jG z-`U|Nwn;pKYO&=~vGKES+5E&p!HY^O#{wb|7qm_aF|}7x7QHp7xN=^GI)K#Fq;nDh zT}N-xU(NX$n@Z^kg|NG5N^T?^TH0~U$VKzbkBllydoD)=y+Y!0Id~B;)-R!}%#KUL z=UXD<&gdwI6{=e3PNu`gCN^}W$H&@?7G|re-N}iy_7YNcl(Dw4w8;KyJb*~JJWGu-#IYtpgs6jwuI4~CJ(tXar?JDh zgOsP3^n+l9N^lzQ-cEaeJBrn~**P~j;jTgz#|GguCSv7LY~d3)LX z>>P~Z4Mxf;eXE6>p`{GL`9#j^cTzKmWw0=aOvT{LLL$r0s>raJc>@1hxDZrR~t|I7`ZQ7xz`UI9mAYd&e`B0&%DtO|Nho`5fz;|0m8#!q#=k+u;))UxKQxgF^f}IJ2=&1RK6s8GQ8dN0^a^!r=!WevmXsz}eK? zNOo+JPm<@5%;O}-Vkv|W09vMb0D^FW6!VnDb8~U$pLCg*fWi!8b=?eI6EIFah7S?+ z;sbFc{ZwZ;vl8YIfAEVM;GF;8)^P$E4$&|JWD{p4-NRQj6K(PoHsyoGgghaH<$tZp z+>x7AqCh;Y(PX+wZ6c@$Z3Zp(D?RS%RQvc(`ZCL%!6wbiJ3Bil498jHox<#J9UL9+ ztFPj`F-AWK7X$F=A|1$^=@(naXy&F0t+Y{)pzod6F6z$ zieLPtkGH+{#-V%eVg=3gOLI6T8YpF;qDj%n^w%(yh(=z~x%fQ1M2b-p6K0qT_X>Ic ziK^7pNd8DzNXT7X<(%d@HkHgLNZPb#?_L%rUU>dR77$rl#dS}yn%dfSk~p=rVn^ci zU?Vet0x;VTsQ?csa~@$EIbdyBP^Jsq=ltX325Vl&9d<0?|0oi;gKD2lC^TvjSoeC8!0p%Vi{xaKgv}ap+IFY@!#)GR3*T9zrvz4br0;b2-Gl zP3jTbX1JBAkuYavn9bw5&i z=JF*L;7}NlKnFZGH2_9xYI?rE-{uR@N%W4*xW=}`$(pQNFOCkCxY%AQ zhgn4^Qrp%-rV8c*$l-Dhc#bBL!)6O4G@`Q(N2H~h<7%K8D1|EMHD)5xoN{j{CjhuK z^kjqrFw(Cw6Q55{jI&fh=#{6Ifc=VMHX{Rza$IyC0mWIWwY8X!nd#Zg=!h*+L+>#7 zaG3dn!Q|keEFSR7E11IJs!H}p%RGfoU{2JwSv#il7D<3emBR>p;#F<&Mode?=E0;H zVlu_n-N3vgP6HOnQG~qVWyN)A(jp?u`hd_16>&S=<;$y6^KrtOmZMQPkP>rqv4L9y zk3RM&`E`laJahUDavFjgGjX6w$z`l9fu()HOgF_pVtc{)vmPupFSL}VvW4lnG}J{j z(FXsC(IJS0goso}6dV%*M3~p>pP8K{tYmz2WM@}Xjo+PLAi{J3ZGe{#91y4kv-4Hr zQ$u5Y(243AobV#mOO5dib`+3&F&8r!72@bYbI1mzC`~1~bSl2MJTjDOQK~go0~L0N zd}n_X=^SzD5d9>*D|<-lM^=D(%gZ7<7k9`a!8{NbCuciwoDSnq53PcNjbSJt^a{2} zNoh2m!eQz2SCaB7oyMFk27)YYR1!-_3a(^Qo1ub!1c5CcFYrPr;vs2#F1w1qbEV8( zShbPH+_sj21LIM)AD1{?Y-rWKbU0^rb|II^`>Xs*rA%@b{GKvM$o^THrVNXWle0R& zmFAot*W~c5eP$sTVY^B>vGa;`Lt7U}{jyBtKIam8y_I;BfSYdO)Pi=B$KnF989fs} zsAIaY&bUKSW<3@OXh^D7ww>mbj9$M3mO^xgv6mVuIDg3=<>sGTF zguyQ34=VCVoFfU&K58A`)Yw!9WL(@aeKLR6KUz2RThG@om83lLl0d+X1XttgB#a?m z;i)JO&oV@eyyg*~_)pb*pae{TK+k4L4Nz~?pH;i5MekZ)^`5HLxA*RtFSqZ!_sA$4 zioG9B)q`p$Rh#kCsxICWB&MLiHAwGbeLyO^E{1HcMXX`(5W1N70^S@uqn=wCbua_wxZ zhMq8|tb6N6_)@>QS5aME%kcoLkmR!Yty{OC>MmR$nXO`vKt6^Mwi+Ja_&nZRotwcZ z#(Rp?V#3LSHzF71i44^gpXv=HnvI!YaVMh+MI(hEx!YuW~+xF3)}bLzkB1}fHX*v4=zSTGb6 z$_z0nBh0{osCCRKi}NI`*0hyd)&nFE?`fpyHp5fHPX?(OxoX4QmfM~b`~(u|C5;hX z!l&U%1Cez@PYj;*2X=yje0X00o?CD8;eT_j18gxS4S`D2U-h3Knh_&VH*{w0%S9A6 zY-90^D9&@~R`9`|ha$?!dWlvEXXhrOh&8_3cJtcM@JOgC2(#t#1&~HzmJUG(TMltB zEG}?-q|mO#2V)p9Nuvgqb;#p@tfsrvOgth22V;)irg&9AU}Hl))<2BOA(%V@%kt$L z-3&mi(qPlTa}Wek;k4VL9I|w?x3ali^E{UA>KncE3ze&gBQ0?P80^XizLq=7Q?a=@ z;;39Wh0WkCD+{zVBh*yV^cYOAIyJ`*JD3!H#RQL%w(pf>^jP+WVMj@bac4_ z0oW^gYhXE_vju`;Hy|x5yY}tx4*ckO=56kp@R48pjq&Tfxw%<;o1aXH3`@rT<*)rc zn}`*x=Cbel&<7W0X4Ab_9UHgc4G~&ww#n0HW-nZ%Lexd`U3Ug*Ye<()6uH!aQd&SX zlqs?W7a*Pzj*qm(W@Cl9xytIA(oj{U-z%GijWyPIw-|>(=k-(&yv2T+@i86Vr=BQj zJG%~tUwO3tV6RD(SZYW@Z=HiQ1c<7` zzOj;{s&+lI%9nQm`##ER*5);|H^7)4MJe&SW=I}vsN~lD$K9ifHJE!x*SJ+ zOiq%TG1*5bR3>L5=N*$mmUc-LucqN)@iAGEOYI&&CaOn7G`D3~i0RB_7+!t4d(Vcl zJ*|aILAy7pfs7SPMa-*(6x^O$Oy`s70*P^nJsKIj<*F#H4F}otnvTt^E$3a>cNj}d zBUnx90SU!SR{=(Mc8SSy zO?7B&a-qlT#4=+m368X4A&uzv`be2Ubeg-EVbi|GKfmR#QwOzW41DuNkAw>hDzP{k z3t7#m0bL@9xJYKM7y<>UUqU8DxcJO(ZH#A;Q>$Q9fZ3~v1Qvo(6;x}&55-_eF`kL6 za>E!Tiw7iM85rPRgpkvb38A)48RaOKwU~oK#&NiD;|5$p)6<+Nmr+8Y-V0bqdsYBUWCEummpaHy$&f@=GTmUHAUheo z7-;}5q8o%m2)$zoR<_ZcdE<0bQwys$8@jsiNRenw6|!`+go@SDJl-`>aih0)>-Oz{ z0Sc*1RQH7*u_)r1B?F2>`Z)Q?;*GVj%W-4VZP7{aldiw@no`NJRlm;_x6XE>c= zx~%r}K`dzQn11V{SkWq_N4E*Een&;$cX8Le_r6>2|M*_*lzOkppB4aExFQ_%ec;It zY~0*KWw#k)ok$e0c!nW_c|;v>tTdLKd760R$p$%~7=&vq>qnDdR{Ed_U6ujlB+MUo zNEvywc!b5n6q*JUkfG=<k|c1}_)2tSgs?{l#CXP8>e{p=P#qB^l#CdZnt?CWDqe}6&QFY! z%c`=jUc;AgrJ8CVmIP-)XZ;Zr=w@Nb z-Q47>3gP~lB^F-xq8JLgLhR(=Ex2VhI??>ZhuG{gJ8+Y;618EHAAAXgLS}dfF_E8{ z2_8QN3ge?AC2l{&;s+GLKiO#xhFT-C-nFqC(GOue+wJ4!^0- z?IINj31VzcCYfcN3W}70lgJe^G?PGR*n&QGI}1+&^=B{cglsDuXPm+xVuiS}`=hj2 zS-M2NvCJ}_QkD$Xmf2B3#tghR1SBTrIE%n#XH^=O24swC*4CIM$i`z4PXU(5Sf-O& zd8c!rN`jb@Xa^pFAQXe{A}z#|;wT{D4){_^FE(Z1tbqmCPHAJOCmBF&E>53;Yf!U- z6amDEOd54mz;9_;{irVL(WH+Gk$T3fWtQsTWDQteWOPU!OWFaLTA{E2K=z^gF~3Sn z;)!gk=)fae#?JZ#haKh#L>Rg`>Dy9AZ8J0RLVjuE#z=j&FFJ>{q>UKqLNT$B^?Bx3 z7dXLYsg#ox!@-chlJn1<-YQ((<>lFM(9_XgJvuU%tz2nuk6d_nbS9eexXBx0kB!Yc zD@yz|UUufrMLC!@hZPa>JEDm!-ofd)g@BVYCURE?XP5KEja}7M?v?BD*o3`M6UGq7 z81+d&ub^?sPs79xx1z90_aK3=ELPTdkaRHVPk63{4|oc7A*pAUErbdEP>f|_$wFHt zUkn|og_?wLLt&`_v9AU`yKi)mg_=Z{LPyPkxNVJ7H7f{I+Sn8#zErOTnkA|EjeHb< zra~1LDAZWGv8(e-U;I<@fW=~QqGt$c;=(N8LOR1TP!>>FTV$XjgV%poR1g|mS6j>6 z7l#MI*T@mmqF@p0%4NRJqkxUB0$zFucM2m~C{&Nxw)Vv@eI8G4G&;uwn$1eE;F4m1 z)OtRL`bRzZJ?OsOrmo3`eNJ{XxqMf_YTMiW~djy=%BH&)*2fdp8d|# z{M2-G)C7Xm6-&;~3{I4}9SyAV3xyh%LWs_q^q`P_<2QbtHQyV(eI$w5zGEBM10hEF zY0hLyf+P)U`io@ek&r12hy@@$_^|9;ZV+(5g=2{QSs=t}ON;>5s4~3=wqvmbX$Tsc z8z3bWZraoX0s8xHpz_QnCW3;reVtAFjDfI6GUhDIsEYZSAhzy9fl-3LpoYDHs5?swF%&`3?;ZFui3LHv>V{ zBH9GCDpGgw`D)H0R?(o-Y6L=2gTnlwhkR0T7=zzJxDaK&NK2z0L^3`KZ!*Ox&OhD) zO{FA-f|T;)>5xBOTS_6eR7;Ga9o%YG~l{}WT-nX z;HUdsR&5g9E31C57f%E$E$n_RVPiN8;ofeffl+?kD#0zUKyC&c zlL_YPJOGCvoFxyPyBrLOFv3cyx4w?!Wi-x>=m4Rx3R5{y$yy5HcdEbtJ&bV5m2b8-Yj#tU>Zop zh^N054ksmYNjV}l0Sz02%%5JLXU{HDQHv+V&teX7Wf^9cBEzuq z^rCc!g?51@-09M^!`NgZCZnAICB`0#Ov7ts4c^m4PTUT6t;oI2W(7$nh{GrV7_q5( zzf$DjYIf%#js0i}0zB3*$d>JXZcwgBz(K%X;;mS9mMueLkBt-$<%_u`^4YAGu6WCe zPW*1h>7yDMcj9sDBfrfjs5N=>W?5mHV7PRGyfv~gkea-;v|5(Rl1X3%2d!`@bqF5; zPX&nrJw%I30+E*&8fama8sT;T!@x3?Q}?<+!|*{8X&BHg<{XAo=_jTnMVU4j#A(Wc z)?@&%6b5!oDd8TD0qGcLZ~^6EBACsbjbq9HwEm$L71%B?WM&x{&j6eqzh8JXr+9 zOsuWw!w(bEaqDpd_Y)XC)-?} zZHermK~*df>YPo(_q@Bjl&6oyLIzBMSXP{2EiOP6dlF(SK4JXGiU0|0?$7`*3K9hH zQNPVhh;+nWzlS8V^bUM66nH=haHa96c^#-WE?;31(JK_BB5NZ9FNXy%%ZSZO6!gp23`^dZc`bNkZ3Pn(X@CkjEjfFj4;4@PQ*=oo{W4j?4O>bJ7m*sY={6DzCaCjAz4UaTioE_5Ru)xcI}=R z9r?@u{3owpxb_?W>eG*Y;4$LoOc<%`PI>4xY=Hvf3bQ~eVSO$o{E6U(Rrn`6{$5 z2}X^C5F4s~gc6^`HEKs3X+cOY#kOh?_7Yk59XyE54G1&}cAMK;0i(aKADlosVgS@4 zfih;Iq*okd;}g#ttc`1uENXO2xE> z&lKze51S)Q4Uz?_9E#DI=qsmA$pnmq_Z`?r1aUD}WFb{~!gARwS1xyVZ-UKdX6DDo zhqr9m3Vvp1W9*v)j9UX7wY#!$V*{dQXy{gLZL{A`UThh}9Qn*Kn#7D-gOS3B2=KrM z&4f z6MH&M>d8GlIVz8=Y|EByAsZHBgV|kRu}#=zA1==>?B3<>K5&5t->_`L*q8KxSukO_ zUXlZbl_krvY)h6bE2q(Da?Uw)kEUnt?|a99;Qk^<=s@|%Y zl1!gP&?T%iHyLo7@&M}wonDL4cJfU1=7oEE@T7xS(@ckB7^u{dP25ao92yYTpjE`0 z8^EASHS7*mncX#w#dppGb!A#Zh>0cjGFI%4E~m@e{%P$_$vlZh6T{wEv@K^`tN5x> zQ19V7;CbuTDRzE}=X-IIMSn|{gx*9l)7S$r!+MMU`1F&H9Xx1ja2uWTBFpc%G{c7P zeGbVD2ZO2k!I9wvr8pv)pw!D>n6Z3W`Zu+AO-s2{Jz?bRIV?T2d-s_gTe_F7DpxeV zQ+#>`-C>l2LsO>r@7=o9=AT_dOERHQ7v|!MO+ZF_`_5e$*}rYe>7&PZE|C_6O;LX_Q#fm~zPr|D#9t>>07)ob5w?j=y=@K|qHOC(sp6X=YQq8KPOU`iBN*EgEE+QR>u&3|2?Z z*}pr6w^|b0UA+cY7@G10Ue+2ZiTgHMxJOY+AkW>`gP^1+;0K#?g9{Syo^mNkwg zLVJz+$bAr+G>|t$CXMvGt{v19^(YvD9jNY-9r4^k|w-Z-4jvzHTjQ_8hNQ;L^lJi)X_x zCJCFw#!sA^J<^AB>(})!U1I%#R*-!-J+%K;YN>pMW`3+)-HmHiG;Xn{UDl-K^sd|! zu6iLuYC5KHKK^mKaGb%Rj5Q}Z4>17)`7^RKbchX7G*L-DMDGQ0spvLfYRs2{1xf|v ztDaKa&eP;E0Fuu|XM=01;vc7U01!~Q8sT>VkdpIoXjw86dKGRTWEse~ii1og|9!s3*ZTbz@V8108m$ zMGD?R#!U3+g`S9ZH0RQY_Y*ZeT{LJW_p~UKSvCu|N#ekP1BxtNHb_lv0EWL#5Nh2v zWihd?sbYf32$|I$WmbDQ8YH14Q_?fENK{6GX2SSo3`aXIj-DN!c;w^-kzNMnt~bBQ zw9&`!zIWBCmEwGG#&LKGIb#W?nBWeei&-PSGfSucxujYf)h;^1MX4}U0+K`M%9GOEf7 zQ_f%o#Yiry7j3LcRUa3&zrXK{wK=Bqqak8iFC%=((xpz!Bo40>S&C{I7MKec%*VZ$ zi;3(tdy!d!7?D_cq58~HrCq45h-%WKp5Z{GhTg~maeQx=R<+l6cE_jV}&cMv4pWgOUKlyK$E**mTXFl`rs#X7R`LZP> z`|!h$-TR4$4jw-JWB=}-u3NYI=idANQ>V`S+WX%p{`%wp`~PXnXdC?eA3yzL3m47b zzyHwB{@ias+*$JT=S_d{#T-lETDx}rQ%~70-*O{M)Amg3nac(qJg{r!$~BI>^LDU) zJcvpP$H!r#44r4)8>mzL9 zXtJ+=>C#*Num4Cf*gf1x7?|^*3rh80FOLz8; zCSN8TKf-NcnwQO%ILU;SMoKobr~_SatU^=zLYXr0OH{#2T)?QqPh&Aa0Mw=;6anF! z2jh>~5dm=|k;7PB-ruu1jJB618TZL%5E2c^b`1H?P8 zkeg+gqy1CMe1z|o{4kvEQdD=!Hef^E-C-j<4-M%$ikO%?rcl3E>YvUmPJhfz^bH4- zIb6<$)G=BX#ZJlDL%}ZjGR74zv4+ns`S`JZ?Sd@yQ7X16)x=8;yeL}n*+;O2j@dW- zn7^QZ{=9ytqGceI-X=~*3yI08lHy1CYq5S4`}z{Agsc9{WB5n!#SbdRn76U2g9+L} z7>+?!PK`mmdQ5F0eWgS-<%)8+<%KH~p#?19S;+(+Zx_z!@0vZ(%MgI4ooGrCd$^X& z1TB9?Z>bP!F+6En#mSg`*&|quCvROs=t6|ZN>pBvRg0uBuh>%0 zu`C0R=pOzX%IO{^wUbb@Qlw|*tV1WS-u|gaE=}7ofB725ZeU>UJKy!)-n@I{;YU}j zSZ>VZ{7UT9uYM!i*X9G zNpf94!X)aRmLMsDmJ)p2sj2hr)>C}HXs?x>J9d8lkw?5&NbL5-x&5`T-M)Rto;`c! z&tHIhFKybqYuC;zue#F4q}4_X^R$eNZkh7XP#>vHGm_M)JCc-AkKL7NXqgK$50aR# z&P{UG&mDR_Fan?FH#$5K1F2r?%n_1pZ~2N`Iqn9 zvSqv75S+=ga>YMhvSjhzefvN7-~agM-}fKAR`Tq#FW!0Q?cgZ>mbd(i=bn4f=>^~P z<~Ka@`6uWMvA*qXZ+Yum-n47?-aq`KKmGUr_fJ|2`0R5VfALrTKoy2S#ekA}Bp*ua z1A?V8^(!h=-Ky1ekyUinq>na8e3{mWIg1qIIcSh|8lp{IHBCYc{ZeG%d{BeHsX^r^QRGb(y*!#{~8E4S`B|LV&MmHB-?XGA2S5 z`i`UkASgZC3N4uHl45}p^*2lfvOGCv2u~)>D5VX&cm^f}G4kT9xpQXBoohbbSVBBa zJ)D!So^DGC33?nqdc=kfQ@eV&n8CYz zPzGOHDdwdOLkUcB3G=fPdTe?)%qgU8jG;(Rch5ktgMCXgc}wwJC(N+NL^Xe9wFq8* z2K1TyC5nu19ibRd#)Nh_Ic;JygVOL_zpP#6h=}UobobdPAxommY^WAUOb6Y4q!>3V zab*jKq8Of`PVvV4{7CN1i5tbmU{*DZs<(Qa{F3%sle2k+Fk&Ilcyd7Fe_!_$hsfsz zR{rkjC2Nt&8OZ6~cIi5O*6`iyGY}!;cpx-RWj32PJAl-t8jwneHOSQ!MXI0neIXrP zsdI;ro1+MAd&ZGZ$WtiQdpi0y+l@ItxsJ4}HsC!a@ie{_WhGnBJ+(AN^QfKalt4#R zp1h;v6Eh3$ceoqA)FfaSU^B7ugaC*)T7XLKu~wol-eI0H@zC)Lb`cW5j`_NHNiY&f zgds$=8un5++V~)rKK5<&GpHO4 zojh{p2&V}Sxq`G(mjo9J4$i_a+fGUc(+%95R&xBzrFqjyGU(AZc%sHnV$!lQe6$8G zM)5ky^!Ok!{OlTX#j`$F*SJ`FHJPbx$487aoNfTfh&-yn8s<%lR1l`beYaWp)SGX)+1@4B zU3c}oIdiR_{p>>z-E#BILqm&qY~Oju>tDBH=PoGm3~s&c=Bux{T3-^7xlmA!Be8;8 zD5)aoh4NI>IH}@xOMk=YH1cqjLF$?@FuMA+wk8u~*SC1xcVPC+fB7$daL(+3Km8vc zhl6?JLb1;a2P z?0`@{@Kc04F~~?bB1rSoy$KObxYTog*tGTNbUUpl(gV$K7zm|Bt!SH$)7BmtA)f-6A$`-i&n|AWASaG;}N*e%dE58+`Zo^&^2Xhd`PG z(~?H(J57L+dnGKqQZKw|8r&+5Vj;78{;nBc9TQ~SXU6c-#k=10rhz%Lj~zMYCu**^ zV!d}5Ll{*cs_M}hOf|qKisU<^|4}dKW^7wkU>P!Ty!cf-^RxYn_N2{t8X(|eT(ckH zObI_jtm0-h$xK1#{E%=s&AueLV)@d;hmL&W6Q5LM=FGQ>{5|@GM;9(ydi3a-HLDkC zP-E#A*Y;7Ps_FsSfgKztmyM~r5{`ddkmwxo${D7J0*SzND z`|p47wXeP9?cerI7L32RX^Yb~y-$7MTmqbEV-C_6Mqxs7(?aOGl>tSD<rF1^&O>Kzl6})t+T&{=Z-s=D4z35O<|kjV}{7D;vW2#hIf#g>MX1w!n*ZM(ySh?s+Cxhf7qK~XX3 z3w+G8*41$&rZB60F?Z?LkfK3A*k?FN0M*@w`&`(}s8#SKoprnhG&bEY@6aQ|oHT3U z{5U;jC*?E<4(t4vE_RR>#F5alF&@aj+C%Wh>4rWYxB)(i zibdP1l@;ykZOb=h4oXx3WSkuGD%`ikiMK?a+^;r(R+XXJtxl$y+zf zWFUa+(mu6^)Rs#^IbCsM{G~l4sc^$>0NncrECEiLJkrzGH@I}RasQSVw^9<1)uFD` z3zMoHVt_;s@{J0rD{bP@6HZdUv|>>YeqWk8+cE?FrHsw5&ls`1;b;w*<#xb^o&ime zk%u413~7A)=uFiZPC~y?6oyF60|?}jtxzgV)>uFci#EN zyY9McXz>ukN|U5SzK#!bkVS+Q|1}l3Akb-pj6W%aU64;Ko8n+ohGBW@;NYRds?Z`V zBlFw}NZJ)@%9WsCIxDgh9#fr5zo82RLML?Jz4!ftw|tYubGw`Ya^kqx@|P@KvgyT5 z5bi6FYSX4oD)QtDhY2Z8g9|&a8Ff2v`+-kAU^cTj+))vbr9eej25Puq6 zwD8;C{x*Uojdo)`P5S{3@%>yZ~wO+`X}H0Z+_*MfBL6>;)m6U zzwpd%5f>dtF>RmaV)sAzec$^XGx~cD6sHS)Q##dh!uaSCGPP+hL+JoVhD}*;&-Kb) zG`8*{5|Tm>2{AAx#-CEcy&w6sagm<|hd3xz#h$XH0k0^oT2c|-E0N#_ij$b?i=!%P zf`LZ(Nr_|YqQ0q!<*u8V19KKG9E=1V>vZPSzI}Tg47X;@IxA-Y+_QVvu@gs^3@z0x zc5L6acxY($tl2MYe4c(TT(Hmty?rZNSt#$^xx+AEaBwhDy~&eJvhLcsePCez(9jYd zn-1HO&TZQ^+c{!z&^GQ9OoeaRve{cSbQ&wBO`5i5%~}xXhI#zG`}ZzgvWy>d=+FTx z6PGSsE@-`P-yYtNmM4zB{)%gM@7%V1%ZobI^;cYHkn!Ti=j_S9b&oyzFcvOZwqnuX5ZXIbkCj%njGyJZttGd^>UM*yCS#h>UK# z;~VD8ofijg%G3h~_rLJmv&PFvi-_V>VKR3)?`#tpx$q2gA35qybqbk9iTER~bV;6O z?#o=7VOvy+0kQu;Y>^ME+kw>S=;ut)W+)WjG`+2dMD4fWk@;qAn|Zbal-VtdP=u>` zdpJTeGI{}K9xr|h2hJ32H1=hKIeomz>N>o;%wk!V*v;`P`j#KrDaNX>bB>#;6}xHO zEL2=1kIWxUv}-z#i1r$WGBsv+atc;9)yFkk+~+BLD%;|msE#=PiklRv#YBrqbra1&hu1kLuPQwuorv!3}y zFzRk0kgB9$Vor)$8U`Misw5td2jrXabeHnGaSCeSd?f(>TaIBM47C7Nhf z2%a)W!Jm*V0ZB~-thnB(u0yMP(CAj|4A=J3=-io;*DkSQLNsX7iHXa5r&knLDIBI& zWh_b~!$~lSpcoZqFG*)Abqmo% zF2aUE>0c{rePoh120B_y1P!|18Lq&Xfwlki63* zLl8!}WuDxNI)ws=MRPK`4a8iP7#%5Gt3EgfKrQ5EzJp#}fXP+Ri^xuT)tKb47LnXwKot-yqM#JD3|s_+B)-fA z6;Pbm*{B)GWX+R~MP^%y@EG)W8l8}g8MJTx*0;@_Gv^bZxNpy%{kFzVjbCwjRq#!3 zzH9xu^>Ci<2n!N2(5e~Os+gw~4E%*zjF``Aupp%+S8AC#M8&=ETmdQb=FK_mY_FYL zC=|MS`}<7)z5LYVtQD^eG93WP@0ZDxzm@z131M(}F@rF05078Qt%>OJzgk(oO+e@t z6HnEx8fDWz)>PmC>%utzF{X}ct6Tzb6`!@T2Wy1&}X=+Mxz4}bVmt5z<1 z;~Q=l+`9Yj`<-|BmbcvHmBafVc*qMFr%#`H+gtB~+p1N|SFBh)H01T?$^Yo>Z}{Z> z4?C%8_wGIG)~@>J|LnUE@@xO~zirrX)$8xL)d1j~@A&r3o3}mq;G=ikaT`|r>7Rar zza^S=?X}tU;PJ%l5Lp3SP2YtlY0URtqyC6!#@ z+`tJFw{PEOgsfhIUA=mZjdKqk*tcTkN*>t$eS4P-E%CnO-aUH~<;pzW)aA=p?b*Fs zFzL!GulC~P_U&8xr_TU(p>u6#%-XW$B}MrCFTM1l*y@!2$$R$fzT%3jQTWnJo5WSb z$ux$cRcjXyE!(kus~%*@@>MHVueW&7r2pKx^RKyfg9D5aB9?jWb+4k?Ml9XE-JWW# zUZfowTKeecKj-9!n{Ija!2{awwrj7u{)(%v`|JbvP@#3}uY&fmqlevZeBn7x=e5_~ zB*P2OKYRA{DO*)uI6uq^%$v7h+47Yy+I@cN6mMW0*uVGq@uREOtlRj)3v|*lvV#XL zp+4)dhuFQ6L|I~jNaH2D3f6jA!Rc)}1c5Gt>Udf-#yXS42C_d_gr2rQ$~>@mCh4u@ zV~eOj8H!Fnk|OFz7=mvtew2|H#yVe`mP|Z9dd7Ld@xUfbGaR5T)S-^_6k7jjH<;OBEn?+ikj7rsm^K24rLRDg{N}Hi@lF|5quDltx4x} zWL;5Eai+;uuto1JH&&MB$L!Njjv3bua$dSK{q0aRkYjIS%P4DlrnCtk*p(ehkRA6k zjG@!%aWc>7ozQe3)jY!X36sxFTg|uczOZxk;_l@O@?I{qL98{ACe(_zMFz%(tvYjI zHm$QPTx((ls=~_Yp3_B0XY5juO|g4t0XTBzm?r6G`NtqKkTQgEK_o3Mz+IZCG&x(1 zIFp=JW95y#NKs|{PO*X!{QxC^0K|&Q7Jx~Lm<(j6^~{*x)oXL3kttn$qeD~I4xGF= zYNPNJ!J{$}U<7`|XZV$1QzC_WTQ`7VLS`B#Og?g9=+ewVyQ_PUP+H0d&DOc8FbGC8 zH8)pWEgb~)Vs{rCXLP|BGVddf7L%KPrZV4E7PNRgaEr*&x0Zwf1xELPYE{KB5?3k; zpD1{cbfrZVdy+8%OS+Q8v_!VAL>(g)af%hr1#-iLeAj za>@n{JUTcOGn7_u)PgQYW>p+&5>uk6@D;dKu4Gts6}JO3D>TjRCWodg5TSEdB_ne4 zEjO8E69Ak*W4}zH_L>O2(I~aD5avoXSYqB+&y5kOCg=o<^yOiIkJJk?ixJ>iwkb<> z_F#~#nT$qqp>HprE}fU$mWAn(?NM&kZ3XG=?POVKPPMb!)}jSAZJ0fG?mTNFYI>zs zFr=fZrlk5HSS_@=E)s#h|Bz8XmK8JeMg8QZl0lTvFI_aJ*0*fPF@g?ax9#{Wt8vBQ zLX`Iht9eOAjjzDFvKUJu2CCYGpfI5x$S$>ZOQNfeogM1! zCERlHO2tcCbXs0eqD)m?aS(DP>r@|!syH^QHG}pe)f6h3 zVh+QdB85MRq_tIrkzfJ!v}PNsbt5RLMm01F?^16SrzcrO8|&#WJx12=*Fy@QTp8hH z$Vwps!Yq?TX8-UXy`9<+ldX4b{i^P+u3qc!Z@6*&&%Wn3yL)@>Hr(IW{o#*%rhkUL z-mKf1I@bm_2c)Z0jQ*y~iB4Nx=W-7k+E|_TB5(uXxY!fce+-Jt zBOD;A(b{Yj$;5)F5F8h|K5DuO*Axjm^^jS0o>45tBM!1lFPWZDQ^*La8f#2xG&8J= zZymc=mD2<}ub1l+8zUsma)cfxHIIcM7g}b(N=`xrcJ9b1<0(S9|WuTQ?GwrHtHW)uV_uMn8=3TMy zwgSQyi92^}n>T;q@)fHd{OkiyKlz1keY=?G-v9S^e`EH{8U4LIbLKA?m@}WP$o5Qq z{k%8qeeT&Ogx^-LSu3LE%=OxTj>~$%4jDJS`VKGmeC&@uWP7=x#Y^lF>`fF?(i@+D zO4Bif7(RE_S?Rd0A0s1;sN^*9k{M3zEXt7WGYBF9=mgYbBmC6F%6Qlp6gStGI9MUi<$b_KYp0t1J3C^bm#CLW zF$H2=C|$x=n2;_Ulm>=vP3K1s(VV}mvO;wn4d%dGH#biiD8E~-urI@uEPRXUVW3%+ za(*(GOir0=^a!jn=3ZS7QfVMKHgail*O|WSMn`7!o;M?quw5`tPL682z$%qHzE<#q zt48IO%9NK9nE{ zU%Y5urUR;$KgG_0e!cQ3zFK z2UJELp*y^f4%BEh(&PYup(SEw$)NxStV9B$m$jsW4$B!^nmS?OCizPzDPZcVKQ~ED zi)Xb1C>%>uOp(f$hrl;{pg{k;h#16vv%u2Z9|^gET$~GWg%x~Y=&y4X zABu@^b1Bc84DO+)SQl~{JE39+JF`)c)kd38N|o5U{E`)1(&R?)N3Xf=8i6z$jXwYU z^DuF8j&38iD4RK5FvDd52*l^r7UAG>i6X;rkXLs>5X)-_UNko~No->B4UrBy9i8x{ z+764=zI6F=fv&;DgBE)sdftNhTeohbjm}-Aq-)o$u|HXBSqYv>n;sls z5?o5688(C~Rasc2z_9m<`QJ9Fj_D8ID7G%yvy5S#18ZfN=IN-lME(-NqC(47t}H;D zUAZz*uwapm?+-q9^kAQZP|hB(ued)D(_txFJlf=53D!TmPj_uAT zn6Pcz0edh%_0$vp>r+qosTSS#b zZZ^*Hs@SF2kK$}K@aQN&x3ZK7dz0;p%7duGk*HQ=vYv1;wwb;dt0ZhB$fy!pF!?RH{{ zw;4R*axvaqE{?U$rRe{wU;SE9pZo9sq)xPU172f1^6`5z{2~Icy!twmgAV=NzkiQ77O(61XP#WPe6=GI@M_ueRj<177MMTy znfq_K?bR&IV_$e=#flZv`+Dcho6onKGk>A6+0pG=1>>~~=QMCGUv1ZQ<_yePe?_*kJ8}HjhU;&{kT-qfw`jdvw{Cv&@yG1&8Lx@sasGV0 z;uFt_45|V7j+iJOZP&Tb3!;t$eNW7Es=-t8=!A6euCi9KCGtn6le}UUYc^@pg2hYh zbIKA5kZYvo75GzG>W;+|%RWnl3*kgJuFAIzo2*07NPj zwJrPPOJ%W~3Sd^+N&tNR3y;3(o8B^c&1%@_w98pcClqM8TI`A=5}ZOz3djGz0lzTX zy?(&9{@gSBy^Z#*IdozAiPJ~4Q9hj9W>2xNB+}4E2US;jdAO?J5eDB1FBkXY3be}L zom0T4Og=wl;#sX2Os4TVE)p->B$-hxYdz^=x*z134o){>nOfmihn=abP3w?mm>^;T zZ(VHIRujFP)bPp=>nJgSPi?iGn=;PnpcBbtVlLXI5?i~^Sy^YHjD>raPi0O4F(%(H zUYwH-)!1vAO|gVY@Fa9VAQ2r-e>fb;(!v;PzV^k*ebeU+%$iY>Vs~&f_3BwO;;SoS zR!wHi829i^6keKJp^lOB!}AC7KFjITCsB^bXmVAx>A$+#+h&>AppSA;`RO!9#Ito<4!Ce?d>WOLa+CX^6;xW9*6F+QO9HJ_?pY zWiSo8Y5$eEn2I{>#(v7(uY29?@Xr#cix+Rb?G{zCfASBevmuVS8tc}r!4zm-yWv`! z7pOoHWS~3$qrDKp0yJe_25>r(idxn@RGulLVPH#+EZ41HS0XCym@x6W4I8e!`l>+9 zaS{FY1cQl#o&|H2H%1ACdhTcpG(#?a>IPJ!o?K=1p%cNol3}v#phZEfGFD6*Nq3^L zY8xIyQ`e9%WMVw0&z@eiVDS8;3pO{GLVp4_#I$8eD(7H517cx@&&<0m%u*oW}X)>Y@=4LEw?<=X?;$mIGITpl(Iv0Sr)@Je4 zgyrEw3BjTksuih)rr85?7cCyDx`TFakJlY8o;q_HIvCHahBIaMF0I}Y~Gn7MG_;%A?I3KZc__&)X2lS7UP zTQq1}vi%45-~O7{ZP~o}@y8w&?~%v;!LElN{+vkCop;`~Wy=Wpw+?7{dXN?5oLc|~Zoe!KlciL%F z1i{D2{8h{a3(@U^@?#_YVH@cT`=o*RDO5tY0M~Wmq#lQoFo@B4eNP-^L4oSj2%Adh zm4#?v1-!EF?iZeT3OAH3C5@ue2SBvErn_#_MA%i!nHZOLSYO&Ks})~dN5-4L-W0Vj zmj%IE?S%_{(`T5n>z+B!%J=B%mpfQ3ZNh{npM4hZ#(w=xza2yXgw16_f;JDmbYt-c zpcriqF`gK*#Gn-Zv5$F@zAHw;*c5FtdEhrd(`GH1H*=|hCW|SfK0r&3bx^1^lz__C zAR0+EwoKYHU^-en15;gTRH-DEwG4BdV&$5dP*FgM)0Bslb(9&HW|T=MbVI6BcS$OJ z7-)}VL;)DAuB zm4&wTc1Z|fm}5i`h^s1!!nAl2aD2pcH+JOD;S4-v=8U4?FdYhRX;&3NLlh1M^K5We zW}Z|oTsU<2(DdHE1@i|H9%ql**1W5!sPK<6$_u^P)Bpp`bf~ToC3l&^G={v7#;__8 z+GAJ4lj3sApY$Z*XXGDuwoVpHkz`B%S3H^I@@YD-1ot16;EAgNnX%}3c_lQmn=3TE=9Xpx;l1))l2#;SfMk<@~y zYm&7T*^ki_MN@{-tE!$Q$m>pcSm?70p`cSsaRnD*;;Cf2a2fEVV)j=^4xDq>~HP zVS4n&E1Tg!u82lW_*vp?0t=fcUNlO-840x9Q4q*Oprmpk(=;(paiP0uC7Gqn8C=%iBl(i8p{Xy zC}<$#gRae49{>ZC+DzEcsq(zzi?BG)LWlSpzC#NhwGXA?O4L9oxqxV@| zmW82Jkr=S0X{imgG!xT>Uh*QE%p7&}Tr+o?1D zr{1a=X_3BSRS}s1$R6^37-37-?Y1gPT5n%wVbB%puW}kDFT|qtjT@i8_S);XJI_4* z)UI7SZHjyF;DN^;`@-tgtHrluzW!A=2-qAxbjXVo=-skq^T6y`=gyv4JhXULxm;%F z`9o?HS8^7oFx83`D`|^a@6UYZ)AXEsgyD-8EYv|kNN)l@6+C|y82oKu3Wut&+c8~<&@BZ^c5>sSpg<|_0mh5UV3q( zRr5STe&j8;-Hv0jealux%$Z`pRizG;O)g1o_F8` zdiPi~Hf5^STK9kQ6Jl>S+<3F%Lraz|9$Kiu-w{p z>wEiqInbLnKCk19M#bbBi1MgPm#o1}1R*La#zcFJSCQr+;b?bJg|a`<85$R1rNd=x z(;!8|DvU&YlSjKIO-nyZRSq6LcJQ$HQB?93zNSta%ue5VEe3|JQml3x`Z`rQOTY?P zJM4+yG(5&}8Oxi(EwNI3j1{g2^(Y@wX^vS~hJZ?GDgMRi@l!3rKW}MH%u{uNF{AET z9L$L)YvI>LF+f7e9HnMg#>=l9(?blI2+hX%8G~|xc$|TqP_Qc@t2C7fmf_b+m>Ny| z6kTOD6O$*Fu@=8_O2QHCm7_N;&5~Rb=FqvOqt@I_CXHt53rTrTK~_DsF&t#Pbf5*M z+lvL|>V-lLceK|Vlt3m91PwGdr#s|$EHA_72e(`13#Ky?+fglTVGqj6L}SoLTW}z1 zr!GhYT4un!#5-YHyz`ho8KP#`$-&}uV|Ze%c0*u5GJotZ+NoR87bI5*!H3u*SC}3^ z<$wuRB+k`b1~rMnzPNegf;kI>c?G5QXZ+MumnKo8%aR;Hf)g$kGh0iDtctxfCK6Ez z5-Rc({b+U#Rk58B0i>j+NXmt!I@jW4SW&7=l-XzvhG>iIrM5KO$^dT$LxE^}iAVCL|~exyB^I4H|i_>CnEk4uZxCUxC&-ybN3tBSa0vBDJvqhK-yKdKs&Pxb)@`aS^US zZgUmskpkcmzu(1-6p2?64z#=;g@kc1!9O5#b$D0+T0(W(1xoue2+}dDx};Y8#$MiH z`r_!c%#_8&F9i?5NZPz(+nM3vl}nZ_85{zL>W9YC zhM*Ye(W`u9&ad=N1_cA7uKF%mu}5j<)*#yDUCW%f77Q$L79mK`bscB78J_ZtJ#f5EZ+HxA*%FA3h`}yKJuw zuA3;)f`z6RA%2PXrpX9~?c2Bii6>OPJHl)j1VIqaH>lubw#bmuozC@fqyA7YdLk#Bz={cKo$8vc4C-?WW*OZa7S({fTzWk_PFdnd0SP5V2=lS~9QZXKLAU}eSd`E$|GYJufowMNkj`HVKlqA6gg z1PN(>lgk2C#Shp+)>5AML>w!+&o<>9DAtc%@(V9pU9m%*rWU#x{Fv8jPC+gkd>e)E zPV6w5GDUwNVxM@8w3=x{Nf{<=&DutvnkU>O$`U803Tn%YLNlZi)4syu zik$I{l-7E~7E;bzZz){zA@8^(MsG0zqV zvupUshK)h6bwnxHFu~8{E2P>-@+70HvSFjuvze2MYRb&}@Sy6ix@C-#bdYMgGS=%z zE~6}uyD&9>p^KJ`RfBu9R6s8s4h^rj;z>a*XzAeHunMN(YdDFzurdEyo&d~BGJ7lM zm{rCl6x90bl!Ll_VYOfBU;Sd5$-6!2>lgP?IX^Xcq!ZRSGZYC9(bMFNrg;*IONce- zCeT2hG6!9q=ApVjirZ92|@Fu&asY>K=02eRlBMD^G^GeSn@ddV2B2(&c8xS5NY{feflEq--#+=E{TBz2{N zUNJ8XIf;r1b*CNC`LxRjN)s$!t#T?DZn_G2eufm;_8-`1o2c1yX1iwz0knw6Tisrr z%e3s2DT@~`b_}q^-Dl43U9ez*t;sjNxRF>ax;lOO?2es}+h>7_f@XaZ9|*!;>Gjh> zOf$a#9{BvjRy9#z=^;6Qw*}7{IL0!0~TNHV)fM1Pg-3b+i2s{J{zIW#=q^` zw~Fu?0HXtNpU*%4ELN;uxr(9Lv15yn|IRHhjhwgf72_5I|J-v=vuw{i^@Kp$^!}k! z$B(|a@mW0n^aBU%;!9E(PT8`c{?v(2eezzElE{h`D-YYke|WpScC{31vlyR~CyuFW z)oM%o`ws4R2E!S+y|j5F-ahj1Lnh`*A0`Ui3c@|})RR_mojrNNi+)Z6<7ZOg=tmkl z8^<2nm?=FY#-?*lK!FFAR))6dTZSxO%Y~>58-gWhACerQvy@8&;{7AnPraa7%7iQ1Xgq zRxQMiMkUhibu!1_iireM?RwCRSoLP1Kvn6~%9o1o9cYThc9drwt>LDdGOLjqurdIj zzA$AcITQvC(qSIpX{z+ovLpzAWi>A<;8~lSS5%|ST5y{>$)2wMGA1i}^;q7mNSxVEt?(Zyyq~QF+Sza3lmfK?AdYb#G#&kr-h62k(mA^I-D@k%fhxcwa{C zCL)KyworiH5s811UGB2In@WjA3E;M9 zWV2k}E%F)xKcBWnV*~ zmRYmOBAQ_c&|W#Da1~+!f_$mJNGHWGODNQX!!Fs&K)|2hqrqgJE;t2y>`6l-dCr;agt-VwQ$n$OH%RQ3|5l9!BL zJQ6JNGKXFl9ChG4)(FZQ4IepqT#;;KX)rT!;_*`_4jkUEuABpvdNkIAT~wNpopR}8 zGIbKA2$$e5QR(W0%4$Uxu_4KTz>-}@KQ{08IoJ3W`#!NBd z0D?x`@=y1ktpfy$*8oYTGp2fGIQ2X^Gwg>g%6fZvjX~U_!Z~ve7J|bQg48dr8)Py+ zlemJT%Dek|4Q4ORYCT{&IGA!hK`)F;h@fDU16#VLdjDhYyty^*6==wuGBxO#*4@{q zD38Q>JDAqh-xrDr2={E_uSuBPoOXVSgGVQKWt&`Ou?es60Xdk^*V8-IsWr7wgl&f@ z8)+MjUVfGXt?LCtY{xol$Z!rLy~m12VLS{oi#MDZ$jO-k?!PqSf*mWYF0biYqJqEH zVh+TygBPdwPWH&ld|9;|qY=3Plf~K8Y>EI6HbFtDw2L-OO;uS2Q-`V3_Ox?pDJwW0 zIi_v!+lt3ACpd4}oLw!9ZSMcn14InZe146t$jlX{R0J#NwWP1fCvjMsXNTWU-uJ1u zzx~@t*Y3AiLHi?-Gy_3$7Xf2uR3C%z)K5&qJ^ytTKBDXC5;JF?PAg#>J%5@-a{7$w zhs`DI+&9X~bx2du+{=a}F9xVpW>skTfk5e_Y+1j87a#^L6{%b%HcXdTyUy@U)2QM> zD;{}QgGrlAr_0#IQOFdsW~*pr3(M8cdZnPPU>T`kPRrvCGQtp+N9&F1Q>k6#y~EzCvgC=80`%WAvdIP>E^f?J&JU(2Bv0ih`q! z`UDsJ*)wM!J#oCb4~m%4=rf<+XQZ9|x zKFQCt*aDA4ZCI(aob0m~>OwdM$6rjTFei^gh(eKUCRL*YYyo%0-oPjo0hT|~ao?DL z1>vD)7R1s=Vxt!r&KXo)^nP&BnVt{fj2=u{jD z0ZU7xPH9%5B}JMv7~yQK0`yEO+4&a`(qTZMIbQy{gApG( zOsKZn)1CD_NP@i7$f#7B3s$t~XzdR~2l0dxFuHJHu01;deH{IiN!a64h|n zJX2yvz-($}f$$XJXqea>bhInAz(sALH&O@yR0ZRJiV`8hPx7D?m7%EaWFT@bX6Qgz zdipU4ZoUX78q$D=%!F=SI5(| z+H|z?5E>9}a9@VTw?Z-%fIyw&jF)x7Zrqg(DymdDD^1vhl0kmho~NV}&a1mDrxfi7 z$7zEWp3zni@#*2UYBn(LL5gqLBb=0&vV14wPDY zjAhxioG{fA|1yeHDroM^v4Cm**^N?%NGaJ~&&F4O2Ztb#H;q6{yOj@WT#xPQh#Ubv-b&ULu( za!)r?JiXu9Kr?4q%J#|o@Bh8u|BxTf{09GPaYZmhI(Cgu5w?WR1H2x~@XCDKP7Qg# z`YXRmuy?-k^>YRWm<6&?kCssSkx_T*jZaepR+5MX$<@$lSYb8(N+dq&Y2DJx;w~9e zQbaP9tLYz^MlS8Ael$;NV~l7gLh~~0{uZS&31*c zI?=ljj(oaUH5CtYB1Di6H`>`U7pIQekTy%YLUkHXNUa)T!gwd@g8;(g+z5+^9H>bh z;hL_+6k+~`dKMX+Z{NLL&juel%q~vpI)7HjoFnkcCRDGZU06xLXc&1l#-2EOxMzC5 zoxHGATTUC!NNZ%WchUi=aZ;pEgA)d#_#gA3Wo6$k(o{i!;RW2obh8E_MkO5G5vM6L z7wT-IYM9HEpiN5FkQdSHOWGmJV&z`erHyEOY`XnS;&yvy^W_n)q#gkslbCzxQckfM zmlA3W_pNOUicnF1@jv2I3u&lWS}VdPvbK#Dt=?9;A{LO9NnFoVxNfTYdj<&m@(Nmc z8nJ^wWHvXNm^xS&k}dACgYl~*Lm^W06UXCIatv{(>D;83&kdg+9a566NgL57{xY*8EtFf592BmE{G%a{_ma99vCU%ATwCh93isAh{XuGx=F zDy0RrGO0PjSCjfIXo?k~TWRKzQM*s?Ty=%b)wQI;s#_u1%Oqt6zEbm`L1{f%>!(|O znFSA&iXj?_uZfw)EYX#yP=-=b56Y8%Izu-FGcqg}8c$@HKQL$ck|9cn%+#2kCdR@F zVq2wb+PN)wq!~97NnFn>oHRS$txO;kkaW%aV< zW~mTnW5b{pm%_XRqo>jUj$2c1v|_L-xdl}4hzAw?l@cRS0$-5pVQE?p9g&MO;c0FJ zO1)TZeQGji%|;3~7eN(icaZN?+G)!%=MiOu8+&rGn^lKie8I*n`QecF#7od+hH7;@ zDWz$rwc|jd<2VCe@fCpbk+uz1*^7t?BGbA*%Dg*T08a{u|z4!jBfAjA^v+A@^4dk!&RSwn{E2%b+b`V~?f*1K&>?Sbyi$DJhpL_73 zcYV*h)?cx{hRbOi2{|R^M1VeORZR?TwXZVhnuLceek@Aqs{+*#M(40xDh)`ZpqvFK zt&7Og92+f3_nWRW7L}}RG*_BVN8RH>2ZH>hDzp>Df{%c<1ZotgLM>%npphcgL#RWW z%&FWpZUaI+Q5S74gas9a{P#%h^&OiP1gkQdR_!OKBDU#J0uY5#Bc$3uET+kYVnexK zS+XBOpq&6%8bO+{Hi8MRcL&{1aS z;+#wk@{w$qUqhu}S@xyE_}PO1kXRe7nE44b^@+x03apC`i_GOu>6S_SkN`q?cwKOP z7_?>M+^LyY89p;LgPSa$9334oK=umylxaCDJp|=67m{Y(ObMOWhE-kGIS(Uv*kE2c z7GW^8!_f4n9t8!`Huul5%c+^>?Cjsh4o=G?VEpqi-2Ur{FqMjmmyF~cLw+9awS|De z05(kW#mSi}Z+&CwOU9|{R9U!GA})DNn9|)f?d-YJ&=3z`d1PX0vP*6DwaS2=R!^O3 z;ip#6=3KA`4up{w7l9{qnh_38rSFa`Xp2cI!I;FisxQs9aumsPn4qwX?GMONw!xPsBE z^g4m5!Ky9~!|JATfNB1sCsimNVJeA!q%kH4VQ;0Hz0Et2qg?4ZD}=83^3EjRh_9Clxh!Y%@zBOWr@x<-jwuM42vklS z>zUTOV)0OaZ=a<_cvEaidfFb2=B@3-@yoRhyjS3Z|!<9Afy~M8$8_U6< zeiF*y$M;Xq5IYh}FL(xz44$DKKR9YqHY56=)5eEb{VIZ^z4#ie8@aTzHqV}zIKt6BUx3V0}$i9iw?-?l|lD_YL*}fRo50_#eKB5NQU!r7#Nq4i$MTG z9VJb*WZ5xHqVP31FJ}=~fmA&v>%lZ)$X9QhW3AKFlqZh#MDWX|p5`7bZHID@fo0e@ z!CPEe&5{sTc2_AmPMW}zk|7;m$@s#A;g}7U#TxJy-@04kjLwK&F#0FYmyi}D!ic{*BcTz7B9YY z;S`!9o|uM0GE9t)Nry7Jv5JtusWN&U83NYN+=H19d6TFjn*iCOFujQ;9qp+iA>aKa z+fZ4QSA9bo2Vp4X-3pC7gVVHp!y!eCiXzd#teKba%J{kh^;EVhO_lPbgN#**VL_2c zP%M-hxuGIdmnt??3vv_-nlhQeO4*WgGN7p%GbW^hken1hX*gZidsK2vQc6+cYH(F? zNRXmP@M~bvMaz?z!AF7jaFXfWMT-~Ps?mb6^XG;gg)uGD^gSZ%aV#U9aGBY%IjdQ7 zYY-a40~@J{k(69Ycxii>T=Jsqk%`*NIga5fucFAWNu_E7md$ANsiEi$50%orU2s19 zMGIBZ@hc;}u>pG<+@N2EAQQgXV+if+3NE8rJ03olbl&yZGmbTdMi$J{6ec6&Gi8w& zVUM7(trLoZz!KbImqr*4mJ!AFxLtV~o+?NY&J5?(PS5FsQ&u3Pk&{yLWOYkOQpREo zYktyGO`WDrrNfl)j=4;$FWV(+Rgl^{_=RPzn5QT?t7W3oGKikO8R)N?8c>c#b$0hZ0uv&&7;CJQm6?9Q5TO|fZkJ7HC)U*6-HzC#|l=z(tC8$=^ z?+jq-0Y@r{Y@nBRf`Gg+XMJ}C4r|)Q=zB4>3W(D&oPgELWI7H{Gf-G_k56d=| z>5jgpEmsAxy=4Uy3_pE=Q1&W!K@d_&57_D&+^7H5VIW2T02}{FL_t)hRKeM=6w1?> z$p9^YDczpR5(fFxVC!+nDVUTGT?H!I6%8#Bs1aH5$cwe|w@2h#O*uu>2nc%%KWfv{ zZJ(ny)slG_cvxE=;K40fLv$_b;M`avvH!pUcGZ$Hj<2%_am*U5&{g@>hs%0Wk}~1Q zTXl2z$e~#?2m1PK5_TMGsH=_pPn|m9xhN9VI(GD^&pzu}qptW6V!#Qu8P^xpO=P7= zs)L(aTC-7wpJ7}UYsOuU;VM>;S#q|{x&#M9#Z`Us@m3#m$1QyJ!3S@>^)?eq_uO;u z(q+rmtX|DcdEnEZk>@RMd5ezmo_jw2m2keMDbCp5zklEU{Rc3Qd&9d@9Y4*DZRoGR z{`yaR;uE|p+vV@ux36JhmsjT+ee*YevtiF4{^1|$*Nr@P zg!sd8dj-vucmL-?`D;FGeq!9L8sY>>VaqvJe{Nyls_u{hjW45T-2MHl;{bBO=K^=r zvYwuq*^Io14}InQ#{cjvhWUj))gt4y7InM9_{8TuxMm((!1Sd%;HW7+FFXtk7J5lyb2 zC2<)NZ1I)ooh>qw7uZ|@E&msbS8C^@QxUiYywJG(`B*kk8l2>FSEJhw+!zv#9JNPA`<>`P4 zY2&qzSM8?NY)*s>VqpqhiGX!R%@yzgE;Oa4JZfJJC>WtST$GSv`Kn*}(a7+$Q@j;2 zl2TtpBdAf;1x6y1K&R5GLsFFo$WYDrEl=ti11lj(1tt&1 zZc@BJr19iI=W6UJ_{Ghp60%n2XszxxIQ`A9f*C;3w?J%Ftxv2wXhyh8EuWY204n|j)%bP5!=tEgY~G-WEw_)Ys&=(+7kr{x*{S$R@$tcFhM z%GSZO0Jrq9T$h(d>xJjf|rbLb5EmYD)>X?lGr@L{2Y+s*% zfLS$j_a?Iv6zJ`AU^RZ&{M$R2O>Wt^%-g2;1 zXJ@suXNL7kPC`6$`iui=X3U(sZTpUQyyG2jc*7g4jp0-c4GnQDzVL-FeA~Bu+q2I; z`)j}UYwvsC`}iE6{`9B4vi7cbeb4Xy?(hEI@BN-cQ21)=)~&nq&O1dRmMmGKzqswT z+du$^_*Si2Wj`?6gdIJ4gyS+6CLCi~lX z=}s6Vm$97KF!G3iJ0nU3<&G!qU@8zSNKg~?lpHJKN8Q?^Wf zMB-m&1CuYjY3#52nhW;SBy&NCBcJ@Gc1bY@AuR*Wl5XW9T-iERb`Z=o!DaYC$m~=+ zX~m;L85NoHHjVCgaHwgzOf*!z(IXYtmst|}kW8U-oaRz4rFCOLsWKyxD$*usT8dU` zrJRSUwQ4TDh454qWQw*zHPe$4xMKbrWqFD|`8u`cEACViMB(`rAH|SD?aLprJ{_KR zo&*dr`pZ(aBtk!R>EZIzeIbA%@Ad`#FD>5z^A~&--~NI;p|SY)CHdIW8pTP(u1w#% z3gL{2>SyE#u`xa;XQ=RIS$1kn{=Y4kPDIy(`#gyZg@Sy<5!$l=*Sp)3d%Jt|GuF`? z3Pxe@AREynQ+gLOU*4R_RtQ-#<#DF?^xM|FdrL(wYB8d{vQgcX(G+$PLpM1%$@jQRoo6EqeIV=IMsciM6j8RPOSS88cQpPGsZX6sL)m3zWg$M#}P-4k) zjpctOzsG7wn`(crB=oO6tQrSLvXI9`Oe1>Levw;^kENDn@xw++oiZsM=Zc0t!fwh7G5xkT9MR9ku zZw|yGA`AG?g%&NCHq+EmZqJzUMe0~s?Sy6)2Av^wQwtw zz1iaAY;C?Oi~VRisxVs04J%>_UIi!{+=fN~UVc@;y?V_?l4q$cMLJyaqf%7rukk%; zjrT$Ma(I9F`@d$fva+>kJpiiHXP9QLt=zVb_AP z%*lmN|It(W8y_iMONR9(PtprbF&W7D627uMYYr+FyAsTj%erm-ct#M}R=r4B$)+G6y}G$|NZa()^GjRn{U3ktz`O{pZS?>+qOOS*kfE7 z&JB-;Pki^?cXOn^YLLaca_JywesX@-cYT++OP);@oQ+BS%VZ4`2VirmIB zpaLjPp+_J1zz6QR=bp7|*Lw7g-}sFHI(Ym0zyJG9`MvqgZ{D(H%SS%)5jaZ}Ig>~2 z5PtE+7vK2CH!AKCkF&X7+)Zq3^Niz|#1>eFpJnn{^fsQ}B3Px)Bx<>3Iap$1a$o@z zTgmYPe`ju*uu7rJ^NHI;kBuDYb5=vgv8va~#AH3GWx^CO8Xe8{l@(e_B!>;FX9;Hv{z%B#}nJOD=Rlf>i_}h5< zMbPzBb)iVshDb{UcPwQL$QL1xwir6Pw zUrJf?JNCca9n2jk%Z&k$4sY>Hl{4SzI#SyIyhod_u`9Qo-xlv2=`;Z*y(+FTk%iUO z%cTY}uc8Ey+=TvupIgt%u}bE0hJ3lIb87$tUp_@zU2apVFzJ9*br$K$4_Y*{c{}y9 zYV-t@y#JMogK)bFF_e`H*s4i&Uvx6W*O{71>kOt94O@c*u2#Sgi9s*v!Bn-Oq4A?r z(bw&(9V+g89vH8<~@$m&fgV`aNA=(uWs ziu5zF@%nzd$wZ?A-`4F`Q(1Ln;?E}+iYq4FA}zkb5C8BFixO?$zFo1!ix-=Q6VS4f zNr%bpc<7;rB;Iw`U5`Hc=#4ktC|F}&Pgy|3Zg^eI@d~A1?c(uB<_SIdO;%*FImWng}pYY0zp;`MfQjhu6c@EDrB zh|X;#MWD54AG3tf?z`_kzT1%_N1AgwR_O1-H+vMt%~&Fxg8M|Hj3_FEBVt95-P)4M zvy2k3CLak{`IinL-GY)}(A;crKwrYXGWt<}@9ae5i>XxJAUkT0Sp@DL{@oj^<_3WIj# z=}>@mj*_qx01L<4o~D?cWf~JMCp74cU0)3=Y3Y&10m;jvU3QK0O`8;o#_%8mH;Cr1 zRiMi1{Gc3Brz?VIZqu@jB_SK1ke%%Tf^Ua)UuT@h(EBTY!xxR(f~`QZVx;to z?9vINt&|yAu`uZaTC>mq%K70w#D6gUPc&)nL#{AIiMjsb>S#D z@t(Y&Z|#>CHchKcwS}$#S(@!=^?|8WRuqqV;}n% z=L6;T>f|u-d$w-f%D1q@ioYV;&;8ub@xZRR<{D8htF^x28@}P=AMY$_7ejj6+umjx zkze89LNTPT6dVGXO?C0oM9WlUZq5wd8{haw82-{P{gM>kl6&kL7XSp@!$(eA6v*(p z*S!uLfj7&)I760U35l7+lueAvlJ?*H&EItYgFpC#;C%k`pWnQBGsg>Q$N}f}+i!>4 zz4zX0J(dZ?U;p)Aml?twM>i~m8ZSq_A>gD)D}P8Qd+y{h{hTmyaOq0YwS;ct^UwVB zPrv(J?|P?*oTnQ%ZnRj88_N?!PqNk(MYxQ%%kSlJNeS?;xW?1RZ(5f2)2E*be%aG; zFyratH-E$8Y1K4s+O+92o#m7>4=g5}In!#w=~kjZN1&>!yKV7fucc@=yUu&XLCW4< z-{6#K4D>QAwx=j_myHMWR>s*gC&_r&`a=85j+`^)eCqhA)1sXe_Qa`^Cr?=kZxMVW z94+ZeB9nZuseUj$7HN4jPV$%CsFX5|v*>@48(X2nlQ=ct>?c*WJCZH=G9Bp$diw z<6Xt&S^lTKFMHi-gU#)UD#kgX-PsL9-{2Q}ozAc<2uVkZK=2U9=i)>9d6rt436^U}F#;{77`;4jF5O*>p zEf0VX){?SQQF#x60I*sL=M)b2+6(H%_9{{;l6sX7IE4d~Bb6I8_%4>%UBWLgQ`h*V zUtyrK1~E(w1l*AmfaI}baB%Z8mH7-B`3t*vYU6OeZZ|e@cTgNJkpv?~^v1~z+XxZG zVNFo%T=^9elH$PJvE;us-~GLIObBskMRSNjYk3K74eLqJ>sAAE24>CHF->=1j>{@{ zUC*@ctZ=vMf<^FNw2HHpwnH5fGAISfD-jM7vq5x@kF}^S{atf$dN_vh1FjS2EzE?LTg3>Us=jz zBEOAH?M!b5%HZ&qHR>V2yyH&xq2**ggd(DwWO z-(yBWqE&?`6J@q%d7a0HWzUR_BWSxBE-OT1?rrV;oRH-;d^Sk;%hPLJ$+sbT zlIbhnDzwT_d)y4?{iDZxyzy>-$M%J{*AMq8m%sn^>*+5)|8enoR%$qT#)jlsJnaAW zKaB-Ovu+CC6O*8B;4~|~O(B^{oAS>ZiNc(7MS>`Y<^K7|1;3{8{1W!iKu}?hSgja+ z7o`-ea_!*9MfI&Eb^<;@en@nC>S5`ONL%`$${t^d5iFHq&f5$+#{g%q$fGYuoH!~F$=;f9rBDpPE`t={!Ktp%r zPFa>1j#`HHuD50st|^p!hX444g~%EC^UWFY6=Q$GP(N&U@@hk@Kt5^O|292VkgOWY zbnN?go0}Up>ouA|hAUhFef#$C;E;PnlawC2YL_fAam?PHz0S?}#JF~*ZSLOQC()o$k)DYg3^O)qhY)-kZ> zU4CY=eSMAP4tmj1H_0Stf8M)0LR+Xn zh9}QDX^U9-f|jV_aqY*@X#y=4JTqIS-= z+(UEHT4*4YNyj-ATY#C9VRD7c0|*R%c>S8SA24As1R8$B(I2qUV_Ya>!tW(Yn`~V) zb4h)YJbJ4(-lGZ@H$IU^dcRm2hF&hL)i-IH2^Jo-?irI!!GmBbqoBZMVvQ;tygwqJ z3@4R|7|X4votjLBUg~5QD8B_%94@Odgsdn-YeYdvr~o^ulocH3K%7a|EoJht74i%b zziR#Ol%FL3jeI)c9yv<7*rRhl(WKdWVYomAZOgqJAS-|l;V_h0wJilV?5adCt#gUE z0uKtTU=pEW(~wRviP5ZVy2c6{DS-3F0~d^Uz%uwidK79jH4KD~UaXB_3N!Vwca{1V zIsi`f(=btC(SmH2t&^uN7*)#JTF}zjlS!UoSMMxoF%;J-?**6n4=zddQ-P!a>a9qC zKbAFU0;`2&$Ob!VM1aC1(O%K22K%fJX$k%4Gl;uNSgvdjtJCq1(GS#pJt=2%4JVug^WW-LZPAC91EiWg#^6P z*oADkKWl|2k?61p4-+!Xv2dNbeMpc$bN`7v8m`z5?kf%zIc?8fqtLRaf@a2VFu~!F z+SHe$<;1=cpFBF)5Las-N2?eR<;OK_Ai;N4JVt>r;_PgA zrO-jcpX^aasFks!6~l#u7zN(B6xQ`MlRGRZ#PW?qVqn>J)wj{m`WFS&P4!11q~x~# zid8j|Qb;TlU)9hpc9jrF-9us?@CP`h0FMF|d(!ZkB*mPBO@wfA09HB)n;MN9tw42C zV;L8<>=y+zu;v#Y6^4No#~B0~c^3=FfyrJ;y}Nr4Ff0C1F*#i(<}O#%Se z>JGB9TFSDr5G_wPdk1Gb06^(&h5?$P&In0?i8d_=T~12nKpmVyAfb0WpBI$(rMif~B$oci)@`dhK`p$-CXp z^V(l~jtuCCQKC|QLd8{hgAtE^>??$3VNZ~+lfi3BT#KgFkN}1R^S@vI3Fb7GNihWg2v9s)wYY}EZW)?k)9;yKnwyo6B`JM%L#2;Lh|yV$ye`#8*n)&n}(~hF!j8$h)zk zP7z}b5GE;NfoKsw*C0{eve6U!$UJeK^de#4M@h>I*{kL*79qotTxlw1?igwslVm*7 zGL>iCDSL_o<@f#U4Yx%v*n&?3Ayn?t081p4szXcEVbra}wNi7xvhNtc00?rn6o@Jc zzKQ=fT%5o!0VQ(7H%3lMf_E~w)&PVF`Qt!TxzkUd_x(G7H|lpfo5%)Fzdrb%oUzs&)e|CM!shfA)z1l>j-0wjc72(>3I7 z!`x4}{ddpz_y&)pP2tXELK;F*3_NrL46}&bxdHk57v>tMW?GUk7Rf`4eYDSEIUk!P zf8s0@@lN*RgdqO}?r&~%y4xEJdq&^u+eZqh+_~WgTt-A4vuF^d^G0nc){%>A_i6O` z$jN6P)tncf2?R2%ucj2toO^YRc7i`y6?4{|)FLlC91Fi9aT>@0U_Fl&u=%lU#Yki1 zk%Vfndg{y*{iLikOGy~8dPDdIB@d0XN6($|V}a`_)kWyi267mLAtDM24(tcj z$sOarj7YeFgenOWgfq}PC^6AV6Ag>mVrWNIDX^uuC~=m+P~wQk!d6|OVmd!9MCl4izgN+*1R)2@M?U945t|E zSK~p8Pw=Qy;y@Yewp|8R5wQS2LjSEq8a; zW`-ZmT1;U-_xANKiF>|K1t`NZp%{jk3;}H#G)_x~h549)iXkuTL|bT6AVtZRp!)64 zQ!_Otd%}H!EPNq+%9x>;{Ql_vAcD6gn)S^7X>`f~+7jgvjiHUv-s#?N=ha`U>eJ^Y z=Z{G55YHSpp&ExM!>W=SPRmXVv~8J>%;29XJuNX4od?NjxVaq*XD&C z1$}s$!i+V=SwHErY0g8c-~6v> zJMw};f{AGxUm0_aw&RM zclGKA;TJq|vc9|?J9IKtvgc%FWZy*PsGX<_G(Kr$PnzfB=1Z8ln2~-1eOqbOYmN44 zgOkn0j@az+8bGi4YIxR@cqsbF=_wk#)gIugHfsk73+Ho$hZ)}m4$Kzb)J)Cd%}>o( zyk9Q~UIGZ!^2G?k?P^yx(xp!f^XtrQP;69(em;9M1{P@Ki|5I6YW`OHt#*BQnPpkY zv9aeM;zbS5-13z0SHJ3C_}esuQE!vpF56a*yhwY=VMY?2F;-!jZ+n?dkrM@HQ~R9# z3LXS)5R4OI=XHX%IcW&I%zX*35pd%>X@J(joq49-O>Si}4y3wd+qJBnHSSWfJ5|b6 zx>e^?6&N2GomXL1A61u%mOap6-t%%w_KMuJgrjaemdZIqIH&{JYvI_lQ}@ zS18-0!z8IK@=ea2!u63e^(&$`soMT(U}&ozan0;V1cg8fANcKv=_y%rsL zoeG;xRL1L=Xun0fTi6=2c!Wpj%Y=6S}tw0^bE`ae1F|EI?jZc>2mk(1i-^m$nd$IU}uQ(`? zNkLKGKwdI28_dVO=gmR?>hBSFx&``2@IHDF(zo{ z6naanK#cNM^JG+%l~%H6B^ljETUo!Ae=KJ#Un(`$%oo2r<_~vhnZwmA)7;mTOjb}* zRr{KMmGW$)X79S}d{(IR8lySvGHhQ)a?{unuM>Z-{h>+w(J^N8QfVE2%3#ypH!c^3O}4A5NoN zZcKYtKlzuQlT5Wq7V-su1=>pg4*L|jhZRTqiW}2hw*lTN{PI_gyoP*b+@o=7bIJSV z4}LFx^|^(0m9x*Vq-2=>Sv>m6xxTsnw(H1H)Z;MfeW7aP`~A_p(a|vpE<%py*AEdV z?48dH3+mc!BEQB?u)k$zWUr|{Gt+A@cJkT$9__?b<6t^wY|?sF<1@FE&*{s$USH5^ zUe{Sk*Z$5wd>|@=QbONe&%FAq#&H?e>~T0KlpB%D<#P4av}*vSx{`TqGGOB7jJ39C z=%Q!Vncv)DdACiC@m%9i_hZqkvYyHM{K8ktuY4Bz=kuz3YO24vG{2FjHZJ>cw361k zzqJ<9F>#4|`IQ2WV|UjrG0^!^vh=~vC3yMq6Y??grKHhg<1fgzr33w}a9LdqYh`(* zNK4DP))#ll?#nc-)nzv5-WaCj7`F(;uQN5toFL<+Hdbyw9DLA*Fq&x zk6qh~kwhJmQ|H6HdH&m$quYH5a_@IEp$QoXc$yvRGqlzTCWe z_dMeHdtZT*%yqtYgNNK7Za8MHX3-~Tt8T&IyNxT^p1%F0og|4@q>`oLT*207C0FI= zbBhItg1D(tv7;caz{jG`JHFNHLY=k#zvi9=1vg$ZUix&UcTnAUt*}2Bv^{#>R#r<^ z{8>9{UwXEd-nkHLa-R6`^Wv`mT2o^Dx6*IJUn5sKWBavJzNlxYs3ig9!S3uKNV_vk z$RU!xNK&e(AxKjR^Q;x;o0ihLFTY5G9trjHY0w1(bN#KPaRGVLx>D z6{DZ-7k+kz%J%B&0Jf(vIsgTU0D$}yLV6+q5(NO|pD+NRf<*b>ur?CQzwl%C!v?tg!Q2;hHmAVUzy{|N)d|7PV@`9Svc!EjeH@&W*`2>-rFfSkOi zNFb_%uA#S~x|*nsn+unft(&zSm!FIKUoL={pXgK2#m?Ib;^*S*>LuzYPWMj^(Wmg= zXl^>lKUut;#OVyxwIH%?o^}vHE+`k2P687Gfrxq9+KXz-Dg2xL=}(*v=I!k+%FXTT z>&xZK$K~evlAA|FM1&j4%gxKn`ILjx%iq=8%8%34i~e7Q{HGi_J1-kg2X}7=H&@8t za;>c0e7wc!=>Asp-{)WL^mefSpPF2~{w>y%fZTsmxOups-2X2&J3oj2AGW_K|6=<` zu76c0_O~!mO-~2Arw0FRmIRO3KNbA1xc_OM*xyn_wH*BHoDJn1Tm;f(!W{7xc~Ol|Iu^*8kT=zpToq}j_KO-sga^1Y@W*P5QV{{q$6CjKz`=McAuw>%a(d4zS zfeu#YFS_F|k?Pj%p9X|JH5;`-ySzJvAwXVj!`|5BEVQEC{#L(ciIxRcN&<8tiotzm zX^q8n&7kOAfyk60HlIl;Dk`E+6t*5cVLhmrb&MxH`Wg=lRbqMVUc`nDzo1BjpQ>`L za5Tma7B6p4!s5Rq#UFCEucg0rkUYx?kJ|Y2$(~R)KWOaL(&{J1fj0iR)?rB*5Tc5- zm4sL|L#7fxitqMqhph3(%#63GR<7N!zGjeN`!-pdv>r+NhngdEz-u$F|Q3}Fqu9eq8?=g zm+=u&Q_KraVN-ZLWeCa8~af{dQZ%&Vh@TFT7XMv^4T@;v*1;oPf|>bhadSv zg-Z=Km7c`+FIyWgnz1ETk*Vs|^$pY%wcHwW>j?!MiohAS>qf;1tRLw%&ThlUnvy9d z2t~Y$x7Sc2HKm|4B)SNnUsT2fD5IGq`kh6=06miu;s zkol_>yL(K&7sH&@Odlp}C^Q7##bG4Cd2YM%55kl~%O|5s;62d@%$oktJsQF^`g<4) zQsT$uk8v?#&qQy)a0SXpu0sF4Z03CLS;@z3Nl(^Ih!M-HCP$Mt()^XStsMVo&S%Gz zAJcCr-*(y_ik}cdWB*|L7(M%DTQt0$crlSes9-Ghv2+Pb4%3z#w_z|gK5|&?49`kO z{^Q6N^19^*KI%@&r46D1IG6Ne#e$q(X*SDqdApM7aiaQ0Wc($^wC95o!u5W;H*@Rd zn$8@VJ%(#`a@zmw34Qo*8iiBCjkliyZqFE1GocrgR)4N11WrL|SPam`Dx*HHmBRQ^ zq_Dc|$yvKuk)Cy-A70uykF!Tv=C2*@u6|(GNnjfWA4-UeZ$=Q}K0k(jyv(}@49=sw zQ_AWgtm(M=G9N_!=D1GrSzu!(GTL^jiV}B6SyYzR;4);&ccV za9GwCJpy_SMF~(_3dvai^LX8YXfiHd$op`?aV&oLy)P?|x8@}xTO9~=(|W0oT_dbw zJv)Jj?;~Z4hkn>O4+{kK^8CG03UdsBA`fg9?OP0cI%?ac^z&J-e|4iqv_c&G9m5%D;y z2uCDrZdwUDN`$B4&e5*Vlx*u(%QVZ5%+JAS`gOA%eCTSJsK^%%C={9W`{S7qIMDY| zJJYMQg!iz8p3QWo85OxlVZBFcks_Ch9c;a`2znvT$fGg6(rtHdRc_PfZzP{_acn}Ic7m_>;#RFp^F;y@m5fmY5 z&~AABiL;!JQYZq2;YqWzW~CxhN@6fV$r{ym1o|*srqicG0;Z+Ay`Q$%wzmFR}C$&7iO2eO-hL-0cHwOcOj=}|P$Jm^|p7UISi z?>MYU%bX?E;&iTH#fVm2l~L~A#~UpkpX>kV+ba-VNLNKuSiZIDuw}1YukGf0;#Z{m zt+hy?vs3G*z|grYU()i8nZd>s@s_-`jz{Xri{u~RoXi#4;<=8j zyrNrdl>__-<629kDHx2^3IsGxIo#xL75m%&sXHumj?Xm)NWH#Pz~EUCL5iEW`FW^{ z#W|@|ql5@8>5VfiGv(&KgT`0qsi|3pr+q&gCyGbL*( z+=hHpc@@E$RL7IfuO0hvZ|}C=^fDSE%`=_C3=HCI{ah(rY&0q_d4{^Wm_K?KHF#2n7&_{xMs>jHUJ-H`7ZRgQ#4A^Q# zhcpqnbPsDpzQZ1EOw&ef!2UqK#`~ot@0Ha|h+uY_;9`qvYKxK)&-{|U44%61c#EJ3 z1)feLh6w{!WauMG!dhhHUde5mN~~0DcV1G6xZ!yuGmfTs!7#&S(Yf9UYKJD%m9n4a z+U)a-Q|~=1PUBSL^tAnBz8Rc(8ypYS>=&^=M=c_W&KDr>twR!BkxLL z<5Yu-F|vpgdHs9Dq_!S@Sg!dGWIvucscM#o$;!m(gpg8k2~p93Pf^HWtf62|Bp72R5SG5FzhH=uW{jpUeh-C+h`$PtwUBBH2qt1q+oIRT2`392u=p zXwaesqPj(Bd_k10oaTYQB=e*1!cnVfkTxm%q*Oda_~w!$VWfHQs*P8G+-M3P#`!H7 z=Z^J(GA(<8#*SE69WAJQF2=kQr8pN+Ax@=kW<5^%vjj)ZUiCca zVDsn1>#sjqRV*vCBGZ0Xr+i*bMvsZ&Mv5r1@?@g`*4_AoBMz28!0hP|!~5S#luQhk zUp}lgh*X$**o*SVDOca8pmOv^FTPKQ(!l|*Lh7&s3~5Gbv3^pL<;1$x?+}JcXULL& zvAY?BTk>1@a#7jyLKBi4%O+GP0T?1ii=#s8yyT=~QmN~2+9D$JrlTxOBSVaShGe@J zPMEgQ8zmhRywOZi`ny|hY{Yb50FD5G`zvfXVnlvKXD|6dC^Gy_r^*yOaYzTiMz`TwD1RMj(HkH(of-0 z%J@1JG5|)$NGTeT$dE>(!f$zMb08!boQl=8M@@>I3F)$zKVn1^?Q%Z$dGsg#aR&8_ zMiIJS1ABWPrPJex)Yj-?rA$sT3b>-x|(dhb3^tT4T@Z@OIVBR(N zoe7E;Y>*s&A9gw>|Kd7DmlLGc&vK?80H=jmQK*mstw6#!#wzg{PUt$oT2|g3bsDwo z4J8-!X#4RP*8Qie39ILRAAcFTp%C9Na-y^v0_{sEmekNhqmGgHItsRkhzp#os<@E^ z6hOCu{`NJibho8CjOlhq3xkJ`8OH@lL%pGogYR3E&T3FZ3_VF?=5dg>mC3>+#uwpJ zO$yJP7uIR1LC8~sF3vpv=noraiQUdX4w?XL&iK-J7O_i~c&=kF^mBu*-^6}L@8Y<& zYObD?Dxwk;Tx9L(2xe9_DPrzlw0*zU-kgY^w4WFF?~xBeL4vNUeh-&Re7LkCf`X`+ zNQFERva7!9fm;V7cU(h`ZVTJku6%jPo^Q@Zn1gS}EP@`6EYQ5U%vy=OW2U?b_x(qx zhKIjiRvOeW5haOVkqKkRD6b}`q;?312TcdW8V;fs$~$e!Z%=DkF0k(YA}8PJ`5Kpjm_3pK7t=j zr#nXHe4G-M6Q{`N3ycc~y;#4O%F1(`bj}{=AFxg6PEOX#a~@3U$;-c>pm93kF(gFp z>9`F&N~tB4%*MHhNK;0e6L{%9dOOZ%t>QO!W67KS%6>2W&Y`1LqXjAnsy@qkK_-sV zYh02_mrMs0qVH}P4&Hn@^_(BbF+KF`T8xFe?U;9vxElnGlU%>urn&KXQj*Nwdr)kJ zBND)}QJa}Rz`?s3_ zKmo=0Npn1VbwcwQ?sl^#N8)U3Dca`P#N>MfX?zp*2zK|g`wg^1dS(3Se8%$lV#9J;GORLXb;6IXzxWjNO)9J`CY=^ zZ1NIz0_v~#qfMxfQfSEobE-y}AH><2yX7>2$aal;cL6@>&o`A>t*AM(dC_oQki(pu z_&_KVFF?S>Ww`!v|5Zpb294Hzp%w4Ah%ld|!|@}Wz*g=Y*#)Gtr{qhnU#e7@+xV)y z1ib`(d>8$cBZtOvg`2Ny{Yq_RUxr-)v1AdPKlIE74}SQw?Zmzm8+|~BSPNy?a@Z=S zbvM&Q$q&j~Cr@ZITW>d}A^b}Y)r?mhfLm``Oizz3)9#)~Adwhq@VY$}pO*1z``I6O zKfs2VjQ#pU;LGR*?#o~K@A1@t*9RvqpT7;(N?RdNgts_Bw|L%ZYlaQ>IQ0A}Mz2;q zD&8qVh!fb+?EiGQCQM!I7Ddz0*~te>-mOffW}39U;xs74dfkU31SnJ5Os>7Z`rM`d z3T?bcyjC;I&7!R_`17xVst~>d$f1Jhu-)&~eTlN;bS`pn>ogB8o~5kZ!jGYK!n;CY zoI^?*9&flUI;p6x&I|ug4WW|;Cr>}D_z>*Rur{skgQ@I*ImVlmJd6Osmn_yYN*Ev< ze_9~tCc}g*fak`3T%eMe3X}gDpje(!0;`hJnpXie-Y98XdXDqS+feVq_F%l;69_c* zP@xct`^G52GNu3@sC6^wp6K$(wUZvohtW)oO^fl2 zk@w}4#%IP-hvHr;e#TPi95YnqY(Aub@XxC#MQL=VfG`(HzR;|QFC)4g0b-njn4Gi(iP8TVmF zDD&{Km9N24d09d1awMtw-|cA+jXc$$##~(mJD;dxI3^k0Dk{YI36KxPH3}XCkHMsEpQcDZotkyhdl78P`P5lNS;GOXS?Tz@ zUI%2Eq;7K4@}sAf`Ifn00~O+dLMV56vz?SSfBUuT!Gu6iEjFduT7W>>vf$!dRBFSr zhpvM+v*h)v*Hj7|^f1TG#r~P+nzmz4#%oA>jHwXJ-F^!6fSfVdAy!R-| zc9e?}d)9@e@}NPv{9yTADD3OoHyCk#i3I4yEet;{kI^B@(jl|b^zjUeuhpwzwh#s? zAav##@Dv$PUc@h@W6Q-)&sxggcQTEt81#-r8P=#gNJoFu`-PGHQ(Xr0-tEeu9%=y9 z`p%L;4?kcBKEN+UNViEDajAmfJFIf>p&-{e`ZBFt6PmH8bq}GE84;?kNyYFg7Vl7E zpHo=P>wbn7_c1|Z_r7R~#4Ug?;a>G0)e#XR<-byI=dXAxzQ;O1nBc=OZXwk-f;BEx zNjn^p5;cy4j4I3edo#_Qu55s%cbg^*1P~5SfoCXV*KJ^1l8bO^Q;+_Vf(o+V_JEr% z4vLwXnGC-H^Q?7k$mj{XyVYFPFha=NbByIrhLeB2|8zh-<)ole&+%q`rQ9)`p3yM0 zZ$m}sEdw&!GBB>DRJ z+($-JVh$%XN4h(u4RePu&6DUIFn=Ng@^~4A;(CK+O}#%-8}1yI74B{E@?g@@3Rm^? zrWsw(zD9E2d>k193pMk}TK+*NV&j%an;8T?|EcL}|5Nsqhoq(NkoVd5&|*op=A6bY znvD}QCgqgPMU=OxLyR1&<9R;|`DS8sacXXyxP-{mZ{*cO(7A=`qg!AJ<4?dAc=(JSCja86Jn^Rs5% zj-M}Xy=x$)_AxdXffV=Pr;R<5yD(9CX0uvx+lpaD!Z+gZyQxV9fuVw)_0284*atff z(p+<`Z5SF8;qk0lb=*u~#R0E1^pLvjg2)O43dCK-;orb0m3G5c z`ViR%9ZwbZWCK|s?^Kbc5{$DJ_pyQ_R7VD^jT0bVHdb?Jz0<~7&b>{;l2;^U3^Lo&_q>yJW=!R_%Ht*%7|{E(okqEVdU@Y*qcIXFn@e&- zw;%oA>9@X@u3Vk1pPY!w==xpRwe^_{h{qc9!WL_engl;2JYh}InmX`YyFSe>+}7{p97JZoIdLR45q8X(_ms* zmKM^jx+VGJ@(!Nb%7qt{yvDP@Q=b)_Rm-@c9Ry&f;b)<-@Snvi{L-}})0aKmREkn- z+WPS7Ba6l4@feQdArFbgA&GKDBZkoha_5IZPvNvXEKycCK!N)z^A@-)o zP2}5P&{|E<3EbZ)ajPW4dRX!#15t5%m^^ zPBx0AN=YFj#gEfxg+4Rftq5S`MN%))7Zv%0MWx90<*+*hyRFhj6wLx=hl3KoL5PDy z4|lP&y&1RO!5Q$DvzvPb%KkECyqS*9r=7wLp1eW>y>tvGvz5yBDMt_sIl)-VZ#zS186E&j$x~xt1G%m+b!)$y! zOZ)<3!EH)iI$V1;6CRXvRE~oaUBCX2-_t0{h;)=uFc37Ug^zO{i2p43-79_c`MvG8 z8keNji!SPVTywD@5hWg^MZX*nQmS>hPA$Z*PB0p&GSJ%mneE;O&- zcPHWy`$!m*FW9z!3_g2B- z`pTX3)2SF?*=>k2m(F#yZ{4P*>R%(xZrsmE1H05CCJ$u5)S}n@csSH&Xr7}B^EmMY zqF86(ov;mlQQRJfC|#-mNJt@q1rDBn*K=^yZ;Gy?Dvd193!Wumk}(8&M(KlRMi-Wm ztl}}cJt$NdIFM#^%Sjjy-qBV!N0-YB4Hp}-oHwj}u)ASW1^Sn=g$soGTwu}~MlY6* zr4Xt`TSMg$OAs7@Qg@6on#(C?B*R3bqLe?=HhF|hf&SvOI{edB6&*if;^wTU)w;1! znsVmKE6W}(mZf_6IzdC*#kK68+s-~cR2?;LpF)#I5Z@9W6D)$Ja!qGt`3=x~EeD}D72N>)#nEspcG48_r z08FHG?_>kLWARl~^wKi^dMn$2ukI%hmiKzygtb_o+r*pI-ojXJGPgV}Z7(%Kj3GFK zmXj3rKvWk5Mt&o!EBPD|6yTdMs$5b~T;odYc)@Z>f%}gtV>zj4Nf9!D967{4)@{VvTN%L7{OM(?q~3Mg z^Q)aRwA~;lct(`i5P7U$Wn7(p@Jcbs`d@7%Q$EcI$?HP{+VZZE$Bk4v%U-;;cV-c~ zo+%JMV>f6C;D>9ouj7?;dF}k(Ld}8;T$-wwlMdS`atV!fZ#JAJn-gMYAiq78XbKdP zDXARc#F^Z{LH!7!;xSsjZYX?+H=sVO`N%y30KAAJ3nYbV6T;ACh^`g9ag=#P=;%9l zzUh{#^A}P#Wpn(o%d#KAx{8cy7lHF4(XK;wa=D(V3IBE>ewMSY9~8Hus%F+eyl#d? zNqIu~&2KMrttj&0T%|geGp&AVjEsUOYt{c6#|Odq_=IeIyHej!1G-QE?;1@Ljs9b+($iIF$*g7rVzZVW}3k;kG}jc1ELp7VA6(UKYgI=MEB1odW#7%ag) z2`0H@`yIUbCeMplA^S8DTw3qm}x4x=(;^Z<*(AX?=AUi7`R0pV3sdPlQ?8K!uSXD&D=3z@-;8UOlka9S;*@H z2(&PAqe|09&(LjWP8Ch-@<$z=J^}3bbDSQ(SYy0M-zFTzfe6Sn?ADC)CjRDK_mbky5py_VPe*U2OX%>T;o-CSveOf$h3-$||RXMC7~ zBzXW4jkvz!Cqn&|SmCF3&QW@d!SOEk@f{kH3M?vwJpZ+#>QWz0W=hZfxUlqVa1rms8h zb+Z*onW`CQ`1>TNNsc6N&dY2V-)?Qc?XjdPSY)~ypYWoANK6));;_{ zY(+h~*;Rfv-#cGyYBo96hgPrV!id{I>{#GN(A$%jDaB!$z~oQo*vBp~^Wrw7y&dN4 zfY7ci2fFVhwTe}DOZS&>vz;tkfb5e+np?*i3qEl}mDN&#K2V}AN5nZ)Ve^0JgF z5pTmsu(`iN8U#>#+bmdz4An^WKPO+)79BRdCObp|;q?5;IRf2u;}fnxUM)GE>Arxl zNEgd}**NX8)J5aV@dnQ_>3#HBLK=b6P7LUmcP)2xkN%yQ8VOFD;7k1*F2%Q!$CmN@Da3X*COkpinWL2hxVw3TzT~Wa`|* zUmq59;Fs;=NQWbp%sz^zTfZjyL>~>K5mWBqMf6ZX7|kx2zepdLC!@s(l$}|9}> z9j0coej&*i^69V=Pmt@|i=?ZaW=9{&W97Y#t&{Su)feOV4YcHx2hrGSGq-o@Y98-S zVtm{`#D0=KX6HK|{V*(qXiItvbViaO{nW#eChPuoUPu<Jnlc{kY`3$p`s8dZTb+4*mkKS8t=yXY}iHJgl zFBK>Y-RzF0N19=m8A7itQEA8$*VmAs3fj*Q(Y z1mjSi1#8_+Hb9&XI*b{o`Ztjiauj<5NfdX*e>od8cj9%&ttivi9f;LgaS!V~1cH zo*u2*exaOs{9aTYY#hHyRW)Ht-g=#5@ohKW+z_XRB^Na+<_`_4X&s%bI)`iqzZG&L z_r7Xcruqw+Q;R;C!JiCoqKlt_a<*Z`)2R+#^Sv)nK!j>@KjdpsgEn%@`NN0I|*mE<@5>>yZ8q+_^*gGiXpVQ{n6@@Kgrh{7{NwhFU9^=I(hPQj0HhTq6e zC|DS1p0vN8Sfj_BzyK{Xo<$I97&f~X=?8=hN)b>+%E%RC!(=?Fqin!ncZ{40%e_M| zf`l*+Xd#Sho`NAoY;r>7yXs*ZzfoI}PuxFFHfjwkEfS_W&v_3jd@lkfC}Sf4=Y`Sl zzhIwm2cvhYAwYo9phZhNl2}>{$qb^<|DkIhpbj82VUi(7<(rxKp1$DvezD*klNO~{ zF!1l!)6vm1LwcxM)C56D}xhSB>DTmZ#RsytekWrT?PZ3{cGQB=x6h zI6V1{L_a(i!W=;xp%Eb{SBw?*WWthA!))41B=vyMhVWWPLCetl(B@)TSCQ{A3By$U z7#U}sAH1Jg&Vr1aro+N@1XKaan3Ka)ivH-*(ouWpF|W*{)({wp^Rb1A|5;-HEHfKW zDS+er{j#%wb28d8beWPY%(;)XPfA9qn?4th1SSpS9%g&9>R z*38bG1o&vgMf9bNjVh0PCrpceDa17K!WgwM{F zv6yh%1Y{A^UpD4QN_{GRl)(~*|GA+3cVay%bb|-ed)ZiXZ;>ywsIJM}-LV;lcbi8C zJ&}sH5|u;1>+fLB)I#1x`p+vPtGBB;i>un$bdMrkY$hg(6LNMdV7p)Hxg(J*R%00( zOUP?D)*}zN)~Qz9V?Ar-!9bEc)HNBYMgWdt{qx1qrj%goVwpsK(A;#g4iTzts4RX! zO2{}Zr1R2MC6pXkq#j}HkuErE1~093kEIWw*ea4pj>c|CZY~Rxf@Rvmu)GM8*&KEL zdaeD3{MPBAoZX&=Hikn>8sr~OR|%Yv723|!An%#TDyLh|;yXJ(Y6trsww|Njy;g!{ zR9r5KjJ5swx_~9|XXe(KJaGKoS#XPIPZoE+WLJ&uOXBC~7%r$r5j(bYTKSz~>-Mew zdxO9O^sNvm&=vy38BWFqg7L&o0c~UeBiFVcg60phs7RlXJD%{Jzd#ujhiZegFg|jG zH5FY8IEP?>-3+TXz%=l|&S3~G)fC$6$$%(&Np+vP7=x zEk1m}cenHI+ey&=^K~mpJvS?~LphT*GzY;xTre>b=pXCEe`OE;E4y~oV5Y(f9V@Dj zDznIB{f}?d_B=b3>sp}fBI2Q|t5>8F9*@X zprIAM@d*g14_asm&sEI*9n_I(YqY51DDD*L$oCS_3r+}v(lQ+b;Q)7#dUZ%7{1hkY zCSqedNfNjF_Y-2PBp4(9E`9O!?QYh|larUkZ9F#O`|W<=pPM1cNd^79cOrO3tFD=& zm|ii`N4ljioc7ug(rSs*M7Kz!f2Sky33rDH_-$$KuNWmm@8r855``brG9f_Lgs)^IvafCfK z&M~x8Q$N?@@us)>tJC_;z^7u;^dz#axRQzB7Ox|5Zp~BL+x+dz$AT?crwVSD50|eR z2-wGTTQJIe8Aqt}HnY^}G0iYuDhu}UbfZtd^`9&FL^urQQ)MNNYiGF43ck&1_w&14 zFi7hGE^7TnW=(w-^f>mdqY^nq3xj?ix&@u*c9S{~YD-hfr-U9rqrIRaDj~%!N41(F zweIv3D%EPWW{VboE-k`uB`(V~f3eYl-L0*+!mzlSuf90pZzDraxmyqD)F8@f)2Ot@ zYVmYQa5(rZ!sdVXfei>7FmAgxs{}v@rcP_B+TDI=p1LR$wPF$keHHyW11ee=@MEv2NqBuV=^Ooyi&@q6 zn?WIAYZ-It+RfhLB#GbSd{b5C9Te^ICUBy;uc)u+pwYxL;9*M{*D{+Y_p<*`_Z?ZN zI5LN)QlN+3+4)!h2^za{toG@%JAeGqGxNt|XtB&9_ufLX z#oe#=$Y3g-n+8Bdl*###swL#)g1F*S{i;^_k8dG@Go@+D%DHJ5$>$pK^I|R`?MILXqMb0jBeU26# zkT0e==EG8PCq{jG%FYc*OE-xm6jh=nhm|sJ4AHJ0)=nO1o;x5_Ydkr=P`b7M>L=9f zX9T&Eflynm%WgD-&eG5;U2Z}|;NJzt5q3HBdjCMre=mH0sr>{WPGU(uh(vl5gh?=s zYxt$U=)tov;d`f%R8eiv_KuYAue<9XB{LXtswP7@eSy35d*_UYi#37XH}f`cPW=3a z7@tuHTfeyGE+ETY?@Po$o$R*zv=~dE^fBQxHhYNWD`aePM3Ef;HwHmG+Rc*(G!20S zkmoS%)LrDzoY_|2EaPhJWCIh{XV$R zi{Hg{fd5Vccb+^SMNx|VG@==Rg`O9|P0<_hs?`4k5K|HB7KtLs4*}sQX45j@7fS*3 zAq>Z2#jK|yD&6F-HqzGnxsVz(6QI+F(Nl$0ec~SYW^$(6h%{|kNZ#=bg!ZYm! z`XkWc|9gsd;V*4&9O>-4Y_* z-Q7JjlG5Fs0)ljRi`39v(jX-v(kTt^c+PX)AubPM2a0GO=>mvIpzLIIDtW#B6rh zxO~b+E_$s{D1wpLEew`7fH*P`HzIP1zW0f)|Ec?b{(e}ennL2)gcEbGYtvs9}%(xDZ_9cKuzMBa_4rD$xlBm`m5&`%G95bl_H(Smpm|j>{V?+xli&kjX zD8x9>&$SRlajOlOI=OROMPl;?1CZ%xW&z9~Q)N12i>as(BJklSz>qvN$sB1`9B3z+ zmoYSc4eS2lx#?HGyf-s4BCq1DN8w-5>Hg@GE=CBC8X`ZG1uRtX0rG~%V}nqucme4& zaLfQf@u^c-_tWMx%m^7S{NHUHFOVLtVhWfkO=urkekY(9vx8$%iyF=2GBcMy|A zD9>r$8N6U4m#{Ra52SWBSxN8~#-^PsQpr%evp>$&_=Rd!YP1~lQY2ei_k!^^#hg+Y zAQ=hPC=qF`Qxu_us$ysTDcHQY?WTk3qzGa8@PYploBzO=e?Du2pp=iydd+Q~^_r=c z_Esi7-c`@1maR4F`y6)S(^yb*xZ%JE_HGQp>(}sU27EYhy;AN0a`WYnw?6?!%kQ%& zi}T7VQ?a=5w3*X? zy7aHwbd6<9v8%_?uTi0?P=_&9)@Z~36)C=qsiXFtk9=YZ_CXopu})IzP9+DiI*|7L zXYAKR>KwEZGL>TiBAmVIX@m74sc7j5oSc+ksRr)(5BE!VcR3AQo!ZCEM_+t0lUnxE zTek7MN_PmKLim}23Zdc!|QQgE};&o1o(|%+Bm!kAXRr;eV#lwj`)D6DmhqWPAQ9GJu?Td?sOfQyc zGV)0A22O?&g)h2tQ#Vl?YvVITtwke#|B?8GbLRLP7~2s!2Ux+EuA~kN%n! zX|ATB%IN&5k3l#snc?t{?|ck}QPA&a44(hhR@mCGjOktzHChFPHeoLnSmp`-;KLFr zTDmgPwHF3k;w^bJTDDdT8x7S9qS>i4G?Zy%+lyAsL$%wSbgfgp-W6ENODO z^>9h1F$DG2JnL7S%d)Coys`++-7ho(UaIOqd7I|gX*|jt)bd0GwR1XHr@`s{;xsb! zy233hOG?AWaknBI+Udncs_6tc%qEq<*FGRj%k?p!6zR5lHD%;Gt+D z;yxf=ci==tP_aomXk4|29uiqFg;yibK`(P2K+O6pcG^&FnYU0oJS%5QQJSk}yF+B%#5+-`1~ zYx0L(&TM>EE)_OT2b+WJ|V}YB?L+$ z041h?01S|DNDoCqPIHK5UP3!IQ<9xGtdV?vTz`IqF?ByULlbZE@c23OJyzW+bIgW? z>^DC&qG(y=q4G>I5rJrOcdQ1?6&oy8Vj}4SA^l}A^J9xbT#Uz zCmn^ZFtR-^kcL`RzMI}j>@!Na@N0=G@j)zFE)KAEVr}~AG&OOM8jvEP8yNb^$b9wb z_~Z!_(xVsUBpl8P>!2K;e>+|bi*}+Yc=UW_%8yO*a4erpiYee4;=-UTO%eicx(ta8 zp<)Dslc{$hqA29*Za16Z2*|BK|L1G}F83qd`HE?>^1t*2-b*TF!)CiBhYnMFZ{JE= zi0At70+UXoU2fX~9B%)|ga$y{oVjwNN^1Y2JE~W0jZiY+Yz(9RbE8v8KT1V|%848L zO4uv;Pwa(#BvdA}q75?;w{f49gs{fi)U5$7f8}1ru&~DO$TMN`Wg3T8MBdk@;suL~ zAN=#a4=Q3(Q&azOOA>V5NVu!Q#ib)3BNXtXvSWy#(1v&(a^%<1%HULU8rf9Qz=;F~ zBZ1PvS>>8GV#4xsqP;KR-*6Rgk=*3s)T>r2 z?QI0bIg4rL(r>5r{iGB93>(zHK%G9hr3%tNGw=i^G(mO_GN}^#JDiL<-6B;kD@Y) z>?A`Z{*|HiDbd4V!h**)!pPty#=wCl)B%@B3M(#82I9B&Rw-ECg`)YP)FYI1 zA!72p`F#C)5SPA;fonv0usS822y7%}ny)|#vL51UdpXxr0*v*cJ)~!`M!JF`$7MP7 zpad{fSy}OWf9(}7nZirLPD6qykFwYwTYZ5y>fu=3Y=2Aju`n<$oSU~XzqL`MH%}VH zXIf;t7n^H&aUiI(HYb!jMndVYmMz@yk31SSty!gVeDLd-0PY9>C=~yom6FDfa6WIO zJ7Fh{qX!-*wa}X|93K6%9mf~*DkiiY41?u#q?q=_A!eK*-kA`}@1$J)DL3<{LzxmS zws1qYPV$Qh;Ae)^YT2TiO;UzMP|6ZrN8Df;9FSU|Ko9~KH{I&HqM+qXiIpqDVH!m) zZykU_2@g}8*r(BvJwd2nYcVO|%X9gtu;eoWkVVA6lnSTh>4+HN1W3Qoa`r@oiKmc^ z8i{+v#M)ue5&zBt5TV4_kzCWCf)u2Q;xvh(G*Jd1!940pA!T?uwB3ifMHrjl1Z-<0 zf*Ac>R*5SCTfW&oiFvO`ESTNQ)5Bb-LmZhx&7B5nF$=uI9KU{z z(QyP+XHH4-hV3z|ERM-*c=Tf%$ltSR{w7&t5l#JBP0odnhzm$o_({ENl#*HlL>G3hk0p#0l>gxy zIL=4M(|@ZO5W323q$u*1avDBvj1M ztmOH~R9Yz*OmLm4R8`@yIN$En4_%H;rp6h!DRA1K7C4T&qr_>W%l;s5#3#P1fI<>H57{@NbmTJ$HLK ztiXIc`cTbDy^f12axl@NX<-ZEjo|hcN3ejD;I@=WHhz8(V*73wNGg5#YxZxuXlO;m zMyiWvUX%Eo)wA8a}<@7%ew%DcR8Ud!*df{O;|CYv*77 znBYIz?uWwKstj5}#F_LeGG>+|TK!so6of5i(-d0SFk< zUT6ITI*0Mh-?xTd>hgxYSPboQ{iW(Pvd;qA{>22)Hlfyi8~xxiDKI$LOn*o``MO0X z$8R^o$~5|gLBT_o%LGa~-Hf`(1~X9VJ2+Nr>KsGy?`_HkNLndB!$*Q8L!k4u8$X>> zTK3zFM4IY3cMxw(@pdUKn$^p7?+2CD zS@hr{lnuR)n*49V{ck3pAC$J)W0?)F?~gsY zdybXYZznt;w%zCADG(dV^9XUcP%}eqd}~G%MXcP?Pr0T3j|=`gxX^HDRm&PGLq0*v zsym7B=})0&KA+C~N7(r$CCoL;z9j<5{?~4?!Y7!&>2udjbQjd~;o+zMlT~xP|5+%( zOW(`s@RweY)vs<84S!sN;&9_!C3sS|v^=~&y3&&ZB|rbIc88k-30%N++XnC}{-qp!NQ=1R% zm`Is^tINh$R&_qmqSR0|gcowU$8o>>P&V@RVvE27z9CLWx%K?8`)Tb*y;cQHkhNkGx%pagxis
@%`lH>nQ*!9~SP);+ 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 cZ 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 barc2rTC_vuSuxlM&yRwPs&tj4X~_QosAOK- literal 0 HcmV?d00001 From e738e7ede2087f761c1b68898ae44494e8ddba00 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sun, 17 May 2026 16:21:43 +0200 Subject: [PATCH 10/35] Fix native renderer resize --- modules/yup_graphics/native/yup_GraphicsContext_metal.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/modules/yup_graphics/native/yup_GraphicsContext_metal.cpp b/modules/yup_graphics/native/yup_GraphicsContext_metal.cpp index 9dc7d5fe3..075de7f42 100644 --- a/modules/yup_graphics/native/yup_GraphicsContext_metal.cpp +++ b/modules/yup_graphics/native/yup_GraphicsContext_metal.cpp @@ -190,10 +190,7 @@ class LowLevelRenderContextMetal : public GraphicsContext m_renderTarget = renderContextImpl->makeRenderTarget (MTLPixelFormatBGRA8Unorm, width, height); if (m_currentTexture != nil) - { - [m_currentTexture setPurgeableState:MTLPurgeableStateEmpty]; m_currentTexture = nil; - } MTLTextureDescriptor* descriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:(MTLPixelFormatBGRA8Unorm) width:width From 1b01b958cf1817146f3cfff2bc7919e0e6286f45 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sun, 17 May 2026 16:21:58 +0200 Subject: [PATCH 11/35] Improved node graph --- .../graph/yup_AudioGraphComponent.cpp | 26 +++++++++++++++++++ .../graph/yup_AudioGraphNodeView.cpp | 24 +++-------------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/modules/yup_audio_gui/graph/yup_AudioGraphComponent.cpp b/modules/yup_audio_gui/graph/yup_AudioGraphComponent.cpp index 64de751c4..18a2892f5 100644 --- a/modules/yup_audio_gui/graph/yup_AudioGraphComponent.cpp +++ b/modules/yup_audio_gui/graph/yup_AudioGraphComponent.cpp @@ -33,6 +33,17 @@ bool endpointsMatch (const AudioGraphEndpoint& a, const AudioGraphEndpoint& b) n { return a == b; } + +AudioGraphNodeView* findNodeViewForEventSource (Component* source) noexcept +{ + if (source == nullptr) + return nullptr; + + if (auto* nodeView = dynamic_cast (source)) + return nodeView; + + return source->getParentComponentWithType(); +} } // namespace //============================================================================== @@ -340,6 +351,9 @@ void AudioGraphComponent::mouseDown (const MouseEvent& event) return; } + if (auto* clickedNodeView = findNodeViewForEventSource (event.getSourceComponent())) + clickedNodeView->toFront (true); + if (auto endpoint = hitTestEndpoint (screenPos)) { if (interaction == Interaction::armedPort && activeEndpoint.has_value()) @@ -496,7 +510,19 @@ void AudioGraphComponent::mouseWheel (const MouseEvent& event, const MouseWheelD void AudioGraphComponent::keyDown (const KeyPress& keys, const Point&) { if (keys.getKey() == KeyPress::spaceKey) + { spacebarDown = true; + return; + } + + if (keys.getKey() == KeyPress::textFKey || keys.getKey() == KeyPress::homeKey) + { + zoomToFitNodes(); + return; + } + + if (keys.getKey() == KeyPress::number0Key) + resetView(); } void AudioGraphComponent::keyUp (const KeyPress& keys, const Point&) diff --git a/modules/yup_audio_gui/graph/yup_AudioGraphNodeView.cpp b/modules/yup_audio_gui/graph/yup_AudioGraphNodeView.cpp index 28a1454f0..843d6c20e 100644 --- a/modules/yup_audio_gui/graph/yup_AudioGraphNodeView.cpp +++ b/modules/yup_audio_gui/graph/yup_AudioGraphNodeView.cpp @@ -50,18 +50,6 @@ void fillFeatheredRoundedRect (Graphics& g, Rectangle bounds, float corne } } -void fillFeatheredEllipse (Graphics& g, Point center, float radius, Color color) -{ - constexpr int numLayers = 4; - - for (int i = numLayers; i > 0; --i) - { - const auto layerRadius = radius * (1.0f + (static_cast (i) * 0.22f)); - const auto alpha = 0.040f + (static_cast (numLayers - i) * 0.045f); - g.setFillColor (color.withAlpha (alpha)); - g.fillEllipse (ellipseBounds (center, layerRadius)); - } -} } // namespace //============================================================================== @@ -263,14 +251,12 @@ void AudioGraphNodeView::paint (Graphics& g) const auto info = getInputPortInfo (i); const auto center = getInputPortCenter (i); - fillFeatheredEllipse (g, center, portRadius * 1.25f, info.color); + g.setFillColor (info.color.withAlpha (0.24f)); + g.fillEllipse (ellipseBounds (center, portRadius * 1.8f)); g.setFillColor (info.color); g.fillEllipse (ellipseBounds (center, portRadius)); g.setFillColor (canvasBackground); g.fillEllipse (ellipseBounds (center, portRadius * 0.45f)); - g.setStrokeColor (info.color.withAlpha (0.72f)); - g.setStrokeWidth (1.2f * viewScale); - g.strokeEllipse (ellipseBounds (center, portRadius * 1.15f)); g.setFillColor (Color (0xffd6d6d6)); g.fillFittedText (info.name, labelFont, { center.getX() + 12.0f * viewScale, center.getY() - 8.0f * viewScale, 72.0f * viewScale, 16.0f * viewScale }, Justification::left); @@ -281,14 +267,12 @@ void AudioGraphNodeView::paint (Graphics& g) const auto info = getOutputPortInfo (i); const auto center = getOutputPortCenter (i); - fillFeatheredEllipse (g, center, portRadius * 1.25f, info.color); + g.setFillColor (info.color.withAlpha (0.24f)); + g.fillEllipse (ellipseBounds (center, portRadius * 1.8f)); g.setFillColor (info.color); g.fillEllipse (ellipseBounds (center, portRadius)); g.setFillColor (canvasBackground); g.fillEllipse (ellipseBounds (center, portRadius * 0.45f)); - g.setStrokeColor (info.color.withAlpha (0.72f)); - g.setStrokeWidth (1.2f * viewScale); - g.strokeEllipse (ellipseBounds (center, portRadius * 1.15f)); g.setFillColor (Color (0xffd6d6d6)); g.fillFittedText (info.name, labelFont, { center.getX() - 84.0f * viewScale, center.getY() - 8.0f * viewScale, 72.0f * viewScale, 16.0f * viewScale }, Justification::right); From 1a4b6b54354eecb1022b6188d946f221f1ed4ba3 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sun, 17 May 2026 16:22:10 +0200 Subject: [PATCH 12/35] Fix slider bars in demo --- examples/graphics/source/examples/AudioFileDemo.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/graphics/source/examples/AudioFileDemo.h b/examples/graphics/source/examples/AudioFileDemo.h index a90bed4fb..227da802f 100644 --- a/examples/graphics/source/examples/AudioFileDemo.h +++ b/examples/graphics/source/examples/AudioFileDemo.h @@ -818,7 +818,7 @@ class AudioFileDemo : public yup::Component timeLabel.setColor (yup::Label::Style::textFillColorId, yup::Colors::white); addAndMakeVisible (timeStretchSlider); - timeStretchSlider.setSliderType (yup::Slider::LinearBarHorizontal); + timeStretchSlider.setSliderType (yup::Slider::LinearHorizontal); timeStretchSlider.setRange (0.5, 2.0, 0.01); timeStretchSlider.setValue (1.0, yup::NotificationType::dontSendNotification); timeStretchSlider.setDefaultValue (1.0); @@ -837,7 +837,7 @@ class AudioFileDemo : public yup::Component pitchLabel.setColor (yup::Label::Style::textFillColorId, yup::Colors::white); addAndMakeVisible (pitchShiftSlider); - pitchShiftSlider.setSliderType (yup::Slider::LinearBarHorizontal); + pitchShiftSlider.setSliderType (yup::Slider::LinearHorizontal); pitchShiftSlider.setRange (0.5, 2.0, 0.01); pitchShiftSlider.setValue (1.0, yup::NotificationType::dontSendNotification); pitchShiftSlider.setDefaultValue (1.0); From 580b01a8d4f012b4003dd20aa3ff8eea0a17f467 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sun, 17 May 2026 17:31:23 +0200 Subject: [PATCH 13/35] More fixes --- cmake/yup_standalone.cmake | 10 +- examples/graphics/CMakeLists.txt | 14 +- .../graph/yup_AudioGraphComponent.cpp | 159 ++++------ .../graph/yup_AudioGraphComponent.h | 73 ++++- .../graph/yup_AudioGraphNodeView.cpp | 142 +-------- .../graph/yup_AudioGraphNodeView.h | 19 +- .../yup_audio_processors.cpp | 3 + .../yup_audio_processors.h | 8 +- .../themes/theme_v1/yup_ThemeVersion1.cpp | 273 ++++++++++++++++++ tests/CMakeLists.txt | 5 +- 10 files changed, 453 insertions(+), 253 deletions(-) diff --git a/cmake/yup_standalone.cmake b/cmake/yup_standalone.cmake index 3465afaee..135f8d14f 100644 --- a/cmake/yup_standalone.cmake +++ b/cmake/yup_standalone.cmake @@ -102,10 +102,12 @@ function (yup_standalone_app) list (APPEND additional_libraries perfetto::perfetto) endif() - _yup_target_list_contains ("${YUP_ARG_MODULES}" yup_audio_plugin_host has_audio_plugin_host) - if (has_audio_plugin_host) - _yup_collect_audio_plugin_host_dependencies ("${YUP_ARG_DEFINITIONS}" audio_plugin_host_libraries) - list (APPEND additional_libraries ${audio_plugin_host_libraries}) + if (YUP_PLATFORM_DESKTOP) + _yup_target_list_contains ("${YUP_ARG_MODULES}" yup_audio_plugin_host has_audio_plugin_host) + if (has_audio_plugin_host) + _yup_collect_audio_plugin_host_dependencies ("${YUP_ARG_DEFINITIONS}" audio_plugin_host_libraries) + list (APPEND additional_libraries ${audio_plugin_host_libraries}) + endif() endif() # ==== Prepare executable diff --git a/examples/graphics/CMakeLists.txt b/examples/graphics/CMakeLists.txt index f4762971e..6c20f4435 100644 --- a/examples/graphics/CMakeLists.txt +++ b/examples/graphics/CMakeLists.txt @@ -48,10 +48,19 @@ endif() # ==== Prepare target set (additional_modules "") +set (additional_definitions "") if (YUP_PLATFORM_DESKTOP AND NOT YUP_PLATFORM_WINDOWS) set (additional_modules yup::yup_python) endif() +if (YUP_PLATFORM_DESKTOP) + list (APPEND additional_modules yup::yup_audio_plugin_host) + list (APPEND additional_definitions + YUP_AUDIO_PLUGIN_HOST_ENABLE_CLAP=1 + YUP_AUDIO_PLUGIN_HOST_ENABLE_AU=1 + YUP_AUDIO_PLUGIN_HOST_ENABLE_VST3=1) +endif() + yup_standalone_app ( TARGET_NAME ${target_name} TARGET_VERSION ${target_version} @@ -60,9 +69,7 @@ yup_standalone_app ( TARGET_APP_NAMESPACE "org.yup" DEFINITIONS YUP_EXAMPLE_GRAPHICS_RIVE_FILE="${rive_file}" - YUP_AUDIO_PLUGIN_HOST_ENABLE_CLAP=1 - YUP_AUDIO_PLUGIN_HOST_ENABLE_AU=1 - YUP_AUDIO_PLUGIN_HOST_ENABLE_VST3=1 + ${additional_definitions} PRELOAD_FILES "${CMAKE_CURRENT_LIST_DIR}/${rive_file}@${rive_file}" MODULES @@ -76,7 +83,6 @@ yup_standalone_app ( yup::yup_audio_gui yup::yup_audio_processors yup::yup_audio_formats - yup::yup_audio_plugin_host pffft_library opus_library flac_library diff --git a/modules/yup_audio_gui/graph/yup_AudioGraphComponent.cpp b/modules/yup_audio_gui/graph/yup_AudioGraphComponent.cpp index 18a2892f5..6d86ea2b6 100644 --- a/modules/yup_audio_gui/graph/yup_AudioGraphComponent.cpp +++ b/modules/yup_audio_gui/graph/yup_AudioGraphComponent.cpp @@ -24,11 +24,6 @@ namespace yup namespace { -constexpr float minZoom = 0.1f; -constexpr float maxZoom = 4.0f; -constexpr float wireHitDistance = 8.0f; -constexpr float dragWireThreshold = 4.0f; - bool endpointsMatch (const AudioGraphEndpoint& a, const AudioGraphEndpoint& b) noexcept { return a == b; @@ -46,6 +41,10 @@ AudioGraphNodeView* findNodeViewForEventSource (Component* source) noexcept } } // namespace +//============================================================================== +const Identifier AudioGraphComponent::Style::backgroundColorId ("audioGraphBackground"); +const Identifier AudioGraphComponent::Style::gridColorId ("audioGraphGrid"); + //============================================================================== AudioGraphComponent::AudioGraphComponent (std::shared_ptr graphIn) : graph (std::move (graphIn)) @@ -186,6 +185,44 @@ void AudioGraphComponent::setZoom (float newZoom) repaint(); } +void AudioGraphComponent::setMinZoom (float newMinZoom) +{ + const auto clampedMinZoom = jmax (0.001f, newMinZoom); + if (minZoom == clampedMinZoom) + return; + + minZoom = clampedMinZoom; + + if (maxZoom < minZoom) + maxZoom = minZoom; + + setZoom (zoom); +} + +void AudioGraphComponent::setMaxZoom (float newMaxZoom) +{ + const auto clampedMaxZoom = jmax (0.001f, newMaxZoom); + if (maxZoom == clampedMaxZoom) + return; + + maxZoom = clampedMaxZoom; + + if (minZoom > maxZoom) + minZoom = maxZoom; + + setZoom (zoom); +} + +void AudioGraphComponent::setWireHitDistance (float newWireHitDistance) +{ + wireHitDistance = jmax (0.0f, newWireHitDistance); +} + +void AudioGraphComponent::setDragWireThreshold (float newDragWireThreshold) +{ + dragWireThreshold = jmax (0.0f, newDragWireThreshold); +} + void AudioGraphComponent::setCanvasOffset (Point offset) { needsInitialReset = false; @@ -206,7 +243,7 @@ void AudioGraphComponent::resetView() needsInitialReset = false; needsInitialZoomToFit = false; - zoom = 1.0f; + zoom = jlimit (minZoom, maxZoom, 1.0f); centerViewOnNodes(); } @@ -307,18 +344,8 @@ void AudioGraphComponent::resized() void AudioGraphComponent::paint (Graphics& g) { - g.setFillColor (Color (0xff101522)); - g.fillAll(); - - drawGrid (g); - - if (graph != nullptr) - { - for (const auto& connection : graph->getConnections()) - drawConnection (g, connection, 1.0f); - } - - drawPendingWire (g); + if (auto style = ApplicationTheme::findComponentStyle (*this)) + style->paint (g, *ApplicationTheme::getGlobalTheme(), *this); } //============================================================================== @@ -773,6 +800,21 @@ Color AudioGraphComponent::getEndpointColor (const AudioGraphEndpoint& endpoint) return AudioGraphNodeView::getPortKindColor (AudioGraphNodeView::PortKind::audio); } +bool AudioGraphComponent::isPendingWireVisible() const noexcept +{ + return activeEndpoint.has_value() && (interaction == Interaction::armedPort || interaction == Interaction::draggingWire); +} + +std::optional AudioGraphComponent::getPendingWireEndpoint() const +{ + return activeEndpoint; +} + +Point AudioGraphComponent::getPendingWireEndPosition() const noexcept +{ + return interaction == Interaction::draggingWire ? pendingWireEnd : lastMouseScreen; +} + bool AudioGraphComponent::tryConnect (const AudioGraphEndpoint& first, const AudioGraphEndpoint& second) { if (graph == nullptr || ! isCompatiblePair (first, second)) @@ -842,87 +884,6 @@ AudioGraphConnection AudioGraphComponent::makeConnection (const AudioGraphEndpoi : AudioGraphConnection { second, first }; } -//============================================================================== -void AudioGraphComponent::drawGrid (Graphics& g) -{ - const auto spacing = 24.0f * zoom; - if (spacing < 6.0f) - return; - - const auto startX = std::fmod (canvasOffset.getX(), spacing); - const auto startY = std::fmod (canvasOffset.getY(), spacing); - - g.setFillColor (Colors::white.withAlpha (0.045f)); - - for (auto x = startX; x < getWidth(); x += spacing) - { - for (auto y = startY; y < getHeight(); y += spacing) - g.fillEllipse (x - 1.0f, y - 1.0f, 2.0f, 2.0f); - } -} - -void AudioGraphComponent::drawConnection (Graphics& g, const AudioGraphConnection& connection, float opacity) -{ - const auto start = getEndpointScreenPosition (connection.source); - const auto end = getEndpointScreenPosition (connection.destination); - - if (! getLocalBounds().reduced (-200.0f).contains (start) && ! getLocalBounds().reduced (-200.0f).contains (end)) - return; - - const auto controlOffset = jmax (60.0f * zoom, std::abs (end.getX() - start.getX()) * 0.5f); - const auto cp1 = Point { start.getX() + controlOffset, start.getY() }; - const auto cp2 = Point { end.getX() - controlOffset, end.getY() }; - - drawBezierHalf (g, start, cp1, cp2, end, getEndpointColor (connection.source).withMultipliedAlpha (opacity), true); - drawBezierHalf (g, start, cp1, cp2, end, getEndpointColor (connection.destination).withMultipliedAlpha (opacity), false); -} - -void AudioGraphComponent::drawBezierHalf (Graphics& g, Point p0, Point p1, Point p2, Point p3, Color color, bool firstHalf) -{ - const auto p01 = (p0 + p1) * 0.5f; - const auto p12 = (p1 + p2) * 0.5f; - const auto p23 = (p2 + p3) * 0.5f; - const auto p012 = (p01 + p12) * 0.5f; - const auto p123 = (p12 + p23) * 0.5f; - const auto midpoint = (p012 + p123) * 0.5f; - - Path path; - if (firstHalf) - path.moveTo (p0).cubicTo (p01, p012.getX(), p012.getY(), midpoint.getX(), midpoint.getY()); - else - path.moveTo (midpoint).cubicTo (p123, p23.getX(), p23.getY(), p3.getX(), p3.getY()); - - g.setStrokeColor (color); - g.setStrokeWidth (jmax (1.5f, 3.0f * zoom)); - g.setStrokeCap (StrokeCap::Round); - g.strokePath (path); -} - -void AudioGraphComponent::drawPendingWire (Graphics& g) -{ - if (! activeEndpoint.has_value() || (interaction != Interaction::armedPort && interaction != Interaction::draggingWire)) - return; - - const auto start = getEndpointScreenPosition (*activeEndpoint); - const auto end = interaction == Interaction::draggingWire ? pendingWireEnd : lastMouseScreen; - const auto controlOffset = jmax (60.0f * zoom, std::abs (end.getX() - start.getX()) * 0.5f); - const auto cp1 = Point { start.getX() + (activeEndpoint->isSource() ? controlOffset : -controlOffset), start.getY() }; - const auto cp2 = Point { end.getX() - (activeEndpoint->isSource() ? controlOffset : -controlOffset), end.getY() }; - - Path path; - path.moveTo (start).cubicTo (cp1, cp2.getX(), cp2.getY(), end.getX(), end.getY()); - - g.setStrokeColor (getEndpointColor (*activeEndpoint).withAlpha (0.55f)); - g.setStrokeWidth (jmax (1.5f, 2.5f * zoom)); - g.setStrokeCap (StrokeCap::Round); - g.strokePath (path); - - g.setFillColor (getEndpointColor (*activeEndpoint).withAlpha (0.20f)); - const auto center = getEndpointScreenPosition (*activeEndpoint); - const auto radius = 14.0f * zoom; - g.fillEllipse (center.getX() - radius, center.getY() - radius, radius * 2.0f, radius * 2.0f); -} - Point AudioGraphComponent::cubicPoint (Point p0, Point p1, Point p2, Point p3, float t) const noexcept { const auto u = 1.0f - t; diff --git a/modules/yup_audio_gui/graph/yup_AudioGraphComponent.h b/modules/yup_audio_gui/graph/yup_AudioGraphComponent.h index 5abae9b5c..285799172 100644 --- a/modules/yup_audio_gui/graph/yup_AudioGraphComponent.h +++ b/modules/yup_audio_gui/graph/yup_AudioGraphComponent.h @@ -34,6 +34,16 @@ namespace yup class YUP_API AudioGraphComponent : public Component { public: + /** Style identifiers for theme customization. */ + struct Style + { + /** Canvas background color. */ + static const Identifier backgroundColorId; + + /** Grid dot color. */ + static const Identifier gridColorId; + }; + /** Receives notifications for graph editor gestures that committed changes. */ struct Listener { @@ -74,12 +84,39 @@ class YUP_API AudioGraphComponent : public Component /** Returns the view for a node, or nullptr. */ AudioGraphNodeView* getNodeView (AudioGraphNodeID nodeID) const noexcept; - /** Sets the zoom factor, clamped to [0.1, 4.0]. */ + /** @internal Returns the graph processor currently being edited. */ + const AudioGraphProcessor* getGraphProcessor() const noexcept { return graph.get(); } + + /** Sets the zoom factor, clamped to [getMinZoom(), getMaxZoom()]. */ void setZoom (float zoom); /** Returns the zoom factor. */ float getZoom() const noexcept { return zoom; } + /** Sets the minimum zoom factor. */ + void setMinZoom (float newMinZoom); + + /** Returns the minimum zoom factor. */ + float getMinZoom() const noexcept { return minZoom; } + + /** Sets the maximum zoom factor. */ + void setMaxZoom (float newMaxZoom); + + /** Returns the maximum zoom factor. */ + float getMaxZoom() const noexcept { return maxZoom; } + + /** Sets the screen-space hit distance used for selecting connection wires. */ + void setWireHitDistance (float newWireHitDistance); + + /** Returns the screen-space hit distance used for selecting connection wires. */ + float getWireHitDistance() const noexcept { return wireHitDistance; } + + /** Sets the drag distance needed before an armed port starts dragging a wire. */ + void setDragWireThreshold (float newDragWireThreshold); + + /** Returns the drag distance needed before an armed port starts dragging a wire. */ + float getDragWireThreshold() const noexcept { return dragWireThreshold; } + /** Sets the canvas-to-screen offset. */ void setCanvasOffset (Point offset); @@ -101,6 +138,21 @@ class YUP_API AudioGraphComponent : public Component /** Converts component-local screen space to canvas space. */ Point screenToCanvas (Point screenPos) const; + /** @internal Returns the screen position for an endpoint. */ + Point getEndpointScreenPosition (const AudioGraphEndpoint& endpoint) const; + + /** @internal Returns the display color for an endpoint. */ + Color getEndpointColor (const AudioGraphEndpoint& endpoint) const; + + /** @internal Returns true when a pending connection wire should be painted. */ + bool isPendingWireVisible() const noexcept; + + /** @internal Returns the endpoint that started the pending connection wire. */ + std::optional getPendingWireEndpoint() const; + + /** @internal Returns the current screen endpoint for the pending connection wire. */ + Point getPendingWireEndPosition() const noexcept; + /** Adds a listener. */ void addListener (Listener* listener); @@ -109,25 +161,18 @@ class YUP_API AudioGraphComponent : public Component /** @internal */ void resized() override; - /** @internal */ void paint (Graphics& g) override; - /** @internal */ void mouseDown (const MouseEvent& event) override; - /** @internal */ void mouseDrag (const MouseEvent& event) override; - /** @internal */ void mouseUp (const MouseEvent& event) override; - /** @internal */ void mouseWheel (const MouseEvent& event, const MouseWheelData& wheelData) override; - /** @internal */ void keyDown (const KeyPress& keys, const Point& position) override; - /** @internal */ void keyUp (const KeyPress& keys, const Point& position) override; @@ -175,9 +220,6 @@ class YUP_API AudioGraphComponent : public Component std::optional hitTestEndpoint (Point screenPos) const; std::optional hitTestConnection (Point screenPos) const; - Point getEndpointScreenPosition (const AudioGraphEndpoint& endpoint) const; - Color getEndpointColor (const AudioGraphEndpoint& endpoint) const; - bool tryConnect (const AudioGraphEndpoint& first, const AudioGraphEndpoint& second); bool removeConnectionAndCommit (const AudioGraphConnection& connection); void removeConnectionsForEndpoint (const AudioGraphEndpoint& endpoint); @@ -185,11 +227,6 @@ class YUP_API AudioGraphComponent : public Component bool isCompatiblePair (const AudioGraphEndpoint& first, const AudioGraphEndpoint& second) const noexcept; AudioGraphConnection makeConnection (const AudioGraphEndpoint& first, const AudioGraphEndpoint& second) const noexcept; - void drawGrid (Graphics& g); - void drawConnection (Graphics& g, const AudioGraphConnection& connection, float opacity); - void drawBezierHalf (Graphics& g, Point p0, Point p1, Point p2, Point p3, Color color, bool firstHalf); - void drawPendingWire (Graphics& g); - Point cubicPoint (Point p0, Point p1, Point p2, Point p3, float t) const noexcept; float distanceToSegment (Point point, Point start, Point end) const noexcept; @@ -198,6 +235,10 @@ class YUP_API AudioGraphComponent : public Component ListenerList listeners; Interaction interaction = Interaction::idle; + float minZoom = 0.1f; + float maxZoom = 4.0f; + float wireHitDistance = 8.0f; + float dragWireThreshold = 4.0f; float zoom = 1.0f; Point canvasOffset { 0.0f, 0.0f }; bool spacebarDown = false; diff --git a/modules/yup_audio_gui/graph/yup_AudioGraphNodeView.cpp b/modules/yup_audio_gui/graph/yup_AudioGraphNodeView.cpp index 843d6c20e..2d1d89ead 100644 --- a/modules/yup_audio_gui/graph/yup_AudioGraphNodeView.cpp +++ b/modules/yup_audio_gui/graph/yup_AudioGraphNodeView.cpp @@ -29,29 +29,21 @@ constexpr float basePortRowHeight = 24.0f; constexpr float basePortTopPadding = 8.0f; constexpr float baseParameterRowHeight = 25.0f; constexpr float basePortRadius = 6.0f; -constexpr float baseCornerRadius = 7.0f; constexpr float baseContentHeight = 8.0f; -Rectangle ellipseBounds (Point center, float radius) -{ - return { center.getX() - radius, center.getY() - radius, radius * 2.0f, radius * 2.0f }; -} - -void fillFeatheredRoundedRect (Graphics& g, Rectangle bounds, float corner, Color color, float viewScale) -{ - constexpr int numLayers = 5; - - for (int i = numLayers; i > 0; --i) - { - const auto amount = static_cast (i) * 1.4f * viewScale; - const auto alpha = 0.020f + (static_cast (numLayers - i) * 0.018f); - g.setFillColor (color.withAlpha (alpha)); - g.fillRoundedRect (bounds.reduced (-amount), corner + amount); - } -} - } // namespace +//============================================================================== +const Identifier AudioGraphNodeView::Style::shadowColorId ("audioGraphNodeShadow"); +const Identifier AudioGraphNodeView::Style::accentBackgroundColorId ("audioGraphNodeAccentBackground"); +const Identifier AudioGraphNodeView::Style::bodyBackgroundColorId ("audioGraphNodeBodyBackground"); +const Identifier AudioGraphNodeView::Style::headerBackgroundColorId ("audioGraphNodeHeaderBackground"); +const Identifier AudioGraphNodeView::Style::textColorId ("audioGraphNodeText"); +const Identifier AudioGraphNodeView::Style::subtitleTextColorId ("audioGraphNodeSubtitleText"); +const Identifier AudioGraphNodeView::Style::parameterBackgroundColorId ("audioGraphNodeParameterBackground"); +const Identifier AudioGraphNodeView::Style::parameterValueBackgroundColorId ("audioGraphNodeParameterValueBackground"); +const Identifier AudioGraphNodeView::Style::portHoleColorId ("audioGraphNodePortHole"); + //============================================================================== AudioGraphNodeView::AudioGraphNodeView (AudioGraphNodeID nodeIDIn) : nodeID (nodeIDIn) @@ -93,7 +85,7 @@ AudioGraphNodeView::ParameterInfo AudioGraphNodeView::getParameterInfo (int para return { String ("param ") + String (parameterIndex + 1), {}, getPortKindColor (PortKind::parameter), -1.0f, PortKind::parameter }; } -void AudioGraphNodeView::paintNodeContent (Graphics&, Rectangle) +void AudioGraphNodeView::paintNodeContent (Graphics&, Rectangle) const { } @@ -104,6 +96,7 @@ int AudioGraphNodeView::getPreferredWidth() const int AudioGraphNodeView::getPreferredHeight() const { + // TODO - this needs to be moved to the style somehow const auto portRows = jmax (1, jmax (getNumInputPorts(), getNumOutputPorts())); return roundToInt (baseHeaderHeight + baseContentHeight @@ -170,113 +163,8 @@ void AudioGraphNodeView::setViewScale (float newScale) //============================================================================== void AudioGraphNodeView::paint (Graphics& g) { - const auto bounds = getLocalBounds().reduced (getPortRadius() * 0.5f, 0.0f); - const auto bodyBounds = bounds.reduced (2.0f * viewScale); - const auto corner = baseCornerRadius * viewScale; - const auto accent = getNodeColor(); - const auto canvasBackground = Color (0xff101522); - const auto headerHeight = baseHeaderHeight * viewScale; - - fillFeatheredRoundedRect (g, bodyBounds.translated (0.0f, 3.0f * viewScale), corner, Colors::black, viewScale); - - g.setFillColor (accent.withAlpha (0.11f)); - g.fillRoundedRect (bodyBounds.reduced (-1.5f * viewScale), corner + 2.0f * viewScale); - - g.setFillColor (Color (0xff1e2535)); - g.fillRoundedRect (bodyBounds, corner); - - auto headerBounds = bodyBounds.withHeight (headerHeight); - g.setFillColor (Color (0xff141a26)); - g.fillRoundedRect (headerBounds, corner, corner, 0.0f, 0.0f); - - g.setStrokeColor (accent.withAlpha (0.70f)); - g.setStrokeWidth (1.35f * viewScale); - g.strokeRoundedRect (bodyBounds, corner); - - const auto headerInner = headerBounds.reduced (9.0f * viewScale, 5.0f * viewScale); - - const auto font = ApplicationTheme::getGlobalTheme()->getDefaultFont(); - g.setFillColor (accent); - g.fillFittedText (getNodeTitle(), font.withHeight (12.0f * viewScale), headerInner.withHeight (18.0f * viewScale), Justification::left); - - const auto subtitle = getNodeSubtitle(); - if (subtitle.isNotEmpty()) - { - g.setFillColor (Color (0xffb8b8b8)); - g.fillFittedText (subtitle, font.withHeight (9.0f * viewScale), headerInner.withHeight (18.0f * viewScale), Justification::right); - } - - const auto ruleY = bodyBounds.getY() + headerHeight; - g.setStrokeColor (accent.withAlpha (0.16f)); - g.strokeLine ({ bodyBounds.getX(), ruleY }, { bodyBounds.getRight(), ruleY }); - - auto contentBounds = bodyBounds.reduced (10.0f * viewScale, 0.0f); - contentBounds = contentBounds.withY (ruleY + 4.0f * viewScale).withHeight (baseContentHeight * viewScale); - paintNodeContent (g, contentBounds); - - auto parameterBounds = bodyBounds.reduced (10.0f * viewScale, 0.0f); - parameterBounds = parameterBounds.withY (ruleY + (baseContentHeight + 5.0f) * viewScale); - - const auto parameterFont = font.withHeight (9.5f * viewScale); - for (int i = 0; i < getNumParameterRows(); ++i) - { - const auto info = getParameterInfo (i); - auto row = parameterBounds.removeFromTop (baseParameterRowHeight * viewScale).reduced (0.0f, 2.0f * viewScale); - auto valueBox = row.removeFromRight (58.0f * viewScale); - - g.setFillColor (Color (0xff263044)); - g.fillRoundedRect (row.reduced (0.0f, 1.0f * viewScale), 3.0f * viewScale); - - if (info.normalizedValue >= 0.0f) - { - const auto fillWidth = row.getWidth() * jlimit (0.0f, 1.0f, info.normalizedValue); - g.setFillColor (info.color.withAlpha (0.20f)); - g.fillRoundedRect (row.withWidth (fillWidth).reduced (0.0f, 1.0f * viewScale), 3.0f * viewScale); - } - - g.setFillColor (Color (0xffd1d5db)); - g.fillFittedText (info.name, parameterFont, row.reduced (6.0f * viewScale, 0.0f), Justification::left); - - g.setFillColor (Color (0xff1a2130)); - g.fillRoundedRect (valueBox.reduced (0.0f, 1.0f * viewScale), 3.0f * viewScale); - g.setFillColor (info.color); - g.fillFittedText (info.value, parameterFont, valueBox.reduced (5.0f * viewScale, 0.0f), Justification::right); - } - - const auto labelFont = font.withHeight (10.5f * viewScale); - const auto portRadius = getPortRadius(); - - for (int i = 0; i < getNumInputPorts(); ++i) - { - const auto info = getInputPortInfo (i); - const auto center = getInputPortCenter (i); - - g.setFillColor (info.color.withAlpha (0.24f)); - g.fillEllipse (ellipseBounds (center, portRadius * 1.8f)); - g.setFillColor (info.color); - g.fillEllipse (ellipseBounds (center, portRadius)); - g.setFillColor (canvasBackground); - g.fillEllipse (ellipseBounds (center, portRadius * 0.45f)); - - g.setFillColor (Color (0xffd6d6d6)); - g.fillFittedText (info.name, labelFont, { center.getX() + 12.0f * viewScale, center.getY() - 8.0f * viewScale, 72.0f * viewScale, 16.0f * viewScale }, Justification::left); - } - - for (int i = 0; i < getNumOutputPorts(); ++i) - { - const auto info = getOutputPortInfo (i); - const auto center = getOutputPortCenter (i); - - g.setFillColor (info.color.withAlpha (0.24f)); - g.fillEllipse (ellipseBounds (center, portRadius * 1.8f)); - g.setFillColor (info.color); - g.fillEllipse (ellipseBounds (center, portRadius)); - g.setFillColor (canvasBackground); - g.fillEllipse (ellipseBounds (center, portRadius * 0.45f)); - - g.setFillColor (Color (0xffd6d6d6)); - g.fillFittedText (info.name, labelFont, { center.getX() - 84.0f * viewScale, center.getY() - 8.0f * viewScale, 72.0f * viewScale, 16.0f * viewScale }, Justification::right); - } + if (auto style = ApplicationTheme::findComponentStyle (*this)) + style->paint (g, *ApplicationTheme::getGlobalTheme(), *this); } Point AudioGraphNodeView::getPortCenter (int busIndex, bool isInput) const diff --git a/modules/yup_audio_gui/graph/yup_AudioGraphNodeView.h b/modules/yup_audio_gui/graph/yup_AudioGraphNodeView.h index b11b93415..37d1b2e94 100644 --- a/modules/yup_audio_gui/graph/yup_AudioGraphNodeView.h +++ b/modules/yup_audio_gui/graph/yup_AudioGraphNodeView.h @@ -35,6 +35,20 @@ namespace yup class YUP_API AudioGraphNodeView : public Component { public: + /** Style identifiers for theme customization. */ + struct Style + { + static const Identifier shadowColorId; + static const Identifier accentBackgroundColorId; + static const Identifier bodyBackgroundColorId; + static const Identifier headerBackgroundColorId; + static const Identifier textColorId; + static const Identifier subtitleTextColorId; + static const Identifier parameterBackgroundColorId; + static const Identifier parameterValueBackgroundColorId; + static const Identifier portHoleColorId; + }; + /** Semantic signal type for ports and future connection routing. */ enum class PortKind { @@ -122,7 +136,7 @@ class YUP_API AudioGraphNodeView : public Component virtual ParameterInfo getParameterInfo (int parameterIndex) const; /** Paints optional custom content between the header and the port rows. */ - virtual void paintNodeContent (Graphics& g, Rectangle contentBounds); + virtual void paintNodeContent (Graphics& g, Rectangle contentBounds) const; /** Returns the preferred width in canvas units. */ virtual int getPreferredWidth() const; @@ -148,6 +162,9 @@ class YUP_API AudioGraphNodeView : public Component /** @internal Sets the display scale applied by AudioGraphComponent. */ void setViewScale (float newScale); + /** @internal Returns the display scale applied by AudioGraphComponent. */ + float getViewScale() const noexcept { return viewScale; } + /** @internal */ void paint (Graphics& g) override; diff --git a/modules/yup_audio_processors/yup_audio_processors.cpp b/modules/yup_audio_processors/yup_audio_processors.cpp index d87eb5779..831317711 100644 --- a/modules/yup_audio_processors/yup_audio_processors.cpp +++ b/modules/yup_audio_processors/yup_audio_processors.cpp @@ -34,4 +34,7 @@ #include "processors/yup_AudioParameter.cpp" #include "processors/yup_AudioParameterBuilder.cpp" #include "processors/yup_AudioProcessor.cpp" + +#if YUP_MODULE_AVAILABLE_yup_gui #include "processors/yup_AudioProcessorEditor.cpp" +#endif diff --git a/modules/yup_audio_processors/yup_audio_processors.h b/modules/yup_audio_processors/yup_audio_processors.h index 638d22f9b..fc1bc4872 100644 --- a/modules/yup_audio_processors/yup_audio_processors.h +++ b/modules/yup_audio_processors/yup_audio_processors.h @@ -32,7 +32,7 @@ website: https://github.com/kunitoki/yup license: ISC - dependencies: yup_audio_basics yup_gui + dependencies: yup_audio_basics END_YUP_MODULE_DECLARATION @@ -43,7 +43,10 @@ #define YUP_AUDIO_PROCESSORS_H_INCLUDED #include + +#if YUP_MODULE_AVAILABLE_yup_gui #include +#endif //============================================================================== #include "processors/yup_AudioBus.h" @@ -52,4 +55,7 @@ #include "processors/yup_AudioParameterBuilder.h" #include "processors/yup_AudioParameterHandle.h" #include "processors/yup_AudioProcessor.h" + +#if YUP_MODULE_AVAILABLE_yup_gui #include "processors/yup_AudioProcessorEditor.h" +#endif diff --git a/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp b/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp index c66346647..fb7ab6b6c 100644 --- a/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp +++ b/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp @@ -945,6 +945,265 @@ void paintListBoxItem (Graphics& g, const ApplicationTheme& theme, const ListBox //============================================================================== #if YUP_MODULE_AVAILABLE_yup_audio_gui +constexpr float audioGraphNodeBaseHeaderHeight = 32.0f; +constexpr float audioGraphNodeBaseParameterRowHeight = 25.0f; +constexpr float audioGraphNodeBaseCornerRadius = 7.0f; +constexpr float audioGraphNodeBaseContentHeight = 8.0f; + +Rectangle audioGraphEllipseBounds (Point center, float radius) +{ + return { center.getX() - radius, center.getY() - radius, radius * 2.0f, radius * 2.0f }; +} + +void fillAudioGraphFeatheredRoundedRect (Graphics& g, Rectangle bounds, float corner, Color color, float viewScale) +{ + constexpr int numLayers = 5; + + for (int i = numLayers; i > 0; --i) + { + const auto amount = static_cast (i) * 1.4f * viewScale; + const auto alpha = 0.020f + (static_cast (numLayers - i) * 0.018f); + g.setFillColor (color.withAlpha (alpha)); + g.fillRoundedRect (bounds.reduced (-amount), corner + amount); + } +} + +void strokeAudioGraphBezierHalf (Graphics& g, Point p0, Point p1, Point p2, Point p3, Color color, float strokeWidth, bool firstHalf) +{ + const auto p01 = (p0 + p1) * 0.5f; + const auto p12 = (p1 + p2) * 0.5f; + const auto p23 = (p2 + p3) * 0.5f; + const auto p012 = (p01 + p12) * 0.5f; + const auto p123 = (p12 + p23) * 0.5f; + const auto midpoint = (p012 + p123) * 0.5f; + + Path path; + if (firstHalf) + path.moveTo (p0).cubicTo (p01, p012.getX(), p012.getY(), midpoint.getX(), midpoint.getY()); + else + path.moveTo (midpoint).cubicTo (p123, p23.getX(), p23.getY(), p3.getX(), p3.getY()); + + g.setStrokeColor (color); + g.setStrokeWidth (strokeWidth); + g.setStrokeCap (StrokeCap::Round); + g.strokePath (path); +} + +void paintAudioGraphConnection (Graphics& g, const AudioGraphComponent& graph, const AudioGraphConnection& connection, float opacity) +{ + const auto zoom = graph.getZoom(); + const auto start = graph.getEndpointScreenPosition (connection.source); + const auto end = graph.getEndpointScreenPosition (connection.destination); + + if (! graph.getLocalBounds().reduced (-200.0f).contains (start) && ! graph.getLocalBounds().reduced (-200.0f).contains (end)) + return; + + const auto controlOffset = jmax (60.0f * zoom, std::abs (end.getX() - start.getX()) * 0.5f); + const auto cp1 = Point { start.getX() + controlOffset, start.getY() }; + const auto cp2 = Point { end.getX() - controlOffset, end.getY() }; + const auto strokeWidth = jmax (1.5f, 3.0f * zoom); + + strokeAudioGraphBezierHalf (g, start, cp1, cp2, end, graph.getEndpointColor (connection.source).withMultipliedAlpha (opacity), strokeWidth, true); + strokeAudioGraphBezierHalf (g, start, cp1, cp2, end, graph.getEndpointColor (connection.destination).withMultipliedAlpha (opacity), strokeWidth, false); +} + +void paintAudioGraphPendingWire (Graphics& g, const AudioGraphComponent& graph) +{ + if (! graph.isPendingWireVisible()) + return; + + const auto activeEndpoint = graph.getPendingWireEndpoint(); + if (! activeEndpoint.has_value()) + return; + + const auto zoom = graph.getZoom(); + const auto start = graph.getEndpointScreenPosition (*activeEndpoint); + const auto end = graph.getPendingWireEndPosition(); + const auto controlOffset = jmax (60.0f * zoom, std::abs (end.getX() - start.getX()) * 0.5f); + const auto cp1 = Point { start.getX() + (activeEndpoint->isSource() ? controlOffset : -controlOffset), start.getY() }; + const auto cp2 = Point { end.getX() - (activeEndpoint->isSource() ? controlOffset : -controlOffset), end.getY() }; + + Path path; + path.moveTo (start).cubicTo (cp1, cp2.getX(), cp2.getY(), end.getX(), end.getY()); + + const auto endpointColor = graph.getEndpointColor (*activeEndpoint); + g.setStrokeColor (endpointColor.withAlpha (0.55f)); + g.setStrokeWidth (jmax (1.5f, 2.5f * zoom)); + g.setStrokeCap (StrokeCap::Round); + g.strokePath (path); + + g.setFillColor (endpointColor.withAlpha (0.20f)); + const auto center = graph.getEndpointScreenPosition (*activeEndpoint); + const auto radius = 14.0f * zoom; + g.fillEllipse (center.getX() - radius, center.getY() - radius, radius * 2.0f, radius * 2.0f); +} + +void paintAudioGraphComponent (Graphics& g, const ApplicationTheme&, const AudioGraphComponent& graph) +{ + const auto zoom = graph.getZoom(); + const auto canvasOffset = graph.getCanvasOffset(); + const auto backgroundColor = graph.findColor (AudioGraphComponent::Style::backgroundColorId) + .value_or (Color (0xff101522)); + const auto gridColor = graph.findColor (AudioGraphComponent::Style::gridColorId) + .value_or (Colors::white.withAlpha (0.045f)); + + g.setFillColor (backgroundColor); + g.fillAll(); + + const auto spacing = 24.0f * zoom; + if (spacing >= 6.0f && ! gridColor.isTransparent()) + { + const auto startX = std::fmod (canvasOffset.getX(), spacing); + const auto startY = std::fmod (canvasOffset.getY(), spacing); + + g.setFillColor (gridColor); + + for (auto x = startX; x < graph.getWidth(); x += spacing) + { + for (auto y = startY; y < graph.getHeight(); y += spacing) + g.fillEllipse (x - 1.0f, y - 1.0f, 2.0f, 2.0f); + } + } + + if (const auto* processor = graph.getGraphProcessor()) + { + for (const auto& connection : processor->getConnections()) + paintAudioGraphConnection (g, graph, connection, 1.0f); + } + + paintAudioGraphPendingWire (g, graph); +} + +void paintAudioGraphPort (Graphics& g, + const AudioGraphNodeView& node, + const Font& labelFont, + const AudioGraphNodeView::PortInfo& info, + Point center, + float viewScale, + bool isInput) +{ + const auto portRadius = node.getPortRadius(); + const auto portHoleColor = node.findColor (AudioGraphNodeView::Style::portHoleColorId) + .value_or (Color (0xff101522)); + const auto textColor = node.findColor (AudioGraphNodeView::Style::textColorId) + .value_or (Color (0xffd6d6d6)); + + g.setFillColor (info.color.withAlpha (0.24f)); + g.fillEllipse (audioGraphEllipseBounds (center, portRadius * 1.8f)); + g.setFillColor (info.color); + g.fillEllipse (audioGraphEllipseBounds (center, portRadius)); + g.setFillColor (portHoleColor); + g.fillEllipse (audioGraphEllipseBounds (center, portRadius * 0.45f)); + + g.setFillColor (textColor); + + if (isInput) + g.fillFittedText (info.name, labelFont, { center.getX() + 12.0f * viewScale, center.getY() - 8.0f * viewScale, 72.0f * viewScale, 16.0f * viewScale }, Justification::left); + else + g.fillFittedText (info.name, labelFont, { center.getX() - 84.0f * viewScale, center.getY() - 8.0f * viewScale, 72.0f * viewScale, 16.0f * viewScale }, Justification::right); +} + +void paintAudioGraphNodeView (Graphics& g, const ApplicationTheme& theme, const AudioGraphNodeView& node) +{ + const auto viewScale = node.getViewScale(); + const auto bounds = node.getLocalBounds().reduced (node.getPortRadius() * 0.5f, 0.0f); + const auto bodyBounds = bounds.reduced (2.0f * viewScale); + const auto corner = audioGraphNodeBaseCornerRadius * viewScale; + const auto accent = node.getNodeColor(); + const auto headerHeight = audioGraphNodeBaseHeaderHeight * viewScale; + + const auto shadowColor = node.findColor (AudioGraphNodeView::Style::shadowColorId) + .value_or (Colors::black); + const auto accentBackgroundColor = node.findColor (AudioGraphNodeView::Style::accentBackgroundColorId) + .value_or (accent); + const auto bodyBackgroundColor = node.findColor (AudioGraphNodeView::Style::bodyBackgroundColorId) + .value_or (Color (0xff1e2535)); + const auto headerBackgroundColor = node.findColor (AudioGraphNodeView::Style::headerBackgroundColorId) + .value_or (Color (0xff141a26)); + const auto textColor = node.findColor (AudioGraphNodeView::Style::textColorId) + .value_or (Color (0xffd6d6d6)); + const auto subtitleTextColor = node.findColor (AudioGraphNodeView::Style::subtitleTextColorId) + .value_or (Color (0xffb8b8b8)); + const auto parameterBackgroundColor = node.findColor (AudioGraphNodeView::Style::parameterBackgroundColorId) + .value_or (Color (0xff263044)); + const auto parameterValueBackgroundColor = node.findColor (AudioGraphNodeView::Style::parameterValueBackgroundColorId) + .value_or (Color (0xff1a2130)); + + fillAudioGraphFeatheredRoundedRect (g, bodyBounds.translated (0.0f, 3.0f * viewScale), corner, shadowColor, viewScale); + + g.setFillColor (accentBackgroundColor.withAlpha (0.11f)); + g.fillRoundedRect (bodyBounds.reduced (-1.5f * viewScale), corner + 2.0f * viewScale); + + g.setFillColor (bodyBackgroundColor); + g.fillRoundedRect (bodyBounds, corner); + + auto headerBounds = bodyBounds.withHeight (headerHeight); + g.setFillColor (headerBackgroundColor); + g.fillRoundedRect (headerBounds, corner, corner, 0.0f, 0.0f); + + g.setStrokeColor (accent.withAlpha (0.70f)); + g.setStrokeWidth (1.35f * viewScale); + g.strokeRoundedRect (bodyBounds, corner); + + const auto headerInner = headerBounds.reduced (9.0f * viewScale, 5.0f * viewScale); + const auto font = theme.getDefaultFont(); + + g.setFillColor (accent); + g.fillFittedText (node.getNodeTitle(), font.withHeight (12.0f * viewScale), headerInner.withHeight (18.0f * viewScale), Justification::left); + + const auto subtitle = node.getNodeSubtitle(); + if (subtitle.isNotEmpty()) + { + g.setFillColor (subtitleTextColor); + g.fillFittedText (subtitle, font.withHeight (9.0f * viewScale), headerInner.withHeight (18.0f * viewScale), Justification::right); + } + + const auto ruleY = bodyBounds.getY() + headerHeight; + g.setStrokeColor (accent.withAlpha (0.16f)); + g.strokeLine ({ bodyBounds.getX(), ruleY }, { bodyBounds.getRight(), ruleY }); + + auto contentBounds = bodyBounds.reduced (10.0f * viewScale, 0.0f); + contentBounds = contentBounds.withY (ruleY + 4.0f * viewScale).withHeight (audioGraphNodeBaseContentHeight * viewScale); + node.paintNodeContent (g, contentBounds); + + auto parameterBounds = bodyBounds.reduced (10.0f * viewScale, 0.0f); + parameterBounds = parameterBounds.withY (ruleY + (audioGraphNodeBaseContentHeight + 5.0f) * viewScale); + + const auto parameterFont = font.withHeight (9.5f * viewScale); + for (int i = 0; i < node.getNumParameterRows(); ++i) + { + const auto info = node.getParameterInfo (i); + auto row = parameterBounds.removeFromTop (audioGraphNodeBaseParameterRowHeight * viewScale).reduced (0.0f, 2.0f * viewScale); + auto valueBox = row.removeFromRight (58.0f * viewScale); + + g.setFillColor (parameterBackgroundColor); + g.fillRoundedRect (row.reduced (0.0f, 1.0f * viewScale), 3.0f * viewScale); + + if (info.normalizedValue >= 0.0f) + { + const auto fillWidth = row.getWidth() * jlimit (0.0f, 1.0f, info.normalizedValue); + g.setFillColor (info.color.withAlpha (0.20f)); + g.fillRoundedRect (row.withWidth (fillWidth).reduced (0.0f, 1.0f * viewScale), 3.0f * viewScale); + } + + g.setFillColor (textColor); + g.fillFittedText (info.name, parameterFont, row.reduced (6.0f * viewScale, 0.0f), Justification::left); + + g.setFillColor (parameterValueBackgroundColor); + g.fillRoundedRect (valueBox.reduced (0.0f, 1.0f * viewScale), 3.0f * viewScale); + g.setFillColor (info.color); + g.fillFittedText (info.value, parameterFont, valueBox.reduced (5.0f * viewScale, 0.0f), Justification::right); + } + + const auto labelFont = font.withHeight (10.5f * viewScale); + + for (int i = 0; i < node.getNumInputPorts(); ++i) + paintAudioGraphPort (g, node, labelFont, node.getInputPortInfo (i), node.getInputPortCenter (i), viewScale, true); + + for (int i = 0; i < node.getNumOutputPorts(); ++i) + paintAudioGraphPort (g, node, labelFont, node.getOutputPortInfo (i), node.getOutputPortCenter (i), viewScale, false); +} + void paintMidiKeyboard (Graphics& g, const ApplicationTheme& theme, const MidiKeyboardComponent& keyboard) { auto bounds = keyboard.getLocalBounds(); @@ -1530,6 +1789,20 @@ ApplicationTheme::Ptr createThemeVersion1() theme->setColor (KMeterComponent::Style::peakLevelColorId, Color (0xffffffff)); theme->setColor (KMeterComponent::Style::peakLevelClipColorId, Color (0xffff0000)); theme->setColor (KMeterComponent::Style::peakHoldColorId, Color (0xffffff00)); + + theme->setComponentStyle (ComponentStyle::createStyle (paintAudioGraphComponent)); + theme->setColor (AudioGraphComponent::Style::backgroundColorId, Color (0xff101522)); + theme->setColor (AudioGraphComponent::Style::gridColorId, Colors::white.withAlpha (0.045f)); + + theme->setComponentStyle (ComponentStyle::createStyle (paintAudioGraphNodeView)); + theme->setColor (AudioGraphNodeView::Style::shadowColorId, Colors::black); + theme->setColor (AudioGraphNodeView::Style::bodyBackgroundColorId, Color (0xff1e2535)); + theme->setColor (AudioGraphNodeView::Style::headerBackgroundColorId, Color (0xff141a26)); + theme->setColor (AudioGraphNodeView::Style::textColorId, Color (0xffd6d6d6)); + theme->setColor (AudioGraphNodeView::Style::subtitleTextColorId, Color (0xffb8b8b8)); + theme->setColor (AudioGraphNodeView::Style::parameterBackgroundColorId, Color (0xff263044)); + theme->setColor (AudioGraphNodeView::Style::parameterValueBackgroundColorId, Color (0xff1a2130)); + theme->setColor (AudioGraphNodeView::Style::portHoleColorId, Color (0xff101522)); #endif return theme; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 405cf2bd7..7f647a985 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -64,7 +64,6 @@ set (target_modules yup_events yup_data_model yup_graphics - yup_audio_plugin_host dr_libs pffft_library opus_library @@ -72,6 +71,10 @@ set (target_modules hmp3_library bungee_library) +if (YUP_PLATFORM_DESKTOP) + list (APPEND target_modules yup_audio_plugin_host) +endif() + if (NOT YUP_PLATFORM_EMSCRIPTEN) list (APPEND target_modules yup_gui yup_audio_gui) if (YUP_PLATFORM_DESKTOP AND NOT YUP_PLATFORM_WINDOWS) From e223743322e98aa2bd3de23591313a440c41646c Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sun, 17 May 2026 17:50:21 +0200 Subject: [PATCH 14/35] More fixes --- .../graph/yup_AudioGraphProcessor.cpp | 44 +- tests/yup_audio_gui.cpp | 1 + .../yup_audio_gui/yup_AudioGraphComponent.cpp | 815 ++++++++++++++++++ 3 files changed, 843 insertions(+), 17 deletions(-) create mode 100644 tests/yup_audio_gui/yup_AudioGraphComponent.cpp diff --git a/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.cpp b/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.cpp index e970da2be..dac75746e 100644 --- a/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.cpp +++ b/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.cpp @@ -810,34 +810,43 @@ class AudioGraphProcessor::Pimpl if (level.empty()) continue; - activeGraph = &graph; - activeLevel = &level; - activeNumSamples = numSamples; + const auto generation = workGeneration.load (std::memory_order_relaxed) + 1; + + activeGraph.store (&graph, std::memory_order_relaxed); + activeLevel.store (&level, std::memory_order_relaxed); + activeNumSamples.store (numSamples, std::memory_order_relaxed); nextJobIndex.store (0, std::memory_order_relaxed); remainingJobs.store (static_cast (level.size()), std::memory_order_release); - workGeneration.fetch_add (1, std::memory_order_release); + activeGeneration.store (generation, std::memory_order_release); + workGeneration.store (generation, std::memory_order_release); workerReadyEvent.reset(); workerReadyEvent.signal(); - drainActiveJobs(); + drainActiveJobs (generation); while (remainingJobs.load (std::memory_order_acquire) > 0) ; } - activeGraph = nullptr; - activeLevel = nullptr; - activeNumSamples = 0; + activeGraph.store (nullptr, std::memory_order_relaxed); + activeLevel.store (nullptr, std::memory_order_relaxed); + activeNumSamples.store (0, std::memory_order_relaxed); + activeGeneration.store (0, std::memory_order_release); } - void drainActiveJobs() + void drainActiveJobs (int generation) { - auto* graph = activeGraph; - auto* level = activeLevel; + if (activeGeneration.load (std::memory_order_acquire) != generation) + return; + + auto* graph = activeGraph.load (std::memory_order_relaxed); + auto* level = activeLevel.load (std::memory_order_relaxed); if (graph == nullptr || level == nullptr) return; + const int numSamples = activeNumSamples.load (std::memory_order_relaxed); + for (;;) { const int jobIndex = nextJobIndex.fetch_add (1); @@ -845,9 +854,9 @@ class AudioGraphProcessor::Pimpl if (jobIndex >= static_cast (level->size())) break; - processNode (*graph, (*level)[static_cast (jobIndex)], activeNumSamples); + processNode (*graph, (*level)[static_cast (jobIndex)], numSamples); - remainingJobs.fetch_sub (1, std::memory_order_release); + remainingJobs.fetch_sub (1, std::memory_order_acq_rel); } } @@ -1059,9 +1068,10 @@ class AudioGraphProcessor::Pimpl std::atomic workGeneration { 0 }; std::atomic nextJobIndex { 0 }; std::atomic remainingJobs { 0 }; - CompiledGraph* activeGraph = nullptr; - std::vector* activeLevel = nullptr; - int activeNumSamples = 0; + std::atomic activeGraph { nullptr }; + std::atomic*> activeLevel { nullptr }; + std::atomic activeNumSamples { 0 }; + std::atomic activeGeneration { 0 }; }; //============================================================================== @@ -1095,7 +1105,7 @@ void AudioGraphProcessor::Pimpl::WorkerThread::run() lastGeneration = generation; ScopedNoDenormals noDenormals; owner.joinWorkgroup (workgroupToken); - owner.drainActiveJobs(); + owner.drainActiveJobs (generation); } } diff --git a/tests/yup_audio_gui.cpp b/tests/yup_audio_gui.cpp index 3b236b95b..0538b6de5 100644 --- a/tests/yup_audio_gui.cpp +++ b/tests/yup_audio_gui.cpp @@ -22,6 +22,7 @@ #include "yup_audio_gui/yup_AudioPeakProfile.cpp" #include "yup_audio_gui/yup_AudioPeakProfileCache.cpp" #include "yup_audio_gui/yup_AudioThumbnail.cpp" +#include "yup_audio_gui/yup_AudioGraphComponent.cpp" #include "yup_audio_gui/yup_AudioViewComponent.cpp" #include "yup_audio_gui/yup_CartesianPlane.cpp" #include "yup_audio_gui/yup_KMeterComponent.cpp" diff --git a/tests/yup_audio_gui/yup_AudioGraphComponent.cpp b/tests/yup_audio_gui/yup_AudioGraphComponent.cpp new file mode 100644 index 000000000..a5004b9bd --- /dev/null +++ b/tests/yup_audio_gui/yup_AudioGraphComponent.cpp @@ -0,0 +1,815 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +#include +#include +#include + +using namespace yup; + +namespace +{ +constexpr float pointTolerance = 0.001f; + +AudioBusLayout stereoLayout() +{ + return AudioBusLayout ({ AudioBus ("Input", AudioBus::Type::Audio, AudioBus::Direction::Input, 2) }, + { AudioBus ("Output", AudioBus::Type::Audio, AudioBus::Direction::Output, 2) }); +} + +void expectPointNear (Point actual, Point expected, float tolerance = pointTolerance) +{ + EXPECT_NEAR (expected.getX(), actual.getX(), tolerance); + EXPECT_NEAR (expected.getY(), actual.getY(), tolerance); +} + +void expectRectangleInside (const Rectangle& outer, const Rectangle& inner) +{ + EXPECT_LE (outer.getLeft(), inner.getLeft()); + EXPECT_LE (outer.getTop(), inner.getTop()); + EXPECT_GE (outer.getRight(), inner.getRight()); + EXPECT_GE (outer.getBottom(), inner.getBottom()); +} + +class TestProcessor : public AudioProcessor +{ +public: + TestProcessor() + : AudioProcessor ("Graph Component Test Processor", stereoLayout()) + { + } + + void prepareToPlay (float, int) override {} + + void releaseResources() override {} + + void processBlock (AudioBuffer&, MidiBuffer&) override {} + + int getLatencySamples() override { return 0; } + + int getCurrentPreset() const noexcept override { return 0; } + + void setCurrentPreset (int) noexcept override {} + + int getNumPresets() const override { return 0; } + + String getPresetName (int) const override { return {}; } + + void setPresetName (int, StringRef) override {} + + Result loadStateFromMemory (const MemoryBlock&) override { return Result::ok(); } + + Result saveStateIntoMemory (MemoryBlock&) override { return Result::ok(); } + + bool hasEditor() const override { return false; } +}; + +class TestNodeView : public AudioGraphNodeView +{ +public: + explicit TestNodeView (AudioGraphNodeID nodeID, + String titleToUse = "Node", + int inputsToUse = 1, + int outputsToUse = 1, + int parametersToUse = 0, + int preferredWidthToUse = 140) + : AudioGraphNodeView (nodeID) + , title (std::move (titleToUse)) + , inputs (inputsToUse) + , outputs (outputsToUse) + , parameters (parametersToUse) + , preferredWidth (preferredWidthToUse) + { + } + + String getNodeTitle() const override { return title; } + + int getNumInputPorts() const override { return inputs; } + + int getNumOutputPorts() const override { return outputs; } + + int getNumParameterRows() const override { return parameters; } + + int getPreferredWidth() const override { return preferredWidth; } + + Color getNodeColor() const override { return nodeColor; } + + String getNodeSubtitle() const override { return subtitle; } + + PortInfo getInputPortInfo (int busIndex) const override + { + return { String ("input ") + String (busIndex), inputColor, PortKind::audio }; + } + + PortInfo getOutputPortInfo (int busIndex) const override + { + return { String ("output ") + String (busIndex), outputColor, PortKind::audio }; + } + + ParameterInfo getParameterInfo (int parameterIndex) const override + { + return { String ("parameter ") + String (parameterIndex), + String (parameterIndex), + parameterColor, + 0.25f * static_cast (parameterIndex + 1), + PortKind::parameter }; + } + + Color nodeColor = Color (0xff4466aa); + Color inputColor = Color (0xff112233); + Color outputColor = Color (0xff445566); + Color parameterColor = Color (0xff778899); + String subtitle = "Subtitle"; + +private: + String title; + int inputs = 1; + int outputs = 1; + int parameters = 0; + int preferredWidth = 140; +}; + +class RecordingGraphListener : public AudioGraphComponent::Listener +{ +public: + void connectionAdded (const AudioGraphConnection& connection) override + { + addedConnections.push_back (connection); + } + + void connectionRemoved (const AudioGraphConnection& connection) override + { + removedConnections.push_back (connection); + } + + void nodeViewMoved (AudioGraphNodeID nodeID, Point newCanvasPos) override + { + movedNodeID = nodeID; + movedCanvasPosition = newCanvasPos; + ++nodeMoveCount; + } + + std::vector addedConnections; + std::vector removedConnections; + AudioGraphNodeID movedNodeID; + Point movedCanvasPosition; + int nodeMoveCount = 0; +}; + +class AudioGraphComponentTests : public ::testing::Test +{ +protected: + void SetUp() override + { + graph = std::make_shared(); + component = std::make_unique (graph); + component->setBounds (0.0f, 0.0f, 800.0f, 600.0f); + } + + void TearDown() override + { + component.reset(); + graph.reset(); + } + + AudioGraphNodeID addProcessorNode() + { + return graph->addNode (std::make_unique()); + } + + TestNodeView* addNodeView (AudioGraphNodeID nodeID, + Point canvasPosition, + int inputs = 1, + int outputs = 1, + int parameters = 0, + int preferredWidth = 140) + { + auto view = std::make_unique (nodeID, "Node", inputs, outputs, parameters, preferredWidth); + auto* rawView = view.get(); + component->addNodeView (nodeID, std::move (view), canvasPosition); + return rawView; + } + + static MouseEvent mouseEventForComponent (MouseEvent::Buttons buttons, Point position) + { + return MouseEvent (buttons, KeyModifiers(), position); + } + + static MouseEvent mouseEventForSource (MouseEvent::Buttons buttons, Component* source, Point positionInSource) + { + return MouseEvent (buttons, KeyModifiers(), positionInSource, source); + } + + std::shared_ptr graph; + std::unique_ptr component; +}; + +} // namespace + +//============================================================================== +// AudioGraphNodeView Tests +//============================================================================== + +TEST (AudioGraphNodeViewTests, DefaultsDescribeBasicAudioNode) +{ + TestNodeView view (AudioGraphNodeID (1)); + + EXPECT_EQ (AudioGraphNodeID (1), view.getNodeID()); + EXPECT_EQ (String ("Node"), view.getNodeTitle()); + EXPECT_EQ (String ("Subtitle"), view.getNodeSubtitle()); + EXPECT_EQ (1, view.getNumInputPorts()); + EXPECT_EQ (1, view.getNumOutputPorts()); + EXPECT_EQ (0, view.getNumParameterRows()); + EXPECT_EQ (140, view.getPreferredWidth()); + EXPECT_EQ (82, view.getPreferredHeight()); + EXPECT_EQ (Color (0xff4466aa), view.getNodeColor()); +} + +TEST (AudioGraphNodeViewTests, PreferredHeightAccountsForPortsAndParameterRows) +{ + TestNodeView onePortNoParameters (AudioGraphNodeID (1), "One", 0, 0, 0); + TestNodeView threePortsTwoParameters (AudioGraphNodeID (2), "Three", 3, 2, 2); + TestNodeView twoOutputsFourParameters (AudioGraphNodeID (3), "Outputs", 1, 2, 4); + + EXPECT_EQ (82, onePortNoParameters.getPreferredHeight()); + EXPECT_EQ (180, threePortsTwoParameters.getPreferredHeight()); + EXPECT_EQ (206, twoOutputsFourParameters.getPreferredHeight()); +} + +TEST (AudioGraphNodeViewTests, PortCentersUseScaledLocalGeometry) +{ + TestNodeView view (AudioGraphNodeID (1), "Scaled", 2, 2, 0); + view.setBounds (0.0f, 0.0f, 140.0f, static_cast (view.getPreferredHeight())); + + expectPointNear (view.getInputPortCenter (0), { 5.0f, 66.0f }); + expectPointNear (view.getInputPortCenter (1), { 5.0f, 90.0f }); + expectPointNear (view.getOutputPortCenter (0), { 135.0f, 66.0f }); + expectPointNear (view.getOutputPortCenter (1), { 135.0f, 90.0f }); + + view.setViewScale (2.0f); + view.setBounds (0.0f, 0.0f, 280.0f, static_cast (view.getPreferredHeight()) * 2.0f); + + EXPECT_FLOAT_EQ (12.0f, view.getPortRadius()); + expectPointNear (view.getInputPortCenter (0), { 10.0f, 132.0f }); + expectPointNear (view.getOutputPortCenter (1), { 270.0f, 180.0f }); +} + +TEST (AudioGraphNodeViewTests, ViewScaleIsClamped) +{ + TestNodeView view (AudioGraphNodeID (1)); + + view.setViewScale (0.001f); + EXPECT_FLOAT_EQ (0.1f, view.getViewScale()); + EXPECT_FLOAT_EQ (0.6f, view.getPortRadius()); + + view.setViewScale (12.0f); + EXPECT_FLOAT_EQ (4.0f, view.getViewScale()); + EXPECT_FLOAT_EQ (24.0f, view.getPortRadius()); +} + +TEST (AudioGraphNodeViewTests, HitTestPortReturnsMatchingInputOrOutput) +{ + TestNodeView view (AudioGraphNodeID (1), "Ports", 2, 2, 0); + view.setBounds (0.0f, 0.0f, 140.0f, static_cast (view.getPreferredHeight())); + + auto inputHit = view.hitTestPort (view.getInputPortCenter (1)); + ASSERT_TRUE (inputHit.has_value()); + EXPECT_EQ (1, inputHit->busIndex); + EXPECT_TRUE (inputHit->isInput); + + auto outputHit = view.hitTestPort (view.getOutputPortCenter (0)); + ASSERT_TRUE (outputHit.has_value()); + EXPECT_EQ (0, outputHit->busIndex); + EXPECT_FALSE (outputHit->isInput); + + EXPECT_FALSE (view.hitTestPort ({ 70.0f, 12.0f }).has_value()); +} + +TEST (AudioGraphNodeViewTests, InvalidPortCentersReturnOrigin) +{ + TestNodeView view (AudioGraphNodeID (1), "Ports", 1, 1, 0); + view.setBounds (0.0f, 0.0f, 140.0f, static_cast (view.getPreferredHeight())); + + expectPointNear (view.getInputPortCenter (-1), Point()); + expectPointNear (view.getInputPortCenter (1), Point()); + expectPointNear (view.getOutputPortCenter (-1), Point()); + expectPointNear (view.getOutputPortCenter (1), Point()); +} + +TEST (AudioGraphNodeViewTests, PortKindColorsAreStable) +{ + EXPECT_EQ (Color (0xffffc43b), AudioGraphNodeView::getPortKindColor (AudioGraphNodeView::PortKind::audio)); + EXPECT_EQ (Color (0xffff4d67), AudioGraphNodeView::getPortKindColor (AudioGraphNodeView::PortKind::midi)); + EXPECT_EQ (Color (0xff2f8cff), AudioGraphNodeView::getPortKindColor (AudioGraphNodeView::PortKind::parameter)); +} + +//============================================================================== +// AudioGraphComponent View Management Tests +//============================================================================== + +TEST_F (AudioGraphComponentTests, ConstructorStoresGraphAndInitialSettings) +{ + EXPECT_EQ (graph.get(), component->getGraphProcessor()); + EXPECT_FLOAT_EQ (1.0f, component->getZoom()); + EXPECT_FLOAT_EQ (0.1f, component->getMinZoom()); + EXPECT_FLOAT_EQ (4.0f, component->getMaxZoom()); + EXPECT_FLOAT_EQ (8.0f, component->getWireHitDistance()); + EXPECT_FLOAT_EQ (4.0f, component->getDragWireThreshold()); +} + +TEST_F (AudioGraphComponentTests, AddNodeViewAddsChildAndAppliesCanvasTransform) +{ + const auto nodeID = addProcessorNode(); + auto* view = addNodeView (nodeID, { 100.0f, 50.0f }); + + ASSERT_EQ (view, component->getNodeView (nodeID)); + EXPECT_EQ (1, component->getNumChildComponents()); + EXPECT_EQ (component.get(), view->getParentComponent()); + expectPointNear (view->getBounds().getPosition(), component->canvasToScreen ({ 100.0f, 50.0f })); + EXPECT_FLOAT_EQ (static_cast (view->getPreferredWidth()), view->getWidth()); + EXPECT_FLOAT_EQ (static_cast (view->getPreferredHeight()), view->getHeight()); +} + +TEST_F (AudioGraphComponentTests, AddNodeViewReplacesExistingViewForNode) +{ + const auto nodeID = addProcessorNode(); + auto* firstView = addNodeView (nodeID, { 100.0f, 50.0f }); + ASSERT_EQ (firstView, component->getNodeView (nodeID)); + + auto replacement = std::make_unique (nodeID, "Replacement", 2, 2, 1, 180); + auto* replacementView = replacement.get(); + component->addNodeView (nodeID, std::move (replacement), { 20.0f, 30.0f }); + + EXPECT_EQ (replacementView, component->getNodeView (nodeID)); + EXPECT_EQ (1, component->getNumChildComponents()); + EXPECT_EQ (component.get(), replacementView->getParentComponent()); + expectPointNear (replacementView->getBounds().getPosition(), component->canvasToScreen ({ 20.0f, 30.0f })); +} + +TEST_F (AudioGraphComponentTests, RemoveNodeViewRemovesOnlyProcessorViews) +{ + const auto firstNodeID = addProcessorNode(); + const auto secondNodeID = addProcessorNode(); + + addNodeView (firstNodeID, { 0.0f, 0.0f }); + auto* secondView = addNodeView (secondNodeID, { 200.0f, 0.0f }); + ASSERT_EQ (2, component->getNumChildComponents()); + + component->removeNodeView (firstNodeID); + + EXPECT_EQ (nullptr, component->getNodeView (firstNodeID)); + EXPECT_EQ (secondView, component->getNodeView (secondNodeID)); + EXPECT_EQ (1, component->getNumChildComponents()); + + component->removeNodeView (AudioGraphNodeID (999)); + EXPECT_EQ (1, component->getNumChildComponents()); +} + +TEST_F (AudioGraphComponentTests, GraphInputAndOutputViewsAreReplaceable) +{ + auto input = std::make_unique (AudioGraphNodeID::invalid(), "Input", 0, 2); + auto* inputView = input.get(); + component->setGraphInputView (std::move (input), { -200.0f, 10.0f }); + + auto output = std::make_unique (AudioGraphNodeID::invalid(), "Output", 2, 0); + auto* outputView = output.get(); + component->setGraphOutputView (std::move (output), { 400.0f, 10.0f }); + + EXPECT_EQ (2, component->getNumChildComponents()); + EXPECT_EQ (component.get(), inputView->getParentComponent()); + EXPECT_EQ (component.get(), outputView->getParentComponent()); + + auto replacement = std::make_unique (AudioGraphNodeID::invalid(), "Input Replacement", 0, 1); + auto* replacementView = replacement.get(); + component->setGraphInputView (std::move (replacement), { -120.0f, 20.0f }); + + EXPECT_EQ (2, component->getNumChildComponents()); + EXPECT_EQ (component.get(), replacementView->getParentComponent()); + expectPointNear (replacementView->getBounds().getPosition(), component->canvasToScreen ({ -120.0f, 20.0f })); +} + +//============================================================================== +// AudioGraphComponent Viewport Tests +//============================================================================== + +TEST_F (AudioGraphComponentTests, ZoomAndOffsetRoundTripCanvasCoordinates) +{ + component->setCanvasOffset ({ 20.0f, -40.0f }); + component->setZoom (2.5f); + + const Point canvasPoint { 32.0f, 48.0f }; + const auto screenPoint = component->canvasToScreen (canvasPoint); + + expectPointNear (screenPoint, { 100.0f, 80.0f }); + expectPointNear (component->screenToCanvas (screenPoint), canvasPoint); +} + +TEST_F (AudioGraphComponentTests, ZoomAndThresholdSettersClampValues) +{ + component->setMinZoom (0.5f); + component->setMaxZoom (2.0f); + + component->setZoom (0.1f); + EXPECT_FLOAT_EQ (0.5f, component->getZoom()); + + component->setZoom (8.0f); + EXPECT_FLOAT_EQ (2.0f, component->getZoom()); + + component->setMinZoom (3.0f); + EXPECT_FLOAT_EQ (3.0f, component->getMinZoom()); + EXPECT_FLOAT_EQ (3.0f, component->getMaxZoom()); + EXPECT_FLOAT_EQ (3.0f, component->getZoom()); + + component->setWireHitDistance (-4.0f); + component->setDragWireThreshold (-9.0f); + + EXPECT_FLOAT_EQ (0.0f, component->getWireHitDistance()); + EXPECT_FLOAT_EQ (0.0f, component->getDragWireThreshold()); +} + +TEST_F (AudioGraphComponentTests, CenterViewOnNodesCentersSingleNode) +{ + const auto nodeID = addProcessorNode(); + auto* view = addNodeView (nodeID, { 100.0f, 50.0f }); + + component->centerViewOnNodes(); + + expectPointNear (view->getBounds().getCenter(), component->getLocalBounds().getCenter()); +} + +TEST_F (AudioGraphComponentTests, CenterViewOnEmptyGraphUsesComponentCenterAsOffset) +{ + component->setCanvasOffset ({ 10.0f, 20.0f }); + component->centerViewOnNodes(); + + expectPointNear (component->getCanvasOffset(), component->getLocalBounds().getCenter()); +} + +TEST_F (AudioGraphComponentTests, ResetViewRestoresUnitZoomAndCentersNodes) +{ + const auto nodeID = addProcessorNode(); + auto* view = addNodeView (nodeID, { 150.0f, 75.0f }); + + component->setZoom (2.0f); + component->setCanvasOffset ({ 0.0f, 0.0f }); + component->resetView(); + + EXPECT_FLOAT_EQ (1.0f, component->getZoom()); + expectPointNear (view->getBounds().getCenter(), component->getLocalBounds().getCenter()); +} + +TEST_F (AudioGraphComponentTests, ZoomToFitNodesFitsAllViewsInsidePadding) +{ + const auto firstNodeID = addProcessorNode(); + const auto secondNodeID = addProcessorNode(); + auto* firstView = addNodeView (firstNodeID, { 0.0f, 0.0f }); + auto* secondView = addNodeView (secondNodeID, { 600.0f, 420.0f }, 2, 2, 1, 180); + + component->zoomToFitNodes (50.0f, 1.0f); + + const Rectangle paddedViewport = component->getLocalBounds().reduced (50.0f); + expectRectangleInside (paddedViewport, firstView->getBounds()); + expectRectangleInside (paddedViewport, secondView->getBounds()); + EXPECT_LE (component->getZoom(), 1.0f); +} + +TEST_F (AudioGraphComponentTests, DeferredViewportOperationsRunOnResize) +{ + auto deferredComponent = std::make_unique (graph); + const auto nodeID = addProcessorNode(); + auto view = std::make_unique (nodeID); + auto* rawView = view.get(); + + deferredComponent->addNodeView (nodeID, std::move (view), { 100.0f, 100.0f }); + deferredComponent->zoomToFitNodes (24.0f, 0.75f); + deferredComponent->setBounds (0.0f, 0.0f, 320.0f, 240.0f); + + EXPECT_LE (deferredComponent->getZoom(), 0.75f); + EXPECT_EQ (deferredComponent.get(), rawView->getParentComponent()); +} + +TEST_F (AudioGraphComponentTests, MouseWheelZoomsAroundCursor) +{ + const auto cursor = Point { 280.0f, 220.0f }; + const auto canvasBefore = component->screenToCanvas (cursor); + + component->mouseWheel (mouseEventForComponent (MouseEvent::noButtons, cursor), MouseWheelData (0.0f, 2.0f)); + + EXPECT_GT (component->getZoom(), 1.0f); + expectPointNear (component->screenToCanvas (cursor), canvasBefore); +} + +TEST_F (AudioGraphComponentTests, SpacebarDragPansCanvas) +{ + const auto originalOffset = component->getCanvasOffset(); + + component->keyDown (KeyPress (KeyPress::spaceKey), Point()); + component->mouseDown (mouseEventForComponent (MouseEvent::leftButton, { 100.0f, 100.0f })); + component->mouseDrag (mouseEventForComponent (MouseEvent::leftButton, { 130.0f, 145.0f })); + component->mouseUp (mouseEventForComponent (MouseEvent::leftButton, { 130.0f, 145.0f })); + component->keyUp (KeyPress (KeyPress::spaceKey), Point()); + + expectPointNear (component->getCanvasOffset(), originalOffset + Point (30.0f, 45.0f)); +} + +//============================================================================== +// AudioGraphComponent Endpoint Tests +//============================================================================== + +TEST_F (AudioGraphComponentTests, EndpointScreenPositionUsesNodePortCenters) +{ + const auto nodeID = addProcessorNode(); + auto* view = addNodeView (nodeID, { 100.0f, 50.0f }, 2, 2); + + expectPointNear (component->getEndpointScreenPosition (AudioGraphEndpoint::nodeInput (nodeID, 1)), + component->getLocalPoint (view, view->getInputPortCenter (1))); + expectPointNear (component->getEndpointScreenPosition (AudioGraphEndpoint::nodeOutput (nodeID, 0)), + component->getLocalPoint (view, view->getOutputPortCenter (0))); +} + +TEST_F (AudioGraphComponentTests, EndpointScreenPositionUsesGraphBoundaryViewsWhenPresent) +{ + auto input = std::make_unique (AudioGraphNodeID::invalid(), "Input", 0, 2); + auto* inputView = input.get(); + component->setGraphInputView (std::move (input), { -200.0f, 10.0f }); + + auto output = std::make_unique (AudioGraphNodeID::invalid(), "Output", 2, 0); + auto* outputView = output.get(); + component->setGraphOutputView (std::move (output), { 400.0f, 10.0f }); + + expectPointNear (component->getEndpointScreenPosition (AudioGraphEndpoint::graphInput (1)), + component->getLocalPoint (inputView, inputView->getOutputPortCenter (1))); + expectPointNear (component->getEndpointScreenPosition (AudioGraphEndpoint::graphOutput (0)), + component->getLocalPoint (outputView, outputView->getInputPortCenter (0))); +} + +TEST_F (AudioGraphComponentTests, EndpointScreenPositionFallsBackForMissingGraphBoundaryViews) +{ + const auto nodeID = addProcessorNode(); + addNodeView (nodeID, { 100.0f, 50.0f }); + + expectPointNear (component->getEndpointScreenPosition (AudioGraphEndpoint::graphInput (0)), + component->canvasToScreen ({ 4.0f, 110.0f })); + expectPointNear (component->getEndpointScreenPosition (AudioGraphEndpoint::graphOutput (1)), + component->canvasToScreen ({ 336.0f, 146.0f })); +} + +TEST_F (AudioGraphComponentTests, EndpointColorUsesPortMetadataAndFallback) +{ + const auto nodeID = addProcessorNode(); + auto* view = addNodeView (nodeID, { 100.0f, 50.0f }); + view->inputColor = Color (0xff010203); + view->outputColor = Color (0xff040506); + + EXPECT_EQ (Color (0xff010203), component->getEndpointColor (AudioGraphEndpoint::nodeInput (nodeID, 0))); + EXPECT_EQ (Color (0xff040506), component->getEndpointColor (AudioGraphEndpoint::nodeOutput (nodeID, 0))); + EXPECT_EQ (AudioGraphNodeView::getPortKindColor (AudioGraphNodeView::PortKind::audio), + component->getEndpointColor (AudioGraphEndpoint::nodeOutput (AudioGraphNodeID (999), 0))); +} + +TEST_F (AudioGraphComponentTests, GraphBoundaryEndpointColorUsesOppositeSidePortMetadata) +{ + auto input = std::make_unique (AudioGraphNodeID::invalid(), "Input", 0, 1); + auto* inputView = input.get(); + inputView->outputColor = Color (0xff101112); + component->setGraphInputView (std::move (input), { -200.0f, 10.0f }); + + auto output = std::make_unique (AudioGraphNodeID::invalid(), "Output", 1, 0); + auto* outputView = output.get(); + outputView->inputColor = Color (0xff131415); + component->setGraphOutputView (std::move (output), { 400.0f, 10.0f }); + + EXPECT_EQ (Color (0xff101112), component->getEndpointColor (AudioGraphEndpoint::graphInput (0))); + EXPECT_EQ (Color (0xff131415), component->getEndpointColor (AudioGraphEndpoint::graphOutput (0))); +} + +TEST_F (AudioGraphComponentTests, MouseDownOnEndpointArmsPendingWire) +{ + const auto nodeID = addProcessorNode(); + addNodeView (nodeID, { 100.0f, 50.0f }); + + const auto endpoint = AudioGraphEndpoint::nodeOutput (nodeID, 0); + const auto screenPosition = component->getEndpointScreenPosition (endpoint); + + EXPECT_FALSE (component->isPendingWireVisible()); + + component->mouseDown (mouseEventForComponent (MouseEvent::leftButton, screenPosition)); + + ASSERT_TRUE (component->isPendingWireVisible()); + ASSERT_TRUE (component->getPendingWireEndpoint().has_value()); + EXPECT_EQ (endpoint, *component->getPendingWireEndpoint()); + expectPointNear (component->getPendingWireEndPosition(), screenPosition); +} + +TEST_F (AudioGraphComponentTests, DragPastThresholdTurnsPendingWireIntoDraggedWire) +{ + const auto nodeID = addProcessorNode(); + addNodeView (nodeID, { 100.0f, 50.0f }); + + const auto start = component->getEndpointScreenPosition (AudioGraphEndpoint::nodeOutput (nodeID, 0)); + const auto end = start + Point (30.0f, 10.0f); + + component->mouseDown (mouseEventForComponent (MouseEvent::leftButton, start)); + component->mouseDrag (mouseEventForComponent (MouseEvent::leftButton, end)); + + EXPECT_TRUE (component->isPendingWireVisible()); + expectPointNear (component->getPendingWireEndPosition(), end); +} + +TEST_F (AudioGraphComponentTests, ClickingAwayClearsArmedPendingWire) +{ + const auto nodeID = addProcessorNode(); + addNodeView (nodeID, { 100.0f, 50.0f }); + + const auto endpointPosition = component->getEndpointScreenPosition (AudioGraphEndpoint::nodeOutput (nodeID, 0)); + component->mouseDown (mouseEventForComponent (MouseEvent::leftButton, endpointPosition)); + ASSERT_TRUE (component->isPendingWireVisible()); + + component->mouseDown (mouseEventForComponent (MouseEvent::leftButton, { 20.0f, 20.0f })); + + EXPECT_FALSE (component->isPendingWireVisible()); + EXPECT_FALSE (component->getPendingWireEndpoint().has_value()); +} + +//============================================================================== +// AudioGraphComponent Interaction Tests +//============================================================================== + +TEST_F (AudioGraphComponentTests, ClickingCompatibleEndpointsAddsConnectionAndNotifiesListener) +{ + const auto nodeID = addProcessorNode(); + addNodeView (nodeID, { 100.0f, 50.0f }); + + auto inputView = std::make_unique (AudioGraphNodeID::invalid(), "Graph Input", 0, 1); + component->setGraphInputView (std::move (inputView), { -120.0f, 50.0f }); + + RecordingGraphListener listener; + component->addListener (&listener); + + const auto source = AudioGraphEndpoint::graphInput (0); + const auto destination = AudioGraphEndpoint::nodeInput (nodeID, 0); + + component->mouseDown (mouseEventForComponent (MouseEvent::leftButton, component->getEndpointScreenPosition (source))); + component->mouseDown (mouseEventForComponent (MouseEvent::leftButton, component->getEndpointScreenPosition (destination))); + + const auto connections = graph->getConnections(); + ASSERT_EQ (1u, connections.size()); + EXPECT_EQ (AudioGraphConnection (source, destination), connections.front()); + + ASSERT_EQ (1u, listener.addedConnections.size()); + EXPECT_EQ (AudioGraphConnection (source, destination), listener.addedConnections.front()); + + component->removeListener (&listener); +} + +TEST_F (AudioGraphComponentTests, DraggingCompatibleEndpointsAddsConnectionAndClearsPendingWire) +{ + const auto nodeID = addProcessorNode(); + addNodeView (nodeID, { 100.0f, 50.0f }); + + auto inputView = std::make_unique (AudioGraphNodeID::invalid(), "Graph Input", 0, 1); + component->setGraphInputView (std::move (inputView), { -120.0f, 50.0f }); + + const auto source = AudioGraphEndpoint::graphInput (0); + const auto destination = AudioGraphEndpoint::nodeInput (nodeID, 0); + const auto sourcePosition = component->getEndpointScreenPosition (source); + const auto destinationPosition = component->getEndpointScreenPosition (destination); + + component->mouseDown (mouseEventForComponent (MouseEvent::leftButton, sourcePosition)); + component->mouseDrag (mouseEventForComponent (MouseEvent::leftButton, sourcePosition + Point (20.0f, 0.0f))); + component->mouseUp (mouseEventForComponent (MouseEvent::leftButton, destinationPosition)); + + const auto connections = graph->getConnections(); + ASSERT_EQ (1u, connections.size()); + EXPECT_EQ (AudioGraphConnection (source, destination), connections.front()); + EXPECT_FALSE (component->isPendingWireVisible()); + EXPECT_FALSE (component->getPendingWireEndpoint().has_value()); +} + +TEST_F (AudioGraphComponentTests, IncompatibleEndpointPairKeepsNewEndpointArmedAndDoesNotConnect) +{ + const auto firstNodeID = addProcessorNode(); + const auto secondNodeID = addProcessorNode(); + addNodeView (firstNodeID, { 100.0f, 50.0f }); + addNodeView (secondNodeID, { 320.0f, 50.0f }); + + const auto firstInput = AudioGraphEndpoint::nodeInput (firstNodeID, 0); + const auto secondInput = AudioGraphEndpoint::nodeInput (secondNodeID, 0); + + component->mouseDown (mouseEventForComponent (MouseEvent::leftButton, component->getEndpointScreenPosition (firstInput))); + component->mouseDown (mouseEventForComponent (MouseEvent::leftButton, component->getEndpointScreenPosition (secondInput))); + + EXPECT_TRUE (graph->getConnections().empty()); + ASSERT_TRUE (component->getPendingWireEndpoint().has_value()); + EXPECT_EQ (secondInput, *component->getPendingWireEndpoint()); +} + +TEST_F (AudioGraphComponentTests, RightClickEndpointRemovesAllConnectionsForEndpoint) +{ + const auto nodeID = addProcessorNode(); + addNodeView (nodeID, { 100.0f, 50.0f }); + + auto inputView = std::make_unique (AudioGraphNodeID::invalid(), "Graph Input", 0, 1); + component->setGraphInputView (std::move (inputView), { -120.0f, 50.0f }); + + const AudioGraphConnection connection { AudioGraphEndpoint::graphInput (0), + AudioGraphEndpoint::nodeInput (nodeID, 0) }; + ASSERT_TRUE (graph->addConnection (connection).wasOk()); + ASSERT_TRUE (graph->commitChanges().wasOk()); + + RecordingGraphListener listener; + component->addListener (&listener); + + component->mouseDown (mouseEventForComponent (MouseEvent::rightButton, component->getEndpointScreenPosition (connection.source))); + + EXPECT_TRUE (graph->getConnections().empty()); + ASSERT_EQ (1u, listener.removedConnections.size()); + EXPECT_EQ (connection, listener.removedConnections.front()); + + component->removeListener (&listener); +} + +TEST_F (AudioGraphComponentTests, RightClickConnectionRemovesSingleConnection) +{ + const auto nodeID = addProcessorNode(); + addNodeView (nodeID, { 100.0f, 50.0f }); + + auto inputView = std::make_unique (AudioGraphNodeID::invalid(), "Graph Input", 0, 1); + component->setGraphInputView (std::move (inputView), { -120.0f, 50.0f }); + + const AudioGraphConnection connection { AudioGraphEndpoint::graphInput (0), + AudioGraphEndpoint::nodeInput (nodeID, 0) }; + ASSERT_TRUE (graph->addConnection (connection).wasOk()); + ASSERT_TRUE (graph->commitChanges().wasOk()); + + const auto midpoint = (component->getEndpointScreenPosition (connection.source) + + component->getEndpointScreenPosition (connection.destination)) + * 0.5f; + component->setWireHitDistance (64.0f); + + component->mouseDown (mouseEventForComponent (MouseEvent::rightButton, midpoint)); + + EXPECT_TRUE (graph->getConnections().empty()); +} + +TEST_F (AudioGraphComponentTests, DraggingNodeUpdatesCanvasPositionAndNotifiesListener) +{ + const auto nodeID = addProcessorNode(); + auto* view = addNodeView (nodeID, { 100.0f, 50.0f }); + RecordingGraphListener listener; + component->addListener (&listener); + + const auto localStart = view->getLocalBounds().getCenter(); + component->mouseDown (mouseEventForSource (MouseEvent::leftButton, view, localStart)); + component->mouseDrag (mouseEventForSource (MouseEvent::leftButton, view, localStart + Point (25.0f, 35.0f))); + component->mouseUp (mouseEventForComponent (MouseEvent::leftButton, component->getLocalBounds().getCenter())); + + EXPECT_EQ (1, listener.nodeMoveCount); + EXPECT_EQ (nodeID, listener.movedNodeID); + expectPointNear (listener.movedCanvasPosition, { 125.0f, 85.0f }); + expectPointNear (view->getBounds().getPosition(), component->canvasToScreen ({ 125.0f, 85.0f })); + + component->removeListener (&listener); +} + +TEST_F (AudioGraphComponentTests, DraggingGraphBoundaryViewDoesNotNotifyNodeMoveListener) +{ + auto graphInput = std::make_unique (AudioGraphNodeID::invalid(), "Graph Input", 0, 1); + auto* graphInputView = graphInput.get(); + component->setGraphInputView (std::move (graphInput), { -120.0f, 50.0f }); + + RecordingGraphListener listener; + component->addListener (&listener); + + const auto localStart = graphInputView->getLocalBounds().getCenter(); + component->mouseDown (mouseEventForSource (MouseEvent::leftButton, graphInputView, localStart)); + component->mouseDrag (mouseEventForSource (MouseEvent::leftButton, graphInputView, localStart + Point (25.0f, 35.0f))); + component->mouseUp (mouseEventForComponent (MouseEvent::leftButton, component->getLocalBounds().getCenter())); + + EXPECT_EQ (0, listener.nodeMoveCount); + expectPointNear (graphInputView->getBounds().getPosition(), component->canvasToScreen ({ -95.0f, 85.0f })); + + component->removeListener (&listener); +} From 500226b614e9f3d24b1092d127eb2c471769fbad Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sun, 17 May 2026 21:14:47 +0200 Subject: [PATCH 15/35] Remove invalid min c++ standard --- modules/yup_audio_plugin_host/yup_audio_plugin_host.h | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/yup_audio_plugin_host/yup_audio_plugin_host.h b/modules/yup_audio_plugin_host/yup_audio_plugin_host.h index e861f3987..9826cf05f 100644 --- a/modules/yup_audio_plugin_host/yup_audio_plugin_host.h +++ b/modules/yup_audio_plugin_host/yup_audio_plugin_host.h @@ -31,7 +31,6 @@ description: In-process hosting of VST3, CLAP, and AUv2 audio plugins. website: https://github.com/kunitoki/yup license: ISC - minimumCppStandard: 17 dependencies: yup_audio_processors searchpaths: native From 48ba99d9bf9c0d14ebbe854036d9dc76db1445d6 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sun, 17 May 2026 21:15:01 +0200 Subject: [PATCH 16/35] Removed deprectaed macros mess in XmlElement --- modules/yup_core/xml/yup_XmlElement.h | 56 --------------------------- 1 file changed, 56 deletions(-) diff --git a/modules/yup_core/xml/yup_XmlElement.h b/modules/yup_core/xml/yup_XmlElement.h index 715082c87..a0d9a81b2 100644 --- a/modules/yup_core/xml/yup_XmlElement.h +++ b/modules/yup_core/xml/yup_XmlElement.h @@ -810,60 +810,4 @@ class YUP_API XmlElement YUP_LEAK_DETECTOR (XmlElement) }; -//============================================================================== -#ifndef DOXYGEN - -/** DEPRECATED: A handy macro to make it easy to iterate all the child elements in an XmlElement. - - New code should avoid this macro, and instead use getChildIterator directly. - - The parentXmlElement should be a reference to the parent XML, and the childElementVariableName - will be the name of a pointer to each child element. - - E.g. @code - XmlElement* myParentXml = createSomeKindOfXmlDocument(); - - forEachXmlChildElement (*myParentXml, child) - { - if (child->hasTagName ("FOO")) - doSomethingWithXmlElement (child); - } - - @endcode - - @see forEachXmlChildElementWithTagName -*/ -#define forEachXmlChildElement(parentXmlElement, childElementVariableName) \ - for (auto*(childElementVariableName) : ((parentXmlElement).macroBasedForLoop(), (parentXmlElement).getChildIterator())) - -/** DEPRECATED: A macro that makes it easy to iterate all the child elements of an XmlElement - which have a specified tag. - - New code should avoid this macro, and instead use getChildWithTagNameIterator directly. - - This does the same job as the forEachXmlChildElement macro, but only for those - elements that have a particular tag name. - - The parentXmlElement should be a reference to the parent XML, and the childElementVariableName - will be the name of a pointer to each child element. The requiredTagName is the - tag name to match. - - E.g. @code - XmlElement* myParentXml = createSomeKindOfXmlDocument(); - - forEachXmlChildElementWithTagName (*myParentXml, child, "MYTAG") - { - // the child object is now guaranteed to be a element.. - doSomethingWithMYTAGElement (child); - } - - @endcode - - @see forEachXmlChildElement -*/ -#define forEachXmlChildElementWithTagName(parentXmlElement, childElementVariableName, requiredTagName) \ - for (auto*(childElementVariableName) : ((parentXmlElement).macroBasedForLoop(), (parentXmlElement).getChildWithTagNameIterator ((requiredTagName)))) - -#endif - } // namespace yup From 61cdbdbabe722b73e299d3f44c3b661a2fb046d8 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sun, 17 May 2026 21:28:26 +0200 Subject: [PATCH 17/35] Fix agents --- AGENTS.md | 1 - CLAUDE.md | 1 - 2 files changed, 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d5ef51a77..9b1abcea1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,7 +57,6 @@ For main module headers (e.g., `yup_graphics.h`), include this declaration block description: Brief module description website: https://github.com/kunitoki/yup license: ISC - minimumCppStandard: 17 dependencies: yup_graphics [other_dependencies] searchpaths: native diff --git a/CLAUDE.md b/CLAUDE.md index d5ef51a77..9b1abcea1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,7 +57,6 @@ For main module headers (e.g., `yup_graphics.h`), include this declaration block description: Brief module description website: https://github.com/kunitoki/yup license: ISC - minimumCppStandard: 17 dependencies: yup_graphics [other_dependencies] searchpaths: native From e70471056e5c140f39e1d764fdbb90e4c8a29df4 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sun, 17 May 2026 21:53:10 +0200 Subject: [PATCH 18/35] More fixes --- .../graph/yup_AudioGraphProcessor.cpp | 353 ++++++++++++++++-- .../graph/yup_AudioGraphComponent.cpp | 29 +- .../yup_AudioGraphProcessor.cpp | 261 +++++++++++++ .../yup_audio_gui/yup_AudioGraphComponent.cpp | 32 ++ 4 files changed, 642 insertions(+), 33 deletions(-) diff --git a/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.cpp b/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.cpp index dac75746e..26ace7f76 100644 --- a/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.cpp +++ b/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.cpp @@ -73,6 +73,9 @@ bool isValidBusIndex (Span buses, int busIndex) { return busIndex >= 0 && busIndex < static_cast (buses.size()); } + +constexpr int audioGraphStateMagic = 0x59414731; // YAG1 +constexpr int audioGraphStateVersion = 2; } // namespace //============================================================================== @@ -96,6 +99,7 @@ class AudioGraphProcessor::Pimpl { AudioGraphNodeID id; std::shared_ptr processor; + AudioGraphNodeProperties properties; float preparedSampleRate = 0.0f; int preparedBlockSize = 0; bool prepared = false; @@ -182,6 +186,7 @@ class AudioGraphProcessor::Pimpl std::vector delayLines; AudioBuffer graphInputAudio; MidiBuffer graphInputMidi; + MidiBuffer graphOutputMidi; AudioGraphAllocationStats stats; }; @@ -193,6 +198,13 @@ class AudioGraphProcessor::Pimpl int nodeIndex = -1; }; + struct SavedNodeState + { + AudioGraphNodeID id; + AudioGraphNodeProperties properties; + MemoryBlock state; + }; + class WorkerThread final : public Thread { public: @@ -206,6 +218,16 @@ class AudioGraphProcessor::Pimpl }; AudioGraphNodeID addNode (std::unique_ptr processor) + { + if (processor == nullptr) + return AudioGraphNodeID::invalid(); + + auto properties = makeDefaultNodeProperties (*processor); + return addNode (std::move (processor), std::move (properties)); + } + + AudioGraphNodeID addNode (std::unique_ptr processor, + AudioGraphNodeProperties properties) { if (processor == nullptr) return AudioGraphNodeID::invalid(); @@ -213,11 +235,33 @@ class AudioGraphProcessor::Pimpl const std::lock_guard lock (modelMutex); const auto id = AudioGraphNodeID (++nextNodeID); - modelNodes.push_back ({ id, std::shared_ptr (std::move (processor)) }); + if (properties.name.isEmpty()) + properties.name = processor->getName(); + + modelNodes.push_back ({ id, std::shared_ptr (std::move (processor)), std::move (properties) }); + ++modelRevision; dirty.store (true); return id; } + AudioGraphNodeID addPluginNode (std::unique_ptr plugin, + float positionX, + float positionY) + { + if (plugin == nullptr) + return AudioGraphNodeID::invalid(); + + AudioGraphNodeProperties properties; + properties.kind = AudioGraphNodeProperties::Kind::plugin; + properties.identifier = plugin->getDescription().identifier; + properties.name = plugin->getDescription().name; + properties.positionX = positionX; + properties.positionY = positionY; + properties.pluginDescription = plugin->getDescription(); + + return addNode (std::move (plugin), std::move (properties)); + } + bool removeNode (AudioGraphNodeID nodeID) { if (! nodeID.isValid()) @@ -241,6 +285,7 @@ class AudioGraphProcessor::Pimpl }), modelConnections.end()); + ++modelRevision; dirty.store (true); return true; } @@ -259,6 +304,7 @@ class AudioGraphProcessor::Pimpl return Result::fail ("Audio graph connection already exists"); modelConnections.push_back (connection); + ++modelRevision; dirty.store (true); return Result::ok(); } @@ -272,7 +318,10 @@ class AudioGraphProcessor::Pimpl const bool removed = modelConnections.size() != oldSize; if (removed) + { + ++modelRevision; dirty.store (true); + } return removed; } @@ -289,18 +338,23 @@ class AudioGraphProcessor::Pimpl modelConnections.clear(); modelNodes.clear(); + ++modelRevision; dirty.store (true); } Result commitChanges() { + const std::lock_guard commitLock (commitMutex); + std::vector nodesSnapshot; std::vector connectionsSnapshot; + uint64_t snapshotRevision = 0; { const std::lock_guard lock (modelMutex); nodesSnapshot = modelNodes; connectionsSnapshot = modelConnections; + snapshotRevision = modelRevision; } for (auto& node : nodesSnapshot) @@ -321,8 +375,11 @@ class AudioGraphProcessor::Pimpl if (! result) return result; + bool snapshotIsCurrent = false; + { const std::lock_guard lock (modelMutex); + snapshotIsCurrent = snapshotRevision == modelRevision; for (auto& modelNode : modelNodes) { @@ -342,7 +399,7 @@ class AudioGraphProcessor::Pimpl storeStats (compiled->stats); latestLatencySamples.store (compiled->graphLatencySamples); - dirty.store (false); + dirty.store (! snapshotIsCurrent); delete pendingPlan.exchange (compiled.release()); deleteRetiredPlans(); @@ -387,31 +444,45 @@ class AudioGraphProcessor::Pimpl return; } - const int numSamples = std::min (audioBuffer.getNumSamples(), graph->maxBlockSize); - if (numSamples <= 0) + const int totalSamples = audioBuffer.getNumSamples(); + if (totalSamples <= 0) { audioBuffer.clear(); midiBuffer.clear(); return; } - captureGraphInput (*graph, audioBuffer, midiBuffer, numSamples); + graph->graphOutputMidi.clear(); - audioBuffer.clear(); - midiBuffer.clear(); - - if (desiredWorkerThreads.load() > 0) - { - processLevels (*graph, numSamples); - } - else + for (int startSample = 0; startSample < totalSamples; startSample += graph->maxBlockSize) { - for (const auto nodeIndex : graph->topologicalOrder) - processNode (*graph, nodeIndex, numSamples); + const int numSamples = std::min (graph->maxBlockSize, totalSamples - startSample); + + captureGraphInput (*graph, audioBuffer, midiBuffer, startSample, numSamples); + audioBuffer.clear (startSample, numSamples); + + if (desiredWorkerThreads.load() > 0) + { + processLevels (*graph, numSamples); + } + else + { + for (const auto nodeIndex : graph->topologicalOrder) + processNode (*graph, nodeIndex, numSamples); + } + + for (const auto connectionIndex : graph->graphOutputConnections) + routeConnection (*graph, + graph->connections[static_cast (connectionIndex)], + audioBuffer, + graph->graphOutputMidi, + numSamples, + startSample, + startSample); } - for (const auto connectionIndex : graph->graphOutputConnections) - routeConnection (*graph, graph->connections[static_cast (connectionIndex)], audioBuffer, midiBuffer, numSamples); + midiBuffer.clear(); + midiBuffer.swapWith (graph->graphOutputMidi); } void flush() @@ -482,6 +553,203 @@ class AudioGraphProcessor::Pimpl return iterator != modelNodes.end() ? iterator->processor.get() : nullptr; } + Result saveStateIntoMemory (MemoryBlock& memoryBlock) + { + std::vector nodesSnapshot; + std::vector connectionsSnapshot; + uint64_t snapshotNextNodeID = 0; + + { + const std::lock_guard lock (modelMutex); + nodesSnapshot = modelNodes; + connectionsSnapshot = modelConnections; + snapshotNextNodeID = nextNodeID; + } + + std::vector savedNodes; + savedNodes.reserve (nodesSnapshot.size()); + + for (const auto& node : nodesSnapshot) + { + if (node.processor == nullptr) + return Result::fail ("Audio graph contains an empty node"); + + MemoryBlock nodeState; + const auto result = node.processor->saveStateIntoMemory (nodeState); + + if (! result) + return Result::fail ("Audio graph node state save failed: " + result.getErrorMessage()); + + savedNodes.push_back ({ node.id, std::move (nodeState) }); + } + + MemoryOutputStream stream (memoryBlock, false); + stream.writeInt (audioGraphStateMagic); + stream.writeCompressedInt (audioGraphStateVersion); + stream.writeInt64 (static_cast (snapshotNextNodeID)); + stream.writeCompressedInt (static_cast (savedNodes.size())); + + for (const auto& node : savedNodes) + { + stream.writeInt64 (static_cast (node.id.getRawID())); + stream.writeCompressedInt (static_cast (node.state.getSize())); + + if (! node.state.isEmpty()) + stream.write (node.state.getData(), node.state.getSize()); + } + + stream.writeCompressedInt (static_cast (connectionsSnapshot.size())); + + for (const auto& connection : connectionsSnapshot) + { + writeEndpoint (stream, connection.source); + writeEndpoint (stream, connection.destination); + } + + stream.flush(); + return Result::ok(); + } + + Result loadStateFromMemory (const MemoryBlock& memoryBlock) + { + MemoryInputStream stream (memoryBlock, false); + + if (stream.readInt() != audioGraphStateMagic) + return Result::fail ("Audio graph state has an invalid header"); + + if (stream.readCompressedInt() != audioGraphStateVersion) + return Result::fail ("Audio graph state has an unsupported version"); + + const auto savedNextNodeID = static_cast (stream.readInt64()); + const int numNodes = stream.readCompressedInt(); + + if (numNodes < 0) + return Result::fail ("Audio graph state has an invalid node count"); + + std::vector savedNodes; + savedNodes.reserve (static_cast (numNodes)); + + for (int i = 0; i < numNodes; ++i) + { + SavedNodeState savedNode; + savedNode.id = AudioGraphNodeID (static_cast (stream.readInt64())); + + const int stateSize = stream.readCompressedInt(); + if (stateSize < 0) + return Result::fail ("Audio graph state has an invalid node state size"); + + savedNode.state.setSize (static_cast (stateSize)); + + if (stateSize > 0 && stream.read (savedNode.state.getData(), stateSize) != stateSize) + return Result::fail ("Audio graph state ended while reading node state"); + + savedNodes.push_back (std::move (savedNode)); + } + + const int numConnections = stream.readCompressedInt(); + if (numConnections < 0) + return Result::fail ("Audio graph state has an invalid connection count"); + + std::vector savedConnections; + savedConnections.reserve (static_cast (numConnections)); + + for (int i = 0; i < numConnections; ++i) + { + AudioGraphEndpoint source; + AudioGraphEndpoint destination; + + if (const auto result = readEndpoint (stream, source); result.failed()) + return result; + + if (const auto result = readEndpoint (stream, destination); result.failed()) + return result; + + savedConnections.push_back ({ source, destination }); + } + + std::vector> processors; + processors.reserve (savedNodes.size()); + + { + const std::lock_guard lock (modelMutex); + + if (savedNodes.size() != modelNodes.size()) + return Result::fail ("Audio graph state does not match the current node set"); + + for (const auto& savedNode : savedNodes) + { + const auto iterator = std::find_if (modelNodes.begin(), modelNodes.end(), [&savedNode] (const ModelNode& node) + { + return node.id == savedNode.id; + }); + + if (iterator == modelNodes.end() || iterator->processor == nullptr) + return Result::fail ("Audio graph state references a missing node"); + + processors.push_back (iterator->processor); + } + } + + for (size_t i = 0; i < savedNodes.size(); ++i) + { + const auto result = processors[i]->loadStateFromMemory (savedNodes[i].state); + + if (! result) + return Result::fail ("Audio graph node state load failed: " + result.getErrorMessage()); + } + + { + const std::lock_guard lock (modelMutex); + modelConnections = std::move (savedConnections); + nextNodeID = std::max (nextNodeID, savedNextNodeID); + ++modelRevision; + dirty.store (true); + } + + return commitChanges(); + } + + static bool writeEndpoint (OutputStream& stream, const AudioGraphEndpoint& endpoint) + { + return stream.writeCompressedInt (static_cast (endpoint.getKind())) + && stream.writeInt64 (static_cast (endpoint.getNodeID().getRawID())) + && stream.writeCompressedInt (endpoint.getBusIndex()); + } + + static Result readEndpoint (InputStream& stream, AudioGraphEndpoint& endpoint) + { + const int kind = stream.readCompressedInt(); + const auto nodeID = AudioGraphNodeID (static_cast (stream.readInt64())); + const int busIndex = stream.readCompressedInt(); + + switch (static_cast (kind)) + { + case AudioGraphEndpoint::Kind::graphInput: + if (nodeID.isValid()) + return Result::fail ("Audio graph state has an invalid graph input endpoint"); + + endpoint = AudioGraphEndpoint::graphInput (busIndex); + return Result::ok(); + + case AudioGraphEndpoint::Kind::graphOutput: + if (nodeID.isValid()) + return Result::fail ("Audio graph state has an invalid graph output endpoint"); + + endpoint = AudioGraphEndpoint::graphOutput (busIndex); + return Result::ok(); + + case AudioGraphEndpoint::Kind::nodeInput: + endpoint = AudioGraphEndpoint::nodeInput (nodeID, busIndex); + return Result::ok(); + + case AudioGraphEndpoint::Kind::nodeOutput: + endpoint = AudioGraphEndpoint::nodeOutput (nodeID, busIndex); + return Result::ok(); + } + + return Result::fail ("Audio graph state has an invalid endpoint kind"); + } + Result compileGraph (CompiledGraph& graph, const std::vector& nodesSnapshot, const std::vector& connectionsSnapshot) @@ -579,6 +847,7 @@ class AudioGraphProcessor::Pimpl const size_t midiReserveBytes = static_cast (std::max (1, numMidiConnections + 1)) * 4096; graph.graphInputMidi.ensureSize (midiReserveBytes); + graph.graphOutputMidi.ensureSize (midiReserveBytes); for (auto& node : graph.nodes) node.midiBuffer.ensureSize (midiReserveBytes); @@ -777,16 +1046,17 @@ class AudioGraphProcessor::Pimpl void captureGraphInput (CompiledGraph& graph, AudioBuffer& audioBuffer, MidiBuffer& midiBuffer, + int startSample, int numSamples) { graph.graphInputAudio.clear (0, numSamples); const int numChannels = std::min (graph.graphInputChannels, audioBuffer.getNumChannels()); for (int channel = 0; channel < numChannels; ++channel) - graph.graphInputAudio.copyFrom (channel, 0, audioBuffer, channel, 0, numSamples); + graph.graphInputAudio.copyFrom (channel, 0, audioBuffer, channel, startSample, numSamples); graph.graphInputMidi.clear(); - graph.graphInputMidi.addEvents (midiBuffer, 0, numSamples, 0); + graph.graphInputMidi.addEvents (midiBuffer, startSample, numSamples, -startSample); } void processNode (CompiledGraph& graph, int nodeIndex, int numSamples) @@ -826,6 +1096,8 @@ class AudioGraphProcessor::Pimpl while (remainingJobs.load (std::memory_order_acquire) > 0) ; + + workerReadyEvent.reset(); } activeGraph.store (nullptr, std::memory_order_relaxed); @@ -864,16 +1136,18 @@ class AudioGraphProcessor::Pimpl CompiledConnection& connection, AudioBuffer& destinationAudio, MidiBuffer& destinationMidi, - int numSamples) + int numSamples, + int destinationStartSample = 0, + int midiSampleOffset = 0) { if (connection.type == GraphSignalType::audio) { const auto& sourceAudio = getSourceAudioBuffer (graph, connection); if (connection.delayLineIndex >= 0) - routeDelayedAudio (graph.delayLines[static_cast (connection.delayLineIndex)], sourceAudio, connection, destinationAudio, numSamples); + routeDelayedAudio (graph.delayLines[static_cast (connection.delayLineIndex)], sourceAudio, connection, destinationAudio, numSamples, destinationStartSample); else - routeAudio (sourceAudio, connection, destinationAudio, numSamples); + routeAudio (sourceAudio, connection, destinationAudio, numSamples, destinationStartSample); return; } @@ -881,9 +1155,9 @@ class AudioGraphProcessor::Pimpl const auto& sourceMidi = getSourceMidiBuffer (graph, connection); if (connection.delayLineIndex >= 0) - routeDelayedMidi (graph.delayLines[static_cast (connection.delayLineIndex)], sourceMidi, destinationMidi, numSamples); + routeDelayedMidi (graph.delayLines[static_cast (connection.delayLineIndex)], sourceMidi, destinationMidi, numSamples, midiSampleOffset); else - destinationMidi.addEvents (sourceMidi, 0, numSamples, 0); + destinationMidi.addEvents (sourceMidi, 0, numSamples, midiSampleOffset); } const AudioBuffer& getSourceAudioBuffer (CompiledGraph& graph, const CompiledConnection& connection) const @@ -905,7 +1179,8 @@ class AudioGraphProcessor::Pimpl void routeAudio (const AudioBuffer& sourceAudio, const CompiledConnection& connection, AudioBuffer& destinationAudio, - int numSamples) + int numSamples, + int destinationStartSample) { const int channels = std::min ({ connection.channels, std::max (0, sourceAudio.getNumChannels() - connection.sourceOffset), @@ -913,7 +1188,7 @@ class AudioGraphProcessor::Pimpl for (int channel = 0; channel < channels; ++channel) destinationAudio.addFrom (connection.destinationOffset + channel, - 0, + destinationStartSample, sourceAudio, connection.sourceOffset + channel, 0, @@ -924,11 +1199,12 @@ class AudioGraphProcessor::Pimpl const AudioBuffer& sourceAudio, const CompiledConnection& connection, AudioBuffer& destinationAudio, - int numSamples) + int numSamples, + int destinationStartSample) { if (delayLine.delaySamples <= 0) { - routeAudio (sourceAudio, connection, destinationAudio, numSamples); + routeAudio (sourceAudio, connection, destinationAudio, numSamples, destinationStartSample); return; } @@ -945,7 +1221,7 @@ class AudioGraphProcessor::Pimpl for (int channel = 0; channel < channels; ++channel) { const float delayedSample = delayLine.audio.getReadPointer (channel)[readPosition]; - destinationAudio.addFrom (connection.destinationOffset + channel, sample, &delayedSample, 1); + destinationAudio.addFrom (connection.destinationOffset + channel, destinationStartSample + sample, &delayedSample, 1); delayLine.audio.getWritePointer (channel)[delayLine.writePosition] = sourceAudio.getReadPointer (connection.sourceOffset + channel)[sample]; @@ -958,14 +1234,15 @@ class AudioGraphProcessor::Pimpl void routeDelayedMidi (DelayLine& delayLine, const MidiBuffer& sourceMidi, MidiBuffer& destinationMidi, - int numSamples) + int numSamples, + int midiSampleOffset) { delayLine.nextPendingMidi.clear(); for (const auto metadata : delayLine.pendingMidi) { if (metadata.samplePosition < numSamples) - destinationMidi.addEvent (metadata.data, metadata.numBytes, metadata.samplePosition); + destinationMidi.addEvent (metadata.data, metadata.numBytes, midiSampleOffset + metadata.samplePosition); else delayLine.nextPendingMidi.addEvent (metadata.data, metadata.numBytes, metadata.samplePosition - numSamples); } @@ -978,7 +1255,7 @@ class AudioGraphProcessor::Pimpl const int delayedPosition = metadata.samplePosition + delayLine.delaySamples; if (delayedPosition < numSamples) - destinationMidi.addEvent (metadata.data, metadata.numBytes, delayedPosition); + destinationMidi.addEvent (metadata.data, metadata.numBytes, midiSampleOffset + delayedPosition); else delayLine.nextPendingMidi.addEvent (metadata.data, metadata.numBytes, delayedPosition - numSamples); } @@ -1041,11 +1318,13 @@ class AudioGraphProcessor::Pimpl } AudioGraphProcessor& owner; + mutable std::mutex commitMutex; mutable std::mutex modelMutex; mutable std::mutex workgroupMutex; std::vector modelNodes; std::vector modelConnections; uint64_t nextNodeID = 0; + uint64_t modelRevision = 0; std::atomic dirty { true }; std::atomic desiredWorkerThreads { 0 }; std::atomic latestLatencySamples { 0 }; @@ -1214,4 +1493,14 @@ int AudioGraphProcessor::getLatencySamples() return pimpl->latestLatencySamples.load(); } +Result AudioGraphProcessor::loadStateFromMemory (const MemoryBlock& memoryBlock) +{ + return pimpl->loadStateFromMemory (memoryBlock); +} + +Result AudioGraphProcessor::saveStateIntoMemory (MemoryBlock& memoryBlock) +{ + return pimpl->saveStateIntoMemory (memoryBlock); +} + } // namespace yup diff --git a/modules/yup_audio_gui/graph/yup_AudioGraphComponent.cpp b/modules/yup_audio_gui/graph/yup_AudioGraphComponent.cpp index 6d86ea2b6..39504f126 100644 --- a/modules/yup_audio_gui/graph/yup_AudioGraphComponent.cpp +++ b/modules/yup_audio_gui/graph/yup_AudioGraphComponent.cpp @@ -866,11 +866,38 @@ void AudioGraphComponent::removeConnectionsForEndpoint (const AudioGraphEndpoint return; const auto connections = graph->getConnections(); + std::vector removedConnections; + for (const auto& connection : connections) { if (endpointsMatch (connection.source, endpoint) || endpointsMatch (connection.destination, endpoint)) - removeConnectionAndCommit (connection); + { + if (graph->removeConnection (connection)) + removedConnections.push_back (connection); + } + } + + if (removedConnections.empty()) + return; + + if (graph->commitChanges().failed()) + { + for (const auto& connection : removedConnections) + graph->addConnection (connection); + + graph->commitChanges(); + return; } + + for (const auto& connection : removedConnections) + { + listeners.call ([&] (Listener& listener) + { + listener.connectionRemoved (connection); + }); + } + + repaint(); } bool AudioGraphComponent::isCompatiblePair (const AudioGraphEndpoint& first, const AudioGraphEndpoint& second) const noexcept diff --git a/tests/yup_audio_graph/yup_AudioGraphProcessor.cpp b/tests/yup_audio_graph/yup_AudioGraphProcessor.cpp index 497b675f7..5124ab596 100644 --- a/tests/yup_audio_graph/yup_AudioGraphProcessor.cpp +++ b/tests/yup_audio_graph/yup_AudioGraphProcessor.cpp @@ -300,6 +300,86 @@ class DelayingProcessor : public TestProcessor AudioBuffer history; }; +class BlockingLatencyProcessor : public TestProcessor +{ +public: + BlockingLatencyProcessor (std::atomic& enteredFlag, std::atomic& continueFlag) + : TestProcessor() + , entered (enteredFlag) + , shouldContinue (continueFlag) + { + } + + int getLatencySamples() override + { + entered.store (true); + + while (! shouldContinue.load()) + std::this_thread::yield(); + + return 0; + } + +private: + std::atomic& entered; + std::atomic& shouldContinue; +}; + +class StatefulGainProcessor : public AudioProcessor +{ +public: + explicit StatefulGainProcessor (float initialGain) + : AudioProcessor ("Stateful Gain", stereoLayout()) + , gain (initialGain) + { + } + + void prepareToPlay (float, int) override {} + + void releaseResources() override {} + + void processBlock (AudioBuffer& audioBuffer, MidiBuffer&) override + { + for (int channel = 0; channel < audioBuffer.getNumChannels(); ++channel) + audioBuffer.applyGain (channel, 0, audioBuffer.getNumSamples(), gain); + } + + int getCurrentPreset() const noexcept override { return 0; } + + void setCurrentPreset (int) noexcept override {} + + int getNumPresets() const override { return 0; } + + String getPresetName (int) const override { return {}; } + + void setPresetName (int, StringRef) override {} + + Result loadStateFromMemory (const MemoryBlock& memoryBlock) override + { + if (memoryBlock.getSize() != sizeof (float)) + return Result::fail ("Invalid gain state"); + + MemoryInputStream stream (memoryBlock, false); + gain = stream.readFloat(); + return Result::ok(); + } + + Result saveStateIntoMemory (MemoryBlock& memoryBlock) override + { + MemoryOutputStream stream (memoryBlock, false); + stream.writeFloat (gain); + stream.flush(); + return Result::ok(); + } + + bool hasEditor() const override { return false; } + + void setGain (float newGain) noexcept { gain = newGain; } + +private: + float gain = 1.0f; +}; + void fillImpulse (AudioBuffer& buffer) { buffer.clear(); @@ -588,6 +668,56 @@ TEST (AudioGraphProcessorTests, ProcessesSerialAudioChain) EXPECT_FLOAT_EQ (1.0f, audio.getReadPointer (1)[0]); } +TEST (AudioGraphProcessorTests, ProcessesBlocksLargerThanPreparedMaximumInChunks) +{ + AudioGraphProcessor graph; + graph.prepareToPlay (48000.0f, 16); + + const auto node = graph.addNode (std::make_unique (0.5f)); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (node, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (node, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + AudioBuffer audio (2, 40); + MidiBuffer midi; + + for (int channel = 0; channel < audio.getNumChannels(); ++channel) + for (int sample = 0; sample < audio.getNumSamples(); ++sample) + audio.getWritePointer (channel)[sample] = 1.0f; + + graph.processBlock (audio, midi); + + for (int channel = 0; channel < audio.getNumChannels(); ++channel) + for (int sample = 0; sample < audio.getNumSamples(); ++sample) + EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (channel)[sample]) << "channel " << channel << " sample " << sample; +} + +TEST (AudioGraphProcessorTests, PreservesMidiEventsInBlocksLargerThanPreparedMaximum) +{ + AudioGraphProcessor graph (midiLayout()); + graph.prepareToPlay (48000.0f, 16); + + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), + AudioGraphEndpoint::graphOutput (0) }) + .wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + AudioBuffer audio (0, 40); + MidiBuffer midi; + const uint8 noteOn[] = { 0x90, 60, 100 }; + + midi.addEvent (noteOn, 3, 7); + midi.addEvent (noteOn, 3, 17); + midi.addEvent (noteOn, 3, 35); + + graph.processBlock (audio, midi); + + EXPECT_EQ (1, countMidiEventsAt (midi, 7)); + EXPECT_EQ (1, countMidiEventsAt (midi, 17)); + EXPECT_EQ (1, countMidiEventsAt (midi, 35)); + EXPECT_EQ (3, countMidiEvents (midi)); +} + TEST (AudioGraphProcessorTests, MixesFanIn) { AudioGraphProcessor graph; @@ -746,6 +876,110 @@ TEST (AudioGraphProcessorTests, MissingCommitKeepsPreviousPlan) EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (0)[0]); } +TEST (AudioGraphProcessorTests, CommitKeepsDirtyWhenModelChangesDuringCompilation) +{ + AudioGraphProcessor graph; + + std::atomic latencyEntered { false }; + std::atomic allowLatencyQueryToContinue { false }; + + const auto blockingNode = graph.addNode (std::make_unique (latencyEntered, allowLatencyQueryToContinue)); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (blockingNode, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (blockingNode, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + + std::atomic commitSucceeded { false }; + std::thread commitThread ([&] + { + commitSucceeded.store (graph.commitChanges().wasOk()); + }); + + for (int spinCount = 0; spinCount < 10000 && ! latencyEntered.load(); ++spinCount) + std::this_thread::yield(); + + const bool didEnterLatencyQuery = latencyEntered.load(); + EXPECT_TRUE (didEnterLatencyQuery); + + if (! didEnterLatencyQuery) + { + allowLatencyQueryToContinue.store (true); + commitThread.join(); + return; + } + + const auto addedDuringCommit = graph.addNode (std::make_unique()); + EXPECT_TRUE (addedDuringCommit.isValid()); + + allowLatencyQueryToContinue.store (true); + commitThread.join(); + + EXPECT_TRUE (commitSucceeded.load()); + EXPECT_TRUE (graph.hasUncommittedChanges()); + + EXPECT_TRUE (graph.commitChanges().wasOk()); + EXPECT_FALSE (graph.hasUncommittedChanges()); +} + +TEST (AudioGraphProcessorTests, SaveAndLoadRestoresConnectionsAndNodeState) +{ + AudioGraphProcessor graph; + auto processor = std::make_unique (0.25f); + auto* processorPtr = processor.get(); + const auto node = graph.addNode (std::move (processor)); + + const AudioGraphConnection inputConnection { AudioGraphEndpoint::graphInput (0), + AudioGraphEndpoint::nodeInput (node, 0) }; + const AudioGraphConnection outputConnection { AudioGraphEndpoint::nodeOutput (node, 0), + AudioGraphEndpoint::graphOutput (0) }; + const AudioGraphConnection bypassConnection { AudioGraphEndpoint::graphInput (0), + AudioGraphEndpoint::graphOutput (0) }; + + ASSERT_TRUE (graph.addConnection (inputConnection).wasOk()); + ASSERT_TRUE (graph.addConnection (outputConnection).wasOk()); + ASSERT_TRUE (graph.commitChanges().wasOk()); + + MemoryBlock savedState; + EXPECT_TRUE (graph.saveStateIntoMemory (savedState).wasOk()); + + processorPtr->setGain (0.75f); + EXPECT_TRUE (graph.removeConnection (inputConnection)); + EXPECT_TRUE (graph.removeConnection (outputConnection)); + EXPECT_TRUE (graph.addConnection (bypassConnection).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + EXPECT_TRUE (graph.loadStateFromMemory (savedState).wasOk()); + EXPECT_FALSE (graph.hasUncommittedChanges()); + + const auto connections = graph.getConnections(); + ASSERT_EQ (2u, connections.size()); + EXPECT_EQ (inputConnection, connections[0]); + EXPECT_EQ (outputConnection, connections[1]); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + fillImpulse (audio); + + graph.processBlock (audio, midi); + + EXPECT_FLOAT_EQ (0.25f, audio.getReadPointer (0)[0]); + EXPECT_FLOAT_EQ (0.25f, audio.getReadPointer (1)[0]); +} + +TEST (AudioGraphProcessorTests, LoadStateFailsWhenSavedNodesAreMissing) +{ + AudioGraphProcessor source; + const auto node = source.addNode (std::make_unique (0.25f)); + + ASSERT_TRUE (source.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (node, 0) }).wasOk()); + ASSERT_TRUE (source.addConnection ({ AudioGraphEndpoint::nodeOutput (node, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + ASSERT_TRUE (source.commitChanges().wasOk()); + + MemoryBlock savedState; + ASSERT_TRUE (source.saveStateIntoMemory (savedState).wasOk()); + + AudioGraphProcessor destination; + EXPECT_TRUE (destination.loadStateFromMemory (savedState).failed()); +} + TEST (AudioGraphProcessorTests, RemoveConnectionStopsRoutingAfterCommit) { AudioGraphProcessor graph; @@ -1440,6 +1674,33 @@ TEST (AudioGraphProcessorTests, WorkerThreadCountCanBeChangedAfterProcessing) graph.setNumWorkerThreads (0); } +TEST (AudioGraphProcessorTests, WorkerThreadsContinueProcessingAfterIdleReset) +{ + AudioGraphProcessor graph; + graph.setNumWorkerThreads (2); + + const auto node = graph.addNode (std::make_unique (0.5f)); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (node, 0) }).wasOk()); + EXPECT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (node, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + EXPECT_TRUE (graph.commitChanges().wasOk()); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + + fillImpulse (audio); + graph.processBlock (audio, midi); + EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (0)[0]); + + for (int i = 0; i < 100; ++i) + std::this_thread::yield(); + + fillImpulse (audio); + graph.processBlock (audio, midi); + EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (0)[0]); + + graph.setNumWorkerThreads (0); +} + TEST (AudioGraphProcessorTests, ZeroWorkerThreadsProcessesCorrectly) { AudioGraphProcessor graph; diff --git a/tests/yup_audio_gui/yup_AudioGraphComponent.cpp b/tests/yup_audio_gui/yup_AudioGraphComponent.cpp index a5004b9bd..ea9147c95 100644 --- a/tests/yup_audio_gui/yup_AudioGraphComponent.cpp +++ b/tests/yup_audio_gui/yup_AudioGraphComponent.cpp @@ -751,6 +751,38 @@ TEST_F (AudioGraphComponentTests, RightClickEndpointRemovesAllConnectionsForEndp component->removeListener (&listener); } +TEST_F (AudioGraphComponentTests, RightClickEndpointRemovesMultipleConnectionsForEndpoint) +{ + const auto firstNodeID = addProcessorNode(); + const auto secondNodeID = addProcessorNode(); + addNodeView (firstNodeID, { 100.0f, 50.0f }); + addNodeView (secondNodeID, { 100.0f, 190.0f }); + + auto inputView = std::make_unique (AudioGraphNodeID::invalid(), "Graph Input", 0, 1); + component->setGraphInputView (std::move (inputView), { -120.0f, 50.0f }); + + const AudioGraphConnection firstConnection { AudioGraphEndpoint::graphInput (0), + AudioGraphEndpoint::nodeInput (firstNodeID, 0) }; + const AudioGraphConnection secondConnection { AudioGraphEndpoint::graphInput (0), + AudioGraphEndpoint::nodeInput (secondNodeID, 0) }; + + ASSERT_TRUE (graph->addConnection (firstConnection).wasOk()); + ASSERT_TRUE (graph->addConnection (secondConnection).wasOk()); + ASSERT_TRUE (graph->commitChanges().wasOk()); + + RecordingGraphListener listener; + component->addListener (&listener); + + component->mouseDown (mouseEventForComponent (MouseEvent::rightButton, component->getEndpointScreenPosition (firstConnection.source))); + + EXPECT_TRUE (graph->getConnections().empty()); + ASSERT_EQ (2u, listener.removedConnections.size()); + EXPECT_EQ (firstConnection, listener.removedConnections[0]); + EXPECT_EQ (secondConnection, listener.removedConnections[1]); + + component->removeListener (&listener); +} + TEST_F (AudioGraphComponentTests, RightClickConnectionRemovesSingleConnection) { const auto nodeID = addProcessorNode(); From b7d14d4a475db8d3c4f2f612ec305c0a9d4100d5 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 18 May 2026 08:41:18 +0200 Subject: [PATCH 19/35] Split graph in smaller chunks --- .../graph/yup_AudioGraphAllocationStats.h | 50 ++ .../graph/yup_AudioGraphConnection.h | 55 ++ .../graph/yup_AudioGraphEndpoint.h | 107 +++ .../graph/yup_AudioGraphNodeID.h | 63 ++ .../graph/yup_AudioGraphNodeProperties.h | 52 ++ .../graph/yup_AudioGraphProcessor.cpp | 518 ++++++++++---- .../graph/yup_AudioGraphProcessor.h | 218 ++---- modules/yup_audio_graph/yup_audio_graph.h | 7 + modules/yup_core/memory/yup_MemoryBlock.cpp | 88 +-- .../yup_AudioGraphProcessor.cpp | 631 +++++++++++++++++- 10 files changed, 1408 insertions(+), 381 deletions(-) create mode 100644 modules/yup_audio_graph/graph/yup_AudioGraphAllocationStats.h create mode 100644 modules/yup_audio_graph/graph/yup_AudioGraphConnection.h create mode 100644 modules/yup_audio_graph/graph/yup_AudioGraphEndpoint.h create mode 100644 modules/yup_audio_graph/graph/yup_AudioGraphNodeID.h create mode 100644 modules/yup_audio_graph/graph/yup_AudioGraphNodeProperties.h diff --git a/modules/yup_audio_graph/graph/yup_AudioGraphAllocationStats.h b/modules/yup_audio_graph/graph/yup_AudioGraphAllocationStats.h new file mode 100644 index 000000000..168f0dc6c --- /dev/null +++ b/modules/yup_audio_graph/graph/yup_AudioGraphAllocationStats.h @@ -0,0 +1,50 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +/** + Lightweight diagnostics describing the most recently compiled graph plan. +*/ +struct AudioGraphAllocationStats +{ + /** Number of preallocated node scratch audio buffers. */ + int scratchAudioBuffers = 0; + + /** Number of preallocated node MIDI buffers. */ + int midiBuffers = 0; + + /** Number of per-connection delay lines. */ + int delayLines = 0; + + /** Maximum latency compensation inserted on any graph output path. */ + int totalCompensationSamples = 0; + + /** Maximum preallocated audio channel count used by any node or graph endpoint. */ + int maxPreallocatedChannels = 0; + + /** Maximum preallocated block size for the compiled plan. */ + int maxPreallocatedBlockSize = 0; +}; + +} // namespace yup diff --git a/modules/yup_audio_graph/graph/yup_AudioGraphConnection.h b/modules/yup_audio_graph/graph/yup_AudioGraphConnection.h new file mode 100644 index 000000000..89d632aa0 --- /dev/null +++ b/modules/yup_audio_graph/graph/yup_AudioGraphConnection.h @@ -0,0 +1,55 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +/** + A directed connection between two graph endpoints. +*/ +class AudioGraphConnection +{ +public: + AudioGraphConnection() = default; + + /** Creates a connection from sourceEndpoint to destinationEndpoint. */ + AudioGraphConnection (AudioGraphEndpoint sourceEndpoint, AudioGraphEndpoint destinationEndpoint) noexcept + : source (sourceEndpoint) + , destination (destinationEndpoint) + { + } + + bool operator== (const AudioGraphConnection& other) const noexcept + { + return source == other.source && destination == other.destination; + } + + bool operator!= (const AudioGraphConnection& other) const noexcept { return ! (*this == other); } + + /** Source endpoint. */ + AudioGraphEndpoint source; + + /** Destination endpoint. */ + AudioGraphEndpoint destination; +}; + +} // namespace yup diff --git a/modules/yup_audio_graph/graph/yup_AudioGraphEndpoint.h b/modules/yup_audio_graph/graph/yup_AudioGraphEndpoint.h new file mode 100644 index 000000000..c17f40c62 --- /dev/null +++ b/modules/yup_audio_graph/graph/yup_AudioGraphEndpoint.h @@ -0,0 +1,107 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +/** + Describes one routable audio or MIDI endpoint in an AudioGraphProcessor. + + Source endpoints are graphInput() and nodeOutput(). Destination endpoints are + graphOutput() and nodeInput(). The bus index refers to the AudioBusLayout of the + graph or the addressed processor node. +*/ +class AudioGraphEndpoint +{ +public: + /** The endpoint owner and direction. */ + enum class Kind + { + graphInput, + graphOutput, + nodeInput, + nodeOutput + }; + + /** Creates an invalid graph input endpoint. */ + AudioGraphEndpoint() = default; + + /** Creates a source endpoint for one graph input bus. */ + static AudioGraphEndpoint graphInput (int busIndex) noexcept + { + return AudioGraphEndpoint (Kind::graphInput, AudioGraphNodeID::invalid(), busIndex); + } + + /** Creates a destination endpoint for one graph output bus. */ + static AudioGraphEndpoint graphOutput (int busIndex) noexcept + { + return AudioGraphEndpoint (Kind::graphOutput, AudioGraphNodeID::invalid(), busIndex); + } + + /** Creates a destination endpoint for one node input bus. */ + static AudioGraphEndpoint nodeInput (AudioGraphNodeID nodeID, int busIndex) noexcept + { + return AudioGraphEndpoint (Kind::nodeInput, nodeID, busIndex); + } + + /** Creates a source endpoint for one node output bus. */ + static AudioGraphEndpoint nodeOutput (AudioGraphNodeID nodeID, int busIndex) noexcept + { + return AudioGraphEndpoint (Kind::nodeOutput, nodeID, busIndex); + } + + /** Returns the endpoint kind. */ + Kind getKind() const noexcept { return kind; } + + /** Returns the addressed node, or an invalid ID for graph endpoints. */ + AudioGraphNodeID getNodeID() const noexcept { return nodeID; } + + /** Returns the bus index on the graph or processor layout. */ + int getBusIndex() const noexcept { return busIndex; } + + /** Returns true when this endpoint can appear as a connection source. */ + bool isSource() const noexcept { return kind == Kind::graphInput || kind == Kind::nodeOutput; } + + /** Returns true when this endpoint can appear as a connection destination. */ + bool isDestination() const noexcept { return kind == Kind::graphOutput || kind == Kind::nodeInput; } + + bool operator== (const AudioGraphEndpoint& other) const noexcept + { + return kind == other.kind && nodeID == other.nodeID && busIndex == other.busIndex; + } + + bool operator!= (const AudioGraphEndpoint& other) const noexcept { return ! (*this == other); } + +private: + AudioGraphEndpoint (Kind endpointKind, AudioGraphNodeID endpointNodeID, int endpointBusIndex) noexcept + : kind (endpointKind) + , nodeID (endpointNodeID) + , busIndex (endpointBusIndex) + { + } + + Kind kind = Kind::graphInput; + AudioGraphNodeID nodeID; + int busIndex = -1; +}; + +} // namespace yup diff --git a/modules/yup_audio_graph/graph/yup_AudioGraphNodeID.h b/modules/yup_audio_graph/graph/yup_AudioGraphNodeID.h new file mode 100644 index 000000000..9f40da5fb --- /dev/null +++ b/modules/yup_audio_graph/graph/yup_AudioGraphNodeID.h @@ -0,0 +1,63 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +/** + Opaque identifier for a processor node owned by an AudioGraphProcessor. + + Node identifiers remain stable until the node is removed. The invalid identifier + is used by graph input and graph output endpoints, and by failed addNode calls. +*/ +class AudioGraphNodeID +{ +public: + /** Creates an invalid node identifier. */ + constexpr AudioGraphNodeID() noexcept = default; + + /** Returns an invalid node identifier. */ + static constexpr AudioGraphNodeID invalid() noexcept { return {}; } + + /** Returns true when this identifier names a graph node. */ + constexpr bool isValid() const noexcept { return value != 0; } + + /** Returns the raw integer identifier. */ + constexpr uint64_t getRawID() const noexcept { return value; } + + /** Creates an identifier from a raw value. Prefer IDs returned by addNode(). */ + explicit constexpr AudioGraphNodeID (uint64_t rawValue) noexcept + : value (rawValue) + { + } + + constexpr bool operator== (AudioGraphNodeID other) const noexcept { return value == other.value; } + + constexpr bool operator!= (AudioGraphNodeID other) const noexcept { return value != other.value; } + + constexpr bool operator< (AudioGraphNodeID other) const noexcept { return value < other.value; } + +private: + uint64_t value = 0; +}; + +} // namespace yup diff --git a/modules/yup_audio_graph/graph/yup_AudioGraphNodeProperties.h b/modules/yup_audio_graph/graph/yup_AudioGraphNodeProperties.h new file mode 100644 index 000000000..d0f98fdcd --- /dev/null +++ b/modules/yup_audio_graph/graph/yup_AudioGraphNodeProperties.h @@ -0,0 +1,52 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +/** + Persistent metadata used to save and recreate an AudioGraphProcessor node. + + Set identifier to a stable application-defined factory key. If node creation + needs additional metadata, store it in creationData. Callers that also link a + plugin-host module can encode plugin descriptions there and recreate hosted + plugin instances from the node factory. +*/ +struct AudioGraphNodeProperties +{ + /** Stable factory key used when loading graph state. */ + String identifier; + + /** Human-readable node name. */ + String name; + + /** Canvas X position in caller-defined units. */ + float positionX = 0.0f; + + /** Canvas Y position in caller-defined units. */ + float positionY = 0.0f; + + /** Opaque caller-defined metadata used to recreate the node. */ + MemoryBlock creationData; +}; + +} // namespace yup diff --git a/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.cpp b/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.cpp index 26ace7f76..32b990cc5 100644 --- a/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.cpp +++ b/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.cpp @@ -74,8 +74,8 @@ bool isValidBusIndex (Span buses, int busIndex) return busIndex >= 0 && busIndex < static_cast (buses.size()); } -constexpr int audioGraphStateMagic = 0x59414731; // YAG1 -constexpr int audioGraphStateVersion = 2; +constexpr int audioGraphStateVersion = 1; +constexpr const char* audioGraphStateTag = "YUPAudioGraphState"; } // namespace //============================================================================== @@ -244,24 +244,6 @@ class AudioGraphProcessor::Pimpl return id; } - AudioGraphNodeID addPluginNode (std::unique_ptr plugin, - float positionX, - float positionY) - { - if (plugin == nullptr) - return AudioGraphNodeID::invalid(); - - AudioGraphNodeProperties properties; - properties.kind = AudioGraphNodeProperties::Kind::plugin; - properties.identifier = plugin->getDescription().identifier; - properties.name = plugin->getDescription().name; - properties.positionX = positionX; - properties.positionY = positionY; - properties.pluginDescription = plugin->getDescription(); - - return addNode (std::move (plugin), std::move (properties)); - } - bool removeNode (AudioGraphNodeID nodeID) { if (! nodeID.isValid()) @@ -352,6 +334,7 @@ class AudioGraphProcessor::Pimpl { const std::lock_guard lock (modelMutex); + nodesSnapshot = modelNodes; connectionsSnapshot = modelConnections; snapshotRevision = modelRevision; @@ -401,7 +384,7 @@ class AudioGraphProcessor::Pimpl latestLatencySamples.store (compiled->graphLatencySamples); dirty.store (! snapshotIsCurrent); - delete pendingPlan.exchange (compiled.release()); + delete pendingPlan.exchange (compiled.release()); // TODO - make it so we don't use delete deleteRetiredPlans(); return Result::ok(); @@ -410,7 +393,7 @@ class AudioGraphProcessor::Pimpl void prepareToPlay (float newSampleRate, int newMaxBlockSize) { sampleRate = newSampleRate; - maxBlockSize = std::max (1, newMaxBlockSize); + maxBlockSize = jmax (1, newMaxBlockSize); const auto result = commitChanges(); jassert (result.wasOk()); @@ -456,7 +439,7 @@ class AudioGraphProcessor::Pimpl for (int startSample = 0; startSample < totalSamples; startSample += graph->maxBlockSize) { - const int numSamples = std::min (graph->maxBlockSize, totalSamples - startSample); + const int numSamples = jmin (graph->maxBlockSize, totalSamples - startSample); captureGraphInput (*graph, audioBuffer, midiBuffer, startSample, numSamples); audioBuffer.clear (startSample, numSamples); @@ -503,7 +486,7 @@ class AudioGraphProcessor::Pimpl void setNumWorkerThreads (int numThreads) { - const int newNumThreads = std::max (0, numThreads); + const int newNumThreads = jmax (0, numThreads); desiredWorkerThreads.store (newNumThreads); resizeWorkers (newNumThreads); } @@ -553,7 +536,64 @@ class AudioGraphProcessor::Pimpl return iterator != modelNodes.end() ? iterator->processor.get() : nullptr; } - Result saveStateIntoMemory (MemoryBlock& memoryBlock) + bool setNodePosition (AudioGraphNodeID nodeID, float positionX, float positionY) + { + const std::lock_guard lock (modelMutex); + + const auto iterator = std::find_if (modelNodes.begin(), modelNodes.end(), [nodeID] (const ModelNode& node) + { + return node.id == nodeID; + }); + + if (iterator == modelNodes.end()) + return false; + + iterator->properties.positionX = positionX; + iterator->properties.positionY = positionY; + return true; + } + + bool setNodeProperties (AudioGraphNodeID nodeID, AudioGraphNodeProperties properties) + { + const std::lock_guard lock (modelMutex); + + const auto iterator = std::find_if (modelNodes.begin(), modelNodes.end(), [nodeID] (const ModelNode& node) + { + return node.id == nodeID; + }); + + if (iterator == modelNodes.end()) + return false; + + if (properties.name.isEmpty() && iterator->processor != nullptr) + properties.name = iterator->processor->getName(); + + iterator->properties = std::move (properties); + return true; + } + + std::optional getNodeProperties (AudioGraphNodeID nodeID) const + { + const std::lock_guard lock (modelMutex); + + const auto iterator = std::find_if (modelNodes.begin(), modelNodes.end(), [nodeID] (const ModelNode& node) + { + return node.id == nodeID; + }); + + if (iterator == modelNodes.end()) + return std::nullopt; + + return iterator->properties; + } + + void setNodeFactory (AudioGraphProcessor::NodeFactory factory) + { + const std::lock_guard lock (factoryMutex); + nodeFactory = std::move (factory); + } + + ResultValue> createXml() const { std::vector nodesSnapshot; std::vector connectionsSnapshot; @@ -572,157 +612,308 @@ class AudioGraphProcessor::Pimpl for (const auto& node : nodesSnapshot) { if (node.processor == nullptr) - return Result::fail ("Audio graph contains an empty node"); + return ResultValue>::fail ("Audio graph contains an empty node"); MemoryBlock nodeState; const auto result = node.processor->saveStateIntoMemory (nodeState); if (! result) - return Result::fail ("Audio graph node state save failed: " + result.getErrorMessage()); + return ResultValue>::fail ("Audio graph node state save failed: " + result.getErrorMessage()); - savedNodes.push_back ({ node.id, std::move (nodeState) }); + savedNodes.push_back ({ node.id, node.properties, std::move (nodeState) }); } - MemoryOutputStream stream (memoryBlock, false); - stream.writeInt (audioGraphStateMagic); - stream.writeCompressedInt (audioGraphStateVersion); - stream.writeInt64 (static_cast (snapshotNextNodeID)); - stream.writeCompressedInt (static_cast (savedNodes.size())); + auto root = std::make_unique (audioGraphStateTag); + root->setAttribute ("version", audioGraphStateVersion); + root->setAttribute ("nextNodeID", String (static_cast (snapshotNextNodeID))); + + auto* nodesElement = new XmlElement ("nodes"); + root->addChildElement (nodesElement); for (const auto& node : savedNodes) { - stream.writeInt64 (static_cast (node.id.getRawID())); - stream.writeCompressedInt (static_cast (node.state.getSize())); + auto* nodeElement = new XmlElement ("node"); + nodesElement->addChildElement (nodeElement); - if (! node.state.isEmpty()) - stream.write (node.state.getData(), node.state.getSize()); + nodeElement->setAttribute ("id", String (static_cast (node.id.getRawID()))); + writeNodeProperties (*nodeElement, node.properties); + writeBase64Element (*nodeElement, "state", node.state); } - stream.writeCompressedInt (static_cast (connectionsSnapshot.size())); + auto* connectionsElement = new XmlElement ("connections"); + root->addChildElement (connectionsElement); for (const auto& connection : connectionsSnapshot) { - writeEndpoint (stream, connection.source); - writeEndpoint (stream, connection.destination); + auto* connectionElement = new XmlElement ("connection"); + connectionsElement->addChildElement (connectionElement); + + auto* sourceElement = new XmlElement ("source"); + connectionElement->addChildElement (sourceElement); + writeEndpoint (*sourceElement, connection.source); + + auto* destinationElement = new XmlElement ("destination"); + connectionElement->addChildElement (destinationElement); + writeEndpoint (*destinationElement, connection.destination); } - stream.flush(); - return Result::ok(); + return ResultValue>::ok (std::move (root)); } - Result loadStateFromMemory (const MemoryBlock& memoryBlock) + Result restoreFromXml (const XmlElement& xml) { - MemoryInputStream stream (memoryBlock, false); - - if (stream.readInt() != audioGraphStateMagic) + if (! xml.hasTagName (audioGraphStateTag)) return Result::fail ("Audio graph state has an invalid header"); - if (stream.readCompressedInt() != audioGraphStateVersion) + if (xml.getIntAttribute ("version", 0) != audioGraphStateVersion) return Result::fail ("Audio graph state has an unsupported version"); - const auto savedNextNodeID = static_cast (stream.readInt64()); - const int numNodes = stream.readCompressedInt(); + const auto savedNextNodeID = static_cast (xml.getStringAttribute ("nextNodeID").getLargeIntValue()); - if (numNodes < 0) - return Result::fail ("Audio graph state has an invalid node count"); + auto* nodesElement = xml.getChildByName ("nodes"); + if (nodesElement == nullptr) + return Result::fail ("Audio graph state is missing nodes"); std::vector savedNodes; - savedNodes.reserve (static_cast (numNodes)); + savedNodes.reserve (static_cast (nodesElement->getNumChildElements())); - for (int i = 0; i < numNodes; ++i) + for (auto* nodeElement : nodesElement->getChildWithTagNameIterator ("node")) { SavedNodeState savedNode; - savedNode.id = AudioGraphNodeID (static_cast (stream.readInt64())); - - const int stateSize = stream.readCompressedInt(); - if (stateSize < 0) - return Result::fail ("Audio graph state has an invalid node state size"); + savedNode.id = AudioGraphNodeID (static_cast (nodeElement->getStringAttribute ("id").getLargeIntValue())); - savedNode.state.setSize (static_cast (stateSize)); + if (const auto result = readNodeProperties (*nodeElement, savedNode.properties); result.failed()) + return result; - if (stateSize > 0 && stream.read (savedNode.state.getData(), stateSize) != stateSize) - return Result::fail ("Audio graph state ended while reading node state"); + if (const auto result = readBase64Element (*nodeElement, "state", savedNode.state); result.failed()) + return result; savedNodes.push_back (std::move (savedNode)); } - const int numConnections = stream.readCompressedInt(); - if (numConnections < 0) - return Result::fail ("Audio graph state has an invalid connection count"); + auto* connectionsElement = xml.getChildByName ("connections"); + if (connectionsElement == nullptr) + return Result::fail ("Audio graph state is missing connections"); std::vector savedConnections; - savedConnections.reserve (static_cast (numConnections)); + savedConnections.reserve (static_cast (connectionsElement->getNumChildElements())); - for (int i = 0; i < numConnections; ++i) + for (auto* connectionElement : connectionsElement->getChildWithTagNameIterator ("connection")) { AudioGraphEndpoint source; AudioGraphEndpoint destination; - if (const auto result = readEndpoint (stream, source); result.failed()) + auto* sourceElement = connectionElement->getChildByName ("source"); + if (sourceElement == nullptr) + return Result::fail ("Audio graph state connection is missing source endpoint"); + + auto* destinationElement = connectionElement->getChildByName ("destination"); + if (destinationElement == nullptr) + return Result::fail ("Audio graph state connection is missing destination endpoint"); + + if (const auto result = readEndpoint (*sourceElement, source); result.failed()) return result; - if (const auto result = readEndpoint (stream, destination); result.failed()) + if (const auto result = readEndpoint (*destinationElement, destination); result.failed()) return result; savedConnections.push_back ({ source, destination }); } - std::vector> processors; - processors.reserve (savedNodes.size()); + return restoreModel (savedNextNodeID, std::move (savedNodes), std::move (savedConnections)); + } - { - const std::lock_guard lock (modelMutex); + Result saveStateIntoMemory (MemoryBlock& memoryBlock) + { + auto xml = createXml(); - if (savedNodes.size() != modelNodes.size()) - return Result::fail ("Audio graph state does not match the current node set"); + if (! xml) + return Result::fail (xml.getErrorMessage()); - for (const auto& savedNode : savedNodes) - { - const auto iterator = std::find_if (modelNodes.begin(), modelNodes.end(), [&savedNode] (const ModelNode& node) - { - return node.id == savedNode.id; - }); + auto root = std::move (xml).getValue(); + MemoryOutputStream stream (memoryBlock, false); + root->writeTo (stream); + stream.flush(); + return Result::ok(); + } + + Result loadStateFromMemory (const MemoryBlock& memoryBlock) + { + MemoryInputStream stream (memoryBlock, false); + const auto xmlText = stream.readEntireStreamAsString(); + auto root = parseXML (xmlText); - if (iterator == modelNodes.end() || iterator->processor == nullptr) - return Result::fail ("Audio graph state references a missing node"); + if (root == nullptr || ! root->hasTagName (audioGraphStateTag)) + return Result::fail ("Audio graph state has an invalid header"); - processors.push_back (iterator->processor); - } - } + return restoreFromXml (*root); + } + + Result restoreModel (uint64_t savedNextNodeID, + std::vector savedNodes, + std::vector savedConnections) + { + std::vector loadedNodes; + loadedNodes.reserve (savedNodes.size()); - for (size_t i = 0; i < savedNodes.size(); ++i) + for (auto& savedNode : savedNodes) { - const auto result = processors[i]->loadStateFromMemory (savedNodes[i].state); + auto processorResult = createProcessorForSavedNode (savedNode.properties); + + if (! processorResult) + return Result::fail (processorResult.getErrorMessage()); + + auto processor = std::move (processorResult).getValue(); + + if (processor == nullptr) + return Result::fail ("Audio graph node factory returned an empty processor"); + + const auto result = processor->loadStateFromMemory (savedNode.state); if (! result) return Result::fail ("Audio graph node state load failed: " + result.getErrorMessage()); + + loadedNodes.push_back ({ savedNode.id, + std::shared_ptr (std::move (processor)), + std::move (savedNode.properties) }); } + std::vector previousNodes; + std::vector previousConnections; + uint64_t previousNextNodeID = 0; + bool previousDirty = false; + uint64_t highestSavedNodeID = 0; + uint64_t loadRevision = 0; + + for (const auto& savedNode : savedNodes) + highestSavedNodeID = jmax (highestSavedNodeID, savedNode.id.getRawID()); + { const std::lock_guard lock (modelMutex); + previousNodes = modelNodes; + previousConnections = modelConnections; + previousNextNodeID = nextNodeID; + previousDirty = dirty.load(); + modelNodes = std::move (loadedNodes); modelConnections = std::move (savedConnections); - nextNodeID = std::max (nextNodeID, savedNextNodeID); + nextNodeID = jmax (savedNextNodeID, highestSavedNodeID); ++modelRevision; + loadRevision = modelRevision; dirty.store (true); } - return commitChanges(); + const auto result = commitChanges(); + + if (! result) + { + const std::lock_guard lock (modelMutex); + + if (modelRevision == loadRevision) + { + modelNodes = std::move (previousNodes); + modelConnections = std::move (previousConnections); + nextNodeID = previousNextNodeID; + ++modelRevision; + dirty.store (previousDirty); + } + else + { + dirty.store (true); + } + } + + return result; + } + + ResultValue> createProcessorForSavedNode (const AudioGraphNodeProperties& properties) + { + AudioGraphProcessor::NodeFactory factoryCopy; + + { + const std::lock_guard lock (factoryMutex); + factoryCopy = nodeFactory; + } + + if (! factoryCopy) + return ResultValue>::fail ("Audio graph node factory is not configured"); + + return factoryCopy (properties); + } + + static AudioGraphNodeProperties makeDefaultNodeProperties (const AudioProcessor& processor) + { + AudioGraphNodeProperties properties; + properties.identifier = processor.getName(); + properties.name = processor.getName(); + return properties; + } + + static void writeNodeProperties (XmlElement& element, const AudioGraphNodeProperties& properties) + { + element.setAttribute ("identifier", properties.identifier); + element.setAttribute ("name", properties.name); + element.setAttribute ("positionX", static_cast (properties.positionX)); + element.setAttribute ("positionY", static_cast (properties.positionY)); + writeBase64Element (element, "creationData", properties.creationData); } - static bool writeEndpoint (OutputStream& stream, const AudioGraphEndpoint& endpoint) + static Result readNodeProperties (const XmlElement& element, AudioGraphNodeProperties& properties) { - return stream.writeCompressedInt (static_cast (endpoint.getKind())) - && stream.writeInt64 (static_cast (endpoint.getNodeID().getRawID())) - && stream.writeCompressedInt (endpoint.getBusIndex()); + properties.identifier = element.getStringAttribute ("identifier"); + properties.name = element.getStringAttribute ("name"); + properties.positionX = static_cast (element.getDoubleAttribute ("positionX")); + properties.positionY = static_cast (element.getDoubleAttribute ("positionY")); + + return readBase64Element (element, "creationData", properties.creationData); + } + + static void writeBase64Element (XmlElement& parent, const char* tagName, const MemoryBlock& block) + { + auto* element = new XmlElement (tagName); + element->setAttribute ("encoding", "base64"); + element->addTextElement (Base64::toBase64 (block.getData(), block.getSize())); + parent.addChildElement (element); + } + + static Result readBase64Element (const XmlElement& parent, const char* tagName, MemoryBlock& block) + { + auto* element = parent.getChildByName (tagName); + + if (element == nullptr) + return Result::fail (String ("Audio graph state is missing ") + tagName); + + if (element->getStringAttribute ("encoding") != "base64") + return Result::fail (String ("Audio graph state has unsupported ") + tagName + " encoding"); + + MemoryOutputStream output (block, false); + const auto base64Text = element->getAllSubText().removeCharacters (" \t\r\n"); + + if (! Base64::convertFromBase64 (output, base64Text)) + return Result::fail (String ("Audio graph state has invalid ") + tagName + " data"); + + output.flush(); + return Result::ok(); + } + + static void writeEndpoint (XmlElement& element, const AudioGraphEndpoint& endpoint) + { + element.setAttribute ("kind", endpointKindToString (endpoint.getKind())); + element.setAttribute ("nodeID", String (static_cast (endpoint.getNodeID().getRawID()))); + element.setAttribute ("busIndex", endpoint.getBusIndex()); } - static Result readEndpoint (InputStream& stream, AudioGraphEndpoint& endpoint) + static Result readEndpoint (const XmlElement& element, AudioGraphEndpoint& endpoint) { - const int kind = stream.readCompressedInt(); - const auto nodeID = AudioGraphNodeID (static_cast (stream.readInt64())); - const int busIndex = stream.readCompressedInt(); + auto kind = AudioGraphEndpoint::Kind::graphInput; + const auto result = endpointKindFromString (element.getStringAttribute ("kind"), kind); + + if (result.failed()) + return result; - switch (static_cast (kind)) + const auto nodeID = AudioGraphNodeID (static_cast (element.getStringAttribute ("nodeID").getLargeIntValue())); + const int busIndex = element.getIntAttribute ("busIndex"); + + switch (kind) { case AudioGraphEndpoint::Kind::graphInput: if (nodeID.isValid()) @@ -750,6 +941,55 @@ class AudioGraphProcessor::Pimpl return Result::fail ("Audio graph state has an invalid endpoint kind"); } + static String endpointKindToString (AudioGraphEndpoint::Kind kind) + { + switch (kind) + { + case AudioGraphEndpoint::Kind::graphInput: + return "graphInput"; + + case AudioGraphEndpoint::Kind::graphOutput: + return "graphOutput"; + + case AudioGraphEndpoint::Kind::nodeInput: + return "nodeInput"; + + case AudioGraphEndpoint::Kind::nodeOutput: + return "nodeOutput"; + } + + return {}; + } + + static Result endpointKindFromString (const String& text, AudioGraphEndpoint::Kind& kind) + { + if (text == "graphInput") + { + kind = AudioGraphEndpoint::Kind::graphInput; + return Result::ok(); + } + + if (text == "graphOutput") + { + kind = AudioGraphEndpoint::Kind::graphOutput; + return Result::ok(); + } + + if (text == "nodeInput") + { + kind = AudioGraphEndpoint::Kind::nodeInput; + return Result::ok(); + } + + if (text == "nodeOutput") + { + kind = AudioGraphEndpoint::Kind::nodeOutput; + return Result::ok(); + } + + return Result::fail ("Audio graph state has an invalid endpoint kind"); + } + Result compileGraph (CompiledGraph& graph, const std::vector& nodesSnapshot, const std::vector& connectionsSnapshot) @@ -779,7 +1019,7 @@ class AudioGraphProcessor::Pimpl runtime.processor = modelNode.processor; runtime.inputChannels = getTotalAudioChannels (modelNode.processor->getBusLayout().getInputBuses()); runtime.outputChannels = getTotalAudioChannels (modelNode.processor->getBusLayout().getOutputBuses()); - runtime.workChannels = std::max (runtime.inputChannels, runtime.outputChannels); + runtime.workChannels = jmax (runtime.inputChannels, runtime.outputChannels); runtime.audioBuffer.setSize (runtime.workChannels, maxBlockSize, false, true, false); runtime.audioBuffer.clear(); graph.nodes.push_back (std::move (runtime)); @@ -844,7 +1084,7 @@ class AudioGraphProcessor::Pimpl if (conn.type == GraphSignalType::midi) ++numMidiConnections; - const size_t midiReserveBytes = static_cast (std::max (1, numMidiConnections + 1)) * 4096; + const size_t midiReserveBytes = static_cast (jmax (1, numMidiConnections + 1)) * 4096; graph.graphInputMidi.ensureSize (midiReserveBytes); graph.graphOutputMidi.ensureSize (midiReserveBytes); @@ -965,7 +1205,7 @@ class AudioGraphProcessor::Pimpl { const auto& connection = graph.connections[static_cast (connectionIndex)]; if (connection.sourceNodeIndex >= 0) - level = std::max (level, nodeLevels[static_cast (connection.sourceNodeIndex)] + 1); + level = jmax (level, nodeLevels[static_cast (connection.sourceNodeIndex)] + 1); } nodeLevels[static_cast (nodeIndex)] = level; @@ -992,15 +1232,15 @@ class AudioGraphProcessor::Pimpl auto& node = graph.nodes[static_cast (nodeIndex)]; for (const auto connectionIndex : node.incomingConnections) - node.inputLatencySamples = std::max (node.inputLatencySamples, - getSourceLatency (graph, graph.connections[static_cast (connectionIndex)])); + node.inputLatencySamples = jmax (node.inputLatencySamples, + getSourceLatency (graph, graph.connections[static_cast (connectionIndex)])); - node.outputLatencySamples = node.inputLatencySamples + std::max (0, node.processor->getLatencySamples()); + node.outputLatencySamples = node.inputLatencySamples + jmax (0, node.processor->getLatencySamples()); } for (const auto connectionIndex : graph.graphOutputConnections) - graph.graphLatencySamples = std::max (graph.graphLatencySamples, - getSourceLatency (graph, graph.connections[static_cast (connectionIndex)])); + graph.graphLatencySamples = jmax (graph.graphLatencySamples, + getSourceLatency (graph, graph.connections[static_cast (connectionIndex)])); for (auto& connection : graph.connections) { @@ -1008,7 +1248,7 @@ class AudioGraphProcessor::Pimpl ? graph.nodes[static_cast (connection.destinationNodeIndex)].inputLatencySamples : graph.graphLatencySamples; - connection.delaySamples = std::max (0, destinationLatency - getSourceLatency (graph, connection)); + connection.delaySamples = jmax (0, destinationLatency - getSourceLatency (graph, connection)); if (connection.delaySamples > 0) { @@ -1027,10 +1267,10 @@ class AudioGraphProcessor::Pimpl graph.stats.delayLines = static_cast (graph.delayLines.size()); graph.stats.totalCompensationSamples = graph.graphLatencySamples; graph.stats.maxPreallocatedBlockSize = graph.maxBlockSize; - graph.stats.maxPreallocatedChannels = std::max (graph.graphInputChannels, graph.graphOutputChannels); + graph.stats.maxPreallocatedChannels = jmax (graph.graphInputChannels, graph.graphOutputChannels); for (const auto& node : graph.nodes) - graph.stats.maxPreallocatedChannels = std::max (graph.stats.maxPreallocatedChannels, node.workChannels); + graph.stats.maxPreallocatedChannels = jmax (graph.stats.maxPreallocatedChannels, node.workChannels); } void storeStats (const AudioGraphAllocationStats& stats) noexcept @@ -1051,7 +1291,7 @@ class AudioGraphProcessor::Pimpl { graph.graphInputAudio.clear (0, numSamples); - const int numChannels = std::min (graph.graphInputChannels, audioBuffer.getNumChannels()); + const int numChannels = jmin (graph.graphInputChannels, audioBuffer.getNumChannels()); for (int channel = 0; channel < numChannels; ++channel) graph.graphInputAudio.copyFrom (channel, 0, audioBuffer, channel, startSample, numSamples); @@ -1182,9 +1422,9 @@ class AudioGraphProcessor::Pimpl int numSamples, int destinationStartSample) { - const int channels = std::min ({ connection.channels, - std::max (0, sourceAudio.getNumChannels() - connection.sourceOffset), - std::max (0, destinationAudio.getNumChannels() - connection.destinationOffset) }); + const int channels = jmin (connection.channels, + jmax (0, sourceAudio.getNumChannels() - connection.sourceOffset), + jmax (0, destinationAudio.getNumChannels() - connection.destinationOffset)); for (int channel = 0; channel < channels; ++channel) destinationAudio.addFrom (connection.destinationOffset + channel, @@ -1208,10 +1448,10 @@ class AudioGraphProcessor::Pimpl return; } - const int channels = std::min ({ connection.channels, - std::max (0, sourceAudio.getNumChannels() - connection.sourceOffset), - std::max (0, destinationAudio.getNumChannels() - connection.destinationOffset), - delayLine.audio.getNumChannels() }); + const int channels = jmin (connection.channels, + jmax (0, sourceAudio.getNumChannels() - connection.sourceOffset), + jmax (0, destinationAudio.getNumChannels() - connection.destinationOffset), + delayLine.audio.getNumChannels()); const int ringSize = delayLine.audio.getNumSamples(); for (int sample = 0; sample < numSamples; ++sample) @@ -1320,6 +1560,7 @@ class AudioGraphProcessor::Pimpl AudioGraphProcessor& owner; mutable std::mutex commitMutex; mutable std::mutex modelMutex; + mutable std::mutex factoryMutex; mutable std::mutex workgroupMutex; std::vector modelNodes; std::vector modelConnections; @@ -1334,6 +1575,7 @@ class AudioGraphProcessor::Pimpl std::atomic latestTotalCompensationSamples { 0 }; std::atomic latestMaxPreallocatedChannels { 0 }; std::atomic latestMaxPreallocatedBlockSize { 0 }; + AudioGraphProcessor::NodeFactory nodeFactory; AudioWorkgroup workgroup; float sampleRate = 44100.0f; int maxBlockSize = 1024; @@ -1382,6 +1624,7 @@ void AudioGraphProcessor::Pimpl::WorkerThread::run() continue; lastGeneration = generation; + ScopedNoDenormals noDenormals; owner.joinWorkgroup (workgroupToken); owner.drainActiveJobs (generation); @@ -1408,6 +1651,12 @@ AudioGraphNodeID AudioGraphProcessor::addNode (std::unique_ptr p return pimpl->addNode (std::move (processor)); } +AudioGraphNodeID AudioGraphProcessor::addNode (std::unique_ptr processor, + AudioGraphNodeProperties properties) +{ + return pimpl->addNode (std::move (processor), std::move (properties)); +} + bool AudioGraphProcessor::removeNode (AudioGraphNodeID nodeID) { return pimpl->removeNode (nodeID); @@ -1468,6 +1717,41 @@ AudioProcessor* AudioGraphProcessor::getNodeProcessor (AudioGraphNodeID nodeID) return pimpl->getNodeProcessor (nodeID); } +bool AudioGraphProcessor::setNodePosition (AudioGraphNodeID nodeID, float positionX, float positionY) +{ + return pimpl->setNodePosition (nodeID, positionX, positionY); +} + +bool AudioGraphProcessor::setNodeProperties (AudioGraphNodeID nodeID, AudioGraphNodeProperties properties) +{ + return pimpl->setNodeProperties (nodeID, std::move (properties)); +} + +std::optional AudioGraphProcessor::getNodeProperties (AudioGraphNodeID nodeID) const +{ + return pimpl->getNodeProperties (nodeID); +} + +void AudioGraphProcessor::setNodeFactory (NodeFactory factory) +{ + pimpl->setNodeFactory (std::move (factory)); +} + +std::unique_ptr AudioGraphProcessor::createXml() const +{ + auto result = pimpl->createXml(); + + if (! result) + return nullptr; + + return std::move (result).getValue(); +} + +Result AudioGraphProcessor::restoreFromXml (const XmlElement& xml) +{ + return pimpl->restoreFromXml (xml); +} + void AudioGraphProcessor::prepareToPlay (float sampleRate, int maxBlockSize) { pimpl->prepareToPlay (sampleRate, maxBlockSize); diff --git a/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.h b/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.h index c1cd91d5c..394123512 100644 --- a/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.h +++ b/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.h @@ -22,181 +22,6 @@ namespace yup { -//============================================================================== -/** - Opaque identifier for a processor node owned by an AudioGraphProcessor. - - Node identifiers remain stable until the node is removed. The invalid identifier - is used by graph input and graph output endpoints, and by failed addNode calls. -*/ -class AudioGraphNodeID -{ -public: - /** Creates an invalid node identifier. */ - constexpr AudioGraphNodeID() noexcept = default; - - /** Returns an invalid node identifier. */ - static constexpr AudioGraphNodeID invalid() noexcept { return {}; } - - /** Returns true when this identifier names a graph node. */ - constexpr bool isValid() const noexcept { return value != 0; } - - /** Returns the raw integer identifier. */ - constexpr uint64_t getRawID() const noexcept { return value; } - - /** Creates an identifier from a raw value. Prefer IDs returned by addNode(). */ - explicit constexpr AudioGraphNodeID (uint64_t rawValue) noexcept - : value (rawValue) - { - } - - constexpr bool operator== (AudioGraphNodeID other) const noexcept { return value == other.value; } - - constexpr bool operator!= (AudioGraphNodeID other) const noexcept { return value != other.value; } - - constexpr bool operator< (AudioGraphNodeID other) const noexcept { return value < other.value; } - -private: - uint64_t value = 0; -}; - -//============================================================================== -/** - Describes one routable audio or MIDI endpoint in an AudioGraphProcessor. - - Source endpoints are graphInput() and nodeOutput(). Destination endpoints are - graphOutput() and nodeInput(). The bus index refers to the AudioBusLayout of the - graph or the addressed processor node. -*/ -class AudioGraphEndpoint -{ -public: - /** The endpoint owner and direction. */ - enum class Kind - { - graphInput, - graphOutput, - nodeInput, - nodeOutput - }; - - /** Creates an invalid graph input endpoint. */ - AudioGraphEndpoint() = default; - - /** Creates a source endpoint for one graph input bus. */ - static AudioGraphEndpoint graphInput (int busIndex) noexcept - { - return AudioGraphEndpoint (Kind::graphInput, AudioGraphNodeID::invalid(), busIndex); - } - - /** Creates a destination endpoint for one graph output bus. */ - static AudioGraphEndpoint graphOutput (int busIndex) noexcept - { - return AudioGraphEndpoint (Kind::graphOutput, AudioGraphNodeID::invalid(), busIndex); - } - - /** Creates a destination endpoint for one node input bus. */ - static AudioGraphEndpoint nodeInput (AudioGraphNodeID nodeID, int busIndex) noexcept - { - return AudioGraphEndpoint (Kind::nodeInput, nodeID, busIndex); - } - - /** Creates a source endpoint for one node output bus. */ - static AudioGraphEndpoint nodeOutput (AudioGraphNodeID nodeID, int busIndex) noexcept - { - return AudioGraphEndpoint (Kind::nodeOutput, nodeID, busIndex); - } - - /** Returns the endpoint kind. */ - Kind getKind() const noexcept { return kind; } - - /** Returns the addressed node, or an invalid ID for graph endpoints. */ - AudioGraphNodeID getNodeID() const noexcept { return nodeID; } - - /** Returns the bus index on the graph or processor layout. */ - int getBusIndex() const noexcept { return busIndex; } - - /** Returns true when this endpoint can appear as a connection source. */ - bool isSource() const noexcept { return kind == Kind::graphInput || kind == Kind::nodeOutput; } - - /** Returns true when this endpoint can appear as a connection destination. */ - bool isDestination() const noexcept { return kind == Kind::graphOutput || kind == Kind::nodeInput; } - - bool operator== (const AudioGraphEndpoint& other) const noexcept - { - return kind == other.kind && nodeID == other.nodeID && busIndex == other.busIndex; - } - - bool operator!= (const AudioGraphEndpoint& other) const noexcept { return ! (*this == other); } - -private: - AudioGraphEndpoint (Kind endpointKind, AudioGraphNodeID endpointNodeID, int endpointBusIndex) noexcept - : kind (endpointKind) - , nodeID (endpointNodeID) - , busIndex (endpointBusIndex) - { - } - - Kind kind = Kind::graphInput; - AudioGraphNodeID nodeID; - int busIndex = -1; -}; - -//============================================================================== -/** - A directed connection between two graph endpoints. -*/ -class AudioGraphConnection -{ -public: - AudioGraphConnection() = default; - - /** Creates a connection from sourceEndpoint to destinationEndpoint. */ - AudioGraphConnection (AudioGraphEndpoint sourceEndpoint, AudioGraphEndpoint destinationEndpoint) noexcept - : source (sourceEndpoint) - , destination (destinationEndpoint) - { - } - - bool operator== (const AudioGraphConnection& other) const noexcept - { - return source == other.source && destination == other.destination; - } - - bool operator!= (const AudioGraphConnection& other) const noexcept { return ! (*this == other); } - - /** Source endpoint. */ - AudioGraphEndpoint source; - - /** Destination endpoint. */ - AudioGraphEndpoint destination; -}; - -//============================================================================== -/** - Lightweight diagnostics describing the most recently compiled graph plan. -*/ -struct AudioGraphAllocationStats -{ - /** Number of preallocated node scratch audio buffers. */ - int scratchAudioBuffers = 0; - - /** Number of preallocated node MIDI buffers. */ - int midiBuffers = 0; - - /** Number of per-connection delay lines. */ - int delayLines = 0; - - /** Maximum latency compensation inserted on any graph output path. */ - int totalCompensationSamples = 0; - - /** Maximum preallocated audio channel count used by any node or graph endpoint. */ - int maxPreallocatedChannels = 0; - - /** Maximum preallocated block size for the compiled plan. */ - int maxPreallocatedBlockSize = 0; -}; - //============================================================================== /** An AudioProcessor that owns and executes an acyclic graph of AudioProcessor nodes. @@ -210,18 +35,27 @@ struct AudioGraphAllocationStats class YUP_API AudioGraphProcessor final : public AudioProcessor { public: + /** Creates a processor for a saved node. */ + using NodeFactory = std::function> (const AudioGraphNodeProperties&)>; + /** Creates the default stereo graph bus layout. */ static AudioBusLayout createDefaultBusLayout(); + //============================================================================== /** Constructs an audio graph with the supplied graph input/output bus layout. */ explicit AudioGraphProcessor (AudioBusLayout busLayout = createDefaultBusLayout()); /** Destructs the graph and releases all owned processors. */ ~AudioGraphProcessor() override; + //============================================================================== /** Adds a processor node and returns its stable node identifier. */ AudioGraphNodeID addNode (std::unique_ptr processor); + /** Adds a processor node with persistent metadata and returns its stable node identifier. */ + AudioGraphNodeID addNode (std::unique_ptr processor, + AudioGraphNodeProperties properties); + /** Removes a processor node and all connections that mention it. */ bool removeNode (AudioGraphNodeID nodeID); @@ -237,9 +71,14 @@ class YUP_API AudioGraphProcessor final : public AudioProcessor /** Removes all graph nodes and connections. */ void clear(); + //============================================================================== /** Validates, compiles, and publishes the current graph model. */ Result commitChanges(); + /** Returns true when graph edits have not yet been committed. */ + bool hasUncommittedChanges() const noexcept; + + //============================================================================== /** Sets the desired worker thread count for future processing blocks. */ void setNumWorkerThreads (int numThreads); @@ -252,12 +91,31 @@ class YUP_API AudioGraphProcessor final : public AudioProcessor /** Returns diagnostics for the last successfully compiled graph. */ AudioGraphAllocationStats getAllocationStats() const noexcept; - /** Returns true when graph edits have not yet been committed. */ - bool hasUncommittedChanges() const noexcept; - /** Returns the processor for a node, or nullptr when the node is not present. */ AudioProcessor* getNodeProcessor (AudioGraphNodeID nodeID) const noexcept; + //============================================================================== + /** Updates the saved canvas position for a node. */ + bool setNodePosition (AudioGraphNodeID nodeID, float positionX, float positionY); + + /** Updates the persistent metadata for a node. */ + bool setNodeProperties (AudioGraphNodeID nodeID, AudioGraphNodeProperties properties); + + /** Returns persistent metadata for a node, or nullopt when the node is not present. */ + std::optional getNodeProperties (AudioGraphNodeID nodeID) const; + + /** Sets the factory used to recreate processor nodes during state loading. */ + void setNodeFactory (NodeFactory factory); + + //============================================================================== + /** Creates an XML representation of the current graph, including node state. */ + std::unique_ptr createXml() const; + + /** Restores the graph from XML previously created by createXml(). */ + Result restoreFromXml (const XmlElement& xml); + + //============================================================================== + // AudioProcessor void prepareToPlay (float sampleRate, int maxBlockSize) override; void releaseResources() override; void processBlock (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) override; @@ -275,9 +133,9 @@ class YUP_API AudioGraphProcessor final : public AudioProcessor void setPresetName (int, StringRef) override {} - Result loadStateFromMemory (const MemoryBlock&) override { return Result::ok(); } + Result loadStateFromMemory (const MemoryBlock& memoryBlock) override; - Result saveStateIntoMemory (MemoryBlock&) override { return Result::ok(); } + Result saveStateIntoMemory (MemoryBlock& memoryBlock) override; bool hasEditor() const override { return false; } diff --git a/modules/yup_audio_graph/yup_audio_graph.h b/modules/yup_audio_graph/yup_audio_graph.h index 6689929b8..6e82336e9 100644 --- a/modules/yup_audio_graph/yup_audio_graph.h +++ b/modules/yup_audio_graph/yup_audio_graph.h @@ -47,13 +47,20 @@ #include #include #include +#include #include #include #include +#include #include #include #include //============================================================================== +#include "graph/yup_AudioGraphNodeID.h" +#include "graph/yup_AudioGraphEndpoint.h" +#include "graph/yup_AudioGraphConnection.h" +#include "graph/yup_AudioGraphAllocationStats.h" +#include "graph/yup_AudioGraphNodeProperties.h" #include "graph/yup_AudioGraphProcessor.h" diff --git a/modules/yup_core/memory/yup_MemoryBlock.cpp b/modules/yup_core/memory/yup_MemoryBlock.cpp index 67cdcb2f2..1707c4f1d 100644 --- a/modules/yup_core/memory/yup_MemoryBlock.cpp +++ b/modules/yup_core/memory/yup_MemoryBlock.cpp @@ -401,88 +401,14 @@ String MemoryBlock::toBase64Encoding() const return destString; } +// clang-format off static const char base64DecodingTable[] = { - 63, - 0, - 0, - 0, - 0, - 53, - 54, - 55, - 56, - 57, - 58, - 59, - 60, - 61, - 62, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 0, - 0, - 0, - 0, - 0, - 0, - 27, - 28, - 29, - 30, - 31, - 32, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 41, - 42, - 43, - 44, - 45, - 46, - 47, - 48, - 49, - 50, - 51, - 52 + 63, 0, 0, 0, 0, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 0, 0, 0, 0, 0, 0, + 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, + 51, 52 }; +// clang-format on bool MemoryBlock::fromBase64Encoding (StringRef s) { @@ -496,7 +422,7 @@ bool MemoryBlock::fromBase64Encoding (StringRef s) setSize ((size_t) numBytesNeeded, true); auto srcChars = dot + 1; - int pos = 0; + std::size_t pos = 0; for (;;) { diff --git a/tests/yup_audio_graph/yup_AudioGraphProcessor.cpp b/tests/yup_audio_graph/yup_AudioGraphProcessor.cpp index 5124ab596..a23d3d48d 100644 --- a/tests/yup_audio_graph/yup_AudioGraphProcessor.cpp +++ b/tests/yup_audio_graph/yup_AudioGraphProcessor.cpp @@ -380,6 +380,146 @@ class StatefulGainProcessor : public AudioProcessor float gain = 1.0f; }; +AudioGraphNodeProperties statefulGainProperties (float positionX = 0.0f, float positionY = 0.0f) +{ + AudioGraphNodeProperties properties; + properties.identifier = "statefulGain"; + properties.name = "Stateful Gain"; + properties.positionX = positionX; + properties.positionY = positionY; + return properties; +} + +AudioGraphProcessor::NodeFactory statefulGainFactory() +{ + return [] (const AudioGraphNodeProperties& properties) -> ResultValue> + { + if (properties.identifier != "statefulGain") + return ResultValue>::fail ("Unknown node type"); + + return ResultValue>::ok (std::make_unique (1.0f)); + }; +} + +class StatefulExternalProcessor : public AudioProcessor +{ +public: + explicit StatefulExternalProcessor (String processorName, float initialGain = 1.0f) + : AudioProcessor (std::move (processorName), stereoLayout()) + , gain (initialGain) + { + } + + void prepareToPlay (float, int) override {} + + void releaseResources() override {} + + void processBlock (AudioBuffer& audioBuffer, MidiBuffer&) override + { + for (int channel = 0; channel < audioBuffer.getNumChannels(); ++channel) + audioBuffer.applyGain (channel, 0, audioBuffer.getNumSamples(), gain); + } + + int getCurrentPreset() const noexcept override { return 0; } + + void setCurrentPreset (int) noexcept override {} + + int getNumPresets() const override { return 0; } + + String getPresetName (int) const override { return {}; } + + void setPresetName (int, StringRef) override {} + + Result loadStateFromMemory (const MemoryBlock& memoryBlock) override + { + if (memoryBlock.getSize() != sizeof (float)) + return Result::fail ("Invalid external processor gain state"); + + MemoryInputStream stream (memoryBlock, false); + gain = stream.readFloat(); + return Result::ok(); + } + + Result saveStateIntoMemory (MemoryBlock& memoryBlock) override + { + MemoryOutputStream stream (memoryBlock, false); + stream.writeFloat (gain); + stream.flush(); + return Result::ok(); + } + + bool hasEditor() const override { return false; } + +private: + float gain = 1.0f; +}; + +class SaveFailingProcessor : public AudioProcessor +{ +public: + SaveFailingProcessor() + : AudioProcessor ("Save Failing", stereoLayout()) + { + } + + void prepareToPlay (float, int) override {} + + void releaseResources() override {} + + void processBlock (AudioBuffer&, MidiBuffer&) override {} + + int getCurrentPreset() const noexcept override { return 0; } + + void setCurrentPreset (int) noexcept override {} + + int getNumPresets() const override { return 0; } + + String getPresetName (int) const override { return {}; } + + void setPresetName (int, StringRef) override {} + + Result loadStateFromMemory (const MemoryBlock&) override { return Result::ok(); } + + Result saveStateIntoMemory (MemoryBlock&) override { return Result::fail ("Save failed"); } + + bool hasEditor() const override { return false; } +}; + +AudioGraphNodeProperties externalProcessorProperties (float positionX = 0.0f, float positionY = 0.0f) +{ + AudioGraphNodeProperties properties; + properties.identifier = "externalPlugin"; + properties.name = "External Plugin"; + properties.positionX = positionX; + properties.positionY = positionY; + + MemoryOutputStream stream (properties.creationData, false); + stream.writeString ("Fake Plugin"); + stream.writeString ("fake.plugin"); + stream.flush(); + + return properties; +} + +AudioGraphProcessor::NodeFactory statefulGainAndExternalFactory (int& externalLoadCount, String& externalIdentifier) +{ + return [&externalLoadCount, &externalIdentifier] (const AudioGraphNodeProperties& properties) -> ResultValue> + { + if (properties.identifier == "statefulGain") + return ResultValue>::ok (std::make_unique (1.0f)); + + if (properties.identifier != "externalPlugin") + return ResultValue>::fail ("Unknown node type"); + + MemoryInputStream stream (properties.creationData, false); + const auto pluginName = stream.readString(); + externalIdentifier = stream.readString(); + ++externalLoadCount; + + return ResultValue>::ok (std::make_unique (pluginName)); + }; +} + void fillImpulse (AudioBuffer& buffer) { buffer.clear(); @@ -416,6 +556,35 @@ int countMidiEvents (const MidiBuffer& midi) return count; } +std::unique_ptr parseMemoryBlockAsXml (const MemoryBlock& memoryBlock) +{ + MemoryInputStream stream (memoryBlock, false); + return parseXML (stream.readEntireStreamAsString()); +} + +MemoryBlock memoryBlockFromString (const String& text) +{ + MemoryBlock result; + MemoryOutputStream stream (result, false); + stream << text; + stream.flush(); + return result; +} + +MemoryBlock memoryBlockFromXml (const XmlElement& xml) +{ + MemoryBlock result; + MemoryOutputStream stream (result, false); + xml.writeTo (stream); + stream.flush(); + return result; +} + +String toBase64Text (const MemoryBlock& block) +{ + return Base64::toBase64 (block.getData(), block.getSize()); +} + bool resetGraphToSingleGainPath (AudioGraphProcessor& graph, float gain) { graph.clear(); @@ -924,7 +1093,7 @@ TEST (AudioGraphProcessorTests, SaveAndLoadRestoresConnectionsAndNodeState) AudioGraphProcessor graph; auto processor = std::make_unique (0.25f); auto* processorPtr = processor.get(); - const auto node = graph.addNode (std::move (processor)); + const auto node = graph.addNode (std::move (processor), statefulGainProperties (12.0f, 34.0f)); const AudioGraphConnection inputConnection { AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (node, 0) }; @@ -940,15 +1109,33 @@ TEST (AudioGraphProcessorTests, SaveAndLoadRestoresConnectionsAndNodeState) MemoryBlock savedState; EXPECT_TRUE (graph.saveStateIntoMemory (savedState).wasOk()); + auto xml = parseMemoryBlockAsXml (savedState); + ASSERT_NE (nullptr, xml.get()); + EXPECT_TRUE (xml->hasTagName ("YUPAudioGraphState")); + + auto* savedNodeElement = xml->getChildByName ("nodes")->getChildByName ("node"); + ASSERT_NE (nullptr, savedNodeElement); + auto* savedNodeStateElement = savedNodeElement->getChildByName ("state"); + ASSERT_NE (nullptr, savedNodeStateElement); + EXPECT_EQ (String ("base64"), savedNodeStateElement->getStringAttribute ("encoding")); + EXPECT_FALSE (savedNodeStateElement->getAllSubText().trim().isEmpty()); + processorPtr->setGain (0.75f); EXPECT_TRUE (graph.removeConnection (inputConnection)); EXPECT_TRUE (graph.removeConnection (outputConnection)); EXPECT_TRUE (graph.addConnection (bypassConnection).wasOk()); EXPECT_TRUE (graph.commitChanges().wasOk()); + graph.setNodeFactory (statefulGainFactory()); EXPECT_TRUE (graph.loadStateFromMemory (savedState).wasOk()); EXPECT_FALSE (graph.hasUncommittedChanges()); + const auto properties = graph.getNodeProperties (node); + ASSERT_TRUE (properties.has_value()); + EXPECT_EQ (String ("statefulGain"), properties->identifier); + EXPECT_FLOAT_EQ (12.0f, properties->positionX); + EXPECT_FLOAT_EQ (34.0f, properties->positionY); + const auto connections = graph.getConnections(); ASSERT_EQ (2u, connections.size()); EXPECT_EQ (inputConnection, connections[0]); @@ -964,10 +1151,81 @@ TEST (AudioGraphProcessorTests, SaveAndLoadRestoresConnectionsAndNodeState) EXPECT_FLOAT_EQ (0.25f, audio.getReadPointer (1)[0]); } -TEST (AudioGraphProcessorTests, LoadStateFailsWhenSavedNodesAreMissing) +TEST (AudioGraphProcessorTests, LoadStateRecreatesProcessorNodesWithFactory) +{ + AudioGraphProcessor source; + const auto node = source.addNode (std::make_unique (0.25f), + statefulGainProperties (64.0f, 128.0f)); + + ASSERT_TRUE (source.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (node, 0) }).wasOk()); + ASSERT_TRUE (source.addConnection ({ AudioGraphEndpoint::nodeOutput (node, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + ASSERT_TRUE (source.commitChanges().wasOk()); + + MemoryBlock savedState; + ASSERT_TRUE (source.saveStateIntoMemory (savedState).wasOk()); + + AudioGraphProcessor destination; + destination.setNodeFactory (statefulGainFactory()); + + EXPECT_TRUE (destination.loadStateFromMemory (savedState).wasOk()); + + const auto properties = destination.getNodeProperties (node); + ASSERT_TRUE (properties.has_value()); + EXPECT_EQ (String ("statefulGain"), properties->identifier); + EXPECT_FLOAT_EQ (64.0f, properties->positionX); + EXPECT_FLOAT_EQ (128.0f, properties->positionY); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + fillImpulse (audio); + + destination.processBlock (audio, midi); + + EXPECT_FLOAT_EQ (0.25f, audio.getReadPointer (0)[0]); + EXPECT_FLOAT_EQ (0.25f, audio.getReadPointer (1)[0]); +} + +TEST (AudioGraphProcessorTests, CreateXmlAndRestoreFromXmlCanBeUsedDirectly) +{ + AudioGraphProcessor source; + const auto node = source.addNode (std::make_unique (0.25f), + statefulGainProperties (96.0f, 192.0f)); + + ASSERT_TRUE (source.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (node, 0) }).wasOk()); + ASSERT_TRUE (source.addConnection ({ AudioGraphEndpoint::nodeOutput (node, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + ASSERT_TRUE (source.commitChanges().wasOk()); + + const auto xml = source.createXml(); + ASSERT_NE (nullptr, xml.get()); + EXPECT_TRUE (xml->hasTagName ("YUPAudioGraphState")); + + AudioGraphProcessor destination; + destination.setNodeFactory (statefulGainFactory()); + + ASSERT_TRUE (destination.restoreFromXml (*xml).wasOk()); + EXPECT_FALSE (destination.hasUncommittedChanges()); + + const auto properties = destination.getNodeProperties (node); + ASSERT_TRUE (properties.has_value()); + EXPECT_EQ (String ("statefulGain"), properties->identifier); + EXPECT_FLOAT_EQ (96.0f, properties->positionX); + EXPECT_FLOAT_EQ (192.0f, properties->positionY); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + fillImpulse (audio); + + destination.processBlock (audio, midi); + + EXPECT_FLOAT_EQ (0.25f, audio.getReadPointer (0)[0]); + EXPECT_FLOAT_EQ (0.25f, audio.getReadPointer (1)[0]); +} + +TEST (AudioGraphProcessorTests, LoadStateFailsWhenFactoryIsMissing) { AudioGraphProcessor source; - const auto node = source.addNode (std::make_unique (0.25f)); + const auto node = source.addNode (std::make_unique (0.25f), + statefulGainProperties()); ASSERT_TRUE (source.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (node, 0) }).wasOk()); ASSERT_TRUE (source.addConnection ({ AudioGraphEndpoint::nodeOutput (node, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); @@ -980,6 +1238,373 @@ TEST (AudioGraphProcessorTests, LoadStateFailsWhenSavedNodesAreMissing) EXPECT_TRUE (destination.loadStateFromMemory (savedState).failed()); } +TEST (AudioGraphProcessorTests, LoadStateRecreatesExternalNodesWithOpaqueCreationData) +{ + AudioGraphProcessor source; + const auto node = source.addNode (std::make_unique ("Fake Plugin", 0.25f), + externalProcessorProperties (4.0f, 8.0f)); + + ASSERT_TRUE (source.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (node, 0) }).wasOk()); + ASSERT_TRUE (source.addConnection ({ AudioGraphEndpoint::nodeOutput (node, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + ASSERT_TRUE (source.commitChanges().wasOk()); + + MemoryBlock savedState; + ASSERT_TRUE (source.saveStateIntoMemory (savedState).wasOk()); + + auto xml = parseMemoryBlockAsXml (savedState); + ASSERT_NE (nullptr, xml.get()); + auto* savedNodeElement = xml->getChildByName ("nodes")->getChildByName ("node"); + ASSERT_NE (nullptr, savedNodeElement); + auto* creationDataElement = savedNodeElement->getChildByName ("creationData"); + ASSERT_NE (nullptr, creationDataElement); + EXPECT_EQ (String ("base64"), creationDataElement->getStringAttribute ("encoding")); + EXPECT_FALSE (creationDataElement->getAllSubText().trim().isEmpty()); + + int externalLoadCount = 0; + String externalIdentifier; + + AudioGraphProcessor destination; + destination.setNodeFactory (statefulGainAndExternalFactory (externalLoadCount, externalIdentifier)); + + EXPECT_TRUE (destination.loadStateFromMemory (savedState).wasOk()); + EXPECT_EQ (1, externalLoadCount); + EXPECT_EQ (String ("fake.plugin"), externalIdentifier); + + const auto properties = destination.getNodeProperties (node); + ASSERT_TRUE (properties.has_value()); + EXPECT_EQ (String ("externalPlugin"), properties->identifier); + EXPECT_FALSE (properties->creationData.isEmpty()); + EXPECT_FLOAT_EQ (4.0f, properties->positionX); + EXPECT_FLOAT_EQ (8.0f, properties->positionY); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + fillImpulse (audio); + + destination.processBlock (audio, midi); + + EXPECT_FLOAT_EQ (0.25f, audio.getReadPointer (0)[0]); + EXPECT_FLOAT_EQ (0.25f, audio.getReadPointer (1)[0]); +} + +TEST (AudioGraphProcessorTests, LoadStateFailureRestoresPreviousGraphModel) +{ + AudioGraphProcessor invalidSource; + ASSERT_TRUE (invalidSource.addConnection ({ AudioGraphEndpoint::nodeOutput (AudioGraphNodeID (999), 0), + AudioGraphEndpoint::graphOutput (0) }) + .wasOk()); + + MemoryBlock invalidState; + ASSERT_TRUE (invalidSource.saveStateIntoMemory (invalidState).wasOk()); + + AudioGraphProcessor destination; + ASSERT_TRUE (resetGraphToSingleGainPath (destination, 0.5f)); + EXPECT_FALSE (destination.hasUncommittedChanges()); + + EXPECT_TRUE (destination.loadStateFromMemory (invalidState).failed()); + EXPECT_FALSE (destination.hasUncommittedChanges()); + + const auto connections = destination.getConnections(); + ASSERT_EQ (2u, connections.size()); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + fillImpulse (audio); + + destination.processBlock (audio, midi); + + EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (0)[0]); + EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (1)[0]); +} + +TEST (AudioGraphProcessorTests, SaveStateWritesCompleteXmlTopology) +{ + AudioGraphProcessor graph; + const auto gainNode = graph.addNode (std::make_unique (0.5f), + statefulGainProperties (11.0f, 22.0f)); + const auto externalNode = graph.addNode (std::make_unique ("External", 0.25f), + externalProcessorProperties (33.0f, 44.0f)); + + ASSERT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (gainNode, 0) }).wasOk()); + ASSERT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (gainNode, 0), AudioGraphEndpoint::nodeInput (externalNode, 0) }).wasOk()); + ASSERT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (externalNode, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + ASSERT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + ASSERT_TRUE (graph.commitChanges().wasOk()); + + MemoryBlock savedState; + ASSERT_TRUE (graph.saveStateIntoMemory (savedState).wasOk()); + + const auto xml = parseMemoryBlockAsXml (savedState); + ASSERT_NE (nullptr, xml.get()); + EXPECT_TRUE (xml->hasTagName ("YUPAudioGraphState")); + EXPECT_EQ (1, xml->getIntAttribute ("version")); + EXPECT_EQ (String (static_cast (externalNode.getRawID())), xml->getStringAttribute ("nextNodeID")); + + auto* nodesElement = xml->getChildByName ("nodes"); + ASSERT_NE (nullptr, nodesElement); + EXPECT_EQ (2, nodesElement->getNumChildElements()); + + auto* gainElement = nodesElement->getChildByAttribute ("id", String (static_cast (gainNode.getRawID()))); + ASSERT_NE (nullptr, gainElement); + EXPECT_EQ (String ("statefulGain"), gainElement->getStringAttribute ("identifier")); + EXPECT_EQ (String ("Stateful Gain"), gainElement->getStringAttribute ("name")); + EXPECT_DOUBLE_EQ (11.0, gainElement->getDoubleAttribute ("positionX")); + EXPECT_DOUBLE_EQ (22.0, gainElement->getDoubleAttribute ("positionY")); + ASSERT_NE (nullptr, gainElement->getChildByName ("state")); + ASSERT_NE (nullptr, gainElement->getChildByName ("creationData")); + + auto* externalElement = nodesElement->getChildByAttribute ("id", String (static_cast (externalNode.getRawID()))); + ASSERT_NE (nullptr, externalElement); + EXPECT_EQ (String ("externalPlugin"), externalElement->getStringAttribute ("identifier")); + EXPECT_EQ (String ("External Plugin"), externalElement->getStringAttribute ("name")); + EXPECT_DOUBLE_EQ (33.0, externalElement->getDoubleAttribute ("positionX")); + EXPECT_DOUBLE_EQ (44.0, externalElement->getDoubleAttribute ("positionY")); + EXPECT_FALSE (externalElement->getChildByName ("creationData")->getAllSubText().trim().isEmpty()); + + auto* connectionsElement = xml->getChildByName ("connections"); + ASSERT_NE (nullptr, connectionsElement); + EXPECT_EQ (4, connectionsElement->getNumChildElements()); + + auto* firstConnection = connectionsElement->getChildByName ("connection"); + ASSERT_NE (nullptr, firstConnection); + ASSERT_NE (nullptr, firstConnection->getChildByName ("source")); + ASSERT_NE (nullptr, firstConnection->getChildByName ("destination")); + EXPECT_EQ (String ("graphInput"), firstConnection->getChildByName ("source")->getStringAttribute ("kind")); + EXPECT_EQ (String ("nodeInput"), firstConnection->getChildByName ("destination")->getStringAttribute ("kind")); +} + +TEST (AudioGraphProcessorTests, LoadStateRestoresMultiNodeXmlGraphAndNextNodeID) +{ + AudioGraphProcessor source; + const auto firstNode = source.addNode (std::make_unique (0.5f), + statefulGainProperties (10.0f, 20.0f)); + const auto secondNode = source.addNode (std::make_unique (0.25f), + statefulGainProperties (30.0f, 40.0f)); + + ASSERT_TRUE (source.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (firstNode, 0) }).wasOk()); + ASSERT_TRUE (source.addConnection ({ AudioGraphEndpoint::nodeOutput (firstNode, 0), AudioGraphEndpoint::nodeInput (secondNode, 0) }).wasOk()); + ASSERT_TRUE (source.addConnection ({ AudioGraphEndpoint::nodeOutput (secondNode, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + ASSERT_TRUE (source.commitChanges().wasOk()); + + MemoryBlock savedState; + ASSERT_TRUE (source.saveStateIntoMemory (savedState).wasOk()); + + AudioGraphProcessor destination; + destination.setNodeFactory (statefulGainFactory()); + + ASSERT_TRUE (destination.loadStateFromMemory (savedState).wasOk()); + EXPECT_FALSE (destination.hasUncommittedChanges()); + + const auto firstProperties = destination.getNodeProperties (firstNode); + ASSERT_TRUE (firstProperties.has_value()); + EXPECT_FLOAT_EQ (10.0f, firstProperties->positionX); + EXPECT_FLOAT_EQ (20.0f, firstProperties->positionY); + + const auto secondProperties = destination.getNodeProperties (secondNode); + ASSERT_TRUE (secondProperties.has_value()); + EXPECT_FLOAT_EQ (30.0f, secondProperties->positionX); + EXPECT_FLOAT_EQ (40.0f, secondProperties->positionY); + + const auto connections = destination.getConnections(); + ASSERT_EQ (3u, connections.size()); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + fillImpulse (audio); + + destination.processBlock (audio, midi); + + EXPECT_FLOAT_EQ (0.125f, audio.getReadPointer (0)[0]); + EXPECT_FLOAT_EQ (0.125f, audio.getReadPointer (1)[0]); + + const auto nextNode = destination.addNode (std::make_unique()); + EXPECT_GT (nextNode.getRawID(), secondNode.getRawID()); +} + +TEST (AudioGraphProcessorTests, SaveStateFailsWhenNodeStateSaveFails) +{ + AudioGraphProcessor graph; + const auto node = graph.addNode (std::make_unique()); + + ASSERT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (node, 0) }).wasOk()); + ASSERT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (node, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + + MemoryBlock savedState; + EXPECT_TRUE (graph.saveStateIntoMemory (savedState).failed()); +} + +TEST (AudioGraphProcessorTests, CreateXmlReturnsNullWhenNodeStateSaveFails) +{ + AudioGraphProcessor graph; + const auto node = graph.addNode (std::make_unique()); + + ASSERT_TRUE (graph.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (node, 0) }).wasOk()); + ASSERT_TRUE (graph.addConnection ({ AudioGraphEndpoint::nodeOutput (node, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + + auto xml = graph.createXml(); + EXPECT_EQ (nullptr, xml.get()); +} + +TEST (AudioGraphProcessorTests, LoadStateRejectsMalformedXmlHeaderAndUnsupportedVersion) +{ + AudioGraphProcessor graph; + const String unsupportedVersionXml = "" + "" + "" + ""; + + EXPECT_TRUE (graph.loadStateFromMemory (memoryBlockFromString ("not xml")).failed()); + EXPECT_TRUE (graph.loadStateFromMemory (memoryBlockFromString ("")).failed()); + EXPECT_TRUE (graph.loadStateFromMemory (memoryBlockFromString (unsupportedVersionXml)).failed()); +} + +TEST (AudioGraphProcessorTests, LoadStateRejectsMissingRequiredXmlSections) +{ + AudioGraphProcessor graph; + const String missingNodesXml = "" + "" + ""; + const String missingConnectionsXml = "" + "" + ""; + + EXPECT_TRUE (graph.loadStateFromMemory (memoryBlockFromString (missingNodesXml)).failed()); + EXPECT_TRUE (graph.loadStateFromMemory (memoryBlockFromString (missingConnectionsXml)).failed()); +} + +TEST (AudioGraphProcessorTests, LoadStateRejectsInvalidBase64Payloads) +{ + AudioGraphProcessor source; + const auto node = source.addNode (std::make_unique ("External", 0.25f), + externalProcessorProperties()); + + ASSERT_TRUE (source.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (node, 0) }).wasOk()); + ASSERT_TRUE (source.addConnection ({ AudioGraphEndpoint::nodeOutput (node, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + ASSERT_TRUE (source.commitChanges().wasOk()); + + MemoryBlock savedState; + ASSERT_TRUE (source.saveStateIntoMemory (savedState).wasOk()); + + auto invalidNodeState = parseMemoryBlockAsXml (savedState); + ASSERT_NE (nullptr, invalidNodeState.get()); + auto* stateElement = invalidNodeState->getChildByName ("nodes")->getChildByName ("node")->getChildByName ("state"); + ASSERT_NE (nullptr, stateElement); + stateElement->deleteAllChildElements(); + stateElement->addTextElement ("not-base64"); + + int externalLoadCount = 0; + String externalIdentifier; + + AudioGraphProcessor destination; + destination.setNodeFactory (statefulGainAndExternalFactory (externalLoadCount, externalIdentifier)); + EXPECT_TRUE (destination.loadStateFromMemory (memoryBlockFromXml (*invalidNodeState)).failed()); + EXPECT_EQ (0, externalLoadCount); + + auto invalidCreationData = parseMemoryBlockAsXml (savedState); + ASSERT_NE (nullptr, invalidCreationData.get()); + auto* creationDataElement = invalidCreationData->getChildByName ("nodes")->getChildByName ("node")->getChildByName ("creationData"); + ASSERT_NE (nullptr, creationDataElement); + creationDataElement->deleteAllChildElements(); + creationDataElement->addTextElement ("not-base64"); + + EXPECT_TRUE (destination.loadStateFromMemory (memoryBlockFromXml (*invalidCreationData)).failed()); + EXPECT_EQ (0, externalLoadCount); +} + +TEST (AudioGraphProcessorTests, LoadStateRejectsInvalidConnectionXml) +{ + AudioGraphProcessor source; + const auto node = source.addNode (std::make_unique (0.5f), + statefulGainProperties()); + + ASSERT_TRUE (source.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (node, 0) }).wasOk()); + ASSERT_TRUE (source.addConnection ({ AudioGraphEndpoint::nodeOutput (node, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + ASSERT_TRUE (source.commitChanges().wasOk()); + + MemoryBlock savedState; + ASSERT_TRUE (source.saveStateIntoMemory (savedState).wasOk()); + + auto invalidKind = parseMemoryBlockAsXml (savedState); + ASSERT_NE (nullptr, invalidKind.get()); + auto* firstConnection = invalidKind->getChildByName ("connections")->getChildByName ("connection"); + ASSERT_NE (nullptr, firstConnection); + firstConnection->getChildByName ("source")->setAttribute ("kind", "invalidKind"); + + AudioGraphProcessor destination; + destination.setNodeFactory (statefulGainFactory()); + EXPECT_TRUE (destination.loadStateFromMemory (memoryBlockFromXml (*invalidKind)).failed()); + + auto missingDestination = parseMemoryBlockAsXml (savedState); + ASSERT_NE (nullptr, missingDestination.get()); + firstConnection = missingDestination->getChildByName ("connections")->getChildByName ("connection"); + ASSERT_NE (nullptr, firstConnection); + firstConnection->removeChildElement (firstConnection->getChildByName ("destination"), true); + + EXPECT_TRUE (destination.loadStateFromMemory (memoryBlockFromXml (*missingDestination)).failed()); +} + +TEST (AudioGraphProcessorTests, LoadStateFailsWhenFactoryReturnsNullProcessor) +{ + AudioGraphProcessor source; + const auto node = source.addNode (std::make_unique (0.25f), + statefulGainProperties()); + + ASSERT_TRUE (source.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (node, 0) }).wasOk()); + ASSERT_TRUE (source.addConnection ({ AudioGraphEndpoint::nodeOutput (node, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + ASSERT_TRUE (source.commitChanges().wasOk()); + + MemoryBlock savedState; + ASSERT_TRUE (source.saveStateIntoMemory (savedState).wasOk()); + + AudioGraphProcessor destination; + destination.setNodeFactory ([] (const AudioGraphNodeProperties&) -> ResultValue> + { + return ResultValue>::ok (std::unique_ptr()); + }); + + EXPECT_TRUE (destination.loadStateFromMemory (savedState).failed()); +} + +TEST (AudioGraphProcessorTests, LoadStateFailureFromNodeStateCallbackRestoresPreviousGraph) +{ + AudioGraphProcessor source; + const auto node = source.addNode (std::make_unique (0.25f), + statefulGainProperties()); + + ASSERT_TRUE (source.addConnection ({ AudioGraphEndpoint::graphInput (0), AudioGraphEndpoint::nodeInput (node, 0) }).wasOk()); + ASSERT_TRUE (source.addConnection ({ AudioGraphEndpoint::nodeOutput (node, 0), AudioGraphEndpoint::graphOutput (0) }).wasOk()); + ASSERT_TRUE (source.commitChanges().wasOk()); + + MemoryBlock savedState; + ASSERT_TRUE (source.saveStateIntoMemory (savedState).wasOk()); + + auto xml = parseMemoryBlockAsXml (savedState); + ASSERT_NE (nullptr, xml.get()); + auto* stateElement = xml->getChildByName ("nodes")->getChildByName ("node")->getChildByName ("state"); + ASSERT_NE (nullptr, stateElement); + stateElement->deleteAllChildElements(); + + const MemoryBlock invalidState ("bad", 3); + stateElement->addTextElement (toBase64Text (invalidState)); + + AudioGraphProcessor destination; + destination.setNodeFactory (statefulGainFactory()); + ASSERT_TRUE (resetGraphToSingleGainPath (destination, 0.5f)); + EXPECT_FALSE (destination.hasUncommittedChanges()); + + EXPECT_TRUE (destination.loadStateFromMemory (memoryBlockFromXml (*xml)).failed()); + EXPECT_FALSE (destination.hasUncommittedChanges()); + + AudioBuffer audio (2, 16); + MidiBuffer midi; + fillImpulse (audio); + + destination.processBlock (audio, midi); + + EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (0)[0]); + EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (1)[0]); +} + TEST (AudioGraphProcessorTests, RemoveConnectionStopsRoutingAfterCommit) { AudioGraphProcessor graph; From ea79de86f80ff6b1b8e7d4c9851b48599db84332 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 18 May 2026 11:35:20 +0200 Subject: [PATCH 20/35] More fixes --- .../graphics/source/examples/PluginHost.h | 144 +-- .../native/yup_AudioPluginInstance_AUv2.mm | 41 +- .../native/yup_AudioPluginInstance_CLAP.cpp | 16 +- .../native/yup_AudioPluginInstance_VST3.cpp | 173 +++- .../processors/yup_AudioProcessor.cpp | 14 + .../processors/yup_AudioProcessor.h | 23 + .../yup_audio_processors.cpp | 80 +- .../yup_audio_processors.h | 122 +-- modules/yup_core/memory/yup_MemoryBlock.cpp | 888 +++++++++--------- modules/yup_core/misc/yup_ResultValue.h | 394 ++++---- modules/yup_core/system/yup_CompilerSupport.h | 236 ++--- 11 files changed, 1152 insertions(+), 979 deletions(-) diff --git a/examples/graphics/source/examples/PluginHost.h b/examples/graphics/source/examples/PluginHost.h index f4a9d976b..abf22b1e2 100644 --- a/examples/graphics/source/examples/PluginHost.h +++ b/examples/graphics/source/examples/PluginHost.h @@ -270,9 +270,10 @@ class PluginHostDemo : public yup::Component if (scanner != nullptr) scanner->cancelPendingScans(); - unloadPlugin(); audioDeviceManager.removeAudioCallback (this); audioDeviceManager.closeAudioDevice(); + unloadPlugin(); + cleanupRetiredPlugins (true); } void resized() override @@ -384,12 +385,8 @@ class PluginHostDemo : public yup::Component // If a plugin is loaded, process through it yup::MidiBuffer midi; - { - const yup::ScopedLock lock (pluginLock); - - if (loadedPlugin != nullptr) - loadedPlugin->processBlock (processBuffer, midi); - } + if (auto plugin = getLoadedPlugin()) + plugin->processBlock (processBuffer, midi); // Mix dry/wet into output const float wet = dryWetMix.getNextValue(); @@ -416,16 +413,14 @@ class PluginHostDemo : public yup::Component processBuffer.setSize (2, bs); dryBuffer.setSize (2, bs); - const yup::ScopedLock lock (pluginLock); - if (loadedPlugin != nullptr) - loadedPlugin->prepareToPlay (sr, bs); + if (auto plugin = getLoadedPlugin()) + plugin->prepareToPlay (sr, bs); } void audioDeviceStopped() override { - const yup::ScopedLock lock (pluginLock); - if (loadedPlugin != nullptr) - loadedPlugin->releaseResources(); + if (auto plugin = getLoadedPlugin()) + plugin->releaseResources(); } //============================================================================== @@ -434,10 +429,10 @@ class PluginHostDemo : public yup::Component void timerCallback() override { updateStatusLine(); + cleanupRetiredPlugins(); // Update visible parameter rows from plugin values - const yup::ScopedLock lock (pluginLock); - if (loadedPlugin != nullptr) + if (getLoadedPlugin() != nullptr) refreshVisibleParameterRows(); } @@ -716,12 +711,9 @@ class PluginHostDemo : public yup::Component clearParameterSliders(); clearPresetCombo(); - { - const yup::ScopedLock lock (pluginLock); - unloadPluginInternal(); - loadedPlugin = std::move (result).getValue(); - loadedPlugin->prepareToPlay (hostCtx.sampleRate, hostCtx.maxBlockSize); - } + auto plugin = std::shared_ptr (std::move (result).getValue()); + plugin->prepareToPlay (hostCtx.sampleRate, hostCtx.maxBlockSize); + retirePlugin (std::atomic_exchange (&loadedPlugin, std::move (plugin))); buildParameterSliders(); populatePresetCombo(); @@ -737,10 +729,7 @@ class PluginHostDemo : public yup::Component clearParameterSliders(); clearPresetCombo(); - { - const yup::ScopedLock lock (pluginLock); - unloadPluginInternal(); - } + unloadPluginInternal(); updateEditorButtonState(); statusText = "Plugin unloaded."; @@ -748,11 +737,7 @@ class PluginHostDemo : public yup::Component void unloadPluginInternal() { - if (loadedPlugin != nullptr) - { - loadedPlugin->releaseResources(); - loadedPlugin.reset(); - } + retirePlugin (std::atomic_exchange (&loadedPlugin, std::shared_ptr())); } void openPluginEditor() @@ -766,25 +751,23 @@ class PluginHostDemo : public yup::Component yup::AudioProcessorEditor* editor = nullptr; yup::String pluginName; - { - const yup::ScopedLock lock (pluginLock); - - if (loadedPlugin == nullptr) - { - statusText = "No plugin loaded."; - return; - } + auto plugin = getLoadedPlugin(); - if (! loadedPlugin->hasEditor()) - { - statusText = "Loaded plugin has no editor."; - return; - } + if (plugin == nullptr) + { + statusText = "No plugin loaded."; + return; + } - pluginName = loadedPlugin->getDescription().name; - editor = loadedPlugin->createEditor(); + if (! plugin->hasEditor()) + { + statusText = "Loaded plugin has no editor."; + return; } + pluginName = plugin->getDescription().name; + editor = plugin->createEditor(); + if (editor == nullptr) { statusText = "Could not create plugin editor."; @@ -822,10 +805,8 @@ class PluginHostDemo : public yup::Component { bool canOpenEditor = false; - { - const yup::ScopedLock lock (pluginLock); - canOpenEditor = loadedPlugin != nullptr && loadedPlugin->hasEditor(); - } + if (auto plugin = getLoadedPlugin()) + canOpenEditor = plugin->hasEditor(); openEditorButton.setEnabled (canOpenEditor); openEditorButton.setButtonText (pluginEditorWindow != nullptr ? "Show Editor" : "Open Editor"); @@ -835,24 +816,24 @@ class PluginHostDemo : public yup::Component { clearPresetCombo(); - const yup::ScopedLock lock (pluginLock); - if (loadedPlugin == nullptr) + auto plugin = getLoadedPlugin(); + if (plugin == nullptr) return; - const auto numPresets = loadedPlugin->getNumPresets(); + const auto numPresets = plugin->getNumPresets(); if (numPresets <= 0) return; for (int i = 0; i < numPresets; ++i) { - auto name = loadedPlugin->getPresetName (i); + auto name = plugin->getPresetName (i); if (name.isEmpty()) name = "Preset " + yup::String (i + 1); presetCombo.addItem (name, i + 1); } - const auto currentPreset = loadedPlugin->getCurrentPreset(); + const auto currentPreset = plugin->getCurrentPreset(); if (yup::isPositiveAndBelow (currentPreset, numPresets)) presetCombo.setSelectedId (currentPreset + 1, yup::dontSendNotification); @@ -873,14 +854,12 @@ class PluginHostDemo : public yup::Component yup::String presetName; - { - const yup::ScopedLock lock (pluginLock); - if (loadedPlugin == nullptr) - return; + auto plugin = getLoadedPlugin(); + if (plugin == nullptr) + return; - loadedPlugin->setCurrentPreset (selectedPreset); - presetName = loadedPlugin->getPresetName (selectedPreset); - } + plugin->setCurrentPreset (selectedPreset); + presetName = plugin->getPresetName (selectedPreset); statusText = "Preset: " + (presetName.isNotEmpty() ? presetName : yup::String (selectedPreset + 1)); refreshVisibleParameterRows(); @@ -890,13 +869,14 @@ class PluginHostDemo : public yup::Component { clearParameterSliders(); - if (loadedPlugin == nullptr) + auto plugin = getLoadedPlugin(); + if (plugin == nullptr) return; - const auto params = loadedPlugin->getParameters(); + const auto params = plugin->getParameters(); if (params.empty()) { - statusText = "Loaded: " + loadedPlugin->getDescription().name + " (no adjustable parameters)"; + statusText = "Loaded: " + plugin->getDescription().name + " (no adjustable parameters)"; return; } @@ -921,6 +901,32 @@ class PluginHostDemo : public yup::Component resized(); } + std::shared_ptr getLoadedPlugin() + { + return std::atomic_load (&loadedPlugin); + } + + void retirePlugin (std::shared_ptr plugin) + { + if (plugin != nullptr) + retiredPlugins.push_back ({ std::move (plugin), 20 }); + } + + void cleanupRetiredPlugins (bool force = false) + { + for (auto iterator = retiredPlugins.begin(); iterator != retiredPlugins.end();) + { + if (! force && --iterator->timerTicksRemaining > 0) + { + ++iterator; + continue; + } + + iterator->plugin->releaseResources(); + iterator = retiredPlugins.erase (iterator); + } + } + void refreshVisibleParameterRows() { if (parameterListModel.getNumRows() <= 0) @@ -991,6 +997,12 @@ class PluginHostDemo : public yup::Component yup::Array items; }; + struct RetiredPlugin + { + std::shared_ptr plugin; + int timerTicksRemaining = 0; + }; + //============================================================================== // Audio @@ -1008,9 +1020,9 @@ class PluginHostDemo : public yup::Component std::unique_ptr scanner; std::vector discoveredPlugins; int selectedPluginIndex = -1; - std::unique_ptr loadedPlugin; + std::shared_ptr loadedPlugin; + std::vector retiredPlugins; std::unique_ptr pluginEditorWindow; - yup::CriticalSection pluginLock; // Dry/wet mix yup::SmoothedValue dryWetMix; diff --git a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.mm b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.mm index 6adfa4d66..911c6597e 100644 --- a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.mm +++ b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.mm @@ -71,6 +71,16 @@ AudioPluginDescription descriptionFromComponent(AudioComponent comp, desc.isInstrument = (acd.componentType == kAudioUnitType_MusicDevice); desc.isEffect = (acd.componentType == kAudioUnitType_Effect || acd.componentType == kAudioUnitType_MusicEffect); + if (acd.componentType == kAudioUnitType_Effect || acd.componentType == kAudioUnitType_MusicEffect) + { + desc.numInputChannels = 2; + desc.numOutputChannels = 2; + } + else if (acd.componentType == kAudioUnitType_MusicDevice || acd.componentType == kAudioUnitType_Generator) + { + desc.numOutputChannels = 2; + } + return desc; } @@ -663,13 +673,40 @@ bool hasEditor() const override if (AudioComponentInstanceNew(comp, &unit) != noErr || unit == nullptr) return nullptr; - AudioBusLayout busLayout({}, {}); - // TODO: query kAudioUnitProperty_ElementCount for proper bus layout + AudioBusLayout busLayout = makeBusLayout(desc, acd.componentType); return std::make_unique(desc, unit, std::move(busLayout)); } private: + static AudioBusLayout makeBusLayout(const AudioPluginDescription& desc, OSType componentType) + { + std::vector inputs; + std::vector outputs; + + int inputChannels = desc.numInputChannels; + int outputChannels = desc.numOutputChannels; + + if (componentType == kAudioUnitType_Effect || componentType == kAudioUnitType_MusicEffect) + { + outputChannels = jmax(1, outputChannels, 2); + inputChannels = jmax(1, inputChannels, outputChannels); + } + else if (componentType == kAudioUnitType_MusicDevice || componentType == kAudioUnitType_Generator) + { + inputChannels = jmax(0, inputChannels); + outputChannels = jmax(1, outputChannels, 2); + } + + if (inputChannels > 0) + inputs.emplace_back("Input", AudioBus::Type::Audio, AudioBus::Direction::Input, inputChannels); + + if (outputChannels > 0) + outputs.emplace_back("Output", AudioBus::Type::Audio, AudioBus::Direction::Output, outputChannels); + + return AudioBusLayout(std::move(inputs), std::move(outputs)); + } + void parameterValueChanged(const AudioParameter::Ptr& parameter, int indexInContainer) override { if (audioUnit == nullptr || !isPositiveAndBelow(indexInContainer, static_cast(auParameterIds.size()))) diff --git a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.cpp b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.cpp index 62bcbd256..629ada293 100644 --- a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.cpp +++ b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.cpp @@ -74,15 +74,21 @@ struct CLAPModule struct YUPCLAPHost { clap_host_t host {}; + String hostName; + String hostVendor; + String hostVersion; YUPCLAPHost (const AudioPluginHostContext& ctx) + : hostName (ctx.hostName) + , hostVendor (ctx.hostVendor) + , hostVersion (ctx.hostVersion) { host.clap_version = CLAP_VERSION; host.host_data = this; - host.name = ctx.hostName.toRawUTF8(); - host.vendor = ctx.hostVendor.toRawUTF8(); + host.name = hostName.toRawUTF8(); + host.vendor = hostVendor.toRawUTF8(); host.url = ""; - host.version = ctx.hostVersion.toRawUTF8(); + host.version = hostVersion.toRawUTF8(); host.get_extension = [] (const clap_host_t*, const char*) -> const void* { return nullptr; @@ -189,7 +195,7 @@ class CLAPInstance : public AudioPluginInstance preparedOutPtrs.resize (static_cast (numChannels)); clapInputEvents.parameterEvents.reserve (clapParameterIds.size()); - clapPlugin->activate (clapPlugin, sampleRate, static_cast (maxBlockSize), static_cast (maxBlockSize)); + clapPlugin->activate (clapPlugin, sampleRate, 1, static_cast (jmax (1, maxBlockSize))); clapPlugin->start_processing (clapPlugin); } @@ -427,7 +433,7 @@ class CLAPInstance : public AudioPluginInstance continue; auto param = AudioParameterBuilder() - .withID (String (info.name)) + .withID (String (static_cast (info.id))) .withName (String (info.name)) .withRange (static_cast (info.min_value), static_cast (info.max_value)) diff --git a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.cpp b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.cpp index d492cd6e5..229466597 100644 --- a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.cpp +++ b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.cpp @@ -36,6 +36,11 @@ String toString (const Vst::TChar* src) return String (CharPointer_UTF16 (reinterpret_cast (src))); } +String classIDToString (const TUID& cid) +{ + return String::toHexString (cid, static_cast (sizeof (TUID))); +} + //============================================================================== // RAII wrapper around a dynamically loaded VST3 module. struct VST3Module @@ -132,12 +137,20 @@ class VST3Instance : public AudioPluginInstance { Vst::ProcessSetup setup; setup.processMode = Vst::kRealtime; - setup.symbolicSampleSize = hostContext.preferDoublePrecision - ? Vst::kSample64 - : Vst::kSample32; + setProcessingPrecision (hostContext.preferDoublePrecision && supportsDoublePrecisionProcessing() + ? ProcessingPrecision::doublePrecision + : ProcessingPrecision::singlePrecision); + + setup.symbolicSampleSize = isUsingDoublePrecision() ? Vst::kSample64 : Vst::kSample32; setup.maxSamplesPerBlock = maxBlockSize; setup.sampleRate = sampleRate; + if (isUsingDoublePrecision()) + { + const int numChannels = jmax (1, pluginDescription.numInputChannels, pluginDescription.numOutputChannels); + doublePrecisionBuffer.setSize (numChannels, maxBlockSize, false, true, false); + } + vst3Processor->setupProcessing (setup); const int numInputs = vst3Component->getBusCount (Vst::kAudio, Vst::kInput); @@ -165,70 +178,89 @@ class VST3Instance : public AudioPluginInstance { ScopedNoDenormals noDenormals; - Vst::ProcessData data; - data.processMode = Vst::kRealtime; - data.symbolicSampleSize = Vst::kSample32; - data.numSamples = audioBuffer.getNumSamples(); + if (isUsingDoublePrecision()) + { + doublePrecisionBuffer.makeCopyOf (audioBuffer, true); + processBlock (doublePrecisionBuffer, midiBuffer); + + const int numChannels = jmin (audioBuffer.getNumChannels(), doublePrecisionBuffer.getNumChannels()); + const int numSamples = jmin (audioBuffer.getNumSamples(), doublePrecisionBuffer.getNumSamples()); + + for (int channel = 0; channel < numChannels; ++channel) + { + auto* destination = audioBuffer.getWritePointer (channel); + const auto* source = doublePrecisionBuffer.getReadPointer (channel); + + for (int sample = 0; sample < numSamples; ++sample) + destination[sample] = static_cast (source[sample]); + } + + return; + } + + Vst::ProcessData data {}; + prepareProcessData (data, audioBuffer.getNumSamples(), Vst::kSample32); // Input busses - Vst::AudioBusBuffers inputBus; + Vst::AudioBusBuffers inputBus {}; inputBus.numChannels = audioBuffer.getNumChannels(); inputBus.channelBuffers32 = const_cast (audioBuffer.getArrayOfReadPointers()); data.inputs = &inputBus; data.numInputs = 1; // Output busses - Vst::AudioBusBuffers outputBus; + Vst::AudioBusBuffers outputBus {}; outputBus.numChannels = audioBuffer.getNumChannels(); outputBus.channelBuffers32 = const_cast (audioBuffer.getArrayOfWritePointers()); data.outputs = &outputBus; data.numOutputs = 1; - // Events (MIDI) + ignoreUnused (midiBuffer); + // TODO: map MidiBuffer to Vst::EventList when MIDI support is added in a later task - inputParameterChanges.clearQueue(); - const auto params = getParameters(); - const auto numParams = yup::jmin (params.size(), vst3ParameterIds.size()); + vst3Processor->process (data); + } - for (std::size_t i = 0; i < numParams; ++i) + void processBlock (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) override + { + ScopedNoDenormals noDenormals; + + if (! isUsingDoublePrecision()) { - int32 queueIndex = 0; - if (auto* queue = inputParameterChanges.addParameterData (vst3ParameterIds[i], queueIndex)) - { - int32 pointIndex = 0; - queue->addPoint (0, - static_cast (params[i]->getValue()), - pointIndex); - } + jassertfalse; + audioBuffer.clear(); + midiBuffer.clear(); + return; } - data.inputParameterChanges = &inputParameterChanges; + Vst::ProcessData data {}; + prepareProcessData (data, audioBuffer.getNumSamples(), Vst::kSample64); - // Process context - if (hostContext.playHead != nullptr) - { - const auto optPos = hostContext.playHead->getPosition(); - - if (optPos.has_value()) - { - const auto& posInfo = optPos.value(); - vst3ProcessContext.state = Vst::ProcessContext::kPlaying; - vst3ProcessContext.sampleRate = getSampleRate(); + Vst::AudioBusBuffers inputBus {}; + inputBus.numChannels = audioBuffer.getNumChannels(); + inputBus.channelBuffers64 = const_cast (audioBuffer.getArrayOfReadPointers()); + data.inputs = &inputBus; + data.numInputs = 1; - if (auto timeSamples = posInfo.getTimeInSamples()) - vst3ProcessContext.projectTimeSamples = *timeSamples; + Vst::AudioBusBuffers outputBus {}; + outputBus.numChannels = audioBuffer.getNumChannels(); + outputBus.channelBuffers64 = const_cast (audioBuffer.getArrayOfWritePointers()); + data.outputs = &outputBus; + data.numOutputs = 1; - if (auto tempo = posInfo.getBpm()) - vst3ProcessContext.tempo = *tempo; + ignoreUnused (midiBuffer); - data.processContext = &vst3ProcessContext; - } - } + // TODO: map MidiBuffer to Vst::EventList when MIDI support is added in a later task vst3Processor->process (data); } + bool supportsDoublePrecisionProcessing() const override + { + return vst3Processor != nullptr && vst3Processor->canProcessSampleSize (Vst::kSample64) == kResultTrue; + } + //============================================================================== int getCurrentPreset() const noexcept override { return currentPreset; } @@ -320,7 +352,13 @@ class VST3Instance : public AudioPluginInstance if (factory->getClassInfo (i, &classInfo) != kResultOk) continue; - if (String (classInfo.name) != desc.name && classInfo.category != String ("Audio Module Class")) + if (String (classInfo.category) != "Audio Module Class") + continue; + + if (desc.identifier.isNotEmpty() && ! classIDToString (classInfo.cid).equalsIgnoreCase (desc.identifier)) + continue; + + if (desc.identifier.isEmpty() && String (classInfo.name) != desc.name) continue; Vst::IComponent* rawComponent = nullptr; @@ -395,7 +433,7 @@ class VST3Instance : public AudioPluginInstance } auto param = AudioParameterBuilder() - .withID (toString (info.title)) + .withID (String (static_cast (info.id))) .withName (toString (info.title)) .withRange (0.0f, 1.0f) .withDefault (static_cast (info.defaultNormalizedValue)) @@ -415,9 +453,55 @@ class VST3Instance : public AudioPluginInstance IPtr vst3Controller; Vst::ProcessContext vst3ProcessContext {}; Vst::ParameterChanges inputParameterChanges; + AudioBuffer doublePrecisionBuffer; std::vector vst3ParameterIds; int currentPreset = 0; int numPresets = 0; + + void prepareProcessData (Vst::ProcessData& data, int numSamples, int32 symbolicSampleSize) + { + data.processMode = Vst::kRealtime; + data.symbolicSampleSize = symbolicSampleSize; + data.numSamples = numSamples; + + inputParameterChanges.clearQueue(); + const auto params = getParameters(); + const auto numParams = yup::jmin (params.size(), vst3ParameterIds.size()); + + for (std::size_t i = 0; i < numParams; ++i) + { + int32 queueIndex = 0; + if (auto* queue = inputParameterChanges.addParameterData (vst3ParameterIds[i], queueIndex)) + { + int32 pointIndex = 0; + queue->addPoint (0, + static_cast (params[i]->getValue()), + pointIndex); + } + } + + data.inputParameterChanges = &inputParameterChanges; + + if (hostContext.playHead == nullptr) + return; + + const auto optPos = hostContext.playHead->getPosition(); + if (! optPos.has_value()) + return; + + const auto& posInfo = optPos.value(); + vst3ProcessContext = {}; + vst3ProcessContext.state = Vst::ProcessContext::kPlaying; + vst3ProcessContext.sampleRate = getSampleRate(); + + if (auto timeSamples = posInfo.getTimeInSamples()) + vst3ProcessContext.projectTimeSamples = *timeSamples; + + if (auto tempo = posInfo.getBpm()) + vst3ProcessContext.tempo = *tempo; + + data.processContext = &vst3ProcessContext; + } }; //============================================================================== @@ -515,10 +599,7 @@ ResultValue> VST3Format::scanFile (const Fil desc.isInstrument = String (info2.subCategories).containsIgnoreCase ("Instrument"); desc.isEffect = ! desc.isInstrument; - // Encode FUID as hex string for use as identifier - char uidStr[33] = {}; - snprintf (uidStr, sizeof (uidStr), "%08X%08X%08X%08X", info2.cid[0], info2.cid[1], info2.cid[2], info2.cid[3]); - desc.identifier = String (uidStr); + desc.identifier = classIDToString (info2.cid); // Briefly instantiate the component to collect channel counts Vst::IComponent* rawComponent = nullptr; diff --git a/modules/yup_audio_processors/processors/yup_AudioProcessor.cpp b/modules/yup_audio_processors/processors/yup_AudioProcessor.cpp index c5fbfc27b..05ff6d264 100644 --- a/modules/yup_audio_processors/processors/yup_AudioProcessor.cpp +++ b/modules/yup_audio_processors/processors/yup_AudioProcessor.cpp @@ -86,6 +86,20 @@ bool AudioProcessor::isSuspended() const //============================================================================== +void AudioProcessor::setProcessingPrecision (ProcessingPrecision precision) +{ + if (precision == ProcessingPrecision::doublePrecision && ! supportsDoublePrecisionProcessing()) + { + jassertfalse; + processingPrecision = ProcessingPrecision::singlePrecision; + return; + } + + processingPrecision = precision; +} + +//============================================================================== + void AudioProcessor::setPlaybackConfiguration (float sampleRate, int samplesPerBlock) { releaseResources(); diff --git a/modules/yup_audio_processors/processors/yup_AudioProcessor.h b/modules/yup_audio_processors/processors/yup_AudioProcessor.h index b08a80103..84de8329f 100644 --- a/modules/yup_audio_processors/processors/yup_AudioProcessor.h +++ b/modules/yup_audio_processors/processors/yup_AudioProcessor.h @@ -33,6 +33,14 @@ class AudioProcessorEditor; class YUP_API AudioProcessor { public: + //============================================================================== + /** The floating-point precision used for processBlock() calls. */ + enum class ProcessingPrecision + { + singlePrecision, + doublePrecision + }; + //============================================================================== /** Constructs an AudioProcessor. */ @@ -98,6 +106,20 @@ class YUP_API AudioProcessor //============================================================================== + /** Returns true if this processor implements the double-precision processBlock(). */ + virtual bool supportsDoublePrecisionProcessing() const { return false; } + + /** Sets the preferred processing precision for future processBlock() calls. */ + void setProcessingPrecision (ProcessingPrecision precision); + + /** Returns the current processing precision. */ + ProcessingPrecision getProcessingPrecision() const noexcept { return processingPrecision; } + + /** Returns true when the current processing precision is double precision. */ + bool isUsingDoublePrecision() const noexcept { return processingPrecision == ProcessingPrecision::doublePrecision; } + + //============================================================================== + CriticalSection& getProcessLock() { return processLock; } bool isSuspended() const; @@ -190,6 +212,7 @@ class YUP_API AudioProcessor float sampleRate = 44100.0f; int samplesPerBlock = 1024; + ProcessingPrecision processingPrecision = ProcessingPrecision::singlePrecision; AudioPlayHead* playHead = nullptr; diff --git a/modules/yup_audio_processors/yup_audio_processors.cpp b/modules/yup_audio_processors/yup_audio_processors.cpp index 831317711..e6a82126e 100644 --- a/modules/yup_audio_processors/yup_audio_processors.cpp +++ b/modules/yup_audio_processors/yup_audio_processors.cpp @@ -1,40 +1,40 @@ -/* - ============================================================================== - - This file is part of the YUP library. - Copyright (c) 2024 - kunitoki@gmail.com - - YUP is an open source library subject to open-source licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - to use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== -*/ - -#ifdef YUP_AUDIO_PROCESSORS_H_INCLUDED -/* When you add this cpp file to your project, you mustn't include it in a file where you've - already included any other headers - just put it inside a file on its own, possibly with your config - flags preceding it, but don't include anything else. That also includes avoiding any automatic prefix - header files that the compiler may be using. -*/ -#error "Incorrect use of YUP cpp file" -#endif - -#include "yup_audio_processors.h" - -//============================================================================== -#include "processors/yup_AudioParameter.cpp" -#include "processors/yup_AudioParameterBuilder.cpp" -#include "processors/yup_AudioProcessor.cpp" - -#if YUP_MODULE_AVAILABLE_yup_gui -#include "processors/yup_AudioProcessorEditor.cpp" -#endif +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#ifdef YUP_AUDIO_PROCESSORS_H_INCLUDED +/* When you add this cpp file to your project, you mustn't include it in a file where you've + already included any other headers - just put it inside a file on its own, possibly with your config + flags preceding it, but don't include anything else. That also includes avoiding any automatic prefix + header files that the compiler may be using. +*/ +#error "Incorrect use of YUP cpp file" +#endif + +#include "yup_audio_processors.h" + +//============================================================================== +#include "processors/yup_AudioParameter.cpp" +#include "processors/yup_AudioParameterBuilder.cpp" +#include "processors/yup_AudioProcessor.cpp" + +#if YUP_MODULE_AVAILABLE_yup_gui +#include "processors/yup_AudioProcessorEditor.cpp" +#endif diff --git a/modules/yup_audio_processors/yup_audio_processors.h b/modules/yup_audio_processors/yup_audio_processors.h index fc1bc4872..eca653ff6 100644 --- a/modules/yup_audio_processors/yup_audio_processors.h +++ b/modules/yup_audio_processors/yup_audio_processors.h @@ -1,61 +1,61 @@ -/* - ============================================================================== - - This file is part of the YUP library. - Copyright (c) 2024 - kunitoki@gmail.com - - YUP is an open source library subject to open-source licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - to use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== -*/ - -/* - ============================================================================== - - BEGIN_YUP_MODULE_DECLARATION - - ID: yup_audio_processors - vendor: yup - version: 1.0.0 - name: YUP Audio Processors - description: The essential set of basic YUP audio processing classes. - website: https://github.com/kunitoki/yup - license: ISC - - dependencies: yup_audio_basics - - END_YUP_MODULE_DECLARATION - - ============================================================================== -*/ - -#pragma once -#define YUP_AUDIO_PROCESSORS_H_INCLUDED - -#include - -#if YUP_MODULE_AVAILABLE_yup_gui -#include -#endif - -//============================================================================== -#include "processors/yup_AudioBus.h" -#include "processors/yup_AudioBusLayout.h" -#include "processors/yup_AudioParameter.h" -#include "processors/yup_AudioParameterBuilder.h" -#include "processors/yup_AudioParameterHandle.h" -#include "processors/yup_AudioProcessor.h" - -#if YUP_MODULE_AVAILABLE_yup_gui -#include "processors/yup_AudioProcessorEditor.h" -#endif +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +/* + ============================================================================== + + BEGIN_YUP_MODULE_DECLARATION + + ID: yup_audio_processors + vendor: yup + version: 1.0.0 + name: YUP Audio Processors + description: The essential set of basic YUP audio processing classes. + website: https://github.com/kunitoki/yup + license: ISC + + dependencies: yup_audio_basics + + END_YUP_MODULE_DECLARATION + + ============================================================================== +*/ + +#pragma once +#define YUP_AUDIO_PROCESSORS_H_INCLUDED + +#include + +#if YUP_MODULE_AVAILABLE_yup_gui +#include +#endif + +//============================================================================== +#include "processors/yup_AudioBus.h" +#include "processors/yup_AudioBusLayout.h" +#include "processors/yup_AudioParameter.h" +#include "processors/yup_AudioParameterBuilder.h" +#include "processors/yup_AudioParameterHandle.h" +#include "processors/yup_AudioProcessor.h" + +#if YUP_MODULE_AVAILABLE_yup_gui +#include "processors/yup_AudioProcessorEditor.h" +#endif diff --git a/modules/yup_core/memory/yup_MemoryBlock.cpp b/modules/yup_core/memory/yup_MemoryBlock.cpp index 1707c4f1d..a1957429c 100644 --- a/modules/yup_core/memory/yup_MemoryBlock.cpp +++ b/modules/yup_core/memory/yup_MemoryBlock.cpp @@ -1,444 +1,444 @@ -/* - ============================================================================== - - This file is part of the YUP library. - Copyright (c) 2024 - kunitoki@gmail.com - - YUP is an open source library subject to open-source licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - to use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== - - This file is part of the JUCE library. - Copyright (c) 2022 - Raw Material Software Limited - - JUCE is an open source library subject to commercial or open-source - licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - To use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== -*/ - -namespace yup -{ - -MemoryBlock::MemoryBlock() noexcept {} - -MemoryBlock::MemoryBlock (size_t initialSize, bool initialiseToZero) -{ - if (initialSize > 0) - { - size = initialSize; - data.allocate (initialSize, initialiseToZero); - } - else - { - size = 0; - } -} - -MemoryBlock::MemoryBlock (const MemoryBlock& other) - : size (other.size) -{ - if (size > 0) - { - jassert (other.data != nullptr); - data.malloc (size); - memcpy (data, other.data, size); - } -} - -MemoryBlock::MemoryBlock (const void* const dataToInitialiseFrom, const size_t sizeInBytes) - : size (sizeInBytes) -{ - jassert (((ssize_t) sizeInBytes) >= 0); - - if (size > 0) - { - jassert (dataToInitialiseFrom != nullptr); // non-zero size, but a zero pointer passed-in? - - data.malloc (size); - - if (dataToInitialiseFrom != nullptr) - memcpy (data, dataToInitialiseFrom, size); - } -} - -MemoryBlock::~MemoryBlock() noexcept -{ -} - -MemoryBlock& MemoryBlock::operator= (const MemoryBlock& other) -{ - if (this != &other) - { - setSize (other.size, false); - memcpy (data, other.data, size); - } - - return *this; -} - -MemoryBlock::MemoryBlock (MemoryBlock&& other) noexcept - : data (std::move (other.data)) - , size (other.size) -{ -} - -MemoryBlock& MemoryBlock::operator= (MemoryBlock&& other) noexcept -{ - data = std::move (other.data); - size = other.size; - return *this; -} - -//============================================================================== -bool MemoryBlock::operator== (const MemoryBlock& other) const noexcept -{ - return matches (other.data, other.size); -} - -bool MemoryBlock::operator!= (const MemoryBlock& other) const noexcept -{ - return ! operator== (other); -} - -bool MemoryBlock::matches (const void* dataToCompare, size_t dataSize) const noexcept -{ - return size == dataSize - && memcmp (data, dataToCompare, size) == 0; -} - -//============================================================================== -// this will resize the block to this size -void MemoryBlock::setSize (const size_t newSize, const bool initialiseToZero) -{ - if (size != newSize) - { - if (newSize <= 0) - { - reset(); - } - else - { - if (data != nullptr) - { - data.realloc (newSize); - - if (initialiseToZero && (newSize > size)) - zeromem (data + size, newSize - size); - } - else - { - data.allocate (newSize, initialiseToZero); - } - - size = newSize; - } - } -} - -void MemoryBlock::reset() -{ - data.free(); - size = 0; -} - -void MemoryBlock::ensureSize (size_t minimumSize, bool initialiseToZero) -{ - if (size < minimumSize) - setSize (minimumSize, initialiseToZero); -} - -void MemoryBlock::swapWith (MemoryBlock& other) noexcept -{ - std::swap (size, other.size); - data.swapWith (other.data); -} - -//============================================================================== -void MemoryBlock::fillWith (uint8 value) noexcept -{ - memset (data, (int) value, size); -} - -void MemoryBlock::append (const void* srcData, size_t numBytes) -{ - if (numBytes > 0) - { - jassert (srcData != nullptr); // this must not be null! - auto oldSize = size; - setSize (size + numBytes); - memcpy (data + oldSize, srcData, numBytes); - } -} - -void MemoryBlock::replaceAll (const void* srcData, size_t numBytes) -{ - if (numBytes <= 0) - { - reset(); - return; - } - - jassert (srcData != nullptr); // this must not be null! - setSize (numBytes); - memcpy (data, srcData, numBytes); -} - -void MemoryBlock::insert (const void* srcData, size_t numBytes, size_t insertPosition) -{ - if (numBytes > 0) - { - jassert (srcData != nullptr); // this must not be null! - insertPosition = jmin (size, insertPosition); - auto trailingDataSize = size - insertPosition; - setSize (size + numBytes, false); - - if (trailingDataSize > 0) - memmove (data + insertPosition + numBytes, - data + insertPosition, - trailingDataSize); - - memcpy (data + insertPosition, srcData, numBytes); - } -} - -void MemoryBlock::removeSection (size_t startByte, size_t numBytesToRemove) -{ - if (startByte + numBytesToRemove >= size) - { - setSize (startByte); - } - else if (numBytesToRemove > 0) - { - memmove (data + startByte, - data + startByte + numBytesToRemove, - size - (startByte + numBytesToRemove)); - - setSize (size - numBytesToRemove); - } -} - -void MemoryBlock::copyFrom (const void* const src, int offset, size_t num) noexcept -{ - auto* d = static_cast (src); - - if (offset < 0) - { - d -= offset; - num += (size_t) -offset; - offset = 0; - } - - if ((size_t) offset + num > size) - num = size - (size_t) offset; - - if (num > 0) - memcpy (data + offset, d, num); -} - -void MemoryBlock::copyTo (void* const dst, int offset, size_t num) const noexcept -{ - auto* d = static_cast (dst); - - if (offset < 0) - { - zeromem (d, (size_t) -offset); - d -= offset; - num -= (size_t) -offset; - offset = 0; - } - - if ((size_t) offset + num > size) - { - auto newNum = (size_t) size - (size_t) offset; - zeromem (d + newNum, num - newNum); - num = newNum; - } - - if (num > 0) - memcpy (d, data + offset, num); -} - -String MemoryBlock::toString() const -{ - return String::fromUTF8 (data, (int) size); -} - -//============================================================================== -int MemoryBlock::getBitRange (size_t bitRangeStart, size_t numBits) const noexcept -{ - int res = 0; - - auto byte = bitRangeStart >> 3; - auto offsetInByte = bitRangeStart & 7; - size_t bitsSoFar = 0; - - while (numBits > 0 && (size_t) byte < size) - { - auto bitsThisTime = jmin (numBits, 8 - offsetInByte); - const int mask = (0xff >> (8 - bitsThisTime)) << offsetInByte; - - res |= (((data[byte] & mask) >> offsetInByte) << bitsSoFar); - - bitsSoFar += bitsThisTime; - numBits -= bitsThisTime; - ++byte; - offsetInByte = 0; - } - - return res; -} - -void MemoryBlock::setBitRange (const size_t bitRangeStart, size_t numBits, int bitsToSet) noexcept -{ - auto byte = bitRangeStart >> 3; - auto offsetInByte = bitRangeStart & 7; - uint32 mask = ~((((uint32) 0xffffffff) << (32 - numBits)) >> (32 - numBits)); - - while (numBits > 0 && (size_t) byte < size) - { - auto bitsThisTime = jmin (numBits, 8 - offsetInByte); - - const uint32 tempMask = (mask << offsetInByte) | ~((((uint32) 0xffffffff) >> offsetInByte) << offsetInByte); - const uint32 tempBits = (uint32) bitsToSet << offsetInByte; - - data[byte] = (char) (((uint32) data[byte] & tempMask) | tempBits); - - ++byte; - numBits -= bitsThisTime; - bitsToSet >>= bitsThisTime; - mask >>= bitsThisTime; - offsetInByte = 0; - } -} - -//============================================================================== -void MemoryBlock::loadFromHexString (StringRef hex) -{ - ensureSize ((size_t) hex.length() >> 1); - char* dest = data; - auto t = hex.text; - - for (;;) - { - yup_wchar byte = 0; - - for (int loop = 2; --loop >= 0;) - { - byte <<= 4; - - for (;;) - { - auto c = t.getAndAdvance(); - - if (c >= '0' && c <= '9') - { - byte |= c - '0'; - break; - } - if (c >= 'a' && c <= 'z') - { - byte |= c - ('a' - 10); - break; - } - if (c >= 'A' && c <= 'Z') - { - byte |= c - ('A' - 10); - break; - } - - if (c == 0) - { - setSize (static_cast (dest - data)); - return; - } - } - } - - *dest++ = (char) byte; - } -} - -//============================================================================== -static const char base64EncodingTable[] = ".ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+"; - -String MemoryBlock::toBase64Encoding() const -{ - auto numChars = ((size << 3) + 5) / 6; - - String destString ((unsigned int) size); // store the length, followed by a '.', and then the data. - auto initialLen = destString.length(); - destString.preallocateBytes ((size_t) initialLen * sizeof (String::CharPointerType::CharType) + 2 + numChars); - - auto d = destString.getCharPointer(); - d += initialLen; - d.write ('.'); - - for (size_t i = 0; i < numChars; ++i) - d.write ((yup_wchar) (uint8) base64EncodingTable[getBitRange (i * 6, 6)]); - - d.writeNull(); - return destString; -} - -// clang-format off -static const char base64DecodingTable[] = { - 63, 0, 0, 0, 0, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, - 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 0, 0, 0, 0, 0, 0, - 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, - 51, 52 -}; -// clang-format on - -bool MemoryBlock::fromBase64Encoding (StringRef s) -{ - auto dot = CharacterFunctions::find (s.text, (yup_wchar) '.'); - - if (dot.isEmpty()) - return false; - - auto numBytesNeeded = String (s.text, dot).getIntValue(); - - setSize ((size_t) numBytesNeeded, true); - - auto srcChars = dot + 1; - std::size_t pos = 0; - - for (;;) - { - auto c = (int) srcChars.getAndAdvance(); - - if (c == 0) - return true; - - c -= 43; - - if (isPositiveAndBelow (c, numElementsInArray (base64DecodingTable))) - { - setBitRange ((size_t) pos, 6, base64DecodingTable[c]); - pos += 6; - } - } -} - -} // namespace yup +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +MemoryBlock::MemoryBlock() noexcept {} + +MemoryBlock::MemoryBlock (size_t initialSize, bool initialiseToZero) +{ + if (initialSize > 0) + { + size = initialSize; + data.allocate (initialSize, initialiseToZero); + } + else + { + size = 0; + } +} + +MemoryBlock::MemoryBlock (const MemoryBlock& other) + : size (other.size) +{ + if (size > 0) + { + jassert (other.data != nullptr); + data.malloc (size); + memcpy (data, other.data, size); + } +} + +MemoryBlock::MemoryBlock (const void* const dataToInitialiseFrom, const size_t sizeInBytes) + : size (sizeInBytes) +{ + jassert (((ssize_t) sizeInBytes) >= 0); + + if (size > 0) + { + jassert (dataToInitialiseFrom != nullptr); // non-zero size, but a zero pointer passed-in? + + data.malloc (size); + + if (dataToInitialiseFrom != nullptr) + memcpy (data, dataToInitialiseFrom, size); + } +} + +MemoryBlock::~MemoryBlock() noexcept +{ +} + +MemoryBlock& MemoryBlock::operator= (const MemoryBlock& other) +{ + if (this != &other) + { + setSize (other.size, false); + memcpy (data, other.data, size); + } + + return *this; +} + +MemoryBlock::MemoryBlock (MemoryBlock&& other) noexcept + : data (std::move (other.data)) + , size (other.size) +{ +} + +MemoryBlock& MemoryBlock::operator= (MemoryBlock&& other) noexcept +{ + data = std::move (other.data); + size = other.size; + return *this; +} + +//============================================================================== +bool MemoryBlock::operator== (const MemoryBlock& other) const noexcept +{ + return matches (other.data, other.size); +} + +bool MemoryBlock::operator!= (const MemoryBlock& other) const noexcept +{ + return ! operator== (other); +} + +bool MemoryBlock::matches (const void* dataToCompare, size_t dataSize) const noexcept +{ + return size == dataSize + && memcmp (data, dataToCompare, size) == 0; +} + +//============================================================================== +// this will resize the block to this size +void MemoryBlock::setSize (const size_t newSize, const bool initialiseToZero) +{ + if (size != newSize) + { + if (newSize <= 0) + { + reset(); + } + else + { + if (data != nullptr) + { + data.realloc (newSize); + + if (initialiseToZero && (newSize > size)) + zeromem (data + size, newSize - size); + } + else + { + data.allocate (newSize, initialiseToZero); + } + + size = newSize; + } + } +} + +void MemoryBlock::reset() +{ + data.free(); + size = 0; +} + +void MemoryBlock::ensureSize (size_t minimumSize, bool initialiseToZero) +{ + if (size < minimumSize) + setSize (minimumSize, initialiseToZero); +} + +void MemoryBlock::swapWith (MemoryBlock& other) noexcept +{ + std::swap (size, other.size); + data.swapWith (other.data); +} + +//============================================================================== +void MemoryBlock::fillWith (uint8 value) noexcept +{ + memset (data, (int) value, size); +} + +void MemoryBlock::append (const void* srcData, size_t numBytes) +{ + if (numBytes > 0) + { + jassert (srcData != nullptr); // this must not be null! + auto oldSize = size; + setSize (size + numBytes); + memcpy (data + oldSize, srcData, numBytes); + } +} + +void MemoryBlock::replaceAll (const void* srcData, size_t numBytes) +{ + if (numBytes <= 0) + { + reset(); + return; + } + + jassert (srcData != nullptr); // this must not be null! + setSize (numBytes); + memcpy (data, srcData, numBytes); +} + +void MemoryBlock::insert (const void* srcData, size_t numBytes, size_t insertPosition) +{ + if (numBytes > 0) + { + jassert (srcData != nullptr); // this must not be null! + insertPosition = jmin (size, insertPosition); + auto trailingDataSize = size - insertPosition; + setSize (size + numBytes, false); + + if (trailingDataSize > 0) + memmove (data + insertPosition + numBytes, + data + insertPosition, + trailingDataSize); + + memcpy (data + insertPosition, srcData, numBytes); + } +} + +void MemoryBlock::removeSection (size_t startByte, size_t numBytesToRemove) +{ + if (startByte + numBytesToRemove >= size) + { + setSize (startByte); + } + else if (numBytesToRemove > 0) + { + memmove (data + startByte, + data + startByte + numBytesToRemove, + size - (startByte + numBytesToRemove)); + + setSize (size - numBytesToRemove); + } +} + +void MemoryBlock::copyFrom (const void* const src, int offset, size_t num) noexcept +{ + auto* d = static_cast (src); + + if (offset < 0) + { + d -= offset; + num += (size_t) -offset; + offset = 0; + } + + if ((size_t) offset + num > size) + num = size - (size_t) offset; + + if (num > 0) + memcpy (data + offset, d, num); +} + +void MemoryBlock::copyTo (void* const dst, int offset, size_t num) const noexcept +{ + auto* d = static_cast (dst); + + if (offset < 0) + { + zeromem (d, (size_t) -offset); + d -= offset; + num -= (size_t) -offset; + offset = 0; + } + + if ((size_t) offset + num > size) + { + auto newNum = (size_t) size - (size_t) offset; + zeromem (d + newNum, num - newNum); + num = newNum; + } + + if (num > 0) + memcpy (d, data + offset, num); +} + +String MemoryBlock::toString() const +{ + return String::fromUTF8 (data, (int) size); +} + +//============================================================================== +int MemoryBlock::getBitRange (size_t bitRangeStart, size_t numBits) const noexcept +{ + int res = 0; + + auto byte = bitRangeStart >> 3; + auto offsetInByte = bitRangeStart & 7; + size_t bitsSoFar = 0; + + while (numBits > 0 && (size_t) byte < size) + { + auto bitsThisTime = jmin (numBits, 8 - offsetInByte); + const int mask = (0xff >> (8 - bitsThisTime)) << offsetInByte; + + res |= (((data[byte] & mask) >> offsetInByte) << bitsSoFar); + + bitsSoFar += bitsThisTime; + numBits -= bitsThisTime; + ++byte; + offsetInByte = 0; + } + + return res; +} + +void MemoryBlock::setBitRange (const size_t bitRangeStart, size_t numBits, int bitsToSet) noexcept +{ + auto byte = bitRangeStart >> 3; + auto offsetInByte = bitRangeStart & 7; + uint32 mask = ~((((uint32) 0xffffffff) << (32 - numBits)) >> (32 - numBits)); + + while (numBits > 0 && (size_t) byte < size) + { + auto bitsThisTime = jmin (numBits, 8 - offsetInByte); + + const uint32 tempMask = (mask << offsetInByte) | ~((((uint32) 0xffffffff) >> offsetInByte) << offsetInByte); + const uint32 tempBits = (uint32) bitsToSet << offsetInByte; + + data[byte] = (char) (((uint32) data[byte] & tempMask) | tempBits); + + ++byte; + numBits -= bitsThisTime; + bitsToSet >>= bitsThisTime; + mask >>= bitsThisTime; + offsetInByte = 0; + } +} + +//============================================================================== +void MemoryBlock::loadFromHexString (StringRef hex) +{ + ensureSize ((size_t) hex.length() >> 1); + char* dest = data; + auto t = hex.text; + + for (;;) + { + yup_wchar byte = 0; + + for (int loop = 2; --loop >= 0;) + { + byte <<= 4; + + for (;;) + { + auto c = t.getAndAdvance(); + + if (c >= '0' && c <= '9') + { + byte |= c - '0'; + break; + } + if (c >= 'a' && c <= 'z') + { + byte |= c - ('a' - 10); + break; + } + if (c >= 'A' && c <= 'Z') + { + byte |= c - ('A' - 10); + break; + } + + if (c == 0) + { + setSize (static_cast (dest - data)); + return; + } + } + } + + *dest++ = (char) byte; + } +} + +//============================================================================== +static const char base64EncodingTable[] = ".ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+"; + +String MemoryBlock::toBase64Encoding() const +{ + auto numChars = ((size << 3) + 5) / 6; + + String destString ((unsigned int) size); // store the length, followed by a '.', and then the data. + auto initialLen = destString.length(); + destString.preallocateBytes ((size_t) initialLen * sizeof (String::CharPointerType::CharType) + 2 + numChars); + + auto d = destString.getCharPointer(); + d += initialLen; + d.write ('.'); + + for (size_t i = 0; i < numChars; ++i) + d.write ((yup_wchar) (uint8) base64EncodingTable[getBitRange (i * 6, 6)]); + + d.writeNull(); + return destString; +} + +// clang-format off +static const char base64DecodingTable[] = { + 63, 0, 0, 0, 0, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 0, 0, 0, 0, 0, 0, + 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, + 51, 52 +}; +// clang-format on + +bool MemoryBlock::fromBase64Encoding (StringRef s) +{ + auto dot = CharacterFunctions::find (s.text, (yup_wchar) '.'); + + if (dot.isEmpty()) + return false; + + auto numBytesNeeded = String (s.text, dot).getIntValue(); + + setSize ((size_t) numBytesNeeded, true); + + auto srcChars = dot + 1; + std::size_t pos = 0; + + for (;;) + { + auto c = (int) srcChars.getAndAdvance(); + + if (c == 0) + return true; + + c -= 43; + + if (isPositiveAndBelow (c, numElementsInArray (base64DecodingTable))) + { + setBitRange ((size_t) pos, 6, base64DecodingTable[c]); + pos += 6; + } + } +} + +} // namespace yup diff --git a/modules/yup_core/misc/yup_ResultValue.h b/modules/yup_core/misc/yup_ResultValue.h index 18b45d7c5..c75796108 100644 --- a/modules/yup_core/misc/yup_ResultValue.h +++ b/modules/yup_core/misc/yup_ResultValue.h @@ -1,197 +1,197 @@ -/* - ============================================================================== - - This file is part of the YUP library. - Copyright (c) 2024 - kunitoki@gmail.com - - YUP is an open source library subject to open-source licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - to use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== -*/ - -namespace yup -{ - -//============================================================================== -/** - Represents the 'success' or 'failure' of an operation that returns a value, and holds an associated - error message to describe the error when there's a failure. - - E.g. - @code - ResultValue myOperation() - { - if (doSomeKindOfFoobar()) - return ResultValue::ok (1337); - else - return ResultValue::fail ("foobar didn't work!"); - } - - const ResultValue result (myOperation()); - - if (result.wasOk()) - { - const int& resultInteger = result.getReference(); - - ...it's all good, use the value... - } - else - { - warnUserAboutFailure ("The foobar operation failed! Error message was: " - + result.getErrorMessage()); - } - @endcode - - @tags{Core} -*/ -template -class YUP_API ResultValue -{ -public: - //============================================================================== - /** Creates and returns a 'successful' result value. */ - template - static auto ok (U&& value) noexcept - -> std::enable_if_t, ResultValue> - { - return ResultValue (std::forward (value)); - } - - /** Creates a 'failure' result. - If you pass a blank error message in here, a default "Unknown Error" message will be used instead. - */ - static ResultValue fail (StringRef errorMessage) noexcept - { - return ResultValue (errorMessage.isEmpty() ? StringRef ("Unknown Error") : errorMessage, ErrorTag {}); - } - - //============================================================================== - /** Returns true if this result indicates a success. */ - bool wasOk() const noexcept - { - return valueOrErrorMessage.index() == 1; - } - - /** Returns true if this result indicates a failure. - You can use getErrorMessage() to retrieve the error message associated - with the failure. - */ - bool failed() const noexcept - { - return valueOrErrorMessage.index() != 1; - } - - /** Returns true if this result indicates a success. - This is equivalent to calling wasOk(). - */ - explicit operator bool() const noexcept - { - return valueOrErrorMessage.index() == 1; - } - - /** Returns true if this result indicates a failure. - This is equivalent to calling failed(). - */ - bool operator!() const noexcept - { - return valueOrErrorMessage.index() != 1; - } - - /** Returns a copy of the value that was set when this result was created. */ - T getValue() const& - requires std::copy_constructible - { - jassert (valueOrErrorMessage.index() == 1); // Trying to access the value of the result, when the result is holding an error instead! - - return std::get<1> (valueOrErrorMessage); - } - - /** Returns a moved from value that was set when this result was created. */ - T getValue() && - requires std::move_constructible - { - jassert (valueOrErrorMessage.index() == 1); // Trying to access the value of the result, when the result is holding an error instead! - - return std::get<1> (std::move (valueOrErrorMessage)); - } - - /** Returns the mutable reference that was set when this result was created. */ - T& getReference() noexcept - { - jassert (valueOrErrorMessage.index() == 1); // Trying to access the value of the result, when the result is holding an error instead! - - return std::get<1> (valueOrErrorMessage); - } - - /** Returns the const reference that was set when this result was created. */ - const T& getReference() const noexcept - { - jassert (valueOrErrorMessage.index() == 1); // Trying to access the value of the result, when the result is holding an error instead! - - return std::get<1> (valueOrErrorMessage); - } - - /** Returns the error message that was set when this result was created. - For a successful result, this will be an empty string; - */ - const String& getErrorMessage() const noexcept - { - jassert (valueOrErrorMessage.index() == 2); // Trying to access the error message of the result, when the result is holding a value instead! - - return std::get<2> (valueOrErrorMessage); - } - - //============================================================================== - ResultValue (const ResultValue&) = default; - ResultValue& operator= (const ResultValue&) = default; - ResultValue (ResultValue&&) noexcept = default; - ResultValue& operator= (ResultValue&&) noexcept = default; - - bool operator== (const ResultValue& other) const noexcept - { - return valueOrErrorMessage == other.valueOrErrorMessage; - } - - bool operator!= (const ResultValue& other) const noexcept - { - return valueOrErrorMessage != other.valueOrErrorMessage; - } - -private: - std::variant valueOrErrorMessage; - - struct ErrorTag - { - }; - - // The default constructor is not for public use! - // Instead, use ResultValue::ok() or ResultValue::fail() - ResultValue() noexcept {} - - template - explicit ResultValue (U&& value) noexcept - : valueOrErrorMessage (std::in_place_index<1>, std::forward (value)) - { - } - - ResultValue (StringRef message, ErrorTag) noexcept - : valueOrErrorMessage (std::in_place_index<2>, message) - { - } - - // These casts are private to prevent people trying to use the ResultValue object in numeric contexts - operator int() const; - operator void*() const; -}; - -} // namespace yup +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +/** + Represents the 'success' or 'failure' of an operation that returns a value, and holds an associated + error message to describe the error when there's a failure. + + E.g. + @code + ResultValue myOperation() + { + if (doSomeKindOfFoobar()) + return ResultValue::ok (1337); + else + return ResultValue::fail ("foobar didn't work!"); + } + + const ResultValue result (myOperation()); + + if (result.wasOk()) + { + const int& resultInteger = result.getReference(); + + ...it's all good, use the value... + } + else + { + warnUserAboutFailure ("The foobar operation failed! Error message was: " + + result.getErrorMessage()); + } + @endcode + + @tags{Core} +*/ +template +class YUP_API ResultValue +{ +public: + //============================================================================== + /** Creates and returns a 'successful' result value. */ + template + static auto ok (U&& value) noexcept + -> std::enable_if_t, ResultValue> + { + return ResultValue (std::forward (value)); + } + + /** Creates a 'failure' result. + If you pass a blank error message in here, a default "Unknown Error" message will be used instead. + */ + static ResultValue fail (StringRef errorMessage) noexcept + { + return ResultValue (errorMessage.isEmpty() ? StringRef ("Unknown Error") : errorMessage, ErrorTag {}); + } + + //============================================================================== + /** Returns true if this result indicates a success. */ + bool wasOk() const noexcept + { + return valueOrErrorMessage.index() == 1; + } + + /** Returns true if this result indicates a failure. + You can use getErrorMessage() to retrieve the error message associated + with the failure. + */ + bool failed() const noexcept + { + return valueOrErrorMessage.index() != 1; + } + + /** Returns true if this result indicates a success. + This is equivalent to calling wasOk(). + */ + explicit operator bool() const noexcept + { + return valueOrErrorMessage.index() == 1; + } + + /** Returns true if this result indicates a failure. + This is equivalent to calling failed(). + */ + bool operator!() const noexcept + { + return valueOrErrorMessage.index() != 1; + } + + /** Returns a copy of the value that was set when this result was created. */ + T getValue() const& + requires std::copy_constructible + { + jassert (valueOrErrorMessage.index() == 1); // Trying to access the value of the result, when the result is holding an error instead! + + return std::get<1> (valueOrErrorMessage); + } + + /** Returns a moved from value that was set when this result was created. */ + T getValue() && + requires std::move_constructible + { + jassert (valueOrErrorMessage.index() == 1); // Trying to access the value of the result, when the result is holding an error instead! + + return std::get<1> (std::move (valueOrErrorMessage)); + } + + /** Returns the mutable reference that was set when this result was created. */ + T& getReference() noexcept + { + jassert (valueOrErrorMessage.index() == 1); // Trying to access the value of the result, when the result is holding an error instead! + + return std::get<1> (valueOrErrorMessage); + } + + /** Returns the const reference that was set when this result was created. */ + const T& getReference() const noexcept + { + jassert (valueOrErrorMessage.index() == 1); // Trying to access the value of the result, when the result is holding an error instead! + + return std::get<1> (valueOrErrorMessage); + } + + /** Returns the error message that was set when this result was created. + For a successful result, this will be an empty string; + */ + const String& getErrorMessage() const noexcept + { + jassert (valueOrErrorMessage.index() == 2); // Trying to access the error message of the result, when the result is holding a value instead! + + return std::get<2> (valueOrErrorMessage); + } + + //============================================================================== + ResultValue (const ResultValue&) = default; + ResultValue& operator= (const ResultValue&) = default; + ResultValue (ResultValue&&) noexcept = default; + ResultValue& operator= (ResultValue&&) noexcept = default; + + bool operator== (const ResultValue& other) const noexcept + { + return valueOrErrorMessage == other.valueOrErrorMessage; + } + + bool operator!= (const ResultValue& other) const noexcept + { + return valueOrErrorMessage != other.valueOrErrorMessage; + } + +private: + std::variant valueOrErrorMessage; + + struct ErrorTag + { + }; + + // The default constructor is not for public use! + // Instead, use ResultValue::ok() or ResultValue::fail() + ResultValue() noexcept {} + + template + explicit ResultValue (U&& value) noexcept + : valueOrErrorMessage (std::in_place_index<1>, std::forward (value)) + { + } + + ResultValue (StringRef message, ErrorTag) noexcept + : valueOrErrorMessage (std::in_place_index<2>, message) + { + } + + // These casts are private to prevent people trying to use the ResultValue object in numeric contexts + operator int() const; + operator void*() const; +}; + +} // namespace yup diff --git a/modules/yup_core/system/yup_CompilerSupport.h b/modules/yup_core/system/yup_CompilerSupport.h index 383369d77..03ce95ee4 100644 --- a/modules/yup_core/system/yup_CompilerSupport.h +++ b/modules/yup_core/system/yup_CompilerSupport.h @@ -1,118 +1,118 @@ -/* - ============================================================================== - - This file is part of the YUP library. - Copyright (c) 2024 - kunitoki@gmail.com - - YUP is an open source library subject to open-source licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - to use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== - - This file is part of the JUCE library. - Copyright (c) 2022 - Raw Material Software Limited - - JUCE is an open source library subject to commercial or open-source - licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - To use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== -*/ - -#pragma once - -/* - This file provides flags for compiler features that aren't supported on all platforms. -*/ - -//============================================================================== -// GCC -#if YUP_GCC - -#if (__GNUC__ * 100 + __GNUC_MINOR__) < 700 -#error "YUP requires GCC 7.0 or later" -#endif - -#ifndef YUP_EXCEPTIONS_DISABLED -#if ! __EXCEPTIONS -#define YUP_EXCEPTIONS_DISABLED 1 -#endif -#endif - -#define YUP_CXX20_IS_AVAILABLE (__cplusplus >= 202002L) -#define YUP_CXX23_IS_AVAILABLE (__cplusplus >= 202302L) - -#endif - -//============================================================================== -// Clang -#if YUP_CLANG - -#if (__clang_major__ < 6) -#error "YUP requires Clang 6 or later" -#endif - -#ifndef YUP_COMPILER_SUPPORTS_ARC -#define YUP_COMPILER_SUPPORTS_ARC 1 -#endif - -#ifndef YUP_EXCEPTIONS_DISABLED -#if ! __has_feature(cxx_exceptions) -#define YUP_EXCEPTIONS_DISABLED 1 -#endif -#endif - -#if ! defined(YUP_SILENCE_XCODE_15_LINKER_WARNING) \ - && defined(__apple_build_version__) \ - && __apple_build_version__ >= 15000000 \ - && __apple_build_version__ < 15000100 - - // Due to known issues, the linker in Xcode 15.0 may produce broken binaries. -#error Please upgrade to Xcode 15.1 or higher -#endif - -#define YUP_CXX20_IS_AVAILABLE (__cplusplus >= 202002L) -#define YUP_CXX23_IS_AVAILABLE (__cplusplus >= 202302L) - -#endif - -//============================================================================== -// MSVC -#if YUP_MSVC - -#if _MSC_FULL_VER < 191025017 // VS2017 -#error "YUP requires Visual Studio 2017 or later" -#endif - -#ifndef YUP_EXCEPTIONS_DISABLED -#if ! _CPPUNWIND -#define YUP_EXCEPTIONS_DISABLED 1 -#endif -#endif - -#define YUP_CXX20_IS_AVAILABLE (_MSVC_LANG == 202002L) -#define YUP_CXX23_IS_AVAILABLE (_MSVC_LANG > 202002L) -#endif - -//============================================================================== -#if ! YUP_CXX20_IS_AVAILABLE -#error "YUP requires C++20 or later" -#endif +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +/* + This file provides flags for compiler features that aren't supported on all platforms. +*/ + +//============================================================================== +// GCC +#if YUP_GCC + +#if (__GNUC__ * 100 + __GNUC_MINOR__) < 700 +#error "YUP requires GCC 7.0 or later" +#endif + +#ifndef YUP_EXCEPTIONS_DISABLED +#if ! __EXCEPTIONS +#define YUP_EXCEPTIONS_DISABLED 1 +#endif +#endif + +#define YUP_CXX20_IS_AVAILABLE (__cplusplus >= 202002L) +#define YUP_CXX23_IS_AVAILABLE (__cplusplus >= 202302L) + +#endif + +//============================================================================== +// Clang +#if YUP_CLANG + +#if (__clang_major__ < 6) +#error "YUP requires Clang 6 or later" +#endif + +#ifndef YUP_COMPILER_SUPPORTS_ARC +#define YUP_COMPILER_SUPPORTS_ARC 1 +#endif + +#ifndef YUP_EXCEPTIONS_DISABLED +#if ! __has_feature(cxx_exceptions) +#define YUP_EXCEPTIONS_DISABLED 1 +#endif +#endif + +#if ! defined(YUP_SILENCE_XCODE_15_LINKER_WARNING) \ + && defined(__apple_build_version__) \ + && __apple_build_version__ >= 15000000 \ + && __apple_build_version__ < 15000100 + + // Due to known issues, the linker in Xcode 15.0 may produce broken binaries. +#error Please upgrade to Xcode 15.1 or higher +#endif + +#define YUP_CXX20_IS_AVAILABLE (__cplusplus >= 202002L) +#define YUP_CXX23_IS_AVAILABLE (__cplusplus >= 202302L) + +#endif + +//============================================================================== +// MSVC +#if YUP_MSVC + +#if _MSC_FULL_VER < 191025017 // VS2017 +#error "YUP requires Visual Studio 2017 or later" +#endif + +#ifndef YUP_EXCEPTIONS_DISABLED +#if ! _CPPUNWIND +#define YUP_EXCEPTIONS_DISABLED 1 +#endif +#endif + +#define YUP_CXX20_IS_AVAILABLE (_MSVC_LANG == 202002L) +#define YUP_CXX23_IS_AVAILABLE (_MSVC_LANG > 202002L) +#endif + +//============================================================================== +#if ! YUP_CXX20_IS_AVAILABLE +#error "YUP requires C++20 or later" +#endif From 6e1f41f85f10152584e9b8a71b1ff9b2eb22682b Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 18 May 2026 11:55:59 +0200 Subject: [PATCH 21/35] More tests --- .../yup_AudioPluginInstance.cpp | 60 ++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/tests/yup_audio_plugin_host/yup_AudioPluginInstance.cpp b/tests/yup_audio_plugin_host/yup_AudioPluginInstance.cpp index 452daec9f..e8574c504 100644 --- a/tests/yup_audio_plugin_host/yup_AudioPluginInstance.cpp +++ b/tests/yup_audio_plugin_host/yup_AudioPluginInstance.cpp @@ -10,10 +10,11 @@ namespace class FakePluginInstance : public AudioPluginInstance { public: - FakePluginInstance() + explicit FakePluginInstance (bool supportsDoublePrecision = false) : AudioPluginInstance (makeDescription(), AudioBusLayout ({ AudioBus ("Input", AudioBus::Type::Audio, AudioBus::Direction::Input, 2) }, { AudioBus ("Output", AudioBus::Type::Audio, AudioBus::Direction::Output, 2) })) + , supportsDoublePrecision (supportsDoublePrecision) { } @@ -27,6 +28,14 @@ class FakePluginInstance : public AudioPluginInstance processCallCount++; } + void processBlock (AudioBuffer& audio, MidiBuffer&) override + { + audio.clear(); + doubleProcessCallCount++; + } + + bool supportsDoublePrecisionProcessing() const override { return supportsDoublePrecision; } + int getCurrentPreset() const noexcept override { return 0; } void setCurrentPreset (int) noexcept override {} @@ -53,9 +62,12 @@ class FakePluginInstance : public AudioPluginInstance bool prepared = false; int processCallCount = 0; + int doubleProcessCallCount = 0; MemoryBlock lastLoadedState; private: + bool supportsDoublePrecision = false; + static AudioPluginDescription makeDescription() { AudioPluginDescription d; @@ -106,6 +118,52 @@ TEST_F (AudioPluginInstanceTests, ProcessBlockIncrementsCounter) EXPECT_EQ (1, instance.processCallCount); } +TEST_F (AudioPluginInstanceTests, DefaultsToSinglePrecision) +{ + EXPECT_FALSE (instance.supportsDoublePrecisionProcessing()); + EXPECT_EQ (AudioProcessor::ProcessingPrecision::singlePrecision, instance.getProcessingPrecision()); + EXPECT_FALSE (instance.isUsingDoublePrecision()); +} + +TEST_F (AudioPluginInstanceTests, UsesDoublePrecisionWhenSupported) +{ + FakePluginInstance doublePrecisionInstance (true); + + doublePrecisionInstance.setProcessingPrecision (AudioProcessor::ProcessingPrecision::doublePrecision); + + EXPECT_TRUE (doublePrecisionInstance.supportsDoublePrecisionProcessing()); + EXPECT_EQ (AudioProcessor::ProcessingPrecision::doublePrecision, doublePrecisionInstance.getProcessingPrecision()); + EXPECT_TRUE (doublePrecisionInstance.isUsingDoublePrecision()); +} + +TEST_F (AudioPluginInstanceTests, CanReturnToSinglePrecisionAfterUsingDoublePrecision) +{ + FakePluginInstance doublePrecisionInstance (true); + + doublePrecisionInstance.setProcessingPrecision (AudioProcessor::ProcessingPrecision::doublePrecision); + doublePrecisionInstance.setProcessingPrecision (AudioProcessor::ProcessingPrecision::singlePrecision); + + EXPECT_EQ (AudioProcessor::ProcessingPrecision::singlePrecision, doublePrecisionInstance.getProcessingPrecision()); + EXPECT_FALSE (doublePrecisionInstance.isUsingDoublePrecision()); +} + +TEST_F (AudioPluginInstanceTests, DoublePrecisionProcessBlockUsesDoublePath) +{ + FakePluginInstance doublePrecisionInstance (true); + AudioBuffer audio (2, 512); + MidiBuffer midi; + + audio.setSample (0, 0, 1.0); + + doublePrecisionInstance.setProcessingPrecision (AudioProcessor::ProcessingPrecision::doublePrecision); + doublePrecisionInstance.prepareToPlay (44100.0f, 512); + doublePrecisionInstance.processBlock (audio, midi); + + EXPECT_EQ (0, doublePrecisionInstance.processCallCount); + EXPECT_EQ (1, doublePrecisionInstance.doubleProcessCallCount); + EXPECT_DOUBLE_EQ (0.0, audio.getSample (0, 0)); +} + TEST_F (AudioPluginInstanceTests, BusLayoutMatchesConstructorArg) { EXPECT_EQ (1, instance.getNumAudioInputs()); From de4ec9b02625c4eaa431a03424590618e8035571 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 18 May 2026 14:06:33 +0200 Subject: [PATCH 22/35] Improved audiograph and added separate app --- AGENTS.md | 4 +- CLAUDE.md | 4 +- CMakeLists.txt | 1 + examples/audiograph/CMakeLists.txt | 54 + examples/audiograph/source/AudioGraphApp.h | 726 ++++++++++++ examples/audiograph/source/main.cpp | 116 ++ examples/audiograph/source/nodes/GainNode.h | 128 ++ .../source/nodes/LowPassFilterNode.h | 151 +++ .../audiograph/source/nodes/NodeRegistry.h | 403 +++++++ .../audiograph/source/nodes/NodeViewHelpers.h | 48 + .../audiograph/source/nodes/OscillatorNode.h | 147 +++ .../audiograph/source/nodes/PluginNodeView.h | 80 ++ .../audiograph/source/ui/PluginEditorWindow.h | 66 ++ examples/graphics/CMakeLists.txt | 7 - .../graphics/source/examples/AudioGraphDemo.h | 549 --------- .../graphics/source/examples/PluginHost.h | 1055 ----------------- examples/graphics/source/main.cpp | 4 - .../graph/yup_AudioGraphProcessor.cpp | 26 +- .../graph/yup_AudioGraphProcessor.h | 3 + .../graph/yup_AudioGraphComponent.cpp | 51 +- .../graph/yup_AudioGraphComponent.h | 20 + .../native/yup_AudioPluginInstance_AUv2.mm | 10 +- .../native/yup_AudioPluginInstance_CLAP.cpp | 18 +- .../native/yup_AudioPluginInstance_VST3.cpp | 18 +- modules/yup_core/misc/yup_ResultValue.h | 95 +- modules/yup_core/system/yup_TargetPlatform.h | 13 +- modules/yup_data_model/tree/yup_DataTree.cpp | 8 +- .../stretching/yup_TimeStretchProcessor.cpp | 22 +- modules/yup_graphics/imaging/yup_Image.cpp | 4 +- .../yup_AudioGraphProcessor.cpp | 32 +- .../yup_AudioPluginScanner.cpp | 6 +- tests/yup_core/yup_ResultValue.cpp | 77 ++ 32 files changed, 2256 insertions(+), 1690 deletions(-) create mode 100644 examples/audiograph/CMakeLists.txt create mode 100644 examples/audiograph/source/AudioGraphApp.h create mode 100644 examples/audiograph/source/main.cpp create mode 100644 examples/audiograph/source/nodes/GainNode.h create mode 100644 examples/audiograph/source/nodes/LowPassFilterNode.h create mode 100644 examples/audiograph/source/nodes/NodeRegistry.h create mode 100644 examples/audiograph/source/nodes/NodeViewHelpers.h create mode 100644 examples/audiograph/source/nodes/OscillatorNode.h create mode 100644 examples/audiograph/source/nodes/PluginNodeView.h create mode 100644 examples/audiograph/source/ui/PluginEditorWindow.h delete mode 100644 examples/graphics/source/examples/AudioGraphDemo.h delete mode 100644 examples/graphics/source/examples/PluginHost.h diff --git a/AGENTS.md b/AGENTS.md index 9b1abcea1..f3e6f51f1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -336,9 +336,9 @@ yup::Result performOperation() yup::ResultValue maybeGetInteger() { if (preconditionFailed) - return yup::ResultValue::fail ("Precondition not met"); + return yup::makeResultValueFail ("Precondition not met"); - return 1; + return 1; // or yup::makeResultValueOk (1) } // Use assertions for programming errors diff --git a/CLAUDE.md b/CLAUDE.md index 9b1abcea1..f3e6f51f1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -336,9 +336,9 @@ yup::Result performOperation() yup::ResultValue maybeGetInteger() { if (preconditionFailed) - return yup::ResultValue::fail ("Precondition not met"); + return yup::makeResultValueFail ("Precondition not met"); - return 1; + return 1; // or yup::makeResultValueOk (1) } // Use assertions for programming errors diff --git a/CMakeLists.txt b/CMakeLists.txt index 683deaa1c..bedc04bab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,6 +78,7 @@ if (YUP_BUILD_EXAMPLES) add_subdirectory (examples/graphics) if (YUP_PLATFORM_DESKTOP) add_subdirectory (examples/plugin) + add_subdirectory (examples/audiograph) endif() endif() diff --git a/examples/audiograph/CMakeLists.txt b/examples/audiograph/CMakeLists.txt new file mode 100644 index 000000000..bbfeebf36 --- /dev/null +++ b/examples/audiograph/CMakeLists.txt @@ -0,0 +1,54 @@ +# ============================================================================== +# +# This file is part of the YUP library. +# Copyright (c) 2026 - kunitoki@gmail.com +# +# YUP is an open source library subject to open-source licensing. +# +# The code included in this file is provided under the terms of the ISC license +# http://www.isc.org/downloads/software-support-policy/isc-license. Permission +# to use, copy, modify, and/or distribute this software for any purpose with or +# without fee is hereby granted provided that the above copyright notice and +# this permission notice appear in all copies. +# +# YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER +# EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE +# DISCLAIMED. +# +# ============================================================================== + +cmake_minimum_required (VERSION 3.31) + +set (target_name example_audiograph) +set (target_version "1.0.0") + +project (${target_name} VERSION ${target_version}) + +yup_standalone_app ( + TARGET_NAME ${target_name} + TARGET_VERSION ${target_version} + TARGET_IDE_GROUP "Examples" + TARGET_APP_ID "org.yup.${target_name}" + TARGET_APP_NAMESPACE "org.yup" + DEFINITIONS + YUP_AUDIO_PLUGIN_HOST_ENABLE_CLAP=1 + YUP_AUDIO_PLUGIN_HOST_ENABLE_AU=1 + YUP_AUDIO_PLUGIN_HOST_ENABLE_VST3=1 + MODULES + yup::yup_core + yup::yup_audio_basics + yup::yup_audio_devices + yup::yup_dsp + yup::yup_events + yup::yup_graphics + yup::yup_gui + yup::yup_audio_gui + yup::yup_audio_processors + yup::yup_audio_graph + yup::yup_audio_plugin_host) + +file (GLOB_RECURSE sources + "${CMAKE_CURRENT_LIST_DIR}/source/*.cpp" + "${CMAKE_CURRENT_LIST_DIR}/source/*.h") +source_group (TREE ${CMAKE_CURRENT_LIST_DIR}/ FILES ${sources}) +target_sources (${target_name} PRIVATE ${sources}) diff --git a/examples/audiograph/source/AudioGraphApp.h b/examples/audiograph/source/AudioGraphApp.h new file mode 100644 index 000000000..249573e69 --- /dev/null +++ b/examples/audiograph/source/AudioGraphApp.h @@ -0,0 +1,726 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +#include +#include +#include +#include + +//============================================================================== + +class SoundCardInputNodeView final : public yup::AudioGraphNodeView +{ +public: + SoundCardInputNodeView() + : AudioGraphNodeView (yup::AudioGraphNodeID::invalid()) + { + } + + yup::String getNodeTitle() const override { return "INPUT"; } + + yup::String getNodeSubtitle() const override { return "sound card"; } + + int getNumInputPorts() const override { return 0; } + + int getNumOutputPorts() const override { return 1; } + + int getPreferredWidth() const override { return 150; } + + yup::Color getNodeColor() const override { return yup::Color (0xfff97316); } + + PortInfo getOutputPortInfo (int) const override + { + return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; + } +}; + +class SoundCardOutputNodeView final : public yup::AudioGraphNodeView +{ +public: + SoundCardOutputNodeView() + : AudioGraphNodeView (yup::AudioGraphNodeID::invalid()) + { + } + + yup::String getNodeTitle() const override { return "OUTPUT"; } + + yup::String getNodeSubtitle() const override { return "sound card"; } + + int getNumInputPorts() const override { return 1; } + + int getNumOutputPorts() const override { return 0; } + + int getPreferredWidth() const override { return 150; } + + yup::Color getNodeColor() const override { return yup::Color (0xff06b6d4); } + + PortInfo getInputPortInfo (int) const override + { + return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; + } +}; + +//============================================================================== + +class AudioGraphApp final + : public yup::Component + , public yup::AudioIODeviceCallback + , public yup::AudioGraphComponent::Listener +{ +public: + AudioGraphApp() + { + deviceManager.initialiseWithDefaultDevices (2, 2); + + graph = std::make_shared(); + nodeRegistry.registerInternalNodes(); + +#if YUP_DESKTOP + scanner = std::make_unique(); + +#if YUP_AUDIO_PLUGIN_HOST_ENABLE_VST3 + scanner->addFormat (std::make_unique()); +#endif +#if YUP_AUDIO_PLUGIN_HOST_ENABLE_CLAP + scanner->addFormat (std::make_unique()); +#endif +#if YUP_AUDIO_PLUGIN_HOST_ENABLE_AU && YUP_MAC + scanner->addFormat (std::make_unique()); +#endif + + nodeRegistry.registerPluginFormats (scanner.get(), makeHostContext()); +#endif + + graph->setNodeFactory (nodeRegistry.makeProcessorFactory()); + + graphComponent = std::make_unique (graph); + graphComponent->onCanvasContextMenu = [this] (yup::Point canvasPos) + { + showAddNodeMenu (canvasPos); + }; + graphComponent->onNodeContextMenu = [this] (yup::AudioGraphNodeID id, yup::Point) + { + removeNode (id); + }; + graphComponent->onNodeDoubleClicked = [this] (yup::AudioGraphNodeID id) + { + openPluginEditor (id); + }; + graphComponent->addListener (this); + addAndMakeVisible (*graphComponent); + + setupToolbar(); + resetToEmptyGraph(); + +#if YUP_DESKTOP + startPluginScan(); +#endif + } + + ~AudioGraphApp() override + { +#if YUP_WINDOWS || YUP_MAC || YUP_LINUX + scanLifetime->store (false); +#endif + + closePluginEditor(); + + if (audioCallbackRegistered) + { + deviceManager.removeAudioCallback (this); + audioCallbackRegistered = false; + } + + deviceManager.closeAudioDevice(); + } + + void resized() override + { + auto bounds = getLocalBounds(); + auto toolbar = bounds.removeFromTop (36); + + newButton.setBounds (toolbar.removeFromLeft (70).reduced (4, 4)); + openButton.setBounds (toolbar.removeFromLeft (70).reduced (4, 4)); + saveButton.setBounds (toolbar.removeFromLeft (70).reduced (4, 4)); + toolbar.removeFromLeft (8); + scanButton.setBounds (toolbar.removeFromLeft (100).reduced (4, 4)); + statusLabel.setBounds (toolbar.reduced (4, 4)); + + graphComponent->setBounds (bounds); + } + + void paint (yup::Graphics& g) override + { + g.setFillColor (yup::Color (0xff0d1117)); + g.fillAll(); + + g.setFillColor (yup::Color (0xff161b22)); + g.fillRect (getLocalBounds().removeFromTop (36).to()); + } + + void visibilityChanged() override + { + if (isVisible()) + { + deviceManager.addAudioCallback (this); + audioCallbackRegistered = true; + } + else if (audioCallbackRegistered) + { + deviceManager.removeAudioCallback (this); + audioCallbackRegistered = false; + } + } + + //============================================================================== + // AudioIODeviceCallback + + void audioDeviceIOCallbackWithContext (const float* const* inputChannelData, + int numInputChannels, + float* const* outputChannelData, + int numOutputChannels, + int numSamples, + const yup::AudioIODeviceCallbackContext&) override + { + for (int ch = 0; ch < numOutputChannels; ++ch) + yup::FloatVectorOperations::clear (outputChannelData[ch], numSamples); + + if (graph == nullptr || numOutputChannels <= 0 || numSamples <= 0) + return; + + yup::AudioBuffer outputBuffer (outputChannelData, numOutputChannels, numSamples); + + for (int ch = 0; ch < yup::jmin (numInputChannels, numOutputChannels); ++ch) + { + if (inputChannelData != nullptr && inputChannelData[ch] != nullptr) + yup::FloatVectorOperations::copy (outputChannelData[ch], inputChannelData[ch], numSamples); + } + + yup::MidiBuffer midi; + graph->processBlock (outputBuffer, midi); + } + + void audioDeviceAboutToStart (yup::AudioIODevice* device) override + { + if (graph == nullptr || device == nullptr) + return; + +#if YUP_DESKTOP + yup::AudioPluginHostContext ctx; + ctx.sampleRate = static_cast (device->getCurrentSampleRate()); + ctx.maxBlockSize = device->getCurrentBufferSizeSamples(); + nodeRegistry.setPluginScanner (scanner.get(), ctx); +#endif + + graph->prepareToPlay (static_cast (device->getCurrentSampleRate()), + device->getCurrentBufferSizeSamples()); + } + + void audioDeviceStopped() override + { + if (graph != nullptr) + graph->releaseResources(); + } + + //============================================================================== + // AudioGraphComponent::Listener + + void nodeViewMoved (yup::AudioGraphNodeID nodeID, yup::Point newCanvasPos) override + { + graph->setNodePosition (nodeID, newCanvasPos.getX(), newCanvasPos.getY()); + } + +private: + //============================================================================== + + void setupToolbar() + { + newButton.setButtonText ("New"); + newButton.onClick = [this] + { + resetToEmptyGraph(); + statusLabel.setText ("New graph.", yup::dontSendNotification); + }; + addAndMakeVisible (newButton); + + openButton.setButtonText ("Open"); + openButton.onClick = [this] + { + openGraph(); + }; + addAndMakeVisible (openButton); + + saveButton.setButtonText ("Save"); + saveButton.onClick = [this] + { + saveGraph(); + }; + addAndMakeVisible (saveButton); + + scanButton.setButtonText ("Scan Plugins"); + scanButton.onClick = [this] + { + startPluginScan(); + }; + addAndMakeVisible (scanButton); + + statusLabel.setText ("Ready.", yup::dontSendNotification); + addAndMakeVisible (statusLabel); + } + + //============================================================================== + + void resetToEmptyGraph() + { + closePluginEditor(); + + for (auto& [nodeID, _] : loadedNodes) + graphComponent->removeNodeView (nodeID); + + loadedNodes.clear(); + graph->clear(); + graph->commitChanges(); + currentFilePath = yup::File(); + + graphComponent->setGraphInputView (std::make_unique(), { 40.0f, 200.0f }); + graphComponent->setGraphOutputView (std::make_unique(), { 760.0f, 200.0f }); + graphComponent->zoomToFitNodes(); + } + + //============================================================================== + + void openGraph() + { + fileChooser = yup::FileChooser::create ( + "Open Audio Graph", + currentFilePath.existsAsFile() ? currentFilePath.getParentDirectory() + : yup::File::getSpecialLocation (yup::File::userDocumentsDirectory), + "*.yug"); + + fileChooser->browseForFileToOpen ([this] (bool success, const yup::Array& results) + { + if (! success || results.isEmpty()) + return; + + const auto& file = results[0]; + + yup::MemoryBlock mb; + if (! file.loadFileAsData (mb)) + { + statusLabel.setText ("Failed to read file.", yup::dontSendNotification); + return; + } + + loadGraphFromMemory (mb, file); + }); + } + + void saveGraph() + { + if (currentFilePath.existsAsFile()) + { + saveGraphToFile (currentFilePath); + return; + } + + fileChooser = yup::FileChooser::create ( + "Save Audio Graph", + yup::File::getSpecialLocation (yup::File::userDocumentsDirectory).getChildFile ("Untitled.yug"), + "*.yug"); + + fileChooser->browseForFileToSave ([this] (bool success, const yup::Array& results) + { + if (! success || results.isEmpty()) + return; + + auto file = results[0]; + if (file.getFileExtension().isEmpty()) + file = file.withFileExtension ("yug"); + + saveGraphToFile (file); + }, + true); + } + + void saveGraphToFile (const yup::File& file) + { + yup::MemoryBlock mb; + const auto result = graph->saveStateIntoMemory (mb); + + if (result.failed()) + { + statusLabel.setText ("Save failed: " + result.getErrorMessage(), yup::dontSendNotification); + return; + } + + if (! file.replaceWithData (mb.getData(), mb.getSize())) + { + statusLabel.setText ("Could not write: " + file.getFileName(), yup::dontSendNotification); + return; + } + + currentFilePath = file; + statusLabel.setText ("Saved: " + file.getFileName(), yup::dontSendNotification); + } + + void loadGraphFromMemory (const yup::MemoryBlock& mb, const yup::File& file) + { + closePluginEditor(); + + for (auto& [nodeID, _] : loadedNodes) + graphComponent->removeNodeView (nodeID); + loadedNodes.clear(); + + graph->clear(); + + const auto result = graph->loadStateFromMemory (mb); + if (result.failed()) + { + statusLabel.setText ("Load failed: " + result.getErrorMessage(), yup::dontSendNotification); + graph->commitChanges(); + return; + } + + graph->commitChanges(); + currentFilePath = file; + + for (auto nodeID : graph->getNodeIDs()) + { + auto props = graph->getNodeProperties (nodeID); + if (! props.has_value()) + continue; + + auto* proc = graph->getNodeProcessor (nodeID); + auto view = nodeRegistry.createView (nodeID, props->identifier, proc); + + if (view != nullptr) + { + graphComponent->addNodeView (nodeID, std::move (view), { props->positionX, props->positionY }); + loadedNodes[nodeID] = props->identifier; + } + } + + graphComponent->setGraphInputView (std::make_unique(), { 40.0f, 200.0f }); + graphComponent->setGraphOutputView (std::make_unique(), { 760.0f, 200.0f }); + graphComponent->zoomToFitNodes(); + + statusLabel.setText ("Loaded: " + file.getFileName(), yup::dontSendNotification); + } + + //============================================================================== + + void showAddNodeMenu (yup::Point canvasPos) + { + pendingMenuCanvasPos = canvasPos; + + auto internalSubMenu = yup::PopupMenu::create(); + internalSubMenu->addItem ("Oscillator", 1); + internalSubMenu->addItem ("Gain", 2); + internalSubMenu->addItem ("Low Pass Filter", 3); + + activeMenu = yup::PopupMenu::create(); + activeMenu->addSubMenu ("Internal Nodes", internalSubMenu); + +#if YUP_DESKTOP + const auto& plugins = nodeRegistry.getDiscoveredPlugins(); + + if (! plugins.empty()) + { + auto pluginSubMenu = yup::PopupMenu::create(); + + for (int i = 0; i < static_cast (plugins.size()); ++i) + { + const auto& desc = plugins[static_cast (i)]; + pluginSubMenu->addItem (desc.name + " [" + formatTypeToString (desc.formatType) + "]", + 100 + i); + } + + activeMenu->addSubMenu ("Plugins", pluginSubMenu); + } + else + { + activeMenu->addItem ("No plugins (click Scan)", -1, false); + } +#endif + + activeMenu->show ([this] (int selectedID) + { + if (selectedID <= 0) + return; + + const yup::String internalIds[] = { + NodeRegistry::oscillatorIdentifier, + NodeRegistry::gainIdentifier, + NodeRegistry::lpfIdentifier + }; + + if (selectedID >= 1 && selectedID <= 3) + { + addInternalNode (internalIds[selectedID - 1], pendingMenuCanvasPos); + } +#if YUP_DESKTOP + else if (selectedID >= 100) + { + const auto& plugins = nodeRegistry.getDiscoveredPlugins(); + const auto index = static_cast (selectedID - 100); + if (index < plugins.size()) + addPluginNode (plugins[index], pendingMenuCanvasPos); + } +#endif + }); + } + + void addInternalNode (const yup::String& identifier, yup::Point canvasPos) + { + yup::AudioGraphNodeProperties props; + props.identifier = identifier; + props.name = NodeRegistry::identifierToDisplayName (identifier); + props.positionX = canvasPos.getX(); + props.positionY = canvasPos.getY(); + + auto processorResult = nodeRegistry.makeProcessorFactory() (props); + + if (processorResult.failed()) + { + statusLabel.setText ("Failed: " + processorResult.getErrorMessage(), yup::dontSendNotification); + return; + } + + const auto nodeID = graph->addNode (std::move (processorResult).getValue(), props); + if (! nodeID.isValid()) + { + statusLabel.setText ("Failed to add node to graph.", yup::dontSendNotification); + return; + } + + graph->commitChanges(); + + auto* rawProc = graph->getNodeProcessor (nodeID); + auto view = nodeRegistry.createView (nodeID, identifier, rawProc); + if (view != nullptr) + { + graphComponent->addNodeView (nodeID, std::move (view), canvasPos); + loadedNodes[nodeID] = identifier; + } + } + +#if YUP_DESKTOP + void addPluginNode (const yup::AudioPluginDescription& desc, yup::Point canvasPos) + { + auto* format = scanner->getFormatForType (desc.formatType); + if (format == nullptr) + { + statusLabel.setText ("No format backend for: " + desc.name, yup::dontSendNotification); + return; + } + + auto result = format->loadPlugin (desc, makeHostContext()); + if (result.failed()) + { + statusLabel.setText ("Load failed: " + result.getErrorMessage(), yup::dontSendNotification); + return; + } + + yup::AudioGraphNodeProperties props; + props.identifier = NodeRegistry::identifierForDescription (desc); + props.name = desc.name; + props.positionX = canvasPos.getX(); + props.positionY = canvasPos.getY(); + props.creationData = NodeRegistry::descriptionToCreationData (desc); + + const auto nodeID = graph->addNode (std::move (result).getValue(), props); + if (! nodeID.isValid()) + { + statusLabel.setText ("Failed to add plugin to graph.", yup::dontSendNotification); + return; + } + + graph->commitChanges(); + + auto* rawProc = graph->getNodeProcessor (nodeID); + auto view = nodeRegistry.createView (nodeID, props.identifier, rawProc); + if (view != nullptr) + { + graphComponent->addNodeView (nodeID, std::move (view), canvasPos); + loadedNodes[nodeID] = props.identifier; + } + + statusLabel.setText ("Loaded: " + desc.name, yup::dontSendNotification); + } +#endif + + void removeNode (yup::AudioGraphNodeID nodeID) + { + if (activePluginEditorNodeID == nodeID) + { + closePluginEditor(); + } + + graphComponent->removeNodeView (nodeID); + graph->removeNode (nodeID); + graph->commitChanges(); + loadedNodes.erase (nodeID); + } + + //============================================================================== + + void openPluginEditor (yup::AudioGraphNodeID nodeID) + { + if (pluginEditorWindow != nullptr && activePluginEditorNodeID == nodeID) + { + pluginEditorWindow->toFront (true); + return; + } + + auto* proc = graph->getNodeProcessor (nodeID); + if (proc == nullptr || ! proc->hasEditor()) + return; + + auto editor = std::unique_ptr (proc->createEditor()); + if (editor == nullptr) + return; + + closePluginEditor(); + activePluginEditorNodeID = nodeID; + + const auto preferredSize = editor->getPreferredSize(); + + yup::WeakReference weakThis (this); + + pluginEditorWindow = std::make_unique ( + proc->getName(), + std::move (editor), + [weakThis]() + { + if (auto* self = weakThis.get()) + self->closePluginEditor(); + }); + + pluginEditorWindow->centreWithSize (preferredSize); + pluginEditorWindow->setVisible (true); + pluginEditorWindow->toFront (true); + } + + void closePluginEditor() + { + pluginEditorWindow.reset(); + activePluginEditorNodeID = yup::AudioGraphNodeID::invalid(); + } + + //============================================================================== + +#if YUP_DESKTOP + void startPluginScan() + { + if (scanInProgress.exchange (true)) + return; + + statusLabel.setText ("Scanning for plugins...", yup::dontSendNotification); + scanButton.setEnabled (false); + + auto lifetime = scanLifetime; + + scanner->scanDefaultsAsync ([this, lifetime] (yup::AudioPluginScanner::ScanResult result) mutable + { + if (! lifetime->load()) + return; + + yup::MessageManager::callAsync ([this, lifetime, result = std::move (result)]() mutable + { + if (! lifetime->load()) + return; + + nodeRegistry.setDiscoveredPlugins (std::move (result.discovered)); + + const auto count = static_cast (nodeRegistry.getDiscoveredPlugins().size()); + statusLabel.setText ("Found " + yup::String (count) + " plugins.", yup::dontSendNotification); + scanButton.setEnabled (true); + scanInProgress = false; + }); + }); + } + + yup::AudioPluginHostContext makeHostContext() const + { + yup::AudioPluginHostContext ctx; + ctx.sampleRate = 44100.0f; + ctx.maxBlockSize = 512; + + if (auto* dev = deviceManager.getCurrentAudioDevice()) + { + ctx.sampleRate = static_cast (dev->getCurrentSampleRate()); + ctx.maxBlockSize = dev->getCurrentBufferSizeSamples(); + } + + return ctx; + } + + static yup::String formatTypeToString (yup::AudioPluginFormatType type) + { + switch (type) + { + case yup::AudioPluginFormatType::vst3: + return "VST3"; + case yup::AudioPluginFormatType::clap: + return "CLAP"; + case yup::AudioPluginFormatType::audioUnit: + return "AU"; + default: + return "?"; + } + } +#endif + + //============================================================================== + + yup::AudioDeviceManager deviceManager; + std::shared_ptr graph; + std::unique_ptr graphComponent; + NodeRegistry nodeRegistry; + std::map loadedNodes; + + yup::File currentFilePath; + yup::FileChooser::Ptr fileChooser; + + std::unique_ptr pluginEditorWindow; + yup::AudioGraphNodeID activePluginEditorNodeID { yup::AudioGraphNodeID::invalid() }; + + yup::TextButton newButton; + yup::TextButton openButton; + yup::TextButton saveButton; + yup::TextButton scanButton; + yup::Label statusLabel; + + bool audioCallbackRegistered = false; + + yup::PopupMenu::Ptr activeMenu; + yup::Point pendingMenuCanvasPos; + +#if YUP_DESKTOP + std::shared_ptr> scanLifetime { std::make_shared> (true) }; + std::atomic scanInProgress { false }; + std::unique_ptr scanner; +#endif + + YUP_DECLARE_WEAK_REFERENCEABLE (AudioGraphApp) +}; diff --git a/examples/audiograph/source/main.cpp b/examples/audiograph/source/main.cpp new file mode 100644 index 000000000..581dceaee --- /dev/null +++ b/examples/audiograph/source/main.cpp @@ -0,0 +1,116 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if YUP_WINDOWS || YUP_MAC || YUP_LINUX +#include +#endif + +#include "nodes/OscillatorNode.h" +#include "nodes/GainNode.h" +#include "nodes/LowPassFilterNode.h" +#include "nodes/PluginNodeView.h" +#include "nodes/NodeRegistry.h" +#include "ui/PluginEditorWindow.h" +#include "AudioGraphApp.h" + +//============================================================================== + +class AudioGraphWindow final : public yup::DocumentWindow +{ +public: + AudioGraphWindow() + : yup::DocumentWindow (yup::ComponentNative::Options().withAllowedHighDensityDisplay (true), + yup::Color (0xff0d1117)) + { + setTitle ("YUP Audio Graph"); + + app = std::make_unique(); + addAndMakeVisible (app.get()); + } + + void resized() override + { + app->setBounds (getLocalBounds()); + } + + void keyDown (const yup::KeyPress& keys, const yup::Point&) override + { + if (keys.getKey() == yup::KeyPress::escapeKey) + userTriedToCloseWindow(); + } + + void userTriedToCloseWindow() override + { + yup::YUPApplication::getInstance()->systemRequestedQuit(); + } + +private: + std::unique_ptr app; +}; + +//============================================================================== + +struct Application final : yup::YUPApplication +{ + Application() = default; + + yup::String getApplicationName() override + { + return "YUP Audio Graph"; + } + + yup::String getApplicationVersion() override + { + return "1.0"; + } + + void initialise (const yup::String&) override + { + yup::MessageManager::callAsync ([this] + { + yup::Process::makeForegroundProcess(); + + window = std::make_unique(); + window->centreWithSize ({ 1200, 800 }); + window->setVisible (true); + }); + } + + void shutdown() override + { + window.reset(); + } + +private: + std::unique_ptr window; +}; + +START_YUP_APPLICATION (Application) diff --git a/examples/audiograph/source/nodes/GainNode.h b/examples/audiograph/source/nodes/GainNode.h new file mode 100644 index 000000000..f292456f0 --- /dev/null +++ b/examples/audiograph/source/nodes/GainNode.h @@ -0,0 +1,128 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +#include + +#include "NodeViewHelpers.h" + +//============================================================================== +class GainProcessor final : public yup::AudioProcessor +{ +public: + GainProcessor() + : AudioProcessor ("Gain", + yup::AudioBusLayout ({ yup::AudioBus ("Main", yup::AudioBus::Audio, yup::AudioBus::Input, 2) }, + { yup::AudioBus ("Main", yup::AudioBus::Audio, yup::AudioBus::Output, 2) })) + { + } + + void prepareToPlay (float, int) override {} + + void releaseResources() override {} + + void processBlock (yup::AudioBuffer& audioBuffer, yup::MidiBuffer&) override + { + audioBuffer.applyGain (gain.load (std::memory_order_relaxed)); + } + + int getCurrentPreset() const noexcept override { return 0; } + + void setCurrentPreset (int) noexcept override {} + + int getNumPresets() const override { return 0; } + + yup::String getPresetName (int) const override { return {}; } + + void setPresetName (int, yup::StringRef) override {} + + yup::Result loadStateFromMemory (const yup::MemoryBlock&) override { return yup::Result::ok(); } + + yup::Result saveStateIntoMemory (yup::MemoryBlock&) override { return yup::Result::ok(); } + + bool hasEditor() const override { return false; } + + yup::AudioProcessorEditor* createEditor() override { return nullptr; } + + float getGain() const noexcept { return gain.load (std::memory_order_relaxed); } + + void setGain (float newGain) noexcept + { + gain.store (yup::jlimit (0.0f, 1.5f, newGain), std::memory_order_relaxed); + } + +private: + std::atomic gain { 0.75f }; +}; + +//============================================================================== +class GainNodeView final : public yup::AudioGraphNodeView +{ +public: + GainNodeView (yup::AudioGraphNodeID nodeID, GainProcessor& processorIn) + : AudioGraphNodeView (nodeID) + , processor (processorIn) + , gainSlider (yup::Slider::LinearBarHorizontal) + { + NodeViewHelpers::configureParameterSlider (gainSlider, getPortKindColor (PortKind::parameter)); + gainSlider.setRange (0.0, 1.5); + gainSlider.setValue (processor.getGain(), yup::dontSendNotification); + gainSlider.onValueChanged = [this] (double value) + { + processor.setGain (static_cast (value)); + repaint(); + }; + addAndMakeVisible (gainSlider); + } + + yup::String getNodeTitle() const override { return "GAIN"; } + + int getNumInputPorts() const override { return 1; } + + int getNumOutputPorts() const override { return 1; } + + int getPreferredWidth() const override { return 220; } + + yup::Color getNodeColor() const override { return yup::Color (0xff10b981); } + + yup::String getNodeSubtitle() const override { return yup::String ("x ") + yup::String (processor.getGain(), 2); } + + PortInfo getInputPortInfo (int) const override { return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; } + + PortInfo getOutputPortInfo (int) const override { return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; } + + int getNumParameterRows() const override { return 1; } + + ParameterInfo getParameterInfo (int) const override + { + return { "Gain", yup::String (processor.getGain(), 2), getPortKindColor (PortKind::parameter), -1.0f, PortKind::parameter }; + } + + void resized() override + { + gainSlider.setBounds (NodeViewHelpers::getInlineSliderBounds (*this, getPreferredWidth())); + } + +private: + GainProcessor& processor; + yup::Slider gainSlider; +}; diff --git a/examples/audiograph/source/nodes/LowPassFilterNode.h b/examples/audiograph/source/nodes/LowPassFilterNode.h new file mode 100644 index 000000000..6018acbed --- /dev/null +++ b/examples/audiograph/source/nodes/LowPassFilterNode.h @@ -0,0 +1,151 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +#include +#include + +#include "NodeViewHelpers.h" + +//============================================================================== +class LowPassFilterProcessor final : public yup::AudioProcessor +{ +public: + LowPassFilterProcessor() + : AudioProcessor ("Low-pass", + yup::AudioBusLayout ({ yup::AudioBus ("Main", yup::AudioBus::Audio, yup::AudioBus::Input, 2) }, + { yup::AudioBus ("Main", yup::AudioBus::Audio, yup::AudioBus::Output, 2) })) + { + } + + void prepareToPlay (float newSampleRate, int) override + { + sampleRate = newSampleRate; + state[0] = 0.0f; + state[1] = 0.0f; + } + + void releaseResources() override {} + + void processBlock (yup::AudioBuffer& audioBuffer, yup::MidiBuffer&) override + { + const auto currentCutoff = static_cast (cutoff.load (std::memory_order_relaxed)); + const auto alpha = static_cast (1.0 - std::exp (-yup::MathConstants::twoPi * currentCutoff / static_cast (sampleRate))); + + for (int channel = 0; channel < audioBuffer.getNumChannels(); ++channel) + { + auto y = state[static_cast (yup::jmin (channel, 1))]; + + for (int sample = 0; sample < audioBuffer.getNumSamples(); ++sample) + { + y += alpha * (audioBuffer.getSample (channel, sample) - y); + audioBuffer.setSample (channel, sample, y); + } + + state[static_cast (yup::jmin (channel, 1))] = y; + } + } + + int getCurrentPreset() const noexcept override { return 0; } + + void setCurrentPreset (int) noexcept override {} + + int getNumPresets() const override { return 0; } + + yup::String getPresetName (int) const override { return {}; } + + void setPresetName (int, yup::StringRef) override {} + + yup::Result loadStateFromMemory (const yup::MemoryBlock&) override { return yup::Result::ok(); } + + yup::Result saveStateIntoMemory (yup::MemoryBlock&) override { return yup::Result::ok(); } + + bool hasEditor() const override { return false; } + + yup::AudioProcessorEditor* createEditor() override { return nullptr; } + + double getCutoff() const noexcept { return static_cast (cutoff.load (std::memory_order_relaxed)); } + + void setCutoff (double newCutoff) noexcept + { + cutoff.store (static_cast (yup::jlimit (80.0, 12000.0, newCutoff)), std::memory_order_relaxed); + } + +private: + float sampleRate = 44100.0f; + std::atomic cutoff { 800.0f }; + float state[2] {}; +}; + +//============================================================================== +class LowPassFilterNodeView final : public yup::AudioGraphNodeView +{ +public: + LowPassFilterNodeView (yup::AudioGraphNodeID nodeID, LowPassFilterProcessor& processorIn) + : AudioGraphNodeView (nodeID) + , processor (processorIn) + , cutoffSlider (yup::Slider::LinearBarHorizontal) + { + NodeViewHelpers::configureParameterSlider (cutoffSlider, getPortKindColor (PortKind::parameter)); + cutoffSlider.setRange (80.0, 12000.0); + cutoffSlider.setSkewFactorFromMidpoint (1000.0); + cutoffSlider.setValue (processor.getCutoff(), yup::dontSendNotification); + cutoffSlider.onValueChanged = [this] (double value) + { + processor.setCutoff (value); + repaint(); + }; + addAndMakeVisible (cutoffSlider); + } + + yup::String getNodeTitle() const override { return "LPF"; } + + int getNumInputPorts() const override { return 1; } + + int getNumOutputPorts() const override { return 1; } + + int getPreferredWidth() const override { return 220; } + + yup::Color getNodeColor() const override { return yup::Color (0xff7c3aed); } + + yup::String getNodeSubtitle() const override { return yup::String (processor.getCutoff(), 0) + " Hz"; } + + PortInfo getInputPortInfo (int) const override { return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; } + + PortInfo getOutputPortInfo (int) const override { return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; } + + int getNumParameterRows() const override { return 1; } + + ParameterInfo getParameterInfo (int) const override + { + return { "Cutoff", yup::String (processor.getCutoff(), 0), getPortKindColor (PortKind::parameter), -1.0f, PortKind::parameter }; + } + + void resized() override + { + cutoffSlider.setBounds (NodeViewHelpers::getInlineSliderBounds (*this, getPreferredWidth())); + } + +private: + LowPassFilterProcessor& processor; + yup::Slider cutoffSlider; +}; diff --git a/examples/audiograph/source/nodes/NodeRegistry.h b/examples/audiograph/source/nodes/NodeRegistry.h new file mode 100644 index 000000000..704533b7d --- /dev/null +++ b/examples/audiograph/source/nodes/NodeRegistry.h @@ -0,0 +1,403 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +#include +#include +#include +#include + +//============================================================================== +/** + Maps stable string identifiers to processor and view factories. + + NodeRegistry drives both the AudioGraphProcessor::NodeFactory (used for + graph state reconstruction after loading) and menu-based node creation in + the editor. Register all internal node types via registerInternalNodes() + and, on desktop builds, plugin format entries via registerPluginFormats(). +*/ +class NodeRegistry +{ +public: + //============================================================================== + /** Stable factory key for the built-in oscillator node. */ + static constexpr const char* oscillatorIdentifier = "internal.oscillator"; + + /** Stable factory key for the built-in gain node. */ + static constexpr const char* gainIdentifier = "internal.gain"; + + /** Stable factory key for the built-in low-pass filter node. */ + static constexpr const char* lpfIdentifier = "internal.lpf"; + +#if YUP_WINDOWS || YUP_MAC || YUP_LINUX + /** Stable factory key for VST3 plugin nodes. */ + static constexpr const char* pluginVst3Identifier = "plugin.vst3"; + + /** Stable factory key for CLAP plugin nodes. */ + static constexpr const char* pluginClapIdentifier = "plugin.clap"; + + /** Stable factory key for Audio Unit plugin nodes. */ + static constexpr const char* pluginAuIdentifier = "plugin.au"; +#endif + + //============================================================================== + using ProcessorFactory = std::function> (const yup::AudioGraphNodeProperties&)>; + using ViewFactory = std::function (yup::AudioGraphNodeID, yup::AudioProcessor*)>; + + //============================================================================== + struct Entry + { + ProcessorFactory createProcessor; + ViewFactory createView; + }; + + //============================================================================== + /** + Registers the built-in internal node types: oscillator, gain, and low-pass filter. + + Each entry gets a processor factory and a matching view factory. The view + factory dynamic_casts to the concrete processor type before constructing + the view. + */ + void registerInternalNodes() + { + entries[oscillatorIdentifier] = { + [] (const yup::AudioGraphNodeProperties&) -> yup::ResultValue> + { + return yup::makeResultValueOk (std::make_unique()); + }, + [] (yup::AudioGraphNodeID nodeID, yup::AudioProcessor* proc) -> std::unique_ptr + { + auto* osc = dynamic_cast (proc); + if (osc == nullptr) + return nullptr; + + return std::make_unique (nodeID, *osc); + } + }; + + entries[gainIdentifier] = { + [] (const yup::AudioGraphNodeProperties&) -> yup::ResultValue> + { + return yup::makeResultValueOk (std::make_unique()); + }, + [] (yup::AudioGraphNodeID nodeID, yup::AudioProcessor* proc) -> std::unique_ptr + { + auto* gain = dynamic_cast (proc); + if (gain == nullptr) + return nullptr; + + return std::make_unique (nodeID, *gain); + } + }; + + entries[lpfIdentifier] = { + [] (const yup::AudioGraphNodeProperties&) -> yup::ResultValue> + { + return yup::makeResultValueOk (std::make_unique()); + }, + [] (yup::AudioGraphNodeID nodeID, yup::AudioProcessor* proc) -> std::unique_ptr + { + auto* lpf = dynamic_cast (proc); + if (lpf == nullptr) + return nullptr; + + return std::make_unique (nodeID, *lpf); + } + }; + } + +#if YUP_WINDOWS || YUP_MAC || YUP_LINUX + //============================================================================== + /** + Stores the plugin scanner and host context, then registers factory entries + for all three plugin format identifiers (VST3, CLAP, AudioUnit). + + All plugin identifiers share the same processor factory (which calls + loadPluginFromProperties) and a view factory that wraps the loaded + AudioPluginInstance in a PluginNodeView. + + @param scanner The plugin scanner that provides access to format backends. + @param ctx The host context passed to the format's loadPlugin call. + */ + void registerPluginFormats (yup::AudioPluginScanner* scanner, yup::AudioPluginHostContext ctx) + { + pluginScanner = scanner; + hostContext = ctx; + + auto processorFactory = [this] (const yup::AudioGraphNodeProperties& props) + -> yup::ResultValue> + { + return loadPluginFromProperties (props); + }; + + auto viewFactory = [] (yup::AudioGraphNodeID nodeID, yup::AudioProcessor* proc) + -> std::unique_ptr + { + auto* instance = dynamic_cast (proc); + if (instance == nullptr) + return nullptr; + + return std::make_unique (nodeID, instance->getDescription()); + }; + + entries[pluginVst3Identifier] = { processorFactory, viewFactory }; + entries[pluginClapIdentifier] = { processorFactory, viewFactory }; + entries[pluginAuIdentifier] = { processorFactory, viewFactory }; + } + + //============================================================================== + /** + Replaces the current list of discovered plugins. + + @param plugins A vector of plugin descriptions, typically obtained from + AudioPluginScanner::scan() or scanDefaults(). + */ + void setDiscoveredPlugins (std::vector plugins) + { + discoveredPlugins = std::move (plugins); + } + + const std::vector& getDiscoveredPlugins() const noexcept + { + return discoveredPlugins; + } + + //============================================================================== + /** + Returns the node registry identifier that corresponds to the given plugin description. + + @param desc A plugin description whose formatType drives the selection. + @returns One of pluginVst3Identifier, pluginClapIdentifier, or pluginAuIdentifier. + */ + static yup::String identifierForDescription (const yup::AudioPluginDescription& desc) + { + switch (desc.formatType) + { + case yup::AudioPluginFormatType::vst3: + return pluginVst3Identifier; + case yup::AudioPluginFormatType::clap: + return pluginClapIdentifier; + case yup::AudioPluginFormatType::audioUnit: + return pluginAuIdentifier; + default: + return "plugin.unknown"; + } + } + + //============================================================================== + /** + Serialises a plugin description into an opaque MemoryBlock for use as + AudioGraphNodeProperties::creationData. + + The data is encoded as XML and can be decoded by loadPluginFromProperties(). + + @param desc The plugin description to encode. + @returns A MemoryBlock containing the serialised XML data. + */ + static yup::MemoryBlock descriptionToCreationData (const yup::AudioPluginDescription& desc) + { + yup::XmlElement xml ("PluginDescription"); + xml.setAttribute ("name", desc.name); + xml.setAttribute ("vendor", desc.vendor); + xml.setAttribute ("version", desc.version); + xml.setAttribute ("identifier", desc.identifier); + xml.setAttribute ("fileOrBundlePath", desc.fileOrBundlePath); + xml.setAttribute ("isInstrument", desc.isInstrument ? 1 : 0); + xml.setAttribute ("isEffect", desc.isEffect ? 1 : 0); + xml.setAttribute ("numInputChannels", desc.numInputChannels); + xml.setAttribute ("numOutputChannels", desc.numOutputChannels); + + yup::String formatTypeStr; + switch (desc.formatType) + { + case yup::AudioPluginFormatType::vst3: + formatTypeStr = "vst3"; + break; + case yup::AudioPluginFormatType::clap: + formatTypeStr = "clap"; + break; + case yup::AudioPluginFormatType::audioUnit: + formatTypeStr = "au"; + break; + default: + formatTypeStr = "unknown"; + break; + } + xml.setAttribute ("formatType", formatTypeStr); + + yup::MemoryBlock block; + yup::MemoryOutputStream stream (block, false); + xml.writeTo (stream); + stream.flush(); + + return block; + } + + //============================================================================== + /** + Updates the stored plugin scanner and host context. + + Call this when the audio device becomes available and the real sample rate + and block size are known. + + @param s The plugin scanner to use for loading plugins. + @param ctx The updated host context. + */ + void setPluginScanner (yup::AudioPluginScanner* s, yup::AudioPluginHostContext ctx) + { + pluginScanner = s; + hostContext = ctx; + } +#endif + + //============================================================================== + /** + Returns an AudioGraphProcessor::NodeFactory that dispatches to the + registered processor factory for a given node identifier. + + If no entry is found for props.identifier, the factory returns a failure + result. + */ + yup::AudioGraphProcessor::NodeFactory makeProcessorFactory() + { + return [this] (const yup::AudioGraphNodeProperties& props) + -> yup::ResultValue> + { + auto it = entries.find (props.identifier); + if (it == entries.end()) + return yup::makeResultValueFail ("Unknown node: " + props.identifier); + + return it->second.createProcessor (props); + }; + } + + //============================================================================== + /** + Creates the visual representation for an already-constructed node. + + @param nodeID The stable graph node identifier. + @param identifier The registry key identifying the node type. + @param proc The processor that was created for this node. + @returns A new view, or nullptr if the identifier is unknown. + */ + std::unique_ptr createView (yup::AudioGraphNodeID nodeID, + const yup::String& identifier, + yup::AudioProcessor* proc) + { + auto it = entries.find (identifier); + if (it == entries.end()) + return nullptr; + + return it->second.createView (nodeID, proc); + } + + //============================================================================== + std::vector getInternalNodeIdentifiers() const + { + return { oscillatorIdentifier, gainIdentifier, lpfIdentifier }; + } + + //============================================================================== + /** + Maps a node registry identifier to a human-readable display name. + + @param id A registry identifier string. + @returns A display name such as "Oscillator", "Gain", or "Low Pass Filter", + or the original identifier if unrecognised. + */ + static yup::String identifierToDisplayName (const yup::String& id) + { + if (id == oscillatorIdentifier) + return "Oscillator"; + if (id == gainIdentifier) + return "Gain"; + if (id == lpfIdentifier) + return "Low Pass Filter"; + + return id; + } + +private: + //============================================================================== + std::map entries; + +#if YUP_WINDOWS || YUP_MAC || YUP_LINUX + yup::AudioPluginScanner* pluginScanner = nullptr; + yup::AudioPluginHostContext hostContext; + std::vector discoveredPlugins; + + //============================================================================== + /** + Decodes AudioGraphNodeProperties::creationData and loads the described plugin. + + @param props Node properties whose creationData holds serialised XML. + @returns A ResultValue containing the loaded AudioPluginInstance on success, + or an error message on failure. + */ + yup::ResultValue> loadPluginFromProperties ( + const yup::AudioGraphNodeProperties& props) + { + if (pluginScanner == nullptr) + return yup::makeResultValueFail ("No plugin scanner available"); + + yup::MemoryInputStream stream (props.creationData, false); + const yup::String xmlText = stream.readEntireStreamAsString(); + + auto xml = yup::parseXML (xmlText); + if (xml == nullptr) + return yup::makeResultValueFail ("Failed to parse plugin description XML"); + + yup::AudioPluginDescription desc; + desc.name = xml->getStringAttribute ("name"); + desc.vendor = xml->getStringAttribute ("vendor"); + desc.version = xml->getStringAttribute ("version"); + desc.identifier = xml->getStringAttribute ("identifier"); + desc.fileOrBundlePath = xml->getStringAttribute ("fileOrBundlePath"); + desc.isInstrument = xml->getBoolAttribute ("isInstrument", false); + desc.isEffect = xml->getBoolAttribute ("isEffect", false); + desc.numInputChannels = xml->getIntAttribute ("numInputChannels", 0); + desc.numOutputChannels = xml->getIntAttribute ("numOutputChannels", 0); + + const yup::String formatTypeStr = xml->getStringAttribute ("formatType"); + if (formatTypeStr == "vst3") + desc.formatType = yup::AudioPluginFormatType::vst3; + else if (formatTypeStr == "clap") + desc.formatType = yup::AudioPluginFormatType::clap; + else if (formatTypeStr == "au") + desc.formatType = yup::AudioPluginFormatType::audioUnit; + else + return yup::makeResultValueFail ("Unknown plugin format in creation data: " + formatTypeStr); + + auto* format = pluginScanner->getFormatForType (desc.formatType); + if (format == nullptr) + return yup::makeResultValueFail ("No format backend registered for type: " + formatTypeStr); + + auto result = format->loadPlugin (desc, hostContext); + if (result.failed()) + return yup::makeResultValueFail (result.getErrorMessage()); + + std::unique_ptr processor = std::move (result).getValue(); + return yup::makeResultValueOk (std::move (processor)); + } +#endif +}; diff --git a/examples/audiograph/source/nodes/NodeViewHelpers.h b/examples/audiograph/source/nodes/NodeViewHelpers.h new file mode 100644 index 000000000..1bf571659 --- /dev/null +++ b/examples/audiograph/source/nodes/NodeViewHelpers.h @@ -0,0 +1,48 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +namespace NodeViewHelpers +{ + +inline void configureParameterSlider (yup::Slider& slider, yup::Color accent) +{ + slider.setSliderType (yup::Slider::LinearBarHorizontal); + slider.setTextBoxStyle (yup::Slider::NoTextBox); + slider.setColor (yup::Slider::Style::backgroundColorId, yup::Color (0xff26282c)); + slider.setColor (yup::Slider::Style::trackColorId, accent.withAlpha (0.65f)); + slider.setColor (yup::Slider::Style::thumbColorId, accent); + slider.setColor (yup::Slider::Style::thumbOverColorId, accent.brighter (0.15f)); + slider.setColor (yup::Slider::Style::thumbDownColorId, accent.darker (0.15f)); +} + +inline yup::Rectangle getInlineSliderBounds (const yup::Component& component, int preferredWidth) +{ + const auto bounds = component.getLocalBounds(); + const auto scale = bounds.getWidth() / static_cast (preferredWidth); + return { 62.0f * scale, + 49.0f * scale, + yup::jmax (42.0f * scale, bounds.getWidth() - (150.0f * scale)), + 20.0f * scale }; +} + +} // namespace NodeViewHelpers diff --git a/examples/audiograph/source/nodes/OscillatorNode.h b/examples/audiograph/source/nodes/OscillatorNode.h new file mode 100644 index 000000000..e494c34ad --- /dev/null +++ b/examples/audiograph/source/nodes/OscillatorNode.h @@ -0,0 +1,147 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +#include +#include + +#include "NodeViewHelpers.h" + +//============================================================================== +class OscillatorProcessor final : public yup::AudioProcessor +{ +public: + OscillatorProcessor() + : AudioProcessor ("Oscillator", + yup::AudioBusLayout ({}, + { yup::AudioBus ("Main", yup::AudioBus::Audio, yup::AudioBus::Output, 2) })) + { + } + + void prepareToPlay (float newSampleRate, int) override + { + sampleRate = newSampleRate; + phase = 0.0; + } + + void releaseResources() override {} + + void processBlock (yup::AudioBuffer& audioBuffer, yup::MidiBuffer&) override + { + const auto currentFrequency = static_cast (frequency.load (std::memory_order_relaxed)); + const auto increment = yup::MathConstants::twoPi * currentFrequency / static_cast (sampleRate); + + for (int sample = 0; sample < audioBuffer.getNumSamples(); ++sample) + { + const auto value = static_cast (std::sin (phase) * 0.22); + + for (int channel = 0; channel < audioBuffer.getNumChannels(); ++channel) + audioBuffer.setSample (channel, sample, value); + + phase += increment; + if (phase >= yup::MathConstants::twoPi) + phase -= yup::MathConstants::twoPi; + } + } + + int getCurrentPreset() const noexcept override { return 0; } + + void setCurrentPreset (int) noexcept override {} + + int getNumPresets() const override { return 0; } + + yup::String getPresetName (int) const override { return {}; } + + void setPresetName (int, yup::StringRef) override {} + + yup::Result loadStateFromMemory (const yup::MemoryBlock&) override { return yup::Result::ok(); } + + yup::Result saveStateIntoMemory (yup::MemoryBlock&) override { return yup::Result::ok(); } + + bool hasEditor() const override { return false; } + + yup::AudioProcessorEditor* createEditor() override { return nullptr; } + + double getFrequency() const noexcept { return static_cast (frequency.load (std::memory_order_relaxed)); } + + void setFrequency (double newFrequency) noexcept + { + frequency.store (static_cast (yup::jlimit (40.0, 2000.0, newFrequency)), std::memory_order_relaxed); + } + +private: + double sampleRate = 44100.0; + double phase = 0.0; + std::atomic frequency { 440.0f }; +}; + +//============================================================================== +class OscillatorNodeView final : public yup::AudioGraphNodeView +{ +public: + OscillatorNodeView (yup::AudioGraphNodeID nodeID, OscillatorProcessor& processorIn) + : AudioGraphNodeView (nodeID) + , processor (processorIn) + , frequencySlider (yup::Slider::LinearBarHorizontal) + { + NodeViewHelpers::configureParameterSlider (frequencySlider, getPortKindColor (PortKind::parameter)); + frequencySlider.setRange (40.0, 2000.0); + frequencySlider.setSkewFactorFromMidpoint (440.0); + frequencySlider.setValue (processor.getFrequency(), yup::dontSendNotification); + frequencySlider.onValueChanged = [this] (double value) + { + processor.setFrequency (value); + repaint(); + }; + addAndMakeVisible (frequencySlider); + } + + yup::String getNodeTitle() const override { return "OSC"; } + + int getNumInputPorts() const override { return 0; } + + int getNumOutputPorts() const override { return 1; } + + int getPreferredWidth() const override { return 220; } + + yup::Color getNodeColor() const override { return yup::Color (0xff2563eb); } + + yup::String getNodeSubtitle() const override { return yup::String (processor.getFrequency(), 0) + " Hz"; } + + PortInfo getOutputPortInfo (int) const override { return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; } + + int getNumParameterRows() const override { return 1; } + + ParameterInfo getParameterInfo (int) const override + { + return { "Frequency", yup::String (processor.getFrequency(), 0), getPortKindColor (PortKind::parameter), -1.0f, PortKind::parameter }; + } + + void resized() override + { + frequencySlider.setBounds (NodeViewHelpers::getInlineSliderBounds (*this, getPreferredWidth())); + } + +private: + OscillatorProcessor& processor; + yup::Slider frequencySlider; +}; diff --git a/examples/audiograph/source/nodes/PluginNodeView.h b/examples/audiograph/source/nodes/PluginNodeView.h new file mode 100644 index 000000000..3ce9ea68d --- /dev/null +++ b/examples/audiograph/source/nodes/PluginNodeView.h @@ -0,0 +1,80 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +class PluginNodeView final : public yup::AudioGraphNodeView +{ +public: + PluginNodeView (yup::AudioGraphNodeID nodeID, + const yup::AudioPluginDescription& descIn) + : AudioGraphNodeView (nodeID) + , desc (descIn) + { + } + + yup::String getNodeTitle() const override + { + return desc.name.isEmpty() ? "Plugin" : desc.name; + } + + yup::String getNodeSubtitle() const override + { + return desc.vendor; + } + + int getNumInputPorts() const override + { + return desc.numInputChannels > 0 ? 1 : 0; + } + + int getNumOutputPorts() const override + { + return desc.numOutputChannels > 0 ? 1 : 0; + } + + int getPreferredWidth() const override + { + return yup::jmax (180, static_cast (desc.name.length()) * 10 + 60); + } + + yup::Color getNodeColor() const override + { + return desc.isInstrument ? yup::Color (0xffe11d48) : yup::Color (0xff0891b2); + } + + PortInfo getInputPortInfo (int) const override + { + return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; + } + + PortInfo getOutputPortInfo (int) const override + { + return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; + } + + int getNumParameterRows() const override { return 0; } + + const yup::AudioPluginDescription& getDescription() const noexcept { return desc; } + +private: + yup::AudioPluginDescription desc; +}; diff --git a/examples/audiograph/source/ui/PluginEditorWindow.h b/examples/audiograph/source/ui/PluginEditorWindow.h new file mode 100644 index 000000000..a6cad3c29 --- /dev/null +++ b/examples/audiograph/source/ui/PluginEditorWindow.h @@ -0,0 +1,66 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +#include +#include + +class PluginEditorWindow final : public yup::DocumentWindow +{ +public: + PluginEditorWindow (yup::StringRef windowTitle, + std::unique_ptr editorToOwn, + std::function onCloseCallback) + : yup::DocumentWindow (makeWindowOptions (editorToOwn.get()), yup::Color (0xff101417)) + , editor (std::move (editorToOwn)) + , onClose (std::move (onCloseCallback)) + { + setTitle (windowTitle); + addAndMakeVisible (*editor); + editor->attachedToNative(); + takeKeyboardFocus(); + } + + void resized() override + { + editor->setBounds (getLocalBounds()); + } + + void userTriedToCloseWindow() override + { + if (onClose != nullptr) + { + yup::MessageManager::callAsync (onClose); + } + } + +private: + static yup::ComponentNative::Options makeWindowOptions (yup::AudioProcessorEditor* editor) + { + return yup::ComponentNative::Options() + .withResizableWindow (editor != nullptr && editor->isResizable()) + .withRenderContinuous (editor != nullptr && editor->shouldRenderContinuous()); + } + + std::unique_ptr editor; + std::function onClose; +}; diff --git a/examples/graphics/CMakeLists.txt b/examples/graphics/CMakeLists.txt index 6c20f4435..ff70a6a15 100644 --- a/examples/graphics/CMakeLists.txt +++ b/examples/graphics/CMakeLists.txt @@ -53,13 +53,6 @@ if (YUP_PLATFORM_DESKTOP AND NOT YUP_PLATFORM_WINDOWS) set (additional_modules yup::yup_python) endif() -if (YUP_PLATFORM_DESKTOP) - list (APPEND additional_modules yup::yup_audio_plugin_host) - list (APPEND additional_definitions - YUP_AUDIO_PLUGIN_HOST_ENABLE_CLAP=1 - YUP_AUDIO_PLUGIN_HOST_ENABLE_AU=1 - YUP_AUDIO_PLUGIN_HOST_ENABLE_VST3=1) -endif() yup_standalone_app ( TARGET_NAME ${target_name} diff --git a/examples/graphics/source/examples/AudioGraphDemo.h b/examples/graphics/source/examples/AudioGraphDemo.h deleted file mode 100644 index d8580c3ea..000000000 --- a/examples/graphics/source/examples/AudioGraphDemo.h +++ /dev/null @@ -1,549 +0,0 @@ -/* - ============================================================================== - - This file is part of the YUP library. - Copyright (c) 2026 - kunitoki@gmail.com - - YUP is an open source library subject to open-source licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - to use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== -*/ - -#pragma once - -#include - -#include -#include - -//============================================================================== -class AudioGraphDemoProcessor : public yup::AudioProcessor -{ -public: - AudioGraphDemoProcessor (yup::StringRef name, yup::AudioBusLayout layout) - : AudioProcessor (name, std::move (layout)) - { - } - - int getCurrentPreset() const noexcept override { return 0; } - - void setCurrentPreset (int) noexcept override {} - - int getNumPresets() const override { return 0; } - - yup::String getPresetName (int) const override { return {}; } - - void setPresetName (int, yup::StringRef) override {} - - yup::Result loadStateFromMemory (const yup::MemoryBlock&) override { return yup::Result::ok(); } - - yup::Result saveStateIntoMemory (yup::MemoryBlock&) override { return yup::Result::ok(); } - - bool hasEditor() const override { return false; } - - yup::AudioProcessorEditor* createEditor() override { return nullptr; } -}; - -class OscillatorProcessor final : public AudioGraphDemoProcessor -{ -public: - OscillatorProcessor() - : AudioGraphDemoProcessor ("Oscillator", - yup::AudioBusLayout ({}, - { yup::AudioBus ("Main", yup::AudioBus::Audio, yup::AudioBus::Output, 2) })) - { - } - - void prepareToPlay (float newSampleRate, int) override - { - sampleRate = newSampleRate; - phase = 0.0; - } - - void releaseResources() override {} - - void processBlock (yup::AudioBuffer& audioBuffer, yup::MidiBuffer&) override - { - const auto currentFrequency = static_cast (frequency.load (std::memory_order_relaxed)); - const auto increment = yup::MathConstants::twoPi * currentFrequency / static_cast (sampleRate); - - for (int sample = 0; sample < audioBuffer.getNumSamples(); ++sample) - { - const auto value = static_cast (std::sin (phase) * 0.22); - - for (int channel = 0; channel < audioBuffer.getNumChannels(); ++channel) - audioBuffer.setSample (channel, sample, value); - - phase += increment; - if (phase >= yup::MathConstants::twoPi) - phase -= yup::MathConstants::twoPi; - } - } - - double getFrequency() const noexcept { return static_cast (frequency.load (std::memory_order_relaxed)); } - - void setFrequency (double newFrequency) noexcept - { - frequency.store (static_cast (yup::jlimit (40.0, 2000.0, newFrequency)), std::memory_order_relaxed); - } - -private: - double sampleRate = 44100.0; - double phase = 0.0; - std::atomic frequency { 440.0f }; -}; - -class GainProcessor final : public AudioGraphDemoProcessor -{ -public: - GainProcessor() - : AudioGraphDemoProcessor ("Gain", - yup::AudioBusLayout ({ yup::AudioBus ("Main", yup::AudioBus::Audio, yup::AudioBus::Input, 2) }, - { yup::AudioBus ("Main", yup::AudioBus::Audio, yup::AudioBus::Output, 2) })) - { - } - - void prepareToPlay (float, int) override {} - - void releaseResources() override {} - - void processBlock (yup::AudioBuffer& audioBuffer, yup::MidiBuffer&) override - { - audioBuffer.applyGain (gain.load (std::memory_order_relaxed)); - } - - float getGain() const noexcept { return gain.load (std::memory_order_relaxed); } - - void setGain (float newGain) noexcept - { - gain.store (yup::jlimit (0.0f, 1.5f, newGain), std::memory_order_relaxed); - } - -private: - std::atomic gain { 0.75f }; -}; - -class LowPassFilterProcessor final : public AudioGraphDemoProcessor -{ -public: - LowPassFilterProcessor() - : AudioGraphDemoProcessor ("Low-pass", - yup::AudioBusLayout ({ yup::AudioBus ("Main", yup::AudioBus::Audio, yup::AudioBus::Input, 2) }, - { yup::AudioBus ("Main", yup::AudioBus::Audio, yup::AudioBus::Output, 2) })) - { - } - - void prepareToPlay (float newSampleRate, int) override - { - sampleRate = newSampleRate; - state[0] = 0.0f; - state[1] = 0.0f; - } - - void releaseResources() override {} - - void processBlock (yup::AudioBuffer& audioBuffer, yup::MidiBuffer&) override - { - const auto currentCutoff = static_cast (cutoff.load (std::memory_order_relaxed)); - const auto alpha = static_cast (1.0 - std::exp (-yup::MathConstants::twoPi * currentCutoff / static_cast (sampleRate))); - - for (int channel = 0; channel < audioBuffer.getNumChannels(); ++channel) - { - auto y = state[static_cast (yup::jmin (channel, 1))]; - - for (int sample = 0; sample < audioBuffer.getNumSamples(); ++sample) - { - y += alpha * (audioBuffer.getSample (channel, sample) - y); - audioBuffer.setSample (channel, sample, y); - } - - state[static_cast (yup::jmin (channel, 1))] = y; - } - } - - double getCutoff() const noexcept { return static_cast (cutoff.load (std::memory_order_relaxed)); } - - void setCutoff (double newCutoff) noexcept - { - cutoff.store (static_cast (yup::jlimit (80.0, 12000.0, newCutoff)), std::memory_order_relaxed); - } - -private: - float sampleRate = 44100.0f; - std::atomic cutoff { 800.0f }; - float state[2] {}; -}; - -namespace -{ -void configureParameterSlider (yup::Slider& slider, yup::Color accent) -{ - slider.setSliderType (yup::Slider::LinearBarHorizontal); - slider.setTextBoxStyle (yup::Slider::NoTextBox); - slider.setColor (yup::Slider::Style::backgroundColorId, yup::Color (0xff26282c)); - slider.setColor (yup::Slider::Style::trackColorId, accent.withAlpha (0.65f)); - slider.setColor (yup::Slider::Style::thumbColorId, accent); - slider.setColor (yup::Slider::Style::thumbOverColorId, accent.brighter (0.15f)); - slider.setColor (yup::Slider::Style::thumbDownColorId, accent.darker (0.15f)); -} - -yup::Rectangle getInlineSliderBounds (const yup::Component& component, int preferredWidth) -{ - const auto bounds = component.getLocalBounds(); - const auto scale = bounds.getWidth() / static_cast (preferredWidth); - return { 62.0f * scale, - 49.0f * scale, - yup::jmax (42.0f * scale, bounds.getWidth() - (150.0f * scale)), - 20.0f * scale }; -} -} // namespace - -//============================================================================== -class OscillatorNodeView final : public yup::AudioGraphNodeView -{ -public: - OscillatorNodeView (yup::AudioGraphNodeID nodeID, OscillatorProcessor& processorIn) - : AudioGraphNodeView (nodeID) - , processor (processorIn) - , frequencySlider (yup::Slider::LinearBarHorizontal) - { - configureParameterSlider (frequencySlider, getPortKindColor (PortKind::parameter)); - frequencySlider.setRange (40.0, 2000.0); - frequencySlider.setSkewFactorFromMidpoint (440.0); - frequencySlider.setValue (processor.getFrequency(), yup::dontSendNotification); - frequencySlider.onValueChanged = [this] (double value) - { - processor.setFrequency (value); - repaint(); - }; - addAndMakeVisible (frequencySlider); - } - - yup::String getNodeTitle() const override { return "OSC"; } - - int getNumInputPorts() const override { return 0; } - - int getNumOutputPorts() const override { return 1; } - - int getPreferredWidth() const override { return 220; } - - yup::Color getNodeColor() const override { return yup::Color (0xff2563eb); } - - yup::String getNodeSubtitle() const override { return yup::String (processor.getFrequency(), 0) + " Hz"; } - - PortInfo getOutputPortInfo (int) const override { return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; } - - int getNumParameterRows() const override { return 1; } - - ParameterInfo getParameterInfo (int) const override - { - return { "Frequency", yup::String (processor.getFrequency(), 0), getPortKindColor (PortKind::parameter), -1.0f, PortKind::parameter }; - } - - void resized() override - { - frequencySlider.setBounds (getInlineSliderBounds (*this, getPreferredWidth())); - } - -private: - OscillatorProcessor& processor; - yup::Slider frequencySlider; -}; - -class GainNodeView final : public yup::AudioGraphNodeView -{ -public: - GainNodeView (yup::AudioGraphNodeID nodeID, GainProcessor& processorIn) - : AudioGraphNodeView (nodeID) - , processor (processorIn) - , gainSlider (yup::Slider::LinearBarHorizontal) - { - configureParameterSlider (gainSlider, getPortKindColor (PortKind::parameter)); - gainSlider.setRange (0.0, 1.5); - gainSlider.setValue (processor.getGain(), yup::dontSendNotification); - gainSlider.onValueChanged = [this] (double value) - { - processor.setGain (static_cast (value)); - repaint(); - }; - addAndMakeVisible (gainSlider); - } - - yup::String getNodeTitle() const override { return "GAIN"; } - - int getNumInputPorts() const override { return 1; } - - int getNumOutputPorts() const override { return 1; } - - int getPreferredWidth() const override { return 220; } - - yup::Color getNodeColor() const override { return yup::Color (0xff10b981); } - - yup::String getNodeSubtitle() const override { return yup::String ("x ") + yup::String (processor.getGain(), 2); } - - PortInfo getInputPortInfo (int) const override { return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; } - - PortInfo getOutputPortInfo (int) const override { return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; } - - int getNumParameterRows() const override { return 1; } - - ParameterInfo getParameterInfo (int) const override - { - return { "Gain", yup::String (processor.getGain(), 2), getPortKindColor (PortKind::parameter), -1.0f, PortKind::parameter }; - } - - void resized() override - { - gainSlider.setBounds (getInlineSliderBounds (*this, getPreferredWidth())); - } - -private: - GainProcessor& processor; - yup::Slider gainSlider; -}; - -class LowPassFilterNodeView final : public yup::AudioGraphNodeView -{ -public: - LowPassFilterNodeView (yup::AudioGraphNodeID nodeID, LowPassFilterProcessor& processorIn) - : AudioGraphNodeView (nodeID) - , processor (processorIn) - , cutoffSlider (yup::Slider::LinearBarHorizontal) - { - configureParameterSlider (cutoffSlider, getPortKindColor (PortKind::parameter)); - cutoffSlider.setRange (80.0, 12000.0); - cutoffSlider.setSkewFactorFromMidpoint (1000.0); - cutoffSlider.setValue (processor.getCutoff(), yup::dontSendNotification); - cutoffSlider.onValueChanged = [this] (double value) - { - processor.setCutoff (value); - repaint(); - }; - addAndMakeVisible (cutoffSlider); - } - - yup::String getNodeTitle() const override { return "LPF"; } - - int getNumInputPorts() const override { return 1; } - - int getNumOutputPorts() const override { return 1; } - - int getPreferredWidth() const override { return 220; } - - yup::Color getNodeColor() const override { return yup::Color (0xff7c3aed); } - - yup::String getNodeSubtitle() const override { return yup::String (processor.getCutoff(), 0) + " Hz"; } - - PortInfo getInputPortInfo (int) const override { return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; } - - PortInfo getOutputPortInfo (int) const override { return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; } - - int getNumParameterRows() const override { return 1; } - - ParameterInfo getParameterInfo (int) const override - { - return { "Cutoff", yup::String (processor.getCutoff(), 0), getPortKindColor (PortKind::parameter), -1.0f, PortKind::parameter }; - } - - void resized() override - { - cutoffSlider.setBounds (getInlineSliderBounds (*this, getPreferredWidth())); - } - -private: - LowPassFilterProcessor& processor; - yup::Slider cutoffSlider; -}; - -class SoundCardInputNodeView final : public yup::AudioGraphNodeView -{ -public: - SoundCardInputNodeView() - : AudioGraphNodeView (yup::AudioGraphNodeID::invalid()) - { - } - - yup::String getNodeTitle() const override { return "INPUT"; } - - yup::String getNodeSubtitle() const override { return "sound card"; } - - int getNumInputPorts() const override { return 0; } - - int getNumOutputPorts() const override { return 1; } - - int getPreferredWidth() const override { return 150; } - - yup::Color getNodeColor() const override { return yup::Color (0xfff97316); } - - PortInfo getOutputPortInfo (int) const override { return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; } -}; - -class SoundCardOutputNodeView final : public yup::AudioGraphNodeView -{ -public: - SoundCardOutputNodeView() - : AudioGraphNodeView (yup::AudioGraphNodeID::invalid()) - { - } - - yup::String getNodeTitle() const override { return "OUTPUT"; } - - yup::String getNodeSubtitle() const override { return "sound card"; } - - int getNumInputPorts() const override { return 1; } - - int getNumOutputPorts() const override { return 0; } - - int getPreferredWidth() const override { return 150; } - - yup::Color getNodeColor() const override { return yup::Color (0xff06b6d4); } - - PortInfo getInputPortInfo (int) const override { return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; } -}; - -//============================================================================== -class AudioGraphDemo final - : public yup::Component - , public yup::AudioIODeviceCallback -{ -public: - AudioGraphDemo() - { - deviceManager.initialiseWithDefaultDevices (2, 2); - - graph = std::make_shared(); - - auto oscillator = std::make_unique(); - auto* oscillatorProcessor = oscillator.get(); - const auto oscillatorID = graph->addNode (std::move (oscillator)); - - auto gain = std::make_unique(); - auto* gainProcessor = gain.get(); - const auto gainID = graph->addNode (std::move (gain)); - - auto filter = std::make_unique(); - auto* filterProcessor = filter.get(); - const auto filterID = graph->addNode (std::move (filter)); - - graph->addConnection ({ yup::AudioGraphEndpoint::graphInput (0), - yup::AudioGraphEndpoint::nodeInput (gainID, 0) }); - graph->addConnection ({ yup::AudioGraphEndpoint::nodeOutput (oscillatorID, 0), - yup::AudioGraphEndpoint::nodeInput (gainID, 0) }); - graph->addConnection ({ yup::AudioGraphEndpoint::nodeOutput (gainID, 0), - yup::AudioGraphEndpoint::nodeInput (filterID, 0) }); - graph->addConnection ({ yup::AudioGraphEndpoint::nodeOutput (filterID, 0), - yup::AudioGraphEndpoint::graphOutput (0) }); - graph->commitChanges(); - - graphComponent = std::make_unique (graph); - addAndMakeVisible (*graphComponent); - - graphComponent->setGraphInputView (std::make_unique(), { 40.0f, 210.0f }); - graphComponent->addNodeView (oscillatorID, std::make_unique (oscillatorID, *oscillatorProcessor), { 70.0f, 80.0f }); - graphComponent->addNodeView (gainID, std::make_unique (gainID, *gainProcessor), { 300.0f, 145.0f }); - graphComponent->addNodeView (filterID, std::make_unique (filterID, *filterProcessor), { 530.0f, 145.0f }); - graphComponent->setGraphOutputView (std::make_unique(), { 760.0f, 145.0f }); - graphComponent->zoomToFitNodes(); - } - - ~AudioGraphDemo() override - { - removeAudioCallback(); - deviceManager.closeAudioDevice(); - } - - void resized() override - { - if (graphComponent != nullptr) - graphComponent->setBounds (getLocalBounds()); - } - - void paint (yup::Graphics& g) override - { - g.setFillColor (yup::Color (0xff101522)); - g.fillAll(); - } - - void visibilityChanged() override - { - if (isVisible()) - addAudioCallback(); - else - removeAudioCallback(); - } - - void audioDeviceIOCallbackWithContext (const float* const* inputChannelData, - int numInputChannels, - float* const* outputChannelData, - int numOutputChannels, - int numSamples, - const yup::AudioIODeviceCallbackContext&) override - { - for (int channel = 0; channel < numOutputChannels; ++channel) - { - if (outputChannelData[channel] != nullptr) - yup::FloatVectorOperations::clear (outputChannelData[channel], numSamples); - } - - if (graph == nullptr || numOutputChannels <= 0 || numSamples <= 0) - return; - - yup::AudioBuffer outputBuffer (outputChannelData, numOutputChannels, numSamples); - const auto channelsToCopy = yup::jmin (numInputChannels, numOutputChannels); - - for (int channel = 0; channel < channelsToCopy; ++channel) - { - if (inputChannelData != nullptr && inputChannelData[channel] != nullptr && outputChannelData[channel] != nullptr) - yup::FloatVectorOperations::copy (outputChannelData[channel], inputChannelData[channel], numSamples); - } - - yup::MidiBuffer midi; - graph->processBlock (outputBuffer, midi); - } - - void audioDeviceAboutToStart (yup::AudioIODevice* device) override - { - if (graph != nullptr && device != nullptr) - graph->prepareToPlay (static_cast (device->getCurrentSampleRate()), device->getCurrentBufferSizeSamples()); - } - - void audioDeviceStopped() override - { - if (graph != nullptr) - graph->releaseResources(); - } - -private: - void addAudioCallback() - { - if (! audioCallbackRegistered) - { - deviceManager.addAudioCallback (this); - audioCallbackRegistered = true; - } - } - - void removeAudioCallback() - { - if (audioCallbackRegistered) - { - deviceManager.removeAudioCallback (this); - audioCallbackRegistered = false; - } - } - - yup::AudioDeviceManager deviceManager; - std::shared_ptr graph; - std::unique_ptr graphComponent; - bool audioCallbackRegistered = false; -}; diff --git a/examples/graphics/source/examples/PluginHost.h b/examples/graphics/source/examples/PluginHost.h deleted file mode 100644 index abf22b1e2..000000000 --- a/examples/graphics/source/examples/PluginHost.h +++ /dev/null @@ -1,1055 +0,0 @@ -/* - ============================================================================== - - This file is part of the YUP library. - Copyright (c) 2026 - kunitoki@gmail.com - - YUP is an open source library subject to open-source licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - to use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== -*/ - -#pragma once - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -//============================================================================== - -/** - Demonstrates scanning for audio plugins, listing results, loading one, - and running a drum loop through it — similar to CrossoverDemo. -*/ -class PluginHostDemo : public yup::Component - , public yup::AudioIODeviceCallback - , public yup::Timer -{ - class PluginEditorWindow : public yup::DocumentWindow - { - public: - PluginEditorWindow (yup::StringRef windowTitle, - yup::AudioProcessorEditor* editorToOwn, - std::function onCloseCallback) - : yup::DocumentWindow (makeWindowOptions (editorToOwn), yup::Color (0xff101417)) - , editor (editorToOwn) - , onClose (std::move (onCloseCallback)) - { - setTitle (windowTitle); - - addAndMakeVisible (*editor); - // DocumentWindow is already native here, so late children need this hook manually. - editor->attachedToNative(); - takeKeyboardFocus(); - } - - void resized() override - { - editor->setBounds (getLocalBounds()); - } - - void userTriedToCloseWindow() override - { - if (onClose != nullptr) - yup::MessageManager::callAsync (onClose); - } - - private: - static yup::ComponentNative::Options makeWindowOptions (yup::AudioProcessorEditor* editor) - { - return yup::ComponentNative::Options() - .withResizableWindow (editor != nullptr && editor->isResizable()) - .withRenderContinuous (editor != nullptr && editor->shouldRenderContinuous()); - } - - std::unique_ptr editor; - std::function onClose; - }; - - struct ParameterRow : public yup::Component - { - ParameterRow() - : slider (yup::Slider::LinearHorizontal) - { - setOpaque (true); - setWantsMouseEvents (false, true); - - const auto font = yup::ApplicationTheme::getGlobalTheme()->getDefaultFont().withHeight (12.0f); - nameLabel.setFont (font); - valueLabel.setFont (font); - valueLabel.setJustification (yup::Justification::right); - - nameLabel.setColor (yup::Label::Style::textFillColorId, yup::Color (0xffe8ecef)); - valueLabel.setColor (yup::Label::Style::textFillColorId, yup::Color (0xffb7c1c8)); - - slider.setColor (yup::Slider::Style::backgroundColorId, yup::Color (0xff343b40)); - slider.setColor (yup::Slider::Style::trackColorId, yup::Color (0xff6d7d88)); - slider.setColor (yup::Slider::Style::thumbColorId, yup::Color (0xffe0a84d)); - slider.setColor (yup::Slider::Style::thumbOverColorId, yup::Color (0xfff0ba60)); - slider.setColor (yup::Slider::Style::thumbDownColorId, yup::Color (0xffc78f36)); - - addAndMakeVisible (nameLabel); - addAndMakeVisible (valueLabel); - addAndMakeVisible (slider); - - slider.onDragStart = [this] (const yup::MouseEvent&) - { - if (parameter != nullptr) - parameter->beginChangeGesture(); - }; - - slider.onValueChanged = [this] (double value) - { - if (parameter == nullptr) - return; - - parameter->setValueNotifyingHost (static_cast (value)); - updateValueLabel(); - }; - - slider.onDragEnd = [this] (const yup::MouseEvent&) - { - if (parameter != nullptr) - parameter->endChangeGesture(); - }; - } - - void setParameter (yup::AudioParameter::Ptr newParameter) - { - parameter = std::move (newParameter); - - if (parameter == nullptr) - { - nameLabel.setText ({}, yup::dontSendNotification); - valueLabel.setText ({}, yup::dontSendNotification); - return; - } - - nameLabel.setText (parameter->getName(), yup::dontSendNotification); - slider.setRange (static_cast (parameter->getMinimumValue()), - static_cast (parameter->getMaximumValue())); - slider.setDefaultValue (static_cast (parameter->getDefaultValue())); - refreshValue(); - } - - void refreshValue() - { - if (parameter == nullptr) - return; - - slider.setValue (static_cast (parameter->getValue()), yup::dontSendNotification); - updateValueLabel(); - } - - void paint (yup::Graphics& g) override - { - g.setFillColor (yup::Color (0xff20262a)); - g.fillAll(); - - g.setStrokeColor (yup::Color (0xff31383e)); - g.setStrokeWidth (1.0f); - g.strokeLine (0.0f, getHeight() - 0.5f, getWidth(), getHeight() - 0.5f); - } - - void resized() override - { - auto bounds = getLocalBounds().reduced (8, 4); - auto labelArea = bounds.removeFromLeft (yup::jmin (180.0f, bounds.getWidth() * 0.35f)); - nameLabel.setBounds (labelArea.removeFromTop (bounds.getHeight())); - - bounds.removeFromLeft (8); - valueLabel.setBounds (bounds.removeFromRight (84)); - bounds.removeFromRight (8); - slider.setBounds (bounds); - } - - private: - void updateValueLabel() - { - if (parameter != nullptr) - valueLabel.setText (parameter->convertToString (parameter->getValue()), yup::dontSendNotification); - } - - yup::AudioParameter::Ptr parameter; - yup::Label nameLabel; - yup::Label valueLabel; - yup::Slider slider; - }; - - struct ParameterListModel : public yup::ListBoxModel - { - int getNumRows() override - { - return static_cast (parameters.size()); - } - - yup::Component* refreshComponentForRow (int rowIndex, yup::Component* existingComponent) override - { - auto* row = dynamic_cast (existingComponent); - if (row == nullptr) - row = new ParameterRow(); - - if (rowIndex >= 0 && rowIndex < static_cast (parameters.size())) - row->setParameter (parameters[static_cast (rowIndex)]); - else - row->setParameter (nullptr); - - return row; - } - - void setParameters (std::vector newParameters) - { - parameters = std::move (newParameters); - } - - void clear() - { - parameters.clear(); - } - - private: - std::vector parameters; - }; - -public: - PluginHostDemo() - : dryWetSlider (yup::Slider::LinearHorizontal) - { - // Load the drum loop audio file - loadAudioFile(); - - // Audio device manager - audioDeviceManager.initialiseWithDefaultDevices (0, 2); - - // Initialize the plugin scanner - scanner = std::make_unique(); - - // Register available format backends -#if YUP_AUDIO_PLUGIN_HOST_ENABLE_VST3 - scanner->addFormat (std::make_unique()); -#endif -#if YUP_AUDIO_PLUGIN_HOST_ENABLE_CLAP - scanner->addFormat (std::make_unique()); -#endif -#if YUP_AUDIO_PLUGIN_HOST_ENABLE_AU && YUP_MAC - scanner->addFormat (std::make_unique()); -#endif - - // Build UI - createUI(); - - startTimerHz (20); - } - - ~PluginHostDemo() override - { - scanLifetime->store (false); - if (scanner != nullptr) - scanner->cancelPendingScans(); - - audioDeviceManager.removeAudioCallback (this); - audioDeviceManager.closeAudioDevice(); - unloadPlugin(); - cleanupRetiredPlugins (true); - } - - void resized() override - { - auto bounds = getLocalBounds().reduced (8); - - // Top bar: scan button and status - auto topBar = bounds.removeFromTop (32); - scanButton.setBounds (topBar.removeFromLeft (140)); - topBar.removeFromLeft (8); - statusLabel.setBounds (topBar); - - bounds.removeFromTop (8); - - // Plugin list takes left portion - auto listArea = bounds.removeFromLeft (220); - scanResultsLabel.setBounds (listArea.removeFromTop (22)); - pluginList.setBounds (listArea.reduced (0, 4)); - - bounds.removeFromLeft (8); - - // Plugin info + load/unload controls - auto infoArea = bounds.removeFromTop (126); - - auto infoLeft = infoArea.removeFromLeft (infoArea.getWidth() / 2); - nameLabel.setBounds (infoLeft.removeFromTop (20)); - vendorLabel.setBounds (infoLeft.removeFromTop (18)); - formatLabel.setBounds (infoLeft.removeFromTop (18)); - infoLeft.removeFromTop (8); - presetLabel.setBounds (infoLeft.removeFromTop (18)); - presetCombo.setBounds (infoLeft.removeFromTop (28).reduced (0, 2)); - - auto infoRight = infoArea; - infoRight.removeFromTop (4); - loadButton.setBounds (infoRight.removeFromTop (28).reduced (2, 0)); - infoRight.removeFromTop (6); - unloadButton.setBounds (infoRight.removeFromTop (28).reduced (2, 0)); - infoRight.removeFromTop (6); - openEditorButton.setBounds (infoRight.removeFromTop (28).reduced (2, 0)); - infoRight.removeFromTop (6); - dryWetLabel.setBounds (infoRight.removeFromTop (20)); - - bounds.removeFromTop (8); - - // Dry/wet slider - auto sliderRow = bounds.removeFromTop (36); - dryWetSlider.setBounds (sliderRow); - - bounds.removeFromTop (8); - - // Parameter controls area (generic fallback UI) - parameterList.setBounds (bounds); - } - - void paint (yup::Graphics& g) override - { - g.setFillColor (yup::Color (0xff1b2024)); - g.fillAll(); - } - - void visibilityChanged() override - { - if (! isVisible()) - audioDeviceManager.removeAudioCallback (this); - else - audioDeviceManager.addAudioCallback (this); - } - - //============================================================================== - // AudioIODeviceCallback - - void audioDeviceIOCallbackWithContext (const float* const* inputChannelData, - int numInputChannels, - float* const* outputChannelData, - int numOutputChannels, - int numSamples, - const yup::AudioIODeviceCallbackContext& context) override - { - // Zero outputs - for (int ch = 0; ch < numOutputChannels; ++ch) - yup::FloatVectorOperations::clear (outputChannelData[ch], numSamples); - - // Read from drum loop buffer into a temp buffer for processing - for (int i = 0; i < numSamples; ++i) - { - // Read drum loop (looping) - float sample = 0.0f; - if (audioBufferLoaded) - { - const int numChannels = audioBuffer.getNumChannels(); - const int totalSamples = audioBuffer.getNumSamples(); - - for (int ch = 0; ch < yup::jmin (2, numChannels); ++ch) - sample += audioBuffer.getSample (ch, readPosition) * 0.5f; - sample /= yup::jmin (2, numChannels); - - readPosition++; - if (readPosition >= totalSamples) - readPosition = 0; - } - - // Write as stereo - processBuffer.setSample (0, i, sample); - processBuffer.setSample (1, i, sample); - dryBuffer.setSample (0, i, sample); - dryBuffer.setSample (1, i, sample); - } - - // If a plugin is loaded, process through it - yup::MidiBuffer midi; - - if (auto plugin = getLoadedPlugin()) - plugin->processBlock (processBuffer, midi); - - // Mix dry/wet into output - const float wet = dryWetMix.getNextValue(); - const float dry = 1.0f - wet; - - for (int i = 0; i < numSamples; ++i) - { - for (int ch = 0; ch < yup::jmin (2, numOutputChannels); ++ch) - { - outputChannelData[ch][i] = dryBuffer.getSample (ch, i) * dry - + processBuffer.getSample (ch, i) * wet; - } - } - } - - void audioDeviceAboutToStart (yup::AudioIODevice* device) override - { - const auto sr = device->getCurrentSampleRate(); - const auto bs = device->getCurrentBufferSizeSamples(); - - readPosition = 0; - dryWetMix.reset (sr, 0.02); - - processBuffer.setSize (2, bs); - dryBuffer.setSize (2, bs); - - if (auto plugin = getLoadedPlugin()) - plugin->prepareToPlay (sr, bs); - } - - void audioDeviceStopped() override - { - if (auto plugin = getLoadedPlugin()) - plugin->releaseResources(); - } - - //============================================================================== - // Timer - - void timerCallback() override - { - updateStatusLine(); - cleanupRetiredPlugins(); - - // Update visible parameter rows from plugin values - if (getLoadedPlugin() != nullptr) - refreshVisibleParameterRows(); - } - -private: - //============================================================================== - - void loadAudioFile() - { -#if YUP_WASM - auto dataDir = yup::File ("/data"); -#else - auto dataDir = yup::File (__FILE__) - .getParentDirectory() - .getParentDirectory() - .getParentDirectory() - .getChildFile ("data"); -#endif - - yup::File audioFile = dataDir.getChildFile ("break_boomblastic_92bpm.mp3"); - if (! audioFile.existsAsFile()) - { - statusText = "Could not find drum loop file"; - return; - } - - yup::AudioFormatManager formatManager; - formatManager.registerDefaultFormats(); - - if (auto reader = formatManager.createReaderFor (audioFile)) - { - audioBuffer.setSize ((int) reader->numChannels, (int) reader->lengthInSamples); - reader->read (&audioBuffer, 0, (int) reader->lengthInSamples, 0, true, true); - audioBufferLoaded = true; - - std::cout << "Loaded: " << audioFile.getFileName() << std::endl; - std::cout << "Sample rate: " << reader->sampleRate << " Hz, " - << reader->numChannels << " ch, " - << reader->lengthInSamples << " samples" << std::endl; - } - else - { - statusText = "Failed to read drum loop"; - } - } - - void createUI() - { - setOpaque (false); - - // Scan button - scanButton.setButtonText ("Scan for Plugins"); - scanButton.onClick = [this] - { - performScan(); - }; - addAndMakeVisible (scanButton); - - // Status label - statusLabel.setFont (yup::ApplicationTheme::getGlobalTheme()->getDefaultFont().withHeight (12.0f)); - statusLabel.setText ("Ready. Click 'Scan for Plugins' to begin.", yup::dontSendNotification); - addAndMakeVisible (statusLabel); - - // Scan results label - scanResultsLabel.setFont (yup::ApplicationTheme::getGlobalTheme()->getDefaultFont().withHeight (12.0f)); - scanResultsLabel.setText ("Plugins (0 found)", yup::dontSendNotification); - addAndMakeVisible (scanResultsLabel); - - // Plugin list box - pluginList.setRowHeight (24); - pluginList.setModel (&pluginListModel); - pluginListModel.onSelectedRowChanged = [this] (int row) - { - if (row >= 0 && row < static_cast (discoveredPlugins.size())) - { - selectedPluginIndex = row; - updateInfoForSelectedPlugin(); - } - }; - addAndMakeVisible (pluginList); - - // Info labels - auto infoFont = yup::ApplicationTheme::getGlobalTheme()->getDefaultFont().withHeight (13.0f); - nameLabel.setFont (infoFont); - nameLabel.setText ("No plugin selected", yup::dontSendNotification); - addAndMakeVisible (nameLabel); - - vendorLabel.setFont (infoFont); - vendorLabel.setText ("", yup::dontSendNotification); - addAndMakeVisible (vendorLabel); - - formatLabel.setFont (infoFont); - formatLabel.setText ("", yup::dontSendNotification); - addAndMakeVisible (formatLabel); - - // Load / Unload buttons - loadButton.setButtonText ("Load Plugin"); - loadButton.onClick = [this] - { - loadSelectedPlugin(); - }; - addAndMakeVisible (loadButton); - - unloadButton.setButtonText ("Unload Plugin"); - unloadButton.onClick = [this] - { - unloadPlugin(); - }; - addAndMakeVisible (unloadButton); - - openEditorButton.setButtonText ("Open Editor"); - openEditorButton.onClick = [this] - { - openPluginEditor(); - }; - openEditorButton.setEnabled (false); - addAndMakeVisible (openEditorButton); - - presetLabel.setFont (infoFont); - presetLabel.setText ("Preset", yup::dontSendNotification); - addAndMakeVisible (presetLabel); - - presetCombo.setTextWhenNothingSelected ("No presets"); - presetCombo.setEnabled (false); - presetCombo.onSelectedItemChanged = [this] - { - changeSelectedPreset(); - }; - addAndMakeVisible (presetCombo); - - // Dry/Wet - dryWetLabel.setFont (infoFont); - dryWetLabel.setText ("Dry / Wet Mix", yup::dontSendNotification); - addAndMakeVisible (dryWetLabel); - - dryWetSlider.setRange (0.0, 1.0); - dryWetSlider.setValue (0.5); - dryWetSlider.onValueChanged = [this] (double value) - { - dryWetMix.setTargetValue ((float) value); - }; - addAndMakeVisible (dryWetSlider); - - dryWetMix.setCurrentAndTargetValue (0.5f); - - // Parameter list area - parameterList.setRowHeight (40); - parameterList.setSelectionMode (yup::ListBox::SelectionMode::none); - parameterList.setColor (yup::ListBox::Style::backgroundColorId, yup::Color (0xff20262a)); - parameterList.setColor (yup::ListBox::Style::outlineColorId, yup::Color (0xff31383e)); - parameterList.setColor (yup::ListBox::Style::rowBackgroundColorId, yup::Color (0xff20262a)); - parameterList.setColor (yup::ListBox::Style::selectedRowBackgroundColorId, yup::Color (0xff20262a)); - parameterList.setColor (yup::ListBox::Style::hoveredRowBackgroundColorId, yup::Color (0xff242b30)); - parameterList.setModel (¶meterListModel); - addAndMakeVisible (parameterList); - parameterList.setVisible (false); - } - - void performScan() - { - if (scanInProgress.exchange (true)) - { - statusText = "Scan already in progress."; - return; - } - - discoveredPlugins.clear(); - pluginListModel.clear(); - pluginList.updateContent(); - selectedPluginIndex = -1; - clearPresetCombo(); - updateEditorButtonState(); - - if (scanner == nullptr || scanner->getNumFormats() == 0) - { - scanInProgress = false; - statusText = "No plugin formats registered."; - return; - } - - statusText = "Scanning..."; - scanButton.setEnabled (false); - scanResultsLabel.setText ("Plugins (scanning...)", yup::dontSendNotification); - - const auto lifetime = scanLifetime; - scanner->scanDefaultsAsync ([this, lifetime] (yup::AudioPluginScanner::ScanResult result) mutable - { - if (! lifetime->load()) - return; - - yup::MessageManager::callAsync ([this, lifetime, result = std::move (result)]() mutable - { - if (! lifetime->load()) - return; - - handleScanFinished (std::move (result)); - }); - }); - } - - void handleScanFinished (yup::AudioPluginScanner::ScanResult result) - { - discoveredPlugins = std::move (result.discovered); - statusText = yup::String ("Found ") + yup::String (discoveredPlugins.size()) + " plugins."; - - if (! result.failedPaths.empty()) - statusText += " (" + yup::String ((int) result.failedPaths.size()) + " failed)"; - - // Populate list model - yup::Array names; - for (const auto& desc : discoveredPlugins) - names.add (desc.name + " [" + formatTypeToString (desc.formatType) + "]"); - - pluginListModel.setItems (std::move (names)); - pluginList.updateContent(); - scanResultsLabel.setText ("Plugins (" + yup::String (discoveredPlugins.size()) + " found)", - yup::dontSendNotification); - scanButton.setEnabled (true); - scanInProgress = false; - } - - void updateInfoForSelectedPlugin() - { - if (selectedPluginIndex < 0 || selectedPluginIndex >= static_cast (discoveredPlugins.size())) - return; - - const auto& desc = discoveredPlugins[static_cast (selectedPluginIndex)]; - - nameLabel.setText (desc.name, yup::dontSendNotification); - vendorLabel.setText ("Vendor: " + desc.vendor, yup::dontSendNotification); - - auto formatText = "Format: " + formatTypeToString (desc.formatType); - if (desc.numInputChannels > 0 || desc.numOutputChannels > 0) - { - formatText += " | " + yup::String (desc.numInputChannels) + " in / " - + yup::String (desc.numOutputChannels) + " out"; - } - - formatLabel.setText (formatText, yup::dontSendNotification); - } - - void loadSelectedPlugin() - { - if (selectedPluginIndex < 0 || selectedPluginIndex >= static_cast (discoveredPlugins.size())) - { - statusText = "No plugin selected."; - return; - } - - const auto& desc = discoveredPlugins[static_cast (selectedPluginIndex)]; - - // Find the right format backend - auto* format = scanner->getFormatForType (desc.formatType); - if (format == nullptr) - { - statusText = "No format backend for: " + formatTypeToString (desc.formatType); - return; - } - - yup::AudioPluginHostContext hostCtx; - hostCtx.sampleRate = audioDeviceManager.getCurrentAudioDevice() - ? audioDeviceManager.getCurrentAudioDevice()->getCurrentSampleRate() - : 44100.0f; - hostCtx.maxBlockSize = audioDeviceManager.getCurrentAudioDevice() - ? audioDeviceManager.getCurrentAudioDevice()->getCurrentBufferSizeSamples() - : 512; - - auto result = format->loadPlugin (desc, hostCtx); - - if (result.failed()) - { - statusText = "Failed to load: " + result.getErrorMessage(); - return; - } - - closePluginEditor(); - clearParameterSliders(); - clearPresetCombo(); - - auto plugin = std::shared_ptr (std::move (result).getValue()); - plugin->prepareToPlay (hostCtx.sampleRate, hostCtx.maxBlockSize); - retirePlugin (std::atomic_exchange (&loadedPlugin, std::move (plugin))); - - buildParameterSliders(); - populatePresetCombo(); - updateEditorButtonState(); - - if (statusText.isEmpty() || ! statusText.startsWith ("Loaded: ")) - statusText = "Loaded: " + desc.name; - } - - void unloadPlugin() - { - closePluginEditor(); - clearParameterSliders(); - clearPresetCombo(); - - unloadPluginInternal(); - - updateEditorButtonState(); - statusText = "Plugin unloaded."; - } - - void unloadPluginInternal() - { - retirePlugin (std::atomic_exchange (&loadedPlugin, std::shared_ptr())); - } - - void openPluginEditor() - { - if (pluginEditorWindow != nullptr) - { - pluginEditorWindow->toFront (true); - return; - } - - yup::AudioProcessorEditor* editor = nullptr; - yup::String pluginName; - - auto plugin = getLoadedPlugin(); - - if (plugin == nullptr) - { - statusText = "No plugin loaded."; - return; - } - - if (! plugin->hasEditor()) - { - statusText = "Loaded plugin has no editor."; - return; - } - - pluginName = plugin->getDescription().name; - editor = plugin->createEditor(); - - if (editor == nullptr) - { - statusText = "Could not create plugin editor."; - updateEditorButtonState(); - return; - } - - const auto preferredSize = editor->getPreferredSize(); - yup::WeakReference weakThis (this); - - pluginEditorWindow = std::make_unique ( - pluginName, - editor, - [weakThis] - { - if (auto* component = weakThis.get()) - if (auto* demo = dynamic_cast (component)) - demo->closePluginEditor(); - }); - - pluginEditorWindow->centreWithSize (preferredSize); - pluginEditorWindow->setVisible (true); - pluginEditorWindow->toFront (true); - - updateEditorButtonState(); - } - - void closePluginEditor() - { - pluginEditorWindow.reset(); - updateEditorButtonState(); - } - - void updateEditorButtonState() - { - bool canOpenEditor = false; - - if (auto plugin = getLoadedPlugin()) - canOpenEditor = plugin->hasEditor(); - - openEditorButton.setEnabled (canOpenEditor); - openEditorButton.setButtonText (pluginEditorWindow != nullptr ? "Show Editor" : "Open Editor"); - } - - void populatePresetCombo() - { - clearPresetCombo(); - - auto plugin = getLoadedPlugin(); - if (plugin == nullptr) - return; - - const auto numPresets = plugin->getNumPresets(); - if (numPresets <= 0) - return; - - for (int i = 0; i < numPresets; ++i) - { - auto name = plugin->getPresetName (i); - if (name.isEmpty()) - name = "Preset " + yup::String (i + 1); - - presetCombo.addItem (name, i + 1); - } - - const auto currentPreset = plugin->getCurrentPreset(); - if (yup::isPositiveAndBelow (currentPreset, numPresets)) - presetCombo.setSelectedId (currentPreset + 1, yup::dontSendNotification); - - presetCombo.setEnabled (true); - } - - void clearPresetCombo() - { - presetCombo.clear(); - presetCombo.setEnabled (false); - } - - void changeSelectedPreset() - { - const auto selectedPreset = presetCombo.getSelectedId() - 1; - if (selectedPreset < 0) - return; - - yup::String presetName; - - auto plugin = getLoadedPlugin(); - if (plugin == nullptr) - return; - - plugin->setCurrentPreset (selectedPreset); - presetName = plugin->getPresetName (selectedPreset); - - statusText = "Preset: " + (presetName.isNotEmpty() ? presetName : yup::String (selectedPreset + 1)); - refreshVisibleParameterRows(); - } - - void buildParameterSliders() - { - clearParameterSliders(); - - auto plugin = getLoadedPlugin(); - if (plugin == nullptr) - return; - - const auto params = plugin->getParameters(); - if (params.empty()) - { - statusText = "Loaded: " + plugin->getDescription().name + " (no adjustable parameters)"; - return; - } - - std::vector parameters; - parameters.reserve (params.size()); - - for (auto& param : params) - parameters.push_back (param); - - parameterListModel.setParameters (std::move (parameters)); - parameterList.updateContent(); - parameterList.setVisible (true); - - resized(); - } - - void clearParameterSliders() - { - parameterListModel.clear(); - parameterList.updateContent(); - parameterList.setVisible (false); - resized(); - } - - std::shared_ptr getLoadedPlugin() - { - return std::atomic_load (&loadedPlugin); - } - - void retirePlugin (std::shared_ptr plugin) - { - if (plugin != nullptr) - retiredPlugins.push_back ({ std::move (plugin), 20 }); - } - - void cleanupRetiredPlugins (bool force = false) - { - for (auto iterator = retiredPlugins.begin(); iterator != retiredPlugins.end();) - { - if (! force && --iterator->timerTicksRemaining > 0) - { - ++iterator; - continue; - } - - iterator->plugin->releaseResources(); - iterator = retiredPlugins.erase (iterator); - } - } - - void refreshVisibleParameterRows() - { - if (parameterListModel.getNumRows() <= 0) - return; - - const auto visibleRows = parameterList.getVisibleRowRange(); - for (int row = visibleRows.getStart(); row < visibleRows.getEnd(); ++row) - if (auto* parameterRow = dynamic_cast (parameterList.getComponentForRow (row))) - parameterRow->refreshValue(); - } - - void updateStatusLine() - { - statusLabel.setText (statusText, yup::dontSendNotification); - } - - static yup::String formatTypeToString (yup::AudioPluginFormatType type) - { - switch (type) - { - case yup::AudioPluginFormatType::vst3: - return "VST3"; - case yup::AudioPluginFormatType::clap: - return "CLAP"; - case yup::AudioPluginFormatType::audioUnit: - return "AU"; - default: - return "Unknown"; - } - } - - //============================================================================== - // Plugin list model - - struct PluginListModel : public yup::ListBoxModel - { - int getNumRows() override - { - return static_cast (items.size()); - } - - yup::String getRowText (int rowIndex) override - { - if (rowIndex >= 0 && rowIndex < static_cast (items.size())) - return items[rowIndex]; - return {}; - } - - void selectedRowsChanged (const yup::Array& selectedRows) override - { - if (onSelectedRowChanged && ! selectedRows.isEmpty()) - onSelectedRowChanged (selectedRows[0]); - } - - void setItems (yup::Array items) - { - this->items = std::move (items); - } - - void clear() - { - items.clear(); - } - - std::function onSelectedRowChanged; - - private: - yup::Array items; - }; - - struct RetiredPlugin - { - std::shared_ptr plugin; - int timerTicksRemaining = 0; - }; - - //============================================================================== - - // Audio - yup::AudioDeviceManager audioDeviceManager; - yup::AudioBuffer audioBuffer; - yup::AudioBuffer processBuffer; - yup::AudioBuffer dryBuffer; - - bool audioBufferLoaded = false; - int readPosition = 0; - - // Plugin hosting - std::shared_ptr> scanLifetime { std::make_shared> (true) }; - std::atomic scanInProgress { false }; - std::unique_ptr scanner; - std::vector discoveredPlugins; - int selectedPluginIndex = -1; - std::shared_ptr loadedPlugin; - std::vector retiredPlugins; - std::unique_ptr pluginEditorWindow; - - // Dry/wet mix - yup::SmoothedValue dryWetMix; - - // UI - yup::TextButton scanButton; - yup::Label statusLabel; - yup::Label scanResultsLabel; - yup::ListBox pluginList; - PluginListModel pluginListModel; - - yup::Label nameLabel; - yup::Label vendorLabel; - yup::Label formatLabel; - - yup::TextButton loadButton; - yup::TextButton unloadButton; - yup::TextButton openEditorButton; - - yup::Label presetLabel; - yup::ComboBox presetCombo; - - yup::Label dryWetLabel; - yup::Slider dryWetSlider; - - yup::ListBox parameterList; - ParameterListModel parameterListModel; - - yup::String statusText; -}; diff --git a/examples/graphics/source/main.cpp b/examples/graphics/source/main.cpp index 79d1ed1a4..a943ca89a 100644 --- a/examples/graphics/source/main.cpp +++ b/examples/graphics/source/main.cpp @@ -39,7 +39,6 @@ #include "examples/Artboard.h" #include "examples/Audio.h" #include "examples/AudioFileDemo.h" -#include "examples/AudioGraphDemo.h" #include "examples/ColorLab.h" #include "examples/ConvolutionDemo.h" #include "examples/CrossoverDemo.h" @@ -48,7 +47,6 @@ #include "examples/LayoutFonts.h" #include "examples/OpaqueDemo.h" #include "examples/Paths.h" -#include "examples/PluginHost.h" #include "examples/PopupMenu.h" #include "examples/ScrollBarDemo.h" #include "examples/SliderDemo.h" @@ -143,7 +141,6 @@ class CustomWindow registerDemo ("Audio", counter++); registerDemo ("Audio File", counter++); - registerDemo ("Audio Graph", counter++); registerDemo ("Color Lab", counter++); registerDemo ("Convolution Demo", counter++); registerDemo ("Crossover Demo", counter++); @@ -152,7 +149,6 @@ class CustomWindow registerDemo ("Layout Fonts", counter++); registerDemo ("Opaque Demo", counter++); registerDemo ("Paths", counter++); - registerDemo ("Plugin Host", counter++); registerDemo ("Popup Menu", counter++); registerDemo ("ScrollBar", counter++); registerDemo ("Sliders", counter++); diff --git a/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.cpp b/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.cpp index 32b990cc5..92ffd7835 100644 --- a/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.cpp +++ b/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.cpp @@ -587,6 +587,19 @@ class AudioGraphProcessor::Pimpl return iterator->properties; } + std::vector getNodeIDs() const + { + const std::lock_guard lock (modelMutex); + + std::vector result; + result.reserve (modelNodes.size()); + + for (const auto& node : modelNodes) + result.push_back (node.id); + + return result; + } + void setNodeFactory (AudioGraphProcessor::NodeFactory factory) { const std::lock_guard lock (factoryMutex); @@ -612,13 +625,13 @@ class AudioGraphProcessor::Pimpl for (const auto& node : nodesSnapshot) { if (node.processor == nullptr) - return ResultValue>::fail ("Audio graph contains an empty node"); + return makeResultValueFail ("Audio graph contains an empty node"); MemoryBlock nodeState; const auto result = node.processor->saveStateIntoMemory (nodeState); if (! result) - return ResultValue>::fail ("Audio graph node state save failed: " + result.getErrorMessage()); + return makeResultValueFail ("Audio graph node state save failed: " + result.getErrorMessage()); savedNodes.push_back ({ node.id, node.properties, std::move (nodeState) }); } @@ -657,7 +670,7 @@ class AudioGraphProcessor::Pimpl writeEndpoint (*destinationElement, connection.destination); } - return ResultValue>::ok (std::move (root)); + return makeResultValueOk (std::move (root)); } Result restoreFromXml (const XmlElement& xml) @@ -835,7 +848,7 @@ class AudioGraphProcessor::Pimpl } if (! factoryCopy) - return ResultValue>::fail ("Audio graph node factory is not configured"); + return makeResultValueFail ("Audio graph node factory is not configured"); return factoryCopy (properties); } @@ -1732,6 +1745,11 @@ std::optional AudioGraphProcessor::getNodeProperties ( return pimpl->getNodeProperties (nodeID); } +std::vector AudioGraphProcessor::getNodeIDs() const +{ + return pimpl->getNodeIDs(); +} + void AudioGraphProcessor::setNodeFactory (NodeFactory factory) { pimpl->setNodeFactory (std::move (factory)); diff --git a/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.h b/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.h index 394123512..831c2bb68 100644 --- a/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.h +++ b/modules/yup_audio_graph/graph/yup_AudioGraphProcessor.h @@ -104,6 +104,9 @@ class YUP_API AudioGraphProcessor final : public AudioProcessor /** Returns persistent metadata for a node, or nullopt when the node is not present. */ std::optional getNodeProperties (AudioGraphNodeID nodeID) const; + /** Returns the identifiers of all nodes currently in the control-thread graph model. */ + std::vector getNodeIDs() const; + /** Sets the factory used to recreate processor nodes during state loading. */ void setNodeFactory (NodeFactory factory); diff --git a/modules/yup_audio_gui/graph/yup_AudioGraphComponent.cpp b/modules/yup_audio_gui/graph/yup_AudioGraphComponent.cpp index 39504f126..42a8383ed 100644 --- a/modules/yup_audio_gui/graph/yup_AudioGraphComponent.cpp +++ b/modules/yup_audio_gui/graph/yup_AudioGraphComponent.cpp @@ -24,11 +24,6 @@ namespace yup namespace { -bool endpointsMatch (const AudioGraphEndpoint& a, const AudioGraphEndpoint& b) noexcept -{ - return a == b; -} - AudioGraphNodeView* findNodeViewForEventSource (Component* source) noexcept { if (source == nullptr) @@ -366,7 +361,29 @@ void AudioGraphComponent::mouseDown (const MouseEvent& event) } if (auto connection = hitTestConnection (screenPos)) + { removeConnectionAndCommit (*connection); + return; + } + + for (const auto& node : nodes) + { + if (node.view == nullptr || ! node.nodeID.isValid()) + continue; + + const auto localPos = node.view->getLocalPoint (this, screenPos); + + if (node.view->getLocalBounds().contains (localPos)) + { + if (onNodeContextMenu != nullptr) + onNodeContextMenu (node.nodeID, screenToCanvas (screenPos)); + + return; + } + } + + if (onCanvasContextMenu != nullptr) + onCanvasContextMenu (screenToCanvas (screenPos)); return; } @@ -516,6 +533,28 @@ void AudioGraphComponent::mouseUp (const MouseEvent& event) } } +void AudioGraphComponent::mouseDoubleClick (const MouseEvent& event) +{ + if (onNodeDoubleClicked == nullptr) + return; + + const auto screenPos = eventPositionInThisComponent (event); + + for (const auto& node : nodes) + { + if (node.view == nullptr || ! node.nodeID.isValid()) + continue; + + const auto localPos = node.view->getLocalPoint (this, screenPos); + + if (node.view->getLocalBounds().contains (localPos)) + { + onNodeDoubleClicked (node.nodeID); + return; + } + } +} + void AudioGraphComponent::mouseWheel (const MouseEvent& event, const MouseWheelData& wheelData) { const auto screenPos = eventPositionInThisComponent (event); @@ -870,7 +909,7 @@ void AudioGraphComponent::removeConnectionsForEndpoint (const AudioGraphEndpoint for (const auto& connection : connections) { - if (endpointsMatch (connection.source, endpoint) || endpointsMatch (connection.destination, endpoint)) + if (connection.source == endpoint || connection.destination == endpoint) { if (graph->removeConnection (connection)) removedConnections.push_back (connection); diff --git a/modules/yup_audio_gui/graph/yup_AudioGraphComponent.h b/modules/yup_audio_gui/graph/yup_AudioGraphComponent.h index 285799172..68bb858da 100644 --- a/modules/yup_audio_gui/graph/yup_AudioGraphComponent.h +++ b/modules/yup_audio_gui/graph/yup_AudioGraphComponent.h @@ -159,6 +159,24 @@ class YUP_API AudioGraphComponent : public Component /** Removes a listener. */ void removeListener (Listener* listener); + /** + Called when the user right-clicks on an empty area of the canvas. + canvasPos is the click position in canvas coordinates. + */ + std::function canvasPos)> onCanvasContextMenu; + + /** + Called when the user right-clicks on a node body (not a port or wire). + nodeID identifies the node; canvasPos is the position in canvas coordinates. + */ + std::function canvasPos)> onNodeContextMenu; + + /** + Called when the user double-clicks on a node body. + nodeID identifies the clicked node. + */ + std::function onNodeDoubleClicked; + /** @internal */ void resized() override; /** @internal */ @@ -170,6 +188,8 @@ class YUP_API AudioGraphComponent : public Component /** @internal */ void mouseUp (const MouseEvent& event) override; /** @internal */ + void mouseDoubleClick (const MouseEvent& event) override; + /** @internal */ void mouseWheel (const MouseEvent& event, const MouseWheelData& wheelData) override; /** @internal */ void keyDown (const KeyPress& keys, const Point& position) override; diff --git a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.mm b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.mm index 911c6597e..f942ce498 100644 --- a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.mm +++ b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.mm @@ -1105,10 +1105,9 @@ void removeParameterListeners() } if (results.empty()) - return ResultValue>::fail( - "No AudioComponents found in registry"); + return makeResultValueFail("No AudioComponents found in registry"); - return ResultValue>::ok(std::move(results)); + return makeResultValueOk(std::move(results)); } ResultValue> AUv2Format::loadPlugin( @@ -1118,10 +1117,9 @@ void removeParameterListeners() auto instance = AUv2Instance::create(description, context); if (instance == nullptr) - return ResultValue>::fail( - "Failed to instantiate AUv2 plugin: " + description.name); + return makeResultValueFail("Failed to instantiate AUv2 plugin: " + description.name); - return ResultValue>::ok(std::move(instance)); + return makeResultValueOk(std::move(instance)); } } // namespace yup diff --git a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.cpp b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.cpp index 629ada293..115c221f3 100644 --- a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.cpp +++ b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.cpp @@ -497,19 +497,17 @@ FileSearchPath CLAPFormat::getDefaultSearchPaths() const ResultValue> CLAPFormat::scanFile (const File& file) { if (file.getFileExtension().toLowerCase() != ".clap") - return ResultValue>::fail ("Not a CLAP file"); + return makeResultValueFail ("Not a CLAP file"); auto mod = CLAPModule::load (file); if (mod == nullptr) - return ResultValue>::fail ( - "Failed to load CLAP module: " + file.getFullPathName()); + return makeResultValueFail ("Failed to load CLAP module: " + file.getFullPathName()); const clap_plugin_factory_t* factory = reinterpret_cast ( mod->entry->get_factory (CLAP_PLUGIN_FACTORY_ID)); if (factory == nullptr) - return ResultValue>::fail ( - "No plugin factory in: " + file.getFullPathName()); + return makeResultValueFail ("No plugin factory in: " + file.getFullPathName()); std::vector results; const uint32_t count = factory->get_plugin_count (factory); @@ -590,10 +588,9 @@ ResultValue> CLAPFormat::scanFile (const Fil } if (results.empty()) - return ResultValue>::fail ( - "No plugins found in: " + file.getFullPathName()); + return makeResultValueFail ("No plugins found in: " + file.getFullPathName()); - return ResultValue>::ok (std::move (results)); + return makeResultValueOk (std::move (results)); } ResultValue> CLAPFormat::loadPlugin ( @@ -603,10 +600,9 @@ ResultValue> CLAPFormat::loadPlugin ( auto instance = CLAPInstance::create (description, context); if (instance == nullptr) - return ResultValue>::fail ( - "Failed to load CLAP plugin: " + description.name); + return makeResultValueFail ("Failed to load CLAP plugin: " + description.name); - return ResultValue>::ok (std::move (instance)); + return makeResultValueOk (std::move (instance)); } } // namespace yup diff --git a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.cpp b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.cpp index 229466597..36466e960 100644 --- a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.cpp +++ b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.cpp @@ -547,17 +547,15 @@ ResultValue> VST3Format::scanFile (const Fil { if (file.getFileExtension().toLowerCase() != ".vst3" && ! file.isDirectory()) - return ResultValue>::fail ("Not a VST3 file"); + return makeResultValueFail ("Not a VST3 file"); auto mod = VST3Module::load (file); if (mod == nullptr) - return ResultValue>::fail ( - "Failed to load VST3 module: " + file.getFullPathName()); + return makeResultValueFail ("Failed to load VST3 module: " + file.getFullPathName()); IPluginFactory* rawFactory = mod->getFactory(); if (rawFactory == nullptr) - return ResultValue>::fail ( - "No factory in " + file.getFullPathName()); + return makeResultValueFail ("No factory in " + file.getFullPathName()); IPtr factory (rawFactory); @@ -634,10 +632,9 @@ ResultValue> VST3Format::scanFile (const Fil } if (results.empty()) - return ResultValue>::fail ( - "No Audio Module Class entries in " + file.getFullPathName()); + return makeResultValueFail ("No Audio Module Class entries in " + file.getFullPathName()); - return ResultValue>::ok (std::move (results)); + return makeResultValueOk (std::move (results)); } ResultValue> VST3Format::loadPlugin ( @@ -647,10 +644,9 @@ ResultValue> VST3Format::loadPlugin ( auto instance = VST3Instance::create (description, context); if (instance == nullptr) - return ResultValue>::fail ( - "Failed to load VST3 plugin: " + description.name); + return makeResultValueFail ("Failed to load VST3 plugin: " + description.name); - return ResultValue>::ok (std::move (instance)); + return makeResultValueOk (std::move (instance)); } } // namespace yup diff --git a/modules/yup_core/misc/yup_ResultValue.h b/modules/yup_core/misc/yup_ResultValue.h index c75796108..70a04a543 100644 --- a/modules/yup_core/misc/yup_ResultValue.h +++ b/modules/yup_core/misc/yup_ResultValue.h @@ -22,19 +22,94 @@ namespace yup { +//============================================================================== +/** Helper type returned by yup::makeResultValueOk(), implicitly convertible to a successful ResultValue. + + @see makeResultValueOk, ResultValue + @tags{Core} +*/ +template +struct YUP_API OkValue +{ + template + requires std::constructible_from + explicit constexpr OkValue (U&& v) noexcept (std::is_nothrow_constructible_v) + : value (std::forward (v)) + { + } + + T value; +}; + +/** Helper type returned by yup::makeResultValueFail(), implicitly convertible to a failed ResultValue. + + @see makeResultValueFail, ResultValue + @tags{Core} +*/ +struct YUP_API FailValue +{ + explicit FailValue (StringRef errorMessage) noexcept + : message (errorMessage.isEmpty() ? StringRef ("Unknown Error") : errorMessage) + { + } + + String message; +}; + +//============================================================================== +/** Creates an OkValue that implicitly converts to a successful ResultValue, allowing + return-site type deduction without naming ResultValue explicitly. + + @code + ResultValue myOperation() + { + return yup::makeResultValueOk (1337); // T is deduced from the argument + } + @endcode + + @see ResultValue, makeResultValueFail + @tags{Core} +*/ +template +[[nodiscard]] constexpr auto makeResultValueOk (T&& value) noexcept (std::is_nothrow_constructible_v, T>) + -> OkValue> +{ + return OkValue> (std::forward (value)); +} + +/** Creates a FailValue that implicitly converts to a failed ResultValue, allowing + return-site failure without naming ResultValue explicitly. + + @code + ResultValue myOperation() + { + return yup::makeResultValueFail ("something went wrong"); // Works for any ResultValue + } + @endcode + + @see ResultValue, makeResultValueOk + @tags{Core} +*/ +[[nodiscard]] inline FailValue makeResultValueFail (StringRef errorMessage) noexcept +{ + return FailValue (errorMessage); +} + //============================================================================== /** Represents the 'success' or 'failure' of an operation that returns a value, and holds an associated error message to describe the error when there's a failure. - E.g. + Prefer the free functions yup::makeResultValueOk (..) and yup::makeResultValueFail() over the static methods to avoid + having to repeat the template type at the call site: + @code ResultValue myOperation() { if (doSomeKindOfFoobar()) - return ResultValue::ok (1337); + return yup::makeResultValueOk (1337); else - return ResultValue::fail ("foobar didn't work!"); + return yup::makeResultValueFail ("foobar didn't work!"); } const ResultValue result (myOperation()); @@ -157,6 +232,20 @@ class YUP_API ResultValue ResultValue (ResultValue&&) noexcept = default; ResultValue& operator= (ResultValue&&) noexcept = default; + /** Constructs a successful result from an OkValue, enabling type-deduced return syntax. */ + template + requires std::constructible_from + ResultValue (OkValue&& okVal) noexcept (std::is_nothrow_constructible_v) + : valueOrErrorMessage (std::in_place_index<1>, std::move (okVal.value)) + { + } + + /** Constructs a failed result from a FailValue, enabling type-deduced return syntax. */ + ResultValue (FailValue&& failVal) noexcept + : valueOrErrorMessage (std::in_place_index<2>, std::move (failVal.message)) + { + } + bool operator== (const ResultValue& other) const noexcept { return valueOrErrorMessage == other.valueOrErrorMessage; diff --git a/modules/yup_core/system/yup_TargetPlatform.h b/modules/yup_core/system/yup_TargetPlatform.h index 03f491ac7..3dcc687b0 100644 --- a/modules/yup_core/system/yup_TargetPlatform.h +++ b/modules/yup_core/system/yup_TargetPlatform.h @@ -70,15 +70,19 @@ //============================================================================== #if defined(_WIN32) || defined(_WIN64) #define YUP_WINDOWS 1 +#define F1 #elif defined(ANDROID) || defined(__ANDROID__) #define YUP_ANDROID 1 +#define YUP_MOBILE 1 #elif defined(__FreeBSD__) || defined(__OpenBSD__) #define YUP_BSD 1 +#define YUP_DESKTOP 1 #elif defined(LINUX) || defined(__linux__) #define YUP_LINUX 1 +#define YUP_DESKTOP 1 #elif defined(__APPLE_CPP__) || defined(__APPLE_CC__) #define CF_EXCLUDE_CSTD_HEADERS 1 @@ -87,19 +91,20 @@ #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR #define YUP_IPHONE 1 #define YUP_IOS 1 +#define YUP_MOBILE 1 #if TARGET_IPHONE_SIMULATOR #define YUP_IOS_SIMULATOR 1 #endif #else #define YUP_MAC 1 +#define YUP_DESKTOP 1 #endif -#elif defined(__wasm__) && defined(__EMSCRIPTEN__) -#define YUP_WASM 1 -#define YUP_EMSCRIPTEN 1 - #elif defined(__wasm__) #define YUP_WASM 1 +#if defined(__EMSCRIPTEN__) +#define YUP_EMSCRIPTEN 1 +#endif #else #error "Unknown platform!" diff --git a/modules/yup_data_model/tree/yup_DataTree.cpp b/modules/yup_data_model/tree/yup_DataTree.cpp index d25873046..dedd4715f 100644 --- a/modules/yup_data_model/tree/yup_DataTree.cpp +++ b/modules/yup_data_model/tree/yup_DataTree.cpp @@ -1710,17 +1710,17 @@ Result DataTree::ValidatedTransaction::addChild (const DataTree& child, int inde ResultValue DataTree::ValidatedTransaction::createAndAddChild (const Identifier& childType, int index) { if (! transaction || ! transaction->isActive() || ! schema) - return ResultValue::fail ("Transaction is not active"); + return makeResultValueFail ("Transaction is not active"); DataTree child = schema->createChildNode (nodeType, childType); if (! child.isValid()) - return ResultValue::fail ("Could not create child of type '" + childType.toString() + "'"); + return makeResultValueFail ("Could not create child of type '" + childType.toString() + "'"); auto addResult = addChild (child, index); if (addResult.failed()) - return ResultValue::fail (addResult.getErrorMessage()); + return makeResultValueFail (addResult.getErrorMessage()); - return ResultValue::ok (child); + return makeResultValueOk (child); } Result DataTree::ValidatedTransaction::removeChild (const DataTree& child) diff --git a/modules/yup_dsp/stretching/yup_TimeStretchProcessor.cpp b/modules/yup_dsp/stretching/yup_TimeStretchProcessor.cpp index 6563e65c1..46767963f 100644 --- a/modules/yup_dsp/stretching/yup_TimeStretchProcessor.cpp +++ b/modules/yup_dsp/stretching/yup_TimeStretchProcessor.cpp @@ -406,26 +406,26 @@ ResultValue TimeStretchProcessor::process (const float* const* inputChannel int outputFrameCount) { if (! prepared || engine == nullptr) - return ResultValue::fail ("TimeStretchProcessor is not prepared"); + return makeResultValueFail ("TimeStretchProcessor is not prepared"); if (inputFrameCount < 0 || outputFrameCount < 0) - return ResultValue::fail ("Invalid frame count"); + return makeResultValueFail ("Invalid frame count"); if (outputFrameCount == 0) - return ResultValue::ok (0); + return makeResultValueOk (0); const bool hasInputProvider = (inputProvider != nullptr); if (! hasInputProvider && inputFrameCount == 0) - return ResultValue::ok (0); + return makeResultValueOk (0); if ((! hasInputProvider && inputChannels == nullptr) || outputChannels == nullptr) - return ResultValue::fail ("Null channel pointers"); + return makeResultValueFail ("Null channel pointers"); if (! hasInputProvider && inputFrameCount > spec.maximumBlockSize) - return ResultValue::fail ("Input frame count exceeds maximum block size"); + return makeResultValueFail ("Input frame count exceeds maximum block size"); const auto processed = engine->process (inputChannels, inputFrameCount, outputChannels, outputFrameCount); - return ResultValue::ok (processed); + return makeResultValueOk (processed); } ResultValue TimeStretchProcessor::process (const AudioBuffer& input, @@ -433,10 +433,10 @@ ResultValue TimeStretchProcessor::process (const AudioBuffer& input, int outputFrameCount) { if (input.getNumChannels() != spec.numChannels || output.getNumChannels() != spec.numChannels) - return ResultValue::fail ("Channel count mismatch"); + return makeResultValueFail ("Channel count mismatch"); if (outputFrameCount > output.getNumSamples()) - return ResultValue::fail ("Output buffer too small"); + return makeResultValueFail ("Output buffer too small"); return process (input.getArrayOfReadPointers(), input.getNumSamples(), @@ -451,7 +451,7 @@ ResultValue TimeStretchProcessor::processUsingTimeRatio (const float* const { const auto expectedOutput = getExpectedOutputFrameCount (inputFrameCount); if (expectedOutput > outputFrameCapacity) - return ResultValue::fail ("Output buffer too small for the current time ratio"); + return makeResultValueFail ("Output buffer too small for the current time ratio"); return process (inputChannels, inputFrameCount, outputChannels, expectedOutput); } @@ -461,7 +461,7 @@ ResultValue TimeStretchProcessor::processUsingTimeRatio (const AudioBuffer< { const auto expectedOutput = getExpectedOutputFrameCount (input.getNumSamples()); if (expectedOutput > output.getNumSamples()) - return ResultValue::fail ("Output buffer too small for the current time ratio"); + return makeResultValueFail ("Output buffer too small for the current time ratio"); return process (input, output, expectedOutput); } diff --git a/modules/yup_graphics/imaging/yup_Image.cpp b/modules/yup_graphics/imaging/yup_Image.cpp index 3073e69bb..2c8284805 100644 --- a/modules/yup_graphics/imaging/yup_Image.cpp +++ b/modules/yup_graphics/imaging/yup_Image.cpp @@ -190,7 +190,7 @@ ResultValue Image::loadFromData (Span imageData) { auto bitmap = rive::Bitmap::decode (imageData.data(), imageData.size()); if (bitmap == nullptr) - return ResultValue::fail ("Unable to decode image"); + return makeResultValueFail ("Unable to decode image"); Image result; @@ -202,7 +202,7 @@ ResultValue Image::loadFromData (Span imageData) : PixelFormat::RGBA, bitmap->detachBytes()); - return ResultValue::ok (result); + return makeResultValueOk (result); } } // namespace yup diff --git a/tests/yup_audio_graph/yup_AudioGraphProcessor.cpp b/tests/yup_audio_graph/yup_AudioGraphProcessor.cpp index a23d3d48d..90a30c7f4 100644 --- a/tests/yup_audio_graph/yup_AudioGraphProcessor.cpp +++ b/tests/yup_audio_graph/yup_AudioGraphProcessor.cpp @@ -395,9 +395,9 @@ AudioGraphProcessor::NodeFactory statefulGainFactory() return [] (const AudioGraphNodeProperties& properties) -> ResultValue> { if (properties.identifier != "statefulGain") - return ResultValue>::fail ("Unknown node type"); + return makeResultValueFail ("Unknown node type"); - return ResultValue>::ok (std::make_unique (1.0f)); + return makeResultValueOk (std::make_unique (1.0f)); }; } @@ -506,17 +506,17 @@ AudioGraphProcessor::NodeFactory statefulGainAndExternalFactory (int& externalLo return [&externalLoadCount, &externalIdentifier] (const AudioGraphNodeProperties& properties) -> ResultValue> { if (properties.identifier == "statefulGain") - return ResultValue>::ok (std::make_unique (1.0f)); + return makeResultValueOk (std::make_unique (1.0f)); if (properties.identifier != "externalPlugin") - return ResultValue>::fail ("Unknown node type"); + return makeResultValueFail ("Unknown node type"); MemoryInputStream stream (properties.creationData, false); const auto pluginName = stream.readString(); externalIdentifier = stream.readString(); ++externalLoadCount; - return ResultValue>::ok (std::make_unique (pluginName)); + return makeResultValueOk (std::make_unique (pluginName)); }; } @@ -1559,7 +1559,7 @@ TEST (AudioGraphProcessorTests, LoadStateFailsWhenFactoryReturnsNullProcessor) AudioGraphProcessor destination; destination.setNodeFactory ([] (const AudioGraphNodeProperties&) -> ResultValue> { - return ResultValue>::ok (std::unique_ptr()); + return makeResultValueOk (std::unique_ptr()); }); EXPECT_TRUE (destination.loadStateFromMemory (savedState).failed()); @@ -2637,3 +2637,23 @@ TEST (AudioGraphProcessorTests, MixedAudioMidiProcessorPdcCompensatesDirectPaths EXPECT_EQ (1, countMidiEventsAt (midi, 7)); EXPECT_EQ (2, countMidiEvents (midi)); } + +TEST (AudioGraphProcessorTests, GetNodeIDsReturnsAllAddedNodes) +{ + AudioGraphProcessor graph; + + EXPECT_TRUE (graph.getNodeIDs().empty()); + + const auto idA = graph.addNode (std::make_unique()); + const auto idB = graph.addNode (std::make_unique()); + + const auto ids = graph.getNodeIDs(); + EXPECT_EQ (2u, ids.size()); + EXPECT_NE (ids.end(), std::find (ids.begin(), ids.end(), idA)); + EXPECT_NE (ids.end(), std::find (ids.begin(), ids.end(), idB)); + + EXPECT_TRUE (graph.removeNode (idA)); + const auto idsAfter = graph.getNodeIDs(); + EXPECT_EQ (1u, idsAfter.size()); + EXPECT_EQ (idsAfter.end(), std::find (idsAfter.begin(), idsAfter.end(), idA)); +} diff --git a/tests/yup_audio_plugin_host/yup_AudioPluginScanner.cpp b/tests/yup_audio_plugin_host/yup_AudioPluginScanner.cpp index 88108e8d4..c16738705 100644 --- a/tests/yup_audio_plugin_host/yup_AudioPluginScanner.cpp +++ b/tests/yup_audio_plugin_host/yup_AudioPluginScanner.cpp @@ -24,20 +24,20 @@ class FakeFormat : public AudioPluginFormat ResultValue> scanFile (const File& file) override { if (file.getFileName() == "bad.vst3") - return ResultValue>::fail ("unsupported"); + return makeResultValueFail ("unsupported"); AudioPluginDescription desc; desc.formatType = fakeType; desc.name = file.getFileNameWithoutExtension(); desc.identifier = "fake." + file.getFileNameWithoutExtension(); - return ResultValue>::ok (std::vector { desc }); + return makeResultValueOk (std::vector { desc }); } ResultValue> loadPlugin ( const AudioPluginDescription&, const AudioPluginHostContext&) override { - return ResultValue>::fail ("not implemented"); + return makeResultValueFail ("not implemented"); } private: diff --git a/tests/yup_core/yup_ResultValue.cpp b/tests/yup_core/yup_ResultValue.cpp index 6363f22f9..a75dd3611 100644 --- a/tests/yup_core/yup_ResultValue.cpp +++ b/tests/yup_core/yup_ResultValue.cpp @@ -137,3 +137,80 @@ TEST (ResultValueTests, InequalityOperator) EXPECT_TRUE (success1 != failure1); EXPECT_TRUE (failure1 != failure3); } + +TEST (ResultValueTests, MakeResultValueOkDeducesTypeFromArgument) +{ + ResultValue result = makeResultValueOk (42); + EXPECT_TRUE (result.wasOk()); + EXPECT_FALSE (result.failed()); + EXPECT_EQ (result.getValue(), 42); +} + +TEST (ResultValueTests, MakeResultValueOkWorksWithReturnDeduction) +{ + auto makeInt = []() -> ResultValue + { + return makeResultValueOk (1337); + }; + auto makeStr = []() -> ResultValue + { + return makeResultValueOk (String ("hello")); + }; + + auto intResult = makeInt(); + EXPECT_TRUE (intResult.wasOk()); + EXPECT_EQ (intResult.getValue(), 1337); + + auto strResult = makeStr(); + EXPECT_TRUE (strResult.wasOk()); + EXPECT_EQ (strResult.getValue(), String ("hello")); +} + +TEST (ResultValueTests, MakeResultValueOkWithImplicitConversion) +{ + ResultValue result = makeResultValueOk (42); + EXPECT_TRUE (result.wasOk()); + EXPECT_DOUBLE_EQ (result.getValue(), 42.0); +} + +TEST (ResultValueTests, MakeResultValueFailDeducesForAnyType) +{ + ResultValue intResult = makeResultValueFail ("int error"); + EXPECT_FALSE (intResult.wasOk()); + EXPECT_TRUE (intResult.failed()); + EXPECT_EQ (intResult.getErrorMessage(), "int error"); + + ResultValue strResult = makeResultValueFail ("string error"); + EXPECT_FALSE (strResult.wasOk()); + EXPECT_EQ (strResult.getErrorMessage(), "string error"); +} + +TEST (ResultValueTests, MakeResultValueFailWorksWithReturnDeduction) +{ + auto makeFailure = []() -> ResultValue + { + return makeResultValueFail ("something went wrong"); + }; + + auto result = makeFailure(); + EXPECT_FALSE (result.wasOk()); + EXPECT_EQ (result.getErrorMessage(), "something went wrong"); +} + +TEST (ResultValueTests, MakeResultValueFailWithEmptyMessageUsesDefault) +{ + ResultValue result = makeResultValueFail (""); + EXPECT_FALSE (result.wasOk()); + EXPECT_EQ (result.getErrorMessage(), "Unknown Error"); +} + +TEST (ResultValueTests, MakeResultValueOkAndFailProduceEqualResults) +{ + ResultValue a = makeResultValueOk (7); + ResultValue b = ResultValue::ok (7); + EXPECT_EQ (a, b); + + ResultValue c = makeResultValueFail ("err"); + ResultValue d = ResultValue::fail ("err"); + EXPECT_EQ (c, d); +} From 387cfbf7adb706a6c52b68cadee8553bfb73c3bc Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 18 May 2026 17:02:12 +0200 Subject: [PATCH 23/35] Fix mouse handling --- examples/audiograph/source/AudioGraphApp.h | 7 +- examples/audiograph/source/main.cpp | 2 +- .../audiograph/source/nodes/NodeRegistry.h | 6 +- .../audiograph/source/ui/PluginEditorWindow.h | 3 +- .../graph/yup_AudioGraphComponent.cpp | 2 +- modules/yup_gui/menus/yup_PopupMenu.cpp | 127 +++++++++++++----- .../native/yup_WindowingUtilities_sdl2.cpp | 16 +++ modules/yup_gui/native/yup_Windowing_sdl2.cpp | 19 +-- 8 files changed, 134 insertions(+), 48 deletions(-) diff --git a/examples/audiograph/source/AudioGraphApp.h b/examples/audiograph/source/AudioGraphApp.h index 249573e69..2ef7be180 100644 --- a/examples/audiograph/source/AudioGraphApp.h +++ b/examples/audiograph/source/AudioGraphApp.h @@ -139,7 +139,7 @@ class AudioGraphApp final ~AudioGraphApp() override { -#if YUP_WINDOWS || YUP_MAC || YUP_LINUX +#if YUP_DESKTOP scanLifetime->store (false); #endif @@ -433,12 +433,15 @@ class AudioGraphApp final { pendingMenuCanvasPos = canvasPos; + const auto screenPos = graphComponent->localToScreen (graphComponent->canvasToScreen (canvasPos)); + const auto options = yup::PopupMenu::Options {}.withPosition (screenPos, yup::Justification::topLeft); + auto internalSubMenu = yup::PopupMenu::create(); internalSubMenu->addItem ("Oscillator", 1); internalSubMenu->addItem ("Gain", 2); internalSubMenu->addItem ("Low Pass Filter", 3); - activeMenu = yup::PopupMenu::create(); + activeMenu = yup::PopupMenu::create (options); activeMenu->addSubMenu ("Internal Nodes", internalSubMenu); #if YUP_DESKTOP diff --git a/examples/audiograph/source/main.cpp b/examples/audiograph/source/main.cpp index 581dceaee..cae671fe4 100644 --- a/examples/audiograph/source/main.cpp +++ b/examples/audiograph/source/main.cpp @@ -29,7 +29,7 @@ #include #include -#if YUP_WINDOWS || YUP_MAC || YUP_LINUX +#if YUP_DESKTOP #include #endif diff --git a/examples/audiograph/source/nodes/NodeRegistry.h b/examples/audiograph/source/nodes/NodeRegistry.h index 704533b7d..03fa37f68 100644 --- a/examples/audiograph/source/nodes/NodeRegistry.h +++ b/examples/audiograph/source/nodes/NodeRegistry.h @@ -48,7 +48,7 @@ class NodeRegistry /** Stable factory key for the built-in low-pass filter node. */ static constexpr const char* lpfIdentifier = "internal.lpf"; -#if YUP_WINDOWS || YUP_MAC || YUP_LINUX +#if YUP_DESKTOP /** Stable factory key for VST3 plugin nodes. */ static constexpr const char* pluginVst3Identifier = "plugin.vst3"; @@ -126,7 +126,7 @@ class NodeRegistry }; } -#if YUP_WINDOWS || YUP_MAC || YUP_LINUX +#if YUP_DESKTOP //============================================================================== /** Stores the plugin scanner and host context, then registers factory entries @@ -341,7 +341,7 @@ class NodeRegistry //============================================================================== std::map entries; -#if YUP_WINDOWS || YUP_MAC || YUP_LINUX +#if YUP_DESKTOP yup::AudioPluginScanner* pluginScanner = nullptr; yup::AudioPluginHostContext hostContext; std::vector discoveredPlugins; diff --git a/examples/audiograph/source/ui/PluginEditorWindow.h b/examples/audiograph/source/ui/PluginEditorWindow.h index a6cad3c29..5e5795068 100644 --- a/examples/audiograph/source/ui/PluginEditorWindow.h +++ b/examples/audiograph/source/ui/PluginEditorWindow.h @@ -58,7 +58,8 @@ class PluginEditorWindow final : public yup::DocumentWindow { return yup::ComponentNative::Options() .withResizableWindow (editor != nullptr && editor->isResizable()) - .withRenderContinuous (editor != nullptr && editor->shouldRenderContinuous()); + .withFramerateRedraw (10) + .withRenderContinuous (false); } std::unique_ptr editor; diff --git a/modules/yup_audio_gui/graph/yup_AudioGraphComponent.cpp b/modules/yup_audio_gui/graph/yup_AudioGraphComponent.cpp index 42a8383ed..18c76f93e 100644 --- a/modules/yup_audio_gui/graph/yup_AudioGraphComponent.cpp +++ b/modules/yup_audio_gui/graph/yup_AudioGraphComponent.cpp @@ -352,7 +352,7 @@ void AudioGraphComponent::mouseDown (const MouseEvent& event) mouseDownScreen = screenPos; lastMouseScreen = screenPos; - if (event.isRightButtonDown()) + if (event.getButtons() == MouseEvent::rightButton) { if (auto endpoint = hitTestEndpoint (screenPos)) { diff --git a/modules/yup_gui/menus/yup_PopupMenu.cpp b/modules/yup_gui/menus/yup_PopupMenu.cpp index 5e040dc1c..f5e4ea4dd 100644 --- a/modules/yup_gui/menus/yup_PopupMenu.cpp +++ b/modules/yup_gui/menus/yup_PopupMenu.cpp @@ -40,6 +40,24 @@ void removeActivePopup (PopupMenu* popupMenu) } } +PopupMenu* findActivePopupAt (Point globalPos) +{ + for (auto it = activePopups.rbegin(); it != activePopups.rend(); ++it) + { + auto* popupMenu = dynamic_cast (it->get()); + if (popupMenu != nullptr && popupMenu->getScreenBounds().contains (globalPos)) + return popupMenu; + } + + return nullptr; +} + +MouseEvent makePopupMouseEvent (const MouseEvent& event, PopupMenu& popupMenu, Point globalPos) +{ + return event.withPosition (popupMenu.screenToLocal (globalPos)) + .withSourceComponent (&popupMenu); +} + void installGlobalMouseListener() { static bool mouseListenerAdded = [] @@ -48,32 +66,34 @@ void installGlobalMouseListener() { void mouseDown (const MouseEvent& event) override { - Point globalPos = event.getScreenPosition().to(); + const auto globalPos = event.getScreenPosition(); - bool clickedInsidePopup = false; - for (const auto& popup : activePopups) + // Walk the component hierarchy from the event source. + // If any ancestor is a PopupMenu the click is inside a popup — don't dismiss. + auto* comp = event.getSourceComponent(); + while (comp != nullptr) { - auto* popupMenu = dynamic_cast (popup.get()); - if (popupMenu == nullptr) - continue; - - if (popupMenu->getScreenBounds().contains (globalPos)) - { - clickedInsidePopup = true; - break; - } + if (dynamic_cast (comp) != nullptr) + return; + comp = comp->getParentComponent(); + } - // Also check if clicked inside any submenu - if (popupMenu->submenuContains (globalPos)) - { - clickedInsidePopup = true; - break; - } + if (auto* popupMenu = findActivePopupAt (globalPos)) + { + popupMenu->mouseDown (makePopupMouseEvent (event, *popupMenu, globalPos)); + return; } - if (! clickedInsidePopup && ! activePopups.empty()) + if (! activePopups.empty()) PopupMenu::dismissAllPopups(); } + + void mouseMove (const MouseEvent& event) override + { + const auto globalPos = event.getScreenPosition(); + if (auto* popupMenu = findActivePopupAt (globalPos)) + popupMenu->mouseMove (makePopupMouseEvent (event, *popupMenu, globalPos)); + } } globalMouseListener {}; Desktop::getInstance()->addGlobalMouseListener (&globalMouseListener); @@ -509,7 +529,19 @@ void PopupMenu::positionMenu() availableArea = Rectangle (0, 0, 1920, 1080); // TODO: Move to magic if (auto* desktop = Desktop::getInstance()) { - if (auto screen = desktop->getScreenContaining (this)) + Screen::Ptr screen; + + if (options.positioningMode == PositioningMode::atPoint) + screen = desktop->getScreenContaining (options.targetPosition.to()); + else if (options.positioningMode == PositioningMode::relativeToArea) + screen = desktop->getScreenContaining (options.targetArea.to()); + else if (options.positioningMode == PositioningMode::relativeToComponent && options.targetComponent) + screen = desktop->getScreenContaining (options.targetComponent); + + if (screen == nullptr) + screen = desktop->getScreenContaining (this); + + if (screen != nullptr) availableArea = screen->workArea; } } @@ -605,6 +637,7 @@ void PopupMenu::showCustom (const Options& options, bool isSubmenu, std::functio if (! isSubmenu) dismissAllPopups(); + isBeingDismissed = false; this->options = options; menuCallback = std::move (callback); @@ -633,6 +666,7 @@ void PopupMenu::showCustom (const Options& options, bool isSubmenu, std::functio addToDesktop (nativeOptions); } + removeActivePopup (this); activePopups.push_back (this); setupMenuItems(); @@ -752,7 +786,13 @@ void PopupMenu::mouseEnter (const MouseEvent& event) { int itemIndex = getItemIndexAt (event.getPosition()); if (itemIndex >= 0 && isItemSelectable (itemIndex)) + { setSelectedItemIndex (itemIndex, true); + + auto& item = *items[itemIndex]; + if (item.isSubMenu() && item.isEnabled) + showSubmenu (itemIndex); + } } void PopupMenu::mouseExit (const MouseEvent& event) @@ -895,7 +935,10 @@ PopupMenu::Placement PopupMenu::calculateSubmenuPlacement (Rectangle item availableArea = Rectangle (0, 0, 1920, 1080); // TODO: move to magic if (auto* desktop = Desktop::getInstance()) { - if (auto screen = desktop->getPrimaryScreen()) + Screen::Ptr screen = desktop->getScreenContaining (getScreenBounds().to()); + if (screen == nullptr) + screen = desktop->getPrimaryScreen(); + if (screen != nullptr) availableArea = screen->workArea.to(); } menuBounds = getScreenBounds().to(); @@ -980,9 +1023,11 @@ void PopupMenu::cleanupSubmenu (PopupMenu::Ptr submenu) void PopupMenu::resetInternalState() { + hideSubmenus(); + // Reset flags that might prevent re-showing isBeingDismissed = false; - setSelectedItemIndex (-1, true); + setSelectedItemIndex (-1, false); // Reset scrolling state for scrollable menus visibleItemRange = Range (0, 0); @@ -1080,7 +1125,12 @@ void PopupMenu::selectCurrentItem() void PopupMenu::setSelectedItemIndex (int index, bool fromMouse) { if (selectedItemIndex == index) + { + if (fromMouse) + updateSubmenuVisibility (index); + return; + } if (selectedItemIndex >= 0 && selectedItemIndex < static_cast (items.size())) items[selectedItemIndex]->isHovered = false; @@ -1250,18 +1300,31 @@ void PopupMenu::calculateAvailableHeight() // Use screen bounds if (auto* desktop = Desktop::getInstance()) { - if (auto screen = desktop->getPrimaryScreen()) + Screen::Ptr screen; + + float menuY = 0.0f; + if (options.positioningMode == PositioningMode::atPoint) { - auto screenBounds = screen->workArea.to(); + menuY = options.targetPosition.getY(); + screen = desktop->getScreenContaining (options.targetPosition.to()); + } + else if (options.positioningMode == PositioningMode::relativeToArea) + { + menuY = options.targetArea.getY(); + screen = desktop->getScreenContaining (options.targetArea.to()); + } + else if (options.positioningMode == PositioningMode::relativeToComponent && options.targetComponent) + { + menuY = options.targetComponent->getScreenBounds().getY(); + screen = desktop->getScreenContaining (options.targetComponent); + } - // Estimate menu position for screen coordinate calculation - float menuY = 0.0f; - if (options.positioningMode == PositioningMode::atPoint) - menuY = options.targetPosition.getY(); - else if (options.positioningMode == PositioningMode::relativeToArea) - menuY = options.targetArea.getY(); - else if (options.positioningMode == PositioningMode::relativeToComponent && options.targetComponent) - menuY = options.targetComponent->getScreenBounds().getY(); + if (screen == nullptr) + screen = desktop->getPrimaryScreen(); + + if (screen != nullptr) + { + auto screenBounds = screen->workArea.to(); availableContentHeight = screenBounds.getBottom() - menuY; availableContentHeight = jmax (100.0f, availableContentHeight); diff --git a/modules/yup_gui/native/yup_WindowingUtilities_sdl2.cpp b/modules/yup_gui/native/yup_WindowingUtilities_sdl2.cpp index 1d7f003e8..2d8f2b5b6 100644 --- a/modules/yup_gui/native/yup_WindowingUtilities_sdl2.cpp +++ b/modules/yup_gui/native/yup_WindowingUtilities_sdl2.cpp @@ -42,6 +42,22 @@ MouseEvent::Buttons toMouseButton (Uint8 sdlButton) noexcept } } +MouseEvent::Buttons toMouseButtons (Uint32 sdlButtons) noexcept +{ + int buttons = MouseEvent::Buttons::noButtons; + + if ((sdlButtons & SDL_BUTTON (SDL_BUTTON_LEFT)) != 0) + buttons |= MouseEvent::Buttons::leftButton; + + if ((sdlButtons & SDL_BUTTON (SDL_BUTTON_RIGHT)) != 0) + buttons |= MouseEvent::Buttons::rightButton; + + if ((sdlButtons & SDL_BUTTON (SDL_BUTTON_MIDDLE)) != 0) + buttons |= MouseEvent::Buttons::middleButton; + + return static_cast (buttons); +} + //============================================================================== KeyModifiers toKeyModifiers (Uint16 sdlMod) noexcept diff --git a/modules/yup_gui/native/yup_Windowing_sdl2.cpp b/modules/yup_gui/native/yup_Windowing_sdl2.cpp index 90545bb79..757fac3b6 100644 --- a/modules/yup_gui/native/yup_Windowing_sdl2.cpp +++ b/modules/yup_gui/native/yup_Windowing_sdl2.cpp @@ -815,7 +815,7 @@ void SDL2ComponentNative::handleMouseMoveOrDrag (const Point& position) void SDL2ComponentNative::handleMouseDown (const Point& position, MouseEvent::Buttons button, KeyModifiers modifiers) { - currentMouseButtons = static_cast (currentMouseButtons | button); + currentMouseButtons = static_cast (toMouseButtons (SDL_GetMouseState (nullptr, nullptr)) | button); currentKeyModifiers = modifiers; auto event = MouseEvent() @@ -823,7 +823,7 @@ void SDL2ComponentNative::handleMouseDown (const Point& position, MouseEv .withModifiers (currentKeyModifiers) .withPosition (position); - if (lastComponentClicked == nullptr) + if (currentMouseButtons == button) lastComponentClicked = findComponentForMouseEvent (position); if (lastComponentClicked != nullptr) @@ -843,7 +843,7 @@ void SDL2ComponentNative::handleMouseDown (const Point& position, MouseEv void SDL2ComponentNative::handleMouseUp (const Point& position, MouseEvent::Buttons button, KeyModifiers modifiers) { - currentMouseButtons = static_cast (currentMouseButtons & ~button); + currentMouseButtons = static_cast (toMouseButtons (SDL_GetMouseState (nullptr, nullptr)) & ~button); currentKeyModifiers = modifiers; auto event = MouseEvent() @@ -857,20 +857,22 @@ void SDL2ComponentNative::handleMouseUp (const Point& position, MouseEven if (lastMouseDownTime) event = event.withLastMouseDownTime (*lastMouseDownTime); - if (lastComponentClicked != nullptr) + if (auto* clickedComponent = lastComponentClicked.get()) { const auto currentMouseDownTime = yup::Time::getCurrentTime(); + auto clickedComponentBailOut = Component::BailOutChecker (clickedComponent); - event = event.withSourceComponent (lastComponentClicked); + event = event.withSourceComponent (clickedComponent); if (lastMouseUpTime && *lastMouseUpTime > yup::Time() && currentMouseDownTime - *lastMouseUpTime < doubleClickTime) { - lastComponentClicked->internalMouseDoubleClick (event.withRelativePositionTo (lastComponentClicked)); + clickedComponent->internalMouseDoubleClick (event.withRelativePositionTo (clickedComponent)); } - lastComponentClicked->internalMouseUp (event.withRelativePositionTo (lastComponentClicked)); + if (! clickedComponentBailOut.shouldBailOut()) + clickedComponent->internalMouseUp (event.withRelativePositionTo (clickedComponent)); lastMouseUpTime = currentMouseDownTime; } @@ -1027,7 +1029,8 @@ void SDL2ComponentNative::handleResized (int width, int height) setPosition (nativeWindowPos.getTopLeft()); } - PopupMenu::dismissAllPopups(); + if (dynamic_cast (&component) == nullptr) + PopupMenu::dismissAllPopups(); repaint(); } From 3ed79e8066d1ed6a26dce728c1d72d6ef131b3fb Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 18 May 2026 18:14:44 +0200 Subject: [PATCH 24/35] Fix issues --- examples/audiograph/source/AudioGraphApp.h | 5 +- .../graph/yup_AudioGraphComponent.cpp | 8 ++- .../yup_gui/component/yup_ComponentNative.cpp | 9 +++ .../yup_gui/component/yup_ComponentNative.h | 18 ++++- modules/yup_gui/menus/yup_PopupMenu.cpp | 71 +++++++++++++++++-- modules/yup_gui/menus/yup_PopupMenu.h | 16 +++++ modules/yup_gui/native/yup_WindowingHelpers.h | 7 ++ .../native/yup_WindowingUtilities_sdl2.cpp | 5 ++ .../yup_gui/native/yup_Windowing_linux.cpp | 5 ++ modules/yup_gui/native/yup_Windowing_mac.mm | 14 ++++ modules/yup_gui/native/yup_Windowing_sdl2.cpp | 26 +++++-- .../yup_gui/native/yup_Windowing_windows.cpp | 9 +++ 12 files changed, 178 insertions(+), 15 deletions(-) diff --git a/examples/audiograph/source/AudioGraphApp.h b/examples/audiograph/source/AudioGraphApp.h index 2ef7be180..20bbee283 100644 --- a/examples/audiograph/source/AudioGraphApp.h +++ b/examples/audiograph/source/AudioGraphApp.h @@ -434,7 +434,9 @@ class AudioGraphApp final pendingMenuCanvasPos = canvasPos; const auto screenPos = graphComponent->localToScreen (graphComponent->canvasToScreen (canvasPos)); - const auto options = yup::PopupMenu::Options {}.withPosition (screenPos, yup::Justification::topLeft); + const auto options = yup::PopupMenu::Options {} + .withFocusComponent (graphComponent.get()) + .withPosition (screenPos, yup::Justification::topLeft); auto internalSubMenu = yup::PopupMenu::create(); internalSubMenu->addItem ("Oscillator", 1); @@ -485,6 +487,7 @@ class AudioGraphApp final else if (selectedID >= 100) { const auto& plugins = nodeRegistry.getDiscoveredPlugins(); + const auto index = static_cast (selectedID - 100); if (index < plugins.size()) addPluginNode (plugins[index], pendingMenuCanvasPos); diff --git a/modules/yup_audio_gui/graph/yup_AudioGraphComponent.cpp b/modules/yup_audio_gui/graph/yup_AudioGraphComponent.cpp index 18c76f93e..07de1dfec 100644 --- a/modules/yup_audio_gui/graph/yup_AudioGraphComponent.cpp +++ b/modules/yup_audio_gui/graph/yup_AudioGraphComponent.cpp @@ -388,10 +388,11 @@ void AudioGraphComponent::mouseDown (const MouseEvent& event) return; } - if (spacebarDown) + if (spacebarDown || event.getButtons() == MouseEvent::middleButton) { interaction = Interaction::panningCanvas; panStartOffset = canvasOffset; + setMouseCursor (MouseCursor::ResizeAll); return; } @@ -525,6 +526,7 @@ void AudioGraphComponent::mouseUp (const MouseEvent& event) case Interaction::panningCanvas: interaction = Interaction::idle; + setMouseCursor (MouseCursor::Default); break; case Interaction::armedPort: @@ -578,6 +580,7 @@ void AudioGraphComponent::keyDown (const KeyPress& keys, const Point&) if (keys.getKey() == KeyPress::spaceKey) { spacebarDown = true; + setMouseCursor (MouseCursor::ResizeAll); return; } @@ -594,7 +597,10 @@ void AudioGraphComponent::keyDown (const KeyPress& keys, const Point&) void AudioGraphComponent::keyUp (const KeyPress& keys, const Point&) { if (keys.getKey() == KeyPress::spaceKey) + { spacebarDown = false; + setMouseCursor (MouseCursor::Default); + } } //============================================================================== diff --git a/modules/yup_gui/component/yup_ComponentNative.cpp b/modules/yup_gui/component/yup_ComponentNative.cpp index ca2b8321c..815911174 100644 --- a/modules/yup_gui/component/yup_ComponentNative.cpp +++ b/modules/yup_gui/component/yup_ComponentNative.cpp @@ -66,6 +66,15 @@ ComponentNative::Options& ComponentNative::Options::withAllowedHighDensityDispla return *this; } +ComponentNative::Options& ComponentNative::Options::withTemporaryWindow (bool shouldBeTemporary) noexcept +{ + if (shouldBeTemporary) + flags |= temporaryWindow; + else + flags &= ~temporaryWindow; + return *this; +} + ComponentNative::Options& ComponentNative::Options::withGraphicsApi (std::optional newGraphicsApi) noexcept { graphicsApi = newGraphicsApi; diff --git a/modules/yup_gui/component/yup_ComponentNative.h b/modules/yup_gui/component/yup_ComponentNative.h index 3c4d23b64..ab64fdc5f 100644 --- a/modules/yup_gui/component/yup_ComponentNative.h +++ b/modules/yup_gui/component/yup_ComponentNative.h @@ -41,6 +41,7 @@ class YUP_API ComponentNative : public ReferenceCountedObject { struct decoratedWindowTag; struct resizableWindowTag; + struct temporaryWindowTag; struct renderContinuousTag; struct allowHighDensityDisplayTag; @@ -51,7 +52,12 @@ class YUP_API ComponentNative : public ReferenceCountedObject //============================================================================== /** Type definition for window configuration flags. */ - using Flags = FlagSet; + using Flags = FlagSet; /** No flags set. */ static inline constexpr Flags noFlags = Flags(); @@ -59,6 +65,8 @@ class YUP_API ComponentNative : public ReferenceCountedObject static inline constexpr Flags decoratedWindow = Flags::declareValue(); /** Flag to enable window resizing by the user. */ static inline constexpr Flags resizableWindow = Flags::declareValue(); + /** Flag to mark the native window as a temporary popup/menu-style window. */ + static inline constexpr Flags temporaryWindow = Flags::declareValue(); /** Flag to enable continuous rendering mode. */ static inline constexpr Flags renderContinuous = Flags::declareValue(); /** Flag to enable high-density display support. */ @@ -119,6 +127,14 @@ class YUP_API ComponentNative : public ReferenceCountedObject */ Options& withAllowedHighDensityDisplay (bool shouldAllowHighDensity) noexcept; + /** Sets whether the window should be treated as a temporary popup/menu window. + + @param shouldBeTemporary True for popup/menu-style windows, false for regular windows. + + @return Reference to this Options object for method chaining. + */ + Options& withTemporaryWindow (bool shouldBeTemporary) noexcept; + /** Sets the graphics API to be used for rendering. @param newGraphicsApi The graphics API to use, or std::nullopt to use the default. diff --git a/modules/yup_gui/menus/yup_PopupMenu.cpp b/modules/yup_gui/menus/yup_PopupMenu.cpp index f5e4ea4dd..1bf617a7c 100644 --- a/modules/yup_gui/menus/yup_PopupMenu.cpp +++ b/modules/yup_gui/menus/yup_PopupMenu.cpp @@ -313,6 +313,7 @@ bool PopupMenu::Item::isCustomComponent() const PopupMenu::Options::Options() : parentComponent (nullptr) , targetComponent (nullptr) + , focusComponent (nullptr) , alignment (Justification::topLeft) , placement (Placement::below()) , positioningMode (PositioningMode::atPoint) @@ -361,6 +362,12 @@ PopupMenu::Options& PopupMenu::Options::withRelativePosition (Component* compone return *this; } +PopupMenu::Options& PopupMenu::Options::withFocusComponent (Component* component) +{ + this->focusComponent = component; + return *this; +} + PopupMenu::Options& PopupMenu::Options::withMinimumWidth (int minWidth) { this->minWidth = minWidth; @@ -641,6 +648,9 @@ void PopupMenu::showCustom (const Options& options, bool isSubmenu, std::functio this->options = options; menuCallback = std::move (callback); + if (! isSubmenu) + focusComponentToRestore = getFocusComponentForDismissal(); + if (isEmpty()) { dismiss(); @@ -660,7 +670,8 @@ void PopupMenu::showCustom (const Options& options, bool isSubmenu, std::functio // When we have no parent component, add to desktop to work in screen coordinates auto nativeOptions = ComponentNative::Options {} .withDecoration (false) - .withResizableWindow (false); + .withResizableWindow (false) + .withTemporaryWindow (true); if (! isOnDesktop()) addToDesktop (nativeOptions); @@ -694,8 +705,13 @@ void PopupMenu::dismiss (int itemID) setVisible (false); + if (getParentComponent() == nullptr && isOnDesktop()) + removeFromDesktop(); + selectedItemIndex = -1; + restoreFocusAfterDismissal(); + if (auto itemCallback = std::exchange (menuCallback, {})) itemCallback (itemID); @@ -714,6 +730,34 @@ bool PopupMenu::isBeingShown() const //============================================================================== +Component* PopupMenu::getFocusComponentForDismissal() const +{ + if (options.focusComponent != nullptr) + return options.focusComponent; + + auto* referenceComponent = options.targetComponent != nullptr ? options.targetComponent + : options.parentComponent; + if (referenceComponent == nullptr) + return nullptr; + + if (auto* nativeComponent = referenceComponent->getNativeComponent()) + return nativeComponent->getFocusedComponent(); + + return nullptr; +} + +void PopupMenu::restoreFocusAfterDismissal() +{ + auto focusToRestore = std::exchange (focusComponentToRestore, {}); + if (focusToRestore == nullptr || focusToRestore == this) + return; + + if (auto* nativeToRestore = focusToRestore->getNativeComponent()) + nativeToRestore->setFocusedComponent (focusToRestore.get()); +} + +//============================================================================== + void PopupMenu::paint (Graphics& g) { if (auto style = ApplicationTheme::findComponentStyle (*this)) @@ -738,18 +782,33 @@ void PopupMenu::mouseDown (const MouseEvent& event) if (item.isSeparator() || ! item.isEnabled) return; + setSelectedItemIndex (itemIndex, true); + if (item.isSubMenu()) { // For submenus, we show them on hover, not on click showSubmenu (itemIndex); } - else - { - // Hide any visible submenus when selecting a non-separator item - hideSubmenus(); +} - dismiss (item.itemID); +void PopupMenu::mouseUp (const MouseEvent& event) +{ + if (! getLocalBounds().contains (event.getPosition())) + { + dismiss(); + return; } + + auto itemIndex = getItemIndexAt (event.getPosition()); + if (! isPositiveAndBelow (itemIndex, getNumItems())) + return; + + auto& item = *items[itemIndex]; + if (item.isSeparator() || ! item.isEnabled || item.isSubMenu()) + return; + + hideSubmenus(); + dismiss (item.itemID); } void PopupMenu::mouseMove (const MouseEvent& event) diff --git a/modules/yup_gui/menus/yup_PopupMenu.h b/modules/yup_gui/menus/yup_PopupMenu.h index 6a7f752f1..3090bcd0f 100644 --- a/modules/yup_gui/menus/yup_PopupMenu.h +++ b/modules/yup_gui/menus/yup_PopupMenu.h @@ -126,6 +126,16 @@ class YUP_API PopupMenu */ Options& withRelativePosition (Component* component, Placement placement = Placement::below()); + /** Sets the component that should regain focus when the menu closes. + + This does not affect menu positioning or parenting. It is useful for desktop popup + menus that are shown in their own native window but should return focus to the + component that opened them. + + @param component The component to focus after the menu closes + */ + Options& withFocusComponent (Component* component); + /** Minimum width for the menu. */ Options& withMinimumWidth (int minWidth); @@ -134,6 +144,7 @@ class YUP_API PopupMenu Component* parentComponent; Component* targetComponent; + Component* focusComponent; Point targetPosition; Rectangle targetArea; Justification alignment; @@ -322,6 +333,8 @@ class YUP_API PopupMenu /** @internal */ void mouseDown (const MouseEvent& event) override; /** @internal */ + void mouseUp (const MouseEvent& event) override; + /** @internal */ void mouseMove (const MouseEvent& event) override; /** @internal */ void mouseEnter (const MouseEvent& event) override; @@ -342,6 +355,8 @@ class YUP_API PopupMenu void setupMenuItems(); void positionMenu(); void resetInternalState(); + Component* getFocusComponentForDismissal() const; + void restoreFocusAfterDismissal(); // Submenu functionality void showSubmenu (int itemIndex); @@ -391,6 +406,7 @@ class YUP_API PopupMenu Options options; int selectedItemIndex = -1; // For keyboard navigation bool isBeingDismissed = false; + WeakReference focusComponentToRestore; std::function menuCallback; diff --git a/modules/yup_gui/native/yup_WindowingHelpers.h b/modules/yup_gui/native/yup_WindowingHelpers.h index e9b85358b..81fa41f49 100644 --- a/modules/yup_gui/native/yup_WindowingHelpers.h +++ b/modules/yup_gui/native/yup_WindowingHelpers.h @@ -34,4 +34,11 @@ namespace yup */ Rectangle getNativeWindowPosition (void* nativeWindow); +/** + Gives focus to a native window handle when supported by the current platform. + + @param nativeWindow The native window handle. + */ +void focusNativeWindow (void* nativeWindow); + } // namespace yup diff --git a/modules/yup_gui/native/yup_WindowingUtilities_sdl2.cpp b/modules/yup_gui/native/yup_WindowingUtilities_sdl2.cpp index 2d8f2b5b6..5681b14c3 100644 --- a/modules/yup_gui/native/yup_WindowingUtilities_sdl2.cpp +++ b/modules/yup_gui/native/yup_WindowingUtilities_sdl2.cpp @@ -288,6 +288,11 @@ Rectangle getNativeWindowPosition (void* nativeWindow) { return {}; } + +void focusNativeWindow (void* nativeWindow) +{ + ignoreUnused (nativeWindow); +} #endif //============================================================================== diff --git a/modules/yup_gui/native/yup_Windowing_linux.cpp b/modules/yup_gui/native/yup_Windowing_linux.cpp index 93882c4f1..fb2ea6bbe 100644 --- a/modules/yup_gui/native/yup_Windowing_linux.cpp +++ b/modules/yup_gui/native/yup_Windowing_linux.cpp @@ -98,4 +98,9 @@ Rectangle getNativeWindowPosition (void* nativeWindow) return {}; } +void focusNativeWindow (void* nativeWindow) +{ + ignoreUnused (nativeWindow); +} + } // namespace yup diff --git a/modules/yup_gui/native/yup_Windowing_mac.mm b/modules/yup_gui/native/yup_Windowing_mac.mm index 4cc61c684..39c2fd94c 100644 --- a/modules/yup_gui/native/yup_Windowing_mac.mm +++ b/modules/yup_gui/native/yup_Windowing_mac.mm @@ -52,4 +52,18 @@ static_cast(NSHeight(windowRect))}; } +void focusNativeWindow (void* nativeWindow) +{ + if (nativeWindow == nullptr) + return; + + id nativeObject = (__bridge id) nativeWindow; + NSWindow* window = [nativeObject isKindOfClass:[NSWindow class]] + ? (NSWindow*) nativeObject + : [nativeObject window]; + + [NSApp activateIgnoringOtherApps:YES]; + [window makeKeyAndOrderFront:nil]; +} + } // namespace yup diff --git a/modules/yup_gui/native/yup_Windowing_sdl2.cpp b/modules/yup_gui/native/yup_Windowing_sdl2.cpp index 757fac3b6..4b0421ba8 100644 --- a/modules/yup_gui/native/yup_Windowing_sdl2.cpp +++ b/modules/yup_gui/native/yup_Windowing_sdl2.cpp @@ -71,6 +71,9 @@ SDL2ComponentNative::SDL2ComponentNative (Component& component, if (options.flags.test (allowHighDensityDisplay)) windowFlags |= SDL_WINDOW_ALLOW_HIGHDPI; + if (options.flags.test (temporaryWindow)) + windowFlags |= SDL_WINDOW_POPUP_MENU | SDL_WINDOW_SKIP_TASKBAR | SDL_WINDOW_ALWAYS_ON_TOP; + if (! options.flags.test (decoratedWindow)) windowFlags |= SDL_WINDOW_BORDERLESS; @@ -405,10 +408,13 @@ void SDL2ComponentNative::setFocusedComponent (Component* comp) lastComponentFocused->repaint(); } - if (window != nullptr) + if (window != nullptr && isVisible() && lastComponentFocused != nullptr) { - if ((SDL_GetWindowFlags (window) & SDL_WINDOW_INPUT_FOCUS) == 0) // SDL_WINDOW_MOUSE_FOCUS - SDL_SetWindowInputFocus (window); + SDL_RaiseWindow (window); + + focusNativeWindow (getNativeHandle()); + + SDL_SetWindowInputFocus (window); } } @@ -857,6 +863,8 @@ void SDL2ComponentNative::handleMouseUp (const Point& position, MouseEven if (lastMouseDownTime) event = event.withLastMouseDownTime (*lastMouseDownTime); + auto nativeBailOut = Component::BailOutChecker (&component); + if (auto* clickedComponent = lastComponentClicked.get()) { const auto currentMouseDownTime = yup::Time::getCurrentTime(); @@ -877,6 +885,9 @@ void SDL2ComponentNative::handleMouseUp (const Point& position, MouseEven lastMouseUpTime = currentMouseDownTime; } + if (nativeBailOut.shouldBailOut()) + return; + if (currentMouseButtons == MouseEvent::noButtons) { updateComponentUnderMouse (event); @@ -1420,8 +1431,11 @@ int SDL2ComponentNative::eventDispatcher (void* userdata, SDL_Event* event) default: { - if (auto nativeComponent = Desktop::getInstance()->getNativeComponent (userdata)) - dynamic_cast (nativeComponent.get())->handleEvent (event); + if (auto component = Desktop::getInstance()->getNativeComponent (userdata)) + { + if (auto nativeComponent = dynamic_cast (component.get())) + nativeComponent->handleEvent (event); + } break; } @@ -1608,7 +1622,7 @@ void Desktop::setMouseCursor (const MouseCursor& cursorToSet) { MouseCursor::ResizeUpDown, SDL_CreateSystemCursor (SDL_SYSTEM_CURSOR_SIZENS) }, { MouseCursor::ResizeTopLeftRightBottom, SDL_CreateSystemCursor (SDL_SYSTEM_CURSOR_SIZENWSE) }, { MouseCursor::ResizeBottomLeftRightTop, SDL_CreateSystemCursor (SDL_SYSTEM_CURSOR_SIZENESW) }, - { MouseCursor::ResizeAll, SDL_CreateSystemCursor (SDL_SYSTEM_CURSOR_ARROW) } + { MouseCursor::ResizeAll, SDL_CreateSystemCursor (SDL_SYSTEM_CURSOR_SIZEALL) } }; }(); diff --git a/modules/yup_gui/native/yup_Windowing_windows.cpp b/modules/yup_gui/native/yup_Windowing_windows.cpp index bd62203d8..c32a94d82 100644 --- a/modules/yup_gui/native/yup_Windowing_windows.cpp +++ b/modules/yup_gui/native/yup_Windowing_windows.cpp @@ -38,4 +38,13 @@ Rectangle getNativeWindowPosition (void* nativeWindow) }; } +void focusNativeWindow (void* nativeWindow) +{ + auto hwnd = reinterpret_cast (nativeWindow); + if (hwnd == nullptr) + return; + + SetForegroundWindow (hwnd); +} + } // namespace yup From e52820ea0c73a1f20a29917bd3c98a86b75f8936 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 18 May 2026 22:51:26 +0200 Subject: [PATCH 25/35] More tweaks --- README.md | 9 + examples/audiograph/CMakeLists.txt | 1 + examples/audiograph/source/AudioGraphApp.h | 6 +- examples/audiograph/source/main.cpp | 2 + .../audiograph/source/nodes/NodeRegistry.h | 24 +- .../source/nodes/SamplePlayerNode.h | 426 ++++++++++++++++++ .../audiograph/source/ui/PluginEditorWindow.h | 2 +- .../waveform/yup_AudioThumbnail.cpp | 3 +- .../yup_graphics/graphics/yup_Graphics.cpp | 6 + modules/yup_gui/native/yup_FileChooser_mac.mm | 145 +++--- modules/yup_gui/yup_gui.h | 2 +- 11 files changed, 545 insertions(+), 81 deletions(-) create mode 100644 examples/audiograph/source/nodes/SamplePlayerNode.h diff --git a/README.md b/README.md index d34125141..acfc0f07b 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,14 @@ YUP brings a suite of powerful features, including: | **Linux** | :construction: | :construction: | | | | | | +## Supported Plugin Hosting Formats +| | **CLAP** | **VST3** | **VST2** | **AUv3** | **AUv2** | **AAX** | **LV2** | +|--------------------------|:------------------:|:------------------:|:------------------:|:------------------:|:-------------------------:|:---------------------:|:---------------------:| +| **Windows** | :construction: | :construction: | | | | | | +| **macOS** | :construction: | :construction: | | | :white_check_mark: | | | +| **Linux** | :construction: | :construction: | | | | | | + + ## Supported Sound Formats | | **Wav** | **Wav64** | **Mp3** | **OGG** | **Flac** | **Opus** | **AAC** | **WMF** | @@ -139,6 +147,7 @@ YUP brings a suite of powerful features, including: | **iOS** (enc) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | **iOS** (dec) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | + ## Prerequisites Before building, ensure you have a: - C++20-compliant compiler diff --git a/examples/audiograph/CMakeLists.txt b/examples/audiograph/CMakeLists.txt index bbfeebf36..dc8a0dde2 100644 --- a/examples/audiograph/CMakeLists.txt +++ b/examples/audiograph/CMakeLists.txt @@ -42,6 +42,7 @@ yup_standalone_app ( yup::yup_events yup::yup_graphics yup::yup_gui + yup::yup_audio_formats yup::yup_audio_gui yup::yup_audio_processors yup::yup_audio_graph diff --git a/examples/audiograph/source/AudioGraphApp.h b/examples/audiograph/source/AudioGraphApp.h index 20bbee283..7f0647969 100644 --- a/examples/audiograph/source/AudioGraphApp.h +++ b/examples/audiograph/source/AudioGraphApp.h @@ -442,6 +442,7 @@ class AudioGraphApp final internalSubMenu->addItem ("Oscillator", 1); internalSubMenu->addItem ("Gain", 2); internalSubMenu->addItem ("Low Pass Filter", 3); + internalSubMenu->addItem ("Sample Player", 4); activeMenu = yup::PopupMenu::create (options); activeMenu->addSubMenu ("Internal Nodes", internalSubMenu); @@ -476,10 +477,11 @@ class AudioGraphApp final const yup::String internalIds[] = { NodeRegistry::oscillatorIdentifier, NodeRegistry::gainIdentifier, - NodeRegistry::lpfIdentifier + NodeRegistry::lpfIdentifier, + NodeRegistry::samplePlayerIdentifier }; - if (selectedID >= 1 && selectedID <= 3) + if (selectedID >= 1 && selectedID <= 4) { addInternalNode (internalIds[selectedID - 1], pendingMenuCanvasPos); } diff --git a/examples/audiograph/source/main.cpp b/examples/audiograph/source/main.cpp index cae671fe4..7f7fc4f24 100644 --- a/examples/audiograph/source/main.cpp +++ b/examples/audiograph/source/main.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,7 @@ #include "nodes/OscillatorNode.h" #include "nodes/GainNode.h" #include "nodes/LowPassFilterNode.h" +#include "nodes/SamplePlayerNode.h" #include "nodes/PluginNodeView.h" #include "nodes/NodeRegistry.h" #include "ui/PluginEditorWindow.h" diff --git a/examples/audiograph/source/nodes/NodeRegistry.h b/examples/audiograph/source/nodes/NodeRegistry.h index 03fa37f68..d8db05e23 100644 --- a/examples/audiograph/source/nodes/NodeRegistry.h +++ b/examples/audiograph/source/nodes/NodeRegistry.h @@ -48,6 +48,9 @@ class NodeRegistry /** Stable factory key for the built-in low-pass filter node. */ static constexpr const char* lpfIdentifier = "internal.lpf"; + /** Stable factory key for the built-in looping sample player node. */ + static constexpr const char* samplePlayerIdentifier = "internal.samplePlayer"; + #if YUP_DESKTOP /** Stable factory key for VST3 plugin nodes. */ static constexpr const char* pluginVst3Identifier = "plugin.vst3"; @@ -72,7 +75,7 @@ class NodeRegistry //============================================================================== /** - Registers the built-in internal node types: oscillator, gain, and low-pass filter. + Registers the built-in internal node types. Each entry gets a processor factory and a matching view factory. The view factory dynamic_casts to the concrete processor type before constructing @@ -124,6 +127,21 @@ class NodeRegistry return std::make_unique (nodeID, *lpf); } }; + + entries[samplePlayerIdentifier] = { + [] (const yup::AudioGraphNodeProperties&) -> yup::ResultValue> + { + return yup::makeResultValueOk (std::make_unique()); + }, + [] (yup::AudioGraphNodeID nodeID, yup::AudioProcessor* proc) -> std::unique_ptr + { + auto* samplePlayer = dynamic_cast (proc); + if (samplePlayer == nullptr) + return nullptr; + + return std::make_unique (nodeID, *samplePlayer); + } + }; } #if YUP_DESKTOP @@ -314,7 +332,7 @@ class NodeRegistry //============================================================================== std::vector getInternalNodeIdentifiers() const { - return { oscillatorIdentifier, gainIdentifier, lpfIdentifier }; + return { oscillatorIdentifier, gainIdentifier, lpfIdentifier, samplePlayerIdentifier }; } //============================================================================== @@ -333,6 +351,8 @@ class NodeRegistry return "Gain"; if (id == lpfIdentifier) return "Low Pass Filter"; + if (id == samplePlayerIdentifier) + return "Sample Player"; return id; } diff --git a/examples/audiograph/source/nodes/SamplePlayerNode.h b/examples/audiograph/source/nodes/SamplePlayerNode.h new file mode 100644 index 000000000..c22620e9c --- /dev/null +++ b/examples/audiograph/source/nodes/SamplePlayerNode.h @@ -0,0 +1,426 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +#include +#include +#include +#include + +#include "NodeViewHelpers.h" + +//============================================================================== +class SamplePlayerProcessor final : public yup::AudioProcessor +{ +public: + struct SampleData + { + yup::AudioBuffer buffer; + double sampleRate = 44100.0; + yup::File file; + }; + + SamplePlayerProcessor() + : AudioProcessor ("Sample Player", + yup::AudioBusLayout ({}, + { yup::AudioBus ("Main", yup::AudioBus::Audio, yup::AudioBus::Output, 2) })) + { + } + + void prepareToPlay (float newSampleRate, int) override + { + playbackSampleRate.store (newSampleRate > 0.0f ? newSampleRate : 44100.0f, std::memory_order_relaxed); + } + + void releaseResources() override {} + + void processBlock (yup::AudioBuffer& audioBuffer, yup::MidiBuffer&) override + { + audioBuffer.clear(); + + const auto* sample = currentSample.load (std::memory_order_acquire); + if (sample == nullptr || sample->buffer.getNumSamples() <= 0 || sample->buffer.getNumChannels() <= 0) + return; + + const auto& source = sample->buffer; + const int sourceNumSamples = source.getNumSamples(); + const int sourceNumChannels = source.getNumChannels(); + const double outputSampleRate = static_cast (playbackSampleRate.load (std::memory_order_relaxed)); + const double sourceSampleRate = sample->sampleRate > 0.0 ? sample->sampleRate : outputSampleRate; + const double positionIncrement = sourceSampleRate / yup::jmax (1.0, outputSampleRate); + + double position = playbackPosition.load (std::memory_order_relaxed); + + while (position >= static_cast (sourceNumSamples)) + position -= static_cast (sourceNumSamples); + + for (int sampleIndex = 0; sampleIndex < audioBuffer.getNumSamples(); ++sampleIndex) + { + const int index0 = yup::jlimit (0, sourceNumSamples - 1, static_cast (position)); + const int index1 = (index0 + 1) < sourceNumSamples ? index0 + 1 : 0; + const float alpha = static_cast (position - static_cast (index0)); + + for (int channel = 0; channel < audioBuffer.getNumChannels(); ++channel) + { + const int sourceChannel = yup::jmin (channel, sourceNumChannels - 1); + const float s0 = source.getSample (sourceChannel, index0); + const float s1 = source.getSample (sourceChannel, index1); + audioBuffer.setSample (channel, sampleIndex, s0 + (s1 - s0) * alpha); + } + + position += positionIncrement; + while (position >= static_cast (sourceNumSamples)) + position -= static_cast (sourceNumSamples); + } + + playbackPosition.store (position, std::memory_order_relaxed); + } + + int getCurrentPreset() const noexcept override { return 0; } + + void setCurrentPreset (int) noexcept override {} + + int getNumPresets() const override { return 0; } + + yup::String getPresetName (int) const override { return {}; } + + void setPresetName (int, yup::StringRef) override {} + + yup::Result loadStateFromMemory (const yup::MemoryBlock& data) override + { + if (data.isEmpty()) + return yup::Result::ok(); + + yup::MemoryInputStream stream (data, false); + const auto path = stream.readEntireStreamAsString(); + + if (path.isEmpty()) + return yup::Result::ok(); + + const auto result = loadSampleFile (yup::File (path)); + return result.wasOk() ? result : yup::Result::ok(); + } + + yup::Result saveStateIntoMemory (yup::MemoryBlock& data) override + { + yup::MemoryOutputStream stream (data, false); + stream.writeText (getSampleFile().getFullPathName(), false, false, nullptr); + stream.flush(); + return yup::Result::ok(); + } + + bool hasEditor() const override { return false; } + + yup::AudioProcessorEditor* createEditor() override { return nullptr; } + + yup::Result loadSampleFile (const yup::File& file) + { + if (! file.existsAsFile()) + return yup::Result::fail ("File not found: " + file.getFullPathName()); + + yup::AudioFormatManager formatManager; + formatManager.registerDefaultFormats(); + + auto reader = formatManager.createReaderFor (file); + if (reader == nullptr) + return yup::Result::fail ("Unsupported or unreadable format: " + file.getFileName()); + + if (reader->numChannels <= 0 || reader->lengthInSamples <= 0) + return yup::Result::fail ("The selected file contains no audio."); + + if (reader->lengthInSamples > static_cast (std::numeric_limits::max())) + return yup::Result::fail ("The selected file is too long to load into memory."); + + auto data = std::make_shared(); + data->sampleRate = reader->sampleRate > 0.0 ? reader->sampleRate : 44100.0; + data->file = file; + data->buffer.setSize (reader->numChannels, static_cast (reader->lengthInSamples)); + + if (! reader->read (&data->buffer, 0, data->buffer.getNumSamples(), 0, true, true)) + return yup::Result::fail ("Could not read audio from: " + file.getFileName()); + + auto retainedSample = std::shared_ptr (std::move (data)); + currentSampleForUi = retainedSample; + retainedSamples.push_back (std::move (retainedSample)); + currentSample.store (currentSampleForUi.get(), std::memory_order_release); + + playbackPosition.store (0.0, std::memory_order_relaxed); + return yup::Result::ok(); + } + + std::shared_ptr getCurrentSample() const + { + return currentSampleForUi; + } + + yup::File getSampleFile() const + { + const auto sample = getCurrentSample(); + return sample != nullptr ? sample->file : yup::File(); + } + + double getPlaybackPositionSamples() const noexcept + { + return playbackPosition.load (std::memory_order_relaxed); + } + +private: + std::atomic currentSample { nullptr }; + std::shared_ptr currentSampleForUi; + // Keeps previously loaded buffers alive because the audio thread reads currentSample as a raw pointer. + std::vector> retainedSamples; + std::atomic playbackPosition { 0.0 }; + std::atomic playbackSampleRate { 44100.0f }; +}; + +//============================================================================== +class SampleWaveformThumbnail final : public yup::AudioThumbnail +{ +public: + explicit SampleWaveformThumbnail (yup::Color waveformColorIn) + : waveformColor (waveformColorIn) + { + } + + yup::Color getChannelColor (int) const override + { + return waveformColor; + } + +private: + yup::Color waveformColor; +}; + +//============================================================================== +class SampleWaveformComponent final + : public yup::Component + , public yup::AudioThumbnail::Listener + , public yup::Timer +{ +public: + SampleWaveformComponent (SamplePlayerProcessor& processorIn, yup::Color waveformColor) + : processor (processorIn) + , thumbnail (waveformColor) + , accentColor (waveformColor) + { + thumbnail.addListener (this); + setOpaque (false); + } + + ~SampleWaveformComponent() override + { + stopTimer(); + thumbnail.removeListener (this); + } + + void setSample (std::shared_ptr sample) + { + currentSample = std::move (sample); + + if (currentSample == nullptr) + { + thumbnail.clear(); + stopTimer(); + repaint(); + return; + } + + thumbnail.setSource (currentSample->buffer, currentSample->sampleRate); + startTimerHz (30); + repaint(); + } + + void paint (yup::Graphics& g) override + { + const auto bounds = getLocalBounds().to(); + + if (currentSample == nullptr || thumbnail.getTotalSamples() <= 0) + { + g.setFillColor (accentColor.withAlpha (0.58f)); + g.fillFittedText ("Load sample", yup::ApplicationTheme::getGlobalTheme()->getDefaultFont().withHeight (10.0f), bounds.reduced (6.0f), yup::Justification::center); + return; + } + + const int numChannels = yup::jmin (2, thumbnail.getNumChannels()); + const auto sampleRange = yup::Range (0.0, static_cast (thumbnail.getTotalSamples())); + const auto laneBounds = bounds.reduced (5.0f, 3.0f); + const float laneHeight = laneBounds.getHeight() / static_cast (numChannels); + + for (int channel = 0; channel < numChannels; ++channel) + { + const auto lane = laneBounds.withY (laneBounds.getY() + static_cast (channel) * laneHeight) + .withHeight (laneHeight - 2.0f); + + g.setFillColor (accentColor.withAlpha (0.12f)); + g.fillRect ({ lane.getX(), lane.getCenterY(), lane.getWidth(), 1.0f }); + + thumbnail.paintChannel (g, lane, channel, sampleRange, lane.getWidth()); + } + + const auto playheadX = yup::jlimit (laneBounds.getX(), + laneBounds.getRight() - 1.5f, + laneBounds.getX() + + laneBounds.getWidth() + * static_cast (yup::jlimit (0.0, + 1.0, + processor.getPlaybackPositionSamples() + / static_cast (thumbnail.getTotalSamples())))); + + g.setFillColor (accentColor.withAlpha (0.95f)); + g.fillRect ({ playheadX, laneBounds.getY(), 1.5f, laneBounds.getHeight() }); + + if (thumbnail.isProgressVisible()) + { + const auto progressBounds = bounds.withY (bounds.getBottom() - 3.0f).withHeight (3.0f); + g.setFillColor (accentColor.withAlpha (0.80f)); + g.fillRect (progressBounds.withWidth (progressBounds.getWidth() * static_cast (thumbnail.getProgress()))); + } + } + +private: + void thumbnailChanged (yup::AudioThumbnail&) override { repaint(); } + + void thumbnailProgressChanged (yup::AudioThumbnail&, double, bool) override { repaint(); } + + void timerCallback() override { repaint(); } + + SamplePlayerProcessor& processor; + SampleWaveformThumbnail thumbnail; + yup::Color accentColor; + std::shared_ptr currentSample; +}; + +//============================================================================== +class SamplePlayerNodeView final : public yup::AudioGraphNodeView +{ +public: + SamplePlayerNodeView (yup::AudioGraphNodeID nodeID, SamplePlayerProcessor& processorIn) + : AudioGraphNodeView (nodeID) + , processor (processorIn) + , waveform (processorIn, getNodeColor()) + { + setColor (Style::parameterBackgroundColorId, yup::Color (0x00000000)); + setColor (Style::parameterValueBackgroundColorId, yup::Color (0x00000000)); + + loadButton.setButtonText ("Load"); + loadButton.onClick = [this] + { + chooseSampleFile(); + }; + addAndMakeVisible (loadButton); + + waveform.setSample (processor.getCurrentSample()); + addAndMakeVisible (waveform); + } + + yup::String getNodeTitle() const override { return "SAMPLE"; } + + int getNumInputPorts() const override { return 0; } + + int getNumOutputPorts() const override { return 1; } + + int getPreferredWidth() const override { return 260; } + + yup::Color getNodeColor() const override { return yup::Color (0xff38bdf8); } + + yup::String getNodeSubtitle() const override + { + if (loadError.isNotEmpty()) + return loadError; + + const auto sample = processor.getCurrentSample(); + return sample != nullptr ? sample->file.getFileName() : "loop player"; + } + + PortInfo getOutputPortInfo (int) const override { return { "audio", getPortKindColor (PortKind::audio), PortKind::audio }; } + + int getNumParameterRows() const override { return 3; } + + ParameterInfo getParameterInfo (int) const override + { + return { {}, {}, getNodeColor(), -1.0f, PortKind::parameter }; + } + + void resized() override + { + const auto scale = getLocalBounds().getWidth() / static_cast (getPreferredWidth()); + auto body = getLocalBounds().to().reduced (14.0f * scale, 0.0f); + + loadButton.setBounds (body.getX() + 8.0f * scale, + 43.0f * scale, + 62.0f * scale, + 20.0f * scale); + + waveform.setBounds (body.getX() + 8.0f * scale, + 67.0f * scale, + body.getWidth() - 16.0f * scale, + 52.0f * scale); + } + +private: + void chooseSampleFile() + { + fileChooser = yup::FileChooser::create ( + "Load Sample", + processor.getSampleFile().existsAsFile() + ? processor.getSampleFile().getParentDirectory() + : yup::File::getSpecialLocation (yup::File::userMusicDirectory), + "*.wav;*.aif;*.aiff;*.flac;*.mp3;*.ogg"); + + yup::WeakReference weakThis (this); + + fileChooser->browseForFileToOpen ([weakThis] (bool success, const yup::Array& results) + { + auto* self = weakThis.get(); + if (self == nullptr || ! success || results.isEmpty()) + return; + + self->loadSample (results[0]); + }); + } + + void loadSample (const yup::File& file) + { + const auto result = processor.loadSampleFile (file); + + if (result.wasOk()) + { + loadError.clear(); + loadButton.setButtonText ("Load"); + waveform.setSample (processor.getCurrentSample()); + repaint(); + return; + } + + loadError = result.getErrorMessage(); + loadButton.setButtonText ("Retry"); + repaint(); + } + + SamplePlayerProcessor& processor; + yup::TextButton loadButton; + SampleWaveformComponent waveform; + yup::FileChooser::Ptr fileChooser; + yup::String loadError; + + YUP_DECLARE_WEAK_REFERENCEABLE (SamplePlayerNodeView) +}; diff --git a/examples/audiograph/source/ui/PluginEditorWindow.h b/examples/audiograph/source/ui/PluginEditorWindow.h index 5e5795068..daec843f5 100644 --- a/examples/audiograph/source/ui/PluginEditorWindow.h +++ b/examples/audiograph/source/ui/PluginEditorWindow.h @@ -58,7 +58,7 @@ class PluginEditorWindow final : public yup::DocumentWindow { return yup::ComponentNative::Options() .withResizableWindow (editor != nullptr && editor->isResizable()) - .withFramerateRedraw (10) + .withFramerateRedraw (0) .withRenderContinuous (false); } diff --git a/modules/yup_audio_gui/waveform/yup_AudioThumbnail.cpp b/modules/yup_audio_gui/waveform/yup_AudioThumbnail.cpp index b69030b02..b71ef37da 100644 --- a/modules/yup_audio_gui/waveform/yup_AudioThumbnail.cpp +++ b/modules/yup_audio_gui/waveform/yup_AudioThumbnail.cpp @@ -189,7 +189,8 @@ void AudioThumbnail::paintChannel (Graphics& g, // Calculate how many pixels we'll actually draw const int pixelColumns = jmin (static_cast (pixelWidth), static_cast (lane.getWidth())); - if (pixelColumns <= 0) + const int pixelRows = jmin (static_cast (pixelWidth), static_cast (lane.getHeight())); + if (pixelColumns <= 0 || pixelRows <= 0) return; // Calculate zoom level and select best aggregation level diff --git a/modules/yup_graphics/graphics/yup_Graphics.cpp b/modules/yup_graphics/graphics/yup_Graphics.cpp index 5ca297936..9200d3b27 100644 --- a/modules/yup_graphics/graphics/yup_Graphics.cpp +++ b/modules/yup_graphics/graphics/yup_Graphics.cpp @@ -728,6 +728,9 @@ void Graphics::fillFittedText (const StyledText& text, const Rectangle& r void Graphics::fillFittedText (const String& text, const Font& font, const Rectangle& rect, Justification justification) { + if (text.isEmpty()) + return; + StyledText styledText; { auto modifier = styledText.startUpdate(); @@ -765,6 +768,9 @@ void Graphics::strokeFittedText (const StyledText& text, const Rectangle& void Graphics::strokeFittedText (const String& text, const Font& font, const Rectangle& rect, Justification justification) { + if (text.isEmpty()) + return; + StyledText styledText; { auto modifier = styledText.startUpdate(); diff --git a/modules/yup_gui/native/yup_FileChooser_mac.mm b/modules/yup_gui/native/yup_FileChooser_mac.mm index 0800c9227..bb318bb77 100644 --- a/modules/yup_gui/native/yup_FileChooser_mac.mm +++ b/modules/yup_gui/native/yup_FileChooser_mac.mm @@ -71,102 +71,99 @@ //============================================================================== void FileChooser::showPlatformDialog(CompletionCallback callback, int flags) { - YUP_AUTORELEASEPOOL + const bool isSave = (flags & saveMode) != 0; + const bool canChooseFiles = (flags & canSelectFiles) != 0; + const bool canChooseDirectories = (flags & canSelectDirectories) != 0; + const bool allowsMultipleSelection = (flags & canSelectMultipleItems) != 0; + const bool warnAboutOverwrite = (flags & warnAboutOverwriting) != 0; + + if (isSave) { - const bool isSave = (flags & saveMode) != 0; - const bool canChooseFiles = (flags & canSelectFiles) != 0; - const bool canChooseDirectories = (flags & canSelectDirectories) != 0; - const bool allowsMultipleSelection = (flags & canSelectMultipleItems) != 0; - const bool warnAboutOverwrite = (flags & warnAboutOverwriting) != 0; + NSSavePanel* panel = [NSSavePanel savePanel]; - if (isSave) - { - NSSavePanel* panel = [NSSavePanel savePanel]; + [panel setTitle:[NSString stringWithUTF8String:title.toUTF8()]]; + [panel setCanCreateDirectories:YES]; + [panel setShowsHiddenFiles:NO]; - [panel setTitle:[NSString stringWithUTF8String:title.toUTF8()]]; - [panel setCanCreateDirectories:YES]; - [panel setShowsHiddenFiles:NO]; + if (warnAboutOverwrite) + [panel setExtensionHidden:NO]; - if (warnAboutOverwrite) - [panel setExtensionHidden:NO]; + NSArray* allowedTypes = createAllowedFileTypes(filters); + if (allowedTypes != nil) + { + [panel setAllowedFileTypes:allowedTypes]; + [panel setAllowsOtherFileTypes:NO]; + } - NSArray* allowedTypes = createAllowedFileTypes(filters); - if (allowedTypes != nil) + if (startingFile.exists()) + { + if (startingFile.isDirectory()) { - [panel setAllowedFileTypes:allowedTypes]; - [panel setAllowsOtherFileTypes:NO]; + [panel setDirectoryURL:[NSURL fileURLWithPath:[NSString stringWithUTF8String:startingFile.getFullPathName().toUTF8()]]]; } - - if (startingFile.exists()) + else { - if (startingFile.isDirectory()) - { - [panel setDirectoryURL:[NSURL fileURLWithPath:[NSString stringWithUTF8String:startingFile.getFullPathName().toUTF8()]]]; - } - else - { - [panel setDirectoryURL:[NSURL fileURLWithPath:[NSString stringWithUTF8String:startingFile.getParentDirectory().getFullPathName().toUTF8()]]]; - [panel setNameFieldStringValue:[NSString stringWithUTF8String:startingFile.getFileName().toUTF8()]]; - } + [panel setDirectoryURL:[NSURL fileURLWithPath:[NSString stringWithUTF8String:startingFile.getParentDirectory().getFullPathName().toUTF8()]]]; + [panel setNameFieldStringValue:[NSString stringWithUTF8String:startingFile.getFileName().toUTF8()]]; } + } - [panel beginWithCompletionHandler:^(NSModalResponse result) { - Array results; + [panel beginWithCompletionHandler:^(NSModalResponse result) { + Array results; - if (result == NSModalResponseOK) + if (result == NSModalResponseOK) + { + auto* url = [panel URL]; + if (url != nil) { - auto* url = [panel URL]; - if (url != nil) - { - NSString* path = [url path]; - if (path != nil) - results.add(File(String::fromUTF8([path UTF8String]))); - } + NSString* path = [url path]; + if (path != nil) + results.add(File(String::fromUTF8([path UTF8String]))); } + } - MessageManager::callAsync([this, callback = std::move(callback), result, results] - { invokeCallback(std::move(callback), result == NSModalResponseOK, results); }); - }]; - } - else - { - NSOpenPanel* panel = [NSOpenPanel openPanel]; + MessageManager::callAsync([this, callback = std::move(callback), result, results] + { invokeCallback(std::move(callback), result == NSModalResponseOK, results); }); + }]; + } + else + { + NSOpenPanel* panel = [NSOpenPanel openPanel]; - [panel setTitle:[NSString stringWithUTF8String:title.toUTF8()]]; - [panel setCanChooseFiles:canChooseFiles]; - [panel setCanChooseDirectories:canChooseDirectories]; - [panel setAllowsMultipleSelection:allowsMultipleSelection]; - [panel setShowsHiddenFiles:NO]; - [panel setTreatsFilePackagesAsDirectories:packageDirsAsFiles]; + [panel setTitle:[NSString stringWithUTF8String:title.toUTF8()]]; + [panel setCanChooseFiles:canChooseFiles]; + [panel setCanChooseDirectories:canChooseDirectories]; + [panel setAllowsMultipleSelection:allowsMultipleSelection]; + [panel setShowsHiddenFiles:NO]; + [panel setTreatsFilePackagesAsDirectories:packageDirsAsFiles]; - NSArray* allowedTypes = createAllowedFileTypes(filters); - if (allowedTypes != nil && canChooseFiles) - { - [panel setAllowedFileTypes:allowedTypes]; - [panel setAllowsOtherFileTypes:NO]; - } + NSArray* allowedTypes = createAllowedFileTypes(filters); + if (allowedTypes != nil && canChooseFiles) + { + [panel setAllowedFileTypes:allowedTypes]; + [panel setAllowsOtherFileTypes:NO]; + } - if (startingFile.exists()) - [panel setDirectoryURL:[NSURL fileURLWithPath:[NSString stringWithUTF8String:startingFile.getFullPathName().toUTF8()]]]; + if (startingFile.exists()) + [panel setDirectoryURL:[NSURL fileURLWithPath:[NSString stringWithUTF8String:startingFile.getFullPathName().toUTF8()]]]; - [panel beginWithCompletionHandler:^(NSModalResponse result) { - Array results; + [panel beginWithCompletionHandler:^(NSModalResponse result) { + Array results; - auto* urls = [panel URLs]; - if (urls != nil) + auto* urls = [panel URLs]; + if (urls != nil) + { + for (NSURL* url in urls) { - for (NSURL* url in urls) - { - NSString* path = [url path]; - if (path != nil) - results.add(File(String::fromUTF8([path UTF8String]))); - } + NSString* path = [url path]; + if (path != nil) + results.add(File(String::fromUTF8([path UTF8String]))); } + } - MessageManager::callAsync([this, callback = std::move(callback), result, results] - { invokeCallback(std::move(callback), result == NSModalResponseOK, results); }); - }]; - } + MessageManager::callAsync([this, callback = std::move(callback), result, results] + { invokeCallback(std::move(callback), result == NSModalResponseOK, results); }); + }]; } } diff --git a/modules/yup_gui/yup_gui.h b/modules/yup_gui/yup_gui.h index 130177e09..9a2de2d1b 100644 --- a/modules/yup_gui/yup_gui.h +++ b/modules/yup_gui/yup_gui.h @@ -66,7 +66,7 @@ Enable logging of windowing events like movement, resizes, mouse interactions. */ #ifndef YUP_ENABLE_WINDOWING_EVENT_LOGGING -#define YUP_ENABLE_WINDOWING_EVENT_LOGGING 0 +#define YUP_ENABLE_WINDOWING_EVENT_LOGGING 1 #endif //============================================================================== From ce9125e41bcbf90c81fd7c9183f95e49f259d387 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 18 May 2026 22:55:10 +0200 Subject: [PATCH 26/35] Disable denormals tests in wasm --- tests/yup_audio_graph/yup_AudioGraphProcessor.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/yup_audio_graph/yup_AudioGraphProcessor.cpp b/tests/yup_audio_graph/yup_AudioGraphProcessor.cpp index 90a30c7f4..1276cdfb2 100644 --- a/tests/yup_audio_graph/yup_AudioGraphProcessor.cpp +++ b/tests/yup_audio_graph/yup_AudioGraphProcessor.cpp @@ -2408,6 +2408,7 @@ TEST (AudioGraphProcessorTests, RapidWorkerThreadResizeDoesNotCrash) } } +#if ! defined(YUP_WASM) TEST (AudioGraphProcessorTests, ProcessBlockDisablesDenormals) { AudioGraphProcessor graph; @@ -2428,6 +2429,7 @@ TEST (AudioGraphProcessorTests, ProcessBlockDisablesDenormals) EXPECT_TRUE (procPtr->denormalsWereDisabled); } +#endif TEST (AudioGraphProcessorTests, ManyMidiEventsPassThroughGraphCorrectly) { From 3f57f8fd41686cc9828729bdf7788c0088a8b31c Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 18 May 2026 23:10:19 +0200 Subject: [PATCH 27/35] Fix windows --- .../yup_AudioDataConverters.cpp | 61 +++++++++++-------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/tests/yup_audio_basics/yup_AudioDataConverters.cpp b/tests/yup_audio_basics/yup_AudioDataConverters.cpp index 52c909519..25e196f0a 100644 --- a/tests/yup_audio_basics/yup_AudioDataConverters.cpp +++ b/tests/yup_audio_basics/yup_AudioDataConverters.cpp @@ -44,10 +44,11 @@ using namespace yup; namespace { + template -void testRoundTripConversion (Random& r) +struct RoundTripConversionTest { - auto testWithInPlace = [&] (bool inPlace) + static void run (Random& r, bool inPlace) { const int numSamples = 2048; int32 original[numSamples], converted[numSamples], reversed[numSamples]; @@ -71,14 +72,14 @@ void testRoundTripConversion (Random& r) EXPECT_FALSE (clippingFailed); } - // convert data from the source to dest format.. - std::unique_ptr conv (new AudioData::ConverterInstance, - AudioData::Pointer>()); + std::unique_ptr conv (new AudioData::ConverterInstance< + AudioData::Pointer, + AudioData::Pointer>()); conv->convertSamples (inPlace ? reversed : converted, original, numSamples); - // ..and back again.. - conv.reset (new AudioData::ConverterInstance, - AudioData::Pointer>()); + conv.reset (new AudioData::ConverterInstance< + AudioData::Pointer, + AudioData::Pointer>()); if (! inPlace) zeromem (reversed, sizeof (reversed)); @@ -101,36 +102,42 @@ void testRoundTripConversion (Random& r) EXPECT_LE (biggestDiff, errorMargin); } - }; - - testWithInPlace (false); - testWithInPlace (true); -} + } +}; template -void testAllEndianness (Random& r) +struct AllEndiannessTest { - testRoundTripConversion (r); - testRoundTripConversion (r); -} + static void run (Random& r) + { + RoundTripConversionTest::run (r, false); + RoundTripConversionTest::run (r, true); + RoundTripConversionTest::run (r, false); + RoundTripConversionTest::run (r, true); + } +}; template -void testAllFormats (Random& r) +struct AllFormatsTest { - testAllEndianness (r); - testAllEndianness (r); - testAllEndianness (r); - testAllEndianness (r); - testAllEndianness (r); - testAllEndianness (r); -} + static void run (Random& r) + { + AllEndiannessTest::run (r); + AllEndiannessTest::run (r); + AllEndiannessTest::run (r); + AllEndiannessTest::run (r); + AllEndiannessTest::run (r); + AllEndiannessTest::run (r); + } +}; template void testFormatWithAllEndianness (Random& r) { - testAllFormats (r); - testAllFormats (r); + AllFormatsTest::run (r); + AllFormatsTest::run (r); } + } // namespace //============================================================================== From 49a73ddf419e1eae770d9aa42ab72ae0cc680975 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 18 May 2026 23:36:33 +0200 Subject: [PATCH 28/35] More theme work --- .../themes/theme_v1/yup_ThemeVersion1.cpp | 14 +- .../yup_gui/themes/yup_ApplicationTheme.cpp | 33 +++- modules/yup_gui/themes/yup_ApplicationTheme.h | 35 +++- tests/yup_gui.cpp | 1 + tests/yup_gui/yup_ApplicationTheme.cpp | 167 ++++++++++++++++++ 5 files changed, 234 insertions(+), 16 deletions(-) create mode 100644 tests/yup_gui/yup_ApplicationTheme.cpp diff --git a/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp b/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp index fb7ab6b6c..d319d1503 100644 --- a/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp +++ b/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp @@ -1216,7 +1216,7 @@ void paintMidiKeyboard (Graphics& g, const ApplicationTheme& theme, const MidiKe // Draw keyboard background with subtle gradient shadow auto keyboardWidth = keyboard.getKeyStartRange().getEnd(); - auto shadowColor = ApplicationTheme::findColor (MidiKeyboardComponent::Style::whiteKeyShadowColorId); + auto shadowColor = ApplicationTheme::findColor (MidiKeyboardComponent::Style::whiteKeyShadowColorId).value_or (Color()); if (! shadowColor.isTransparent()) { @@ -1230,7 +1230,7 @@ void paintMidiKeyboard (Graphics& g, const ApplicationTheme& theme, const MidiKe } // Draw separator line at bottom - auto lineColor = ApplicationTheme::findColor (MidiKeyboardComponent::Style::keyOutlineColorId); + auto lineColor = ApplicationTheme::findColor (MidiKeyboardComponent::Style::keyOutlineColorId).value_or (Color()); if (! lineColor.isTransparent()) { g.setFillColor (lineColor); @@ -1251,9 +1251,9 @@ void paintMidiKeyboard (Graphics& g, const ApplicationTheme& theme, const MidiKe auto isOver = keyboard.isMouseOverNote (note); // Base colors from theme - auto whiteKeyColor = ApplicationTheme::findColor (MidiKeyboardComponent::Style::whiteKeyColorId); - auto pressedColor = ApplicationTheme::findColor (MidiKeyboardComponent::Style::whiteKeyPressedColorId); - auto outlineColor = ApplicationTheme::findColor (MidiKeyboardComponent::Style::keyOutlineColorId); + auto whiteKeyColor = ApplicationTheme::findColor (MidiKeyboardComponent::Style::whiteKeyColorId).value_or (Color()); + auto pressedColor = ApplicationTheme::findColor (MidiKeyboardComponent::Style::whiteKeyPressedColorId).value_or (Color()); + auto outlineColor = ApplicationTheme::findColor (MidiKeyboardComponent::Style::keyOutlineColorId).value_or (Color()); // Determine fill color based on state Color fillColor = whiteKeyColor; @@ -1344,8 +1344,8 @@ void paintMidiKeyboard (Graphics& g, const ApplicationTheme& theme, const MidiKe auto isOver = keyboard.isMouseOverNote (note); // Base colors from theme - auto blackKeyColor = ApplicationTheme::findColor (MidiKeyboardComponent::Style::blackKeyColorId); - auto blackPressedColor = ApplicationTheme::findColor (MidiKeyboardComponent::Style::blackKeyPressedColorId); + auto blackKeyColor = ApplicationTheme::findColor (MidiKeyboardComponent::Style::blackKeyColorId).value_or (Color()); + auto blackPressedColor = ApplicationTheme::findColor (MidiKeyboardComponent::Style::blackKeyPressedColorId).value_or (Color()); // Determine fill color based on state Color fillColor = blackKeyColor; diff --git a/modules/yup_gui/themes/yup_ApplicationTheme.cpp b/modules/yup_gui/themes/yup_ApplicationTheme.cpp index 8a3c6d4fa..27519dd5a 100644 --- a/modules/yup_gui/themes/yup_ApplicationTheme.cpp +++ b/modules/yup_gui/themes/yup_ApplicationTheme.cpp @@ -35,7 +35,7 @@ void ApplicationTheme::setGlobalTheme (ApplicationTheme::Ptr s) getGlobalThemeInstance() = std::move (s); } -ApplicationTheme::ConstPtr ApplicationTheme::getGlobalTheme() +ApplicationTheme::Ptr ApplicationTheme::getGlobalTheme() { return getGlobalThemeInstance(); } @@ -48,10 +48,16 @@ ApplicationTheme::Ptr& ApplicationTheme::getGlobalThemeInstance() //============================================================================== -Color ApplicationTheme::findColor (const Identifier& colorId) +std::optional ApplicationTheme::findColor (const Identifier& colorId) { - auto it = getGlobalThemeInstance()->defaultColors.find (colorId); - return it != getGlobalThemeInstance()->defaultColors.end() ? it->second : Color(); + jassert (getGlobalThemeInstance() != nullptr); + + const auto& colors = getGlobalThemeInstance()->defaultColors; + + if (auto it = colors.find (colorId); it != colors.end()) + return it->second; + + return std::nullopt; } void ApplicationTheme::setColor (const Identifier& colorId, const Color& color) @@ -67,6 +73,25 @@ void ApplicationTheme::setColors (std::initializer_list ApplicationTheme::findMetric (const Identifier& metricId) +{ + jassert (getGlobalThemeInstance() != nullptr); + + const auto& metrics = getGlobalThemeInstance()->defaultMetrics; + + if (auto it = metrics.find (metricId); it != metrics.end()) + return it->second; + + return std::nullopt; +} + +void ApplicationTheme::setMetric (const Identifier& metricId, float value) +{ + defaultMetrics.insert_or_assign (metricId, value); +} + +//============================================================================== + void ApplicationTheme::setDefaultFont (Font font) { defaultFont = std::move (font); diff --git a/modules/yup_gui/themes/yup_ApplicationTheme.h b/modules/yup_gui/themes/yup_ApplicationTheme.h index a251b9c42..75605c6c7 100644 --- a/modules/yup_gui/themes/yup_ApplicationTheme.h +++ b/modules/yup_gui/themes/yup_ApplicationTheme.h @@ -65,7 +65,7 @@ class YUP_API ApplicationTheme final : public ReferenceCountedObject @returns A reference-counted pointer to the global ApplicationTheme. */ - static ApplicationTheme::ConstPtr getGlobalTheme(); + static ApplicationTheme::Ptr getGlobalTheme(); //============================================================================== /** @@ -119,15 +119,39 @@ class YUP_API ApplicationTheme final : public ReferenceCountedObject } //============================================================================== - /** */ - static Color findColor (const Identifier& colorId); + /** Returns a color from the global theme. + + @param colorId The identifier for the color to retrieve. + */ + static std::optional findColor (const Identifier& colorId); + + /** Sets a color in the global theme. - /** */ + @param colorId The identifier for the color to set. + @param color The color to set. + */ void setColor (const Identifier& colorId, const Color& color); - /** */ + /** Sets multiple colors in the global theme. + + @param colors An initializer list of color identifier and color pairs. + */ void setColors (std::initializer_list> colors); + //============================================================================== + /** Returns a named float metric from the global theme. + + Returns @p defaultValue when the metric has not been registered. + */ + static std::optional findMetric (const Identifier& metricId); + + /** Registers a named float metric in this theme. + + @param metricId The identifier for the metric to set. + @param value The value to set for the metric. + */ + void setMetric (const Identifier& metricId, float value); + //============================================================================== /** Sets the default text font for the application theme. @@ -164,6 +188,7 @@ class YUP_API ApplicationTheme final : public ReferenceCountedObject //============================================================================== std::unordered_map componentStyles; std::unordered_map defaultColors; + std::unordered_map defaultMetrics; Font defaultFont; Font defaultIconFont; diff --git a/tests/yup_gui.cpp b/tests/yup_gui.cpp index 31eb68a04..5467ea7a0 100644 --- a/tests/yup_gui.cpp +++ b/tests/yup_gui.cpp @@ -19,6 +19,7 @@ ============================================================================== */ +#include "yup_gui/yup_ApplicationTheme.cpp" #include "yup_gui/yup_ComboBox.cpp" #include "yup_gui/yup_Component.cpp" #include "yup_gui/yup_Desktop.cpp" diff --git a/tests/yup_gui/yup_ApplicationTheme.cpp b/tests/yup_gui/yup_ApplicationTheme.cpp new file mode 100644 index 000000000..7f4a57805 --- /dev/null +++ b/tests/yup_gui/yup_ApplicationTheme.cpp @@ -0,0 +1,167 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +// ============================================================================= + +class ApplicationThemeTest : public ::testing::Test +{ +protected: + void SetUp() override + { + oldTheme = ApplicationTheme::getGlobalTheme(); + + theme = new ApplicationTheme(); + ApplicationTheme::setGlobalTheme (theme); + } + + void TearDown() override + { + ApplicationTheme::setGlobalTheme (oldTheme.get()); + theme = nullptr; + oldTheme = nullptr; + } + + ApplicationTheme::Ptr theme; + ApplicationTheme::Ptr oldTheme; +}; + +// ============================================================================= + +TEST_F (ApplicationThemeTest, FindColorReturnsNulloptWhenColorNotRegistered) +{ + auto result = ApplicationTheme::findColor (Identifier ("unknownColor")); + EXPECT_FALSE (result.has_value()); +} + +TEST_F (ApplicationThemeTest, FindColorReturnsRegisteredColor) +{ + const Color expected = Color::fromRGBA (255, 128, 0, 255); + theme->setColor (Identifier ("testColor"), expected); + + auto result = ApplicationTheme::findColor (Identifier ("testColor")); + ASSERT_TRUE (result.has_value()); + EXPECT_EQ (result.value(), expected); +} + +TEST_F (ApplicationThemeTest, SetColorsRegistersMultipleColors) +{ + const Identifier idA ("colorA"); + const Identifier idB ("colorB"); + const Color colorA = Color::fromRGBA (10, 20, 30, 255); + const Color colorB = Color::fromRGBA (40, 50, 60, 128); + + theme->setColors ({ { idA, colorA }, { idB, colorB } }); + + auto resultA = ApplicationTheme::findColor (idA); + auto resultB = ApplicationTheme::findColor (idB); + + ASSERT_TRUE (resultA.has_value()); + EXPECT_EQ (resultA.value(), colorA); + ASSERT_TRUE (resultB.has_value()); + EXPECT_EQ (resultB.value(), colorB); +} + +TEST_F (ApplicationThemeTest, SetColorOverwritesExistingColor) +{ + const Identifier id ("myColor"); + const Color first = Color::fromRGBA (1, 2, 3, 255); + const Color second = Color::fromRGBA (4, 5, 6, 255); + + theme->setColor (id, first); + theme->setColor (id, second); + + auto result = ApplicationTheme::findColor (id); + ASSERT_TRUE (result.has_value()); + EXPECT_EQ (result.value(), second); +} + +TEST_F (ApplicationThemeTest, FindColorUnregisteredIdDoesNotAffectOtherIds) +{ + const Identifier registered ("registered"); + const Identifier unregistered ("unregistered"); + const Color color = Color::fromRGBA (0, 0, 255, 255); + + theme->setColor (registered, color); + + EXPECT_TRUE (ApplicationTheme::findColor (registered).has_value()); + EXPECT_FALSE (ApplicationTheme::findColor (unregistered).has_value()); +} + +// ============================================================================= + +TEST_F (ApplicationThemeTest, FindMetricReturnsNulloptWhenMetricNotRegistered) +{ + auto result = ApplicationTheme::findMetric (Identifier ("unknownMetric")); + EXPECT_FALSE (result.has_value()); +} + +TEST_F (ApplicationThemeTest, FindMetricReturnsRegisteredValue) +{ + theme->setMetric (Identifier ("cornerRadius"), 8.0f); + + auto result = ApplicationTheme::findMetric (Identifier ("cornerRadius")); + ASSERT_TRUE (result.has_value()); + EXPECT_FLOAT_EQ (result.value(), 8.0f); +} + +TEST_F (ApplicationThemeTest, SetMetricOverwritesExistingValue) +{ + const Identifier id ("borderWidth"); + + theme->setMetric (id, 1.0f); + theme->setMetric (id, 3.5f); + + auto result = ApplicationTheme::findMetric (id); + ASSERT_TRUE (result.has_value()); + EXPECT_FLOAT_EQ (result.value(), 3.5f); +} + +TEST_F (ApplicationThemeTest, FindMetricUnregisteredIdDoesNotAffectOtherIds) +{ + const Identifier registered ("spacing"); + const Identifier unregistered ("padding"); + + theme->setMetric (registered, 4.0f); + + EXPECT_TRUE (ApplicationTheme::findMetric (registered).has_value()); + EXPECT_FALSE (ApplicationTheme::findMetric (unregistered).has_value()); +} + +TEST_F (ApplicationThemeTest, SetMetricAcceptsZeroAndNegativeValues) +{ + theme->setMetric (Identifier ("zeroMetric"), 0.0f); + theme->setMetric (Identifier ("negativeMetric"), -2.5f); + + auto zero = ApplicationTheme::findMetric (Identifier ("zeroMetric")); + auto negative = ApplicationTheme::findMetric (Identifier ("negativeMetric")); + + ASSERT_TRUE (zero.has_value()); + EXPECT_FLOAT_EQ (zero.value(), 0.0f); + + ASSERT_TRUE (negative.has_value()); + EXPECT_FLOAT_EQ (negative.value(), -2.5f); +} From 5dafcf770a6962d23ceceb1d523b8618d657b0c8 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 18 May 2026 23:42:17 +0200 Subject: [PATCH 29/35] Missing inclusion of --- modules/yup_core/system/yup_StandardHeader.h | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/yup_core/system/yup_StandardHeader.h b/modules/yup_core/system/yup_StandardHeader.h index 8d5929896..3bd1b9ccb 100644 --- a/modules/yup_core/system/yup_StandardHeader.h +++ b/modules/yup_core/system/yup_StandardHeader.h @@ -68,6 +68,7 @@ #include #include #include +#include #include #include #include From e49b585753f48c2004a9cee783cae1872f0cfd61 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Tue, 19 May 2026 00:10:46 +0200 Subject: [PATCH 30/35] Fix NDK issues --- modules/yup_core/misc/yup_ResultValue.h | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/modules/yup_core/misc/yup_ResultValue.h b/modules/yup_core/misc/yup_ResultValue.h index 70a04a543..da1968e79 100644 --- a/modules/yup_core/misc/yup_ResultValue.h +++ b/modules/yup_core/misc/yup_ResultValue.h @@ -31,8 +31,7 @@ namespace yup template struct YUP_API OkValue { - template - requires std::constructible_from + template , int> = 0> explicit constexpr OkValue (U&& v) noexcept (std::is_nothrow_constructible_v) : value (std::forward (v)) { @@ -184,7 +183,6 @@ class YUP_API ResultValue /** Returns a copy of the value that was set when this result was created. */ T getValue() const& - requires std::copy_constructible { jassert (valueOrErrorMessage.index() == 1); // Trying to access the value of the result, when the result is holding an error instead! @@ -193,7 +191,6 @@ class YUP_API ResultValue /** Returns a moved from value that was set when this result was created. */ T getValue() && - requires std::move_constructible { jassert (valueOrErrorMessage.index() == 1); // Trying to access the value of the result, when the result is holding an error instead! @@ -233,8 +230,7 @@ class YUP_API ResultValue ResultValue& operator= (ResultValue&&) noexcept = default; /** Constructs a successful result from an OkValue, enabling type-deduced return syntax. */ - template - requires std::constructible_from + template , int> = 0> ResultValue (OkValue&& okVal) noexcept (std::is_nothrow_constructible_v) : valueOrErrorMessage (std::in_place_index<1>, std::move (okVal.value)) { From 8d42b88bda909aebb7bba39faf2234db23836dd4 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Tue, 19 May 2026 00:16:08 +0200 Subject: [PATCH 31/35] Fix windows --- .../yup_AudioDataConverters.cpp | 52 +++++++++++-------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/tests/yup_audio_basics/yup_AudioDataConverters.cpp b/tests/yup_audio_basics/yup_AudioDataConverters.cpp index 25e196f0a..125ce60dc 100644 --- a/tests/yup_audio_basics/yup_AudioDataConverters.cpp +++ b/tests/yup_audio_basics/yup_AudioDataConverters.cpp @@ -48,13 +48,20 @@ namespace template struct RoundTripConversionTest { + using WritablePtr = AudioData::Pointer; + using ConstPtr = AudioData::Pointer; + using F2WritablePtr = AudioData::Pointer; + using F2ConstPtr = AudioData::Pointer; + using FwdConv = AudioData::ConverterInstance; + using RevConv = AudioData::ConverterInstance; + static void run (Random& r, bool inPlace) { const int numSamples = 2048; int32 original[numSamples], converted[numSamples], reversed[numSamples]; { - AudioData::Pointer d (original); + WritablePtr d (original); bool clippingFailed = false; for (int i = 0; i < numSamples / 2; ++i) @@ -72,14 +79,10 @@ struct RoundTripConversionTest EXPECT_FALSE (clippingFailed); } - std::unique_ptr conv (new AudioData::ConverterInstance< - AudioData::Pointer, - AudioData::Pointer>()); + std::unique_ptr conv (new FwdConv()); conv->convertSamples (inPlace ? reversed : converted, original, numSamples); - conv.reset (new AudioData::ConverterInstance< - AudioData::Pointer, - AudioData::Pointer>()); + conv.reset (new RevConv()); if (! inPlace) zeromem (reversed, sizeof (reversed)); @@ -87,11 +90,11 @@ struct RoundTripConversionTest { int biggestDiff = 0; - AudioData::Pointer d1 (original); - AudioData::Pointer d2 (reversed); + ConstPtr d1 (original); + ConstPtr d2 (reversed); - const int errorMargin = 2 * AudioData::Pointer::get32BitResolution() - + AudioData::Pointer::get32BitResolution(); + const int errorMargin = 2 * ConstPtr::get32BitResolution() + + F2ConstPtr::get32BitResolution(); for (int i = 0; i < numSamples; ++i) { @@ -108,12 +111,15 @@ struct RoundTripConversionTest template struct AllEndiannessTest { + using BigTest = RoundTripConversionTest; + using LittleTest = RoundTripConversionTest; + static void run (Random& r) { - RoundTripConversionTest::run (r, false); - RoundTripConversionTest::run (r, true); - RoundTripConversionTest::run (r, false); - RoundTripConversionTest::run (r, true); + BigTest::run (r, false); + BigTest::run (r, true); + LittleTest::run (r, false); + LittleTest::run (r, true); } }; @@ -187,7 +193,7 @@ TEST (AudioDataConvertersTest, Float64Conversions) TEST (AudioDataConvertersTest, PointerAdvance) { float data[10] = { 0.0f, 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f }; - AudioData::Pointer ptr (data); + auto ptr = AudioData::Pointer (data); EXPECT_FLOAT_EQ (ptr.getAsFloat(), 0.0f); ++ptr; @@ -199,7 +205,7 @@ TEST (AudioDataConvertersTest, PointerAdvance) TEST (AudioDataConvertersTest, PointerDecrement) { float data[10] = { 0.0f, 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f }; - AudioData::Pointer ptr (data + 5); + auto ptr = AudioData::Pointer (data + 5); EXPECT_FLOAT_EQ (ptr.getAsFloat(), 0.5f); --ptr; @@ -209,7 +215,7 @@ TEST (AudioDataConvertersTest, PointerDecrement) TEST (AudioDataConvertersTest, PointerJump) { float data[10] = { 0.0f, 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f }; - AudioData::Pointer ptr (data); + auto ptr = AudioData::Pointer (data); ptr += 5; EXPECT_FLOAT_EQ (ptr.getAsFloat(), 0.5f); @@ -221,7 +227,7 @@ TEST (AudioDataConvertersTest, PointerJump) TEST (AudioDataConvertersTest, InterleavedPointer) { float data[8] = { 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f }; - AudioData::Pointer ptr (data, 2); + auto ptr = AudioData::Pointer (data, 2); EXPECT_FLOAT_EQ (ptr.getAsFloat(), 0.1f); ++ptr; @@ -233,7 +239,7 @@ TEST (AudioDataConvertersTest, InterleavedPointer) TEST (AudioDataConvertersTest, ClearSamples) { float data[10] = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f }; - AudioData::Pointer ptr (data); + auto ptr = AudioData::Pointer (data); ptr.clearSamples (5); @@ -246,7 +252,7 @@ TEST (AudioDataConvertersTest, ClearSamples) TEST (AudioDataConvertersTest, FindMinAndMax) { float data[10] = { 0.1f, -0.5f, 0.8f, -0.2f, 0.4f, 0.9f, -0.7f, 0.3f, -0.1f, 0.6f }; - AudioData::Pointer ptr (data); + auto ptr = AudioData::Pointer (data); auto range = ptr.findMinAndMax (10); @@ -257,7 +263,7 @@ TEST (AudioDataConvertersTest, FindMinAndMax) TEST (AudioDataConvertersTest, FindMinAndMaxEmpty) { float data[1] = { 0.0f }; - AudioData::Pointer ptr (data); + auto ptr = AudioData::Pointer (data); auto range = ptr.findMinAndMax (0); @@ -270,7 +276,7 @@ TEST (AudioDataConvertersTest, FindMinAndMaxInteger) for (int i = 0; i < 10; ++i) data[i] = static_cast ((i - 5) * 1000); - AudioData::Pointer ptr (data); + auto ptr = AudioData::Pointer (data); float minVal, maxVal; ptr.findMinAndMax (10, minVal, maxVal); From 2bcaef9b8d65a1044c1acd11745ef833db9bbd60 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Tue, 19 May 2026 08:02:03 +0200 Subject: [PATCH 32/35] More windows fixes --- .../yup_AudioDataConverters.cpp | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/tests/yup_audio_basics/yup_AudioDataConverters.cpp b/tests/yup_audio_basics/yup_AudioDataConverters.cpp index 125ce60dc..4b6e401d1 100644 --- a/tests/yup_audio_basics/yup_AudioDataConverters.cpp +++ b/tests/yup_audio_basics/yup_AudioDataConverters.cpp @@ -45,13 +45,23 @@ using namespace yup; namespace { +// MSVC strict-mode C++20 two-phase lookup fails to parse 4-argument template +// instantiations with dependent types inside template struct bodies (C2059). +// These 2-arg namespace-scope aliases sidestep that by reducing each problematic +// 4-arg Pointer to a 2-arg wrapper. +template +using ADNonInterleavedWritable = AudioData::Pointer; + +template +using ADNonInterleavedConst = AudioData::Pointer; + template struct RoundTripConversionTest { - using WritablePtr = AudioData::Pointer; - using ConstPtr = AudioData::Pointer; - using F2WritablePtr = AudioData::Pointer; - using F2ConstPtr = AudioData::Pointer; + using WritablePtr = ADNonInterleavedWritable; + using ConstPtr = ADNonInterleavedConst; + using F2WritablePtr = ADNonInterleavedWritable; + using F2ConstPtr = ADNonInterleavedConst; using FwdConv = AudioData::ConverterInstance; using RevConv = AudioData::ConverterInstance; @@ -108,11 +118,18 @@ struct RoundTripConversionTest } }; +// 3-arg wrappers that fix the same MSVC 4-arg issue in AllEndiannessTest. +template +using RoundTripBigEndian = RoundTripConversionTest; + +template +using RoundTripLittleEndian = RoundTripConversionTest; + template struct AllEndiannessTest { - using BigTest = RoundTripConversionTest; - using LittleTest = RoundTripConversionTest; + using BigTest = RoundTripBigEndian; + using LittleTest = RoundTripLittleEndian; static void run (Random& r) { From a0b061662b709b0fc04dece0d293b86015bde3c0 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Tue, 19 May 2026 09:02:19 +0200 Subject: [PATCH 33/35] Typo error --- modules/yup_core/system/yup_TargetPlatform.h | 2 +- .../yup_AudioDataConverters.cpp | 108 +++++++----------- 2 files changed, 40 insertions(+), 70 deletions(-) diff --git a/modules/yup_core/system/yup_TargetPlatform.h b/modules/yup_core/system/yup_TargetPlatform.h index 3dcc687b0..7c8043d27 100644 --- a/modules/yup_core/system/yup_TargetPlatform.h +++ b/modules/yup_core/system/yup_TargetPlatform.h @@ -70,7 +70,7 @@ //============================================================================== #if defined(_WIN32) || defined(_WIN64) #define YUP_WINDOWS 1 -#define F1 +#define YUP_DESKTOP 1 #elif defined(ANDROID) || defined(__ANDROID__) #define YUP_ANDROID 1 diff --git a/tests/yup_audio_basics/yup_AudioDataConverters.cpp b/tests/yup_audio_basics/yup_AudioDataConverters.cpp index 4b6e401d1..52c909519 100644 --- a/tests/yup_audio_basics/yup_AudioDataConverters.cpp +++ b/tests/yup_audio_basics/yup_AudioDataConverters.cpp @@ -44,34 +44,16 @@ using namespace yup; namespace { - -// MSVC strict-mode C++20 two-phase lookup fails to parse 4-argument template -// instantiations with dependent types inside template struct bodies (C2059). -// These 2-arg namespace-scope aliases sidestep that by reducing each problematic -// 4-arg Pointer to a 2-arg wrapper. -template -using ADNonInterleavedWritable = AudioData::Pointer; - -template -using ADNonInterleavedConst = AudioData::Pointer; - template -struct RoundTripConversionTest +void testRoundTripConversion (Random& r) { - using WritablePtr = ADNonInterleavedWritable; - using ConstPtr = ADNonInterleavedConst; - using F2WritablePtr = ADNonInterleavedWritable; - using F2ConstPtr = ADNonInterleavedConst; - using FwdConv = AudioData::ConverterInstance; - using RevConv = AudioData::ConverterInstance; - - static void run (Random& r, bool inPlace) + auto testWithInPlace = [&] (bool inPlace) { const int numSamples = 2048; int32 original[numSamples], converted[numSamples], reversed[numSamples]; { - WritablePtr d (original); + AudioData::Pointer d (original); bool clippingFailed = false; for (int i = 0; i < numSamples / 2; ++i) @@ -89,10 +71,14 @@ struct RoundTripConversionTest EXPECT_FALSE (clippingFailed); } - std::unique_ptr conv (new FwdConv()); + // convert data from the source to dest format.. + std::unique_ptr conv (new AudioData::ConverterInstance, + AudioData::Pointer>()); conv->convertSamples (inPlace ? reversed : converted, original, numSamples); - conv.reset (new RevConv()); + // ..and back again.. + conv.reset (new AudioData::ConverterInstance, + AudioData::Pointer>()); if (! inPlace) zeromem (reversed, sizeof (reversed)); @@ -100,11 +86,11 @@ struct RoundTripConversionTest { int biggestDiff = 0; - ConstPtr d1 (original); - ConstPtr d2 (reversed); + AudioData::Pointer d1 (original); + AudioData::Pointer d2 (reversed); - const int errorMargin = 2 * ConstPtr::get32BitResolution() - + F2ConstPtr::get32BitResolution(); + const int errorMargin = 2 * AudioData::Pointer::get32BitResolution() + + AudioData::Pointer::get32BitResolution(); for (int i = 0; i < numSamples; ++i) { @@ -115,52 +101,36 @@ struct RoundTripConversionTest EXPECT_LE (biggestDiff, errorMargin); } - } -}; + }; -// 3-arg wrappers that fix the same MSVC 4-arg issue in AllEndiannessTest. -template -using RoundTripBigEndian = RoundTripConversionTest; - -template -using RoundTripLittleEndian = RoundTripConversionTest; + testWithInPlace (false); + testWithInPlace (true); +} template -struct AllEndiannessTest +void testAllEndianness (Random& r) { - using BigTest = RoundTripBigEndian; - using LittleTest = RoundTripLittleEndian; - - static void run (Random& r) - { - BigTest::run (r, false); - BigTest::run (r, true); - LittleTest::run (r, false); - LittleTest::run (r, true); - } -}; + testRoundTripConversion (r); + testRoundTripConversion (r); +} template -struct AllFormatsTest +void testAllFormats (Random& r) { - static void run (Random& r) - { - AllEndiannessTest::run (r); - AllEndiannessTest::run (r); - AllEndiannessTest::run (r); - AllEndiannessTest::run (r); - AllEndiannessTest::run (r); - AllEndiannessTest::run (r); - } -}; + testAllEndianness (r); + testAllEndianness (r); + testAllEndianness (r); + testAllEndianness (r); + testAllEndianness (r); + testAllEndianness (r); +} template void testFormatWithAllEndianness (Random& r) { - AllFormatsTest::run (r); - AllFormatsTest::run (r); + testAllFormats (r); + testAllFormats (r); } - } // namespace //============================================================================== @@ -210,7 +180,7 @@ TEST (AudioDataConvertersTest, Float64Conversions) TEST (AudioDataConvertersTest, PointerAdvance) { float data[10] = { 0.0f, 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f }; - auto ptr = AudioData::Pointer (data); + AudioData::Pointer ptr (data); EXPECT_FLOAT_EQ (ptr.getAsFloat(), 0.0f); ++ptr; @@ -222,7 +192,7 @@ TEST (AudioDataConvertersTest, PointerAdvance) TEST (AudioDataConvertersTest, PointerDecrement) { float data[10] = { 0.0f, 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f }; - auto ptr = AudioData::Pointer (data + 5); + AudioData::Pointer ptr (data + 5); EXPECT_FLOAT_EQ (ptr.getAsFloat(), 0.5f); --ptr; @@ -232,7 +202,7 @@ TEST (AudioDataConvertersTest, PointerDecrement) TEST (AudioDataConvertersTest, PointerJump) { float data[10] = { 0.0f, 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f }; - auto ptr = AudioData::Pointer (data); + AudioData::Pointer ptr (data); ptr += 5; EXPECT_FLOAT_EQ (ptr.getAsFloat(), 0.5f); @@ -244,7 +214,7 @@ TEST (AudioDataConvertersTest, PointerJump) TEST (AudioDataConvertersTest, InterleavedPointer) { float data[8] = { 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f }; - auto ptr = AudioData::Pointer (data, 2); + AudioData::Pointer ptr (data, 2); EXPECT_FLOAT_EQ (ptr.getAsFloat(), 0.1f); ++ptr; @@ -256,7 +226,7 @@ TEST (AudioDataConvertersTest, InterleavedPointer) TEST (AudioDataConvertersTest, ClearSamples) { float data[10] = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f }; - auto ptr = AudioData::Pointer (data); + AudioData::Pointer ptr (data); ptr.clearSamples (5); @@ -269,7 +239,7 @@ TEST (AudioDataConvertersTest, ClearSamples) TEST (AudioDataConvertersTest, FindMinAndMax) { float data[10] = { 0.1f, -0.5f, 0.8f, -0.2f, 0.4f, 0.9f, -0.7f, 0.3f, -0.1f, 0.6f }; - auto ptr = AudioData::Pointer (data); + AudioData::Pointer ptr (data); auto range = ptr.findMinAndMax (10); @@ -280,7 +250,7 @@ TEST (AudioDataConvertersTest, FindMinAndMax) TEST (AudioDataConvertersTest, FindMinAndMaxEmpty) { float data[1] = { 0.0f }; - auto ptr = AudioData::Pointer (data); + AudioData::Pointer ptr (data); auto range = ptr.findMinAndMax (0); @@ -293,7 +263,7 @@ TEST (AudioDataConvertersTest, FindMinAndMaxInteger) for (int i = 0; i < 10; ++i) data[i] = static_cast ((i - 5) * 1000); - auto ptr = AudioData::Pointer (data); + AudioData::Pointer ptr (data); float minVal, maxVal; ptr.findMinAndMax (10, minVal, maxVal); From 4c2bac76b1067375342d5c335371c3758bed4c12 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Tue, 19 May 2026 10:02:18 +0200 Subject: [PATCH 34/35] Enable SSE on windows x64 --- modules/yup_simd/yup_simd.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/yup_simd/yup_simd.h b/modules/yup_simd/yup_simd.h index f1e567a9d..9520b154e 100644 --- a/modules/yup_simd/yup_simd.h +++ b/modules/yup_simd/yup_simd.h @@ -65,7 +65,7 @@ //============================================================================== #ifndef YUP_USE_SSE_INTRINSICS -#if defined(__SSE__) +#if defined(__SSE__) || defined(_M_X64) || defined(_M_AMD64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2) #define YUP_USE_SSE_INTRINSICS 1 #endif #endif From 99c357abdbf7e37307fa14109755747b7b9d5fe3 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Tue, 19 May 2026 12:02:20 +0200 Subject: [PATCH 35/35] Fix hosting vst and clap on mac --- cmake/yup_dependencies.cmake | 17 +- .../clap/yup_audio_plugin_client_CLAP.cpp | 40 +- .../host/yup_AudioPluginDescription.h | 10 +- .../host/yup_AudioPluginFormat.h | 15 + .../host/yup_AudioPluginHostContext.h | 3 + .../host/yup_AudioPluginInstance.cpp | 68 + .../host/yup_AudioPluginInstance.h | 33 + .../host/yup_AudioPluginScanner.cpp | 64 +- .../host/yup_AudioPluginScanner.h | 4 +- .../native/yup_AudioPluginInstance_AUv2.h | 1 + .../native/yup_AudioPluginInstance_AUv2.mm | 135 +- .../native/yup_AudioPluginInstance_CLAP.cpp | 236 +++- .../native/yup_AudioPluginInstance_CLAP.h | 1 + .../native/yup_AudioPluginInstance_VST3.cpp | 1132 +++++++++++++++-- .../native/yup_AudioPluginInstance_VST3.h | 1 + .../yup_audio_plugin_host.cpp | 26 +- .../processors/yup_AudioProcessor.cpp | 28 +- .../processors/yup_AudioProcessor.h | 20 + .../yup_dsp/windowing/yup_WindowFunctions.h | 28 + .../yup_AudioGraphProcessor.cpp | 4 + .../yup_AudioPluginDescription.cpp | 13 + .../yup_AudioPluginInstance.cpp | 180 +++ .../yup_AudioPluginScanner.cpp | 2 + 23 files changed, 1891 insertions(+), 170 deletions(-) diff --git a/cmake/yup_dependencies.cmake b/cmake/yup_dependencies.cmake index d3de61497..c2e74f493 100644 --- a/cmake/yup_dependencies.cmake +++ b/cmake/yup_dependencies.cmake @@ -128,14 +128,19 @@ function (_yup_fetch_vst3sdk) get_target_property (vst3sdk_source_dir sdk SOURCE_DIR) endif() - if (vst3sdk_source_dir AND EXISTS "${vst3sdk_source_dir}/public.sdk/source/common/memorystream.cpp") - target_sources (yup_audio_plugin_host_vst3sdk INTERFACE - "${vst3sdk_source_dir}/public.sdk/source/common/memorystream.cpp") + set (vst3sdk_memorystream_source "${vst3sdk_source_dir}/public.sdk/source/common/memorystream.cpp") + if (vst3sdk_source_dir AND EXISTS "${vst3sdk_memorystream_source}") + target_sources (yup_audio_plugin_host_vst3sdk INTERFACE "${vst3sdk_memorystream_source}") endif() - if (vst3sdk_source_dir AND EXISTS "${vst3sdk_source_dir}/public.sdk/source/vst/hosting/parameterchanges.cpp") - target_sources (yup_audio_plugin_host_vst3sdk INTERFACE - "${vst3sdk_source_dir}/public.sdk/source/vst/hosting/parameterchanges.cpp") + set (vst3sdk_parameterchanges_source "${vst3sdk_source_dir}/public.sdk/source/vst/hosting/parameterchanges.cpp") + if (vst3sdk_source_dir AND EXISTS "${vst3sdk_parameterchanges_source}") + target_sources (yup_audio_plugin_host_vst3sdk INTERFACE "${vst3sdk_parameterchanges_source}") + endif() + + set (vst3sdk_eventlist_source "${vst3sdk_source_dir}/public.sdk/source/vst/hosting/eventlist.cpp") + if (vst3sdk_source_dir AND EXISTS "${vst3sdk_eventlist_source}") + target_sources (yup_audio_plugin_host_vst3sdk INTERFACE "${vst3sdk_eventlist_source}") endif() endif() endfunction() diff --git a/modules/yup_audio_plugin_client/clap/yup_audio_plugin_client_CLAP.cpp b/modules/yup_audio_plugin_client/clap/yup_audio_plugin_client_CLAP.cpp index bb24117d0..beb01079d 100644 --- a/modules/yup_audio_plugin_client/clap/yup_audio_plugin_client_CLAP.cpp +++ b/modules/yup_audio_plugin_client/clap/yup_audio_plugin_client_CLAP.cpp @@ -603,9 +603,16 @@ bool AudioPluginProcessorCLAP::initialise() auto wrapper = getWrapper (plugin); auto* audioProcessor = wrapper->audioProcessor.get(); - return static_cast (isInput - ? audioProcessor->getBusLayout().getInputBuses().size() - : audioProcessor->getBusLayout().getOutputBuses().size()); + Span busses = isInput + ? audioProcessor->getBusLayout().getInputBuses() + : audioProcessor->getBusLayout().getOutputBuses(); + + uint32_t count = 0; + for (const auto& bus : busses) + if (bus.getType() == AudioBus::Type::Audio) + ++count; + + return count; }; extensionAudioPorts.get = [] (const clap_plugin_t* plugin, uint32_t index, bool isInput, clap_audio_port_info_t* info) -> bool @@ -617,17 +624,32 @@ bool AudioPluginProcessorCLAP::initialise() ? audioProcessor->getBusLayout().getInputBuses() : audioProcessor->getBusLayout().getOutputBuses(); - if (index >= static_cast (busses.size())) - return false; + const AudioBus* audioBus = nullptr; + uint32_t audioBusIndex = 0; - const AudioBus& bus = busses[index]; + for (const auto& bus : busses) + { + if (bus.getType() != AudioBus::Type::Audio) + continue; + + if (audioBusIndex == index) + { + audioBus = &bus; + break; + } + + ++audioBusIndex; + } + + if (audioBus == nullptr) + return false; info->id = index; - info->channel_count = bus.getNumChannels(); + info->channel_count = audioBus->getNumChannels(); info->flags = (index == 0) ? CLAP_AUDIO_PORT_IS_MAIN : 0; - info->port_type = bus.isStereo() ? CLAP_PORT_STEREO : CLAP_PORT_MONO; + info->port_type = audioBus->isStereo() ? CLAP_PORT_STEREO : CLAP_PORT_MONO; info->in_place_pair = CLAP_INVALID_ID; - bus.getName().copyToUTF8 (info->name, sizeof (info->name)); + audioBus->getName().copyToUTF8 (info->name, sizeof (info->name)); return true; }; diff --git a/modules/yup_audio_plugin_host/host/yup_AudioPluginDescription.h b/modules/yup_audio_plugin_host/host/yup_AudioPluginDescription.h index b1f7892d7..ee6401553 100644 --- a/modules/yup_audio_plugin_host/host/yup_AudioPluginDescription.h +++ b/modules/yup_audio_plugin_host/host/yup_AudioPluginDescription.h @@ -68,6 +68,12 @@ struct AudioPluginDescription /** Reported output channel count (sum across main audio output buses). */ int numOutputChannels = 0; + /** Reported MIDI input port count. */ + int numMidiInputPorts = 0; + + /** Reported MIDI output port count. */ + int numMidiOutputPorts = 0; + bool operator== (const AudioPluginDescription& other) const noexcept { return formatType == other.formatType @@ -80,7 +86,9 @@ struct AudioPluginDescription && isInstrument == other.isInstrument && isEffect == other.isEffect && numInputChannels == other.numInputChannels - && numOutputChannels == other.numOutputChannels; + && numOutputChannels == other.numOutputChannels + && numMidiInputPorts == other.numMidiInputPorts + && numMidiOutputPorts == other.numMidiOutputPorts; } bool operator!= (const AudioPluginDescription& other) const noexcept diff --git a/modules/yup_audio_plugin_host/host/yup_AudioPluginFormat.h b/modules/yup_audio_plugin_host/host/yup_AudioPluginFormat.h index 4914c9d63..1ebe51625 100644 --- a/modules/yup_audio_plugin_host/host/yup_AudioPluginFormat.h +++ b/modules/yup_audio_plugin_host/host/yup_AudioPluginFormat.h @@ -46,6 +46,21 @@ class AudioPluginFormat /** Returns a human-readable name for the format (e.g. "VST3"). */ virtual String getFormatName() const = 0; + /** + Returns the file extensions this format recognises, including the leading dot + (e.g. { ".vst3" } or { ".clap" }). + + The scanner uses these extensions to filter filesystem entries before calling + scanFile(). Return an empty array for registry-based formats (e.g. AUv2) that + do not perform file-system scanning — the scanner will call scanFile() with an + invalid File instead. + + Platform-specific extensions (e.g. ".dll" on Windows, ".so" on Linux) should + be returned conditionally so only the relevant extensions are active on each + platform. + */ + virtual StringArray getFileExtensions() const = 0; + //============================================================================== /** diff --git a/modules/yup_audio_plugin_host/host/yup_AudioPluginHostContext.h b/modules/yup_audio_plugin_host/host/yup_AudioPluginHostContext.h index 42a3b0861..9edcd76b7 100644 --- a/modules/yup_audio_plugin_host/host/yup_AudioPluginHostContext.h +++ b/modules/yup_audio_plugin_host/host/yup_AudioPluginHostContext.h @@ -40,6 +40,9 @@ struct AudioPluginHostContext /** True to prefer double-precision processing where the plugin supports it. */ bool preferDoublePrecision = false; + /** True when the host is preparing the plugin for offline/non-realtime rendering. */ + bool isNonRealtime = false; + /** Optional playhead — the host sets this before processing. */ AudioPlayHead* playHead = nullptr; diff --git a/modules/yup_audio_plugin_host/host/yup_AudioPluginInstance.cpp b/modules/yup_audio_plugin_host/host/yup_AudioPluginInstance.cpp index 9115386d3..fb4cb596c 100644 --- a/modules/yup_audio_plugin_host/host/yup_AudioPluginInstance.cpp +++ b/modules/yup_audio_plugin_host/host/yup_AudioPluginInstance.cpp @@ -22,6 +22,34 @@ namespace yup { +namespace +{ + +template +void processPluginBypassedBlock (const AudioPluginDescription& description, + AudioBuffer& audioBuffer) noexcept +{ + const int numSamples = audioBuffer.getNumSamples(); + const int numBufferChannels = audioBuffer.getNumChannels(); + const int numInputs = jmin (description.numInputChannels, numBufferChannels); + const int numOutputs = jmin (description.numOutputChannels, numBufferChannels); + const int numChannelsToCopy = jmin (numInputs, numOutputs); + + for (int channel = 0; channel < numChannelsToCopy; ++channel) + { + const auto* source = audioBuffer.getReadPointer (channel); + auto* destination = audioBuffer.getWritePointer (channel); + + if (source != destination) + FloatVectorOperations::copy (destination, source, numSamples); + } + + for (int channel = numChannelsToCopy; channel < numOutputs; ++channel) + audioBuffer.clear (channel, 0, numSamples); +} + +} // namespace + AudioPluginInstance::AudioPluginInstance (const AudioPluginDescription& description, AudioBusLayout busLayout) : AudioProcessor (description.name, std::move (busLayout)) @@ -41,4 +69,44 @@ AudioPluginFormatType AudioPluginInstance::getFormatType() const noexcept return pluginDescription.formatType; } +void AudioPluginInstance::setNonRealtime (bool shouldBeNonRealtime) noexcept +{ + if (nonRealtime == shouldBeNonRealtime) + return; + + nonRealtime = shouldBeNonRealtime; + nonRealtimeStateChanged(); +} + +bool AudioPluginInstance::isNonRealtime() const noexcept +{ + return nonRealtime; +} + +void AudioPluginInstance::setBypassed (bool shouldBeBypassed) noexcept +{ + if (bypassed == shouldBeBypassed) + return; + + bypassed = shouldBeBypassed; + bypassStateChanged(); +} + +bool AudioPluginInstance::isBypassed() const noexcept +{ + return bypassed; +} + +void AudioPluginInstance::processBlockBypassed (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) +{ + ignoreUnused (midiBuffer); + processPluginBypassedBlock (pluginDescription, audioBuffer); +} + +void AudioPluginInstance::processBlockBypassed (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) +{ + ignoreUnused (midiBuffer); + processPluginBypassedBlock (pluginDescription, audioBuffer); +} + } // namespace yup diff --git a/modules/yup_audio_plugin_host/host/yup_AudioPluginInstance.h b/modules/yup_audio_plugin_host/host/yup_AudioPluginInstance.h index a4cf3bb78..e696344b6 100644 --- a/modules/yup_audio_plugin_host/host/yup_AudioPluginInstance.h +++ b/modules/yup_audio_plugin_host/host/yup_AudioPluginInstance.h @@ -52,10 +52,43 @@ class AudioPluginInstance : public AudioProcessor /** Returns the format type of this instance. */ AudioPluginFormatType getFormatType() const noexcept; + //============================================================================== + + /** Sets whether this instance should render in offline/non-realtime mode. */ + void setNonRealtime (bool shouldBeNonRealtime) noexcept; + + /** Returns true when this instance is rendering in offline/non-realtime mode. */ + bool isNonRealtime() const noexcept; + + /** Sets whether processBlock() should render the hosted plugin bypassed. */ + void setBypassed (bool shouldBeBypassed) noexcept; + + /** Returns true when this instance is currently bypassed. */ + bool isBypassed() const noexcept; + + /** Processes a bypassed single-precision block by copying matching inputs to + outputs and clearing outputs without matching inputs. + */ + void processBlockBypassed (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) override; + + /** Processes a bypassed double-precision block by copying matching inputs to + outputs and clearing outputs without matching inputs. + */ + void processBlockBypassed (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) override; + protected: AudioPluginDescription pluginDescription; + /** Called after setNonRealtime() changes state. */ + virtual void nonRealtimeStateChanged() {} + + /** Called after setBypassed() changes state. */ + virtual void bypassStateChanged() {} + private: + bool nonRealtime = false; + bool bypassed = false; + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioPluginInstance) }; diff --git a/modules/yup_audio_plugin_host/host/yup_AudioPluginScanner.cpp b/modules/yup_audio_plugin_host/host/yup_AudioPluginScanner.cpp index 7024ace6c..5db4ff8a0 100644 --- a/modules/yup_audio_plugin_host/host/yup_AudioPluginScanner.cpp +++ b/modules/yup_audio_plugin_host/host/yup_AudioPluginScanner.cpp @@ -25,26 +25,6 @@ namespace yup namespace { -bool canScanFileWithFormat (AudioPluginFormatType type, const File& file) -{ - switch (type) - { - case AudioPluginFormatType::vst3: - return file.hasFileExtension (".vst3"); - - case AudioPluginFormatType::clap: - return file.hasFileExtension (".clap"); - - case AudioPluginFormatType::audioUnit: - return false; - - default: - break; - } - - return false; -} - struct FileScanTask { File file; @@ -128,35 +108,45 @@ AudioPluginScanner::ScanResult AudioPluginScanner::scan (const FileSearchPath& s { ScanResult result; - // AUv2 does not use file system scanning; delegate directly if registered. - if (auto* auFormat = getFormatForType (AudioPluginFormatType::audioUnit)) + for (const auto& format : formats) { - // AUv2 scanFile() with an invalid File triggers registry enumeration - auto auResult = auFormat->scanFile (File {}); - if (auResult.wasOk()) + if (format->getFileExtensions().isEmpty()) { - auto descriptions = auResult.getValue(); - result.discovered.insert (result.discovered.end(), - descriptions.begin(), - descriptions.end()); + auto scanResult = format->scanFile (File {}); + if (scanResult.wasOk()) + { + auto descriptions = scanResult.getValue(); + result.discovered.insert (result.discovered.end(), + descriptions.begin(), + descriptions.end()); + } } } std::vector tasks; const int numPaths = searchPath.getNumPaths(); - for (int p = 0; p < numPaths; ++p) + for (const auto& format : formats) { - Array files; - searchPath[p].findChildFiles (files, File::findFilesAndDirectories, true, "*", File::FollowSymlinks::noCycles); + String wildcardPattern; + for (const auto& ext : format->getFileExtensions()) + { + if (wildcardPattern.isNotEmpty()) + wildcardPattern += ";"; - for (const auto& file : files) + wildcardPattern += "*" + ext; + } + + if (wildcardPattern.isEmpty()) + continue; + + for (int p = 0; p < numPaths; ++p) { - for (const auto& format : formats) - { - if (! canScanFileWithFormat (format->getFormatType(), file)) - continue; + Array files; + searchPath[p].findChildFiles (files, File::findFilesAndDirectories, true, wildcardPattern, File::FollowSymlinks::noCycles); + for (const auto& file : files) + { FileScanTask task; task.file = file; task.format = format.get(); diff --git a/modules/yup_audio_plugin_host/host/yup_AudioPluginScanner.h b/modules/yup_audio_plugin_host/host/yup_AudioPluginScanner.h index e59d9aaae..e8116e4e1 100644 --- a/modules/yup_audio_plugin_host/host/yup_AudioPluginScanner.h +++ b/modules/yup_audio_plugin_host/host/yup_AudioPluginScanner.h @@ -85,8 +85,8 @@ class AudioPluginScanner matches the format. If a format returns a success, its results are appended; if it fails, the path is added to ScanResult::failedPaths. - AUv2 scanning enumerates the AudioComponent registry and does not walk - the search path — pass an empty FileSearchPath when only AUv2 is registered. + Formats that return an empty array from getFileExtensions() (e.g. AUv2) use + registry-based discovery and are scanned independently of the search path. */ ScanResult scan (const FileSearchPath& searchPath); diff --git a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.h b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.h index 3aa7e74e8..08a597704 100644 --- a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.h +++ b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.h @@ -38,6 +38,7 @@ class AUv2Format : public AudioPluginFormat AudioPluginFormatType getFormatType() const override; String getFormatName() const override; + StringArray getFileExtensions() const override; /** Returns an empty FileSearchPath — AUv2 uses AudioComponent registry. */ FileSearchPath getDefaultSearchPaths() const override; diff --git a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.mm b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.mm index f942ce498..d20d10dff 100644 --- a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.mm +++ b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_AUv2.mm @@ -71,6 +71,14 @@ AudioPluginDescription descriptionFromComponent(AudioComponent comp, desc.isInstrument = (acd.componentType == kAudioUnitType_MusicDevice); desc.isEffect = (acd.componentType == kAudioUnitType_Effect || acd.componentType == kAudioUnitType_MusicEffect); + if (acd.componentType == kAudioUnitType_MusicDevice || acd.componentType == kAudioUnitType_MusicEffect || acd.componentType == kAudioUnitType_MIDIProcessor) + { + desc.numMidiInputPorts = 1; + } + + if (acd.componentType == kAudioUnitType_MIDIProcessor) + desc.numMidiOutputPorts = 1; + if (acd.componentType == kAudioUnitType_Effect || acd.componentType == kAudioUnitType_MusicEffect) { desc.numInputChannels = 2; @@ -343,9 +351,24 @@ void updateCocoaViewFrame() if (audioUnit != nullptr) { - AudioUnitUninitialize(audioUnit); - AudioComponentInstanceDispose(audioUnit); + // AudioComponentInstanceDispose must run on the main thread so that + // CoreAudio's internal timers (MAudioPluginInterface::OnTimer etc.) + // registered on CFRunLoopGetMain() are properly invalidated before + // the AudioUnit object is freed. Calling from a background thread + // leaves a window where those timers fire on freed memory. + AudioUnit capturedUnit = audioUnit; audioUnit = nullptr; + + if ([NSThread isMainThread]) + { + AudioComponentInstanceDispose (capturedUnit); + } + else + { + dispatch_sync (dispatch_get_main_queue(), ^{ + AudioComponentInstanceDispose (capturedUnit); + }); + } } } @@ -376,6 +399,7 @@ void prepareToPlay(float sampleRate, int maxBlockSize) override configureStreamFormat(sampleRateValue, numHostedChannels); installInputCallback(); + installMIDIOutputCallback(); if (![NSThread isMainThread]) { @@ -409,14 +433,20 @@ void releaseResources() override AudioUnitUninitialize(audioUnit); } - void processBlock(AudioBuffer& audioBuffer, MidiBuffer&) override + void processBlock(AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) override { ScopedNoDenormals noDenormals; + if (isBypassed()) + { + processBlockBypassed(audioBuffer, midiBuffer); + return; + } + const int numSamples = audioBuffer.getNumSamples(); const int numChannels = audioBuffer.getNumChannels(); - if (numSamples <= 0 || numChannels <= 0) + if (numSamples <= 0) return; if (numChannels > preparedNumChannels || numSamples > preparedMaxBlockSize) @@ -425,6 +455,18 @@ void processBlock(AudioBuffer& audioBuffer, MidiBuffer&) override return; } + outputMidiBuffer.clear(); + currentMidiOutputBuffer = &outputMidiBuffer; + sendMidiInputEvents(midiBuffer); + + if (numChannels <= 0 || pluginDescription.numOutputChannels <= 0) + { + currentMidiOutputBuffer = nullptr; + midiBuffer.clear(); + midiBuffer.addEvents(outputMidiBuffer, 0, -1, 0); + return; + } + for (int c = 0; c < numChannels; ++c) { std::memcpy(inputBuffer.getWritePointer(c), @@ -454,6 +496,7 @@ void processBlock(AudioBuffer& audioBuffer, MidiBuffer&) override currentInputBuffer = nullptr; currentInputNumChannels = 0; currentInputNumSamples = 0; + currentMidiOutputBuffer = nullptr; if (status != noErr) { @@ -468,6 +511,9 @@ void processBlock(AudioBuffer& audioBuffer, MidiBuffer&) override abl->mBuffers[c].mData, static_cast(numSamples) * sizeof(float)); } + + midiBuffer.clear(); + midiBuffer.addEvents(outputMidiBuffer, 0, -1, 0); } //============================================================================== @@ -645,7 +691,7 @@ bool hasEditor() const override //============================================================================== static std::unique_ptr create(const AudioPluginDescription& desc, - const AudioPluginHostContext&) + const AudioPluginHostContext& context) { // Parse "type/subt/mfgr" identifier const auto tokens = StringArray::fromTokens(desc.identifier, "/", ""); @@ -675,7 +721,9 @@ bool hasEditor() const override AudioBusLayout busLayout = makeBusLayout(desc, acd.componentType); - return std::make_unique(desc, unit, std::move(busLayout)); + auto instance = std::make_unique(desc, unit, std::move(busLayout)); + instance->setNonRealtime(context.isNonRealtime); + return instance; } private: @@ -704,6 +752,12 @@ static AudioBusLayout makeBusLayout(const AudioPluginDescription& desc, OSType c if (outputChannels > 0) outputs.emplace_back("Output", AudioBus::Type::Audio, AudioBus::Direction::Output, outputChannels); + if (desc.numMidiInputPorts > 0) + inputs.emplace_back("MIDI Input", AudioBus::Type::MIDI, AudioBus::Direction::Input, 1); + + if (desc.numMidiOutputPorts > 0) + outputs.emplace_back("MIDI Output", AudioBus::Type::MIDI, AudioBus::Direction::Output, 1); + return AudioBusLayout(std::move(inputs), std::move(outputs)); } @@ -885,6 +939,68 @@ void installInputCallback() &callback, sizeof(callback)); } + void installMIDIOutputCallback() + { + AUMIDIOutputCallbackStruct callback{}; + callback.midiOutputCallback = midiOutputCallback; + callback.userData = this; + + AudioUnitSetProperty(audioUnit, + kAudioUnitProperty_MIDIOutputCallback, + kAudioUnitScope_Global, 0, + &callback, sizeof(callback)); + } + + void sendMidiInputEvents(const MidiBuffer& midiBuffer) + { + for (const auto& metadata : midiBuffer) + { + const auto message = metadata.getMessage(); + + if (message.isSysEx()) + { + MusicDeviceSysEx(audioUnit, + message.getSysExData(), + static_cast(message.getSysExDataSize())); + continue; + } + + const auto* data = message.getRawData(); + const int size = message.getRawDataSize(); + + if (size <= 0) + continue; + + MusicDeviceMIDIEvent(audioUnit, + data[0], + size > 1 ? data[1] : 0, + size > 2 ? data[2] : 0, + static_cast(jmax(0, metadata.samplePosition))); + } + } + + static OSStatus midiOutputCallback(void* userData, + const AudioTimeStamp*, + UInt32, + const MIDIPacketList* packetList) + { + auto* instance = static_cast(userData); + if (instance == nullptr || instance->currentMidiOutputBuffer == nullptr || packetList == nullptr) + return noErr; + + const MIDIPacket* packet = &packetList->packet[0]; + + for (UInt32 i = 0; i < packetList->numPackets; ++i) + { + instance->currentMidiOutputBuffer->addEvent(packet->data, + packet->length, + static_cast(packet->timeStamp)); + packet = MIDIPacketNext(packet); + } + + return noErr; + } + static std::size_t getAudioBufferListSize(int numChannels) { return sizeof(AudioBufferList) + static_cast(jmax(0, numChannels - 1)) * sizeof(::AudioBuffer); @@ -1049,7 +1165,9 @@ void removeParameterListeners() double renderSampleTime = 0.0; std::vector auParameterIds; AudioBuffer inputBuffer; + MidiBuffer outputMidiBuffer; AudioBuffer* currentInputBuffer = nullptr; + MidiBuffer* currentMidiOutputBuffer = nullptr; int currentInputNumChannels = 0; int currentInputNumSamples = 0; int preparedNumChannels = 0; @@ -1072,6 +1190,11 @@ void removeParameterListeners() return "AUv2"; } +StringArray AUv2Format::getFileExtensions() const +{ + return {}; +} + FileSearchPath AUv2Format::getDefaultSearchPaths() const { return {}; diff --git a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.cpp b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.cpp index 115c221f3..9497c71ad 100644 --- a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.cpp +++ b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.cpp @@ -74,6 +74,7 @@ struct CLAPModule struct YUPCLAPHost { clap_host_t host {}; + clap_host_note_ports_t notePorts {}; String hostName; String hostVendor; String hostVersion; @@ -89,8 +90,18 @@ struct YUPCLAPHost host.vendor = hostVendor.toRawUTF8(); host.url = ""; host.version = hostVersion.toRawUTF8(); - host.get_extension = [] (const clap_host_t*, const char*) -> const void* + notePorts.supported_dialects = [] (const clap_host_t*) -> uint32_t { + return CLAP_NOTE_DIALECT_CLAP | CLAP_NOTE_DIALECT_MIDI; + }; + notePorts.rescan = [] (const clap_host_t*, uint32_t) {}; + + host.get_extension = [] (const clap_host_t* host, const char* extensionId) -> const void* + { + auto* self = static_cast (host->host_data); + if (std::strcmp (extensionId, CLAP_EXT_NOTE_PORTS) == 0) + return &self->notePorts; + return nullptr; }; host.request_restart = [] (const clap_host_t*) {}; @@ -101,7 +112,11 @@ struct YUPCLAPHost struct CLAPInputEvents { - std::vector parameterEvents; + std::deque parameterEvents; + std::deque noteEvents; + std::deque midiEvents; + std::deque sysexEvents; + std::vector eventHeaders; clap_input_events_t inputEvents {}; CLAPInputEvents() @@ -110,18 +125,27 @@ struct CLAPInputEvents inputEvents.size = [] (const clap_input_events_t* events) -> uint32_t { auto* self = static_cast (events->ctx); - return static_cast (self->parameterEvents.size()); + return static_cast (self->eventHeaders.size()); }; inputEvents.get = [] (const clap_input_events_t* events, uint32_t index) -> const clap_event_header_t* { auto* self = static_cast (events->ctx); - if (index >= self->parameterEvents.size()) + if (index >= self->eventHeaders.size()) return nullptr; - return &self->parameterEvents[static_cast (index)].header; + return self->eventHeaders[static_cast (index)]; }; } + void clear() + { + parameterEvents.clear(); + noteEvents.clear(); + midiEvents.clear(); + sysexEvents.clear(); + eventHeaders.clear(); + } + void addParameterValue (clap_id id, double value) { clap_event_param_value_t event {}; @@ -137,21 +161,117 @@ struct CLAPInputEvents event.value = value; parameterEvents.push_back (event); + eventHeaders.push_back (¶meterEvents.back().header); + } + + void addMidiMessage (const MidiMessageMetadata& metadata) + { + const auto message = metadata.getMessage(); + + if (message.isNoteOn() || message.isNoteOff()) + { + clap_event_note_t event {}; + event.header.size = sizeof (event); + event.header.time = static_cast (jmax (0, metadata.samplePosition)); + event.header.space_id = CLAP_CORE_EVENT_SPACE_ID; + event.header.type = message.isNoteOn() ? CLAP_EVENT_NOTE_ON : CLAP_EVENT_NOTE_OFF; + event.note_id = -1; + event.port_index = 0; + event.channel = static_cast (message.getChannel() - 1); + event.key = static_cast (message.getNoteNumber()); + event.velocity = static_cast (message.getFloatVelocity()); + + noteEvents.push_back (event); + eventHeaders.push_back (¬eEvents.back().header); + return; + } + + if (message.isSysEx()) + { + clap_event_midi_sysex_t event {}; + event.header.size = sizeof (event); + event.header.time = static_cast (jmax (0, metadata.samplePosition)); + event.header.space_id = CLAP_CORE_EVENT_SPACE_ID; + event.header.type = CLAP_EVENT_MIDI_SYSEX; + event.port_index = 0; + event.buffer = message.getSysExData(); + event.size = static_cast (message.getSysExDataSize()); + + sysexEvents.push_back (event); + eventHeaders.push_back (&sysexEvents.back().header); + return; + } + + if (metadata.numBytes > 0 && metadata.numBytes <= 3) + { + clap_event_midi_t event {}; + event.header.size = sizeof (event); + event.header.time = static_cast (jmax (0, metadata.samplePosition)); + event.header.space_id = CLAP_CORE_EVENT_SPACE_ID; + event.header.type = CLAP_EVENT_MIDI; + event.port_index = 0; + std::memcpy (event.data, metadata.data, static_cast (metadata.numBytes)); + + midiEvents.push_back (event); + eventHeaders.push_back (&midiEvents.back().header); + } } }; struct CLAPOutputEvents { clap_output_events_t outputEvents {}; + MidiBuffer* midiOutput = nullptr; CLAPOutputEvents() { outputEvents.ctx = this; - outputEvents.try_push = [] (const clap_output_events_t*, const clap_event_header_t*) -> bool + outputEvents.try_push = [] (const clap_output_events_t* events, const clap_event_header_t* event) -> bool { + auto* self = static_cast (events->ctx); + if (self == nullptr || self->midiOutput == nullptr || event == nullptr) + return false; + + self->addEventToMidiBuffer (*event); return true; }; } + + void addEventToMidiBuffer (const clap_event_header_t& event) + { + if (event.space_id != CLAP_CORE_EVENT_SPACE_ID) + return; + + const int samplePosition = static_cast (event.time); + + if (event.type == CLAP_EVENT_NOTE_ON + || event.type == CLAP_EVENT_NOTE_OFF + || event.type == CLAP_EVENT_NOTE_CHOKE + || event.type == CLAP_EVENT_NOTE_END) + { + const auto& note = *reinterpret_cast (&event); + const int channel = note.channel >= 0 ? note.channel + 1 : 1; + const int key = note.key >= 0 ? note.key : 0; + const auto velocity = static_cast (note.velocity); + + midiOutput->addEvent (event.type == CLAP_EVENT_NOTE_ON + ? MidiMessage::noteOn (channel, key, velocity) + : MidiMessage::noteOff (channel, key, velocity), + samplePosition); + } + else if (event.type == CLAP_EVENT_MIDI) + { + const auto& midi = *reinterpret_cast (&event); + const int numBytes = MidiMessage::getMessageLengthFromFirstByte (midi.data[0]); + midiOutput->addEvent (midi.data, numBytes, samplePosition); + } + else if (event.type == CLAP_EVENT_MIDI_SYSEX) + { + const auto& sysex = *reinterpret_cast (&event); + midiOutput->addEvent (MidiMessage::createSysExMessage (sysex.buffer, static_cast (sysex.size)), + samplePosition); + } + } }; } // namespace @@ -173,6 +293,7 @@ class CLAPInstance : public AudioPluginInstance , clapPlugin (plugin) { buildParameterList(); + setNonRealtime (context.isNonRealtime); } ~CLAPInstance() override @@ -193,9 +314,9 @@ class CLAPInstance : public AudioPluginInstance const int numChannels = jmax (2, pluginDescription.numInputChannels, pluginDescription.numOutputChannels); preparedInPtrs.resize (static_cast (numChannels)); preparedOutPtrs.resize (static_cast (numChannels)); - clapInputEvents.parameterEvents.reserve (clapParameterIds.size()); clapPlugin->activate (clapPlugin, sampleRate, 1, static_cast (jmax (1, maxBlockSize))); + updateRenderMode(); clapPlugin->start_processing (clapPlugin); } @@ -209,14 +330,20 @@ class CLAPInstance : public AudioPluginInstance } } - void processBlock (AudioBuffer& audioBuffer, MidiBuffer& /*midiBuffer*/) override + void processBlock (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) override { ScopedNoDenormals noDenormals; + if (isBypassed()) + { + processBlockBypassed (audioBuffer, midiBuffer); + return; + } + const int numSamples = audioBuffer.getNumSamples(); const int numChannels = audioBuffer.getNumChannels(); - if (static_cast (preparedInPtrs.size()) < numChannels) + if (static_cast (preparedInPtrs.size()) < numChannels && numChannels > 0) { jassertfalse; return; @@ -229,36 +356,50 @@ class CLAPInstance : public AudioPluginInstance } clap_audio_buffer_t inputBuf {}; - inputBuf.data32 = const_cast (preparedInPtrs.data()); - inputBuf.channel_count = static_cast (numChannels); + if (pluginDescription.numInputChannels > 0 && numChannels > 0) + { + inputBuf.data32 = const_cast (preparedInPtrs.data()); + inputBuf.channel_count = static_cast (numChannels); + } inputBuf.latency = 0; inputBuf.constant_mask = 0; clap_audio_buffer_t outputBuf {}; - outputBuf.data32 = preparedOutPtrs.data(); - outputBuf.channel_count = static_cast (numChannels); + if (pluginDescription.numOutputChannels > 0 && numChannels > 0) + { + outputBuf.data32 = preparedOutPtrs.data(); + outputBuf.channel_count = static_cast (numChannels); + } clap_process_t process {}; process.steady_time = -1; process.frames_count = static_cast (numSamples); - process.audio_inputs = &inputBuf; - process.audio_inputs_count = 1; - process.audio_outputs = &outputBuf; - process.audio_outputs_count = 1; + process.audio_inputs = inputBuf.channel_count > 0 ? &inputBuf : nullptr; + process.audio_inputs_count = inputBuf.channel_count > 0 ? 1 : 0; + process.audio_outputs = outputBuf.channel_count > 0 ? &outputBuf : nullptr; + process.audio_outputs_count = outputBuf.channel_count > 0 ? 1 : 0; - clapInputEvents.parameterEvents.clear(); + clapInputEvents.clear(); const auto params = getParameters(); const auto numParams = yup::jmin (params.size(), clapParameterIds.size()); for (std::size_t i = 0; i < numParams; ++i) clapInputEvents.addParameterValue (clapParameterIds[i], static_cast (params[i]->getValue())); + for (const auto& metadata : midiBuffer) + clapInputEvents.addMidiMessage (metadata); + process.in_events = &clapInputEvents.inputEvents; process.out_events = &clapOutputEvents.outputEvents; - // TODO: map MidiBuffer to CLAP event list in a later task + outputMidiBuffer.clear(); + clapOutputEvents.midiOutput = &outputMidiBuffer; clapPlugin->process (clapPlugin, &process); + + clapOutputEvents.midiOutput = nullptr; + midiBuffer.clear(); + midiBuffer.addEvents (outputMidiBuffer, 0, -1, 0); } //============================================================================== @@ -414,6 +555,28 @@ class CLAPInstance : public AudioPluginInstance } } + auto* notePortsExt = reinterpret_cast ( + plugin->get_extension (plugin, CLAP_EXT_NOTE_PORTS)); + + if (notePortsExt != nullptr) + { + const uint32_t numInputs = notePortsExt->count (plugin, true); + for (uint32_t i = 0; i < numInputs; ++i) + { + clap_note_port_info_t info {}; + if (notePortsExt->get (plugin, i, true, &info)) + inputs.emplace_back (String (info.name), AudioBus::Type::MIDI, AudioBus::Direction::Input, 1); + } + + const uint32_t numOutputs = notePortsExt->count (plugin, false); + for (uint32_t i = 0; i < numOutputs; ++i) + { + clap_note_port_info_t info {}; + if (notePortsExt->get (plugin, i, false, &info)) + outputs.emplace_back (String (info.name), AudioBus::Type::MIDI, AudioBus::Direction::Output, 1); + } + } + return AudioBusLayout (std::move (inputs), std::move (outputs)); } @@ -445,6 +608,23 @@ class CLAPInstance : public AudioPluginInstance } } + void updateRenderMode() + { + if (clapPlugin == nullptr) + return; + + auto* renderExt = reinterpret_cast ( + clapPlugin->get_extension (clapPlugin, CLAP_EXT_RENDER)); + + if (renderExt != nullptr && renderExt->set != nullptr) + renderExt->set (clapPlugin, isNonRealtime() ? CLAP_RENDER_OFFLINE : CLAP_RENDER_REALTIME); + } + + void nonRealtimeStateChanged() override + { + updateRenderMode(); + } + AudioPluginHostContext hostContext; std::unique_ptr clapModule; std::unique_ptr yupHost; @@ -453,6 +633,7 @@ class CLAPInstance : public AudioPluginInstance CLAPOutputEvents clapOutputEvents; std::vector preparedInPtrs; std::vector preparedOutPtrs; + MidiBuffer outputMidiBuffer; std::vector clapParameterIds; int currentPreset = 0; }; @@ -472,14 +653,18 @@ String CLAPFormat::getFormatName() const return "CLAP"; } +StringArray CLAPFormat::getFileExtensions() const +{ + return { ".clap" }; +} + FileSearchPath CLAPFormat::getDefaultSearchPaths() const { FileSearchPath paths; #if YUP_MAC paths.add (File ("/Library/Audio/Plug-Ins/CLAP")); - paths.add (File::getSpecialLocation (File::userHomeDirectory) - .getChildFile ("Library/Audio/Plug-Ins/CLAP")); + paths.add (File::getSpecialLocation (File::userHomeDirectory).getChildFile ("Library/Audio/Plug-Ins/CLAP")); #elif YUP_WINDOWS if (const char* pf = getenv ("CommonProgramFiles")) paths.add (File (String (pf) + "\\CLAP")); @@ -580,6 +765,15 @@ ResultValue> CLAPFormat::scanFile (const Fil } } + auto* notePortsExt = reinterpret_cast ( + plugin->get_extension (plugin, CLAP_EXT_NOTE_PORTS)); + + if (notePortsExt != nullptr) + { + desc.numMidiInputPorts = static_cast (notePortsExt->count (plugin, true)); + desc.numMidiOutputPorts = static_cast (notePortsExt->count (plugin, false)); + } + plugin->destroy (plugin); } } diff --git a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.h b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.h index c91069e32..94d6a37bb 100644 --- a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.h +++ b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_CLAP.h @@ -38,6 +38,7 @@ class CLAPFormat : public AudioPluginFormat AudioPluginFormatType getFormatType() const override; String getFormatName() const override; + StringArray getFileExtensions() const override; FileSearchPath getDefaultSearchPaths() const override; diff --git a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.cpp b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.cpp index 36466e960..a56b37e5e 100644 --- a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.cpp +++ b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.cpp @@ -21,6 +21,10 @@ #if YUP_AUDIO_PLUGIN_HOST_ENABLE_VST3 +#if YUP_MAC +#include +#endif + namespace yup { @@ -29,6 +33,21 @@ using namespace Steinberg; namespace { +#if YUP_MAC +template +struct VST3CFObjectDeleter +{ + void operator() (CFType object) const noexcept + { + if (object != nullptr) + CFRelease (object); + } +}; + +template +using VST3CFUniquePtr = std::unique_ptr, VST3CFObjectDeleter>; +#endif + //============================================================================== // Converts a Steinberg TChar (UTF-16) string to a yup::String. String toString (const Vst::TChar* src) @@ -41,44 +60,529 @@ String classIDToString (const TUID& cid) return String::toHexString (cid, static_cast (sizeof (TUID))); } +void toString128 (const String& source, Vst::String128 destination) +{ + if (destination == nullptr) + return; + + const auto utf16 = source.toUTF16(); + const auto length = jmin (static_cast (127), utf16.length()); + + std::memcpy (destination, utf16.getAddress(), length * sizeof (Vst::TChar)); + destination[length] = 0; +} + +void addMidiMessageToVST3Events (Vst::EventList& events, const MidiMessageMetadata& metadata) +{ + const auto message = metadata.getMessage(); + + Vst::Event event {}; + event.busIndex = 0; + event.sampleOffset = metadata.samplePosition; + + if (message.isNoteOn()) + { + event.type = Vst::Event::kNoteOnEvent; + event.noteOn.channel = static_cast (message.getChannel() - 1); + event.noteOn.pitch = static_cast (message.getNoteNumber()); + event.noteOn.velocity = message.getFloatVelocity(); + event.noteOn.length = 0; + event.noteOn.noteId = -1; + events.addEvent (event); + return; + } + + if (message.isNoteOff()) + { + event.type = Vst::Event::kNoteOffEvent; + event.noteOff.channel = static_cast (message.getChannel() - 1); + event.noteOff.pitch = static_cast (message.getNoteNumber()); + event.noteOff.velocity = message.getFloatVelocity(); + event.noteOff.noteId = -1; + events.addEvent (event); + return; + } + + if (message.isAftertouch()) + { + event.type = Vst::Event::kPolyPressureEvent; + event.polyPressure.channel = static_cast (message.getChannel() - 1); + event.polyPressure.pitch = static_cast (message.getNoteNumber()); + event.polyPressure.pressure = static_cast (message.getAfterTouchValue()) / 127.0f; + event.polyPressure.noteId = -1; + events.addEvent (event); + return; + } + + if (message.isSysEx()) + { + event.type = Vst::Event::kDataEvent; + event.data.type = Vst::DataEvent::kMidiSysEx; + event.data.bytes = message.getSysExData(); + event.data.size = static_cast (message.getSysExDataSize()); + events.addEvent (event); + return; + } + + event.type = Vst::Event::kLegacyMIDICCOutEvent; + event.midiCCOut.channel = static_cast (jlimit (0, 15, message.getChannel() - 1)); + + if (message.isController()) + { + event.midiCCOut.controlNumber = static_cast (message.getControllerNumber()); + event.midiCCOut.value = static_cast (message.getControllerValue()); + events.addEvent (event); + } + else if (message.isProgramChange()) + { + event.midiCCOut.controlNumber = static_cast (Vst::kCtrlProgramChange); + event.midiCCOut.value = static_cast (message.getProgramChangeNumber()); + events.addEvent (event); + } + else if (message.isPitchWheel()) + { + const auto value = jlimit (0, 0x3fff, message.getPitchWheelValue()); + event.midiCCOut.controlNumber = static_cast (Vst::kPitchBend); + event.midiCCOut.value = static_cast (value & 0x7f); + event.midiCCOut.value2 = static_cast ((value >> 7) & 0x7f); + events.addEvent (event); + } + else if (message.isChannelPressure()) + { + event.midiCCOut.controlNumber = static_cast (Vst::kAfterTouch); + event.midiCCOut.value = static_cast (message.getChannelPressureValue()); + events.addEvent (event); + } +} + +void addVST3EventToMidiBuffer (const Vst::Event& event, MidiBuffer& midiBuffer) +{ + switch (event.type) + { + case Vst::Event::kNoteOnEvent: + midiBuffer.addEvent (MidiMessage::noteOn (event.noteOn.channel + 1, + event.noteOn.pitch, + event.noteOn.velocity), + event.sampleOffset); + break; + + case Vst::Event::kNoteOffEvent: + midiBuffer.addEvent (MidiMessage::noteOff (event.noteOff.channel + 1, + event.noteOff.pitch, + event.noteOff.velocity), + event.sampleOffset); + break; + + case Vst::Event::kPolyPressureEvent: + midiBuffer.addEvent (MidiMessage::aftertouchChange (event.polyPressure.channel + 1, + event.polyPressure.pitch, + jlimit (0, 127, static_cast (event.polyPressure.pressure * 127.0f))), + event.sampleOffset); + break; + + case Vst::Event::kDataEvent: + if (event.data.type == Vst::DataEvent::kMidiSysEx) + midiBuffer.addEvent (MidiMessage::createSysExMessage (event.data.bytes, static_cast (event.data.size)), + event.sampleOffset); + break; + + case Vst::Event::kLegacyMIDICCOutEvent: + { + const int channel = event.midiCCOut.channel + 1; + + if (event.midiCCOut.controlNumber <= 127) + { + midiBuffer.addEvent (MidiMessage::controllerEvent (channel, + event.midiCCOut.controlNumber, + static_cast (event.midiCCOut.value)), + event.sampleOffset); + } + else if (event.midiCCOut.controlNumber == Vst::kCtrlProgramChange) + { + midiBuffer.addEvent (MidiMessage::programChange (channel, static_cast (event.midiCCOut.value)), + event.sampleOffset); + } + else if (event.midiCCOut.controlNumber == Vst::kPitchBend) + { + midiBuffer.addEvent (MidiMessage::pitchWheel (channel, + (static_cast (event.midiCCOut.value2) << 7) + | static_cast (event.midiCCOut.value)), + event.sampleOffset); + } + else if (event.midiCCOut.controlNumber == Vst::kAfterTouch) + { + midiBuffer.addEvent (MidiMessage::channelPressureChange (channel, static_cast (event.midiCCOut.value)), + event.sampleOffset); + } + else if (event.midiCCOut.controlNumber == Vst::kCtrlPolyPressure) + { + midiBuffer.addEvent (MidiMessage::aftertouchChange (channel, + static_cast (event.midiCCOut.value), + static_cast (event.midiCCOut.value2)), + event.sampleOffset); + } + + break; + } + + default: + break; + } +} + +//============================================================================== +class HostAttributeList final : public Vst::IAttributeList +{ +public: + HostAttributeList() + { + FUNKNOWN_CTOR + } + + ~HostAttributeList() noexcept { + FUNKNOWN_DTOR + } + + tresult PLUGIN_API setInt (Vst::IAttributeList::AttrID id, int64 value) override + { + if (id == nullptr) + return kInvalidArgument; + + attributes[std::string (id)] = Attribute (value); + return kResultTrue; + } + + tresult PLUGIN_API getInt (Vst::IAttributeList::AttrID id, int64& value) override + { + if (const auto* attribute = findAttribute (id, Attribute::Type::integer)) + { + value = attribute->intValue; + return kResultTrue; + } + + return kResultFalse; + } + + tresult PLUGIN_API setFloat (Vst::IAttributeList::AttrID id, double value) override + { + if (id == nullptr) + return kInvalidArgument; + + attributes[std::string (id)] = Attribute (value); + return kResultTrue; + } + + tresult PLUGIN_API getFloat (Vst::IAttributeList::AttrID id, double& value) override + { + if (const auto* attribute = findAttribute (id, Attribute::Type::floatingPoint)) + { + value = attribute->floatValue; + return kResultTrue; + } + + return kResultFalse; + } + + tresult PLUGIN_API setString (Vst::IAttributeList::AttrID id, const Vst::TChar* value) override + { + if (id == nullptr || value == nullptr) + return kInvalidArgument; + + Attribute attribute; + attribute.type = Attribute::Type::string; + + while (value[attribute.stringValue.size()] != 0) + attribute.stringValue.push_back (value[attribute.stringValue.size()]); + + attribute.stringValue.push_back (0); + attributes[std::string (id)] = std::move (attribute); + return kResultTrue; + } + + tresult PLUGIN_API getString (Vst::IAttributeList::AttrID id, Vst::TChar* value, uint32 sizeInBytes) override + { + if (value == nullptr) + return kInvalidArgument; + + if (const auto* attribute = findAttribute (id, Attribute::Type::string)) + { + const auto bytesToCopy = jmin (sizeInBytes, + static_cast (attribute->stringValue.size() * sizeof (Vst::TChar))); + std::memcpy (value, attribute->stringValue.data(), bytesToCopy); + return kResultTrue; + } + + return kResultFalse; + } + + tresult PLUGIN_API setBinary (Vst::IAttributeList::AttrID id, const void* data, uint32 sizeInBytes) override + { + if (id == nullptr || (data == nullptr && sizeInBytes > 0)) + return kInvalidArgument; + + Attribute attribute; + attribute.type = Attribute::Type::binary; + attribute.binaryValue.replaceAll (data, static_cast (sizeInBytes)); + attributes[std::string (id)] = std::move (attribute); + return kResultTrue; + } + + tresult PLUGIN_API getBinary (Vst::IAttributeList::AttrID id, const void*& data, uint32& sizeInBytes) override + { + if (const auto* attribute = findAttribute (id, Attribute::Type::binary)) + { + data = attribute->binaryValue.getData(); + sizeInBytes = static_cast (attribute->binaryValue.getSize()); + return kResultTrue; + } + + data = nullptr; + sizeInBytes = 0; + return kResultFalse; + } + + DECLARE_FUNKNOWN_METHODS + +private: + struct Attribute + { + enum class Type + { + integer, + floatingPoint, + string, + binary + }; + + Attribute() = default; + + explicit Attribute (int64 value) + : type (Type::integer) + , intValue (value) + { + } + + explicit Attribute (double value) + : type (Type::floatingPoint) + , floatValue (value) + { + } + + Type type = Type::integer; + int64 intValue = 0; + double floatValue = 0.0; + std::vector stringValue; + MemoryBlock binaryValue; + }; + + const Attribute* findAttribute (Vst::IAttributeList::AttrID id, Attribute::Type type) const + { + if (id == nullptr) + return nullptr; + + const auto iter = attributes.find (std::string (id)); + if (iter == attributes.end() || iter->second.type != type) + return nullptr; + + return std::addressof (iter->second); + } + + std::map attributes; +}; + +IMPLEMENT_FUNKNOWN_METHODS (HostAttributeList, Vst::IAttributeList, Vst::IAttributeList::iid) + +//============================================================================== +class HostMessage final : public Vst::IMessage +{ +public: + HostMessage() + { + FUNKNOWN_CTOR + } + + ~HostMessage() noexcept { + FUNKNOWN_DTOR + } + + FIDString PLUGIN_API getMessageID() override + { + return messageId.c_str(); + } + + void PLUGIN_API setMessageID (FIDString id) override + { + messageId = id != nullptr ? id : ""; + } + + Vst::IAttributeList* PLUGIN_API getAttributes() override + { + if (attributes == nullptr) + attributes = IPtr::adopt (new HostAttributeList()); + + return attributes.get(); + } + + DECLARE_FUNKNOWN_METHODS + +private: + std::string messageId; + IPtr attributes; +}; + +IMPLEMENT_FUNKNOWN_METHODS (HostMessage, Vst::IMessage, Vst::IMessage::iid) + +//============================================================================== +class HostApplication final : public Vst::IHostApplication +{ +public: + explicit HostApplication (const AudioPluginHostContext& context) + : hostName (context.hostName.isNotEmpty() ? context.hostName : "YUP") + { + FUNKNOWN_CTOR + } + + ~HostApplication() noexcept { + FUNKNOWN_DTOR + } + + tresult PLUGIN_API getName (Vst::String128 name) override + { + toString128 (hostName, name); + return kResultTrue; + } + + tresult PLUGIN_API createInstance (TUID cid, TUID iid, void** object) override + { + if (object == nullptr) + return kInvalidArgument; + + if (FUnknownPrivate::iidEqual (cid, Vst::IMessage::iid) + && FUnknownPrivate::iidEqual (iid, Vst::IMessage::iid)) + { + *object = new HostMessage(); + return kResultTrue; + } + + if (FUnknownPrivate::iidEqual (cid, Vst::IAttributeList::iid) + && FUnknownPrivate::iidEqual (iid, Vst::IAttributeList::iid)) + { + *object = new HostAttributeList(); + return kResultTrue; + } + + *object = nullptr; + return kResultFalse; + } + + DECLARE_FUNKNOWN_METHODS + +private: + String hostName; +}; + +IMPLEMENT_FUNKNOWN_METHODS (HostApplication, Vst::IHostApplication, Vst::IHostApplication::iid) + //============================================================================== // RAII wrapper around a dynamically loaded VST3 module. struct VST3Module { - ModuleHandle handle = nullptr; - using FactoryFn = IPluginFactory* (*) (); + DynamicLibrary library; + +#if YUP_MAC + using BundleEntryFn = bool (*) (CFBundleRef); + using BundleExitFn = bool (*)(); + + VST3CFUniquePtr bundle; + BundleExitFn bundleExit = nullptr; + bool bundleEntryCalled = false; +#endif + + using FactoryFn = IPluginFactory*(PLUGIN_API*) (); FactoryFn getFactory = nullptr; +#if YUP_MAC + static VST3CFUniquePtr createBundle (const File& file) + { + const auto path = file.getFullPathName(); + VST3CFUniquePtr url (CFURLCreateFromFileSystemRepresentation (kCFAllocatorDefault, + reinterpret_cast (path.toRawUTF8()), + static_cast (path.getNumBytesAsUTF8()), + true)); + + if (url == nullptr) + return {}; + + return VST3CFUniquePtr (CFBundleCreate (kCFAllocatorDefault, url.get())); + } + + static File getBundleExecutableFile (CFBundleRef bundleToUse, const File& bundleFile) + { + const auto macOSFolder = bundleFile.getChildFile ("Contents/MacOS"); + + if (bundleToUse != nullptr) + { + auto* executableValue = CFBundleGetValueForInfoDictionaryKey (bundleToUse, kCFBundleExecutableKey); + + if (executableValue != nullptr && CFGetTypeID (executableValue) == CFStringGetTypeID()) + return macOSFolder.getChildFile (String::fromCFString (static_cast (executableValue))); + } + + return macOSFolder.getChildFile (bundleFile.getFileNameWithoutExtension()); + } +#endif + static std::unique_ptr load (const File& file) { auto m = std::make_unique(); + auto libraryFile = file; #if YUP_MAC - const auto bundlePath = file.getFullPathName().toStdString(); - m->handle = loadModule (bundlePath.c_str()); -#else - m->handle = loadModule (file.getFullPathName().toRawUTF8()); + if (file.isDirectory()) + { + m->bundle = createBundle (file); + + if (m->bundle == nullptr) + return nullptr; + + libraryFile = getBundleExecutableFile (m->bundle.get(), file); + } #endif - if (m->handle == nullptr) + if (! m->library.open (libraryFile.getFullPathName())) return nullptr; +#if YUP_MAC + if (m->bundle != nullptr) + { + auto bundleEntry = reinterpret_cast (m->library.getFunction ("bundleEntry")); + m->bundleExit = reinterpret_cast (m->library.getFunction ("bundleExit")); + + if (bundleEntry == nullptr || m->bundleExit == nullptr) + return nullptr; + + if (! bundleEntry (m->bundle.get())) + return nullptr; + + m->bundleEntryCalled = true; + } +#endif + m->getFactory = reinterpret_cast ( - getFunctionAddress (m->handle, "GetPluginFactory")); + m->library.getFunction ("GetPluginFactory")); if (m->getFactory == nullptr) - { - unloadModule (m->handle); return nullptr; - } return m; } ~VST3Module() { - if (handle != nullptr) - unloadModule (handle); +#if YUP_MAC + if (bundleEntryCalled && bundleExit != nullptr) + bundleExit(); +#endif } }; @@ -87,7 +591,19 @@ struct VST3Module class HostComponentHandler : public Vst::IComponentHandler { public: - tresult PLUGIN_API beginEdit (Vst::ParamID) override { return kResultOk; } + HostComponentHandler() + { + FUNKNOWN_CTOR + } + + ~HostComponentHandler() noexcept { + FUNKNOWN_DTOR + } + + tresult PLUGIN_API beginEdit (Vst::ParamID) override + { + return kResultOk; + } tresult PLUGIN_API performEdit (Vst::ParamID, Vst::ParamValue) override { return kResultOk; } @@ -98,6 +614,246 @@ class HostComponentHandler : public Vst::IComponentHandler DECLARE_FUNKNOWN_METHODS }; +IMPLEMENT_FUNKNOWN_METHODS (HostComponentHandler, Vst::IComponentHandler, Vst::IComponentHandler::iid) + +//============================================================================== +FIDString getVST3PlatformType() +{ +#if YUP_WINDOWS + return kPlatformTypeHWND; +#elif YUP_MAC + return kPlatformTypeNSView; +#elif YUP_LINUX + return kPlatformTypeX11EmbedWindowID; +#else + return nullptr; +#endif +} + +#if YUP_MAC +void* getVST3ParentViewFromNativeHandle (void* nativeHandle) +{ + if (nativeHandle == nullptr) + return nullptr; + + id nativeObject = (__bridge id) nativeHandle; + if ([nativeObject isKindOfClass:[NSWindow class]]) + return (__bridge void*) [(NSWindow*) nativeObject contentView]; + + if ([nativeObject isKindOfClass:[NSView class]]) + return nativeHandle; + + return nullptr; +} +#endif + +class VST3Editor; + +class VST3EditorFrame final : public IPlugFrame +{ +public: + explicit VST3EditorFrame (VST3Editor& ownerToUse) + : owner (std::addressof (ownerToUse)) + { + FUNKNOWN_CTOR + } + + ~VST3EditorFrame() noexcept { + FUNKNOWN_DTOR + } + + tresult PLUGIN_API resizeView (IPlugView* view, ViewRect* newSize) override; + + void clearOwner() noexcept { owner = nullptr; } + + DECLARE_FUNKNOWN_METHODS + +private: + VST3Editor* owner = nullptr; +}; + +IMPLEMENT_FUNKNOWN_METHODS (VST3EditorFrame, IPlugFrame, IPlugFrame::iid) + +class VST3Editor final : public AudioProcessorEditor +{ +public: + static std::unique_ptr create (Vst::IEditController* controller) + { + if (controller == nullptr) + return nullptr; + + IPlugView* rawView = controller->createView (Vst::ViewType::kEditor); + if (rawView == nullptr) + return nullptr; + + IPtr view = IPtr::adopt (rawView); + const auto* platformType = getVST3PlatformType(); + + if (platformType == nullptr || view->isPlatformTypeSupported (platformType) != kResultTrue) + return nullptr; + + return std::unique_ptr (new VST3Editor (std::move (view))); + } + + ~VST3Editor() override + { + detachPlugView(); + + if (frame != nullptr) + { + static_cast (frame.get())->clearOwner(); + frame = nullptr; + } + + plugView = nullptr; + } + + bool isResizable() const override + { + return plugView != nullptr && plugView->canResize() == kResultTrue; + } + + Size getPreferredSize() const override { return preferredSize; } + + void paint (Graphics& g) override + { + g.setFillColor (Color (0xff101417)); + g.fillAll(); + } + + void resized() override + { + resizePlugViewToBounds(); + } + + void attachedToNative() override + { + attachPlugView(); + } + + void detachedFromNative() override + { + detachPlugView(); + } + + tresult handleResizeRequest (IPlugView* view, ViewRect* newSize) + { + if (plugView == nullptr || view != plugView.get() || newSize == nullptr) + return kInvalidArgument; + + preferredSize = { + jmax (1, static_cast (newSize->getWidth())), + jmax (1, static_cast (newSize->getHeight())) + }; + + newSize->right = newSize->left + preferredSize.getWidth(); + newSize->bottom = newSize->top + preferredSize.getHeight(); + + if (auto* topLevel = getTopLevelComponent()) + topLevel->setSize (preferredSize.to()); + else + setSize (preferredSize.to()); + + plugView->onSize (newSize); + return kResultTrue; + } + +private: + explicit VST3Editor (IPtr view) + : plugView (std::move (view)) + , frame (IPtr::adopt (new VST3EditorFrame (*this))) + { + ViewRect size; + if (plugView->getSize (std::addressof (size)) == kResultTrue + && size.getWidth() > 0 + && size.getHeight() > 0) + { + preferredSize = { + jmax (320, static_cast (size.getWidth())), + jmax (240, static_cast (size.getHeight())) + }; + } + + setSize (preferredSize.to()); + } + + void attachPlugView() + { + if (plugView == nullptr || attached) + return; + + auto* nativeComponent = getNativeComponent(); + if (nativeComponent == nullptr) + return; + + void* parentHandle = nativeComponent->getNativeHandle(); + +#if YUP_MAC + parentHandle = getVST3ParentViewFromNativeHandle (parentHandle); +#endif + + const auto* platformType = getVST3PlatformType(); + if (parentHandle == nullptr || platformType == nullptr) + return; + + if (plugView->setFrame (frame.get()) != kResultTrue) + return; + + if (plugView->attached (parentHandle, platformType) != kResultTrue) + { + plugView->setFrame (nullptr); + return; + } + + attached = true; + resizePlugViewToBounds(); + } + + void detachPlugView() + { + if (plugView == nullptr) + return; + + if (attached) + { + plugView->removed(); + attached = false; + } + + plugView->setFrame (nullptr); + } + + void resizePlugViewToBounds() + { + if (plugView == nullptr || ! attached) + return; + + const auto bounds = getBoundsRelativeToTopLevelComponent(); + ViewRect size; + size.left = static_cast (bounds.getX()); + size.top = static_cast (bounds.getY()); + size.right = size.left + jmax (1, static_cast (bounds.getWidth())); + size.bottom = size.top + jmax (1, static_cast (bounds.getHeight())); + + plugView->onSize (std::addressof (size)); + } + + IPtr plugView; + IPtr frame; + Size preferredSize { 640, 480 }; + bool attached = false; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VST3Editor) +}; + +tresult PLUGIN_API VST3EditorFrame::resizeView (IPlugView* view, ViewRect* newSize) +{ + if (owner == nullptr) + return kResultFalse; + + return owner->handleResizeRequest (view, newSize); +} + } // namespace //============================================================================== @@ -108,27 +864,47 @@ class VST3Instance : public AudioPluginInstance VST3Instance (const AudioPluginDescription& desc, const AudioPluginHostContext& context, std::unique_ptr module, + IPtr hostApplication, + IPtr componentHandler, IPtr component, IPtr processor, - IPtr controller) + IPtr controller, + bool controllerWasInitialized) : AudioPluginInstance (desc, buildBusLayout (component.get())) , hostContext (context) , vst3Module (std::move (module)) + , vst3HostApplication (std::move (hostApplication)) + , vst3ComponentHandler (std::move (componentHandler)) , vst3Component (std::move (component)) , vst3Processor (std::move (processor)) , vst3Controller (std::move (controller)) + , vst3ControllerInitialized (controllerWasInitialized) { + connectComponentAndController(); buildParameterList(); + setNonRealtime (context.isNonRealtime); } ~VST3Instance() override { + disconnectComponentAndController(); releaseResources(); + + if (vst3Controller != nullptr) + { + vst3Controller->setComponentHandler (nullptr); + + if (vst3ControllerInitialized) + vst3Controller->terminate(); + } + vst3Processor = nullptr; vst3Controller = nullptr; vst3Component->terminate(); vst3Component = nullptr; + vst3ComponentHandler = nullptr; + vst3HostApplication = nullptr; } //============================================================================== @@ -136,7 +912,7 @@ class VST3Instance : public AudioPluginInstance void prepareToPlay (float sampleRate, int maxBlockSize) override { Vst::ProcessSetup setup; - setup.processMode = Vst::kRealtime; + setup.processMode = isNonRealtime() ? Vst::kOffline : Vst::kRealtime; setProcessingPrecision (hostContext.preferDoublePrecision && supportsDoublePrecisionProcessing() ? ProcessingPrecision::doublePrecision : ProcessingPrecision::singlePrecision); @@ -145,22 +921,32 @@ class VST3Instance : public AudioPluginInstance setup.maxSamplesPerBlock = maxBlockSize; setup.sampleRate = sampleRate; + const int numInputs = vst3Component->getBusCount (Vst::kAudio, Vst::kInput); + const int numOutputs = vst3Component->getBusCount (Vst::kAudio, Vst::kOutput); + if (isUsingDoublePrecision()) { - const int numChannels = jmax (1, pluginDescription.numInputChannels, pluginDescription.numOutputChannels); + const int numChannels = jmax (1, numInputs, numOutputs); doublePrecisionBuffer.setSize (numChannels, maxBlockSize, false, true, false); } vst3Processor->setupProcessing (setup); + processingPrepared = true; - const int numInputs = vst3Component->getBusCount (Vst::kAudio, Vst::kInput); for (int i = 0; i < numInputs; ++i) vst3Component->activateBus (Vst::kAudio, Vst::kInput, i, true); - const int numOutputs = vst3Component->getBusCount (Vst::kAudio, Vst::kOutput); for (int i = 0; i < numOutputs; ++i) vst3Component->activateBus (Vst::kAudio, Vst::kOutput, i, true); + const int numEventInputs = vst3Component->getBusCount (Vst::kEvent, Vst::kInput); + for (int i = 0; i < numEventInputs; ++i) + vst3Component->activateBus (Vst::kEvent, Vst::kInput, i, true); + + const int numEventOutputs = vst3Component->getBusCount (Vst::kEvent, Vst::kOutput); + for (int i = 0; i < numEventOutputs; ++i) + vst3Component->activateBus (Vst::kEvent, Vst::kOutput, i, true); + vst3Component->setActive (true); vst3Processor->setProcessing (true); } @@ -172,12 +958,20 @@ class VST3Instance : public AudioPluginInstance if (vst3Component != nullptr) vst3Component->setActive (false); + + processingPrepared = false; } void processBlock (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) override { ScopedNoDenormals noDenormals; + if (isBypassed()) + { + processBlockBypassed (audioBuffer, midiBuffer); + return; + } + if (isUsingDoublePrecision()) { doublePrecisionBuffer.makeCopyOf (audioBuffer, true); @@ -200,32 +994,43 @@ class VST3Instance : public AudioPluginInstance Vst::ProcessData data {}; prepareProcessData (data, audioBuffer.getNumSamples(), Vst::kSample32); + prepareMidiInputEvents (midiBuffer); // Input busses Vst::AudioBusBuffers inputBus {}; - inputBus.numChannels = audioBuffer.getNumChannels(); - inputBus.channelBuffers32 = const_cast (audioBuffer.getArrayOfReadPointers()); - data.inputs = &inputBus; - data.numInputs = 1; + if (vst3Component->getBusCount (Vst::kAudio, Vst::kInput) > 0 && audioBuffer.getNumChannels() > 0) + { + inputBus.numChannels = audioBuffer.getNumChannels(); + inputBus.channelBuffers32 = const_cast (audioBuffer.getArrayOfReadPointers()); + data.inputs = &inputBus; + data.numInputs = 1; + } // Output busses Vst::AudioBusBuffers outputBus {}; - outputBus.numChannels = audioBuffer.getNumChannels(); - outputBus.channelBuffers32 = const_cast (audioBuffer.getArrayOfWritePointers()); - data.outputs = &outputBus; - data.numOutputs = 1; - - ignoreUnused (midiBuffer); - - // TODO: map MidiBuffer to Vst::EventList when MIDI support is added in a later task + if (vst3Component->getBusCount (Vst::kAudio, Vst::kOutput) > 0 && audioBuffer.getNumChannels() > 0) + { + outputBus.numChannels = audioBuffer.getNumChannels(); + outputBus.channelBuffers32 = const_cast (audioBuffer.getArrayOfWritePointers()); + data.outputs = &outputBus; + data.numOutputs = 1; + } vst3Processor->process (data); + + collectOutputEvents (midiBuffer); } void processBlock (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) override { ScopedNoDenormals noDenormals; + if (isBypassed()) + { + processBlockBypassed (audioBuffer, midiBuffer); + return; + } + if (! isUsingDoublePrecision()) { jassertfalse; @@ -236,24 +1041,29 @@ class VST3Instance : public AudioPluginInstance Vst::ProcessData data {}; prepareProcessData (data, audioBuffer.getNumSamples(), Vst::kSample64); + prepareMidiInputEvents (midiBuffer); Vst::AudioBusBuffers inputBus {}; - inputBus.numChannels = audioBuffer.getNumChannels(); - inputBus.channelBuffers64 = const_cast (audioBuffer.getArrayOfReadPointers()); - data.inputs = &inputBus; - data.numInputs = 1; + if (vst3Component->getBusCount (Vst::kAudio, Vst::kInput) > 0 && audioBuffer.getNumChannels() > 0) + { + inputBus.numChannels = audioBuffer.getNumChannels(); + inputBus.channelBuffers64 = const_cast (audioBuffer.getArrayOfReadPointers()); + data.inputs = &inputBus; + data.numInputs = 1; + } Vst::AudioBusBuffers outputBus {}; - outputBus.numChannels = audioBuffer.getNumChannels(); - outputBus.channelBuffers64 = const_cast (audioBuffer.getArrayOfWritePointers()); - data.outputs = &outputBus; - data.numOutputs = 1; - - ignoreUnused (midiBuffer); - - // TODO: map MidiBuffer to Vst::EventList when MIDI support is added in a later task + if (vst3Component->getBusCount (Vst::kAudio, Vst::kOutput) > 0 && audioBuffer.getNumChannels() > 0) + { + outputBus.numChannels = audioBuffer.getNumChannels(); + outputBus.channelBuffers64 = const_cast (audioBuffer.getArrayOfWritePointers()); + data.outputs = &outputBus; + data.numOutputs = 1; + } vst3Processor->process (data); + + collectOutputEvents (midiBuffer); } bool supportsDoublePrecisionProcessing() const override @@ -327,7 +1137,18 @@ class VST3Instance : public AudioPluginInstance //============================================================================== - bool hasEditor() const override { return false; } + bool hasEditor() const override + { + return VST3Editor::create (vst3Controller.get()) != nullptr; + } + + AudioProcessorEditor* createEditor() override + { + if (auto editor = VST3Editor::create (vst3Controller.get())) + return editor.release(); + + return nullptr; + } //============================================================================== @@ -367,8 +1188,8 @@ class VST3Instance : public AudioPluginInstance IPtr component = IPtr::adopt (rawComponent); - IPtr host; - if (component->initialize (host) != kResultOk) + auto host = IPtr::adopt (new HostApplication (context)); + if (component->initialize (host.get()) != kResultOk) continue; Vst::IAudioProcessor* rawProcessor = nullptr; @@ -383,7 +1204,40 @@ class VST3Instance : public AudioPluginInstance reinterpret_cast (&rawController)); IPtr controller = IPtr::adopt (rawController); - return std::make_unique (desc, context, std::move (mod), std::move (component), std::move (processor), std::move (controller)); + bool controllerInitialized = false; + if (controller == nullptr) + { + TUID controllerClassId {}; + if (component->getControllerClassId (controllerClassId) == kResultTrue) + { + if (factory->createInstance (controllerClassId, + Vst::IEditController::iid, + reinterpret_cast (&rawController)) + == kResultOk) + { + controller = IPtr::adopt (rawController); + + if (controller->initialize (host.get()) != kResultOk) + controller = nullptr; + else + controllerInitialized = true; + } + } + } + + auto componentHandler = IPtr::adopt (new HostComponentHandler()); + if (controller != nullptr) + controller->setComponentHandler (componentHandler.get()); + + return std::make_unique (desc, + context, + std::move (mod), + std::move (host), + std::move (componentHandler), + std::move (component), + std::move (processor), + std::move (controller), + controllerInitialized); } return nullptr; @@ -410,6 +1264,22 @@ class VST3Instance : public AudioPluginInstance outputs.emplace_back (toString (info.name), AudioBus::Type::Audio, AudioBus::Direction::Output, static_cast (info.channelCount)); } + const int numMidiInputs = component->getBusCount (Vst::kEvent, Vst::kInput); + for (int i = 0; i < numMidiInputs; ++i) + { + Vst::BusInfo info; + component->getBusInfo (Vst::kEvent, Vst::kInput, i, info); + inputs.emplace_back (toString (info.name), AudioBus::Type::MIDI, AudioBus::Direction::Input, 1); + } + + const int numMidiOutputs = component->getBusCount (Vst::kEvent, Vst::kOutput); + for (int i = 0; i < numMidiOutputs; ++i) + { + Vst::BusInfo info; + component->getBusInfo (Vst::kEvent, Vst::kOutput, i, info); + outputs.emplace_back (toString (info.name), AudioBus::Type::MIDI, AudioBus::Direction::Output, 1); + } + return AudioBusLayout (std::move (inputs), std::move (outputs)); } @@ -448,19 +1318,95 @@ class VST3Instance : public AudioPluginInstance AudioPluginHostContext hostContext; std::unique_ptr vst3Module; + IPtr vst3HostApplication; + IPtr vst3ComponentHandler; IPtr vst3Component; IPtr vst3Processor; IPtr vst3Controller; Vst::ProcessContext vst3ProcessContext {}; Vst::ParameterChanges inputParameterChanges; + Vst::EventList inputEvents; + Vst::EventList outputEvents; AudioBuffer doublePrecisionBuffer; std::vector vst3ParameterIds; int currentPreset = 0; int numPresets = 0; + bool processingPrepared = false; + bool vst3ControllerInitialized = false; + bool vst3ComponentsConnected = false; + + bool connectComponentAndController() + { + if (vst3Component == nullptr || vst3Controller == nullptr) + return false; + + Vst::IConnectionPoint* rawComponentConnection = nullptr; + if (vst3Component->queryInterface (Vst::IConnectionPoint::iid, + reinterpret_cast (&rawComponentConnection)) + != kResultOk) + { + return false; + } + + Vst::IConnectionPoint* rawControllerConnection = nullptr; + if (vst3Controller->queryInterface (Vst::IConnectionPoint::iid, + reinterpret_cast (&rawControllerConnection)) + != kResultOk) + { + rawComponentConnection->release(); + return false; + } + + auto componentConnection = IPtr::adopt (rawComponentConnection); + auto controllerConnection = IPtr::adopt (rawControllerConnection); + + if (componentConnection->connect (controllerConnection.get()) != kResultTrue) + return false; + + if (controllerConnection->connect (componentConnection.get()) != kResultTrue) + { + componentConnection->disconnect (controllerConnection.get()); + return false; + } + + vst3ComponentsConnected = true; + return true; + } + + void disconnectComponentAndController() + { + if (! vst3ComponentsConnected || vst3Component == nullptr || vst3Controller == nullptr) + return; + + Vst::IConnectionPoint* rawComponentConnection = nullptr; + Vst::IConnectionPoint* rawControllerConnection = nullptr; + + if (vst3Component->queryInterface (Vst::IConnectionPoint::iid, + reinterpret_cast (&rawComponentConnection)) + != kResultOk) + { + return; + } + + if (vst3Controller->queryInterface (Vst::IConnectionPoint::iid, + reinterpret_cast (&rawControllerConnection)) + != kResultOk) + { + rawComponentConnection->release(); + return; + } + + auto componentConnection = IPtr::adopt (rawComponentConnection); + auto controllerConnection = IPtr::adopt (rawControllerConnection); + + componentConnection->disconnect (controllerConnection.get()); + controllerConnection->disconnect (componentConnection.get()); + vst3ComponentsConnected = false; + } void prepareProcessData (Vst::ProcessData& data, int numSamples, int32 symbolicSampleSize) { - data.processMode = Vst::kRealtime; + data.processMode = isNonRealtime() ? Vst::kOffline : Vst::kRealtime; data.symbolicSampleSize = symbolicSampleSize; data.numSamples = numSamples; @@ -481,6 +1427,11 @@ class VST3Instance : public AudioPluginInstance } data.inputParameterChanges = &inputParameterChanges; + data.inputEvents = &inputEvents; + data.outputEvents = &outputEvents; + + inputEvents.clear(); + outputEvents.clear(); if (hostContext.playHead == nullptr) return; @@ -502,6 +1453,51 @@ class VST3Instance : public AudioPluginInstance data.processContext = &vst3ProcessContext; } + + void prepareMidiInputEvents (const MidiBuffer& midiBuffer) + { + int count = 0; + + for (const auto& metadata : midiBuffer) + { + ignoreUnused (metadata); + ++count; + } + + inputEvents.setMaxSize (jmax (64, count)); + inputEvents.clear(); + + for (const auto& metadata : midiBuffer) + addMidiMessageToVST3Events (inputEvents, metadata); + + outputEvents.setMaxSize (jmax (64, count * 2)); + outputEvents.clear(); + } + + void collectOutputEvents (MidiBuffer& midiBuffer) + { + midiBuffer.clear(); + + for (int32 i = 0; i < outputEvents.getEventCount(); ++i) + { + Vst::Event event {}; + if (outputEvents.getEvent (i, event) == kResultOk) + addVST3EventToMidiBuffer (event, midiBuffer); + } + } + + void nonRealtimeStateChanged() override + { + if (! processingPrepared || vst3Processor == nullptr || getSampleRate() <= 0.0f || getSamplesPerBlock() <= 0) + return; + + Vst::ProcessSetup setup; + setup.processMode = isNonRealtime() ? Vst::kOffline : Vst::kRealtime; + setup.symbolicSampleSize = isUsingDoublePrecision() ? Vst::kSample64 : Vst::kSample32; + setup.maxSamplesPerBlock = getSamplesPerBlock(); + setup.sampleRate = getSampleRate(); + vst3Processor->setupProcessing (setup); + } }; //============================================================================== @@ -519,6 +1515,11 @@ String VST3Format::getFormatName() const return "VST3"; } +StringArray VST3Format::getFileExtensions() const +{ + return { ".vst3" }; +} + FileSearchPath VST3Format::getDefaultSearchPaths() const { FileSearchPath paths; @@ -599,33 +1600,16 @@ ResultValue> VST3Format::scanFile (const Fil desc.identifier = classIDToString (info2.cid); - // Briefly instantiate the component to collect channel counts - Vst::IComponent* rawComponent = nullptr; - if (factory->createInstance (info2.cid, Vst::IComponent::iid, reinterpret_cast (&rawComponent)) == kResultOk) + if (desc.isInstrument) { - IPtr component = IPtr::adopt (rawComponent); - IPtr host; - - if (component->initialize (host) == kResultOk) - { - const int numInputs = component->getBusCount (Vst::kAudio, Vst::kInput); - for (int b = 0; b < numInputs; ++b) - { - Vst::BusInfo busInfo; - if (component->getBusInfo (Vst::kAudio, Vst::kInput, b, busInfo) == kResultOk) - desc.numInputChannels += static_cast (busInfo.channelCount); - } - - const int numOutputs = component->getBusCount (Vst::kAudio, Vst::kOutput); - for (int b = 0; b < numOutputs; ++b) - { - Vst::BusInfo busInfo; - if (component->getBusInfo (Vst::kAudio, Vst::kOutput, b, busInfo) == kResultOk) - desc.numOutputChannels += static_cast (busInfo.channelCount); - } - - component->terminate(); - } + desc.numInputChannels = 0; + desc.numOutputChannels = 2; + desc.numMidiInputPorts = 1; + } + else + { + desc.numInputChannels = 2; + desc.numOutputChannels = 2; } results.push_back (std::move (desc)); diff --git a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.h b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.h index 90d59d8ef..c1613809e 100644 --- a/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.h +++ b/modules/yup_audio_plugin_host/native/yup_AudioPluginInstance_VST3.h @@ -38,6 +38,7 @@ class VST3Format : public AudioPluginFormat AudioPluginFormatType getFormatType() const override; String getFormatName() const override; + StringArray getFileExtensions() const override; FileSearchPath getDefaultSearchPaths() const override; diff --git a/modules/yup_audio_plugin_host/yup_audio_plugin_host.cpp b/modules/yup_audio_plugin_host/yup_audio_plugin_host.cpp index 5be88a873..7c00b9eb8 100644 --- a/modules/yup_audio_plugin_host/yup_audio_plugin_host.cpp +++ b/modules/yup_audio_plugin_host/yup_audio_plugin_host.cpp @@ -25,6 +25,13 @@ #include "yup_audio_plugin_host.h" +#include +#include +#include +#include +#include +#include + //============================================================================== #include "host/yup_AudioPluginScanner.cpp" #include "host/yup_AudioPluginInstance.cpp" @@ -38,24 +45,18 @@ #include #include #include +#include #include #include #include +#include #include +#include #include -#if YUP_WINDOWS -#include -using ModuleHandle = HMODULE; -#define loadModule(path) LoadLibraryA (path) -#define getFunctionAddress GetProcAddress -#define unloadModule FreeLibrary -#elif YUP_MAC || YUP_LINUX -#include -using ModuleHandle = void*; -#define loadModule(path) dlopen (path, RTLD_LAZY | RTLD_LOCAL) -#define getFunctionAddress dlsym -#define unloadModule dlclose +#if YUP_MAC +#import +#include #endif #include "native/yup_AudioPluginInstance_VST3.cpp" @@ -88,6 +89,7 @@ using CLAPModuleHandle = void*; #import #import #import +#import #include "native/yup_AudioPluginInstance_AUv2.mm" #endif diff --git a/modules/yup_audio_processors/processors/yup_AudioProcessor.cpp b/modules/yup_audio_processors/processors/yup_AudioProcessor.cpp index 05ff6d264..259e758ca 100644 --- a/modules/yup_audio_processors/processors/yup_AudioProcessor.cpp +++ b/modules/yup_audio_processors/processors/yup_AudioProcessor.cpp @@ -55,12 +55,24 @@ void AudioProcessor::addParameter (AudioParameter::Ptr parameter) int AudioProcessor::getNumAudioOutputs() const { - return static_cast (busLayout.getOutputBuses().size()); + int count = 0; + + for (const auto& bus : busLayout.getOutputBuses()) + if (bus.getType() == AudioBus::Type::Audio) + ++count; + + return count; } int AudioProcessor::getNumAudioInputs() const { - return static_cast (busLayout.getInputBuses().size()); + int count = 0; + + for (const auto& bus : busLayout.getInputBuses()) + if (bus.getType() == AudioBus::Type::Audio) + ++count; + + return count; } //============================================================================== @@ -100,6 +112,18 @@ void AudioProcessor::setProcessingPrecision (ProcessingPrecision precision) //============================================================================== +void AudioProcessor::processBlockBypassed (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) +{ + ignoreUnused (audioBuffer, midiBuffer); +} + +void AudioProcessor::processBlockBypassed (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) +{ + ignoreUnused (audioBuffer, midiBuffer); +} + +//============================================================================== + void AudioProcessor::setPlaybackConfiguration (float sampleRate, int samplesPerBlock) { releaseResources(); diff --git a/modules/yup_audio_processors/processors/yup_AudioProcessor.h b/modules/yup_audio_processors/processors/yup_AudioProcessor.h index 84de8329f..d560a9903 100644 --- a/modules/yup_audio_processors/processors/yup_AudioProcessor.h +++ b/modules/yup_audio_processors/processors/yup_AudioProcessor.h @@ -101,6 +101,26 @@ class YUP_API AudioProcessor */ virtual void processBlock (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer) {} + /** + Processes a block while the processor is bypassed. + + The default implementation leaves audio and MIDI unchanged. + + @param audioBuffer The audio buffer to process. + @param midiBuffer The MIDI buffer to process. + */ + virtual void processBlockBypassed (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer); + + /** + Processes a block while the processor is bypassed. + + The default implementation leaves audio and MIDI unchanged. + + @param audioBuffer The audio buffer to process. + @param midiBuffer The MIDI buffer to process. + */ + virtual void processBlockBypassed (AudioBuffer& audioBuffer, MidiBuffer& midiBuffer); + /** Flushes the processor. */ virtual void flush() {} diff --git a/modules/yup_dsp/windowing/yup_WindowFunctions.h b/modules/yup_dsp/windowing/yup_WindowFunctions.h index 2e9ddadfa..e9d823991 100644 --- a/modules/yup_dsp/windowing/yup_WindowFunctions.h +++ b/modules/yup_dsp/windowing/yup_WindowFunctions.h @@ -295,6 +295,34 @@ class WindowFunctions return FloatType (1); } + /** Evaluates a Tukey window at a continuous normalised phase value. + + Unlike the discrete tukey(n, N, alpha) variant which operates on integer sample + indices, this overload takes a continuous phase value in [0, 1] directly. It is + useful for per-sample window evaluation in oscillators and modulators where no + fixed buffer length exists. + + @param phi Normalised phase in [0, 1] + @param alpha Taper ratio in (0, 1]: 1.0 produces a full Hann shape, values + near 0 approach a rectangular window + @returns Window amplitude in [0, 1] + */ + static FloatType tukeyFromPhase (FloatType phi, FloatType alpha = FloatType (0.5)) noexcept + { + const FloatType halfAlpha = alpha / FloatType (2); + + if (halfAlpha <= FloatType (0)) + return FloatType (1); + + if (phi < halfAlpha) + return FloatType (0.5) * (FloatType (1) + std::cos (MathConstants::pi * (phi / halfAlpha - FloatType (1)))); + + if (phi > FloatType (1) - halfAlpha) + return FloatType (0.5) * (FloatType (1) + std::cos (MathConstants::pi * ((phi - FloatType (1) + halfAlpha) / halfAlpha))); + + return FloatType (1); + } + static FloatType bartlett (int n, int N) noexcept { return FloatType (1) - FloatType (2) * std::abs (n - (N - 1) / FloatType (2)) / (N - 1); diff --git a/tests/yup_audio_graph/yup_AudioGraphProcessor.cpp b/tests/yup_audio_graph/yup_AudioGraphProcessor.cpp index 1276cdfb2..f73116cc3 100644 --- a/tests/yup_audio_graph/yup_AudioGraphProcessor.cpp +++ b/tests/yup_audio_graph/yup_AudioGraphProcessor.cpp @@ -1045,6 +1045,7 @@ TEST (AudioGraphProcessorTests, MissingCommitKeepsPreviousPlan) EXPECT_FLOAT_EQ (0.5f, audio.getReadPointer (0)[0]); } +#if ! defined(YUP_WASM) TEST (AudioGraphProcessorTests, CommitKeepsDirtyWhenModelChangesDuringCompilation) { AudioGraphProcessor graph; @@ -1087,6 +1088,7 @@ TEST (AudioGraphProcessorTests, CommitKeepsDirtyWhenModelChangesDuringCompilatio EXPECT_TRUE (graph.commitChanges().wasOk()); EXPECT_FALSE (graph.hasUncommittedChanges()); } +#endif TEST (AudioGraphProcessorTests, SaveAndLoadRestoresConnectionsAndNodeState) { @@ -1898,6 +1900,7 @@ TEST (AudioGraphProcessorTests, SwitchingWorkerThreadsBetweenBlocksPreservesOutp } } +#if ! defined(YUP_WASM) TEST (AudioGraphProcessorTests, ConcurrentCommitsDoNotInvalidateAudioThreadPlan) { AudioGraphProcessor graph; @@ -1975,6 +1978,7 @@ TEST (AudioGraphProcessorTests, ConcurrentCommitsDoNotInvalidateAudioThreadPlan) EXPECT_EQ (0, invalidBlocks.load()); EXPECT_GT (processedBlocks.load(), 0); } +#endif TEST (AudioGraphProcessorTests, MidiCompensationCanSpillIntoNextBlock) { diff --git a/tests/yup_audio_plugin_host/yup_AudioPluginDescription.cpp b/tests/yup_audio_plugin_host/yup_AudioPluginDescription.cpp index 9f218a75f..1203af3ec 100644 --- a/tests/yup_audio_plugin_host/yup_AudioPluginDescription.cpp +++ b/tests/yup_audio_plugin_host/yup_AudioPluginDescription.cpp @@ -16,6 +16,13 @@ TEST (AudioPluginDescriptionTests, IsInstrumentDefaultsFalse) EXPECT_FALSE (desc.isInstrument); } +TEST (AudioPluginDescriptionTests, MidiPortCountsDefaultToZero) +{ + AudioPluginDescription desc; + EXPECT_EQ (0, desc.numMidiInputPorts); + EXPECT_EQ (0, desc.numMidiOutputPorts); +} + TEST (AudioPluginDescriptionTests, EqualityMatchesAllFields) { AudioPluginDescription a; @@ -41,3 +48,9 @@ TEST (AudioPluginHostContextTests, DefaultMaxBlockSize) AudioPluginHostContext ctx; EXPECT_EQ (512, ctx.maxBlockSize); } + +TEST (AudioPluginHostContextTests, DefaultsToRealtimeRendering) +{ + AudioPluginHostContext ctx; + EXPECT_FALSE (ctx.isNonRealtime); +} diff --git a/tests/yup_audio_plugin_host/yup_AudioPluginInstance.cpp b/tests/yup_audio_plugin_host/yup_AudioPluginInstance.cpp index e8574c504..07799367d 100644 --- a/tests/yup_audio_plugin_host/yup_AudioPluginInstance.cpp +++ b/tests/yup_audio_plugin_host/yup_AudioPluginInstance.cpp @@ -78,6 +78,116 @@ class FakePluginInstance : public AudioPluginInstance } }; +class CountOnlyPluginInstance : public AudioPluginInstance +{ +public: + CountOnlyPluginInstance() + : AudioPluginInstance (makeDescription(), + AudioBusLayout ({ AudioBus ("MIDI Input", AudioBus::Type::MIDI, AudioBus::Direction::Input, 1) }, + { AudioBus ("MIDI Output", AudioBus::Type::MIDI, AudioBus::Direction::Output, 1) })) + { + } + + void prepareToPlay (float, int) override {} + + void releaseResources() override {} + + void processBlock (AudioBuffer&, MidiBuffer&) override {} + + int getCurrentPreset() const noexcept override { return 0; } + + void setCurrentPreset (int) noexcept override {} + + int getNumPresets() const override { return 0; } + + String getPresetName (int) const override { return {}; } + + void setPresetName (int, StringRef) override {} + + Result loadStateFromMemory (const MemoryBlock&) override { return Result::ok(); } + + Result saveStateIntoMemory (MemoryBlock&) override { return Result::ok(); } + + bool hasEditor() const override { return false; } + +private: + static AudioPluginDescription makeDescription() + { + AudioPluginDescription d; + d.name = "CountOnlyPlugin"; + d.identifier = "count.only"; + d.formatType = AudioPluginFormatType::clap; + return d; + } +}; + +class BypassPluginInstance : public AudioPluginInstance +{ +public: + BypassPluginInstance (int numInputChannels, int numOutputChannels) + : AudioPluginInstance (makeDescription (numInputChannels, numOutputChannels), + makeLayout (numInputChannels, numOutputChannels)) + { + } + + void prepareToPlay (float, int) override {} + + void releaseResources() override {} + + void processBlock (AudioBuffer& audio, MidiBuffer& midi) override + { + if (isBypassed()) + { + processBlockBypassed (audio, midi); + return; + } + + audio.clear(); + } + + int getCurrentPreset() const noexcept override { return 0; } + + void setCurrentPreset (int) noexcept override {} + + int getNumPresets() const override { return 0; } + + String getPresetName (int) const override { return {}; } + + void setPresetName (int, StringRef) override {} + + Result loadStateFromMemory (const MemoryBlock&) override { return Result::ok(); } + + Result saveStateIntoMemory (MemoryBlock&) override { return Result::ok(); } + + bool hasEditor() const override { return false; } + +private: + static AudioPluginDescription makeDescription (int numInputChannels, int numOutputChannels) + { + AudioPluginDescription d; + d.name = "BypassPlugin"; + d.identifier = "bypass.plugin"; + d.formatType = AudioPluginFormatType::vst3; + d.numInputChannels = numInputChannels; + d.numOutputChannels = numOutputChannels; + return d; + } + + static AudioBusLayout makeLayout (int numInputChannels, int numOutputChannels) + { + std::vector inputs; + std::vector outputs; + + if (numInputChannels > 0) + inputs.emplace_back ("Input", AudioBus::Type::Audio, AudioBus::Direction::Input, numInputChannels); + + if (numOutputChannels > 0) + outputs.emplace_back ("Output", AudioBus::Type::Audio, AudioBus::Direction::Output, numOutputChannels); + + return AudioBusLayout (std::move (inputs), std::move (outputs)); + } +}; + } // namespace class AudioPluginInstanceTests : public ::testing::Test @@ -169,3 +279,73 @@ TEST_F (AudioPluginInstanceTests, BusLayoutMatchesConstructorArg) EXPECT_EQ (1, instance.getNumAudioInputs()); EXPECT_EQ (1, instance.getNumAudioOutputs()); } + +TEST_F (AudioPluginInstanceTests, DefaultsToRealtimeAndNotBypassed) +{ + EXPECT_FALSE (instance.isNonRealtime()); + EXPECT_FALSE (instance.isBypassed()); +} + +TEST_F (AudioPluginInstanceTests, RenderModeAndBypassCanBeSetByHost) +{ + instance.setNonRealtime (true); + instance.setBypassed (true); + + EXPECT_TRUE (instance.isNonRealtime()); + EXPECT_TRUE (instance.isBypassed()); +} + +TEST (AudioPluginInstanceBusLayoutTests, AudioInputAndOutputCountsIgnoreMidiBuses) +{ + CountOnlyPluginInstance instanceWithMidiBuses; + + EXPECT_EQ (0, instanceWithMidiBuses.getNumAudioInputs()); + EXPECT_EQ (0, instanceWithMidiBuses.getNumAudioOutputs()); +} + +TEST (AudioPluginInstanceBypassTests, CopiesMatchingInputChannelsAndClearsExtraOutputs) +{ + BypassPluginInstance bypassInstance (1, 2); + AudioBuffer audio (2, 8); + MidiBuffer midi; + + audio.setSample (0, 0, 0.75f); + audio.setSample (1, 0, 0.5f); + + bypassInstance.setBypassed (true); + bypassInstance.processBlock (audio, midi); + + EXPECT_FLOAT_EQ (0.75f, audio.getSample (0, 0)); + EXPECT_FLOAT_EQ (0.0f, audio.getSample (1, 0)); +} + +TEST (AudioPluginInstanceBypassTests, ClearsInstrumentOutputsWithoutInputs) +{ + BypassPluginInstance bypassInstance (0, 2); + AudioBuffer audio (2, 8); + MidiBuffer midi; + + audio.setSample (0, 0, 0.75f); + audio.setSample (1, 0, 0.5f); + + bypassInstance.setBypassed (true); + bypassInstance.processBlock (audio, midi); + + EXPECT_FLOAT_EQ (0.0f, audio.getSample (0, 0)); + EXPECT_FLOAT_EQ (0.0f, audio.getSample (1, 0)); +} + +TEST (AudioPluginInstanceBypassTests, SupportsDoublePrecisionBypass) +{ + BypassPluginInstance bypassInstance (1, 2); + AudioBuffer audio (2, 8); + MidiBuffer midi; + + audio.setSample (0, 0, 0.75); + audio.setSample (1, 0, 0.5); + + bypassInstance.processBlockBypassed (audio, midi); + + EXPECT_DOUBLE_EQ (0.75, audio.getSample (0, 0)); + EXPECT_DOUBLE_EQ (0.0, audio.getSample (1, 0)); +} diff --git a/tests/yup_audio_plugin_host/yup_AudioPluginScanner.cpp b/tests/yup_audio_plugin_host/yup_AudioPluginScanner.cpp index c16738705..9966c8f74 100644 --- a/tests/yup_audio_plugin_host/yup_AudioPluginScanner.cpp +++ b/tests/yup_audio_plugin_host/yup_AudioPluginScanner.cpp @@ -19,6 +19,8 @@ class FakeFormat : public AudioPluginFormat String getFormatName() const override { return "Fake"; } + StringArray getFileExtensions() const override { return { ".vst3" }; } + FileSearchPath getDefaultSearchPaths() const override { return {}; } ResultValue> scanFile (const File& file) override