diff --git a/.github/workflows/compat-test-build.yml b/.github/workflows/compat-test-build.yml index 44dc94c..fa264a4 100644 --- a/.github/workflows/compat-test-build.yml +++ b/.github/workflows/compat-test-build.yml @@ -8,6 +8,9 @@ on: - 'src/obs-plugin/obs_compat_floor.hpp' - 'src/obs-plugin/plugin.cpp' - 'src/host/native_editor.cpp' + - 'src/host/process_block_view.hpp' + - 'src/host/vst3_engine.cpp' + - 'src/host/vst3_engine.hpp' - 'src/scanner/main.cpp' - 'installer/windows/obs-safe-vst3.iss' - 'data/locale/en-US.ini' diff --git a/.github/workflows/r0-1-process-seam.yml b/.github/workflows/r0-1-process-seam.yml new file mode 100644 index 0000000..6d23f0f --- /dev/null +++ b/.github/workflows/r0-1-process-seam.yml @@ -0,0 +1,33 @@ +name: R0-1 Process Seam Characterization + +on: + pull_request: + paths: + - '.github/workflows/r0-1-process-seam.yml' + - 'tests/r0_1/**' + - 'src/host/process_block_view.hpp' + - 'src/host/vst3_engine.cpp' + - 'src/host/vst3_engine.hpp' + - 'src/common/**' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: r0-1-process-seam-${{ github.ref }} + cancel-in-progress: true + +jobs: + real-vst3-engine-process: + runs-on: windows-2022 + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Configure deterministic real-VST3 characterization + run: cmake -S tests/r0_1 -B build/r0-1 -A x64 + - name: Build fixture and engine characterization + run: cmake --build build/r0-1 --config Release --target r0-1-vst3-engine-process-test --parallel + - name: Run AudioSlot and ProcessBlockView characterization + run: ctest --test-dir build/r0-1 -C Release --output-on-failure diff --git a/src/host/process_block_view.hpp b/src/host/process_block_view.hpp new file mode 100644 index 0000000..021435f --- /dev/null +++ b/src/host/process_block_view.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include + +namespace safevst3 { + +// Non-owning protocol-neutral audio view. Buffer ownership and lifetime remain +// with the caller for the duration of process(); no transport layout is +// embedded here. +struct ProcessBlockView { + float* const* input = nullptr; + float* const* output = nullptr; + std::uint32_t channels = 0; + std::uint32_t frames = 0; + std::uint64_t sequence = 0; +}; + +} // namespace safevst3 diff --git a/src/host/vst3_engine.cpp b/src/host/vst3_engine.cpp index 05f5b1c..9e92d2a 100644 --- a/src/host/vst3_engine.cpp +++ b/src/host/vst3_engine.cpp @@ -1130,25 +1130,45 @@ bool Vst3Engine::flush_parameter_changes() noexcept bool Vst3Engine::process(AudioSlot& slot) noexcept { - if (!processor_ || slot.frames == 0 || slot.frames > kMaxFrames || - slot.channels != channels_ || + float* input[kMaxChannels] = {slot.input[0], slot.input[1]}; + float* output[kMaxChannels] = {slot.output[0], slot.output[1]}; + const ProcessBlockView block{ + input, + output, + slot.channels, + slot.frames, + slot.sequence, + }; + return process(block); +} + +bool Vst3Engine::process(const ProcessBlockView& block) noexcept +{ + if (!processor_ || !block.input || !block.output || + block.frames == 0 || block.frames > kMaxFrames || + block.channels != channels_ || (plugin_input_channels_ != 1 && plugin_input_channels_ != 2) || (plugin_output_channels_ != 1 && plugin_output_channels_ != 2)) return false; + for (std::uint32_t ch = 0; ch < block.channels; ++ch) { + if (!block.input[ch] || !block.output[ch]) + return false; + } + Sample32* in[kMaxChannels]{}; Sample32* out[kMaxChannels]{}; if (plugin_input_channels_ == channels_) { for (std::uint32_t ch = 0; ch < channels_; ++ch) - in[ch] = slot.input[ch]; + in[ch] = block.input[ch]; } else if (channels_ == 2 && plugin_input_channels_ == 1) { average_stereo_to_mono( - slot.input[0], slot.input[1], input_adapter_[0].data(), slot.frames); + block.input[0], block.input[1], input_adapter_[0].data(), block.frames); in[0] = input_adapter_[0].data(); } else if (channels_ == 1 && plugin_input_channels_ == 2) { duplicate_mono_to_stereo( - slot.input[0], input_adapter_[0].data(), input_adapter_[1].data(), slot.frames); + block.input[0], input_adapter_[0].data(), input_adapter_[1].data(), block.frames); in[0] = input_adapter_[0].data(); in[1] = input_adapter_[1].data(); } else { @@ -1158,7 +1178,7 @@ bool Vst3Engine::process(AudioSlot& slot) noexcept const bool output_direct = plugin_output_channels_ == channels_; if (output_direct) { for (std::uint32_t ch = 0; ch < channels_; ++ch) - out[ch] = slot.output[ch]; + out[ch] = block.output[ch]; } else { for (std::uint32_t ch = 0; ch < plugin_output_channels_; ++ch) out[ch] = output_adapter_[ch].data(); @@ -1170,7 +1190,7 @@ bool Vst3Engine::process(AudioSlot& slot) noexcept kOutput, main_output_bus_, out, static_cast(plugin_output_channels_))) return false; - process_data_.numSamples = static_cast(slot.frames); + process_data_.numSamples = static_cast(block.frames); process_data_.inputEvents = nullptr; process_data_.outputEvents = nullptr; process_data_.outputParameterChanges = &output_parameter_changes_; @@ -1188,16 +1208,16 @@ bool Vst3Engine::process(AudioSlot& slot) noexcept capture_output_parameter_changes(); if (result != kResultOk) return false; - sample_position_ += slot.frames; + sample_position_ += block.frames; if (!output_direct) { if (channels_ == 2 && plugin_output_channels_ == 1) { duplicate_mono_to_stereo( - output_adapter_[0].data(), slot.output[0], slot.output[1], slot.frames); + output_adapter_[0].data(), block.output[0], block.output[1], block.frames); } else if (channels_ == 1 && plugin_output_channels_ == 2) { average_stereo_to_mono( output_adapter_[0].data(), output_adapter_[1].data(), - slot.output[0], slot.frames); + block.output[0], block.frames); } else { return false; } @@ -1208,4 +1228,4 @@ bool Vst3Engine::process(AudioSlot& slot) noexcept } // namespace safevst3 -#endif +#endif \ No newline at end of file diff --git a/src/host/vst3_engine.hpp b/src/host/vst3_engine.hpp index 8f33cdb..0475f24 100644 --- a/src/host/vst3_engine.hpp +++ b/src/host/vst3_engine.hpp @@ -8,6 +8,7 @@ #include "common/process_context_policy.hpp" #include "common/startup_error.hpp" #include "common/state_snapshot.hpp" +#include "host/process_block_view.hpp" #include "host/vst3_processing_compat.hpp" #include "pluginterfaces/vst/ivstaudioprocessor.h" @@ -73,6 +74,7 @@ class Vst3Engine final : public LatencyRestartTarget, public IoRestartLifecycleT return opened; } void close() noexcept; + bool process(const ProcessBlockView& block) noexcept; bool process(AudioSlot& slot) noexcept; bool capture_state(PluginStateSnapshot& snapshot, std::string& error); diff --git a/src/obs-plugin/obs_compat_floor.hpp b/src/obs-plugin/obs_compat_floor.hpp index 93b7b83..d2076a9 100644 --- a/src/obs-plugin/obs_compat_floor.hpp +++ b/src/obs-plugin/obs_compat_floor.hpp @@ -1,8 +1,28 @@ #pragma once +#include #include // Minimum libobs API used by this plugin: OBS Studio 29.1. -// clean-test3 rebuild marker: also validates stale-copy installer cleanup. #define SAFEVST3_OBS_MIN_API_VER MAKE_SEMANTIC_VERSION(29, 1, 0) #undef LIBOBS_API_VER #define LIBOBS_API_VER SAFEVST3_OBS_MIN_API_VER + +// obs_register_source() always forwards sizeof(struct obs_source_info) from +// the SDK used to build the module. Newer OBS SDKs append fields to that +// structure, so forwarding the full current size makes otherwise-compatible +// modules fail registration on older libobs runtimes. +// +// Safe VST3 only populates source-info fields through `save`; all fields after +// it are intentionally unused. Register exactly that ABI prefix so old libobs +// zero-fills its own trailing fields while current libobs receives every +// callback this module actually uses. Keep this boundary in sync if a future +// source implementation starts using a field after `save`. +#define SAFEVST3_OBS_SOURCE_INFO_COMPAT_SIZE \ + (offsetof(struct obs_source_info, save) + sizeof(((struct obs_source_info*)0)->save)) + +static_assert(SAFEVST3_OBS_SOURCE_INFO_COMPAT_SIZE <= sizeof(struct obs_source_info), + "OBS source-info compatibility prefix exceeds the build SDK structure"); + +#undef obs_register_source +#define obs_register_source(info) \ + obs_register_source_s((info), SAFEVST3_OBS_SOURCE_INFO_COMPAT_SIZE) diff --git a/tests/process_context_source_contract.cmake b/tests/process_context_source_contract.cmake index ed64181..ce2d51c 100644 --- a/tests/process_context_source_contract.cmake +++ b/tests/process_context_source_contract.cmake @@ -28,7 +28,7 @@ endif() string(FIND "${SOURCE}" "const auto context_frame = make_process_context_frame(" BLOCK_FRAME_POS) string(FIND "${SOURCE}" "processor_->process(process_data_)" PROCESS_POS) -string(FIND "${SOURCE}" "sample_position_ += slot.frames" ADVANCE_POS) +string(FIND "${SOURCE}" "sample_position_ += block.frames" ADVANCE_POS) if(BLOCK_FRAME_POS LESS 0 OR PROCESS_POS LESS 0 OR ADVANCE_POS LESS 0) message(FATAL_ERROR "Could not find deterministic audio-block process-context markers") endif() diff --git a/tests/r0_1/CMakeLists.txt b/tests/r0_1/CMakeLists.txt new file mode 100644 index 0000000..0d031a9 --- /dev/null +++ b/tests/r0_1/CMakeLists.txt @@ -0,0 +1,82 @@ +cmake_minimum_required(VERSION 3.25) +project(safevst3-r0-1-characterization VERSION 1.0.0 LANGUAGES C CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +if(NOT WIN32) + message(FATAL_ERROR "R0-1 real-VST3 characterization is Windows-only") +endif() + +# The repository deliberately builds its Steinberg hosting/runtime targets with +# the static MSVC runtime. Keep the standalone fixture/test targets on the same +# runtime so the characterization is about VST3 behavior, not CRT mixing. +set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") +set(SAFEVST3_BUILD_OBS_PLUGIN OFF CACHE BOOL "" FORCE) +set(SAFEVST3_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(SAFEVST3_STATIC_MSVC_RUNTIME ON CACHE BOOL "" FORCE) + +# These are Steinberg-supported build options. The R0-1 fixture exists only to +# exercise the production engine seam inside CI, so do not run the SDK's own +# post-build validator/module-info generation or create a per-user VST3 link. +# Production builds keep the repository defaults; these settings are local to +# this standalone test project before it includes the repository root. +set(SMTG_RUN_VST_VALIDATOR OFF CACHE BOOL "" FORCE) +set(SMTG_CREATE_MODULE_INFO OFF CACHE BOOL "" FORCE) +set(SMTG_CREATE_PLUGIN_LINK OFF CACHE BOOL "" FORCE) + +get_filename_component(SAFEVST3_ROOT "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE) +add_subdirectory("${SAFEVST3_ROOT}" safevst3-root) + +set(_fixture_source "${CMAKE_CURRENT_LIST_DIR}/vst3_process_fixture.cpp") + +smtg_add_vst3plugin(r0-1-fixture-mono "${_fixture_source}") +target_link_libraries(r0-1-fixture-mono PRIVATE sdk) +target_compile_definitions(r0-1-fixture-mono PRIVATE + SAFEVST3_FIXTURE_CHANNELS=1 + WIN32_LEAN_AND_MEAN + NOMINMAX +) + +smtg_add_vst3plugin(r0-1-fixture-stereo "${_fixture_source}") +target_link_libraries(r0-1-fixture-stereo PRIVATE sdk) +target_compile_definitions(r0-1-fixture-stereo PRIVATE + SAFEVST3_FIXTURE_CHANNELS=2 + WIN32_LEAN_AND_MEAN + NOMINMAX +) + +add_executable(r0-1-vst3-engine-process-test + "${CMAKE_CURRENT_LIST_DIR}/vst3_engine_process_characterization.cpp" + "${SAFEVST3_ROOT}/src/host/vst3_engine.cpp" +) +target_include_directories(r0-1-vst3-engine-process-test PRIVATE + "${SAFEVST3_ROOT}/src" +) +target_link_libraries(r0-1-vst3-engine-process-test PRIVATE + safevst3_vst3_hosting_runtime + safevst3_lifecycle_policy + safevst3_state_restore_policy + safevst3_latency_restart_transaction + safevst3_parameter_refresh_transaction + safevst3_io_restart_transaction + safevst3_reload_component_transaction + safevst3_process_context_policy +) +target_compile_definitions(r0-1-vst3-engine-process-test PRIVATE + WIN32_LEAN_AND_MEAN + NOMINMAX +) +add_dependencies(r0-1-vst3-engine-process-test + r0-1-fixture-mono + r0-1-fixture-stereo +) + +enable_testing() +add_test( + NAME r0-1-vst3-engine-process-characterization + COMMAND $ + $ + $ +) diff --git a/tests/r0_1/README.md b/tests/r0_1/README.md new file mode 100644 index 0000000..8966942 --- /dev/null +++ b/tests/r0_1/README.md @@ -0,0 +1,16 @@ +# R0-1 deterministic process characterization + +This directory is the deterministic real-VST3 gate for Rack R0-1. + +It builds two minimal VST3 modules with the pinned Steinberg SDK already used by the repository: + +- a fixed-mono effect; +- a fixed-stereo effect. + +The gate was deliberately established in two stages. First, before any production process mutation, `vst3_engine_process_characterization.cpp` opened the fixture modules through the production `Vst3Engine` and exercised the unchanged Single `process(AudioSlot&)` seam. That pre-mutation proof passed on source head `819e394949184ed64d86901b0cf3e13a3306c6ea` in R0-1 Process Seam Characterization run `33135381993`. + +After that gate passed, R0-1 introduced `ProcessBlockView`. The same harness now keeps all of the original AudioSlot characterization and also drives the protocol-neutral process entry with caller-owned raw buffers, requiring its deterministic output to match an independently opened Single AudioSlot engine. It also verifies invalid neutral buffer tables fail cleanly. + +The fixture encodes `projectTimeSamples` into deterministic audio, so the test locks current block-position behavior in addition to direct mono/stereo processing, mono/stereo adaptation, validation failures, and VST3 process-error propagation. + +This harness is non-shipping. It does not define or change the Single IPC protocol and it contains no Rack production runtime. diff --git a/tests/r0_1/vst3_engine_process_characterization.cpp b/tests/r0_1/vst3_engine_process_characterization.cpp new file mode 100644 index 0000000..4731be4 --- /dev/null +++ b/tests/r0_1/vst3_engine_process_characterization.cpp @@ -0,0 +1,265 @@ +#include "host/process_block_view.hpp" +#include "host/vst3_engine.hpp" + +#include +#include +#include +#include + +#ifdef _WIN32 + +namespace { + +using safevst3::AudioSlot; +using safevst3::ProcessBlockView; +using safevst3::Vst3Engine; + +bool close_enough(float actual, float expected) { + return std::fabs(actual - expected) <= 1.0e-6f; +} + +bool expect(bool condition, const char* message) { + if (!condition) + std::cerr << "FAIL: " << message << '\n'; + return condition; +} + +bool expect_sample(float actual, float expected, const char* message) { + if (close_enough(actual, expected)) + return true; + std::cerr << "FAIL: " << message << " expected=" << expected + << " actual=" << actual << '\n'; + return false; +} + +bool open_engine(Vst3Engine& engine, + const char* module_path, + std::uint32_t channels) { + std::string error; + if (engine.open(module_path, "", 48000, channels, nullptr, error)) + return true; + std::cerr << "FAIL: open(" << module_path << ", channels=" << channels + << "): " << error << '\n'; + return false; +} + +bool characterize_invalid_blocks(const char* mono_path) { + bool ok = true; + + AudioSlot unopened_slot{}; + unopened_slot.frames = 1; + unopened_slot.channels = 1; + Vst3Engine unopened; + ok &= expect(!unopened.process(unopened_slot), + "unopened engine must reject processing"); + + Vst3Engine engine; + if (!open_engine(engine, mono_path, 1)) + return false; + + AudioSlot slot{}; + slot.channels = 1; + slot.frames = 0; + ok &= expect(!engine.process(slot), "zero-frame block must be rejected"); + + slot.frames = safevst3::kMaxFrames + 1; + ok &= expect(!engine.process(slot), "oversized block must be rejected"); + + slot.frames = 1; + slot.channels = 2; + ok &= expect(!engine.process(slot), "channel-count mismatch must be rejected"); + return ok; +} + +bool characterize_direct_mono_and_position(const char* mono_path) { + Vst3Engine engine; + if (!open_engine(engine, mono_path, 1)) + return false; + + bool ok = true; + AudioSlot first{}; + first.channels = 1; + first.frames = 3; + first.input[0][0] = 1.0f; + first.input[0][1] = 2.0f; + first.input[0][2] = 3.0f; + ok &= expect(engine.process(first), "first mono block must process"); + ok &= expect_sample(first.output[0][0], 2.0f, "mono frame 0"); + ok &= expect_sample(first.output[0][1], 4.0f, "mono frame 1"); + ok &= expect_sample(first.output[0][2], 6.0f, "mono frame 2"); + + AudioSlot second{}; + second.channels = 1; + second.frames = 2; + second.input[0][0] = 0.5f; + second.input[0][1] = 1.5f; + ok &= expect(engine.process(second), "second mono block must process"); + // projectTimeSamples starts at the previous successful block length (3). + ok &= expect_sample(second.output[0][0], 4.0f, + "second block must observe projectTimeSamples=3"); + ok &= expect_sample(second.output[0][1], 6.0f, + "second block frame 1 must preserve block position"); + + AudioSlot failed{}; + failed.channels = 1; + failed.frames = 2; + failed.input[0][0] = -1000.0f; + failed.input[0][1] = 0.0f; + ok &= expect(!engine.process(failed), + "fixture process error must propagate as false"); + + AudioSlot after_failure{}; + after_failure.channels = 1; + after_failure.frames = 1; + after_failure.input[0][0] = 1.0f; + ok &= expect(engine.process(after_failure), + "processing must remain callable after process error"); + // Only the 3-frame and 2-frame successful blocks advanced position. + ok &= expect_sample(after_failure.output[0][0], 7.0f, + "failed block must not advance projectTimeSamples"); + return ok; +} + +bool characterize_stereo_host_to_mono_plugin(const char* mono_path) { + Vst3Engine engine; + if (!open_engine(engine, mono_path, 2)) + return false; + + bool ok = true; + AudioSlot slot{}; + slot.channels = 2; + slot.frames = 2; + slot.input[0][0] = 1.0f; + slot.input[0][1] = 3.0f; + slot.input[1][0] = 3.0f; + slot.input[1][1] = 5.0f; + + ok &= expect(engine.process(slot), + "stereo host -> fixed-mono fixture must process"); + // Engine averages host stereo to [2,4], fixture applies x2, then engine + // duplicates the mono result back to both host output channels. + ok &= expect_sample(slot.output[0][0], 4.0f, "stereo->mono L frame 0"); + ok &= expect_sample(slot.output[1][0], 4.0f, "stereo->mono R frame 0"); + ok &= expect_sample(slot.output[0][1], 8.0f, "stereo->mono L frame 1"); + ok &= expect_sample(slot.output[1][1], 8.0f, "stereo->mono R frame 1"); + return ok; +} + +bool characterize_mono_host_to_stereo_plugin(const char* stereo_path) { + Vst3Engine engine; + if (!open_engine(engine, stereo_path, 1)) + return false; + + bool ok = true; + AudioSlot slot{}; + slot.channels = 1; + slot.frames = 2; + slot.input[0][0] = 2.0f; + slot.input[0][1] = 4.0f; + + ok &= expect(engine.process(slot), + "mono host -> fixed-stereo fixture must process"); + // Engine duplicates mono input. Fixture produces x2 on L and x4 on R; + // engine averages those stereo outputs back to mono => x3. + ok &= expect_sample(slot.output[0][0], 6.0f, "mono->stereo frame 0"); + ok &= expect_sample(slot.output[0][1], 12.0f, "mono->stereo frame 1"); + return ok; +} + +bool characterize_direct_stereo(const char* stereo_path) { + Vst3Engine engine; + if (!open_engine(engine, stereo_path, 2)) + return false; + + bool ok = true; + AudioSlot slot{}; + slot.channels = 2; + slot.frames = 2; + slot.input[0][0] = 1.0f; + slot.input[0][1] = 2.0f; + slot.input[1][0] = 3.0f; + slot.input[1][1] = 4.0f; + + ok &= expect(engine.process(slot), "direct stereo block must process"); + ok &= expect_sample(slot.output[0][0], 2.0f, "direct stereo L frame 0"); + ok &= expect_sample(slot.output[0][1], 4.0f, "direct stereo L frame 1"); + ok &= expect_sample(slot.output[1][0], 12.0f, "direct stereo R frame 0"); + ok &= expect_sample(slot.output[1][1], 16.0f, "direct stereo R frame 1"); + return ok; +} + +bool characterize_protocol_neutral_view_matches_single(const char* mono_path) { + Vst3Engine single_engine; + Vst3Engine neutral_engine; + if (!open_engine(single_engine, mono_path, 1) || + !open_engine(neutral_engine, mono_path, 1)) + return false; + + bool ok = true; + AudioSlot slot{}; + slot.sequence = 77; + slot.channels = 1; + slot.frames = 3; + slot.input[0][0] = 0.25f; + slot.input[0][1] = 1.25f; + slot.input[0][2] = 2.25f; + ok &= expect(single_engine.process(slot), "Single adapter comparison block must process"); + + float neutral_input[3] = {0.25f, 1.25f, 2.25f}; + float neutral_output[3] = {}; + float* neutral_inputs[1] = {neutral_input}; + float* neutral_outputs[1] = {neutral_output}; + ProcessBlockView block{ + neutral_inputs, + neutral_outputs, + 1, + 3, + 77, + }; + + ok &= expect(neutral_engine.process(block), + "protocol-neutral ProcessBlockView must process"); + for (std::uint32_t frame = 0; frame < block.frames; ++frame) { + ok &= expect_sample(neutral_output[frame], slot.output[0][frame], + "ProcessBlockView must match Single AudioSlot adapter"); + } + + ProcessBlockView invalid{ + nullptr, + neutral_outputs, + 1, + 1, + 78, + }; + ok &= expect(!neutral_engine.process(invalid), + "protocol-neutral view with null input table must be rejected"); + return ok; +} + +} // namespace + +int main(int argc, char** argv) { + if (argc != 3) { + std::cerr << "usage: r0-1-vst3-engine-process-test \n"; + return 2; + } + + bool ok = true; + ok &= characterize_invalid_blocks(argv[1]); + ok &= characterize_direct_mono_and_position(argv[1]); + ok &= characterize_stereo_host_to_mono_plugin(argv[1]); + ok &= characterize_mono_host_to_stereo_plugin(argv[2]); + ok &= characterize_direct_stereo(argv[2]); + ok &= characterize_protocol_neutral_view_matches_single(argv[1]); + + if (!ok) + return 1; + std::cout << "R0-1 AudioSlot and ProcessBlockView engine seams characterized successfully\n"; + return 0; +} + +#else + +int main() { return 0; } + +#endif diff --git a/tests/r0_1/vst3_process_fixture.cpp b/tests/r0_1/vst3_process_fixture.cpp new file mode 100644 index 0000000..b018586 --- /dev/null +++ b/tests/r0_1/vst3_process_fixture.cpp @@ -0,0 +1,130 @@ +#include "pluginterfaces/vst/ivstprocesscontext.h" +#include "pluginterfaces/vst/vstspeaker.h" +#include "public.sdk/source/main/pluginfactory.h" +#include "public.sdk/source/vst/vstaudioeffect.h" + +#include + +#ifndef SAFEVST3_FIXTURE_CHANNELS +#error "SAFEVST3_FIXTURE_CHANNELS must be defined to 1 or 2" +#endif + +#if SAFEVST3_FIXTURE_CHANNELS != 1 && SAFEVST3_FIXTURE_CHANNELS != 2 +#error "SAFEVST3_FIXTURE_CHANNELS must be 1 or 2" +#endif + +namespace safevst3::test_fixture { + +using namespace Steinberg; +using namespace Steinberg::Vst; + +#if SAFEVST3_FIXTURE_CHANNELS == 1 +static const FUID kProcessorUid(0x3B1069C1, 0x5C474C0E, 0xAA2E925E, 0x6D6B4F01); +constexpr auto kPluginName = "SafeVST3 R0-1 Mono Fixture"; +constexpr SpeakerArrangement kArrangement = SpeakerArr::kMono; +#else +static const FUID kProcessorUid(0x51C2A2D7, 0xE2E047FA, 0xBF0C133E, 0x71A0B202); +constexpr auto kPluginName = "SafeVST3 R0-1 Stereo Fixture"; +constexpr SpeakerArrangement kArrangement = SpeakerArr::kStereo; +#endif + +class ProcessFixture final : public AudioEffect { +public: + static FUnknown* create_instance(void*) { + return static_cast(new ProcessFixture()); + } + + tresult PLUGIN_API initialize(FUnknown* context) override { + const tresult result = AudioEffect::initialize(context); + if (result != kResultOk) + return result; + addAudioInput(STR16("Input"), kArrangement); + addAudioOutput(STR16("Output"), kArrangement); + return kResultOk; + } + + tresult PLUGIN_API setBusArrangements(SpeakerArrangement* inputs, + int32 num_inputs, + SpeakerArrangement* outputs, + int32 num_outputs) override { + if (!inputs || !outputs || num_inputs != 1 || num_outputs != 1) + return kResultFalse; + if (inputs[0] != kArrangement || outputs[0] != kArrangement) + return kResultFalse; + return kResultTrue; + } + + tresult PLUGIN_API setProcessing(TBool) override { + return kResultTrue; + } + + tresult PLUGIN_API canProcessSampleSize(int32 symbolic_sample_size) override { + return symbolic_sample_size == kSample32 ? kResultTrue : kResultFalse; + } + + tresult PLUGIN_API process(ProcessData& data) override { + if (data.numSamples < 0) + return kInvalidArgument; + if (data.numSamples == 0) + return kResultOk; + if (data.numInputs != 1 || data.numOutputs != 1 || !data.inputs || !data.outputs) + return kResultFalse; + + constexpr int32 kChannels = SAFEVST3_FIXTURE_CHANNELS; + if (data.inputs[0].numChannels != kChannels || + data.outputs[0].numChannels != kChannels || + !data.inputs[0].channelBuffers32 || !data.outputs[0].channelBuffers32) + return kResultFalse; + + auto** input = data.inputs[0].channelBuffers32; + auto** output = data.outputs[0].channelBuffers32; + for (int32 channel = 0; channel < kChannels; ++channel) { + if (!input[channel] || !output[channel]) + return kResultFalse; + } + + // A sentinel forces a real IAudioProcessor::process failure. The engine + // characterization uses this to lock the current rule that a failed + // block returns false and does not advance projectTimeSamples. + if (input[0][0] <= -900.0f) + return kResultFalse; + + if (!data.processContext) + return kResultFalse; + const float block_position = + static_cast(data.processContext->projectTimeSamples); + + // Channel-specific gains make stereo->mono and mono->stereo adaptation + // observable without parameters or controller state. Mono uses x2; + // stereo uses x2 on L and x4 on R. + for (int32 channel = 0; channel < kChannels; ++channel) { + const float gain = channel == 0 ? 2.0f : 4.0f; + for (int32 frame = 0; frame < data.numSamples; ++frame) + output[channel][frame] = input[channel][frame] * gain + block_position; + } + data.outputs[0].silenceFlags = 0; + return kResultOk; + } +}; + +} // namespace safevst3::test_fixture + +using namespace Steinberg; +using namespace Steinberg::Vst; +using safevst3::test_fixture::ProcessFixture; +using safevst3::test_fixture::kPluginName; +using safevst3::test_fixture::kProcessorUid; + +BEGIN_FACTORY_DEF("OBS Safe VST3 Tests", "https://github.com/masarray/obs-vst3", "") + +DEF_CLASS2(INLINE_UID_FROM_FUID(kProcessorUid), + PClassInfo::kManyInstances, + kVstAudioEffectClass, + kPluginName, + 0, + "Fx", + "1.0.0", + kVstVersionString, + ProcessFixture::create_instance) + +END_FACTORY