From ef0e92540ae9eb86584782fd2a80b2402b023213 Mon Sep 17 00:00:00 2001 From: Lukas Berbuer Date: Sun, 2 Aug 2026 22:54:28 +0200 Subject: [PATCH 1/6] feat: Vamp plugin exposing features as audio analysis plugins Build bindings/vamp into a self-contained plugin library (openae.dll/ .so/.dylib) that adapts all time- and frequency-domain features to the Vamp C API. AE features can now be computed and visualized in any Vamp host (e.g. Sonic Visualiser, sonic-annotator) without writing code. --- .clang-format | 1 + .github/workflows/ci.yml | 1 + CMakeLists.txt | 32 ++ CMakePresets.json | 11 +- bindings/vamp/.clang-tidy | 13 + bindings/vamp/CMakeLists.txt | 43 +++ bindings/vamp/src/plugin.cpp | 484 ++++++++++++++++++++++++++++ bindings/vamp/src/vamp.h | 388 ++++++++++++++++++++++ bindings/vamp/tests/CMakeLists.txt | 12 + bindings/vamp/tests/test_plugin.cpp | 216 +++++++++++++ src/CMakeLists.txt | 13 - tests/CMakeLists.txt | 14 - 12 files changed, 1200 insertions(+), 28 deletions(-) create mode 100644 bindings/vamp/.clang-tidy create mode 100644 bindings/vamp/CMakeLists.txt create mode 100644 bindings/vamp/src/plugin.cpp create mode 100644 bindings/vamp/src/vamp.h create mode 100644 bindings/vamp/tests/CMakeLists.txt create mode 100644 bindings/vamp/tests/test_plugin.cpp diff --git a/.clang-format b/.clang-format index be3250b..45e6c0a 100644 --- a/.clang-format +++ b/.clang-format @@ -15,6 +15,7 @@ AlwaysBreakTemplateDeclarations: Yes BinPackArguments: false BinPackParameters: false BreakAfterAttributes: Leave +BreakStringLiterals: false ColumnLimit: 100 FixNamespaceComments: true IndentWidth: 4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 93809af..a55c709 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,6 +59,7 @@ jobs: -DOPENAE_BUILD_BENCHMARKS=ON -DOPENAE_BUILD_TESTS=ON -DOPENAE_BUILD_EXAMPLES=ON + -DOPENAE_BUILD_VAMP=ON -DOPENAE_WARNINGS_AS_ERRORS=ON ${{ matrix.config.flags }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 6b01b72..ed5ee11 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,6 +58,20 @@ else() target_compile_options(openae_project_options INTERFACE ${warnings}) endif() +# dependencies +include(FetchContent) +option(XXHASH_BUILD_XXHSUM "Build the xxhsum binary" OFF) +FetchContent_Declare( + xxHash + GIT_REPOSITORY https://github.com/Cyan4973/xxHash.git + GIT_TAG v0.8.3 + SOURCE_SUBDIR cmake_unofficial + EXCLUDE_FROM_ALL + SYSTEM + FIND_PACKAGE_ARGS 0.8.3 +) +FetchContent_MakeAvailable(xxHash) + # library add_subdirectory(src) @@ -66,6 +80,19 @@ option(OPENAE_BUILD_TESTS "Build unit tests" OFF) if(OPENAE_BUILD_TESTS) message(STATUS "Unit tests enabled") enable_testing() + option(CATCH_INSTALL_DOCS "Install documentation alongside library" OFF) + FetchContent_Declare( + Catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2.git + GIT_TAG v3.8.0 + EXCLUDE_FROM_ALL + SYSTEM + FIND_PACKAGE_ARGS 3.8.0 + ) + FetchContent_MakeAvailable(Catch2) + if(TARGET Catch2) + set_target_properties(Catch2 Catch2WithMain PROPERTIES CXX_CLANG_TIDY "") + endif() add_subdirectory(tests) endif() @@ -83,3 +110,8 @@ cmake_dependent_option(OPENAE_BUILD_PYTHON_STUBS "Build Python stubs" ON "OPENAE if(OPENAE_BUILD_PYTHON) add_subdirectory(bindings/python) endif() + +option(OPENAE_BUILD_VAMP "Build Vamp plugin" OFF) +if(OPENAE_BUILD_VAMP) + add_subdirectory(bindings/vamp) +endif() diff --git a/CMakePresets.json b/CMakePresets.json index b20837b..d25c8b0 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -36,7 +36,16 @@ "OPENAE_WARNINGS_AS_ERRORS": "ON", "OPENAE_BUILD_TESTS": "ON", "OPENAE_BUILD_BENCHMARKS": "ON", - "OPENAE_BUILD_PYTHON": "ON" + "OPENAE_BUILD_PYTHON": "ON", + "OPENAE_BUILD_VAMP": "ON" + } + }, + { + "name": "vamp", + "inherits": ["release"], + "installDir": "${sourceDir}/install/${presetName}", + "cacheVariables": { + "OPENAE_BUILD_VAMP": "ON" } }, { diff --git a/bindings/vamp/.clang-tidy b/bindings/vamp/.clang-tidy new file mode 100644 index 0000000..f929425 --- /dev/null +++ b/bindings/vamp/.clang-tidy @@ -0,0 +1,13 @@ +--- +# Extends the project config. The checks disabled here flag constructs the Vamp C plugin ABI forces on us: +# - raw new/delete across the interface boundary +# - reinterpret_cast for the interleaved spectrum buffer +# - C-style buffer/array indexing behind explicit bounds checks +# - fixed callback signatures +InheritParentConfig: true +Checks: > + -bugprone-easily-swappable-parameters, + -cppcoreguidelines-owning-memory, + -cppcoreguidelines-pro-bounds-constant-array-index, + -cppcoreguidelines-pro-bounds-pointer-arithmetic, + -cppcoreguidelines-pro-type-reinterpret-cast, diff --git a/bindings/vamp/CMakeLists.txt b/bindings/vamp/CMakeLists.txt new file mode 100644 index 0000000..3a79901 --- /dev/null +++ b/bindings/vamp/CMakeLists.txt @@ -0,0 +1,43 @@ +# Vamp plugin module exposing OpenAE features. +# +# Produces a single shared module that exports vampGetPluginDescriptor. +# The file name defines the plugin library id, so it must be exactly: +# Linux : openae.so +# macOS : openae.dylib +# Windows : openae.dll +# To avoid clashes with the core openae library, the module gets its own output directory in the build tree and its own install destination. + +if(BUILD_SHARED_LIBS) + message(WARNING "Vamp plugin depends on shared openae library and will not be loadable by hosts; use BUILD_SHARED_LIBS=OFF") +endif() + +add_library(openae_vamp MODULE src/plugin.cpp src/vamp.h) +target_link_libraries(openae_vamp PRIVATE openae::openae openae_project_options) +target_include_directories(openae_vamp PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + +set_target_properties( + openae_vamp + PROPERTIES + PREFIX "" + OUTPUT_NAME "openae" + CXX_VISIBILITY_PRESET hidden + CXX_VISIBILITY_INLINES_HIDDEN ON + POSITION_INDEPENDENT_CODE ON + LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/vamp" + ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/vamp" + RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/vamp" +) +if(APPLE) + # MODULE targets default to .so on macOS, but Vamp hosts scan for .dylib. + set_target_properties(openae_vamp PROPERTIES SUFFIX ".dylib") +endif() + +include(GNUInstallDirs) +install( + TARGETS openae_vamp + LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}/vamp" COMPONENT vamp +) + +if(OPENAE_BUILD_TESTS) + add_subdirectory(tests) +endif() diff --git a/bindings/vamp/src/plugin.cpp b/bindings/vamp/src/plugin.cpp new file mode 100644 index 0000000..a8e1a85 --- /dev/null +++ b/bindings/vamp/src/plugin.cpp @@ -0,0 +1,484 @@ +#include "vamp.h" + +#include "openae/common.hpp" +#include "openae/features.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using openae::Env; +using openae::features::Input; + +enum class Domain : unsigned char { Time, Frequency }; + +struct PluginSpec { + const char* identifier = nullptr; + const char* name = nullptr; + const char* description = nullptr; + const char* unit = ""; + Domain domain = Domain::Time; + std::span parameters; + float (*compute)(Env& env, Input input, std::span parameters) = nullptr; +}; + +inline constexpr VampParameterDescriptor param_fmin{ + .identifier = "fmin", + .name = "Lower frequency", + .description = "Lower frequency bound (clamped to Nyquist by the plugin).", + .unit = "Hz", + .minValue = 0.0F, + .maxValue = 1.0e6F, + .defaultValue = 0.0F, + .isQuantized = 0, + .quantizeStep = 0.0F, + .valueNames = nullptr, +}; +inline constexpr VampParameterDescriptor param_fmax{ + .identifier = "fmax", + .name = "Upper frequency", + .description = "Upper frequency bound (clamped to Nyquist by the plugin).", + .unit = "Hz", + .minValue = 0.0F, + .maxValue = 1.0e6F, + .defaultValue = 1.0e6F, + .isQuantized = 0, + .quantizeStep = 0.0F, + .valueNames = nullptr, +}; +inline constexpr VampParameterDescriptor param_rolloff{ + .identifier = "rolloff", + .name = "Rolloff fraction", + .description = "Fraction of total spectral energy below the rolloff frequency (range [0, 1]).", + .unit = "", + .minValue = 0.0F, + .maxValue = 1.0F, + .defaultValue = 0.85F, + .isQuantized = 0, + .quantizeStep = 0.0F, + .valueNames = nullptr, +}; + +inline constexpr std::array params_partial_power = {¶m_fmin, ¶m_fmax}; +inline constexpr std::array params_rolloff = {¶m_rolloff}; + +inline constexpr std::array specs{ + // Time-domain features + PluginSpec{ + .identifier = "peak-amplitude", + .name = "Peak amplitude", + .description = "The maximum absolute amplitude of a signal.", + .unit = "V", + .domain = Domain::Time, + .parameters = {}, + .compute = [](Env& env, Input input, [[maybe_unused]] std::span parameters) { + return openae::features::peak_amplitude(env, input); + }, + }, + PluginSpec{ + .identifier = "energy", + .name = "Energy", + .description = "The integral of the signal's squared values over time.", + .unit = "V^2s", + .domain = Domain::Time, + .parameters = {}, + .compute = [](Env& env, Input input, [[maybe_unused]] std::span parameters) { + return openae::features::energy(env, input); + }, + }, + PluginSpec{ + .identifier = "rms", + .name = "RMS", + .description = "A measure for the average energy of a signal.", + .unit = "V", + .domain = Domain::Time, + .parameters = {}, + .compute = [](Env& env, Input input, [[maybe_unused]] std::span parameters) { + return openae::features::rms(env, input); + }, + }, + PluginSpec{ + .identifier = "crest-factor", + .name = "Crest factor", + .description = "The ratio of the peak amplitude to the RMS of a signal.", + .unit = "", + .domain = Domain::Time, + .parameters = {}, + .compute = [](Env& env, Input input, [[maybe_unused]] std::span parameters) { + return openae::features::crest_factor(env, input); + }, + }, + PluginSpec{ + .identifier = "impulse-factor", + .name = "Impulse factor", + .description = "The ratio of peak amplitude and mean of absolute values.", + .unit = "", + .domain = Domain::Time, + .parameters = {}, + .compute = [](Env& env, Input input, [[maybe_unused]] std::span parameters) { + return openae::features::impulse_factor(env, input); + }, + }, + PluginSpec{ + .identifier = "clearance-factor", + .name = "Clearance factor", + .description = "The ratio of the peak amplitude and the squared mean of the square roots of the absolute amplitudes.", + .unit = "", + .domain = Domain::Time, + .parameters = {}, + .compute = [](Env& env, Input input, [[maybe_unused]] std::span parameters) { + return openae::features::clearance_factor(env, input); + }, + }, + PluginSpec{ + .identifier = "shape-factor", + .name = "Shape factor", + .description = "The ratio of the RMS value and the mean of absolute values.", + .unit = "", + .domain = Domain::Time, + .parameters = {}, + .compute = [](Env& env, Input input, [[maybe_unused]] std::span parameters) { + return openae::features::shape_factor(env, input); + }, + }, + PluginSpec{ + .identifier = "skewness", + .name = "Skewness", + .description = "A statistical measure that quantifies the asymmetry of a dataset's probability distribution.", + .unit = "", + .domain = Domain::Time, + .parameters = {}, + .compute = [](Env& env, Input input, [[maybe_unused]] std::span parameters) { + return openae::features::skewness(env, input); + }, + }, + PluginSpec{ + .identifier = "kurtosis", + .name = "Kurtosis", + .description = "A statistical measure that describes the shape of a distribution, focusing on its tails.", + .unit = "", + .domain = Domain::Time, + .parameters = {}, + .compute = [](Env& env, Input input, [[maybe_unused]] std::span parameters) { + return openae::features::kurtosis(env, input); + }, + }, + PluginSpec{ + .identifier = "zero-crossing-rate", + .name = "Zero-crossing rate", + .description = "The rate at which a signal changes from positive to zero to negative or vice versa.", + .unit = "Hz", + .domain = Domain::Time, + .parameters = {}, + .compute = [](Env& env, Input input, [[maybe_unused]] std::span parameters) { + return openae::features::zero_crossing_rate(env, input); + }, + }, + + // Frequency-domain features + PluginSpec{ + .identifier = "partial-power", + .name = "Partial power", + .description = "The proportion of energy within a specified frequency band [fmin, fmax) relative to total energy.", + .unit = "", + .domain = Domain::Frequency, + .parameters = params_partial_power, + .compute = [](Env& env, Input input, std::span parameters) { + return openae::features::partial_power(env, input, parameters[0], parameters[1]); + }, + }, + PluginSpec{ + .identifier = "spectral-peak-frequency", + .name = "Spectral peak frequency", + .description = "The frequency at which a signal has its highest energy.", + .unit = "Hz", + .domain = Domain::Frequency, + .parameters = {}, + .compute = [](Env& env, Input input, [[maybe_unused]] std::span parameters) { + return openae::features::spectral_peak_frequency(env, input); + }, + }, + PluginSpec{ + .identifier = "spectral-centroid", + .name = "Spectral centroid", + .description = "The centroid frequency indicates where the center of mass of the spectrum is located.", + .unit = "Hz", + .domain = Domain::Frequency, + .parameters = {}, + .compute = [](Env& env, Input input, [[maybe_unused]] std::span parameters) { + return openae::features::spectral_centroid(env, input); + }, + }, + PluginSpec{ + .identifier = "spectral-variance", + .name = "Spectral variance", + .description = "Quantifies the dispersion of the spectral content around the spectral centroid.", + .unit = "Hz^2", + .domain = Domain::Frequency, + .parameters = {}, + .compute = [](Env& env, Input input, [[maybe_unused]] std::span parameters) { + return openae::features::spectral_variance(env, input); + }, + }, + PluginSpec{ + .identifier = "spectral-skewness", + .name = "Spectral skewness", + .description = "A measure of the asymmetry of the power spectrum around its mean (the spectral centroid).", + .unit = "", + .domain = Domain::Frequency, + .parameters = {}, + .compute = [](Env& env, Input input, [[maybe_unused]] std::span parameters) { + return openae::features::spectral_skewness(env, input); + }, + }, + PluginSpec{ + .identifier = "spectral-kurtosis", + .name = "Spectral kurtosis", + .description = "A measure of the tailedness or peakedness of the power spectrum around its mean.", + .unit = "", + .domain = Domain::Frequency, + .parameters = {}, + .compute = [](Env& env, Input input, [[maybe_unused]] std::span parameters) { + return openae::features::spectral_kurtosis(env, input); + }, + }, + PluginSpec{ + .identifier = "spectral-rolloff", + .name = "Spectral rolloff", + .description = "The frequency below which a specified proportion of the total spectral energy is contained.", + .unit = "Hz", + .domain = Domain::Frequency, + .parameters = params_rolloff, + .compute = [](Env& env, Input input, std::span parameters) { + return openae::features::spectral_rolloff(env, input, parameters[0]); + }, + }, + PluginSpec{ + .identifier = "spectral-entropy", + .name = "Spectral entropy", + .description = "Spectral peakedness: sharp peaks yield low entropy, flat spectra yield high entropy.", + .unit = "", + .domain = Domain::Frequency, + .parameters = {}, + .compute = [](Env& env, Input input, [[maybe_unused]] std::span parameters) { + return openae::features::spectral_entropy(env, input); + }, + }, + PluginSpec{ + .identifier = "spectral-flatness", + .name = "Spectral flatness", + .description = "A measure used to quantify how flat or \"white\" a signal's power spectrum is.", + .unit = "", + .domain = Domain::Frequency, + .parameters = {}, + .compute = [](Env& env, Input input, [[maybe_unused]] std::span parameters) { + return openae::features::spectral_flatness(env, input); + }, + }, +}; + +constexpr std::size_t maxParameters = 2; +static_assert( + std::ranges::all_of( + specs, [](const PluginSpec& s) { return s.parameters.size() <= maxParameters; } + ), + "a PluginSpec has more parameters than Instance::params can hold" +); + +struct Instance { + const PluginSpec* spec; + float samplerate; + unsigned int block_size = 0; + std::array parameters{}; + + // Execution context with cache, reused across process() calls. + std::unique_ptr cache = openae::make_cache(); + Env env{}; + + // Output value; featureUnion.v1.values points here. + float value = 0.0F; + + // Single-value API version 1 feature list, wired up in the constructor. + VampFeatureUnion featureUnion{}; + VampFeatureList featureList{}; + + Instance(const PluginSpec* s, float sr) + : spec(s), + samplerate(sr) { + env.cache = cache.get(); + for (std::size_t i = 0; i < s->parameters.size() && i < maxParameters; ++i) { + parameters[i] = s->parameters[i]->defaultValue; + } + featureUnion.v1.valueCount = 1; + featureUnion.v1.values = &value; + featureList.featureCount = 1; + featureList.features = &featureUnion; + } + + // featureUnion.v1.values points into this object; copying/moving would dangle. + Instance(const Instance&) = delete; + Instance(Instance&&) = delete; + Instance& operator=(const Instance&) = delete; + Instance& operator=(Instance&&) = delete; + ~Instance() = default; +}; + +inline VampFeatureList& empty_feature_list() { + static VampFeatureList empty{0, nullptr}; + return empty; +} + +template +inline constexpr VampOutputDescriptor output_descriptor{ + .identifier = specs[I].identifier, + .name = specs[I].name, + .description = specs[I].description, + .unit = specs[I].unit, + .hasFixedBinCount = 1, + .binCount = 1, + .binNames = nullptr, + .hasKnownExtents = 0, + .minValue = 0.0F, + .maxValue = 0.0F, + .isQuantized = 0, + .quantizeStep = 0.0F, + .sampleType = vampOneSamplePerStep, + .sampleRate = 0.0F, + .hasDuration = 0, +}; + +template +constexpr VampPluginDescriptor make_descriptor() { + constexpr const PluginSpec& spec = specs[I]; + VampPluginDescriptor d{}; + // No V2-only features used (no durations), so declare API version 1 for any-host compatibility. + d.vampApiVersion = 1; + d.identifier = spec.identifier; + d.name = spec.name; + d.description = spec.description; + d.maker = "OpenAE (https://openae.io)"; + d.pluginVersion = 1; + d.copyright = "MIT"; + d.parameterCount = static_cast(spec.parameters.size()); + d.parameters = const_cast(spec.parameters.data()); + d.programCount = 0; + d.programs = nullptr; + d.inputDomain = (spec.domain == Domain::Time) ? vampTimeDomain : vampFrequencyDomain; + + d.instantiate = [](const VampPluginDescriptor*, float sr) -> VampPluginHandle { + // Exceptions must not cross the C ABI into the host. + try { + return new Instance(&specs[I], sr); + } catch (...) { + return nullptr; + } + }; + d.cleanup = [](VampPluginHandle h) { delete static_cast(h); }; + d.initialise = + [](VampPluginHandle h, unsigned int channels, unsigned int, unsigned int block_size + ) -> int { + if (channels != 1) { + return 0; + } + static_cast(h)->block_size = block_size; + return 1; + }; + d.reset = [](VampPluginHandle) {}; + d.getParameter = [](VampPluginHandle h, int idx) -> float { + auto* self = static_cast(h); + if (idx < 0 || idx >= static_cast(self->spec->parameters.size())) { + return 0.0F; + } + return self->parameters[idx]; + }; + d.setParameter = [](VampPluginHandle h, int idx, float v) { + auto* self = static_cast(h); + if (idx < 0 || idx >= static_cast(self->spec->parameters.size())) { + return; + } + self->parameters[idx] = v; + }; + d.getCurrentProgram = [](VampPluginHandle) -> unsigned int { return 0; }; + d.selectProgram = [](VampPluginHandle, unsigned int) {}; + d.getPreferredStepSize = [](VampPluginHandle) -> unsigned int { return 0; }; + d.getPreferredBlockSize = [](VampPluginHandle) -> unsigned int { return 0; }; + d.getMinChannelCount = [](VampPluginHandle) -> unsigned int { return 1; }; + d.getMaxChannelCount = [](VampPluginHandle) -> unsigned int { return 1; }; + + d.getOutputCount = [](VampPluginHandle) -> unsigned int { return 1; }; + d.getOutputDescriptor = [](VampPluginHandle, unsigned int idx) -> VampOutputDescriptor* { + if (idx != 0) { + return nullptr; + } + // The host treats the descriptor as read-only; releaseOutputDescriptor is a no-op. + return const_cast(&output_descriptor); + }; + d.releaseOutputDescriptor = [](VampOutputDescriptor*) {}; + + d.process = + [](VampPluginHandle h, const float* const* input_buffers, int, int) -> VampFeatureList* { + auto* self = static_cast(h); + Input in{}; + in.samplerate = self->samplerate; + if (self->spec->domain == Domain::Time) { + in.timedata = std::span(input_buffers[0], self->block_size); + } else { + // Hosts deliver block_size/2 + 1 complex bins as interleaved real/imag floats, + // matching the guaranteed layout of std::complex ([complex.numbers.general]). + const std::size_t bins = (self->block_size / 2) + 1; + in.spectrum = std::span>( + reinterpret_cast*>(input_buffers[0]), bins + ); + } + self->value = self->spec->compute( + self->env, + in, + std::span(self->parameters.data(), self->spec->parameters.size()) + ); + return &self->featureList; + }; + d.getRemainingFeatures = [](VampPluginHandle) -> VampFeatureList* { + return &empty_feature_list(); + }; + d.releaseFeatureSet = [](VampFeatureList*) {}; + + return d; +} + +template +constexpr auto make_descriptor_table(std::index_sequence /*unused*/) { + return std::array{make_descriptor()...}; +} + +inline constexpr auto descriptor_table = make_descriptor_table( + std::make_index_sequence{} +); + +} // namespace + +// Export `vampGetPluginDescriptor` only. MSVC needs the linker pragma: vamp.h declares the function +// without dllexport, and a redeclaration with dllexport would be a linkage mismatch. +#if defined(_MSC_VER) +#define OPENAE_VAMP_EXPORT +#pragma comment(linker, "/EXPORT:vampGetPluginDescriptor") +#else +#define OPENAE_VAMP_EXPORT __attribute__((visibility("default"))) +#endif + +extern "C" OPENAE_VAMP_EXPORT const VampPluginDescriptor* vampGetPluginDescriptor( + unsigned int host_api_version, unsigned int index +) { + if (host_api_version < 1) { + return nullptr; + } + if (index >= descriptor_table.size()) { + return nullptr; + } + return &descriptor_table[index]; +} diff --git a/bindings/vamp/src/vamp.h b/bindings/vamp/src/vamp.h new file mode 100644 index 0000000..fced082 --- /dev/null +++ b/bindings/vamp/src/vamp.h @@ -0,0 +1,388 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Vamp + + An API for audio analysis and feature extraction plugins. + + Centre for Digital Music, Queen Mary, University of London. + Copyright 2006 Chris Cannam. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, copy, + modify, merge, publish, distribute, sublicense, and/or sell copies + of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + Except as contained in this notice, the names of the Centre for + Digital Music; Queen Mary, University of London; and Chris Cannam + shall not be used in advertising or otherwise to promote the sale, + use or other dealings in this Software without prior written + authorization. +*/ + +#ifndef VAMP_HEADER_INCLUDED +#define VAMP_HEADER_INCLUDED + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Plugin API version. This is incremented when a change is made that + * changes the binary layout of the descriptor records. When this + * happens, there should be a mechanism for retaining compatibility + * with older hosts and/or plugins. + * + * See also the vampApiVersion field in the plugin descriptor, and the + * hostApiVersion argument to the vampGetPluginDescriptor function. + */ +#define VAMP_API_VERSION 2 + +/** + * C language API for Vamp plugins. + * + * This is the formal plugin API for Vamp. Plugin authors may prefer + * to use the C++ classes provided in the Vamp plugin SDK, instead of + * using this API directly. There is an adapter class provided that + * makes C++ plugins available using this C API with relatively little + * work, and the C++ headers are more thoroughly documented. + * + * IMPORTANT: The comments in this file summarise the purpose of each + * of the declared fields and functions, but do not provide a complete + * guide to their permitted values and expected usage. Please refer + * to the C++ headers in the Vamp plugin SDK for further details and + * plugin lifecycle documentation. + */ + +typedef struct _VampParameterDescriptor +{ + /** Computer-usable name of the parameter. Must not change. [a-zA-Z0-9_-] */ + const char *identifier; + + /** Human-readable name of the parameter. May be translatable. */ + const char *name; + + /** Human-readable short text about the parameter. May be translatable. */ + const char *description; + + /** Human-readable unit of the parameter. */ + const char *unit; + + /** Minimum value. */ + float minValue; + + /** Maximum value. */ + float maxValue; + + /** Default value. Plugin is responsible for setting this on initialise. */ + float defaultValue; + + /** 1 if parameter values are quantized to a particular resolution. */ + int isQuantized; + + /** Quantization resolution, if isQuantized. */ + float quantizeStep; + + /** Human-readable names of the values, if isQuantized. May be NULL. */ + const char **valueNames; + +} VampParameterDescriptor; + +typedef enum +{ + /** Each process call returns results aligned with call's block start. */ + vampOneSamplePerStep, + + /** Returned results are evenly spaced at samplerate specified below. */ + vampFixedSampleRate, + + /** Returned results have their own individual timestamps. */ + vampVariableSampleRate + +} VampSampleType; + +typedef struct _VampOutputDescriptor +{ + /** Computer-usable name of the output. Must not change. [a-zA-Z0-9_-] */ + const char *identifier; + + /** Human-readable name of the output. May be translatable. */ + const char *name; + + /** Human-readable short text about the output. May be translatable. */ + const char *description; + + /** Human-readable name of the unit of the output. */ + const char *unit; + + /** 1 if output has equal number of values for each returned result. */ + int hasFixedBinCount; + + /** Number of values per result, if hasFixedBinCount. */ + unsigned int binCount; + + /** Names of returned value bins, if hasFixedBinCount. May be NULL. */ + const char **binNames; + + /** 1 if each returned value falls within the same fixed min/max range. */ + int hasKnownExtents; + + /** Minimum value for a returned result in any bin, if hasKnownExtents. */ + float minValue; + + /** Maximum value for a returned result in any bin, if hasKnownExtents. */ + float maxValue; + + /** 1 if returned results are quantized to a particular resolution. */ + int isQuantized; + + /** Quantization resolution for returned results, if isQuantized. */ + float quantizeStep; + + /** Time positioning method for returned results (see VampSampleType). */ + VampSampleType sampleType; + + /** Sample rate of returned results, if sampleType is vampFixedSampleRate. + "Resolution" of result, if sampleType is vampVariableSampleRate. */ + float sampleRate; + + /** 1 if the returned results for this output are known to have a + duration field. + + This field is new in Vamp API version 2; it must not be tested + for plugins that report an older API version in their plugin + descriptor. + */ + int hasDuration; + +} VampOutputDescriptor; + +typedef struct _VampFeature +{ + /** 1 if the feature has a timestamp (i.e. if vampVariableSampleRate). */ + int hasTimestamp; + + /** Seconds component of timestamp. */ + int sec; + + /** Nanoseconds component of timestamp. */ + int nsec; + + /** Number of values. Must be binCount if hasFixedBinCount. */ + unsigned int valueCount; + + /** Values for this returned sample. */ + float *values; + + /** Label for this returned sample. May be NULL. */ + char *label; + +} VampFeature; + +typedef struct _VampFeatureV2 +{ + /** 1 if the feature has a duration. */ + int hasDuration; + + /** Seconds component of duratiion. */ + int durationSec; + + /** Nanoseconds component of duration. */ + int durationNsec; + +} VampFeatureV2; + +typedef union _VampFeatureUnion +{ + // sizeof(featureV1) >= sizeof(featureV2) for backward compatibility + VampFeature v1; + VampFeatureV2 v2; + +} VampFeatureUnion; + +typedef struct _VampFeatureList +{ + /** Number of features in this feature list. */ + unsigned int featureCount; + + /** Features in this feature list. May be NULL if featureCount is + zero. + + If present, this array must contain featureCount feature + structures for a Vamp API version 1 plugin, or 2*featureCount + feature unions for a Vamp API version 2 plugin. + + The features returned by an API version 2 plugin must consist + of the same feature structures as in API version 1 for the + first featureCount array elements, followed by featureCount + unions that contain VampFeatureV2 structures (or NULL pointers + if no V2 feature structures are present). + */ + VampFeatureUnion *features; + +} VampFeatureList; + +typedef enum +{ + vampTimeDomain, + vampFrequencyDomain + +} VampInputDomain; + +typedef void *VampPluginHandle; + +typedef struct _VampPluginDescriptor +{ + /** API version with which this descriptor is compatible. */ + unsigned int vampApiVersion; + + /** Computer-usable name of the plugin. Must not change. [a-zA-Z0-9_-] */ + const char *identifier; + + /** Human-readable name of the plugin. May be translatable. */ + const char *name; + + /** Human-readable short text about the plugin. May be translatable. */ + const char *description; + + /** Human-readable name of plugin's author or vendor. */ + const char *maker; + + /** Version number of the plugin. */ + int pluginVersion; + + /** Human-readable summary of copyright or licensing for plugin. */ + const char *copyright; + + /** Number of parameter inputs. */ + unsigned int parameterCount; + + /** Fixed descriptors for parameter inputs. */ + const VampParameterDescriptor **parameters; + + /** Number of programs. */ + unsigned int programCount; + + /** Fixed names for programs. */ + const char **programs; + + /** Preferred input domain for audio input (time or frequency). */ + VampInputDomain inputDomain; + + /** Create and return a new instance of this plugin. */ + VampPluginHandle (*instantiate)(const struct _VampPluginDescriptor *, + float inputSampleRate); + + /** Destroy an instance of this plugin. */ + void (*cleanup)(VampPluginHandle); + + /** Initialise an instance following parameter configuration. */ + int (*initialise)(VampPluginHandle, + unsigned int inputChannels, + unsigned int stepSize, + unsigned int blockSize); + + /** Reset an instance, ready to use again on new input data. */ + void (*reset)(VampPluginHandle); + + /** Get a parameter value. */ + float (*getParameter)(VampPluginHandle, int); + + /** Set a parameter value. May only be called before initialise. */ + void (*setParameter)(VampPluginHandle, int, float); + + /** Get the current program (if programCount > 0). */ + unsigned int (*getCurrentProgram)(VampPluginHandle); + + /** Set the current program. May only be called before initialise. */ + void (*selectProgram)(VampPluginHandle, unsigned int); + + /** Get the plugin's preferred processing window increment in samples. */ + unsigned int (*getPreferredStepSize)(VampPluginHandle); + + /** Get the plugin's preferred processing window size in samples. */ + unsigned int (*getPreferredBlockSize)(VampPluginHandle); + + /** Get the minimum number of input channels this plugin can handle. */ + unsigned int (*getMinChannelCount)(VampPluginHandle); + + /** Get the maximum number of input channels this plugin can handle. */ + unsigned int (*getMaxChannelCount)(VampPluginHandle); + + /** Get the number of feature outputs (distinct sets of results). */ + unsigned int (*getOutputCount)(VampPluginHandle); + + /** Get a descriptor for a given feature output. Returned pointer + is valid only until next call to getOutputDescriptor for this + handle, or releaseOutputDescriptor for this descriptor. Host + must call releaseOutputDescriptor after use. */ + VampOutputDescriptor *(*getOutputDescriptor)(VampPluginHandle, + unsigned int); + + /** Destroy a descriptor for a feature output. */ + void (*releaseOutputDescriptor)(VampOutputDescriptor *); + + /** Process an input block and return a set of features. Returned + pointer is valid only until next call to process, + getRemainingFeatures, or cleanup for this handle, or + releaseFeatureSet for this feature set. Host must call + releaseFeatureSet after use. */ + VampFeatureList *(*process)(VampPluginHandle, + const float *const *inputBuffers, + int sec, + int nsec); + + /** Return any remaining features at the end of processing. */ + VampFeatureList *(*getRemainingFeatures)(VampPluginHandle); + + /** Release a feature set returned from process or getRemainingFeatures. */ + void (*releaseFeatureSet)(VampFeatureList *); + +} VampPluginDescriptor; + + +/** Get the descriptor for a given plugin index in this library. + Return NULL if the index is outside the range of valid indices for + this plugin library. + + The hostApiVersion argument tells the library code the highest + Vamp API version supported by the host. The function should + return a plugin descriptor compatible with the highest API version + supported by the library that is no higher than that supported by + the host. Provided the descriptor has the correct vampApiVersion + field for its actual compatibility level, the host should be able + to do the right thing with it: use it if possible, discard it + otherwise. + + This is the only symbol that a Vamp plugin actually needs to + export from its shared object; all others can be hidden. See the + accompanying documentation for notes on how to achieve this with + certain compilers. +*/ +const VampPluginDescriptor *vampGetPluginDescriptor + (unsigned int hostApiVersion, unsigned int index); + + +/** Function pointer type for vampGetPluginDescriptor. */ +typedef const VampPluginDescriptor *(*VampGetPluginDescriptorFunction) + (unsigned int, unsigned int); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/bindings/vamp/tests/CMakeLists.txt b/bindings/vamp/tests/CMakeLists.txt new file mode 100644 index 0000000..8014b19 --- /dev/null +++ b/bindings/vamp/tests/CMakeLists.txt @@ -0,0 +1,12 @@ +add_executable(openae_vamp_test_plugin test_plugin.cpp ../src/plugin.cpp) +target_link_libraries( + openae_vamp_test_plugin + PRIVATE + openae_project_options + openae::openae + Catch2::Catch2WithMain +) +target_include_directories(openae_vamp_test_plugin PRIVATE ../src) + +include(Catch) +catch_discover_tests(openae_vamp_test_plugin) diff --git a/bindings/vamp/tests/test_plugin.cpp b/bindings/vamp/tests/test_plugin.cpp new file mode 100644 index 0000000..7867b79 --- /dev/null +++ b/bindings/vamp/tests/test_plugin.cpp @@ -0,0 +1,216 @@ +#include "vamp.h" + +#include "openae/common.hpp" +#include "openae/features.hpp" + +#include +#include + +#include +#include +#include +#include +#include + +namespace { + +constexpr unsigned int host_api_version = VAMP_API_VERSION; +constexpr float samplerate = 1000.0F; +constexpr unsigned int block_size = 128; +constexpr unsigned int bins = (block_size / 2) + 1; + +const VampPluginDescriptor* find_plugin(const char* identifier) { + for (unsigned int i = 0;; ++i) { + const auto* d = vampGetPluginDescriptor(host_api_version, i); + if (d == nullptr) { + return nullptr; + } + if (std::strcmp(d->identifier, identifier) == 0) { + return d; + } + } +} + +unsigned int plugin_count() { + unsigned int n = 0; + while (vampGetPluginDescriptor(host_api_version, n) != nullptr) { + ++n; + } + return n; +} + +// Process a single block and return the single output value. +float run_plugin(const VampPluginDescriptor* d, VampPluginHandle handle, const float* block) { + const std::array inputs = {block}; + auto* features = d->process(handle, inputs.data(), 0, 0); + REQUIRE(features != nullptr); + REQUIRE(features->featureCount == 1); + REQUIRE(features->features[0].v1.valueCount == 1); + const float value = features->features[0].v1.values[0]; + d->releaseFeatureSet(features); + return value; +} + +// Complex bins -> interleaved real/imag pairs (Vamp host format). +std::vector interleave(std::span> spectrum) { + std::vector buffer; + buffer.reserve(2 * spectrum.size()); + for (const auto& c : spectrum) { + buffer.push_back(c.real()); + buffer.push_back(c.imag()); + } + return buffer; +} + +} // namespace + +TEST_CASE("vampGetPluginDescriptor rejects unsupported host API version", "[vamp]") { + CHECK(vampGetPluginDescriptor(0, 0) == nullptr); +} + +TEST_CASE("vampGetPluginDescriptor returns null past the last plugin", "[vamp]") { + const auto n = plugin_count(); + REQUIRE(n > 0); + CHECK(vampGetPluginDescriptor(host_api_version, n) == nullptr); + CHECK(vampGetPluginDescriptor(host_api_version, n + 100) == nullptr); +} + +// NOLINTNEXTLINE(readability-function-cognitive-complexity), inflated by assertion macros +TEST_CASE("every plugin descriptor has well-formed metadata", "[vamp]") { + const auto n = plugin_count(); + for (unsigned int i = 0; i < n; ++i) { + const auto* d = vampGetPluginDescriptor(host_api_version, i); + REQUIRE(d != nullptr); + CAPTURE(i, d->identifier); + // Descriptors declare API version 1 (no V2-only features used). + CHECK(d->vampApiVersion == 1); + CHECK(d->identifier != nullptr); + CHECK(d->name != nullptr); + CHECK(d->description != nullptr); + CHECK(d->maker != nullptr); + CHECK(std::strlen(d->identifier) > 0); + CHECK(std::strlen(d->name) > 0); + CHECK((d->inputDomain == vampTimeDomain || d->inputDomain == vampFrequencyDomain)); + for (unsigned int p = 0; p < d->parameterCount; ++p) { + REQUIRE(d->parameters[p] != nullptr); + CHECK(d->parameters[p]->identifier != nullptr); + } + } +} + +// The algorithms are covered by the library tests. Here, expected values come from direct library +// calls, so a mismatch can only be a marshalling error in the adapter, not an algorithm change. + +TEST_CASE("process() feeds time-domain input to the feature and returns its value", "[vamp]") { + const auto* d = find_plugin("rms"); + REQUIRE(d != nullptr); + REQUIRE(d->inputDomain == vampTimeDomain); + + // Varying signal so marshalling errors change the result. + std::vector block(block_size); + for (unsigned int i = 0; i < block_size; ++i) { + block[i] = static_cast(i % 17) - 8.0F; + } + + openae::Env env{}; + const auto expected = + openae::features::rms(env, {.samplerate = samplerate, .timedata = block}); + + auto* handle = d->instantiate(d, samplerate); + REQUIRE(handle != nullptr); + REQUIRE(d->initialise(handle, 1, block_size, block_size) == 1); + CHECK(d->getOutputCount(handle) == 1); + CHECK_THAT(run_plugin(d, handle, block.data()), Catch::Matchers::WithinRel(expected, 1e-6F)); + d->cleanup(handle); +} + +TEST_CASE("process() maps frequency-domain input to a one-sided complex spectrum", "[vamp]") { + const auto* d = find_plugin("spectral-peak-frequency"); + REQUIRE(d != nullptr); + REQUIRE(d->inputDomain == vampFrequencyDomain); + + // Hot interior bin on a varying background: bin count or layout errors change the result. + std::vector> spectrum(bins); + for (unsigned int b = 0; b < bins; ++b) { + spectrum[b] = {0.01F * static_cast(b), 0.02F * static_cast(b)}; + } + spectrum[16] = {10.0F, 0.0F}; + const auto block = interleave(spectrum); + + openae::Env env{}; + const auto expected = openae::features::spectral_peak_frequency( + env, {.samplerate = samplerate, .spectrum = spectrum} + ); + + auto* handle = d->instantiate(d, samplerate); + REQUIRE(handle != nullptr); + REQUIRE(d->initialise(handle, 1, block_size, block_size) == 1); + CHECK_THAT(run_plugin(d, handle, block.data()), Catch::Matchers::WithinRel(expected, 1e-6F)); + d->cleanup(handle); +} + +// NOLINTNEXTLINE(readability-function-cognitive-complexity), inflated by assertion macros +TEST_CASE("parameters round-trip and are forwarded to the feature computation", "[vamp]") { + const auto* d = find_plugin("partial-power"); + REQUIRE(d != nullptr); + REQUIRE(d->inputDomain == vampFrequencyDomain); + REQUIRE(d->parameterCount == 2); + + auto* handle = d->instantiate(d, samplerate); + REQUIRE(handle != nullptr); + + SECTION("defaults and get/set round-trip") { + CHECK(d->getParameter(handle, 0) == d->parameters[0]->defaultValue); + CHECK(d->getParameter(handle, 1) == d->parameters[1]->defaultValue); + + d->setParameter(handle, 0, 100.0F); + CHECK(d->getParameter(handle, 0) == 100.0F); + + // Out-of-range indices: get returns 0, set is a no-op. + CHECK(d->getParameter(handle, -1) == 0.0F); + CHECK(d->getParameter(handle, 2) == 0.0F); + d->setParameter(handle, -1, 1.0F); + d->setParameter(handle, 2, 1.0F); + CHECK(d->getParameter(handle, 0) == 100.0F); + } + + SECTION("parameter values are forwarded to the computation") { + constexpr float fmin = 50.0F; + constexpr float fmax = 200.0F; + + std::vector> spectrum(bins); + for (unsigned int b = 0; b < bins; ++b) { + spectrum[b] = {static_cast(b), 0.0F}; + } + const auto block = interleave(spectrum); + + openae::Env env{}; + const openae::features::Input input{.samplerate = samplerate, .spectrum = spectrum}; + const auto expected = openae::features::partial_power(env, input, fmin, fmax); + // Must differ from the default-parameter result, or dropped parameters go unnoticed. + REQUIRE( + expected + != openae::features::partial_power( + env, input, d->parameters[0]->defaultValue, d->parameters[1]->defaultValue + ) + ); + + d->setParameter(handle, 0, fmin); + d->setParameter(handle, 1, fmax); + REQUIRE(d->initialise(handle, 1, block_size, block_size) == 1); + CHECK_THAT( + run_plugin(d, handle, block.data()), Catch::Matchers::WithinRel(expected, 1e-6F) + ); + } + + d->cleanup(handle); +} + +TEST_CASE("rejecting non-mono channel counts in initialise", "[vamp]") { + const auto* d = find_plugin("rms"); + REQUIRE(d != nullptr); + auto* handle = d->instantiate(d, samplerate); + REQUIRE(handle != nullptr); + CHECK(d->initialise(handle, 2, block_size, block_size) == 0); + d->cleanup(handle); +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f81710d..3106ddd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,16 +1,3 @@ -include(FetchContent) -option(XXHASH_BUILD_XXHSUM "Build the xxhsum binary" OFF) -FetchContent_Declare( - xxHash - GIT_REPOSITORY https://github.com/Cyan4973/xxHash.git - GIT_TAG v0.8.3 - SOURCE_SUBDIR cmake_unofficial - EXCLUDE_FROM_ALL - SYSTEM - FIND_PACKAGE_ARGS 0.8.3 -) -FetchContent_MakeAvailable(xxHash) - configure_file( "${PROJECT_SOURCE_DIR}/include/openae/config.hpp.in" "${PROJECT_BINARY_DIR}/include/openae/config.hpp" diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2bc7667..372f2a2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,19 +1,5 @@ include(FetchContent) -option(CATCH_INSTALL_DOCS "Install documentation alongside library" OFF) -FetchContent_Declare( - Catch2 - GIT_REPOSITORY https://github.com/catchorg/Catch2.git - GIT_TAG v3.8.0 - EXCLUDE_FROM_ALL - SYSTEM - FIND_PACKAGE_ARGS 3.8.0 -) -FetchContent_MakeAvailable(Catch2) -if(TARGET Catch2) - set_target_properties(Catch2 Catch2WithMain PROPERTIES CXX_CLANG_TIDY "") -endif() - FetchContent_Declare( tomlplusplus GIT_REPOSITORY https://github.com/marzer/tomlplusplus.git From 161d9a5c18dcaf0b33abdbed527687f3b5430281 Mon Sep 17 00:00:00 2001 From: Lukas Berbuer Date: Sun, 2 Aug 2026 23:01:09 +0200 Subject: [PATCH 2/6] fix: initialize all Input fields in Vamp plugin tests --- bindings/vamp/tests/test_plugin.cpp | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/bindings/vamp/tests/test_plugin.cpp b/bindings/vamp/tests/test_plugin.cpp index 7867b79..c810c05 100644 --- a/bindings/vamp/tests/test_plugin.cpp +++ b/bindings/vamp/tests/test_plugin.cpp @@ -113,8 +113,15 @@ TEST_CASE("process() feeds time-domain input to the feature and returns its valu } openae::Env env{}; - const auto expected = - openae::features::rms(env, {.samplerate = samplerate, .timedata = block}); + const auto expected = openae::features::rms( + env, + { + .samplerate = samplerate, + .timedata = block, + .spectrum = {}, + .fingerprint = {}, + } + ); auto* handle = d->instantiate(d, samplerate); REQUIRE(handle != nullptr); @@ -139,7 +146,13 @@ TEST_CASE("process() maps frequency-domain input to a one-sided complex spectrum openae::Env env{}; const auto expected = openae::features::spectral_peak_frequency( - env, {.samplerate = samplerate, .spectrum = spectrum} + env, + { + .samplerate = samplerate, + .timedata = {}, + .spectrum = spectrum, + .fingerprint = {}, + } ); auto* handle = d->instantiate(d, samplerate); @@ -185,7 +198,12 @@ TEST_CASE("parameters round-trip and are forwarded to the feature computation", const auto block = interleave(spectrum); openae::Env env{}; - const openae::features::Input input{.samplerate = samplerate, .spectrum = spectrum}; + const openae::features::Input input{ + .samplerate = samplerate, + .timedata = {}, + .spectrum = spectrum, + .fingerprint = {}, + }; const auto expected = openae::features::partial_power(env, input, fmin, fmax); // Must differ from the default-parameter result, or dropped parameters go unnoticed. REQUIRE( From 5e437399455c39a7d8142e7cbd961de6a0f91f67 Mon Sep 17 00:00:00 2001 From: Lukas Berbuer Date: Sun, 2 Aug 2026 23:03:43 +0200 Subject: [PATCH 3/6] fix: clang-tidy errors --- bindings/vamp/src/plugin.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bindings/vamp/src/plugin.cpp b/bindings/vamp/src/plugin.cpp index a8e1a85..98a3cc6 100644 --- a/bindings/vamp/src/plugin.cpp +++ b/bindings/vamp/src/plugin.cpp @@ -472,9 +472,10 @@ inline constexpr auto descriptor_table = make_descriptor_table( #endif extern "C" OPENAE_VAMP_EXPORT const VampPluginDescriptor* vampGetPluginDescriptor( - unsigned int host_api_version, unsigned int index + // NOLINTNEXTLINE(readability-identifier-naming), parameter name dictated by vamp.h declaration + unsigned int hostApiVersion, unsigned int index ) { - if (host_api_version < 1) { + if (hostApiVersion < 1) { return nullptr; } if (index >= descriptor_table.size()) { From 121e15304a3a22301446a0831f6af92012c0e5c6 Mon Sep 17 00:00:00 2001 From: Lukas Berbuer Date: Mon, 3 Aug 2026 00:04:48 +0200 Subject: [PATCH 4/6] ci: build and release Vamp plugin binaries Build the Vamp plugin on Linux (x86_64, arm64), macOS (universal binary) and Windows, and attach platform archives to GitHub releases. Plugin binaries are self-contained: static openae library, static MSVC runtime (no VC++ redistributable required) and macOS deployment target 14.0. The vamp preset pins the same settings for local builds. --- .github/workflows/vamp.yml | 97 ++++++++++++++++++++++++++++++++++++ CMakePresets.json | 2 + bindings/vamp/dist/README.md | 37 ++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 .github/workflows/vamp.yml create mode 100644 bindings/vamp/dist/README.md diff --git a/.github/workflows/vamp.yml b/.github/workflows/vamp.yml new file mode 100644 index 0000000..780ada0 --- /dev/null +++ b/.github/workflows/vamp.yml @@ -0,0 +1,97 @@ +name: Vamp plugin + +on: + push: + branches: [main] + pull_request: + release: + types: [published] + workflow_dispatch: + +jobs: + build: + name: Build ${{ matrix.config.name }} + runs-on: ${{ matrix.config.os }} + strategy: + fail-fast: false + matrix: + config: + - name: linux-x86_64 + os: ubuntu-24.04 + + - name: linux-arm64 + os: ubuntu-24.04-arm + + - name: macos # universal binary + os: macos-15 + osx_architectures: "x86_64;arm64" + + - name: windows-x64 + os: windows-2022 + + steps: + - uses: actions/checkout@v6 + + - name: Configure CMake + env: + # requires 14.0: https://developer.apple.com/xcode/cpp/ + MACOSX_DEPLOYMENT_TARGET: "14.0" + CMAKE_OSX_ARCHITECTURES: ${{ matrix.config.osx_architectures }} + run: > + cmake -S . -B build + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded + -DBUILD_SHARED_LIBS=OFF + -DOPENAE_BUILD_TESTS=ON + -DOPENAE_BUILD_VAMP=ON + + - name: Build + run: cmake --build build --config Release --parallel + + - name: Run tests + run: ctest --test-dir build --build-config Release --output-on-failure + + - name: Package + shell: bash + run: | + cmake --install build --config Release --component vamp --prefix stage + mkdir package + cp stage/lib*/vamp/* package/ + cp bindings/vamp/dist/README.md package/ + cp LICENSE package/ + + - name: Verify universal binary + if: runner.os == 'macOS' + run: lipo package/openae.dylib -verify_arch x86_64 arm64 + + - uses: actions/upload-artifact@v6 + with: + name: vamp-${{ matrix.config.name }} + path: package/* + if-no-files-found: error + + release-assets: + name: Attach release assets + if: github.event_name == 'release' + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v6 + with: + pattern: vamp-* + path: artifacts/ + + - name: Create archives and upload to release + env: + GH_TOKEN: ${{ github.token }} + run: | + version="${GITHUB_REF_NAME#v}" + cd artifacts + for dir in vamp-*/; do + platform="${dir#vamp-}" + platform="${platform%/}" + (cd "$dir" && zip -r "../openae-vamp-${version}-${platform}.zip" .) + done + gh release upload "$GITHUB_REF_NAME" openae-vamp-*.zip --clobber --repo "$GITHUB_REPOSITORY" diff --git a/CMakePresets.json b/CMakePresets.json index d25c8b0..b3cd86e 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -45,6 +45,8 @@ "inherits": ["release"], "installDir": "${sourceDir}/install/${presetName}", "cacheVariables": { + "BUILD_SHARED_LIBS": "OFF", + "CMAKE_MSVC_RUNTIME_LIBRARY": "MultiThreaded", "OPENAE_BUILD_VAMP": "ON" } }, diff --git a/bindings/vamp/dist/README.md b/bindings/vamp/dist/README.md new file mode 100644 index 0000000..eb11647 --- /dev/null +++ b/bindings/vamp/dist/README.md @@ -0,0 +1,37 @@ +# OpenAE Vamp plugin + +[Vamp](https://vamp-plugins.org) audio analysis plugin exposing the [OpenAE](https://openae.io) acoustic emission features. +The plugin can be used in Vamp hosts such as [Sonic Visualiser](https://sonicvisualiser.org), [Audacity](https://www.audacityteam.org) and [Sonic Annotator](https://vamp-plugins.org/sonic-annotator/). + +## Installation + +Copy the plugin binary into one of the Vamp plugin directories and restart your Vamp host: + +**Linux** (`openae.so`) + +- `$HOME/vamp` +- `/usr/local/lib/vamp` +- `/usr/lib/vamp` + +**macOS** (`openae.dylib`) + +- `$HOME/Library/Audio/Plug-Ins/Vamp` +- `/Library/Audio/Plug-Ins/Vamp` + +The binary is not notarized by Apple. macOS quarantines downloaded files and will refuse to load the plugin ("cannot be opened because the developer cannot be verified"). Remove the quarantine attribute after extracting: + +```sh +xattr -d com.apple.quarantine openae.dylib +``` + +**Windows** (`openae.dll`) + +- `C:\Program Files\Vamp Plugins` + +Alternatively, set the `VAMP_PATH` environment variable to a custom plugin directory. + +## Links + +- Documentation: https://openae.io +- Source code: https://github.com/openae-io/openae-lib +- Issues: https://github.com/openae-io/openae-lib/issues From cec0cfa6117f0e22979c63ea8c07d13c8909df92 Mon Sep 17 00:00:00 2001 From: Lukas Berbuer Date: Mon, 3 Aug 2026 09:55:19 +0200 Subject: [PATCH 5/6] ci: package versioned Vamp plugin archives in build jobs Name archives openae-vamp--.zip at build time using git describe: the exact tag on release builds, last tag + commit count + short hash on CI builds. The release job now only downloads and uploads the pre-named archives instead of reconstructing names. Use cmake -E tar for portable zip creation across all runners. --- .github/workflows/vamp.yml | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/.github/workflows/vamp.yml b/.github/workflows/vamp.yml index 780ada0..49c8e22 100644 --- a/.github/workflows/vamp.yml +++ b/.github/workflows/vamp.yml @@ -31,6 +31,8 @@ jobs: steps: - uses: actions/checkout@v6 + with: + fetch-depth: 0 # tags + history for git describe - name: Configure CMake env: @@ -64,10 +66,19 @@ jobs: if: runner.os == 'macOS' run: lipo package/openae.dylib -verify_arch x86_64 arm64 + - name: Create archive + # cmake -E tar for a portable zip (no zip command on Windows runners) + shell: bash + working-directory: package + run: | + version=$(git describe --tags --always) + version="${version#v}" + cmake -E tar cf "../openae-vamp-${version}-${{ matrix.config.name }}.zip" --format=zip -- . + - uses: actions/upload-artifact@v6 with: name: vamp-${{ matrix.config.name }} - path: package/* + path: openae-vamp-*.zip if-no-files-found: error release-assets: @@ -82,16 +93,9 @@ jobs: with: pattern: vamp-* path: artifacts/ + merge-multiple: true - - name: Create archives and upload to release + - name: Upload archives to release env: GH_TOKEN: ${{ github.token }} - run: | - version="${GITHUB_REF_NAME#v}" - cd artifacts - for dir in vamp-*/; do - platform="${dir#vamp-}" - platform="${platform%/}" - (cd "$dir" && zip -r "../openae-vamp-${version}-${platform}.zip" .) - done - gh release upload "$GITHUB_REF_NAME" openae-vamp-*.zip --clobber --repo "$GITHUB_REPOSITORY" + run: gh release upload "$GITHUB_REF_NAME" artifacts/openae-vamp-*.zip --clobber --repo "$GITHUB_REPOSITORY" From 06b83336facb89be040cbd1cf37d9b77ee72784b Mon Sep 17 00:00:00 2001 From: Lukas Berbuer Date: Mon, 3 Aug 2026 10:11:59 +0200 Subject: [PATCH 6/6] ci: report generated Vamp plugin archives in workflow summary --- .github/workflows/vamp.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/vamp.yml b/.github/workflows/vamp.yml index 49c8e22..e5fdec3 100644 --- a/.github/workflows/vamp.yml +++ b/.github/workflows/vamp.yml @@ -81,6 +81,30 @@ jobs: path: openae-vamp-*.zip if-no-files-found: error + summary: + name: Package summary + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v6 + with: + pattern: vamp-* + path: artifacts/ + merge-multiple: true + + - name: Write summary table + working-directory: artifacts + run: | + { + echo "### Vamp plugin packages" + echo "" + echo "| File | Size | SHA256 |" + echo "| ---- | ---- | ------ |" + for f in openae-vamp-*.zip; do + echo "| \`$f\` | $(du -h "$f" | cut -f1) | \`$(sha256sum "$f" | cut -d' ' -f1)\` |" + done + } >> "$GITHUB_STEP_SUMMARY" + release-assets: name: Attach release assets if: github.event_name == 'release'