diff --git a/CMakeLists.txt b/CMakeLists.txt index e8d38ed1..633a333e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -127,6 +127,9 @@ if (ALTVANE_BUILD_TESTS) endif () add_subdirectory(libs/ui) +if (ALTVANE_BUILD_TESTS) + add_subdirectory(libs/testing) +endif () foreach (product ${ALTVANE_PRODUCTS}) if (NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/products/${product}/CMakeLists.txt") diff --git a/README.md b/README.md index 4a96e12d..ff9e1eb5 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ CMakePresets.json mac-arm64-debug / mac-arm64-release cmake/ AltvanePlugin.cmake (altvane_add_plugin), AltvaneVersion.cmake, AltvaneTests.cmake, ... third_party/ JUCE, highway, nlopt (git submodules) libs/ui/ altvane::ui: design tokens, fonts, look-and-feel, brand mark, widgets, editor shell, presets +libs/testing/ what every product's tests link: alloc_counter, JUCE set up for the whole test run products// one directory per product: CMakeLists.txt, source/, tests/, tools/, packaging/, docs/ packaging/ installer scripts shared by every product tools/icons/ renders each product's app icon and installer art diff --git a/cmake/AltvaneTests.cmake b/cmake/AltvaneTests.cmake index bcb46f02..db113c9f 100644 --- a/cmake/AltvaneTests.cmake +++ b/cmake/AltvaneTests.cmake @@ -3,7 +3,7 @@ # altvane_add_tests( ) # # builds every .cpp under into Tests, linked against the plugin's compiled code (see -# altvane_link_plugin_code), and registers the cases with CTest. +# altvane_link_plugin_code) and the shared test helpers (libs/testing), and registers the cases with CTest. # Required for ctest (which is just an easier way to run in cross-platform CI) # See: https://github.com/catchorg/Catch2/issues/2026 @@ -28,7 +28,7 @@ function(altvane_add_tests plugin_target test_dir) set(tests "${plugin_target}Tests") add_executable(${tests} ${test_files}) altvane_link_plugin_code(${tests} ${plugin_target}) - target_link_libraries(${tests} PRIVATE Catch2::Catch2WithMain) + target_link_libraries(${tests} PRIVATE Catch2::Catch2WithMain altvane_testing) set_target_properties(${tests} PROPERTIES XCODE_GENERATE_SCHEME ON) catch_discover_tests(${tests}) diff --git a/libs/testing/CMakeLists.txt b/libs/testing/CMakeLists.txt new file mode 100644 index 00000000..a9da491b --- /dev/null +++ b/libs/testing/CMakeLists.txt @@ -0,0 +1,15 @@ +# altvane_testing: what every product's unit tests share +# (alloc_counter, which holds the audio thread to no allocations, and the Catch2 listener that keeps JUCE initialised +# for the whole run). altvane_add_tests (cmake/AltvaneTests.cmake) links it into each test binary. +# Include as "altvane_testing/.hpp". + +# an INTERFACE library: the sources compile into each test binary, so the listener's registration and the operator +# new replacement are always linked in (a static library would drop them, as nothing refers to them); JUCE's headers +# and configuration come from the plugin the tests link (altvane_link_plugin_code) +add_library(altvane_testing INTERFACE) +file(GLOB_RECURSE testing_files CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/source/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/source/*.hpp") +target_sources(altvane_testing INTERFACE ${testing_files}) +source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}/source" PREFIX "" FILES ${testing_files}) +target_include_directories(altvane_testing INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/source") +target_compile_features(altvane_testing INTERFACE cxx_std_20) +target_link_libraries(altvane_testing INTERFACE Catch2::Catch2) diff --git a/products/eq/tests/alloc_counter.cpp b/libs/testing/source/altvane_testing/alloc_counter.cpp similarity index 81% rename from products/eq/tests/alloc_counter.cpp rename to libs/testing/source/altvane_testing/alloc_counter.cpp index 7bbf217d..a7d42b15 100644 --- a/products/eq/tests/alloc_counter.cpp +++ b/libs/testing/source/altvane_testing/alloc_counter.cpp @@ -1,11 +1,11 @@ // Copyright (C) 2026 - Altvane -// This file is part of Altvane EQ +// This file is part of Altvane // -// Altvane EQ is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License Version 3 as published by the Free Software Foundation. +// Altvane is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License Version 3 as published by the Free Software Foundation. // -// Altvane EQ is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. +// Altvane is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. // -// You should have received a copy of the GNU Affero General Public License along with Altvane EQ. If not, see . +// You should have received a copy of the GNU Affero General Public License along with Altvane. If not, see . #include "alloc_counter.hpp" #include diff --git a/libs/testing/source/altvane_testing/alloc_counter.hpp b/libs/testing/source/altvane_testing/alloc_counter.hpp new file mode 100644 index 00000000..9d56cb48 --- /dev/null +++ b/libs/testing/source/altvane_testing/alloc_counter.hpp @@ -0,0 +1,30 @@ +// Copyright (C) 2026 - Altvane +// This file is part of Altvane +// +// Altvane is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License Version 3 as published by the Free Software Foundation. +// +// Altvane is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License along with Altvane. If not, see . +#pragma once + +#include + +/** + * counts heap allocations made by the current thread while a Scope is alive (operator new is replaced in + * alloc_counter.cpp for the whole test binary) + */ +namespace alloc_counter { + void begin(); + + size_t end(); + + class Scope { + public: + Scope() { begin(); } + + ~Scope() { end(); } + + size_t count() const; + }; +} diff --git a/products/eq/tests/juce_init.cpp b/libs/testing/source/altvane_testing/juce_init.cpp similarity index 63% rename from products/eq/tests/juce_init.cpp rename to libs/testing/source/altvane_testing/juce_init.cpp index fcd1576c..ade1674b 100644 --- a/products/eq/tests/juce_init.cpp +++ b/libs/testing/source/altvane_testing/juce_init.cpp @@ -1,11 +1,11 @@ // Copyright (C) 2026 - Altvane, based on ZLEqualizer by zsliu98 -// This file is part of Altvane EQ +// This file is part of Altvane // -// Altvane EQ is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License Version 3 as published by the Free Software Foundation. +// Altvane is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License Version 3 as published by the Free Software Foundation. // -// Altvane EQ is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. +// Altvane is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. // -// You should have received a copy of the GNU Affero General Public License along with Altvane EQ. If not, see . +// You should have received a copy of the GNU Affero General Public License along with Altvane. If not, see . #include #include #include diff --git a/libs/ui/source/altvane_ui/util/peak_exchange.hpp b/libs/ui/source/altvane_ui/util/peak_exchange.hpp new file mode 100644 index 00000000..f08448d2 --- /dev/null +++ b/libs/ui/source/altvane_ui/util/peak_exchange.hpp @@ -0,0 +1,49 @@ +// Copyright (C) 2026 - Altvane +// This file is part of Altvane +// +// Altvane is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License Version 3 as published by the Free Software Foundation. +// +// Altvane is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License along with Altvane. If not, see . + +#pragma once + +#include +#include +#include + +namespace altvane::ui { + /** + * a stereo peak meter's feed: the audio thread raises each channel's peak, the message thread takes the peaks + * once a frame and starts them again from silence (lock-free, no allocation) + */ + class PeakExchange { + public: + /** + * audio thread: raise `channel`'s peak to `magnitude` (linear) if it is louder + */ + void store(const size_t channel, const float magnitude) { + auto& peak = peaks_[channel]; + auto current = peak.load(std::memory_order::relaxed); + while (magnitude > current && !peak.compare_exchange_weak(current, magnitude, std::memory_order::relaxed)) { + } + } + + /** + * message thread: the peaks since the last call, in dB (-120 for silence) + */ + std::array take() { + std::array result{}; + for (size_t c = 0; c < 2; ++c) { + result[c] = juce::Decibels::gainToDecibels(peaks_[c].exchange(0.f, std::memory_order::relaxed), -120.f); + } + return result; + } + + private: + static_assert(std::atomic::is_always_lock_free, "PeakExchange requires lock-free atomic float"); + + std::array, 2> peaks_{}; + }; +} diff --git a/libs/ui/source/altvane_ui/util/value_format.cpp b/libs/ui/source/altvane_ui/util/value_format.cpp index fa7d612a..1714ca69 100644 --- a/libs/ui/source/altvane_ui/util/value_format.cpp +++ b/libs/ui/source/altvane_ui/util/value_format.cpp @@ -56,6 +56,11 @@ namespace altvane::ui::format { return with_unit ? s + " dB" : s; } + juce::String decibels(const double db, const bool force_sign) { + const auto sign = db < -.04 ? kMinus : force_sign && db > .04 ? juce::String("+") : juce::String(); + return sign + juce::String(std::abs(db), 1) + " dB"; + } + juce::String q(const double q) { return juce::String(q, q < 10.0 ? 2 : 1); } @@ -97,6 +102,11 @@ namespace altvane::ui::format { return std::isfinite(v) ? std::optional{v} : std::nullopt; } + std::optional parseLenient(const juce::String& text) { + const auto t = text.replace(kMinus, "-").retainCharacters("+-.0123456789"); + return t.containsAnyOf("0123456789") ? std::optional{t.getDoubleValue()} : std::nullopt; + } + std::optional parseFreq(const juce::String& text) { auto t = normalise(text).removeCharacters(" "); if (t.isEmpty()) { diff --git a/libs/ui/source/altvane_ui/util/value_format.hpp b/libs/ui/source/altvane_ui/util/value_format.hpp index 32b55d66..dd666237 100644 --- a/libs/ui/source/altvane_ui/util/value_format.hpp +++ b/libs/ui/source/altvane_ui/util/value_format.hpp @@ -19,6 +19,11 @@ namespace altvane::ui::format { juce::String gain(double db, bool with_unit = true, bool force_sign = true); + /** + * dB to one decimal, "−3.5 dB", and "+3.5 dB" with force_sign; no sign within 0.04 dB of zero + */ + juce::String decibels(double db, bool force_sign); + juce::String q(double q); juce::String ms(double ms); @@ -36,4 +41,10 @@ namespace altvane::ui::format { * parse "-3", "+3.5", "3.5 dB", using either a hyphen or a minus sign */ std::optional parseNumber(const juce::String& text); + + /** + * a number in loosely typed text, keeping only its digits, signs and points: "−3.5 dB" is -3.5, "1,5" is 15 + * (nullopt without digits) + */ + std::optional parseLenient(const juce::String& text); } diff --git a/libs/ui/source/altvane_ui/widgets/output_rail.cpp b/libs/ui/source/altvane_ui/widgets/output_rail.cpp new file mode 100644 index 00000000..6d744188 --- /dev/null +++ b/libs/ui/source/altvane_ui/widgets/output_rail.cpp @@ -0,0 +1,62 @@ +// Copyright (C) 2026 - Altvane +// This file is part of Altvane +// +// Altvane is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License Version 3 as published by the Free Software Foundation. +// +// Altvane is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License along with Altvane. If not, see . +#include "output_rail.hpp" + +#include "../theme/fonts.hpp" +#include "../theme/tokens.hpp" + +namespace altvane::ui { + using namespace tokens; + + namespace { + juce::Font captionFont() { + return Fonts::get().font(9.5f, Fonts::kSemiBold).withExtraKerningFactor(.08f); + } + } + + OutputRail::OutputRail(const juce::AudioProcessor& processor, std::function()> take_peaks) : + processor_(processor), take_peaks_(std::move(take_peaks)) { + setTitle("Output meter"); + } + + void OutputRail::resized() { + auto b = getLocalBounds().toFloat().reduced(6.f, 8.f); + label_area_ = b.removeFromTop(16.f); + b.removeFromTop(8.f); + peak_area_ = b.removeFromBottom(16.f); + b.removeFromBottom(6.f); + meter_area_ = b.withSizeKeepingCentre(14.f, b.getHeight()); + } + + void OutputRail::tick(const double dt) { + const auto peaks = take_peaks_(); + if (meter_.tick(dt, peaks[0], processor_.getMainBusNumOutputChannels() > 1 ? peaks[1] : peaks[0])) { + repaint(); + } + } + + void OutputRail::paint(juce::Graphics& g) { + const auto b = getLocalBounds().toFloat(); + g.setColour(colour::kWindow); + g.fillRect(b); + g.setColour(colour::kHairline); + g.fillRect(juce::Rectangle(0.f, 0.f, 1.f, b.getHeight())); + g.setFont(captionFont()); + g.setColour(colour::kTextTertiary); + g.drawText("OUT", label_area_, juce::Justification::centred); + meter_.paint(g, meter_area_, peak_area_); + } + + void OutputRail::mouseUp(const juce::MouseEvent& e) { + if (meter_area_.expanded(8.f, 0.f).contains(e.position) || peak_area_.contains(e.position)) { + meter_.resetPeak(); + repaint(); + } + } +} diff --git a/libs/ui/source/altvane_ui/widgets/output_rail.hpp b/libs/ui/source/altvane_ui/widgets/output_rail.hpp new file mode 100644 index 00000000..13f673b6 --- /dev/null +++ b/libs/ui/source/altvane_ui/widgets/output_rail.hpp @@ -0,0 +1,43 @@ +// Copyright (C) 2026 - Altvane +// This file is part of Altvane +// +// Altvane is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License Version 3 as published by the Free Software Foundation. +// +// Altvane is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License along with Altvane. If not, see . +#pragma once + +#include +#include +#include + +#include "level_meter.hpp" + +namespace altvane::ui { + /** + * an editor's right rail: the output meter under an "OUT" caption; a click on the meter resets its peak hold + * a mono main output shows its one channel in both bars + */ + class OutputRail final : public juce::Component { + public: + /** + * @param take_peaks the output peaks since the last call, in dB (e.g. a PeakExchange's take) + */ + OutputRail(const juce::AudioProcessor& processor, std::function()> take_peaks); + + void tick(double dt); + + void paint(juce::Graphics& g) override; + + void resized() override; + + void mouseUp(const juce::MouseEvent& e) override; + + private: + const juce::AudioProcessor& processor_; + std::function()> take_peaks_; + LevelMeter meter_; + juce::Rectangle label_area_, meter_area_, peak_area_; + }; +} diff --git a/products/compressor/tests/alloc_counter.cpp b/products/compressor/tests/alloc_counter.cpp deleted file mode 100644 index 1453691d..00000000 --- a/products/compressor/tests/alloc_counter.cpp +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (C) 2026 - Altvane -// This file is part of Altvane Compressor -// -// Altvane Limiter is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License Version 3 as published by the Free Software Foundation. -// -// Altvane Limiter is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License along with Altvane Limiter. If not, see . -#include "alloc_counter.hpp" - -#include -#include -#if defined(_WIN32) -#include -#endif - -namespace { - thread_local bool counting = false; - thread_local size_t allocations = 0; - - void* allocate(const std::size_t size) { - if (counting) { - ++allocations; - } - if (void* p = std::malloc(size == 0 ? 1 : size)) { - return p; - } - throw std::bad_alloc(); - } - - void* allocateAligned(const std::size_t size, const std::align_val_t alignment) { - if (counting) { - ++allocations; - } - const auto a = static_cast(alignment); -#if defined(_WIN32) - // the MSVC runtime has no aligned_alloc; its aligned blocks must be freed with _aligned_free - void* p = _aligned_malloc(size == 0 ? 1 : size, a); -#else - void* p = std::aligned_alloc(a, (size + a - 1) / a * a); -#endif - if (p != nullptr) { - return p; - } - throw std::bad_alloc(); - } - - void freeAligned(void* p) { -#if defined(_WIN32) - _aligned_free(p); -#else - std::free(p); -#endif - } -} - -namespace alloc_counter { - void begin() { - allocations = 0; - counting = true; - } - - size_t end() { - counting = false; - return allocations; - } - - size_t Scope::count() const { - return allocations; - } -} - -void* operator new(const std::size_t size) { return allocate(size); } -void* operator new[](const std::size_t size) { return allocate(size); } -void* operator new(const std::size_t size, const std::align_val_t a) { return allocateAligned(size, a); } -void* operator new[](const std::size_t size, const std::align_val_t a) { return allocateAligned(size, a); } -void operator delete(void* p) noexcept { std::free(p); } -void operator delete[](void* p) noexcept { std::free(p); } -void operator delete(void* p, std::size_t) noexcept { std::free(p); } -void operator delete[](void* p, std::size_t) noexcept { std::free(p); } -void operator delete(void* p, std::align_val_t) noexcept { freeAligned(p); } -void operator delete[](void* p, std::align_val_t) noexcept { freeAligned(p); } -void operator delete(void* p, std::size_t, std::align_val_t) noexcept { freeAligned(p); } -void operator delete[](void* p, std::size_t, std::align_val_t) noexcept { freeAligned(p); } diff --git a/products/compressor/tests/alloc_counter.hpp b/products/compressor/tests/alloc_counter.hpp deleted file mode 100644 index c4837d5e..00000000 --- a/products/compressor/tests/alloc_counter.hpp +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (C) 2026 - Altvane -// This file is part of Altvane Compressor -// -// Altvane Limiter is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License Version 3 as published by the Free Software Foundation. -// -// Altvane Limiter is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License along with Altvane Limiter. If not, see . -#pragma once - -#include - -/** - * counts heap allocations made by the current thread while a Scope is alive (operator new is replaced in - * alloc_counter.cpp for the whole test binary) - */ -namespace alloc_counter { - void begin(); - - size_t end(); - - class Scope { - public: - Scope() { begin(); } - - ~Scope() { end(); } - - size_t count() const; - }; -} diff --git a/products/compressor/tests/juce_init.cpp b/products/compressor/tests/juce_init.cpp deleted file mode 100644 index bc4f5053..00000000 --- a/products/compressor/tests/juce_init.cpp +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (C) 2026 - Altvane, based on ZLCompressor by zsliu98 -// This file is part of Altvane Compressor -// -// Altvane Compressor is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License Version 3 as published by the Free Software Foundation. -// -// Altvane Compressor is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License along with Altvane Compressor. If not, see . -#include -#include -#include - -/** - * the processor and editor need JUCE (message manager, desktop) for the whole run; - * scoping it to the run gives a clean shutdown before JUCE's leak detector checks - */ -class JuceRunListener final : public Catch::EventListenerBase { -public: - using EventListenerBase::EventListenerBase; - - void testRunStarting(const Catch::TestRunInfo&) override { - juce_ = std::make_unique(); - } - - void testRunEnded(const Catch::TestRunStats&) override { - juce_.reset(); - } - -private: - std::unique_ptr juce_; -}; - -CATCH_REGISTER_LISTENER(JuceRunListener) diff --git a/products/compressor/tests/processor_tests.cpp b/products/compressor/tests/processor_tests.cpp index 3199578e..0c9d9d79 100644 --- a/products/compressor/tests/processor_tests.cpp +++ b/products/compressor/tests/processor_tests.cpp @@ -10,7 +10,8 @@ #include #include -#include "alloc_counter.hpp" +#include + #include "test_helpers.hpp" using Catch::Matchers::WithinAbs; diff --git a/products/deesser/source/PluginProcessor.cpp b/products/deesser/source/PluginProcessor.cpp index 59d8c88e..8095fc4f 100644 --- a/products/deesser/source/PluginProcessor.cpp +++ b/products/deesser/source/PluginProcessor.cpp @@ -12,12 +12,6 @@ namespace altvane::deesser { namespace { - void storePeak(std::atomic& peak, const float value) { - auto current = peak.load(std::memory_order::relaxed); - while (value > current && !peak.compare_exchange_weak(current, value, std::memory_order::relaxed)) { - } - } - int choice(const std::atomic* value) { return juce::roundToInt(value->load(std::memory_order::relaxed)); } @@ -140,7 +134,7 @@ namespace altvane::deesser { return; } for (int c = 0; c < channels; ++c) { - storePeak(input_peak_[static_cast(c)], main.getMagnitude(c, 0, n)); + input_peaks_.store(static_cast(c), main.getMagnitude(c, 0, n)); } // the side chain's view keeps its channel pointers inside itself: it has to outlive the engine's use of them @@ -156,7 +150,7 @@ namespace altvane::deesser { engine_.process(main.getArrayOfWritePointers(), channels, side, side_channels, n); for (int c = 0; c < channels; ++c) { - storePeak(output_peak_[static_cast(c)], main.getMagnitude(c, 0, n)); + output_peaks_.store(static_cast(c), main.getMagnitude(c, 0, n)); } // anything beyond the main output (e.g. the side chain's channels) is not ours to pass on for (auto c = getTotalNumOutputChannels(); c < buffer.getNumChannels(); ++c) { @@ -164,22 +158,6 @@ namespace altvane::deesser { } } - std::array PluginProcessor::takePeaks(std::array, 2>& peaks) { - std::array result{}; - for (size_t c = 0; c < 2; ++c) { - result[c] = juce::Decibels::gainToDecibels(peaks[c].exchange(0.f, std::memory_order::relaxed), -120.f); - } - return result; - } - - std::array PluginProcessor::takeInputPeaks() { - return takePeaks(input_peak_); - } - - std::array PluginProcessor::takeOutputPeaks() { - return takePeaks(output_peak_); - } - void PluginProcessor::getStateInformation(juce::MemoryBlock& dest_data) { auto state = parameters_.copyState(); state.setProperty("version", VERSION, nullptr); diff --git a/products/deesser/source/PluginProcessor.hpp b/products/deesser/source/PluginProcessor.hpp index a6061491..5c9037e7 100644 --- a/products/deesser/source/PluginProcessor.hpp +++ b/products/deesser/source/PluginProcessor.hpp @@ -10,6 +10,7 @@ #include #include +#include #include "params.hpp" #include "dsp/engine.hpp" @@ -93,9 +94,9 @@ namespace altvane::deesser { /** * the input and output peaks since the last call, in dB (message thread) */ - std::array takeInputPeaks(); + std::array takeInputPeaks() { return input_peaks_.take(); } - std::array takeOutputPeaks(); + std::array takeOutputPeaks() { return output_peaks_.take(); } private: dsp::Engine engine_; @@ -113,13 +114,12 @@ namespace altvane::deesser { std::atomic* side_chain_{nullptr}; std::atomic* output_{nullptr}; std::atomic listen_{0}; - std::array, 2> input_peak_{}, output_peak_{}; + ui::PeakExchange input_peaks_, output_peaks_; dsp::Controls readControls(bool force_bypass) const; void process(juce::AudioBuffer& buffer, bool force_bypass); void updateLatency(); void parameterChanged(const juce::String& id, float value) override; void timerCallback() override; - static std::array takePeaks(std::array, 2>& peaks); }; } diff --git a/products/deesser/source/params.cpp b/products/deesser/source/params.cpp index f8432acb..ba476ba5 100644 --- a/products/deesser/source/params.cpp +++ b/products/deesser/source/params.cpp @@ -15,7 +15,7 @@ namespace altvane::deesser::params { constexpr int kVersion = 1; float parseNumber(const juce::String& t) { - return t.replace(juce::String::fromUTF8("\xe2\x88\x92"), "-").retainCharacters("+-.0123456789").getFloatValue(); + return static_cast(ui::format::parseLenient(t).value_or(0.0)); } float parseHz(const juce::String& t) { @@ -59,9 +59,7 @@ namespace altvane::deesser::params { } juce::String formatDb(const float db) { - // the typographic minus, as everywhere else in the interface - const auto sign = db < -.04f ? juce::String::fromUTF8("\xe2\x88\x92") : juce::String(); - return sign + juce::String(std::abs(db), 1) + " dB"; + return ui::format::decibels(db, false); } juce::String formatHz(const float hz) { diff --git a/products/deesser/tests/alloc_counter.cpp b/products/deesser/tests/alloc_counter.cpp deleted file mode 100644 index d418498c..00000000 --- a/products/deesser/tests/alloc_counter.cpp +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (C) 2026 - Altvane -// This file is part of Altvane De-Esser -// -// Altvane De-Esser is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License Version 3 as published by the Free Software Foundation. -// -// Altvane De-Esser is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License along with Altvane De-Esser. If not, see . -#include "alloc_counter.hpp" - -#include -#include -#ifdef _WIN32 -#include -#endif - -namespace { - thread_local bool counting = false; - thread_local size_t allocations = 0; - - void* allocate(const std::size_t size) { - if (counting) { - ++allocations; - } - if (void* p = std::malloc(size == 0 ? 1 : size)) { - return p; - } - throw std::bad_alloc(); - } - - void* allocateAligned(const std::size_t size, const std::align_val_t alignment) { - if (counting) { - ++allocations; - } - const auto a = static_cast(alignment); -#ifdef _WIN32 - // the Windows C runtime has no aligned_alloc; its aligned blocks are freed with _aligned_free - if (void* p = _aligned_malloc(size == 0 ? 1 : size, a)) { -#else - if (void* p = std::aligned_alloc(a, (size + a - 1) / a * a)) { -#endif - return p; - } - throw std::bad_alloc(); - } - - void freeAligned(void* p) { -#ifdef _WIN32 - _aligned_free(p); -#else - std::free(p); -#endif - } -} - -namespace alloc_counter { - void begin() { - allocations = 0; - counting = true; - } - - size_t end() { - counting = false; - return allocations; - } - - size_t Scope::count() const { - return allocations; - } -} - -void* operator new(const std::size_t size) { return allocate(size); } -void* operator new[](const std::size_t size) { return allocate(size); } -void* operator new(const std::size_t size, const std::align_val_t a) { return allocateAligned(size, a); } -void* operator new[](const std::size_t size, const std::align_val_t a) { return allocateAligned(size, a); } -void operator delete(void* p) noexcept { std::free(p); } -void operator delete[](void* p) noexcept { std::free(p); } -void operator delete(void* p, std::size_t) noexcept { std::free(p); } -void operator delete[](void* p, std::size_t) noexcept { std::free(p); } -void operator delete(void* p, std::align_val_t) noexcept { freeAligned(p); } -void operator delete[](void* p, std::align_val_t) noexcept { freeAligned(p); } -void operator delete(void* p, std::size_t, std::align_val_t) noexcept { freeAligned(p); } -void operator delete[](void* p, std::size_t, std::align_val_t) noexcept { freeAligned(p); } diff --git a/products/deesser/tests/alloc_counter.hpp b/products/deesser/tests/alloc_counter.hpp deleted file mode 100644 index bcf667ef..00000000 --- a/products/deesser/tests/alloc_counter.hpp +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (C) 2026 - Altvane -// This file is part of Altvane De-Esser -// -// Altvane De-Esser is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License Version 3 as published by the Free Software Foundation. -// -// Altvane De-Esser is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License along with Altvane De-Esser. If not, see . -#pragma once - -#include - -/** - * counts heap allocations made by the current thread while a Scope is alive (operator new is replaced in - * alloc_counter.cpp for the whole test binary) - */ -namespace alloc_counter { - void begin(); - - size_t end(); - - class Scope { - public: - Scope() { begin(); } - - ~Scope() { end(); } - - size_t count() const; - }; -} diff --git a/products/deesser/tests/engine_tests.cpp b/products/deesser/tests/engine_tests.cpp index 22e3d372..77c95cfb 100644 --- a/products/deesser/tests/engine_tests.cpp +++ b/products/deesser/tests/engine_tests.cpp @@ -9,9 +9,10 @@ #include #include +#include + #include "PluginProcessor.hpp" #include "dsp/learner.hpp" -#include "alloc_counter.hpp" #include "signals.hpp" using namespace altvane::deesser; diff --git a/products/deesser/tests/juce_init.cpp b/products/deesser/tests/juce_init.cpp deleted file mode 100644 index 057a86d4..00000000 --- a/products/deesser/tests/juce_init.cpp +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (C) 2026 - Altvane -// This file is part of Altvane De-Esser -// -// Altvane De-Esser is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License Version 3 as published by the Free Software Foundation. -// -// Altvane De-Esser is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License along with Altvane De-Esser. If not, see . -#include -#include -#include - -/** - * the processor and editor need JUCE (message manager, desktop) for the whole run; - * scoping it to the run gives a clean shutdown before JUCE's leak detector checks - */ -class JuceRunListener final : public Catch::EventListenerBase { -public: - using EventListenerBase::EventListenerBase; - - void testRunStarting(const Catch::TestRunInfo&) override { - juce_ = std::make_unique(); - } - - void testRunEnded(const Catch::TestRunStats&) override { - juce_.reset(); - } - -private: - std::unique_ptr juce_; -}; - -CATCH_REGISTER_LISTENER(JuceRunListener) diff --git a/products/dialogue/docs/ARCHITECTURE.md b/products/dialogue/docs/ARCHITECTURE.md index b422ddaa..d1d41f18 100644 --- a/products/dialogue/docs/ARCHITECTURE.md +++ b/products/dialogue/docs/ARCHITECTURE.md @@ -135,9 +135,9 @@ recovers from a skip, and a skip ends a switch in progress. `ui/editor` derives from `altvane::ui::EditorShell` (header, presets, A/B, undo, settings, scale and frame loop are shared with Altvane EQ). `ui/visualiser` draws the engine's `AnalysisFrame`s (64 log bands, weighted by how much of each bin a band covers, published through a `TriBuffer`) as stacked layers, and edits sensitivity nodes through -`ui::ParamEditor`. `ui/control_deck` holds the three stem cards; `ui/chrome` the footer and output rail; -`ui/theme/dialogue_tokens.hpp` the stem colours and sizes. Factory presets (`ui/factory_presets`) are installed once -into `Presets/Factory`. +`ui::ParamEditor`. `ui/control_deck` holds the three stem cards; `ui/chrome` the footer (the output rail is the shared +`ui::OutputRail`); `ui/theme/dialogue_tokens.hpp` the stem colours and sizes. Factory presets (`ui/factory_presets`) are +installed once into `Presets/Factory`. ## Verification diff --git a/products/dialogue/source/PluginProcessor.cpp b/products/dialogue/source/PluginProcessor.cpp index 56be6942..e700d642 100644 --- a/products/dialogue/source/PluginProcessor.cpp +++ b/products/dialogue/source/PluginProcessor.cpp @@ -11,14 +11,6 @@ #include "ui/editor.hpp" namespace altvane::dialogue { - namespace { - void storePeak(std::atomic& peak, const float value) { - auto current = peak.load(std::memory_order::relaxed); - while (value > current && !peak.compare_exchange_weak(current, value, std::memory_order::relaxed)) { - } - } - } - PluginProcessor::PluginProcessor() : AudioProcessor(BusesProperties() .withInput("Input", juce::AudioChannelSet::stereo(), true) @@ -142,7 +134,7 @@ namespace altvane::dialogue { } updateControls(); for (int c = 0; c < channels; ++c) { - storePeak(input_peak_[static_cast(c)], buffer.getMagnitude(c, 0, buffer.getNumSamples())); + input_peaks_.store(static_cast(c), buffer.getMagnitude(c, 0, buffer.getNumSamples())); } // what the 48 kHz round trip loses (above 24 kHz) follows the ambience, the stem it belongs to @@ -202,7 +194,7 @@ namespace altvane::dialogue { } } for (int c = 0; c < channels; ++c) { - storePeak(output_peak_[static_cast(c)], buffer.getMagnitude(c, 0, buffer.getNumSamples())); + output_peaks_.store(static_cast(c), buffer.getMagnitude(c, 0, buffer.getNumSamples())); } // a stereo stem output on a mono instance carries its stem on both channels if (channels == 1) { @@ -221,22 +213,6 @@ namespace altvane::dialogue { } } - std::array PluginProcessor::takePeaks(std::array, 2>& peaks) { - std::array result{}; - for (size_t c = 0; c < 2; ++c) { - result[c] = juce::Decibels::gainToDecibels(peaks[c].exchange(0.f, std::memory_order::relaxed), -120.f); - } - return result; - } - - std::array PluginProcessor::takeInputPeaks() { - return takePeaks(input_peak_); - } - - std::array PluginProcessor::takeOutputPeaks() { - return takePeaks(output_peak_); - } - void PluginProcessor::getStateInformation(juce::MemoryBlock& dest_data) { auto state = parameters_.copyState(); state.setProperty("version", VERSION, nullptr); diff --git a/products/dialogue/source/PluginProcessor.hpp b/products/dialogue/source/PluginProcessor.hpp index f36916f9..b7d9384d 100644 --- a/products/dialogue/source/PluginProcessor.hpp +++ b/products/dialogue/source/PluginProcessor.hpp @@ -10,6 +10,7 @@ #include #include +#include #include "params.hpp" #include "dsp/rate_bridge.hpp" @@ -90,9 +91,9 @@ namespace altvane::dialogue { /** * the input and output peaks since the last call, in dB (message thread) */ - std::array takeInputPeaks(); + std::array takeInputPeaks() { return input_peaks_.take(); } - std::array takeOutputPeaks(); + std::array takeOutputPeaks() { return output_peaks_.take(); } private: dsp::RateBridge bridge_; @@ -109,10 +110,9 @@ namespace altvane::dialogue { bool stem_outputs_{false}; juce::AudioBuffer stem_scratch_; juce::SmoothedValue bypass_mix_, output_gain_, residual_gain_, stem_residual_gain_; - std::array, 2> input_peak_{}, output_peak_{}; + ui::PeakExchange input_peaks_, output_peaks_; void updateControls(); void process(juce::AudioBuffer& buffer, bool force_bypass); - static std::array takePeaks(std::array, 2>& peaks); }; } diff --git a/products/dialogue/source/params.cpp b/products/dialogue/source/params.cpp index c0e50203..e65482be 100644 --- a/products/dialogue/source/params.cpp +++ b/products/dialogue/source/params.cpp @@ -26,8 +26,7 @@ namespace altvane::dialogue::params { if (t.trim().equalsIgnoreCase("off")) { return kLevelOff; } - return t.replace(juce::String::fromUTF8("\xe2\x88\x92"), "-") - .retainCharacters("+-.0123456789").getFloatValue(); + return static_cast(ui::format::parseLenient(t).value_or(0.0)); }) .withLabel("dB"); for (size_t s = 0; s < dsp::kNumStems; ++s) { diff --git a/products/dialogue/source/params.hpp b/products/dialogue/source/params.hpp index c78ee117..020d9285 100644 --- a/products/dialogue/source/params.hpp +++ b/products/dialogue/source/params.hpp @@ -9,6 +9,7 @@ #pragma once #include +#include #include "dsp/stems.hpp" @@ -51,8 +52,7 @@ namespace altvane::dialogue::params { if (db <= kLevelOff + .05f) { return "Off"; } - // the typographic minus, as everywhere else in the interface - return (db > .04f ? "+" : db < -.04f ? juce::String::fromUTF8("\xe2\x88\x92") : "") + juce::String(std::abs(db), 1) + " dB"; + return ui::format::decibels(db, true); } juce::AudioProcessorValueTreeState::ParameterLayout createLayout(); diff --git a/products/dialogue/source/ui/chrome.cpp b/products/dialogue/source/ui/chrome.cpp index fc399443..dbf560a9 100644 --- a/products/dialogue/source/ui/chrome.cpp +++ b/products/dialogue/source/ui/chrome.cpp @@ -155,45 +155,4 @@ namespace altvane::dialogue { g.setColour(colour::kTextSecondary); Fonts::get().drawTabular(g, latency_text_, value, juce::Justification::centredRight, type::kBody); } - - // ---------------------------------------------------------------- OutputRail - - OutputRail::OutputRail(PluginProcessor& p) : p_ref_(p) { - setTitle("Output meter"); - } - - void OutputRail::resized() { - auto b = getLocalBounds().toFloat().reduced(6.f, 8.f); - label_area_ = b.removeFromTop(16.f); - b.removeFromTop(8.f); - peak_area_ = b.removeFromBottom(16.f); - b.removeFromBottom(6.f); - meter_area_ = b.withSizeKeepingCentre(14.f, b.getHeight()); - } - - void OutputRail::tick(const double dt) { - const auto peaks = p_ref_.takeOutputPeaks(); - if (meter_.tick(dt, peaks[0], p_ref_.getMainBusNumOutputChannels() > 1 ? peaks[1] : peaks[0])) { - repaint(); - } - } - - void OutputRail::paint(juce::Graphics& g) { - const auto b = getLocalBounds().toFloat(); - g.setColour(colour::kWindow); - g.fillRect(b); - g.setColour(colour::kHairline); - g.fillRect(juce::Rectangle(0.f, 0.f, 1.f, b.getHeight())); - g.setFont(captionFont()); - g.setColour(colour::kTextTertiary); - g.drawText("OUT", label_area_, juce::Justification::centred); - meter_.paint(g, meter_area_, peak_area_); - } - - void OutputRail::mouseUp(const juce::MouseEvent& e) { - if (meter_area_.expanded(8.f, 0.f).contains(e.position) || peak_area_.contains(e.position)) { - meter_.resetPeak(); - repaint(); - } - } } diff --git a/products/dialogue/source/ui/chrome.hpp b/products/dialogue/source/ui/chrome.hpp index 9c05015a..04e1efb6 100644 --- a/products/dialogue/source/ui/chrome.hpp +++ b/products/dialogue/source/ui/chrome.hpp @@ -9,7 +9,6 @@ #pragma once #include -#include #include #include "../PluginProcessor.hpp" @@ -41,25 +40,4 @@ namespace altvane::dialogue { void setChoice(const char* id, int index, const juce::String& name); }; - - /** - * right rail: the output meter - */ - class OutputRail final : public juce::Component { - public: - explicit OutputRail(PluginProcessor& p); - - void tick(double dt); - - void paint(juce::Graphics& g) override; - - void resized() override; - - void mouseUp(const juce::MouseEvent& e) override; - - private: - PluginProcessor& p_ref_; - ui::LevelMeter meter_; - juce::Rectangle label_area_, meter_area_, peak_area_; - }; } diff --git a/products/dialogue/source/ui/editor.cpp b/products/dialogue/source/ui/editor.cpp index 35902b17..63eea231 100644 --- a/products/dialogue/source/ui/editor.cpp +++ b/products/dialogue/source/ui/editor.cpp @@ -35,7 +35,7 @@ namespace altvane::dialogue { visualiser_(p, param_editor_), deck_(p, param_editor_), footer_(p, param_editor_), - rail_(p) { + rail_(p, [&p] { return p.takeOutputPeaks(); }) { presets_.installFactoryPresets(factoryPresets(), kFactoryPresetsVersion); root_.addAndMakeVisible(visualiser_); root_.addAndMakeVisible(deck_); diff --git a/products/dialogue/source/ui/editor.hpp b/products/dialogue/source/ui/editor.hpp index 17c79463..7e4a069c 100644 --- a/products/dialogue/source/ui/editor.hpp +++ b/products/dialogue/source/ui/editor.hpp @@ -9,6 +9,7 @@ #pragma once #include +#include #include "chrome.hpp" #include "control_deck.hpp" @@ -40,7 +41,7 @@ namespace altvane::dialogue { Visualiser visualiser_; ControlDeck deck_; Footer footer_; - OutputRail rail_; + ui::OutputRail rail_; uint64_t underruns_shown_{~uint64_t{0}}; void layout(juce::Rectangle area) override; diff --git a/products/dialogue/tests/alloc_counter.cpp b/products/dialogue/tests/alloc_counter.cpp deleted file mode 100644 index 542e87d7..00000000 --- a/products/dialogue/tests/alloc_counter.cpp +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (C) 2026 - Altvane -// This file is part of Altvane Dialogue -// -// Altvane Dialogue is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License Version 3 as published by the Free Software Foundation. -// -// Altvane Dialogue is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License along with Altvane Dialogue. If not, see . -#include "alloc_counter.hpp" - -#include -#include -#ifdef _WIN32 -#include -#endif - -namespace { - thread_local bool counting = false; - thread_local size_t allocations = 0; - - void* allocate(const std::size_t size) { - if (counting) { - ++allocations; - } - if (void* p = std::malloc(size == 0 ? 1 : size)) { - return p; - } - throw std::bad_alloc(); - } - - void* allocateAligned(const std::size_t size, const std::align_val_t alignment) { - if (counting) { - ++allocations; - } - const auto a = static_cast(alignment); -#ifdef _WIN32 - // the Windows C runtime has no aligned_alloc; its aligned blocks are freed with _aligned_free - if (void* p = _aligned_malloc(size == 0 ? 1 : size, a)) { -#else - if (void* p = std::aligned_alloc(a, (size + a - 1) / a * a)) { -#endif - return p; - } - throw std::bad_alloc(); - } - - void freeAligned(void* p) { -#ifdef _WIN32 - _aligned_free(p); -#else - std::free(p); -#endif - } -} - -namespace alloc_counter { - void begin() { - allocations = 0; - counting = true; - } - - size_t end() { - counting = false; - return allocations; - } - - size_t Scope::count() const { - return allocations; - } -} - -void* operator new(const std::size_t size) { return allocate(size); } -void* operator new[](const std::size_t size) { return allocate(size); } -void* operator new(const std::size_t size, const std::align_val_t a) { return allocateAligned(size, a); } -void* operator new[](const std::size_t size, const std::align_val_t a) { return allocateAligned(size, a); } -void operator delete(void* p) noexcept { std::free(p); } -void operator delete[](void* p) noexcept { std::free(p); } -void operator delete(void* p, std::size_t) noexcept { std::free(p); } -void operator delete[](void* p, std::size_t) noexcept { std::free(p); } -void operator delete(void* p, std::align_val_t) noexcept { freeAligned(p); } -void operator delete[](void* p, std::align_val_t) noexcept { freeAligned(p); } -void operator delete(void* p, std::size_t, std::align_val_t) noexcept { freeAligned(p); } -void operator delete[](void* p, std::size_t, std::align_val_t) noexcept { freeAligned(p); } diff --git a/products/dialogue/tests/alloc_counter.hpp b/products/dialogue/tests/alloc_counter.hpp deleted file mode 100644 index c86c3907..00000000 --- a/products/dialogue/tests/alloc_counter.hpp +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (C) 2026 - Altvane -// This file is part of Altvane Dialogue -// -// Altvane Dialogue is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License Version 3 as published by the Free Software Foundation. -// -// Altvane Dialogue is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License along with Altvane Dialogue. If not, see . -#pragma once - -#include - -/** - * counts heap allocations made by the current thread while a Scope is alive (operator new is replaced in - * alloc_counter.cpp for the whole test binary) - */ -namespace alloc_counter { - void begin(); - - size_t end(); - - class Scope { - public: - Scope() { begin(); } - - ~Scope() { end(); } - - size_t count() const; - }; -} diff --git a/products/dialogue/tests/engine_tests.cpp b/products/dialogue/tests/engine_tests.cpp index ac2f2e4b..6e8c9885 100644 --- a/products/dialogue/tests/engine_tests.cpp +++ b/products/dialogue/tests/engine_tests.cpp @@ -11,8 +11,9 @@ #include #include +#include + #include "PluginProcessor.hpp" -#include "alloc_counter.hpp" using namespace altvane::dialogue; diff --git a/products/dialogue/tests/juce_init.cpp b/products/dialogue/tests/juce_init.cpp deleted file mode 100644 index ebd45de0..00000000 --- a/products/dialogue/tests/juce_init.cpp +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (C) 2026 - Altvane -// This file is part of Altvane Dialogue -// -// Altvane Dialogue is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License Version 3 as published by the Free Software Foundation. -// -// Altvane Dialogue is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License along with Altvane Dialogue. If not, see . -#include -#include -#include - -/** - * the processor and editor need JUCE (message manager, desktop) for the whole run; - * scoping it to the run gives a clean shutdown before JUCE's leak detector checks - */ -class JuceRunListener final : public Catch::EventListenerBase { -public: - using EventListenerBase::EventListenerBase; - - void testRunStarting(const Catch::TestRunInfo&) override { - juce_ = std::make_unique(); - } - - void testRunEnded(const Catch::TestRunStats&) override { - juce_.reset(); - } - -private: - std::unique_ptr juce_; -}; - -CATCH_REGISTER_LISTENER(JuceRunListener) diff --git a/products/eq/tests/alloc_counter.hpp b/products/eq/tests/alloc_counter.hpp deleted file mode 100644 index 73de83a7..00000000 --- a/products/eq/tests/alloc_counter.hpp +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (C) 2026 - Altvane -// This file is part of Altvane EQ -// -// Altvane EQ is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License Version 3 as published by the Free Software Foundation. -// -// Altvane EQ is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License along with Altvane EQ. If not, see . -#pragma once - -#include - -/** - * counts heap allocations made by the current thread while a Scope is alive (operator new is replaced in - * alloc_counter.cpp for the whole test binary) - */ -namespace alloc_counter { - void begin(); - - size_t end(); - - class Scope { - public: - Scope() { begin(); } - - ~Scope() { end(); } - - size_t count() const; - }; -} diff --git a/products/eq/tests/processor_tests.cpp b/products/eq/tests/processor_tests.cpp index b0688166..9116dbed 100644 --- a/products/eq/tests/processor_tests.cpp +++ b/products/eq/tests/processor_tests.cpp @@ -9,7 +9,8 @@ #include #include -#include "alloc_counter.hpp" +#include + #include "test_helpers.hpp" using Catch::Matchers::WithinAbs; diff --git a/products/leveler/docs/ARCHITECTURE.md b/products/leveler/docs/ARCHITECTURE.md index ee44a74e..fce3d919 100644 --- a/products/leveler/docs/ARCHITECTURE.md +++ b/products/leveler/docs/ARCHITECTURE.md @@ -92,7 +92,7 @@ and frame loop. - `ui/rider_panel`: `RiderFader`, a `ui::ValueControl` with range handles and an idle marker, plus the mode chips and the Max, Min and Idle fields. - `ui/control_deck`: the knobs, Learn and Lookahead. -- `ui/chrome`: the footer and the output rail. +- `ui/chrome`: the footer. The output rail is the shared `ui::OutputRail` (`libs/ui/widgets/output_rail`). - `ui/theme/leveler_tokens.hpp`: colours and sizes. - Factory presets (`ui/factory_presets`) are installed once into `Presets/Factory`. diff --git a/products/leveler/source/PluginProcessor.cpp b/products/leveler/source/PluginProcessor.cpp index 78760e86..49068908 100644 --- a/products/leveler/source/PluginProcessor.cpp +++ b/products/leveler/source/PluginProcessor.cpp @@ -12,12 +12,6 @@ namespace altvane::leveler { namespace { - void storePeak(std::atomic& peak, const float value) { - auto current = peak.load(std::memory_order::relaxed); - while (value > current && !peak.compare_exchange_weak(current, value, std::memory_order::relaxed)) { - } - } - // the session-only part of the state: automation mode and the rider fader, never in presets const juce::Identifier kSessionTag{"Session"}; const juce::Identifier kModeProperty{"mode"}; @@ -162,7 +156,7 @@ namespace altvane::leveler { } } for (int c = 0; c < channels; ++c) { - storePeak(output_peak_[static_cast(c)], main.getMagnitude(c, 0, main.getNumSamples())); + output_peaks_.store(static_cast(c), main.getMagnitude(c, 0, main.getNumSamples())); } } @@ -238,14 +232,6 @@ namespace altvane::leveler { return engine_.getControls().manual.load(); } - std::array PluginProcessor::takeOutputPeaks() { - std::array result{}; - for (size_t c = 0; c < 2; ++c) { - result[c] = juce::Decibels::gainToDecibels(output_peak_[c].exchange(0.f, std::memory_order::relaxed), -120.f); - } - return result; - } - void PluginProcessor::getStateInformation(juce::MemoryBlock& dest_data) { auto state = parameters_.copyState(); state.setProperty("version", VERSION, nullptr); diff --git a/products/leveler/source/PluginProcessor.hpp b/products/leveler/source/PluginProcessor.hpp index b643520f..1f6a4364 100644 --- a/products/leveler/source/PluginProcessor.hpp +++ b/products/leveler/source/PluginProcessor.hpp @@ -10,6 +10,7 @@ #include #include +#include #include "params.hpp" #include "dsp/engine.hpp" @@ -105,7 +106,7 @@ namespace altvane::leveler { /** * the output peaks since the last call, in dB (message thread) */ - std::array takeOutputPeaks(); + std::array takeOutputPeaks() { return output_peaks_.take(); } /** * the message-thread work the timer does (latency changes, automation writes), for tests @@ -130,7 +131,7 @@ namespace altvane::leveler { int max_block_{512}; std::atomic prepared_{false}; juce::SmoothedValue bypass_mix_, output_gain_; - std::array, 2> output_peak_{}; + ui::PeakExchange output_peaks_; std::atomic key_channels_{0}; std::atomic playing_{false}; diff --git a/products/leveler/source/params.cpp b/products/leveler/source/params.cpp index 0758b919..cdc9b130 100644 --- a/products/leveler/source/params.cpp +++ b/products/leveler/source/params.cpp @@ -13,7 +13,7 @@ namespace altvane::leveler::params { constexpr int kVersion = 1; float parseNumber(const juce::String& t) { - return t.replace(juce::String::fromUTF8("\xe2\x88\x92"), "-").retainCharacters("+-.0123456789").getFloatValue(); + return static_cast(ui::format::parseLenient(t).value_or(0.0)); } juce::AudioParameterFloatAttributes dbAttributes() { diff --git a/products/leveler/source/params.hpp b/products/leveler/source/params.hpp index 69c38875..5a2519dc 100644 --- a/products/leveler/source/params.hpp +++ b/products/leveler/source/params.hpp @@ -9,6 +9,7 @@ #pragma once #include +#include /** * Altvane Leveler's parameters; the IDs are part of every saved session and preset, never change them @@ -39,9 +40,8 @@ namespace altvane::leveler::params { /** * signed dB with the typographic minus, as everywhere else in the interface */ - inline juce::String formatDb(const float db, const int decimals = 1) { - return (db > .04f ? "+" : db < -.04f ? juce::String::fromUTF8("\xe2\x88\x92") : "") - + juce::String(std::abs(db), decimals) + " dB"; + inline juce::String formatDb(const float db) { + return ui::format::decibels(db, true); } inline juce::String formatPercent(const float v) { diff --git a/products/leveler/source/ui/chrome.cpp b/products/leveler/source/ui/chrome.cpp index 3aa19892..efb07398 100644 --- a/products/leveler/source/ui/chrome.cpp +++ b/products/leveler/source/ui/chrome.cpp @@ -85,45 +85,4 @@ namespace altvane::leveler { drawCaptioned(g, key_area_, "MUSIC KEY", key_text_, juce::Justification::centredLeft); drawCaptioned(g, latency_area_, "LATENCY", latency_text_, juce::Justification::centredRight); } - - // ---------------------------------------------------------------- OutputRail - - OutputRail::OutputRail(PluginProcessor& p) : p_ref_(p) { - setTitle("Output meter"); - } - - void OutputRail::resized() { - auto b = getLocalBounds().toFloat().reduced(6.f, 8.f); - label_area_ = b.removeFromTop(16.f); - b.removeFromTop(8.f); - peak_area_ = b.removeFromBottom(16.f); - b.removeFromBottom(6.f); - meter_area_ = b.withSizeKeepingCentre(14.f, b.getHeight()); - } - - void OutputRail::tick(const double dt) { - const auto peaks = p_ref_.takeOutputPeaks(); - if (meter_.tick(dt, peaks[0], p_ref_.getTotalNumOutputChannels() > 1 ? peaks[1] : peaks[0])) { - repaint(); - } - } - - void OutputRail::paint(juce::Graphics& g) { - const auto b = getLocalBounds().toFloat(); - g.setColour(colour::kWindow); - g.fillRect(b); - g.setColour(colour::kHairline); - g.fillRect(juce::Rectangle(0.f, 0.f, 1.f, b.getHeight())); - g.setFont(captionFont()); - g.setColour(colour::kTextTertiary); - g.drawText("OUT", label_area_, juce::Justification::centred); - meter_.paint(g, meter_area_, peak_area_); - } - - void OutputRail::mouseUp(const juce::MouseEvent& e) { - if (meter_area_.expanded(8.f, 0.f).contains(e.position) || peak_area_.contains(e.position)) { - meter_.resetPeak(); - repaint(); - } - } } diff --git a/products/leveler/source/ui/chrome.hpp b/products/leveler/source/ui/chrome.hpp index fa7dea63..ff44b306 100644 --- a/products/leveler/source/ui/chrome.hpp +++ b/products/leveler/source/ui/chrome.hpp @@ -9,7 +9,6 @@ #pragma once #include -#include #include #include "../PluginProcessor.hpp" @@ -36,25 +35,4 @@ namespace altvane::leveler { juce::Rectangle key_area_, latency_area_; juce::String key_text_, latency_text_; }; - - /** - * right rail: the output meter - */ - class OutputRail final : public juce::Component { - public: - explicit OutputRail(PluginProcessor& p); - - void tick(double dt); - - void paint(juce::Graphics& g) override; - - void resized() override; - - void mouseUp(const juce::MouseEvent& e) override; - - private: - PluginProcessor& p_ref_; - ui::LevelMeter meter_; - juce::Rectangle label_area_, meter_area_, peak_area_; - }; } diff --git a/products/leveler/source/ui/editor.cpp b/products/leveler/source/ui/editor.cpp index c077ad56..159430e6 100644 --- a/products/leveler/source/ui/editor.cpp +++ b/products/leveler/source/ui/editor.cpp @@ -37,7 +37,7 @@ namespace altvane::leveler { rider_(p, param_editor_), deck_(p, param_editor_), footer_(p, param_editor_), - rail_(p) { + rail_(p, [&p] { return p.takeOutputPeaks(); }) { presets_.installFactoryPresets(factoryPresets(), kFactoryPresetsVersion); for (auto* c : std::initializer_list{&history_, &rider_, &deck_, &footer_, &rail_}) { root_.addAndMakeVisible(c); diff --git a/products/leveler/source/ui/editor.hpp b/products/leveler/source/ui/editor.hpp index f32409e8..5774cd11 100644 --- a/products/leveler/source/ui/editor.hpp +++ b/products/leveler/source/ui/editor.hpp @@ -9,6 +9,7 @@ #pragma once #include +#include #include "chrome.hpp" #include "control_deck.hpp" @@ -42,7 +43,7 @@ namespace altvane::leveler { RiderPanel rider_; ControlDeck deck_; Footer footer_; - OutputRail rail_; + ui::OutputRail rail_; void layout(juce::Rectangle area) override; void tick(double dt) override; diff --git a/products/leveler/source/ui/rider_panel.cpp b/products/leveler/source/ui/rider_panel.cpp index 2675fd31..77e2f7fa 100644 --- a/products/leveler/source/ui/rider_panel.cpp +++ b/products/leveler/source/ui/rider_panel.cpp @@ -8,6 +8,8 @@ // You should have received a copy of the GNU Affero General Public License along with Altvane Leveler. If not, see . #include "rider_panel.hpp" +#include + #include "theme/leveler_tokens.hpp" namespace altvane::leveler { @@ -23,8 +25,10 @@ namespace altvane::leveler { setRange({-scale_, scale_, .01f}, 0.f); format = [](const float v) { return params::formatDb(v); }; parse = [](const juce::String& t) -> std::optional { - const auto clean = t.replace(juce::String::fromUTF8("\xe2\x88\x92"), "-").retainCharacters("+-.0123456789"); - return clean.containsAnyOf("0123456789") ? std::optional(clean.getFloatValue()) : std::nullopt; + if (const auto v = ui::format::parseLenient(t)) { + return static_cast(*v); + } + return std::nullopt; }; // a hand on the fader: in Read mode it edits the automation, otherwise it rides over the engine on_begin = [this] { diff --git a/products/leveler/tests/alloc_counter.cpp b/products/leveler/tests/alloc_counter.cpp deleted file mode 100644 index cd0f6cb3..00000000 --- a/products/leveler/tests/alloc_counter.cpp +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (C) 2026 - Altvane -// This file is part of Altvane Leveler -// -// Altvane Leveler is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License Version 3 as published by the Free Software Foundation. -// -// Altvane Leveler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License along with Altvane Leveler. If not, see . -#include "alloc_counter.hpp" - -#include -#include -#if defined(_WIN32) -#include -#endif - -namespace { - thread_local bool counting = false; - thread_local size_t allocations = 0; - - void* allocate(const std::size_t size) { - if (counting) { - ++allocations; - } - if (void* p = std::malloc(size == 0 ? 1 : size)) { - return p; - } - throw std::bad_alloc(); - } - - void* allocateAligned(const std::size_t size, const std::align_val_t alignment) { - if (counting) { - ++allocations; - } - const auto a = static_cast(alignment); -#if defined(_WIN32) - // the MSVC runtime has no aligned_alloc; its aligned blocks must be freed with _aligned_free - void* p = _aligned_malloc(size == 0 ? 1 : size, a); -#else - void* p = std::aligned_alloc(a, (size + a - 1) / a * a); -#endif - if (p != nullptr) { - return p; - } - throw std::bad_alloc(); - } - - void freeAligned(void* p) { -#if defined(_WIN32) - _aligned_free(p); -#else - std::free(p); -#endif - } -} - -namespace alloc_counter { - void begin() { - allocations = 0; - counting = true; - } - - size_t end() { - counting = false; - return allocations; - } - - size_t Scope::count() const { - return allocations; - } -} - -void* operator new(const std::size_t size) { return allocate(size); } -void* operator new[](const std::size_t size) { return allocate(size); } -void* operator new(const std::size_t size, const std::align_val_t a) { return allocateAligned(size, a); } -void* operator new[](const std::size_t size, const std::align_val_t a) { return allocateAligned(size, a); } -void operator delete(void* p) noexcept { std::free(p); } -void operator delete[](void* p) noexcept { std::free(p); } -void operator delete(void* p, std::size_t) noexcept { std::free(p); } -void operator delete[](void* p, std::size_t) noexcept { std::free(p); } -void operator delete(void* p, std::align_val_t) noexcept { freeAligned(p); } -void operator delete[](void* p, std::align_val_t) noexcept { freeAligned(p); } -void operator delete(void* p, std::size_t, std::align_val_t) noexcept { freeAligned(p); } -void operator delete[](void* p, std::size_t, std::align_val_t) noexcept { freeAligned(p); } diff --git a/products/leveler/tests/alloc_counter.hpp b/products/leveler/tests/alloc_counter.hpp deleted file mode 100644 index 817158ed..00000000 --- a/products/leveler/tests/alloc_counter.hpp +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (C) 2026 - Altvane -// This file is part of Altvane Leveler -// -// Altvane Leveler is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License Version 3 as published by the Free Software Foundation. -// -// Altvane Leveler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License along with Altvane Leveler. If not, see . -#pragma once - -#include - -/** - * counts heap allocations made by the current thread while a Scope is alive (operator new is replaced in - * alloc_counter.cpp for the whole test binary) - */ -namespace alloc_counter { - void begin(); - - size_t end(); - - class Scope { - public: - Scope() { begin(); } - - ~Scope() { end(); } - - size_t count() const; - }; -} diff --git a/products/leveler/tests/engine_tests.cpp b/products/leveler/tests/engine_tests.cpp index 8af3f36d..66e2e570 100644 --- a/products/leveler/tests/engine_tests.cpp +++ b/products/leveler/tests/engine_tests.cpp @@ -9,7 +9,8 @@ #include #include -#include "alloc_counter.hpp" +#include + #include "signals.hpp" using namespace test_signals; diff --git a/products/leveler/tests/juce_init.cpp b/products/leveler/tests/juce_init.cpp deleted file mode 100644 index a7e89ef3..00000000 --- a/products/leveler/tests/juce_init.cpp +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (C) 2026 - Altvane -// This file is part of Altvane Leveler -// -// Altvane Leveler is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License Version 3 as published by the Free Software Foundation. -// -// Altvane Leveler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License along with Altvane Leveler. If not, see . -#include -#include -#include - -/** - * the processor and editor need JUCE (message manager, desktop) for the whole run; - * scoping it to the run gives a clean shutdown before JUCE's leak detector checks - */ -class JuceRunListener final : public Catch::EventListenerBase { -public: - using EventListenerBase::EventListenerBase; - - void testRunStarting(const Catch::TestRunInfo&) override { - juce_ = std::make_unique(); - } - - void testRunEnded(const Catch::TestRunStats&) override { - juce_.reset(); - } - -private: - std::unique_ptr juce_; -}; - -CATCH_REGISTER_LISTENER(JuceRunListener) diff --git a/products/limiter/tests/alloc_counter.cpp b/products/limiter/tests/alloc_counter.cpp deleted file mode 100644 index 2aedcee7..00000000 --- a/products/limiter/tests/alloc_counter.cpp +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (C) 2026 - Altvane -// This file is part of Altvane Limiter -// -// Altvane Limiter is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License Version 3 as published by the Free Software Foundation. -// -// Altvane Limiter is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License along with Altvane Limiter. If not, see . -#include "alloc_counter.hpp" - -#include -#include -#if defined(_WIN32) -#include -#endif - -namespace { - thread_local bool counting = false; - thread_local size_t allocations = 0; - - void* allocate(const std::size_t size) { - if (counting) { - ++allocations; - } - if (void* p = std::malloc(size == 0 ? 1 : size)) { - return p; - } - throw std::bad_alloc(); - } - - void* allocateAligned(const std::size_t size, const std::align_val_t alignment) { - if (counting) { - ++allocations; - } - const auto a = static_cast(alignment); -#if defined(_WIN32) - // the MSVC runtime has no aligned_alloc; its aligned blocks must be freed with _aligned_free - void* p = _aligned_malloc(size == 0 ? 1 : size, a); -#else - void* p = std::aligned_alloc(a, (size + a - 1) / a * a); -#endif - if (p != nullptr) { - return p; - } - throw std::bad_alloc(); - } - - void freeAligned(void* p) { -#if defined(_WIN32) - _aligned_free(p); -#else - std::free(p); -#endif - } -} - -namespace alloc_counter { - void begin() { - allocations = 0; - counting = true; - } - - size_t end() { - counting = false; - return allocations; - } - - size_t Scope::count() const { - return allocations; - } -} - -void* operator new(const std::size_t size) { return allocate(size); } -void* operator new[](const std::size_t size) { return allocate(size); } -void* operator new(const std::size_t size, const std::align_val_t a) { return allocateAligned(size, a); } -void* operator new[](const std::size_t size, const std::align_val_t a) { return allocateAligned(size, a); } -void operator delete(void* p) noexcept { std::free(p); } -void operator delete[](void* p) noexcept { std::free(p); } -void operator delete(void* p, std::size_t) noexcept { std::free(p); } -void operator delete[](void* p, std::size_t) noexcept { std::free(p); } -void operator delete(void* p, std::align_val_t) noexcept { freeAligned(p); } -void operator delete[](void* p, std::align_val_t) noexcept { freeAligned(p); } -void operator delete(void* p, std::size_t, std::align_val_t) noexcept { freeAligned(p); } -void operator delete[](void* p, std::size_t, std::align_val_t) noexcept { freeAligned(p); } diff --git a/products/limiter/tests/alloc_counter.hpp b/products/limiter/tests/alloc_counter.hpp deleted file mode 100644 index 2f2abcee..00000000 --- a/products/limiter/tests/alloc_counter.hpp +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (C) 2026 - Altvane -// This file is part of Altvane Limiter -// -// Altvane Limiter is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License Version 3 as published by the Free Software Foundation. -// -// Altvane Limiter is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License along with Altvane Limiter. If not, see . -#pragma once - -#include - -/** - * counts heap allocations made by the current thread while a Scope is alive (operator new is replaced in - * alloc_counter.cpp for the whole test binary) - */ -namespace alloc_counter { - void begin(); - - size_t end(); - - class Scope { - public: - Scope() { begin(); } - - ~Scope() { end(); } - - size_t count() const; - }; -} diff --git a/products/limiter/tests/juce_init.cpp b/products/limiter/tests/juce_init.cpp deleted file mode 100644 index 10eff71c..00000000 --- a/products/limiter/tests/juce_init.cpp +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (C) 2026 - Altvane, based on ZLLimiter by zsliu98 -// This file is part of Altvane Limiter -// -// Altvane Limiter is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License Version 3 as published by the Free Software Foundation. -// -// Altvane Limiter is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License along with Altvane Limiter. If not, see . -#include -#include -#include - -/** - * the processor and editor need JUCE (message manager, desktop) for the whole run; - * scoping it to the run gives a clean shutdown before JUCE's leak detector checks - */ -class JuceRunListener final : public Catch::EventListenerBase { -public: - using EventListenerBase::EventListenerBase; - - void testRunStarting(const Catch::TestRunInfo&) override { - juce_ = std::make_unique(); - } - - void testRunEnded(const Catch::TestRunStats&) override { - juce_.reset(); - } - -private: - std::unique_ptr juce_; -}; - -CATCH_REGISTER_LISTENER(JuceRunListener) diff --git a/products/limiter/tests/processor_tests.cpp b/products/limiter/tests/processor_tests.cpp index 3d75e0b9..d7244691 100644 --- a/products/limiter/tests/processor_tests.cpp +++ b/products/limiter/tests/processor_tests.cpp @@ -11,7 +11,8 @@ #include #include -#include "alloc_counter.hpp" +#include + #include "test_helpers.hpp" #include "ui/model/character.hpp"