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
18 changes: 18 additions & 0 deletions cmake/avendish.clap.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@ if(DEFINED AVND_ENABLE_CLAP AND NOT AVND_ENABLE_CLAP)
endif()

find_path(CLAP_HEADER NAMES clap/clap.h)
if(NOT CLAP_HEADER)
# The CLAP "SDK" is a set of plain C headers: fetch them when absent.
include(FetchContent)
FetchContent_Declare(
avnd_clap_headers
GIT_REPOSITORY "https://github.com/free-audio/clap"
GIT_TAG 1.2.6
GIT_PROGRESS true
)
FetchContent_GetProperties(avnd_clap_headers)
if(NOT avnd_clap_headers_POPULATED)
FetchContent_Populate(avnd_clap_headers)
endif()
if(EXISTS "${avnd_clap_headers_SOURCE_DIR}/include/clap/clap.h")
set(CLAP_HEADER "${avnd_clap_headers_SOURCE_DIR}/include" CACHE PATH "clap headers" FORCE)
endif()
endif()

if(NOT CLAP_HEADER)
message(STATUS "Clap not found, skipping bindings...")

Expand Down
3 changes: 3 additions & 0 deletions cmake/avendish.dependencies.cmake
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
include(FetchContent)

if(NOT TARGET fmt::fmt AND NOT TARGET fmt::fmt_header_only)
# fmt >= 12 auto-enables its C++-modules target when CMake supports it and
# then hard-requires a module-scanning toolchain; we never want that here.
set(FMT_MODULE OFF CACHE BOOL "Build fmt as a C++ module")
FetchContent_Declare(
fmt
GIT_REPOSITORY "https://github.com/fmtlib/fmt"
Expand Down
34 changes: 34 additions & 0 deletions cmake/avendish.vst3.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,32 @@ if(DEFINED AVND_ENABLE_VST3 AND NOT AVND_ENABLE_VST3)
endif()

set(VST3_SDK_ROOT "" CACHE PATH "VST3 SDK path")
option(AVND_FETCH_VST3_SDK "Fetch the VST3 SDK when VST3_SDK_ROOT is not set" OFF)

if(NOT VST3_SDK_ROOT AND AVND_FETCH_VST3_SDK)
include(FetchContent)
FetchContent_Declare(
avnd_vst3sdk
GIT_REPOSITORY "https://github.com/steinbergmedia/vst3sdk"
GIT_TAG v3.7.14_build_55
GIT_SUBMODULES base cmake pluginterfaces public.sdk
GIT_PROGRESS true
)
FetchContent_GetProperties(avnd_vst3sdk)
if(NOT avnd_vst3sdk_POPULATED)
FetchContent_Populate(avnd_vst3sdk)
endif()
if(EXISTS "${avnd_vst3sdk_SOURCE_DIR}/pluginterfaces/base/funknown.h")
set(VST3_SDK_ROOT "${avnd_vst3sdk_SOURCE_DIR}" CACHE PATH "VST3 SDK path" FORCE)
# Plug-in library only: no samples, hosting tools, validator or vstgui
set(SMTG_ENABLE_VST3_PLUGIN_EXAMPLES OFF CACHE BOOL "" FORCE)
set(SMTG_ENABLE_VST3_HOSTING_EXAMPLES OFF CACHE BOOL "" FORCE)
set(SMTG_ENABLE_VSTGUI_SUPPORT OFF CACHE BOOL "" FORCE)
set(SMTG_CREATE_PLUGIN_LINK OFF CACHE BOOL "" FORCE)
set(SMTG_RUN_VST_VALIDATOR OFF CACHE BOOL "" FORCE)
endif()
endif()

if(NOT VST3_SDK_ROOT)
function(avnd_make_vst3)
endfunction()
Expand Down Expand Up @@ -53,6 +79,14 @@ endif()

add_subdirectory("${VST3_SDK_ROOT}" "${CMAKE_BINARY_DIR}/vst3_sdk")

# The SDK's `base` target uses pluginterfaces symbols (FUID, FUnknown, ...)
# but never declares the dependency; with a single-pass linker (GNU ld) base
# then ends up after pluginterfaces on the link line and the plug-ins ship
# with undefined Steinberg::FUID::* symbols. Declare it for them.
if(TARGET base AND TARGET pluginterfaces)
target_link_libraries(base PUBLIC pluginterfaces)
endif()

function(avnd_make_vst3)
cmake_parse_arguments(AVND "" "TARGET;MAIN_FILE;MAIN_CLASS;C_NAME" "" ${ARGN})
set(AVND_FX_TARGET "${AVND_TARGET}_vst3")
Expand Down
106 changes: 94 additions & 12 deletions include/avnd/binding/clap/audio_effect.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
#include <avnd/wrappers/control_display.hpp>
#include <avnd/wrappers/controls.hpp>
#include <avnd/wrappers/controls_double.hpp>
#include <avnd/wrappers/controls_storage.hpp>
#include <avnd/wrappers/metadatas.hpp>
#include <avnd/wrappers/prepare.hpp>
#include <avnd/wrappers/process_adapter.hpp>
#include <avnd/wrappers/widgets.hpp>
#include <clap/all.h>
Expand Down Expand Up @@ -126,6 +128,9 @@ struct SimpleAudioEffect : clap_plugin

AVND_NO_UNIQUE_ADDRESS avnd_clap::audio_bus_info<T> audio_busses;
AVND_NO_UNIQUE_ADDRESS avnd::process_adapter<T> processor;

// Per-buffer storage backing sample-accurate control ports
AVND_NO_UNIQUE_ADDRESS avnd::control_storage<T> control_buffers;
AVND_NO_UNIQUE_ADDRESS midi_processor<T> midi;

float sample_rate{44100.};
Expand All @@ -147,6 +152,7 @@ struct SimpleAudioEffect : clap_plugin
auto& p = *self(plugin);
p.sample_rate = sample_rate;
p.buffer_size = max_frames_count;
p.ensure_scratch(max_frames_count);

p.start();
return true;
Expand Down Expand Up @@ -190,6 +196,10 @@ struct SimpleAudioEffect : clap_plugin
{
avnd::init_controls(effect);
}

// Safe no-op handlers for worker.request / request_channels members:
// an empty std::function call terminates under -fno-exceptions.
avnd::wire_fallback_callbacks(effect);
}

