From b188a4380039282fbed41d118f37f99540962b17 Mon Sep 17 00:00:00 2001 From: jatinchowdhury18 Date: Mon, 2 Dec 2024 00:47:14 -0800 Subject: [PATCH] State serialization improvements (#570) * Re-working plugin state serialization * Remove dead comments * Better backrds compatibility * Apply clang-format * Serialization work * Apply clang-format * More byte serialization work * Serialization for non-parameter state * Binary serialization mostly working * Apply clang-format * Mossing arena frame * Tweaking plugin state arena usage * Small fixes * Apply clang-format * Using ChunkList for parameter deserialization * Updating non-param state * Fixes * More fixes * Adding a couple of tests * Apply clang-format --------- Co-authored-by: github-actions[bot] --- examples/StatefulPlugin/PluginEditor.cpp | 4 +- examples/StatefulPlugin/StatefulPlugin.h | 2 +- .../Allocators/chowdsp_ArenaAllocator.h | 7 + .../chowdsp_ChainedArenaAllocator.h | 11 + .../JSONUtils/chowdsp_JSONAdaptors.h | 52 ++++ .../JSONUtils/chowdsp_StringAdapter.h | 21 -- modules/common/chowdsp_json/chowdsp_json.h | 3 +- .../Serialization/chowdsp_ByteSerializer.cpp | 78 ++++++ .../Serialization/chowdsp_ByteSerializer.h | 132 ++++++++++ .../chowdsp_serialization.cpp | 3 + .../chowdsp_serialization.h | 4 +- .../ParamUtils/chowdsp_ParameterTypes.cpp | 2 +- .../ParamUtils/chowdsp_ParameterTypes.h | 10 +- .../Backend/chowdsp_NonParamState.cpp | 129 +++++++-- .../Backend/chowdsp_NonParamState.h | 36 ++- .../Backend/chowdsp_ParamHolder.cpp | 245 +++++++++++++----- .../Backend/chowdsp_ParamHolder.h | 29 ++- .../Backend/chowdsp_ParameterTypeHelpers.h | 9 + .../Backend/chowdsp_PluginState.h | 8 +- .../Backend/chowdsp_PluginStateImpl.cpp | 112 +++++--- .../Backend/chowdsp_PluginStateImpl.h | 16 +- .../Backend/chowdsp_StateValue.h | 90 +++++-- .../chowdsp_plugin_state.cpp | 1 + .../chowdsp_plugin_state.h | 4 + .../Backend/chowdsp_PresetState.cpp | 40 ++- .../Backend/chowdsp_PresetState.h | 10 +- .../chowdsp_version/Version/chowdsp_Version.h | 9 + tests/common_tests/CMakeLists.txt | 1 + .../chowdsp_json_test/JSONTest.cpp | 15 ++ .../ByteSerializationTest.cpp | 166 ++++++++++++ .../chowdsp_serialization_test/CMakeLists.txt | 1 + .../chowdsp_plugin_state_test/CMakeLists.txt | 1 + .../NonParamTest.cpp | 230 ++++++++++++++++ .../ParamHolderTest.cpp | 78 +++++- .../StateSerializationTest.cpp | 177 ++++++++++++- .../VersionStreamingTest.cpp | 21 +- .../PresetManagerTest.cpp | 51 +++- .../chowdsp_version_test/VersionUtilsTest.cpp | 4 + 38 files changed, 1587 insertions(+), 225 deletions(-) create mode 100644 modules/common/chowdsp_json/JSONUtils/chowdsp_JSONAdaptors.h delete mode 100644 modules/common/chowdsp_json/JSONUtils/chowdsp_StringAdapter.h create mode 100644 modules/common/chowdsp_serialization/Serialization/chowdsp_ByteSerializer.cpp create mode 100644 modules/common/chowdsp_serialization/Serialization/chowdsp_ByteSerializer.h create mode 100644 modules/common/chowdsp_serialization/chowdsp_serialization.cpp create mode 100644 tests/common_tests/chowdsp_serialization_test/ByteSerializationTest.cpp create mode 100644 tests/plugin_tests/chowdsp_plugin_state_test/NonParamTest.cpp diff --git a/examples/StatefulPlugin/PluginEditor.cpp b/examples/StatefulPlugin/PluginEditor.cpp index 6138d88dc..835b6b38c 100644 --- a/examples/StatefulPlugin/PluginEditor.cpp +++ b/examples/StatefulPlugin/PluginEditor.cpp @@ -28,7 +28,7 @@ PluginEditor::PluginEditor (StatefulPlugin& plug) : juce::AudioProcessorEditor ( const auto setSizeFromState = [this] { const auto& stateSize = plugin.getState().nonParams.editorBounds.get(); - setSize (stateSize.x, stateSize.y); + setSize (stateSize.first, stateSize.second); }; setSizeFromState(); @@ -65,5 +65,5 @@ void PluginEditor::resized() undoButton.setBounds (bounds.removeFromLeft (80)); redoButton.setBounds (bounds.removeFromLeft (80)); - plugin.getState().nonParams.editorBounds = getLocalBounds().getBottomRight(); + plugin.getState().nonParams.editorBounds = std::make_pair (getLocalBounds().getRight(), getLocalBounds().getBottom()); } diff --git a/examples/StatefulPlugin/StatefulPlugin.h b/examples/StatefulPlugin/StatefulPlugin.h index ce91d092d..d60dec202 100644 --- a/examples/StatefulPlugin/StatefulPlugin.h +++ b/examples/StatefulPlugin/StatefulPlugin.h @@ -34,7 +34,7 @@ struct PluginNonParameterState : chowdsp::NonParamState addStateValues ({ &editorBounds }); } - chowdsp::StateValue> editorBounds { "editor_bounds", { 300, 500 } }; + chowdsp::StateValue> editorBounds { "editor_bounds", { 300, 500 } }; }; using State = chowdsp::PluginStateImpl; diff --git a/modules/common/chowdsp_data_structures/Allocators/chowdsp_ArenaAllocator.h b/modules/common/chowdsp_data_structures/Allocators/chowdsp_ArenaAllocator.h index e71b9df19..d88b1ed6a 100644 --- a/modules/common/chowdsp_data_structures/Allocators/chowdsp_ArenaAllocator.h +++ b/modules/common/chowdsp_data_structures/Allocators/chowdsp_ArenaAllocator.h @@ -103,6 +103,13 @@ class ArenaAllocator return reinterpret_cast (raw_data.data() + offset_bytes); } + /** Returns a pointer to the internal buffer with a given offset in bytes */ + template + const T* data (IntType offset_bytes) const noexcept + { + return reinterpret_cast (raw_data.data() + offset_bytes); + } + /** * Creates a "frame" for the allocator. * Once the frame goes out of scope, the allocator will be reset diff --git a/modules/common/chowdsp_data_structures/Allocators/chowdsp_ChainedArenaAllocator.h b/modules/common/chowdsp_data_structures/Allocators/chowdsp_ChainedArenaAllocator.h index b48c85be3..688d27af9 100644 --- a/modules/common/chowdsp_data_structures/Allocators/chowdsp_ChainedArenaAllocator.h +++ b/modules/common/chowdsp_data_structures/Allocators/chowdsp_ChainedArenaAllocator.h @@ -114,6 +114,12 @@ class ChainedArenaAllocator return get_current_arena().template data (offset_bytes); } + /** Returns the default size for an individual arena */ + [[nodiscard]] size_t get_default_arena_size() const noexcept + { + return arena_size_bytes; + } + /** Returns the arena currently being used */ ArenaAllocatorView& get_current_arena() { @@ -133,6 +139,11 @@ class ChainedArenaAllocator return arena_list.count; } + [[nodiscard]] auto* get_extra_alloc_list() const noexcept + { + return extra_alloc_list; + } + /** * Returns the total number of bytes currently being used * by this allocator. diff --git a/modules/common/chowdsp_json/JSONUtils/chowdsp_JSONAdaptors.h b/modules/common/chowdsp_json/JSONUtils/chowdsp_JSONAdaptors.h new file mode 100644 index 000000000..27ece8209 --- /dev/null +++ b/modules/common/chowdsp_json/JSONUtils/chowdsp_JSONAdaptors.h @@ -0,0 +1,52 @@ +#pragma once + +#if JUCE_MODULE_AVAILABLE_chowdsp_version +#include +#endif + +#ifndef DOXYGEN +namespace nlohmann +{ +/** Adapter so that nlohmann::json can serialize juce::String */ +template <> +struct adl_serializer +{ + static void to_json (json& j, const juce::String& s) + { + j = s.toUTF8(); + } + + static void from_json (const json& j, juce::String& s) + { + s = j.get(); + } +}; + +#if JUCE_MODULE_AVAILABLE_chowdsp_version +/** Adapter so that nlohmann::json can serialize chowdsp::Version */ +template <> +struct adl_serializer<::chowdsp::Version> +{ + static void to_json (json& j, const ::chowdsp::Version& version) + { + j = version.getVersionHint(); + } + + static void from_json (const json& j, ::chowdsp::Version& version) + { + if (! j.is_number_integer()) + { + version = {}; + return; + } + + const auto versionHint = j.get(); + const auto major = versionHint / 10000; + const auto minor = (versionHint % 10000) / 100; + const auto patch = versionHint % 100; + version = { major, minor, patch }; + } +}; +#endif +} // namespace nlohmann +#endif diff --git a/modules/common/chowdsp_json/JSONUtils/chowdsp_StringAdapter.h b/modules/common/chowdsp_json/JSONUtils/chowdsp_StringAdapter.h deleted file mode 100644 index 2cf2eb216..000000000 --- a/modules/common/chowdsp_json/JSONUtils/chowdsp_StringAdapter.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#ifndef DOXYGEN -namespace nlohmann -{ -/** Adapter so that nlohmann::json can serialize juce::String */ -template <> -struct adl_serializer -{ - static void to_json (json& j, const juce::String& s) - { - j = s.toUTF8(); - } - - static void from_json (const json& j, juce::String& s) - { - s = j.get(); - } -}; -} // namespace nlohmann -#endif diff --git a/modules/common/chowdsp_json/chowdsp_json.h b/modules/common/chowdsp_json/chowdsp_json.h index 26ded9f65..d49f7a24c 100644 --- a/modules/common/chowdsp_json/chowdsp_json.h +++ b/modules/common/chowdsp_json/chowdsp_json.h @@ -32,6 +32,5 @@ namespace chowdsp using json = nlohmann::json; } // namespace chowdsp -#include "JSONUtils/chowdsp_StringAdapter.h" - +#include "JSONUtils/chowdsp_JSONAdaptors.h" #include "JSONUtils/chowdsp_JSONUtils.h" diff --git a/modules/common/chowdsp_serialization/Serialization/chowdsp_ByteSerializer.cpp b/modules/common/chowdsp_serialization/Serialization/chowdsp_ByteSerializer.cpp new file mode 100644 index 000000000..41a9468ef --- /dev/null +++ b/modules/common/chowdsp_serialization/Serialization/chowdsp_ByteSerializer.cpp @@ -0,0 +1,78 @@ +namespace chowdsp +{ +nonstd::span get_bytes_for_deserialization (nonstd::span& bytes) +{ + using namespace bytes_detail; + + size_type bytes_count; + std::memcpy (&bytes_count, bytes.data(), sizeof_s); + jassert (sizeof_s + bytes_count <= bytes.size()); + + const auto serial_bytes = bytes.subspan (sizeof_s, bytes_count); + bytes = bytes.subspan (sizeof_s + bytes_count); + + return serial_bytes; +} + +size_t get_serial_num_bytes (ChainedArenaAllocator& arena, + const ChainedArenaAllocator::Frame* frame) +{ + size_t num_bytes = 0; + auto* start_arena = frame == nullptr ? arena.get_arenas().head : frame->arena_at_start; + const auto add_bytes_count = [&num_bytes, start_arena, frame] (const ArenaAllocatorView& arena_node) + { + size_t bytes_offset = 0; + if (start_arena == &arena_node && frame != nullptr) + bytes_offset = frame->arena_frame.bytes_used_at_start; + num_bytes += arena_node.get_bytes_used() - bytes_offset; + }; + + for (auto* arena_node = start_arena; arena_node != &arena.get_current_arena(); arena_node = arena_node->next) + add_bytes_count (*arena_node); + add_bytes_count (arena.get_current_arena()); + + return num_bytes; +} + +void dump_serialized_bytes (nonstd::span serial, + ChainedArenaAllocator& arena, + const ChainedArenaAllocator::Frame* frame) +{ + const auto num_bytes = serial.size(); + jassert (num_bytes == get_serial_num_bytes (arena, frame)); + + auto* start_arena = frame == nullptr ? arena.get_arenas().head : frame->arena_at_start; + size_t bytes_counter = 0; + const auto copy_bytes = [num_bytes, start_arena, frame, &serial, &bytes_counter] (const ArenaAllocatorView& arena_node) + { + size_t bytes_offset = 0; + if (start_arena == &arena_node && frame != nullptr) + bytes_offset = frame->arena_frame.bytes_used_at_start; + + const auto bytes_to_copy = std::min (arena_node.get_bytes_used() - bytes_offset, + num_bytes - bytes_counter); + std::memcpy (serial.data() + bytes_counter, arena_node.data (bytes_offset), bytes_to_copy); + + bytes_counter += bytes_to_copy; + }; + for (auto* arena_node = start_arena; arena_node != &arena.get_current_arena(); arena_node = arena_node->next) + copy_bytes (*arena_node); + copy_bytes (arena.get_current_arena()); +} + +void dump_serialized_bytes (juce::MemoryBlock& data, + ChainedArenaAllocator& arena, + const ChainedArenaAllocator::Frame* frame) +{ + const auto initial_size = data.getSize(); + const auto num_bytes = get_serial_num_bytes (arena, frame); + data.setSize (initial_size + num_bytes); + dump_serialized_bytes ({ static_cast (data.getData()) + initial_size, num_bytes }, arena, frame); +} + +std::string_view deserialize_string (nonstd::span& bytes) +{ + const auto serial_bytes = get_bytes_for_deserialization (bytes); + return { reinterpret_cast (serial_bytes.data()), serial_bytes.size() }; +} +} // namespace chowdsp diff --git a/modules/common/chowdsp_serialization/Serialization/chowdsp_ByteSerializer.h b/modules/common/chowdsp_serialization/Serialization/chowdsp_ByteSerializer.h new file mode 100644 index 000000000..d6b3a742a --- /dev/null +++ b/modules/common/chowdsp_serialization/Serialization/chowdsp_ByteSerializer.h @@ -0,0 +1,132 @@ +#pragma once + +namespace chowdsp +{ +#ifndef DOXYGEN +namespace bytes_detail +{ + using size_type = uint16_t; + static constexpr auto sizeof_s = sizeof (size_type); +} // namespace bytes_detail +#endif + +template +static std::byte* get_bytes_for_serialization (size_t bytes_count, ArenaType& arena) +{ + using namespace bytes_detail; + jassert (bytes_count <= std::numeric_limits::max()); + + if constexpr (std::is_same_v) + jassert (sizeof_s + bytes_count <= arena.get_default_arena_size()); + + auto* bytes = static_cast (arena.allocate_bytes (sizeof_s + bytes_count, 1)); + jassert (bytes != nullptr); + + const auto bytes_count_cast = static_cast (bytes_count); + std::memcpy (bytes, &bytes_count_cast, sizeof_s); + + return bytes + sizeof_s; // NOLINT +} + +nonstd::span get_bytes_for_deserialization (nonstd::span& bytes); + +template +void serialize_direct (TDest* ptr, const TSource& source) +{ + const auto source_cast = static_cast (source); + std::memcpy (ptr, &source_cast, sizeof (TDest)); +} + +template +static size_t serialize_object (const T& object, ArenaType& arena) +{ + auto* bytes = get_bytes_for_serialization (sizeof (T), arena); + std::memcpy (bytes, &object, sizeof (T)); // NOLINT + return bytes_detail::sizeof_s + sizeof (T); +} + +template +static size_t serialize_span (nonstd::span data, ArenaType& arena) +{ + const auto num_bytes = sizeof (T) * data.size(); + auto* bytes = get_bytes_for_serialization (num_bytes, arena); + std::memcpy (bytes, data.data(), num_bytes); // NOLINT + return bytes_detail::sizeof_s + num_bytes; +} + +template +static size_t serialize_string (std::string_view str, ArenaType& arena) +{ + const auto num_bytes = sizeof (char) * str.size(); + auto* bytes = get_bytes_for_serialization (num_bytes, arena); + std::memcpy (bytes, str.data(), num_bytes); // NOLINT + return bytes_detail::sizeof_s + num_bytes; +} + +template +static nonstd::span dump_serialized_bytes (const ArenaAllocator& arena, + const typename ArenaAllocator::Frame* frame = nullptr) +{ + const auto bytes_offset = frame == nullptr ? 0 : frame->bytes_used_at_start; + const auto bytes_count = arena.get_bytes_used() - bytes_offset; + return { arena.template data (bytes_offset), bytes_count }; +} + +size_t get_serial_num_bytes (ChainedArenaAllocator& arena, + const ChainedArenaAllocator::Frame* frame = nullptr); + +void dump_serialized_bytes (nonstd::span serial, + ChainedArenaAllocator& arena, + const ChainedArenaAllocator::Frame* frame = nullptr); + +void dump_serialized_bytes (juce::MemoryBlock& data, + ChainedArenaAllocator& arena, + const ChainedArenaAllocator::Frame* frame = nullptr); + +template +T deserialize_direct (nonstd::span& bytes) +{ + T x; + std::memcpy (&x, bytes.data(), sizeof (T)); + bytes = bytes.subspan (sizeof (T)); + return x; +} + +template +static T deserialize_object (nonstd::span& bytes) +{ + const auto serial_bytes = get_bytes_for_deserialization (bytes); + jassert (serial_bytes.size() == sizeof (T)); + + JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wclass-memaccess") + T object; + std::memcpy (&object, serial_bytes.data(), serial_bytes.size()); + return object; + JUCE_END_IGNORE_WARNINGS_GCC_LIKE +} + +template +static size_t get_span_length (const nonstd::span& bytes) +{ + using namespace bytes_detail; + + size_type sizeof_span_bytes; + std::memcpy (&sizeof_span_bytes, bytes.data(), sizeof_s); + + jassert (sizeof_span_bytes % sizeof (T) == 0); + return static_cast (sizeof_span_bytes) / sizeof (T); +} + +template +static void deserialize_span (nonstd::span span, nonstd::span& bytes) +{ + const auto serial_bytes = get_bytes_for_deserialization (bytes); + + jassert (serial_bytes.size() % sizeof (T) == 0); + jassert (serial_bytes.size() / sizeof (T) == span.size()); + + std::memcpy (span.data(), serial_bytes.data(), serial_bytes.size()); +} + +std::string_view deserialize_string (nonstd::span& bytes); +} // namespace chowdsp diff --git a/modules/common/chowdsp_serialization/chowdsp_serialization.cpp b/modules/common/chowdsp_serialization/chowdsp_serialization.cpp new file mode 100644 index 000000000..4cf9bcffc --- /dev/null +++ b/modules/common/chowdsp_serialization/chowdsp_serialization.cpp @@ -0,0 +1,3 @@ +#include "chowdsp_serialization.h" + +#include "Serialization/chowdsp_ByteSerializer.cpp" diff --git a/modules/common/chowdsp_serialization/chowdsp_serialization.h b/modules/common/chowdsp_serialization/chowdsp_serialization.h index a4872c10d..f1e6b85c4 100644 --- a/modules/common/chowdsp_serialization/chowdsp_serialization.h +++ b/modules/common/chowdsp_serialization/chowdsp_serialization.h @@ -8,7 +8,7 @@ BEGIN_JUCE_MODULE_DECLARATION version: 2.3.0 name: ChowDSP Serialization Utilities description: Utility methods for serializing data structures into XML, JSON, or some other format - dependencies: juce_core, chowdsp_core, chowdsp_json, chowdsp_reflection + dependencies: juce_core, chowdsp_core, chowdsp_json, chowdsp_reflection, chowdsp_data_structures website: https://ccrma.stanford.edu/~jatin/chowdsp license: BSD 3-Clause @@ -25,8 +25,10 @@ BEGIN_JUCE_MODULE_DECLARATION #include #include #include +#include #include "Serialization/chowdsp_BaseSerializer.h" #include "Serialization/chowdsp_Serialization.h" #include "Serialization/chowdsp_JSONSerializer.h" #include "Serialization/chowdsp_XMLSerializer.h" +#include "Serialization/chowdsp_ByteSerializer.h" diff --git a/modules/plugin/chowdsp_parameters/ParamUtils/chowdsp_ParameterTypes.cpp b/modules/plugin/chowdsp_parameters/ParamUtils/chowdsp_ParameterTypes.cpp index 646a6dafa..9ccc409c3 100644 --- a/modules/plugin/chowdsp_parameters/ParamUtils/chowdsp_ParameterTypes.cpp +++ b/modules/plugin/chowdsp_parameters/ParamUtils/chowdsp_ParameterTypes.cpp @@ -33,7 +33,7 @@ FloatParameter::FloatParameter (const ParameterID& parameterID, { return valueToTextFunction (v); }) .withValueFromStringFunction (std::move (textToValueFunction))), #endif - unsnappedDefault (valueRange.convertTo0to1 (defaultFloatValue)), + defaultValueInRange (defaultFloatValue), normalisableRange (valueRange) { } diff --git a/modules/plugin/chowdsp_parameters/ParamUtils/chowdsp_ParameterTypes.h b/modules/plugin/chowdsp_parameters/ParamUtils/chowdsp_ParameterTypes.h index 96afdcfc7..1f97afac3 100644 --- a/modules/plugin/chowdsp_parameters/ParamUtils/chowdsp_ParameterTypes.h +++ b/modules/plugin/chowdsp_parameters/ParamUtils/chowdsp_ParameterTypes.h @@ -64,7 +64,10 @@ class FloatParameter : public juce::AudioParameterFloat, void setParameterValue (float newValue) { AudioParameterFloat::operator= (newValue); } /** Returns the default value for the parameter. */ - float getDefaultValue() const override { return unsnappedDefault; } + float getDefaultValue() const override { return normalisableRange.convertTo0to1 (defaultValueInRange); } + + /** Returns the default value for the parameter (in range). */ + float getDefault() const { return defaultValueInRange; } /** TRUE! */ bool supportsMonophonicModulation() override { return true; } @@ -79,7 +82,7 @@ class FloatParameter : public juce::AudioParameterFloat, operator float() const noexcept { return getCurrentValue(); } // NOSONAR, NOLINT(google-explicit-constructor): we want to be able to do implicit conversion here private: - const float unsnappedDefault; + const float defaultValueInRange; const juce::NormalisableRange normalisableRange; float modulationAmount = 0.0f; @@ -180,6 +183,9 @@ class BoolParameter : public juce::AudioParameterBool, */ void setParameterValue (bool newValue) { AudioParameterBool::operator= (newValue); } + /** Returns the default value for the parameter (in range). */ + bool getDefault() const { return static_cast (*this).getDefaultValue() > 0.5f; } + private: JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BoolParameter) }; diff --git a/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_NonParamState.cpp b/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_NonParamState.cpp index c0d609de5..9f19d6a9e 100644 --- a/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_NonParamState.cpp +++ b/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_NonParamState.cpp @@ -1,28 +1,12 @@ namespace chowdsp { -inline void NonParamState::addStateValues (const std::initializer_list& newStateValues) +void NonParamState::addStateValues (const std::initializer_list& newStateValues) { values.insert (values.end(), newStateValues.begin(), newStateValues.end()); validateStateValues(); } -template -inline void NonParamState::addStateValues (nonstd::span> newStateValues) -{ - for (auto& val : newStateValues) - values.push_back (&val); - validateStateValues(); -} - -template -inline void NonParamState::addStateValues (ContainerType& container) -{ - for (auto& val : container) - values.push_back (&val); - validateStateValues(); -} - -inline void NonParamState::validateStateValues() const +void NonParamState::validateStateValues() const { #if JUCE_DEBUG std::vector stateValueNames; @@ -41,31 +25,112 @@ inline void NonParamState::validateStateValues() const #endif } -template -typename Serializer::SerializedType NonParamState::serialize (const NonParamState& state) +void NonParamState::reset() +{ + for (auto* value : values) + value->reset(); +} + +void NonParamState::serialize (ChainedArenaAllocator& arena, const NonParamState& state) +{ + auto* serialize_num_bytes = arena.allocate (1, 1); + size_t num_bytes = 0; + for (const auto& value : state.values) + { + num_bytes += serialize_string (value->name, arena); + num_bytes += value->serialize (arena); + } + serialize_direct (serialize_num_bytes, num_bytes); +} + +void NonParamState::deserialize (nonstd::span& serial_data, NonParamState& state, ChainedArenaAllocator& arena) +{ + auto num_bytes = deserialize_direct (serial_data); + if (num_bytes == 0) + { + state.reset(); + return; + } + + auto data = serial_data.subspan (0, num_bytes); + serial_data = serial_data.subspan (num_bytes); + + const auto _ = arena.create_frame(); + auto values_copy = arena::make_span (arena, state.values.size()); + std::copy (state.values.begin(), state.values.end(), values_copy.begin()); + auto values_iter = values_copy.begin(); + size_t counter = 0; + const auto get_value_ptr = [&] (std::string_view name) -> StateValueBase* + { + const auto returner = [&] (auto& iter) + { + auto* ptr = *iter; + *iter = nullptr; + ++iter; + values_iter = iter; + counter++; + return ptr; + }; + + for (auto iter = values_iter; iter != values_copy.end(); ++iter) + { + if (*iter != nullptr && (*iter)->name == name) + return returner (iter); + } + for (auto iter = values_copy.begin(); iter != values_iter; ++iter) + { + if (*iter != nullptr && (*iter)->name == name) + return returner (iter); + } + return nullptr; + }; + + while (! data.empty()) + { + const auto value_name = deserialize_string (data); + auto* value = get_value_ptr (value_name); + if (value == nullptr) + { + const auto value_num_bytes = deserialize_direct (data); + data = data.subspan (value_num_bytes); + continue; + } + + value->deserialize (data); + } + + if (counter < values_copy.size()) + { + for (auto* value : values_copy) + if (value != nullptr) + value->reset(); + } +} + +json NonParamState::serialize_json (const NonParamState& state) { - auto serial = Serializer::createBaseElement(); + auto serial = nlohmann::json::object(); for (const auto& value : state.values) - value->serialize (serial); + serial[value->name] = value->serialize_json(); return serial; } -template -void NonParamState::deserialize (typename Serializer::DeserializedType deserial, const NonParamState& state) +void NonParamState::legacy_deserialize (const json& deserial, const NonParamState& state) { + using Serializer = JSONSerializer; std::vector namesThatHaveBeenDeserialized {}; if (const auto numNamesAndVals = Serializer::getNumChildElements (deserial); numNamesAndVals % 2 == 0) { namesThatHaveBeenDeserialized.reserve (static_cast (numNamesAndVals) / 2); for (int i = 0; i < numNamesAndVals; i += 2) { - const auto name = Serializer::getChildElement (deserial, i).template get(); + const auto name = Serializer::getChildElement (deserial, i).get(); const auto& valueDeserial = Serializer::getChildElement (deserial, i + 1); for (auto& value : state.values) { if (name == value->name) { - value->deserialize (valueDeserial); + value->deserialize_json (valueDeserial); namesThatHaveBeenDeserialized.push_back (name); } } @@ -86,4 +151,16 @@ void NonParamState::deserialize (typename Serializer::DeserializedType deserial, } } } + +void NonParamState::deserialize_json (const json& deserial, const NonParamState& state) +{ + for (auto& value : state.values) + { + auto iter = deserial.find (value->name); + if (iter != deserial.end()) + value->deserialize_json (*iter); + else + value->reset(); + } +} } // namespace chowdsp diff --git a/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_NonParamState.h b/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_NonParamState.h index 4f222b499..52ff4138e 100644 --- a/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_NonParamState.h +++ b/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_NonParamState.h @@ -16,19 +16,39 @@ class NonParamState /** Adds more state values to this state. */ template - void addStateValues (nonstd::span> newStateValues); + void addStateValues (nonstd::span> newStateValues) + { + for (auto& val : newStateValues) + values.push_back (&val); + validateStateValues(); + } /** Adds more state values to this state. */ template - void addStateValues (ContainerType& container); + void addStateValues (ContainerType& container) + { + for (auto& val : container) + values.push_back (&val); + validateStateValues(); + } + + /** Resets all the state values to their defaults */ + void reset(); + + /** Custom serializer */ + static void serialize (ChainedArenaAllocator& arena, const NonParamState& state); + + /** Custom deserializer */ + static void deserialize (nonstd::span& serial_data, NonParamState& state, ChainedArenaAllocator& arena); /** Custom serializer */ - template - static typename Serializer::SerializedType serialize (const NonParamState& state); + static json serialize_json (const NonParamState& state); + + /** Custom deserializer */ + static void deserialize_json (const json& deserial, const NonParamState& state); /** Custom deserializer */ - template - static void deserialize (typename Serializer::DeserializedType deserial, const NonParamState& state); + static void legacy_deserialize (const json& deserial, const NonParamState& state); /** Assign this function to apply version streaming to your non-parameter state. */ std::function versionStreamingCallback = nullptr; @@ -36,10 +56,8 @@ class NonParamState private: void validateStateValues() const; - std::vector values; + std::vector values {}; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NonParamState) }; } // namespace chowdsp - -#include "chowdsp_NonParamState.cpp" diff --git a/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_ParamHolder.cpp b/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_ParamHolder.cpp index 0a250ced6..f37d9d845 100644 --- a/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_ParamHolder.cpp +++ b/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_ParamHolder.cpp @@ -10,10 +10,9 @@ inline ParamHolder::ParamHolder (ParamHolder* parent, std::string_view phName, b jassert (parent->arena != nullptr); return OptionalPointer { parent->arena.get(), false }; } - return OptionalPointer { static_cast (1024) }; + return OptionalPointer { static_cast (32 * CHOWDSP_PLUGIN_STATE_MAX_PARAM_COUNT) }; }(), }, - allParamsMap { MapAllocator { arena } }, name { arena::alloc_string (*arena, phName) }, isOwning { phIsOwning } { @@ -21,7 +20,6 @@ inline ParamHolder::ParamHolder (ParamHolder* parent, std::string_view phName, b inline ParamHolder::ParamHolder (ChainedArenaAllocator& alloc, std::string_view phName, bool phIsOwning) : arena { &alloc, false }, - allParamsMap { MapAllocator { arena } }, name { arena::alloc_string (*arena, phName) }, isOwning { phIsOwning } { @@ -49,17 +47,21 @@ inline ParamHolder::~ParamHolder() } } } + + if (arena.isOwner()) + { + // If you're hitting this assertion, you probably want to increase + // CHOWDSP_PLUGIN_STATE_MAX_PARAM_COUNT. + jassert (arena->get_extra_alloc_list() == nullptr); + } } template std::enable_if_t, void> ParamHolder::add (OptionalPointer& floatParam, OtherParams&... others) { - const auto paramID = toStringView (floatParam->paramID); - ThingPtr paramPtr { reinterpret_cast (isOwning ? floatParam.release() : floatParam.get()), - getFlags (FloatParam, isOwning) }; - allParamsMap.insert ({ paramID, paramPtr }); - things.insert (std::move (paramPtr)); + things.insert (ThingPtr { reinterpret_cast (isOwning ? floatParam.release() : floatParam.get()), + getFlags (FloatParam, isOwning) }); add (others...); } @@ -67,11 +69,8 @@ template std::enable_if_t, void> ParamHolder::add (OptionalPointer& choiceParam, OtherParams&... others) { - const auto paramID = toStringView (choiceParam->paramID); - ThingPtr paramPtr { reinterpret_cast (isOwning ? choiceParam.release() : choiceParam.get()), - getFlags (ChoiceParam, isOwning) }; - allParamsMap.insert ({ paramID, paramPtr }); - things.insert (std::move (paramPtr)); + things.insert (ThingPtr { reinterpret_cast (isOwning ? choiceParam.release() : choiceParam.get()), + getFlags (ChoiceParam, isOwning) }); add (others...); } @@ -79,11 +78,8 @@ template std::enable_if_t, void> ParamHolder::add (OptionalPointer& boolParam, OtherParams&... others) { - const auto paramID = toStringView (boolParam->paramID); - ThingPtr paramPtr { reinterpret_cast (isOwning ? boolParam.release() : boolParam.get()), - getFlags (BoolParam, isOwning) }; - allParamsMap.insert ({ paramID, paramPtr }); - things.insert (std::move (paramPtr)); + things.insert (ThingPtr { reinterpret_cast (isOwning ? boolParam.release() : boolParam.get()), + getFlags (BoolParam, isOwning) }); add (others...); } @@ -114,12 +110,6 @@ std::enable_if_t, void> template void ParamHolder::add (ParamHolder& paramHolder, OtherParams&... others) { - // This should be the parent of the holder being added. - // Maybe we can relax this restriction if we no longer need the allParamsMap. - jassert (arena == paramHolder.arena); - - allParamsMap.merge (paramHolder.allParamsMap); - jassert (paramHolder.allParamsMap.empty()); // assuming no duplicate parameter IDs, all the parameters should be moved in the merge! things.insert (ThingPtr { reinterpret_cast (¶mHolder), Holder }); add (others...); } @@ -263,59 +253,198 @@ size_t ParamHolder::doForAllParameters (Callable&& callable, size_t index) const return index; } -template -typename Serializer::SerializedType ParamHolder::serialize (const ParamHolder& paramHolder) +inline void ParamHolder::getParameterPointers (ParamHolder& holder, ParamDeserialList& parameters) +{ + for (auto& thing : holder.things) + { + const auto type = getType (thing); + if (type == Holder) + { + getParameterPointers (*reinterpret_cast (thing.get_ptr()), parameters); + continue; + } + + std::string_view paramID {}; + switch (type) + { + case FloatParam: + paramID = toStringView (reinterpret_cast (thing.get_ptr())->paramID); + break; + case ChoiceParam: + paramID = toStringView (reinterpret_cast (thing.get_ptr())->paramID); + break; + case BoolParam: + paramID = toStringView (reinterpret_cast (thing.get_ptr())->paramID); + break; + default: + break; + } + + parameters.insert (ParamDeserial { paramID, thing, false }); + } +} + +inline void ParamHolder::serialize (ChainedArenaAllocator& arena, const ParamHolder& paramHolder) +{ + auto* serialize_num_bytes = arena.allocate (1, 1); + size_t num_bytes = 0; + paramHolder.doForAllParameters ( + [&] (auto& param, size_t) + { + num_bytes += serialize_string (toStringView (param.paramID), arena); + num_bytes += serialize_object (ParameterTypeHelpers::getValue (param), arena); + }); + serialize_direct (serialize_num_bytes, num_bytes); +} + +inline void ParamHolder::deserialize (nonstd::span& serial_data, ParamHolder& paramHolder) +{ + using namespace ParameterTypeHelpers; + auto num_bytes = deserialize_direct (serial_data); + if (num_bytes == 0) + { + paramHolder.doForAllParameters ( + [&] (auto& param, size_t) + { + ParameterTypeHelpers::resetParameter (param); + }); + return; + } + + auto data = serial_data.subspan (0, num_bytes); + serial_data = serial_data.subspan (num_bytes); + + const auto _ = paramHolder.arena->create_frame(); + ParamDeserialList parameters { *paramHolder.arena }; + getParameterPointers (paramHolder, parameters); + + auto params_iter = parameters.begin(); + size_t counter = 0; + const auto get_param_ptr = [&] (std::string_view paramID) -> ThingPtr + { + const auto returner = [&] (auto& iter) + { + (*iter).found = true; + auto ptr = (*iter).ptr; + ++iter; + params_iter = iter; + counter++; + return ptr; + }; + + for (auto iter = params_iter; iter != parameters.end(); ++iter) + { + if ((*iter).id == paramID) + return returner (iter); + } + for (auto iter = parameters.begin(); iter != params_iter; ++iter) + { + if ((*iter).id == paramID) + return returner (iter); + } + return {}; + }; + + while (! data.empty()) + { + const auto param_id = deserialize_string (data); + auto param_ptr = get_param_ptr (param_id); + if (param_ptr == nullptr) + { + const auto param_num_bytes = deserialize_direct (data); + data = data.subspan (param_num_bytes); + continue; + } + + const auto type = getType (param_ptr); + switch (type) + { + case FloatParam: + setValue (deserialize_object> (data), + *reinterpret_cast (param_ptr.get_ptr())); + break; + case ChoiceParam: + setValue (deserialize_object> (data), + *reinterpret_cast (param_ptr.get_ptr())); + break; + case BoolParam: + setValue (deserialize_object> (data), + *reinterpret_cast (param_ptr.get_ptr())); + break; + default: + break; + } + } + + if (counter < parameters.count()) + { + for (auto [param_id, param_ptr, found] : parameters) + { + if (found) + continue; + + const auto type = getType (param_ptr); + switch (type) + { + case FloatParam: + resetParameter (*reinterpret_cast (param_ptr.get_ptr())); + break; + case ChoiceParam: + resetParameter (*reinterpret_cast (param_ptr.get_ptr())); + break; + case BoolParam: + resetParameter (*reinterpret_cast (param_ptr.get_ptr())); + break; + default: + break; + } + } + } +} + +inline json ParamHolder::serialize_json (const ParamHolder& paramHolder) { - auto serial = Serializer::createBaseElement(); + auto serial = nlohmann::json::object(); paramHolder.doForAllParameters ( [&serial] (auto& param, size_t) { - ParameterTypeHelpers::serializeParameter (serial, param); + const auto paramID = toStringView (param.paramID); + serial[paramID] = ParameterTypeHelpers::getValue (param); }); return serial; } -template -void ParamHolder::deserialize (typename Serializer::DeserializedType deserial, ParamHolder& paramHolder) +inline void ParamHolder::deserialize_json (const json& deserial, ParamHolder& paramHolder) +{ + paramHolder.doForAllParameters ( + [&deserial] (auto& param, size_t) + { + const auto paramID = toStringView (param.paramID); + ParameterTypeHelpers::setValue (deserial.value (paramID, ParameterTypeHelpers::getDefaultValue (param)), param); + }); +} + +inline void ParamHolder::legacy_deserialize (const json& deserial, ParamHolder& paramHolder) { + using Serializer = JSONSerializer; std::vector paramIDsThatHaveBeenDeserialized {}; if (const auto numParamIDsAndVals = Serializer::getNumChildElements (deserial); numParamIDsAndVals % 2 == 0) { paramIDsThatHaveBeenDeserialized.reserve (static_cast (numParamIDsAndVals) / 2); for (int i = 0; i < numParamIDsAndVals; i += 2) { - const auto paramID = Serializer::getChildElement (deserial, i).template get(); + const auto paramID = Serializer::getChildElement (deserial, i).get(); const auto& paramDeserial = Serializer::getChildElement (deserial, i + 1); - auto paramPtrIter = paramHolder.allParamsMap.find (std::string { paramID }); - if (paramPtrIter == paramHolder.allParamsMap.end()) - continue; - - paramIDsThatHaveBeenDeserialized.push_back (paramID); - [¶mDeserial] (ThingPtr& paramPtr) - { - const auto deserializeParam = [] (auto* param, auto& pd) + paramHolder.doForAllParameters ( + [&] (auto& param, size_t) { - ParameterTypeHelpers::deserializeParameter (pd, *param); - }; + if (toStringView (param.paramID) != paramID) + return; - const auto type = getType (paramPtr); - switch (type) - { - case FloatParam: - deserializeParam (reinterpret_cast (paramPtr.get_ptr()), paramDeserial); - break; - case ChoiceParam: - deserializeParam (reinterpret_cast (paramPtr.get_ptr()), paramDeserial); - break; - case BoolParam: - deserializeParam (reinterpret_cast (paramPtr.get_ptr()), paramDeserial); - break; - default: - jassertfalse; - break; - } - }(paramPtrIter->second); + paramIDsThatHaveBeenDeserialized.push_back (paramID); + ParameterTypeHelpers::deserializeParameter (paramDeserial, param); + }); } } else diff --git a/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_ParamHolder.h b/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_ParamHolder.h index bf6961975..b7582f0a7 100644 --- a/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_ParamHolder.h +++ b/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_ParamHolder.h @@ -92,12 +92,19 @@ class ParamHolder size_t doForAllParameters (Callable&& callable, size_t index = 0) const; /** Custom serializer */ - template - static typename Serializer::SerializedType serialize (const ParamHolder& paramHolder); + static void serialize (ChainedArenaAllocator& arena, const ParamHolder& paramHolder); /** Custom deserializer */ - template - static void deserialize (typename Serializer::DeserializedType deserial, ParamHolder& paramHolder); + static void deserialize (nonstd::span& serial_data, ParamHolder& paramHolder); + + /** Custom serializer */ + static json serialize_json (const ParamHolder& paramHolder); + + /** Custom deserializer */ + static void deserialize_json (const json& deserial, ParamHolder& paramHolder); + + /** Legacy deserializer */ + static void legacy_deserialize (const json& deserial, ParamHolder& paramHolder); /** Recursively applies version streaming to the parameters herein. */ void applyVersionStreaming (const Version&); @@ -105,7 +112,6 @@ class ParamHolder /** Assign this function to apply version streaming to your non-parameter state. */ FixedSizeFunction<8, void (const Version&)> versionStreamingCallback {}; -protected: OptionalPointer arena {}; private: @@ -139,11 +145,14 @@ class ParamHolder return static_cast (type | (shouldDelete ? ShouldDelete : 0)); } - using MapKey = std::string_view; - using MapValue = ThingPtr; - using MapAllocator = STLArenaAllocator, ChainedArenaAllocator>; - using AllParamsMap = std::unordered_map, std::equal_to<>, MapAllocator>; - AllParamsMap allParamsMap; + struct ParamDeserial + { + std::string_view id {}; + ThingPtr ptr {}; + bool found = false; + }; + using ParamDeserialList = ChunkList; + static void getParameterPointers (ParamHolder& holder, ParamDeserialList& parameters); std::string_view name; bool isOwning; diff --git a/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_ParameterTypeHelpers.h b/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_ParameterTypeHelpers.h index 9c887485f..43615910c 100644 --- a/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_ParameterTypeHelpers.h +++ b/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_ParameterTypeHelpers.h @@ -50,6 +50,15 @@ namespace ParameterTypeHelpers return param.getIndex(); } + template + ParameterElementType getDefaultValue (const ParamType& param) + { + if constexpr (std::is_base_of_v || std::is_base_of_v) + return param.getDefault(); + else if constexpr (std::is_base_of_v) + return param.getDefaultIndex(); + } + /** Resets a parameter to it's default value. */ inline void resetParameter (juce::AudioProcessorParameter& param) { diff --git a/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_PluginState.h b/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_PluginState.h index 02646abf7..a0e1ecd59 100644 --- a/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_PluginState.h +++ b/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_PluginState.h @@ -32,7 +32,7 @@ class PluginState virtual void serialize (juce::MemoryBlock& data) const = 0; /** Deserializes the plugin state from the given MemoryBlock */ - virtual void deserialize (const juce::MemoryBlock& data) = 0; + virtual void deserialize (juce::MemoryBlock&& data) = 0; /** * Adds a parameter listener which will be called on either the message @@ -84,8 +84,10 @@ class PluginState juce::AudioProcessor* processor = nullptr; juce::UndoManager* undoManager = nullptr; -private: +protected: std::optional listeners; + +private: ParamHolder* params = nullptr; DeferredAction mainThreadAction; @@ -96,7 +98,7 @@ class PluginState struct DummyPluginState : PluginState { void serialize (juce::MemoryBlock&) const override {} - void deserialize (const juce::MemoryBlock&) override {} + void deserialize (juce::MemoryBlock&&) override {} NonParamState non_params {}; [[nodiscard]] NonParamState& getNonParameters() override { return non_params; } diff --git a/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_PluginStateImpl.cpp b/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_PluginStateImpl.cpp index acb2d4b09..ac70ee4e8 100644 --- a/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_PluginStateImpl.cpp +++ b/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_PluginStateImpl.cpp @@ -1,30 +1,61 @@ namespace chowdsp { -template -PluginStateImpl::PluginStateImpl (juce::UndoManager* um) +template +PluginStateImpl::PluginStateImpl (juce::UndoManager* um) { initialise (params, nullptr, um); } -template -PluginStateImpl::PluginStateImpl (juce::AudioProcessor& proc, juce::UndoManager* um) +template +PluginStateImpl::PluginStateImpl (juce::AudioProcessor& proc, juce::UndoManager* um) { initialise (params, &proc, um); } -template -void PluginStateImpl::serialize (juce::MemoryBlock& data) const +template +PluginStateImpl::~PluginStateImpl() { - Serialization::serialize (*this, data); + // Otherwise the listeners won't be deleted until after the parameters themselves. + listeners.reset(); } -template -void PluginStateImpl::deserialize (const juce::MemoryBlock& data) +template +void PluginStateImpl::serialize (juce::MemoryBlock& data) const +{ + auto& arena = const_cast (*params.arena); + const auto frame = arena.create_frame(); + +#if defined JucePlugin_VersionString + serialize_object (currentPluginVersion.getVersionHint(), arena); +#else + serialize_object (int {}, arena); +#endif + + NonParamState::serialize (arena, nonParams); + ParamHolder::serialize (arena, params); + + dump_serialized_bytes (data, arena, &frame); +} + +template +void PluginStateImpl::deserialize (juce::MemoryBlock&& dataBlock) { callOnMainThread ( - [this, data] + [this, data = std::move (dataBlock)] { - Serialization::deserialize (data, *this); + nonstd::span serial_data { static_cast (data.getData()), data.getSize() }; + if (serial_data.size() > 8 + && (static_cast (serial_data[2]) == '[' + || static_cast (serial_data[2]) == '{')) + { + deserialize (JSONUtils::fromMemoryBlock (data), *this); + } + else + { + pluginStateVersion = Version::fromVersionHint (deserialize_object (serial_data)); + NonParamState::deserialize (serial_data, nonParams, *params.arena); + ParamHolder::deserialize (serial_data, params); + } params.applyVersionStreaming (pluginStateVersion); if (nonParams.versionStreamingCallback != nullptr) @@ -38,26 +69,26 @@ void PluginStateImpl::deserialize } /** Serializer */ -template -template -typename Serializer::SerializedType PluginStateImpl::serialize (const PluginStateImpl& object) +template +json PluginStateImpl::serialize (const PluginStateImpl& object) { - auto serial = Serializer::createBaseElement(); - + return + { #if defined JucePlugin_VersionString - Serializer::addChildElement (serial, Serializer::template serialize (currentPluginVersion)); + { "version", currentPluginVersion }, +#else + { "version", chowdsp::Version {} }, #endif - - Serializer::addChildElement (serial, Serializer::template serialize (object.nonParams)); - Serializer::addChildElement (serial, Serializer::template serialize (object.params)); - return serial; + { "params", ParamHolder::serialize_json (object.params) }, + { "non-params", NonParamState::serialize_json (object.nonParams) }, + }; } -/** Deserializer */ -template -template -void PluginStateImpl::deserialize (typename Serializer::DeserializedType serial, PluginStateImpl& object) +/** Legacy Deserializer */ +template +void PluginStateImpl::legacy_deserialize (const json& serial, PluginStateImpl& object) { + using Serializer = JSONSerializer; enum { #if defined JucePlugin_VersionString @@ -75,24 +106,41 @@ void PluginStateImpl::deserialize } #if defined JucePlugin_VersionString - Serializer::template deserialize (Serializer::getChildElement (serial, versionChildIndex), object.pluginStateVersion); + Serializer::deserialize (Serializer::getChildElement (serial, versionChildIndex), object.pluginStateVersion); #else using namespace version_literals; object.pluginStateVersion = "0.0.0"_v; #endif - Serializer::template deserialize (Serializer::getChildElement (serial, nonParamStateChildIndex), object.nonParams); - Serializer::template deserialize (Serializer::getChildElement (serial, paramStateChildIndex), object.params); + NonParamState::legacy_deserialize (Serializer::getChildElement (serial, nonParamStateChildIndex), object.nonParams); + ParamHolder::legacy_deserialize (Serializer::getChildElement (serial, paramStateChildIndex), object.params); +} + +/** Deserializer */ +template +void PluginStateImpl::deserialize (const json& serial, PluginStateImpl& object) +{ + if (serial.is_array()) + { + legacy_deserialize (serial, object); + return; + } + + jassert (serial.find ("version") != serial.end()); + object.pluginStateVersion = serial.value ("version", Version {}); + + NonParamState::deserialize_json (serial.at ("non-params"), object.nonParams); + ParamHolder::deserialize_json (serial.at ("params"), object.params); } -template -NonParamState& PluginStateImpl::getNonParameters() +template +NonParamState& PluginStateImpl::getNonParameters() { return nonParams; } -template -const NonParamState& PluginStateImpl::getNonParameters() const +template +const NonParamState& PluginStateImpl::getNonParameters() const { return nonParams; } diff --git a/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_PluginStateImpl.h b/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_PluginStateImpl.h index 76b5ee5fc..dbbdd3e10 100644 --- a/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_PluginStateImpl.h +++ b/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_PluginStateImpl.h @@ -7,9 +7,8 @@ namespace chowdsp * * @tparam ParameterState Struct containing all of the plugin's parameters as chowdsp::OptionalPointer's. * @tparam NonParameterState Struct containing all of the plugin's non-parameter state as StateValue objects. - * @tparam Serializer A type that implements chowdsp::BaseSerializer (JSONSerializer by default) */ -template +template class PluginStateImpl : public PluginState { static_assert (std::is_base_of_v, "ParameterState must be a chowdsp::ParamHolder!"); @@ -22,19 +21,22 @@ class PluginStateImpl : public PluginState /** Constructs the state and adds all the state parameters to the given processor */ explicit PluginStateImpl (juce::AudioProcessor& proc, juce::UndoManager* um = nullptr); + ~PluginStateImpl() override; + /** Serializes the plugin state to the given MemoryBlock */ void serialize (juce::MemoryBlock& data) const override; /** Deserializes the plugin state from the given MemoryBlock */ - void deserialize (const juce::MemoryBlock& data) override; + void deserialize (juce::MemoryBlock&& data) override; /** Serializer */ - template - static typename Serializer::SerializedType serialize (const PluginStateImpl& object); + static json serialize (const PluginStateImpl& object); /** Deserializer */ - template - static void deserialize (typename Serializer::DeserializedType serial, PluginStateImpl& object); + static void deserialize (const json& serial, PluginStateImpl& object); + + /** Legacy Deserializer */ + static void legacy_deserialize (const json& serial, PluginStateImpl& object); /** Returns the plugin non-parameter state */ [[nodiscard]] NonParamState& getNonParameters() override; diff --git a/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_StateValue.h b/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_StateValue.h index 3001625b2..cc858c8e5 100644 --- a/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_StateValue.h +++ b/modules/plugin/chowdsp_plugin_state/Backend/chowdsp_StateValue.h @@ -1,4 +1,5 @@ #pragma once +#include namespace chowdsp { @@ -11,8 +12,11 @@ struct StateValueBase virtual void reset() {} - virtual void serialize (JSONSerializer::SerializedType&) const {} - virtual void deserialize (JSONSerializer::DeserializedType) {} + [[nodiscard]] virtual nlohmann::json serialize_json() const { return {}; } + virtual void deserialize_json (const nlohmann::json&) {} + + [[nodiscard]] virtual size_t serialize (ChainedArenaAllocator&) const { return 0; } + virtual void deserialize (nonstd::span&) {} const std::string_view name {}; Broadcaster changeBroadcaster {}; @@ -70,35 +74,85 @@ struct StateValue : StateValueBase void reset() override { set (defaultValue); } /** JSON Serializer */ - void serialize (JSONSerializer::SerializedType& serial) const override + [[nodiscard]] nlohmann::json serialize_json() const override { - serialize (serial, *this); + return get(); } /** JSON Deserializer */ - void deserialize (JSONSerializer::DeserializedType deserial) override + void deserialize_json (const nlohmann::json& deserial) override { - deserialize (deserial, *this); + set (deserial.get()); } - const element_type defaultValue; - -private: - template - static void serialize (typename Serializer::SerializedType& serial, const StateValue& value) + /** Binary serializer */ + [[nodiscard]] size_t serialize (ChainedArenaAllocator& arena) const override { - Serializer::addChildElement (serial, value.name); - Serializer::addChildElement (serial, Serialization::serialize (value.get())); + static constexpr auto is_span = TypeTraits::IsIterable && ! TypeTraits::IsMapLike; + + // Values need to track how many bytes they're serializing so the parent can know also. + auto* serialize_num_bytes = arena.allocate (1, 1); + + size_t num_bytes = 0; + if constexpr (std::is_same_v) + { + num_bytes = serialize_string (get().dump(), arena); + } + else if constexpr (std::is_same_v || std::is_same_v) + { + num_bytes = serialize_string (currentValue, arena); + } + else if constexpr (std::is_same_v) + { + num_bytes = serialize_string (toStringView (currentValue), arena); + } + else if constexpr (is_span) + { + num_bytes = serialize_span (currentValue, arena); + } + else + { + num_bytes = serialize_object (get(), arena); + } + + serialize_direct (serialize_num_bytes, num_bytes); + return bytes_detail::sizeof_s + num_bytes; } - template - static void deserialize (typename Serializer::DeserializedType deserial, StateValue& value) + void deserialize (nonstd::span& data) override { - element_type val {}; - Serialization::deserialize (deserial, val); - value.set (val); + static constexpr auto is_span = TypeTraits::IsIterable && ! TypeTraits::IsMapLike; + + [[maybe_unused]] const auto num_bytes = deserialize_direct (data); + if constexpr (std::is_same_v) + { + set (json::parse (deserialize_string (data))); + } + else if constexpr (std::is_same_v) + { + set (std::string { deserialize_string (data) }); + } + else if constexpr (std::is_same_v) + { + set (deserialize_string (data)); + } + else if constexpr (std::is_same_v) + { + set (toString (deserialize_string (data))); + } + else if constexpr (is_span) + { + deserialize_span (currentValue, data); + } + else + { + set (deserialize_object (data)); + } } + const element_type defaultValue; + +private: T currentValue; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (StateValue) diff --git a/modules/plugin/chowdsp_plugin_state/chowdsp_plugin_state.cpp b/modules/plugin/chowdsp_plugin_state/chowdsp_plugin_state.cpp index ccdb63065..943c889c7 100644 --- a/modules/plugin/chowdsp_plugin_state/chowdsp_plugin_state.cpp +++ b/modules/plugin/chowdsp_plugin_state/chowdsp_plugin_state.cpp @@ -1,5 +1,6 @@ #include "chowdsp_plugin_state.h" +#include "Backend/chowdsp_NonParamState.cpp" #include "Backend/chowdsp_ParameterListeners.cpp" #include "Frontend/chowdsp_SliderAttachment.cpp" diff --git a/modules/plugin/chowdsp_plugin_state/chowdsp_plugin_state.h b/modules/plugin/chowdsp_plugin_state/chowdsp_plugin_state.h index 22919c92c..05ae4252d 100644 --- a/modules/plugin/chowdsp_plugin_state/chowdsp_plugin_state.h +++ b/modules/plugin/chowdsp_plugin_state/chowdsp_plugin_state.h @@ -20,6 +20,10 @@ BEGIN_JUCE_MODULE_DECLARATION #pragma once +#ifndef CHOWDSP_PLUGIN_STATE_MAX_PARAM_COUNT +#define CHOWDSP_PLUGIN_STATE_MAX_PARAM_COUNT 128 +#endif + #include #include #include diff --git a/modules/plugin/chowdsp_presets_v2/Backend/chowdsp_PresetState.cpp b/modules/plugin/chowdsp_presets_v2/Backend/chowdsp_PresetState.cpp index ac4ddfa9e..d16324113 100644 --- a/modules/plugin/chowdsp_presets_v2/Backend/chowdsp_PresetState.cpp +++ b/modules/plugin/chowdsp_presets_v2/Backend/chowdsp_PresetState.cpp @@ -37,16 +37,15 @@ void PresetState::reset() set ({}); } -void PresetState::serialize (JSONSerializer::SerializedType& serial) const +nlohmann::json PresetState::serialize_json() const { - JSONSerializer::addChildElement (serial, name); - if (preset == nullptr) - JSONSerializer::addChildElement (serial, {}); - else - JSONSerializer::addChildElement (serial, preset->toJson()); + if (preset != nullptr) + return preset->toJson(); + + return {}; } -void PresetState::deserialize (JSONSerializer::DeserializedType deserial) +void PresetState::deserialize_json (const nlohmann::json& deserial) { if (deserial.is_null()) { @@ -57,6 +56,33 @@ void PresetState::deserialize (JSONSerializer::DeserializedType deserial) set (PresetPtr { deserial }); } +[[nodiscard]] size_t PresetState::serialize (ChainedArenaAllocator& arena) const +{ + size_t num_bytes = 0; + if (preset == nullptr) + { + num_bytes += serialize_string ("", arena); + return num_bytes; + } + + num_bytes += serialize_string (preset->toJson().dump(), arena); + return num_bytes; +} + +void PresetState::deserialize (nonstd::span& bytes) +{ + try + { + const auto stateJson = json::parse (deserialize_string (bytes)); + set (PresetPtr { stateJson }); + } + catch (const std::exception& e) + { + juce::Logger::writeToLog (std::string { "Unable to load preset state: " } + e.what()); + reset(); + } +} + bool operator== (const PresetState& presetState, std::nullptr_t) { return presetState.get() == nullptr; diff --git a/modules/plugin/chowdsp_presets_v2/Backend/chowdsp_PresetState.h b/modules/plugin/chowdsp_presets_v2/Backend/chowdsp_PresetState.h index 94910732f..06746ae18 100644 --- a/modules/plugin/chowdsp_presets_v2/Backend/chowdsp_PresetState.h +++ b/modules/plugin/chowdsp_presets_v2/Backend/chowdsp_PresetState.h @@ -42,10 +42,16 @@ class PresetState : public StateValueBase void reset() override; /** Internal use only! */ - void serialize (JSONSerializer::SerializedType& serial) const override; + [[nodiscard]] nlohmann::json serialize_json() const override; /** Internal use only! */ - void deserialize (JSONSerializer::DeserializedType deserial) override; + void deserialize_json (const nlohmann::json& deserial) override; + + /** Internal use only! */ + [[nodiscard]] size_t serialize (ChainedArenaAllocator&) const override; + + /** Internal use only! */ + void deserialize (nonstd::span&) override; private: PresetPtr preset {}; diff --git a/modules/plugin/chowdsp_version/Version/chowdsp_Version.h b/modules/plugin/chowdsp_version/Version/chowdsp_Version.h index f189e32e8..69b482ec8 100644 --- a/modules/plugin/chowdsp_version/Version/chowdsp_Version.h +++ b/modules/plugin/chowdsp_version/Version/chowdsp_Version.h @@ -59,6 +59,15 @@ class Version /** Returns an integer hint for this version value. */ [[nodiscard]] constexpr int getVersionHint() const { return major * 10000 + minor * 100 + patch; } + /** Returns a version from a version hint */ + static constexpr Version fromVersionHint (int versionHint) + { + const auto major = versionHint / 10000; + const auto minor = (versionHint / 100) % 100; + const auto patch = versionHint % 100; + return { major, minor, patch }; + } + /** Custom serializer */ template static typename Serializer::SerializedType serialize (const Version& object) diff --git a/tests/common_tests/CMakeLists.txt b/tests/common_tests/CMakeLists.txt index 52a0b547d..2f39384b3 100644 --- a/tests/common_tests/CMakeLists.txt +++ b/tests/common_tests/CMakeLists.txt @@ -6,6 +6,7 @@ setup_juce_lib(common_tests_lib chowdsp::chowdsp_logging chowdsp::chowdsp_json chowdsp::chowdsp_units + chowdsp::chowdsp_version ) add_subdirectory(chowdsp_core_test) diff --git a/tests/common_tests/chowdsp_json_test/JSONTest.cpp b/tests/common_tests/chowdsp_json_test/JSONTest.cpp index 524d2dd09..b972c7d46 100644 --- a/tests/common_tests/chowdsp_json_test/JSONTest.cpp +++ b/tests/common_tests/chowdsp_json_test/JSONTest.cpp @@ -14,6 +14,21 @@ TEST_CASE ("JSON Test", "[common][json]") REQUIRE_MESSAGE (returnString == testString, "JSON serialized string is incorrect!"); } + SECTION ("Version Serialization Test") + { + { + chowdsp::json parent; + chowdsp::Version testVersion { 2, 5, 7 }; + parent["key"] = testVersion; + auto returnVersion = parent["key"].get(); + REQUIRE (returnVersion == testVersion); + } + { + const auto badVersion = chowdsp::json::object().get(); + REQUIRE (badVersion == chowdsp::Version {}); + } + } + SECTION ("JSON File Test") { chowdsp::json jTest = { diff --git a/tests/common_tests/chowdsp_serialization_test/ByteSerializationTest.cpp b/tests/common_tests/chowdsp_serialization_test/ByteSerializationTest.cpp new file mode 100644 index 000000000..0002b4876 --- /dev/null +++ b/tests/common_tests/chowdsp_serialization_test/ByteSerializationTest.cpp @@ -0,0 +1,166 @@ +#include +#include + +TEST_CASE ("Byte Serialization Test", "[common][serialization]") +{ + struct Test + { + int x = 44; + float y = 128.0f; + double z = -19.0f; + bool b = false; + }; + + SECTION ("Basic Arena") + { + chowdsp::ArenaAllocator> arena {}; + + const auto arr = chowdsp::make_array_lambda ([] (auto idx) + { return static_cast (idx); }); + const auto str = std::string { "Hello world" }; + + chowdsp::serialize_object (int { 42 }, arena); + chowdsp::serialize_object (float { 99.0f }, arena); + chowdsp::serialize_object (Test {}, arena); + chowdsp::serialize_object (Test { 1, 2.0f, 3.0, true }, arena); + chowdsp::serialize_span (arr, arena); + chowdsp::serialize_string (str, arena); + + auto bytes = chowdsp::dump_serialized_bytes (arena); + + const auto int_test = chowdsp::deserialize_object (bytes); + REQUIRE (int_test == 42); + const auto float_test = chowdsp::deserialize_object (bytes); + REQUIRE (float_test == 99.0f); + auto struct_test = chowdsp::deserialize_object (bytes); + REQUIRE (pfr::eq_fields (struct_test, Test {})); + struct_test = chowdsp::deserialize_object (bytes); + REQUIRE (pfr::eq_fields (struct_test, Test { 1, 2.0f, 3.0, true })); + + const auto arr_test = chowdsp::deserialize_object> (bytes); + for (const auto& [x, y] : chowdsp::zip (arr, arr_test)) + REQUIRE (juce::exactlyEqual (x, y)); + + const auto str_test = chowdsp::deserialize_string (bytes); + REQUIRE (str_test == str); + REQUIRE (bytes.empty()); + } + + SECTION ("Arena w/ previous usage") + { + chowdsp::ArenaAllocator> arena {}; + arena.allocate (30); + + std::vector vec (12); + std::iota (vec.begin(), vec.end(), 100); + const auto str = juce::String { "Hello world" }; + + const auto frame = arena.create_frame(); + chowdsp::serialize_object (int { 42 }, arena); + chowdsp::serialize_object (float { 99.0f }, arena); + chowdsp::serialize_object (Test {}, arena); + chowdsp::serialize_object (Test { 1, 2.0f, 3.0, true }, arena); + chowdsp::serialize_span (vec, arena); + chowdsp::serialize_string (chowdsp::toStringView (str), arena); + + auto bytes = chowdsp::dump_serialized_bytes (arena, &frame); + + const auto int_test = chowdsp::deserialize_object (bytes); + REQUIRE (int_test == 42); + const auto float_test = chowdsp::deserialize_object (bytes); + REQUIRE (float_test == 99.0f); + auto struct_test = chowdsp::deserialize_object (bytes); + REQUIRE (pfr::eq_fields (struct_test, Test {})); + struct_test = chowdsp::deserialize_object (bytes); + REQUIRE (pfr::eq_fields (struct_test, Test { 1, 2.0f, 3.0, true })); + + std::vector vec_test (chowdsp::get_span_length (bytes)); + REQUIRE (vec_test.size() == vec.size()); + chowdsp::deserialize_span (vec_test, bytes); + for (const auto& [x, y] : chowdsp::zip (vec, vec_test)) + REQUIRE (juce::exactlyEqual (x, y)); + + const auto str_test = chowdsp::deserialize_string (bytes); + REQUIRE (str_test == chowdsp::toStringView (str)); + REQUIRE (bytes.empty()); + } + + SECTION ("Basic Chained Arena") + { + chowdsp::ChainedArenaAllocator arena { 64 }; + + const auto arr = chowdsp::make_array_lambda ([] (auto idx) + { return static_cast (idx); }); + const auto str = std::string { "Hello world" }; + + chowdsp::serialize_object (int { 42 }, arena); + chowdsp::serialize_object (float { 99.0f }, arena); + chowdsp::serialize_object (Test {}, arena); + chowdsp::serialize_object (Test { 1, 2.0f, 3.0, true }, arena); + chowdsp::serialize_span (arr, arena); + chowdsp::serialize_string (str, arena); + + std::vector raw_bytes (chowdsp::get_serial_num_bytes (arena)); + chowdsp::dump_serialized_bytes (raw_bytes, arena); + nonstd::span bytes { raw_bytes }; + + const auto int_test = chowdsp::deserialize_object (bytes); + REQUIRE (int_test == 42); + const auto float_test = chowdsp::deserialize_object (bytes); + REQUIRE (float_test == 99.0f); + auto struct_test = chowdsp::deserialize_object (bytes); + REQUIRE (pfr::eq_fields (struct_test, Test {})); + struct_test = chowdsp::deserialize_object (bytes); + REQUIRE (pfr::eq_fields (struct_test, Test { 1, 2.0f, 3.0, true })); + + const auto arr_test = chowdsp::deserialize_object> (bytes); + for (const auto& [x, y] : chowdsp::zip (arr, arr_test)) + REQUIRE (juce::exactlyEqual (x, y)); + + const auto str_test = chowdsp::deserialize_string (bytes); + REQUIRE (str_test == str); + REQUIRE (bytes.empty()); + } + + SECTION ("Chained Arena w/ previous usage") + { + chowdsp::ChainedArenaAllocator arena { 64 }; + arena.allocate (12); + arena.allocate (12); + + std::vector vec (12); + std::iota (vec.begin(), vec.end(), 100); + const auto str = juce::String { "Hello world" }; + + const auto frame = arena.create_frame(); + chowdsp::serialize_object (int { 42 }, arena); + chowdsp::serialize_object (float { 99.0f }, arena); + chowdsp::serialize_object (Test {}, arena); + chowdsp::serialize_object (Test { 1, 2.0f, 3.0, true }, arena); + chowdsp::serialize_span (vec, arena); + chowdsp::serialize_string (chowdsp::toStringView (str), arena); + + juce::MemoryBlock raw_data {}; + chowdsp::dump_serialized_bytes (raw_data, arena, &frame); + nonstd::span bytes { static_cast (raw_data.getData()), raw_data.getSize() }; + + const auto int_test = chowdsp::deserialize_object (bytes); + REQUIRE (int_test == 42); + const auto float_test = chowdsp::deserialize_object (bytes); + REQUIRE (float_test == 99.0f); + auto struct_test = chowdsp::deserialize_object (bytes); + REQUIRE (pfr::eq_fields (struct_test, Test {})); + struct_test = chowdsp::deserialize_object (bytes); + REQUIRE (pfr::eq_fields (struct_test, Test { 1, 2.0f, 3.0, true })); + + std::vector vec_test (chowdsp::get_span_length (bytes)); + REQUIRE (vec_test.size() == vec.size()); + chowdsp::deserialize_span (vec_test, bytes); + for (const auto& [x, y] : chowdsp::zip (vec, vec_test)) + REQUIRE (juce::exactlyEqual (x, y)); + + const auto str_test = chowdsp::deserialize_string (bytes); + REQUIRE (str_test == chowdsp::toStringView (str)); + REQUIRE (bytes.empty()); + } +} diff --git a/tests/common_tests/chowdsp_serialization_test/CMakeLists.txt b/tests/common_tests/chowdsp_serialization_test/CMakeLists.txt index 65a58b7be..58c233005 100644 --- a/tests/common_tests/chowdsp_serialization_test/CMakeLists.txt +++ b/tests/common_tests/chowdsp_serialization_test/CMakeLists.txt @@ -2,5 +2,6 @@ setup_catch_lib_test(chowdsp_serialization_test common_tests_lib) target_sources(chowdsp_serialization_test PRIVATE SerializationTest.cpp + ByteSerializationTest.cpp TestSerialBinaryData.cpp ) diff --git a/tests/plugin_tests/chowdsp_plugin_state_test/CMakeLists.txt b/tests/plugin_tests/chowdsp_plugin_state_test/CMakeLists.txt index c3d848edb..67a7b3a5d 100644 --- a/tests/plugin_tests/chowdsp_plugin_state_test/CMakeLists.txt +++ b/tests/plugin_tests/chowdsp_plugin_state_test/CMakeLists.txt @@ -8,4 +8,5 @@ target_sources(chowdsp_plugin_state_test ParamHolderTest.cpp StatePluginInterfaceTest.cpp VersionStreamingTest.cpp + NonParamTest.cpp ) diff --git a/tests/plugin_tests/chowdsp_plugin_state_test/NonParamTest.cpp b/tests/plugin_tests/chowdsp_plugin_state_test/NonParamTest.cpp new file mode 100644 index 000000000..d06cab6f0 --- /dev/null +++ b/tests/plugin_tests/chowdsp_plugin_state_test/NonParamTest.cpp @@ -0,0 +1,230 @@ +#include +#include + +TEST_CASE ("Non-Param Test", "[plugin][state]") +{ + SECTION ("JSON Serialization") + { + chowdsp::StateValue int_val { "int", 42 }; + chowdsp::StateValue, int> atomic_int_val { "atomic_int", 99 }; + chowdsp::StateValue> bool_vals { "bools", { true, false, true, true } }; + chowdsp::StateValue string_val { "string", "blah" }; + chowdsp::StateValue string_view_val { "string_view", "fff" }; + chowdsp::StateValue juce_string_val { "juce_string", "juce" }; + chowdsp::StateValue json_val { "json", { { "val1", 100 }, { "val2", "test" } } }; + + chowdsp::json serial; + { + chowdsp::NonParamState state {}; + state.addStateValues ({ &int_val, &atomic_int_val, &bool_vals, &string_val, &string_view_val, &juce_string_val, &json_val }); + int_val = 101; + atomic_int_val.set (102); + bool_vals.set ({ false, true, false, true }); + string_val = "blah blah"; + string_view_val = "ggg"; + juce_string_val = "ecuj"; + json_val.set ({}); + serial = chowdsp::NonParamState::serialize_json (state); + } + + chowdsp::NonParamState state {}; + state.addStateValues ({ &int_val, &atomic_int_val, &bool_vals, &string_val, &string_view_val, &juce_string_val, &json_val }); + state.reset(); + REQUIRE (int_val.get() == 42); + REQUIRE (atomic_int_val.get() == 99); + REQUIRE (bool_vals.get()[0]); + REQUIRE (! bool_vals.get()[1]); + REQUIRE (bool_vals.get()[2]); + REQUIRE (bool_vals.get()[3]); + REQUIRE (string_val.get() == "blah"); + REQUIRE (string_view_val.get() == "fff"); + REQUIRE (juce_string_val.get() == "juce"); + REQUIRE (json_val.get() == chowdsp::json { { "val1", 100 }, { "val2", "test" } }); + + chowdsp::NonParamState::deserialize_json (serial, state); + REQUIRE (int_val.get() == 101); + REQUIRE (atomic_int_val.get() == 102); + REQUIRE (! bool_vals.get()[0]); + REQUIRE (bool_vals.get()[1]); + REQUIRE (! bool_vals.get()[2]); + REQUIRE (bool_vals.get()[3]); + REQUIRE (string_val.get() == "blah blah"); + REQUIRE (string_view_val.get() == "ggg"); + REQUIRE (juce_string_val.get() == "ecuj"); + REQUIRE (json_val.get().is_null()); + } + + SECTION ("Bytes Serialization") + { + chowdsp::StateValue int_val { "int", 42 }; + chowdsp::StateValue, int> atomic_int_val { "atomic_int", 99 }; + chowdsp::StateValue> bool_vals { "bools", { true, false, true, true } }; + chowdsp::StateValue string_val { "string", "blah" }; + chowdsp::StateValue string_view_val { "string_view", "fff" }; + chowdsp::StateValue juce_string_val { "juce_string", "juce" }; + chowdsp::StateValue json_val { "json", { { "val1", 100 }, { "val2", "test" } } }; + + chowdsp::ChainedArenaAllocator arena { 1024 }; + { + chowdsp::NonParamState state {}; + state.addStateValues ({ &int_val, &atomic_int_val, &bool_vals, &string_val, &string_view_val, &juce_string_val, &json_val }); + int_val = 101; + atomic_int_val.set (102); + bool_vals.set ({ false, true, false, true }); + string_val = "blah blah"; + string_view_val = "ggg"; + juce_string_val = "ecuj"; + json_val.set ({}); + chowdsp::NonParamState::serialize (arena, state); + } + + std::vector serial_bytes (chowdsp::get_serial_num_bytes (arena)); + chowdsp::dump_serialized_bytes (serial_bytes, arena); + nonstd::span bytes { serial_bytes }; + + chowdsp::NonParamState state {}; + state.addStateValues ({ &int_val, &atomic_int_val, &bool_vals, &string_val, &string_view_val, &juce_string_val, &json_val }); + state.reset(); + REQUIRE (int_val.get() == 42); + REQUIRE (atomic_int_val.get() == 99); + REQUIRE (bool_vals.get()[0]); + REQUIRE (! bool_vals.get()[1]); + REQUIRE (bool_vals.get()[2]); + REQUIRE (bool_vals.get()[3]); + REQUIRE (string_val.get() == "blah"); + REQUIRE (string_view_val.get() == "fff"); + REQUIRE (juce_string_val.get() == "juce"); + REQUIRE (json_val.get() == chowdsp::json { { "val1", 100 }, { "val2", "test" } }); + + chowdsp::NonParamState::deserialize (bytes, state, arena); + REQUIRE (int_val.get() == 101); + REQUIRE (atomic_int_val.get() == 102); + REQUIRE (! bool_vals.get()[0]); + REQUIRE (bool_vals.get()[1]); + REQUIRE (! bool_vals.get()[2]); + REQUIRE (bool_vals.get()[3]); + REQUIRE (string_val.get() == "blah blah"); + REQUIRE (string_view_val.get() == "ggg"); + REQUIRE (juce_string_val.get() == "ecuj"); + REQUIRE (json_val.get().is_null()); + } + + SECTION ("Bytes Serialization Re-order") + { + chowdsp::StateValue int_val { "int", 42 }; + chowdsp::StateValue, int> atomic_int_val { "atomic_int", 99 }; + chowdsp::StateValue string_val { "string", "blah" }; + + chowdsp::ChainedArenaAllocator arena { 1024 }; + { + chowdsp::NonParamState state {}; + state.addStateValues ({ &int_val, &atomic_int_val, &string_val }); + int_val = 101; + atomic_int_val.set (102); + string_val = "blah blah"; + chowdsp::NonParamState::serialize (arena, state); + } + + std::vector serial_bytes (chowdsp::get_serial_num_bytes (arena)); + chowdsp::dump_serialized_bytes (serial_bytes, arena); + nonstd::span bytes { serial_bytes }; + + chowdsp::NonParamState state {}; + state.addStateValues ({ &string_val, &atomic_int_val, &int_val }); + state.reset(); + REQUIRE (int_val.get() == 42); + REQUIRE (atomic_int_val.get() == 99); + REQUIRE (string_val.get() == "blah"); + + chowdsp::NonParamState::deserialize (bytes, state, arena); + REQUIRE (int_val.get() == 101); + REQUIRE (atomic_int_val.get() == 102); + REQUIRE (string_val.get() == "blah blah"); + } + + SECTION ("Bytes Serialization Adding Value") + { + chowdsp::StateValue int_val { "int", 42 }; + chowdsp::StateValue string_val { "string", "blah" }; + + chowdsp::ChainedArenaAllocator arena { 1024 }; + { + chowdsp::NonParamState state {}; + state.addStateValues ({ &int_val, &string_val }); + int_val = 101; + string_val = "blah blah"; + chowdsp::NonParamState::serialize (arena, state); + } + + std::vector serial_bytes (chowdsp::get_serial_num_bytes (arena)); + chowdsp::dump_serialized_bytes (serial_bytes, arena); + nonstd::span bytes { serial_bytes }; + + chowdsp::StateValue float_val { "float", 90.0f }; + chowdsp::NonParamState state {}; + state.addStateValues ({ &string_val, &float_val, &int_val }); + state.reset(); + float_val = 100.0f; + REQUIRE (int_val.get() == 42); + REQUIRE (float_val.get() == 100.0f); + REQUIRE (string_val.get() == "blah"); + + chowdsp::NonParamState::deserialize (bytes, state, arena); + REQUIRE (int_val.get() == 101); + REQUIRE (float_val.get() == 90.0f); + REQUIRE (string_val.get() == "blah blah"); + } + + SECTION ("Bytes Serialization Removing Value") + { + chowdsp::StateValue int_val { "int", 42 }; + chowdsp::StateValue, int> atomic_int_val { "atomic_int", 99 }; + chowdsp::StateValue string_val { "string", "blah" }; + + chowdsp::ChainedArenaAllocator arena { 1024 }; + { + chowdsp::NonParamState state {}; + state.addStateValues ({ &int_val, &atomic_int_val, &string_val }); + int_val = 101; + atomic_int_val.set (102); + string_val = "blah blah"; + chowdsp::NonParamState::serialize (arena, state); + } + + std::vector serial_bytes (chowdsp::get_serial_num_bytes (arena)); + chowdsp::dump_serialized_bytes (serial_bytes, arena); + nonstd::span bytes { serial_bytes }; + + chowdsp::NonParamState state {}; + state.addStateValues ({ &string_val, &int_val }); + state.reset(); + REQUIRE (int_val.get() == 42); + REQUIRE (string_val.get() == "blah"); + + chowdsp::NonParamState::deserialize (bytes, state, arena); + REQUIRE (int_val.get() == 101); + REQUIRE (string_val.get() == "blah blah"); + } + + SECTION ("Bytes Serialization Removing Value") + { + chowdsp::StateValue int_val { "int", 42 }; + chowdsp::StateValue, int> atomic_int_val { "atomic_int", 99 }; + chowdsp::StateValue string_val { "string", "blah" }; + + chowdsp::NonParamState state {}; + state.addStateValues ({ &int_val, &atomic_int_val, &string_val }); + int_val = 101; + atomic_int_val.set (102); + string_val = "blah blah"; + + const auto serial = std::array {}; + nonstd::span bytes { serial }; + + chowdsp::ChainedArenaAllocator arena { 1024 }; + chowdsp::NonParamState::deserialize (bytes, state, arena); + REQUIRE (int_val.get() == 42); + REQUIRE (atomic_int_val.get() == 99); + REQUIRE (string_val.get() == "blah"); + } +} diff --git a/tests/plugin_tests/chowdsp_plugin_state_test/ParamHolderTest.cpp b/tests/plugin_tests/chowdsp_plugin_state_test/ParamHolderTest.cpp index a872c6ed7..7434bc972 100644 --- a/tests/plugin_tests/chowdsp_plugin_state_test/ParamHolderTest.cpp +++ b/tests/plugin_tests/chowdsp_plugin_state_test/ParamHolderTest.cpp @@ -3,21 +3,20 @@ TEST_CASE ("ParamHolder Test", "[plugin][state]") { - SECTION ("add()") - { - chowdsp::ParamHolder params; - std::array floatParams { - chowdsp::PercentParameter::Ptr { "param3", "Param", 0.5f }, - chowdsp::PercentParameter::Ptr { "param4", "Param", 0.5f }, - }; - - chowdsp::BoolParameter::Ptr boolNested { "param1", "Param", false }; - chowdsp::ChoiceParameter::Ptr choiceNested { "param2", "Param", juce::StringArray { "One", "Two" }, 0 }; - chowdsp::ParamHolder nestedParams { ¶ms }; - nestedParams.add (boolNested, choiceNested); + std::array floatParams { + chowdsp::PercentParameter::Ptr { "param3", "Param", 0.5f }, + chowdsp::PercentParameter::Ptr { "param4", "Param", 0.5f }, + }; + chowdsp::BoolParameter::Ptr boolNested { "param1", "Param", false }; + chowdsp::ChoiceParameter::Ptr choiceNested { "param2", "Param", juce::StringArray { "One", "Two" }, 0 }; - params.add (nestedParams, floatParams); + chowdsp::ParamHolder params; + chowdsp::ParamHolder nestedParams { ¶ms }; + nestedParams.add (boolNested, choiceNested); + params.add (nestedParams, floatParams); + SECTION ("add()") + { auto allParamIDs = [¶ms] { juce::StringArray allIDs; @@ -31,4 +30,57 @@ TEST_CASE ("ParamHolder Test", "[plugin][state]") REQUIRE (allParamIDs.contains (floatParams[0]->paramID)); REQUIRE (allParamIDs.contains (floatParams[1]->paramID)); } + + SECTION ("Serialize JSON") + { + using namespace chowdsp::ParameterTypeHelpers; + setValue (0.0f, *floatParams[0]); + setValue (1.0f, *floatParams[1]); + setValue (true, *boolNested); + setValue (1, *choiceNested); + + const auto json_state = chowdsp::ParamHolder::serialize_json (params); + params.doForAllParameters ([] (auto& param, size_t) + { setValue (getDefaultValue (param), param); }); + + REQUIRE (getValue (*floatParams[0]) == 0.5f); + REQUIRE (getValue (*floatParams[1]) == 0.5f); + REQUIRE (getValue (*boolNested) == false); + REQUIRE (getValue (*choiceNested) == 0); + + chowdsp::ParamHolder::deserialize_json (json_state, params); + REQUIRE (getValue (*floatParams[0]) == 0.0f); + REQUIRE (getValue (*floatParams[1]) == 1.0f); + REQUIRE (getValue (*boolNested) == true); + REQUIRE (getValue (*choiceNested) == 1); + } + + SECTION ("Serialize Bytes") + { + using namespace chowdsp::ParameterTypeHelpers; + setValue (0.0f, *floatParams[0]); + setValue (1.0f, *floatParams[1]); + setValue (true, *boolNested); + setValue (1, *choiceNested); + + chowdsp::ChainedArenaAllocator arena { 128 }; + chowdsp::ParamHolder::serialize (arena, params); + juce::MemoryBlock state {}; + chowdsp::dump_serialized_bytes (state, arena); + + params.doForAllParameters ([] (auto& param, size_t) + { setValue (getDefaultValue (param), param); }); + + REQUIRE (getValue (*floatParams[0]) == 0.5f); + REQUIRE (getValue (*floatParams[1]) == 0.5f); + REQUIRE (getValue (*boolNested) == false); + REQUIRE (getValue (*choiceNested) == 0); + + nonstd::span state_data = { (const std::byte*) state.getData(), state.getSize() }; + chowdsp::ParamHolder::deserialize (state_data, params); + REQUIRE (getValue (*floatParams[0]) == 0.0f); + REQUIRE (getValue (*floatParams[1]) == 1.0f); + REQUIRE (getValue (*boolNested) == true); + REQUIRE (getValue (*choiceNested) == 1); + } } diff --git a/tests/plugin_tests/chowdsp_plugin_state_test/StateSerializationTest.cpp b/tests/plugin_tests/chowdsp_plugin_state_test/StateSerializationTest.cpp index c7dbafa95..fac60ec21 100644 --- a/tests/plugin_tests/chowdsp_plugin_state_test/StateSerializationTest.cpp +++ b/tests/plugin_tests/chowdsp_plugin_state_test/StateSerializationTest.cpp @@ -72,17 +72,32 @@ struct PluginParameterStateNewParam : chowdsp::ParamHolder { PluginParameterStateNewParam() { - add (levelParams, mode, onOff, newParam); + add (newParam2, onOff, mode, newParam3, levelParams, newParam); } LevelParams levelParams { this }; chowdsp::ChoiceParameter::Ptr mode { "mode", "Mode", juce::StringArray { "Percent", "Gain", "Percent / Gain", "Gain / Percent" }, 2 }; chowdsp::BoolParameter::Ptr onOff { "on_off", "On/Off", true }; chowdsp::GainDBParameter::Ptr newParam { "gain_new", "New Gain", juce::NormalisableRange { -45.0f, 12.0f }, 3.3f }; + chowdsp::ChoiceParameter::Ptr newParam2 { "choice_new", "New Choice", juce::StringArray { "a", "b", "c" }, 2 }; + chowdsp::BoolParameter::Ptr newParam3 { "bool_new", "New Bool", false }; }; using StateWithNewParam = chowdsp::PluginStateImpl; +struct PluginParameterStateRemovedParam : chowdsp::ParamHolder +{ + PluginParameterStateRemovedParam() + { + add (levelParams, onOff); + } + + LevelParams levelParams { this }; + chowdsp::BoolParameter::Ptr onOff { "on_off", "On/Off", true }; +}; + +using StateWithRemovedParam = chowdsp::PluginStateImpl; + struct PluginParameterStateDoubleOfSameType : chowdsp::ParamHolder { PluginParameterStateDoubleOfSameType() @@ -184,7 +199,7 @@ TEST_CASE ("State Serialization Test", "[plugin][state]") State state { &um }; um.beginNewTransaction(); um.perform (new DummyAction {}); - state.deserialize (block); + state.deserialize (std::move (block)); REQUIRE_MESSAGE (juce::approximatelyEqual (state.params.levelParams.percent->get(), percentVal), "Percent value is incorrect"); REQUIRE_MESSAGE (juce::approximatelyEqual (state.params.levelParams.gain->get(), gainVal), "Gain value is incorrect"); REQUIRE_MESSAGE (state.params.mode->getIndex() == choiceVal, "Choice value is incorrect"); @@ -210,7 +225,7 @@ TEST_CASE ("State Serialization Test", "[plugin][state]") } State state; - state.deserialize (block); + state.deserialize (std::move (block)); REQUIRE_MESSAGE (state.nonParams.editorWidth.get() == width, "Editor width is incorrect"); REQUIRE_MESSAGE (state.nonParams.editorHeight.get() == height, "Editor height is incorrect"); REQUIRE_MESSAGE (state.nonParams.atomicThing.get() == atomic, "Atomic thing is incorrect"); @@ -220,6 +235,8 @@ TEST_CASE ("State Serialization Test", "[plugin][state]") SECTION ("Added Parameter Test") { static constexpr float newGainVal = -22.0f; + static constexpr int newChoiceVal = 0; + static constexpr bool newBoolVal = true; juce::MemoryBlock block; { @@ -229,8 +246,34 @@ TEST_CASE ("State Serialization Test", "[plugin][state]") StateWithNewParam state; static_cast (state.params.newParam) = newGainVal; - state.deserialize (block); + static_cast (state.params.newParam2) = newChoiceVal; + static_cast (state.params.newParam3) = newBoolVal; + state.deserialize (std::move (block)); REQUIRE_MESSAGE (state.params.newParam->get() == Catch::Approx (3.3f).margin (1.0e-6f), "Added param value is incorrect"); + REQUIRE_MESSAGE (state.params.newParam2->getIndex() == 2, "Added param value is incorrect"); + REQUIRE_MESSAGE (state.params.newParam3->get() == false, "Added param value is incorrect"); + } + + SECTION ("Removed Parameter Test") + { + static constexpr float percentVal = 0.25f; + static constexpr float gainVal = -22.0f; + static constexpr bool boolVal = false; + + juce::MemoryBlock block; + { + State state; + static_cast (state.params.levelParams.percent) = percentVal; + static_cast (state.params.levelParams.gain) = gainVal; + static_cast (state.params.onOff) = boolVal; + state.serialize (block); + } + + StateWithRemovedParam state {}; + state.deserialize (std::move (block)); + REQUIRE_MESSAGE (juce::approximatelyEqual (state.params.levelParams.percent->get(), percentVal), "Percent value is incorrect"); + REQUIRE_MESSAGE (juce::approximatelyEqual (state.params.levelParams.gain->get(), gainVal), "Gain value is incorrect"); + REQUIRE_MESSAGE (state.params.onOff->get() == boolVal, "Bool value is incorrect"); } SECTION ("Added Parameter Group Test") @@ -245,7 +288,7 @@ TEST_CASE ("State Serialization Test", "[plugin][state]") StateWithNewGroup state; static_cast (state.params.newGroup.newParam) = newGainVal; - state.deserialize (block); + state.deserialize (std::move (block)); REQUIRE_MESSAGE (state.params.newGroup.newParam->get() == Catch::Approx (3.3f).margin (1.0e-6f), "Added param value is incorrect"); } @@ -271,7 +314,7 @@ TEST_CASE ("State Serialization Test", "[plugin][state]") } StateWithTripleOfSameType state {}; - state.deserialize (block); + state.deserialize (std::move (block)); REQUIRE_MESSAGE (juce::approximatelyEqual (state.params.levelParams1.percent->get(), percentVal1), "Percent value 1 is incorrect"); REQUIRE_MESSAGE (juce::approximatelyEqual (state.params.levelParams1.gain->get(), gainVal1), "Gain value 1 is incorrect"); REQUIRE_MESSAGE (juce::approximatelyEqual (state.params.levelParams2.percent->get(), percentVal2), "Percent value 2 is incorrect"); @@ -296,7 +339,127 @@ TEST_CASE ("State Serialization Test", "[plugin][state]") } StateWithNewNonParameterField state; - state.deserialize (block); + state.deserialize (std::move (block)); + REQUIRE_MESSAGE (state.nonParams.editorWidth.get() == width, "Editor width is incorrect"); + REQUIRE_MESSAGE (state.nonParams.editorHeight.get() == height, "Editor height is incorrect"); + REQUIRE_MESSAGE (state.nonParams.randomString.get() == juce::String { "default" }, "Added field is incorrect"); + } +} + +TEST_CASE ("Legacy State Serialization Test", "[plugin][state]") +{ + SECTION ("Save/Load Parameters Test") + { + static constexpr float percentVal = 0.25f; + static constexpr float gainVal = -22.0f; + static constexpr int choiceVal = 0; + static constexpr bool boolVal = false; + + const auto jsonState = nlohmann::json::parse (R"(["9.9.9",["editor_width",300,"editor_height",500,"something_atomic",12,"json_thing",{"answer":{"everything":42},"happy":true,"list":[1,0,2],"name":"Niels","nothing":null,"object":{"currency":"USD","value":42.99},"pi":3.141},"yes_no0",1,"yes_no1",1,"yes_no2",1,"yes_no3",1],["percent",0.25,"gain",-22.0,"mode",0,"on_off",false]])"); + juce::MemoryBlock block; + chowdsp::JSONUtils::toMemoryBlock (jsonState, block); + + struct DummyAction : juce::UndoableAction + { + bool perform() override { return true; } + bool undo() override { return true; } + }; + + juce::UndoManager um { 100 }; + State state { &um }; + um.beginNewTransaction(); + um.perform (new DummyAction {}); + state.deserialize (std::move (block)); + REQUIRE_MESSAGE (juce::approximatelyEqual (state.params.levelParams.percent->get(), percentVal), "Percent value is incorrect"); + REQUIRE_MESSAGE (juce::approximatelyEqual (state.params.levelParams.gain->get(), gainVal), "Gain value is incorrect"); + REQUIRE_MESSAGE (state.params.mode->getIndex() == choiceVal, "Choice value is incorrect"); + REQUIRE_MESSAGE (state.params.onOff->get() == boolVal, "Bool value is incorrect"); + REQUIRE_MESSAGE (! um.canUndo(), "Undo manager was not cleared after loading new state!"); + } + + SECTION ("Save/Load Non-Parameters Test") + { + static constexpr int width = 200; + static constexpr int height = 150; + static constexpr int atomic = 24; + const auto testJSON = nlohmann::json { { "new", 20 } }; + + const auto jsonState = nlohmann::json::parse (R"(["9.9.9",["editor_width",200,"editor_height",150,"something_atomic",24,"json_thing",{"new":20},"yes_no0",1,"yes_no1",1,"yes_no2",1,"yes_no3",1],["percent",0.5,"gain",0.0,"mode",2,"on_off",true]])"); + juce::MemoryBlock block; + chowdsp::JSONUtils::toMemoryBlock (jsonState, block); + + State state; + state.deserialize (std::move (block)); + REQUIRE_MESSAGE (state.nonParams.editorWidth.get() == width, "Editor width is incorrect"); + REQUIRE_MESSAGE (state.nonParams.editorHeight.get() == height, "Editor height is incorrect"); + REQUIRE_MESSAGE (state.nonParams.atomicThing.get() == atomic, "Atomic thing is incorrect"); + REQUIRE_MESSAGE (state.nonParams.jsonThing.get() == testJSON, "JSON thing is incorrect"); + } + + SECTION ("Added Parameter Test") + { + static constexpr float newGainVal = -22.0f; + + const auto jsonState = nlohmann::json::parse (R"(["9.9.9",["editor_width",300,"editor_height",500,"something_atomic",12,"json_thing",{"answer":{"everything":42},"happy":true,"list":[1,0,2],"name":"Niels","nothing":null,"object":{"currency":"USD","value":42.99},"pi":3.141},"yes_no0",1,"yes_no1",1,"yes_no2",1,"yes_no3",1],["percent",0.5,"gain",0.0,"mode",2,"on_off",true]])"); + juce::MemoryBlock block; + chowdsp::JSONUtils::toMemoryBlock (jsonState, block); + + StateWithNewParam state; + static_cast (state.params.newParam) = newGainVal; + state.deserialize (std::move (block)); + REQUIRE_MESSAGE (state.params.newParam->get() == Catch::Approx (3.3f).margin (1.0e-6f), "Added param value is incorrect"); + } + + SECTION ("Added Parameter Group Test") + { + static constexpr float newGainVal = -22.0f; + + const auto jsonState = nlohmann::json::parse (R"(["9.9.9",["editor_width",300,"editor_height",500,"something_atomic",12,"json_thing",{"answer":{"everything":42},"happy":true,"list":[1,0,2],"name":"Niels","nothing":null,"object":{"currency":"USD","value":42.99},"pi":3.141},"yes_no0",1,"yes_no1",1,"yes_no2",1,"yes_no3",1],["percent",0.5,"gain",0.0,"mode",2,"on_off",true]])"); + juce::MemoryBlock block; + chowdsp::JSONUtils::toMemoryBlock (jsonState, block); + + StateWithNewGroup state; + static_cast (state.params.newGroup.newParam) = newGainVal; + state.deserialize (std::move (block)); + REQUIRE_MESSAGE (state.params.newGroup.newParam->get() == Catch::Approx (3.3f).margin (1.0e-6f), "Added param value is incorrect"); + } + + SECTION ("Double of Same Type Test") + { + static constexpr float percentVal1 = 0.25f; + static constexpr float gainVal1 = -22.0f; + static constexpr float percentVal2 = 0.85f; + static constexpr float gainVal2 = -29.0f; + static constexpr int choiceVal = 0; + static constexpr bool boolVal = false; + + const auto jsonState = nlohmann::json::parse (R"(["9.9.9",[],["level_params1percent",0.25,"level_params1gain",-22.0,"level_params2percent",0.8500000238418579,"level_params2gain",-29.0,"mode",0,"on_off",false]])"); + juce::MemoryBlock block; + chowdsp::JSONUtils::toMemoryBlock (jsonState, block); + + StateWithTripleOfSameType state {}; + state.deserialize (std::move (block)); + REQUIRE_MESSAGE (juce::approximatelyEqual (state.params.levelParams1.percent->get(), percentVal1), "Percent value 1 is incorrect"); + REQUIRE_MESSAGE (juce::approximatelyEqual (state.params.levelParams1.gain->get(), gainVal1), "Gain value 1 is incorrect"); + REQUIRE_MESSAGE (juce::approximatelyEqual (state.params.levelParams2.percent->get(), percentVal2), "Percent value 2 is incorrect"); + REQUIRE_MESSAGE (juce::approximatelyEqual (state.params.levelParams2.gain->get(), gainVal2), "Gain value 2 is incorrect"); + REQUIRE_MESSAGE (juce::approximatelyEqual (state.params.levelParams3.percent->get(), 0.5f), "Percent value 3 is incorrect"); + REQUIRE_MESSAGE (juce::approximatelyEqual (state.params.levelParams3.gain->get(), 0.0f), "Gain value 3 is incorrect"); + REQUIRE_MESSAGE (state.params.mode->getIndex() == choiceVal, "Choice value is incorrect"); + REQUIRE_MESSAGE (state.params.onOff->get() == boolVal, "Bool value is incorrect"); + } + + SECTION ("Added Non-Parameter Field Test") + { + static constexpr int width = 200; + static constexpr int height = 150; + + const auto jsonState = nlohmann::json::parse (R"(["9.9.9",["editor_width",200,"editor_height",150,"something_atomic",12,"json_thing",{"answer":{"everything":42},"happy":true,"list":[1,0,2],"name":"Niels","nothing":null,"object":{"currency":"USD","value":42.99},"pi":3.141},"yes_no0",1,"yes_no1",1,"yes_no2",1,"yes_no3",1],["percent",0.5,"gain",0.0,"mode",2,"on_off",true]])"); + juce::MemoryBlock block; + chowdsp::JSONUtils::toMemoryBlock (jsonState, block); + + StateWithNewNonParameterField state; + state.deserialize (std::move (block)); REQUIRE_MESSAGE (state.nonParams.editorWidth.get() == width, "Editor width is incorrect"); REQUIRE_MESSAGE (state.nonParams.editorHeight.get() == height, "Editor height is incorrect"); REQUIRE_MESSAGE (state.nonParams.randomString.get() == juce::String { "default" }, "Added field is incorrect"); diff --git a/tests/plugin_tests/chowdsp_plugin_state_test/VersionStreamingTest.cpp b/tests/plugin_tests/chowdsp_plugin_state_test/VersionStreamingTest.cpp index aa5e43f99..0656c4ccc 100644 --- a/tests/plugin_tests/chowdsp_plugin_state_test/VersionStreamingTest.cpp +++ b/tests/plugin_tests/chowdsp_plugin_state_test/VersionStreamingTest.cpp @@ -89,7 +89,26 @@ TEST_CASE ("Version Streaming Test", "[plugin][state][version]") State state {}; REQUIRE (state.params.innerParams.param->get() == false); REQUIRE (juce::approximatelyEqual (state.nonParams.editorSize.get(), 1.0f)); - state.deserialize (stateBlock); + state.deserialize (std::move (stateBlock)); + REQUIRE (state.params.innerParams.param->get() == true); + REQUIRE (juce::approximatelyEqual (state.nonParams.editorSize.get(), 1.5f)); + } + + SECTION ("Version Streaming with Legacy State Serialization") + { + static_assert (chowdsp::currentPluginVersion == "9.9.9"_v, "Tests are tuned for JucePlugin_VersionString = 9.9.9"); + + using State = chowdsp::PluginStateImpl; + + const auto jsonState = nlohmann::json::parse (R"(["9.9.9",["editor_size",1.0],["bool",false,"bool2",false]])"); + juce::MemoryBlock stateBlock; + chowdsp::JSONUtils::toMemoryBlock (jsonState, stateBlock); + + // check new state + State state {}; + REQUIRE (state.params.innerParams.param->get() == false); + REQUIRE (juce::approximatelyEqual (state.nonParams.editorSize.get(), 1.0f)); + state.deserialize (std::move (stateBlock)); REQUIRE (state.params.innerParams.param->get() == true); REQUIRE (juce::approximatelyEqual (state.nonParams.editorSize.get(), 1.5f)); } diff --git a/tests/plugin_tests/chowdsp_presets_v2_test/PresetManagerTest.cpp b/tests/plugin_tests/chowdsp_presets_v2_test/PresetManagerTest.cpp index 21db6f62c..35114c4b6 100644 --- a/tests/plugin_tests/chowdsp_presets_v2_test/PresetManagerTest.cpp +++ b/tests/plugin_tests/chowdsp_presets_v2_test/PresetManagerTest.cpp @@ -235,7 +235,28 @@ TEST_CASE ("Preset Manager Test", "[plugin][presets][state]") } ScopedPresetManager presetMgr {}; - presetMgr.state.deserialize (state); + presetMgr.state.deserialize (std::move (state)); + REQUIRE_MESSAGE (juce::approximatelyEqual (presetMgr.getFloatParam(), otherValue), "Preset state is overriding parameter state!"); + REQUIRE (presetMgr->getCurrentPreset() == nullptr); + REQUIRE (presetMgr->getIsPresetDirty()); + } + + SECTION ("Null State JSON Test") + { + static constexpr float otherValue = 0.15f; + + chowdsp::json json_state; + { + ScopedPresetManager presetMgr {}; + + presetMgr.setFloatParam (otherValue); + REQUIRE (juce::approximatelyEqual (presetMgr.getFloatParam(), otherValue)); + + json_state = decltype (presetMgr.state)::serialize (presetMgr.state); + } + + ScopedPresetManager presetMgr {}; + decltype (presetMgr.state)::deserialize (json_state, presetMgr.state); REQUIRE_MESSAGE (juce::approximatelyEqual (presetMgr.getFloatParam(), otherValue), "Preset state is overriding parameter state!"); REQUIRE (presetMgr->getCurrentPreset() == nullptr); REQUIRE (presetMgr->getIsPresetDirty()); @@ -261,7 +282,33 @@ TEST_CASE ("Preset Manager Test", "[plugin][presets][state]") } ScopedPresetManager presetMgr {}; - presetMgr.state.deserialize (state); + presetMgr.state.deserialize (std::move (state)); + REQUIRE_MESSAGE (juce::approximatelyEqual (presetMgr.getFloatParam(), otherValue), "Preset state is overriding parameter state!"); + REQUIRE (*presetMgr->getCurrentPreset() == preset); + REQUIRE (presetMgr->getIsPresetDirty()); + } + + SECTION ("Preset State JSON Test") + { + static constexpr float testValue = 0.05f; + static constexpr float otherValue = 0.15f; + auto preset = saveUserPreset ("test.preset", testValue); + + chowdsp::json json_state; + { + ScopedPresetManager presetMgr {}; + presetMgr->addPresets ({ chowdsp::presets::Preset { preset } }); + presetMgr->loadPreset (presetMgr->getPresetTree().getRootNode().first_child->value.leaf()); + REQUIRE (juce::approximatelyEqual (presetMgr.getFloatParam(), testValue)); + + presetMgr.setFloatParam (otherValue); + REQUIRE (juce::approximatelyEqual (presetMgr.getFloatParam(), otherValue)); + + json_state = decltype (presetMgr.state)::serialize (presetMgr.state); + } + + ScopedPresetManager presetMgr {}; + decltype (presetMgr.state)::deserialize (json_state, presetMgr.state); REQUIRE_MESSAGE (juce::approximatelyEqual (presetMgr.getFloatParam(), otherValue), "Preset state is overriding parameter state!"); REQUIRE (*presetMgr->getCurrentPreset() == preset); REQUIRE (presetMgr->getIsPresetDirty()); diff --git a/tests/plugin_tests/chowdsp_version_test/VersionUtilsTest.cpp b/tests/plugin_tests/chowdsp_version_test/VersionUtilsTest.cpp index 13d352f46..98480d5c9 100644 --- a/tests/plugin_tests/chowdsp_version_test/VersionUtilsTest.cpp +++ b/tests/plugin_tests/chowdsp_version_test/VersionUtilsTest.cpp @@ -113,15 +113,19 @@ TEST_CASE ("Version Test", "[plugin][version]") using namespace chowdsp::version_literals; static constexpr auto v1_2_3 = "1.2.3"_v; static_assert (v1_2_3.getVersionHint() == 10203); + static_assert (chowdsp::Version::fromVersionHint (v1_2_3.getVersionHint()) == v1_2_3); static constexpr auto v50_49_5 = chowdsp::Version { 50, 49, 5 }; static_assert (v50_49_5.getVersionHint() == 504905); + static_assert (chowdsp::Version::fromVersionHint (v50_49_5.getVersionHint()) == v50_49_5); const auto v99_99_99 = "99.99.99"_v; REQUIRE (v99_99_99.getVersionHint() == 999999); + REQUIRE (chowdsp::Version::fromVersionHint (v99_99_99.getVersionHint()) == v99_99_99); const auto v5_49_15 = chowdsp::Version { 5, 49, 15 }; REQUIRE (v5_49_15.getVersionHint() == 54915); + REQUIRE (chowdsp::Version::fromVersionHint (v5_49_15.getVersionHint()) == v5_49_15); REQUIRE (chowdsp::Version {}.getVersionHint() == 0); }