Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<code>/ 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
Expand Down
4 changes: 2 additions & 2 deletions cmake/AltvaneTests.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# altvane_add_tests(<plugin-target> <test-dir>)
#
# builds every .cpp under <test-dir> into <plugin-target>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
Expand All @@ -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})
Expand Down
15 changes: 15 additions & 0 deletions libs/testing/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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/<file>.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)
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
// You should have received a copy of the GNU Affero General Public License along with Altvane. If not, see <https://www.gnu.org/licenses/>.
#include "alloc_counter.hpp"

#include <cstdlib>
Expand Down
30 changes: 30 additions & 0 deletions libs/testing/source/altvane_testing/alloc_counter.hpp
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
#pragma once

#include <cstddef>

/**
* 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;
};
}
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
// You should have received a copy of the GNU Affero General Public License along with Altvane. If not, see <https://www.gnu.org/licenses/>.
#include <catch2/reporters/catch_reporter_event_listener.hpp>
#include <catch2/reporters/catch_reporter_registrars.hpp>
#include <juce_gui_basics/juce_gui_basics.h>
Expand Down
49 changes: 49 additions & 0 deletions libs/ui/source/altvane_ui/util/peak_exchange.hpp
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.

#pragma once

#include <array>
#include <atomic>
#include <juce_audio_basics/juce_audio_basics.h>

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<float, 2> take() {
std::array<float, 2> 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<float>::is_always_lock_free, "PeakExchange requires lock-free atomic float");

std::array<std::atomic<float>, 2> peaks_{};
};
}
10 changes: 10 additions & 0 deletions libs/ui/source/altvane_ui/util/value_format.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -97,6 +102,11 @@ namespace altvane::ui::format {
return std::isfinite(v) ? std::optional<double>{v} : std::nullopt;
}

std::optional<double> parseLenient(const juce::String& text) {
const auto t = text.replace(kMinus, "-").retainCharacters("+-.0123456789");
return t.containsAnyOf("0123456789") ? std::optional<double>{t.getDoubleValue()} : std::nullopt;
}

std::optional<double> parseFreq(const juce::String& text) {
auto t = normalise(text).removeCharacters(" ");
if (t.isEmpty()) {
Expand Down
11 changes: 11 additions & 0 deletions libs/ui/source/altvane_ui/util/value_format.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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<double> 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<double> parseLenient(const juce::String& text);
}
62 changes: 62 additions & 0 deletions libs/ui/source/altvane_ui/widgets/output_rail.cpp
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
#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<std::array<float, 2>()> 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<float>(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();
}
}
}
43 changes: 43 additions & 0 deletions libs/ui/source/altvane_ui/widgets/output_rail.hpp
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
#pragma once

#include <array>
#include <functional>
#include <juce_audio_processors/juce_audio_processors.h>

#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<std::array<float, 2>()> 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<std::array<float, 2>()> take_peaks_;
LevelMeter meter_;
juce::Rectangle<float> label_area_, meter_area_, peak_area_;
};
}
Loading
Loading