void start()
Expand All @@ -201,9 +211,17 @@ struct SimpleAudioEffect : clap_plugin
.frames_per_buffer = buffer_size,
.rate = sample_rate};

// Hosts choose float or double per process() call (data32 / data64):
// conversion storage must exist for both source sample types.
processor.allocate_buffers(setup_info, float{});
processor.allocate_buffers(setup_info, double{});
effect.init_channels(setup_info.input_channels, setup_info.output_channels);

// Sample-accurate control ports need their per-buffer storage reserved
// before process() (else the effect reads unallocated `values` arrays).
if constexpr(sizeof(control_buffers) > 1)
control_buffers.reserve_space(effect, buffer_size);

// Setup buffers for storing MIDI messages
if constexpr(midi_in_info::size > 0)
{
Expand Down Expand Up @@ -251,15 +269,52 @@ struct SimpleAudioEffect : clap_plugin
}

using samples_t = std::decay_t<decltype(inputs[0][0])>;

// Hosts may hand buses whose channel rows are null (clap-validator
// does): point those at binding-owned scratch so no adapter shape ever
// dereferences null — silence in, discarded writes out.
ensure_scratch(process.frames_count);
for(int i = 0; i < in_N; i++)
if(!inputs[i])
inputs[i] = zero_buffer<samples_t>();
for(int i = 0; i < out_N; i++)
if(!outputs[i])
outputs[i] = trash_buffer<samples_t>();

processor.process(
effect, avnd::span<samples_t*>{inputs, std::size_t(in_N)},
avnd::span<samples_t*>{outputs, std::size_t(out_N)}, process.frames_count);
}

// Scratch rows substituted for null host channel pointers. Sized in
// activate(); the process-time call only allocates if a non-conformant
// host processes more frames than it activated for (or never activated).
void ensure_scratch(uint32_t frames)
{
if(m_zero64.size() < frames)
{
m_zero64.assign(frames, 0.);
m_trash64.resize(frames);
}
}
template <typename Sample>
Sample* zero_buffer() noexcept
{
// A double row of N zeros is also a valid float row of 2N zeros.
return reinterpret_cast<Sample*>(m_zero64.data());
}
template <typename Sample>
Sample* trash_buffer() noexcept
{
return reinterpret_cast<Sample*>(m_trash64.data());
}
std::vector<double> m_zero64, m_trash64;

void process(const clap_process& process)
{
// Clear the control out ports
// FIXME
// Clear the sample-accurate control out ports
if constexpr(sizeof(control_buffers) > 1)
control_buffers.clear_outputs(this->effect);

// Clear the midi out ports
midi.clear_outputs(this->effect);
Expand All @@ -272,33 +327,52 @@ struct SimpleAudioEffect : clap_plugin
int in_N = avnd::input_channels<T>(2);
int out_N = avnd::output_channels<T>(2);

if constexpr(avnd::float_processor<T>)
// SUPPORTS_64BITS is a per-process() negotiation: the host decides
// each call which of data32 / data64 it filled. Read what is actually
// there — the adapters convert between float and double effects and
// buffers as needed.
const bool host_64 = [&] {
if(process.audio_inputs_count + process.audio_outputs_count == 0)
return avnd::double_processor<T>;
for(uint32_t b = 0; b < process.audio_inputs_count; b++)
if(process.audio_inputs[b].channel_count > 0
&& !process.audio_inputs[b].data64)
return false;
for(uint32_t b = 0; b < process.audio_outputs_count; b++)
if(process.audio_outputs[b].channel_count > 0
&& !process.audio_outputs[b].data64)
return false;
return true;
}();

if(host_64)
{
auto inputs = (float**)alloca(sizeof(float*) * std::max(in_N, 1));
auto outputs = (float**)alloca(sizeof(float*) * std::max(out_N, 1));
auto inputs = (double**)alloca(sizeof(double*) * std::max(in_N, 1));
auto outputs = (double**)alloca(sizeof(double*) * std::max(out_N, 1));
// Null the slots so the zero-filled-channels invariant substitutes
// silent buffers rather than the effect dereferencing garbage.
std::fill_n(inputs, in_N, nullptr);
std::fill_n(outputs, out_N, nullptr);

process_impl<&clap_audio_buffer::data32>(process, inputs, in_N, outputs, out_N);
process_impl<&clap_audio_buffer::data64>(process, inputs, in_N, outputs, out_N);
}
else if constexpr(avnd::double_processor<T>)
else
{
auto inputs = (double**)alloca(sizeof(double*) * std::max(in_N, 1));
auto outputs = (double**)alloca(sizeof(double*) * std::max(out_N, 1));
auto inputs = (float**)alloca(sizeof(float*) * std::max(in_N, 1));
auto outputs = (float**)alloca(sizeof(float*) * std::max(out_N, 1));
std::fill_n(inputs, in_N, nullptr);
std::fill_n(outputs, out_N, nullptr);

process_impl<&clap_audio_buffer::data64>(process, inputs, in_N, outputs, out_N);
process_impl<&clap_audio_buffer::data32>(process, inputs, in_N, outputs, out_N);
}
}

// Process the output events
process_out_events(process);

// Clear the control in ports
// FIXME
// Clear the sample-accurate control in ports
if constexpr(sizeof(control_buffers) > 1)
control_buffers.clear_inputs(this->effect);

// Clear the midi in ports
midi.clear_inputs(this->effect);
Expand Down Expand Up @@ -433,6 +507,14 @@ struct SimpleAudioEffect : clap_plugin
info->max_value = avnd::get_enum_choices_count<C>() - 1;
info->flags |= CLAP_PARAM_IS_STEPPED;
}
else if constexpr(requires { std::size(range.values); })
{
// Values-list controls (combo boxes...): the value is an index
// into range.values; there is no range.min / range.max.
info->min_value = 0;
info->max_value = double(std::size(range.values)) - 1.;
info->flags |= CLAP_PARAM_IS_STEPPED;
}
else
{
if constexpr(requires {
Expand Down
44 changes: 34 additions & 10 deletions include/avnd/binding/clap/bus_info.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ struct event_bus_info
[&]<std::size_t I, typename C>(const avnd::field_reflection<I, C>& port) {
copy_string(info.name, C::name());
});
// process_in_events consumes both note events and raw MIDI; the
// ports are MIDI-message-based, so prefer that dialect. (A zero
// preferred_dialect is invalid and makes hosts reject the config.)
info.supported_dialects = CLAP_NOTE_DIALECT_MIDI | CLAP_NOTE_DIALECT_CLAP;
info.preferred_dialect = CLAP_NOTE_DIALECT_MIDI;
return true;
}
else
Expand All @@ -56,6 +61,8 @@ struct event_bus_info
[&]<std::size_t I, typename C>(const avnd::field_reflection<I, C>& port) {
copy_string(info.name, C::name());
});
info.supported_dialects = CLAP_NOTE_DIALECT_MIDI | CLAP_NOTE_DIALECT_CLAP;
info.preferred_dialect = CLAP_NOTE_DIALECT_MIDI;
return true;
}
else
Expand Down Expand Up @@ -286,10 +293,15 @@ struct audio_bus_info<T>
= avnd::poly_array_sample_port<double, C>
? (CLAP_AUDIO_PORT_SUPPORTS_64BITS | CLAP_AUDIO_PORT_PREFERS_64BITS)
: 0;
// Each port reports its own channel count, not the plug-in total
// (a stereo main + stereo sidechain is two 2-channel ports, not
// two 4-channel ones). Dynamic buses default to stereo.
const int channels = avnd::channels_in_bus<C>();
info.channel_count = channels > 0 ? channels : 2;
info.port_type = info.channel_count == 1 ? CLAP_PORT_MONO
: info.channel_count == 2 ? CLAP_PORT_STEREO
: nullptr;
});

info.channel_count = default_input_channel_count();
info.port_type = CLAP_PORT_STEREO;
info.in_place_pair = CLAP_INVALID_ID; // FIXME

return true;
Expand All @@ -311,10 +323,12 @@ struct audio_bus_info<T>
= avnd::poly_array_sample_port<double, C>
? (CLAP_AUDIO_PORT_SUPPORTS_64BITS | CLAP_AUDIO_PORT_PREFERS_64BITS)
: 0;
const int channels = avnd::channels_in_bus<C>();
info.channel_count = channels > 0 ? channels : 2;
info.port_type = info.channel_count == 1 ? CLAP_PORT_MONO
: info.channel_count == 2 ? CLAP_PORT_STEREO
: nullptr;
});

info.channel_count = default_output_channel_count();
info.port_type = CLAP_PORT_STEREO;
info.in_place_pair = CLAP_INVALID_ID; // FIXME

return true;
Expand Down Expand Up @@ -393,8 +407,11 @@ struct audio_bus_info<T>
template <avnd::channel_port_processor T>
struct audio_bus_info<T>
{
using input_refl = avnd::audio_bus_input_introspection<T>;
using output_refl = avnd::audio_bus_output_introspection<T>;
// Channel ports (mono `.channel` members), not bus ports: the bus
// introspection is empty for these, hiding the plug-in's real audio
// shape from hosts (issue #154).
using input_refl = avnd::audio_channel_input_introspection<T>;
using output_refl = avnd::audio_channel_output_introspection<T>;
static constexpr int input_count() noexcept { return input_refl::size; }
static constexpr int output_count() noexcept { return output_refl::size; }
static constexpr int default_input_channel_count() noexcept { return input_count(); }
Expand All @@ -408,7 +425,11 @@ struct audio_bus_info<T>
input_refl::for_nth_mapped(
index,
[&]<std::size_t I, typename C>(const avnd::field_reflection<I, C>& port) {
copy_string(info.name, C::name());
// name() may be a static_string NTTP rather than string_view-like
if constexpr(requires { copy_string(info.name, C::name()); })
copy_string(info.name, C::name());
else
copy_string(info.name, std::string_view{C::name().value});
info.flags
= avnd::mono_array_sample_port<double, C>
? (CLAP_AUDIO_PORT_SUPPORTS_64BITS | CLAP_AUDIO_PORT_PREFERS_64BITS)
Expand All @@ -433,7 +454,10 @@ struct audio_bus_info<T>
output_refl::for_nth_mapped(
index,
[&]<std::size_t I, typename C>(const avnd::field_reflection<I, C>& port) {
copy_string(info.name, C::name());
if constexpr(requires { copy_string(info.name, C::name()); })
copy_string(info.name, C::name());
else
copy_string(info.name, std::string_view{C::name().value});
info.flags
= avnd::mono_array_sample_port<double, C>
? (CLAP_AUDIO_PORT_SUPPORTS_64BITS | CLAP_AUDIO_PORT_PREFERS_64BITS)
Expand Down
Loading
Loading